@dungle-scrubs/harness-cli-normalizer 0.1.3 → 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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
2
|
+
import type { HarnessDescriptor } from "../knowledge/descriptor.js";
|
|
3
|
+
import { HARNESS_NAMES } from "../knowledge/descriptor.js";
|
|
4
|
+
import { defaultDescriptors } from "../knowledge/overrides.js";
|
|
5
|
+
|
|
6
|
+
export const resolveHarness = (name: string): HarnessDescriptor => {
|
|
7
|
+
if (!(HARNESS_NAMES as readonly string[]).includes(name)) {
|
|
8
|
+
// Use ArgvRefusalError shape for consistency but throw generic with supported list
|
|
9
|
+
// Caller will map to exit 2 with supported list
|
|
10
|
+
const err = new ArgvRefusalError({
|
|
11
|
+
issue: "unsupported-option",
|
|
12
|
+
harness: "claude",
|
|
13
|
+
supported: [...HARNESS_NAMES],
|
|
14
|
+
detail: `unknown harness ${JSON.stringify(name)}`,
|
|
15
|
+
});
|
|
16
|
+
// Overwrite message to be harness-specific
|
|
17
|
+
err.message = `unknown harness ${JSON.stringify(name)}; supported: ${[...HARNESS_NAMES].join(", ")}`;
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
const map = defaultDescriptors();
|
|
21
|
+
const h = map[name as keyof typeof map];
|
|
22
|
+
if (!h) {
|
|
23
|
+
throw new ArgvRefusalError({
|
|
24
|
+
issue: "unsupported-option",
|
|
25
|
+
harness: name as "claude",
|
|
26
|
+
supported: [...HARNESS_NAMES],
|
|
27
|
+
detail: `harness ${name} not found`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return h;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const SUPPORTED_HARNESSES = [...HARNESS_NAMES] as const;
|
package/src/cli/run.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import type { HarnessEvent } from "../execution/events.js";
|
|
2
|
+
import { nodeRunnerDeps } from "../execution/node-deps.js";
|
|
3
|
+
import { KILL_GRACE_MS, redactArgv, streamTurn } from "../execution/stream-turn.js";
|
|
4
|
+
import { buildLaunchArgv, buildResumeArgv } from "../interpretation/argv.js";
|
|
5
|
+
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
6
|
+
import { parseRunExtra, parseTurnOptions, resolvePromptAsync } from "./args.js";
|
|
7
|
+
import { createRenderState, renderEvent, writeEventNdjson } from "./render.js";
|
|
8
|
+
import { resolveHarness } from "./resolve-harness.js";
|
|
9
|
+
|
|
10
|
+
export const run = async (harnessName: string, rawArgs: string[]): Promise<void> => {
|
|
11
|
+
const h = resolveHarness(harnessName);
|
|
12
|
+
|
|
13
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
14
|
+
const { RUN_HELP } = await import("./help.js");
|
|
15
|
+
process.stdout.write(RUN_HELP);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { parseCommonFlags, detectPositionalPromptInjection } = await import("./args.js");
|
|
20
|
+
const injection = detectPositionalPromptInjection(rawArgs);
|
|
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
|
+
let parsed: ReturnType<typeof parseCommonFlags>;
|
|
34
|
+
try {
|
|
35
|
+
parsed = parseCommonFlags(rawArgs);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
38
|
+
process.stderr.write(`unknown flag: ${message}\n`);
|
|
39
|
+
process.stderr.write(`Run 'hcn run --help' for usage.\n`);
|
|
40
|
+
process.exitCode = 2;
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const values = parsed.values as Record<string, unknown>;
|
|
45
|
+
const positionals = parsed.positionals as string[];
|
|
46
|
+
// positionals may contain prompt if not using flag; harness already consumed so first positional is prompt
|
|
47
|
+
const positionalPrompt = positionals.length > 0 ? positionals[0] : undefined;
|
|
48
|
+
if (positionals.length > 1) {
|
|
49
|
+
process.stderr.write(`too many positionals for run; expected one prompt\n`);
|
|
50
|
+
process.exitCode = 2;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const wantJson = values.json === true;
|
|
55
|
+
|
|
56
|
+
// Resolve prompt (async for --prompt-file -)
|
|
57
|
+
let prompt: string;
|
|
58
|
+
let promptSource: string;
|
|
59
|
+
try {
|
|
60
|
+
const resolved = await resolvePromptAsync({
|
|
61
|
+
positionalPrompt,
|
|
62
|
+
promptFlag: values.prompt as string | undefined,
|
|
63
|
+
promptFile: values["prompt-file"] as string | undefined,
|
|
64
|
+
});
|
|
65
|
+
prompt = resolved.prompt;
|
|
66
|
+
promptSource = resolved.source;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (err instanceof ArgvRefusalError) {
|
|
69
|
+
process.stderr.write(`${err.message}\n`);
|
|
70
|
+
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
71
|
+
process.exitCode = 2;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (err instanceof Error && (err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
75
|
+
process.stderr.write(`prompt file not found: ${(err as Error).message}\n`);
|
|
76
|
+
process.exitCode = 2;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Build turn options
|
|
83
|
+
let turnOpts: ReturnType<typeof parseTurnOptions>;
|
|
84
|
+
try {
|
|
85
|
+
turnOpts = parseTurnOptions(values);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err instanceof ArgvRefusalError) {
|
|
88
|
+
process.stderr.write(`${err.message}\n`);
|
|
89
|
+
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
90
|
+
process.exitCode = 2;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let extra: ReturnType<typeof parseRunExtra>;
|
|
97
|
+
try {
|
|
98
|
+
extra = parseRunExtra(values);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (err instanceof ArgvRefusalError) {
|
|
101
|
+
process.stderr.write(`${err.message}\n`);
|
|
102
|
+
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
103
|
+
process.exitCode = 2;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const isExplicit = promptSource !== "positional";
|
|
110
|
+
const fullOpts = {
|
|
111
|
+
...turnOpts,
|
|
112
|
+
prompt,
|
|
113
|
+
cwd: extra.cwd,
|
|
114
|
+
env: extra.env,
|
|
115
|
+
resume: extra.resume,
|
|
116
|
+
...(isExplicit ? { __explicitPrompt: true as const } : {}),
|
|
117
|
+
} as Parameters<typeof streamTurn>[1] & { resume?: string; __explicitPrompt?: boolean };
|
|
118
|
+
|
|
119
|
+
// Pre-validate via building argv to catch refusals before spawn (so we don't spawn on bad args)
|
|
120
|
+
let _validated = false;
|
|
121
|
+
let preArgv: string[] | null = null;
|
|
122
|
+
try {
|
|
123
|
+
if (fullOpts.resume) {
|
|
124
|
+
preArgv = buildResumeArgv(h, {
|
|
125
|
+
...(turnOpts as object),
|
|
126
|
+
prompt,
|
|
127
|
+
sessionId: fullOpts.resume,
|
|
128
|
+
__explicitPrompt: isExplicit,
|
|
129
|
+
} as never);
|
|
130
|
+
} else {
|
|
131
|
+
preArgv = buildLaunchArgv(h, {
|
|
132
|
+
...(turnOpts as object),
|
|
133
|
+
prompt,
|
|
134
|
+
__explicitPrompt: isExplicit,
|
|
135
|
+
} as never);
|
|
136
|
+
}
|
|
137
|
+
_validated = true;
|
|
138
|
+
} catch (err) {
|
|
139
|
+
if (err instanceof ArgvRefusalError) {
|
|
140
|
+
process.stderr.write(`${err.message}\n`);
|
|
141
|
+
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
142
|
+
process.exitCode = 2;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
throw err;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// preArgv is already set via validated build; no extra handling needed for explicit prompt bypass
|
|
149
|
+
// since buildLaunchArgv now respects __explicitPrompt.
|
|
150
|
+
|
|
151
|
+
if (preArgv) {
|
|
152
|
+
const redacted = redactArgv(preArgv, prompt);
|
|
153
|
+
if (!wantJson) {
|
|
154
|
+
process.stderr.write(`spawn: ${redacted.join(" ")}\n`);
|
|
155
|
+
} else {
|
|
156
|
+
// In JSON mode, diagnostics to stderr only
|
|
157
|
+
process.stderr.write(`spawn: ${redacted.join(" ")}\n`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Delete HERDR_ENV before spawn
|
|
162
|
+
delete (process.env as Record<string, string | undefined>).HERDR_ENV;
|
|
163
|
+
|
|
164
|
+
const deps = nodeRunnerDeps();
|
|
165
|
+
|
|
166
|
+
// Signal handling
|
|
167
|
+
const _currentProc: { signal: (sig: "SIGTERM" | "SIGKILL") => void } | null = null;
|
|
168
|
+
// We'll need to track the spawned process via deps.signal; but streamTurn owns process handle.
|
|
169
|
+
// Instead we intercept deps.signal via a wrapper that captures proc.
|
|
170
|
+
// Simpler: use deps directly and handle SIGINT by calling process.kill? But spec says via injected signal.
|
|
171
|
+
// We'll create a wrapper deps where signal captures last proc.
|
|
172
|
+
let lastProc: import("../execution/deps.js").SpawnedProcess | null = null;
|
|
173
|
+
const originalSignal = deps.signal;
|
|
174
|
+
const wrappedDeps = {
|
|
175
|
+
...deps,
|
|
176
|
+
signal: (proc: import("../execution/deps.js").SpawnedProcess, sig: "SIGTERM" | "SIGKILL") => {
|
|
177
|
+
lastProc = proc;
|
|
178
|
+
originalSignal(proc, sig);
|
|
179
|
+
},
|
|
180
|
+
spawn: (argv: readonly string[], opts: import("../execution/deps.js").SpawnOptions) => {
|
|
181
|
+
const proc = deps.spawn(argv, opts);
|
|
182
|
+
lastProc = proc;
|
|
183
|
+
return proc;
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
let interrupted = false;
|
|
188
|
+
const onSig = async () => {
|
|
189
|
+
if (interrupted) return;
|
|
190
|
+
interrupted = true;
|
|
191
|
+
if (lastProc) {
|
|
192
|
+
try {
|
|
193
|
+
wrappedDeps.signal(lastProc, "SIGTERM");
|
|
194
|
+
await new Promise<void>((resolve) => {
|
|
195
|
+
setTimeout(() => {
|
|
196
|
+
if (lastProc) {
|
|
197
|
+
try {
|
|
198
|
+
wrappedDeps.signal(lastProc, "SIGKILL");
|
|
199
|
+
} catch {}
|
|
200
|
+
}
|
|
201
|
+
resolve();
|
|
202
|
+
}, KILL_GRACE_MS);
|
|
203
|
+
});
|
|
204
|
+
} catch {}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
process.on("SIGINT", onSig);
|
|
208
|
+
process.on("SIGTERM", onSig);
|
|
209
|
+
|
|
210
|
+
const state = createRenderState();
|
|
211
|
+
let lastEvent: HarnessEvent | null = null;
|
|
212
|
+
let exitCode = 0;
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// streamTurn handles both launch and resume via TurnRunOptions
|
|
216
|
+
const events = streamTurn(h, fullOpts, wrappedDeps);
|
|
217
|
+
for await (const event of events) {
|
|
218
|
+
lastEvent = event;
|
|
219
|
+
if (wantJson) {
|
|
220
|
+
writeEventNdjson(event);
|
|
221
|
+
} else {
|
|
222
|
+
renderEvent(event, state);
|
|
223
|
+
}
|
|
224
|
+
if (event.kind === "done") {
|
|
225
|
+
if (event.cause === "clean") exitCode = 0;
|
|
226
|
+
else exitCode = 1;
|
|
227
|
+
// If failure class is rejected? But done.cause for rejected would be failed? Still 1 per mapping, but refusal before spawn is 2.
|
|
228
|
+
// The RFC says limit/auth ->1, transport ->1, refusal ->2 (already handled). So done non-clean =>1.
|
|
229
|
+
}
|
|
230
|
+
if (event.kind === "limit") {
|
|
231
|
+
// Also ensure exitCode will be 1 (handled via done)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (err instanceof ArgvRefusalError) {
|
|
236
|
+
process.stderr.write(`${err.message}\n`);
|
|
237
|
+
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
238
|
+
process.exitCode = 2;
|
|
239
|
+
process.off("SIGINT", onSig);
|
|
240
|
+
process.off("SIGTERM", onSig);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// Transport / spawn failure
|
|
244
|
+
process.stderr.write(`run failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
245
|
+
if (wantJson && lastEvent?.kind !== "done") {
|
|
246
|
+
// Emit failure+done if stream didn't? streamTurn should already emit done, but if we crashed before spawn, synthesize?
|
|
247
|
+
const failure = {
|
|
248
|
+
kind: "failure" as const,
|
|
249
|
+
class: "transport" as const,
|
|
250
|
+
retryable: true,
|
|
251
|
+
message: String(err),
|
|
252
|
+
};
|
|
253
|
+
process.stdout.write(`${JSON.stringify(failure)}\n`);
|
|
254
|
+
process.stdout.write(
|
|
255
|
+
`${JSON.stringify({ kind: "done", exitCode: null, cause: "failed", failure })}\n`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
process.exitCode = 1;
|
|
259
|
+
process.off("SIGINT", onSig);
|
|
260
|
+
process.off("SIGTERM", onSig);
|
|
261
|
+
return;
|
|
262
|
+
} finally {
|
|
263
|
+
process.off("SIGINT", onSig);
|
|
264
|
+
process.off("SIGTERM", onSig);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// If no done event was seen (should not happen), exit 1
|
|
268
|
+
if (lastEvent === null) {
|
|
269
|
+
process.exitCode = 1;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (lastEvent.kind === "done") {
|
|
273
|
+
process.exitCode = exitCode;
|
|
274
|
+
} else {
|
|
275
|
+
// Stream ended without done (e.g., consumer break via head)
|
|
276
|
+
process.exitCode = interrupted ? 1 : 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (interrupted && process.exitCode === 0) process.exitCode = 1;
|
|
280
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { nodeRunnerDeps } from "../execution/node-deps.js";
|
|
4
|
+
import { CLOSE_GRACE_MS, openSession } from "../execution/open-session.js";
|
|
5
|
+
import { ArgvRefusalError } from "../interpretation/refusal.js";
|
|
6
|
+
import { createRenderState, renderEvent } from "./render.js";
|
|
7
|
+
import { resolveHarness } from "./resolve-harness.js";
|
|
8
|
+
|
|
9
|
+
export const session = async (harnessName: string, rawArgs: string[]): Promise<void> => {
|
|
10
|
+
// Only claude supported
|
|
11
|
+
if (harnessName !== "claude") {
|
|
12
|
+
const err = new ArgvRefusalError({
|
|
13
|
+
issue: "no-session-mode",
|
|
14
|
+
harness: harnessName as "claude",
|
|
15
|
+
supported: ["claude"],
|
|
16
|
+
detail: `session is claude-only; ${harnessName} declares no persistent headless session mode`,
|
|
17
|
+
});
|
|
18
|
+
process.stderr.write(`${err.message}\n`);
|
|
19
|
+
process.stderr.write(`supported: claude\n`);
|
|
20
|
+
process.exitCode = 2;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const h = resolveHarness(harnessName);
|
|
25
|
+
|
|
26
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
27
|
+
const { SESSION_HELP } = await import("./help.js");
|
|
28
|
+
process.stdout.write(SESSION_HELP);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { parseCommonFlags } = await import("./args.js");
|
|
33
|
+
let parsed: ReturnType<typeof parseCommonFlags>;
|
|
34
|
+
try {
|
|
35
|
+
parsed = parseCommonFlags(rawArgs);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
38
|
+
process.stderr.write(`unknown flag: ${message}\n`);
|
|
39
|
+
process.exitCode = 2;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const values = parsed.values as Record<string, unknown>;
|
|
44
|
+
const sessionId =
|
|
45
|
+
(values["session-id"] as string | undefined) ??
|
|
46
|
+
(values.resume as string | undefined) ??
|
|
47
|
+
randomUUID();
|
|
48
|
+
const model = values.model as string | undefined;
|
|
49
|
+
const cwd = values.cwd as string | undefined;
|
|
50
|
+
|
|
51
|
+
// Validate sessionId shape? let openSession handle via assertUsableSessionId
|
|
52
|
+
delete (process.env as Record<string, string | undefined>).HERDR_ENV;
|
|
53
|
+
|
|
54
|
+
const deps = nodeRunnerDeps();
|
|
55
|
+
|
|
56
|
+
let handle: ReturnType<typeof openSession>;
|
|
57
|
+
try {
|
|
58
|
+
handle = openSession(h, { sessionId, model, cwd }, deps);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err instanceof ArgvRefusalError) {
|
|
61
|
+
process.stderr.write(`${err.message}\n`);
|
|
62
|
+
if (err.supported.length) process.stderr.write(`supported: ${err.supported.join(", ")}\n`);
|
|
63
|
+
process.exitCode = 2;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
process.stderr.write(
|
|
67
|
+
`could not open session: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
68
|
+
);
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
process.stdout.write(
|
|
74
|
+
`interactive ${h.name} session ${sessionId}\n(empty line or "exit" to quit)\n`,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
78
|
+
|
|
79
|
+
// Handle SIGINT to close session cleanly
|
|
80
|
+
let closing = false;
|
|
81
|
+
const doClose = async () => {
|
|
82
|
+
if (closing) return;
|
|
83
|
+
closing = true;
|
|
84
|
+
try {
|
|
85
|
+
// race with CLOSE_GRACE_MS ? openSession's close does escalation internally, but we add outer guard
|
|
86
|
+
const timeout = setTimeout(() => {
|
|
87
|
+
process.stderr.write(`close timed out after ${CLOSE_GRACE_MS}ms\n`);
|
|
88
|
+
}, CLOSE_GRACE_MS + 1000);
|
|
89
|
+
await handle.close();
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
process.stderr.write(`close failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
93
|
+
}
|
|
94
|
+
rl.close();
|
|
95
|
+
};
|
|
96
|
+
process.on("SIGINT", doClose);
|
|
97
|
+
process.on("SIGTERM", doClose);
|
|
98
|
+
|
|
99
|
+
const turns = handle.turns[Symbol.asyncIterator]();
|
|
100
|
+
try {
|
|
101
|
+
while (true) {
|
|
102
|
+
let line: string;
|
|
103
|
+
try {
|
|
104
|
+
line = await rl.question("you › ");
|
|
105
|
+
} catch {
|
|
106
|
+
// readline closed (Ctrl-D)
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
const trimmed = line.trim();
|
|
110
|
+
if (trimmed === "" || trimmed === "exit") break;
|
|
111
|
+
|
|
112
|
+
const result = handle.send(line);
|
|
113
|
+
if (result.disposition === "queued") {
|
|
114
|
+
process.stderr.write(`disposition: queued (turn in progress)\n`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const turn = (await turns.next()).value as
|
|
118
|
+
| AsyncIterable<import("../execution/events.js").HarnessEvent>
|
|
119
|
+
| undefined;
|
|
120
|
+
if (turn === undefined) break;
|
|
121
|
+
const state = createRenderState();
|
|
122
|
+
for await (const event of turn) {
|
|
123
|
+
renderEvent(event, state);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch (err) {
|
|
127
|
+
process.stderr.write(`session error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
} finally {
|
|
130
|
+
process.off("SIGINT", doClose);
|
|
131
|
+
process.off("SIGTERM", doClose);
|
|
132
|
+
try {
|
|
133
|
+
await doClose();
|
|
134
|
+
} catch {}
|
|
135
|
+
rl.close();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
let cached: string | null = null;
|
|
6
|
+
|
|
7
|
+
export const getVersion = (): string => {
|
|
8
|
+
if (cached !== null) return cached;
|
|
9
|
+
// dist/cli.js is at dist/cli.js -> repo root is one level up from dist, or two from src/cli per build.
|
|
10
|
+
// Try to locate package.json via fileURLToPath.
|
|
11
|
+
try {
|
|
12
|
+
const current = fileURLToPath(import.meta.url);
|
|
13
|
+
// src/cli/version.ts -> ../../package.json ; dist/cli.js -> ../package.json
|
|
14
|
+
const candidates = [
|
|
15
|
+
resolve(dirname(current), "../../package.json"),
|
|
16
|
+
resolve(dirname(current), "../package.json"),
|
|
17
|
+
resolve(dirname(current), "../../../package.json"),
|
|
18
|
+
];
|
|
19
|
+
for (const p of candidates) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = readFileSync(p, "utf8");
|
|
22
|
+
const parsed = JSON.parse(raw) as { version?: string };
|
|
23
|
+
if (typeof parsed.version === "string") {
|
|
24
|
+
cached = parsed.version;
|
|
25
|
+
return cached;
|
|
26
|
+
}
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
cached = "0.0.0";
|
|
31
|
+
return cached;
|
|
32
|
+
};
|
package/src/execution/decode.ts
CHANGED
|
@@ -75,5 +75,37 @@ export const decodeParsed = (
|
|
|
75
75
|
// Content (message/token/tool/error) is per-harness; identity above is
|
|
76
76
|
// descriptor-driven. contentEventsOf dispatches by harness name.
|
|
77
77
|
for (const content of contentEventsOf(h.name, raw)) events.push(content);
|
|
78
|
+
|
|
79
|
+
// claude's rate_limit_event: only non-"allowed" statuses are failures.
|
|
80
|
+
// overageStatus is deliberately not classified - it is a separate
|
|
81
|
+
// billing signal, not a rate limit.
|
|
82
|
+
if (h.name === "claude" && typeof (raw as Record<string, unknown>).type === "string") {
|
|
83
|
+
const rec = raw as Record<string, unknown>;
|
|
84
|
+
if (rec.type === "rate_limit_event") {
|
|
85
|
+
const info = rec.rate_limit_info as Record<string, unknown> | undefined;
|
|
86
|
+
const status = info?.status as string | undefined;
|
|
87
|
+
if (status !== undefined && status !== "allowed") {
|
|
88
|
+
const rawResets = info?.resetsAt;
|
|
89
|
+
let resetsAt: number | undefined;
|
|
90
|
+
if (typeof rawResets === "number" && Number.isFinite(rawResets) && rawResets > 0) {
|
|
91
|
+
// Arithmetic conversion: resetsAt is seconds, we need milliseconds. No wall clock read.
|
|
92
|
+
resetsAt = rawResets * 1000;
|
|
93
|
+
if (!Number.isFinite(resetsAt) || resetsAt <= 0) resetsAt = undefined;
|
|
94
|
+
}
|
|
95
|
+
// Push a failure event directly - this is a structured record, not a wall scan
|
|
96
|
+
const failure: import("./failure.js").FailureSummary = {
|
|
97
|
+
class: "rate-limit",
|
|
98
|
+
retryable: true,
|
|
99
|
+
message: `Rate limit hit (rate_limit_event status=${status}) - retry after backoff or route to another provider`,
|
|
100
|
+
code: "rate-limit" as const,
|
|
101
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
102
|
+
};
|
|
103
|
+
events.push({
|
|
104
|
+
kind: "failure",
|
|
105
|
+
...failure,
|
|
106
|
+
} as unknown as import("./events.js").HarnessEvent);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
78
110
|
return events;
|
|
79
111
|
};
|
package/src/execution/deps.ts
CHANGED
|
@@ -29,6 +29,10 @@ export interface SpawnOptions {
|
|
|
29
29
|
/** `pipe` opens a writable stdin (sessions require it); `inherit` hands
|
|
30
30
|
* the child the parent's fd 0; `close` gives it nothing (pi's rule). */
|
|
31
31
|
readonly stdin: "inherit" | "close" | "pipe";
|
|
32
|
+
/** Merged over the parent environment by the spawn adapter. A key mapped
|
|
33
|
+
* to "" means DELETE the variable (not an empty string). Validated
|
|
34
|
+
* against /^[A-Za-z_][A-Za-z0-9_]*$/ and NUL-free. */
|
|
35
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
export type TimerHandle = number;
|
|
@@ -50,8 +54,10 @@ export interface RunnerDeps {
|
|
|
50
54
|
readonly spawn: (argv: readonly string[], opts: SpawnOptions) => SpawnedProcess;
|
|
51
55
|
readonly clock: Clock;
|
|
52
56
|
readonly signal: (proc: SpawnedProcess, sig: SignalName) => void;
|
|
53
|
-
/**
|
|
57
|
+
/** Inactivity budget; arms at every granularity and rearms on any output chunk. */
|
|
54
58
|
readonly stallMs?: number;
|
|
59
|
+
/** Wall-clock ceiling; armed once at spawn and never rearmed. */
|
|
60
|
+
readonly turnTimeoutMs?: number;
|
|
55
61
|
readonly log?: BoundaryLog;
|
|
56
62
|
/** Host-minted correlation id; without it the runner falls back to a
|
|
57
63
|
* per-process monotonic counter (collides across processes). */
|
package/src/execution/events.ts
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
* concatenate both.
|
|
16
16
|
*/
|
|
17
17
|
import type { CapabilityResult } from "../interpretation/capabilities.js";
|
|
18
|
+
import type { FailureSummary } from "./failure.js";
|
|
18
19
|
|
|
19
|
-
export type ExitCause = "clean" | "limit" | "crash" | "stall" | "killed";
|
|
20
|
+
export type ExitCause = "clean" | "limit" | "crash" | "stall" | "killed" | "failed";
|
|
20
21
|
|
|
21
22
|
export type HarnessEvent =
|
|
22
23
|
| {
|
|
@@ -32,6 +33,12 @@ export type HarnessEvent =
|
|
|
32
33
|
| { readonly kind: "context"; readonly usedPct: number }
|
|
33
34
|
| { readonly kind: "limit"; readonly code: string; readonly message: string }
|
|
34
35
|
| { readonly kind: "error"; readonly message: string }
|
|
35
|
-
| { readonly kind: "
|
|
36
|
+
| ({ readonly kind: "failure" } & FailureSummary)
|
|
37
|
+
| {
|
|
38
|
+
readonly kind: "done";
|
|
39
|
+
readonly exitCode: number | null;
|
|
40
|
+
readonly cause: ExitCause;
|
|
41
|
+
readonly failure?: FailureSummary;
|
|
42
|
+
};
|
|
36
43
|
|
|
37
44
|
export const DROPPABLE_KINDS = new Set(["token", "progress", "context"]);
|