chatccc 0.2.196 → 0.2.197
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 -0
- package/src/__tests__/builtin-chat-session.test.ts +277 -181
- 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 -196
- 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 -113
- 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 -0
- package/src/__tests__/codex-raw-stream-log.test.ts +120 -0
- 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 +113 -113
- package/src/__tests__/feishu-avatar.test.ts +40 -40
- package/src/__tests__/orchestrator.test.ts +181 -154
- 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 -119
- package/src/adapters/claude-adapter.ts +603 -566
- package/src/adapters/codex-adapter.ts +57 -32
- package/src/adapters/cursor-adapter.ts +264 -264
- package/src/adapters/raw-stream-log.ts +124 -124
- package/src/agent-reload-config-rpc.ts +34 -0
- package/src/builtin/cli.ts +473 -461
- package/src/builtin/context.ts +323 -323
- package/src/builtin/file-tools.ts +1072 -915
- package/src/builtin/index.ts +404 -353
- 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/index.ts +8 -13
- package/src/orchestrator.ts +166 -145
- package/src/runtime-reload.ts +34 -0
- package/src/session.ts +141 -141
- package/src/web-ui.ts +205 -205
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { describe, it, expect } 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 type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
|
|
1
|
+
import { describe, it, expect } 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 type { UnifiedStreamMessage } from "../adapters/adapter-interface.ts";
|
|
14
14
|
import type {
|
|
15
15
|
CursorSessionMeta,
|
|
16
16
|
CursorSessionMetaStore,
|
|
@@ -44,43 +44,43 @@ function createInMemoryMetaStore(
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
48
|
-
|
|
49
|
-
type CursorSpawnForTest = NonNullable<
|
|
50
|
-
NonNullable<Parameters<typeof createCursorAdapter>[0]>["spawn"]
|
|
51
|
-
>;
|
|
52
|
-
|
|
53
|
-
function readFixture(name: string): unknown[] {
|
|
54
|
-
const raw = readFileSync(join(__dirname, "fixtures", name), "utf-8");
|
|
55
|
-
return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function createMockCursorProcess(args: {
|
|
59
|
-
stdout?: string;
|
|
60
|
-
stderr?: string;
|
|
61
|
-
exitCode?: number;
|
|
62
|
-
}): ChildProcess {
|
|
63
|
-
const child = new EventEmitter() as ChildProcess;
|
|
64
|
-
const stdout = new PassThrough();
|
|
65
|
-
const stderr = new PassThrough();
|
|
66
|
-
const stdin = new PassThrough();
|
|
67
|
-
Object.assign(child, {
|
|
68
|
-
stdout,
|
|
69
|
-
stderr,
|
|
70
|
-
stdin,
|
|
71
|
-
pid: undefined,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
setImmediate(() => {
|
|
75
|
-
if (args.stdout) stdout.write(args.stdout);
|
|
76
|
-
if (args.stderr) stderr.write(args.stderr);
|
|
77
|
-
stdout.end();
|
|
78
|
-
stderr.end();
|
|
79
|
-
child.emit("close", args.exitCode ?? 0, null);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
return child;
|
|
83
|
-
}
|
|
47
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
|
|
49
|
+
type CursorSpawnForTest = NonNullable<
|
|
50
|
+
NonNullable<Parameters<typeof createCursorAdapter>[0]>["spawn"]
|
|
51
|
+
>;
|
|
52
|
+
|
|
53
|
+
function readFixture(name: string): unknown[] {
|
|
54
|
+
const raw = readFileSync(join(__dirname, "fixtures", name), "utf-8");
|
|
55
|
+
return raw.split(/\r?\n/).filter(Boolean).map((line) => JSON.parse(line));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createMockCursorProcess(args: {
|
|
59
|
+
stdout?: string;
|
|
60
|
+
stderr?: string;
|
|
61
|
+
exitCode?: number;
|
|
62
|
+
}): ChildProcess {
|
|
63
|
+
const child = new EventEmitter() as ChildProcess;
|
|
64
|
+
const stdout = new PassThrough();
|
|
65
|
+
const stderr = new PassThrough();
|
|
66
|
+
const stdin = new PassThrough();
|
|
67
|
+
Object.assign(child, {
|
|
68
|
+
stdout,
|
|
69
|
+
stderr,
|
|
70
|
+
stdin,
|
|
71
|
+
pid: undefined,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
setImmediate(() => {
|
|
75
|
+
if (args.stdout) stdout.write(args.stdout);
|
|
76
|
+
if (args.stderr) stderr.write(args.stderr);
|
|
77
|
+
stdout.end();
|
|
78
|
+
stderr.end();
|
|
79
|
+
child.emit("close", args.exitCode ?? 0, null);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return child;
|
|
83
|
+
}
|
|
84
84
|
|
|
85
85
|
// ---------------------------------------------------------------------------
|
|
86
86
|
// normalizeCursorMessage — 核心映射逻辑测试(纯函数)
|
|
@@ -680,7 +680,7 @@ describe("Cursor stream fixture - 端到端不重复", () => {
|
|
|
680
680
|
});
|
|
681
681
|
});
|
|
682
682
|
|
|
683
|
-
it("mapToolCallKey: maps known keys to readable names", () => {
|
|
683
|
+
it("mapToolCallKey: maps known keys to readable names", () => {
|
|
684
684
|
const cases: [string, string][] = [
|
|
685
685
|
["globToolCall", "Glob"],
|
|
686
686
|
["shellToolCall", "Bash"],
|
|
@@ -696,65 +696,65 @@ describe("Cursor stream fixture - 端到端不重复", () => {
|
|
|
696
696
|
tool_call: { [raw]: { args: {} } },
|
|
697
697
|
} as Parameters<typeof normalizeCursorMessage>[0]);
|
|
698
698
|
expect(r!.blocks[0]).toMatchObject({ type: "tool_use", name: expected });
|
|
699
|
-
}
|
|
700
|
-
});
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
describe("Cursor adapter process failures", () => {
|
|
704
|
-
it("formats empty stdout auth stderr as a cautious visible message", () => {
|
|
705
|
-
const message = formatCursorAgentEmptyOutputMessage({
|
|
706
|
-
exitCode: 1,
|
|
707
|
-
stdoutLength: 0,
|
|
708
|
-
stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
expect(message).toContain("检测到认证相关错误");
|
|
712
|
-
expect(message).toContain("可能需要重新登录 Cursor Agent");
|
|
713
|
-
expect(message).toContain("CURSOR_API_KEY");
|
|
714
|
-
expect(message).toContain("agent status");
|
|
715
|
-
expect(message).toContain("agent login");
|
|
716
|
-
expect(message).not.toContain("登录态已失效");
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
it("does not format a message when stdout is non-empty", () => {
|
|
720
|
-
expect(
|
|
721
|
-
formatCursorAgentEmptyOutputMessage({
|
|
722
|
-
exitCode: 1,
|
|
723
|
-
stdoutLength: 10,
|
|
724
|
-
stderr: "Error: Authentication required",
|
|
725
|
-
}),
|
|
726
|
-
).toBeNull();
|
|
727
|
-
});
|
|
728
|
-
|
|
729
|
-
it("prompt surfaces auth-related empty output before failing the stream", async () => {
|
|
730
|
-
const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
|
|
731
|
-
const spawnImpl = (() =>
|
|
732
|
-
createMockCursorProcess({
|
|
733
|
-
exitCode: 1,
|
|
734
|
-
stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
|
|
735
|
-
})) as CursorSpawnForTest;
|
|
736
|
-
const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
|
|
737
|
-
const iterator = adapter.prompt(
|
|
738
|
-
"sid",
|
|
739
|
-
"[User message]\nhello\n[/User message]",
|
|
740
|
-
"F:/repo",
|
|
741
|
-
)[Symbol.asyncIterator]();
|
|
742
|
-
|
|
743
|
-
const first = await iterator.next();
|
|
744
|
-
expect(first.done).toBe(false);
|
|
745
|
-
expect(first.value.blocks).toHaveLength(1);
|
|
746
|
-
expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
|
|
747
|
-
expect(first.value.blocks[0]).toHaveProperty(
|
|
748
|
-
"text",
|
|
749
|
-
expect.stringContaining("可能需要重新登录 Cursor Agent"),
|
|
750
|
-
);
|
|
751
|
-
expect(first.value.blocks[0]).toHaveProperty(
|
|
752
|
-
"text",
|
|
753
|
-
expect.not.stringContaining("登录态已失效"),
|
|
754
|
-
);
|
|
755
|
-
|
|
756
|
-
await expect(iterator.next()).rejects.toThrow(
|
|
757
|
-
/Cursor Agent exited without stream-json output/,
|
|
758
|
-
);
|
|
759
|
-
});
|
|
760
|
-
});
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
describe("Cursor adapter process failures", () => {
|
|
704
|
+
it("formats empty stdout auth stderr as a cautious visible message", () => {
|
|
705
|
+
const message = formatCursorAgentEmptyOutputMessage({
|
|
706
|
+
exitCode: 1,
|
|
707
|
+
stdoutLength: 0,
|
|
708
|
+
stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
expect(message).toContain("检测到认证相关错误");
|
|
712
|
+
expect(message).toContain("可能需要重新登录 Cursor Agent");
|
|
713
|
+
expect(message).toContain("CURSOR_API_KEY");
|
|
714
|
+
expect(message).toContain("agent status");
|
|
715
|
+
expect(message).toContain("agent login");
|
|
716
|
+
expect(message).not.toContain("登录态已失效");
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("does not format a message when stdout is non-empty", () => {
|
|
720
|
+
expect(
|
|
721
|
+
formatCursorAgentEmptyOutputMessage({
|
|
722
|
+
exitCode: 1,
|
|
723
|
+
stdoutLength: 10,
|
|
724
|
+
stderr: "Error: Authentication required",
|
|
725
|
+
}),
|
|
726
|
+
).toBeNull();
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it("prompt surfaces auth-related empty output before failing the stream", async () => {
|
|
730
|
+
const store = createInMemoryMetaStore({ sid: { cwd: "F:/repo" } });
|
|
731
|
+
const spawnImpl = (() =>
|
|
732
|
+
createMockCursorProcess({
|
|
733
|
+
exitCode: 1,
|
|
734
|
+
stderr: "Error: Authentication required. Please run 'agent login' first, or set CURSOR_API_KEY environment variable.\n",
|
|
735
|
+
})) as CursorSpawnForTest;
|
|
736
|
+
const adapter = createCursorAdapter({ metaStore: store, spawn: spawnImpl });
|
|
737
|
+
const iterator = adapter.prompt(
|
|
738
|
+
"sid",
|
|
739
|
+
"[User message]\nhello\n[/User message]",
|
|
740
|
+
"F:/repo",
|
|
741
|
+
)[Symbol.asyncIterator]();
|
|
742
|
+
|
|
743
|
+
const first = await iterator.next();
|
|
744
|
+
expect(first.done).toBe(false);
|
|
745
|
+
expect(first.value.blocks).toHaveLength(1);
|
|
746
|
+
expect(first.value.blocks[0]).toMatchObject({ type: "text_final" });
|
|
747
|
+
expect(first.value.blocks[0]).toHaveProperty(
|
|
748
|
+
"text",
|
|
749
|
+
expect.stringContaining("可能需要重新登录 Cursor Agent"),
|
|
750
|
+
);
|
|
751
|
+
expect(first.value.blocks[0]).toHaveProperty(
|
|
752
|
+
"text",
|
|
753
|
+
expect.not.stringContaining("登录态已失效"),
|
|
754
|
+
);
|
|
755
|
+
|
|
756
|
+
await expect(iterator.next()).rejects.toThrow(
|
|
757
|
+
/Cursor Agent exited without stream-json output/,
|
|
758
|
+
);
|
|
759
|
+
});
|
|
760
|
+
});
|
|
@@ -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");
|