@gajae-code/ai 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/types/auth-storage.d.ts +2 -2
  3. package/dist/types/model-cache.d.ts +2 -0
  4. package/dist/types/provider-models/special.d.ts +3 -1
  5. package/dist/types/providers/anthropic.d.ts +1 -0
  6. package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
  7. package/dist/types/providers/cursor/gen/agent_pb.d.ts +3854 -107
  8. package/dist/types/providers/cursor-pi-args.d.ts +119 -0
  9. package/dist/types/providers/cursor.d.ts +8 -1
  10. package/dist/types/providers/openai-codex-responses.d.ts +2 -0
  11. package/dist/types/providers/openai-responses-shared.d.ts +1 -1
  12. package/dist/types/types.d.ts +41 -1
  13. package/dist/types/utils/block-symbols.d.ts +6 -0
  14. package/dist/types/utils/discovery/openai-compatible.d.ts +2 -0
  15. package/dist/types/utils/idle-iterator.d.ts +5 -2
  16. package/dist/types/utils/oauth/kimi.d.ts +3 -9
  17. package/dist/types/utils/oauth/openrouter.d.ts +1 -0
  18. package/dist/types/utils/oauth/types.d.ts +1 -1
  19. package/package.json +4 -4
  20. package/src/auth-broker/remote-store.ts +13 -2
  21. package/src/auth-storage.ts +10 -11
  22. package/src/model-cache.ts +78 -0
  23. package/src/model-manager.ts +194 -25
  24. package/src/provider-models/special.ts +67 -4
  25. package/src/providers/anthropic.ts +115 -40
  26. package/src/providers/aws-credential-config.ts +2 -3
  27. package/src/providers/aws-credentials.ts +2 -3
  28. package/src/providers/azure-openai-responses.ts +18 -2
  29. package/src/providers/cursor/exec-modern.ts +497 -0
  30. package/src/providers/cursor/gen/agent_pb.ts +4687 -181
  31. package/src/providers/cursor/proto/agent.proto +1007 -0
  32. package/src/providers/cursor-pi-args.ts +187 -0
  33. package/src/providers/cursor.ts +382 -47
  34. package/src/providers/google-auth.ts +2 -3
  35. package/src/providers/openai-codex-responses.ts +358 -73
  36. package/src/providers/openai-completions.ts +2 -2
  37. package/src/providers/openai-responses-shared.ts +55 -6
  38. package/src/providers/openai-responses.ts +27 -4
  39. package/src/stream.ts +8 -3
  40. package/src/types.ts +55 -0
  41. package/src/utils/block-symbols.ts +11 -0
  42. package/src/utils/discovery/openai-compatible.ts +21 -6
  43. package/src/utils/idle-iterator.ts +22 -4
  44. package/src/utils/oauth/index.ts +6 -0
  45. package/src/utils/oauth/kimi.ts +14 -8
  46. package/src/utils/oauth/kiro.ts +2 -2
  47. package/src/utils/oauth/openrouter.ts +16 -0
  48. package/src/utils/oauth/types.ts +1 -0
@@ -26,12 +26,39 @@ import type {
26
26
  ToolResultMessage,
27
27
  } from "../types";
28
28
  import { normalizeSystemPrompts } from "../utils";
29
+ import { kCursorExecResolved } from "../utils/block-symbols";
29
30
  import { AssistantMessageEventStream } from "../utils/event-stream";
30
31
  import { findUnnecessaryUnicodeEscape, parseStreamingJson } from "../utils/json-parse";
31
32
  import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
32
33
  import { flattenToolRootCombinators, toolWireSchema } from "../utils/schema";
33
34
  import { CURSOR_COMPOSER_EDIT_DISCIPLINE_PROMPT, isComposerHarnessModel } from "./composer-discipline";
34
35
  import { CURSOR_CLIENT_VERSION } from "./cursor/client-version";
36
+ import {
37
+ buildMcpStateResult,
38
+ buildNeutralHookResult,
39
+ buildPiBashError,
40
+ buildPiBashResult,
41
+ buildPiEditError,
42
+ buildPiEditRejected,
43
+ buildPiEditResult,
44
+ buildPiFindError,
45
+ buildPiFindResult,
46
+ buildPiGrepError,
47
+ buildPiGrepResult,
48
+ buildPiLsError,
49
+ buildPiLsResult,
50
+ buildPiReadError,
51
+ buildPiReadResult,
52
+ buildPiWriteError,
53
+ buildPiWriteRejected,
54
+ buildPiWriteResult,
55
+ piEscapeRegexLiteral,
56
+ piJoinPath,
57
+ piLimit,
58
+ piLsPath,
59
+ piReadDisplayPath,
60
+ piTimeout,
61
+ } from "./cursor/exec-modern";
35
62
  import type { McpToolDefinition } from "./cursor/gen/agent_pb";
36
63
  import {
37
64
  AgentClientMessageSchema,
@@ -60,6 +87,7 @@ import {
60
87
  type ExecClientMessage,
61
88
  ExecClientMessageSchema,
62
89
  ExecClientStreamCloseSchema,
90
+ ExecClientThrowSchema,
63
91
  type ExecServerMessage,
64
92
  FetchErrorSchema,
65
93
  FetchResultSchema,
@@ -87,6 +115,7 @@ import {
87
115
  LsRejectedSchema,
88
116
  LsResultSchema,
89
117
  LsSuccessSchema,
118
+ McpAllowlistPrecheckResultSchema,
90
119
  McpErrorSchema,
91
120
  McpImageContentSchema,
92
121
  McpResultSchema,
@@ -109,6 +138,7 @@ import {
109
138
  SelectedContextSchema,
110
139
  SelectedImageSchema,
111
140
  SetBlobResultSchema,
141
+ ShellAllowlistPrecheckResultSchema,
112
142
  type ShellArgs,
113
143
  ShellFailureSchema,
114
144
  ShellRejectedSchema,
@@ -123,6 +153,7 @@ import {
123
153
  ShellSuccessSchema,
124
154
  UserMessageActionSchema,
125
155
  UserMessageSchema,
156
+ WebFetchAllowlistPrecheckResultSchema,
126
157
  WriteErrorSchema,
127
158
  WriteRejectedSchema,
128
159
  WriteResultSchema,
@@ -443,6 +474,10 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
443
474
  };
444
475
 
445
476
  let resolveH2: (() => void) | undefined;
477
+ let rejectH2: ((error: unknown) => void) | undefined;
478
+ const messageQueue = createCursorMessageQueueForTest(error => {
479
+ log("error", "handleServerMessage", { error: String(error) });
480
+ });
446
481
 
447
482
  h2Request.on("data", (chunk: Buffer) => {
448
483
  pendingBuffer = Buffer.concat([pendingBuffer, chunk]);
@@ -469,28 +504,35 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
469
504
  const isTurnEnded =
470
505
  serverMessage.message.case === "interactionUpdate" &&
471
506
  serverMessage.message.value.message?.case === "turnEnded";
472
- void handleServerMessage(
473
- serverMessage,
474
- output,
475
- stream,
476
- state,
477
- blobStore,
478
- h2Request!,
479
- options?.execHandlers,
480
- options?.onToolResult,
481
- usageState,
482
- requestContextTools,
483
- onConversationCheckpoint,
484
- ).catch(error => {
485
- log("error", "handleServerMessage", { error: String(error) });
486
- });
507
+ // Serialize handlers: exec messages can be asynchronous, and resolving the
508
+ // request on turnEnded before prior handlers finish loses their responses.
509
+ messageQueue.enqueue(() =>
510
+ handleServerMessage(
511
+ serverMessage,
512
+ output,
513
+ stream,
514
+ state,
515
+ blobStore,
516
+ h2Request!,
517
+ options?.execHandlers,
518
+ options?.onToolResult,
519
+ usageState,
520
+ requestContextTools,
521
+ onConversationCheckpoint,
522
+ ),
523
+ );
487
524
 
488
525
  // Resolve only on explicit turnEnded. stopReason defaults to "stop"
489
526
  // and is not a reliable signal for stream completion.
490
- if (isTurnEnded && resolveH2) {
491
- const r = resolveH2;
492
- resolveH2 = undefined;
493
- r();
527
+ if (isTurnEnded) {
528
+ messageQueue.drain().then(() => {
529
+ if (resolveH2) {
530
+ const r = resolveH2;
531
+ resolveH2 = undefined;
532
+ rejectH2 = undefined;
533
+ r();
534
+ }
535
+ });
494
536
  }
495
537
  } catch (e) {
496
538
  log("error", "parseServerMessage", { error: String(e) });
@@ -515,6 +557,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
515
557
 
516
558
  await new Promise<void>((resolve, reject) => {
517
559
  resolveH2 = resolve;
560
+ rejectH2 = reject;
518
561
 
519
562
  h2Request!.on("trailers", trailers => {
520
563
  const status = trailers["grpc-status"];
@@ -525,15 +568,17 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
525
568
  });
526
569
 
527
570
  h2Request!.on("end", () => {
528
- resolveH2 = undefined;
529
- if (endStreamError) {
530
- reject(endStreamError);
531
- return;
532
- }
533
- resolve();
571
+ messageQueue.drain().then(() => {
572
+ resolveH2 = undefined;
573
+ rejectH2 = undefined;
574
+ if (endStreamError) reject(endStreamError);
575
+ else resolve();
576
+ });
534
577
  });
535
578
 
536
- h2Request!.on("error", reject);
579
+ h2Request!.on("error", error => {
580
+ if (rejectH2) rejectH2(error);
581
+ });
537
582
 
538
583
  if (options?.signal) {
539
584
  options.signal.addEventListener("abort", () => {
@@ -608,7 +653,12 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
608
653
  return stream;
609
654
  };
610
655
 
611
- type ToolCallState = ToolCall & { index: number; partialJson?: string; kind: "mcp" | "todo_write" | "native" };
656
+ type ToolCallState = ToolCall & {
657
+ index: number;
658
+ partialJson?: string;
659
+ kind: "mcp" | "todo_write" | "native" | "cursor-exec";
660
+ [kCursorExecResolved]?: true;
661
+ };
612
662
 
613
663
  interface BlockState {
614
664
  currentTextBlock: (TextContent & { index: number }) | null;
@@ -653,6 +703,8 @@ async function handleServerMessage(
653
703
  execHandlers,
654
704
  onToolResult,
655
705
  requestContextTools,
706
+ output,
707
+ stream,
656
708
  );
657
709
  } else if (msgCase === "conversationCheckpointUpdate") {
658
710
  handleConversationCheckpointUpdate(msg.message.value, output, usageState, onConversationCheckpoint);
@@ -1009,6 +1061,8 @@ async function handleExecServerMessage(
1009
1061
  execHandlers: CursorExecHandlers | undefined,
1010
1062
  onToolResult: CursorToolResultHandler | undefined,
1011
1063
  requestContextTools: McpToolDefinition[],
1064
+ output: AssistantMessage,
1065
+ stream: AssistantMessageEventStream,
1012
1066
  ): Promise<void> {
1013
1067
  const execCase = execMsg.message.case;
1014
1068
  log("exec", "dispatch", { execCase, execId: execMsg.execId, hasHandlers: !!execHandlers });
@@ -1224,35 +1278,212 @@ async function handleExecServerMessage(
1224
1278
  sendExecClientMessage(h2Request, execMsg, "computerUseResult", execResult);
1225
1279
  return;
1226
1280
  }
1227
- default: {
1228
- log("warn", "unhandledExecMessage", { execCase });
1229
- // Send a bare ExecClientMessage (id + execId only, no typed result) so the
1230
- // server gets an acknowledgement and doesn't hang waiting forever.
1231
- const ack = create(ExecClientMessageSchema, {
1232
- id: execMsg.id,
1233
- execId: execMsg.execId,
1281
+ case "piReadArgs": {
1282
+ const args = execMsg.message.value;
1283
+ const toolCallId = crypto.randomUUID();
1284
+ synthesizeCursorExecToolCall(output, stream, toolCallId, "read", {
1285
+ path: piReadDisplayPath(args.path, args.offset, args.limit),
1286
+ });
1287
+ const call = { args, toolCallId };
1288
+ const { execResult } = await resolveExecHandler(
1289
+ call,
1290
+ execHandlers?.piRead?.bind(execHandlers),
1291
+ onToolResult,
1292
+ buildPiReadResult,
1293
+ buildPiReadError,
1294
+ buildPiReadError,
1295
+ );
1296
+ sendExecClientMessage(h2Request, execMsg, "piReadResult", execResult);
1297
+ return;
1298
+ }
1299
+ case "piBashArgs": {
1300
+ const args = execMsg.message.value;
1301
+ const toolCallId = crypto.randomUUID();
1302
+ synthesizeCursorExecToolCall(output, stream, toolCallId, "bash", {
1303
+ command: args.command,
1304
+ timeout: piTimeout(args.timeout),
1234
1305
  });
1235
- const clientMessage = create(AgentClientMessageSchema, {
1236
- message: { case: "execClientMessage", value: ack },
1306
+ const call = { args, toolCallId };
1307
+ const { execResult } = await resolveExecHandler(
1308
+ call,
1309
+ execHandlers?.piBash?.bind(execHandlers),
1310
+ onToolResult,
1311
+ buildPiBashResult,
1312
+ buildPiBashError,
1313
+ buildPiBashError,
1314
+ );
1315
+ sendExecClientMessage(h2Request, execMsg, "piBashResult", execResult);
1316
+ return;
1317
+ }
1318
+ case "piEditArgs": {
1319
+ const args = execMsg.message.value;
1320
+ const toolCallId = crypto.randomUUID();
1321
+ synthesizeCursorExecToolCall(output, stream, toolCallId, "edit", {
1322
+ path: args.path,
1323
+ edits: args.edits.map(edit => ({ old_text: edit.oldText, new_text: edit.newText })),
1237
1324
  });
1238
- h2Request.write(frameConnectMessage(toBinary(AgentClientMessageSchema, clientMessage)));
1325
+ const call = { args, toolCallId };
1326
+ const { execResult } = await resolveExecHandler(
1327
+ call,
1328
+ execHandlers?.piEdit?.bind(execHandlers),
1329
+ onToolResult,
1330
+ buildPiEditResult,
1331
+ buildPiEditRejected,
1332
+ buildPiEditError,
1333
+ );
1334
+ sendExecClientMessage(h2Request, execMsg, "piEditResult", execResult);
1335
+ return;
1336
+ }
1337
+ case "piWriteArgs": {
1338
+ const args = execMsg.message.value;
1339
+ const toolCallId = crypto.randomUUID();
1340
+ synthesizeCursorExecToolCall(output, stream, toolCallId, "write", {
1341
+ path: args.path,
1342
+ content: args.content,
1343
+ });
1344
+ const call = { args, toolCallId };
1345
+ const { execResult } = await resolveExecHandler(
1346
+ call,
1347
+ execHandlers?.piWrite?.bind(execHandlers),
1348
+ onToolResult,
1349
+ buildPiWriteResult,
1350
+ buildPiWriteRejected,
1351
+ buildPiWriteError,
1352
+ );
1353
+ sendExecClientMessage(h2Request, execMsg, "piWriteResult", execResult);
1354
+ return;
1355
+ }
1356
+ case "piGrepArgs": {
1357
+ const args = execMsg.message.value;
1358
+ const toolCallId = crypto.randomUUID();
1359
+ synthesizeCursorExecToolCall(output, stream, toolCallId, "search", {
1360
+ pattern: args.literal === true ? piEscapeRegexLiteral(args.pattern) : args.pattern,
1361
+ paths: [args.glob ? piJoinPath(args.path, args.glob) : args.path || "."],
1362
+ // The model-facing search schema uses `i: true` for
1363
+ // case-insensitive matching. Keep the field absent otherwise.
1364
+ ...(args.ignoreCase === true ? { i: true } : {}),
1365
+ context: args.context,
1366
+ limit: piLimit(args.limit),
1367
+ });
1368
+ const call = { args, toolCallId };
1369
+ const { execResult } = await resolveExecHandler(
1370
+ call,
1371
+ execHandlers?.piGrep?.bind(execHandlers),
1372
+ onToolResult,
1373
+ buildPiGrepResult,
1374
+ buildPiGrepError,
1375
+ buildPiGrepError,
1376
+ );
1377
+ sendExecClientMessage(h2Request, execMsg, "piGrepResult", execResult);
1378
+ return;
1379
+ }
1380
+ case "piFindArgs": {
1381
+ const args = execMsg.message.value;
1382
+ const toolCallId = crypto.randomUUID();
1383
+ synthesizeCursorExecToolCall(output, stream, toolCallId, "find", {
1384
+ paths: [piJoinPath(args.path, args.pattern)],
1385
+ limit: piLimit(args.limit),
1386
+ });
1387
+ const call = { args, toolCallId };
1388
+ const { execResult } = await resolveExecHandler(
1389
+ call,
1390
+ execHandlers?.piFind?.bind(execHandlers),
1391
+ onToolResult,
1392
+ buildPiFindResult,
1393
+ buildPiFindError,
1394
+ buildPiFindError,
1395
+ );
1396
+ sendExecClientMessage(h2Request, execMsg, "piFindResult", execResult);
1397
+ return;
1398
+ }
1399
+ case "piLsArgs": {
1400
+ const args = execMsg.message.value;
1401
+ const toolCallId = crypto.randomUUID();
1402
+ synthesizeCursorExecToolCall(output, stream, toolCallId, "read", { path: piLsPath(args.path) });
1403
+ const call = { args, toolCallId };
1404
+ const { execResult } = await resolveExecHandler(
1405
+ call,
1406
+ execHandlers?.piLs?.bind(execHandlers),
1407
+ onToolResult,
1408
+ buildPiLsResult,
1409
+ buildPiLsError,
1410
+ buildPiLsError,
1411
+ );
1412
+ sendExecClientMessage(h2Request, execMsg, "piLsResult", execResult);
1413
+ return;
1414
+ }
1415
+ case "mcpStateExecArgs": {
1416
+ sendExecClientMessage(
1417
+ h2Request,
1418
+ execMsg,
1419
+ "mcpStateExecResult",
1420
+ buildMcpStateResult(requestContextTools, execMsg.message.value.serverIdentifiers),
1421
+ );
1422
+ return;
1423
+ }
1424
+ case "executeHookArgs": {
1425
+ const execResult = buildNeutralHookResult(execMsg.message.value.request);
1426
+ if (!execResult) {
1427
+ sendExecClientThrow(
1428
+ h2Request,
1429
+ execMsg,
1430
+ `Unsupported hook request: ${execMsg.message.value.request?.request.case ?? "unset"}`,
1431
+ "unknown_hook_request",
1432
+ );
1433
+ return;
1434
+ }
1435
+ sendExecClientMessage(h2Request, execMsg, "executeHookResult", execResult);
1436
+ return;
1437
+ }
1438
+ case "shellAllowlistPrecheckArgs": {
1439
+ sendExecClientMessage(
1440
+ h2Request,
1441
+ execMsg,
1442
+ "shellAllowlistPrecheckResult",
1443
+ create(ShellAllowlistPrecheckResultSchema, { allowlisted: false }),
1444
+ );
1445
+ return;
1446
+ }
1447
+ case "mcpAllowlistPrecheckArgs": {
1448
+ sendExecClientMessage(
1449
+ h2Request,
1450
+ execMsg,
1451
+ "mcpAllowlistPrecheckResult",
1452
+ create(McpAllowlistPrecheckResultSchema, { allowlisted: false }),
1453
+ );
1454
+ return;
1455
+ }
1456
+ case "webFetchAllowlistPrecheckArgs": {
1457
+ sendExecClientMessage(
1458
+ h2Request,
1459
+ execMsg,
1460
+ "webFetchAllowlistPrecheckResult",
1461
+ create(WebFetchAllowlistPrecheckResultSchema, { allowlisted: false }),
1462
+ );
1463
+ return;
1464
+ }
1465
+ default: {
1466
+ log("warn", "unhandledExecMessage", { execCase });
1467
+ sendExecClientThrow(
1468
+ h2Request,
1469
+ execMsg,
1470
+ `No handler for exec message of type ${execCase}`,
1471
+ "exec_variant_unsupported",
1472
+ );
1239
1473
  }
1240
1474
  }
1241
1475
  }
1242
1476
 
1243
- function sendExecClientMessage<T>(
1477
+ function sendExecClientMessage<TCase extends NonNullable<ExecClientMessage["message"]["case"]>>(
1244
1478
  h2Request: http2.ClientHttp2Stream,
1245
1479
  execMsg: ExecServerMessage,
1246
- messageCase: ExecClientMessage["message"]["case"],
1247
- value: T,
1480
+ messageCase: TCase,
1481
+ value: Extract<ExecClientMessage["message"], { case: TCase }>["value"],
1248
1482
  ): void {
1249
1483
  const execClientMessage = create(ExecClientMessageSchema, {
1250
1484
  id: execMsg.id,
1251
1485
  execId: execMsg.execId,
1252
- message: {
1253
- case: messageCase,
1254
- value: value as any,
1255
- },
1486
+ message: { case: messageCase, value } as ExecClientMessage["message"],
1256
1487
  });
1257
1488
 
1258
1489
  const clientMessage = create(AgentClientMessageSchema, {
@@ -1265,6 +1496,25 @@ function sendExecClientMessage<T>(
1265
1496
  log("execClientMessage", messageCase, value);
1266
1497
  }
1267
1498
 
1499
+ function sendExecClientThrow(
1500
+ h2Request: http2.ClientHttp2Stream,
1501
+ execMsg: ExecServerMessage,
1502
+ error: string,
1503
+ errorCode: string,
1504
+ ): void {
1505
+ const controlMessage = create(ExecClientControlMessageSchema, {
1506
+ message: {
1507
+ case: "throw",
1508
+ value: create(ExecClientThrowSchema, { id: execMsg.id, error, errorCode }),
1509
+ },
1510
+ });
1511
+ const clientMessage = create(AgentClientMessageSchema, {
1512
+ message: { case: "execClientControlMessage", value: controlMessage },
1513
+ });
1514
+ h2Request.write(frameConnectMessage(toBinary(AgentClientMessageSchema, clientMessage)));
1515
+ sendExecClientStreamClose(h2Request, execMsg);
1516
+ }
1517
+
1268
1518
  function sendExecClientStreamClose(h2Request: http2.ClientHttp2Stream, execMsg: ExecServerMessage): void {
1269
1519
  const closeMessage = create(ExecClientControlMessageSchema, {
1270
1520
  message: {
@@ -1313,6 +1563,26 @@ export async function resolveExecHandler<TArgs, TResult>(
1313
1563
  }
1314
1564
  }
1315
1565
 
1566
+ /** Exported for deterministic coverage of ordered server-message handling. */
1567
+ export function createCursorMessageQueueForTest(onError?: (error: unknown) => void): {
1568
+ enqueue(handler: () => void | Promise<void>): Promise<void>;
1569
+ drain(): Promise<void>;
1570
+ } {
1571
+ let chain = Promise.resolve();
1572
+ return {
1573
+ enqueue(handler) {
1574
+ const result = chain.then(handler);
1575
+ chain = result.catch(error => {
1576
+ onError?.(error);
1577
+ });
1578
+ return result;
1579
+ },
1580
+ drain() {
1581
+ return chain;
1582
+ },
1583
+ };
1584
+ }
1585
+
1316
1586
  function splitExecHandlerResult<TResult>(result: CursorExecHandlerResult<TResult>): {
1317
1587
  execResult?: TResult;
1318
1588
  toolResult?: ToolResultMessage;
@@ -1866,12 +2136,15 @@ interface CursorTodoItem {
1866
2136
 
1867
2137
  interface CursorUpdateTodosToolCall {
1868
2138
  updateTodosToolCall?: { args?: { todos?: CursorTodoItem[] } };
2139
+ tool?: { case?: string; value?: { args?: { todos?: CursorTodoItem[] } } };
1869
2140
  }
1870
2141
 
1871
2142
  function buildTodoWriteArgs(toolCall: CursorUpdateTodosToolCall): {
1872
2143
  todos: Array<{ id?: string; content: string; activeForm: string; status: "pending" | "in_progress" | "completed" }>;
1873
2144
  } | null {
1874
- const todos = toolCall.updateTodosToolCall?.args?.todos;
2145
+ const updateCall =
2146
+ toolCall.tool?.case === "updateTodosToolCall" ? toolCall.tool.value : toolCall.updateTodosToolCall;
2147
+ const todos = updateCall?.args?.todos;
1875
2148
  if (!todos) return null;
1876
2149
  return {
1877
2150
  todos: todos.map(todo => ({
@@ -1984,11 +2257,45 @@ export function cursorJsonSafeValueForTest(value: unknown): unknown {
1984
2257
  return cursorJsonSafeValue(value);
1985
2258
  }
1986
2259
 
2260
+ function selectMcpToolCall(toolCall: any): any {
2261
+ return toolCall?.tool?.case === "mcpToolCall" ? toolCall.tool.value : toolCall?.mcpToolCall;
2262
+ }
2263
+
2264
+ const CURSOR_EXEC_OWNED_TOOL_CASES = new Set([
2265
+ "piReadToolCall",
2266
+ "piBashToolCall",
2267
+ "piEditToolCall",
2268
+ "piWriteToolCall",
2269
+ "piGrepToolCall",
2270
+ "piFindToolCall",
2271
+ "piLsToolCall",
2272
+ ]);
2273
+
2274
+ function isExecOwnedToolCall(toolCall: any): boolean {
2275
+ return CURSOR_EXEC_OWNED_TOOL_CASES.has(toolCall?.tool?.case);
2276
+ }
2277
+
1987
2278
  export function buildNativeToolCallBlock(
1988
2279
  toolCall: Record<string, unknown>,
1989
2280
  callId: string,
1990
2281
  index: number,
1991
2282
  ): ToolCallState | null {
2283
+ const oneof = toolCall.tool as { case?: string; value?: unknown } | undefined;
2284
+ if (oneof?.case && oneof.value && typeof oneof.value === "object") {
2285
+ const args = (oneof.value as { args?: unknown }).args;
2286
+ const convertedArgs = cursorJsonSafeValue(args ?? oneof.value);
2287
+ return {
2288
+ type: "toolCall",
2289
+ id: callId,
2290
+ name: cursorNativeToolName(oneof.case),
2291
+ arguments:
2292
+ convertedArgs && typeof convertedArgs === "object" && !Array.isArray(convertedArgs)
2293
+ ? (convertedArgs as Record<string, unknown>)
2294
+ : { raw: convertedArgs },
2295
+ index,
2296
+ kind: "native",
2297
+ };
2298
+ }
1992
2299
  for (const [key, payload] of Object.entries(toolCall)) {
1993
2300
  if (!/ToolCall$/.test(key) || !payload || typeof payload !== "object") continue;
1994
2301
  if (key === "mcpToolCall" || key === "updateTodosToolCall") continue;
@@ -2067,6 +2374,30 @@ function buildMcpErrorResult(error: string) {
2067
2374
  });
2068
2375
  }
2069
2376
 
2377
+ function synthesizeCursorExecToolCall(
2378
+ output: AssistantMessage,
2379
+ stream: AssistantMessageEventStream,
2380
+ toolCallId: string,
2381
+ name: string,
2382
+ args: Record<string, unknown>,
2383
+ ): void {
2384
+ const block: ToolCallState = {
2385
+ type: "toolCall",
2386
+ id: toolCallId,
2387
+ name,
2388
+ arguments: cursorJsonSafeValue(args) as Record<string, unknown>,
2389
+ index: output.content.length,
2390
+ kind: "cursor-exec",
2391
+ [kCursorExecResolved]: true,
2392
+ };
2393
+ output.content.push(block);
2394
+ const contentIndex = output.content.length - 1;
2395
+ stream.push({ type: "toolcall_start", contentIndex, partial: output });
2396
+ delete (block as Partial<ToolCallState>).index;
2397
+ delete (block as Partial<ToolCallState>).kind;
2398
+ stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output });
2399
+ }
2400
+
2070
2401
  function processInteractionUpdate(
2071
2402
  update: any,
2072
2403
  output: AssistantMessage,
@@ -2122,10 +2453,14 @@ function processInteractionUpdate(
2122
2453
  });
2123
2454
  state.setThinkingBlock(null);
2124
2455
  }
2456
+ } else if (updateCase === "toolCallStarted" && isExecOwnedToolCall(update.message.value.toolCall)) {
2457
+ // Pi stream call IDs and exec IDs are distinct namespaces; without a shared
2458
+ // correlation field, suppress the streamed variant and synthesize from exec.
2459
+ log("exec", "streamedToolCallOwnedByExec", { case: update.message.value.toolCall?.tool?.case });
2125
2460
  } else if (updateCase === "toolCallStarted") {
2126
2461
  const toolCall = update.message.value.toolCall;
2127
2462
  if (toolCall) {
2128
- const mcpCall = toolCall.mcpToolCall;
2463
+ const mcpCall = selectMcpToolCall(toolCall);
2129
2464
  if (mcpCall) {
2130
2465
  const args = mcpCall.args || {};
2131
2466
  const block: ToolCallState = {
@@ -2186,7 +2521,7 @@ function processInteractionUpdate(
2186
2521
  if (state.currentToolCall) {
2187
2522
  const toolCall = update.message.value.toolCall;
2188
2523
  if (state.currentToolCall.kind === "mcp") {
2189
- const decodedArgs = decodeMcpArgsMap(toolCall?.mcpToolCall?.args?.args);
2524
+ const decodedArgs = decodeMcpArgsMap(selectMcpToolCall(toolCall)?.args?.args);
2190
2525
  if (decodedArgs) {
2191
2526
  state.currentToolCall.arguments = decodedArgs;
2192
2527
  }
@@ -13,9 +13,8 @@
13
13
  */
14
14
 
15
15
  import { Buffer } from "node:buffer";
16
- import * as os from "node:os";
17
16
  import * as path from "node:path";
18
- import { $credentialEnv, $envpos, isEnoent, logger } from "@gajae-code/utils";
17
+ import { $credentialEnv, $envpos, getTrustedHomeDir, isEnoent, logger } from "@gajae-code/utils";
19
18
  import type { FetchImpl } from "../types";
20
19
 
21
20
  const OAUTH_TOKEN_URL = "https://oauth2.googleapis.com/token";
@@ -58,7 +57,7 @@ function getRefreshSkewMs(): number {
58
57
  }
59
58
 
60
59
  function userAdcPath(): string {
61
- return path.join(os.homedir(), ".config", "gcloud", "application_default_credentials.json");
60
+ return path.join(getTrustedHomeDir(), ".config", "gcloud", "application_default_credentials.json");
62
61
  }
63
62
 
64
63
  async function readJsonFile<T>(filePath: string): Promise<T | undefined> {