@noir-ai/cli 1.14.0 → 1.15.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 +2 -2
- package/dist/bin.d.ts +10 -3
- package/dist/bin.js +1240 -365
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +39 -12
- package/dist/index.js +1258 -383
- package/dist/index.js.map +1 -1
- package/dist/{output-BNE9htEm.d.ts → output-2T2lGR7a.d.ts} +3 -3
- package/dist/tui/{chunk-6LO4LXEH.js → chunk-5KWETEMF.js} +1191 -352
- package/dist/tui/chunk-5KWETEMF.js.map +1 -0
- package/dist/tui/{chunk-ESZTOQEC.js → chunk-KETMM3BA.js} +25 -3
- package/dist/tui/chunk-KETMM3BA.js.map +1 -0
- package/dist/tui/{chunk-2RK6AFZQ.js → chunk-TPNJEN4B.js} +2 -2
- package/dist/tui/chunk-TPNJEN4B.js.map +1 -0
- package/dist/tui/{create-FD4E626Z.js → create-6KDIFPUL.js} +10 -6
- package/dist/tui/create-6KDIFPUL.js.map +1 -0
- package/dist/tui/index.js +994 -120
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/{install-YGTUSLRO.js → install-KA35ONQP.js} +2 -2
- package/dist/tui/{sync-N633SQSS.js → sync-AII5WCRO.js} +8 -4
- package/dist/tui/sync-AII5WCRO.js.map +1 -0
- package/dist/tui/{update-LET5BWOU.js → update-XGKJ4KBR.js} +3 -3
- package/package.json +11 -11
- package/dist/tui/chunk-2RK6AFZQ.js.map +0 -1
- package/dist/tui/chunk-6LO4LXEH.js.map +0 -1
- package/dist/tui/chunk-ESZTOQEC.js.map +0 -1
- package/dist/tui/create-FD4E626Z.js.map +0 -1
- package/dist/tui/sync-N633SQSS.js.map +0 -1
- /package/dist/tui/{install-YGTUSLRO.js.map → install-KA35ONQP.js.map} +0 -0
- /package/dist/tui/{update-LET5BWOU.js.map → update-XGKJ4KBR.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -235,16 +235,36 @@ function spinner(text = "", opts = {}) {
|
|
|
235
235
|
}
|
|
236
236
|
return new OraSpinner(text);
|
|
237
237
|
}
|
|
238
|
+
function failureEnvelope(exitCode, message) {
|
|
239
|
+
return `${JSON.stringify({ ok: false, error: { code: exitCode, message } })}
|
|
240
|
+
`;
|
|
241
|
+
}
|
|
238
242
|
function fail(exitCode, message, opts = {}) {
|
|
239
243
|
if (isJsonMode(opts)) {
|
|
240
|
-
process.stdout.write(
|
|
241
|
-
`);
|
|
244
|
+
process.stdout.write(failureEnvelope(exitCode, message));
|
|
242
245
|
} else if (message.length > 0) {
|
|
243
246
|
process.stderr.write(`${message}
|
|
244
247
|
`);
|
|
245
248
|
}
|
|
246
249
|
throw new CommanderError(exitCode, NOIR_ERROR_CODE, message);
|
|
247
250
|
}
|
|
251
|
+
function failAndExit(exitCode, message, opts = {}) {
|
|
252
|
+
process.exitCode = exitCode;
|
|
253
|
+
const jsonMode = isJsonMode(opts);
|
|
254
|
+
const text = jsonMode ? failureEnvelope(exitCode, message) : message.length > 0 ? `${message}
|
|
255
|
+
` : "";
|
|
256
|
+
if (text.length === 0) {
|
|
257
|
+
process.exit(exitCode);
|
|
258
|
+
}
|
|
259
|
+
let left = false;
|
|
260
|
+
const leave = () => {
|
|
261
|
+
if (left) return;
|
|
262
|
+
left = true;
|
|
263
|
+
process.exit(exitCode);
|
|
264
|
+
};
|
|
265
|
+
(jsonMode ? process.stdout : process.stderr).write(text, leave);
|
|
266
|
+
setTimeout(leave, EXIT_FLUSH_MS);
|
|
267
|
+
}
|
|
248
268
|
function requireInteractive(opts, hint) {
|
|
249
269
|
if (!isInteractive(opts)) {
|
|
250
270
|
fail(
|
|
@@ -285,7 +305,7 @@ function handleError(err) {
|
|
|
285
305
|
}
|
|
286
306
|
process.exitCode = inferExitCode(err);
|
|
287
307
|
}
|
|
288
|
-
var EXIT, NoirCliError, NOIR_ERROR_CODE, NoopSpinner, OraSpinner;
|
|
308
|
+
var EXIT, NoirCliError, NOIR_ERROR_CODE, NoopSpinner, OraSpinner, EXIT_FLUSH_MS;
|
|
289
309
|
var init_output = __esm({
|
|
290
310
|
"src/output.ts"() {
|
|
291
311
|
"use strict";
|
|
@@ -364,6 +384,7 @@ var init_output = __esm({
|
|
|
364
384
|
this.handle.info(text);
|
|
365
385
|
}
|
|
366
386
|
};
|
|
387
|
+
EXIT_FLUSH_MS = 250;
|
|
367
388
|
}
|
|
368
389
|
});
|
|
369
390
|
|
|
@@ -482,8 +503,8 @@ async function contextIndex(opts) {
|
|
|
482
503
|
}
|
|
483
504
|
renderIndex(data, opts.paths, opts);
|
|
484
505
|
}
|
|
485
|
-
function renderIndex(data,
|
|
486
|
-
const scope =
|
|
506
|
+
function renderIndex(data, paths5, opts) {
|
|
507
|
+
const scope = paths5 && paths5.length > 0 ? paths5.join(", ") : ".";
|
|
487
508
|
const flag = data.degraded ? ` ${badge("warn", "degraded: vectors skipped")}` : "";
|
|
488
509
|
log(`context index \u2014 ${scope}${flag}`, opts);
|
|
489
510
|
definitionList(
|
|
@@ -1242,12 +1263,12 @@ function describeSource(row) {
|
|
|
1242
1263
|
return row.shadowed === true ? ".noir/.env (shadows environment)" : ".noir/.env";
|
|
1243
1264
|
}
|
|
1244
1265
|
async function env(opts = {}) {
|
|
1245
|
-
const
|
|
1266
|
+
const report2 = resolveEnv(process.cwd(), ambientBaseline ?? process.env);
|
|
1246
1267
|
if (opts.json === true) {
|
|
1247
|
-
json({ ok: true, data: { vars:
|
|
1268
|
+
json({ ok: true, data: { vars: report2.vars, warnings: report2.warnings } });
|
|
1248
1269
|
return;
|
|
1249
1270
|
}
|
|
1250
|
-
if (
|
|
1271
|
+
if (report2.vars.length === 0) {
|
|
1251
1272
|
info(
|
|
1252
1273
|
"nothing to report \u2014 .noir/.env defines no keys in effect and no Noir-relevant environment variable is set",
|
|
1253
1274
|
opts
|
|
@@ -1255,10 +1276,10 @@ async function env(opts = {}) {
|
|
|
1255
1276
|
return;
|
|
1256
1277
|
}
|
|
1257
1278
|
table(
|
|
1258
|
-
|
|
1279
|
+
report2.vars.map((row) => ({
|
|
1259
1280
|
KEY: row.key,
|
|
1260
1281
|
SOURCE: describeSource(row),
|
|
1261
|
-
VALUE:
|
|
1282
|
+
VALUE: report2.shapes[row.key] ?? ""
|
|
1262
1283
|
})),
|
|
1263
1284
|
["KEY", "SOURCE", "VALUE"],
|
|
1264
1285
|
opts
|
|
@@ -1276,6 +1297,9 @@ var init_env = __esm({
|
|
|
1276
1297
|
"COHERE_API_KEY",
|
|
1277
1298
|
"OLLAMA_BASE_URL",
|
|
1278
1299
|
"ANTHROPIC_API_KEY",
|
|
1300
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
1301
|
+
"ANTHROPIC_BASE_URL",
|
|
1302
|
+
"API_TIMEOUT_MS",
|
|
1279
1303
|
"NOIR_PROFILE"
|
|
1280
1304
|
];
|
|
1281
1305
|
SHORT_VALUE_MAX = 8;
|
|
@@ -2330,7 +2354,7 @@ async function taskVerify(opts) {
|
|
|
2330
2354
|
`${JSON.stringify({
|
|
2331
2355
|
ok: false,
|
|
2332
2356
|
error: { code: EXIT.ERROR, message: `task verify: ${detail}` },
|
|
2333
|
-
//
|
|
2357
|
+
// `{ok:false, error}` + a payload — recovery context + evidence nest
|
|
2334
2358
|
// under `data`, not as top-level siblings (same envelope shape as the
|
|
2335
2359
|
// success path; consumers read `data` for the full picture).
|
|
2336
2360
|
data: {
|
|
@@ -2448,7 +2472,7 @@ var init_task = __esm({
|
|
|
2448
2472
|
init_output();
|
|
2449
2473
|
init_theme();
|
|
2450
2474
|
PHASE_SKILL = {
|
|
2451
|
-
//
|
|
2475
|
+
// Skill curation: intake+clarify merged into brainstorming;
|
|
2452
2476
|
// execute → executing-plans; verify → verifying; document → wrap.
|
|
2453
2477
|
intake: "noir-brainstorming",
|
|
2454
2478
|
clarify: "noir-brainstorming",
|
|
@@ -2717,6 +2741,10 @@ function capitalizeFirst(s) {
|
|
|
2717
2741
|
function hasAction(cmd) {
|
|
2718
2742
|
return cmd._actionHandler != null;
|
|
2719
2743
|
}
|
|
2744
|
+
function requiredArgLabel(cmd) {
|
|
2745
|
+
const required = cmd.registeredArguments.find((arg) => arg.required);
|
|
2746
|
+
return required?.name();
|
|
2747
|
+
}
|
|
2720
2748
|
function collectLeaves(cmd, path, out) {
|
|
2721
2749
|
const children = cmd.commands;
|
|
2722
2750
|
const isLeaf = hasAction(cmd) && children.length === 0;
|
|
@@ -2731,7 +2759,8 @@ function collectLeaves(cmd, path, out) {
|
|
|
2731
2759
|
description,
|
|
2732
2760
|
category: argv[0] ?? "",
|
|
2733
2761
|
keywords: argv,
|
|
2734
|
-
destructive: isDestructive(argv)
|
|
2762
|
+
destructive: isDestructive(argv),
|
|
2763
|
+
needsArg: requiredArgLabel(cmd)
|
|
2735
2764
|
});
|
|
2736
2765
|
return;
|
|
2737
2766
|
}
|
|
@@ -2844,7 +2873,15 @@ var init_sections = __esm({
|
|
|
2844
2873
|
label: "Save memory",
|
|
2845
2874
|
hint: "save an observation \xB7 \u26A0 destructive",
|
|
2846
2875
|
destructive: true,
|
|
2847
|
-
|
|
2876
|
+
// The value travels as `--content <text>` (memory save declares no
|
|
2877
|
+
// positional), so the flag rides in the dispatch prefix and the
|
|
2878
|
+
// collected value is appended after it.
|
|
2879
|
+
dispatch: ["memory", "save", "--content"],
|
|
2880
|
+
needsArg: {
|
|
2881
|
+
label: "content",
|
|
2882
|
+
prompt: "Memory content:",
|
|
2883
|
+
placeholder: "what you want to remember"
|
|
2884
|
+
}
|
|
2848
2885
|
},
|
|
2849
2886
|
{
|
|
2850
2887
|
id: "memory sessions",
|
|
@@ -2868,8 +2905,21 @@ var init_sections = __esm({
|
|
|
2868
2905
|
{
|
|
2869
2906
|
id: "workflow",
|
|
2870
2907
|
label: "Workflow",
|
|
2871
|
-
hint: "next \xB7 status \xB7 advance \xB7 handoff \xB7 wrap",
|
|
2908
|
+
hint: "run \xB7 next \xB7 status \xB7 advance \xB7 handoff \xB7 wrap",
|
|
2872
2909
|
items: [
|
|
2910
|
+
{
|
|
2911
|
+
id: "run",
|
|
2912
|
+
label: "Ask the host",
|
|
2913
|
+
hint: "ask the host agent a question \xB7 \u26A0 destructive",
|
|
2914
|
+
destructive: true,
|
|
2915
|
+
// The prompt is not a required argument (the command also takes flags
|
|
2916
|
+
// like --list-profiles), so the palette only learns to ask for it here.
|
|
2917
|
+
needsArg: {
|
|
2918
|
+
label: "prompt",
|
|
2919
|
+
prompt: "Prompt for the host:",
|
|
2920
|
+
placeholder: "e.g. summarise the auth flow"
|
|
2921
|
+
}
|
|
2922
|
+
},
|
|
2873
2923
|
{
|
|
2874
2924
|
id: "task next",
|
|
2875
2925
|
label: "Next task",
|
|
@@ -3721,6 +3771,15 @@ var init_release = __esm({
|
|
|
3721
3771
|
}
|
|
3722
3772
|
});
|
|
3723
3773
|
|
|
3774
|
+
// src/run-timing.ts
|
|
3775
|
+
var INTERRUPT_GRACE_MS;
|
|
3776
|
+
var init_run_timing = __esm({
|
|
3777
|
+
"src/run-timing.ts"() {
|
|
3778
|
+
"use strict";
|
|
3779
|
+
INTERRUPT_GRACE_MS = 5e3;
|
|
3780
|
+
}
|
|
3781
|
+
});
|
|
3782
|
+
|
|
3724
3783
|
// src/shell-bridge.ts
|
|
3725
3784
|
import { spawn } from "child_process";
|
|
3726
3785
|
import { basename } from "path";
|
|
@@ -3755,7 +3814,7 @@ function shellKind(shell) {
|
|
|
3755
3814
|
function probeCommand(name, shell, opts) {
|
|
3756
3815
|
const kind = shellKind(shell);
|
|
3757
3816
|
const timeoutMs = opts.timeoutMs ?? PROBE_TIMEOUT_MS2;
|
|
3758
|
-
return new Promise((
|
|
3817
|
+
return new Promise((resolve3) => {
|
|
3759
3818
|
const script = kind ? PROBE_SCRIPTS[kind] : "";
|
|
3760
3819
|
const flags = kind ? PROBE_FLAGS[kind] : "-lic";
|
|
3761
3820
|
const probeArgs = kind === "fish" ? [name] : ["noirbridge", name];
|
|
@@ -3778,15 +3837,15 @@ function probeCommand(name, shell, opts) {
|
|
|
3778
3837
|
} catch {
|
|
3779
3838
|
}
|
|
3780
3839
|
}
|
|
3781
|
-
|
|
3840
|
+
resolve3({ stdout, timedOut: true });
|
|
3782
3841
|
}, timeoutMs);
|
|
3783
3842
|
child.on("error", () => {
|
|
3784
3843
|
clearTimeout(timer);
|
|
3785
|
-
|
|
3844
|
+
resolve3({ stdout, timedOut: false });
|
|
3786
3845
|
});
|
|
3787
3846
|
child.on("close", () => {
|
|
3788
3847
|
clearTimeout(timer);
|
|
3789
|
-
|
|
3848
|
+
resolve3({ stdout, timedOut: false });
|
|
3790
3849
|
});
|
|
3791
3850
|
});
|
|
3792
3851
|
}
|
|
@@ -3882,18 +3941,23 @@ function usageFrom(v) {
|
|
|
3882
3941
|
if (input === void 0 && output === void 0 && cache === void 0) return void 0;
|
|
3883
3942
|
return { inputTokens: input, outputTokens: output, cacheReadTokens: cache };
|
|
3884
3943
|
}
|
|
3885
|
-
function
|
|
3886
|
-
if (!message) return
|
|
3944
|
+
function messageContent(message) {
|
|
3945
|
+
if (!message) return {};
|
|
3887
3946
|
const content = message.content;
|
|
3888
|
-
if (typeof content === "string") return content;
|
|
3889
|
-
if (Array.isArray(content)) {
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
if (
|
|
3947
|
+
if (typeof content === "string") return { text: content };
|
|
3948
|
+
if (!Array.isArray(content)) return {};
|
|
3949
|
+
const parts = [];
|
|
3950
|
+
const tools = [];
|
|
3951
|
+
for (const block of content) {
|
|
3952
|
+
const b = asObj(block);
|
|
3953
|
+
if (!b) continue;
|
|
3954
|
+
if (typeof b.text === "string") parts.push(b.text);
|
|
3955
|
+
else if (b.type === "tool_use" && typeof b.name === "string") tools.push(b.name);
|
|
3895
3956
|
}
|
|
3896
|
-
return
|
|
3957
|
+
return {
|
|
3958
|
+
...parts.length > 0 ? { text: parts.join("") } : {},
|
|
3959
|
+
...tools.length > 0 ? { tools } : {}
|
|
3960
|
+
};
|
|
3897
3961
|
}
|
|
3898
3962
|
function parseStreamLine(line) {
|
|
3899
3963
|
const trimmed = line.trim();
|
|
@@ -3912,15 +3976,14 @@ function normalizeStreamEvent(raw) {
|
|
|
3912
3976
|
if (r.subtype === "init") {
|
|
3913
3977
|
return { kind: "init", sessionId: str(r.session_id), model: str(r.model) };
|
|
3914
3978
|
}
|
|
3915
|
-
return { kind: "other" };
|
|
3979
|
+
return { kind: "other", subtype: str(r.subtype) ?? "system" };
|
|
3916
3980
|
}
|
|
3917
3981
|
if (type === "assistant") {
|
|
3918
3982
|
const message = asObj(r.message);
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
usage: message ? usageFrom(message.usage) : void 0,
|
|
3983
|
+
const content = messageContent(message);
|
|
3984
|
+
const messageId = message ? str(message.id) : void 0;
|
|
3985
|
+
const usage = message ? usageFrom(message.usage) : void 0;
|
|
3986
|
+
const error2 = {
|
|
3924
3987
|
// An API error message is a diagnostic, not the answer — flag it so the
|
|
3925
3988
|
// caller never streams it as assistant text (optional-when-true keeps
|
|
3926
3989
|
// exact-shape consumers of the existing union intact).
|
|
@@ -3929,6 +3992,20 @@ function normalizeStreamEvent(raw) {
|
|
|
3929
3992
|
...typeof r.error === "string" ? { errorCategory: r.error } : {}
|
|
3930
3993
|
} : {}
|
|
3931
3994
|
};
|
|
3995
|
+
if (content.text !== void 0) {
|
|
3996
|
+
return {
|
|
3997
|
+
kind: "assistant",
|
|
3998
|
+
messageId,
|
|
3999
|
+
text: content.text,
|
|
4000
|
+
usage,
|
|
4001
|
+
...content.tools === void 0 ? {} : { tools: content.tools },
|
|
4002
|
+
...error2
|
|
4003
|
+
};
|
|
4004
|
+
}
|
|
4005
|
+
if (content.tools !== void 0) {
|
|
4006
|
+
return { kind: "tool", name: content.tools[0], messageId, usage };
|
|
4007
|
+
}
|
|
4008
|
+
return { kind: "assistant", messageId, usage, ...error2 };
|
|
3932
4009
|
}
|
|
3933
4010
|
if (type === "result") {
|
|
3934
4011
|
return {
|
|
@@ -3940,7 +4017,33 @@ function normalizeStreamEvent(raw) {
|
|
|
3940
4017
|
usage: usageFrom(r.usage)
|
|
3941
4018
|
};
|
|
3942
4019
|
}
|
|
3943
|
-
|
|
4020
|
+
if (type === "stream_event") {
|
|
4021
|
+
return normalizeStreamEventInner(asObj(r.event));
|
|
4022
|
+
}
|
|
4023
|
+
return { kind: "other", ...typeof type === "string" ? { subtype: type } : {} };
|
|
4024
|
+
}
|
|
4025
|
+
function normalizeStreamEventInner(inner) {
|
|
4026
|
+
if (!inner) return { kind: "other", subtype: "stream_event" };
|
|
4027
|
+
const innerType = str(inner.type);
|
|
4028
|
+
if (innerType === "content_block_start") {
|
|
4029
|
+
const block = asObj(inner.content_block);
|
|
4030
|
+
const name = block ? str(block.name) : void 0;
|
|
4031
|
+
if (block && str(block.type) === "tool_use" && name !== void 0) {
|
|
4032
|
+
return { kind: "tool", name };
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
if (innerType === "content_block_delta") {
|
|
4036
|
+
const delta = asObj(inner.delta);
|
|
4037
|
+
const text = delta ? str(delta.text) : void 0;
|
|
4038
|
+
if (delta && delta.type === "text_delta" && text !== void 0) {
|
|
4039
|
+
return { kind: "delta", text };
|
|
4040
|
+
}
|
|
4041
|
+
}
|
|
4042
|
+
return { kind: "other", subtype: innerType ?? "stream_event" };
|
|
4043
|
+
}
|
|
4044
|
+
function signalChild(child, signal) {
|
|
4045
|
+
if (child.pid === void 0) return;
|
|
4046
|
+
child.kill(signal);
|
|
3944
4047
|
}
|
|
3945
4048
|
function runHost(opts) {
|
|
3946
4049
|
const spec = resolveHostRun(opts.host, opts.customBinary);
|
|
@@ -3956,12 +4059,13 @@ function runHost(opts) {
|
|
|
3956
4059
|
false
|
|
3957
4060
|
);
|
|
3958
4061
|
}
|
|
3959
|
-
function spawnAndConsume(binary, args, opts, shellRun) {
|
|
3960
|
-
return new Promise((
|
|
4062
|
+
function spawnAndConsume(binary, args, opts, shellRun, bridged = false) {
|
|
4063
|
+
return new Promise((resolve3, reject) => {
|
|
3961
4064
|
const child = spawn2(binary, [...args], {
|
|
3962
4065
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3963
4066
|
...opts.env ? { env: opts.env } : {}
|
|
3964
4067
|
});
|
|
4068
|
+
opts.onChild?.(child);
|
|
3965
4069
|
const reducer = new UsageReducer();
|
|
3966
4070
|
let eventCount = 0;
|
|
3967
4071
|
let stderrBuf = "";
|
|
@@ -3969,10 +4073,40 @@ function spawnAndConsume(binary, args, opts, shellRun) {
|
|
|
3969
4073
|
let errorText;
|
|
3970
4074
|
let errorCategory;
|
|
3971
4075
|
let spawnError = null;
|
|
4076
|
+
const { signal } = opts;
|
|
4077
|
+
let resend;
|
|
4078
|
+
let offersLeft = Math.ceil(BRIDGE_POLITE_WINDOW_MS / BRIDGE_POLITE_RESEND_MS);
|
|
4079
|
+
const stopResend = () => {
|
|
4080
|
+
if (resend === void 0) return;
|
|
4081
|
+
clearTimeout(resend);
|
|
4082
|
+
resend = void 0;
|
|
4083
|
+
};
|
|
4084
|
+
const offerAgain = () => {
|
|
4085
|
+
if (offersLeft <= 0) return;
|
|
4086
|
+
offersLeft -= 1;
|
|
4087
|
+
resend = setTimeout(() => {
|
|
4088
|
+
resend = void 0;
|
|
4089
|
+
signalChild(child, "SIGTERM");
|
|
4090
|
+
offerAgain();
|
|
4091
|
+
}, BRIDGE_POLITE_RESEND_MS);
|
|
4092
|
+
};
|
|
4093
|
+
const onAbort = () => {
|
|
4094
|
+
signalChild(child, "SIGTERM");
|
|
4095
|
+
if (bridged) offerAgain();
|
|
4096
|
+
};
|
|
4097
|
+
if (signal) {
|
|
4098
|
+
if (signal.aborted) onAbort();
|
|
4099
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
4100
|
+
}
|
|
4101
|
+
const detachAbort = () => {
|
|
4102
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4103
|
+
stopResend();
|
|
4104
|
+
};
|
|
3972
4105
|
child.on("error", (err) => {
|
|
3973
4106
|
spawnError = err;
|
|
4107
|
+
detachAbort();
|
|
3974
4108
|
if (!shellRun && err.code === "ENOENT") {
|
|
3975
|
-
void shellFallback(binary, args, opts,
|
|
4109
|
+
void shellFallback(binary, args, opts, resolve3, reject, err);
|
|
3976
4110
|
return;
|
|
3977
4111
|
}
|
|
3978
4112
|
reject(err);
|
|
@@ -3999,41 +4133,43 @@ function spawnAndConsume(binary, args, opts, shellRun) {
|
|
|
3999
4133
|
child.stderr.on("data", (chunk) => {
|
|
4000
4134
|
stderrBuf += chunk;
|
|
4001
4135
|
});
|
|
4002
|
-
child.on("close", (code,
|
|
4136
|
+
child.on("close", (code, signal2) => {
|
|
4137
|
+
detachAbort();
|
|
4003
4138
|
if (spawnError !== null) return;
|
|
4004
|
-
const killed = code === null &&
|
|
4005
|
-
|
|
4006
|
-
exitCode: killed ? 128 + (signalNumber(
|
|
4139
|
+
const killed = code === null && signal2 != null;
|
|
4140
|
+
resolve3({
|
|
4141
|
+
exitCode: killed ? 128 + (signalNumber(signal2) ?? 0) : code ?? 0,
|
|
4007
4142
|
usage: reducer.snapshot(),
|
|
4008
4143
|
eventCount,
|
|
4009
4144
|
stderr: stderrBuf,
|
|
4010
4145
|
isError: errored || killed,
|
|
4011
|
-
errorText: killed && errorText === void 0 ? `terminated by signal ${
|
|
4146
|
+
errorText: killed && errorText === void 0 ? `terminated by signal ${signal2}` : errorText,
|
|
4012
4147
|
errorCategory
|
|
4013
4148
|
});
|
|
4014
4149
|
});
|
|
4015
4150
|
});
|
|
4016
4151
|
}
|
|
4017
|
-
async function shellFallback(binary, args, opts,
|
|
4152
|
+
async function shellFallback(binary, args, opts, resolve3, reject, originalErr) {
|
|
4018
4153
|
try {
|
|
4019
4154
|
const res = await resolveCommandViaShell(binary, { env: opts.env ?? process.env });
|
|
4020
4155
|
if (res.kind === "path") {
|
|
4021
|
-
|
|
4156
|
+
resolve3(await spawnAndConsume(res.path, args, opts, true));
|
|
4022
4157
|
return;
|
|
4023
4158
|
}
|
|
4024
4159
|
if (res.kind === "alias" || res.kind === "function") {
|
|
4025
4160
|
const bridge = buildBridgeArgs(binary, args, res.shell);
|
|
4026
|
-
|
|
4161
|
+
resolve3(await spawnAndConsume(bridge.binary, bridge.args, opts, true, true));
|
|
4027
4162
|
return;
|
|
4028
4163
|
}
|
|
4029
4164
|
} catch {
|
|
4030
4165
|
}
|
|
4031
4166
|
reject(originalErr);
|
|
4032
4167
|
}
|
|
4033
|
-
var HOST_BINARY, HOST_FLAGS, UsageReducer;
|
|
4168
|
+
var HOST_BINARY, HOST_FLAGS, UsageReducer, BRIDGE_POLITE_RESEND_MS, BRIDGE_POLITE_WINDOW_MS;
|
|
4034
4169
|
var init_orchestrator = __esm({
|
|
4035
4170
|
"src/orchestrator.ts"() {
|
|
4036
4171
|
"use strict";
|
|
4172
|
+
init_run_timing();
|
|
4037
4173
|
init_shell_bridge();
|
|
4038
4174
|
HOST_BINARY = {
|
|
4039
4175
|
claude: "claude",
|
|
@@ -4044,7 +4180,7 @@ var init_orchestrator = __esm({
|
|
|
4044
4180
|
// emits AGENTS.md — a file host, not a spawnable CLI
|
|
4045
4181
|
};
|
|
4046
4182
|
HOST_FLAGS = {
|
|
4047
|
-
claude: ["-p", "--output-format", "stream-json", "--verbose"],
|
|
4183
|
+
claude: ["-p", "--output-format", "stream-json", "--verbose", "--include-partial-messages"],
|
|
4048
4184
|
gemini: ["-p", "--output-format", "stream-json"],
|
|
4049
4185
|
opencode: ["run"],
|
|
4050
4186
|
cursor: ["-p"],
|
|
@@ -4058,27 +4194,29 @@ var init_orchestrator = __esm({
|
|
|
4058
4194
|
turns = 0;
|
|
4059
4195
|
/** Feed one normalized event into the reducer. */
|
|
4060
4196
|
add(event) {
|
|
4061
|
-
if (event.kind === "
|
|
4062
|
-
const key = event.messageId;
|
|
4063
|
-
const input = event.usage.inputTokens ?? 0;
|
|
4064
|
-
const output = event.usage.outputTokens ?? 0;
|
|
4065
|
-
const prev = this.maxByMessage.get(key);
|
|
4066
|
-
if (!prev) {
|
|
4067
|
-
this.maxByMessage.set(key, { input, output });
|
|
4068
|
-
this.input += input;
|
|
4069
|
-
this.output += output;
|
|
4070
|
-
} else {
|
|
4071
|
-
this.input += Math.max(0, input - prev.input);
|
|
4072
|
-
this.output += Math.max(0, output - prev.output);
|
|
4073
|
-
this.maxByMessage.set(key, {
|
|
4074
|
-
input: Math.max(prev.input, input),
|
|
4075
|
-
output: Math.max(prev.output, output)
|
|
4076
|
-
});
|
|
4077
|
-
}
|
|
4078
|
-
} else if (event.kind === "result") {
|
|
4197
|
+
if (event.kind === "result") {
|
|
4079
4198
|
if (event.totalCostUsd !== void 0) this.costUsd = event.totalCostUsd;
|
|
4080
4199
|
if (event.numTurns !== void 0) this.turns = event.numTurns;
|
|
4200
|
+
return;
|
|
4201
|
+
}
|
|
4202
|
+
const usage = event.kind === "assistant" || event.kind === "tool" ? event.usage : void 0;
|
|
4203
|
+
const key = event.kind === "assistant" || event.kind === "tool" ? event.messageId : void 0;
|
|
4204
|
+
if (!key || !usage) return;
|
|
4205
|
+
const input = usage.inputTokens ?? 0;
|
|
4206
|
+
const output = usage.outputTokens ?? 0;
|
|
4207
|
+
const prev = this.maxByMessage.get(key);
|
|
4208
|
+
if (!prev) {
|
|
4209
|
+
this.maxByMessage.set(key, { input, output });
|
|
4210
|
+
this.input += input;
|
|
4211
|
+
this.output += output;
|
|
4212
|
+
return;
|
|
4081
4213
|
}
|
|
4214
|
+
this.input += Math.max(0, input - prev.input);
|
|
4215
|
+
this.output += Math.max(0, output - prev.output);
|
|
4216
|
+
this.maxByMessage.set(key, {
|
|
4217
|
+
input: Math.max(prev.input, input),
|
|
4218
|
+
output: Math.max(prev.output, output)
|
|
4219
|
+
});
|
|
4082
4220
|
}
|
|
4083
4221
|
/** The current accumulator snapshot. */
|
|
4084
4222
|
snapshot() {
|
|
@@ -4090,11 +4228,362 @@ var init_orchestrator = __esm({
|
|
|
4090
4228
|
};
|
|
4091
4229
|
}
|
|
4092
4230
|
};
|
|
4231
|
+
BRIDGE_POLITE_RESEND_MS = 250;
|
|
4232
|
+
BRIDGE_POLITE_WINDOW_MS = INTERRUPT_GRACE_MS;
|
|
4233
|
+
}
|
|
4234
|
+
});
|
|
4235
|
+
|
|
4236
|
+
// src/run-actions.ts
|
|
4237
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
4238
|
+
import { resolve } from "path";
|
|
4239
|
+
import { CommanderError as CommanderError2 } from "commander";
|
|
4240
|
+
function postRunActions(input) {
|
|
4241
|
+
const answer = input.answer.trim();
|
|
4242
|
+
const canContinue = input.canContinue ?? (input.sessionId !== void 0 && input.resume !== void 0);
|
|
4243
|
+
const options = [];
|
|
4244
|
+
if (answer.length > 0) {
|
|
4245
|
+
options.push({
|
|
4246
|
+
value: "memory",
|
|
4247
|
+
label: "Save answer to memory",
|
|
4248
|
+
hint: "distil it into this project's memory (needs the daemon)"
|
|
4249
|
+
});
|
|
4250
|
+
options.push({
|
|
4251
|
+
value: "research",
|
|
4252
|
+
label: "Add as task research",
|
|
4253
|
+
hint: "record it as a finding on the active task"
|
|
4254
|
+
});
|
|
4255
|
+
}
|
|
4256
|
+
options.push({
|
|
4257
|
+
value: "handoff",
|
|
4258
|
+
label: "Write a handoff artifact",
|
|
4259
|
+
hint: "snapshot the task, the next step and a seed for your host"
|
|
4260
|
+
});
|
|
4261
|
+
if (canContinue) {
|
|
4262
|
+
options.push({
|
|
4263
|
+
value: "resume",
|
|
4264
|
+
label: "Continue this session",
|
|
4265
|
+
hint: `ask ${input.host} a follow-up in the same session`,
|
|
4266
|
+
needsValue: {
|
|
4267
|
+
label: "follow-up prompt",
|
|
4268
|
+
placeholder: "e.g. now write the tests for that"
|
|
4269
|
+
}
|
|
4270
|
+
});
|
|
4271
|
+
}
|
|
4272
|
+
if (answer.length > 0) {
|
|
4273
|
+
options.push({
|
|
4274
|
+
value: "save",
|
|
4275
|
+
label: "Save answer to a file",
|
|
4276
|
+
hint: "write the answer text to a path you choose",
|
|
4277
|
+
needsValue: { label: "file path", placeholder: "e.g. notes/answer.md" }
|
|
4278
|
+
});
|
|
4279
|
+
}
|
|
4280
|
+
options.push({
|
|
4281
|
+
value: "dismiss",
|
|
4282
|
+
label: "Dismiss",
|
|
4283
|
+
hint: "nothing further \u2014 the run is already complete"
|
|
4284
|
+
});
|
|
4285
|
+
return options;
|
|
4286
|
+
}
|
|
4287
|
+
async function performPostRunAction(choice, input, value) {
|
|
4288
|
+
const label = ACTION_LABEL[choice];
|
|
4289
|
+
try {
|
|
4290
|
+
switch (choice) {
|
|
4291
|
+
case "memory":
|
|
4292
|
+
await saveToMemory(input);
|
|
4293
|
+
return { ok: true, message: "" };
|
|
4294
|
+
case "research":
|
|
4295
|
+
await recordAsResearch(input);
|
|
4296
|
+
return { ok: true, message: "" };
|
|
4297
|
+
case "handoff":
|
|
4298
|
+
await writeHandoff(input);
|
|
4299
|
+
return { ok: true, message: "" };
|
|
4300
|
+
case "resume":
|
|
4301
|
+
return await continueSession(input, value);
|
|
4302
|
+
case "save":
|
|
4303
|
+
return saveAnswerToFile(input, value);
|
|
4304
|
+
case "dismiss":
|
|
4305
|
+
return { ok: true, message: "" };
|
|
4306
|
+
}
|
|
4307
|
+
} catch (err) {
|
|
4308
|
+
const quoted = err instanceof CommanderError2 || err instanceof NoirCliError ? void 0 : `${label}: ${err instanceof Error ? err.message : String(err)}`;
|
|
4309
|
+
return {
|
|
4310
|
+
ok: false,
|
|
4311
|
+
message: `${label} did not complete \u2014 the run's result stands.`,
|
|
4312
|
+
...quoted === void 0 ? {} : { detail: quoted }
|
|
4313
|
+
};
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
async function offerPostRunActions(input) {
|
|
4317
|
+
if (!isInteractive(input.opts)) return;
|
|
4318
|
+
const options = postRunActions(input);
|
|
4319
|
+
if (options.length <= 1) return;
|
|
4320
|
+
const menu = await openMenu(options);
|
|
4321
|
+
if (menu === void 0) return;
|
|
4322
|
+
const chosen = options.find((o) => o.value === menu.choice);
|
|
4323
|
+
if (chosen === void 0) return;
|
|
4324
|
+
const value = chosen.needsValue === void 0 ? void 0 : await askForValue(menu.clack, chosen, input);
|
|
4325
|
+
if (chosen.needsValue !== void 0 && value === void 0) return;
|
|
4326
|
+
report(await performPostRunAction(menu.choice, input, value), input.opts);
|
|
4327
|
+
}
|
|
4328
|
+
function report(outcome, opts) {
|
|
4329
|
+
const caution = outcome.tone === "caution" || !outcome.ok && outcome.tone !== "confirm";
|
|
4330
|
+
if (outcome.detail !== void 0) warn(outcome.detail, opts);
|
|
4331
|
+
if (outcome.message.length === 0) return;
|
|
4332
|
+
if (caution) warn(outcome.message, opts);
|
|
4333
|
+
else success(outcome.message, opts);
|
|
4334
|
+
}
|
|
4335
|
+
async function openMenu(options) {
|
|
4336
|
+
try {
|
|
4337
|
+
const clack = await import("@clack/prompts");
|
|
4338
|
+
const choice = await clack.select({
|
|
4339
|
+
message: "The run finished. What next?",
|
|
4340
|
+
initialValue: "dismiss",
|
|
4341
|
+
// Spread because the prompt library takes a mutable list; the rows
|
|
4342
|
+
// themselves are the shared descriptors and are never rewritten.
|
|
4343
|
+
options: [...options]
|
|
4344
|
+
});
|
|
4345
|
+
if (clack.isCancel(choice)) return void 0;
|
|
4346
|
+
return { clack, choice };
|
|
4347
|
+
} catch {
|
|
4348
|
+
return void 0;
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
async function askForValue(clack, option, input) {
|
|
4352
|
+
const needs = option.needsValue;
|
|
4353
|
+
if (needs === void 0) return void 0;
|
|
4354
|
+
const message = option.value === "save" ? `Write the answer to a file (the raw transcript is at ${input.transcript})` : `What should ${input.host} do next?`;
|
|
4355
|
+
try {
|
|
4356
|
+
const written = await clack.text({ message, placeholder: needs.placeholder });
|
|
4357
|
+
if (clack.isCancel(written)) return void 0;
|
|
4358
|
+
return String(written ?? "");
|
|
4359
|
+
} catch {
|
|
4360
|
+
return void 0;
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
async function saveToMemory(input) {
|
|
4364
|
+
await memoryCapture({
|
|
4365
|
+
...input.opts,
|
|
4366
|
+
content: input.answer,
|
|
4367
|
+
eventType: CAPTURE_EVENT
|
|
4368
|
+
});
|
|
4369
|
+
}
|
|
4370
|
+
async function recordAsResearch(input) {
|
|
4371
|
+
await taskResearchRecord({
|
|
4372
|
+
...input.opts,
|
|
4373
|
+
type: "discovery",
|
|
4374
|
+
text: researchText(input.answer),
|
|
4375
|
+
source: RESEARCH_SOURCE
|
|
4376
|
+
});
|
|
4377
|
+
}
|
|
4378
|
+
function researchText(answer) {
|
|
4379
|
+
const condensed = answer.replace(/\s+/g, " ").trim();
|
|
4380
|
+
if (condensed.length <= RESEARCH_TEXT_LIMIT) return condensed;
|
|
4381
|
+
return `${condensed.slice(0, RESEARCH_TEXT_LIMIT - 1)}\u2026`;
|
|
4382
|
+
}
|
|
4383
|
+
async function writeHandoff(input) {
|
|
4384
|
+
await handoff({ ...input.opts, write: true });
|
|
4385
|
+
}
|
|
4386
|
+
async function continueSession(input, value) {
|
|
4387
|
+
const { sessionId, resume } = input;
|
|
4388
|
+
if (sessionId === void 0 || resume === void 0) return { ok: true, message: "" };
|
|
4389
|
+
const prompt = (value ?? "").trim();
|
|
4390
|
+
if (prompt.length === 0) {
|
|
4391
|
+
return {
|
|
4392
|
+
ok: true,
|
|
4393
|
+
tone: "caution",
|
|
4394
|
+
message: "No prompt given \u2014 the session was not continued."
|
|
4395
|
+
};
|
|
4396
|
+
}
|
|
4397
|
+
await resume(prompt, sessionId);
|
|
4398
|
+
return { ok: true, message: "" };
|
|
4399
|
+
}
|
|
4400
|
+
function saveAnswerToFile(input, value) {
|
|
4401
|
+
const fallback = `noir-run-${stamp2()}.md`;
|
|
4402
|
+
const chosen = (value ?? "").trim();
|
|
4403
|
+
const target = resolve(chosen.length > 0 ? chosen : fallback);
|
|
4404
|
+
const body = input.answer.endsWith("\n") ? input.answer : `${input.answer}
|
|
4405
|
+
`;
|
|
4406
|
+
try {
|
|
4407
|
+
writeFileSync3(target, body, { mode: 384 });
|
|
4408
|
+
} catch (err) {
|
|
4409
|
+
return {
|
|
4410
|
+
ok: false,
|
|
4411
|
+
tone: "caution",
|
|
4412
|
+
message: `could not write ${target}: ${err instanceof Error ? err.message : String(err)}`
|
|
4413
|
+
};
|
|
4414
|
+
}
|
|
4415
|
+
return { ok: true, message: `Answer written to ${target}` };
|
|
4416
|
+
}
|
|
4417
|
+
function stamp2() {
|
|
4418
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4419
|
+
}
|
|
4420
|
+
var CAPTURE_EVENT, RESEARCH_SOURCE, RESEARCH_TEXT_LIMIT, ACTION_LABEL;
|
|
4421
|
+
var init_run_actions = __esm({
|
|
4422
|
+
"src/run-actions.ts"() {
|
|
4423
|
+
"use strict";
|
|
4424
|
+
init_handoff();
|
|
4425
|
+
init_memory();
|
|
4426
|
+
init_task();
|
|
4427
|
+
init_output();
|
|
4428
|
+
CAPTURE_EVENT = "capture";
|
|
4429
|
+
RESEARCH_SOURCE = "noir run";
|
|
4430
|
+
RESEARCH_TEXT_LIMIT = 220;
|
|
4431
|
+
ACTION_LABEL = {
|
|
4432
|
+
memory: "Saving to memory",
|
|
4433
|
+
research: "Recording the finding",
|
|
4434
|
+
handoff: "Writing the handoff",
|
|
4435
|
+
resume: "Continuing the session",
|
|
4436
|
+
save: "Writing the file",
|
|
4437
|
+
dismiss: "Dismissing"
|
|
4438
|
+
};
|
|
4439
|
+
}
|
|
4440
|
+
});
|
|
4441
|
+
|
|
4442
|
+
// src/run-interrupt.ts
|
|
4443
|
+
import { constants as constants2 } from "os";
|
|
4444
|
+
function exitCodeForSignal(signal) {
|
|
4445
|
+
return 128 + (constants2.signals[signal] ?? 0);
|
|
4446
|
+
}
|
|
4447
|
+
function interruptedNotice(transcript) {
|
|
4448
|
+
return `interrupted \xB7 transcript: ${transcript}`;
|
|
4449
|
+
}
|
|
4450
|
+
var INTERRUPT_SIGNALS, RunInterrupt;
|
|
4451
|
+
var init_run_interrupt = __esm({
|
|
4452
|
+
"src/run-interrupt.ts"() {
|
|
4453
|
+
"use strict";
|
|
4454
|
+
init_orchestrator();
|
|
4455
|
+
init_run_timing();
|
|
4456
|
+
INTERRUPT_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
4457
|
+
RunInterrupt = class {
|
|
4458
|
+
graceMs;
|
|
4459
|
+
exitNow;
|
|
4460
|
+
controller = new AbortController();
|
|
4461
|
+
/** The live child, or undefined between spawns. */
|
|
4462
|
+
child;
|
|
4463
|
+
/** The child the ladder already ran for — a repeat call is not a new ladder. */
|
|
4464
|
+
stopping;
|
|
4465
|
+
grace;
|
|
4466
|
+
signalName;
|
|
4467
|
+
stopRequested = false;
|
|
4468
|
+
forced = false;
|
|
4469
|
+
watching = false;
|
|
4470
|
+
/** The installed listeners, keyed by the signal each answers. */
|
|
4471
|
+
handlers = /* @__PURE__ */ new Map();
|
|
4472
|
+
constructor(opts = {}) {
|
|
4473
|
+
this.graceMs = opts.graceMs ?? INTERRUPT_GRACE_MS;
|
|
4474
|
+
this.exitNow = opts.exitNow ?? ((code) => {
|
|
4475
|
+
if (this.child !== void 0) signalChild(this.child, "SIGKILL");
|
|
4476
|
+
process.exit(code);
|
|
4477
|
+
});
|
|
4478
|
+
}
|
|
4479
|
+
/**
|
|
4480
|
+
* The cancel signal for the spawn. Aborting it is what a caller holding only
|
|
4481
|
+
* the signal can do; the ladder itself does not depend on it, which matters
|
|
4482
|
+
* because the child a run ends up driving is not always the first one spawned.
|
|
4483
|
+
*/
|
|
4484
|
+
get signal() {
|
|
4485
|
+
return this.controller.signal;
|
|
4486
|
+
}
|
|
4487
|
+
/** The signal that stopped the run, or undefined when nothing did. */
|
|
4488
|
+
get interruptedBy() {
|
|
4489
|
+
return this.signalName;
|
|
4490
|
+
}
|
|
4491
|
+
/** True once a child had to be forced — it did not honour the polite signal. */
|
|
4492
|
+
get escalated() {
|
|
4493
|
+
return this.forced;
|
|
4494
|
+
}
|
|
4495
|
+
/**
|
|
4496
|
+
* Hand over the child a spawn produced. Called for every spawn of one run: a
|
|
4497
|
+
* command resolved through the shell bridge is its own child, and a stop that
|
|
4498
|
+
* arrived before that child existed has to land on it too.
|
|
4499
|
+
*/
|
|
4500
|
+
track(child) {
|
|
4501
|
+
this.child = child;
|
|
4502
|
+
child.once("exit", () => this.forget(child));
|
|
4503
|
+
if (this.stopRequested) this.terminate();
|
|
4504
|
+
}
|
|
4505
|
+
/**
|
|
4506
|
+
* Stop the host. The polite signal goes to the child itself, and a child still
|
|
4507
|
+
* alive after the grace is forced. Repeat calls are the same call: the ladder
|
|
4508
|
+
* runs once per child, so a keystroke held down is not a stream of signals.
|
|
4509
|
+
*/
|
|
4510
|
+
terminate() {
|
|
4511
|
+
this.stopRequested = true;
|
|
4512
|
+
if (!this.controller.signal.aborted) this.controller.abort();
|
|
4513
|
+
const child = this.child;
|
|
4514
|
+
if (child === void 0 || child.pid === void 0 || child === this.stopping) return;
|
|
4515
|
+
this.stopping = child;
|
|
4516
|
+
this.clearGrace();
|
|
4517
|
+
signalChild(child, "SIGTERM");
|
|
4518
|
+
this.grace = setTimeout(() => {
|
|
4519
|
+
this.grace = void 0;
|
|
4520
|
+
if (this.child !== child) return;
|
|
4521
|
+
this.forced = true;
|
|
4522
|
+
signalChild(child, "SIGKILL");
|
|
4523
|
+
}, this.graceMs);
|
|
4524
|
+
}
|
|
4525
|
+
/**
|
|
4526
|
+
* Skip the grace and force the host now. The "you asked twice" path: the
|
|
4527
|
+
* terminal leaves at once on a second signal, and the run screen forces and
|
|
4528
|
+
* returns to the dashboard rather than holding a keystroke while the grace
|
|
4529
|
+
* runs out. Does nothing when no stop is in flight.
|
|
4530
|
+
*/
|
|
4531
|
+
forceNow() {
|
|
4532
|
+
const child = this.child;
|
|
4533
|
+
if (child === void 0 || child.pid === void 0 || child !== this.stopping) return false;
|
|
4534
|
+
this.forced = true;
|
|
4535
|
+
this.clearGrace();
|
|
4536
|
+
signalChild(child, "SIGKILL");
|
|
4537
|
+
return true;
|
|
4538
|
+
}
|
|
4539
|
+
/**
|
|
4540
|
+
* Install the terminal interrupt handlers. Only a run installs these, and only
|
|
4541
|
+
* for as long as it is driving the host: the process's own handling has to
|
|
4542
|
+
* come back afterwards, or a later command inherits an interrupt contract with
|
|
4543
|
+
* nothing left to interrupt.
|
|
4544
|
+
*/
|
|
4545
|
+
watch() {
|
|
4546
|
+
if (this.watching) return;
|
|
4547
|
+
this.watching = true;
|
|
4548
|
+
for (const signal of INTERRUPT_SIGNALS) {
|
|
4549
|
+
const listener = () => this.onSignal(signal);
|
|
4550
|
+
this.handlers.set(signal, listener);
|
|
4551
|
+
process.on(signal, listener);
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
/** Put the process's own handling back, whatever the run left behind. */
|
|
4555
|
+
unwatch() {
|
|
4556
|
+
if (!this.watching) return;
|
|
4557
|
+
this.watching = false;
|
|
4558
|
+
for (const [signal, listener] of this.handlers) process.off(signal, listener);
|
|
4559
|
+
this.handlers.clear();
|
|
4560
|
+
}
|
|
4561
|
+
onSignal(signal) {
|
|
4562
|
+
if (this.signalName !== void 0) {
|
|
4563
|
+
this.exitNow(exitCodeForSignal(signal));
|
|
4564
|
+
return;
|
|
4565
|
+
}
|
|
4566
|
+
this.signalName = signal;
|
|
4567
|
+
this.terminate();
|
|
4568
|
+
}
|
|
4569
|
+
/** The child is gone: stop tracking it and disarm its ladder. */
|
|
4570
|
+
forget(child) {
|
|
4571
|
+
if (this.child !== child) return;
|
|
4572
|
+
this.child = void 0;
|
|
4573
|
+
this.stopping = void 0;
|
|
4574
|
+
this.clearGrace();
|
|
4575
|
+
}
|
|
4576
|
+
clearGrace() {
|
|
4577
|
+
if (this.grace === void 0) return;
|
|
4578
|
+
clearTimeout(this.grace);
|
|
4579
|
+
this.grace = void 0;
|
|
4580
|
+
}
|
|
4581
|
+
};
|
|
4093
4582
|
}
|
|
4094
4583
|
});
|
|
4095
4584
|
|
|
4096
4585
|
// src/run-profiles.ts
|
|
4097
|
-
import { loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
|
|
4586
|
+
import { isDeniedEnvKey, loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
|
|
4098
4587
|
function loadRunConfig(root) {
|
|
4099
4588
|
try {
|
|
4100
4589
|
return loadProjectInfo11(root).config;
|
|
@@ -4129,6 +4618,14 @@ function resolveRunProfile(requested, config, env2) {
|
|
|
4129
4618
|
envExpanded[k] = v === null ? void 0 : expandEnvVars(v, env2);
|
|
4130
4619
|
}
|
|
4131
4620
|
}
|
|
4621
|
+
for (const key of Object.keys(envExpanded)) {
|
|
4622
|
+
if (isDeniedEnvKey(key)) {
|
|
4623
|
+
return {
|
|
4624
|
+
ok: false,
|
|
4625
|
+
message: `run profile "${name}" refuses process-injection key "${key}" \u2014 a profile env block in .noir/config.yml must not set ${key} (it can inject into the spawned host process). Remove "${key}" from the profile.`
|
|
4626
|
+
};
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4132
4629
|
return {
|
|
4133
4630
|
ok: true,
|
|
4134
4631
|
profile: { binary: profile.binary, env: envExpanded, args: profile.args, profileName: name }
|
|
@@ -4149,11 +4646,195 @@ var init_run_profiles = __esm({
|
|
|
4149
4646
|
}
|
|
4150
4647
|
});
|
|
4151
4648
|
|
|
4649
|
+
// src/run-status.ts
|
|
4650
|
+
function humanizeElapsed(ms) {
|
|
4651
|
+
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
4652
|
+
const seconds = total % 60;
|
|
4653
|
+
const minutes = Math.floor(total / 60) % 60;
|
|
4654
|
+
const hours = Math.floor(total / 3600);
|
|
4655
|
+
if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
4656
|
+
if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
4657
|
+
return `${seconds}s`;
|
|
4658
|
+
}
|
|
4659
|
+
function hostStderrTail(stderr, maxLines = HOST_STDERR_TAIL_LINES) {
|
|
4660
|
+
if (maxLines <= 0) return "";
|
|
4661
|
+
const lines = stderr.split("\n").map((line) => line.trimEnd());
|
|
4662
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
4663
|
+
if (lines.length === 0) return "";
|
|
4664
|
+
if (lines.length <= maxLines) return lines.join("\n");
|
|
4665
|
+
const omitted = lines.length - maxLines;
|
|
4666
|
+
return [`\u2026 (${omitted} earlier lines omitted)`, ...lines.slice(-maxLines)].join("\n");
|
|
4667
|
+
}
|
|
4668
|
+
var HOST_STDERR_TAIL_LINES, RunStatusLine;
|
|
4669
|
+
var init_run_status = __esm({
|
|
4670
|
+
"src/run-status.ts"() {
|
|
4671
|
+
"use strict";
|
|
4672
|
+
init_orchestrator();
|
|
4673
|
+
HOST_STDERR_TAIL_LINES = 20;
|
|
4674
|
+
RunStatusLine = class {
|
|
4675
|
+
host;
|
|
4676
|
+
enabled;
|
|
4677
|
+
animated;
|
|
4678
|
+
sharesCursor;
|
|
4679
|
+
write;
|
|
4680
|
+
now;
|
|
4681
|
+
/**
|
|
4682
|
+
* Running token totals for the live line. This is a SECOND accumulator: the
|
|
4683
|
+
* snapshot the run reports at the end is reduced independently from the full
|
|
4684
|
+
* event stream, and both apply the same `max usage per message id` rule, so
|
|
4685
|
+
* the number on screen and the number in the summary agree.
|
|
4686
|
+
*/
|
|
4687
|
+
usage = new UsageReducer();
|
|
4688
|
+
startedAt;
|
|
4689
|
+
model;
|
|
4690
|
+
streaming = false;
|
|
4691
|
+
begun = false;
|
|
4692
|
+
ended = false;
|
|
4693
|
+
/** True while the status text is sitting on the row the cursor is currently at. */
|
|
4694
|
+
onRow = false;
|
|
4695
|
+
/** True when the last stdout write ended a line — the only safe moment to move the cursor. */
|
|
4696
|
+
stdoutAtRowStart = true;
|
|
4697
|
+
/** The text currently on the row (empty when the row is not ours). */
|
|
4698
|
+
rendered = "";
|
|
4699
|
+
constructor(opts) {
|
|
4700
|
+
this.host = opts.host;
|
|
4701
|
+
this.enabled = opts.json !== true && opts.quiet !== true;
|
|
4702
|
+
this.animated = this.enabled && opts.stderrIsTty === true;
|
|
4703
|
+
this.sharesCursor = opts.stdoutSharesCursor !== false;
|
|
4704
|
+
this.write = opts.write ?? ((chunk) => void process.stderr.write(chunk));
|
|
4705
|
+
this.now = opts.now ?? (() => performance.now());
|
|
4706
|
+
this.startedAt = this.now();
|
|
4707
|
+
}
|
|
4708
|
+
/**
|
|
4709
|
+
* Claim the row: show the seeded "waiting" state immediately, before the host
|
|
4710
|
+
* has said anything, so the spawn itself is never blank. Calling this twice
|
|
4711
|
+
* is a no-op.
|
|
4712
|
+
*/
|
|
4713
|
+
begin() {
|
|
4714
|
+
if (!this.enabled || this.begun) return;
|
|
4715
|
+
this.begun = true;
|
|
4716
|
+
this.startedAt = this.now();
|
|
4717
|
+
if (this.animated) {
|
|
4718
|
+
this.draw();
|
|
4719
|
+
return;
|
|
4720
|
+
}
|
|
4721
|
+
this.write(`${this.render()}
|
|
4722
|
+
`);
|
|
4723
|
+
}
|
|
4724
|
+
/** Feed one normalized host event. Anything unrenderable is ignored. */
|
|
4725
|
+
event(e) {
|
|
4726
|
+
if (!this.enabled || this.ended || !this.begun) return;
|
|
4727
|
+
if (e.kind === "result") {
|
|
4728
|
+
this.end();
|
|
4729
|
+
return;
|
|
4730
|
+
}
|
|
4731
|
+
if (e.kind === "init") {
|
|
4732
|
+
if (e.model !== void 0) this.model = e.model;
|
|
4733
|
+
} else if (e.kind === "assistant" || e.kind === "tool") {
|
|
4734
|
+
this.streaming = true;
|
|
4735
|
+
this.usage.add(e);
|
|
4736
|
+
} else if (e.kind === "delta") {
|
|
4737
|
+
this.streaming = true;
|
|
4738
|
+
} else {
|
|
4739
|
+
return;
|
|
4740
|
+
}
|
|
4741
|
+
this.draw();
|
|
4742
|
+
}
|
|
4743
|
+
/**
|
|
4744
|
+
* Announce the chunk the caller is about to write to STDOUT — before writing
|
|
4745
|
+
* it. Warning the line first lets it finish its row: the answer would
|
|
4746
|
+
* otherwise start glued to the tail of the status text. It also records
|
|
4747
|
+
* whether that write ended a line, which is the only moment the cursor can be
|
|
4748
|
+
* moved without landing inside the answer.
|
|
4749
|
+
*/
|
|
4750
|
+
beforeStdout(chunk) {
|
|
4751
|
+
if (chunk.length === 0) return;
|
|
4752
|
+
if (this.animated && this.sharesCursor && this.onRow) {
|
|
4753
|
+
this.write("\n");
|
|
4754
|
+
this.onRow = false;
|
|
4755
|
+
}
|
|
4756
|
+
this.stdoutAtRowStart = chunk.endsWith("\n");
|
|
4757
|
+
}
|
|
4758
|
+
/**
|
|
4759
|
+
* Finish the row the host's answer left open. A failure message printed onto
|
|
4760
|
+
* a half-written answer reads as part of the answer, so a caller about to
|
|
4761
|
+
* report a failure asks for the row to be terminated first. A no-op unless
|
|
4762
|
+
* this line is drawing on a terminal stdout also writes to and the answer
|
|
4763
|
+
* stopped mid-line — the only state where the cursor is inside the answer.
|
|
4764
|
+
*/
|
|
4765
|
+
finishRow() {
|
|
4766
|
+
if (!this.animated || !this.sharesCursor || this.stdoutAtRowStart) return;
|
|
4767
|
+
this.write("\n");
|
|
4768
|
+
this.stdoutAtRowStart = true;
|
|
4769
|
+
}
|
|
4770
|
+
/**
|
|
4771
|
+
* Close the line out. On a terminal the line is erased so the run summary
|
|
4772
|
+
* takes its place — unless the answer has since moved the cursor off it, in
|
|
4773
|
+
* which case the text stays as the record of how long the wait was. On a pipe
|
|
4774
|
+
* it becomes the final marker. Safe to call twice.
|
|
4775
|
+
*/
|
|
4776
|
+
end() {
|
|
4777
|
+
if (!this.enabled || this.ended) return;
|
|
4778
|
+
this.ended = true;
|
|
4779
|
+
if (!this.animated) {
|
|
4780
|
+
this.write(`${this.render(true)}
|
|
4781
|
+
`);
|
|
4782
|
+
return;
|
|
4783
|
+
}
|
|
4784
|
+
if (!this.onRow || !this.cursorFree()) return;
|
|
4785
|
+
this.write("\r\x1B[K");
|
|
4786
|
+
this.rendered = "";
|
|
4787
|
+
this.onRow = false;
|
|
4788
|
+
}
|
|
4789
|
+
/** Rewrite the current row in place, skipping no-op rewrites and unsafe moments. */
|
|
4790
|
+
draw() {
|
|
4791
|
+
if (!this.animated || !this.cursorFree()) return;
|
|
4792
|
+
const text = this.render();
|
|
4793
|
+
if (text === this.rendered) return;
|
|
4794
|
+
this.write(`\r\x1B[K${text}`);
|
|
4795
|
+
this.rendered = text;
|
|
4796
|
+
this.onRow = true;
|
|
4797
|
+
}
|
|
4798
|
+
/**
|
|
4799
|
+
* Whether the cursor is at the start of a row nobody else is using. On stdout
|
|
4800
|
+
* that is not this terminal there is no shared cursor to disturb; when it is,
|
|
4801
|
+
* only a write that ended a line leaves one free.
|
|
4802
|
+
*/
|
|
4803
|
+
cursorFree() {
|
|
4804
|
+
return !this.sharesCursor || this.stdoutAtRowStart;
|
|
4805
|
+
}
|
|
4806
|
+
/**
|
|
4807
|
+
* `final` marks the line the run ends on: a run that never got past the spawn
|
|
4808
|
+
* is over, so saying it is still waiting — directly above whatever the run
|
|
4809
|
+
* reported — would be a lie.
|
|
4810
|
+
*/
|
|
4811
|
+
render(final = false) {
|
|
4812
|
+
const parts = [];
|
|
4813
|
+
if (this.streaming) {
|
|
4814
|
+
const u = this.usage.snapshot();
|
|
4815
|
+
parts.push(
|
|
4816
|
+
this.model ?? this.host,
|
|
4817
|
+
this.elapsed(),
|
|
4818
|
+
`\u2193${u.inputTokens.toLocaleString()}\u2191${u.outputTokens.toLocaleString()} tokens`
|
|
4819
|
+
);
|
|
4820
|
+
return `\u25CF ${parts.join(" \xB7 ")}`;
|
|
4821
|
+
}
|
|
4822
|
+
if (this.model !== void 0) return `\u25B6 ${[this.host, this.model, this.elapsed()].join(" \xB7 ")}`;
|
|
4823
|
+
if (final) return `\u25B6 ${this.host} \xB7 no output after ${this.elapsed()}`;
|
|
4824
|
+
return `\u25B6 ${this.host} \xB7 waiting for first event\u2026`;
|
|
4825
|
+
}
|
|
4826
|
+
elapsed() {
|
|
4827
|
+
return humanizeElapsed(this.now() - this.startedAt);
|
|
4828
|
+
}
|
|
4829
|
+
};
|
|
4830
|
+
}
|
|
4831
|
+
});
|
|
4832
|
+
|
|
4152
4833
|
// src/commands/run.ts
|
|
4153
|
-
import { existsSync as existsSync2, mkdirSync as mkdirSync3, writeFileSync as
|
|
4834
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
4154
4835
|
import { join as join4 } from "path";
|
|
4155
4836
|
import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
|
|
4156
|
-
import { loadNoirEnv as loadNoirEnv2, NOIR_DIR as NOIR_DIR2, parseConfig } from "@noir-ai/core";
|
|
4837
|
+
import { isDeniedEnvKey as isDeniedEnvKey2, loadNoirEnv as loadNoirEnv2, NOIR_DIR as NOIR_DIR2, parseConfig, paths } from "@noir-ai/core";
|
|
4157
4838
|
function credentialNote(env2, sources) {
|
|
4158
4839
|
const named = CREDENTIAL_ENV_VARS.filter((name) => {
|
|
4159
4840
|
const value = env2[name];
|
|
@@ -4173,13 +4854,24 @@ function formatUsage(u) {
|
|
|
4173
4854
|
const turns = u.numTurns > 0 ? ` \xB7 ${u.numTurns} turns` : "";
|
|
4174
4855
|
return `${u.inputTokens.toLocaleString()} in / ${u.outputTokens.toLocaleString()} out${cost}${turns}`;
|
|
4175
4856
|
}
|
|
4176
|
-
function
|
|
4177
|
-
if (opts.json === true) return;
|
|
4857
|
+
function answerText(event) {
|
|
4178
4858
|
if (event.kind === "assistant" && event.text && event.text.length > 0 && event.isError !== true) {
|
|
4179
|
-
|
|
4859
|
+
return event.text;
|
|
4860
|
+
}
|
|
4861
|
+
return void 0;
|
|
4862
|
+
}
|
|
4863
|
+
function streamEvent(event, opts, status2) {
|
|
4864
|
+
if (opts.json === true) return;
|
|
4865
|
+
const text = answerText(event);
|
|
4866
|
+
if (text !== void 0) {
|
|
4867
|
+
status2.beforeStdout(text);
|
|
4868
|
+
process.stdout.write(text);
|
|
4180
4869
|
}
|
|
4181
4870
|
}
|
|
4182
4871
|
async function run(prompt, opts) {
|
|
4872
|
+
await runOnce(prompt, opts, void 0);
|
|
4873
|
+
}
|
|
4874
|
+
async function runOnce(prompt, opts, resumeSessionId) {
|
|
4183
4875
|
const host = opts.host ?? "claude";
|
|
4184
4876
|
if (!SUPPORTED_HOSTS.includes(host)) {
|
|
4185
4877
|
fail(EXIT.USAGE, `unknown host '${host}' (supported: ${SUPPORTED_HOSTS.join(", ")})`, opts);
|
|
@@ -4198,7 +4890,7 @@ async function run(prompt, opts) {
|
|
|
4198
4890
|
if (!resolved.ok) fail(EXIT.USAGE, resolved.message, opts);
|
|
4199
4891
|
const profile = resolved.profile;
|
|
4200
4892
|
const customBinary = opts.command ?? profile.binary;
|
|
4201
|
-
const extraArgs = profile.args;
|
|
4893
|
+
const extraArgs = resumeSessionId === void 0 ? profile.args : [...profile.args ?? [], "--resume", resumeSessionId];
|
|
4202
4894
|
const env2 = profile.env ? mergeEnv(process.env, profile.env) : void 0;
|
|
4203
4895
|
if (!existsSync2(join4(root, NOIR_DIR2, ".env"))) {
|
|
4204
4896
|
log(
|
|
@@ -4207,6 +4899,37 @@ async function run(prompt, opts) {
|
|
|
4207
4899
|
);
|
|
4208
4900
|
}
|
|
4209
4901
|
const transcriptLines = [];
|
|
4902
|
+
const answer = [];
|
|
4903
|
+
let sessionId;
|
|
4904
|
+
const binary = customBinary && customBinary.length > 0 ? customBinary : host;
|
|
4905
|
+
const status2 = new RunStatusLine({
|
|
4906
|
+
host: binary,
|
|
4907
|
+
json: opts.json,
|
|
4908
|
+
quiet: opts.quiet,
|
|
4909
|
+
stderrIsTty: process.stderr.isTTY === true,
|
|
4910
|
+
stdoutSharesCursor: process.stdout.isTTY === true
|
|
4911
|
+
});
|
|
4912
|
+
status2.begin();
|
|
4913
|
+
let verdictWritten = false;
|
|
4914
|
+
const claimVerdict = () => {
|
|
4915
|
+
if (verdictWritten) return false;
|
|
4916
|
+
verdictWritten = true;
|
|
4917
|
+
return true;
|
|
4918
|
+
};
|
|
4919
|
+
const interrupt = new RunInterrupt({
|
|
4920
|
+
// A second interrupt leaves at once, and leaves the same verdict behind as
|
|
4921
|
+
// the first: the run owes its consumer an envelope under --json, and this is
|
|
4922
|
+
// the one path where `fail()` cannot write it (there is no caller left to
|
|
4923
|
+
// catch its throw). The child is forced first, so leaving does not leave a
|
|
4924
|
+
// host behind.
|
|
4925
|
+
exitNow: (code) => {
|
|
4926
|
+
interrupt.forceNow();
|
|
4927
|
+
if (claimVerdict()) {
|
|
4928
|
+
failAndExit(code, interruptedNotice(safeTranscript(host, transcriptLines)), opts);
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
});
|
|
4932
|
+
interrupt.watch();
|
|
4210
4933
|
let result;
|
|
4211
4934
|
try {
|
|
4212
4935
|
result = await runHost({
|
|
@@ -4216,28 +4939,61 @@ async function run(prompt, opts) {
|
|
|
4216
4939
|
extraArgs,
|
|
4217
4940
|
env: env2,
|
|
4218
4941
|
onLine: (line) => transcriptLines.push(line),
|
|
4219
|
-
onEvent: (event) =>
|
|
4942
|
+
onEvent: (event) => {
|
|
4943
|
+
if (event.kind === "init" && sessionId === void 0) sessionId = event.sessionId;
|
|
4944
|
+
status2.event(event);
|
|
4945
|
+
streamEvent(event, opts, status2);
|
|
4946
|
+
const text = answerText(event);
|
|
4947
|
+
if (text !== void 0) answer.push(text);
|
|
4948
|
+
},
|
|
4949
|
+
signal: interrupt.signal,
|
|
4950
|
+
onChild: (child) => interrupt.track(child)
|
|
4220
4951
|
});
|
|
4221
4952
|
} catch (err) {
|
|
4222
|
-
|
|
4953
|
+
status2.end();
|
|
4954
|
+
status2.finishRow();
|
|
4955
|
+
if (interrupt.interruptedBy !== void 0) {
|
|
4956
|
+
if (claimVerdict()) {
|
|
4957
|
+
failInterrupted(interrupt.interruptedBy, host, transcriptLines, opts);
|
|
4958
|
+
}
|
|
4959
|
+
return;
|
|
4960
|
+
}
|
|
4223
4961
|
const detail = err instanceof Error ? err.message : String(err);
|
|
4224
4962
|
const enoent = err?.code === "ENOENT";
|
|
4225
4963
|
const guidance = enoent ? ` No executable '${binary}' was found. Shell aliases and functions (e.g. from .zshrc) are invisible to noir \u2014 use an executable on PATH, an absolute path, or a launcher script such as ~/.local/bin/${binary}.` : "";
|
|
4226
4964
|
const subject = customBinary && customBinary.length > 0 ? `custom command '${customBinary}'` : `host '${host}'`;
|
|
4227
4965
|
fail(EXIT.ERROR, `failed to run ${subject}: ${detail}.${guidance}`, opts);
|
|
4966
|
+
} finally {
|
|
4967
|
+
interrupt.unwatch();
|
|
4968
|
+
}
|
|
4969
|
+
if (interrupt.interruptedBy !== void 0) {
|
|
4970
|
+
status2.end();
|
|
4971
|
+
status2.finishRow();
|
|
4972
|
+
if (claimVerdict()) {
|
|
4973
|
+
failInterrupted(interrupt.interruptedBy, host, transcriptLines, opts);
|
|
4974
|
+
}
|
|
4975
|
+
return;
|
|
4228
4976
|
}
|
|
4229
|
-
const transcript =
|
|
4977
|
+
const transcript = safeTranscript(host, transcriptLines);
|
|
4230
4978
|
const failed = result.exitCode !== 0 || result.isError;
|
|
4231
4979
|
if (failed) {
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
const
|
|
4980
|
+
status2.end();
|
|
4981
|
+
status2.finishRow();
|
|
4982
|
+
const stderrTail = hostStderrTail(result.stderr);
|
|
4983
|
+
const reason = result.errorText && result.errorText.trim().length > 0 ? result.errorText.trim() : `exit code ${result.exitCode}`;
|
|
4984
|
+
const isAuth = result.errorCategory === "authentication_failed" || result.errorCategory === "oauth_org_not_allowed" || /not logged|login|authenticate|invalid api key/i.test(`${reason}
|
|
4985
|
+
${stderrTail}`);
|
|
4235
4986
|
let message = `host '${binary}' failed (exit ${result.exitCode}): ${reason}`;
|
|
4236
4987
|
if (isAuth) {
|
|
4237
4988
|
message += ` Open a terminal and run \`${binary} /login\` (interactive-only \u2014 it cannot run inside \`noir run\`), then retry.`;
|
|
4238
4989
|
message += credentialNote(env2 ?? process.env, envSources);
|
|
4239
4990
|
}
|
|
4240
4991
|
message += ` If you use another profile, pass \`--command <binary>\` or define a run profile under run.profiles. transcript: ${transcript}`;
|
|
4992
|
+
if (opts.json !== true && stderrTail.length > 0) {
|
|
4993
|
+
message += `
|
|
4994
|
+
${binary} stderr (last ${HOST_STDERR_TAIL_LINES} lines):
|
|
4995
|
+
${stderrTail}`;
|
|
4996
|
+
}
|
|
4241
4997
|
fail(EXIT.ERROR, message, opts);
|
|
4242
4998
|
}
|
|
4243
4999
|
if (opts.json === true) {
|
|
@@ -4250,26 +5006,59 @@ async function run(prompt, opts) {
|
|
|
4250
5006
|
usage: result.usage,
|
|
4251
5007
|
numTurns: result.usage.numTurns,
|
|
4252
5008
|
events: result.eventCount,
|
|
4253
|
-
transcript
|
|
5009
|
+
transcript,
|
|
5010
|
+
// The answer as plain words, so a consumer does not have to reassemble
|
|
5011
|
+
// it from the stream-json transcript.
|
|
5012
|
+
answerText: answer.join(""),
|
|
5013
|
+
...sessionId === void 0 ? {} : { sessionId }
|
|
4254
5014
|
}
|
|
4255
5015
|
});
|
|
4256
5016
|
return;
|
|
4257
5017
|
}
|
|
5018
|
+
status2.end();
|
|
4258
5019
|
process.stdout.write("\n");
|
|
4259
5020
|
success(`usage: ${formatUsage(result.usage)} (API-equivalent estimate, not billed)`, opts);
|
|
4260
5021
|
log(`transcript: ${transcript}`, opts);
|
|
5022
|
+
await offerPostRunActions({
|
|
5023
|
+
answer: answer.join(""),
|
|
5024
|
+
transcript,
|
|
5025
|
+
sessionId,
|
|
5026
|
+
host: binary,
|
|
5027
|
+
opts,
|
|
5028
|
+
// Only the first run of a session can be continued: a resumed run is
|
|
5029
|
+
// itself the continuation, so the menu never nests more than one deep.
|
|
5030
|
+
...resumeSessionId === void 0 && sessionId !== void 0 ? { resume: (nextPrompt, id) => runOnce(nextPrompt, opts, id) } : {}
|
|
5031
|
+
});
|
|
4261
5032
|
}
|
|
4262
5033
|
function mergeEnv(base, overlay) {
|
|
5034
|
+
for (const key of Object.keys(overlay)) {
|
|
5035
|
+
if (isDeniedEnvKey2(key)) {
|
|
5036
|
+
fail(
|
|
5037
|
+
EXIT.ERROR,
|
|
5038
|
+
`refusing to run the host: the profile env defines process-injection key "${key}" (it can inject into the spawned host process)`
|
|
5039
|
+
);
|
|
5040
|
+
}
|
|
5041
|
+
}
|
|
4263
5042
|
const merged = { ...base, ...overlay };
|
|
4264
5043
|
return Object.fromEntries(Object.entries(merged).filter(([, v]) => v !== void 0));
|
|
4265
5044
|
}
|
|
4266
|
-
function
|
|
5045
|
+
function failInterrupted(signal, host, lines, opts) {
|
|
5046
|
+
return fail(exitCodeForSignal(signal), interruptedNotice(safeTranscript(host, lines)), opts);
|
|
5047
|
+
}
|
|
5048
|
+
function safeTranscript(host, lines) {
|
|
5049
|
+
try {
|
|
5050
|
+
return writeTranscript(host, lines);
|
|
5051
|
+
} catch {
|
|
5052
|
+
return "(not persisted)";
|
|
5053
|
+
}
|
|
5054
|
+
}
|
|
5055
|
+
function writeTranscript(host, lines, root = process.cwd()) {
|
|
4267
5056
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4268
|
-
const dir =
|
|
5057
|
+
const dir = paths.transcriptsDir(root);
|
|
4269
5058
|
const file = join4(dir, `${host}-${ts}.jsonl`);
|
|
4270
5059
|
try {
|
|
4271
5060
|
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
4272
|
-
|
|
5061
|
+
writeFileSync4(file, `${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`, { mode: 384 });
|
|
4273
5062
|
} catch {
|
|
4274
5063
|
return "(not persisted)";
|
|
4275
5064
|
}
|
|
@@ -4281,7 +5070,10 @@ var init_run = __esm({
|
|
|
4281
5070
|
"use strict";
|
|
4282
5071
|
init_orchestrator();
|
|
4283
5072
|
init_output();
|
|
5073
|
+
init_run_actions();
|
|
5074
|
+
init_run_interrupt();
|
|
4284
5075
|
init_run_profiles();
|
|
5076
|
+
init_run_status();
|
|
4285
5077
|
CREDENTIAL_ENV_VARS = [
|
|
4286
5078
|
"ANTHROPIC_API_KEY",
|
|
4287
5079
|
"ANTHROPIC_AUTH_TOKEN",
|
|
@@ -4290,257 +5082,20 @@ var init_run = __esm({
|
|
|
4290
5082
|
}
|
|
4291
5083
|
});
|
|
4292
5084
|
|
|
4293
|
-
// src/
|
|
4294
|
-
import { resolveAdapter as resolveAdapter3 } from "@noir-ai/adapters";
|
|
4295
|
-
import { loadProjectInfo as loadProjectInfo12 } from "@noir-ai/core";
|
|
5085
|
+
// src/conflict.ts
|
|
4296
5086
|
import {
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
function categoryOf(name) {
|
|
4303
|
-
const mapped = CATEGORY[name];
|
|
4304
|
-
if (mapped !== void 0) return mapped;
|
|
4305
|
-
return name.replace(/^noir-/, "") || "general";
|
|
4306
|
-
}
|
|
4307
|
-
function toRow(s, kind = "builtin") {
|
|
4308
|
-
const description = typeof s.frontmatter.description === "string" ? s.frontmatter.description : "";
|
|
4309
|
-
const status2 = s.skillMd.includes("> **Stub:**") ? "stub" : "full";
|
|
4310
|
-
const category = s.frontmatter.metadata?.category?.trim() || categoryOf(s.name);
|
|
4311
|
-
return { name: s.name, category, description, kind, status: status2 };
|
|
4312
|
-
}
|
|
4313
|
-
function truncate(text, max) {
|
|
4314
|
-
if (text.length <= max) return text;
|
|
4315
|
-
const slice = text.slice(0, Math.max(0, max - 1));
|
|
4316
|
-
const boundary = slice.lastIndexOf(" ");
|
|
4317
|
-
const head = boundary > 0 ? slice.slice(0, boundary) : slice;
|
|
4318
|
-
return `${head.trimEnd()}\u2026`;
|
|
5087
|
+
lineDiff
|
|
5088
|
+
} from "@noir-ai/create";
|
|
5089
|
+
function flaggedNonInteractive() {
|
|
5090
|
+
const v = process.env.NOIR_NON_INTERACTIVE;
|
|
5091
|
+
return v !== void 0 && v !== "";
|
|
4319
5092
|
}
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
try {
|
|
4324
|
-
const all = discoverAll();
|
|
4325
|
-
builtins = all.builtins;
|
|
4326
|
-
integrations = all.integrations;
|
|
4327
|
-
} catch (err) {
|
|
4328
|
-
fail(
|
|
4329
|
-
EXIT.ERROR,
|
|
4330
|
-
`skills list: could not discover skills (${err instanceof Error ? err.message : String(err)})`,
|
|
4331
|
-
opts
|
|
4332
|
-
);
|
|
5093
|
+
function buildConflictOpts(input = {}) {
|
|
5094
|
+
if (input.force === true) {
|
|
5095
|
+
return { conflictPolicy: "overwrite" };
|
|
4333
5096
|
}
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
...integrations.map((i) => toRow(i, "integration"))
|
|
4337
|
-
];
|
|
4338
|
-
const data = { count: rows.length, skills: rows };
|
|
4339
|
-
if (opts.json === true) {
|
|
4340
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
4341
|
-
`);
|
|
4342
|
-
return;
|
|
4343
|
-
}
|
|
4344
|
-
const intCount = integrations.length;
|
|
4345
|
-
const banner = intCount > 0 ? `noir skills \u2014 ${rows.length} skill${rows.length === 1 ? "" : "s"} (${builtins.length} builtin, ${intCount} integration${intCount === 1 ? "" : "s"})` : `noir skills \u2014 ${rows.length} builtin skill${rows.length === 1 ? "" : "s"}`;
|
|
4346
|
-
log(banner, opts);
|
|
4347
|
-
table(
|
|
4348
|
-
rows.map((r) => ({
|
|
4349
|
-
Skill: r.name,
|
|
4350
|
-
Kind: r.kind,
|
|
4351
|
-
Category: r.category,
|
|
4352
|
-
Status: r.status,
|
|
4353
|
-
Description: truncate(r.description, 80)
|
|
4354
|
-
})),
|
|
4355
|
-
["Skill", "Kind", "Category", "Status", "Description"],
|
|
4356
|
-
opts
|
|
4357
|
-
);
|
|
4358
|
-
}
|
|
4359
|
-
async function skillsSync(opts) {
|
|
4360
|
-
const root = process.cwd();
|
|
4361
|
-
let project;
|
|
4362
|
-
try {
|
|
4363
|
-
project = loadProjectInfo12(root);
|
|
4364
|
-
} catch {
|
|
4365
|
-
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
4366
|
-
}
|
|
4367
|
-
const adapter = resolveAdapter3(project.config.host);
|
|
4368
|
-
if (!adapter.skillsDir) {
|
|
4369
|
-
const data2 = {
|
|
4370
|
-
emitted: [],
|
|
4371
|
-
references: 0,
|
|
4372
|
-
host: project.config.host,
|
|
4373
|
-
reason: "no-skill-emitter"
|
|
4374
|
-
};
|
|
4375
|
-
if (opts.json === true) {
|
|
4376
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
|
|
4377
|
-
`);
|
|
4378
|
-
return;
|
|
4379
|
-
}
|
|
4380
|
-
info("This host has no skill emitter; nothing to sync.", opts);
|
|
4381
|
-
return;
|
|
4382
|
-
}
|
|
4383
|
-
const dir = adapter.skillsDir({ root });
|
|
4384
|
-
const target = project.config.host;
|
|
4385
|
-
const summary = await emitSkillsToDir(dir, { target });
|
|
4386
|
-
const pruned = summary.pruned ?? [];
|
|
4387
|
-
const data = {
|
|
4388
|
-
emitted: summary.emitted,
|
|
4389
|
-
references: summary.references,
|
|
4390
|
-
dir,
|
|
4391
|
-
pruned
|
|
4392
|
-
};
|
|
4393
|
-
if (opts.json === true) {
|
|
4394
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
4395
|
-
`);
|
|
4396
|
-
return;
|
|
4397
|
-
}
|
|
4398
|
-
log(
|
|
4399
|
-
`Synced ${summary.emitted.length} Noir skill${summary.emitted.length === 1 ? "" : "s"} to ${dir}.`,
|
|
4400
|
-
opts
|
|
4401
|
-
);
|
|
4402
|
-
if (pruned.length > 0) {
|
|
4403
|
-
warn(
|
|
4404
|
-
`Pruned ${pruned.length} stale noir-* skill dir${pruned.length === 1 ? "" : "s"}: ${pruned.join(", ")}`,
|
|
4405
|
-
opts
|
|
4406
|
-
);
|
|
4407
|
-
}
|
|
4408
|
-
}
|
|
4409
|
-
async function skillsLint(opts) {
|
|
4410
|
-
let skills;
|
|
4411
|
-
try {
|
|
4412
|
-
const all = discoverAll();
|
|
4413
|
-
skills = [...all.builtins, ...all.integrations].map(lintSkill);
|
|
4414
|
-
} catch (err) {
|
|
4415
|
-
fail(
|
|
4416
|
-
EXIT.ERROR,
|
|
4417
|
-
`skills lint: could not lint skills (${err instanceof Error ? err.message : String(err)})`,
|
|
4418
|
-
opts
|
|
4419
|
-
);
|
|
4420
|
-
}
|
|
4421
|
-
const errored = skills.filter((s) => s.errors.length > 0);
|
|
4422
|
-
const warned = skills.filter((s) => s.warnings.length > 0 && s.errors.length === 0);
|
|
4423
|
-
const data = { count: skills.length, errored: errored.length, skills };
|
|
4424
|
-
if (errored.length > 0) {
|
|
4425
|
-
const message = `skills lint: ${errored.length} skill${errored.length === 1 ? "" : "s"} failed validation`;
|
|
4426
|
-
if (opts.json === true) {
|
|
4427
|
-
process.stdout.write(
|
|
4428
|
-
`${JSON.stringify({ ok: false, error: { code: EXIT.ERROR, message }, data })}
|
|
4429
|
-
`
|
|
4430
|
-
);
|
|
4431
|
-
process.exitCode = EXIT.ERROR;
|
|
4432
|
-
return;
|
|
4433
|
-
}
|
|
4434
|
-
warn(
|
|
4435
|
-
`noir skills lint \u2014 ${errored.length} skill${errored.length === 1 ? "" : "s"} FAIL validation:`,
|
|
4436
|
-
opts
|
|
4437
|
-
);
|
|
4438
|
-
for (const s of errored) {
|
|
4439
|
-
for (const e of s.errors) warn(` ${s.name}: ${e}`, opts);
|
|
4440
|
-
}
|
|
4441
|
-
if (warned.length > 0) {
|
|
4442
|
-
info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
|
|
4443
|
-
for (const s of warned) {
|
|
4444
|
-
for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
|
|
4445
|
-
}
|
|
4446
|
-
}
|
|
4447
|
-
fail(EXIT.ERROR, message, opts);
|
|
4448
|
-
}
|
|
4449
|
-
if (opts.json === true) {
|
|
4450
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
4451
|
-
`);
|
|
4452
|
-
return;
|
|
4453
|
-
}
|
|
4454
|
-
log(`noir skills lint \u2014 ${skills.length} skills, all validate clean.`, opts);
|
|
4455
|
-
if (warned.length > 0) {
|
|
4456
|
-
info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
|
|
4457
|
-
for (const s of warned) {
|
|
4458
|
-
for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
|
|
4459
|
-
}
|
|
4460
|
-
}
|
|
4461
|
-
}
|
|
4462
|
-
async function skillsRegistry(opts) {
|
|
4463
|
-
let reg;
|
|
4464
|
-
try {
|
|
4465
|
-
reg = buildRegistry();
|
|
4466
|
-
} catch (err) {
|
|
4467
|
-
fail(
|
|
4468
|
-
EXIT.ERROR,
|
|
4469
|
-
`skills registry: could not build registry (${err instanceof Error ? err.message : String(err)})`,
|
|
4470
|
-
opts
|
|
4471
|
-
);
|
|
4472
|
-
}
|
|
4473
|
-
const data = { count: reg.length, skills: reg };
|
|
4474
|
-
if (opts.json === true) {
|
|
4475
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
4476
|
-
`);
|
|
4477
|
-
return;
|
|
4478
|
-
}
|
|
4479
|
-
log(`noir skills registry \u2014 ${reg.length} skills.`, opts);
|
|
4480
|
-
table(
|
|
4481
|
-
reg.map((r) => ({
|
|
4482
|
-
Skill: r.name,
|
|
4483
|
-
Kind: r.kind,
|
|
4484
|
-
Category: r.category,
|
|
4485
|
-
Version: r.version,
|
|
4486
|
-
Status: r.status,
|
|
4487
|
-
Refs: String(r.referenceCount),
|
|
4488
|
-
Lines: String(r.lines)
|
|
4489
|
-
})),
|
|
4490
|
-
["Skill", "Kind", "Category", "Version", "Status", "Refs", "Lines"],
|
|
4491
|
-
opts
|
|
4492
|
-
);
|
|
4493
|
-
}
|
|
4494
|
-
var CATEGORY;
|
|
4495
|
-
var init_skills = __esm({
|
|
4496
|
-
"src/commands/skills.ts"() {
|
|
4497
|
-
"use strict";
|
|
4498
|
-
init_output();
|
|
4499
|
-
CATEGORY = {
|
|
4500
|
-
"noir-brainstorming": "discovery",
|
|
4501
|
-
"noir-sync": "discovery",
|
|
4502
|
-
"noir-checkpoint": "discovery",
|
|
4503
|
-
"noir-exploring": "discovery",
|
|
4504
|
-
"noir-spec": "spec",
|
|
4505
|
-
"noir-planning": "plan",
|
|
4506
|
-
"noir-prd": "plan",
|
|
4507
|
-
"noir-executing-plans": "execute",
|
|
4508
|
-
"noir-test-driven-development": "execute",
|
|
4509
|
-
"noir-systematic-debugging": "execute",
|
|
4510
|
-
"noir-subagent": "execute",
|
|
4511
|
-
"noir-parallel": "execute",
|
|
4512
|
-
"noir-verifying": "verify",
|
|
4513
|
-
"noir-security": "verify",
|
|
4514
|
-
"noir-wrap": "document",
|
|
4515
|
-
"noir-readme": "document",
|
|
4516
|
-
"noir-shipping": "git",
|
|
4517
|
-
"noir-worktree": "git",
|
|
4518
|
-
"noir-recall": "memory",
|
|
4519
|
-
"noir-remember": "memory",
|
|
4520
|
-
"noir-context": "context",
|
|
4521
|
-
"noir-frontend": "domain",
|
|
4522
|
-
"noir-backend": "domain",
|
|
4523
|
-
"noir-doctor": "meta",
|
|
4524
|
-
"noir-writing-skills": "meta",
|
|
4525
|
-
"noir-rules": "meta"
|
|
4526
|
-
};
|
|
4527
|
-
}
|
|
4528
|
-
});
|
|
4529
|
-
|
|
4530
|
-
// src/conflict.ts
|
|
4531
|
-
import {
|
|
4532
|
-
lineDiff
|
|
4533
|
-
} from "@noir-ai/create";
|
|
4534
|
-
function flaggedNonInteractive() {
|
|
4535
|
-
const v = process.env.NOIR_NON_INTERACTIVE;
|
|
4536
|
-
return v !== void 0 && v !== "";
|
|
4537
|
-
}
|
|
4538
|
-
function buildConflictOpts(input = {}) {
|
|
4539
|
-
if (input.force === true) {
|
|
4540
|
-
return { conflictPolicy: "overwrite" };
|
|
4541
|
-
}
|
|
4542
|
-
if (input.interactive === false) {
|
|
4543
|
-
return { conflictPolicy: "preserve" };
|
|
5097
|
+
if (input.interactive === false) {
|
|
5098
|
+
return { conflictPolicy: "preserve" };
|
|
4544
5099
|
}
|
|
4545
5100
|
if (input.interactive === true) {
|
|
4546
5101
|
return { conflictPolicy: "preserve", onConflict: clackConflictResolver };
|
|
@@ -4636,7 +5191,7 @@ var init_conflict = __esm({
|
|
|
4636
5191
|
});
|
|
4637
5192
|
|
|
4638
5193
|
// src/dedup-write.ts
|
|
4639
|
-
import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, writeFileSync as
|
|
5194
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync5 } from "fs";
|
|
4640
5195
|
import { join as join5 } from "path";
|
|
4641
5196
|
import { sha256Hex as sha256Hex2, sha256Hex12 } from "@noir-ai/core";
|
|
4642
5197
|
async function checkWritePathDedup(root, res, opts) {
|
|
@@ -4785,14 +5340,14 @@ async function promptDedupAction(root, proposedRel, matchedRel, proposedText, si
|
|
|
4785
5340
|
return "preserve";
|
|
4786
5341
|
} else if (choice === "replace") {
|
|
4787
5342
|
try {
|
|
4788
|
-
|
|
5343
|
+
writeFileSync5(join5(root, matchedRel), proposedText, "utf8");
|
|
4789
5344
|
} catch {
|
|
4790
5345
|
}
|
|
4791
5346
|
return "replace";
|
|
4792
5347
|
} else if (choice === "mirror") {
|
|
4793
5348
|
try {
|
|
4794
5349
|
const existing = readFileSync3(join5(root, matchedRel), "utf8");
|
|
4795
|
-
|
|
5350
|
+
writeFileSync5(
|
|
4796
5351
|
join5(root, matchedRel),
|
|
4797
5352
|
`${existing}
|
|
4798
5353
|
|
|
@@ -4831,7 +5386,7 @@ function loadCache(root) {
|
|
|
4831
5386
|
function saveCache(root, cache) {
|
|
4832
5387
|
try {
|
|
4833
5388
|
mkdirSync4(join5(root, ".noir"), { recursive: true });
|
|
4834
|
-
|
|
5389
|
+
writeFileSync5(cachePath(root), JSON.stringify(cache), "utf8");
|
|
4835
5390
|
} catch {
|
|
4836
5391
|
}
|
|
4837
5392
|
}
|
|
@@ -4853,16 +5408,16 @@ function safeRead(abs) {
|
|
|
4853
5408
|
}
|
|
4854
5409
|
}
|
|
4855
5410
|
function withTimeout(p, ms) {
|
|
4856
|
-
return new Promise((
|
|
4857
|
-
const t = setTimeout(() =>
|
|
5411
|
+
return new Promise((resolve3) => {
|
|
5412
|
+
const t = setTimeout(() => resolve3(void 0), ms);
|
|
4858
5413
|
p.then(
|
|
4859
5414
|
(v) => {
|
|
4860
5415
|
clearTimeout(t);
|
|
4861
|
-
|
|
5416
|
+
resolve3(v);
|
|
4862
5417
|
},
|
|
4863
5418
|
() => {
|
|
4864
5419
|
clearTimeout(t);
|
|
4865
|
-
|
|
5420
|
+
resolve3(void 0);
|
|
4866
5421
|
}
|
|
4867
5422
|
);
|
|
4868
5423
|
});
|
|
@@ -4885,13 +5440,16 @@ var init_dedup_write = __esm({
|
|
|
4885
5440
|
});
|
|
4886
5441
|
|
|
4887
5442
|
// src/init.ts
|
|
4888
|
-
import {
|
|
4889
|
-
import {
|
|
5443
|
+
import { existsSync as existsSync3 } from "fs";
|
|
5444
|
+
import { resolveAdapter as resolveAdapter3 } from "@noir-ai/adapters";
|
|
5445
|
+
import { loadProjectInfo as loadProjectInfo12, paths as paths2 } from "@noir-ai/core";
|
|
4890
5446
|
import { scaffold } from "@noir-ai/create";
|
|
4891
|
-
import {
|
|
5447
|
+
import {
|
|
5448
|
+
emitSkillsToDir
|
|
5449
|
+
} from "@noir-ai/skills";
|
|
4892
5450
|
async function init(root, opts) {
|
|
4893
5451
|
assertTransportUrl(opts);
|
|
4894
|
-
const host = opts
|
|
5452
|
+
const host = resolveInitHost(root, opts);
|
|
4895
5453
|
const interactive = resolveInteractive();
|
|
4896
5454
|
const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
|
|
4897
5455
|
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
@@ -4912,15 +5470,29 @@ async function init(root, opts) {
|
|
|
4912
5470
|
return res;
|
|
4913
5471
|
}
|
|
4914
5472
|
if (res.noop) return res;
|
|
4915
|
-
await emitHostSkills(root, host, conflictOpts, interactive);
|
|
5473
|
+
const skillSummary = await emitHostSkills(root, host, conflictOpts, interactive);
|
|
5474
|
+
res.skillConflicts = skillSummary?.conflicts ?? [];
|
|
5475
|
+
res.preservedSkills = skillSummary?.preserved ?? [];
|
|
4916
5476
|
let projectInfo;
|
|
4917
5477
|
try {
|
|
4918
|
-
projectInfo =
|
|
5478
|
+
projectInfo = loadProjectInfo12(root);
|
|
4919
5479
|
} catch {
|
|
4920
5480
|
projectInfo = void 0;
|
|
4921
5481
|
}
|
|
4922
5482
|
const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });
|
|
4923
5483
|
if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);
|
|
5484
|
+
if (res.refreshed.length > 0) {
|
|
5485
|
+
process.stderr.write(
|
|
5486
|
+
`Refreshed ${res.refreshed.length} unedited doc seed(s): ${res.refreshed.join(", ")}
|
|
5487
|
+
`
|
|
5488
|
+
);
|
|
5489
|
+
}
|
|
5490
|
+
if (res.migrationChanged.length > 0) {
|
|
5491
|
+
process.stderr.write(
|
|
5492
|
+
`Migrated ${res.migrationChanged.length} file(s): ${res.migrationChanged.join(", ")}
|
|
5493
|
+
`
|
|
5494
|
+
);
|
|
5495
|
+
}
|
|
4924
5496
|
process.stderr.write(
|
|
4925
5497
|
`Noir initialized in ${root} (host: ${host}, transport: ${opts.transport}).
|
|
4926
5498
|
`
|
|
@@ -4930,6 +5502,19 @@ async function init(root, opts) {
|
|
|
4930
5502
|
);
|
|
4931
5503
|
return res;
|
|
4932
5504
|
}
|
|
5505
|
+
function resolveInitHost(root, opts) {
|
|
5506
|
+
if (opts.host !== void 0) return opts.host;
|
|
5507
|
+
if (!existsSync3(paths2.config(root))) return "claude";
|
|
5508
|
+
try {
|
|
5509
|
+
return loadProjectInfo12(root).config.host;
|
|
5510
|
+
} catch {
|
|
5511
|
+
process.stderr.write(
|
|
5512
|
+
`Could not read the configured host from ${paths2.config(root)}; using 'claude'.
|
|
5513
|
+
`
|
|
5514
|
+
);
|
|
5515
|
+
return "claude";
|
|
5516
|
+
}
|
|
5517
|
+
}
|
|
4933
5518
|
function reportPlannedWrites(res) {
|
|
4934
5519
|
log("Dry run \u2014 no files were written.");
|
|
4935
5520
|
if (res.written.length > 0) {
|
|
@@ -4939,6 +5524,10 @@ function reportPlannedWrites(res) {
|
|
|
4939
5524
|
log(mode === void 0 ? ` ${p}` : ` ${p} (mode ${mode.toString(8).padStart(3, "0")})`);
|
|
4940
5525
|
}
|
|
4941
5526
|
}
|
|
5527
|
+
if (res.refreshed.length > 0) {
|
|
5528
|
+
log("Would refresh (unedited older copy):");
|
|
5529
|
+
for (const p of res.refreshed) log(` ${p}`);
|
|
5530
|
+
}
|
|
4942
5531
|
if (res.skipped.length > 0) {
|
|
4943
5532
|
log("Would leave as-is (already present):");
|
|
4944
5533
|
for (const p of res.skipped) log(` ${p}`);
|
|
@@ -4949,12 +5538,12 @@ function reportPlannedWrites(res) {
|
|
|
4949
5538
|
}
|
|
4950
5539
|
}
|
|
4951
5540
|
async function emitHostSkills(root, host, conflictOpts, interactive) {
|
|
4952
|
-
const adapter =
|
|
5541
|
+
const adapter = resolveAdapter3(host);
|
|
4953
5542
|
const skillsDir = adapter.skillsDir?.({ root });
|
|
4954
5543
|
if (skillsDir === void 0) {
|
|
4955
5544
|
process.stderr.write(`host '${host}' has no skill emitter; skipping skills
|
|
4956
5545
|
`);
|
|
4957
|
-
return;
|
|
5546
|
+
return void 0;
|
|
4958
5547
|
}
|
|
4959
5548
|
const target = host;
|
|
4960
5549
|
const skillOpts = {
|
|
@@ -4966,12 +5555,20 @@ async function emitHostSkills(root, host, conflictOpts, interactive) {
|
|
|
4966
5555
|
if (conflictOpts.onConflict !== void 0) {
|
|
4967
5556
|
skillOpts.onConflict = conflictOpts.onConflict;
|
|
4968
5557
|
}
|
|
4969
|
-
const summary = await
|
|
5558
|
+
const summary = await emitSkillsToDir(skillsDir, skillOpts);
|
|
4970
5559
|
const relDir = skillsDir.replace(`${root}/`, "");
|
|
4971
5560
|
process.stderr.write(
|
|
4972
5561
|
`Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).
|
|
4973
5562
|
`
|
|
4974
5563
|
);
|
|
5564
|
+
const stale = preservedStaleLine(summary.preserved ?? []);
|
|
5565
|
+
if (stale !== void 0) process.stderr.write(`${stale}
|
|
5566
|
+
`);
|
|
5567
|
+
return summary;
|
|
5568
|
+
}
|
|
5569
|
+
function preservedStaleLine(preserved) {
|
|
5570
|
+
if (preserved.length === 0) return void 0;
|
|
5571
|
+
return `${preserved.length} skill(s) preserved as stale (interactive TTY required to refresh): ${preserved.join(", ")}`;
|
|
4975
5572
|
}
|
|
4976
5573
|
function assertTransportUrl(opts) {
|
|
4977
5574
|
if (opts.transport === "streamable-http" && opts.url === void 0) {
|
|
@@ -5004,6 +5601,251 @@ var init_init = __esm({
|
|
|
5004
5601
|
}
|
|
5005
5602
|
});
|
|
5006
5603
|
|
|
5604
|
+
// src/commands/skills.ts
|
|
5605
|
+
import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
|
|
5606
|
+
import { loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
|
|
5607
|
+
import {
|
|
5608
|
+
buildRegistry,
|
|
5609
|
+
discoverAll,
|
|
5610
|
+
emitSkillsToDir as emitSkillsToDir2,
|
|
5611
|
+
lintSkill
|
|
5612
|
+
} from "@noir-ai/skills";
|
|
5613
|
+
function categoryOf(name) {
|
|
5614
|
+
const mapped = CATEGORY[name];
|
|
5615
|
+
if (mapped !== void 0) return mapped;
|
|
5616
|
+
return name.replace(/^noir-/, "") || "general";
|
|
5617
|
+
}
|
|
5618
|
+
function toRow(s, kind = "builtin") {
|
|
5619
|
+
const description = typeof s.frontmatter.description === "string" ? s.frontmatter.description : "";
|
|
5620
|
+
const status2 = s.skillMd.includes("> **Stub:**") ? "stub" : "full";
|
|
5621
|
+
const category = s.frontmatter.metadata?.category?.trim() || categoryOf(s.name);
|
|
5622
|
+
return { name: s.name, category, description, kind, status: status2 };
|
|
5623
|
+
}
|
|
5624
|
+
function truncate(text, max) {
|
|
5625
|
+
if (text.length <= max) return text;
|
|
5626
|
+
const slice = text.slice(0, Math.max(0, max - 1));
|
|
5627
|
+
const boundary = slice.lastIndexOf(" ");
|
|
5628
|
+
const head = boundary > 0 ? slice.slice(0, boundary) : slice;
|
|
5629
|
+
return `${head.trimEnd()}\u2026`;
|
|
5630
|
+
}
|
|
5631
|
+
async function skillsList(opts) {
|
|
5632
|
+
let builtins;
|
|
5633
|
+
let integrations;
|
|
5634
|
+
try {
|
|
5635
|
+
const all = discoverAll();
|
|
5636
|
+
builtins = all.builtins;
|
|
5637
|
+
integrations = all.integrations;
|
|
5638
|
+
} catch (err) {
|
|
5639
|
+
fail(
|
|
5640
|
+
EXIT.ERROR,
|
|
5641
|
+
`skills list: could not discover skills (${err instanceof Error ? err.message : String(err)})`,
|
|
5642
|
+
opts
|
|
5643
|
+
);
|
|
5644
|
+
}
|
|
5645
|
+
const rows = [
|
|
5646
|
+
...builtins.map((b) => toRow(b, "builtin")),
|
|
5647
|
+
...integrations.map((i) => toRow(i, "integration"))
|
|
5648
|
+
];
|
|
5649
|
+
const data = { count: rows.length, skills: rows };
|
|
5650
|
+
if (opts.json === true) {
|
|
5651
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
5652
|
+
`);
|
|
5653
|
+
return;
|
|
5654
|
+
}
|
|
5655
|
+
const intCount = integrations.length;
|
|
5656
|
+
const banner = intCount > 0 ? `noir skills \u2014 ${rows.length} skill${rows.length === 1 ? "" : "s"} (${builtins.length} builtin, ${intCount} integration${intCount === 1 ? "" : "s"})` : `noir skills \u2014 ${rows.length} builtin skill${rows.length === 1 ? "" : "s"}`;
|
|
5657
|
+
log(banner, opts);
|
|
5658
|
+
table(
|
|
5659
|
+
rows.map((r) => ({
|
|
5660
|
+
Skill: r.name,
|
|
5661
|
+
Kind: r.kind,
|
|
5662
|
+
Category: r.category,
|
|
5663
|
+
Status: r.status,
|
|
5664
|
+
Description: truncate(r.description, 80)
|
|
5665
|
+
})),
|
|
5666
|
+
["Skill", "Kind", "Category", "Status", "Description"],
|
|
5667
|
+
opts
|
|
5668
|
+
);
|
|
5669
|
+
}
|
|
5670
|
+
async function skillsSync(opts) {
|
|
5671
|
+
const root = process.cwd();
|
|
5672
|
+
let project;
|
|
5673
|
+
try {
|
|
5674
|
+
project = loadProjectInfo13(root);
|
|
5675
|
+
} catch {
|
|
5676
|
+
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
5677
|
+
}
|
|
5678
|
+
const adapter = resolveAdapter4(project.config.host);
|
|
5679
|
+
if (!adapter.skillsDir) {
|
|
5680
|
+
const data2 = {
|
|
5681
|
+
emitted: [],
|
|
5682
|
+
references: 0,
|
|
5683
|
+
host: project.config.host,
|
|
5684
|
+
reason: "no-skill-emitter"
|
|
5685
|
+
};
|
|
5686
|
+
if (opts.json === true) {
|
|
5687
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
|
|
5688
|
+
`);
|
|
5689
|
+
return;
|
|
5690
|
+
}
|
|
5691
|
+
info("This host has no skill emitter; nothing to sync.", opts);
|
|
5692
|
+
return;
|
|
5693
|
+
}
|
|
5694
|
+
const dir = adapter.skillsDir({ root });
|
|
5695
|
+
const target = project.config.host;
|
|
5696
|
+
const summary = await emitSkillsToDir2(dir, { target });
|
|
5697
|
+
const pruned = summary.pruned ?? [];
|
|
5698
|
+
const preserved = summary.preserved ?? [];
|
|
5699
|
+
const data = {
|
|
5700
|
+
emitted: summary.emitted,
|
|
5701
|
+
references: summary.references,
|
|
5702
|
+
dir,
|
|
5703
|
+
pruned,
|
|
5704
|
+
// What the count above does NOT cover: skills whose files were preserved
|
|
5705
|
+
// (stale on disk, by design, in a non-interactive run). Same field `noir
|
|
5706
|
+
// init --json` carries, so a consumer reads one shape from every emitter.
|
|
5707
|
+
preserved
|
|
5708
|
+
};
|
|
5709
|
+
if (opts.json === true) {
|
|
5710
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
5711
|
+
`);
|
|
5712
|
+
return;
|
|
5713
|
+
}
|
|
5714
|
+
log(
|
|
5715
|
+
`Synced ${summary.emitted.length} Noir skill${summary.emitted.length === 1 ? "" : "s"} to ${dir}.`,
|
|
5716
|
+
opts
|
|
5717
|
+
);
|
|
5718
|
+
if (pruned.length > 0) {
|
|
5719
|
+
warn(
|
|
5720
|
+
`Pruned ${pruned.length} stale noir-* skill dir${pruned.length === 1 ? "" : "s"}: ${pruned.join(", ")}`,
|
|
5721
|
+
opts
|
|
5722
|
+
);
|
|
5723
|
+
}
|
|
5724
|
+
const stale = preservedStaleLine(preserved);
|
|
5725
|
+
if (stale !== void 0) warn(stale, opts);
|
|
5726
|
+
}
|
|
5727
|
+
async function skillsLint(opts) {
|
|
5728
|
+
let skills;
|
|
5729
|
+
try {
|
|
5730
|
+
const all = discoverAll();
|
|
5731
|
+
skills = [...all.builtins, ...all.integrations].map(lintSkill);
|
|
5732
|
+
} catch (err) {
|
|
5733
|
+
fail(
|
|
5734
|
+
EXIT.ERROR,
|
|
5735
|
+
`skills lint: could not lint skills (${err instanceof Error ? err.message : String(err)})`,
|
|
5736
|
+
opts
|
|
5737
|
+
);
|
|
5738
|
+
}
|
|
5739
|
+
const errored = skills.filter((s) => s.errors.length > 0);
|
|
5740
|
+
const warned = skills.filter((s) => s.warnings.length > 0 && s.errors.length === 0);
|
|
5741
|
+
const data = { count: skills.length, errored: errored.length, skills };
|
|
5742
|
+
if (errored.length > 0) {
|
|
5743
|
+
const message = `skills lint: ${errored.length} skill${errored.length === 1 ? "" : "s"} failed validation`;
|
|
5744
|
+
if (opts.json === true) {
|
|
5745
|
+
process.stdout.write(
|
|
5746
|
+
`${JSON.stringify({ ok: false, error: { code: EXIT.ERROR, message }, data })}
|
|
5747
|
+
`
|
|
5748
|
+
);
|
|
5749
|
+
process.exitCode = EXIT.ERROR;
|
|
5750
|
+
return;
|
|
5751
|
+
}
|
|
5752
|
+
warn(
|
|
5753
|
+
`noir skills lint \u2014 ${errored.length} skill${errored.length === 1 ? "" : "s"} FAIL validation:`,
|
|
5754
|
+
opts
|
|
5755
|
+
);
|
|
5756
|
+
for (const s of errored) {
|
|
5757
|
+
for (const e of s.errors) warn(` ${s.name}: ${e}`, opts);
|
|
5758
|
+
}
|
|
5759
|
+
if (warned.length > 0) {
|
|
5760
|
+
info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
|
|
5761
|
+
for (const s of warned) {
|
|
5762
|
+
for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
|
|
5763
|
+
}
|
|
5764
|
+
}
|
|
5765
|
+
fail(EXIT.ERROR, message, opts);
|
|
5766
|
+
}
|
|
5767
|
+
if (opts.json === true) {
|
|
5768
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
5769
|
+
`);
|
|
5770
|
+
return;
|
|
5771
|
+
}
|
|
5772
|
+
log(`noir skills lint \u2014 ${skills.length} skills, all validate clean.`, opts);
|
|
5773
|
+
if (warned.length > 0) {
|
|
5774
|
+
info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
|
|
5775
|
+
for (const s of warned) {
|
|
5776
|
+
for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
|
|
5777
|
+
}
|
|
5778
|
+
}
|
|
5779
|
+
}
|
|
5780
|
+
async function skillsRegistry(opts) {
|
|
5781
|
+
let reg;
|
|
5782
|
+
try {
|
|
5783
|
+
reg = buildRegistry();
|
|
5784
|
+
} catch (err) {
|
|
5785
|
+
fail(
|
|
5786
|
+
EXIT.ERROR,
|
|
5787
|
+
`skills registry: could not build registry (${err instanceof Error ? err.message : String(err)})`,
|
|
5788
|
+
opts
|
|
5789
|
+
);
|
|
5790
|
+
}
|
|
5791
|
+
const data = { count: reg.length, skills: reg };
|
|
5792
|
+
if (opts.json === true) {
|
|
5793
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
5794
|
+
`);
|
|
5795
|
+
return;
|
|
5796
|
+
}
|
|
5797
|
+
log(`noir skills registry \u2014 ${reg.length} skills.`, opts);
|
|
5798
|
+
table(
|
|
5799
|
+
reg.map((r) => ({
|
|
5800
|
+
Skill: r.name,
|
|
5801
|
+
Kind: r.kind,
|
|
5802
|
+
Category: r.category,
|
|
5803
|
+
Version: r.version,
|
|
5804
|
+
Status: r.status,
|
|
5805
|
+
Refs: String(r.referenceCount),
|
|
5806
|
+
Lines: String(r.lines)
|
|
5807
|
+
})),
|
|
5808
|
+
["Skill", "Kind", "Category", "Version", "Status", "Refs", "Lines"],
|
|
5809
|
+
opts
|
|
5810
|
+
);
|
|
5811
|
+
}
|
|
5812
|
+
var CATEGORY;
|
|
5813
|
+
var init_skills = __esm({
|
|
5814
|
+
"src/commands/skills.ts"() {
|
|
5815
|
+
"use strict";
|
|
5816
|
+
init_init();
|
|
5817
|
+
init_output();
|
|
5818
|
+
CATEGORY = {
|
|
5819
|
+
"noir-brainstorming": "discovery",
|
|
5820
|
+
"noir-sync": "discovery",
|
|
5821
|
+
"noir-checkpoint": "discovery",
|
|
5822
|
+
"noir-exploring": "discovery",
|
|
5823
|
+
"noir-spec": "spec",
|
|
5824
|
+
"noir-planning": "plan",
|
|
5825
|
+
"noir-prd": "plan",
|
|
5826
|
+
"noir-executing-plans": "execute",
|
|
5827
|
+
"noir-test-driven-development": "execute",
|
|
5828
|
+
"noir-systematic-debugging": "execute",
|
|
5829
|
+
"noir-subagent": "execute",
|
|
5830
|
+
"noir-parallel": "execute",
|
|
5831
|
+
"noir-verifying": "verify",
|
|
5832
|
+
"noir-security": "verify",
|
|
5833
|
+
"noir-wrap": "document",
|
|
5834
|
+
"noir-readme": "document",
|
|
5835
|
+
"noir-shipping": "git",
|
|
5836
|
+
"noir-worktree": "git",
|
|
5837
|
+
"noir-recall": "memory",
|
|
5838
|
+
"noir-remember": "memory",
|
|
5839
|
+
"noir-context": "context",
|
|
5840
|
+
"noir-frontend": "domain",
|
|
5841
|
+
"noir-backend": "domain",
|
|
5842
|
+
"noir-doctor": "meta",
|
|
5843
|
+
"noir-writing-skills": "meta",
|
|
5844
|
+
"noir-rules": "meta"
|
|
5845
|
+
};
|
|
5846
|
+
}
|
|
5847
|
+
});
|
|
5848
|
+
|
|
5007
5849
|
// src/serve.ts
|
|
5008
5850
|
import { applyNoirEnv, loadProjectInfo as loadProjectInfo14 } from "@noir-ai/core";
|
|
5009
5851
|
import { ensureDaemonRunning as ensureDaemonRunning2, startStdioServer } from "@noir-ai/daemon";
|
|
@@ -5043,22 +5885,21 @@ var create_exports = {};
|
|
|
5043
5885
|
__export(create_exports, {
|
|
5044
5886
|
create: () => create
|
|
5045
5887
|
});
|
|
5046
|
-
import { resolve } from "path";
|
|
5888
|
+
import { resolve as resolve2 } from "path";
|
|
5047
5889
|
import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
|
|
5048
5890
|
import { loadProjectInfo as loadProjectInfo15 } from "@noir-ai/core";
|
|
5049
5891
|
import { scaffold as scaffold2 } from "@noir-ai/create";
|
|
5050
5892
|
import { emitSkillsToDir as emitSkillsToDir3 } from "@noir-ai/skills";
|
|
5051
5893
|
async function create(dir, opts) {
|
|
5052
5894
|
assertTransportUrl(opts);
|
|
5053
|
-
const root =
|
|
5054
|
-
const host = opts.host ?? "claude";
|
|
5895
|
+
const root = resolve2(dir ?? process.cwd());
|
|
5055
5896
|
const interactive = resolveInteractive();
|
|
5056
5897
|
const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
|
|
5057
5898
|
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
5058
5899
|
const res = await scaffold2({
|
|
5059
5900
|
root,
|
|
5060
5901
|
mode: "create",
|
|
5061
|
-
host,
|
|
5902
|
+
...opts.host !== void 0 ? { host: opts.host } : {},
|
|
5062
5903
|
transport: opts.transport,
|
|
5063
5904
|
interactive,
|
|
5064
5905
|
...opts.url !== void 0 ? { url: opts.url } : {},
|
|
@@ -5071,6 +5912,7 @@ async function create(dir, opts) {
|
|
|
5071
5912
|
return res;
|
|
5072
5913
|
}
|
|
5073
5914
|
if (res.noop) return res;
|
|
5915
|
+
const host = res.host;
|
|
5074
5916
|
const adapter = resolveAdapter5(host);
|
|
5075
5917
|
const skillsDir = adapter.skillsDir?.({ root });
|
|
5076
5918
|
if (skillsDir === void 0) {
|
|
@@ -5093,6 +5935,9 @@ async function create(dir, opts) {
|
|
|
5093
5935
|
`Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).
|
|
5094
5936
|
`
|
|
5095
5937
|
);
|
|
5938
|
+
const stale = preservedStaleLine(summary.preserved ?? []);
|
|
5939
|
+
if (stale !== void 0) process.stderr.write(`${stale}
|
|
5940
|
+
`);
|
|
5096
5941
|
}
|
|
5097
5942
|
let projectInfo;
|
|
5098
5943
|
try {
|
|
@@ -5122,9 +5967,9 @@ var sync_exports = {};
|
|
|
5122
5967
|
__export(sync_exports, {
|
|
5123
5968
|
sync: () => sync
|
|
5124
5969
|
});
|
|
5125
|
-
import { existsSync as
|
|
5970
|
+
import { existsSync as existsSync4 } from "fs";
|
|
5126
5971
|
import { resolveAdapter as resolveAdapter6 } from "@noir-ai/adapters";
|
|
5127
|
-
import { loadProjectInfo as loadProjectInfo16, paths } from "@noir-ai/core";
|
|
5972
|
+
import { loadProjectInfo as loadProjectInfo16, paths as paths3 } from "@noir-ai/core";
|
|
5128
5973
|
import { scaffold as scaffold3 } from "@noir-ai/create";
|
|
5129
5974
|
import { emitSkillsToDir as emitSkillsToDir4 } from "@noir-ai/skills";
|
|
5130
5975
|
async function sync(root, opts = {}) {
|
|
@@ -5184,6 +6029,9 @@ async function sync(root, opts = {}) {
|
|
|
5184
6029
|
`
|
|
5185
6030
|
);
|
|
5186
6031
|
}
|
|
6032
|
+
const stale = preservedStaleLine(summary.preserved ?? []);
|
|
6033
|
+
if (stale !== void 0) process.stderr.write(`${stale}
|
|
6034
|
+
`);
|
|
5187
6035
|
}
|
|
5188
6036
|
const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });
|
|
5189
6037
|
if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);
|
|
@@ -5191,7 +6039,7 @@ async function sync(root, opts = {}) {
|
|
|
5191
6039
|
}
|
|
5192
6040
|
function resolveSyncHost(root, opts) {
|
|
5193
6041
|
if (opts.host !== void 0) return opts.host;
|
|
5194
|
-
if (!
|
|
6042
|
+
if (!existsSync4(paths3.config(root))) return "claude";
|
|
5195
6043
|
try {
|
|
5196
6044
|
return loadProjectInfo16(root).config.host;
|
|
5197
6045
|
} catch {
|
|
@@ -5321,13 +6169,15 @@ function createProgram() {
|
|
|
5321
6169
|
"--upgrade",
|
|
5322
6170
|
"run scaffold migrations before re-emitting (re-run on an existing project)"
|
|
5323
6171
|
).option("--force", "re-scaffold even if already initialized (bypasses the already-init no-op)").option("--dry-run", "report planned writes without writing anything").option("--preview", "alias for --dry-run").addOption(
|
|
5324
|
-
//
|
|
5325
|
-
//
|
|
5326
|
-
//
|
|
5327
|
-
//
|
|
6172
|
+
// Target host. When the flag is absent the host is read from an existing
|
|
6173
|
+
// `.noir/config.yml`, falling back to `'claude'` (so a fresh project is
|
|
6174
|
+
// unaffected). The chosen host is forwarded to scaffold() + skills
|
|
6175
|
+
// emission via resolveAdapter. The choice list mirrors `SUPPORTED_HOSTS`
|
|
6176
|
+
// so a new host lands here automatically; commander rejects anything else
|
|
6177
|
+
// as usage=2.
|
|
5328
6178
|
new Option(
|
|
5329
6179
|
"--host <id>",
|
|
5330
|
-
"target agentic CLI (default:
|
|
6180
|
+
"target agentic CLI (default: the host in .noir/config.yml, else claude)"
|
|
5331
6181
|
).choices(SUPPORTED_HOSTS2)
|
|
5332
6182
|
).action(
|
|
5333
6183
|
async (opts, cmd) => {
|
|
@@ -5390,7 +6240,7 @@ function createProgram() {
|
|
|
5390
6240
|
"strip-replace managed regions (discard hand-edits inside <!-- noir:* --> markers)"
|
|
5391
6241
|
)
|
|
5392
6242
|
).addOption(
|
|
5393
|
-
//
|
|
6243
|
+
// Optional `--host` override. When omitted, sync reads host from
|
|
5394
6244
|
// `.noir/config.yml` (whatever `noir init --host <id>` persisted). The
|
|
5395
6245
|
// override is rarely needed — documented as advanced.
|
|
5396
6246
|
new Option(
|
|
@@ -5946,7 +6796,7 @@ var init_bin = __esm({
|
|
|
5946
6796
|
}
|
|
5947
6797
|
},
|
|
5948
6798
|
// The grouped home menu resolves its sections against the LIVE palette
|
|
5949
|
-
// registry
|
|
6799
|
+
// registry — the same source the TUI palette
|
|
5950
6800
|
// uses — so the menu cannot drift from the commander tree.
|
|
5951
6801
|
commands: buildPaletteCommands(createProgram())
|
|
5952
6802
|
};
|
|
@@ -6367,7 +7217,7 @@ var init_daemon_client = __esm({
|
|
|
6367
7217
|
|
|
6368
7218
|
// src/commands/doctor.ts
|
|
6369
7219
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
6370
|
-
import { existsSync as
|
|
7220
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync } from "fs";
|
|
6371
7221
|
import { dirname, join as join6, relative } from "path";
|
|
6372
7222
|
import { fileURLToPath } from "url";
|
|
6373
7223
|
import { resolveAdapter as resolveAdapter7 } from "@noir-ai/adapters";
|
|
@@ -6377,11 +7227,17 @@ import {
|
|
|
6377
7227
|
loadNoirEnv as loadNoirEnv3,
|
|
6378
7228
|
loadProjectInfo as loadProjectInfo18,
|
|
6379
7229
|
NOIR_VERSION as NOIR_VERSION6,
|
|
6380
|
-
paths as
|
|
7230
|
+
paths as paths4,
|
|
6381
7231
|
readInstallRecord as readInstallRecord4,
|
|
6382
7232
|
readUpdateCache as readUpdateCache3
|
|
6383
7233
|
} from "@noir-ai/core";
|
|
6384
|
-
import {
|
|
7234
|
+
import {
|
|
7235
|
+
CURRENT_SCAFFOLD_VERSION,
|
|
7236
|
+
isStaleSeed,
|
|
7237
|
+
loadTemplate,
|
|
7238
|
+
readScaffoldVersion,
|
|
7239
|
+
render
|
|
7240
|
+
} from "@noir-ai/create";
|
|
6385
7241
|
import { pidAlive as pidAlive4, readProjectDaemonRecord as readProjectDaemonRecord3 } from "@noir-ai/daemon";
|
|
6386
7242
|
import { resolveModelConfig as resolveModelConfig2 } from "@noir-ai/model";
|
|
6387
7243
|
async function probeOnnx() {
|
|
@@ -6418,8 +7274,8 @@ async function checkRuntime(checks) {
|
|
|
6418
7274
|
});
|
|
6419
7275
|
}
|
|
6420
7276
|
function checkConfig(root) {
|
|
6421
|
-
const configPath =
|
|
6422
|
-
if (!
|
|
7277
|
+
const configPath = paths4.config(root);
|
|
7278
|
+
if (!existsSync5(configPath)) {
|
|
6423
7279
|
return {
|
|
6424
7280
|
project: void 0,
|
|
6425
7281
|
result: {
|
|
@@ -6518,8 +7374,8 @@ async function checkStore(checks, project, root) {
|
|
|
6518
7374
|
checks.push({ name: "store", status: "warn", detail: "skipped \u2014 not initialized" });
|
|
6519
7375
|
return;
|
|
6520
7376
|
}
|
|
6521
|
-
const dbPath =
|
|
6522
|
-
if (!
|
|
7377
|
+
const dbPath = paths4.storeDb(root, project.id);
|
|
7378
|
+
if (!existsSync5(dbPath)) {
|
|
6523
7379
|
checks.push({
|
|
6524
7380
|
name: "store",
|
|
6525
7381
|
status: "warn",
|
|
@@ -6534,7 +7390,7 @@ async function checkStore(checks, project, root) {
|
|
|
6534
7390
|
checks.push({
|
|
6535
7391
|
name: "store",
|
|
6536
7392
|
status: "ok",
|
|
6537
|
-
detail: `opens readonly (db: ${
|
|
7393
|
+
detail: `opens readonly (db: ${paths4.storeDb(root, project.id)})`
|
|
6538
7394
|
});
|
|
6539
7395
|
} catch (e) {
|
|
6540
7396
|
checks.push({ name: "store", status: "fail", detail: `open failed: ${msg(e)}` });
|
|
@@ -6548,7 +7404,7 @@ async function checkStore(checks, project, root) {
|
|
|
6548
7404
|
}
|
|
6549
7405
|
}
|
|
6550
7406
|
function checkNoirEnv(checks, root, loaded) {
|
|
6551
|
-
const path = join6(
|
|
7407
|
+
const path = join6(paths4.noirDir(root), ".env");
|
|
6552
7408
|
let st;
|
|
6553
7409
|
try {
|
|
6554
7410
|
st = statSync(path);
|
|
@@ -6627,9 +7483,10 @@ function checkProvider(checks, project, sources) {
|
|
|
6627
7483
|
for (const name of names) {
|
|
6628
7484
|
const p = resolved.providers[name];
|
|
6629
7485
|
if (!p) continue;
|
|
6630
|
-
const fromFile = p.apiKeyEnv !== void 0 && sources[p.apiKeyEnv] === "file";
|
|
6631
|
-
const
|
|
6632
|
-
|
|
7486
|
+
const fromFile = p.apiKeyEnv !== void 0 && sources[p.apiKeyEnv] === "file" || p.authTokenEnv !== void 0 && sources[p.authTokenEnv] === "file";
|
|
7487
|
+
const credentialName = p.apiKeyEnv ?? p.authTokenEnv;
|
|
7488
|
+
const key = p.hasKey ? fromFile ? "key present (from .noir/.env)" : "key present" : credentialName ? `missing ${credentialName}` : "anonymous";
|
|
7489
|
+
if (!p.hasKey && credentialName) missing = true;
|
|
6633
7490
|
parts.push(`${name}/${p.model ?? "?"} (${key})`);
|
|
6634
7491
|
}
|
|
6635
7492
|
checks.push({
|
|
@@ -6665,13 +7522,30 @@ function checkScaffoldVersion(checks, root) {
|
|
|
6665
7522
|
checks.push({ name: "scaffold version", status: status2, detail });
|
|
6666
7523
|
return { onDisk, current, drift };
|
|
6667
7524
|
}
|
|
7525
|
+
function checkSeedDrift(checks, root) {
|
|
7526
|
+
const seedPath = join6(root, ".noir", ".env.example");
|
|
7527
|
+
let bytes;
|
|
7528
|
+
try {
|
|
7529
|
+
bytes = readFileSync4(seedPath, "utf8");
|
|
7530
|
+
} catch {
|
|
7531
|
+
return;
|
|
7532
|
+
}
|
|
7533
|
+
const currentRender = render(loadTemplate("env.example.tmpl"), {});
|
|
7534
|
+
if (isStaleSeed("envExample", bytes, currentRender)) {
|
|
7535
|
+
checks.push({
|
|
7536
|
+
name: "doc seed",
|
|
7537
|
+
status: "warn",
|
|
7538
|
+
detail: `.noir/.env.example is an unedited copy of an older Noir \u2014 run \`noir init --upgrade\` to refresh it`
|
|
7539
|
+
});
|
|
7540
|
+
}
|
|
7541
|
+
}
|
|
6668
7542
|
function checkRulesMdBudget(checks, root, project) {
|
|
6669
7543
|
if (!project) {
|
|
6670
7544
|
checks.push({ name: "rules budget", status: "warn", detail: "skipped \u2014 not initialized" });
|
|
6671
7545
|
return null;
|
|
6672
7546
|
}
|
|
6673
|
-
const rulesPath =
|
|
6674
|
-
if (!
|
|
7547
|
+
const rulesPath = paths4.rulesMd(root);
|
|
7548
|
+
if (!existsSync5(rulesPath)) {
|
|
6675
7549
|
checks.push({
|
|
6676
7550
|
name: "rules budget",
|
|
6677
7551
|
status: "ok",
|
|
@@ -6735,7 +7609,7 @@ function checkHostArtifacts(checks, root, project) {
|
|
|
6735
7609
|
if (host === "claude") expected.push("CLAUDE.md");
|
|
6736
7610
|
else if (host === "gemini") expected.push("GEMINI.md");
|
|
6737
7611
|
expected.push(relOr(adapter.mcpConfigPath?.(ectx) ?? ".mcp.json", root));
|
|
6738
|
-
const missing = expected.filter((p) => !
|
|
7612
|
+
const missing = expected.filter((p) => !existsSync5(joinRoot(root, p)));
|
|
6739
7613
|
const status2 = missing.length === 0 ? "ok" : "warn";
|
|
6740
7614
|
const detail = missing.length === 0 ? `host=${host} \xB7 all ${expected.length} primary artifacts present` : `host=${host} \xB7 missing ${missing.join(", ")} (run \`noir sync\` to restore)`;
|
|
6741
7615
|
checks.push({ name: "host artifacts", status: status2, detail });
|
|
@@ -6752,7 +7626,7 @@ function checkNestedNoir(checks, root) {
|
|
|
6752
7626
|
".noir/.npmignore",
|
|
6753
7627
|
".noir/.prettierignore"
|
|
6754
7628
|
];
|
|
6755
|
-
const found = candidates.filter((rel) =>
|
|
7629
|
+
const found = candidates.filter((rel) => existsSync5(join6(root, rel)));
|
|
6756
7630
|
const detected = found.length > 0;
|
|
6757
7631
|
checks.push({
|
|
6758
7632
|
name: "nested .noir",
|
|
@@ -6766,7 +7640,7 @@ function collectDedupCandidates(root) {
|
|
|
6766
7640
|
const out = [];
|
|
6767
7641
|
for (const rel of rels) {
|
|
6768
7642
|
const abs = join6(root, rel);
|
|
6769
|
-
if (!
|
|
7643
|
+
if (!existsSync5(abs)) continue;
|
|
6770
7644
|
let text;
|
|
6771
7645
|
try {
|
|
6772
7646
|
text = readFileSync4(abs, "utf8");
|
|
@@ -6827,12 +7701,12 @@ function resolveWorkspacePackagesDir() {
|
|
|
6827
7701
|
let here = dirname(fileURLToPath(import.meta.url));
|
|
6828
7702
|
for (let i = 0; i < 8; i++) {
|
|
6829
7703
|
const pkgPath = join6(here, "package.json");
|
|
6830
|
-
if (
|
|
7704
|
+
if (existsSync5(pkgPath)) {
|
|
6831
7705
|
try {
|
|
6832
7706
|
const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
6833
7707
|
if (pkg.name === "@noir-ai/cli") {
|
|
6834
7708
|
const packagesDir = dirname(here);
|
|
6835
|
-
if (
|
|
7709
|
+
if (existsSync5(join6(dirname(packagesDir), "pnpm-workspace.yaml"))) {
|
|
6836
7710
|
return packagesDir;
|
|
6837
7711
|
}
|
|
6838
7712
|
return null;
|
|
@@ -6891,7 +7765,7 @@ function checkPublish(checks, packagesDir, opts = {}) {
|
|
|
6891
7765
|
let cliDir = null;
|
|
6892
7766
|
for (const name of entries) {
|
|
6893
7767
|
const pkgPath = join6(packagesDir, name, "package.json");
|
|
6894
|
-
if (!
|
|
7768
|
+
if (!existsSync5(pkgPath)) continue;
|
|
6895
7769
|
let pkg;
|
|
6896
7770
|
try {
|
|
6897
7771
|
pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
|
|
@@ -6999,6 +7873,7 @@ async function doctor(opts = {}) {
|
|
|
6999
7873
|
checkProvider(checks, project, noirEnv.sources);
|
|
7000
7874
|
checkNoirEnv(checks, root, noirEnv);
|
|
7001
7875
|
const scaffold4 = checkScaffoldVersion(checks, root);
|
|
7876
|
+
checkSeedDrift(checks, root);
|
|
7002
7877
|
const rules = checkRulesMdBudget(checks, root, project);
|
|
7003
7878
|
const host = checkHostArtifacts(checks, root, project);
|
|
7004
7879
|
checkNestedNoir(checks, root);
|