@noir-ai/cli 1.0.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/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/bin.d.ts +14 -0
- package/dist/bin.js +1445 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-UXT7YWQY.js +530 -0
- package/dist/chunk-UXT7YWQY.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/output-De3lKbZq.d.ts +38 -0
- package/dist/sync-7YLRJYMR.js +20 -0
- package/dist/sync-7YLRJYMR.js.map +1 -0
- package/package.json +69 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,1445 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
EXIT,
|
|
4
|
+
NoirCliError,
|
|
5
|
+
doctor,
|
|
6
|
+
fail,
|
|
7
|
+
handleError,
|
|
8
|
+
inferExitCode,
|
|
9
|
+
info,
|
|
10
|
+
init,
|
|
11
|
+
isInteractive,
|
|
12
|
+
log,
|
|
13
|
+
serve,
|
|
14
|
+
table
|
|
15
|
+
} from "./chunk-UXT7YWQY.js";
|
|
16
|
+
|
|
17
|
+
// src/bin.ts
|
|
18
|
+
import { pathToFileURL } from "url";
|
|
19
|
+
import { NOIR_VERSION as NOIR_VERSION3 } from "@noir-ai/core";
|
|
20
|
+
import { Command, Option } from "commander";
|
|
21
|
+
|
|
22
|
+
// src/daemon-client.ts
|
|
23
|
+
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
24
|
+
import { loadProjectInfo, NOIR_VERSION } from "@noir-ai/core";
|
|
25
|
+
import { ensureDaemonRunning, pidAlive, readDaemonRecord } from "@noir-ai/daemon";
|
|
26
|
+
var DAEMON_DOWN_HINT = "daemon not reachable \u2014 try `noir daemon start`";
|
|
27
|
+
function describeCause(cause) {
|
|
28
|
+
if (cause instanceof Error) return `${cause.name}: ${cause.message}`;
|
|
29
|
+
try {
|
|
30
|
+
return JSON.stringify(cause);
|
|
31
|
+
} catch {
|
|
32
|
+
return String(cause);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function failDaemonDown(opts, cause) {
|
|
36
|
+
if (opts.verbose) {
|
|
37
|
+
process.stderr.write(`noir: daemon transport detail: ${describeCause(cause)}
|
|
38
|
+
`);
|
|
39
|
+
}
|
|
40
|
+
fail(EXIT.DAEMON_DOWN, DAEMON_DOWN_HINT, opts);
|
|
41
|
+
}
|
|
42
|
+
async function probeDaemon(opts = {}) {
|
|
43
|
+
const rec = readDaemonRecord();
|
|
44
|
+
if (!rec) {
|
|
45
|
+
if (opts.verbose) process.stderr.write("noir: daemon probe: no daemon record\n");
|
|
46
|
+
return { running: false };
|
|
47
|
+
}
|
|
48
|
+
if (!pidAlive(rec.pid)) {
|
|
49
|
+
if (opts.verbose) process.stderr.write(`noir: daemon probe: pid ${rec.pid} not alive
|
|
50
|
+
`);
|
|
51
|
+
return { running: false };
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const res = await fetch(`http://127.0.0.1:${rec.port}/health`);
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
if (opts.verbose) process.stderr.write(`noir: daemon probe: /health \u2192 HTTP ${res.status}
|
|
57
|
+
`);
|
|
58
|
+
return { running: false };
|
|
59
|
+
}
|
|
60
|
+
const body = await res.json().catch(() => null);
|
|
61
|
+
if (!body || body.ok !== true) {
|
|
62
|
+
if (opts.verbose) process.stderr.write("noir: daemon probe: /health body not ok\n");
|
|
63
|
+
return { running: false };
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
running: true,
|
|
67
|
+
pid: typeof body.pid === "number" ? body.pid : rec.pid,
|
|
68
|
+
port: rec.port,
|
|
69
|
+
uptimeSec: typeof body.uptimeSec === "number" ? body.uptimeSec : void 0
|
|
70
|
+
};
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (opts.verbose)
|
|
73
|
+
process.stderr.write(`noir: daemon probe: unreachable (${describeCause(err)})
|
|
74
|
+
`);
|
|
75
|
+
return { running: false };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function resolveDaemon(opts) {
|
|
79
|
+
const project = opts.project ?? loadProjectInfo(process.cwd());
|
|
80
|
+
const idleTimeoutSec = opts.idleTimeoutSec ?? project.config.daemon.idleTimeoutSec;
|
|
81
|
+
try {
|
|
82
|
+
const ensured = await ensureDaemonRunning({ project, idleTimeoutSec });
|
|
83
|
+
return { url: ensured.url, stop: ensured.stop };
|
|
84
|
+
} catch (err) {
|
|
85
|
+
failDaemonDown(opts, err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function connectClient(client, url, opts) {
|
|
89
|
+
try {
|
|
90
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(url)));
|
|
91
|
+
} catch (err) {
|
|
92
|
+
failDaemonDown(opts, err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function callToolParse(client, opts, name, args) {
|
|
96
|
+
const result = await client.callTool({ name, arguments: args }).catch((err) => failDaemonDown(opts, err));
|
|
97
|
+
const block = result.content?.[0];
|
|
98
|
+
if (!block || block.type !== "text" || typeof block.text !== "string") {
|
|
99
|
+
failDaemonDown(opts, new Error(`tool '${name}' returned no text content`));
|
|
100
|
+
}
|
|
101
|
+
const text = block.text;
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(text);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
failDaemonDown(opts, err);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function buildCaller(client, opts) {
|
|
109
|
+
return {
|
|
110
|
+
callTool: (name, args = {}) => callToolParse(client, opts, name, args),
|
|
111
|
+
listTools: async () => {
|
|
112
|
+
const res = await client.listTools().catch((err) => failDaemonDown(opts, err));
|
|
113
|
+
const tools = res?.tools;
|
|
114
|
+
if (!Array.isArray(tools)) return [];
|
|
115
|
+
const names = [];
|
|
116
|
+
for (const t of tools) {
|
|
117
|
+
const name = t?.name;
|
|
118
|
+
if (typeof name === "string") names.push(name);
|
|
119
|
+
}
|
|
120
|
+
return names;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
async function withDaemon(opts, fn) {
|
|
125
|
+
const { url, stop } = await resolveDaemon(opts);
|
|
126
|
+
let client;
|
|
127
|
+
try {
|
|
128
|
+
client = new Client(
|
|
129
|
+
{ name: "noir-cli", version: NOIR_VERSION },
|
|
130
|
+
{ versionNegotiation: { mode: "auto" } }
|
|
131
|
+
);
|
|
132
|
+
const connected = client;
|
|
133
|
+
await connectClient(connected, url, opts);
|
|
134
|
+
return await fn(buildCaller(connected, opts));
|
|
135
|
+
} finally {
|
|
136
|
+
if (client) {
|
|
137
|
+
await client.close().catch(() => {
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
await stop().catch(() => {
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async function withRunningDaemon(opts, fn, probe) {
|
|
145
|
+
const p = probe ?? await probeDaemon(opts);
|
|
146
|
+
if (!p.running) return null;
|
|
147
|
+
const url = `http://127.0.0.1:${p.port}/mcp`;
|
|
148
|
+
let client;
|
|
149
|
+
try {
|
|
150
|
+
client = new Client(
|
|
151
|
+
{ name: "noir-cli", version: NOIR_VERSION },
|
|
152
|
+
{ versionNegotiation: { mode: "auto" } }
|
|
153
|
+
);
|
|
154
|
+
const connected = client;
|
|
155
|
+
await connected.connect(new StreamableHTTPClientTransport(new URL(url)));
|
|
156
|
+
return await fn(buildCaller(connected, opts));
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
} finally {
|
|
160
|
+
if (client) {
|
|
161
|
+
await client.close().catch(() => {
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async function callDaemonTool(opts, name, args = {}) {
|
|
167
|
+
return withDaemon(opts, (caller) => caller.callTool(name, args));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/commands/context.ts
|
|
171
|
+
function parseLimit(raw, label, opts) {
|
|
172
|
+
if (raw === void 0) return void 0;
|
|
173
|
+
const n = Number(raw);
|
|
174
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
175
|
+
fail(EXIT.USAGE, `${label}: --limit must be a positive integer (got '${raw}')`, opts);
|
|
176
|
+
}
|
|
177
|
+
return n;
|
|
178
|
+
}
|
|
179
|
+
function failTool(label, envelope, opts) {
|
|
180
|
+
const detail = envelope.error ?? envelope.reason ?? "unknown failure";
|
|
181
|
+
fail(EXIT.ERROR, `${label}: ${detail}`, opts);
|
|
182
|
+
}
|
|
183
|
+
function toHit(raw) {
|
|
184
|
+
const h = raw ?? {};
|
|
185
|
+
return {
|
|
186
|
+
path: typeof h["path"] === "string" ? h["path"] : "<unknown>",
|
|
187
|
+
score: typeof h["score"] === "number" ? h["score"] : 0,
|
|
188
|
+
snippet: typeof h["snippet"] === "string" ? h["snippet"] : "",
|
|
189
|
+
source: typeof h["source"] === "string" ? h["source"] : ""
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
async function contextSearch(opts) {
|
|
193
|
+
const limit = parseLimit(opts.limit, "context search", opts);
|
|
194
|
+
const res = await callDaemonTool(opts, "context_search", {
|
|
195
|
+
query: opts.query,
|
|
196
|
+
...limit === void 0 ? {} : { limit }
|
|
197
|
+
});
|
|
198
|
+
if (res.ok !== true) failTool("context search", res, opts);
|
|
199
|
+
const data = {
|
|
200
|
+
query: opts.query,
|
|
201
|
+
hits: Array.isArray(res.results) ? res.results.map(toHit) : [],
|
|
202
|
+
consumedTokens: typeof res.consumedTokens === "number" ? res.consumedTokens : 0,
|
|
203
|
+
truncated: res.truncated === true,
|
|
204
|
+
degraded: res.degraded === true,
|
|
205
|
+
mode: typeof res.mode === "string" ? res.mode : "unknown"
|
|
206
|
+
};
|
|
207
|
+
if (opts.json === true) {
|
|
208
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
209
|
+
`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
renderSearch(data, opts);
|
|
213
|
+
}
|
|
214
|
+
function renderSearch(data, opts) {
|
|
215
|
+
const flag = data.degraded ? " [degraded: BM25-only]" : "";
|
|
216
|
+
const trunc = data.truncated ? " (budget hit \u2014 results truncated)" : "";
|
|
217
|
+
log(
|
|
218
|
+
`context search \u2014 ${data.hits.length} hit${data.hits.length === 1 ? "" : "s"} \xB7 ${data.mode} \xB7 ${data.consumedTokens} tokens${flag}${trunc}`,
|
|
219
|
+
opts
|
|
220
|
+
);
|
|
221
|
+
table(
|
|
222
|
+
data.hits.map((h, i) => ({
|
|
223
|
+
"#": i + 1,
|
|
224
|
+
Path: h.path,
|
|
225
|
+
Score: h.score.toFixed(4),
|
|
226
|
+
Snippet: h.snippet
|
|
227
|
+
})),
|
|
228
|
+
["#", "Path", "Score", "Snippet"],
|
|
229
|
+
opts
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
async function contextIndex(opts) {
|
|
233
|
+
if (opts.force === true) {
|
|
234
|
+
info(
|
|
235
|
+
"context index: --force is recognized but not yet honored (content-hash is always incremental).",
|
|
236
|
+
opts
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const args = opts.paths && opts.paths.length > 0 ? { paths: opts.paths } : {};
|
|
240
|
+
const res = await callDaemonTool(opts, "context_index", args);
|
|
241
|
+
if (res.ok !== true) failTool("context index", res, opts);
|
|
242
|
+
const data = {
|
|
243
|
+
indexed: typeof res.indexed === "number" ? res.indexed : 0,
|
|
244
|
+
skipped: typeof res.skipped === "number" ? res.skipped : 0,
|
|
245
|
+
deleted: typeof res.deleted === "number" ? res.deleted : 0,
|
|
246
|
+
failed: typeof res.failed === "number" ? res.failed : 0,
|
|
247
|
+
totalChunks: typeof res.totalChunks === "number" ? res.totalChunks : 0,
|
|
248
|
+
degraded: res.degraded === true
|
|
249
|
+
};
|
|
250
|
+
if (opts.json === true) {
|
|
251
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
252
|
+
`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
renderIndex(data, opts.paths, opts);
|
|
256
|
+
}
|
|
257
|
+
function renderIndex(data, paths, opts) {
|
|
258
|
+
const scope = paths && paths.length > 0 ? paths.join(", ") : ".";
|
|
259
|
+
const flag = data.degraded ? " [degraded: vectors skipped]" : "";
|
|
260
|
+
log(`context index \u2014 ${scope}${flag}`, opts);
|
|
261
|
+
table(
|
|
262
|
+
[
|
|
263
|
+
{ Field: "Indexed (new)", Value: data.indexed },
|
|
264
|
+
{ Field: "Skipped (unchanged)", Value: data.skipped },
|
|
265
|
+
{ Field: "Deleted (removed)", Value: data.deleted },
|
|
266
|
+
{ Field: "Failed", Value: data.failed },
|
|
267
|
+
{ Field: "Total chunks", Value: data.totalChunks }
|
|
268
|
+
],
|
|
269
|
+
["Field", "Value"],
|
|
270
|
+
opts
|
|
271
|
+
);
|
|
272
|
+
if (data.failed > 0) {
|
|
273
|
+
info(`${data.failed} file(s) could not be indexed (binary / IO / encoding).`, opts);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function contextStatus(opts) {
|
|
277
|
+
const res = await callDaemonTool(opts, "context_status");
|
|
278
|
+
if (res.ok !== true) failTool("context status", res, opts);
|
|
279
|
+
const data = res;
|
|
280
|
+
if (opts.json === true) {
|
|
281
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
282
|
+
`);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
renderStatus(data, opts);
|
|
286
|
+
}
|
|
287
|
+
function describeEmbedder(e) {
|
|
288
|
+
if (!e || e.kind === "none") return "none (BM25-only)";
|
|
289
|
+
const model = typeof e.model === "string" && e.model.length > 0 ? e.model : "<unset>";
|
|
290
|
+
return `${e.kind} \xB7 ${model} (${e.dim}-dim)`;
|
|
291
|
+
}
|
|
292
|
+
function renderStatus(data, opts) {
|
|
293
|
+
const flag = data.degraded ? " [degraded]" : "";
|
|
294
|
+
log(`context status \u2014 ${data.projectId}${flag}`, opts);
|
|
295
|
+
table(
|
|
296
|
+
[
|
|
297
|
+
{ Field: "Project", Value: data.projectId },
|
|
298
|
+
{ Field: "Docs", Value: data.docCount },
|
|
299
|
+
{ Field: "Vectors", Value: data.vecCount },
|
|
300
|
+
{ Field: "Indexed files", Value: data.indexedFiles },
|
|
301
|
+
{ Field: "Embedder", Value: describeEmbedder(data.embedder) },
|
|
302
|
+
{ Field: "Degraded", Value: data.degraded ? "yes" : "no" }
|
|
303
|
+
],
|
|
304
|
+
["Field", "Value"],
|
|
305
|
+
opts
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/commands/daemon.ts
|
|
310
|
+
import { loadProjectInfo as loadProjectInfo2 } from "@noir-ai/core";
|
|
311
|
+
import {
|
|
312
|
+
clearDaemonRecord,
|
|
313
|
+
ensureDaemonRunning as ensureDaemonRunning2,
|
|
314
|
+
pidAlive as pidAlive2,
|
|
315
|
+
readDaemonRecord as readDaemonRecord2
|
|
316
|
+
} from "@noir-ai/daemon";
|
|
317
|
+
var MODE = "foreground";
|
|
318
|
+
function formatUptime(sec) {
|
|
319
|
+
if (!Number.isFinite(sec) || sec < 0) return "unknown";
|
|
320
|
+
if (sec < 60) return `${sec}s`;
|
|
321
|
+
if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
322
|
+
return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
|
|
323
|
+
}
|
|
324
|
+
async function daemonStart(opts) {
|
|
325
|
+
if (opts.detach === true) {
|
|
326
|
+
fail(EXIT.USAGE, "not implemented (tracked: v1.x)", opts);
|
|
327
|
+
}
|
|
328
|
+
const project = loadProjectInfo2(process.cwd());
|
|
329
|
+
const ensured = await ensureDaemonRunning2({
|
|
330
|
+
project,
|
|
331
|
+
idleTimeoutSec: project.config.daemon.idleTimeoutSec
|
|
332
|
+
});
|
|
333
|
+
if (ensured.started) {
|
|
334
|
+
if (opts.json === true) {
|
|
335
|
+
process.stdout.write(
|
|
336
|
+
`${JSON.stringify({
|
|
337
|
+
ok: true,
|
|
338
|
+
data: {
|
|
339
|
+
url: ensured.url,
|
|
340
|
+
port: ensured.port,
|
|
341
|
+
pid: process.pid,
|
|
342
|
+
mode: MODE,
|
|
343
|
+
reused: false
|
|
344
|
+
}
|
|
345
|
+
})}
|
|
346
|
+
`
|
|
347
|
+
);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
info("noir daemon: foreground mode (backgrounding deferred to v1.x). Ctrl+C to stop.", opts);
|
|
351
|
+
log(`Noir daemon listening at ${ensured.url}`, opts);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (opts.json === true) {
|
|
355
|
+
process.stdout.write(
|
|
356
|
+
`${JSON.stringify({ ok: true, data: { url: ensured.url, port: ensured.port, reused: true } })}
|
|
357
|
+
`
|
|
358
|
+
);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
log(`Noir daemon already running at ${ensured.url}`, opts);
|
|
362
|
+
}
|
|
363
|
+
async function daemonStop(opts) {
|
|
364
|
+
const rec = readDaemonRecord2();
|
|
365
|
+
if (!rec) {
|
|
366
|
+
if (opts.json === true) {
|
|
367
|
+
process.stdout.write(
|
|
368
|
+
`${JSON.stringify({ ok: true, data: { running: false, stopped: false } })}
|
|
369
|
+
`
|
|
370
|
+
);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
info("No Noir daemon is running.", opts);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
let signalled = false;
|
|
377
|
+
let errMsg;
|
|
378
|
+
try {
|
|
379
|
+
process.kill(rec.pid, "SIGTERM");
|
|
380
|
+
signalled = true;
|
|
381
|
+
} catch (err) {
|
|
382
|
+
errMsg = err instanceof Error ? err.message : String(err);
|
|
383
|
+
} finally {
|
|
384
|
+
clearDaemonRecord();
|
|
385
|
+
}
|
|
386
|
+
if (opts.json === true) {
|
|
387
|
+
process.stdout.write(
|
|
388
|
+
`${JSON.stringify({
|
|
389
|
+
ok: true,
|
|
390
|
+
data: {
|
|
391
|
+
running: signalled,
|
|
392
|
+
stopped: signalled,
|
|
393
|
+
pid: rec.pid,
|
|
394
|
+
...errMsg !== void 0 ? { error: errMsg } : {}
|
|
395
|
+
}
|
|
396
|
+
})}
|
|
397
|
+
`
|
|
398
|
+
);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (signalled) {
|
|
402
|
+
log(`Stopped Noir daemon (pid ${rec.pid}).`, opts);
|
|
403
|
+
} else {
|
|
404
|
+
info(`Noir daemon (pid ${rec.pid}) could not be signalled: ${errMsg}`, opts);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
async function daemonStatus(opts) {
|
|
408
|
+
const rec = readDaemonRecord2();
|
|
409
|
+
if (!rec) {
|
|
410
|
+
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (start with `noir daemon start`).", opts);
|
|
411
|
+
}
|
|
412
|
+
if (!pidAlive2(rec.pid)) {
|
|
413
|
+
clearDaemonRecord();
|
|
414
|
+
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
|
|
415
|
+
}
|
|
416
|
+
let health = null;
|
|
417
|
+
try {
|
|
418
|
+
const res = await fetch(`http://127.0.0.1:${rec.port}/health`);
|
|
419
|
+
if (res.status === 200) {
|
|
420
|
+
health = await res.json();
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
health = null;
|
|
424
|
+
}
|
|
425
|
+
if (!health || health.ok !== true) {
|
|
426
|
+
clearDaemonRecord();
|
|
427
|
+
fail(
|
|
428
|
+
EXIT.DAEMON_DOWN,
|
|
429
|
+
"Noir daemon is not running (port unresponsive; stale record removed).",
|
|
430
|
+
opts
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
const uptimeSec = typeof health?.uptimeSec === "number" ? health.uptimeSec : Math.max(0, Math.floor((Date.now() - rec.startedAt) / 1e3));
|
|
434
|
+
const data = {
|
|
435
|
+
running: true,
|
|
436
|
+
pid: rec.pid,
|
|
437
|
+
port: rec.port,
|
|
438
|
+
startedAt: rec.startedAt,
|
|
439
|
+
uptimeSec,
|
|
440
|
+
mode: MODE
|
|
441
|
+
};
|
|
442
|
+
if (opts.json === true) {
|
|
443
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
444
|
+
`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
log(
|
|
448
|
+
`Noir daemon: running (pid ${data.pid}, port ${data.port}, up ${formatUptime(uptimeSec)}, ${data.mode})`,
|
|
449
|
+
opts
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
async function daemonRestart(opts) {
|
|
453
|
+
await daemonStop({ ...opts, json: false, quiet: true });
|
|
454
|
+
await daemonStart(opts);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/commands/home.ts
|
|
458
|
+
import { loadProjectInfo as loadProjectInfo3 } from "@noir-ai/core";
|
|
459
|
+
function tryProject() {
|
|
460
|
+
try {
|
|
461
|
+
const info2 = loadProjectInfo3(process.cwd());
|
|
462
|
+
return { id: info2.id, name: info2.name };
|
|
463
|
+
} catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
async function home(opts, deps) {
|
|
468
|
+
if (isInteractive(opts)) {
|
|
469
|
+
await runMenu(opts, deps);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
if (opts.json === true) {
|
|
473
|
+
await deps.dispatch(["status", "--json"]);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
await deps.dispatch(["status"]);
|
|
477
|
+
}
|
|
478
|
+
async function runMenu(opts, deps) {
|
|
479
|
+
const clack = await import("@clack/prompts");
|
|
480
|
+
const project = tryProject();
|
|
481
|
+
const banner = project ? `noir \u2014 ${project.name}` : "noir";
|
|
482
|
+
clack.intro(banner);
|
|
483
|
+
const choice = await clack.select({
|
|
484
|
+
message: "What would you like to do?",
|
|
485
|
+
initialValue: "status",
|
|
486
|
+
options: [
|
|
487
|
+
{ value: "status", label: "Status", hint: "project + daemon + store snapshot" },
|
|
488
|
+
{ value: "index", label: "Index project", hint: "(re)index files into context" },
|
|
489
|
+
{ value: "recall", label: "Recall memory", hint: "search cross-session memory" },
|
|
490
|
+
{ value: "next", label: "Next task", hint: "suggest next phase + skill" },
|
|
491
|
+
{ value: "daemon", label: "Start daemon", hint: "foreground daemon" },
|
|
492
|
+
{ value: "sync", label: "Sync skills", hint: "re-emit builtin skills" },
|
|
493
|
+
{ value: "exit", label: "Exit" }
|
|
494
|
+
]
|
|
495
|
+
});
|
|
496
|
+
if (clack.isCancel(choice)) {
|
|
497
|
+
clack.cancel("Cancelled.");
|
|
498
|
+
fail(EXIT.CANCELLED, "cancelled", opts);
|
|
499
|
+
}
|
|
500
|
+
const argv = await argvForChoice(choice, clack, opts);
|
|
501
|
+
if (argv === null) {
|
|
502
|
+
clack.outro("bye");
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
await deps.dispatch(argv);
|
|
506
|
+
clack.outro("done");
|
|
507
|
+
}
|
|
508
|
+
async function argvForChoice(choice, clack, opts) {
|
|
509
|
+
switch (choice) {
|
|
510
|
+
case "status":
|
|
511
|
+
return ["status"];
|
|
512
|
+
case "index":
|
|
513
|
+
return ["context", "index"];
|
|
514
|
+
case "next":
|
|
515
|
+
return ["task", "next"];
|
|
516
|
+
case "daemon":
|
|
517
|
+
return ["daemon", "start"];
|
|
518
|
+
case "sync":
|
|
519
|
+
return ["sync"];
|
|
520
|
+
case "exit":
|
|
521
|
+
return null;
|
|
522
|
+
case "recall": {
|
|
523
|
+
const query = await clack.text({
|
|
524
|
+
message: "Recall query:",
|
|
525
|
+
placeholder: "e.g. auth flow, ContextEngine, deploy steps"
|
|
526
|
+
});
|
|
527
|
+
if (clack.isCancel(query)) {
|
|
528
|
+
clack.cancel("Cancelled.");
|
|
529
|
+
fail(EXIT.CANCELLED, "cancelled", opts);
|
|
530
|
+
}
|
|
531
|
+
return ["memory", "recall", String(query)];
|
|
532
|
+
}
|
|
533
|
+
default:
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/commands/memory.ts
|
|
539
|
+
function parseLimit2(raw, label, opts) {
|
|
540
|
+
if (raw === void 0) return void 0;
|
|
541
|
+
const n = Number(raw);
|
|
542
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
543
|
+
fail(EXIT.USAGE, `${label}: --limit must be a positive integer (got '${raw}')`, opts);
|
|
544
|
+
}
|
|
545
|
+
return n;
|
|
546
|
+
}
|
|
547
|
+
function splitCsv(raw) {
|
|
548
|
+
if (raw === void 0) return void 0;
|
|
549
|
+
const out = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
550
|
+
return out.length > 0 ? out : void 0;
|
|
551
|
+
}
|
|
552
|
+
function toHit2(raw) {
|
|
553
|
+
const h = raw ?? {};
|
|
554
|
+
const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
555
|
+
return {
|
|
556
|
+
id: typeof h["id"] === "string" ? h["id"] : "",
|
|
557
|
+
type: typeof h["type"] === "string" ? h["type"] : "",
|
|
558
|
+
score: typeof h["score"] === "number" ? h["score"] : 0,
|
|
559
|
+
content: typeof h["content"] === "string" ? h["content"] : "",
|
|
560
|
+
concepts: arr(h["concepts"]),
|
|
561
|
+
files: arr(h["files"]),
|
|
562
|
+
ts: typeof h["ts"] === "number" ? h["ts"] : 0,
|
|
563
|
+
importance: typeof h["importance"] === "number" ? h["importance"] : 0,
|
|
564
|
+
source: typeof h["source"] === "string" ? h["source"] : ""
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function failTool2(label, envelope, opts) {
|
|
568
|
+
const detail = envelope.error ?? envelope.reason ?? "unknown failure";
|
|
569
|
+
fail(EXIT.ERROR, `${label}: ${detail}`, opts);
|
|
570
|
+
}
|
|
571
|
+
function stamp(ts) {
|
|
572
|
+
if (typeof ts !== "number" || ts <= 0) return "-";
|
|
573
|
+
return new Date(ts).toISOString().replace("T", " ").replace(/\..*$/, "Z");
|
|
574
|
+
}
|
|
575
|
+
async function memoryRecall(opts) {
|
|
576
|
+
const limit = parseLimit2(opts.limit, "memory recall", opts);
|
|
577
|
+
const res = await callDaemonTool(opts, "memory_recall", {
|
|
578
|
+
query: opts.query,
|
|
579
|
+
...limit === void 0 ? {} : { limit }
|
|
580
|
+
});
|
|
581
|
+
if (res.ok !== true) failTool2("memory recall", res, opts);
|
|
582
|
+
const hits = Array.isArray(res.results) ? res.results.map(toHit2) : [];
|
|
583
|
+
const data = { query: opts.query, hits, degraded: res.degraded === true };
|
|
584
|
+
if (opts.json === true) {
|
|
585
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
586
|
+
`);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
renderRecall(data.query, hits, data.degraded, opts);
|
|
590
|
+
}
|
|
591
|
+
function renderRecall(query, hits, degraded, opts) {
|
|
592
|
+
const flag = degraded ? " [degraded: BM25-only]" : "";
|
|
593
|
+
log(
|
|
594
|
+
`memory recall \u2014 ${hits.length} hit${hits.length === 1 ? "" : "s"} for '${query}'${flag}`,
|
|
595
|
+
opts
|
|
596
|
+
);
|
|
597
|
+
if (hits.length === 0) {
|
|
598
|
+
info("(no memories matched)", opts);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
for (let i = 0; i < hits.length; i++) {
|
|
602
|
+
const h = hits[i];
|
|
603
|
+
if (h === void 0) continue;
|
|
604
|
+
const head = `[${i + 1}] ${h.type} \xB7 score ${h.score.toFixed(4)} \xB7 importance ${h.importance.toFixed(2)} \xB7 ${h.id}`;
|
|
605
|
+
log(head, opts);
|
|
606
|
+
if (h.files.length > 0) log(` files: ${h.files.join(", ")}`, opts);
|
|
607
|
+
if (h.concepts.length > 0) log(` tags: ${h.concepts.join(", ")}`, opts);
|
|
608
|
+
log(` ${h.content}`, opts);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
async function memorySave(opts) {
|
|
612
|
+
const content = await resolveContent(opts);
|
|
613
|
+
const files = splitCsv(opts.files);
|
|
614
|
+
const args = { content };
|
|
615
|
+
if (typeof opts.type === "string" && opts.type.length > 0) args["type"] = opts.type;
|
|
616
|
+
if (files !== void 0) args["files"] = files;
|
|
617
|
+
const res = await callDaemonTool(opts, "memory_save", args);
|
|
618
|
+
if (res.ok !== true) failTool2("memory save", res, opts);
|
|
619
|
+
const id = typeof res.id === "string" ? res.id : "";
|
|
620
|
+
const observation = res.observation ?? {};
|
|
621
|
+
const data = { id, observation };
|
|
622
|
+
if (opts.json === true) {
|
|
623
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
624
|
+
`);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
log(`Saved memory ${id}.`, opts);
|
|
628
|
+
renderObservation(observation, opts);
|
|
629
|
+
}
|
|
630
|
+
async function resolveContent(opts) {
|
|
631
|
+
if (typeof opts.content === "string" && opts.content.length > 0) return opts.content;
|
|
632
|
+
if (!isInteractive(opts)) {
|
|
633
|
+
fail(
|
|
634
|
+
EXIT.USAGE,
|
|
635
|
+
"memory save requires --content <text> (or re-run in an interactive terminal to be prompted)",
|
|
636
|
+
opts
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
const clack = await import("@clack/prompts");
|
|
640
|
+
const value = await clack.text({
|
|
641
|
+
message: "Memory to save:",
|
|
642
|
+
placeholder: "the insight / decision / pattern to remember",
|
|
643
|
+
validate: (v) => v.trim().length === 0 ? "content cannot be empty" : void 0
|
|
644
|
+
});
|
|
645
|
+
if (clack.isCancel(value)) {
|
|
646
|
+
clack.cancel("Cancelled.");
|
|
647
|
+
fail(EXIT.CANCELLED, "cancelled", opts);
|
|
648
|
+
}
|
|
649
|
+
return String(value);
|
|
650
|
+
}
|
|
651
|
+
function renderObservation(obs, opts) {
|
|
652
|
+
const rows = [];
|
|
653
|
+
for (const key of ["id", "type", "importance", "ts", "source", "project", "sessionId"]) {
|
|
654
|
+
if (obs[key] !== void 0) rows.push({ Field: key, Value: obs[key] });
|
|
655
|
+
}
|
|
656
|
+
if (rows.length > 0) table(rows, ["Field", "Value"], opts);
|
|
657
|
+
const content = obs["content"];
|
|
658
|
+
if (typeof content === "string") log(`
|
|
659
|
+
${content}`, opts);
|
|
660
|
+
}
|
|
661
|
+
async function memorySessions(opts) {
|
|
662
|
+
const res = await callDaemonTool(opts, "memory_sessions");
|
|
663
|
+
if (res.ok !== true) failTool2("memory sessions", res, opts);
|
|
664
|
+
const sessions = Array.isArray(res.sessions) ? res.sessions.map((raw) => {
|
|
665
|
+
const s = raw ?? {};
|
|
666
|
+
return {
|
|
667
|
+
id: typeof s["id"] === "string" ? s["id"] : "",
|
|
668
|
+
count: typeof s["count"] === "number" ? s["count"] : 0,
|
|
669
|
+
lastTs: typeof s["lastTs"] === "number" ? s["lastTs"] : 0
|
|
670
|
+
};
|
|
671
|
+
}) : [];
|
|
672
|
+
const data = { sessions };
|
|
673
|
+
if (opts.json === true) {
|
|
674
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
675
|
+
`);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
log(`memory sessions \u2014 ${sessions.length} session${sessions.length === 1 ? "" : "s"}`, opts);
|
|
679
|
+
table(
|
|
680
|
+
sessions.map((s) => ({ Session: s.id, Observations: s.count, "Last seen": stamp(s.lastTs) })),
|
|
681
|
+
["Session", "Observations", "Last seen"],
|
|
682
|
+
opts
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
async function memoryForget(opts) {
|
|
686
|
+
if (opts.ids.length === 0) {
|
|
687
|
+
fail(EXIT.USAGE, "memory forget requires at least one <id>", opts);
|
|
688
|
+
}
|
|
689
|
+
const res = await callDaemonTool(opts, "memory_forget", {
|
|
690
|
+
ids: opts.ids
|
|
691
|
+
});
|
|
692
|
+
if (res.ok !== true) failTool2("memory forget", res, opts);
|
|
693
|
+
const deleted = typeof res.deleted === "number" ? res.deleted : 0;
|
|
694
|
+
const data = { deleted, ids: opts.ids };
|
|
695
|
+
if (opts.json === true) {
|
|
696
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
697
|
+
`);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
log(`Forgot ${deleted} observation${deleted === 1 ? "" : "s"}.`, opts);
|
|
701
|
+
}
|
|
702
|
+
async function memoryConsolidate(opts) {
|
|
703
|
+
const exposed = await withDaemon(opts, async (caller) => caller.listTools());
|
|
704
|
+
if (!exposed.includes("memory_consolidate")) {
|
|
705
|
+
fail(
|
|
706
|
+
EXIT.ERROR,
|
|
707
|
+
"memory consolidate: the daemon does not expose the memory_consolidate tool. Enable it in .noir/config under memory.consolidation (enabled: true + a provider + model), then restart the daemon.",
|
|
708
|
+
opts
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
const types = splitCsv(opts.types);
|
|
712
|
+
const limit = parseLimit2(opts.limit, "memory consolidate", opts);
|
|
713
|
+
const args = {};
|
|
714
|
+
if (types !== void 0) args["types"] = types;
|
|
715
|
+
if (limit !== void 0) args["limit"] = limit;
|
|
716
|
+
const res = await callDaemonTool(
|
|
717
|
+
opts,
|
|
718
|
+
"memory_consolidate",
|
|
719
|
+
args
|
|
720
|
+
);
|
|
721
|
+
if (res.ok === true) {
|
|
722
|
+
const lessons = Array.isArray(res.lessons) ? res.lessons : [];
|
|
723
|
+
const from = Array.isArray(res.from) ? res.from : [];
|
|
724
|
+
const data = { lessons, from };
|
|
725
|
+
if (opts.json === true) {
|
|
726
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
727
|
+
`);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
log(
|
|
731
|
+
`Consolidated ${lessons.length} lesson${lessons.length === 1 ? "" : "s"} from ${from.length} observation${from.length === 1 ? "" : "s"}.`,
|
|
732
|
+
opts
|
|
733
|
+
);
|
|
734
|
+
for (let i = 0; i < lessons.length; i++) {
|
|
735
|
+
const l = lessons[i];
|
|
736
|
+
if (l === void 0) continue;
|
|
737
|
+
const obs = l ?? {};
|
|
738
|
+
const id = typeof obs["id"] === "string" ? obs["id"] : `<lesson ${i + 1}>`;
|
|
739
|
+
const content = typeof obs["content"] === "string" ? obs["content"] : "";
|
|
740
|
+
log(`- ${id}: ${content}`, opts);
|
|
741
|
+
}
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const env = res;
|
|
745
|
+
const reason = (typeof env["reason"] === "string" ? env["reason"] : void 0) ?? (typeof env["error"] === "string" ? env["error"] : void 0) ?? "unknown";
|
|
746
|
+
const logged = env["logged"] === true || env["degraded"] === true;
|
|
747
|
+
const suffix = logged ? " (logged)" : "";
|
|
748
|
+
fail(EXIT.ERROR, `memory consolidate did not run: ${reason}${suffix}`, opts);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/commands/skills.ts
|
|
752
|
+
import { claudeAdapter } from "@noir-ai/adapters";
|
|
753
|
+
import { loadProjectInfo as loadProjectInfo4 } from "@noir-ai/core";
|
|
754
|
+
import { discoverBuiltin, emitSkillsToDir } from "@noir-ai/skills";
|
|
755
|
+
var CATEGORY = {
|
|
756
|
+
"noir-brainstorm": "discovery",
|
|
757
|
+
"noir-intake": "discovery",
|
|
758
|
+
"noir-clarify": "discovery",
|
|
759
|
+
"noir-explore": "discovery",
|
|
760
|
+
"noir-spec": "spec",
|
|
761
|
+
"noir-plan": "plan",
|
|
762
|
+
"noir-execute": "execute",
|
|
763
|
+
"noir-tdd": "execute",
|
|
764
|
+
"noir-debug": "execute",
|
|
765
|
+
"noir-subagent": "execute",
|
|
766
|
+
"noir-parallel": "execute",
|
|
767
|
+
"noir-verify": "verify",
|
|
768
|
+
"noir-review": "verify",
|
|
769
|
+
"noir-security": "verify",
|
|
770
|
+
"noir-test": "verify",
|
|
771
|
+
"noir-document": "document",
|
|
772
|
+
"noir-readme": "document",
|
|
773
|
+
"noir-commit": "git",
|
|
774
|
+
"noir-branch": "git",
|
|
775
|
+
"noir-pr": "git",
|
|
776
|
+
"noir-worktree": "git",
|
|
777
|
+
"noir-recall": "memory",
|
|
778
|
+
"noir-remember": "memory",
|
|
779
|
+
"noir-checkpoint": "memory",
|
|
780
|
+
"noir-context": "context",
|
|
781
|
+
"noir-frontend": "domain",
|
|
782
|
+
"noir-backend": "domain",
|
|
783
|
+
"noir-doctor": "meta",
|
|
784
|
+
"noir-skill-author": "meta",
|
|
785
|
+
"noir-sync": "meta",
|
|
786
|
+
"noir-wrap": "meta"
|
|
787
|
+
};
|
|
788
|
+
function categoryOf(name) {
|
|
789
|
+
const mapped = CATEGORY[name];
|
|
790
|
+
if (mapped !== void 0) return mapped;
|
|
791
|
+
return name.replace(/^noir-/, "") || "general";
|
|
792
|
+
}
|
|
793
|
+
function toRow(s) {
|
|
794
|
+
const description = typeof s.frontmatter.description === "string" ? s.frontmatter.description : "";
|
|
795
|
+
return { name: s.name, category: categoryOf(s.name), description };
|
|
796
|
+
}
|
|
797
|
+
function truncate(text, max) {
|
|
798
|
+
if (text.length <= max) return text;
|
|
799
|
+
const slice = text.slice(0, Math.max(0, max - 1));
|
|
800
|
+
const boundary = slice.lastIndexOf(" ");
|
|
801
|
+
const head = boundary > 0 ? slice.slice(0, boundary) : slice;
|
|
802
|
+
return `${head.trimEnd()}\u2026`;
|
|
803
|
+
}
|
|
804
|
+
async function skillsList(opts) {
|
|
805
|
+
let builtins;
|
|
806
|
+
try {
|
|
807
|
+
builtins = discoverBuiltin();
|
|
808
|
+
} catch (err) {
|
|
809
|
+
fail(
|
|
810
|
+
EXIT.ERROR,
|
|
811
|
+
`skills list: could not discover builtin skills (${err instanceof Error ? err.message : String(err)})`,
|
|
812
|
+
opts
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
const rows = builtins.map(toRow);
|
|
816
|
+
const data = { count: rows.length, skills: rows };
|
|
817
|
+
if (opts.json === true) {
|
|
818
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
819
|
+
`);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
log(`noir skills \u2014 ${rows.length} builtin skill${rows.length === 1 ? "" : "s"}`, opts);
|
|
823
|
+
table(
|
|
824
|
+
rows.map((r) => ({
|
|
825
|
+
Skill: r.name,
|
|
826
|
+
Category: r.category,
|
|
827
|
+
Description: truncate(r.description, 80)
|
|
828
|
+
})),
|
|
829
|
+
["Skill", "Category", "Description"],
|
|
830
|
+
opts
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
async function skillsSync(opts) {
|
|
834
|
+
const root = process.cwd();
|
|
835
|
+
const project = loadProjectInfo4(root);
|
|
836
|
+
if (!claudeAdapter.skillsDir) {
|
|
837
|
+
const data2 = {
|
|
838
|
+
emitted: [],
|
|
839
|
+
references: 0,
|
|
840
|
+
host: project.config.host,
|
|
841
|
+
reason: "no-skill-emitter"
|
|
842
|
+
};
|
|
843
|
+
if (opts.json === true) {
|
|
844
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
|
|
845
|
+
`);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
info("This host has no skill emitter; nothing to sync.", opts);
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
const dir = claudeAdapter.skillsDir({ root });
|
|
852
|
+
const summary = await emitSkillsToDir(dir);
|
|
853
|
+
const data = {
|
|
854
|
+
emitted: summary.emitted,
|
|
855
|
+
references: summary.references,
|
|
856
|
+
dir
|
|
857
|
+
};
|
|
858
|
+
if (opts.json === true) {
|
|
859
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
860
|
+
`);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
log(
|
|
864
|
+
`Synced ${summary.emitted.length} Noir skill${summary.emitted.length === 1 ? "" : "s"} to ${dir}.`,
|
|
865
|
+
opts
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// src/commands/status.ts
|
|
870
|
+
import { loadProjectInfo as loadProjectInfo5, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
|
|
871
|
+
async function tryTool(caller, name) {
|
|
872
|
+
try {
|
|
873
|
+
return await caller.callTool(name);
|
|
874
|
+
} catch {
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function pickStore(s) {
|
|
879
|
+
if (!s || s.ok !== true) return null;
|
|
880
|
+
return {
|
|
881
|
+
docCount: s.docCount,
|
|
882
|
+
vecCount: s.vecCount,
|
|
883
|
+
dbPath: s.dbPath,
|
|
884
|
+
degraded: s.degraded === true
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
function describeEmbedder2(e) {
|
|
888
|
+
if (!e || e.kind === "none") return "none (BM25-only)";
|
|
889
|
+
const model = typeof e.model === "string" && e.model.length > 0 ? e.model : "<unset>";
|
|
890
|
+
return `${e.kind} \xB7 ${model} (${e.dim}-dim)`;
|
|
891
|
+
}
|
|
892
|
+
function pickContext(c) {
|
|
893
|
+
if (!c || c.ok !== true) return null;
|
|
894
|
+
return {
|
|
895
|
+
docCount: c.docCount,
|
|
896
|
+
vecCount: c.vecCount,
|
|
897
|
+
indexedFiles: c.indexedFiles,
|
|
898
|
+
embedder: describeEmbedder2(c.embedder),
|
|
899
|
+
degraded: c.degraded === true
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
function pickWorkflow(w) {
|
|
903
|
+
if (!w || w.ok !== true) return null;
|
|
904
|
+
return {
|
|
905
|
+
taskId: w.taskId,
|
|
906
|
+
phase: w.phase,
|
|
907
|
+
state: w.state,
|
|
908
|
+
mode: w.mode,
|
|
909
|
+
nextGate: w.nextGate,
|
|
910
|
+
degraded: w.degraded === true
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function pickMemory(m) {
|
|
914
|
+
if (!m || m.ok !== true || !Array.isArray(m.sessions)) return null;
|
|
915
|
+
let observations = 0;
|
|
916
|
+
for (const s of m.sessions) observations += s.count;
|
|
917
|
+
return { sessions: m.sessions.length, observations };
|
|
918
|
+
}
|
|
919
|
+
function buildPayload(project, probe, host, store, context, workflow, memory) {
|
|
920
|
+
return {
|
|
921
|
+
noir: NOIR_VERSION2,
|
|
922
|
+
project: { id: project.id, name: project.name },
|
|
923
|
+
host: project.config.host,
|
|
924
|
+
daemon: {
|
|
925
|
+
running: probe.running,
|
|
926
|
+
...probe.pid !== void 0 ? { pid: probe.pid } : {},
|
|
927
|
+
...probe.uptimeSec !== void 0 ? { uptimeSec: probe.uptimeSec } : {},
|
|
928
|
+
...host && typeof host.transport === "string" ? { transport: host.transport } : {}
|
|
929
|
+
},
|
|
930
|
+
store: pickStore(store),
|
|
931
|
+
context: pickContext(context),
|
|
932
|
+
workflow: pickWorkflow(workflow),
|
|
933
|
+
memory: pickMemory(memory)
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
function formatDuration(sec) {
|
|
937
|
+
if (typeof sec !== "number" || sec < 0) return "unknown";
|
|
938
|
+
if (sec < 60) return `${sec}s`;
|
|
939
|
+
if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
940
|
+
return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
|
|
941
|
+
}
|
|
942
|
+
function describeDaemon(d) {
|
|
943
|
+
if (!d.running) return "not running (start with `noir daemon start`)";
|
|
944
|
+
const pid = typeof d.pid === "number" ? `pid ${d.pid}` : "pid unknown";
|
|
945
|
+
const up = typeof d.uptimeSec === "number" ? `, up ${formatDuration(d.uptimeSec)}` : "";
|
|
946
|
+
const transport = typeof d.transport === "string" ? `, ${d.transport}` : "";
|
|
947
|
+
return `running (${pid}${up}${transport})`;
|
|
948
|
+
}
|
|
949
|
+
function describeStore(s) {
|
|
950
|
+
if (!s) return "unavailable (store engine not wired)";
|
|
951
|
+
const flag = s.degraded ? " [degraded: read-only]" : "";
|
|
952
|
+
return `${s.docCount} docs / ${s.vecCount} vecs${flag}`;
|
|
953
|
+
}
|
|
954
|
+
function describeContext(c) {
|
|
955
|
+
if (!c) return "unavailable (context engine not wired)";
|
|
956
|
+
const flag = c.degraded ? " [degraded]" : "";
|
|
957
|
+
return `${c.docCount} docs, ${c.vecCount} vecs, ${c.indexedFiles} files \xB7 embedder ${c.embedder}${flag}`;
|
|
958
|
+
}
|
|
959
|
+
function describeWorkflow(w) {
|
|
960
|
+
if (!w) return "no active task";
|
|
961
|
+
const gate = w.nextGate ? ` \u2192 next gate: ${w.nextGate}` : "";
|
|
962
|
+
const flag = w.degraded ? " [degraded]" : "";
|
|
963
|
+
return `${w.phase} (${w.state}, ${w.mode}) \xB7 task ${w.taskId}${gate}${flag}`;
|
|
964
|
+
}
|
|
965
|
+
function describeMemory(m) {
|
|
966
|
+
if (!m) return "unavailable (memory engine not wired)";
|
|
967
|
+
return `${m.observations} observations across ${m.sessions} session${m.sessions === 1 ? "" : "s"}`;
|
|
968
|
+
}
|
|
969
|
+
function renderHuman(p, opts) {
|
|
970
|
+
log(`noir status \u2014 ${p.project.name} (${p.project.id})`, opts);
|
|
971
|
+
table(
|
|
972
|
+
[
|
|
973
|
+
{ Field: "Project", Value: `${p.project.name} (${p.project.id})` },
|
|
974
|
+
{ Field: "Host", Value: p.host },
|
|
975
|
+
{ Field: "Noir", Value: p.noir },
|
|
976
|
+
{ Field: "Daemon", Value: describeDaemon(p.daemon) },
|
|
977
|
+
{ Field: "Store", Value: describeStore(p.store) },
|
|
978
|
+
{ Field: "Context", Value: describeContext(p.context) },
|
|
979
|
+
{ Field: "Workflow", Value: describeWorkflow(p.workflow) },
|
|
980
|
+
{ Field: "Memory", Value: describeMemory(p.memory) }
|
|
981
|
+
],
|
|
982
|
+
["Field", "Value"],
|
|
983
|
+
opts
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
async function status(opts) {
|
|
987
|
+
const project = loadProjectInfo5(process.cwd());
|
|
988
|
+
const probe = await probeDaemon(opts);
|
|
989
|
+
let host = null;
|
|
990
|
+
let store = null;
|
|
991
|
+
let context = null;
|
|
992
|
+
let workflow = null;
|
|
993
|
+
let memory = null;
|
|
994
|
+
if (probe.running) {
|
|
995
|
+
await withRunningDaemon(
|
|
996
|
+
opts,
|
|
997
|
+
async (caller) => {
|
|
998
|
+
host = await tryTool(caller, "host_status");
|
|
999
|
+
store = await tryTool(caller, "store_status");
|
|
1000
|
+
context = await tryTool(caller, "context_status");
|
|
1001
|
+
workflow = await tryTool(caller, "workflow_status");
|
|
1002
|
+
memory = await tryTool(caller, "memory_sessions");
|
|
1003
|
+
},
|
|
1004
|
+
probe
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
const payload = buildPayload(project, probe, host, store, context, workflow, memory);
|
|
1008
|
+
if (opts.json === true) {
|
|
1009
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
|
|
1010
|
+
`);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
renderHuman(payload, opts);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/commands/task.ts
|
|
1017
|
+
var PHASE_SKILL = {
|
|
1018
|
+
intake: "noir-intake",
|
|
1019
|
+
clarify: "noir-clarify",
|
|
1020
|
+
spec: "noir-spec",
|
|
1021
|
+
plan: "noir-plan",
|
|
1022
|
+
execute: "noir-execute",
|
|
1023
|
+
verify: "noir-verify",
|
|
1024
|
+
document: "noir-document"
|
|
1025
|
+
};
|
|
1026
|
+
function skillFor(phase) {
|
|
1027
|
+
if (typeof phase !== "string") return null;
|
|
1028
|
+
return PHASE_SKILL[phase] ?? null;
|
|
1029
|
+
}
|
|
1030
|
+
async function fetchStatus(opts, taskId) {
|
|
1031
|
+
const args = typeof taskId === "string" && taskId.length > 0 ? { taskId } : {};
|
|
1032
|
+
const res = await callDaemonTool(
|
|
1033
|
+
opts,
|
|
1034
|
+
"workflow_status",
|
|
1035
|
+
args
|
|
1036
|
+
);
|
|
1037
|
+
if (res.ok === true) return res;
|
|
1038
|
+
const detail = res.error ?? "task not found";
|
|
1039
|
+
fail(EXIT.NOT_FOUND, `task: ${detail}`, opts);
|
|
1040
|
+
}
|
|
1041
|
+
function formatStamp(ms) {
|
|
1042
|
+
if (typeof ms !== "number" || ms <= 0) return "-";
|
|
1043
|
+
return new Date(ms).toISOString().replace("T", " ").replace(/\..*$/, "Z");
|
|
1044
|
+
}
|
|
1045
|
+
function renderStatusRow(s, opts) {
|
|
1046
|
+
const flag = s.degraded === true ? " [degraded]" : "";
|
|
1047
|
+
log(`task status \u2014 ${s.taskId}${flag}`, opts);
|
|
1048
|
+
table(
|
|
1049
|
+
[
|
|
1050
|
+
{ Field: "Task", Value: s.taskId },
|
|
1051
|
+
{ Field: "Phase", Value: s.phase },
|
|
1052
|
+
{ Field: "State", Value: s.state },
|
|
1053
|
+
{ Field: "Mode", Value: s.mode },
|
|
1054
|
+
{ Field: "Next gate", Value: s.nextGate ?? "\u2014" },
|
|
1055
|
+
{ Field: "Updated", Value: formatStamp(s.updatedAt) }
|
|
1056
|
+
],
|
|
1057
|
+
["Field", "Value"],
|
|
1058
|
+
opts
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
async function taskStatus(opts) {
|
|
1062
|
+
const s = await fetchStatus(opts, opts.id);
|
|
1063
|
+
if (opts.json === true) {
|
|
1064
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: s })}
|
|
1065
|
+
`);
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
renderStatusRow(s, opts);
|
|
1069
|
+
}
|
|
1070
|
+
async function taskNext(opts) {
|
|
1071
|
+
const s = await fetchStatus(opts);
|
|
1072
|
+
const suggestion = skillFor(s.phase);
|
|
1073
|
+
const nextSkill = skillFor(s.nextGate);
|
|
1074
|
+
const data = {
|
|
1075
|
+
taskId: s.taskId,
|
|
1076
|
+
phase: s.phase,
|
|
1077
|
+
state: s.state,
|
|
1078
|
+
mode: s.mode,
|
|
1079
|
+
nextGate: s.nextGate,
|
|
1080
|
+
suggestion,
|
|
1081
|
+
nextSkill,
|
|
1082
|
+
degraded: s.degraded === true
|
|
1083
|
+
};
|
|
1084
|
+
if (opts.json === true) {
|
|
1085
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
1086
|
+
`);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
const flag = data.degraded ? " [degraded]" : "";
|
|
1090
|
+
log(`task next \u2014 ${s.taskId} \xB7 phase ${s.phase} (${s.state}, ${s.mode})${flag}`, opts);
|
|
1091
|
+
if (s.nextGate) {
|
|
1092
|
+
info(`next gate: ${s.nextGate}`, opts);
|
|
1093
|
+
} else {
|
|
1094
|
+
info("no further gate ahead (past verify, or stopped).", opts);
|
|
1095
|
+
}
|
|
1096
|
+
if (suggestion) info(`skill for current phase (${s.phase}): ${suggestion}`, opts);
|
|
1097
|
+
if (nextSkill) info(`skill for next gate (${s.nextGate}): ${nextSkill}`, opts);
|
|
1098
|
+
if (!suggestion && !nextSkill) {
|
|
1099
|
+
info("run `noir skills list` to see applicable skills.", opts);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
async function taskNew(opts) {
|
|
1103
|
+
let mode;
|
|
1104
|
+
if (opts.mode !== void 0) {
|
|
1105
|
+
if (opts.mode !== "full" && opts.mode !== "quick") {
|
|
1106
|
+
fail(EXIT.USAGE, `task new: invalid mode '${opts.mode}' (expected 'full' or 'quick')`, opts);
|
|
1107
|
+
}
|
|
1108
|
+
mode = opts.mode;
|
|
1109
|
+
}
|
|
1110
|
+
const res = await callDaemonTool(
|
|
1111
|
+
opts,
|
|
1112
|
+
"workflow_start",
|
|
1113
|
+
{
|
|
1114
|
+
taskId: opts.slug,
|
|
1115
|
+
slug: opts.slug,
|
|
1116
|
+
...mode === void 0 ? {} : { mode }
|
|
1117
|
+
}
|
|
1118
|
+
);
|
|
1119
|
+
if (res.ok !== true) {
|
|
1120
|
+
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "start failed";
|
|
1121
|
+
fail(EXIT.ERROR, `task new --slug ${opts.slug}: ${detail}`, opts);
|
|
1122
|
+
}
|
|
1123
|
+
if (opts.json === true) {
|
|
1124
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
|
|
1125
|
+
`);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
renderStatusRow(res, opts);
|
|
1129
|
+
}
|
|
1130
|
+
async function taskAdvance(opts) {
|
|
1131
|
+
const args = {};
|
|
1132
|
+
if (typeof opts.to === "string" && opts.to.length > 0) {
|
|
1133
|
+
if (!Object.keys(PHASE_SKILL).includes(opts.to)) {
|
|
1134
|
+
fail(EXIT.USAGE, `task advance: invalid phase '${opts.to}'`, opts);
|
|
1135
|
+
}
|
|
1136
|
+
args.to = opts.to;
|
|
1137
|
+
}
|
|
1138
|
+
if (typeof opts.force === "string" && opts.force.length > 0) {
|
|
1139
|
+
args.force = { reason: opts.force };
|
|
1140
|
+
}
|
|
1141
|
+
const res = await callDaemonTool(
|
|
1142
|
+
opts,
|
|
1143
|
+
"workflow_advance",
|
|
1144
|
+
args
|
|
1145
|
+
);
|
|
1146
|
+
if (res.ok !== true) {
|
|
1147
|
+
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "advance failed";
|
|
1148
|
+
fail(EXIT.ERROR, `task advance: ${detail}`, opts);
|
|
1149
|
+
}
|
|
1150
|
+
if (opts.json === true) {
|
|
1151
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
|
|
1152
|
+
`);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
renderStatusRow(res, opts);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/bin.ts
|
|
1159
|
+
function actionGlobals(args) {
|
|
1160
|
+
const last = args[args.length - 1];
|
|
1161
|
+
return last?.optsWithGlobals() ?? {};
|
|
1162
|
+
}
|
|
1163
|
+
function trailingCmd(args) {
|
|
1164
|
+
const last = args[args.length - 1];
|
|
1165
|
+
if (!(last instanceof Command)) {
|
|
1166
|
+
throw new NoirCliError(
|
|
1167
|
+
EXIT.ERROR,
|
|
1168
|
+
"internal: commander action callback received no trailing Command"
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
return last;
|
|
1172
|
+
}
|
|
1173
|
+
function toCliOptions(g) {
|
|
1174
|
+
return {
|
|
1175
|
+
json: g.json === true,
|
|
1176
|
+
quiet: g.quiet === true,
|
|
1177
|
+
verbose: g.verbose === true,
|
|
1178
|
+
input: g.input !== false
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
function toStatusOptions(g) {
|
|
1182
|
+
return {
|
|
1183
|
+
json: g.json === true,
|
|
1184
|
+
quiet: g.quiet === true,
|
|
1185
|
+
verbose: g.verbose === true,
|
|
1186
|
+
input: g.input !== false
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
function createProgram() {
|
|
1190
|
+
const program2 = new Command();
|
|
1191
|
+
program2.name("noir").description("Noir \u2014 discipline, context, and memory layer for agentic CLIs.").version(NOIR_VERSION3, "-v, --version").addOption(new Option("--json", "emit machine-readable JSON to stdout")).addOption(new Option("--no-input", "never prompt; error if input is required")).addOption(new Option("--quiet", "suppress non-essential diagnostics")).addOption(new Option("--verbose", "show additional diagnostic detail")).addOption(new Option("--cwd <path>", "run as if started in <path>")).exitOverride((err) => {
|
|
1192
|
+
throw err;
|
|
1193
|
+
}).configureOutput({
|
|
1194
|
+
// S9 stream discipline: data → stdout, diagnostics → stderr. Help and
|
|
1195
|
+
// errors are diagnostics, so both go to stderr (keeps stdout pristine for
|
|
1196
|
+
// --json payloads). commander's default error formatting is preserved.
|
|
1197
|
+
writeOut: (str) => process.stderr.write(str),
|
|
1198
|
+
writeErr: (str) => process.stderr.write(str)
|
|
1199
|
+
});
|
|
1200
|
+
program2.hook("preAction", (_thisCmd, actionCmd) => {
|
|
1201
|
+
const opts = actionCmd.optsWithGlobals();
|
|
1202
|
+
const cwd = opts.cwd;
|
|
1203
|
+
if (typeof cwd === "string" && cwd.length > 0) {
|
|
1204
|
+
try {
|
|
1205
|
+
process.chdir(cwd);
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
throw new NoirCliError(
|
|
1208
|
+
EXIT.USAGE,
|
|
1209
|
+
`--cwd: ${err instanceof Error ? err.message : String(err)}`
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1214
|
+
program2.command("init").description("scaffold Noir in the current project (.noir/, .mcp.json, CLAUDE.md, skills)").option("--transport <transport>", "stdio | streamable-http (default: stdio)", "stdio").option("--url <url>", "streamable-http daemon URL (localhost only)").action(async (opts) => {
|
|
1215
|
+
const transport = opts.transport === "streamable-http" ? "streamable-http" : "stdio";
|
|
1216
|
+
await init(process.cwd(), { transport, url: opts.url });
|
|
1217
|
+
});
|
|
1218
|
+
program2.command("sync").description("re-emit the Noir builtin skills into the host skills dir").action(async () => {
|
|
1219
|
+
const { sync } = await import("./sync-7YLRJYMR.js");
|
|
1220
|
+
await sync(process.cwd());
|
|
1221
|
+
});
|
|
1222
|
+
const mcpCmd = program2.command("mcp").description("MCP server control");
|
|
1223
|
+
mcpCmd.command("serve").description("run the Noir MCP server (stdio, or via the shared daemon)").option("--stdio", "force the stdio transport").action(async (opts) => {
|
|
1224
|
+
await serve({ stdio: opts.stdio === true });
|
|
1225
|
+
});
|
|
1226
|
+
mcpCmd.action(() => {
|
|
1227
|
+
throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio]");
|
|
1228
|
+
});
|
|
1229
|
+
const daemonGrp = program2.command("daemon").description("control the Noir daemon");
|
|
1230
|
+
daemonGrp.command("start").description("start the Noir daemon (foreground; backgrounding deferred)").option("--detach", "run in the background (not yet implemented; exits 2)").action(async (...args) => {
|
|
1231
|
+
const cmd = trailingCmd(args);
|
|
1232
|
+
const g = cmd.optsWithGlobals();
|
|
1233
|
+
const detach = g["detach"] === true;
|
|
1234
|
+
await daemonStart({ ...toCliOptions(g), ...detach ? { detach } : {} });
|
|
1235
|
+
});
|
|
1236
|
+
daemonGrp.command("stop").description("stop the Noir daemon").action(async (...args) => {
|
|
1237
|
+
await daemonStop(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
1238
|
+
});
|
|
1239
|
+
daemonGrp.command("status").description("report daemon pid/uptime/mode (exit 4 if not running)").action(async (...args) => {
|
|
1240
|
+
await daemonStatus(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
1241
|
+
});
|
|
1242
|
+
daemonGrp.command("restart").description("stop then start the daemon").action(async (...args) => {
|
|
1243
|
+
await daemonRestart(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
1244
|
+
});
|
|
1245
|
+
daemonGrp.action(() => {
|
|
1246
|
+
throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|stop|status|restart");
|
|
1247
|
+
});
|
|
1248
|
+
program2.command("doctor").description("environment + project health").action(async (...args) => {
|
|
1249
|
+
await doctor(toCliOptions(actionGlobals(args)));
|
|
1250
|
+
});
|
|
1251
|
+
program2.command("status").description("project + daemon + workflow + store snapshot").action(async (...args) => {
|
|
1252
|
+
await status(toStatusOptions(actionGlobals(args)));
|
|
1253
|
+
});
|
|
1254
|
+
const contextGrp = program2.command("context").description("context engine (S6)");
|
|
1255
|
+
contextGrp.command("search").description("hybrid search over the indexed context").argument("<query>", "search query").option("--limit <n>", "max results", "10").action(async (...args) => {
|
|
1256
|
+
const cmd = trailingCmd(args);
|
|
1257
|
+
const g = cmd.optsWithGlobals();
|
|
1258
|
+
const query = typeof args[0] === "string" ? args[0] : "";
|
|
1259
|
+
const limit = typeof g["limit"] === "string" ? g["limit"] : void 0;
|
|
1260
|
+
await contextSearch({
|
|
1261
|
+
...toCliOptions(g),
|
|
1262
|
+
query,
|
|
1263
|
+
...limit === void 0 ? {} : { limit }
|
|
1264
|
+
});
|
|
1265
|
+
});
|
|
1266
|
+
contextGrp.command("index").description("(re)index project files into the context store").option(
|
|
1267
|
+
"--path <p>",
|
|
1268
|
+
"path to index (repeatable)",
|
|
1269
|
+
(val, acc) => [...acc, val],
|
|
1270
|
+
[]
|
|
1271
|
+
).option("--force", "ignore content-hash caching (recognized; not yet honored)").action(async (...args) => {
|
|
1272
|
+
const cmd = trailingCmd(args);
|
|
1273
|
+
const g = cmd.optsWithGlobals();
|
|
1274
|
+
const rawPaths = Array.isArray(g["path"]) ? g["path"].filter((p) => typeof p === "string") : [];
|
|
1275
|
+
const force = g["force"] === true;
|
|
1276
|
+
await contextIndex({
|
|
1277
|
+
...toCliOptions(g),
|
|
1278
|
+
...rawPaths.length === 0 ? {} : { paths: rawPaths },
|
|
1279
|
+
...force ? { force } : {}
|
|
1280
|
+
});
|
|
1281
|
+
});
|
|
1282
|
+
contextGrp.command("status").description("index freshness + counts").action(async (...args) => {
|
|
1283
|
+
await contextStatus(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
1284
|
+
});
|
|
1285
|
+
contextGrp.action(() => {
|
|
1286
|
+
throw new NoirCliError(EXIT.USAGE, "Usage: noir context search|index|status");
|
|
1287
|
+
});
|
|
1288
|
+
const memoryGrp = program2.command("memory").description("memory engine (S7)");
|
|
1289
|
+
memoryGrp.command("recall").description("recall memories for a query").argument("<query>", "recall query").option("--limit <n>", "max results", "10").action(async (...args) => {
|
|
1290
|
+
const cmd = trailingCmd(args);
|
|
1291
|
+
const g = cmd.optsWithGlobals();
|
|
1292
|
+
const query = typeof args[0] === "string" ? args[0] : "";
|
|
1293
|
+
const limit = typeof g["limit"] === "string" ? g["limit"] : void 0;
|
|
1294
|
+
await memoryRecall({
|
|
1295
|
+
...toCliOptions(g),
|
|
1296
|
+
query,
|
|
1297
|
+
...limit === void 0 ? {} : { limit }
|
|
1298
|
+
});
|
|
1299
|
+
});
|
|
1300
|
+
memoryGrp.command("save").description("save an observation to long-term memory").option("--content <text>", "memory content (prompted interactively if omitted)").option(
|
|
1301
|
+
"--type <type>",
|
|
1302
|
+
"observation type (pattern | preference | architecture | bug | workflow | fact | decision)"
|
|
1303
|
+
).option("--files <files>", "comma-separated related file paths").action(async (...args) => {
|
|
1304
|
+
const cmd = trailingCmd(args);
|
|
1305
|
+
const g = cmd.optsWithGlobals();
|
|
1306
|
+
const content = typeof g["content"] === "string" ? g["content"] : void 0;
|
|
1307
|
+
const type = typeof g["type"] === "string" ? g["type"] : void 0;
|
|
1308
|
+
const files = typeof g["files"] === "string" ? g["files"] : void 0;
|
|
1309
|
+
await memorySave({
|
|
1310
|
+
...toCliOptions(g),
|
|
1311
|
+
...content === void 0 ? {} : { content },
|
|
1312
|
+
...type === void 0 ? {} : { type },
|
|
1313
|
+
...files === void 0 ? {} : { files }
|
|
1314
|
+
});
|
|
1315
|
+
});
|
|
1316
|
+
memoryGrp.command("sessions").description("list recent memory sessions").action(async (...args) => {
|
|
1317
|
+
await memorySessions(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
1318
|
+
});
|
|
1319
|
+
memoryGrp.command("forget").description("delete one or more memories by id").argument("<ids...>", "observation id(s)").action(async (...args) => {
|
|
1320
|
+
const cmd = trailingCmd(args);
|
|
1321
|
+
const g = cmd.optsWithGlobals();
|
|
1322
|
+
const rawIds = Array.isArray(args[0]) ? args[0] : [];
|
|
1323
|
+
const ids = rawIds.filter((x) => typeof x === "string");
|
|
1324
|
+
await memoryForget({ ...toCliOptions(g), ids });
|
|
1325
|
+
});
|
|
1326
|
+
memoryGrp.command("consolidate").description("consolidate memories into a derived lesson (provider-explicit)").option("--types <types>", "comma-separated observation types to consolidate").option("--limit <n>", "cap on candidate observations").action(async (...args) => {
|
|
1327
|
+
const cmd = trailingCmd(args);
|
|
1328
|
+
const g = cmd.optsWithGlobals();
|
|
1329
|
+
const types = typeof g["types"] === "string" ? g["types"] : void 0;
|
|
1330
|
+
const limit = typeof g["limit"] === "string" ? g["limit"] : void 0;
|
|
1331
|
+
await memoryConsolidate({
|
|
1332
|
+
...toCliOptions(g),
|
|
1333
|
+
...types === void 0 ? {} : { types },
|
|
1334
|
+
...limit === void 0 ? {} : { limit }
|
|
1335
|
+
});
|
|
1336
|
+
});
|
|
1337
|
+
memoryGrp.action(() => {
|
|
1338
|
+
throw new NoirCliError(
|
|
1339
|
+
EXIT.USAGE,
|
|
1340
|
+
"Usage: noir memory recall|save|sessions|forget|consolidate"
|
|
1341
|
+
);
|
|
1342
|
+
});
|
|
1343
|
+
const skillsGrp = program2.command("skills").description("builtin skills (S5)");
|
|
1344
|
+
skillsGrp.command("list").description("list installed Noir skills").action(async (...args) => {
|
|
1345
|
+
await skillsList(toCliOptions(actionGlobals(args)));
|
|
1346
|
+
});
|
|
1347
|
+
skillsGrp.command("sync").description("re-emit skills to the host skills dir").action(async (...args) => {
|
|
1348
|
+
await skillsSync(toCliOptions(actionGlobals(args)));
|
|
1349
|
+
});
|
|
1350
|
+
skillsGrp.action(() => {
|
|
1351
|
+
throw new NoirCliError(EXIT.USAGE, "Usage: noir skills list|sync");
|
|
1352
|
+
});
|
|
1353
|
+
const taskGrp = program2.command("task").description("workflow task control (S4)");
|
|
1354
|
+
taskGrp.command("new").description("start a new workflow task").requiredOption("--slug <slug>", "task slug").option("--mode <mode>", "full | quick").action(async (...args) => {
|
|
1355
|
+
const cmd = trailingCmd(args);
|
|
1356
|
+
const g = cmd.optsWithGlobals();
|
|
1357
|
+
const slug = typeof g["slug"] === "string" ? g["slug"] : "";
|
|
1358
|
+
const mode = typeof g["mode"] === "string" ? g["mode"] : void 0;
|
|
1359
|
+
await taskNew({
|
|
1360
|
+
...toCliOptions(g),
|
|
1361
|
+
slug,
|
|
1362
|
+
...mode === void 0 ? {} : { mode }
|
|
1363
|
+
});
|
|
1364
|
+
});
|
|
1365
|
+
taskGrp.command("status").description("active (or named) task status").argument("[id]", "task id (defaults to active)").action(async (...args) => {
|
|
1366
|
+
const cmd = trailingCmd(args);
|
|
1367
|
+
const g = cmd.optsWithGlobals();
|
|
1368
|
+
const first = args[0];
|
|
1369
|
+
const id = typeof first === "string" ? first : void 0;
|
|
1370
|
+
await taskStatus({
|
|
1371
|
+
...toCliOptions(g),
|
|
1372
|
+
...id === void 0 ? {} : { id }
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
taskGrp.command("advance").description("advance the active task to the next phase").option("--to <phase>", "target phase").option("--force <reason>", "force the gate with a reason").action(async (...args) => {
|
|
1376
|
+
const cmd = trailingCmd(args);
|
|
1377
|
+
const g = cmd.optsWithGlobals();
|
|
1378
|
+
const to = typeof g["to"] === "string" ? g["to"] : void 0;
|
|
1379
|
+
const force = typeof g["force"] === "string" ? g["force"] : void 0;
|
|
1380
|
+
await taskAdvance({
|
|
1381
|
+
...toCliOptions(g),
|
|
1382
|
+
...to === void 0 ? {} : { to },
|
|
1383
|
+
...force === void 0 ? {} : { force }
|
|
1384
|
+
});
|
|
1385
|
+
});
|
|
1386
|
+
taskGrp.command("next").description("suggest the next phase + applicable skill").action(async (...args) => {
|
|
1387
|
+
await taskNext(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
1388
|
+
});
|
|
1389
|
+
taskGrp.action(() => {
|
|
1390
|
+
throw new NoirCliError(EXIT.USAGE, "Usage: noir task new|status|advance|next");
|
|
1391
|
+
});
|
|
1392
|
+
const homeDeps = {
|
|
1393
|
+
dispatch: async (argv) => {
|
|
1394
|
+
const sub = createProgram();
|
|
1395
|
+
try {
|
|
1396
|
+
await sub.parseAsync([...argv], { from: "user" });
|
|
1397
|
+
} catch (err) {
|
|
1398
|
+
handleError(err);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
program2.action(async (...args) => {
|
|
1403
|
+
const cmd = trailingCmd(args);
|
|
1404
|
+
const leftovers = cmd.args;
|
|
1405
|
+
if (leftovers.length > 0) {
|
|
1406
|
+
fail(
|
|
1407
|
+
EXIT.NOT_FOUND,
|
|
1408
|
+
`unknown command '${leftovers[0]}' (no such subcommand). Run \`noir --help\` for the list.`,
|
|
1409
|
+
toCliOptions(cmd.optsWithGlobals())
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
await home(toCliOptions(actionGlobals(args)), homeDeps);
|
|
1413
|
+
});
|
|
1414
|
+
return program2;
|
|
1415
|
+
}
|
|
1416
|
+
var program = createProgram();
|
|
1417
|
+
async function run(argv = []) {
|
|
1418
|
+
try {
|
|
1419
|
+
await program.parseAsync([...argv], { from: "user" });
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
handleError(err);
|
|
1422
|
+
}
|
|
1423
|
+
return typeof process.exitCode === "number" ? process.exitCode : EXIT.OK;
|
|
1424
|
+
}
|
|
1425
|
+
var isMainModule = (() => {
|
|
1426
|
+
try {
|
|
1427
|
+
const entry = process.argv[1];
|
|
1428
|
+
return typeof entry === "string" && entry.length > 0 ? pathToFileURL(entry).href === import.meta.url : false;
|
|
1429
|
+
} catch {
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
})();
|
|
1433
|
+
if (isMainModule) {
|
|
1434
|
+
void run(process.argv.slice(2));
|
|
1435
|
+
}
|
|
1436
|
+
export {
|
|
1437
|
+
EXIT,
|
|
1438
|
+
NoirCliError,
|
|
1439
|
+
createProgram,
|
|
1440
|
+
fail,
|
|
1441
|
+
inferExitCode,
|
|
1442
|
+
program,
|
|
1443
|
+
run
|
|
1444
|
+
};
|
|
1445
|
+
//# sourceMappingURL=bin.js.map
|