chatccc 0.2.195 → 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 +99 -2
- 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 +182 -57
- 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 +132 -130
- package/src/web-ui.ts +205 -205
|
@@ -1,124 +1,124 @@
|
|
|
1
|
-
import { createWriteStream, type Dirent } from "node:fs";
|
|
2
|
-
import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
|
|
3
|
-
import { once } from "node:events";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { createGzip } from "node:zlib";
|
|
6
|
-
|
|
7
|
-
export interface RawStreamLogOptions {
|
|
8
|
-
enabled: boolean;
|
|
9
|
-
rootDir: string;
|
|
10
|
-
tool: string;
|
|
11
|
-
sessionId: string;
|
|
12
|
-
label: string;
|
|
13
|
-
maxBytesPerTurn: number;
|
|
14
|
-
retentionDays: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface RawStreamLogHandle {
|
|
18
|
-
filePath: string;
|
|
19
|
-
writeLine(line: string): void;
|
|
20
|
-
close(options: { keep: boolean }): Promise<void>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function sanitizeLogPathSegment(value: string): string {
|
|
24
|
-
const safe = value.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^[._]+|_+$/g, "");
|
|
25
|
-
return safe || "unknown";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function cleanupOldRawStreamLogs(rootDir: string, retentionDays: number): Promise<void> {
|
|
29
|
-
if (!Number.isFinite(retentionDays) || retentionDays <= 0) return;
|
|
30
|
-
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
31
|
-
|
|
32
|
-
const visit = async (dir: string): Promise<void> => {
|
|
33
|
-
let entries: Dirent[];
|
|
34
|
-
try {
|
|
35
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
36
|
-
} catch {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
await Promise.all(entries.map(async (entry) => {
|
|
41
|
-
const path = join(dir, entry.name);
|
|
42
|
-
if (entry.isDirectory()) {
|
|
43
|
-
await visit(path);
|
|
44
|
-
try {
|
|
45
|
-
await rm(path, { recursive: false });
|
|
46
|
-
} catch {
|
|
47
|
-
// Directory is not empty or already gone.
|
|
48
|
-
}
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (!entry.isFile()) return;
|
|
53
|
-
try {
|
|
54
|
-
const info = await stat(path);
|
|
55
|
-
if (info.mtimeMs < cutoff) await rm(path, { force: true });
|
|
56
|
-
} catch {
|
|
57
|
-
// Best-effort cleanup only.
|
|
58
|
-
}
|
|
59
|
-
}));
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
await visit(rootDir);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function createRawStreamLog(options: RawStreamLogOptions): Promise<RawStreamLogHandle | null> {
|
|
66
|
-
if (!options.enabled) return null;
|
|
67
|
-
|
|
68
|
-
const maxBytes = Math.max(0, Math.floor(options.maxBytesPerTurn));
|
|
69
|
-
const tool = sanitizeLogPathSegment(options.tool);
|
|
70
|
-
const session = sanitizeLogPathSegment(options.sessionId);
|
|
71
|
-
const label = sanitizeLogPathSegment(options.label);
|
|
72
|
-
const dir = join(options.rootDir, tool, session);
|
|
73
|
-
await mkdir(dir, { recursive: true });
|
|
74
|
-
void cleanupOldRawStreamLogs(join(options.rootDir, tool), options.retentionDays);
|
|
75
|
-
|
|
76
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
77
|
-
const filePath = join(dir, `${timestamp}-${label}.jsonl.gz`);
|
|
78
|
-
const output = createWriteStream(filePath);
|
|
79
|
-
const gzip = createGzip();
|
|
80
|
-
gzip.pipe(output);
|
|
81
|
-
|
|
82
|
-
let bytes = 0;
|
|
83
|
-
let truncated = false;
|
|
84
|
-
let ended = false;
|
|
85
|
-
|
|
86
|
-
const writeLine = (line: string): void => {
|
|
87
|
-
if (ended || truncated) return;
|
|
88
|
-
const payload = `${line}\n`;
|
|
89
|
-
const payloadBytes = Buffer.byteLength(payload, "utf-8");
|
|
90
|
-
if (maxBytes > 0 && bytes + payloadBytes > maxBytes) {
|
|
91
|
-
const marker = JSON.stringify({
|
|
92
|
-
type: "chatccc_raw_stream_log_truncated",
|
|
93
|
-
reason: "max_bytes_per_turn_exceeded",
|
|
94
|
-
maxBytesPerTurn: maxBytes,
|
|
95
|
-
writtenBytes: bytes,
|
|
96
|
-
});
|
|
97
|
-
gzip.write(`${marker}\n`);
|
|
98
|
-
truncated = true;
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
bytes += payloadBytes;
|
|
102
|
-
gzip.write(payload);
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const close = async ({ keep }: { keep: boolean }): Promise<void> => {
|
|
106
|
-
if (ended) return;
|
|
107
|
-
ended = true;
|
|
108
|
-
gzip.end();
|
|
109
|
-
try {
|
|
110
|
-
await once(output, "finish");
|
|
111
|
-
} catch {
|
|
112
|
-
// Ignore close errors; caller cannot recover from debug log failures.
|
|
113
|
-
}
|
|
114
|
-
if (!keep) {
|
|
115
|
-
try {
|
|
116
|
-
await unlink(filePath);
|
|
117
|
-
} catch {
|
|
118
|
-
// Best-effort cleanup only.
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
return { filePath, writeLine, close };
|
|
124
|
-
}
|
|
1
|
+
import { createWriteStream, type Dirent } from "node:fs";
|
|
2
|
+
import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { createGzip } from "node:zlib";
|
|
6
|
+
|
|
7
|
+
export interface RawStreamLogOptions {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
rootDir: string;
|
|
10
|
+
tool: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
label: string;
|
|
13
|
+
maxBytesPerTurn: number;
|
|
14
|
+
retentionDays: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RawStreamLogHandle {
|
|
18
|
+
filePath: string;
|
|
19
|
+
writeLine(line: string): void;
|
|
20
|
+
close(options: { keep: boolean }): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function sanitizeLogPathSegment(value: string): string {
|
|
24
|
+
const safe = value.replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^[._]+|_+$/g, "");
|
|
25
|
+
return safe || "unknown";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function cleanupOldRawStreamLogs(rootDir: string, retentionDays: number): Promise<void> {
|
|
29
|
+
if (!Number.isFinite(retentionDays) || retentionDays <= 0) return;
|
|
30
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
const visit = async (dir: string): Promise<void> => {
|
|
33
|
+
let entries: Dirent[];
|
|
34
|
+
try {
|
|
35
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
36
|
+
} catch {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await Promise.all(entries.map(async (entry) => {
|
|
41
|
+
const path = join(dir, entry.name);
|
|
42
|
+
if (entry.isDirectory()) {
|
|
43
|
+
await visit(path);
|
|
44
|
+
try {
|
|
45
|
+
await rm(path, { recursive: false });
|
|
46
|
+
} catch {
|
|
47
|
+
// Directory is not empty or already gone.
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!entry.isFile()) return;
|
|
53
|
+
try {
|
|
54
|
+
const info = await stat(path);
|
|
55
|
+
if (info.mtimeMs < cutoff) await rm(path, { force: true });
|
|
56
|
+
} catch {
|
|
57
|
+
// Best-effort cleanup only.
|
|
58
|
+
}
|
|
59
|
+
}));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
await visit(rootDir);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function createRawStreamLog(options: RawStreamLogOptions): Promise<RawStreamLogHandle | null> {
|
|
66
|
+
if (!options.enabled) return null;
|
|
67
|
+
|
|
68
|
+
const maxBytes = Math.max(0, Math.floor(options.maxBytesPerTurn));
|
|
69
|
+
const tool = sanitizeLogPathSegment(options.tool);
|
|
70
|
+
const session = sanitizeLogPathSegment(options.sessionId);
|
|
71
|
+
const label = sanitizeLogPathSegment(options.label);
|
|
72
|
+
const dir = join(options.rootDir, tool, session);
|
|
73
|
+
await mkdir(dir, { recursive: true });
|
|
74
|
+
void cleanupOldRawStreamLogs(join(options.rootDir, tool), options.retentionDays);
|
|
75
|
+
|
|
76
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
77
|
+
const filePath = join(dir, `${timestamp}-${label}.jsonl.gz`);
|
|
78
|
+
const output = createWriteStream(filePath);
|
|
79
|
+
const gzip = createGzip();
|
|
80
|
+
gzip.pipe(output);
|
|
81
|
+
|
|
82
|
+
let bytes = 0;
|
|
83
|
+
let truncated = false;
|
|
84
|
+
let ended = false;
|
|
85
|
+
|
|
86
|
+
const writeLine = (line: string): void => {
|
|
87
|
+
if (ended || truncated) return;
|
|
88
|
+
const payload = `${line}\n`;
|
|
89
|
+
const payloadBytes = Buffer.byteLength(payload, "utf-8");
|
|
90
|
+
if (maxBytes > 0 && bytes + payloadBytes > maxBytes) {
|
|
91
|
+
const marker = JSON.stringify({
|
|
92
|
+
type: "chatccc_raw_stream_log_truncated",
|
|
93
|
+
reason: "max_bytes_per_turn_exceeded",
|
|
94
|
+
maxBytesPerTurn: maxBytes,
|
|
95
|
+
writtenBytes: bytes,
|
|
96
|
+
});
|
|
97
|
+
gzip.write(`${marker}\n`);
|
|
98
|
+
truncated = true;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
bytes += payloadBytes;
|
|
102
|
+
gzip.write(payload);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const close = async ({ keep }: { keep: boolean }): Promise<void> => {
|
|
106
|
+
if (ended) return;
|
|
107
|
+
ended = true;
|
|
108
|
+
gzip.end();
|
|
109
|
+
try {
|
|
110
|
+
await once(output, "finish");
|
|
111
|
+
} catch {
|
|
112
|
+
// Ignore close errors; caller cannot recover from debug log failures.
|
|
113
|
+
}
|
|
114
|
+
if (!keep) {
|
|
115
|
+
try {
|
|
116
|
+
await unlink(filePath);
|
|
117
|
+
} catch {
|
|
118
|
+
// Best-effort cleanup only.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return { filePath, writeLine, close };
|
|
124
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
|
|
3
|
+
import { reloadRuntimeConfig, type RuntimeReloadResult } from "./runtime-reload.ts";
|
|
4
|
+
|
|
5
|
+
export const AGENT_RELOAD_CONFIG_PATH = "/api/agent/reload-config";
|
|
6
|
+
|
|
7
|
+
type ReloadRuntimeConfig = (source: string) => RuntimeReloadResult | Promise<RuntimeReloadResult>;
|
|
8
|
+
|
|
9
|
+
function jsonReply(res: ServerResponse, status: number, data: unknown): void {
|
|
10
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
11
|
+
res.end(JSON.stringify(data));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function handleAgentReloadConfigRequest(
|
|
15
|
+
req: IncomingMessage,
|
|
16
|
+
res: ServerResponse,
|
|
17
|
+
reload: ReloadRuntimeConfig = reloadRuntimeConfig,
|
|
18
|
+
): Promise<boolean> {
|
|
19
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
20
|
+
if (url.pathname !== AGENT_RELOAD_CONFIG_PATH) return false;
|
|
21
|
+
|
|
22
|
+
if (req.method !== "POST") {
|
|
23
|
+
jsonReply(res, 405, { ok: false, error: "Method not allowed" });
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = await reload("agent-reload-api");
|
|
29
|
+
jsonReply(res, 200, { ok: true, ...result });
|
|
30
|
+
} catch (err) {
|
|
31
|
+
jsonReply(res, 500, { ok: false, error: (err as Error).message });
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|