@noir-ai/cli 1.7.4-beta.1 → 1.8.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.d.ts CHANGED
@@ -2,6 +2,41 @@ import { Command } from 'commander';
2
2
  import { C as CliOptions } from './output-BNE9htEm.js';
3
3
  export { E as EXIT, N as NoirCliError, f as fail, i as inferExitCode } from './output-BNE9htEm.js';
4
4
 
5
+ /**
6
+ * A palette entry projecting a leaf `noir` subcommand for the TUI palette.
7
+ */
8
+ interface PaletteCommand {
9
+ /** Space-joined argv — the stable dispatch key (e.g. `context search`). */
10
+ readonly id: string;
11
+ /** Title-cased argv joined by `: ` for the palette's human label. */
12
+ readonly label: string;
13
+ /**
14
+ * The raw token path, root name excluded. This is what the palette re-dispatches
15
+ * (it is NOT a live reference to commander's parsed argv).
16
+ */
17
+ readonly argv: readonly string[];
18
+ /** One-line summary; falls back to `argv.join(' ')` when the command omits one. */
19
+ readonly description: string;
20
+ /** Top-level group the command belongs to (`argv[0]`), for palette grouping. */
21
+ readonly category: string;
22
+ /** Search keywords — currently the argv tokens, used by the palette's matcher. */
23
+ readonly keywords: readonly string[];
24
+ /**
25
+ * True for commands that mutate the store / workflow / host artifacts (context
26
+ * index/forget, memory save/forget/consolidate, task new/advance, skills sync,
27
+ * daemon start/stop/restart, init, create, sync, install, update). The palette
28
+ * uses this to require confirmation before dispatch.
29
+ */
30
+ readonly destructive: boolean;
31
+ }
32
+
33
+ /**
34
+ * The TUI palette source (B3). Walks a FRESH {@link createProgram} at `noir tui`
35
+ * launch and projects every leaf subcommand into a {@link PaletteCommand}.
36
+ * Defined here (not in the tui) so the palette derives from the REAL command
37
+ * tree without the tui graph importing the bin (which would be circular).
38
+ */
39
+ declare function buildPaletteCommandsForTui(): PaletteCommand[];
5
40
  /** One entry in the {@link DEPRECATIONS} registry. */
6
41
  interface DeprecationEntry {
7
42
  /** Old command form as a path prefix, e.g. `['legacy-status']` or
@@ -15,9 +50,9 @@ interface DeprecationEntry {
15
50
  }
16
51
  /**
17
52
  * Deprecation registry. **Empty today** — no `noir` command is deprecated.
18
- * When adding an entry, also update `docs/deprecation.md` (and, if the old
19
- * form still routes, add a redirect). {@link emitDeprecationHintsFor} scans
20
- * this on every dispatch and emits one `tip()` per match; `--no-tips` /
53
+ * When adding an entry, update the CHANGELOG + `docs/reference/cli.md` (and,
54
+ * if the old form still routes, add a redirect). {@link emitDeprecationHintsFor}
55
+ * scans this on every dispatch and emits one `tip()` per match; `--no-tips` /
21
56
  * `--json` suppress the hint (see {@link tip}).
22
57
  */
23
58
  declare const DEPRECATIONS: DeprecationEntry[];
@@ -39,4 +74,4 @@ declare const program: Command;
39
74
  */
40
75
  declare function run(argv?: readonly string[]): Promise<number>;
41
76
 
42
- export { DEPRECATIONS, type DeprecationEntry, createProgram, emitDeprecationHintsFor, program, run };
77
+ export { DEPRECATIONS, type DeprecationEntry, buildPaletteCommandsForTui, createProgram, emitDeprecationHintsFor, program, run };
package/dist/bin.js CHANGED
@@ -8,11 +8,9 @@ import {
8
8
  } from "./chunk-JUVY6L3F.js";
9
9
  import "./chunk-L7XR7EA3.js";
10
10
  import {
11
+ doctor,
11
12
  init
12
- } from "./chunk-KQYVVGOH.js";
13
- import {
14
- doctor
15
- } from "./chunk-B7OKXQKF.js";
13
+ } from "./chunk-BM3VEJ2J.js";
16
14
  import {
17
15
  EXIT,
18
16
  NoirCliError,
@@ -44,9 +42,14 @@ import { Command, Option } from "commander";
44
42
 
45
43
  // src/daemon-client.ts
46
44
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
45
+ import { ContextEngine, createEmbedFn, resolveEmbedderConfig } from "@noir-ai/context";
47
46
  import { loadProjectInfo, NOIR_VERSION } from "@noir-ai/core";
48
47
  import { ensureDaemonRunning, pidAlive, readDaemonRecord } from "@noir-ai/daemon";
48
+ import { createMemoryEngine } from "@noir-ai/memory";
49
+ import { openStore } from "@noir-ai/store";
50
+ import { WorkflowEngine } from "@noir-ai/workflow";
49
51
  var DAEMON_DOWN_HINT = "daemon not reachable \u2014 try `noir daemon start`";
52
+ var PROBE_TIMEOUT_MS = 1500;
50
53
  function describeCause(cause) {
51
54
  if (cause instanceof Error) return `${cause.name}: ${cause.message}`;
52
55
  try {
@@ -74,7 +77,9 @@ async function probeDaemon(opts = {}) {
74
77
  return { running: false };
75
78
  }
76
79
  try {
77
- const res = await fetch(`http://127.0.0.1:${rec.port}/health`);
80
+ const res = await fetch(`http://127.0.0.1:${rec.port}/health`, {
81
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
82
+ });
78
83
  if (!res.ok) {
79
84
  if (opts.verbose) process.stderr.write(`noir: daemon probe: /health \u2192 HTTP ${res.status}
80
85
  `);
@@ -189,6 +194,33 @@ async function withRunningDaemon(opts, fn, probe) {
189
194
  async function callDaemonTool(opts, name, args = {}) {
190
195
  return withDaemon(opts, (caller) => caller.callTool(name, args));
191
196
  }
197
+ async function withInProcessRead(opts, fn) {
198
+ const project = opts.project ?? loadProjectInfo(process.cwd());
199
+ const store = await openStore({ projectId: project.id, root: project.root, readonly: true });
200
+ try {
201
+ const embedderCfg = resolveEmbedderConfig(project.config.context);
202
+ const embed = createEmbedFn(embedderCfg).embed;
203
+ const context = new ContextEngine({
204
+ store,
205
+ root: project.root,
206
+ projectId: project.id,
207
+ embedderCfg,
208
+ storeDegraded: true
209
+ });
210
+ const memory = createMemoryEngine({
211
+ store,
212
+ root: project.root,
213
+ projectId: project.id,
214
+ embed,
215
+ storeDegraded: true
216
+ });
217
+ const workflow = new WorkflowEngine(store, project.root, project.id);
218
+ return await fn({ context, memory, workflow });
219
+ } finally {
220
+ await store.close().catch(() => {
221
+ });
222
+ }
223
+ }
192
224
 
193
225
  // src/commands/context.ts
194
226
  function parseLimit(raw, label, opts) {
@@ -214,6 +246,39 @@ function toHit(raw) {
214
246
  }
215
247
  async function contextSearch(opts) {
216
248
  const limit = parseLimit(opts.limit, "context search", opts);
249
+ let probe;
250
+ try {
251
+ probe = await probeDaemon(opts);
252
+ } catch {
253
+ probe = { running: true };
254
+ }
255
+ if (!probe.running) {
256
+ await withInProcessRead(opts, async (engines) => {
257
+ const result = await engines.context.search(opts.query, {
258
+ ...limit === void 0 ? {} : { limit }
259
+ });
260
+ const data2 = {
261
+ query: opts.query,
262
+ hits: result.results.map((h) => ({
263
+ path: h.path,
264
+ score: h.score,
265
+ snippet: h.snippet,
266
+ source: h.source
267
+ })),
268
+ consumedTokens: result.consumedTokens,
269
+ truncated: result.truncated,
270
+ degraded: result.degraded,
271
+ mode: result.mode
272
+ };
273
+ if (opts.json === true) {
274
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
275
+ `);
276
+ return;
277
+ }
278
+ renderSearch(data2, opts);
279
+ });
280
+ return;
281
+ }
217
282
  const res = await callDaemonTool(opts, "context_search", {
218
283
  query: opts.query,
219
284
  ...limit === void 0 ? {} : { limit }
@@ -253,13 +318,8 @@ function renderSearch(data, opts) {
253
318
  );
254
319
  }
255
320
  async function contextIndex(opts) {
256
- if (opts.force === true) {
257
- info(
258
- "context index: --force is recognized but not yet honored (content-hash is always incremental).",
259
- opts
260
- );
261
- }
262
321
  const args = opts.paths && opts.paths.length > 0 ? { paths: opts.paths } : {};
322
+ if (opts.force === true) args.force = true;
263
323
  const res = await callDaemonTool(opts, "context_index", args);
264
324
  if (res.ok !== true) failTool("context index", res, opts);
265
325
  const data = {
@@ -333,7 +393,8 @@ import {
333
393
  clearDaemonRecord,
334
394
  ensureDaemonRunning as ensureDaemonRunning2,
335
395
  pidAlive as pidAlive2,
336
- readDaemonRecord as readDaemonRecord2
396
+ readDaemonRecord as readDaemonRecord2,
397
+ spawnDetachedDaemon
337
398
  } from "@noir-ai/daemon";
338
399
  var MODE = "foreground";
339
400
  function formatUptime(sec) {
@@ -342,12 +403,69 @@ function formatUptime(sec) {
342
403
  if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
343
404
  return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
344
405
  }
406
+ async function isHealthy(port) {
407
+ try {
408
+ const res = await fetch(`http://127.0.0.1:${port}/health`);
409
+ return res.status === 200;
410
+ } catch {
411
+ return false;
412
+ }
413
+ }
345
414
  async function daemonStart(opts) {
415
+ const project = loadProjectInfo2(process.cwd());
416
+ if (opts.detachChild === true) {
417
+ const child = await ensureDaemonRunning2({
418
+ project,
419
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec
420
+ });
421
+ if (child.started) {
422
+ if (opts.json === true) {
423
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { mode: "detached" } })}
424
+ `);
425
+ return;
426
+ }
427
+ info("noir daemon: detached foreground. Ctrl+C to stop.", opts);
428
+ log(`Noir daemon listening at ${child.url}`, opts);
429
+ return;
430
+ }
431
+ if (opts.json === true) {
432
+ process.stdout.write(
433
+ `${JSON.stringify({ ok: true, data: { mode: "detached", reused: true } })}
434
+ `
435
+ );
436
+ return;
437
+ }
438
+ info("Noir daemon already running; detached child exiting.", opts);
439
+ return;
440
+ }
346
441
  if (opts.detach === true) {
347
- fail(EXIT.USAGE, "not implemented (tracked: v1.x)", opts);
442
+ const existing = readDaemonRecord2();
443
+ if (existing && pidAlive2(existing.pid) && await isHealthy(existing.port)) {
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(
452
+ `Noir daemon already running at http://127.0.0.1:${existing.port}/mcp (detach not needed).`,
453
+ opts
454
+ );
455
+ return;
456
+ }
457
+ const spawned = await spawnDetachedDaemon({ project });
458
+ if (opts.json === true) {
459
+ process.stdout.write(
460
+ `${JSON.stringify({ ok: true, data: { mode: "detached", pid: spawned.pid, port: spawned.port } })}
461
+ `
462
+ );
463
+ return;
464
+ }
465
+ info(`Noir daemon started in the background (pid ${spawned.pid}, port ${spawned.port}).`, opts);
466
+ return;
348
467
  }
349
468
  const ds = spinner("Starting Noir daemon...", opts).start();
350
- const project = loadProjectInfo2(process.cwd());
351
469
  const ensured = await ensureDaemonRunning2({
352
470
  project,
353
471
  idleTimeoutSec: project.config.daemon.idleTimeoutSec
@@ -370,7 +488,7 @@ async function daemonStart(opts) {
370
488
  );
371
489
  return;
372
490
  }
373
- info("noir daemon: foreground mode (backgrounding deferred to v1.x). Ctrl+C to stop.", opts);
491
+ info("noir daemon: foreground mode. Ctrl+C to stop.", opts);
374
492
  log(`Noir daemon listening at ${ensured.url}`, opts);
375
493
  return;
376
494
  }
@@ -466,7 +584,9 @@ async function daemonStatus(opts) {
466
584
  port: rec.port,
467
585
  startedAt: rec.startedAt,
468
586
  uptimeSec,
469
- mode: MODE
587
+ // Honest ownership: the detached child writes `mode:'detached'`; anything
588
+ // else (legacy record, foreground start) reports 'foreground'.
589
+ mode: rec.mode ?? MODE
470
590
  };
471
591
  if (opts.json === true) {
472
592
  process.stdout.write(`${JSON.stringify({ ok: true, data })}
@@ -655,6 +775,7 @@ async function status(opts) {
655
775
  }
656
776
 
657
777
  // src/commands/task.ts
778
+ import { PHASES } from "@noir-ai/workflow";
658
779
  var PHASE_SKILL = {
659
780
  intake: "noir-intake",
660
781
  clarify: "noir-clarify",
@@ -698,7 +819,49 @@ function renderStatusRow(s, opts) {
698
819
  opts
699
820
  );
700
821
  }
822
+ var GATE_PHASES = ["spec", "plan", "verify"];
823
+ function nextGateAfter(phase) {
824
+ const cur = PHASES.indexOf(phase);
825
+ for (const p of GATE_PHASES) {
826
+ if (PHASES.indexOf(p) > cur) return p;
827
+ }
828
+ return null;
829
+ }
701
830
  async function taskStatus(opts) {
831
+ let probe;
832
+ try {
833
+ probe = await probeDaemon(opts);
834
+ } catch {
835
+ probe = { running: true };
836
+ }
837
+ if (!probe.running) {
838
+ const s2 = await withInProcessRead(opts, async (engines) => {
839
+ const id = typeof opts.id === "string" && opts.id.length > 0 ? opts.id : null;
840
+ const taskId = id ?? engines.workflow.activeTaskId();
841
+ if (!taskId) fail(EXIT.NOT_FOUND, "task: no active task", opts);
842
+ const task = engines.workflow.status(taskId);
843
+ if (!task) fail(EXIT.NOT_FOUND, `task: unknown task '${taskId}'`, opts);
844
+ const stopped = task.state === "blocked" || task.state === "abandoned";
845
+ return {
846
+ ok: true,
847
+ taskId: task.taskId,
848
+ phase: task.phase,
849
+ state: task.state,
850
+ mode: task.mode,
851
+ nextGate: stopped ? null : nextGateAfter(task.phase),
852
+ history: task.history,
853
+ updatedAt: task.updatedAt,
854
+ degraded: true
855
+ };
856
+ });
857
+ if (opts.json === true) {
858
+ process.stdout.write(`${JSON.stringify({ ok: true, data: s2 })}
859
+ `);
860
+ return;
861
+ }
862
+ renderStatusRow(s2, opts);
863
+ return;
864
+ }
702
865
  const s = await fetchStatus(opts, opts.id);
703
866
  if (opts.json === true) {
704
867
  process.stdout.write(`${JSON.stringify({ ok: true, data: s })}
@@ -1179,6 +1342,27 @@ function stamp(ts) {
1179
1342
  }
1180
1343
  async function memoryRecall(opts) {
1181
1344
  const limit = parseLimit2(opts.limit, "memory recall", opts);
1345
+ let probe;
1346
+ try {
1347
+ probe = await probeDaemon(opts);
1348
+ } catch {
1349
+ probe = { running: true };
1350
+ }
1351
+ if (!probe.running) {
1352
+ await withInProcessRead(opts, async (engines) => {
1353
+ const result = await engines.memory.recall(opts.query, {
1354
+ ...limit === void 0 ? {} : { limit }
1355
+ });
1356
+ const data2 = { query: opts.query, hits: result, degraded: false };
1357
+ if (opts.json === true) {
1358
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
1359
+ `);
1360
+ return;
1361
+ }
1362
+ renderRecall(data2.query, result, data2.degraded, opts);
1363
+ });
1364
+ return;
1365
+ }
1182
1366
  const res = await callDaemonTool(opts, "memory_recall", {
1183
1367
  query: opts.query,
1184
1368
  ...limit === void 0 ? {} : { limit }
@@ -1264,6 +1448,30 @@ function renderObservation(obs, opts) {
1264
1448
  ${content}`, opts);
1265
1449
  }
1266
1450
  async function memorySessions(opts) {
1451
+ let probe;
1452
+ try {
1453
+ probe = await probeDaemon(opts);
1454
+ } catch {
1455
+ probe = { running: true };
1456
+ }
1457
+ if (!probe.running) {
1458
+ await withInProcessRead(opts, async (engines) => {
1459
+ const raw = engines.memory.sessions();
1460
+ const sessions2 = raw.map((s) => ({
1461
+ id: s.id,
1462
+ count: s.count,
1463
+ lastTs: s.lastTs
1464
+ }));
1465
+ const data2 = { sessions: sessions2 };
1466
+ if (opts.json === true) {
1467
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
1468
+ `);
1469
+ return;
1470
+ }
1471
+ renderSessions(sessions2, opts);
1472
+ });
1473
+ return;
1474
+ }
1267
1475
  const res = await callDaemonTool(opts, "memory_sessions");
1268
1476
  if (res.ok !== true) failTool2("memory sessions", res, opts);
1269
1477
  const sessions = Array.isArray(res.sessions) ? res.sessions.map((raw) => {
@@ -1280,6 +1488,9 @@ async function memorySessions(opts) {
1280
1488
  `);
1281
1489
  return;
1282
1490
  }
1491
+ renderSessions(sessions, opts);
1492
+ }
1493
+ function renderSessions(sessions, opts) {
1283
1494
  log(`memory sessions \u2014 ${sessions.length} session${sessions.length === 1 ? "" : "s"}`, opts);
1284
1495
  table(
1285
1496
  sessions.map((s) => ({ Session: s.id, Observations: s.count, "Last seen": stamp(s.lastTs) })),
@@ -1491,7 +1702,70 @@ async function skillsSync(opts) {
1491
1702
  }
1492
1703
  }
1493
1704
 
1705
+ // src/tui/commands/registry.ts
1706
+ var DESTRUCTIVE_PREFIXES = [
1707
+ ["context", "index"],
1708
+ ["context", "forget"],
1709
+ ["memory", "save"],
1710
+ ["memory", "forget"],
1711
+ ["memory", "consolidate"],
1712
+ ["task", "new"],
1713
+ ["task", "advance"],
1714
+ ["skills", "sync"],
1715
+ ["daemon", "start"],
1716
+ ["daemon", "stop"],
1717
+ ["daemon", "restart"],
1718
+ ["init"],
1719
+ ["create"],
1720
+ ["sync"],
1721
+ ["install"],
1722
+ ["update"]
1723
+ ];
1724
+ function isDestructive(argv) {
1725
+ return DESTRUCTIVE_PREFIXES.some((prefix) => {
1726
+ if (argv.length < prefix.length) return false;
1727
+ return prefix.every((tok, i) => argv[i] === tok);
1728
+ });
1729
+ }
1730
+ function capitalizeFirst(s) {
1731
+ if (s.length === 0) return s;
1732
+ return `${s[0]?.toUpperCase() ?? ""}${s.slice(1)}`;
1733
+ }
1734
+ function hasAction(cmd) {
1735
+ return cmd._actionHandler != null;
1736
+ }
1737
+ function collectLeaves(cmd, path, out) {
1738
+ const children = cmd.commands;
1739
+ const isLeaf = hasAction(cmd) && children.length === 0;
1740
+ if (isLeaf) {
1741
+ const argv = path;
1742
+ const id = argv.join(" ");
1743
+ const description = cmd.description() || argv.join(" ");
1744
+ out.push({
1745
+ id,
1746
+ label: argv.map(capitalizeFirst).join(": "),
1747
+ argv,
1748
+ description,
1749
+ category: argv[0] ?? "",
1750
+ keywords: argv,
1751
+ destructive: isDestructive(argv)
1752
+ });
1753
+ return;
1754
+ }
1755
+ for (const child of children) {
1756
+ collectLeaves(child, [...path, child.name()], out);
1757
+ }
1758
+ }
1759
+ function buildPaletteCommands(program2) {
1760
+ const out = [];
1761
+ collectLeaves(program2, [], out);
1762
+ return out;
1763
+ }
1764
+
1494
1765
  // src/bin.ts
1766
+ function buildPaletteCommandsForTui() {
1767
+ return buildPaletteCommands(createProgram());
1768
+ }
1495
1769
  function actionGlobals(args) {
1496
1770
  const last = args[args.length - 1];
1497
1771
  return last?.optsWithGlobals() ?? {};
@@ -1596,7 +1870,7 @@ function createProgram() {
1596
1870
  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(
1597
1871
  "--upgrade",
1598
1872
  "run scaffold migrations before re-emitting (re-run on an existing project)"
1599
- ).option("--force", "re-scaffold even if already initialized (bypasses the already-init no-op)").addOption(
1873
+ ).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(
1600
1874
  // S10: target host. Defaults to `'claude'` (the regression anchor). The
1601
1875
  // chosen host is forwarded to scaffold() + skills emission via
1602
1876
  // resolveAdapter. The choice list mirrors `SUPPORTED_HOSTS` so a new host
@@ -1611,19 +1885,21 @@ function createProgram() {
1611
1885
  const upgrade = opts.upgrade === true;
1612
1886
  const force = opts.force === true;
1613
1887
  const host = parseHost(opts.host);
1888
+ const dryRun = opts.dryRun === true || opts.preview === true;
1614
1889
  const result = await init(process.cwd(), {
1615
1890
  transport,
1616
1891
  url: opts.url,
1617
1892
  ...upgrade ? { upgrade } : {},
1618
1893
  ...force ? { force } : {},
1619
- ...host !== void 0 ? { host } : {}
1894
+ ...host !== void 0 ? { host } : {},
1895
+ ...dryRun ? { dryRun } : {}
1620
1896
  });
1621
1897
  if (cmd.optsWithGlobals().json === true && result !== void 0) {
1622
1898
  json({ ok: true, data: result });
1623
1899
  }
1624
1900
  }
1625
1901
  );
1626
- 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(
1902
+ 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(
1627
1903
  new Option(
1628
1904
  "--host <id>",
1629
1905
  "target agentic CLI (default: claude) \u2014 drives host-side emission"
@@ -1633,12 +1909,14 @@ function createProgram() {
1633
1909
  const transport = opts.transport === "streamable-http" ? "streamable-http" : "stdio";
1634
1910
  const force = opts.force === true;
1635
1911
  const host = parseHost(opts.host);
1636
- const { create } = await import("./create-RHFPB5Q2.js");
1912
+ const dryRun = opts.dryRun === true || opts.preview === true;
1913
+ const { create } = await import("./create-UO3QTGL7.js");
1637
1914
  const result = await create(dir, {
1638
1915
  transport,
1639
1916
  url: opts.url,
1640
1917
  ...force ? { force } : {},
1641
- ...host !== void 0 ? { host } : {}
1918
+ ...host !== void 0 ? { host } : {},
1919
+ ...dryRun ? { dryRun } : {}
1642
1920
  });
1643
1921
  if (cmd.optsWithGlobals().json === true && result !== void 0) {
1644
1922
  json({ ok: true, data: result });
@@ -1653,7 +1931,7 @@ function createProgram() {
1653
1931
  ).option(
1654
1932
  "--merge",
1655
1933
  "three-way merge managed regions (default since 1.3.0; flag kept for compatibility)"
1656
- ).addOption(
1934
+ ).option("--dry-run", "report planned writes without writing anything").option("--preview", "alias for --dry-run").addOption(
1657
1935
  // Opt OUT of managed-region merge (restore strip-replace). Commander
1658
1936
  // stores `--no-merge-regions` under `mergeRegions` (default true; flag →
1659
1937
  // false). Bare `noir sync` keeps the merge default (true).
@@ -1671,16 +1949,18 @@ function createProgram() {
1671
1949
  ).choices(SUPPORTED_HOSTS)
1672
1950
  ).action(
1673
1951
  async (opts, cmd) => {
1674
- const { sync } = await import("./sync-ZGCGL3OU.js");
1952
+ const { sync } = await import("./sync-MZETGPOJ.js");
1675
1953
  const host = parseHost(opts.host);
1676
1954
  const force = opts.force === true;
1677
1955
  const merge = opts.merge === true;
1678
1956
  const noMergeRegions = opts.mergeRegions === false;
1679
- const result = host === void 0 && !force && !merge && !noMergeRegions ? await sync(process.cwd()) : await sync(process.cwd(), {
1957
+ const dryRun = opts.dryRun === true || opts.preview === true;
1958
+ const result = host === void 0 && !force && !merge && !noMergeRegions && !dryRun ? await sync(process.cwd()) : await sync(process.cwd(), {
1680
1959
  ...host !== void 0 ? { host } : {},
1681
1960
  ...force ? { force } : {},
1682
1961
  ...merge ? { merge } : {},
1683
- ...noMergeRegions ? { mergeManagedRegions: false } : {}
1962
+ ...noMergeRegions ? { mergeManagedRegions: false } : {},
1963
+ ...dryRun ? { dryRun } : {}
1684
1964
  });
1685
1965
  if (cmd.optsWithGlobals().json === true) {
1686
1966
  json({ ok: true, data: result });
@@ -1695,11 +1975,18 @@ function createProgram() {
1695
1975
  throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio]");
1696
1976
  });
1697
1977
  const daemonGrp = program2.command("daemon").description("control the Noir daemon");
1698
- 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) => {
1978
+ daemonGrp.command("start").description("start the Noir daemon (foreground, or background with --detach)").option("--detach", "run the daemon in the background and exit").addOption(
1979
+ new Option("--_detached-child", "reserved: detached daemon child (internal)").hideHelp()
1980
+ ).action(async (...args) => {
1699
1981
  const cmd = trailingCmd(args);
1700
1982
  const g = cmd.optsWithGlobals();
1701
1983
  const detach = g.detach === true;
1702
- await daemonStart({ ...toCliOptions(g), ...detach ? { detach } : {} });
1984
+ const detachChild = g._detachedChild === true;
1985
+ await daemonStart({
1986
+ ...toCliOptions(g),
1987
+ ...detach ? { detach } : {},
1988
+ ...detachChild ? { detachChild } : {}
1989
+ });
1703
1990
  });
1704
1991
  daemonGrp.command("stop").description("stop the Noir daemon").action(async (...args) => {
1705
1992
  await daemonStop(toCliOptions(trailingCmd(args).optsWithGlobals()));
@@ -1741,7 +2028,7 @@ function createProgram() {
1741
2028
  "path to index (repeatable)",
1742
2029
  (val, acc) => [...acc, val],
1743
2030
  []
1744
- ).option("--force", "ignore content-hash caching (recognized; not yet honored)").action(async (...args) => {
2031
+ ).option("--force", "force a full reindex (drop all chunks+vectors, re-index from scratch)").action(async (...args) => {
1745
2032
  const cmd = trailingCmd(args);
1746
2033
  const g = cmd.optsWithGlobals();
1747
2034
  const rawPaths = Array.isArray(g.path) ? g.path.filter((p) => typeof p === "string") : [];
@@ -1963,6 +2250,7 @@ export {
1963
2250
  DEPRECATIONS,
1964
2251
  EXIT,
1965
2252
  NoirCliError,
2253
+ buildPaletteCommandsForTui,
1966
2254
  createProgram,
1967
2255
  emitDeprecationHintsFor,
1968
2256
  fail,