@steerable/agent-shell 0.6.51 → 0.6.53

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 (43) hide show
  1. package/dist/brand.d.ts +5 -0
  2. package/dist/goal-store.d.ts +43 -0
  3. package/dist/goal-store.js +155 -0
  4. package/dist/host/runtime.js +15 -3
  5. package/dist/host-tools.d.ts +1 -1
  6. package/dist/host-tools.js +6 -0
  7. package/dist/local-backend/portable-bundle.d.ts +16 -2
  8. package/dist/local-backend/portable-bundle.js +21 -9
  9. package/dist/local-backend/portable-service.js +25 -11
  10. package/dist/local-backend/router.js +71 -27
  11. package/dist/local-backend/subagent-profiles.js +4 -0
  12. package/dist/local-backend/task-service.d.ts +6 -0
  13. package/dist/local-backend/task-service.js +47 -2
  14. package/dist/local-executor.d.ts +36 -2
  15. package/dist/local-executor.js +171 -19
  16. package/dist/mcp-executor.d.ts +42 -10
  17. package/dist/mcp-executor.js +248 -41
  18. package/dist/mcp-server-registry.d.ts +56 -8
  19. package/dist/mcp-server-registry.js +191 -26
  20. package/dist/product-config.d.ts +14 -0
  21. package/dist/project-home.d.ts +1 -1
  22. package/dist/project-home.js +1 -1
  23. package/dist/project-registry.d.ts +4 -4
  24. package/dist/project-registry.js +2 -2
  25. package/dist/shell-exec.d.ts +62 -0
  26. package/dist/shell-exec.js +147 -0
  27. package/dist/shell-process.d.ts +3 -0
  28. package/dist/shell-process.js +137 -0
  29. package/dist/sidecar/boot.d.ts +5 -2
  30. package/dist/sidecar/boot.js +5 -0
  31. package/dist/sidecar/reverse-approval.d.ts +2 -0
  32. package/dist/sidecar/reverse-approval.js +2 -0
  33. package/dist/sidecar/reverse-tools.d.ts +6 -2
  34. package/dist/sidecar/reverse-tools.js +9 -1
  35. package/dist/sidecar/types.d.ts +10 -1
  36. package/dist/storage/write-lease-error.js +4 -1
  37. package/dist/tool-presentation.d.ts +23 -0
  38. package/dist/tool-presentation.js +266 -0
  39. package/dist/tool-router.d.ts +26 -8
  40. package/dist/tool-router.js +275 -24
  41. package/dist/workspace-search.d.ts +35 -0
  42. package/dist/workspace-search.js +186 -0
  43. package/package.json +5 -5
@@ -9,7 +9,7 @@
9
9
  * 实现,单测用内存实现——本模块不 import electron,保持纯 Node 可测。
10
10
  */
11
11
  import { randomUUID } from 'node:crypto';
12
- import { mcpExecutor } from './mcp-executor.js';
12
+ import { MAX_MCP_SERVER_TOOLS, mcpExecutor, } from './mcp-executor.js';
13
13
  const STORE_KEY = 'mcpServers';
14
14
  /** 工具名只允许 [a-z0-9-];中文名会被剥光,退化为 srv-<id前6位>。 */
15
15
  export function sanitizeServerKey(name, id) {
@@ -19,15 +19,90 @@ export function sanitizeServerKey(name, id) {
19
19
  .replace(/^-+|-+$/g, '');
20
20
  return cleaned || `srv-${id.slice(0, 6)}`;
21
21
  }
22
+ function normalizeStoredEntry(entry) {
23
+ if ('transport' in entry)
24
+ return entry;
25
+ const legacy = entry;
26
+ return { ...legacy, transport: 'stdio' };
27
+ }
28
+ function normalizeTransportInput(input) {
29
+ if (input.transport === 'streamable-http') {
30
+ const raw = input;
31
+ if (typeof raw.command === 'string' && raw.command.trim()) {
32
+ throw new Error('Streamable HTTP 配置不能包含 command');
33
+ }
34
+ const url = parseMcpHttpUrl(input.url);
35
+ const headers = { ...(input.headers ?? {}) };
36
+ if (Object.keys(headers).some((name) => name.toLowerCase() === 'authorization')) {
37
+ throw new Error('Authorization 不允许明文保存;请使用 bearerTokenEnvVar 或 headersFromEnv');
38
+ }
39
+ const bearerTokenEnvVar = input.bearerTokenEnvVar?.trim() || undefined;
40
+ return {
41
+ transport: 'streamable-http',
42
+ url,
43
+ headers,
44
+ headersFromEnv: { ...(input.headersFromEnv ?? {}) },
45
+ bearerTokenEnvVar,
46
+ reconnect: input.reconnect ? { ...input.reconnect } : undefined,
47
+ };
48
+ }
49
+ if ('url' in input && typeof input.url === 'string' && input.url.trim()) {
50
+ throw new Error('stdio 配置不能包含 url');
51
+ }
52
+ const command = input.command.trim();
53
+ if (!command)
54
+ throw new Error('启动命令不能为空');
55
+ return {
56
+ transport: 'stdio',
57
+ command,
58
+ args: (input.args ?? []).map(String),
59
+ env: { ...(input.env ?? {}) },
60
+ cwd: input.cwd?.trim() || undefined,
61
+ };
62
+ }
63
+ function parseMcpHttpUrl(raw) {
64
+ let url;
65
+ try {
66
+ url = new URL(raw.trim());
67
+ }
68
+ catch {
69
+ throw new Error('MCP HTTP URL 无效');
70
+ }
71
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
72
+ throw new Error('MCP HTTP URL 必须使用 http 或 https');
73
+ }
74
+ if (url.username || url.password) {
75
+ throw new Error('MCP HTTP URL 不能包含凭据');
76
+ }
77
+ return url.toString();
78
+ }
79
+ function stringRecord(value) {
80
+ if (!value || typeof value !== 'object' || Array.isArray(value))
81
+ return {};
82
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
83
+ key,
84
+ String(item),
85
+ ]));
86
+ }
87
+ function stringValue(value) {
88
+ return typeof value === 'string' ? value.trim() : '';
89
+ }
22
90
  export class McpServerRegistry {
23
91
  store;
92
+ executor;
24
93
  /** serverId → 最近一次 listTools 的结果(内存缓存,重启后由后台刷新重建)。 */
25
94
  toolCache = new Map();
26
- constructor(store) {
95
+ constructor(store, executor = mcpExecutor) {
27
96
  this.store = store;
97
+ this.executor = executor;
28
98
  }
29
99
  list() {
30
- return this.store.get(STORE_KEY) ?? [];
100
+ const stored = this.store.get(STORE_KEY) ?? [];
101
+ const migrated = stored.map(normalizeStoredEntry);
102
+ if (migrated.some((entry, index) => entry !== stored[index])) {
103
+ this.store.set(STORE_KEY, migrated);
104
+ }
105
+ return migrated;
31
106
  }
32
107
  get(idOrName) {
33
108
  const needle = idOrName.trim().toLowerCase();
@@ -43,24 +118,18 @@ export class McpServerRegistry {
43
118
  }
44
119
  create(input) {
45
120
  const name = input.name.trim();
46
- const command = input.command.trim();
47
121
  if (!name)
48
122
  throw new Error('服务名称不能为空');
49
- if (!command)
50
- throw new Error('启动命令不能为空');
51
123
  if (this.get(name))
52
124
  throw new Error(`已存在同名服务「${name}」`);
53
125
  const now = new Date().toISOString();
54
126
  const entry = {
55
127
  id: randomUUID(),
56
128
  name,
57
- command,
58
- args: (input.args ?? []).map((a) => String(a)),
59
- env: { ...(input.env ?? {}) },
60
- cwd: input.cwd?.trim() || undefined,
61
129
  enabled: input.enabled ?? true,
62
130
  createdAt: now,
63
131
  updatedAt: now,
132
+ ...normalizeTransportInput(input),
64
133
  };
65
134
  this.store.set(STORE_KEY, [...this.list(), entry]);
66
135
  return entry;
@@ -77,18 +146,39 @@ export class McpServerRegistry {
77
146
  const nameClash = servers.some((s) => s.id !== id && s.name.toLowerCase() === nextName.toLowerCase());
78
147
  if (nameClash)
79
148
  throw new Error(`已存在同名服务「${nextName}」`);
149
+ const transport = updates.transport ?? current.transport;
150
+ const transportInput = transport === 'stdio'
151
+ ? {
152
+ name: nextName,
153
+ transport: 'stdio',
154
+ command: updates.command ?? (current.transport === 'stdio' ? current.command : ''),
155
+ args: updates.args ?? (current.transport === 'stdio' ? current.args : []),
156
+ env: updates.env ?? (current.transport === 'stdio' ? current.env : {}),
157
+ cwd: updates.cwd ?? (current.transport === 'stdio' ? current.cwd : undefined),
158
+ }
159
+ : {
160
+ name: nextName,
161
+ transport: 'streamable-http',
162
+ url: updates.url ?? (current.transport === 'streamable-http' ? current.url : ''),
163
+ headers: updates.headers ??
164
+ (current.transport === 'streamable-http' ? current.headers : {}),
165
+ headersFromEnv: updates.headersFromEnv ??
166
+ (current.transport === 'streamable-http' ? current.headersFromEnv : {}),
167
+ bearerTokenEnvVar: updates.bearerTokenEnvVar ??
168
+ (current.transport === 'streamable-http'
169
+ ? current.bearerTokenEnvVar
170
+ : undefined),
171
+ reconnect: updates.reconnect ??
172
+ (current.transport === 'streamable-http' ? current.reconnect : undefined),
173
+ };
80
174
  const next = {
81
- ...current,
175
+ id: current.id,
82
176
  name: nextName,
83
- command: updates.command !== undefined ? updates.command.trim() : current.command,
84
- args: updates.args !== undefined ? updates.args.map((a) => String(a)) : current.args,
85
- env: updates.env !== undefined ? { ...updates.env } : current.env,
86
- cwd: updates.cwd !== undefined ? updates.cwd.trim() || undefined : current.cwd,
87
177
  enabled: updates.enabled !== undefined ? updates.enabled : current.enabled,
178
+ createdAt: current.createdAt,
88
179
  updatedAt: new Date().toISOString(),
180
+ ...normalizeTransportInput(transportInput),
89
181
  };
90
- if (!next.command)
91
- throw new Error('启动命令不能为空');
92
182
  servers[idx] = next;
93
183
  this.store.set(STORE_KEY, servers);
94
184
  // 配置变了,旧工具缓存不可信
@@ -131,10 +221,27 @@ export class McpServerRegistry {
131
221
  for (const [name, cfgRaw] of Object.entries(container)) {
132
222
  const cfg = (cfgRaw ?? {});
133
223
  const command = typeof cfg.command === 'string' ? cfg.command.trim() : '';
134
- if (!command || this.get(name)) {
224
+ const url = typeof cfg.url === 'string' ? cfg.url.trim() : '';
225
+ if ((!command && !url) || (command && url) || this.get(name)) {
135
226
  result.skipped.push(name);
136
227
  continue;
137
228
  }
229
+ if (url) {
230
+ const headers = stringRecord(cfg.headers);
231
+ const headersFromEnv = stringRecord(cfg.envHttpHeaders ?? cfg.env_http_headers ?? cfg.headersFromEnv);
232
+ const bearerTokenEnvVar = stringValue(cfg.bearerTokenEnvVar ?? cfg.bearer_token_env_var) || undefined;
233
+ const entry = this.create({
234
+ name: name.trim(),
235
+ transport: 'streamable-http',
236
+ url,
237
+ headers,
238
+ headersFromEnv,
239
+ bearerTokenEnvVar,
240
+ enabled: true,
241
+ });
242
+ result.added.push(entry);
243
+ continue;
244
+ }
138
245
  const env = {};
139
246
  if (cfg.env && typeof cfg.env === 'object' && !Array.isArray(cfg.env)) {
140
247
  for (const [k, v] of Object.entries(cfg.env)) {
@@ -160,12 +267,48 @@ export class McpServerRegistry {
160
267
  return clash ? `${base}-${entry.id.slice(0, 4)}` : base;
161
268
  }
162
269
  toExecutorConfig(entry) {
163
- return {
164
- command: entry.command,
165
- args: entry.args,
166
- env: entry.env,
167
- cwd: entry.cwd,
270
+ return entry.transport === 'stdio'
271
+ ? {
272
+ transport: 'stdio',
273
+ command: entry.command,
274
+ args: entry.args,
275
+ env: entry.env,
276
+ cwd: entry.cwd,
277
+ }
278
+ : {
279
+ transport: 'streamable-http',
280
+ url: entry.url,
281
+ headers: entry.headers,
282
+ headersFromEnv: entry.headersFromEnv,
283
+ bearerTokenEnvVar: entry.bearerTokenEnvVar,
284
+ reconnect: entry.reconnect,
285
+ };
286
+ }
287
+ toPublicEntry(entry) {
288
+ const common = {
289
+ id: entry.id,
290
+ name: entry.name,
291
+ transport: entry.transport,
292
+ enabled: entry.enabled,
293
+ createdAt: entry.createdAt,
294
+ updatedAt: entry.updatedAt,
168
295
  };
296
+ return entry.transport === 'stdio'
297
+ ? {
298
+ ...common,
299
+ command: entry.command,
300
+ args: [...entry.args],
301
+ cwd: entry.cwd,
302
+ envKeys: Object.keys(entry.env).sort(),
303
+ }
304
+ : {
305
+ ...common,
306
+ url: entry.url,
307
+ headerNames: Object.keys(entry.headers).sort(),
308
+ headersFromEnv: { ...entry.headersFromEnv },
309
+ bearerTokenEnvVar: entry.bearerTokenEnvVar,
310
+ reconnect: entry.reconnect,
311
+ };
169
312
  }
170
313
  getCachedTools(serverId) {
171
314
  return this.toolCache.get(serverId) ?? null;
@@ -211,8 +354,24 @@ export class McpServerRegistry {
211
354
  const entry = this.list().find((s) => s.id === serverId);
212
355
  if (!entry)
213
356
  throw new Error('服务不存在');
214
- const result = await mcpExecutor.listTools(this.toExecutorConfig(entry));
215
- const cached = result.success
357
+ const applyChanged = (tools) => {
358
+ if (tools.length > MAX_MCP_SERVER_TOOLS)
359
+ return;
360
+ this.toolCache.set(serverId, {
361
+ tools: tools.map((tool) => ({ ...tool })),
362
+ fetchedAt: new Date().toISOString(),
363
+ error: null,
364
+ });
365
+ };
366
+ const result = await this.executor.listTools(this.toExecutorConfig(entry), applyChanged, (error) => {
367
+ this.toolCache.set(serverId, {
368
+ tools: [],
369
+ fetchedAt: new Date().toISOString(),
370
+ error,
371
+ });
372
+ });
373
+ const overCap = (result.tools?.length ?? 0) > MAX_MCP_SERVER_TOOLS;
374
+ const cached = result.success && !overCap
216
375
  ? {
217
376
  tools: (result.tools ?? []).map((t) => ({
218
377
  name: t.name,
@@ -222,7 +381,13 @@ export class McpServerRegistry {
222
381
  fetchedAt: new Date().toISOString(),
223
382
  error: null,
224
383
  }
225
- : { tools: [], fetchedAt: new Date().toISOString(), error: result.error ?? '未知错误' };
384
+ : {
385
+ tools: [],
386
+ fetchedAt: new Date().toISOString(),
387
+ error: overCap
388
+ ? `服务工具数超过每服务 ${MAX_MCP_SERVER_TOOLS} 个的上限`
389
+ : result.error ?? '未知错误',
390
+ };
226
391
  this.toolCache.set(serverId, cached);
227
392
  return cached;
228
393
  }
@@ -58,6 +58,11 @@ export interface ProductConfig {
58
58
  * STEERABLE_APPROVAL=0 仍是调试逃生口。
59
59
  */
60
60
  approval?: 'host' | 'off';
61
+ /**
62
+ * 白名单外出网询问:host = 弹宿主审批;off = 自动加入当前代理会话。
63
+ * 缺省 host。自动放行仅存活到代理进程退出,不修改持久白名单。
64
+ */
65
+ egressApproval?: 'host' | 'off';
61
66
  /**
62
67
  * 对话模式。缺省 `['agent','plan']`。只留一种时渲染层不显示切换。
63
68
  */
@@ -100,11 +105,20 @@ export interface ProductConfig {
100
105
  */
101
106
  builtinMcp?: Array<{
102
107
  name: string;
108
+ transport?: 'stdio';
103
109
  command: string;
104
110
  args?: string[];
105
111
  env?: Record<string, string>;
106
112
  cwd?: string;
107
113
  enabled?: boolean;
114
+ } | {
115
+ name: string;
116
+ transport: 'streamable-http';
117
+ url: string;
118
+ headers?: Record<string, string>;
119
+ headersFromEnv?: Record<string, string>;
120
+ bearerTokenEnvVar?: string;
121
+ enabled?: boolean;
108
122
  }>;
109
123
  /**
110
124
  * P3.1 多智能体编排六件套 (agent_spawn / agent_wait / agent_send / agent_interrupt / agent_close / agent_list)。
@@ -2,7 +2,7 @@
2
2
  * 项目默认家目录:`Documents/<应用名>/<项目名>/`。
3
3
  *
4
4
  * 新建项目不再等于「选一个已有文件夹」——项目是带名字的容器,
5
- * 家目录由本模块分配并创建。用户另加的源文件夹是附加只读根,
5
+ * 家目录由本模块分配并创建。用户另加的源文件夹是额外可写根(含子目录),
6
6
  * 不替代这个家目录。
7
7
  */
8
8
  /** 把 `~` / `~/…` 展开成绝对路径;其它输入原样(已 trim)。 */
@@ -2,7 +2,7 @@
2
2
  * 项目默认家目录:`Documents/<应用名>/<项目名>/`。
3
3
  *
4
4
  * 新建项目不再等于「选一个已有文件夹」——项目是带名字的容器,
5
- * 家目录由本模块分配并创建。用户另加的源文件夹是附加只读根,
5
+ * 家目录由本模块分配并创建。用户另加的源文件夹是额外可写根(含子目录),
6
6
  * 不替代这个家目录。
7
7
  */
8
8
  import fs from 'node:fs';
@@ -4,8 +4,8 @@
4
4
  * 项目 = 名字 + 托管家目录 + 可选源文件夹。家目录默认建在
5
5
  * `Documents/<应用名>/<项目名>/`(见 project-home.ts)。chat 通过
6
6
  * `chat_sessions.project_id` 绑定项目;绑定后文件读写与命令执行被硬限制
7
- * 在家目录内(见 tool-router.ts 的 ToolExecContext.projectRoot);源文件夹
8
- * 只放宽 local_read_file。
7
+ * 在家目录及其子目录内(见 tool-router.ts 的 ToolExecContext.projectRoot);
8
+ * 源文件夹及其子目录同样可写。
9
9
  *
10
10
  * 持久化在 userData/agent-projects.json。存储通过 {@link ProjectKvStore}
11
11
  * 接口注入:main.ts 用 electron-store 实现,单测用内存实现——本模块不
@@ -18,8 +18,8 @@ export interface ProjectRecord {
18
18
  /** 托管家目录(绝对路径)。默认 `Documents/<应用名>/<项目名>/`。 */
19
19
  folderPath: string;
20
20
  /**
21
- * 附加源文件夹(已有代码目录)。只放宽读取,不替代家目录,也不放宽写入。
22
- * 旧记录没有此字段,读取时按空列表处理。
21
+ * 附加源文件夹(已有代码目录)。与家目录一样可读写,范围含各自子目录。
22
+ * 不替代家目录。旧记录没有此字段,读取时按空列表处理。
23
23
  */
24
24
  sourceFolders?: string[];
25
25
  /**
@@ -4,8 +4,8 @@
4
4
  * 项目 = 名字 + 托管家目录 + 可选源文件夹。家目录默认建在
5
5
  * `Documents/<应用名>/<项目名>/`(见 project-home.ts)。chat 通过
6
6
  * `chat_sessions.project_id` 绑定项目;绑定后文件读写与命令执行被硬限制
7
- * 在家目录内(见 tool-router.ts 的 ToolExecContext.projectRoot);源文件夹
8
- * 只放宽 local_read_file。
7
+ * 在家目录及其子目录内(见 tool-router.ts 的 ToolExecContext.projectRoot);
8
+ * 源文件夹及其子目录同样可写。
9
9
  *
10
10
  * 持久化在 userData/agent-projects.json。存储通过 {@link ProjectKvStore}
11
11
  * 接口注入:main.ts 用 electron-store 实现,单测用内存实现——本模块不
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Shell 执行规划:前缀策略,以及本地 / SSH 的启动参数。
3
+ * 这里不启动进程。多条前缀同时命中时,取更严的决定:
4
+ * allow < prompt < forbidden。
5
+ */
6
+ export type ExecDecisionName = 'allow' | 'prompt' | 'forbidden';
7
+ export interface PrefixRule {
8
+ argv: string[];
9
+ decision: ExecDecisionName;
10
+ justification?: string;
11
+ }
12
+ export interface ExecPolicy {
13
+ rules: PrefixRule[];
14
+ }
15
+ export interface ExecEvaluation {
16
+ decision: ExecDecisionName;
17
+ command: string[];
18
+ rule?: PrefixRule;
19
+ }
20
+ export interface SshEndpoint {
21
+ kind: 'ssh';
22
+ destination: string;
23
+ remoteCwd?: string;
24
+ }
25
+ export type ShellEndpoint = {
26
+ kind: 'local';
27
+ } | SshEndpoint;
28
+ export interface ShellRunSpec {
29
+ file: string;
30
+ args: string[];
31
+ cwd?: string;
32
+ env?: Record<string, string>;
33
+ pty: boolean;
34
+ transport: 'local' | 'ssh';
35
+ }
36
+ export interface ShellRunLimits {
37
+ timeoutMs: number;
38
+ maxOutputBytes: number;
39
+ gui: boolean;
40
+ }
41
+ export interface ShellRunOutcome {
42
+ exitCode: number;
43
+ stdout: string;
44
+ stderr: string;
45
+ truncated?: boolean;
46
+ timedOut?: boolean;
47
+ stillRunning?: boolean;
48
+ error?: string;
49
+ }
50
+ export interface ShellProcessBackend {
51
+ run(spec: ShellRunSpec, limits: ShellRunLimits): Promise<ShellRunOutcome>;
52
+ }
53
+ export declare function splitShellScript(script: string): string[][];
54
+ export declare function evaluateExecPolicy(script: string, policy: ExecPolicy): ExecEvaluation;
55
+ export declare function planSshArgv(input: {
56
+ command: string;
57
+ cwd?: string;
58
+ pty: boolean;
59
+ shell?: 'powershell';
60
+ endpoint: SshEndpoint;
61
+ }): ShellRunSpec;
62
+ export declare function shellQuote(value: string): string;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Shell 执行规划:前缀策略,以及本地 / SSH 的启动参数。
3
+ * 这里不启动进程。多条前缀同时命中时,取更严的决定:
4
+ * allow < prompt < forbidden。
5
+ */
6
+ const DECISION_RANK = {
7
+ allow: 0,
8
+ prompt: 1,
9
+ forbidden: 2,
10
+ };
11
+ export function splitShellScript(script) {
12
+ return splitSegments(script).map(tokenize).filter((argv) => argv.length > 0);
13
+ }
14
+ export function evaluateExecPolicy(script, policy) {
15
+ const commands = splitShellScript(script);
16
+ let best = { decision: 'allow', command: commands[0] ?? [] };
17
+ for (const command of commands) {
18
+ for (const rule of policy.rules) {
19
+ if (rule.argv.length === 0 || !isPrefix(rule.argv, command))
20
+ continue;
21
+ const stricter = DECISION_RANK[rule.decision] > DECISION_RANK[best.decision];
22
+ const sameDecisionLonger = rule.decision === best.decision && rule.argv.length > (best.rule?.argv.length ?? 0);
23
+ if (stricter || sameDecisionLonger) {
24
+ best = { decision: rule.decision, command, rule };
25
+ }
26
+ }
27
+ }
28
+ return best;
29
+ }
30
+ export function planSshArgv(input) {
31
+ const destination = input.endpoint.destination.trim();
32
+ if (!destination) {
33
+ throw new Error('ssh destination is required');
34
+ }
35
+ const cwd = input.cwd ?? input.endpoint.remoteCwd;
36
+ const script = remoteScript(input.command, cwd, input.shell);
37
+ const args = ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=15'];
38
+ if (input.pty)
39
+ args.push('-tt');
40
+ args.push(destination, '--');
41
+ if (input.shell === 'powershell') {
42
+ args.push('pwsh', '-NoProfile', '-NonInteractive', '-Command', script);
43
+ }
44
+ else {
45
+ args.push('bash', '-lc', script);
46
+ }
47
+ return { file: 'ssh', args, pty: input.pty, transport: 'ssh' };
48
+ }
49
+ export function shellQuote(value) {
50
+ return `'${value.replace(/'/g, `'\\''`)}'`;
51
+ }
52
+ function remoteScript(command, cwd, shell) {
53
+ if (!cwd)
54
+ return command;
55
+ if (shell === 'powershell')
56
+ return `Set-Location ${shellQuote(cwd)}; ${command}`;
57
+ return `cd ${shellQuote(cwd)} && ${command}`;
58
+ }
59
+ function isPrefix(prefix, argv) {
60
+ if (prefix.length > argv.length)
61
+ return false;
62
+ return prefix.every((token, index) => token === argv[index]);
63
+ }
64
+ function splitSegments(script) {
65
+ const segments = [];
66
+ let current = '';
67
+ let quote = null;
68
+ for (let i = 0; i < script.length; i += 1) {
69
+ const ch = script[i];
70
+ if (quote) {
71
+ current += ch;
72
+ if (ch === '\\' && quote === '"' && i + 1 < script.length) {
73
+ current += script[i + 1];
74
+ i += 1;
75
+ continue;
76
+ }
77
+ if (ch === quote)
78
+ quote = null;
79
+ continue;
80
+ }
81
+ if (ch === '\\' && i + 1 < script.length) {
82
+ current += ch + script[i + 1];
83
+ i += 1;
84
+ continue;
85
+ }
86
+ if (ch === '"' || ch === "'") {
87
+ quote = ch;
88
+ current += ch;
89
+ continue;
90
+ }
91
+ const two = script.slice(i, i + 2);
92
+ if (two === '&&' || two === '||' || two === '|&') {
93
+ segments.push(current);
94
+ current = '';
95
+ i += 1;
96
+ continue;
97
+ }
98
+ if (ch === ';' || ch === '|' || ch === '\n') {
99
+ segments.push(current);
100
+ current = '';
101
+ continue;
102
+ }
103
+ current += ch;
104
+ }
105
+ segments.push(current);
106
+ return segments.map((segment) => segment.trim()).filter((segment) => segment.length > 0);
107
+ }
108
+ function tokenize(segment) {
109
+ const tokens = [];
110
+ let current = '';
111
+ let quote = null;
112
+ for (let i = 0; i < segment.length; i += 1) {
113
+ const ch = segment[i];
114
+ if (quote) {
115
+ if (ch === '\\' && quote === '"' && i + 1 < segment.length) {
116
+ current += segment[i + 1];
117
+ i += 1;
118
+ continue;
119
+ }
120
+ if (ch === quote) {
121
+ quote = null;
122
+ continue;
123
+ }
124
+ current += ch;
125
+ continue;
126
+ }
127
+ if (ch === '\\' && i + 1 < segment.length) {
128
+ current += segment[i + 1];
129
+ i += 1;
130
+ continue;
131
+ }
132
+ if (ch === '"' || ch === "'") {
133
+ quote = ch;
134
+ continue;
135
+ }
136
+ if (ch === ' ' || ch === '\t') {
137
+ if (current)
138
+ tokens.push(current);
139
+ current = '';
140
+ continue;
141
+ }
142
+ current += ch;
143
+ }
144
+ if (current)
145
+ tokens.push(current);
146
+ return tokens;
147
+ }
@@ -0,0 +1,3 @@
1
+ import type { ShellProcessBackend } from './shell-exec.js';
2
+ export declare function stripAnsi(input: string): string;
3
+ export declare const nodeShellBackend: ShellProcessBackend;