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.
- package/dist/cli/src/commands/doctor.js +12 -23
- package/dist/cli/src/commands/doctor.js.map +1 -1
- package/dist/cli/src/commands/setup.js +8 -19
- package/dist/cli/src/commands/setup.js.map +1 -1
- package/dist/cli/src/index.js +6 -7
- package/dist/cli/src/index.js.map +1 -1
- package/dist/cli/src/providers.d.ts +1 -1
- package/dist/cli/src/providers.js +14 -77
- package/dist/cli/src/providers.js.map +1 -1
- package/dist/cli/src/runtime/acp/agent-workspace.js +41 -11
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/commands/doctor.ts +11 -22
- package/src/commands/setup.ts +11 -29
- package/src/index.ts +11 -8
- package/src/providers.ts +16 -108
- package/src/runtime/acp/agent-workspace.ts +40 -8
package/src/commands/setup.ts
CHANGED
|
@@ -16,23 +16,11 @@ function ask(
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
40
|
+
const command = await ask(
|
|
53
41
|
rl,
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
-
"
|
|
49
|
-
|
|
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",
|
|
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:
|
|
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
|
|
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
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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: "
|
|
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
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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> {
|