odai-dsh-agent 0.2.39 → 0.2.51

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 (69) hide show
  1. package/README.md +13 -11
  2. package/build/src/control-center-installer.d.mts.map +1 -1
  3. package/build/src/control-center-installer.mjs +5 -0
  4. package/build/src/control-center-installer.mjs.map +1 -1
  5. package/build/src/installer.d.mts.map +1 -1
  6. package/build/src/installer.mjs +1 -1
  7. package/build/src/installer.mjs.map +1 -1
  8. package/client/client.js +27 -6
  9. package/package.json +1 -1
  10. package/preset/odai/package.json +2 -1
  11. package/preset/odai/runtime/compaction-config.d.mts +2 -2
  12. package/preset/odai/runtime/compaction-config.d.mts.map +1 -1
  13. package/preset/odai/runtime/context-activation.mjs +1 -1
  14. package/preset/odai/runtime/context-activation.mjs.map +1 -1
  15. package/preset/odai/runtime/index.mjs +1 -1
  16. package/preset/odai/runtime/index.mjs.map +1 -1
  17. package/preset/odai/runtime/lifecycle-runtime.d.mts.map +1 -1
  18. package/preset/odai/runtime/lifecycle-runtime.mjs +25 -4
  19. package/preset/odai/runtime/lifecycle-runtime.mjs.map +1 -1
  20. package/preset/odai/runtime/model-route.d.mts +5 -0
  21. package/preset/odai/runtime/model-route.d.mts.map +1 -1
  22. package/preset/odai/runtime/model-route.mjs +21 -2
  23. package/preset/odai/runtime/model-route.mjs.map +1 -1
  24. package/preset/odai/runtime/output-config.d.mts.map +1 -1
  25. package/preset/odai/runtime/output-config.mjs +2 -1
  26. package/preset/odai/runtime/output-config.mjs.map +1 -1
  27. package/preset/odai/runtime/role-overlays.d.mts +1 -1
  28. package/preset/odai/runtime/role-overlays.d.mts.map +1 -1
  29. package/preset/odai/runtime/role-overlays.mjs +4 -1
  30. package/preset/odai/runtime/role-overlays.mjs.map +1 -1
  31. package/preset/odai/runtime/router.d.mts +5 -1
  32. package/preset/odai/runtime/router.d.mts.map +1 -1
  33. package/preset/odai/runtime/router.mjs +47 -2
  34. package/preset/odai/runtime/router.mjs.map +1 -1
  35. package/preset/odai/runtime/routing-config.d.mts +5 -2
  36. package/preset/odai/runtime/routing-config.d.mts.map +1 -1
  37. package/preset/odai/runtime/routing-config.mjs +87 -42
  38. package/preset/odai/runtime/routing-config.mjs.map +1 -1
  39. package/preset/odai/runtime/runtime-config.d.mts +1 -1
  40. package/preset/odai/runtime/runtime-config.d.mts.map +1 -1
  41. package/preset/odai/runtime/runtime-config.mjs +15 -9
  42. package/preset/odai/runtime/runtime-config.mjs.map +1 -1
  43. package/preset/odai/runtime/runtime-support.d.mts +6 -1
  44. package/preset/odai/runtime/runtime-support.d.mts.map +1 -1
  45. package/preset/odai/runtime/runtime-support.mjs +90 -7
  46. package/preset/odai/runtime/runtime-support.mjs.map +1 -1
  47. package/preset/odai/runtime/runtime-types.d.mts +8 -1
  48. package/preset/odai/runtime/runtime-types.d.mts.map +1 -1
  49. package/preset/odai/runtime/runtime-types.mjs.map +1 -1
  50. package/preset/odai/runtime/skill-bundle.d.mts +1 -1
  51. package/preset/odai/runtime/skill-bundle.d.mts.map +1 -1
  52. package/preset/odai/runtime/work-policy.d.mts.map +1 -1
  53. package/preset/odai/runtime/work-policy.mjs +8 -2
  54. package/preset/odai/runtime/work-policy.mjs.map +1 -1
  55. package/preset/odai/skills/odai-orchestration/SKILL.md +1 -1
  56. package/preset/odai/skills/odai-orchestration/assets/codex-agents/role.toml +1 -0
  57. package/preset/odai/skills/odai-orchestration/assets/routing-roles/controller.md +1 -1
  58. package/preset/odai/skills/odai-orchestration/assets/routing-roles/creative.md +9 -0
  59. package/preset/odai/skills/odai-orchestration/manifest.json +9 -4
  60. package/preset/odai/skills/odai-orchestration/references/install.md +31 -1
  61. package/preset/odai/skills/odai-orchestration/references/orchestration.md +1 -0
  62. package/preset/odai/skills/odai-orchestration/scripts/build-routing.mjs +78 -17
  63. package/preset/odai/skills/odai-orchestration/scripts/compose-contracts.d.mts +1 -1
  64. package/preset/odai/skills/odai-orchestration/scripts/compose-contracts.mjs +2 -2
  65. package/preset/odai/skills/odai-orchestration/scripts/install-routing.mjs +101 -23
  66. package/preset/odai/skills/odai-orchestration/scripts/local-cli.d.mts +75 -0
  67. package/preset/odai/skills/odai-orchestration/scripts/local-cli.mjs +409 -0
  68. package/preset/odai/skills/odai-orchestration/scripts/run-role.mjs +190 -92
  69. package/preset/odai/skills/odai-orchestration/scripts/verify-routing.mjs +3 -3
@@ -20,12 +20,13 @@ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
20
20
  const builder = path.join(scriptDir, "build-routing.mjs");
21
21
  const manifestName = "odai-routing.json";
22
22
  const requiredRoles = ["controller", "planner", "reviewer"];
23
- const configurableRoles = [...requiredRoles, "researcher", "frontend"];
23
+ const configurableRoles = [...requiredRoles, "researcher", "frontend", "creative"];
24
24
  const args = parseArgs(process.argv.slice(2));
25
25
  const roles = Object.freeze([
26
26
  ...requiredRoles,
27
27
  ...(args.researcherModel ? ["researcher"] : []),
28
28
  ...(args.frontendModel ? ["frontend"] : []),
29
+ ...(args.creativeModel ? ["creative"] : []),
29
30
  ]);
30
31
  const retiredByHost = {
31
32
  codex: [
@@ -41,6 +42,12 @@ const retiredByHost = {
41
42
  if (args.help) { printHelp(); process.exit(0); }
42
43
  if (!new Set(["codex", "claude", "copilot"]).has(args.host)) fail(`不支持的 host:${args.host || "(missing)"}`);
43
44
  if (!new Set(["project", "user"]).has(args.scope)) fail(`不支持的 scope:${args.scope}`);
45
+ for (const role of configurableRoles) {
46
+ if (!args[`${role}Cli`]) continue;
47
+ if (args.host !== "claude") fail(`--${role}-cli is only supported with --host claude`);
48
+ if (!["codex", "antigravity", "grok"].includes(args[`${role}Cli`])) fail(`--${role}-cli must be codex, antigravity, or grok`);
49
+ if (!args[`${role}Model`]) fail(`--${role}-cli 需要同时提供 --${role}-model`);
50
+ }
44
51
  if (!args.yes) fail("安装、更新或卸载会持久化宿主配置;取得用户授权后传入 --yes");
45
52
 
46
53
  const targetRoot = resolveTarget(args.host, args.scope, args.target);
@@ -65,6 +72,7 @@ function main() {
65
72
  for (const role of requiredRoles) if (!args[`${role}Model`]) fail(`缺少 --${role}-model`);
66
73
  if (args.researcherEffort && !args.researcherModel) fail("--researcher-effort 需要同时提供 --researcher-model");
67
74
  if (args.frontendEffort && !args.frontendModel) fail("--frontend-effort 需要同时提供 --frontend-model");
75
+ if (args.creativeEffort && !args.creativeModel) fail("--creative-effort 需要同时提供 --creative-model");
68
76
  if (!existsSync(builder)) fail(`缺少路由生成器:${builder}`);
69
77
 
70
78
  const generatedRoot = mkdtempSync(path.join(tmpdir(), `odai-${args.host}-routing-install-`));
@@ -124,6 +132,7 @@ function buildAdapter(outputRoot, root) {
124
132
  for (const role of roles) {
125
133
  command.push(`--${role}-model`, args[`${role}Model`]);
126
134
  if (args[`${role}Effort`]) command.push(`--${role}-effort`, args[`${role}Effort`]);
135
+ if (args[`${role}Cli`]) command.push(`--${role}-cli`, args[`${role}Cli`]);
127
136
  }
128
137
  if (args.host === "codex") command.push("--verifier-command", `node ${JSON.stringify(path.join(root, "odai-verify-routing.mjs"))}`);
129
138
  const result = spawnSync(process.execPath, command, { encoding: "utf8", windowsHide: true });
@@ -138,7 +147,11 @@ function collectGeneratedFiles(root, layoutValue) {
138
147
  for (const [sourceRelative, targetRelative] of layoutValue.generatedFiles) {
139
148
  const source = path.join(root, args.host, sourceRelative);
140
149
  if (!existsSync(source)) fail(`生成结果缺少文件:${sourceRelative}`);
141
- result.set(targetRelative, readFileSync(source));
150
+ let content = readFileSync(source);
151
+ if (args.host === "claude" && args.scope === "user" && targetRelative === "agents/odai-controller.md") {
152
+ content = Buffer.from(content.toString("utf8").replaceAll("node .claude/odai-run-role.mjs", claudeRunnerCommand()));
153
+ }
154
+ result.set(targetRelative, content);
142
155
  }
143
156
  return result;
144
157
  }
@@ -163,7 +176,7 @@ function originalFileRecord(file) {
163
176
  }
164
177
 
165
178
  function mergeCodexConfig(original, generated) {
166
- if (/^\s*\[agents\.odai_(?:researcher|planner|executor|reviewer|frontend)\]\s*$/m.test(original)) {
179
+ if (/^\s*\[agents\.odai_(?:researcher|planner|executor|reviewer|frontend|creative)\]\s*$/m.test(original)) {
167
180
  fail("既有 Codex 配置已声明 odai 角色但不受当前安装清单管理");
168
181
  }
169
182
  const generatedFeatures = generated.search(/^\s*\[features\]\s*$/m);
@@ -245,6 +258,11 @@ function hostLayout(host, scope) {
245
258
  [".codex/role-contracts/odai-frontend.md", "role-contracts/odai-frontend.md"],
246
259
  ];
247
260
  const frontend = roles.includes("frontend") ? frontendFiles : [];
261
+ const creativeFiles = [
262
+ [".codex/agents/odai-creative.toml", "agents/odai-creative.toml"],
263
+ [".codex/role-contracts/odai-creative.md", "role-contracts/odai-creative.md"],
264
+ ];
265
+ const creative = roles.includes("creative") ? creativeFiles : [];
248
266
  const common = [
249
267
  [".codex/config.toml", "config.toml"],
250
268
  [".codex/odai-run-role.mjs", "odai-run-role.mjs"], [".codex/odai-verify-routing.mjs", "odai-verify-routing.mjs"],
@@ -255,21 +273,30 @@ function hostLayout(host, scope) {
255
273
  [".codex/role-contracts/odai-reviewer.md", "role-contracts/odai-reviewer.md"],
256
274
  ...researcher,
257
275
  ...frontend,
276
+ ...creative,
258
277
  ];
259
278
  const generatedFiles = common;
260
279
  return {
261
- knownFiles: [...new Set([...common, ...researcherFiles, ...frontendFiles].map(([, target]) => target)), "hooks.json", "odai-route-hook.mjs", "odai-run-routing.mjs", "agents/odai-executor.toml", "role-contracts/odai-executor.md"],
280
+ knownFiles: [...new Set([...common, ...researcherFiles, ...frontendFiles, ...creativeFiles].map(([, target]) => target)), "hooks.json", "odai-route-hook.mjs", "odai-run-routing.mjs", "agents/odai-executor.toml", "role-contracts/odai-executor.md"],
262
281
  managedFiles: generatedFiles.map(([, target]) => target),
263
282
  generatedFiles,
264
283
  settings: null,
265
284
  };
266
285
  }
267
- if (host === "claude") return {
268
- knownFiles: configurableRoles.map((role) => `agents/odai-${role}.md`),
269
- managedFiles: roles.map((role) => `agents/odai-${role}.md`),
270
- generatedFiles: roles.map((role) => [`.claude/agents/odai-${role}.md`, `agents/odai-${role}.md`]),
271
- settings: { file: scope === "project" ? "settings.local.json" : "settings.json", key: "agent", value: "odai-controller" },
272
- };
286
+ if (host === "claude") {
287
+ const executors = ["odai-run-role.mjs", "odai-local-cli.mjs"];
288
+ const local = roles.filter(role => args[`${role}Cli`]);
289
+ const managedFiles = roles.map(role => `${args[`${role}Cli`] ? "role-contracts" : "agents"}/odai-${role}.md`);
290
+ if (local.length) managedFiles.push(...executors);
291
+ return {
292
+ knownFiles: [...configurableRoles.map(role => `agents/odai-${role}.md`),
293
+ ...configurableRoles.filter(role => role !== "controller").map(role => `role-contracts/odai-${role}.md`), ...executors],
294
+ managedFiles,
295
+ generatedFiles: managedFiles.map(file => [`.claude/${file}`, file]),
296
+ settings: { file: scope === "project" ? "settings.local.json" : "settings.json", key: "agent", value: "odai-controller",
297
+ permission: local.length ? `Bash(${claudeRunnerCommand()}:*)` : null },
298
+ };
299
+ }
273
300
  return {
274
301
  knownFiles: configurableRoles.map((role) => `agents/odai-${role}.agent.md`),
275
302
  managedFiles: roles.map((role) => `agents/odai-${role}.agent.md`),
@@ -283,22 +310,67 @@ function planSettings(root, layoutValue, manifest) {
283
310
  const spec = layoutValue.settings;
284
311
  const file = path.join(root, spec.file);
285
312
  const current = readJsonObject(file);
313
+ let installed = manifest?.settings;
286
314
  if (manifest) {
287
- const installed = manifest.settings;
288
315
  if (!installed || installed.file !== spec.file || installed.key !== spec.key || installed.installed !== spec.value) fail("旧路由缺少可恢复的宿主设置记录");
289
316
  if (current[spec.key] !== spec.value) fail(`宿主设置 ${spec.key} 已被外部修改`);
290
- return { file: spec.file, value: current, manifest: installed };
317
+ restorePermission(current, installed.permission);
318
+ } else {
319
+ const present = Object.hasOwn(current, spec.key);
320
+ if (present && current[spec.key] !== spec.value) fail(`目标已有非 odai 管理的宿主设置 ${spec.key}`);
321
+ installed = { file: spec.file, key: spec.key, installed: spec.value, fileExistedBefore: existsSync(file), previous: { present, value: present ? current[spec.key] : null } };
291
322
  }
292
- const present = Object.hasOwn(current, spec.key);
293
- if (present && current[spec.key] !== spec.value) fail(`目标已有非 odai 管理的宿主设置 ${spec.key}`);
294
- return {
295
- file: spec.file,
296
- value: { ...current, [spec.key]: spec.value },
297
- manifest: { file: spec.file, key: spec.key, installed: spec.value, fileExistedBefore: existsSync(file), previous: { present, value: present ? current[spec.key] : null } },
298
- };
323
+ const record = { ...installed };
324
+ delete record.permission;
325
+ if (spec.permission) {
326
+ const permissionsPresent = Object.hasOwn(current, "permissions");
327
+ if (permissionsPresent && !isObject(current.permissions)) fail("宿主 permissions 必须是对象");
328
+ const permissions = current.permissions || {};
329
+ const allowPresent = Object.hasOwn(permissions, "allow");
330
+ if (allowPresent && (!Array.isArray(permissions.allow) || permissions.allow.some(rule => typeof rule !== "string"))) fail("宿主 permissions.allow 必须是字符串数组");
331
+ const allow = permissions.allow || [];
332
+ record.permission = { rule: spec.permission, added: !allow.includes(spec.permission), permissionsPresent, allowPresent };
333
+ current.permissions = { ...permissions, allow: record.permission.added ? [...allow, spec.permission] : [...allow] };
334
+ }
335
+ current[spec.key] = spec.value;
336
+ return { file: spec.file, value: current, manifest: record };
337
+ }
338
+
339
+ function isObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
340
+
341
+ function claudeRunnerCommand() {
342
+ return args.scope === "project" ? "node .claude/odai-run-role.mjs"
343
+ : `node '${path.join(configRoot, "odai-run-role.mjs").replaceAll("'", "'\\''")}'`;
344
+ }
345
+
346
+ function assertPermission(current, record) {
347
+ if (!record) return;
348
+ if (record.rule !== `Bash(${claudeRunnerCommand()}:*)` || typeof record.added !== "boolean"
349
+ || typeof record.permissionsPresent !== "boolean" || typeof record.allowPresent !== "boolean") fail("旧路由的权限恢复记录无效");
350
+ if (!isObject(current.permissions) || !Array.isArray(current.permissions.allow)
351
+ || !current.permissions.allow.includes(record.rule)) fail("宿主设置 permissions.allow 已被外部修改");
352
+ }
353
+
354
+ function restorePermission(current, record) {
355
+ if (!record) return;
356
+ assertPermission(current, record);
357
+ if (!record.added) return;
358
+ current.permissions.allow = current.permissions.allow.filter(rule => rule !== record.rule);
359
+ if (!record.allowPresent && current.permissions.allow.length === 0) delete current.permissions.allow;
360
+ if (!record.permissionsPresent && Object.keys(current.permissions).length === 0) delete current.permissions;
299
361
  }
300
362
 
301
363
  function assertManagedState(root, layoutValue, manifest) {
364
+ if (args.host === "claude") {
365
+ const localRoles = new Set([
366
+ ...roles.filter(role => args[`${role}Cli`]),
367
+ ...Object.entries(manifest?.mapping || {}).filter(([, route]) => route.backend === "local-cli").map(([role]) => role),
368
+ ]);
369
+ for (const role of configurableRoles.filter(role => localRoles.has(role))) {
370
+ const nativeFile = `agents/odai-${role}.md`;
371
+ if (existsSync(path.join(root, nativeFile)) && !manifest?.files?.[nativeFile]) fail(`本机 CLI 职责存在非 odai 托管的原生代理:${nativeFile}`);
372
+ }
373
+ }
302
374
  if (!manifest) {
303
375
  const mergeable = args.host === "codex" ? new Set(["config.toml"]) : new Set();
304
376
  const conflicts = layoutValue.managedFiles.filter((relative) => !mergeable.has(relative) && existsSync(path.join(root, relative)));
@@ -316,6 +388,7 @@ function assertManagedState(root, layoutValue, manifest) {
316
388
  || installedSetting.installed !== expectedSetting.value) {
317
389
  fail("旧路由缺少可恢复的宿主设置记录");
318
390
  }
391
+ assertPermission(readJsonObject(path.join(root, installedSetting.file)), installedSetting.permission);
319
392
  } else if (installedSetting) {
320
393
  fail("旧路由包含当前宿主不支持的设置记录");
321
394
  }
@@ -388,6 +461,7 @@ function uninstall(root, manifestPathValue, manifest) {
388
461
  function planSettingRestore(root, spec) {
389
462
  const current = readJsonObject(path.join(root, spec.file));
390
463
  if (current[spec.key] !== spec.installed) fail(`宿主设置 ${spec.key} 已被外部修改,拒绝删除`);
464
+ restorePermission(current, spec.permission);
391
465
  if (spec.previous?.present) current[spec.key] = spec.previous.value; else delete current[spec.key];
392
466
  return {
393
467
  relative: spec.file,
@@ -466,7 +540,10 @@ function parseArgs(values) {
466
540
  const result = { host: "", scope: "project", target: "", uninstall: false, yes: false, help: false };
467
541
  for (const role of configurableRoles) { result[`${role}Model`] = ""; result[`${role}Effort`] = ""; }
468
542
  const fields = new Map([["--host", "host"], ["--scope", "scope"], ["--target", "target"], ["--governance-root", "governanceRoot"]]);
469
- for (const role of configurableRoles) { fields.set(`--${role}-model`, `${role}Model`); fields.set(`--${role}-effort`, `${role}Effort`); }
543
+ for (const role of configurableRoles) {
544
+ fields.set(`--${role}-model`, `${role}Model`); fields.set(`--${role}-effort`, `${role}Effort`);
545
+ if (role !== "controller") fields.set(`--${role}-cli`, `${role}Cli`);
546
+ }
470
547
  for (let index = 0; index < values.length; index += 1) {
471
548
  const value = values[index];
472
549
  if (value === "--help" || value === "-h") result.help = true;
@@ -478,18 +555,19 @@ function parseArgs(values) {
478
555
  return result;
479
556
  }
480
557
 
481
- function hasMappingArgs(value) { return configurableRoles.some((role) => value[`${role}Model`] || value[`${role}Effort`]); }
558
+ function hasMappingArgs(value) { return configurableRoles.some((role) => value[`${role}Model`] || value[`${role}Effort`] || value[`${role}Cli`]); }
482
559
  function printHelp() {
483
560
  console.log(`由 odai 在取得用户授权后安装、更新或卸载宿主路由。
484
561
 
485
562
  Usage:
486
563
  node skills/odai-orchestration/scripts/install-routing.mjs --host <codex|claude|copilot> --scope <project|user> [--target <path>] \\
487
564
  --controller-model <model> --planner-model <model> --reviewer-model <model> \\
488
- [--researcher-model <model>] [--frontend-model <model>] [--<role>-effort <effort>] [--governance-root <path>] --yes
565
+ [--researcher-model <model>] [--frontend-model <model>] [--creative-model <model>] [--<role>-effort <effort>] [--governance-root <path>] --yes
566
+ Claude Code only: [--<role>-cli <codex|antigravity|grok>] (non-controller roles)
489
567
 
490
568
  node skills/odai-orchestration/scripts/install-routing.mjs --host <codex|claude|copilot> --scope <project|user> [--target <path>] --uninstall --yes
491
569
 
492
- 安装后用户只需正常使用 odai。总控是唯一持续任务线程并负责实施整合;planner、reviewer 以及可选 researcher、frontend 只在能改变结果时启动。researcher 与 frontend 映射默认不配置。更新会安全移除旧版 advisor、implementer、worker、executor 和 stage runner 托管文件。`);
570
+ 安装后用户只需正常使用 odai。总控是唯一持续任务线程并负责实施整合;planner、reviewer 以及可选 researcher、frontend、creative 只在能改变结果时启动。researcher、frontend 与 creative 映射默认不配置。更新会安全移除旧版 advisor、implementer、worker、executor 和 stage runner 托管文件。`);
493
571
  }
494
572
 
495
573
  function returnResult(value) { process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); }
@@ -0,0 +1,75 @@
1
+ import type { spawn } from "node:child_process";
2
+
3
+ export interface LocalCliRoute {
4
+ backend: "local-cli";
5
+ cli: "claude" | "codex" | "antigravity" | "grok";
6
+ model: string;
7
+ /** Validated by the selected CLI adapter. */
8
+ reasoningEffort?: string;
9
+ timeoutSeconds?: number;
10
+ provider?: never;
11
+ maxTokens?: never;
12
+ }
13
+
14
+ export interface LocalCliInvocation {
15
+ command: string;
16
+ args: string[];
17
+ }
18
+
19
+ export interface LocalCliParsedResult {
20
+ status: "completed" | "error";
21
+ text: string;
22
+ actualModel?: string;
23
+ modelVerified: boolean;
24
+ /** Unverified successful results stay completed so the host can choose its policy. */
25
+ modelVerification: "verified" | "mismatch" | "missing";
26
+ error?: string;
27
+ usage?: Record<string, unknown>;
28
+ /** Claude's reported API-equivalent cost; not evidence of a subscription charge. */
29
+ cliReportedCostUsd?: number;
30
+ reasoningEffortVerification: "verified" | "mismatch" | "requested-only";
31
+ actualReasoningEffort?: string;
32
+ sessionId?: string;
33
+ deniedActionsCount?: number;
34
+ }
35
+
36
+ export interface LocalCliExecutionResult extends Omit<LocalCliParsedResult, "status"> {
37
+ status: "completed" | "error" | "timeout" | "cancelled";
38
+ durationMs: number;
39
+ /** Actual process exit code, including zero on parsed-output failure; null if unavailable. */
40
+ exitCode: number | null;
41
+ }
42
+
43
+ export interface ExecuteLocalCliOptions {
44
+ route: LocalCliRoute;
45
+ prompt: string;
46
+ cwd: string;
47
+ signal?: AbortSignal;
48
+ env?: NodeJS.ProcessEnv;
49
+ /** Test seam. Injected spawns use child.kill unless killProcess is also supplied. */
50
+ spawnProcess?: typeof spawn;
51
+ /** POSIX signal seam, called with a negative process-group ID; defaults to process.kill for native spawn. */
52
+ killProcess?: (pid: number, signal: "SIGTERM" | "SIGKILL") => unknown;
53
+ /** Time between SIGTERM and SIGKILL, default 1000 milliseconds. */
54
+ killGraceMs?: number;
55
+ /** Maximum stdout bytes, default 16 MiB. Stderr retains only its last 8 KiB. */
56
+ maxOutputBytes?: number;
57
+ }
58
+
59
+ export interface LocalCliContext {
60
+ cwd?: string;
61
+ prompt?: string;
62
+ promptFile?: string;
63
+ env?: NodeJS.ProcessEnv;
64
+ }
65
+
66
+ /** Throws TypeError for invalid routes; preserves omitted timeoutSeconds. */
67
+ export function validateLocalCliRoute(value: unknown): LocalCliRoute;
68
+ export function buildLocalCliInvocation(route: LocalCliRoute, env?: NodeJS.ProcessEnv, context?: LocalCliContext): LocalCliInvocation;
69
+ export function parseLocalCliResult(stdout: string, requestedRoute: string | LocalCliRoute, context?: LocalCliContext): LocalCliParsedResult;
70
+ /**
71
+ * Input validation rejects; process, parse, timeout and cancellation failures return a result.
72
+ * Native POSIX execution terminates the entire process group. Windows only terminates
73
+ * the direct child; process-tree cleanup on Windows is not verified.
74
+ */
75
+ export function executeLocalCli(options: ExecuteLocalCliOptions): Promise<LocalCliExecutionResult>;