@noir-ai/cli 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,13 +28,13 @@ import {
28
28
  } from "./chunk-ESZTOQEC.js";
29
29
 
30
30
  // src/commands/status.ts
31
- import { loadProjectInfo as loadProjectInfo13, NOIR_VERSION as NOIR_VERSION5 } from "@noir-ai/core";
31
+ import { loadProjectInfo as loadProjectInfo14, NOIR_VERSION as NOIR_VERSION5 } from "@noir-ai/core";
32
32
 
33
33
  // src/daemon-client.ts
34
34
  import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
35
35
  import { ContextEngine, createEmbedFn, resolveEmbedderConfig } from "@noir-ai/context";
36
36
  import {
37
- loadProjectInfo as loadProjectInfo12,
37
+ loadProjectInfo as loadProjectInfo13,
38
38
  NOIR_VERSION as NOIR_VERSION4,
39
39
  readWorkspaceMarker as readWorkspaceMarker2,
40
40
  workspaceStoreDbPath
@@ -42,9 +42,11 @@ import {
42
42
  import {
43
43
  ensureDaemonRunning as ensureDaemonRunning3,
44
44
  pidAlive as pidAlive4,
45
- readDaemonRecord as readDaemonRecord3,
45
+ readDaemonToken as readDaemonToken2,
46
+ readProjectDaemonRecord as readProjectDaemonRecord3,
46
47
  readWorkspaceDaemonRecord as readWorkspaceDaemonRecord2,
47
- resolveGateConfig
48
+ resolveGateConfig,
49
+ tokenPath
48
50
  } from "@noir-ai/daemon";
49
51
  import { createMemoryEngine } from "@noir-ai/memory";
50
52
  import { openStore } from "@noir-ai/store";
@@ -225,10 +227,11 @@ function renderStatus(data, opts) {
225
227
  // src/commands/daemon.ts
226
228
  import { loadProjectInfo as loadProjectInfo2 } from "@noir-ai/core";
227
229
  import {
228
- clearDaemonRecord,
230
+ clearProjectDaemonRecord,
229
231
  ensureDaemonRunning,
230
232
  pidAlive as pidAlive2,
231
- readDaemonRecord,
233
+ readDaemonToken,
234
+ readProjectDaemonRecord,
232
235
  spawnDetachedDaemon
233
236
  } from "@noir-ai/daemon";
234
237
 
@@ -584,7 +587,8 @@ async function daemonStart(opts) {
584
587
  if (opts.detachChild === true) {
585
588
  const child = await ensureDaemonRunning({
586
589
  project,
587
- idleTimeoutSec: project.config.daemon.idleTimeoutSec
590
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec,
591
+ ...project.config.daemon.port !== void 0 ? { port: project.config.daemon.port } : {}
588
592
  });
589
593
  if (child.started) {
590
594
  if (opts.json === true) {
@@ -607,7 +611,7 @@ async function daemonStart(opts) {
607
611
  return;
608
612
  }
609
613
  if (opts.detach === true) {
610
- const existing = readDaemonRecord();
614
+ const existing = readProjectDaemonRecord(project.id);
611
615
  if (existing && pidAlive2(existing.pid) && await isHealthy(existing.port, existing.pid, project.id)) {
612
616
  if (opts.json === true) {
613
617
  process.stdout.write(
@@ -647,7 +651,8 @@ async function daemonStart(opts) {
647
651
  try {
648
652
  ensured = await ensureDaemonRunning({
649
653
  project,
650
- idleTimeoutSec: project.config.daemon.idleTimeoutSec
654
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec,
655
+ ...project.config.daemon.port !== void 0 ? { port: project.config.daemon.port } : {}
651
656
  });
652
657
  } catch (err) {
653
658
  ds.fail("Daemon failed to start");
@@ -690,7 +695,19 @@ async function daemonStart(opts) {
690
695
  log(`Noir daemon already running at ${ensured.url}`, opts);
691
696
  }
692
697
  async function daemonStop(opts) {
693
- const rec = readDaemonRecord();
698
+ const callerProject = resolveCallerProjectId();
699
+ if (callerProject === void 0) {
700
+ if (opts.json === true) {
701
+ process.stdout.write(
702
+ `${JSON.stringify({ ok: true, data: { running: false, stopped: false } })}
703
+ `
704
+ );
705
+ return;
706
+ }
707
+ info("No Noir daemon is running for this directory (not a Noir project).", opts);
708
+ return;
709
+ }
710
+ const rec = readProjectDaemonRecord(callerProject);
694
711
  if (!rec) {
695
712
  if (opts.json === true) {
696
713
  process.stdout.write(
@@ -705,9 +722,7 @@ async function daemonStop(opts) {
705
722
  const ds = spinner(`Stopping daemon (pid ${rec.pid})...`, opts).start();
706
723
  let signalled = false;
707
724
  let errMsg2;
708
- const callerProject = resolveCallerProjectId();
709
- const wrongProject = rec.projectId !== void 0 && callerProject !== void 0 && rec.projectId !== callerProject;
710
- const healthy = !wrongProject && pidAlive2(rec.pid) && await isHealthy(rec.port, rec.pid, rec.projectId === void 0 ? void 0 : callerProject);
725
+ const healthy = pidAlive2(rec.pid) && await isHealthy(rec.port, rec.pid, callerProject);
711
726
  if (healthy) {
712
727
  try {
713
728
  process.kill(rec.pid, "SIGTERM");
@@ -716,7 +731,7 @@ async function daemonStop(opts) {
716
731
  errMsg2 = err instanceof Error ? err.message : String(err);
717
732
  }
718
733
  }
719
- if (!wrongProject) clearDaemonRecord();
734
+ clearProjectDaemonRecord(callerProject);
720
735
  if (opts.json === true) {
721
736
  process.stdout.write(
722
737
  `${JSON.stringify({
@@ -735,26 +750,22 @@ async function daemonStop(opts) {
735
750
  if (signalled) {
736
751
  ds.succeed(`Stopped Noir daemon (pid ${rec.pid})`);
737
752
  } else {
738
- 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)";
753
+ const reason = errMsg2 !== void 0 ? errMsg2 : "the recorded pid did not answer as our daemon (stale record cleared)";
739
754
  ds.warn(`Daemon (pid ${rec.pid}) could not be signalled: ${reason}`);
740
755
  info(`Noir daemon (pid ${rec.pid}) could not be signalled: ${reason}`, opts);
741
756
  }
742
757
  }
743
758
  async function daemonStatus(opts) {
744
- const rec = readDaemonRecord();
759
+ const callerProject = resolveCallerProjectId();
760
+ if (callerProject === void 0) {
761
+ fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (not a Noir project).", opts);
762
+ }
763
+ const rec = readProjectDaemonRecord(callerProject);
745
764
  if (!rec) {
746
765
  fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (start with `noir daemon start`).", opts);
747
766
  }
748
- const callerProject = resolveCallerProjectId();
749
- if (rec.projectId !== void 0 && callerProject !== void 0 && rec.projectId !== callerProject) {
750
- fail(
751
- EXIT.DAEMON_DOWN,
752
- "Noir daemon is not running for this project (a daemon for another project is running).",
753
- opts
754
- );
755
- }
756
767
  if (!pidAlive2(rec.pid)) {
757
- clearDaemonRecord();
768
+ clearProjectDaemonRecord(callerProject);
758
769
  fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
759
770
  }
760
771
  const hs = spinner("Probing daemon health...", opts).start();
@@ -772,7 +783,7 @@ async function daemonStatus(opts) {
772
783
  const pidOk = typeof health?.pid === "number" && health.pid === rec.pid;
773
784
  if (health?.ok !== true || !pidOk) {
774
785
  hs.fail("Daemon not responding");
775
- clearDaemonRecord();
786
+ clearProjectDaemonRecord(callerProject);
776
787
  fail(
777
788
  EXIT.DAEMON_DOWN,
778
789
  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).",
@@ -788,7 +799,8 @@ async function daemonStatus(opts) {
788
799
  startedAt: rec.startedAt,
789
800
  uptimeSec,
790
801
  // Honest ownership: the detached child writes `mode:'detached'`; anything
791
- // else (legacy record, foreground start) reports 'foreground'.
802
+ // else (a record written before the field existed, foreground start)
803
+ // reports 'foreground'.
792
804
  mode: rec.mode ?? MODE
793
805
  };
794
806
  if (opts.json === true) {
@@ -801,6 +813,42 @@ async function daemonStatus(opts) {
801
813
  opts
802
814
  );
803
815
  }
816
+ async function daemonToken(opts) {
817
+ const callerProject = resolveCallerProjectId();
818
+ if (callerProject === void 0) {
819
+ fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (not a Noir project).", opts);
820
+ }
821
+ const rec = readProjectDaemonRecord(callerProject);
822
+ if (!rec) {
823
+ fail(
824
+ EXIT.DAEMON_DOWN,
825
+ "Noir daemon is not running (no daemon record \u2014 start it with `noir daemon start`).",
826
+ opts
827
+ );
828
+ }
829
+ if (!pidAlive2(rec.pid) || !await isHealthy(rec.port, rec.pid, callerProject)) {
830
+ fail(
831
+ EXIT.DAEMON_DOWN,
832
+ "Noir daemon is not running (stale record \u2014 start it with `noir daemon start`).",
833
+ opts
834
+ );
835
+ }
836
+ const token = readDaemonToken(callerProject);
837
+ if (token === null) {
838
+ fail(
839
+ EXIT.DAEMON_DOWN,
840
+ "Noir daemon is not running (the record has no token beside it \u2014 restart it with `noir daemon start`).",
841
+ opts
842
+ );
843
+ }
844
+ if (opts.json === true) {
845
+ process.stdout.write(`${JSON.stringify({ ok: true, data: { token } })}
846
+ `);
847
+ return;
848
+ }
849
+ process.stdout.write(`${token}
850
+ `);
851
+ }
804
852
  async function daemonRestart(opts) {
805
853
  await daemonStop({ ...opts, json: false, quiet: true });
806
854
  await daemonStart(opts);
@@ -815,6 +863,7 @@ import { resolveAdapter as resolveAdapter2 } from "@noir-ai/adapters";
815
863
  import {
816
864
  detectActiveMethod,
817
865
  latestVersionFromCache,
866
+ loadNoirEnv,
818
867
  loadProjectInfo as loadProjectInfo3,
819
868
  NOIR_VERSION,
820
869
  paths,
@@ -822,7 +871,7 @@ import {
822
871
  readUpdateCache
823
872
  } from "@noir-ai/core";
824
873
  import { CURRENT_SCAFFOLD_VERSION, readScaffoldVersion } from "@noir-ai/create";
825
- import { pidAlive as pidAlive3, readDaemonRecord as readDaemonRecord2 } from "@noir-ai/daemon";
874
+ import { pidAlive as pidAlive3, readProjectDaemonRecord as readProjectDaemonRecord2 } from "@noir-ai/daemon";
826
875
  import { resolveModelConfig } from "@noir-ai/model";
827
876
  async function probeOnnx() {
828
877
  try {
@@ -886,8 +935,16 @@ function checkConfig(root) {
886
935
  };
887
936
  }
888
937
  }
889
- async function checkDaemon(checks) {
890
- const rec = readDaemonRecord2();
938
+ async function checkDaemon(checks, project) {
939
+ if (project === void 0) {
940
+ checks.push({
941
+ name: "daemon",
942
+ status: "warn",
943
+ detail: "skipped \u2014 project not initialized"
944
+ });
945
+ return;
946
+ }
947
+ const rec = readProjectDaemonRecord2(project.id);
891
948
  if (!rec) {
892
949
  checks.push({
893
950
  name: "daemon",
@@ -979,7 +1036,8 @@ async function checkStore(checks, project, root) {
979
1036
  }
980
1037
  }
981
1038
  }
982
- function checkNoirEnv(checks, root) {
1039
+ var ENV_REFUSAL_PREFIX = ".noir/.env: refusing to load";
1040
+ function checkNoirEnv(checks, root, loaded) {
983
1041
  const path = join2(paths.noirDir(root), ".env");
984
1042
  let st;
985
1043
  try {
@@ -987,6 +1045,14 @@ function checkNoirEnv(checks, root) {
987
1045
  } catch {
988
1046
  return;
989
1047
  }
1048
+ if (loaded.warnings.some((w) => w.startsWith(ENV_REFUSAL_PREFIX))) {
1049
+ checks.push({
1050
+ name: "noir-env",
1051
+ status: "warn",
1052
+ 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\`.`
1053
+ });
1054
+ return;
1055
+ }
990
1056
  const mode = st.mode & 511;
991
1057
  const tooOpen = (st.mode & 63) !== 0;
992
1058
  checks.push(
@@ -996,6 +1062,10 @@ function checkNoirEnv(checks, root) {
996
1062
  detail: `${path} is ${mode.toString(8)} \u2014 readable by others; run chmod 600`
997
1063
  } : { name: "noir-env", status: "ok", detail: `${path} permissions ok (${mode.toString(8)})` }
998
1064
  );
1065
+ for (const [key, source] of Object.entries(loaded.sources)) {
1066
+ if (source !== "file") continue;
1067
+ checks.push({ name: `noir-env:${key}`, status: "ok", detail: "from .noir/.env" });
1068
+ }
999
1069
  }
1000
1070
  function checkEmbedder(checks, project, vecOk) {
1001
1071
  if (!project) {
@@ -1027,7 +1097,7 @@ function checkEmbedder(checks, project, vecOk) {
1027
1097
  detail: `${kind}/${model} (${emb.dim}-dim) \xB7 vec backend available`
1028
1098
  });
1029
1099
  }
1030
- function checkProvider(checks, project) {
1100
+ function checkProvider(checks, project, sources) {
1031
1101
  if (!project) {
1032
1102
  checks.push({ name: "provider", status: "warn", detail: "skipped \u2014 not initialized" });
1033
1103
  return;
@@ -1047,7 +1117,8 @@ function checkProvider(checks, project) {
1047
1117
  for (const name of names) {
1048
1118
  const p = resolved.providers[name];
1049
1119
  if (!p) continue;
1050
- const key = p.hasKey ? "key present" : p.apiKeyEnv ? `missing ${p.apiKeyEnv}` : "anonymous";
1120
+ const fromFile = p.apiKeyEnv !== void 0 && sources[p.apiKeyEnv] === "file";
1121
+ const key = p.hasKey ? fromFile ? "key present (from .noir/.env)" : "key present" : p.apiKeyEnv ? `missing ${p.apiKeyEnv}` : "anonymous";
1051
1122
  if (!p.hasKey && p.apiKeyEnv) missing = true;
1052
1123
  parts.push(`${name}/${p.model ?? "?"} (${key})`);
1053
1124
  }
@@ -1411,12 +1482,13 @@ async function doctor(opts = {}) {
1411
1482
  await checkRuntime(checks);
1412
1483
  const { project, result: configResult } = checkConfig(root);
1413
1484
  checks.push(configResult);
1414
- await checkDaemon(checks);
1485
+ await checkDaemon(checks, project);
1415
1486
  const { vecOk } = await checkNativeDeps(checks);
1416
1487
  await checkStore(checks, project, root);
1417
1488
  checkEmbedder(checks, project, vecOk);
1418
- checkProvider(checks, project);
1419
- checkNoirEnv(checks, root);
1489
+ const noirEnv = loadNoirEnv(root);
1490
+ checkProvider(checks, project, noirEnv.sources);
1491
+ checkNoirEnv(checks, root, noirEnv);
1420
1492
  const scaffold2 = checkScaffoldVersion(checks, root);
1421
1493
  const rules = checkRulesMdBudget(checks, root, project);
1422
1494
  const host = checkHostArtifacts(checks, root, project);
@@ -1460,6 +1532,94 @@ async function doctor(opts = {}) {
1460
1532
  }
1461
1533
  }
1462
1534
 
1535
+ // src/commands/env.ts
1536
+ import { loadNoirEnv as loadNoirEnv2, loadProjectInfo as loadProjectInfo4 } from "@noir-ai/core";
1537
+ import { resolveModelConfig as resolveModelConfig2 } from "@noir-ai/model";
1538
+ var CURATED_AMBIENT_KEYS = [
1539
+ "CLICKUP_API_TOKEN",
1540
+ "OPENAI_API_KEY",
1541
+ "VOYAGE_API_KEY",
1542
+ "COHERE_API_KEY",
1543
+ "OLLAMA_BASE_URL",
1544
+ "ANTHROPIC_API_KEY",
1545
+ "NOIR_PROFILE"
1546
+ ];
1547
+ var ambientBaseline;
1548
+ function captureAmbientEnv(env2 = process.env) {
1549
+ ambientBaseline = { ...env2 };
1550
+ }
1551
+ var SHORT_VALUE_MAX = 8;
1552
+ function redactShape(value) {
1553
+ if (value.length <= SHORT_VALUE_MAX) return `\u2026(${value.length})`;
1554
+ return `${value.slice(0, 3)}\u2026(${value.length})`;
1555
+ }
1556
+ function configApiKeyEnvNames(root) {
1557
+ try {
1558
+ const project = loadProjectInfo4(root);
1559
+ const names = /* @__PURE__ */ new Set();
1560
+ for (const provider of Object.values(resolveModelConfig2(project.config.model).providers)) {
1561
+ if (provider.apiKeyEnv !== void 0 && provider.apiKeyEnv.length > 0) {
1562
+ names.add(provider.apiKeyEnv);
1563
+ }
1564
+ }
1565
+ return [...names];
1566
+ } catch {
1567
+ return [];
1568
+ }
1569
+ }
1570
+ function resolveEnv(root, ambientEnv) {
1571
+ const { overlay, warnings, sources } = loadNoirEnv2(root, ambientEnv);
1572
+ const curated = new Set(CURATED_AMBIENT_KEYS);
1573
+ for (const name of configApiKeyEnvNames(root)) curated.add(name);
1574
+ const candidates = new Set(Object.keys(sources));
1575
+ for (const key of curated) {
1576
+ if (ambientEnv[key] !== void 0) candidates.add(key);
1577
+ }
1578
+ const vars = [];
1579
+ const shapes = {};
1580
+ for (const key of [...candidates].sort()) {
1581
+ const source = sources[key] ?? "env";
1582
+ if (source === "env" && !curated.has(key)) continue;
1583
+ const value = source === "file" ? overlay[key] ?? "" : ambientEnv[key] ?? "";
1584
+ const shadowed = source === "file" && ambientEnv[key] !== void 0 && ambientEnv[key] !== value;
1585
+ vars.push({
1586
+ key,
1587
+ source,
1588
+ ...shadowed ? { shadowed: true } : {},
1589
+ valueLength: value.length
1590
+ });
1591
+ shapes[key] = redactShape(value);
1592
+ }
1593
+ return { vars, warnings, shapes };
1594
+ }
1595
+ function describeSource(row) {
1596
+ if (row.source !== "file") return "environment";
1597
+ return row.shadowed === true ? ".noir/.env (shadows environment)" : ".noir/.env";
1598
+ }
1599
+ async function env(opts = {}) {
1600
+ const report = resolveEnv(process.cwd(), ambientBaseline ?? process.env);
1601
+ if (opts.json === true) {
1602
+ json({ ok: true, data: { vars: report.vars, warnings: report.warnings } });
1603
+ return;
1604
+ }
1605
+ if (report.vars.length === 0) {
1606
+ info(
1607
+ "nothing to report \u2014 .noir/.env defines no keys in effect and no Noir-relevant environment variable is set",
1608
+ opts
1609
+ );
1610
+ return;
1611
+ }
1612
+ table(
1613
+ report.vars.map((row) => ({
1614
+ KEY: row.key,
1615
+ SOURCE: describeSource(row),
1616
+ VALUE: report.shapes[row.key] ?? ""
1617
+ })),
1618
+ ["KEY", "SOURCE", "VALUE"],
1619
+ opts
1620
+ );
1621
+ }
1622
+
1463
1623
  // src/commands/handoff.ts
1464
1624
  import { mkdirSync, writeFileSync as writeFileSync2 } from "fs";
1465
1625
  import { join as join3 } from "path";
@@ -1468,12 +1628,12 @@ import {
1468
1628
  hostLaunchDirective,
1469
1629
  resolveAdapter as resolveAdapter3
1470
1630
  } from "@noir-ai/adapters";
1471
- import { loadProjectInfo as loadProjectInfo5, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
1631
+ import { loadProjectInfo as loadProjectInfo6, NOIR_DIR, resolveArtifactPath } from "@noir-ai/core";
1472
1632
 
1473
1633
  // src/commands/task.ts
1474
1634
  import { spawnSync as spawnSync2 } from "child_process";
1475
1635
  import { writeFileSync } from "fs";
1476
- import { loadProjectInfo as loadProjectInfo4, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
1636
+ import { loadProjectInfo as loadProjectInfo5, nextArtifactSequence, sha256Hex } from "@noir-ai/core";
1477
1637
  import {
1478
1638
  PHASES,
1479
1639
  TASK_CLASSES,
@@ -1497,7 +1657,7 @@ function skillFor(phase) {
1497
1657
  }
1498
1658
  function writeDoneArtifacts(opts, taskId) {
1499
1659
  try {
1500
- const project = loadProjectInfo4(process.cwd());
1660
+ const project = loadProjectInfo5(process.cwd());
1501
1661
  const conflict = { conflictPolicy: "preserve", interactive: false };
1502
1662
  writeChangelogStub(project.root, `- ${taskId}: completed (verify gate passed)`, conflict);
1503
1663
  const nextN = nextArtifactSequence(project.root, "adr");
@@ -1821,7 +1981,7 @@ function runCheck(cmd) {
1821
1981
  async function taskVerify(opts) {
1822
1982
  let project;
1823
1983
  try {
1824
- project = loadProjectInfo4(process.cwd());
1984
+ project = loadProjectInfo5(process.cwd());
1825
1985
  } catch {
1826
1986
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
1827
1987
  }
@@ -2144,7 +2304,7 @@ async function probeOnly(opts) {
2144
2304
  async function handoff(opts) {
2145
2305
  let project;
2146
2306
  try {
2147
- project = loadProjectInfo5(process.cwd());
2307
+ project = loadProjectInfo6(process.cwd());
2148
2308
  } catch {
2149
2309
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
2150
2310
  }
@@ -2193,7 +2353,7 @@ import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
2193
2353
  import {
2194
2354
  detectActiveMethod as detectActiveMethod2,
2195
2355
  latestVersionFromCache as latestVersionFromCache2,
2196
- loadProjectInfo as loadProjectInfo6,
2356
+ loadProjectInfo as loadProjectInfo7,
2197
2357
  NOIR_VERSION as NOIR_VERSION2,
2198
2358
  readInstallRecord as readInstallRecord2,
2199
2359
  readUpdateCache as readUpdateCache2
@@ -2487,7 +2647,7 @@ async function resolveSections(commands) {
2487
2647
  // src/commands/home.ts
2488
2648
  function tryProject() {
2489
2649
  try {
2490
- const info2 = loadProjectInfo6(process.cwd());
2650
+ const info2 = loadProjectInfo7(process.cwd());
2491
2651
  return { id: info2.id, name: info2.name, host: info2.config.host };
2492
2652
  } catch {
2493
2653
  return null;
@@ -2651,7 +2811,7 @@ async function runMenu(opts, deps) {
2651
2811
  env: process.env,
2652
2812
  configUpdate: (() => {
2653
2813
  try {
2654
- const info2 = loadProjectInfo6(process.cwd());
2814
+ const info2 = loadProjectInfo7(process.cwd());
2655
2815
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
2656
2816
  } catch {
2657
2817
  return DEFAULT_UPDATE_CONFIG;
@@ -2701,12 +2861,12 @@ ${COMMANDS_HINT}
2701
2861
 
2702
2862
  // src/commands/memory.ts
2703
2863
  import { readFileSync as readFileSync3 } from "fs";
2704
- import { loadProjectInfo as loadProjectInfo7 } from "@noir-ai/core";
2864
+ import { loadProjectInfo as loadProjectInfo8 } from "@noir-ai/core";
2705
2865
  function resolveWorkspace() {
2706
2866
  const routing = workspaceRoutingTarget(process.cwd());
2707
2867
  if (routing === null) return null;
2708
2868
  try {
2709
- return { routing, project: loadProjectInfo7(process.cwd()) };
2869
+ return { routing, project: loadProjectInfo8(process.cwd()) };
2710
2870
  } catch {
2711
2871
  return null;
2712
2872
  }
@@ -3098,9 +3258,9 @@ async function memoryConsolidate(opts) {
3098
3258
  }
3099
3259
  return;
3100
3260
  }
3101
- const env = res;
3102
- const reason = (typeof env.reason === "string" ? env.reason : void 0) ?? (typeof env.error === "string" ? env.error : void 0) ?? "unknown";
3103
- const logged = env.logged === true || env.degraded === true;
3261
+ const env2 = res;
3262
+ const reason = (typeof env2.reason === "string" ? env2.reason : void 0) ?? (typeof env2.error === "string" ? env2.error : void 0) ?? "unknown";
3263
+ const logged = env2.logged === true || env2.degraded === true;
3104
3264
  const suffix = logged ? " (logged)" : "";
3105
3265
  fail(EXIT.ERROR, `memory consolidate did not run: ${reason}${suffix}`, opts);
3106
3266
  }
@@ -3202,10 +3362,10 @@ async function release(opts) {
3202
3362
  }
3203
3363
 
3204
3364
  // src/commands/run.ts
3205
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
3365
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
3206
3366
  import { join as join4 } from "path";
3207
3367
  import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
3208
- import { parseConfig } from "@noir-ai/core";
3368
+ import { loadNoirEnv as loadNoirEnv3, NOIR_DIR as NOIR_DIR2, parseConfig } from "@noir-ai/core";
3209
3369
 
3210
3370
  // src/orchestrator.ts
3211
3371
  import { spawn as spawn2 } from "child_process";
@@ -3219,10 +3379,10 @@ var PROBE_TIMEOUT_MS2 = 3e3;
3219
3379
  var NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
3220
3380
  var SUPPORTED_SHELLS = /* @__PURE__ */ new Set(["zsh", "bash", "fish"]);
3221
3381
  function isEligibleForShellFallback(name, opts = {}) {
3222
- const { env = process.env, platform = process.platform } = opts;
3382
+ const { env: env2 = process.env, platform = process.platform } = opts;
3223
3383
  if (platform === "win32") return false;
3224
3384
  if (name.length === 0 || name.includes("/") || !NAME_RE.test(name)) return false;
3225
- const shell = env.SHELL;
3385
+ const shell = env2.SHELL;
3226
3386
  if (!shell || shell.length === 0) return false;
3227
3387
  return SUPPORTED_SHELLS.has(basename(shell).toLowerCase());
3228
3388
  }
@@ -3296,9 +3456,9 @@ function probeCommand(name, shell, opts) {
3296
3456
  }
3297
3457
  var pathCache = /* @__PURE__ */ new Map();
3298
3458
  async function resolveCommandViaShell(name, opts = {}) {
3299
- const { env = process.env } = opts;
3459
+ const { env: env2 = process.env } = opts;
3300
3460
  if (!isEligibleForShellFallback(name, opts)) return { kind: "unsupported" };
3301
- const shell = env.SHELL;
3461
+ const shell = env2.SHELL;
3302
3462
  const cached = pathCache.get(`${shell}:${name}`);
3303
3463
  if (cached) return { kind: "path", path: cached, shell };
3304
3464
  const probe = await probeCommand(name, shell, opts);
@@ -3569,24 +3729,24 @@ async function shellFallback(binary, args, opts, resolve, reject, originalErr) {
3569
3729
  }
3570
3730
 
3571
3731
  // src/run-profiles.ts
3572
- import { loadProjectInfo as loadProjectInfo8 } from "@noir-ai/core";
3732
+ import { loadProjectInfo as loadProjectInfo9 } from "@noir-ai/core";
3573
3733
  function loadRunConfig(root) {
3574
3734
  try {
3575
- return loadProjectInfo8(root).config;
3735
+ return loadProjectInfo9(root).config;
3576
3736
  } catch {
3577
3737
  return null;
3578
3738
  }
3579
3739
  }
3580
- function expandEnvVars(value, env) {
3740
+ function expandEnvVars(value, env2) {
3581
3741
  return value.replace(
3582
3742
  /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
3583
- (match, name) => env[name] !== void 0 ? env[name] : match
3743
+ (match, name) => env2[name] !== void 0 ? env2[name] : match
3584
3744
  );
3585
3745
  }
3586
- function resolveRunProfile(requested, config, env) {
3746
+ function resolveRunProfile(requested, config, env2) {
3587
3747
  const profiles = config.run?.profiles ?? {};
3588
3748
  const names = Object.keys(profiles);
3589
- const name = requested ?? env.NOIR_PROFILE ?? config.run?.defaultProfile;
3749
+ const name = requested ?? env2.NOIR_PROFILE ?? config.run?.defaultProfile;
3590
3750
  if (name === void 0 || name.length === 0) {
3591
3751
  return { ok: true, profile: {} };
3592
3752
  }
@@ -3601,7 +3761,7 @@ function resolveRunProfile(requested, config, env) {
3601
3761
  const envExpanded = {};
3602
3762
  if (profile.env) {
3603
3763
  for (const [k, v] of Object.entries(profile.env)) {
3604
- envExpanded[k] = v === null ? void 0 : expandEnvVars(v, env);
3764
+ envExpanded[k] = v === null ? void 0 : expandEnvVars(v, env2);
3605
3765
  }
3606
3766
  }
3607
3767
  return {
@@ -3620,6 +3780,25 @@ function listProfiles(config) {
3620
3780
  }
3621
3781
 
3622
3782
  // src/commands/run.ts
3783
+ var CREDENTIAL_ENV_VARS = [
3784
+ "ANTHROPIC_API_KEY",
3785
+ "ANTHROPIC_AUTH_TOKEN",
3786
+ "ANTHROPIC_BASE_URL"
3787
+ ];
3788
+ function credentialNote(env2, sources) {
3789
+ const named = CREDENTIAL_ENV_VARS.filter((name) => {
3790
+ const value = env2[name];
3791
+ return value !== void 0 && value.length > 0;
3792
+ }).map(
3793
+ (name) => `${name} (from ${sources[name] === "file" ? `${NOIR_DIR2}/.env` : "the environment"})`
3794
+ );
3795
+ if (named.length === 0) return "";
3796
+ const one = named.length === 1;
3797
+ const subject = one ? `${named[0]} is set` : `${named.join(", ")} are set`;
3798
+ const pronoun = one ? "it" : "them";
3799
+ const verb = one ? "overrides" : "override";
3800
+ 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.`;
3801
+ }
3623
3802
  function formatUsage(u) {
3624
3803
  const cost = u.totalCostUsd > 0 ? ` $${u.totalCostUsd.toFixed(2)}` : "";
3625
3804
  const turns = u.numTurns > 0 ? ` \xB7 ${u.numTurns} turns` : "";
@@ -3643,13 +3822,21 @@ async function run(prompt, opts) {
3643
3822
  const usage = selectors.length > 0 ? `noir run ${selectors.join(" ")} <prompt>` : "noir run <prompt>";
3644
3823
  fail(EXIT.USAGE, `a prompt is required: \`${usage}\``, opts);
3645
3824
  }
3646
- const config = loadRunConfig(process.cwd()) ?? parseConfig({});
3825
+ const root = process.cwd();
3826
+ const envSources = loadNoirEnv3(root).sources;
3827
+ const config = loadRunConfig(root) ?? parseConfig({});
3647
3828
  const resolved = resolveRunProfile(opts.profile, config, process.env);
3648
3829
  if (!resolved.ok) fail(EXIT.USAGE, resolved.message, opts);
3649
3830
  const profile = resolved.profile;
3650
3831
  const customBinary = opts.command ?? profile.binary;
3651
3832
  const extraArgs = profile.args;
3652
- const env = profile.env ? mergeEnv(process.env, profile.env) : void 0;
3833
+ const env2 = profile.env ? mergeEnv(process.env, profile.env) : void 0;
3834
+ if (!existsSync3(join4(root, NOIR_DIR2, ".env"))) {
3835
+ log(
3836
+ `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).`,
3837
+ opts
3838
+ );
3839
+ }
3653
3840
  const transcriptLines = [];
3654
3841
  let result;
3655
3842
  try {
@@ -3658,7 +3845,7 @@ async function run(prompt, opts) {
3658
3845
  prompt,
3659
3846
  customBinary,
3660
3847
  extraArgs,
3661
- env,
3848
+ env: env2,
3662
3849
  onLine: (line) => transcriptLines.push(line),
3663
3850
  onEvent: (event) => streamEvent(event, opts)
3664
3851
  });
@@ -3679,9 +3866,7 @@ async function run(prompt, opts) {
3679
3866
  let message = `host '${binary}' failed (exit ${result.exitCode}): ${reason}`;
3680
3867
  if (isAuth) {
3681
3868
  message += ` Open a terminal and run \`${binary} /login\` (interactive-only \u2014 it cannot run inside \`noir run\`), then retry.`;
3682
- if (process.env.ANTHROPIC_API_KEY) {
3683
- 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.`;
3684
- }
3869
+ message += credentialNote(env2 ?? process.env, envSources);
3685
3870
  }
3686
3871
  message += ` If you use another profile, pass \`--command <binary>\` or define a run profile under run.profiles. transcript: ${transcript}`;
3687
3872
  fail(EXIT.ERROR, message, opts);
@@ -3724,7 +3909,7 @@ function writeTranscript(host, lines) {
3724
3909
 
3725
3910
  // src/commands/skills.ts
3726
3911
  import { resolveAdapter as resolveAdapter4 } from "@noir-ai/adapters";
3727
- import { loadProjectInfo as loadProjectInfo9 } from "@noir-ai/core";
3912
+ import { loadProjectInfo as loadProjectInfo10 } from "@noir-ai/core";
3728
3913
  import {
3729
3914
  buildRegistry,
3730
3915
  discoverAll,
@@ -3820,7 +4005,7 @@ async function skillsSync(opts) {
3820
4005
  const root = process.cwd();
3821
4006
  let project;
3822
4007
  try {
3823
- project = loadProjectInfo9(root);
4008
+ project = loadProjectInfo10(root);
3824
4009
  } catch {
3825
4010
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
3826
4011
  }
@@ -3954,7 +4139,7 @@ async function skillsRegistry(opts) {
3954
4139
 
3955
4140
  // src/init.ts
3956
4141
  import { resolveAdapter as resolveAdapter5 } from "@noir-ai/adapters";
3957
- import { loadProjectInfo as loadProjectInfo10 } from "@noir-ai/core";
4142
+ import { loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
3958
4143
  import { scaffold } from "@noir-ai/create";
3959
4144
  import { emitSkillsToDir as emitSkillsToDir2 } from "@noir-ai/skills";
3960
4145
 
@@ -4327,7 +4512,7 @@ async function init(root, opts) {
4327
4512
  await emitHostSkills(root, host, conflictOpts, interactive);
4328
4513
  let projectInfo;
4329
4514
  try {
4330
- projectInfo = loadProjectInfo10(root);
4515
+ projectInfo = loadProjectInfo11(root);
4331
4516
  } catch {
4332
4517
  projectInfo = void 0;
4333
4518
  }
@@ -4346,7 +4531,10 @@ function reportPlannedWrites(res) {
4346
4531
  log("Dry run \u2014 no files were written.");
4347
4532
  if (res.written.length > 0) {
4348
4533
  log("Planned writes:");
4349
- for (const p of res.written) log(` ${p}`);
4534
+ for (const p of res.written) {
4535
+ const mode = res.fileModes?.[p];
4536
+ log(mode === void 0 ? ` ${p}` : ` ${p} (mode ${mode.toString(8).padStart(3, "0")})`);
4537
+ }
4350
4538
  }
4351
4539
  if (res.skipped.length > 0) {
4352
4540
  log("Would leave as-is (already present):");
@@ -4406,11 +4594,11 @@ function assertLocalhostUrl(raw) {
4406
4594
  }
4407
4595
 
4408
4596
  // src/serve.ts
4409
- import { applyNoirEnv, loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
4597
+ import { applyNoirEnv, loadProjectInfo as loadProjectInfo12 } from "@noir-ai/core";
4410
4598
  import { ensureDaemonRunning as ensureDaemonRunning2, startStdioServer } from "@noir-ai/daemon";
4411
4599
  async function serve(opts) {
4412
4600
  applyNoirEnv(process.cwd());
4413
- const project = loadProjectInfo11(process.cwd());
4601
+ const project = loadProjectInfo12(process.cwd());
4414
4602
  if (opts.stdio) {
4415
4603
  await startStdioServer({ project, transport: "stdio", daemon: false });
4416
4604
  return;
@@ -4418,10 +4606,11 @@ async function serve(opts) {
4418
4606
  try {
4419
4607
  const { url } = await ensureDaemonRunning2({
4420
4608
  project,
4421
- idleTimeoutSec: project.config.daemon.idleTimeoutSec
4609
+ idleTimeoutSec: project.config.daemon.idleTimeoutSec,
4610
+ ...project.config.daemon.port !== void 0 ? { port: project.config.daemon.port } : {}
4422
4611
  });
4423
4612
  process.stderr.write(
4424
- `Noir daemon available at ${url}. (For HTTP clients, use this URL in .mcp.json.)
4613
+ `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\`).
4425
4614
  `
4426
4615
  );
4427
4616
  } catch (err) {
@@ -4534,6 +4723,7 @@ function createProgram() {
4534
4723
  );
4535
4724
  }
4536
4725
  }
4726
+ if (actionCmd.name() === "env") captureAmbientEnv();
4537
4727
  applyNoirEnv2(process.cwd());
4538
4728
  const nonInteractive = opts.json === true || opts.input === false;
4539
4729
  if (nonInteractive) process.env.NOIR_NON_INTERACTIVE = "1";
@@ -4583,7 +4773,7 @@ function createProgram() {
4583
4773
  const force = opts.force === true;
4584
4774
  const host = parseHost(opts.host);
4585
4775
  const dryRun = opts.dryRun === true || opts.preview === true;
4586
- const { create } = await import("./create-HYFBTBLR.js");
4776
+ const { create } = await import("./create-FD4E626Z.js");
4587
4777
  const result = await create(dir, {
4588
4778
  transport,
4589
4779
  url: opts.url,
@@ -4622,7 +4812,7 @@ function createProgram() {
4622
4812
  ).choices(SUPPORTED_HOSTS2)
4623
4813
  ).action(
4624
4814
  async (opts, cmd) => {
4625
- const { sync } = await import("./sync-QDOJ7INZ.js");
4815
+ const { sync } = await import("./sync-N633SQSS.js");
4626
4816
  const host = parseHost(opts.host);
4627
4817
  const force = opts.force === true;
4628
4818
  const merge = opts.merge === true;
@@ -4676,6 +4866,9 @@ function createProgram() {
4676
4866
  daemonGrp.command("status").description("report daemon pid/uptime/mode (exit 4 if not running)").action(async (...args) => {
4677
4867
  await daemonStatus(toCliOptions(trailingCmd(args).optsWithGlobals()));
4678
4868
  });
4869
+ daemonGrp.command("token").description("print the daemon bearer token to stdout (for a host headersHelper)").action(async (...args) => {
4870
+ await daemonToken(toCliOptions(trailingCmd(args).optsWithGlobals()));
4871
+ });
4679
4872
  daemonGrp.command("restart").description("stop then start the daemon").option("--detach", "run the daemon in the background and exit").action(async (...args) => {
4680
4873
  const cmd = trailingCmd(args);
4681
4874
  const g = cmd.optsWithGlobals();
@@ -4685,8 +4878,19 @@ function createProgram() {
4685
4878
  });
4686
4879
  });
4687
4880
  daemonGrp.action(() => {
4688
- throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|join|stop|status|restart");
4881
+ throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|join|stop|status|restart|token");
4689
4882
  });
4883
+ daemonGrp.addHelpText(
4884
+ "after",
4885
+ [
4886
+ "",
4887
+ "Shared workspaces (two repos, one product \u2014 e.g. backend + frontend):",
4888
+ " noir daemon start --workspace <name> --detach found a workspace from this repo",
4889
+ " noir daemon join <name> join it from the other repo",
4890
+ "",
4891
+ "Guide: docs/how-to/shared-workspaces.md"
4892
+ ].join("\n")
4893
+ );
4690
4894
  const workspaceGrp = program2.command("workspace").description("shared cross-repo workspaces");
4691
4895
  workspaceGrp.command("list").description("list workspaces on this machine").action(async (...args) => {
4692
4896
  await workspaceList(toCliOptions(trailingCmd(args).optsWithGlobals()));
@@ -4715,6 +4919,17 @@ function createProgram() {
4715
4919
  ...name !== void 0 ? { name } : {}
4716
4920
  });
4717
4921
  });
4922
+ workspaceGrp.addHelpText(
4923
+ "after",
4924
+ [
4925
+ "",
4926
+ "This group only inspects and leaves workspaces. To CREATE or JOIN one:",
4927
+ " noir daemon start --workspace <name> --detach found it (from the first repo)",
4928
+ " noir daemon join <name> join it (from the second repo)",
4929
+ "",
4930
+ "Guide: docs/how-to/shared-workspaces.md"
4931
+ ].join("\n")
4932
+ );
4718
4933
  workspaceGrp.action(() => {
4719
4934
  throw new NoirCliError(EXIT.USAGE, "Usage: noir workspace list|status|leave|stop");
4720
4935
  });
@@ -4729,6 +4944,9 @@ function createProgram() {
4729
4944
  program2.command("status").description("project + daemon + workflow + store snapshot").action(async (...args) => {
4730
4945
  await status(toStatusOptions(actionGlobals(args)));
4731
4946
  });
4947
+ program2.command("env").description("which configuration is in effect and where each value comes from").action(async (...args) => {
4948
+ await env(toCliOptions(actionGlobals(args)));
4949
+ });
4732
4950
  const contextGrp = program2.command("context").description("context engine");
4733
4951
  contextGrp.command("search").description("hybrid search over the indexed context").argument("<query>", "search query").option("--limit <n>", "max results", "10").action(async (...args) => {
4734
4952
  const cmd = trailingCmd(args);
@@ -5154,7 +5372,18 @@ function failDaemonDown(opts, cause) {
5154
5372
  fail(EXIT.DAEMON_DOWN, DAEMON_DOWN_HINT, opts);
5155
5373
  }
5156
5374
  async function probeDaemon(opts = {}) {
5157
- const rec = readDaemonRecord3();
5375
+ const expectedProject = opts.project?.id ?? (() => {
5376
+ try {
5377
+ return loadProjectInfo13(process.cwd()).id;
5378
+ } catch {
5379
+ return void 0;
5380
+ }
5381
+ })();
5382
+ if (expectedProject === void 0) {
5383
+ if (opts.verbose) process.stderr.write("noir: daemon probe: no project (uninitialized)\n");
5384
+ return { running: false };
5385
+ }
5386
+ const rec = readProjectDaemonRecord3(expectedProject);
5158
5387
  if (!rec) {
5159
5388
  if (opts.verbose) process.stderr.write("noir: daemon probe: no daemon record\n");
5160
5389
  return { running: false };
@@ -5175,19 +5404,10 @@ async function probeDaemon(opts = {}) {
5175
5404
  }
5176
5405
  const body = await res.json().catch(() => null);
5177
5406
  const pidOk = typeof body?.pid === "number" && body.pid === rec.pid;
5178
- const expectedProject = opts.project?.id ?? (() => {
5179
- try {
5180
- return loadProjectInfo12(process.cwd()).id;
5181
- } catch {
5182
- return void 0;
5183
- }
5184
- })();
5185
- const projectOk = expectedProject === void 0 || body?.projectId !== void 0 && body.projectId === expectedProject;
5186
- if (body?.ok !== true || !pidOk || !projectOk) {
5407
+ if (body?.ok !== true || !pidOk) {
5187
5408
  if (opts.verbose)
5188
5409
  process.stderr.write(
5189
- !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
5190
- ` : "noir: daemon probe: /health body not ok\n"
5410
+ !pidOk ? "noir: daemon probe: pid missing/mismatched (foreign process on recorded port)\n" : "noir: daemon probe: /health body not ok\n"
5191
5411
  );
5192
5412
  return { running: false };
5193
5413
  }
@@ -5207,25 +5427,112 @@ async function probeDaemon(opts = {}) {
5207
5427
  async function resolveDaemon(opts) {
5208
5428
  let project;
5209
5429
  try {
5210
- project = opts.project ?? loadProjectInfo12(process.cwd());
5430
+ project = opts.project ?? loadProjectInfo13(process.cwd());
5211
5431
  } catch {
5212
5432
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
5213
5433
  }
5214
5434
  const idleTimeoutSec = opts.idleTimeoutSec ?? project.config.daemon.idleTimeoutSec;
5435
+ const preferred = project.config.daemon.port;
5436
+ if (preferred !== void 0 && preferred !== 0) {
5437
+ const direct = `http://127.0.0.1:${preferred}/mcp`;
5438
+ const probe = newClient();
5439
+ let probeTimer;
5440
+ try {
5441
+ await Promise.race([
5442
+ probe.connect(httpTransport(direct, project.id)),
5443
+ new Promise((_, reject) => {
5444
+ probeTimer = setTimeout(
5445
+ () => reject(new Error(`no response within ${PROBE_TIMEOUT_MS}ms`)),
5446
+ PROBE_TIMEOUT_MS
5447
+ );
5448
+ })
5449
+ ]);
5450
+ return { client: probe, stop: async () => {
5451
+ } };
5452
+ } catch (err) {
5453
+ if (opts.verbose) {
5454
+ process.stderr.write(
5455
+ `noir: no daemon at the configured port ${preferred} (${describeCause(err)})
5456
+ `
5457
+ );
5458
+ }
5459
+ await probe.close().catch(() => {
5460
+ });
5461
+ } finally {
5462
+ if (probeTimer !== void 0) clearTimeout(probeTimer);
5463
+ }
5464
+ }
5465
+ let url;
5466
+ let stop;
5215
5467
  try {
5216
- const ensured = await ensureDaemonRunning3({ project, idleTimeoutSec });
5217
- return { url: ensured.url, stop: ensured.stop };
5468
+ const ensured = await ensureDaemonRunning3({
5469
+ project,
5470
+ idleTimeoutSec,
5471
+ ...preferred !== void 0 ? { port: preferred } : {}
5472
+ });
5473
+ url = ensured.url;
5474
+ stop = ensured.stop;
5218
5475
  } catch (err) {
5219
5476
  failDaemonDown(opts, err);
5220
5477
  }
5478
+ const client = newClient();
5479
+ try {
5480
+ await connectClient(client, url, opts, project.id);
5481
+ } catch (err) {
5482
+ await client.close().catch(() => {
5483
+ });
5484
+ await stop().catch(() => {
5485
+ });
5486
+ throw err;
5487
+ }
5488
+ return { client, stop };
5489
+ }
5490
+ function httpTransport(url, scopeKey) {
5491
+ const token = scopeKey === null ? null : readDaemonToken2(scopeKey);
5492
+ return new StreamableHTTPClientTransport(
5493
+ new URL(url),
5494
+ // Verified present: StreamableHTTPClientTransportOptions.requestInit?: RequestInit
5495
+ token ? { requestInit: { headers: { Authorization: `Bearer ${token}` } } } : {}
5496
+ );
5497
+ }
5498
+ function callerScopeKey(opts) {
5499
+ try {
5500
+ return (opts.project ?? loadProjectInfo13(process.cwd())).id;
5501
+ } catch {
5502
+ return null;
5503
+ }
5504
+ }
5505
+ function newClient() {
5506
+ return new Client(
5507
+ { name: "noir-cli", version: NOIR_VERSION4 },
5508
+ { versionNegotiation: { mode: "auto" } }
5509
+ );
5510
+ }
5511
+ async function connectRaw(client, url, scopeKey) {
5512
+ await client.connect(httpTransport(url, scopeKey));
5221
5513
  }
5222
- async function connectClient(client, url, opts) {
5514
+ async function connectClient(client, url, opts, scopeKey) {
5223
5515
  try {
5224
- await client.connect(new StreamableHTTPClientTransport(new URL(url)));
5516
+ await connectRaw(client, url, scopeKey);
5225
5517
  } catch (err) {
5226
5518
  failDaemonDown(opts, err);
5227
5519
  }
5228
5520
  }
5521
+ function failWorkspaceConnect(opts, routing, cause) {
5522
+ const text = String(cause).toLowerCase();
5523
+ if (text.includes("401") || text.includes("unauthorized")) {
5524
+ if (opts.verbose) {
5525
+ process.stderr.write(`noir: daemon transport detail: ${describeCause(cause)}
5526
+ `);
5527
+ }
5528
+ fail(
5529
+ EXIT.DAEMON_DOWN,
5530
+ `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.`,
5531
+ opts
5532
+ );
5533
+ }
5534
+ failDaemonDown(opts, cause);
5535
+ }
5229
5536
  async function callToolParse(client, opts, name, args) {
5230
5537
  const result = await client.callTool({ name, arguments: args }).catch((err) => failDaemonDown(opts, err));
5231
5538
  const block = result.content?.[0];
@@ -5256,21 +5563,12 @@ function buildCaller(client, opts) {
5256
5563
  };
5257
5564
  }
5258
5565
  async function withDaemon(opts, fn) {
5259
- const { url, stop } = await resolveDaemon(opts);
5260
- let client;
5566
+ const { client, stop } = await resolveDaemon(opts);
5261
5567
  try {
5262
- client = new Client(
5263
- { name: "noir-cli", version: NOIR_VERSION4 },
5264
- { versionNegotiation: { mode: "auto" } }
5265
- );
5266
- const connected = client;
5267
- await connectClient(connected, url, opts);
5268
- return await fn(buildCaller(connected, opts));
5568
+ return await fn(buildCaller(client, opts));
5269
5569
  } finally {
5270
- if (client) {
5271
- await client.close().catch(() => {
5272
- });
5273
- }
5570
+ await client.close().catch(() => {
5571
+ });
5274
5572
  await stop().catch(() => {
5275
5573
  });
5276
5574
  }
@@ -5281,12 +5579,9 @@ async function withRunningDaemon(opts, fn, probe) {
5281
5579
  const url = `http://127.0.0.1:${p.port}/mcp`;
5282
5580
  let client;
5283
5581
  try {
5284
- client = new Client(
5285
- { name: "noir-cli", version: NOIR_VERSION4 },
5286
- { versionNegotiation: { mode: "auto" } }
5287
- );
5582
+ client = newClient();
5288
5583
  const connected = client;
5289
- await connected.connect(new StreamableHTTPClientTransport(new URL(url)));
5584
+ await connected.connect(httpTransport(url, callerScopeKey(opts)));
5290
5585
  return await fn(buildCaller(connected, opts));
5291
5586
  } catch {
5292
5587
  return null;
@@ -5303,7 +5598,7 @@ async function callDaemonTool(opts, name, args = {}) {
5303
5598
  async function withInProcessRead(opts, fn) {
5304
5599
  let project;
5305
5600
  try {
5306
- project = opts.project ?? loadProjectInfo12(process.cwd());
5601
+ project = opts.project ?? loadProjectInfo13(process.cwd());
5307
5602
  } catch {
5308
5603
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
5309
5604
  }
@@ -5350,7 +5645,7 @@ function workspaceRoutingTarget(root) {
5350
5645
  const name = readWorkspaceMarker2(root);
5351
5646
  if (name === null) return null;
5352
5647
  try {
5353
- return { name, projectId: loadProjectInfo12(root).id };
5648
+ return { name, projectId: loadProjectInfo13(root).id };
5354
5649
  } catch {
5355
5650
  return null;
5356
5651
  }
@@ -5384,12 +5679,11 @@ async function withWorkspaceDaemon(opts, routing, fn) {
5384
5679
  const url = `http://127.0.0.1:${probe.port}/mcp?p=${routing.projectId}`;
5385
5680
  let client;
5386
5681
  try {
5387
- client = new Client(
5388
- { name: "noir-cli", version: NOIR_VERSION4 },
5389
- { versionNegotiation: { mode: "auto" } }
5390
- );
5682
+ client = newClient();
5391
5683
  const connected = client;
5392
- await connectClient(connected, url, opts);
5684
+ await connectRaw(connected, url, routing.name).catch(
5685
+ (err) => failWorkspaceConnect(opts, routing, err)
5686
+ );
5393
5687
  return await fn(buildCaller(connected, opts));
5394
5688
  } finally {
5395
5689
  if (client) {
@@ -5553,7 +5847,7 @@ function renderHuman2(p, opts) {
5553
5847
  async function gatherStatusPayload(opts) {
5554
5848
  let project;
5555
5849
  try {
5556
- project = loadProjectInfo13(process.cwd());
5850
+ project = loadProjectInfo14(process.cwd());
5557
5851
  } catch {
5558
5852
  fail(EXIT.ERROR, "Noir is not initialized in this directory. Run `noir init` first.", opts);
5559
5853
  }
@@ -5583,7 +5877,7 @@ async function status(opts) {
5583
5877
  env: process.env,
5584
5878
  configUpdate: (() => {
5585
5879
  try {
5586
- const info2 = loadProjectInfo13(process.cwd());
5880
+ const info2 = loadProjectInfo14(process.cwd());
5587
5881
  return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
5588
5882
  } catch {
5589
5883
  return DEFAULT_UPDATE_CONFIG;
@@ -5615,4 +5909,4 @@ export {
5615
5909
  assertTransportUrl,
5616
5910
  buildPaletteCommandsForTui
5617
5911
  };
5618
- //# sourceMappingURL=chunk-KMLSJRYV.js.map
5912
+ //# sourceMappingURL=chunk-6LO4LXEH.js.map