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
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,7 +65,7 @@ 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
  });
189
70
  this.transport.notify("initialized", {});
190
71
  return result;
@@ -199,9 +80,12 @@ export class AcpClient {
199
80
  });
200
81
  }
201
82
 
202
- newSession(input: { cwd: string; mcpServers?: unknown; model?: string; permissionMode?: AgentPermissionMode }): Promise<unknown> {
83
+ newSession(input: { cwd: string; mcpServers?: unknown }): Promise<unknown> {
203
84
  if (this.protocol === "codex-app-server") {
204
- return this.transport.request("thread/start", codexThreadConfig(input));
85
+ return this.transport.request("thread/start", {
86
+ cwd: input.cwd,
87
+ sessionStartSource: "startup",
88
+ });
205
89
  }
206
90
  return this.transport.request("session/new", {
207
91
  cwd: input.cwd,
@@ -209,11 +93,12 @@ export class AcpClient {
209
93
  });
210
94
  }
211
95
 
212
- 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> {
213
97
  if (this.protocol === "codex-app-server") {
214
98
  return this.transport.request("thread/resume", {
215
99
  threadId: input.sessionId,
216
- ...codexThreadConfig(input),
100
+ cwd: input.cwd,
101
+ excludeTurns: false,
217
102
  });
218
103
  }
219
104
  return this.transport.request("session/load", {
@@ -223,15 +108,9 @@ export class AcpClient {
223
108
  });
224
109
  }
225
110
 
226
- async listSessions(): Promise<unknown> {
111
+ listSessions(): Promise<unknown> {
227
112
  if (this.protocol === "codex-app-server") {
228
- const localSessions = listCodexStoredSessions(this.cwd);
229
- try {
230
- const remoteSessions = await this.transport.request("thread/list", { limit: 200 });
231
- return mergeCodexSessionLists(remoteSessions, localSessions);
232
- } catch {
233
- return localSessions;
234
- }
113
+ return this.transport.request("thread/list", { limit: 20 });
235
114
  }
236
115
  return this.transport.request("session/list", {});
237
116
  }
@@ -243,56 +122,39 @@ export class AcpClient {
243
122
  return Promise.resolve(undefined);
244
123
  }
245
124
 
246
- listTurns(input: { sessionId: string; limit?: number }): Promise<unknown> {
247
- if (this.protocol === "codex-app-server") {
248
- return this.transport.request("thread/turns/list", {
249
- threadId: input.sessionId,
250
- limit: input.limit ?? 200,
251
- });
252
- }
253
- return Promise.resolve(undefined);
254
- }
255
-
256
125
  prompt(input: {
257
126
  sessionId: string;
258
127
  content: unknown[];
259
128
  clientMessageId: string;
260
129
  model?: string;
261
130
  reasoningEffort?: string;
262
- serviceTier?: string;
263
131
  permissionMode?: AgentPermissionMode;
264
132
  collaborationMode?: AgentCollaborationMode;
265
133
  cwd: string;
266
134
  }): Promise<unknown> {
267
135
  if (this.protocol === "codex-app-server") {
268
- 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"
269
141
  ? {
270
142
  mode: input.collaborationMode,
271
- settings: {
272
- model: input.model,
273
- reasoning_effort: input.reasoningEffort ?? null,
274
- developer_instructions: null,
275
- },
143
+ settings: collaborationSettings,
276
144
  }
277
145
  : undefined;
278
146
  return this.transport.request("turn/start", {
279
147
  threadId: input.sessionId,
280
- cwd: input.cwd,
281
148
  model: input.model,
282
149
  effort: input.reasoningEffort,
283
- ...(input.serviceTier === "fast" ? { serviceTier: input.serviceTier } : {}),
284
- ...(approvalPolicyForMode(input.permissionMode) ? { approvalPolicy: approvalPolicyForMode(input.permissionMode) } : {}),
285
- ...(sandboxPolicyForMode(input.permissionMode, input.cwd) ? { sandboxPolicy: sandboxPolicyForMode(input.permissionMode, input.cwd) } : {}),
286
- ...(collaborationMode ? { collaborationMode } : {}),
150
+ permissions: permissionsForMode(input.permissionMode, input.cwd),
151
+ collaborationMode,
287
152
  input: input.content.map((block) => {
288
- 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 };
289
154
  if (raw.type === "image" && raw.data) {
290
155
  return { type: "image", url: raw.data };
291
156
  }
292
- if (raw.type === "file" && raw.path) {
293
- return { type: "mention", path: raw.path, name: raw.name };
294
- }
295
- return { type: "text", text: raw.text ?? "", text_elements: [] };
157
+ return { type: "text", text: raw.text ?? "" };
296
158
  }),
297
159
  }, null);
298
160
  }