knodin 0.5.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/LICENSE +21 -0
- package/README.md +590 -0
- package/dist/bin/cli.js +1704 -0
- package/dist/src/agent-integration.js +250 -0
- package/dist/src/artifact-refresh.js +81 -0
- package/dist/src/cli-args.js +267 -0
- package/dist/src/cli-model.js +324 -0
- package/dist/src/compact-structural.js +96 -0
- package/dist/src/competitive-constraints.js +20 -0
- package/dist/src/competitive-manifest.js +330 -0
- package/dist/src/competitive-measurement.js +183 -0
- package/dist/src/competitive-runner.js +453 -0
- package/dist/src/competitive-sandbox.js +108 -0
- package/dist/src/context-export.js +422 -0
- package/dist/src/context.js +102 -0
- package/dist/src/docs-sections.js +141 -0
- package/dist/src/doctor.js +380 -0
- package/dist/src/engine/ann-hnsw.js +271 -0
- package/dist/src/engine/embeddings.js +193 -0
- package/dist/src/engine/file-walker.js +43 -0
- package/dist/src/engine/index.js +13030 -0
- package/dist/src/engine/perf.js +115 -0
- package/dist/src/engine/prune.js +112 -0
- package/dist/src/engine/source-policy.js +69 -0
- package/dist/src/engine/sqlite.js +71 -0
- package/dist/src/engine/symbol-delete.js +58 -0
- package/dist/src/failure-diagnosis.js +590 -0
- package/dist/src/fleet.js +7 -0
- package/dist/src/git-executable.js +31 -0
- package/dist/src/graph-query-health.js +115 -0
- package/dist/src/index-activity.js +125 -0
- package/dist/src/init-progress-worker.js +107 -0
- package/dist/src/init-progress.js +155 -0
- package/dist/src/init.js +985 -0
- package/dist/src/lifecycle-health.js +213 -0
- package/dist/src/lsp-readonly.js +217 -0
- package/dist/src/output-compression.js +629 -0
- package/dist/src/output-telemetry.js +359 -0
- package/dist/src/pr-triage.js +638 -0
- package/dist/src/relationship-adapters.js +370 -0
- package/dist/src/release-attestation.js +533 -0
- package/dist/src/repair-progress-worker.js +121 -0
- package/dist/src/repair-progress.js +262 -0
- package/dist/src/repository-init-process.js +173 -0
- package/dist/src/repository-management.js +1089 -0
- package/dist/src/response-budget.js +184 -0
- package/dist/src/server.js +53 -0
- package/dist/src/system-config.js +615 -0
- package/dist/src/terminal-help.js +83 -0
- package/dist/src/tools/knodin-tools.js +1438 -0
- package/dist/src/tools/reckon-tools.js +5 -0
- package/dist/src/update-policy.js +944 -0
- package/dist/src/update-trust.js +503 -0
- package/dist/src/version.js +13 -0
- package/dist/src/visualization.js +162 -0
- package/dist/src/wait-for-fresh.js +98 -0
- package/dist/src/worktree-lifecycle.js +231 -0
- package/docs/CLI.md +39 -0
- package/docs/COMMAND-OUTPUT-COMPRESSION.md +194 -0
- package/docs/DEAD-CODE-AND-IMPACT.md +27 -0
- package/docs/DOCTOR-AND-UPDATES.md +84 -0
- package/docs/INDEXING-POLICY-AND-PROVENANCE.md +37 -0
- package/docs/INSTALLATION.md +208 -0
- package/docs/MCP.md +100 -0
- package/docs/PT-ACCESS-RECOMMENDATION.md +91 -0
- package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
- package/docs/REPOSITORIES-AND-WORKTREES.md +81 -0
- package/docs/SIGNED-UPDATES.md +146 -0
- package/docs/SYSTEMS-AND-RELATIONSHIPS.md +45 -0
- package/docs/TELEMETRY.md +42 -0
- package/docs/releases/0.3.0.md +46 -0
- package/docs/releases/0.4.0.md +68 -0
- package/docs/releases/0.4.1.md +28 -0
- package/docs/releases/0.4.2.md +27 -0
- package/docs/releases/0.4.3.md +23 -0
- package/docs/releases/0.5.0.md +29 -0
- package/package.json +110 -0
- package/schemas/release-attestation-v1.schema.json +210 -0
- package/tree-sitter-prisma.wasm +0 -0
- package/tree-sitter-sql.wasm +0 -0
- package/tree-sitter-xml.wasm +0 -0
|
@@ -0,0 +1,1438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `knodin` gateway tool.
|
|
3
|
+
*
|
|
4
|
+
* Following the AtlasMCP pattern: a single low-level tool with an `operation`
|
|
5
|
+
* enum, registered via a hand-written JSON Schema literal (no high-level
|
|
6
|
+
* `server.tool()` / Zod-to-handler inference — that path OOMs `tsc`). One flat
|
|
7
|
+
* gateway keeps the client LLM's context small.
|
|
8
|
+
*/
|
|
9
|
+
import nodePath from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { resolveCliRuntimeCommand } from "../cli-args.js";
|
|
12
|
+
import { compactExplainResult, compactQueryResult, compactSearchResult, expandCompactIdentity, } from "../compact-structural.js";
|
|
13
|
+
import { buildKnodinContext } from "../context.js";
|
|
14
|
+
import { exportContext, grepPackedArtifact, readPackedArtifact } from "../context-export.js";
|
|
15
|
+
import { getDocSection, listDocTopics } from "../docs-sections.js";
|
|
16
|
+
import { diagnoseInstallation } from "../doctor.js";
|
|
17
|
+
import { createEngine, REPO_WIDE_QUERY_PATTERNS, } from "../engine/index.js";
|
|
18
|
+
import { measurePerfPhaseSync } from "../engine/perf.js";
|
|
19
|
+
import { diagnoseFailure, } from "../failure-diagnosis.js";
|
|
20
|
+
import { decorateGraphQueryResult, inspectGraphQueryHealth } from "../graph-query-health.js";
|
|
21
|
+
import { inspectRepositoryIntegrationStatus } from "../init.js";
|
|
22
|
+
import { attachLifecycleHealth } from "../lifecycle-health.js";
|
|
23
|
+
import { compressOutput, compressOutputFile, deleteOutputArtifact, readOutputArtifact, } from "../output-compression.js";
|
|
24
|
+
import { appendTelemetryRecord, clearTelemetry, countOutputTokens, exportTelemetry, measureOutput, readTelemetryRecords, telemetryStatus, writeTelemetryReport, } from "../output-telemetry.js";
|
|
25
|
+
import { auditPullRequests, ghUnavailableReason, triagePrDetail } from "../pr-triage.js";
|
|
26
|
+
import { createRepairPlan } from "../repair-progress.js";
|
|
27
|
+
import { runRepositoryInitializationProcess } from "../repository-init-process.js";
|
|
28
|
+
import { discoverRepositories, initializeRepositories, inventoryRepository, searchRepositories, } from "../repository-management.js";
|
|
29
|
+
import { applyResponseBudget } from "../response-budget.js";
|
|
30
|
+
import { enrichSystemRelationships, incorporateSystemQueryEvidence, indexModeForPath, loadSystemConfiguration, queryConfiguredSystem, systemMembershipsForPath, validateSystemHealth, } from "../system-config.js";
|
|
31
|
+
import { trustedUpdateStatus } from "../update-policy.js";
|
|
32
|
+
import { waitForFresh } from "../wait-for-fresh.js";
|
|
33
|
+
import { inspectWorktrees, reconcileWorktrees, removeManagedWorktree, } from "../worktree-lifecycle.js";
|
|
34
|
+
const engine = createEngine();
|
|
35
|
+
function gatewayCliCommand() {
|
|
36
|
+
const extension = import.meta.url.endsWith(".ts") ? "ts" : "js";
|
|
37
|
+
const entry = nodePath.resolve(nodePath.dirname(fileURLToPath(import.meta.url)), `../../bin/cli.${extension}`);
|
|
38
|
+
return resolveCliRuntimeCommand({
|
|
39
|
+
argv: [process.argv[0] ?? process.execPath, entry],
|
|
40
|
+
cwd: () => process.cwd(),
|
|
41
|
+
execArgv: process.execArgv,
|
|
42
|
+
execPath: process.execPath,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Release the gateway engine's local watcher and database resources. */
|
|
46
|
+
export async function closeKnodinToolEngine() {
|
|
47
|
+
await engine.close();
|
|
48
|
+
compactReadyRepos.clear();
|
|
49
|
+
}
|
|
50
|
+
const localTelemetry = [];
|
|
51
|
+
const compactReadyRepos = new Set();
|
|
52
|
+
let cachedSchemaTokens;
|
|
53
|
+
function gatewaySchemaTokens() {
|
|
54
|
+
cachedSchemaTokens ??= countOutputTokens(JSON.stringify(buildDocumentedKnodinTools()[0]?.inputSchema ?? {}));
|
|
55
|
+
return cachedSchemaTokens;
|
|
56
|
+
}
|
|
57
|
+
function inspectGatewayGraphHealth(repo) {
|
|
58
|
+
return inspectGraphQueryHealth(repo, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })));
|
|
59
|
+
}
|
|
60
|
+
async function compactGraphUnavailable(repo) {
|
|
61
|
+
const key = nodePath.resolve(repo);
|
|
62
|
+
if (compactReadyRepos.has(key))
|
|
63
|
+
return null;
|
|
64
|
+
const health = await inspectGatewayGraphHealth(repo);
|
|
65
|
+
if (!health.available)
|
|
66
|
+
return health;
|
|
67
|
+
compactReadyRepos.add(key);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/** Runs `fn`, converting any thrown error into a structured `{ error }` result
|
|
71
|
+
* instead of letting it escape into the stdio transport. Extracted from the
|
|
72
|
+
* `prs` case (R22's "never throw into MCP" rule) so a future operation that
|
|
73
|
+
* needs the same guarantee can reuse it by name rather than re-implementing
|
|
74
|
+
* its own try/catch. Deliberately scoped to `prs` only for now — `explain`,
|
|
75
|
+
* `review`, `map`, `search`, and `query` keep their existing throwing
|
|
76
|
+
* behavior; wrapping them is out of scope here. */
|
|
77
|
+
async function withStructuredError(fn) {
|
|
78
|
+
try {
|
|
79
|
+
return await fn();
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return { error: err.message };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const QUERY_PATTERNS = [
|
|
86
|
+
"lsp_diagnostics",
|
|
87
|
+
"lsp_definitions",
|
|
88
|
+
"lsp_declarations",
|
|
89
|
+
"lsp_implementations",
|
|
90
|
+
"callers_of",
|
|
91
|
+
"callees_of",
|
|
92
|
+
"imports_of",
|
|
93
|
+
"importers_of",
|
|
94
|
+
"import_cycles",
|
|
95
|
+
"inheritors_of",
|
|
96
|
+
"structural_implementations_of",
|
|
97
|
+
"tests_for",
|
|
98
|
+
"file_summary",
|
|
99
|
+
"batch_outline",
|
|
100
|
+
"project_overview",
|
|
101
|
+
"shortest_path",
|
|
102
|
+
"impact",
|
|
103
|
+
"dead_code",
|
|
104
|
+
"large_functions",
|
|
105
|
+
"large_files",
|
|
106
|
+
"rename_preview",
|
|
107
|
+
"flows",
|
|
108
|
+
"flow_of",
|
|
109
|
+
"stats",
|
|
110
|
+
"traverse",
|
|
111
|
+
"feature_path",
|
|
112
|
+
"flow_analysis",
|
|
113
|
+
"knowledge_gaps",
|
|
114
|
+
"surprising_connections",
|
|
115
|
+
"suggested_questions",
|
|
116
|
+
"architecture_overview",
|
|
117
|
+
"triggers_of",
|
|
118
|
+
"publishers_of",
|
|
119
|
+
"listeners_of",
|
|
120
|
+
"handlers_of",
|
|
121
|
+
"endpoints_for",
|
|
122
|
+
"consumers_of",
|
|
123
|
+
"children_of",
|
|
124
|
+
"community",
|
|
125
|
+
"federated_repos",
|
|
126
|
+
"mcp_tools",
|
|
127
|
+
"api_contract_mismatches",
|
|
128
|
+
];
|
|
129
|
+
const GRAPH_INDEPENDENT_QUERY_PATTERNS = new Set([
|
|
130
|
+
"lsp_diagnostics",
|
|
131
|
+
"lsp_definitions",
|
|
132
|
+
"lsp_declarations",
|
|
133
|
+
"lsp_implementations",
|
|
134
|
+
]);
|
|
135
|
+
function buildDocumentedKnodinTools() {
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
name: "knodin",
|
|
139
|
+
description: "Primary local code-intelligence gateway. Start with context when unsure; use explain before editing a symbol, review for diffs, map for boundaries, search for fuzzy discovery, query for structured graph questions/outlines, pack for bounded source context, compress for hard-budgeted recoverable diagnostic output, and prs for authenticated GitHub audits. Explain/source results are already Read-equivalent. Prefer grep for exact literals and direct reads for non-code or just-edited files. Every graph answer carries freshness evidence.",
|
|
140
|
+
inputSchema: {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
operation: {
|
|
144
|
+
type: "string",
|
|
145
|
+
enum: [
|
|
146
|
+
"explain",
|
|
147
|
+
"review",
|
|
148
|
+
"map",
|
|
149
|
+
"search",
|
|
150
|
+
"query",
|
|
151
|
+
"prs",
|
|
152
|
+
"context",
|
|
153
|
+
"wiki",
|
|
154
|
+
"docs",
|
|
155
|
+
"doctor",
|
|
156
|
+
"pack",
|
|
157
|
+
"compress",
|
|
158
|
+
"status",
|
|
159
|
+
"wait",
|
|
160
|
+
"worktrees",
|
|
161
|
+
"repair",
|
|
162
|
+
"repositories",
|
|
163
|
+
"system",
|
|
164
|
+
"telemetry",
|
|
165
|
+
],
|
|
166
|
+
description: "Which knodin capability to run. context: call this FIRST when starting any investigation and unsure which operation to reach for — one ultra-compact orientation (repo stats + top subsystems/hubs/flows + a risk score if there's a diff + a heuristic next-operation suggestion); the suggestion is only a hint and never blocks calling any operation directly. explain: use when orienting on a symbol/file or before editing it — returns edit-ready source + call paths + blast radius. review: use before writing a PR description or approving a diff — risk-scored context (changed symbols, affected flows, test gaps). map: use before a cross-cutting refactor or to understand subsystem boundaries — communities + hub/bridge nodes + confidence-tagged edges. search: use when you don't know the exact symbol name — hybrid semantic + keyword lookup over code symbols. query: use for a structured question about a known symbol — callers_of, tests_for, shortest_path, dead_code, rename_preview, flows, and more (see `pattern`). pack: create deterministic Markdown/JSON/XML source context under hard budgets, or bounded-read/exact-regex-grep a saved artifact. compress: reduce already-produced diagnostic text under exact line/content-byte budgets, preserve exit metadata and detected signals, retain private local drill-down data by default, and never label insufficient-fidelity output complete. prs: use to triage open GitHub PRs (via your local authenticated `gh`) — per-PR status, CI, and blast radius sorted ready-small-impact first; pass `prNumber` for one PR's impacted files + community names. wiki: write a static markdown documentation site for the repository's logical subsystems to `.reckon/wiki/`. Generates an index plus one page per mapped community. Reuses the `map` output. Idempotent: unchanged pages are untouched on disk unless `force` is true. docs: call this to retrieve curated, focused markdown usage guidance directly over MCP.",
|
|
167
|
+
},
|
|
168
|
+
symbol: {
|
|
169
|
+
type: "string",
|
|
170
|
+
description: "explain: the symbol (function/class/file) to explain. query: the target symbol/file (symbol selector for impact by default; comma-separated files only with impactMode=file; file-substring filter for dead_code/large_functions/large_files; entry-point name for flow_of; unused for flows).",
|
|
171
|
+
},
|
|
172
|
+
identity: {
|
|
173
|
+
type: "string",
|
|
174
|
+
description: "Stable identity selecting one same-named definition.",
|
|
175
|
+
},
|
|
176
|
+
file: {
|
|
177
|
+
type: "string",
|
|
178
|
+
description: "Repo-relative definition file selector.",
|
|
179
|
+
},
|
|
180
|
+
kind: {
|
|
181
|
+
type: "string",
|
|
182
|
+
description: "Definition kind selector (function, class, method, etc.).",
|
|
183
|
+
},
|
|
184
|
+
toIdentity: {
|
|
185
|
+
type: "string",
|
|
186
|
+
description: "shortest_path destination identity selector.",
|
|
187
|
+
},
|
|
188
|
+
toFile: {
|
|
189
|
+
type: "string",
|
|
190
|
+
description: "shortest_path destination file selector.",
|
|
191
|
+
},
|
|
192
|
+
toKind: {
|
|
193
|
+
type: "string",
|
|
194
|
+
description: "shortest_path destination kind selector.",
|
|
195
|
+
},
|
|
196
|
+
pattern: {
|
|
197
|
+
type: "string",
|
|
198
|
+
enum: [...QUERY_PATTERNS],
|
|
199
|
+
description: "feature_path — deterministic downstream DFS over resolved source references. It is bounded by depth and item limits, source-evidenced per hop, cycle-guarded, and never claims runtime execution; dynamic dispatch and unresolved calls are omitted. " +
|
|
200
|
+
"lsp_diagnostics accepts a repo-relative TypeScript/JavaScript file; lsp_definitions, lsp_declarations, and lsp_implementations accept file:line:column. These use an optional local TypeScript language-service adapter, never start a daemon or write files, and return an explicit unavailable result when no local adapter supports the file type. " +
|
|
201
|
+
"api_contract_mismatches — bounded, source-evidenced static mismatch findings for literal Express/Fastify routes and literal fetch/Axios clients. Matching requires method and normalized path, and requires exact origin equality when a client uses an absolute origin; dynamic routes remain explicitly unresolved. mcp_tools — list indexed TypeScript MCP SDK tool registrations, or pass symbol to look up one exact tool name; returns description/schema/handler/file and exact versus heuristic confidence. import_cycles — repo-wide canonical directed file-import cycles; no symbol required, type-only imports included, non-import lineage/ORM edges excluded, and limit/truncated bounds output. " +
|
|
202
|
+
"query: the graph pattern to run (callers_of, callees_of, imports_of, importers_of, inheritors_of, structural_implementations_of, tests_for, file_summary, shortest_path, impact, dead_code, large_functions, large_files, rename_preview, flows, flow_of, stats, traverse, knowledge_gaps, community, federated_repos). structural_implementations_of returns TypeScript structurally typed object implementations separately from nominal extends/implements results. federated_repos — discover and list all registered/configured repository paths that this engine federates queries across. community — fetch a single community's full detail (name, size, cohesion, files, and symbols) by name or substring filter; empty `symbol` = all communities. large_files — whole-file line counts (min 200), sorted descending — the file-level counterpart to large_functions, for spotting god files rather than god functions; same optional path-substring filter, empty `symbol` = all files. stats — repo-level index size + health in one call (per-repo symbol/reference/dependency/embedding/file counts, per-kind and per-language breakdowns, orphaned-embedding count, schema version, last-indexed HEAD, and cached community/hub/bridge/flow totals); federated, empty `symbol`. traverse — typed BFS from `symbol` within `depth` hops (default 3, 1–6), selectable upstream/downstream/both and explicit edge kinds; every discovered edge includes direction, kind, provenance, confidence, files, and source line, with optional Tree-sitter-grounded argument expressions honestly marked heuristic. Sets `truncated` when the `limit` cap binds; single-repo. knowledge_gaps — repo-health weaknesses in one call: thin communities (<3 symbols), single-file communities, isolated (zero-degree) symbols, and untested hub/bridge hotspots; sourced from the cached map(), federated, empty `symbol`, one row carrying a `knowledgeGaps` block. surprising_connections — resolved edges scored by a composite surprise formula (cross-community, cross-language, peripheral-to-hub, cross-test-boundary, unusual edge kind) to surface unexpected coupling; sorted highest-first, top 15 by default, federated, empty `symbol`, each row carrying a `surprise` block. suggested_questions — prioritized, human-readable review prompts synthesized from knowledge_gaps + surprising_connections (untested hotspots first, then surprising edges, then thin/single-file communities); federated, empty `symbol`, each row a `question` string citing the real symbol/file, top 10 by default. architecture_overview — a federated architecture view with scoped community coupling and independently selectable packages, layers, boundaries, hotspots, entry points, and language facets; `path` is segment-safe and applies consistently. `detailLevel` 'minimal' omits per-edge coupling; 'standard' includes it. Spring/event patterns (Java): triggers_of — the schedule edge between a @Scheduled method and its synthetic scheduler (pass the method to get its scheduler, or the scheduler to list scheduled methods). publishers_of — methods that publish a given event type (ApplicationEventPublisher.publishEvent). listeners_of — @EventListener methods that listen for a given event type. handlers_of — methods that handle a given HTTP endpoint path (Spring @*Mapping + the existing JS/Python endpoints). endpoints_for — the inverse: endpoint path(s) a given method handles. consumers_of — classes/methods consuming a given @Value config property. children_of — symbols contained in a file path (same as file_summary) or, for a class name, its member symbols by line-range containment. All seven require `symbol` (the target); single-repo, not federated.",
|
|
203
|
+
},
|
|
204
|
+
depth: {
|
|
205
|
+
type: "number",
|
|
206
|
+
description: "query traverse/feature_path/impact: maximum hops from `symbol` (default 3, clamped to 1–6).",
|
|
207
|
+
},
|
|
208
|
+
impactMode: {
|
|
209
|
+
type: "string",
|
|
210
|
+
enum: ["symbol", "file"],
|
|
211
|
+
description: "query impact only: stable symbol reach (default) or explicitly labeled changed-file blast radius.",
|
|
212
|
+
},
|
|
213
|
+
direction: {
|
|
214
|
+
type: "string",
|
|
215
|
+
enum: ["upstream", "downstream", "both"],
|
|
216
|
+
description: "query impact/traverse: inbound/upstream, outbound/downstream, or both (default).",
|
|
217
|
+
},
|
|
218
|
+
architectureFacets: {
|
|
219
|
+
type: "array",
|
|
220
|
+
items: {
|
|
221
|
+
type: "string",
|
|
222
|
+
enum: ["packages", "layers", "boundaries", "hotspots", "entryPoints", "languages"],
|
|
223
|
+
},
|
|
224
|
+
description: "query architecture_overview: independently select architecture dimensions; omitted selects all.",
|
|
225
|
+
},
|
|
226
|
+
relationKinds: {
|
|
227
|
+
type: "array",
|
|
228
|
+
items: { type: "string" },
|
|
229
|
+
description: "query impact/traverse/feature_path: only include these exact edge kinds (for example call).",
|
|
230
|
+
},
|
|
231
|
+
minConfidence: {
|
|
232
|
+
type: "number",
|
|
233
|
+
minimum: 0,
|
|
234
|
+
maximum: 1,
|
|
235
|
+
description: "query impact: minimum edge confidence (extracted calls are 1.0).",
|
|
236
|
+
},
|
|
237
|
+
includeTests: {
|
|
238
|
+
type: "boolean",
|
|
239
|
+
description: "query impact: include test/spec files (default true).",
|
|
240
|
+
},
|
|
241
|
+
includeDataFlow: {
|
|
242
|
+
type: "boolean",
|
|
243
|
+
description: "query impact/traverse: attach bounded call-site argument evidence when parsed syntax supports it; never claims runtime proof.",
|
|
244
|
+
},
|
|
245
|
+
flowVariable: {
|
|
246
|
+
type: "string",
|
|
247
|
+
description: "query flow_analysis only: optional simple local variable name to filter compact source-evidenced facts.",
|
|
248
|
+
},
|
|
249
|
+
to: {
|
|
250
|
+
type: "string",
|
|
251
|
+
description: "query shortest_path: destination symbol (start symbol goes in `symbol`). query rename_preview: the new name (old name goes in `symbol`). Read-only by default (returns edit sites + a unified diff); pass `apply: true` to write the rename to disk.",
|
|
252
|
+
},
|
|
253
|
+
apply: {
|
|
254
|
+
type: "boolean",
|
|
255
|
+
description: "query rename_preview only: when true, perform the rename on disk (line-scoped word-boundary edits, atomic writes, reindex of touched files). Omitted/false is a dry-run that returns the preview plus a unified diff of what would change. Refuses (no writes) on ambiguous definitions, out-of-repo paths, or a name collision with an existing symbol.",
|
|
256
|
+
},
|
|
257
|
+
base: {
|
|
258
|
+
type: "string",
|
|
259
|
+
description: "review/context: compatibility base ref (default HEAD~1). For review this maps to scope=all versus the base; compare uses it as the default older ref. context uses it only for risk.",
|
|
260
|
+
},
|
|
261
|
+
diffScope: {
|
|
262
|
+
type: "string",
|
|
263
|
+
enum: ["unstaged", "staged", "all", "compare"],
|
|
264
|
+
description: "review: unstaged=index vs worktree; staged=HEAD vs index; all=base (default HEAD~1) vs worktree; compare=base/from vs HEAD/to.",
|
|
265
|
+
},
|
|
266
|
+
from: {
|
|
267
|
+
type: "string",
|
|
268
|
+
description: "review: older ref of an explicit revision pair; requires `to`.",
|
|
269
|
+
},
|
|
270
|
+
toRevision: {
|
|
271
|
+
type: "string",
|
|
272
|
+
description: "review: newer ref of an explicit revision pair; requires `from`.",
|
|
273
|
+
},
|
|
274
|
+
reviewFiles: {
|
|
275
|
+
type: "array",
|
|
276
|
+
items: { type: "string" },
|
|
277
|
+
description: "review: explicit repo-relative file list, independent of live git state.",
|
|
278
|
+
},
|
|
279
|
+
force: {
|
|
280
|
+
type: "boolean",
|
|
281
|
+
description: "wiki only: force rewriting all wiki pages even if unchanged.",
|
|
282
|
+
},
|
|
283
|
+
detailLevel: {
|
|
284
|
+
type: "string",
|
|
285
|
+
enum: ["standard", "minimal", "source", "compact"],
|
|
286
|
+
description: "Structural explain/search/file_summary/batch_outline/project_overview: 'compact' returns the terse stable-identity contract. explain/review: 'minimal' caps each list and returns total counts for a token-efficient structured summary; 'standard' returns fuller arrays. compress: compact auditable metadata is the default; 'standard' returns the complete compression/omission envelope. query architecture_overview: 'minimal' aggregates cross-community edges; 'standard' adds the full per-edge list. map: 'minimal' drops member lists and aggregates edges; 'standard' returns the full map.",
|
|
287
|
+
},
|
|
288
|
+
query: {
|
|
289
|
+
type: "string",
|
|
290
|
+
description: "search: natural language query or keyword to find code symbols (e.g., 'session verification' or 'auth').",
|
|
291
|
+
},
|
|
292
|
+
limit: {
|
|
293
|
+
type: "number",
|
|
294
|
+
description: "search: maximum number of search results to return (default is 5, capped at 100). prs: maximum number of open PRs to list (default 50).",
|
|
295
|
+
},
|
|
296
|
+
byteBudget: {
|
|
297
|
+
type: "number",
|
|
298
|
+
minimum: 256,
|
|
299
|
+
description: "Maximum serialized UTF-8 response bytes (operation defaults apply).",
|
|
300
|
+
},
|
|
301
|
+
tokenBudget: {
|
|
302
|
+
type: "number",
|
|
303
|
+
minimum: 64,
|
|
304
|
+
description: "Maximum estimated tokens, using a deterministic four-bytes-per-token estimate.",
|
|
305
|
+
},
|
|
306
|
+
itemBudget: {
|
|
307
|
+
type: "number",
|
|
308
|
+
description: "Maximum items retained in every returned collection.",
|
|
309
|
+
},
|
|
310
|
+
includeSource: {
|
|
311
|
+
type: "boolean",
|
|
312
|
+
description: "explain minimal only: explicitly opt in to verbatim source (false by default).",
|
|
313
|
+
},
|
|
314
|
+
languages: {
|
|
315
|
+
type: "array",
|
|
316
|
+
items: { type: "string" },
|
|
317
|
+
description: "search: language filters.",
|
|
318
|
+
},
|
|
319
|
+
extensions: {
|
|
320
|
+
type: "array",
|
|
321
|
+
items: { type: "string" },
|
|
322
|
+
description: "search: file-extension filters.",
|
|
323
|
+
},
|
|
324
|
+
kinds: {
|
|
325
|
+
type: "array",
|
|
326
|
+
items: { type: "string" },
|
|
327
|
+
description: "search/large-code: symbol-kind filters.",
|
|
328
|
+
},
|
|
329
|
+
path: {
|
|
330
|
+
type: "string",
|
|
331
|
+
description: "search/large-code path filter; architecture_overview path scope.",
|
|
332
|
+
},
|
|
333
|
+
testScope: {
|
|
334
|
+
type: "string",
|
|
335
|
+
enum: ["all", "test", "production"],
|
|
336
|
+
description: "search: include all, test-only, or production-only symbols.",
|
|
337
|
+
},
|
|
338
|
+
offset: {
|
|
339
|
+
type: "number",
|
|
340
|
+
minimum: 0,
|
|
341
|
+
description: "search pagination offset.",
|
|
342
|
+
},
|
|
343
|
+
minLines: {
|
|
344
|
+
type: "number",
|
|
345
|
+
minimum: 0,
|
|
346
|
+
description: "large-code minimum line count.",
|
|
347
|
+
},
|
|
348
|
+
minComplexity: {
|
|
349
|
+
type: "number",
|
|
350
|
+
minimum: 0,
|
|
351
|
+
description: "large-functions minimum cyclomatic complexity.",
|
|
352
|
+
},
|
|
353
|
+
topN: {
|
|
354
|
+
type: "number",
|
|
355
|
+
minimum: 1,
|
|
356
|
+
maximum: 1000,
|
|
357
|
+
description: "map/query drill-down top-N cap.",
|
|
358
|
+
},
|
|
359
|
+
sort: {
|
|
360
|
+
type: "string",
|
|
361
|
+
enum: ["relevance", "name", "size", "degree", "complexity"],
|
|
362
|
+
description: "map/query drill-down sort.",
|
|
363
|
+
},
|
|
364
|
+
prNumber: {
|
|
365
|
+
type: "number",
|
|
366
|
+
description: "prs only: when set, return that single PR's detailed impact (impacted file list + community names) instead of the ranked summary table.",
|
|
367
|
+
},
|
|
368
|
+
prState: {
|
|
369
|
+
type: "string",
|
|
370
|
+
enum: ["open", "merged", "closed", "all"],
|
|
371
|
+
description: "prs audit only: GitHub pull-request state to inspect.",
|
|
372
|
+
},
|
|
373
|
+
branches: {
|
|
374
|
+
type: "string",
|
|
375
|
+
description: "prs audit only: bounded glob matched against head branches (`*` any text, `?` one character).",
|
|
376
|
+
},
|
|
377
|
+
auditRange: {
|
|
378
|
+
type: "string",
|
|
379
|
+
description: "prs audit only: inclusive pull-request number range such as 723..739.",
|
|
380
|
+
},
|
|
381
|
+
auditBase: {
|
|
382
|
+
type: "string",
|
|
383
|
+
description: "prs audit only: base revision for merge-commit range filtering.",
|
|
384
|
+
},
|
|
385
|
+
auditHead: {
|
|
386
|
+
type: "string",
|
|
387
|
+
description: "prs audit only: head revision for merge-commit range filtering.",
|
|
388
|
+
},
|
|
389
|
+
expectedLogin: {
|
|
390
|
+
type: "string",
|
|
391
|
+
description: "prs audit only: require this GitHub login; a mismatch returns unavailable without changing gh account state.",
|
|
392
|
+
},
|
|
393
|
+
worktreeAction: {
|
|
394
|
+
type: "string",
|
|
395
|
+
enum: ["status", "reconcile", "remove"],
|
|
396
|
+
description: "worktrees only: inspect, reconcile registry, or remove one linked checkout.",
|
|
397
|
+
},
|
|
398
|
+
worktreePath: {
|
|
399
|
+
type: "string",
|
|
400
|
+
description: "worktrees remove only: exact linked-worktree path.",
|
|
401
|
+
},
|
|
402
|
+
telemetryAction: {
|
|
403
|
+
type: "string",
|
|
404
|
+
enum: ["records", "status", "report", "export", "clear"],
|
|
405
|
+
description: "telemetry action.",
|
|
406
|
+
},
|
|
407
|
+
telemetryRetentionDays: {
|
|
408
|
+
type: "integer",
|
|
409
|
+
description: "telemetry retention days; default 30.",
|
|
410
|
+
},
|
|
411
|
+
task: {
|
|
412
|
+
type: "string",
|
|
413
|
+
description: "context only (required): free-text description of what you're doing (e.g. 'review PR #42', 'debug login timeout', 'how does auth work'). Drives a keyword heuristic that suggests which operation to run next — a hint only, never a constraint.",
|
|
414
|
+
},
|
|
415
|
+
changedFiles: {
|
|
416
|
+
type: "array",
|
|
417
|
+
items: { type: "string" },
|
|
418
|
+
description: "context only (optional): repo-relative paths the caller already knows changed. The engine's risk-score path always re-derives the diff from `git diff base`, so this is only a hint — an explicit empty array means 'no changes, skip the risk score'; omit it to auto-detect from git.",
|
|
419
|
+
},
|
|
420
|
+
repoPath: {
|
|
421
|
+
type: "string",
|
|
422
|
+
description: "Absolute path to the repo. Defaults to the current working directory.",
|
|
423
|
+
},
|
|
424
|
+
section: {
|
|
425
|
+
type: "string",
|
|
426
|
+
enum: [
|
|
427
|
+
"quickstart",
|
|
428
|
+
"query-patterns",
|
|
429
|
+
"federation",
|
|
430
|
+
"rename-safety",
|
|
431
|
+
"language-support",
|
|
432
|
+
"troubleshooting",
|
|
433
|
+
"installation",
|
|
434
|
+
"mcp",
|
|
435
|
+
"repositories",
|
|
436
|
+
"systems",
|
|
437
|
+
"provenance",
|
|
438
|
+
"dead-code",
|
|
439
|
+
"doctor",
|
|
440
|
+
],
|
|
441
|
+
description: "docs only (required): which documentation section to retrieve.",
|
|
442
|
+
},
|
|
443
|
+
systemAction: {
|
|
444
|
+
type: "string",
|
|
445
|
+
enum: ["list", "show", "validate", "query"],
|
|
446
|
+
description: "system only: list configured systems, show or validate one system, or query its declared relationships.",
|
|
447
|
+
},
|
|
448
|
+
repositoryAction: {
|
|
449
|
+
type: "string",
|
|
450
|
+
enum: ["discover", "init", "status", "doctor", "search"],
|
|
451
|
+
description: "repositories only: discover, initialize, inspect, diagnose, or independently search local checkouts.",
|
|
452
|
+
},
|
|
453
|
+
roots: {
|
|
454
|
+
type: "array",
|
|
455
|
+
items: { type: "string" },
|
|
456
|
+
description: "repositories only: local discovery roots; defaults to repoPath/current directory.",
|
|
457
|
+
},
|
|
458
|
+
linkedWorktrees: {
|
|
459
|
+
type: "string",
|
|
460
|
+
enum: ["skip", "include"],
|
|
461
|
+
description: "repositories only: explicit linked-worktree discovery policy.",
|
|
462
|
+
},
|
|
463
|
+
cursor: {
|
|
464
|
+
type: "string",
|
|
465
|
+
description: "repositories search only: opaque continuation cursor.",
|
|
466
|
+
},
|
|
467
|
+
allowPartial: {
|
|
468
|
+
type: "boolean",
|
|
469
|
+
description: "system/repository query: return partial results and name omissions.",
|
|
470
|
+
},
|
|
471
|
+
dryRun: {
|
|
472
|
+
type: "boolean",
|
|
473
|
+
description: "repositories init only: report planned changes without writing repositories.",
|
|
474
|
+
},
|
|
475
|
+
manifestPath: {
|
|
476
|
+
type: "string",
|
|
477
|
+
description: "repositories init: resumability manifest path for matching retries.",
|
|
478
|
+
},
|
|
479
|
+
packAction: {
|
|
480
|
+
type: "string",
|
|
481
|
+
enum: ["export", "read", "grep"],
|
|
482
|
+
description: "pack only: export context (default), read a bounded artifact range, or exact-regex grep an artifact.",
|
|
483
|
+
},
|
|
484
|
+
compressionAction: {
|
|
485
|
+
type: "string",
|
|
486
|
+
enum: ["create", "read", "diagnose", "delete"],
|
|
487
|
+
description: "compress: create, read, graph-diagnose, or delete a retained output artifact.",
|
|
488
|
+
},
|
|
489
|
+
text: {
|
|
490
|
+
type: "string",
|
|
491
|
+
description: "compress create: already-produced combined diagnostic text. Prefer artifactPath when the data is already on disk so it does not cross the tool boundary first.",
|
|
492
|
+
},
|
|
493
|
+
exitCode: {
|
|
494
|
+
type: ["number", "null"],
|
|
495
|
+
description: "compress create: original process exit code, preserved as metadata.",
|
|
496
|
+
},
|
|
497
|
+
lineBudget: {
|
|
498
|
+
type: "number",
|
|
499
|
+
minimum: 1,
|
|
500
|
+
maximum: 10000,
|
|
501
|
+
description: "compress create: hard maximum rendered content lines.",
|
|
502
|
+
},
|
|
503
|
+
compressionByteBudget: {
|
|
504
|
+
type: "number",
|
|
505
|
+
minimum: 256,
|
|
506
|
+
maximum: 4194304,
|
|
507
|
+
description: "compress create/read: hard UTF-8 content budget, separate from byteBudget for the whole gateway envelope.",
|
|
508
|
+
},
|
|
509
|
+
artifactId: {
|
|
510
|
+
type: "string",
|
|
511
|
+
description: "compress read/diagnose/delete: retained SHA-256 artifact identity.",
|
|
512
|
+
},
|
|
513
|
+
format: {
|
|
514
|
+
type: "string",
|
|
515
|
+
enum: ["markdown", "json", "xml"],
|
|
516
|
+
description: "pack export: deterministic artifact format.",
|
|
517
|
+
},
|
|
518
|
+
include: {
|
|
519
|
+
type: "array",
|
|
520
|
+
items: { type: "string" },
|
|
521
|
+
description: "pack export: composable repo-relative glob includes.",
|
|
522
|
+
},
|
|
523
|
+
exclude: {
|
|
524
|
+
type: "array",
|
|
525
|
+
items: { type: "string" },
|
|
526
|
+
description: "pack export: composable repo-relative glob excludes.",
|
|
527
|
+
},
|
|
528
|
+
filePolicies: {
|
|
529
|
+
type: "object",
|
|
530
|
+
additionalProperties: {
|
|
531
|
+
type: "string",
|
|
532
|
+
enum: ["full", "summary", "structure-only"],
|
|
533
|
+
},
|
|
534
|
+
description: "pack export: glob-to-policy map.",
|
|
535
|
+
},
|
|
536
|
+
alreadyPresent: {
|
|
537
|
+
type: "array",
|
|
538
|
+
items: { type: "string" },
|
|
539
|
+
description: "pack export: files already in caller context; omitted from source.",
|
|
540
|
+
},
|
|
541
|
+
chatFiles: {
|
|
542
|
+
type: "array",
|
|
543
|
+
items: { type: "string" },
|
|
544
|
+
description: "pack export: chat-mentioned file paths to omit.",
|
|
545
|
+
},
|
|
546
|
+
lineNumbers: {
|
|
547
|
+
type: "boolean",
|
|
548
|
+
description: "pack export: prefix source with line numbers.",
|
|
549
|
+
},
|
|
550
|
+
includeTree: {
|
|
551
|
+
type: "boolean",
|
|
552
|
+
description: "pack export: include deterministic relative-path tree.",
|
|
553
|
+
},
|
|
554
|
+
outputPath: {
|
|
555
|
+
type: "string",
|
|
556
|
+
description: "pack export: optional repo-contained artifact path.",
|
|
557
|
+
},
|
|
558
|
+
artifactPath: {
|
|
559
|
+
type: "string",
|
|
560
|
+
description: "pack read/grep: repo-contained artifact path. compress create: repo-contained regular input file; symlinks are refused.",
|
|
561
|
+
},
|
|
562
|
+
startLine: {
|
|
563
|
+
type: "number",
|
|
564
|
+
minimum: 1,
|
|
565
|
+
description: "pack read: first line (1-based).",
|
|
566
|
+
},
|
|
567
|
+
endLine: {
|
|
568
|
+
type: "number",
|
|
569
|
+
minimum: 1,
|
|
570
|
+
description: "pack read: last line (max 1000 lines).",
|
|
571
|
+
},
|
|
572
|
+
regex: {
|
|
573
|
+
type: "string",
|
|
574
|
+
description: "pack grep: exact JavaScript regular expression.",
|
|
575
|
+
},
|
|
576
|
+
regexFlags: {
|
|
577
|
+
type: "string",
|
|
578
|
+
description: "pack grep: optional gimsuy flags.",
|
|
579
|
+
},
|
|
580
|
+
gitDiffScope: {
|
|
581
|
+
type: "string",
|
|
582
|
+
enum: ["unstaged", "staged", "all", "compare"],
|
|
583
|
+
description: "pack export: optional C5-compatible diff scope.",
|
|
584
|
+
},
|
|
585
|
+
gitLog: {
|
|
586
|
+
type: "number",
|
|
587
|
+
minimum: 0,
|
|
588
|
+
maximum: 100,
|
|
589
|
+
description: "pack export: optional number of local one-line commits.",
|
|
590
|
+
},
|
|
591
|
+
statusAudit: {
|
|
592
|
+
type: "string",
|
|
593
|
+
enum: ["cached", "deep"],
|
|
594
|
+
description: "status only: cached (default) reuses a warm deep audit only after a bounded freshness probe; deep forces a complete filesystem/schema/orphan audit.",
|
|
595
|
+
},
|
|
596
|
+
timeoutMs: {
|
|
597
|
+
type: "number",
|
|
598
|
+
minimum: 0,
|
|
599
|
+
maximum: 300000,
|
|
600
|
+
description: "wait: maximum milliseconds to wait for freshness.",
|
|
601
|
+
},
|
|
602
|
+
client: {
|
|
603
|
+
type: "string",
|
|
604
|
+
enum: ["claude", "codex", "gemini", "antigravity"],
|
|
605
|
+
description: "doctor: optionally return one client-specific integration diagnosis.",
|
|
606
|
+
},
|
|
607
|
+
repairPlan: {
|
|
608
|
+
type: "boolean",
|
|
609
|
+
description: "repair: return a non-mutating auditable plan instead of applying it.",
|
|
610
|
+
},
|
|
611
|
+
persistTelemetry: {
|
|
612
|
+
type: "boolean",
|
|
613
|
+
description: "Opt in to local metadata-only JSONL persistence at .reckon-telemetry.jsonl; never records source.",
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
required: ["operation"],
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
];
|
|
620
|
+
}
|
|
621
|
+
/** @internal Baseline schema used only by the checked-in equivalence proof. */
|
|
622
|
+
export function getDocumentedKnodinToolsForSchemaProof() {
|
|
623
|
+
return buildDocumentedKnodinTools();
|
|
624
|
+
}
|
|
625
|
+
const COMPACT_TOOL_DESCRIPTION = "Local code intelligence with freshness evidence. Start with context; use docs for guidance.";
|
|
626
|
+
const COMPACT_PARAMETER_DESCRIPTIONS = {
|
|
627
|
+
operation: "Capability to run. Use docs for complete operation guidance.",
|
|
628
|
+
symbol: "Target symbol or file; exact meaning depends on operation.",
|
|
629
|
+
pattern: "Query pattern; applies only when operation is query.",
|
|
630
|
+
impactMode: "Impact by stable symbol identity or explicitly labeled files.",
|
|
631
|
+
apply: "Apply rename_preview edits; false returns a dry-run.",
|
|
632
|
+
diffScope: "Review scope: unstaged, staged, all, or compare.",
|
|
633
|
+
section: "Docs section; quickstart includes the complete parameter reference.",
|
|
634
|
+
persistTelemetry: "Persist metadata-only telemetry locally; never source.",
|
|
635
|
+
};
|
|
636
|
+
function compactParameterDescription(name, description) {
|
|
637
|
+
void description;
|
|
638
|
+
return COMPACT_PARAMETER_DESCRIPTIONS[name];
|
|
639
|
+
}
|
|
640
|
+
/** The compact, always-on MCP surface. Full prose remains available via docs. */
|
|
641
|
+
export function getKnodinTools() {
|
|
642
|
+
const tools = buildDocumentedKnodinTools();
|
|
643
|
+
for (const tool of tools) {
|
|
644
|
+
tool.description = COMPACT_TOOL_DESCRIPTION;
|
|
645
|
+
const properties = tool.inputSchema.properties;
|
|
646
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
647
|
+
property.description = compactParameterDescription(name, property.description);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return tools;
|
|
651
|
+
}
|
|
652
|
+
function renderParameterDocumentation() {
|
|
653
|
+
const [tool] = buildDocumentedKnodinTools();
|
|
654
|
+
const properties = tool.inputSchema.properties;
|
|
655
|
+
const lines = ["# knodin operation and parameter reference", "", tool.description ?? "", ""];
|
|
656
|
+
for (const [name, property] of Object.entries(properties)) {
|
|
657
|
+
lines.push(`## ${name}`, "", property.description ?? "No additional guidance.", "");
|
|
658
|
+
}
|
|
659
|
+
return lines.join("\n");
|
|
660
|
+
}
|
|
661
|
+
const RESPONSE_DEFAULTS = {
|
|
662
|
+
explain: { bytes: 65_536, tokens: 16_384, items: 200 },
|
|
663
|
+
"explain:minimal": { bytes: 16_384, tokens: 4_096, items: 25 },
|
|
664
|
+
map: { bytes: 131_072, tokens: 32_768, items: 200 },
|
|
665
|
+
"map:minimal": { bytes: 32_768, tokens: 8_192, items: 15 },
|
|
666
|
+
default: { bytes: 65_536, tokens: 16_384, items: 100 },
|
|
667
|
+
};
|
|
668
|
+
async function populateRepositoryDiagnoses(records, repositories, configRoot) {
|
|
669
|
+
repositories.length = 0;
|
|
670
|
+
for (const record of records) {
|
|
671
|
+
try {
|
|
672
|
+
const graph = attachLifecycleHealth(record.path, await engine.status(record.path, { audit: "deep" }));
|
|
673
|
+
const repository = await inventoryRepository(record, {
|
|
674
|
+
status: async () => graph,
|
|
675
|
+
systemMemberships: (target) => systemMembershipsForPath(loadSystemConfiguration(configRoot), target),
|
|
676
|
+
});
|
|
677
|
+
repositories.push({
|
|
678
|
+
...repository,
|
|
679
|
+
diagnosis: await diagnoseInstallation(record.path, {
|
|
680
|
+
currentVersion: graph.version,
|
|
681
|
+
runtimeCommand: [process.execPath, process.argv[1] ?? "", "serve"],
|
|
682
|
+
graph,
|
|
683
|
+
}),
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
catch (error) {
|
|
687
|
+
repositories.push({
|
|
688
|
+
schemaVersion: 1,
|
|
689
|
+
path: record.path,
|
|
690
|
+
classification: record.classification,
|
|
691
|
+
health: "unknown",
|
|
692
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
693
|
+
remediation: ["knodin doctor"],
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
async function handleRepositoriesOperation(options) {
|
|
699
|
+
const { repositoryAction, roots, repo, depth, linkedWorktrees, query, include, exclude, allowPartial, dryRun, manifestPath, itemBudget, byteBudget, tokenBudget, cursor, bounded, } = options;
|
|
700
|
+
const discoveryRoots = roots?.length ? roots : [repo];
|
|
701
|
+
const discoveryOptions = {
|
|
702
|
+
depth,
|
|
703
|
+
linkedWorktrees: linkedWorktrees ?? "skip",
|
|
704
|
+
};
|
|
705
|
+
const inventory = async () => {
|
|
706
|
+
const discovery = await discoverRepositories(discoveryRoots, discoveryOptions);
|
|
707
|
+
const systemConfig = loadSystemConfiguration(discoveryRoots[0] ?? repo);
|
|
708
|
+
const repositories = [];
|
|
709
|
+
for (const record of discovery.repositories) {
|
|
710
|
+
repositories.push(await inventoryRepository(record, {
|
|
711
|
+
status: async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })),
|
|
712
|
+
systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
|
|
713
|
+
}));
|
|
714
|
+
}
|
|
715
|
+
return { discovery, repositories };
|
|
716
|
+
};
|
|
717
|
+
if (repositoryAction === "init") {
|
|
718
|
+
const command = gatewayCliCommand();
|
|
719
|
+
const systemConfig = loadSystemConfiguration(discoveryRoots[0] ?? repo);
|
|
720
|
+
const summary = await initializeRepositories(discoveryRoots, {
|
|
721
|
+
command,
|
|
722
|
+
index: (target) => engine.index(target),
|
|
723
|
+
status: (target) => engine.status(target),
|
|
724
|
+
depth,
|
|
725
|
+
worktrees: linkedWorktrees,
|
|
726
|
+
dryRun,
|
|
727
|
+
manifestPath,
|
|
728
|
+
indexMode: (target) => indexModeForPath(systemConfig, target),
|
|
729
|
+
isolatedInitialize: (target) => runRepositoryInitializationProcess({ repository: target, command }),
|
|
730
|
+
});
|
|
731
|
+
const { repositories } = await inventory();
|
|
732
|
+
return bounded({ schemaVersion: 1, ...summary, repositories }, "repositories:init");
|
|
733
|
+
}
|
|
734
|
+
if (repositoryAction === "search") {
|
|
735
|
+
if (!query?.trim())
|
|
736
|
+
throw new Error("knodin repositories search requires a non-empty `query`");
|
|
737
|
+
const systemConfig = loadSystemConfiguration(discoveryRoots[0] ?? repo);
|
|
738
|
+
return searchRepositories(discoveryRoots, query, {
|
|
739
|
+
...discoveryOptions,
|
|
740
|
+
include,
|
|
741
|
+
exclude,
|
|
742
|
+
allowPartial,
|
|
743
|
+
itemBudget,
|
|
744
|
+
byteBudget,
|
|
745
|
+
tokenBudget,
|
|
746
|
+
cursor,
|
|
747
|
+
status: async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })),
|
|
748
|
+
search: (searchQuery, target, searchLimit, searchOffset) => engine.search(searchQuery, target, searchLimit, {
|
|
749
|
+
offset: searchOffset,
|
|
750
|
+
includeSource: true,
|
|
751
|
+
federate: false,
|
|
752
|
+
}),
|
|
753
|
+
systemMemberships: (target) => systemMembershipsForPath(systemConfig, target),
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
const { discovery, repositories } = await inventory();
|
|
757
|
+
if (repositoryAction === "doctor")
|
|
758
|
+
await populateRepositoryDiagnoses(discovery.repositories, repositories, discoveryRoots[0] ?? repo);
|
|
759
|
+
return bounded({
|
|
760
|
+
schemaVersion: 1,
|
|
761
|
+
linkedWorktrees: discoveryOptions.linkedWorktrees,
|
|
762
|
+
repositories,
|
|
763
|
+
issues: discovery.issues,
|
|
764
|
+
emptyRoots: discovery.emptyRoots,
|
|
765
|
+
staleLinkedWorktrees: discovery.staleLinkedWorktrees,
|
|
766
|
+
}, `repositories:${repositoryAction}`);
|
|
767
|
+
}
|
|
768
|
+
async function handleSystemOperation(options) {
|
|
769
|
+
const { systemAction, symbol, repo, allowPartial, bounded } = options;
|
|
770
|
+
const config = await enrichSystemRelationships(loadSystemConfiguration(repo));
|
|
771
|
+
if (systemAction === "list") {
|
|
772
|
+
return bounded({
|
|
773
|
+
schemaVersion: config.schemaVersion,
|
|
774
|
+
systems: config.systems.map(({ id, components }) => ({
|
|
775
|
+
id,
|
|
776
|
+
componentCount: components.length,
|
|
777
|
+
})),
|
|
778
|
+
}, "system:list");
|
|
779
|
+
}
|
|
780
|
+
if (!symbol)
|
|
781
|
+
throw new Error(`knodin system ${systemAction} requires \`symbol\``);
|
|
782
|
+
const system = config.systems.find(({ id }) => id === symbol);
|
|
783
|
+
if (!system) {
|
|
784
|
+
return bounded({
|
|
785
|
+
status: "not-found",
|
|
786
|
+
systemId: symbol,
|
|
787
|
+
available: config.systems.map(({ id }) => id),
|
|
788
|
+
}, `system:${systemAction}`);
|
|
789
|
+
}
|
|
790
|
+
if (systemAction === "show")
|
|
791
|
+
return bounded({ status: "ok", system, repositories: config.repositories }, "system:show");
|
|
792
|
+
const validation = await validateSystemHealth(config, symbol, async (target) => attachLifecycleHealth(target, await engine.status(target, { audit: "cached" })), repo);
|
|
793
|
+
if (systemAction === "validate")
|
|
794
|
+
return bounded({ systemId: symbol, ...validation }, "system:validate");
|
|
795
|
+
return bounded(queryConfiguredSystem(config, symbol, allowPartial === true, validation), "system:query");
|
|
796
|
+
}
|
|
797
|
+
async function handlePullRequestsOperation(options) {
|
|
798
|
+
const { repo, engine, limit, prNumber, prState, branches, auditRange, auditBase, auditHead, expectedLogin, } = options;
|
|
799
|
+
const prNum = prNumber === undefined || prNumber === null ? undefined : Number(prNumber);
|
|
800
|
+
if (prNum !== undefined && Number.isNaN(prNum))
|
|
801
|
+
return { error: "knodin prs: prNumber must be a number" };
|
|
802
|
+
if (prNum !== undefined) {
|
|
803
|
+
const reason = ghUnavailableReason();
|
|
804
|
+
if (reason)
|
|
805
|
+
return { error: reason };
|
|
806
|
+
return withStructuredError(() => triagePrDetail(repo, engine, prNum, limit));
|
|
807
|
+
}
|
|
808
|
+
return withStructuredError(() => auditPullRequests(repo, engine, {
|
|
809
|
+
state: prState,
|
|
810
|
+
limit,
|
|
811
|
+
branches,
|
|
812
|
+
range: auditRange,
|
|
813
|
+
base: auditBase,
|
|
814
|
+
head: auditHead,
|
|
815
|
+
expectedLogin,
|
|
816
|
+
}));
|
|
817
|
+
}
|
|
818
|
+
function validateKnodinArgs(args) {
|
|
819
|
+
const requireStringArray = (name, value) => {
|
|
820
|
+
if (value !== undefined &&
|
|
821
|
+
(!Array.isArray(value) || value.some((item) => typeof item !== "string")))
|
|
822
|
+
throw new Error(`knodin: ${name} must be an array of strings`);
|
|
823
|
+
};
|
|
824
|
+
for (const name of [
|
|
825
|
+
"languages",
|
|
826
|
+
"extensions",
|
|
827
|
+
"kinds",
|
|
828
|
+
"relationKinds",
|
|
829
|
+
"include",
|
|
830
|
+
"exclude",
|
|
831
|
+
"roots",
|
|
832
|
+
"alreadyPresent",
|
|
833
|
+
"chatFiles",
|
|
834
|
+
])
|
|
835
|
+
requireStringArray(name, args[name]);
|
|
836
|
+
requireStringArray("architectureFacets", args.architectureFacets);
|
|
837
|
+
if (args.direction !== undefined && !["upstream", "downstream", "both"].includes(args.direction))
|
|
838
|
+
throw new Error("knodin: invalid direction");
|
|
839
|
+
if (args.architectureFacets?.some((facet) => !["packages", "layers", "boundaries", "hotspots", "entryPoints", "languages"].includes(facet)))
|
|
840
|
+
throw new Error("knodin: invalid architectureFacets");
|
|
841
|
+
for (const [name, value, min] of [
|
|
842
|
+
["offset", args.offset, 0],
|
|
843
|
+
["minLines", args.minLines, 0],
|
|
844
|
+
["minComplexity", args.minComplexity, 0],
|
|
845
|
+
["topN", args.topN, 1],
|
|
846
|
+
])
|
|
847
|
+
if (value !== undefined && (!Number.isInteger(value) || value < min))
|
|
848
|
+
throw new Error(`knodin: ${name} must be an integer >= ${min}`);
|
|
849
|
+
if (args.topN !== undefined && args.topN > 1000)
|
|
850
|
+
throw new Error("knodin: topN must be <= 1000");
|
|
851
|
+
if (args.sort !== undefined &&
|
|
852
|
+
!["relevance", "name", "size", "degree", "complexity"].includes(args.sort))
|
|
853
|
+
throw new Error("knodin: invalid sort");
|
|
854
|
+
if (args.testScope !== undefined && !["all", "test", "production"].includes(args.testScope))
|
|
855
|
+
throw new Error("knodin: invalid testScope");
|
|
856
|
+
if (args.statusAudit !== undefined && !["cached", "deep"].includes(args.statusAudit))
|
|
857
|
+
throw new Error("knodin: invalid statusAudit");
|
|
858
|
+
if (args.timeoutMs !== undefined &&
|
|
859
|
+
(!Number.isInteger(args.timeoutMs) || args.timeoutMs < 0 || args.timeoutMs > 300_000))
|
|
860
|
+
throw new Error("knodin: timeoutMs must be an integer between 0 and 300000");
|
|
861
|
+
if (args.client !== undefined &&
|
|
862
|
+
!["claude", "codex", "gemini", "antigravity"].includes(args.client))
|
|
863
|
+
throw new Error("knodin: invalid client");
|
|
864
|
+
}
|
|
865
|
+
function compactExplainSource(result) {
|
|
866
|
+
const source = result.source
|
|
867
|
+
?.split("\n")
|
|
868
|
+
.map((line) => {
|
|
869
|
+
const separator = line.indexOf(": ");
|
|
870
|
+
if (separator <= 0)
|
|
871
|
+
return line;
|
|
872
|
+
const prefix = line.slice(0, separator);
|
|
873
|
+
return /^\d+$/.test(prefix) ? line.slice(separator + 2) : line;
|
|
874
|
+
})
|
|
875
|
+
.join("\n");
|
|
876
|
+
return {
|
|
877
|
+
mode: "source",
|
|
878
|
+
identity: result.identity,
|
|
879
|
+
symbol: result.symbol,
|
|
880
|
+
source,
|
|
881
|
+
staleness: result.staleness,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
function compactCompression(result) {
|
|
885
|
+
return {
|
|
886
|
+
status: result.status,
|
|
887
|
+
content: result.content,
|
|
888
|
+
// Compact tuples keep the default agent response small. `exit` is
|
|
889
|
+
// [code, signal], `budget` is [used lines, line limit, used UTF-8 bytes,
|
|
890
|
+
// byte limit], and `fidelity` is [detected, preserved, unpreserved ids].
|
|
891
|
+
exit: [result.exit.code, result.exit.signal],
|
|
892
|
+
artifactId: result.artifact.retained ? result.artifact.id : null,
|
|
893
|
+
budget: [
|
|
894
|
+
result.output.lines,
|
|
895
|
+
result.output.lineBudget,
|
|
896
|
+
result.output.bytes,
|
|
897
|
+
result.output.byteBudget,
|
|
898
|
+
],
|
|
899
|
+
fidelity: [
|
|
900
|
+
result.fidelity.detectedSignals,
|
|
901
|
+
result.fidelity.preservedSignals,
|
|
902
|
+
result.fidelity.unpreservedSignals,
|
|
903
|
+
],
|
|
904
|
+
// Omission tuples are [inclusive start line, inclusive end line, exact
|
|
905
|
+
// source bytes including original line delimiters].
|
|
906
|
+
omitted: result.omittedRanges.map(({ startLine, endLine, byteCount }) => [
|
|
907
|
+
startLine,
|
|
908
|
+
endLine,
|
|
909
|
+
byteCount,
|
|
910
|
+
]),
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function compactFailureRelation(relation) {
|
|
914
|
+
return [
|
|
915
|
+
relation.identity,
|
|
916
|
+
relation.symbol,
|
|
917
|
+
relation.file,
|
|
918
|
+
relation.kind,
|
|
919
|
+
relation.line,
|
|
920
|
+
relation.confidence,
|
|
921
|
+
];
|
|
922
|
+
}
|
|
923
|
+
function compactFailureDiagnosis(result) {
|
|
924
|
+
return {
|
|
925
|
+
status: result.status,
|
|
926
|
+
input: [result.input.kind, result.input.artifactId, result.input.bytes, result.input.complete],
|
|
927
|
+
// `reference` is [reported path, line, column]. `owner` is
|
|
928
|
+
// [stable identity, symbol, kind, start, end, signature].
|
|
929
|
+
diagnostics: result.diagnostics.map((diagnostic) => ({
|
|
930
|
+
reference: [
|
|
931
|
+
diagnostic.reference.path,
|
|
932
|
+
diagnostic.reference.line,
|
|
933
|
+
diagnostic.reference.column,
|
|
934
|
+
],
|
|
935
|
+
file: diagnostic.file,
|
|
936
|
+
package: diagnostic.package
|
|
937
|
+
? [diagnostic.package.name, diagnostic.package.manifest, diagnostic.package.kind]
|
|
938
|
+
: null,
|
|
939
|
+
owner: diagnostic.owner
|
|
940
|
+
? [
|
|
941
|
+
diagnostic.owner.identity,
|
|
942
|
+
diagnostic.owner.symbol,
|
|
943
|
+
diagnostic.owner.kind,
|
|
944
|
+
diagnostic.owner.line,
|
|
945
|
+
diagnostic.owner.endLine,
|
|
946
|
+
diagnostic.owner.signature,
|
|
947
|
+
]
|
|
948
|
+
: null,
|
|
949
|
+
tests: diagnostic.tests.map(compactFailureRelation),
|
|
950
|
+
upstream: diagnostic.upstream.map(compactFailureRelation),
|
|
951
|
+
downstream: diagnostic.downstream.map(compactFailureRelation),
|
|
952
|
+
recent: diagnostic.recentChanges.map(({ commit, at, subject }) => [
|
|
953
|
+
commit.slice(0, 12),
|
|
954
|
+
at,
|
|
955
|
+
subject,
|
|
956
|
+
]),
|
|
957
|
+
})),
|
|
958
|
+
unresolved: result.unresolved,
|
|
959
|
+
context: result.contextBundle,
|
|
960
|
+
freshness: [result.freshness.state, result.freshness.indexedHead, result.freshness.currentHead],
|
|
961
|
+
limitations: result.limitations,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
function hasCompressionResponseBudget(args) {
|
|
965
|
+
return (args.byteBudget !== undefined || args.tokenBudget !== undefined || args.itemBudget !== undefined);
|
|
966
|
+
}
|
|
967
|
+
async function handleCompressionDiagnosis(repo, args, engine, graphRead) {
|
|
968
|
+
const sources = [args.artifactId !== undefined, args.text !== undefined].filter(Boolean).length;
|
|
969
|
+
if (sources !== 1)
|
|
970
|
+
throw new Error("knodin compress diagnose requires exactly one of artifactId or text");
|
|
971
|
+
return graphRead("compress:diagnose", async () => {
|
|
972
|
+
const result = await diagnoseFailure(engine, repo, {
|
|
973
|
+
artifactId: args.artifactId,
|
|
974
|
+
text: args.text,
|
|
975
|
+
maxDiagnostics: args.limit ?? args.itemBudget,
|
|
976
|
+
contextLines: args.contextLines,
|
|
977
|
+
contextByteBudget: args.compressionByteBudget,
|
|
978
|
+
});
|
|
979
|
+
return args.detailLevel === "standard" ? result : compactFailureDiagnosis(result);
|
|
980
|
+
}, args.detailLevel !== "standard");
|
|
981
|
+
}
|
|
982
|
+
function handleCompressionRead(repo, args, observe, bounded) {
|
|
983
|
+
if (!args.artifactId)
|
|
984
|
+
throw new Error("knodin compress read requires `artifactId`");
|
|
985
|
+
const result = readOutputArtifact(repo, args.artifactId, {
|
|
986
|
+
startLine: args.startLine,
|
|
987
|
+
endLine: args.endLine,
|
|
988
|
+
byteBudget: args.compressionByteBudget,
|
|
989
|
+
raw: args.raw,
|
|
990
|
+
});
|
|
991
|
+
return hasCompressionResponseBudget(args)
|
|
992
|
+
? bounded(result, "compress:read")
|
|
993
|
+
: observe(result, "compress:read", true);
|
|
994
|
+
}
|
|
995
|
+
function handleCompressionCreate(repo, args, observe, bounded) {
|
|
996
|
+
const request = {
|
|
997
|
+
exitCode: args.exitCode,
|
|
998
|
+
signal: args.signal,
|
|
999
|
+
strategy: args.strategy,
|
|
1000
|
+
adapter: args.adapter,
|
|
1001
|
+
lineBudget: args.lineBudget,
|
|
1002
|
+
byteBudget: args.compressionByteBudget,
|
|
1003
|
+
contextLines: args.contextLines,
|
|
1004
|
+
retain: args.retain,
|
|
1005
|
+
redactSecrets: args.redactSecrets,
|
|
1006
|
+
maxInputBytes: args.maxInputBytes,
|
|
1007
|
+
};
|
|
1008
|
+
const sources = [
|
|
1009
|
+
args.text !== undefined,
|
|
1010
|
+
args.events !== undefined,
|
|
1011
|
+
args.artifactPath !== undefined,
|
|
1012
|
+
].filter(Boolean).length;
|
|
1013
|
+
if (sources !== 1)
|
|
1014
|
+
throw new Error("knodin compress create requires exactly one of text, events, artifactPath");
|
|
1015
|
+
let result;
|
|
1016
|
+
if (args.artifactPath)
|
|
1017
|
+
result = compressOutputFile(repo, args.artifactPath, request);
|
|
1018
|
+
else if (args.events)
|
|
1019
|
+
result = compressOutput(repo, { ...request, events: args.events });
|
|
1020
|
+
else
|
|
1021
|
+
result = compressOutput(repo, { ...request, text: args.text });
|
|
1022
|
+
const minimal = args.detailLevel !== "standard";
|
|
1023
|
+
const presentation = minimal ? compactCompression(result) : result;
|
|
1024
|
+
return hasCompressionResponseBudget(args)
|
|
1025
|
+
? bounded(presentation, "compress:create", minimal)
|
|
1026
|
+
: observe(presentation, "compress:create", minimal);
|
|
1027
|
+
}
|
|
1028
|
+
async function handleCompressionOperation(repo, args, engine, observe, bounded, graphRead) {
|
|
1029
|
+
const action = args.compressionAction ?? "create";
|
|
1030
|
+
switch (action) {
|
|
1031
|
+
case "diagnose":
|
|
1032
|
+
return handleCompressionDiagnosis(repo, args, engine, graphRead);
|
|
1033
|
+
case "read":
|
|
1034
|
+
return handleCompressionRead(repo, args, observe, bounded);
|
|
1035
|
+
case "delete":
|
|
1036
|
+
if (!args.artifactId)
|
|
1037
|
+
throw new Error("knodin compress delete requires `artifactId`");
|
|
1038
|
+
return observe(deleteOutputArtifact(repo, args.artifactId), "compress:delete", true);
|
|
1039
|
+
case "create":
|
|
1040
|
+
return handleCompressionCreate(repo, args, observe, bounded);
|
|
1041
|
+
default:
|
|
1042
|
+
throw new Error("knodin compress: invalid compressionAction");
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
async function runCompactExplain(symbol, repo, selector, file, byteBudget) {
|
|
1046
|
+
const result = await engine.explain(symbol, repo, "minimal", selector);
|
|
1047
|
+
let sourceFile = file;
|
|
1048
|
+
if (!sourceFile && result.identity && !result.ambiguity) {
|
|
1049
|
+
const locations = await engine.search(symbol, repo, 100, {
|
|
1050
|
+
includeSource: false,
|
|
1051
|
+
structural: true,
|
|
1052
|
+
});
|
|
1053
|
+
sourceFile = locations.results.find((row) => row.identity === result.identity)?.filePath;
|
|
1054
|
+
}
|
|
1055
|
+
return compactExplainResult(result, sourceFile, byteBudget);
|
|
1056
|
+
}
|
|
1057
|
+
async function runCompactSearch(query, repo, limit, options, byteBudget) {
|
|
1058
|
+
const result = await engine.search(query, repo, limit, {
|
|
1059
|
+
...options,
|
|
1060
|
+
includeSource: false,
|
|
1061
|
+
structural: true,
|
|
1062
|
+
});
|
|
1063
|
+
return compactSearchResult(result, query, byteBudget);
|
|
1064
|
+
}
|
|
1065
|
+
async function runCompactQuery(pattern, target, repo, limit, depth, selector, byteBudget) {
|
|
1066
|
+
const result = await engine.query(pattern, target, repo, undefined, limit, depth, undefined, selector, undefined, {});
|
|
1067
|
+
return compactQueryResult(result, byteBudget);
|
|
1068
|
+
}
|
|
1069
|
+
const NOT_COMPACT_STRUCTURAL = Symbol("not-compact-structural");
|
|
1070
|
+
function handleDocsOperation(section, bounded) {
|
|
1071
|
+
const topics = listDocTopics();
|
|
1072
|
+
if (!section)
|
|
1073
|
+
return bounded({
|
|
1074
|
+
error: `knodin docs requires \`section\` parameter. Valid sections: ${topics.join(", ")}`,
|
|
1075
|
+
}, "docs");
|
|
1076
|
+
const sectionContent = getDocSection(section);
|
|
1077
|
+
const content = section === "quickstart" && sectionContent !== undefined
|
|
1078
|
+
? `${sectionContent}\n\n${renderParameterDocumentation()}`
|
|
1079
|
+
: sectionContent;
|
|
1080
|
+
return content === undefined
|
|
1081
|
+
? bounded({ error: `unknown docs section. Valid sections: ${topics.join(", ")}` }, "docs")
|
|
1082
|
+
: bounded({ status: "ok", section, content }, "docs");
|
|
1083
|
+
}
|
|
1084
|
+
async function tryCompactStructural(args, repo, selector, bounded) {
|
|
1085
|
+
const { operation, detailLevel, includeSource, symbol, file, byteBudget } = args;
|
|
1086
|
+
const unavailable = async (op) => {
|
|
1087
|
+
const health = await compactGraphUnavailable(repo);
|
|
1088
|
+
return health ? bounded(health, op, true) : null;
|
|
1089
|
+
};
|
|
1090
|
+
if (operation === "explain" && detailLevel === "compact" && includeSource === true && symbol) {
|
|
1091
|
+
const blocked = await unavailable("explain");
|
|
1092
|
+
return {
|
|
1093
|
+
handled: true,
|
|
1094
|
+
value: blocked ?? (await runCompactExplain(symbol, repo, selector, file, byteBudget)),
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
if (operation === "search" && detailLevel === "compact" && args.query) {
|
|
1098
|
+
const blocked = await unavailable("search");
|
|
1099
|
+
return {
|
|
1100
|
+
handled: true,
|
|
1101
|
+
value: blocked ??
|
|
1102
|
+
(await runCompactSearch(args.query, repo, Math.min(args.limit ?? args.itemBudget ?? 5, args.itemBudget ?? 100), {
|
|
1103
|
+
languages: args.languages,
|
|
1104
|
+
extensions: args.extensions,
|
|
1105
|
+
kinds: args.kinds,
|
|
1106
|
+
path: args.path,
|
|
1107
|
+
testScope: args.testScope,
|
|
1108
|
+
offset: args.offset,
|
|
1109
|
+
}, byteBudget)),
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
const compactPattern = operation === "query" &&
|
|
1113
|
+
detailLevel === "compact" &&
|
|
1114
|
+
(args.pattern === "file_summary" ||
|
|
1115
|
+
args.pattern === "batch_outline" ||
|
|
1116
|
+
args.pattern === "project_overview")
|
|
1117
|
+
? args.pattern
|
|
1118
|
+
: null;
|
|
1119
|
+
if (!compactPattern)
|
|
1120
|
+
return { handled: false, value: NOT_COMPACT_STRUCTURAL };
|
|
1121
|
+
const blocked = await unavailable(`query:${compactPattern}`);
|
|
1122
|
+
return {
|
|
1123
|
+
handled: true,
|
|
1124
|
+
value: blocked ??
|
|
1125
|
+
(await runCompactQuery(compactPattern, symbol ?? "", repo, Math.min(args.limit ?? args.itemBudget ?? 100, args.itemBudget ?? 1000), args.depth, selector, byteBudget)),
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
export async function handleKnodinTool(args) {
|
|
1129
|
+
const startedAt = performance.now();
|
|
1130
|
+
const parsedArgs = (args ?? {});
|
|
1131
|
+
const { operation, symbol, base, diffScope, from, toRevision, reviewFiles, query, pattern, to, limit, depth, impactMode, direction, relationKinds, minConfidence, includeTests, includeDataFlow, flowVariable, apply, force, detailLevel, prNumber, prState, branches, auditRange, auditBase, auditHead, expectedLogin, worktreeAction, worktreePath, telemetryAction, telemetryRetentionDays, task, changedFiles, repoPath, section, systemAction, repositoryAction, roots, linkedWorktrees, cursor, allowPartial, dryRun, manifestPath, identity, file, kind, toIdentity, toFile, toKind, byteBudget, tokenBudget, itemBudget, includeSource, languages, extensions, kinds, path, architectureFacets, testScope, offset, minLines, minComplexity, topN, sort, packAction, format, include, exclude, filePolicies, alreadyPresent, chatFiles, lineNumbers, includeTree, outputPath, artifactPath, startLine, endLine, regex, regexFlags, gitDiffScope, gitLog, statusAudit, timeoutMs, client, repairPlan, persistTelemetry, } = parsedArgs;
|
|
1132
|
+
const repo = repoPath ?? process.cwd();
|
|
1133
|
+
const selector = {
|
|
1134
|
+
identity: expandCompactIdentity(identity),
|
|
1135
|
+
file,
|
|
1136
|
+
kind,
|
|
1137
|
+
toIdentity: expandCompactIdentity(toIdentity),
|
|
1138
|
+
toFile,
|
|
1139
|
+
toKind,
|
|
1140
|
+
};
|
|
1141
|
+
const budget = {
|
|
1142
|
+
bytes: byteBudget,
|
|
1143
|
+
tokens: tokenBudget,
|
|
1144
|
+
items: itemBudget,
|
|
1145
|
+
};
|
|
1146
|
+
const observe = (output, op, minimal = false) => {
|
|
1147
|
+
const root = output;
|
|
1148
|
+
const budgetMeta = root.responseBudget;
|
|
1149
|
+
if (op === "review") {
|
|
1150
|
+
measurePerfPhaseSync("review_serialization", () => JSON.stringify(output));
|
|
1151
|
+
}
|
|
1152
|
+
const telemetry = measureOutput({
|
|
1153
|
+
repo,
|
|
1154
|
+
operation: op,
|
|
1155
|
+
output: root,
|
|
1156
|
+
startedAt,
|
|
1157
|
+
detailMode: minimal ? "minimal" : (detailLevel ?? "standard"),
|
|
1158
|
+
truncated: budgetMeta?.truncated === true ||
|
|
1159
|
+
root.telemetry?.truncated === true,
|
|
1160
|
+
schemaTokens: gatewaySchemaTokens(),
|
|
1161
|
+
});
|
|
1162
|
+
telemetry.temperature = localTelemetry.some(({ operation, repositoryId }) => operation === op && repositoryId === telemetry.repositoryId)
|
|
1163
|
+
? "warm"
|
|
1164
|
+
: "cold";
|
|
1165
|
+
localTelemetry.push(telemetry);
|
|
1166
|
+
if (localTelemetry.length > 1000)
|
|
1167
|
+
localTelemetry.shift();
|
|
1168
|
+
if (persistTelemetry === true) {
|
|
1169
|
+
appendTelemetryRecord(repo, telemetry, telemetryRetentionDays);
|
|
1170
|
+
}
|
|
1171
|
+
return root;
|
|
1172
|
+
};
|
|
1173
|
+
const bounded = (result, op = String(operation), minimal = false) => {
|
|
1174
|
+
const key = `${op}${minimal ? ":minimal" : ""}`;
|
|
1175
|
+
const output = applyResponseBudget(result, op, budget, RESPONSE_DEFAULTS[key] ?? RESPONSE_DEFAULTS.default);
|
|
1176
|
+
return observe(output, op, minimal);
|
|
1177
|
+
};
|
|
1178
|
+
const graphRead = async (op, run, minimal = false) => {
|
|
1179
|
+
const health = await inspectGatewayGraphHealth(repo);
|
|
1180
|
+
if (!health.available)
|
|
1181
|
+
return bounded(health, op, minimal);
|
|
1182
|
+
const result = await run();
|
|
1183
|
+
const verified = await inspectGatewayGraphHealth(repo);
|
|
1184
|
+
if (!verified.available)
|
|
1185
|
+
return bounded(verified, op, minimal);
|
|
1186
|
+
return bounded(decorateGraphQueryResult(result, verified.state, verified.graph.freshness), op, minimal);
|
|
1187
|
+
};
|
|
1188
|
+
validateKnodinArgs(parsedArgs);
|
|
1189
|
+
const compactStructural = await tryCompactStructural(parsedArgs, repo, selector, bounded);
|
|
1190
|
+
if (compactStructural.handled)
|
|
1191
|
+
return compactStructural.value;
|
|
1192
|
+
if (detailLevel === "compact")
|
|
1193
|
+
throw new Error("knodin: compact detail is unsupported here");
|
|
1194
|
+
switch (operation) {
|
|
1195
|
+
case "telemetry": {
|
|
1196
|
+
const action = telemetryAction ?? "records";
|
|
1197
|
+
if (action === "records")
|
|
1198
|
+
return bounded({ records: localTelemetry.slice() }, "telemetry");
|
|
1199
|
+
if (action === "status")
|
|
1200
|
+
return bounded(telemetryStatus(repo, artifactPath, telemetryRetentionDays), "telemetry:status");
|
|
1201
|
+
const records = readTelemetryRecords(repo, artifactPath, telemetryRetentionDays);
|
|
1202
|
+
if (action === "report")
|
|
1203
|
+
return bounded(writeTelemetryReport(repo, records, outputPath), "telemetry:report");
|
|
1204
|
+
if (action === "export")
|
|
1205
|
+
return bounded(exportTelemetry(repo, records, outputPath), "telemetry:export");
|
|
1206
|
+
return bounded(clearTelemetry(repo, artifactPath), "telemetry:clear");
|
|
1207
|
+
}
|
|
1208
|
+
case "doctor": {
|
|
1209
|
+
const graph = await engine.status(repo);
|
|
1210
|
+
return bounded(await diagnoseInstallation(repo, {
|
|
1211
|
+
currentVersion: graph.version,
|
|
1212
|
+
runtimeCommand: [process.execPath, process.argv[1] ?? "", "serve"],
|
|
1213
|
+
graph,
|
|
1214
|
+
client,
|
|
1215
|
+
}), "doctor");
|
|
1216
|
+
}
|
|
1217
|
+
case "status":
|
|
1218
|
+
return engine.status(repo, { audit: statusAudit }).then((result) => bounded({
|
|
1219
|
+
...attachLifecycleHealth(repo, result),
|
|
1220
|
+
integration: inspectRepositoryIntegrationStatus(repo),
|
|
1221
|
+
update: trustedUpdateStatus({
|
|
1222
|
+
currentVersion: result.version,
|
|
1223
|
+
installMethod: "unknown",
|
|
1224
|
+
env: process.env,
|
|
1225
|
+
}),
|
|
1226
|
+
}, "status"));
|
|
1227
|
+
case "wait":
|
|
1228
|
+
return bounded(await waitForFresh(engine, repo, timeoutMs), "wait");
|
|
1229
|
+
case "worktrees":
|
|
1230
|
+
if (!worktreeAction || worktreeAction === "status")
|
|
1231
|
+
return bounded(await inspectWorktrees(repo, (worktree) => engine.status(worktree, { audit: "cached" })), "worktrees:status");
|
|
1232
|
+
if (worktreeAction === "reconcile")
|
|
1233
|
+
return bounded(reconcileWorktrees(repo), "worktrees:reconcile");
|
|
1234
|
+
if (!worktreePath)
|
|
1235
|
+
throw new Error("knodin worktrees remove requires `worktreePath`");
|
|
1236
|
+
return bounded(removeManagedWorktree(repo, worktreePath, dryRun !== false), "worktrees:remove");
|
|
1237
|
+
case "repair":
|
|
1238
|
+
if (repairPlan)
|
|
1239
|
+
return bounded(createRepairPlan(await engine.status(repo, { audit: "deep" })), "repair:plan");
|
|
1240
|
+
return engine.repair(repo).then((result) => bounded(result, "repair"));
|
|
1241
|
+
case "repositories": {
|
|
1242
|
+
if (!repositoryAction)
|
|
1243
|
+
throw new Error("knodin repositories requires `repositoryAction`");
|
|
1244
|
+
return handleRepositoriesOperation({
|
|
1245
|
+
repositoryAction,
|
|
1246
|
+
roots,
|
|
1247
|
+
repo,
|
|
1248
|
+
depth,
|
|
1249
|
+
linkedWorktrees,
|
|
1250
|
+
query,
|
|
1251
|
+
include,
|
|
1252
|
+
exclude,
|
|
1253
|
+
allowPartial,
|
|
1254
|
+
dryRun,
|
|
1255
|
+
manifestPath,
|
|
1256
|
+
itemBudget,
|
|
1257
|
+
byteBudget,
|
|
1258
|
+
tokenBudget,
|
|
1259
|
+
cursor,
|
|
1260
|
+
bounded,
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
case "system": {
|
|
1264
|
+
if (!systemAction)
|
|
1265
|
+
throw new Error("knodin system requires `systemAction`");
|
|
1266
|
+
return handleSystemOperation({
|
|
1267
|
+
systemAction,
|
|
1268
|
+
symbol,
|
|
1269
|
+
repo,
|
|
1270
|
+
allowPartial,
|
|
1271
|
+
bounded,
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
case "compress":
|
|
1275
|
+
return handleCompressionOperation(repo, parsedArgs, engine, observe, bounded, graphRead);
|
|
1276
|
+
case "pack": {
|
|
1277
|
+
if (packAction !== undefined && !["export", "read", "grep"].includes(packAction))
|
|
1278
|
+
throw new Error("knodin pack: invalid packAction");
|
|
1279
|
+
if (packAction === "read") {
|
|
1280
|
+
if (!artifactPath)
|
|
1281
|
+
throw new Error("knodin pack read requires `artifactPath`");
|
|
1282
|
+
return bounded(readPackedArtifact(repo, artifactPath, startLine, endLine, byteBudget), "pack:read");
|
|
1283
|
+
}
|
|
1284
|
+
if (packAction === "grep") {
|
|
1285
|
+
if (!artifactPath || !regex)
|
|
1286
|
+
throw new Error("knodin pack grep requires `artifactPath` and `regex`");
|
|
1287
|
+
return bounded(grepPackedArtifact(repo, artifactPath, regex, regexFlags, limit), "pack:grep");
|
|
1288
|
+
}
|
|
1289
|
+
return observe(exportContext(repo, {
|
|
1290
|
+
format,
|
|
1291
|
+
include,
|
|
1292
|
+
exclude,
|
|
1293
|
+
policies: filePolicies,
|
|
1294
|
+
alreadyPresent,
|
|
1295
|
+
chatFiles,
|
|
1296
|
+
lineNumbers,
|
|
1297
|
+
includeTree,
|
|
1298
|
+
outputPath,
|
|
1299
|
+
byteBudget,
|
|
1300
|
+
tokenBudget,
|
|
1301
|
+
git: gitDiffScope || gitLog !== undefined
|
|
1302
|
+
? { diffScope: gitDiffScope, from, to: toRevision, log: gitLog }
|
|
1303
|
+
: undefined,
|
|
1304
|
+
}), "pack:export");
|
|
1305
|
+
}
|
|
1306
|
+
case "docs":
|
|
1307
|
+
return handleDocsOperation(section, bounded);
|
|
1308
|
+
case "explain":
|
|
1309
|
+
if (!symbol)
|
|
1310
|
+
throw new Error("knodin explain requires `symbol`");
|
|
1311
|
+
return graphRead("explain", async () => {
|
|
1312
|
+
const result = await engine.explain(symbol, repo, detailLevel === "minimal" ? "minimal" : "standard", selector);
|
|
1313
|
+
if (detailLevel === "source" && !result.ambiguity)
|
|
1314
|
+
return compactExplainSource(result);
|
|
1315
|
+
if (detailLevel === "minimal" && includeSource !== true && result && !result.ambiguity) {
|
|
1316
|
+
delete result.source;
|
|
1317
|
+
result.sourceOmitted = true;
|
|
1318
|
+
result.sourceContinuation = {
|
|
1319
|
+
operation: "explain",
|
|
1320
|
+
symbol,
|
|
1321
|
+
includeSource: true,
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
return result;
|
|
1325
|
+
}, detailLevel === "minimal");
|
|
1326
|
+
case "review":
|
|
1327
|
+
return graphRead("review", () => engine.review(base ?? "HEAD~1", repo, detailLevel === "minimal" ? "minimal" : "standard", {
|
|
1328
|
+
scope: diffScope,
|
|
1329
|
+
files: reviewFiles,
|
|
1330
|
+
from,
|
|
1331
|
+
to: toRevision,
|
|
1332
|
+
}), detailLevel === "minimal");
|
|
1333
|
+
case "map":
|
|
1334
|
+
return graphRead("map", () => engine.map(repo, detailLevel === "standard" ? "standard" : "minimal", {
|
|
1335
|
+
topN,
|
|
1336
|
+
sort,
|
|
1337
|
+
relationKinds,
|
|
1338
|
+
}));
|
|
1339
|
+
case "wiki":
|
|
1340
|
+
return graphRead("wiki", () => engine.wiki(repo, force));
|
|
1341
|
+
case "context": {
|
|
1342
|
+
if (!task)
|
|
1343
|
+
throw new Error("knodin context requires `task`");
|
|
1344
|
+
return graphRead("context", () => buildKnodinContext(engine, task, repo, base, changedFiles));
|
|
1345
|
+
}
|
|
1346
|
+
case "search":
|
|
1347
|
+
if (!query)
|
|
1348
|
+
throw new Error("knodin search requires `query`");
|
|
1349
|
+
if (offset !== undefined && (!Number.isInteger(offset) || offset < 0))
|
|
1350
|
+
throw new Error("knodin search: offset must be a non-negative integer");
|
|
1351
|
+
return graphRead("search", () => engine.search(query, repo, Math.min(limit ?? itemBudget ?? 5, itemBudget ?? 100), {
|
|
1352
|
+
languages,
|
|
1353
|
+
extensions,
|
|
1354
|
+
kinds,
|
|
1355
|
+
path,
|
|
1356
|
+
testScope,
|
|
1357
|
+
includeSource,
|
|
1358
|
+
offset,
|
|
1359
|
+
}));
|
|
1360
|
+
case "prs": {
|
|
1361
|
+
return bounded(await handlePullRequestsOperation({
|
|
1362
|
+
repo,
|
|
1363
|
+
engine,
|
|
1364
|
+
limit,
|
|
1365
|
+
prNumber,
|
|
1366
|
+
prState,
|
|
1367
|
+
branches,
|
|
1368
|
+
auditRange,
|
|
1369
|
+
auditBase,
|
|
1370
|
+
auditHead,
|
|
1371
|
+
expectedLogin,
|
|
1372
|
+
}), "prs");
|
|
1373
|
+
}
|
|
1374
|
+
case "query": {
|
|
1375
|
+
if (!pattern)
|
|
1376
|
+
throw new Error("knodin query requires `pattern`");
|
|
1377
|
+
if (!QUERY_PATTERNS.includes(pattern))
|
|
1378
|
+
throw new Error(`knodin query: unknown pattern ${String(pattern)}`);
|
|
1379
|
+
if (limit !== undefined && (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1))
|
|
1380
|
+
throw new Error("knodin query: limit must be a positive integer");
|
|
1381
|
+
const repoWide = REPO_WIDE_QUERY_PATTERNS.includes(pattern);
|
|
1382
|
+
if (!symbol && !repoWide)
|
|
1383
|
+
throw new Error(`knodin query ${pattern} requires \`symbol\``);
|
|
1384
|
+
if (pattern === "shortest_path" && !to)
|
|
1385
|
+
throw new Error("knodin query shortest_path requires `to`");
|
|
1386
|
+
if (pattern === "rename_preview" && !to)
|
|
1387
|
+
throw new Error("knodin query rename_preview requires `symbol` and `to`");
|
|
1388
|
+
const queryHealth = GRAPH_INDEPENDENT_QUERY_PATTERNS.has(pattern)
|
|
1389
|
+
? null
|
|
1390
|
+
: await inspectGatewayGraphHealth(repo);
|
|
1391
|
+
if (queryHealth && !queryHealth.available)
|
|
1392
|
+
return bounded(queryHealth, `query:${pattern}`);
|
|
1393
|
+
if (pattern === "rename_preview") {
|
|
1394
|
+
// rename_preview routes through the rename engine: dry-run (preview +
|
|
1395
|
+
// unified diff) by default, writing to disk only when `apply` is true.
|
|
1396
|
+
return engine
|
|
1397
|
+
.rename(symbol ?? "", to ?? "", repo, apply === true, true, selector)
|
|
1398
|
+
.then((result) => bounded(decorateGraphQueryResult(result, queryHealth?.available ? queryHealth.state : "healthy", queryHealth?.available ? queryHealth.graph.freshness : undefined), "query:rename_preview"));
|
|
1399
|
+
}
|
|
1400
|
+
const queryResult = await engine.query(pattern, symbol ?? "", repo, to, Math.min(limit ?? itemBudget ?? 100, itemBudget ?? 1000), depth, detailLevel === "source" ? undefined : detailLevel, selector, pattern === "impact"
|
|
1401
|
+
? {
|
|
1402
|
+
mode: impactMode ?? "symbol",
|
|
1403
|
+
direction,
|
|
1404
|
+
relationKinds,
|
|
1405
|
+
minConfidence,
|
|
1406
|
+
includeTests,
|
|
1407
|
+
includeDataFlow,
|
|
1408
|
+
}
|
|
1409
|
+
: undefined, {
|
|
1410
|
+
minLines,
|
|
1411
|
+
minComplexity,
|
|
1412
|
+
kinds,
|
|
1413
|
+
path,
|
|
1414
|
+
direction: pattern === "traverse" ? direction : undefined,
|
|
1415
|
+
includeDataFlow: pattern === "traverse" ? includeDataFlow : undefined,
|
|
1416
|
+
flowVariable: pattern === "flow_analysis" ? flowVariable : undefined,
|
|
1417
|
+
architectureFacets,
|
|
1418
|
+
topN,
|
|
1419
|
+
sort,
|
|
1420
|
+
relationKinds: pattern === "impact" ? undefined : relationKinds,
|
|
1421
|
+
detailLevel: detailLevel === "source" ? undefined : detailLevel,
|
|
1422
|
+
});
|
|
1423
|
+
const verifiedQueryHealth = queryHealth ? await inspectGatewayGraphHealth(repo) : null;
|
|
1424
|
+
if (verifiedQueryHealth && !verifiedQueryHealth.available)
|
|
1425
|
+
return bounded(verifiedQueryHealth, `query:${pattern}`);
|
|
1426
|
+
const result = pattern === "impact" || pattern === "dead_code"
|
|
1427
|
+
? incorporateSystemQueryEvidence(await enrichSystemRelationships(loadSystemConfiguration(repo)), repo, pattern, symbol ?? "", queryResult)
|
|
1428
|
+
: queryResult;
|
|
1429
|
+
return bounded(queryHealth?.available
|
|
1430
|
+
? decorateGraphQueryResult(result, verifiedQueryHealth?.available ? verifiedQueryHealth.state : queryHealth.state, verifiedQueryHealth?.available
|
|
1431
|
+
? verifiedQueryHealth.graph.freshness
|
|
1432
|
+
: queryHealth.graph.freshness)
|
|
1433
|
+
: result, `query:${pattern}`);
|
|
1434
|
+
}
|
|
1435
|
+
default:
|
|
1436
|
+
throw new Error(`unknown knodin operation: ${String(operation)}`);
|
|
1437
|
+
}
|
|
1438
|
+
}
|