@noir-ai/cli 1.3.0-beta.6 → 1.4.0-beta.2

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.
@@ -0,0 +1,2253 @@
1
+ import {
2
+ init
3
+ } from "./chunk-OMF7XZMN.js";
4
+ import {
5
+ EXIT,
6
+ NoirCliError,
7
+ accessibleMode,
8
+ badge,
9
+ c,
10
+ definitionList,
11
+ doctor,
12
+ fail,
13
+ handleError,
14
+ info,
15
+ isInteractive,
16
+ json,
17
+ log,
18
+ requireInteractive,
19
+ table,
20
+ terminalWidth,
21
+ tip,
22
+ useColor,
23
+ warn
24
+ } from "./chunk-YNEBQJXA.js";
25
+
26
+ // src/tui/index.tsx
27
+ import { render } from "ink";
28
+
29
+ // src/commands/status.ts
30
+ import { loadProjectInfo as loadProjectInfo7, NOIR_VERSION as NOIR_VERSION3 } from "@noir-ai/core";
31
+
32
+ // src/daemon-client.ts
33
+ import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
34
+ import { loadProjectInfo as loadProjectInfo6, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
35
+ import { ensureDaemonRunning as ensureDaemonRunning3, pidAlive as pidAlive2, readDaemonRecord as readDaemonRecord2 } from "@noir-ai/daemon";
36
+
37
+ // src/bin.ts
38
+ import { realpathSync } from "fs";
39
+ import { pathToFileURL } from "url";
40
+ import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
41
+ import { NOIR_VERSION } from "@noir-ai/core";
42
+ import { Command, Option } from "commander";
43
+
44
+ // src/commands/context.ts
45
+ function parseLimit(raw, label, opts) {
46
+ if (raw === void 0) return void 0;
47
+ const n = Number(raw);
48
+ if (!Number.isInteger(n) || n <= 0) {
49
+ fail(EXIT.USAGE, `${label}: --limit must be a positive integer (got '${raw}')`, opts);
50
+ }
51
+ return n;
52
+ }
53
+ function failTool(label, envelope, opts) {
54
+ const detail = envelope.error ?? envelope.reason ?? "unknown failure";
55
+ fail(EXIT.ERROR, `${label}: ${detail}`, opts);
56
+ }
57
+ function toHit(raw) {
58
+ const h = raw ?? {};
59
+ return {
60
+ path: typeof h.path === "string" ? h.path : "<unknown>",
61
+ score: typeof h.score === "number" ? h.score : 0,
62
+ snippet: typeof h.snippet === "string" ? h.snippet : "",
63
+ source: typeof h.source === "string" ? h.source : ""
64
+ };
65
+ }
66
+ async function contextSearch(opts) {
67
+ const limit = parseLimit(opts.limit, "context search", opts);
68
+ const res = await callDaemonTool(opts, "context_search", {
69
+ query: opts.query,
70
+ ...limit === void 0 ? {} : { limit }
71
+ });
72
+ if (res.ok !== true) failTool("context search", res, opts);
73
+ const data = {
74
+ query: opts.query,
75
+ hits: Array.isArray(res.results) ? res.results.map(toHit) : [],
76
+ consumedTokens: typeof res.consumedTokens === "number" ? res.consumedTokens : 0,
77
+ truncated: res.truncated === true,
78
+ degraded: res.degraded === true,
79
+ mode: typeof res.mode === "string" ? res.mode : "unknown"
80
+ };
81
+ if (opts.json === true) {
82
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
83
+ `);
84
+ return;
85
+ }
86
+ renderSearch(data, opts);
87
+ }
88
+ function renderSearch(data, opts) {
89
+ const flag = data.degraded ? ` ${badge("warn", "degraded: BM25-only")}` : "";
90
+ const trunc = data.truncated ? " (budget hit \u2014 results truncated)" : "";
91
+ log(
92
+ `context search \u2014 ${data.hits.length} hit${data.hits.length === 1 ? "" : "s"} \xB7 ${data.mode} \xB7 ${data.consumedTokens} tokens${flag}${trunc}`,
93
+ opts
94
+ );
95
+ table(
96
+ data.hits.map((h, i) => ({
97
+ "#": i + 1,
98
+ Path: h.path,
99
+ Score: h.score.toFixed(4),
100
+ Snippet: h.snippet
101
+ })),
102
+ ["#", "Path", "Score", "Snippet"],
103
+ opts
104
+ );
105
+ }
106
+ async function contextIndex(opts) {
107
+ if (opts.force === true) {
108
+ info(
109
+ "context index: --force is recognized but not yet honored (content-hash is always incremental).",
110
+ opts
111
+ );
112
+ }
113
+ const args = opts.paths && opts.paths.length > 0 ? { paths: opts.paths } : {};
114
+ const res = await callDaemonTool(opts, "context_index", args);
115
+ if (res.ok !== true) failTool("context index", res, opts);
116
+ const data = {
117
+ indexed: typeof res.indexed === "number" ? res.indexed : 0,
118
+ skipped: typeof res.skipped === "number" ? res.skipped : 0,
119
+ deleted: typeof res.deleted === "number" ? res.deleted : 0,
120
+ failed: typeof res.failed === "number" ? res.failed : 0,
121
+ totalChunks: typeof res.totalChunks === "number" ? res.totalChunks : 0,
122
+ degraded: res.degraded === true
123
+ };
124
+ if (opts.json === true) {
125
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
126
+ `);
127
+ return;
128
+ }
129
+ renderIndex(data, opts.paths, opts);
130
+ }
131
+ function renderIndex(data, paths, opts) {
132
+ const scope = paths && paths.length > 0 ? paths.join(", ") : ".";
133
+ const flag = data.degraded ? ` ${badge("warn", "degraded: vectors skipped")}` : "";
134
+ log(`context index \u2014 ${scope}${flag}`, opts);
135
+ definitionList(
136
+ [
137
+ { label: "Indexed (new)", value: data.indexed },
138
+ { label: "Skipped (unchanged)", value: data.skipped },
139
+ { label: "Deleted (removed)", value: data.deleted },
140
+ { label: "Failed", value: data.failed },
141
+ { label: "Total chunks", value: data.totalChunks }
142
+ ],
143
+ opts
144
+ );
145
+ if (data.failed > 0) {
146
+ info(`${data.failed} file(s) could not be indexed (binary / IO / encoding).`, opts);
147
+ }
148
+ }
149
+ async function contextStatus(opts) {
150
+ const res = await callDaemonTool(opts, "context_status");
151
+ if (res.ok !== true) failTool("context status", res, opts);
152
+ const data = res;
153
+ if (opts.json === true) {
154
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
155
+ `);
156
+ return;
157
+ }
158
+ renderStatus(data, opts);
159
+ }
160
+ function describeEmbedder(e) {
161
+ if (!e || e.kind === "none") return "none (BM25-only)";
162
+ const model = typeof e.model === "string" && e.model.length > 0 ? e.model : "<unset>";
163
+ return `${e.kind} \xB7 ${model} (${e.dim}-dim)`;
164
+ }
165
+ function renderStatus(data, opts) {
166
+ const flag = data.degraded ? ` ${badge("warn", "degraded")}` : "";
167
+ log(`context status \u2014 ${data.projectId}${flag}`, opts);
168
+ definitionList(
169
+ [
170
+ { label: "Project", value: data.projectId },
171
+ { label: "Docs", value: data.docCount },
172
+ { label: "Vectors", value: data.vecCount },
173
+ { label: "Indexed files", value: data.indexedFiles },
174
+ { label: "Embedder", value: describeEmbedder(data.embedder) },
175
+ { label: "Degraded", value: data.degraded ? "yes" : "no" }
176
+ ],
177
+ opts
178
+ );
179
+ }
180
+
181
+ // src/commands/daemon.ts
182
+ import { loadProjectInfo } from "@noir-ai/core";
183
+ import {
184
+ clearDaemonRecord,
185
+ ensureDaemonRunning,
186
+ pidAlive,
187
+ readDaemonRecord
188
+ } from "@noir-ai/daemon";
189
+ var MODE = "foreground";
190
+ function formatUptime(sec) {
191
+ if (!Number.isFinite(sec) || sec < 0) return "unknown";
192
+ if (sec < 60) return `${sec}s`;
193
+ if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
194
+ return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
195
+ }
196
+ async function daemonStart(opts) {
197
+ if (opts.detach === true) {
198
+ fail(EXIT.USAGE, "not implemented (tracked: v1.x)", opts);
199
+ }
200
+ const project = loadProjectInfo(process.cwd());
201
+ const ensured = await ensureDaemonRunning({
202
+ project,
203
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec
204
+ });
205
+ if (ensured.started) {
206
+ if (opts.json === true) {
207
+ process.stdout.write(
208
+ `${JSON.stringify({
209
+ ok: true,
210
+ data: {
211
+ url: ensured.url,
212
+ port: ensured.port,
213
+ pid: process.pid,
214
+ mode: MODE,
215
+ reused: false
216
+ }
217
+ })}
218
+ `
219
+ );
220
+ return;
221
+ }
222
+ info("noir daemon: foreground mode (backgrounding deferred to v1.x). Ctrl+C to stop.", opts);
223
+ log(`Noir daemon listening at ${ensured.url}`, opts);
224
+ return;
225
+ }
226
+ if (opts.json === true) {
227
+ process.stdout.write(
228
+ `${JSON.stringify({ ok: true, data: { url: ensured.url, port: ensured.port, reused: true } })}
229
+ `
230
+ );
231
+ return;
232
+ }
233
+ log(`Noir daemon already running at ${ensured.url}`, opts);
234
+ }
235
+ async function daemonStop(opts) {
236
+ const rec = readDaemonRecord();
237
+ if (!rec) {
238
+ if (opts.json === true) {
239
+ process.stdout.write(
240
+ `${JSON.stringify({ ok: true, data: { running: false, stopped: false } })}
241
+ `
242
+ );
243
+ return;
244
+ }
245
+ info("No Noir daemon is running.", opts);
246
+ return;
247
+ }
248
+ let signalled = false;
249
+ let errMsg;
250
+ try {
251
+ process.kill(rec.pid, "SIGTERM");
252
+ signalled = true;
253
+ } catch (err) {
254
+ errMsg = err instanceof Error ? err.message : String(err);
255
+ } finally {
256
+ clearDaemonRecord();
257
+ }
258
+ if (opts.json === true) {
259
+ process.stdout.write(
260
+ `${JSON.stringify({
261
+ ok: true,
262
+ data: {
263
+ running: signalled,
264
+ stopped: signalled,
265
+ pid: rec.pid,
266
+ ...errMsg !== void 0 ? { error: errMsg } : {}
267
+ }
268
+ })}
269
+ `
270
+ );
271
+ return;
272
+ }
273
+ if (signalled) {
274
+ log(`Stopped Noir daemon (pid ${rec.pid}).`, opts);
275
+ } else {
276
+ info(`Noir daemon (pid ${rec.pid}) could not be signalled: ${errMsg}`, opts);
277
+ }
278
+ }
279
+ async function daemonStatus(opts) {
280
+ const rec = readDaemonRecord();
281
+ if (!rec) {
282
+ fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (start with `noir daemon start`).", opts);
283
+ }
284
+ if (!pidAlive(rec.pid)) {
285
+ clearDaemonRecord();
286
+ fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
287
+ }
288
+ let health = null;
289
+ try {
290
+ const res = await fetch(`http://127.0.0.1:${rec.port}/health`);
291
+ if (res.status === 200) {
292
+ health = await res.json();
293
+ }
294
+ } catch {
295
+ health = null;
296
+ }
297
+ if (health?.ok !== true) {
298
+ clearDaemonRecord();
299
+ fail(
300
+ EXIT.DAEMON_DOWN,
301
+ "Noir daemon is not running (port unresponsive; stale record removed).",
302
+ opts
303
+ );
304
+ }
305
+ const uptimeSec = typeof health?.uptimeSec === "number" ? health.uptimeSec : Math.max(0, Math.floor((Date.now() - rec.startedAt) / 1e3));
306
+ const data = {
307
+ running: true,
308
+ pid: rec.pid,
309
+ port: rec.port,
310
+ startedAt: rec.startedAt,
311
+ uptimeSec,
312
+ mode: MODE
313
+ };
314
+ if (opts.json === true) {
315
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
316
+ `);
317
+ return;
318
+ }
319
+ log(
320
+ `Noir daemon: running (pid ${data.pid}, port ${data.port}, up ${formatUptime(uptimeSec)}, ${data.mode})`,
321
+ opts
322
+ );
323
+ }
324
+ async function daemonRestart(opts) {
325
+ await daemonStop({ ...opts, json: false, quiet: true });
326
+ await daemonStart(opts);
327
+ }
328
+
329
+ // src/commands/handoff.ts
330
+ import { mkdirSync, writeFileSync } from "fs";
331
+ import { join } from "path";
332
+ import {
333
+ defaultHandoffBlock,
334
+ hostLaunchDirective,
335
+ resolveAdapter
336
+ } from "@noir-ai/adapters";
337
+ import { loadProjectInfo as loadProjectInfo2, NOIR_DIR } from "@noir-ai/core";
338
+
339
+ // src/commands/task.ts
340
+ var PHASE_SKILL = {
341
+ intake: "noir-intake",
342
+ clarify: "noir-clarify",
343
+ spec: "noir-spec",
344
+ plan: "noir-plan",
345
+ execute: "noir-execute",
346
+ verify: "noir-verify",
347
+ document: "noir-document"
348
+ };
349
+ function skillFor(phase) {
350
+ if (typeof phase !== "string") return null;
351
+ return PHASE_SKILL[phase] ?? null;
352
+ }
353
+ async function fetchStatus(opts, taskId) {
354
+ const args = typeof taskId === "string" && taskId.length > 0 ? { taskId } : {};
355
+ const res = await callDaemonTool(
356
+ opts,
357
+ "workflow_status",
358
+ args
359
+ );
360
+ if (res.ok === true) return res;
361
+ const detail = res.error ?? "task not found";
362
+ fail(EXIT.NOT_FOUND, `task: ${detail}`, opts);
363
+ }
364
+ function formatStamp(ms) {
365
+ if (typeof ms !== "number" || ms <= 0) return "-";
366
+ return new Date(ms).toISOString().replace("T", " ").replace(/\..*$/, "Z");
367
+ }
368
+ function renderStatusRow(s, opts) {
369
+ const flag = s.degraded === true ? ` ${badge("warn", "degraded")}` : "";
370
+ log(`task status \u2014 ${s.taskId}${flag}`, opts);
371
+ definitionList(
372
+ [
373
+ { label: "Task", value: s.taskId },
374
+ { label: "Phase", value: s.phase },
375
+ { label: "State", value: s.state },
376
+ { label: "Mode", value: s.mode },
377
+ { label: "Next gate", value: s.nextGate ?? "\u2014" },
378
+ { label: "Updated", value: formatStamp(s.updatedAt) }
379
+ ],
380
+ opts
381
+ );
382
+ }
383
+ async function taskStatus(opts) {
384
+ const s = await fetchStatus(opts, opts.id);
385
+ if (opts.json === true) {
386
+ process.stdout.write(`${JSON.stringify({ ok: true, data: s })}
387
+ `);
388
+ return;
389
+ }
390
+ renderStatusRow(s, opts);
391
+ }
392
+ async function taskNext(opts) {
393
+ const s = await fetchStatus(opts);
394
+ const suggestion = skillFor(s.phase);
395
+ const nextSkill = skillFor(s.nextGate);
396
+ const data = {
397
+ taskId: s.taskId,
398
+ phase: s.phase,
399
+ state: s.state,
400
+ mode: s.mode,
401
+ nextGate: s.nextGate,
402
+ suggestion,
403
+ nextSkill,
404
+ degraded: s.degraded === true
405
+ };
406
+ if (opts.json === true) {
407
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
408
+ `);
409
+ return;
410
+ }
411
+ const flag = data.degraded ? ` ${badge("warn", "degraded")}` : "";
412
+ log(`task next \u2014 ${s.taskId} \xB7 phase ${s.phase} (${s.state}, ${s.mode})${flag}`, opts);
413
+ if (s.nextGate) {
414
+ info(`next gate: ${s.nextGate}`, opts);
415
+ } else {
416
+ info("no further gate ahead (past verify, or stopped).", opts);
417
+ }
418
+ if (suggestion) info(`skill for current phase (${s.phase}): ${suggestion}`, opts);
419
+ if (nextSkill) info(`skill for next gate (${s.nextGate}): ${nextSkill}`, opts);
420
+ if (!suggestion && !nextSkill) {
421
+ info("run `noir skills list` to see applicable skills.", opts);
422
+ }
423
+ }
424
+ async function taskNew(opts) {
425
+ let mode;
426
+ if (opts.mode !== void 0) {
427
+ if (opts.mode !== "full" && opts.mode !== "quick") {
428
+ fail(EXIT.USAGE, `task new: invalid mode '${opts.mode}' (expected 'full' or 'quick')`, opts);
429
+ }
430
+ mode = opts.mode;
431
+ }
432
+ const res = await callDaemonTool(
433
+ opts,
434
+ "workflow_start",
435
+ {
436
+ taskId: opts.slug,
437
+ slug: opts.slug,
438
+ ...mode === void 0 ? {} : { mode }
439
+ }
440
+ );
441
+ if (res.ok !== true) {
442
+ const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "start failed";
443
+ fail(EXIT.ERROR, `task new --slug ${opts.slug}: ${detail}`, opts);
444
+ }
445
+ if (opts.json === true) {
446
+ process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
447
+ `);
448
+ return;
449
+ }
450
+ renderStatusRow(res, opts);
451
+ }
452
+ async function taskAdvance(opts) {
453
+ const args = {};
454
+ if (typeof opts.to === "string" && opts.to.length > 0) {
455
+ if (!Object.keys(PHASE_SKILL).includes(opts.to)) {
456
+ fail(EXIT.USAGE, `task advance: invalid phase '${opts.to}'`, opts);
457
+ }
458
+ args.to = opts.to;
459
+ }
460
+ if (typeof opts.force === "string" && opts.force.length > 0) {
461
+ args.force = { reason: opts.force };
462
+ }
463
+ const res = await callDaemonTool(
464
+ opts,
465
+ "workflow_advance",
466
+ args
467
+ );
468
+ if (res.ok !== true) {
469
+ const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "advance failed";
470
+ fail(EXIT.ERROR, `task advance: ${detail}`, opts);
471
+ }
472
+ if (typeof opts.to === "string" && opts.to === "verify") {
473
+ tip("run `noir handoff` for a ready-to-paste host prompt", opts);
474
+ }
475
+ if (opts.json === true) {
476
+ process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
477
+ `);
478
+ return;
479
+ }
480
+ renderStatusRow(res, opts);
481
+ }
482
+
483
+ // src/commands/handoff.ts
484
+ var SEED_LIMIT = 5;
485
+ function heading(name, id) {
486
+ return `# Noir handoff \u2014 ${name} (${id})`;
487
+ }
488
+ function renderMarkdown(p) {
489
+ const lines = [];
490
+ lines.push(heading(p.project.name, p.project.id));
491
+ lines.push("");
492
+ if (p.task) {
493
+ const gate = p.task.nextGate ?? "\u2014";
494
+ lines.push(`**Phase:** ${p.task.phase} \u2014 next gate: ${gate}`);
495
+ } else {
496
+ lines.push("**Phase:** no active task");
497
+ }
498
+ lines.push("");
499
+ lines.push("## Open host");
500
+ lines.push(p.directive);
501
+ lines.push("");
502
+ lines.push("## Next step");
503
+ if (p.task?.nextSkill) {
504
+ lines.push(`Run \`${p.task.nextSkill}\` \u2014 the host's next gate skill.`);
505
+ } else {
506
+ lines.push("No further gate ahead (past verify, or no active task).");
507
+ }
508
+ lines.push("");
509
+ lines.push("## Extracted context (seed)");
510
+ if (p.contextSeed && p.contextSeed.length > 0) {
511
+ for (const h of p.contextSeed) {
512
+ lines.push(`- \`${h.path}\` (score ${h.score.toFixed(4)})`);
513
+ const snip = h.snippet.trim();
514
+ if (snip.length > 0) lines.push(` > ${snip.replace(/\n/g, "\n > ")}`);
515
+ }
516
+ } else if (p.degraded.embedderMissing) {
517
+ lines.push(
518
+ "_Embedder unavailable \u2014 degraded to BM25-only or no hits. Start `noir daemon start` for live context._"
519
+ );
520
+ } else {
521
+ lines.push("_No context hits (or daemon down). Start `noir daemon start` for live context._");
522
+ }
523
+ lines.push("");
524
+ lines.push("## Extracted memory (seed)");
525
+ if (p.memorySeed && p.memorySeed.length > 0) {
526
+ for (const m of p.memorySeed) {
527
+ lines.push(`- \`${m.id}\` \u2014 ${m.summary}`);
528
+ }
529
+ } else {
530
+ lines.push(
531
+ "_No memory observations (or daemon down). Start `noir daemon start` for live memory._"
532
+ );
533
+ }
534
+ lines.push("");
535
+ lines.push("## Live data \u2014 call these from the host (already wired via the host MCP config)");
536
+ lines.push("- `noir.workflow_status` \xB7 `noir.context_search` \xB7 `noir.memory_recall`");
537
+ if (p.degraded.daemonDown) {
538
+ lines.push("");
539
+ lines.push("> **Note:** daemon not running \u2014 start `noir daemon start` for live task/memory.");
540
+ }
541
+ lines.push("");
542
+ return lines.join("\n");
543
+ }
544
+ function toContextHit(raw) {
545
+ const h = raw ?? {};
546
+ return {
547
+ path: typeof h.path === "string" ? h.path : "<unknown>",
548
+ score: typeof h.score === "number" ? h.score : 0,
549
+ snippet: typeof h.snippet === "string" ? h.snippet : ""
550
+ };
551
+ }
552
+ function observationSummary(raw) {
553
+ const obs = raw.observation;
554
+ if (obs && typeof obs === "object") {
555
+ const content = obs.content;
556
+ if (typeof content === "string" && content.length > 0) {
557
+ return content.length > 160 ? `${content.slice(0, 157)}\u2026` : content;
558
+ }
559
+ const type = obs.type;
560
+ if (typeof type === "string") return `(${type})`;
561
+ }
562
+ return "";
563
+ }
564
+ function toMemoryHit(raw) {
565
+ const h = raw ?? {};
566
+ return {
567
+ id: typeof h.id === "string" ? h.id : "<unknown>",
568
+ summary: observationSummary(h)
569
+ };
570
+ }
571
+ function toHostPayload(p) {
572
+ return {
573
+ project: p.project,
574
+ host: p.host,
575
+ task: p.task
576
+ };
577
+ }
578
+ function resolveDirective(root, p) {
579
+ const ctx = { root };
580
+ const adapter = resolveAdapter(p.host);
581
+ const hostPayload = toHostPayload(p);
582
+ if (typeof adapter.emitHandoff === "function") {
583
+ return adapter.emitHandoff(ctx, hostPayload);
584
+ }
585
+ return defaultHandoffBlock(ctx, hostPayload);
586
+ }
587
+ function handoffFilePath(root, taskId, projectId) {
588
+ const id = typeof taskId === "string" && taskId.length > 0 ? taskId : projectId;
589
+ return join(root, NOIR_DIR, "handoff", `${id}.md`);
590
+ }
591
+ async function extractSeeds(opts, probe, query) {
592
+ if (!probe.running) return { context: null, memory: null };
593
+ let context = null;
594
+ let memory = null;
595
+ await withRunningDaemon(
596
+ opts,
597
+ async (caller) => {
598
+ const ctxRes = await tryTool(caller, "context_search", {
599
+ query,
600
+ limit: SEED_LIMIT
601
+ });
602
+ if (ctxRes && ctxRes.ok !== false && Array.isArray(ctxRes.results)) {
603
+ context = ctxRes.results.slice(0, SEED_LIMIT).map(toContextHit).filter((h) => h.path !== "<unknown>" || h.snippet.length > 0);
604
+ }
605
+ const memRes = await tryTool(caller, "memory_recall", {
606
+ query,
607
+ limit: SEED_LIMIT
608
+ });
609
+ if (memRes && memRes.ok !== false && Array.isArray(memRes.results)) {
610
+ memory = memRes.results.slice(0, SEED_LIMIT).map(toMemoryHit).filter((h) => h.summary.length > 0);
611
+ }
612
+ },
613
+ probe
614
+ );
615
+ return { context, memory };
616
+ }
617
+ async function probeOnly(opts) {
618
+ return probeDaemon(opts);
619
+ }
620
+ async function handoff(opts) {
621
+ const project = loadProjectInfo2(process.cwd());
622
+ const host = project.config.host;
623
+ const snapshot = await gatherStatusPayload(opts);
624
+ const probe = await probeOnly(opts);
625
+ const task = snapshot.workflow ? {
626
+ taskId: snapshot.workflow.taskId,
627
+ phase: snapshot.workflow.phase,
628
+ nextGate: snapshot.workflow.nextGate,
629
+ nextSkill: skillFor(snapshot.workflow.nextGate) ?? skillFor(snapshot.workflow.phase)
630
+ } : null;
631
+ const query = task?.taskId ?? project.name;
632
+ const { context, memory } = await extractSeeds(opts, probe, query);
633
+ const full = {
634
+ project: { id: project.id, name: project.name },
635
+ host,
636
+ directive: "",
637
+ // filled after the host payload is assembled (below)
638
+ task,
639
+ contextSeed: context,
640
+ memorySeed: memory,
641
+ degraded: {
642
+ daemonDown: !probe.running,
643
+ embedderMissing: context === null && probe.running
644
+ }
645
+ };
646
+ full.directive = resolveDirective(project.root, full);
647
+ if (opts.json === true) {
648
+ process.stdout.write(`${JSON.stringify({ ok: true, data: full })}
649
+ `);
650
+ return;
651
+ }
652
+ const md = renderMarkdown(full);
653
+ if (opts.write === true) {
654
+ const path = handoffFilePath(project.root, task?.taskId, project.id);
655
+ mkdirSync(join(project.root, NOIR_DIR, "handoff"), { recursive: true });
656
+ writeFileSync(path, md, "utf8");
657
+ info(`handoff written to ${path}`, opts);
658
+ return;
659
+ }
660
+ process.stdout.write(md);
661
+ }
662
+
663
+ // src/commands/home.ts
664
+ import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
665
+ import { loadProjectInfo as loadProjectInfo3 } from "@noir-ai/core";
666
+
667
+ // src/banner.ts
668
+ import gradient from "gradient-string";
669
+ var NOIR_BLOCK = [
670
+ "\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
671
+ "\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",
672
+ "\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",
673
+ "\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",
674
+ "\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",
675
+ "\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D"
676
+ ];
677
+ var NOIR_GRADIENT = gradient("#2c5282", "#3b82f6", "#7dd3fc");
678
+ var NOIR_TAGLINE = "discipline, context, and memory layer for agentic CLIs";
679
+ function renderBanner(opts = {}) {
680
+ const width = opts.width ?? process.stdout.columns ?? 80;
681
+ const colorOn = opts.color !== false && useColor();
682
+ if (width < 50) {
683
+ return colorOn ? `${c.accent("\u25C6")} noir` : "\u25C6 noir";
684
+ }
685
+ const block = NOIR_BLOCK.join("\n");
686
+ if (!colorOn) return block;
687
+ if (accessibleMode()) {
688
+ return block.split("\n").map((line) => c.accent(line)).join("\n");
689
+ }
690
+ return NOIR_GRADIENT.multiline(block);
691
+ }
692
+ function shouldShowBanner(opts) {
693
+ if (opts.quiet === true || opts.json === true) return false;
694
+ const v = process.env.NOIR_NO_BANNER;
695
+ if (v !== void 0 && v !== "") return false;
696
+ return true;
697
+ }
698
+
699
+ // src/commands/home.ts
700
+ function tryProject() {
701
+ try {
702
+ const info2 = loadProjectInfo3(process.cwd());
703
+ return { id: info2.id, name: info2.name, host: info2.config.host };
704
+ } catch {
705
+ return null;
706
+ }
707
+ }
708
+ var COMMANDS_HINT = "Commands: init \xB7 create \xB7 sync \xB7 status \xB7 context \xB7 memory \xB7 skills \xB7 task \xB7 daemon \xB7 doctor";
709
+ async function home(opts, deps) {
710
+ if (isInteractive(opts) && opts.tui !== false) {
711
+ await runMenu(opts, deps);
712
+ return;
713
+ }
714
+ if (opts.json === true) {
715
+ await deps.dispatch(["status", "--json"]);
716
+ return;
717
+ }
718
+ await deps.dispatch(["status"]);
719
+ }
720
+ async function runMenu(opts, deps) {
721
+ const clack = await import("@clack/prompts");
722
+ const project = tryProject();
723
+ if (shouldShowBanner(opts)) {
724
+ const host = project?.host ?? "claude";
725
+ process.stderr.write(
726
+ `
727
+ ${renderBanner()}
728
+ ${NOIR_TAGLINE}
729
+
730
+ ${hostLaunchDirective2(host)}
731
+ ${COMMANDS_HINT}
732
+
733
+ `
734
+ );
735
+ }
736
+ clack.intro(project ? `noir \u2014 ${project.name}` : "noir");
737
+ const choice = await clack.select({
738
+ message: "What would you like to do?",
739
+ initialValue: "status",
740
+ options: [
741
+ { value: "status", label: "Status", hint: "project + daemon + store snapshot" },
742
+ { value: "index", label: "Index project", hint: "(re)index files into context" },
743
+ { value: "recall", label: "Recall memory", hint: "search cross-session memory" },
744
+ { value: "next", label: "Next task", hint: "suggest next phase + skill" },
745
+ { value: "handoff", label: "Handoff", hint: "ready-to-paste host prompt" },
746
+ { value: "daemon", label: "Start daemon", hint: "foreground daemon" },
747
+ { value: "sync", label: "Sync skills", hint: "re-emit builtin skills" },
748
+ { value: "exit", label: "Exit" }
749
+ ]
750
+ });
751
+ if (clack.isCancel(choice)) {
752
+ clack.cancel("Cancelled.");
753
+ fail(EXIT.CANCELLED, "cancelled", opts);
754
+ }
755
+ const argv = await argvForChoice(choice, clack, opts);
756
+ if (argv === null) {
757
+ clack.outro("bye");
758
+ return;
759
+ }
760
+ await deps.dispatch(argv);
761
+ clack.outro("done");
762
+ }
763
+ async function argvForChoice(choice, clack, opts) {
764
+ switch (choice) {
765
+ case "status":
766
+ return ["status"];
767
+ case "index":
768
+ return ["context", "index"];
769
+ case "next":
770
+ return ["task", "next"];
771
+ case "handoff":
772
+ return ["handoff"];
773
+ case "daemon":
774
+ return ["daemon", "start"];
775
+ case "sync":
776
+ return ["sync"];
777
+ case "exit":
778
+ return null;
779
+ case "recall": {
780
+ const query = await clack.text({
781
+ message: "Recall query:",
782
+ placeholder: "e.g. auth flow, ContextEngine, deploy steps"
783
+ });
784
+ if (clack.isCancel(query)) {
785
+ clack.cancel("Cancelled.");
786
+ fail(EXIT.CANCELLED, "cancelled", opts);
787
+ }
788
+ return ["memory", "recall", String(query)];
789
+ }
790
+ default:
791
+ return null;
792
+ }
793
+ }
794
+
795
+ // src/commands/memory.ts
796
+ function parseLimit2(raw, label, opts) {
797
+ if (raw === void 0) return void 0;
798
+ const n = Number(raw);
799
+ if (!Number.isInteger(n) || n <= 0) {
800
+ fail(EXIT.USAGE, `${label}: --limit must be a positive integer (got '${raw}')`, opts);
801
+ }
802
+ return n;
803
+ }
804
+ function splitCsv(raw) {
805
+ if (raw === void 0) return void 0;
806
+ const out = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
807
+ return out.length > 0 ? out : void 0;
808
+ }
809
+ function toHit2(raw) {
810
+ const h = raw ?? {};
811
+ const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
812
+ return {
813
+ id: typeof h.id === "string" ? h.id : "",
814
+ type: typeof h.type === "string" ? h.type : "",
815
+ score: typeof h.score === "number" ? h.score : 0,
816
+ content: typeof h.content === "string" ? h.content : "",
817
+ concepts: arr(h.concepts),
818
+ files: arr(h.files),
819
+ ts: typeof h.ts === "number" ? h.ts : 0,
820
+ importance: typeof h.importance === "number" ? h.importance : 0,
821
+ source: typeof h.source === "string" ? h.source : ""
822
+ };
823
+ }
824
+ function failTool2(label, envelope, opts) {
825
+ const detail = envelope.error ?? envelope.reason ?? "unknown failure";
826
+ fail(EXIT.ERROR, `${label}: ${detail}`, opts);
827
+ }
828
+ function stamp(ts) {
829
+ if (typeof ts !== "number" || ts <= 0) return "-";
830
+ return new Date(ts).toISOString().replace("T", " ").replace(/\..*$/, "Z");
831
+ }
832
+ async function memoryRecall(opts) {
833
+ const limit = parseLimit2(opts.limit, "memory recall", opts);
834
+ const res = await callDaemonTool(opts, "memory_recall", {
835
+ query: opts.query,
836
+ ...limit === void 0 ? {} : { limit }
837
+ });
838
+ if (res.ok !== true) failTool2("memory recall", res, opts);
839
+ const hits = Array.isArray(res.results) ? res.results.map(toHit2) : [];
840
+ const data = { query: opts.query, hits, degraded: res.degraded === true };
841
+ if (opts.json === true) {
842
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
843
+ `);
844
+ return;
845
+ }
846
+ renderRecall(data.query, hits, data.degraded, opts);
847
+ }
848
+ function renderRecall(query, hits, degraded, opts) {
849
+ const flag = degraded ? ` ${badge("warn", "degraded: BM25-only")}` : "";
850
+ log(
851
+ `memory recall \u2014 ${hits.length} hit${hits.length === 1 ? "" : "s"} for '${query}'${flag}`,
852
+ opts
853
+ );
854
+ if (hits.length === 0) {
855
+ info("(no memories matched)", opts);
856
+ return;
857
+ }
858
+ for (let i = 0; i < hits.length; i++) {
859
+ const h = hits[i];
860
+ if (h === void 0) continue;
861
+ const head = `[${i + 1}] ${h.type} \xB7 score ${h.score.toFixed(4)} \xB7 importance ${h.importance.toFixed(2)} \xB7 ${h.id}`;
862
+ log(head, opts);
863
+ if (h.files.length > 0) log(` files: ${h.files.join(", ")}`, opts);
864
+ if (h.concepts.length > 0) log(` tags: ${h.concepts.join(", ")}`, opts);
865
+ log(` ${h.content}`, opts);
866
+ }
867
+ }
868
+ async function memorySave(opts) {
869
+ const content = await resolveContent(opts);
870
+ const files = splitCsv(opts.files);
871
+ const args = { content };
872
+ if (typeof opts.type === "string" && opts.type.length > 0) args.type = opts.type;
873
+ if (files !== void 0) args.files = files;
874
+ const res = await callDaemonTool(opts, "memory_save", args);
875
+ if (res.ok !== true) failTool2("memory save", res, opts);
876
+ const id = typeof res.id === "string" ? res.id : "";
877
+ const observation = res.observation ?? {};
878
+ const data = { id, observation };
879
+ if (opts.json === true) {
880
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
881
+ `);
882
+ return;
883
+ }
884
+ log(`Saved memory ${id}.`, opts);
885
+ renderObservation(observation, opts);
886
+ }
887
+ async function resolveContent(opts) {
888
+ if (typeof opts.content === "string" && opts.content.length > 0) return opts.content;
889
+ if (!isInteractive(opts)) {
890
+ fail(
891
+ EXIT.USAGE,
892
+ "memory save requires --content <text> (or re-run in an interactive terminal to be prompted)",
893
+ opts
894
+ );
895
+ }
896
+ const clack = await import("@clack/prompts");
897
+ const value = await clack.text({
898
+ message: "Memory to save:",
899
+ placeholder: "the insight / decision / pattern to remember",
900
+ validate: (v) => v.trim().length === 0 ? "content cannot be empty" : void 0
901
+ });
902
+ if (clack.isCancel(value)) {
903
+ clack.cancel("Cancelled.");
904
+ fail(EXIT.CANCELLED, "cancelled", opts);
905
+ }
906
+ return String(value);
907
+ }
908
+ function renderObservation(obs, opts) {
909
+ const rows = [];
910
+ for (const key of ["id", "type", "importance", "ts", "source", "project", "sessionId"]) {
911
+ if (obs[key] !== void 0) rows.push({ label: key, value: obs[key] });
912
+ }
913
+ if (rows.length > 0) definitionList(rows, opts);
914
+ const content = obs.content;
915
+ if (typeof content === "string") log(`
916
+ ${content}`, opts);
917
+ }
918
+ async function memorySessions(opts) {
919
+ const res = await callDaemonTool(opts, "memory_sessions");
920
+ if (res.ok !== true) failTool2("memory sessions", res, opts);
921
+ const sessions = Array.isArray(res.sessions) ? res.sessions.map((raw) => {
922
+ const s = raw ?? {};
923
+ return {
924
+ id: typeof s.id === "string" ? s.id : "",
925
+ count: typeof s.count === "number" ? s.count : 0,
926
+ lastTs: typeof s.lastTs === "number" ? s.lastTs : 0
927
+ };
928
+ }) : [];
929
+ const data = { sessions };
930
+ if (opts.json === true) {
931
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
932
+ `);
933
+ return;
934
+ }
935
+ log(`memory sessions \u2014 ${sessions.length} session${sessions.length === 1 ? "" : "s"}`, opts);
936
+ table(
937
+ sessions.map((s) => ({ Session: s.id, Observations: s.count, "Last seen": stamp(s.lastTs) })),
938
+ ["Session", "Observations", "Last seen"],
939
+ opts
940
+ );
941
+ }
942
+ async function memoryForget(opts) {
943
+ if (opts.ids.length === 0) {
944
+ fail(EXIT.USAGE, "memory forget requires at least one <id>", opts);
945
+ }
946
+ const res = await callDaemonTool(opts, "memory_forget", {
947
+ ids: opts.ids
948
+ });
949
+ if (res.ok !== true) failTool2("memory forget", res, opts);
950
+ const deleted = typeof res.deleted === "number" ? res.deleted : 0;
951
+ const data = { deleted, ids: opts.ids };
952
+ if (opts.json === true) {
953
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
954
+ `);
955
+ return;
956
+ }
957
+ log(`Forgot ${deleted} observation${deleted === 1 ? "" : "s"}.`, opts);
958
+ }
959
+ async function memoryConsolidate(opts) {
960
+ const exposed = await withDaemon(opts, async (caller) => caller.listTools());
961
+ if (!exposed.includes("memory_consolidate")) {
962
+ fail(
963
+ EXIT.ERROR,
964
+ "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.",
965
+ opts
966
+ );
967
+ }
968
+ const types = splitCsv(opts.types);
969
+ const limit = parseLimit2(opts.limit, "memory consolidate", opts);
970
+ const args = {};
971
+ if (types !== void 0) args.types = types;
972
+ if (limit !== void 0) args.limit = limit;
973
+ const res = await callDaemonTool(
974
+ opts,
975
+ "memory_consolidate",
976
+ args
977
+ );
978
+ if (res.ok === true) {
979
+ const lessons = Array.isArray(res.lessons) ? res.lessons : [];
980
+ const from = Array.isArray(res.from) ? res.from : [];
981
+ const data = { lessons, from };
982
+ if (opts.json === true) {
983
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
984
+ `);
985
+ return;
986
+ }
987
+ log(
988
+ `Consolidated ${lessons.length} lesson${lessons.length === 1 ? "" : "s"} from ${from.length} observation${from.length === 1 ? "" : "s"}.`,
989
+ opts
990
+ );
991
+ for (let i = 0; i < lessons.length; i++) {
992
+ const l = lessons[i];
993
+ if (l === void 0) continue;
994
+ const obs = l ?? {};
995
+ const id = typeof obs.id === "string" ? obs.id : `<lesson ${i + 1}>`;
996
+ const content = typeof obs.content === "string" ? obs.content : "";
997
+ log(`- ${id}: ${content}`, opts);
998
+ }
999
+ return;
1000
+ }
1001
+ const env = res;
1002
+ const reason = (typeof env.reason === "string" ? env.reason : void 0) ?? (typeof env.error === "string" ? env.error : void 0) ?? "unknown";
1003
+ const logged = env.logged === true || env.degraded === true;
1004
+ const suffix = logged ? " (logged)" : "";
1005
+ fail(EXIT.ERROR, `memory consolidate did not run: ${reason}${suffix}`, opts);
1006
+ }
1007
+
1008
+ // src/commands/skills.ts
1009
+ import { claudeAdapter } from "@noir-ai/adapters";
1010
+ import { loadProjectInfo as loadProjectInfo4 } from "@noir-ai/core";
1011
+ import {
1012
+ discoverAll,
1013
+ emitSkillsToDir
1014
+ } from "@noir-ai/skills";
1015
+ var CATEGORY = {
1016
+ "noir-brainstorm": "discovery",
1017
+ "noir-intake": "discovery",
1018
+ "noir-clarify": "discovery",
1019
+ "noir-explore": "discovery",
1020
+ "noir-spec": "spec",
1021
+ "noir-plan": "plan",
1022
+ "noir-execute": "execute",
1023
+ "noir-tdd": "execute",
1024
+ "noir-debug": "execute",
1025
+ "noir-subagent": "execute",
1026
+ "noir-parallel": "execute",
1027
+ "noir-verify": "verify",
1028
+ "noir-review": "verify",
1029
+ "noir-security": "verify",
1030
+ "noir-test": "verify",
1031
+ "noir-document": "document",
1032
+ "noir-readme": "document",
1033
+ "noir-commit": "git",
1034
+ "noir-branch": "git",
1035
+ "noir-pr": "git",
1036
+ "noir-worktree": "git",
1037
+ "noir-recall": "memory",
1038
+ "noir-remember": "memory",
1039
+ "noir-checkpoint": "memory",
1040
+ "noir-context": "context",
1041
+ "noir-frontend": "domain",
1042
+ "noir-backend": "domain",
1043
+ "noir-doctor": "meta",
1044
+ "noir-skill-author": "meta",
1045
+ "noir-sync": "meta",
1046
+ "noir-wrap": "meta"
1047
+ };
1048
+ function categoryOf(name) {
1049
+ const mapped = CATEGORY[name];
1050
+ if (mapped !== void 0) return mapped;
1051
+ return name.replace(/^noir-/, "") || "general";
1052
+ }
1053
+ function toRow(s, kind = "builtin") {
1054
+ const description = typeof s.frontmatter.description === "string" ? s.frontmatter.description : "";
1055
+ return { name: s.name, category: categoryOf(s.name), description, kind };
1056
+ }
1057
+ function truncate(text, max) {
1058
+ if (text.length <= max) return text;
1059
+ const slice = text.slice(0, Math.max(0, max - 1));
1060
+ const boundary = slice.lastIndexOf(" ");
1061
+ const head = boundary > 0 ? slice.slice(0, boundary) : slice;
1062
+ return `${head.trimEnd()}\u2026`;
1063
+ }
1064
+ async function skillsList(opts) {
1065
+ let builtins;
1066
+ let integrations;
1067
+ try {
1068
+ const all = discoverAll();
1069
+ builtins = all.builtins;
1070
+ integrations = all.integrations;
1071
+ } catch (err) {
1072
+ fail(
1073
+ EXIT.ERROR,
1074
+ `skills list: could not discover skills (${err instanceof Error ? err.message : String(err)})`,
1075
+ opts
1076
+ );
1077
+ }
1078
+ const rows = [
1079
+ ...builtins.map((b) => toRow(b, "builtin")),
1080
+ ...integrations.map((i) => toRow(i, "integration"))
1081
+ ];
1082
+ const data = { count: rows.length, skills: rows };
1083
+ if (opts.json === true) {
1084
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
1085
+ `);
1086
+ return;
1087
+ }
1088
+ const intCount = integrations.length;
1089
+ 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"}`;
1090
+ log(banner, opts);
1091
+ table(
1092
+ rows.map((r) => ({
1093
+ Skill: r.name,
1094
+ Kind: r.kind,
1095
+ Category: r.category,
1096
+ Description: truncate(r.description, 80)
1097
+ })),
1098
+ ["Skill", "Kind", "Category", "Description"],
1099
+ opts
1100
+ );
1101
+ }
1102
+ async function skillsSync(opts) {
1103
+ const root = process.cwd();
1104
+ const project = loadProjectInfo4(root);
1105
+ if (!claudeAdapter.skillsDir) {
1106
+ const data2 = {
1107
+ emitted: [],
1108
+ references: 0,
1109
+ host: project.config.host,
1110
+ reason: "no-skill-emitter"
1111
+ };
1112
+ if (opts.json === true) {
1113
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
1114
+ `);
1115
+ return;
1116
+ }
1117
+ info("This host has no skill emitter; nothing to sync.", opts);
1118
+ return;
1119
+ }
1120
+ const dir = claudeAdapter.skillsDir({ root });
1121
+ const summary = await emitSkillsToDir(dir);
1122
+ const pruned = summary.pruned ?? [];
1123
+ const data = {
1124
+ emitted: summary.emitted,
1125
+ references: summary.references,
1126
+ dir,
1127
+ pruned
1128
+ };
1129
+ if (opts.json === true) {
1130
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
1131
+ `);
1132
+ return;
1133
+ }
1134
+ log(
1135
+ `Synced ${summary.emitted.length} Noir skill${summary.emitted.length === 1 ? "" : "s"} to ${dir}.`,
1136
+ opts
1137
+ );
1138
+ if (pruned.length > 0) {
1139
+ warn(
1140
+ `Pruned ${pruned.length} stale noir-* skill dir${pruned.length === 1 ? "" : "s"}: ${pruned.join(", ")}`,
1141
+ opts
1142
+ );
1143
+ }
1144
+ }
1145
+
1146
+ // src/serve.ts
1147
+ import { loadProjectInfo as loadProjectInfo5 } from "@noir-ai/core";
1148
+ import { ensureDaemonRunning as ensureDaemonRunning2, startStdioServer } from "@noir-ai/daemon";
1149
+ async function serve(opts) {
1150
+ const project = loadProjectInfo5(process.cwd());
1151
+ if (opts.stdio) {
1152
+ await startStdioServer({ project, transport: "stdio", daemon: false });
1153
+ return;
1154
+ }
1155
+ try {
1156
+ const { url } = await ensureDaemonRunning2({
1157
+ project,
1158
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec
1159
+ });
1160
+ process.stderr.write(
1161
+ `Noir daemon available at ${url}. (For HTTP clients, use this URL in .mcp.json.)
1162
+ `
1163
+ );
1164
+ } catch (err) {
1165
+ process.stderr.write(
1166
+ `Noir daemon unavailable (${err instanceof Error ? err.message : String(err)}); falling back to stdio.
1167
+ `
1168
+ );
1169
+ await startStdioServer({ project, transport: "stdio", daemon: false });
1170
+ }
1171
+ }
1172
+
1173
+ // src/bin.ts
1174
+ function actionGlobals(args) {
1175
+ const last = args[args.length - 1];
1176
+ return last?.optsWithGlobals() ?? {};
1177
+ }
1178
+ function trailingCmd(args) {
1179
+ const last = args[args.length - 1];
1180
+ if (!(last instanceof Command)) {
1181
+ throw new NoirCliError(
1182
+ EXIT.ERROR,
1183
+ "internal: commander action callback received no trailing Command"
1184
+ );
1185
+ }
1186
+ return last;
1187
+ }
1188
+ function toCliOptions(g) {
1189
+ return {
1190
+ json: g.json === true,
1191
+ quiet: g.quiet === true,
1192
+ verbose: g.verbose === true,
1193
+ input: g.input !== false,
1194
+ // TUI policy — additive only. `--tui`/`--no-tui` land on `tui`
1195
+ // (true/false; absent when neither flag is given). Spread conditionally so
1196
+ // the default-args case (no flag) does NOT add a `tui` key — keeps the
1197
+ // exact-shape assertions in bin.test.ts green.
1198
+ ...g.tui === true || g.tui === false ? { tui: g.tui === true } : {},
1199
+ // `--no-tips` is stored under `tips` (default true; flag ⇒ false). Map to
1200
+ // the additive `noTips` spelling used by CliOptions + `tip()`.
1201
+ ...g.tips === false ? { noTips: true } : {}
1202
+ };
1203
+ }
1204
+ function toStatusOptions(g) {
1205
+ return {
1206
+ json: g.json === true,
1207
+ quiet: g.quiet === true,
1208
+ verbose: g.verbose === true,
1209
+ input: g.input !== false
1210
+ };
1211
+ }
1212
+ function parseHost(raw) {
1213
+ if (raw === void 0) return void 0;
1214
+ return raw;
1215
+ }
1216
+ var DEPRECATIONS = [];
1217
+ function commandPath(cmd) {
1218
+ const path = [];
1219
+ let c2 = cmd;
1220
+ while (c2 !== null) {
1221
+ path.unshift(c2.name());
1222
+ c2 = c2.parent;
1223
+ }
1224
+ return path;
1225
+ }
1226
+ function emitDeprecationHintsFor(cmd, opts) {
1227
+ if (DEPRECATIONS.length === 0) return;
1228
+ const rel = commandPath(cmd).slice(1);
1229
+ for (const d of DEPRECATIONS) {
1230
+ if (d.oldArgv.every((a, i) => rel[i] === a)) {
1231
+ tip(
1232
+ `\`noir ${d.oldArgv.join(" ")}\` is deprecated since v${d.since}; use \`noir ${d.newArgv.join(" ")}\`.`,
1233
+ opts
1234
+ );
1235
+ }
1236
+ }
1237
+ }
1238
+ function createProgram() {
1239
+ const program2 = new Command();
1240
+ program2.name("noir").description("Noir \u2014 discipline, context, and memory layer for agentic CLIs.").version(NOIR_VERSION, "-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(
1241
+ new Option("--tui", "prefer the interactive home menu for bare `noir` (advisory; TTY-only)")
1242
+ ).addOption(
1243
+ new Option(
1244
+ "--no-tui",
1245
+ "route bare `noir` to the non-interactive `status` path even in a TTY"
1246
+ )
1247
+ ).addOption(new Option("--no-tips", "suppress redirect / deprecation hints on stderr")).exitOverride((err) => {
1248
+ throw err;
1249
+ }).configureOutput({
1250
+ // Stream discipline: --help/--version → stdout (conventional; users pipe
1251
+ // them), errors → stderr. --json data is emitted by command actions via
1252
+ // process.stdout directly, NOT by commander's writeOut, so routing help/
1253
+ // version to stdout does not conflict with --json payloads.
1254
+ writeOut: (str) => process.stdout.write(str),
1255
+ writeErr: (str) => process.stderr.write(str)
1256
+ });
1257
+ program2.hook("preAction", (_thisCmd, actionCmd) => {
1258
+ const opts = actionCmd.optsWithGlobals();
1259
+ const cwd = opts.cwd;
1260
+ if (typeof cwd === "string" && cwd.length > 0) {
1261
+ try {
1262
+ process.chdir(cwd);
1263
+ } catch (err) {
1264
+ throw new NoirCliError(
1265
+ EXIT.USAGE,
1266
+ `--cwd: ${err instanceof Error ? err.message : String(err)}`
1267
+ );
1268
+ }
1269
+ }
1270
+ const nonInteractive = opts.json === true || opts.input === false;
1271
+ if (nonInteractive) process.env.NOIR_NON_INTERACTIVE = "1";
1272
+ else delete process.env.NOIR_NON_INTERACTIVE;
1273
+ emitDeprecationHintsFor(actionCmd, toCliOptions(opts));
1274
+ });
1275
+ 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(
1276
+ "--upgrade",
1277
+ "run scaffold migrations before re-emitting (re-run on an existing project)"
1278
+ ).option("--force", "re-scaffold even if already initialized (bypasses the already-init no-op)").addOption(
1279
+ // S10: target host. Defaults to `'claude'` (the regression anchor). The
1280
+ // chosen host is forwarded to scaffold() + skills emission via
1281
+ // resolveAdapter. The choice list mirrors `SUPPORTED_HOSTS` so a new host
1282
+ // lands here automatically; commander rejects anything else as usage=2.
1283
+ new Option(
1284
+ "--host <id>",
1285
+ "target agentic CLI (default: claude) \u2014 drives host-side emission"
1286
+ ).choices(SUPPORTED_HOSTS)
1287
+ ).action(
1288
+ async (opts, cmd) => {
1289
+ const transport = opts.transport === "streamable-http" ? "streamable-http" : "stdio";
1290
+ const upgrade = opts.upgrade === true;
1291
+ const force = opts.force === true;
1292
+ const host = parseHost(opts.host);
1293
+ const result = await init(process.cwd(), {
1294
+ transport,
1295
+ url: opts.url,
1296
+ ...upgrade ? { upgrade } : {},
1297
+ ...force ? { force } : {},
1298
+ ...host !== void 0 ? { host } : {}
1299
+ });
1300
+ if (cmd.optsWithGlobals().json === true && result !== void 0) {
1301
+ json({ ok: true, data: result });
1302
+ }
1303
+ }
1304
+ );
1305
+ 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)").addOption(
1306
+ new Option(
1307
+ "--host <id>",
1308
+ "target agentic CLI (default: claude) \u2014 drives host-side emission"
1309
+ ).choices(SUPPORTED_HOSTS)
1310
+ ).action(
1311
+ async (dir, opts, cmd) => {
1312
+ const transport = opts.transport === "streamable-http" ? "streamable-http" : "stdio";
1313
+ const force = opts.force === true;
1314
+ const host = parseHost(opts.host);
1315
+ const { create } = await import("./create-U2YJH5QC.js");
1316
+ const result = await create(dir, {
1317
+ transport,
1318
+ url: opts.url,
1319
+ ...force ? { force } : {},
1320
+ ...host !== void 0 ? { host } : {}
1321
+ });
1322
+ if (cmd.optsWithGlobals().json === true && result !== void 0) {
1323
+ json({ ok: true, data: result });
1324
+ }
1325
+ }
1326
+ );
1327
+ program2.command("sync").description(
1328
+ "re-emit Noir managed files (.mcp.json, CLAUDE.md blocks, NOIR.md brief, ignores) + skills"
1329
+ ).option(
1330
+ "--force",
1331
+ "overwrite differing regenerated files without prompting (bypasses the conflict menu)"
1332
+ ).option(
1333
+ "--merge",
1334
+ "three-way merge managed regions (default since 1.3.0; flag kept for compatibility)"
1335
+ ).addOption(
1336
+ // Opt OUT of managed-region merge (restore strip-replace). Commander
1337
+ // stores `--no-merge-regions` under `mergeRegions` (default true; flag →
1338
+ // false). Bare `noir sync` keeps the merge default (true).
1339
+ new Option(
1340
+ "--no-merge-regions",
1341
+ "strip-replace managed regions (discard hand-edits inside <!-- noir:* --> markers)"
1342
+ )
1343
+ ).addOption(
1344
+ // S10: optional `--host` override. When omitted, sync reads host from
1345
+ // `.noir/config.yml` (whatever `noir init --host <id>` persisted). The
1346
+ // override is rarely needed — documented as advanced.
1347
+ new Option(
1348
+ "--host <id>",
1349
+ "override the configured host (advanced; default reads .noir/config.yml)"
1350
+ ).choices(SUPPORTED_HOSTS)
1351
+ ).action(
1352
+ async (opts, cmd) => {
1353
+ const { sync } = await import("./sync-VYRXCGK4.js");
1354
+ const host = parseHost(opts.host);
1355
+ const force = opts.force === true;
1356
+ const merge = opts.merge === true;
1357
+ const noMergeRegions = opts.mergeRegions === false;
1358
+ const result = host === void 0 && !force && !merge && !noMergeRegions ? await sync(process.cwd()) : await sync(process.cwd(), {
1359
+ ...host !== void 0 ? { host } : {},
1360
+ ...force ? { force } : {},
1361
+ ...merge ? { merge } : {},
1362
+ ...noMergeRegions ? { mergeManagedRegions: false } : {}
1363
+ });
1364
+ if (cmd.optsWithGlobals().json === true) {
1365
+ json({ ok: true, data: result });
1366
+ }
1367
+ }
1368
+ );
1369
+ const mcpCmd = program2.command("mcp").description("MCP server control");
1370
+ mcpCmd.command("serve").description("run the Noir MCP server (stdio, or via the shared daemon)").option("--stdio", "force the stdio transport").action(async (opts) => {
1371
+ await serve({ stdio: opts.stdio === true });
1372
+ });
1373
+ mcpCmd.action(() => {
1374
+ throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio]");
1375
+ });
1376
+ const daemonGrp = program2.command("daemon").description("control the Noir daemon");
1377
+ daemonGrp.command("start").description("start the Noir daemon (foreground; backgrounding deferred)").option("--detach", "run in the background (not yet implemented; exits 2)").action(async (...args) => {
1378
+ const cmd = trailingCmd(args);
1379
+ const g = cmd.optsWithGlobals();
1380
+ const detach = g.detach === true;
1381
+ await daemonStart({ ...toCliOptions(g), ...detach ? { detach } : {} });
1382
+ });
1383
+ daemonGrp.command("stop").description("stop the Noir daemon").action(async (...args) => {
1384
+ await daemonStop(toCliOptions(trailingCmd(args).optsWithGlobals()));
1385
+ });
1386
+ daemonGrp.command("status").description("report daemon pid/uptime/mode (exit 4 if not running)").action(async (...args) => {
1387
+ await daemonStatus(toCliOptions(trailingCmd(args).optsWithGlobals()));
1388
+ });
1389
+ daemonGrp.command("restart").description("stop then start the daemon").action(async (...args) => {
1390
+ await daemonRestart(toCliOptions(trailingCmd(args).optsWithGlobals()));
1391
+ });
1392
+ daemonGrp.action(() => {
1393
+ throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|stop|status|restart");
1394
+ });
1395
+ program2.command("doctor").description("environment + project health").option(
1396
+ "--dedup",
1397
+ "scan host-context + .noir/ docs for semantic near-duplicates (loads the local embedder)"
1398
+ ).action(async (...args) => {
1399
+ const g = actionGlobals(args);
1400
+ const dedup = trailingCmd(args).opts().dedup === true;
1401
+ await doctor({ ...toCliOptions(g), ...dedup ? { dedup: true } : {} });
1402
+ });
1403
+ program2.command("status").description("project + daemon + workflow + store snapshot").action(async (...args) => {
1404
+ await status(toStatusOptions(actionGlobals(args)));
1405
+ });
1406
+ const contextGrp = program2.command("context").description("context engine (S6)");
1407
+ contextGrp.command("search").description("hybrid search over the indexed context").argument("<query>", "search query").option("--limit <n>", "max results", "10").action(async (...args) => {
1408
+ const cmd = trailingCmd(args);
1409
+ const g = cmd.optsWithGlobals();
1410
+ const query = typeof args[0] === "string" ? args[0] : "";
1411
+ const limit = typeof g.limit === "string" ? g.limit : void 0;
1412
+ await contextSearch({
1413
+ ...toCliOptions(g),
1414
+ query,
1415
+ ...limit === void 0 ? {} : { limit }
1416
+ });
1417
+ });
1418
+ contextGrp.command("index").description("(re)index project files into the context store").option(
1419
+ "--path <p>",
1420
+ "path to index (repeatable)",
1421
+ (val, acc) => [...acc, val],
1422
+ []
1423
+ ).option("--force", "ignore content-hash caching (recognized; not yet honored)").action(async (...args) => {
1424
+ const cmd = trailingCmd(args);
1425
+ const g = cmd.optsWithGlobals();
1426
+ const rawPaths = Array.isArray(g.path) ? g.path.filter((p) => typeof p === "string") : [];
1427
+ const force = g.force === true;
1428
+ await contextIndex({
1429
+ ...toCliOptions(g),
1430
+ ...rawPaths.length === 0 ? {} : { paths: rawPaths },
1431
+ ...force ? { force } : {}
1432
+ });
1433
+ });
1434
+ contextGrp.command("status").description("index freshness + counts").action(async (...args) => {
1435
+ await contextStatus(toCliOptions(trailingCmd(args).optsWithGlobals()));
1436
+ });
1437
+ contextGrp.action(() => {
1438
+ throw new NoirCliError(EXIT.USAGE, "Usage: noir context search|index|status");
1439
+ });
1440
+ const memoryGrp = program2.command("memory").description("memory engine");
1441
+ memoryGrp.command("recall").description("recall memories for a query").argument("<query>", "recall query").option("--limit <n>", "max results", "10").action(async (...args) => {
1442
+ const cmd = trailingCmd(args);
1443
+ const g = cmd.optsWithGlobals();
1444
+ const query = typeof args[0] === "string" ? args[0] : "";
1445
+ const limit = typeof g.limit === "string" ? g.limit : void 0;
1446
+ await memoryRecall({
1447
+ ...toCliOptions(g),
1448
+ query,
1449
+ ...limit === void 0 ? {} : { limit }
1450
+ });
1451
+ });
1452
+ memoryGrp.command("save").description("save an observation to long-term memory").option("--content <text>", "memory content (prompted interactively if omitted)").option(
1453
+ "--type <type>",
1454
+ "observation type (pattern | preference | architecture | bug | workflow | fact | decision)"
1455
+ ).option("--files <files>", "comma-separated related file paths").action(async (...args) => {
1456
+ const cmd = trailingCmd(args);
1457
+ const g = cmd.optsWithGlobals();
1458
+ const content = typeof g.content === "string" ? g.content : void 0;
1459
+ const type = typeof g.type === "string" ? g.type : void 0;
1460
+ const files = typeof g.files === "string" ? g.files : void 0;
1461
+ await memorySave({
1462
+ ...toCliOptions(g),
1463
+ ...content === void 0 ? {} : { content },
1464
+ ...type === void 0 ? {} : { type },
1465
+ ...files === void 0 ? {} : { files }
1466
+ });
1467
+ });
1468
+ memoryGrp.command("sessions").description("list recent memory sessions").action(async (...args) => {
1469
+ await memorySessions(toCliOptions(trailingCmd(args).optsWithGlobals()));
1470
+ });
1471
+ memoryGrp.command("forget").description("delete one or more memories by id").argument("<ids...>", "observation id(s)").action(async (...args) => {
1472
+ const cmd = trailingCmd(args);
1473
+ const g = cmd.optsWithGlobals();
1474
+ const rawIds = Array.isArray(args[0]) ? args[0] : [];
1475
+ const ids = rawIds.filter((x) => typeof x === "string");
1476
+ await memoryForget({ ...toCliOptions(g), ids });
1477
+ });
1478
+ 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) => {
1479
+ const cmd = trailingCmd(args);
1480
+ const g = cmd.optsWithGlobals();
1481
+ const types = typeof g.types === "string" ? g.types : void 0;
1482
+ const limit = typeof g.limit === "string" ? g.limit : void 0;
1483
+ await memoryConsolidate({
1484
+ ...toCliOptions(g),
1485
+ ...types === void 0 ? {} : { types },
1486
+ ...limit === void 0 ? {} : { limit }
1487
+ });
1488
+ });
1489
+ memoryGrp.action(() => {
1490
+ throw new NoirCliError(
1491
+ EXIT.USAGE,
1492
+ "Usage: noir memory recall|save|sessions|forget|consolidate"
1493
+ );
1494
+ });
1495
+ const skillsGrp = program2.command("skills").description("builtin skills (S5)");
1496
+ skillsGrp.command("list").description("list installed Noir skills").action(async (...args) => {
1497
+ await skillsList(toCliOptions(actionGlobals(args)));
1498
+ });
1499
+ skillsGrp.command("sync").description("re-emit skills to the host skills dir").action(async (...args) => {
1500
+ await skillsSync(toCliOptions(actionGlobals(args)));
1501
+ });
1502
+ skillsGrp.action(() => {
1503
+ throw new NoirCliError(EXIT.USAGE, "Usage: noir skills list|sync");
1504
+ });
1505
+ const taskGrp = program2.command("task").description("workflow task control");
1506
+ taskGrp.command("new").description("start a new workflow task").requiredOption("--slug <slug>", "task slug").option("--mode <mode>", "full | quick").action(async (...args) => {
1507
+ const cmd = trailingCmd(args);
1508
+ const g = cmd.optsWithGlobals();
1509
+ const slug = typeof g.slug === "string" ? g.slug : "";
1510
+ const mode = typeof g.mode === "string" ? g.mode : void 0;
1511
+ await taskNew({
1512
+ ...toCliOptions(g),
1513
+ slug,
1514
+ ...mode === void 0 ? {} : { mode }
1515
+ });
1516
+ });
1517
+ taskGrp.command("status").description("active (or named) task status").argument("[id]", "task id (defaults to active)").action(async (...args) => {
1518
+ const cmd = trailingCmd(args);
1519
+ const g = cmd.optsWithGlobals();
1520
+ const first = args[0];
1521
+ const id = typeof first === "string" ? first : void 0;
1522
+ await taskStatus({
1523
+ ...toCliOptions(g),
1524
+ ...id === void 0 ? {} : { id }
1525
+ });
1526
+ });
1527
+ 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").action(async (...args) => {
1528
+ const cmd = trailingCmd(args);
1529
+ const g = cmd.optsWithGlobals();
1530
+ const to = typeof g.to === "string" ? g.to : void 0;
1531
+ const force = typeof g.force === "string" ? g.force : void 0;
1532
+ await taskAdvance({
1533
+ ...toCliOptions(g),
1534
+ ...to === void 0 ? {} : { to },
1535
+ ...force === void 0 ? {} : { force }
1536
+ });
1537
+ });
1538
+ taskGrp.command("next").description("suggest the next phase + applicable skill").action(async (...args) => {
1539
+ await taskNext(toCliOptions(trailingCmd(args).optsWithGlobals()));
1540
+ });
1541
+ taskGrp.action(() => {
1542
+ throw new NoirCliError(EXIT.USAGE, "Usage: noir task new|status|advance|next");
1543
+ });
1544
+ function buildHandoffOptions(g) {
1545
+ return { ...toCliOptions(g), ...g.write === true ? { write: true } : {} };
1546
+ }
1547
+ program2.command("handoff").description("emit a ready-to-paste host handoff prompt").option("--write", "persist to .noir/handoff/<id>.md (gitignored)").action(async (...args) => {
1548
+ await handoff(buildHandoffOptions(trailingCmd(args).optsWithGlobals()));
1549
+ });
1550
+ program2.command("wrap").description("session-end alias for `noir handoff`").option("--write", "persist to .noir/handoff/<id>.md (gitignored)").action(async (...args) => {
1551
+ await handoff(buildHandoffOptions(trailingCmd(args).optsWithGlobals()));
1552
+ });
1553
+ program2.command("tui").description("interactive Ink dashboard (host \xB7 phase \xB7 daemon + /command dispatch)").action(async (...args) => {
1554
+ const g = actionGlobals(args);
1555
+ const opts = toCliOptions(g);
1556
+ requireInteractive(opts, "`noir tui`");
1557
+ const tuiUrl = new URL("./tui/index.js", import.meta.url).href;
1558
+ const { runTui: runTui2 } = await import(
1559
+ /* @vite-ignore */
1560
+ tuiUrl
1561
+ );
1562
+ const dispatch = async (argv) => {
1563
+ const sub = createProgram();
1564
+ try {
1565
+ await sub.parseAsync([...argv], { from: "user" });
1566
+ } catch (err) {
1567
+ handleError(err);
1568
+ }
1569
+ };
1570
+ await runTui2(opts, dispatch);
1571
+ });
1572
+ const homeDeps = {
1573
+ dispatch: async (argv) => {
1574
+ const sub = createProgram();
1575
+ try {
1576
+ await sub.parseAsync([...argv], { from: "user" });
1577
+ } catch (err) {
1578
+ handleError(err);
1579
+ }
1580
+ }
1581
+ };
1582
+ program2.action(async (...args) => {
1583
+ const cmd = trailingCmd(args);
1584
+ const leftovers = cmd.args;
1585
+ if (leftovers.length > 0) {
1586
+ fail(
1587
+ EXIT.NOT_FOUND,
1588
+ `unknown command '${leftovers[0]}' (no such subcommand). Run \`noir --help\` for the list.`,
1589
+ toCliOptions(cmd.optsWithGlobals())
1590
+ );
1591
+ }
1592
+ await home(toCliOptions(actionGlobals(args)), homeDeps);
1593
+ });
1594
+ return program2;
1595
+ }
1596
+ var program = createProgram();
1597
+ async function run(argv = []) {
1598
+ try {
1599
+ await program.parseAsync([...argv], { from: "user" });
1600
+ } catch (err) {
1601
+ handleError(err);
1602
+ }
1603
+ return typeof process.exitCode === "number" ? process.exitCode : EXIT.OK;
1604
+ }
1605
+ var isMainModule = (() => {
1606
+ try {
1607
+ const entry = process.argv[1];
1608
+ if (typeof entry !== "string" || entry.length === 0) return false;
1609
+ return pathToFileURL(realpathSync(entry)).href === import.meta.url;
1610
+ } catch {
1611
+ return false;
1612
+ }
1613
+ })();
1614
+ if (isMainModule) {
1615
+ void run(process.argv.slice(2));
1616
+ }
1617
+
1618
+ // src/daemon-client.ts
1619
+ var DAEMON_DOWN_HINT = "daemon not reachable \u2014 try `noir daemon start`";
1620
+ function describeCause(cause) {
1621
+ if (cause instanceof Error) return `${cause.name}: ${cause.message}`;
1622
+ try {
1623
+ return JSON.stringify(cause);
1624
+ } catch {
1625
+ return String(cause);
1626
+ }
1627
+ }
1628
+ function failDaemonDown(opts, cause) {
1629
+ if (opts.verbose) {
1630
+ process.stderr.write(`noir: daemon transport detail: ${describeCause(cause)}
1631
+ `);
1632
+ }
1633
+ fail(EXIT.DAEMON_DOWN, DAEMON_DOWN_HINT, opts);
1634
+ }
1635
+ async function probeDaemon(opts = {}) {
1636
+ const rec = readDaemonRecord2();
1637
+ if (!rec) {
1638
+ if (opts.verbose) process.stderr.write("noir: daemon probe: no daemon record\n");
1639
+ return { running: false };
1640
+ }
1641
+ if (!pidAlive2(rec.pid)) {
1642
+ if (opts.verbose) process.stderr.write(`noir: daemon probe: pid ${rec.pid} not alive
1643
+ `);
1644
+ return { running: false };
1645
+ }
1646
+ try {
1647
+ const res = await fetch(`http://127.0.0.1:${rec.port}/health`);
1648
+ if (!res.ok) {
1649
+ if (opts.verbose) process.stderr.write(`noir: daemon probe: /health \u2192 HTTP ${res.status}
1650
+ `);
1651
+ return { running: false };
1652
+ }
1653
+ const body = await res.json().catch(() => null);
1654
+ if (body?.ok !== true) {
1655
+ if (opts.verbose) process.stderr.write("noir: daemon probe: /health body not ok\n");
1656
+ return { running: false };
1657
+ }
1658
+ return {
1659
+ running: true,
1660
+ pid: typeof body.pid === "number" ? body.pid : rec.pid,
1661
+ port: rec.port,
1662
+ uptimeSec: typeof body.uptimeSec === "number" ? body.uptimeSec : void 0
1663
+ };
1664
+ } catch (err) {
1665
+ if (opts.verbose)
1666
+ process.stderr.write(`noir: daemon probe: unreachable (${describeCause(err)})
1667
+ `);
1668
+ return { running: false };
1669
+ }
1670
+ }
1671
+ async function resolveDaemon(opts) {
1672
+ const project = opts.project ?? loadProjectInfo6(process.cwd());
1673
+ const idleTimeoutSec = opts.idleTimeoutSec ?? project.config.daemon.idleTimeoutSec;
1674
+ try {
1675
+ const ensured = await ensureDaemonRunning3({ project, idleTimeoutSec });
1676
+ return { url: ensured.url, stop: ensured.stop };
1677
+ } catch (err) {
1678
+ failDaemonDown(opts, err);
1679
+ }
1680
+ }
1681
+ async function connectClient(client, url, opts) {
1682
+ try {
1683
+ await client.connect(new StreamableHTTPClientTransport(new URL(url)));
1684
+ } catch (err) {
1685
+ failDaemonDown(opts, err);
1686
+ }
1687
+ }
1688
+ async function callToolParse(client, opts, name, args) {
1689
+ const result = await client.callTool({ name, arguments: args }).catch((err) => failDaemonDown(opts, err));
1690
+ const block = result.content?.[0];
1691
+ if (block?.type !== "text" || typeof block.text !== "string") {
1692
+ failDaemonDown(opts, new Error(`tool '${name}' returned no text content`));
1693
+ }
1694
+ const text = block.text;
1695
+ try {
1696
+ return JSON.parse(text);
1697
+ } catch (err) {
1698
+ failDaemonDown(opts, err);
1699
+ }
1700
+ }
1701
+ function buildCaller(client, opts) {
1702
+ return {
1703
+ callTool: (name, args = {}) => callToolParse(client, opts, name, args),
1704
+ listTools: async () => {
1705
+ const res = await client.listTools().catch((err) => failDaemonDown(opts, err));
1706
+ const tools = res?.tools;
1707
+ if (!Array.isArray(tools)) return [];
1708
+ const names = [];
1709
+ for (const t of tools) {
1710
+ const name = t?.name;
1711
+ if (typeof name === "string") names.push(name);
1712
+ }
1713
+ return names;
1714
+ }
1715
+ };
1716
+ }
1717
+ async function withDaemon(opts, fn) {
1718
+ const { url, stop } = await resolveDaemon(opts);
1719
+ let client;
1720
+ try {
1721
+ client = new Client(
1722
+ { name: "noir-cli", version: NOIR_VERSION2 },
1723
+ { versionNegotiation: { mode: "auto" } }
1724
+ );
1725
+ const connected = client;
1726
+ await connectClient(connected, url, opts);
1727
+ return await fn(buildCaller(connected, opts));
1728
+ } finally {
1729
+ if (client) {
1730
+ await client.close().catch(() => {
1731
+ });
1732
+ }
1733
+ await stop().catch(() => {
1734
+ });
1735
+ }
1736
+ }
1737
+ async function withRunningDaemon(opts, fn, probe) {
1738
+ const p = probe ?? await probeDaemon(opts);
1739
+ if (!p.running) return null;
1740
+ const url = `http://127.0.0.1:${p.port}/mcp`;
1741
+ let client;
1742
+ try {
1743
+ client = new Client(
1744
+ { name: "noir-cli", version: NOIR_VERSION2 },
1745
+ { versionNegotiation: { mode: "auto" } }
1746
+ );
1747
+ const connected = client;
1748
+ await connected.connect(new StreamableHTTPClientTransport(new URL(url)));
1749
+ return await fn(buildCaller(connected, opts));
1750
+ } catch {
1751
+ return null;
1752
+ } finally {
1753
+ if (client) {
1754
+ await client.close().catch(() => {
1755
+ });
1756
+ }
1757
+ }
1758
+ }
1759
+ async function callDaemonTool(opts, name, args = {}) {
1760
+ return withDaemon(opts, (caller) => caller.callTool(name, args));
1761
+ }
1762
+
1763
+ // src/commands/status.ts
1764
+ async function tryTool(caller, name, args) {
1765
+ try {
1766
+ return await caller.callTool(name, args);
1767
+ } catch {
1768
+ return null;
1769
+ }
1770
+ }
1771
+ function pickStore(s) {
1772
+ if (s?.ok !== true) return null;
1773
+ return {
1774
+ docCount: s.docCount,
1775
+ vecCount: s.vecCount,
1776
+ dbPath: s.dbPath,
1777
+ degraded: s.degraded === true
1778
+ };
1779
+ }
1780
+ function describeEmbedder2(e) {
1781
+ if (!e || e.kind === "none") return "none (BM25-only)";
1782
+ const model = typeof e.model === "string" && e.model.length > 0 ? e.model : "<unset>";
1783
+ return `${e.kind} \xB7 ${model} (${e.dim}-dim)`;
1784
+ }
1785
+ function pickContext(c2) {
1786
+ if (c2?.ok !== true) return null;
1787
+ return {
1788
+ docCount: c2.docCount,
1789
+ vecCount: c2.vecCount,
1790
+ indexedFiles: c2.indexedFiles,
1791
+ embedder: describeEmbedder2(c2.embedder),
1792
+ degraded: c2.degraded === true
1793
+ };
1794
+ }
1795
+ function pickWorkflow(w) {
1796
+ if (w?.ok !== true) return null;
1797
+ return {
1798
+ taskId: w.taskId,
1799
+ phase: w.phase,
1800
+ state: w.state,
1801
+ mode: w.mode,
1802
+ nextGate: w.nextGate,
1803
+ degraded: w.degraded === true
1804
+ };
1805
+ }
1806
+ function pickMemory(m) {
1807
+ if (m?.ok !== true || !Array.isArray(m.sessions)) return null;
1808
+ let observations = 0;
1809
+ for (const s of m.sessions) observations += s.count;
1810
+ return { sessions: m.sessions.length, observations };
1811
+ }
1812
+ function buildPayload(project, probe, host, store, context, workflow, memory) {
1813
+ return {
1814
+ noir: NOIR_VERSION3,
1815
+ project: { id: project.id, name: project.name },
1816
+ host: project.config.host,
1817
+ daemon: {
1818
+ running: probe.running,
1819
+ ...probe.pid !== void 0 ? { pid: probe.pid } : {},
1820
+ ...probe.uptimeSec !== void 0 ? { uptimeSec: probe.uptimeSec } : {},
1821
+ ...host && typeof host.transport === "string" ? { transport: host.transport } : {}
1822
+ },
1823
+ store: pickStore(store),
1824
+ context: pickContext(context),
1825
+ workflow: pickWorkflow(workflow),
1826
+ memory: pickMemory(memory)
1827
+ };
1828
+ }
1829
+ function formatDuration(sec) {
1830
+ if (typeof sec !== "number" || sec < 0) return "unknown";
1831
+ if (sec < 60) return `${sec}s`;
1832
+ if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
1833
+ return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
1834
+ }
1835
+ function describeDaemon(d) {
1836
+ if (!d.running) return "not running (start with `noir daemon start`)";
1837
+ const pid = typeof d.pid === "number" ? `pid ${d.pid}` : "pid unknown";
1838
+ const up = typeof d.uptimeSec === "number" ? `, up ${formatDuration(d.uptimeSec)}` : "";
1839
+ const transport = typeof d.transport === "string" ? `, ${d.transport}` : "";
1840
+ return `running (${pid}${up}${transport})`;
1841
+ }
1842
+ function describeStore(s) {
1843
+ if (!s) return "unavailable (store engine not wired)";
1844
+ const flag = s.degraded ? ` ${badge("warn", "degraded: read-only")}` : "";
1845
+ return `${s.docCount} docs / ${s.vecCount} vecs${flag}`;
1846
+ }
1847
+ function describeContext(c2) {
1848
+ if (!c2) return "unavailable (context engine not wired)";
1849
+ const flag = c2.degraded ? ` ${badge("warn", "degraded")}` : "";
1850
+ return `${c2.docCount} docs, ${c2.vecCount} vecs, ${c2.indexedFiles} files \xB7 embedder ${c2.embedder}${flag}`;
1851
+ }
1852
+ function describeWorkflow(w) {
1853
+ if (!w) return "no active task";
1854
+ const gate = w.nextGate ? ` \u2192 next gate: ${w.nextGate}` : "";
1855
+ const flag = w.degraded ? ` ${badge("warn", "degraded")}` : "";
1856
+ return `${w.phase} (${w.state}, ${w.mode}) \xB7 task ${w.taskId}${gate}${flag}`;
1857
+ }
1858
+ function describeMemory(m) {
1859
+ if (!m) return "unavailable (memory engine not wired)";
1860
+ return `${m.observations} observations across ${m.sessions} session${m.sessions === 1 ? "" : "s"}`;
1861
+ }
1862
+ function renderHuman(p, opts) {
1863
+ log(`noir status \u2014 ${p.project.name} (${p.project.id})`, opts);
1864
+ definitionList(
1865
+ [
1866
+ { label: "Project", value: `${p.project.name} (${p.project.id})` },
1867
+ { label: "Host", value: p.host },
1868
+ { label: "Noir", value: p.noir },
1869
+ { label: "Daemon", value: describeDaemon(p.daemon) },
1870
+ { label: "Store", value: describeStore(p.store) },
1871
+ { label: "Context", value: describeContext(p.context) },
1872
+ { label: "Workflow", value: describeWorkflow(p.workflow) },
1873
+ { label: "Memory", value: describeMemory(p.memory) }
1874
+ ],
1875
+ opts
1876
+ );
1877
+ }
1878
+ async function gatherStatusPayload(opts) {
1879
+ const project = loadProjectInfo7(process.cwd());
1880
+ const probe = await probeDaemon(opts);
1881
+ let host = null;
1882
+ let store = null;
1883
+ let context = null;
1884
+ let workflow = null;
1885
+ let memory = null;
1886
+ if (probe.running) {
1887
+ await withRunningDaemon(
1888
+ opts,
1889
+ async (caller) => {
1890
+ host = await tryTool(caller, "host_status");
1891
+ store = await tryTool(caller, "store_status");
1892
+ context = await tryTool(caller, "context_status");
1893
+ workflow = await tryTool(caller, "workflow_status");
1894
+ memory = await tryTool(caller, "memory_sessions");
1895
+ },
1896
+ probe
1897
+ );
1898
+ }
1899
+ return buildPayload(project, probe, host, store, context, workflow, memory);
1900
+ }
1901
+ async function status(opts) {
1902
+ const payload = await gatherStatusPayload(opts);
1903
+ if (opts.json === true) {
1904
+ process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
1905
+ `);
1906
+ return;
1907
+ }
1908
+ renderHuman(payload, opts);
1909
+ }
1910
+
1911
+ // src/tui/App.tsx
1912
+ import { Box, Text as Text6, useApp, useInput } from "ink";
1913
+ import { useEffect, useState } from "react";
1914
+
1915
+ // src/tui/CommandInput.tsx
1916
+ import { Text } from "ink";
1917
+ import { jsx, jsxs } from "react/jsx-runtime";
1918
+ function CommandInput({ buffer, running }) {
1919
+ if (running) {
1920
+ return /* @__PURE__ */ jsx(Text, { children: c.dim("\u2026") });
1921
+ }
1922
+ if (buffer.length === 0) {
1923
+ return /* @__PURE__ */ jsx(Text, { children: c.dim("type a /command (e.g. /status, /sync, /task next), or q to quit") });
1924
+ }
1925
+ const isCommand = buffer.startsWith("/");
1926
+ return /* @__PURE__ */ jsxs(Text, { children: [
1927
+ /* @__PURE__ */ jsx(Text, { children: isCommand ? c.accent("\u25B8") : c.dim("\u25B8") }),
1928
+ " ",
1929
+ buffer,
1930
+ /* @__PURE__ */ jsx(Text, { children: c.dim("\u258C") })
1931
+ ] });
1932
+ }
1933
+
1934
+ // src/tui/capture.ts
1935
+ async function captureProcessOutput(fn) {
1936
+ const outChunks = [];
1937
+ const errChunks = [];
1938
+ const origOut = process.stdout.write.bind(process.stdout);
1939
+ const origErr = process.stderr.write.bind(process.stderr);
1940
+ const prevExitCode = process.exitCode;
1941
+ process.stdout.write = ((chunk) => {
1942
+ outChunks.push(typeof chunk === "string" ? chunk : String(chunk));
1943
+ return true;
1944
+ });
1945
+ process.stderr.write = ((chunk) => {
1946
+ errChunks.push(typeof chunk === "string" ? chunk : String(chunk));
1947
+ return true;
1948
+ });
1949
+ try {
1950
+ await fn();
1951
+ } finally {
1952
+ process.stdout.write = origOut;
1953
+ process.stderr.write = origErr;
1954
+ }
1955
+ const exitCode = typeof process.exitCode === "number" ? process.exitCode : 0;
1956
+ process.exitCode = prevExitCode;
1957
+ return { stdout: outChunks.join(""), stderr: errChunks.join(""), exitCode };
1958
+ }
1959
+
1960
+ // src/tui/Footer.tsx
1961
+ import { Text as Text2 } from "ink";
1962
+ import { jsx as jsx2 } from "react/jsx-runtime";
1963
+ function Footer({ running }) {
1964
+ if (running) {
1965
+ return /* @__PURE__ */ jsx2(Text2, { children: c.dim("running\u2026 (Ctrl+C to force exit)") });
1966
+ }
1967
+ return /* @__PURE__ */ jsx2(Text2, { children: c.dim("? help \xB7 q/Esc quit \xB7 \u2191/\u2193 scroll \xB7 Enter run \xB7 Ctrl+C exit") });
1968
+ }
1969
+
1970
+ // src/tui/format.ts
1971
+ function formatStatusPayload(p) {
1972
+ if (p === null) return null;
1973
+ return [
1974
+ `Project: ${p.project.name} (${p.project.id})`,
1975
+ `Host: ${p.host}`,
1976
+ `Noir: ${p.noir}`,
1977
+ `Daemon: ${describeDaemon(p.daemon)}`,
1978
+ `Store: ${describeStore(p.store)}`,
1979
+ `Context: ${describeContext(p.context)}`,
1980
+ `Workflow: ${describeWorkflow(p.workflow)}`,
1981
+ `Memory: ${describeMemory(p.memory)}`
1982
+ ];
1983
+ }
1984
+
1985
+ // src/tui/Header.tsx
1986
+ import { Text as Text3 } from "ink";
1987
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1988
+ function Header({ tagline }) {
1989
+ return /* @__PURE__ */ jsxs2(Text3, { children: [
1990
+ c.accent("\u25C6"),
1991
+ " ",
1992
+ c.bold("noir"),
1993
+ " ",
1994
+ /* @__PURE__ */ jsx3(Text3, { children: c.dim(tagline ?? "dashboard") })
1995
+ ] });
1996
+ }
1997
+
1998
+ // src/tui/OutputPane.tsx
1999
+ import { Text as Text4 } from "ink";
2000
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
2001
+ function toRows(visible) {
2002
+ const rows = [];
2003
+ const seen = /* @__PURE__ */ new Map();
2004
+ for (const text of visible) {
2005
+ const n = (seen.get(text) ?? 0) + 1;
2006
+ seen.set(text, n);
2007
+ rows.push({ key: n === 1 ? text || "_blank" : `${text || "_blank"}#${n}`, text });
2008
+ }
2009
+ return rows;
2010
+ }
2011
+ function OutputPane({
2012
+ lines,
2013
+ scrollOffset,
2014
+ height = 12,
2015
+ title
2016
+ }) {
2017
+ const width = terminalWidth();
2018
+ if (lines.length === 0) {
2019
+ return /* @__PURE__ */ jsx4(Text4, { children: c.dim("(no output \u2014 type a /command, or ? for help)") });
2020
+ }
2021
+ const maxOffset = Math.max(0, lines.length - height);
2022
+ const offset = Math.min(Math.max(0, scrollOffset), maxOffset);
2023
+ const rows = toRows(lines.slice(offset, offset + height));
2024
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
2025
+ title !== void 0 ? /* @__PURE__ */ jsx4(Text4, { children: c.dim(`\u2500\u2500 ${title} \u2500\u2500`) }) : null,
2026
+ rows.map((row) => /* @__PURE__ */ jsx4(Text4, { children: row.text.length > width ? `${row.text.slice(0, Math.max(1, width - 1))}\u2026` : row.text }, row.key))
2027
+ ] });
2028
+ }
2029
+
2030
+ // src/tui/StatusBar.tsx
2031
+ import { Text as Text5 } from "ink";
2032
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2033
+ function Cell({
2034
+ label,
2035
+ value,
2036
+ accent
2037
+ }) {
2038
+ return /* @__PURE__ */ jsxs4(Text5, { children: [
2039
+ /* @__PURE__ */ jsx5(Text5, { children: c.dim(`${label}:`) }),
2040
+ " ",
2041
+ /* @__PURE__ */ jsx5(Text5, { children: accent ? c.accent(value) : value })
2042
+ ] });
2043
+ }
2044
+ function StatusBar({ payload, loading }) {
2045
+ const host = payload?.host ?? "\u2014";
2046
+ const mode = payload?.workflow?.mode ?? "\u2014";
2047
+ const phase = payload?.workflow?.phase ?? (loading ? "\u2026" : "idle");
2048
+ const daemonRunning = payload?.daemon.running === true;
2049
+ const daemon = payload === null && loading ? "\u2026" : daemonRunning ? "up" : "down";
2050
+ return /* @__PURE__ */ jsxs4(Text5, { children: [
2051
+ /* @__PURE__ */ jsx5(Cell, { label: "host", value: host, accent: true }),
2052
+ /* @__PURE__ */ jsx5(Text5, { children: c.dim(" \xB7 ") }),
2053
+ /* @__PURE__ */ jsx5(Cell, { label: "mode", value: mode }),
2054
+ /* @__PURE__ */ jsx5(Text5, { children: c.dim(" \xB7 ") }),
2055
+ /* @__PURE__ */ jsx5(Cell, { label: "phase", value: phase }),
2056
+ /* @__PURE__ */ jsx5(Text5, { children: c.dim(" \xB7 ") }),
2057
+ /* @__PURE__ */ jsxs4(Text5, { children: [
2058
+ c.dim("daemon:"),
2059
+ " ",
2060
+ daemonRunning ? c.ok(daemon) : c.warn(daemon)
2061
+ ] })
2062
+ ] });
2063
+ }
2064
+
2065
+ // src/tui/App.tsx
2066
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
2067
+ function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
2068
+ const { exit } = useApp();
2069
+ const [buffer, setBuffer] = useState("");
2070
+ const [payload, setPayload] = useState(initialPayload);
2071
+ const [loading, setLoading] = useState(initialPayload === null);
2072
+ const [output, setOutput] = useState(null);
2073
+ const [scrollOffset, setScrollOffset] = useState(0);
2074
+ const [help, setHelp] = useState(false);
2075
+ const [running, setRunning] = useState(false);
2076
+ const [notice, setNotice] = useState(null);
2077
+ const [pending, setPending] = useState(null);
2078
+ useEffect(() => {
2079
+ if (running) return;
2080
+ let cancelled = false;
2081
+ setLoading(true);
2082
+ void deps.fetchStatus().then((p) => {
2083
+ if (!cancelled) {
2084
+ setPayload(p);
2085
+ setLoading(false);
2086
+ }
2087
+ }).catch(() => {
2088
+ if (!cancelled) {
2089
+ setPayload(null);
2090
+ setLoading(false);
2091
+ }
2092
+ });
2093
+ const id = setInterval(() => {
2094
+ void deps.fetchStatus().then((p) => {
2095
+ if (!cancelled) setPayload(p);
2096
+ }).catch(() => {
2097
+ });
2098
+ }, refreshMs);
2099
+ return () => {
2100
+ cancelled = true;
2101
+ clearInterval(id);
2102
+ };
2103
+ }, [running, refreshMs, deps]);
2104
+ useEffect(() => {
2105
+ if (pending === null) return;
2106
+ const argv = pending;
2107
+ let active = true;
2108
+ const run2 = async () => {
2109
+ const captured = await captureProcessOutput(() => deps.dispatch(argv));
2110
+ if (!active) return;
2111
+ const combined = `${captured.stdout}${captured.stderr}`;
2112
+ const lines = combined.split("\n");
2113
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
2114
+ const title = `/${argv.join(" ")} \u2192 exit ${captured.exitCode}`;
2115
+ setOutput({ title, lines });
2116
+ setNotice(`ran /${argv.join(" ")} \u2192 exit ${captured.exitCode}`);
2117
+ setRunning(false);
2118
+ setPending(null);
2119
+ setScrollOffset(0);
2120
+ };
2121
+ void run2();
2122
+ return () => {
2123
+ active = false;
2124
+ };
2125
+ }, [pending, deps]);
2126
+ useInput((input, key) => {
2127
+ if (running) return;
2128
+ setNotice(null);
2129
+ if (help) {
2130
+ if (input === "?" || key.escape || input === "q") setHelp(false);
2131
+ return;
2132
+ }
2133
+ if (key.return) {
2134
+ submit();
2135
+ return;
2136
+ }
2137
+ if (key.escape) {
2138
+ if (buffer.length > 0) setBuffer("");
2139
+ else if (output !== null) setOutput(null);
2140
+ else exit();
2141
+ return;
2142
+ }
2143
+ if (key.backspace || key.delete) {
2144
+ setBuffer((b) => b.slice(0, -1));
2145
+ return;
2146
+ }
2147
+ if (key.upArrow) {
2148
+ setScrollOffset((o) => Math.max(0, o - 1));
2149
+ return;
2150
+ }
2151
+ if (key.downArrow) {
2152
+ setScrollOffset((o) => o + 1);
2153
+ return;
2154
+ }
2155
+ if (key.ctrl) return;
2156
+ if (input.length === 1) {
2157
+ if (buffer.length === 0) {
2158
+ if (input === "q") {
2159
+ exit();
2160
+ return;
2161
+ }
2162
+ if (input === "?") {
2163
+ setHelp(true);
2164
+ return;
2165
+ }
2166
+ }
2167
+ setBuffer((b) => b + input);
2168
+ return;
2169
+ }
2170
+ if (input.length > 1) {
2171
+ setBuffer((b) => b + input);
2172
+ }
2173
+ });
2174
+ function submit() {
2175
+ const text = buffer;
2176
+ if (text.length === 0) return;
2177
+ setBuffer("");
2178
+ if (!text.startsWith("/")) {
2179
+ setNotice("type a /command to run it (bare text is just a hint)");
2180
+ return;
2181
+ }
2182
+ const argv = text.slice(1).split(/\s+/).map((s) => s.trim()).filter((s) => s.length > 0);
2183
+ if (argv.length === 0) {
2184
+ setNotice("empty /command");
2185
+ return;
2186
+ }
2187
+ setOutput(null);
2188
+ setRunning(true);
2189
+ setPending(argv);
2190
+ }
2191
+ if (help) {
2192
+ return /* @__PURE__ */ jsx6(Help, {});
2193
+ }
2194
+ const snapshotLines = formatStatusPayload(payload);
2195
+ const paneLines = output?.lines ?? snapshotLines ?? [];
2196
+ const paneTitle = output?.title ?? "snapshot";
2197
+ return /* @__PURE__ */ jsxs5(Box, { flexDirection: "column", children: [
2198
+ /* @__PURE__ */ jsx6(Header, { tagline: "dashboard" }),
2199
+ /* @__PURE__ */ jsx6(StatusBar, { payload, loading }),
2200
+ /* @__PURE__ */ jsx6(OutputPane, { lines: paneLines, scrollOffset, title: paneTitle }),
2201
+ /* @__PURE__ */ jsx6(CommandInput, { buffer, running }),
2202
+ notice !== null ? /* @__PURE__ */ jsx6(Text6, { children: c.dim(notice) }) : null,
2203
+ /* @__PURE__ */ jsx6(Footer, { running })
2204
+ ] });
2205
+ }
2206
+ function Help() {
2207
+ return /* @__PURE__ */ jsxs5(Box, { flexDirection: "column", children: [
2208
+ /* @__PURE__ */ jsx6(Header, { tagline: "help" }),
2209
+ /* @__PURE__ */ jsx6(Text6, { children: " " }),
2210
+ /* @__PURE__ */ jsx6(Text6, { children: c.bold("Keybindings") }),
2211
+ /* @__PURE__ */ jsx6(Text6, { children: c.dim(" /&lt;command&gt; run a Noir sub-command (e.g. /status, /sync, /task next)") }),
2212
+ /* @__PURE__ */ jsx6(Text6, { children: c.dim(" Enter run the typed /command") }),
2213
+ /* @__PURE__ */ jsx6(Text6, { children: c.dim(" Esc back: clear input \u2192 dismiss output \u2192 quit") }),
2214
+ /* @__PURE__ */ jsx6(Text6, { children: c.dim(" q quit (when the input is empty)") }),
2215
+ /* @__PURE__ */ jsx6(Text6, { children: c.dim(" \u2191 / \u2193 scroll the output pane") }),
2216
+ /* @__PURE__ */ jsx6(Text6, { children: c.dim(" ? toggle this help") }),
2217
+ /* @__PURE__ */ jsx6(Text6, { children: c.dim(" Ctrl+C force exit") }),
2218
+ /* @__PURE__ */ jsx6(Text6, { children: " " }),
2219
+ /* @__PURE__ */ jsx6(Text6, { children: c.bold("Commands") }),
2220
+ /* @__PURE__ */ jsx6(Text6, { children: c.dim(" Dispatched through the same routing as `noir` at the prompt.") }),
2221
+ /* @__PURE__ */ jsx6(Text6, { children: c.dim(
2222
+ " /status /sync /doctor /context search &lt;q&gt; /task next /memory recall &lt;q&gt;"
2223
+ ) }),
2224
+ /* @__PURE__ */ jsx6(Text6, { children: c.dim(" Commands that need their own interactive prompts (e.g. a /sync with a") }),
2225
+ /* @__PURE__ */ jsx6(Text6, { children: c.dim(" conflict) are best run directly \u2014 exit first with q.") }),
2226
+ /* @__PURE__ */ jsx6(Text6, { children: " " }),
2227
+ /* @__PURE__ */ jsx6(Text6, { children: c.dim("press ? / Esc / q to close this help") })
2228
+ ] });
2229
+ }
2230
+
2231
+ // src/tui/index.tsx
2232
+ import { jsx as jsx7 } from "react/jsx-runtime";
2233
+ function defaultFetchStatus(opts) {
2234
+ return async () => {
2235
+ try {
2236
+ return await gatherStatusPayload(opts);
2237
+ } catch {
2238
+ return null;
2239
+ }
2240
+ };
2241
+ }
2242
+ async function runTui(opts, dispatch) {
2243
+ const deps = {
2244
+ dispatch,
2245
+ fetchStatus: defaultFetchStatus(opts)
2246
+ };
2247
+ const instance = render(/* @__PURE__ */ jsx7(App, { deps }));
2248
+ await instance.waitUntilExit();
2249
+ }
2250
+ export {
2251
+ runTui
2252
+ };
2253
+ //# sourceMappingURL=index.js.map