authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-05 22:01:38-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
log2435a9cd58d697332f304833770febec0ff01262
tree950d951fd323faa7cac25cf8a9b84862dccf19e3
parent78d53062d1669c911cd938dd3a32faceafd6a57c

chore: move cursor acp probe artifacts to cursor stack

Co-authored-by: codex <codex@users.noreply.github.com>

3 files changed, 1 insertions(+), 840 deletions(-)

.plans/18-cursor-agent-provider.md deleted-327
......@@ -1,327 +0,0 @@
1# Plan: Cursor ACP (`agent acp`) Provider Integration
2
3## Goal
4
5Add Cursor as a first-class provider in T3 Code using ACP (`agent acp`) over JSON-RPC 2.0 stdio, with robust session lifecycle handling and canonical `ProviderRuntimeEvent` projection.
6
7---
8
9## 1) Exploration Findings (from live ACP probes)
10
11### 1.1 Core invocation and transport
12
131. Binary is `agent` on PATH (`2026.02.27-e7d2ef6` observed).
142. ACP server command is `agent acp`.
153. Transport is newline-delimited JSON-RPC 2.0 over stdio.
164. Messages:
17 - client -> server: requests and responses to server-initiated requests
18 - server -> client: responses, notifications (`session/update`), and server requests (`session/request_permission`)
19
20### 1.2 Handshake and session calls observed
21
221. `initialize` returns:
23 - `protocolVersion`
24 - `agentCapabilities` (`loadSession`, `mcpCapabilities`, `promptCapabilities`)
25 - `authMethods` (includes `cursor_login`)
262. `authenticate { methodId: "cursor_login" }` returns `{}` when logged in.
273. `session/new` returns:
28 - `sessionId`
29 - `modes` (`agent`, `plan`, `ask`)
304. `session/load` works and requires `sessionId`, `cwd`, `mcpServers`.
315. `session/prompt` returns terminal response `{ stopReason: "end_turn" | "cancelled" }`.
32
33Important sequence note:
341. ACP currently allows `session/new` even without explicit `initialize`/`authenticate` when local auth already exists.
352. For adapter consistency and forward compatibility, we should still send `initialize` and `authenticate` during startup.
36
37### 1.3 `session/update` event families observed
38
39Observed `params.update.sessionUpdate` values:
40
411. `available_commands_update`
422. `agent_thought_chunk`
433. `agent_message_chunk`
444. `tool_call`
455. `tool_call_update`
46
47Observed payload behavior:
48
491. `agent_*_chunk` provides `content: { type: "text", text: string }`.
502. `tool_call` may be emitted multiple times for same `toolCallId`:
51 - initial generic form (`title: "Terminal"`, `rawInput: {}`)
52 - enriched form (`title: "\`pwd\`"`, `rawInput: { command: "pwd" }`)
533. `tool_call_update` statuses observed:
54 - `in_progress`
55 - `completed`
564. `tool_call_update` on completion may include `rawOutput`:
57 - terminal: `{ exitCode, stdout, stderr }`
58 - search/find: `{ totalFiles, truncated }`
59
60### 1.4 Permission flow observed
61
621. ACP server sends `session/request_permission` (JSON-RPC request with `id`).
632. Request shape includes:
64 - `params.sessionId`
65 - `params.toolCall`
66 - `params.options` (`allow-once`, `allow-always`, `reject-once`)
673. Client must respond on same `id` with:
68 - `{ outcome: { outcome: "selected", optionId: "<one-option-id>" } }`
694. Reject path still results in tool lifecycle completion events (`tool_call_update status: completed`), typically without `rawOutput`.
70
71### 1.5 Error and capability quirks
72
731. `session/cancel` currently returns:
74 - JSON-RPC error `-32601` Method not found
752. Error shape examples:
76 - unknown auth method: `-32602`
77 - `session/load` missing/invalid params: `-32602`
78 - `session/prompt` unknown session: `-32603` with details
793. Parallel prompts on same session are effectively single-flight:
80 - second prompt can cause first to complete with `stopReason: "cancelled"`.
814. `session/new` accepts a `model` field (no explicit echo in response).
82
83Probe artifacts:
841. `.tmp/acp-probe/*/transcript.ndjson`
852. `.tmp/acp-probe/*/summary.json`
863. `scripts/cursor-acp-probe.mjs`
87
88---
89
90## 2) Integration Constraints for T3
91
921. T3 adapter contract still requires:
93 - `startSession`, `sendTurn`, `interruptTurn`, `respondToRequest`, `readThread`, `rollbackThread`, `stopSession`, `listSessions`, `hasSession`, `stopAll`, `streamEvents`.
942. Orchestration consumes canonical `ProviderRuntimeEvent` only.
953. `ProviderCommandReactor` provider precedence fix remains required (respect explicit provider on turn start).
964. ACP now supports external permission decisions, so Cursor can participate in T3 approval UX via adapter-managed request/response plumbing.
97
98---
99
100## 3) Proposed Architecture
101
102### 3.1 New server components
103
1041. `apps/server/src/provider/Services/CursorAdapter.ts` (service contract/tag + ACP event schemas).
1052. `apps/server/src/provider/Layers/CursorAdapter.ts` (single implementation unit; owns ACP process lifecycle, JSON-RPC routing, runtime projection).
1063. No manager indirection; keep logic in layer implementation.
107
108### 3.2 Session model
109
1101. One long-lived ACP child process per T3 Cursor provider session.
1112. Track:
112 - `providerSessionId` (T3 synthetic ID)
113 - `acpSessionId` (from `session/new` or restored via `session/load`)
114 - `cwd`, `model`, in-flight turn state
115 - pending permission requests by JSON-RPC request id
1163. Resume support:
117 - persist `acpSessionId` in provider resume metadata and call `session/load` on reattach.
118
119### 3.3 Command strategy
120
1211. `startSession`:
122 - spawn `agent acp`
123 - `initialize`
124 - `authenticate(cursor_login)` (best-effort, typed failure handling)
125 - `session/new` or `session/load`
1262. `sendTurn`:
127 - send `session/prompt { sessionId, prompt: [...] }`
128 - consume streaming `session/update` notifications until terminal prompt response
1293. `interruptTurn`:
130 - no native `session/cancel` today; implement fallback:
131 - terminate ACP process + restart + `session/load` for subsequent turns
132 - mark in-flight turn as interrupted/failed in canonical events
1334. `respondToRequest`:
134 - map T3 approval decision -> ACP `optionId`
135 - reply to exact JSON-RPC request id from `session/request_permission`
136
137### 3.4 Effect-first implementation style (required)
138
1391. Keep logic inside `CursorAdapterLive`.
1402. Use Effect primitives:
141 - `Queue` + `Stream.fromQueue` for event fan-out
142 - `Ref` / `Ref.Synchronized` for session/process/request state
143 - scoped fibers for stdout/stderr read loops
1443. Typed JSON decode at boundary:
145 - request/response envelopes
146 - `session/update` union schema
147 - permission-request schema
1484. Keep adapter errors in typed error algebra with explicit mapping at process/protocol boundaries.
149
150---
151
152## 4) Canonical Event Mapping Plan (ACP -> ProviderRuntimeEvent)
153
1541. `session/update: agent_message_chunk`
155 - emit `message.delta` for assistant stream
1562. prompt terminal response (`session/prompt` result `stopReason: end_turn`)
157 - emit `message.completed` + `turn.completed`
1583. `session/update: agent_thought_chunk`
159 - initial mapping: emit thinking activity (or ignore if we keep current canonical surface minimal)
1604. `session/update: tool_call`
161 - first-seen `toolCallId` emits `tool.started`
162 - subsequent `tool_call` for same ID treated as metadata update (no duplicate started event)
1635. `session/update: tool_call_update`
164 - `in_progress`: optional progress activity
165 - `completed`: emit `tool.completed` with summarized `rawOutput` when present
1666. `session/request_permission`
167 - emit `approval.requested` with mapped options
168 - when client decision sent, emit `approval.resolved`
1697. protocol/process error
170 - emit `runtime.error`
171 - fail active turn/session as appropriate
172
173Synthetic IDs:
1741. `turnId`: T3-generated UUID per `sendTurn`.
1752. `itemId`:
176 - assistant stream: `${turnId}:assistant`
177 - tools: `${turnId}:${toolCallId}`
178
179---
180
181## 5) Approval, Resume, and Rollback Behavior
182
183### 5.1 Approvals
184
1851. Cursor ACP permission requests are externally controllable; implement full `respondToRequest` path in v1.
1862. Decision mapping:
187 - allow once -> `allow-once`
188 - allow always -> `allow-always`
189 - reject -> `reject-once`
190
191### 5.2 Resume
192
1931. `session/load` is available and should be first-class for adapter restart/reconnect.
1942. Must send required params: `sessionId`, `cwd`, `mcpServers`.
195
196### 5.3 Rollback / thread read
197
1981. ACP currently has no observed rollback API.
1992. Plan for v1:
200 - `readThread`: adapter-maintained snapshot projection
201 - `rollbackThread`: explicit unsupported error
2023. Product guard:
203 - disable checkpoint revert for Cursor threads in UI until rollback exists.
204
205---
206
207## 6) Required Contract and Runtime Changes
208
209### 6.1 Contracts
210
2111. Add `cursor` to `ProviderKind`.
2122. Add Cursor provider start options (`providerOptions.cursor`), ACP-oriented:
213 - optional `binaryPath`
214 - optional auth/mode knobs if needed later
2153. Extend model options for Cursor list and traits mapping.
2164. Add schemas for ACP-native event union in Cursor adapter service file.
217
218### 6.2 Server orchestration and registry
219
2201. Register `CursorAdapter` in provider registry and server layer wiring.
2212. Update provider-kind persistence decoding for `cursor`.
2223. Fix `ProviderCommandReactor` precedence to honor explicit provider in turn-start command.
223
224### 6.3 Web
225
2261. Cursor in provider picker and model picker (already partially done).
2272. Trait controls map to concrete Cursor model identifiers.
2283. Surface unsupported rollback behavior in UX.
229
230---
231
232## 7) Implementation Phases
233
234### Phase A: ACP process and protocol skeleton
235
2361. Implement ACP process lifecycle in `CursorAdapterLive`.
2372. Implement JSON-RPC request/response multiplexer.
2383. Implement `initialize`/`authenticate`/`session/new|load` flow.
2394. Wire `streamEvents` from ACP notifications.
240
241### Phase B: Runtime projection and approvals
242
2431. Map `session/update` variants to canonical runtime events.
2442. Implement permission-request bridging to `respondToRequest`.
2453. Implement dedupe for repeated `tool_call` on same `toolCallId`.
246
247### Phase C: Turn control and interruption
248
2491. Implement single in-flight prompt protection per session.
2502. Implement interruption fallback (process restart + reload) because `session/cancel` unavailable.
2513. Ensure clean state recovery on ACP process crash.
252
253### Phase D: Orchestration + UX polish
254
2551. Provider routing precedence fix.
2562. Cursor-specific UX notes for unsupported rollback.
2573. End-to-end smoke and event log validation.
258
259---
260
261## 8) Test Plan
262
263Follow project rule: backend external-service integrations tested via layered fakes, not by mocking core business logic.
264
265### 8.1 Unit tests (`CursorAdapter`)
266
2671. JSON-RPC envelope parsing:
268 - response matching by id
269 - server request handling (`session/request_permission`)
270 - notification decode (`session/update`)
2712. Event projection:
272 - `agent_message_chunk` / `agent_thought_chunk`
273 - `tool_call` + `tool_call_update` dedupe/lifecycle
274 - permission request -> approval events
2753. Error mapping:
276 - unknown session
277 - method-not-found (`session/cancel`)
278 - invalid params
279
280### 8.2 Provider service/routing tests
281
2821. Registry resolves `cursor`.
2832. Session directory persistence reads/writes `cursor`.
2843. ProviderService fan-out ordering with Cursor ACP events.
285
286### 8.3 Orchestration tests
287
2881. `thread.turn.start` with `provider: cursor` routes to Cursor adapter.
2892. approval response command maps to ACP permission response.
2903. checkpoint revert on Cursor thread returns controlled unsupported failure.
291
292### 8.4 Optional live smoke
293
2941. Env-gated ACP smoke:
295 - start session
296 - run prompt
297 - observe deltas + completion
298 - exercise permission request path with one tool call
299
300---
301
302## 9) Operational Notes
303
3041. Keep one in-flight turn per ACP session.
3052. Keep per-session ACP process logs/NDJSON artifacts for debugging.
3063. Treat `session/cancel` as unsupported until Cursor ships it; avoid relying on it.
3074. Preserve resume metadata (`acpSessionId`) for crash recovery.
308
309---
310
311## 10) Open Questions
312
3131. Should we call `authenticate` always, or only after auth-required errors?
3142. Should model selection be passed at `session/new` only, or can/should we support model switching mid-session if ACP adds API?
3153. For interruption UX, do we expose “hard interrupt” semantics (process restart) explicitly?
316
317---
318
319## 11) Delivery Checklist
320
3211. Plan/documentation switched from headless `agent -p` to ACP `agent acp`.
3222. Contracts updated (`ProviderKind`, Cursor options, model/trait mapping).
3233. Cursor ACP adapter layer implemented and registered.
3244. Provider precedence fixed in orchestration router.
3255. Approval response path wired through ACP permission requests.
3266. Tests added for protocol decode, projection, approval flow, and routing.
3277. Lint + tests green.
package.json+1-2
......@@ -35,7 +35,6 @@
3535 "test": "turbo run test",
3636 "test:desktop-smoke": "turbo run smoke-test --filter=@t3tools/desktop",
3737 "fmt": "oxfmt",
38 "probe:cursor-acp": "node scripts/cursor-acp-probe.mjs",
3938 "build:contracts": "turbo run build --filter=@t3tools/contracts",
4039 "dist:desktop:artifact": "node scripts/build-desktop-artifact.ts",
4140 "dist:desktop:dmg": "node scripts/build-desktop-artifact.ts --platform mac --target dmg",
......@@ -63,4 +62,4 @@
6362 "apps/web/public"
6463 ]
6564 }
66}
\ No newline at end of file
65}
scripts/cursor-acp-probe.mjs deleted-511
......@@ -1,511 +0,0 @@
1import { spawn } from "node:child_process";
2import fs from "node:fs/promises";
3import path from "node:path";
4import readline from "node:readline";
5
6const DEFAULT_PROMPT_TIMEOUT_MS = 120_000;
7const CANCEL_AFTER_MS = 1_500;
8
9function parseArgv(argv) {
10 const parsed = {
11 outputDir: "",
12 workspace: process.cwd(),
13 model: "",
14 permissionOption: "allow-once",
15 };
16
17 for (let i = 0; i < argv.length; i += 1) {
18 const token = argv[i];
19 const next = argv[i + 1];
20 if ((token === "--output-dir" || token === "-o") && next) {
21 parsed.outputDir = next;
22 i += 1;
23 continue;
24 }
25 if ((token === "--workspace" || token === "-w") && next) {
26 parsed.workspace = path.resolve(next);
27 i += 1;
28 continue;
29 }
30 if ((token === "--model" || token === "-m") && next) {
31 parsed.model = next;
32 i += 1;
33 continue;
34 }
35 if (token === "--permission-option" && next) {
36 parsed.permissionOption = next;
37 i += 1;
38 }
39 }
40
41 return parsed;
42}
43
44function nowIso() {
45 return new Date().toISOString();
46}
47
48function safeJsonParse(line) {
49 try {
50 return { ok: true, value: JSON.parse(line) };
51 } catch (error) {
52 return { ok: false, error };
53 }
54}
55
56class AcpProbeClient {
57 #child;
58 #stdoutRl;
59 #stderrRl;
60 #nextId = 1;
61 #closed = false;
62 #pending = new Map();
63 #onMessage;
64 #onServerRequest;
65
66 constructor({ onMessage, onServerRequest }) {
67 this.#onMessage = onMessage;
68 this.#onServerRequest = onServerRequest;
69
70 this.#child = spawn("agent", ["acp"], {
71 stdio: ["pipe", "pipe", "pipe"],
72 env: {
73 ...process.env,
74 NO_COLOR: "1",
75 },
76 });
77
78 this.#stdoutRl = readline.createInterface({ input: this.#child.stdout });
79 this.#stderrRl = readline.createInterface({ input: this.#child.stderr });
80
81 this.#stdoutRl.on("line", (line) => this.#handleStdoutLine(line));
82 this.#stderrRl.on("line", (line) => {
83 this.#onMessage({
84 ts: nowIso(),
85 channel: "stderr",
86 line,
87 });
88 });
89
90 this.#child.once("exit", (code, signal) => {
91 this.#closed = true;
92 const reason = `ACP process exited (code=${String(code)}, signal=${String(signal)})`;
93 for (const [id, pending] of this.#pending.entries()) {
94 this.#pending.delete(id);
95 pending.reject(new Error(reason));
96 }
97 this.#onMessage({
98 ts: nowIso(),
99 channel: "lifecycle",
100 event: "exit",
101 code,
102 signal,
103 });
104 });
105 }
106
107 async close() {
108 if (this.#closed) return;
109 try {
110 this.#child.stdin.end();
111 } catch {
112 // ignored
113 }
114 this.#child.kill("SIGTERM");
115 await new Promise((resolve) => setTimeout(resolve, 100));
116 if (!this.#closed) {
117 this.#child.kill("SIGKILL");
118 }
119 }
120
121 async send(method, params, { timeoutMs = DEFAULT_PROMPT_TIMEOUT_MS } = {}) {
122 if (this.#closed) {
123 throw new Error("Cannot send: ACP process is already closed.");
124 }
125 const id = this.#nextId;
126 this.#nextId += 1;
127
128 const message = {
129 jsonrpc: "2.0",
130 id,
131 method,
132 params,
133 };
134
135 this.#onMessage({
136 ts: nowIso(),
137 channel: "client->server",
138 message,
139 });
140 this.#child.stdin.write(`${JSON.stringify(message)}\n`);
141
142 return new Promise((resolve, reject) => {
143 const timeout = setTimeout(() => {
144 this.#pending.delete(id);
145 reject(new Error(`Timed out waiting for response to '${method}' (id=${id}).`));
146 }, timeoutMs);
147
148 this.#pending.set(id, {
149 resolve: (value) => {
150 clearTimeout(timeout);
151 resolve(value);
152 },
153 reject: (error) => {
154 clearTimeout(timeout);
155 reject(error);
156 },
157 });
158 });
159 }
160
161 #respond(id, result) {
162 if (this.#closed) return;
163 const message = {
164 jsonrpc: "2.0",
165 id,
166 result,
167 };
168 this.#onMessage({
169 ts: nowIso(),
170 channel: "client->server",
171 message,
172 });
173 this.#child.stdin.write(`${JSON.stringify(message)}\n`);
174 }
175
176 #handleStdoutLine(line) {
177 const parsed = safeJsonParse(line);
178 if (!parsed.ok) {
179 this.#onMessage({
180 ts: nowIso(),
181 channel: "stdout-non-json",
182 line,
183 });
184 return;
185 }
186
187 const message = parsed.value;
188 this.#onMessage({
189 ts: nowIso(),
190 channel: "server->client",
191 message,
192 });
193
194 if (
195 Object.prototype.hasOwnProperty.call(message, "id") &&
196 (Object.prototype.hasOwnProperty.call(message, "result") ||
197 Object.prototype.hasOwnProperty.call(message, "error")) &&
198 !Object.prototype.hasOwnProperty.call(message, "method")
199 ) {
200 const pending = this.#pending.get(message.id);
201 if (!pending) return;
202 this.#pending.delete(message.id);
203 if (Object.prototype.hasOwnProperty.call(message, "error")) {
204 pending.reject(message.error);
205 return;
206 }
207 pending.resolve(message.result);
208 return;
209 }
210
211 if (
212 Object.prototype.hasOwnProperty.call(message, "id") &&
213 Object.prototype.hasOwnProperty.call(message, "method")
214 ) {
215 this.#onServerRequest?.(message, (result) => this.#respond(message.id, result));
216 }
217 }
218}
219
220function summarizeTranscript(entries) {
221 const summary = {
222 counts: {
223 notificationsByMethod: {},
224 sessionUpdateByType: {},
225 serverRequestsByMethod: {},
226 permissionDecisions: {},
227 },
228 samples: {
229 initializeResult: null,
230 authenticateResult: null,
231 sessionNewResult: null,
232 sessionPromptResultByScenario: {},
233 sessionUpdateByType: {},
234 serverRequestByMethod: {},
235 },
236 stderr: [],
237 };
238
239 for (const entry of entries) {
240 if (entry.channel === "stderr") {
241 summary.stderr.push(entry.line);
242 continue;
243 }
244 if (entry.channel !== "server->client") continue;
245
246 const message = entry.message;
247 if (!message || typeof message !== "object") continue;
248
249 if (typeof message.method === "string" && !Object.prototype.hasOwnProperty.call(message, "id")) {
250 summary.counts.notificationsByMethod[message.method] =
251 (summary.counts.notificationsByMethod[message.method] ?? 0) + 1;
252 if (message.method === "session/update") {
253 const updateType = message.params?.update?.sessionUpdate;
254 if (typeof updateType === "string") {
255 summary.counts.sessionUpdateByType[updateType] =
256 (summary.counts.sessionUpdateByType[updateType] ?? 0) + 1;
257 if (!summary.samples.sessionUpdateByType[updateType]) {
258 summary.samples.sessionUpdateByType[updateType] = message;
259 }
260 }
261 }
262 continue;
263 }
264
265 if (typeof message.method === "string" && Object.prototype.hasOwnProperty.call(message, "id")) {
266 summary.counts.serverRequestsByMethod[message.method] =
267 (summary.counts.serverRequestsByMethod[message.method] ?? 0) + 1;
268 if (!summary.samples.serverRequestByMethod[message.method]) {
269 summary.samples.serverRequestByMethod[message.method] = message;
270 }
271 continue;
272 }
273 }
274
275 for (const entry of entries) {
276 if (entry.channel !== "scenario-result") continue;
277 if (entry.scenario === "initialize" && !summary.samples.initializeResult) {
278 summary.samples.initializeResult = entry.result;
279 continue;
280 }
281 if (entry.scenario === "authenticate" && !summary.samples.authenticateResult) {
282 summary.samples.authenticateResult = entry.result;
283 continue;
284 }
285 if (entry.scenario === "session/new" && !summary.samples.sessionNewResult) {
286 summary.samples.sessionNewResult = entry.result;
287 continue;
288 }
289 if (entry.scenarioName) {
290 summary.samples.sessionPromptResultByScenario[entry.scenarioName] = entry.result;
291 }
292 }
293
294 for (const entry of entries) {
295 if (entry.channel !== "permission-decision") continue;
296 const optionId = entry.optionId;
297 summary.counts.permissionDecisions[optionId] =
298 (summary.counts.permissionDecisions[optionId] ?? 0) + 1;
299 }
300
301 return summary;
302}
303
304async function run() {
305 const args = parseArgv(process.argv.slice(2));
306 const allowedPermissionOptions = new Set(["allow-once", "allow-always", "reject-once"]);
307 if (!allowedPermissionOptions.has(args.permissionOption)) {
308 throw new Error(
309 `Invalid --permission-option '${args.permissionOption}'. Expected one of: ${Array.from(allowedPermissionOptions).join(", ")}`,
310 );
311 }
312 const stamp = nowIso().replaceAll(":", "-");
313 const outputDir = args.outputDir
314 ? path.resolve(args.outputDir)
315 : path.join(process.cwd(), ".tmp", "acp-probe", stamp);
316 await fs.mkdir(outputDir, { recursive: true });
317
318 const transcript = [];
319 const pushEntry = (entry) => transcript.push(entry);
320
321 let activeScenarioName = null;
322
323 const client = new AcpProbeClient({
324 onMessage: (entry) => {
325 if (
326 activeScenarioName &&
327 entry.channel === "server->client" &&
328 entry.message?.method === "session/update"
329 ) {
330 transcript.push({ ...entry, scenarioName: activeScenarioName });
331 return;
332 }
333 transcript.push(entry);
334 },
335 onServerRequest: (message, respond) => {
336 if (message.method === "session/request_permission") {
337 const defaultChoice = args.permissionOption;
338 pushEntry({
339 ts: nowIso(),
340 channel: "permission-decision",
341 requestId: message.id,
342 optionId: defaultChoice,
343 params: message.params,
344 });
345 respond({
346 outcome: {
347 outcome: "selected",
348 optionId: defaultChoice,
349 },
350 });
351 return;
352 }
353
354 respond({
355 outcome: {
356 outcome: "selected",
357 optionId: "deny",
358 },
359 });
360 pushEntry({
361 ts: nowIso(),
362 channel: "server-request-unhandled",
363 method: message.method,
364 requestId: message.id,
365 params: message.params,
366 });
367 },
368 });
369
370 try {
371 const initializeResult = await client.send("initialize", {
372 protocolVersion: 1,
373 clientCapabilities: {
374 fs: { readTextFile: false, writeTextFile: false },
375 terminal: false,
376 },
377 clientInfo: {
378 name: "t3-cursor-acp-probe",
379 version: "0.1.0",
380 },
381 });
382 pushEntry({
383 ts: nowIso(),
384 channel: "scenario-result",
385 scenario: "initialize",
386 result: initializeResult,
387 });
388
389 const authenticateResult = await client.send("authenticate", {
390 methodId: "cursor_login",
391 });
392 pushEntry({
393 ts: nowIso(),
394 channel: "scenario-result",
395 scenario: "authenticate",
396 result: authenticateResult,
397 });
398
399 const sessionParams = {
400 cwd: args.workspace,
401 mcpServers: [],
402 ...(args.model ? { model: args.model } : {}),
403 };
404 const sessionResult = await client.send("session/new", sessionParams);
405 pushEntry({
406 ts: nowIso(),
407 channel: "scenario-result",
408 scenario: "session/new",
409 result: sessionResult,
410 });
411
412 const sessionId = sessionResult?.sessionId;
413 if (typeof sessionId !== "string" || sessionId.length === 0) {
414 throw new Error(`Missing sessionId from session/new response: ${JSON.stringify(sessionResult)}`);
415 }
416
417 const scenarios = [
418 {
419 name: "hello",
420 prompt: "Say hello in one sentence.",
421 },
422 {
423 name: "tooling",
424 prompt:
425 "Use tools to run `pwd` and then `ls -1 | head -n 8`, and summarize what you found in one paragraph.",
426 },
427 {
428 name: "cancel",
429 prompt:
430 "Think for a while and draft a long detailed migration plan with at least 20 bullet points before answering.",
431 cancelAfterMs: CANCEL_AFTER_MS,
432 },
433 ];
434
435 for (const scenario of scenarios) {
436 activeScenarioName = scenario.name;
437 const promptParams = {
438 sessionId,
439 prompt: [{ type: "text", text: scenario.prompt }],
440 };
441
442 const promptPromise = client.send("session/prompt", promptParams, {
443 timeoutMs: DEFAULT_PROMPT_TIMEOUT_MS,
444 });
445
446 if (scenario.cancelAfterMs) {
447 setTimeout(() => {
448 client
449 .send("session/cancel", { sessionId }, { timeoutMs: 15_000 })
450 .then((cancelResult) => {
451 pushEntry({
452 ts: nowIso(),
453 channel: "scenario-result",
454 scenario: "session/cancel",
455 scenarioName: scenario.name,
456 result: cancelResult,
457 });
458 })
459 .catch((error) => {
460 pushEntry({
461 ts: nowIso(),
462 channel: "scenario-error",
463 scenario: "session/cancel",
464 scenarioName: scenario.name,
465 error:
466 error instanceof Error
467 ? error.message
468 : typeof error === "string"
469 ? error
470 : JSON.stringify(error),
471 });
472 });
473 }, scenario.cancelAfterMs);
474 }
475
476 const promptResult = await promptPromise;
477 pushEntry({
478 ts: nowIso(),
479 channel: "scenario-result",
480 scenario: "session/prompt",
481 scenarioName: scenario.name,
482 result: promptResult,
483 });
484 activeScenarioName = null;
485 await new Promise((resolve) => setTimeout(resolve, 500));
486 }
487 } finally {
488 await client.close();
489 }
490
491 const summary = summarizeTranscript(transcript);
492 const transcriptPath = path.join(outputDir, "transcript.ndjson");
493 const summaryPath = path.join(outputDir, "summary.json");
494
495 await fs.writeFile(
496 transcriptPath,
497 `${transcript.map((entry) => JSON.stringify(entry)).join("\n")}\n`,
498 "utf8",
499 );
500 await fs.writeFile(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
501
502 process.stdout.write(`ACP probe complete.\n`);
503 process.stdout.write(` outputDir: ${outputDir}\n`);
504 process.stdout.write(` transcript: ${transcriptPath}\n`);
505 process.stdout.write(` summary: ${summaryPath}\n`);
506}
507
508run().catch((error) => {
509 process.stderr.write(`ACP probe failed: ${String(error)}\n`);
510 process.exitCode = 1;
511});