@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/index.js CHANGED
@@ -857,10 +857,11 @@ var init_workspace = __esm({
857
857
  // src/commands/daemon.ts
858
858
  import { loadProjectInfo as loadProjectInfo2 } from "@noir-ai/core";
859
859
  import {
860
- clearDaemonRecord,
860
+ clearProjectDaemonRecord,
861
861
  ensureDaemonRunning,
862
862
  pidAlive as pidAlive2,
863
- readDaemonRecord,
863
+ readDaemonToken,
864
+ readProjectDaemonRecord,
864
865
  spawnDetachedDaemon
865
866
  } from "@noir-ai/daemon";
866
867
  function formatUptime(sec) {
@@ -910,7 +911,8 @@ async function daemonStart(opts) {
910
911
  if (opts.detachChild === true) {
911
912
  const child = await ensureDaemonRunning({
912
913
  project,
913
- idleTimeoutSec: project.config.daemon.idleTimeoutSec
914
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec,
915
+ ...project.config.daemon.port !== void 0 ? { port: project.config.daemon.port } : {}
914
916
  });
915
917
  if (child.started) {
916
918
  if (opts.json === true) {
@@ -933,7 +935,7 @@ async function daemonStart(opts) {
933
935
  return;
934
936
  }
935
937
  if (opts.detach === true) {
936
- const existing = readDaemonRecord();
938
+ const existing = readProjectDaemonRecord(project.id);
937
939
  if (existing && pidAlive2(existing.pid) && await isHealthy(existing.port, existing.pid, project.id)) {
938
940
  if (opts.json === true) {
939
941
  process.stdout.write(
@@ -973,7 +975,8 @@ async function daemonStart(opts) {
973
975
  try {
974
976
  ensured = await ensureDaemonRunning({
975
977
  project,
976
- idleTimeoutSec: project.config.daemon.idleTimeoutSec
978
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec,
979
+ ...project.config.daemon.port !== void 0 ? { port: project.config.daemon.port } : {}
977
980
  });
978
981
  } catch (err) {
979
982
  ds.fail("Daemon failed to start");
@@ -1016,7 +1019,19 @@ async function daemonStart(opts) {
1016
1019
  log(`Noir daemon already running at ${ensured.url}`, opts);
1017
1020
  }
1018
1021
  async function daemonStop(opts) {
1019
- const rec = readDaemonRecord();
1022
+ const callerProject = resolveCallerProjectId();
1023
+ if (callerProject === void 0) {
1024
+ if (opts.json === true) {
1025
+ process.stdout.write(
1026
+ `${JSON.stringify({ ok: true, data: { running: false, stopped: false } })}
1027
+ `
1028
+ );
1029
+ return;
1030
+ }
1031
+ info("No Noir daemon is running for this directory (not a Noir project).", opts);
1032
+ return;
1033
+ }
1034
+ const rec = readProjectDaemonRecord(callerProject);
1020
1035
  if (!rec) {
1021
1036
  if (opts.json === true) {
1022
1037
  process.stdout.write(
@@ -1031,9 +1046,7 @@ async function daemonStop(opts) {
1031
1046
  const ds = spinner(`Stopping daemon (pid ${rec.pid})...`, opts).start();
1032
1047
  let signalled = false;
1033
1048
  let errMsg2;
1034
- const callerProject = resolveCallerProjectId();
1035
- const wrongProject = rec.projectId !== void 0 && callerProject !== void 0 && rec.projectId !== callerProject;
1036
- const healthy = !wrongProject && pidAlive2(rec.pid) && await isHealthy(rec.port, rec.pid, rec.projectId === void 0 ? void 0 : callerProject);
1049
+ const healthy = pidAlive2(rec.pid) && await isHealthy(rec.port, rec.pid, callerProject);
1037
1050
  if (healthy) {
1038
1051
  try {
1039
1052
  process.kill(rec.pid, "SIGTERM");
@@ -1042,7 +1055,7 @@ async function daemonStop(opts) {
1042
1055
  errMsg2 = err instanceof Error ? err.message : String(err);
1043
1056
  }
1044
1057
  }
1045
- if (!wrongProject) clearDaemonRecord();
1058
+ clearProjectDaemonRecord(callerProject);
1046
1059
  if (opts.json === true) {
1047
1060
  process.stdout.write(
1048
1061
  `${JSON.stringify({
@@ -1061,26 +1074,22 @@ async function daemonStop(opts) {
1061
1074
  if (signalled) {
1062
1075
  ds.succeed(`Stopped Noir daemon (pid ${rec.pid})`);
1063
1076
  } else {
1064
- 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)";
1077
+ const reason = errMsg2 !== void 0 ? errMsg2 : "the recorded pid did not answer as our daemon (stale record cleared)";
1065
1078
  ds.warn(`Daemon (pid ${rec.pid}) could not be signalled: ${reason}`);
1066
1079
  info(`Noir daemon (pid ${rec.pid}) could not be signalled: ${reason}`, opts);
1067
1080
  }
1068
1081
  }
1069
1082
  async function daemonStatus(opts) {
1070
- const rec = readDaemonRecord();
1083
+ const callerProject = resolveCallerProjectId();
1084
+ if (callerProject === void 0) {
1085
+ fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (not a Noir project).", opts);
1086
+ }
1087
+ const rec = readProjectDaemonRecord(callerProject);
1071
1088
  if (!rec) {
1072
1089
  fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (start with `noir daemon start`).", opts);
1073
1090
  }
1074
- const callerProject = resolveCallerProjectId();
1075
- if (rec.projectId !== void 0 && callerProject !== void 0 && rec.projectId !== callerProject) {
1076
- fail(
1077
- EXIT.DAEMON_DOWN,
1078
- "Noir daemon is not running for this project (a daemon for another project is running).",
1079
- opts
1080
- );
1081
- }
1082
1091
  if (!pidAlive2(rec.pid)) {
1083
- clearDaemonRecord();
1092
+ clearProjectDaemonRecord(callerProject);
1084
1093
  fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
1085
1094
  }
1086
1095
  const hs = spinner("Probing daemon health...", opts).start();
@@ -1098,7 +1107,7 @@ async function daemonStatus(opts) {
1098
1107
  const pidOk = typeof health?.pid === "number" && health.pid === rec.pid;
1099
1108
  if (health?.ok !== true || !pidOk) {
1100
1109
  hs.fail("Daemon not responding");
1101
- clearDaemonRecord();
1110
+ clearProjectDaemonRecord(callerProject);
1102
1111
  fail(
1103
1112
  EXIT.DAEMON_DOWN,
1104
1113
  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).",
@@ -1114,7 +1123,8 @@ async function daemonStatus(opts) {
1114
1123
  startedAt: rec.startedAt,
1115
1124
  uptimeSec,
1116
1125
  // Honest ownership: the detached child writes `mode:'detached'`; anything
1117
- // else (legacy record, foreground start) reports 'foreground'.
1126
+ // else (a record written before the field existed, foreground start)
1127
+ // reports 'foreground'.
1118
1128
  mode: rec.mode ?? MODE
1119
1129
  };
1120
1130
  if (opts.json === true) {
@@ -1127,6 +1137,42 @@ async function daemonStatus(opts) {
1127
1137
  opts
1128
1138
  );
1129
1139
  }
1140
+ async function daemonToken(opts) {
1141
+ const callerProject = resolveCallerProjectId();
1142
+ if (callerProject === void 0) {
1143
+ fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (not a Noir project).", opts);
1144
+ }
1145
+ const rec = readProjectDaemonRecord(callerProject);
1146
+ if (!rec) {
1147
+ fail(
1148
+ EXIT.DAEMON_DOWN,
1149
+ "Noir daemon is not running (no daemon record \u2014 start it with `noir daemon start`).",
1150
+ opts
1151
+ );
1152
+ }
1153
+ if (!pidAlive2(rec.pid) || !await isHealthy(rec.port, rec.pid, callerProject)) {
1154
+ fail(
1155
+ EXIT.DAEMON_DOWN,
1156
+ "Noir daemon is not running (stale record \u2014 start it with `noir daemon start`).",
1157
+ opts
1158
+ );
1159
+ }
1160
+ const token = readDaemonToken(callerProject);
1161
+ if (token === null) {
1162
+ fail(
1163
+ EXIT.DAEMON_DOWN,
1164
+ "Noir daemon is not running (the record has no token beside it \u2014 restart it with `noir daemon start`).",
1165
+ opts
1166
+ );
1167
+ }
1168
+ if (opts.json === true) {
1169
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { token } })}
1170
+ `);
1171
+ return;
1172
+ }
1173
+ process.stdout.write(`${token}
1174
+ `);
1175
+ }
1130
1176
  async function daemonRestart(opts) {
1131
1177
  await daemonStop({ ...opts, json: false, quiet: true });
1132
1178
  await daemonStart(opts);
@@ -1142,6 +1188,100 @@ var init_daemon = __esm({
1142
1188
  }
1143
1189
  });
1144
1190
 
1191
+ // src/commands/env.ts
1192
+ import { loadNoirEnv, loadProjectInfo as loadProjectInfo3 } from "@noir-ai/core";
1193
+ import { resolveModelConfig } from "@noir-ai/model";
1194
+ function captureAmbientEnv(env2 = process.env) {
1195
+ ambientBaseline = { ...env2 };
1196
+ }
1197
+ function redactShape(value) {
1198
+ if (value.length <= SHORT_VALUE_MAX) return `\u2026(${value.length})`;
1199
+ return `${value.slice(0, 3)}\u2026(${value.length})`;
1200
+ }
1201
+ function configApiKeyEnvNames(root) {
1202
+ try {
1203
+ const project = loadProjectInfo3(root);
1204
+ const names = /* @__PURE__ */ new Set();
1205
+ for (const provider of Object.values(resolveModelConfig(project.config.model).providers)) {
1206
+ if (provider.apiKeyEnv !== void 0 && provider.apiKeyEnv.length > 0) {
1207
+ names.add(provider.apiKeyEnv);
1208
+ }
1209
+ }
1210
+ return [...names];
1211
+ } catch {
1212
+ return [];
1213
+ }
1214
+ }
1215
+ function resolveEnv(root, ambientEnv) {
1216
+ const { overlay, warnings, sources } = loadNoirEnv(root, ambientEnv);
1217
+ const curated = new Set(CURATED_AMBIENT_KEYS);
1218
+ for (const name of configApiKeyEnvNames(root)) curated.add(name);
1219
+ const candidates = new Set(Object.keys(sources));
1220
+ for (const key of curated) {
1221
+ if (ambientEnv[key] !== void 0) candidates.add(key);
1222
+ }
1223
+ const vars = [];
1224
+ const shapes = {};
1225
+ for (const key of [...candidates].sort()) {
1226
+ const source = sources[key] ?? "env";
1227
+ if (source === "env" && !curated.has(key)) continue;
1228
+ const value = source === "file" ? overlay[key] ?? "" : ambientEnv[key] ?? "";
1229
+ const shadowed = source === "file" && ambientEnv[key] !== void 0 && ambientEnv[key] !== value;
1230
+ vars.push({
1231
+ key,
1232
+ source,
1233
+ ...shadowed ? { shadowed: true } : {},
1234
+ valueLength: value.length
1235
+ });
1236
+ shapes[key] = redactShape(value);
1237
+ }
1238
+ return { vars, warnings, shapes };
1239
+ }
1240
+ function describeSource(row) {
1241
+ if (row.source !== "file") return "environment";
1242
+ return row.shadowed === true ? ".noir/.env (shadows environment)" : ".noir/.env";
1243
+ }
1244
+ async function env(opts = {}) {
1245
+ const report = resolveEnv(process.cwd(), ambientBaseline ?? process.env);
1246
+ if (opts.json === true) {
1247
+ json({ ok: true, data: { vars: report.vars, warnings: report.warnings } });
1248
+ return;
1249
+ }
1250
+ if (report.vars.length === 0) {
1251
+ info(
1252
+ "nothing to report \u2014 .noir/.env defines no keys in effect and no Noir-relevant environment variable is set",
1253
+ opts
1254
+ );
1255
+ return;
1256
+ }
1257
+ table(
1258
+ report.vars.map((row) => ({
1259
+ KEY: row.key,
1260
+ SOURCE: describeSource(row),
1261
+ VALUE: report.shapes[row.key] ?? ""
1262
+ })),
1263
+ ["KEY", "SOURCE", "VALUE"],
1264
+ opts
1265
+ );
1266
+ }
1267
+ var CURATED_AMBIENT_KEYS, ambientBaseline, SHORT_VALUE_MAX;
1268
+ var init_env = __esm({
1269
+ "src/commands/env.ts"() {
1270
+ "use strict";
1271
+ init_output();
1272
+ CURATED_AMBIENT_KEYS = [
1273
+ "CLICKUP_API_TOKEN",
1274
+ "OPENAI_API_KEY",
1275
+ "VOYAGE_API_KEY",
1276
+ "COHERE_API_KEY",
1277
+ "OLLAMA_BASE_URL",
1278
+ "ANTHROPIC_API_KEY",
1279
+ "NOIR_PROFILE"
1280
+ ];
1281
+ SHORT_VALUE_MAX = 8;
1282
+ }
1283
+ });
1284
+
1145
1285
  // src/commands/install.ts
1146
1286
  var install_exports = {};
1147
1287
  __export(install_exports, {
@@ -1156,7 +1296,7 @@ import {
1156
1296
  detectActiveMethod,
1157
1297
  detectInstallMethods,
1158
1298
  ensureShimExecutable,
1159
- loadProjectInfo as loadProjectInfo3,
1299
+ loadProjectInfo as loadProjectInfo4,
1160
1300
  MANAGED_NODE_VERSION,
1161
1301
  NOIR_VERSION,
1162
1302
  noirHome,
@@ -1295,7 +1435,7 @@ async function install(opts = {}) {
1295
1435
  const installedRecord = readInstallRecord();
1296
1436
  let minVersion = "1.6.0";
1297
1437
  try {
1298
- minVersion = loadProjectInfo3(process.cwd()).config.update.minVersion;
1438
+ minVersion = loadProjectInfo4(process.cwd()).config.update.minVersion;
1299
1439
  } catch {
1300
1440
  }
1301
1441
  const plan = buildMigrationPlan({
@@ -1372,7 +1512,7 @@ import {
1372
1512
  detectActiveMethod as detectActiveMethod2,
1373
1513
  ensureShimExecutable as ensureShimExecutable2,
1374
1514
  fetchLatestVersion,
1375
- loadProjectInfo as loadProjectInfo4,
1515
+ loadProjectInfo as loadProjectInfo5,
1376
1516
  readInstallRecord as readInstallRecord2,
1377
1517
  readUpdateCache,
1378
1518
  runManagerCmd as runManagerCmd2,
@@ -1452,7 +1592,7 @@ async function update(opts = {}) {
1452
1592
  fetchSpin.succeed(`Latest: ${latest}`);
1453
1593
  let minVersion = DEFAULT_UPDATE_CONFIG.minVersion;
1454
1594
  try {
1455
- minVersion = loadProjectInfo4(process.cwd()).config.update.minVersion;
1595
+ minVersion = loadProjectInfo5(process.cwd()).config.update.minVersion;
1456
1596
  } catch {
1457
1597
  }
1458
1598
  const target = buildUpdateTarget({
@@ -1602,7 +1742,7 @@ var init_update = __esm({
1602
1742
  });
1603
1743
 
1604
1744
  // src/commands/status.ts
1605
- import { loadProjectInfo as loadProjectInfo5, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
1745
+ import { loadProjectInfo as loadProjectInfo6, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
1606
1746
  async function tryTool(caller, name, args) {
1607
1747
  try {
1608
1748
  return await caller.callTool(name, args);
@@ -1723,7 +1863,7 @@ function renderHuman(p, opts) {
1723
1863
  async function gatherStatusPayload(opts) {
1724
1864
  let project;
1725
1865
  try {
1726
- project = loadProjectInfo5(process.cwd());
1866
+ project = loadProjectInfo6(process.cwd());
1727
1867
  } catch {
1728
1868
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
1729
1869
  }
@@ -1753,7 +1893,7 @@ async function status(opts) {
1753
1893
  env: process.env,
1754
1894
  configUpdate: (() => {
1755
1895
  try {
1756
- const info2 = loadProjectInfo5(process.cwd());
1896
+ const info2 = loadProjectInfo6(process.cwd());
1757
1897
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
1758
1898
  } catch {
1759
1899
  return DEFAULT_UPDATE_CONFIG;
@@ -1782,7 +1922,7 @@ var init_status = __esm({
1782
1922
  // src/commands/task.ts
1783
1923
  import { spawnSync } from "child_process";
1784
1924
  import { writeFileSync } from "fs";
1785
- import { loadProjectInfo as loadProjectInfo6, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
1925
+ import { loadProjectInfo as loadProjectInfo7, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
1786
1926
  import {
1787
1927
  PHASES,
1788
1928
  TASK_CLASSES,
@@ -1795,7 +1935,7 @@ function skillFor(phase) {
1795
1935
  }
1796
1936
  function writeDoneArtifacts(opts, taskId) {
1797
1937
  try {
1798
- const project = loadProjectInfo6(process.cwd());
1938
+ const project = loadProjectInfo7(process.cwd());
1799
1939
  const conflict = { conflictPolicy: "preserve", interactive: false };
1800
1940
  writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
1801
1941
  const nextN = nextArtifactSequence(project.root, "adr");
@@ -2118,7 +2258,7 @@ function runCheck(cmd) {
2118
2258
  async function taskVerify(opts) {
2119
2259
  let project;
2120
2260
  try {
2121
- project = loadProjectInfo6(process.cwd());
2261
+ project = loadProjectInfo7(process.cwd());
2122
2262
  } catch {
2123
2263
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
2124
2264
  }
@@ -2330,7 +2470,7 @@ import {
2330
2470
  hostLaunchDirective,
2331
2471
  resolveAdapter as resolveAdapter2
2332
2472
  } from "@noir-ai/adapters";
2333
- import { loadProjectInfo as loadProjectInfo7, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
2473
+ import { loadProjectInfo as loadProjectInfo8, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
2334
2474
  function heading(name, id) {
2335
2475
  return `# Noir handoff \u2014 ${name} (${id})`;
2336
2476
  }
@@ -2469,7 +2609,7 @@ async function probeOnly(opts) {
2469
2609
  async function handoff(opts) {
2470
2610
  let project;
2471
2611
  try {
2472
- project = loadProjectInfo7(process.cwd());
2612
+ project = loadProjectInfo8(process.cwd());
2473
2613
  } catch {
2474
2614
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
2475
2615
  }
@@ -2833,14 +2973,14 @@ import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
2833
2973
  import {
2834
2974
  detectActiveMethod as detectActiveMethod3,
2835
2975
  latestVersionFromCache,
2836
- loadProjectInfo as loadProjectInfo8,
2976
+ loadProjectInfo as loadProjectInfo9,
2837
2977
  NOIR_VERSION as NOIR_VERSION3,
2838
2978
  readInstallRecord as readInstallRecord3,
2839
2979
  readUpdateCache as readUpdateCache2
2840
2980
  } from "@noir-ai/core";
2841
2981
  function tryProject() {
2842
2982
  try {
2843
- const info2 = loadProjectInfo8(process.cwd());
2983
+ const info2 = loadProjectInfo9(process.cwd());
2844
2984
  return { id: info2.id, name: info2.name, host: info2.config.host };
2845
2985
  } catch {
2846
2986
  return null;
@@ -3003,7 +3143,7 @@ async function runMenu(opts, deps) {
3003
3143
  env: process.env,
3004
3144
  configUpdate: (() => {
3005
3145
  try {
3006
- const info2 = loadProjectInfo8(process.cwd());
3146
+ const info2 = loadProjectInfo9(process.cwd());
3007
3147
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
3008
3148
  } catch {
3009
3149
  return DEFAULT_UPDATE_CONFIG;
@@ -3066,12 +3206,12 @@ var init_home = __esm({
3066
3206
 
3067
3207
  // src/commands/memory.ts
3068
3208
  import { readFileSync as readFileSync2 } from "fs";
3069
- import { loadProjectInfo as loadProjectInfo9 } from "@noir-ai/core";
3209
+ import { loadProjectInfo as loadProjectInfo10 } from "@noir-ai/core";
3070
3210
  function resolveWorkspace() {
3071
3211
  const routing = workspaceRoutingTarget(process.cwd());
3072
3212
  if (routing === null) return null;
3073
3213
  try {
3074
- return { routing, project: loadProjectInfo9(process.cwd()) };
3214
+ return { routing, project: loadProjectInfo10(process.cwd()) };
3075
3215
  } catch {
3076
3216
  return null;
3077
3217
  }
@@ -3463,9 +3603,9 @@ async function memoryConsolidate(opts) {
3463
3603
  }
3464
3604
  return;
3465
3605
  }
3466
- const env = res;
3467
- const reason = (typeof env.reason === "string" ? env.reason : void 0) ?? (typeof env.error === "string" ? env.error : void 0) ?? "unknown";
3468
- const logged = env.logged === true || env.degraded === true;
3606
+ const env2 = res;
3607
+ const reason = (typeof env2.reason === "string" ? env2.reason : void 0) ?? (typeof env2.error === "string" ? env2.error : void 0) ?? "unknown";
3608
+ const logged = env2.logged === true || env2.degraded === true;
3469
3609
  const suffix = logged ? " (logged)" : "";
3470
3610
  fail(EXIT.ERROR, `memory consolidate did not run: ${reason}${suffix}`, opts);
3471
3611
  }
@@ -3585,10 +3725,10 @@ var init_release = __esm({
3585
3725
  import { spawn } from "child_process";
3586
3726
  import { basename } from "path";
3587
3727
  function isEligibleForShellFallback(name, opts = {}) {
3588
- const { env = process.env, platform = process.platform } = opts;
3728
+ const { env: env2 = process.env, platform = process.platform } = opts;
3589
3729
  if (platform === "win32") return false;
3590
3730
  if (name.length === 0 || name.includes("/") || !NAME_RE.test(name)) return false;
3591
- const shell = env.SHELL;
3731
+ const shell = env2.SHELL;
3592
3732
  if (!shell || shell.length === 0) return false;
3593
3733
  return SUPPORTED_SHELLS.has(basename(shell).toLowerCase());
3594
3734
  }
@@ -3651,9 +3791,9 @@ function probeCommand(name, shell, opts) {
3651
3791
  });
3652
3792
  }
3653
3793
  async function resolveCommandViaShell(name, opts = {}) {
3654
- const { env = process.env } = opts;
3794
+ const { env: env2 = process.env } = opts;
3655
3795
  if (!isEligibleForShellFallback(name, opts)) return { kind: "unsupported" };
3656
- const shell = env.SHELL;
3796
+ const shell = env2.SHELL;
3657
3797
  const cached = pathCache.get(`${shell}:${name}`);
3658
3798
  if (cached) return { kind: "path", path: cached, shell };
3659
3799
  const probe = await probeCommand(name, shell, opts);
@@ -3954,24 +4094,24 @@ var init_orchestrator = __esm({
3954
4094
  });
3955
4095
 
3956
4096
  // src/run-profiles.ts
3957
- import { loadProjectInfo as loadProjectInfo10 } from "@noir-ai/core";
4097
+ import { loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
3958
4098
  function loadRunConfig(root) {
3959
4099
  try {
3960
- return loadProjectInfo10(root).config;
4100
+ return loadProjectInfo11(root).config;
3961
4101
  } catch {
3962
4102
  return null;
3963
4103
  }
3964
4104
  }
3965
- function expandEnvVars(value, env) {
4105
+ function expandEnvVars(value, env2) {
3966
4106
  return value.replace(
3967
4107
  /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
3968
- (match, name) => env[name] !== void 0 ? env[name] : match
4108
+ (match, name) => env2[name] !== void 0 ? env2[name] : match
3969
4109
  );
3970
4110
  }
3971
- function resolveRunProfile(requested, config, env) {
4111
+ function resolveRunProfile(requested, config, env2) {
3972
4112
  const profiles = config.run?.profiles ?? {};
3973
4113
  const names = Object.keys(profiles);
3974
- const name = requested ?? env.NOIR_PROFILE ?? config.run?.defaultProfile;
4114
+ const name = requested ?? env2.NOIR_PROFILE ?? config.run?.defaultProfile;
3975
4115
  if (name === void 0 || name.length === 0) {
3976
4116
  return { ok: true, profile: {} };
3977
4117
  }
@@ -3986,7 +4126,7 @@ function resolveRunProfile(requested, config, env) {
3986
4126
  const envExpanded = {};
3987
4127
  if (profile.env) {
3988
4128
  for (const [k, v] of Object.entries(profile.env)) {
3989
- envExpanded[k] = v === null ? void 0 : expandEnvVars(v, env);
4129
+ envExpanded[k] = v === null ? void 0 : expandEnvVars(v, env2);
3990
4130
  }
3991
4131
  }
3992
4132
  return {
@@ -4010,10 +4150,24 @@ var init_run_profiles = __esm({
4010
4150
  });
4011
4151
 
4012
4152
  // src/commands/run.ts
4013
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
4153
+ import { existsSync as existsSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
4014
4154
  import { join as join4 } from "path";
4015
4155
  import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
4016
- import { parseConfig } from "@noir-ai/core";
4156
+ import { loadNoirEnv as loadNoirEnv2, NOIR_DIR as NOIR_DIR2, parseConfig } from "@noir-ai/core";
4157
+ function credentialNote(env2, sources) {
4158
+ const named = CREDENTIAL_ENV_VARS.filter((name) => {
4159
+ const value = env2[name];
4160
+ return value !== void 0 && value.length > 0;
4161
+ }).map(
4162
+ (name) => `${name} (from ${sources[name] === "file" ? `${NOIR_DIR2}/.env` : "the environment"})`
4163
+ );
4164
+ if (named.length === 0) return "";
4165
+ const one = named.length === 1;
4166
+ const subject = one ? `${named[0]} is set` : `${named.join(", ")} are set`;
4167
+ const pronoun = one ? "it" : "them";
4168
+ const verb = one ? "overrides" : "override";
4169
+ 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.`;
4170
+ }
4017
4171
  function formatUsage(u) {
4018
4172
  const cost = u.totalCostUsd > 0 ? ` $${u.totalCostUsd.toFixed(2)}` : "";
4019
4173
  const turns = u.numTurns > 0 ? ` \xB7 ${u.numTurns} turns` : "";
@@ -4037,13 +4191,21 @@ async function run(prompt, opts) {
4037
4191
  const usage = selectors.length > 0 ? `noir run ${selectors.join(" ")} <prompt>` : "noir run <prompt>";
4038
4192
  fail(EXIT.USAGE, `a prompt is required: \`${usage}\``, opts);
4039
4193
  }
4040
- const config = loadRunConfig(process.cwd()) ?? parseConfig({});
4194
+ const root = process.cwd();
4195
+ const envSources = loadNoirEnv2(root).sources;
4196
+ const config = loadRunConfig(root) ?? parseConfig({});
4041
4197
  const resolved = resolveRunProfile(opts.profile, config, process.env);
4042
4198
  if (!resolved.ok) fail(EXIT.USAGE, resolved.message, opts);
4043
4199
  const profile = resolved.profile;
4044
4200
  const customBinary = opts.command ?? profile.binary;
4045
4201
  const extraArgs = profile.args;
4046
- const env = profile.env ? mergeEnv(process.env, profile.env) : void 0;
4202
+ const env2 = profile.env ? mergeEnv(process.env, profile.env) : void 0;
4203
+ if (!existsSync2(join4(root, NOIR_DIR2, ".env"))) {
4204
+ log(
4205
+ `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).`,
4206
+ opts
4207
+ );
4208
+ }
4047
4209
  const transcriptLines = [];
4048
4210
  let result;
4049
4211
  try {
@@ -4052,7 +4214,7 @@ async function run(prompt, opts) {
4052
4214
  prompt,
4053
4215
  customBinary,
4054
4216
  extraArgs,
4055
- env,
4217
+ env: env2,
4056
4218
  onLine: (line) => transcriptLines.push(line),
4057
4219
  onEvent: (event) => streamEvent(event, opts)
4058
4220
  });
@@ -4073,9 +4235,7 @@ async function run(prompt, opts) {
4073
4235
  let message = `host '${binary}' failed (exit ${result.exitCode}): ${reason}`;
4074
4236
  if (isAuth) {
4075
4237
  message += ` Open a terminal and run \`${binary} /login\` (interactive-only \u2014 it cannot run inside \`noir run\`), then retry.`;
4076
- if (process.env.ANTHROPIC_API_KEY) {
4077
- 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.`;
4078
- }
4238
+ message += credentialNote(env2 ?? process.env, envSources);
4079
4239
  }
4080
4240
  message += ` If you use another profile, pass \`--command <binary>\` or define a run profile under run.profiles. transcript: ${transcript}`;
4081
4241
  fail(EXIT.ERROR, message, opts);
@@ -4115,18 +4275,24 @@ function writeTranscript(host, lines) {
4115
4275
  }
4116
4276
  return file;
4117
4277
  }
4278
+ var CREDENTIAL_ENV_VARS;
4118
4279
  var init_run = __esm({
4119
4280
  "src/commands/run.ts"() {
4120
4281
  "use strict";
4121
4282
  init_orchestrator();
4122
4283
  init_output();
4123
4284
  init_run_profiles();
4285
+ CREDENTIAL_ENV_VARS = [
4286
+ "ANTHROPIC_API_KEY",
4287
+ "ANTHROPIC_AUTH_TOKEN",
4288
+ "ANTHROPIC_BASE_URL"
4289
+ ];
4124
4290
  }
4125
4291
  });
4126
4292
 
4127
4293
  // src/commands/skills.ts
4128
4294
  import { resolveAdapter as resolveAdapter3 } from "@noir-ai/adapters";
4129
- import { loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
4295
+ import { loadProjectInfo as loadProjectInfo12 } from "@noir-ai/core";
4130
4296
  import {
4131
4297
  buildRegistry,
4132
4298
  discoverAll,
@@ -4194,7 +4360,7 @@ async function skillsSync(opts) {
4194
4360
  const root = process.cwd();
4195
4361
  let project;
4196
4362
  try {
4197
- project = loadProjectInfo11(root);
4363
+ project = loadProjectInfo12(root);
4198
4364
  } catch {
4199
4365
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
4200
4366
  }
@@ -4720,7 +4886,7 @@ var init_dedup_write = __esm({
4720
4886
 
4721
4887
  // src/init.ts
4722
4888
  import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
4723
- import { loadProjectInfo as loadProjectInfo12 } from "@noir-ai/core";
4889
+ import { loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
4724
4890
  import { scaffold } from "@noir-ai/create";
4725
4891
  import { emitSkillsToDir as emitSkillsToDir2 } from "@noir-ai/skills";
4726
4892
  async function init(root, opts) {
@@ -4749,7 +4915,7 @@ async function init(root, opts) {
4749
4915
  await emitHostSkills(root, host, conflictOpts, interactive);
4750
4916
  let projectInfo;
4751
4917
  try {
4752
- projectInfo = loadProjectInfo12(root);
4918
+ projectInfo = loadProjectInfo13(root);
4753
4919
  } catch {
4754
4920
  projectInfo = void 0;
4755
4921
  }
@@ -4768,7 +4934,10 @@ function reportPlannedWrites(res) {
4768
4934
  log("Dry run \u2014 no files were written.");
4769
4935
  if (res.written.length > 0) {
4770
4936
  log("Planned writes:");
4771
- for (const p of res.written) log(` ${p}`);
4937
+ for (const p of res.written) {
4938
+ const mode = res.fileModes?.[p];
4939
+ log(mode === void 0 ? ` ${p}` : ` ${p} (mode ${mode.toString(8).padStart(3, "0")})`);
4940
+ }
4772
4941
  }
4773
4942
  if (res.skipped.length > 0) {
4774
4943
  log("Would leave as-is (already present):");
@@ -4836,11 +5005,11 @@ var init_init = __esm({
4836
5005
  });
4837
5006
 
4838
5007
  // src/serve.ts
4839
- import { applyNoirEnv, loadProjectInfo as loadProjectInfo13 } from "@noir-ai/core";
5008
+ import { applyNoirEnv, loadProjectInfo as loadProjectInfo14 } from "@noir-ai/core";
4840
5009
  import { ensureDaemonRunning as ensureDaemonRunning2, startStdioServer } from "@noir-ai/daemon";
4841
5010
  async function serve(opts) {
4842
5011
  applyNoirEnv(process.cwd());
4843
- const project = loadProjectInfo13(process.cwd());
5012
+ const project = loadProjectInfo14(process.cwd());
4844
5013
  if (opts.stdio) {
4845
5014
  await startStdioServer({ project, transport: "stdio", daemon: false });
4846
5015
  return;
@@ -4848,10 +5017,11 @@ async function serve(opts) {
4848
5017
  try {
4849
5018
  const { url } = await ensureDaemonRunning2({
4850
5019
  project,
4851
- idleTimeoutSec: project.config.daemon.idleTimeoutSec
5020
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec,
5021
+ ...project.config.daemon.port !== void 0 ? { port: project.config.daemon.port } : {}
4852
5022
  });
4853
5023
  process.stderr.write(
4854
- `Noir daemon available at ${url}. (For HTTP clients, use this URL in .mcp.json.)
5024
+ `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\`).
4855
5025
  `
4856
5026
  );
4857
5027
  } catch (err) {
@@ -4875,7 +5045,7 @@ __export(create_exports, {
4875
5045
  });
4876
5046
  import { resolve } from "path";
4877
5047
  import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
4878
- import { loadProjectInfo as loadProjectInfo14 } from "@noir-ai/core";
5048
+ import { loadProjectInfo as loadProjectInfo15 } from "@noir-ai/core";
4879
5049
  import { scaffold as scaffold2 } from "@noir-ai/create";
4880
5050
  import { emitSkillsToDir as emitSkillsToDir3 } from "@noir-ai/skills";
4881
5051
  async function create(dir, opts) {
@@ -4926,7 +5096,7 @@ async function create(dir, opts) {
4926
5096
  }
4927
5097
  let projectInfo;
4928
5098
  try {
4929
- projectInfo = loadProjectInfo14(root);
5099
+ projectInfo = loadProjectInfo15(root);
4930
5100
  } catch {
4931
5101
  projectInfo = void 0;
4932
5102
  }
@@ -4952,9 +5122,9 @@ var sync_exports = {};
4952
5122
  __export(sync_exports, {
4953
5123
  sync: () => sync
4954
5124
  });
4955
- import { existsSync as existsSync2 } from "fs";
5125
+ import { existsSync as existsSync3 } from "fs";
4956
5126
  import { resolveAdapter as resolveAdapter6 } from "@noir-ai/adapters";
4957
- import { loadProjectInfo as loadProjectInfo15, paths } from "@noir-ai/core";
5127
+ import { loadProjectInfo as loadProjectInfo16, paths } from "@noir-ai/core";
4958
5128
  import { scaffold as scaffold3 } from "@noir-ai/create";
4959
5129
  import { emitSkillsToDir as emitSkillsToDir4 } from "@noir-ai/skills";
4960
5130
  async function sync(root, opts = {}) {
@@ -4963,7 +5133,7 @@ async function sync(root, opts = {}) {
4963
5133
  const conflictOpts = buildConflictOpts({ force: opts.force, interactive });
4964
5134
  let projectInfo;
4965
5135
  try {
4966
- projectInfo = loadProjectInfo15(root);
5136
+ projectInfo = loadProjectInfo16(root);
4967
5137
  } catch {
4968
5138
  projectInfo = void 0;
4969
5139
  }
@@ -5021,9 +5191,9 @@ async function sync(root, opts = {}) {
5021
5191
  }
5022
5192
  function resolveSyncHost(root, opts) {
5023
5193
  if (opts.host !== void 0) return opts.host;
5024
- if (!existsSync2(paths.config(root))) return "claude";
5194
+ if (!existsSync3(paths.config(root))) return "claude";
5025
5195
  try {
5026
- return loadProjectInfo15(root).config.host;
5196
+ return loadProjectInfo16(root).config.host;
5027
5197
  } catch {
5028
5198
  return "claude";
5029
5199
  }
@@ -5140,6 +5310,7 @@ function createProgram() {
5140
5310
  );
5141
5311
  }
5142
5312
  }
5313
+ if (actionCmd.name() === "env") captureAmbientEnv();
5143
5314
  applyNoirEnv2(process.cwd());
5144
5315
  const nonInteractive = opts.json === true || opts.input === false;
5145
5316
  if (nonInteractive) process.env.NOIR_NON_INTERACTIVE = "1";
@@ -5282,6 +5453,9 @@ function createProgram() {
5282
5453
  daemonGrp.command("status").description("report daemon pid/uptime/mode (exit 4 if not running)").action(async (...args) => {
5283
5454
  await daemonStatus(toCliOptions(trailingCmd(args).optsWithGlobals()));
5284
5455
  });
5456
+ daemonGrp.command("token").description("print the daemon bearer token to stdout (for a host headersHelper)").action(async (...args) => {
5457
+ await daemonToken(toCliOptions(trailingCmd(args).optsWithGlobals()));
5458
+ });
5285
5459
  daemonGrp.command("restart").description("stop then start the daemon").option("--detach", "run the daemon in the background and exit").action(async (...args) => {
5286
5460
  const cmd = trailingCmd(args);
5287
5461
  const g = cmd.optsWithGlobals();
@@ -5291,8 +5465,19 @@ function createProgram() {
5291
5465
  });
5292
5466
  });
5293
5467
  daemonGrp.action(() => {
5294
- throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|join|stop|status|restart");
5468
+ throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|join|stop|status|restart|token");
5295
5469
  });
5470
+ daemonGrp.addHelpText(
5471
+ "after",
5472
+ [
5473
+ "",
5474
+ "Shared workspaces (two repos, one product \u2014 e.g. backend + frontend):",
5475
+ " noir daemon start --workspace <name> --detach found a workspace from this repo",
5476
+ " noir daemon join <name> join it from the other repo",
5477
+ "",
5478
+ "Guide: docs/how-to/shared-workspaces.md"
5479
+ ].join("\n")
5480
+ );
5296
5481
  const workspaceGrp = program2.command("workspace").description("shared cross-repo workspaces");
5297
5482
  workspaceGrp.command("list").description("list workspaces on this machine").action(async (...args) => {
5298
5483
  await workspaceList(toCliOptions(trailingCmd(args).optsWithGlobals()));
@@ -5321,6 +5506,17 @@ function createProgram() {
5321
5506
  ...name !== void 0 ? { name } : {}
5322
5507
  });
5323
5508
  });
5509
+ workspaceGrp.addHelpText(
5510
+ "after",
5511
+ [
5512
+ "",
5513
+ "This group only inspects and leaves workspaces. To CREATE or JOIN one:",
5514
+ " noir daemon start --workspace <name> --detach found it (from the first repo)",
5515
+ " noir daemon join <name> join it (from the second repo)",
5516
+ "",
5517
+ "Guide: docs/how-to/shared-workspaces.md"
5518
+ ].join("\n")
5519
+ );
5324
5520
  workspaceGrp.action(() => {
5325
5521
  throw new NoirCliError(EXIT.USAGE, "Usage: noir workspace list|status|leave|stop");
5326
5522
  });
@@ -5335,6 +5531,9 @@ function createProgram() {
5335
5531
  program2.command("status").description("project + daemon + workflow + store snapshot").action(async (...args) => {
5336
5532
  await status(toStatusOptions(actionGlobals(args)));
5337
5533
  });
5534
+ program2.command("env").description("which configuration is in effect and where each value comes from").action(async (...args) => {
5535
+ await env(toCliOptions(actionGlobals(args)));
5536
+ });
5338
5537
  const contextGrp = program2.command("context").description("context engine");
5339
5538
  contextGrp.command("search").description("hybrid search over the indexed context").argument("<query>", "search query").option("--limit <n>", "max results", "10").action(async (...args) => {
5340
5539
  const cmd = trailingCmd(args);
@@ -5720,6 +5919,7 @@ var init_bin = __esm({
5720
5919
  init_context();
5721
5920
  init_daemon();
5722
5921
  init_doctor();
5922
+ init_env();
5723
5923
  init_handoff();
5724
5924
  init_home();
5725
5925
  init_memory();
@@ -5769,7 +5969,7 @@ var init_bin = __esm({
5769
5969
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
5770
5970
  import { ContextEngine, createEmbedFn, resolveEmbedderConfig } from "@noir-ai/context";
5771
5971
  import {
5772
- loadProjectInfo as loadProjectInfo16,
5972
+ loadProjectInfo as loadProjectInfo17,
5773
5973
  NOIR_VERSION as NOIR_VERSION5,
5774
5974
  readWorkspaceMarker as readWorkspaceMarker2,
5775
5975
  workspaceStoreDbPath
@@ -5777,9 +5977,11 @@ import {
5777
5977
  import {
5778
5978
  ensureDaemonRunning as ensureDaemonRunning3,
5779
5979
  pidAlive as pidAlive3,
5780
- readDaemonRecord as readDaemonRecord2,
5980
+ readDaemonToken as readDaemonToken2,
5981
+ readProjectDaemonRecord as readProjectDaemonRecord2,
5781
5982
  readWorkspaceDaemonRecord as readWorkspaceDaemonRecord2,
5782
- resolveGateConfig
5983
+ resolveGateConfig,
5984
+ tokenPath
5783
5985
  } from "@noir-ai/daemon";
5784
5986
  import { createMemoryEngine } from "@noir-ai/memory";
5785
5987
  import { openStore } from "@noir-ai/store";
@@ -5800,7 +6002,18 @@ function failDaemonDown(opts, cause) {
5800
6002
  fail(EXIT.DAEMON_DOWN, DAEMON_DOWN_HINT, opts);
5801
6003
  }
5802
6004
  async function probeDaemon(opts = {}) {
5803
- const rec = readDaemonRecord2();
6005
+ const expectedProject = opts.project?.id ?? (() => {
6006
+ try {
6007
+ return loadProjectInfo17(process.cwd()).id;
6008
+ } catch {
6009
+ return void 0;
6010
+ }
6011
+ })();
6012
+ if (expectedProject === void 0) {
6013
+ if (opts.verbose) process.stderr.write("noir: daemon probe: no project (uninitialized)\n");
6014
+ return { running: false };
6015
+ }
6016
+ const rec = readProjectDaemonRecord2(expectedProject);
5804
6017
  if (!rec) {
5805
6018
  if (opts.verbose) process.stderr.write("noir: daemon probe: no daemon record\n");
5806
6019
  return { running: false };
@@ -5821,19 +6034,10 @@ async function probeDaemon(opts = {}) {
5821
6034
  }
5822
6035
  const body = await res.json().catch(() => null);
5823
6036
  const pidOk = typeof body?.pid === "number" && body.pid === rec.pid;
5824
- const expectedProject = opts.project?.id ?? (() => {
5825
- try {
5826
- return loadProjectInfo16(process.cwd()).id;
5827
- } catch {
5828
- return void 0;
5829
- }
5830
- })();
5831
- const projectOk = expectedProject === void 0 || body?.projectId !== void 0 && body.projectId === expectedProject;
5832
- if (body?.ok !== true || !pidOk || !projectOk) {
6037
+ if (body?.ok !== true || !pidOk) {
5833
6038
  if (opts.verbose)
5834
6039
  process.stderr.write(
5835
- !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
5836
- ` : "noir: daemon probe: /health body not ok\n"
6040
+ !pidOk ? "noir: daemon probe: pid missing/mismatched (foreign process on recorded port)\n" : "noir: daemon probe: /health body not ok\n"
5837
6041
  );
5838
6042
  return { running: false };
5839
6043
  }
@@ -5853,25 +6057,112 @@ async function probeDaemon(opts = {}) {
5853
6057
  async function resolveDaemon(opts) {
5854
6058
  let project;
5855
6059
  try {
5856
- project = opts.project ?? loadProjectInfo16(process.cwd());
6060
+ project = opts.project ?? loadProjectInfo17(process.cwd());
5857
6061
  } catch {
5858
6062
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
5859
6063
  }
5860
6064
  const idleTimeoutSec = opts.idleTimeoutSec ?? project.config.daemon.idleTimeoutSec;
6065
+ const preferred = project.config.daemon.port;
6066
+ if (preferred !== void 0 && preferred !== 0) {
6067
+ const direct = `http://127.0.0.1:${preferred}/mcp`;
6068
+ const probe = newClient();
6069
+ let probeTimer;
6070
+ try {
6071
+ await Promise.race([
6072
+ probe.connect(httpTransport(direct, project.id)),
6073
+ new Promise((_, reject) => {
6074
+ probeTimer = setTimeout(
6075
+ () => reject(new Error(`no response within ${PROBE_TIMEOUT_MS}ms`)),
6076
+ PROBE_TIMEOUT_MS
6077
+ );
6078
+ })
6079
+ ]);
6080
+ return { client: probe, stop: async () => {
6081
+ } };
6082
+ } catch (err) {
6083
+ if (opts.verbose) {
6084
+ process.stderr.write(
6085
+ `noir: no daemon at the configured port ${preferred} (${describeCause(err)})
6086
+ `
6087
+ );
6088
+ }
6089
+ await probe.close().catch(() => {
6090
+ });
6091
+ } finally {
6092
+ if (probeTimer !== void 0) clearTimeout(probeTimer);
6093
+ }
6094
+ }
6095
+ let url;
6096
+ let stop;
5861
6097
  try {
5862
- const ensured = await ensureDaemonRunning3({ project, idleTimeoutSec });
5863
- return { url: ensured.url, stop: ensured.stop };
6098
+ const ensured = await ensureDaemonRunning3({
6099
+ project,
6100
+ idleTimeoutSec,
6101
+ ...preferred !== void 0 ? { port: preferred } : {}
6102
+ });
6103
+ url = ensured.url;
6104
+ stop = ensured.stop;
5864
6105
  } catch (err) {
5865
6106
  failDaemonDown(opts, err);
5866
6107
  }
6108
+ const client = newClient();
6109
+ try {
6110
+ await connectClient(client, url, opts, project.id);
6111
+ } catch (err) {
6112
+ await client.close().catch(() => {
6113
+ });
6114
+ await stop().catch(() => {
6115
+ });
6116
+ throw err;
6117
+ }
6118
+ return { client, stop };
5867
6119
  }
5868
- async function connectClient(client, url, opts) {
6120
+ function httpTransport(url, scopeKey) {
6121
+ const token = scopeKey === null ? null : readDaemonToken2(scopeKey);
6122
+ return new StreamableHTTPClientTransport(
6123
+ new URL(url),
6124
+ // Verified present: StreamableHTTPClientTransportOptions.requestInit?: RequestInit
6125
+ token ? { requestInit: { headers: { Authorization: `Bearer ${token}` } } } : {}
6126
+ );
6127
+ }
6128
+ function callerScopeKey(opts) {
6129
+ try {
6130
+ return (opts.project ?? loadProjectInfo17(process.cwd())).id;
6131
+ } catch {
6132
+ return null;
6133
+ }
6134
+ }
6135
+ function newClient() {
6136
+ return new Client(
6137
+ { name: "noir-cli", version: NOIR_VERSION5 },
6138
+ { versionNegotiation: { mode: "auto" } }
6139
+ );
6140
+ }
6141
+ async function connectRaw(client, url, scopeKey) {
6142
+ await client.connect(httpTransport(url, scopeKey));
6143
+ }
6144
+ async function connectClient(client, url, opts, scopeKey) {
5869
6145
  try {
5870
- await client.connect(new StreamableHTTPClientTransport(new URL(url)));
6146
+ await connectRaw(client, url, scopeKey);
5871
6147
  } catch (err) {
5872
6148
  failDaemonDown(opts, err);
5873
6149
  }
5874
6150
  }
6151
+ function failWorkspaceConnect(opts, routing, cause) {
6152
+ const text = String(cause).toLowerCase();
6153
+ if (text.includes("401") || text.includes("unauthorized")) {
6154
+ if (opts.verbose) {
6155
+ process.stderr.write(`noir: daemon transport detail: ${describeCause(cause)}
6156
+ `);
6157
+ }
6158
+ fail(
6159
+ EXIT.DAEMON_DOWN,
6160
+ `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.`,
6161
+ opts
6162
+ );
6163
+ }
6164
+ failDaemonDown(opts, cause);
6165
+ }
5875
6166
  async function callToolParse(client, opts, name, args) {
5876
6167
  const result = await client.callTool({ name, arguments: args }).catch((err) => failDaemonDown(opts, err));
5877
6168
  const block = result.content?.[0];
@@ -5902,21 +6193,12 @@ function buildCaller(client, opts) {
5902
6193
  };
5903
6194
  }
5904
6195
  async function withDaemon(opts, fn) {
5905
- const { url, stop } = await resolveDaemon(opts);
5906
- let client;
6196
+ const { client, stop } = await resolveDaemon(opts);
5907
6197
  try {
5908
- client = new Client(
5909
- { name: "noir-cli", version: NOIR_VERSION5 },
5910
- { versionNegotiation: { mode: "auto" } }
5911
- );
5912
- const connected = client;
5913
- await connectClient(connected, url, opts);
5914
- return await fn(buildCaller(connected, opts));
6198
+ return await fn(buildCaller(client, opts));
5915
6199
  } finally {
5916
- if (client) {
5917
- await client.close().catch(() => {
5918
- });
5919
- }
6200
+ await client.close().catch(() => {
6201
+ });
5920
6202
  await stop().catch(() => {
5921
6203
  });
5922
6204
  }
@@ -5927,12 +6209,9 @@ async function withRunningDaemon(opts, fn, probe) {
5927
6209
  const url = `http://127.0.0.1:${p.port}/mcp`;
5928
6210
  let client;
5929
6211
  try {
5930
- client = new Client(
5931
- { name: "noir-cli", version: NOIR_VERSION5 },
5932
- { versionNegotiation: { mode: "auto" } }
5933
- );
6212
+ client = newClient();
5934
6213
  const connected = client;
5935
- await connected.connect(new StreamableHTTPClientTransport(new URL(url)));
6214
+ await connected.connect(httpTransport(url, callerScopeKey(opts)));
5936
6215
  return await fn(buildCaller(connected, opts));
5937
6216
  } catch {
5938
6217
  return null;
@@ -5949,7 +6228,7 @@ async function callDaemonTool(opts, name, args = {}) {
5949
6228
  async function withInProcessRead(opts, fn) {
5950
6229
  let project;
5951
6230
  try {
5952
- project = opts.project ?? loadProjectInfo16(process.cwd());
6231
+ project = opts.project ?? loadProjectInfo17(process.cwd());
5953
6232
  } catch {
5954
6233
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
5955
6234
  }
@@ -5996,7 +6275,7 @@ function workspaceRoutingTarget(root) {
5996
6275
  const name = readWorkspaceMarker2(root);
5997
6276
  if (name === null) return null;
5998
6277
  try {
5999
- return { name, projectId: loadProjectInfo16(root).id };
6278
+ return { name, projectId: loadProjectInfo17(root).id };
6000
6279
  } catch {
6001
6280
  return null;
6002
6281
  }
@@ -6030,12 +6309,11 @@ async function withWorkspaceDaemon(opts, routing, fn) {
6030
6309
  const url = `http://127.0.0.1:${probe.port}/mcp?p=${routing.projectId}`;
6031
6310
  let client;
6032
6311
  try {
6033
- client = new Client(
6034
- { name: "noir-cli", version: NOIR_VERSION5 },
6035
- { versionNegotiation: { mode: "auto" } }
6036
- );
6312
+ client = newClient();
6037
6313
  const connected = client;
6038
- await connectClient(connected, url, opts);
6314
+ await connectRaw(connected, url, routing.name).catch(
6315
+ (err) => failWorkspaceConnect(opts, routing, err)
6316
+ );
6039
6317
  return await fn(buildCaller(connected, opts));
6040
6318
  } finally {
6041
6319
  if (client) {
@@ -6089,22 +6367,23 @@ var init_daemon_client = __esm({
6089
6367
 
6090
6368
  // src/commands/doctor.ts
6091
6369
  import { spawnSync as spawnSync3 } from "child_process";
6092
- import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync } from "fs";
6370
+ import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync } from "fs";
6093
6371
  import { dirname, join as join6, relative } from "path";
6094
6372
  import { fileURLToPath } from "url";
6095
6373
  import { resolveAdapter as resolveAdapter7 } from "@noir-ai/adapters";
6096
6374
  import {
6097
6375
  detectActiveMethod as detectActiveMethod4,
6098
6376
  latestVersionFromCache as latestVersionFromCache2,
6099
- loadProjectInfo as loadProjectInfo17,
6377
+ loadNoirEnv as loadNoirEnv3,
6378
+ loadProjectInfo as loadProjectInfo18,
6100
6379
  NOIR_VERSION as NOIR_VERSION6,
6101
6380
  paths as paths2,
6102
6381
  readInstallRecord as readInstallRecord4,
6103
6382
  readUpdateCache as readUpdateCache3
6104
6383
  } from "@noir-ai/core";
6105
6384
  import { CURRENT_SCAFFOLD_VERSION, readScaffoldVersion } from "@noir-ai/create";
6106
- import { pidAlive as pidAlive4, readDaemonRecord as readDaemonRecord3 } from "@noir-ai/daemon";
6107
- import { resolveModelConfig } from "@noir-ai/model";
6385
+ import { pidAlive as pidAlive4, readProjectDaemonRecord as readProjectDaemonRecord3 } from "@noir-ai/daemon";
6386
+ import { resolveModelConfig as resolveModelConfig2 } from "@noir-ai/model";
6108
6387
  async function probeOnnx() {
6109
6388
  try {
6110
6389
  const spec = "onnxruntime-node";
@@ -6140,7 +6419,7 @@ async function checkRuntime(checks) {
6140
6419
  }
6141
6420
  function checkConfig(root) {
6142
6421
  const configPath = paths2.config(root);
6143
- if (!existsSync3(configPath)) {
6422
+ if (!existsSync4(configPath)) {
6144
6423
  return {
6145
6424
  project: void 0,
6146
6425
  result: {
@@ -6151,7 +6430,7 @@ function checkConfig(root) {
6151
6430
  };
6152
6431
  }
6153
6432
  try {
6154
- const project = loadProjectInfo17(root);
6433
+ const project = loadProjectInfo18(root);
6155
6434
  return {
6156
6435
  project,
6157
6436
  result: {
@@ -6167,8 +6446,16 @@ function checkConfig(root) {
6167
6446
  };
6168
6447
  }
6169
6448
  }
6170
- async function checkDaemon(checks) {
6171
- const rec = readDaemonRecord3();
6449
+ async function checkDaemon(checks, project) {
6450
+ if (project === void 0) {
6451
+ checks.push({
6452
+ name: "daemon",
6453
+ status: "warn",
6454
+ detail: "skipped \u2014 project not initialized"
6455
+ });
6456
+ return;
6457
+ }
6458
+ const rec = readProjectDaemonRecord3(project.id);
6172
6459
  if (!rec) {
6173
6460
  checks.push({
6174
6461
  name: "daemon",
@@ -6232,7 +6519,7 @@ async function checkStore(checks, project, root) {
6232
6519
  return;
6233
6520
  }
6234
6521
  const dbPath = paths2.storeDb(root, project.id);
6235
- if (!existsSync3(dbPath)) {
6522
+ if (!existsSync4(dbPath)) {
6236
6523
  checks.push({
6237
6524
  name: "store",
6238
6525
  status: "warn",
@@ -6260,7 +6547,7 @@ async function checkStore(checks, project, root) {
6260
6547
  }
6261
6548
  }
6262
6549
  }
6263
- function checkNoirEnv(checks, root) {
6550
+ function checkNoirEnv(checks, root, loaded) {
6264
6551
  const path = join6(paths2.noirDir(root), ".env");
6265
6552
  let st;
6266
6553
  try {
@@ -6268,6 +6555,14 @@ function checkNoirEnv(checks, root) {
6268
6555
  } catch {
6269
6556
  return;
6270
6557
  }
6558
+ if (loaded.warnings.some((w) => w.startsWith(ENV_REFUSAL_PREFIX))) {
6559
+ checks.push({
6560
+ name: "noir-env",
6561
+ status: "warn",
6562
+ 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\`.`
6563
+ });
6564
+ return;
6565
+ }
6271
6566
  const mode = st.mode & 511;
6272
6567
  const tooOpen = (st.mode & 63) !== 0;
6273
6568
  checks.push(
@@ -6277,6 +6572,10 @@ function checkNoirEnv(checks, root) {
6277
6572
  detail: `${path} is ${mode.toString(8)} \u2014 readable by others; run chmod 600`
6278
6573
  } : { name: "noir-env", status: "ok", detail: `${path} permissions ok (${mode.toString(8)})` }
6279
6574
  );
6575
+ for (const [key, source] of Object.entries(loaded.sources)) {
6576
+ if (source !== "file") continue;
6577
+ checks.push({ name: `noir-env:${key}`, status: "ok", detail: "from .noir/.env" });
6578
+ }
6280
6579
  }
6281
6580
  function checkEmbedder(checks, project, vecOk) {
6282
6581
  if (!project) {
@@ -6308,12 +6607,12 @@ function checkEmbedder(checks, project, vecOk) {
6308
6607
  detail: `${kind}/${model} (${emb.dim}-dim) \xB7 vec backend available`
6309
6608
  });
6310
6609
  }
6311
- function checkProvider(checks, project) {
6610
+ function checkProvider(checks, project, sources) {
6312
6611
  if (!project) {
6313
6612
  checks.push({ name: "provider", status: "warn", detail: "skipped \u2014 not initialized" });
6314
6613
  return;
6315
6614
  }
6316
- const resolved = resolveModelConfig(project.config.model);
6615
+ const resolved = resolveModelConfig2(project.config.model);
6317
6616
  const names = Object.keys(resolved.providers);
6318
6617
  if (names.length === 0) {
6319
6618
  checks.push({
@@ -6328,7 +6627,8 @@ function checkProvider(checks, project) {
6328
6627
  for (const name of names) {
6329
6628
  const p = resolved.providers[name];
6330
6629
  if (!p) continue;
6331
- const key = p.hasKey ? "key present" : p.apiKeyEnv ? `missing ${p.apiKeyEnv}` : "anonymous";
6630
+ const fromFile = p.apiKeyEnv !== void 0 && sources[p.apiKeyEnv] === "file";
6631
+ const key = p.hasKey ? fromFile ? "key present (from .noir/.env)" : "key present" : p.apiKeyEnv ? `missing ${p.apiKeyEnv}` : "anonymous";
6332
6632
  if (!p.hasKey && p.apiKeyEnv) missing = true;
6333
6633
  parts.push(`${name}/${p.model ?? "?"} (${key})`);
6334
6634
  }
@@ -6371,7 +6671,7 @@ function checkRulesMdBudget(checks, root, project) {
6371
6671
  return null;
6372
6672
  }
6373
6673
  const rulesPath = paths2.rulesMd(root);
6374
- if (!existsSync3(rulesPath)) {
6674
+ if (!existsSync4(rulesPath)) {
6375
6675
  checks.push({
6376
6676
  name: "rules budget",
6377
6677
  status: "ok",
@@ -6435,7 +6735,7 @@ function checkHostArtifacts(checks, root, project) {
6435
6735
  if (host === "claude") expected.push("CLAUDE.md");
6436
6736
  else if (host === "gemini") expected.push("GEMINI.md");
6437
6737
  expected.push(relOr(adapter.mcpConfigPath?.(ectx) ?? ".mcp.json", root));
6438
- const missing = expected.filter((p) => !existsSync3(joinRoot(root, p)));
6738
+ const missing = expected.filter((p) => !existsSync4(joinRoot(root, p)));
6439
6739
  const status2 = missing.length === 0 ? "ok" : "warn";
6440
6740
  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)`;
6441
6741
  checks.push({ name: "host artifacts", status: status2, detail });
@@ -6452,7 +6752,7 @@ function checkNestedNoir(checks, root) {
6452
6752
  ".noir/.npmignore",
6453
6753
  ".noir/.prettierignore"
6454
6754
  ];
6455
- const found = candidates.filter((rel) => existsSync3(join6(root, rel)));
6755
+ const found = candidates.filter((rel) => existsSync4(join6(root, rel)));
6456
6756
  const detected = found.length > 0;
6457
6757
  checks.push({
6458
6758
  name: "nested .noir",
@@ -6466,7 +6766,7 @@ function collectDedupCandidates(root) {
6466
6766
  const out = [];
6467
6767
  for (const rel of rels) {
6468
6768
  const abs = join6(root, rel);
6469
- if (!existsSync3(abs)) continue;
6769
+ if (!existsSync4(abs)) continue;
6470
6770
  let text;
6471
6771
  try {
6472
6772
  text = readFileSync4(abs, "utf8");
@@ -6527,12 +6827,12 @@ function resolveWorkspacePackagesDir() {
6527
6827
  let here = dirname(fileURLToPath(import.meta.url));
6528
6828
  for (let i = 0; i < 8; i++) {
6529
6829
  const pkgPath = join6(here, "package.json");
6530
- if (existsSync3(pkgPath)) {
6830
+ if (existsSync4(pkgPath)) {
6531
6831
  try {
6532
6832
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
6533
6833
  if (pkg.name === "@noir-ai/cli") {
6534
6834
  const packagesDir = dirname(here);
6535
- if (existsSync3(join6(dirname(packagesDir), "pnpm-workspace.yaml"))) {
6835
+ if (existsSync4(join6(dirname(packagesDir), "pnpm-workspace.yaml"))) {
6536
6836
  return packagesDir;
6537
6837
  }
6538
6838
  return null;
@@ -6591,7 +6891,7 @@ function checkPublish(checks, packagesDir, opts = {}) {
6591
6891
  let cliDir = null;
6592
6892
  for (const name of entries) {
6593
6893
  const pkgPath = join6(packagesDir, name, "package.json");
6594
- if (!existsSync3(pkgPath)) continue;
6894
+ if (!existsSync4(pkgPath)) continue;
6595
6895
  let pkg;
6596
6896
  try {
6597
6897
  pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
@@ -6691,12 +6991,13 @@ async function doctor(opts = {}) {
6691
6991
  await checkRuntime(checks);
6692
6992
  const { project, result: configResult } = checkConfig(root);
6693
6993
  checks.push(configResult);
6694
- await checkDaemon(checks);
6994
+ await checkDaemon(checks, project);
6695
6995
  const { vecOk } = await checkNativeDeps(checks);
6696
6996
  await checkStore(checks, project, root);
6697
6997
  checkEmbedder(checks, project, vecOk);
6698
- checkProvider(checks, project);
6699
- checkNoirEnv(checks, root);
6998
+ const noirEnv = loadNoirEnv3(root);
6999
+ checkProvider(checks, project, noirEnv.sources);
7000
+ checkNoirEnv(checks, root, noirEnv);
6700
7001
  const scaffold4 = checkScaffoldVersion(checks, root);
6701
7002
  const rules = checkRulesMdBudget(checks, root, project);
6702
7003
  const host = checkHostArtifacts(checks, root, project);
@@ -6739,13 +7040,14 @@ async function doctor(opts = {}) {
6739
7040
  );
6740
7041
  }
6741
7042
  }
6742
- var SEMVER_ISH;
7043
+ var ENV_REFUSAL_PREFIX, SEMVER_ISH;
6743
7044
  var init_doctor = __esm({
6744
7045
  "src/commands/doctor.ts"() {
6745
7046
  "use strict";
6746
7047
  init_daemon_client();
6747
7048
  init_output();
6748
7049
  init_theme();
7050
+ ENV_REFUSAL_PREFIX = ".noir/.env: refusing to load";
6749
7051
  SEMVER_ISH = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
6750
7052
  }
6751
7053
  });