@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.
Files changed (45) hide show
  1. package/dist/bin.js +3295 -575
  2. package/dist/bin.js.map +1 -1
  3. package/dist/index.js +6136 -6
  4. package/dist/index.js.map +1 -1
  5. package/dist/{chunk-QIVHIKYD.js → tui/chunk-2RK6AFZQ.js} +37 -16
  6. package/dist/tui/chunk-2RK6AFZQ.js.map +1 -0
  7. package/dist/tui/{chunk-R5YX44UL.js → chunk-ESZTOQEC.js} +211 -2
  8. package/dist/tui/chunk-ESZTOQEC.js.map +1 -0
  9. package/dist/tui/chunk-J77RHAWG.js +5010 -0
  10. package/dist/tui/chunk-J77RHAWG.js.map +1 -0
  11. package/dist/{create-VK2QMXYS.js → tui/create-BTYLBVV6.js} +4 -4
  12. package/dist/tui/{create-WKFVM237.js.map → create-BTYLBVV6.js.map} +1 -1
  13. package/dist/tui/index.js +45 -3454
  14. package/dist/tui/index.js.map +1 -1
  15. package/dist/tui/{install-JR6ZXNYA.js → install-YGTUSLRO.js} +2 -3
  16. package/dist/{sync-VXZKODQA.js → tui/sync-LRHPJ5VV.js} +4 -4
  17. package/dist/tui/{sync-DUWJAF7Z.js.map → sync-LRHPJ5VV.js.map} +1 -1
  18. package/dist/tui/{update-PGAM6OVL.js → update-LET5BWOU.js} +3 -4
  19. package/package.json +12 -12
  20. package/dist/chunk-A5W2E3X2.js +0 -33
  21. package/dist/chunk-A5W2E3X2.js.map +0 -1
  22. package/dist/chunk-CT6TYLHN.js +0 -1105
  23. package/dist/chunk-CT6TYLHN.js.map +0 -1
  24. package/dist/chunk-I2R55ACD.js +0 -369
  25. package/dist/chunk-I2R55ACD.js.map +0 -1
  26. package/dist/chunk-QIVHIKYD.js.map +0 -1
  27. package/dist/chunk-RON2XESF.js +0 -210
  28. package/dist/chunk-RON2XESF.js.map +0 -1
  29. package/dist/create-VK2QMXYS.js.map +0 -1
  30. package/dist/install-4U4UDDNE.js +0 -13
  31. package/dist/sync-VXZKODQA.js.map +0 -1
  32. package/dist/tui/chunk-F6XRS72N.js +0 -1106
  33. package/dist/tui/chunk-F6XRS72N.js.map +0 -1
  34. package/dist/tui/chunk-NLK6UNIA.js +0 -223
  35. package/dist/tui/chunk-NLK6UNIA.js.map +0 -1
  36. package/dist/tui/chunk-QF6777NJ.js +0 -209
  37. package/dist/tui/chunk-QF6777NJ.js.map +0 -1
  38. package/dist/tui/chunk-R5YX44UL.js.map +0 -1
  39. package/dist/tui/create-WKFVM237.js +0 -79
  40. package/dist/tui/sync-DUWJAF7Z.js +0 -90
  41. package/dist/tui/update-PGAM6OVL.js.map +0 -1
  42. package/dist/update-ZDAVLOXB.js +0 -20
  43. package/dist/update-ZDAVLOXB.js.map +0 -1
  44. /package/dist/{install-4U4UDDNE.js.map → tui/install-YGTUSLRO.js.map} +0 -0
  45. /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
- DEFAULT_UPDATE_CONFIG,
3
- buildUpdateNotice,
4
- runAsyncUpdateCheck
5
- } from "./chunk-NLK6UNIA.js";
6
- import "./chunk-QF6777NJ.js";
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
- doctor,
9
- init
10
- } from "./chunk-F6XRS72N.js";
11
- import {
12
- EXIT,
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/bin.ts
2821
- function buildPaletteCommandsForTui() {
2822
- return buildPaletteCommands(createProgram());
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
- return /* @__PURE__ */ jsx5(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "gray", paddingX: 1, width, children });
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 = 34;
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 ((cur + " " + w).length <= width) cur = `${cur} ${w}`;
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.argv && row.secondary) {
3957
- const detailLines = wrap(row.secondary, PALETTE_WIDTH - 12);
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 run3 = async () => {
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 run3();
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 as mkdirSync3, readFileSync } from "fs";
955
+ import { existsSync, mkdirSync, readFileSync } from "fs";
4365
956
  import { homedir } from "os";
4366
- import { join as join3 } from "path";
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 ?? join3(homedir(), ".noir");
963
+ return testHome ?? join(homedir(), ".noir");
4373
964
  }
4374
965
  function historyFile(projectId) {
4375
- return join3(homeDir(), projectId, "tui-history.json");
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 = join3(homeDir(), projectId);
4411
- mkdirSync3(dir, { recursive: true });
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 = loadProjectInfo9(process.cwd()).id;
1037
+ projectId = loadProjectInfo(process.cwd()).id;
4447
1038
  } catch {
4448
1039
  projectId = null;
4449
1040
  }