chatccc 0.2.231 → 0.2.233

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 (130) hide show
  1. package/.agents/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  2. package/.claude/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  3. package/.cursor/skills/create-chatccc-feishu-app/SKILL.md +85 -85
  4. package/README.md +90 -90
  5. package/agent-prompts/claude_specific.md +45 -45
  6. package/agent-prompts/codex_specific.md +2 -2
  7. package/agent-prompts/cursor_specific.md +13 -13
  8. package/bin/cccagent.mjs +17 -17
  9. package/im-skills/feishu-skill/receive-send-file.md +63 -63
  10. package/im-skills/feishu-skill/receive-send-image.md +24 -24
  11. package/im-skills/feishu-skill/skill.md +3 -3
  12. package/im-skills/wechat-file-skill/receive-send-file.md +38 -38
  13. package/im-skills/wechat-file-skill/send-file.mjs +83 -83
  14. package/im-skills/wechat-file-skill/skill.md +10 -10
  15. package/im-skills/wechat-image-skill/skill.md +10 -10
  16. package/im-skills/wechat-video-skill/receive-send-video.md +38 -38
  17. package/im-skills/wechat-video-skill/send-video.mjs +79 -79
  18. package/im-skills/wechat-video-skill/skill.md +10 -10
  19. package/package.json +1 -1
  20. package/scripts/postinstall-sharp-check.mjs +58 -58
  21. package/src/__tests__/agent-activity.test.ts +86 -86
  22. package/src/__tests__/agent-delegate-task-rpc.test.ts +165 -165
  23. package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
  24. package/src/__tests__/builtin-chat-session.test.ts +449 -393
  25. package/src/__tests__/builtin-cli-json.test.ts +39 -39
  26. package/src/__tests__/builtin-config.test.ts +26 -26
  27. package/src/__tests__/builtin-context.test.ts +319 -202
  28. package/src/__tests__/builtin-file-tools.test.ts +240 -240
  29. package/src/__tests__/builtin-permissions.test.ts +211 -211
  30. package/src/__tests__/builtin-session-search.test.ts +228 -0
  31. package/src/__tests__/builtin-session-select.test.ts +116 -116
  32. package/src/__tests__/builtin-sigint.test.ts +56 -56
  33. package/src/__tests__/builtin-skills.test.ts +284 -284
  34. package/src/__tests__/builtin-web-tools.test.ts +220 -220
  35. package/src/__tests__/card-action-routing.test.ts +18 -18
  36. package/src/__tests__/cardkit.test.ts +60 -60
  37. package/src/__tests__/ccc-adapter.test.ts +170 -170
  38. package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
  39. package/src/__tests__/chatgpt-subscription.test.ts +135 -135
  40. package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
  41. package/src/__tests__/claude-adapter.test.ts +614 -614
  42. package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
  43. package/src/__tests__/codex-adapter.test.ts +58 -58
  44. package/src/__tests__/codex-raw-stream-log.test.ts +170 -170
  45. package/src/__tests__/codex-reset-actions.test.ts +146 -146
  46. package/src/__tests__/crash-logging.test.ts +95 -1
  47. package/src/__tests__/cursor-adapter.test.ts +268 -268
  48. package/src/__tests__/feishu-api.test.ts +60 -60
  49. package/src/__tests__/feishu-avatar.test.ts +164 -164
  50. package/src/__tests__/feishu-message-ingress.test.ts +138 -138
  51. package/src/__tests__/feishu-platform.test.ts +22 -22
  52. package/src/__tests__/format-message.test.ts +47 -47
  53. package/src/__tests__/jsonl-stream.test.ts +79 -79
  54. package/src/__tests__/package-files.test.ts +24 -24
  55. package/src/__tests__/privacy.test.ts +198 -198
  56. package/src/__tests__/progress-reducer.test.ts +121 -121
  57. package/src/__tests__/raw-stream-log.test.ts +106 -106
  58. package/src/__tests__/response-stall.test.ts +49 -49
  59. package/src/__tests__/restart.test.ts +55 -10
  60. package/src/__tests__/session.test.ts +199 -199
  61. package/src/__tests__/shared-prefix.test.ts +36 -36
  62. package/src/__tests__/sim-platform.test.ts +16 -16
  63. package/src/__tests__/startup-lifecycle.test.ts +231 -231
  64. package/src/__tests__/stop-session.test.ts +34 -34
  65. package/src/__tests__/stream-state.test.ts +42 -42
  66. package/src/__tests__/terminal-error.test.ts +54 -54
  67. package/src/__tests__/terminal-renderer.test.ts +247 -247
  68. package/src/__tests__/update-command-guard.test.ts +144 -144
  69. package/src/__tests__/web-ui.test.ts +326 -326
  70. package/src/adapters/adapter-interface.ts +33 -33
  71. package/src/adapters/ccc-adapter.ts +141 -141
  72. package/src/adapters/claude-adapter.ts +620 -620
  73. package/src/adapters/claude-session-meta-store.ts +120 -120
  74. package/src/adapters/codex-adapter.ts +426 -426
  75. package/src/adapters/cursor-adapter.ts +681 -681
  76. package/src/adapters/jsonl-stream.ts +157 -157
  77. package/src/adapters/raw-stream-log.ts +124 -124
  78. package/src/adapters/resource-monitor.ts +140 -140
  79. package/src/agent-activity.ts +175 -175
  80. package/src/agent-delegate-task-rpc.ts +153 -153
  81. package/src/agent-delegate-task.ts +91 -91
  82. package/src/agent-reload-config-rpc.ts +34 -34
  83. package/src/agent-stop-stuck.ts +129 -129
  84. package/src/builtin/cli.ts +6 -0
  85. package/src/builtin/config.ts +84 -84
  86. package/src/builtin/context.ts +465 -391
  87. package/src/builtin/file-log.ts +38 -38
  88. package/src/builtin/file-tools.ts +34 -0
  89. package/src/builtin/index.ts +24 -23
  90. package/src/builtin/proc-tree-kill.ts +61 -61
  91. package/src/builtin/progress/cards-helpers.ts +76 -76
  92. package/src/builtin/progress/reducer.ts +113 -113
  93. package/src/builtin/progress/terminal-renderer.ts +294 -294
  94. package/src/builtin/progress/view.ts +77 -77
  95. package/src/builtin/raw-stream-log.ts +124 -124
  96. package/src/builtin/session-search.ts +363 -0
  97. package/src/builtin/session-select.ts +48 -48
  98. package/src/builtin/sigint.ts +50 -50
  99. package/src/builtin/skills.ts +205 -205
  100. package/src/builtin/web-tools.ts +313 -313
  101. package/src/card-action-routing.ts +14 -14
  102. package/src/chatgpt-subscription-rpc.ts +27 -27
  103. package/src/chatgpt-subscription.ts +299 -299
  104. package/src/chrome-devtools-guard.ts +318 -318
  105. package/src/codex-reset-actions.ts +184 -184
  106. package/src/feishu-api.ts +193 -193
  107. package/src/feishu-message-ingress.ts +195 -195
  108. package/src/feishu-platform.ts +20 -20
  109. package/src/format-message.ts +293 -293
  110. package/src/index.ts +6 -1
  111. package/src/litellm-proxy.ts +374 -374
  112. package/src/orchestrator.ts +36 -13
  113. package/src/platform-adapter.ts +6 -6
  114. package/src/privacy.ts +118 -118
  115. package/src/progress/reducer.ts +113 -113
  116. package/src/progress/terminal-renderer.ts +294 -294
  117. package/src/progress/view.ts +77 -77
  118. package/src/response-stall.ts +28 -28
  119. package/src/runtime-reload.ts +34 -34
  120. package/src/session-chat-binding.ts +82 -82
  121. package/src/session-name.ts +8 -8
  122. package/src/session.ts +201 -201
  123. package/src/shared-prefix.ts +29 -29
  124. package/src/shared.ts +44 -0
  125. package/src/sim-platform.ts +20 -20
  126. package/src/startup-lifecycle.ts +250 -250
  127. package/src/stream-state.ts +21 -21
  128. package/src/terminal-error.ts +129 -129
  129. package/src/turn-cards.ts +117 -117
  130. package/src/update-command-guard.ts +165 -165
@@ -1,17 +1,17 @@
1
- import { afterEach, describe, it, expect, vi } from "vitest";
2
- import { EventEmitter } from "node:events";
3
- import { readFileSync } from "node:fs";
4
- import { join, dirname } from "node:path";
5
- import { PassThrough } from "node:stream";
6
- import type { ChildProcess } from "node:child_process";
7
- import { fileURLToPath } from "node:url";
8
- import {
9
- normalizeCursorMessage,
10
- createCursorAdapter,
11
- formatCursorAgentEmptyOutputMessage,
12
- } from "../adapters/cursor-adapter.ts";
13
- import { BadJsonIdleTimeoutError } from "../adapters/jsonl-stream.ts";
14
- import type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
1
+ import { afterEach, describe, it, expect, vi } from "vitest";
2
+ import { EventEmitter } from "node:events";
3
+ import { readFileSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { PassThrough } from "node:stream";
6
+ import type { ChildProcess } from "node:child_process";
7
+ import { fileURLToPath } from "node:url";
8
+ import {
9
+ normalizeCursorMessage,
10
+ createCursorAdapter,
11
+ formatCursorAgentEmptyOutputMessage,
12
+ } from "../adapters/cursor-adapter.ts";
13
+ import { BadJsonIdleTimeoutError } from "../adapters/jsonl-stream.ts";
14
+ import type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
15
15
  import type {
16
16
  CursorSessionMeta,
17
17
  CursorSessionMetaStore,
@@ -45,70 +45,70 @@ function createInMemoryMetaStore(
45
45
  };
46
46
  }
47
47
 
48
- const __dirname = dirname(fileURLToPath(import.meta.url));
49
-
50
- type CursorSpawnForTest = NonNullable<
51
- NonNullable<Parameters<typeof createCursorAdapter>[0]>["spawn"]
52
- >;
53
-
54
- function readFixture(name: string): unknown[] {
55
- const raw = readFileSync(join(__dirname, "fixtures", name), "utf-8");
56
- return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
57
- }
58
-
59
- function createMockCursorProcess(args: {
60
- stdout?: string;
61
- stderr?: string;
62
- exitCode?: number;
63
- }): ChildProcess {
64
- const child = new EventEmitter() as ChildProcess;
65
- const stdout = new PassThrough();
66
- const stderr = new PassThrough();
67
- const stdin = new PassThrough();
68
- Object.assign(child, {
69
- stdout,
70
- stderr,
71
- stdin,
72
- pid: undefined,
73
- });
74
-
75
- setImmediate(() => {
76
- if (args.stdout) stdout.write(args.stdout);
77
- if (args.stderr) stderr.write(args.stderr);
78
- stdout.end();
79
- stderr.end();
80
- child.emit("close", args.exitCode ?? 0, null);
81
- });
82
-
83
- return child;
84
- }
85
-
86
- function createHangingMockCursorProcess(args: {
87
- stdout?: string;
88
- stderr?: string;
89
- }): ChildProcess {
90
- const child = new EventEmitter() as ChildProcess;
91
- const stdout = new PassThrough();
92
- const stderr = new PassThrough();
93
- const stdin = new PassThrough();
94
- Object.assign(child, {
95
- stdout,
96
- stderr,
97
- stdin,
98
- pid: undefined,
99
- });
100
-
101
- queueMicrotask(() => {
102
- if (args.stdout) stdout.write(args.stdout);
103
- if (args.stderr) stderr.write(args.stderr);
104
- });
105
-
106
- return child;
107
- }
108
-
109
- afterEach(() => {
110
- vi.useRealTimers();
111
- });
48
+ const __dirname = dirname(fileURLToPath(import.meta.url));
49
+
50
+ type CursorSpawnForTest = NonNullable<
51
+ NonNullable<Parameters<typeof createCursorAdapter>[0]>["spawn"]
52
+ >;
53
+
54
+ function readFixture(name: string): unknown[] {
55
+ const raw = readFileSync(join(__dirname, "fixtures", name), "utf-8");
56
+ return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
57
+ }
58
+
59
+ function createMockCursorProcess(args: {
60
+ stdout?: string;
61
+ stderr?: string;
62
+ exitCode?: number;
63
+ }): ChildProcess {
64
+ const child = new EventEmitter() as ChildProcess;
65
+ const stdout = new PassThrough();
66
+ const stderr = new PassThrough();
67
+ const stdin = new PassThrough();
68
+ Object.assign(child, {
69
+ stdout,
70
+ stderr,
71
+ stdin,
72
+ pid: undefined,
73
+ });
74
+
75
+ setImmediate(() => {
76
+ if (args.stdout) stdout.write(args.stdout);
77
+ if (args.stderr) stderr.write(args.stderr);
78
+ stdout.end();
79
+ stderr.end();
80
+ child.emit("close", args.exitCode ?? 0, null);
81
+ });
82
+
83
+ return child;
84
+ }
85
+
86
+ function createHangingMockCursorProcess(args: {
87
+ stdout?: string;
88
+ stderr?: string;
89
+ }): ChildProcess {
90
+ const child = new EventEmitter() as ChildProcess;
91
+ const stdout = new PassThrough();
92
+ const stderr = new PassThrough();
93
+ const stdin = new PassThrough();
94
+ Object.assign(child, {
95
+ stdout,
96
+ stderr,
97
+ stdin,
98
+ pid: undefined,
99
+ });
100
+
101
+ queueMicrotask(() => {
102
+ if (args.stdout) stdout.write(args.stdout);
103
+ if (args.stderr) stderr.write(args.stderr);
104
+ });
105
+
106
+ return child;
107
+ }
108
+
109
+ afterEach(() => {
110
+ vi.useRealTimers();
111
+ });
112
112
 
113
113
  // ---------------------------------------------------------------------------
114
114
  // normalizeCursorMessage — 核心映射逻辑测试(纯函数)
@@ -347,32 +347,32 @@ describe("normalizeCursorMessage", () => {
347
347
  // createCursorAdapter — 工厂函数测试
348
348
  // ---------------------------------------------------------------------------
349
349
 
350
- describe("createCursorAdapter", () => {
350
+ describe("createCursorAdapter", () => {
351
351
  it("returns adapter with correct displayName and sessionDescPrefix", () => {
352
352
  const adapter = createCursorAdapter();
353
353
  expect(adapter.displayName).toBe("Cursor");
354
354
  expect(adapter.sessionDescPrefix).toBe("Cursor Session:");
355
355
  });
356
356
 
357
- it("closeSession does not throw", async () => {
358
- const adapter = createCursorAdapter();
359
- await expect(adapter.closeSession("any-id")).resolves.toBeUndefined();
360
- });
361
-
362
- it("aborts a zero-output createSession stream instead of leaving Cursor Agent alive", async () => {
363
- const store = createInMemoryMetaStore();
364
- const spawnImpl = (() =>
365
- createHangingMockCursorProcess({})) as CursorSpawnForTest;
366
- const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
367
- const controller = new AbortController();
368
-
369
- const pending = adapter.createSession("F:/repo", controller.signal);
370
- controller.abort();
371
-
372
- await expect(pending).rejects.toThrow("Cursor session creation aborted");
373
- });
374
-
375
- // -------------------------------------------------------------------------
357
+ it("closeSession does not throw", async () => {
358
+ const adapter = createCursorAdapter();
359
+ await expect(adapter.closeSession("any-id")).resolves.toBeUndefined();
360
+ });
361
+
362
+ it("aborts a zero-output createSession stream instead of leaving Cursor Agent alive", async () => {
363
+ const store = createInMemoryMetaStore();
364
+ const spawnImpl = (() =>
365
+ createHangingMockCursorProcess({})) as CursorSpawnForTest;
366
+ const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
367
+ const controller = new AbortController();
368
+
369
+ const pending = adapter.createSession("F:/repo", controller.signal);
370
+ controller.abort();
371
+
372
+ await expect(pending).rejects.toThrow("Cursor session creation aborted");
373
+ });
374
+
375
+ // -------------------------------------------------------------------------
376
376
  // getSessionInfo 行为契约
377
377
  // - cwd 决定 /git 是否可用
378
378
  // - model 决定 /state、/sessions 显示的是否是 Cursor 真实模型
@@ -458,10 +458,10 @@ describe("normalizeCursorMessage - 三类 assistant 事件区分", () => {
458
458
  type: "assistant",
459
459
  message: { role: "assistant", content: [{ type: "text", text: "你" }] },
460
460
  timestamp_ms: 1778411927583,
461
- });
462
- expect(result).not.toBeNull();
463
- expect(result!.blocks).toEqual([{ type: "text", text: "你" }]);
464
- expect(result!.isFinalResponse).toBeUndefined();
461
+ });
462
+ expect(result).not.toBeNull();
463
+ expect(result!.blocks).toEqual([{ type: "text", text: "你" }]);
464
+ expect(result!.isFinalResponse).toBeUndefined();
465
465
  });
466
466
 
467
467
  it("Buffered flush(has timestamp_ms, has model_call_id)→ text_final(覆盖,避免与 delta 重复累加)", () => {
@@ -475,10 +475,10 @@ describe("normalizeCursorMessage - 三类 assistant 事件区分", () => {
475
475
  model_call_id: "mc-1",
476
476
  } as Parameters<typeof normalizeCursorMessage>[0]);
477
477
  expect(result).not.toBeNull();
478
- expect(result!.blocks).toEqual([
479
- { type: "text_final", text: "完整快照(与 delta 累计相同)" },
480
- ]);
481
- expect(result!.isFinalResponse).toBeUndefined();
478
+ expect(result!.blocks).toEqual([
479
+ { type: "text_final", text: "完整快照(与 delta 累计相同)" },
480
+ ]);
481
+ expect(result!.isFinalResponse).toBeUndefined();
482
482
  });
483
483
 
484
484
  it("Final flush(no timestamp_ms)→ text_final(覆盖)", () => {
@@ -490,10 +490,10 @@ describe("normalizeCursorMessage - 三类 assistant 事件区分", () => {
490
490
  },
491
491
  });
492
492
  expect(result).not.toBeNull();
493
- expect(result!.blocks).toEqual([
494
- { type: "text_final", text: "你上一题问的是 1+2=?" },
495
- ]);
496
- expect(result!.isFinalResponse).toBeUndefined();
493
+ expect(result!.blocks).toEqual([
494
+ { type: "text_final", text: "你上一题问的是 1+2=?" },
495
+ ]);
496
+ expect(result!.isFinalResponse).toBeUndefined();
497
497
  });
498
498
  });
499
499
 
@@ -506,11 +506,11 @@ describe("normalizeCursorMessage - result 消息(官方权威最终文本)",
506
506
  });
507
507
  expect(result).not.toBeNull();
508
508
  expect(result!.type).toBe("assistant");
509
- expect(result!.blocks).toEqual([
510
- { type: "text_final", text: "权威最终文本" },
511
- ]);
512
- expect(result!.isFinalResponse).toBe(true);
513
- });
509
+ expect(result!.blocks).toEqual([
510
+ { type: "text_final", text: "权威最终文本" },
511
+ ]);
512
+ expect(result!.isFinalResponse).toBe(true);
513
+ });
514
514
 
515
515
  it("result 消息没有 result 字段时返回 null(无可用文本)", () => {
516
516
  expect(
@@ -677,11 +677,11 @@ describe("Cursor stream fixture - 端到端不重复", () => {
677
677
  session_id: "sid",
678
678
  timestamp_ms: 1000,
679
679
  } as Parameters<typeof normalizeCursorMessage>[0]);
680
- expect(result).not.toBeNull();
681
- expect(result!.blocks).toEqual([
682
- { type: "tool_use", id: "toolu_abc", name: "Bash", input: { command: "ls" } },
683
- ]);
684
- });
680
+ expect(result).not.toBeNull();
681
+ expect(result!.blocks).toEqual([
682
+ { type: "tool_use", id: "toolu_abc", name: "Bash", input: { command: "ls" } },
683
+ ]);
684
+ });
685
685
 
686
686
  it("normalizes tool_call completed → tool_result block (success)", () => {
687
687
  const result = normalizeCursorMessage({
@@ -725,7 +725,7 @@ describe("Cursor stream fixture - 端到端不重复", () => {
725
725
  });
726
726
  });
727
727
 
728
- it("mapToolCallKey: maps known keys to readable names", () => {
728
+ it("mapToolCallKey: maps known keys to readable names", () => {
729
729
  const cases: [string, string][] = [
730
730
  ["globToolCall", "Glob"],
731
731
  ["shellToolCall", "Bash"],
@@ -741,150 +741,150 @@ describe("Cursor stream fixture - 端到端不重复", () => {
741
741
  tool_call: { [raw]: { args: {} } },
742
742
  } as Parameters<typeof normalizeCursorMessage>[0]);
743
743
  expect(r!.blocks[0]).toMatchObject({ type: "tool_use", name: expected });
744
- }
745
- });
746
- });
747
-
748
- describe("Cursor adapter process failures", () => {
749
- it("formats empty stdout auth stderr as a cautious visible message", () => {
750
- const message = formatCursorAgentEmptyOutputMessage({
751
- exitCode: 1,
752
- stdoutLength: 0,
753
- stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
754
- });
755
-
756
- expect(message).toContain("检测到认证相关错误");
757
- expect(message).toContain("可能需要重新登录 Cursor Agent");
758
- expect(message).toContain("CURSOR_API_KEY");
759
- expect(message).toContain("agent status");
760
- expect(message).toContain("agent login");
761
- expect(message).not.toContain("登录态已失效");
762
- });
763
-
764
- it("formats empty stdout action-required stderr with the actionable Cursor error", () => {
765
- const message = formatCursorAgentEmptyOutputMessage({
766
- exitCode: 1,
767
- stdoutLength: 0,
768
- stderr: "ActionRequiredError: You have an unpaid invoice Your team has an unpaid invoice. Please contact your team administrator to pay your invoice and continue using Cursor.\n",
769
- });
770
-
771
- expect(message).toContain("底层命令异常退出");
772
- expect(message).toContain("[Cursor stderr] exit=1");
773
- expect(message).toContain("ActionRequiredError");
774
- expect(message).toContain("unpaid invoice");
775
- expect(message).toContain("team administrator");
776
- });
777
-
778
- it("redacts obvious secrets from visible Cursor stderr", () => {
779
- const message = formatCursorAgentEmptyOutputMessage({
780
- exitCode: 1,
781
- stdoutLength: 0,
782
- stderr: "Error: failed with token=secret-token-value and Bearer abcdefghijklmnop\n",
783
- });
784
-
785
- expect(message).toContain("token=<redacted>");
786
- expect(message).toContain("Bearer <redacted>");
787
- expect(message).not.toContain("secret-token-value");
788
- expect(message).not.toContain("abcdefghijklmnop");
789
- });
790
-
791
- it("does not format a message when stdout is non-empty", () => {
792
- expect(
793
- formatCursorAgentEmptyOutputMessage({
794
- exitCode: 1,
795
- stdoutLength: 10,
796
- stderr: "Error: Authentication required",
797
- }),
798
- ).toBeNull();
799
- });
800
-
801
- it("prompt surfaces auth-related empty output before failing the stream", async () => {
802
- const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
803
- const spawnImpl = (() =>
804
- createMockCursorProcess({
805
- exitCode: 1,
806
- stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
807
- })) as CursorSpawnForTest;
808
- const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
809
- const iterator = adapter.prompt(
810
- "sid",
811
- "[User message]\nhello\n[/User message]",
812
- "F:/repo",
813
- )[Symbol.asyncIterator]();
814
-
815
- const first = await iterator.next();
816
- expect(first.done).toBe(false);
817
- expect(first.value.blocks).toHaveLength(1);
818
- expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
819
- expect(first.value.blocks[0]).toHaveProperty(
820
- "text",
821
- expect.stringContaining("可能需要重新登录 Cursor Agent"),
822
- );
823
- expect(first.value.blocks[0]).toHaveProperty(
824
- "text",
825
- expect.not.stringContaining("登录态已失效"),
826
- );
827
-
828
- await expect(iterator.next()).rejects.toThrow(
829
- /Cursor Agent exited without stream-json output/,
830
- );
831
- });
832
-
833
- it("prompt surfaces action-required stderr before failing the stream", async () => {
834
- const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
835
- const spawnImpl = (() =>
836
- createMockCursorProcess({
837
- exitCode: 1,
838
- stderr: "ActionRequiredError: You have an unpaid invoice Your team has an unpaid invoice. Please contact your team administrator to pay your invoice and continue using Cursor.\n",
839
- })) as CursorSpawnForTest;
840
- const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
841
- const iterator = adapter.prompt(
842
- "sid",
843
- "[User message]\nhello\n[/User message]",
844
- "F:/repo",
845
- )[Symbol.asyncIterator]();
846
-
847
- const first = await iterator.next();
848
- expect(first.done).toBe(false);
849
- expect(first.value.blocks).toHaveLength(1);
850
- expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
851
- expect(first.value.blocks[0]).toHaveProperty(
852
- "text",
853
- expect.stringContaining("ActionRequiredError"),
854
- );
855
- expect(first.value.blocks[0]).toHaveProperty(
856
- "text",
857
- expect.stringContaining("unpaid invoice"),
858
- );
859
-
860
- await expect(iterator.next()).rejects.toThrow(
861
- /Cursor Agent exited without stream-json output/,
862
- );
863
- });
864
-
865
- it("prompt fails when Cursor emits bad JSON and then leaves stdout idle", async () => {
866
- const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
867
- const spawnImpl = (() =>
868
- createHangingMockCursorProcess({
869
- stdout: "{\"type\":\"tool_call\",\"subtype\":\"started\"\n",
870
- })) as CursorSpawnForTest;
871
- const adapter = createCursorAdapter({
872
- metaStore: store,
873
- spawn: spawnImpl,
874
- badJsonIdleTimeoutMs: 10,
875
- });
876
-
877
- const pending = (async () => {
878
- for await (const _event of adapter.prompt(
879
- "sid",
880
- "[User message]\nhello\n[/User message]",
881
- "F:/repo",
882
- )) {
883
- // No normalized events are expected before the watchdog fires.
884
- }
885
- })().catch((error: unknown) => error);
886
-
887
- const error = await pending;
888
- expect(error).toBeInstanceOf(BadJsonIdleTimeoutError);
889
- });
890
- });
744
+ }
745
+ });
746
+ });
747
+
748
+ describe("Cursor adapter process failures", () => {
749
+ it("formats empty stdout auth stderr as a cautious visible message", () => {
750
+ const message = formatCursorAgentEmptyOutputMessage({
751
+ exitCode: 1,
752
+ stdoutLength: 0,
753
+ stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
754
+ });
755
+
756
+ expect(message).toContain("检测到认证相关错误");
757
+ expect(message).toContain("可能需要重新登录 Cursor Agent");
758
+ expect(message).toContain("CURSOR_API_KEY");
759
+ expect(message).toContain("agent status");
760
+ expect(message).toContain("agent login");
761
+ expect(message).not.toContain("登录态已失效");
762
+ });
763
+
764
+ it("formats empty stdout action-required stderr with the actionable Cursor error", () => {
765
+ const message = formatCursorAgentEmptyOutputMessage({
766
+ exitCode: 1,
767
+ stdoutLength: 0,
768
+ stderr: "ActionRequiredError: You have an unpaid invoice Your team has an unpaid invoice. Please contact your team administrator to pay your invoice and continue using Cursor.\n",
769
+ });
770
+
771
+ expect(message).toContain("底层命令异常退出");
772
+ expect(message).toContain("[Cursor stderr] exit=1");
773
+ expect(message).toContain("ActionRequiredError");
774
+ expect(message).toContain("unpaid invoice");
775
+ expect(message).toContain("team administrator");
776
+ });
777
+
778
+ it("redacts obvious secrets from visible Cursor stderr", () => {
779
+ const message = formatCursorAgentEmptyOutputMessage({
780
+ exitCode: 1,
781
+ stdoutLength: 0,
782
+ stderr: "Error: failed with token=secret-token-value and Bearer abcdefghijklmnop\n",
783
+ });
784
+
785
+ expect(message).toContain("token=<redacted>");
786
+ expect(message).toContain("Bearer <redacted>");
787
+ expect(message).not.toContain("secret-token-value");
788
+ expect(message).not.toContain("abcdefghijklmnop");
789
+ });
790
+
791
+ it("does not format a message when stdout is non-empty", () => {
792
+ expect(
793
+ formatCursorAgentEmptyOutputMessage({
794
+ exitCode: 1,
795
+ stdoutLength: 10,
796
+ stderr: "Error: Authentication required",
797
+ }),
798
+ ).toBeNull();
799
+ });
800
+
801
+ it("prompt surfaces auth-related empty output before failing the stream", async () => {
802
+ const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
803
+ const spawnImpl = (() =>
804
+ createMockCursorProcess({
805
+ exitCode: 1,
806
+ stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
807
+ })) as CursorSpawnForTest;
808
+ const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
809
+ const iterator = adapter.prompt(
810
+ "sid",
811
+ "[User message]\nhello\n[/User message]",
812
+ "F:/repo",
813
+ )[Symbol.asyncIterator]();
814
+
815
+ const first = await iterator.next();
816
+ expect(first.done).toBe(false);
817
+ expect(first.value.blocks).toHaveLength(1);
818
+ expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
819
+ expect(first.value.blocks[0]).toHaveProperty(
820
+ "text",
821
+ expect.stringContaining("可能需要重新登录 Cursor Agent"),
822
+ );
823
+ expect(first.value.blocks[0]).toHaveProperty(
824
+ "text",
825
+ expect.not.stringContaining("登录态已失效"),
826
+ );
827
+
828
+ await expect(iterator.next()).rejects.toThrow(
829
+ /Cursor Agent exited without stream-json output/,
830
+ );
831
+ });
832
+
833
+ it("prompt surfaces action-required stderr before failing the stream", async () => {
834
+ const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
835
+ const spawnImpl = (() =>
836
+ createMockCursorProcess({
837
+ exitCode: 1,
838
+ stderr: "ActionRequiredError: You have an unpaid invoice Your team has an unpaid invoice. Please contact your team administrator to pay your invoice and continue using Cursor.\n",
839
+ })) as CursorSpawnForTest;
840
+ const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
841
+ const iterator = adapter.prompt(
842
+ "sid",
843
+ "[User message]\nhello\n[/User message]",
844
+ "F:/repo",
845
+ )[Symbol.asyncIterator]();
846
+
847
+ const first = await iterator.next();
848
+ expect(first.done).toBe(false);
849
+ expect(first.value.blocks).toHaveLength(1);
850
+ expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
851
+ expect(first.value.blocks[0]).toHaveProperty(
852
+ "text",
853
+ expect.stringContaining("ActionRequiredError"),
854
+ );
855
+ expect(first.value.blocks[0]).toHaveProperty(
856
+ "text",
857
+ expect.stringContaining("unpaid invoice"),
858
+ );
859
+
860
+ await expect(iterator.next()).rejects.toThrow(
861
+ /Cursor Agent exited without stream-json output/,
862
+ );
863
+ });
864
+
865
+ it("prompt fails when Cursor emits bad JSON and then leaves stdout idle", async () => {
866
+ const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
867
+ const spawnImpl = (() =>
868
+ createHangingMockCursorProcess({
869
+ stdout: "{\"type\":\"tool_call\",\"subtype\":\"started\"\n",
870
+ })) as CursorSpawnForTest;
871
+ const adapter = createCursorAdapter({
872
+ metaStore: store,
873
+ spawn: spawnImpl,
874
+ badJsonIdleTimeoutMs: 10,
875
+ });
876
+
877
+ const pending = (async () => {
878
+ for await (const _event of adapter.prompt(
879
+ "sid",
880
+ "[User message]\nhello\n[/User message]",
881
+ "F:/repo",
882
+ )) {
883
+ // No normalized events are expected before the watchdog fires.
884
+ }
885
+ })().catch((error: unknown) => error);
886
+
887
+ const error = await pending;
888
+ expect(error).toBeInstanceOf(BadJsonIdleTimeoutError);
889
+ });
890
+ });