@sentropic/h2a-runtime 0.98.7-rc.1 → 0.98.7-rc.2

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 (92) hide show
  1. package/dist/agent-launch-args.d.ts.map +1 -1
  2. package/dist/agent-launch-args.js +5 -5
  3. package/dist/agent-launch-args.js.map +1 -1
  4. package/dist/agy-claude.d.ts +2 -0
  5. package/dist/agy-claude.d.ts.map +1 -0
  6. package/dist/agy-claude.js +3 -0
  7. package/dist/agy-claude.js.map +1 -0
  8. package/dist/agy-headless.d.ts +11 -0
  9. package/dist/agy-headless.d.ts.map +1 -0
  10. package/dist/agy-headless.js +104 -0
  11. package/dist/agy-headless.js.map +1 -0
  12. package/dist/claude-code-cli-isolation.d.ts +53 -0
  13. package/dist/claude-code-cli-isolation.d.ts.map +1 -0
  14. package/dist/claude-code-cli-isolation.js +277 -0
  15. package/dist/claude-code-cli-isolation.js.map +1 -0
  16. package/dist/claude-code-cli-lifecycle.d.ts +80 -0
  17. package/dist/claude-code-cli-lifecycle.d.ts.map +1 -0
  18. package/dist/claude-code-cli-lifecycle.js +402 -0
  19. package/dist/claude-code-cli-lifecycle.js.map +1 -0
  20. package/dist/claude-code-cli-runner.d.ts +89 -0
  21. package/dist/claude-code-cli-runner.d.ts.map +1 -0
  22. package/dist/claude-code-cli-runner.js +659 -0
  23. package/dist/claude-code-cli-runner.js.map +1 -0
  24. package/dist/claude-seat-input.d.ts +47 -0
  25. package/dist/claude-seat-input.d.ts.map +1 -0
  26. package/dist/claude-seat-input.js +272 -0
  27. package/dist/claude-seat-input.js.map +1 -0
  28. package/dist/claude-seat-mesh-contract.d.ts +88 -0
  29. package/dist/claude-seat-mesh-contract.d.ts.map +1 -0
  30. package/dist/claude-seat-mesh-contract.js +13 -0
  31. package/dist/claude-seat-mesh-contract.js.map +1 -0
  32. package/dist/delegate.d.ts +10 -0
  33. package/dist/delegate.d.ts.map +1 -1
  34. package/dist/delegate.js +29 -0
  35. package/dist/delegate.js.map +1 -1
  36. package/dist/gateway-host/claude-seat-client.d.ts +32 -0
  37. package/dist/gateway-host/claude-seat-client.d.ts.map +1 -0
  38. package/dist/gateway-host/claude-seat-client.js +38 -0
  39. package/dist/gateway-host/claude-seat-client.js.map +1 -0
  40. package/dist/gateway-host/host.d.ts +6 -0
  41. package/dist/gateway-host/host.d.ts.map +1 -1
  42. package/dist/gateway-host/host.js +24 -3
  43. package/dist/gateway-host/host.js.map +1 -1
  44. package/dist/gateway-host/live-declarations.d.ts.map +1 -1
  45. package/dist/gateway-host/live-declarations.js +13 -7
  46. package/dist/gateway-host/live-declarations.js.map +1 -1
  47. package/dist/gateway-host/live-github.d.ts +11 -0
  48. package/dist/gateway-host/live-github.d.ts.map +1 -0
  49. package/dist/gateway-host/live-github.js +374 -0
  50. package/dist/gateway-host/live-github.js.map +1 -0
  51. package/dist/gateway-host/live-object-store.d.ts +35 -0
  52. package/dist/gateway-host/live-object-store.d.ts.map +1 -0
  53. package/dist/gateway-host/live-object-store.js +447 -0
  54. package/dist/gateway-host/live-object-store.js.map +1 -0
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +211 -22
  57. package/dist/index.js.map +1 -1
  58. package/dist/llm-mesh-accounts.d.ts +70 -4
  59. package/dist/llm-mesh-accounts.d.ts.map +1 -1
  60. package/dist/llm-mesh-accounts.js +296 -9
  61. package/dist/llm-mesh-accounts.js.map +1 -1
  62. package/dist/native-host.d.ts +1 -1
  63. package/dist/native-host.d.ts.map +1 -1
  64. package/dist/native-host.js +3 -12
  65. package/dist/native-host.js.map +1 -1
  66. package/dist/native-terminal/host.d.ts +6 -1
  67. package/dist/native-terminal/host.d.ts.map +1 -1
  68. package/dist/native-terminal/host.js +6 -25
  69. package/dist/native-terminal/host.js.map +1 -1
  70. package/dist/proc-identity.d.ts +31 -0
  71. package/dist/proc-identity.d.ts.map +1 -1
  72. package/dist/proc-identity.js +54 -0
  73. package/dist/proc-identity.js.map +1 -1
  74. package/dist/prompt-delivery.d.ts +3 -0
  75. package/dist/prompt-delivery.d.ts.map +1 -1
  76. package/dist/prompt-delivery.js +8 -3
  77. package/dist/prompt-delivery.js.map +1 -1
  78. package/dist/tmux.d.ts +3 -1
  79. package/dist/tmux.d.ts.map +1 -1
  80. package/dist/tmux.js +13 -9
  81. package/dist/tmux.js.map +1 -1
  82. package/dist/vendor/engram-live/contracts.d.ts +619 -2
  83. package/dist/vendor/engram-live/contracts.js +1201 -25
  84. package/dist/vendor/engram-live/integration.d.ts +44 -4
  85. package/dist/vendor/engram-live/integration.js +65 -5
  86. package/dist/vendor/engram-live/pr-sync.d.ts +144 -0
  87. package/dist/vendor/engram-live/pr-sync.js +411 -0
  88. package/dist/vendor/engram-live/registration.d.ts +2 -2
  89. package/dist/vendor/engram-live/registration.js +2 -4
  90. package/dist/vendor/engram-live/retention.d.ts +56 -0
  91. package/dist/vendor/engram-live/retention.js +179 -0
  92. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { spawnSync } from "node:child_process";
4
- import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from "node:fs";
4
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, } from "node:fs";
5
5
  import { homedir, tmpdir } from "node:os";
6
6
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
8
  import { Command, Help } from "commander";
9
+ import { agyClaudeQuotaError, assertAgyClaudeBuildAllowed, detectAgyClaudeQuotaExhaustion, isAgyClaudeModel, normalizeAgyEffort, readAgyClaudeQuotaStatus, resolveAgyModel, writeAgyClaudeQuotaStatus } from "./agy-claude.js";
9
10
  import { applyRuntimeHelpGroups, groupRuntimeHelpItems, } from "./cli-help-groups.js";
10
11
  import { attach, createRemoteSession, getRemoteSession, listRemoteSessions, refreshRemoteSession, renameRemoteSession, sessionTerminalHealth, stopRemoteSession, } from "./attach.js";
11
12
  import { authHeaders, clearDefaultRemote, getDefaultRemote, getDefaultTarget, getDefaultTools, getH2aConfig, getJobMaxAgeHours, getMaxConcurrent, getLlmMeshRuntimeConfig, getTmuxProfileConfig, getTunnel, resolveConfigPath, setDefaultRemote, setDefaultTarget, migrateConfigHomeIfNeeded, setDefaultTools, setLlmMeshRuntimeConfig, setTmuxProfileConfig, setToken, setTunnel, } from "./config.js";
@@ -1436,7 +1437,7 @@ async function injectLlmMeshGatewayEnv(mode = "auto", allowDirectFallback = true
1436
1437
  delete process.env.ANTHROPIC_BASE_URL;
1437
1438
  delete process.env.ANTHROPIC_AUTH_TOKEN;
1438
1439
  if (mode === "gateway") {
1439
- throw new Error("llm-mesh gateway is required but unavailable; no agent was started. Run `h2a llm-mesh start` or `h2a llm-mesh account enroll codex|cloud-code|muse` first");
1440
+ throw new Error("llm-mesh gateway is required but unavailable; no agent was started. Run `h2a llm-mesh start` or `h2a llm-mesh account enroll codex|cloud-code|muse|claude-code` first");
1440
1441
  }
1441
1442
  if (allowDirectFallback) {
1442
1443
  process.stderr.write("[h2a] llm-mesh: gateway unavailable; continuing with native authentication.\n");
@@ -1518,7 +1519,7 @@ function renderNativeRestartResult(result, json) {
1518
1519
  export async function prepareStructuredGateway(mode, inject = (selectedMode) => injectLlmMeshGatewayEnv(selectedMode, selectedMode !== "gateway")) {
1519
1520
  const gateway = await inject(mode);
1520
1521
  if (mode === "gateway" && !gateway) {
1521
- throw new Error("llm-mesh gateway is required but unavailable; no agent was started. Run `h2a llm-mesh start` or `h2a llm-mesh account enroll codex|cloud-code|muse` first");
1522
+ throw new Error("llm-mesh gateway is required but unavailable; no agent was started. Run `h2a llm-mesh start` or `h2a llm-mesh account enroll codex|cloud-code|muse|claude-code` first");
1522
1523
  }
1523
1524
  return gateway;
1524
1525
  }
@@ -1720,6 +1721,40 @@ function setAndReportDefaultRemote(url) {
1720
1721
  ensureManagedTmuxProfile(getTmuxProfileConfig().profile);
1721
1722
  process.stderr.write(`[h2a] default remote set to ${configured}\n`);
1722
1723
  }
1724
+ // Root commander error boundary (DEC-119): commander never prints its own
1725
+ // error text. `outputError` is a no-op so the commander-built message (which
1726
+ // echoes argv, possibly multi-line) is dropped; `exitOverride` writes a
1727
+ // static message chosen ONLY from `err.code` — never `err.message`, never
1728
+ // user-typed text. Registered on the ROOT program before any `.command()`
1729
+ // so the whole tree inherits it via `copyInheritedSettings`.
1730
+ function staticCommanderErrorForCode(code) {
1731
+ switch (code) {
1732
+ case "commander.unknownOption":
1733
+ return "error: unknown option\n";
1734
+ case "commander.unknownCommand":
1735
+ return "error: unknown command\n";
1736
+ case "commander.excessArguments":
1737
+ return "error: too many arguments\n";
1738
+ case "commander.missingArgument":
1739
+ return "error: missing required argument\n";
1740
+ case "commander.optionMissingArgument":
1741
+ return "error: option argument missing\n";
1742
+ case "commander.missingMandatoryOptionValue":
1743
+ return "error: required option not specified\n";
1744
+ case "commander.conflictingOption":
1745
+ return "error: conflicting option\n";
1746
+ case "commander.invalidArgument":
1747
+ return "error: invalid argument\n";
1748
+ default:
1749
+ return "error: command error\n";
1750
+ }
1751
+ }
1752
+ function isCommanderNonErrorCode(code) {
1753
+ return (code === "commander.help" ||
1754
+ code === "commander.helpDisplayed" ||
1755
+ code === "commander.version" ||
1756
+ code === "commander.executeSubCommandAsync");
1757
+ }
1723
1758
  export async function main(argv) {
1724
1759
  // Passive fatal-path witness on the real entry path (see
1725
1760
  // installCliWitnessOnce): observes via uncaughtExceptionMonitor, so the
@@ -1773,6 +1808,25 @@ export async function main(argv) {
1773
1808
  "the separate remote and track CLIs. The heavy session runtime loads on " +
1774
1809
  "demand. h2a runs and coordinates agents; it is not itself an agent.")
1775
1810
  .version("0.0.0");
1811
+ // Root error boundary (DEC-119): commander never prints its own error
1812
+ // text. `outputError` is a no-op (commander message dropped);
1813
+ // `exitOverride` writes the static per-code message. Registered here —
1814
+ // before any `.command()` — so the whole tree inherits it through
1815
+ // `copyInheritedSettings`.
1816
+ program.configureOutput({
1817
+ writeErr: (str) => {
1818
+ process.stderr.write(str);
1819
+ },
1820
+ outputError: () => { },
1821
+ });
1822
+ program.showHelpAfterError(false);
1823
+ program.showSuggestionAfterError(false);
1824
+ program.exitOverride((err) => {
1825
+ if (isCommanderNonErrorCode(err.code))
1826
+ throw err;
1827
+ process.stderr.write(staticCommanderErrorForCode(err.code));
1828
+ throw err;
1829
+ });
1776
1830
  const identityCommand = program
1777
1831
  .command("identity")
1778
1832
  .description("Inspect identity bindings or unlock a token-fenced identity lock");
@@ -3997,8 +4051,11 @@ export async function main(argv) {
3997
4051
  .option("--prompt-stdin", "read the initial Claude/Codex/AGY prompt from stdin (keeps it out of process argv)")
3998
4052
  .option("--agent <agent>", "AGY agent override (for example: stp)")
3999
4053
  .option("--model <model>", "Claude/Codex/AGY model override")
4000
- .option("--effort <level>", "reasoning effort: low|medium|high|xhigh (AGY supports low|medium|high)")
4001
- .option("--headless", "run once and exit (Claude -p / Codex exec / AGY --print), recording output under .h2a/runs/<name>")
4054
+ .option("--role <role>", "launch purpose: review|challenge|build; AGY Claude builds require explicit opt-in")
4055
+ .option("--allow-claude-build", "explicitly spend the reserved AGY Claude pool on a build")
4056
+ .option("--output-file <path>", "expected AGY Claude headless artifact; report missing or unchanged output at exit")
4057
+ .option("--effort <level>", "reasoning effort: low|medium|high|xhigh (AGY: low|medium|high, max maps to high)")
4058
+ .option("--headless", "run once and exit (Claude -p / Codex exec / AGY stream-json), recording output under .h2a/runs/<name>")
4002
4059
  .option("--background", "classify this detached session as a background worker excluded from human restore")
4003
4060
  .option("--json", "emit one machine-readable h2a.run.result object")
4004
4061
  .option("--llm-gateway", "launch the CLI through the local llm-mesh gateway")
@@ -4020,13 +4077,30 @@ export async function main(argv) {
4020
4077
  process.exitCode = 2;
4021
4078
  return;
4022
4079
  }
4023
- if (opts.effort !== undefined && !isAgentLaunchEffort(opts.effort)) {
4024
- process.stderr.write(`[h2a] --effort must be low|medium|high|xhigh\n`);
4080
+ try {
4081
+ if (profile === "agy") {
4082
+ const effort = normalizeAgyEffort(opts.effort);
4083
+ if (effort !== undefined)
4084
+ opts.effort = effort;
4085
+ const model = resolveAgyModel(opts.model, effort);
4086
+ if (model !== undefined)
4087
+ opts.model = model;
4088
+ }
4089
+ assertAgyClaudeBuildAllowed({ profile, model: opts.model, role: opts.role, allowClaudeBuild: opts.allowClaudeBuild });
4090
+ if (opts.outputFile && !(profile === "agy" && isAgyClaudeModel(opts.model) && opts.headless)) {
4091
+ throw new Error("--output-file requires an AGY Claude headless run");
4092
+ }
4093
+ }
4094
+ catch (error) {
4095
+ process.stderr.write(`[h2a] ${error.message}\n`);
4096
+ if (opts.json)
4097
+ process.stdout.write(`${JSON.stringify({ kind: "h2a.run.failure", version: 1, state: "not-started",
4098
+ code: error.code ?? "invalid-agy-launch", error: error.message, retrySafe: false })}\n`);
4025
4099
  process.exitCode = 2;
4026
4100
  return;
4027
4101
  }
4028
- if (profile === "agy" && opts.effort === "xhigh") {
4029
- process.stderr.write("[h2a] AGY --effort must be low|medium|high\n");
4102
+ if (opts.effort !== undefined && !isAgentLaunchEffort(opts.effort)) {
4103
+ process.stderr.write(`[h2a] --effort must be low|medium|high|xhigh\n`);
4030
4104
  process.exitCode = 2;
4031
4105
  return;
4032
4106
  }
@@ -4389,6 +4463,10 @@ export async function main(argv) {
4389
4463
  mkdirSync(runDir, { recursive: true });
4390
4464
  outputLog = join(runDir, "output.log");
4391
4465
  resultJson = join(runDir, "result.json");
4466
+ // Name admission above has completed. Retire the previous result
4467
+ // before returning a path or starting the new run-once supervisor.
4468
+ if (profile === "agy" && isAgyClaudeModel(opts.model))
4469
+ rmSync(resultJson, { force: true });
4392
4470
  const headlessPromptInput = structuredLaunch && isAgentLaunchProfile(profile)
4393
4471
  ? buildAgentLaunchStdin({
4394
4472
  profile,
@@ -4402,10 +4480,10 @@ export async function main(argv) {
4402
4480
  ({ name, slug, promptFile } = startNativeHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, headlessPromptInput, structuredLaunch, sessionClass, onCreateAttempt, structuredLaunch ? (owned) => {
4403
4481
  launchOwnership = { host: "native", sessions: [owned] };
4404
4482
  launchGuard = startLaunchGuard(runDir, launchOwnership);
4405
- } : undefined));
4483
+ } : undefined, opts.outputFile ? resolve(cwd, opts.outputFile) : undefined));
4406
4484
  }
4407
4485
  else {
4408
- ({ name, slug, agentPane, promptFile } = startHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, getTmuxProfileConfig().profile, headlessPromptInput, structuredLaunch, sessionClass, onCreateAttempt));
4486
+ ({ name, slug, agentPane, promptFile } = startHeadlessSession(profile, command, cwd, args, resultJson, outputLog, label, getTmuxProfileConfig().profile, headlessPromptInput, structuredLaunch, sessionClass, onCreateAttempt, opts.outputFile ? resolve(cwd, opts.outputFile) : undefined));
4409
4487
  }
4410
4488
  }
4411
4489
  else if (sessionHost === "native") {
@@ -4538,6 +4616,18 @@ export async function main(argv) {
4538
4616
  now: () => Date.now(),
4539
4617
  }, { profile });
4540
4618
  if (promptDelivery.state !== "working") {
4619
+ const quotaDiagnostic = promptDelivery.state === "submitted-idle" ? "" : `${promptDelivery.reason}\n${promptDelivery.capture ?? ""}`;
4620
+ const quotaVerdict = profile === "agy" && isAgyClaudeModel(opts.model)
4621
+ ? detectAgyClaudeQuotaExhaustion(quotaDiagnostic) : { exhausted: false };
4622
+ const quotaFailure = profile === "agy" && isAgyClaudeModel(opts.model) &&
4623
+ promptDelivery.state === "provider-blocked" && promptDelivery.providerError
4624
+ ? promptDelivery.providerError : quotaVerdict.exhausted
4625
+ ? agyClaudeQuotaError(quotaVerdict.signature, quotaDiagnostic) : undefined;
4626
+ if (quotaFailure)
4627
+ try {
4628
+ writeAgyClaudeQuotaStatus(quotaFailure.quota);
4629
+ }
4630
+ catch { /* Response still carries the observation. */ }
4541
4631
  cleanupHeadlessPromptFile(promptFile);
4542
4632
  // Only claim the session is gone when the kill actually reported
4543
4633
  // success. "nothing is left running" over an unchecked call is the
@@ -4567,15 +4657,47 @@ export async function main(argv) {
4567
4657
  ? `[h2a] last screen:\n${promptDelivery.capture}\n`
4568
4658
  : ""));
4569
4659
  process.exitCode = 1;
4570
- if (opts.json && promptDelivery.state === "provider-blocked") {
4660
+ if (opts.json && (promptDelivery.state === "provider-blocked" || quotaFailure)) {
4571
4661
  process.stdout.write(`${JSON.stringify({ kind: "h2a.run.failure", version: 1,
4572
- state: "provider-blocked", launchId: slug, error: promptDelivery.reason,
4573
- stopped, retrySafe: false, prompt: { delivered: true, waitedMs: promptDelivery.waitedMs },
4662
+ state: "provider-blocked", launchId: slug, error: quotaFailure?.message ?? (promptDelivery.state === "submitted-idle" ? "agent idle after submission" : promptDelivery.reason),
4663
+ stopped, retrySafe: false, prompt: { delivered: promptDelivery.state === "provider-blocked", ...("waitedMs" in promptDelivery ? { waitedMs: promptDelivery.waitedMs } : {}) },
4664
+ ...(quotaFailure ? { code: quotaFailure.code, error: quotaFailure.message, quota: quotaFailure.quota,
4665
+ resetsAt: quotaFailure.resetsAt, retryable: false, model: opts.model } : {}),
4574
4666
  })}\n`);
4575
4667
  }
4576
4668
  return;
4577
4669
  }
4578
4670
  }
4671
+ const reportAgyCompletion = () => {
4672
+ if (!(profile === "agy" && isAgyClaudeModel(opts.model) && opts.headless && resultJson))
4673
+ return false;
4674
+ try {
4675
+ const completed = JSON.parse(readFileSync(resultJson, "utf8"));
4676
+ // The previous result was removed before this supervisor started.
4677
+ // Filesystem and wall-clock timestamps cannot prove generation.
4678
+ if (!["h2a.run.failure", "h2a.run.completion"].includes(completed.kind))
4679
+ return false;
4680
+ launchGuard?.complete();
4681
+ if (opts.json)
4682
+ process.stdout.write(`${JSON.stringify({ ...completed, launchId: slug, result: { path: resultJson }, logs: { path: outputLog } })}\n`);
4683
+ else
4684
+ process.stderr.write(`[h2a] ${completed.error ?? "AGY run completed"}\n`);
4685
+ process.exitCode = completed.state === "done" ? 0 : 1;
4686
+ return true;
4687
+ }
4688
+ catch {
4689
+ return false;
4690
+ } // Still running; the durable result may appear later.
4691
+ };
4692
+ if (profile === "agy" && isAgyClaudeModel(opts.model) && opts.headless && resultJson) {
4693
+ // A fast provider refusal may finish before the host PID probe.
4694
+ // Return that typed completion rather than a misleading PID error.
4695
+ for (let attempt = 0; attempt < 5; attempt++) {
4696
+ if (reportAgyCompletion())
4697
+ return;
4698
+ sleepSync(50);
4699
+ }
4700
+ }
4579
4701
  const pid = sessionHost === "native"
4580
4702
  ? nativeSessionPid(name)
4581
4703
  : agentPane
@@ -4587,6 +4709,9 @@ export async function main(argv) {
4587
4709
  ? paneWorkerPid(agentPane)
4588
4710
  : undefined;
4589
4711
  if (structuredLaunch && pid === undefined) {
4712
+ // The supervisor can publish and exit DURING the PID probe.
4713
+ if (reportAgyCompletion())
4714
+ return;
4590
4715
  cleanupHeadlessPromptFile(promptFile);
4591
4716
  // OWN-CLEANUP, deliberately NOT resolver-derived (F1 sorting):
4592
4717
  // `name`/`sessionHost` are the session and host THIS command
@@ -4656,6 +4781,8 @@ export async function main(argv) {
4656
4781
  tmuxSession: only.name,
4657
4782
  host: sessionHost === "native" ? "native" : "tmux",
4658
4783
  profile,
4784
+ ...(opts.model ? { model: opts.model, modelSource: "launch-argument", effectiveModel: "unknown" } : {}),
4785
+ ...(profile === "agy" && isAgyClaudeModel(opts.model) ? { quota: readAgyClaudeQuotaStatus() } : {}),
4659
4786
  workspace: cwd,
4660
4787
  mode: opts.headless ? "headless" : "interactive",
4661
4788
  background: opts.background === true,
@@ -7531,11 +7658,26 @@ export async function main(argv) {
7531
7658
  llmMeshAccountCommand
7532
7659
  .command("enroll <provider>")
7533
7660
  .description("Enroll through the sentropic-owned OAuth state machine " +
7534
- "(cloud-code, codex, muse OAuth device flow, or mistral-vibe browser sign-in — alias: vibe). " +
7535
- "--cli imports credentials from the provider's local CLI store instead (muse only)")
7661
+ "(cloud-code, codex, muse OAuth device flow, mistral-vibe browser sign-in — alias: vibe, or claude-code). " +
7662
+ "--from oauth|cli|token selects the source (default oauth): cli imports from the provider's official CLI store (muse, claude-code), " +
7663
+ "token imports a serialized credential from a secret manager via stdin pipe or masked prompt (claude-code only). " +
7664
+ "--cli / --token are the same modes as booleans")
7536
7665
  .option("--config-ref <ref>", "sentropic configuration reference for OAuth")
7537
- .option("--cli", "import credentials from the provider's local CLI store (muse only) instead of the OAuth flow")
7538
- .action(async (provider, opts) => {
7666
+ .option("--from <mode>", "enrollment source: oauth (default), cli (provider official CLI store) or token (secret-manager credential)")
7667
+ .option("--cli", "import credentials from the provider's local CLI store (muse, claude-code) instead of the OAuth flow. Takes no value")
7668
+ .option("--token", "import a serialized credential from a secret manager into llm-mesh (claude-code only): pipe it on stdin, or enter it at the masked prompt. Takes no value; never argv, never logged")
7669
+ .allowExcessArguments(true)
7670
+ .action(async (provider, opts, command) => {
7671
+ // Static excess-argument refusal (m1): commander would otherwise echo
7672
+ // every positional value (including a secret) on stderr before the
7673
+ // action runs. Excess positionals are refused here without echoing
7674
+ // any value. Token material travels on stdin only, never argv.
7675
+ const positionalCount = Array.isArray(command?.args) ? command.args.length : 1;
7676
+ if (positionalCount > 1) {
7677
+ process.stderr.write("[h2a] llm-mesh account: too many arguments (expected provider only; never pass secrets in argv)\n");
7678
+ process.exitCode = 1;
7679
+ return;
7680
+ }
7539
7681
  // "vibe" is the CLI-friendly alias of the mistral-vibe transport.
7540
7682
  const normalized = provider === "vibe" ? "mistral-vibe" : provider;
7541
7683
  if (normalized === "muse-code") {
@@ -7547,19 +7689,48 @@ export async function main(argv) {
7547
7689
  process.exitCode = 1;
7548
7690
  return;
7549
7691
  }
7692
+ if (opts.from !== undefined && opts.from !== "oauth" && opts.from !== "cli" && opts.from !== "token") {
7693
+ process.stderr.write("[h2a] llm-mesh account: --from must be one of oauth, cli, token\n");
7694
+ process.exitCode = 1;
7695
+ return;
7696
+ }
7697
+ if (opts.from !== undefined && (opts.cli || opts.token)) {
7698
+ process.stderr.write("[h2a] llm-mesh account: --from cannot be combined with --cli or --token\n");
7699
+ process.exitCode = 1;
7700
+ return;
7701
+ }
7702
+ if (opts.cli && opts.token) {
7703
+ process.stderr.write("[h2a] llm-mesh account: --cli and --token cannot be combined\n");
7704
+ process.exitCode = 1;
7705
+ return;
7706
+ }
7707
+ if ((opts.cli || opts.from === "cli") && normalized !== "muse" && normalized !== "claude-code") {
7708
+ process.stderr.write("[h2a] llm-mesh account: provider has no CLI-store import; " +
7709
+ "--cli/--from cli is only valid for providers with a local CLI import (muse, claude-code)\n");
7710
+ process.exitCode = 1;
7711
+ return;
7712
+ }
7713
+ if ((opts.token || opts.from === "token") && normalized !== "claude-code") {
7714
+ process.stderr.write(`[h2a] llm-mesh account: --token/--from token is only valid for claude-code\n`);
7715
+ process.exitCode = 1;
7716
+ return;
7717
+ }
7550
7718
  if (normalized !== "cloud-code" &&
7551
7719
  normalized !== "codex" &&
7552
7720
  normalized !== "muse" &&
7553
- normalized !== "mistral-vibe") {
7554
- process.stderr.write(`[h2a] llm-mesh account: unsupported provider "${provider}". ` +
7555
- "Supported: cloud-code, codex, muse (OAuth; --cli imports the CLI store), mistral-vibe (alias: vibe)\n");
7721
+ normalized !== "mistral-vibe" &&
7722
+ normalized !== "claude-code") {
7723
+ process.stderr.write("[h2a] llm-mesh account: unsupported provider. " +
7724
+ "Supported: cloud-code, codex, muse (OAuth; --cli imports the CLI store), mistral-vibe (alias: vibe), claude-code (OAuth; --cli imports the CLI store; --token imports a secret-manager credential)\n");
7556
7725
  process.exitCode = 1;
7557
7726
  return;
7558
7727
  }
7559
7728
  try {
7560
7729
  const account = await enrollViaFacade(normalized, {
7561
7730
  ...(opts.configRef ? { configRef: opts.configRef } : {}),
7731
+ ...(opts.from ? { from: opts.from } : {}),
7562
7732
  ...(opts.cli ? { cliImport: true } : {}),
7733
+ ...(opts.token ? { tokenImport: true } : {}),
7563
7734
  });
7564
7735
  process.stdout.write(`[h2a] llm-mesh account: enrolled ${account.provider}; ` +
7565
7736
  "account inventory remains in Sentropic\n");
@@ -7897,7 +8068,25 @@ export async function main(argv) {
7897
8068
  applyRuntimeHelpGroups(program.createHelp().visibleCommands(program));
7898
8069
  program.configureHelp({ groupItems: groupRuntimeHelpItems });
7899
8070
  program.addHelpText("after", "\nGrouped map of every h2a command (core + runtime): h2a explain\n");
7900
- await program.parseAsync([...argv]);
8071
+ try {
8072
+ await program.parseAsync([...argv]);
8073
+ }
8074
+ catch (err) {
8075
+ // Commander errors already wrote their static per-code message in
8076
+ // `exitOverride`; only set the exit code here, never `err.message`.
8077
+ if (err !== null &&
8078
+ typeof err === "object" &&
8079
+ "code" in err &&
8080
+ typeof err.code === "string") {
8081
+ const code = err.code;
8082
+ if (code.startsWith("commander.")) {
8083
+ const exitCode = err.exitCode;
8084
+ process.exitCode = typeof exitCode === "number" ? exitCode : 1;
8085
+ return process.exitCode;
8086
+ }
8087
+ }
8088
+ throw err;
8089
+ }
7901
8090
  const code = process.exitCode;
7902
8091
  return typeof code === "number" ? code : 0;
7903
8092
  }