@gmickel/gno 1.43.0 → 1.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/skill/SKILL.md +3 -0
- package/assets/skill/recipes/memory-file-decision.md +76 -0
- package/assets/skill/recipes/memory-scoped-recall.md +66 -0
- package/assets/skill/recipes/memory-supersede-fact.md +68 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.43.0.zip → gno-browser-clipper-v1.45.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.45.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +89 -8
- package/spec/mcp.md +12 -0
- package/spec/output-schemas/changes-follow-event.schema.json +35 -0
- package/spec/output-schemas/index-receipt.schema.json +135 -0
- package/spec/output-schemas/process-status.schema.json +76 -0
- package/src/cli/commands/agents/block.ts +9 -8
- package/src/cli/commands/changes-follow.ts +167 -0
- package/src/cli/commands/changes.ts +63 -0
- package/src/cli/commands/daemon.ts +35 -0
- package/src/cli/commands/doctor.ts +71 -0
- package/src/cli/commands/embed.ts +236 -178
- package/src/cli/commands/index-cmd.ts +238 -57
- package/src/cli/program.ts +94 -4
- package/src/config/types.ts +48 -0
- package/src/core/capture-sync.ts +144 -0
- package/src/core/capture.ts +10 -0
- package/src/core/findings-records.ts +381 -0
- package/src/core/findings-run-state.ts +282 -0
- package/src/embed/stage-state.ts +199 -0
- package/src/mcp/tools/capture.ts +91 -136
- package/src/serve/capture-service.ts +227 -53
- package/src/serve/findings-pass.ts +335 -0
- package/src/serve/resident-runtime.ts +42 -0
- package/src/serve/routes/api.ts +14 -14
- package/browser-extension/artifacts/gno-browser-clipper-v1.43.0.zip.sha256 +0 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* gno index command implementation.
|
|
3
|
-
* Build or update the index end-to-end
|
|
3
|
+
* Build or update the index end-to-end as two separable, resumable stages:
|
|
4
|
+
* `lexical` (sync) then `embed`. Each stage reports its own receipt state and
|
|
5
|
+
* counts; the run exits 0 only when every attempted stage completed (fn-132).
|
|
4
6
|
*
|
|
5
7
|
* @module src/cli/commands/indexCmd
|
|
6
8
|
*/
|
|
@@ -10,11 +12,22 @@ import {
|
|
|
10
12
|
type WriteLeaseContention,
|
|
11
13
|
withCliWriteLease,
|
|
12
14
|
} from "../../core/write-lease";
|
|
15
|
+
import {
|
|
16
|
+
clearIndexStage,
|
|
17
|
+
findInterruptedStage,
|
|
18
|
+
formatInterruptedStage,
|
|
19
|
+
type IndexStageState,
|
|
20
|
+
type InterruptedStage,
|
|
21
|
+
markIndexStageFinished,
|
|
22
|
+
markIndexStageRunning,
|
|
23
|
+
readIndexStageState,
|
|
24
|
+
} from "../../embed/stage-state";
|
|
13
25
|
import {
|
|
14
26
|
defaultSyncService,
|
|
15
27
|
type SyncResult,
|
|
16
28
|
withContentTypeRules,
|
|
17
29
|
} from "../../ingestion";
|
|
30
|
+
import { type EmbedResult, embedStageOutcome } from "./embed";
|
|
18
31
|
import { formatSyncResultLines, initStore } from "./shared";
|
|
19
32
|
|
|
20
33
|
/**
|
|
@@ -41,22 +54,118 @@ export interface IndexOptions extends CliWriteLeaseOptions {
|
|
|
41
54
|
json?: boolean;
|
|
42
55
|
}
|
|
43
56
|
|
|
57
|
+
/** Receipt for the lexical (sync) stage. */
|
|
58
|
+
export interface LexicalStageReceipt {
|
|
59
|
+
state: IndexStageState;
|
|
60
|
+
filesProcessed: number;
|
|
61
|
+
filesAdded: number;
|
|
62
|
+
filesUpdated: number;
|
|
63
|
+
filesErrored: number;
|
|
64
|
+
filesSkipped: number;
|
|
65
|
+
durationMs: number;
|
|
66
|
+
error?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Receipt for the embed stage. */
|
|
70
|
+
export interface EmbedStageReceipt {
|
|
71
|
+
state: IndexStageState;
|
|
72
|
+
embedded: number;
|
|
73
|
+
errors: number;
|
|
74
|
+
contentionErrors: number;
|
|
75
|
+
durationMs: number;
|
|
76
|
+
/** Why the stage was skipped (`--no-embed`, lexical failure). */
|
|
77
|
+
reason?: string;
|
|
78
|
+
error?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface IndexStageReceipts {
|
|
82
|
+
lexical: LexicalStageReceipt;
|
|
83
|
+
embed: EmbedStageReceipt;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface IndexEmbedSummary {
|
|
87
|
+
embedded: number;
|
|
88
|
+
errors: number;
|
|
89
|
+
contentionErrors: number;
|
|
90
|
+
duration: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Receipt fields shared by successful and failed runs that reached a stage. */
|
|
94
|
+
export interface IndexReceipt {
|
|
95
|
+
stages: IndexStageReceipts;
|
|
96
|
+
/** Stage a previous run left `running`; null when the run started clean. */
|
|
97
|
+
resumedFrom: InterruptedStage | null;
|
|
98
|
+
syncResult?: SyncResult;
|
|
99
|
+
embedSkipped: boolean;
|
|
100
|
+
embedResult?: IndexEmbedSummary;
|
|
101
|
+
}
|
|
102
|
+
|
|
44
103
|
/**
|
|
45
|
-
* Result of index command.
|
|
104
|
+
* Result of index command. A failure that reached a stage carries the partial
|
|
105
|
+
* receipt (`stages` present); a failure before any stage (init, lease) does not.
|
|
46
106
|
*/
|
|
47
107
|
export type IndexResult =
|
|
108
|
+
| ({ success: true; syncResult: SyncResult } & IndexReceipt)
|
|
109
|
+
| ({ success: false; error: string } & IndexReceipt)
|
|
48
110
|
| {
|
|
49
|
-
success:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
111
|
+
success: false;
|
|
112
|
+
error: string;
|
|
113
|
+
contention?: WriteLeaseContention;
|
|
114
|
+
stages?: undefined;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const EMPTY_LEXICAL_COUNTS = {
|
|
118
|
+
filesProcessed: 0,
|
|
119
|
+
filesAdded: 0,
|
|
120
|
+
filesUpdated: 0,
|
|
121
|
+
filesErrored: 0,
|
|
122
|
+
filesSkipped: 0,
|
|
123
|
+
durationMs: 0,
|
|
124
|
+
} as const;
|
|
125
|
+
|
|
126
|
+
const EMPTY_EMBED_COUNTS = {
|
|
127
|
+
embedded: 0,
|
|
128
|
+
errors: 0,
|
|
129
|
+
contentionErrors: 0,
|
|
130
|
+
durationMs: 0,
|
|
131
|
+
} as const;
|
|
132
|
+
|
|
133
|
+
function lexicalReceipt(syncResult: SyncResult): LexicalStageReceipt {
|
|
134
|
+
return {
|
|
135
|
+
state: "completed",
|
|
136
|
+
filesProcessed: syncResult.totalFilesProcessed,
|
|
137
|
+
filesAdded: syncResult.totalFilesAdded,
|
|
138
|
+
filesUpdated: syncResult.totalFilesUpdated,
|
|
139
|
+
filesErrored: syncResult.totalFilesErrored,
|
|
140
|
+
filesSkipped: syncResult.totalFilesSkipped,
|
|
141
|
+
durationMs: syncResult.totalDurationMs,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function skippedEmbedReceipt(reason: string): EmbedStageReceipt {
|
|
146
|
+
return { state: "skipped", ...EMPTY_EMBED_COUNTS, reason };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function embedReceipt(result: EmbedResult): EmbedStageReceipt {
|
|
150
|
+
if (!result.success) {
|
|
151
|
+
return { state: "failed", ...EMPTY_EMBED_COUNTS, error: result.error };
|
|
152
|
+
}
|
|
153
|
+
const state = embedStageOutcome(result);
|
|
154
|
+
const error =
|
|
155
|
+
state === "failed"
|
|
156
|
+
? (result.syncError ??
|
|
157
|
+
result.errorSamples?.[0] ??
|
|
158
|
+
`${result.errors} chunks failed to embed`)
|
|
159
|
+
: undefined;
|
|
160
|
+
return {
|
|
161
|
+
state,
|
|
162
|
+
embedded: result.embedded,
|
|
163
|
+
errors: result.errors,
|
|
164
|
+
contentionErrors: result.contentionErrors,
|
|
165
|
+
durationMs: Math.round(result.duration * 1000),
|
|
166
|
+
...(error ? { error } : {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
60
169
|
|
|
61
170
|
/**
|
|
62
171
|
* Execute gno index command.
|
|
@@ -73,52 +182,103 @@ export async function index(options: IndexOptions = {}): Promise<IndexResult> {
|
|
|
73
182
|
}
|
|
74
183
|
|
|
75
184
|
const { store, collections, config } = initResult;
|
|
185
|
+
const db = store.getRawDb();
|
|
186
|
+
const embedSkipped = options.noEmbed ?? false;
|
|
76
187
|
|
|
77
188
|
try {
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
189
|
+
// Resume preamble (fn-132 R4): report a stage the previous run left
|
|
190
|
+
// `running` before this run overwrites its marker.
|
|
191
|
+
const resumedFrom = findInterruptedStage(readIndexStageState(db));
|
|
192
|
+
if (resumedFrom && !options.json) {
|
|
193
|
+
process.stderr.write(`${formatInterruptedStage(resumedFrom)}\n`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Lexical stage (sync). Per-file errors are counted, not fatal; only a
|
|
197
|
+
// sync that cannot run at all fails the stage.
|
|
198
|
+
markIndexStageRunning(db, "lexical", { collection: options.collection });
|
|
199
|
+
let syncResult: SyncResult;
|
|
200
|
+
try {
|
|
201
|
+
syncResult = await defaultSyncService.syncAll(
|
|
202
|
+
collections,
|
|
203
|
+
store,
|
|
204
|
+
withContentTypeRules(
|
|
205
|
+
{ gitPull: options.gitPull, runUpdateCmd: true },
|
|
206
|
+
config
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
211
|
+
markIndexStageFinished(db, "lexical", "failed");
|
|
212
|
+
return {
|
|
213
|
+
success: false,
|
|
214
|
+
error: `Lexical sync failed: ${message}`,
|
|
215
|
+
stages: {
|
|
216
|
+
lexical: {
|
|
217
|
+
state: "failed",
|
|
218
|
+
...EMPTY_LEXICAL_COUNTS,
|
|
219
|
+
error: message,
|
|
220
|
+
},
|
|
221
|
+
embed: skippedEmbedReceipt("lexical stage failed"),
|
|
86
222
|
},
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
223
|
+
resumedFrom,
|
|
224
|
+
embedSkipped,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
markIndexStageFinished(db, "lexical", "completed");
|
|
228
|
+
const lexical = lexicalReceipt(syncResult);
|
|
90
229
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
230
|
+
if (embedSkipped) {
|
|
231
|
+
// A stale `embed: running` marker (killed embed run) was surfaced in
|
|
232
|
+
// this run's preamble; settle it so the next run does not repeat it
|
|
233
|
+
// or mask a real lexical interruption. Embed progress stays on disk.
|
|
234
|
+
if (resumedFrom?.stage === "embed") {
|
|
235
|
+
clearIndexStage(db, "embed");
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
success: true,
|
|
239
|
+
syncResult,
|
|
240
|
+
embedSkipped,
|
|
241
|
+
stages: { lexical, embed: skippedEmbedReceipt("--no-embed") },
|
|
242
|
+
resumedFrom,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Embed stage. embed() owns its own persisted marker.
|
|
247
|
+
const { embed } = await import("./embed");
|
|
248
|
+
const result = await embed({
|
|
249
|
+
configPath: options.configPath,
|
|
250
|
+
indexName: options.indexName,
|
|
251
|
+
collection: options.collection,
|
|
252
|
+
verbose: options.verbose,
|
|
253
|
+
json: options.json,
|
|
254
|
+
skipWriteLease: true,
|
|
255
|
+
resumedFrom,
|
|
256
|
+
});
|
|
257
|
+
const embedStage = embedReceipt(result);
|
|
258
|
+
const embedResult: IndexEmbedSummary | undefined = result.success
|
|
259
|
+
? {
|
|
113
260
|
embedded: result.embedded,
|
|
114
261
|
errors: result.errors,
|
|
115
262
|
contentionErrors: result.contentionErrors,
|
|
116
263
|
duration: result.duration,
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
264
|
+
}
|
|
265
|
+
: undefined;
|
|
266
|
+
const receipt: IndexReceipt = {
|
|
267
|
+
stages: { lexical, embed: embedStage },
|
|
268
|
+
resumedFrom,
|
|
269
|
+
syncResult,
|
|
270
|
+
embedSkipped,
|
|
271
|
+
...(embedResult ? { embedResult } : {}),
|
|
272
|
+
};
|
|
120
273
|
|
|
121
|
-
|
|
274
|
+
if (embedStage.state !== "completed") {
|
|
275
|
+
return {
|
|
276
|
+
success: false,
|
|
277
|
+
error: `Embed stage failed: ${embedStage.error ?? "unknown error"}`,
|
|
278
|
+
...receipt,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return { success: true, syncResult, ...receipt };
|
|
122
282
|
} finally {
|
|
123
283
|
await store.close();
|
|
124
284
|
}
|
|
@@ -141,14 +301,18 @@ export function formatIndex(
|
|
|
141
301
|
return `${mins}m ${secs.toFixed(0)}s`;
|
|
142
302
|
}
|
|
143
303
|
|
|
144
|
-
if (!result.success) {
|
|
304
|
+
if (!result.success && !result.stages) {
|
|
145
305
|
return `Error: ${result.error}`;
|
|
146
306
|
}
|
|
147
307
|
|
|
148
308
|
if (options.json) {
|
|
149
309
|
return JSON.stringify(
|
|
150
310
|
{
|
|
151
|
-
|
|
311
|
+
success: result.success,
|
|
312
|
+
...(result.success ? {} : { error: result.error }),
|
|
313
|
+
stages: result.stages,
|
|
314
|
+
resumedFrom: result.resumedFrom,
|
|
315
|
+
...(result.syncResult ? { syncResult: result.syncResult } : {}),
|
|
152
316
|
embedSkipped: result.embedSkipped,
|
|
153
317
|
...(result.embedResult ? { embedResult: result.embedResult } : {}),
|
|
154
318
|
},
|
|
@@ -156,16 +320,28 @@ export function formatIndex(
|
|
|
156
320
|
2
|
|
157
321
|
);
|
|
158
322
|
}
|
|
159
|
-
const { syncResult, embedSkipped } = result;
|
|
160
|
-
const lines: string[] = ["Indexing complete.", ""];
|
|
161
323
|
|
|
162
|
-
|
|
324
|
+
const { stages, embedSkipped } = result;
|
|
325
|
+
const lines: string[] = [
|
|
326
|
+
result.success ? "Indexing complete." : "Indexing failed.",
|
|
327
|
+
"",
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
if (result.syncResult) {
|
|
331
|
+
lines.push(...formatSyncResultLines(result.syncResult, options));
|
|
332
|
+
} else {
|
|
333
|
+
lines.push(`Lexical stage ${stages.lexical.state}.`);
|
|
334
|
+
}
|
|
335
|
+
if (stages.lexical.error) {
|
|
336
|
+
lines.push(`Lexical error: ${stages.lexical.error}`);
|
|
337
|
+
}
|
|
163
338
|
|
|
339
|
+
lines.push("");
|
|
164
340
|
if (embedSkipped) {
|
|
165
|
-
lines.push("");
|
|
166
341
|
lines.push("Embedding skipped (--no-embed)");
|
|
342
|
+
} else if (stages.embed.state === "skipped") {
|
|
343
|
+
lines.push(`Embedding skipped (${stages.embed.reason ?? "not attempted"})`);
|
|
167
344
|
} else if (result.embedResult) {
|
|
168
|
-
lines.push("");
|
|
169
345
|
const { embedded, errors, contentionErrors, duration } = result.embedResult;
|
|
170
346
|
lines.push(
|
|
171
347
|
`Embedded ${embedded.toLocaleString()} chunks in ${formatDuration(duration)}`
|
|
@@ -179,6 +355,11 @@ export function formatIndex(
|
|
|
179
355
|
);
|
|
180
356
|
}
|
|
181
357
|
}
|
|
358
|
+
if (stages.embed.state === "failed") {
|
|
359
|
+
lines.push(
|
|
360
|
+
`Embed stage failed: ${stages.embed.error ?? "unknown error"}. Lexical index is intact; rerun \`gno embed\` to resume from persisted progress.`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
182
363
|
|
|
183
364
|
return lines.join("\n");
|
|
184
365
|
}
|
package/src/cli/program.ts
CHANGED
|
@@ -20,6 +20,11 @@ import {
|
|
|
20
20
|
} from "../app/constants";
|
|
21
21
|
import { INDEX_NAME_REQUIREMENTS, isValidIndexName } from "../app/index-name";
|
|
22
22
|
import { resolveDepthPolicy } from "../core/depth-policy";
|
|
23
|
+
import {
|
|
24
|
+
findingsRunStatePathForIndex,
|
|
25
|
+
formatFindingsRunStatusLine,
|
|
26
|
+
readFindingsRunStatus,
|
|
27
|
+
} from "../core/findings-run-state";
|
|
23
28
|
import { parseAndValidateTagFilter } from "../core/tags";
|
|
24
29
|
import {
|
|
25
30
|
formatWriteLeaseBusyJson,
|
|
@@ -1630,7 +1635,14 @@ function wireOnboardingCommands(program: Command): void {
|
|
|
1630
1635
|
|
|
1631
1636
|
if (!result.success) {
|
|
1632
1637
|
throwIfWriteLeaseBusy(result, opts.json);
|
|
1633
|
-
|
|
1638
|
+
if (!result.stages) {
|
|
1639
|
+
throw new CliError("RUNTIME", result.error ?? "Index failed");
|
|
1640
|
+
}
|
|
1641
|
+
// A stage failed after the run started: emit the partial per-stage
|
|
1642
|
+
// receipt, then exit non-zero (fn-132 R4 - never exit 0 on a failed
|
|
1643
|
+
// embed stage).
|
|
1644
|
+
process.stdout.write(`${formatIndex(result, opts)}\n`);
|
|
1645
|
+
throw new CliError("RUNTIME", result.error, { silent: true });
|
|
1634
1646
|
}
|
|
1635
1647
|
process.stdout.write(`${formatIndex(result, opts)}\n`);
|
|
1636
1648
|
if ((result.embedResult?.contentionErrors ?? 0) > 0) {
|
|
@@ -4094,25 +4106,89 @@ function wireGraphCommand(program: Command): void {
|
|
|
4094
4106
|
);
|
|
4095
4107
|
}
|
|
4096
4108
|
|
|
4109
|
+
/**
|
|
4110
|
+
* `gno changes --follow --jsonl`: the follow flags form one mode that
|
|
4111
|
+
* excludes the one-shot listing flags. The cursor-expiry record is the
|
|
4112
|
+
* stdout envelope, so its non-zero exit is silent on stderr.
|
|
4113
|
+
*/
|
|
4114
|
+
async function runChangesFollow(
|
|
4115
|
+
cmdOpts: Record<string, unknown>,
|
|
4116
|
+
format: string,
|
|
4117
|
+
globals: GlobalOptions
|
|
4118
|
+
): Promise<void> {
|
|
4119
|
+
if (!cmdOpts.follow || !cmdOpts.jsonl) {
|
|
4120
|
+
throw new CliError(
|
|
4121
|
+
"VALIDATION",
|
|
4122
|
+
"--follow and --jsonl must be used together (--cursor requires both)"
|
|
4123
|
+
);
|
|
4124
|
+
}
|
|
4125
|
+
if (format === "json" || cmdOpts.since !== undefined) {
|
|
4126
|
+
throw new CliError(
|
|
4127
|
+
"VALIDATION",
|
|
4128
|
+
"--follow cannot be combined with --json or --since; use --cursor to resume"
|
|
4129
|
+
);
|
|
4130
|
+
}
|
|
4131
|
+
if (cmdOpts.limit !== undefined) {
|
|
4132
|
+
throw new CliError(
|
|
4133
|
+
"VALIDATION",
|
|
4134
|
+
"--follow cannot be combined with --limit"
|
|
4135
|
+
);
|
|
4136
|
+
}
|
|
4137
|
+
const { changesFollow } = await import("./commands/changes");
|
|
4138
|
+
const result = await changesFollow(
|
|
4139
|
+
{
|
|
4140
|
+
cursor: cmdOpts.cursor as string | undefined,
|
|
4141
|
+
collection: cmdOpts.collection as string | undefined,
|
|
4142
|
+
},
|
|
4143
|
+
{ configPath: globals.config, indexName: globals.index }
|
|
4144
|
+
);
|
|
4145
|
+
if (result.status === "error") {
|
|
4146
|
+
throw new CliError(
|
|
4147
|
+
result.isValidation ? "VALIDATION" : "RUNTIME",
|
|
4148
|
+
result.error
|
|
4149
|
+
);
|
|
4150
|
+
}
|
|
4151
|
+
if (result.status === "expired") {
|
|
4152
|
+
throw new CliError(
|
|
4153
|
+
"RUNTIME",
|
|
4154
|
+
`Follow cursor expired; earliest retained cursor is ${result.earliestCursor}`,
|
|
4155
|
+
{ silent: true }
|
|
4156
|
+
);
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
|
|
4097
4160
|
function wireKnowledgeDeltaCommands(program: Command): void {
|
|
4098
4161
|
program
|
|
4099
4162
|
.command("changes")
|
|
4100
4163
|
.description("List retained metadata-only document changes")
|
|
4101
4164
|
.option("--since <time-or-cursor>", "ISO-8601 time or opaque cursor")
|
|
4102
4165
|
.option("-c, --collection <name>", "filter by collection")
|
|
4103
|
-
.option("-n, --limit <num>", "maximum changes
|
|
4166
|
+
.option("-n, --limit <num>", "maximum changes (default 100)")
|
|
4104
4167
|
.option("--json", "JSON output")
|
|
4168
|
+
.option(
|
|
4169
|
+
"--follow",
|
|
4170
|
+
"stream new changes as they land (requires --jsonl; SIGINT exits 0)"
|
|
4171
|
+
)
|
|
4172
|
+
.option("--jsonl", "one JSON object per line (only with --follow)")
|
|
4173
|
+
.option(
|
|
4174
|
+
"--cursor <cursor>",
|
|
4175
|
+
"resume a --follow stream from a persisted postCursor"
|
|
4176
|
+
)
|
|
4105
4177
|
.action(async (cmdOpts: Record<string, unknown>) => {
|
|
4106
4178
|
const format = getFormat(cmdOpts);
|
|
4107
4179
|
assertFormatSupported(CMD.changes, format);
|
|
4108
4180
|
const deltaFormat = format === "json" ? "json" : "terminal";
|
|
4109
4181
|
const globals = getGlobals();
|
|
4182
|
+
if (cmdOpts.follow || cmdOpts.jsonl || cmdOpts.cursor !== undefined) {
|
|
4183
|
+
await runChangesFollow(cmdOpts, format, globals);
|
|
4184
|
+
return;
|
|
4185
|
+
}
|
|
4110
4186
|
const { changes, formatChanges } = await import("./commands/changes");
|
|
4111
4187
|
const result = await changes(
|
|
4112
4188
|
{
|
|
4113
4189
|
since: cmdOpts.since as string | undefined,
|
|
4114
4190
|
collection: cmdOpts.collection as string | undefined,
|
|
4115
|
-
limit: parsePositiveInt("limit", cmdOpts.limit),
|
|
4191
|
+
limit: parsePositiveInt("limit", cmdOpts.limit ?? "100"),
|
|
4116
4192
|
},
|
|
4117
4193
|
{ configPath: globals.config, indexName: globals.index }
|
|
4118
4194
|
);
|
|
@@ -4345,6 +4421,7 @@ async function handleDaemonAction(
|
|
|
4345
4421
|
await runDaemonStatus({
|
|
4346
4422
|
paths,
|
|
4347
4423
|
json,
|
|
4424
|
+
indexName: globals.index,
|
|
4348
4425
|
statusProcess,
|
|
4349
4426
|
inspectForeignLive,
|
|
4350
4427
|
});
|
|
@@ -4412,6 +4489,7 @@ async function handleDaemonAction(
|
|
|
4412
4489
|
interface DaemonStatusDeps {
|
|
4413
4490
|
paths: { pidFile: string; logFile: string };
|
|
4414
4491
|
json: boolean;
|
|
4492
|
+
indexName?: string;
|
|
4415
4493
|
statusProcess: typeof import("./detach.js").statusProcess;
|
|
4416
4494
|
inspectForeignLive: typeof import("./detach.js").inspectForeignLive;
|
|
4417
4495
|
}
|
|
@@ -4426,9 +4504,16 @@ async function runDaemonStatus(deps: DaemonStatusDeps): Promise<void> {
|
|
|
4426
4504
|
kind: "daemon",
|
|
4427
4505
|
pidFile: deps.paths.pidFile,
|
|
4428
4506
|
});
|
|
4507
|
+
// Persisted by the daemon after every scheduled findings attempt; absent
|
|
4508
|
+
// (null) when the pass is not configured. Read from disk, never live.
|
|
4509
|
+
const findings = await readFindingsRunStatus(
|
|
4510
|
+
findingsRunStatePathForIndex(deps.indexName)
|
|
4511
|
+
);
|
|
4429
4512
|
|
|
4430
4513
|
if (deps.json) {
|
|
4431
|
-
process.stdout.write(
|
|
4514
|
+
process.stdout.write(
|
|
4515
|
+
`${JSON.stringify({ ...status, findings }, null, 2)}\n`
|
|
4516
|
+
);
|
|
4432
4517
|
// In JSON mode, foreign-live metadata flows into the NOT_RUNNING
|
|
4433
4518
|
// envelope's `details` payload below so stderr stays a single JSON
|
|
4434
4519
|
// object that machine clients can parse deterministically.
|
|
@@ -4457,6 +4542,11 @@ async function runDaemonStatus(deps: DaemonStatusDeps): Promise<void> {
|
|
|
4457
4542
|
} else {
|
|
4458
4543
|
process.stdout.write(` (${status.log_size_bytes} bytes)\n`);
|
|
4459
4544
|
}
|
|
4545
|
+
if (findings) {
|
|
4546
|
+
process.stdout.write(
|
|
4547
|
+
` findings ${formatFindingsRunStatusLine(findings)}\n`
|
|
4548
|
+
);
|
|
4549
|
+
}
|
|
4460
4550
|
|
|
4461
4551
|
if (foreign) {
|
|
4462
4552
|
// Terminal mode: emit the operator-facing warning on stderr. JSON
|
package/src/config/types.ts
CHANGED
|
@@ -472,6 +472,51 @@ export const HttpGatewayConfigSchema = z.object({
|
|
|
472
472
|
|
|
473
473
|
export type HttpGatewayConfig = z.infer<typeof HttpGatewayConfigSchema>;
|
|
474
474
|
|
|
475
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
476
|
+
// Scheduled findings pass (daemon-only, opt-in, report-only)
|
|
477
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
/** `<integer><unit>` with unit s|m|h|d, e.g. `30m`, `6h`, `1d`. */
|
|
480
|
+
export const FINDINGS_CADENCE_PATTERN =
|
|
481
|
+
/^(?<value>[1-9]\d{0,5})(?<unit>[smhd])$/;
|
|
482
|
+
export const DEFAULT_FINDINGS_CADENCE = "6h";
|
|
483
|
+
export const MIN_FINDINGS_CADENCE_MS = 10_000;
|
|
484
|
+
export const MAX_FINDINGS_CADENCE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
485
|
+
|
|
486
|
+
const CADENCE_UNIT_MS: Record<string, number> = {
|
|
487
|
+
s: 1_000,
|
|
488
|
+
m: 60_000,
|
|
489
|
+
h: 3_600_000,
|
|
490
|
+
d: 86_400_000,
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
/** Parse a findings cadence into milliseconds; null when malformed or out of range. */
|
|
494
|
+
export function parseFindingsCadenceMs(raw: string): number | null {
|
|
495
|
+
const match = FINDINGS_CADENCE_PATTERN.exec(raw.trim());
|
|
496
|
+
const value = match?.groups?.value;
|
|
497
|
+
const unit = match?.groups?.unit;
|
|
498
|
+
if (!value || !unit) return null;
|
|
499
|
+
const ms = Number(value) * (CADENCE_UNIT_MS[unit] ?? 0);
|
|
500
|
+
if (ms < MIN_FINDINGS_CADENCE_MS || ms > MAX_FINDINGS_CADENCE_MS) return null;
|
|
501
|
+
return ms;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export const FindingsConfigSchema = z.object({
|
|
505
|
+
/** Off by default; the daemon never audits on cadence unless asked. */
|
|
506
|
+
enabled: z.boolean().default(false),
|
|
507
|
+
/** Run interval (`10s`..`30d`). Evaluated only when enabled. */
|
|
508
|
+
cadence: z
|
|
509
|
+
.string()
|
|
510
|
+
.refine((value) => parseFindingsCadenceMs(value) !== null, {
|
|
511
|
+
message: "cadence must be <n>s|m|h|d between 10s and 30d (e.g. 6h)",
|
|
512
|
+
})
|
|
513
|
+
.default(DEFAULT_FINDINGS_CADENCE),
|
|
514
|
+
/** Name of an already-configured collection that receives findings records. */
|
|
515
|
+
collection: z.string().regex(COLLECTION_NAME_REGEX).optional(),
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
export type FindingsConfig = z.infer<typeof FindingsConfigSchema>;
|
|
519
|
+
|
|
475
520
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
476
521
|
// Content Type Schema
|
|
477
522
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -556,6 +601,9 @@ export const ConfigSchema = z.object({
|
|
|
556
601
|
/** Resident Streamable HTTP MCP gateway configuration */
|
|
557
602
|
gateway: HttpGatewayConfigSchema.optional(),
|
|
558
603
|
|
|
604
|
+
/** Daemon-only scheduled read-only audit writing findings records. Absent means off. */
|
|
605
|
+
findings: FindingsConfigSchema.optional(),
|
|
606
|
+
|
|
559
607
|
/** Private local retrieval trace recording. Absent means recording off. */
|
|
560
608
|
retrievalTraces: RetrievalTraceConfigSchema.optional(),
|
|
561
609
|
|