@noir-ai/cli 1.14.0 → 1.15.0-beta.1
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/bin.js
CHANGED
|
@@ -635,16 +635,36 @@ function spinner(text = "", opts = {}) {
|
|
|
635
635
|
}
|
|
636
636
|
return new OraSpinner(text);
|
|
637
637
|
}
|
|
638
|
+
function failureEnvelope(exitCode, message) {
|
|
639
|
+
return `${JSON.stringify({ ok: false, error: { code: exitCode, message } })}
|
|
640
|
+
`;
|
|
641
|
+
}
|
|
638
642
|
function fail(exitCode, message, opts = {}) {
|
|
639
643
|
if (isJsonMode(opts)) {
|
|
640
|
-
process.stdout.write(
|
|
641
|
-
`);
|
|
644
|
+
process.stdout.write(failureEnvelope(exitCode, message));
|
|
642
645
|
} else if (message.length > 0) {
|
|
643
646
|
process.stderr.write(`${message}
|
|
644
647
|
`);
|
|
645
648
|
}
|
|
646
649
|
throw new CommanderError(exitCode, NOIR_ERROR_CODE, message);
|
|
647
650
|
}
|
|
651
|
+
function failAndExit(exitCode, message, opts = {}) {
|
|
652
|
+
process.exitCode = exitCode;
|
|
653
|
+
const jsonMode = isJsonMode(opts);
|
|
654
|
+
const text = jsonMode ? failureEnvelope(exitCode, message) : message.length > 0 ? `${message}
|
|
655
|
+
` : "";
|
|
656
|
+
if (text.length === 0) {
|
|
657
|
+
process.exit(exitCode);
|
|
658
|
+
}
|
|
659
|
+
let left = false;
|
|
660
|
+
const leave = () => {
|
|
661
|
+
if (left) return;
|
|
662
|
+
left = true;
|
|
663
|
+
process.exit(exitCode);
|
|
664
|
+
};
|
|
665
|
+
(jsonMode ? process.stdout : process.stderr).write(text, leave);
|
|
666
|
+
setTimeout(leave, EXIT_FLUSH_MS);
|
|
667
|
+
}
|
|
648
668
|
function requireInteractive(opts, hint) {
|
|
649
669
|
if (!isInteractive(opts)) {
|
|
650
670
|
fail(
|
|
@@ -685,7 +705,7 @@ function handleError(err) {
|
|
|
685
705
|
}
|
|
686
706
|
process.exitCode = inferExitCode(err);
|
|
687
707
|
}
|
|
688
|
-
var EXIT, NoirCliError, NOIR_ERROR_CODE, NoopSpinner, OraSpinner;
|
|
708
|
+
var EXIT, NoirCliError, NOIR_ERROR_CODE, NoopSpinner, OraSpinner, EXIT_FLUSH_MS;
|
|
689
709
|
var init_output = __esm({
|
|
690
710
|
"src/output.ts"() {
|
|
691
711
|
"use strict";
|
|
@@ -764,6 +784,7 @@ var init_output = __esm({
|
|
|
764
784
|
this.handle.info(text);
|
|
765
785
|
}
|
|
766
786
|
};
|
|
787
|
+
EXIT_FLUSH_MS = 250;
|
|
767
788
|
}
|
|
768
789
|
});
|
|
769
790
|
|
|
@@ -882,8 +903,8 @@ async function contextIndex(opts) {
|
|
|
882
903
|
}
|
|
883
904
|
renderIndex(data, opts.paths, opts);
|
|
884
905
|
}
|
|
885
|
-
function renderIndex(data,
|
|
886
|
-
const scope =
|
|
906
|
+
function renderIndex(data, paths5, opts) {
|
|
907
|
+
const scope = paths5 && paths5.length > 0 ? paths5.join(", ") : ".";
|
|
887
908
|
const flag = data.degraded ? ` ${badge("warn", "degraded: vectors skipped")}` : "";
|
|
888
909
|
log(`context index \u2014 ${scope}${flag}`, opts);
|
|
889
910
|
definitionList(
|
|
@@ -1604,7 +1625,13 @@ import {
|
|
|
1604
1625
|
readInstallRecord,
|
|
1605
1626
|
readUpdateCache
|
|
1606
1627
|
} from "@noir-ai/core";
|
|
1607
|
-
import {
|
|
1628
|
+
import {
|
|
1629
|
+
CURRENT_SCAFFOLD_VERSION,
|
|
1630
|
+
isStaleSeed,
|
|
1631
|
+
loadTemplate,
|
|
1632
|
+
readScaffoldVersion,
|
|
1633
|
+
render
|
|
1634
|
+
} from "@noir-ai/create";
|
|
1608
1635
|
import { pidAlive as pidAlive4, readProjectDaemonRecord as readProjectDaemonRecord3 } from "@noir-ai/daemon";
|
|
1609
1636
|
import { resolveModelConfig } from "@noir-ai/model";
|
|
1610
1637
|
async function probeOnnx() {
|
|
@@ -1850,9 +1877,10 @@ function checkProvider(checks, project, sources) {
|
|
|
1850
1877
|
for (const name of names) {
|
|
1851
1878
|
const p = resolved.providers[name];
|
|
1852
1879
|
if (!p) continue;
|
|
1853
|
-
const fromFile = p.apiKeyEnv !== void 0 && sources[p.apiKeyEnv] === "file";
|
|
1854
|
-
const
|
|
1855
|
-
|
|
1880
|
+
const fromFile = p.apiKeyEnv !== void 0 && sources[p.apiKeyEnv] === "file" || p.authTokenEnv !== void 0 && sources[p.authTokenEnv] === "file";
|
|
1881
|
+
const credentialName = p.apiKeyEnv ?? p.authTokenEnv;
|
|
1882
|
+
const key = p.hasKey ? fromFile ? "key present (from .noir/.env)" : "key present" : credentialName ? `missing ${credentialName}` : "anonymous";
|
|
1883
|
+
if (!p.hasKey && credentialName) missing = true;
|
|
1856
1884
|
parts.push(`${name}/${p.model ?? "?"} (${key})`);
|
|
1857
1885
|
}
|
|
1858
1886
|
checks.push({
|
|
@@ -1888,6 +1916,23 @@ function checkScaffoldVersion(checks, root) {
|
|
|
1888
1916
|
checks.push({ name: "scaffold version", status: status2, detail });
|
|
1889
1917
|
return { onDisk, current, drift };
|
|
1890
1918
|
}
|
|
1919
|
+
function checkSeedDrift(checks, root) {
|
|
1920
|
+
const seedPath = join2(root, ".noir", ".env.example");
|
|
1921
|
+
let bytes;
|
|
1922
|
+
try {
|
|
1923
|
+
bytes = readFileSync2(seedPath, "utf8");
|
|
1924
|
+
} catch {
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
const currentRender = render(loadTemplate("env.example.tmpl"), {});
|
|
1928
|
+
if (isStaleSeed("envExample", bytes, currentRender)) {
|
|
1929
|
+
checks.push({
|
|
1930
|
+
name: "doc seed",
|
|
1931
|
+
status: "warn",
|
|
1932
|
+
detail: `.noir/.env.example is an unedited copy of an older Noir \u2014 run \`noir init --upgrade\` to refresh it`
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1891
1936
|
function checkRulesMdBudget(checks, root, project) {
|
|
1892
1937
|
if (!project) {
|
|
1893
1938
|
checks.push({ name: "rules budget", status: "warn", detail: "skipped \u2014 not initialized" });
|
|
@@ -2222,6 +2267,7 @@ async function doctor(opts = {}) {
|
|
|
2222
2267
|
checkProvider(checks, project, noirEnv.sources);
|
|
2223
2268
|
checkNoirEnv(checks, root, noirEnv);
|
|
2224
2269
|
const scaffold4 = checkScaffoldVersion(checks, root);
|
|
2270
|
+
checkSeedDrift(checks, root);
|
|
2225
2271
|
const rules = checkRulesMdBudget(checks, root, project);
|
|
2226
2272
|
const host = checkHostArtifacts(checks, root, project);
|
|
2227
2273
|
checkNestedNoir(checks, root);
|
|
@@ -2329,12 +2375,12 @@ function describeSource(row) {
|
|
|
2329
2375
|
return row.shadowed === true ? ".noir/.env (shadows environment)" : ".noir/.env";
|
|
2330
2376
|
}
|
|
2331
2377
|
async function env(opts = {}) {
|
|
2332
|
-
const
|
|
2378
|
+
const report2 = resolveEnv(process.cwd(), ambientBaseline ?? process.env);
|
|
2333
2379
|
if (opts.json === true) {
|
|
2334
|
-
json({ ok: true, data: { vars:
|
|
2380
|
+
json({ ok: true, data: { vars: report2.vars, warnings: report2.warnings } });
|
|
2335
2381
|
return;
|
|
2336
2382
|
}
|
|
2337
|
-
if (
|
|
2383
|
+
if (report2.vars.length === 0) {
|
|
2338
2384
|
info(
|
|
2339
2385
|
"nothing to report \u2014 .noir/.env defines no keys in effect and no Noir-relevant environment variable is set",
|
|
2340
2386
|
opts
|
|
@@ -2342,10 +2388,10 @@ async function env(opts = {}) {
|
|
|
2342
2388
|
return;
|
|
2343
2389
|
}
|
|
2344
2390
|
table(
|
|
2345
|
-
|
|
2391
|
+
report2.vars.map((row) => ({
|
|
2346
2392
|
KEY: row.key,
|
|
2347
2393
|
SOURCE: describeSource(row),
|
|
2348
|
-
VALUE:
|
|
2394
|
+
VALUE: report2.shapes[row.key] ?? ""
|
|
2349
2395
|
})),
|
|
2350
2396
|
["KEY", "SOURCE", "VALUE"],
|
|
2351
2397
|
opts
|
|
@@ -2363,6 +2409,9 @@ var init_env = __esm({
|
|
|
2363
2409
|
"COHERE_API_KEY",
|
|
2364
2410
|
"OLLAMA_BASE_URL",
|
|
2365
2411
|
"ANTHROPIC_API_KEY",
|
|
2412
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
2413
|
+
"ANTHROPIC_BASE_URL",
|
|
2414
|
+
"API_TIMEOUT_MS",
|
|
2366
2415
|
"NOIR_PROFILE"
|
|
2367
2416
|
];
|
|
2368
2417
|
SHORT_VALUE_MAX = 8;
|
|
@@ -3417,7 +3466,7 @@ async function taskVerify(opts) {
|
|
|
3417
3466
|
`${JSON.stringify({
|
|
3418
3467
|
ok: false,
|
|
3419
3468
|
error: { code: EXIT.ERROR, message: `task verify: ${detail}` },
|
|
3420
|
-
//
|
|
3469
|
+
// `{ok:false, error}` + a payload — recovery context + evidence nest
|
|
3421
3470
|
// under `data`, not as top-level siblings (same envelope shape as the
|
|
3422
3471
|
// success path; consumers read `data` for the full picture).
|
|
3423
3472
|
data: {
|
|
@@ -3535,7 +3584,7 @@ var init_task = __esm({
|
|
|
3535
3584
|
init_output();
|
|
3536
3585
|
init_theme();
|
|
3537
3586
|
PHASE_SKILL = {
|
|
3538
|
-
//
|
|
3587
|
+
// Skill curation: intake+clarify merged into brainstorming;
|
|
3539
3588
|
// execute → executing-plans; verify → verifying; document → wrap.
|
|
3540
3589
|
intake: "noir-brainstorming",
|
|
3541
3590
|
clarify: "noir-brainstorming",
|
|
@@ -3804,6 +3853,10 @@ function capitalizeFirst(s) {
|
|
|
3804
3853
|
function hasAction(cmd) {
|
|
3805
3854
|
return cmd._actionHandler != null;
|
|
3806
3855
|
}
|
|
3856
|
+
function requiredArgLabel(cmd) {
|
|
3857
|
+
const required = cmd.registeredArguments.find((arg) => arg.required);
|
|
3858
|
+
return required?.name();
|
|
3859
|
+
}
|
|
3807
3860
|
function collectLeaves(cmd, path, out) {
|
|
3808
3861
|
const children = cmd.commands;
|
|
3809
3862
|
const isLeaf = hasAction(cmd) && children.length === 0;
|
|
@@ -3818,7 +3871,8 @@ function collectLeaves(cmd, path, out) {
|
|
|
3818
3871
|
description,
|
|
3819
3872
|
category: argv[0] ?? "",
|
|
3820
3873
|
keywords: argv,
|
|
3821
|
-
destructive: isDestructive(argv)
|
|
3874
|
+
destructive: isDestructive(argv),
|
|
3875
|
+
needsArg: requiredArgLabel(cmd)
|
|
3822
3876
|
});
|
|
3823
3877
|
return;
|
|
3824
3878
|
}
|
|
@@ -3931,7 +3985,15 @@ var init_sections = __esm({
|
|
|
3931
3985
|
label: "Save memory",
|
|
3932
3986
|
hint: "save an observation \xB7 \u26A0 destructive",
|
|
3933
3987
|
destructive: true,
|
|
3934
|
-
|
|
3988
|
+
// The value travels as `--content <text>` (memory save declares no
|
|
3989
|
+
// positional), so the flag rides in the dispatch prefix and the
|
|
3990
|
+
// collected value is appended after it.
|
|
3991
|
+
dispatch: ["memory", "save", "--content"],
|
|
3992
|
+
needsArg: {
|
|
3993
|
+
label: "content",
|
|
3994
|
+
prompt: "Memory content:",
|
|
3995
|
+
placeholder: "what you want to remember"
|
|
3996
|
+
}
|
|
3935
3997
|
},
|
|
3936
3998
|
{
|
|
3937
3999
|
id: "memory sessions",
|
|
@@ -3955,8 +4017,21 @@ var init_sections = __esm({
|
|
|
3955
4017
|
{
|
|
3956
4018
|
id: "workflow",
|
|
3957
4019
|
label: "Workflow",
|
|
3958
|
-
hint: "next \xB7 status \xB7 advance \xB7 handoff \xB7 wrap",
|
|
4020
|
+
hint: "run \xB7 next \xB7 status \xB7 advance \xB7 handoff \xB7 wrap",
|
|
3959
4021
|
items: [
|
|
4022
|
+
{
|
|
4023
|
+
id: "run",
|
|
4024
|
+
label: "Ask the host",
|
|
4025
|
+
hint: "ask the host agent a question \xB7 \u26A0 destructive",
|
|
4026
|
+
destructive: true,
|
|
4027
|
+
// The prompt is not a required argument (the command also takes flags
|
|
4028
|
+
// like --list-profiles), so the palette only learns to ask for it here.
|
|
4029
|
+
needsArg: {
|
|
4030
|
+
label: "prompt",
|
|
4031
|
+
prompt: "Prompt for the host:",
|
|
4032
|
+
placeholder: "e.g. summarise the auth flow"
|
|
4033
|
+
}
|
|
4034
|
+
},
|
|
3960
4035
|
{
|
|
3961
4036
|
id: "task next",
|
|
3962
4037
|
label: "Next task",
|
|
@@ -4808,6 +4883,15 @@ var init_release = __esm({
|
|
|
4808
4883
|
}
|
|
4809
4884
|
});
|
|
4810
4885
|
|
|
4886
|
+
// src/run-timing.ts
|
|
4887
|
+
var INTERRUPT_GRACE_MS;
|
|
4888
|
+
var init_run_timing = __esm({
|
|
4889
|
+
"src/run-timing.ts"() {
|
|
4890
|
+
"use strict";
|
|
4891
|
+
INTERRUPT_GRACE_MS = 5e3;
|
|
4892
|
+
}
|
|
4893
|
+
});
|
|
4894
|
+
|
|
4811
4895
|
// src/shell-bridge.ts
|
|
4812
4896
|
import { spawn } from "child_process";
|
|
4813
4897
|
import { basename } from "path";
|
|
@@ -4842,7 +4926,7 @@ function shellKind(shell) {
|
|
|
4842
4926
|
function probeCommand(name, shell, opts) {
|
|
4843
4927
|
const kind = shellKind(shell);
|
|
4844
4928
|
const timeoutMs = opts.timeoutMs ?? PROBE_TIMEOUT_MS2;
|
|
4845
|
-
return new Promise((
|
|
4929
|
+
return new Promise((resolve3) => {
|
|
4846
4930
|
const script = kind ? PROBE_SCRIPTS[kind] : "";
|
|
4847
4931
|
const flags = kind ? PROBE_FLAGS[kind] : "-lic";
|
|
4848
4932
|
const probeArgs = kind === "fish" ? [name] : ["noirbridge", name];
|
|
@@ -4865,15 +4949,15 @@ function probeCommand(name, shell, opts) {
|
|
|
4865
4949
|
} catch {
|
|
4866
4950
|
}
|
|
4867
4951
|
}
|
|
4868
|
-
|
|
4952
|
+
resolve3({ stdout, timedOut: true });
|
|
4869
4953
|
}, timeoutMs);
|
|
4870
4954
|
child.on("error", () => {
|
|
4871
4955
|
clearTimeout(timer);
|
|
4872
|
-
|
|
4956
|
+
resolve3({ stdout, timedOut: false });
|
|
4873
4957
|
});
|
|
4874
4958
|
child.on("close", () => {
|
|
4875
4959
|
clearTimeout(timer);
|
|
4876
|
-
|
|
4960
|
+
resolve3({ stdout, timedOut: false });
|
|
4877
4961
|
});
|
|
4878
4962
|
});
|
|
4879
4963
|
}
|
|
@@ -4969,18 +5053,23 @@ function usageFrom(v) {
|
|
|
4969
5053
|
if (input === void 0 && output === void 0 && cache === void 0) return void 0;
|
|
4970
5054
|
return { inputTokens: input, outputTokens: output, cacheReadTokens: cache };
|
|
4971
5055
|
}
|
|
4972
|
-
function
|
|
4973
|
-
if (!message) return
|
|
5056
|
+
function messageContent(message) {
|
|
5057
|
+
if (!message) return {};
|
|
4974
5058
|
const content = message.content;
|
|
4975
|
-
if (typeof content === "string") return content;
|
|
4976
|
-
if (Array.isArray(content)) {
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
if (
|
|
5059
|
+
if (typeof content === "string") return { text: content };
|
|
5060
|
+
if (!Array.isArray(content)) return {};
|
|
5061
|
+
const parts = [];
|
|
5062
|
+
const tools = [];
|
|
5063
|
+
for (const block of content) {
|
|
5064
|
+
const b = asObj(block);
|
|
5065
|
+
if (!b) continue;
|
|
5066
|
+
if (typeof b.text === "string") parts.push(b.text);
|
|
5067
|
+
else if (b.type === "tool_use" && typeof b.name === "string") tools.push(b.name);
|
|
4982
5068
|
}
|
|
4983
|
-
return
|
|
5069
|
+
return {
|
|
5070
|
+
...parts.length > 0 ? { text: parts.join("") } : {},
|
|
5071
|
+
...tools.length > 0 ? { tools } : {}
|
|
5072
|
+
};
|
|
4984
5073
|
}
|
|
4985
5074
|
function parseStreamLine(line) {
|
|
4986
5075
|
const trimmed = line.trim();
|
|
@@ -4999,15 +5088,14 @@ function normalizeStreamEvent(raw) {
|
|
|
4999
5088
|
if (r.subtype === "init") {
|
|
5000
5089
|
return { kind: "init", sessionId: str(r.session_id), model: str(r.model) };
|
|
5001
5090
|
}
|
|
5002
|
-
return { kind: "other" };
|
|
5091
|
+
return { kind: "other", subtype: str(r.subtype) ?? "system" };
|
|
5003
5092
|
}
|
|
5004
5093
|
if (type === "assistant") {
|
|
5005
5094
|
const message = asObj(r.message);
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
usage: message ? usageFrom(message.usage) : void 0,
|
|
5095
|
+
const content = messageContent(message);
|
|
5096
|
+
const messageId = message ? str(message.id) : void 0;
|
|
5097
|
+
const usage = message ? usageFrom(message.usage) : void 0;
|
|
5098
|
+
const error2 = {
|
|
5011
5099
|
// An API error message is a diagnostic, not the answer — flag it so the
|
|
5012
5100
|
// caller never streams it as assistant text (optional-when-true keeps
|
|
5013
5101
|
// exact-shape consumers of the existing union intact).
|
|
@@ -5016,6 +5104,20 @@ function normalizeStreamEvent(raw) {
|
|
|
5016
5104
|
...typeof r.error === "string" ? { errorCategory: r.error } : {}
|
|
5017
5105
|
} : {}
|
|
5018
5106
|
};
|
|
5107
|
+
if (content.text !== void 0) {
|
|
5108
|
+
return {
|
|
5109
|
+
kind: "assistant",
|
|
5110
|
+
messageId,
|
|
5111
|
+
text: content.text,
|
|
5112
|
+
usage,
|
|
5113
|
+
...content.tools === void 0 ? {} : { tools: content.tools },
|
|
5114
|
+
...error2
|
|
5115
|
+
};
|
|
5116
|
+
}
|
|
5117
|
+
if (content.tools !== void 0) {
|
|
5118
|
+
return { kind: "tool", name: content.tools[0], messageId, usage };
|
|
5119
|
+
}
|
|
5120
|
+
return { kind: "assistant", messageId, usage, ...error2 };
|
|
5019
5121
|
}
|
|
5020
5122
|
if (type === "result") {
|
|
5021
5123
|
return {
|
|
@@ -5027,7 +5129,33 @@ function normalizeStreamEvent(raw) {
|
|
|
5027
5129
|
usage: usageFrom(r.usage)
|
|
5028
5130
|
};
|
|
5029
5131
|
}
|
|
5030
|
-
|
|
5132
|
+
if (type === "stream_event") {
|
|
5133
|
+
return normalizeStreamEventInner(asObj(r.event));
|
|
5134
|
+
}
|
|
5135
|
+
return { kind: "other", ...typeof type === "string" ? { subtype: type } : {} };
|
|
5136
|
+
}
|
|
5137
|
+
function normalizeStreamEventInner(inner) {
|
|
5138
|
+
if (!inner) return { kind: "other", subtype: "stream_event" };
|
|
5139
|
+
const innerType = str(inner.type);
|
|
5140
|
+
if (innerType === "content_block_start") {
|
|
5141
|
+
const block = asObj(inner.content_block);
|
|
5142
|
+
const name = block ? str(block.name) : void 0;
|
|
5143
|
+
if (block && str(block.type) === "tool_use" && name !== void 0) {
|
|
5144
|
+
return { kind: "tool", name };
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
if (innerType === "content_block_delta") {
|
|
5148
|
+
const delta = asObj(inner.delta);
|
|
5149
|
+
const text = delta ? str(delta.text) : void 0;
|
|
5150
|
+
if (delta && delta.type === "text_delta" && text !== void 0) {
|
|
5151
|
+
return { kind: "delta", text };
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
return { kind: "other", subtype: innerType ?? "stream_event" };
|
|
5155
|
+
}
|
|
5156
|
+
function signalChild(child, signal) {
|
|
5157
|
+
if (child.pid === void 0) return;
|
|
5158
|
+
child.kill(signal);
|
|
5031
5159
|
}
|
|
5032
5160
|
function runHost(opts) {
|
|
5033
5161
|
const spec = resolveHostRun(opts.host, opts.customBinary);
|
|
@@ -5043,12 +5171,13 @@ function runHost(opts) {
|
|
|
5043
5171
|
false
|
|
5044
5172
|
);
|
|
5045
5173
|
}
|
|
5046
|
-
function spawnAndConsume(binary, args, opts, shellRun) {
|
|
5047
|
-
return new Promise((
|
|
5174
|
+
function spawnAndConsume(binary, args, opts, shellRun, bridged = false) {
|
|
5175
|
+
return new Promise((resolve3, reject) => {
|
|
5048
5176
|
const child = spawn2(binary, [...args], {
|
|
5049
5177
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5050
5178
|
...opts.env ? { env: opts.env } : {}
|
|
5051
5179
|
});
|
|
5180
|
+
opts.onChild?.(child);
|
|
5052
5181
|
const reducer = new UsageReducer();
|
|
5053
5182
|
let eventCount = 0;
|
|
5054
5183
|
let stderrBuf = "";
|
|
@@ -5056,10 +5185,40 @@ function spawnAndConsume(binary, args, opts, shellRun) {
|
|
|
5056
5185
|
let errorText;
|
|
5057
5186
|
let errorCategory;
|
|
5058
5187
|
let spawnError = null;
|
|
5188
|
+
const { signal } = opts;
|
|
5189
|
+
let resend;
|
|
5190
|
+
let offersLeft = Math.ceil(BRIDGE_POLITE_WINDOW_MS / BRIDGE_POLITE_RESEND_MS);
|
|
5191
|
+
const stopResend = () => {
|
|
5192
|
+
if (resend === void 0) return;
|
|
5193
|
+
clearTimeout(resend);
|
|
5194
|
+
resend = void 0;
|
|
5195
|
+
};
|
|
5196
|
+
const offerAgain = () => {
|
|
5197
|
+
if (offersLeft <= 0) return;
|
|
5198
|
+
offersLeft -= 1;
|
|
5199
|
+
resend = setTimeout(() => {
|
|
5200
|
+
resend = void 0;
|
|
5201
|
+
signalChild(child, "SIGTERM");
|
|
5202
|
+
offerAgain();
|
|
5203
|
+
}, BRIDGE_POLITE_RESEND_MS);
|
|
5204
|
+
};
|
|
5205
|
+
const onAbort = () => {
|
|
5206
|
+
signalChild(child, "SIGTERM");
|
|
5207
|
+
if (bridged) offerAgain();
|
|
5208
|
+
};
|
|
5209
|
+
if (signal) {
|
|
5210
|
+
if (signal.aborted) onAbort();
|
|
5211
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
5212
|
+
}
|
|
5213
|
+
const detachAbort = () => {
|
|
5214
|
+
signal?.removeEventListener("abort", onAbort);
|
|
5215
|
+
stopResend();
|
|
5216
|
+
};
|
|
5059
5217
|
child.on("error", (err) => {
|
|
5060
5218
|
spawnError = err;
|
|
5219
|
+
detachAbort();
|
|
5061
5220
|
if (!shellRun && err.code === "ENOENT") {
|
|
5062
|
-
void shellFallback(binary, args, opts,
|
|
5221
|
+
void shellFallback(binary, args, opts, resolve3, reject, err);
|
|
5063
5222
|
return;
|
|
5064
5223
|
}
|
|
5065
5224
|
reject(err);
|
|
@@ -5086,41 +5245,43 @@ function spawnAndConsume(binary, args, opts, shellRun) {
|
|
|
5086
5245
|
child.stderr.on("data", (chunk) => {
|
|
5087
5246
|
stderrBuf += chunk;
|
|
5088
5247
|
});
|
|
5089
|
-
child.on("close", (code,
|
|
5248
|
+
child.on("close", (code, signal2) => {
|
|
5249
|
+
detachAbort();
|
|
5090
5250
|
if (spawnError !== null) return;
|
|
5091
|
-
const killed = code === null &&
|
|
5092
|
-
|
|
5093
|
-
exitCode: killed ? 128 + (signalNumber(
|
|
5251
|
+
const killed = code === null && signal2 != null;
|
|
5252
|
+
resolve3({
|
|
5253
|
+
exitCode: killed ? 128 + (signalNumber(signal2) ?? 0) : code ?? 0,
|
|
5094
5254
|
usage: reducer.snapshot(),
|
|
5095
5255
|
eventCount,
|
|
5096
5256
|
stderr: stderrBuf,
|
|
5097
5257
|
isError: errored || killed,
|
|
5098
|
-
errorText: killed && errorText === void 0 ? `terminated by signal ${
|
|
5258
|
+
errorText: killed && errorText === void 0 ? `terminated by signal ${signal2}` : errorText,
|
|
5099
5259
|
errorCategory
|
|
5100
5260
|
});
|
|
5101
5261
|
});
|
|
5102
5262
|
});
|
|
5103
5263
|
}
|
|
5104
|
-
async function shellFallback(binary, args, opts,
|
|
5264
|
+
async function shellFallback(binary, args, opts, resolve3, reject, originalErr) {
|
|
5105
5265
|
try {
|
|
5106
5266
|
const res = await resolveCommandViaShell(binary, { env: opts.env ?? process.env });
|
|
5107
5267
|
if (res.kind === "path") {
|
|
5108
|
-
|
|
5268
|
+
resolve3(await spawnAndConsume(res.path, args, opts, true));
|
|
5109
5269
|
return;
|
|
5110
5270
|
}
|
|
5111
5271
|
if (res.kind === "alias" || res.kind === "function") {
|
|
5112
5272
|
const bridge = buildBridgeArgs(binary, args, res.shell);
|
|
5113
|
-
|
|
5273
|
+
resolve3(await spawnAndConsume(bridge.binary, bridge.args, opts, true, true));
|
|
5114
5274
|
return;
|
|
5115
5275
|
}
|
|
5116
5276
|
} catch {
|
|
5117
5277
|
}
|
|
5118
5278
|
reject(originalErr);
|
|
5119
5279
|
}
|
|
5120
|
-
var HOST_BINARY, HOST_FLAGS, UsageReducer;
|
|
5280
|
+
var HOST_BINARY, HOST_FLAGS, UsageReducer, BRIDGE_POLITE_RESEND_MS, BRIDGE_POLITE_WINDOW_MS;
|
|
5121
5281
|
var init_orchestrator = __esm({
|
|
5122
5282
|
"src/orchestrator.ts"() {
|
|
5123
5283
|
"use strict";
|
|
5284
|
+
init_run_timing();
|
|
5124
5285
|
init_shell_bridge();
|
|
5125
5286
|
HOST_BINARY = {
|
|
5126
5287
|
claude: "claude",
|
|
@@ -5131,7 +5292,7 @@ var init_orchestrator = __esm({
|
|
|
5131
5292
|
// emits AGENTS.md — a file host, not a spawnable CLI
|
|
5132
5293
|
};
|
|
5133
5294
|
HOST_FLAGS = {
|
|
5134
|
-
claude: ["-p", "--output-format", "stream-json", "--verbose"],
|
|
5295
|
+
claude: ["-p", "--output-format", "stream-json", "--verbose", "--include-partial-messages"],
|
|
5135
5296
|
gemini: ["-p", "--output-format", "stream-json"],
|
|
5136
5297
|
opencode: ["run"],
|
|
5137
5298
|
cursor: ["-p"],
|
|
@@ -5145,27 +5306,29 @@ var init_orchestrator = __esm({
|
|
|
5145
5306
|
turns = 0;
|
|
5146
5307
|
/** Feed one normalized event into the reducer. */
|
|
5147
5308
|
add(event) {
|
|
5148
|
-
if (event.kind === "
|
|
5149
|
-
const key = event.messageId;
|
|
5150
|
-
const input = event.usage.inputTokens ?? 0;
|
|
5151
|
-
const output = event.usage.outputTokens ?? 0;
|
|
5152
|
-
const prev = this.maxByMessage.get(key);
|
|
5153
|
-
if (!prev) {
|
|
5154
|
-
this.maxByMessage.set(key, { input, output });
|
|
5155
|
-
this.input += input;
|
|
5156
|
-
this.output += output;
|
|
5157
|
-
} else {
|
|
5158
|
-
this.input += Math.max(0, input - prev.input);
|
|
5159
|
-
this.output += Math.max(0, output - prev.output);
|
|
5160
|
-
this.maxByMessage.set(key, {
|
|
5161
|
-
input: Math.max(prev.input, input),
|
|
5162
|
-
output: Math.max(prev.output, output)
|
|
5163
|
-
});
|
|
5164
|
-
}
|
|
5165
|
-
} else if (event.kind === "result") {
|
|
5309
|
+
if (event.kind === "result") {
|
|
5166
5310
|
if (event.totalCostUsd !== void 0) this.costUsd = event.totalCostUsd;
|
|
5167
5311
|
if (event.numTurns !== void 0) this.turns = event.numTurns;
|
|
5312
|
+
return;
|
|
5168
5313
|
}
|
|
5314
|
+
const usage = event.kind === "assistant" || event.kind === "tool" ? event.usage : void 0;
|
|
5315
|
+
const key = event.kind === "assistant" || event.kind === "tool" ? event.messageId : void 0;
|
|
5316
|
+
if (!key || !usage) return;
|
|
5317
|
+
const input = usage.inputTokens ?? 0;
|
|
5318
|
+
const output = usage.outputTokens ?? 0;
|
|
5319
|
+
const prev = this.maxByMessage.get(key);
|
|
5320
|
+
if (!prev) {
|
|
5321
|
+
this.maxByMessage.set(key, { input, output });
|
|
5322
|
+
this.input += input;
|
|
5323
|
+
this.output += output;
|
|
5324
|
+
return;
|
|
5325
|
+
}
|
|
5326
|
+
this.input += Math.max(0, input - prev.input);
|
|
5327
|
+
this.output += Math.max(0, output - prev.output);
|
|
5328
|
+
this.maxByMessage.set(key, {
|
|
5329
|
+
input: Math.max(prev.input, input),
|
|
5330
|
+
output: Math.max(prev.output, output)
|
|
5331
|
+
});
|
|
5169
5332
|
}
|
|
5170
5333
|
/** The current accumulator snapshot. */
|
|
5171
5334
|
snapshot() {
|
|
@@ -5177,11 +5340,362 @@ var init_orchestrator = __esm({
|
|
|
5177
5340
|
};
|
|
5178
5341
|
}
|
|
5179
5342
|
};
|
|
5343
|
+
BRIDGE_POLITE_RESEND_MS = 250;
|
|
5344
|
+
BRIDGE_POLITE_WINDOW_MS = INTERRUPT_GRACE_MS;
|
|
5345
|
+
}
|
|
5346
|
+
});
|
|
5347
|
+
|
|
5348
|
+
// src/run-actions.ts
|
|
5349
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
5350
|
+
import { resolve } from "path";
|
|
5351
|
+
import { CommanderError as CommanderError2 } from "commander";
|
|
5352
|
+
function postRunActions(input) {
|
|
5353
|
+
const answer = input.answer.trim();
|
|
5354
|
+
const canContinue = input.canContinue ?? (input.sessionId !== void 0 && input.resume !== void 0);
|
|
5355
|
+
const options = [];
|
|
5356
|
+
if (answer.length > 0) {
|
|
5357
|
+
options.push({
|
|
5358
|
+
value: "memory",
|
|
5359
|
+
label: "Save answer to memory",
|
|
5360
|
+
hint: "distil it into this project's memory (needs the daemon)"
|
|
5361
|
+
});
|
|
5362
|
+
options.push({
|
|
5363
|
+
value: "research",
|
|
5364
|
+
label: "Add as task research",
|
|
5365
|
+
hint: "record it as a finding on the active task"
|
|
5366
|
+
});
|
|
5367
|
+
}
|
|
5368
|
+
options.push({
|
|
5369
|
+
value: "handoff",
|
|
5370
|
+
label: "Write a handoff artifact",
|
|
5371
|
+
hint: "snapshot the task, the next step and a seed for your host"
|
|
5372
|
+
});
|
|
5373
|
+
if (canContinue) {
|
|
5374
|
+
options.push({
|
|
5375
|
+
value: "resume",
|
|
5376
|
+
label: "Continue this session",
|
|
5377
|
+
hint: `ask ${input.host} a follow-up in the same session`,
|
|
5378
|
+
needsValue: {
|
|
5379
|
+
label: "follow-up prompt",
|
|
5380
|
+
placeholder: "e.g. now write the tests for that"
|
|
5381
|
+
}
|
|
5382
|
+
});
|
|
5383
|
+
}
|
|
5384
|
+
if (answer.length > 0) {
|
|
5385
|
+
options.push({
|
|
5386
|
+
value: "save",
|
|
5387
|
+
label: "Save answer to a file",
|
|
5388
|
+
hint: "write the answer text to a path you choose",
|
|
5389
|
+
needsValue: { label: "file path", placeholder: "e.g. notes/answer.md" }
|
|
5390
|
+
});
|
|
5391
|
+
}
|
|
5392
|
+
options.push({
|
|
5393
|
+
value: "dismiss",
|
|
5394
|
+
label: "Dismiss",
|
|
5395
|
+
hint: "nothing further \u2014 the run is already complete"
|
|
5396
|
+
});
|
|
5397
|
+
return options;
|
|
5398
|
+
}
|
|
5399
|
+
async function performPostRunAction(choice, input, value) {
|
|
5400
|
+
const label = ACTION_LABEL[choice];
|
|
5401
|
+
try {
|
|
5402
|
+
switch (choice) {
|
|
5403
|
+
case "memory":
|
|
5404
|
+
await saveToMemory(input);
|
|
5405
|
+
return { ok: true, message: "" };
|
|
5406
|
+
case "research":
|
|
5407
|
+
await recordAsResearch(input);
|
|
5408
|
+
return { ok: true, message: "" };
|
|
5409
|
+
case "handoff":
|
|
5410
|
+
await writeHandoff(input);
|
|
5411
|
+
return { ok: true, message: "" };
|
|
5412
|
+
case "resume":
|
|
5413
|
+
return await continueSession(input, value);
|
|
5414
|
+
case "save":
|
|
5415
|
+
return saveAnswerToFile(input, value);
|
|
5416
|
+
case "dismiss":
|
|
5417
|
+
return { ok: true, message: "" };
|
|
5418
|
+
}
|
|
5419
|
+
} catch (err) {
|
|
5420
|
+
const quoted = err instanceof CommanderError2 || err instanceof NoirCliError ? void 0 : `${label}: ${err instanceof Error ? err.message : String(err)}`;
|
|
5421
|
+
return {
|
|
5422
|
+
ok: false,
|
|
5423
|
+
message: `${label} did not complete \u2014 the run's result stands.`,
|
|
5424
|
+
...quoted === void 0 ? {} : { detail: quoted }
|
|
5425
|
+
};
|
|
5426
|
+
}
|
|
5427
|
+
}
|
|
5428
|
+
async function offerPostRunActions(input) {
|
|
5429
|
+
if (!isInteractive(input.opts)) return;
|
|
5430
|
+
const options = postRunActions(input);
|
|
5431
|
+
if (options.length <= 1) return;
|
|
5432
|
+
const menu = await openMenu(options);
|
|
5433
|
+
if (menu === void 0) return;
|
|
5434
|
+
const chosen = options.find((o) => o.value === menu.choice);
|
|
5435
|
+
if (chosen === void 0) return;
|
|
5436
|
+
const value = chosen.needsValue === void 0 ? void 0 : await askForValue(menu.clack, chosen, input);
|
|
5437
|
+
if (chosen.needsValue !== void 0 && value === void 0) return;
|
|
5438
|
+
report(await performPostRunAction(menu.choice, input, value), input.opts);
|
|
5439
|
+
}
|
|
5440
|
+
function report(outcome, opts) {
|
|
5441
|
+
const caution = outcome.tone === "caution" || !outcome.ok && outcome.tone !== "confirm";
|
|
5442
|
+
if (outcome.detail !== void 0) warn(outcome.detail, opts);
|
|
5443
|
+
if (outcome.message.length === 0) return;
|
|
5444
|
+
if (caution) warn(outcome.message, opts);
|
|
5445
|
+
else success(outcome.message, opts);
|
|
5446
|
+
}
|
|
5447
|
+
async function openMenu(options) {
|
|
5448
|
+
try {
|
|
5449
|
+
const clack = await import("@clack/prompts");
|
|
5450
|
+
const choice = await clack.select({
|
|
5451
|
+
message: "The run finished. What next?",
|
|
5452
|
+
initialValue: "dismiss",
|
|
5453
|
+
// Spread because the prompt library takes a mutable list; the rows
|
|
5454
|
+
// themselves are the shared descriptors and are never rewritten.
|
|
5455
|
+
options: [...options]
|
|
5456
|
+
});
|
|
5457
|
+
if (clack.isCancel(choice)) return void 0;
|
|
5458
|
+
return { clack, choice };
|
|
5459
|
+
} catch {
|
|
5460
|
+
return void 0;
|
|
5461
|
+
}
|
|
5462
|
+
}
|
|
5463
|
+
async function askForValue(clack, option, input) {
|
|
5464
|
+
const needs = option.needsValue;
|
|
5465
|
+
if (needs === void 0) return void 0;
|
|
5466
|
+
const message = option.value === "save" ? `Write the answer to a file (the raw transcript is at ${input.transcript})` : `What should ${input.host} do next?`;
|
|
5467
|
+
try {
|
|
5468
|
+
const written = await clack.text({ message, placeholder: needs.placeholder });
|
|
5469
|
+
if (clack.isCancel(written)) return void 0;
|
|
5470
|
+
return String(written ?? "");
|
|
5471
|
+
} catch {
|
|
5472
|
+
return void 0;
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5475
|
+
async function saveToMemory(input) {
|
|
5476
|
+
await memoryCapture({
|
|
5477
|
+
...input.opts,
|
|
5478
|
+
content: input.answer,
|
|
5479
|
+
eventType: CAPTURE_EVENT
|
|
5480
|
+
});
|
|
5481
|
+
}
|
|
5482
|
+
async function recordAsResearch(input) {
|
|
5483
|
+
await taskResearchRecord({
|
|
5484
|
+
...input.opts,
|
|
5485
|
+
type: "discovery",
|
|
5486
|
+
text: researchText(input.answer),
|
|
5487
|
+
source: RESEARCH_SOURCE
|
|
5488
|
+
});
|
|
5489
|
+
}
|
|
5490
|
+
function researchText(answer) {
|
|
5491
|
+
const condensed = answer.replace(/\s+/g, " ").trim();
|
|
5492
|
+
if (condensed.length <= RESEARCH_TEXT_LIMIT) return condensed;
|
|
5493
|
+
return `${condensed.slice(0, RESEARCH_TEXT_LIMIT - 1)}\u2026`;
|
|
5494
|
+
}
|
|
5495
|
+
async function writeHandoff(input) {
|
|
5496
|
+
await handoff({ ...input.opts, write: true });
|
|
5497
|
+
}
|
|
5498
|
+
async function continueSession(input, value) {
|
|
5499
|
+
const { sessionId, resume } = input;
|
|
5500
|
+
if (sessionId === void 0 || resume === void 0) return { ok: true, message: "" };
|
|
5501
|
+
const prompt = (value ?? "").trim();
|
|
5502
|
+
if (prompt.length === 0) {
|
|
5503
|
+
return {
|
|
5504
|
+
ok: true,
|
|
5505
|
+
tone: "caution",
|
|
5506
|
+
message: "No prompt given \u2014 the session was not continued."
|
|
5507
|
+
};
|
|
5508
|
+
}
|
|
5509
|
+
await resume(prompt, sessionId);
|
|
5510
|
+
return { ok: true, message: "" };
|
|
5511
|
+
}
|
|
5512
|
+
function saveAnswerToFile(input, value) {
|
|
5513
|
+
const fallback = `noir-run-${stamp2()}.md`;
|
|
5514
|
+
const chosen = (value ?? "").trim();
|
|
5515
|
+
const target = resolve(chosen.length > 0 ? chosen : fallback);
|
|
5516
|
+
const body = input.answer.endsWith("\n") ? input.answer : `${input.answer}
|
|
5517
|
+
`;
|
|
5518
|
+
try {
|
|
5519
|
+
writeFileSync3(target, body, { mode: 384 });
|
|
5520
|
+
} catch (err) {
|
|
5521
|
+
return {
|
|
5522
|
+
ok: false,
|
|
5523
|
+
tone: "caution",
|
|
5524
|
+
message: `could not write ${target}: ${err instanceof Error ? err.message : String(err)}`
|
|
5525
|
+
};
|
|
5526
|
+
}
|
|
5527
|
+
return { ok: true, message: `Answer written to ${target}` };
|
|
5528
|
+
}
|
|
5529
|
+
function stamp2() {
|
|
5530
|
+
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5531
|
+
}
|
|
5532
|
+
var CAPTURE_EVENT, RESEARCH_SOURCE, RESEARCH_TEXT_LIMIT, ACTION_LABEL;
|
|
5533
|
+
var init_run_actions = __esm({
|
|
5534
|
+
"src/run-actions.ts"() {
|
|
5535
|
+
"use strict";
|
|
5536
|
+
init_handoff();
|
|
5537
|
+
init_memory();
|
|
5538
|
+
init_task();
|
|
5539
|
+
init_output();
|
|
5540
|
+
CAPTURE_EVENT = "capture";
|
|
5541
|
+
RESEARCH_SOURCE = "noir run";
|
|
5542
|
+
RESEARCH_TEXT_LIMIT = 220;
|
|
5543
|
+
ACTION_LABEL = {
|
|
5544
|
+
memory: "Saving to memory",
|
|
5545
|
+
research: "Recording the finding",
|
|
5546
|
+
handoff: "Writing the handoff",
|
|
5547
|
+
resume: "Continuing the session",
|
|
5548
|
+
save: "Writing the file",
|
|
5549
|
+
dismiss: "Dismissing"
|
|
5550
|
+
};
|
|
5551
|
+
}
|
|
5552
|
+
});
|
|
5553
|
+
|
|
5554
|
+
// src/run-interrupt.ts
|
|
5555
|
+
import { constants as constants2 } from "os";
|
|
5556
|
+
function exitCodeForSignal(signal) {
|
|
5557
|
+
return 128 + (constants2.signals[signal] ?? 0);
|
|
5558
|
+
}
|
|
5559
|
+
function interruptedNotice(transcript) {
|
|
5560
|
+
return `interrupted \xB7 transcript: ${transcript}`;
|
|
5561
|
+
}
|
|
5562
|
+
var INTERRUPT_SIGNALS, RunInterrupt;
|
|
5563
|
+
var init_run_interrupt = __esm({
|
|
5564
|
+
"src/run-interrupt.ts"() {
|
|
5565
|
+
"use strict";
|
|
5566
|
+
init_orchestrator();
|
|
5567
|
+
init_run_timing();
|
|
5568
|
+
INTERRUPT_SIGNALS = ["SIGINT", "SIGTERM"];
|
|
5569
|
+
RunInterrupt = class {
|
|
5570
|
+
graceMs;
|
|
5571
|
+
exitNow;
|
|
5572
|
+
controller = new AbortController();
|
|
5573
|
+
/** The live child, or undefined between spawns. */
|
|
5574
|
+
child;
|
|
5575
|
+
/** The child the ladder already ran for — a repeat call is not a new ladder. */
|
|
5576
|
+
stopping;
|
|
5577
|
+
grace;
|
|
5578
|
+
signalName;
|
|
5579
|
+
stopRequested = false;
|
|
5580
|
+
forced = false;
|
|
5581
|
+
watching = false;
|
|
5582
|
+
/** The installed listeners, keyed by the signal each answers. */
|
|
5583
|
+
handlers = /* @__PURE__ */ new Map();
|
|
5584
|
+
constructor(opts = {}) {
|
|
5585
|
+
this.graceMs = opts.graceMs ?? INTERRUPT_GRACE_MS;
|
|
5586
|
+
this.exitNow = opts.exitNow ?? ((code) => {
|
|
5587
|
+
if (this.child !== void 0) signalChild(this.child, "SIGKILL");
|
|
5588
|
+
process.exit(code);
|
|
5589
|
+
});
|
|
5590
|
+
}
|
|
5591
|
+
/**
|
|
5592
|
+
* The cancel signal for the spawn. Aborting it is what a caller holding only
|
|
5593
|
+
* the signal can do; the ladder itself does not depend on it, which matters
|
|
5594
|
+
* because the child a run ends up driving is not always the first one spawned.
|
|
5595
|
+
*/
|
|
5596
|
+
get signal() {
|
|
5597
|
+
return this.controller.signal;
|
|
5598
|
+
}
|
|
5599
|
+
/** The signal that stopped the run, or undefined when nothing did. */
|
|
5600
|
+
get interruptedBy() {
|
|
5601
|
+
return this.signalName;
|
|
5602
|
+
}
|
|
5603
|
+
/** True once a child had to be forced — it did not honour the polite signal. */
|
|
5604
|
+
get escalated() {
|
|
5605
|
+
return this.forced;
|
|
5606
|
+
}
|
|
5607
|
+
/**
|
|
5608
|
+
* Hand over the child a spawn produced. Called for every spawn of one run: a
|
|
5609
|
+
* command resolved through the shell bridge is its own child, and a stop that
|
|
5610
|
+
* arrived before that child existed has to land on it too.
|
|
5611
|
+
*/
|
|
5612
|
+
track(child) {
|
|
5613
|
+
this.child = child;
|
|
5614
|
+
child.once("exit", () => this.forget(child));
|
|
5615
|
+
if (this.stopRequested) this.terminate();
|
|
5616
|
+
}
|
|
5617
|
+
/**
|
|
5618
|
+
* Stop the host. The polite signal goes to the child itself, and a child still
|
|
5619
|
+
* alive after the grace is forced. Repeat calls are the same call: the ladder
|
|
5620
|
+
* runs once per child, so a keystroke held down is not a stream of signals.
|
|
5621
|
+
*/
|
|
5622
|
+
terminate() {
|
|
5623
|
+
this.stopRequested = true;
|
|
5624
|
+
if (!this.controller.signal.aborted) this.controller.abort();
|
|
5625
|
+
const child = this.child;
|
|
5626
|
+
if (child === void 0 || child.pid === void 0 || child === this.stopping) return;
|
|
5627
|
+
this.stopping = child;
|
|
5628
|
+
this.clearGrace();
|
|
5629
|
+
signalChild(child, "SIGTERM");
|
|
5630
|
+
this.grace = setTimeout(() => {
|
|
5631
|
+
this.grace = void 0;
|
|
5632
|
+
if (this.child !== child) return;
|
|
5633
|
+
this.forced = true;
|
|
5634
|
+
signalChild(child, "SIGKILL");
|
|
5635
|
+
}, this.graceMs);
|
|
5636
|
+
}
|
|
5637
|
+
/**
|
|
5638
|
+
* Skip the grace and force the host now. The "you asked twice" path: the
|
|
5639
|
+
* terminal leaves at once on a second signal, and the run screen forces and
|
|
5640
|
+
* returns to the dashboard rather than holding a keystroke while the grace
|
|
5641
|
+
* runs out. Does nothing when no stop is in flight.
|
|
5642
|
+
*/
|
|
5643
|
+
forceNow() {
|
|
5644
|
+
const child = this.child;
|
|
5645
|
+
if (child === void 0 || child.pid === void 0 || child !== this.stopping) return false;
|
|
5646
|
+
this.forced = true;
|
|
5647
|
+
this.clearGrace();
|
|
5648
|
+
signalChild(child, "SIGKILL");
|
|
5649
|
+
return true;
|
|
5650
|
+
}
|
|
5651
|
+
/**
|
|
5652
|
+
* Install the terminal interrupt handlers. Only a run installs these, and only
|
|
5653
|
+
* for as long as it is driving the host: the process's own handling has to
|
|
5654
|
+
* come back afterwards, or a later command inherits an interrupt contract with
|
|
5655
|
+
* nothing left to interrupt.
|
|
5656
|
+
*/
|
|
5657
|
+
watch() {
|
|
5658
|
+
if (this.watching) return;
|
|
5659
|
+
this.watching = true;
|
|
5660
|
+
for (const signal of INTERRUPT_SIGNALS) {
|
|
5661
|
+
const listener = () => this.onSignal(signal);
|
|
5662
|
+
this.handlers.set(signal, listener);
|
|
5663
|
+
process.on(signal, listener);
|
|
5664
|
+
}
|
|
5665
|
+
}
|
|
5666
|
+
/** Put the process's own handling back, whatever the run left behind. */
|
|
5667
|
+
unwatch() {
|
|
5668
|
+
if (!this.watching) return;
|
|
5669
|
+
this.watching = false;
|
|
5670
|
+
for (const [signal, listener] of this.handlers) process.off(signal, listener);
|
|
5671
|
+
this.handlers.clear();
|
|
5672
|
+
}
|
|
5673
|
+
onSignal(signal) {
|
|
5674
|
+
if (this.signalName !== void 0) {
|
|
5675
|
+
this.exitNow(exitCodeForSignal(signal));
|
|
5676
|
+
return;
|
|
5677
|
+
}
|
|
5678
|
+
this.signalName = signal;
|
|
5679
|
+
this.terminate();
|
|
5680
|
+
}
|
|
5681
|
+
/** The child is gone: stop tracking it and disarm its ladder. */
|
|
5682
|
+
forget(child) {
|
|
5683
|
+
if (this.child !== child) return;
|
|
5684
|
+
this.child = void 0;
|
|
5685
|
+
this.stopping = void 0;
|
|
5686
|
+
this.clearGrace();
|
|
5687
|
+
}
|
|
5688
|
+
clearGrace() {
|
|
5689
|
+
if (this.grace === void 0) return;
|
|
5690
|
+
clearTimeout(this.grace);
|
|
5691
|
+
this.grace = void 0;
|
|
5692
|
+
}
|
|
5693
|
+
};
|
|
5180
5694
|
}
|
|
5181
5695
|
});
|
|
5182
5696
|
|
|
5183
5697
|
// src/run-profiles.ts
|
|
5184
|
-
import { loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
|
|
5698
|
+
import { isDeniedEnvKey, loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
|
|
5185
5699
|
function loadRunConfig(root) {
|
|
5186
5700
|
try {
|
|
5187
5701
|
return loadProjectInfo13(root).config;
|
|
@@ -5216,6 +5730,14 @@ function resolveRunProfile(requested, config, env2) {
|
|
|
5216
5730
|
envExpanded[k] = v === null ? void 0 : expandEnvVars(v, env2);
|
|
5217
5731
|
}
|
|
5218
5732
|
}
|
|
5733
|
+
for (const key of Object.keys(envExpanded)) {
|
|
5734
|
+
if (isDeniedEnvKey(key)) {
|
|
5735
|
+
return {
|
|
5736
|
+
ok: false,
|
|
5737
|
+
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.`
|
|
5738
|
+
};
|
|
5739
|
+
}
|
|
5740
|
+
}
|
|
5219
5741
|
return {
|
|
5220
5742
|
ok: true,
|
|
5221
5743
|
profile: { binary: profile.binary, env: envExpanded, args: profile.args, profileName: name }
|
|
@@ -5236,11 +5758,195 @@ var init_run_profiles = __esm({
|
|
|
5236
5758
|
}
|
|
5237
5759
|
});
|
|
5238
5760
|
|
|
5761
|
+
// src/run-status.ts
|
|
5762
|
+
function humanizeElapsed(ms) {
|
|
5763
|
+
const total = Math.max(0, Math.floor(ms / 1e3));
|
|
5764
|
+
const seconds = total % 60;
|
|
5765
|
+
const minutes = Math.floor(total / 60) % 60;
|
|
5766
|
+
const hours = Math.floor(total / 3600);
|
|
5767
|
+
if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
5768
|
+
if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
5769
|
+
return `${seconds}s`;
|
|
5770
|
+
}
|
|
5771
|
+
function hostStderrTail(stderr, maxLines = HOST_STDERR_TAIL_LINES) {
|
|
5772
|
+
if (maxLines <= 0) return "";
|
|
5773
|
+
const lines = stderr.split("\n").map((line) => line.trimEnd());
|
|
5774
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
5775
|
+
if (lines.length === 0) return "";
|
|
5776
|
+
if (lines.length <= maxLines) return lines.join("\n");
|
|
5777
|
+
const omitted = lines.length - maxLines;
|
|
5778
|
+
return [`\u2026 (${omitted} earlier lines omitted)`, ...lines.slice(-maxLines)].join("\n");
|
|
5779
|
+
}
|
|
5780
|
+
var HOST_STDERR_TAIL_LINES, RunStatusLine;
|
|
5781
|
+
var init_run_status = __esm({
|
|
5782
|
+
"src/run-status.ts"() {
|
|
5783
|
+
"use strict";
|
|
5784
|
+
init_orchestrator();
|
|
5785
|
+
HOST_STDERR_TAIL_LINES = 20;
|
|
5786
|
+
RunStatusLine = class {
|
|
5787
|
+
host;
|
|
5788
|
+
enabled;
|
|
5789
|
+
animated;
|
|
5790
|
+
sharesCursor;
|
|
5791
|
+
write;
|
|
5792
|
+
now;
|
|
5793
|
+
/**
|
|
5794
|
+
* Running token totals for the live line. This is a SECOND accumulator: the
|
|
5795
|
+
* snapshot the run reports at the end is reduced independently from the full
|
|
5796
|
+
* event stream, and both apply the same `max usage per message id` rule, so
|
|
5797
|
+
* the number on screen and the number in the summary agree.
|
|
5798
|
+
*/
|
|
5799
|
+
usage = new UsageReducer();
|
|
5800
|
+
startedAt;
|
|
5801
|
+
model;
|
|
5802
|
+
streaming = false;
|
|
5803
|
+
begun = false;
|
|
5804
|
+
ended = false;
|
|
5805
|
+
/** True while the status text is sitting on the row the cursor is currently at. */
|
|
5806
|
+
onRow = false;
|
|
5807
|
+
/** True when the last stdout write ended a line — the only safe moment to move the cursor. */
|
|
5808
|
+
stdoutAtRowStart = true;
|
|
5809
|
+
/** The text currently on the row (empty when the row is not ours). */
|
|
5810
|
+
rendered = "";
|
|
5811
|
+
constructor(opts) {
|
|
5812
|
+
this.host = opts.host;
|
|
5813
|
+
this.enabled = opts.json !== true && opts.quiet !== true;
|
|
5814
|
+
this.animated = this.enabled && opts.stderrIsTty === true;
|
|
5815
|
+
this.sharesCursor = opts.stdoutSharesCursor !== false;
|
|
5816
|
+
this.write = opts.write ?? ((chunk) => void process.stderr.write(chunk));
|
|
5817
|
+
this.now = opts.now ?? (() => performance.now());
|
|
5818
|
+
this.startedAt = this.now();
|
|
5819
|
+
}
|
|
5820
|
+
/**
|
|
5821
|
+
* Claim the row: show the seeded "waiting" state immediately, before the host
|
|
5822
|
+
* has said anything, so the spawn itself is never blank. Calling this twice
|
|
5823
|
+
* is a no-op.
|
|
5824
|
+
*/
|
|
5825
|
+
begin() {
|
|
5826
|
+
if (!this.enabled || this.begun) return;
|
|
5827
|
+
this.begun = true;
|
|
5828
|
+
this.startedAt = this.now();
|
|
5829
|
+
if (this.animated) {
|
|
5830
|
+
this.draw();
|
|
5831
|
+
return;
|
|
5832
|
+
}
|
|
5833
|
+
this.write(`${this.render()}
|
|
5834
|
+
`);
|
|
5835
|
+
}
|
|
5836
|
+
/** Feed one normalized host event. Anything unrenderable is ignored. */
|
|
5837
|
+
event(e) {
|
|
5838
|
+
if (!this.enabled || this.ended || !this.begun) return;
|
|
5839
|
+
if (e.kind === "result") {
|
|
5840
|
+
this.end();
|
|
5841
|
+
return;
|
|
5842
|
+
}
|
|
5843
|
+
if (e.kind === "init") {
|
|
5844
|
+
if (e.model !== void 0) this.model = e.model;
|
|
5845
|
+
} else if (e.kind === "assistant" || e.kind === "tool") {
|
|
5846
|
+
this.streaming = true;
|
|
5847
|
+
this.usage.add(e);
|
|
5848
|
+
} else if (e.kind === "delta") {
|
|
5849
|
+
this.streaming = true;
|
|
5850
|
+
} else {
|
|
5851
|
+
return;
|
|
5852
|
+
}
|
|
5853
|
+
this.draw();
|
|
5854
|
+
}
|
|
5855
|
+
/**
|
|
5856
|
+
* Announce the chunk the caller is about to write to STDOUT — before writing
|
|
5857
|
+
* it. Warning the line first lets it finish its row: the answer would
|
|
5858
|
+
* otherwise start glued to the tail of the status text. It also records
|
|
5859
|
+
* whether that write ended a line, which is the only moment the cursor can be
|
|
5860
|
+
* moved without landing inside the answer.
|
|
5861
|
+
*/
|
|
5862
|
+
beforeStdout(chunk) {
|
|
5863
|
+
if (chunk.length === 0) return;
|
|
5864
|
+
if (this.animated && this.sharesCursor && this.onRow) {
|
|
5865
|
+
this.write("\n");
|
|
5866
|
+
this.onRow = false;
|
|
5867
|
+
}
|
|
5868
|
+
this.stdoutAtRowStart = chunk.endsWith("\n");
|
|
5869
|
+
}
|
|
5870
|
+
/**
|
|
5871
|
+
* Finish the row the host's answer left open. A failure message printed onto
|
|
5872
|
+
* a half-written answer reads as part of the answer, so a caller about to
|
|
5873
|
+
* report a failure asks for the row to be terminated first. A no-op unless
|
|
5874
|
+
* this line is drawing on a terminal stdout also writes to and the answer
|
|
5875
|
+
* stopped mid-line — the only state where the cursor is inside the answer.
|
|
5876
|
+
*/
|
|
5877
|
+
finishRow() {
|
|
5878
|
+
if (!this.animated || !this.sharesCursor || this.stdoutAtRowStart) return;
|
|
5879
|
+
this.write("\n");
|
|
5880
|
+
this.stdoutAtRowStart = true;
|
|
5881
|
+
}
|
|
5882
|
+
/**
|
|
5883
|
+
* Close the line out. On a terminal the line is erased so the run summary
|
|
5884
|
+
* takes its place — unless the answer has since moved the cursor off it, in
|
|
5885
|
+
* which case the text stays as the record of how long the wait was. On a pipe
|
|
5886
|
+
* it becomes the final marker. Safe to call twice.
|
|
5887
|
+
*/
|
|
5888
|
+
end() {
|
|
5889
|
+
if (!this.enabled || this.ended) return;
|
|
5890
|
+
this.ended = true;
|
|
5891
|
+
if (!this.animated) {
|
|
5892
|
+
this.write(`${this.render(true)}
|
|
5893
|
+
`);
|
|
5894
|
+
return;
|
|
5895
|
+
}
|
|
5896
|
+
if (!this.onRow || !this.cursorFree()) return;
|
|
5897
|
+
this.write("\r\x1B[K");
|
|
5898
|
+
this.rendered = "";
|
|
5899
|
+
this.onRow = false;
|
|
5900
|
+
}
|
|
5901
|
+
/** Rewrite the current row in place, skipping no-op rewrites and unsafe moments. */
|
|
5902
|
+
draw() {
|
|
5903
|
+
if (!this.animated || !this.cursorFree()) return;
|
|
5904
|
+
const text = this.render();
|
|
5905
|
+
if (text === this.rendered) return;
|
|
5906
|
+
this.write(`\r\x1B[K${text}`);
|
|
5907
|
+
this.rendered = text;
|
|
5908
|
+
this.onRow = true;
|
|
5909
|
+
}
|
|
5910
|
+
/**
|
|
5911
|
+
* Whether the cursor is at the start of a row nobody else is using. On stdout
|
|
5912
|
+
* that is not this terminal there is no shared cursor to disturb; when it is,
|
|
5913
|
+
* only a write that ended a line leaves one free.
|
|
5914
|
+
*/
|
|
5915
|
+
cursorFree() {
|
|
5916
|
+
return !this.sharesCursor || this.stdoutAtRowStart;
|
|
5917
|
+
}
|
|
5918
|
+
/**
|
|
5919
|
+
* `final` marks the line the run ends on: a run that never got past the spawn
|
|
5920
|
+
* is over, so saying it is still waiting — directly above whatever the run
|
|
5921
|
+
* reported — would be a lie.
|
|
5922
|
+
*/
|
|
5923
|
+
render(final = false) {
|
|
5924
|
+
const parts = [];
|
|
5925
|
+
if (this.streaming) {
|
|
5926
|
+
const u = this.usage.snapshot();
|
|
5927
|
+
parts.push(
|
|
5928
|
+
this.model ?? this.host,
|
|
5929
|
+
this.elapsed(),
|
|
5930
|
+
`\u2193${u.inputTokens.toLocaleString()}\u2191${u.outputTokens.toLocaleString()} tokens`
|
|
5931
|
+
);
|
|
5932
|
+
return `\u25CF ${parts.join(" \xB7 ")}`;
|
|
5933
|
+
}
|
|
5934
|
+
if (this.model !== void 0) return `\u25B6 ${[this.host, this.model, this.elapsed()].join(" \xB7 ")}`;
|
|
5935
|
+
if (final) return `\u25B6 ${this.host} \xB7 no output after ${this.elapsed()}`;
|
|
5936
|
+
return `\u25B6 ${this.host} \xB7 waiting for first event\u2026`;
|
|
5937
|
+
}
|
|
5938
|
+
elapsed() {
|
|
5939
|
+
return humanizeElapsed(this.now() - this.startedAt);
|
|
5940
|
+
}
|
|
5941
|
+
};
|
|
5942
|
+
}
|
|
5943
|
+
});
|
|
5944
|
+
|
|
5239
5945
|
// src/commands/run.ts
|
|
5240
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as
|
|
5946
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
5241
5947
|
import { join as join5 } from "path";
|
|
5242
5948
|
import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
|
|
5243
|
-
import { loadNoirEnv as loadNoirEnv3, NOIR_DIR as NOIR_DIR2, parseConfig } from "@noir-ai/core";
|
|
5949
|
+
import { isDeniedEnvKey as isDeniedEnvKey2, loadNoirEnv as loadNoirEnv3, NOIR_DIR as NOIR_DIR2, parseConfig, paths as paths2 } from "@noir-ai/core";
|
|
5244
5950
|
function credentialNote(env2, sources) {
|
|
5245
5951
|
const named = CREDENTIAL_ENV_VARS.filter((name) => {
|
|
5246
5952
|
const value = env2[name];
|
|
@@ -5260,13 +5966,24 @@ function formatUsage(u) {
|
|
|
5260
5966
|
const turns = u.numTurns > 0 ? ` \xB7 ${u.numTurns} turns` : "";
|
|
5261
5967
|
return `${u.inputTokens.toLocaleString()} in / ${u.outputTokens.toLocaleString()} out${cost}${turns}`;
|
|
5262
5968
|
}
|
|
5263
|
-
function
|
|
5264
|
-
if (opts.json === true) return;
|
|
5969
|
+
function answerText(event) {
|
|
5265
5970
|
if (event.kind === "assistant" && event.text && event.text.length > 0 && event.isError !== true) {
|
|
5266
|
-
|
|
5971
|
+
return event.text;
|
|
5972
|
+
}
|
|
5973
|
+
return void 0;
|
|
5974
|
+
}
|
|
5975
|
+
function streamEvent(event, opts, status2) {
|
|
5976
|
+
if (opts.json === true) return;
|
|
5977
|
+
const text = answerText(event);
|
|
5978
|
+
if (text !== void 0) {
|
|
5979
|
+
status2.beforeStdout(text);
|
|
5980
|
+
process.stdout.write(text);
|
|
5267
5981
|
}
|
|
5268
5982
|
}
|
|
5269
5983
|
async function run(prompt, opts) {
|
|
5984
|
+
await runOnce(prompt, opts, void 0);
|
|
5985
|
+
}
|
|
5986
|
+
async function runOnce(prompt, opts, resumeSessionId) {
|
|
5270
5987
|
const host = opts.host ?? "claude";
|
|
5271
5988
|
if (!SUPPORTED_HOSTS.includes(host)) {
|
|
5272
5989
|
fail(EXIT.USAGE, `unknown host '${host}' (supported: ${SUPPORTED_HOSTS.join(", ")})`, opts);
|
|
@@ -5285,7 +6002,7 @@ async function run(prompt, opts) {
|
|
|
5285
6002
|
if (!resolved.ok) fail(EXIT.USAGE, resolved.message, opts);
|
|
5286
6003
|
const profile = resolved.profile;
|
|
5287
6004
|
const customBinary = opts.command ?? profile.binary;
|
|
5288
|
-
const extraArgs = profile.args;
|
|
6005
|
+
const extraArgs = resumeSessionId === void 0 ? profile.args : [...profile.args ?? [], "--resume", resumeSessionId];
|
|
5289
6006
|
const env2 = profile.env ? mergeEnv(process.env, profile.env) : void 0;
|
|
5290
6007
|
if (!existsSync3(join5(root, NOIR_DIR2, ".env"))) {
|
|
5291
6008
|
log(
|
|
@@ -5294,6 +6011,37 @@ async function run(prompt, opts) {
|
|
|
5294
6011
|
);
|
|
5295
6012
|
}
|
|
5296
6013
|
const transcriptLines = [];
|
|
6014
|
+
const answer = [];
|
|
6015
|
+
let sessionId;
|
|
6016
|
+
const binary = customBinary && customBinary.length > 0 ? customBinary : host;
|
|
6017
|
+
const status2 = new RunStatusLine({
|
|
6018
|
+
host: binary,
|
|
6019
|
+
json: opts.json,
|
|
6020
|
+
quiet: opts.quiet,
|
|
6021
|
+
stderrIsTty: process.stderr.isTTY === true,
|
|
6022
|
+
stdoutSharesCursor: process.stdout.isTTY === true
|
|
6023
|
+
});
|
|
6024
|
+
status2.begin();
|
|
6025
|
+
let verdictWritten = false;
|
|
6026
|
+
const claimVerdict = () => {
|
|
6027
|
+
if (verdictWritten) return false;
|
|
6028
|
+
verdictWritten = true;
|
|
6029
|
+
return true;
|
|
6030
|
+
};
|
|
6031
|
+
const interrupt = new RunInterrupt({
|
|
6032
|
+
// A second interrupt leaves at once, and leaves the same verdict behind as
|
|
6033
|
+
// the first: the run owes its consumer an envelope under --json, and this is
|
|
6034
|
+
// the one path where `fail()` cannot write it (there is no caller left to
|
|
6035
|
+
// catch its throw). The child is forced first, so leaving does not leave a
|
|
6036
|
+
// host behind.
|
|
6037
|
+
exitNow: (code) => {
|
|
6038
|
+
interrupt.forceNow();
|
|
6039
|
+
if (claimVerdict()) {
|
|
6040
|
+
failAndExit(code, interruptedNotice(safeTranscript(host, transcriptLines)), opts);
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
});
|
|
6044
|
+
interrupt.watch();
|
|
5297
6045
|
let result;
|
|
5298
6046
|
try {
|
|
5299
6047
|
result = await runHost({
|
|
@@ -5303,28 +6051,61 @@ async function run(prompt, opts) {
|
|
|
5303
6051
|
extraArgs,
|
|
5304
6052
|
env: env2,
|
|
5305
6053
|
onLine: (line) => transcriptLines.push(line),
|
|
5306
|
-
onEvent: (event) =>
|
|
6054
|
+
onEvent: (event) => {
|
|
6055
|
+
if (event.kind === "init" && sessionId === void 0) sessionId = event.sessionId;
|
|
6056
|
+
status2.event(event);
|
|
6057
|
+
streamEvent(event, opts, status2);
|
|
6058
|
+
const text = answerText(event);
|
|
6059
|
+
if (text !== void 0) answer.push(text);
|
|
6060
|
+
},
|
|
6061
|
+
signal: interrupt.signal,
|
|
6062
|
+
onChild: (child) => interrupt.track(child)
|
|
5307
6063
|
});
|
|
5308
6064
|
} catch (err) {
|
|
5309
|
-
|
|
6065
|
+
status2.end();
|
|
6066
|
+
status2.finishRow();
|
|
6067
|
+
if (interrupt.interruptedBy !== void 0) {
|
|
6068
|
+
if (claimVerdict()) {
|
|
6069
|
+
failInterrupted(interrupt.interruptedBy, host, transcriptLines, opts);
|
|
6070
|
+
}
|
|
6071
|
+
return;
|
|
6072
|
+
}
|
|
5310
6073
|
const detail = err instanceof Error ? err.message : String(err);
|
|
5311
6074
|
const enoent = err?.code === "ENOENT";
|
|
5312
6075
|
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}.` : "";
|
|
5313
6076
|
const subject = customBinary && customBinary.length > 0 ? `custom command '${customBinary}'` : `host '${host}'`;
|
|
5314
6077
|
fail(EXIT.ERROR, `failed to run ${subject}: ${detail}.${guidance}`, opts);
|
|
6078
|
+
} finally {
|
|
6079
|
+
interrupt.unwatch();
|
|
5315
6080
|
}
|
|
5316
|
-
|
|
6081
|
+
if (interrupt.interruptedBy !== void 0) {
|
|
6082
|
+
status2.end();
|
|
6083
|
+
status2.finishRow();
|
|
6084
|
+
if (claimVerdict()) {
|
|
6085
|
+
failInterrupted(interrupt.interruptedBy, host, transcriptLines, opts);
|
|
6086
|
+
}
|
|
6087
|
+
return;
|
|
6088
|
+
}
|
|
6089
|
+
const transcript = safeTranscript(host, transcriptLines);
|
|
5317
6090
|
const failed = result.exitCode !== 0 || result.isError;
|
|
5318
6091
|
if (failed) {
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
const
|
|
6092
|
+
status2.end();
|
|
6093
|
+
status2.finishRow();
|
|
6094
|
+
const stderrTail = hostStderrTail(result.stderr);
|
|
6095
|
+
const reason = result.errorText && result.errorText.trim().length > 0 ? result.errorText.trim() : `exit code ${result.exitCode}`;
|
|
6096
|
+
const isAuth = result.errorCategory === "authentication_failed" || result.errorCategory === "oauth_org_not_allowed" || /not logged|login|authenticate|invalid api key/i.test(`${reason}
|
|
6097
|
+
${stderrTail}`);
|
|
5322
6098
|
let message = `host '${binary}' failed (exit ${result.exitCode}): ${reason}`;
|
|
5323
6099
|
if (isAuth) {
|
|
5324
6100
|
message += ` Open a terminal and run \`${binary} /login\` (interactive-only \u2014 it cannot run inside \`noir run\`), then retry.`;
|
|
5325
6101
|
message += credentialNote(env2 ?? process.env, envSources);
|
|
5326
6102
|
}
|
|
5327
6103
|
message += ` If you use another profile, pass \`--command <binary>\` or define a run profile under run.profiles. transcript: ${transcript}`;
|
|
6104
|
+
if (opts.json !== true && stderrTail.length > 0) {
|
|
6105
|
+
message += `
|
|
6106
|
+
${binary} stderr (last ${HOST_STDERR_TAIL_LINES} lines):
|
|
6107
|
+
${stderrTail}`;
|
|
6108
|
+
}
|
|
5328
6109
|
fail(EXIT.ERROR, message, opts);
|
|
5329
6110
|
}
|
|
5330
6111
|
if (opts.json === true) {
|
|
@@ -5337,26 +6118,59 @@ async function run(prompt, opts) {
|
|
|
5337
6118
|
usage: result.usage,
|
|
5338
6119
|
numTurns: result.usage.numTurns,
|
|
5339
6120
|
events: result.eventCount,
|
|
5340
|
-
transcript
|
|
6121
|
+
transcript,
|
|
6122
|
+
// The answer as plain words, so a consumer does not have to reassemble
|
|
6123
|
+
// it from the stream-json transcript.
|
|
6124
|
+
answerText: answer.join(""),
|
|
6125
|
+
...sessionId === void 0 ? {} : { sessionId }
|
|
5341
6126
|
}
|
|
5342
6127
|
});
|
|
5343
6128
|
return;
|
|
5344
6129
|
}
|
|
6130
|
+
status2.end();
|
|
5345
6131
|
process.stdout.write("\n");
|
|
5346
6132
|
success(`usage: ${formatUsage(result.usage)} (API-equivalent estimate, not billed)`, opts);
|
|
5347
6133
|
log(`transcript: ${transcript}`, opts);
|
|
6134
|
+
await offerPostRunActions({
|
|
6135
|
+
answer: answer.join(""),
|
|
6136
|
+
transcript,
|
|
6137
|
+
sessionId,
|
|
6138
|
+
host: binary,
|
|
6139
|
+
opts,
|
|
6140
|
+
// Only the first run of a session can be continued: a resumed run is
|
|
6141
|
+
// itself the continuation, so the menu never nests more than one deep.
|
|
6142
|
+
...resumeSessionId === void 0 && sessionId !== void 0 ? { resume: (nextPrompt, id) => runOnce(nextPrompt, opts, id) } : {}
|
|
6143
|
+
});
|
|
5348
6144
|
}
|
|
5349
6145
|
function mergeEnv(base, overlay) {
|
|
6146
|
+
for (const key of Object.keys(overlay)) {
|
|
6147
|
+
if (isDeniedEnvKey2(key)) {
|
|
6148
|
+
fail(
|
|
6149
|
+
EXIT.ERROR,
|
|
6150
|
+
`refusing to run the host: the profile env defines process-injection key "${key}" (it can inject into the spawned host process)`
|
|
6151
|
+
);
|
|
6152
|
+
}
|
|
6153
|
+
}
|
|
5350
6154
|
const merged = { ...base, ...overlay };
|
|
5351
6155
|
return Object.fromEntries(Object.entries(merged).filter(([, v]) => v !== void 0));
|
|
5352
6156
|
}
|
|
5353
|
-
function
|
|
6157
|
+
function failInterrupted(signal, host, lines, opts) {
|
|
6158
|
+
return fail(exitCodeForSignal(signal), interruptedNotice(safeTranscript(host, lines)), opts);
|
|
6159
|
+
}
|
|
6160
|
+
function safeTranscript(host, lines) {
|
|
6161
|
+
try {
|
|
6162
|
+
return writeTranscript(host, lines);
|
|
6163
|
+
} catch {
|
|
6164
|
+
return "(not persisted)";
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
function writeTranscript(host, lines, root = process.cwd()) {
|
|
5354
6168
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
5355
|
-
const dir =
|
|
6169
|
+
const dir = paths2.transcriptsDir(root);
|
|
5356
6170
|
const file = join5(dir, `${host}-${ts}.jsonl`);
|
|
5357
6171
|
try {
|
|
5358
6172
|
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
5359
|
-
|
|
6173
|
+
writeFileSync4(file, `${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`, { mode: 384 });
|
|
5360
6174
|
} catch {
|
|
5361
6175
|
return "(not persisted)";
|
|
5362
6176
|
}
|
|
@@ -5368,7 +6182,10 @@ var init_run = __esm({
|
|
|
5368
6182
|
"use strict";
|
|
5369
6183
|
init_orchestrator();
|
|
5370
6184
|
init_output();
|
|
6185
|
+
init_run_actions();
|
|
6186
|
+
init_run_interrupt();
|
|
5371
6187
|
init_run_profiles();
|
|
6188
|
+
init_run_status();
|
|
5372
6189
|
CREDENTIAL_ENV_VARS = [
|
|
5373
6190
|
"ANTHROPIC_API_KEY",
|
|
5374
6191
|
"ANTHROPIC_AUTH_TOKEN",
|
|
@@ -5377,254 +6194,17 @@ var init_run = __esm({
|
|
|
5377
6194
|
}
|
|
5378
6195
|
});
|
|
5379
6196
|
|
|
5380
|
-
// src/
|
|
5381
|
-
import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
|
|
5382
|
-
import { loadProjectInfo as loadProjectInfo14 } from "@noir-ai/core";
|
|
6197
|
+
// src/conflict.ts
|
|
5383
6198
|
import {
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
function categoryOf(name) {
|
|
5390
|
-
const mapped = CATEGORY[name];
|
|
5391
|
-
if (mapped !== void 0) return mapped;
|
|
5392
|
-
return name.replace(/^noir-/, "") || "general";
|
|
5393
|
-
}
|
|
5394
|
-
function toRow(s, kind = "builtin") {
|
|
5395
|
-
const description = typeof s.frontmatter.description === "string" ? s.frontmatter.description : "";
|
|
5396
|
-
const status2 = s.skillMd.includes("> **Stub:**") ? "stub" : "full";
|
|
5397
|
-
const category = s.frontmatter.metadata?.category?.trim() || categoryOf(s.name);
|
|
5398
|
-
return { name: s.name, category, description, kind, status: status2 };
|
|
5399
|
-
}
|
|
5400
|
-
function truncate(text, max) {
|
|
5401
|
-
if (text.length <= max) return text;
|
|
5402
|
-
const slice = text.slice(0, Math.max(0, max - 1));
|
|
5403
|
-
const boundary = slice.lastIndexOf(" ");
|
|
5404
|
-
const head = boundary > 0 ? slice.slice(0, boundary) : slice;
|
|
5405
|
-
return `${head.trimEnd()}\u2026`;
|
|
6199
|
+
lineDiff
|
|
6200
|
+
} from "@noir-ai/create";
|
|
6201
|
+
function flaggedNonInteractive() {
|
|
6202
|
+
const v = process.env.NOIR_NON_INTERACTIVE;
|
|
6203
|
+
return v !== void 0 && v !== "";
|
|
5406
6204
|
}
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5410
|
-
try {
|
|
5411
|
-
const all = discoverAll();
|
|
5412
|
-
builtins = all.builtins;
|
|
5413
|
-
integrations = all.integrations;
|
|
5414
|
-
} catch (err) {
|
|
5415
|
-
fail(
|
|
5416
|
-
EXIT.ERROR,
|
|
5417
|
-
`skills list: could not discover skills (${err instanceof Error ? err.message : String(err)})`,
|
|
5418
|
-
opts
|
|
5419
|
-
);
|
|
5420
|
-
}
|
|
5421
|
-
const rows = [
|
|
5422
|
-
...builtins.map((b) => toRow(b, "builtin")),
|
|
5423
|
-
...integrations.map((i) => toRow(i, "integration"))
|
|
5424
|
-
];
|
|
5425
|
-
const data = { count: rows.length, skills: rows };
|
|
5426
|
-
if (opts.json === true) {
|
|
5427
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
5428
|
-
`);
|
|
5429
|
-
return;
|
|
5430
|
-
}
|
|
5431
|
-
const intCount = integrations.length;
|
|
5432
|
-
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"}`;
|
|
5433
|
-
log(banner, opts);
|
|
5434
|
-
table(
|
|
5435
|
-
rows.map((r) => ({
|
|
5436
|
-
Skill: r.name,
|
|
5437
|
-
Kind: r.kind,
|
|
5438
|
-
Category: r.category,
|
|
5439
|
-
Status: r.status,
|
|
5440
|
-
Description: truncate(r.description, 80)
|
|
5441
|
-
})),
|
|
5442
|
-
["Skill", "Kind", "Category", "Status", "Description"],
|
|
5443
|
-
opts
|
|
5444
|
-
);
|
|
5445
|
-
}
|
|
5446
|
-
async function skillsSync(opts) {
|
|
5447
|
-
const root = process.cwd();
|
|
5448
|
-
let project;
|
|
5449
|
-
try {
|
|
5450
|
-
project = loadProjectInfo14(root);
|
|
5451
|
-
} catch {
|
|
5452
|
-
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
5453
|
-
}
|
|
5454
|
-
const adapter = resolveAdapter4(project.config.host);
|
|
5455
|
-
if (!adapter.skillsDir) {
|
|
5456
|
-
const data2 = {
|
|
5457
|
-
emitted: [],
|
|
5458
|
-
references: 0,
|
|
5459
|
-
host: project.config.host,
|
|
5460
|
-
reason: "no-skill-emitter"
|
|
5461
|
-
};
|
|
5462
|
-
if (opts.json === true) {
|
|
5463
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
|
|
5464
|
-
`);
|
|
5465
|
-
return;
|
|
5466
|
-
}
|
|
5467
|
-
info("This host has no skill emitter; nothing to sync.", opts);
|
|
5468
|
-
return;
|
|
5469
|
-
}
|
|
5470
|
-
const dir = adapter.skillsDir({ root });
|
|
5471
|
-
const target = project.config.host;
|
|
5472
|
-
const summary = await emitSkillsToDir(dir, { target });
|
|
5473
|
-
const pruned = summary.pruned ?? [];
|
|
5474
|
-
const data = {
|
|
5475
|
-
emitted: summary.emitted,
|
|
5476
|
-
references: summary.references,
|
|
5477
|
-
dir,
|
|
5478
|
-
pruned
|
|
5479
|
-
};
|
|
5480
|
-
if (opts.json === true) {
|
|
5481
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
5482
|
-
`);
|
|
5483
|
-
return;
|
|
5484
|
-
}
|
|
5485
|
-
log(
|
|
5486
|
-
`Synced ${summary.emitted.length} Noir skill${summary.emitted.length === 1 ? "" : "s"} to ${dir}.`,
|
|
5487
|
-
opts
|
|
5488
|
-
);
|
|
5489
|
-
if (pruned.length > 0) {
|
|
5490
|
-
warn(
|
|
5491
|
-
`Pruned ${pruned.length} stale noir-* skill dir${pruned.length === 1 ? "" : "s"}: ${pruned.join(", ")}`,
|
|
5492
|
-
opts
|
|
5493
|
-
);
|
|
5494
|
-
}
|
|
5495
|
-
}
|
|
5496
|
-
async function skillsLint(opts) {
|
|
5497
|
-
let skills;
|
|
5498
|
-
try {
|
|
5499
|
-
const all = discoverAll();
|
|
5500
|
-
skills = [...all.builtins, ...all.integrations].map(lintSkill);
|
|
5501
|
-
} catch (err) {
|
|
5502
|
-
fail(
|
|
5503
|
-
EXIT.ERROR,
|
|
5504
|
-
`skills lint: could not lint skills (${err instanceof Error ? err.message : String(err)})`,
|
|
5505
|
-
opts
|
|
5506
|
-
);
|
|
5507
|
-
}
|
|
5508
|
-
const errored = skills.filter((s) => s.errors.length > 0);
|
|
5509
|
-
const warned = skills.filter((s) => s.warnings.length > 0 && s.errors.length === 0);
|
|
5510
|
-
const data = { count: skills.length, errored: errored.length, skills };
|
|
5511
|
-
if (errored.length > 0) {
|
|
5512
|
-
const message = `skills lint: ${errored.length} skill${errored.length === 1 ? "" : "s"} failed validation`;
|
|
5513
|
-
if (opts.json === true) {
|
|
5514
|
-
process.stdout.write(
|
|
5515
|
-
`${JSON.stringify({ ok: false, error: { code: EXIT.ERROR, message }, data })}
|
|
5516
|
-
`
|
|
5517
|
-
);
|
|
5518
|
-
process.exitCode = EXIT.ERROR;
|
|
5519
|
-
return;
|
|
5520
|
-
}
|
|
5521
|
-
warn(
|
|
5522
|
-
`noir skills lint \u2014 ${errored.length} skill${errored.length === 1 ? "" : "s"} FAIL validation:`,
|
|
5523
|
-
opts
|
|
5524
|
-
);
|
|
5525
|
-
for (const s of errored) {
|
|
5526
|
-
for (const e of s.errors) warn(` ${s.name}: ${e}`, opts);
|
|
5527
|
-
}
|
|
5528
|
-
if (warned.length > 0) {
|
|
5529
|
-
info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
|
|
5530
|
-
for (const s of warned) {
|
|
5531
|
-
for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
|
|
5532
|
-
}
|
|
5533
|
-
}
|
|
5534
|
-
fail(EXIT.ERROR, message, opts);
|
|
5535
|
-
}
|
|
5536
|
-
if (opts.json === true) {
|
|
5537
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
5538
|
-
`);
|
|
5539
|
-
return;
|
|
5540
|
-
}
|
|
5541
|
-
log(`noir skills lint \u2014 ${skills.length} skills, all validate clean.`, opts);
|
|
5542
|
-
if (warned.length > 0) {
|
|
5543
|
-
info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
|
|
5544
|
-
for (const s of warned) {
|
|
5545
|
-
for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
|
|
5546
|
-
}
|
|
5547
|
-
}
|
|
5548
|
-
}
|
|
5549
|
-
async function skillsRegistry(opts) {
|
|
5550
|
-
let reg;
|
|
5551
|
-
try {
|
|
5552
|
-
reg = buildRegistry();
|
|
5553
|
-
} catch (err) {
|
|
5554
|
-
fail(
|
|
5555
|
-
EXIT.ERROR,
|
|
5556
|
-
`skills registry: could not build registry (${err instanceof Error ? err.message : String(err)})`,
|
|
5557
|
-
opts
|
|
5558
|
-
);
|
|
5559
|
-
}
|
|
5560
|
-
const data = { count: reg.length, skills: reg };
|
|
5561
|
-
if (opts.json === true) {
|
|
5562
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
5563
|
-
`);
|
|
5564
|
-
return;
|
|
5565
|
-
}
|
|
5566
|
-
log(`noir skills registry \u2014 ${reg.length} skills.`, opts);
|
|
5567
|
-
table(
|
|
5568
|
-
reg.map((r) => ({
|
|
5569
|
-
Skill: r.name,
|
|
5570
|
-
Kind: r.kind,
|
|
5571
|
-
Category: r.category,
|
|
5572
|
-
Version: r.version,
|
|
5573
|
-
Status: r.status,
|
|
5574
|
-
Refs: String(r.referenceCount),
|
|
5575
|
-
Lines: String(r.lines)
|
|
5576
|
-
})),
|
|
5577
|
-
["Skill", "Kind", "Category", "Version", "Status", "Refs", "Lines"],
|
|
5578
|
-
opts
|
|
5579
|
-
);
|
|
5580
|
-
}
|
|
5581
|
-
var CATEGORY;
|
|
5582
|
-
var init_skills = __esm({
|
|
5583
|
-
"src/commands/skills.ts"() {
|
|
5584
|
-
"use strict";
|
|
5585
|
-
init_output();
|
|
5586
|
-
CATEGORY = {
|
|
5587
|
-
"noir-brainstorming": "discovery",
|
|
5588
|
-
"noir-sync": "discovery",
|
|
5589
|
-
"noir-checkpoint": "discovery",
|
|
5590
|
-
"noir-exploring": "discovery",
|
|
5591
|
-
"noir-spec": "spec",
|
|
5592
|
-
"noir-planning": "plan",
|
|
5593
|
-
"noir-prd": "plan",
|
|
5594
|
-
"noir-executing-plans": "execute",
|
|
5595
|
-
"noir-test-driven-development": "execute",
|
|
5596
|
-
"noir-systematic-debugging": "execute",
|
|
5597
|
-
"noir-subagent": "execute",
|
|
5598
|
-
"noir-parallel": "execute",
|
|
5599
|
-
"noir-verifying": "verify",
|
|
5600
|
-
"noir-security": "verify",
|
|
5601
|
-
"noir-wrap": "document",
|
|
5602
|
-
"noir-readme": "document",
|
|
5603
|
-
"noir-shipping": "git",
|
|
5604
|
-
"noir-worktree": "git",
|
|
5605
|
-
"noir-recall": "memory",
|
|
5606
|
-
"noir-remember": "memory",
|
|
5607
|
-
"noir-context": "context",
|
|
5608
|
-
"noir-frontend": "domain",
|
|
5609
|
-
"noir-backend": "domain",
|
|
5610
|
-
"noir-doctor": "meta",
|
|
5611
|
-
"noir-writing-skills": "meta",
|
|
5612
|
-
"noir-rules": "meta"
|
|
5613
|
-
};
|
|
5614
|
-
}
|
|
5615
|
-
});
|
|
5616
|
-
|
|
5617
|
-
// src/conflict.ts
|
|
5618
|
-
import {
|
|
5619
|
-
lineDiff
|
|
5620
|
-
} from "@noir-ai/create";
|
|
5621
|
-
function flaggedNonInteractive() {
|
|
5622
|
-
const v = process.env.NOIR_NON_INTERACTIVE;
|
|
5623
|
-
return v !== void 0 && v !== "";
|
|
5624
|
-
}
|
|
5625
|
-
function buildConflictOpts(input = {}) {
|
|
5626
|
-
if (input.force === true) {
|
|
5627
|
-
return { conflictPolicy: "overwrite" };
|
|
6205
|
+
function buildConflictOpts(input = {}) {
|
|
6206
|
+
if (input.force === true) {
|
|
6207
|
+
return { conflictPolicy: "overwrite" };
|
|
5628
6208
|
}
|
|
5629
6209
|
if (input.interactive === false) {
|
|
5630
6210
|
return { conflictPolicy: "preserve" };
|
|
@@ -5723,7 +6303,7 @@ var init_conflict = __esm({
|
|
|
5723
6303
|
});
|
|
5724
6304
|
|
|
5725
6305
|
// src/dedup-write.ts
|
|
5726
|
-
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, writeFileSync as
|
|
6306
|
+
import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync5 } from "fs";
|
|
5727
6307
|
import { join as join6 } from "path";
|
|
5728
6308
|
import { sha256Hex as sha256Hex2, sha256Hex12 } from "@noir-ai/core";
|
|
5729
6309
|
async function checkWritePathDedup(root, res, opts) {
|
|
@@ -5872,14 +6452,14 @@ async function promptDedupAction(root, proposedRel, matchedRel, proposedText, si
|
|
|
5872
6452
|
return "preserve";
|
|
5873
6453
|
} else if (choice === "replace") {
|
|
5874
6454
|
try {
|
|
5875
|
-
|
|
6455
|
+
writeFileSync5(join6(root, matchedRel), proposedText, "utf8");
|
|
5876
6456
|
} catch {
|
|
5877
6457
|
}
|
|
5878
6458
|
return "replace";
|
|
5879
6459
|
} else if (choice === "mirror") {
|
|
5880
6460
|
try {
|
|
5881
6461
|
const existing = readFileSync4(join6(root, matchedRel), "utf8");
|
|
5882
|
-
|
|
6462
|
+
writeFileSync5(
|
|
5883
6463
|
join6(root, matchedRel),
|
|
5884
6464
|
`${existing}
|
|
5885
6465
|
|
|
@@ -5918,7 +6498,7 @@ function loadCache(root) {
|
|
|
5918
6498
|
function saveCache(root, cache) {
|
|
5919
6499
|
try {
|
|
5920
6500
|
mkdirSync4(join6(root, ".noir"), { recursive: true });
|
|
5921
|
-
|
|
6501
|
+
writeFileSync5(cachePath(root), JSON.stringify(cache), "utf8");
|
|
5922
6502
|
} catch {
|
|
5923
6503
|
}
|
|
5924
6504
|
}
|
|
@@ -5940,16 +6520,16 @@ function safeRead(abs) {
|
|
|
5940
6520
|
}
|
|
5941
6521
|
}
|
|
5942
6522
|
function withTimeout(p, ms) {
|
|
5943
|
-
return new Promise((
|
|
5944
|
-
const t = setTimeout(() =>
|
|
6523
|
+
return new Promise((resolve3) => {
|
|
6524
|
+
const t = setTimeout(() => resolve3(void 0), ms);
|
|
5945
6525
|
p.then(
|
|
5946
6526
|
(v) => {
|
|
5947
6527
|
clearTimeout(t);
|
|
5948
|
-
|
|
6528
|
+
resolve3(v);
|
|
5949
6529
|
},
|
|
5950
6530
|
() => {
|
|
5951
6531
|
clearTimeout(t);
|
|
5952
|
-
|
|
6532
|
+
resolve3(void 0);
|
|
5953
6533
|
}
|
|
5954
6534
|
);
|
|
5955
6535
|
});
|
|
@@ -5972,13 +6552,16 @@ var init_dedup_write = __esm({
|
|
|
5972
6552
|
});
|
|
5973
6553
|
|
|
5974
6554
|
// src/init.ts
|
|
5975
|
-
import {
|
|
5976
|
-
import {
|
|
6555
|
+
import { existsSync as existsSync4 } from "fs";
|
|
6556
|
+
import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
|
|
6557
|
+
import { loadProjectInfo as loadProjectInfo14, paths as paths3 } from "@noir-ai/core";
|
|
5977
6558
|
import { scaffold } from "@noir-ai/create";
|
|
5978
|
-
import {
|
|
6559
|
+
import {
|
|
6560
|
+
emitSkillsToDir
|
|
6561
|
+
} from "@noir-ai/skills";
|
|
5979
6562
|
async function init(root, opts) {
|
|
5980
6563
|
assertTransportUrl(opts);
|
|
5981
|
-
const host = opts
|
|
6564
|
+
const host = resolveInitHost(root, opts);
|
|
5982
6565
|
const interactive = resolveInteractive();
|
|
5983
6566
|
const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
|
|
5984
6567
|
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
@@ -5999,15 +6582,29 @@ async function init(root, opts) {
|
|
|
5999
6582
|
return res;
|
|
6000
6583
|
}
|
|
6001
6584
|
if (res.noop) return res;
|
|
6002
|
-
await emitHostSkills(root, host, conflictOpts, interactive);
|
|
6585
|
+
const skillSummary = await emitHostSkills(root, host, conflictOpts, interactive);
|
|
6586
|
+
res.skillConflicts = skillSummary?.conflicts ?? [];
|
|
6587
|
+
res.preservedSkills = skillSummary?.preserved ?? [];
|
|
6003
6588
|
let projectInfo;
|
|
6004
6589
|
try {
|
|
6005
|
-
projectInfo =
|
|
6590
|
+
projectInfo = loadProjectInfo14(root);
|
|
6006
6591
|
} catch {
|
|
6007
6592
|
projectInfo = void 0;
|
|
6008
6593
|
}
|
|
6009
6594
|
const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });
|
|
6010
6595
|
if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);
|
|
6596
|
+
if (res.refreshed.length > 0) {
|
|
6597
|
+
process.stderr.write(
|
|
6598
|
+
`Refreshed ${res.refreshed.length} unedited doc seed(s): ${res.refreshed.join(", ")}
|
|
6599
|
+
`
|
|
6600
|
+
);
|
|
6601
|
+
}
|
|
6602
|
+
if (res.migrationChanged.length > 0) {
|
|
6603
|
+
process.stderr.write(
|
|
6604
|
+
`Migrated ${res.migrationChanged.length} file(s): ${res.migrationChanged.join(", ")}
|
|
6605
|
+
`
|
|
6606
|
+
);
|
|
6607
|
+
}
|
|
6011
6608
|
process.stderr.write(
|
|
6012
6609
|
`Noir initialized in ${root} (host: ${host}, transport: ${opts.transport}).
|
|
6013
6610
|
`
|
|
@@ -6017,6 +6614,19 @@ async function init(root, opts) {
|
|
|
6017
6614
|
);
|
|
6018
6615
|
return res;
|
|
6019
6616
|
}
|
|
6617
|
+
function resolveInitHost(root, opts) {
|
|
6618
|
+
if (opts.host !== void 0) return opts.host;
|
|
6619
|
+
if (!existsSync4(paths3.config(root))) return "claude";
|
|
6620
|
+
try {
|
|
6621
|
+
return loadProjectInfo14(root).config.host;
|
|
6622
|
+
} catch {
|
|
6623
|
+
process.stderr.write(
|
|
6624
|
+
`Could not read the configured host from ${paths3.config(root)}; using 'claude'.
|
|
6625
|
+
`
|
|
6626
|
+
);
|
|
6627
|
+
return "claude";
|
|
6628
|
+
}
|
|
6629
|
+
}
|
|
6020
6630
|
function reportPlannedWrites(res) {
|
|
6021
6631
|
log("Dry run \u2014 no files were written.");
|
|
6022
6632
|
if (res.written.length > 0) {
|
|
@@ -6026,6 +6636,10 @@ function reportPlannedWrites(res) {
|
|
|
6026
6636
|
log(mode === void 0 ? ` ${p}` : ` ${p} (mode ${mode.toString(8).padStart(3, "0")})`);
|
|
6027
6637
|
}
|
|
6028
6638
|
}
|
|
6639
|
+
if (res.refreshed.length > 0) {
|
|
6640
|
+
log("Would refresh (unedited older copy):");
|
|
6641
|
+
for (const p of res.refreshed) log(` ${p}`);
|
|
6642
|
+
}
|
|
6029
6643
|
if (res.skipped.length > 0) {
|
|
6030
6644
|
log("Would leave as-is (already present):");
|
|
6031
6645
|
for (const p of res.skipped) log(` ${p}`);
|
|
@@ -6036,12 +6650,12 @@ function reportPlannedWrites(res) {
|
|
|
6036
6650
|
}
|
|
6037
6651
|
}
|
|
6038
6652
|
async function emitHostSkills(root, host, conflictOpts, interactive) {
|
|
6039
|
-
const adapter =
|
|
6653
|
+
const adapter = resolveAdapter4(host);
|
|
6040
6654
|
const skillsDir = adapter.skillsDir?.({ root });
|
|
6041
6655
|
if (skillsDir === void 0) {
|
|
6042
6656
|
process.stderr.write(`host '${host}' has no skill emitter; skipping skills
|
|
6043
6657
|
`);
|
|
6044
|
-
return;
|
|
6658
|
+
return void 0;
|
|
6045
6659
|
}
|
|
6046
6660
|
const target = host;
|
|
6047
6661
|
const skillOpts = {
|
|
@@ -6053,12 +6667,20 @@ async function emitHostSkills(root, host, conflictOpts, interactive) {
|
|
|
6053
6667
|
if (conflictOpts.onConflict !== void 0) {
|
|
6054
6668
|
skillOpts.onConflict = conflictOpts.onConflict;
|
|
6055
6669
|
}
|
|
6056
|
-
const summary = await
|
|
6670
|
+
const summary = await emitSkillsToDir(skillsDir, skillOpts);
|
|
6057
6671
|
const relDir = skillsDir.replace(`${root}/`, "");
|
|
6058
6672
|
process.stderr.write(
|
|
6059
6673
|
`Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).
|
|
6060
6674
|
`
|
|
6061
6675
|
);
|
|
6676
|
+
const stale = preservedStaleLine(summary.preserved ?? []);
|
|
6677
|
+
if (stale !== void 0) process.stderr.write(`${stale}
|
|
6678
|
+
`);
|
|
6679
|
+
return summary;
|
|
6680
|
+
}
|
|
6681
|
+
function preservedStaleLine(preserved) {
|
|
6682
|
+
if (preserved.length === 0) return void 0;
|
|
6683
|
+
return `${preserved.length} skill(s) preserved as stale (interactive TTY required to refresh): ${preserved.join(", ")}`;
|
|
6062
6684
|
}
|
|
6063
6685
|
function assertTransportUrl(opts) {
|
|
6064
6686
|
if (opts.transport === "streamable-http" && opts.url === void 0) {
|
|
@@ -6091,6 +6713,251 @@ var init_init = __esm({
|
|
|
6091
6713
|
}
|
|
6092
6714
|
});
|
|
6093
6715
|
|
|
6716
|
+
// src/commands/skills.ts
|
|
6717
|
+
import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
|
|
6718
|
+
import { loadProjectInfo as loadProjectInfo15 } from "@noir-ai/core";
|
|
6719
|
+
import {
|
|
6720
|
+
buildRegistry,
|
|
6721
|
+
discoverAll,
|
|
6722
|
+
emitSkillsToDir as emitSkillsToDir2,
|
|
6723
|
+
lintSkill
|
|
6724
|
+
} from "@noir-ai/skills";
|
|
6725
|
+
function categoryOf(name) {
|
|
6726
|
+
const mapped = CATEGORY[name];
|
|
6727
|
+
if (mapped !== void 0) return mapped;
|
|
6728
|
+
return name.replace(/^noir-/, "") || "general";
|
|
6729
|
+
}
|
|
6730
|
+
function toRow(s, kind = "builtin") {
|
|
6731
|
+
const description = typeof s.frontmatter.description === "string" ? s.frontmatter.description : "";
|
|
6732
|
+
const status2 = s.skillMd.includes("> **Stub:**") ? "stub" : "full";
|
|
6733
|
+
const category = s.frontmatter.metadata?.category?.trim() || categoryOf(s.name);
|
|
6734
|
+
return { name: s.name, category, description, kind, status: status2 };
|
|
6735
|
+
}
|
|
6736
|
+
function truncate(text, max) {
|
|
6737
|
+
if (text.length <= max) return text;
|
|
6738
|
+
const slice = text.slice(0, Math.max(0, max - 1));
|
|
6739
|
+
const boundary = slice.lastIndexOf(" ");
|
|
6740
|
+
const head = boundary > 0 ? slice.slice(0, boundary) : slice;
|
|
6741
|
+
return `${head.trimEnd()}\u2026`;
|
|
6742
|
+
}
|
|
6743
|
+
async function skillsList(opts) {
|
|
6744
|
+
let builtins;
|
|
6745
|
+
let integrations;
|
|
6746
|
+
try {
|
|
6747
|
+
const all = discoverAll();
|
|
6748
|
+
builtins = all.builtins;
|
|
6749
|
+
integrations = all.integrations;
|
|
6750
|
+
} catch (err) {
|
|
6751
|
+
fail(
|
|
6752
|
+
EXIT.ERROR,
|
|
6753
|
+
`skills list: could not discover skills (${err instanceof Error ? err.message : String(err)})`,
|
|
6754
|
+
opts
|
|
6755
|
+
);
|
|
6756
|
+
}
|
|
6757
|
+
const rows = [
|
|
6758
|
+
...builtins.map((b) => toRow(b, "builtin")),
|
|
6759
|
+
...integrations.map((i) => toRow(i, "integration"))
|
|
6760
|
+
];
|
|
6761
|
+
const data = { count: rows.length, skills: rows };
|
|
6762
|
+
if (opts.json === true) {
|
|
6763
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
6764
|
+
`);
|
|
6765
|
+
return;
|
|
6766
|
+
}
|
|
6767
|
+
const intCount = integrations.length;
|
|
6768
|
+
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"}`;
|
|
6769
|
+
log(banner, opts);
|
|
6770
|
+
table(
|
|
6771
|
+
rows.map((r) => ({
|
|
6772
|
+
Skill: r.name,
|
|
6773
|
+
Kind: r.kind,
|
|
6774
|
+
Category: r.category,
|
|
6775
|
+
Status: r.status,
|
|
6776
|
+
Description: truncate(r.description, 80)
|
|
6777
|
+
})),
|
|
6778
|
+
["Skill", "Kind", "Category", "Status", "Description"],
|
|
6779
|
+
opts
|
|
6780
|
+
);
|
|
6781
|
+
}
|
|
6782
|
+
async function skillsSync(opts) {
|
|
6783
|
+
const root = process.cwd();
|
|
6784
|
+
let project;
|
|
6785
|
+
try {
|
|
6786
|
+
project = loadProjectInfo15(root);
|
|
6787
|
+
} catch {
|
|
6788
|
+
fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
|
|
6789
|
+
}
|
|
6790
|
+
const adapter = resolveAdapter5(project.config.host);
|
|
6791
|
+
if (!adapter.skillsDir) {
|
|
6792
|
+
const data2 = {
|
|
6793
|
+
emitted: [],
|
|
6794
|
+
references: 0,
|
|
6795
|
+
host: project.config.host,
|
|
6796
|
+
reason: "no-skill-emitter"
|
|
6797
|
+
};
|
|
6798
|
+
if (opts.json === true) {
|
|
6799
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
|
|
6800
|
+
`);
|
|
6801
|
+
return;
|
|
6802
|
+
}
|
|
6803
|
+
info("This host has no skill emitter; nothing to sync.", opts);
|
|
6804
|
+
return;
|
|
6805
|
+
}
|
|
6806
|
+
const dir = adapter.skillsDir({ root });
|
|
6807
|
+
const target = project.config.host;
|
|
6808
|
+
const summary = await emitSkillsToDir2(dir, { target });
|
|
6809
|
+
const pruned = summary.pruned ?? [];
|
|
6810
|
+
const preserved = summary.preserved ?? [];
|
|
6811
|
+
const data = {
|
|
6812
|
+
emitted: summary.emitted,
|
|
6813
|
+
references: summary.references,
|
|
6814
|
+
dir,
|
|
6815
|
+
pruned,
|
|
6816
|
+
// What the count above does NOT cover: skills whose files were preserved
|
|
6817
|
+
// (stale on disk, by design, in a non-interactive run). Same field `noir
|
|
6818
|
+
// init --json` carries, so a consumer reads one shape from every emitter.
|
|
6819
|
+
preserved
|
|
6820
|
+
};
|
|
6821
|
+
if (opts.json === true) {
|
|
6822
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
6823
|
+
`);
|
|
6824
|
+
return;
|
|
6825
|
+
}
|
|
6826
|
+
log(
|
|
6827
|
+
`Synced ${summary.emitted.length} Noir skill${summary.emitted.length === 1 ? "" : "s"} to ${dir}.`,
|
|
6828
|
+
opts
|
|
6829
|
+
);
|
|
6830
|
+
if (pruned.length > 0) {
|
|
6831
|
+
warn(
|
|
6832
|
+
`Pruned ${pruned.length} stale noir-* skill dir${pruned.length === 1 ? "" : "s"}: ${pruned.join(", ")}`,
|
|
6833
|
+
opts
|
|
6834
|
+
);
|
|
6835
|
+
}
|
|
6836
|
+
const stale = preservedStaleLine(preserved);
|
|
6837
|
+
if (stale !== void 0) warn(stale, opts);
|
|
6838
|
+
}
|
|
6839
|
+
async function skillsLint(opts) {
|
|
6840
|
+
let skills;
|
|
6841
|
+
try {
|
|
6842
|
+
const all = discoverAll();
|
|
6843
|
+
skills = [...all.builtins, ...all.integrations].map(lintSkill);
|
|
6844
|
+
} catch (err) {
|
|
6845
|
+
fail(
|
|
6846
|
+
EXIT.ERROR,
|
|
6847
|
+
`skills lint: could not lint skills (${err instanceof Error ? err.message : String(err)})`,
|
|
6848
|
+
opts
|
|
6849
|
+
);
|
|
6850
|
+
}
|
|
6851
|
+
const errored = skills.filter((s) => s.errors.length > 0);
|
|
6852
|
+
const warned = skills.filter((s) => s.warnings.length > 0 && s.errors.length === 0);
|
|
6853
|
+
const data = { count: skills.length, errored: errored.length, skills };
|
|
6854
|
+
if (errored.length > 0) {
|
|
6855
|
+
const message = `skills lint: ${errored.length} skill${errored.length === 1 ? "" : "s"} failed validation`;
|
|
6856
|
+
if (opts.json === true) {
|
|
6857
|
+
process.stdout.write(
|
|
6858
|
+
`${JSON.stringify({ ok: false, error: { code: EXIT.ERROR, message }, data })}
|
|
6859
|
+
`
|
|
6860
|
+
);
|
|
6861
|
+
process.exitCode = EXIT.ERROR;
|
|
6862
|
+
return;
|
|
6863
|
+
}
|
|
6864
|
+
warn(
|
|
6865
|
+
`noir skills lint \u2014 ${errored.length} skill${errored.length === 1 ? "" : "s"} FAIL validation:`,
|
|
6866
|
+
opts
|
|
6867
|
+
);
|
|
6868
|
+
for (const s of errored) {
|
|
6869
|
+
for (const e of s.errors) warn(` ${s.name}: ${e}`, opts);
|
|
6870
|
+
}
|
|
6871
|
+
if (warned.length > 0) {
|
|
6872
|
+
info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
|
|
6873
|
+
for (const s of warned) {
|
|
6874
|
+
for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
|
|
6875
|
+
}
|
|
6876
|
+
}
|
|
6877
|
+
fail(EXIT.ERROR, message, opts);
|
|
6878
|
+
}
|
|
6879
|
+
if (opts.json === true) {
|
|
6880
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
6881
|
+
`);
|
|
6882
|
+
return;
|
|
6883
|
+
}
|
|
6884
|
+
log(`noir skills lint \u2014 ${skills.length} skills, all validate clean.`, opts);
|
|
6885
|
+
if (warned.length > 0) {
|
|
6886
|
+
info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
|
|
6887
|
+
for (const s of warned) {
|
|
6888
|
+
for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
|
|
6889
|
+
}
|
|
6890
|
+
}
|
|
6891
|
+
}
|
|
6892
|
+
async function skillsRegistry(opts) {
|
|
6893
|
+
let reg;
|
|
6894
|
+
try {
|
|
6895
|
+
reg = buildRegistry();
|
|
6896
|
+
} catch (err) {
|
|
6897
|
+
fail(
|
|
6898
|
+
EXIT.ERROR,
|
|
6899
|
+
`skills registry: could not build registry (${err instanceof Error ? err.message : String(err)})`,
|
|
6900
|
+
opts
|
|
6901
|
+
);
|
|
6902
|
+
}
|
|
6903
|
+
const data = { count: reg.length, skills: reg };
|
|
6904
|
+
if (opts.json === true) {
|
|
6905
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
6906
|
+
`);
|
|
6907
|
+
return;
|
|
6908
|
+
}
|
|
6909
|
+
log(`noir skills registry \u2014 ${reg.length} skills.`, opts);
|
|
6910
|
+
table(
|
|
6911
|
+
reg.map((r) => ({
|
|
6912
|
+
Skill: r.name,
|
|
6913
|
+
Kind: r.kind,
|
|
6914
|
+
Category: r.category,
|
|
6915
|
+
Version: r.version,
|
|
6916
|
+
Status: r.status,
|
|
6917
|
+
Refs: String(r.referenceCount),
|
|
6918
|
+
Lines: String(r.lines)
|
|
6919
|
+
})),
|
|
6920
|
+
["Skill", "Kind", "Category", "Version", "Status", "Refs", "Lines"],
|
|
6921
|
+
opts
|
|
6922
|
+
);
|
|
6923
|
+
}
|
|
6924
|
+
var CATEGORY;
|
|
6925
|
+
var init_skills = __esm({
|
|
6926
|
+
"src/commands/skills.ts"() {
|
|
6927
|
+
"use strict";
|
|
6928
|
+
init_init();
|
|
6929
|
+
init_output();
|
|
6930
|
+
CATEGORY = {
|
|
6931
|
+
"noir-brainstorming": "discovery",
|
|
6932
|
+
"noir-sync": "discovery",
|
|
6933
|
+
"noir-checkpoint": "discovery",
|
|
6934
|
+
"noir-exploring": "discovery",
|
|
6935
|
+
"noir-spec": "spec",
|
|
6936
|
+
"noir-planning": "plan",
|
|
6937
|
+
"noir-prd": "plan",
|
|
6938
|
+
"noir-executing-plans": "execute",
|
|
6939
|
+
"noir-test-driven-development": "execute",
|
|
6940
|
+
"noir-systematic-debugging": "execute",
|
|
6941
|
+
"noir-subagent": "execute",
|
|
6942
|
+
"noir-parallel": "execute",
|
|
6943
|
+
"noir-verifying": "verify",
|
|
6944
|
+
"noir-security": "verify",
|
|
6945
|
+
"noir-wrap": "document",
|
|
6946
|
+
"noir-readme": "document",
|
|
6947
|
+
"noir-shipping": "git",
|
|
6948
|
+
"noir-worktree": "git",
|
|
6949
|
+
"noir-recall": "memory",
|
|
6950
|
+
"noir-remember": "memory",
|
|
6951
|
+
"noir-context": "context",
|
|
6952
|
+
"noir-frontend": "domain",
|
|
6953
|
+
"noir-backend": "domain",
|
|
6954
|
+
"noir-doctor": "meta",
|
|
6955
|
+
"noir-writing-skills": "meta",
|
|
6956
|
+
"noir-rules": "meta"
|
|
6957
|
+
};
|
|
6958
|
+
}
|
|
6959
|
+
});
|
|
6960
|
+
|
|
6094
6961
|
// src/serve.ts
|
|
6095
6962
|
import { applyNoirEnv, loadProjectInfo as loadProjectInfo16 } from "@noir-ai/core";
|
|
6096
6963
|
import { ensureDaemonRunning as ensureDaemonRunning3, startStdioServer } from "@noir-ai/daemon";
|
|
@@ -6130,22 +6997,21 @@ var create_exports = {};
|
|
|
6130
6997
|
__export(create_exports, {
|
|
6131
6998
|
create: () => create
|
|
6132
6999
|
});
|
|
6133
|
-
import { resolve } from "path";
|
|
7000
|
+
import { resolve as resolve2 } from "path";
|
|
6134
7001
|
import { resolveAdapter as resolveAdapter6 } from "@noir-ai/adapters";
|
|
6135
7002
|
import { loadProjectInfo as loadProjectInfo17 } from "@noir-ai/core";
|
|
6136
7003
|
import { scaffold as scaffold2 } from "@noir-ai/create";
|
|
6137
7004
|
import { emitSkillsToDir as emitSkillsToDir3 } from "@noir-ai/skills";
|
|
6138
7005
|
async function create(dir, opts) {
|
|
6139
7006
|
assertTransportUrl(opts);
|
|
6140
|
-
const root =
|
|
6141
|
-
const host = opts.host ?? "claude";
|
|
7007
|
+
const root = resolve2(dir ?? process.cwd());
|
|
6142
7008
|
const interactive = resolveInteractive();
|
|
6143
7009
|
const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
|
|
6144
7010
|
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
6145
7011
|
const res = await scaffold2({
|
|
6146
7012
|
root,
|
|
6147
7013
|
mode: "create",
|
|
6148
|
-
host,
|
|
7014
|
+
...opts.host !== void 0 ? { host: opts.host } : {},
|
|
6149
7015
|
transport: opts.transport,
|
|
6150
7016
|
interactive,
|
|
6151
7017
|
...opts.url !== void 0 ? { url: opts.url } : {},
|
|
@@ -6158,6 +7024,7 @@ async function create(dir, opts) {
|
|
|
6158
7024
|
return res;
|
|
6159
7025
|
}
|
|
6160
7026
|
if (res.noop) return res;
|
|
7027
|
+
const host = res.host;
|
|
6161
7028
|
const adapter = resolveAdapter6(host);
|
|
6162
7029
|
const skillsDir = adapter.skillsDir?.({ root });
|
|
6163
7030
|
if (skillsDir === void 0) {
|
|
@@ -6180,6 +7047,9 @@ async function create(dir, opts) {
|
|
|
6180
7047
|
`Emitted ${summary.emitted.length} Noir skills to ${relDir}/ (target: ${target}).
|
|
6181
7048
|
`
|
|
6182
7049
|
);
|
|
7050
|
+
const stale = preservedStaleLine(summary.preserved ?? []);
|
|
7051
|
+
if (stale !== void 0) process.stderr.write(`${stale}
|
|
7052
|
+
`);
|
|
6183
7053
|
}
|
|
6184
7054
|
let projectInfo;
|
|
6185
7055
|
try {
|
|
@@ -6209,9 +7079,9 @@ var sync_exports = {};
|
|
|
6209
7079
|
__export(sync_exports, {
|
|
6210
7080
|
sync: () => sync
|
|
6211
7081
|
});
|
|
6212
|
-
import { existsSync as
|
|
7082
|
+
import { existsSync as existsSync5 } from "fs";
|
|
6213
7083
|
import { resolveAdapter as resolveAdapter7 } from "@noir-ai/adapters";
|
|
6214
|
-
import { loadProjectInfo as loadProjectInfo18, paths as
|
|
7084
|
+
import { loadProjectInfo as loadProjectInfo18, paths as paths4 } from "@noir-ai/core";
|
|
6215
7085
|
import { scaffold as scaffold3 } from "@noir-ai/create";
|
|
6216
7086
|
import { emitSkillsToDir as emitSkillsToDir4 } from "@noir-ai/skills";
|
|
6217
7087
|
async function sync(root, opts = {}) {
|
|
@@ -6271,6 +7141,9 @@ async function sync(root, opts = {}) {
|
|
|
6271
7141
|
`
|
|
6272
7142
|
);
|
|
6273
7143
|
}
|
|
7144
|
+
const stale = preservedStaleLine(summary.preserved ?? []);
|
|
7145
|
+
if (stale !== void 0) process.stderr.write(`${stale}
|
|
7146
|
+
`);
|
|
6274
7147
|
}
|
|
6275
7148
|
const dedup = await checkWritePathDedup(root, res, { interactive, project: projectInfo });
|
|
6276
7149
|
if (dedup.conflicts.length > 0) res.conflicts.push(...dedup.conflicts);
|
|
@@ -6278,7 +7151,7 @@ async function sync(root, opts = {}) {
|
|
|
6278
7151
|
}
|
|
6279
7152
|
function resolveSyncHost(root, opts) {
|
|
6280
7153
|
if (opts.host !== void 0) return opts.host;
|
|
6281
|
-
if (!
|
|
7154
|
+
if (!existsSync5(paths4.config(root))) return "claude";
|
|
6282
7155
|
try {
|
|
6283
7156
|
return loadProjectInfo18(root).config.host;
|
|
6284
7157
|
} catch {
|
|
@@ -6411,13 +7284,15 @@ function createProgram() {
|
|
|
6411
7284
|
"--upgrade",
|
|
6412
7285
|
"run scaffold migrations before re-emitting (re-run on an existing project)"
|
|
6413
7286
|
).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(
|
|
6414
|
-
//
|
|
6415
|
-
//
|
|
6416
|
-
//
|
|
6417
|
-
//
|
|
7287
|
+
// Target host. When the flag is absent the host is read from an existing
|
|
7288
|
+
// `.noir/config.yml`, falling back to `'claude'` (so a fresh project is
|
|
7289
|
+
// unaffected). The chosen host is forwarded to scaffold() + skills
|
|
7290
|
+
// emission via resolveAdapter. The choice list mirrors `SUPPORTED_HOSTS`
|
|
7291
|
+
// so a new host lands here automatically; commander rejects anything else
|
|
7292
|
+
// as usage=2.
|
|
6418
7293
|
new Option(
|
|
6419
7294
|
"--host <id>",
|
|
6420
|
-
"target agentic CLI (default:
|
|
7295
|
+
"target agentic CLI (default: the host in .noir/config.yml, else claude)"
|
|
6421
7296
|
).choices(SUPPORTED_HOSTS2)
|
|
6422
7297
|
).action(
|
|
6423
7298
|
async (opts, cmd) => {
|
|
@@ -6480,7 +7355,7 @@ function createProgram() {
|
|
|
6480
7355
|
"strip-replace managed regions (discard hand-edits inside <!-- noir:* --> markers)"
|
|
6481
7356
|
)
|
|
6482
7357
|
).addOption(
|
|
6483
|
-
//
|
|
7358
|
+
// Optional `--host` override. When omitted, sync reads host from
|
|
6484
7359
|
// `.noir/config.yml` (whatever `noir init --host <id>` persisted). The
|
|
6485
7360
|
// override is rarely needed — documented as advanced.
|
|
6486
7361
|
new Option(
|
|
@@ -7035,7 +7910,7 @@ var init_bin = __esm({
|
|
|
7035
7910
|
}
|
|
7036
7911
|
},
|
|
7037
7912
|
// The grouped home menu resolves its sections against the LIVE palette
|
|
7038
|
-
// registry
|
|
7913
|
+
// registry — the same source the TUI palette
|
|
7039
7914
|
// uses — so the menu cannot drift from the commander tree.
|
|
7040
7915
|
commands: buildPaletteCommands(createProgram())
|
|
7041
7916
|
};
|