chatccc 0.2.198 → 0.2.199
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.
- package/agent-prompts/cursor_specific.md +13 -13
- package/bin/cccagent.mjs +17 -17
- package/config.sample.json +27 -27
- package/package.json +1 -1
- package/src/__tests__/agent-reload-config-rpc.test.ts +99 -99
- package/src/__tests__/builtin-chat-session.test.ts +277 -277
- package/src/__tests__/builtin-cli-json.test.ts +39 -39
- package/src/__tests__/builtin-config.test.ts +33 -33
- package/src/__tests__/builtin-context.test.ts +163 -163
- package/src/__tests__/builtin-file-tools.test.ts +224 -224
- package/src/__tests__/builtin-session-select.test.ts +116 -116
- package/src/__tests__/builtin-sigint.test.ts +56 -56
- package/src/__tests__/cards.test.ts +109 -109
- package/src/__tests__/ccc-adapter.test.ts +114 -114
- package/src/__tests__/chatgpt-subscription-rpc.test.ts +89 -89
- package/src/__tests__/chatgpt-subscription.test.ts +135 -135
- package/src/__tests__/chrome-devtools-guard.test.ts +165 -165
- package/src/__tests__/claude-raw-stream-log.test.ts +87 -87
- package/src/__tests__/codex-raw-stream-log.test.ts +163 -163
- package/src/__tests__/config-reload.test.ts +10 -10
- package/src/__tests__/config-sample.test.ts +18 -18
- package/src/__tests__/cursor-adapter.test.ts +66 -7
- package/src/__tests__/feishu-avatar.test.ts +40 -40
- package/src/__tests__/jsonl-stream.test.ts +79 -79
- package/src/__tests__/orchestrator.test.ts +200 -200
- package/src/__tests__/raw-stream-log.test.ts +106 -106
- package/src/__tests__/session.test.ts +40 -40
- package/src/__tests__/sim-platform.test.ts +12 -12
- package/src/__tests__/web-ui.test.ts +209 -209
- package/src/adapters/ccc-adapter.ts +121 -121
- package/src/adapters/claude-adapter.ts +603 -603
- package/src/adapters/codex-adapter.ts +380 -380
- package/src/adapters/cursor-adapter.ts +39 -6
- package/src/adapters/jsonl-stream.ts +157 -157
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/agent-reload-config-rpc.ts +34 -34
- package/src/builtin/cli.ts +473 -473
- package/src/builtin/context.ts +323 -323
- package/src/builtin/file-tools.ts +1072 -1072
- package/src/builtin/index.ts +404 -404
- package/src/builtin/session-select.ts +48 -48
- package/src/builtin/sigint.ts +50 -50
- package/src/cards.ts +195 -195
- package/src/chatgpt-subscription-rpc.ts +27 -27
- package/src/chatgpt-subscription.ts +299 -299
- package/src/chrome-devtools-guard.ts +318 -318
- package/src/config.ts +125 -125
- package/src/feishu-api.ts +49 -49
- package/src/orchestrator.ts +179 -179
- package/src/runtime-reload.ts +34 -34
- package/src/session.ts +141 -141
- package/src/web-ui.ts +205 -205
|
@@ -98,10 +98,10 @@ function createHangingMockCursorProcess(args: {
|
|
|
98
98
|
pid: undefined,
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
queueMicrotask(() => {
|
|
102
102
|
if (args.stdout) stdout.write(args.stdout);
|
|
103
103
|
if (args.stderr) stderr.write(args.stderr);
|
|
104
|
-
}
|
|
104
|
+
});
|
|
105
105
|
|
|
106
106
|
return child;
|
|
107
107
|
}
|
|
@@ -744,6 +744,33 @@ describe("Cursor adapter process failures", () => {
|
|
|
744
744
|
expect(message).not.toContain("登录态已失效");
|
|
745
745
|
});
|
|
746
746
|
|
|
747
|
+
it("formats empty stdout action-required stderr with the actionable Cursor error", () => {
|
|
748
|
+
const message = formatCursorAgentEmptyOutputMessage({
|
|
749
|
+
exitCode: 1,
|
|
750
|
+
stdoutLength: 0,
|
|
751
|
+
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",
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
expect(message).toContain("底层命令异常退出");
|
|
755
|
+
expect(message).toContain("[Cursor stderr] exit=1");
|
|
756
|
+
expect(message).toContain("ActionRequiredError");
|
|
757
|
+
expect(message).toContain("unpaid invoice");
|
|
758
|
+
expect(message).toContain("team administrator");
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it("redacts obvious secrets from visible Cursor stderr", () => {
|
|
762
|
+
const message = formatCursorAgentEmptyOutputMessage({
|
|
763
|
+
exitCode: 1,
|
|
764
|
+
stdoutLength: 0,
|
|
765
|
+
stderr: "Error: failed with token=secret-token-value and Bearer abcdefghijklmnop\n",
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
expect(message).toContain("token=<redacted>");
|
|
769
|
+
expect(message).toContain("Bearer <redacted>");
|
|
770
|
+
expect(message).not.toContain("secret-token-value");
|
|
771
|
+
expect(message).not.toContain("abcdefghijklmnop");
|
|
772
|
+
});
|
|
773
|
+
|
|
747
774
|
it("does not format a message when stdout is non-empty", () => {
|
|
748
775
|
expect(
|
|
749
776
|
formatCursorAgentEmptyOutputMessage({
|
|
@@ -786,14 +813,49 @@ describe("Cursor adapter process failures", () => {
|
|
|
786
813
|
);
|
|
787
814
|
});
|
|
788
815
|
|
|
816
|
+
it("prompt surfaces action-required stderr before failing the stream", async () => {
|
|
817
|
+
const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
|
|
818
|
+
const spawnImpl = (() =>
|
|
819
|
+
createMockCursorProcess({
|
|
820
|
+
exitCode: 1,
|
|
821
|
+
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",
|
|
822
|
+
})) as CursorSpawnForTest;
|
|
823
|
+
const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
|
|
824
|
+
const iterator = adapter.prompt(
|
|
825
|
+
"sid",
|
|
826
|
+
"[User message]\nhello\n[/User message]",
|
|
827
|
+
"F:/repo",
|
|
828
|
+
)[Symbol.asyncIterator]();
|
|
829
|
+
|
|
830
|
+
const first = await iterator.next();
|
|
831
|
+
expect(first.done).toBe(false);
|
|
832
|
+
expect(first.value.blocks).toHaveLength(1);
|
|
833
|
+
expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
|
|
834
|
+
expect(first.value.blocks[0]).toHaveProperty(
|
|
835
|
+
"text",
|
|
836
|
+
expect.stringContaining("ActionRequiredError"),
|
|
837
|
+
);
|
|
838
|
+
expect(first.value.blocks[0]).toHaveProperty(
|
|
839
|
+
"text",
|
|
840
|
+
expect.stringContaining("unpaid invoice"),
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
await expect(iterator.next()).rejects.toThrow(
|
|
844
|
+
/Cursor Agent exited without stream-json output/,
|
|
845
|
+
);
|
|
846
|
+
});
|
|
847
|
+
|
|
789
848
|
it("prompt fails when Cursor emits bad JSON and then leaves stdout idle", async () => {
|
|
790
|
-
vi.useFakeTimers();
|
|
791
849
|
const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
|
|
792
850
|
const spawnImpl = (() =>
|
|
793
851
|
createHangingMockCursorProcess({
|
|
794
852
|
stdout: "{\"type\":\"tool_call\",\"subtype\":\"started\"\n",
|
|
795
853
|
})) as CursorSpawnForTest;
|
|
796
|
-
const adapter = createCursorAdapter({
|
|
854
|
+
const adapter = createCursorAdapter({
|
|
855
|
+
metaStore: store,
|
|
856
|
+
spawn: spawnImpl,
|
|
857
|
+
badJsonIdleTimeoutMs: 10,
|
|
858
|
+
});
|
|
797
859
|
|
|
798
860
|
const pending = (async () => {
|
|
799
861
|
for await (const _event of adapter.prompt(
|
|
@@ -805,9 +867,6 @@ describe("Cursor adapter process failures", () => {
|
|
|
805
867
|
}
|
|
806
868
|
})().catch((error: unknown) => error);
|
|
807
869
|
|
|
808
|
-
await vi.advanceTimersByTimeAsync(0);
|
|
809
|
-
await vi.advanceTimersByTimeAsync(120_000);
|
|
810
|
-
|
|
811
870
|
const error = await pending;
|
|
812
871
|
expect(error).toBeInstanceOf(BadJsonIdleTimeoutError);
|
|
813
872
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
|
|
@@ -68,7 +68,7 @@ function mockAvatarFetch(uploadedNames: string[], usageResponse: Response): void
|
|
|
68
68
|
}));
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
function mockAvatarUploadOnlyFetch(uploadedNames: string[]): ReturnType<typeof vi.fn> {
|
|
71
|
+
function mockAvatarUploadOnlyFetch(uploadedNames: string[]): ReturnType<typeof vi.fn> {
|
|
72
72
|
const fetchMock = vi.fn(async (url: string | URL | Request, init?: RequestInit) => {
|
|
73
73
|
const urlText = String(url);
|
|
74
74
|
if (urlText === "https://open.feishu.test/im/v1/images") {
|
|
@@ -83,44 +83,44 @@ function mockAvatarUploadOnlyFetch(uploadedNames: string[]): ReturnType<typeof v
|
|
|
83
83
|
throw new Error(`unexpected fetch: ${urlText}`);
|
|
84
84
|
});
|
|
85
85
|
vi.stubGlobal("fetch", fetchMock);
|
|
86
|
-
return fetchMock;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
describe("Plain avatar fallback", () => {
|
|
90
|
-
afterEach(() => {
|
|
91
|
-
vi.unstubAllGlobals();
|
|
92
|
-
vi.doUnmock("node:os");
|
|
93
|
-
vi.doUnmock("../config.ts");
|
|
94
|
-
vi.doUnmock("../cursor-usage.ts");
|
|
95
|
-
vi.restoreAllMocks();
|
|
96
|
-
getCursorUsageSummaryMock.mockReset();
|
|
97
|
-
mockConfig.cursor.avatarBatteryMode = "apiPercent";
|
|
98
|
-
mockConfig.cursor.onDemandMonthlyBudget = 1000;
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("uses a status-only avatar for unknown tools instead of falling back to Claude", async () => {
|
|
102
|
-
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
103
|
-
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
104
|
-
const uploadedNames: string[] = [];
|
|
105
|
-
mockAvatarUploadOnlyFetch(uploadedNames);
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
109
|
-
await setChatAvatar("tenant-token", "chat_1", "ccc", "new");
|
|
110
|
-
|
|
111
|
-
expect(uploadedNames).toEqual(["avatar_plain_new.jpg"]);
|
|
112
|
-
const cacheRaw = await readFile(join(userDataDir, "state", "avatar-image-keys.json"), "utf-8");
|
|
113
|
-
const cache = JSON.parse(cacheRaw) as Record<string, string>;
|
|
114
|
-
expect(cache["plain:new"]).toBe("img_test");
|
|
115
|
-
expect(cache["claude:new"]).toBeUndefined();
|
|
116
|
-
} finally {
|
|
117
|
-
await rm(homeDir, { recursive: true, force: true });
|
|
118
|
-
await rm(userDataDir, { recursive: true, force: true });
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe("Codex avatar usage battery", () => {
|
|
86
|
+
return fetchMock;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe("Plain avatar fallback", () => {
|
|
90
|
+
afterEach(() => {
|
|
91
|
+
vi.unstubAllGlobals();
|
|
92
|
+
vi.doUnmock("node:os");
|
|
93
|
+
vi.doUnmock("../config.ts");
|
|
94
|
+
vi.doUnmock("../cursor-usage.ts");
|
|
95
|
+
vi.restoreAllMocks();
|
|
96
|
+
getCursorUsageSummaryMock.mockReset();
|
|
97
|
+
mockConfig.cursor.avatarBatteryMode = "apiPercent";
|
|
98
|
+
mockConfig.cursor.onDemandMonthlyBudget = 1000;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("uses a status-only avatar for unknown tools instead of falling back to Claude", async () => {
|
|
102
|
+
const homeDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-home-"));
|
|
103
|
+
const userDataDir = await mkdtemp(join(tmpdir(), "chatccc-avatar-data-"));
|
|
104
|
+
const uploadedNames: string[] = [];
|
|
105
|
+
mockAvatarUploadOnlyFetch(uploadedNames);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const { setChatAvatar } = await loadFeishuApiWithHome(homeDir, userDataDir);
|
|
109
|
+
await setChatAvatar("tenant-token", "chat_1", "ccc", "new");
|
|
110
|
+
|
|
111
|
+
expect(uploadedNames).toEqual(["avatar_plain_new.jpg"]);
|
|
112
|
+
const cacheRaw = await readFile(join(userDataDir, "state", "avatar-image-keys.json"), "utf-8");
|
|
113
|
+
const cache = JSON.parse(cacheRaw) as Record<string, string>;
|
|
114
|
+
expect(cache["plain:new"]).toBe("img_test");
|
|
115
|
+
expect(cache["claude:new"]).toBeUndefined();
|
|
116
|
+
} finally {
|
|
117
|
+
await rm(homeDir, { recursive: true, force: true });
|
|
118
|
+
await rm(userDataDir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("Codex avatar usage battery", () => {
|
|
124
124
|
afterEach(() => {
|
|
125
125
|
vi.unstubAllGlobals();
|
|
126
126
|
vi.doUnmock("node:os");
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import { PassThrough } from "node:stream";
|
|
2
|
-
|
|
3
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
BadJsonIdleTimeoutError,
|
|
7
|
-
readJsonLinesWithBadJsonIdleWatchdog,
|
|
8
|
-
} from "../adapters/jsonl-stream.ts";
|
|
9
|
-
|
|
10
|
-
function createReader(input: PassThrough, idleTimeoutMs = 100): AsyncIterator<unknown> {
|
|
11
|
-
return readJsonLinesWithBadJsonIdleWatchdog({
|
|
12
|
-
input,
|
|
13
|
-
tool: "test-agent",
|
|
14
|
-
tag: "sid-test",
|
|
15
|
-
idleTimeoutMs,
|
|
16
|
-
parse: (line) => JSON.parse(line) as unknown,
|
|
17
|
-
})[Symbol.asyncIterator]();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
vi.useRealTimers();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe("readJsonLinesWithBadJsonIdleWatchdog", () => {
|
|
25
|
-
it("throws when a JSON-like bad line remains the last stdout past the idle timeout", async () => {
|
|
26
|
-
vi.useFakeTimers();
|
|
27
|
-
const input = new PassThrough();
|
|
28
|
-
const iterator = createReader(input);
|
|
29
|
-
|
|
30
|
-
const pending = iterator.next().catch((error: unknown) => error);
|
|
31
|
-
input.write("{\"type\":\"tool_call\",\"subtype\":\"started\"\n");
|
|
32
|
-
|
|
33
|
-
await vi.advanceTimersByTimeAsync(100);
|
|
34
|
-
|
|
35
|
-
const error = await pending;
|
|
36
|
-
expect(error).toBeInstanceOf(BadJsonIdleTimeoutError);
|
|
37
|
-
expect(error).toMatchObject({
|
|
38
|
-
code: "BAD_JSON_IDLE_TIMEOUT",
|
|
39
|
-
tool: "test-agent",
|
|
40
|
-
tag: "sid-test",
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("does not throw when a valid JSON line arrives after the bad line before timeout", async () => {
|
|
45
|
-
vi.useFakeTimers();
|
|
46
|
-
const input = new PassThrough();
|
|
47
|
-
const iterator = createReader(input);
|
|
48
|
-
|
|
49
|
-
const pending = iterator.next();
|
|
50
|
-
input.write("{\"type\":\"tool_call\"\n");
|
|
51
|
-
await vi.advanceTimersByTimeAsync(50);
|
|
52
|
-
input.write("{\"type\":\"ok\"}\n");
|
|
53
|
-
|
|
54
|
-
await expect(pending).resolves.toEqual({
|
|
55
|
-
done: false,
|
|
56
|
-
value: { type: "ok" },
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const done = iterator.next();
|
|
60
|
-
input.end();
|
|
61
|
-
await expect(done).resolves.toEqual({ done: true, value: undefined });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("ignores non-JSON banner lines for the bad JSON idle watchdog", async () => {
|
|
65
|
-
vi.useFakeTimers();
|
|
66
|
-
const input = new PassThrough();
|
|
67
|
-
const iterator = createReader(input);
|
|
68
|
-
|
|
69
|
-
const pending = iterator.next();
|
|
70
|
-
input.write("Reading prompt from stdin...\n");
|
|
71
|
-
await vi.advanceTimersByTimeAsync(200);
|
|
72
|
-
input.write("{\"type\":\"ok\"}\n");
|
|
73
|
-
|
|
74
|
-
await expect(pending).resolves.toEqual({
|
|
75
|
-
done: false,
|
|
76
|
-
value: { type: "ok" },
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
});
|
|
1
|
+
import { PassThrough } from "node:stream";
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BadJsonIdleTimeoutError,
|
|
7
|
+
readJsonLinesWithBadJsonIdleWatchdog,
|
|
8
|
+
} from "../adapters/jsonl-stream.ts";
|
|
9
|
+
|
|
10
|
+
function createReader(input: PassThrough, idleTimeoutMs = 100): AsyncIterator<unknown> {
|
|
11
|
+
return readJsonLinesWithBadJsonIdleWatchdog({
|
|
12
|
+
input,
|
|
13
|
+
tool: "test-agent",
|
|
14
|
+
tag: "sid-test",
|
|
15
|
+
idleTimeoutMs,
|
|
16
|
+
parse: (line) => JSON.parse(line) as unknown,
|
|
17
|
+
})[Symbol.asyncIterator]();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.useRealTimers();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("readJsonLinesWithBadJsonIdleWatchdog", () => {
|
|
25
|
+
it("throws when a JSON-like bad line remains the last stdout past the idle timeout", async () => {
|
|
26
|
+
vi.useFakeTimers();
|
|
27
|
+
const input = new PassThrough();
|
|
28
|
+
const iterator = createReader(input);
|
|
29
|
+
|
|
30
|
+
const pending = iterator.next().catch((error: unknown) => error);
|
|
31
|
+
input.write("{\"type\":\"tool_call\",\"subtype\":\"started\"\n");
|
|
32
|
+
|
|
33
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
34
|
+
|
|
35
|
+
const error = await pending;
|
|
36
|
+
expect(error).toBeInstanceOf(BadJsonIdleTimeoutError);
|
|
37
|
+
expect(error).toMatchObject({
|
|
38
|
+
code: "BAD_JSON_IDLE_TIMEOUT",
|
|
39
|
+
tool: "test-agent",
|
|
40
|
+
tag: "sid-test",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("does not throw when a valid JSON line arrives after the bad line before timeout", async () => {
|
|
45
|
+
vi.useFakeTimers();
|
|
46
|
+
const input = new PassThrough();
|
|
47
|
+
const iterator = createReader(input);
|
|
48
|
+
|
|
49
|
+
const pending = iterator.next();
|
|
50
|
+
input.write("{\"type\":\"tool_call\"\n");
|
|
51
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
52
|
+
input.write("{\"type\":\"ok\"}\n");
|
|
53
|
+
|
|
54
|
+
await expect(pending).resolves.toEqual({
|
|
55
|
+
done: false,
|
|
56
|
+
value: { type: "ok" },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const done = iterator.next();
|
|
60
|
+
input.end();
|
|
61
|
+
await expect(done).resolves.toEqual({ done: true, value: undefined });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("ignores non-JSON banner lines for the bad JSON idle watchdog", async () => {
|
|
65
|
+
vi.useFakeTimers();
|
|
66
|
+
const input = new PassThrough();
|
|
67
|
+
const iterator = createReader(input);
|
|
68
|
+
|
|
69
|
+
const pending = iterator.next();
|
|
70
|
+
input.write("Reading prompt from stdin...\n");
|
|
71
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
72
|
+
input.write("{\"type\":\"ok\"}\n");
|
|
73
|
+
|
|
74
|
+
await expect(pending).resolves.toEqual({
|
|
75
|
+
done: false,
|
|
76
|
+
value: { type: "ok" },
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|