@noir-ai/cli 1.13.0-beta.1 → 1.14.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +432 -130
- package/dist/bin.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +450 -148
- package/dist/index.js.map +1 -1
- package/dist/tui/{chunk-KMLSJRYV.js → chunk-6LO4LXEH.js} +419 -125
- package/dist/tui/chunk-6LO4LXEH.js.map +1 -0
- package/dist/tui/{create-HYFBTBLR.js → create-FD4E626Z.js} +2 -2
- package/dist/tui/index.js +1 -1
- package/dist/tui/{sync-QDOJ7INZ.js → sync-N633SQSS.js} +2 -2
- package/package.json +11 -11
- package/dist/tui/chunk-KMLSJRYV.js.map +0 -1
- /package/dist/tui/{create-HYFBTBLR.js.map → create-FD4E626Z.js.map} +0 -0
- /package/dist/tui/{sync-QDOJ7INZ.js.map → sync-N633SQSS.js.map} +0 -0
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
|
-
|
|
860
|
+
clearProjectDaemonRecord,
|
|
861
861
|
ensureDaemonRunning,
|
|
862
862
|
pidAlive as pidAlive2,
|
|
863
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
3467
|
-
const reason = (typeof
|
|
3468
|
-
const logged =
|
|
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 =
|
|
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 =
|
|
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
|
|
4097
|
+
import { loadProjectInfo as loadProjectInfo11 } from "@noir-ai/core";
|
|
3958
4098
|
function loadRunConfig(root) {
|
|
3959
4099
|
try {
|
|
3960
|
-
return
|
|
4100
|
+
return loadProjectInfo11(root).config;
|
|
3961
4101
|
} catch {
|
|
3962
4102
|
return null;
|
|
3963
4103
|
}
|
|
3964
4104
|
}
|
|
3965
|
-
function expandEnvVars(value,
|
|
4105
|
+
function expandEnvVars(value, env2) {
|
|
3966
4106
|
return value.replace(
|
|
3967
4107
|
/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
|
|
3968
|
-
(match, name) =>
|
|
4108
|
+
(match, name) => env2[name] !== void 0 ? env2[name] : match
|
|
3969
4109
|
);
|
|
3970
4110
|
}
|
|
3971
|
-
function resolveRunProfile(requested, config,
|
|
4111
|
+
function resolveRunProfile(requested, config, env2) {
|
|
3972
4112
|
const profiles = config.run?.profiles ?? {};
|
|
3973
4113
|
const names = Object.keys(profiles);
|
|
3974
|
-
const name = requested ??
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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)
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
5125
|
+
import { existsSync as existsSync3 } from "fs";
|
|
4956
5126
|
import { resolveAdapter as resolveAdapter6 } from "@noir-ai/adapters";
|
|
4957
|
-
import { loadProjectInfo as
|
|
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 =
|
|
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 (!
|
|
5194
|
+
if (!existsSync3(paths.config(root))) return "claude";
|
|
5025
5195
|
try {
|
|
5026
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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" :
|
|
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 ??
|
|
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({
|
|
5863
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
5906
|
-
let client;
|
|
6196
|
+
const { client, stop } = await resolveDaemon(opts);
|
|
5907
6197
|
try {
|
|
5908
|
-
|
|
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
|
-
|
|
5917
|
-
|
|
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 =
|
|
5931
|
-
{ name: "noir-cli", version: NOIR_VERSION5 },
|
|
5932
|
-
{ versionNegotiation: { mode: "auto" } }
|
|
5933
|
-
);
|
|
6212
|
+
client = newClient();
|
|
5934
6213
|
const connected = client;
|
|
5935
|
-
await connected.connect(
|
|
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 ??
|
|
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:
|
|
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 =
|
|
6034
|
-
{ name: "noir-cli", version: NOIR_VERSION5 },
|
|
6035
|
-
{ versionNegotiation: { mode: "auto" } }
|
|
6036
|
-
);
|
|
6312
|
+
client = newClient();
|
|
6037
6313
|
const connected = client;
|
|
6038
|
-
await
|
|
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
|
|
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
|
-
|
|
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,
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
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
|
|
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 (!
|
|
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) => !
|
|
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) =>
|
|
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 (!
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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
|
-
|
|
6699
|
-
|
|
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
|
});
|