chatccc 0.2.106 → 0.2.108
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/README.md +4 -1
- package/package.json +63 -63
- package/src/__tests__/claude-adapter.test.ts +441 -441
- package/src/__tests__/config-reload.test.ts +2 -1
- package/src/__tests__/web-ui.test.ts +82 -82
- package/src/adapters/claude-adapter.ts +454 -454
- package/src/adapters/claude-session-meta-store.ts +119 -119
- package/src/config.ts +2 -2
|
@@ -1,120 +1,120 @@
|
|
|
1
|
-
// =============================================================================
|
|
2
|
-
// claude-session-meta-store.ts — Claude 会话 sessionId → meta 持久化映射
|
|
3
|
-
// =============================================================================
|
|
4
|
-
// 背景:切换到直接调用 Claude CLI 后,不再有 SDK 的 getSessionInfo 可用。
|
|
5
|
-
// ChatCCC 必须自己维护 sessionId → { cwd, model } 映射。
|
|
6
|
-
//
|
|
7
|
-
// 存储:
|
|
8
|
-
// 文件 state/claude-session-meta.json,结构:
|
|
9
|
-
// { "<sessionId>": { "cwd": "...", "model": "..." } }
|
|
10
|
-
//
|
|
11
|
-
// API 设计:
|
|
12
|
-
// set(sid, partial) → 部分合并写入;只更新非空字段,不会清空其他字段
|
|
13
|
-
// =============================================================================
|
|
14
|
-
|
|
15
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
16
|
-
import { dirname, join } from "node:path";
|
|
17
|
-
|
|
18
|
-
import { USER_DATA_DIR } from "../config.ts";
|
|
19
|
-
|
|
20
|
-
export const CLAUDE_SESSION_META_FILE = join(
|
|
21
|
-
USER_DATA_DIR,
|
|
22
|
-
"state",
|
|
23
|
-
"claude-session-meta.json",
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
export interface ClaudeSessionMeta {
|
|
27
|
-
cwd: string;
|
|
28
|
-
model?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface ClaudeSessionMetaStore {
|
|
32
|
-
get(sessionId: string): Promise<ClaudeSessionMeta | undefined>;
|
|
33
|
-
set(sessionId: string, partial: Partial<ClaudeSessionMeta>): Promise<void>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface RawEntry {
|
|
37
|
-
cwd?: string;
|
|
38
|
-
model?: string;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function isNonEmptyString(v: unknown): v is string {
|
|
42
|
-
return typeof v === "string" && v.length > 0;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function parseEntry(raw: unknown): RawEntry | null {
|
|
46
|
-
if (typeof raw === "string" && raw.length > 0) {
|
|
47
|
-
return { cwd: raw };
|
|
48
|
-
}
|
|
49
|
-
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
50
|
-
const obj = raw as Record<string, unknown>;
|
|
51
|
-
const out: RawEntry = {};
|
|
52
|
-
if (isNonEmptyString(obj.cwd)) out.cwd = obj.cwd;
|
|
53
|
-
if (isNonEmptyString(obj.model)) out.model = obj.model;
|
|
54
|
-
return out;
|
|
55
|
-
}
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function createClaudeSessionMetaStore(
|
|
60
|
-
filePath: string = CLAUDE_SESSION_META_FILE,
|
|
61
|
-
): ClaudeSessionMetaStore {
|
|
62
|
-
let cache: Record<string, RawEntry> | null = null;
|
|
63
|
-
|
|
64
|
-
async function load(): Promise<Record<string, RawEntry>> {
|
|
65
|
-
if (cache) return cache;
|
|
66
|
-
try {
|
|
67
|
-
const raw = await readFile(filePath, "utf-8");
|
|
68
|
-
const parsed = JSON.parse(raw);
|
|
69
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
70
|
-
const out: Record<string, RawEntry> = {};
|
|
71
|
-
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
|
|
72
|
-
const entry = parseEntry(v);
|
|
73
|
-
if (entry) out[k] = entry;
|
|
74
|
-
}
|
|
75
|
-
cache = out;
|
|
76
|
-
return out;
|
|
77
|
-
}
|
|
78
|
-
} catch {
|
|
79
|
-
// 文件不存在 / JSON 损坏 → 视为空映射
|
|
80
|
-
}
|
|
81
|
-
cache = {};
|
|
82
|
-
return cache;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
async get(sessionId: string): Promise<ClaudeSessionMeta | undefined> {
|
|
87
|
-
const map = await load();
|
|
88
|
-
const entry = map[sessionId];
|
|
89
|
-
if (!entry || !isNonEmptyString(entry.cwd)) return undefined;
|
|
90
|
-
return entry.model
|
|
91
|
-
? { cwd: entry.cwd, model: entry.model }
|
|
92
|
-
: { cwd: entry.cwd };
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
async set(
|
|
96
|
-
sessionId: string,
|
|
97
|
-
partial: Partial<ClaudeSessionMeta>,
|
|
98
|
-
): Promise<void> {
|
|
99
|
-
const map = await load();
|
|
100
|
-
const existing = map[sessionId] ?? {};
|
|
101
|
-
const merged: RawEntry = { ...existing };
|
|
102
|
-
if (isNonEmptyString(partial.cwd)) merged.cwd = partial.cwd;
|
|
103
|
-
if (isNonEmptyString(partial.model)) merged.model = partial.model;
|
|
104
|
-
|
|
105
|
-
if (existing.cwd === merged.cwd && existing.model === merged.model) return;
|
|
106
|
-
|
|
107
|
-
map[sessionId] = merged;
|
|
108
|
-
try {
|
|
109
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
110
|
-
await writeFile(filePath, JSON.stringify(map, null, 2), "utf-8");
|
|
111
|
-
} catch (err) {
|
|
112
|
-
console.error(
|
|
113
|
-
`[claude-session-meta] failed to persist ${filePath}: ${(err as Error).message}`,
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// claude-session-meta-store.ts — Claude 会话 sessionId → meta 持久化映射
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// 背景:切换到直接调用 Claude CLI 后,不再有 SDK 的 getSessionInfo 可用。
|
|
5
|
+
// ChatCCC 必须自己维护 sessionId → { cwd, model } 映射。
|
|
6
|
+
//
|
|
7
|
+
// 存储:
|
|
8
|
+
// 文件 state/claude-session-meta.json,结构:
|
|
9
|
+
// { "<sessionId>": { "cwd": "...", "model": "..." } }
|
|
10
|
+
//
|
|
11
|
+
// API 设计:
|
|
12
|
+
// set(sid, partial) → 部分合并写入;只更新非空字段,不会清空其他字段
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
|
|
18
|
+
import { USER_DATA_DIR } from "../config.ts";
|
|
19
|
+
|
|
20
|
+
export const CLAUDE_SESSION_META_FILE = join(
|
|
21
|
+
USER_DATA_DIR,
|
|
22
|
+
"state",
|
|
23
|
+
"claude-session-meta.json",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export interface ClaudeSessionMeta {
|
|
27
|
+
cwd: string;
|
|
28
|
+
model?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ClaudeSessionMetaStore {
|
|
32
|
+
get(sessionId: string): Promise<ClaudeSessionMeta | undefined>;
|
|
33
|
+
set(sessionId: string, partial: Partial<ClaudeSessionMeta>): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface RawEntry {
|
|
37
|
+
cwd?: string;
|
|
38
|
+
model?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isNonEmptyString(v: unknown): v is string {
|
|
42
|
+
return typeof v === "string" && v.length > 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseEntry(raw: unknown): RawEntry | null {
|
|
46
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
47
|
+
return { cwd: raw };
|
|
48
|
+
}
|
|
49
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
50
|
+
const obj = raw as Record<string, unknown>;
|
|
51
|
+
const out: RawEntry = {};
|
|
52
|
+
if (isNonEmptyString(obj.cwd)) out.cwd = obj.cwd;
|
|
53
|
+
if (isNonEmptyString(obj.model)) out.model = obj.model;
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createClaudeSessionMetaStore(
|
|
60
|
+
filePath: string = CLAUDE_SESSION_META_FILE,
|
|
61
|
+
): ClaudeSessionMetaStore {
|
|
62
|
+
let cache: Record<string, RawEntry> | null = null;
|
|
63
|
+
|
|
64
|
+
async function load(): Promise<Record<string, RawEntry>> {
|
|
65
|
+
if (cache) return cache;
|
|
66
|
+
try {
|
|
67
|
+
const raw = await readFile(filePath, "utf-8");
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
70
|
+
const out: Record<string, RawEntry> = {};
|
|
71
|
+
for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
|
|
72
|
+
const entry = parseEntry(v);
|
|
73
|
+
if (entry) out[k] = entry;
|
|
74
|
+
}
|
|
75
|
+
cache = out;
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// 文件不存在 / JSON 损坏 → 视为空映射
|
|
80
|
+
}
|
|
81
|
+
cache = {};
|
|
82
|
+
return cache;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
async get(sessionId: string): Promise<ClaudeSessionMeta | undefined> {
|
|
87
|
+
const map = await load();
|
|
88
|
+
const entry = map[sessionId];
|
|
89
|
+
if (!entry || !isNonEmptyString(entry.cwd)) return undefined;
|
|
90
|
+
return entry.model
|
|
91
|
+
? { cwd: entry.cwd, model: entry.model }
|
|
92
|
+
: { cwd: entry.cwd };
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
async set(
|
|
96
|
+
sessionId: string,
|
|
97
|
+
partial: Partial<ClaudeSessionMeta>,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
const map = await load();
|
|
100
|
+
const existing = map[sessionId] ?? {};
|
|
101
|
+
const merged: RawEntry = { ...existing };
|
|
102
|
+
if (isNonEmptyString(partial.cwd)) merged.cwd = partial.cwd;
|
|
103
|
+
if (isNonEmptyString(partial.model)) merged.model = partial.model;
|
|
104
|
+
|
|
105
|
+
if (existing.cwd === merged.cwd && existing.model === merged.model) return;
|
|
106
|
+
|
|
107
|
+
map[sessionId] = merged;
|
|
108
|
+
try {
|
|
109
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
110
|
+
await writeFile(filePath, JSON.stringify(map, null, 2), "utf-8");
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(
|
|
113
|
+
`[claude-session-meta] failed to persist ${filePath}: ${(err as Error).message}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
120
|
export const defaultClaudeSessionMetaStore = createClaudeSessionMetaStore();
|
package/src/config.ts
CHANGED
|
@@ -555,7 +555,7 @@ function detectCursorAgent(): string {
|
|
|
555
555
|
export let CURSOR_AGENT_COMMAND = detectCursorAgent();
|
|
556
556
|
|
|
557
557
|
function resolveCursorAgentArgs(): string[] {
|
|
558
|
-
let args = "-p --force --output-format stream-json --stream-partial-output";
|
|
558
|
+
let args = "-p --force --approve-mcps --output-format stream-json --stream-partial-output";
|
|
559
559
|
const model = config.cursor.model;
|
|
560
560
|
if (model.trim() !== "") {
|
|
561
561
|
args += ` --model ${model}`;
|
|
@@ -563,7 +563,7 @@ function resolveCursorAgentArgs(): string[] {
|
|
|
563
563
|
return args.split(/\s+/).filter(Boolean);
|
|
564
564
|
}
|
|
565
565
|
|
|
566
|
-
/** Cursor agent 参数:-p 非交互模式,--force
|
|
566
|
+
/** Cursor agent 参数:-p 非交互模式,--force 强制允许命令,--approve-mcps 自动批准 MCP,stream-json 流式 JSONL 输出 */
|
|
567
567
|
export let CURSOR_AGENT_ARGS = resolveCursorAgentArgs();
|
|
568
568
|
|
|
569
569
|
// ---------------------------------------------------------------------------
|