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.
Files changed (62) 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 +20 -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 -27
  17. package/dist/cli/src/runtime/acp/agent-workspace.js +168 -1008
  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/json-rpc.d.ts +1 -34
  26. package/dist/cli/src/runtime/acp/json-rpc.js +8 -32
  27. package/dist/cli/src/runtime/acp/json-rpc.js.map +1 -1
  28. package/dist/cli/src/runtime/acp/provider-resolver.d.ts +0 -3
  29. package/dist/cli/src/runtime/acp/provider-resolver.js +6 -26
  30. package/dist/cli/src/runtime/acp/provider-resolver.js.map +1 -1
  31. package/dist/cli/src/runtime/bridge-session.d.ts +31 -1
  32. package/dist/cli/src/runtime/bridge-session.js +991 -81
  33. package/dist/cli/src/runtime/bridge-session.js.map +1 -1
  34. package/dist/cli/src/runtime/screen-fallback.d.ts +1 -1
  35. package/dist/cli/src/runtime/screen-fallback.js +4 -4
  36. package/dist/cli/src/runtime/screen-fallback.js.map +1 -1
  37. package/dist/cli/src/runtime/screen-share.d.ts +1 -1
  38. package/dist/cli/src/runtime/screen-share.js +7 -7
  39. package/dist/cli/src/runtime/screen-share.js.map +1 -1
  40. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  41. package/dist/shared-protocol/src/index.d.ts +11940 -3423
  42. package/dist/shared-protocol/src/index.js +98 -164
  43. package/dist/shared-protocol/src/index.js.map +1 -1
  44. package/package.json +13 -13
  45. package/src/commands/setup.ts +31 -5
  46. package/src/commands/upgrade.ts +8 -28
  47. package/src/index.ts +34 -29
  48. package/src/providers.ts +108 -26
  49. package/src/runtime/acp/acp-client.ts +22 -159
  50. package/src/runtime/acp/agent-session.ts +1180 -0
  51. package/src/runtime/acp/agent-workspace.ts +176 -1094
  52. package/src/runtime/acp/claude-sdk-client.ts +37 -85
  53. package/src/runtime/acp/claude-stream-json-client.ts +41 -59
  54. package/src/runtime/acp/json-rpc.ts +14 -46
  55. package/src/runtime/acp/provider-resolver.ts +6 -29
  56. package/src/runtime/bridge-session.ts +1092 -84
  57. package/src/runtime/screen-fallback.ts +5 -5
  58. package/src/runtime/screen-share.ts +8 -8
  59. package/src/runtime/acp/claude-sessions.ts +0 -563
  60. package/src/runtime/acp/codex-rpc-bridge.ts +0 -233
  61. package/src/runtime/acp/codex-sessions.ts +0 -802
  62. package/src/types/linkshell-gateway.d.ts +0 -18
package/src/providers.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { accessSync, constants, existsSync } from "node:fs";
2
- import { basename, delimiter, join } from "node:path";
2
+ import { delimiter, join } from "node:path";
3
+ import type { TerminalProvider } from "@linkshell/protocol";
3
4
 
4
- export type ProviderName = "shell";
5
+ export type ProviderName = TerminalProvider;
5
6
 
6
7
  export interface ProviderConfig {
7
8
  provider: ProviderName;
@@ -10,8 +11,6 @@ export interface ProviderConfig {
10
11
  env: NodeJS.ProcessEnv;
11
12
  }
12
13
 
13
- export type ShellConfig = ProviderConfig;
14
-
15
14
  function stripOuterQuotes(value: string): string {
16
15
  const trimmed = value.trim();
17
16
  if (
@@ -66,42 +65,125 @@ function which(bin: string): string | undefined {
66
65
  return undefined;
67
66
  }
68
67
 
69
- function shellSupportsLoginArg(command: string): boolean {
70
- const name = basename(command).toLowerCase();
71
- return ["bash", "zsh", "sh", "fish"].includes(name);
68
+ function resolveClaudeProvider(input: {
69
+ command?: string;
70
+ args: string[];
71
+ }): ProviderConfig {
72
+ const command = input.command ?? "claude";
73
+ const resolved = which(command);
74
+ if (!resolved) {
75
+ throw new Error(
76
+ `Claude CLI not found ("${command}"). Install it with: npm install -g @anthropic-ai/claude-code`,
77
+ );
78
+ }
79
+
80
+ // Claude starts an interactive REPL by default — that's exactly what we want in the PTY.
81
+ // Pass through any extra args the user provided.
82
+ return {
83
+ provider: "claude",
84
+ command: resolved,
85
+ args: input.args,
86
+ env: { ...process.env },
87
+ };
72
88
  }
73
89
 
74
- export function resolveShellConfig(input: {
90
+ function resolveCodexProvider(input: {
75
91
  command?: string;
76
- args?: string[];
77
- } = {}): ShellConfig {
78
- const configuredShell = input.command ?? process.env.SHELL ?? (process.platform === "darwin" ? "/bin/zsh" : undefined);
79
- const command = configuredShell ?? (process.platform === "win32" ? "cmd.exe" : "sh");
80
- const resolved = which(command) ?? command;
81
- const passthrough = input.args ?? [];
82
- const args = passthrough.length > 0
83
- ? passthrough
84
- : shellSupportsLoginArg(resolved)
85
- ? ["-l"]
86
- : [];
92
+ args: string[];
93
+ }): ProviderConfig {
94
+ const command = input.command ?? "codex";
95
+ const resolved = which(command);
96
+ if (!resolved) {
97
+ throw new Error(
98
+ `Codex CLI not found ("${command}"). Install it with: npm install -g @openai/codex`,
99
+ );
100
+ }
101
+
102
+ if (!process.env.OPENAI_API_KEY) {
103
+ process.stderr.write(
104
+ "[warn] OPENAI_API_KEY not set — Codex may fail to authenticate\n",
105
+ );
106
+ }
87
107
 
88
108
  return {
89
- provider: "shell",
109
+ provider: "codex",
90
110
  command: resolved,
91
- args,
111
+ args: input.args,
92
112
  env: { ...process.env },
93
113
  };
94
114
  }
95
115
 
96
- export function resolveProviderConfig(input: {
97
- provider?: string;
116
+ function resolveGeminiProvider(input: {
98
117
  command?: string;
99
118
  args: string[];
100
119
  }): ProviderConfig {
101
- if (input.provider && input.provider !== "shell") {
120
+ const command = input.command ?? "gemini";
121
+ const resolved = which(command);
122
+ if (!resolved) {
123
+ throw new Error(
124
+ `Gemini CLI not found ("${command}"). Install it with: npm install -g @anthropic-ai/gemini-cli or check https://github.com/anthropics/gemini-cli`,
125
+ );
126
+ }
127
+
128
+ if (!process.env.GOOGLE_API_KEY && !process.env.GEMINI_API_KEY) {
102
129
  process.stderr.write(
103
- `[warn] terminal provider "${input.provider}" is ignored in protocol v2; starting the system shell instead\n`,
130
+ "[warn] GOOGLE_API_KEY / GEMINI_API_KEY not set — Gemini may fail to authenticate\n",
104
131
  );
105
132
  }
106
- return resolveShellConfig({ command: input.command, args: input.args });
133
+
134
+ return {
135
+ provider: "gemini",
136
+ command: resolved,
137
+ args: input.args,
138
+ env: { ...process.env },
139
+ };
140
+ }
141
+
142
+ function resolveCopilotProvider(input: {
143
+ command?: string;
144
+ args: string[];
145
+ }): ProviderConfig {
146
+ const command = input.command ?? "github-copilot";
147
+ const resolved = which(command);
148
+ if (!resolved) {
149
+ throw new Error(
150
+ `GitHub Copilot CLI not found ("${command}").`,
151
+ );
152
+ }
153
+
154
+ return {
155
+ provider: "copilot",
156
+ command: resolved,
157
+ args: input.args,
158
+ env: { ...process.env },
159
+ };
160
+ }
161
+
162
+ export function resolveProviderConfig(input: {
163
+ provider: ProviderName;
164
+ command?: string;
165
+ args: string[];
166
+ }): ProviderConfig {
167
+ switch (input.provider) {
168
+ case "claude":
169
+ return resolveClaudeProvider(input);
170
+ case "codex":
171
+ return resolveCodexProvider(input);
172
+ case "gemini":
173
+ return resolveGeminiProvider(input);
174
+ case "copilot":
175
+ return resolveCopilotProvider(input);
176
+ case "custom": {
177
+ if (!input.command) {
178
+ throw new Error("custom provider requires --command");
179
+ }
180
+ const resolved = which(input.command);
181
+ return {
182
+ provider: "custom",
183
+ command: resolved ?? input.command,
184
+ args: input.args,
185
+ env: { ...process.env },
186
+ };
187
+ }
188
+ }
107
189
  }
@@ -1,5 +1,4 @@
1
1
  import { JsonRpcStdioTransport } from "./json-rpc.js";
2
- import { listCodexStoredSessions, type CodexStoredSession } from "./codex-sessions.js";
3
2
  import type { AgentFraming, AgentProtocol } from "./provider-resolver.js";
4
3
 
5
4
  type AgentPermissionMode = "read_only" | "workspace_write" | "full_access";
@@ -16,83 +15,6 @@ function normalizeMcpServers(value: unknown): unknown[] {
16
15
  });
17
16
  }
18
17
 
19
- function asRecord(value: unknown): Record<string, unknown> | undefined {
20
- return value && typeof value === "object" && !Array.isArray(value)
21
- ? value as Record<string, unknown>
22
- : undefined;
23
- }
24
-
25
- function parseTimestamp(value: unknown): number | undefined {
26
- if (typeof value === "number" && Number.isFinite(value)) {
27
- return value > 10_000_000_000 ? value : value * 1000;
28
- }
29
- if (typeof value === "string" && value.trim()) {
30
- const parsed = Date.parse(value);
31
- return Number.isNaN(parsed) ? undefined : parsed;
32
- }
33
- return undefined;
34
- }
35
-
36
- function firstString(record: Record<string, unknown>, keys: string[]): string | undefined {
37
- for (const key of keys) {
38
- const value = record[key];
39
- if (typeof value === "string" && value.trim()) return value;
40
- }
41
- return undefined;
42
- }
43
-
44
- function remoteSessionEntries(value: unknown): unknown[] {
45
- const raw = asRecord(value);
46
- return Array.isArray(value) ? value :
47
- Array.isArray(raw?.data) ? raw.data :
48
- Array.isArray(raw?.threads) ? raw.threads :
49
- Array.isArray(raw?.sessions) ? raw.sessions :
50
- Array.isArray(raw?.items) ? raw.items :
51
- [];
52
- }
53
-
54
- function normalizeRemoteSession(entry: unknown, fallback?: CodexStoredSession): CodexStoredSession | undefined {
55
- if (typeof entry === "string" && entry.trim()) {
56
- return fallback ?? { id: entry, cwd: "", lastModified: Date.now() };
57
- }
58
- const session = asRecord(entry);
59
- if (!session) return fallback;
60
- const nestedThread = asRecord(session.thread);
61
- const source = nestedThread ?? session;
62
- const id = firstString(source, ["id", "threadId", "sessionId", "agentSessionId"]) ?? fallback?.id;
63
- if (!id) return undefined;
64
- return {
65
- id,
66
- cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]) ?? fallback?.cwd ?? "",
67
- title: firstString(source, ["title", "name", "summary", "thread_name"]) ?? fallback?.title,
68
- createdAt: parseTimestamp(source.createdAt ?? source.created_at) ?? fallback?.createdAt,
69
- lastModified: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at) ??
70
- fallback?.lastModified ??
71
- Date.now(),
72
- archived: typeof source.archived === "boolean" ? source.archived : fallback?.archived,
73
- status: firstString(source, ["status", "state", "phase"]) ?? fallback?.status,
74
- runningTurnId: firstString(source, ["runningTurnId", "running_turn_id", "turnId", "activeTurnId"]) ?? fallback?.runningTurnId,
75
- };
76
- }
77
-
78
- function mergeCodexSessionLists(remote: unknown, local: { sessions: CodexStoredSession[] }): { sessions: CodexStoredSession[] } {
79
- const byId = new Map(local.sessions.map((session) => [session.id, session]));
80
- for (const entry of remoteSessionEntries(remote)) {
81
- const id = typeof entry === "string"
82
- ? entry
83
- : (() => {
84
- const session = asRecord(entry);
85
- const source = asRecord(session?.thread) ?? session;
86
- return source ? firstString(source, ["id", "threadId", "sessionId", "agentSessionId"]) : undefined;
87
- })();
88
- const merged = normalizeRemoteSession(entry, id ? byId.get(id) : undefined);
89
- if (merged?.id) byId.set(merged.id, merged);
90
- }
91
- return {
92
- sessions: [...byId.values()].sort((a, b) => b.lastModified - a.lastModified),
93
- };
94
- }
95
-
96
18
  function permissionsForMode(
97
19
  mode: AgentPermissionMode | undefined,
98
20
  cwd: string,
@@ -115,47 +37,9 @@ function permissionsForMode(
115
37
  };
116
38
  }
117
39
 
118
- function sandboxPolicyForMode(
119
- mode: AgentPermissionMode | undefined,
120
- cwd: string,
121
- ): unknown | undefined {
122
- if (mode === "full_access") return { type: "dangerFullAccess" };
123
- if (mode === "read_only") return { type: "readOnly", networkAccess: false };
124
- if (mode === "workspace_write") {
125
- return {
126
- type: "workspaceWrite",
127
- writableRoots: [cwd],
128
- networkAccess: false,
129
- excludeTmpdirEnvVar: false,
130
- excludeSlashTmp: false,
131
- };
132
- }
133
- return undefined;
134
- }
135
-
136
- function approvalPolicyForMode(mode: AgentPermissionMode | undefined): unknown | undefined {
137
- if (mode === "full_access") return "never";
138
- if (mode === "read_only" || mode === "workspace_write") return "on-request";
139
- return undefined;
140
- }
141
-
142
- function codexThreadConfig(input: {
143
- cwd: string;
144
- model?: string;
145
- permissionMode?: AgentPermissionMode;
146
- }): Record<string, unknown> {
147
- return {
148
- cwd: input.cwd,
149
- ...(input.model ? { model: input.model } : {}),
150
- ...(approvalPolicyForMode(input.permissionMode) ? { approvalPolicy: approvalPolicyForMode(input.permissionMode) } : {}),
151
- ...(sandboxPolicyForMode(input.permissionMode, input.cwd) ? { sandboxPolicy: sandboxPolicyForMode(input.permissionMode, input.cwd) } : {}),
152
- };
153
- }
154
-
155
40
  export class AcpClient {
156
41
  private readonly transport: JsonRpcStdioTransport;
157
42
  private readonly protocol: AgentProtocol;
158
- private readonly cwd: string;
159
43
 
160
44
  constructor(input: {
161
45
  command: string;
@@ -167,15 +51,12 @@ export class AcpClient {
167
51
  onExit: (message: string) => void;
168
52
  }) {
169
53
  this.protocol = input.protocol;
170
- this.cwd = input.cwd;
171
54
  this.transport = new JsonRpcStdioTransport(
172
55
  input.command,
173
56
  input.framing,
174
57
  input.onNotification,
175
58
  input.onRequest,
176
59
  input.onExit,
177
- undefined,
178
- { includeJsonrpc: input.protocol !== "codex-app-server" },
179
60
  );
180
61
  this.transport.start(input.cwd);
181
62
  }
@@ -184,8 +65,9 @@ export class AcpClient {
184
65
  if (this.protocol === "codex-app-server") {
185
66
  const result = await this.transport.request("initialize", {
186
67
  clientInfo: { name: "LinkShell", version: "0.1" },
187
- capabilities: { experimentalApi: true, optOutNotificationMethods: [] },
68
+ capabilities: { experimentalApi: true },
188
69
  });
70
+ this.transport.notify("initialized", {});
189
71
  return result;
190
72
  }
191
73
  return this.transport.request("initialize", {
@@ -198,9 +80,12 @@ export class AcpClient {
198
80
  });
199
81
  }
200
82
 
201
- newSession(input: { cwd: string; mcpServers?: unknown; model?: string; permissionMode?: AgentPermissionMode }): Promise<unknown> {
83
+ newSession(input: { cwd: string; mcpServers?: unknown }): Promise<unknown> {
202
84
  if (this.protocol === "codex-app-server") {
203
- return this.transport.request("thread/start", codexThreadConfig(input));
85
+ return this.transport.request("thread/start", {
86
+ cwd: input.cwd,
87
+ sessionStartSource: "startup",
88
+ });
204
89
  }
205
90
  return this.transport.request("session/new", {
206
91
  cwd: input.cwd,
@@ -208,11 +93,12 @@ export class AcpClient {
208
93
  });
209
94
  }
210
95
 
211
- loadSession(input: { sessionId: string; cwd: string; mcpServers?: unknown; model?: string; permissionMode?: AgentPermissionMode }): Promise<unknown> {
96
+ loadSession(input: { sessionId: string; cwd: string; mcpServers?: unknown }): Promise<unknown> {
212
97
  if (this.protocol === "codex-app-server") {
213
98
  return this.transport.request("thread/resume", {
214
99
  threadId: input.sessionId,
215
- ...codexThreadConfig(input),
100
+ cwd: input.cwd,
101
+ excludeTurns: false,
216
102
  });
217
103
  }
218
104
  return this.transport.request("session/load", {
@@ -222,15 +108,9 @@ export class AcpClient {
222
108
  });
223
109
  }
224
110
 
225
- async listSessions(): Promise<unknown> {
111
+ listSessions(): Promise<unknown> {
226
112
  if (this.protocol === "codex-app-server") {
227
- const localSessions = listCodexStoredSessions(this.cwd);
228
- try {
229
- const remoteSessions = await this.transport.request("thread/list", { limit: 200 });
230
- return mergeCodexSessionLists(remoteSessions, localSessions);
231
- } catch {
232
- return localSessions;
233
- }
113
+ return this.transport.request("thread/list", { limit: 20 });
234
114
  }
235
115
  return this.transport.request("session/list", {});
236
116
  }
@@ -242,56 +122,39 @@ export class AcpClient {
242
122
  return Promise.resolve(undefined);
243
123
  }
244
124
 
245
- listTurns(input: { sessionId: string; limit?: number }): Promise<unknown> {
246
- if (this.protocol === "codex-app-server") {
247
- return this.transport.request("thread/turns/list", {
248
- threadId: input.sessionId,
249
- limit: input.limit ?? 200,
250
- });
251
- }
252
- return Promise.resolve(undefined);
253
- }
254
-
255
125
  prompt(input: {
256
126
  sessionId: string;
257
127
  content: unknown[];
258
128
  clientMessageId: string;
259
129
  model?: string;
260
130
  reasoningEffort?: string;
261
- serviceTier?: string;
262
131
  permissionMode?: AgentPermissionMode;
263
132
  collaborationMode?: AgentCollaborationMode;
264
133
  cwd: string;
265
134
  }): Promise<unknown> {
266
135
  if (this.protocol === "codex-app-server") {
267
- const collaborationMode = input.collaborationMode && input.model
136
+ const collaborationSettings = {
137
+ ...(input.model ? { model: input.model } : {}),
138
+ ...(input.reasoningEffort ? { reasoning_effort: input.reasoningEffort } : {}),
139
+ };
140
+ const collaborationMode = input.collaborationMode && input.collaborationMode !== "default"
268
141
  ? {
269
142
  mode: input.collaborationMode,
270
- settings: {
271
- model: input.model,
272
- reasoning_effort: input.reasoningEffort ?? null,
273
- developer_instructions: null,
274
- },
143
+ settings: collaborationSettings,
275
144
  }
276
145
  : undefined;
277
146
  return this.transport.request("turn/start", {
278
147
  threadId: input.sessionId,
279
- cwd: input.cwd,
280
148
  model: input.model,
281
149
  effort: input.reasoningEffort,
282
- ...(input.serviceTier === "fast" ? { serviceTier: input.serviceTier } : {}),
283
- ...(approvalPolicyForMode(input.permissionMode) ? { approvalPolicy: approvalPolicyForMode(input.permissionMode) } : {}),
284
- ...(sandboxPolicyForMode(input.permissionMode, input.cwd) ? { sandboxPolicy: sandboxPolicyForMode(input.permissionMode, input.cwd) } : {}),
285
- ...(collaborationMode ? { collaborationMode } : {}),
150
+ permissions: permissionsForMode(input.permissionMode, input.cwd),
151
+ collaborationMode,
286
152
  input: input.content.map((block) => {
287
- const raw = block as { type?: string; text?: string; data?: string; path?: string; name?: string };
153
+ const raw = block as { type?: string; text?: string; data?: string };
288
154
  if (raw.type === "image" && raw.data) {
289
155
  return { type: "image", url: raw.data };
290
156
  }
291
- if (raw.type === "file" && raw.path) {
292
- return { type: "mention", path: raw.path, name: raw.name };
293
- }
294
- return { type: "text", text: raw.text ?? "", text_elements: [] };
157
+ return { type: "text", text: raw.text ?? "" };
295
158
  }),
296
159
  }, null);
297
160
  }