linkshell-cli 0.9.0 → 0.10.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/README.md +11 -6
- package/dist/cli/src/commands/gateway.d.ts +2 -0
- package/dist/cli/src/commands/gateway.js +29 -0
- package/dist/cli/src/commands/gateway.js.map +1 -0
- package/dist/cli/src/commands/host.d.ts +2 -0
- package/dist/cli/src/commands/host.js +12 -2
- package/dist/cli/src/commands/host.js.map +1 -1
- package/dist/cli/src/commands/setup.js +4 -2
- package/dist/cli/src/commands/setup.js.map +1 -1
- package/dist/cli/src/index.js +12 -334
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/utils/daemon.d.ts +0 -7
- package/dist/cli/src/utils/daemon.js +1 -18
- package/dist/cli/src/utils/daemon.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/dist/gateway/src/accounts.d.ts +6 -0
- package/dist/gateway/src/accounts.js +32 -0
- package/dist/gateway/src/accounts.js.map +1 -0
- package/dist/gateway/src/index.d.ts +4 -0
- package/dist/gateway/src/index.js +5 -0
- package/dist/gateway/src/index.js.map +1 -0
- package/dist/gateway/src/main.d.ts +1 -0
- package/dist/gateway/src/main.js +57 -0
- package/dist/gateway/src/main.js.map +1 -0
- package/dist/gateway/src/rate-limit.d.ts +22 -0
- package/dist/gateway/src/rate-limit.js +66 -0
- package/dist/gateway/src/rate-limit.js.map +1 -0
- package/dist/gateway/src/relay.d.ts +84 -0
- package/dist/gateway/src/relay.js +434 -0
- package/dist/gateway/src/relay.js.map +1 -0
- package/dist/gateway/src/serve.d.ts +34 -0
- package/dist/gateway/src/serve.js +102 -0
- package/dist/gateway/src/serve.js.map +1 -0
- package/dist/gateway/src/store.d.ts +35 -0
- package/dist/gateway/src/store.js +109 -0
- package/dist/gateway/src/store.js.map +1 -0
- package/dist/gateway/src/subscription.d.ts +18 -0
- package/dist/gateway/src/subscription.js +46 -0
- package/dist/gateway/src/subscription.js.map +1 -0
- package/dist/host/src/drivers/claude/transcript.d.ts +23 -23
- package/dist/host/src/fs.d.ts +3 -3
- package/dist/host/src/input.d.ts +4 -4
- package/dist/host/src/terminals.d.ts +1 -1
- package/dist/wire/src/model.d.ts +6 -6
- package/dist/wire/src/relay.d.ts +8 -8
- package/dist/wire/src/rpc.d.ts +818 -818
- package/dist/wire/src/updates.d.ts +18 -18
- package/package.json +4 -13
- package/src/commands/gateway.ts +31 -0
- package/src/commands/host.ts +13 -2
- package/src/commands/setup.ts +4 -2
- package/src/index.ts +14 -429
- package/src/utils/daemon.ts +2 -27
- package/dist/cli/src/config.d.ts +0 -14
- package/dist/cli/src/config.js +0 -23
- package/dist/cli/src/config.js.map +0 -1
- package/dist/cli/src/machine-id.d.ts +0 -6
- package/dist/cli/src/machine-id.js +0 -50
- package/dist/cli/src/machine-id.js.map +0 -1
- package/dist/cli/src/providers.d.ts +0 -13
- package/dist/cli/src/providers.js +0 -77
- package/dist/cli/src/providers.js.map +0 -1
- package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -128
- package/dist/cli/src/runtime/acp/acp-client.js +0 -430
- package/dist/cli/src/runtime/acp/acp-client.js.map +0 -1
- package/dist/cli/src/runtime/acp/agent-process-discovery.d.ts +0 -48
- package/dist/cli/src/runtime/acp/agent-process-discovery.js +0 -278
- package/dist/cli/src/runtime/acp/agent-process-discovery.js.map +0 -1
- package/dist/cli/src/runtime/acp/agent-session.d.ts +0 -62
- package/dist/cli/src/runtime/acp/agent-session.js +0 -1081
- package/dist/cli/src/runtime/acp/agent-session.js.map +0 -1
- package/dist/cli/src/runtime/acp/agent-workspace.d.ts +0 -311
- package/dist/cli/src/runtime/acp/agent-workspace.js +0 -6083
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +0 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +0 -121
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +0 -1147
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +0 -1
- package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +0 -53
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js +0 -552
- package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +0 -1
- package/dist/cli/src/runtime/acp/json-rpc.d.ts +0 -22
- package/dist/cli/src/runtime/acp/json-rpc.js +0 -160
- package/dist/cli/src/runtime/acp/json-rpc.js.map +0 -1
- package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -16
- package/dist/cli/src/runtime/acp/provider-resolver.js +0 -132
- package/dist/cli/src/runtime/acp/provider-resolver.js.map +0 -1
- package/dist/cli/src/runtime/bridge-session.d.ts +0 -97
- package/dist/cli/src/runtime/bridge-session.js +0 -1364
- package/dist/cli/src/runtime/bridge-session.js.map +0 -1
- package/dist/cli/src/runtime/hook-command.d.ts +0 -9
- package/dist/cli/src/runtime/hook-command.js +0 -80
- package/dist/cli/src/runtime/hook-command.js.map +0 -1
- package/dist/cli/src/runtime/screen-fallback.d.ts +0 -22
- package/dist/cli/src/runtime/screen-fallback.js +0 -202
- package/dist/cli/src/runtime/screen-fallback.js.map +0 -1
- package/dist/cli/src/runtime/screen-share.d.ts +0 -34
- package/dist/cli/src/runtime/screen-share.js +0 -376
- package/dist/cli/src/runtime/screen-share.js.map +0 -1
- package/dist/cli/src/runtime/scrollback.d.ts +0 -11
- package/dist/cli/src/runtime/scrollback.js +0 -33
- package/dist/cli/src/runtime/scrollback.js.map +0 -1
- package/dist/cli/src/runtime/usage-report.d.ts +0 -34
- package/dist/cli/src/runtime/usage-report.js +0 -332
- package/dist/cli/src/runtime/usage-report.js.map +0 -1
- package/dist/cli/src/utils/keep-awake.d.ts +0 -6
- package/dist/cli/src/utils/keep-awake.js +0 -48
- package/dist/cli/src/utils/keep-awake.js.map +0 -1
- package/dist/cli/src/utils/lan-ip.d.ts +0 -9
- package/dist/cli/src/utils/lan-ip.js +0 -61
- package/dist/cli/src/utils/lan-ip.js.map +0 -1
- package/dist/host/src/input-helper.d.ts +0 -1
- package/dist/host/src/input-helper.js +0 -564
- package/dist/host/src/input-helper.js.map +0 -1
- package/dist/shared-protocol/src/index.d.ts +0 -17029
- package/dist/shared-protocol/src/index.js +0 -1064
- package/dist/shared-protocol/src/index.js.map +0 -1
- package/scripts/copy-web.mjs +0 -34
- package/src/config.ts +0 -36
- package/src/machine-id.ts +0 -61
- package/src/providers.ts +0 -97
- package/src/runtime/acp/acp-client.ts +0 -521
- package/src/runtime/acp/agent-process-discovery.ts +0 -308
- package/src/runtime/acp/agent-session.ts +0 -1186
- package/src/runtime/acp/agent-workspace.ts +0 -6590
- package/src/runtime/acp/claude-sdk-client.ts +0 -1221
- package/src/runtime/acp/claude-stream-json-client.ts +0 -633
- package/src/runtime/acp/json-rpc.ts +0 -184
- package/src/runtime/acp/provider-resolver.ts +0 -154
- package/src/runtime/bridge-session.ts +0 -1549
- package/src/runtime/hook-command.ts +0 -73
- package/src/runtime/screen-fallback.ts +0 -235
- package/src/runtime/screen-share.ts +0 -426
- package/src/runtime/scrollback.ts +0 -43
- package/src/runtime/usage-report.ts +0 -342
- package/src/utils/keep-awake.ts +0 -61
- package/src/utils/lan-ip.ts +0 -61
- package/web/assets/FileBrowser-wOAfY-xj.js +0 -2
- package/web/assets/PortPreview-Cbvv7yzA.js +0 -2
- package/web/assets/TerminalPanel-CTSklkct.js +0 -1
- package/web/assets/UsageDashboard-CJ1AsNmd.js +0 -1
- package/web/assets/index-BsnDaeCt.css +0 -33
- package/web/assets/index-q85psYZ5.js +0 -36
- package/web/assets/vendor-highlight-NXWAAyIv.js +0 -5
- package/web/assets/vendor-react-M7xG3qiw.js +0 -49
- package/web/assets/vendor-xterm-D15THcI4.js +0 -9
- package/web/index.html +0 -37
- package/web/logos/claude.png +0 -0
- package/web/logos/codex.png +0 -0
- package/web/logos/copilot.png +0 -0
- package/web/logos/gemini.png +0 -0
- package/web/logos/linkshell-1024.png +0 -0
- package/web/logos/linkshell-splash.png +0 -0
- package/web/logos/linkshell.png +0 -0
|
@@ -1,1147 +0,0 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import { readdirSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
-
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
-
function id(prefix) {
|
|
6
|
-
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
7
|
-
}
|
|
8
|
-
function projectHash(cwd) {
|
|
9
|
-
return ("-" +
|
|
10
|
-
resolve(cwd)
|
|
11
|
-
.replace(/\/$/, "")
|
|
12
|
-
.replace(/\//g, "-"));
|
|
13
|
-
}
|
|
14
|
-
function claudeProjectDir(cwd) {
|
|
15
|
-
return join(homedir(), ".claude", "projects", projectHash(cwd));
|
|
16
|
-
}
|
|
17
|
-
function claudeProjectsRoot() {
|
|
18
|
-
return join(homedir(), ".claude", "projects");
|
|
19
|
-
}
|
|
20
|
-
function claudeProjectDirs(preferredCwd) {
|
|
21
|
-
const root = claudeProjectsRoot();
|
|
22
|
-
const preferred = claudeProjectDir(preferredCwd);
|
|
23
|
-
const dirs = [];
|
|
24
|
-
if (existsSync(preferred))
|
|
25
|
-
dirs.push(preferred);
|
|
26
|
-
if (!existsSync(root))
|
|
27
|
-
return dirs;
|
|
28
|
-
try {
|
|
29
|
-
for (const entry of readdirSync(root)) {
|
|
30
|
-
const fullPath = join(root, entry);
|
|
31
|
-
if (fullPath !== preferred && statSync(fullPath).isDirectory()) {
|
|
32
|
-
dirs.push(fullPath);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
// Ignore unreadable Claude storage.
|
|
38
|
-
}
|
|
39
|
-
return dirs;
|
|
40
|
-
}
|
|
41
|
-
function parseTimestamp(value) {
|
|
42
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
43
|
-
return value > 10_000_000_000 ? value : value * 1000;
|
|
44
|
-
}
|
|
45
|
-
if (typeof value === "string" && value.trim()) {
|
|
46
|
-
const parsed = Date.parse(value);
|
|
47
|
-
return Number.isNaN(parsed) ? undefined : parsed;
|
|
48
|
-
}
|
|
49
|
-
return undefined;
|
|
50
|
-
}
|
|
51
|
-
function extractToolResultText(content) {
|
|
52
|
-
if (typeof content === "string")
|
|
53
|
-
return content;
|
|
54
|
-
if (Array.isArray(content)) {
|
|
55
|
-
return content
|
|
56
|
-
.map((part) => {
|
|
57
|
-
if (typeof part === "string")
|
|
58
|
-
return part;
|
|
59
|
-
if (part && typeof part === "object" && "text" in part && typeof part.text === "string") {
|
|
60
|
-
return part.text;
|
|
61
|
-
}
|
|
62
|
-
return "";
|
|
63
|
-
})
|
|
64
|
-
.join("");
|
|
65
|
-
}
|
|
66
|
-
if (content && typeof content === "object" && "text" in content && typeof content.text === "string") {
|
|
67
|
-
return content.text;
|
|
68
|
-
}
|
|
69
|
-
return String(content ?? "");
|
|
70
|
-
}
|
|
71
|
-
function splitImageDataUrl(value, fallbackMimeType = "image/png") {
|
|
72
|
-
const match = value.match(/^data:([^;,]+)?;base64,(.*)$/is);
|
|
73
|
-
if (!match)
|
|
74
|
-
return { data: value, mimeType: fallbackMimeType };
|
|
75
|
-
return {
|
|
76
|
-
data: match[2] ?? "",
|
|
77
|
-
mimeType: match[1] || fallbackMimeType,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
function toClaudeMessageContent(blocks) {
|
|
81
|
-
return blocks
|
|
82
|
-
.map((block) => {
|
|
83
|
-
if (block.type === "image" && block.data) {
|
|
84
|
-
const image = splitImageDataUrl(block.data, block.mimeType);
|
|
85
|
-
return {
|
|
86
|
-
type: "image",
|
|
87
|
-
source: {
|
|
88
|
-
type: "base64",
|
|
89
|
-
media_type: image.mimeType,
|
|
90
|
-
data: image.data,
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
return { type: "text", text: block.text ?? "" };
|
|
95
|
-
})
|
|
96
|
-
.filter((block) => block.type === "image" ||
|
|
97
|
-
(typeof block.text === "string" && block.text.trim().length > 0));
|
|
98
|
-
}
|
|
99
|
-
async function* singleUserMessage(content) {
|
|
100
|
-
yield {
|
|
101
|
-
type: "user",
|
|
102
|
-
message: {
|
|
103
|
-
role: "user",
|
|
104
|
-
content,
|
|
105
|
-
},
|
|
106
|
-
parent_tool_use_id: null,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
function isRealClaudeSessionId(value) {
|
|
110
|
-
return Boolean(value && !value.startsWith("agent-session-"));
|
|
111
|
-
}
|
|
112
|
-
function asRecord(value) {
|
|
113
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
114
|
-
? value
|
|
115
|
-
: undefined;
|
|
116
|
-
}
|
|
117
|
-
function stringField(value, keys) {
|
|
118
|
-
const record = asRecord(value);
|
|
119
|
-
if (!record)
|
|
120
|
-
return undefined;
|
|
121
|
-
for (const key of keys) {
|
|
122
|
-
const candidate = record[key];
|
|
123
|
-
if (typeof candidate === "string" && candidate.trim())
|
|
124
|
-
return candidate;
|
|
125
|
-
}
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
function previewText(text) {
|
|
129
|
-
return text.replace(/\s+/g, " ").trim().slice(0, 160);
|
|
130
|
-
}
|
|
131
|
-
function contentText(value) {
|
|
132
|
-
if (typeof value === "string")
|
|
133
|
-
return value;
|
|
134
|
-
if (!Array.isArray(value))
|
|
135
|
-
return "";
|
|
136
|
-
return value
|
|
137
|
-
.map((entry) => {
|
|
138
|
-
if (typeof entry === "string")
|
|
139
|
-
return entry;
|
|
140
|
-
const raw = asRecord(entry);
|
|
141
|
-
if (!raw)
|
|
142
|
-
return "";
|
|
143
|
-
if (typeof raw.text === "string")
|
|
144
|
-
return raw.text;
|
|
145
|
-
if (raw.type === "image")
|
|
146
|
-
return `[${stringField(raw.source, ["media_type", "mimeType"]) ?? "image"} attachment]`;
|
|
147
|
-
return "";
|
|
148
|
-
})
|
|
149
|
-
.filter(Boolean)
|
|
150
|
-
.join("\n");
|
|
151
|
-
}
|
|
152
|
-
function claudeToolItemType(toolName) {
|
|
153
|
-
if (toolName === "Bash")
|
|
154
|
-
return "commandExecution";
|
|
155
|
-
if (toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit")
|
|
156
|
-
return "fileChange";
|
|
157
|
-
return "toolCall";
|
|
158
|
-
}
|
|
159
|
-
function claudeToolHistoryItem(toolId, toolName, toolInput, cwd) {
|
|
160
|
-
const filePath = stringField(toolInput, ["file_path", "path", "notebook_path"]);
|
|
161
|
-
return {
|
|
162
|
-
id: toolId,
|
|
163
|
-
type: claudeToolItemType(toolName),
|
|
164
|
-
toolName: toolName ?? "tool",
|
|
165
|
-
tool: toolName ?? "tool",
|
|
166
|
-
input: toolInput,
|
|
167
|
-
toolInput,
|
|
168
|
-
command: stringField(toolInput, ["command"]),
|
|
169
|
-
cwd: stringField(toolInput, ["cwd"]) ?? cwd,
|
|
170
|
-
path: filePath,
|
|
171
|
-
status: "running",
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
export function parseClaudeJsonlSession(input) {
|
|
175
|
-
const turns = [];
|
|
176
|
-
const toolItems = new Map();
|
|
177
|
-
let sessionId = input.sessionId;
|
|
178
|
-
let model;
|
|
179
|
-
let title;
|
|
180
|
-
let preview;
|
|
181
|
-
let createdAt;
|
|
182
|
-
let updatedAt = input.fallbackUpdatedAt;
|
|
183
|
-
let cwd = input.cwd;
|
|
184
|
-
// Last assistant line's token usage. input_tokens at the latest turn is the
|
|
185
|
-
// current context-window occupancy (NOT a running sum), so we keep the last
|
|
186
|
-
// one we see rather than accumulating. Decoupled from any live LinkShell turn
|
|
187
|
-
// — this is read straight from the on-disk transcript.
|
|
188
|
-
let usage;
|
|
189
|
-
// Last-seen raw permissionMode ("default" | "acceptEdits" | ...). Claude
|
|
190
|
-
// records it on user records; the LAST one is the session's current mode.
|
|
191
|
-
// Captured here so readSessionConfig can be a cache field read instead of a
|
|
192
|
-
// second full parse of the transcript.
|
|
193
|
-
let permissionMode;
|
|
194
|
-
let index = 0;
|
|
195
|
-
for (const line of input.text.split(/\r?\n/)) {
|
|
196
|
-
if (!line.trim())
|
|
197
|
-
continue;
|
|
198
|
-
index += 1;
|
|
199
|
-
let raw;
|
|
200
|
-
try {
|
|
201
|
-
const parsed = JSON.parse(line);
|
|
202
|
-
const record = asRecord(parsed);
|
|
203
|
-
if (!record)
|
|
204
|
-
continue;
|
|
205
|
-
raw = record;
|
|
206
|
-
}
|
|
207
|
-
catch {
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
permissionMode = stringField(raw, ["permissionMode"]) ?? permissionMode;
|
|
211
|
-
const timestamp = parseTimestamp(raw.timestamp ?? raw.createdAt ?? raw.created_at);
|
|
212
|
-
cwd = stringField(raw, ["cwd", "workingDirectory", "workspacePath"]) ?? cwd;
|
|
213
|
-
if (timestamp !== undefined) {
|
|
214
|
-
createdAt = createdAt === undefined ? timestamp : Math.min(createdAt, timestamp);
|
|
215
|
-
updatedAt = updatedAt === undefined ? timestamp : Math.max(updatedAt, timestamp);
|
|
216
|
-
}
|
|
217
|
-
sessionId = sessionId ?? stringField(raw, ["sessionId", "session_id"]);
|
|
218
|
-
model = model ?? stringField(raw, ["model"]);
|
|
219
|
-
const message = asRecord(raw.message);
|
|
220
|
-
model = model ?? stringField(message, ["model"]);
|
|
221
|
-
const type = stringField(raw, ["type"]);
|
|
222
|
-
const role = stringField(message, ["role"]) ?? type;
|
|
223
|
-
const content = message && "content" in message ? message.content : raw.content;
|
|
224
|
-
const turnId = stringField(raw, ["uuid", "id"]) ?? `claude-history-${index}`;
|
|
225
|
-
const items = [];
|
|
226
|
-
if (type === "summary" || typeof raw.summary === "string") {
|
|
227
|
-
const summary = typeof raw.summary === "string" ? raw.summary : undefined;
|
|
228
|
-
if (summary) {
|
|
229
|
-
items.push({
|
|
230
|
-
id: turnId,
|
|
231
|
-
type: "contextCompaction",
|
|
232
|
-
summary,
|
|
233
|
-
status: "completed",
|
|
234
|
-
});
|
|
235
|
-
preview = previewText(summary) || preview;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
else if (role === "user") {
|
|
239
|
-
const contentBlocks = Array.isArray(content) ? content : [];
|
|
240
|
-
const toolResults = contentBlocks
|
|
241
|
-
.map(asRecord)
|
|
242
|
-
.filter((block) => Boolean(block && block.type === "tool_result"));
|
|
243
|
-
const ordinaryText = Array.isArray(content)
|
|
244
|
-
? contentText(contentBlocks.filter((block) => asRecord(block)?.type !== "tool_result"))
|
|
245
|
-
: contentText(content);
|
|
246
|
-
if (ordinaryText.trim()) {
|
|
247
|
-
items.push({
|
|
248
|
-
id: stringField(message, ["id"]) ?? turnId,
|
|
249
|
-
type: "userMessage",
|
|
250
|
-
content: [{ type: "text", text: ordinaryText }],
|
|
251
|
-
});
|
|
252
|
-
title = title ?? previewText(ordinaryText);
|
|
253
|
-
preview = previewText(ordinaryText) || preview;
|
|
254
|
-
}
|
|
255
|
-
for (const block of toolResults) {
|
|
256
|
-
const toolId = stringField(block, ["tool_use_id"]) ?? `tool-result-${index}`;
|
|
257
|
-
const previous = toolItems.get(toolId);
|
|
258
|
-
const output = extractToolResultText(block.content);
|
|
259
|
-
items.push({
|
|
260
|
-
...previous,
|
|
261
|
-
id: toolId,
|
|
262
|
-
type: stringField(previous, ["type"]) ?? "toolCall",
|
|
263
|
-
toolName: stringField(previous, ["toolName", "tool", "name"]),
|
|
264
|
-
tool: stringField(previous, ["toolName", "tool", "name"]),
|
|
265
|
-
status: block.is_error === true ? "failed" : "completed",
|
|
266
|
-
output,
|
|
267
|
-
aggregatedOutput: output,
|
|
268
|
-
isError: block.is_error === true,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
else if (role === "assistant") {
|
|
273
|
-
const assistantMessageId = stringField(message, ["id"]) ?? turnId;
|
|
274
|
-
const lineUsage = asRecord(message?.usage);
|
|
275
|
-
if (lineUsage)
|
|
276
|
-
usage = lineUsage;
|
|
277
|
-
if (typeof content === "string") {
|
|
278
|
-
if (content.trim()) {
|
|
279
|
-
items.push({
|
|
280
|
-
id: assistantMessageId,
|
|
281
|
-
type: "agentMessage",
|
|
282
|
-
content: [{ type: "text", text: content }],
|
|
283
|
-
status: "completed",
|
|
284
|
-
});
|
|
285
|
-
preview = previewText(content) || preview;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
const blocks = Array.isArray(content) ? content.map(asRecord).filter((block) => Boolean(block)) : [];
|
|
290
|
-
const text = contentText(blocks.filter((block) => block.type === "text"));
|
|
291
|
-
if (text.trim()) {
|
|
292
|
-
items.push({
|
|
293
|
-
id: assistantMessageId,
|
|
294
|
-
type: "agentMessage",
|
|
295
|
-
content: [{ type: "text", text }],
|
|
296
|
-
status: "completed",
|
|
297
|
-
});
|
|
298
|
-
preview = previewText(text) || preview;
|
|
299
|
-
}
|
|
300
|
-
blocks.forEach((block, blockIndex) => {
|
|
301
|
-
if (block.type === "thinking" && typeof block.thinking === "string" && block.thinking.trim()) {
|
|
302
|
-
items.push({
|
|
303
|
-
id: `${turnId}:thinking:${blockIndex}`,
|
|
304
|
-
type: "thinking",
|
|
305
|
-
text: block.thinking,
|
|
306
|
-
status: "completed",
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
else if (block.type === "tool_use") {
|
|
310
|
-
const toolId = stringField(block, ["id"]) ?? `${turnId}:tool:${blockIndex}`;
|
|
311
|
-
const toolName = stringField(block, ["name"]);
|
|
312
|
-
const toolInput = asRecord(block.input);
|
|
313
|
-
const item = claudeToolHistoryItem(toolId, toolName, toolInput, cwd);
|
|
314
|
-
toolItems.set(toolId, item);
|
|
315
|
-
items.push(item);
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
else if (type === "system") {
|
|
321
|
-
model = model ?? stringField(raw, ["model"]);
|
|
322
|
-
}
|
|
323
|
-
if (items.length > 0) {
|
|
324
|
-
turns.push({
|
|
325
|
-
id: turnId,
|
|
326
|
-
createdAt: timestamp,
|
|
327
|
-
items,
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
return {
|
|
332
|
-
thread: {
|
|
333
|
-
id: sessionId ?? input.sessionId,
|
|
334
|
-
sessionId: sessionId ?? input.sessionId,
|
|
335
|
-
cwd,
|
|
336
|
-
model,
|
|
337
|
-
title,
|
|
338
|
-
preview,
|
|
339
|
-
createdAt,
|
|
340
|
-
updatedAt,
|
|
341
|
-
usage,
|
|
342
|
-
turns,
|
|
343
|
-
...(permissionMode ? { permissionMode } : {}),
|
|
344
|
-
},
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
/** Derive whether an on-disk Claude transcript is being actively driven RIGHT
|
|
348
|
-
* NOW by some process (LinkShell or an external `claude` terminal), purely from
|
|
349
|
-
* the file. Combines a structural signal (the last record) with a freshness
|
|
350
|
-
* signal (mtime), so it neither misses a live external turn nor sticks at
|
|
351
|
-
* "running" forever if that process dies mid-turn.
|
|
352
|
-
*
|
|
353
|
-
* - last record is a completed assistant turn (stop_reason "end_turn") → idle,
|
|
354
|
-
* regardless of mtime (the turn is definitively done).
|
|
355
|
-
* - otherwise the structure is mid-turn (awaiting an assistant response, an
|
|
356
|
-
* assistant still calling tools, or a half-written trailing line). If the
|
|
357
|
-
* file changed within FRESH_MS, a process is appending → running; if it's
|
|
358
|
-
* stale, the driver is gone → idle.
|
|
359
|
-
* Verified against real transcripts: mid-turn+fresh→running, mid-turn+stale→
|
|
360
|
-
* idle, end_turn+fresh→idle, half-written-line+fresh→running. */
|
|
361
|
-
const TRANSCRIPT_FRESH_MS = 10_000;
|
|
362
|
-
/** The structural half of the status verdict: does the LAST record of a
|
|
363
|
-
* transcript indicate an in-progress turn (awaiting/producing an assistant
|
|
364
|
-
* response, or a half-written trailing line)? This is purely content-derived
|
|
365
|
-
* — it does NOT depend on the clock — so it can be cached alongside the parsed
|
|
366
|
-
* metadata and reused across syncs until the file's (mtime,size) changes.
|
|
367
|
-
* Scans backward from the end to avoid split()-ing a possibly-huge transcript. */
|
|
368
|
-
function deriveTranscriptMidTurn(text) {
|
|
369
|
-
let last;
|
|
370
|
-
let sawUnparseableTail = false;
|
|
371
|
-
let end = text.length;
|
|
372
|
-
while (end > 0) {
|
|
373
|
-
const nl = text.lastIndexOf("\n", end - 1);
|
|
374
|
-
const line = text.slice(nl + 1, end).trim();
|
|
375
|
-
end = nl; // step before this newline; nl === -1 ends the loop (first line)
|
|
376
|
-
if (!line)
|
|
377
|
-
continue;
|
|
378
|
-
try {
|
|
379
|
-
last = asRecord(JSON.parse(line));
|
|
380
|
-
break;
|
|
381
|
-
}
|
|
382
|
-
catch {
|
|
383
|
-
// A trailing half-written line means a writer is mid-append.
|
|
384
|
-
sawUnparseableTail = true;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
if (!last)
|
|
388
|
-
return false;
|
|
389
|
-
const message = asRecord(last.message);
|
|
390
|
-
const role = stringField(message, ["role"]) ?? stringField(last, ["type"]);
|
|
391
|
-
const stop = message ? message.stop_reason ?? null : null;
|
|
392
|
-
if (role === "assistant" && stop === "end_turn")
|
|
393
|
-
return false;
|
|
394
|
-
return (role === "user" ||
|
|
395
|
-
last.type === "user" ||
|
|
396
|
-
(role === "assistant" && (stop === "tool_use" || stop === null)) ||
|
|
397
|
-
sawUnparseableTail);
|
|
398
|
-
}
|
|
399
|
-
/** Combine the (clock-independent) structural verdict with the freshness signal.
|
|
400
|
-
* A stale file (no writes within FRESH_MS) is always idle; a mid-turn file that
|
|
401
|
-
* is still being appended to is running. Cheap: one comparison + one boolean. */
|
|
402
|
-
function statusFromMidTurn(midTurn, mtimeMs, nowMs) {
|
|
403
|
-
if (nowMs - mtimeMs >= TRANSCRIPT_FRESH_MS)
|
|
404
|
-
return "idle";
|
|
405
|
-
return midTurn ? "running" : "idle";
|
|
406
|
-
}
|
|
407
|
-
/** Derive whether an on-disk Claude transcript is being actively driven RIGHT
|
|
408
|
-
* NOW, purely from the file. See {@link deriveTranscriptMidTurn} /
|
|
409
|
-
* {@link statusFromMidTurn}; kept as a convenience wrapper for callers that
|
|
410
|
-
* hold the full text and the clock but no cache. */
|
|
411
|
-
function deriveTranscriptStatus(text, mtimeMs, nowMs) {
|
|
412
|
-
// Stale short-circuit: most sessions in ~/.claude/projects are old, so avoid
|
|
413
|
-
// the backward scan entirely when the file can't possibly be running.
|
|
414
|
-
if (nowMs - mtimeMs >= TRANSCRIPT_FRESH_MS)
|
|
415
|
-
return "idle";
|
|
416
|
-
return statusFromMidTurn(deriveTranscriptMidTurn(text), mtimeMs, nowMs);
|
|
417
|
-
}
|
|
418
|
-
/** Parse a transcript into the clock-independent, cacheable half of its list
|
|
419
|
-
* metadata: everything except `status` (which depends on `now`). The caller
|
|
420
|
-
* layers status on via {@link statusFromMidTurn}, and can cache this result
|
|
421
|
-
* keyed by (path, mtime, size) so an unchanged transcript is never re-read. */
|
|
422
|
-
function claudeSessionBaseFromFile(filePath, cwd, sessionId, mtimeMs) {
|
|
423
|
-
const text = readFileSync(filePath, "utf8");
|
|
424
|
-
const parsed = parseClaudeJsonlSession({
|
|
425
|
-
text,
|
|
426
|
-
cwd,
|
|
427
|
-
sessionId,
|
|
428
|
-
fallbackUpdatedAt: mtimeMs,
|
|
429
|
-
});
|
|
430
|
-
const thread = asRecord(parsed.thread) ?? {};
|
|
431
|
-
const base = {
|
|
432
|
-
id: sessionId,
|
|
433
|
-
cwd: stringField(thread, ["cwd", "workingDirectory", "workspacePath"]) ?? cwd,
|
|
434
|
-
title: stringField(thread, ["title", "preview"]),
|
|
435
|
-
model: stringField(thread, ["model"]),
|
|
436
|
-
createdAt: parseTimestamp(thread.createdAt),
|
|
437
|
-
lastActivityAt: parseTimestamp(thread.updatedAt) ?? mtimeMs,
|
|
438
|
-
lastModified: mtimeMs,
|
|
439
|
-
usage: asRecord(thread.usage),
|
|
440
|
-
};
|
|
441
|
-
return { base, midTurn: deriveTranscriptMidTurn(text) };
|
|
442
|
-
}
|
|
443
|
-
function claudeSessionMetadataFromFile(filePath, cwd, sessionId) {
|
|
444
|
-
const stat = statSync(filePath);
|
|
445
|
-
const { base, midTurn } = claudeSessionBaseFromFile(filePath, cwd, sessionId, stat.mtimeMs);
|
|
446
|
-
// Live status read straight from the transcript, so a session another
|
|
447
|
-
// `claude` process is actively driving shows as "running" in the list
|
|
448
|
-
// rather than the hardcoded "idle" the workspace would otherwise assign.
|
|
449
|
-
return { ...base, status: statusFromMidTurn(midTurn, stat.mtimeMs, Date.now()) };
|
|
450
|
-
}
|
|
451
|
-
function isInsideCwd(cwd, candidate) {
|
|
452
|
-
const root = resolve(cwd);
|
|
453
|
-
const target = resolve(root, candidate);
|
|
454
|
-
const rel = relative(root, target);
|
|
455
|
-
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
456
|
-
}
|
|
457
|
-
function outcomeFromPermissionResponse(value) {
|
|
458
|
-
const raw = value && typeof value === "object" ? value : {};
|
|
459
|
-
if (raw.behavior === "allow")
|
|
460
|
-
return "allow";
|
|
461
|
-
if (raw.behavior === "deny")
|
|
462
|
-
return "deny";
|
|
463
|
-
const outcome = raw.outcome && typeof raw.outcome === "object"
|
|
464
|
-
? raw.outcome.outcome
|
|
465
|
-
: raw.outcome;
|
|
466
|
-
if (outcome === "selected") {
|
|
467
|
-
const optionId = raw.outcome && typeof raw.outcome === "object"
|
|
468
|
-
? String(raw.outcome.optionId ?? "")
|
|
469
|
-
: "";
|
|
470
|
-
return optionId.toLowerCase().includes("allow") ? "allow" : "deny";
|
|
471
|
-
}
|
|
472
|
-
return outcome === "allow" ? "allow" : "deny";
|
|
473
|
-
}
|
|
474
|
-
export function claudePermissionModeFor(input) {
|
|
475
|
-
if (input.collaborationMode === "plan" || input.permissionMode === "read_only")
|
|
476
|
-
return "plan";
|
|
477
|
-
if (input.permissionMode === "workspace_write")
|
|
478
|
-
return "acceptEdits";
|
|
479
|
-
if (input.permissionMode === "full_access")
|
|
480
|
-
return "bypassPermissions";
|
|
481
|
-
return "default";
|
|
482
|
-
}
|
|
483
|
-
const FALLBACK_MODEL_LABELS = {
|
|
484
|
-
default: "默认模型",
|
|
485
|
-
sonnet: "Sonnet",
|
|
486
|
-
opus: "Opus",
|
|
487
|
-
opusplan: "Opus Plan",
|
|
488
|
-
haiku: "Haiku",
|
|
489
|
-
"sonnet[1m]": "Sonnet 1M",
|
|
490
|
-
"opus[1m]": "Opus 1M",
|
|
491
|
-
};
|
|
492
|
-
// Canonical Claude Code model aliases, used whenever live model discovery
|
|
493
|
-
// (supportedModels()) returns nothing. Hoisted so the two listModels() fallback
|
|
494
|
-
// branches stay in sync — a new alias added here reaches both.
|
|
495
|
-
const DEFAULT_FALLBACK_MODELS = ["default", "sonnet", "opus", "opusplan", "haiku", "sonnet[1m]", "opus[1m]"];
|
|
496
|
-
const toModelEntries = (ids) => ids.map((id) => ({ id, label: FALLBACK_MODEL_LABELS[id] ?? id }));
|
|
497
|
-
export class ClaudeSdkClient {
|
|
498
|
-
input;
|
|
499
|
-
query;
|
|
500
|
-
startup;
|
|
501
|
-
claudeSessionId;
|
|
502
|
-
abortController;
|
|
503
|
-
permissionWaiters = new Map();
|
|
504
|
-
// Per-file cache for listSessions: an unchanged transcript (same mtime+size)
|
|
505
|
-
// is never re-read or re-parsed. fs.watch re-syncs fire every ~1.5s while any
|
|
506
|
-
// session is active, and ~/.claude/projects can hold hundreds of old
|
|
507
|
-
// transcripts — without this, each sync would readFileSync + fully parse
|
|
508
|
-
// every one of them (the "scan storm"). Only the file being actively appended
|
|
509
|
-
// to changes, so the steady-state cost drops to stat()+status per file.
|
|
510
|
-
sessionMetaCache = new Map();
|
|
511
|
-
// Full parsed-transcript cache shared by readSession / readSessionConfig /
|
|
512
|
-
// listTurns, keyed by file path and validated by (mtime,size) like
|
|
513
|
-
// sessionMetaCache above. Opening a conversation hits all three of those in
|
|
514
|
-
// quick succession, and every history page re-slices the same parse — on a
|
|
515
|
-
// 39MB transcript that was ~257ms per open plus ~93ms per scroll page of
|
|
516
|
-
// redundant readFileSync + per-line JSON.parse. Unlike sessionMetaCache this
|
|
517
|
-
// holds the FULL turn list (large for big transcripts), so it is bounded to a
|
|
518
|
-
// small LRU (Map iteration order + delete/re-set on hit = LRU).
|
|
519
|
-
parsedTranscriptCache = new Map();
|
|
520
|
-
static PARSED_TRANSCRIPT_CACHE_MAX = 8;
|
|
521
|
-
/** Read + parse a session transcript through the LRU cache. Returns the
|
|
522
|
-
* parseClaudeJsonlSession result (stable for a given (mtime,size)); an
|
|
523
|
-
* appended-to or rewritten file re-parses on next access. */
|
|
524
|
-
parsedTranscript(filePath, sessionId) {
|
|
525
|
-
const stat = statSync(filePath);
|
|
526
|
-
const cached = this.parsedTranscriptCache.get(filePath);
|
|
527
|
-
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
528
|
-
// Refresh recency so the hottest transcripts survive the LRU bound.
|
|
529
|
-
this.parsedTranscriptCache.delete(filePath);
|
|
530
|
-
this.parsedTranscriptCache.set(filePath, cached);
|
|
531
|
-
return cached.parsed;
|
|
532
|
-
}
|
|
533
|
-
const parsed = parseClaudeJsonlSession({
|
|
534
|
-
text: readFileSync(filePath, "utf8"),
|
|
535
|
-
cwd: this.input.cwd,
|
|
536
|
-
sessionId,
|
|
537
|
-
fallbackUpdatedAt: stat.mtimeMs,
|
|
538
|
-
});
|
|
539
|
-
this.parsedTranscriptCache.delete(filePath);
|
|
540
|
-
this.parsedTranscriptCache.set(filePath, { mtimeMs: stat.mtimeMs, size: stat.size, parsed });
|
|
541
|
-
while (this.parsedTranscriptCache.size > ClaudeSdkClient.PARSED_TRANSCRIPT_CACHE_MAX) {
|
|
542
|
-
const oldest = this.parsedTranscriptCache.keys().next().value;
|
|
543
|
-
if (oldest === undefined)
|
|
544
|
-
break;
|
|
545
|
-
this.parsedTranscriptCache.delete(oldest);
|
|
546
|
-
}
|
|
547
|
-
return parsed;
|
|
548
|
-
}
|
|
549
|
-
constructor(input) {
|
|
550
|
-
this.input = input;
|
|
551
|
-
}
|
|
552
|
-
async initialize() {
|
|
553
|
-
try {
|
|
554
|
-
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
555
|
-
const mod = await dynamicImport("@anthropic-ai/claude-agent-sdk");
|
|
556
|
-
const query = mod.query;
|
|
557
|
-
if (typeof query !== "function") {
|
|
558
|
-
throw new Error("Claude Agent SDK does not export query()");
|
|
559
|
-
}
|
|
560
|
-
this.query = query;
|
|
561
|
-
// startup() pre-warms the CLI subprocess so we can discover available models
|
|
562
|
-
// without starting a full conversation (via supportedModels() on the Query).
|
|
563
|
-
const startup = mod.startup;
|
|
564
|
-
if (typeof startup === "function") {
|
|
565
|
-
this.startup = startup;
|
|
566
|
-
}
|
|
567
|
-
return { status: "ok", protocol: "claude-agent-sdk" };
|
|
568
|
-
}
|
|
569
|
-
catch (error) {
|
|
570
|
-
throw new Error(`Claude Agent SDK is not available: ${error instanceof Error ? error.message : String(error)}`);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
async newSession(input) {
|
|
574
|
-
this.claudeSessionId = undefined;
|
|
575
|
-
return { sessionId: undefined, status: "ready", cwd: input.cwd };
|
|
576
|
-
}
|
|
577
|
-
async loadSession(input) {
|
|
578
|
-
this.claudeSessionId = input.sessionId;
|
|
579
|
-
return { sessionId: input.sessionId, status: "loaded", cwd: input.cwd };
|
|
580
|
-
}
|
|
581
|
-
async readSession(input) {
|
|
582
|
-
const filePath = this.resolveSessionFile(input.sessionId);
|
|
583
|
-
if (!filePath) {
|
|
584
|
-
return {
|
|
585
|
-
thread: {
|
|
586
|
-
id: input.sessionId,
|
|
587
|
-
sessionId: input.sessionId,
|
|
588
|
-
cwd: this.input.cwd,
|
|
589
|
-
turns: [],
|
|
590
|
-
},
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
return this.parsedTranscript(filePath, input.sessionId);
|
|
594
|
-
}
|
|
595
|
-
/** Resolve the on-disk JSONL transcript for a session, preferring the cwd's
|
|
596
|
-
* project dir and falling back to a scan of sibling project dirs. */
|
|
597
|
-
resolveSessionFile(sessionId) {
|
|
598
|
-
const preferred = join(claudeProjectDir(this.input.cwd), `${sessionId}.jsonl`);
|
|
599
|
-
if (existsSync(preferred))
|
|
600
|
-
return preferred;
|
|
601
|
-
return claudeProjectDirs(this.input.cwd)
|
|
602
|
-
.map((dir) => join(dir, `${sessionId}.jsonl`))
|
|
603
|
-
.find((candidate) => existsSync(candidate));
|
|
604
|
-
}
|
|
605
|
-
/** Fork a session: copy the source transcript into a NEW session file,
|
|
606
|
-
* truncated to include everything up to and including the turn `uptoTurnId`
|
|
607
|
-
* (omitted → full clone). Claude has no native fork primitive, so this
|
|
608
|
-
* simulates one by writing a fresh <uuid>.jsonl alongside the source. The
|
|
609
|
-
* source file is NEVER modified. Turn ids match the per-line uuid/id (or the
|
|
610
|
-
* `claude-history-<n>` fallback) that parseClaudeJsonlSession assigns, where
|
|
611
|
-
* <n> counts non-empty lines — replicated here so the cut lands exactly on
|
|
612
|
-
* the turn the client asked for. Returns the new session id, or undefined if
|
|
613
|
-
* the source can't be read or the new file can't be created. */
|
|
614
|
-
forkSession(input) {
|
|
615
|
-
const sourceFile = this.resolveSessionFile(input.sourceSessionId);
|
|
616
|
-
if (!sourceFile)
|
|
617
|
-
return undefined;
|
|
618
|
-
let text;
|
|
619
|
-
try {
|
|
620
|
-
text = readFileSync(sourceFile, "utf8");
|
|
621
|
-
}
|
|
622
|
-
catch {
|
|
623
|
-
return undefined;
|
|
624
|
-
}
|
|
625
|
-
const lines = text.split("\n");
|
|
626
|
-
let cutIndex = lines.length - 1; // default: keep everything (full clone)
|
|
627
|
-
if (input.uptoTurnId) {
|
|
628
|
-
let nonEmpty = 0;
|
|
629
|
-
let found = -1;
|
|
630
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
631
|
-
const line = lines[i]?.trim();
|
|
632
|
-
if (!line)
|
|
633
|
-
continue;
|
|
634
|
-
nonEmpty += 1;
|
|
635
|
-
let turnId;
|
|
636
|
-
try {
|
|
637
|
-
const rec = asRecord(JSON.parse(line));
|
|
638
|
-
turnId = stringField(rec, ["uuid", "id"]) ?? `claude-history-${nonEmpty}`;
|
|
639
|
-
}
|
|
640
|
-
catch {
|
|
641
|
-
continue; // unparseable line — parser skips it too (index still bumped)
|
|
642
|
-
}
|
|
643
|
-
if (turnId === input.uptoTurnId) {
|
|
644
|
-
found = i;
|
|
645
|
-
break;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
if (found < 0)
|
|
649
|
-
return undefined; // turn not found — don't silently full-clone
|
|
650
|
-
cutIndex = found;
|
|
651
|
-
}
|
|
652
|
-
const truncated = lines.slice(0, cutIndex + 1).join("\n") + "\n";
|
|
653
|
-
const newId = randomUUID();
|
|
654
|
-
const dir = claudeProjectDir(this.input.cwd);
|
|
655
|
-
const newPath = join(dir, `${newId}.jsonl`);
|
|
656
|
-
try {
|
|
657
|
-
// wx: fail if it somehow already exists — never overwrite.
|
|
658
|
-
writeFileSync(newPath, truncated, { flag: "wx" });
|
|
659
|
-
}
|
|
660
|
-
catch {
|
|
661
|
-
return undefined;
|
|
662
|
-
}
|
|
663
|
-
return { sessionId: newId };
|
|
664
|
-
}
|
|
665
|
-
/** Read a session's last-known settings (permission mode, plan mode) straight
|
|
666
|
-
* from its on-disk transcript, so opening a session LinkShell never drove
|
|
667
|
-
* shows the config it actually ran with instead of UI defaults. Claude
|
|
668
|
-
* records `permissionMode` on user records (not reasoning effort), so effort
|
|
669
|
-
* is left undefined. Only called on open (one file), never per-list. */
|
|
670
|
-
async readSessionConfig(input) {
|
|
671
|
-
const filePath = this.resolveSessionFile(input.sessionId);
|
|
672
|
-
if (!filePath)
|
|
673
|
-
return {};
|
|
674
|
-
let permissionModeRaw;
|
|
675
|
-
try {
|
|
676
|
-
// The shared parse already tracked the last-seen permissionMode, so this
|
|
677
|
-
// is a cache field read on the open path (readSession runs right before).
|
|
678
|
-
const thread = asRecord(this.parsedTranscript(filePath, input.sessionId).thread);
|
|
679
|
-
permissionModeRaw = stringField(thread, ["permissionMode"]);
|
|
680
|
-
}
|
|
681
|
-
catch {
|
|
682
|
-
return {};
|
|
683
|
-
}
|
|
684
|
-
// Map Claude's permission mode to ours. "default" means no explicit
|
|
685
|
-
// override → leave undefined so the pill shows 默认 rather than a guess.
|
|
686
|
-
switch (permissionModeRaw) {
|
|
687
|
-
case "acceptEdits":
|
|
688
|
-
return { permissionMode: "workspace_write" };
|
|
689
|
-
case "bypassPermissions":
|
|
690
|
-
return { permissionMode: "full_access" };
|
|
691
|
-
case "plan":
|
|
692
|
-
return { permissionMode: "read_only", collaborationMode: "plan" };
|
|
693
|
-
default:
|
|
694
|
-
return {};
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
/** Page OLDER transcript turns for scroll-back, mirroring Codex's
|
|
698
|
-
* thread/turns/list. The whole transcript lives on disk and is already
|
|
699
|
-
* parsed by parseClaudeJsonlSession, so we slice a window of chronological
|
|
700
|
-
* turns by an integer end-exclusive cursor.
|
|
701
|
-
*
|
|
702
|
-
* Cursor = the exclusive upper-bound turn index for the page. No cursor =
|
|
703
|
-
* start at the end (total turn count). A page returns turns [lo, hi) where
|
|
704
|
-
* lo = max(0, hi - limit); nextCursor = String(lo) while lo > 0, else absent
|
|
705
|
-
* (start of history reached). The web client's mergeTimeline dedupes by id
|
|
706
|
-
* and re-sorts by createdAt, so any overlap between this page and what the
|
|
707
|
-
* client already shows is harmless. */
|
|
708
|
-
async listTurns(input) {
|
|
709
|
-
const empty = { sessionId: input.sessionId, turns: [], sortDirection: "asc" };
|
|
710
|
-
const filePath = this.resolveSessionFile(input.sessionId);
|
|
711
|
-
if (!filePath)
|
|
712
|
-
return empty;
|
|
713
|
-
const parsed = this.parsedTranscript(filePath, input.sessionId);
|
|
714
|
-
const thread = asRecord(parsed.thread);
|
|
715
|
-
const allTurns = Array.isArray(thread?.turns) ? thread.turns : [];
|
|
716
|
-
const total = allTurns.length;
|
|
717
|
-
if (total === 0)
|
|
718
|
-
return empty;
|
|
719
|
-
const limit = input.limit && input.limit > 0 ? Math.floor(input.limit) : 50;
|
|
720
|
-
// Parse the end-exclusive cursor; default to the end of history. Clamp into
|
|
721
|
-
// [0, total] so a stale/garbage cursor can't slice out of bounds.
|
|
722
|
-
const parsedCursor = input.cursor !== undefined ? Number.parseInt(input.cursor, 10) : total;
|
|
723
|
-
const hi = Number.isFinite(parsedCursor) ? Math.max(0, Math.min(total, parsedCursor)) : total;
|
|
724
|
-
const lo = Math.max(0, hi - limit);
|
|
725
|
-
const page = allTurns.slice(lo, hi);
|
|
726
|
-
return {
|
|
727
|
-
sessionId: input.sessionId,
|
|
728
|
-
turns: page, // already chronological (ascending)
|
|
729
|
-
sortDirection: "asc",
|
|
730
|
-
...(lo > 0 ? { nextCursor: String(lo) } : {}),
|
|
731
|
-
};
|
|
732
|
-
}
|
|
733
|
-
async prompt(input) {
|
|
734
|
-
if (!this.query)
|
|
735
|
-
await this.initialize();
|
|
736
|
-
if (!this.query)
|
|
737
|
-
throw new Error("Claude Agent SDK is not initialized");
|
|
738
|
-
this.abortController?.abort();
|
|
739
|
-
const abortController = new AbortController();
|
|
740
|
-
this.abortController = abortController;
|
|
741
|
-
const inputBlocks = input.content;
|
|
742
|
-
const hasImages = inputBlocks.some((block) => block.type === "image" && block.data);
|
|
743
|
-
const prompt = inputBlocks
|
|
744
|
-
.map((block) => {
|
|
745
|
-
if (block.type === "image")
|
|
746
|
-
return `[${block.mimeType ?? "image"} attachment]`;
|
|
747
|
-
return block.text ?? "";
|
|
748
|
-
})
|
|
749
|
-
.filter(Boolean)
|
|
750
|
-
.join("\n");
|
|
751
|
-
const sdkOptions = {
|
|
752
|
-
cwd: input.cwd ?? this.input.cwd,
|
|
753
|
-
abortController,
|
|
754
|
-
permissionMode: claudePermissionModeFor({
|
|
755
|
-
permissionMode: input.permissionMode,
|
|
756
|
-
collaborationMode: input.collaborationMode,
|
|
757
|
-
}),
|
|
758
|
-
toolConfig: {
|
|
759
|
-
askUserQuestion: { previewFormat: "markdown" },
|
|
760
|
-
},
|
|
761
|
-
canUseTool: async (toolName, toolInput) => {
|
|
762
|
-
if (toolName === "AskUserQuestion") {
|
|
763
|
-
const response = await this.input.onRequest("claude/askUserQuestion", {
|
|
764
|
-
...(asRecord(toolInput) ?? {}),
|
|
765
|
-
requestId: id("claude-input"),
|
|
766
|
-
sessionId: this.claudeSessionId,
|
|
767
|
-
});
|
|
768
|
-
return response;
|
|
769
|
-
}
|
|
770
|
-
if (input.permissionMode === "full_access")
|
|
771
|
-
return { behavior: "allow", updatedInput: toolInput };
|
|
772
|
-
if (["Read", "Glob", "Grep", "LS", "NotebookRead", "TodoRead"].includes(toolName)) {
|
|
773
|
-
return { behavior: "allow", updatedInput: toolInput };
|
|
774
|
-
}
|
|
775
|
-
if (input.permissionMode === "read_only" && ["Write", "Edit", "MultiEdit", "Bash"].includes(toolName)) {
|
|
776
|
-
return { behavior: "deny", message: "Read-only mode is active." };
|
|
777
|
-
}
|
|
778
|
-
if (input.permissionMode === "workspace_write" && ["Write", "Edit", "MultiEdit"].includes(toolName)) {
|
|
779
|
-
const filePath = stringField(toolInput, ["file_path", "path", "notebook_path"]);
|
|
780
|
-
if (filePath && isInsideCwd(input.cwd ?? this.input.cwd, filePath)) {
|
|
781
|
-
return { behavior: "allow", updatedInput: toolInput };
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
const requestId = id("claude-perm");
|
|
785
|
-
const response = await this.input.onRequest("claude/requestApproval", {
|
|
786
|
-
requestId,
|
|
787
|
-
sessionId: this.claudeSessionId,
|
|
788
|
-
toolCall: {
|
|
789
|
-
toolName,
|
|
790
|
-
input: toolInput,
|
|
791
|
-
},
|
|
792
|
-
context: `Claude wants to use ${toolName}`,
|
|
793
|
-
options: [
|
|
794
|
-
{ id: "deny", label: "拒绝", kind: "deny" },
|
|
795
|
-
{ id: "allow_once", label: "允许一次", kind: "allow" },
|
|
796
|
-
],
|
|
797
|
-
});
|
|
798
|
-
return outcomeFromPermissionResponse(response) === "allow"
|
|
799
|
-
? { behavior: "allow", updatedInput: toolInput }
|
|
800
|
-
: { behavior: "deny", message: "User denied this action from LinkShell." };
|
|
801
|
-
},
|
|
802
|
-
};
|
|
803
|
-
if (input.model)
|
|
804
|
-
sdkOptions.model = input.model;
|
|
805
|
-
if (isRealClaudeSessionId(input.sessionId ?? this.claudeSessionId)) {
|
|
806
|
-
sdkOptions.resume = input.sessionId ?? this.claudeSessionId;
|
|
807
|
-
}
|
|
808
|
-
const toolNames = new Map();
|
|
809
|
-
let currentToolId;
|
|
810
|
-
let currentToolName;
|
|
811
|
-
let currentMessageId;
|
|
812
|
-
const progressItemId = `claude-progress:${input.clientMessageId}`;
|
|
813
|
-
try {
|
|
814
|
-
this.input.onNotification("item/started", {
|
|
815
|
-
sessionId: input.sessionId ?? this.claudeSessionId,
|
|
816
|
-
item: {
|
|
817
|
-
id: progressItemId,
|
|
818
|
-
type: "thinking",
|
|
819
|
-
text: "Claude 正在处理请求",
|
|
820
|
-
status: "running",
|
|
821
|
-
},
|
|
822
|
-
});
|
|
823
|
-
const queryPrompt = hasImages ? singleUserMessage(toClaudeMessageContent(inputBlocks)) : prompt;
|
|
824
|
-
for await (const message of this.query({ prompt: queryPrompt, options: sdkOptions })) {
|
|
825
|
-
if (abortController.signal.aborted)
|
|
826
|
-
break;
|
|
827
|
-
this.handleSdkMessage(message, {
|
|
828
|
-
cwd: input.cwd ?? this.input.cwd,
|
|
829
|
-
toolNames,
|
|
830
|
-
currentToolId: (value) => {
|
|
831
|
-
if (value !== undefined)
|
|
832
|
-
currentToolId = value ?? undefined;
|
|
833
|
-
return currentToolId;
|
|
834
|
-
},
|
|
835
|
-
currentToolName: (value) => {
|
|
836
|
-
if (value !== undefined)
|
|
837
|
-
currentToolName = value ?? undefined;
|
|
838
|
-
return currentToolName;
|
|
839
|
-
},
|
|
840
|
-
currentMessageId: (value) => {
|
|
841
|
-
if (value !== undefined)
|
|
842
|
-
currentMessageId = value ?? undefined;
|
|
843
|
-
return currentMessageId;
|
|
844
|
-
},
|
|
845
|
-
});
|
|
846
|
-
}
|
|
847
|
-
return { sessionId: this.claudeSessionId, status: abortController.signal.aborted ? "cancelled" : "completed" };
|
|
848
|
-
}
|
|
849
|
-
finally {
|
|
850
|
-
this.input.onNotification("item/completed", {
|
|
851
|
-
sessionId: this.claudeSessionId ?? input.sessionId,
|
|
852
|
-
item: {
|
|
853
|
-
id: progressItemId,
|
|
854
|
-
type: "thinking",
|
|
855
|
-
text: abortController.signal.aborted ? "Claude 已停止" : "Claude 已完成",
|
|
856
|
-
status: abortController.signal.aborted ? "failed" : "completed",
|
|
857
|
-
},
|
|
858
|
-
});
|
|
859
|
-
if (this.abortController === abortController)
|
|
860
|
-
this.abortController = undefined;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
cancel(input) {
|
|
864
|
-
this.abortController?.abort();
|
|
865
|
-
this.abortController = undefined;
|
|
866
|
-
for (const [, resolve] of this.permissionWaiters)
|
|
867
|
-
resolve("deny");
|
|
868
|
-
this.permissionWaiters.clear();
|
|
869
|
-
}
|
|
870
|
-
respondPermission(input) {
|
|
871
|
-
const waiter = this.permissionWaiters.get(input.requestId);
|
|
872
|
-
if (!waiter)
|
|
873
|
-
return;
|
|
874
|
-
this.permissionWaiters.delete(input.requestId);
|
|
875
|
-
waiter(input.outcome);
|
|
876
|
-
}
|
|
877
|
-
async listSessions() {
|
|
878
|
-
const sessions = [];
|
|
879
|
-
const seen = new Set();
|
|
880
|
-
const now = Date.now();
|
|
881
|
-
const liveKeys = new Set();
|
|
882
|
-
for (const projectDir of claudeProjectDirs(this.input.cwd)) {
|
|
883
|
-
try {
|
|
884
|
-
for (const entry of readdirSync(projectDir)) {
|
|
885
|
-
if (!entry.endsWith(".jsonl"))
|
|
886
|
-
continue;
|
|
887
|
-
const sessionId = entry.replace(".jsonl", "");
|
|
888
|
-
if (seen.has(sessionId))
|
|
889
|
-
continue;
|
|
890
|
-
seen.add(sessionId);
|
|
891
|
-
const filePath = join(projectDir, entry);
|
|
892
|
-
let stat;
|
|
893
|
-
try {
|
|
894
|
-
stat = statSync(filePath);
|
|
895
|
-
}
|
|
896
|
-
catch {
|
|
897
|
-
continue; // vanished between readdir and stat
|
|
898
|
-
}
|
|
899
|
-
liveKeys.add(filePath);
|
|
900
|
-
let cached = this.sessionMetaCache.get(filePath);
|
|
901
|
-
if (!cached || cached.mtimeMs !== stat.mtimeMs || cached.size !== stat.size) {
|
|
902
|
-
// Miss or stale: re-read + re-parse this ONE file, refresh the cache.
|
|
903
|
-
const { base, midTurn } = claudeSessionBaseFromFile(filePath, this.input.cwd, sessionId, stat.mtimeMs);
|
|
904
|
-
cached = { mtimeMs: stat.mtimeMs, size: stat.size, base, midTurn };
|
|
905
|
-
this.sessionMetaCache.set(filePath, cached);
|
|
906
|
-
}
|
|
907
|
-
// status is clock-dependent, so recompute it every call (cheap) even
|
|
908
|
-
// on a cache hit — a running session goes idle purely by time passing.
|
|
909
|
-
sessions.push({ ...cached.base, status: statusFromMidTurn(cached.midTurn, stat.mtimeMs, now) });
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
catch {
|
|
913
|
-
// Ignore unreadable Claude project storage.
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
// Drop cache entries for transcripts that no longer exist so the map can't
|
|
917
|
-
// grow without bound across a long-lived process.
|
|
918
|
-
if (this.sessionMetaCache.size > liveKeys.size) {
|
|
919
|
-
for (const key of this.sessionMetaCache.keys()) {
|
|
920
|
-
if (!liveKeys.has(key))
|
|
921
|
-
this.sessionMetaCache.delete(key);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
return { sessions };
|
|
925
|
-
}
|
|
926
|
-
async listModels() {
|
|
927
|
-
let defaultModel = "default";
|
|
928
|
-
// Read the user's configured default model from settings.json. This is always
|
|
929
|
-
// consulted (both for live discovery and for the fallback path).
|
|
930
|
-
let availableModels;
|
|
931
|
-
try {
|
|
932
|
-
const settingsPath = join(homedir(), ".claude", "settings.json");
|
|
933
|
-
if (existsSync(settingsPath)) {
|
|
934
|
-
const raw = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
935
|
-
if (typeof raw.model === "string" && raw.model.trim().length > 0) {
|
|
936
|
-
defaultModel = raw.model.trim();
|
|
937
|
-
}
|
|
938
|
-
if (Array.isArray(raw.availableModels)) {
|
|
939
|
-
availableModels = raw.availableModels.filter((m) => typeof m === "string");
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
catch {
|
|
944
|
-
// Fallback to "default" if settings file is unreadable.
|
|
945
|
-
}
|
|
946
|
-
// Dynamic model discovery: use the Claude Agent SDK's startup() to get the
|
|
947
|
-
// REAL model list for THIS API key (different keys/tiers see different models).
|
|
948
|
-
// Falls back to settings.json availableModels → hardcoded list on any failure.
|
|
949
|
-
if (this.startup) {
|
|
950
|
-
let warm;
|
|
951
|
-
try {
|
|
952
|
-
warm = await this.startup({ initializeTimeoutMs: 10_000 });
|
|
953
|
-
const q = warm.query("");
|
|
954
|
-
try {
|
|
955
|
-
const sdkModels = await q.supportedModels();
|
|
956
|
-
// Discover the REAL slash-command set for the installed Claude Code
|
|
957
|
-
// (built-ins like /goal + project/user custom commands), instead of a
|
|
958
|
-
// hardcoded list that drifts out of sync. Best-effort: an older SDK
|
|
959
|
-
// without supportedCommands() just leaves commands undefined and the
|
|
960
|
-
// static defaults still apply.
|
|
961
|
-
let commands;
|
|
962
|
-
try {
|
|
963
|
-
const sdkCommands = await q.supportedCommands?.();
|
|
964
|
-
if (Array.isArray(sdkCommands) && sdkCommands.length > 0)
|
|
965
|
-
commands = sdkCommands;
|
|
966
|
-
}
|
|
967
|
-
catch {
|
|
968
|
-
// supportedCommands() unavailable or failed — fall back to static defaults.
|
|
969
|
-
}
|
|
970
|
-
// Discover MCP server status the same best-effort way. Older SDKs
|
|
971
|
-
// without mcpServerStatus() just leave mcpServers undefined; live
|
|
972
|
-
// startupStatus events still populate it. Shape is passed through
|
|
973
|
-
// verbatim to the workspace parser, which tolerates array/record forms.
|
|
974
|
-
let mcpServers;
|
|
975
|
-
try {
|
|
976
|
-
mcpServers = await q.mcpServerStatus?.();
|
|
977
|
-
}
|
|
978
|
-
catch {
|
|
979
|
-
// mcpServerStatus() unavailable or failed — rely on live events.
|
|
980
|
-
}
|
|
981
|
-
if (sdkModels.length > 0) {
|
|
982
|
-
// Filter to admin-configured allowlist if present, then map to our format.
|
|
983
|
-
const allow = availableModels;
|
|
984
|
-
const filtered = allow
|
|
985
|
-
? sdkModels.filter((m) => allow.includes(m.value) || m.value === "default")
|
|
986
|
-
: sdkModels;
|
|
987
|
-
return {
|
|
988
|
-
defaultModel,
|
|
989
|
-
models: filtered.map((m) => ({ id: m.value, label: m.displayName || m.value })),
|
|
990
|
-
...(commands ? { commands } : {}),
|
|
991
|
-
...(mcpServers ? { mcpServers } : {}),
|
|
992
|
-
};
|
|
993
|
-
}
|
|
994
|
-
// No models surfaced, but commands may still be present — return them
|
|
995
|
-
// so the command palette stays in sync even when the model list is empty.
|
|
996
|
-
if (commands || mcpServers) {
|
|
997
|
-
const fallbackModels = availableModels ?? DEFAULT_FALLBACK_MODELS;
|
|
998
|
-
return {
|
|
999
|
-
defaultModel,
|
|
1000
|
-
models: toModelEntries(fallbackModels),
|
|
1001
|
-
...(commands ? { commands } : {}),
|
|
1002
|
-
...(mcpServers ? { mcpServers } : {}),
|
|
1003
|
-
};
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
catch {
|
|
1007
|
-
// supportedModels() failed — the subprocess may be in a bad state. Close it.
|
|
1008
|
-
}
|
|
1009
|
-
finally {
|
|
1010
|
-
try {
|
|
1011
|
-
warm.close();
|
|
1012
|
-
}
|
|
1013
|
-
catch { /* ignore */ }
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
catch {
|
|
1017
|
-
// startup() failed — clean up if it partially succeeded.
|
|
1018
|
-
try {
|
|
1019
|
-
warm?.close();
|
|
1020
|
-
}
|
|
1021
|
-
catch { /* ignore */ }
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
// Fallback: admin-configured allowlist, or the full Claude Code alias set.
|
|
1025
|
-
const fallbackModels = availableModels ?? DEFAULT_FALLBACK_MODELS;
|
|
1026
|
-
return {
|
|
1027
|
-
defaultModel,
|
|
1028
|
-
models: toModelEntries(fallbackModels),
|
|
1029
|
-
};
|
|
1030
|
-
}
|
|
1031
|
-
stop() {
|
|
1032
|
-
this.cancel({});
|
|
1033
|
-
}
|
|
1034
|
-
handleSdkMessage(event, state) {
|
|
1035
|
-
switch (event.type) {
|
|
1036
|
-
case "system": {
|
|
1037
|
-
if (event.subtype === "init" && typeof event.session_id === "string") {
|
|
1038
|
-
this.claudeSessionId = event.session_id;
|
|
1039
|
-
this.input.onNotification("thread/started", {
|
|
1040
|
-
sessionId: event.session_id,
|
|
1041
|
-
threadId: event.session_id,
|
|
1042
|
-
});
|
|
1043
|
-
this.input.onNotification("initialized", {
|
|
1044
|
-
sessionId: event.session_id,
|
|
1045
|
-
threadId: event.session_id,
|
|
1046
|
-
cwd: event.cwd ?? state.cwd,
|
|
1047
|
-
model: event.model,
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
break;
|
|
1051
|
-
}
|
|
1052
|
-
case "assistant": {
|
|
1053
|
-
const rawMessage = event.message;
|
|
1054
|
-
const content = (Array.isArray(rawMessage?.content) ? rawMessage.content : []);
|
|
1055
|
-
state.currentMessageId(null);
|
|
1056
|
-
for (const block of content) {
|
|
1057
|
-
if (block.type === "thinking") {
|
|
1058
|
-
this.input.onNotification("item/completed", {
|
|
1059
|
-
sessionId: this.claudeSessionId,
|
|
1060
|
-
item: {
|
|
1061
|
-
id: event.uuid ?? id("thinking"),
|
|
1062
|
-
type: "thinking",
|
|
1063
|
-
text: block.thinking,
|
|
1064
|
-
status: "completed",
|
|
1065
|
-
},
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
else if (block.type === "text") {
|
|
1069
|
-
const messageId = (typeof rawMessage?.id === "string" ? rawMessage.id : undefined) ?? event.uuid ?? id("msg");
|
|
1070
|
-
state.currentMessageId(messageId);
|
|
1071
|
-
this.input.onNotification("item/agentMessage/delta", {
|
|
1072
|
-
sessionId: this.claudeSessionId,
|
|
1073
|
-
itemId: messageId,
|
|
1074
|
-
delta: block.text,
|
|
1075
|
-
});
|
|
1076
|
-
}
|
|
1077
|
-
else if (block.type === "tool_use") {
|
|
1078
|
-
const toolId = block.id ?? id("tool");
|
|
1079
|
-
const toolName = block.name ?? "tool";
|
|
1080
|
-
state.currentToolId(toolId);
|
|
1081
|
-
state.currentToolName(toolName);
|
|
1082
|
-
state.toolNames.set(toolId, toolName);
|
|
1083
|
-
this.input.onNotification("item/started", {
|
|
1084
|
-
sessionId: this.claudeSessionId,
|
|
1085
|
-
item: {
|
|
1086
|
-
id: toolId,
|
|
1087
|
-
type: toolName === "Bash" ? "commandExecution" : toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit" ? "fileChange" : "toolCall",
|
|
1088
|
-
toolName,
|
|
1089
|
-
tool: toolName,
|
|
1090
|
-
input: block.input,
|
|
1091
|
-
command: block.input?.command,
|
|
1092
|
-
cwd: block.input?.cwd ?? state.cwd,
|
|
1093
|
-
status: "running",
|
|
1094
|
-
},
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
const messageId = state.currentMessageId();
|
|
1099
|
-
if (messageId) {
|
|
1100
|
-
this.input.onNotification("item/completed", {
|
|
1101
|
-
sessionId: this.claudeSessionId,
|
|
1102
|
-
item: { id: messageId, type: "agentMessage", status: "completed" },
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1105
|
-
break;
|
|
1106
|
-
}
|
|
1107
|
-
case "user": {
|
|
1108
|
-
const rawMessage = event.message;
|
|
1109
|
-
const content = (Array.isArray(rawMessage?.content) ? rawMessage.content : []);
|
|
1110
|
-
for (const block of content) {
|
|
1111
|
-
if (block.type !== "tool_result")
|
|
1112
|
-
continue;
|
|
1113
|
-
const toolId = block.tool_use_id ?? state.currentToolId();
|
|
1114
|
-
const toolName = (toolId ? state.toolNames.get(toolId) : undefined) ?? state.currentToolName();
|
|
1115
|
-
const output = extractToolResultText(block.content);
|
|
1116
|
-
this.input.onNotification("item/completed", {
|
|
1117
|
-
sessionId: this.claudeSessionId,
|
|
1118
|
-
item: {
|
|
1119
|
-
id: toolId ?? id("tool"),
|
|
1120
|
-
type: "toolCall",
|
|
1121
|
-
toolName,
|
|
1122
|
-
tool: toolName,
|
|
1123
|
-
status: block.is_error ? "failed" : "completed",
|
|
1124
|
-
output,
|
|
1125
|
-
aggregatedOutput: output,
|
|
1126
|
-
isError: block.is_error === true,
|
|
1127
|
-
},
|
|
1128
|
-
});
|
|
1129
|
-
}
|
|
1130
|
-
break;
|
|
1131
|
-
}
|
|
1132
|
-
case "result": {
|
|
1133
|
-
if (typeof event.session_id === "string")
|
|
1134
|
-
this.claudeSessionId = event.session_id;
|
|
1135
|
-
this.input.onNotification("turn/completed", {
|
|
1136
|
-
sessionId: this.claudeSessionId,
|
|
1137
|
-
durationMs: event.duration_ms,
|
|
1138
|
-
totalCostUsd: event.total_cost_usd,
|
|
1139
|
-
usage: event.usage,
|
|
1140
|
-
isError: event.subtype === "error",
|
|
1141
|
-
});
|
|
1142
|
-
break;
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
//# sourceMappingURL=claude-sdk-client.js.map
|