@noir-ai/cli 1.7.4-beta.1 → 1.8.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.d.ts +39 -4
- package/dist/bin.js +316 -28
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-B7OKXQKF.js → chunk-BM3VEJ2J.js} +117 -4
- package/dist/chunk-BM3VEJ2J.js.map +1 -0
- package/dist/{create-RHFPB5Q2.js → create-UO3QTGL7.js} +11 -6
- package/dist/create-UO3QTGL7.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2 -4
- package/dist/{sync-ZGCGL3OU.js → sync-MZETGPOJ.js} +10 -3
- package/dist/sync-MZETGPOJ.js.map +1 -0
- package/dist/tui/{chunk-MGRKJMHG.js → chunk-XFBZ5X3B.js} +117 -4
- package/dist/tui/chunk-XFBZ5X3B.js.map +1 -0
- package/dist/tui/{create-DH2VPDVS.js → create-Z5HEBB7K.js} +11 -6
- package/dist/tui/create-Z5HEBB7K.js.map +1 -0
- package/dist/tui/index.js +1311 -476
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/{sync-DT4JKXZK.js → sync-5M2UEOKW.js} +10 -3
- package/dist/tui/sync-5M2UEOKW.js.map +1 -0
- package/package.json +11 -9
- package/dist/chunk-B7OKXQKF.js.map +0 -1
- package/dist/chunk-KQYVVGOH.js +0 -102
- package/dist/chunk-KQYVVGOH.js.map +0 -1
- package/dist/create-RHFPB5Q2.js.map +0 -1
- package/dist/sync-ZGCGL3OU.js.map +0 -1
- package/dist/tui/chunk-KBM2UXAC.js +0 -101
- package/dist/tui/chunk-KBM2UXAC.js.map +0 -1
- package/dist/tui/chunk-MGRKJMHG.js.map +0 -1
- package/dist/tui/create-DH2VPDVS.js.map +0 -1
- package/dist/tui/sync-DT4JKXZK.js.map +0 -1
package/dist/tui/index.js
CHANGED
|
@@ -4,11 +4,9 @@ import {
|
|
|
4
4
|
} from "./chunk-FQCYGUCG.js";
|
|
5
5
|
import "./chunk-X2CROVY6.js";
|
|
6
6
|
import {
|
|
7
|
+
doctor,
|
|
7
8
|
init
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import {
|
|
10
|
-
doctor
|
|
11
|
-
} from "./chunk-MGRKJMHG.js";
|
|
9
|
+
} from "./chunk-XFBZ5X3B.js";
|
|
12
10
|
import {
|
|
13
11
|
EXIT,
|
|
14
12
|
NoirCliError,
|
|
@@ -32,23 +30,198 @@ import {
|
|
|
32
30
|
} from "./chunk-A2Q74DOV.js";
|
|
33
31
|
|
|
34
32
|
// src/tui/index.tsx
|
|
33
|
+
import { loadProjectInfo as loadProjectInfo8 } from "@noir-ai/core";
|
|
35
34
|
import { render } from "ink";
|
|
36
35
|
|
|
37
|
-
// src/commands/status.ts
|
|
38
|
-
import { loadProjectInfo as loadProjectInfo7, NOIR_VERSION as NOIR_VERSION4 } from "@noir-ai/core";
|
|
39
|
-
|
|
40
|
-
// src/daemon-client.ts
|
|
41
|
-
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
42
|
-
import { loadProjectInfo as loadProjectInfo6, NOIR_VERSION as NOIR_VERSION3 } from "@noir-ai/core";
|
|
43
|
-
import { ensureDaemonRunning as ensureDaemonRunning3, pidAlive as pidAlive2, readDaemonRecord as readDaemonRecord2 } from "@noir-ai/daemon";
|
|
44
|
-
|
|
45
36
|
// src/bin.ts
|
|
46
37
|
import { realpathSync } from "fs";
|
|
47
38
|
import { pathToFileURL } from "url";
|
|
48
39
|
import { SUPPORTED_HOSTS } from "@noir-ai/adapters";
|
|
49
|
-
import { NOIR_VERSION as
|
|
40
|
+
import { NOIR_VERSION as NOIR_VERSION4 } from "@noir-ai/core";
|
|
50
41
|
import { Command, Option } from "commander";
|
|
51
42
|
|
|
43
|
+
// src/daemon-client.ts
|
|
44
|
+
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
45
|
+
import { ContextEngine, createEmbedFn, resolveEmbedderConfig } from "@noir-ai/context";
|
|
46
|
+
import { loadProjectInfo, NOIR_VERSION } from "@noir-ai/core";
|
|
47
|
+
import { ensureDaemonRunning, pidAlive, readDaemonRecord } from "@noir-ai/daemon";
|
|
48
|
+
import { createMemoryEngine } from "@noir-ai/memory";
|
|
49
|
+
import { openStore } from "@noir-ai/store";
|
|
50
|
+
import { WorkflowEngine } from "@noir-ai/workflow";
|
|
51
|
+
var DAEMON_DOWN_HINT = "daemon not reachable \u2014 try `noir daemon start`";
|
|
52
|
+
var PROBE_TIMEOUT_MS = 1500;
|
|
53
|
+
function describeCause(cause) {
|
|
54
|
+
if (cause instanceof Error) return `${cause.name}: ${cause.message}`;
|
|
55
|
+
try {
|
|
56
|
+
return JSON.stringify(cause);
|
|
57
|
+
} catch {
|
|
58
|
+
return String(cause);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function failDaemonDown(opts, cause) {
|
|
62
|
+
if (opts.verbose) {
|
|
63
|
+
process.stderr.write(`noir: daemon transport detail: ${describeCause(cause)}
|
|
64
|
+
`);
|
|
65
|
+
}
|
|
66
|
+
fail(EXIT.DAEMON_DOWN, DAEMON_DOWN_HINT, opts);
|
|
67
|
+
}
|
|
68
|
+
async function probeDaemon(opts = {}) {
|
|
69
|
+
const rec = readDaemonRecord();
|
|
70
|
+
if (!rec) {
|
|
71
|
+
if (opts.verbose) process.stderr.write("noir: daemon probe: no daemon record\n");
|
|
72
|
+
return { running: false };
|
|
73
|
+
}
|
|
74
|
+
if (!pidAlive(rec.pid)) {
|
|
75
|
+
if (opts.verbose) process.stderr.write(`noir: daemon probe: pid ${rec.pid} not alive
|
|
76
|
+
`);
|
|
77
|
+
return { running: false };
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch(`http://127.0.0.1:${rec.port}/health`, {
|
|
81
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
82
|
+
});
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
if (opts.verbose) process.stderr.write(`noir: daemon probe: /health \u2192 HTTP ${res.status}
|
|
85
|
+
`);
|
|
86
|
+
return { running: false };
|
|
87
|
+
}
|
|
88
|
+
const body = await res.json().catch(() => null);
|
|
89
|
+
if (body?.ok !== true) {
|
|
90
|
+
if (opts.verbose) process.stderr.write("noir: daemon probe: /health body not ok\n");
|
|
91
|
+
return { running: false };
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
running: true,
|
|
95
|
+
pid: typeof body.pid === "number" ? body.pid : rec.pid,
|
|
96
|
+
port: rec.port,
|
|
97
|
+
uptimeSec: typeof body.uptimeSec === "number" ? body.uptimeSec : void 0
|
|
98
|
+
};
|
|
99
|
+
} catch (err) {
|
|
100
|
+
if (opts.verbose)
|
|
101
|
+
process.stderr.write(`noir: daemon probe: unreachable (${describeCause(err)})
|
|
102
|
+
`);
|
|
103
|
+
return { running: false };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function resolveDaemon(opts) {
|
|
107
|
+
const project = opts.project ?? loadProjectInfo(process.cwd());
|
|
108
|
+
const idleTimeoutSec = opts.idleTimeoutSec ?? project.config.daemon.idleTimeoutSec;
|
|
109
|
+
try {
|
|
110
|
+
const ensured = await ensureDaemonRunning({ project, idleTimeoutSec });
|
|
111
|
+
return { url: ensured.url, stop: ensured.stop };
|
|
112
|
+
} catch (err) {
|
|
113
|
+
failDaemonDown(opts, err);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function connectClient(client, url, opts) {
|
|
117
|
+
try {
|
|
118
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(url)));
|
|
119
|
+
} catch (err) {
|
|
120
|
+
failDaemonDown(opts, err);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function callToolParse(client, opts, name, args) {
|
|
124
|
+
const result = await client.callTool({ name, arguments: args }).catch((err) => failDaemonDown(opts, err));
|
|
125
|
+
const block = result.content?.[0];
|
|
126
|
+
if (block?.type !== "text" || typeof block.text !== "string") {
|
|
127
|
+
failDaemonDown(opts, new Error(`tool '${name}' returned no text content`));
|
|
128
|
+
}
|
|
129
|
+
const text = block.text;
|
|
130
|
+
try {
|
|
131
|
+
return JSON.parse(text);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
failDaemonDown(opts, err);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function buildCaller(client, opts) {
|
|
137
|
+
return {
|
|
138
|
+
callTool: (name, args = {}) => callToolParse(client, opts, name, args),
|
|
139
|
+
listTools: async () => {
|
|
140
|
+
const res = await client.listTools().catch((err) => failDaemonDown(opts, err));
|
|
141
|
+
const tools = res?.tools;
|
|
142
|
+
if (!Array.isArray(tools)) return [];
|
|
143
|
+
const names = [];
|
|
144
|
+
for (const t of tools) {
|
|
145
|
+
const name = t?.name;
|
|
146
|
+
if (typeof name === "string") names.push(name);
|
|
147
|
+
}
|
|
148
|
+
return names;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function withDaemon(opts, fn) {
|
|
153
|
+
const { url, stop } = await resolveDaemon(opts);
|
|
154
|
+
let client;
|
|
155
|
+
try {
|
|
156
|
+
client = new Client(
|
|
157
|
+
{ name: "noir-cli", version: NOIR_VERSION },
|
|
158
|
+
{ versionNegotiation: { mode: "auto" } }
|
|
159
|
+
);
|
|
160
|
+
const connected = client;
|
|
161
|
+
await connectClient(connected, url, opts);
|
|
162
|
+
return await fn(buildCaller(connected, opts));
|
|
163
|
+
} finally {
|
|
164
|
+
if (client) {
|
|
165
|
+
await client.close().catch(() => {
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
await stop().catch(() => {
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function withRunningDaemon(opts, fn, probe) {
|
|
173
|
+
const p = probe ?? await probeDaemon(opts);
|
|
174
|
+
if (!p.running) return null;
|
|
175
|
+
const url = `http://127.0.0.1:${p.port}/mcp`;
|
|
176
|
+
let client;
|
|
177
|
+
try {
|
|
178
|
+
client = new Client(
|
|
179
|
+
{ name: "noir-cli", version: NOIR_VERSION },
|
|
180
|
+
{ versionNegotiation: { mode: "auto" } }
|
|
181
|
+
);
|
|
182
|
+
const connected = client;
|
|
183
|
+
await connected.connect(new StreamableHTTPClientTransport(new URL(url)));
|
|
184
|
+
return await fn(buildCaller(connected, opts));
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
} finally {
|
|
188
|
+
if (client) {
|
|
189
|
+
await client.close().catch(() => {
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function callDaemonTool(opts, name, args = {}) {
|
|
195
|
+
return withDaemon(opts, (caller) => caller.callTool(name, args));
|
|
196
|
+
}
|
|
197
|
+
async function withInProcessRead(opts, fn) {
|
|
198
|
+
const project = opts.project ?? loadProjectInfo(process.cwd());
|
|
199
|
+
const store = await openStore({ projectId: project.id, root: project.root, readonly: true });
|
|
200
|
+
try {
|
|
201
|
+
const embedderCfg = resolveEmbedderConfig(project.config.context);
|
|
202
|
+
const embed = createEmbedFn(embedderCfg).embed;
|
|
203
|
+
const context = new ContextEngine({
|
|
204
|
+
store,
|
|
205
|
+
root: project.root,
|
|
206
|
+
projectId: project.id,
|
|
207
|
+
embedderCfg,
|
|
208
|
+
storeDegraded: true
|
|
209
|
+
});
|
|
210
|
+
const memory = createMemoryEngine({
|
|
211
|
+
store,
|
|
212
|
+
root: project.root,
|
|
213
|
+
projectId: project.id,
|
|
214
|
+
embed,
|
|
215
|
+
storeDegraded: true
|
|
216
|
+
});
|
|
217
|
+
const workflow = new WorkflowEngine(store, project.root, project.id);
|
|
218
|
+
return await fn({ context, memory, workflow });
|
|
219
|
+
} finally {
|
|
220
|
+
await store.close().catch(() => {
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
52
225
|
// src/commands/context.ts
|
|
53
226
|
function parseLimit(raw, label, opts) {
|
|
54
227
|
if (raw === void 0) return void 0;
|
|
@@ -73,6 +246,39 @@ function toHit(raw) {
|
|
|
73
246
|
}
|
|
74
247
|
async function contextSearch(opts) {
|
|
75
248
|
const limit = parseLimit(opts.limit, "context search", opts);
|
|
249
|
+
let probe;
|
|
250
|
+
try {
|
|
251
|
+
probe = await probeDaemon(opts);
|
|
252
|
+
} catch {
|
|
253
|
+
probe = { running: true };
|
|
254
|
+
}
|
|
255
|
+
if (!probe.running) {
|
|
256
|
+
await withInProcessRead(opts, async (engines) => {
|
|
257
|
+
const result = await engines.context.search(opts.query, {
|
|
258
|
+
...limit === void 0 ? {} : { limit }
|
|
259
|
+
});
|
|
260
|
+
const data2 = {
|
|
261
|
+
query: opts.query,
|
|
262
|
+
hits: result.results.map((h) => ({
|
|
263
|
+
path: h.path,
|
|
264
|
+
score: h.score,
|
|
265
|
+
snippet: h.snippet,
|
|
266
|
+
source: h.source
|
|
267
|
+
})),
|
|
268
|
+
consumedTokens: result.consumedTokens,
|
|
269
|
+
truncated: result.truncated,
|
|
270
|
+
degraded: result.degraded,
|
|
271
|
+
mode: result.mode
|
|
272
|
+
};
|
|
273
|
+
if (opts.json === true) {
|
|
274
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
|
|
275
|
+
`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
renderSearch(data2, opts);
|
|
279
|
+
});
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
76
282
|
const res = await callDaemonTool(opts, "context_search", {
|
|
77
283
|
query: opts.query,
|
|
78
284
|
...limit === void 0 ? {} : { limit }
|
|
@@ -112,13 +318,8 @@ function renderSearch(data, opts) {
|
|
|
112
318
|
);
|
|
113
319
|
}
|
|
114
320
|
async function contextIndex(opts) {
|
|
115
|
-
if (opts.force === true) {
|
|
116
|
-
info(
|
|
117
|
-
"context index: --force is recognized but not yet honored (content-hash is always incremental).",
|
|
118
|
-
opts
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
321
|
const args = opts.paths && opts.paths.length > 0 ? { paths: opts.paths } : {};
|
|
322
|
+
if (opts.force === true) args.force = true;
|
|
122
323
|
const res = await callDaemonTool(opts, "context_index", args);
|
|
123
324
|
if (res.ok !== true) failTool("context index", res, opts);
|
|
124
325
|
const data = {
|
|
@@ -187,12 +388,13 @@ function renderStatus(data, opts) {
|
|
|
187
388
|
}
|
|
188
389
|
|
|
189
390
|
// src/commands/daemon.ts
|
|
190
|
-
import { loadProjectInfo } from "@noir-ai/core";
|
|
391
|
+
import { loadProjectInfo as loadProjectInfo2 } from "@noir-ai/core";
|
|
191
392
|
import {
|
|
192
393
|
clearDaemonRecord,
|
|
193
|
-
ensureDaemonRunning,
|
|
194
|
-
pidAlive,
|
|
195
|
-
readDaemonRecord
|
|
394
|
+
ensureDaemonRunning as ensureDaemonRunning2,
|
|
395
|
+
pidAlive as pidAlive2,
|
|
396
|
+
readDaemonRecord as readDaemonRecord2,
|
|
397
|
+
spawnDetachedDaemon
|
|
196
398
|
} from "@noir-ai/daemon";
|
|
197
399
|
var MODE = "foreground";
|
|
198
400
|
function formatUptime(sec) {
|
|
@@ -201,13 +403,70 @@ function formatUptime(sec) {
|
|
|
201
403
|
if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
202
404
|
return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
|
|
203
405
|
}
|
|
406
|
+
async function isHealthy(port) {
|
|
407
|
+
try {
|
|
408
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`);
|
|
409
|
+
return res.status === 200;
|
|
410
|
+
} catch {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
204
414
|
async function daemonStart(opts) {
|
|
415
|
+
const project = loadProjectInfo2(process.cwd());
|
|
416
|
+
if (opts.detachChild === true) {
|
|
417
|
+
const child = await ensureDaemonRunning2({
|
|
418
|
+
project,
|
|
419
|
+
idleTimeoutSec: project.config.daemon.idleTimeoutSec
|
|
420
|
+
});
|
|
421
|
+
if (child.started) {
|
|
422
|
+
if (opts.json === true) {
|
|
423
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: { mode: "detached" } })}
|
|
424
|
+
`);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
info("noir daemon: detached foreground. Ctrl+C to stop.", opts);
|
|
428
|
+
log(`Noir daemon listening at ${child.url}`, opts);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (opts.json === true) {
|
|
432
|
+
process.stdout.write(
|
|
433
|
+
`${JSON.stringify({ ok: true, data: { mode: "detached", reused: true } })}
|
|
434
|
+
`
|
|
435
|
+
);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
info("Noir daemon already running; detached child exiting.", opts);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
205
441
|
if (opts.detach === true) {
|
|
206
|
-
|
|
442
|
+
const existing = readDaemonRecord2();
|
|
443
|
+
if (existing && pidAlive2(existing.pid) && await isHealthy(existing.port)) {
|
|
444
|
+
if (opts.json === true) {
|
|
445
|
+
process.stdout.write(
|
|
446
|
+
`${JSON.stringify({ ok: true, data: { mode: "detached", reused: true } })}
|
|
447
|
+
`
|
|
448
|
+
);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
info(
|
|
452
|
+
`Noir daemon already running at http://127.0.0.1:${existing.port}/mcp (detach not needed).`,
|
|
453
|
+
opts
|
|
454
|
+
);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const spawned = await spawnDetachedDaemon({ project });
|
|
458
|
+
if (opts.json === true) {
|
|
459
|
+
process.stdout.write(
|
|
460
|
+
`${JSON.stringify({ ok: true, data: { mode: "detached", pid: spawned.pid, port: spawned.port } })}
|
|
461
|
+
`
|
|
462
|
+
);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
info(`Noir daemon started in the background (pid ${spawned.pid}, port ${spawned.port}).`, opts);
|
|
466
|
+
return;
|
|
207
467
|
}
|
|
208
468
|
const ds = spinner("Starting Noir daemon...", opts).start();
|
|
209
|
-
const
|
|
210
|
-
const ensured = await ensureDaemonRunning({
|
|
469
|
+
const ensured = await ensureDaemonRunning2({
|
|
211
470
|
project,
|
|
212
471
|
idleTimeoutSec: project.config.daemon.idleTimeoutSec
|
|
213
472
|
});
|
|
@@ -229,7 +488,7 @@ async function daemonStart(opts) {
|
|
|
229
488
|
);
|
|
230
489
|
return;
|
|
231
490
|
}
|
|
232
|
-
info("noir daemon: foreground mode
|
|
491
|
+
info("noir daemon: foreground mode. Ctrl+C to stop.", opts);
|
|
233
492
|
log(`Noir daemon listening at ${ensured.url}`, opts);
|
|
234
493
|
return;
|
|
235
494
|
}
|
|
@@ -244,7 +503,7 @@ async function daemonStart(opts) {
|
|
|
244
503
|
log(`Noir daemon already running at ${ensured.url}`, opts);
|
|
245
504
|
}
|
|
246
505
|
async function daemonStop(opts) {
|
|
247
|
-
const rec =
|
|
506
|
+
const rec = readDaemonRecord2();
|
|
248
507
|
if (!rec) {
|
|
249
508
|
if (opts.json === true) {
|
|
250
509
|
process.stdout.write(
|
|
@@ -290,11 +549,11 @@ async function daemonStop(opts) {
|
|
|
290
549
|
}
|
|
291
550
|
}
|
|
292
551
|
async function daemonStatus(opts) {
|
|
293
|
-
const rec =
|
|
552
|
+
const rec = readDaemonRecord2();
|
|
294
553
|
if (!rec) {
|
|
295
554
|
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (start with `noir daemon start`).", opts);
|
|
296
555
|
}
|
|
297
|
-
if (!
|
|
556
|
+
if (!pidAlive2(rec.pid)) {
|
|
298
557
|
clearDaemonRecord();
|
|
299
558
|
fail(EXIT.DAEMON_DOWN, "Noir daemon is not running (stale record removed).", opts);
|
|
300
559
|
}
|
|
@@ -325,7 +584,9 @@ async function daemonStatus(opts) {
|
|
|
325
584
|
port: rec.port,
|
|
326
585
|
startedAt: rec.startedAt,
|
|
327
586
|
uptimeSec,
|
|
328
|
-
mode:
|
|
587
|
+
// Honest ownership: the detached child writes `mode:'detached'`; anything
|
|
588
|
+
// else (legacy record, foreground start) reports 'foreground'.
|
|
589
|
+
mode: rec.mode ?? MODE
|
|
329
590
|
};
|
|
330
591
|
if (opts.json === true) {
|
|
331
592
|
process.stdout.write(`${JSON.stringify({ ok: true, data })}
|
|
@@ -350,40 +611,202 @@ import {
|
|
|
350
611
|
hostLaunchDirective,
|
|
351
612
|
resolveAdapter
|
|
352
613
|
} from "@noir-ai/adapters";
|
|
353
|
-
import { loadProjectInfo as
|
|
614
|
+
import { loadProjectInfo as loadProjectInfo4, NOIR_DIR } from "@noir-ai/core";
|
|
354
615
|
|
|
355
|
-
// src/commands/
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
document: "noir-document"
|
|
364
|
-
};
|
|
365
|
-
function skillFor(phase) {
|
|
366
|
-
if (typeof phase !== "string") return null;
|
|
367
|
-
return PHASE_SKILL[phase] ?? null;
|
|
616
|
+
// src/commands/status.ts
|
|
617
|
+
import { loadProjectInfo as loadProjectInfo3, NOIR_VERSION as NOIR_VERSION2 } from "@noir-ai/core";
|
|
618
|
+
async function tryTool(caller, name, args) {
|
|
619
|
+
try {
|
|
620
|
+
return await caller.callTool(name, args);
|
|
621
|
+
} catch {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
368
624
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const detail = res.error ?? "task not found";
|
|
378
|
-
fail(EXIT.NOT_FOUND, `task: ${detail}`, opts);
|
|
625
|
+
function pickStore(s) {
|
|
626
|
+
if (s?.ok !== true) return null;
|
|
627
|
+
return {
|
|
628
|
+
docCount: s.docCount,
|
|
629
|
+
vecCount: s.vecCount,
|
|
630
|
+
dbPath: s.dbPath,
|
|
631
|
+
degraded: s.degraded === true
|
|
632
|
+
};
|
|
379
633
|
}
|
|
380
|
-
function
|
|
381
|
-
if (
|
|
382
|
-
|
|
634
|
+
function describeEmbedder2(e) {
|
|
635
|
+
if (!e || e.kind === "none") return "none (BM25-only)";
|
|
636
|
+
const model = typeof e.model === "string" && e.model.length > 0 ? e.model : "<unset>";
|
|
637
|
+
return `${e.kind} \xB7 ${model} (${e.dim}-dim)`;
|
|
383
638
|
}
|
|
384
|
-
function
|
|
385
|
-
|
|
386
|
-
|
|
639
|
+
function pickContext(c2) {
|
|
640
|
+
if (c2?.ok !== true) return null;
|
|
641
|
+
return {
|
|
642
|
+
docCount: c2.docCount,
|
|
643
|
+
vecCount: c2.vecCount,
|
|
644
|
+
indexedFiles: c2.indexedFiles,
|
|
645
|
+
embedder: describeEmbedder2(c2.embedder),
|
|
646
|
+
degraded: c2.degraded === true
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function pickWorkflow(w) {
|
|
650
|
+
if (w?.ok !== true) return null;
|
|
651
|
+
return {
|
|
652
|
+
taskId: w.taskId,
|
|
653
|
+
phase: w.phase,
|
|
654
|
+
state: w.state,
|
|
655
|
+
mode: w.mode,
|
|
656
|
+
nextGate: w.nextGate,
|
|
657
|
+
degraded: w.degraded === true
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function pickMemory(m) {
|
|
661
|
+
if (m?.ok !== true || !Array.isArray(m.sessions)) return null;
|
|
662
|
+
let observations = 0;
|
|
663
|
+
for (const s of m.sessions) observations += s.count;
|
|
664
|
+
return { sessions: m.sessions.length, observations };
|
|
665
|
+
}
|
|
666
|
+
function buildPayload(project, probe, host, store, context, workflow, memory) {
|
|
667
|
+
return {
|
|
668
|
+
noir: NOIR_VERSION2,
|
|
669
|
+
project: { id: project.id, name: project.name },
|
|
670
|
+
host: project.config.host,
|
|
671
|
+
daemon: {
|
|
672
|
+
running: probe.running,
|
|
673
|
+
...probe.pid !== void 0 ? { pid: probe.pid } : {},
|
|
674
|
+
...probe.uptimeSec !== void 0 ? { uptimeSec: probe.uptimeSec } : {},
|
|
675
|
+
...host && typeof host.transport === "string" ? { transport: host.transport } : {}
|
|
676
|
+
},
|
|
677
|
+
store: pickStore(store),
|
|
678
|
+
context: pickContext(context),
|
|
679
|
+
workflow: pickWorkflow(workflow),
|
|
680
|
+
memory: pickMemory(memory)
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function formatDuration(sec) {
|
|
684
|
+
if (typeof sec !== "number" || sec < 0) return "unknown";
|
|
685
|
+
if (sec < 60) return `${sec}s`;
|
|
686
|
+
if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
687
|
+
return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
|
|
688
|
+
}
|
|
689
|
+
function describeDaemon(d) {
|
|
690
|
+
if (!d.running) return "not running (start with `noir daemon start`)";
|
|
691
|
+
const pid = typeof d.pid === "number" ? `pid ${d.pid}` : "pid unknown";
|
|
692
|
+
const up = typeof d.uptimeSec === "number" ? `, up ${formatDuration(d.uptimeSec)}` : "";
|
|
693
|
+
const transport = typeof d.transport === "string" ? `, ${d.transport}` : "";
|
|
694
|
+
return `running (${pid}${up}${transport})`;
|
|
695
|
+
}
|
|
696
|
+
function describeStore(s) {
|
|
697
|
+
if (!s) return "unavailable (store engine not wired)";
|
|
698
|
+
const flag = s.degraded ? ` ${badge("warn", "degraded: read-only")}` : "";
|
|
699
|
+
return `${s.docCount} docs / ${s.vecCount} vecs${flag}`;
|
|
700
|
+
}
|
|
701
|
+
function describeContext(c2) {
|
|
702
|
+
if (!c2) return "unavailable (context engine not wired)";
|
|
703
|
+
const flag = c2.degraded ? ` ${badge("warn", "degraded")}` : "";
|
|
704
|
+
return `${c2.docCount} docs, ${c2.vecCount} vecs, ${c2.indexedFiles} files \xB7 embedder ${c2.embedder}${flag}`;
|
|
705
|
+
}
|
|
706
|
+
function describeWorkflow(w) {
|
|
707
|
+
if (!w) return "no active task";
|
|
708
|
+
const gate = w.nextGate ? ` \u2192 next gate: ${w.nextGate}` : "";
|
|
709
|
+
const flag = w.degraded ? ` ${badge("warn", "degraded")}` : "";
|
|
710
|
+
return `${w.phase} (${w.state}, ${w.mode}) \xB7 task ${w.taskId}${gate}${flag}`;
|
|
711
|
+
}
|
|
712
|
+
function describeMemory(m) {
|
|
713
|
+
if (!m) return "unavailable (memory engine not wired)";
|
|
714
|
+
return `${m.observations} observations across ${m.sessions} session${m.sessions === 1 ? "" : "s"}`;
|
|
715
|
+
}
|
|
716
|
+
function renderHuman(p, opts) {
|
|
717
|
+
log(`noir status \u2014 ${p.project.name} (${p.project.id})`, opts);
|
|
718
|
+
definitionList(
|
|
719
|
+
[
|
|
720
|
+
{ label: "Project", value: `${p.project.name} (${p.project.id})` },
|
|
721
|
+
{ label: "Host", value: p.host },
|
|
722
|
+
{ label: "Noir", value: p.noir },
|
|
723
|
+
{ label: "Daemon", value: describeDaemon(p.daemon) },
|
|
724
|
+
{ label: "Store", value: describeStore(p.store) },
|
|
725
|
+
{ label: "Context", value: describeContext(p.context) },
|
|
726
|
+
{ label: "Workflow", value: describeWorkflow(p.workflow) },
|
|
727
|
+
{ label: "Memory", value: describeMemory(p.memory) }
|
|
728
|
+
],
|
|
729
|
+
opts
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
async function gatherStatusPayload(opts) {
|
|
733
|
+
const project = loadProjectInfo3(process.cwd());
|
|
734
|
+
const probe = await probeDaemon(opts);
|
|
735
|
+
let host = null;
|
|
736
|
+
let store = null;
|
|
737
|
+
let context = null;
|
|
738
|
+
let workflow = null;
|
|
739
|
+
let memory = null;
|
|
740
|
+
if (probe.running) {
|
|
741
|
+
await withRunningDaemon(
|
|
742
|
+
opts,
|
|
743
|
+
async (caller) => {
|
|
744
|
+
host = await tryTool(caller, "host_status");
|
|
745
|
+
store = await tryTool(caller, "store_status");
|
|
746
|
+
context = await tryTool(caller, "context_status");
|
|
747
|
+
workflow = await tryTool(caller, "workflow_status");
|
|
748
|
+
memory = await tryTool(caller, "memory_sessions");
|
|
749
|
+
},
|
|
750
|
+
probe
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
return buildPayload(project, probe, host, store, context, workflow, memory);
|
|
754
|
+
}
|
|
755
|
+
async function status(opts) {
|
|
756
|
+
void runAsyncUpdateCheck({
|
|
757
|
+
env: process.env,
|
|
758
|
+
configUpdate: (() => {
|
|
759
|
+
try {
|
|
760
|
+
const info2 = loadProjectInfo3(process.cwd());
|
|
761
|
+
return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
|
|
762
|
+
} catch {
|
|
763
|
+
return DEFAULT_UPDATE_CONFIG;
|
|
764
|
+
}
|
|
765
|
+
})(),
|
|
766
|
+
quiet: opts.json === true || opts.quiet === true || !process.stdout.isTTY
|
|
767
|
+
});
|
|
768
|
+
const payload = await gatherStatusPayload(opts);
|
|
769
|
+
if (opts.json === true) {
|
|
770
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
|
|
771
|
+
`);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
renderHuman(payload, opts);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/commands/task.ts
|
|
778
|
+
import { PHASES } from "@noir-ai/workflow";
|
|
779
|
+
var PHASE_SKILL = {
|
|
780
|
+
intake: "noir-intake",
|
|
781
|
+
clarify: "noir-clarify",
|
|
782
|
+
spec: "noir-spec",
|
|
783
|
+
plan: "noir-plan",
|
|
784
|
+
execute: "noir-execute",
|
|
785
|
+
verify: "noir-verify",
|
|
786
|
+
document: "noir-document"
|
|
787
|
+
};
|
|
788
|
+
function skillFor(phase) {
|
|
789
|
+
if (typeof phase !== "string") return null;
|
|
790
|
+
return PHASE_SKILL[phase] ?? null;
|
|
791
|
+
}
|
|
792
|
+
async function fetchStatus(opts, taskId) {
|
|
793
|
+
const args = typeof taskId === "string" && taskId.length > 0 ? { taskId } : {};
|
|
794
|
+
const res = await callDaemonTool(
|
|
795
|
+
opts,
|
|
796
|
+
"workflow_status",
|
|
797
|
+
args
|
|
798
|
+
);
|
|
799
|
+
if (res.ok === true) return res;
|
|
800
|
+
const detail = res.error ?? "task not found";
|
|
801
|
+
fail(EXIT.NOT_FOUND, `task: ${detail}`, opts);
|
|
802
|
+
}
|
|
803
|
+
function formatStamp(ms) {
|
|
804
|
+
if (typeof ms !== "number" || ms <= 0) return "-";
|
|
805
|
+
return new Date(ms).toISOString().replace("T", " ").replace(/\..*$/, "Z");
|
|
806
|
+
}
|
|
807
|
+
function renderStatusRow(s, opts) {
|
|
808
|
+
const flag = s.degraded === true ? ` ${badge("warn", "degraded")}` : "";
|
|
809
|
+
log(`task status \u2014 ${s.taskId}${flag}`, opts);
|
|
387
810
|
definitionList(
|
|
388
811
|
[
|
|
389
812
|
{ label: "Task", value: s.taskId },
|
|
@@ -396,7 +819,49 @@ function renderStatusRow(s, opts) {
|
|
|
396
819
|
opts
|
|
397
820
|
);
|
|
398
821
|
}
|
|
822
|
+
var GATE_PHASES = ["spec", "plan", "verify"];
|
|
823
|
+
function nextGateAfter(phase) {
|
|
824
|
+
const cur = PHASES.indexOf(phase);
|
|
825
|
+
for (const p of GATE_PHASES) {
|
|
826
|
+
if (PHASES.indexOf(p) > cur) return p;
|
|
827
|
+
}
|
|
828
|
+
return null;
|
|
829
|
+
}
|
|
399
830
|
async function taskStatus(opts) {
|
|
831
|
+
let probe;
|
|
832
|
+
try {
|
|
833
|
+
probe = await probeDaemon(opts);
|
|
834
|
+
} catch {
|
|
835
|
+
probe = { running: true };
|
|
836
|
+
}
|
|
837
|
+
if (!probe.running) {
|
|
838
|
+
const s2 = await withInProcessRead(opts, async (engines) => {
|
|
839
|
+
const id = typeof opts.id === "string" && opts.id.length > 0 ? opts.id : null;
|
|
840
|
+
const taskId = id ?? engines.workflow.activeTaskId();
|
|
841
|
+
if (!taskId) fail(EXIT.NOT_FOUND, "task: no active task", opts);
|
|
842
|
+
const task = engines.workflow.status(taskId);
|
|
843
|
+
if (!task) fail(EXIT.NOT_FOUND, `task: unknown task '${taskId}'`, opts);
|
|
844
|
+
const stopped = task.state === "blocked" || task.state === "abandoned";
|
|
845
|
+
return {
|
|
846
|
+
ok: true,
|
|
847
|
+
taskId: task.taskId,
|
|
848
|
+
phase: task.phase,
|
|
849
|
+
state: task.state,
|
|
850
|
+
mode: task.mode,
|
|
851
|
+
nextGate: stopped ? null : nextGateAfter(task.phase),
|
|
852
|
+
history: task.history,
|
|
853
|
+
updatedAt: task.updatedAt,
|
|
854
|
+
degraded: true
|
|
855
|
+
};
|
|
856
|
+
});
|
|
857
|
+
if (opts.json === true) {
|
|
858
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: s2 })}
|
|
859
|
+
`);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
renderStatusRow(s2, opts);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
400
865
|
const s = await fetchStatus(opts, opts.id);
|
|
401
866
|
if (opts.json === true) {
|
|
402
867
|
process.stdout.write(`${JSON.stringify({ ok: true, data: s })}
|
|
@@ -634,7 +1099,7 @@ async function probeOnly(opts) {
|
|
|
634
1099
|
return probeDaemon(opts);
|
|
635
1100
|
}
|
|
636
1101
|
async function handoff(opts) {
|
|
637
|
-
const project =
|
|
1102
|
+
const project = loadProjectInfo4(process.cwd());
|
|
638
1103
|
const host = project.config.host;
|
|
639
1104
|
const snapshot = await gatherStatusPayload(opts);
|
|
640
1105
|
const probe = await probeOnly(opts);
|
|
@@ -679,8 +1144,8 @@ async function handoff(opts) {
|
|
|
679
1144
|
// src/commands/home.ts
|
|
680
1145
|
import { hostLaunchDirective as hostLaunchDirective2 } from "@noir-ai/adapters";
|
|
681
1146
|
import {
|
|
682
|
-
loadProjectInfo as
|
|
683
|
-
NOIR_VERSION,
|
|
1147
|
+
loadProjectInfo as loadProjectInfo5,
|
|
1148
|
+
NOIR_VERSION as NOIR_VERSION3,
|
|
684
1149
|
readInstallRecord
|
|
685
1150
|
} from "@noir-ai/core";
|
|
686
1151
|
|
|
@@ -719,7 +1184,7 @@ function shouldShowBanner(opts) {
|
|
|
719
1184
|
// src/commands/home.ts
|
|
720
1185
|
function tryProject() {
|
|
721
1186
|
try {
|
|
722
|
-
const info2 =
|
|
1187
|
+
const info2 = loadProjectInfo5(process.cwd());
|
|
723
1188
|
return { id: info2.id, name: info2.name, host: info2.config.host };
|
|
724
1189
|
} catch {
|
|
725
1190
|
return null;
|
|
@@ -749,7 +1214,7 @@ async function runMenu(opts, deps) {
|
|
|
749
1214
|
env: process.env,
|
|
750
1215
|
configUpdate: (() => {
|
|
751
1216
|
try {
|
|
752
|
-
const info2 =
|
|
1217
|
+
const info2 = loadProjectInfo5(process.cwd());
|
|
753
1218
|
return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
|
|
754
1219
|
} catch {
|
|
755
1220
|
return DEFAULT_UPDATE_CONFIG;
|
|
@@ -771,7 +1236,7 @@ ${COMMANDS_HINT}
|
|
|
771
1236
|
);
|
|
772
1237
|
}
|
|
773
1238
|
const rec = readInstallRecord();
|
|
774
|
-
if (rec && shouldShowMigrationBanner(rec,
|
|
1239
|
+
if (rec && shouldShowMigrationBanner(rec, NOIR_VERSION3)) {
|
|
775
1240
|
process.stderr.write(
|
|
776
1241
|
`
|
|
777
1242
|
${c.warn(`noir installed via ${rec.method}`)} \u2014 consider \`noir install\` for the native path (auto-update, no npm prefix/PATH issues). Dismiss with: \`noir install --dismiss\` (persisted per version).
|
|
@@ -877,6 +1342,27 @@ function stamp(ts) {
|
|
|
877
1342
|
}
|
|
878
1343
|
async function memoryRecall(opts) {
|
|
879
1344
|
const limit = parseLimit2(opts.limit, "memory recall", opts);
|
|
1345
|
+
let probe;
|
|
1346
|
+
try {
|
|
1347
|
+
probe = await probeDaemon(opts);
|
|
1348
|
+
} catch {
|
|
1349
|
+
probe = { running: true };
|
|
1350
|
+
}
|
|
1351
|
+
if (!probe.running) {
|
|
1352
|
+
await withInProcessRead(opts, async (engines) => {
|
|
1353
|
+
const result = await engines.memory.recall(opts.query, {
|
|
1354
|
+
...limit === void 0 ? {} : { limit }
|
|
1355
|
+
});
|
|
1356
|
+
const data2 = { query: opts.query, hits: result, degraded: false };
|
|
1357
|
+
if (opts.json === true) {
|
|
1358
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
|
|
1359
|
+
`);
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
renderRecall(data2.query, result, data2.degraded, opts);
|
|
1363
|
+
});
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
880
1366
|
const res = await callDaemonTool(opts, "memory_recall", {
|
|
881
1367
|
query: opts.query,
|
|
882
1368
|
...limit === void 0 ? {} : { limit }
|
|
@@ -962,6 +1448,30 @@ function renderObservation(obs, opts) {
|
|
|
962
1448
|
${content}`, opts);
|
|
963
1449
|
}
|
|
964
1450
|
async function memorySessions(opts) {
|
|
1451
|
+
let probe;
|
|
1452
|
+
try {
|
|
1453
|
+
probe = await probeDaemon(opts);
|
|
1454
|
+
} catch {
|
|
1455
|
+
probe = { running: true };
|
|
1456
|
+
}
|
|
1457
|
+
if (!probe.running) {
|
|
1458
|
+
await withInProcessRead(opts, async (engines) => {
|
|
1459
|
+
const raw = engines.memory.sessions();
|
|
1460
|
+
const sessions2 = raw.map((s) => ({
|
|
1461
|
+
id: s.id,
|
|
1462
|
+
count: s.count,
|
|
1463
|
+
lastTs: s.lastTs
|
|
1464
|
+
}));
|
|
1465
|
+
const data2 = { sessions: sessions2 };
|
|
1466
|
+
if (opts.json === true) {
|
|
1467
|
+
process.stdout.write(`${JSON.stringify({ ok: true, data: data2 })}
|
|
1468
|
+
`);
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
renderSessions(sessions2, opts);
|
|
1472
|
+
});
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
965
1475
|
const res = await callDaemonTool(opts, "memory_sessions");
|
|
966
1476
|
if (res.ok !== true) failTool2("memory sessions", res, opts);
|
|
967
1477
|
const sessions = Array.isArray(res.sessions) ? res.sessions.map((raw) => {
|
|
@@ -978,6 +1488,9 @@ async function memorySessions(opts) {
|
|
|
978
1488
|
`);
|
|
979
1489
|
return;
|
|
980
1490
|
}
|
|
1491
|
+
renderSessions(sessions, opts);
|
|
1492
|
+
}
|
|
1493
|
+
function renderSessions(sessions, opts) {
|
|
981
1494
|
log(`memory sessions \u2014 ${sessions.length} session${sessions.length === 1 ? "" : "s"}`, opts);
|
|
982
1495
|
table(
|
|
983
1496
|
sessions.map((s) => ({ Session: s.id, Observations: s.count, "Last seen": stamp(s.lastTs) })),
|
|
@@ -1053,7 +1566,7 @@ async function memoryConsolidate(opts) {
|
|
|
1053
1566
|
|
|
1054
1567
|
// src/commands/skills.ts
|
|
1055
1568
|
import { claudeAdapter } from "@noir-ai/adapters";
|
|
1056
|
-
import { loadProjectInfo as
|
|
1569
|
+
import { loadProjectInfo as loadProjectInfo6 } from "@noir-ai/core";
|
|
1057
1570
|
import {
|
|
1058
1571
|
discoverAll,
|
|
1059
1572
|
emitSkillsToDir
|
|
@@ -1147,7 +1660,7 @@ async function skillsList(opts) {
|
|
|
1147
1660
|
}
|
|
1148
1661
|
async function skillsSync(opts) {
|
|
1149
1662
|
const root = process.cwd();
|
|
1150
|
-
const project =
|
|
1663
|
+
const project = loadProjectInfo6(root);
|
|
1151
1664
|
if (!claudeAdapter.skillsDir) {
|
|
1152
1665
|
const data2 = {
|
|
1153
1666
|
emitted: [],
|
|
@@ -1190,16 +1703,16 @@ async function skillsSync(opts) {
|
|
|
1190
1703
|
}
|
|
1191
1704
|
|
|
1192
1705
|
// src/serve.ts
|
|
1193
|
-
import { loadProjectInfo as
|
|
1194
|
-
import { ensureDaemonRunning as
|
|
1706
|
+
import { loadProjectInfo as loadProjectInfo7 } from "@noir-ai/core";
|
|
1707
|
+
import { ensureDaemonRunning as ensureDaemonRunning3, startStdioServer } from "@noir-ai/daemon";
|
|
1195
1708
|
async function serve(opts) {
|
|
1196
|
-
const project =
|
|
1709
|
+
const project = loadProjectInfo7(process.cwd());
|
|
1197
1710
|
if (opts.stdio) {
|
|
1198
1711
|
await startStdioServer({ project, transport: "stdio", daemon: false });
|
|
1199
1712
|
return;
|
|
1200
1713
|
}
|
|
1201
1714
|
try {
|
|
1202
|
-
const { url } = await
|
|
1715
|
+
const { url } = await ensureDaemonRunning3({
|
|
1203
1716
|
project,
|
|
1204
1717
|
idleTimeoutSec: project.config.daemon.idleTimeoutSec
|
|
1205
1718
|
});
|
|
@@ -1216,7 +1729,70 @@ async function serve(opts) {
|
|
|
1216
1729
|
}
|
|
1217
1730
|
}
|
|
1218
1731
|
|
|
1732
|
+
// src/tui/commands/registry.ts
|
|
1733
|
+
var DESTRUCTIVE_PREFIXES = [
|
|
1734
|
+
["context", "index"],
|
|
1735
|
+
["context", "forget"],
|
|
1736
|
+
["memory", "save"],
|
|
1737
|
+
["memory", "forget"],
|
|
1738
|
+
["memory", "consolidate"],
|
|
1739
|
+
["task", "new"],
|
|
1740
|
+
["task", "advance"],
|
|
1741
|
+
["skills", "sync"],
|
|
1742
|
+
["daemon", "start"],
|
|
1743
|
+
["daemon", "stop"],
|
|
1744
|
+
["daemon", "restart"],
|
|
1745
|
+
["init"],
|
|
1746
|
+
["create"],
|
|
1747
|
+
["sync"],
|
|
1748
|
+
["install"],
|
|
1749
|
+
["update"]
|
|
1750
|
+
];
|
|
1751
|
+
function isDestructive(argv) {
|
|
1752
|
+
return DESTRUCTIVE_PREFIXES.some((prefix) => {
|
|
1753
|
+
if (argv.length < prefix.length) return false;
|
|
1754
|
+
return prefix.every((tok, i) => argv[i] === tok);
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
function capitalizeFirst(s) {
|
|
1758
|
+
if (s.length === 0) return s;
|
|
1759
|
+
return `${s[0]?.toUpperCase() ?? ""}${s.slice(1)}`;
|
|
1760
|
+
}
|
|
1761
|
+
function hasAction(cmd) {
|
|
1762
|
+
return cmd._actionHandler != null;
|
|
1763
|
+
}
|
|
1764
|
+
function collectLeaves(cmd, path, out) {
|
|
1765
|
+
const children = cmd.commands;
|
|
1766
|
+
const isLeaf = hasAction(cmd) && children.length === 0;
|
|
1767
|
+
if (isLeaf) {
|
|
1768
|
+
const argv = path;
|
|
1769
|
+
const id = argv.join(" ");
|
|
1770
|
+
const description = cmd.description() || argv.join(" ");
|
|
1771
|
+
out.push({
|
|
1772
|
+
id,
|
|
1773
|
+
label: argv.map(capitalizeFirst).join(": "),
|
|
1774
|
+
argv,
|
|
1775
|
+
description,
|
|
1776
|
+
category: argv[0] ?? "",
|
|
1777
|
+
keywords: argv,
|
|
1778
|
+
destructive: isDestructive(argv)
|
|
1779
|
+
});
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
for (const child of children) {
|
|
1783
|
+
collectLeaves(child, [...path, child.name()], out);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
function buildPaletteCommands(program2) {
|
|
1787
|
+
const out = [];
|
|
1788
|
+
collectLeaves(program2, [], out);
|
|
1789
|
+
return out;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1219
1792
|
// src/bin.ts
|
|
1793
|
+
function buildPaletteCommandsForTui() {
|
|
1794
|
+
return buildPaletteCommands(createProgram());
|
|
1795
|
+
}
|
|
1220
1796
|
function actionGlobals(args) {
|
|
1221
1797
|
const last = args[args.length - 1];
|
|
1222
1798
|
return last?.optsWithGlobals() ?? {};
|
|
@@ -1283,7 +1859,7 @@ function emitDeprecationHintsFor(cmd, opts) {
|
|
|
1283
1859
|
}
|
|
1284
1860
|
function createProgram() {
|
|
1285
1861
|
const program2 = new Command();
|
|
1286
|
-
program2.name("noir").description("Noir \u2014 discipline, context, and memory layer for agentic CLIs.").version(
|
|
1862
|
+
program2.name("noir").description("Noir \u2014 discipline, context, and memory layer for agentic CLIs.").version(NOIR_VERSION4, "-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(
|
|
1287
1863
|
new Option("--tui", "prefer the interactive home menu for bare `noir` (advisory; TTY-only)")
|
|
1288
1864
|
).addOption(
|
|
1289
1865
|
new Option(
|
|
@@ -1321,7 +1897,7 @@ function createProgram() {
|
|
|
1321
1897
|
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(
|
|
1322
1898
|
"--upgrade",
|
|
1323
1899
|
"run scaffold migrations before re-emitting (re-run on an existing project)"
|
|
1324
|
-
).option("--force", "re-scaffold even if already initialized (bypasses the already-init no-op)").addOption(
|
|
1900
|
+
).option("--force", "re-scaffold even if already initialized (bypasses the already-init no-op)").option("--dry-run", "report planned writes without writing anything").option("--preview", "alias for --dry-run").addOption(
|
|
1325
1901
|
// S10: target host. Defaults to `'claude'` (the regression anchor). The
|
|
1326
1902
|
// chosen host is forwarded to scaffold() + skills emission via
|
|
1327
1903
|
// resolveAdapter. The choice list mirrors `SUPPORTED_HOSTS` so a new host
|
|
@@ -1336,19 +1912,21 @@ function createProgram() {
|
|
|
1336
1912
|
const upgrade = opts.upgrade === true;
|
|
1337
1913
|
const force = opts.force === true;
|
|
1338
1914
|
const host = parseHost(opts.host);
|
|
1915
|
+
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
1339
1916
|
const result = await init(process.cwd(), {
|
|
1340
1917
|
transport,
|
|
1341
1918
|
url: opts.url,
|
|
1342
1919
|
...upgrade ? { upgrade } : {},
|
|
1343
1920
|
...force ? { force } : {},
|
|
1344
|
-
...host !== void 0 ? { host } : {}
|
|
1921
|
+
...host !== void 0 ? { host } : {},
|
|
1922
|
+
...dryRun ? { dryRun } : {}
|
|
1345
1923
|
});
|
|
1346
1924
|
if (cmd.optsWithGlobals().json === true && result !== void 0) {
|
|
1347
1925
|
json({ ok: true, data: result });
|
|
1348
1926
|
}
|
|
1349
1927
|
}
|
|
1350
1928
|
);
|
|
1351
|
-
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(
|
|
1929
|
+
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)").option("--dry-run", "report planned writes without writing anything").option("--preview", "alias for --dry-run").addOption(
|
|
1352
1930
|
new Option(
|
|
1353
1931
|
"--host <id>",
|
|
1354
1932
|
"target agentic CLI (default: claude) \u2014 drives host-side emission"
|
|
@@ -1358,12 +1936,14 @@ function createProgram() {
|
|
|
1358
1936
|
const transport = opts.transport === "streamable-http" ? "streamable-http" : "stdio";
|
|
1359
1937
|
const force = opts.force === true;
|
|
1360
1938
|
const host = parseHost(opts.host);
|
|
1361
|
-
const
|
|
1939
|
+
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
1940
|
+
const { create } = await import("./create-Z5HEBB7K.js");
|
|
1362
1941
|
const result = await create(dir, {
|
|
1363
1942
|
transport,
|
|
1364
1943
|
url: opts.url,
|
|
1365
1944
|
...force ? { force } : {},
|
|
1366
|
-
...host !== void 0 ? { host } : {}
|
|
1945
|
+
...host !== void 0 ? { host } : {},
|
|
1946
|
+
...dryRun ? { dryRun } : {}
|
|
1367
1947
|
});
|
|
1368
1948
|
if (cmd.optsWithGlobals().json === true && result !== void 0) {
|
|
1369
1949
|
json({ ok: true, data: result });
|
|
@@ -1378,7 +1958,7 @@ function createProgram() {
|
|
|
1378
1958
|
).option(
|
|
1379
1959
|
"--merge",
|
|
1380
1960
|
"three-way merge managed regions (default since 1.3.0; flag kept for compatibility)"
|
|
1381
|
-
).addOption(
|
|
1961
|
+
).option("--dry-run", "report planned writes without writing anything").option("--preview", "alias for --dry-run").addOption(
|
|
1382
1962
|
// Opt OUT of managed-region merge (restore strip-replace). Commander
|
|
1383
1963
|
// stores `--no-merge-regions` under `mergeRegions` (default true; flag →
|
|
1384
1964
|
// false). Bare `noir sync` keeps the merge default (true).
|
|
@@ -1396,16 +1976,18 @@ function createProgram() {
|
|
|
1396
1976
|
).choices(SUPPORTED_HOSTS)
|
|
1397
1977
|
).action(
|
|
1398
1978
|
async (opts, cmd) => {
|
|
1399
|
-
const { sync } = await import("./sync-
|
|
1979
|
+
const { sync } = await import("./sync-5M2UEOKW.js");
|
|
1400
1980
|
const host = parseHost(opts.host);
|
|
1401
1981
|
const force = opts.force === true;
|
|
1402
1982
|
const merge = opts.merge === true;
|
|
1403
1983
|
const noMergeRegions = opts.mergeRegions === false;
|
|
1404
|
-
const
|
|
1984
|
+
const dryRun = opts.dryRun === true || opts.preview === true;
|
|
1985
|
+
const result = host === void 0 && !force && !merge && !noMergeRegions && !dryRun ? await sync(process.cwd()) : await sync(process.cwd(), {
|
|
1405
1986
|
...host !== void 0 ? { host } : {},
|
|
1406
1987
|
...force ? { force } : {},
|
|
1407
1988
|
...merge ? { merge } : {},
|
|
1408
|
-
...noMergeRegions ? { mergeManagedRegions: false } : {}
|
|
1989
|
+
...noMergeRegions ? { mergeManagedRegions: false } : {},
|
|
1990
|
+
...dryRun ? { dryRun } : {}
|
|
1409
1991
|
});
|
|
1410
1992
|
if (cmd.optsWithGlobals().json === true) {
|
|
1411
1993
|
json({ ok: true, data: result });
|
|
@@ -1420,11 +2002,18 @@ function createProgram() {
|
|
|
1420
2002
|
throw new NoirCliError(EXIT.USAGE, "Usage: noir mcp serve [--stdio]");
|
|
1421
2003
|
});
|
|
1422
2004
|
const daemonGrp = program2.command("daemon").description("control the Noir daemon");
|
|
1423
|
-
daemonGrp.command("start").description("start the Noir daemon (foreground
|
|
2005
|
+
daemonGrp.command("start").description("start the Noir daemon (foreground, or background with --detach)").option("--detach", "run the daemon in the background and exit").addOption(
|
|
2006
|
+
new Option("--_detached-child", "reserved: detached daemon child (internal)").hideHelp()
|
|
2007
|
+
).action(async (...args) => {
|
|
1424
2008
|
const cmd = trailingCmd(args);
|
|
1425
2009
|
const g = cmd.optsWithGlobals();
|
|
1426
2010
|
const detach = g.detach === true;
|
|
1427
|
-
|
|
2011
|
+
const detachChild = g._detachedChild === true;
|
|
2012
|
+
await daemonStart({
|
|
2013
|
+
...toCliOptions(g),
|
|
2014
|
+
...detach ? { detach } : {},
|
|
2015
|
+
...detachChild ? { detachChild } : {}
|
|
2016
|
+
});
|
|
1428
2017
|
});
|
|
1429
2018
|
daemonGrp.command("stop").description("stop the Noir daemon").action(async (...args) => {
|
|
1430
2019
|
await daemonStop(toCliOptions(trailingCmd(args).optsWithGlobals()));
|
|
@@ -1466,7 +2055,7 @@ function createProgram() {
|
|
|
1466
2055
|
"path to index (repeatable)",
|
|
1467
2056
|
(val, acc) => [...acc, val],
|
|
1468
2057
|
[]
|
|
1469
|
-
).option("--force", "
|
|
2058
|
+
).option("--force", "force a full reindex (drop all chunks+vectors, re-index from scratch)").action(async (...args) => {
|
|
1470
2059
|
const cmd = trailingCmd(args);
|
|
1471
2060
|
const g = cmd.optsWithGlobals();
|
|
1472
2061
|
const rawPaths = Array.isArray(g.path) ? g.path.filter((p) => typeof p === "string") : [];
|
|
@@ -1685,332 +2274,27 @@ if (isMainModule) {
|
|
|
1685
2274
|
void run(process.argv.slice(2));
|
|
1686
2275
|
}
|
|
1687
2276
|
|
|
1688
|
-
// src/
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2277
|
+
// src/tui/App.tsx
|
|
2278
|
+
import { Box as Box2, Text as Text8, useApp, useInput as useInput2 } from "ink";
|
|
2279
|
+
import { useEffect, useState as useState3 } from "react";
|
|
2280
|
+
|
|
2281
|
+
// src/tui/CommandInput.tsx
|
|
2282
|
+
import { Text } from "ink";
|
|
2283
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2284
|
+
function CommandInput({ buffer, running }) {
|
|
2285
|
+
if (running) {
|
|
2286
|
+
return /* @__PURE__ */ jsx(Text, { children: c.dim("\u2026") });
|
|
1696
2287
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
if (opts.verbose) {
|
|
1700
|
-
process.stderr.write(`noir: daemon transport detail: ${describeCause(cause)}
|
|
1701
|
-
`);
|
|
2288
|
+
if (buffer.length === 0) {
|
|
2289
|
+
return /* @__PURE__ */ jsx(Text, { children: c.dim("type a /command (e.g. /status, /sync, /task next), or q to quit") });
|
|
1702
2290
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
}
|
|
1711
|
-
if (!pidAlive2(rec.pid)) {
|
|
1712
|
-
if (opts.verbose) process.stderr.write(`noir: daemon probe: pid ${rec.pid} not alive
|
|
1713
|
-
`);
|
|
1714
|
-
return { running: false };
|
|
1715
|
-
}
|
|
1716
|
-
try {
|
|
1717
|
-
const res = await fetch(`http://127.0.0.1:${rec.port}/health`);
|
|
1718
|
-
if (!res.ok) {
|
|
1719
|
-
if (opts.verbose) process.stderr.write(`noir: daemon probe: /health \u2192 HTTP ${res.status}
|
|
1720
|
-
`);
|
|
1721
|
-
return { running: false };
|
|
1722
|
-
}
|
|
1723
|
-
const body = await res.json().catch(() => null);
|
|
1724
|
-
if (body?.ok !== true) {
|
|
1725
|
-
if (opts.verbose) process.stderr.write("noir: daemon probe: /health body not ok\n");
|
|
1726
|
-
return { running: false };
|
|
1727
|
-
}
|
|
1728
|
-
return {
|
|
1729
|
-
running: true,
|
|
1730
|
-
pid: typeof body.pid === "number" ? body.pid : rec.pid,
|
|
1731
|
-
port: rec.port,
|
|
1732
|
-
uptimeSec: typeof body.uptimeSec === "number" ? body.uptimeSec : void 0
|
|
1733
|
-
};
|
|
1734
|
-
} catch (err) {
|
|
1735
|
-
if (opts.verbose)
|
|
1736
|
-
process.stderr.write(`noir: daemon probe: unreachable (${describeCause(err)})
|
|
1737
|
-
`);
|
|
1738
|
-
return { running: false };
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
async function resolveDaemon(opts) {
|
|
1742
|
-
const project = opts.project ?? loadProjectInfo6(process.cwd());
|
|
1743
|
-
const idleTimeoutSec = opts.idleTimeoutSec ?? project.config.daemon.idleTimeoutSec;
|
|
1744
|
-
try {
|
|
1745
|
-
const ensured = await ensureDaemonRunning3({ project, idleTimeoutSec });
|
|
1746
|
-
return { url: ensured.url, stop: ensured.stop };
|
|
1747
|
-
} catch (err) {
|
|
1748
|
-
failDaemonDown(opts, err);
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
async function connectClient(client, url, opts) {
|
|
1752
|
-
try {
|
|
1753
|
-
await client.connect(new StreamableHTTPClientTransport(new URL(url)));
|
|
1754
|
-
} catch (err) {
|
|
1755
|
-
failDaemonDown(opts, err);
|
|
1756
|
-
}
|
|
1757
|
-
}
|
|
1758
|
-
async function callToolParse(client, opts, name, args) {
|
|
1759
|
-
const result = await client.callTool({ name, arguments: args }).catch((err) => failDaemonDown(opts, err));
|
|
1760
|
-
const block = result.content?.[0];
|
|
1761
|
-
if (block?.type !== "text" || typeof block.text !== "string") {
|
|
1762
|
-
failDaemonDown(opts, new Error(`tool '${name}' returned no text content`));
|
|
1763
|
-
}
|
|
1764
|
-
const text = block.text;
|
|
1765
|
-
try {
|
|
1766
|
-
return JSON.parse(text);
|
|
1767
|
-
} catch (err) {
|
|
1768
|
-
failDaemonDown(opts, err);
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
function buildCaller(client, opts) {
|
|
1772
|
-
return {
|
|
1773
|
-
callTool: (name, args = {}) => callToolParse(client, opts, name, args),
|
|
1774
|
-
listTools: async () => {
|
|
1775
|
-
const res = await client.listTools().catch((err) => failDaemonDown(opts, err));
|
|
1776
|
-
const tools = res?.tools;
|
|
1777
|
-
if (!Array.isArray(tools)) return [];
|
|
1778
|
-
const names = [];
|
|
1779
|
-
for (const t of tools) {
|
|
1780
|
-
const name = t?.name;
|
|
1781
|
-
if (typeof name === "string") names.push(name);
|
|
1782
|
-
}
|
|
1783
|
-
return names;
|
|
1784
|
-
}
|
|
1785
|
-
};
|
|
1786
|
-
}
|
|
1787
|
-
async function withDaemon(opts, fn) {
|
|
1788
|
-
const { url, stop } = await resolveDaemon(opts);
|
|
1789
|
-
let client;
|
|
1790
|
-
try {
|
|
1791
|
-
client = new Client(
|
|
1792
|
-
{ name: "noir-cli", version: NOIR_VERSION3 },
|
|
1793
|
-
{ versionNegotiation: { mode: "auto" } }
|
|
1794
|
-
);
|
|
1795
|
-
const connected = client;
|
|
1796
|
-
await connectClient(connected, url, opts);
|
|
1797
|
-
return await fn(buildCaller(connected, opts));
|
|
1798
|
-
} finally {
|
|
1799
|
-
if (client) {
|
|
1800
|
-
await client.close().catch(() => {
|
|
1801
|
-
});
|
|
1802
|
-
}
|
|
1803
|
-
await stop().catch(() => {
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
async function withRunningDaemon(opts, fn, probe) {
|
|
1808
|
-
const p = probe ?? await probeDaemon(opts);
|
|
1809
|
-
if (!p.running) return null;
|
|
1810
|
-
const url = `http://127.0.0.1:${p.port}/mcp`;
|
|
1811
|
-
let client;
|
|
1812
|
-
try {
|
|
1813
|
-
client = new Client(
|
|
1814
|
-
{ name: "noir-cli", version: NOIR_VERSION3 },
|
|
1815
|
-
{ versionNegotiation: { mode: "auto" } }
|
|
1816
|
-
);
|
|
1817
|
-
const connected = client;
|
|
1818
|
-
await connected.connect(new StreamableHTTPClientTransport(new URL(url)));
|
|
1819
|
-
return await fn(buildCaller(connected, opts));
|
|
1820
|
-
} catch {
|
|
1821
|
-
return null;
|
|
1822
|
-
} finally {
|
|
1823
|
-
if (client) {
|
|
1824
|
-
await client.close().catch(() => {
|
|
1825
|
-
});
|
|
1826
|
-
}
|
|
1827
|
-
}
|
|
1828
|
-
}
|
|
1829
|
-
async function callDaemonTool(opts, name, args = {}) {
|
|
1830
|
-
return withDaemon(opts, (caller) => caller.callTool(name, args));
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
// src/commands/status.ts
|
|
1834
|
-
async function tryTool(caller, name, args) {
|
|
1835
|
-
try {
|
|
1836
|
-
return await caller.callTool(name, args);
|
|
1837
|
-
} catch {
|
|
1838
|
-
return null;
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
function pickStore(s) {
|
|
1842
|
-
if (s?.ok !== true) return null;
|
|
1843
|
-
return {
|
|
1844
|
-
docCount: s.docCount,
|
|
1845
|
-
vecCount: s.vecCount,
|
|
1846
|
-
dbPath: s.dbPath,
|
|
1847
|
-
degraded: s.degraded === true
|
|
1848
|
-
};
|
|
1849
|
-
}
|
|
1850
|
-
function describeEmbedder2(e) {
|
|
1851
|
-
if (!e || e.kind === "none") return "none (BM25-only)";
|
|
1852
|
-
const model = typeof e.model === "string" && e.model.length > 0 ? e.model : "<unset>";
|
|
1853
|
-
return `${e.kind} \xB7 ${model} (${e.dim}-dim)`;
|
|
1854
|
-
}
|
|
1855
|
-
function pickContext(c2) {
|
|
1856
|
-
if (c2?.ok !== true) return null;
|
|
1857
|
-
return {
|
|
1858
|
-
docCount: c2.docCount,
|
|
1859
|
-
vecCount: c2.vecCount,
|
|
1860
|
-
indexedFiles: c2.indexedFiles,
|
|
1861
|
-
embedder: describeEmbedder2(c2.embedder),
|
|
1862
|
-
degraded: c2.degraded === true
|
|
1863
|
-
};
|
|
1864
|
-
}
|
|
1865
|
-
function pickWorkflow(w) {
|
|
1866
|
-
if (w?.ok !== true) return null;
|
|
1867
|
-
return {
|
|
1868
|
-
taskId: w.taskId,
|
|
1869
|
-
phase: w.phase,
|
|
1870
|
-
state: w.state,
|
|
1871
|
-
mode: w.mode,
|
|
1872
|
-
nextGate: w.nextGate,
|
|
1873
|
-
degraded: w.degraded === true
|
|
1874
|
-
};
|
|
1875
|
-
}
|
|
1876
|
-
function pickMemory(m) {
|
|
1877
|
-
if (m?.ok !== true || !Array.isArray(m.sessions)) return null;
|
|
1878
|
-
let observations = 0;
|
|
1879
|
-
for (const s of m.sessions) observations += s.count;
|
|
1880
|
-
return { sessions: m.sessions.length, observations };
|
|
1881
|
-
}
|
|
1882
|
-
function buildPayload(project, probe, host, store, context, workflow, memory) {
|
|
1883
|
-
return {
|
|
1884
|
-
noir: NOIR_VERSION4,
|
|
1885
|
-
project: { id: project.id, name: project.name },
|
|
1886
|
-
host: project.config.host,
|
|
1887
|
-
daemon: {
|
|
1888
|
-
running: probe.running,
|
|
1889
|
-
...probe.pid !== void 0 ? { pid: probe.pid } : {},
|
|
1890
|
-
...probe.uptimeSec !== void 0 ? { uptimeSec: probe.uptimeSec } : {},
|
|
1891
|
-
...host && typeof host.transport === "string" ? { transport: host.transport } : {}
|
|
1892
|
-
},
|
|
1893
|
-
store: pickStore(store),
|
|
1894
|
-
context: pickContext(context),
|
|
1895
|
-
workflow: pickWorkflow(workflow),
|
|
1896
|
-
memory: pickMemory(memory)
|
|
1897
|
-
};
|
|
1898
|
-
}
|
|
1899
|
-
function formatDuration(sec) {
|
|
1900
|
-
if (typeof sec !== "number" || sec < 0) return "unknown";
|
|
1901
|
-
if (sec < 60) return `${sec}s`;
|
|
1902
|
-
if (sec < 3600) return `${Math.floor(sec / 60)}m ${sec % 60}s`;
|
|
1903
|
-
return `${Math.floor(sec / 3600)}h ${Math.floor(sec % 3600 / 60)}m`;
|
|
1904
|
-
}
|
|
1905
|
-
function describeDaemon(d) {
|
|
1906
|
-
if (!d.running) return "not running (start with `noir daemon start`)";
|
|
1907
|
-
const pid = typeof d.pid === "number" ? `pid ${d.pid}` : "pid unknown";
|
|
1908
|
-
const up = typeof d.uptimeSec === "number" ? `, up ${formatDuration(d.uptimeSec)}` : "";
|
|
1909
|
-
const transport = typeof d.transport === "string" ? `, ${d.transport}` : "";
|
|
1910
|
-
return `running (${pid}${up}${transport})`;
|
|
1911
|
-
}
|
|
1912
|
-
function describeStore(s) {
|
|
1913
|
-
if (!s) return "unavailable (store engine not wired)";
|
|
1914
|
-
const flag = s.degraded ? ` ${badge("warn", "degraded: read-only")}` : "";
|
|
1915
|
-
return `${s.docCount} docs / ${s.vecCount} vecs${flag}`;
|
|
1916
|
-
}
|
|
1917
|
-
function describeContext(c2) {
|
|
1918
|
-
if (!c2) return "unavailable (context engine not wired)";
|
|
1919
|
-
const flag = c2.degraded ? ` ${badge("warn", "degraded")}` : "";
|
|
1920
|
-
return `${c2.docCount} docs, ${c2.vecCount} vecs, ${c2.indexedFiles} files \xB7 embedder ${c2.embedder}${flag}`;
|
|
1921
|
-
}
|
|
1922
|
-
function describeWorkflow(w) {
|
|
1923
|
-
if (!w) return "no active task";
|
|
1924
|
-
const gate = w.nextGate ? ` \u2192 next gate: ${w.nextGate}` : "";
|
|
1925
|
-
const flag = w.degraded ? ` ${badge("warn", "degraded")}` : "";
|
|
1926
|
-
return `${w.phase} (${w.state}, ${w.mode}) \xB7 task ${w.taskId}${gate}${flag}`;
|
|
1927
|
-
}
|
|
1928
|
-
function describeMemory(m) {
|
|
1929
|
-
if (!m) return "unavailable (memory engine not wired)";
|
|
1930
|
-
return `${m.observations} observations across ${m.sessions} session${m.sessions === 1 ? "" : "s"}`;
|
|
1931
|
-
}
|
|
1932
|
-
function renderHuman(p, opts) {
|
|
1933
|
-
log(`noir status \u2014 ${p.project.name} (${p.project.id})`, opts);
|
|
1934
|
-
definitionList(
|
|
1935
|
-
[
|
|
1936
|
-
{ label: "Project", value: `${p.project.name} (${p.project.id})` },
|
|
1937
|
-
{ label: "Host", value: p.host },
|
|
1938
|
-
{ label: "Noir", value: p.noir },
|
|
1939
|
-
{ label: "Daemon", value: describeDaemon(p.daemon) },
|
|
1940
|
-
{ label: "Store", value: describeStore(p.store) },
|
|
1941
|
-
{ label: "Context", value: describeContext(p.context) },
|
|
1942
|
-
{ label: "Workflow", value: describeWorkflow(p.workflow) },
|
|
1943
|
-
{ label: "Memory", value: describeMemory(p.memory) }
|
|
1944
|
-
],
|
|
1945
|
-
opts
|
|
1946
|
-
);
|
|
1947
|
-
}
|
|
1948
|
-
async function gatherStatusPayload(opts) {
|
|
1949
|
-
const project = loadProjectInfo7(process.cwd());
|
|
1950
|
-
const probe = await probeDaemon(opts);
|
|
1951
|
-
let host = null;
|
|
1952
|
-
let store = null;
|
|
1953
|
-
let context = null;
|
|
1954
|
-
let workflow = null;
|
|
1955
|
-
let memory = null;
|
|
1956
|
-
if (probe.running) {
|
|
1957
|
-
await withRunningDaemon(
|
|
1958
|
-
opts,
|
|
1959
|
-
async (caller) => {
|
|
1960
|
-
host = await tryTool(caller, "host_status");
|
|
1961
|
-
store = await tryTool(caller, "store_status");
|
|
1962
|
-
context = await tryTool(caller, "context_status");
|
|
1963
|
-
workflow = await tryTool(caller, "workflow_status");
|
|
1964
|
-
memory = await tryTool(caller, "memory_sessions");
|
|
1965
|
-
},
|
|
1966
|
-
probe
|
|
1967
|
-
);
|
|
1968
|
-
}
|
|
1969
|
-
return buildPayload(project, probe, host, store, context, workflow, memory);
|
|
1970
|
-
}
|
|
1971
|
-
async function status(opts) {
|
|
1972
|
-
void runAsyncUpdateCheck({
|
|
1973
|
-
env: process.env,
|
|
1974
|
-
configUpdate: (() => {
|
|
1975
|
-
try {
|
|
1976
|
-
const info2 = loadProjectInfo7(process.cwd());
|
|
1977
|
-
return info2.config.update ?? DEFAULT_UPDATE_CONFIG;
|
|
1978
|
-
} catch {
|
|
1979
|
-
return DEFAULT_UPDATE_CONFIG;
|
|
1980
|
-
}
|
|
1981
|
-
})(),
|
|
1982
|
-
quiet: opts.json === true || opts.quiet === true || !process.stdout.isTTY
|
|
1983
|
-
});
|
|
1984
|
-
const payload = await gatherStatusPayload(opts);
|
|
1985
|
-
if (opts.json === true) {
|
|
1986
|
-
process.stdout.write(`${JSON.stringify({ ok: true, data: payload })}
|
|
1987
|
-
`);
|
|
1988
|
-
return;
|
|
1989
|
-
}
|
|
1990
|
-
renderHuman(payload, opts);
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
// src/tui/App.tsx
|
|
1994
|
-
import { Box, Text as Text6, useApp, useInput } from "ink";
|
|
1995
|
-
import { useEffect, useState } from "react";
|
|
1996
|
-
|
|
1997
|
-
// src/tui/CommandInput.tsx
|
|
1998
|
-
import { Text } from "ink";
|
|
1999
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2000
|
-
function CommandInput({ buffer, running }) {
|
|
2001
|
-
if (running) {
|
|
2002
|
-
return /* @__PURE__ */ jsx(Text, { children: c.dim("\u2026") });
|
|
2003
|
-
}
|
|
2004
|
-
if (buffer.length === 0) {
|
|
2005
|
-
return /* @__PURE__ */ jsx(Text, { children: c.dim("type a /command (e.g. /status, /sync, /task next), or q to quit") });
|
|
2006
|
-
}
|
|
2007
|
-
const isCommand = buffer.startsWith("/");
|
|
2008
|
-
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
2009
|
-
/* @__PURE__ */ jsx(Text, { children: isCommand ? c.accent("\u25B8") : c.dim("\u25B8") }),
|
|
2010
|
-
" ",
|
|
2011
|
-
buffer,
|
|
2012
|
-
/* @__PURE__ */ jsx(Text, { children: c.dim("\u258C") })
|
|
2013
|
-
] });
|
|
2291
|
+
const isCommand = buffer.startsWith("/");
|
|
2292
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
2293
|
+
/* @__PURE__ */ jsx(Text, { children: isCommand ? c.accent("\u25B8") : c.dim("\u25B8") }),
|
|
2294
|
+
" ",
|
|
2295
|
+
buffer,
|
|
2296
|
+
/* @__PURE__ */ jsx(Text, { children: c.dim("\u258C") })
|
|
2297
|
+
] });
|
|
2014
2298
|
}
|
|
2015
2299
|
|
|
2016
2300
|
// src/tui/capture.ts
|
|
@@ -2046,7 +2330,9 @@ function Footer({ running }) {
|
|
|
2046
2330
|
if (running) {
|
|
2047
2331
|
return /* @__PURE__ */ jsx2(Text2, { children: c.dim("running\u2026 (Ctrl+C to force exit)") });
|
|
2048
2332
|
}
|
|
2049
|
-
return /* @__PURE__ */ jsx2(Text2, { children: c.dim(
|
|
2333
|
+
return /* @__PURE__ */ jsx2(Text2, { children: c.dim(
|
|
2334
|
+
"? help \xB7 q/Esc quit \xB7 \u2191/\u2193 scroll \xB7 Enter run \xB7 Ctrl+K palette \xB7 Ctrl+F find \xB7 Ctrl+C exit"
|
|
2335
|
+
) });
|
|
2050
2336
|
}
|
|
2051
2337
|
|
|
2052
2338
|
// src/tui/format.ts
|
|
@@ -2077,24 +2363,70 @@ function Header({ tagline }) {
|
|
|
2077
2363
|
] });
|
|
2078
2364
|
}
|
|
2079
2365
|
|
|
2366
|
+
// src/tui/hooks/useInputBuffer.ts
|
|
2367
|
+
import { useRef, useState } from "react";
|
|
2368
|
+
function useInputBuffer() {
|
|
2369
|
+
const [buffer, setBuffer] = useState("");
|
|
2370
|
+
const historyRef = useRef([]);
|
|
2371
|
+
const cursorRef = useRef(-1);
|
|
2372
|
+
function pushHistory(text) {
|
|
2373
|
+
if (text.length === 0) return;
|
|
2374
|
+
const history = historyRef.current;
|
|
2375
|
+
if (history.length > 0 && history[0] === text) return;
|
|
2376
|
+
historyRef.current = [text, ...history];
|
|
2377
|
+
}
|
|
2378
|
+
function recall(dir) {
|
|
2379
|
+
const history = historyRef.current;
|
|
2380
|
+
if (history.length === 0) return null;
|
|
2381
|
+
let cursor = cursorRef.current;
|
|
2382
|
+
if (dir === "up") {
|
|
2383
|
+
cursor = cursor === -1 ? 0 : Math.min(cursor + 1, history.length - 1);
|
|
2384
|
+
} else {
|
|
2385
|
+
cursor = cursor === -1 ? -1 : cursor - 1;
|
|
2386
|
+
}
|
|
2387
|
+
cursorRef.current = cursor;
|
|
2388
|
+
return cursor === -1 ? null : history[cursor] ?? null;
|
|
2389
|
+
}
|
|
2390
|
+
function clear() {
|
|
2391
|
+
setBuffer(() => "");
|
|
2392
|
+
cursorRef.current = -1;
|
|
2393
|
+
}
|
|
2394
|
+
return { buffer, setBuffer, pushHistory, recall, clear };
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2080
2397
|
// src/tui/OutputPane.tsx
|
|
2081
2398
|
import { Text as Text4 } from "ink";
|
|
2082
2399
|
import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2083
|
-
function toRows(visible) {
|
|
2400
|
+
function toRows(visible, baseIndex) {
|
|
2084
2401
|
const rows = [];
|
|
2085
2402
|
const seen = /* @__PURE__ */ new Map();
|
|
2086
|
-
for (
|
|
2403
|
+
for (let i = 0; i < visible.length; i++) {
|
|
2404
|
+
const text = visible[i] ?? "";
|
|
2087
2405
|
const n = (seen.get(text) ?? 0) + 1;
|
|
2088
2406
|
seen.set(text, n);
|
|
2089
|
-
rows.push({
|
|
2407
|
+
rows.push({
|
|
2408
|
+
key: n === 1 ? text || "_blank" : `${text || "_blank"}#${n}`,
|
|
2409
|
+
text,
|
|
2410
|
+
index: baseIndex + i
|
|
2411
|
+
});
|
|
2090
2412
|
}
|
|
2091
2413
|
return rows;
|
|
2092
2414
|
}
|
|
2415
|
+
function highlightFirst(text, query) {
|
|
2416
|
+
if (query.length === 0) return text;
|
|
2417
|
+
const idx = text.toLowerCase().indexOf(query.toLowerCase());
|
|
2418
|
+
if (idx < 0) return text;
|
|
2419
|
+
return `${text.slice(0, idx)}${c.bold(text.slice(idx, idx + query.length))}${text.slice(
|
|
2420
|
+
idx + query.length
|
|
2421
|
+
)}`;
|
|
2422
|
+
}
|
|
2093
2423
|
function OutputPane({
|
|
2094
2424
|
lines,
|
|
2095
2425
|
scrollOffset,
|
|
2096
2426
|
height = 12,
|
|
2097
|
-
title
|
|
2427
|
+
title,
|
|
2428
|
+
highlightQuery,
|
|
2429
|
+
activeLine
|
|
2098
2430
|
}) {
|
|
2099
2431
|
const width = terminalWidth();
|
|
2100
2432
|
if (lines.length === 0) {
|
|
@@ -2102,25 +2434,260 @@ function OutputPane({
|
|
|
2102
2434
|
}
|
|
2103
2435
|
const maxOffset = Math.max(0, lines.length - height);
|
|
2104
2436
|
const offset = Math.min(Math.max(0, scrollOffset), maxOffset);
|
|
2105
|
-
const rows = toRows(lines.slice(offset, offset + height));
|
|
2437
|
+
const rows = toRows(lines.slice(offset, offset + height), offset);
|
|
2106
2438
|
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
2107
2439
|
title !== void 0 ? /* @__PURE__ */ jsx4(Text4, { children: c.dim(`\u2500\u2500 ${title} \u2500\u2500`) }) : null,
|
|
2108
|
-
rows.map((row) =>
|
|
2440
|
+
rows.map((row) => {
|
|
2441
|
+
const truncated = row.text.length > width ? `${row.text.slice(0, Math.max(1, width - 1))}\u2026` : row.text;
|
|
2442
|
+
let body = truncated;
|
|
2443
|
+
if (activeLine !== void 0 && row.index === activeLine) {
|
|
2444
|
+
body = c.accent(truncated);
|
|
2445
|
+
} else if (highlightQuery !== void 0 && highlightQuery.length > 0) {
|
|
2446
|
+
body = highlightFirst(truncated, highlightQuery);
|
|
2447
|
+
}
|
|
2448
|
+
return /* @__PURE__ */ jsx4(Text4, { children: body }, row.key);
|
|
2449
|
+
})
|
|
2109
2450
|
] });
|
|
2110
2451
|
}
|
|
2111
2452
|
|
|
2112
|
-
// src/tui/
|
|
2453
|
+
// src/tui/overlays/ConfirmOverlay.tsx
|
|
2113
2454
|
import { Text as Text5 } from "ink";
|
|
2114
2455
|
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2456
|
+
function ConfirmOverlay({ argv }) {
|
|
2457
|
+
return /* @__PURE__ */ jsxs4(Text5, { children: [
|
|
2458
|
+
c.warn(`! run /${argv.join(" ")}? (y/N)`),
|
|
2459
|
+
/* @__PURE__ */ jsx5(Text5, { children: c.dim(" [y approve \xB7 n/Esc back to palette]") })
|
|
2460
|
+
] });
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
// src/tui/overlays/SearchMode.ts
|
|
2464
|
+
function computeMatches(lines, query) {
|
|
2465
|
+
if (query.length === 0) return [];
|
|
2466
|
+
const needle = query.toLowerCase();
|
|
2467
|
+
const out = [];
|
|
2468
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2469
|
+
if ((lines[i] ?? "").toLowerCase().includes(needle)) out.push(i);
|
|
2470
|
+
}
|
|
2471
|
+
return out;
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
// src/tui/palette/fuzzyMatch.ts
|
|
2475
|
+
var WEIGHTS = {
|
|
2476
|
+
label: 1,
|
|
2477
|
+
keyword: 0.7,
|
|
2478
|
+
description: 0.5
|
|
2479
|
+
};
|
|
2480
|
+
var LABEL_TIE_BONUS = 50;
|
|
2481
|
+
function fuzzyScore(query, text) {
|
|
2482
|
+
if (query.length === 0) return { score: 0, indices: [] };
|
|
2483
|
+
const q = query.toLowerCase();
|
|
2484
|
+
const t = text.toLowerCase();
|
|
2485
|
+
const indices = [];
|
|
2486
|
+
let score = 0;
|
|
2487
|
+
let prevMatchedIndex = -1;
|
|
2488
|
+
let qi = 0;
|
|
2489
|
+
for (let ti = 0; ti < t.length && qi < q.length; ti++) {
|
|
2490
|
+
if (t[ti] === q[qi]) {
|
|
2491
|
+
indices.push(ti);
|
|
2492
|
+
const consecutive = prevMatchedIndex !== -1 && ti === prevMatchedIndex + 1;
|
|
2493
|
+
if (consecutive) score += 8;
|
|
2494
|
+
if (ti < q.length) score += 4;
|
|
2495
|
+
if (prevMatchedIndex !== -1) {
|
|
2496
|
+
const skipped = ti - prevMatchedIndex - 1;
|
|
2497
|
+
if (skipped > 0) score -= 0.1 * skipped;
|
|
2498
|
+
}
|
|
2499
|
+
prevMatchedIndex = ti;
|
|
2500
|
+
qi++;
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
if (qi < q.length) return null;
|
|
2504
|
+
return { score, indices };
|
|
2505
|
+
}
|
|
2506
|
+
function matchCommand(query, item) {
|
|
2507
|
+
if (query.length === 0) {
|
|
2508
|
+
return { item, score: 0, matchedIndices: [] };
|
|
2509
|
+
}
|
|
2510
|
+
const best = {
|
|
2511
|
+
score: Number.NEGATIVE_INFINITY,
|
|
2512
|
+
indices: []
|
|
2513
|
+
};
|
|
2514
|
+
let matched = false;
|
|
2515
|
+
const consider = (score, indices) => {
|
|
2516
|
+
if (!matched || score > best.score) {
|
|
2517
|
+
best.score = score;
|
|
2518
|
+
best.indices = indices;
|
|
2519
|
+
matched = true;
|
|
2520
|
+
}
|
|
2521
|
+
};
|
|
2522
|
+
const labelHit = fuzzyScore(query, item.label);
|
|
2523
|
+
if (labelHit) {
|
|
2524
|
+
consider(labelHit.score * WEIGHTS.label + LABEL_TIE_BONUS, labelHit.indices);
|
|
2525
|
+
}
|
|
2526
|
+
if (item.keywords) {
|
|
2527
|
+
let bestKw = null;
|
|
2528
|
+
for (const kw of item.keywords) {
|
|
2529
|
+
const kwHit = fuzzyScore(query, kw);
|
|
2530
|
+
if (kwHit && (bestKw === null || kwHit.score > bestKw.score)) bestKw = kwHit;
|
|
2531
|
+
}
|
|
2532
|
+
if (bestKw) {
|
|
2533
|
+
consider(bestKw.score * WEIGHTS.keyword, bestKw.indices);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
if (item.description) {
|
|
2537
|
+
const descHit = fuzzyScore(query, item.description);
|
|
2538
|
+
if (descHit) {
|
|
2539
|
+
consider(descHit.score * WEIGHTS.description, descHit.indices);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
if (!matched) return null;
|
|
2543
|
+
return { item, score: best.score, matchedIndices: best.indices };
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
// src/tui/palette/matcher.ts
|
|
2547
|
+
var handRolledMatcher = {
|
|
2548
|
+
search(query, items, limit) {
|
|
2549
|
+
if (query.length === 0) {
|
|
2550
|
+
return items.slice(0, Math.max(0, limit)).map((item) => ({
|
|
2551
|
+
item,
|
|
2552
|
+
score: 0,
|
|
2553
|
+
matchedIndices: []
|
|
2554
|
+
}));
|
|
2555
|
+
}
|
|
2556
|
+
const matched = [];
|
|
2557
|
+
for (const item of items) {
|
|
2558
|
+
const m = matchCommand(query, item);
|
|
2559
|
+
if (m) matched.push(m);
|
|
2560
|
+
}
|
|
2561
|
+
matched.sort((a, b) => b.score - a.score);
|
|
2562
|
+
return matched.slice(0, Math.max(0, limit));
|
|
2563
|
+
}
|
|
2564
|
+
};
|
|
2565
|
+
|
|
2566
|
+
// src/tui/palette/Palette.tsx
|
|
2567
|
+
import { Box, Text as Text6, useInput } from "ink";
|
|
2568
|
+
import { useState as useState2 } from "react";
|
|
2569
|
+
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2570
|
+
var VISIBLE_ROWS = 10;
|
|
2571
|
+
var MATCH_LIMIT = 20;
|
|
2572
|
+
function rankPalette(query, matcher, commands, recent) {
|
|
2573
|
+
if (query.length === 0) {
|
|
2574
|
+
const recents = recentDeduped(recent, commands);
|
|
2575
|
+
const recentIds = new Set(recents.map((r) => r.id));
|
|
2576
|
+
const rest = commands.filter((cmd) => !recentIds.has(cmd.id));
|
|
2577
|
+
return [
|
|
2578
|
+
...recents.map((cmd) => ({ cmd, indices: [] })),
|
|
2579
|
+
...rest.map((cmd) => ({ cmd, indices: [] }))
|
|
2580
|
+
];
|
|
2581
|
+
}
|
|
2582
|
+
return matcher.search(query, commands, MATCH_LIMIT).map((m) => ({
|
|
2583
|
+
cmd: m.item,
|
|
2584
|
+
indices: m.matchedIndices
|
|
2585
|
+
}));
|
|
2586
|
+
}
|
|
2587
|
+
function highlight(label, matchedIndices) {
|
|
2588
|
+
const set = new Set(matchedIndices);
|
|
2589
|
+
const chars = [];
|
|
2590
|
+
for (let i = 0; i < label.length; i++) {
|
|
2591
|
+
const ch = label[i] ?? "";
|
|
2592
|
+
chars.push(set.has(i) ? /* @__PURE__ */ jsx6(Text6, { children: c.accent(ch) }, i) : /* @__PURE__ */ jsx6(Text6, { children: ch }, i));
|
|
2593
|
+
}
|
|
2594
|
+
return /* @__PURE__ */ jsx6(Fragment2, { children: chars });
|
|
2595
|
+
}
|
|
2596
|
+
function Palette({
|
|
2597
|
+
commands,
|
|
2598
|
+
matcher,
|
|
2599
|
+
recent,
|
|
2600
|
+
onSelect,
|
|
2601
|
+
onClose
|
|
2602
|
+
}) {
|
|
2603
|
+
const [query, setQuery] = useState2("");
|
|
2604
|
+
const [activeIndex, setActiveIndex] = useState2(0);
|
|
2605
|
+
const ranked = rankPalette(query, matcher, commands, recent);
|
|
2606
|
+
const visible = ranked.slice(0, VISIBLE_ROWS);
|
|
2607
|
+
const active = Math.min(Math.max(activeIndex, 0), Math.max(0, visible.length - 1));
|
|
2608
|
+
const recents = recentDeduped(recent, commands);
|
|
2609
|
+
useInput((input, key) => {
|
|
2610
|
+
if (key.escape) {
|
|
2611
|
+
onClose();
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
if (key.return) {
|
|
2615
|
+
const hit = visible[active];
|
|
2616
|
+
if (hit) onSelect(hit.cmd);
|
|
2617
|
+
return;
|
|
2618
|
+
}
|
|
2619
|
+
if (key.upArrow) {
|
|
2620
|
+
setActiveIndex((i) => Math.max(0, i - 1));
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
if (key.downArrow) {
|
|
2624
|
+
setActiveIndex((i) => i + 1);
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
if (key.backspace || key.delete) {
|
|
2628
|
+
setQuery((q) => q.slice(0, -1));
|
|
2629
|
+
setActiveIndex(0);
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
if (key.ctrl) return;
|
|
2633
|
+
if (input.length > 0) {
|
|
2634
|
+
setQuery((q) => q + input);
|
|
2635
|
+
setActiveIndex(0);
|
|
2636
|
+
}
|
|
2637
|
+
});
|
|
2638
|
+
const rows = [
|
|
2639
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2640
|
+
c.bold("\u25B8 palette "),
|
|
2641
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim(`search (${visible.length} shown)`) })
|
|
2642
|
+
] }, "header"),
|
|
2643
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2644
|
+
c.dim("> "),
|
|
2645
|
+
query,
|
|
2646
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim("\u258C") })
|
|
2647
|
+
] }, "query")
|
|
2648
|
+
];
|
|
2649
|
+
let lastCategory = null;
|
|
2650
|
+
for (const { cmd, indices } of visible) {
|
|
2651
|
+
const isRecent = recents.some((r) => r.id === cmd.id);
|
|
2652
|
+
if (query.length === 0) {
|
|
2653
|
+
if (isRecent && lastCategory !== "recent") {
|
|
2654
|
+
rows.push(/* @__PURE__ */ jsx6(Text6, { children: c.dim("\u2500\u2500 recent \u2500\u2500") }, "cat:recent"));
|
|
2655
|
+
lastCategory = "recent";
|
|
2656
|
+
} else if (!isRecent && cmd.category !== lastCategory) {
|
|
2657
|
+
rows.push(/* @__PURE__ */ jsx6(Text6, { children: c.dim(`\u2500\u2500 ${cmd.category} \u2500\u2500`) }, `cat:${cmd.category}`));
|
|
2658
|
+
lastCategory = cmd.category;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
const prefix = cmd.id === visible[active]?.cmd.id ? "\u25B8 " : " ";
|
|
2662
|
+
rows.push(
|
|
2663
|
+
/* @__PURE__ */ jsxs5(Text6, { children: [
|
|
2664
|
+
isRecent && query.length === 0 ? /* @__PURE__ */ jsx6(Text6, { children: c.dim("\u21BA ") }) : null,
|
|
2665
|
+
prefix,
|
|
2666
|
+
highlight(cmd.label, indices),
|
|
2667
|
+
/* @__PURE__ */ jsx6(Text6, { children: c.dim(` ${cmd.description}`) })
|
|
2668
|
+
] }, cmd.id)
|
|
2669
|
+
);
|
|
2670
|
+
}
|
|
2671
|
+
rows.push(/* @__PURE__ */ jsx6(Text6, { children: c.dim("\u2191/\u2193 navigate \xB7 Enter run \xB7 Esc close") }, "footer"));
|
|
2672
|
+
return /* @__PURE__ */ jsx6(Box, { flexDirection: "column", children: rows.map((row) => row) });
|
|
2673
|
+
}
|
|
2674
|
+
function recentDeduped(recent, commands) {
|
|
2675
|
+
const known = new Set(commands.map((cmd) => cmd.id));
|
|
2676
|
+
return recent.filter((cmd) => known.has(cmd.id));
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
// src/tui/StatusBar.tsx
|
|
2680
|
+
import { Text as Text7 } from "ink";
|
|
2681
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2115
2682
|
function Cell({
|
|
2116
2683
|
label,
|
|
2117
2684
|
value,
|
|
2118
2685
|
accent
|
|
2119
2686
|
}) {
|
|
2120
|
-
return /* @__PURE__ */
|
|
2121
|
-
/* @__PURE__ */
|
|
2687
|
+
return /* @__PURE__ */ jsxs6(Text7, { children: [
|
|
2688
|
+
/* @__PURE__ */ jsx7(Text7, { children: c.dim(`${label}:`) }),
|
|
2122
2689
|
" ",
|
|
2123
|
-
/* @__PURE__ */
|
|
2690
|
+
/* @__PURE__ */ jsx7(Text7, { children: accent ? c.accent(value) : value })
|
|
2124
2691
|
] });
|
|
2125
2692
|
}
|
|
2126
2693
|
function StatusBar({ payload, loading }) {
|
|
@@ -2129,14 +2696,14 @@ function StatusBar({ payload, loading }) {
|
|
|
2129
2696
|
const phase = payload?.workflow?.phase ?? (loading ? "\u2026" : "idle");
|
|
2130
2697
|
const daemonRunning = payload?.daemon.running === true;
|
|
2131
2698
|
const daemon = payload === null && loading ? "\u2026" : daemonRunning ? "up" : "down";
|
|
2132
|
-
return /* @__PURE__ */
|
|
2133
|
-
/* @__PURE__ */
|
|
2134
|
-
/* @__PURE__ */
|
|
2135
|
-
/* @__PURE__ */
|
|
2136
|
-
/* @__PURE__ */
|
|
2137
|
-
/* @__PURE__ */
|
|
2138
|
-
/* @__PURE__ */
|
|
2139
|
-
/* @__PURE__ */
|
|
2699
|
+
return /* @__PURE__ */ jsxs6(Text7, { children: [
|
|
2700
|
+
/* @__PURE__ */ jsx7(Cell, { label: "host", value: host, accent: true }),
|
|
2701
|
+
/* @__PURE__ */ jsx7(Text7, { children: c.dim(" \xB7 ") }),
|
|
2702
|
+
/* @__PURE__ */ jsx7(Cell, { label: "mode", value: mode }),
|
|
2703
|
+
/* @__PURE__ */ jsx7(Text7, { children: c.dim(" \xB7 ") }),
|
|
2704
|
+
/* @__PURE__ */ jsx7(Cell, { label: "phase", value: phase }),
|
|
2705
|
+
/* @__PURE__ */ jsx7(Text7, { children: c.dim(" \xB7 ") }),
|
|
2706
|
+
/* @__PURE__ */ jsxs6(Text7, { children: [
|
|
2140
2707
|
c.dim("daemon:"),
|
|
2141
2708
|
" ",
|
|
2142
2709
|
daemonRunning ? c.ok(daemon) : c.warn(daemon)
|
|
@@ -2145,18 +2712,33 @@ function StatusBar({ payload, loading }) {
|
|
|
2145
2712
|
}
|
|
2146
2713
|
|
|
2147
2714
|
// src/tui/App.tsx
|
|
2148
|
-
import { jsx as
|
|
2715
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2149
2716
|
function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
|
|
2150
2717
|
const { exit } = useApp();
|
|
2151
|
-
const
|
|
2152
|
-
const [
|
|
2153
|
-
const [
|
|
2154
|
-
const [
|
|
2155
|
-
const [
|
|
2156
|
-
const [
|
|
2157
|
-
const [
|
|
2158
|
-
const [
|
|
2159
|
-
const [
|
|
2718
|
+
const { buffer, setBuffer, pushHistory, recall, clear } = useInputBuffer();
|
|
2719
|
+
const [mode, setMode] = useState3({ kind: "dashboard" });
|
|
2720
|
+
const [payload, setPayload] = useState3(initialPayload);
|
|
2721
|
+
const [loading, setLoading] = useState3(initialPayload === null);
|
|
2722
|
+
const [output, setOutput] = useState3(null);
|
|
2723
|
+
const [searchLines, setSearchLines] = useState3([]);
|
|
2724
|
+
const [scrollOffset, setScrollOffset] = useState3(0);
|
|
2725
|
+
const [help, setHelp] = useState3(false);
|
|
2726
|
+
const [running, setRunning] = useState3(false);
|
|
2727
|
+
const [notice, setNotice] = useState3(null);
|
|
2728
|
+
const [pending, setPending] = useState3(null);
|
|
2729
|
+
const [recent, setRecent] = useState3([]);
|
|
2730
|
+
useEffect(() => {
|
|
2731
|
+
if (!deps.loadRecent) return;
|
|
2732
|
+
const loader = deps.loadRecent;
|
|
2733
|
+
let cancelled = false;
|
|
2734
|
+
void loader().then((entries) => {
|
|
2735
|
+
if (!cancelled) setRecent(entries);
|
|
2736
|
+
}).catch(() => {
|
|
2737
|
+
});
|
|
2738
|
+
return () => {
|
|
2739
|
+
cancelled = true;
|
|
2740
|
+
};
|
|
2741
|
+
}, [deps.loadRecent]);
|
|
2160
2742
|
useEffect(() => {
|
|
2161
2743
|
if (running) return;
|
|
2162
2744
|
let cancelled = false;
|
|
@@ -2205,7 +2787,12 @@ function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
|
|
|
2205
2787
|
active = false;
|
|
2206
2788
|
};
|
|
2207
2789
|
}, [pending, deps]);
|
|
2208
|
-
|
|
2790
|
+
function dispatchCmd(argv) {
|
|
2791
|
+
setOutput(null);
|
|
2792
|
+
setRunning(true);
|
|
2793
|
+
setPending([...argv]);
|
|
2794
|
+
}
|
|
2795
|
+
function handleDashboardInput(input, key) {
|
|
2209
2796
|
if (running) return;
|
|
2210
2797
|
setNotice(null);
|
|
2211
2798
|
if (help) {
|
|
@@ -2217,7 +2804,7 @@ function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
|
|
|
2217
2804
|
return;
|
|
2218
2805
|
}
|
|
2219
2806
|
if (key.escape) {
|
|
2220
|
-
if (buffer.length > 0)
|
|
2807
|
+
if (buffer.length > 0) clear();
|
|
2221
2808
|
else if (output !== null) setOutput(null);
|
|
2222
2809
|
else exit();
|
|
2223
2810
|
return;
|
|
@@ -2227,14 +2814,38 @@ function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
|
|
|
2227
2814
|
return;
|
|
2228
2815
|
}
|
|
2229
2816
|
if (key.upArrow) {
|
|
2817
|
+
if (buffer === "/") {
|
|
2818
|
+
const entry = recall("up");
|
|
2819
|
+
if (entry !== null) {
|
|
2820
|
+
setBuffer(() => entry);
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2230
2824
|
setScrollOffset((o) => Math.max(0, o - 1));
|
|
2231
2825
|
return;
|
|
2232
2826
|
}
|
|
2233
2827
|
if (key.downArrow) {
|
|
2828
|
+
if (buffer === "/") {
|
|
2829
|
+
const entry = recall("down");
|
|
2830
|
+
if (entry !== null) {
|
|
2831
|
+
setBuffer(() => entry);
|
|
2832
|
+
return;
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2234
2835
|
setScrollOffset((o) => o + 1);
|
|
2235
2836
|
return;
|
|
2236
2837
|
}
|
|
2237
|
-
if (key.ctrl)
|
|
2838
|
+
if (key.ctrl) {
|
|
2839
|
+
if (input === "k") {
|
|
2840
|
+
openPalette();
|
|
2841
|
+
return;
|
|
2842
|
+
}
|
|
2843
|
+
if (input === "f" && output !== null) {
|
|
2844
|
+
openSearch();
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2238
2849
|
if (input.length === 1) {
|
|
2239
2850
|
if (buffer.length === 0) {
|
|
2240
2851
|
if (input === "q") {
|
|
@@ -2252,11 +2863,108 @@ function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
|
|
|
2252
2863
|
if (input.length > 1) {
|
|
2253
2864
|
setBuffer((b) => b + input);
|
|
2254
2865
|
}
|
|
2866
|
+
}
|
|
2867
|
+
function openPalette() {
|
|
2868
|
+
setMode({ kind: "palette" });
|
|
2869
|
+
setNotice(null);
|
|
2870
|
+
}
|
|
2871
|
+
function openSearch() {
|
|
2872
|
+
if (output === null) return;
|
|
2873
|
+
setSearchLines(output.lines);
|
|
2874
|
+
setMode({ kind: "search", query: "", matches: [], active: -1 });
|
|
2875
|
+
setNotice(null);
|
|
2876
|
+
}
|
|
2877
|
+
function handlePaletteSelect(cmd) {
|
|
2878
|
+
if (cmd.destructive) {
|
|
2879
|
+
setMode({ kind: "confirm", argv: [...cmd.argv] });
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
dispatchCmd([...cmd.argv]);
|
|
2883
|
+
void recordRuns([...cmd.argv]);
|
|
2884
|
+
setMode({ kind: "dashboard" });
|
|
2885
|
+
}
|
|
2886
|
+
function handleConfirmApprove() {
|
|
2887
|
+
if (mode.kind !== "confirm") return;
|
|
2888
|
+
dispatchCmd([...mode.argv]);
|
|
2889
|
+
void recordRuns([...mode.argv]);
|
|
2890
|
+
setMode({ kind: "dashboard" });
|
|
2891
|
+
}
|
|
2892
|
+
function handleConfirmDecline() {
|
|
2893
|
+
setMode({ kind: "palette" });
|
|
2894
|
+
}
|
|
2895
|
+
function recordRuns(argv) {
|
|
2896
|
+
const recorder = deps.record;
|
|
2897
|
+
if (recorder) void recorder(argv);
|
|
2898
|
+
}
|
|
2899
|
+
function handleConfirmInput(input, key) {
|
|
2900
|
+
if (key.escape || input === "n" || input === "N") {
|
|
2901
|
+
handleConfirmDecline();
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
if (input === "y" || input === "Y") {
|
|
2905
|
+
handleConfirmApprove();
|
|
2906
|
+
return;
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
function handleSearchInput(input, key) {
|
|
2910
|
+
if (mode.kind !== "search") return;
|
|
2911
|
+
if (key.escape) {
|
|
2912
|
+
setMode({ kind: "dashboard" });
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
if (key.backspace || key.delete) {
|
|
2916
|
+
const nextQuery = mode.query.slice(0, -1);
|
|
2917
|
+
setMode(searchFor(nextQuery));
|
|
2918
|
+
return;
|
|
2919
|
+
}
|
|
2920
|
+
if (key.ctrl) return;
|
|
2921
|
+
if (key.return) {
|
|
2922
|
+
advanceActive(1);
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
if (input === "n" && mode.matches.length > 0) {
|
|
2926
|
+
advanceActive(1);
|
|
2927
|
+
return;
|
|
2928
|
+
}
|
|
2929
|
+
if (input === "N" && mode.matches.length > 0) {
|
|
2930
|
+
advanceActive(-1);
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
if (input.length > 0) {
|
|
2934
|
+
setMode(searchFor(mode.query + input));
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
function searchFor(query) {
|
|
2938
|
+
const matches = computeMatches(searchLines, query);
|
|
2939
|
+
return { kind: "search", query, matches, active: matches.length > 0 ? 0 : -1 };
|
|
2940
|
+
}
|
|
2941
|
+
function advanceActive(delta) {
|
|
2942
|
+
if (mode.kind !== "search") return;
|
|
2943
|
+
const n = mode.matches.length;
|
|
2944
|
+
if (n === 0) return;
|
|
2945
|
+
const next = (mode.active + delta + n) % n;
|
|
2946
|
+
setMode({ ...mode, active: next });
|
|
2947
|
+
}
|
|
2948
|
+
useInput2((input, key) => {
|
|
2949
|
+
switch (mode.kind) {
|
|
2950
|
+
case "dashboard":
|
|
2951
|
+
handleDashboardInput(input, key);
|
|
2952
|
+
return;
|
|
2953
|
+
case "palette":
|
|
2954
|
+
return;
|
|
2955
|
+
// Palette renders its own useInput; no App-level keys here
|
|
2956
|
+
case "confirm":
|
|
2957
|
+
handleConfirmInput(input, key);
|
|
2958
|
+
return;
|
|
2959
|
+
case "search":
|
|
2960
|
+
handleSearchInput(input, key);
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2255
2963
|
});
|
|
2256
2964
|
function submit() {
|
|
2257
2965
|
const text = buffer;
|
|
2258
2966
|
if (text.length === 0) return;
|
|
2259
|
-
|
|
2967
|
+
clear();
|
|
2260
2968
|
if (!text.startsWith("/")) {
|
|
2261
2969
|
setNotice("type a /command to run it (bare text is just a hint)");
|
|
2262
2970
|
return;
|
|
@@ -2266,52 +2974,160 @@ function App({ deps, initialPayload = null, refreshMs = 5e3 }) {
|
|
|
2266
2974
|
setNotice("empty /command");
|
|
2267
2975
|
return;
|
|
2268
2976
|
}
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2977
|
+
pushHistory(text);
|
|
2978
|
+
dispatchCmd(argv);
|
|
2979
|
+
void recordRuns(argv);
|
|
2272
2980
|
}
|
|
2273
2981
|
if (help) {
|
|
2274
|
-
return /* @__PURE__ */
|
|
2982
|
+
return /* @__PURE__ */ jsx8(Help, {});
|
|
2983
|
+
}
|
|
2984
|
+
if (mode.kind === "palette") {
|
|
2985
|
+
return /* @__PURE__ */ jsxs7(Box2, { flexDirection: "column", children: [
|
|
2986
|
+
/* @__PURE__ */ jsx8(Header, { tagline: "palette" }),
|
|
2987
|
+
/* @__PURE__ */ jsx8(
|
|
2988
|
+
Palette,
|
|
2989
|
+
{
|
|
2990
|
+
commands: deps.commands ?? [],
|
|
2991
|
+
matcher: deps.matcher ?? handRolledMatcher,
|
|
2992
|
+
recent,
|
|
2993
|
+
onSelect: handlePaletteSelect,
|
|
2994
|
+
onClose: () => setMode({ kind: "dashboard" })
|
|
2995
|
+
}
|
|
2996
|
+
),
|
|
2997
|
+
/* @__PURE__ */ jsx8(Footer, { running: false })
|
|
2998
|
+
] });
|
|
2999
|
+
}
|
|
3000
|
+
if (mode.kind === "confirm") {
|
|
3001
|
+
return /* @__PURE__ */ jsxs7(Box2, { flexDirection: "column", children: [
|
|
3002
|
+
/* @__PURE__ */ jsx8(Header, { tagline: "confirm" }),
|
|
3003
|
+
/* @__PURE__ */ jsx8(ConfirmOverlay, { argv: mode.argv }),
|
|
3004
|
+
/* @__PURE__ */ jsx8(Footer, { running: false })
|
|
3005
|
+
] });
|
|
3006
|
+
}
|
|
3007
|
+
if (mode.kind === "search") {
|
|
3008
|
+
const activeLine = mode.active >= 0 ? mode.matches[mode.active] : void 0;
|
|
3009
|
+
const matchCount = mode.matches.length;
|
|
3010
|
+
return /* @__PURE__ */ jsxs7(Box2, { flexDirection: "column", children: [
|
|
3011
|
+
/* @__PURE__ */ jsx8(Header, { tagline: "search" }),
|
|
3012
|
+
/* @__PURE__ */ jsx8(
|
|
3013
|
+
OutputPane,
|
|
3014
|
+
{
|
|
3015
|
+
lines: searchLines,
|
|
3016
|
+
scrollOffset,
|
|
3017
|
+
title: output?.title,
|
|
3018
|
+
highlightQuery: mode.query,
|
|
3019
|
+
activeLine
|
|
3020
|
+
}
|
|
3021
|
+
),
|
|
3022
|
+
/* @__PURE__ */ jsxs7(Text8, { children: [
|
|
3023
|
+
c.dim("search: "),
|
|
3024
|
+
mode.query.length > 0 ? mode.query : c.dim("(type to filter output)"),
|
|
3025
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" \u258C") })
|
|
3026
|
+
] }),
|
|
3027
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(
|
|
3028
|
+
matchCount === 0 ? "no matches \xB7 Esc exit" : `${mode.active + 1}/${matchCount} matches \xB7 n/Enter next \xB7 N prev \xB7 Esc exit`
|
|
3029
|
+
) }),
|
|
3030
|
+
/* @__PURE__ */ jsx8(Footer, { running: false })
|
|
3031
|
+
] });
|
|
2275
3032
|
}
|
|
2276
3033
|
const snapshotLines = formatStatusPayload(payload);
|
|
2277
3034
|
const paneLines = output?.lines ?? snapshotLines ?? [];
|
|
2278
3035
|
const paneTitle = output?.title ?? "snapshot";
|
|
2279
|
-
return /* @__PURE__ */
|
|
2280
|
-
/* @__PURE__ */
|
|
2281
|
-
/* @__PURE__ */
|
|
2282
|
-
/* @__PURE__ */
|
|
2283
|
-
/* @__PURE__ */
|
|
2284
|
-
notice !== null ? /* @__PURE__ */
|
|
2285
|
-
/* @__PURE__ */
|
|
3036
|
+
return /* @__PURE__ */ jsxs7(Box2, { flexDirection: "column", children: [
|
|
3037
|
+
/* @__PURE__ */ jsx8(Header, { tagline: "dashboard" }),
|
|
3038
|
+
/* @__PURE__ */ jsx8(StatusBar, { payload, loading }),
|
|
3039
|
+
/* @__PURE__ */ jsx8(OutputPane, { lines: paneLines, scrollOffset, title: paneTitle }),
|
|
3040
|
+
/* @__PURE__ */ jsx8(CommandInput, { buffer, running }),
|
|
3041
|
+
notice !== null ? /* @__PURE__ */ jsx8(Text8, { children: c.dim(notice) }) : null,
|
|
3042
|
+
/* @__PURE__ */ jsx8(Footer, { running })
|
|
2286
3043
|
] });
|
|
2287
3044
|
}
|
|
2288
3045
|
function Help() {
|
|
2289
|
-
return /* @__PURE__ */
|
|
2290
|
-
/* @__PURE__ */
|
|
2291
|
-
/* @__PURE__ */
|
|
2292
|
-
/* @__PURE__ */
|
|
2293
|
-
/* @__PURE__ */
|
|
2294
|
-
/* @__PURE__ */
|
|
2295
|
-
/* @__PURE__ */
|
|
2296
|
-
/* @__PURE__ */
|
|
2297
|
-
/* @__PURE__ */
|
|
2298
|
-
/* @__PURE__ */
|
|
2299
|
-
/* @__PURE__ */
|
|
2300
|
-
/* @__PURE__ */
|
|
2301
|
-
/* @__PURE__ */
|
|
2302
|
-
/* @__PURE__ */
|
|
2303
|
-
/* @__PURE__ */
|
|
3046
|
+
return /* @__PURE__ */ jsxs7(Box2, { flexDirection: "column", children: [
|
|
3047
|
+
/* @__PURE__ */ jsx8(Header, { tagline: "help" }),
|
|
3048
|
+
/* @__PURE__ */ jsx8(Text8, { children: " " }),
|
|
3049
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.bold("Keybindings") }),
|
|
3050
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" /<command> run a Noir sub-command (e.g. /status, /sync, /task next)") }),
|
|
3051
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" Enter run the typed /command") }),
|
|
3052
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" Esc back: clear input \u2192 dismiss output \u2192 quit") }),
|
|
3053
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" q quit (when the input is empty)") }),
|
|
3054
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" \u2191 / \u2193 scroll the output pane") }),
|
|
3055
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" ? toggle this help") }),
|
|
3056
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" Ctrl+K open the command palette") }),
|
|
3057
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" Ctrl+F find in the dispatched output pane") }),
|
|
3058
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" n / N next / previous match in search (Enter = next)") }),
|
|
3059
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" y / n approve / decline a destructive command prompt") }),
|
|
3060
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" Ctrl+C force exit") }),
|
|
3061
|
+
/* @__PURE__ */ jsx8(Text8, { children: " " }),
|
|
3062
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.bold("Commands") }),
|
|
3063
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" Dispatched through the same routing as `noir` at the prompt.") }),
|
|
3064
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(
|
|
2304
3065
|
" /status /sync /doctor /context search <q> /task next /memory recall <q>"
|
|
2305
3066
|
) }),
|
|
2306
|
-
/* @__PURE__ */
|
|
2307
|
-
/* @__PURE__ */
|
|
2308
|
-
/* @__PURE__ */
|
|
2309
|
-
/* @__PURE__ */
|
|
3067
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" Commands that need their own interactive prompts (e.g. a /sync with a") }),
|
|
3068
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim(" conflict) are best run directly \u2014 exit first with q.") }),
|
|
3069
|
+
/* @__PURE__ */ jsx8(Text8, { children: " " }),
|
|
3070
|
+
/* @__PURE__ */ jsx8(Text8, { children: c.dim("press ? / Esc / q to close this help") })
|
|
2310
3071
|
] });
|
|
2311
3072
|
}
|
|
2312
3073
|
|
|
3074
|
+
// src/tui/palette/history.ts
|
|
3075
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync } from "fs";
|
|
3076
|
+
import { homedir } from "os";
|
|
3077
|
+
import { join as join2 } from "path";
|
|
3078
|
+
import { atomicWriteFile } from "@noir-ai/core";
|
|
3079
|
+
var MAX_ENTRIES = 50;
|
|
3080
|
+
var TUI_HISTORY_DISABLE_ENV = "NOIR_DISABLE_TUI_HISTORY";
|
|
3081
|
+
var testHome = null;
|
|
3082
|
+
function homeDir() {
|
|
3083
|
+
return testHome ?? join2(homedir(), ".noir");
|
|
3084
|
+
}
|
|
3085
|
+
function historyFile(projectId) {
|
|
3086
|
+
return join2(homeDir(), projectId, "tui-history.json");
|
|
3087
|
+
}
|
|
3088
|
+
function isHistoryEnabled() {
|
|
3089
|
+
const v = process.env[TUI_HISTORY_DISABLE_ENV];
|
|
3090
|
+
return v === void 0 || v === "";
|
|
3091
|
+
}
|
|
3092
|
+
function loadRecent(projectId) {
|
|
3093
|
+
if (!isHistoryEnabled()) return [];
|
|
3094
|
+
const file = historyFile(projectId);
|
|
3095
|
+
try {
|
|
3096
|
+
if (!existsSync(file)) return [];
|
|
3097
|
+
const raw = readFileSync(file, "utf8");
|
|
3098
|
+
const parsed = JSON.parse(raw);
|
|
3099
|
+
if (!Array.isArray(parsed)) return [];
|
|
3100
|
+
const entries = [];
|
|
3101
|
+
for (const item of parsed) {
|
|
3102
|
+
if (typeof item !== "object" || item === null) continue;
|
|
3103
|
+
const argv = item.argv;
|
|
3104
|
+
const id = item.id;
|
|
3105
|
+
if (!Array.isArray(argv) || typeof id !== "string") continue;
|
|
3106
|
+
if (!argv.every((tok) => typeof tok === "string")) continue;
|
|
3107
|
+
entries.push({ argv, id });
|
|
3108
|
+
}
|
|
3109
|
+
return entries;
|
|
3110
|
+
} catch {
|
|
3111
|
+
return [];
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
function recordRecent(projectId, argv) {
|
|
3115
|
+
if (argv.length === 0) return;
|
|
3116
|
+
if (!isHistoryEnabled()) return;
|
|
3117
|
+
const entry = { argv, id: argv.join(" ") };
|
|
3118
|
+
const current = loadRecent(projectId).filter((e) => e.id !== entry.id);
|
|
3119
|
+
const next = [entry, ...current].slice(0, MAX_ENTRIES);
|
|
3120
|
+
try {
|
|
3121
|
+
const dir = join2(homeDir(), projectId);
|
|
3122
|
+
mkdirSync2(dir, { recursive: true });
|
|
3123
|
+
atomicWriteFile(historyFile(projectId), `${JSON.stringify(next, null, 2)}
|
|
3124
|
+
`);
|
|
3125
|
+
} catch {
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
|
|
2313
3129
|
// src/tui/index.tsx
|
|
2314
|
-
import { jsx as
|
|
3130
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
2315
3131
|
function defaultFetchStatus(opts) {
|
|
2316
3132
|
return async () => {
|
|
2317
3133
|
try {
|
|
@@ -2322,11 +3138,30 @@ function defaultFetchStatus(opts) {
|
|
|
2322
3138
|
};
|
|
2323
3139
|
}
|
|
2324
3140
|
async function runTui(opts, dispatch) {
|
|
3141
|
+
let projectId = null;
|
|
3142
|
+
try {
|
|
3143
|
+
projectId = loadProjectInfo8(process.cwd()).id;
|
|
3144
|
+
} catch {
|
|
3145
|
+
projectId = null;
|
|
3146
|
+
}
|
|
2325
3147
|
const deps = {
|
|
2326
3148
|
dispatch,
|
|
2327
|
-
fetchStatus: defaultFetchStatus(opts)
|
|
3149
|
+
fetchStatus: defaultFetchStatus(opts),
|
|
3150
|
+
// The palette source, derived from a fresh commander program at launch (B3).
|
|
3151
|
+
commands: buildPaletteCommandsForTui(),
|
|
3152
|
+
// C3 — persistent recent commands (projectId-keyed). recordRecent is async
|
|
3153
|
+
// only to match the TuiDeps seam; it never rejects.
|
|
3154
|
+
record: (argv) => {
|
|
3155
|
+
if (projectId) recordRecent(projectId, argv);
|
|
3156
|
+
return Promise.resolve();
|
|
3157
|
+
},
|
|
3158
|
+
loadRecent: async () => {
|
|
3159
|
+
if (!projectId) return [];
|
|
3160
|
+
const byId = new Map(deps.commands?.map((c2) => [c2.id, c2]) ?? []);
|
|
3161
|
+
return loadRecent(projectId).map((e) => byId.get(e.id)).filter((c2) => c2 !== void 0);
|
|
3162
|
+
}
|
|
2328
3163
|
};
|
|
2329
|
-
const instance = render(/* @__PURE__ */
|
|
3164
|
+
const instance = render(/* @__PURE__ */ jsx9(App, { deps }));
|
|
2330
3165
|
await instance.waitUntilExit();
|
|
2331
3166
|
}
|
|
2332
3167
|
export {
|