@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.
- package/README.md +144 -7
- package/dist/cli/args.d.ts +74 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +339 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/check.d.ts +2 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +110 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +7 -0
- package/dist/cli/exit-codes.d.ts.map +1 -0
- package/dist/cli/exit-codes.js +11 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/help.d.ts +7 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +110 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +170 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/inspect.d.ts +3 -0
- package/dist/cli/inspect.d.ts.map +1 -0
- package/dist/cli/inspect.js +170 -0
- package/dist/cli/inspect.js.map +1 -0
- package/dist/cli/ls.d.ts +2 -0
- package/dist/cli/ls.d.ts.map +1 -0
- package/dist/cli/ls.js +10 -0
- package/dist/cli/ls.js.map +1 -0
- package/dist/cli/render.d.ts +8 -0
- package/dist/cli/render.d.ts.map +1 -0
- package/dist/cli/render.js +55 -0
- package/dist/cli/render.js.map +1 -0
- package/dist/cli/resolve-harness.d.ts +4 -0
- package/dist/cli/resolve-harness.d.ts.map +1 -0
- package/dist/cli/resolve-harness.js +31 -0
- package/dist/cli/resolve-harness.js.map +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +280 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/session.d.ts +2 -0
- package/dist/cli/session.d.ts.map +1 -0
- package/dist/cli/session.js +128 -0
- package/dist/cli/session.js.map +1 -0
- package/dist/cli/version.d.ts +2 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/cli/version.js +34 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli.js +2 -0
- package/dist/execution/decode.d.ts.map +1 -1
- package/dist/execution/decode.js +32 -0
- package/dist/execution/decode.js.map +1 -1
- package/dist/execution/deps.d.ts +7 -1
- package/dist/execution/deps.d.ts.map +1 -1
- package/dist/execution/events.d.ts +6 -2
- package/dist/execution/events.d.ts.map +1 -1
- package/dist/execution/events.js.map +1 -1
- package/dist/execution/failure.d.ts +45 -0
- package/dist/execution/failure.d.ts.map +1 -0
- package/dist/execution/failure.js +121 -0
- package/dist/execution/failure.js.map +1 -0
- package/dist/execution/node-deps.d.ts.map +1 -1
- package/dist/execution/node-deps.js +19 -0
- package/dist/execution/node-deps.js.map +1 -1
- package/dist/execution/stream-turn.d.ts +2 -0
- package/dist/execution/stream-turn.d.ts.map +1 -1
- package/dist/execution/stream-turn.js +181 -16
- package/dist/execution/stream-turn.js.map +1 -1
- package/dist/interpretation/argv.d.ts +16 -4
- package/dist/interpretation/argv.d.ts.map +1 -1
- package/dist/interpretation/argv.js +68 -19
- package/dist/interpretation/argv.js.map +1 -1
- package/dist/interpretation/dimensions.d.ts +0 -2
- package/dist/interpretation/dimensions.d.ts.map +1 -1
- package/dist/interpretation/dimensions.js +0 -2
- package/dist/interpretation/dimensions.js.map +1 -1
- package/dist/interpretation/limits.d.ts +10 -1
- package/dist/interpretation/limits.d.ts.map +1 -1
- package/dist/interpretation/limits.js +84 -5
- package/dist/interpretation/limits.js.map +1 -1
- package/dist/interpretation/refusal.d.ts +31 -0
- package/dist/interpretation/refusal.d.ts.map +1 -0
- package/dist/interpretation/refusal.js +87 -0
- package/dist/interpretation/refusal.js.map +1 -0
- package/dist/interpretation/turn-options.d.ts +18 -0
- package/dist/interpretation/turn-options.d.ts.map +1 -0
- package/dist/interpretation/turn-options.js +417 -0
- package/dist/interpretation/turn-options.js.map +1 -0
- package/dist/interpretation/vocabulary.d.ts +5 -0
- package/dist/interpretation/vocabulary.d.ts.map +1 -1
- package/dist/interpretation/vocabulary.js +1 -1
- package/dist/interpretation/vocabulary.js.map +1 -1
- package/dist/knowledge/claude-code.d.ts +13 -1
- package/dist/knowledge/claude-code.d.ts.map +1 -1
- package/dist/knowledge/claude-code.js +35 -13
- package/dist/knowledge/claude-code.js.map +1 -1
- package/dist/knowledge/codex.d.ts.map +1 -1
- package/dist/knowledge/codex.js +25 -14
- package/dist/knowledge/codex.js.map +1 -1
- package/dist/knowledge/descriptor.d.ts +118 -14
- package/dist/knowledge/descriptor.d.ts.map +1 -1
- package/dist/knowledge/descriptor.js +58 -0
- package/dist/knowledge/descriptor.js.map +1 -1
- package/dist/knowledge/matchers.d.ts +3 -3
- package/dist/knowledge/matchers.d.ts.map +1 -1
- package/dist/knowledge/matchers.js +19 -6
- package/dist/knowledge/matchers.js.map +1 -1
- package/dist/knowledge/muse.d.ts +2 -2
- package/dist/knowledge/muse.d.ts.map +1 -1
- package/dist/knowledge/muse.js +22 -6
- package/dist/knowledge/muse.js.map +1 -1
- package/dist/knowledge/overrides.d.ts +7 -0
- package/dist/knowledge/overrides.d.ts.map +1 -1
- package/dist/knowledge/overrides.js +76 -0
- package/dist/knowledge/overrides.js.map +1 -1
- package/dist/knowledge/pi.d.ts.map +1 -1
- package/dist/knowledge/pi.js +25 -3
- package/dist/knowledge/pi.js.map +1 -1
- package/package.json +5 -2
- package/src/cli/args.ts +381 -0
- package/src/cli/check.ts +136 -0
- package/src/cli/exit-codes.ts +13 -0
- package/src/cli/help.ts +114 -0
- package/src/cli/index.ts +177 -0
- package/src/cli/inspect.ts +174 -0
- package/src/cli/ls.ts +14 -0
- package/src/cli/render.ts +62 -0
- package/src/cli/resolve-harness.ts +33 -0
- package/src/cli/run.ts +280 -0
- package/src/cli/session.ts +137 -0
- package/src/cli/version.ts +32 -0
- package/src/execution/decode.ts +32 -0
- package/src/execution/deps.ts +7 -1
- package/src/execution/events.ts +9 -2
- package/src/execution/failure.ts +159 -0
- package/src/execution/node-deps.ts +17 -0
- package/src/execution/stream-turn.ts +189 -17
- package/src/interpretation/argv.ts +85 -27
- package/src/interpretation/dimensions.ts +0 -6
- package/src/interpretation/limits.ts +97 -6
- package/src/interpretation/refusal.ts +116 -0
- package/src/interpretation/turn-options.ts +424 -0
- package/src/interpretation/vocabulary.ts +1 -1
- package/src/knowledge/claude-code.ts +35 -13
- package/src/knowledge/codex.ts +25 -14
- package/src/knowledge/descriptor.ts +138 -12
- package/src/knowledge/matchers.ts +22 -9
- package/src/knowledge/muse.ts +22 -6
- package/src/knowledge/overrides.ts +97 -0
- package/src/knowledge/pi.ts +25 -3
package/src/cli/check.ts
ADDED
|
@@ -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;
|
package/src/cli/help.ts
ADDED
|
@@ -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
|
+
`;
|
package/src/cli/index.ts
ADDED
|
@@ -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
|
+
};
|