@noir-ai/cli 1.13.0-beta.1 → 1.14.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
@@ -26,9 +26,11 @@ import {
26
26
  import {
27
27
  ensureDaemonRunning,
28
28
  pidAlive,
29
- readDaemonRecord,
29
+ readDaemonToken,
30
+ readProjectDaemonRecord,
30
31
  readWorkspaceDaemonRecord,
31
- resolveGateConfig
32
+ resolveGateConfig,
33
+ tokenPath
32
34
  } from "@noir-ai/daemon";
33
35
  import { createMemoryEngine } from "@noir-ai/memory";
34
36
  import { openStore } from "@noir-ai/store";
@@ -49,7 +51,18 @@ function failDaemonDown(opts, cause) {
49
51
  fail(EXIT.DAEMON_DOWN, DAEMON_DOWN_HINT, opts);
50
52
  }
51
53
  async function probeDaemon(opts = {}) {
52
- const rec = readDaemonRecord();
54
+ const expectedProject = opts.project?.id ?? (() => {
55
+ try {
56
+ return loadProjectInfo(process.cwd()).id;
57
+ } catch {
58
+ return void 0;
59
+ }
60
+ })();
61
+ if (expectedProject === void 0) {
62
+ if (opts.verbose) process.stderr.write("noir: daemon probe: no project (uninitialized)\n");
63
+ return { running: false };
64
+ }
65
+ const rec = readProjectDaemonRecord(expectedProject);
53
66
  if (!rec) {
54
67
  if (opts.verbose) process.stderr.write("noir: daemon probe: no daemon record\n");
55
68
  return { running: false };
@@ -70,19 +83,10 @@ async function probeDaemon(opts = {}) {
70
83
  }
71
84
  const body = await res.json().catch(() => null);
72
85
  const pidOk = typeof body?.pid === "number" && body.pid === rec.pid;
73
- const expectedProject = opts.project?.id ?? (() => {
74
- try {
75
- return loadProjectInfo(process.cwd()).id;
76
- } catch {
77
- return void 0;
78
- }
79
- })();
80
- const projectOk = expectedProject === void 0 || body?.projectId !== void 0 && body.projectId === expectedProject;
81
- if (body?.ok !== true || !pidOk || !projectOk) {
86
+ if (body?.ok !== true || !pidOk) {
82
87
  if (opts.verbose)
83
88
  process.stderr.write(
84
- !pidOk ? "noir: daemon probe: pid missing/mismatched (foreign process on recorded port)\n" : !projectOk ? `noir: daemon probe: daemon serves a different project (${body?.projectId ?? "unknown"}); using in-process fallback
85
- ` : "noir: daemon probe: /health body not ok\n"
89
+ !pidOk ? "noir: daemon probe: pid missing/mismatched (foreign process on recorded port)\n" : "noir: daemon probe: /health body not ok\n"
86
90
  );
87
91
  return { running: false };
88
92
  }
@@ -107,20 +111,107 @@ async function resolveDaemon(opts) {
107
111
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
108
112
  }
109
113
  const idleTimeoutSec = opts.idleTimeoutSec ?? project.config.daemon.idleTimeoutSec;
114
+ const preferred = project.config.daemon.port;
115
+ if (preferred !== void 0 && preferred !== 0) {
116
+ const direct = `http://127.0.0.1:${preferred}/mcp`;
117
+ const probe = newClient();
118
+ let probeTimer;
119
+ try {
120
+ await Promise.race([
121
+ probe.connect(httpTransport(direct, project.id)),
122
+ new Promise((_, reject) => {
123
+ probeTimer = setTimeout(
124
+ () => reject(new Error(`no response within ${PROBE_TIMEOUT_MS}ms`)),
125
+ PROBE_TIMEOUT_MS
126
+ );
127
+ })
128
+ ]);
129
+ return { client: probe, stop: async () => {
130
+ } };
131
+ } catch (err) {
132
+ if (opts.verbose) {
133
+ process.stderr.write(
134
+ `noir: no daemon at the configured port ${preferred} (${describeCause(err)})
135
+ `
136
+ );
137
+ }
138
+ await probe.close().catch(() => {
139
+ });
140
+ } finally {
141
+ if (probeTimer !== void 0) clearTimeout(probeTimer);
142
+ }
143
+ }
144
+ let url;
145
+ let stop;
110
146
  try {
111
- const ensured = await ensureDaemonRunning({ project, idleTimeoutSec });
112
- return { url: ensured.url, stop: ensured.stop };
147
+ const ensured = await ensureDaemonRunning({
148
+ project,
149
+ idleTimeoutSec,
150
+ ...preferred !== void 0 ? { port: preferred } : {}
151
+ });
152
+ url = ensured.url;
153
+ stop = ensured.stop;
113
154
  } catch (err) {
114
155
  failDaemonDown(opts, err);
115
156
  }
157
+ const client = newClient();
158
+ try {
159
+ await connectClient(client, url, opts, project.id);
160
+ } catch (err) {
161
+ await client.close().catch(() => {
162
+ });
163
+ await stop().catch(() => {
164
+ });
165
+ throw err;
166
+ }
167
+ return { client, stop };
168
+ }
169
+ function httpTransport(url, scopeKey) {
170
+ const token = scopeKey === null ? null : readDaemonToken(scopeKey);
171
+ return new StreamableHTTPClientTransport(
172
+ new URL(url),
173
+ // Verified present: StreamableHTTPClientTransportOptions.requestInit?: RequestInit
174
+ token ? { requestInit: { headers: { Authorization: `Bearer ${token}` } } } : {}
175
+ );
176
+ }
177
+ function callerScopeKey(opts) {
178
+ try {
179
+ return (opts.project ?? loadProjectInfo(process.cwd())).id;
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+ function newClient() {
185
+ return new Client(
186
+ { name: "noir-cli", version: NOIR_VERSION },
187
+ { versionNegotiation: { mode: "auto" } }
188
+ );
189
+ }
190
+ async function connectRaw(client, url, scopeKey) {
191
+ await client.connect(httpTransport(url, scopeKey));
116
192
  }
117
- async function connectClient(client, url, opts) {
193
+ async function connectClient(client, url, opts, scopeKey) {
118
194
  try {
119
- await client.connect(new StreamableHTTPClientTransport(new URL(url)));
195
+ await connectRaw(client, url, scopeKey);
120
196
  } catch (err) {
121
197
  failDaemonDown(opts, err);
122
198
  }
123
199
  }
200
+ function failWorkspaceConnect(opts, routing, cause) {
201
+ const text = String(cause).toLowerCase();
202
+ if (text.includes("401") || text.includes("unauthorized")) {
203
+ if (opts.verbose) {
204
+ process.stderr.write(`noir: daemon transport detail: ${describeCause(cause)}
205
+ `);
206
+ }
207
+ fail(
208
+ EXIT.DAEMON_DOWN,
209
+ `workspace ${JSON.stringify(routing.name)} daemon is running but rejected this client's token (HTTP 401) \u2014 its token file is ${tokenPath(routing.name)}; make sure it is readable, or restart the workspace daemon with \`noir daemon start --workspace ${routing.name}\` to mint a fresh one.`,
210
+ opts
211
+ );
212
+ }
213
+ failDaemonDown(opts, cause);
214
+ }
124
215
  async function callToolParse(client, opts, name, args) {
125
216
  const result = await client.callTool({ name, arguments: args }).catch((err) => failDaemonDown(opts, err));
126
217
  const block = result.content?.[0];
@@ -151,21 +242,12 @@ function buildCaller(client, opts) {
151
242
  };
152
243
  }
153
244
  async function withDaemon(opts, fn) {
154
- const { url, stop } = await resolveDaemon(opts);
155
- let client;
245
+ const { client, stop } = await resolveDaemon(opts);
156
246
  try {
157
- client = new Client(
158
- { name: "noir-cli", version: NOIR_VERSION },
159
- { versionNegotiation: { mode: "auto" } }
160
- );
161
- const connected = client;
162
- await connectClient(connected, url, opts);
163
- return await fn(buildCaller(connected, opts));
247
+ return await fn(buildCaller(client, opts));
164
248
  } finally {
165
- if (client) {
166
- await client.close().catch(() => {
167
- });
168
- }
249
+ await client.close().catch(() => {
250
+ });
169
251
  await stop().catch(() => {
170
252
  });
171
253
  }
@@ -176,12 +258,9 @@ async function withRunningDaemon(opts, fn, probe) {
176
258
  const url = `http://127.0.0.1:${p.port}/mcp`;
177
259
  let client;
178
260
  try {
179
- client = new Client(
180
- { name: "noir-cli", version: NOIR_VERSION },
181
- { versionNegotiation: { mode: "auto" } }
182
- );
261
+ client = newClient();
183
262
  const connected = client;
184
- await connected.connect(new StreamableHTTPClientTransport(new URL(url)));
263
+ await connected.connect(httpTransport(url, callerScopeKey(opts)));
185
264
  return await fn(buildCaller(connected, opts));
186
265
  } catch {
187
266
  return null;
@@ -279,12 +358,11 @@ async function withWorkspaceDaemon(opts, routing, fn) {
279
358
  const url = `http://127.0.0.1:${probe.port}/mcp?p=${routing.projectId}`;
280
359
  let client;
281
360
  try {
282
- client = new Client(
283
- { name: "noir-cli", version: NOIR_VERSION },
284
- { versionNegotiation: { mode: "auto" } }
285
- );
361
+ client = newClient();
286
362
  const connected = client;
287
- await connectClient(connected, url, opts);
363
+ await connectRaw(connected, url, routing.name).catch(
364
+ (err) => failWorkspaceConnect(opts, routing, err)
365
+ );
288
366
  return await fn(buildCaller(connected, opts));
289
367
  } finally {
290
368
  if (client) {
@@ -1179,10 +1257,11 @@ var init_workspace = __esm({
1179
1257
  // src/commands/daemon.ts
1180
1258
  import { loadProjectInfo as loadProjectInfo3 } from "@noir-ai/core";
1181
1259
  import {
1182
- clearDaemonRecord,
1260
+ clearProjectDaemonRecord,
1183
1261
  ensureDaemonRunning as ensureDaemonRunning2,
1184
1262
  pidAlive as pidAlive3,
1185
- readDaemonRecord as readDaemonRecord2,
1263
+ readDaemonToken as readDaemonToken2,
1264
+ readProjectDaemonRecord as readProjectDaemonRecord2,
1186
1265
  spawnDetachedDaemon
1187
1266
  } from "@noir-ai/daemon";
1188
1267
  function formatUptime(sec) {
@@ -1232,7 +1311,8 @@ async function daemonStart(opts) {
1232
1311
  if (opts.detachChild === true) {
1233
1312
  const child = await ensureDaemonRunning2({
1234
1313
  project,
1235
- idleTimeoutSec: project.config.daemon.idleTimeoutSec
1314
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec,
1315
+ ...project.config.daemon.port !== void 0 ? { port: project.config.daemon.port } : {}
1236
1316
  });
1237
1317
  if (child.started) {
1238
1318
  if (opts.json === true) {
@@ -1255,7 +1335,7 @@ async function daemonStart(opts) {
1255
1335
  return;
1256
1336
  }
1257
1337
  if (opts.detach === true) {
1258
- const existing = readDaemonRecord2();
1338
+ const existing = readProjectDaemonRecord2(project.id);
1259
1339
  if (existing && pidAlive3(existing.pid) && await isHealthy(existing.port, existing.pid, project.id)) {
1260
1340
  if (opts.json === true) {
1261
1341
  process.stdout.write(
@@ -1295,7 +1375,8 @@ async function daemonStart(opts) {
1295
1375
  try {
1296
1376
  ensured = await ensureDaemonRunning2({
1297
1377
  project,
1298
- idleTimeoutSec: project.config.daemon.idleTimeoutSec
1378
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec,
1379
+ ...project.config.daemon.port !== void 0 ? { port: project.config.daemon.port } : {}
1299
1380
  });
1300
1381
  } catch (err) {
1301
1382
  ds.fail("Daemon failed to start");
@@ -1338,7 +1419,19 @@ async function daemonStart(opts) {
1338
1419
  log(`Noir daemon already running at ${ensured.url}`, opts);
1339
1420
  }
1340
1421
  async function daemonStop(opts) {
1341
- const rec = readDaemonRecord2();
1422
+ const callerProject = resolveCallerProjectId();
1423
+ if (callerProject === void 0) {
1424
+ if (opts.json === true) {
1425
+ process.stdout.write(
1426
+ `${JSON.stringify({ ok: true, data: { running: false, stopped: false } })}
1427
+ `
1428
+ );
1429
+ return;
1430
+ }
1431
+ info("No Noir daemon is running for this directory (not a Noir project).", opts);
1432
+ return;
1433
+ }
1434
+ const rec = readProjectDaemonRecord2(callerProject);
1342
1435
  if (!rec) {
1343
1436
  if (opts.json === true) {
1344
1437
  process.stdout.write(
@@ -1353,9 +1446,7 @@ async function daemonStop(opts) {
1353
1446
  const ds = spinner(`Stopping daemon (pid ${rec.pid})...`, opts).start();
1354
1447
  let signalled = false;
1355
1448
  let errMsg2;
1356
- const callerProject = resolveCallerProjectId();
1357
- const wrongProject = rec.projectId !== void 0 && callerProject !== void 0 && rec.projectId !== callerProject;
1358
- const healthy = !wrongProject && pidAlive3(rec.pid) && await isHealthy(rec.port, rec.pid, rec.projectId === void 0 ? void 0 : callerProject);
1449
+ const healthy = pidAlive3(rec.pid) && await isHealthy(rec.port, rec.pid, callerProject);
1359
1450
  if (healthy) {
1360
1451
  try {
1361
1452
  process.kill(rec.pid, "SIGTERM");
@@ -1364,7 +1455,7 @@ async function daemonStop(opts) {
1364
1455
  errMsg2 = err instanceof Error ? err.message : String(err);
1365
1456
  }
1366
1457
  }
1367
- if (!wrongProject) clearDaemonRecord();
1458
+ clearProjectDaemonRecord(callerProject);
1368
1459
  if (opts.json === true) {
1369
1460
  process.stdout.write(
1370
1461
  `${JSON.stringify({
@@ -1383,26 +1474,22 @@ async function daemonStop(opts) {
1383
1474
  if (signalled) {
1384
1475
  ds.succeed(`Stopped Noir daemon (pid ${rec.pid})`);
1385
1476
  } else {
1386
- const reason = wrongProject ? "the daemon belongs to another project (left running)" : errMsg2 !== void 0 ? errMsg2 : "the recorded pid did not answer as our daemon (stale record cleared)";
1477
+ const reason = errMsg2 !== void 0 ? errMsg2 : "the recorded pid did not answer as our daemon (stale record cleared)";
1387
1478
  ds.warn(`Daemon (pid ${rec.pid}) could not be signalled: ${reason}`);
1388
1479
  info(`Noir daemon (pid ${rec.pid}) could not be signalled: ${reason}`, opts);
1389
1480
  }
1390
1481
  }
1391
1482
  async function daemonStatus(opts) {
1392
- const rec = readDaemonRecord2();
1483
+ const callerProject = resolveCallerProjectId();
1484
+ if (callerProject === void 0) {
1485
+ fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (not a Noir project).", opts);
1486
+ }
1487
+ const rec = readProjectDaemonRecord2(callerProject);
1393
1488
  if (!rec) {
1394
1489
  fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (start with `noir daemon start`).", opts);
1395
1490
  }
1396
- const callerProject = resolveCallerProjectId();
1397
- if (rec.projectId !== void 0 && callerProject !== void 0 && rec.projectId !== callerProject) {
1398
- fail(
1399
- EXIT.DAEMON_DOWN,
1400
- "Noir daemon is not running for this project (a daemon for another project is running).",
1401
- opts
1402
- );
1403
- }
1404
1491
  if (!pidAlive3(rec.pid)) {
1405
- clearDaemonRecord();
1492
+ clearProjectDaemonRecord(callerProject);
1406
1493
  fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
1407
1494
  }
1408
1495
  const hs = spinner("Probing daemon health...", opts).start();
@@ -1420,7 +1507,7 @@ async function daemonStatus(opts) {
1420
1507
  const pidOk = typeof health?.pid === "number" && health.pid === rec.pid;
1421
1508
  if (health?.ok !== true || !pidOk) {
1422
1509
  hs.fail("Daemon not responding");
1423
- clearDaemonRecord();
1510
+ clearProjectDaemonRecord(callerProject);
1424
1511
  fail(
1425
1512
  EXIT.DAEMON_DOWN,
1426
1513
  health !== null && !pidOk ? "Noir daemon is not running (pid missing/mismatched \u2014 a foreign process holds the recorded port; stale record removed)." : "Noir daemon is not running (port unresponsive; stale record removed).",
@@ -1436,7 +1523,8 @@ async function daemonStatus(opts) {
1436
1523
  startedAt: rec.startedAt,
1437
1524
  uptimeSec,
1438
1525
  // Honest ownership: the detached child writes `mode:'detached'`; anything
1439
- // else (legacy record, foreground start) reports 'foreground'.
1526
+ // else (a record written before the field existed, foreground start)
1527
+ // reports 'foreground'.
1440
1528
  mode: rec.mode ?? MODE
1441
1529
  };
1442
1530
  if (opts.json === true) {
@@ -1449,6 +1537,42 @@ async function daemonStatus(opts) {
1449
1537
  opts
1450
1538
  );
1451
1539
  }
1540
+ async function daemonToken(opts) {
1541
+ const callerProject = resolveCallerProjectId();
1542
+ if (callerProject === void 0) {
1543
+ fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (not a Noir project).", opts);
1544
+ }
1545
+ const rec = readProjectDaemonRecord2(callerProject);
1546
+ if (!rec) {
1547
+ fail(
1548
+ EXIT.DAEMON_DOWN,
1549
+ "Noir daemon is not running (no daemon record \u2014 start it with `noir daemon start`).",
1550
+ opts
1551
+ );
1552
+ }
1553
+ if (!pidAlive3(rec.pid) || !await isHealthy(rec.port, rec.pid, callerProject)) {
1554
+ fail(
1555
+ EXIT.DAEMON_DOWN,
1556
+ "Noir daemon is not running (stale record \u2014 start it with `noir daemon start`).",
1557
+ opts
1558
+ );
1559
+ }
1560
+ const token = readDaemonToken2(callerProject);
1561
+ if (token === null) {
1562
+ fail(
1563
+ EXIT.DAEMON_DOWN,
1564
+ "Noir daemon is not running (the record has no token beside it \u2014 restart it with `noir daemon start`).",
1565
+ opts
1566
+ );
1567
+ }
1568
+ if (opts.json === true) {
1569
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { token } })}
1570
+ `);
1571
+ return;
1572
+ }
1573
+ process.stdout.write(`${token}
1574
+ `);
1575
+ }
1452
1576
  async function daemonRestart(opts) {
1453
1577
  await daemonStop({ ...opts, json: false, quiet: true });
1454
1578
  await daemonStart(opts);
@@ -1473,6 +1597,7 @@ import { resolveAdapter as resolveAdapter2 } from "@noir-ai/adapters";
1473
1597
  import {
1474
1598
  detectActiveMethod,
1475
1599
  latestVersionFromCache,
1600
+ loadNoirEnv,
1476
1601
  loadProjectInfo as loadProjectInfo4,
1477
1602
  NOIR_VERSION as NOIR_VERSION2,
1478
1603
  paths,
@@ -1480,7 +1605,7 @@ import {
1480
1605
  readUpdateCache
1481
1606
  } from "@noir-ai/core";
1482
1607
  import { CURRENT_SCAFFOLD_VERSION, readScaffoldVersion } from "@noir-ai/create";
1483
- import { pidAlive as pidAlive4, readDaemonRecord as readDaemonRecord3 } from "@noir-ai/daemon";
1608
+ import { pidAlive as pidAlive4, readProjectDaemonRecord as readProjectDaemonRecord3 } from "@noir-ai/daemon";
1484
1609
  import { resolveModelConfig } from "@noir-ai/model";
1485
1610
  async function probeOnnx() {
1486
1611
  try {
@@ -1544,8 +1669,16 @@ function checkConfig(root) {
1544
1669
  };
1545
1670
  }
1546
1671
  }
1547
- async function checkDaemon(checks) {
1548
- const rec = readDaemonRecord3();
1672
+ async function checkDaemon(checks, project) {
1673
+ if (project === void 0) {
1674
+ checks.push({
1675
+ name: "daemon",
1676
+ status: "warn",
1677
+ detail: "skipped \u2014 project not initialized"
1678
+ });
1679
+ return;
1680
+ }
1681
+ const rec = readProjectDaemonRecord3(project.id);
1549
1682
  if (!rec) {
1550
1683
  checks.push({
1551
1684
  name: "daemon",
@@ -1637,7 +1770,7 @@ async function checkStore(checks, project, root) {
1637
1770
  }
1638
1771
  }
1639
1772
  }
1640
- function checkNoirEnv(checks, root) {
1773
+ function checkNoirEnv(checks, root, loaded) {
1641
1774
  const path = join2(paths.noirDir(root), ".env");
1642
1775
  let st;
1643
1776
  try {
@@ -1645,6 +1778,14 @@ function checkNoirEnv(checks, root) {
1645
1778
  } catch {
1646
1779
  return;
1647
1780
  }
1781
+ if (loaded.warnings.some((w) => w.startsWith(ENV_REFUSAL_PREFIX))) {
1782
+ checks.push({
1783
+ name: "noir-env",
1784
+ status: "warn",
1785
+ detail: `${path} is tracked by git \u2014 refused, so NONE of its keys are in effect. Fix: add \`.noir/.env\` to .gitignore and run \`git rm --cached .noir/.env\`.`
1786
+ });
1787
+ return;
1788
+ }
1648
1789
  const mode = st.mode & 511;
1649
1790
  const tooOpen = (st.mode & 63) !== 0;
1650
1791
  checks.push(
@@ -1654,6 +1795,10 @@ function checkNoirEnv(checks, root) {
1654
1795
  detail: `${path} is ${mode.toString(8)} \u2014 readable by others; run chmod 600`
1655
1796
  } : { name: "noir-env", status: "ok", detail: `${path} permissions ok (${mode.toString(8)})` }
1656
1797
  );
1798
+ for (const [key, source] of Object.entries(loaded.sources)) {
1799
+ if (source !== "file") continue;
1800
+ checks.push({ name: `noir-env:${key}`, status: "ok", detail: "from .noir/.env" });
1801
+ }
1657
1802
  }
1658
1803
  function checkEmbedder(checks, project, vecOk) {
1659
1804
  if (!project) {
@@ -1685,7 +1830,7 @@ function checkEmbedder(checks, project, vecOk) {
1685
1830
  detail: `${kind}/${model} (${emb.dim}-dim) \xB7 vec backend available`
1686
1831
  });
1687
1832
  }
1688
- function checkProvider(checks, project) {
1833
+ function checkProvider(checks, project, sources) {
1689
1834
  if (!project) {
1690
1835
  checks.push({ name: "provider", status: "warn", detail: "skipped \u2014 not initialized" });
1691
1836
  return;
@@ -1705,7 +1850,8 @@ function checkProvider(checks, project) {
1705
1850
  for (const name of names) {
1706
1851
  const p = resolved.providers[name];
1707
1852
  if (!p) continue;
1708
- const key = p.hasKey ? "key present" : p.apiKeyEnv ? `missing ${p.apiKeyEnv}` : "anonymous";
1853
+ const fromFile = p.apiKeyEnv !== void 0 && sources[p.apiKeyEnv] === "file";
1854
+ const key = p.hasKey ? fromFile ? "key present (from .noir/.env)" : "key present" : p.apiKeyEnv ? `missing ${p.apiKeyEnv}` : "anonymous";
1709
1855
  if (!p.hasKey && p.apiKeyEnv) missing = true;
1710
1856
  parts.push(`${name}/${p.model ?? "?"} (${key})`);
1711
1857
  }
@@ -2068,12 +2214,13 @@ async function doctor(opts = {}) {
2068
2214
  await checkRuntime(checks);
2069
2215
  const { project, result: configResult } = checkConfig(root);
2070
2216
  checks.push(configResult);
2071
- await checkDaemon(checks);
2217
+ await checkDaemon(checks, project);
2072
2218
  const { vecOk } = await checkNativeDeps(checks);
2073
2219
  await checkStore(checks, project, root);
2074
2220
  checkEmbedder(checks, project, vecOk);
2075
- checkProvider(checks, project);
2076
- checkNoirEnv(checks, root);
2221
+ const noirEnv = loadNoirEnv(root);
2222
+ checkProvider(checks, project, noirEnv.sources);
2223
+ checkNoirEnv(checks, root, noirEnv);
2077
2224
  const scaffold4 = checkScaffoldVersion(checks, root);
2078
2225
  const rules = checkRulesMdBudget(checks, root, project);
2079
2226
  const host = checkHostArtifacts(checks, root, project);
@@ -2116,17 +2263,112 @@ async function doctor(opts = {}) {
2116
2263
  );
2117
2264
  }
2118
2265
  }
2119
- var SEMVER_ISH;
2266
+ var ENV_REFUSAL_PREFIX, SEMVER_ISH;
2120
2267
  var init_doctor = __esm({
2121
2268
  "src/commands/doctor.ts"() {
2122
2269
  "use strict";
2123
2270
  init_daemon_client();
2124
2271
  init_output();
2125
2272
  init_theme();
2273
+ ENV_REFUSAL_PREFIX = ".noir/.env: refusing to load";
2126
2274
  SEMVER_ISH = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
2127
2275
  }
2128
2276
  });
2129
2277
 
2278
+ // src/commands/env.ts
2279
+ import { loadNoirEnv as loadNoirEnv2, loadProjectInfo as loadProjectInfo5 } from "@noir-ai/core";
2280
+ import { resolveModelConfig as resolveModelConfig2 } from "@noir-ai/model";
2281
+ function captureAmbientEnv(env2 = process.env) {
2282
+ ambientBaseline = { ...env2 };
2283
+ }
2284
+ function redactShape(value) {
2285
+ if (value.length <= SHORT_VALUE_MAX) return `\u2026(${value.length})`;
2286
+ return `${value.slice(0, 3)}\u2026(${value.length})`;
2287
+ }
2288
+ function configApiKeyEnvNames(root) {
2289
+ try {
2290
+ const project = loadProjectInfo5(root);
2291
+ const names = /* @__PURE__ */ new Set();
2292
+ for (const provider of Object.values(resolveModelConfig2(project.config.model).providers)) {
2293
+ if (provider.apiKeyEnv !== void 0 && provider.apiKeyEnv.length > 0) {
2294
+ names.add(provider.apiKeyEnv);
2295
+ }
2296
+ }
2297
+ return [...names];
2298
+ } catch {
2299
+ return [];
2300
+ }
2301
+ }
2302
+ function resolveEnv(root, ambientEnv) {
2303
+ const { overlay, warnings, sources } = loadNoirEnv2(root, ambientEnv);
2304
+ const curated = new Set(CURATED_AMBIENT_KEYS);
2305
+ for (const name of configApiKeyEnvNames(root)) curated.add(name);
2306
+ const candidates = new Set(Object.keys(sources));
2307
+ for (const key of curated) {
2308
+ if (ambientEnv[key] !== void 0) candidates.add(key);
2309
+ }
2310
+ const vars = [];
2311
+ const shapes = {};
2312
+ for (const key of [...candidates].sort()) {
2313
+ const source = sources[key] ?? "env";
2314
+ if (source === "env" && !curated.has(key)) continue;
2315
+ const value = source === "file" ? overlay[key] ?? "" : ambientEnv[key] ?? "";
2316
+ const shadowed = source === "file" && ambientEnv[key] !== void 0 && ambientEnv[key] !== value;
2317
+ vars.push({
2318
+ key,
2319
+ source,
2320
+ ...shadowed ? { shadowed: true } : {},
2321
+ valueLength: value.length
2322
+ });
2323
+ shapes[key] = redactShape(value);
2324
+ }
2325
+ return { vars, warnings, shapes };
2326
+ }
2327
+ function describeSource(row) {
2328
+ if (row.source !== "file") return "environment";
2329
+ return row.shadowed === true ? ".noir/.env (shadows environment)" : ".noir/.env";
2330
+ }
2331
+ async function env(opts = {}) {
2332
+ const report = resolveEnv(process.cwd(), ambientBaseline ?? process.env);
2333
+ if (opts.json === true) {
2334
+ json({ ok: true, data: { vars: report.vars, warnings: report.warnings } });
2335
+ return;
2336
+ }
2337
+ if (report.vars.length === 0) {
2338
+ info(
2339
+ "nothing to report \u2014 .noir/.env defines no keys in effect and no Noir-relevant environment variable is set",
2340
+ opts
2341
+ );
2342
+ return;
2343
+ }
2344
+ table(
2345
+ report.vars.map((row) => ({
2346
+ KEY: row.key,
2347
+ SOURCE: describeSource(row),
2348
+ VALUE: report.shapes[row.key] ?? ""
2349
+ })),
2350
+ ["KEY", "SOURCE", "VALUE"],
2351
+ opts
2352
+ );
2353
+ }
2354
+ var CURATED_AMBIENT_KEYS, ambientBaseline, SHORT_VALUE_MAX;
2355
+ var init_env = __esm({
2356
+ "src/commands/env.ts"() {
2357
+ "use strict";
2358
+ init_output();
2359
+ CURATED_AMBIENT_KEYS = [
2360
+ "CLICKUP_API_TOKEN",
2361
+ "OPENAI_API_KEY",
2362
+ "VOYAGE_API_KEY",
2363
+ "COHERE_API_KEY",
2364
+ "OLLAMA_BASE_URL",
2365
+ "ANTHROPIC_API_KEY",
2366
+ "NOIR_PROFILE"
2367
+ ];
2368
+ SHORT_VALUE_MAX = 8;
2369
+ }
2370
+ });
2371
+
2130
2372
  // src/commands/install.ts
2131
2373
  var install_exports = {};
2132
2374
  __export(install_exports, {
@@ -2141,7 +2383,7 @@ import {
2141
2383
  detectActiveMethod as detectActiveMethod2,
2142
2384
  detectInstallMethods,
2143
2385
  ensureShimExecutable,
2144
- loadProjectInfo as loadProjectInfo5,
2386
+ loadProjectInfo as loadProjectInfo6,
2145
2387
  MANAGED_NODE_VERSION,
2146
2388
  NOIR_VERSION as NOIR_VERSION3,
2147
2389
  noirHome,
@@ -2280,7 +2522,7 @@ async function install(opts = {}) {
2280
2522
  const installedRecord = readInstallRecord2();
2281
2523
  let minVersion = "1.6.0";
2282
2524
  try {
2283
- minVersion = loadProjectInfo5(process.cwd()).config.update.minVersion;
2525
+ minVersion = loadProjectInfo6(process.cwd()).config.update.minVersion;
2284
2526
  } catch {
2285
2527
  }
2286
2528
  const plan = buildMigrationPlan({
@@ -2357,7 +2599,7 @@ import {
2357
2599
  detectActiveMethod as detectActiveMethod3,
2358
2600
  ensureShimExecutable as ensureShimExecutable2,
2359
2601
  fetchLatestVersion,
2360
- loadProjectInfo as loadProjectInfo6,
2602
+ loadProjectInfo as loadProjectInfo7,
2361
2603
  readInstallRecord as readInstallRecord3,
2362
2604
  readUpdateCache as readUpdateCache2,
2363
2605
  runManagerCmd as runManagerCmd2,
@@ -2437,7 +2679,7 @@ async function update(opts = {}) {
2437
2679
  fetchSpin.succeed(`Latest: ${latest}`);
2438
2680
  let minVersion = DEFAULT_UPDATE_CONFIG.minVersion;
2439
2681
  try {
2440
- minVersion = loadProjectInfo6(process.cwd()).config.update.minVersion;
2682
+ minVersion = loadProjectInfo7(process.cwd()).config.update.minVersion;
2441
2683
  } catch {
2442
2684
  }
2443
2685
  const target = buildUpdateTarget({
@@ -2587,7 +2829,7 @@ var init_update = __esm({
2587
2829
  });
2588
2830
 
2589
2831
  // src/commands/status.ts
2590
- import { loadProjectInfo as loadProjectInfo7, NOIR_VERSION as NOIR_VERSION4 } from "@noir-ai/core";
2832
+ import { loadProjectInfo as loadProjectInfo8, NOIR_VERSION as NOIR_VERSION4 } from "@noir-ai/core";
2591
2833
  async function tryTool(caller, name, args) {
2592
2834
  try {
2593
2835
  return await caller.callTool(name, args);
@@ -2708,7 +2950,7 @@ function renderHuman2(p, opts) {
2708
2950
  async function gatherStatusPayload(opts) {
2709
2951
  let project;
2710
2952
  try {
2711
- project = loadProjectInfo7(process.cwd());
2953
+ project = loadProjectInfo8(process.cwd());
2712
2954
  } catch {
2713
2955
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
2714
2956
  }
@@ -2738,7 +2980,7 @@ async function status(opts) {
2738
2980
  env: process.env,
2739
2981
  configUpdate: (() => {
2740
2982
  try {
2741
- const info2 = loadProjectInfo7(process.cwd());
2983
+ const info2 = loadProjectInfo8(process.cwd());
2742
2984
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
2743
2985
  } catch {
2744
2986
  return DEFAULT_UPDATE_CONFIG;
@@ -2767,7 +3009,7 @@ var init_status = __esm({
2767
3009
  // src/commands/task.ts
2768
3010
  import { spawnSync as spawnSync2 } from "child_process";
2769
3011
  import { writeFileSync } from "fs";
2770
- import { loadProjectInfo as loadProjectInfo8, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
3012
+ import { loadProjectInfo as loadProjectInfo9, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
2771
3013
  import {
2772
3014
  PHASES,
2773
3015
  TASK_CLASSES,
@@ -2780,7 +3022,7 @@ function skillFor(phase) {
2780
3022
  }
2781
3023
  function writeDoneArtifacts(opts, taskId) {
2782
3024
  try {
2783
- const project = loadProjectInfo8(process.cwd());
3025
+ const project = loadProjectInfo9(process.cwd());
2784
3026
  const conflict = { conflictPolicy: "preserve", interactive: false };
2785
3027
  writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
2786
3028
  const nextN = nextArtifactSequence(project.root, "adr");
@@ -3103,7 +3345,7 @@ function runCheck(cmd) {
3103
3345
  async function taskVerify(opts) {
3104
3346
  let project;
3105
3347
  try {
3106
- project = loadProjectInfo8(process.cwd());
3348
+ project = loadProjectInfo9(process.cwd());
3107
3349
  } catch {
3108
3350
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
3109
3351
  }
@@ -3315,7 +3557,7 @@ import {
3315
3557
  hostLaunchDirective,
3316
3558
  resolveAdapter as resolveAdapter3
3317
3559
  } from "@noir-ai/adapters";
3318
- import { loadProjectInfo as loadProjectInfo9, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
3560
+ import { loadProjectInfo as loadProjectInfo10, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
3319
3561
  function heading(name, id) {
3320
3562
  return `# Noir handoff \u2014 ${name} (${id})`;
3321
3563
  }
@@ -3454,7 +3696,7 @@ async function probeOnly(opts) {
3454
3696
  async function handoff(opts) {
3455
3697
  let project;
3456
3698
  try {
3457
- project = loadProjectInfo9(process.cwd());
3699
+ project = loadProjectInfo10(process.cwd());
3458
3700
  } catch {
3459
3701
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
3460
3702
  }
@@ -3818,14 +4060,14 @@ import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
3818
4060
  import {
3819
4061
  detectActiveMethod as detectActiveMethod4,
3820
4062
  latestVersionFromCache as latestVersionFromCache2,
3821
- loadProjectInfo as loadProjectInfo10,
4063
+ loadProjectInfo as loadProjectInfo11,
3822
4064
  NOIR_VERSION as NOIR_VERSION5,
3823
4065
  readInstallRecord as readInstallRecord4,
3824
4066
  readUpdateCache as readUpdateCache3
3825
4067
  } from "@noir-ai/core";
3826
4068
  function tryProject() {
3827
4069
  try {
3828
- const info2 = loadProjectInfo10(process.cwd());
4070
+ const info2 = loadProjectInfo11(process.cwd());
3829
4071
  return { id: info2.id, name: info2.name, host: info2.config.host };
3830
4072
  } catch {
3831
4073
  return null;
@@ -3988,7 +4230,7 @@ async function runMenu(opts, deps) {
3988
4230
  env: process.env,
3989
4231
  configUpdate: (() => {
3990
4232
  try {
3991
- const info2 = loadProjectInfo10(process.cwd());
4233
+ const info2 = loadProjectInfo11(process.cwd());
3992
4234
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
3993
4235
  } catch {
3994
4236
  return DEFAULT_UPDATE_CONFIG;
@@ -4051,12 +4293,12 @@ var init_home = __esm({
4051
4293
 
4052
4294
  // src/commands/memory.ts
4053
4295
  import { readFileSync as readFileSync3 } from "fs";
4054
- import { loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
4296
+ import { loadProjectInfo as loadProjectInfo12 } from "@noir-ai/core";
4055
4297
  function resolveWorkspace() {
4056
4298
  const routing = workspaceRoutingTarget(process.cwd());
4057
4299
  if (routing === null) return null;
4058
4300
  try {
4059
- return { routing, project: loadProjectInfo11(process.cwd()) };
4301
+ return { routing, project: loadProjectInfo12(process.cwd()) };
4060
4302
  } catch {
4061
4303
  return null;
4062
4304
  }
@@ -4448,9 +4690,9 @@ async function memoryConsolidate(opts) {
4448
4690
  }
4449
4691
  return;
4450
4692
  }
4451
- const env = res;
4452
- const reason = (typeof env.reason === "string" ? env.reason : void 0) ?? (typeof env.error === "string" ? env.error : void 0) ?? "unknown";
4453
- const logged = env.logged === true || env.degraded === true;
4693
+ const env2 = res;
4694
+ const reason = (typeof env2.reason === "string" ? env2.reason : void 0) ?? (typeof env2.error === "string" ? env2.error : void 0) ?? "unknown";
4695
+ const logged = env2.logged === true || env2.degraded === true;
4454
4696
  const suffix = logged ? " (logged)" : "";
4455
4697
  fail(EXIT.ERROR, `memory consolidate did not run: ${reason}${suffix}`, opts);
4456
4698
  }
@@ -4570,10 +4812,10 @@ var init_release = __esm({
4570
4812
  import { spawn } from "child_process";
4571
4813
  import { basename } from "path";
4572
4814
  function isEligibleForShellFallback(name, opts = {}) {
4573
- const { env = process.env, platform = process.platform } = opts;
4815
+ const { env: env2 = process.env, platform = process.platform } = opts;
4574
4816
  if (platform === "win32") return false;
4575
4817
  if (name.length === 0 || name.includes("/") || !NAME_RE.test(name)) return false;
4576
- const shell = env.SHELL;
4818
+ const shell = env2.SHELL;
4577
4819
  if (!shell || shell.length === 0) return false;
4578
4820
  return SUPPORTED_SHELLS.has(basename(shell).toLowerCase());
4579
4821
  }
@@ -4636,9 +4878,9 @@ function probeCommand(name, shell, opts) {
4636
4878
  });
4637
4879
  }
4638
4880
  async function resolveCommandViaShell(name, opts = {}) {
4639
- const { env = process.env } = opts;
4881
+ const { env: env2 = process.env } = opts;
4640
4882
  if (!isEligibleForShellFallback(name, opts)) return { kind: "unsupported" };
4641
- const shell = env.SHELL;
4883
+ const shell = env2.SHELL;
4642
4884
  const cached = pathCache.get(`${shell}:${name}`);
4643
4885
  if (cached) return { kind: "path", path: cached, shell };
4644
4886
  const probe = await probeCommand(name, shell, opts);
@@ -4939,24 +5181,24 @@ var init_orchestrator = __esm({
4939
5181
  });
4940
5182
 
4941
5183
  // src/run-profiles.ts
4942
- import { loadProjectInfo as loadProjectInfo12 } from "@noir-ai/core";
5184
+ import { loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
4943
5185
  function loadRunConfig(root) {
4944
5186
  try {
4945
- return loadProjectInfo12(root).config;
5187
+ return loadProjectInfo13(root).config;
4946
5188
  } catch {
4947
5189
  return null;
4948
5190
  }
4949
5191
  }
4950
- function expandEnvVars(value, env) {
5192
+ function expandEnvVars(value, env2) {
4951
5193
  return value.replace(
4952
5194
  /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
4953
- (match, name) => env[name] !== void 0 ? env[name] : match
5195
+ (match, name) => env2[name] !== void 0 ? env2[name] : match
4954
5196
  );
4955
5197
  }
4956
- function resolveRunProfile(requested, config, env) {
5198
+ function resolveRunProfile(requested, config, env2) {
4957
5199
  const profiles = config.run?.profiles ?? {};
4958
5200
  const names = Object.keys(profiles);
4959
- const name = requested ?? env.NOIR_PROFILE ?? config.run?.defaultProfile;
5201
+ const name = requested ?? env2.NOIR_PROFILE ?? config.run?.defaultProfile;
4960
5202
  if (name === void 0 || name.length === 0) {
4961
5203
  return { ok: true, profile: {} };
4962
5204
  }
@@ -4971,7 +5213,7 @@ function resolveRunProfile(requested, config, env) {
4971
5213
  const envExpanded = {};
4972
5214
  if (profile.env) {
4973
5215
  for (const [k, v] of Object.entries(profile.env)) {
4974
- envExpanded[k] = v === null ? void 0 : expandEnvVars(v, env);
5216
+ envExpanded[k] = v === null ? void 0 : expandEnvVars(v, env2);
4975
5217
  }
4976
5218
  }
4977
5219
  return {
@@ -4995,10 +5237,24 @@ var init_run_profiles = __esm({
4995
5237
  });
4996
5238
 
4997
5239
  // src/commands/run.ts
4998
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
5240
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
4999
5241
  import { join as join5 } from "path";
5000
5242
  import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
5001
- import { parseConfig } from "@noir-ai/core";
5243
+ import { loadNoirEnv as loadNoirEnv3, NOIR_DIR as NOIR_DIR2, parseConfig } from "@noir-ai/core";
5244
+ function credentialNote(env2, sources) {
5245
+ const named = CREDENTIAL_ENV_VARS.filter((name) => {
5246
+ const value = env2[name];
5247
+ return value !== void 0 && value.length > 0;
5248
+ }).map(
5249
+ (name) => `${name} (from ${sources[name] === "file" ? `${NOIR_DIR2}/.env` : "the environment"})`
5250
+ );
5251
+ if (named.length === 0) return "";
5252
+ const one = named.length === 1;
5253
+ const subject = one ? `${named[0]} is set` : `${named.join(", ")} are set`;
5254
+ const pronoun = one ? "it" : "them";
5255
+ const verb = one ? "overrides" : "override";
5256
+ return ` Note: ${subject} \u2014 ${pronoun} ${verb} the logged-in account in \`-p\` mode; unset ${pronoun} in the named source (or fix the value) if you meant to use your subscription.`;
5257
+ }
5002
5258
  function formatUsage(u) {
5003
5259
  const cost = u.totalCostUsd > 0 ? ` $${u.totalCostUsd.toFixed(2)}` : "";
5004
5260
  const turns = u.numTurns > 0 ? ` \xB7 ${u.numTurns} turns` : "";
@@ -5022,13 +5278,21 @@ async function run(prompt, opts) {
5022
5278
  const usage = selectors.length > 0 ? `noir run ${selectors.join(" ")} <prompt>` : "noir run <prompt>";
5023
5279
  fail(EXIT.USAGE, `a prompt is required: \`${usage}\``, opts);
5024
5280
  }
5025
- const config = loadRunConfig(process.cwd()) ?? parseConfig({});
5281
+ const root = process.cwd();
5282
+ const envSources = loadNoirEnv3(root).sources;
5283
+ const config = loadRunConfig(root) ?? parseConfig({});
5026
5284
  const resolved = resolveRunProfile(opts.profile, config, process.env);
5027
5285
  if (!resolved.ok) fail(EXIT.USAGE, resolved.message, opts);
5028
5286
  const profile = resolved.profile;
5029
5287
  const customBinary = opts.command ?? profile.binary;
5030
5288
  const extraArgs = profile.args;
5031
- const env = profile.env ? mergeEnv(process.env, profile.env) : void 0;
5289
+ const env2 = profile.env ? mergeEnv(process.env, profile.env) : void 0;
5290
+ if (!existsSync3(join5(root, NOIR_DIR2, ".env"))) {
5291
+ log(
5292
+ `This project has no ${NOIR_DIR2}/.env \u2014 that is where Noir reads project credentials and run config from. Run \`noir init\` to scaffold one (\`noir run\` works without it).`,
5293
+ opts
5294
+ );
5295
+ }
5032
5296
  const transcriptLines = [];
5033
5297
  let result;
5034
5298
  try {
@@ -5037,7 +5301,7 @@ async function run(prompt, opts) {
5037
5301
  prompt,
5038
5302
  customBinary,
5039
5303
  extraArgs,
5040
- env,
5304
+ env: env2,
5041
5305
  onLine: (line) => transcriptLines.push(line),
5042
5306
  onEvent: (event) => streamEvent(event, opts)
5043
5307
  });
@@ -5058,9 +5322,7 @@ async function run(prompt, opts) {
5058
5322
  let message = `host '${binary}' failed (exit ${result.exitCode}): ${reason}`;
5059
5323
  if (isAuth) {
5060
5324
  message += ` Open a terminal and run \`${binary} /login\` (interactive-only \u2014 it cannot run inside \`noir run\`), then retry.`;
5061
- if (process.env.ANTHROPIC_API_KEY) {
5062
- message += ` Note: ANTHROPIC_API_KEY is set in your environment \u2014 it overrides the logged-in account in \`-p\` mode; unset it (or fix the key) if you meant to use your subscription.`;
5063
- }
5325
+ message += credentialNote(env2 ?? process.env, envSources);
5064
5326
  }
5065
5327
  message += ` If you use another profile, pass \`--command <binary>\` or define a run profile under run.profiles. transcript: ${transcript}`;
5066
5328
  fail(EXIT.ERROR, message, opts);
@@ -5100,18 +5362,24 @@ function writeTranscript(host, lines) {
5100
5362
  }
5101
5363
  return file;
5102
5364
  }
5365
+ var CREDENTIAL_ENV_VARS;
5103
5366
  var init_run = __esm({
5104
5367
  "src/commands/run.ts"() {
5105
5368
  "use strict";
5106
5369
  init_orchestrator();
5107
5370
  init_output();
5108
5371
  init_run_profiles();
5372
+ CREDENTIAL_ENV_VARS = [
5373
+ "ANTHROPIC_API_KEY",
5374
+ "ANTHROPIC_AUTH_TOKEN",
5375
+ "ANTHROPIC_BASE_URL"
5376
+ ];
5109
5377
  }
5110
5378
  });
5111
5379
 
5112
5380
  // src/commands/skills.ts
5113
5381
  import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
5114
- import { loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
5382
+ import { loadProjectInfo as loadProjectInfo14 } from "@noir-ai/core";
5115
5383
  import {
5116
5384
  buildRegistry,
5117
5385
  discoverAll,
@@ -5179,7 +5447,7 @@ async function skillsSync(opts) {
5179
5447
  const root = process.cwd();
5180
5448
  let project;
5181
5449
  try {
5182
- project = loadProjectInfo13(root);
5450
+ project = loadProjectInfo14(root);
5183
5451
  } catch {
5184
5452
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
5185
5453
  }
@@ -5705,7 +5973,7 @@ var init_dedup_write = __esm({
5705
5973
 
5706
5974
  // src/init.ts
5707
5975
  import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
5708
- import { loadProjectInfo as loadProjectInfo14 } from "@noir-ai/core";
5976
+ import { loadProjectInfo as loadProjectInfo15 } from "@noir-ai/core";
5709
5977
  import { scaffold } from "@noir-ai/create";
5710
5978
  import { emitSkillsToDir as emitSkillsToDir2 } from "@noir-ai/skills";
5711
5979
  async function init(root, opts) {
@@ -5734,7 +6002,7 @@ async function init(root, opts) {
5734
6002
  await emitHostSkills(root, host, conflictOpts, interactive);
5735
6003
  let projectInfo;
5736
6004
  try {
5737
- projectInfo = loadProjectInfo14(root);
6005
+ projectInfo = loadProjectInfo15(root);
5738
6006
  } catch {
5739
6007
  projectInfo = void 0;
5740
6008
  }
@@ -5753,7 +6021,10 @@ function reportPlannedWrites(res) {
5753
6021
  log("Dry run \u2014 no files were written.");
5754
6022
  if (res.written.length > 0) {
5755
6023
  log("Planned writes:");
5756
- for (const p of res.written) log(` ${p}`);
6024
+ for (const p of res.written) {
6025
+ const mode = res.fileModes?.[p];
6026
+ log(mode === void 0 ? ` ${p}` : ` ${p} (mode ${mode.toString(8).padStart(3, "0")})`);
6027
+ }
5757
6028
  }
5758
6029
  if (res.skipped.length > 0) {
5759
6030
  log("Would leave as-is (already present):");
@@ -5821,11 +6092,11 @@ var init_init = __esm({
5821
6092
  });
5822
6093
 
5823
6094
  // src/serve.ts
5824
- import { applyNoirEnv, loadProjectInfo as loadProjectInfo15 } from "@noir-ai/core";
6095
+ import { applyNoirEnv, loadProjectInfo as loadProjectInfo16 } from "@noir-ai/core";
5825
6096
  import { ensureDaemonRunning as ensureDaemonRunning3, startStdioServer } from "@noir-ai/daemon";
5826
6097
  async function serve(opts) {
5827
6098
  applyNoirEnv(process.cwd());
5828
- const project = loadProjectInfo15(process.cwd());
6099
+ const project = loadProjectInfo16(process.cwd());
5829
6100
  if (opts.stdio) {
5830
6101
  await startStdioServer({ project, transport: "stdio", daemon: false });
5831
6102
  return;
@@ -5833,10 +6104,11 @@ async function serve(opts) {
5833
6104
  try {
5834
6105
  const { url } = await ensureDaemonRunning3({
5835
6106
  project,
5836
- idleTimeoutSec: project.config.daemon.idleTimeoutSec
6107
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec,
6108
+ ...project.config.daemon.port !== void 0 ? { port: project.config.daemon.port } : {}
5837
6109
  });
5838
6110
  process.stderr.write(
5839
- `Noir daemon available at ${url}. (For HTTP clients, use this URL in .mcp.json.)
6111
+ `Noir daemon available at ${url}. (For HTTP clients, use this URL in .mcp.json.) Add the daemon's bearer token via the host MCP headersHelper (run \`noir daemon token\`).
5840
6112
  `
5841
6113
  );
5842
6114
  } catch (err) {
@@ -5860,7 +6132,7 @@ __export(create_exports, {
5860
6132
  });
5861
6133
  import { resolve } from "path";
5862
6134
  import { resolveAdapter as resolveAdapter6 } from "@noir-ai/adapters";
5863
- import { loadProjectInfo as loadProjectInfo16 } from "@noir-ai/core";
6135
+ import { loadProjectInfo as loadProjectInfo17 } from "@noir-ai/core";
5864
6136
  import { scaffold as scaffold2 } from "@noir-ai/create";
5865
6137
  import { emitSkillsToDir as emitSkillsToDir3 } from "@noir-ai/skills";
5866
6138
  async function create(dir, opts) {
@@ -5911,7 +6183,7 @@ async function create(dir, opts) {
5911
6183
  }
5912
6184
  let projectInfo;
5913
6185
  try {
5914
- projectInfo = loadProjectInfo16(root);
6186
+ projectInfo = loadProjectInfo17(root);
5915
6187
  } catch {
5916
6188
  projectInfo = void 0;
5917
6189
  }
@@ -5937,9 +6209,9 @@ var sync_exports = {};
5937
6209
  __export(sync_exports, {
5938
6210
  sync: () => sync
5939
6211
  });
5940
- import { existsSync as existsSync3 } from "fs";
6212
+ import { existsSync as existsSync4 } from "fs";
5941
6213
  import { resolveAdapter as resolveAdapter7 } from "@noir-ai/adapters";
5942
- import { loadProjectInfo as loadProjectInfo17, paths as paths2 } from "@noir-ai/core";
6214
+ import { loadProjectInfo as loadProjectInfo18, paths as paths2 } from "@noir-ai/core";
5943
6215
  import { scaffold as scaffold3 } from "@noir-ai/create";
5944
6216
  import { emitSkillsToDir as emitSkillsToDir4 } from "@noir-ai/skills";
5945
6217
  async function sync(root, opts = {}) {
@@ -5948,7 +6220,7 @@ async function sync(root, opts = {}) {
5948
6220
  const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
5949
6221
  let projectInfo;
5950
6222
  try {
5951
- projectInfo = loadProjectInfo17(root);
6223
+ projectInfo = loadProjectInfo18(root);
5952
6224
  } catch {
5953
6225
  projectInfo = void 0;
5954
6226
  }
@@ -6006,9 +6278,9 @@ async function sync(root, opts = {}) {
6006
6278
  }
6007
6279
  function resolveSyncHost(root, opts) {
6008
6280
  if (opts.host !== void 0) return opts.host;
6009
- if (!existsSync3(paths2.config(root))) return "claude";
6281
+ if (!existsSync4(paths2.config(root))) return "claude";
6010
6282
  try {
6011
- return loadProjectInfo17(root).config.host;
6283
+ return loadProjectInfo18(root).config.host;
6012
6284
  } catch {
6013
6285
  return "claude";
6014
6286
  }
@@ -6128,6 +6400,7 @@ function createProgram() {
6128
6400
  );
6129
6401
  }
6130
6402
  }
6403
+ if (actionCmd.name() === "env") captureAmbientEnv();
6131
6404
  applyNoirEnv2(process.cwd());
6132
6405
  const nonInteractive = opts.json === true || opts.input === false;
6133
6406
  if (nonInteractive) process.env.NOIR_NON_INTERACTIVE = "1";
@@ -6270,6 +6543,9 @@ function createProgram() {
6270
6543
  daemonGrp.command("status").description("report daemon pid/uptime/mode (exit 4 if not running)").action(async (...args) => {
6271
6544
  await daemonStatus(toCliOptions(trailingCmd(args).optsWithGlobals()));
6272
6545
  });
6546
+ daemonGrp.command("token").description("print the daemon bearer token to stdout (for a host headersHelper)").action(async (...args) => {
6547
+ await daemonToken(toCliOptions(trailingCmd(args).optsWithGlobals()));
6548
+ });
6273
6549
  daemonGrp.command("restart").description("stop then start the daemon").option("--detach", "run the daemon in the background and exit").action(async (...args) => {
6274
6550
  const cmd = trailingCmd(args);
6275
6551
  const g = cmd.optsWithGlobals();
@@ -6279,8 +6555,19 @@ function createProgram() {
6279
6555
  });
6280
6556
  });
6281
6557
  daemonGrp.action(() => {
6282
- throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|join|stop|status|restart");
6558
+ throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|join|stop|status|restart|token");
6283
6559
  });
6560
+ daemonGrp.addHelpText(
6561
+ "after",
6562
+ [
6563
+ "",
6564
+ "Shared workspaces (two repos, one product \u2014 e.g. backend + frontend):",
6565
+ " noir daemon start --workspace <name> --detach found a workspace from this repo",
6566
+ " noir daemon join <name> join it from the other repo",
6567
+ "",
6568
+ "Guide: docs/how-to/shared-workspaces.md"
6569
+ ].join("\n")
6570
+ );
6284
6571
  const workspaceGrp = program2.command("workspace").description("shared cross-repo workspaces");
6285
6572
  workspaceGrp.command("list").description("list workspaces on this machine").action(async (...args) => {
6286
6573
  await workspaceList(toCliOptions(trailingCmd(args).optsWithGlobals()));
@@ -6309,6 +6596,17 @@ function createProgram() {
6309
6596
  ...name !== void 0 ? { name } : {}
6310
6597
  });
6311
6598
  });
6599
+ workspaceGrp.addHelpText(
6600
+ "after",
6601
+ [
6602
+ "",
6603
+ "This group only inspects and leaves workspaces. To CREATE or JOIN one:",
6604
+ " noir daemon start --workspace <name> --detach found it (from the first repo)",
6605
+ " noir daemon join <name> join it (from the second repo)",
6606
+ "",
6607
+ "Guide: docs/how-to/shared-workspaces.md"
6608
+ ].join("\n")
6609
+ );
6312
6610
  workspaceGrp.action(() => {
6313
6611
  throw new NoirCliError(EXIT.USAGE, "Usage: noir workspace list|status|leave|stop");
6314
6612
  });
@@ -6323,6 +6621,9 @@ function createProgram() {
6323
6621
  program2.command("status").description("project + daemon + workflow + store snapshot").action(async (...args) => {
6324
6622
  await status(toStatusOptions(actionGlobals(args)));
6325
6623
  });
6624
+ program2.command("env").description("which configuration is in effect and where each value comes from").action(async (...args) => {
6625
+ await env(toCliOptions(actionGlobals(args)));
6626
+ });
6326
6627
  const contextGrp = program2.command("context").description("context engine");
6327
6628
  contextGrp.command("search").description("hybrid search over the indexed context").argument("<query>", "search query").option("--limit <n>", "max results", "10").action(async (...args) => {
6328
6629
  const cmd = trailingCmd(args);
@@ -6707,6 +7008,7 @@ var init_bin = __esm({
6707
7008
  init_context();
6708
7009
  init_daemon();
6709
7010
  init_doctor();
7011
+ init_env();
6710
7012
  init_handoff();
6711
7013
  init_home();
6712
7014
  init_memory();