linkshell-cli 0.3.28 → 0.4.0
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/dist/cli/src/commands/setup.js +20 -2
- package/dist/cli/src/commands/setup.js.map +1 -1
- package/dist/cli/src/commands/upgrade.js +9 -24
- package/dist/cli/src/commands/upgrade.js.map +1 -1
- package/dist/cli/src/index.js +28 -26
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/providers.d.ts +3 -7
- package/dist/cli/src/providers.js +76 -19
- package/dist/cli/src/providers.js.map +1 -1
- package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -10
- package/dist/cli/src/runtime/acp/acp-client.js +20 -146
- package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-session.d.ts +62 -0
- package/dist/cli/src/runtime/acp/agent-session.js +1075 -0
- package/dist/cli/src/runtime/acp/agent-session.js.map +1 -0
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +2 -27
- package/dist/cli/src/runtime/acp/agent-workspace.js +224 -1008
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +1 -3
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +50 -82
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +1 -2
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +36 -54
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
- package/dist/cli/src/runtime/acp/json-rpc.d.ts +1 -34
- package/dist/cli/src/runtime/acp/json-rpc.js +8 -32
- package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -3
- package/dist/cli/src/runtime/acp/provider-resolver.js +6 -26
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +31 -1
- package/dist/cli/src/runtime/bridge-session.js +991 -81
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/src/runtime/screen-fallback.d.ts +1 -1
- package/dist/cli/src/runtime/screen-fallback.js +4 -4
- package/dist/cli/src/runtime/screen-fallback.js.map +1 -1
- package/dist/cli/src/runtime/screen-share.d.ts +1 -1
- package/dist/cli/src/runtime/screen-share.js +7 -7
- package/dist/cli/src/runtime/screen-share.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/shared-protocol/src/index.d.ts +11978 -3423
- package/dist/shared-protocol/src/index.js +114 -163
- package/dist/shared-protocol/src/index.js.map +1 -1
- package/package.json +13 -13
- package/src/commands/setup.ts +31 -5
- package/src/commands/upgrade.ts +8 -28
- package/src/index.ts +34 -29
- package/src/providers.ts +108 -26
- package/src/runtime/acp/acp-client.ts +22 -159
- package/src/runtime/acp/agent-session.ts +1180 -0
- package/src/runtime/acp/agent-workspace.ts +246 -1094
- package/src/runtime/acp/claude-sdk-client.ts +51 -86
- package/src/runtime/acp/claude-stream-json-client.ts +42 -59
- package/src/runtime/acp/json-rpc.ts +14 -46
- package/src/runtime/acp/provider-resolver.ts +6 -29
- package/src/runtime/bridge-session.ts +1092 -84
- package/src/runtime/screen-fallback.ts +5 -5
- package/src/runtime/screen-share.ts +8 -8
- package/src/runtime/acp/claude-sessions.ts +0 -563
- package/src/runtime/acp/codex-rpc-bridge.ts +0 -233
- package/src/runtime/acp/codex-sessions.ts +0 -802
- package/src/types/linkshell-gateway.d.ts +0 -18
|
@@ -10,7 +10,7 @@ export interface ScreenFallbackOptions {
|
|
|
10
10
|
fps: number;
|
|
11
11
|
quality: number;
|
|
12
12
|
scale: number;
|
|
13
|
-
|
|
13
|
+
sessionId: string;
|
|
14
14
|
onFrame: (envelope: Envelope) => void;
|
|
15
15
|
onStatus: (envelope: Envelope) => void;
|
|
16
16
|
}
|
|
@@ -38,7 +38,7 @@ export class ScreenFallback {
|
|
|
38
38
|
this.options.onStatus(
|
|
39
39
|
createEnvelope({
|
|
40
40
|
type: "screen.status",
|
|
41
|
-
|
|
41
|
+
sessionId: this.options.sessionId,
|
|
42
42
|
payload: { active: true, mode: "fallback" as const },
|
|
43
43
|
}),
|
|
44
44
|
);
|
|
@@ -64,7 +64,7 @@ export class ScreenFallback {
|
|
|
64
64
|
this.options.onStatus(
|
|
65
65
|
createEnvelope({
|
|
66
66
|
type: "screen.status",
|
|
67
|
-
|
|
67
|
+
sessionId: this.options.sessionId,
|
|
68
68
|
payload: { active: false, mode: "off" as const },
|
|
69
69
|
}),
|
|
70
70
|
);
|
|
@@ -88,7 +88,7 @@ export class ScreenFallback {
|
|
|
88
88
|
this.options.onFrame(
|
|
89
89
|
createEnvelope({
|
|
90
90
|
type: "screen.frame",
|
|
91
|
-
|
|
91
|
+
sessionId: this.options.sessionId,
|
|
92
92
|
payload: {
|
|
93
93
|
data: base64,
|
|
94
94
|
width: dims.width,
|
|
@@ -108,7 +108,7 @@ export class ScreenFallback {
|
|
|
108
108
|
this.options.onFrame(
|
|
109
109
|
createEnvelope({
|
|
110
110
|
type: "screen.frame",
|
|
111
|
-
|
|
111
|
+
sessionId: this.options.sessionId,
|
|
112
112
|
payload: {
|
|
113
113
|
data: chunk,
|
|
114
114
|
width: dims.width,
|
|
@@ -22,7 +22,7 @@ function detectMacScreenIndex(): string {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export interface ScreenShareOptions {
|
|
25
|
-
|
|
25
|
+
sessionId: string;
|
|
26
26
|
fps: number;
|
|
27
27
|
quality: number;
|
|
28
28
|
scale: number;
|
|
@@ -111,7 +111,7 @@ export class ScreenShare {
|
|
|
111
111
|
this.options.onSignal(
|
|
112
112
|
createEnvelope({
|
|
113
113
|
type: "screen.ice",
|
|
114
|
-
|
|
114
|
+
sessionId: this.options.sessionId,
|
|
115
115
|
payload: {
|
|
116
116
|
candidate: candidate.candidate,
|
|
117
117
|
sdpMid: candidate.sdpMid ?? null,
|
|
@@ -128,7 +128,7 @@ export class ScreenShare {
|
|
|
128
128
|
this.options.onStatus(
|
|
129
129
|
createEnvelope({
|
|
130
130
|
type: "screen.status",
|
|
131
|
-
|
|
131
|
+
sessionId: this.options.sessionId,
|
|
132
132
|
payload: { active: true, mode: "webrtc" as const },
|
|
133
133
|
}),
|
|
134
134
|
);
|
|
@@ -136,7 +136,7 @@ export class ScreenShare {
|
|
|
136
136
|
this.options.onStatus(
|
|
137
137
|
createEnvelope({
|
|
138
138
|
type: "screen.status",
|
|
139
|
-
|
|
139
|
+
sessionId: this.options.sessionId,
|
|
140
140
|
payload: { active: false, mode: "off" as const, error: `WebRTC ${state}` },
|
|
141
141
|
}),
|
|
142
142
|
);
|
|
@@ -152,7 +152,7 @@ export class ScreenShare {
|
|
|
152
152
|
this.options.onStatus(
|
|
153
153
|
createEnvelope({
|
|
154
154
|
type: "screen.status",
|
|
155
|
-
|
|
155
|
+
sessionId: this.options.sessionId,
|
|
156
156
|
payload: { active: true, mode: "webrtc" as const },
|
|
157
157
|
}),
|
|
158
158
|
);
|
|
@@ -160,7 +160,7 @@ export class ScreenShare {
|
|
|
160
160
|
this.options.onSignal(
|
|
161
161
|
createEnvelope({
|
|
162
162
|
type: "screen.offer",
|
|
163
|
-
|
|
163
|
+
sessionId: this.options.sessionId,
|
|
164
164
|
payload: { sdp: offer.sdp },
|
|
165
165
|
}),
|
|
166
166
|
);
|
|
@@ -172,7 +172,7 @@ export class ScreenShare {
|
|
|
172
172
|
this.options.onStatus(
|
|
173
173
|
createEnvelope({
|
|
174
174
|
type: "screen.status",
|
|
175
|
-
|
|
175
|
+
sessionId: this.options.sessionId,
|
|
176
176
|
payload: {
|
|
177
177
|
active: false,
|
|
178
178
|
mode: "off" as const,
|
|
@@ -221,7 +221,7 @@ export class ScreenShare {
|
|
|
221
221
|
this.options.onStatus(
|
|
222
222
|
createEnvelope({
|
|
223
223
|
type: "screen.status",
|
|
224
|
-
|
|
224
|
+
sessionId: this.options.sessionId,
|
|
225
225
|
payload: { active: false, mode: "off" as const },
|
|
226
226
|
}),
|
|
227
227
|
);
|
|
@@ -1,563 +0,0 @@
|
|
|
1
|
-
import { closeSync, existsSync, openSync, readFileSync, readdirSync, readSync, statSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { basename, join, resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
const SAMPLE_BYTES = 64 * 1024;
|
|
6
|
-
const HISTORY_BYTES = 2 * 1024 * 1024;
|
|
7
|
-
const MAX_SESSIONS = 200;
|
|
8
|
-
const MAX_HISTORY_ITEMS = 200;
|
|
9
|
-
|
|
10
|
-
export interface ClaudeStoredSession {
|
|
11
|
-
id: string;
|
|
12
|
-
cwd: string;
|
|
13
|
-
title?: string;
|
|
14
|
-
createdAt?: number;
|
|
15
|
-
lastModified: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface StoredAgentTimelineItem {
|
|
19
|
-
id: string;
|
|
20
|
-
conversationId: string;
|
|
21
|
-
type: "message" | "tool_call";
|
|
22
|
-
kind?: "tool_activity" | "command_execution" | "file_change";
|
|
23
|
-
itemId?: string;
|
|
24
|
-
role?: "user" | "assistant" | "system";
|
|
25
|
-
content?: Array<{ type: "text"; text: string }>;
|
|
26
|
-
text?: string;
|
|
27
|
-
toolCall?: {
|
|
28
|
-
id: string;
|
|
29
|
-
name: string;
|
|
30
|
-
input?: string;
|
|
31
|
-
output?: string;
|
|
32
|
-
createdAt?: number;
|
|
33
|
-
status: "pending" | "running" | "completed" | "failed";
|
|
34
|
-
};
|
|
35
|
-
commandExecution?: {
|
|
36
|
-
command?: string;
|
|
37
|
-
cwd?: string;
|
|
38
|
-
output?: string;
|
|
39
|
-
exitCode?: number | null;
|
|
40
|
-
status?: "pending" | "running" | "completed" | "failed";
|
|
41
|
-
};
|
|
42
|
-
fileChange?: {
|
|
43
|
-
entries: Array<{
|
|
44
|
-
path: string;
|
|
45
|
-
kind?: string;
|
|
46
|
-
added?: number;
|
|
47
|
-
removed?: number;
|
|
48
|
-
}>;
|
|
49
|
-
diff?: string;
|
|
50
|
-
summary?: string;
|
|
51
|
-
status?: "pending" | "running" | "completed" | "failed";
|
|
52
|
-
};
|
|
53
|
-
createdAt: number;
|
|
54
|
-
updatedAt?: number;
|
|
55
|
-
metadata?: Record<string, unknown>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
59
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
60
|
-
? value as Record<string, unknown>
|
|
61
|
-
: undefined;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function parseTimestamp(value: unknown): number | undefined {
|
|
65
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
66
|
-
return value > 10_000_000_000 ? value : value * 1000;
|
|
67
|
-
}
|
|
68
|
-
if (typeof value === "string" && value.trim()) {
|
|
69
|
-
const parsed = Date.parse(value);
|
|
70
|
-
return Number.isNaN(parsed) ? undefined : parsed;
|
|
71
|
-
}
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function normalizeTitle(value: string | undefined): string | undefined {
|
|
76
|
-
const compact = value?.replace(/\s+/g, " ").trim();
|
|
77
|
-
if (!compact) return undefined;
|
|
78
|
-
return compact.length > 80 ? `${compact.slice(0, 77)}...` : compact;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function findStringDeep(value: unknown, keys: string[], depth = 0): string | undefined {
|
|
82
|
-
if (depth > 4) return undefined;
|
|
83
|
-
const record = asRecord(value);
|
|
84
|
-
if (!record) return undefined;
|
|
85
|
-
for (const key of keys) {
|
|
86
|
-
const candidate = record[key];
|
|
87
|
-
if (typeof candidate === "string" && candidate.trim()) return candidate;
|
|
88
|
-
}
|
|
89
|
-
for (const candidate of Object.values(record)) {
|
|
90
|
-
if (candidate && typeof candidate === "object") {
|
|
91
|
-
const found = findStringDeep(candidate, keys, depth + 1);
|
|
92
|
-
if (found) return found;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function extractMessageText(value: unknown): string | undefined {
|
|
99
|
-
if (typeof value === "string") return normalizeTitle(value);
|
|
100
|
-
if (Array.isArray(value)) {
|
|
101
|
-
const text = value
|
|
102
|
-
.map((part) => {
|
|
103
|
-
if (typeof part === "string") return part;
|
|
104
|
-
const record = asRecord(part);
|
|
105
|
-
if (record?.type === "tool_use" || record?.type === "tool_result") return "";
|
|
106
|
-
return typeof record?.text === "string" ? record.text : "";
|
|
107
|
-
})
|
|
108
|
-
.join(" ");
|
|
109
|
-
return normalizeTitle(text);
|
|
110
|
-
}
|
|
111
|
-
const record = asRecord(value);
|
|
112
|
-
if (!record) return undefined;
|
|
113
|
-
return extractMessageText(record.content ?? record.text);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function extractHistoryText(value: unknown): string | undefined {
|
|
117
|
-
if (typeof value === "string") {
|
|
118
|
-
const text = value.replace(/\r\n/g, "\n").trim();
|
|
119
|
-
return text || undefined;
|
|
120
|
-
}
|
|
121
|
-
if (Array.isArray(value)) {
|
|
122
|
-
const text = value
|
|
123
|
-
.map((part) => {
|
|
124
|
-
if (typeof part === "string") return part;
|
|
125
|
-
const record = asRecord(part);
|
|
126
|
-
return typeof record?.text === "string"
|
|
127
|
-
? record.text
|
|
128
|
-
: typeof record?.content === "string"
|
|
129
|
-
? record.content
|
|
130
|
-
: "";
|
|
131
|
-
})
|
|
132
|
-
.filter(Boolean)
|
|
133
|
-
.join("\n")
|
|
134
|
-
.trim();
|
|
135
|
-
return text || undefined;
|
|
136
|
-
}
|
|
137
|
-
const record = asRecord(value);
|
|
138
|
-
if (!record) return undefined;
|
|
139
|
-
return extractHistoryText(record.content ?? record.text ?? record.message);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function extractVisibleClaudeText(value: unknown): string | undefined {
|
|
143
|
-
if (!Array.isArray(value)) return extractHistoryText(value);
|
|
144
|
-
const text = value
|
|
145
|
-
.map((part) => {
|
|
146
|
-
if (typeof part === "string") return part;
|
|
147
|
-
const record = asRecord(part);
|
|
148
|
-
if (!record || record.type === "tool_use" || record.type === "tool_result") return "";
|
|
149
|
-
return typeof record.text === "string"
|
|
150
|
-
? record.text
|
|
151
|
-
: typeof record.content === "string"
|
|
152
|
-
? record.content
|
|
153
|
-
: "";
|
|
154
|
-
})
|
|
155
|
-
.filter(Boolean)
|
|
156
|
-
.join("\n")
|
|
157
|
-
.trim();
|
|
158
|
-
return text || undefined;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function stringifyHistoryValue(value: unknown): string | undefined {
|
|
162
|
-
if (value === undefined || value === null || value === "") return undefined;
|
|
163
|
-
if (typeof value === "string") return value;
|
|
164
|
-
try {
|
|
165
|
-
return JSON.stringify(value, null, 2);
|
|
166
|
-
} catch {
|
|
167
|
-
return String(value);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function guessCwdFromProjectDir(projectDirName: string, fallbackCwd: string): string {
|
|
172
|
-
const trimmed = projectDirName.replace(/^-+/, "");
|
|
173
|
-
if (!trimmed) return resolve(fallbackCwd);
|
|
174
|
-
return `/${trimmed.split("-").filter(Boolean).join("/")}`;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function readSample(filePath: string, size: number): string {
|
|
178
|
-
let fd: number | undefined;
|
|
179
|
-
try {
|
|
180
|
-
fd = openSync(filePath, "r");
|
|
181
|
-
if (size <= SAMPLE_BYTES * 2) {
|
|
182
|
-
const buffer = Buffer.alloc(size);
|
|
183
|
-
const bytesRead = readSync(fd, buffer, 0, size, 0);
|
|
184
|
-
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const head = Buffer.alloc(SAMPLE_BYTES);
|
|
188
|
-
const tail = Buffer.alloc(SAMPLE_BYTES);
|
|
189
|
-
const headBytes = readSync(fd, head, 0, SAMPLE_BYTES, 0);
|
|
190
|
-
const tailBytes = readSync(fd, tail, 0, SAMPLE_BYTES, Math.max(0, size - SAMPLE_BYTES));
|
|
191
|
-
return `${head.subarray(0, headBytes).toString("utf8")}\n${tail.subarray(0, tailBytes).toString("utf8")}`;
|
|
192
|
-
} catch {
|
|
193
|
-
return "";
|
|
194
|
-
} finally {
|
|
195
|
-
if (typeof fd === "number") {
|
|
196
|
-
try {
|
|
197
|
-
closeSync(fd);
|
|
198
|
-
} catch {
|
|
199
|
-
// Ignore close failures while listing best-effort local history.
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function readHistorySample(filePath: string, size: number): string {
|
|
206
|
-
try {
|
|
207
|
-
if (size <= HISTORY_BYTES) return readFileSync(filePath, "utf8");
|
|
208
|
-
} catch {
|
|
209
|
-
return "";
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
let fd: number | undefined;
|
|
213
|
-
try {
|
|
214
|
-
fd = openSync(filePath, "r");
|
|
215
|
-
const buffer = Buffer.alloc(HISTORY_BYTES);
|
|
216
|
-
const bytesRead = readSync(fd, buffer, 0, HISTORY_BYTES, Math.max(0, size - HISTORY_BYTES));
|
|
217
|
-
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
218
|
-
} catch {
|
|
219
|
-
return "";
|
|
220
|
-
} finally {
|
|
221
|
-
if (typeof fd === "number") {
|
|
222
|
-
try {
|
|
223
|
-
closeSync(fd);
|
|
224
|
-
} catch {
|
|
225
|
-
// Ignore close failures while reading best-effort local history.
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function readClaudeSessionMetadata(filePath: string, fallbackCwd: string): Omit<ClaudeStoredSession, "id" | "lastModified"> & {
|
|
232
|
-
lastModified?: number;
|
|
233
|
-
} {
|
|
234
|
-
let statMtime: number | undefined;
|
|
235
|
-
let statSize = 0;
|
|
236
|
-
try {
|
|
237
|
-
const stat = statSync(filePath);
|
|
238
|
-
statMtime = stat.mtimeMs;
|
|
239
|
-
statSize = stat.size;
|
|
240
|
-
} catch {
|
|
241
|
-
// Best effort only.
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const sample = readSample(filePath, statSize);
|
|
245
|
-
let cwd: string | undefined;
|
|
246
|
-
let title: string | undefined;
|
|
247
|
-
let createdAt: number | undefined;
|
|
248
|
-
let lastActivityAt: number | undefined;
|
|
249
|
-
|
|
250
|
-
for (const line of sample.split(/\r?\n/)) {
|
|
251
|
-
const trimmed = line.trim();
|
|
252
|
-
if (!trimmed.startsWith("{")) continue;
|
|
253
|
-
try {
|
|
254
|
-
const entry = JSON.parse(trimmed) as unknown;
|
|
255
|
-
const record = asRecord(entry);
|
|
256
|
-
if (!record) continue;
|
|
257
|
-
cwd ??= findStringDeep(record, ["cwd", "workingDirectory", "workspacePath"]);
|
|
258
|
-
const timestamp = parseTimestamp(record.timestamp ?? record.createdAt ?? record.created_at);
|
|
259
|
-
createdAt ??= timestamp;
|
|
260
|
-
if (timestamp) lastActivityAt = timestamp;
|
|
261
|
-
if (!title && record.type === "user") {
|
|
262
|
-
title = extractMessageText(asRecord(record.message)?.content ?? record.content);
|
|
263
|
-
}
|
|
264
|
-
} catch {
|
|
265
|
-
// The sample may start in the middle of a JSONL line; skip partial lines.
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return {
|
|
270
|
-
cwd: cwd ?? resolve(fallbackCwd),
|
|
271
|
-
title,
|
|
272
|
-
createdAt,
|
|
273
|
-
lastModified: lastActivityAt ?? statMtime,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export function listClaudeStoredSessions(inputCwd: string): { sessions: ClaudeStoredSession[] } {
|
|
278
|
-
const root = join(homedir(), ".claude", "projects");
|
|
279
|
-
if (!existsSync(root)) return { sessions: [] };
|
|
280
|
-
|
|
281
|
-
const sessions: ClaudeStoredSession[] = [];
|
|
282
|
-
try {
|
|
283
|
-
for (const projectEntry of readdirSync(root, { withFileTypes: true })) {
|
|
284
|
-
if (!projectEntry.isDirectory()) continue;
|
|
285
|
-
const projectDir = join(root, projectEntry.name);
|
|
286
|
-
const fallbackCwd = guessCwdFromProjectDir(projectEntry.name, inputCwd);
|
|
287
|
-
for (const sessionEntry of readdirSync(projectDir, { withFileTypes: true })) {
|
|
288
|
-
if (!sessionEntry.isFile() || !sessionEntry.name.endsWith(".jsonl")) continue;
|
|
289
|
-
const filePath = join(projectDir, sessionEntry.name);
|
|
290
|
-
try {
|
|
291
|
-
const stat = statSync(filePath);
|
|
292
|
-
const metadata = readClaudeSessionMetadata(filePath, fallbackCwd);
|
|
293
|
-
sessions.push({
|
|
294
|
-
id: basename(sessionEntry.name, ".jsonl"),
|
|
295
|
-
cwd: metadata.cwd,
|
|
296
|
-
title: metadata.title,
|
|
297
|
-
createdAt: metadata.createdAt,
|
|
298
|
-
lastModified: metadata.lastModified ?? stat.mtimeMs,
|
|
299
|
-
});
|
|
300
|
-
} catch {
|
|
301
|
-
// Skip individual history files that disappear or are unreadable during the scan.
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
} catch {
|
|
306
|
-
// Ignore unreadable Claude storage; the caller treats an empty list as no local history.
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
sessions.sort((a, b) => b.lastModified - a.lastModified);
|
|
310
|
-
return { sessions: sessions.slice(0, MAX_SESSIONS) };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function findClaudeSessionFile(sessionId: string): string | undefined {
|
|
314
|
-
const root = join(homedir(), ".claude", "projects");
|
|
315
|
-
if (!existsSync(root)) return undefined;
|
|
316
|
-
try {
|
|
317
|
-
for (const projectEntry of readdirSync(root, { withFileTypes: true })) {
|
|
318
|
-
if (!projectEntry.isDirectory()) continue;
|
|
319
|
-
const projectDir = join(root, projectEntry.name);
|
|
320
|
-
for (const sessionEntry of readdirSync(projectDir, { withFileTypes: true })) {
|
|
321
|
-
if (!sessionEntry.isFile() || !sessionEntry.name.endsWith(".jsonl")) continue;
|
|
322
|
-
if (basename(sessionEntry.name, ".jsonl") === sessionId) {
|
|
323
|
-
return join(projectDir, sessionEntry.name);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
} catch {
|
|
328
|
-
return undefined;
|
|
329
|
-
}
|
|
330
|
-
return undefined;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function historyMessage(
|
|
334
|
-
conversationId: string,
|
|
335
|
-
index: number,
|
|
336
|
-
role: "user" | "assistant" | "system",
|
|
337
|
-
text: string,
|
|
338
|
-
createdAt: number,
|
|
339
|
-
source: string,
|
|
340
|
-
): StoredAgentTimelineItem {
|
|
341
|
-
return {
|
|
342
|
-
id: `history:${source}:${index}`,
|
|
343
|
-
conversationId,
|
|
344
|
-
type: "message",
|
|
345
|
-
role,
|
|
346
|
-
content: [{ type: "text", text }],
|
|
347
|
-
text,
|
|
348
|
-
createdAt,
|
|
349
|
-
metadata: { source: "device-history", provider: "claude" },
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function claudeToolName(name: string | undefined): string {
|
|
354
|
-
if (!name) return "工具";
|
|
355
|
-
if (name === "Bash") return "命令";
|
|
356
|
-
if (isClaudeFileTool(name)) return "文件修改";
|
|
357
|
-
return name;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function isClaudeFileTool(name: string | undefined): boolean {
|
|
361
|
-
return name === "Edit" || name === "MultiEdit" || name === "Write" || name === "NotebookEdit";
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function claudeToolKind(name: string | undefined): "tool_activity" | "command_execution" | "file_change" {
|
|
365
|
-
if (name === "Bash") return "command_execution";
|
|
366
|
-
if (isClaudeFileTool(name)) return "file_change";
|
|
367
|
-
return "tool_activity";
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function claudeFileEntry(name: string | undefined, input: Record<string, unknown> | undefined): StoredAgentTimelineItem["fileChange"] | undefined {
|
|
371
|
-
if (!isClaudeFileTool(name) || !input) return undefined;
|
|
372
|
-
const rawPath = input.file_path ?? input.path ?? input.notebook_path;
|
|
373
|
-
if (typeof rawPath !== "string" || !rawPath.trim()) return undefined;
|
|
374
|
-
const path = rawPath.trim();
|
|
375
|
-
const kind = name === "Write" ? "create" : "update";
|
|
376
|
-
const edits = Array.isArray(input.edits) ? input.edits.map((entry) => asRecord(entry)).filter(Boolean) : [];
|
|
377
|
-
const added = typeof input.new_string === "string"
|
|
378
|
-
? countNonEmptyLines(input.new_string)
|
|
379
|
-
: typeof input.content === "string"
|
|
380
|
-
? countNonEmptyLines(input.content)
|
|
381
|
-
: edits.length > 0
|
|
382
|
-
? edits.reduce((sum, edit) => sum + countNonEmptyLines(typeof edit?.new_string === "string" ? edit.new_string : ""), 0)
|
|
383
|
-
: undefined;
|
|
384
|
-
const removed = typeof input.old_string === "string"
|
|
385
|
-
? countNonEmptyLines(input.old_string)
|
|
386
|
-
: edits.length > 0
|
|
387
|
-
? edits.reduce((sum, edit) => sum + countNonEmptyLines(typeof edit?.old_string === "string" ? edit.old_string : ""), 0)
|
|
388
|
-
: undefined;
|
|
389
|
-
const entry: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"][number] = { path, kind };
|
|
390
|
-
if (added && added > 0) entry.added = added;
|
|
391
|
-
if (removed && removed > 0) entry.removed = removed;
|
|
392
|
-
return {
|
|
393
|
-
entries: [entry],
|
|
394
|
-
summary: [kind, path].filter(Boolean).join(" "),
|
|
395
|
-
status: "running",
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function countNonEmptyLines(value: string): number {
|
|
400
|
-
return value.split(/\r?\n/).filter((line) => line.length > 0).length;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function claudeCommandExecution(input: Record<string, unknown> | undefined): StoredAgentTimelineItem["commandExecution"] | undefined {
|
|
404
|
-
if (!input) return undefined;
|
|
405
|
-
const command = typeof input.command === "string" ? input.command : undefined;
|
|
406
|
-
const cwd = typeof input.cwd === "string" ? input.cwd : undefined;
|
|
407
|
-
if (!command && !cwd) return undefined;
|
|
408
|
-
return { command, cwd, status: "running" };
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
function createClaudeToolItem(
|
|
412
|
-
conversationId: string,
|
|
413
|
-
toolUseId: string,
|
|
414
|
-
name: string | undefined,
|
|
415
|
-
input: Record<string, unknown> | undefined,
|
|
416
|
-
createdAt: number,
|
|
417
|
-
): StoredAgentTimelineItem {
|
|
418
|
-
const kind = claudeToolKind(name);
|
|
419
|
-
const inputText = stringifyHistoryValue(input);
|
|
420
|
-
const fileChange = claudeFileEntry(name, input);
|
|
421
|
-
return {
|
|
422
|
-
id: `history-tool:${toolUseId}`,
|
|
423
|
-
conversationId,
|
|
424
|
-
type: "tool_call",
|
|
425
|
-
kind,
|
|
426
|
-
itemId: toolUseId,
|
|
427
|
-
toolCall: {
|
|
428
|
-
id: toolUseId,
|
|
429
|
-
name: claudeToolName(name),
|
|
430
|
-
input: inputText,
|
|
431
|
-
createdAt,
|
|
432
|
-
status: "running",
|
|
433
|
-
},
|
|
434
|
-
commandExecution: name === "Bash" ? claudeCommandExecution(input) : undefined,
|
|
435
|
-
fileChange,
|
|
436
|
-
text: fileChange ? `已编辑 ${fileChange.entries.length} 个文件` : undefined,
|
|
437
|
-
createdAt,
|
|
438
|
-
updatedAt: createdAt,
|
|
439
|
-
metadata: { source: "device-history", provider: "claude", toolName: name },
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function completeClaudeToolItem(
|
|
444
|
-
itemsById: Map<string, StoredAgentTimelineItem>,
|
|
445
|
-
conversationId: string,
|
|
446
|
-
toolUseId: string,
|
|
447
|
-
output: string | undefined,
|
|
448
|
-
failed: boolean,
|
|
449
|
-
createdAt: number,
|
|
450
|
-
): void {
|
|
451
|
-
const id = `history-tool:${toolUseId}`;
|
|
452
|
-
const existing = itemsById.get(id);
|
|
453
|
-
const status = failed ? "failed" : "completed";
|
|
454
|
-
itemsById.set(id, {
|
|
455
|
-
id,
|
|
456
|
-
conversationId,
|
|
457
|
-
type: "tool_call",
|
|
458
|
-
kind: existing?.kind ?? "tool_activity",
|
|
459
|
-
itemId: toolUseId,
|
|
460
|
-
toolCall: {
|
|
461
|
-
id: toolUseId,
|
|
462
|
-
name: existing?.toolCall?.name ?? "工具",
|
|
463
|
-
input: existing?.toolCall?.input,
|
|
464
|
-
output,
|
|
465
|
-
createdAt: existing?.toolCall?.createdAt ?? createdAt,
|
|
466
|
-
status,
|
|
467
|
-
},
|
|
468
|
-
commandExecution: existing?.commandExecution
|
|
469
|
-
? { ...existing.commandExecution, output, status }
|
|
470
|
-
: undefined,
|
|
471
|
-
fileChange: existing?.fileChange
|
|
472
|
-
? { ...existing.fileChange, summary: output ?? existing.fileChange.summary, status }
|
|
473
|
-
: undefined,
|
|
474
|
-
text: existing?.text,
|
|
475
|
-
createdAt: existing?.createdAt ?? createdAt,
|
|
476
|
-
updatedAt: createdAt,
|
|
477
|
-
metadata: existing?.metadata ?? { source: "device-history", provider: "claude" },
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
export function loadClaudeStoredTimeline(
|
|
482
|
-
sessionId: string,
|
|
483
|
-
conversationId: string,
|
|
484
|
-
options: { maxItems?: number } = {},
|
|
485
|
-
): { items: StoredAgentTimelineItem[] } {
|
|
486
|
-
const filePath = findClaudeSessionFile(sessionId);
|
|
487
|
-
if (!filePath) return { items: [] };
|
|
488
|
-
let statMtime = Date.now();
|
|
489
|
-
let statSize = 0;
|
|
490
|
-
try {
|
|
491
|
-
const stat = statSync(filePath);
|
|
492
|
-
statMtime = stat.mtimeMs;
|
|
493
|
-
statSize = stat.size;
|
|
494
|
-
} catch {
|
|
495
|
-
return { items: [] };
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
const itemsById = new Map<string, StoredAgentTimelineItem>();
|
|
499
|
-
let index = 0;
|
|
500
|
-
for (const line of readHistorySample(filePath, statSize).split(/\r?\n/)) {
|
|
501
|
-
const trimmed = line.trim();
|
|
502
|
-
if (!trimmed.startsWith("{")) continue;
|
|
503
|
-
try {
|
|
504
|
-
const record = asRecord(JSON.parse(trimmed));
|
|
505
|
-
if (!record) continue;
|
|
506
|
-
const message = asRecord(record.message);
|
|
507
|
-
const rawRole = typeof record.role === "string"
|
|
508
|
-
? record.role
|
|
509
|
-
: typeof message?.role === "string"
|
|
510
|
-
? message.role
|
|
511
|
-
: typeof record.type === "string"
|
|
512
|
-
? record.type
|
|
513
|
-
: undefined;
|
|
514
|
-
const role = rawRole === "assistant" ? "assistant" : rawRole === "user" ? "user" : undefined;
|
|
515
|
-
if (!role) continue;
|
|
516
|
-
const createdAt =
|
|
517
|
-
parseTimestamp(record.timestamp ?? record.createdAt ?? record.created_at) ??
|
|
518
|
-
statMtime + index;
|
|
519
|
-
const content = message?.content ?? record.content ?? record.text;
|
|
520
|
-
const text = extractVisibleClaudeText(content);
|
|
521
|
-
if (text) {
|
|
522
|
-
itemsById.set(
|
|
523
|
-
`history:${sessionId}:${index}`,
|
|
524
|
-
historyMessage(conversationId, index, role, text, createdAt, sessionId),
|
|
525
|
-
);
|
|
526
|
-
index += 1;
|
|
527
|
-
}
|
|
528
|
-
if (Array.isArray(content)) {
|
|
529
|
-
for (const part of content) {
|
|
530
|
-
const block = asRecord(part);
|
|
531
|
-
if (!block) continue;
|
|
532
|
-
if (role === "assistant" && block.type === "tool_use") {
|
|
533
|
-
const toolUseId = typeof block.id === "string" ? block.id : undefined;
|
|
534
|
-
if (!toolUseId) continue;
|
|
535
|
-
const name = typeof block.name === "string" ? block.name : undefined;
|
|
536
|
-
const input = asRecord(block.input);
|
|
537
|
-
const item = createClaudeToolItem(conversationId, toolUseId, name, input, createdAt);
|
|
538
|
-
itemsById.set(item.id, item);
|
|
539
|
-
} else if (role === "user" && block.type === "tool_result") {
|
|
540
|
-
const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
|
|
541
|
-
if (!toolUseId) continue;
|
|
542
|
-
completeClaudeToolItem(
|
|
543
|
-
itemsById,
|
|
544
|
-
conversationId,
|
|
545
|
-
toolUseId,
|
|
546
|
-
extractHistoryText(block.content),
|
|
547
|
-
block.is_error === true,
|
|
548
|
-
createdAt,
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
} catch {
|
|
554
|
-
// Ignore malformed or partial JSONL lines in the history window.
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
return {
|
|
559
|
-
items: [...itemsById.values()]
|
|
560
|
-
.sort((a, b) => a.createdAt - b.createdAt)
|
|
561
|
-
.slice(-Math.max(1, options.maxItems ?? MAX_HISTORY_ITEMS)),
|
|
562
|
-
};
|
|
563
|
-
}
|