@zzusp/ccsm 1.0.1 → 1.0.2
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/LICENSE +21 -21
- package/README.md +236 -236
- package/bin/cli.mjs +52 -52
- package/dist/assets/{DiskUsage-CKhggLs5.js → DiskUsage-BY6XwffG.js} +2 -2
- package/dist/assets/DiskUsage-BY6XwffG.js.map +1 -0
- package/dist/assets/{ImportPage-wge4VhZ-.js → ImportPage-Cwq5bx7G.js} +2 -2
- package/dist/assets/ImportPage-Cwq5bx7G.js.map +1 -0
- package/dist/assets/MarkdownContent-BFu7Nkk_.js +2 -0
- package/dist/assets/MarkdownContent-BFu7Nkk_.js.map +1 -0
- package/dist/assets/{ProjectMemory-Q4XX40j_.js → ProjectMemory-CcE3KbUK.js} +2 -2
- package/dist/assets/ProjectMemory-CcE3KbUK.js.map +1 -0
- package/dist/assets/index-CrWxV6sb.css +1 -0
- package/dist/assets/index-DTbWl1jb.js +11 -0
- package/dist/assets/index-DTbWl1jb.js.map +1 -0
- package/dist/assets/markdown-Bag5rX3T.js +30 -0
- package/dist/assets/markdown-Bag5rX3T.js.map +1 -0
- package/dist/index.html +26 -26
- package/package.json +85 -83
- package/server/index.ts +130 -130
- package/server/lib/active-sessions.test.ts +119 -119
- package/server/lib/active-sessions.ts +95 -95
- package/server/lib/bundle.test.ts +182 -182
- package/server/lib/bundle.ts +86 -86
- package/server/lib/claude-paths.test.ts +126 -126
- package/server/lib/claude-paths.ts +43 -43
- package/server/lib/cleanup-suggestions.ts +131 -131
- package/server/lib/constants.ts +8 -8
- package/server/lib/delete-project.ts +100 -100
- package/server/lib/delete.test.ts +244 -244
- package/server/lib/delete.ts +192 -192
- package/server/lib/disk-usage.ts +81 -81
- package/server/lib/encode-cwd.ts +24 -24
- package/server/lib/export-bundle.ts +236 -236
- package/server/lib/export-import-bundle.test.ts +337 -337
- package/server/lib/fs-size.ts +38 -38
- package/server/lib/import-bundle.ts +488 -488
- package/server/lib/load-memory.ts +120 -120
- package/server/lib/load-session.ts +209 -209
- package/server/lib/modified-files.test.ts +280 -280
- package/server/lib/modified-files.ts +228 -228
- package/server/lib/open-folder.ts +47 -47
- package/server/lib/parse-jsonl.ts +160 -139
- package/server/lib/port.ts +23 -23
- package/server/lib/safe-id.test.ts +41 -41
- package/server/lib/safe-id.ts +6 -6
- package/server/lib/safe-remove.test.ts +73 -73
- package/server/lib/safe-remove.ts +25 -25
- package/server/lib/scan.ts +289 -286
- package/server/lib/search-all.ts +130 -130
- package/server/lib/search-session.ts +203 -203
- package/server/lib/system-tags.ts +20 -20
- package/server/lib/update.ts +67 -67
- package/server/lib/version.test.ts +39 -39
- package/server/lib/version.ts +117 -117
- package/server/routes/disk-cleanup.ts +54 -54
- package/server/routes/disk.ts +9 -9
- package/server/routes/import.ts +87 -87
- package/server/routes/projects.ts +104 -104
- package/server/routes/search.ts +79 -79
- package/server/routes/sessions.ts +130 -130
- package/server/routes/version.ts +34 -34
- package/server/types.ts +1 -1
- package/shared/constants.ts +7 -7
- package/shared/types.ts +513 -511
- package/dist/assets/DiskUsage-CKhggLs5.js.map +0 -1
- package/dist/assets/ImportPage-wge4VhZ-.js.map +0 -1
- package/dist/assets/ProjectMemory-Q4XX40j_.js.map +0 -1
- package/dist/assets/index-7aMrnHJG.js +0 -7
- package/dist/assets/index-7aMrnHJG.js.map +0 -1
- package/dist/assets/index-BOeI_J4B.css +0 -1
|
@@ -1,228 +1,228 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import readline from 'node:readline';
|
|
4
|
-
import { PATHS } from './claude-paths.ts';
|
|
5
|
-
import type {
|
|
6
|
-
DiffHunk,
|
|
7
|
-
ModifiedFileOperation,
|
|
8
|
-
ModifiedFileSummary,
|
|
9
|
-
ModifiedFileToolName,
|
|
10
|
-
ModifiedFilesResponse,
|
|
11
|
-
} from '../types.ts';
|
|
12
|
-
|
|
13
|
-
const FILE_MOD_TOOLS = new Set<ModifiedFileToolName>([
|
|
14
|
-
'Edit',
|
|
15
|
-
'Write',
|
|
16
|
-
'MultiEdit',
|
|
17
|
-
'NotebookEdit',
|
|
18
|
-
]);
|
|
19
|
-
|
|
20
|
-
interface PendingOp {
|
|
21
|
-
toolUseId: string;
|
|
22
|
-
toolName: ModifiedFileToolName;
|
|
23
|
-
ts: string | null;
|
|
24
|
-
messageUuid: string | null;
|
|
25
|
-
filePath: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export async function loadModifiedFiles(
|
|
29
|
-
projectId: string,
|
|
30
|
-
sessionId: string,
|
|
31
|
-
): Promise<ModifiedFilesResponse | null> {
|
|
32
|
-
const jsonlPath = path.join(PATHS.projects, projectId, `${sessionId}.jsonl`);
|
|
33
|
-
if (!fs.existsSync(jsonlPath)) return null;
|
|
34
|
-
|
|
35
|
-
const ops: PendingOp[] = [];
|
|
36
|
-
// tool_use_id → is_error;tool_result 在 jsonl 中通常出现在对应 tool_use 之后,
|
|
37
|
-
// 但不强依赖顺序——单次扫完再回填。
|
|
38
|
-
const resultErr = new Map<string, boolean>();
|
|
39
|
-
// tool_use_id → 该次工具结果的 structuredPatch(带真实行号的 hunk)。
|
|
40
|
-
// 哨兵记录在 user 消息顶层的 obj.toolUseResult 上,与同一行 content 里的 tool_result 一一对应。
|
|
41
|
-
const resultPatch = new Map<string, DiffHunk[]>();
|
|
42
|
-
let cwd: string | null = null;
|
|
43
|
-
|
|
44
|
-
const rl = readline.createInterface({
|
|
45
|
-
input: fs.createReadStream(jsonlPath, { encoding: 'utf8' }),
|
|
46
|
-
crlfDelay: Infinity,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
for await (const raw of rl) {
|
|
50
|
-
const line = raw.trim();
|
|
51
|
-
if (!line) continue;
|
|
52
|
-
let obj: Record<string, unknown>;
|
|
53
|
-
try {
|
|
54
|
-
obj = JSON.parse(line) as Record<string, unknown>;
|
|
55
|
-
} catch {
|
|
56
|
-
continue;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (typeof obj.cwd === 'string' && !cwd) cwd = obj.cwd;
|
|
60
|
-
|
|
61
|
-
if (obj.type !== 'user' && obj.type !== 'assistant') continue;
|
|
62
|
-
const message = obj.message;
|
|
63
|
-
if (!message || typeof message !== 'object') continue;
|
|
64
|
-
const content = (message as { content?: unknown }).content;
|
|
65
|
-
if (!Array.isArray(content)) continue;
|
|
66
|
-
|
|
67
|
-
const ts = typeof obj.timestamp === 'string' ? obj.timestamp : null;
|
|
68
|
-
const messageUuid = typeof obj.uuid === 'string' ? obj.uuid : null;
|
|
69
|
-
|
|
70
|
-
for (const block of content) {
|
|
71
|
-
if (!block || typeof block !== 'object') continue;
|
|
72
|
-
const b = block as Record<string, unknown>;
|
|
73
|
-
if (b.type === 'tool_use') {
|
|
74
|
-
const name = b.name;
|
|
75
|
-
if (typeof name !== 'string') continue;
|
|
76
|
-
if (!FILE_MOD_TOOLS.has(name as ModifiedFileToolName)) continue;
|
|
77
|
-
const input = b.input;
|
|
78
|
-
if (!input || typeof input !== 'object') continue;
|
|
79
|
-
const filePath = extractFilePath(input as Record<string, unknown>);
|
|
80
|
-
if (!filePath) continue;
|
|
81
|
-
const id = typeof b.id === 'string' ? b.id : '';
|
|
82
|
-
if (!id) continue;
|
|
83
|
-
ops.push({
|
|
84
|
-
toolUseId: id,
|
|
85
|
-
toolName: name as ModifiedFileToolName,
|
|
86
|
-
ts,
|
|
87
|
-
messageUuid,
|
|
88
|
-
filePath,
|
|
89
|
-
});
|
|
90
|
-
} else if (b.type === 'tool_result') {
|
|
91
|
-
const id = b.tool_use_id;
|
|
92
|
-
if (typeof id !== 'string' || !id) continue;
|
|
93
|
-
// 同一 tool_use_id 理论上只对应一条 result;以首次出现为准。
|
|
94
|
-
if (!resultErr.has(id)) resultErr.set(id, b.is_error === true);
|
|
95
|
-
if (!resultPatch.has(id)) {
|
|
96
|
-
const patch = extractStructuredPatch(obj.toolUseResult);
|
|
97
|
-
if (patch) resultPatch.set(id, patch);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// 按 filePath 聚合
|
|
104
|
-
const byPath = new Map<string, ModifiedFileSummary>();
|
|
105
|
-
for (const op of ops) {
|
|
106
|
-
const errored = resultErr.get(op.toolUseId) === true;
|
|
107
|
-
const pending = !resultErr.has(op.toolUseId);
|
|
108
|
-
const operation: ModifiedFileOperation = {
|
|
109
|
-
toolUseId: op.toolUseId,
|
|
110
|
-
toolName: op.toolName,
|
|
111
|
-
ts: op.ts,
|
|
112
|
-
messageUuid: op.messageUuid,
|
|
113
|
-
errored,
|
|
114
|
-
pending,
|
|
115
|
-
structuredPatch: resultPatch.has(op.toolUseId) ? resultPatch.get(op.toolUseId)! : null,
|
|
116
|
-
};
|
|
117
|
-
let summary = byPath.get(op.filePath);
|
|
118
|
-
if (!summary) {
|
|
119
|
-
summary = {
|
|
120
|
-
filePath: op.filePath,
|
|
121
|
-
relativePath: null,
|
|
122
|
-
editCount: 0,
|
|
123
|
-
writeCount: 0,
|
|
124
|
-
multiEditCount: 0,
|
|
125
|
-
notebookEditCount: 0,
|
|
126
|
-
totalCount: 0,
|
|
127
|
-
errorCount: 0,
|
|
128
|
-
firstAt: null,
|
|
129
|
-
lastAt: null,
|
|
130
|
-
operations: [],
|
|
131
|
-
};
|
|
132
|
-
byPath.set(op.filePath, summary);
|
|
133
|
-
}
|
|
134
|
-
summary.operations.push(operation);
|
|
135
|
-
summary.totalCount += 1;
|
|
136
|
-
if (errored) summary.errorCount += 1;
|
|
137
|
-
switch (op.toolName) {
|
|
138
|
-
case 'Edit':
|
|
139
|
-
summary.editCount += 1;
|
|
140
|
-
break;
|
|
141
|
-
case 'Write':
|
|
142
|
-
summary.writeCount += 1;
|
|
143
|
-
break;
|
|
144
|
-
case 'MultiEdit':
|
|
145
|
-
summary.multiEditCount += 1;
|
|
146
|
-
break;
|
|
147
|
-
case 'NotebookEdit':
|
|
148
|
-
summary.notebookEditCount += 1;
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
if (op.ts) {
|
|
152
|
-
if (!summary.firstAt || op.ts < summary.firstAt) summary.firstAt = op.ts;
|
|
153
|
-
if (!summary.lastAt || op.ts > summary.lastAt) summary.lastAt = op.ts;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
for (const summary of byPath.values()) {
|
|
158
|
-
summary.operations.sort(compareByTs);
|
|
159
|
-
summary.relativePath = relativizeIfUnder(summary.filePath, cwd);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const files = Array.from(byPath.values()).sort((a, b) => {
|
|
163
|
-
// lastAt desc,null 排末尾
|
|
164
|
-
if (a.lastAt && b.lastAt) return a.lastAt < b.lastAt ? 1 : a.lastAt > b.lastAt ? -1 : 0;
|
|
165
|
-
if (a.lastAt) return -1;
|
|
166
|
-
if (b.lastAt) return 1;
|
|
167
|
-
return a.filePath.localeCompare(b.filePath);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
return { sessionId, projectId, cwd, files };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** Pull the structuredPatch hunks out of a `toolUseResult` sentinel. Returns an
|
|
174
|
-
* empty array for a brand-new file (Write/NotebookEdit create, where Claude Code
|
|
175
|
-
* records `structuredPatch: []`), and null when the field is absent/malformed. */
|
|
176
|
-
function extractStructuredPatch(tur: unknown): DiffHunk[] | null {
|
|
177
|
-
if (!tur || typeof tur !== 'object') return null;
|
|
178
|
-
const sp = (tur as Record<string, unknown>).structuredPatch;
|
|
179
|
-
if (!Array.isArray(sp)) return null;
|
|
180
|
-
const hunks: DiffHunk[] = [];
|
|
181
|
-
for (const h of sp) {
|
|
182
|
-
if (!h || typeof h !== 'object') continue;
|
|
183
|
-
const r = h as Record<string, unknown>;
|
|
184
|
-
if (
|
|
185
|
-
typeof r.oldStart === 'number' &&
|
|
186
|
-
typeof r.oldLines === 'number' &&
|
|
187
|
-
typeof r.newStart === 'number' &&
|
|
188
|
-
typeof r.newLines === 'number' &&
|
|
189
|
-
Array.isArray(r.lines)
|
|
190
|
-
) {
|
|
191
|
-
hunks.push({
|
|
192
|
-
oldStart: r.oldStart,
|
|
193
|
-
oldLines: r.oldLines,
|
|
194
|
-
newStart: r.newStart,
|
|
195
|
-
newLines: r.newLines,
|
|
196
|
-
lines: r.lines.filter((l): l is string => typeof l === 'string'),
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return hunks;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function extractFilePath(input: Record<string, unknown>): string | null {
|
|
204
|
-
const fp = input.file_path;
|
|
205
|
-
if (typeof fp === 'string' && fp) return fp;
|
|
206
|
-
// NotebookEdit uses notebook_path.
|
|
207
|
-
const np = input.notebook_path;
|
|
208
|
-
if (typeof np === 'string' && np) return np;
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function relativizeIfUnder(filePath: string, cwd: string | null): string | null {
|
|
213
|
-
if (!cwd) return null;
|
|
214
|
-
// 用 posix-style 简单前缀判断即可——session 是在 macOS/Linux/Windows
|
|
215
|
-
// 各自原生路径下记录的 cwd,不跨平台。
|
|
216
|
-
const normCwd = cwd.replace(/[\\/]+$/, '');
|
|
217
|
-
if (filePath === normCwd) return '.';
|
|
218
|
-
if (filePath.startsWith(normCwd + '/')) return filePath.slice(normCwd.length + 1);
|
|
219
|
-
if (filePath.startsWith(normCwd + '\\')) return filePath.slice(normCwd.length + 1);
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function compareByTs(a: ModifiedFileOperation, b: ModifiedFileOperation): number {
|
|
224
|
-
if (a.ts && b.ts) return a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0;
|
|
225
|
-
if (a.ts) return -1;
|
|
226
|
-
if (b.ts) return 1;
|
|
227
|
-
return 0;
|
|
228
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
import { PATHS } from './claude-paths.ts';
|
|
5
|
+
import type {
|
|
6
|
+
DiffHunk,
|
|
7
|
+
ModifiedFileOperation,
|
|
8
|
+
ModifiedFileSummary,
|
|
9
|
+
ModifiedFileToolName,
|
|
10
|
+
ModifiedFilesResponse,
|
|
11
|
+
} from '../types.ts';
|
|
12
|
+
|
|
13
|
+
const FILE_MOD_TOOLS = new Set<ModifiedFileToolName>([
|
|
14
|
+
'Edit',
|
|
15
|
+
'Write',
|
|
16
|
+
'MultiEdit',
|
|
17
|
+
'NotebookEdit',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
interface PendingOp {
|
|
21
|
+
toolUseId: string;
|
|
22
|
+
toolName: ModifiedFileToolName;
|
|
23
|
+
ts: string | null;
|
|
24
|
+
messageUuid: string | null;
|
|
25
|
+
filePath: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function loadModifiedFiles(
|
|
29
|
+
projectId: string,
|
|
30
|
+
sessionId: string,
|
|
31
|
+
): Promise<ModifiedFilesResponse | null> {
|
|
32
|
+
const jsonlPath = path.join(PATHS.projects, projectId, `${sessionId}.jsonl`);
|
|
33
|
+
if (!fs.existsSync(jsonlPath)) return null;
|
|
34
|
+
|
|
35
|
+
const ops: PendingOp[] = [];
|
|
36
|
+
// tool_use_id → is_error;tool_result 在 jsonl 中通常出现在对应 tool_use 之后,
|
|
37
|
+
// 但不强依赖顺序——单次扫完再回填。
|
|
38
|
+
const resultErr = new Map<string, boolean>();
|
|
39
|
+
// tool_use_id → 该次工具结果的 structuredPatch(带真实行号的 hunk)。
|
|
40
|
+
// 哨兵记录在 user 消息顶层的 obj.toolUseResult 上,与同一行 content 里的 tool_result 一一对应。
|
|
41
|
+
const resultPatch = new Map<string, DiffHunk[]>();
|
|
42
|
+
let cwd: string | null = null;
|
|
43
|
+
|
|
44
|
+
const rl = readline.createInterface({
|
|
45
|
+
input: fs.createReadStream(jsonlPath, { encoding: 'utf8' }),
|
|
46
|
+
crlfDelay: Infinity,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
for await (const raw of rl) {
|
|
50
|
+
const line = raw.trim();
|
|
51
|
+
if (!line) continue;
|
|
52
|
+
let obj: Record<string, unknown>;
|
|
53
|
+
try {
|
|
54
|
+
obj = JSON.parse(line) as Record<string, unknown>;
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof obj.cwd === 'string' && !cwd) cwd = obj.cwd;
|
|
60
|
+
|
|
61
|
+
if (obj.type !== 'user' && obj.type !== 'assistant') continue;
|
|
62
|
+
const message = obj.message;
|
|
63
|
+
if (!message || typeof message !== 'object') continue;
|
|
64
|
+
const content = (message as { content?: unknown }).content;
|
|
65
|
+
if (!Array.isArray(content)) continue;
|
|
66
|
+
|
|
67
|
+
const ts = typeof obj.timestamp === 'string' ? obj.timestamp : null;
|
|
68
|
+
const messageUuid = typeof obj.uuid === 'string' ? obj.uuid : null;
|
|
69
|
+
|
|
70
|
+
for (const block of content) {
|
|
71
|
+
if (!block || typeof block !== 'object') continue;
|
|
72
|
+
const b = block as Record<string, unknown>;
|
|
73
|
+
if (b.type === 'tool_use') {
|
|
74
|
+
const name = b.name;
|
|
75
|
+
if (typeof name !== 'string') continue;
|
|
76
|
+
if (!FILE_MOD_TOOLS.has(name as ModifiedFileToolName)) continue;
|
|
77
|
+
const input = b.input;
|
|
78
|
+
if (!input || typeof input !== 'object') continue;
|
|
79
|
+
const filePath = extractFilePath(input as Record<string, unknown>);
|
|
80
|
+
if (!filePath) continue;
|
|
81
|
+
const id = typeof b.id === 'string' ? b.id : '';
|
|
82
|
+
if (!id) continue;
|
|
83
|
+
ops.push({
|
|
84
|
+
toolUseId: id,
|
|
85
|
+
toolName: name as ModifiedFileToolName,
|
|
86
|
+
ts,
|
|
87
|
+
messageUuid,
|
|
88
|
+
filePath,
|
|
89
|
+
});
|
|
90
|
+
} else if (b.type === 'tool_result') {
|
|
91
|
+
const id = b.tool_use_id;
|
|
92
|
+
if (typeof id !== 'string' || !id) continue;
|
|
93
|
+
// 同一 tool_use_id 理论上只对应一条 result;以首次出现为准。
|
|
94
|
+
if (!resultErr.has(id)) resultErr.set(id, b.is_error === true);
|
|
95
|
+
if (!resultPatch.has(id)) {
|
|
96
|
+
const patch = extractStructuredPatch(obj.toolUseResult);
|
|
97
|
+
if (patch) resultPatch.set(id, patch);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 按 filePath 聚合
|
|
104
|
+
const byPath = new Map<string, ModifiedFileSummary>();
|
|
105
|
+
for (const op of ops) {
|
|
106
|
+
const errored = resultErr.get(op.toolUseId) === true;
|
|
107
|
+
const pending = !resultErr.has(op.toolUseId);
|
|
108
|
+
const operation: ModifiedFileOperation = {
|
|
109
|
+
toolUseId: op.toolUseId,
|
|
110
|
+
toolName: op.toolName,
|
|
111
|
+
ts: op.ts,
|
|
112
|
+
messageUuid: op.messageUuid,
|
|
113
|
+
errored,
|
|
114
|
+
pending,
|
|
115
|
+
structuredPatch: resultPatch.has(op.toolUseId) ? resultPatch.get(op.toolUseId)! : null,
|
|
116
|
+
};
|
|
117
|
+
let summary = byPath.get(op.filePath);
|
|
118
|
+
if (!summary) {
|
|
119
|
+
summary = {
|
|
120
|
+
filePath: op.filePath,
|
|
121
|
+
relativePath: null,
|
|
122
|
+
editCount: 0,
|
|
123
|
+
writeCount: 0,
|
|
124
|
+
multiEditCount: 0,
|
|
125
|
+
notebookEditCount: 0,
|
|
126
|
+
totalCount: 0,
|
|
127
|
+
errorCount: 0,
|
|
128
|
+
firstAt: null,
|
|
129
|
+
lastAt: null,
|
|
130
|
+
operations: [],
|
|
131
|
+
};
|
|
132
|
+
byPath.set(op.filePath, summary);
|
|
133
|
+
}
|
|
134
|
+
summary.operations.push(operation);
|
|
135
|
+
summary.totalCount += 1;
|
|
136
|
+
if (errored) summary.errorCount += 1;
|
|
137
|
+
switch (op.toolName) {
|
|
138
|
+
case 'Edit':
|
|
139
|
+
summary.editCount += 1;
|
|
140
|
+
break;
|
|
141
|
+
case 'Write':
|
|
142
|
+
summary.writeCount += 1;
|
|
143
|
+
break;
|
|
144
|
+
case 'MultiEdit':
|
|
145
|
+
summary.multiEditCount += 1;
|
|
146
|
+
break;
|
|
147
|
+
case 'NotebookEdit':
|
|
148
|
+
summary.notebookEditCount += 1;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
if (op.ts) {
|
|
152
|
+
if (!summary.firstAt || op.ts < summary.firstAt) summary.firstAt = op.ts;
|
|
153
|
+
if (!summary.lastAt || op.ts > summary.lastAt) summary.lastAt = op.ts;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const summary of byPath.values()) {
|
|
158
|
+
summary.operations.sort(compareByTs);
|
|
159
|
+
summary.relativePath = relativizeIfUnder(summary.filePath, cwd);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const files = Array.from(byPath.values()).sort((a, b) => {
|
|
163
|
+
// lastAt desc,null 排末尾
|
|
164
|
+
if (a.lastAt && b.lastAt) return a.lastAt < b.lastAt ? 1 : a.lastAt > b.lastAt ? -1 : 0;
|
|
165
|
+
if (a.lastAt) return -1;
|
|
166
|
+
if (b.lastAt) return 1;
|
|
167
|
+
return a.filePath.localeCompare(b.filePath);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return { sessionId, projectId, cwd, files };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Pull the structuredPatch hunks out of a `toolUseResult` sentinel. Returns an
|
|
174
|
+
* empty array for a brand-new file (Write/NotebookEdit create, where Claude Code
|
|
175
|
+
* records `structuredPatch: []`), and null when the field is absent/malformed. */
|
|
176
|
+
function extractStructuredPatch(tur: unknown): DiffHunk[] | null {
|
|
177
|
+
if (!tur || typeof tur !== 'object') return null;
|
|
178
|
+
const sp = (tur as Record<string, unknown>).structuredPatch;
|
|
179
|
+
if (!Array.isArray(sp)) return null;
|
|
180
|
+
const hunks: DiffHunk[] = [];
|
|
181
|
+
for (const h of sp) {
|
|
182
|
+
if (!h || typeof h !== 'object') continue;
|
|
183
|
+
const r = h as Record<string, unknown>;
|
|
184
|
+
if (
|
|
185
|
+
typeof r.oldStart === 'number' &&
|
|
186
|
+
typeof r.oldLines === 'number' &&
|
|
187
|
+
typeof r.newStart === 'number' &&
|
|
188
|
+
typeof r.newLines === 'number' &&
|
|
189
|
+
Array.isArray(r.lines)
|
|
190
|
+
) {
|
|
191
|
+
hunks.push({
|
|
192
|
+
oldStart: r.oldStart,
|
|
193
|
+
oldLines: r.oldLines,
|
|
194
|
+
newStart: r.newStart,
|
|
195
|
+
newLines: r.newLines,
|
|
196
|
+
lines: r.lines.filter((l): l is string => typeof l === 'string'),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return hunks;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function extractFilePath(input: Record<string, unknown>): string | null {
|
|
204
|
+
const fp = input.file_path;
|
|
205
|
+
if (typeof fp === 'string' && fp) return fp;
|
|
206
|
+
// NotebookEdit uses notebook_path.
|
|
207
|
+
const np = input.notebook_path;
|
|
208
|
+
if (typeof np === 'string' && np) return np;
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function relativizeIfUnder(filePath: string, cwd: string | null): string | null {
|
|
213
|
+
if (!cwd) return null;
|
|
214
|
+
// 用 posix-style 简单前缀判断即可——session 是在 macOS/Linux/Windows
|
|
215
|
+
// 各自原生路径下记录的 cwd,不跨平台。
|
|
216
|
+
const normCwd = cwd.replace(/[\\/]+$/, '');
|
|
217
|
+
if (filePath === normCwd) return '.';
|
|
218
|
+
if (filePath.startsWith(normCwd + '/')) return filePath.slice(normCwd.length + 1);
|
|
219
|
+
if (filePath.startsWith(normCwd + '\\')) return filePath.slice(normCwd.length + 1);
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function compareByTs(a: ModifiedFileOperation, b: ModifiedFileOperation): number {
|
|
224
|
+
if (a.ts && b.ts) return a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0;
|
|
225
|
+
if (a.ts) return -1;
|
|
226
|
+
if (b.ts) return 1;
|
|
227
|
+
return 0;
|
|
228
|
+
}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
|
|
4
|
-
export interface OpenResult {
|
|
5
|
-
ok: boolean;
|
|
6
|
-
error?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function openFolder(folderPath: string): OpenResult {
|
|
10
|
-
try {
|
|
11
|
-
const st = fs.statSync(folderPath);
|
|
12
|
-
if (!st.isDirectory()) return { ok: false, error: 'not a directory' };
|
|
13
|
-
} catch {
|
|
14
|
-
return { ok: false, error: 'path not found' };
|
|
15
|
-
}
|
|
16
|
-
return launch(folderPath);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function openFile(filePath: string): OpenResult {
|
|
20
|
-
try {
|
|
21
|
-
const st = fs.statSync(filePath);
|
|
22
|
-
if (!st.isFile()) return { ok: false, error: 'not a file' };
|
|
23
|
-
} catch {
|
|
24
|
-
return { ok: false, error: 'path not found' };
|
|
25
|
-
}
|
|
26
|
-
return launch(filePath);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// 交给系统默认程序打开(文件 → 关联应用;文件夹 → 资源管理器),等价于双击。
|
|
30
|
-
// detached + unref,不阻塞、不等子进程;spawn 失败在异步回调里只记日志。
|
|
31
|
-
function launch(target: string): OpenResult {
|
|
32
|
-
let cmd: string;
|
|
33
|
-
if (process.platform === 'win32') cmd = 'explorer.exe';
|
|
34
|
-
else if (process.platform === 'darwin') cmd = 'open';
|
|
35
|
-
else cmd = 'xdg-open';
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const child = spawn(cmd, [target], { detached: true, stdio: 'ignore' });
|
|
39
|
-
child.on('error', (err) => {
|
|
40
|
-
console.error(`[open] spawn ${cmd} failed:`, err);
|
|
41
|
-
});
|
|
42
|
-
child.unref();
|
|
43
|
-
return { ok: true };
|
|
44
|
-
} catch (err) {
|
|
45
|
-
return { ok: false, error: (err as Error).message };
|
|
46
|
-
}
|
|
47
|
-
}
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
export interface OpenResult {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function openFolder(folderPath: string): OpenResult {
|
|
10
|
+
try {
|
|
11
|
+
const st = fs.statSync(folderPath);
|
|
12
|
+
if (!st.isDirectory()) return { ok: false, error: 'not a directory' };
|
|
13
|
+
} catch {
|
|
14
|
+
return { ok: false, error: 'path not found' };
|
|
15
|
+
}
|
|
16
|
+
return launch(folderPath);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function openFile(filePath: string): OpenResult {
|
|
20
|
+
try {
|
|
21
|
+
const st = fs.statSync(filePath);
|
|
22
|
+
if (!st.isFile()) return { ok: false, error: 'not a file' };
|
|
23
|
+
} catch {
|
|
24
|
+
return { ok: false, error: 'path not found' };
|
|
25
|
+
}
|
|
26
|
+
return launch(filePath);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 交给系统默认程序打开(文件 → 关联应用;文件夹 → 资源管理器),等价于双击。
|
|
30
|
+
// detached + unref,不阻塞、不等子进程;spawn 失败在异步回调里只记日志。
|
|
31
|
+
function launch(target: string): OpenResult {
|
|
32
|
+
let cmd: string;
|
|
33
|
+
if (process.platform === 'win32') cmd = 'explorer.exe';
|
|
34
|
+
else if (process.platform === 'darwin') cmd = 'open';
|
|
35
|
+
else cmd = 'xdg-open';
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const child = spawn(cmd, [target], { detached: true, stdio: 'ignore' });
|
|
39
|
+
child.on('error', (err) => {
|
|
40
|
+
console.error(`[open] spawn ${cmd} failed:`, err);
|
|
41
|
+
});
|
|
42
|
+
child.unref();
|
|
43
|
+
return { ok: true };
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return { ok: false, error: (err as Error).message };
|
|
46
|
+
}
|
|
47
|
+
}
|