@noir-ai/cli 1.12.0-beta.1 → 1.13.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.
@@ -28,16 +28,22 @@ import {
28
28
  } from "./chunk-ESZTOQEC.js";
29
29
 
30
30
  // src/commands/status.ts
31
- import { loadProjectInfo as loadProjectInfo11, NOIR_VERSION as NOIR_VERSION5 } from "@noir-ai/core";
31
+ import { loadProjectInfo as loadProjectInfo13, NOIR_VERSION as NOIR_VERSION5 } from "@noir-ai/core";
32
32
 
33
33
  // src/daemon-client.ts
34
34
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
35
35
  import { ContextEngine, createEmbedFn, resolveEmbedderConfig } from "@noir-ai/context";
36
- import { loadProjectInfo as loadProjectInfo10, NOIR_VERSION as NOIR_VERSION4 } from "@noir-ai/core";
36
+ import {
37
+ loadProjectInfo as loadProjectInfo12,
38
+ NOIR_VERSION as NOIR_VERSION4,
39
+ readWorkspaceMarker as readWorkspaceMarker2,
40
+ workspaceStoreDbPath
41
+ } from "@noir-ai/core";
37
42
  import {
38
43
  ensureDaemonRunning as ensureDaemonRunning3,
39
- pidAlive as pidAlive3,
44
+ pidAlive as pidAlive4,
40
45
  readDaemonRecord as readDaemonRecord3,
46
+ readWorkspaceDaemonRecord as readWorkspaceDaemonRecord2,
41
47
  resolveGateConfig
42
48
  } from "@noir-ai/daemon";
43
49
  import { createMemoryEngine } from "@noir-ai/memory";
@@ -217,14 +223,319 @@ function renderStatus(data, opts) {
217
223
  }
218
224
 
219
225
  // src/commands/daemon.ts
220
- import { loadProjectInfo } from "@noir-ai/core";
226
+ import { loadProjectInfo as loadProjectInfo2 } from "@noir-ai/core";
221
227
  import {
222
228
  clearDaemonRecord,
223
229
  ensureDaemonRunning,
224
- pidAlive,
230
+ pidAlive as pidAlive2,
225
231
  readDaemonRecord,
226
232
  spawnDetachedDaemon
227
233
  } from "@noir-ai/daemon";
234
+
235
+ // src/commands/workspace.ts
236
+ import { readdirSync } from "fs";
237
+ import {
238
+ clearWorkspaceMarker,
239
+ ensureWorkspaceRegistry,
240
+ isValidWorkspaceName,
241
+ isWorkspaceMember,
242
+ loadProjectInfo,
243
+ readWorkspaceMarker,
244
+ readWorkspaceRegistry,
245
+ removeWorkspaceMember,
246
+ upsertWorkspaceMember,
247
+ workspaceHomeDir,
248
+ writeWorkspaceMarker
249
+ } from "@noir-ai/core";
250
+ import {
251
+ clearWorkspaceDaemonRecord,
252
+ ensureWorkspaceDaemonRunning,
253
+ pidAlive,
254
+ readWorkspaceDaemonRecord,
255
+ spawnDetachedWorkspaceDaemon
256
+ } from "@noir-ai/daemon";
257
+
258
+ // src/workspace-mcp.ts
259
+ import { existsSync, readFileSync } from "fs";
260
+ import { join } from "path";
261
+ import { resolveAdapter } from "@noir-ai/adapters";
262
+ import { atomicWriteFile, resolveNoirCommand } from "@noir-ai/core";
263
+ function mcpPathFor(root, host) {
264
+ return resolveAdapter(host).mcpConfigPath?.({ root }) ?? join(root, ".mcp.json");
265
+ }
266
+ function readJson(path) {
267
+ if (!existsSync(path)) return null;
268
+ return JSON.parse(readFileSync(path, "utf8"));
269
+ }
270
+ function loadExisting(path, opts) {
271
+ let existing;
272
+ try {
273
+ existing = readJson(path);
274
+ } catch {
275
+ if (opts.force !== true) {
276
+ fail(
277
+ EXIT.ERROR,
278
+ `Refusing to rewrite ${path}: it is not valid JSON (add --force to overwrite).`,
279
+ opts
280
+ );
281
+ }
282
+ existing = null;
283
+ }
284
+ if (existing !== null && !isNoirEmitted(existing.mcpServers) && opts.force !== true) {
285
+ fail(
286
+ EXIT.ERROR,
287
+ `Refusing to rewrite ${path}: it does not look like a Noir-emitted config (add --force to overwrite).`,
288
+ opts
289
+ );
290
+ }
291
+ return existing;
292
+ }
293
+ function isNoirEmitted(mcpServers) {
294
+ const noir = mcpServers?.noir;
295
+ if (!noir) return false;
296
+ if (typeof noir.command === "string") return true;
297
+ return noir.type === "http";
298
+ }
299
+ function writeWorkspaceHttpEntry(root, host, url, projectId, opts) {
300
+ const path = mcpPathFor(root, host);
301
+ const existing = loadExisting(path, opts);
302
+ const next = {
303
+ ...existing ?? {},
304
+ mcpServers: {
305
+ ...existing?.mcpServers ?? {},
306
+ noir: { type: "http", url: `${url}?p=${projectId}` }
307
+ }
308
+ };
309
+ atomicWriteFile(path, `${JSON.stringify(next, null, 2)}
310
+ `);
311
+ }
312
+ function writeStdioEntry(root, host, opts) {
313
+ const path = mcpPathFor(root, host);
314
+ const existing = loadExisting(path, opts);
315
+ const noir = { command: resolveNoirCommand(), args: ["mcp", "serve", "--stdio"] };
316
+ const next = { ...existing ?? {}, mcpServers: { ...existing?.mcpServers ?? {}, noir } };
317
+ atomicWriteFile(path, `${JSON.stringify(next, null, 2)}
318
+ `);
319
+ }
320
+
321
+ // src/commands/workspace.ts
322
+ function loadProjectOrFail(opts) {
323
+ try {
324
+ return loadProjectInfo(process.cwd());
325
+ } catch {
326
+ fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
327
+ }
328
+ }
329
+ function finishJoin(project, name, url, opts) {
330
+ writeWorkspaceMarker(project.root, name);
331
+ writeWorkspaceHttpEntry(project.root, project.config.host, url, project.id, opts);
332
+ }
333
+ async function daemonStartWorkspace(opts) {
334
+ const project = loadProjectOrFail(opts);
335
+ if (!isValidWorkspaceName(opts.name)) {
336
+ fail(
337
+ EXIT.USAGE,
338
+ `invalid workspace name ${JSON.stringify(opts.name)} (lowercase letters, digits, and dashes).`,
339
+ opts
340
+ );
341
+ }
342
+ upsertWorkspaceMember(ensureWorkspaceRegistry(opts.name), {
343
+ projectId: project.id,
344
+ root: project.root,
345
+ joinedAt: Date.now()
346
+ });
347
+ if (opts.detachChild === true) {
348
+ const ensured2 = await ensureWorkspaceDaemonRunning({
349
+ name: opts.name,
350
+ project,
351
+ idleTimeoutSec: project.config.workspace.idleTimeoutSec
352
+ });
353
+ if (opts.json === true) {
354
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { mode: "detached" } })}
355
+ `);
356
+ return;
357
+ }
358
+ info("noir workspace daemon: detached foreground. Ctrl+C to stop.", opts);
359
+ log(`workspace daemon listening at ${ensured2.url}`, opts);
360
+ return;
361
+ }
362
+ if (opts.detach === true) {
363
+ const spawned = await spawnDetachedWorkspaceDaemon({ name: opts.name, project });
364
+ finishJoin(project, opts.name, `http://127.0.0.1:${spawned.port}/mcp`, opts);
365
+ if (opts.json === true) {
366
+ process.stdout.write(
367
+ `${JSON.stringify({ ok: true, data: { mode: "detached", pid: spawned.pid, port: spawned.port } })}
368
+ `
369
+ );
370
+ return;
371
+ }
372
+ info(
373
+ `workspace "${opts.name}" daemon started (pid ${spawned.pid}, port ${spawned.port}).`,
374
+ opts
375
+ );
376
+ return;
377
+ }
378
+ const ensured = await ensureWorkspaceDaemonRunning({
379
+ name: opts.name,
380
+ project,
381
+ idleTimeoutSec: project.config.workspace.idleTimeoutSec
382
+ });
383
+ finishJoin(project, opts.name, ensured.url, opts);
384
+ if (opts.json === true) {
385
+ process.stdout.write(
386
+ `${JSON.stringify({ ok: true, data: { url: ensured.url, port: ensured.port, reused: !ensured.started } })}
387
+ `
388
+ );
389
+ return;
390
+ }
391
+ info("noir workspace daemon: foreground mode. Ctrl+C to stop.", opts);
392
+ log(`workspace daemon listening at ${ensured.url}`, opts);
393
+ }
394
+ async function daemonJoin(opts) {
395
+ const project = loadProjectOrFail(opts);
396
+ if (!isValidWorkspaceName(opts.name)) {
397
+ fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(opts.name)}.`, opts);
398
+ }
399
+ const reg = readWorkspaceRegistry(opts.name);
400
+ if (!reg) {
401
+ fail(
402
+ EXIT.ERROR,
403
+ `workspace ${JSON.stringify(opts.name)} not found \u2014 start it from a member repo with \`noir daemon start --workspace ${opts.name}\`.`,
404
+ opts
405
+ );
406
+ }
407
+ const ensured = await ensureWorkspaceDaemonRunning({
408
+ name: opts.name,
409
+ project,
410
+ idleTimeoutSec: project.config.workspace.idleTimeoutSec
411
+ });
412
+ upsertWorkspaceMember(reg, { projectId: project.id, root: project.root, joinedAt: Date.now() });
413
+ finishJoin(project, opts.name, ensured.url, opts);
414
+ if (opts.json === true) {
415
+ process.stdout.write(
416
+ `${JSON.stringify({ ok: true, data: { joined: true, url: ensured.url, port: ensured.port } })}
417
+ `
418
+ );
419
+ return;
420
+ }
421
+ info(`joined workspace ${JSON.stringify(opts.name)} (${ensured.url}).`, opts);
422
+ }
423
+ async function workspaceList(opts) {
424
+ let names = [];
425
+ try {
426
+ names = readdirSync(workspaceHomeDir(), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
427
+ } catch {
428
+ names = [];
429
+ }
430
+ const rows = names.map((n) => {
431
+ const reg = readWorkspaceRegistry(n);
432
+ return {
433
+ name: n,
434
+ members: reg?.members.length ?? 0,
435
+ // A record file whose pid is dead (daemon killed without cleanup) must not
436
+ // read as "running" — mirror the pidAlive liveness `workspace status` uses.
437
+ running: (() => {
438
+ const rec = readWorkspaceDaemonRecord(n);
439
+ return rec !== null && pidAlive(rec.pid);
440
+ })()
441
+ };
442
+ });
443
+ if (opts.json === true) {
444
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { workspaces: rows } })}
445
+ `);
446
+ return;
447
+ }
448
+ if (rows.length === 0) {
449
+ info("No workspaces yet.", opts);
450
+ return;
451
+ }
452
+ for (const r of rows) {
453
+ log(`${r.name} \u2014 ${r.members} member(s)${r.running ? ", running" : ""}`, opts);
454
+ }
455
+ }
456
+ async function workspaceStatus(opts) {
457
+ const name = opts.name ?? readWorkspaceMarker(process.cwd()) ?? void 0;
458
+ if (name === void 0) {
459
+ fail(EXIT.USAGE, "workspace status requires a name (or run from a joined repo).", opts);
460
+ }
461
+ if (!isValidWorkspaceName(name)) {
462
+ fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(name)}.`, opts);
463
+ }
464
+ const reg = readWorkspaceRegistry(name);
465
+ const rec = readWorkspaceDaemonRecord(name);
466
+ const running = rec !== null && pidAlive(rec.pid);
467
+ const data = {
468
+ name,
469
+ members: reg?.members.map((m) => m.projectId) ?? [],
470
+ running,
471
+ ...rec ? { pid: rec.pid, port: rec.port } : {}
472
+ };
473
+ if (opts.json === true) {
474
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
475
+ `);
476
+ return;
477
+ }
478
+ log(
479
+ `workspace ${name}: ${running ? `running (pid ${rec?.pid}, port ${rec?.port})` : "not running"}`,
480
+ opts
481
+ );
482
+ for (const m of data.members) log(` member: ${m}`, opts);
483
+ }
484
+ async function workspaceLeave(opts) {
485
+ const project = loadProjectOrFail(opts);
486
+ const name = readWorkspaceMarker(project.root);
487
+ if (name === null) {
488
+ fail(EXIT.USAGE, "this repo is not joined to a workspace.", opts);
489
+ }
490
+ writeStdioEntry(project.root, project.config.host, opts);
491
+ const reg = readWorkspaceRegistry(name);
492
+ if (reg && isWorkspaceMember(reg, project.id)) {
493
+ removeWorkspaceMember(reg, project.id);
494
+ }
495
+ clearWorkspaceMarker(project.root);
496
+ if (opts.json === true) {
497
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { left: name } })}
498
+ `);
499
+ return;
500
+ }
501
+ info(`left workspace ${JSON.stringify(name)}.`, opts);
502
+ }
503
+ async function workspaceStop(opts) {
504
+ const name = opts.name ?? readWorkspaceMarker(process.cwd()) ?? void 0;
505
+ if (name === void 0) {
506
+ fail(EXIT.USAGE, "workspace stop requires a name (or run from a joined repo).", opts);
507
+ }
508
+ if (!isValidWorkspaceName(name)) {
509
+ fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(name)}.`, opts);
510
+ }
511
+ const rec = readWorkspaceDaemonRecord(name);
512
+ if (rec === null || !pidAlive(rec.pid)) {
513
+ if (opts.json === true) {
514
+ process.stdout.write(
515
+ `${JSON.stringify({ ok: true, data: { running: false, stopped: false } })}
516
+ `
517
+ );
518
+ return;
519
+ }
520
+ info("workspace daemon is not running.", opts);
521
+ return;
522
+ }
523
+ try {
524
+ process.kill(rec.pid, "SIGTERM");
525
+ } catch {
526
+ }
527
+ clearWorkspaceDaemonRecord(name);
528
+ if (opts.json === true) {
529
+ process.stdout.write(
530
+ `${JSON.stringify({ ok: true, data: { running: false, stopped: true, pid: rec.pid } })}
531
+ `
532
+ );
533
+ return;
534
+ }
535
+ info(`stopped workspace daemon (pid ${rec.pid}).`, opts);
536
+ }
537
+
538
+ // src/commands/daemon.ts
228
539
  var MODE = "foreground";
229
540
  function formatUptime(sec) {
230
541
  if (!Number.isFinite(sec) || sec < 0) return "unknown";
@@ -234,7 +545,7 @@ function formatUptime(sec) {
234
545
  }
235
546
  function resolveCallerProjectId() {
236
547
  try {
237
- return loadProjectInfo(process.cwd()).id;
548
+ return loadProjectInfo2(process.cwd()).id;
238
549
  } catch {
239
550
  return void 0;
240
551
  }
@@ -255,9 +566,18 @@ async function isHealthy(port, expectedPid, expectedProjectId) {
255
566
  }
256
567
  }
257
568
  async function daemonStart(opts) {
569
+ if (opts.workspace !== void 0) {
570
+ await daemonStartWorkspace({
571
+ ...opts,
572
+ name: opts.workspace,
573
+ ...opts.detach === true ? { detach: true } : {},
574
+ ...opts.detachChild === true ? { detachChild: true } : {}
575
+ });
576
+ return;
577
+ }
258
578
  let project;
259
579
  try {
260
- project = loadProjectInfo(process.cwd());
580
+ project = loadProjectInfo2(process.cwd());
261
581
  } catch {
262
582
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
263
583
  }
@@ -288,7 +608,7 @@ async function daemonStart(opts) {
288
608
  }
289
609
  if (opts.detach === true) {
290
610
  const existing = readDaemonRecord();
291
- if (existing && pidAlive(existing.pid) && await isHealthy(existing.port, existing.pid, project.id)) {
611
+ if (existing && pidAlive2(existing.pid) && await isHealthy(existing.port, existing.pid, project.id)) {
292
612
  if (opts.json === true) {
293
613
  process.stdout.write(
294
614
  `${JSON.stringify({ ok: true, data: { mode: "detached", reused: true } })}
@@ -387,7 +707,7 @@ async function daemonStop(opts) {
387
707
  let errMsg2;
388
708
  const callerProject = resolveCallerProjectId();
389
709
  const wrongProject = rec.projectId !== void 0 && callerProject !== void 0 && rec.projectId !== callerProject;
390
- const healthy = !wrongProject && pidAlive(rec.pid) && await isHealthy(rec.port, rec.pid, rec.projectId === void 0 ? void 0 : callerProject);
710
+ const healthy = !wrongProject && pidAlive2(rec.pid) && await isHealthy(rec.port, rec.pid, rec.projectId === void 0 ? void 0 : callerProject);
391
711
  if (healthy) {
392
712
  try {
393
713
  process.kill(rec.pid, "SIGTERM");
@@ -433,7 +753,7 @@ async function daemonStatus(opts) {
433
753
  opts
434
754
  );
435
755
  }
436
- if (!pidAlive(rec.pid)) {
756
+ if (!pidAlive2(rec.pid)) {
437
757
  clearDaemonRecord();
438
758
  fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
439
759
  }
@@ -488,21 +808,21 @@ async function daemonRestart(opts) {
488
808
 
489
809
  // src/commands/doctor.ts
490
810
  import { spawnSync } from "child_process";
491
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
492
- import { dirname, join, relative } from "path";
811
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync } from "fs";
812
+ import { dirname, join as join2, relative } from "path";
493
813
  import { fileURLToPath } from "url";
494
- import { resolveAdapter } from "@noir-ai/adapters";
814
+ import { resolveAdapter as resolveAdapter2 } from "@noir-ai/adapters";
495
815
  import {
496
816
  detectActiveMethod,
497
817
  latestVersionFromCache,
498
- loadProjectInfo as loadProjectInfo2,
818
+ loadProjectInfo as loadProjectInfo3,
499
819
  NOIR_VERSION,
500
820
  paths,
501
821
  readInstallRecord,
502
822
  readUpdateCache
503
823
  } from "@noir-ai/core";
504
824
  import { CURRENT_SCAFFOLD_VERSION, readScaffoldVersion } from "@noir-ai/create";
505
- import { pidAlive as pidAlive2, readDaemonRecord as readDaemonRecord2 } from "@noir-ai/daemon";
825
+ import { pidAlive as pidAlive3, readDaemonRecord as readDaemonRecord2 } from "@noir-ai/daemon";
506
826
  import { resolveModelConfig } from "@noir-ai/model";
507
827
  async function probeOnnx() {
508
828
  try {
@@ -539,7 +859,7 @@ async function checkRuntime(checks) {
539
859
  }
540
860
  function checkConfig(root) {
541
861
  const configPath = paths.config(root);
542
- if (!existsSync(configPath)) {
862
+ if (!existsSync2(configPath)) {
543
863
  return {
544
864
  project: void 0,
545
865
  result: {
@@ -550,7 +870,7 @@ function checkConfig(root) {
550
870
  };
551
871
  }
552
872
  try {
553
- const project = loadProjectInfo2(root);
873
+ const project = loadProjectInfo3(root);
554
874
  return {
555
875
  project,
556
876
  result: {
@@ -576,7 +896,7 @@ async function checkDaemon(checks) {
576
896
  });
577
897
  return;
578
898
  }
579
- if (!pidAlive2(rec.pid)) {
899
+ if (!pidAlive3(rec.pid)) {
580
900
  checks.push({
581
901
  name: "daemon",
582
902
  status: "warn",
@@ -631,7 +951,7 @@ async function checkStore(checks, project, root) {
631
951
  return;
632
952
  }
633
953
  const dbPath = paths.storeDb(root, project.id);
634
- if (!existsSync(dbPath)) {
954
+ if (!existsSync2(dbPath)) {
635
955
  checks.push({
636
956
  name: "store",
637
957
  status: "warn",
@@ -660,7 +980,7 @@ async function checkStore(checks, project, root) {
660
980
  }
661
981
  }
662
982
  function checkNoirEnv(checks, root) {
663
- const path = join(paths.noirDir(root), ".env");
983
+ const path = join2(paths.noirDir(root), ".env");
664
984
  let st;
665
985
  try {
666
986
  st = statSync(path);
@@ -770,7 +1090,7 @@ function checkRulesMdBudget(checks, root, project) {
770
1090
  return null;
771
1091
  }
772
1092
  const rulesPath = paths.rulesMd(root);
773
- if (!existsSync(rulesPath)) {
1093
+ if (!existsSync2(rulesPath)) {
774
1094
  checks.push({
775
1095
  name: "rules budget",
776
1096
  status: "ok",
@@ -780,7 +1100,7 @@ function checkRulesMdBudget(checks, root, project) {
780
1100
  }
781
1101
  let content;
782
1102
  try {
783
- content = readFileSync(rulesPath, "utf8");
1103
+ content = readFileSync2(rulesPath, "utf8");
784
1104
  } catch (e) {
785
1105
  checks.push({
786
1106
  name: "rules budget",
@@ -824,7 +1144,7 @@ function checkHostArtifacts(checks, root, project) {
824
1144
  return null;
825
1145
  }
826
1146
  const host = project.config.host;
827
- const adapter = resolveAdapter(host);
1147
+ const adapter = resolveAdapter2(host);
828
1148
  const ectx = { root };
829
1149
  const expected = [];
830
1150
  const emitsAgentsMd = host === "agents-md" || host === "cursor" || host === "opencode";
@@ -834,7 +1154,7 @@ function checkHostArtifacts(checks, root, project) {
834
1154
  if (host === "claude") expected.push("CLAUDE.md");
835
1155
  else if (host === "gemini") expected.push("GEMINI.md");
836
1156
  expected.push(relOr(adapter.mcpConfigPath?.(ectx) ?? ".mcp.json", root));
837
- const missing = expected.filter((p) => !existsSync(joinRoot(root, p)));
1157
+ const missing = expected.filter((p) => !existsSync2(joinRoot(root, p)));
838
1158
  const status2 = missing.length === 0 ? "ok" : "warn";
839
1159
  const detail = missing.length === 0 ? `host=${host} \xB7 all ${expected.length} primary artifacts present` : `host=${host} \xB7 missing ${missing.join(", ")} (run \`noir sync\` to restore)`;
840
1160
  checks.push({ name: "host artifacts", status: status2, detail });
@@ -851,7 +1171,7 @@ function checkNestedNoir(checks, root) {
851
1171
  ".noir/.npmignore",
852
1172
  ".noir/.prettierignore"
853
1173
  ];
854
- const found = candidates.filter((rel) => existsSync(join(root, rel)));
1174
+ const found = candidates.filter((rel) => existsSync2(join2(root, rel)));
855
1175
  const detected = found.length > 0;
856
1176
  checks.push({
857
1177
  name: "nested .noir",
@@ -864,11 +1184,11 @@ function collectDedupCandidates(root) {
864
1184
  const rels = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"];
865
1185
  const out = [];
866
1186
  for (const rel of rels) {
867
- const abs = join(root, rel);
868
- if (!existsSync(abs)) continue;
1187
+ const abs = join2(root, rel);
1188
+ if (!existsSync2(abs)) continue;
869
1189
  let text;
870
1190
  try {
871
- text = readFileSync(abs, "utf8");
1191
+ text = readFileSync2(abs, "utf8");
872
1192
  } catch {
873
1193
  continue;
874
1194
  }
@@ -925,13 +1245,13 @@ async function checkSemanticDupDoctor(checks, root, project, opts = {}) {
925
1245
  function resolveWorkspacePackagesDir() {
926
1246
  let here = dirname(fileURLToPath(import.meta.url));
927
1247
  for (let i = 0; i < 8; i++) {
928
- const pkgPath = join(here, "package.json");
929
- if (existsSync(pkgPath)) {
1248
+ const pkgPath = join2(here, "package.json");
1249
+ if (existsSync2(pkgPath)) {
930
1250
  try {
931
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
1251
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
932
1252
  if (pkg.name === "@noir-ai/cli") {
933
1253
  const packagesDir = dirname(here);
934
- if (existsSync(join(dirname(packagesDir), "pnpm-workspace.yaml"))) {
1254
+ if (existsSync2(join2(dirname(packagesDir), "pnpm-workspace.yaml"))) {
935
1255
  return packagesDir;
936
1256
  }
937
1257
  return null;
@@ -977,7 +1297,7 @@ function checkPublish(checks, packagesDir, opts = {}) {
977
1297
  }
978
1298
  let entries = [];
979
1299
  try {
980
- entries = readdirSync(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1300
+ entries = readdirSync2(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
981
1301
  } catch (e) {
982
1302
  checks.push({
983
1303
  name: "publish",
@@ -990,11 +1310,11 @@ function checkPublish(checks, packagesDir, opts = {}) {
990
1310
  let checked = 0;
991
1311
  let cliDir = null;
992
1312
  for (const name of entries) {
993
- const pkgPath = join(packagesDir, name, "package.json");
994
- if (!existsSync(pkgPath)) continue;
1313
+ const pkgPath = join2(packagesDir, name, "package.json");
1314
+ if (!existsSync2(pkgPath)) continue;
995
1315
  let pkg;
996
1316
  try {
997
- pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
1317
+ pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
998
1318
  } catch (e) {
999
1319
  issues.push(`${name}/package.json: unparseable (${msg(e)})`);
1000
1320
  checked++;
@@ -1016,7 +1336,7 @@ function checkPublish(checks, packagesDir, opts = {}) {
1016
1336
  if (pkg.bin == null || typeof pkg.bin === "object" && Object.keys(pkg.bin).length === 0) {
1017
1337
  issues.push(`${name}/package.json: bin missing (cli package must declare a bin)`);
1018
1338
  }
1019
- cliDir = join(packagesDir, name);
1339
+ cliDir = join2(packagesDir, name);
1020
1340
  }
1021
1341
  }
1022
1342
  let packNote = "";
@@ -1142,18 +1462,18 @@ async function doctor(opts = {}) {
1142
1462
 
1143
1463
  // src/commands/handoff.ts
1144
1464
  import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
1145
- import { join as join2 } from "path";
1465
+ import { join as join3 } from "path";
1146
1466
  import {
1147
1467
  defaultHandoffBlock,
1148
1468
  hostLaunchDirective,
1149
- resolveAdapter as resolveAdapter2
1469
+ resolveAdapter as resolveAdapter3
1150
1470
  } from "@noir-ai/adapters";
1151
- import { loadProjectInfo as loadProjectInfo4, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
1471
+ import { loadProjectInfo as loadProjectInfo5, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
1152
1472
 
1153
1473
  // src/commands/task.ts
1154
1474
  import { spawnSync as spawnSync2 } from "child_process";
1155
1475
  import { writeFileSync } from "fs";
1156
- import { loadProjectInfo as loadProjectInfo3, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
1476
+ import { loadProjectInfo as loadProjectInfo4, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
1157
1477
  import {
1158
1478
  PHASES,
1159
1479
  TASK_CLASSES,
@@ -1177,7 +1497,7 @@ function skillFor(phase) {
1177
1497
  }
1178
1498
  function writeDoneArtifacts(opts, taskId) {
1179
1499
  try {
1180
- const project = loadProjectInfo3(process.cwd());
1500
+ const project = loadProjectInfo4(process.cwd());
1181
1501
  const conflict = { conflictPolicy: "preserve", interactive: false };
1182
1502
  writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
1183
1503
  const nextN = nextArtifactSequence(project.root, "adr");
@@ -1501,7 +1821,7 @@ function runCheck(cmd) {
1501
1821
  async function taskVerify(opts) {
1502
1822
  let project;
1503
1823
  try {
1504
- project = loadProjectInfo3(process.cwd());
1824
+ project = loadProjectInfo4(process.cwd());
1505
1825
  } catch {
1506
1826
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
1507
1827
  }
@@ -1781,7 +2101,7 @@ function toHostPayload(p) {
1781
2101
  }
1782
2102
  function resolveDirective(root, p) {
1783
2103
  const ctx = { root };
1784
- const adapter = resolveAdapter2(p.host);
2104
+ const adapter = resolveAdapter3(p.host);
1785
2105
  const hostPayload = toHostPayload(p);
1786
2106
  if (typeof adapter.emitHandoff === "function") {
1787
2107
  return adapter.emitHandoff(ctx, hostPayload);
@@ -1824,7 +2144,7 @@ async function probeOnly(opts) {
1824
2144
  async function handoff(opts) {
1825
2145
  let project;
1826
2146
  try {
1827
- project = loadProjectInfo4(process.cwd());
2147
+ project = loadProjectInfo5(process.cwd());
1828
2148
  } catch {
1829
2149
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
1830
2150
  }
@@ -1856,7 +2176,7 @@ async function handoff(opts) {
1856
2176
  if (opts.write === true) {
1857
2177
  const md = renderMarkdown(full);
1858
2178
  const path = handoffFilePath(project.root, task?.taskId, project.id);
1859
- mkdirSync(join2(project.root, NOIR_DIR, "handoff"), { recursive: true });
2179
+ mkdirSync(join3(project.root, NOIR_DIR, "handoff"), { recursive: true });
1860
2180
  writeFileSync2(path, md, "utf8");
1861
2181
  info(`handoff written to ${path}`, opts);
1862
2182
  }
@@ -1873,7 +2193,7 @@ import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
1873
2193
  import {
1874
2194
  detectActiveMethod as detectActiveMethod2,
1875
2195
  latestVersionFromCache as latestVersionFromCache2,
1876
- loadProjectInfo as loadProjectInfo5,
2196
+ loadProjectInfo as loadProjectInfo6,
1877
2197
  NOIR_VERSION as NOIR_VERSION2,
1878
2198
  readInstallRecord as readInstallRecord2,
1879
2199
  readUpdateCache as readUpdateCache2
@@ -2167,7 +2487,7 @@ async function resolveSections(commands) {
2167
2487
  // src/commands/home.ts
2168
2488
  function tryProject() {
2169
2489
  try {
2170
- const info2 = loadProjectInfo5(process.cwd());
2490
+ const info2 = loadProjectInfo6(process.cwd());
2171
2491
  return { id: info2.id, name: info2.name, host: info2.config.host };
2172
2492
  } catch {
2173
2493
  return null;
@@ -2331,7 +2651,7 @@ async function runMenu(opts, deps) {
2331
2651
  env: process.env,
2332
2652
  configUpdate: (() => {
2333
2653
  try {
2334
- const info2 = loadProjectInfo5(process.cwd());
2654
+ const info2 = loadProjectInfo6(process.cwd());
2335
2655
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
2336
2656
  } catch {
2337
2657
  return DEFAULT_UPDATE_CONFIG;
@@ -2380,6 +2700,27 @@ ${COMMANDS_HINT}
2380
2700
  }
2381
2701
 
2382
2702
  // src/commands/memory.ts
2703
+ import { readFileSync as readFileSync3 } from "fs";
2704
+ import { loadProjectInfo as loadProjectInfo7 } from "@noir-ai/core";
2705
+ function resolveWorkspace() {
2706
+ const routing = workspaceRoutingTarget(process.cwd());
2707
+ if (routing === null) return null;
2708
+ try {
2709
+ return { routing, project: loadProjectInfo7(process.cwd()) };
2710
+ } catch {
2711
+ return null;
2712
+ }
2713
+ }
2714
+ async function callMemory(opts, name, args = {}) {
2715
+ const ws = resolveWorkspace();
2716
+ if (ws === null) return callDaemonTool(opts, name, args);
2717
+ return withWorkspaceDaemon(opts, ws.routing, (caller) => caller.callTool(name, args));
2718
+ }
2719
+ async function withMemoryDaemon(opts, fn) {
2720
+ const ws = resolveWorkspace();
2721
+ if (ws === null) return withDaemon(opts, fn);
2722
+ return withWorkspaceDaemon(opts, ws.routing, fn);
2723
+ }
2383
2724
  function parseLimit2(raw, label, opts) {
2384
2725
  if (raw === void 0) return void 0;
2385
2726
  const n = Number(raw);
@@ -2418,6 +2759,39 @@ function stamp(ts) {
2418
2759
  }
2419
2760
  async function memoryRecall(opts) {
2420
2761
  const limit = parseLimit2(opts.limit, "memory recall", opts);
2762
+ const ws = resolveWorkspace();
2763
+ if (ws !== null) {
2764
+ const probe2 = await probeWorkspaceDaemon(ws.routing);
2765
+ if (!probe2.running) {
2766
+ await withWorkspaceMemoryRead(opts, ws.routing, ws.project, async (memory) => {
2767
+ const { hits: hits3, degraded, mode } = await memory.recallWithMeta(opts.query, {
2768
+ ...limit === void 0 ? {} : { limit }
2769
+ });
2770
+ const data3 = { query: opts.query, hits: hits3, degraded, mode };
2771
+ if (opts.json === true) {
2772
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data3 })}
2773
+ `);
2774
+ return;
2775
+ }
2776
+ renderRecall(data3.query, data3.hits, data3.degraded, opts);
2777
+ });
2778
+ return;
2779
+ }
2780
+ const res2 = await callMemory(opts, "memory_recall", {
2781
+ query: opts.query,
2782
+ ...limit === void 0 ? {} : { limit }
2783
+ });
2784
+ if (res2.ok !== true) failTool2("memory recall", res2, opts);
2785
+ const hits2 = Array.isArray(res2.results) ? res2.results.map(toHit2) : [];
2786
+ const data2 = { query: opts.query, hits: hits2, degraded: res2.degraded === true };
2787
+ if (opts.json === true) {
2788
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
2789
+ `);
2790
+ return;
2791
+ }
2792
+ renderRecall(data2.query, hits2, data2.degraded, opts);
2793
+ return;
2794
+ }
2421
2795
  let probe;
2422
2796
  try {
2423
2797
  probe = await probeDaemon(opts);
@@ -2479,7 +2853,7 @@ async function memorySave(opts) {
2479
2853
  const args = { content };
2480
2854
  if (typeof opts.type === "string" && opts.type.length > 0) args.type = opts.type;
2481
2855
  if (files !== void 0) args.files = files;
2482
- const res = await callDaemonTool(opts, "memory_save", args);
2856
+ const res = await callMemory(opts, "memory_save", args);
2483
2857
  if (res.ok !== true) failTool2("memory save", res, opts);
2484
2858
  const id = typeof res.id === "string" ? res.id : "";
2485
2859
  const observation = res.observation ?? {};
@@ -2523,7 +2897,98 @@ function renderObservation(obs, opts) {
2523
2897
  if (typeof content === "string") log(`
2524
2898
  ${content}`, opts);
2525
2899
  }
2900
+ async function memoryCapture(opts) {
2901
+ const content = await resolveCaptureContent(opts);
2902
+ const args = { content };
2903
+ if (typeof opts.eventType === "string" && opts.eventType.length > 0)
2904
+ args.eventType = opts.eventType;
2905
+ const res = await callMemory(opts, "memory_capture", args);
2906
+ if (res.ok !== true) failTool2("memory capture", res, opts);
2907
+ const id = typeof res.id === "string" ? res.id : "";
2908
+ const observation = res.observation ?? {};
2909
+ if (opts.json === true) {
2910
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { id, observation } })}
2911
+ `);
2912
+ return;
2913
+ }
2914
+ log(`Captured memory ${id}.`, opts);
2915
+ renderObservation(observation, opts);
2916
+ }
2917
+ async function resolveCaptureContent(opts) {
2918
+ if (typeof opts.content === "string" && opts.content.length > 0) return opts.content;
2919
+ if (typeof opts.file === "string" && opts.file.length > 0) {
2920
+ try {
2921
+ return readFileSync3(opts.file, "utf8");
2922
+ } catch {
2923
+ fail(EXIT.USAGE, `memory capture could not read --file ${opts.file}`, opts);
2924
+ }
2925
+ }
2926
+ if (!process.stdin.isTTY) {
2927
+ try {
2928
+ return readFileSync3(0, "utf8");
2929
+ } catch {
2930
+ fail(
2931
+ EXIT.USAGE,
2932
+ "memory capture could not read piped stdin \u2014 pass --content <text> or --file <path>.",
2933
+ opts
2934
+ );
2935
+ }
2936
+ }
2937
+ if (!isInteractive(opts)) {
2938
+ fail(
2939
+ EXIT.USAGE,
2940
+ "memory capture requires --content <text> or --file <path> (or piped stdin).",
2941
+ opts
2942
+ );
2943
+ }
2944
+ const clack = await import("@clack/prompts");
2945
+ const value = await clack.text({ message: "Captured memory content:" });
2946
+ if (clack.isCancel(value)) {
2947
+ clack.cancel("Cancelled.");
2948
+ fail(EXIT.CANCELLED, "cancelled", opts);
2949
+ }
2950
+ return String(value);
2951
+ }
2526
2952
  async function memorySessions(opts) {
2953
+ const ws = resolveWorkspace();
2954
+ if (ws !== null) {
2955
+ const probe2 = await probeWorkspaceDaemon(ws.routing);
2956
+ if (!probe2.running) {
2957
+ await withWorkspaceMemoryRead(opts, ws.routing, ws.project, async (memory) => {
2958
+ const sessions3 = memory.sessions().map((s) => ({
2959
+ id: s.id,
2960
+ count: s.count,
2961
+ lastTs: s.lastTs
2962
+ }));
2963
+ const data3 = { sessions: sessions3 };
2964
+ if (opts.json === true) {
2965
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data3 })}
2966
+ `);
2967
+ return;
2968
+ }
2969
+ renderSessions(sessions3, opts);
2970
+ });
2971
+ return;
2972
+ }
2973
+ const res2 = await callMemory(opts, "memory_sessions");
2974
+ if (res2.ok !== true) failTool2("memory sessions", res2, opts);
2975
+ const sessions2 = Array.isArray(res2.sessions) ? res2.sessions.map((raw) => {
2976
+ const s = raw ?? {};
2977
+ return {
2978
+ id: typeof s.id === "string" ? s.id : "",
2979
+ count: typeof s.count === "number" ? s.count : 0,
2980
+ lastTs: typeof s.lastTs === "number" ? s.lastTs : 0
2981
+ };
2982
+ }) : [];
2983
+ const data2 = { sessions: sessions2 };
2984
+ if (opts.json === true) {
2985
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
2986
+ `);
2987
+ return;
2988
+ }
2989
+ renderSessions(sessions2, opts);
2990
+ return;
2991
+ }
2527
2992
  let probe;
2528
2993
  try {
2529
2994
  probe = await probeDaemon(opts);
@@ -2578,7 +3043,7 @@ async function memoryForget(opts) {
2578
3043
  if (opts.ids.length === 0) {
2579
3044
  fail(EXIT.USAGE, "memory forget requires at least one <id>", opts);
2580
3045
  }
2581
- const res = await callDaemonTool(opts, "memory_forget", {
3046
+ const res = await callMemory(opts, "memory_forget", {
2582
3047
  ids: opts.ids
2583
3048
  });
2584
3049
  if (res.ok !== true) failTool2("memory forget", res, opts);
@@ -2592,11 +3057,11 @@ async function memoryForget(opts) {
2592
3057
  log(`Forgot ${deleted} observation${deleted === 1 ? "" : "s"}.`, opts);
2593
3058
  }
2594
3059
  async function memoryConsolidate(opts) {
2595
- const exposed = await withDaemon(opts, async (caller) => caller.listTools());
3060
+ const exposed = await withMemoryDaemon(opts, async (caller) => caller.listTools());
2596
3061
  if (!exposed.includes("memory_consolidate")) {
2597
3062
  fail(
2598
3063
  EXIT.ERROR,
2599
- "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.",
3064
+ resolveWorkspace() !== null ? "memory consolidate is not supported on shared workspaces (the workspace memory store has no consolidation model)." : "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.",
2600
3065
  opts
2601
3066
  );
2602
3067
  }
@@ -2605,7 +3070,7 @@ async function memoryConsolidate(opts) {
2605
3070
  const args = {};
2606
3071
  if (types !== void 0) args.types = types;
2607
3072
  if (limit !== void 0) args.limit = limit;
2608
- const res = await callDaemonTool(
3073
+ const res = await callMemory(
2609
3074
  opts,
2610
3075
  "memory_consolidate",
2611
3076
  args
@@ -2738,7 +3203,7 @@ async function release(opts) {
2738
3203
 
2739
3204
  // src/commands/run.ts
2740
3205
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
2741
- import { join as join3 } from "path";
3206
+ import { join as join4 } from "path";
2742
3207
  import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
2743
3208
  import { parseConfig } from "@noir-ai/core";
2744
3209
 
@@ -3104,10 +3569,10 @@ async function shellFallback(binary, args, opts, resolve, reject, originalErr) {
3104
3569
  }
3105
3570
 
3106
3571
  // src/run-profiles.ts
3107
- import { loadProjectInfo as loadProjectInfo6 } from "@noir-ai/core";
3572
+ import { loadProjectInfo as loadProjectInfo8 } from "@noir-ai/core";
3108
3573
  function loadRunConfig(root) {
3109
3574
  try {
3110
- return loadProjectInfo6(root).config;
3575
+ return loadProjectInfo8(root).config;
3111
3576
  } catch {
3112
3577
  return null;
3113
3578
  }
@@ -3246,8 +3711,8 @@ function mergeEnv(base, overlay) {
3246
3711
  }
3247
3712
  function writeTranscript(host, lines) {
3248
3713
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3249
- const dir = join3(process.cwd(), ".noir", "transcripts");
3250
- const file = join3(dir, `${host}-${ts}.jsonl`);
3714
+ const dir = join4(process.cwd(), ".noir", "transcripts");
3715
+ const file = join4(dir, `${host}-${ts}.jsonl`);
3251
3716
  try {
3252
3717
  mkdirSync2(dir, { recursive: true, mode: 448 });
3253
3718
  writeFileSync3(file, `${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`, { mode: 384 });
@@ -3258,8 +3723,8 @@ function writeTranscript(host, lines) {
3258
3723
  }
3259
3724
 
3260
3725
  // src/commands/skills.ts
3261
- import { resolveAdapter as resolveAdapter3 } from "@noir-ai/adapters";
3262
- import { loadProjectInfo as loadProjectInfo7 } from "@noir-ai/core";
3726
+ import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
3727
+ import { loadProjectInfo as loadProjectInfo9 } from "@noir-ai/core";
3263
3728
  import {
3264
3729
  buildRegistry,
3265
3730
  discoverAll,
@@ -3355,11 +3820,11 @@ async function skillsSync(opts) {
3355
3820
  const root = process.cwd();
3356
3821
  let project;
3357
3822
  try {
3358
- project = loadProjectInfo7(root);
3823
+ project = loadProjectInfo9(root);
3359
3824
  } catch {
3360
3825
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
3361
3826
  }
3362
- const adapter = resolveAdapter3(project.config.host);
3827
+ const adapter = resolveAdapter4(project.config.host);
3363
3828
  if (!adapter.skillsDir) {
3364
3829
  const data2 = {
3365
3830
  emitted: [],
@@ -3488,8 +3953,8 @@ async function skillsRegistry(opts) {
3488
3953
  }
3489
3954
 
3490
3955
  // src/init.ts
3491
- import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
3492
- import { loadProjectInfo as loadProjectInfo8 } from "@noir-ai/core";
3956
+ import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
3957
+ import { loadProjectInfo as loadProjectInfo10 } from "@noir-ai/core";
3493
3958
  import { scaffold } from "@noir-ai/create";
3494
3959
  import { emitSkillsToDir as emitSkillsToDir2 } from "@noir-ai/skills";
3495
3960
 
@@ -3595,8 +4060,8 @@ function labelFor(resolution) {
3595
4060
  }
3596
4061
 
3597
4062
  // src/dedup-write.ts
3598
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync4 } from "fs";
3599
- import { join as join4 } from "path";
4063
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync4 } from "fs";
4064
+ import { join as join5 } from "path";
3600
4065
  import { sha256Hex as sha256Hex2, sha256Hex12 } from "@noir-ai/core";
3601
4066
  var HOST_CONTEXT_RELS = /* @__PURE__ */ new Set(["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"]);
3602
4067
  var ACTION_THRESHOLD = 0.95;
@@ -3612,10 +4077,10 @@ async function checkWritePathDedup(root, res, opts) {
3612
4077
  if (embed === void 0) return result;
3613
4078
  const cachedEmbed = makeCachedEmbed(cache, embed);
3614
4079
  for (const proposedRel of writtenHostFiles) {
3615
- const proposedAbs = join4(root, proposedRel);
4080
+ const proposedAbs = join5(root, proposedRel);
3616
4081
  let proposedText;
3617
4082
  try {
3618
- proposedText = readFileSync2(proposedAbs, "utf8");
4083
+ proposedText = readFileSync4(proposedAbs, "utf8");
3619
4084
  } catch {
3620
4085
  continue;
3621
4086
  }
@@ -3659,7 +4124,7 @@ async function checkWritePathDedup(root, res, opts) {
3659
4124
  `
3660
4125
  );
3661
4126
  }
3662
- const matchedText = safeRead(join4(root, matchedRel));
4127
+ const matchedText = safeRead(join5(root, matchedRel));
3663
4128
  result.conflicts.push({
3664
4129
  path: proposedRel,
3665
4130
  mode: "artifact",
@@ -3743,21 +4208,21 @@ async function promptDedupAction(root, proposedRel, matchedRel, proposedText, si
3743
4208
  if (clack.isCancel(choice)) return "preserve";
3744
4209
  if (choice === "skip") {
3745
4210
  try {
3746
- rmSync(join4(root, proposedRel), { force: true });
4211
+ rmSync(join5(root, proposedRel), { force: true });
3747
4212
  } catch {
3748
4213
  }
3749
4214
  return "preserve";
3750
4215
  } else if (choice === "replace") {
3751
4216
  try {
3752
- writeFileSync4(join4(root, matchedRel), proposedText, "utf8");
4217
+ writeFileSync4(join5(root, matchedRel), proposedText, "utf8");
3753
4218
  } catch {
3754
4219
  }
3755
4220
  return "replace";
3756
4221
  } else if (choice === "mirror") {
3757
4222
  try {
3758
- const existing = readFileSync2(join4(root, matchedRel), "utf8");
4223
+ const existing = readFileSync4(join5(root, matchedRel), "utf8");
3759
4224
  writeFileSync4(
3760
- join4(root, matchedRel),
4225
+ join5(root, matchedRel),
3761
4226
  `${existing}
3762
4227
 
3763
4228
  <!-- mirrored from ${proposedRel} -->
@@ -3775,11 +4240,11 @@ function emptyCache() {
3775
4240
  return { version: DEDUP_CACHE_VERSION, model: "", entries: {} };
3776
4241
  }
3777
4242
  function cachePath(root) {
3778
- return join4(root, ".noir", "dedup-cache.json");
4243
+ return join5(root, ".noir", "dedup-cache.json");
3779
4244
  }
3780
4245
  function loadCache(root) {
3781
4246
  try {
3782
- const raw = readFileSync2(cachePath(root), "utf8");
4247
+ const raw = readFileSync4(cachePath(root), "utf8");
3783
4248
  const parsed = JSON.parse(raw);
3784
4249
  if (parsed.version !== DEDUP_CACHE_VERSION) return emptyCache();
3785
4250
  if (typeof parsed.entries !== "object" || parsed.entries === null) return emptyCache();
@@ -3794,7 +4259,7 @@ function loadCache(root) {
3794
4259
  }
3795
4260
  function saveCache(root, cache) {
3796
4261
  try {
3797
- mkdirSync3(join4(root, ".noir"), { recursive: true });
4262
+ mkdirSync3(join5(root, ".noir"), { recursive: true });
3798
4263
  writeFileSync4(cachePath(root), JSON.stringify(cache), "utf8");
3799
4264
  } catch {
3800
4265
  }
@@ -3811,7 +4276,7 @@ function makeCachedEmbed(cache, embed) {
3811
4276
  }
3812
4277
  function safeRead(abs) {
3813
4278
  try {
3814
- return readFileSync2(abs, "utf8");
4279
+ return readFileSync4(abs, "utf8");
3815
4280
  } catch {
3816
4281
  return void 0;
3817
4282
  }
@@ -3862,7 +4327,7 @@ async function init(root, opts) {
3862
4327
  await emitHostSkills(root, host, conflictOpts, interactive);
3863
4328
  let projectInfo;
3864
4329
  try {
3865
- projectInfo = loadProjectInfo8(root);
4330
+ projectInfo = loadProjectInfo10(root);
3866
4331
  } catch {
3867
4332
  projectInfo = void 0;
3868
4333
  }
@@ -3893,7 +4358,7 @@ function reportPlannedWrites(res) {
3893
4358
  }
3894
4359
  }
3895
4360
  async function emitHostSkills(root, host, conflictOpts, interactive) {
3896
- const adapter = resolveAdapter4(host);
4361
+ const adapter = resolveAdapter5(host);
3897
4362
  const skillsDir = adapter.skillsDir?.({ root });
3898
4363
  if (skillsDir === void 0) {
3899
4364
  process.stderr.write(`host '${host}' has no skill emitter; skipping skills
@@ -3941,11 +4406,11 @@ function assertLocalhostUrl(raw) {
3941
4406
  }
3942
4407
 
3943
4408
  // src/serve.ts
3944
- import { applyNoirEnv, loadProjectInfo as loadProjectInfo9 } from "@noir-ai/core";
4409
+ import { applyNoirEnv, loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
3945
4410
  import { ensureDaemonRunning as ensureDaemonRunning2, startStdioServer } from "@noir-ai/daemon";
3946
4411
  async function serve(opts) {
3947
4412
  applyNoirEnv(process.cwd());
3948
- const project = loadProjectInfo9(process.cwd());
4413
+ const project = loadProjectInfo11(process.cwd());
3949
4414
  if (opts.stdio) {
3950
4415
  await startStdioServer({ project, transport: "stdio", daemon: false });
3951
4416
  return;
@@ -4118,7 +4583,7 @@ function createProgram() {
4118
4583
  const force = opts.force === true;
4119
4584
  const host = parseHost(opts.host);
4120
4585
  const dryRun = opts.dryRun === true || opts.preview === true;
4121
- const { create } = await import("./create-BTYLBVV6.js");
4586
+ const { create } = await import("./create-HYFBTBLR.js");
4122
4587
  const result = await create(dir, {
4123
4588
  transport,
4124
4589
  url: opts.url,
@@ -4157,7 +4622,7 @@ function createProgram() {
4157
4622
  ).choices(SUPPORTED_HOSTS2)
4158
4623
  ).action(
4159
4624
  async (opts, cmd) => {
4160
- const { sync } = await import("./sync-LRHPJ5VV.js");
4625
+ const { sync } = await import("./sync-QDOJ7INZ.js");
4161
4626
  const host = parseHost(opts.host);
4162
4627
  const force = opts.force === true;
4163
4628
  const merge = opts.merge === true;
@@ -4183,30 +4648,75 @@ function createProgram() {
4183
4648
  throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio]");
4184
4649
  });
4185
4650
  const daemonGrp = program2.command("daemon").description("control the Noir daemon");
4186
- daemonGrp.command("start").description("start the Noir daemon (foreground, or background with --detach)").option("--detach", "run the daemon in the background and exit").addOption(
4651
+ daemonGrp.command("start").description("start the Noir daemon (foreground, or background with --detach)").option("--detach", "run the daemon in the background and exit").option("--workspace <name>", "start a shared cross-repo workspace daemon instead").option("--force", "overwrite a non-Noir .mcp.json entry (--workspace only)").addOption(
4187
4652
  new Option("--_detached-child", "reserved: detached daemon child (internal)").hideHelp()
4188
4653
  ).action(async (...args) => {
4189
4654
  const cmd = trailingCmd(args);
4190
4655
  const g = cmd.optsWithGlobals();
4191
4656
  const detach = g.detach === true;
4192
4657
  const detachChild = g._detachedChild === true;
4658
+ const workspace = typeof g.workspace === "string" ? g.workspace : void 0;
4193
4659
  await daemonStart({
4194
4660
  ...toCliOptions(g),
4195
4661
  ...detach ? { detach } : {},
4196
- ...detachChild ? { detachChild } : {}
4662
+ ...detachChild ? { detachChild } : {},
4663
+ ...workspace !== void 0 ? { workspace } : {},
4664
+ ...g.force === true ? { force: true } : {}
4197
4665
  });
4198
4666
  });
4667
+ daemonGrp.command("join").description("join a shared workspace from this repo").argument("<name>", "workspace name").option("--force", "overwrite a non-Noir .mcp.json entry").action(async (...args) => {
4668
+ const cmd = trailingCmd(args);
4669
+ const g = cmd.optsWithGlobals();
4670
+ const name = typeof args[0] === "string" ? args[0] : "";
4671
+ await daemonJoin({ ...toCliOptions(g), name, ...g.force === true ? { force: true } : {} });
4672
+ });
4199
4673
  daemonGrp.command("stop").description("stop the Noir daemon").action(async (...args) => {
4200
4674
  await daemonStop(toCliOptions(trailingCmd(args).optsWithGlobals()));
4201
4675
  });
4202
4676
  daemonGrp.command("status").description("report daemon pid/uptime/mode (exit 4 if not running)").action(async (...args) => {
4203
4677
  await daemonStatus(toCliOptions(trailingCmd(args).optsWithGlobals()));
4204
4678
  });
4205
- daemonGrp.command("restart").description("stop then start the daemon").action(async (...args) => {
4206
- await daemonRestart(toCliOptions(trailingCmd(args).optsWithGlobals()));
4679
+ daemonGrp.command("restart").description("stop then start the daemon").option("--detach", "run the daemon in the background and exit").action(async (...args) => {
4680
+ const cmd = trailingCmd(args);
4681
+ const g = cmd.optsWithGlobals();
4682
+ await daemonRestart({
4683
+ ...toCliOptions(g),
4684
+ ...g.detach === true ? { detach: true } : {}
4685
+ });
4207
4686
  });
4208
4687
  daemonGrp.action(() => {
4209
- throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|stop|status|restart");
4688
+ throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|join|stop|status|restart");
4689
+ });
4690
+ const workspaceGrp = program2.command("workspace").description("shared cross-repo workspaces");
4691
+ workspaceGrp.command("list").description("list workspaces on this machine").action(async (...args) => {
4692
+ await workspaceList(toCliOptions(trailingCmd(args).optsWithGlobals()));
4693
+ });
4694
+ workspaceGrp.command("status").description("members + daemon liveness for a workspace").argument("[name]", "workspace name (defaults to this repo's joined workspace)").action(async (...args) => {
4695
+ const cmd = trailingCmd(args);
4696
+ const name = typeof args[0] === "string" && args[0].length > 0 ? args[0] : void 0;
4697
+ await workspaceStatus({
4698
+ ...toCliOptions(cmd.optsWithGlobals()),
4699
+ ...name !== void 0 ? { name } : {}
4700
+ });
4701
+ });
4702
+ workspaceGrp.command("leave").description("leave the workspace this repo joined (restore stdio)").option("--force", "overwrite a non-Noir .mcp.json entry").action(async (...args) => {
4703
+ const cmd = trailingCmd(args);
4704
+ const g = cmd.optsWithGlobals();
4705
+ await workspaceLeave({
4706
+ ...toCliOptions(g),
4707
+ ...g.force === true ? { force: true } : {}
4708
+ });
4709
+ });
4710
+ workspaceGrp.command("stop").description("stop a workspace daemon (membership retained)").argument("[name]", "workspace name (defaults to this repo's joined workspace)").action(async (...args) => {
4711
+ const cmd = trailingCmd(args);
4712
+ const name = typeof args[0] === "string" && args[0].length > 0 ? args[0] : void 0;
4713
+ await workspaceStop({
4714
+ ...toCliOptions(cmd.optsWithGlobals()),
4715
+ ...name !== void 0 ? { name } : {}
4716
+ });
4717
+ });
4718
+ workspaceGrp.action(() => {
4719
+ throw new NoirCliError(EXIT.USAGE, "Usage: noir workspace list|status|leave|stop");
4210
4720
  });
4211
4721
  program2.command("doctor").description("environment + project health").option(
4212
4722
  "--dedup",
@@ -4302,10 +4812,23 @@ function createProgram() {
4302
4812
  ...limit === void 0 ? {} : { limit }
4303
4813
  });
4304
4814
  });
4815
+ memoryGrp.command("capture").description("distill a transcript/notes file (or stdin) into a memory (manual)").argument("[file]", "transcript/notes file (or pipe stdin)").option("--content <text>", "inline distilled content").option("--event-type <type>", "capture hook label (defaults to Stop)").action(async (...args) => {
4816
+ const cmd = trailingCmd(args);
4817
+ const g = cmd.optsWithGlobals();
4818
+ const file = typeof args[0] === "string" && args[0].length > 0 ? args[0] : void 0;
4819
+ const content = typeof g.content === "string" ? g.content : void 0;
4820
+ const eventType = typeof g.eventType === "string" ? g.eventType : void 0;
4821
+ await memoryCapture({
4822
+ ...toCliOptions(g),
4823
+ ...file === void 0 ? {} : { file },
4824
+ ...content === void 0 ? {} : { content },
4825
+ ...eventType === void 0 ? {} : { eventType }
4826
+ });
4827
+ });
4305
4828
  memoryGrp.action(() => {
4306
4829
  throw new NoirCliError(
4307
4830
  EXIT.USAGE,
4308
- "Usage: noir memory recall|save|sessions|forget|consolidate"
4831
+ "Usage: noir memory recall|save|capture|sessions|forget|consolidate"
4309
4832
  );
4310
4833
  });
4311
4834
  const skillsGrp = program2.command("skills").description("builtin skills");
@@ -4636,7 +5159,7 @@ async function probeDaemon(opts = {}) {
4636
5159
  if (opts.verbose) process.stderr.write("noir: daemon probe: no daemon record\n");
4637
5160
  return { running: false };
4638
5161
  }
4639
- if (!pidAlive3(rec.pid)) {
5162
+ if (!pidAlive4(rec.pid)) {
4640
5163
  if (opts.verbose) process.stderr.write(`noir: daemon probe: pid ${rec.pid} not alive
4641
5164
  `);
4642
5165
  return { running: false };
@@ -4654,7 +5177,7 @@ async function probeDaemon(opts = {}) {
4654
5177
  const pidOk = typeof body?.pid === "number" && body.pid === rec.pid;
4655
5178
  const expectedProject = opts.project?.id ?? (() => {
4656
5179
  try {
4657
- return loadProjectInfo10(process.cwd()).id;
5180
+ return loadProjectInfo12(process.cwd()).id;
4658
5181
  } catch {
4659
5182
  return void 0;
4660
5183
  }
@@ -4684,7 +5207,7 @@ async function probeDaemon(opts = {}) {
4684
5207
  async function resolveDaemon(opts) {
4685
5208
  let project;
4686
5209
  try {
4687
- project = opts.project ?? loadProjectInfo10(process.cwd());
5210
+ project = opts.project ?? loadProjectInfo12(process.cwd());
4688
5211
  } catch {
4689
5212
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
4690
5213
  }
@@ -4780,7 +5303,7 @@ async function callDaemonTool(opts, name, args = {}) {
4780
5303
  async function withInProcessRead(opts, fn) {
4781
5304
  let project;
4782
5305
  try {
4783
- project = opts.project ?? loadProjectInfo10(process.cwd());
5306
+ project = opts.project ?? loadProjectInfo12(process.cwd());
4784
5307
  } catch {
4785
5308
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
4786
5309
  }
@@ -4823,6 +5346,91 @@ async function withInProcessRead(opts, fn) {
4823
5346
  });
4824
5347
  }
4825
5348
  }
5349
+ function workspaceRoutingTarget(root) {
5350
+ const name = readWorkspaceMarker2(root);
5351
+ if (name === null) return null;
5352
+ try {
5353
+ return { name, projectId: loadProjectInfo12(root).id };
5354
+ } catch {
5355
+ return null;
5356
+ }
5357
+ }
5358
+ async function probeWorkspaceDaemon(routing) {
5359
+ const rec = readWorkspaceDaemonRecord2(routing.name);
5360
+ if (rec === null || !pidAlive4(rec.pid)) return { running: false };
5361
+ try {
5362
+ const res = await fetch(`http://127.0.0.1:${rec.port}/health`, {
5363
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
5364
+ });
5365
+ if (!res.ok) return { running: false };
5366
+ const body = await res.json().catch(() => null);
5367
+ if (body?.ok !== true || body.workspace !== routing.name || body.pid !== rec.pid) {
5368
+ return { running: false };
5369
+ }
5370
+ return { running: true, pid: rec.pid, port: rec.port };
5371
+ } catch {
5372
+ return { running: false };
5373
+ }
5374
+ }
5375
+ async function withWorkspaceDaemon(opts, routing, fn) {
5376
+ const probe = await probeWorkspaceDaemon(routing);
5377
+ if (!probe.running || probe.port === void 0) {
5378
+ fail(
5379
+ EXIT.DAEMON_DOWN,
5380
+ `workspace ${JSON.stringify(routing.name)} daemon is not running \u2014 start it with \`noir daemon start --workspace ${routing.name}\` (or run \`noir workspace status\`).`,
5381
+ opts
5382
+ );
5383
+ }
5384
+ const url = `http://127.0.0.1:${probe.port}/mcp?p=${routing.projectId}`;
5385
+ let client;
5386
+ try {
5387
+ client = new Client(
5388
+ { name: "noir-cli", version: NOIR_VERSION4 },
5389
+ { versionNegotiation: { mode: "auto" } }
5390
+ );
5391
+ const connected = client;
5392
+ await connectClient(connected, url, opts);
5393
+ return await fn(buildCaller(connected, opts));
5394
+ } finally {
5395
+ if (client) {
5396
+ await client.close().catch(() => {
5397
+ });
5398
+ }
5399
+ }
5400
+ }
5401
+ async function withWorkspaceMemoryRead(opts, routing, project, fn) {
5402
+ let store;
5403
+ try {
5404
+ store = await openStore({
5405
+ projectId: `ws-${routing.name}`,
5406
+ root: project.root,
5407
+ dbPath: workspaceStoreDbPath(routing.name),
5408
+ readonly: true
5409
+ });
5410
+ } catch (err) {
5411
+ fail(
5412
+ EXIT.DAEMON_DOWN,
5413
+ `workspace ${JSON.stringify(routing.name)} store is unavailable (${err instanceof Error ? err.message : String(err)}) \u2014 start its daemon with \`noir daemon start --workspace ${routing.name}\` (or run \`noir workspace status\`).`,
5414
+ opts
5415
+ );
5416
+ }
5417
+ const embedderCfg = resolveEmbedderConfig(project.config.context);
5418
+ const embed = createEmbedFn(embedderCfg).embed;
5419
+ const memory = createMemoryEngine({
5420
+ store,
5421
+ root: project.root,
5422
+ projectId: `ws-${routing.name}`,
5423
+ embed,
5424
+ storeDegraded: true,
5425
+ softForget: true
5426
+ });
5427
+ try {
5428
+ return await fn(memory);
5429
+ } finally {
5430
+ await store.close().catch(() => {
5431
+ });
5432
+ }
5433
+ }
4826
5434
 
4827
5435
  // src/commands/status.ts
4828
5436
  async function tryTool(caller, name, args) {
@@ -4945,7 +5553,7 @@ function renderHuman2(p, opts) {
4945
5553
  async function gatherStatusPayload(opts) {
4946
5554
  let project;
4947
5555
  try {
4948
- project = loadProjectInfo11(process.cwd());
5556
+ project = loadProjectInfo13(process.cwd());
4949
5557
  } catch {
4950
5558
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
4951
5559
  }
@@ -4975,7 +5583,7 @@ async function status(opts) {
4975
5583
  env: process.env,
4976
5584
  configUpdate: (() => {
4977
5585
  try {
4978
- const info2 = loadProjectInfo11(process.cwd());
5586
+ const info2 = loadProjectInfo13(process.cwd());
4979
5587
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
4980
5588
  } catch {
4981
5589
  return DEFAULT_UPDATE_CONFIG;
@@ -5007,4 +5615,4 @@ export {
5007
5615
  assertTransportUrl,
5008
5616
  buildPaletteCommandsForTui
5009
5617
  };
5010
- //# sourceMappingURL=chunk-J77RHAWG.js.map
5618
+ //# sourceMappingURL=chunk-KMLSJRYV.js.map