linkshell-cli 0.2.125 → 0.3.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.
Files changed (39) hide show
  1. package/dist/cli/src/commands/setup.js +2 -20
  2. package/dist/cli/src/commands/setup.js.map +1 -1
  3. package/dist/cli/src/index.js +26 -28
  4. package/dist/cli/src/index.js.map +1 -1
  5. package/dist/cli/src/providers.d.ts +7 -3
  6. package/dist/cli/src/providers.js +19 -76
  7. package/dist/cli/src/providers.js.map +1 -1
  8. package/dist/cli/src/runtime/acp/agent-session.d.ts +1 -1
  9. package/dist/cli/src/runtime/acp/agent-session.js +4 -4
  10. package/dist/cli/src/runtime/acp/agent-session.js.map +1 -1
  11. package/dist/cli/src/runtime/acp/agent-workspace.d.ts +1 -1
  12. package/dist/cli/src/runtime/acp/agent-workspace.js +17 -62
  13. package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
  14. package/dist/cli/src/runtime/bridge-session.d.ts +1 -31
  15. package/dist/cli/src/runtime/bridge-session.js +57 -993
  16. package/dist/cli/src/runtime/bridge-session.js.map +1 -1
  17. package/dist/cli/src/runtime/screen-fallback.d.ts +1 -1
  18. package/dist/cli/src/runtime/screen-fallback.js +4 -4
  19. package/dist/cli/src/runtime/screen-fallback.js.map +1 -1
  20. package/dist/cli/src/runtime/screen-share.d.ts +1 -1
  21. package/dist/cli/src/runtime/screen-share.js +7 -7
  22. package/dist/cli/src/runtime/screen-share.js.map +1 -1
  23. package/dist/cli/tsconfig.tsbuildinfo +1 -1
  24. package/dist/shared-protocol/src/index.d.ts +3743 -5570
  25. package/dist/shared-protocol/src/index.js +19 -84
  26. package/dist/shared-protocol/src/index.js.map +1 -1
  27. package/package.json +12 -12
  28. package/src/commands/setup.ts +5 -31
  29. package/src/index.ts +29 -34
  30. package/src/providers.ts +26 -108
  31. package/src/runtime/acp/agent-workspace.ts +18 -63
  32. package/src/runtime/bridge-session.ts +57 -1091
  33. package/src/runtime/screen-fallback.ts +5 -5
  34. package/src/runtime/screen-share.ts +8 -8
  35. package/src/types/linkshell-gateway.d.ts +18 -0
  36. package/dist/cli/src/runtime/acp-relay.d.ts +0 -23
  37. package/dist/cli/src/runtime/acp-relay.js +0 -73
  38. package/dist/cli/src/runtime/acp-relay.js.map +0 -1
  39. package/src/runtime/acp/agent-session.ts +0 -1180
package/src/providers.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import { accessSync, constants, existsSync } from "node:fs";
2
- import { delimiter, join } from "node:path";
3
- import type { TerminalProvider } from "@linkshell/protocol";
2
+ import { basename, delimiter, join } from "node:path";
4
3
 
5
- export type ProviderName = TerminalProvider;
4
+ export type ProviderName = "shell";
6
5
 
7
6
  export interface ProviderConfig {
8
7
  provider: ProviderName;
@@ -11,6 +10,8 @@ export interface ProviderConfig {
11
10
  env: NodeJS.ProcessEnv;
12
11
  }
13
12
 
13
+ export type ShellConfig = ProviderConfig;
14
+
14
15
  function stripOuterQuotes(value: string): string {
15
16
  const trimmed = value.trim();
16
17
  if (
@@ -65,125 +66,42 @@ function which(bin: string): string | undefined {
65
66
  return undefined;
66
67
  }
67
68
 
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
- };
69
+ function shellSupportsLoginArg(command: string): boolean {
70
+ const name = basename(command).toLowerCase();
71
+ return ["bash", "zsh", "sh", "fish"].includes(name);
88
72
  }
89
73
 
90
- function resolveCodexProvider(input: {
74
+ export function resolveShellConfig(input: {
91
75
  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
- }
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
+ : [];
107
87
 
108
88
  return {
109
- provider: "codex",
89
+ provider: "shell",
110
90
  command: resolved,
111
- args: input.args,
91
+ args,
112
92
  env: { ...process.env },
113
93
  };
114
94
  }
115
95
 
116
- function resolveGeminiProvider(input: {
96
+ export function resolveProviderConfig(input: {
97
+ provider?: string;
117
98
  command?: string;
118
99
  args: string[];
119
100
  }): 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) {
101
+ if (input.provider && input.provider !== "shell") {
129
102
  process.stderr.write(
130
- "[warn] GOOGLE_API_KEY / GEMINI_API_KEY not set Gemini may fail to authenticate\n",
103
+ `[warn] terminal provider "${input.provider}" is ignored in protocol v2; starting the system shell instead\n`,
131
104
  );
132
105
  }
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
- }
106
+ return resolveShellConfig({ command: input.command, args: input.args });
189
107
  }
@@ -764,8 +764,6 @@ interface ProviderRuntimeCapabilities {
764
764
  }
765
765
 
766
766
  const ALL_REASONING_EFFORTS = ["none", "minimal", "low", "medium", "high", "xhigh"] as const;
767
- const ALL_PERMISSION_MODES = ["read_only", "workspace_write", "full_access"] as const;
768
- const CODEX_COMMAND_NAMES = ["plan", "exit-plan", "compact", "clear", "status", "review", "subagents"] as const;
769
767
  const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
770
768
  "add-dir",
771
769
  "agents",
@@ -1039,56 +1037,16 @@ function customClaudeCommands(cwd: string): AgentCommandDescriptor[] {
1039
1037
  function defaultProviderCommands(provider: AgentProvider, cwd: string, enabled: boolean): AgentCommandDescriptor[] {
1040
1038
  const disabledReason = enabled ? undefined : `${providerLabel(provider)} 未安装或启动失败`;
1041
1039
  if (provider === "codex") {
1042
- return CODEX_COMMAND_NAMES.map((name) => makeCommand({
1043
- provider,
1044
- name,
1045
- source: "linkshell",
1046
- category: name === "plan" || name === "exit-plan" ? "Modes" : "Codex",
1047
- description: {
1048
- "plan": "Enter Codex plan mode",
1049
- "exit-plan": "Exit Codex plan mode",
1050
- compact: "Compact the current thread",
1051
- clear: "Start a fresh Codex thread",
1052
- status: "Show LinkShell agent status",
1053
- review: "Ask Codex to review local changes",
1054
- subagents: "Insert a delegation prompt",
1055
- }[name],
1056
- argsMode: name === "review" || name === "subagents" ? "optional" : "none",
1057
- destructive: name === "clear",
1058
- disabledReason,
1059
- executionKind: name === "review" || name === "subagents" ? "prompt" : "native",
1060
- }));
1040
+ return [];
1061
1041
  }
1062
1042
  if (provider === "claude") {
1063
- const builtIns = CLAUDE_BUILT_IN_COMMANDS
1064
- .filter((entry) => isClaudeRemoteFriendlyCommand(entry.name))
1065
- .map((entry) => makeCommand({
1066
- provider,
1067
- name: entry.name,
1068
- description: entry.description,
1069
- argsMode: entry.argsMode,
1070
- destructive: entry.destructive,
1071
- disabledReason,
1072
- executionKind: "prompt",
1073
- }));
1074
1043
  const custom = customClaudeCommands(cwd).map((command) => ({
1075
1044
  ...command,
1076
1045
  disabledReason: command.disabledReason ?? disabledReason,
1077
1046
  }));
1078
- return [...builtIns, ...custom];
1047
+ return custom;
1079
1048
  }
1080
- return [
1081
- makeCommand({
1082
- provider,
1083
- name: "status",
1084
- source: "linkshell",
1085
- category: "LinkShell",
1086
- description: "Show LinkShell agent status",
1087
- argsMode: "none",
1088
- disabledReason,
1089
- executionKind: "native",
1090
- }),
1091
- ];
1049
+ return [];
1092
1050
  }
1093
1051
 
1094
1052
  function mergeCommands(...groups: Array<AgentCommandDescriptor[] | undefined>): AgentCommandDescriptor[] {
@@ -1265,7 +1223,7 @@ export class AgentWorkspaceProxy {
1265
1223
 
1266
1224
  constructor(
1267
1225
  private readonly input: {
1268
- sessionId: string;
1226
+ hostDeviceId: string;
1269
1227
  cwd: string;
1270
1228
  availableProviders: AgentProvider[];
1271
1229
  command?: string;
@@ -1293,7 +1251,7 @@ export class AgentWorkspaceProxy {
1293
1251
  );
1294
1252
  this.input.send(createEnvelope({
1295
1253
  type: "agent.v2.conversation.list.result",
1296
- sessionId: this.input.sessionId,
1254
+ hostDeviceId: this.input.hostDeviceId,
1297
1255
  payload: { conversations },
1298
1256
  }));
1299
1257
  break;
@@ -1541,17 +1499,14 @@ export class AgentWorkspaceProxy {
1541
1499
  supportsPermission,
1542
1500
  supportsPlan: enabled,
1543
1501
  supportsCancel: enabled,
1544
- models: runtimeCapabilities?.models ?? [{ id: "default", label: "默认模型" }],
1545
- defaultModel: runtimeCapabilities?.defaultModel ?? "default",
1502
+ models: runtimeCapabilities?.models,
1503
+ defaultModel: runtimeCapabilities?.defaultModel,
1546
1504
  reasoningEfforts: supportsReasoningEffort
1547
- ? runtimeCapabilities?.reasoningEfforts ?? [...ALL_REASONING_EFFORTS]
1505
+ ? runtimeCapabilities?.reasoningEfforts ?? []
1548
1506
  : [],
1549
- permissionModes: supportsPermission ? [...ALL_PERMISSION_MODES] : [],
1507
+ permissionModes: [],
1550
1508
  commands,
1551
- modes: runtimeCapabilities?.modes ?? (provider === "codex" ? [
1552
- { id: "default", title: "Default", description: "Run normal implementation turns" },
1553
- { id: "plan", title: "Plan", description: "Discuss and produce an implementation plan first" },
1554
- ] : []),
1509
+ modes: runtimeCapabilities?.modes ?? [],
1555
1510
  currentMode,
1556
1511
  features: {
1557
1512
  images: supportsImages,
@@ -1567,7 +1522,7 @@ export class AgentWorkspaceProxy {
1567
1522
  const anyPermission = providers.some((p) => p.supportsPermission);
1568
1523
  this.input.send(createEnvelope({
1569
1524
  type: "agent.v2.capabilities",
1570
- sessionId: this.input.sessionId,
1525
+ hostDeviceId: this.input.hostDeviceId,
1571
1526
  payload: {
1572
1527
  enabled: anyEnabled,
1573
1528
  provider: this.input.availableProviders[0] ?? "codex",
@@ -1629,7 +1584,7 @@ export class AgentWorkspaceProxy {
1629
1584
  this.activeConversationId = existingConversation.id;
1630
1585
  this.input.send(createEnvelope({
1631
1586
  type: "agent.v2.conversation.opened",
1632
- sessionId: this.input.sessionId,
1587
+ hostDeviceId: this.input.hostDeviceId,
1633
1588
  payload: {
1634
1589
  conversation: existingConversation,
1635
1590
  snapshot: this.timelines.get(existingConversation.id) ?? [],
@@ -1668,7 +1623,7 @@ export class AgentWorkspaceProxy {
1668
1623
  this.timelines.set(conversation.id, this.timelines.get(conversation.id) ?? []);
1669
1624
  this.input.send(createEnvelope({
1670
1625
  type: "agent.v2.conversation.opened",
1671
- sessionId: this.input.sessionId,
1626
+ hostDeviceId: this.input.hostDeviceId,
1672
1627
  payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
1673
1628
  }));
1674
1629
  return conversation;
@@ -1720,7 +1675,7 @@ export class AgentWorkspaceProxy {
1720
1675
  });
1721
1676
  this.input.send(createEnvelope({
1722
1677
  type: "agent.v2.conversation.opened",
1723
- sessionId: this.input.sessionId,
1678
+ hostDeviceId: this.input.hostDeviceId,
1724
1679
  payload: { conversation, snapshot: this.timelines.get(conversation.id) ?? [] },
1725
1680
  }));
1726
1681
  return conversation;
@@ -2678,7 +2633,7 @@ export class AgentWorkspaceProxy {
2678
2633
  this.upsertItem(conversationId, item);
2679
2634
  this.input.send(createEnvelope({
2680
2635
  type: "agent.v2.permission.request",
2681
- sessionId: this.input.sessionId,
2636
+ hostDeviceId: this.input.hostDeviceId,
2682
2637
  payload: { conversationId, ...permission, item },
2683
2638
  }));
2684
2639
 
@@ -2956,7 +2911,7 @@ export class AgentWorkspaceProxy {
2956
2911
  const conversation = this.conversations.get(conversationId);
2957
2912
  this.input.send(createEnvelope({
2958
2913
  type: "agent.v2.event",
2959
- sessionId: this.input.sessionId,
2914
+ hostDeviceId: this.input.hostDeviceId,
2960
2915
  payload: { conversationId, conversation, item },
2961
2916
  }));
2962
2917
  }
@@ -2964,7 +2919,7 @@ export class AgentWorkspaceProxy {
2964
2919
  private emitConversation(conversation: AgentConversation): void {
2965
2920
  this.input.send(createEnvelope({
2966
2921
  type: "agent.v2.event",
2967
- sessionId: this.input.sessionId,
2922
+ hostDeviceId: this.input.hostDeviceId,
2968
2923
  payload: { conversationId: conversation.id, conversation },
2969
2924
  }));
2970
2925
  }
@@ -3013,7 +2968,7 @@ export class AgentWorkspaceProxy {
3013
2968
  : [...this.timelines.values()].flat();
3014
2969
  this.input.send(createEnvelope({
3015
2970
  type: "agent.v2.snapshot",
3016
- sessionId: this.input.sessionId,
2971
+ hostDeviceId: this.input.hostDeviceId,
3017
2972
  payload: {
3018
2973
  conversations,
3019
2974
  activeConversationId: this.activeConversationId,