linkshell-cli 0.3.27 → 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.
Files changed (64) hide show
  1. package/dist/cli/src/commands/setup.js +20 -2
  2. package/dist/cli/src/commands/setup.js.map +1 -1
  3. package/dist/cli/src/commands/upgrade.js +9 -24
  4. package/dist/cli/src/commands/upgrade.js.map +1 -1
  5. package/dist/cli/src/index.js +28 -26
  6. package/dist/cli/src/index.js.map +1 -1
  7. package/dist/cli/src/providers.d.ts +3 -7
  8. package/dist/cli/src/providers.js +76 -19
  9. package/dist/cli/src/providers.js.map +1 -1
  10. package/dist/cli/src/runtime/acp/acp-client.d.ts +0 -10
  11. package/dist/cli/src/runtime/acp/acp-client.js +19 -146
  12. package/dist/cli/src/runtime/acp/acp-client.js.map +1 -1
  13. package/dist/cli/src/runtime/acp/agent-session.d.ts +62 -0
  14. package/dist/cli/src/runtime/acp/agent-session.js +1075 -0
  15. package/dist/cli/src/runtime/acp/agent-session.js.map +1 -0
  16. package/dist/cli/src/runtime/acp/agent-workspace.d.ts +1 -24
  17. package/dist/cli/src/runtime/acp/agent-workspace.js +162 -981
  18. package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
  19. package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +1 -3
  20. package/dist/cli/src/runtime/acp/claude-sdk-client.js +35 -81
  21. package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
  22. package/dist/cli/src/runtime/acp/claude-stream-json-client.d.ts +1 -2
  23. package/dist/cli/src/runtime/acp/claude-stream-json-client.js +35 -54
  24. package/dist/cli/src/runtime/acp/claude-stream-json-client.js.map +1 -1
  25. package/dist/cli/src/runtime/acp/codex-rpc-bridge.js +12 -1
  26. package/dist/cli/src/runtime/acp/codex-rpc-bridge.js.map +1 -1
  27. package/dist/cli/src/runtime/acp/json-rpc.d.ts +1 -34
  28. package/dist/cli/src/runtime/acp/json-rpc.js +8 -32
  29. package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
  30. package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -3
  31. package/dist/cli/src/runtime/acp/provider-resolver.js +6 -26
  32. package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
  33. package/dist/cli/src/runtime/bridge-session.d.ts +31 -1
  34. package/dist/cli/src/runtime/bridge-session.js +991 -81
  35. package/dist/cli/src/runtime/bridge-session.js.map +1 -1
  36. package/dist/cli/src/runtime/screen-fallback.d.ts +1 -1
  37. package/dist/cli/src/runtime/screen-fallback.js +4 -4
  38. package/dist/cli/src/runtime/screen-fallback.js.map +1 -1
  39. package/dist/cli/src/runtime/screen-share.d.ts +1 -1
  40. package/dist/cli/src/runtime/screen-share.js +7 -7
  41. package/dist/cli/src/runtime/screen-share.js.map +1 -1
  42. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  43. package/dist/shared-protocol/src/index.d.ts +11940 -3423
  44. package/dist/shared-protocol/src/index.js +98 -164
  45. package/dist/shared-protocol/src/index.js.map +1 -1
  46. package/package.json +5 -5
  47. package/src/commands/setup.ts +31 -5
  48. package/src/commands/upgrade.ts +8 -28
  49. package/src/index.ts +34 -29
  50. package/src/providers.ts +108 -26
  51. package/src/runtime/acp/acp-client.ts +21 -159
  52. package/src/runtime/acp/agent-session.ts +1180 -0
  53. package/src/runtime/acp/agent-workspace.ts +170 -1068
  54. package/src/runtime/acp/claude-sdk-client.ts +37 -85
  55. package/src/runtime/acp/claude-stream-json-client.ts +41 -59
  56. package/src/runtime/acp/json-rpc.ts +14 -46
  57. package/src/runtime/acp/provider-resolver.ts +6 -29
  58. package/src/runtime/bridge-session.ts +1092 -84
  59. package/src/runtime/screen-fallback.ts +5 -5
  60. package/src/runtime/screen-share.ts +8 -8
  61. package/src/runtime/acp/claude-sessions.ts +0 -563
  62. package/src/runtime/acp/codex-rpc-bridge.ts +0 -221
  63. package/src/runtime/acp/codex-sessions.ts +0 -802
  64. package/src/types/linkshell-gateway.d.ts +0 -18
@@ -1,9 +1,9 @@
1
- import { isAbsolute, relative, resolve } from "node:path";
2
- import { listClaudeStoredSessions } from "./claude-sessions.js";
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?: AgentCollaborationMode;
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", "NotebookEdit", "Bash"].includes(toolName)) {
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" && isClaudeFileTool(toolName)) {
284
- const filePath = filePathFromToolInput(toolInput);
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
- const effort = claudeEffort(input.reasoningEffort);
308
- if (effort) sdkOptions.effort = effort;
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
- return listClaudeStoredSessions(this.input.cwd);
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" : isClaudeFileTool(toolName) ? "fileChange" : "toolCall",
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: toolName === "Bash" ? "commandExecution" : toolName && isClaudeFileTool(toolName) ? "fileChange" : "toolCall",
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 { listClaudeStoredSessions } from "./claude-sessions.js";
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?: AgentCollaborationMode;
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", claudePermissionMode(input.permissionMode, input.collaborationMode),
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" : isClaudeFileTool(toolName) ? "fileChange" : "toolCall",
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: toolName === "Bash" ? "commandExecution" : toolName && isClaudeFileTool(toolName) ? "fileChange" : "toolCall",
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
- return listClaudeStoredSessions(this.input.cwd);
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
- export interface JsonRpcRequest {
5
- jsonrpc?: "2.0";
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
- export interface JsonRpcResponse {
12
- jsonrpc?: "2.0";
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
- export interface JsonRpcNotification {
19
- jsonrpc?: "2.0";
18
+ interface JsonRpcNotification {
19
+ jsonrpc: "2.0";
20
20
  method: string;
21
21
  params?: unknown;
22
22
  }
23
23
 
24
- export type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
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 = this.withJsonrpc({ id, method, params });
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(this.withJsonrpc({ method, params }));
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(this.withJsonrpc({ id: message.id, result })))
152
+ .then((result) => this.write({ jsonrpc: "2.0", id: message.id, result }))
186
153
  .catch((error) => {
187
- this.write(this.withJsonrpc({
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 resolveCodexAppServerCommands({ command: input.command })[0] ?? null;
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") {