linkshell-cli 0.4.1 → 0.5.1
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 +72 -21
- 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 +65 -14
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,13 @@ 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
|
+
// Codex `model/list` is the source of truth — these are only used when the RPC
|
|
770
|
+
// returns nothing so mobile still sees a usable picker.
|
|
771
|
+
const CODEX_FALLBACK_MODELS: AgentModelOption[] = [
|
|
772
|
+
{ id: "default", label: "默认模型" },
|
|
773
|
+
];
|
|
774
|
+
const CODEX_FALLBACK_DEFAULT_MODEL = "default";
|
|
768
775
|
const CODEX_COMMAND_NAMES = ["plan", "exit-plan", "compact", "clear", "status", "review", "subagents"] as const;
|
|
769
776
|
const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
|
|
770
777
|
"add-dir",
|
|
@@ -1150,10 +1157,13 @@ function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities
|
|
|
1150
1157
|
const raw = asRecord(value);
|
|
1151
1158
|
const modelsValue =
|
|
1152
1159
|
Array.isArray(value) ? value :
|
|
1160
|
+
Array.isArray(raw?.data) ? raw.data :
|
|
1153
1161
|
Array.isArray(raw?.models) ? raw.models :
|
|
1154
1162
|
Array.isArray(raw?.items) ? raw.items :
|
|
1155
1163
|
Array.isArray(raw?.modelOptions) ? raw.modelOptions :
|
|
1156
1164
|
[];
|
|
1165
|
+
let inferredDefault: string | undefined;
|
|
1166
|
+
const efforts = new Set<string>();
|
|
1157
1167
|
const models = modelsValue
|
|
1158
1168
|
.map((entry, index) => {
|
|
1159
1169
|
const model = asRecord(entry);
|
|
@@ -1162,24 +1172,44 @@ function parseModelListCapabilities(value: unknown): ProviderRuntimeCapabilities
|
|
|
1162
1172
|
? { id: entry, label: entry }
|
|
1163
1173
|
: undefined;
|
|
1164
1174
|
}
|
|
1175
|
+
if (model.hidden === true) return undefined;
|
|
1165
1176
|
const modelId = firstString(model, ["id", "model", "name", "value"]) ?? `model-${index + 1}`;
|
|
1166
|
-
const label = firstString(model, ["
|
|
1177
|
+
const label = firstString(model, ["displayName", "label", "title", "name"]) ?? modelId;
|
|
1178
|
+
if (model.isDefault === true && !inferredDefault) inferredDefault = modelId;
|
|
1179
|
+
const supported = Array.isArray(model.supportedReasoningEfforts) ? model.supportedReasoningEfforts : [];
|
|
1180
|
+
for (const item of supported) {
|
|
1181
|
+
const effort = typeof item === "string"
|
|
1182
|
+
? item
|
|
1183
|
+
: firstString(asRecord(item), ["reasoningEffort", "id", "value"]);
|
|
1184
|
+
if (effort && ALL_REASONING_EFFORTS.includes(effort as typeof ALL_REASONING_EFFORTS[number])) {
|
|
1185
|
+
efforts.add(effort);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1167
1188
|
return { id: modelId, label };
|
|
1168
1189
|
})
|
|
1169
1190
|
.filter((entry): entry is AgentModelOption => Boolean(entry));
|
|
1170
1191
|
const defaultModel =
|
|
1171
1192
|
firstString(raw, ["defaultModel", "default_model", "currentModel"]) ??
|
|
1172
|
-
firstString(asRecord(raw?.defaults), ["model"])
|
|
1173
|
-
|
|
1193
|
+
firstString(asRecord(raw?.defaults), ["model"]) ??
|
|
1194
|
+
inferredDefault;
|
|
1195
|
+
const explicitEffortsValue =
|
|
1174
1196
|
Array.isArray(raw?.reasoningEfforts) ? raw.reasoningEfforts :
|
|
1175
1197
|
Array.isArray(raw?.reasoning_efforts) ? raw.reasoning_efforts :
|
|
1176
1198
|
Array.isArray(raw?.efforts) ? raw.efforts :
|
|
1177
1199
|
undefined;
|
|
1178
|
-
|
|
1179
|
-
|
|
1200
|
+
if (explicitEffortsValue) {
|
|
1201
|
+
for (const item of explicitEffortsValue) {
|
|
1202
|
+
if (typeof item === "string" && ALL_REASONING_EFFORTS.includes(item as typeof ALL_REASONING_EFFORTS[number])) {
|
|
1203
|
+
efforts.add(item);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
const reasoningEfforts = efforts.size
|
|
1208
|
+
? ALL_REASONING_EFFORTS.filter((effort) => efforts.has(effort))
|
|
1209
|
+
: undefined;
|
|
1180
1210
|
if (models.length === 0 && !defaultModel && !reasoningEfforts?.length) return undefined;
|
|
1181
1211
|
return {
|
|
1182
|
-
...(models.length > 0 ? { models
|
|
1212
|
+
...(models.length > 0 ? { models } : {}),
|
|
1183
1213
|
...(defaultModel ? { defaultModel } : {}),
|
|
1184
1214
|
...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
|
|
1185
1215
|
};
|
|
@@ -1300,6 +1330,7 @@ export class AgentWorkspaceProxy {
|
|
|
1300
1330
|
}
|
|
1301
1331
|
case "agent.v2.snapshot.request": {
|
|
1302
1332
|
const payload = parseTypedPayload("agent.v2.snapshot.request", envelope.payload);
|
|
1333
|
+
await this.syncProviderSessions();
|
|
1303
1334
|
this.sendSnapshot(payload.conversationId);
|
|
1304
1335
|
break;
|
|
1305
1336
|
}
|
|
@@ -1465,16 +1496,36 @@ export class AgentWorkspaceProxy {
|
|
|
1465
1496
|
): Promise<void> {
|
|
1466
1497
|
if (client instanceof AcpClient && protocol !== "codex-app-server") return;
|
|
1467
1498
|
const listModels = (client as { listModels?: () => Promise<unknown> }).listModels;
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1499
|
+
let runtimeCapabilities: ProviderRuntimeCapabilities | undefined;
|
|
1500
|
+
if (typeof listModels === "function") {
|
|
1501
|
+
try {
|
|
1502
|
+
const result = await listModels.call(client);
|
|
1503
|
+
runtimeCapabilities = parseModelListCapabilities(result);
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
if (this.input.verbose) {
|
|
1506
|
+
process.stderr.write(`[agent:v2] model/list failed for ${provider}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
1507
|
+
}
|
|
1476
1508
|
}
|
|
1477
1509
|
}
|
|
1510
|
+
if (provider === "codex") {
|
|
1511
|
+
// Codex `model/list` is unreliable across versions — make sure mobile always
|
|
1512
|
+
// sees a usable model picker by merging in a static fallback list.
|
|
1513
|
+
const merged: ProviderRuntimeCapabilities = {
|
|
1514
|
+
models: runtimeCapabilities?.models?.length
|
|
1515
|
+
? runtimeCapabilities.models
|
|
1516
|
+
: [...CODEX_FALLBACK_MODELS],
|
|
1517
|
+
defaultModel: runtimeCapabilities?.defaultModel ?? CODEX_FALLBACK_DEFAULT_MODEL,
|
|
1518
|
+
reasoningEfforts: runtimeCapabilities?.reasoningEfforts?.length
|
|
1519
|
+
? runtimeCapabilities.reasoningEfforts
|
|
1520
|
+
: [...ALL_REASONING_EFFORTS],
|
|
1521
|
+
commands: runtimeCapabilities?.commands,
|
|
1522
|
+
modes: runtimeCapabilities?.modes,
|
|
1523
|
+
currentMode: runtimeCapabilities?.currentMode,
|
|
1524
|
+
};
|
|
1525
|
+
this.providerCapabilities.set(provider, merged);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
if (runtimeCapabilities) this.providerCapabilities.set(provider, runtimeCapabilities);
|
|
1478
1529
|
}
|
|
1479
1530
|
|
|
1480
1531
|
private async syncProviderSessions(): Promise<void> {
|