knodin 0.7.5 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -3
- package/benchmarks/competitors/SYNTHESIS.md +66 -0
- package/dist/bin/cli.js +371 -66
- package/dist/bin/launcher.js +16 -1
- package/dist/src/agent-integration.js +82 -16
- package/dist/src/artifact-refresh.js +2 -1
- package/dist/src/cli-args.js +19 -1
- package/dist/src/cli-model.js +28 -2
- package/dist/src/codeflow-replay.js +2 -1
- package/dist/src/compare.js +39 -0
- package/dist/src/competitive-constraints.js +2 -1
- package/dist/src/competitive-runner.js +4 -4
- package/dist/src/context-export.js +3 -2
- package/dist/src/context.js +1 -1
- package/dist/src/deterministic-random.js +34 -0
- package/dist/src/diagnostics-write-helper.js +473 -0
- package/dist/src/diagnostics.js +1160 -133
- package/dist/src/doctor.js +3 -1
- package/dist/src/engine/ann-hnsw.js +2 -12
- package/dist/src/engine/file-walker.js +8 -2
- package/dist/src/engine/git-history.js +12 -12
- package/dist/src/engine/index.js +1174 -313
- package/dist/src/engine/sarif-import.js +341 -0
- package/dist/src/engine/scip-import.js +28 -13
- package/dist/src/engine/source-policy.js +16 -0
- package/dist/src/engine/state-paths.js +175 -0
- package/dist/src/execution-profile.js +15 -10
- package/dist/src/failure-diagnosis.js +7 -1
- package/dist/src/graph-layout.js +173 -0
- package/dist/src/index-activity.js +2 -1
- package/dist/src/init.js +86 -45
- package/dist/src/lifecycle-health.js +41 -9
- package/dist/src/mcp-graph-worker.js +69 -0
- package/dist/src/mcp-reliability.js +154 -0
- package/dist/src/mcp-worker-supervisor.js +350 -0
- package/dist/src/mirror.js +290 -0
- package/dist/src/node-runtime.js +157 -0
- package/dist/src/output-compression.js +2 -1
- package/dist/src/output-telemetry.js +16 -11
- package/dist/src/progressive-evidence.js +30 -26
- package/dist/src/pure-compression-cli.js +4 -3
- package/dist/src/relationship-adapters.js +15 -8
- package/dist/src/release-preflight.js +13 -10
- package/dist/src/repair-lease.js +85 -0
- package/dist/src/repository-init-process.js +13 -9
- package/dist/src/repository-management.js +34 -4
- package/dist/src/response-budget.js +8 -6
- package/dist/src/server.js +80 -35
- package/dist/src/structural-fast-path.js +16 -10
- package/dist/src/structural-snapshot.js +6 -2
- package/dist/src/system-config.js +25 -2
- package/dist/src/tools/knodin-tools.js +142 -31
- package/dist/src/update-ceremony.js +9 -5
- package/dist/src/update-trust.js +5 -4
- package/dist/src/visualization.js +372 -19
- package/dist/src/worktree-lifecycle.js +5 -2
- package/docs/BEHAVIORAL-CONTRACT.md +72 -0
- package/docs/CLI.md +20 -1
- package/docs/COMPARISON.md +403 -0
- package/docs/COMPETITIVE-LANDSCAPE-2026-08.md +267 -0
- package/docs/DIAGNOSTICS.md +46 -11
- package/docs/HANDOFF.md +180 -0
- package/docs/INSTALLATION.md +21 -2
- package/docs/MCP.md +59 -8
- package/docs/PT-ACCESS-RECOMMENDATION.md +5 -7
- package/docs/REPOSITORIES-AND-WORKTREES.md +18 -6
- package/docs/SCIP-IMPORT.md +5 -0
- package/docs/TOKEN-OPTIMIZER-SCORECARD.md +79 -0
- package/docs/releases/0.5.1.md +4 -4
- package/docs/releases/0.8.0.md +74 -0
- package/docs/releases/0.8.2.md +34 -0
- package/package.json +17 -4
- package/roadmap/competitive-roadmap.md +3801 -0
- package/schemas/release-attestation-v1.schema.json +1 -1
- package/schemas/support-bundle-v2.schema.json +212 -0
package/dist/bin/cli.js
CHANGED
|
@@ -22,10 +22,11 @@ import { checkIndexed, extractPositionals, extractRepoFlag, parseReviewArgs, pla
|
|
|
22
22
|
import { helpCommandPath, parseCliInvocation, renderCliHelp } from "../src/cli-model.js";
|
|
23
23
|
import { buildKnodinContext } from "../src/context.js";
|
|
24
24
|
import { exportContext, grepPackedArtifact, readPackedArtifact } from "../src/context-export.js";
|
|
25
|
-
import { clearDiagnostics, collectDiagnostics, diagnosticsStatus, disableDiagnostics, enableDiagnostics, inspectDiagnosticsBundle, recordDiagnosticFailure, } from "../src/diagnostics.js";
|
|
25
|
+
import { clearDiagnostics, collectDiagnostics, diagnosticsStatus, disableDiagnostics, enableDiagnostics, inspectDiagnosticsBundle, persistDiagnosticsPreview, recordDiagnosticFailure, } from "../src/diagnostics.js";
|
|
26
26
|
import { getDocSection, listDocTopics } from "../src/docs-sections.js";
|
|
27
27
|
import { diagnoseInstallation } from "../src/doctor.js";
|
|
28
28
|
import { createEngine, REPO_WIDE_QUERY_PATTERNS, } from "../src/engine/index.js";
|
|
29
|
+
import { resolveDbPath } from "../src/engine/state-paths.js";
|
|
29
30
|
import { diagnoseFailure, } from "../src/failure-diagnosis.js";
|
|
30
31
|
import { gitExecutable } from "../src/git-executable.js";
|
|
31
32
|
import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../src/graph-query-health.js";
|
|
@@ -33,6 +34,7 @@ import { createIndexActivityReporter } from "../src/index-activity.js";
|
|
|
33
34
|
import { detectTrackedTeamIntegration, InitializationHealthError, initializeRepository, inspectRepositoryIntegrationStatus, readRepositoryIntegrationConfig, refreshFromGitEvent, } from "../src/init.js";
|
|
34
35
|
import { createInitProgressRenderer } from "../src/init-progress.js";
|
|
35
36
|
import { attachLifecycleHealth, attachRepairLifecycle } from "../src/lifecycle-health.js";
|
|
37
|
+
import { addMirror, listMirrors, refreshMirror, removeMirror } from "../src/mirror.js";
|
|
36
38
|
import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../src/output-compression.js";
|
|
37
39
|
import { clearTelemetry, exportTelemetry, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../src/output-telemetry.js";
|
|
38
40
|
import { auditPullRequests } from "../src/pr-triage.js";
|
|
@@ -41,10 +43,10 @@ import { createRepairPlan, createRepairProgressRenderer, parseRepairCliArgs, res
|
|
|
41
43
|
import { runRepositoryInitializationProcess } from "../src/repository-init-process.js";
|
|
42
44
|
import { detectRepositorySignals, discoverRepositories, formatRepositoryHuman, initializeRepositories, inventoryRepository, parseFleetInitArgs, parseRepositoryCommandArgs, repositorySignalInspectionLimit, searchRepositories, withRepositorySignals, } from "../src/repository-management.js";
|
|
43
45
|
import { applyResponseBudget } from "../src/response-budget.js";
|
|
44
|
-
import { enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../src/system-config.js";
|
|
46
|
+
import { configuredRepositoryInitMemoryLimitBytes, enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../src/system-config.js";
|
|
45
47
|
import { applyTrustedUpdate, checkTrustedUpdate, claimScheduledUpdateCheck, detectUpdateInstallMethod, explainTrustedUpdate, releaseScheduledUpdateCheck, rollbackTrustedUpdate, trustedUpdateStatus, } from "../src/update-policy.js";
|
|
46
48
|
import { KNODIN_VERSION } from "../src/version.js";
|
|
47
|
-
import { writeVisualization } from "../src/visualization.js";
|
|
49
|
+
import { writeVisualization, } from "../src/visualization.js";
|
|
48
50
|
import { waitForFresh } from "../src/wait-for-fresh.js";
|
|
49
51
|
import { inspectWorktrees, reconcileWorktrees, removeManagedWorktree, } from "../src/worktree-lifecycle.js";
|
|
50
52
|
function explicitScope(args) {
|
|
@@ -102,11 +104,11 @@ function integrationAgents(repo) {
|
|
|
102
104
|
return [...new Set([...detectSupportedAgents(), ...previous, ...repositoryDetected])];
|
|
103
105
|
}
|
|
104
106
|
function formatInitHuman(result) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
let agents = `${result.paths.scope} — no supported coding agents detected`;
|
|
108
|
+
if (result.paths.scope === "cli-only")
|
|
109
|
+
agents = "CLI-only — AI agents are not configured to discover knodin";
|
|
110
|
+
else if (result.paths.agentIntegration.configured.length > 0)
|
|
111
|
+
agents = `${result.paths.scope} — ${result.paths.agentIntegration.configured.join(", ")}`;
|
|
110
112
|
const failures = result.paths.agentIntegration.failed
|
|
111
113
|
.map(({ agent, message }) => `\nAgent warning (${agent}): ${message}`)
|
|
112
114
|
.join("");
|
|
@@ -174,8 +176,220 @@ function formatIndexVerificationError(result) {
|
|
|
174
176
|
const detail = firstIssue ? ` First issue: ${firstIssue}.` : "";
|
|
175
177
|
return `knodin index: requested work completed, but ${result.verification.issueCount.toLocaleString()} graph issue(s) remain.${detail} Run \`knodin repair\`.\n`;
|
|
176
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* The hooks/lifecycle line of `status`.
|
|
181
|
+
*
|
|
182
|
+
* A mirror has no hooks by design, so neither "installed" nor "degraded" is true
|
|
183
|
+
* of it: claiming they are "installed and executable" would describe a directory
|
|
184
|
+
* containing none, and reporting "degraded" would demand a `knodin init` that
|
|
185
|
+
* must never run there.
|
|
186
|
+
*/
|
|
187
|
+
function formatLifecycleLine(lifecycle, isMirror) {
|
|
188
|
+
if (!lifecycle)
|
|
189
|
+
return "";
|
|
190
|
+
if (isMirror)
|
|
191
|
+
return "Hooks: not applicable; a mirror is refreshed explicitly, not by Git events.\n";
|
|
192
|
+
if (lifecycle.status === "healthy")
|
|
193
|
+
return "Hooks: installed and executable.\n";
|
|
194
|
+
const issue = lifecycle.issues[0] ?? "refresh capability is not verified";
|
|
195
|
+
return `Lifecycle refresh: ${lifecycle.status}; ${issue}. Run \`knodin init\`.\n`;
|
|
196
|
+
}
|
|
197
|
+
/** Renders the top extensions of a tally as `.md 180, .yml 74, +3 more`. */
|
|
198
|
+
function formatTally(counts, limit = 4) {
|
|
199
|
+
const entries = Object.entries(counts).sort((left, right) => right[1] - left[1]);
|
|
200
|
+
const shown = entries.slice(0, limit).map(([ext, count]) => `${ext} ${count}`);
|
|
201
|
+
const remaining = entries.length - shown.length;
|
|
202
|
+
if (remaining > 0)
|
|
203
|
+
shown.push(`+${remaining} more`);
|
|
204
|
+
return shown.join(", ");
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* One clause naming what the graph does not cover. Silence here would let a
|
|
208
|
+
* language gap read as an absence of facts, so this is appended to the coverage
|
|
209
|
+
* line whenever anything was skipped.
|
|
210
|
+
*/
|
|
211
|
+
function formatCoverageGaps(skipped) {
|
|
212
|
+
if (!skipped)
|
|
213
|
+
return "";
|
|
214
|
+
// A zero total is only reportable as "no gaps" when the tally was actually
|
|
215
|
+
// read. If it wasn't, staying silent would state full coverage on the
|
|
216
|
+
// strength of a measurement that never happened.
|
|
217
|
+
if (skipped.total === 0 && !skipped.unparsedUnknown)
|
|
218
|
+
return "";
|
|
219
|
+
const clauses = [];
|
|
220
|
+
const byExtension = formatTally(skipped.byExtension);
|
|
221
|
+
if (byExtension)
|
|
222
|
+
clauses.push(`not indexed: ${byExtension}`);
|
|
223
|
+
const unparsed = formatTally(skipped.unparsedByExtension);
|
|
224
|
+
if (unparsed)
|
|
225
|
+
clauses.push(`parsed but empty: ${unparsed}`);
|
|
226
|
+
if (skipped.unparsedUnknown)
|
|
227
|
+
clauses.push("parsed-but-empty tally unavailable — run a full index; this is a lower bound");
|
|
228
|
+
const count = skipped.unparsedUnknown ? `${skipped.total}+` : `${skipped.total}`;
|
|
229
|
+
return `. Not in graph: ${count} files (${clauses.join("; ")})`;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Recursive on-disk size of a directory.
|
|
233
|
+
*
|
|
234
|
+
* Reports unreadable entries rather than treating them as zero. The listing
|
|
235
|
+
* exists so acquired disk is auditable; a mirror that cannot be measured showing
|
|
236
|
+
* a confident `0 B` invites exactly the wrong conclusion — that it is empty and
|
|
237
|
+
* safe to remove — which is the same absence-as-fact error this whole feature
|
|
238
|
+
* is built to avoid.
|
|
239
|
+
*/
|
|
240
|
+
function directorySizeBytes(target) {
|
|
241
|
+
let bytes = 0;
|
|
242
|
+
let partial = false;
|
|
243
|
+
let entries;
|
|
244
|
+
try {
|
|
245
|
+
entries = fs.readdirSync(target, { withFileTypes: true });
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
return { bytes: 0, partial: true };
|
|
249
|
+
}
|
|
250
|
+
for (const entry of entries) {
|
|
251
|
+
const child = path.join(target, entry.name);
|
|
252
|
+
if (entry.isDirectory()) {
|
|
253
|
+
const nested = directorySizeBytes(child);
|
|
254
|
+
bytes += nested.bytes;
|
|
255
|
+
partial = partial || nested.partial;
|
|
256
|
+
}
|
|
257
|
+
else if (entry.isFile()) {
|
|
258
|
+
try {
|
|
259
|
+
bytes += fs.statSync(child).size;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
partial = true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { bytes, partial };
|
|
267
|
+
}
|
|
268
|
+
function formatBytes(bytes) {
|
|
269
|
+
if (bytes < 1024)
|
|
270
|
+
return `${bytes} B`;
|
|
271
|
+
const units = ["KiB", "MiB", "GiB"];
|
|
272
|
+
let value = bytes / 1024;
|
|
273
|
+
let unit = 0;
|
|
274
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
275
|
+
value /= 1024;
|
|
276
|
+
unit++;
|
|
277
|
+
}
|
|
278
|
+
return `${value.toFixed(1)} ${units[unit]}`;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* `knodin remote <add|list|remove|refresh>`.
|
|
282
|
+
*
|
|
283
|
+
* Mirrors are reported as snapshots, never as `fresh`: the clone is pinned to
|
|
284
|
+
* the commit it was fetched at, and nothing watches the remote for changes. Size
|
|
285
|
+
* and fetch time are shown for every mirror so acquired disk is auditable rather
|
|
286
|
+
* than silently accumulating.
|
|
287
|
+
*/
|
|
288
|
+
function writeMirrorListing() {
|
|
289
|
+
const mirrors = listMirrors();
|
|
290
|
+
if (mirrors.length === 0) {
|
|
291
|
+
process.stdout.write("No mirrors acquired. Add one with `knodin remote add <url>`.\n");
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
let total = 0;
|
|
295
|
+
let anyPartial = false;
|
|
296
|
+
for (const mirror of mirrors) {
|
|
297
|
+
const size = directorySizeBytes(path.dirname(mirror.path));
|
|
298
|
+
total += size.bytes;
|
|
299
|
+
anyPartial = anyPartial || size.partial;
|
|
300
|
+
// "at least" rather than a bare figure when something was unreadable: a
|
|
301
|
+
// mirror reported as 0 B reads as empty, and the obvious next action on an
|
|
302
|
+
// empty mirror is to delete it.
|
|
303
|
+
const shown = size.partial ? `at least ${formatBytes(size.bytes)}` : formatBytes(size.bytes);
|
|
304
|
+
process.stdout.write(`${mirror.identity} ${mirror.url}\n snapshot ${mirror.sha.slice(0, 12)}, fetched ${mirror.fetchedAt}, ${shown}\n ${mirror.path}\n`);
|
|
305
|
+
}
|
|
306
|
+
const totalShown = anyPartial ? `at least ${formatBytes(total)}` : formatBytes(total);
|
|
307
|
+
process.stdout.write(`${mirrors.length} mirror(s), ${totalShown} on disk. Remove one with \`knodin remote remove <identity>\`.\n`);
|
|
308
|
+
}
|
|
309
|
+
async function remoteAdd(url, options) {
|
|
310
|
+
const { record, alreadyPresent } = addMirror(url);
|
|
311
|
+
process.stdout.write(alreadyPresent
|
|
312
|
+
? `Mirror already present: ${record.identity} (snapshot ${record.sha.slice(0, 12)}). Use \`knodin remote refresh ${record.identity}\` to update it.\n`
|
|
313
|
+
: `Acquired ${record.identity} at snapshot ${record.sha.slice(0, 12)} into ${record.path}\n`);
|
|
314
|
+
if (options.skipIndex || alreadyPresent)
|
|
315
|
+
return;
|
|
316
|
+
await indexMirror(record.path, options.deferSemantic);
|
|
317
|
+
}
|
|
318
|
+
async function remoteRefresh(identity, options) {
|
|
319
|
+
const record = refreshMirror(identity);
|
|
320
|
+
process.stdout.write(`Refreshed ${record.identity} to snapshot ${record.sha.slice(0, 12)} (${record.fetchedAt})\n`);
|
|
321
|
+
if (options.skipIndex)
|
|
322
|
+
return;
|
|
323
|
+
await indexMirror(record.path, options.deferSemantic);
|
|
324
|
+
}
|
|
325
|
+
async function runRemoteCommand(args) {
|
|
326
|
+
const [action, target] = args;
|
|
327
|
+
const options = {
|
|
328
|
+
skipIndex: args.includes("--no-index"),
|
|
329
|
+
deferSemantic: args.includes("--defer-semantic"),
|
|
330
|
+
};
|
|
331
|
+
if (action === "list")
|
|
332
|
+
return writeMirrorListing();
|
|
333
|
+
if (action === "remove") {
|
|
334
|
+
if (!target)
|
|
335
|
+
throw new Error("knodin remote remove requires an <identity>");
|
|
336
|
+
process.stdout.write(removeMirror(target) ? `Removed ${target}\n` : `No mirror named ${target}\n`);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (action === "add") {
|
|
340
|
+
if (!target)
|
|
341
|
+
throw new Error("knodin remote add requires a <url>");
|
|
342
|
+
return remoteAdd(target, options);
|
|
343
|
+
}
|
|
344
|
+
if (action === "refresh") {
|
|
345
|
+
if (!target)
|
|
346
|
+
throw new Error("knodin remote refresh requires an <identity>");
|
|
347
|
+
return remoteRefresh(target, options);
|
|
348
|
+
}
|
|
349
|
+
throw new Error("knodin remote <add|list|remove|refresh>");
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Builds a mirror's graph structure-first, then fills embeddings.
|
|
353
|
+
*
|
|
354
|
+
* The embedding pass dominates indexing cost, so structural answers (`explain`,
|
|
355
|
+
* `callers_of`, blast radius) become available in a fraction of the total. The
|
|
356
|
+
* gap is reported rather than hidden: until embeddings land, `search` silently
|
|
357
|
+
* under-returns, so saying nothing here would hand back exactly the confident
|
|
358
|
+
* empty result this whole feature exists to avoid.
|
|
359
|
+
*/
|
|
360
|
+
async function indexMirror(source, deferSemantic) {
|
|
361
|
+
const engine = createEngine();
|
|
362
|
+
try {
|
|
363
|
+
await engine.index(source, undefined, true, { skipEmbeddings: true });
|
|
364
|
+
const structural = await engine.status(source, { audit: "deep" });
|
|
365
|
+
process.stdout.write(`Indexed ${structural.coverage.indexedFiles} file(s), ${structural.coverage.filesWithSymbols} with symbols${formatCoverageGaps(structural.coverage.skipped)}\n`);
|
|
366
|
+
process.stdout.write("Structural queries (explain, query, map) are ready now; semantic search is not.\n");
|
|
367
|
+
if (deferSemantic) {
|
|
368
|
+
process.stdout.write("Semantic coverage deferred. Run `knodin index --repo <mirror>` to build it; until then `search` will under-return.\n");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
process.stdout.write("Building semantic index...\n");
|
|
372
|
+
await engine.index(source, undefined, false);
|
|
373
|
+
const full = await engine.status(source, { audit: "deep" });
|
|
374
|
+
process.stdout.write(`Semantic coverage: ${full.semanticReadiness ?? "unknown"}\n`);
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
await engine.close();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
177
380
|
function formatStatusHuman(result) {
|
|
178
|
-
|
|
381
|
+
// A mirror's `freshness` describes the graph against its local clone and can
|
|
382
|
+
// legitimately read `fresh`. Saying so without also saying the clone is a
|
|
383
|
+
// snapshot would imply knodin is tracking the remote, which it is not.
|
|
384
|
+
const mirrorNote = result.mirror
|
|
385
|
+
? ` Mirror of ${result.mirror.url}: snapshot ${result.mirror.sha.slice(0, 12)}, fetched ${result.mirror.fetchedAt}; the remote is not watched.`
|
|
386
|
+
: "";
|
|
387
|
+
// Semantic search drops symbols that have no embedding, so an incomplete pass
|
|
388
|
+
// silently shortens results. Say so rather than let it read as "no matches".
|
|
389
|
+
const semanticNote = result.semanticReadiness && result.semanticReadiness !== "ready"
|
|
390
|
+
? ` Semantic search coverage is ${result.semanticReadiness}: \`search\` will under-return until embedding completes (\`knodin index\`).`
|
|
391
|
+
: "";
|
|
392
|
+
const coverage = `${result.coverage.sourceFiles} source files, ${result.coverage.indexedFiles} indexed files, ${result.coverage.filesWithSymbols} files with symbols${formatCoverageGaps(result.coverage.skipped)}${mirrorNote}${semanticNote}`;
|
|
179
393
|
if (result.status === "indexing" && result.activity) {
|
|
180
394
|
const count = result.activity.phaseTotal === undefined
|
|
181
395
|
? ""
|
|
@@ -184,14 +398,14 @@ function formatStatusHuman(result) {
|
|
|
184
398
|
return `Graph update in progress: ${result.activity.phase}${count} — ${result.activity.message} (${elapsed}s elapsed; ${coverage}).\n`;
|
|
185
399
|
}
|
|
186
400
|
const integration = result.integration;
|
|
401
|
+
const integrationAgents = integration && integration.agents.length > 0 ? ` (${integration.agents.join(", ")})` : "";
|
|
187
402
|
const integrationLine = integration
|
|
188
|
-
? `Agent integration: ${integration.scope}${
|
|
403
|
+
? `Agent integration: ${integration.scope}${integrationAgents}.\n`
|
|
189
404
|
: "Agent integration: unconfigured. AI agents will not discover knodin automatically; run `knodin configure --scope personal`.\n";
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
: "";
|
|
405
|
+
// Replaces main's inline form: a mirror installs no hooks by design, so the
|
|
406
|
+
// inline version reported "needs repair — run knodin init" on every mirror
|
|
407
|
+
// status call, an instruction that must never be followed there.
|
|
408
|
+
const lifecycleLine = formatLifecycleLine(result.lifecycle, result.mirror !== undefined);
|
|
195
409
|
const head = (value) => value?.slice(0, 12) ?? "unknown";
|
|
196
410
|
const distance = result.freshness.commitDistance === null
|
|
197
411
|
? ""
|
|
@@ -219,7 +433,8 @@ function humanLabel(key) {
|
|
|
219
433
|
/** Render bounded CLI data for a terminal without turning it back into JSON. */
|
|
220
434
|
function formatHumanValue(value, indent = "", label) {
|
|
221
435
|
if (value === null || typeof value !== "object") {
|
|
222
|
-
|
|
436
|
+
const labelPrefix = label ? `${humanLabel(label)}: ` : "";
|
|
437
|
+
return [`${indent}${labelPrefix}${String(value)}`];
|
|
223
438
|
}
|
|
224
439
|
if (Array.isArray(value)) {
|
|
225
440
|
if (value.length === 0)
|
|
@@ -245,7 +460,8 @@ function formatHumanValue(value, indent = "", label) {
|
|
|
245
460
|
return lines;
|
|
246
461
|
}
|
|
247
462
|
function formatGenericHuman(cmd, result) {
|
|
248
|
-
|
|
463
|
+
const lines = [`${humanLabel(cmd)}:`, ...formatHumanValue(result, " ")];
|
|
464
|
+
return `${lines.join("\n")}\n`;
|
|
249
465
|
}
|
|
250
466
|
function formatCompressionHuman(result) {
|
|
251
467
|
const omitted = result.omittedRanges.reduce((total, range) => total + range.lineCount, 0);
|
|
@@ -318,6 +534,22 @@ async function readBoundedStdin(maxBytes) {
|
|
|
318
534
|
}
|
|
319
535
|
const CLEAR_TERMINAL_LINE = "\r\x1b[2K";
|
|
320
536
|
const PROGRESS_WORKER_CLOSE_TIMEOUT_MS = 2_000;
|
|
537
|
+
/** Resolve true when `closed` settles first, false when the deadline wins. */
|
|
538
|
+
function raceWorkerClose(closed, timeoutMs) {
|
|
539
|
+
return new Promise((resolve) => {
|
|
540
|
+
let settled = false;
|
|
541
|
+
const finish = (closedCleanly) => {
|
|
542
|
+
if (settled)
|
|
543
|
+
return;
|
|
544
|
+
settled = true;
|
|
545
|
+
clearTimeout(timer);
|
|
546
|
+
resolve(closedCleanly);
|
|
547
|
+
};
|
|
548
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
549
|
+
timer.unref();
|
|
550
|
+
void closed.then(() => finish(true));
|
|
551
|
+
});
|
|
552
|
+
}
|
|
321
553
|
function createProgressWorkerRenderer(workerName, startMessage) {
|
|
322
554
|
const extension = fileURLToPath(import.meta.url).endsWith(".ts") ? ".ts" : ".js";
|
|
323
555
|
const workerPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), `../src/${workerName}${extension}`);
|
|
@@ -355,19 +587,7 @@ function createProgressWorkerRenderer(workerName, startMessage) {
|
|
|
355
587
|
stop: async () => {
|
|
356
588
|
send({ type: "stop" });
|
|
357
589
|
worker.stdin.end();
|
|
358
|
-
const waitForClose = (timeoutMs) =>
|
|
359
|
-
let settled = false;
|
|
360
|
-
const finish = (closedCleanly) => {
|
|
361
|
-
if (settled)
|
|
362
|
-
return;
|
|
363
|
-
settled = true;
|
|
364
|
-
clearTimeout(timer);
|
|
365
|
-
resolve(closedCleanly);
|
|
366
|
-
};
|
|
367
|
-
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
368
|
-
timer.unref();
|
|
369
|
-
void closed.then(() => finish(true));
|
|
370
|
-
});
|
|
590
|
+
const waitForClose = (timeoutMs) => raceWorkerClose(closed, timeoutMs);
|
|
371
591
|
if (!(await waitForClose(PROGRESS_WORKER_CLOSE_TIMEOUT_MS))) {
|
|
372
592
|
// Rendering is observational. A wedged terminal renderer must never
|
|
373
593
|
// hold graph work or its completion summary hostage.
|
|
@@ -531,6 +751,7 @@ async function main() {
|
|
|
531
751
|
}
|
|
532
752
|
const engine = createEngine();
|
|
533
753
|
const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
|
|
754
|
+
const memoryLimitBytes = configuredRepositoryInitMemoryLimitBytes(systemConfig);
|
|
534
755
|
const summary = await initializeRepositories(plan.roots, {
|
|
535
756
|
command: runtimeCommand,
|
|
536
757
|
depth: plan.depth,
|
|
@@ -540,13 +761,21 @@ async function main() {
|
|
|
540
761
|
status: (target) => engine.status(target),
|
|
541
762
|
agents: detectSupportedAgents(),
|
|
542
763
|
indexMode: (target) => indexModeForPath(systemConfig, target),
|
|
543
|
-
isolatedInitialize: (target) => runRepositoryInitializationProcess({
|
|
764
|
+
isolatedInitialize: (target) => runRepositoryInitializationProcess({
|
|
765
|
+
repository: target,
|
|
766
|
+
command: runtimeCommand,
|
|
767
|
+
memoryLimitBytes,
|
|
768
|
+
}),
|
|
544
769
|
});
|
|
545
770
|
await engine.close();
|
|
546
771
|
process.stdout.write(plan.json ? `${JSON.stringify(summary)}\n` : formatRepositoryHuman(summary));
|
|
547
772
|
process.exitCode = summary.exitCode;
|
|
548
773
|
return;
|
|
549
774
|
}
|
|
775
|
+
if (cmd === "remote") {
|
|
776
|
+
await runRemoteCommand(rawRest);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
550
779
|
if (cmd === "repos") {
|
|
551
780
|
if (repoFlag !== undefined) {
|
|
552
781
|
throw new Error("knodin repos accepts discovery roots, not --repo");
|
|
@@ -587,6 +816,7 @@ async function main() {
|
|
|
587
816
|
}
|
|
588
817
|
if (plan.command === "init") {
|
|
589
818
|
const systemConfig = loadSystemConfiguration(plan.roots[0] ?? process.cwd());
|
|
819
|
+
const memoryLimitBytes = plan.memoryLimitBytes ?? configuredRepositoryInitMemoryLimitBytes(systemConfig);
|
|
590
820
|
if (plan.dryRun) {
|
|
591
821
|
const summary = await initializeRepositories(plan.roots, {
|
|
592
822
|
command: runtimeCommand,
|
|
@@ -636,7 +866,11 @@ async function main() {
|
|
|
636
866
|
status: (target) => engine.status(target),
|
|
637
867
|
agents: detectSupportedAgents(),
|
|
638
868
|
indexMode: (target) => indexModeForPath(systemConfig, target),
|
|
639
|
-
isolatedInitialize: (target) => runRepositoryInitializationProcess({
|
|
869
|
+
isolatedInitialize: (target) => runRepositoryInitializationProcess({
|
|
870
|
+
repository: target,
|
|
871
|
+
command: runtimeCommand,
|
|
872
|
+
memoryLimitBytes,
|
|
873
|
+
}),
|
|
640
874
|
});
|
|
641
875
|
const discovery = await discoverRepositories(plan.roots, {
|
|
642
876
|
depth: plan.depth,
|
|
@@ -1010,8 +1244,9 @@ async function main() {
|
|
|
1010
1244
|
switch (cmd) {
|
|
1011
1245
|
case "doctor": {
|
|
1012
1246
|
const client = selectorValue("--client");
|
|
1013
|
-
if (client !== undefined &&
|
|
1014
|
-
|
|
1247
|
+
if (client !== undefined &&
|
|
1248
|
+
!["claude", "codex", "gemini", "copilot", "antigravity"].includes(client))
|
|
1249
|
+
throw new Error("knodin doctor: --client must be claude, codex, gemini, copilot, or antigravity");
|
|
1015
1250
|
const unsupported = rest.filter((argument, index) => argument !== "--client" && rest[index - 1] !== "--client");
|
|
1016
1251
|
if (unsupported.length > 0)
|
|
1017
1252
|
throw new Error(`knodin doctor: unknown option ${unsupported[0]}`);
|
|
@@ -1158,7 +1393,7 @@ async function main() {
|
|
|
1158
1393
|
const scope = explicitScope(rawRest);
|
|
1159
1394
|
if (!scope)
|
|
1160
1395
|
throw new Error("knodin configure: missing --scope");
|
|
1161
|
-
if (!fs.existsSync(
|
|
1396
|
+
if (!fs.existsSync(resolveDbPath(repo))) {
|
|
1162
1397
|
throw new Error("knodin configure changes agent integration only; this repository is not initialized. Run `knodin init` first.");
|
|
1163
1398
|
}
|
|
1164
1399
|
const agents = scope === "team" ? [] : integrationAgents(repo);
|
|
@@ -1192,6 +1427,28 @@ async function main() {
|
|
|
1192
1427
|
}
|
|
1193
1428
|
const clean = rest.includes("--clean") || rest.includes("--force");
|
|
1194
1429
|
const scipPath = selectorValue("--scip");
|
|
1430
|
+
const sarifPath = selectorValue("--sarif");
|
|
1431
|
+
// A bound the operator cannot move is just a failure, so every import
|
|
1432
|
+
// ceiling is overridable. Reject junk here rather than letting NaN
|
|
1433
|
+
// silently disable a limit downstream.
|
|
1434
|
+
const importLimit = (flag) => {
|
|
1435
|
+
const raw = selectorValue(flag);
|
|
1436
|
+
if (raw === undefined)
|
|
1437
|
+
return undefined;
|
|
1438
|
+
const parsed = Number(raw);
|
|
1439
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
1440
|
+
process.stderr.write(`knodin index: ${flag} must be a positive integer\n`);
|
|
1441
|
+
process.exit(1);
|
|
1442
|
+
}
|
|
1443
|
+
return parsed;
|
|
1444
|
+
};
|
|
1445
|
+
const sarifMaxBytes = importLimit("--sarif-max-bytes");
|
|
1446
|
+
const sarifMaxFindings = importLimit("--sarif-max-findings");
|
|
1447
|
+
const sarifTimeoutMs = importLimit("--sarif-timeout-ms");
|
|
1448
|
+
const scipMaxBytes = importLimit("--scip-max-bytes");
|
|
1449
|
+
const scipMaxFiles = importLimit("--scip-max-files");
|
|
1450
|
+
const scipMaxFacts = importLimit("--scip-max-facts");
|
|
1451
|
+
const scipTimeoutMs = importLimit("--scip-timeout-ms");
|
|
1195
1452
|
const renderer = createInitRenderer("index");
|
|
1196
1453
|
const activity = createIndexActivityReporter(plan.repo);
|
|
1197
1454
|
activity.start();
|
|
@@ -1199,7 +1456,25 @@ async function main() {
|
|
|
1199
1456
|
let indexResult;
|
|
1200
1457
|
try {
|
|
1201
1458
|
indexResult = await engine.index(plan.repo, plan.files, clean, {
|
|
1202
|
-
scip: scipPath
|
|
1459
|
+
scip: scipPath
|
|
1460
|
+
? {
|
|
1461
|
+
path: scipPath,
|
|
1462
|
+
// Only override a default when the operator actually asked.
|
|
1463
|
+
...(scipMaxBytes !== undefined ? { maxBytes: scipMaxBytes } : {}),
|
|
1464
|
+
...(scipMaxFiles !== undefined ? { maxFiles: scipMaxFiles } : {}),
|
|
1465
|
+
...(scipMaxFacts !== undefined ? { maxFacts: scipMaxFacts } : {}),
|
|
1466
|
+
...(scipTimeoutMs !== undefined ? { timeoutMs: scipTimeoutMs } : {}),
|
|
1467
|
+
}
|
|
1468
|
+
: undefined,
|
|
1469
|
+
sarif: sarifPath
|
|
1470
|
+
? {
|
|
1471
|
+
path: sarifPath,
|
|
1472
|
+
// Only override a default when the operator actually asked.
|
|
1473
|
+
...(sarifMaxBytes !== undefined ? { maxBytes: sarifMaxBytes } : {}),
|
|
1474
|
+
...(sarifMaxFindings !== undefined ? { maxFindings: sarifMaxFindings } : {}),
|
|
1475
|
+
...(sarifTimeoutMs !== undefined ? { timeoutMs: sarifTimeoutMs } : {}),
|
|
1476
|
+
}
|
|
1477
|
+
: undefined,
|
|
1203
1478
|
onProgress: (event) => {
|
|
1204
1479
|
activity.update(event);
|
|
1205
1480
|
renderer.onProgress(event);
|
|
@@ -1376,13 +1651,18 @@ async function main() {
|
|
|
1376
1651
|
case "visualize": {
|
|
1377
1652
|
const entry = rest.find((value, index) => !value.startsWith("--") && !rest[index - 1]?.startsWith("--"));
|
|
1378
1653
|
const outputPath = selectorValue("--output");
|
|
1379
|
-
|
|
1654
|
+
const scope = selectorValue("--scope");
|
|
1655
|
+
const granularity = selectorValue("--granularity");
|
|
1656
|
+
// Repo scope draws the whole graph, so it takes no entry selector.
|
|
1657
|
+
if (!entry && scope !== "repo")
|
|
1380
1658
|
throw new Error("knodin visualize requires an <entry> selector");
|
|
1381
1659
|
if (!outputPath)
|
|
1382
1660
|
throw new Error("knodin visualize requires --output <path.html>");
|
|
1383
1661
|
result = await graphRead(() => writeVisualization(engine, repo, {
|
|
1384
1662
|
entry,
|
|
1385
1663
|
outputPath,
|
|
1664
|
+
scope,
|
|
1665
|
+
granularity,
|
|
1386
1666
|
depth: selectorValue("--depth") ? Number(selectorValue("--depth")) : undefined,
|
|
1387
1667
|
byteBudget: selectorValue("--max-bytes")
|
|
1388
1668
|
? Number(selectorValue("--max-bytes"))
|
|
@@ -1405,16 +1685,17 @@ async function main() {
|
|
|
1405
1685
|
const limit = Number(selectorValue("--limit") ?? positionalLimit ?? 5);
|
|
1406
1686
|
if (!Number.isInteger(limit) || limit < 1)
|
|
1407
1687
|
throw new Error("knodin search: limit must be a positive integer");
|
|
1688
|
+
let testScope = "all";
|
|
1689
|
+
if (rest.includes("--tests-only"))
|
|
1690
|
+
testScope = "test";
|
|
1691
|
+
else if (rest.includes("--production-only"))
|
|
1692
|
+
testScope = "production";
|
|
1408
1693
|
result = await graphRead(() => engine.search(query, repo, limit, {
|
|
1409
1694
|
languages: selectorValue("--languages")?.split(",").filter(Boolean),
|
|
1410
1695
|
extensions: selectorValue("--extensions")?.split(",").filter(Boolean),
|
|
1411
1696
|
kinds: selectorValue("--kinds")?.split(",").filter(Boolean),
|
|
1412
1697
|
path: selectorValue("--path"),
|
|
1413
|
-
testScope
|
|
1414
|
-
? "test"
|
|
1415
|
-
: rest.includes("--production-only")
|
|
1416
|
-
? "production"
|
|
1417
|
-
: "all",
|
|
1698
|
+
testScope,
|
|
1418
1699
|
includeSource: !rest.includes("--no-source"),
|
|
1419
1700
|
offset: selectorValue("--offset") ? Number(selectorValue("--offset")) : 0,
|
|
1420
1701
|
}));
|
|
@@ -1692,6 +1973,7 @@ async function main() {
|
|
|
1692
1973
|
const rawRetention = selectorValue("--retention-days");
|
|
1693
1974
|
const sinceOption = selectorValue("--since");
|
|
1694
1975
|
const outputOption = selectorValue("--output");
|
|
1976
|
+
const previewId = selectorValue("--preview-id");
|
|
1695
1977
|
const retentionDays = rawRetention === undefined ? 14 : Number(rawRetention);
|
|
1696
1978
|
if (!Number.isInteger(retentionDays) || retentionDays < 1 || retentionDays > 365)
|
|
1697
1979
|
throw new Error("knodin diagnostics: --retention-days must be an integer from 1 to 365");
|
|
@@ -1699,10 +1981,12 @@ async function main() {
|
|
|
1699
1981
|
throw new Error(`knodin diagnostics ${action}: unexpected bundle argument`);
|
|
1700
1982
|
if (rawRetention !== undefined && action !== "enable")
|
|
1701
1983
|
throw new Error(`knodin diagnostics ${action}: --retention-days applies only to enable`);
|
|
1702
|
-
if (sinceOption !== undefined &&
|
|
1703
|
-
throw new Error(`knodin diagnostics ${action}: --since applies only to
|
|
1704
|
-
if (outputOption !== undefined && action
|
|
1705
|
-
throw new Error(`knodin diagnostics ${action}: --output applies only to
|
|
1984
|
+
if (sinceOption !== undefined && !["preview", "archive", "collect"].includes(action ?? ""))
|
|
1985
|
+
throw new Error(`knodin diagnostics ${action}: --since applies only to preview or archive`);
|
|
1986
|
+
if (outputOption !== undefined && !["archive", "collect"].includes(action ?? ""))
|
|
1987
|
+
throw new Error(`knodin diagnostics ${action}: --output applies only to archive`);
|
|
1988
|
+
if (previewId !== undefined && !["archive", "collect"].includes(action ?? ""))
|
|
1989
|
+
throw new Error(`knodin diagnostics ${action}: --preview-id applies only to archive`);
|
|
1706
1990
|
if (action === "enable")
|
|
1707
1991
|
result = enableDiagnostics(repo, retentionDays);
|
|
1708
1992
|
else if (action === "status")
|
|
@@ -1716,29 +2000,45 @@ async function main() {
|
|
|
1716
2000
|
throw new Error("knodin diagnostics inspect requires <bundle>");
|
|
1717
2001
|
result = inspectDiagnosticsBundle(repo, bundlePath);
|
|
1718
2002
|
}
|
|
1719
|
-
else if (
|
|
2003
|
+
else if (["preview", "archive", "collect"].includes(action ?? "")) {
|
|
1720
2004
|
const since = sinceOption ?? "24h";
|
|
1721
|
-
const match = /^(\d+)(
|
|
2005
|
+
const match = /^(\d+)([hd])$/.exec(since);
|
|
1722
2006
|
if (!match)
|
|
1723
2007
|
throw new Error("knodin diagnostics collect: --since must be hours or days, such as 24h or 7d");
|
|
1724
2008
|
const sinceHours = Number(match[1]) * (match[2] === "d" ? 24 : 1);
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
2009
|
+
let graph;
|
|
2010
|
+
let doctor;
|
|
2011
|
+
try {
|
|
2012
|
+
graph = attachLifecycleHealth(repo, await engine.status(repo, { audit: "deep" }));
|
|
2013
|
+
}
|
|
2014
|
+
catch {
|
|
2015
|
+
// Support evidence remains available with an explicit unavailable graph section.
|
|
2016
|
+
}
|
|
2017
|
+
if (graph) {
|
|
2018
|
+
try {
|
|
2019
|
+
doctor = await diagnoseInstallation(repo, {
|
|
2020
|
+
currentVersion: KNODIN_VERSION,
|
|
2021
|
+
runtimeCommand: [...runtimeCommand, "serve"],
|
|
2022
|
+
graph,
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
catch {
|
|
2026
|
+
// Installation diagnosis is represented as unavailable in the manifest.
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
const options = {
|
|
1732
2030
|
sinceHours,
|
|
1733
|
-
outputPath: outputOption,
|
|
1734
2031
|
doctor,
|
|
1735
2032
|
graph,
|
|
1736
|
-
telemetry: readTelemetryRecords(repo, undefined, Math.max(1, Math.ceil(sinceHours / 24))),
|
|
1737
2033
|
knodinVersion: KNODIN_VERSION,
|
|
1738
|
-
}
|
|
2034
|
+
};
|
|
2035
|
+
result =
|
|
2036
|
+
action === "preview"
|
|
2037
|
+
? persistDiagnosticsPreview(repo, options)
|
|
2038
|
+
: collectDiagnostics(repo, { ...options, outputPath: outputOption, previewId });
|
|
1739
2039
|
}
|
|
1740
2040
|
else {
|
|
1741
|
-
throw new Error("knodin diagnostics requires enable, status,
|
|
2041
|
+
throw new Error("knodin diagnostics requires enable, status, preview, archive, inspect, clear, or disable");
|
|
1742
2042
|
}
|
|
1743
2043
|
break;
|
|
1744
2044
|
}
|
|
@@ -1806,15 +2106,19 @@ async function main() {
|
|
|
1806
2106
|
: `${JSON.stringify(boundedResult)}\n`);
|
|
1807
2107
|
process.exitCode = finalExitCode;
|
|
1808
2108
|
}
|
|
1809
|
-
|
|
2109
|
+
try {
|
|
2110
|
+
await main();
|
|
2111
|
+
}
|
|
2112
|
+
catch (err) {
|
|
1810
2113
|
const argv = process.argv.slice(2);
|
|
1811
2114
|
const repoIndex = argv.indexOf("--repo");
|
|
1812
2115
|
const equalsRepo = argv.find((argument) => argument.startsWith("--repo="));
|
|
1813
|
-
const
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
2116
|
+
const explicitRepo = repoIndex >= 0 ? argv[repoIndex + 1] : undefined;
|
|
2117
|
+
let candidate = process.cwd();
|
|
2118
|
+
if (explicitRepo)
|
|
2119
|
+
candidate = explicitRepo;
|
|
2120
|
+
else if (equalsRepo)
|
|
2121
|
+
candidate = equalsRepo.slice("--repo=".length);
|
|
1818
2122
|
const command = argv.find((argument, index) => {
|
|
1819
2123
|
if (argument.startsWith("-"))
|
|
1820
2124
|
return false;
|
|
@@ -1846,6 +2150,7 @@ main().catch((err) => {
|
|
|
1846
2150
|
"diagnostics",
|
|
1847
2151
|
"system",
|
|
1848
2152
|
"repos",
|
|
2153
|
+
"remote",
|
|
1849
2154
|
"update",
|
|
1850
2155
|
]);
|
|
1851
2156
|
const diagnostic = recordDiagnosticFailure(candidate, {
|
|
@@ -1857,4 +2162,4 @@ main().catch((err) => {
|
|
|
1857
2162
|
const correlation = diagnostic.recorded ? ` [diagnostic ${diagnostic.correlationId}]` : "";
|
|
1858
2163
|
console.error(`${err instanceof Error ? err.message : String(err)}${correlation}`);
|
|
1859
2164
|
process.exit(1);
|
|
1860
|
-
}
|
|
2165
|
+
}
|