@dungle-scrubs/harness-cli-normalizer 0.1.2 → 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 (151) hide show
  1. package/README.md +144 -7
  2. package/dist/cli/args.d.ts +74 -0
  3. package/dist/cli/args.d.ts.map +1 -0
  4. package/dist/cli/args.js +339 -0
  5. package/dist/cli/args.js.map +1 -0
  6. package/dist/cli/check.d.ts +2 -0
  7. package/dist/cli/check.d.ts.map +1 -0
  8. package/dist/cli/check.js +110 -0
  9. package/dist/cli/check.js.map +1 -0
  10. package/dist/cli/exit-codes.d.ts +7 -0
  11. package/dist/cli/exit-codes.d.ts.map +1 -0
  12. package/dist/cli/exit-codes.js +11 -0
  13. package/dist/cli/exit-codes.js.map +1 -0
  14. package/dist/cli/help.d.ts +7 -0
  15. package/dist/cli/help.d.ts.map +1 -0
  16. package/dist/cli/help.js +110 -0
  17. package/dist/cli/help.js.map +1 -0
  18. package/dist/cli/index.d.ts +4 -0
  19. package/dist/cli/index.d.ts.map +1 -0
  20. package/dist/cli/index.js +170 -0
  21. package/dist/cli/index.js.map +1 -0
  22. package/dist/cli/inspect.d.ts +3 -0
  23. package/dist/cli/inspect.d.ts.map +1 -0
  24. package/dist/cli/inspect.js +170 -0
  25. package/dist/cli/inspect.js.map +1 -0
  26. package/dist/cli/ls.d.ts +2 -0
  27. package/dist/cli/ls.d.ts.map +1 -0
  28. package/dist/cli/ls.js +10 -0
  29. package/dist/cli/ls.js.map +1 -0
  30. package/dist/cli/render.d.ts +8 -0
  31. package/dist/cli/render.d.ts.map +1 -0
  32. package/dist/cli/render.js +55 -0
  33. package/dist/cli/render.js.map +1 -0
  34. package/dist/cli/resolve-harness.d.ts +4 -0
  35. package/dist/cli/resolve-harness.d.ts.map +1 -0
  36. package/dist/cli/resolve-harness.js +31 -0
  37. package/dist/cli/resolve-harness.js.map +1 -0
  38. package/dist/cli/run.d.ts +2 -0
  39. package/dist/cli/run.d.ts.map +1 -0
  40. package/dist/cli/run.js +280 -0
  41. package/dist/cli/run.js.map +1 -0
  42. package/dist/cli/session.d.ts +2 -0
  43. package/dist/cli/session.d.ts.map +1 -0
  44. package/dist/cli/session.js +128 -0
  45. package/dist/cli/session.js.map +1 -0
  46. package/dist/cli/version.d.ts +2 -0
  47. package/dist/cli/version.d.ts.map +1 -0
  48. package/dist/cli/version.js +34 -0
  49. package/dist/cli/version.js.map +1 -0
  50. package/dist/cli.js +2 -0
  51. package/dist/execution/decode.d.ts.map +1 -1
  52. package/dist/execution/decode.js +32 -0
  53. package/dist/execution/decode.js.map +1 -1
  54. package/dist/execution/deps.d.ts +7 -1
  55. package/dist/execution/deps.d.ts.map +1 -1
  56. package/dist/execution/events.d.ts +6 -2
  57. package/dist/execution/events.d.ts.map +1 -1
  58. package/dist/execution/events.js.map +1 -1
  59. package/dist/execution/failure.d.ts +45 -0
  60. package/dist/execution/failure.d.ts.map +1 -0
  61. package/dist/execution/failure.js +121 -0
  62. package/dist/execution/failure.js.map +1 -0
  63. package/dist/execution/node-deps.d.ts.map +1 -1
  64. package/dist/execution/node-deps.js +19 -0
  65. package/dist/execution/node-deps.js.map +1 -1
  66. package/dist/execution/stream-turn.d.ts +2 -0
  67. package/dist/execution/stream-turn.d.ts.map +1 -1
  68. package/dist/execution/stream-turn.js +181 -16
  69. package/dist/execution/stream-turn.js.map +1 -1
  70. package/dist/interpretation/argv.d.ts +16 -4
  71. package/dist/interpretation/argv.d.ts.map +1 -1
  72. package/dist/interpretation/argv.js +68 -19
  73. package/dist/interpretation/argv.js.map +1 -1
  74. package/dist/interpretation/dimensions.d.ts +0 -2
  75. package/dist/interpretation/dimensions.d.ts.map +1 -1
  76. package/dist/interpretation/dimensions.js +0 -2
  77. package/dist/interpretation/dimensions.js.map +1 -1
  78. package/dist/interpretation/limits.d.ts +10 -1
  79. package/dist/interpretation/limits.d.ts.map +1 -1
  80. package/dist/interpretation/limits.js +84 -5
  81. package/dist/interpretation/limits.js.map +1 -1
  82. package/dist/interpretation/refusal.d.ts +31 -0
  83. package/dist/interpretation/refusal.d.ts.map +1 -0
  84. package/dist/interpretation/refusal.js +87 -0
  85. package/dist/interpretation/refusal.js.map +1 -0
  86. package/dist/interpretation/turn-options.d.ts +18 -0
  87. package/dist/interpretation/turn-options.d.ts.map +1 -0
  88. package/dist/interpretation/turn-options.js +417 -0
  89. package/dist/interpretation/turn-options.js.map +1 -0
  90. package/dist/interpretation/vocabulary.d.ts +5 -0
  91. package/dist/interpretation/vocabulary.d.ts.map +1 -1
  92. package/dist/interpretation/vocabulary.js +1 -1
  93. package/dist/interpretation/vocabulary.js.map +1 -1
  94. package/dist/knowledge/claude-code.d.ts +13 -1
  95. package/dist/knowledge/claude-code.d.ts.map +1 -1
  96. package/dist/knowledge/claude-code.js +35 -13
  97. package/dist/knowledge/claude-code.js.map +1 -1
  98. package/dist/knowledge/codex.d.ts.map +1 -1
  99. package/dist/knowledge/codex.js +25 -14
  100. package/dist/knowledge/codex.js.map +1 -1
  101. package/dist/knowledge/descriptor.d.ts +118 -14
  102. package/dist/knowledge/descriptor.d.ts.map +1 -1
  103. package/dist/knowledge/descriptor.js +58 -0
  104. package/dist/knowledge/descriptor.js.map +1 -1
  105. package/dist/knowledge/matchers.d.ts +3 -3
  106. package/dist/knowledge/matchers.d.ts.map +1 -1
  107. package/dist/knowledge/matchers.js +19 -6
  108. package/dist/knowledge/matchers.js.map +1 -1
  109. package/dist/knowledge/muse.d.ts +2 -2
  110. package/dist/knowledge/muse.d.ts.map +1 -1
  111. package/dist/knowledge/muse.js +22 -6
  112. package/dist/knowledge/muse.js.map +1 -1
  113. package/dist/knowledge/overrides.d.ts +7 -0
  114. package/dist/knowledge/overrides.d.ts.map +1 -1
  115. package/dist/knowledge/overrides.js +76 -0
  116. package/dist/knowledge/overrides.js.map +1 -1
  117. package/dist/knowledge/pi.d.ts.map +1 -1
  118. package/dist/knowledge/pi.js +25 -3
  119. package/dist/knowledge/pi.js.map +1 -1
  120. package/package.json +5 -2
  121. package/src/cli/args.ts +381 -0
  122. package/src/cli/check.ts +136 -0
  123. package/src/cli/exit-codes.ts +13 -0
  124. package/src/cli/help.ts +114 -0
  125. package/src/cli/index.ts +177 -0
  126. package/src/cli/inspect.ts +174 -0
  127. package/src/cli/ls.ts +14 -0
  128. package/src/cli/render.ts +62 -0
  129. package/src/cli/resolve-harness.ts +33 -0
  130. package/src/cli/run.ts +280 -0
  131. package/src/cli/session.ts +137 -0
  132. package/src/cli/version.ts +32 -0
  133. package/src/execution/decode.ts +32 -0
  134. package/src/execution/deps.ts +7 -1
  135. package/src/execution/events.ts +9 -2
  136. package/src/execution/failure.ts +159 -0
  137. package/src/execution/node-deps.ts +17 -0
  138. package/src/execution/stream-turn.ts +189 -17
  139. package/src/interpretation/argv.ts +85 -27
  140. package/src/interpretation/dimensions.ts +0 -6
  141. package/src/interpretation/limits.ts +97 -6
  142. package/src/interpretation/refusal.ts +116 -0
  143. package/src/interpretation/turn-options.ts +424 -0
  144. package/src/interpretation/vocabulary.ts +1 -1
  145. package/src/knowledge/claude-code.ts +35 -13
  146. package/src/knowledge/codex.ts +25 -14
  147. package/src/knowledge/descriptor.ts +138 -12
  148. package/src/knowledge/matchers.ts +22 -9
  149. package/src/knowledge/muse.ts +22 -6
  150. package/src/knowledge/overrides.ts +97 -0
  151. package/src/knowledge/pi.ts +25 -3
@@ -0,0 +1,136 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { type VersionStatus, versionStatus } from "../interpretation/versions.js";
3
+ import type { HarnessDescriptor } from "../knowledge/descriptor.js";
4
+ import { defaultDescriptors } from "../knowledge/overrides.js";
5
+
6
+ interface Row {
7
+ harness: string;
8
+ bin: string;
9
+ verifiedAgainst: string;
10
+ latest: string | null;
11
+ source: string;
12
+ status: VersionStatus;
13
+ }
14
+
15
+ const npmLatest = async (pkg: string): Promise<string | null> => {
16
+ try {
17
+ const res = await fetch(`https://registry.npmjs.org/${pkg}`, {
18
+ headers: { accept: "application/vnd.npm.install-v1+json" },
19
+ signal: AbortSignal.timeout(10_000),
20
+ });
21
+ if (!res.ok) return null;
22
+ const body = (await res.json()) as { "dist-tags"?: { latest?: unknown } };
23
+ const latest = body["dist-tags"]?.latest;
24
+ return typeof latest === "string" ? latest : null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ };
29
+
30
+ const installedVersion = (bin: string): string | null => {
31
+ try {
32
+ const out = execFileSync(bin, ["--version"], { encoding: "utf8", timeout: 10_000 });
33
+ const match = out.match(/\d+\.\d+\.\d+(?:[.+-][0-9A-Za-z.-]+)?/);
34
+ return match ? match[0] : null;
35
+ } catch {
36
+ return null;
37
+ }
38
+ };
39
+
40
+ const resolveLatest = async (
41
+ h: HarnessDescriptor,
42
+ ): Promise<{ latest: string | null; source: string }> => {
43
+ if (h.versionSource.kind === "npm") {
44
+ return {
45
+ latest: await npmLatest(h.versionSource.package),
46
+ source: `npm:${h.versionSource.package}`,
47
+ };
48
+ }
49
+ return { latest: installedVersion(h.bin), source: `installed:${h.bin}` };
50
+ };
51
+
52
+ export const check = async (rawArgs: string[]): Promise<void> => {
53
+ const wantJson = rawArgs.includes("--json");
54
+ const wantHelp = rawArgs.includes("--help") || rawArgs.includes("-h");
55
+ if (wantHelp) {
56
+ const { CHECK_HELP } = await import("./help.js");
57
+ process.stdout.write(CHECK_HELP);
58
+ return;
59
+ }
60
+
61
+ const descriptors = Object.values(defaultDescriptors()).filter(
62
+ (d): d is HarnessDescriptor => d !== undefined,
63
+ );
64
+
65
+ const rows: Row[] = await Promise.all(
66
+ descriptors.map(async (h) => {
67
+ const { latest, source } = await resolveLatest(h);
68
+ return {
69
+ harness: h.name,
70
+ bin: h.bin,
71
+ verifiedAgainst: h.verifiedAgainst,
72
+ latest,
73
+ source,
74
+ status: versionStatus(h.verifiedAgainst, latest),
75
+ };
76
+ }),
77
+ );
78
+
79
+ const behind = rows.filter((r) => r.status === "behind");
80
+ const unknown = rows.filter((r) => r.status === "unknown");
81
+
82
+ if (wantJson) {
83
+ process.stdout.write(
84
+ `${JSON.stringify({ checkedAt: new Date().toISOString(), rows, behind: behind.map((r) => r.harness) }, null, 2)}\n`,
85
+ );
86
+ } else {
87
+ const mark = (s: VersionStatus) =>
88
+ s === "ok" ? "✓ ok" : s === "behind" ? "⬆ NEW" : s === "ahead" ? "? ahead" : "– unknown";
89
+ process.stdout.write(
90
+ `\n${"harness".padEnd(9)}${"verified".padEnd(12)}${"latest".padEnd(12)}status\n`,
91
+ );
92
+ for (const r of rows) {
93
+ process.stdout.write(
94
+ `${r.harness.padEnd(9)}${r.verifiedAgainst.padEnd(12)}${(r.latest ?? "?").padEnd(12)}${mark(r.status)}\n`,
95
+ );
96
+ }
97
+ if (behind.length > 0) {
98
+ process.stdout.write(
99
+ `\n${behind.length} harness(es) behind: ${behind.map((r) => `${r.harness} ${r.verifiedAgainst}→${r.latest}`).join(", ")}\n`,
100
+ );
101
+ process.stdout.write(
102
+ "Run the local capability tripwires (smoke:seven) against the new version, then file an issue + bump verifiedAgainst.\n",
103
+ );
104
+ } else if (unknown.length > 0) {
105
+ process.stdout.write(`\n${unknown.length} harness(es) unknown (network or missing bin)\n`);
106
+ } else {
107
+ process.stdout.write("\nall descriptors current with their published versions.\n");
108
+ }
109
+ }
110
+
111
+ // Exit logic: behind => 1, unknown (network failure) => 1 with partial results, else 0
112
+ // But we must not exit 1 for unknown if behind already counts? We'll handle:
113
+ if (behind.length > 0) {
114
+ process.exitCode = 1;
115
+ } else if (unknown.length > 0 && rows.some((r) => r.latest === null)) {
116
+ // network failure at least one
117
+ // Per spec: network failure prints warning and exits 1 with partial results
118
+ // We'll check if any latest is null and that we attempted npm fetch (could be network)
119
+ // Simpler: if unknown and not all ok, exit 1
120
+ // But spec says check should reuse versions.ts and not crash; exit 1 with partial results
121
+ // We'll exit 1 if any harness has unknown and at least one npm source failed - but we can't distinguish installed missing vs network.
122
+ // For now, exit 1 if unknown.length >0 and at least one behind==0? Actually spec says network failure => exit 1.
123
+ // We'll exit 1 if unknown includes npm harnesses (they shouldn't be unknown without network).
124
+ const npmUnknown = rows.filter((r) => r.source.startsWith("npm:") && r.status === "unknown");
125
+ if (npmUnknown.length > 0) process.exitCode = 1;
126
+ else if (behind.length === 0 && unknown.length > 0) {
127
+ // installed unknown (muse missing) is not necessarily failure? But spec says network failure case is 1.
128
+ // For installed missing, we treat as unknown but not failure - keep 0 to not break CI where muse not installed.
129
+ // However spec says check prints partial results and exits 1 on network failure.
130
+ // We'll only exit 1 for npmUnknown, otherwise 0.
131
+ process.exitCode = 0;
132
+ }
133
+ } else {
134
+ process.exitCode = 0;
135
+ }
136
+ };
@@ -0,0 +1,13 @@
1
+ import type { ExitCause } from "../execution/events.js";
2
+
3
+ export const EXIT_CLEAN = 0;
4
+ export const EXIT_REFUSAL = 2;
5
+ export const EXIT_FAILURE = 1;
6
+
7
+ export const exitCodeForCause = (cause: ExitCause): number => {
8
+ if (cause === "clean") return EXIT_CLEAN;
9
+ // All non-clean causes are failures that should be exit 1, except refusal is already handled separately
10
+ return EXIT_FAILURE;
11
+ };
12
+
13
+ export const exitCodeForRefusal = (): number => EXIT_REFUSAL;
@@ -0,0 +1,114 @@
1
+ export const TOP_LEVEL_HELP = `hcn - One stable interface to four coding-agent CLIs
2
+
3
+ Usage: hcn <command> [options] [prompt]
4
+
5
+ Commands:
6
+ run <harness> [prompt] One-shot headless turn (streamTurn)
7
+ session <harness> Interactive session (openSession, Claude-only)
8
+ inspect <harness> Descriptor / argv / capability inspection (no spawn)
9
+ ls List harnesses with verifiedAgainst versions
10
+ check Drift check (published version vs verifiedAgainst)
11
+
12
+ Options:
13
+ -h, --help Show help
14
+ -V, --version Show version
15
+
16
+ Run 'hcn <command> --help' for command-specific help.
17
+ `;
18
+
19
+ export const RUN_HELP = `hcn run - One-shot headless turn
20
+
21
+ Usage: hcn run <harness> [prompt] [options]
22
+
23
+ Arguments:
24
+ <harness> claude | codex | pi | muse
25
+ [prompt] Prompt text (positional). Must not start with '-'.
26
+ Use --prompt or --prompt-file for leading '-' or multi-line.
27
+
28
+ Options:
29
+ --prompt <text> Prompt text (alternative to positional)
30
+ --prompt-file <path|-> Read prompt from file, or stdin when '-'
31
+ --model <id> Model id (validated per harness)
32
+ --effort <value> Effort level (validated per harness/model)
33
+ --sandbox <value> Sandbox mode (codex only)
34
+ --provider <value> Provider (pi only)
35
+ --tools <a,b> Tool grants (claude only, comma-separated)
36
+ --autonomy Enable autonomy flag (claude/codex/muse)
37
+ --no-autonomy Disable autonomy
38
+ --write Enable write (muse)
39
+ --no-write Disable write
40
+ --shell Enable shell (muse)
41
+ --no-shell Disable shell
42
+ --max-steps <n> Max steps (muse, 1-10000)
43
+ --no-tools Disable tools discovery facet
44
+ --no-instruction-files Disable instructionFiles discovery facet
45
+ --no-extensions Disable extensions discovery facet
46
+ --no-skills Disable skills discovery facet
47
+ --cwd <path> Working directory for spawn
48
+ --env KEY=VAL Environment (repeatable; KEY= deletes)
49
+ --resume <uuid> Resume session id (UUID)
50
+ --json NDJSON HarnessEvent to stdout
51
+ -h, --help Show help
52
+ -V, --version Show version
53
+ `;
54
+
55
+ export const SESSION_HELP = `hcn session - Interactive session (Claude-only)
56
+
57
+ Usage: hcn session <harness> [options]
58
+
59
+ Arguments:
60
+ <harness> Must be 'claude' (other harnesses have no sessionMode)
61
+
62
+ Options:
63
+ --model <id> Model for the session
64
+ --session-id <uuid> Session id (UUID, else random)
65
+ --cwd <path> Working directory
66
+ --help Show help
67
+ --version Show version
68
+ `;
69
+
70
+ export const INSPECT_HELP = `hcn inspect - Descriptor / argv inspection
71
+
72
+ Usage: hcn inspect <harness> [options]
73
+
74
+ Arguments:
75
+ <harness> claude | codex | pi | muse
76
+
77
+ Options:
78
+ --argv Preview argv that would be spawned
79
+ --prompt <text> Prompt for argv preview
80
+ --prompt-file <path|-> Read prompt from file
81
+ --model <id> Model
82
+ --effort <value> Effort
83
+ --sandbox <value> Sandbox
84
+ --provider <value> Provider
85
+ --tools <a,b> Tools
86
+ --autonomy / --no-autonomy
87
+ --write / --no-write
88
+ --shell / --no-shell
89
+ --max-steps <n>
90
+ --no-tools, --no-instruction-files, --no-extensions, --no-skills
91
+ --cwd <path>
92
+ --env KEY=VAL
93
+ --resume <uuid>
94
+ -h, --help Show help
95
+ `;
96
+
97
+ export const LS_HELP = `hcn ls - List harnesses
98
+
99
+ Usage: hcn ls
100
+
101
+ Prints each harness name with its verifiedAgainst version and versionSource.
102
+ `;
103
+
104
+ export const CHECK_HELP = `hcn check - Drift check
105
+
106
+ Usage: hcn check [--json]
107
+
108
+ Compares each harness's verifiedAgainst against the latest published version.
109
+ Exits 0 when no drift, 1 when drift found, 1 on network failure with partial results.
110
+
111
+ Options:
112
+ --json Machine-readable output
113
+ -h, --help Show help
114
+ `;
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ import { TOP_LEVEL_HELP } from "./help.js";
3
+ import { getVersion } from "./version.js";
4
+
5
+ const SUPPORTED = ["claude", "codex", "pi", "muse"] as const;
6
+
7
+ // Prevent EPIPE crashes when piped to head/grep -q (e.g., hcn ls | head, hcn run --json | head)
8
+ process.stdout.on("error", (err) => {
9
+ const code = (err as NodeJS.ErrnoException).code;
10
+ if (code === "EPIPE") process.exit(0);
11
+ });
12
+ process.stderr.on("error", (err) => {
13
+ const code = (err as NodeJS.ErrnoException).code;
14
+ if (code === "EPIPE") process.exit(0);
15
+ });
16
+
17
+ const printVersion = (): void => {
18
+ process.stdout.write(`${getVersion()}\n`);
19
+ };
20
+
21
+ const printTopHelp = (): void => {
22
+ process.stdout.write(TOP_LEVEL_HELP);
23
+ };
24
+
25
+ export const failUnknownHarness = (name: string): never => {
26
+ process.stderr.write(
27
+ `unknown harness ${JSON.stringify(name)}; supported: ${SUPPORTED.join(", ")}\n`,
28
+ );
29
+ process.stderr.write(`supported: ${SUPPORTED.join(", ")}\n`);
30
+ process.exitCode = 2;
31
+ // Use process.exit to ensure exit code is set even if async
32
+ process.exit(2);
33
+ };
34
+
35
+ export const dispatch = async (raw: string[]): Promise<void> => {
36
+ // Shared dispatch for programmatic use (tests) - mirrors main but takes argv slice
37
+ // Global --help / --version without command
38
+ if (raw.length === 0 || raw[0] === "--help" || raw[0] === "-h") {
39
+ if (raw.includes("--version") || raw.includes("-V")) {
40
+ printVersion();
41
+ return;
42
+ }
43
+ printTopHelp();
44
+ return;
45
+ }
46
+ if (raw[0] === "--version" || raw[0] === "-V") {
47
+ printVersion();
48
+ return;
49
+ }
50
+
51
+ const cmd = raw[0] as string;
52
+
53
+ switch (cmd) {
54
+ case "ls": {
55
+ if (raw.includes("--help") || raw.includes("-h")) {
56
+ const { LS_HELP } = await import("./help.js");
57
+ process.stdout.write(LS_HELP);
58
+ return;
59
+ }
60
+ if (raw.includes("--version") || raw.includes("-V")) {
61
+ printVersion();
62
+ return;
63
+ }
64
+ if (raw.length > 1) {
65
+ const extra = raw.slice(1);
66
+ const hasUnknown = extra.some((a) => a.startsWith("-"));
67
+ if (hasUnknown) {
68
+ process.stderr.write(`unknown flag for ls: ${extra.join(" ")}\n`);
69
+ process.exitCode = 2;
70
+ return;
71
+ }
72
+ }
73
+ const { ls } = await import("./ls.js");
74
+ ls();
75
+ return;
76
+ }
77
+ case "check": {
78
+ const { check } = await import("./check.js");
79
+ await check(raw.slice(1));
80
+ return;
81
+ }
82
+ case "inspect": {
83
+ if (raw.slice(1).includes("--help") || raw.slice(1).includes("-h")) {
84
+ const { INSPECT_HELP } = await import("./help.js");
85
+ process.stdout.write(INSPECT_HELP);
86
+ return;
87
+ }
88
+ const harness = raw[1];
89
+ if (!harness || harness.startsWith("-")) {
90
+ process.stderr.write(`inspect requires <harness>; supported: ${SUPPORTED.join(", ")}\n`);
91
+ process.exitCode = 2;
92
+ return;
93
+ }
94
+ if (!(SUPPORTED as readonly string[]).includes(harness)) failUnknownHarness(harness);
95
+ const { inspect } = await import("./inspect.js");
96
+ await inspect(harness, raw.slice(2));
97
+ return;
98
+ }
99
+ case "run": {
100
+ if (raw.slice(1).includes("--help") || raw.slice(1).includes("-h")) {
101
+ const { RUN_HELP } = await import("./help.js");
102
+ process.stdout.write(RUN_HELP);
103
+ return;
104
+ }
105
+ if (raw.slice(1).includes("--version") || raw.slice(1).includes("-V")) {
106
+ printVersion();
107
+ return;
108
+ }
109
+ const harness = raw[1];
110
+ if (!harness || harness.startsWith("-")) {
111
+ process.stderr.write(`run requires <harness>; supported: ${SUPPORTED.join(", ")}\n`);
112
+ process.exitCode = 2;
113
+ return;
114
+ }
115
+ if (!(SUPPORTED as readonly string[]).includes(harness)) failUnknownHarness(harness);
116
+ const { run } = await import("./run.js");
117
+ await run(harness, raw.slice(2));
118
+ return;
119
+ }
120
+ case "session": {
121
+ if (raw.slice(1).includes("--help") || raw.slice(1).includes("-h")) {
122
+ const { SESSION_HELP } = await import("./help.js");
123
+ process.stdout.write(SESSION_HELP);
124
+ return;
125
+ }
126
+ const harness = raw[1];
127
+ if (!harness || harness.startsWith("-")) {
128
+ process.stderr.write(`session requires <harness>; supported: ${SUPPORTED.join(", ")}\n`);
129
+ process.exitCode = 2;
130
+ return;
131
+ }
132
+ if (!(SUPPORTED as readonly string[]).includes(harness)) failUnknownHarness(harness);
133
+ const { session } = await import("./session.js");
134
+ await session(harness, raw.slice(2));
135
+ return;
136
+ }
137
+ case "help":
138
+ case "--help":
139
+ case "-h": {
140
+ printTopHelp();
141
+ return;
142
+ }
143
+ default: {
144
+ if ((SUPPORTED as readonly string[]).includes(cmd)) {
145
+ process.stderr.write(`missing command; did you mean 'hcn run ${cmd} <prompt>'?\n`);
146
+ process.stderr.write(TOP_LEVEL_HELP);
147
+ process.exitCode = 2;
148
+ return;
149
+ }
150
+ process.stderr.write(`unknown command ${JSON.stringify(cmd)}\n`);
151
+ process.stderr.write(TOP_LEVEL_HELP);
152
+ process.exitCode = 2;
153
+ return;
154
+ }
155
+ }
156
+ };
157
+
158
+ const main = async (): Promise<void> => {
159
+ await dispatch(process.argv.slice(2));
160
+ };
161
+
162
+ const shouldAutoRun = (() => {
163
+ const entry = process.argv[1] ?? "";
164
+ return (
165
+ entry.endsWith("cli.js") || entry.endsWith("cli/index.js") || entry.endsWith("src/cli/index.ts")
166
+ );
167
+ })();
168
+
169
+ if (shouldAutoRun) {
170
+ main().catch((err) => {
171
+ const message = err instanceof Error ? err.message : String(err);
172
+ process.stderr.write(`fatal: ${message}\n`);
173
+ if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}\n`);
174
+ process.exitCode = 1;
175
+ if (process.exitCode === 0) process.exitCode = 1;
176
+ });
177
+ }
@@ -0,0 +1,174 @@
1
+ import { redactArgv } from "../execution/stream-turn.js";
2
+ import { buildLaunchArgv } from "../interpretation/argv.js";
3
+ import { ArgvRefusalError } from "../interpretation/refusal.js";
4
+ import { parseTurnOptions, resolvePromptAsync } from "./args.js";
5
+ import { resolveHarness } from "./resolve-harness.js";
6
+
7
+ export const inspect = async (harnessName: string, rawArgs: string[]): Promise<void> => {
8
+ const h = resolveHarness(harnessName);
9
+
10
+ // Check for --help
11
+ if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
12
+ const { INSPECT_HELP } = await import("./help.js");
13
+ process.stdout.write(INSPECT_HELP);
14
+ return;
15
+ }
16
+
17
+ // Check for positional prompt injection before parse (only when --argv and no explicit prompt)
18
+ const { parseCommonFlags, detectPositionalPromptInjection } = await import("./args.js");
19
+ if (rawArgs.includes("--argv")) {
20
+ const injection = detectPositionalPromptInjection(rawArgs.filter((a) => a !== "--argv"));
21
+ if (injection) {
22
+ const err = new ArgvRefusalError({
23
+ issue: "prompt-flag-injection",
24
+ harness: h.name,
25
+ supported: ["prompt must not start with '-'"],
26
+ detail: `it would be parsed as a flag by ${h.bin}`,
27
+ });
28
+ process.stderr.write(`${err.message}\n`);
29
+ process.stderr.write(`supported: prompt must not start with '-'\n`);
30
+ process.exitCode = 2;
31
+ return;
32
+ }
33
+ }
34
+
35
+ // Parse flags for inspect
36
+ let parsed: ReturnType<typeof parseCommonFlags>;
37
+ try {
38
+ parsed = parseCommonFlags(rawArgs);
39
+ } catch (err) {
40
+ const message = err instanceof Error ? err.message : String(err);
41
+ process.stderr.write(`unknown flag: ${message}\n`);
42
+ process.stderr.write(`Run 'hcn inspect --help' for usage.\n`);
43
+ process.exitCode = 2;
44
+ return;
45
+ }
46
+
47
+ const values = parsed.values as Record<string, unknown>;
48
+ const wantArgv = values.argv === true;
49
+
50
+ if (!wantArgv) {
51
+ // Pure descriptor dump
52
+ const out = {
53
+ name: h.name,
54
+ bin: h.bin,
55
+ verifiedAgainst: h.verifiedAgainst,
56
+ versionSource: h.versionSource,
57
+ launch: {
58
+ baseFlags: h.launch.baseFlags,
59
+ subcommands: h.launch.subcommands,
60
+ streamFlags: h.launch.streamFlags,
61
+ promptStyle: h.launch.promptStyle,
62
+ toolsFlag: h.launch.toolsFlag,
63
+ idFlag: h.launch.idFlag,
64
+ },
65
+ resume: h.resume,
66
+ sessionMode: h.sessionMode,
67
+ vocabulary: {
68
+ models: h.vocabulary.models,
69
+ aliases: h.vocabulary.aliases,
70
+ efforts: h.vocabulary.efforts,
71
+ effortsByModel: (h.vocabulary as { effortsByModel?: unknown }).effortsByModel,
72
+ extensible: h.vocabulary.extensible,
73
+ modelFlag: h.vocabulary.modelFlag,
74
+ },
75
+ turnOptions: h.turnOptions,
76
+ };
77
+ process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
78
+ return;
79
+ }
80
+
81
+ // --argv preview path: need prompt + turn options
82
+ const positional = parsed.positionals as string[];
83
+ // positional[0] would be harness already consumed, but we passed rawArgs without harness - check: caller passes remaining after harness
84
+ // So positionals are potential prompt positional if not using --prompt
85
+ const positionalPrompt = positional.length > 0 ? positional[0] : undefined;
86
+ if (positional.length > 1) {
87
+ process.stderr.write(`too many positionals for inspect --argv\n`);
88
+ process.exitCode = 2;
89
+ return;
90
+ }
91
+
92
+ let prompt: string;
93
+ let promptSource: string;
94
+ try {
95
+ const resolved = await resolvePromptAsync({
96
+ positionalPrompt,
97
+ promptFlag: values.prompt as string | undefined,
98
+ promptFile: values["prompt-file"] as string | undefined,
99
+ });
100
+ prompt = resolved.prompt;
101
+ promptSource = resolved.source;
102
+ } catch (err) {
103
+ if (err instanceof ArgvRefusalError) {
104
+ process.stderr.write(`${err.message}\n`);
105
+ if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
106
+ process.exitCode = 2;
107
+ return;
108
+ }
109
+ throw err;
110
+ }
111
+
112
+ // Build TurnOptions from flags
113
+ let turnOpts: ReturnType<typeof parseTurnOptions>;
114
+ try {
115
+ turnOpts = parseTurnOptions(values);
116
+ } catch (err) {
117
+ if (err instanceof ArgvRefusalError) {
118
+ process.stderr.write(`${err.message}\n`);
119
+ if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
120
+ process.exitCode = 2;
121
+ return;
122
+ }
123
+ throw err;
124
+ }
125
+
126
+ const fullOpts = {
127
+ ...turnOpts,
128
+ prompt,
129
+ ...(promptSource !== "positional" ? { __explicitPrompt: true as const } : {}),
130
+ } as Parameters<typeof buildLaunchArgv>[1];
131
+
132
+ let argv: string[];
133
+ try {
134
+ argv = buildLaunchArgv(h, fullOpts);
135
+ } catch (err) {
136
+ if (err instanceof ArgvRefusalError) {
137
+ process.stderr.write(`${err.message}\n`);
138
+ if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
139
+ process.exitCode = 2;
140
+ return;
141
+ }
142
+ throw err;
143
+ }
144
+
145
+ // Redact prompt for display, but keep structure
146
+ const redacted = redactArgv(argv, prompt);
147
+ // Also handle --resume case: need to use buildResumeArgv if resume present
148
+ if (values.resume !== undefined || values["session-id"] !== undefined) {
149
+ // For inspect --argv with --resume, show resume argv
150
+ const { buildResumeArgv } = await import("../interpretation/argv.js");
151
+ const resumeId = String(values.resume ?? values["session-id"]);
152
+ try {
153
+ const resumeOpts = { ...fullOpts, sessionId: resumeId };
154
+ const resumeArgv = buildResumeArgv(h, resumeOpts);
155
+ const redactedResume = redactArgv(resumeArgv, prompt);
156
+ process.stdout.write(`${JSON.stringify(redactedResume)}\n`);
157
+ process.stderr.write(`argv: ${redactedResume.join(" ")}\n`);
158
+ return;
159
+ } catch (err) {
160
+ if (err instanceof ArgvRefusalError) {
161
+ process.stderr.write(`${err.message}\n`);
162
+ if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
163
+ process.exitCode = 2;
164
+ return;
165
+ }
166
+ throw err;
167
+ }
168
+ }
169
+
170
+ process.stdout.write(`${JSON.stringify(redacted)}\n`);
171
+ process.stderr.write(`argv: ${redacted.join(" ")}\n`);
172
+ };
173
+
174
+ export const inspectCommand = inspect;
package/src/cli/ls.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { HarnessDescriptor } from "../knowledge/descriptor.js";
2
+ import { defaultDescriptors } from "../knowledge/overrides.js";
3
+
4
+ export const ls = (): void => {
5
+ const descriptors = Object.values(defaultDescriptors()).filter(
6
+ (d): d is HarnessDescriptor => d !== undefined,
7
+ );
8
+ for (const h of descriptors) {
9
+ const source =
10
+ h.versionSource.kind === "npm" ? `npm:${h.versionSource.package}` : `installed:${h.bin}`;
11
+ // Format: "claude@2.1.229 (npm:@anthropic-ai/claude-code)"
12
+ process.stdout.write(`${h.name}@${h.verifiedAgainst} (${source})\n`);
13
+ }
14
+ };
@@ -0,0 +1,62 @@
1
+ import type { HarnessEvent } from "../execution/events.js";
2
+
3
+ const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
4
+ const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
5
+ const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
6
+ const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
7
+ const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
8
+
9
+ export interface RenderState {
10
+ streamed: boolean;
11
+ }
12
+
13
+ export const createRenderState = (): RenderState => ({ streamed: false });
14
+
15
+ export const renderEvent = (event: HarnessEvent, state: RenderState): void => {
16
+ switch (event.kind) {
17
+ case "identity":
18
+ process.stdout.write(dim(` ● session ${event.sessionId} (${event.authority})\n`));
19
+ break;
20
+ case "token":
21
+ process.stdout.write(event.text);
22
+ state.streamed = true;
23
+ break;
24
+ case "message":
25
+ if (!state.streamed) process.stdout.write(event.text);
26
+ break;
27
+ case "tool":
28
+ process.stdout.write(cyan(`\n ⚙ ${event.name}`));
29
+ break;
30
+ case "progress":
31
+ // droppable
32
+ break;
33
+ case "context":
34
+ process.stdout.write(dim(`\n ▪ context ${event.usedPct}%`));
35
+ break;
36
+ case "limit":
37
+ process.stdout.write(yellow(`\n ⚠ limit: ${event.code} ${event.message}`));
38
+ break;
39
+ case "error":
40
+ process.stdout.write(red(`\n ✗ ${event.message}`));
41
+ break;
42
+ case "failure": {
43
+ const detail = event.message ?? `${event.class}`;
44
+ process.stdout.write(red(`\n ✗ failure ${event.class}: ${detail}`));
45
+ break;
46
+ }
47
+ case "done": {
48
+ const mark = event.cause === "clean" ? green("○ clean") : red(`○ ${event.cause}`);
49
+ const tail = event.failure ? ` ${event.failure.class}: ${event.failure.message}` : "";
50
+ process.stdout.write(`\n ${mark} (exit ${event.exitCode ?? "none"})${tail}\n`);
51
+ break;
52
+ }
53
+ default: {
54
+ const exhaustive: never = event as never;
55
+ process.stdout.write(dim(`\n ? ${(exhaustive as { kind: string }).kind}`));
56
+ }
57
+ }
58
+ };
59
+
60
+ export const writeEventNdjson = (event: HarnessEvent): void => {
61
+ process.stdout.write(`${JSON.stringify(event)}\n`);
62
+ };