@noir-ai/cli 1.11.2-beta.1 → 1.12.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/dist/bin.js +3295 -575
- package/dist/bin.js.map +1 -1
- package/dist/index.js +6136 -6
- package/dist/index.js.map +1 -1
- package/dist/{chunk-QIVHIKYD.js → tui/chunk-2RK6AFZQ.js} +37 -16
- package/dist/tui/chunk-2RK6AFZQ.js.map +1 -0
- package/dist/tui/{chunk-R5YX44UL.js → chunk-ESZTOQEC.js} +211 -2
- package/dist/tui/chunk-ESZTOQEC.js.map +1 -0
- package/dist/tui/chunk-J77RHAWG.js +5010 -0
- package/dist/tui/chunk-J77RHAWG.js.map +1 -0
- package/dist/{create-VK2QMXYS.js → tui/create-BTYLBVV6.js} +4 -4
- package/dist/tui/{create-WKFVM237.js.map → create-BTYLBVV6.js.map} +1 -1
- package/dist/tui/index.js +45 -3454
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/{install-JR6ZXNYA.js → install-YGTUSLRO.js} +2 -3
- package/dist/{sync-VXZKODQA.js → tui/sync-LRHPJ5VV.js} +4 -4
- package/dist/tui/{sync-DUWJAF7Z.js.map → sync-LRHPJ5VV.js.map} +1 -1
- package/dist/tui/{update-PGAM6OVL.js → update-LET5BWOU.js} +3 -4
- package/package.json +12 -12
- package/dist/chunk-A5W2E3X2.js +0 -33
- package/dist/chunk-A5W2E3X2.js.map +0 -1
- package/dist/chunk-CT6TYLHN.js +0 -1105
- package/dist/chunk-CT6TYLHN.js.map +0 -1
- package/dist/chunk-I2R55ACD.js +0 -369
- package/dist/chunk-I2R55ACD.js.map +0 -1
- package/dist/chunk-QIVHIKYD.js.map +0 -1
- package/dist/chunk-RON2XESF.js +0 -210
- package/dist/chunk-RON2XESF.js.map +0 -1
- package/dist/create-VK2QMXYS.js.map +0 -1
- package/dist/install-4U4UDDNE.js +0 -13
- package/dist/sync-VXZKODQA.js.map +0 -1
- package/dist/tui/chunk-F6XRS72N.js +0 -1106
- package/dist/tui/chunk-F6XRS72N.js.map +0 -1
- package/dist/tui/chunk-NLK6UNIA.js +0 -223
- package/dist/tui/chunk-NLK6UNIA.js.map +0 -1
- package/dist/tui/chunk-QF6777NJ.js +0 -209
- package/dist/tui/chunk-QF6777NJ.js.map +0 -1
- package/dist/tui/chunk-R5YX44UL.js.map +0 -1
- package/dist/tui/create-WKFVM237.js +0 -79
- package/dist/tui/sync-DUWJAF7Z.js +0 -90
- package/dist/tui/update-PGAM6OVL.js.map +0 -1
- package/dist/update-ZDAVLOXB.js +0 -20
- package/dist/update-ZDAVLOXB.js.map +0 -1
- /package/dist/{install-4U4UDDNE.js.map → tui/install-YGTUSLRO.js.map} +0 -0
- /package/dist/tui/{install-JR6ZXNYA.js.map → update-LET5BWOU.js.map} +0 -0
package/dist/tui/index.js
CHANGED
|
@@ -1,3446 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
buildPaletteCommandsForTui,
|
|
3
|
+
describeContext,
|
|
4
|
+
describeDaemon,
|
|
5
|
+
describeMemory,
|
|
6
|
+
describeStore,
|
|
7
|
+
describeWorkflow,
|
|
8
|
+
gatherStatusPayload,
|
|
9
|
+
isDestructive,
|
|
10
|
+
resolveSections
|
|
11
|
+
} from "./chunk-J77RHAWG.js";
|
|
12
|
+
import "./chunk-2RK6AFZQ.js";
|
|
7
13
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
NoirCliError,
|
|
14
|
-
accessibleMode,
|
|
15
|
-
badge,
|
|
16
|
-
c,
|
|
17
|
-
contentWidth,
|
|
18
|
-
definitionList,
|
|
19
|
-
divider,
|
|
20
|
-
fail,
|
|
21
|
-
handleError,
|
|
22
|
-
info,
|
|
23
|
-
isInteractive,
|
|
24
|
-
json,
|
|
25
|
-
log,
|
|
26
|
-
requireInteractive,
|
|
27
|
-
spinner,
|
|
28
|
-
success,
|
|
29
|
-
table,
|
|
30
|
-
tip,
|
|
31
|
-
useColor,
|
|
32
|
-
warn
|
|
33
|
-
} from "./chunk-R5YX44UL.js";
|
|
34
|
-
|
|
35
|
-
// src/tui/index.tsx
|
|
36
|
-
import { loadProjectInfo as loadProjectInfo9 } from "@noir-ai/core";
|
|
37
|
-
import { render } from "ink";
|
|
38
|
-
|
|
39
|
-
// src/bin.ts
|
|
40
|
-
import { realpathSync } from "fs";
|
|
41
|
-
import { pathToFileURL } from "url";
|
|
42
|
-
import { SUPPORTED_HOSTS as SUPPORTED_HOSTS2 } from "@noir-ai/adapters";
|
|
43
|
-
import { NOIR_VERSION as NOIR_VERSION4 } from "@noir-ai/core";
|
|
44
|
-
import { Command, Option } from "commander";
|
|
45
|
-
|
|
46
|
-
// src/daemon-client.ts
|
|
47
|
-
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
48
|
-
import { ContextEngine, createEmbedFn, resolveEmbedderConfig } from "@noir-ai/context";
|
|
49
|
-
import { loadProjectInfo, NOIR_VERSION } from "@noir-ai/core";
|
|
50
|
-
import {
|
|
51
|
-
ensureDaemonRunning,
|
|
52
|
-
pidAlive,
|
|
53
|
-
readDaemonRecord,
|
|
54
|
-
resolveGateConfig
|
|
55
|
-
} from "@noir-ai/daemon";
|
|
56
|
-
import { createMemoryEngine } from "@noir-ai/memory";
|
|
57
|
-
import { openStore } from "@noir-ai/store";
|
|
58
|
-
import { WorkflowEngine } from "@noir-ai/workflow";
|
|
59
|
-
var DAEMON_DOWN_HINT = "daemon not reachable \u2014 try `noir daemon start`";
|
|
60
|
-
var PROBE_TIMEOUT_MS = 1500;
|
|
61
|
-
function describeCause(cause) {
|
|
62
|
-
if (cause instanceof Error) return `${cause.name}: ${cause.message}`;
|
|
63
|
-
try {
|
|
64
|
-
return JSON.stringify(cause);
|
|
65
|
-
} catch {
|
|
66
|
-
return String(cause);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function failDaemonDown(opts, cause) {
|
|
70
|
-
if (opts.verbose) {
|
|
71
|
-
process.stderr.write(`noir: daemon transport detail: ${describeCause(cause)}
|
|
72
|
-
`);
|
|
73
|
-
}
|
|
74
|
-
fail(EXIT.DAEMON_DOWN, DAEMON_DOWN_HINT, opts);
|
|
75
|
-
}
|
|
76
|
-
async function probeDaemon(opts = {}) {
|
|
77
|
-
const rec = readDaemonRecord();
|
|
78
|
-
if (!rec) {
|
|
79
|
-
if (opts.verbose) process.stderr.write("noir: daemon probe: no daemon record\n");
|
|
80
|
-
return { running: false };
|
|
81
|
-
}
|
|
82
|
-
if (!pidAlive(rec.pid)) {
|
|
83
|
-
if (opts.verbose) process.stderr.write(`noir: daemon probe: pid ${rec.pid} not alive
|
|
84
|
-
`);
|
|
85
|
-
return { running: false };
|
|
86
|
-
}
|
|
87
|
-
try {
|
|
88
|
-
const res = await fetch(`http://127.0.0.1:${rec.port}/health`, {
|
|
89
|
-
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
90
|
-
});
|
|
91
|
-
if (!res.ok) {
|
|
92
|
-
if (opts.verbose) process.stderr.write(`noir: daemon probe: /health \u2192 HTTP ${res.status}
|
|
93
|
-
`);
|
|
94
|
-
return { running: false };
|
|
95
|
-
}
|
|
96
|
-
const body = await res.json().catch(() => null);
|
|
97
|
-
if (body?.ok !== true) {
|
|
98
|
-
if (opts.verbose) process.stderr.write("noir: daemon probe: /health body not ok\n");
|
|
99
|
-
return { running: false };
|
|
100
|
-
}
|
|
101
|
-
return {
|
|
102
|
-
running: true,
|
|
103
|
-
pid: typeof body.pid === "number" ? body.pid : rec.pid,
|
|
104
|
-
port: rec.port,
|
|
105
|
-
uptimeSec: typeof body.uptimeSec === "number" ? body.uptimeSec : void 0
|
|
106
|
-
};
|
|
107
|
-
} catch (err) {
|
|
108
|
-
if (opts.verbose)
|
|
109
|
-
process.stderr.write(`noir: daemon probe: unreachable (${describeCause(err)})
|
|
110
|
-
`);
|
|
111
|
-
return { running: false };
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
async function resolveDaemon(opts) {
|
|
115
|
-
const project = opts.project ?? loadProjectInfo(process.cwd());
|
|
116
|
-
const idleTimeoutSec = opts.idleTimeoutSec ?? project.config.daemon.idleTimeoutSec;
|
|
117
|
-
try {
|
|
118
|
-
const ensured = await ensureDaemonRunning({ project, idleTimeoutSec });
|
|
119
|
-
return { url: ensured.url, stop: ensured.stop };
|
|
120
|
-
} catch (err) {
|
|
121
|
-
failDaemonDown(opts, err);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
async function connectClient(client, url, opts) {
|
|
125
|
-
try {
|
|
126
|
-
await client.connect(new StreamableHTTPClientTransport(new URL(url)));
|
|
127
|
-
} catch (err) {
|
|
128
|
-
failDaemonDown(opts, err);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
async function callToolParse(client, opts, name, args) {
|
|
132
|
-
const result = await client.callTool({ name, arguments: args }).catch((err) => failDaemonDown(opts, err));
|
|
133
|
-
const block = result.content?.[0];
|
|
134
|
-
if (block?.type !== "text" || typeof block.text !== "string") {
|
|
135
|
-
failDaemonDown(opts, new Error(`tool '${name}' returned no text content`));
|
|
136
|
-
}
|
|
137
|
-
const text = block.text;
|
|
138
|
-
try {
|
|
139
|
-
return JSON.parse(text);
|
|
140
|
-
} catch (err) {
|
|
141
|
-
failDaemonDown(opts, err);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
function buildCaller(client, opts) {
|
|
145
|
-
return {
|
|
146
|
-
callTool: (name, args = {}) => callToolParse(client, opts, name, args),
|
|
147
|
-
listTools: async () => {
|
|
148
|
-
const res = await client.listTools().catch((err) => failDaemonDown(opts, err));
|
|
149
|
-
const tools = res?.tools;
|
|
150
|
-
if (!Array.isArray(tools)) return [];
|
|
151
|
-
const names = [];
|
|
152
|
-
for (const t of tools) {
|
|
153
|
-
const name = t?.name;
|
|
154
|
-
if (typeof name === "string") names.push(name);
|
|
155
|
-
}
|
|
156
|
-
return names;
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
async function withDaemon(opts, fn) {
|
|
161
|
-
const { url, stop } = await resolveDaemon(opts);
|
|
162
|
-
let client;
|
|
163
|
-
try {
|
|
164
|
-
client = new Client(
|
|
165
|
-
{ name: "noir-cli", version: NOIR_VERSION },
|
|
166
|
-
{ versionNegotiation: { mode: "auto" } }
|
|
167
|
-
);
|
|
168
|
-
const connected = client;
|
|
169
|
-
await connectClient(connected, url, opts);
|
|
170
|
-
return await fn(buildCaller(connected, opts));
|
|
171
|
-
} finally {
|
|
172
|
-
if (client) {
|
|
173
|
-
await client.close().catch(() => {
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
await stop().catch(() => {
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
async function withRunningDaemon(opts, fn, probe) {
|
|
181
|
-
const p = probe ?? await probeDaemon(opts);
|
|
182
|
-
if (!p.running) return null;
|
|
183
|
-
const url = `http://127.0.0.1:${p.port}/mcp`;
|
|
184
|
-
let client;
|
|
185
|
-
try {
|
|
186
|
-
client = new Client(
|
|
187
|
-
{ name: "noir-cli", version: NOIR_VERSION },
|
|
188
|
-
{ versionNegotiation: { mode: "auto" } }
|
|
189
|
-
);
|
|
190
|
-
const connected = client;
|
|
191
|
-
await connected.connect(new StreamableHTTPClientTransport(new URL(url)));
|
|
192
|
-
return await fn(buildCaller(connected, opts));
|
|
193
|
-
} catch {
|
|
194
|
-
return null;
|
|
195
|
-
} finally {
|
|
196
|
-
if (client) {
|
|
197
|
-
await client.close().catch(() => {
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
async function callDaemonTool(opts, name, args = {}) {
|
|
203
|
-
return withDaemon(opts, (caller) => caller.callTool(name, args));
|
|
204
|
-
}
|
|
205
|
-
async function withInProcessRead(opts, fn) {
|
|
206
|
-
const project = opts.project ?? loadProjectInfo(process.cwd());
|
|
207
|
-
const store = await openStore({ projectId: project.id, root: project.root, readonly: true });
|
|
208
|
-
try {
|
|
209
|
-
const embedderCfg = resolveEmbedderConfig(project.config.context);
|
|
210
|
-
const embed = createEmbedFn(embedderCfg).embed;
|
|
211
|
-
const context = new ContextEngine({
|
|
212
|
-
store,
|
|
213
|
-
root: project.root,
|
|
214
|
-
projectId: project.id,
|
|
215
|
-
embedderCfg,
|
|
216
|
-
storeDegraded: true
|
|
217
|
-
});
|
|
218
|
-
const memory = createMemoryEngine({
|
|
219
|
-
store,
|
|
220
|
-
root: project.root,
|
|
221
|
-
projectId: project.id,
|
|
222
|
-
embed,
|
|
223
|
-
storeDegraded: true
|
|
224
|
-
});
|
|
225
|
-
const workflow = new WorkflowEngine(
|
|
226
|
-
store,
|
|
227
|
-
project.root,
|
|
228
|
-
project.id,
|
|
229
|
-
resolveGateConfig(project.config)
|
|
230
|
-
);
|
|
231
|
-
return await fn({ context, memory, workflow });
|
|
232
|
-
} finally {
|
|
233
|
-
await store.close().catch(() => {
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// src/commands/context.ts
|
|
239
|
-
function parseLimit(raw, label, opts) {
|
|
240
|
-
if (raw === void 0) return void 0;
|
|
241
|
-
const n = Number(raw);
|
|
242
|
-
if (!Number.isInteger(n) || n <= 0) {
|
|
243
|
-
fail(EXIT.USAGE, `${label}: --limit must be a positive integer (got '${raw}')`, opts);
|
|
244
|
-
}
|
|
245
|
-
return n;
|
|
246
|
-
}
|
|
247
|
-
function failTool(label, envelope, opts) {
|
|
248
|
-
const detail = envelope.error ?? envelope.reason ?? "unknown failure";
|
|
249
|
-
fail(EXIT.ERROR, `${label}: ${detail}`, opts);
|
|
250
|
-
}
|
|
251
|
-
function toHit(raw) {
|
|
252
|
-
const h = raw ?? {};
|
|
253
|
-
return {
|
|
254
|
-
path: typeof h.path === "string" ? h.path : "<unknown>",
|
|
255
|
-
score: typeof h.score === "number" ? h.score : 0,
|
|
256
|
-
snippet: typeof h.snippet === "string" ? h.snippet : "",
|
|
257
|
-
source: typeof h.source === "string" ? h.source : ""
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
async function contextSearch(opts) {
|
|
261
|
-
const limit = parseLimit(opts.limit, "context search", opts);
|
|
262
|
-
let probe;
|
|
263
|
-
try {
|
|
264
|
-
probe = await probeDaemon(opts);
|
|
265
|
-
} catch {
|
|
266
|
-
probe = { running: true };
|
|
267
|
-
}
|
|
268
|
-
if (!probe.running) {
|
|
269
|
-
await withInProcessRead(opts, async (engines) => {
|
|
270
|
-
const result = await engines.context.search(opts.query, {
|
|
271
|
-
...limit === void 0 ? {} : { limit }
|
|
272
|
-
});
|
|
273
|
-
const data2 = {
|
|
274
|
-
query: opts.query,
|
|
275
|
-
hits: result.results.map((h) => ({
|
|
276
|
-
path: h.path,
|
|
277
|
-
score: h.score,
|
|
278
|
-
snippet: h.snippet,
|
|
279
|
-
source: h.source
|
|
280
|
-
})),
|
|
281
|
-
consumedTokens: result.consumedTokens,
|
|
282
|
-
truncated: result.truncated,
|
|
283
|
-
degraded: result.degraded,
|
|
284
|
-
mode: result.mode
|
|
285
|
-
};
|
|
286
|
-
if (opts.json === true) {
|
|
287
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
|
|
288
|
-
`);
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
renderSearch(data2, opts);
|
|
292
|
-
});
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
const res = await callDaemonTool(opts, "context_search", {
|
|
296
|
-
query: opts.query,
|
|
297
|
-
...limit === void 0 ? {} : { limit }
|
|
298
|
-
});
|
|
299
|
-
if (res.ok !== true) failTool("context search", res, opts);
|
|
300
|
-
const data = {
|
|
301
|
-
query: opts.query,
|
|
302
|
-
hits: Array.isArray(res.results) ? res.results.map(toHit) : [],
|
|
303
|
-
consumedTokens: typeof res.consumedTokens === "number" ? res.consumedTokens : 0,
|
|
304
|
-
truncated: res.truncated === true,
|
|
305
|
-
degraded: res.degraded === true,
|
|
306
|
-
mode: typeof res.mode === "string" ? res.mode : "unknown"
|
|
307
|
-
};
|
|
308
|
-
if (opts.json === true) {
|
|
309
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
310
|
-
`);
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
renderSearch(data, opts);
|
|
314
|
-
}
|
|
315
|
-
function renderSearch(data, opts) {
|
|
316
|
-
const flag = data.degraded ? ` ${badge("warn", "degraded: BM25-only")}` : "";
|
|
317
|
-
const trunc = data.truncated ? " (budget hit \u2014 results truncated)" : "";
|
|
318
|
-
log(
|
|
319
|
-
`context search \u2014 ${data.hits.length} hit${data.hits.length === 1 ? "" : "s"} \xB7 ${data.mode} \xB7 ${data.consumedTokens} tokens${flag}${trunc}`,
|
|
320
|
-
opts
|
|
321
|
-
);
|
|
322
|
-
table(
|
|
323
|
-
data.hits.map((h, i) => ({
|
|
324
|
-
"#": i + 1,
|
|
325
|
-
Path: h.path,
|
|
326
|
-
Score: h.score.toFixed(4),
|
|
327
|
-
Snippet: h.snippet
|
|
328
|
-
})),
|
|
329
|
-
["#", "Path", "Score", "Snippet"],
|
|
330
|
-
opts
|
|
331
|
-
);
|
|
332
|
-
}
|
|
333
|
-
async function contextIndex(opts) {
|
|
334
|
-
const args = opts.paths && opts.paths.length > 0 ? { paths: opts.paths } : {};
|
|
335
|
-
if (opts.force === true) args.force = true;
|
|
336
|
-
const res = await callDaemonTool(opts, "context_index", args);
|
|
337
|
-
if (res.ok !== true) failTool("context index", res, opts);
|
|
338
|
-
const data = {
|
|
339
|
-
indexed: typeof res.indexed === "number" ? res.indexed : 0,
|
|
340
|
-
skipped: typeof res.skipped === "number" ? res.skipped : 0,
|
|
341
|
-
deleted: typeof res.deleted === "number" ? res.deleted : 0,
|
|
342
|
-
failed: typeof res.failed === "number" ? res.failed : 0,
|
|
343
|
-
totalChunks: typeof res.totalChunks === "number" ? res.totalChunks : 0,
|
|
344
|
-
degraded: res.degraded === true
|
|
345
|
-
};
|
|
346
|
-
if (opts.json === true) {
|
|
347
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
348
|
-
`);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
renderIndex(data, opts.paths, opts);
|
|
352
|
-
}
|
|
353
|
-
function renderIndex(data, paths, opts) {
|
|
354
|
-
const scope = paths && paths.length > 0 ? paths.join(", ") : ".";
|
|
355
|
-
const flag = data.degraded ? ` ${badge("warn", "degraded: vectors skipped")}` : "";
|
|
356
|
-
log(`context index \u2014 ${scope}${flag}`, opts);
|
|
357
|
-
definitionList(
|
|
358
|
-
[
|
|
359
|
-
{ label: "Indexed (new)", value: data.indexed },
|
|
360
|
-
{ label: "Skipped (unchanged)", value: data.skipped },
|
|
361
|
-
{ label: "Deleted (removed)", value: data.deleted },
|
|
362
|
-
{ label: "Failed", value: data.failed },
|
|
363
|
-
{ label: "Total chunks", value: data.totalChunks }
|
|
364
|
-
],
|
|
365
|
-
opts
|
|
366
|
-
);
|
|
367
|
-
if (data.failed > 0) {
|
|
368
|
-
info(`${data.failed} file(s) could not be indexed (binary / IO / encoding).`, opts);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
async function contextStatus(opts) {
|
|
372
|
-
const res = await callDaemonTool(opts, "context_status");
|
|
373
|
-
if (res.ok !== true) failTool("context status", res, opts);
|
|
374
|
-
const data = res;
|
|
375
|
-
if (opts.json === true) {
|
|
376
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
377
|
-
`);
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
renderStatus(data, opts);
|
|
381
|
-
}
|
|
382
|
-
function describeEmbedder(e) {
|
|
383
|
-
if (!e || e.kind === "none") return "none (BM25-only)";
|
|
384
|
-
const model = typeof e.model === "string" && e.model.length > 0 ? e.model : "<unset>";
|
|
385
|
-
return `${e.kind} \xB7 ${model} (${e.dim}-dim)`;
|
|
386
|
-
}
|
|
387
|
-
function renderStatus(data, opts) {
|
|
388
|
-
const flag = data.degraded ? ` ${badge("warn", "degraded")}` : "";
|
|
389
|
-
log(`context status \u2014 ${data.projectId}${flag}`, opts);
|
|
390
|
-
definitionList(
|
|
391
|
-
[
|
|
392
|
-
{ label: "Project", value: data.projectId },
|
|
393
|
-
{ label: "Docs", value: data.docCount },
|
|
394
|
-
{ label: "Vectors", value: data.vecCount },
|
|
395
|
-
{ label: "Indexed files", value: data.indexedFiles },
|
|
396
|
-
{ label: "Embedder", value: describeEmbedder(data.embedder) },
|
|
397
|
-
{ label: "Degraded", value: data.degraded ? "yes" : "no" }
|
|
398
|
-
],
|
|
399
|
-
opts
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// src/commands/daemon.ts
|
|
404
|
-
import { loadProjectInfo as loadProjectInfo2 } from "@noir-ai/core";
|
|
405
|
-
import {
|
|
406
|
-
clearDaemonRecord,
|
|
407
|
-
ensureDaemonRunning as ensureDaemonRunning2,
|
|
408
|
-
pidAlive as pidAlive2,
|
|
409
|
-
readDaemonRecord as readDaemonRecord2,
|
|
410
|
-
spawnDetachedDaemon
|
|
411
|
-
} from "@noir-ai/daemon";
|
|
412
|
-
var MODE = "foreground";
|
|
413
|
-
function formatUptime(sec) {
|
|
414
|
-
if (!Number.isFinite(sec) || sec < 0) return "unknown";
|
|
415
|
-
if (sec < 60) return `${sec}s`;
|
|
416
|
-
if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
417
|
-
return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
|
|
418
|
-
}
|
|
419
|
-
async function isHealthy(port) {
|
|
420
|
-
try {
|
|
421
|
-
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
|
422
|
-
return res.status === 200;
|
|
423
|
-
} catch {
|
|
424
|
-
return false;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
async function daemonStart(opts) {
|
|
428
|
-
const project = loadProjectInfo2(process.cwd());
|
|
429
|
-
if (opts.detachChild === true) {
|
|
430
|
-
const child = await ensureDaemonRunning2({
|
|
431
|
-
project,
|
|
432
|
-
idleTimeoutSec: project.config.daemon.idleTimeoutSec
|
|
433
|
-
});
|
|
434
|
-
if (child.started) {
|
|
435
|
-
if (opts.json === true) {
|
|
436
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: { mode: "detached" } })}
|
|
437
|
-
`);
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
info("noir daemon: detached foreground. Ctrl+C to stop.", opts);
|
|
441
|
-
log(`Noir daemon listening at ${child.url}`, opts);
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
if (opts.json === true) {
|
|
445
|
-
process.stdout.write(
|
|
446
|
-
`${JSON.stringify({ ok: true, data: { mode: "detached", reused: true } })}
|
|
447
|
-
`
|
|
448
|
-
);
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
info("Noir daemon already running; detached child exiting.", opts);
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
if (opts.detach === true) {
|
|
455
|
-
const existing = readDaemonRecord2();
|
|
456
|
-
if (existing && pidAlive2(existing.pid) && await isHealthy(existing.port)) {
|
|
457
|
-
if (opts.json === true) {
|
|
458
|
-
process.stdout.write(
|
|
459
|
-
`${JSON.stringify({ ok: true, data: { mode: "detached", reused: true } })}
|
|
460
|
-
`
|
|
461
|
-
);
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
info(
|
|
465
|
-
`Noir daemon already running at http://127.0.0.1:${existing.port}/mcp (detach not needed).`,
|
|
466
|
-
opts
|
|
467
|
-
);
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
const spawned = await spawnDetachedDaemon({ project });
|
|
471
|
-
if (opts.json === true) {
|
|
472
|
-
process.stdout.write(
|
|
473
|
-
`${JSON.stringify({ ok: true, data: { mode: "detached", pid: spawned.pid, port: spawned.port } })}
|
|
474
|
-
`
|
|
475
|
-
);
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
info(`Noir daemon started in the background (pid ${spawned.pid}, port ${spawned.port}).`, opts);
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
const ds = spinner("Starting Noir daemon...", opts).start();
|
|
482
|
-
const ensured = await ensureDaemonRunning2({
|
|
483
|
-
project,
|
|
484
|
-
idleTimeoutSec: project.config.daemon.idleTimeoutSec
|
|
485
|
-
});
|
|
486
|
-
if (ensured.started) {
|
|
487
|
-
ds.succeed("Daemon started");
|
|
488
|
-
if (opts.json === true) {
|
|
489
|
-
process.stdout.write(
|
|
490
|
-
`${JSON.stringify({
|
|
491
|
-
ok: true,
|
|
492
|
-
data: {
|
|
493
|
-
url: ensured.url,
|
|
494
|
-
port: ensured.port,
|
|
495
|
-
pid: process.pid,
|
|
496
|
-
mode: MODE,
|
|
497
|
-
reused: false
|
|
498
|
-
}
|
|
499
|
-
})}
|
|
500
|
-
`
|
|
501
|
-
);
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
info("noir daemon: foreground mode. Ctrl+C to stop.", opts);
|
|
505
|
-
log(`Noir daemon listening at ${ensured.url}`, opts);
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
ds.succeed("Daemon already running");
|
|
509
|
-
if (opts.json === true) {
|
|
510
|
-
process.stdout.write(
|
|
511
|
-
`${JSON.stringify({ ok: true, data: { url: ensured.url, port: ensured.port, reused: true } })}
|
|
512
|
-
`
|
|
513
|
-
);
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
log(`Noir daemon already running at ${ensured.url}`, opts);
|
|
517
|
-
}
|
|
518
|
-
async function daemonStop(opts) {
|
|
519
|
-
const rec = readDaemonRecord2();
|
|
520
|
-
if (!rec) {
|
|
521
|
-
if (opts.json === true) {
|
|
522
|
-
process.stdout.write(
|
|
523
|
-
`${JSON.stringify({ ok: true, data: { running: false, stopped: false } })}
|
|
524
|
-
`
|
|
525
|
-
);
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
info("No Noir daemon is running.", opts);
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
const ds = spinner(`Stopping daemon (pid ${rec.pid})...`, opts).start();
|
|
532
|
-
let signalled = false;
|
|
533
|
-
let errMsg;
|
|
534
|
-
try {
|
|
535
|
-
process.kill(rec.pid, "SIGTERM");
|
|
536
|
-
signalled = true;
|
|
537
|
-
} catch (err) {
|
|
538
|
-
errMsg = err instanceof Error ? err.message : String(err);
|
|
539
|
-
} finally {
|
|
540
|
-
clearDaemonRecord();
|
|
541
|
-
}
|
|
542
|
-
if (opts.json === true) {
|
|
543
|
-
process.stdout.write(
|
|
544
|
-
`${JSON.stringify({
|
|
545
|
-
ok: true,
|
|
546
|
-
data: {
|
|
547
|
-
running: signalled,
|
|
548
|
-
stopped: signalled,
|
|
549
|
-
pid: rec.pid,
|
|
550
|
-
...errMsg !== void 0 ? { error: errMsg } : {}
|
|
551
|
-
}
|
|
552
|
-
})}
|
|
553
|
-
`
|
|
554
|
-
);
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
if (signalled) {
|
|
558
|
-
ds.succeed(`Stopped Noir daemon (pid ${rec.pid})`);
|
|
559
|
-
} else {
|
|
560
|
-
ds.warn(`Daemon (pid ${rec.pid}) could not be signalled: ${errMsg}`);
|
|
561
|
-
info(`Noir daemon (pid ${rec.pid}) could not be signalled: ${errMsg}`, opts);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
async function daemonStatus(opts) {
|
|
565
|
-
const rec = readDaemonRecord2();
|
|
566
|
-
if (!rec) {
|
|
567
|
-
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (start with `noir daemon start`).", opts);
|
|
568
|
-
}
|
|
569
|
-
if (!pidAlive2(rec.pid)) {
|
|
570
|
-
clearDaemonRecord();
|
|
571
|
-
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
|
|
572
|
-
}
|
|
573
|
-
const hs = spinner("Probing daemon health...", opts).start();
|
|
574
|
-
let health = null;
|
|
575
|
-
try {
|
|
576
|
-
const res = await fetch(`http://127.0.0.1:${rec.port}/health`);
|
|
577
|
-
if (res.status === 200) {
|
|
578
|
-
health = await res.json();
|
|
579
|
-
}
|
|
580
|
-
} catch {
|
|
581
|
-
health = null;
|
|
582
|
-
}
|
|
583
|
-
if (health?.ok !== true) {
|
|
584
|
-
hs.fail("Daemon not responding");
|
|
585
|
-
clearDaemonRecord();
|
|
586
|
-
fail(
|
|
587
|
-
EXIT.DAEMON_DOWN,
|
|
588
|
-
"Noir daemon is not running (port unresponsive; stale record removed).",
|
|
589
|
-
opts
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
hs.succeed("Daemon healthy");
|
|
593
|
-
const uptimeSec = typeof health?.uptimeSec === "number" ? health.uptimeSec : Math.max(0, Math.floor((Date.now() - rec.startedAt) / 1e3));
|
|
594
|
-
const data = {
|
|
595
|
-
running: true,
|
|
596
|
-
pid: rec.pid,
|
|
597
|
-
port: rec.port,
|
|
598
|
-
startedAt: rec.startedAt,
|
|
599
|
-
uptimeSec,
|
|
600
|
-
// Honest ownership: the detached child writes `mode:'detached'`; anything
|
|
601
|
-
// else (legacy record, foreground start) reports 'foreground'.
|
|
602
|
-
mode: rec.mode ?? MODE
|
|
603
|
-
};
|
|
604
|
-
if (opts.json === true) {
|
|
605
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
606
|
-
`);
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
log(
|
|
610
|
-
`Noir daemon: running (pid ${data.pid}, port ${data.port}, up ${formatUptime(uptimeSec)}, ${data.mode})`,
|
|
611
|
-
opts
|
|
612
|
-
);
|
|
613
|
-
}
|
|
614
|
-
async function daemonRestart(opts) {
|
|
615
|
-
await daemonStop({ ...opts, json: false, quiet: true });
|
|
616
|
-
await daemonStart(opts);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// src/commands/handoff.ts
|
|
620
|
-
import { mkdirSync, writeFileSync } from "fs";
|
|
621
|
-
import { join } from "path";
|
|
622
|
-
import {
|
|
623
|
-
defaultHandoffBlock,
|
|
624
|
-
hostLaunchDirective,
|
|
625
|
-
resolveAdapter
|
|
626
|
-
} from "@noir-ai/adapters";
|
|
627
|
-
import { loadProjectInfo as loadProjectInfo5, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
|
|
628
|
-
|
|
629
|
-
// src/commands/status.ts
|
|
630
|
-
import { loadProjectInfo as loadProjectInfo3, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
|
|
631
|
-
async function tryTool(caller, name, args) {
|
|
632
|
-
try {
|
|
633
|
-
return await caller.callTool(name, args);
|
|
634
|
-
} catch {
|
|
635
|
-
return null;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
function pickStore(s) {
|
|
639
|
-
if (s?.ok !== true) return null;
|
|
640
|
-
return {
|
|
641
|
-
docCount: s.docCount,
|
|
642
|
-
vecCount: s.vecCount,
|
|
643
|
-
dbPath: s.dbPath,
|
|
644
|
-
degraded: s.degraded === true
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
function describeEmbedder2(e) {
|
|
648
|
-
if (!e || e.kind === "none") return "none (BM25-only)";
|
|
649
|
-
const model = typeof e.model === "string" && e.model.length > 0 ? e.model : "<unset>";
|
|
650
|
-
return `${e.kind} \xB7 ${model} (${e.dim}-dim)`;
|
|
651
|
-
}
|
|
652
|
-
function pickContext(c2) {
|
|
653
|
-
if (c2?.ok !== true) return null;
|
|
654
|
-
return {
|
|
655
|
-
docCount: c2.docCount,
|
|
656
|
-
vecCount: c2.vecCount,
|
|
657
|
-
indexedFiles: c2.indexedFiles,
|
|
658
|
-
embedder: describeEmbedder2(c2.embedder),
|
|
659
|
-
degraded: c2.degraded === true
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
function pickWorkflow(w) {
|
|
663
|
-
if (w?.ok !== true) return null;
|
|
664
|
-
return {
|
|
665
|
-
taskId: w.taskId,
|
|
666
|
-
phase: w.phase,
|
|
667
|
-
state: w.state,
|
|
668
|
-
mode: w.mode,
|
|
669
|
-
nextGate: w.nextGate,
|
|
670
|
-
degraded: w.degraded === true
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
function pickMemory(m) {
|
|
674
|
-
if (m?.ok !== true || !Array.isArray(m.sessions)) return null;
|
|
675
|
-
let observations = 0;
|
|
676
|
-
for (const s of m.sessions) observations += s.count;
|
|
677
|
-
return { sessions: m.sessions.length, observations };
|
|
678
|
-
}
|
|
679
|
-
function buildPayload(project, probe, host, store, context, workflow, memory) {
|
|
680
|
-
return {
|
|
681
|
-
noir: NOIR_VERSION2,
|
|
682
|
-
project: { id: project.id, name: project.name },
|
|
683
|
-
host: project.config.host,
|
|
684
|
-
daemon: {
|
|
685
|
-
running: probe.running,
|
|
686
|
-
...probe.pid !== void 0 ? { pid: probe.pid } : {},
|
|
687
|
-
...probe.uptimeSec !== void 0 ? { uptimeSec: probe.uptimeSec } : {},
|
|
688
|
-
...host && typeof host.transport === "string" ? { transport: host.transport } : {}
|
|
689
|
-
},
|
|
690
|
-
store: pickStore(store),
|
|
691
|
-
context: pickContext(context),
|
|
692
|
-
workflow: pickWorkflow(workflow),
|
|
693
|
-
memory: pickMemory(memory)
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
function formatDuration(sec) {
|
|
697
|
-
if (typeof sec !== "number" || sec < 0) return "unknown";
|
|
698
|
-
if (sec < 60) return `${sec}s`;
|
|
699
|
-
if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
700
|
-
return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
|
|
701
|
-
}
|
|
702
|
-
function describeDaemon(d) {
|
|
703
|
-
if (!d.running) return "not running (start with `noir daemon start`)";
|
|
704
|
-
const pid = typeof d.pid === "number" ? `pid ${d.pid}` : "pid unknown";
|
|
705
|
-
const up = typeof d.uptimeSec === "number" ? `, up ${formatDuration(d.uptimeSec)}` : "";
|
|
706
|
-
const transport = typeof d.transport === "string" ? `, ${d.transport}` : "";
|
|
707
|
-
return `running (${pid}${up}${transport})`;
|
|
708
|
-
}
|
|
709
|
-
function describeStore(s) {
|
|
710
|
-
if (!s) return "unavailable (store engine not wired)";
|
|
711
|
-
const flag = s.degraded ? ` ${badge("warn", "degraded: read-only")}` : "";
|
|
712
|
-
return `${s.docCount} docs / ${s.vecCount} vecs${flag}`;
|
|
713
|
-
}
|
|
714
|
-
function describeContext(c2) {
|
|
715
|
-
if (!c2) return "unavailable (context engine not wired)";
|
|
716
|
-
const flag = c2.degraded ? ` ${badge("warn", "degraded")}` : "";
|
|
717
|
-
return `${c2.docCount} docs, ${c2.vecCount} vecs, ${c2.indexedFiles} files \xB7 embedder ${c2.embedder}${flag}`;
|
|
718
|
-
}
|
|
719
|
-
function describeWorkflow(w) {
|
|
720
|
-
if (!w) return "no active task";
|
|
721
|
-
const gate = w.nextGate ? ` \u2192 next gate: ${w.nextGate}` : "";
|
|
722
|
-
const flag = w.degraded ? ` ${badge("warn", "degraded")}` : "";
|
|
723
|
-
return `${w.phase} (${w.state}, ${w.mode}) \xB7 task ${w.taskId}${gate}${flag}`;
|
|
724
|
-
}
|
|
725
|
-
function describeMemory(m) {
|
|
726
|
-
if (!m) return "unavailable (memory engine not wired)";
|
|
727
|
-
return `${m.observations} observations across ${m.sessions} session${m.sessions === 1 ? "" : "s"}`;
|
|
728
|
-
}
|
|
729
|
-
function renderHuman(p, opts) {
|
|
730
|
-
log(`noir status \u2014 ${p.project.name} (${p.project.id})`, opts);
|
|
731
|
-
definitionList(
|
|
732
|
-
[
|
|
733
|
-
{ label: "Project", value: `${p.project.name} (${p.project.id})` },
|
|
734
|
-
{ label: "Host", value: p.host },
|
|
735
|
-
{ label: "Noir", value: p.noir },
|
|
736
|
-
{ label: "Daemon", value: describeDaemon(p.daemon) },
|
|
737
|
-
{ label: "Store", value: describeStore(p.store) },
|
|
738
|
-
{ label: "Context", value: describeContext(p.context) },
|
|
739
|
-
{ label: "Workflow", value: describeWorkflow(p.workflow) },
|
|
740
|
-
{ label: "Memory", value: describeMemory(p.memory) }
|
|
741
|
-
],
|
|
742
|
-
opts
|
|
743
|
-
);
|
|
744
|
-
if (p.workflow && p.workflow.state !== "done" && p.workflow.state !== "abandoned") {
|
|
745
|
-
tip("resume: `noir task resume`", opts);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
async function gatherStatusPayload(opts) {
|
|
749
|
-
const project = loadProjectInfo3(process.cwd());
|
|
750
|
-
const probe = await probeDaemon(opts);
|
|
751
|
-
let host = null;
|
|
752
|
-
let store = null;
|
|
753
|
-
let context = null;
|
|
754
|
-
let workflow = null;
|
|
755
|
-
let memory = null;
|
|
756
|
-
if (probe.running) {
|
|
757
|
-
await withRunningDaemon(
|
|
758
|
-
opts,
|
|
759
|
-
async (caller) => {
|
|
760
|
-
host = await tryTool(caller, "host_status");
|
|
761
|
-
store = await tryTool(caller, "store_status");
|
|
762
|
-
context = await tryTool(caller, "context_status");
|
|
763
|
-
workflow = await tryTool(caller, "workflow_status");
|
|
764
|
-
memory = await tryTool(caller, "memory_sessions");
|
|
765
|
-
},
|
|
766
|
-
probe
|
|
767
|
-
);
|
|
768
|
-
}
|
|
769
|
-
return buildPayload(project, probe, host, store, context, workflow, memory);
|
|
770
|
-
}
|
|
771
|
-
async function status(opts) {
|
|
772
|
-
void runAsyncUpdateCheck({
|
|
773
|
-
env: process.env,
|
|
774
|
-
configUpdate: (() => {
|
|
775
|
-
try {
|
|
776
|
-
const info2 = loadProjectInfo3(process.cwd());
|
|
777
|
-
return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
|
|
778
|
-
} catch {
|
|
779
|
-
return DEFAULT_UPDATE_CONFIG;
|
|
780
|
-
}
|
|
781
|
-
})(),
|
|
782
|
-
quiet: opts.json === true || opts.quiet === true || !process.stdout.isTTY
|
|
783
|
-
});
|
|
784
|
-
const payload = await gatherStatusPayload(opts);
|
|
785
|
-
if (opts.json === true) {
|
|
786
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
|
|
787
|
-
`);
|
|
788
|
-
return;
|
|
789
|
-
}
|
|
790
|
-
renderHuman(payload, opts);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
// src/commands/task.ts
|
|
794
|
-
import { spawnSync } from "child_process";
|
|
795
|
-
import { createHash } from "crypto";
|
|
796
|
-
import { loadProjectInfo as loadProjectInfo4, nextArtifactSequence } from "@noir-ai/core";
|
|
797
|
-
import {
|
|
798
|
-
PHASES,
|
|
799
|
-
TASK_CLASSES,
|
|
800
|
-
writeChangelogStub,
|
|
801
|
-
writeDecisionStub
|
|
802
|
-
} from "@noir-ai/workflow";
|
|
803
|
-
var PHASE_SKILL = {
|
|
804
|
-
// C3 curation (2026-08-10): intake+clarify merged into brainstorming;
|
|
805
|
-
// execute → executing-plans; verify → verifying; document → wrap.
|
|
806
|
-
intake: "noir-brainstorming",
|
|
807
|
-
clarify: "noir-brainstorming",
|
|
808
|
-
spec: "noir-spec",
|
|
809
|
-
plan: "noir-planning",
|
|
810
|
-
execute: "noir-executing-plans",
|
|
811
|
-
verify: "noir-verifying",
|
|
812
|
-
document: "noir-wrap"
|
|
813
|
-
};
|
|
814
|
-
function skillFor(phase) {
|
|
815
|
-
if (typeof phase !== "string") return null;
|
|
816
|
-
return PHASE_SKILL[phase] ?? null;
|
|
817
|
-
}
|
|
818
|
-
function writeDoneArtifacts(opts, taskId) {
|
|
819
|
-
try {
|
|
820
|
-
const project = loadProjectInfo4(process.cwd());
|
|
821
|
-
const conflict = { conflictPolicy: "preserve", interactive: false };
|
|
822
|
-
writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
|
|
823
|
-
const nextN = nextArtifactSequence(project.root, "adr");
|
|
824
|
-
writeDecisionStub(project.root, nextN, taskId, void 0, conflict);
|
|
825
|
-
} catch {
|
|
826
|
-
tip("document-phase artifacts could not be written (use --no-artifacts to skip)", opts);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
async function fetchStatus(opts, taskId) {
|
|
830
|
-
const args = typeof taskId === "string" && taskId.length > 0 ? { taskId } : {};
|
|
831
|
-
const res = await callDaemonTool(
|
|
832
|
-
opts,
|
|
833
|
-
"workflow_status",
|
|
834
|
-
args
|
|
835
|
-
);
|
|
836
|
-
if (res.ok === true) return res;
|
|
837
|
-
const detail = res.error ?? "task not found";
|
|
838
|
-
fail(EXIT.NOT_FOUND, `task: ${detail}`, opts);
|
|
839
|
-
}
|
|
840
|
-
function formatStamp(ms) {
|
|
841
|
-
if (typeof ms !== "number" || ms <= 0) return "-";
|
|
842
|
-
return new Date(ms).toISOString().replace("T", " ").replace(/\..*$/, "Z");
|
|
843
|
-
}
|
|
844
|
-
function renderStatusRow(s, opts) {
|
|
845
|
-
const flag = s.degraded === true ? ` ${badge("warn", "degraded")}` : "";
|
|
846
|
-
log(`task status \u2014 ${s.taskId}${flag}`, opts);
|
|
847
|
-
const rows = [
|
|
848
|
-
{ label: "Task", value: s.taskId },
|
|
849
|
-
{ label: "Phase", value: s.phase },
|
|
850
|
-
{ label: "State", value: s.state },
|
|
851
|
-
{ label: "Mode", value: s.mode }
|
|
852
|
-
];
|
|
853
|
-
if (typeof s.taskClass === "string" && s.taskClass.length > 0) {
|
|
854
|
-
rows.push({ label: "Class", value: s.taskClass });
|
|
855
|
-
}
|
|
856
|
-
if (typeof s.blockReason === "string" && s.blockReason.length > 0) {
|
|
857
|
-
rows.push({ label: "Block reason", value: s.blockReason });
|
|
858
|
-
}
|
|
859
|
-
rows.push(
|
|
860
|
-
{ label: "Next gate", value: s.nextGate ?? "\u2014" },
|
|
861
|
-
{ label: "Updated", value: formatStamp(s.updatedAt) }
|
|
862
|
-
);
|
|
863
|
-
definitionList(rows, opts);
|
|
864
|
-
}
|
|
865
|
-
var GATE_PHASES = ["spec", "plan", "verify"];
|
|
866
|
-
function nextGateAfter(phase) {
|
|
867
|
-
const cur = PHASES.indexOf(phase);
|
|
868
|
-
for (const p of GATE_PHASES) {
|
|
869
|
-
if (PHASES.indexOf(p) > cur) return p;
|
|
870
|
-
}
|
|
871
|
-
return null;
|
|
872
|
-
}
|
|
873
|
-
async function taskStatus(opts) {
|
|
874
|
-
let probe;
|
|
875
|
-
try {
|
|
876
|
-
probe = await probeDaemon(opts);
|
|
877
|
-
} catch {
|
|
878
|
-
probe = { running: true };
|
|
879
|
-
}
|
|
880
|
-
if (!probe.running) {
|
|
881
|
-
const s2 = await withInProcessRead(opts, async (engines) => {
|
|
882
|
-
const id = typeof opts.id === "string" && opts.id.length > 0 ? opts.id : null;
|
|
883
|
-
const taskId = id ?? engines.workflow.activeTaskId();
|
|
884
|
-
if (!taskId) fail(EXIT.NOT_FOUND, "task: no active task", opts);
|
|
885
|
-
const task = engines.workflow.status(taskId);
|
|
886
|
-
if (!task) fail(EXIT.NOT_FOUND, `task: unknown task '${taskId}'`, opts);
|
|
887
|
-
const stopped = task.state === "blocked" || task.state === "abandoned";
|
|
888
|
-
return {
|
|
889
|
-
ok: true,
|
|
890
|
-
taskId: task.taskId,
|
|
891
|
-
phase: task.phase,
|
|
892
|
-
state: task.state,
|
|
893
|
-
mode: task.mode,
|
|
894
|
-
nextGate: stopped ? null : nextGateAfter(task.phase),
|
|
895
|
-
history: task.history,
|
|
896
|
-
updatedAt: task.updatedAt,
|
|
897
|
-
degraded: true
|
|
898
|
-
};
|
|
899
|
-
});
|
|
900
|
-
if (opts.json === true) {
|
|
901
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: s2 })}
|
|
902
|
-
`);
|
|
903
|
-
return;
|
|
904
|
-
}
|
|
905
|
-
renderStatusRow(s2, opts);
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
const s = await fetchStatus(opts, opts.id);
|
|
909
|
-
if (opts.json === true) {
|
|
910
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: s })}
|
|
911
|
-
`);
|
|
912
|
-
return;
|
|
913
|
-
}
|
|
914
|
-
renderStatusRow(s, opts);
|
|
915
|
-
}
|
|
916
|
-
async function taskNext(opts) {
|
|
917
|
-
const s = await fetchStatus(opts);
|
|
918
|
-
const suggestion = skillFor(s.phase);
|
|
919
|
-
const nextSkill = skillFor(s.nextGate);
|
|
920
|
-
const data = {
|
|
921
|
-
taskId: s.taskId,
|
|
922
|
-
phase: s.phase,
|
|
923
|
-
state: s.state,
|
|
924
|
-
mode: s.mode,
|
|
925
|
-
nextGate: s.nextGate,
|
|
926
|
-
suggestion,
|
|
927
|
-
nextSkill,
|
|
928
|
-
degraded: s.degraded === true
|
|
929
|
-
};
|
|
930
|
-
if (opts.json === true) {
|
|
931
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
932
|
-
`);
|
|
933
|
-
return;
|
|
934
|
-
}
|
|
935
|
-
const flag = data.degraded ? ` ${badge("warn", "degraded")}` : "";
|
|
936
|
-
log(`task next \u2014 ${s.taskId} \xB7 phase ${s.phase} (${s.state}, ${s.mode})${flag}`, opts);
|
|
937
|
-
if (s.nextGate) {
|
|
938
|
-
info(`next gate: ${s.nextGate}`, opts);
|
|
939
|
-
} else {
|
|
940
|
-
info("no further gate ahead (past verify, or stopped).", opts);
|
|
941
|
-
}
|
|
942
|
-
if (suggestion) info(`skill for current phase (${s.phase}): ${suggestion}`, opts);
|
|
943
|
-
if (nextSkill) info(`skill for next gate (${s.nextGate}): ${nextSkill}`, opts);
|
|
944
|
-
if (!suggestion && !nextSkill) {
|
|
945
|
-
info("run `noir skills list` to see applicable skills.", opts);
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
async function taskNew(opts) {
|
|
949
|
-
let mode;
|
|
950
|
-
if (opts.mode !== void 0) {
|
|
951
|
-
if (opts.mode !== "full" && opts.mode !== "quick") {
|
|
952
|
-
fail(EXIT.USAGE, `task new: invalid mode '${opts.mode}' (expected 'full' or 'quick')`, opts);
|
|
953
|
-
}
|
|
954
|
-
mode = opts.mode;
|
|
955
|
-
}
|
|
956
|
-
let taskClass;
|
|
957
|
-
if (opts.taskClass !== void 0) {
|
|
958
|
-
if (!TASK_CLASSES.includes(opts.taskClass)) {
|
|
959
|
-
fail(
|
|
960
|
-
EXIT.USAGE,
|
|
961
|
-
`task new: invalid class '${opts.taskClass}' (expected one of: ${TASK_CLASSES.join(", ")})`,
|
|
962
|
-
opts
|
|
963
|
-
);
|
|
964
|
-
}
|
|
965
|
-
taskClass = opts.taskClass;
|
|
966
|
-
}
|
|
967
|
-
const res = await callDaemonTool(
|
|
968
|
-
opts,
|
|
969
|
-
"workflow_start",
|
|
970
|
-
{
|
|
971
|
-
taskId: opts.slug,
|
|
972
|
-
slug: opts.slug,
|
|
973
|
-
...mode === void 0 ? {} : { mode },
|
|
974
|
-
...taskClass === void 0 ? {} : { taskClass }
|
|
975
|
-
}
|
|
976
|
-
);
|
|
977
|
-
if (res.ok !== true) {
|
|
978
|
-
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "start failed";
|
|
979
|
-
fail(EXIT.ERROR, `task new --slug ${opts.slug}: ${detail}`, opts);
|
|
980
|
-
}
|
|
981
|
-
if (opts.json === true) {
|
|
982
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
|
|
983
|
-
`);
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
renderStatusRow(res, opts);
|
|
987
|
-
}
|
|
988
|
-
async function taskAdvance(opts) {
|
|
989
|
-
const args = {};
|
|
990
|
-
if (typeof opts.to === "string" && opts.to.length > 0) {
|
|
991
|
-
if (!Object.keys(PHASE_SKILL).includes(opts.to)) {
|
|
992
|
-
fail(EXIT.USAGE, `task advance: invalid phase '${opts.to}'`, opts);
|
|
993
|
-
}
|
|
994
|
-
args.to = opts.to;
|
|
995
|
-
}
|
|
996
|
-
if (typeof opts.force === "string" && opts.force.length > 0) {
|
|
997
|
-
args.force = { reason: opts.force };
|
|
998
|
-
}
|
|
999
|
-
const res = await callDaemonTool(
|
|
1000
|
-
opts,
|
|
1001
|
-
"workflow_advance",
|
|
1002
|
-
args
|
|
1003
|
-
);
|
|
1004
|
-
if (res.ok !== true) {
|
|
1005
|
-
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "advance failed";
|
|
1006
|
-
fail(EXIT.ERROR, `task advance: ${detail}`, opts);
|
|
1007
|
-
}
|
|
1008
|
-
if (res.state === "done" && opts.noArtifacts !== true) {
|
|
1009
|
-
writeDoneArtifacts(opts, res.taskId);
|
|
1010
|
-
}
|
|
1011
|
-
if (typeof opts.to === "string" && opts.to === "verify") {
|
|
1012
|
-
tip("run `noir handoff` for a ready-to-paste host prompt", opts);
|
|
1013
|
-
}
|
|
1014
|
-
if (opts.json === true) {
|
|
1015
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
|
|
1016
|
-
`);
|
|
1017
|
-
return;
|
|
1018
|
-
}
|
|
1019
|
-
renderStatusRow(res, opts);
|
|
1020
|
-
}
|
|
1021
|
-
function renderResumeBriefing(r, opts) {
|
|
1022
|
-
const flag = r.degraded === true ? ` ${badge("warn", "degraded")}` : "";
|
|
1023
|
-
log(`resume \u2014 ${r.taskId} (${r.state}, ${r.mode})${flag}`, opts);
|
|
1024
|
-
const rows = [
|
|
1025
|
-
{ label: "Task", value: String(r.taskId) },
|
|
1026
|
-
{ label: "Phase", value: String(r.phase) },
|
|
1027
|
-
{ label: "State", value: String(r.state) },
|
|
1028
|
-
{ label: "Mode", value: String(r.mode) }
|
|
1029
|
-
];
|
|
1030
|
-
if (typeof r.taskClass === "string" && r.taskClass.length > 0) {
|
|
1031
|
-
rows.push({ label: "Class", value: r.taskClass });
|
|
1032
|
-
}
|
|
1033
|
-
if (typeof r.blockReason === "string" && r.blockReason.length > 0) {
|
|
1034
|
-
rows.push({ label: "Block reason", value: r.blockReason });
|
|
1035
|
-
}
|
|
1036
|
-
rows.push({ label: "Next gate", value: r.nextGate ?? "\u2014" });
|
|
1037
|
-
definitionList(rows, opts);
|
|
1038
|
-
const suggestion = skillFor(r.phase ?? null);
|
|
1039
|
-
if (suggestion) info(`skill for current phase (${r.phase}): ${suggestion}`, opts);
|
|
1040
|
-
if (r.state === "blocked") {
|
|
1041
|
-
info("resume a blocked task with: noir task advance --to <phase>", opts);
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
async function taskResume(opts) {
|
|
1045
|
-
const args = {};
|
|
1046
|
-
if (typeof opts.id === "string" && opts.id.length > 0) {
|
|
1047
|
-
args.taskId = opts.id;
|
|
1048
|
-
}
|
|
1049
|
-
const res = await callDaemonTool(opts, "workflow_resume", args);
|
|
1050
|
-
if (res.resumable !== true) {
|
|
1051
|
-
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "nothing to resume";
|
|
1052
|
-
if (opts.json === true) {
|
|
1053
|
-
process.stdout.write(`${JSON.stringify({ ok: false, resumable: false, error: detail })}
|
|
1054
|
-
`);
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
fail(EXIT.ERROR, `task resume: ${detail}`, opts);
|
|
1058
|
-
}
|
|
1059
|
-
if (typeof opts.prompt === "string" && opts.prompt.length > 0) {
|
|
1060
|
-
info(`resume prompt: ${opts.prompt}`, opts);
|
|
1061
|
-
}
|
|
1062
|
-
if (opts.json === true) {
|
|
1063
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
|
|
1064
|
-
`);
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
renderResumeBriefing(res, opts);
|
|
1068
|
-
}
|
|
1069
|
-
async function taskBlock(opts) {
|
|
1070
|
-
if (typeof opts.reason !== "string" || opts.reason.trim().length === 0) {
|
|
1071
|
-
fail(EXIT.USAGE, "task block: a non-empty <reason> is required", opts);
|
|
1072
|
-
}
|
|
1073
|
-
const args = { reason: opts.reason };
|
|
1074
|
-
if (typeof opts.task === "string" && opts.task.length > 0) args.taskId = opts.task;
|
|
1075
|
-
const res = await callDaemonTool(
|
|
1076
|
-
opts,
|
|
1077
|
-
"workflow_block",
|
|
1078
|
-
args
|
|
1079
|
-
);
|
|
1080
|
-
if (res.ok !== true) {
|
|
1081
|
-
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "block failed";
|
|
1082
|
-
fail(EXIT.ERROR, `task block: ${detail}`, opts);
|
|
1083
|
-
}
|
|
1084
|
-
if (opts.json === true) {
|
|
1085
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
|
|
1086
|
-
`);
|
|
1087
|
-
return;
|
|
1088
|
-
}
|
|
1089
|
-
renderStatusRow(res, opts);
|
|
1090
|
-
}
|
|
1091
|
-
async function taskAbandon(opts) {
|
|
1092
|
-
if (isInteractive(opts)) {
|
|
1093
|
-
const clack = await import("@clack/prompts");
|
|
1094
|
-
const confirm = await clack.confirm({
|
|
1095
|
-
message: "Abandon this task? This is terminal and cannot be undone.",
|
|
1096
|
-
initialValue: false
|
|
1097
|
-
});
|
|
1098
|
-
if (clack.isCancel(confirm)) {
|
|
1099
|
-
clack.cancel("Cancelled.");
|
|
1100
|
-
fail(EXIT.CANCELLED, "cancelled", opts);
|
|
1101
|
-
}
|
|
1102
|
-
if (confirm !== true) {
|
|
1103
|
-
fail(EXIT.CANCELLED, "abandon cancelled", opts);
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
const args = {};
|
|
1107
|
-
if (typeof opts.task === "string" && opts.task.length > 0) args.taskId = opts.task;
|
|
1108
|
-
const res = await callDaemonTool(
|
|
1109
|
-
opts,
|
|
1110
|
-
"workflow_abandon",
|
|
1111
|
-
args
|
|
1112
|
-
);
|
|
1113
|
-
if (res.ok !== true) {
|
|
1114
|
-
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "abandon failed";
|
|
1115
|
-
fail(EXIT.ERROR, `task abandon: ${detail}`, opts);
|
|
1116
|
-
}
|
|
1117
|
-
if (opts.json === true) {
|
|
1118
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
|
|
1119
|
-
`);
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
renderStatusRow(res, opts);
|
|
1123
|
-
}
|
|
1124
|
-
function runCheck(cmd) {
|
|
1125
|
-
try {
|
|
1126
|
-
const buf = spawnSync("sh", ["-c", cmd], {
|
|
1127
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1128
|
-
encoding: "utf8"
|
|
1129
|
-
});
|
|
1130
|
-
let out = "";
|
|
1131
|
-
if (buf.stdout) out += buf.stdout.toString();
|
|
1132
|
-
if (buf.stderr) out += buf.stderr.toString();
|
|
1133
|
-
const status2 = buf.status;
|
|
1134
|
-
return {
|
|
1135
|
-
exitCode: typeof status2 === "number" ? status2 : 1,
|
|
1136
|
-
digest: createHash("sha256").update(out).digest("hex")
|
|
1137
|
-
};
|
|
1138
|
-
} catch {
|
|
1139
|
-
return { exitCode: 1, digest: createHash("sha256").update("").digest("hex") };
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
async function taskVerify(opts) {
|
|
1143
|
-
const project = loadProjectInfo4(process.cwd());
|
|
1144
|
-
const cfgChecks = project.config.workflow?.gate?.verify?.checks ?? [];
|
|
1145
|
-
const all = cfgChecks.map((c2) => ({
|
|
1146
|
-
name: c2.name,
|
|
1147
|
-
command: c2.command,
|
|
1148
|
-
tier: c2.tier ?? "hard"
|
|
1149
|
-
}));
|
|
1150
|
-
let checks = all;
|
|
1151
|
-
if (opts.check && opts.check.length > 0) {
|
|
1152
|
-
const want = new Set(opts.check);
|
|
1153
|
-
checks = all.filter((c2) => want.has(c2.name));
|
|
1154
|
-
if (checks.length === 0) {
|
|
1155
|
-
fail(
|
|
1156
|
-
EXIT.USAGE,
|
|
1157
|
-
`task verify: no configured checks match --check ${[...want].join(",")}`,
|
|
1158
|
-
opts
|
|
1159
|
-
);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
if (checks.length === 0) {
|
|
1163
|
-
fail(
|
|
1164
|
-
EXIT.USAGE,
|
|
1165
|
-
"task verify: no verify checks configured (set workflow.gate.verify.checks in noir.config)",
|
|
1166
|
-
opts
|
|
1167
|
-
);
|
|
1168
|
-
}
|
|
1169
|
-
const ranAt = Date.now();
|
|
1170
|
-
const evidence = {
|
|
1171
|
-
ranAt,
|
|
1172
|
-
checks: checks.map((c2) => {
|
|
1173
|
-
const r = runCheck(c2.command);
|
|
1174
|
-
return {
|
|
1175
|
-
name: c2.name,
|
|
1176
|
-
exitCode: r.exitCode,
|
|
1177
|
-
outputDigest: r.digest,
|
|
1178
|
-
command: c2.command,
|
|
1179
|
-
tier: c2.tier
|
|
1180
|
-
};
|
|
1181
|
-
}),
|
|
1182
|
-
summary: ""
|
|
1183
|
-
};
|
|
1184
|
-
const passed = evidence.checks.filter((c2) => c2.exitCode === 0).length;
|
|
1185
|
-
const failed = evidence.checks.length - passed;
|
|
1186
|
-
evidence.summary = `${passed} passed, ${failed} failed`;
|
|
1187
|
-
const res = await callDaemonTool(opts, "workflow_advance", { evidence });
|
|
1188
|
-
if (res.ok === true) {
|
|
1189
|
-
if (opts.json === true) {
|
|
1190
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: res, evidence })}
|
|
1191
|
-
`);
|
|
1192
|
-
return;
|
|
1193
|
-
}
|
|
1194
|
-
success(`verify \u2014 ${evidence.summary} \u2192 gate approved`, opts);
|
|
1195
|
-
renderStatusRow(res, opts);
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
if (opts.json === true) {
|
|
1199
|
-
process.stdout.write(`${JSON.stringify({ evidence, ...res })}
|
|
1200
|
-
`);
|
|
1201
|
-
return;
|
|
1202
|
-
}
|
|
1203
|
-
if (res.pendingGate) {
|
|
1204
|
-
warn(`verify gate: ${res.pendingGate.reason} (${evidence.summary})`, opts);
|
|
1205
|
-
const recovery = res.recovery ?? ["retry", "force", "skip", "block"];
|
|
1206
|
-
info(
|
|
1207
|
-
`recovery: ${recovery.map(
|
|
1208
|
-
(r) => r === "retry" ? "`noir task verify`" : r === "force" ? "`noir task advance --force <reason>`" : r === "skip" ? "`noir task advance --skip`" : r === "block" ? "`noir task block <reason>`" : `\`${r}\``
|
|
1209
|
-
).join(" | ")}`,
|
|
1210
|
-
opts
|
|
1211
|
-
);
|
|
1212
|
-
} else {
|
|
1213
|
-
fail(EXIT.ERROR, `task verify: ${res.error ?? "verify failed"}`, opts);
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
async function taskResearch(opts) {
|
|
1217
|
-
const s = await fetchStatus(opts, opts.id);
|
|
1218
|
-
if (opts.json === true) {
|
|
1219
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: s })}
|
|
1220
|
-
`);
|
|
1221
|
-
return;
|
|
1222
|
-
}
|
|
1223
|
-
renderStatusRow(s, opts);
|
|
1224
|
-
info("research findings are recorded via `noir task research record` (list + record)", opts);
|
|
1225
|
-
}
|
|
1226
|
-
async function taskResearchRecord(opts) {
|
|
1227
|
-
const TYPES = ["assumption", "discovery", "decision", "grounding-fact"];
|
|
1228
|
-
if (!TYPES.includes(opts.type)) {
|
|
1229
|
-
fail(
|
|
1230
|
-
EXIT.USAGE,
|
|
1231
|
-
`task research record: invalid type '${opts.type}' (expected one of: ${TYPES.join(", ")})`,
|
|
1232
|
-
opts
|
|
1233
|
-
);
|
|
1234
|
-
}
|
|
1235
|
-
const args = { type: opts.type, text: opts.text };
|
|
1236
|
-
if (typeof opts.source === "string" && opts.source.length > 0) args.source = opts.source;
|
|
1237
|
-
if (typeof opts.task === "string" && opts.task.length > 0) args.taskId = opts.task;
|
|
1238
|
-
const res = await callDaemonTool(opts, "workflow_research_record", args);
|
|
1239
|
-
if (res.ok !== true) {
|
|
1240
|
-
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "record failed";
|
|
1241
|
-
fail(EXIT.ERROR, `task research record: ${detail}`, opts);
|
|
1242
|
-
}
|
|
1243
|
-
if (opts.json === true) {
|
|
1244
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
|
|
1245
|
-
`);
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
success(
|
|
1249
|
-
`research recorded \u2192 ${opts.type}: ${opts.text.slice(0, 80)}${opts.text.length > 80 ? "\u2026" : ""}`,
|
|
1250
|
-
opts
|
|
1251
|
-
);
|
|
1252
|
-
}
|
|
1253
|
-
async function taskDecompose(opts) {
|
|
1254
|
-
const cap = opts.capability.trim();
|
|
1255
|
-
if (cap.length === 0) fail(EXIT.USAGE, "task decompose: <capability-id> is required", opts);
|
|
1256
|
-
const plan = {
|
|
1257
|
-
capabilityId: cap,
|
|
1258
|
-
intent: `Capability ${cap} \u2014 details TBD (run with a provider for an LLM-drafted plan)`,
|
|
1259
|
-
slices: [
|
|
1260
|
-
{
|
|
1261
|
-
id: "s1-walking-skeleton",
|
|
1262
|
-
title: "Walking skeleton \u2014 thinnest end-to-end",
|
|
1263
|
-
type: "feature",
|
|
1264
|
-
rationale: `The first slice for capability ${cap}.`,
|
|
1265
|
-
scopeIn: "End-to-end path linking the main architecture.",
|
|
1266
|
-
scopeOut: "Polish, edge cases, full coverage.",
|
|
1267
|
-
dependsOn: [],
|
|
1268
|
-
files: { create: [], modify: [], preserve: [] },
|
|
1269
|
-
acceptance: [`verify acceptance criteria for capability ${cap}`],
|
|
1270
|
-
doD: ["pnpm test", "pnpm typecheck"],
|
|
1271
|
-
rollbackPlan: { procedure: "revert the slice commit", verifyCommand: "pnpm test" }
|
|
1272
|
-
}
|
|
1273
|
-
],
|
|
1274
|
-
status: { "s1-walking-skeleton": "planned" }
|
|
1275
|
-
};
|
|
1276
|
-
if (opts.json === true) {
|
|
1277
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: plan })}
|
|
1278
|
-
`);
|
|
1279
|
-
return;
|
|
1280
|
-
}
|
|
1281
|
-
log(`decompose \u2014 ${cap}`, opts);
|
|
1282
|
-
for (const s of plan.slices) {
|
|
1283
|
-
info(`${s.id}: ${s.title} (${s.type}) \u2014 rationale: ${s.rationale}`, opts);
|
|
1284
|
-
}
|
|
1285
|
-
tip("each piece enters the existing clarify\u2192spec\u2192plan\u2192execute\u2192verify\u2192document workflow", opts);
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
// src/commands/handoff.ts
|
|
1289
|
-
var SEED_LIMIT = 5;
|
|
1290
|
-
function heading(name, id) {
|
|
1291
|
-
return `# Noir handoff \u2014 ${name} (${id})`;
|
|
1292
|
-
}
|
|
1293
|
-
function renderMarkdown(p) {
|
|
1294
|
-
const lines = [];
|
|
1295
|
-
lines.push(heading(p.project.name, p.project.id));
|
|
1296
|
-
lines.push("");
|
|
1297
|
-
if (p.task) {
|
|
1298
|
-
const gate = p.task.nextGate ?? "\u2014";
|
|
1299
|
-
lines.push(`**Phase:** ${p.task.phase} \u2014 next gate: ${gate}`);
|
|
1300
|
-
} else {
|
|
1301
|
-
lines.push("**Phase:** no active task");
|
|
1302
|
-
}
|
|
1303
|
-
lines.push("");
|
|
1304
|
-
lines.push("## Open host");
|
|
1305
|
-
lines.push(p.directive);
|
|
1306
|
-
lines.push("");
|
|
1307
|
-
lines.push("## Next step");
|
|
1308
|
-
if (p.task?.nextSkill) {
|
|
1309
|
-
lines.push(`Run \`${p.task.nextSkill}\` \u2014 the host's next gate skill.`);
|
|
1310
|
-
} else {
|
|
1311
|
-
lines.push("No further gate ahead (past verify, or no active task).");
|
|
1312
|
-
}
|
|
1313
|
-
lines.push("");
|
|
1314
|
-
lines.push("## Extracted context (seed)");
|
|
1315
|
-
if (p.contextSeed && p.contextSeed.length > 0) {
|
|
1316
|
-
for (const h of p.contextSeed) {
|
|
1317
|
-
lines.push(`- \`${h.path}\` (score ${h.score.toFixed(4)})`);
|
|
1318
|
-
const snip = h.snippet.trim();
|
|
1319
|
-
if (snip.length > 0) lines.push(` > ${snip.replace(/\n/g, "\n > ")}`);
|
|
1320
|
-
}
|
|
1321
|
-
} else if (p.degraded.embedderMissing) {
|
|
1322
|
-
lines.push(
|
|
1323
|
-
"_Embedder unavailable \u2014 degraded to BM25-only or no hits. Start `noir daemon start` for live context._"
|
|
1324
|
-
);
|
|
1325
|
-
} else {
|
|
1326
|
-
lines.push("_No context hits (or daemon down). Start `noir daemon start` for live context._");
|
|
1327
|
-
}
|
|
1328
|
-
lines.push("");
|
|
1329
|
-
lines.push("## Extracted memory (seed)");
|
|
1330
|
-
if (p.memorySeed && p.memorySeed.length > 0) {
|
|
1331
|
-
for (const m of p.memorySeed) {
|
|
1332
|
-
lines.push(`- \`${m.id}\` \u2014 ${m.summary}`);
|
|
1333
|
-
}
|
|
1334
|
-
} else {
|
|
1335
|
-
lines.push(
|
|
1336
|
-
"_No memory observations (or daemon down). Start `noir daemon start` for live memory._"
|
|
1337
|
-
);
|
|
1338
|
-
}
|
|
1339
|
-
lines.push("");
|
|
1340
|
-
lines.push("## Live data \u2014 call these from the host (already wired via the host MCP config)");
|
|
1341
|
-
lines.push("- `noir.workflow_status` \xB7 `noir.context_search` \xB7 `noir.memory_recall`");
|
|
1342
|
-
if (p.degraded.daemonDown) {
|
|
1343
|
-
lines.push("");
|
|
1344
|
-
lines.push("> **Note:** daemon not running \u2014 start `noir daemon start` for live task/memory.");
|
|
1345
|
-
}
|
|
1346
|
-
lines.push("");
|
|
1347
|
-
return lines.join("\n");
|
|
1348
|
-
}
|
|
1349
|
-
function toContextHit(raw) {
|
|
1350
|
-
const h = raw ?? {};
|
|
1351
|
-
return {
|
|
1352
|
-
path: typeof h.path === "string" ? h.path : "<unknown>",
|
|
1353
|
-
score: typeof h.score === "number" ? h.score : 0,
|
|
1354
|
-
snippet: typeof h.snippet === "string" ? h.snippet : ""
|
|
1355
|
-
};
|
|
1356
|
-
}
|
|
1357
|
-
function observationSummary(raw) {
|
|
1358
|
-
const obs = raw.observation;
|
|
1359
|
-
if (obs && typeof obs === "object") {
|
|
1360
|
-
const content = obs.content;
|
|
1361
|
-
if (typeof content === "string" && content.length > 0) {
|
|
1362
|
-
return content.length > 160 ? `${content.slice(0, 157)}\u2026` : content;
|
|
1363
|
-
}
|
|
1364
|
-
const type = obs.type;
|
|
1365
|
-
if (typeof type === "string") return `(${type})`;
|
|
1366
|
-
}
|
|
1367
|
-
return "";
|
|
1368
|
-
}
|
|
1369
|
-
function toMemoryHit(raw) {
|
|
1370
|
-
const h = raw ?? {};
|
|
1371
|
-
return {
|
|
1372
|
-
id: typeof h.id === "string" ? h.id : "<unknown>",
|
|
1373
|
-
summary: observationSummary(h)
|
|
1374
|
-
};
|
|
1375
|
-
}
|
|
1376
|
-
function toHostPayload(p) {
|
|
1377
|
-
return {
|
|
1378
|
-
project: p.project,
|
|
1379
|
-
host: p.host,
|
|
1380
|
-
task: p.task
|
|
1381
|
-
};
|
|
1382
|
-
}
|
|
1383
|
-
function resolveDirective(root, p) {
|
|
1384
|
-
const ctx = { root };
|
|
1385
|
-
const adapter = resolveAdapter(p.host);
|
|
1386
|
-
const hostPayload = toHostPayload(p);
|
|
1387
|
-
if (typeof adapter.emitHandoff === "function") {
|
|
1388
|
-
return adapter.emitHandoff(ctx, hostPayload);
|
|
1389
|
-
}
|
|
1390
|
-
return defaultHandoffBlock(ctx, hostPayload);
|
|
1391
|
-
}
|
|
1392
|
-
function handoffFilePath(root, taskId, projectId) {
|
|
1393
|
-
const id = typeof taskId === "string" && taskId.length > 0 ? taskId : projectId;
|
|
1394
|
-
return resolveArtifactPath(root, "handoff", { taskId: id });
|
|
1395
|
-
}
|
|
1396
|
-
async function extractSeeds(opts, probe, query) {
|
|
1397
|
-
if (!probe.running) return { context: null, memory: null };
|
|
1398
|
-
let context = null;
|
|
1399
|
-
let memory = null;
|
|
1400
|
-
await withRunningDaemon(
|
|
1401
|
-
opts,
|
|
1402
|
-
async (caller) => {
|
|
1403
|
-
const ctxRes = await tryTool(caller, "context_search", {
|
|
1404
|
-
query,
|
|
1405
|
-
limit: SEED_LIMIT
|
|
1406
|
-
});
|
|
1407
|
-
if (ctxRes && ctxRes.ok !== false && Array.isArray(ctxRes.results)) {
|
|
1408
|
-
context = ctxRes.results.slice(0, SEED_LIMIT).map(toContextHit).filter((h) => h.path !== "<unknown>" || h.snippet.length > 0);
|
|
1409
|
-
}
|
|
1410
|
-
const memRes = await tryTool(caller, "memory_recall", {
|
|
1411
|
-
query,
|
|
1412
|
-
limit: SEED_LIMIT
|
|
1413
|
-
});
|
|
1414
|
-
if (memRes && memRes.ok !== false && Array.isArray(memRes.results)) {
|
|
1415
|
-
memory = memRes.results.slice(0, SEED_LIMIT).map(toMemoryHit).filter((h) => h.summary.length > 0);
|
|
1416
|
-
}
|
|
1417
|
-
},
|
|
1418
|
-
probe
|
|
1419
|
-
);
|
|
1420
|
-
return { context, memory };
|
|
1421
|
-
}
|
|
1422
|
-
async function probeOnly(opts) {
|
|
1423
|
-
return probeDaemon(opts);
|
|
1424
|
-
}
|
|
1425
|
-
async function handoff(opts) {
|
|
1426
|
-
const project = loadProjectInfo5(process.cwd());
|
|
1427
|
-
const host = project.config.host;
|
|
1428
|
-
const snapshot = await gatherStatusPayload(opts);
|
|
1429
|
-
const probe = await probeOnly(opts);
|
|
1430
|
-
const task = snapshot.workflow ? {
|
|
1431
|
-
taskId: snapshot.workflow.taskId,
|
|
1432
|
-
phase: snapshot.workflow.phase,
|
|
1433
|
-
nextGate: snapshot.workflow.nextGate,
|
|
1434
|
-
nextSkill: skillFor(snapshot.workflow.nextGate) ?? skillFor(snapshot.workflow.phase)
|
|
1435
|
-
} : null;
|
|
1436
|
-
const query = task?.taskId ?? project.name;
|
|
1437
|
-
const { context, memory } = await extractSeeds(opts, probe, query);
|
|
1438
|
-
const full = {
|
|
1439
|
-
project: { id: project.id, name: project.name },
|
|
1440
|
-
host,
|
|
1441
|
-
directive: "",
|
|
1442
|
-
// filled after the host payload is assembled (below)
|
|
1443
|
-
task,
|
|
1444
|
-
contextSeed: context,
|
|
1445
|
-
memorySeed: memory,
|
|
1446
|
-
degraded: {
|
|
1447
|
-
daemonDown: !probe.running,
|
|
1448
|
-
embedderMissing: context === null && probe.running
|
|
1449
|
-
}
|
|
1450
|
-
};
|
|
1451
|
-
full.directive = resolveDirective(project.root, full);
|
|
1452
|
-
if (opts.json === true) {
|
|
1453
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: full })}
|
|
1454
|
-
`);
|
|
1455
|
-
return;
|
|
1456
|
-
}
|
|
1457
|
-
const md = renderMarkdown(full);
|
|
1458
|
-
if (opts.write === true) {
|
|
1459
|
-
const path = handoffFilePath(project.root, task?.taskId, project.id);
|
|
1460
|
-
mkdirSync(join(project.root, NOIR_DIR, "handoff"), { recursive: true });
|
|
1461
|
-
writeFileSync(path, md, "utf8");
|
|
1462
|
-
info(`handoff written to ${path}`, opts);
|
|
1463
|
-
return;
|
|
1464
|
-
}
|
|
1465
|
-
process.stdout.write(md);
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
// src/commands/home.ts
|
|
1469
|
-
import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
|
|
1470
|
-
import {
|
|
1471
|
-
detectActiveMethod,
|
|
1472
|
-
latestVersionFromCache,
|
|
1473
|
-
loadProjectInfo as loadProjectInfo6,
|
|
1474
|
-
NOIR_VERSION as NOIR_VERSION3,
|
|
1475
|
-
readInstallRecord,
|
|
1476
|
-
readUpdateCache
|
|
1477
|
-
} from "@noir-ai/core";
|
|
1478
|
-
|
|
1479
|
-
// src/banner.ts
|
|
1480
|
-
import gradient from "gradient-string";
|
|
1481
|
-
var NOIR_BLOCK = [
|
|
1482
|
-
"\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
1483
|
-
"\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
|
|
1484
|
-
"\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
|
|
1485
|
-
"\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551",
|
|
1486
|
-
"\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
1487
|
-
"\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D"
|
|
1488
|
-
];
|
|
1489
|
-
var NOIR_GRADIENT = gradient("#2c5282", "#3b82f6", "#7dd3fc");
|
|
1490
|
-
var NOIR_TAGLINE = "discipline, context, and memory layer for agentic CLIs";
|
|
1491
|
-
function renderBanner(opts = {}) {
|
|
1492
|
-
const width = opts.width ?? process.stdout.columns ?? 80;
|
|
1493
|
-
const colorOn = opts.color !== false && useColor();
|
|
1494
|
-
if (width < 50) {
|
|
1495
|
-
return colorOn ? `${c.accent("\u25C6")} noir` : "\u25C6 noir";
|
|
1496
|
-
}
|
|
1497
|
-
const block = NOIR_BLOCK.join("\n");
|
|
1498
|
-
if (!colorOn) return block;
|
|
1499
|
-
if (accessibleMode()) {
|
|
1500
|
-
return block.split("\n").map((line) => c.accent(line)).join("\n");
|
|
1501
|
-
}
|
|
1502
|
-
return NOIR_GRADIENT.multiline(block);
|
|
1503
|
-
}
|
|
1504
|
-
function shouldShowBanner(opts) {
|
|
1505
|
-
if (opts.quiet === true || opts.json === true) return false;
|
|
1506
|
-
const v = process.env.NOIR_NO_BANNER;
|
|
1507
|
-
if (v !== void 0 && v !== "") return false;
|
|
1508
|
-
return true;
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
// src/tui/commands/registry.ts
|
|
1512
|
-
var DESTRUCTIVE_PREFIXES = [
|
|
1513
|
-
["context", "index"],
|
|
1514
|
-
["memory", "save"],
|
|
1515
|
-
["memory", "forget"],
|
|
1516
|
-
["memory", "consolidate"],
|
|
1517
|
-
["task", "new"],
|
|
1518
|
-
["task", "advance"],
|
|
1519
|
-
["skills", "sync"],
|
|
1520
|
-
["daemon", "start"],
|
|
1521
|
-
["daemon", "stop"],
|
|
1522
|
-
["daemon", "restart"],
|
|
1523
|
-
["init"],
|
|
1524
|
-
["create"],
|
|
1525
|
-
["sync"],
|
|
1526
|
-
["install"],
|
|
1527
|
-
["update"],
|
|
1528
|
-
["run"]
|
|
1529
|
-
];
|
|
1530
|
-
function isDestructive(argv) {
|
|
1531
|
-
return DESTRUCTIVE_PREFIXES.some((prefix) => {
|
|
1532
|
-
if (argv.length < prefix.length) return false;
|
|
1533
|
-
return prefix.every((tok, i) => argv[i] === tok);
|
|
1534
|
-
});
|
|
1535
|
-
}
|
|
1536
|
-
function capitalizeFirst(s) {
|
|
1537
|
-
if (s.length === 0) return s;
|
|
1538
|
-
return `${s[0]?.toUpperCase() ?? ""}${s.slice(1)}`;
|
|
1539
|
-
}
|
|
1540
|
-
function hasAction(cmd) {
|
|
1541
|
-
return cmd._actionHandler != null;
|
|
1542
|
-
}
|
|
1543
|
-
function collectLeaves(cmd, path, out) {
|
|
1544
|
-
const children = cmd.commands;
|
|
1545
|
-
const isLeaf = hasAction(cmd) && children.length === 0;
|
|
1546
|
-
if (isLeaf) {
|
|
1547
|
-
const argv = path;
|
|
1548
|
-
const id = argv.join(" ");
|
|
1549
|
-
const description = cmd.description() || argv.join(" ");
|
|
1550
|
-
out.push({
|
|
1551
|
-
id,
|
|
1552
|
-
label: argv.map(capitalizeFirst).join(": "),
|
|
1553
|
-
argv,
|
|
1554
|
-
description,
|
|
1555
|
-
category: argv[0] ?? "",
|
|
1556
|
-
keywords: argv,
|
|
1557
|
-
destructive: isDestructive(argv)
|
|
1558
|
-
});
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
for (const child of children) {
|
|
1562
|
-
collectLeaves(child, [...path, child.name()], out);
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
function buildPaletteCommands(program2) {
|
|
1566
|
-
const out = [];
|
|
1567
|
-
collectLeaves(program2, [], out);
|
|
1568
|
-
return out;
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
// src/tui/commands/sections.ts
|
|
1572
|
-
var HOME_SECTIONS = [
|
|
1573
|
-
{
|
|
1574
|
-
id: "status",
|
|
1575
|
-
label: "Status & context",
|
|
1576
|
-
hint: "snapshot \xB7 index \xB7 search \xB7 doctor",
|
|
1577
|
-
items: [
|
|
1578
|
-
{
|
|
1579
|
-
id: "status",
|
|
1580
|
-
label: "Status",
|
|
1581
|
-
hint: "project + daemon + workflow + store snapshot (noir status)"
|
|
1582
|
-
},
|
|
1583
|
-
{
|
|
1584
|
-
id: "context index",
|
|
1585
|
-
label: "Index project",
|
|
1586
|
-
hint: "(re)index files into context \xB7 \u26A0 destructive",
|
|
1587
|
-
destructive: true
|
|
1588
|
-
},
|
|
1589
|
-
{
|
|
1590
|
-
id: "context search",
|
|
1591
|
-
label: "Search context",
|
|
1592
|
-
hint: "hybrid search over indexed files",
|
|
1593
|
-
needsArg: { prompt: "Context search query:", placeholder: "e.g. auth flow, deploy steps" }
|
|
1594
|
-
},
|
|
1595
|
-
{
|
|
1596
|
-
id: "doctor",
|
|
1597
|
-
label: "Doctor",
|
|
1598
|
-
hint: "environment + project health checks"
|
|
1599
|
-
},
|
|
1600
|
-
{
|
|
1601
|
-
id: "context status",
|
|
1602
|
-
label: "Context status",
|
|
1603
|
-
hint: "index freshness + counts"
|
|
1604
|
-
}
|
|
1605
|
-
]
|
|
1606
|
-
},
|
|
1607
|
-
{
|
|
1608
|
-
id: "memory",
|
|
1609
|
-
label: "Memory",
|
|
1610
|
-
hint: "recall \xB7 save \xB7 sessions \xB7 forget \xB7 consolidate",
|
|
1611
|
-
items: [
|
|
1612
|
-
{
|
|
1613
|
-
id: "memory recall",
|
|
1614
|
-
label: "Recall memory",
|
|
1615
|
-
hint: "search cross-session memory",
|
|
1616
|
-
needsArg: { prompt: "Recall query:", placeholder: "e.g. auth flow, deploy steps" }
|
|
1617
|
-
},
|
|
1618
|
-
{
|
|
1619
|
-
id: "memory save",
|
|
1620
|
-
label: "Save memory",
|
|
1621
|
-
hint: "save an observation \xB7 \u26A0 destructive",
|
|
1622
|
-
destructive: true,
|
|
1623
|
-
needsArg: { prompt: "Memory content:", placeholder: "what you want to remember" }
|
|
1624
|
-
},
|
|
1625
|
-
{
|
|
1626
|
-
id: "memory sessions",
|
|
1627
|
-
label: "Sessions",
|
|
1628
|
-
hint: "list recent memory sessions"
|
|
1629
|
-
},
|
|
1630
|
-
{
|
|
1631
|
-
id: "memory forget",
|
|
1632
|
-
label: "Forget memory",
|
|
1633
|
-
hint: "forget observations by id \xB7 \u26A0 destructive",
|
|
1634
|
-
destructive: true
|
|
1635
|
-
},
|
|
1636
|
-
{
|
|
1637
|
-
id: "memory consolidate",
|
|
1638
|
-
label: "Consolidate",
|
|
1639
|
-
hint: "fold observations into lessons \xB7 \u26A0 destructive",
|
|
1640
|
-
destructive: true
|
|
1641
|
-
}
|
|
1642
|
-
]
|
|
1643
|
-
},
|
|
1644
|
-
{
|
|
1645
|
-
id: "workflow",
|
|
1646
|
-
label: "Workflow",
|
|
1647
|
-
hint: "next \xB7 status \xB7 advance \xB7 handoff \xB7 wrap",
|
|
1648
|
-
items: [
|
|
1649
|
-
{
|
|
1650
|
-
id: "task next",
|
|
1651
|
-
label: "Next task",
|
|
1652
|
-
hint: "suggest next phase + applicable skill"
|
|
1653
|
-
},
|
|
1654
|
-
{
|
|
1655
|
-
id: "task status",
|
|
1656
|
-
label: "Task status",
|
|
1657
|
-
hint: "active task status"
|
|
1658
|
-
},
|
|
1659
|
-
{
|
|
1660
|
-
id: "task advance",
|
|
1661
|
-
label: "Advance task",
|
|
1662
|
-
hint: "advance to next phase \xB7 \u26A0 destructive",
|
|
1663
|
-
destructive: true
|
|
1664
|
-
},
|
|
1665
|
-
{
|
|
1666
|
-
id: "handoff",
|
|
1667
|
-
label: "Handoff",
|
|
1668
|
-
hint: "ready-to-paste host prompt"
|
|
1669
|
-
},
|
|
1670
|
-
{
|
|
1671
|
-
id: "wrap",
|
|
1672
|
-
label: "Wrap (end session)",
|
|
1673
|
-
hint: "session-end handoff",
|
|
1674
|
-
dispatch: ["wrap", "--write"]
|
|
1675
|
-
}
|
|
1676
|
-
]
|
|
1677
|
-
},
|
|
1678
|
-
{
|
|
1679
|
-
id: "setup",
|
|
1680
|
-
label: "Setup & maintenance",
|
|
1681
|
-
hint: "init \xB7 create \xB7 sync \xB7 skills \xB7 install \xB7 update",
|
|
1682
|
-
items: [
|
|
1683
|
-
{
|
|
1684
|
-
id: "init",
|
|
1685
|
-
label: "Init",
|
|
1686
|
-
hint: "scaffold Noir in this project \xB7 \u26A0 destructive",
|
|
1687
|
-
destructive: true
|
|
1688
|
-
},
|
|
1689
|
-
{
|
|
1690
|
-
id: "create",
|
|
1691
|
-
label: "Create",
|
|
1692
|
-
hint: "bootstrap in a new/empty dir \xB7 \u26A0 destructive",
|
|
1693
|
-
destructive: true
|
|
1694
|
-
},
|
|
1695
|
-
{
|
|
1696
|
-
id: "sync",
|
|
1697
|
-
label: "Sync",
|
|
1698
|
-
hint: "re-emit managed files + skills \xB7 \u26A0 destructive",
|
|
1699
|
-
destructive: true
|
|
1700
|
-
},
|
|
1701
|
-
{
|
|
1702
|
-
id: "skills list",
|
|
1703
|
-
label: "Skills list",
|
|
1704
|
-
hint: "list installed skills"
|
|
1705
|
-
},
|
|
1706
|
-
{
|
|
1707
|
-
id: "skills sync",
|
|
1708
|
-
label: "Sync skills",
|
|
1709
|
-
hint: "re-emit builtin skills \xB7 \u26A0 destructive",
|
|
1710
|
-
destructive: true
|
|
1711
|
-
},
|
|
1712
|
-
{
|
|
1713
|
-
id: "update",
|
|
1714
|
-
label: "Update",
|
|
1715
|
-
hint: "check for a new version \xB7 \u26A0 destructive",
|
|
1716
|
-
destructive: true
|
|
1717
|
-
},
|
|
1718
|
-
{
|
|
1719
|
-
id: "install",
|
|
1720
|
-
label: "Install / migrate",
|
|
1721
|
-
hint: "native install / migrate \xB7 \u26A0 destructive",
|
|
1722
|
-
destructive: true
|
|
1723
|
-
}
|
|
1724
|
-
]
|
|
1725
|
-
},
|
|
1726
|
-
{
|
|
1727
|
-
id: "dashboard",
|
|
1728
|
-
label: "Dashboard (full-screen)",
|
|
1729
|
-
hint: "noir tui \xB7 noir palette \xB7 home",
|
|
1730
|
-
items: [
|
|
1731
|
-
{
|
|
1732
|
-
id: "tui",
|
|
1733
|
-
label: "Dashboard (full-screen)",
|
|
1734
|
-
hint: "live status \xB7 /command \xB7 Ctrl+K palette (noir tui)"
|
|
1735
|
-
},
|
|
1736
|
-
{
|
|
1737
|
-
id: "palette",
|
|
1738
|
-
label: "All commands (fuzzy palette)",
|
|
1739
|
-
hint: "fuzzy search over every command (noir palette)"
|
|
1740
|
-
}
|
|
1741
|
-
]
|
|
1742
|
-
}
|
|
1743
|
-
];
|
|
1744
|
-
async function resolveSections(commands) {
|
|
1745
|
-
const byId = new Map(commands.map((c2) => [c2.id, c2]));
|
|
1746
|
-
const out = [];
|
|
1747
|
-
for (const section of HOME_SECTIONS) {
|
|
1748
|
-
const items = [];
|
|
1749
|
-
for (const action of section.items) {
|
|
1750
|
-
const entry = byId.get(action.id);
|
|
1751
|
-
if (!entry) continue;
|
|
1752
|
-
items.push({
|
|
1753
|
-
...action,
|
|
1754
|
-
// Resolve the dispatch argv from the registry (never hand-written).
|
|
1755
|
-
dispatch: action.dispatch ?? [...entry.argv]
|
|
1756
|
-
});
|
|
1757
|
-
}
|
|
1758
|
-
if (items.length === 0) continue;
|
|
1759
|
-
out.push({ ...section, items });
|
|
1760
|
-
}
|
|
1761
|
-
return out;
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
// src/commands/home.ts
|
|
1765
|
-
function tryProject() {
|
|
1766
|
-
try {
|
|
1767
|
-
const info2 = loadProjectInfo6(process.cwd());
|
|
1768
|
-
return { id: info2.id, name: info2.name, host: info2.config.host };
|
|
1769
|
-
} catch {
|
|
1770
|
-
return null;
|
|
1771
|
-
}
|
|
1772
|
-
}
|
|
1773
|
-
function shouldShowMigrationBanner(rec, currentVersion) {
|
|
1774
|
-
if (rec.method === "native") return false;
|
|
1775
|
-
if (rec.dismissedVersions?.includes(currentVersion)) return false;
|
|
1776
|
-
return true;
|
|
1777
|
-
}
|
|
1778
|
-
var COMMANDS_HINT = "Commands: status \xB7 context \xB7 memory \xB7 task \xB7 skills \xB7 daemon \xB7 doctor \xB7 install \xB7 update \xB7 tui \xB7 palette";
|
|
1779
|
-
async function home(opts, deps) {
|
|
1780
|
-
if (isInteractive(opts) && opts.tui !== false) {
|
|
1781
|
-
await runMenu(opts, deps);
|
|
1782
|
-
return;
|
|
1783
|
-
}
|
|
1784
|
-
if (opts.json === true) {
|
|
1785
|
-
await deps.dispatch(["status", "--json"]);
|
|
1786
|
-
return;
|
|
1787
|
-
}
|
|
1788
|
-
await deps.dispatch(["status"]);
|
|
1789
|
-
}
|
|
1790
|
-
async function pickSection(clack, sections, opts) {
|
|
1791
|
-
const result = await clack.select({
|
|
1792
|
-
message: "What would you like to do?",
|
|
1793
|
-
options: [
|
|
1794
|
-
...sections.map((s) => ({
|
|
1795
|
-
value: s.id,
|
|
1796
|
-
label: s.label,
|
|
1797
|
-
hint: s.hint
|
|
1798
|
-
})),
|
|
1799
|
-
{ value: "exit", label: "Exit", hint: "leave the home menu" }
|
|
1800
|
-
]
|
|
1801
|
-
});
|
|
1802
|
-
if (clack.isCancel(result)) {
|
|
1803
|
-
clack.cancel("Cancelled.");
|
|
1804
|
-
fail(EXIT.CANCELLED, "cancelled", opts);
|
|
1805
|
-
}
|
|
1806
|
-
return result;
|
|
1807
|
-
}
|
|
1808
|
-
async function pickAction(clack, section, sections, opts) {
|
|
1809
|
-
const idx = sections.findIndex((s) => s.id === section.id);
|
|
1810
|
-
const hasPrev = idx > 0;
|
|
1811
|
-
const hasNext = idx !== -1 && idx < sections.length - 1;
|
|
1812
|
-
const result = await clack.select({
|
|
1813
|
-
message: section.label,
|
|
1814
|
-
initialValue: section.items[0]?.id,
|
|
1815
|
-
options: [
|
|
1816
|
-
...section.items.map((a) => ({
|
|
1817
|
-
value: a.id,
|
|
1818
|
-
label: a.label,
|
|
1819
|
-
hint: a.hint
|
|
1820
|
-
})),
|
|
1821
|
-
// Navigation options so the user can move across sections without
|
|
1822
|
-
// re-entering the section picker (the "lancar" navigation requirement).
|
|
1823
|
-
...hasPrev ? [{ value: "prev", label: "\u2190 Prev section", hint: sections[idx - 1]?.label }] : [],
|
|
1824
|
-
...hasNext ? [{ value: "next", label: "\u2192 Next section", hint: sections[idx + 1]?.label }] : [],
|
|
1825
|
-
{ value: "back", label: "\u2191 Back to sections", hint: "return to the section picker" }
|
|
1826
|
-
]
|
|
1827
|
-
});
|
|
1828
|
-
if (clack.isCancel(result)) {
|
|
1829
|
-
clack.cancel("Back to the home menu.");
|
|
1830
|
-
return "back";
|
|
1831
|
-
}
|
|
1832
|
-
if (opts.verbose) {
|
|
1833
|
-
process.stderr.write(`noir: home: section '${section.id}' selected
|
|
1834
|
-
`);
|
|
1835
|
-
}
|
|
1836
|
-
return result;
|
|
1837
|
-
}
|
|
1838
|
-
async function collectArg(clack, action, opts) {
|
|
1839
|
-
if (!action.needsArg) return null;
|
|
1840
|
-
const value = await clack.text({
|
|
1841
|
-
message: action.needsArg.prompt,
|
|
1842
|
-
placeholder: action.needsArg.placeholder,
|
|
1843
|
-
validate: (v) => !v || v.trim().length === 0 ? "Please enter a value." : void 0
|
|
1844
|
-
});
|
|
1845
|
-
if (clack.isCancel(value)) {
|
|
1846
|
-
clack.cancel("Cancelled.");
|
|
1847
|
-
fail(EXIT.CANCELLED, "cancelled", opts);
|
|
1848
|
-
}
|
|
1849
|
-
return String(value);
|
|
1850
|
-
}
|
|
1851
|
-
async function argvForAction(clack, action, opts) {
|
|
1852
|
-
const base = action.dispatch ?? [action.id];
|
|
1853
|
-
let argv = [...base];
|
|
1854
|
-
if (action.needsArg) {
|
|
1855
|
-
const arg = await collectArg(clack, action, opts);
|
|
1856
|
-
if (arg === null) return null;
|
|
1857
|
-
argv = [...base, arg];
|
|
1858
|
-
}
|
|
1859
|
-
if (action.destructive || isDestructive(argv)) {
|
|
1860
|
-
const confirmed = await clack.confirm({
|
|
1861
|
-
message: `Run \`noir ${argv.join(" ")}\`? This may modify project files / the store.`,
|
|
1862
|
-
initialValue: false
|
|
1863
|
-
});
|
|
1864
|
-
if (clack.isCancel(confirmed) || !confirmed) return null;
|
|
1865
|
-
}
|
|
1866
|
-
return argv;
|
|
1867
|
-
}
|
|
1868
|
-
async function runGroupedMenu(clack, opts, deps, sections) {
|
|
1869
|
-
let currentSection = null;
|
|
1870
|
-
while (true) {
|
|
1871
|
-
if (currentSection === null) {
|
|
1872
|
-
const picked = await pickSection(clack, sections, opts);
|
|
1873
|
-
if (clack.isCancel(picked) || picked === "exit") {
|
|
1874
|
-
clack.outro("bye");
|
|
1875
|
-
return;
|
|
1876
|
-
}
|
|
1877
|
-
const idx = sections.findIndex((s) => s.id === picked);
|
|
1878
|
-
if (idx === -1) {
|
|
1879
|
-
continue;
|
|
1880
|
-
}
|
|
1881
|
-
currentSection = idx;
|
|
1882
|
-
continue;
|
|
1883
|
-
}
|
|
1884
|
-
const section = sections[currentSection];
|
|
1885
|
-
if (!section) {
|
|
1886
|
-
currentSection = null;
|
|
1887
|
-
continue;
|
|
1888
|
-
}
|
|
1889
|
-
const chosen = await pickAction(clack, section, sections, opts);
|
|
1890
|
-
if (clack.isCancel(chosen)) {
|
|
1891
|
-
currentSection = null;
|
|
1892
|
-
continue;
|
|
1893
|
-
}
|
|
1894
|
-
switch (chosen) {
|
|
1895
|
-
case "back": {
|
|
1896
|
-
currentSection = null;
|
|
1897
|
-
continue;
|
|
1898
|
-
}
|
|
1899
|
-
case "next": {
|
|
1900
|
-
currentSection = (currentSection + 1) % sections.length;
|
|
1901
|
-
continue;
|
|
1902
|
-
}
|
|
1903
|
-
case "prev": {
|
|
1904
|
-
currentSection = (currentSection - 1 + sections.length) % sections.length;
|
|
1905
|
-
continue;
|
|
1906
|
-
}
|
|
1907
|
-
default: {
|
|
1908
|
-
const action = section.items.find((a) => a.id === chosen);
|
|
1909
|
-
if (!action) {
|
|
1910
|
-
currentSection = null;
|
|
1911
|
-
continue;
|
|
1912
|
-
}
|
|
1913
|
-
const argv = await argvForAction(clack, action, opts);
|
|
1914
|
-
if (argv === null) {
|
|
1915
|
-
continue;
|
|
1916
|
-
}
|
|
1917
|
-
await deps.dispatch(argv);
|
|
1918
|
-
clack.outro("done");
|
|
1919
|
-
return;
|
|
1920
|
-
}
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
async function runMenu(opts, deps) {
|
|
1925
|
-
const clack = await import("@clack/prompts");
|
|
1926
|
-
const project = tryProject();
|
|
1927
|
-
void runAsyncUpdateCheck({
|
|
1928
|
-
env: process.env,
|
|
1929
|
-
configUpdate: (() => {
|
|
1930
|
-
try {
|
|
1931
|
-
const info2 = loadProjectInfo6(process.cwd());
|
|
1932
|
-
return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
|
|
1933
|
-
} catch {
|
|
1934
|
-
return DEFAULT_UPDATE_CONFIG;
|
|
1935
|
-
}
|
|
1936
|
-
})(),
|
|
1937
|
-
quiet: opts.json === true || opts.quiet === true || !process.stdout.isTTY
|
|
1938
|
-
});
|
|
1939
|
-
if (shouldShowBanner(opts)) {
|
|
1940
|
-
const host = project?.host ?? "claude";
|
|
1941
|
-
process.stderr.write(
|
|
1942
|
-
`
|
|
1943
|
-
${renderBanner()}
|
|
1944
|
-
${NOIR_TAGLINE}
|
|
1945
|
-
|
|
1946
|
-
${hostLaunchDirective2(host)}
|
|
1947
|
-
${COMMANDS_HINT}
|
|
1948
|
-
|
|
1949
|
-
`
|
|
1950
|
-
);
|
|
1951
|
-
}
|
|
1952
|
-
const rec = readInstallRecord();
|
|
1953
|
-
if (rec && shouldShowMigrationBanner(rec, NOIR_VERSION3)) {
|
|
1954
|
-
process.stderr.write(
|
|
1955
|
-
`
|
|
1956
|
-
${c.warn(`noir installed via ${rec.method}`)} \u2014 consider \`noir install\` for the native path (auto-update, no npm prefix/PATH issues). Dismiss with: \`noir install --dismiss\` (persisted per version).
|
|
1957
|
-
|
|
1958
|
-
`
|
|
1959
|
-
);
|
|
1960
|
-
}
|
|
1961
|
-
const updateNotice = buildUpdateNotice({
|
|
1962
|
-
method: detectActiveMethod(),
|
|
1963
|
-
currentVersion: NOIR_VERSION3,
|
|
1964
|
-
latestKnown: latestVersionFromCache(readUpdateCache(), "latest")
|
|
1965
|
-
});
|
|
1966
|
-
if (updateNotice) {
|
|
1967
|
-
process.stderr.write(
|
|
1968
|
-
`
|
|
1969
|
-
${c.warn(`noir ${updateNotice.latestVersion} is available`)} (you have ${updateNotice.currentVersion}) \u2014 ${updateNotice.advice}.
|
|
1970
|
-
|
|
1971
|
-
`
|
|
1972
|
-
);
|
|
1973
|
-
}
|
|
1974
|
-
clack.intro(project ? `noir \u2014 ${project.name}` : "noir");
|
|
1975
|
-
const sections = await resolveSections(deps.commands ?? []);
|
|
1976
|
-
await runGroupedMenu(clack, opts, deps, sections);
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
// src/commands/memory.ts
|
|
1980
|
-
function parseLimit2(raw, label, opts) {
|
|
1981
|
-
if (raw === void 0) return void 0;
|
|
1982
|
-
const n = Number(raw);
|
|
1983
|
-
if (!Number.isInteger(n) || n <= 0) {
|
|
1984
|
-
fail(EXIT.USAGE, `${label}: --limit must be a positive integer (got '${raw}')`, opts);
|
|
1985
|
-
}
|
|
1986
|
-
return n;
|
|
1987
|
-
}
|
|
1988
|
-
function splitCsv(raw) {
|
|
1989
|
-
if (raw === void 0) return void 0;
|
|
1990
|
-
const out = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1991
|
-
return out.length > 0 ? out : void 0;
|
|
1992
|
-
}
|
|
1993
|
-
function toHit2(raw) {
|
|
1994
|
-
const h = raw ?? {};
|
|
1995
|
-
const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
1996
|
-
return {
|
|
1997
|
-
id: typeof h.id === "string" ? h.id : "",
|
|
1998
|
-
type: typeof h.type === "string" ? h.type : "",
|
|
1999
|
-
score: typeof h.score === "number" ? h.score : 0,
|
|
2000
|
-
content: typeof h.content === "string" ? h.content : "",
|
|
2001
|
-
concepts: arr(h.concepts),
|
|
2002
|
-
files: arr(h.files),
|
|
2003
|
-
ts: typeof h.ts === "number" ? h.ts : 0,
|
|
2004
|
-
importance: typeof h.importance === "number" ? h.importance : 0,
|
|
2005
|
-
source: typeof h.source === "string" ? h.source : ""
|
|
2006
|
-
};
|
|
2007
|
-
}
|
|
2008
|
-
function failTool2(label, envelope, opts) {
|
|
2009
|
-
const detail = envelope.error ?? envelope.reason ?? "unknown failure";
|
|
2010
|
-
fail(EXIT.ERROR, `${label}: ${detail}`, opts);
|
|
2011
|
-
}
|
|
2012
|
-
function stamp(ts) {
|
|
2013
|
-
if (typeof ts !== "number" || ts <= 0) return "-";
|
|
2014
|
-
return new Date(ts).toISOString().replace("T", " ").replace(/\..*$/, "Z");
|
|
2015
|
-
}
|
|
2016
|
-
async function memoryRecall(opts) {
|
|
2017
|
-
const limit = parseLimit2(opts.limit, "memory recall", opts);
|
|
2018
|
-
let probe;
|
|
2019
|
-
try {
|
|
2020
|
-
probe = await probeDaemon(opts);
|
|
2021
|
-
} catch {
|
|
2022
|
-
probe = { running: true };
|
|
2023
|
-
}
|
|
2024
|
-
if (!probe.running) {
|
|
2025
|
-
await withInProcessRead(opts, async (engines) => {
|
|
2026
|
-
const result = await engines.memory.recall(opts.query, {
|
|
2027
|
-
...limit === void 0 ? {} : { limit }
|
|
2028
|
-
});
|
|
2029
|
-
const data2 = { query: opts.query, hits: result, degraded: false };
|
|
2030
|
-
if (opts.json === true) {
|
|
2031
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
|
|
2032
|
-
`);
|
|
2033
|
-
return;
|
|
2034
|
-
}
|
|
2035
|
-
renderRecall(data2.query, result, data2.degraded, opts);
|
|
2036
|
-
});
|
|
2037
|
-
return;
|
|
2038
|
-
}
|
|
2039
|
-
const res = await callDaemonTool(opts, "memory_recall", {
|
|
2040
|
-
query: opts.query,
|
|
2041
|
-
...limit === void 0 ? {} : { limit }
|
|
2042
|
-
});
|
|
2043
|
-
if (res.ok !== true) failTool2("memory recall", res, opts);
|
|
2044
|
-
const hits = Array.isArray(res.results) ? res.results.map(toHit2) : [];
|
|
2045
|
-
const data = { query: opts.query, hits, degraded: res.degraded === true };
|
|
2046
|
-
if (opts.json === true) {
|
|
2047
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
2048
|
-
`);
|
|
2049
|
-
return;
|
|
2050
|
-
}
|
|
2051
|
-
renderRecall(data.query, hits, data.degraded, opts);
|
|
2052
|
-
}
|
|
2053
|
-
function renderRecall(query, hits, degraded, opts) {
|
|
2054
|
-
const flag = degraded ? ` ${badge("warn", "degraded: BM25-only")}` : "";
|
|
2055
|
-
log(
|
|
2056
|
-
`memory recall \u2014 ${hits.length} hit${hits.length === 1 ? "" : "s"} for '${query}'${flag}`,
|
|
2057
|
-
opts
|
|
2058
|
-
);
|
|
2059
|
-
if (hits.length === 0) {
|
|
2060
|
-
info("(no memories matched)", opts);
|
|
2061
|
-
return;
|
|
2062
|
-
}
|
|
2063
|
-
for (let i = 0; i < hits.length; i++) {
|
|
2064
|
-
const h = hits[i];
|
|
2065
|
-
if (h === void 0) continue;
|
|
2066
|
-
const head = `[${i + 1}] ${h.type} \xB7 score ${h.score.toFixed(4)} \xB7 importance ${h.importance.toFixed(2)} \xB7 ${h.id}`;
|
|
2067
|
-
log(head, opts);
|
|
2068
|
-
if (h.files.length > 0) log(` files: ${h.files.join(", ")}`, opts);
|
|
2069
|
-
if (h.concepts.length > 0) log(` tags: ${h.concepts.join(", ")}`, opts);
|
|
2070
|
-
log(` ${h.content}`, opts);
|
|
2071
|
-
}
|
|
2072
|
-
}
|
|
2073
|
-
async function memorySave(opts) {
|
|
2074
|
-
const content = await resolveContent(opts);
|
|
2075
|
-
const files = splitCsv(opts.files);
|
|
2076
|
-
const args = { content };
|
|
2077
|
-
if (typeof opts.type === "string" && opts.type.length > 0) args.type = opts.type;
|
|
2078
|
-
if (files !== void 0) args.files = files;
|
|
2079
|
-
const res = await callDaemonTool(opts, "memory_save", args);
|
|
2080
|
-
if (res.ok !== true) failTool2("memory save", res, opts);
|
|
2081
|
-
const id = typeof res.id === "string" ? res.id : "";
|
|
2082
|
-
const observation = res.observation ?? {};
|
|
2083
|
-
const data = { id, observation };
|
|
2084
|
-
if (opts.json === true) {
|
|
2085
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
2086
|
-
`);
|
|
2087
|
-
return;
|
|
2088
|
-
}
|
|
2089
|
-
log(`Saved memory ${id}.`, opts);
|
|
2090
|
-
renderObservation(observation, opts);
|
|
2091
|
-
}
|
|
2092
|
-
async function resolveContent(opts) {
|
|
2093
|
-
if (typeof opts.content === "string" && opts.content.length > 0) return opts.content;
|
|
2094
|
-
if (!isInteractive(opts)) {
|
|
2095
|
-
fail(
|
|
2096
|
-
EXIT.USAGE,
|
|
2097
|
-
"memory save requires --content <text> (or re-run in an interactive terminal to be prompted)",
|
|
2098
|
-
opts
|
|
2099
|
-
);
|
|
2100
|
-
}
|
|
2101
|
-
const clack = await import("@clack/prompts");
|
|
2102
|
-
const value = await clack.text({
|
|
2103
|
-
message: "Memory to save:",
|
|
2104
|
-
placeholder: "the insight / decision / pattern to remember",
|
|
2105
|
-
validate: (v) => !v || v.trim().length === 0 ? "content cannot be empty" : void 0
|
|
2106
|
-
});
|
|
2107
|
-
if (clack.isCancel(value)) {
|
|
2108
|
-
clack.cancel("Cancelled.");
|
|
2109
|
-
fail(EXIT.CANCELLED, "cancelled", opts);
|
|
2110
|
-
}
|
|
2111
|
-
return String(value);
|
|
2112
|
-
}
|
|
2113
|
-
function renderObservation(obs, opts) {
|
|
2114
|
-
const rows = [];
|
|
2115
|
-
for (const key of ["id", "type", "importance", "ts", "source", "project", "sessionId"]) {
|
|
2116
|
-
if (obs[key] !== void 0) rows.push({ label: key, value: obs[key] });
|
|
2117
|
-
}
|
|
2118
|
-
if (rows.length > 0) definitionList(rows, opts);
|
|
2119
|
-
const content = obs.content;
|
|
2120
|
-
if (typeof content === "string") log(`
|
|
2121
|
-
${content}`, opts);
|
|
2122
|
-
}
|
|
2123
|
-
async function memorySessions(opts) {
|
|
2124
|
-
let probe;
|
|
2125
|
-
try {
|
|
2126
|
-
probe = await probeDaemon(opts);
|
|
2127
|
-
} catch {
|
|
2128
|
-
probe = { running: true };
|
|
2129
|
-
}
|
|
2130
|
-
if (!probe.running) {
|
|
2131
|
-
await withInProcessRead(opts, async (engines) => {
|
|
2132
|
-
const raw = engines.memory.sessions();
|
|
2133
|
-
const sessions2 = raw.map((s) => ({
|
|
2134
|
-
id: s.id,
|
|
2135
|
-
count: s.count,
|
|
2136
|
-
lastTs: s.lastTs
|
|
2137
|
-
}));
|
|
2138
|
-
const data2 = { sessions: sessions2 };
|
|
2139
|
-
if (opts.json === true) {
|
|
2140
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
|
|
2141
|
-
`);
|
|
2142
|
-
return;
|
|
2143
|
-
}
|
|
2144
|
-
renderSessions(sessions2, opts);
|
|
2145
|
-
});
|
|
2146
|
-
return;
|
|
2147
|
-
}
|
|
2148
|
-
const res = await callDaemonTool(opts, "memory_sessions");
|
|
2149
|
-
if (res.ok !== true) failTool2("memory sessions", res, opts);
|
|
2150
|
-
const sessions = Array.isArray(res.sessions) ? res.sessions.map((raw) => {
|
|
2151
|
-
const s = raw ?? {};
|
|
2152
|
-
return {
|
|
2153
|
-
id: typeof s.id === "string" ? s.id : "",
|
|
2154
|
-
count: typeof s.count === "number" ? s.count : 0,
|
|
2155
|
-
lastTs: typeof s.lastTs === "number" ? s.lastTs : 0
|
|
2156
|
-
};
|
|
2157
|
-
}) : [];
|
|
2158
|
-
const data = { sessions };
|
|
2159
|
-
if (opts.json === true) {
|
|
2160
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
2161
|
-
`);
|
|
2162
|
-
return;
|
|
2163
|
-
}
|
|
2164
|
-
renderSessions(sessions, opts);
|
|
2165
|
-
}
|
|
2166
|
-
function renderSessions(sessions, opts) {
|
|
2167
|
-
log(`memory sessions \u2014 ${sessions.length} session${sessions.length === 1 ? "" : "s"}`, opts);
|
|
2168
|
-
table(
|
|
2169
|
-
sessions.map((s) => ({ Session: s.id, Observations: s.count, "Last seen": stamp(s.lastTs) })),
|
|
2170
|
-
["Session", "Observations", "Last seen"],
|
|
2171
|
-
opts
|
|
2172
|
-
);
|
|
2173
|
-
}
|
|
2174
|
-
async function memoryForget(opts) {
|
|
2175
|
-
if (opts.ids.length === 0) {
|
|
2176
|
-
fail(EXIT.USAGE, "memory forget requires at least one <id>", opts);
|
|
2177
|
-
}
|
|
2178
|
-
const res = await callDaemonTool(opts, "memory_forget", {
|
|
2179
|
-
ids: opts.ids
|
|
2180
|
-
});
|
|
2181
|
-
if (res.ok !== true) failTool2("memory forget", res, opts);
|
|
2182
|
-
const deleted = typeof res.deleted === "number" ? res.deleted : 0;
|
|
2183
|
-
const data = { deleted, ids: opts.ids };
|
|
2184
|
-
if (opts.json === true) {
|
|
2185
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
2186
|
-
`);
|
|
2187
|
-
return;
|
|
2188
|
-
}
|
|
2189
|
-
log(`Forgot ${deleted} observation${deleted === 1 ? "" : "s"}.`, opts);
|
|
2190
|
-
}
|
|
2191
|
-
async function memoryConsolidate(opts) {
|
|
2192
|
-
const exposed = await withDaemon(opts, async (caller) => caller.listTools());
|
|
2193
|
-
if (!exposed.includes("memory_consolidate")) {
|
|
2194
|
-
fail(
|
|
2195
|
-
EXIT.ERROR,
|
|
2196
|
-
"memory consolidate: the daemon does not expose the memory_consolidate tool. Enable it in .noir/config under memory.consolidation (enabled: true + a provider + model), then restart the daemon.",
|
|
2197
|
-
opts
|
|
2198
|
-
);
|
|
2199
|
-
}
|
|
2200
|
-
const types = splitCsv(opts.types);
|
|
2201
|
-
const limit = parseLimit2(opts.limit, "memory consolidate", opts);
|
|
2202
|
-
const args = {};
|
|
2203
|
-
if (types !== void 0) args.types = types;
|
|
2204
|
-
if (limit !== void 0) args.limit = limit;
|
|
2205
|
-
const res = await callDaemonTool(
|
|
2206
|
-
opts,
|
|
2207
|
-
"memory_consolidate",
|
|
2208
|
-
args
|
|
2209
|
-
);
|
|
2210
|
-
if (res.ok === true) {
|
|
2211
|
-
const lessons = Array.isArray(res.lessons) ? res.lessons : [];
|
|
2212
|
-
const from = Array.isArray(res.from) ? res.from : [];
|
|
2213
|
-
const data = { lessons, from };
|
|
2214
|
-
if (opts.json === true) {
|
|
2215
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
2216
|
-
`);
|
|
2217
|
-
return;
|
|
2218
|
-
}
|
|
2219
|
-
log(
|
|
2220
|
-
`Consolidated ${lessons.length} lesson${lessons.length === 1 ? "" : "s"} from ${from.length} observation${from.length === 1 ? "" : "s"}.`,
|
|
2221
|
-
opts
|
|
2222
|
-
);
|
|
2223
|
-
for (let i = 0; i < lessons.length; i++) {
|
|
2224
|
-
const l = lessons[i];
|
|
2225
|
-
if (l === void 0) continue;
|
|
2226
|
-
const obs = l ?? {};
|
|
2227
|
-
const id = typeof obs.id === "string" ? obs.id : `<lesson ${i + 1}>`;
|
|
2228
|
-
const content = typeof obs.content === "string" ? obs.content : "";
|
|
2229
|
-
log(`- ${id}: ${content}`, opts);
|
|
2230
|
-
}
|
|
2231
|
-
return;
|
|
2232
|
-
}
|
|
2233
|
-
const env = res;
|
|
2234
|
-
const reason = (typeof env.reason === "string" ? env.reason : void 0) ?? (typeof env.error === "string" ? env.error : void 0) ?? "unknown";
|
|
2235
|
-
const logged = env.logged === true || env.degraded === true;
|
|
2236
|
-
const suffix = logged ? " (logged)" : "";
|
|
2237
|
-
fail(EXIT.ERROR, `memory consolidate did not run: ${reason}${suffix}`, opts);
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
// src/commands/release.ts
|
|
2241
|
-
import { execSync, spawnSync as spawnSync2 } from "child_process";
|
|
2242
|
-
function describe(step, who) {
|
|
2243
|
-
const m = {
|
|
2244
|
-
preflight: "check clean tree, on develop, HEAD pushed, gate green, no version collision",
|
|
2245
|
-
bump: "run bump-version.mjs + update CHANGELOG + roadmap docs",
|
|
2246
|
-
gate: "re-run the full gate (lint/build/typecheck/test/docs:validate)",
|
|
2247
|
-
commit: "commit chore(release) + push develop",
|
|
2248
|
-
"ci-develop": "wait for CI on develop \u2192 green",
|
|
2249
|
-
"beta-tag": "pnpm release:tag \u2192 beta tag + push",
|
|
2250
|
-
"human-approve-beta": "approve the release workflow publish job in GitHub Actions",
|
|
2251
|
-
"merge-main": "merge develop \u2192 main (fast-forward) + push",
|
|
2252
|
-
"ci-main": "wait for CI on main \u2192 green",
|
|
2253
|
-
"stable-tag": "pnpm release:tag \u2192 stable tag + push",
|
|
2254
|
-
"human-approve-stable": "approve the release workflow publish job in GitHub Actions (again)",
|
|
2255
|
-
"dist-bump": "update Homebrew (packaging/homebrew/noir.rb) + Scoop (packaging/scoop/noir.json) from npm",
|
|
2256
|
-
sync: "merge main \u2192 develop + verify both branches at same SHA"
|
|
2257
|
-
};
|
|
2258
|
-
return `${m[step]} [${who}]`;
|
|
2259
|
-
}
|
|
2260
|
-
var ALL = [
|
|
2261
|
-
"preflight",
|
|
2262
|
-
"bump",
|
|
2263
|
-
"gate",
|
|
2264
|
-
"commit",
|
|
2265
|
-
"ci-develop",
|
|
2266
|
-
"beta-tag",
|
|
2267
|
-
"human-approve-beta",
|
|
2268
|
-
"merge-main",
|
|
2269
|
-
"ci-main",
|
|
2270
|
-
"stable-tag",
|
|
2271
|
-
"human-approve-stable",
|
|
2272
|
-
"dist-bump",
|
|
2273
|
-
"sync"
|
|
2274
|
-
];
|
|
2275
|
-
async function release(opts) {
|
|
2276
|
-
if (opts.dryRun) {
|
|
2277
|
-
log("noir release --dry-run (no changes will be made)", opts);
|
|
2278
|
-
for (const s of ALL) {
|
|
2279
|
-
const who = s.startsWith("human-approve") ? "human" : "noir";
|
|
2280
|
-
info(`${s}: ${describe(s, who)}`, opts);
|
|
2281
|
-
}
|
|
2282
|
-
tip('full release flow documented in CLAUDE.md "Patch release flow (beta \u2192 stable)"', opts);
|
|
2283
|
-
return;
|
|
2284
|
-
}
|
|
2285
|
-
const version = opts.version?.trim();
|
|
2286
|
-
if (!version) {
|
|
2287
|
-
fail(EXIT.USAGE, "release: <version> is required (e.g. noir release 1.10.0)", opts);
|
|
2288
|
-
}
|
|
2289
|
-
const channel = opts.channel === "stable" ? "stable" : "beta";
|
|
2290
|
-
log(`noir release ${version} (${channel} channel)`, opts);
|
|
2291
|
-
try {
|
|
2292
|
-
info(`[1/13] preflight: ${describe("preflight", "noir")}`, opts);
|
|
2293
|
-
execSync("git diff --quiet", { encoding: "utf8" });
|
|
2294
|
-
const branch = execSync("git branch --show-current", { encoding: "utf8" }).trim();
|
|
2295
|
-
if (branch !== "develop") fail(EXIT.ERROR, "release: must be on develop", opts);
|
|
2296
|
-
const status2 = execSync("git status --porcelain", { encoding: "utf8" }).trim();
|
|
2297
|
-
if (status2) fail(EXIT.ERROR, "release: tree must be clean", opts);
|
|
2298
|
-
success("preflight: \u2713 clean, on develop", opts);
|
|
2299
|
-
info(`[2/13] bump: ${describe("bump", "noir")}`, opts);
|
|
2300
|
-
spawnSync2("node", ["scripts/bump-version.mjs", version], {
|
|
2301
|
-
encoding: "utf8",
|
|
2302
|
-
stdio: "inherit"
|
|
2303
|
-
});
|
|
2304
|
-
success(`bump: \u2713 version ${version}`, opts);
|
|
2305
|
-
info(`[3/13] gate: ${describe("gate", "noir")}`, opts);
|
|
2306
|
-
execSync("pnpm lint && pnpm build && pnpm typecheck && pnpm test && pnpm docs:validate", {
|
|
2307
|
-
encoding: "utf8",
|
|
2308
|
-
stdio: "inherit"
|
|
2309
|
-
});
|
|
2310
|
-
success("gate: \u2713 full gate green", opts);
|
|
2311
|
-
info(`[4/13] commit/push: ${describe("commit", "noir")}`, opts);
|
|
2312
|
-
spawnSync2("git", ["add", "-A"], { encoding: "utf8", stdio: "inherit" });
|
|
2313
|
-
spawnSync2("git", ["commit", "-m", `chore(release): v${version} + docs sync`], {
|
|
2314
|
-
encoding: "utf8",
|
|
2315
|
-
stdio: "inherit"
|
|
2316
|
-
});
|
|
2317
|
-
execSync("git push origin develop", { encoding: "utf8" });
|
|
2318
|
-
success("commit: \u2713 pushed to develop", opts);
|
|
2319
|
-
info(`[5/13] ci-develop: waiting for CI on develop\u2026`, opts);
|
|
2320
|
-
info(`[6/13] beta-tag: ${describe("beta-tag", "noir")}`, opts);
|
|
2321
|
-
tip("after CI green: run `pnpm release:tag` and push the beta tag", opts);
|
|
2322
|
-
warn(`[7/13] ${describe("human-approve-beta", "human")}`, opts);
|
|
2323
|
-
tip("open GitHub Actions \u2192 approve the publish job, then continue", opts);
|
|
2324
|
-
tip("after approval: verify `npm view @noir-ai/cli dist-tags beta` = beta version", opts);
|
|
2325
|
-
info("remaining steps (merge\u2192stable\u2192dist\u2192sync): see CLAUDE.md", opts);
|
|
2326
|
-
for (const s of ALL.slice(7)) {
|
|
2327
|
-
const who = s.startsWith("human-approve") ? "human" : "noir";
|
|
2328
|
-
info(` ${s}: ${describe(s, who)}`, opts);
|
|
2329
|
-
}
|
|
2330
|
-
success(`release ${version}: preflight\u2192bump\u2192gate\u2192commit done. Resume after approval.`, opts);
|
|
2331
|
-
} catch (err) {
|
|
2332
|
-
fail(EXIT.ERROR, `release failed: ${err instanceof Error ? err.message : String(err)}`, opts);
|
|
2333
|
-
}
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
// src/commands/run.ts
|
|
2337
|
-
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
2338
|
-
import { join as join2 } from "path";
|
|
2339
|
-
import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
|
|
2340
|
-
|
|
2341
|
-
// src/orchestrator.ts
|
|
2342
|
-
import { spawn } from "child_process";
|
|
2343
|
-
import { createInterface } from "readline";
|
|
2344
|
-
var HOST_BINARY = {
|
|
2345
|
-
claude: "claude",
|
|
2346
|
-
gemini: "gemini",
|
|
2347
|
-
opencode: "opencode",
|
|
2348
|
-
cursor: "cursor-agent",
|
|
2349
|
-
"agents-md": null
|
|
2350
|
-
// emits AGENTS.md — a file host, not a spawnable CLI
|
|
2351
|
-
};
|
|
2352
|
-
var HOST_FLAGS = {
|
|
2353
|
-
claude: ["-p", "--output-format", "stream-json", "--verbose"],
|
|
2354
|
-
gemini: ["-p", "--output-format", "stream-json"],
|
|
2355
|
-
opencode: ["run"],
|
|
2356
|
-
cursor: ["-p"],
|
|
2357
|
-
"agents-md": []
|
|
2358
|
-
};
|
|
2359
|
-
function resolveHostRun(host, customBinary) {
|
|
2360
|
-
const binary = customBinary && customBinary.length > 0 ? customBinary : HOST_BINARY[host];
|
|
2361
|
-
if (!binary) return null;
|
|
2362
|
-
return { binary, flags: HOST_FLAGS[host] };
|
|
2363
|
-
}
|
|
2364
|
-
function asObj(v) {
|
|
2365
|
-
return typeof v === "object" && v !== null ? v : null;
|
|
2366
|
-
}
|
|
2367
|
-
function str(v) {
|
|
2368
|
-
return typeof v === "string" ? v : void 0;
|
|
2369
|
-
}
|
|
2370
|
-
function num(v) {
|
|
2371
|
-
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
2372
|
-
}
|
|
2373
|
-
function usageFrom(v) {
|
|
2374
|
-
const o = asObj(v);
|
|
2375
|
-
if (!o) return void 0;
|
|
2376
|
-
const input = num(o.input_tokens);
|
|
2377
|
-
const output = num(o.output_tokens);
|
|
2378
|
-
const cache = num(o.cache_read_input_tokens) ?? num(o.cache_creation_input_tokens);
|
|
2379
|
-
if (input === void 0 && output === void 0 && cache === void 0) return void 0;
|
|
2380
|
-
return { inputTokens: input, outputTokens: output, cacheReadTokens: cache };
|
|
2381
|
-
}
|
|
2382
|
-
function messageText(message) {
|
|
2383
|
-
if (!message) return void 0;
|
|
2384
|
-
const content = message.content;
|
|
2385
|
-
if (typeof content === "string") return content;
|
|
2386
|
-
if (Array.isArray(content)) {
|
|
2387
|
-
const parts = content.map((block) => {
|
|
2388
|
-
const b = asObj(block);
|
|
2389
|
-
return b && typeof b.text === "string" ? b.text : null;
|
|
2390
|
-
}).filter((t) => t !== null);
|
|
2391
|
-
if (parts.length > 0) return parts.join("");
|
|
2392
|
-
}
|
|
2393
|
-
return void 0;
|
|
2394
|
-
}
|
|
2395
|
-
function parseStreamLine(line) {
|
|
2396
|
-
const trimmed = line.trim();
|
|
2397
|
-
if (trimmed.length === 0) return null;
|
|
2398
|
-
try {
|
|
2399
|
-
return JSON.parse(trimmed);
|
|
2400
|
-
} catch {
|
|
2401
|
-
return null;
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
function normalizeStreamEvent(raw) {
|
|
2405
|
-
const r = asObj(raw);
|
|
2406
|
-
if (!r) return null;
|
|
2407
|
-
const type = r.type;
|
|
2408
|
-
if (type === "system") {
|
|
2409
|
-
if (r.subtype === "init") {
|
|
2410
|
-
return { kind: "init", sessionId: str(r.session_id), model: str(r.model) };
|
|
2411
|
-
}
|
|
2412
|
-
return { kind: "other" };
|
|
2413
|
-
}
|
|
2414
|
-
if (type === "assistant") {
|
|
2415
|
-
const message = asObj(r.message);
|
|
2416
|
-
return {
|
|
2417
|
-
kind: "assistant",
|
|
2418
|
-
messageId: message ? str(message.id) : void 0,
|
|
2419
|
-
text: messageText(message),
|
|
2420
|
-
usage: message ? usageFrom(message.usage) : void 0
|
|
2421
|
-
};
|
|
2422
|
-
}
|
|
2423
|
-
if (type === "result") {
|
|
2424
|
-
return {
|
|
2425
|
-
kind: "result",
|
|
2426
|
-
isError: r.is_error === true,
|
|
2427
|
-
durationMs: num(r.duration_ms),
|
|
2428
|
-
numTurns: num(r.num_turns),
|
|
2429
|
-
totalCostUsd: num(r.total_cost_usd),
|
|
2430
|
-
usage: usageFrom(r.usage)
|
|
2431
|
-
};
|
|
2432
|
-
}
|
|
2433
|
-
return { kind: "other" };
|
|
2434
|
-
}
|
|
2435
|
-
var UsageReducer = class {
|
|
2436
|
-
maxByMessage = /* @__PURE__ */ new Map();
|
|
2437
|
-
input = 0;
|
|
2438
|
-
output = 0;
|
|
2439
|
-
costUsd = 0;
|
|
2440
|
-
turns = 0;
|
|
2441
|
-
/** Feed one normalized event into the reducer. */
|
|
2442
|
-
add(event) {
|
|
2443
|
-
if (event.kind === "assistant" && event.messageId && event.usage) {
|
|
2444
|
-
const key = event.messageId;
|
|
2445
|
-
const input = event.usage.inputTokens ?? 0;
|
|
2446
|
-
const output = event.usage.outputTokens ?? 0;
|
|
2447
|
-
const prev = this.maxByMessage.get(key);
|
|
2448
|
-
if (!prev) {
|
|
2449
|
-
this.maxByMessage.set(key, { input, output });
|
|
2450
|
-
this.input += input;
|
|
2451
|
-
this.output += output;
|
|
2452
|
-
} else {
|
|
2453
|
-
this.input += Math.max(0, input - prev.input);
|
|
2454
|
-
this.output += Math.max(0, output - prev.output);
|
|
2455
|
-
this.maxByMessage.set(key, {
|
|
2456
|
-
input: Math.max(prev.input, input),
|
|
2457
|
-
output: Math.max(prev.output, output)
|
|
2458
|
-
});
|
|
2459
|
-
}
|
|
2460
|
-
} else if (event.kind === "result") {
|
|
2461
|
-
if (event.totalCostUsd !== void 0) this.costUsd = event.totalCostUsd;
|
|
2462
|
-
if (event.numTurns !== void 0) this.turns = event.numTurns;
|
|
2463
|
-
}
|
|
2464
|
-
}
|
|
2465
|
-
/** The current accumulator snapshot. */
|
|
2466
|
-
snapshot() {
|
|
2467
|
-
return {
|
|
2468
|
-
inputTokens: this.input,
|
|
2469
|
-
outputTokens: this.output,
|
|
2470
|
-
totalCostUsd: this.costUsd,
|
|
2471
|
-
numTurns: this.turns
|
|
2472
|
-
};
|
|
2473
|
-
}
|
|
2474
|
-
};
|
|
2475
|
-
function runHost(opts) {
|
|
2476
|
-
const spec = resolveHostRun(opts.host, opts.customBinary);
|
|
2477
|
-
return new Promise((resolve, reject) => {
|
|
2478
|
-
if (!spec) {
|
|
2479
|
-
reject(new Error(`host '${opts.host}' is not a spawnable CLI (no default command)`));
|
|
2480
|
-
return;
|
|
2481
|
-
}
|
|
2482
|
-
const child = spawn(spec.binary, [...spec.flags, opts.prompt], {
|
|
2483
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2484
|
-
});
|
|
2485
|
-
const reducer = new UsageReducer();
|
|
2486
|
-
let eventCount = 0;
|
|
2487
|
-
let stderrBuf = "";
|
|
2488
|
-
child.on("error", (err) => reject(err));
|
|
2489
|
-
const rl = createInterface({ input: child.stdout });
|
|
2490
|
-
rl.on("line", (line) => {
|
|
2491
|
-
opts.onLine?.(line);
|
|
2492
|
-
const raw = parseStreamLine(line);
|
|
2493
|
-
if (raw === null) return;
|
|
2494
|
-
const event = normalizeStreamEvent(raw);
|
|
2495
|
-
if (event === null) return;
|
|
2496
|
-
eventCount += 1;
|
|
2497
|
-
reducer.add(event);
|
|
2498
|
-
opts.onEvent?.(event);
|
|
2499
|
-
});
|
|
2500
|
-
child.stderr.setEncoding("utf8");
|
|
2501
|
-
child.stderr.on("data", (chunk) => {
|
|
2502
|
-
stderrBuf += chunk;
|
|
2503
|
-
});
|
|
2504
|
-
child.on("close", (code) => {
|
|
2505
|
-
resolve({ exitCode: code ?? 0, usage: reducer.snapshot(), eventCount, stderr: stderrBuf });
|
|
2506
|
-
});
|
|
2507
|
-
});
|
|
2508
|
-
}
|
|
2509
|
-
|
|
2510
|
-
// src/commands/run.ts
|
|
2511
|
-
function formatUsage(u) {
|
|
2512
|
-
const cost = u.totalCostUsd > 0 ? ` $${u.totalCostUsd.toFixed(2)}` : "";
|
|
2513
|
-
const turns = u.numTurns > 0 ? ` \xB7 ${u.numTurns} turns` : "";
|
|
2514
|
-
return `${u.inputTokens.toLocaleString()} in / ${u.outputTokens.toLocaleString()} out${cost}${turns}`;
|
|
2515
|
-
}
|
|
2516
|
-
function streamEvent(event, opts) {
|
|
2517
|
-
if (opts.json === true) return;
|
|
2518
|
-
if (event.kind === "assistant" && event.text && event.text.length > 0) {
|
|
2519
|
-
process.stdout.write(event.text);
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
|
-
async function run(prompt, opts) {
|
|
2523
|
-
const host = opts.host ?? "claude";
|
|
2524
|
-
if (!SUPPORTED_HOSTS.includes(host)) {
|
|
2525
|
-
fail(EXIT.USAGE, `unknown host '${host}' (supported: ${SUPPORTED_HOSTS.join(", ")})`, opts);
|
|
2526
|
-
}
|
|
2527
|
-
if (prompt.length === 0) {
|
|
2528
|
-
fail(EXIT.USAGE, "a prompt is required: `noir run <prompt>`", opts);
|
|
2529
|
-
}
|
|
2530
|
-
const transcriptLines = [];
|
|
2531
|
-
try {
|
|
2532
|
-
const result = await runHost({
|
|
2533
|
-
host,
|
|
2534
|
-
prompt,
|
|
2535
|
-
customBinary: opts.command,
|
|
2536
|
-
onLine: (line) => transcriptLines.push(line),
|
|
2537
|
-
onEvent: (event) => streamEvent(event, opts)
|
|
2538
|
-
});
|
|
2539
|
-
if (opts.json !== true) {
|
|
2540
|
-
process.stdout.write("\n");
|
|
2541
|
-
}
|
|
2542
|
-
const transcript = writeTranscript(host, transcriptLines);
|
|
2543
|
-
if (opts.json === true) {
|
|
2544
|
-
json({
|
|
2545
|
-
ok: true,
|
|
2546
|
-
data: {
|
|
2547
|
-
host,
|
|
2548
|
-
prompt,
|
|
2549
|
-
exitCode: result.exitCode,
|
|
2550
|
-
usage: result.usage,
|
|
2551
|
-
numTurns: result.usage.numTurns,
|
|
2552
|
-
events: result.eventCount,
|
|
2553
|
-
transcript
|
|
2554
|
-
}
|
|
2555
|
-
});
|
|
2556
|
-
return;
|
|
2557
|
-
}
|
|
2558
|
-
if (result.exitCode !== 0) {
|
|
2559
|
-
log(`host exited ${result.exitCode}`, opts);
|
|
2560
|
-
if (result.stderr.length > 0) log(result.stderr.trim(), opts);
|
|
2561
|
-
}
|
|
2562
|
-
success(`usage: ${formatUsage(result.usage)} (API-equivalent estimate, not billed)`, opts);
|
|
2563
|
-
log(`transcript: ${transcript}`, opts);
|
|
2564
|
-
} catch (err) {
|
|
2565
|
-
fail(
|
|
2566
|
-
EXIT.ERROR,
|
|
2567
|
-
`failed to run host '${host}': ${err instanceof Error ? err.message : String(err)}`,
|
|
2568
|
-
opts
|
|
2569
|
-
);
|
|
2570
|
-
}
|
|
2571
|
-
}
|
|
2572
|
-
function writeTranscript(host, lines) {
|
|
2573
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2574
|
-
const dir = join2(process.cwd(), ".noir", "transcripts");
|
|
2575
|
-
const file = join2(dir, `${host}-${ts}.jsonl`);
|
|
2576
|
-
try {
|
|
2577
|
-
mkdirSync2(dir, { recursive: true });
|
|
2578
|
-
writeFileSync2(file, `${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`);
|
|
2579
|
-
} catch {
|
|
2580
|
-
return "(not persisted)";
|
|
2581
|
-
}
|
|
2582
|
-
return file;
|
|
2583
|
-
}
|
|
2584
|
-
|
|
2585
|
-
// src/commands/skills.ts
|
|
2586
|
-
import { claudeAdapter } from "@noir-ai/adapters";
|
|
2587
|
-
import { loadProjectInfo as loadProjectInfo7 } from "@noir-ai/core";
|
|
2588
|
-
import {
|
|
2589
|
-
buildRegistry,
|
|
2590
|
-
discoverAll,
|
|
2591
|
-
emitSkillsToDir,
|
|
2592
|
-
lintSkill
|
|
2593
|
-
} from "@noir-ai/skills";
|
|
2594
|
-
var CATEGORY = {
|
|
2595
|
-
"noir-brainstorming": "discovery",
|
|
2596
|
-
"noir-sync": "discovery",
|
|
2597
|
-
"noir-checkpoint": "discovery",
|
|
2598
|
-
"noir-exploring": "discovery",
|
|
2599
|
-
"noir-spec": "spec",
|
|
2600
|
-
"noir-planning": "plan",
|
|
2601
|
-
"noir-prd": "plan",
|
|
2602
|
-
"noir-executing-plans": "execute",
|
|
2603
|
-
"noir-test-driven-development": "execute",
|
|
2604
|
-
"noir-systematic-debugging": "execute",
|
|
2605
|
-
"noir-subagent": "execute",
|
|
2606
|
-
"noir-parallel": "execute",
|
|
2607
|
-
"noir-verifying": "verify",
|
|
2608
|
-
"noir-security": "verify",
|
|
2609
|
-
"noir-wrap": "document",
|
|
2610
|
-
"noir-readme": "document",
|
|
2611
|
-
"noir-shipping": "git",
|
|
2612
|
-
"noir-worktree": "git",
|
|
2613
|
-
"noir-recall": "memory",
|
|
2614
|
-
"noir-remember": "memory",
|
|
2615
|
-
"noir-context": "context",
|
|
2616
|
-
"noir-frontend": "domain",
|
|
2617
|
-
"noir-backend": "domain",
|
|
2618
|
-
"noir-doctor": "meta",
|
|
2619
|
-
"noir-writing-skills": "meta",
|
|
2620
|
-
"noir-rules": "meta"
|
|
2621
|
-
};
|
|
2622
|
-
function categoryOf(name) {
|
|
2623
|
-
const mapped = CATEGORY[name];
|
|
2624
|
-
if (mapped !== void 0) return mapped;
|
|
2625
|
-
return name.replace(/^noir-/, "") || "general";
|
|
2626
|
-
}
|
|
2627
|
-
function toRow(s, kind = "builtin") {
|
|
2628
|
-
const description = typeof s.frontmatter.description === "string" ? s.frontmatter.description : "";
|
|
2629
|
-
const status2 = s.skillMd.includes("> **Stub:**") ? "stub" : "full";
|
|
2630
|
-
const category = s.frontmatter.metadata?.category?.trim() || categoryOf(s.name);
|
|
2631
|
-
return { name: s.name, category, description, kind, status: status2 };
|
|
2632
|
-
}
|
|
2633
|
-
function truncate(text, max) {
|
|
2634
|
-
if (text.length <= max) return text;
|
|
2635
|
-
const slice = text.slice(0, Math.max(0, max - 1));
|
|
2636
|
-
const boundary = slice.lastIndexOf(" ");
|
|
2637
|
-
const head = boundary > 0 ? slice.slice(0, boundary) : slice;
|
|
2638
|
-
return `${head.trimEnd()}\u2026`;
|
|
2639
|
-
}
|
|
2640
|
-
async function skillsList(opts) {
|
|
2641
|
-
let builtins;
|
|
2642
|
-
let integrations;
|
|
2643
|
-
try {
|
|
2644
|
-
const all = discoverAll();
|
|
2645
|
-
builtins = all.builtins;
|
|
2646
|
-
integrations = all.integrations;
|
|
2647
|
-
} catch (err) {
|
|
2648
|
-
fail(
|
|
2649
|
-
EXIT.ERROR,
|
|
2650
|
-
`skills list: could not discover skills (${err instanceof Error ? err.message : String(err)})`,
|
|
2651
|
-
opts
|
|
2652
|
-
);
|
|
2653
|
-
}
|
|
2654
|
-
const rows = [
|
|
2655
|
-
...builtins.map((b) => toRow(b, "builtin")),
|
|
2656
|
-
...integrations.map((i) => toRow(i, "integration"))
|
|
2657
|
-
];
|
|
2658
|
-
const data = { count: rows.length, skills: rows };
|
|
2659
|
-
if (opts.json === true) {
|
|
2660
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
2661
|
-
`);
|
|
2662
|
-
return;
|
|
2663
|
-
}
|
|
2664
|
-
const intCount = integrations.length;
|
|
2665
|
-
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"}`;
|
|
2666
|
-
log(banner, opts);
|
|
2667
|
-
table(
|
|
2668
|
-
rows.map((r) => ({
|
|
2669
|
-
Skill: r.name,
|
|
2670
|
-
Kind: r.kind,
|
|
2671
|
-
Category: r.category,
|
|
2672
|
-
Status: r.status,
|
|
2673
|
-
Description: truncate(r.description, 80)
|
|
2674
|
-
})),
|
|
2675
|
-
["Skill", "Kind", "Category", "Status", "Description"],
|
|
2676
|
-
opts
|
|
2677
|
-
);
|
|
2678
|
-
}
|
|
2679
|
-
async function skillsSync(opts) {
|
|
2680
|
-
const root = process.cwd();
|
|
2681
|
-
const project = loadProjectInfo7(root);
|
|
2682
|
-
if (!claudeAdapter.skillsDir) {
|
|
2683
|
-
const data2 = {
|
|
2684
|
-
emitted: [],
|
|
2685
|
-
references: 0,
|
|
2686
|
-
host: project.config.host,
|
|
2687
|
-
reason: "no-skill-emitter"
|
|
2688
|
-
};
|
|
2689
|
-
if (opts.json === true) {
|
|
2690
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
|
|
2691
|
-
`);
|
|
2692
|
-
return;
|
|
2693
|
-
}
|
|
2694
|
-
info("This host has no skill emitter; nothing to sync.", opts);
|
|
2695
|
-
return;
|
|
2696
|
-
}
|
|
2697
|
-
const dir = claudeAdapter.skillsDir({ root });
|
|
2698
|
-
const summary = await emitSkillsToDir(dir);
|
|
2699
|
-
const pruned = summary.pruned ?? [];
|
|
2700
|
-
const data = {
|
|
2701
|
-
emitted: summary.emitted,
|
|
2702
|
-
references: summary.references,
|
|
2703
|
-
dir,
|
|
2704
|
-
pruned
|
|
2705
|
-
};
|
|
2706
|
-
if (opts.json === true) {
|
|
2707
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
2708
|
-
`);
|
|
2709
|
-
return;
|
|
2710
|
-
}
|
|
2711
|
-
log(
|
|
2712
|
-
`Synced ${summary.emitted.length} Noir skill${summary.emitted.length === 1 ? "" : "s"} to ${dir}.`,
|
|
2713
|
-
opts
|
|
2714
|
-
);
|
|
2715
|
-
if (pruned.length > 0) {
|
|
2716
|
-
warn(
|
|
2717
|
-
`Pruned ${pruned.length} stale noir-* skill dir${pruned.length === 1 ? "" : "s"}: ${pruned.join(", ")}`,
|
|
2718
|
-
opts
|
|
2719
|
-
);
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
async function skillsLint(opts) {
|
|
2723
|
-
let skills;
|
|
2724
|
-
try {
|
|
2725
|
-
const all = discoverAll();
|
|
2726
|
-
skills = [...all.builtins, ...all.integrations].map(lintSkill);
|
|
2727
|
-
} catch (err) {
|
|
2728
|
-
fail(
|
|
2729
|
-
EXIT.ERROR,
|
|
2730
|
-
`skills lint: could not lint skills (${err instanceof Error ? err.message : String(err)})`,
|
|
2731
|
-
opts
|
|
2732
|
-
);
|
|
2733
|
-
}
|
|
2734
|
-
const errored = skills.filter((s) => s.errors.length > 0);
|
|
2735
|
-
const warned = skills.filter((s) => s.warnings.length > 0 && s.errors.length === 0);
|
|
2736
|
-
const data = { count: skills.length, errored: errored.length, skills };
|
|
2737
|
-
if (opts.json === true) {
|
|
2738
|
-
process.stdout.write(`${JSON.stringify({ ok: errored.length === 0, data })}
|
|
2739
|
-
`);
|
|
2740
|
-
return;
|
|
2741
|
-
}
|
|
2742
|
-
if (errored.length === 0) {
|
|
2743
|
-
log(`noir skills lint \u2014 ${skills.length} skills, all validate clean.`, opts);
|
|
2744
|
-
} else {
|
|
2745
|
-
warn(
|
|
2746
|
-
`noir skills lint \u2014 ${errored.length} skill${errored.length === 1 ? "" : "s"} FAIL validation:`,
|
|
2747
|
-
opts
|
|
2748
|
-
);
|
|
2749
|
-
}
|
|
2750
|
-
for (const s of errored) {
|
|
2751
|
-
for (const e of s.errors) warn(` ${s.name}: ${e}`, opts);
|
|
2752
|
-
}
|
|
2753
|
-
if (warned.length > 0) {
|
|
2754
|
-
info(` ${warned.length} skill${warned.length === 1 ? "" : "s"} carry lint warnings:`, opts);
|
|
2755
|
-
for (const s of warned) {
|
|
2756
|
-
for (const w of s.warnings) info(` ${s.name}: ${w}`, opts);
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
}
|
|
2760
|
-
async function skillsRegistry(opts) {
|
|
2761
|
-
let reg;
|
|
2762
|
-
try {
|
|
2763
|
-
reg = buildRegistry();
|
|
2764
|
-
} catch (err) {
|
|
2765
|
-
fail(
|
|
2766
|
-
EXIT.ERROR,
|
|
2767
|
-
`skills registry: could not build registry (${err instanceof Error ? err.message : String(err)})`,
|
|
2768
|
-
opts
|
|
2769
|
-
);
|
|
2770
|
-
}
|
|
2771
|
-
const data = { count: reg.length, skills: reg };
|
|
2772
|
-
if (opts.json === true) {
|
|
2773
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
2774
|
-
`);
|
|
2775
|
-
return;
|
|
2776
|
-
}
|
|
2777
|
-
log(`noir skills registry \u2014 ${reg.length} skills.`, opts);
|
|
2778
|
-
table(
|
|
2779
|
-
reg.map((r) => ({
|
|
2780
|
-
Skill: r.name,
|
|
2781
|
-
Kind: r.kind,
|
|
2782
|
-
Category: r.category,
|
|
2783
|
-
Version: r.version,
|
|
2784
|
-
Status: r.status,
|
|
2785
|
-
Refs: String(r.referenceCount),
|
|
2786
|
-
Lines: String(r.lines)
|
|
2787
|
-
})),
|
|
2788
|
-
["Skill", "Kind", "Category", "Version", "Status", "Refs", "Lines"],
|
|
2789
|
-
opts
|
|
2790
|
-
);
|
|
2791
|
-
}
|
|
2792
|
-
|
|
2793
|
-
// src/serve.ts
|
|
2794
|
-
import { loadProjectInfo as loadProjectInfo8 } from "@noir-ai/core";
|
|
2795
|
-
import { ensureDaemonRunning as ensureDaemonRunning3, startStdioServer } from "@noir-ai/daemon";
|
|
2796
|
-
async function serve(opts) {
|
|
2797
|
-
const project = loadProjectInfo8(process.cwd());
|
|
2798
|
-
if (opts.stdio) {
|
|
2799
|
-
await startStdioServer({ project, transport: "stdio", daemon: false });
|
|
2800
|
-
return;
|
|
2801
|
-
}
|
|
2802
|
-
try {
|
|
2803
|
-
const { url } = await ensureDaemonRunning3({
|
|
2804
|
-
project,
|
|
2805
|
-
idleTimeoutSec: project.config.daemon.idleTimeoutSec
|
|
2806
|
-
});
|
|
2807
|
-
process.stderr.write(
|
|
2808
|
-
`Noir daemon available at ${url}. (For HTTP clients, use this URL in .mcp.json.)
|
|
2809
|
-
`
|
|
2810
|
-
);
|
|
2811
|
-
} catch (err) {
|
|
2812
|
-
process.stderr.write(
|
|
2813
|
-
`Noir daemon unavailable (${err instanceof Error ? err.message : String(err)}); falling back to stdio.
|
|
2814
|
-
`
|
|
2815
|
-
);
|
|
2816
|
-
await startStdioServer({ project, transport: "stdio", daemon: false });
|
|
2817
|
-
}
|
|
2818
|
-
}
|
|
14
|
+
c,
|
|
15
|
+
contentWidth,
|
|
16
|
+
divider,
|
|
17
|
+
useColor
|
|
18
|
+
} from "./chunk-ESZTOQEC.js";
|
|
2819
19
|
|
|
2820
|
-
// src/
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
}
|
|
2824
|
-
function actionGlobals(args) {
|
|
2825
|
-
const last = args[args.length - 1];
|
|
2826
|
-
return last?.optsWithGlobals() ?? {};
|
|
2827
|
-
}
|
|
2828
|
-
function trailingCmd(args) {
|
|
2829
|
-
const last = args[args.length - 1];
|
|
2830
|
-
if (!(last instanceof Command)) {
|
|
2831
|
-
throw new NoirCliError(
|
|
2832
|
-
EXIT.ERROR,
|
|
2833
|
-
"internal: commander action callback received no trailing Command"
|
|
2834
|
-
);
|
|
2835
|
-
}
|
|
2836
|
-
return last;
|
|
2837
|
-
}
|
|
2838
|
-
function toCliOptions(g) {
|
|
2839
|
-
return {
|
|
2840
|
-
json: g.json === true,
|
|
2841
|
-
quiet: g.quiet === true,
|
|
2842
|
-
verbose: g.verbose === true,
|
|
2843
|
-
input: g.input !== false,
|
|
2844
|
-
// TUI policy — additive only. `--tui`/`--no-tui` land on `tui`
|
|
2845
|
-
// (true/false; absent when neither flag is given). Spread conditionally so
|
|
2846
|
-
// the default-args case (no flag) does NOT add a `tui` key — keeps the
|
|
2847
|
-
// exact-shape assertions in bin.test.ts green.
|
|
2848
|
-
...g.tui === true || g.tui === false ? { tui: g.tui === true } : {},
|
|
2849
|
-
// `--no-tips` is stored under `tips` (default true; flag ⇒ false). Map to
|
|
2850
|
-
// the additive `noTips` spelling used by CliOptions + `tip()`.
|
|
2851
|
-
...g.tips === false ? { noTips: true } : {}
|
|
2852
|
-
};
|
|
2853
|
-
}
|
|
2854
|
-
function toStatusOptions(g) {
|
|
2855
|
-
return {
|
|
2856
|
-
json: g.json === true,
|
|
2857
|
-
quiet: g.quiet === true,
|
|
2858
|
-
verbose: g.verbose === true,
|
|
2859
|
-
input: g.input !== false
|
|
2860
|
-
};
|
|
2861
|
-
}
|
|
2862
|
-
function parseHost(raw) {
|
|
2863
|
-
if (raw === void 0) return void 0;
|
|
2864
|
-
return raw;
|
|
2865
|
-
}
|
|
2866
|
-
var DEPRECATIONS = [];
|
|
2867
|
-
function commandPath(cmd) {
|
|
2868
|
-
const path = [];
|
|
2869
|
-
let c2 = cmd;
|
|
2870
|
-
while (c2 !== null) {
|
|
2871
|
-
path.unshift(c2.name());
|
|
2872
|
-
c2 = c2.parent;
|
|
2873
|
-
}
|
|
2874
|
-
return path;
|
|
2875
|
-
}
|
|
2876
|
-
function emitDeprecationHintsFor(cmd, opts) {
|
|
2877
|
-
if (DEPRECATIONS.length === 0) return;
|
|
2878
|
-
const rel = commandPath(cmd).slice(1);
|
|
2879
|
-
for (const d of DEPRECATIONS) {
|
|
2880
|
-
if (d.oldArgv.every((a, i) => rel[i] === a)) {
|
|
2881
|
-
tip(
|
|
2882
|
-
`\`noir ${d.oldArgv.join(" ")}\` is deprecated since v${d.since}; use \`noir ${d.newArgv.join(" ")}\`.`,
|
|
2883
|
-
opts
|
|
2884
|
-
);
|
|
2885
|
-
}
|
|
2886
|
-
}
|
|
2887
|
-
}
|
|
2888
|
-
function createProgram() {
|
|
2889
|
-
const program2 = new Command();
|
|
2890
|
-
program2.name("noir").description("Noir \u2014 discipline, context, and memory layer for agentic CLIs.").version(NOIR_VERSION4, "-v, --version").addOption(new Option("--json", "emit machine-readable JSON to stdout")).addOption(new Option("--no-input", "never prompt; error if input is required")).addOption(new Option("--quiet", "suppress non-essential diagnostics")).addOption(new Option("--verbose", "show additional diagnostic detail")).addOption(new Option("--cwd <path>", "run as if started in <path>")).addOption(
|
|
2891
|
-
new Option("--tui", "prefer the interactive home menu for bare `noir` (advisory; TTY-only)")
|
|
2892
|
-
).addOption(
|
|
2893
|
-
new Option(
|
|
2894
|
-
"--no-tui",
|
|
2895
|
-
"route bare `noir` to the non-interactive `status` path even in a TTY"
|
|
2896
|
-
)
|
|
2897
|
-
).addOption(new Option("--no-tips", "suppress redirect / deprecation hints on stderr")).exitOverride((err) => {
|
|
2898
|
-
throw err;
|
|
2899
|
-
}).configureOutput({
|
|
2900
|
-
// Stream discipline: --help/--version → stdout (conventional; users pipe
|
|
2901
|
-
// them), errors → stderr. --json data is emitted by command actions via
|
|
2902
|
-
// process.stdout directly, NOT by commander's writeOut, so routing help/
|
|
2903
|
-
// version to stdout does not conflict with --json payloads.
|
|
2904
|
-
writeOut: (str2) => process.stdout.write(str2),
|
|
2905
|
-
writeErr: (str2) => process.stderr.write(str2)
|
|
2906
|
-
});
|
|
2907
|
-
program2.hook("preAction", (_thisCmd, actionCmd) => {
|
|
2908
|
-
const opts = actionCmd.optsWithGlobals();
|
|
2909
|
-
const cwd = opts.cwd;
|
|
2910
|
-
if (typeof cwd === "string" && cwd.length > 0) {
|
|
2911
|
-
try {
|
|
2912
|
-
process.chdir(cwd);
|
|
2913
|
-
} catch (err) {
|
|
2914
|
-
throw new NoirCliError(
|
|
2915
|
-
EXIT.USAGE,
|
|
2916
|
-
`--cwd: ${err instanceof Error ? err.message : String(err)}`
|
|
2917
|
-
);
|
|
2918
|
-
}
|
|
2919
|
-
}
|
|
2920
|
-
const nonInteractive = opts.json === true || opts.input === false;
|
|
2921
|
-
if (nonInteractive) process.env.NOIR_NON_INTERACTIVE = "1";
|
|
2922
|
-
else delete process.env.NOIR_NON_INTERACTIVE;
|
|
2923
|
-
emitDeprecationHintsFor(actionCmd, toCliOptions(opts));
|
|
2924
|
-
});
|
|
2925
|
-
program2.command("init").description("scaffold Noir in the current project (.noir/, .mcp.json, CLAUDE.md, skills)").option("--transport <transport>", "stdio | streamable-http (default: stdio)", "stdio").option("--url <url>", "streamable-http daemon URL (localhost only)").option(
|
|
2926
|
-
"--upgrade",
|
|
2927
|
-
"run scaffold migrations before re-emitting (re-run on an existing project)"
|
|
2928
|
-
).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(
|
|
2929
|
-
// S10: target host. Defaults to `'claude'` (the regression anchor). The
|
|
2930
|
-
// chosen host is forwarded to scaffold() + skills emission via
|
|
2931
|
-
// resolveAdapter. The choice list mirrors `SUPPORTED_HOSTS` so a new host
|
|
2932
|
-
// lands here automatically; commander rejects anything else as usage=2.
|
|
2933
|
-
new Option(
|
|
2934
|
-
"--host <id>",
|
|
2935
|
-
"target agentic CLI (default: claude) \u2014 drives host-side emission"
|
|
2936
|
-
).choices(SUPPORTED_HOSTS2)
|
|
2937
|
-
).action(
|
|
2938
|
-
async (opts, cmd) => {
|
|
2939
|
-
const transport = opts.transport === "streamable-http" ? "streamable-http" : "stdio";
|
|
2940
|
-
const upgrade = opts.upgrade === true;
|
|
2941
|
-
const force = opts.force === true;
|
|
2942
|
-
const host = parseHost(opts.host);
|
|
2943
|
-
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
2944
|
-
const result = await init(process.cwd(), {
|
|
2945
|
-
transport,
|
|
2946
|
-
url: opts.url,
|
|
2947
|
-
...upgrade ? { upgrade } : {},
|
|
2948
|
-
...force ? { force } : {},
|
|
2949
|
-
...host !== void 0 ? { host } : {},
|
|
2950
|
-
...dryRun ? { dryRun } : {}
|
|
2951
|
-
});
|
|
2952
|
-
if (cmd.optsWithGlobals().json === true && result !== void 0) {
|
|
2953
|
-
json({ ok: true, data: result });
|
|
2954
|
-
}
|
|
2955
|
-
}
|
|
2956
|
-
);
|
|
2957
|
-
program2.command("create [dir]").description("bootstrap the Noir AI layer in a new or empty directory").option("--transport <transport>", "stdio | streamable-http (default: stdio)", "stdio").option("--url <url>", "streamable-http daemon URL (localhost only)").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(
|
|
2958
|
-
new Option(
|
|
2959
|
-
"--host <id>",
|
|
2960
|
-
"target agentic CLI (default: claude) \u2014 drives host-side emission"
|
|
2961
|
-
).choices(SUPPORTED_HOSTS2)
|
|
2962
|
-
).action(
|
|
2963
|
-
async (dir, opts, cmd) => {
|
|
2964
|
-
const transport = opts.transport === "streamable-http" ? "streamable-http" : "stdio";
|
|
2965
|
-
const force = opts.force === true;
|
|
2966
|
-
const host = parseHost(opts.host);
|
|
2967
|
-
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
2968
|
-
const { create } = await import("./create-WKFVM237.js");
|
|
2969
|
-
const result = await create(dir, {
|
|
2970
|
-
transport,
|
|
2971
|
-
url: opts.url,
|
|
2972
|
-
...force ? { force } : {},
|
|
2973
|
-
...host !== void 0 ? { host } : {},
|
|
2974
|
-
...dryRun ? { dryRun } : {}
|
|
2975
|
-
});
|
|
2976
|
-
if (cmd.optsWithGlobals().json === true && result !== void 0) {
|
|
2977
|
-
json({ ok: true, data: result });
|
|
2978
|
-
}
|
|
2979
|
-
}
|
|
2980
|
-
);
|
|
2981
|
-
program2.command("sync").description(
|
|
2982
|
-
"re-emit Noir managed files (.mcp.json, CLAUDE.md blocks, NOIR.md brief, ignores) + skills"
|
|
2983
|
-
).option(
|
|
2984
|
-
"--force",
|
|
2985
|
-
"overwrite differing regenerated files without prompting (bypasses the conflict menu)"
|
|
2986
|
-
).option(
|
|
2987
|
-
"--merge",
|
|
2988
|
-
"three-way merge managed regions (default since 1.3.0; flag kept for compatibility)"
|
|
2989
|
-
).option("--dry-run", "report planned writes without writing anything").option("--preview", "alias for --dry-run").addOption(
|
|
2990
|
-
// Opt OUT of managed-region merge (restore strip-replace). Commander
|
|
2991
|
-
// stores `--no-merge-regions` under `mergeRegions` (default true; flag →
|
|
2992
|
-
// false). Bare `noir sync` keeps the merge default (true).
|
|
2993
|
-
new Option(
|
|
2994
|
-
"--no-merge-regions",
|
|
2995
|
-
"strip-replace managed regions (discard hand-edits inside <!-- noir:* --> markers)"
|
|
2996
|
-
)
|
|
2997
|
-
).addOption(
|
|
2998
|
-
// S10: optional `--host` override. When omitted, sync reads host from
|
|
2999
|
-
// `.noir/config.yml` (whatever `noir init --host <id>` persisted). The
|
|
3000
|
-
// override is rarely needed — documented as advanced.
|
|
3001
|
-
new Option(
|
|
3002
|
-
"--host <id>",
|
|
3003
|
-
"override the configured host (advanced; default reads .noir/config.yml)"
|
|
3004
|
-
).choices(SUPPORTED_HOSTS2)
|
|
3005
|
-
).action(
|
|
3006
|
-
async (opts, cmd) => {
|
|
3007
|
-
const { sync } = await import("./sync-DUWJAF7Z.js");
|
|
3008
|
-
const host = parseHost(opts.host);
|
|
3009
|
-
const force = opts.force === true;
|
|
3010
|
-
const merge = opts.merge === true;
|
|
3011
|
-
const noMergeRegions = opts.mergeRegions === false;
|
|
3012
|
-
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
3013
|
-
const result = host === void 0 && !force && !merge && !noMergeRegions && !dryRun ? await sync(process.cwd()) : await sync(process.cwd(), {
|
|
3014
|
-
...host !== void 0 ? { host } : {},
|
|
3015
|
-
...force ? { force } : {},
|
|
3016
|
-
...merge ? { merge } : {},
|
|
3017
|
-
...noMergeRegions ? { mergeManagedRegions: false } : {},
|
|
3018
|
-
...dryRun ? { dryRun } : {}
|
|
3019
|
-
});
|
|
3020
|
-
if (cmd.optsWithGlobals().json === true) {
|
|
3021
|
-
json({ ok: true, data: result });
|
|
3022
|
-
}
|
|
3023
|
-
}
|
|
3024
|
-
);
|
|
3025
|
-
const mcpCmd = program2.command("mcp").description("MCP server control");
|
|
3026
|
-
mcpCmd.command("serve").description("run the Noir MCP server (stdio, or via the shared daemon)").option("--stdio", "force the stdio transport").action(async (opts) => {
|
|
3027
|
-
await serve({ stdio: opts.stdio === true });
|
|
3028
|
-
});
|
|
3029
|
-
mcpCmd.action(() => {
|
|
3030
|
-
throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio]");
|
|
3031
|
-
});
|
|
3032
|
-
const daemonGrp = program2.command("daemon").description("control the Noir daemon");
|
|
3033
|
-
daemonGrp.command("start").description("start the Noir daemon (foreground, or background with --detach)").option("--detach", "run the daemon in the background and exit").addOption(
|
|
3034
|
-
new Option("--_detached-child", "reserved: detached daemon child (internal)").hideHelp()
|
|
3035
|
-
).action(async (...args) => {
|
|
3036
|
-
const cmd = trailingCmd(args);
|
|
3037
|
-
const g = cmd.optsWithGlobals();
|
|
3038
|
-
const detach = g.detach === true;
|
|
3039
|
-
const detachChild = g._detachedChild === true;
|
|
3040
|
-
await daemonStart({
|
|
3041
|
-
...toCliOptions(g),
|
|
3042
|
-
...detach ? { detach } : {},
|
|
3043
|
-
...detachChild ? { detachChild } : {}
|
|
3044
|
-
});
|
|
3045
|
-
});
|
|
3046
|
-
daemonGrp.command("stop").description("stop the Noir daemon").action(async (...args) => {
|
|
3047
|
-
await daemonStop(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
3048
|
-
});
|
|
3049
|
-
daemonGrp.command("status").description("report daemon pid/uptime/mode (exit 4 if not running)").action(async (...args) => {
|
|
3050
|
-
await daemonStatus(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
3051
|
-
});
|
|
3052
|
-
daemonGrp.command("restart").description("stop then start the daemon").action(async (...args) => {
|
|
3053
|
-
await daemonRestart(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
3054
|
-
});
|
|
3055
|
-
daemonGrp.action(() => {
|
|
3056
|
-
throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|stop|status|restart");
|
|
3057
|
-
});
|
|
3058
|
-
program2.command("doctor").description("environment + project health").option(
|
|
3059
|
-
"--dedup",
|
|
3060
|
-
"scan host-context + .noir/ docs for semantic near-duplicates (loads the local embedder)"
|
|
3061
|
-
).action(async (...args) => {
|
|
3062
|
-
const g = actionGlobals(args);
|
|
3063
|
-
const dedup = trailingCmd(args).opts().dedup === true;
|
|
3064
|
-
await doctor({ ...toCliOptions(g), ...dedup ? { dedup: true } : {} });
|
|
3065
|
-
});
|
|
3066
|
-
program2.command("status").description("project + daemon + workflow + store snapshot").action(async (...args) => {
|
|
3067
|
-
await status(toStatusOptions(actionGlobals(args)));
|
|
3068
|
-
});
|
|
3069
|
-
const contextGrp = program2.command("context").description("context engine");
|
|
3070
|
-
contextGrp.command("search").description("hybrid search over the indexed context").argument("<query>", "search query").option("--limit <n>", "max results", "10").action(async (...args) => {
|
|
3071
|
-
const cmd = trailingCmd(args);
|
|
3072
|
-
const g = cmd.optsWithGlobals();
|
|
3073
|
-
const query = typeof args[0] === "string" ? args[0] : "";
|
|
3074
|
-
const limit = typeof g.limit === "string" ? g.limit : void 0;
|
|
3075
|
-
await contextSearch({
|
|
3076
|
-
...toCliOptions(g),
|
|
3077
|
-
query,
|
|
3078
|
-
...limit === void 0 ? {} : { limit }
|
|
3079
|
-
});
|
|
3080
|
-
});
|
|
3081
|
-
contextGrp.command("index").description("(re)index project files into the context store").option(
|
|
3082
|
-
"--path <p>",
|
|
3083
|
-
"path to index (repeatable)",
|
|
3084
|
-
(val, acc) => [...acc, val],
|
|
3085
|
-
[]
|
|
3086
|
-
).option("--force", "force a full reindex (drop all chunks+vectors, re-index from scratch)").action(async (...args) => {
|
|
3087
|
-
const cmd = trailingCmd(args);
|
|
3088
|
-
const g = cmd.optsWithGlobals();
|
|
3089
|
-
const rawPaths = Array.isArray(g.path) ? g.path.filter((p) => typeof p === "string") : [];
|
|
3090
|
-
const force = g.force === true;
|
|
3091
|
-
await contextIndex({
|
|
3092
|
-
...toCliOptions(g),
|
|
3093
|
-
...rawPaths.length === 0 ? {} : { paths: rawPaths },
|
|
3094
|
-
...force ? { force } : {}
|
|
3095
|
-
});
|
|
3096
|
-
});
|
|
3097
|
-
contextGrp.command("status").description("index freshness + counts").action(async (...args) => {
|
|
3098
|
-
await contextStatus(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
3099
|
-
});
|
|
3100
|
-
contextGrp.action(() => {
|
|
3101
|
-
throw new NoirCliError(EXIT.USAGE, "Usage: noir context search|index|status");
|
|
3102
|
-
});
|
|
3103
|
-
const memoryGrp = program2.command("memory").description("memory engine");
|
|
3104
|
-
memoryGrp.command("recall").description("recall memories for a query").argument("<query>", "recall query").option("--limit <n>", "max results", "10").action(async (...args) => {
|
|
3105
|
-
const cmd = trailingCmd(args);
|
|
3106
|
-
const g = cmd.optsWithGlobals();
|
|
3107
|
-
const query = typeof args[0] === "string" ? args[0] : "";
|
|
3108
|
-
const limit = typeof g.limit === "string" ? g.limit : void 0;
|
|
3109
|
-
await memoryRecall({
|
|
3110
|
-
...toCliOptions(g),
|
|
3111
|
-
query,
|
|
3112
|
-
...limit === void 0 ? {} : { limit }
|
|
3113
|
-
});
|
|
3114
|
-
});
|
|
3115
|
-
memoryGrp.command("save").description("save an observation to long-term memory").option("--content <text>", "memory content (prompted interactively if omitted)").option(
|
|
3116
|
-
"--type <type>",
|
|
3117
|
-
"observation type (pattern | preference | architecture | bug | workflow | fact | decision)"
|
|
3118
|
-
).option("--files <files>", "comma-separated related file paths").action(async (...args) => {
|
|
3119
|
-
const cmd = trailingCmd(args);
|
|
3120
|
-
const g = cmd.optsWithGlobals();
|
|
3121
|
-
const content = typeof g.content === "string" ? g.content : void 0;
|
|
3122
|
-
const type = typeof g.type === "string" ? g.type : void 0;
|
|
3123
|
-
const files = typeof g.files === "string" ? g.files : void 0;
|
|
3124
|
-
await memorySave({
|
|
3125
|
-
...toCliOptions(g),
|
|
3126
|
-
...content === void 0 ? {} : { content },
|
|
3127
|
-
...type === void 0 ? {} : { type },
|
|
3128
|
-
...files === void 0 ? {} : { files }
|
|
3129
|
-
});
|
|
3130
|
-
});
|
|
3131
|
-
memoryGrp.command("sessions").description("list recent memory sessions").action(async (...args) => {
|
|
3132
|
-
await memorySessions(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
3133
|
-
});
|
|
3134
|
-
memoryGrp.command("forget").description("delete one or more memories by id").argument("<ids...>", "observation id(s)").action(async (...args) => {
|
|
3135
|
-
const cmd = trailingCmd(args);
|
|
3136
|
-
const g = cmd.optsWithGlobals();
|
|
3137
|
-
const rawIds = Array.isArray(args[0]) ? args[0] : [];
|
|
3138
|
-
const ids = rawIds.filter((x) => typeof x === "string");
|
|
3139
|
-
await memoryForget({ ...toCliOptions(g), ids });
|
|
3140
|
-
});
|
|
3141
|
-
memoryGrp.command("consolidate").description("consolidate memories into a derived lesson (provider-explicit)").option("--types <types>", "comma-separated observation types to consolidate").option("--limit <n>", "cap on candidate observations").action(async (...args) => {
|
|
3142
|
-
const cmd = trailingCmd(args);
|
|
3143
|
-
const g = cmd.optsWithGlobals();
|
|
3144
|
-
const types = typeof g.types === "string" ? g.types : void 0;
|
|
3145
|
-
const limit = typeof g.limit === "string" ? g.limit : void 0;
|
|
3146
|
-
await memoryConsolidate({
|
|
3147
|
-
...toCliOptions(g),
|
|
3148
|
-
...types === void 0 ? {} : { types },
|
|
3149
|
-
...limit === void 0 ? {} : { limit }
|
|
3150
|
-
});
|
|
3151
|
-
});
|
|
3152
|
-
memoryGrp.action(() => {
|
|
3153
|
-
throw new NoirCliError(
|
|
3154
|
-
EXIT.USAGE,
|
|
3155
|
-
"Usage: noir memory recall|save|sessions|forget|consolidate"
|
|
3156
|
-
);
|
|
3157
|
-
});
|
|
3158
|
-
const skillsGrp = program2.command("skills").description("builtin skills");
|
|
3159
|
-
skillsGrp.command("list").description("list installed Noir skills").action(async (...args) => {
|
|
3160
|
-
await skillsList(toCliOptions(actionGlobals(args)));
|
|
3161
|
-
});
|
|
3162
|
-
skillsGrp.command("sync").description("re-emit skills to the host skills dir").action(async (...args) => {
|
|
3163
|
-
await skillsSync(toCliOptions(actionGlobals(args)));
|
|
3164
|
-
});
|
|
3165
|
-
skillsGrp.command("lint").description("structural quality gate over the shipped pack").action(async (...args) => {
|
|
3166
|
-
await skillsLint(toCliOptions(actionGlobals(args)));
|
|
3167
|
-
});
|
|
3168
|
-
skillsGrp.command("registry").description("emit the runtime-derived skill registry").action(async (...args) => {
|
|
3169
|
-
await skillsRegistry(toCliOptions(actionGlobals(args)));
|
|
3170
|
-
});
|
|
3171
|
-
skillsGrp.action(() => {
|
|
3172
|
-
throw new NoirCliError(EXIT.USAGE, "Usage: noir skills list|sync|lint|registry");
|
|
3173
|
-
});
|
|
3174
|
-
const taskGrp = program2.command("task").description("workflow task control");
|
|
3175
|
-
taskGrp.command("new").description("start a new workflow task").requiredOption("--slug <slug>", "task slug").option("--mode <mode>", "full | quick").option(
|
|
3176
|
-
"--class <taskClass>",
|
|
3177
|
-
"task class (feature/epic/enhancement/bugfix/spike/quick-task/refactor) \u2014 drives the PRD gate"
|
|
3178
|
-
).action(async (...args) => {
|
|
3179
|
-
const cmd = trailingCmd(args);
|
|
3180
|
-
const g = cmd.optsWithGlobals();
|
|
3181
|
-
const slug = typeof g.slug === "string" ? g.slug : "";
|
|
3182
|
-
const mode = typeof g.mode === "string" ? g.mode : void 0;
|
|
3183
|
-
const taskClass = typeof g.class === "string" ? g.class : void 0;
|
|
3184
|
-
await taskNew({
|
|
3185
|
-
...toCliOptions(g),
|
|
3186
|
-
slug,
|
|
3187
|
-
...mode === void 0 ? {} : { mode },
|
|
3188
|
-
...taskClass === void 0 ? {} : { taskClass }
|
|
3189
|
-
});
|
|
3190
|
-
});
|
|
3191
|
-
taskGrp.command("status").description("active (or named) task status").argument("[id]", "task id (defaults to active)").action(async (...args) => {
|
|
3192
|
-
const cmd = trailingCmd(args);
|
|
3193
|
-
const g = cmd.optsWithGlobals();
|
|
3194
|
-
const first = args[0];
|
|
3195
|
-
const id = typeof first === "string" ? first : void 0;
|
|
3196
|
-
await taskStatus({
|
|
3197
|
-
...toCliOptions(g),
|
|
3198
|
-
...id === void 0 ? {} : { id }
|
|
3199
|
-
});
|
|
3200
|
-
});
|
|
3201
|
-
taskGrp.command("advance").description("advance the active task to the next phase").option("--to <phase>", "target phase").option("--force <reason>", "force the gate with a reason").option("--no-artifacts", "skip the document-phase artifact writes at done").action(async (...args) => {
|
|
3202
|
-
const cmd = trailingCmd(args);
|
|
3203
|
-
const g = cmd.optsWithGlobals();
|
|
3204
|
-
const to = typeof g.to === "string" ? g.to : void 0;
|
|
3205
|
-
const force = typeof g.force === "string" ? g.force : void 0;
|
|
3206
|
-
await taskAdvance({
|
|
3207
|
-
...toCliOptions(g),
|
|
3208
|
-
...to === void 0 ? {} : { to },
|
|
3209
|
-
...force === void 0 ? {} : { force },
|
|
3210
|
-
...g.artifacts === false ? { noArtifacts: true } : {}
|
|
3211
|
-
});
|
|
3212
|
-
});
|
|
3213
|
-
taskGrp.command("next").description("suggest the next phase + applicable skill").action(async (...args) => {
|
|
3214
|
-
await taskNext(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
3215
|
-
});
|
|
3216
|
-
taskGrp.command("decompose").description("decompose a capability into an implementation plan").argument("<capability>", "capability id").option("--out <path>", "output path for the plan JSON").action(async (...args) => {
|
|
3217
|
-
const cmd = trailingCmd(args);
|
|
3218
|
-
const g = cmd.optsWithGlobals();
|
|
3219
|
-
const capability = typeof args[0] === "string" ? args[0] : "";
|
|
3220
|
-
await taskDecompose({
|
|
3221
|
-
...toCliOptions(g),
|
|
3222
|
-
capability,
|
|
3223
|
-
...typeof g.out === "string" && g.out.length > 0 ? { out: g.out } : {}
|
|
3224
|
-
});
|
|
3225
|
-
});
|
|
3226
|
-
taskGrp.command("verify").description("run configured verify checks and submit evidence to the verify gate").option(
|
|
3227
|
-
"--check <name>",
|
|
3228
|
-
"restrict to a named check (repeatable)",
|
|
3229
|
-
(val, acc) => [...acc, val],
|
|
3230
|
-
[]
|
|
3231
|
-
).action(async (...args) => {
|
|
3232
|
-
const cmd = trailingCmd(args);
|
|
3233
|
-
const g = cmd.optsWithGlobals();
|
|
3234
|
-
const check = Array.isArray(g.check) ? g.check : void 0;
|
|
3235
|
-
await taskVerify({
|
|
3236
|
-
...toCliOptions(g),
|
|
3237
|
-
...check === void 0 || check.length === 0 ? {} : { check }
|
|
3238
|
-
});
|
|
3239
|
-
});
|
|
3240
|
-
taskGrp.command("research").description("list research findings for the active (or named) task").argument("[id]", "task id (defaults to active)").action(async (...args) => {
|
|
3241
|
-
const cmd = trailingCmd(args);
|
|
3242
|
-
const g = cmd.optsWithGlobals();
|
|
3243
|
-
const first = args[0];
|
|
3244
|
-
const id = typeof first === "string" ? first : void 0;
|
|
3245
|
-
await taskResearch({
|
|
3246
|
-
...toCliOptions(g),
|
|
3247
|
-
...id === void 0 ? {} : { id }
|
|
3248
|
-
});
|
|
3249
|
-
});
|
|
3250
|
-
taskGrp.command("research-record").description("record a research finding for the active (or named) task").requiredOption("--type <type>", "assumption | discovery | decision | grounding-fact").requiredOption("--text <text>", "finding text (capped)").option("--source <ref>", "evidence/citation (required unless grounding-fact)").option("--task <id>", "task id (defaults to active)").action(async (...args) => {
|
|
3251
|
-
const cmd = trailingCmd(args);
|
|
3252
|
-
const g = cmd.optsWithGlobals();
|
|
3253
|
-
await taskResearchRecord({
|
|
3254
|
-
...toCliOptions(g),
|
|
3255
|
-
type: String(g.type ?? ""),
|
|
3256
|
-
text: String(g.text ?? ""),
|
|
3257
|
-
...typeof g.source === "string" && g.source.length > 0 ? { source: g.source } : {},
|
|
3258
|
-
...typeof g.task === "string" && g.task.length > 0 ? { task: g.task } : {}
|
|
3259
|
-
});
|
|
3260
|
-
});
|
|
3261
|
-
taskGrp.command("resume").description("resume the active (or named) in-flight/blocked task").argument("[id]", "task id (defaults to active)").option("--last", "target the active task explicitly (scripting)").option("--prompt <text>", "a continue instruction to surface in the briefing").action(async (...args) => {
|
|
3262
|
-
const cmd = trailingCmd(args);
|
|
3263
|
-
const g = cmd.optsWithGlobals();
|
|
3264
|
-
const first = args[0];
|
|
3265
|
-
const id = typeof first === "string" ? first : void 0;
|
|
3266
|
-
const last = g.last === true;
|
|
3267
|
-
const prompt = typeof g.prompt === "string" ? g.prompt : void 0;
|
|
3268
|
-
await taskResume({
|
|
3269
|
-
...toCliOptions(g),
|
|
3270
|
-
...id === void 0 ? {} : { id },
|
|
3271
|
-
...last ? { last } : {},
|
|
3272
|
-
...prompt === void 0 ? {} : { prompt }
|
|
3273
|
-
});
|
|
3274
|
-
});
|
|
3275
|
-
taskGrp.command("block").description("mark the active (or named) task blocked with a reason").argument("<reason>", "why the task is stuck (non-empty)").option("--task <id>", "task id (defaults to active)").action(async (...args) => {
|
|
3276
|
-
const cmd = trailingCmd(args);
|
|
3277
|
-
const g = cmd.optsWithGlobals();
|
|
3278
|
-
const reason = typeof args[0] === "string" ? args[0] : "";
|
|
3279
|
-
const task = typeof g.task === "string" ? g.task : void 0;
|
|
3280
|
-
await taskBlock({
|
|
3281
|
-
...toCliOptions(g),
|
|
3282
|
-
reason,
|
|
3283
|
-
...task === void 0 ? {} : { task }
|
|
3284
|
-
});
|
|
3285
|
-
});
|
|
3286
|
-
taskGrp.command("abandon").description("abandon the active (or named) task (terminal, confirmed)").option("--task <id>", "task id (defaults to active)").action(async (...args) => {
|
|
3287
|
-
const cmd = trailingCmd(args);
|
|
3288
|
-
const g = cmd.optsWithGlobals();
|
|
3289
|
-
const task = typeof g.task === "string" ? g.task : void 0;
|
|
3290
|
-
await taskAbandon({
|
|
3291
|
-
...toCliOptions(g),
|
|
3292
|
-
...task === void 0 ? {} : { task }
|
|
3293
|
-
});
|
|
3294
|
-
});
|
|
3295
|
-
taskGrp.action(() => {
|
|
3296
|
-
throw new NoirCliError(
|
|
3297
|
-
EXIT.USAGE,
|
|
3298
|
-
"Usage: noir task new|status|advance|next|verify|research|research-record|resume|block|abandon"
|
|
3299
|
-
);
|
|
3300
|
-
});
|
|
3301
|
-
const installCmd = program2.command("install").description(
|
|
3302
|
-
"install Noir via the native managed-Node path (or migrate from another install method)"
|
|
3303
|
-
).option("--list", "list detected install methods").option("--uninstall-prev", "after a successful migrate, uninstall the previous install method").option(
|
|
3304
|
-
"--dismiss",
|
|
3305
|
-
"dismiss the migration banner for the current version (persists in install.json)"
|
|
3306
|
-
).argument("[spec]", "channel ('latest'|'beta') or exact version (default: latest)").action(
|
|
3307
|
-
async (spec, opts, cmd) => {
|
|
3308
|
-
const { install } = await import("./install-JR6ZXNYA.js");
|
|
3309
|
-
const cli = cmd.optsWithGlobals();
|
|
3310
|
-
await install({
|
|
3311
|
-
...cli,
|
|
3312
|
-
spec,
|
|
3313
|
-
list: opts.list === true,
|
|
3314
|
-
uninstallPrev: opts.uninstallPrev === true,
|
|
3315
|
-
dismiss: opts.dismiss === true
|
|
3316
|
-
});
|
|
3317
|
-
}
|
|
3318
|
-
);
|
|
3319
|
-
installCmd.alias("migrate");
|
|
3320
|
-
program2.command("update").description("update Noir to the latest version via the active install method").option("--check", "check for a new version without changing anything").argument("[spec]", "channel ('latest'|'beta') or exact version (default: latest)").action(async (spec, opts, cmd) => {
|
|
3321
|
-
const { update } = await import("./update-PGAM6OVL.js");
|
|
3322
|
-
const cli = cmd.optsWithGlobals();
|
|
3323
|
-
await update({ ...cli, spec, check: opts.check === true });
|
|
3324
|
-
});
|
|
3325
|
-
function buildHandoffOptions(g) {
|
|
3326
|
-
return { ...toCliOptions(g), ...g.write === true ? { write: true } : {} };
|
|
3327
|
-
}
|
|
3328
|
-
program2.command("handoff").description("emit a ready-to-paste host handoff prompt").option("--write", "persist to .noir/handoff/HO-<NNNN>-<id>.md (gitignored)").action(async (...args) => {
|
|
3329
|
-
await handoff(buildHandoffOptions(trailingCmd(args).optsWithGlobals()));
|
|
3330
|
-
});
|
|
3331
|
-
program2.command("wrap").description("session-end alias for `noir handoff`").option("--write", "persist to .noir/handoff/HO-<NNNN>-<id>.md (gitignored)").action(async (...args) => {
|
|
3332
|
-
await handoff(buildHandoffOptions(trailingCmd(args).optsWithGlobals()));
|
|
3333
|
-
});
|
|
3334
|
-
program2.command("release").description("guided release orchestrator over the patch-release flow").argument("[version]", "target version, e.g. 1.10.0").option("--channel <channel>", "beta (default) | stable").option("--dry-run", "print the checklist without executing any steps").action(async (...args) => {
|
|
3335
|
-
const cmd = trailingCmd(args);
|
|
3336
|
-
const g = cmd.optsWithGlobals();
|
|
3337
|
-
await release({
|
|
3338
|
-
...toCliOptions(g),
|
|
3339
|
-
...typeof args[0] === "string" && args[0].length > 0 ? { version: args[0] } : {},
|
|
3340
|
-
...typeof g.channel === "string" && g.channel.length > 0 ? { channel: g.channel } : {},
|
|
3341
|
-
...g.dryRun === true ? { dryRun: true } : {}
|
|
3342
|
-
});
|
|
3343
|
-
});
|
|
3344
|
-
program2.command("tui").description("interactive Ink dashboard (host \xB7 phase \xB7 daemon + /command dispatch)").action(async (...args) => {
|
|
3345
|
-
const g = actionGlobals(args);
|
|
3346
|
-
const opts = toCliOptions(g);
|
|
3347
|
-
requireInteractive(opts, "`noir tui`");
|
|
3348
|
-
const tuiUrl = new URL("./tui/index.js", import.meta.url).href;
|
|
3349
|
-
process.env.NODE_ENV ||= "production";
|
|
3350
|
-
const { runTui: runTui2 } = await import(
|
|
3351
|
-
/* @vite-ignore */
|
|
3352
|
-
tuiUrl
|
|
3353
|
-
);
|
|
3354
|
-
const dispatch = async (argv) => {
|
|
3355
|
-
const sub = createProgram();
|
|
3356
|
-
try {
|
|
3357
|
-
await sub.parseAsync([...argv], { from: "user" });
|
|
3358
|
-
} catch (err) {
|
|
3359
|
-
handleError(err);
|
|
3360
|
-
}
|
|
3361
|
-
};
|
|
3362
|
-
await runTui2(opts, dispatch);
|
|
3363
|
-
});
|
|
3364
|
-
program2.command("palette").description("fuzzy command palette \u2014 run any noir command (Ink)").action(async (...args) => {
|
|
3365
|
-
const g = actionGlobals(args);
|
|
3366
|
-
const opts = toCliOptions(g);
|
|
3367
|
-
requireInteractive(opts, "`noir palette`");
|
|
3368
|
-
const tuiUrl = new URL("./tui/index.js", import.meta.url).href;
|
|
3369
|
-
process.env.NODE_ENV ||= "production";
|
|
3370
|
-
const { runPalette: runPalette2 } = await import(
|
|
3371
|
-
/* @vite-ignore */
|
|
3372
|
-
tuiUrl
|
|
3373
|
-
);
|
|
3374
|
-
const dispatch = async (argv) => {
|
|
3375
|
-
const sub = createProgram();
|
|
3376
|
-
try {
|
|
3377
|
-
await sub.parseAsync([...argv], { from: "user" });
|
|
3378
|
-
} catch (err) {
|
|
3379
|
-
handleError(err);
|
|
3380
|
-
}
|
|
3381
|
-
};
|
|
3382
|
-
await runPalette2(opts, dispatch);
|
|
3383
|
-
});
|
|
3384
|
-
program2.command("run").description("ask the host agent a question and print the answer").argument("[prompt...]", "prompt to send to the host").addOption(new Option("--host <id>", "host to drive (default claude)").choices(SUPPORTED_HOSTS2)).option("--command <binary>", "custom host binary (e.g. claude-work)").action(async (...args) => {
|
|
3385
|
-
const cmd = trailingCmd(args);
|
|
3386
|
-
const g = cmd.optsWithGlobals();
|
|
3387
|
-
const prompt = cmd.args.join(" ").trim();
|
|
3388
|
-
const opts = {
|
|
3389
|
-
...toCliOptions(g),
|
|
3390
|
-
...typeof g.host === "string" ? { host: g.host } : {},
|
|
3391
|
-
...typeof g.command === "string" ? { command: g.command } : {}
|
|
3392
|
-
};
|
|
3393
|
-
await run(prompt, opts);
|
|
3394
|
-
});
|
|
3395
|
-
program2.action(async (...args) => {
|
|
3396
|
-
const cmd = trailingCmd(args);
|
|
3397
|
-
const leftovers = cmd.args;
|
|
3398
|
-
if (leftovers.length > 0) {
|
|
3399
|
-
fail(
|
|
3400
|
-
EXIT.NOT_FOUND,
|
|
3401
|
-
`unknown command '${leftovers[0]}' (no such subcommand). Run \`noir --help\` for the list.`,
|
|
3402
|
-
toCliOptions(cmd.optsWithGlobals())
|
|
3403
|
-
);
|
|
3404
|
-
}
|
|
3405
|
-
await home(toCliOptions(actionGlobals(args)), homeDeps);
|
|
3406
|
-
});
|
|
3407
|
-
return program2;
|
|
3408
|
-
}
|
|
3409
|
-
var program = createProgram();
|
|
3410
|
-
var homeDeps = {
|
|
3411
|
-
dispatch: async (argv) => {
|
|
3412
|
-
const sub = createProgram();
|
|
3413
|
-
try {
|
|
3414
|
-
await sub.parseAsync([...argv], { from: "user" });
|
|
3415
|
-
} catch (err) {
|
|
3416
|
-
handleError(err);
|
|
3417
|
-
}
|
|
3418
|
-
},
|
|
3419
|
-
// The grouped home menu resolves its sections against the LIVE palette
|
|
3420
|
-
// registry (home-consolidation S1/S2) — the same source the TUI palette
|
|
3421
|
-
// uses — so the menu cannot drift from the commander tree.
|
|
3422
|
-
commands: buildPaletteCommands(createProgram())
|
|
3423
|
-
};
|
|
3424
|
-
async function run2(argv = []) {
|
|
3425
|
-
try {
|
|
3426
|
-
await program.parseAsync([...argv], { from: "user" });
|
|
3427
|
-
} catch (err) {
|
|
3428
|
-
handleError(err);
|
|
3429
|
-
}
|
|
3430
|
-
return typeof process.exitCode === "number" ? process.exitCode : EXIT.OK;
|
|
3431
|
-
}
|
|
3432
|
-
var isMainModule = (() => {
|
|
3433
|
-
try {
|
|
3434
|
-
const entry = process.argv[1];
|
|
3435
|
-
if (typeof entry !== "string" || entry.length === 0) return false;
|
|
3436
|
-
return pathToFileURL(realpathSync(entry)).href === import.meta.url;
|
|
3437
|
-
} catch {
|
|
3438
|
-
return false;
|
|
3439
|
-
}
|
|
3440
|
-
})();
|
|
3441
|
-
if (isMainModule) {
|
|
3442
|
-
void run2(process.argv.slice(2));
|
|
3443
|
-
}
|
|
20
|
+
// src/tui/index.tsx
|
|
21
|
+
import { loadProjectInfo } from "@noir-ai/core";
|
|
22
|
+
import { render } from "ink";
|
|
3444
23
|
|
|
3445
24
|
// src/tui/App.tsx
|
|
3446
25
|
import { Box as Box5, Text as Text8, useApp, useInput } from "ink";
|
|
@@ -3634,7 +213,18 @@ import { jsx as jsx5 } from "react/jsx-runtime";
|
|
|
3634
213
|
function Panel({ children, maxWidth }) {
|
|
3635
214
|
const full = contentWidth() + 4;
|
|
3636
215
|
const width = maxWidth != null ? Math.min(full, maxWidth) : full;
|
|
3637
|
-
|
|
216
|
+
const borderColor = useColor() ? "gray" : void 0;
|
|
217
|
+
return /* @__PURE__ */ jsx5(
|
|
218
|
+
Box2,
|
|
219
|
+
{
|
|
220
|
+
flexDirection: "column",
|
|
221
|
+
borderStyle: "round",
|
|
222
|
+
borderColor,
|
|
223
|
+
paddingX: 1,
|
|
224
|
+
width,
|
|
225
|
+
children
|
|
226
|
+
}
|
|
227
|
+
);
|
|
3638
228
|
}
|
|
3639
229
|
|
|
3640
230
|
// src/tui/overlays/ConfirmOverlay.tsx
|
|
@@ -3878,15 +468,16 @@ function buildPaletteRows(input) {
|
|
|
3878
468
|
// src/tui/palette/Palette.tsx
|
|
3879
469
|
import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
3880
470
|
var PALETTE_WIDTH = 64;
|
|
471
|
+
var ROW_TEXT_WIDTH = PALETTE_WIDTH - 6;
|
|
3881
472
|
var LABEL_WIDTH = 26;
|
|
3882
|
-
var HINT_WIDTH =
|
|
473
|
+
var HINT_WIDTH = ROW_TEXT_WIDTH - LABEL_WIDTH;
|
|
3883
474
|
function wrap(text, width) {
|
|
3884
475
|
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
3885
476
|
const lines = [];
|
|
3886
477
|
let cur = "";
|
|
3887
478
|
for (const w of words) {
|
|
3888
479
|
if (cur.length === 0) cur = w;
|
|
3889
|
-
else if (
|
|
480
|
+
else if (`${cur} ${w}`.length <= width) cur = `${cur} ${w}`;
|
|
3890
481
|
else {
|
|
3891
482
|
lines.push(cur);
|
|
3892
483
|
cur = w;
|
|
@@ -3953,8 +544,8 @@ function Palette({ corpus, query, active, rows }) {
|
|
|
3953
544
|
] }) }, row.key)
|
|
3954
545
|
);
|
|
3955
546
|
}
|
|
3956
|
-
if (isActive && row.
|
|
3957
|
-
const detailLines = wrap(row.secondary,
|
|
547
|
+
if (isActive && row.secondary && (row.argv || corpus === "help")) {
|
|
548
|
+
const detailLines = wrap(row.secondary, ROW_TEXT_WIDTH - 4);
|
|
3958
549
|
for (let di = 0; di < detailLines.length; di++) {
|
|
3959
550
|
const line = detailLines[di] ?? "";
|
|
3960
551
|
const prefix2 = di === 0 ? " \u21B3 " : " ";
|
|
@@ -4081,7 +672,7 @@ function App({
|
|
|
4081
672
|
if (pending === null) return;
|
|
4082
673
|
const argv = pending;
|
|
4083
674
|
let active = true;
|
|
4084
|
-
const
|
|
675
|
+
const run = async () => {
|
|
4085
676
|
const captured = await captureProcessOutput(() => deps.dispatch(argv));
|
|
4086
677
|
if (!active) return;
|
|
4087
678
|
const combined = `${captured.stdout}${captured.stderr}`;
|
|
@@ -4094,7 +685,7 @@ function App({
|
|
|
4094
685
|
setPending(null);
|
|
4095
686
|
setScrollOffset(0);
|
|
4096
687
|
};
|
|
4097
|
-
void
|
|
688
|
+
void run();
|
|
4098
689
|
return () => {
|
|
4099
690
|
active = false;
|
|
4100
691
|
};
|
|
@@ -4361,18 +952,18 @@ function App({
|
|
|
4361
952
|
}
|
|
4362
953
|
|
|
4363
954
|
// src/tui/palette/history.ts
|
|
4364
|
-
import { existsSync, mkdirSync
|
|
955
|
+
import { existsSync, mkdirSync, readFileSync } from "fs";
|
|
4365
956
|
import { homedir } from "os";
|
|
4366
|
-
import { join
|
|
957
|
+
import { join } from "path";
|
|
4367
958
|
import { atomicWriteFile } from "@noir-ai/core";
|
|
4368
959
|
var MAX_ENTRIES = 50;
|
|
4369
960
|
var TUI_HISTORY_DISABLE_ENV = "NOIR_DISABLE_TUI_HISTORY";
|
|
4370
961
|
var testHome = null;
|
|
4371
962
|
function homeDir() {
|
|
4372
|
-
return testHome ??
|
|
963
|
+
return testHome ?? join(homedir(), ".noir");
|
|
4373
964
|
}
|
|
4374
965
|
function historyFile(projectId) {
|
|
4375
|
-
return
|
|
966
|
+
return join(homeDir(), projectId, "tui-history.json");
|
|
4376
967
|
}
|
|
4377
968
|
function isHistoryEnabled() {
|
|
4378
969
|
const v = process.env[TUI_HISTORY_DISABLE_ENV];
|
|
@@ -4407,8 +998,8 @@ function recordRecent(projectId, argv) {
|
|
|
4407
998
|
const current = loadRecent(projectId).filter((e) => e.id !== entry.id);
|
|
4408
999
|
const next = [entry, ...current].slice(0, MAX_ENTRIES);
|
|
4409
1000
|
try {
|
|
4410
|
-
const dir =
|
|
4411
|
-
|
|
1001
|
+
const dir = join(homeDir(), projectId);
|
|
1002
|
+
mkdirSync(dir, { recursive: true });
|
|
4412
1003
|
atomicWriteFile(historyFile(projectId), `${JSON.stringify(next, null, 2)}
|
|
4413
1004
|
`);
|
|
4414
1005
|
} catch {
|
|
@@ -4443,7 +1034,7 @@ async function runPalette(opts, dispatch) {
|
|
|
4443
1034
|
async function buildTuiDeps(opts, dispatch) {
|
|
4444
1035
|
let projectId = null;
|
|
4445
1036
|
try {
|
|
4446
|
-
projectId =
|
|
1037
|
+
projectId = loadProjectInfo(process.cwd()).id;
|
|
4447
1038
|
} catch {
|
|
4448
1039
|
projectId = null;
|
|
4449
1040
|
}
|