@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.
package/dist/bin.js CHANGED
@@ -17,11 +17,17 @@ var __export = (target, all) => {
17
17
  // src/daemon-client.ts
18
18
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
19
19
  import { ContextEngine, createEmbedFn, resolveEmbedderConfig } from "@noir-ai/context";
20
- import { loadProjectInfo, NOIR_VERSION } from "@noir-ai/core";
20
+ import {
21
+ loadProjectInfo,
22
+ NOIR_VERSION,
23
+ readWorkspaceMarker,
24
+ workspaceStoreDbPath
25
+ } from "@noir-ai/core";
21
26
  import {
22
27
  ensureDaemonRunning,
23
28
  pidAlive,
24
29
  readDaemonRecord,
30
+ readWorkspaceDaemonRecord,
25
31
  resolveGateConfig
26
32
  } from "@noir-ai/daemon";
27
33
  import { createMemoryEngine } from "@noir-ai/memory";
@@ -235,6 +241,91 @@ async function withInProcessRead(opts, fn) {
235
241
  });
236
242
  }
237
243
  }
244
+ function workspaceRoutingTarget(root) {
245
+ const name = readWorkspaceMarker(root);
246
+ if (name === null) return null;
247
+ try {
248
+ return { name, projectId: loadProjectInfo(root).id };
249
+ } catch {
250
+ return null;
251
+ }
252
+ }
253
+ async function probeWorkspaceDaemon(routing) {
254
+ const rec = readWorkspaceDaemonRecord(routing.name);
255
+ if (rec === null || !pidAlive(rec.pid)) return { running: false };
256
+ try {
257
+ const res = await fetch(`http://127.0.0.1:${rec.port}/health`, {
258
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
259
+ });
260
+ if (!res.ok) return { running: false };
261
+ const body = await res.json().catch(() => null);
262
+ if (body?.ok !== true || body.workspace !== routing.name || body.pid !== rec.pid) {
263
+ return { running: false };
264
+ }
265
+ return { running: true, pid: rec.pid, port: rec.port };
266
+ } catch {
267
+ return { running: false };
268
+ }
269
+ }
270
+ async function withWorkspaceDaemon(opts, routing, fn) {
271
+ const probe = await probeWorkspaceDaemon(routing);
272
+ if (!probe.running || probe.port === void 0) {
273
+ fail(
274
+ EXIT.DAEMON_DOWN,
275
+ `workspace ${JSON.stringify(routing.name)} daemon is not running \u2014 start it with \`noir daemon start --workspace ${routing.name}\` (or run \`noir workspace status\`).`,
276
+ opts
277
+ );
278
+ }
279
+ const url = `http://127.0.0.1:${probe.port}/mcp?p=${routing.projectId}`;
280
+ let client;
281
+ try {
282
+ client = new Client(
283
+ { name: "noir-cli", version: NOIR_VERSION },
284
+ { versionNegotiation: { mode: "auto" } }
285
+ );
286
+ const connected = client;
287
+ await connectClient(connected, url, opts);
288
+ return await fn(buildCaller(connected, opts));
289
+ } finally {
290
+ if (client) {
291
+ await client.close().catch(() => {
292
+ });
293
+ }
294
+ }
295
+ }
296
+ async function withWorkspaceMemoryRead(opts, routing, project, fn) {
297
+ let store;
298
+ try {
299
+ store = await openStore({
300
+ projectId: `ws-${routing.name}`,
301
+ root: project.root,
302
+ dbPath: workspaceStoreDbPath(routing.name),
303
+ readonly: true
304
+ });
305
+ } catch (err) {
306
+ fail(
307
+ EXIT.DAEMON_DOWN,
308
+ `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\`).`,
309
+ opts
310
+ );
311
+ }
312
+ const embedderCfg = resolveEmbedderConfig(project.config.context);
313
+ const embed = createEmbedFn(embedderCfg).embed;
314
+ const memory = createMemoryEngine({
315
+ store,
316
+ root: project.root,
317
+ projectId: `ws-${routing.name}`,
318
+ embed,
319
+ storeDegraded: true,
320
+ softForget: true
321
+ });
322
+ try {
323
+ return await fn(memory);
324
+ } finally {
325
+ await store.close().catch(() => {
326
+ });
327
+ }
328
+ }
238
329
  var DAEMON_DOWN_HINT, PROBE_TIMEOUT_MS;
239
330
  var init_daemon_client = __esm({
240
331
  "src/daemon-client.ts"() {
@@ -771,12 +862,326 @@ var init_context = __esm({
771
862
  }
772
863
  });
773
864
 
865
+ // src/workspace-mcp.ts
866
+ import { existsSync, readFileSync } from "fs";
867
+ import { join } from "path";
868
+ import { resolveAdapter } from "@noir-ai/adapters";
869
+ import { atomicWriteFile, resolveNoirCommand } from "@noir-ai/core";
870
+ function mcpPathFor(root, host) {
871
+ return resolveAdapter(host).mcpConfigPath?.({ root }) ?? join(root, ".mcp.json");
872
+ }
873
+ function readJson(path) {
874
+ if (!existsSync(path)) return null;
875
+ return JSON.parse(readFileSync(path, "utf8"));
876
+ }
877
+ function loadExisting(path, opts) {
878
+ let existing;
879
+ try {
880
+ existing = readJson(path);
881
+ } catch {
882
+ if (opts.force !== true) {
883
+ fail(
884
+ EXIT.ERROR,
885
+ `Refusing to rewrite ${path}: it is not valid JSON (add --force to overwrite).`,
886
+ opts
887
+ );
888
+ }
889
+ existing = null;
890
+ }
891
+ if (existing !== null && !isNoirEmitted(existing.mcpServers) && opts.force !== true) {
892
+ fail(
893
+ EXIT.ERROR,
894
+ `Refusing to rewrite ${path}: it does not look like a Noir-emitted config (add --force to overwrite).`,
895
+ opts
896
+ );
897
+ }
898
+ return existing;
899
+ }
900
+ function isNoirEmitted(mcpServers) {
901
+ const noir = mcpServers?.noir;
902
+ if (!noir) return false;
903
+ if (typeof noir.command === "string") return true;
904
+ return noir.type === "http";
905
+ }
906
+ function writeWorkspaceHttpEntry(root, host, url, projectId, opts) {
907
+ const path = mcpPathFor(root, host);
908
+ const existing = loadExisting(path, opts);
909
+ const next = {
910
+ ...existing ?? {},
911
+ mcpServers: {
912
+ ...existing?.mcpServers ?? {},
913
+ noir: { type: "http", url: `${url}?p=${projectId}` }
914
+ }
915
+ };
916
+ atomicWriteFile(path, `${JSON.stringify(next, null, 2)}
917
+ `);
918
+ }
919
+ function writeStdioEntry(root, host, opts) {
920
+ const path = mcpPathFor(root, host);
921
+ const existing = loadExisting(path, opts);
922
+ const noir = { command: resolveNoirCommand(), args: ["mcp", "serve", "--stdio"] };
923
+ const next = { ...existing ?? {}, mcpServers: { ...existing?.mcpServers ?? {}, noir } };
924
+ atomicWriteFile(path, `${JSON.stringify(next, null, 2)}
925
+ `);
926
+ }
927
+ var init_workspace_mcp = __esm({
928
+ "src/workspace-mcp.ts"() {
929
+ "use strict";
930
+ init_output();
931
+ }
932
+ });
933
+
934
+ // src/commands/workspace.ts
935
+ import { readdirSync } from "fs";
936
+ import {
937
+ clearWorkspaceMarker,
938
+ ensureWorkspaceRegistry,
939
+ isValidWorkspaceName,
940
+ isWorkspaceMember,
941
+ loadProjectInfo as loadProjectInfo2,
942
+ readWorkspaceMarker as readWorkspaceMarker2,
943
+ readWorkspaceRegistry,
944
+ removeWorkspaceMember,
945
+ upsertWorkspaceMember,
946
+ workspaceHomeDir,
947
+ writeWorkspaceMarker
948
+ } from "@noir-ai/core";
949
+ import {
950
+ clearWorkspaceDaemonRecord,
951
+ ensureWorkspaceDaemonRunning,
952
+ pidAlive as pidAlive2,
953
+ readWorkspaceDaemonRecord as readWorkspaceDaemonRecord2,
954
+ spawnDetachedWorkspaceDaemon
955
+ } from "@noir-ai/daemon";
956
+ function loadProjectOrFail(opts) {
957
+ try {
958
+ return loadProjectInfo2(process.cwd());
959
+ } catch {
960
+ fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
961
+ }
962
+ }
963
+ function finishJoin(project, name, url, opts) {
964
+ writeWorkspaceMarker(project.root, name);
965
+ writeWorkspaceHttpEntry(project.root, project.config.host, url, project.id, opts);
966
+ }
967
+ async function daemonStartWorkspace(opts) {
968
+ const project = loadProjectOrFail(opts);
969
+ if (!isValidWorkspaceName(opts.name)) {
970
+ fail(
971
+ EXIT.USAGE,
972
+ `invalid workspace name ${JSON.stringify(opts.name)} (lowercase letters, digits, and dashes).`,
973
+ opts
974
+ );
975
+ }
976
+ upsertWorkspaceMember(ensureWorkspaceRegistry(opts.name), {
977
+ projectId: project.id,
978
+ root: project.root,
979
+ joinedAt: Date.now()
980
+ });
981
+ if (opts.detachChild === true) {
982
+ const ensured2 = await ensureWorkspaceDaemonRunning({
983
+ name: opts.name,
984
+ project,
985
+ idleTimeoutSec: project.config.workspace.idleTimeoutSec
986
+ });
987
+ if (opts.json === true) {
988
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { mode: "detached" } })}
989
+ `);
990
+ return;
991
+ }
992
+ info("noir workspace daemon: detached foreground. Ctrl+C to stop.", opts);
993
+ log(`workspace daemon listening at ${ensured2.url}`, opts);
994
+ return;
995
+ }
996
+ if (opts.detach === true) {
997
+ const spawned = await spawnDetachedWorkspaceDaemon({ name: opts.name, project });
998
+ finishJoin(project, opts.name, `http://127.0.0.1:${spawned.port}/mcp`, opts);
999
+ if (opts.json === true) {
1000
+ process.stdout.write(
1001
+ `${JSON.stringify({ ok: true, data: { mode: "detached", pid: spawned.pid, port: spawned.port } })}
1002
+ `
1003
+ );
1004
+ return;
1005
+ }
1006
+ info(
1007
+ `workspace "${opts.name}" daemon started (pid ${spawned.pid}, port ${spawned.port}).`,
1008
+ opts
1009
+ );
1010
+ return;
1011
+ }
1012
+ const ensured = await ensureWorkspaceDaemonRunning({
1013
+ name: opts.name,
1014
+ project,
1015
+ idleTimeoutSec: project.config.workspace.idleTimeoutSec
1016
+ });
1017
+ finishJoin(project, opts.name, ensured.url, opts);
1018
+ if (opts.json === true) {
1019
+ process.stdout.write(
1020
+ `${JSON.stringify({ ok: true, data: { url: ensured.url, port: ensured.port, reused: !ensured.started } })}
1021
+ `
1022
+ );
1023
+ return;
1024
+ }
1025
+ info("noir workspace daemon: foreground mode. Ctrl+C to stop.", opts);
1026
+ log(`workspace daemon listening at ${ensured.url}`, opts);
1027
+ }
1028
+ async function daemonJoin(opts) {
1029
+ const project = loadProjectOrFail(opts);
1030
+ if (!isValidWorkspaceName(opts.name)) {
1031
+ fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(opts.name)}.`, opts);
1032
+ }
1033
+ const reg = readWorkspaceRegistry(opts.name);
1034
+ if (!reg) {
1035
+ fail(
1036
+ EXIT.ERROR,
1037
+ `workspace ${JSON.stringify(opts.name)} not found \u2014 start it from a member repo with \`noir daemon start --workspace ${opts.name}\`.`,
1038
+ opts
1039
+ );
1040
+ }
1041
+ const ensured = await ensureWorkspaceDaemonRunning({
1042
+ name: opts.name,
1043
+ project,
1044
+ idleTimeoutSec: project.config.workspace.idleTimeoutSec
1045
+ });
1046
+ upsertWorkspaceMember(reg, { projectId: project.id, root: project.root, joinedAt: Date.now() });
1047
+ finishJoin(project, opts.name, ensured.url, opts);
1048
+ if (opts.json === true) {
1049
+ process.stdout.write(
1050
+ `${JSON.stringify({ ok: true, data: { joined: true, url: ensured.url, port: ensured.port } })}
1051
+ `
1052
+ );
1053
+ return;
1054
+ }
1055
+ info(`joined workspace ${JSON.stringify(opts.name)} (${ensured.url}).`, opts);
1056
+ }
1057
+ async function workspaceList(opts) {
1058
+ let names = [];
1059
+ try {
1060
+ names = readdirSync(workspaceHomeDir(), { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
1061
+ } catch {
1062
+ names = [];
1063
+ }
1064
+ const rows = names.map((n) => {
1065
+ const reg = readWorkspaceRegistry(n);
1066
+ return {
1067
+ name: n,
1068
+ members: reg?.members.length ?? 0,
1069
+ // A record file whose pid is dead (daemon killed without cleanup) must not
1070
+ // read as "running" — mirror the pidAlive liveness `workspace status` uses.
1071
+ running: (() => {
1072
+ const rec = readWorkspaceDaemonRecord2(n);
1073
+ return rec !== null && pidAlive2(rec.pid);
1074
+ })()
1075
+ };
1076
+ });
1077
+ if (opts.json === true) {
1078
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { workspaces: rows } })}
1079
+ `);
1080
+ return;
1081
+ }
1082
+ if (rows.length === 0) {
1083
+ info("No workspaces yet.", opts);
1084
+ return;
1085
+ }
1086
+ for (const r of rows) {
1087
+ log(`${r.name} \u2014 ${r.members} member(s)${r.running ? ", running" : ""}`, opts);
1088
+ }
1089
+ }
1090
+ async function workspaceStatus(opts) {
1091
+ const name = opts.name ?? readWorkspaceMarker2(process.cwd()) ?? void 0;
1092
+ if (name === void 0) {
1093
+ fail(EXIT.USAGE, "workspace status requires a name (or run from a joined repo).", opts);
1094
+ }
1095
+ if (!isValidWorkspaceName(name)) {
1096
+ fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(name)}.`, opts);
1097
+ }
1098
+ const reg = readWorkspaceRegistry(name);
1099
+ const rec = readWorkspaceDaemonRecord2(name);
1100
+ const running = rec !== null && pidAlive2(rec.pid);
1101
+ const data = {
1102
+ name,
1103
+ members: reg?.members.map((m) => m.projectId) ?? [],
1104
+ running,
1105
+ ...rec ? { pid: rec.pid, port: rec.port } : {}
1106
+ };
1107
+ if (opts.json === true) {
1108
+ process.stdout.write(`${JSON.stringify({ ok: true, data })}
1109
+ `);
1110
+ return;
1111
+ }
1112
+ log(
1113
+ `workspace ${name}: ${running ? `running (pid ${rec?.pid}, port ${rec?.port})` : "not running"}`,
1114
+ opts
1115
+ );
1116
+ for (const m of data.members) log(` member: ${m}`, opts);
1117
+ }
1118
+ async function workspaceLeave(opts) {
1119
+ const project = loadProjectOrFail(opts);
1120
+ const name = readWorkspaceMarker2(project.root);
1121
+ if (name === null) {
1122
+ fail(EXIT.USAGE, "this repo is not joined to a workspace.", opts);
1123
+ }
1124
+ writeStdioEntry(project.root, project.config.host, opts);
1125
+ const reg = readWorkspaceRegistry(name);
1126
+ if (reg && isWorkspaceMember(reg, project.id)) {
1127
+ removeWorkspaceMember(reg, project.id);
1128
+ }
1129
+ clearWorkspaceMarker(project.root);
1130
+ if (opts.json === true) {
1131
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { left: name } })}
1132
+ `);
1133
+ return;
1134
+ }
1135
+ info(`left workspace ${JSON.stringify(name)}.`, opts);
1136
+ }
1137
+ async function workspaceStop(opts) {
1138
+ const name = opts.name ?? readWorkspaceMarker2(process.cwd()) ?? void 0;
1139
+ if (name === void 0) {
1140
+ fail(EXIT.USAGE, "workspace stop requires a name (or run from a joined repo).", opts);
1141
+ }
1142
+ if (!isValidWorkspaceName(name)) {
1143
+ fail(EXIT.USAGE, `invalid workspace name ${JSON.stringify(name)}.`, opts);
1144
+ }
1145
+ const rec = readWorkspaceDaemonRecord2(name);
1146
+ if (rec === null || !pidAlive2(rec.pid)) {
1147
+ if (opts.json === true) {
1148
+ process.stdout.write(
1149
+ `${JSON.stringify({ ok: true, data: { running: false, stopped: false } })}
1150
+ `
1151
+ );
1152
+ return;
1153
+ }
1154
+ info("workspace daemon is not running.", opts);
1155
+ return;
1156
+ }
1157
+ try {
1158
+ process.kill(rec.pid, "SIGTERM");
1159
+ } catch {
1160
+ }
1161
+ clearWorkspaceDaemonRecord(name);
1162
+ if (opts.json === true) {
1163
+ process.stdout.write(
1164
+ `${JSON.stringify({ ok: true, data: { running: false, stopped: true, pid: rec.pid } })}
1165
+ `
1166
+ );
1167
+ return;
1168
+ }
1169
+ info(`stopped workspace daemon (pid ${rec.pid}).`, opts);
1170
+ }
1171
+ var init_workspace = __esm({
1172
+ "src/commands/workspace.ts"() {
1173
+ "use strict";
1174
+ init_output();
1175
+ init_workspace_mcp();
1176
+ }
1177
+ });
1178
+
774
1179
  // src/commands/daemon.ts
775
- import { loadProjectInfo as loadProjectInfo2 } from "@noir-ai/core";
1180
+ import { loadProjectInfo as loadProjectInfo3 } from "@noir-ai/core";
776
1181
  import {
777
1182
  clearDaemonRecord,
778
1183
  ensureDaemonRunning as ensureDaemonRunning2,
779
- pidAlive as pidAlive2,
1184
+ pidAlive as pidAlive3,
780
1185
  readDaemonRecord as readDaemonRecord2,
781
1186
  spawnDetachedDaemon
782
1187
  } from "@noir-ai/daemon";
@@ -788,7 +1193,7 @@ function formatUptime(sec) {
788
1193
  }
789
1194
  function resolveCallerProjectId() {
790
1195
  try {
791
- return loadProjectInfo2(process.cwd()).id;
1196
+ return loadProjectInfo3(process.cwd()).id;
792
1197
  } catch {
793
1198
  return void 0;
794
1199
  }
@@ -809,9 +1214,18 @@ async function isHealthy(port, expectedPid, expectedProjectId) {
809
1214
  }
810
1215
  }
811
1216
  async function daemonStart(opts) {
1217
+ if (opts.workspace !== void 0) {
1218
+ await daemonStartWorkspace({
1219
+ ...opts,
1220
+ name: opts.workspace,
1221
+ ...opts.detach === true ? { detach: true } : {},
1222
+ ...opts.detachChild === true ? { detachChild: true } : {}
1223
+ });
1224
+ return;
1225
+ }
812
1226
  let project;
813
1227
  try {
814
- project = loadProjectInfo2(process.cwd());
1228
+ project = loadProjectInfo3(process.cwd());
815
1229
  } catch {
816
1230
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
817
1231
  }
@@ -842,7 +1256,7 @@ async function daemonStart(opts) {
842
1256
  }
843
1257
  if (opts.detach === true) {
844
1258
  const existing = readDaemonRecord2();
845
- if (existing && pidAlive2(existing.pid) && await isHealthy(existing.port, existing.pid, project.id)) {
1259
+ if (existing && pidAlive3(existing.pid) && await isHealthy(existing.port, existing.pid, project.id)) {
846
1260
  if (opts.json === true) {
847
1261
  process.stdout.write(
848
1262
  `${JSON.stringify({ ok: true, data: { mode: "detached", reused: true } })}
@@ -941,7 +1355,7 @@ async function daemonStop(opts) {
941
1355
  let errMsg2;
942
1356
  const callerProject = resolveCallerProjectId();
943
1357
  const wrongProject = rec.projectId !== void 0 && callerProject !== void 0 && rec.projectId !== callerProject;
944
- const healthy = !wrongProject && pidAlive2(rec.pid) && await isHealthy(rec.port, rec.pid, rec.projectId === void 0 ? void 0 : callerProject);
1358
+ const healthy = !wrongProject && pidAlive3(rec.pid) && await isHealthy(rec.port, rec.pid, rec.projectId === void 0 ? void 0 : callerProject);
945
1359
  if (healthy) {
946
1360
  try {
947
1361
  process.kill(rec.pid, "SIGTERM");
@@ -987,7 +1401,7 @@ async function daemonStatus(opts) {
987
1401
  opts
988
1402
  );
989
1403
  }
990
- if (!pidAlive2(rec.pid)) {
1404
+ if (!pidAlive3(rec.pid)) {
991
1405
  clearDaemonRecord();
992
1406
  fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
993
1407
  }
@@ -1045,27 +1459,28 @@ var init_daemon = __esm({
1045
1459
  "use strict";
1046
1460
  init_daemon_client();
1047
1461
  init_output();
1462
+ init_workspace();
1048
1463
  MODE = "foreground";
1049
1464
  }
1050
1465
  });
1051
1466
 
1052
1467
  // src/commands/doctor.ts
1053
1468
  import { spawnSync } from "child_process";
1054
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
1055
- import { dirname, join, relative } from "path";
1469
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync } from "fs";
1470
+ import { dirname, join as join2, relative } from "path";
1056
1471
  import { fileURLToPath } from "url";
1057
- import { resolveAdapter } from "@noir-ai/adapters";
1472
+ import { resolveAdapter as resolveAdapter2 } from "@noir-ai/adapters";
1058
1473
  import {
1059
1474
  detectActiveMethod,
1060
1475
  latestVersionFromCache,
1061
- loadProjectInfo as loadProjectInfo3,
1476
+ loadProjectInfo as loadProjectInfo4,
1062
1477
  NOIR_VERSION as NOIR_VERSION2,
1063
1478
  paths,
1064
1479
  readInstallRecord,
1065
1480
  readUpdateCache
1066
1481
  } from "@noir-ai/core";
1067
1482
  import { CURRENT_SCAFFOLD_VERSION, readScaffoldVersion } from "@noir-ai/create";
1068
- import { pidAlive as pidAlive3, readDaemonRecord as readDaemonRecord3 } from "@noir-ai/daemon";
1483
+ import { pidAlive as pidAlive4, readDaemonRecord as readDaemonRecord3 } from "@noir-ai/daemon";
1069
1484
  import { resolveModelConfig } from "@noir-ai/model";
1070
1485
  async function probeOnnx() {
1071
1486
  try {
@@ -1102,7 +1517,7 @@ async function checkRuntime(checks) {
1102
1517
  }
1103
1518
  function checkConfig(root) {
1104
1519
  const configPath = paths.config(root);
1105
- if (!existsSync(configPath)) {
1520
+ if (!existsSync2(configPath)) {
1106
1521
  return {
1107
1522
  project: void 0,
1108
1523
  result: {
@@ -1113,7 +1528,7 @@ function checkConfig(root) {
1113
1528
  };
1114
1529
  }
1115
1530
  try {
1116
- const project = loadProjectInfo3(root);
1531
+ const project = loadProjectInfo4(root);
1117
1532
  return {
1118
1533
  project,
1119
1534
  result: {
@@ -1139,7 +1554,7 @@ async function checkDaemon(checks) {
1139
1554
  });
1140
1555
  return;
1141
1556
  }
1142
- if (!pidAlive3(rec.pid)) {
1557
+ if (!pidAlive4(rec.pid)) {
1143
1558
  checks.push({
1144
1559
  name: "daemon",
1145
1560
  status: "warn",
@@ -1194,7 +1609,7 @@ async function checkStore(checks, project, root) {
1194
1609
  return;
1195
1610
  }
1196
1611
  const dbPath = paths.storeDb(root, project.id);
1197
- if (!existsSync(dbPath)) {
1612
+ if (!existsSync2(dbPath)) {
1198
1613
  checks.push({
1199
1614
  name: "store",
1200
1615
  status: "warn",
@@ -1223,7 +1638,7 @@ async function checkStore(checks, project, root) {
1223
1638
  }
1224
1639
  }
1225
1640
  function checkNoirEnv(checks, root) {
1226
- const path = join(paths.noirDir(root), ".env");
1641
+ const path = join2(paths.noirDir(root), ".env");
1227
1642
  let st;
1228
1643
  try {
1229
1644
  st = statSync(path);
@@ -1333,7 +1748,7 @@ function checkRulesMdBudget(checks, root, project) {
1333
1748
  return null;
1334
1749
  }
1335
1750
  const rulesPath = paths.rulesMd(root);
1336
- if (!existsSync(rulesPath)) {
1751
+ if (!existsSync2(rulesPath)) {
1337
1752
  checks.push({
1338
1753
  name: "rules budget",
1339
1754
  status: "ok",
@@ -1343,7 +1758,7 @@ function checkRulesMdBudget(checks, root, project) {
1343
1758
  }
1344
1759
  let content;
1345
1760
  try {
1346
- content = readFileSync(rulesPath, "utf8");
1761
+ content = readFileSync2(rulesPath, "utf8");
1347
1762
  } catch (e) {
1348
1763
  checks.push({
1349
1764
  name: "rules budget",
@@ -1387,7 +1802,7 @@ function checkHostArtifacts(checks, root, project) {
1387
1802
  return null;
1388
1803
  }
1389
1804
  const host = project.config.host;
1390
- const adapter = resolveAdapter(host);
1805
+ const adapter = resolveAdapter2(host);
1391
1806
  const ectx = { root };
1392
1807
  const expected = [];
1393
1808
  const emitsAgentsMd = host === "agents-md" || host === "cursor" || host === "opencode";
@@ -1397,7 +1812,7 @@ function checkHostArtifacts(checks, root, project) {
1397
1812
  if (host === "claude") expected.push("CLAUDE.md");
1398
1813
  else if (host === "gemini") expected.push("GEMINI.md");
1399
1814
  expected.push(relOr(adapter.mcpConfigPath?.(ectx) ?? ".mcp.json", root));
1400
- const missing = expected.filter((p) => !existsSync(joinRoot(root, p)));
1815
+ const missing = expected.filter((p) => !existsSync2(joinRoot(root, p)));
1401
1816
  const status2 = missing.length === 0 ? "ok" : "warn";
1402
1817
  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)`;
1403
1818
  checks.push({ name: "host artifacts", status: status2, detail });
@@ -1414,7 +1829,7 @@ function checkNestedNoir(checks, root) {
1414
1829
  ".noir/.npmignore",
1415
1830
  ".noir/.prettierignore"
1416
1831
  ];
1417
- const found = candidates.filter((rel) => existsSync(join(root, rel)));
1832
+ const found = candidates.filter((rel) => existsSync2(join2(root, rel)));
1418
1833
  const detected = found.length > 0;
1419
1834
  checks.push({
1420
1835
  name: "nested .noir",
@@ -1427,11 +1842,11 @@ function collectDedupCandidates(root) {
1427
1842
  const rels = ["CLAUDE.md", "AGENTS.md", "GEMINI.md", ".noir/rules/RULES.md"];
1428
1843
  const out = [];
1429
1844
  for (const rel of rels) {
1430
- const abs = join(root, rel);
1431
- if (!existsSync(abs)) continue;
1845
+ const abs = join2(root, rel);
1846
+ if (!existsSync2(abs)) continue;
1432
1847
  let text;
1433
1848
  try {
1434
- text = readFileSync(abs, "utf8");
1849
+ text = readFileSync2(abs, "utf8");
1435
1850
  } catch {
1436
1851
  continue;
1437
1852
  }
@@ -1488,13 +1903,13 @@ async function checkSemanticDupDoctor(checks, root, project, opts = {}) {
1488
1903
  function resolveWorkspacePackagesDir() {
1489
1904
  let here = dirname(fileURLToPath(import.meta.url));
1490
1905
  for (let i = 0; i < 8; i++) {
1491
- const pkgPath = join(here, "package.json");
1492
- if (existsSync(pkgPath)) {
1906
+ const pkgPath = join2(here, "package.json");
1907
+ if (existsSync2(pkgPath)) {
1493
1908
  try {
1494
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
1909
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
1495
1910
  if (pkg.name === "@noir-ai/cli") {
1496
1911
  const packagesDir = dirname(here);
1497
- if (existsSync(join(dirname(packagesDir), "pnpm-workspace.yaml"))) {
1912
+ if (existsSync2(join2(dirname(packagesDir), "pnpm-workspace.yaml"))) {
1498
1913
  return packagesDir;
1499
1914
  }
1500
1915
  return null;
@@ -1539,7 +1954,7 @@ function checkPublish(checks, packagesDir, opts = {}) {
1539
1954
  }
1540
1955
  let entries = [];
1541
1956
  try {
1542
- entries = readdirSync(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1957
+ entries = readdirSync2(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1543
1958
  } catch (e) {
1544
1959
  checks.push({
1545
1960
  name: "publish",
@@ -1552,11 +1967,11 @@ function checkPublish(checks, packagesDir, opts = {}) {
1552
1967
  let checked = 0;
1553
1968
  let cliDir = null;
1554
1969
  for (const name of entries) {
1555
- const pkgPath = join(packagesDir, name, "package.json");
1556
- if (!existsSync(pkgPath)) continue;
1970
+ const pkgPath = join2(packagesDir, name, "package.json");
1971
+ if (!existsSync2(pkgPath)) continue;
1557
1972
  let pkg;
1558
1973
  try {
1559
- pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
1974
+ pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
1560
1975
  } catch (e) {
1561
1976
  issues.push(`${name}/package.json: unparseable (${msg(e)})`);
1562
1977
  checked++;
@@ -1578,7 +1993,7 @@ function checkPublish(checks, packagesDir, opts = {}) {
1578
1993
  if (pkg.bin == null || typeof pkg.bin === "object" && Object.keys(pkg.bin).length === 0) {
1579
1994
  issues.push(`${name}/package.json: bin missing (cli package must declare a bin)`);
1580
1995
  }
1581
- cliDir = join(packagesDir, name);
1996
+ cliDir = join2(packagesDir, name);
1582
1997
  }
1583
1998
  }
1584
1999
  let packNote = "";
@@ -1720,13 +2135,13 @@ __export(install_exports, {
1720
2135
  installManagedNode: () => installManagedNode
1721
2136
  });
1722
2137
  import { chmodSync, mkdirSync } from "fs";
1723
- import { join as join2 } from "path";
2138
+ import { join as join3 } from "path";
1724
2139
  import {
1725
- atomicWriteFile,
2140
+ atomicWriteFile as atomicWriteFile2,
1726
2141
  detectActiveMethod as detectActiveMethod2,
1727
2142
  detectInstallMethods,
1728
2143
  ensureShimExecutable,
1729
- loadProjectInfo as loadProjectInfo4,
2144
+ loadProjectInfo as loadProjectInfo5,
1730
2145
  MANAGED_NODE_VERSION,
1731
2146
  NOIR_VERSION as NOIR_VERSION3,
1732
2147
  noirHome,
@@ -1772,8 +2187,8 @@ function buildMigrationPlan(opts) {
1772
2187
  async function installManagedNode(opts) {
1773
2188
  const spec = opts.version ?? opts.channel ?? "latest";
1774
2189
  const home2 = noirHome();
1775
- const cliDir = join2(home2, "cli");
1776
- const binDir = join2(home2, "bin");
2190
+ const cliDir = join3(home2, "cli");
2191
+ const binDir = join3(home2, "bin");
1777
2192
  mkdirSync(cliDir, { recursive: true });
1778
2193
  mkdirSync(binDir, { recursive: true });
1779
2194
  let runtime;
@@ -1799,17 +2214,17 @@ async function installManagedNode(opts) {
1799
2214
  if (code !== 0) {
1800
2215
  return { ok: false, version: null, error: `npm install failed: ${stderr.slice(0, 300)}` };
1801
2216
  }
1802
- const shim = join2(binDir, "noir");
2217
+ const shim = join3(binDir, "noir");
1803
2218
  const shimBody = `#!/usr/bin/env bash
1804
- "${nodeBin}" "${join2(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js")}" "$@"
2219
+ "${nodeBin}" "${join3(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js")}" "$@"
1805
2220
  `;
1806
- atomicWriteFile(shim, shimBody);
2221
+ atomicWriteFile2(shim, shimBody);
1807
2222
  chmodSync(shim, 493);
1808
2223
  ensureShimExecutable();
1809
2224
  const ver = await runManagerCmd(
1810
2225
  [
1811
2226
  nodeBin,
1812
- join2(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js"),
2227
+ join3(cliDir, "lib", "node_modules", "@noir-ai", "cli", "dist", "bin.js"),
1813
2228
  "--version"
1814
2229
  ],
1815
2230
  { env: opts.env, timeoutMs: 3e4 }
@@ -1865,7 +2280,7 @@ async function install(opts = {}) {
1865
2280
  const installedRecord = readInstallRecord2();
1866
2281
  let minVersion = "1.6.0";
1867
2282
  try {
1868
- minVersion = loadProjectInfo4(process.cwd()).config.update.minVersion;
2283
+ minVersion = loadProjectInfo5(process.cwd()).config.update.minVersion;
1869
2284
  } catch {
1870
2285
  }
1871
2286
  const plan = buildMigrationPlan({
@@ -1942,7 +2357,7 @@ import {
1942
2357
  detectActiveMethod as detectActiveMethod3,
1943
2358
  ensureShimExecutable as ensureShimExecutable2,
1944
2359
  fetchLatestVersion,
1945
- loadProjectInfo as loadProjectInfo5,
2360
+ loadProjectInfo as loadProjectInfo6,
1946
2361
  readInstallRecord as readInstallRecord3,
1947
2362
  readUpdateCache as readUpdateCache2,
1948
2363
  runManagerCmd as runManagerCmd2,
@@ -2022,7 +2437,7 @@ async function update(opts = {}) {
2022
2437
  fetchSpin.succeed(`Latest: ${latest}`);
2023
2438
  let minVersion = DEFAULT_UPDATE_CONFIG.minVersion;
2024
2439
  try {
2025
- minVersion = loadProjectInfo5(process.cwd()).config.update.minVersion;
2440
+ minVersion = loadProjectInfo6(process.cwd()).config.update.minVersion;
2026
2441
  } catch {
2027
2442
  }
2028
2443
  const target = buildUpdateTarget({
@@ -2172,7 +2587,7 @@ var init_update = __esm({
2172
2587
  });
2173
2588
 
2174
2589
  // src/commands/status.ts
2175
- import { loadProjectInfo as loadProjectInfo6, NOIR_VERSION as NOIR_VERSION4 } from "@noir-ai/core";
2590
+ import { loadProjectInfo as loadProjectInfo7, NOIR_VERSION as NOIR_VERSION4 } from "@noir-ai/core";
2176
2591
  async function tryTool(caller, name, args) {
2177
2592
  try {
2178
2593
  return await caller.callTool(name, args);
@@ -2293,7 +2708,7 @@ function renderHuman2(p, opts) {
2293
2708
  async function gatherStatusPayload(opts) {
2294
2709
  let project;
2295
2710
  try {
2296
- project = loadProjectInfo6(process.cwd());
2711
+ project = loadProjectInfo7(process.cwd());
2297
2712
  } catch {
2298
2713
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
2299
2714
  }
@@ -2323,7 +2738,7 @@ async function status(opts) {
2323
2738
  env: process.env,
2324
2739
  configUpdate: (() => {
2325
2740
  try {
2326
- const info2 = loadProjectInfo6(process.cwd());
2741
+ const info2 = loadProjectInfo7(process.cwd());
2327
2742
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
2328
2743
  } catch {
2329
2744
  return DEFAULT_UPDATE_CONFIG;
@@ -2352,7 +2767,7 @@ var init_status = __esm({
2352
2767
  // src/commands/task.ts
2353
2768
  import { spawnSync as spawnSync2 } from "child_process";
2354
2769
  import { writeFileSync } from "fs";
2355
- import { loadProjectInfo as loadProjectInfo7, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
2770
+ import { loadProjectInfo as loadProjectInfo8, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
2356
2771
  import {
2357
2772
  PHASES,
2358
2773
  TASK_CLASSES,
@@ -2365,7 +2780,7 @@ function skillFor(phase) {
2365
2780
  }
2366
2781
  function writeDoneArtifacts(opts, taskId) {
2367
2782
  try {
2368
- const project = loadProjectInfo7(process.cwd());
2783
+ const project = loadProjectInfo8(process.cwd());
2369
2784
  const conflict = { conflictPolicy: "preserve", interactive: false };
2370
2785
  writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
2371
2786
  const nextN = nextArtifactSequence(project.root, "adr");
@@ -2688,7 +3103,7 @@ function runCheck(cmd) {
2688
3103
  async function taskVerify(opts) {
2689
3104
  let project;
2690
3105
  try {
2691
- project = loadProjectInfo7(process.cwd());
3106
+ project = loadProjectInfo8(process.cwd());
2692
3107
  } catch {
2693
3108
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
2694
3109
  }
@@ -2894,13 +3309,13 @@ var init_task = __esm({
2894
3309
 
2895
3310
  // src/commands/handoff.ts
2896
3311
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
2897
- import { join as join3 } from "path";
3312
+ import { join as join4 } from "path";
2898
3313
  import {
2899
3314
  defaultHandoffBlock,
2900
3315
  hostLaunchDirective,
2901
- resolveAdapter as resolveAdapter2
3316
+ resolveAdapter as resolveAdapter3
2902
3317
  } from "@noir-ai/adapters";
2903
- import { loadProjectInfo as loadProjectInfo8, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
3318
+ import { loadProjectInfo as loadProjectInfo9, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
2904
3319
  function heading(name, id) {
2905
3320
  return `# Noir handoff \u2014 ${name} (${id})`;
2906
3321
  }
@@ -2996,7 +3411,7 @@ function toHostPayload(p) {
2996
3411
  }
2997
3412
  function resolveDirective(root, p) {
2998
3413
  const ctx = { root };
2999
- const adapter = resolveAdapter2(p.host);
3414
+ const adapter = resolveAdapter3(p.host);
3000
3415
  const hostPayload = toHostPayload(p);
3001
3416
  if (typeof adapter.emitHandoff === "function") {
3002
3417
  return adapter.emitHandoff(ctx, hostPayload);
@@ -3039,7 +3454,7 @@ async function probeOnly(opts) {
3039
3454
  async function handoff(opts) {
3040
3455
  let project;
3041
3456
  try {
3042
- project = loadProjectInfo8(process.cwd());
3457
+ project = loadProjectInfo9(process.cwd());
3043
3458
  } catch {
3044
3459
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
3045
3460
  }
@@ -3071,7 +3486,7 @@ async function handoff(opts) {
3071
3486
  if (opts.write === true) {
3072
3487
  const md = renderMarkdown(full);
3073
3488
  const path = handoffFilePath(project.root, task?.taskId, project.id);
3074
- mkdirSync2(join3(project.root, NOIR_DIR, "handoff"), { recursive: true });
3489
+ mkdirSync2(join4(project.root, NOIR_DIR, "handoff"), { recursive: true });
3075
3490
  writeFileSync2(path, md, "utf8");
3076
3491
  info(`handoff written to ${path}`, opts);
3077
3492
  }
@@ -3403,14 +3818,14 @@ import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
3403
3818
  import {
3404
3819
  detectActiveMethod as detectActiveMethod4,
3405
3820
  latestVersionFromCache as latestVersionFromCache2,
3406
- loadProjectInfo as loadProjectInfo9,
3821
+ loadProjectInfo as loadProjectInfo10,
3407
3822
  NOIR_VERSION as NOIR_VERSION5,
3408
3823
  readInstallRecord as readInstallRecord4,
3409
3824
  readUpdateCache as readUpdateCache3
3410
3825
  } from "@noir-ai/core";
3411
3826
  function tryProject() {
3412
3827
  try {
3413
- const info2 = loadProjectInfo9(process.cwd());
3828
+ const info2 = loadProjectInfo10(process.cwd());
3414
3829
  return { id: info2.id, name: info2.name, host: info2.config.host };
3415
3830
  } catch {
3416
3831
  return null;
@@ -3573,7 +3988,7 @@ async function runMenu(opts, deps) {
3573
3988
  env: process.env,
3574
3989
  configUpdate: (() => {
3575
3990
  try {
3576
- const info2 = loadProjectInfo9(process.cwd());
3991
+ const info2 = loadProjectInfo10(process.cwd());
3577
3992
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
3578
3993
  } catch {
3579
3994
  return DEFAULT_UPDATE_CONFIG;
@@ -3635,6 +4050,27 @@ var init_home = __esm({
3635
4050
  });
3636
4051
 
3637
4052
  // src/commands/memory.ts
4053
+ import { readFileSync as readFileSync3 } from "fs";
4054
+ import { loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
4055
+ function resolveWorkspace() {
4056
+ const routing = workspaceRoutingTarget(process.cwd());
4057
+ if (routing === null) return null;
4058
+ try {
4059
+ return { routing, project: loadProjectInfo11(process.cwd()) };
4060
+ } catch {
4061
+ return null;
4062
+ }
4063
+ }
4064
+ async function callMemory(opts, name, args = {}) {
4065
+ const ws = resolveWorkspace();
4066
+ if (ws === null) return callDaemonTool(opts, name, args);
4067
+ return withWorkspaceDaemon(opts, ws.routing, (caller) => caller.callTool(name, args));
4068
+ }
4069
+ async function withMemoryDaemon(opts, fn) {
4070
+ const ws = resolveWorkspace();
4071
+ if (ws === null) return withDaemon(opts, fn);
4072
+ return withWorkspaceDaemon(opts, ws.routing, fn);
4073
+ }
3638
4074
  function parseLimit2(raw, label, opts) {
3639
4075
  if (raw === void 0) return void 0;
3640
4076
  const n = Number(raw);
@@ -3673,6 +4109,39 @@ function stamp(ts) {
3673
4109
  }
3674
4110
  async function memoryRecall(opts) {
3675
4111
  const limit = parseLimit2(opts.limit, "memory recall", opts);
4112
+ const ws = resolveWorkspace();
4113
+ if (ws !== null) {
4114
+ const probe2 = await probeWorkspaceDaemon(ws.routing);
4115
+ if (!probe2.running) {
4116
+ await withWorkspaceMemoryRead(opts, ws.routing, ws.project, async (memory) => {
4117
+ const { hits: hits3, degraded, mode } = await memory.recallWithMeta(opts.query, {
4118
+ ...limit === void 0 ? {} : { limit }
4119
+ });
4120
+ const data3 = { query: opts.query, hits: hits3, degraded, mode };
4121
+ if (opts.json === true) {
4122
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data3 })}
4123
+ `);
4124
+ return;
4125
+ }
4126
+ renderRecall(data3.query, data3.hits, data3.degraded, opts);
4127
+ });
4128
+ return;
4129
+ }
4130
+ const res2 = await callMemory(opts, "memory_recall", {
4131
+ query: opts.query,
4132
+ ...limit === void 0 ? {} : { limit }
4133
+ });
4134
+ if (res2.ok !== true) failTool2("memory recall", res2, opts);
4135
+ const hits2 = Array.isArray(res2.results) ? res2.results.map(toHit2) : [];
4136
+ const data2 = { query: opts.query, hits: hits2, degraded: res2.degraded === true };
4137
+ if (opts.json === true) {
4138
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
4139
+ `);
4140
+ return;
4141
+ }
4142
+ renderRecall(data2.query, hits2, data2.degraded, opts);
4143
+ return;
4144
+ }
3676
4145
  let probe;
3677
4146
  try {
3678
4147
  probe = await probeDaemon(opts);
@@ -3734,7 +4203,7 @@ async function memorySave(opts) {
3734
4203
  const args = { content };
3735
4204
  if (typeof opts.type === "string" && opts.type.length > 0) args.type = opts.type;
3736
4205
  if (files !== void 0) args.files = files;
3737
- const res = await callDaemonTool(opts, "memory_save", args);
4206
+ const res = await callMemory(opts, "memory_save", args);
3738
4207
  if (res.ok !== true) failTool2("memory save", res, opts);
3739
4208
  const id = typeof res.id === "string" ? res.id : "";
3740
4209
  const observation = res.observation ?? {};
@@ -3778,7 +4247,98 @@ function renderObservation(obs, opts) {
3778
4247
  if (typeof content === "string") log(`
3779
4248
  ${content}`, opts);
3780
4249
  }
4250
+ async function memoryCapture(opts) {
4251
+ const content = await resolveCaptureContent(opts);
4252
+ const args = { content };
4253
+ if (typeof opts.eventType === "string" && opts.eventType.length > 0)
4254
+ args.eventType = opts.eventType;
4255
+ const res = await callMemory(opts, "memory_capture", args);
4256
+ if (res.ok !== true) failTool2("memory capture", res, opts);
4257
+ const id = typeof res.id === "string" ? res.id : "";
4258
+ const observation = res.observation ?? {};
4259
+ if (opts.json === true) {
4260
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { id, observation } })}
4261
+ `);
4262
+ return;
4263
+ }
4264
+ log(`Captured memory ${id}.`, opts);
4265
+ renderObservation(observation, opts);
4266
+ }
4267
+ async function resolveCaptureContent(opts) {
4268
+ if (typeof opts.content === "string" && opts.content.length > 0) return opts.content;
4269
+ if (typeof opts.file === "string" && opts.file.length > 0) {
4270
+ try {
4271
+ return readFileSync3(opts.file, "utf8");
4272
+ } catch {
4273
+ fail(EXIT.USAGE, `memory capture could not read --file ${opts.file}`, opts);
4274
+ }
4275
+ }
4276
+ if (!process.stdin.isTTY) {
4277
+ try {
4278
+ return readFileSync3(0, "utf8");
4279
+ } catch {
4280
+ fail(
4281
+ EXIT.USAGE,
4282
+ "memory capture could not read piped stdin \u2014 pass --content <text> or --file <path>.",
4283
+ opts
4284
+ );
4285
+ }
4286
+ }
4287
+ if (!isInteractive(opts)) {
4288
+ fail(
4289
+ EXIT.USAGE,
4290
+ "memory capture requires --content <text> or --file <path> (or piped stdin).",
4291
+ opts
4292
+ );
4293
+ }
4294
+ const clack = await import("@clack/prompts");
4295
+ const value = await clack.text({ message: "Captured memory content:" });
4296
+ if (clack.isCancel(value)) {
4297
+ clack.cancel("Cancelled.");
4298
+ fail(EXIT.CANCELLED, "cancelled", opts);
4299
+ }
4300
+ return String(value);
4301
+ }
3781
4302
  async function memorySessions(opts) {
4303
+ const ws = resolveWorkspace();
4304
+ if (ws !== null) {
4305
+ const probe2 = await probeWorkspaceDaemon(ws.routing);
4306
+ if (!probe2.running) {
4307
+ await withWorkspaceMemoryRead(opts, ws.routing, ws.project, async (memory) => {
4308
+ const sessions3 = memory.sessions().map((s) => ({
4309
+ id: s.id,
4310
+ count: s.count,
4311
+ lastTs: s.lastTs
4312
+ }));
4313
+ const data3 = { sessions: sessions3 };
4314
+ if (opts.json === true) {
4315
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data3 })}
4316
+ `);
4317
+ return;
4318
+ }
4319
+ renderSessions(sessions3, opts);
4320
+ });
4321
+ return;
4322
+ }
4323
+ const res2 = await callMemory(opts, "memory_sessions");
4324
+ if (res2.ok !== true) failTool2("memory sessions", res2, opts);
4325
+ const sessions2 = Array.isArray(res2.sessions) ? res2.sessions.map((raw) => {
4326
+ const s = raw ?? {};
4327
+ return {
4328
+ id: typeof s.id === "string" ? s.id : "",
4329
+ count: typeof s.count === "number" ? s.count : 0,
4330
+ lastTs: typeof s.lastTs === "number" ? s.lastTs : 0
4331
+ };
4332
+ }) : [];
4333
+ const data2 = { sessions: sessions2 };
4334
+ if (opts.json === true) {
4335
+ process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
4336
+ `);
4337
+ return;
4338
+ }
4339
+ renderSessions(sessions2, opts);
4340
+ return;
4341
+ }
3782
4342
  let probe;
3783
4343
  try {
3784
4344
  probe = await probeDaemon(opts);
@@ -3833,7 +4393,7 @@ async function memoryForget(opts) {
3833
4393
  if (opts.ids.length === 0) {
3834
4394
  fail(EXIT.USAGE, "memory forget requires at least one <id>", opts);
3835
4395
  }
3836
- const res = await callDaemonTool(opts, "memory_forget", {
4396
+ const res = await callMemory(opts, "memory_forget", {
3837
4397
  ids: opts.ids
3838
4398
  });
3839
4399
  if (res.ok !== true) failTool2("memory forget", res, opts);
@@ -3847,11 +4407,11 @@ async function memoryForget(opts) {
3847
4407
  log(`Forgot ${deleted} observation${deleted === 1 ? "" : "s"}.`, opts);
3848
4408
  }
3849
4409
  async function memoryConsolidate(opts) {
3850
- const exposed = await withDaemon(opts, async (caller) => caller.listTools());
4410
+ const exposed = await withMemoryDaemon(opts, async (caller) => caller.listTools());
3851
4411
  if (!exposed.includes("memory_consolidate")) {
3852
4412
  fail(
3853
4413
  EXIT.ERROR,
3854
- "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.",
4414
+ 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.",
3855
4415
  opts
3856
4416
  );
3857
4417
  }
@@ -3860,7 +4420,7 @@ async function memoryConsolidate(opts) {
3860
4420
  const args = {};
3861
4421
  if (types !== void 0) args.types = types;
3862
4422
  if (limit !== void 0) args.limit = limit;
3863
- const res = await callDaemonTool(
4423
+ const res = await callMemory(
3864
4424
  opts,
3865
4425
  "memory_consolidate",
3866
4426
  args
@@ -4379,10 +4939,10 @@ var init_orchestrator = __esm({
4379
4939
  });
4380
4940
 
4381
4941
  // src/run-profiles.ts
4382
- import { loadProjectInfo as loadProjectInfo10 } from "@noir-ai/core";
4942
+ import { loadProjectInfo as loadProjectInfo12 } from "@noir-ai/core";
4383
4943
  function loadRunConfig(root) {
4384
4944
  try {
4385
- return loadProjectInfo10(root).config;
4945
+ return loadProjectInfo12(root).config;
4386
4946
  } catch {
4387
4947
  return null;
4388
4948
  }
@@ -4436,7 +4996,7 @@ var init_run_profiles = __esm({
4436
4996
 
4437
4997
  // src/commands/run.ts
4438
4998
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
4439
- import { join as join4 } from "path";
4999
+ import { join as join5 } from "path";
4440
5000
  import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
4441
5001
  import { parseConfig } from "@noir-ai/core";
4442
5002
  function formatUsage(u) {
@@ -4530,8 +5090,8 @@ function mergeEnv(base, overlay) {
4530
5090
  }
4531
5091
  function writeTranscript(host, lines) {
4532
5092
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4533
- const dir = join4(process.cwd(), ".noir", "transcripts");
4534
- const file = join4(dir, `${host}-${ts}.jsonl`);
5093
+ const dir = join5(process.cwd(), ".noir", "transcripts");
5094
+ const file = join5(dir, `${host}-${ts}.jsonl`);
4535
5095
  try {
4536
5096
  mkdirSync3(dir, { recursive: true, mode: 448 });
4537
5097
  writeFileSync3(file, `${lines.join("\n")}${lines.length > 0 ? "\n" : ""}`, { mode: 384 });
@@ -4550,8 +5110,8 @@ var init_run = __esm({
4550
5110
  });
4551
5111
 
4552
5112
  // src/commands/skills.ts
4553
- import { resolveAdapter as resolveAdapter3 } from "@noir-ai/adapters";
4554
- import { loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
5113
+ import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
5114
+ import { loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
4555
5115
  import {
4556
5116
  buildRegistry,
4557
5117
  discoverAll,
@@ -4619,11 +5179,11 @@ async function skillsSync(opts) {
4619
5179
  const root = process.cwd();
4620
5180
  let project;
4621
5181
  try {
4622
- project = loadProjectInfo11(root);
5182
+ project = loadProjectInfo13(root);
4623
5183
  } catch {
4624
5184
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
4625
5185
  }
4626
- const adapter = resolveAdapter3(project.config.host);
5186
+ const adapter = resolveAdapter4(project.config.host);
4627
5187
  if (!adapter.skillsDir) {
4628
5188
  const data2 = {
4629
5189
  emitted: [],
@@ -4895,8 +5455,8 @@ var init_conflict = __esm({
4895
5455
  });
4896
5456
 
4897
5457
  // src/dedup-write.ts
4898
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync4 } from "fs";
4899
- import { join as join5 } from "path";
5458
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync4 } from "fs";
5459
+ import { join as join6 } from "path";
4900
5460
  import { sha256Hex as sha256Hex2, sha256Hex12 } from "@noir-ai/core";
4901
5461
  async function checkWritePathDedup(root, res, opts) {
4902
5462
  const result = { found: [], conflicts: [] };
@@ -4907,10 +5467,10 @@ async function checkWritePathDedup(root, res, opts) {
4907
5467
  if (embed === void 0) return result;
4908
5468
  const cachedEmbed = makeCachedEmbed(cache, embed);
4909
5469
  for (const proposedRel of writtenHostFiles) {
4910
- const proposedAbs = join5(root, proposedRel);
5470
+ const proposedAbs = join6(root, proposedRel);
4911
5471
  let proposedText;
4912
5472
  try {
4913
- proposedText = readFileSync2(proposedAbs, "utf8");
5473
+ proposedText = readFileSync4(proposedAbs, "utf8");
4914
5474
  } catch {
4915
5475
  continue;
4916
5476
  }
@@ -4954,7 +5514,7 @@ async function checkWritePathDedup(root, res, opts) {
4954
5514
  `
4955
5515
  );
4956
5516
  }
4957
- const matchedText = safeRead(join5(root, matchedRel));
5517
+ const matchedText = safeRead(join6(root, matchedRel));
4958
5518
  result.conflicts.push({
4959
5519
  path: proposedRel,
4960
5520
  mode: "artifact",
@@ -5038,21 +5598,21 @@ async function promptDedupAction(root, proposedRel, matchedRel, proposedText, si
5038
5598
  if (clack.isCancel(choice)) return "preserve";
5039
5599
  if (choice === "skip") {
5040
5600
  try {
5041
- rmSync(join5(root, proposedRel), { force: true });
5601
+ rmSync(join6(root, proposedRel), { force: true });
5042
5602
  } catch {
5043
5603
  }
5044
5604
  return "preserve";
5045
5605
  } else if (choice === "replace") {
5046
5606
  try {
5047
- writeFileSync4(join5(root, matchedRel), proposedText, "utf8");
5607
+ writeFileSync4(join6(root, matchedRel), proposedText, "utf8");
5048
5608
  } catch {
5049
5609
  }
5050
5610
  return "replace";
5051
5611
  } else if (choice === "mirror") {
5052
5612
  try {
5053
- const existing = readFileSync2(join5(root, matchedRel), "utf8");
5613
+ const existing = readFileSync4(join6(root, matchedRel), "utf8");
5054
5614
  writeFileSync4(
5055
- join5(root, matchedRel),
5615
+ join6(root, matchedRel),
5056
5616
  `${existing}
5057
5617
 
5058
5618
  <!-- mirrored from ${proposedRel} -->
@@ -5070,11 +5630,11 @@ function emptyCache() {
5070
5630
  return { version: DEDUP_CACHE_VERSION, model: "", entries: {} };
5071
5631
  }
5072
5632
  function cachePath(root) {
5073
- return join5(root, ".noir", "dedup-cache.json");
5633
+ return join6(root, ".noir", "dedup-cache.json");
5074
5634
  }
5075
5635
  function loadCache(root) {
5076
5636
  try {
5077
- const raw = readFileSync2(cachePath(root), "utf8");
5637
+ const raw = readFileSync4(cachePath(root), "utf8");
5078
5638
  const parsed = JSON.parse(raw);
5079
5639
  if (parsed.version !== DEDUP_CACHE_VERSION) return emptyCache();
5080
5640
  if (typeof parsed.entries !== "object" || parsed.entries === null) return emptyCache();
@@ -5089,7 +5649,7 @@ function loadCache(root) {
5089
5649
  }
5090
5650
  function saveCache(root, cache) {
5091
5651
  try {
5092
- mkdirSync4(join5(root, ".noir"), { recursive: true });
5652
+ mkdirSync4(join6(root, ".noir"), { recursive: true });
5093
5653
  writeFileSync4(cachePath(root), JSON.stringify(cache), "utf8");
5094
5654
  } catch {
5095
5655
  }
@@ -5106,7 +5666,7 @@ function makeCachedEmbed(cache, embed) {
5106
5666
  }
5107
5667
  function safeRead(abs) {
5108
5668
  try {
5109
- return readFileSync2(abs, "utf8");
5669
+ return readFileSync4(abs, "utf8");
5110
5670
  } catch {
5111
5671
  return void 0;
5112
5672
  }
@@ -5144,8 +5704,8 @@ var init_dedup_write = __esm({
5144
5704
  });
5145
5705
 
5146
5706
  // src/init.ts
5147
- import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
5148
- import { loadProjectInfo as loadProjectInfo12 } from "@noir-ai/core";
5707
+ import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
5708
+ import { loadProjectInfo as loadProjectInfo14 } from "@noir-ai/core";
5149
5709
  import { scaffold } from "@noir-ai/create";
5150
5710
  import { emitSkillsToDir as emitSkillsToDir2 } from "@noir-ai/skills";
5151
5711
  async function init(root, opts) {
@@ -5174,7 +5734,7 @@ async function init(root, opts) {
5174
5734
  await emitHostSkills(root, host, conflictOpts, interactive);
5175
5735
  let projectInfo;
5176
5736
  try {
5177
- projectInfo = loadProjectInfo12(root);
5737
+ projectInfo = loadProjectInfo14(root);
5178
5738
  } catch {
5179
5739
  projectInfo = void 0;
5180
5740
  }
@@ -5205,7 +5765,7 @@ function reportPlannedWrites(res) {
5205
5765
  }
5206
5766
  }
5207
5767
  async function emitHostSkills(root, host, conflictOpts, interactive) {
5208
- const adapter = resolveAdapter4(host);
5768
+ const adapter = resolveAdapter5(host);
5209
5769
  const skillsDir = adapter.skillsDir?.({ root });
5210
5770
  if (skillsDir === void 0) {
5211
5771
  process.stderr.write(`host '${host}' has no skill emitter; skipping skills
@@ -5261,11 +5821,11 @@ var init_init = __esm({
5261
5821
  });
5262
5822
 
5263
5823
  // src/serve.ts
5264
- import { applyNoirEnv, loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
5824
+ import { applyNoirEnv, loadProjectInfo as loadProjectInfo15 } from "@noir-ai/core";
5265
5825
  import { ensureDaemonRunning as ensureDaemonRunning3, startStdioServer } from "@noir-ai/daemon";
5266
5826
  async function serve(opts) {
5267
5827
  applyNoirEnv(process.cwd());
5268
- const project = loadProjectInfo13(process.cwd());
5828
+ const project = loadProjectInfo15(process.cwd());
5269
5829
  if (opts.stdio) {
5270
5830
  await startStdioServer({ project, transport: "stdio", daemon: false });
5271
5831
  return;
@@ -5299,8 +5859,8 @@ __export(create_exports, {
5299
5859
  create: () => create
5300
5860
  });
5301
5861
  import { resolve } from "path";
5302
- import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
5303
- import { loadProjectInfo as loadProjectInfo14 } from "@noir-ai/core";
5862
+ import { resolveAdapter as resolveAdapter6 } from "@noir-ai/adapters";
5863
+ import { loadProjectInfo as loadProjectInfo16 } from "@noir-ai/core";
5304
5864
  import { scaffold as scaffold2 } from "@noir-ai/create";
5305
5865
  import { emitSkillsToDir as emitSkillsToDir3 } from "@noir-ai/skills";
5306
5866
  async function create(dir, opts) {
@@ -5326,7 +5886,7 @@ async function create(dir, opts) {
5326
5886
  return res;
5327
5887
  }
5328
5888
  if (res.noop) return res;
5329
- const adapter = resolveAdapter5(host);
5889
+ const adapter = resolveAdapter6(host);
5330
5890
  const skillsDir = adapter.skillsDir?.({ root });
5331
5891
  if (skillsDir === void 0) {
5332
5892
  process.stderr.write(`host '${host}' has no skill emitter; skipping skills
@@ -5351,7 +5911,7 @@ async function create(dir, opts) {
5351
5911
  }
5352
5912
  let projectInfo;
5353
5913
  try {
5354
- projectInfo = loadProjectInfo14(root);
5914
+ projectInfo = loadProjectInfo16(root);
5355
5915
  } catch {
5356
5916
  projectInfo = void 0;
5357
5917
  }
@@ -5377,9 +5937,9 @@ var sync_exports = {};
5377
5937
  __export(sync_exports, {
5378
5938
  sync: () => sync
5379
5939
  });
5380
- import { existsSync as existsSync2 } from "fs";
5381
- import { resolveAdapter as resolveAdapter6 } from "@noir-ai/adapters";
5382
- import { loadProjectInfo as loadProjectInfo15, paths as paths2 } from "@noir-ai/core";
5940
+ import { existsSync as existsSync3 } from "fs";
5941
+ import { resolveAdapter as resolveAdapter7 } from "@noir-ai/adapters";
5942
+ import { loadProjectInfo as loadProjectInfo17, paths as paths2 } from "@noir-ai/core";
5383
5943
  import { scaffold as scaffold3 } from "@noir-ai/create";
5384
5944
  import { emitSkillsToDir as emitSkillsToDir4 } from "@noir-ai/skills";
5385
5945
  async function sync(root, opts = {}) {
@@ -5388,7 +5948,7 @@ async function sync(root, opts = {}) {
5388
5948
  const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
5389
5949
  let projectInfo;
5390
5950
  try {
5391
- projectInfo = loadProjectInfo15(root);
5951
+ projectInfo = loadProjectInfo17(root);
5392
5952
  } catch {
5393
5953
  projectInfo = void 0;
5394
5954
  }
@@ -5410,7 +5970,7 @@ async function sync(root, opts = {}) {
5410
5970
  if (res.written.length === 0 && res.identical.length > 0) {
5411
5971
  process.stderr.write("Nothing to sync (Noir-managed files are up to date).\n");
5412
5972
  }
5413
- const adapter = resolveAdapter6(host);
5973
+ const adapter = resolveAdapter7(host);
5414
5974
  const skillsDir = adapter.skillsDir?.({ root });
5415
5975
  if (skillsDir === void 0) {
5416
5976
  process.stderr.write(`host '${host}' has no skill emitter; skipping skills
@@ -5446,9 +6006,9 @@ async function sync(root, opts = {}) {
5446
6006
  }
5447
6007
  function resolveSyncHost(root, opts) {
5448
6008
  if (opts.host !== void 0) return opts.host;
5449
- if (!existsSync2(paths2.config(root))) return "claude";
6009
+ if (!existsSync3(paths2.config(root))) return "claude";
5450
6010
  try {
5451
- return loadProjectInfo15(root).config.host;
6011
+ return loadProjectInfo17(root).config.host;
5452
6012
  } catch {
5453
6013
  return "claude";
5454
6014
  }
@@ -5682,30 +6242,75 @@ function createProgram() {
5682
6242
  throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio]");
5683
6243
  });
5684
6244
  const daemonGrp = program2.command("daemon").description("control the Noir daemon");
5685
- daemonGrp.command("start").description("start the Noir daemon (foreground, or background with --detach)").option("--detach", "run the daemon in the background and exit").addOption(
6245
+ 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(
5686
6246
  new Option("--_detached-child", "reserved: detached daemon child (internal)").hideHelp()
5687
6247
  ).action(async (...args) => {
5688
6248
  const cmd = trailingCmd(args);
5689
6249
  const g = cmd.optsWithGlobals();
5690
6250
  const detach = g.detach === true;
5691
6251
  const detachChild = g._detachedChild === true;
6252
+ const workspace = typeof g.workspace === "string" ? g.workspace : void 0;
5692
6253
  await daemonStart({
5693
6254
  ...toCliOptions(g),
5694
6255
  ...detach ? { detach } : {},
5695
- ...detachChild ? { detachChild } : {}
6256
+ ...detachChild ? { detachChild } : {},
6257
+ ...workspace !== void 0 ? { workspace } : {},
6258
+ ...g.force === true ? { force: true } : {}
5696
6259
  });
5697
6260
  });
6261
+ 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) => {
6262
+ const cmd = trailingCmd(args);
6263
+ const g = cmd.optsWithGlobals();
6264
+ const name = typeof args[0] === "string" ? args[0] : "";
6265
+ await daemonJoin({ ...toCliOptions(g), name, ...g.force === true ? { force: true } : {} });
6266
+ });
5698
6267
  daemonGrp.command("stop").description("stop the Noir daemon").action(async (...args) => {
5699
6268
  await daemonStop(toCliOptions(trailingCmd(args).optsWithGlobals()));
5700
6269
  });
5701
6270
  daemonGrp.command("status").description("report daemon pid/uptime/mode (exit 4 if not running)").action(async (...args) => {
5702
6271
  await daemonStatus(toCliOptions(trailingCmd(args).optsWithGlobals()));
5703
6272
  });
5704
- daemonGrp.command("restart").description("stop then start the daemon").action(async (...args) => {
5705
- await daemonRestart(toCliOptions(trailingCmd(args).optsWithGlobals()));
6273
+ daemonGrp.command("restart").description("stop then start the daemon").option("--detach", "run the daemon in the background and exit").action(async (...args) => {
6274
+ const cmd = trailingCmd(args);
6275
+ const g = cmd.optsWithGlobals();
6276
+ await daemonRestart({
6277
+ ...toCliOptions(g),
6278
+ ...g.detach === true ? { detach: true } : {}
6279
+ });
5706
6280
  });
5707
6281
  daemonGrp.action(() => {
5708
- throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|stop|status|restart");
6282
+ throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|join|stop|status|restart");
6283
+ });
6284
+ const workspaceGrp = program2.command("workspace").description("shared cross-repo workspaces");
6285
+ workspaceGrp.command("list").description("list workspaces on this machine").action(async (...args) => {
6286
+ await workspaceList(toCliOptions(trailingCmd(args).optsWithGlobals()));
6287
+ });
6288
+ workspaceGrp.command("status").description("members + daemon liveness for a workspace").argument("[name]", "workspace name (defaults to this repo's joined workspace)").action(async (...args) => {
6289
+ const cmd = trailingCmd(args);
6290
+ const name = typeof args[0] === "string" && args[0].length > 0 ? args[0] : void 0;
6291
+ await workspaceStatus({
6292
+ ...toCliOptions(cmd.optsWithGlobals()),
6293
+ ...name !== void 0 ? { name } : {}
6294
+ });
6295
+ });
6296
+ workspaceGrp.command("leave").description("leave the workspace this repo joined (restore stdio)").option("--force", "overwrite a non-Noir .mcp.json entry").action(async (...args) => {
6297
+ const cmd = trailingCmd(args);
6298
+ const g = cmd.optsWithGlobals();
6299
+ await workspaceLeave({
6300
+ ...toCliOptions(g),
6301
+ ...g.force === true ? { force: true } : {}
6302
+ });
6303
+ });
6304
+ workspaceGrp.command("stop").description("stop a workspace daemon (membership retained)").argument("[name]", "workspace name (defaults to this repo's joined workspace)").action(async (...args) => {
6305
+ const cmd = trailingCmd(args);
6306
+ const name = typeof args[0] === "string" && args[0].length > 0 ? args[0] : void 0;
6307
+ await workspaceStop({
6308
+ ...toCliOptions(cmd.optsWithGlobals()),
6309
+ ...name !== void 0 ? { name } : {}
6310
+ });
6311
+ });
6312
+ workspaceGrp.action(() => {
6313
+ throw new NoirCliError(EXIT.USAGE, "Usage: noir workspace list|status|leave|stop");
5709
6314
  });
5710
6315
  program2.command("doctor").description("environment + project health").option(
5711
6316
  "--dedup",
@@ -5801,10 +6406,23 @@ function createProgram() {
5801
6406
  ...limit === void 0 ? {} : { limit }
5802
6407
  });
5803
6408
  });
6409
+ 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) => {
6410
+ const cmd = trailingCmd(args);
6411
+ const g = cmd.optsWithGlobals();
6412
+ const file = typeof args[0] === "string" && args[0].length > 0 ? args[0] : void 0;
6413
+ const content = typeof g.content === "string" ? g.content : void 0;
6414
+ const eventType = typeof g.eventType === "string" ? g.eventType : void 0;
6415
+ await memoryCapture({
6416
+ ...toCliOptions(g),
6417
+ ...file === void 0 ? {} : { file },
6418
+ ...content === void 0 ? {} : { content },
6419
+ ...eventType === void 0 ? {} : { eventType }
6420
+ });
6421
+ });
5804
6422
  memoryGrp.action(() => {
5805
6423
  throw new NoirCliError(
5806
6424
  EXIT.USAGE,
5807
- "Usage: noir memory recall|save|sessions|forget|consolidate"
6425
+ "Usage: noir memory recall|save|capture|sessions|forget|consolidate"
5808
6426
  );
5809
6427
  });
5810
6428
  const skillsGrp = program2.command("skills").description("builtin skills");
@@ -6097,6 +6715,7 @@ var init_bin = __esm({
6097
6715
  init_skills();
6098
6716
  init_status();
6099
6717
  init_task();
6718
+ init_workspace();
6100
6719
  init_init();
6101
6720
  init_output();
6102
6721
  init_run_profiles();