linkshell-cli 0.3.28 → 0.3.29
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 +1 -27
- package/dist/cli/src/runtime/acp/agent-workspace.js +168 -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 +35 -81
- 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 +35 -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 +11940 -3423
- package/dist/shared-protocol/src/index.js +98 -164
- 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 +176 -1094
- package/src/runtime/acp/claude-sdk-client.ts +37 -85
- package/src/runtime/acp/claude-stream-json-client.ts +41 -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
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { readdirSync, existsSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
4
|
import type { AgentFraming, AgentProtocol } from "./provider-resolver.js";
|
|
4
5
|
|
|
5
6
|
type AgentPermissionMode = "read_only" | "workspace_write" | "full_access";
|
|
6
|
-
type AgentCollaborationMode = "default" | "plan";
|
|
7
7
|
|
|
8
8
|
interface ClaudeContentBlock {
|
|
9
9
|
type?: string;
|
|
@@ -37,13 +37,21 @@ type AgentInputContentBlock = {
|
|
|
37
37
|
text?: string;
|
|
38
38
|
data?: string;
|
|
39
39
|
mimeType?: string;
|
|
40
|
-
path?: string;
|
|
41
40
|
};
|
|
42
41
|
|
|
43
42
|
function id(prefix: string): string {
|
|
44
43
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
45
44
|
}
|
|
46
45
|
|
|
46
|
+
function projectHash(cwd: string): string {
|
|
47
|
+
return (
|
|
48
|
+
"-" +
|
|
49
|
+
resolve(cwd)
|
|
50
|
+
.replace(/\/$/, "")
|
|
51
|
+
.replace(/\//g, "-")
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
function extractToolResultText(content: unknown): string {
|
|
48
56
|
if (typeof content === "string") return content;
|
|
49
57
|
if (Array.isArray(content)) {
|
|
@@ -86,9 +94,6 @@ function toClaudeMessageContent(blocks: AgentInputContentBlock[]): Record<string
|
|
|
86
94
|
},
|
|
87
95
|
};
|
|
88
96
|
}
|
|
89
|
-
if (block.type === "file" && block.path) {
|
|
90
|
-
return { type: "text", text: block.text ?? `@${block.path}` };
|
|
91
|
-
}
|
|
92
97
|
return { type: "text", text: block.text ?? "" };
|
|
93
98
|
})
|
|
94
99
|
.filter((block) =>
|
|
@@ -135,42 +140,6 @@ function isInsideCwd(cwd: string, candidate: string): boolean {
|
|
|
135
140
|
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
136
141
|
}
|
|
137
142
|
|
|
138
|
-
function samePath(left: string, right: string): boolean {
|
|
139
|
-
return resolve(left) === resolve(right);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function isClaudeFileTool(toolName: string): boolean {
|
|
143
|
-
return toolName === "Write" ||
|
|
144
|
-
toolName === "Edit" ||
|
|
145
|
-
toolName === "MultiEdit" ||
|
|
146
|
-
toolName === "NotebookEdit";
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function filePathFromToolInput(toolInput: unknown): string | undefined {
|
|
150
|
-
return stringField(toolInput, ["file_path", "path", "notebook_path"]);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function fileChangeKind(toolName: string): string {
|
|
154
|
-
return toolName === "Write" ? "create" : "update";
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function claudeEffort(value: string | undefined): "low" | "medium" | "high" | "xhigh" | undefined {
|
|
158
|
-
if (value === "low" || value === "medium" || value === "high" || value === "xhigh") return value;
|
|
159
|
-
if (value === "minimal") return "low";
|
|
160
|
-
return undefined;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function claudePermissionMode(
|
|
164
|
-
permissionMode: AgentPermissionMode | undefined,
|
|
165
|
-
collaborationMode: AgentCollaborationMode | undefined,
|
|
166
|
-
): "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" | undefined {
|
|
167
|
-
if (collaborationMode === "plan") return "plan";
|
|
168
|
-
if (permissionMode === "full_access") return "bypassPermissions";
|
|
169
|
-
if (permissionMode === "workspace_write") return "acceptEdits";
|
|
170
|
-
if (permissionMode === "read_only") return "dontAsk";
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
143
|
function outcomeFromPermissionResponse(value: unknown): "allow" | "deny" {
|
|
175
144
|
const raw = value && typeof value === "object" ? value as Record<string, unknown> : {};
|
|
176
145
|
if (raw.behavior === "allow") return "allow";
|
|
@@ -228,11 +197,6 @@ export class ClaudeSdkClient {
|
|
|
228
197
|
}
|
|
229
198
|
|
|
230
199
|
async loadSession(input: { sessionId: string; cwd: string; mcpServers?: unknown }): Promise<unknown> {
|
|
231
|
-
if (!isRealClaudeSessionId(input.sessionId)) {
|
|
232
|
-
this.claudeSessionId = undefined;
|
|
233
|
-
return { sessionId: undefined, status: "ready", cwd: input.cwd };
|
|
234
|
-
}
|
|
235
|
-
this.assertResumeCwd(input.sessionId, input.cwd);
|
|
236
200
|
this.claudeSessionId = input.sessionId;
|
|
237
201
|
return { sessionId: input.sessionId, status: "loaded", cwd: input.cwd };
|
|
238
202
|
}
|
|
@@ -244,15 +208,11 @@ export class ClaudeSdkClient {
|
|
|
244
208
|
model?: string;
|
|
245
209
|
reasoningEffort?: string;
|
|
246
210
|
permissionMode?: AgentPermissionMode;
|
|
247
|
-
collaborationMode?:
|
|
211
|
+
collaborationMode?: "default" | "plan";
|
|
248
212
|
cwd: string;
|
|
249
213
|
}): Promise<unknown> {
|
|
250
214
|
if (!this.query) await this.initialize();
|
|
251
215
|
if (!this.query) throw new Error("Claude Agent SDK is not initialized");
|
|
252
|
-
const resumeSessionId = isRealClaudeSessionId(input.sessionId ?? this.claudeSessionId)
|
|
253
|
-
? input.sessionId ?? this.claudeSessionId
|
|
254
|
-
: undefined;
|
|
255
|
-
if (resumeSessionId) this.assertResumeCwd(resumeSessionId, input.cwd ?? this.input.cwd);
|
|
256
216
|
|
|
257
217
|
this.abortController?.abort();
|
|
258
218
|
const abortController = new AbortController();
|
|
@@ -263,7 +223,6 @@ export class ClaudeSdkClient {
|
|
|
263
223
|
const prompt = inputBlocks
|
|
264
224
|
.map((block) => {
|
|
265
225
|
if (block.type === "image") return `[${block.mimeType ?? "image"} attachment]`;
|
|
266
|
-
if (block.type === "file" && block.path) return block.text ?? `@${block.path}`;
|
|
267
226
|
return block.text ?? "";
|
|
268
227
|
})
|
|
269
228
|
.filter(Boolean)
|
|
@@ -277,11 +236,11 @@ export class ClaudeSdkClient {
|
|
|
277
236
|
if (["Read", "Glob", "Grep", "LS", "NotebookRead", "TodoRead"].includes(toolName)) {
|
|
278
237
|
return { behavior: "allow" };
|
|
279
238
|
}
|
|
280
|
-
if (input.permissionMode === "read_only" && ["Write", "Edit", "MultiEdit", "
|
|
239
|
+
if (input.permissionMode === "read_only" && ["Write", "Edit", "MultiEdit", "Bash"].includes(toolName)) {
|
|
281
240
|
return { behavior: "deny", message: "Read-only mode is active." };
|
|
282
241
|
}
|
|
283
|
-
if (input.permissionMode === "workspace_write" &&
|
|
284
|
-
const filePath =
|
|
242
|
+
if (input.permissionMode === "workspace_write" && ["Write", "Edit", "MultiEdit"].includes(toolName)) {
|
|
243
|
+
const filePath = stringField(toolInput, ["file_path", "path", "notebook_path"]);
|
|
285
244
|
if (filePath && isInsideCwd(input.cwd ?? this.input.cwd, filePath)) {
|
|
286
245
|
return { behavior: "allow" };
|
|
287
246
|
}
|
|
@@ -304,15 +263,8 @@ export class ClaudeSdkClient {
|
|
|
304
263
|
},
|
|
305
264
|
};
|
|
306
265
|
if (input.model) sdkOptions.model = input.model;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const permissionMode = claudePermissionMode(input.permissionMode, input.collaborationMode);
|
|
310
|
-
if (permissionMode) {
|
|
311
|
-
sdkOptions.permissionMode = permissionMode;
|
|
312
|
-
if (permissionMode === "bypassPermissions") sdkOptions.allowDangerouslySkipPermissions = true;
|
|
313
|
-
}
|
|
314
|
-
if (resumeSessionId) {
|
|
315
|
-
sdkOptions.resume = resumeSessionId;
|
|
266
|
+
if (isRealClaudeSessionId(input.sessionId ?? this.claudeSessionId)) {
|
|
267
|
+
sdkOptions.resume = input.sessionId ?? this.claudeSessionId;
|
|
316
268
|
}
|
|
317
269
|
|
|
318
270
|
const toolNames = new Map<string, string>();
|
|
@@ -381,13 +333,28 @@ export class ClaudeSdkClient {
|
|
|
381
333
|
}
|
|
382
334
|
|
|
383
335
|
async listSessions(): Promise<unknown> {
|
|
384
|
-
|
|
336
|
+
const projectDir = join(homedir(), ".claude", "projects", projectHash(this.input.cwd));
|
|
337
|
+
if (!existsSync(projectDir)) return { sessions: [] };
|
|
338
|
+
const sessions: Array<{ id: string; cwd: string; lastModified: number }> = [];
|
|
339
|
+
try {
|
|
340
|
+
for (const entry of readdirSync(projectDir)) {
|
|
341
|
+
if (entry.endsWith(".jsonl")) {
|
|
342
|
+
sessions.push({
|
|
343
|
+
id: entry.replace(".jsonl", ""),
|
|
344
|
+
cwd: this.input.cwd,
|
|
345
|
+
lastModified: 0,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} catch {
|
|
350
|
+
// Ignore unreadable Claude project storage.
|
|
351
|
+
}
|
|
352
|
+
return { sessions };
|
|
385
353
|
}
|
|
386
354
|
|
|
387
355
|
async listModels(): Promise<unknown> {
|
|
388
356
|
return {
|
|
389
357
|
defaultModel: "default",
|
|
390
|
-
reasoningEfforts: ["low", "medium", "high", "xhigh"],
|
|
391
358
|
models: [
|
|
392
359
|
{ id: "sonnet", label: "Sonnet" },
|
|
393
360
|
{ id: "opus", label: "Opus" },
|
|
@@ -462,17 +429,10 @@ export class ClaudeSdkClient {
|
|
|
462
429
|
sessionId: this.claudeSessionId,
|
|
463
430
|
item: {
|
|
464
431
|
id: toolId,
|
|
465
|
-
type: toolName === "Bash" ? "commandExecution" :
|
|
432
|
+
type: toolName === "Bash" ? "commandExecution" : toolName === "Write" || toolName === "Edit" || toolName === "MultiEdit" ? "fileChange" : "toolCall",
|
|
466
433
|
toolName,
|
|
467
434
|
tool: toolName,
|
|
468
435
|
input: block.input,
|
|
469
|
-
path: isClaudeFileTool(toolName) ? filePathFromToolInput(block.input) : undefined,
|
|
470
|
-
changes: isClaudeFileTool(toolName)
|
|
471
|
-
? [{
|
|
472
|
-
path: filePathFromToolInput(block.input),
|
|
473
|
-
kind: fileChangeKind(toolName),
|
|
474
|
-
}].filter((entry) => Boolean(entry.path))
|
|
475
|
-
: undefined,
|
|
476
436
|
command: block.input?.command as string | undefined,
|
|
477
437
|
cwd: block.input?.cwd as string | undefined ?? state.cwd,
|
|
478
438
|
status: "running",
|
|
@@ -501,7 +461,7 @@ export class ClaudeSdkClient {
|
|
|
501
461
|
sessionId: this.claudeSessionId,
|
|
502
462
|
item: {
|
|
503
463
|
id: toolId ?? id("tool"),
|
|
504
|
-
type:
|
|
464
|
+
type: "toolCall",
|
|
505
465
|
toolName,
|
|
506
466
|
tool: toolName,
|
|
507
467
|
status: block.is_error ? "failed" : "completed",
|
|
@@ -526,12 +486,4 @@ export class ClaudeSdkClient {
|
|
|
526
486
|
}
|
|
527
487
|
}
|
|
528
488
|
}
|
|
529
|
-
|
|
530
|
-
private assertResumeCwd(sessionId: string, cwd: string): void {
|
|
531
|
-
const stored = listClaudeStoredSessions(cwd).sessions.find((session) => session.id === sessionId);
|
|
532
|
-
if (!stored || samePath(stored.cwd, cwd)) return;
|
|
533
|
-
throw new Error(
|
|
534
|
-
`Claude session ${sessionId} belongs to ${stored.cwd}. Reopen that workspace or start a new Claude session from ${cwd}.`,
|
|
535
|
-
);
|
|
536
|
-
}
|
|
537
489
|
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
|
-
import {
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { readdirSync, existsSync } from "node:fs";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
4
6
|
import type { AgentFraming, AgentProtocol } from "./provider-resolver.js";
|
|
5
7
|
|
|
6
8
|
type AgentPermissionMode = "read_only" | "workspace_write" | "full_access";
|
|
7
|
-
type AgentCollaborationMode = "default" | "plan";
|
|
8
9
|
|
|
9
10
|
type AgentInputContentBlock = {
|
|
10
11
|
type?: string;
|
|
11
12
|
text?: string;
|
|
12
13
|
data?: string;
|
|
13
14
|
mimeType?: string;
|
|
14
|
-
path?: string;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
interface ClaudeStreamEvent {
|
|
@@ -37,6 +37,16 @@ interface ClaudeContentBlock {
|
|
|
37
37
|
signature?: string;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// Hash a directory path the same way Claude Code does for project storage
|
|
41
|
+
function projectHash(cwd: string): string {
|
|
42
|
+
return (
|
|
43
|
+
"-" +
|
|
44
|
+
resolve(cwd)
|
|
45
|
+
.replace(/\/$/, "")
|
|
46
|
+
.replace(/\//g, "-")
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
function id(prefix: string): string {
|
|
41
51
|
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
42
52
|
}
|
|
@@ -71,39 +81,6 @@ function splitImageDataUrl(value: string, fallbackMimeType = "image/png"): { dat
|
|
|
71
81
|
};
|
|
72
82
|
}
|
|
73
83
|
|
|
74
|
-
function claudeEffort(value: string | undefined): "low" | "medium" | "high" | "xhigh" | undefined {
|
|
75
|
-
if (value === "low" || value === "medium" || value === "high" || value === "xhigh") return value;
|
|
76
|
-
if (value === "minimal") return "low";
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function claudePermissionMode(
|
|
81
|
-
permissionMode: AgentPermissionMode | undefined,
|
|
82
|
-
collaborationMode: AgentCollaborationMode | undefined,
|
|
83
|
-
): "default" | "acceptEdits" | "bypassPermissions" | "plan" | "dontAsk" {
|
|
84
|
-
if (collaborationMode === "plan") return "plan";
|
|
85
|
-
if (permissionMode === "full_access") return "bypassPermissions";
|
|
86
|
-
if (permissionMode === "workspace_write") return "acceptEdits";
|
|
87
|
-
if (permissionMode === "read_only") return "dontAsk";
|
|
88
|
-
return "default";
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function isClaudeFileTool(toolName: string): boolean {
|
|
92
|
-
return toolName === "Write" ||
|
|
93
|
-
toolName === "Edit" ||
|
|
94
|
-
toolName === "MultiEdit" ||
|
|
95
|
-
toolName === "NotebookEdit";
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function filePathFromToolInput(toolInput: Record<string, unknown> | undefined): string | undefined {
|
|
99
|
-
const filePath = toolInput?.file_path ?? toolInput?.path ?? toolInput?.notebook_path;
|
|
100
|
-
return typeof filePath === "string" && filePath.trim() ? filePath : undefined;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function fileChangeKind(toolName: string): string {
|
|
104
|
-
return toolName === "Write" ? "create" : "update";
|
|
105
|
-
}
|
|
106
|
-
|
|
107
84
|
export class ClaudeStreamJsonClient {
|
|
108
85
|
private child: ChildProcessWithoutNullStreams | undefined;
|
|
109
86
|
private claudeSessionId: string | undefined;
|
|
@@ -153,7 +130,7 @@ export class ClaudeStreamJsonClient {
|
|
|
153
130
|
model?: string;
|
|
154
131
|
reasoningEffort?: string;
|
|
155
132
|
permissionMode?: AgentPermissionMode;
|
|
156
|
-
collaborationMode?:
|
|
133
|
+
collaborationMode?: "default" | "plan";
|
|
157
134
|
cwd: string;
|
|
158
135
|
}): Promise<unknown> {
|
|
159
136
|
if (this.child) {
|
|
@@ -168,11 +145,8 @@ export class ClaudeStreamJsonClient {
|
|
|
168
145
|
"--output-format", "stream-json",
|
|
169
146
|
"--input-format", "stream-json",
|
|
170
147
|
"--verbose",
|
|
171
|
-
"--permission-mode",
|
|
148
|
+
"--permission-mode", "bypassPermissions",
|
|
172
149
|
];
|
|
173
|
-
if (input.permissionMode === "read_only") {
|
|
174
|
-
args.push("--disallowedTools", "Bash,Write,Edit,MultiEdit,NotebookEdit");
|
|
175
|
-
}
|
|
176
150
|
|
|
177
151
|
// Use stored session for --resume (only when we have a real session ID from system.init)
|
|
178
152
|
if (this.claudeSessionId) {
|
|
@@ -186,10 +160,6 @@ export class ClaudeStreamJsonClient {
|
|
|
186
160
|
if (input.model) {
|
|
187
161
|
args.push("--model", input.model);
|
|
188
162
|
}
|
|
189
|
-
const effort = claudeEffort(input.reasoningEffort);
|
|
190
|
-
if (effort) {
|
|
191
|
-
args.push("--effort", effort);
|
|
192
|
-
}
|
|
193
163
|
|
|
194
164
|
// Build the user message
|
|
195
165
|
const contentBlocks = (input.content as AgentInputContentBlock[]).map(
|
|
@@ -198,9 +168,6 @@ export class ClaudeStreamJsonClient {
|
|
|
198
168
|
const image = splitImageDataUrl(block.data, block.mimeType);
|
|
199
169
|
return { type: "image", source: { type: "base64", media_type: image.mimeType, data: image.data } };
|
|
200
170
|
}
|
|
201
|
-
if (block.type === "file" && block.path) {
|
|
202
|
-
return { type: "text", text: block.text ?? `@${block.path}` };
|
|
203
|
-
}
|
|
204
171
|
return { type: "text", text: block.text ?? "" };
|
|
205
172
|
},
|
|
206
173
|
);
|
|
@@ -352,17 +319,10 @@ export class ClaudeStreamJsonClient {
|
|
|
352
319
|
sessionId: this.claudeSessionId,
|
|
353
320
|
item: {
|
|
354
321
|
id: block.id ?? id("tool"),
|
|
355
|
-
type: toolName === "Bash" ? "commandExecution" :
|
|
322
|
+
type: toolName === "Bash" ? "commandExecution" : toolName === "Write" || toolName === "Edit" ? "fileChange" : "toolCall",
|
|
356
323
|
toolName: block.name,
|
|
357
324
|
tool: block.name,
|
|
358
325
|
input: block.input,
|
|
359
|
-
path: isClaudeFileTool(toolName) ? filePathFromToolInput(block.input) : undefined,
|
|
360
|
-
changes: isClaudeFileTool(toolName)
|
|
361
|
-
? [{
|
|
362
|
-
path: filePathFromToolInput(block.input),
|
|
363
|
-
kind: fileChangeKind(toolName),
|
|
364
|
-
}].filter((entry) => Boolean(entry.path))
|
|
365
|
-
: undefined,
|
|
366
326
|
command: block.input?.command as string | undefined,
|
|
367
327
|
cwd: block.input?.cwd as string | undefined ?? cwd,
|
|
368
328
|
status: "running",
|
|
@@ -411,7 +371,7 @@ export class ClaudeStreamJsonClient {
|
|
|
411
371
|
sessionId: this.claudeSessionId,
|
|
412
372
|
item: {
|
|
413
373
|
id: toolId ?? id("tool"),
|
|
414
|
-
type:
|
|
374
|
+
type: "toolCall",
|
|
415
375
|
toolName,
|
|
416
376
|
tool: toolName,
|
|
417
377
|
status: isError ? "failed" : "completed",
|
|
@@ -514,13 +474,35 @@ export class ClaudeStreamJsonClient {
|
|
|
514
474
|
}
|
|
515
475
|
|
|
516
476
|
async listSessions(): Promise<unknown> {
|
|
517
|
-
|
|
477
|
+
const home = homedir();
|
|
478
|
+
const projectDir = join(home, ".claude", "projects", projectHash(this.input.cwd));
|
|
479
|
+
|
|
480
|
+
if (!existsSync(projectDir)) {
|
|
481
|
+
return { sessions: [] };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const sessions: Array<{ id: string; cwd: string; lastModified: number }> = [];
|
|
485
|
+
try {
|
|
486
|
+
for (const entry of readdirSync(projectDir)) {
|
|
487
|
+
if (entry.endsWith(".jsonl")) {
|
|
488
|
+
const sessionId = entry.replace(".jsonl", "");
|
|
489
|
+
sessions.push({
|
|
490
|
+
id: sessionId,
|
|
491
|
+
cwd: this.input.cwd,
|
|
492
|
+
lastModified: 0, // would need fs.statSync for accurate time
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} catch {
|
|
497
|
+
// directory read failed
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return { sessions };
|
|
518
501
|
}
|
|
519
502
|
|
|
520
503
|
async listModels(): Promise<unknown> {
|
|
521
504
|
return {
|
|
522
505
|
defaultModel: "default",
|
|
523
|
-
reasoningEfforts: ["low", "medium", "high", "xhigh"],
|
|
524
506
|
models: [
|
|
525
507
|
{ id: "sonnet", label: "Sonnet" },
|
|
526
508
|
{ id: "opus", label: "Opus" },
|
|
@@ -1,33 +1,27 @@
|
|
|
1
1
|
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
2
|
import type { AgentFraming } from "./provider-resolver.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
jsonrpc
|
|
4
|
+
interface JsonRpcRequest {
|
|
5
|
+
jsonrpc: "2.0";
|
|
6
6
|
id: number | string;
|
|
7
7
|
method: string;
|
|
8
8
|
params?: unknown;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
jsonrpc
|
|
11
|
+
interface JsonRpcResponse {
|
|
12
|
+
jsonrpc: "2.0";
|
|
13
13
|
id: number | string;
|
|
14
14
|
result?: unknown;
|
|
15
15
|
error?: { code: number; message: string; data?: unknown };
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
jsonrpc
|
|
18
|
+
interface JsonRpcNotification {
|
|
19
|
+
jsonrpc: "2.0";
|
|
20
20
|
method: string;
|
|
21
21
|
params?: unknown;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
interface JsonRpcTransportOptions {
|
|
27
|
-
includeJsonrpc?: boolean;
|
|
28
|
-
verbose?: boolean;
|
|
29
|
-
onStderrData?: (message: string) => void;
|
|
30
|
-
}
|
|
24
|
+
type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
|
|
31
25
|
|
|
32
26
|
export class JsonRpcStdioTransport {
|
|
33
27
|
private child: ChildProcessWithoutNullStreams | undefined;
|
|
@@ -48,8 +42,6 @@ export class JsonRpcStdioTransport {
|
|
|
48
42
|
private readonly onNotification: (method: string, params: unknown) => void,
|
|
49
43
|
private readonly onRequest: (method: string, params: unknown) => Promise<unknown> | unknown,
|
|
50
44
|
private readonly onExit: (message: string) => void,
|
|
51
|
-
private readonly onMessage?: (message: JsonRpcMessage) => boolean | void,
|
|
52
|
-
private readonly options: JsonRpcTransportOptions = {},
|
|
53
45
|
) {}
|
|
54
46
|
|
|
55
47
|
start(cwd: string): void {
|
|
@@ -65,10 +57,7 @@ export class JsonRpcStdioTransport {
|
|
|
65
57
|
this.child.stdout.on("data", (chunk: string) => this.read(chunk));
|
|
66
58
|
this.child.stderr.on("data", (chunk: string) => {
|
|
67
59
|
const trimmed = chunk.trim();
|
|
68
|
-
if (trimmed) {
|
|
69
|
-
process.stderr.write(`[agent:stderr] ${trimmed}\n`);
|
|
70
|
-
this.options.onStderrData?.(trimmed);
|
|
71
|
-
}
|
|
60
|
+
if (trimmed) process.stderr.write(`[agent:stderr] ${trimmed}\n`);
|
|
72
61
|
});
|
|
73
62
|
this.child.on("error", (error) => this.failAll(error.message));
|
|
74
63
|
this.child.on("exit", (code, signal) => {
|
|
@@ -81,7 +70,7 @@ export class JsonRpcStdioTransport {
|
|
|
81
70
|
return Promise.reject(new Error("ACP agent is not running"));
|
|
82
71
|
}
|
|
83
72
|
const id = this.nextId++;
|
|
84
|
-
const message: JsonRpcRequest =
|
|
73
|
+
const message: JsonRpcRequest = { jsonrpc: "2.0", id, method, params };
|
|
85
74
|
return new Promise((resolve, reject) => {
|
|
86
75
|
const timer = timeoutMs && timeoutMs > 0
|
|
87
76
|
? setTimeout(() => {
|
|
@@ -95,11 +84,7 @@ export class JsonRpcStdioTransport {
|
|
|
95
84
|
}
|
|
96
85
|
|
|
97
86
|
notify(method: string, params?: unknown): void {
|
|
98
|
-
this.write(
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
send(message: JsonRpcMessage): void {
|
|
102
|
-
this.write(this.normalizeOutgoing(message));
|
|
87
|
+
this.write({ jsonrpc: "2.0", method, params });
|
|
103
88
|
}
|
|
104
89
|
|
|
105
90
|
stop(): void {
|
|
@@ -109,17 +94,6 @@ export class JsonRpcStdioTransport {
|
|
|
109
94
|
this.failAll("ACP transport stopped");
|
|
110
95
|
}
|
|
111
96
|
|
|
112
|
-
private normalizeOutgoing<T extends JsonRpcMessage>(message: T): T {
|
|
113
|
-
const includeJsonrpc = this.options.includeJsonrpc ?? true;
|
|
114
|
-
if (includeJsonrpc) return { jsonrpc: "2.0", ...message } as T;
|
|
115
|
-
const { jsonrpc: _jsonrpc, ...rest } = message;
|
|
116
|
-
return rest as T;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private withJsonrpc<T extends JsonRpcMessage>(message: T): T {
|
|
120
|
-
return this.normalizeOutgoing(message);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
97
|
private write(message: JsonRpcMessage): void {
|
|
124
98
|
const raw = JSON.stringify(message);
|
|
125
99
|
if (this.framing === "newline") {
|
|
@@ -159,13 +133,6 @@ export class JsonRpcStdioTransport {
|
|
|
159
133
|
try {
|
|
160
134
|
message = JSON.parse(raw) as JsonRpcMessage;
|
|
161
135
|
} catch {
|
|
162
|
-
if (this.options.verbose) {
|
|
163
|
-
process.stderr.write(`[agent:rpc] ignored invalid JSON: ${raw.slice(0, 200)}\n`);
|
|
164
|
-
}
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (this.onMessage?.(message)) {
|
|
169
136
|
return;
|
|
170
137
|
}
|
|
171
138
|
|
|
@@ -182,15 +149,16 @@ export class JsonRpcStdioTransport {
|
|
|
182
149
|
|
|
183
150
|
if ("method" in message && "id" in message) {
|
|
184
151
|
Promise.resolve(this.onRequest(message.method, message.params))
|
|
185
|
-
.then((result) => this.write(
|
|
152
|
+
.then((result) => this.write({ jsonrpc: "2.0", id: message.id, result }))
|
|
186
153
|
.catch((error) => {
|
|
187
|
-
this.write(
|
|
154
|
+
this.write({
|
|
155
|
+
jsonrpc: "2.0",
|
|
188
156
|
id: message.id,
|
|
189
157
|
error: {
|
|
190
158
|
code: -32000,
|
|
191
159
|
message: error instanceof Error ? error.message : String(error),
|
|
192
160
|
},
|
|
193
|
-
})
|
|
161
|
+
});
|
|
194
162
|
});
|
|
195
163
|
return;
|
|
196
164
|
}
|
|
@@ -16,34 +16,6 @@ export interface AgentCommandConfig {
|
|
|
16
16
|
framing: AgentFraming;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function resolveCodexAppServerCommands(input: {
|
|
20
|
-
command?: string;
|
|
21
|
-
}): AgentCommandConfig[] {
|
|
22
|
-
const explicit = input.command?.trim();
|
|
23
|
-
if (explicit) {
|
|
24
|
-
return [{
|
|
25
|
-
provider: "codex",
|
|
26
|
-
command: explicit,
|
|
27
|
-
protocol: "codex-app-server",
|
|
28
|
-
framing: "newline",
|
|
29
|
-
}];
|
|
30
|
-
}
|
|
31
|
-
return [
|
|
32
|
-
{
|
|
33
|
-
provider: "codex",
|
|
34
|
-
command: "codex app-server --listen stdio://",
|
|
35
|
-
protocol: "codex-app-server",
|
|
36
|
-
framing: "newline",
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
provider: "codex",
|
|
40
|
-
command: "codex app-server proxy",
|
|
41
|
-
protocol: "codex-app-server",
|
|
42
|
-
framing: "newline",
|
|
43
|
-
},
|
|
44
|
-
];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
19
|
export function resolveAgentCommand(input: {
|
|
48
20
|
provider: AgentProvider;
|
|
49
21
|
command?: string;
|
|
@@ -61,7 +33,12 @@ export function resolveAgentCommand(input: {
|
|
|
61
33
|
}
|
|
62
34
|
|
|
63
35
|
if (input.provider === "codex") {
|
|
64
|
-
return
|
|
36
|
+
return {
|
|
37
|
+
provider: "codex",
|
|
38
|
+
command: "codex app-server --listen stdio://",
|
|
39
|
+
protocol: "codex-app-server",
|
|
40
|
+
framing: "newline",
|
|
41
|
+
};
|
|
65
42
|
}
|
|
66
43
|
|
|
67
44
|
if (input.provider === "claude") {
|