@noir-ai/cli 1.3.0-beta.6 → 1.4.0-beta.2
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/README.md +7 -7
- package/dist/bin.d.ts +30 -2
- package/dist/bin.js +869 -582
- package/dist/bin.js.map +1 -1
- package/dist/chunk-A5W2E3X2.js +33 -0
- package/dist/chunk-A5W2E3X2.js.map +1 -0
- package/dist/{chunk-J2Y3BOVK.js → chunk-SVFQGAOF.js} +32 -8
- package/dist/chunk-SVFQGAOF.js.map +1 -0
- package/dist/{chunk-3QMQXBII.js → chunk-YES5UCU5.js} +648 -40
- package/dist/chunk-YES5UCU5.js.map +1 -0
- package/dist/{create-VK7JX6IB.js → create-7ERBL42K.js} +31 -7
- package/dist/create-7ERBL42K.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +5 -4
- package/dist/{output-De3lKbZq.d.ts → output-BNE9htEm.d.ts} +9 -0
- package/dist/sync-SCAF6Z5H.js +82 -0
- package/dist/sync-SCAF6Z5H.js.map +1 -0
- package/dist/tui/chunk-OMF7XZMN.js +99 -0
- package/dist/tui/chunk-OMF7XZMN.js.map +1 -0
- package/dist/tui/chunk-YNEBQJXA.js +1243 -0
- package/dist/tui/chunk-YNEBQJXA.js.map +1 -0
- package/dist/tui/create-U2YJH5QC.js +72 -0
- package/dist/tui/create-U2YJH5QC.js.map +1 -0
- package/dist/tui/index.js +2253 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/sync-VYRXCGK4.js +81 -0
- package/dist/tui/sync-VYRXCGK4.js.map +1 -0
- package/package.json +15 -11
- package/dist/chunk-3QMQXBII.js.map +0 -1
- package/dist/chunk-J2Y3BOVK.js.map +0 -1
- package/dist/chunk-OF4OO5W6.js +0 -190
- package/dist/chunk-OF4OO5W6.js.map +0 -1
- package/dist/create-VK7JX6IB.js.map +0 -1
- package/dist/sync-7XFGCRJN.js +0 -55
- package/dist/sync-7XFGCRJN.js.map +0 -1
|
@@ -0,0 +1,2253 @@
|
|
|
1
|
+
import {
|
|
2
|
+
init
|
|
3
|
+
} from "./chunk-OMF7XZMN.js";
|
|
4
|
+
import {
|
|
5
|
+
EXIT,
|
|
6
|
+
NoirCliError,
|
|
7
|
+
accessibleMode,
|
|
8
|
+
badge,
|
|
9
|
+
c,
|
|
10
|
+
definitionList,
|
|
11
|
+
doctor,
|
|
12
|
+
fail,
|
|
13
|
+
handleError,
|
|
14
|
+
info,
|
|
15
|
+
isInteractive,
|
|
16
|
+
json,
|
|
17
|
+
log,
|
|
18
|
+
requireInteractive,
|
|
19
|
+
table,
|
|
20
|
+
terminalWidth,
|
|
21
|
+
tip,
|
|
22
|
+
useColor,
|
|
23
|
+
warn
|
|
24
|
+
} from "./chunk-YNEBQJXA.js";
|
|
25
|
+
|
|
26
|
+
// src/tui/index.tsx
|
|
27
|
+
import { render } from "ink";
|
|
28
|
+
|
|
29
|
+
// src/commands/status.ts
|
|
30
|
+
import { loadProjectInfo as loadProjectInfo7, NOIR_VERSION as NOIR_VERSION3 } from "@noir-ai/core";
|
|
31
|
+
|
|
32
|
+
// src/daemon-client.ts
|
|
33
|
+
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
34
|
+
import { loadProjectInfo as loadProjectInfo6, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
|
|
35
|
+
import { ensureDaemonRunning as ensureDaemonRunning3, pidAlive as pidAlive2, readDaemonRecord as readDaemonRecord2 } from "@noir-ai/daemon";
|
|
36
|
+
|
|
37
|
+
// src/bin.ts
|
|
38
|
+
import { realpathSync } from "fs";
|
|
39
|
+
import { pathToFileURL } from "url";
|
|
40
|
+
import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
|
|
41
|
+
import { NOIR_VERSION } from "@noir-ai/core";
|
|
42
|
+
import { Command, Option } from "commander";
|
|
43
|
+
|
|
44
|
+
// src/commands/context.ts
|
|
45
|
+
function parseLimit(raw, label, opts) {
|
|
46
|
+
if (raw === void 0) return void 0;
|
|
47
|
+
const n = Number(raw);
|
|
48
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
49
|
+
fail(EXIT.USAGE, `${label}: --limit must be a positive integer (got '${raw}')`, opts);
|
|
50
|
+
}
|
|
51
|
+
return n;
|
|
52
|
+
}
|
|
53
|
+
function failTool(label, envelope, opts) {
|
|
54
|
+
const detail = envelope.error ?? envelope.reason ?? "unknown failure";
|
|
55
|
+
fail(EXIT.ERROR, `${label}: ${detail}`, opts);
|
|
56
|
+
}
|
|
57
|
+
function toHit(raw) {
|
|
58
|
+
const h = raw ?? {};
|
|
59
|
+
return {
|
|
60
|
+
path: typeof h.path === "string" ? h.path : "<unknown>",
|
|
61
|
+
score: typeof h.score === "number" ? h.score : 0,
|
|
62
|
+
snippet: typeof h.snippet === "string" ? h.snippet : "",
|
|
63
|
+
source: typeof h.source === "string" ? h.source : ""
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async function contextSearch(opts) {
|
|
67
|
+
const limit = parseLimit(opts.limit, "context search", opts);
|
|
68
|
+
const res = await callDaemonTool(opts, "context_search", {
|
|
69
|
+
query: opts.query,
|
|
70
|
+
...limit === void 0 ? {} : { limit }
|
|
71
|
+
});
|
|
72
|
+
if (res.ok !== true) failTool("context search", res, opts);
|
|
73
|
+
const data = {
|
|
74
|
+
query: opts.query,
|
|
75
|
+
hits: Array.isArray(res.results) ? res.results.map(toHit) : [],
|
|
76
|
+
consumedTokens: typeof res.consumedTokens === "number" ? res.consumedTokens : 0,
|
|
77
|
+
truncated: res.truncated === true,
|
|
78
|
+
degraded: res.degraded === true,
|
|
79
|
+
mode: typeof res.mode === "string" ? res.mode : "unknown"
|
|
80
|
+
};
|
|
81
|
+
if (opts.json === true) {
|
|
82
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
83
|
+
`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
renderSearch(data, opts);
|
|
87
|
+
}
|
|
88
|
+
function renderSearch(data, opts) {
|
|
89
|
+
const flag = data.degraded ? ` ${badge("warn", "degraded: BM25-only")}` : "";
|
|
90
|
+
const trunc = data.truncated ? " (budget hit \u2014 results truncated)" : "";
|
|
91
|
+
log(
|
|
92
|
+
`context search \u2014 ${data.hits.length} hit${data.hits.length === 1 ? "" : "s"} \xB7 ${data.mode} \xB7 ${data.consumedTokens} tokens${flag}${trunc}`,
|
|
93
|
+
opts
|
|
94
|
+
);
|
|
95
|
+
table(
|
|
96
|
+
data.hits.map((h, i) => ({
|
|
97
|
+
"#": i + 1,
|
|
98
|
+
Path: h.path,
|
|
99
|
+
Score: h.score.toFixed(4),
|
|
100
|
+
Snippet: h.snippet
|
|
101
|
+
})),
|
|
102
|
+
["#", "Path", "Score", "Snippet"],
|
|
103
|
+
opts
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
async function contextIndex(opts) {
|
|
107
|
+
if (opts.force === true) {
|
|
108
|
+
info(
|
|
109
|
+
"context index: --force is recognized but not yet honored (content-hash is always incremental).",
|
|
110
|
+
opts
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const args = opts.paths && opts.paths.length > 0 ? { paths: opts.paths } : {};
|
|
114
|
+
const res = await callDaemonTool(opts, "context_index", args);
|
|
115
|
+
if (res.ok !== true) failTool("context index", res, opts);
|
|
116
|
+
const data = {
|
|
117
|
+
indexed: typeof res.indexed === "number" ? res.indexed : 0,
|
|
118
|
+
skipped: typeof res.skipped === "number" ? res.skipped : 0,
|
|
119
|
+
deleted: typeof res.deleted === "number" ? res.deleted : 0,
|
|
120
|
+
failed: typeof res.failed === "number" ? res.failed : 0,
|
|
121
|
+
totalChunks: typeof res.totalChunks === "number" ? res.totalChunks : 0,
|
|
122
|
+
degraded: res.degraded === true
|
|
123
|
+
};
|
|
124
|
+
if (opts.json === true) {
|
|
125
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
126
|
+
`);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
renderIndex(data, opts.paths, opts);
|
|
130
|
+
}
|
|
131
|
+
function renderIndex(data, paths, opts) {
|
|
132
|
+
const scope = paths && paths.length > 0 ? paths.join(", ") : ".";
|
|
133
|
+
const flag = data.degraded ? ` ${badge("warn", "degraded: vectors skipped")}` : "";
|
|
134
|
+
log(`context index \u2014 ${scope}${flag}`, opts);
|
|
135
|
+
definitionList(
|
|
136
|
+
[
|
|
137
|
+
{ label: "Indexed (new)", value: data.indexed },
|
|
138
|
+
{ label: "Skipped (unchanged)", value: data.skipped },
|
|
139
|
+
{ label: "Deleted (removed)", value: data.deleted },
|
|
140
|
+
{ label: "Failed", value: data.failed },
|
|
141
|
+
{ label: "Total chunks", value: data.totalChunks }
|
|
142
|
+
],
|
|
143
|
+
opts
|
|
144
|
+
);
|
|
145
|
+
if (data.failed > 0) {
|
|
146
|
+
info(`${data.failed} file(s) could not be indexed (binary / IO / encoding).`, opts);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function contextStatus(opts) {
|
|
150
|
+
const res = await callDaemonTool(opts, "context_status");
|
|
151
|
+
if (res.ok !== true) failTool("context status", res, opts);
|
|
152
|
+
const data = res;
|
|
153
|
+
if (opts.json === true) {
|
|
154
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
155
|
+
`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
renderStatus(data, opts);
|
|
159
|
+
}
|
|
160
|
+
function describeEmbedder(e) {
|
|
161
|
+
if (!e || e.kind === "none") return "none (BM25-only)";
|
|
162
|
+
const model = typeof e.model === "string" && e.model.length > 0 ? e.model : "<unset>";
|
|
163
|
+
return `${e.kind} \xB7 ${model} (${e.dim}-dim)`;
|
|
164
|
+
}
|
|
165
|
+
function renderStatus(data, opts) {
|
|
166
|
+
const flag = data.degraded ? ` ${badge("warn", "degraded")}` : "";
|
|
167
|
+
log(`context status \u2014 ${data.projectId}${flag}`, opts);
|
|
168
|
+
definitionList(
|
|
169
|
+
[
|
|
170
|
+
{ label: "Project", value: data.projectId },
|
|
171
|
+
{ label: "Docs", value: data.docCount },
|
|
172
|
+
{ label: "Vectors", value: data.vecCount },
|
|
173
|
+
{ label: "Indexed files", value: data.indexedFiles },
|
|
174
|
+
{ label: "Embedder", value: describeEmbedder(data.embedder) },
|
|
175
|
+
{ label: "Degraded", value: data.degraded ? "yes" : "no" }
|
|
176
|
+
],
|
|
177
|
+
opts
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/commands/daemon.ts
|
|
182
|
+
import { loadProjectInfo } from "@noir-ai/core";
|
|
183
|
+
import {
|
|
184
|
+
clearDaemonRecord,
|
|
185
|
+
ensureDaemonRunning,
|
|
186
|
+
pidAlive,
|
|
187
|
+
readDaemonRecord
|
|
188
|
+
} from "@noir-ai/daemon";
|
|
189
|
+
var MODE = "foreground";
|
|
190
|
+
function formatUptime(sec) {
|
|
191
|
+
if (!Number.isFinite(sec) || sec < 0) return "unknown";
|
|
192
|
+
if (sec < 60) return `${sec}s`;
|
|
193
|
+
if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
194
|
+
return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
|
|
195
|
+
}
|
|
196
|
+
async function daemonStart(opts) {
|
|
197
|
+
if (opts.detach === true) {
|
|
198
|
+
fail(EXIT.USAGE, "not implemented (tracked: v1.x)", opts);
|
|
199
|
+
}
|
|
200
|
+
const project = loadProjectInfo(process.cwd());
|
|
201
|
+
const ensured = await ensureDaemonRunning({
|
|
202
|
+
project,
|
|
203
|
+
idleTimeoutSec: project.config.daemon.idleTimeoutSec
|
|
204
|
+
});
|
|
205
|
+
if (ensured.started) {
|
|
206
|
+
if (opts.json === true) {
|
|
207
|
+
process.stdout.write(
|
|
208
|
+
`${JSON.stringify({
|
|
209
|
+
ok: true,
|
|
210
|
+
data: {
|
|
211
|
+
url: ensured.url,
|
|
212
|
+
port: ensured.port,
|
|
213
|
+
pid: process.pid,
|
|
214
|
+
mode: MODE,
|
|
215
|
+
reused: false
|
|
216
|
+
}
|
|
217
|
+
})}
|
|
218
|
+
`
|
|
219
|
+
);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
info("noir daemon: foreground mode (backgrounding deferred to v1.x). Ctrl+C to stop.", opts);
|
|
223
|
+
log(`Noir daemon listening at ${ensured.url}`, opts);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (opts.json === true) {
|
|
227
|
+
process.stdout.write(
|
|
228
|
+
`${JSON.stringify({ ok: true, data: { url: ensured.url, port: ensured.port, reused: true } })}
|
|
229
|
+
`
|
|
230
|
+
);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
log(`Noir daemon already running at ${ensured.url}`, opts);
|
|
234
|
+
}
|
|
235
|
+
async function daemonStop(opts) {
|
|
236
|
+
const rec = readDaemonRecord();
|
|
237
|
+
if (!rec) {
|
|
238
|
+
if (opts.json === true) {
|
|
239
|
+
process.stdout.write(
|
|
240
|
+
`${JSON.stringify({ ok: true, data: { running: false, stopped: false } })}
|
|
241
|
+
`
|
|
242
|
+
);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
info("No Noir daemon is running.", opts);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
let signalled = false;
|
|
249
|
+
let errMsg;
|
|
250
|
+
try {
|
|
251
|
+
process.kill(rec.pid, "SIGTERM");
|
|
252
|
+
signalled = true;
|
|
253
|
+
} catch (err) {
|
|
254
|
+
errMsg = err instanceof Error ? err.message : String(err);
|
|
255
|
+
} finally {
|
|
256
|
+
clearDaemonRecord();
|
|
257
|
+
}
|
|
258
|
+
if (opts.json === true) {
|
|
259
|
+
process.stdout.write(
|
|
260
|
+
`${JSON.stringify({
|
|
261
|
+
ok: true,
|
|
262
|
+
data: {
|
|
263
|
+
running: signalled,
|
|
264
|
+
stopped: signalled,
|
|
265
|
+
pid: rec.pid,
|
|
266
|
+
...errMsg !== void 0 ? { error: errMsg } : {}
|
|
267
|
+
}
|
|
268
|
+
})}
|
|
269
|
+
`
|
|
270
|
+
);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (signalled) {
|
|
274
|
+
log(`Stopped Noir daemon (pid ${rec.pid}).`, opts);
|
|
275
|
+
} else {
|
|
276
|
+
info(`Noir daemon (pid ${rec.pid}) could not be signalled: ${errMsg}`, opts);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function daemonStatus(opts) {
|
|
280
|
+
const rec = readDaemonRecord();
|
|
281
|
+
if (!rec) {
|
|
282
|
+
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (start with `noir daemon start`).", opts);
|
|
283
|
+
}
|
|
284
|
+
if (!pidAlive(rec.pid)) {
|
|
285
|
+
clearDaemonRecord();
|
|
286
|
+
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
|
|
287
|
+
}
|
|
288
|
+
let health = null;
|
|
289
|
+
try {
|
|
290
|
+
const res = await fetch(`http://127.0.0.1:${rec.port}/health`);
|
|
291
|
+
if (res.status === 200) {
|
|
292
|
+
health = await res.json();
|
|
293
|
+
}
|
|
294
|
+
} catch {
|
|
295
|
+
health = null;
|
|
296
|
+
}
|
|
297
|
+
if (health?.ok !== true) {
|
|
298
|
+
clearDaemonRecord();
|
|
299
|
+
fail(
|
|
300
|
+
EXIT.DAEMON_DOWN,
|
|
301
|
+
"Noir daemon is not running (port unresponsive; stale record removed).",
|
|
302
|
+
opts
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const uptimeSec = typeof health?.uptimeSec === "number" ? health.uptimeSec : Math.max(0, Math.floor((Date.now() - rec.startedAt) / 1e3));
|
|
306
|
+
const data = {
|
|
307
|
+
running: true,
|
|
308
|
+
pid: rec.pid,
|
|
309
|
+
port: rec.port,
|
|
310
|
+
startedAt: rec.startedAt,
|
|
311
|
+
uptimeSec,
|
|
312
|
+
mode: MODE
|
|
313
|
+
};
|
|
314
|
+
if (opts.json === true) {
|
|
315
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
316
|
+
`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
log(
|
|
320
|
+
`Noir daemon: running (pid ${data.pid}, port ${data.port}, up ${formatUptime(uptimeSec)}, ${data.mode})`,
|
|
321
|
+
opts
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
async function daemonRestart(opts) {
|
|
325
|
+
await daemonStop({ ...opts, json: false, quiet: true });
|
|
326
|
+
await daemonStart(opts);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/commands/handoff.ts
|
|
330
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
331
|
+
import { join } from "path";
|
|
332
|
+
import {
|
|
333
|
+
defaultHandoffBlock,
|
|
334
|
+
hostLaunchDirective,
|
|
335
|
+
resolveAdapter
|
|
336
|
+
} from "@noir-ai/adapters";
|
|
337
|
+
import { loadProjectInfo as loadProjectInfo2, NOIR_DIR } from "@noir-ai/core";
|
|
338
|
+
|
|
339
|
+
// src/commands/task.ts
|
|
340
|
+
var PHASE_SKILL = {
|
|
341
|
+
intake: "noir-intake",
|
|
342
|
+
clarify: "noir-clarify",
|
|
343
|
+
spec: "noir-spec",
|
|
344
|
+
plan: "noir-plan",
|
|
345
|
+
execute: "noir-execute",
|
|
346
|
+
verify: "noir-verify",
|
|
347
|
+
document: "noir-document"
|
|
348
|
+
};
|
|
349
|
+
function skillFor(phase) {
|
|
350
|
+
if (typeof phase !== "string") return null;
|
|
351
|
+
return PHASE_SKILL[phase] ?? null;
|
|
352
|
+
}
|
|
353
|
+
async function fetchStatus(opts, taskId) {
|
|
354
|
+
const args = typeof taskId === "string" && taskId.length > 0 ? { taskId } : {};
|
|
355
|
+
const res = await callDaemonTool(
|
|
356
|
+
opts,
|
|
357
|
+
"workflow_status",
|
|
358
|
+
args
|
|
359
|
+
);
|
|
360
|
+
if (res.ok === true) return res;
|
|
361
|
+
const detail = res.error ?? "task not found";
|
|
362
|
+
fail(EXIT.NOT_FOUND, `task: ${detail}`, opts);
|
|
363
|
+
}
|
|
364
|
+
function formatStamp(ms) {
|
|
365
|
+
if (typeof ms !== "number" || ms <= 0) return "-";
|
|
366
|
+
return new Date(ms).toISOString().replace("T", " ").replace(/\..*$/, "Z");
|
|
367
|
+
}
|
|
368
|
+
function renderStatusRow(s, opts) {
|
|
369
|
+
const flag = s.degraded === true ? ` ${badge("warn", "degraded")}` : "";
|
|
370
|
+
log(`task status \u2014 ${s.taskId}${flag}`, opts);
|
|
371
|
+
definitionList(
|
|
372
|
+
[
|
|
373
|
+
{ label: "Task", value: s.taskId },
|
|
374
|
+
{ label: "Phase", value: s.phase },
|
|
375
|
+
{ label: "State", value: s.state },
|
|
376
|
+
{ label: "Mode", value: s.mode },
|
|
377
|
+
{ label: "Next gate", value: s.nextGate ?? "\u2014" },
|
|
378
|
+
{ label: "Updated", value: formatStamp(s.updatedAt) }
|
|
379
|
+
],
|
|
380
|
+
opts
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
async function taskStatus(opts) {
|
|
384
|
+
const s = await fetchStatus(opts, opts.id);
|
|
385
|
+
if (opts.json === true) {
|
|
386
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: s })}
|
|
387
|
+
`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
renderStatusRow(s, opts);
|
|
391
|
+
}
|
|
392
|
+
async function taskNext(opts) {
|
|
393
|
+
const s = await fetchStatus(opts);
|
|
394
|
+
const suggestion = skillFor(s.phase);
|
|
395
|
+
const nextSkill = skillFor(s.nextGate);
|
|
396
|
+
const data = {
|
|
397
|
+
taskId: s.taskId,
|
|
398
|
+
phase: s.phase,
|
|
399
|
+
state: s.state,
|
|
400
|
+
mode: s.mode,
|
|
401
|
+
nextGate: s.nextGate,
|
|
402
|
+
suggestion,
|
|
403
|
+
nextSkill,
|
|
404
|
+
degraded: s.degraded === true
|
|
405
|
+
};
|
|
406
|
+
if (opts.json === true) {
|
|
407
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
408
|
+
`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const flag = data.degraded ? ` ${badge("warn", "degraded")}` : "";
|
|
412
|
+
log(`task next \u2014 ${s.taskId} \xB7 phase ${s.phase} (${s.state}, ${s.mode})${flag}`, opts);
|
|
413
|
+
if (s.nextGate) {
|
|
414
|
+
info(`next gate: ${s.nextGate}`, opts);
|
|
415
|
+
} else {
|
|
416
|
+
info("no further gate ahead (past verify, or stopped).", opts);
|
|
417
|
+
}
|
|
418
|
+
if (suggestion) info(`skill for current phase (${s.phase}): ${suggestion}`, opts);
|
|
419
|
+
if (nextSkill) info(`skill for next gate (${s.nextGate}): ${nextSkill}`, opts);
|
|
420
|
+
if (!suggestion && !nextSkill) {
|
|
421
|
+
info("run `noir skills list` to see applicable skills.", opts);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async function taskNew(opts) {
|
|
425
|
+
let mode;
|
|
426
|
+
if (opts.mode !== void 0) {
|
|
427
|
+
if (opts.mode !== "full" && opts.mode !== "quick") {
|
|
428
|
+
fail(EXIT.USAGE, `task new: invalid mode '${opts.mode}' (expected 'full' or 'quick')`, opts);
|
|
429
|
+
}
|
|
430
|
+
mode = opts.mode;
|
|
431
|
+
}
|
|
432
|
+
const res = await callDaemonTool(
|
|
433
|
+
opts,
|
|
434
|
+
"workflow_start",
|
|
435
|
+
{
|
|
436
|
+
taskId: opts.slug,
|
|
437
|
+
slug: opts.slug,
|
|
438
|
+
...mode === void 0 ? {} : { mode }
|
|
439
|
+
}
|
|
440
|
+
);
|
|
441
|
+
if (res.ok !== true) {
|
|
442
|
+
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "start failed";
|
|
443
|
+
fail(EXIT.ERROR, `task new --slug ${opts.slug}: ${detail}`, opts);
|
|
444
|
+
}
|
|
445
|
+
if (opts.json === true) {
|
|
446
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
|
|
447
|
+
`);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
renderStatusRow(res, opts);
|
|
451
|
+
}
|
|
452
|
+
async function taskAdvance(opts) {
|
|
453
|
+
const args = {};
|
|
454
|
+
if (typeof opts.to === "string" && opts.to.length > 0) {
|
|
455
|
+
if (!Object.keys(PHASE_SKILL).includes(opts.to)) {
|
|
456
|
+
fail(EXIT.USAGE, `task advance: invalid phase '${opts.to}'`, opts);
|
|
457
|
+
}
|
|
458
|
+
args.to = opts.to;
|
|
459
|
+
}
|
|
460
|
+
if (typeof opts.force === "string" && opts.force.length > 0) {
|
|
461
|
+
args.force = { reason: opts.force };
|
|
462
|
+
}
|
|
463
|
+
const res = await callDaemonTool(
|
|
464
|
+
opts,
|
|
465
|
+
"workflow_advance",
|
|
466
|
+
args
|
|
467
|
+
);
|
|
468
|
+
if (res.ok !== true) {
|
|
469
|
+
const detail = typeof res.error === "string" && res.error.length > 0 ? res.error : "advance failed";
|
|
470
|
+
fail(EXIT.ERROR, `task advance: ${detail}`, opts);
|
|
471
|
+
}
|
|
472
|
+
if (typeof opts.to === "string" && opts.to === "verify") {
|
|
473
|
+
tip("run `noir handoff` for a ready-to-paste host prompt", opts);
|
|
474
|
+
}
|
|
475
|
+
if (opts.json === true) {
|
|
476
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: res })}
|
|
477
|
+
`);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
renderStatusRow(res, opts);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/commands/handoff.ts
|
|
484
|
+
var SEED_LIMIT = 5;
|
|
485
|
+
function heading(name, id) {
|
|
486
|
+
return `# Noir handoff \u2014 ${name} (${id})`;
|
|
487
|
+
}
|
|
488
|
+
function renderMarkdown(p) {
|
|
489
|
+
const lines = [];
|
|
490
|
+
lines.push(heading(p.project.name, p.project.id));
|
|
491
|
+
lines.push("");
|
|
492
|
+
if (p.task) {
|
|
493
|
+
const gate = p.task.nextGate ?? "\u2014";
|
|
494
|
+
lines.push(`**Phase:** ${p.task.phase} \u2014 next gate: ${gate}`);
|
|
495
|
+
} else {
|
|
496
|
+
lines.push("**Phase:** no active task");
|
|
497
|
+
}
|
|
498
|
+
lines.push("");
|
|
499
|
+
lines.push("## Open host");
|
|
500
|
+
lines.push(p.directive);
|
|
501
|
+
lines.push("");
|
|
502
|
+
lines.push("## Next step");
|
|
503
|
+
if (p.task?.nextSkill) {
|
|
504
|
+
lines.push(`Run \`${p.task.nextSkill}\` \u2014 the host's next gate skill.`);
|
|
505
|
+
} else {
|
|
506
|
+
lines.push("No further gate ahead (past verify, or no active task).");
|
|
507
|
+
}
|
|
508
|
+
lines.push("");
|
|
509
|
+
lines.push("## Extracted context (seed)");
|
|
510
|
+
if (p.contextSeed && p.contextSeed.length > 0) {
|
|
511
|
+
for (const h of p.contextSeed) {
|
|
512
|
+
lines.push(`- \`${h.path}\` (score ${h.score.toFixed(4)})`);
|
|
513
|
+
const snip = h.snippet.trim();
|
|
514
|
+
if (snip.length > 0) lines.push(` > ${snip.replace(/\n/g, "\n > ")}`);
|
|
515
|
+
}
|
|
516
|
+
} else if (p.degraded.embedderMissing) {
|
|
517
|
+
lines.push(
|
|
518
|
+
"_Embedder unavailable \u2014 degraded to BM25-only or no hits. Start `noir daemon start` for live context._"
|
|
519
|
+
);
|
|
520
|
+
} else {
|
|
521
|
+
lines.push("_No context hits (or daemon down). Start `noir daemon start` for live context._");
|
|
522
|
+
}
|
|
523
|
+
lines.push("");
|
|
524
|
+
lines.push("## Extracted memory (seed)");
|
|
525
|
+
if (p.memorySeed && p.memorySeed.length > 0) {
|
|
526
|
+
for (const m of p.memorySeed) {
|
|
527
|
+
lines.push(`- \`${m.id}\` \u2014 ${m.summary}`);
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
lines.push(
|
|
531
|
+
"_No memory observations (or daemon down). Start `noir daemon start` for live memory._"
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
lines.push("");
|
|
535
|
+
lines.push("## Live data \u2014 call these from the host (already wired via the host MCP config)");
|
|
536
|
+
lines.push("- `noir.workflow_status` \xB7 `noir.context_search` \xB7 `noir.memory_recall`");
|
|
537
|
+
if (p.degraded.daemonDown) {
|
|
538
|
+
lines.push("");
|
|
539
|
+
lines.push("> **Note:** daemon not running \u2014 start `noir daemon start` for live task/memory.");
|
|
540
|
+
}
|
|
541
|
+
lines.push("");
|
|
542
|
+
return lines.join("\n");
|
|
543
|
+
}
|
|
544
|
+
function toContextHit(raw) {
|
|
545
|
+
const h = raw ?? {};
|
|
546
|
+
return {
|
|
547
|
+
path: typeof h.path === "string" ? h.path : "<unknown>",
|
|
548
|
+
score: typeof h.score === "number" ? h.score : 0,
|
|
549
|
+
snippet: typeof h.snippet === "string" ? h.snippet : ""
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
function observationSummary(raw) {
|
|
553
|
+
const obs = raw.observation;
|
|
554
|
+
if (obs && typeof obs === "object") {
|
|
555
|
+
const content = obs.content;
|
|
556
|
+
if (typeof content === "string" && content.length > 0) {
|
|
557
|
+
return content.length > 160 ? `${content.slice(0, 157)}\u2026` : content;
|
|
558
|
+
}
|
|
559
|
+
const type = obs.type;
|
|
560
|
+
if (typeof type === "string") return `(${type})`;
|
|
561
|
+
}
|
|
562
|
+
return "";
|
|
563
|
+
}
|
|
564
|
+
function toMemoryHit(raw) {
|
|
565
|
+
const h = raw ?? {};
|
|
566
|
+
return {
|
|
567
|
+
id: typeof h.id === "string" ? h.id : "<unknown>",
|
|
568
|
+
summary: observationSummary(h)
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
function toHostPayload(p) {
|
|
572
|
+
return {
|
|
573
|
+
project: p.project,
|
|
574
|
+
host: p.host,
|
|
575
|
+
task: p.task
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function resolveDirective(root, p) {
|
|
579
|
+
const ctx = { root };
|
|
580
|
+
const adapter = resolveAdapter(p.host);
|
|
581
|
+
const hostPayload = toHostPayload(p);
|
|
582
|
+
if (typeof adapter.emitHandoff === "function") {
|
|
583
|
+
return adapter.emitHandoff(ctx, hostPayload);
|
|
584
|
+
}
|
|
585
|
+
return defaultHandoffBlock(ctx, hostPayload);
|
|
586
|
+
}
|
|
587
|
+
function handoffFilePath(root, taskId, projectId) {
|
|
588
|
+
const id = typeof taskId === "string" && taskId.length > 0 ? taskId : projectId;
|
|
589
|
+
return join(root, NOIR_DIR, "handoff", `${id}.md`);
|
|
590
|
+
}
|
|
591
|
+
async function extractSeeds(opts, probe, query) {
|
|
592
|
+
if (!probe.running) return { context: null, memory: null };
|
|
593
|
+
let context = null;
|
|
594
|
+
let memory = null;
|
|
595
|
+
await withRunningDaemon(
|
|
596
|
+
opts,
|
|
597
|
+
async (caller) => {
|
|
598
|
+
const ctxRes = await tryTool(caller, "context_search", {
|
|
599
|
+
query,
|
|
600
|
+
limit: SEED_LIMIT
|
|
601
|
+
});
|
|
602
|
+
if (ctxRes && ctxRes.ok !== false && Array.isArray(ctxRes.results)) {
|
|
603
|
+
context = ctxRes.results.slice(0, SEED_LIMIT).map(toContextHit).filter((h) => h.path !== "<unknown>" || h.snippet.length > 0);
|
|
604
|
+
}
|
|
605
|
+
const memRes = await tryTool(caller, "memory_recall", {
|
|
606
|
+
query,
|
|
607
|
+
limit: SEED_LIMIT
|
|
608
|
+
});
|
|
609
|
+
if (memRes && memRes.ok !== false && Array.isArray(memRes.results)) {
|
|
610
|
+
memory = memRes.results.slice(0, SEED_LIMIT).map(toMemoryHit).filter((h) => h.summary.length > 0);
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
probe
|
|
614
|
+
);
|
|
615
|
+
return { context, memory };
|
|
616
|
+
}
|
|
617
|
+
async function probeOnly(opts) {
|
|
618
|
+
return probeDaemon(opts);
|
|
619
|
+
}
|
|
620
|
+
async function handoff(opts) {
|
|
621
|
+
const project = loadProjectInfo2(process.cwd());
|
|
622
|
+
const host = project.config.host;
|
|
623
|
+
const snapshot = await gatherStatusPayload(opts);
|
|
624
|
+
const probe = await probeOnly(opts);
|
|
625
|
+
const task = snapshot.workflow ? {
|
|
626
|
+
taskId: snapshot.workflow.taskId,
|
|
627
|
+
phase: snapshot.workflow.phase,
|
|
628
|
+
nextGate: snapshot.workflow.nextGate,
|
|
629
|
+
nextSkill: skillFor(snapshot.workflow.nextGate) ?? skillFor(snapshot.workflow.phase)
|
|
630
|
+
} : null;
|
|
631
|
+
const query = task?.taskId ?? project.name;
|
|
632
|
+
const { context, memory } = await extractSeeds(opts, probe, query);
|
|
633
|
+
const full = {
|
|
634
|
+
project: { id: project.id, name: project.name },
|
|
635
|
+
host,
|
|
636
|
+
directive: "",
|
|
637
|
+
// filled after the host payload is assembled (below)
|
|
638
|
+
task,
|
|
639
|
+
contextSeed: context,
|
|
640
|
+
memorySeed: memory,
|
|
641
|
+
degraded: {
|
|
642
|
+
daemonDown: !probe.running,
|
|
643
|
+
embedderMissing: context === null && probe.running
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
full.directive = resolveDirective(project.root, full);
|
|
647
|
+
if (opts.json === true) {
|
|
648
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: full })}
|
|
649
|
+
`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const md = renderMarkdown(full);
|
|
653
|
+
if (opts.write === true) {
|
|
654
|
+
const path = handoffFilePath(project.root, task?.taskId, project.id);
|
|
655
|
+
mkdirSync(join(project.root, NOIR_DIR, "handoff"), { recursive: true });
|
|
656
|
+
writeFileSync(path, md, "utf8");
|
|
657
|
+
info(`handoff written to ${path}`, opts);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
process.stdout.write(md);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// src/commands/home.ts
|
|
664
|
+
import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
|
|
665
|
+
import { loadProjectInfo as loadProjectInfo3 } from "@noir-ai/core";
|
|
666
|
+
|
|
667
|
+
// src/banner.ts
|
|
668
|
+
import gradient from "gradient-string";
|
|
669
|
+
var NOIR_BLOCK = [
|
|
670
|
+
"\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
671
|
+
"\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
|
|
672
|
+
"\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
|
|
673
|
+
"\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551",
|
|
674
|
+
"\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551",
|
|
675
|
+
"\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D"
|
|
676
|
+
];
|
|
677
|
+
var NOIR_GRADIENT = gradient("#2c5282", "#3b82f6", "#7dd3fc");
|
|
678
|
+
var NOIR_TAGLINE = "discipline, context, and memory layer for agentic CLIs";
|
|
679
|
+
function renderBanner(opts = {}) {
|
|
680
|
+
const width = opts.width ?? process.stdout.columns ?? 80;
|
|
681
|
+
const colorOn = opts.color !== false && useColor();
|
|
682
|
+
if (width < 50) {
|
|
683
|
+
return colorOn ? `${c.accent("\u25C6")} noir` : "\u25C6 noir";
|
|
684
|
+
}
|
|
685
|
+
const block = NOIR_BLOCK.join("\n");
|
|
686
|
+
if (!colorOn) return block;
|
|
687
|
+
if (accessibleMode()) {
|
|
688
|
+
return block.split("\n").map((line) => c.accent(line)).join("\n");
|
|
689
|
+
}
|
|
690
|
+
return NOIR_GRADIENT.multiline(block);
|
|
691
|
+
}
|
|
692
|
+
function shouldShowBanner(opts) {
|
|
693
|
+
if (opts.quiet === true || opts.json === true) return false;
|
|
694
|
+
const v = process.env.NOIR_NO_BANNER;
|
|
695
|
+
if (v !== void 0 && v !== "") return false;
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// src/commands/home.ts
|
|
700
|
+
function tryProject() {
|
|
701
|
+
try {
|
|
702
|
+
const info2 = loadProjectInfo3(process.cwd());
|
|
703
|
+
return { id: info2.id, name: info2.name, host: info2.config.host };
|
|
704
|
+
} catch {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
var COMMANDS_HINT = "Commands: init \xB7 create \xB7 sync \xB7 status \xB7 context \xB7 memory \xB7 skills \xB7 task \xB7 daemon \xB7 doctor";
|
|
709
|
+
async function home(opts, deps) {
|
|
710
|
+
if (isInteractive(opts) && opts.tui !== false) {
|
|
711
|
+
await runMenu(opts, deps);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (opts.json === true) {
|
|
715
|
+
await deps.dispatch(["status", "--json"]);
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
await deps.dispatch(["status"]);
|
|
719
|
+
}
|
|
720
|
+
async function runMenu(opts, deps) {
|
|
721
|
+
const clack = await import("@clack/prompts");
|
|
722
|
+
const project = tryProject();
|
|
723
|
+
if (shouldShowBanner(opts)) {
|
|
724
|
+
const host = project?.host ?? "claude";
|
|
725
|
+
process.stderr.write(
|
|
726
|
+
`
|
|
727
|
+
${renderBanner()}
|
|
728
|
+
${NOIR_TAGLINE}
|
|
729
|
+
|
|
730
|
+
${hostLaunchDirective2(host)}
|
|
731
|
+
${COMMANDS_HINT}
|
|
732
|
+
|
|
733
|
+
`
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
clack.intro(project ? `noir \u2014 ${project.name}` : "noir");
|
|
737
|
+
const choice = await clack.select({
|
|
738
|
+
message: "What would you like to do?",
|
|
739
|
+
initialValue: "status",
|
|
740
|
+
options: [
|
|
741
|
+
{ value: "status", label: "Status", hint: "project + daemon + store snapshot" },
|
|
742
|
+
{ value: "index", label: "Index project", hint: "(re)index files into context" },
|
|
743
|
+
{ value: "recall", label: "Recall memory", hint: "search cross-session memory" },
|
|
744
|
+
{ value: "next", label: "Next task", hint: "suggest next phase + skill" },
|
|
745
|
+
{ value: "handoff", label: "Handoff", hint: "ready-to-paste host prompt" },
|
|
746
|
+
{ value: "daemon", label: "Start daemon", hint: "foreground daemon" },
|
|
747
|
+
{ value: "sync", label: "Sync skills", hint: "re-emit builtin skills" },
|
|
748
|
+
{ value: "exit", label: "Exit" }
|
|
749
|
+
]
|
|
750
|
+
});
|
|
751
|
+
if (clack.isCancel(choice)) {
|
|
752
|
+
clack.cancel("Cancelled.");
|
|
753
|
+
fail(EXIT.CANCELLED, "cancelled", opts);
|
|
754
|
+
}
|
|
755
|
+
const argv = await argvForChoice(choice, clack, opts);
|
|
756
|
+
if (argv === null) {
|
|
757
|
+
clack.outro("bye");
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
await deps.dispatch(argv);
|
|
761
|
+
clack.outro("done");
|
|
762
|
+
}
|
|
763
|
+
async function argvForChoice(choice, clack, opts) {
|
|
764
|
+
switch (choice) {
|
|
765
|
+
case "status":
|
|
766
|
+
return ["status"];
|
|
767
|
+
case "index":
|
|
768
|
+
return ["context", "index"];
|
|
769
|
+
case "next":
|
|
770
|
+
return ["task", "next"];
|
|
771
|
+
case "handoff":
|
|
772
|
+
return ["handoff"];
|
|
773
|
+
case "daemon":
|
|
774
|
+
return ["daemon", "start"];
|
|
775
|
+
case "sync":
|
|
776
|
+
return ["sync"];
|
|
777
|
+
case "exit":
|
|
778
|
+
return null;
|
|
779
|
+
case "recall": {
|
|
780
|
+
const query = await clack.text({
|
|
781
|
+
message: "Recall query:",
|
|
782
|
+
placeholder: "e.g. auth flow, ContextEngine, deploy steps"
|
|
783
|
+
});
|
|
784
|
+
if (clack.isCancel(query)) {
|
|
785
|
+
clack.cancel("Cancelled.");
|
|
786
|
+
fail(EXIT.CANCELLED, "cancelled", opts);
|
|
787
|
+
}
|
|
788
|
+
return ["memory", "recall", String(query)];
|
|
789
|
+
}
|
|
790
|
+
default:
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// src/commands/memory.ts
|
|
796
|
+
function parseLimit2(raw, label, opts) {
|
|
797
|
+
if (raw === void 0) return void 0;
|
|
798
|
+
const n = Number(raw);
|
|
799
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
800
|
+
fail(EXIT.USAGE, `${label}: --limit must be a positive integer (got '${raw}')`, opts);
|
|
801
|
+
}
|
|
802
|
+
return n;
|
|
803
|
+
}
|
|
804
|
+
function splitCsv(raw) {
|
|
805
|
+
if (raw === void 0) return void 0;
|
|
806
|
+
const out = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
807
|
+
return out.length > 0 ? out : void 0;
|
|
808
|
+
}
|
|
809
|
+
function toHit2(raw) {
|
|
810
|
+
const h = raw ?? {};
|
|
811
|
+
const arr = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
812
|
+
return {
|
|
813
|
+
id: typeof h.id === "string" ? h.id : "",
|
|
814
|
+
type: typeof h.type === "string" ? h.type : "",
|
|
815
|
+
score: typeof h.score === "number" ? h.score : 0,
|
|
816
|
+
content: typeof h.content === "string" ? h.content : "",
|
|
817
|
+
concepts: arr(h.concepts),
|
|
818
|
+
files: arr(h.files),
|
|
819
|
+
ts: typeof h.ts === "number" ? h.ts : 0,
|
|
820
|
+
importance: typeof h.importance === "number" ? h.importance : 0,
|
|
821
|
+
source: typeof h.source === "string" ? h.source : ""
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function failTool2(label, envelope, opts) {
|
|
825
|
+
const detail = envelope.error ?? envelope.reason ?? "unknown failure";
|
|
826
|
+
fail(EXIT.ERROR, `${label}: ${detail}`, opts);
|
|
827
|
+
}
|
|
828
|
+
function stamp(ts) {
|
|
829
|
+
if (typeof ts !== "number" || ts <= 0) return "-";
|
|
830
|
+
return new Date(ts).toISOString().replace("T", " ").replace(/\..*$/, "Z");
|
|
831
|
+
}
|
|
832
|
+
async function memoryRecall(opts) {
|
|
833
|
+
const limit = parseLimit2(opts.limit, "memory recall", opts);
|
|
834
|
+
const res = await callDaemonTool(opts, "memory_recall", {
|
|
835
|
+
query: opts.query,
|
|
836
|
+
...limit === void 0 ? {} : { limit }
|
|
837
|
+
});
|
|
838
|
+
if (res.ok !== true) failTool2("memory recall", res, opts);
|
|
839
|
+
const hits = Array.isArray(res.results) ? res.results.map(toHit2) : [];
|
|
840
|
+
const data = { query: opts.query, hits, degraded: res.degraded === true };
|
|
841
|
+
if (opts.json === true) {
|
|
842
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
843
|
+
`);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
renderRecall(data.query, hits, data.degraded, opts);
|
|
847
|
+
}
|
|
848
|
+
function renderRecall(query, hits, degraded, opts) {
|
|
849
|
+
const flag = degraded ? ` ${badge("warn", "degraded: BM25-only")}` : "";
|
|
850
|
+
log(
|
|
851
|
+
`memory recall \u2014 ${hits.length} hit${hits.length === 1 ? "" : "s"} for '${query}'${flag}`,
|
|
852
|
+
opts
|
|
853
|
+
);
|
|
854
|
+
if (hits.length === 0) {
|
|
855
|
+
info("(no memories matched)", opts);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
for (let i = 0; i < hits.length; i++) {
|
|
859
|
+
const h = hits[i];
|
|
860
|
+
if (h === void 0) continue;
|
|
861
|
+
const head = `[${i + 1}] ${h.type} \xB7 score ${h.score.toFixed(4)} \xB7 importance ${h.importance.toFixed(2)} \xB7 ${h.id}`;
|
|
862
|
+
log(head, opts);
|
|
863
|
+
if (h.files.length > 0) log(` files: ${h.files.join(", ")}`, opts);
|
|
864
|
+
if (h.concepts.length > 0) log(` tags: ${h.concepts.join(", ")}`, opts);
|
|
865
|
+
log(` ${h.content}`, opts);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
async function memorySave(opts) {
|
|
869
|
+
const content = await resolveContent(opts);
|
|
870
|
+
const files = splitCsv(opts.files);
|
|
871
|
+
const args = { content };
|
|
872
|
+
if (typeof opts.type === "string" && opts.type.length > 0) args.type = opts.type;
|
|
873
|
+
if (files !== void 0) args.files = files;
|
|
874
|
+
const res = await callDaemonTool(opts, "memory_save", args);
|
|
875
|
+
if (res.ok !== true) failTool2("memory save", res, opts);
|
|
876
|
+
const id = typeof res.id === "string" ? res.id : "";
|
|
877
|
+
const observation = res.observation ?? {};
|
|
878
|
+
const data = { id, observation };
|
|
879
|
+
if (opts.json === true) {
|
|
880
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
881
|
+
`);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
log(`Saved memory ${id}.`, opts);
|
|
885
|
+
renderObservation(observation, opts);
|
|
886
|
+
}
|
|
887
|
+
async function resolveContent(opts) {
|
|
888
|
+
if (typeof opts.content === "string" && opts.content.length > 0) return opts.content;
|
|
889
|
+
if (!isInteractive(opts)) {
|
|
890
|
+
fail(
|
|
891
|
+
EXIT.USAGE,
|
|
892
|
+
"memory save requires --content <text> (or re-run in an interactive terminal to be prompted)",
|
|
893
|
+
opts
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
const clack = await import("@clack/prompts");
|
|
897
|
+
const value = await clack.text({
|
|
898
|
+
message: "Memory to save:",
|
|
899
|
+
placeholder: "the insight / decision / pattern to remember",
|
|
900
|
+
validate: (v) => v.trim().length === 0 ? "content cannot be empty" : void 0
|
|
901
|
+
});
|
|
902
|
+
if (clack.isCancel(value)) {
|
|
903
|
+
clack.cancel("Cancelled.");
|
|
904
|
+
fail(EXIT.CANCELLED, "cancelled", opts);
|
|
905
|
+
}
|
|
906
|
+
return String(value);
|
|
907
|
+
}
|
|
908
|
+
function renderObservation(obs, opts) {
|
|
909
|
+
const rows = [];
|
|
910
|
+
for (const key of ["id", "type", "importance", "ts", "source", "project", "sessionId"]) {
|
|
911
|
+
if (obs[key] !== void 0) rows.push({ label: key, value: obs[key] });
|
|
912
|
+
}
|
|
913
|
+
if (rows.length > 0) definitionList(rows, opts);
|
|
914
|
+
const content = obs.content;
|
|
915
|
+
if (typeof content === "string") log(`
|
|
916
|
+
${content}`, opts);
|
|
917
|
+
}
|
|
918
|
+
async function memorySessions(opts) {
|
|
919
|
+
const res = await callDaemonTool(opts, "memory_sessions");
|
|
920
|
+
if (res.ok !== true) failTool2("memory sessions", res, opts);
|
|
921
|
+
const sessions = Array.isArray(res.sessions) ? res.sessions.map((raw) => {
|
|
922
|
+
const s = raw ?? {};
|
|
923
|
+
return {
|
|
924
|
+
id: typeof s.id === "string" ? s.id : "",
|
|
925
|
+
count: typeof s.count === "number" ? s.count : 0,
|
|
926
|
+
lastTs: typeof s.lastTs === "number" ? s.lastTs : 0
|
|
927
|
+
};
|
|
928
|
+
}) : [];
|
|
929
|
+
const data = { sessions };
|
|
930
|
+
if (opts.json === true) {
|
|
931
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
932
|
+
`);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
log(`memory sessions \u2014 ${sessions.length} session${sessions.length === 1 ? "" : "s"}`, opts);
|
|
936
|
+
table(
|
|
937
|
+
sessions.map((s) => ({ Session: s.id, Observations: s.count, "Last seen": stamp(s.lastTs) })),
|
|
938
|
+
["Session", "Observations", "Last seen"],
|
|
939
|
+
opts
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
async function memoryForget(opts) {
|
|
943
|
+
if (opts.ids.length === 0) {
|
|
944
|
+
fail(EXIT.USAGE, "memory forget requires at least one <id>", opts);
|
|
945
|
+
}
|
|
946
|
+
const res = await callDaemonTool(opts, "memory_forget", {
|
|
947
|
+
ids: opts.ids
|
|
948
|
+
});
|
|
949
|
+
if (res.ok !== true) failTool2("memory forget", res, opts);
|
|
950
|
+
const deleted = typeof res.deleted === "number" ? res.deleted : 0;
|
|
951
|
+
const data = { deleted, ids: opts.ids };
|
|
952
|
+
if (opts.json === true) {
|
|
953
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
954
|
+
`);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
log(`Forgot ${deleted} observation${deleted === 1 ? "" : "s"}.`, opts);
|
|
958
|
+
}
|
|
959
|
+
async function memoryConsolidate(opts) {
|
|
960
|
+
const exposed = await withDaemon(opts, async (caller) => caller.listTools());
|
|
961
|
+
if (!exposed.includes("memory_consolidate")) {
|
|
962
|
+
fail(
|
|
963
|
+
EXIT.ERROR,
|
|
964
|
+
"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.",
|
|
965
|
+
opts
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
const types = splitCsv(opts.types);
|
|
969
|
+
const limit = parseLimit2(opts.limit, "memory consolidate", opts);
|
|
970
|
+
const args = {};
|
|
971
|
+
if (types !== void 0) args.types = types;
|
|
972
|
+
if (limit !== void 0) args.limit = limit;
|
|
973
|
+
const res = await callDaemonTool(
|
|
974
|
+
opts,
|
|
975
|
+
"memory_consolidate",
|
|
976
|
+
args
|
|
977
|
+
);
|
|
978
|
+
if (res.ok === true) {
|
|
979
|
+
const lessons = Array.isArray(res.lessons) ? res.lessons : [];
|
|
980
|
+
const from = Array.isArray(res.from) ? res.from : [];
|
|
981
|
+
const data = { lessons, from };
|
|
982
|
+
if (opts.json === true) {
|
|
983
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
984
|
+
`);
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
log(
|
|
988
|
+
`Consolidated ${lessons.length} lesson${lessons.length === 1 ? "" : "s"} from ${from.length} observation${from.length === 1 ? "" : "s"}.`,
|
|
989
|
+
opts
|
|
990
|
+
);
|
|
991
|
+
for (let i = 0; i < lessons.length; i++) {
|
|
992
|
+
const l = lessons[i];
|
|
993
|
+
if (l === void 0) continue;
|
|
994
|
+
const obs = l ?? {};
|
|
995
|
+
const id = typeof obs.id === "string" ? obs.id : `<lesson ${i + 1}>`;
|
|
996
|
+
const content = typeof obs.content === "string" ? obs.content : "";
|
|
997
|
+
log(`- ${id}: ${content}`, opts);
|
|
998
|
+
}
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const env = res;
|
|
1002
|
+
const reason = (typeof env.reason === "string" ? env.reason : void 0) ?? (typeof env.error === "string" ? env.error : void 0) ?? "unknown";
|
|
1003
|
+
const logged = env.logged === true || env.degraded === true;
|
|
1004
|
+
const suffix = logged ? " (logged)" : "";
|
|
1005
|
+
fail(EXIT.ERROR, `memory consolidate did not run: ${reason}${suffix}`, opts);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/commands/skills.ts
|
|
1009
|
+
import { claudeAdapter } from "@noir-ai/adapters";
|
|
1010
|
+
import { loadProjectInfo as loadProjectInfo4 } from "@noir-ai/core";
|
|
1011
|
+
import {
|
|
1012
|
+
discoverAll,
|
|
1013
|
+
emitSkillsToDir
|
|
1014
|
+
} from "@noir-ai/skills";
|
|
1015
|
+
var CATEGORY = {
|
|
1016
|
+
"noir-brainstorm": "discovery",
|
|
1017
|
+
"noir-intake": "discovery",
|
|
1018
|
+
"noir-clarify": "discovery",
|
|
1019
|
+
"noir-explore": "discovery",
|
|
1020
|
+
"noir-spec": "spec",
|
|
1021
|
+
"noir-plan": "plan",
|
|
1022
|
+
"noir-execute": "execute",
|
|
1023
|
+
"noir-tdd": "execute",
|
|
1024
|
+
"noir-debug": "execute",
|
|
1025
|
+
"noir-subagent": "execute",
|
|
1026
|
+
"noir-parallel": "execute",
|
|
1027
|
+
"noir-verify": "verify",
|
|
1028
|
+
"noir-review": "verify",
|
|
1029
|
+
"noir-security": "verify",
|
|
1030
|
+
"noir-test": "verify",
|
|
1031
|
+
"noir-document": "document",
|
|
1032
|
+
"noir-readme": "document",
|
|
1033
|
+
"noir-commit": "git",
|
|
1034
|
+
"noir-branch": "git",
|
|
1035
|
+
"noir-pr": "git",
|
|
1036
|
+
"noir-worktree": "git",
|
|
1037
|
+
"noir-recall": "memory",
|
|
1038
|
+
"noir-remember": "memory",
|
|
1039
|
+
"noir-checkpoint": "memory",
|
|
1040
|
+
"noir-context": "context",
|
|
1041
|
+
"noir-frontend": "domain",
|
|
1042
|
+
"noir-backend": "domain",
|
|
1043
|
+
"noir-doctor": "meta",
|
|
1044
|
+
"noir-skill-author": "meta",
|
|
1045
|
+
"noir-sync": "meta",
|
|
1046
|
+
"noir-wrap": "meta"
|
|
1047
|
+
};
|
|
1048
|
+
function categoryOf(name) {
|
|
1049
|
+
const mapped = CATEGORY[name];
|
|
1050
|
+
if (mapped !== void 0) return mapped;
|
|
1051
|
+
return name.replace(/^noir-/, "") || "general";
|
|
1052
|
+
}
|
|
1053
|
+
function toRow(s, kind = "builtin") {
|
|
1054
|
+
const description = typeof s.frontmatter.description === "string" ? s.frontmatter.description : "";
|
|
1055
|
+
return { name: s.name, category: categoryOf(s.name), description, kind };
|
|
1056
|
+
}
|
|
1057
|
+
function truncate(text, max) {
|
|
1058
|
+
if (text.length <= max) return text;
|
|
1059
|
+
const slice = text.slice(0, Math.max(0, max - 1));
|
|
1060
|
+
const boundary = slice.lastIndexOf(" ");
|
|
1061
|
+
const head = boundary > 0 ? slice.slice(0, boundary) : slice;
|
|
1062
|
+
return `${head.trimEnd()}\u2026`;
|
|
1063
|
+
}
|
|
1064
|
+
async function skillsList(opts) {
|
|
1065
|
+
let builtins;
|
|
1066
|
+
let integrations;
|
|
1067
|
+
try {
|
|
1068
|
+
const all = discoverAll();
|
|
1069
|
+
builtins = all.builtins;
|
|
1070
|
+
integrations = all.integrations;
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
fail(
|
|
1073
|
+
EXIT.ERROR,
|
|
1074
|
+
`skills list: could not discover skills (${err instanceof Error ? err.message : String(err)})`,
|
|
1075
|
+
opts
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
const rows = [
|
|
1079
|
+
...builtins.map((b) => toRow(b, "builtin")),
|
|
1080
|
+
...integrations.map((i) => toRow(i, "integration"))
|
|
1081
|
+
];
|
|
1082
|
+
const data = { count: rows.length, skills: rows };
|
|
1083
|
+
if (opts.json === true) {
|
|
1084
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
1085
|
+
`);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
const intCount = integrations.length;
|
|
1089
|
+
const banner = intCount > 0 ? `noir skills \u2014 ${rows.length} skill${rows.length === 1 ? "" : "s"} (${builtins.length} builtin, ${intCount} integration${intCount === 1 ? "" : "s"})` : `noir skills \u2014 ${rows.length} builtin skill${rows.length === 1 ? "" : "s"}`;
|
|
1090
|
+
log(banner, opts);
|
|
1091
|
+
table(
|
|
1092
|
+
rows.map((r) => ({
|
|
1093
|
+
Skill: r.name,
|
|
1094
|
+
Kind: r.kind,
|
|
1095
|
+
Category: r.category,
|
|
1096
|
+
Description: truncate(r.description, 80)
|
|
1097
|
+
})),
|
|
1098
|
+
["Skill", "Kind", "Category", "Description"],
|
|
1099
|
+
opts
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
async function skillsSync(opts) {
|
|
1103
|
+
const root = process.cwd();
|
|
1104
|
+
const project = loadProjectInfo4(root);
|
|
1105
|
+
if (!claudeAdapter.skillsDir) {
|
|
1106
|
+
const data2 = {
|
|
1107
|
+
emitted: [],
|
|
1108
|
+
references: 0,
|
|
1109
|
+
host: project.config.host,
|
|
1110
|
+
reason: "no-skill-emitter"
|
|
1111
|
+
};
|
|
1112
|
+
if (opts.json === true) {
|
|
1113
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
|
|
1114
|
+
`);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
info("This host has no skill emitter; nothing to sync.", opts);
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
const dir = claudeAdapter.skillsDir({ root });
|
|
1121
|
+
const summary = await emitSkillsToDir(dir);
|
|
1122
|
+
const pruned = summary.pruned ?? [];
|
|
1123
|
+
const data = {
|
|
1124
|
+
emitted: summary.emitted,
|
|
1125
|
+
references: summary.references,
|
|
1126
|
+
dir,
|
|
1127
|
+
pruned
|
|
1128
|
+
};
|
|
1129
|
+
if (opts.json === true) {
|
|
1130
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
1131
|
+
`);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
log(
|
|
1135
|
+
`Synced ${summary.emitted.length} Noir skill${summary.emitted.length === 1 ? "" : "s"} to ${dir}.`,
|
|
1136
|
+
opts
|
|
1137
|
+
);
|
|
1138
|
+
if (pruned.length > 0) {
|
|
1139
|
+
warn(
|
|
1140
|
+
`Pruned ${pruned.length} stale noir-* skill dir${pruned.length === 1 ? "" : "s"}: ${pruned.join(", ")}`,
|
|
1141
|
+
opts
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// src/serve.ts
|
|
1147
|
+
import { loadProjectInfo as loadProjectInfo5 } from "@noir-ai/core";
|
|
1148
|
+
import { ensureDaemonRunning as ensureDaemonRunning2, startStdioServer } from "@noir-ai/daemon";
|
|
1149
|
+
async function serve(opts) {
|
|
1150
|
+
const project = loadProjectInfo5(process.cwd());
|
|
1151
|
+
if (opts.stdio) {
|
|
1152
|
+
await startStdioServer({ project, transport: "stdio", daemon: false });
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
try {
|
|
1156
|
+
const { url } = await ensureDaemonRunning2({
|
|
1157
|
+
project,
|
|
1158
|
+
idleTimeoutSec: project.config.daemon.idleTimeoutSec
|
|
1159
|
+
});
|
|
1160
|
+
process.stderr.write(
|
|
1161
|
+
`Noir daemon available at ${url}. (For HTTP clients, use this URL in .mcp.json.)
|
|
1162
|
+
`
|
|
1163
|
+
);
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
process.stderr.write(
|
|
1166
|
+
`Noir daemon unavailable (${err instanceof Error ? err.message : String(err)}); falling back to stdio.
|
|
1167
|
+
`
|
|
1168
|
+
);
|
|
1169
|
+
await startStdioServer({ project, transport: "stdio", daemon: false });
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// src/bin.ts
|
|
1174
|
+
function actionGlobals(args) {
|
|
1175
|
+
const last = args[args.length - 1];
|
|
1176
|
+
return last?.optsWithGlobals() ?? {};
|
|
1177
|
+
}
|
|
1178
|
+
function trailingCmd(args) {
|
|
1179
|
+
const last = args[args.length - 1];
|
|
1180
|
+
if (!(last instanceof Command)) {
|
|
1181
|
+
throw new NoirCliError(
|
|
1182
|
+
EXIT.ERROR,
|
|
1183
|
+
"internal: commander action callback received no trailing Command"
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
return last;
|
|
1187
|
+
}
|
|
1188
|
+
function toCliOptions(g) {
|
|
1189
|
+
return {
|
|
1190
|
+
json: g.json === true,
|
|
1191
|
+
quiet: g.quiet === true,
|
|
1192
|
+
verbose: g.verbose === true,
|
|
1193
|
+
input: g.input !== false,
|
|
1194
|
+
// TUI policy — additive only. `--tui`/`--no-tui` land on `tui`
|
|
1195
|
+
// (true/false; absent when neither flag is given). Spread conditionally so
|
|
1196
|
+
// the default-args case (no flag) does NOT add a `tui` key — keeps the
|
|
1197
|
+
// exact-shape assertions in bin.test.ts green.
|
|
1198
|
+
...g.tui === true || g.tui === false ? { tui: g.tui === true } : {},
|
|
1199
|
+
// `--no-tips` is stored under `tips` (default true; flag ⇒ false). Map to
|
|
1200
|
+
// the additive `noTips` spelling used by CliOptions + `tip()`.
|
|
1201
|
+
...g.tips === false ? { noTips: true } : {}
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
function toStatusOptions(g) {
|
|
1205
|
+
return {
|
|
1206
|
+
json: g.json === true,
|
|
1207
|
+
quiet: g.quiet === true,
|
|
1208
|
+
verbose: g.verbose === true,
|
|
1209
|
+
input: g.input !== false
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
function parseHost(raw) {
|
|
1213
|
+
if (raw === void 0) return void 0;
|
|
1214
|
+
return raw;
|
|
1215
|
+
}
|
|
1216
|
+
var DEPRECATIONS = [];
|
|
1217
|
+
function commandPath(cmd) {
|
|
1218
|
+
const path = [];
|
|
1219
|
+
let c2 = cmd;
|
|
1220
|
+
while (c2 !== null) {
|
|
1221
|
+
path.unshift(c2.name());
|
|
1222
|
+
c2 = c2.parent;
|
|
1223
|
+
}
|
|
1224
|
+
return path;
|
|
1225
|
+
}
|
|
1226
|
+
function emitDeprecationHintsFor(cmd, opts) {
|
|
1227
|
+
if (DEPRECATIONS.length === 0) return;
|
|
1228
|
+
const rel = commandPath(cmd).slice(1);
|
|
1229
|
+
for (const d of DEPRECATIONS) {
|
|
1230
|
+
if (d.oldArgv.every((a, i) => rel[i] === a)) {
|
|
1231
|
+
tip(
|
|
1232
|
+
`\`noir ${d.oldArgv.join(" ")}\` is deprecated since v${d.since}; use \`noir ${d.newArgv.join(" ")}\`.`,
|
|
1233
|
+
opts
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
function createProgram() {
|
|
1239
|
+
const program2 = new Command();
|
|
1240
|
+
program2.name("noir").description("Noir \u2014 discipline, context, and memory layer for agentic CLIs.").version(NOIR_VERSION, "-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>")).addOption(
|
|
1241
|
+
new Option("--tui", "prefer the interactive home menu for bare `noir` (advisory; TTY-only)")
|
|
1242
|
+
).addOption(
|
|
1243
|
+
new Option(
|
|
1244
|
+
"--no-tui",
|
|
1245
|
+
"route bare `noir` to the non-interactive `status` path even in a TTY"
|
|
1246
|
+
)
|
|
1247
|
+
).addOption(new Option("--no-tips", "suppress redirect / deprecation hints on stderr")).exitOverride((err) => {
|
|
1248
|
+
throw err;
|
|
1249
|
+
}).configureOutput({
|
|
1250
|
+
// Stream discipline: --help/--version → stdout (conventional; users pipe
|
|
1251
|
+
// them), errors → stderr. --json data is emitted by command actions via
|
|
1252
|
+
// process.stdout directly, NOT by commander's writeOut, so routing help/
|
|
1253
|
+
// version to stdout does not conflict with --json payloads.
|
|
1254
|
+
writeOut: (str) => process.stdout.write(str),
|
|
1255
|
+
writeErr: (str) => process.stderr.write(str)
|
|
1256
|
+
});
|
|
1257
|
+
program2.hook("preAction", (_thisCmd, actionCmd) => {
|
|
1258
|
+
const opts = actionCmd.optsWithGlobals();
|
|
1259
|
+
const cwd = opts.cwd;
|
|
1260
|
+
if (typeof cwd === "string" && cwd.length > 0) {
|
|
1261
|
+
try {
|
|
1262
|
+
process.chdir(cwd);
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
throw new NoirCliError(
|
|
1265
|
+
EXIT.USAGE,
|
|
1266
|
+
`--cwd: ${err instanceof Error ? err.message : String(err)}`
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
const nonInteractive = opts.json === true || opts.input === false;
|
|
1271
|
+
if (nonInteractive) process.env.NOIR_NON_INTERACTIVE = "1";
|
|
1272
|
+
else delete process.env.NOIR_NON_INTERACTIVE;
|
|
1273
|
+
emitDeprecationHintsFor(actionCmd, toCliOptions(opts));
|
|
1274
|
+
});
|
|
1275
|
+
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)").option(
|
|
1276
|
+
"--upgrade",
|
|
1277
|
+
"run scaffold migrations before re-emitting (re-run on an existing project)"
|
|
1278
|
+
).option("--force", "re-scaffold even if already initialized (bypasses the already-init no-op)").addOption(
|
|
1279
|
+
// S10: target host. Defaults to `'claude'` (the regression anchor). The
|
|
1280
|
+
// chosen host is forwarded to scaffold() + skills emission via
|
|
1281
|
+
// resolveAdapter. The choice list mirrors `SUPPORTED_HOSTS` so a new host
|
|
1282
|
+
// lands here automatically; commander rejects anything else as usage=2.
|
|
1283
|
+
new Option(
|
|
1284
|
+
"--host <id>",
|
|
1285
|
+
"target agentic CLI (default: claude) \u2014 drives host-side emission"
|
|
1286
|
+
).choices(SUPPORTED_HOSTS)
|
|
1287
|
+
).action(
|
|
1288
|
+
async (opts, cmd) => {
|
|
1289
|
+
const transport = opts.transport === "streamable-http" ? "streamable-http" : "stdio";
|
|
1290
|
+
const upgrade = opts.upgrade === true;
|
|
1291
|
+
const force = opts.force === true;
|
|
1292
|
+
const host = parseHost(opts.host);
|
|
1293
|
+
const result = await init(process.cwd(), {
|
|
1294
|
+
transport,
|
|
1295
|
+
url: opts.url,
|
|
1296
|
+
...upgrade ? { upgrade } : {},
|
|
1297
|
+
...force ? { force } : {},
|
|
1298
|
+
...host !== void 0 ? { host } : {}
|
|
1299
|
+
});
|
|
1300
|
+
if (cmd.optsWithGlobals().json === true && result !== void 0) {
|
|
1301
|
+
json({ ok: true, data: result });
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
);
|
|
1305
|
+
program2.command("create [dir]").description("bootstrap the Noir AI layer in a new or empty directory").option("--transport <transport>", "stdio | streamable-http (default: stdio)", "stdio").option("--url <url>", "streamable-http daemon URL (localhost only)").option("--force", "re-scaffold even if already initialized (bypasses the already-init no-op)").addOption(
|
|
1306
|
+
new Option(
|
|
1307
|
+
"--host <id>",
|
|
1308
|
+
"target agentic CLI (default: claude) \u2014 drives host-side emission"
|
|
1309
|
+
).choices(SUPPORTED_HOSTS)
|
|
1310
|
+
).action(
|
|
1311
|
+
async (dir, opts, cmd) => {
|
|
1312
|
+
const transport = opts.transport === "streamable-http" ? "streamable-http" : "stdio";
|
|
1313
|
+
const force = opts.force === true;
|
|
1314
|
+
const host = parseHost(opts.host);
|
|
1315
|
+
const { create } = await import("./create-U2YJH5QC.js");
|
|
1316
|
+
const result = await create(dir, {
|
|
1317
|
+
transport,
|
|
1318
|
+
url: opts.url,
|
|
1319
|
+
...force ? { force } : {},
|
|
1320
|
+
...host !== void 0 ? { host } : {}
|
|
1321
|
+
});
|
|
1322
|
+
if (cmd.optsWithGlobals().json === true && result !== void 0) {
|
|
1323
|
+
json({ ok: true, data: result });
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
);
|
|
1327
|
+
program2.command("sync").description(
|
|
1328
|
+
"re-emit Noir managed files (.mcp.json, CLAUDE.md blocks, NOIR.md brief, ignores) + skills"
|
|
1329
|
+
).option(
|
|
1330
|
+
"--force",
|
|
1331
|
+
"overwrite differing regenerated files without prompting (bypasses the conflict menu)"
|
|
1332
|
+
).option(
|
|
1333
|
+
"--merge",
|
|
1334
|
+
"three-way merge managed regions (default since 1.3.0; flag kept for compatibility)"
|
|
1335
|
+
).addOption(
|
|
1336
|
+
// Opt OUT of managed-region merge (restore strip-replace). Commander
|
|
1337
|
+
// stores `--no-merge-regions` under `mergeRegions` (default true; flag →
|
|
1338
|
+
// false). Bare `noir sync` keeps the merge default (true).
|
|
1339
|
+
new Option(
|
|
1340
|
+
"--no-merge-regions",
|
|
1341
|
+
"strip-replace managed regions (discard hand-edits inside <!-- noir:* --> markers)"
|
|
1342
|
+
)
|
|
1343
|
+
).addOption(
|
|
1344
|
+
// S10: optional `--host` override. When omitted, sync reads host from
|
|
1345
|
+
// `.noir/config.yml` (whatever `noir init --host <id>` persisted). The
|
|
1346
|
+
// override is rarely needed — documented as advanced.
|
|
1347
|
+
new Option(
|
|
1348
|
+
"--host <id>",
|
|
1349
|
+
"override the configured host (advanced; default reads .noir/config.yml)"
|
|
1350
|
+
).choices(SUPPORTED_HOSTS)
|
|
1351
|
+
).action(
|
|
1352
|
+
async (opts, cmd) => {
|
|
1353
|
+
const { sync } = await import("./sync-VYRXCGK4.js");
|
|
1354
|
+
const host = parseHost(opts.host);
|
|
1355
|
+
const force = opts.force === true;
|
|
1356
|
+
const merge = opts.merge === true;
|
|
1357
|
+
const noMergeRegions = opts.mergeRegions === false;
|
|
1358
|
+
const result = host === void 0 && !force && !merge && !noMergeRegions ? await sync(process.cwd()) : await sync(process.cwd(), {
|
|
1359
|
+
...host !== void 0 ? { host } : {},
|
|
1360
|
+
...force ? { force } : {},
|
|
1361
|
+
...merge ? { merge } : {},
|
|
1362
|
+
...noMergeRegions ? { mergeManagedRegions: false } : {}
|
|
1363
|
+
});
|
|
1364
|
+
if (cmd.optsWithGlobals().json === true) {
|
|
1365
|
+
json({ ok: true, data: result });
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
);
|
|
1369
|
+
const mcpCmd = program2.command("mcp").description("MCP server control");
|
|
1370
|
+
mcpCmd.command("serve").description("run the Noir MCP server (stdio, or via the shared daemon)").option("--stdio", "force the stdio transport").action(async (opts) => {
|
|
1371
|
+
await serve({ stdio: opts.stdio === true });
|
|
1372
|
+
});
|
|
1373
|
+
mcpCmd.action(() => {
|
|
1374
|
+
throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio]");
|
|
1375
|
+
});
|
|
1376
|
+
const daemonGrp = program2.command("daemon").description("control the Noir daemon");
|
|
1377
|
+
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) => {
|
|
1378
|
+
const cmd = trailingCmd(args);
|
|
1379
|
+
const g = cmd.optsWithGlobals();
|
|
1380
|
+
const detach = g.detach === true;
|
|
1381
|
+
await daemonStart({ ...toCliOptions(g), ...detach ? { detach } : {} });
|
|
1382
|
+
});
|
|
1383
|
+
daemonGrp.command("stop").description("stop the Noir daemon").action(async (...args) => {
|
|
1384
|
+
await daemonStop(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
1385
|
+
});
|
|
1386
|
+
daemonGrp.command("status").description("report daemon pid/uptime/mode (exit 4 if not running)").action(async (...args) => {
|
|
1387
|
+
await daemonStatus(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
1388
|
+
});
|
|
1389
|
+
daemonGrp.command("restart").description("stop then start the daemon").action(async (...args) => {
|
|
1390
|
+
await daemonRestart(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
1391
|
+
});
|
|
1392
|
+
daemonGrp.action(() => {
|
|
1393
|
+
throw new NoirCliError(EXIT.USAGE, "Usage: noir daemon start|stop|status|restart");
|
|
1394
|
+
});
|
|
1395
|
+
program2.command("doctor").description("environment + project health").option(
|
|
1396
|
+
"--dedup",
|
|
1397
|
+
"scan host-context + .noir/ docs for semantic near-duplicates (loads the local embedder)"
|
|
1398
|
+
).action(async (...args) => {
|
|
1399
|
+
const g = actionGlobals(args);
|
|
1400
|
+
const dedup = trailingCmd(args).opts().dedup === true;
|
|
1401
|
+
await doctor({ ...toCliOptions(g), ...dedup ? { dedup: true } : {} });
|
|
1402
|
+
});
|
|
1403
|
+
program2.command("status").description("project + daemon + workflow + store snapshot").action(async (...args) => {
|
|
1404
|
+
await status(toStatusOptions(actionGlobals(args)));
|
|
1405
|
+
});
|
|
1406
|
+
const contextGrp = program2.command("context").description("context engine (S6)");
|
|
1407
|
+
contextGrp.command("search").description("hybrid search over the indexed context").argument("<query>", "search query").option("--limit <n>", "max results", "10").action(async (...args) => {
|
|
1408
|
+
const cmd = trailingCmd(args);
|
|
1409
|
+
const g = cmd.optsWithGlobals();
|
|
1410
|
+
const query = typeof args[0] === "string" ? args[0] : "";
|
|
1411
|
+
const limit = typeof g.limit === "string" ? g.limit : void 0;
|
|
1412
|
+
await contextSearch({
|
|
1413
|
+
...toCliOptions(g),
|
|
1414
|
+
query,
|
|
1415
|
+
...limit === void 0 ? {} : { limit }
|
|
1416
|
+
});
|
|
1417
|
+
});
|
|
1418
|
+
contextGrp.command("index").description("(re)index project files into the context store").option(
|
|
1419
|
+
"--path <p>",
|
|
1420
|
+
"path to index (repeatable)",
|
|
1421
|
+
(val, acc) => [...acc, val],
|
|
1422
|
+
[]
|
|
1423
|
+
).option("--force", "ignore content-hash caching (recognized; not yet honored)").action(async (...args) => {
|
|
1424
|
+
const cmd = trailingCmd(args);
|
|
1425
|
+
const g = cmd.optsWithGlobals();
|
|
1426
|
+
const rawPaths = Array.isArray(g.path) ? g.path.filter((p) => typeof p === "string") : [];
|
|
1427
|
+
const force = g.force === true;
|
|
1428
|
+
await contextIndex({
|
|
1429
|
+
...toCliOptions(g),
|
|
1430
|
+
...rawPaths.length === 0 ? {} : { paths: rawPaths },
|
|
1431
|
+
...force ? { force } : {}
|
|
1432
|
+
});
|
|
1433
|
+
});
|
|
1434
|
+
contextGrp.command("status").description("index freshness + counts").action(async (...args) => {
|
|
1435
|
+
await contextStatus(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
1436
|
+
});
|
|
1437
|
+
contextGrp.action(() => {
|
|
1438
|
+
throw new NoirCliError(EXIT.USAGE, "Usage: noir context search|index|status");
|
|
1439
|
+
});
|
|
1440
|
+
const memoryGrp = program2.command("memory").description("memory engine");
|
|
1441
|
+
memoryGrp.command("recall").description("recall memories for a query").argument("<query>", "recall query").option("--limit <n>", "max results", "10").action(async (...args) => {
|
|
1442
|
+
const cmd = trailingCmd(args);
|
|
1443
|
+
const g = cmd.optsWithGlobals();
|
|
1444
|
+
const query = typeof args[0] === "string" ? args[0] : "";
|
|
1445
|
+
const limit = typeof g.limit === "string" ? g.limit : void 0;
|
|
1446
|
+
await memoryRecall({
|
|
1447
|
+
...toCliOptions(g),
|
|
1448
|
+
query,
|
|
1449
|
+
...limit === void 0 ? {} : { limit }
|
|
1450
|
+
});
|
|
1451
|
+
});
|
|
1452
|
+
memoryGrp.command("save").description("save an observation to long-term memory").option("--content <text>", "memory content (prompted interactively if omitted)").option(
|
|
1453
|
+
"--type <type>",
|
|
1454
|
+
"observation type (pattern | preference | architecture | bug | workflow | fact | decision)"
|
|
1455
|
+
).option("--files <files>", "comma-separated related file paths").action(async (...args) => {
|
|
1456
|
+
const cmd = trailingCmd(args);
|
|
1457
|
+
const g = cmd.optsWithGlobals();
|
|
1458
|
+
const content = typeof g.content === "string" ? g.content : void 0;
|
|
1459
|
+
const type = typeof g.type === "string" ? g.type : void 0;
|
|
1460
|
+
const files = typeof g.files === "string" ? g.files : void 0;
|
|
1461
|
+
await memorySave({
|
|
1462
|
+
...toCliOptions(g),
|
|
1463
|
+
...content === void 0 ? {} : { content },
|
|
1464
|
+
...type === void 0 ? {} : { type },
|
|
1465
|
+
...files === void 0 ? {} : { files }
|
|
1466
|
+
});
|
|
1467
|
+
});
|
|
1468
|
+
memoryGrp.command("sessions").description("list recent memory sessions").action(async (...args) => {
|
|
1469
|
+
await memorySessions(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
1470
|
+
});
|
|
1471
|
+
memoryGrp.command("forget").description("delete one or more memories by id").argument("<ids...>", "observation id(s)").action(async (...args) => {
|
|
1472
|
+
const cmd = trailingCmd(args);
|
|
1473
|
+
const g = cmd.optsWithGlobals();
|
|
1474
|
+
const rawIds = Array.isArray(args[0]) ? args[0] : [];
|
|
1475
|
+
const ids = rawIds.filter((x) => typeof x === "string");
|
|
1476
|
+
await memoryForget({ ...toCliOptions(g), ids });
|
|
1477
|
+
});
|
|
1478
|
+
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) => {
|
|
1479
|
+
const cmd = trailingCmd(args);
|
|
1480
|
+
const g = cmd.optsWithGlobals();
|
|
1481
|
+
const types = typeof g.types === "string" ? g.types : void 0;
|
|
1482
|
+
const limit = typeof g.limit === "string" ? g.limit : void 0;
|
|
1483
|
+
await memoryConsolidate({
|
|
1484
|
+
...toCliOptions(g),
|
|
1485
|
+
...types === void 0 ? {} : { types },
|
|
1486
|
+
...limit === void 0 ? {} : { limit }
|
|
1487
|
+
});
|
|
1488
|
+
});
|
|
1489
|
+
memoryGrp.action(() => {
|
|
1490
|
+
throw new NoirCliError(
|
|
1491
|
+
EXIT.USAGE,
|
|
1492
|
+
"Usage: noir memory recall|save|sessions|forget|consolidate"
|
|
1493
|
+
);
|
|
1494
|
+
});
|
|
1495
|
+
const skillsGrp = program2.command("skills").description("builtin skills (S5)");
|
|
1496
|
+
skillsGrp.command("list").description("list installed Noir skills").action(async (...args) => {
|
|
1497
|
+
await skillsList(toCliOptions(actionGlobals(args)));
|
|
1498
|
+
});
|
|
1499
|
+
skillsGrp.command("sync").description("re-emit skills to the host skills dir").action(async (...args) => {
|
|
1500
|
+
await skillsSync(toCliOptions(actionGlobals(args)));
|
|
1501
|
+
});
|
|
1502
|
+
skillsGrp.action(() => {
|
|
1503
|
+
throw new NoirCliError(EXIT.USAGE, "Usage: noir skills list|sync");
|
|
1504
|
+
});
|
|
1505
|
+
const taskGrp = program2.command("task").description("workflow task control");
|
|
1506
|
+
taskGrp.command("new").description("start a new workflow task").requiredOption("--slug <slug>", "task slug").option("--mode <mode>", "full | quick").action(async (...args) => {
|
|
1507
|
+
const cmd = trailingCmd(args);
|
|
1508
|
+
const g = cmd.optsWithGlobals();
|
|
1509
|
+
const slug = typeof g.slug === "string" ? g.slug : "";
|
|
1510
|
+
const mode = typeof g.mode === "string" ? g.mode : void 0;
|
|
1511
|
+
await taskNew({
|
|
1512
|
+
...toCliOptions(g),
|
|
1513
|
+
slug,
|
|
1514
|
+
...mode === void 0 ? {} : { mode }
|
|
1515
|
+
});
|
|
1516
|
+
});
|
|
1517
|
+
taskGrp.command("status").description("active (or named) task status").argument("[id]", "task id (defaults to active)").action(async (...args) => {
|
|
1518
|
+
const cmd = trailingCmd(args);
|
|
1519
|
+
const g = cmd.optsWithGlobals();
|
|
1520
|
+
const first = args[0];
|
|
1521
|
+
const id = typeof first === "string" ? first : void 0;
|
|
1522
|
+
await taskStatus({
|
|
1523
|
+
...toCliOptions(g),
|
|
1524
|
+
...id === void 0 ? {} : { id }
|
|
1525
|
+
});
|
|
1526
|
+
});
|
|
1527
|
+
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) => {
|
|
1528
|
+
const cmd = trailingCmd(args);
|
|
1529
|
+
const g = cmd.optsWithGlobals();
|
|
1530
|
+
const to = typeof g.to === "string" ? g.to : void 0;
|
|
1531
|
+
const force = typeof g.force === "string" ? g.force : void 0;
|
|
1532
|
+
await taskAdvance({
|
|
1533
|
+
...toCliOptions(g),
|
|
1534
|
+
...to === void 0 ? {} : { to },
|
|
1535
|
+
...force === void 0 ? {} : { force }
|
|
1536
|
+
});
|
|
1537
|
+
});
|
|
1538
|
+
taskGrp.command("next").description("suggest the next phase + applicable skill").action(async (...args) => {
|
|
1539
|
+
await taskNext(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
1540
|
+
});
|
|
1541
|
+
taskGrp.action(() => {
|
|
1542
|
+
throw new NoirCliError(EXIT.USAGE, "Usage: noir task new|status|advance|next");
|
|
1543
|
+
});
|
|
1544
|
+
function buildHandoffOptions(g) {
|
|
1545
|
+
return { ...toCliOptions(g), ...g.write === true ? { write: true } : {} };
|
|
1546
|
+
}
|
|
1547
|
+
program2.command("handoff").description("emit a ready-to-paste host handoff prompt").option("--write", "persist to .noir/handoff/<id>.md (gitignored)").action(async (...args) => {
|
|
1548
|
+
await handoff(buildHandoffOptions(trailingCmd(args).optsWithGlobals()));
|
|
1549
|
+
});
|
|
1550
|
+
program2.command("wrap").description("session-end alias for `noir handoff`").option("--write", "persist to .noir/handoff/<id>.md (gitignored)").action(async (...args) => {
|
|
1551
|
+
await handoff(buildHandoffOptions(trailingCmd(args).optsWithGlobals()));
|
|
1552
|
+
});
|
|
1553
|
+
program2.command("tui").description("interactive Ink dashboard (host \xB7 phase \xB7 daemon + /command dispatch)").action(async (...args) => {
|
|
1554
|
+
const g = actionGlobals(args);
|
|
1555
|
+
const opts = toCliOptions(g);
|
|
1556
|
+
requireInteractive(opts, "`noir tui`");
|
|
1557
|
+
const tuiUrl = new URL("./tui/index.js", import.meta.url).href;
|
|
1558
|
+
const { runTui: runTui2 } = await import(
|
|
1559
|
+
/* @vite-ignore */
|
|
1560
|
+
tuiUrl
|
|
1561
|
+
);
|
|
1562
|
+
const dispatch = async (argv) => {
|
|
1563
|
+
const sub = createProgram();
|
|
1564
|
+
try {
|
|
1565
|
+
await sub.parseAsync([...argv], { from: "user" });
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
handleError(err);
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
await runTui2(opts, dispatch);
|
|
1571
|
+
});
|
|
1572
|
+
const homeDeps = {
|
|
1573
|
+
dispatch: async (argv) => {
|
|
1574
|
+
const sub = createProgram();
|
|
1575
|
+
try {
|
|
1576
|
+
await sub.parseAsync([...argv], { from: "user" });
|
|
1577
|
+
} catch (err) {
|
|
1578
|
+
handleError(err);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
program2.action(async (...args) => {
|
|
1583
|
+
const cmd = trailingCmd(args);
|
|
1584
|
+
const leftovers = cmd.args;
|
|
1585
|
+
if (leftovers.length > 0) {
|
|
1586
|
+
fail(
|
|
1587
|
+
EXIT.NOT_FOUND,
|
|
1588
|
+
`unknown command '${leftovers[0]}' (no such subcommand). Run \`noir --help\` for the list.`,
|
|
1589
|
+
toCliOptions(cmd.optsWithGlobals())
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
await home(toCliOptions(actionGlobals(args)), homeDeps);
|
|
1593
|
+
});
|
|
1594
|
+
return program2;
|
|
1595
|
+
}
|
|
1596
|
+
var program = createProgram();
|
|
1597
|
+
async function run(argv = []) {
|
|
1598
|
+
try {
|
|
1599
|
+
await program.parseAsync([...argv], { from: "user" });
|
|
1600
|
+
} catch (err) {
|
|
1601
|
+
handleError(err);
|
|
1602
|
+
}
|
|
1603
|
+
return typeof process.exitCode === "number" ? process.exitCode : EXIT.OK;
|
|
1604
|
+
}
|
|
1605
|
+
var isMainModule = (() => {
|
|
1606
|
+
try {
|
|
1607
|
+
const entry = process.argv[1];
|
|
1608
|
+
if (typeof entry !== "string" || entry.length === 0) return false;
|
|
1609
|
+
return pathToFileURL(realpathSync(entry)).href === import.meta.url;
|
|
1610
|
+
} catch {
|
|
1611
|
+
return false;
|
|
1612
|
+
}
|
|
1613
|
+
})();
|
|
1614
|
+
if (isMainModule) {
|
|
1615
|
+
void run(process.argv.slice(2));
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// src/daemon-client.ts
|
|
1619
|
+
var DAEMON_DOWN_HINT = "daemon not reachable \u2014 try `noir daemon start`";
|
|
1620
|
+
function describeCause(cause) {
|
|
1621
|
+
if (cause instanceof Error) return `${cause.name}: ${cause.message}`;
|
|
1622
|
+
try {
|
|
1623
|
+
return JSON.stringify(cause);
|
|
1624
|
+
} catch {
|
|
1625
|
+
return String(cause);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
function failDaemonDown(opts, cause) {
|
|
1629
|
+
if (opts.verbose) {
|
|
1630
|
+
process.stderr.write(`noir: daemon transport detail: ${describeCause(cause)}
|
|
1631
|
+
`);
|
|
1632
|
+
}
|
|
1633
|
+
fail(EXIT.DAEMON_DOWN, DAEMON_DOWN_HINT, opts);
|
|
1634
|
+
}
|
|
1635
|
+
async function probeDaemon(opts = {}) {
|
|
1636
|
+
const rec = readDaemonRecord2();
|
|
1637
|
+
if (!rec) {
|
|
1638
|
+
if (opts.verbose) process.stderr.write("noir: daemon probe: no daemon record\n");
|
|
1639
|
+
return { running: false };
|
|
1640
|
+
}
|
|
1641
|
+
if (!pidAlive2(rec.pid)) {
|
|
1642
|
+
if (opts.verbose) process.stderr.write(`noir: daemon probe: pid ${rec.pid} not alive
|
|
1643
|
+
`);
|
|
1644
|
+
return { running: false };
|
|
1645
|
+
}
|
|
1646
|
+
try {
|
|
1647
|
+
const res = await fetch(`http://127.0.0.1:${rec.port}/health`);
|
|
1648
|
+
if (!res.ok) {
|
|
1649
|
+
if (opts.verbose) process.stderr.write(`noir: daemon probe: /health \u2192 HTTP ${res.status}
|
|
1650
|
+
`);
|
|
1651
|
+
return { running: false };
|
|
1652
|
+
}
|
|
1653
|
+
const body = await res.json().catch(() => null);
|
|
1654
|
+
if (body?.ok !== true) {
|
|
1655
|
+
if (opts.verbose) process.stderr.write("noir: daemon probe: /health body not ok\n");
|
|
1656
|
+
return { running: false };
|
|
1657
|
+
}
|
|
1658
|
+
return {
|
|
1659
|
+
running: true,
|
|
1660
|
+
pid: typeof body.pid === "number" ? body.pid : rec.pid,
|
|
1661
|
+
port: rec.port,
|
|
1662
|
+
uptimeSec: typeof body.uptimeSec === "number" ? body.uptimeSec : void 0
|
|
1663
|
+
};
|
|
1664
|
+
} catch (err) {
|
|
1665
|
+
if (opts.verbose)
|
|
1666
|
+
process.stderr.write(`noir: daemon probe: unreachable (${describeCause(err)})
|
|
1667
|
+
`);
|
|
1668
|
+
return { running: false };
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
async function resolveDaemon(opts) {
|
|
1672
|
+
const project = opts.project ?? loadProjectInfo6(process.cwd());
|
|
1673
|
+
const idleTimeoutSec = opts.idleTimeoutSec ?? project.config.daemon.idleTimeoutSec;
|
|
1674
|
+
try {
|
|
1675
|
+
const ensured = await ensureDaemonRunning3({ project, idleTimeoutSec });
|
|
1676
|
+
return { url: ensured.url, stop: ensured.stop };
|
|
1677
|
+
} catch (err) {
|
|
1678
|
+
failDaemonDown(opts, err);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
async function connectClient(client, url, opts) {
|
|
1682
|
+
try {
|
|
1683
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(url)));
|
|
1684
|
+
} catch (err) {
|
|
1685
|
+
failDaemonDown(opts, err);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
async function callToolParse(client, opts, name, args) {
|
|
1689
|
+
const result = await client.callTool({ name, arguments: args }).catch((err) => failDaemonDown(opts, err));
|
|
1690
|
+
const block = result.content?.[0];
|
|
1691
|
+
if (block?.type !== "text" || typeof block.text !== "string") {
|
|
1692
|
+
failDaemonDown(opts, new Error(`tool '${name}' returned no text content`));
|
|
1693
|
+
}
|
|
1694
|
+
const text = block.text;
|
|
1695
|
+
try {
|
|
1696
|
+
return JSON.parse(text);
|
|
1697
|
+
} catch (err) {
|
|
1698
|
+
failDaemonDown(opts, err);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function buildCaller(client, opts) {
|
|
1702
|
+
return {
|
|
1703
|
+
callTool: (name, args = {}) => callToolParse(client, opts, name, args),
|
|
1704
|
+
listTools: async () => {
|
|
1705
|
+
const res = await client.listTools().catch((err) => failDaemonDown(opts, err));
|
|
1706
|
+
const tools = res?.tools;
|
|
1707
|
+
if (!Array.isArray(tools)) return [];
|
|
1708
|
+
const names = [];
|
|
1709
|
+
for (const t of tools) {
|
|
1710
|
+
const name = t?.name;
|
|
1711
|
+
if (typeof name === "string") names.push(name);
|
|
1712
|
+
}
|
|
1713
|
+
return names;
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
async function withDaemon(opts, fn) {
|
|
1718
|
+
const { url, stop } = await resolveDaemon(opts);
|
|
1719
|
+
let client;
|
|
1720
|
+
try {
|
|
1721
|
+
client = new Client(
|
|
1722
|
+
{ name: "noir-cli", version: NOIR_VERSION2 },
|
|
1723
|
+
{ versionNegotiation: { mode: "auto" } }
|
|
1724
|
+
);
|
|
1725
|
+
const connected = client;
|
|
1726
|
+
await connectClient(connected, url, opts);
|
|
1727
|
+
return await fn(buildCaller(connected, opts));
|
|
1728
|
+
} finally {
|
|
1729
|
+
if (client) {
|
|
1730
|
+
await client.close().catch(() => {
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
await stop().catch(() => {
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
async function withRunningDaemon(opts, fn, probe) {
|
|
1738
|
+
const p = probe ?? await probeDaemon(opts);
|
|
1739
|
+
if (!p.running) return null;
|
|
1740
|
+
const url = `http://127.0.0.1:${p.port}/mcp`;
|
|
1741
|
+
let client;
|
|
1742
|
+
try {
|
|
1743
|
+
client = new Client(
|
|
1744
|
+
{ name: "noir-cli", version: NOIR_VERSION2 },
|
|
1745
|
+
{ versionNegotiation: { mode: "auto" } }
|
|
1746
|
+
);
|
|
1747
|
+
const connected = client;
|
|
1748
|
+
await connected.connect(new StreamableHTTPClientTransport(new URL(url)));
|
|
1749
|
+
return await fn(buildCaller(connected, opts));
|
|
1750
|
+
} catch {
|
|
1751
|
+
return null;
|
|
1752
|
+
} finally {
|
|
1753
|
+
if (client) {
|
|
1754
|
+
await client.close().catch(() => {
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
async function callDaemonTool(opts, name, args = {}) {
|
|
1760
|
+
return withDaemon(opts, (caller) => caller.callTool(name, args));
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
// src/commands/status.ts
|
|
1764
|
+
async function tryTool(caller, name, args) {
|
|
1765
|
+
try {
|
|
1766
|
+
return await caller.callTool(name, args);
|
|
1767
|
+
} catch {
|
|
1768
|
+
return null;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
function pickStore(s) {
|
|
1772
|
+
if (s?.ok !== true) return null;
|
|
1773
|
+
return {
|
|
1774
|
+
docCount: s.docCount,
|
|
1775
|
+
vecCount: s.vecCount,
|
|
1776
|
+
dbPath: s.dbPath,
|
|
1777
|
+
degraded: s.degraded === true
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
function describeEmbedder2(e) {
|
|
1781
|
+
if (!e || e.kind === "none") return "none (BM25-only)";
|
|
1782
|
+
const model = typeof e.model === "string" && e.model.length > 0 ? e.model : "<unset>";
|
|
1783
|
+
return `${e.kind} \xB7 ${model} (${e.dim}-dim)`;
|
|
1784
|
+
}
|
|
1785
|
+
function pickContext(c2) {
|
|
1786
|
+
if (c2?.ok !== true) return null;
|
|
1787
|
+
return {
|
|
1788
|
+
docCount: c2.docCount,
|
|
1789
|
+
vecCount: c2.vecCount,
|
|
1790
|
+
indexedFiles: c2.indexedFiles,
|
|
1791
|
+
embedder: describeEmbedder2(c2.embedder),
|
|
1792
|
+
degraded: c2.degraded === true
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
function pickWorkflow(w) {
|
|
1796
|
+
if (w?.ok !== true) return null;
|
|
1797
|
+
return {
|
|
1798
|
+
taskId: w.taskId,
|
|
1799
|
+
phase: w.phase,
|
|
1800
|
+
state: w.state,
|
|
1801
|
+
mode: w.mode,
|
|
1802
|
+
nextGate: w.nextGate,
|
|
1803
|
+
degraded: w.degraded === true
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
function pickMemory(m) {
|
|
1807
|
+
if (m?.ok !== true || !Array.isArray(m.sessions)) return null;
|
|
1808
|
+
let observations = 0;
|
|
1809
|
+
for (const s of m.sessions) observations += s.count;
|
|
1810
|
+
return { sessions: m.sessions.length, observations };
|
|
1811
|
+
}
|
|
1812
|
+
function buildPayload(project, probe, host, store, context, workflow, memory) {
|
|
1813
|
+
return {
|
|
1814
|
+
noir: NOIR_VERSION3,
|
|
1815
|
+
project: { id: project.id, name: project.name },
|
|
1816
|
+
host: project.config.host,
|
|
1817
|
+
daemon: {
|
|
1818
|
+
running: probe.running,
|
|
1819
|
+
...probe.pid !== void 0 ? { pid: probe.pid } : {},
|
|
1820
|
+
...probe.uptimeSec !== void 0 ? { uptimeSec: probe.uptimeSec } : {},
|
|
1821
|
+
...host && typeof host.transport === "string" ? { transport: host.transport } : {}
|
|
1822
|
+
},
|
|
1823
|
+
store: pickStore(store),
|
|
1824
|
+
context: pickContext(context),
|
|
1825
|
+
workflow: pickWorkflow(workflow),
|
|
1826
|
+
memory: pickMemory(memory)
|
|
1827
|
+
};
|
|
1828
|
+
}
|
|
1829
|
+
function formatDuration(sec) {
|
|
1830
|
+
if (typeof sec !== "number" || sec < 0) return "unknown";
|
|
1831
|
+
if (sec < 60) return `${sec}s`;
|
|
1832
|
+
if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
1833
|
+
return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
|
|
1834
|
+
}
|
|
1835
|
+
function describeDaemon(d) {
|
|
1836
|
+
if (!d.running) return "not running (start with `noir daemon start`)";
|
|
1837
|
+
const pid = typeof d.pid === "number" ? `pid ${d.pid}` : "pid unknown";
|
|
1838
|
+
const up = typeof d.uptimeSec === "number" ? `, up ${formatDuration(d.uptimeSec)}` : "";
|
|
1839
|
+
const transport = typeof d.transport === "string" ? `, ${d.transport}` : "";
|
|
1840
|
+
return `running (${pid}${up}${transport})`;
|
|
1841
|
+
}
|
|
1842
|
+
function describeStore(s) {
|
|
1843
|
+
if (!s) return "unavailable (store engine not wired)";
|
|
1844
|
+
const flag = s.degraded ? ` ${badge("warn", "degraded: read-only")}` : "";
|
|
1845
|
+
return `${s.docCount} docs / ${s.vecCount} vecs${flag}`;
|
|
1846
|
+
}
|
|
1847
|
+
function describeContext(c2) {
|
|
1848
|
+
if (!c2) return "unavailable (context engine not wired)";
|
|
1849
|
+
const flag = c2.degraded ? ` ${badge("warn", "degraded")}` : "";
|
|
1850
|
+
return `${c2.docCount} docs, ${c2.vecCount} vecs, ${c2.indexedFiles} files \xB7 embedder ${c2.embedder}${flag}`;
|
|
1851
|
+
}
|
|
1852
|
+
function describeWorkflow(w) {
|
|
1853
|
+
if (!w) return "no active task";
|
|
1854
|
+
const gate = w.nextGate ? ` \u2192 next gate: ${w.nextGate}` : "";
|
|
1855
|
+
const flag = w.degraded ? ` ${badge("warn", "degraded")}` : "";
|
|
1856
|
+
return `${w.phase} (${w.state}, ${w.mode}) \xB7 task ${w.taskId}${gate}${flag}`;
|
|
1857
|
+
}
|
|
1858
|
+
function describeMemory(m) {
|
|
1859
|
+
if (!m) return "unavailable (memory engine not wired)";
|
|
1860
|
+
return `${m.observations} observations across ${m.sessions} session${m.sessions === 1 ? "" : "s"}`;
|
|
1861
|
+
}
|
|
1862
|
+
function renderHuman(p, opts) {
|
|
1863
|
+
log(`noir status \u2014 ${p.project.name} (${p.project.id})`, opts);
|
|
1864
|
+
definitionList(
|
|
1865
|
+
[
|
|
1866
|
+
{ label: "Project", value: `${p.project.name} (${p.project.id})` },
|
|
1867
|
+
{ label: "Host", value: p.host },
|
|
1868
|
+
{ label: "Noir", value: p.noir },
|
|
1869
|
+
{ label: "Daemon", value: describeDaemon(p.daemon) },
|
|
1870
|
+
{ label: "Store", value: describeStore(p.store) },
|
|
1871
|
+
{ label: "Context", value: describeContext(p.context) },
|
|
1872
|
+
{ label: "Workflow", value: describeWorkflow(p.workflow) },
|
|
1873
|
+
{ label: "Memory", value: describeMemory(p.memory) }
|
|
1874
|
+
],
|
|
1875
|
+
opts
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
async function gatherStatusPayload(opts) {
|
|
1879
|
+
const project = loadProjectInfo7(process.cwd());
|
|
1880
|
+
const probe = await probeDaemon(opts);
|
|
1881
|
+
let host = null;
|
|
1882
|
+
let store = null;
|
|
1883
|
+
let context = null;
|
|
1884
|
+
let workflow = null;
|
|
1885
|
+
let memory = null;
|
|
1886
|
+
if (probe.running) {
|
|
1887
|
+
await withRunningDaemon(
|
|
1888
|
+
opts,
|
|
1889
|
+
async (caller) => {
|
|
1890
|
+
host = await tryTool(caller, "host_status");
|
|
1891
|
+
store = await tryTool(caller, "store_status");
|
|
1892
|
+
context = await tryTool(caller, "context_status");
|
|
1893
|
+
workflow = await tryTool(caller, "workflow_status");
|
|
1894
|
+
memory = await tryTool(caller, "memory_sessions");
|
|
1895
|
+
},
|
|
1896
|
+
probe
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
return buildPayload(project, probe, host, store, context, workflow, memory);
|
|
1900
|
+
}
|
|
1901
|
+
async function status(opts) {
|
|
1902
|
+
const payload = await gatherStatusPayload(opts);
|
|
1903
|
+
if (opts.json === true) {
|
|
1904
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
|
|
1905
|
+
`);
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
renderHuman(payload, opts);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// src/tui/App.tsx
|
|
1912
|
+
import { Box, Text as Text6, useApp, useInput } from "ink";
|
|
1913
|
+
import { useEffect, useState } from "react";
|
|
1914
|
+
|
|
1915
|
+
// src/tui/CommandInput.tsx
|
|
1916
|
+
import { Text } from "ink";
|
|
1917
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
1918
|
+
function CommandInput({ buffer, running }) {
|
|
1919
|
+
if (running) {
|
|
1920
|
+
return /* @__PURE__ */ jsx(Text, { children: c.dim("\u2026") });
|
|
1921
|
+
}
|
|
1922
|
+
if (buffer.length === 0) {
|
|
1923
|
+
return /* @__PURE__ */ jsx(Text, { children: c.dim("type a /command (e.g. /status, /sync, /task next), or q to quit") });
|
|
1924
|
+
}
|
|
1925
|
+
const isCommand = buffer.startsWith("/");
|
|
1926
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
1927
|
+
/* @__PURE__ */ jsx(Text, { children: isCommand ? c.accent("\u25B8") : c.dim("\u25B8") }),
|
|
1928
|
+
" ",
|
|
1929
|
+
buffer,
|
|
1930
|
+
/* @__PURE__ */ jsx(Text, { children: c.dim("\u258C") })
|
|
1931
|
+
] });
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// src/tui/capture.ts
|
|
1935
|
+
async function captureProcessOutput(fn) {
|
|
1936
|
+
const outChunks = [];
|
|
1937
|
+
const errChunks = [];
|
|
1938
|
+
const origOut = process.stdout.write.bind(process.stdout);
|
|
1939
|
+
const origErr = process.stderr.write.bind(process.stderr);
|
|
1940
|
+
const prevExitCode = process.exitCode;
|
|
1941
|
+
process.stdout.write = ((chunk) => {
|
|
1942
|
+
outChunks.push(typeof chunk === "string" ? chunk : String(chunk));
|
|
1943
|
+
return true;
|
|
1944
|
+
});
|
|
1945
|
+
process.stderr.write = ((chunk) => {
|
|
1946
|
+
errChunks.push(typeof chunk === "string" ? chunk : String(chunk));
|
|
1947
|
+
return true;
|
|
1948
|
+
});
|
|
1949
|
+
try {
|
|
1950
|
+
await fn();
|
|
1951
|
+
} finally {
|
|
1952
|
+
process.stdout.write = origOut;
|
|
1953
|
+
process.stderr.write = origErr;
|
|
1954
|
+
}
|
|
1955
|
+
const exitCode = typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
1956
|
+
process.exitCode = prevExitCode;
|
|
1957
|
+
return { stdout: outChunks.join(""), stderr: errChunks.join(""), exitCode };
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// src/tui/Footer.tsx
|
|
1961
|
+
import { Text as Text2 } from "ink";
|
|
1962
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
1963
|
+
function Footer({ running }) {
|
|
1964
|
+
if (running) {
|
|
1965
|
+
return /* @__PURE__ */ jsx2(Text2, { children: c.dim("running\u2026 (Ctrl+C to force exit)") });
|
|
1966
|
+
}
|
|
1967
|
+
return /* @__PURE__ */ jsx2(Text2, { children: c.dim("? help \xB7 q/Esc quit \xB7 \u2191/\u2193 scroll \xB7 Enter run \xB7 Ctrl+C exit") });
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// src/tui/format.ts
|
|
1971
|
+
function formatStatusPayload(p) {
|
|
1972
|
+
if (p === null) return null;
|
|
1973
|
+
return [
|
|
1974
|
+
`Project: ${p.project.name} (${p.project.id})`,
|
|
1975
|
+
`Host: ${p.host}`,
|
|
1976
|
+
`Noir: ${p.noir}`,
|
|
1977
|
+
`Daemon: ${describeDaemon(p.daemon)}`,
|
|
1978
|
+
`Store: ${describeStore(p.store)}`,
|
|
1979
|
+
`Context: ${describeContext(p.context)}`,
|
|
1980
|
+
`Workflow: ${describeWorkflow(p.workflow)}`,
|
|
1981
|
+
`Memory: ${describeMemory(p.memory)}`
|
|
1982
|
+
];
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// src/tui/Header.tsx
|
|
1986
|
+
import { Text as Text3 } from "ink";
|
|
1987
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1988
|
+
function Header({ tagline }) {
|
|
1989
|
+
return /* @__PURE__ */ jsxs2(Text3, { children: [
|
|
1990
|
+
c.accent("\u25C6"),
|
|
1991
|
+
" ",
|
|
1992
|
+
c.bold("noir"),
|
|
1993
|
+
" ",
|
|
1994
|
+
/* @__PURE__ */ jsx3(Text3, { children: c.dim(tagline ?? "dashboard") })
|
|
1995
|
+
] });
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/tui/OutputPane.tsx
|
|
1999
|
+
import { Text as Text4 } from "ink";
|
|
2000
|
+
import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2001
|
+
function toRows(visible) {
|
|
2002
|
+
const rows = [];
|
|
2003
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2004
|
+
for (const text of visible) {
|
|
2005
|
+
const n = (seen.get(text) ?? 0) + 1;
|
|
2006
|
+
seen.set(text, n);
|
|
2007
|
+
rows.push({ key: n === 1 ? text || "_blank" : `${text || "_blank"}#${n}`, text });
|
|
2008
|
+
}
|
|
2009
|
+
return rows;
|
|
2010
|
+
}
|
|
2011
|
+
function OutputPane({
|
|
2012
|
+
lines,
|
|
2013
|
+
scrollOffset,
|
|
2014
|
+
height = 12,
|
|
2015
|
+
title
|
|
2016
|
+
}) {
|
|
2017
|
+
const width = terminalWidth();
|
|
2018
|
+
if (lines.length === 0) {
|
|
2019
|
+
return /* @__PURE__ */ jsx4(Text4, { children: c.dim("(no output \u2014 type a /command, or ? for help)") });
|
|
2020
|
+
}
|
|
2021
|
+
const maxOffset = Math.max(0, lines.length - height);
|
|
2022
|
+
const offset = Math.min(Math.max(0, scrollOffset), maxOffset);
|
|
2023
|
+
const rows = toRows(lines.slice(offset, offset + height));
|
|
2024
|
+
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
2025
|
+
title !== void 0 ? /* @__PURE__ */ jsx4(Text4, { children: c.dim(`\u2500\u2500 ${title} \u2500\u2500`) }) : null,
|
|
2026
|
+
rows.map((row) => /* @__PURE__ */ jsx4(Text4, { children: row.text.length > width ? `${row.text.slice(0, Math.max(1, width - 1))}\u2026` : row.text }, row.key))
|
|
2027
|
+
] });
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// src/tui/StatusBar.tsx
|
|
2031
|
+
import { Text as Text5 } from "ink";
|
|
2032
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2033
|
+
function Cell({
|
|
2034
|
+
label,
|
|
2035
|
+
value,
|
|
2036
|
+
accent
|
|
2037
|
+
}) {
|
|
2038
|
+
return /* @__PURE__ */ jsxs4(Text5, { children: [
|
|
2039
|
+
/* @__PURE__ */ jsx5(Text5, { children: c.dim(`${label}:`) }),
|
|
2040
|
+
" ",
|
|
2041
|
+
/* @__PURE__ */ jsx5(Text5, { children: accent ? c.accent(value) : value })
|
|
2042
|
+
] });
|
|
2043
|
+
}
|
|
2044
|
+
function StatusBar({ payload, loading }) {
|
|
2045
|
+
const host = payload?.host ?? "\u2014";
|
|
2046
|
+
const mode = payload?.workflow?.mode ?? "\u2014";
|
|
2047
|
+
const phase = payload?.workflow?.phase ?? (loading ? "\u2026" : "idle");
|
|
2048
|
+
const daemonRunning = payload?.daemon.running === true;
|
|
2049
|
+
const daemon = payload === null && loading ? "\u2026" : daemonRunning ? "up" : "down";
|
|
2050
|
+
return /* @__PURE__ */ jsxs4(Text5, { children: [
|
|
2051
|
+
/* @__PURE__ */ jsx5(Cell, { label: "host", value: host, accent: true }),
|
|
2052
|
+
/* @__PURE__ */ jsx5(Text5, { children: c.dim(" \xB7 ") }),
|
|
2053
|
+
/* @__PURE__ */ jsx5(Cell, { label: "mode", value: mode }),
|
|
2054
|
+
/* @__PURE__ */ jsx5(Text5, { children: c.dim(" \xB7 ") }),
|
|
2055
|
+
/* @__PURE__ */ jsx5(Cell, { label: "phase", value: phase }),
|
|
2056
|
+
/* @__PURE__ */ jsx5(Text5, { children: c.dim(" \xB7 ") }),
|
|
2057
|
+
/* @__PURE__ */ jsxs4(Text5, { children: [
|
|
2058
|
+
c.dim("daemon:"),
|
|
2059
|
+
" ",
|
|
2060
|
+
daemonRunning ? c.ok(daemon) : c.warn(daemon)
|
|
2061
|
+
] })
|
|
2062
|
+
] });
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// src/tui/App.tsx
|
|
2066
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2067
|
+
function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
|
|
2068
|
+
const { exit } = useApp();
|
|
2069
|
+
const [buffer, setBuffer] = useState("");
|
|
2070
|
+
const [payload, setPayload] = useState(initialPayload);
|
|
2071
|
+
const [loading, setLoading] = useState(initialPayload === null);
|
|
2072
|
+
const [output, setOutput] = useState(null);
|
|
2073
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
2074
|
+
const [help, setHelp] = useState(false);
|
|
2075
|
+
const [running, setRunning] = useState(false);
|
|
2076
|
+
const [notice, setNotice] = useState(null);
|
|
2077
|
+
const [pending, setPending] = useState(null);
|
|
2078
|
+
useEffect(() => {
|
|
2079
|
+
if (running) return;
|
|
2080
|
+
let cancelled = false;
|
|
2081
|
+
setLoading(true);
|
|
2082
|
+
void deps.fetchStatus().then((p) => {
|
|
2083
|
+
if (!cancelled) {
|
|
2084
|
+
setPayload(p);
|
|
2085
|
+
setLoading(false);
|
|
2086
|
+
}
|
|
2087
|
+
}).catch(() => {
|
|
2088
|
+
if (!cancelled) {
|
|
2089
|
+
setPayload(null);
|
|
2090
|
+
setLoading(false);
|
|
2091
|
+
}
|
|
2092
|
+
});
|
|
2093
|
+
const id = setInterval(() => {
|
|
2094
|
+
void deps.fetchStatus().then((p) => {
|
|
2095
|
+
if (!cancelled) setPayload(p);
|
|
2096
|
+
}).catch(() => {
|
|
2097
|
+
});
|
|
2098
|
+
}, refreshMs);
|
|
2099
|
+
return () => {
|
|
2100
|
+
cancelled = true;
|
|
2101
|
+
clearInterval(id);
|
|
2102
|
+
};
|
|
2103
|
+
}, [running, refreshMs, deps]);
|
|
2104
|
+
useEffect(() => {
|
|
2105
|
+
if (pending === null) return;
|
|
2106
|
+
const argv = pending;
|
|
2107
|
+
let active = true;
|
|
2108
|
+
const run2 = async () => {
|
|
2109
|
+
const captured = await captureProcessOutput(() => deps.dispatch(argv));
|
|
2110
|
+
if (!active) return;
|
|
2111
|
+
const combined = `${captured.stdout}${captured.stderr}`;
|
|
2112
|
+
const lines = combined.split("\n");
|
|
2113
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
2114
|
+
const title = `/${argv.join(" ")} \u2192 exit ${captured.exitCode}`;
|
|
2115
|
+
setOutput({ title, lines });
|
|
2116
|
+
setNotice(`ran /${argv.join(" ")} \u2192 exit ${captured.exitCode}`);
|
|
2117
|
+
setRunning(false);
|
|
2118
|
+
setPending(null);
|
|
2119
|
+
setScrollOffset(0);
|
|
2120
|
+
};
|
|
2121
|
+
void run2();
|
|
2122
|
+
return () => {
|
|
2123
|
+
active = false;
|
|
2124
|
+
};
|
|
2125
|
+
}, [pending, deps]);
|
|
2126
|
+
useInput((input, key) => {
|
|
2127
|
+
if (running) return;
|
|
2128
|
+
setNotice(null);
|
|
2129
|
+
if (help) {
|
|
2130
|
+
if (input === "?" || key.escape || input === "q") setHelp(false);
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
if (key.return) {
|
|
2134
|
+
submit();
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
if (key.escape) {
|
|
2138
|
+
if (buffer.length > 0) setBuffer("");
|
|
2139
|
+
else if (output !== null) setOutput(null);
|
|
2140
|
+
else exit();
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
if (key.backspace || key.delete) {
|
|
2144
|
+
setBuffer((b) => b.slice(0, -1));
|
|
2145
|
+
return;
|
|
2146
|
+
}
|
|
2147
|
+
if (key.upArrow) {
|
|
2148
|
+
setScrollOffset((o) => Math.max(0, o - 1));
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
if (key.downArrow) {
|
|
2152
|
+
setScrollOffset((o) => o + 1);
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
if (key.ctrl) return;
|
|
2156
|
+
if (input.length === 1) {
|
|
2157
|
+
if (buffer.length === 0) {
|
|
2158
|
+
if (input === "q") {
|
|
2159
|
+
exit();
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
if (input === "?") {
|
|
2163
|
+
setHelp(true);
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
setBuffer((b) => b + input);
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
if (input.length > 1) {
|
|
2171
|
+
setBuffer((b) => b + input);
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
2174
|
+
function submit() {
|
|
2175
|
+
const text = buffer;
|
|
2176
|
+
if (text.length === 0) return;
|
|
2177
|
+
setBuffer("");
|
|
2178
|
+
if (!text.startsWith("/")) {
|
|
2179
|
+
setNotice("type a /command to run it (bare text is just a hint)");
|
|
2180
|
+
return;
|
|
2181
|
+
}
|
|
2182
|
+
const argv = text.slice(1).split(/\s+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2183
|
+
if (argv.length === 0) {
|
|
2184
|
+
setNotice("empty /command");
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
setOutput(null);
|
|
2188
|
+
setRunning(true);
|
|
2189
|
+
setPending(argv);
|
|
2190
|
+
}
|
|
2191
|
+
if (help) {
|
|
2192
|
+
return /* @__PURE__ */ jsx6(Help, {});
|
|
2193
|
+
}
|
|
2194
|
+
const snapshotLines = formatStatusPayload(payload);
|
|
2195
|
+
const paneLines = output?.lines ?? snapshotLines ?? [];
|
|
2196
|
+
const paneTitle = output?.title ?? "snapshot";
|
|
2197
|
+
return /* @__PURE__ */ jsxs5(Box, { flexDirection: "column", children: [
|
|
2198
|
+
/* @__PURE__ */ jsx6(Header, { tagline: "dashboard" }),
|
|
2199
|
+
/* @__PURE__ */ jsx6(StatusBar, { payload, loading }),
|
|
2200
|
+
/* @__PURE__ */ jsx6(OutputPane, { lines: paneLines, scrollOffset, title: paneTitle }),
|
|
2201
|
+
/* @__PURE__ */ jsx6(CommandInput, { buffer, running }),
|
|
2202
|
+
notice !== null ? /* @__PURE__ */ jsx6(Text6, { children: c.dim(notice) }) : null,
|
|
2203
|
+
/* @__PURE__ */ jsx6(Footer, { running })
|
|
2204
|
+
] });
|
|
2205
|
+
}
|
|
2206
|
+
function Help() {
|
|
2207
|
+
return /* @__PURE__ */ jsxs5(Box, { flexDirection: "column", children: [
|
|
2208
|
+
/* @__PURE__ */ jsx6(Header, { tagline: "help" }),
|
|
2209
|
+
/* @__PURE__ */ jsx6(Text6, { children: " " }),
|
|
2210
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.bold("Keybindings") }),
|
|
2211
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim(" /<command> run a Noir sub-command (e.g. /status, /sync, /task next)") }),
|
|
2212
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim(" Enter run the typed /command") }),
|
|
2213
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim(" Esc back: clear input \u2192 dismiss output \u2192 quit") }),
|
|
2214
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim(" q quit (when the input is empty)") }),
|
|
2215
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim(" \u2191 / \u2193 scroll the output pane") }),
|
|
2216
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim(" ? toggle this help") }),
|
|
2217
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim(" Ctrl+C force exit") }),
|
|
2218
|
+
/* @__PURE__ */ jsx6(Text6, { children: " " }),
|
|
2219
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.bold("Commands") }),
|
|
2220
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim(" Dispatched through the same routing as `noir` at the prompt.") }),
|
|
2221
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim(
|
|
2222
|
+
" /status /sync /doctor /context search <q> /task next /memory recall <q>"
|
|
2223
|
+
) }),
|
|
2224
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim(" Commands that need their own interactive prompts (e.g. a /sync with a") }),
|
|
2225
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim(" conflict) are best run directly \u2014 exit first with q.") }),
|
|
2226
|
+
/* @__PURE__ */ jsx6(Text6, { children: " " }),
|
|
2227
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim("press ? / Esc / q to close this help") })
|
|
2228
|
+
] });
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
// src/tui/index.tsx
|
|
2232
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
2233
|
+
function defaultFetchStatus(opts) {
|
|
2234
|
+
return async () => {
|
|
2235
|
+
try {
|
|
2236
|
+
return await gatherStatusPayload(opts);
|
|
2237
|
+
} catch {
|
|
2238
|
+
return null;
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
async function runTui(opts, dispatch) {
|
|
2243
|
+
const deps = {
|
|
2244
|
+
dispatch,
|
|
2245
|
+
fetchStatus: defaultFetchStatus(opts)
|
|
2246
|
+
};
|
|
2247
|
+
const instance = render(/* @__PURE__ */ jsx7(App, { deps }));
|
|
2248
|
+
await instance.waitUntilExit();
|
|
2249
|
+
}
|
|
2250
|
+
export {
|
|
2251
|
+
runTui
|
|
2252
|
+
};
|
|
2253
|
+
//# sourceMappingURL=index.js.map
|