linkshell-cli 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,23 +16,11 @@ function ask(
16
16
  });
17
17
  }
18
18
 
19
- function choose(
20
- rl: readline.Interface,
21
- question: string,
22
- options: string[],
23
- defaultIdx = 0,
24
- ): Promise<string> {
25
- return new Promise((resolve) => {
26
- process.stdout.write(` ${question}\n`);
27
- for (let i = 0; i < options.length; i++) {
28
- const marker = i === defaultIdx ? "\x1b[36m>\x1b[0m" : " ";
29
- process.stdout.write(` ${marker} ${i + 1}. ${options[i]}\n`);
30
- }
31
- rl.question(` Choice (${defaultIdx + 1}): `, (answer) => {
32
- const idx = Number(answer.trim()) - 1;
33
- resolve(options[idx >= 0 && idx < options.length ? idx : defaultIdx]!);
34
- });
35
- });
19
+ function defaultShell(): string {
20
+ if (process.platform === "win32") {
21
+ return process.env.COMSPEC ?? "cmd.exe";
22
+ }
23
+ return process.env.SHELL ?? "/bin/bash";
36
24
  }
37
25
 
38
26
  export async function runSetup(): Promise<void> {
@@ -49,17 +37,11 @@ export async function runSetup(): Promise<void> {
49
37
 
50
38
  const gateway = await ask(rl, "Gateway URL (leave empty for built-in)", "");
51
39
 
52
- const provider = (await choose(
40
+ const command = await ask(
53
41
  rl,
54
- "Default provider:",
55
- ["claude", "codex", "gemini", "copilot", "custom"],
56
- ["claude", "codex", "gemini", "copilot", "custom"].indexOf(existing.provider ?? "claude"),
57
- )) as LinkShellConfig["provider"];
58
-
59
- let command: string | undefined;
60
- if (provider === "custom") {
61
- command = await ask(rl, "Custom command", existing.command ?? "bash");
62
- }
42
+ "Terminal command (the shell or CLI you want to bridge)",
43
+ existing.command ?? defaultShell(),
44
+ );
63
45
 
64
46
  const clientName = await ask(
65
47
  rl,
@@ -76,8 +58,8 @@ export async function runSetup(): Promise<void> {
76
58
 
77
59
  const config: LinkShellConfig = {
78
60
  gateway: gateway || undefined,
79
- provider,
80
- command,
61
+ provider: "custom",
62
+ command: command || undefined,
81
63
  clientName,
82
64
  hostname: hostnameName || undefined,
83
65
  };
package/src/index.ts CHANGED
@@ -22,7 +22,7 @@ const program = new Command();
22
22
  program
23
23
  .name("linkshell")
24
24
  .description(
25
- "Bridge a local Claude/Codex terminal session to a remote gateway",
25
+ "Bridge a local terminal session to a remote gateway. Launch any CLI (Claude/Codex/etc.) manually inside the shell.",
26
26
  )
27
27
  .version(pkg.version);
28
28
 
@@ -45,10 +45,14 @@ program
45
45
  .option("--session-id <id>", "Session identifier (auto-created if omitted)")
46
46
  .option(
47
47
  "--provider <provider>",
48
- "claude | codex | gemini | copilot | custom",
49
- config.provider ?? "claude",
48
+ "(deprecated — always custom; launch your CLI manually inside the shell)",
49
+ "custom",
50
+ )
51
+ .option(
52
+ "--command <command>",
53
+ "Shell or CLI to spawn in the PTY (default: $SHELL)",
54
+ config.command,
50
55
  )
51
- .option("--command <command>", "Override provider executable", config.command)
52
56
  .option(
53
57
  "--client-name <name>",
54
58
  "Display name for this CLI",
@@ -485,12 +489,11 @@ program
485
489
  return;
486
490
  }
487
491
 
488
- const provider = config.provider ?? "claude";
489
492
  const childArgs = [
490
493
  "start",
491
494
  "--_foreground-bridge",
492
495
  "--gateway", gwWsUrl,
493
- "--provider", provider,
496
+ "--provider", "custom",
494
497
  "--client-name", config.clientName ?? "local-cli",
495
498
  "--cols", String(config.cols ?? 120),
496
499
  "--rows", String(config.rows ?? 36),
@@ -505,7 +508,7 @@ program
505
508
  daemon.saveMetadata("bridge", { keepAwake, startedAt: Date.now() });
506
509
  process.stderr.write(`\n \x1b[32m✓\x1b[0m Bridge started in background (PID ${pid})\n`);
507
510
  process.stderr.write(` Gateway: ${chosen.name}\n`);
508
- process.stderr.write(` Provider: ${provider}\n`);
511
+ process.stderr.write(` Provider: custom (shell)\n`);
509
512
  process.stderr.write(` Keep awake: ${keepAwake ? "enabled" : "disabled"}\n`);
510
513
  process.stderr.write(` Desktop: enabled\n`);
511
514
  process.stderr.write(` Agent GUI: enabled (Codex app-server)\n`);
@@ -607,7 +610,7 @@ program
607
610
  }
608
611
 
609
612
  process.stderr.write(
610
- " Connect: linkshell start --gateway <url> --provider claude\n\n",
613
+ " Connect: linkshell start --gateway <url>\n\n",
611
614
  );
612
615
  });
613
616
 
package/src/providers.ts CHANGED
@@ -11,6 +11,13 @@ export interface ProviderConfig {
11
11
  env: NodeJS.ProcessEnv;
12
12
  }
13
13
 
14
+ function defaultShell(): string {
15
+ if (process.platform === "win32") {
16
+ return process.env.COMSPEC ?? "cmd.exe";
17
+ }
18
+ return process.env.SHELL ?? "/bin/bash";
19
+ }
20
+
14
21
  function stripOuterQuotes(value: string): string {
15
22
  const trimmed = value.trim();
16
23
  if (
@@ -65,125 +72,26 @@ function which(bin: string): string | undefined {
65
72
  return undefined;
66
73
  }
67
74
 
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
- };
88
- }
89
-
90
- function resolveCodexProvider(input: {
91
- command?: string;
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
- }
107
-
108
- return {
109
- provider: "codex",
110
- command: resolved,
111
- args: input.args,
112
- env: { ...process.env },
113
- };
114
- }
115
-
116
- function resolveGeminiProvider(input: {
75
+ export function resolveProviderConfig(input: {
76
+ provider?: ProviderName;
117
77
  command?: string;
118
78
  args: string[];
119
79
  }): ProviderConfig {
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) {
80
+ if (input.provider && input.provider !== "custom") {
129
81
  process.stderr.write(
130
- "[warn] GOOGLE_API_KEY / GEMINI_API_KEY not set — Gemini may fail to authenticate\n",
82
+ `[warn] --provider=${input.provider} is no longer supported; falling back to a shell. ` +
83
+ `Launch your CLI manually inside the terminal (e.g. type "${input.provider}").\n`,
131
84
  );
132
85
  }
133
86
 
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
- }
87
+ const requested = input.command?.trim();
88
+ const command = requested && requested.length > 0 ? requested : defaultShell();
89
+ const resolved = which(command) ?? command;
153
90
 
154
91
  return {
155
- provider: "copilot",
92
+ provider: "custom",
156
93
  command: resolved,
157
94
  args: input.args,
158
95
  env: { ...process.env },
159
96
  };
160
97
  }
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
- }
189
- }
@@ -765,6 +765,17 @@ interface ProviderRuntimeCapabilities {
765
765
 
766
766
  const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"] as const;
767
767
  const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"] as const;
768
+
769
+ const CODEX_FALLBACK_MODELS: AgentModelOption[] = [
770
+ { id: "default", label: "默认模型" },
771
+ { id: "gpt-5", label: "GPT-5" },
772
+ { id: "gpt-5-codex", label: "GPT-5 Codex" },
773
+ { id: "gpt-5-pro", label: "GPT-5 Pro" },
774
+ { id: "gpt-5-high", label: "GPT-5 High" },
775
+ { id: "o3", label: "o3" },
776
+ { id: "o3-mini", label: "o3-mini" },
777
+ ];
778
+ const CODEX_FALLBACK_DEFAULT_MODEL = "gpt-5";
768
779
  const CODEX_COMMAND_NAMES = ["plan", "exit-plan", "compact", "clear", "status", "review", "subagents"] as const;
769
780
  const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
770
781
  "add-dir",
@@ -1300,6 +1311,7 @@ export class AgentWorkspaceProxy {
1300
1311
  }
1301
1312
  case "agent.v2.snapshot.request": {
1302
1313
  const payload = parseTypedPayload("agent.v2.snapshot.request", envelope.payload);
1314
+ await this.syncProviderSessions();
1303
1315
  this.sendSnapshot(payload.conversationId);
1304
1316
  break;
1305
1317
  }
@@ -1465,16 +1477,36 @@ export class AgentWorkspaceProxy {
1465
1477
  ): Promise<void> {
1466
1478
  if (client instanceof AcpClient && protocol !== "codex-app-server") return;
1467
1479
  const listModels = (client as { listModels?: () => Promise<unknown> }).listModels;
1468
- if (typeof listModels !== "function") return;
1469
- try {
1470
- const result = await listModels.call(client);
1471
- const runtimeCapabilities = parseModelListCapabilities(result);
1472
- if (runtimeCapabilities) this.providerCapabilities.set(provider, runtimeCapabilities);
1473
- } catch (error) {
1474
- if (this.input.verbose) {
1475
- process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1480
+ let runtimeCapabilities: ProviderRuntimeCapabilities | undefined;
1481
+ if (typeof listModels === "function") {
1482
+ try {
1483
+ const result = await listModels.call(client);
1484
+ runtimeCapabilities = parseModelListCapabilities(result);
1485
+ } catch (error) {
1486
+ if (this.input.verbose) {
1487
+ process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
1488
+ }
1476
1489
  }
1477
1490
  }
1491
+ if (provider === "codex") {
1492
+ // Codex `model/list` is unreliable across versions — make sure mobile always
1493
+ // sees a usable model picker by merging in a static fallback list.
1494
+ const merged: ProviderRuntimeCapabilities = {
1495
+ models: runtimeCapabilities?.models?.length
1496
+ ? runtimeCapabilities.models
1497
+ : [...CODEX_FALLBACK_MODELS],
1498
+ defaultModel: runtimeCapabilities?.defaultModel ?? CODEX_FALLBACK_DEFAULT_MODEL,
1499
+ reasoningEfforts: runtimeCapabilities?.reasoningEfforts?.length
1500
+ ? runtimeCapabilities.reasoningEfforts
1501
+ : [...ALL_REASONING_EFFORTS],
1502
+ commands: runtimeCapabilities?.commands,
1503
+ modes: runtimeCapabilities?.modes,
1504
+ currentMode: runtimeCapabilities?.currentMode,
1505
+ };
1506
+ this.providerCapabilities.set(provider, merged);
1507
+ return;
1508
+ }
1509
+ if (runtimeCapabilities) this.providerCapabilities.set(provider, runtimeCapabilities);
1478
1510
  }
1479
1511
 
1480
1512
  private async syncProviderSessions(): Promise<void> {