knodin 0.7.6 → 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 +19 -7
- package/benchmarks/competitors/SYNTHESIS.md +66 -0
- package/dist/bin/cli.js +2164 -108
- package/dist/bin/launcher.js +25 -3
- package/dist/src/agent-integration.js +304 -0
- package/dist/src/artifact-refresh.js +82 -0
- package/dist/src/cli-args.js +292 -0
- package/dist/src/cli-model.js +384 -0
- package/dist/src/codeflow-replay.js +81 -0
- package/dist/src/compact-structural.js +96 -0
- package/dist/src/compare.js +39 -0
- package/dist/src/competitive-cold-mcp.js +40 -0
- package/dist/src/competitive-constraints.js +21 -0
- package/dist/src/competitive-manifest.js +411 -0
- package/dist/src/competitive-measurement.js +183 -0
- package/dist/src/competitive-runner.js +487 -0
- package/dist/src/competitive-sandbox.js +108 -0
- package/dist/src/context-export.js +423 -0
- package/dist/src/context.js +102 -0
- package/dist/src/deterministic-random.js +34 -0
- package/dist/src/diagnostics-write-helper.js +473 -0
- package/dist/src/diagnostics.js +1476 -0
- package/dist/src/docs-sections.js +141 -0
- package/dist/src/doctor.js +382 -0
- package/dist/src/engine/ann-hnsw.js +261 -0
- package/dist/src/engine/embeddings.js +193 -0
- package/dist/src/engine/file-walker.js +49 -0
- package/dist/src/engine/git-history.js +289 -0
- package/dist/src/engine/index.js +14238 -0
- package/dist/src/engine/perf.js +115 -0
- package/dist/src/engine/prune.js +112 -0
- package/dist/src/engine/sarif-import.js +341 -0
- package/dist/src/engine/scip-import.js +423 -0
- package/dist/src/engine/source-policy.js +85 -0
- package/dist/src/engine/sqlite.js +71 -0
- package/dist/src/engine/state-paths.js +175 -0
- package/dist/src/engine/symbol-delete.js +58 -0
- package/dist/src/execution-profile.js +208 -0
- package/dist/src/failure-diagnosis.js +655 -0
- package/dist/src/fleet.js +7 -0
- package/dist/src/git-executable.js +31 -0
- package/dist/src/graph-layout.js +173 -0
- package/dist/src/graph-query-health.js +115 -0
- package/dist/src/hook-manager-integration.js +156 -0
- package/dist/src/index-activity.js +126 -0
- package/dist/src/init-progress-worker.js +106 -2
- package/dist/src/init-progress.js +155 -0
- package/dist/src/init.js +1295 -0
- package/dist/src/lifecycle-health.js +282 -0
- package/dist/src/lsp-readonly.js +217 -0
- 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 +630 -0
- package/dist/src/output-telemetry.js +368 -0
- package/dist/src/pr-triage.js +638 -0
- package/dist/src/progressive-evidence.js +477 -0
- package/dist/src/pure-compression-cli.js +102 -0
- package/dist/src/relationship-adapters.js +377 -0
- package/dist/src/release-attestation.js +533 -0
- package/dist/src/release-preflight.js +513 -0
- package/dist/src/repair-lease.js +85 -0
- package/dist/src/repair-progress-worker.js +120 -2
- package/dist/src/repair-progress.js +262 -0
- package/dist/src/repository-init-process.js +177 -0
- package/dist/src/repository-management.js +1261 -0
- package/dist/src/response-budget.js +196 -0
- package/dist/src/server.js +217 -0
- package/dist/src/structural-fast-path.js +344 -0
- package/dist/src/structural-snapshot.js +37 -0
- package/dist/src/system-config.js +638 -0
- package/dist/src/terminal-help.js +83 -0
- package/dist/src/tools/knodin-tools.js +1640 -0
- package/dist/src/update-ceremony.js +162 -0
- package/dist/src/update-policy.js +944 -0
- package/dist/src/update-trust.js +504 -0
- package/dist/src/version.js +13 -0
- package/dist/src/visualization.js +515 -0
- package/dist/src/wait-for-fresh.js +98 -0
- package/dist/src/worktree-lifecycle.js +234 -0
- 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/CONTAINED-EXECUTION.md +77 -0
- package/docs/DIAGNOSTICS.md +80 -0
- package/docs/GIT-HISTORY-REVIEW.md +39 -0
- package/docs/HANDOFF.md +180 -0
- package/docs/INSTALLATION.md +21 -18
- package/docs/MCP.md +59 -8
- package/docs/PROGRESSIVE-EVIDENCE.md +37 -0
- package/docs/PT-ACCESS-RECOMMENDATION.md +89 -0
- package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
- package/docs/REPOSITORIES-AND-WORKTREES.md +18 -6
- package/docs/SCIP-IMPORT.md +62 -0
- package/docs/SIGNED-UPDATES.md +151 -0
- package/docs/TELEMETRY.md +46 -0
- package/docs/TOKEN-OPTIMIZER-SCORECARD.md +79 -0
- package/docs/assets/knodin-favicon.svg +4 -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/docs/releases/0.5.1.md +17 -0
- package/docs/releases/0.6.0.md +18 -0
- package/docs/releases/0.7.0.md +24 -0
- package/docs/releases/0.7.1.md +21 -0
- package/docs/releases/0.7.2.md +21 -0
- package/docs/releases/0.7.3.md +23 -0
- package/docs/releases/0.7.4.md +17 -0
- package/docs/releases/0.7.5.md +20 -0
- package/docs/releases/0.8.0.md +74 -0
- package/docs/releases/0.8.2.md +34 -0
- package/package.json +127 -4
- package/roadmap/competitive-roadmap.md +3801 -0
- package/schemas/release-attestation-v1.schema.json +210 -0
- package/schemas/support-bundle-v2.schema.json +212 -0
- package/dist/chunks/chunk-DMQAGX77.js +0 -654
- package/dist/chunks/chunk-F4Z3Z766.js +0 -4
- package/dist/chunks/chunk-SIJAQVSX.js +0 -3
- package/dist/chunks/chunk-X6M4HUUE.js +0 -2
- package/dist/chunks/chunk-YPRMY2LP.js +0 -8
- package/dist/chunks/pure-compression-cli-4TA2TQD5.js +0 -5
- package/dist/chunks/server-7EDF4CBY.js +0 -14
- package/dist/chunks/structural-fast-path-KD5KQSPX.js +0 -4
- package/docs/releases/0.7.6.md +0 -25
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { Argument, Command, CommanderError, Option } from "commander";
|
|
2
|
+
import { formatTerminalHelp } from "./terminal-help.js";
|
|
3
|
+
function integer(value, flags) {
|
|
4
|
+
const parsed = Number(value);
|
|
5
|
+
if (!Number.isInteger(parsed))
|
|
6
|
+
throw new CommanderError(1, "knodin.invalidNumber", `${flags.split(" ")[0]} must be an integer`);
|
|
7
|
+
return parsed;
|
|
8
|
+
}
|
|
9
|
+
function numeric(value, flags) {
|
|
10
|
+
const parsed = Number(value);
|
|
11
|
+
if (!Number.isFinite(parsed))
|
|
12
|
+
throw new CommanderError(1, "knodin.invalidNumber", `${flags.split(" ")[0]} must be a number`);
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
function collect(value, previous) {
|
|
16
|
+
return [...previous, value];
|
|
17
|
+
}
|
|
18
|
+
function option(flags, description, parser) {
|
|
19
|
+
const configured = new Option(flags, description);
|
|
20
|
+
if (parser === "integer")
|
|
21
|
+
configured.argParser((value) => integer(value, flags));
|
|
22
|
+
else if (parser === "number")
|
|
23
|
+
configured.argParser((value) => numeric(value, flags));
|
|
24
|
+
else if (parser === "collect")
|
|
25
|
+
configured.argParser(collect).default([]);
|
|
26
|
+
return configured;
|
|
27
|
+
}
|
|
28
|
+
function leaf(parent, syntax, description, capture) {
|
|
29
|
+
const command = parent.command(syntax).description(description).allowExcessArguments(false);
|
|
30
|
+
command.action((...values) => capture(values.at(-1)));
|
|
31
|
+
return command;
|
|
32
|
+
}
|
|
33
|
+
function addGlobalOptions(program) {
|
|
34
|
+
program
|
|
35
|
+
.option("-v, --version", "print the installed knodin version")
|
|
36
|
+
.addOption(option("--repo <path>", "target a repository instead of the current directory"))
|
|
37
|
+
.option("--json", "emit stable JSON")
|
|
38
|
+
.addOption(option("--identity <id>", "select a stable symbol identity"))
|
|
39
|
+
.addOption(option("--file <path>", "select a repo-relative definition file"))
|
|
40
|
+
.addOption(option("--kind <kind>", "select a symbol kind"))
|
|
41
|
+
.addOption(option("--to-identity <id>", "select a destination identity"))
|
|
42
|
+
.addOption(option("--to-file <path>", "select a destination file"))
|
|
43
|
+
.addOption(option("--to-kind <kind>", "select a destination kind"))
|
|
44
|
+
.addOption(option("--bytes <count>", "bound serialized response bytes", "integer"))
|
|
45
|
+
.addOption(option("--tokens <count>", "bound serialized response tokens", "integer"))
|
|
46
|
+
.addOption(option("--items <count>", "bound serialized response items", "integer"))
|
|
47
|
+
.option("--impact-mode <mode>", "symbol or file impact")
|
|
48
|
+
.option("--direction <direction>", "upstream, downstream, or both")
|
|
49
|
+
.addOption(option("--depth <count>", "bounded traversal depth", "integer"))
|
|
50
|
+
.option("--relations <kinds>", "comma-separated relationship kinds")
|
|
51
|
+
.addOption(option("--min-confidence <value>", "minimum edge confidence", "number"))
|
|
52
|
+
.option("--limit <count>", "result limit")
|
|
53
|
+
.option("--exclude-tests", "exclude tests from impact")
|
|
54
|
+
.option("--data-flow", "include bounded data-flow evidence");
|
|
55
|
+
}
|
|
56
|
+
function addRemoteCommands(program, capture) {
|
|
57
|
+
const remote = program
|
|
58
|
+
.command("remote")
|
|
59
|
+
.description("mirror repositories that are reachable but not checked out locally");
|
|
60
|
+
leaf(remote, "add <url>", "acquire a read-only mirror and index it", capture)
|
|
61
|
+
.option("--no-index", "acquire the clone without building a graph")
|
|
62
|
+
.option("--defer-semantic", "build structure only; leave semantic search coverage for a later `knodin index`");
|
|
63
|
+
leaf(remote, "list", "list acquired mirrors with their snapshot and size", capture);
|
|
64
|
+
leaf(remote, "remove <identity>", "delete a mirror's clone, graph, and registry entry", capture);
|
|
65
|
+
leaf(remote, "refresh <identity>", "fetch the mirror again and re-index it", capture);
|
|
66
|
+
}
|
|
67
|
+
function addRepositoryCommands(program, capture) {
|
|
68
|
+
const repos = program.command("repos").description("manage a portfolio of repositories");
|
|
69
|
+
for (const action of ["discover", "init", "status", "doctor"]) {
|
|
70
|
+
const command = leaf(repos, `${action} <roots...>`, `${action} repositories beneath one or more roots`, capture)
|
|
71
|
+
.addOption(option("--depth <count>", "maximum discovery depth", "integer"))
|
|
72
|
+
.option("--linked-worktrees <mode>", "skip or include linked worktrees");
|
|
73
|
+
if (action === "init") {
|
|
74
|
+
command
|
|
75
|
+
.addOption(option("--include <selector>", "include a repository id or path", "collect"))
|
|
76
|
+
.addOption(option("--exclude <selector>", "exclude a repository id or path", "collect"))
|
|
77
|
+
.addOption(option("--memory-limit-mib <mib>", "optional per-repository worker RSS ceiling (default: uncapped)", "integer"))
|
|
78
|
+
.option("--manifest <path>", "write or resume a portfolio manifest")
|
|
79
|
+
.option("--dry-run", "report actions without changing repositories");
|
|
80
|
+
}
|
|
81
|
+
if (action === "discover") {
|
|
82
|
+
command
|
|
83
|
+
.option("--signals", "include bounded repository applicability signals")
|
|
84
|
+
.addOption(option("--items <count>", "bound returned repositories and signal arrays", "integer"))
|
|
85
|
+
.addOption(option("--bytes <count>", "bound serialized response bytes", "integer"))
|
|
86
|
+
.addOption(option("--tokens <count>", "bound estimated response tokens", "integer"))
|
|
87
|
+
.addHelpText("after", "\n--signals returns hookManager, markerFiles, sanitized remotes, ciProviders, agentConfigs, and aidevTrackReferenced when known; every returned repository has signals, including {}. Inspection uses fixed documented marker/hook allowlists, reads only bounded hook configuration, and performs no network, credential, or marker-content reads. Linked worktrees inspect their own checkout when --linked-worktrees include is selected. See docs/REPOSITORIES-AND-WORKTREES.md for omissions and limits.\n");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
leaf(repos, "search <query>", "search selected repositories sequentially", capture)
|
|
91
|
+
.addOption(option("--root <path>", "portfolio root", "collect"))
|
|
92
|
+
.addOption(option("--include <selector>", "include a repository id or path", "collect"))
|
|
93
|
+
.addOption(option("--exclude <selector>", "exclude a repository id or path", "collect"))
|
|
94
|
+
.option("--cursor <cursor>", "resume from an opaque cursor")
|
|
95
|
+
.option("--allow-partial", "return healthy repository results when another degrades");
|
|
96
|
+
const fleet = program.command("fleet").description("deprecated repository-fleet compatibility");
|
|
97
|
+
leaf(fleet, "init <roots...>", "initialize a repository fleet", capture)
|
|
98
|
+
.addOption(option("--depth <count>", "maximum discovery depth", "integer"))
|
|
99
|
+
.option("--worktrees <mode>", "skip or include linked worktrees")
|
|
100
|
+
.option("--dry-run", "report actions without mutation");
|
|
101
|
+
}
|
|
102
|
+
function addGraphCommands(program, capture) {
|
|
103
|
+
leaf(program, "context <task> [base]", "build compact task orientation", capture);
|
|
104
|
+
leaf(program, "explain <symbol> [detail]", "explain one ambiguity-safe symbol", capture);
|
|
105
|
+
leaf(program, "review [base] [detail]", "review an explicit Git diff scope", capture)
|
|
106
|
+
.option("--scope <scope>", "unstaged, staged, all, or compare")
|
|
107
|
+
.option("--from <ref>", "comparison start revision")
|
|
108
|
+
.option("--to <ref>", "comparison end revision")
|
|
109
|
+
.option("--files <paths>", "comma-separated repo-relative paths");
|
|
110
|
+
leaf(program, "map", "show architecture communities and edges", capture)
|
|
111
|
+
.option("--standard", "include full map detail")
|
|
112
|
+
.addOption(option("--top <count>", "maximum ranked communities", "integer"))
|
|
113
|
+
.option("--sort <mode>", "relevance, name, size, degree, or complexity")
|
|
114
|
+
.option("--relations <kinds>", "comma-separated relationship kinds");
|
|
115
|
+
leaf(program, "wiki", "write local architecture wiki pages", capture).option("--force", "rewrite unchanged pages");
|
|
116
|
+
leaf(program, "visualize [entry]", "write a local architecture/call-flow HTML artifact", capture)
|
|
117
|
+
.requiredOption("--output <path>", "repo-relative HTML output path")
|
|
118
|
+
.option("--scope <scope>", "call-flow (default) or repo")
|
|
119
|
+
.option("--granularity <granularity>", "repo scope: file (default) or symbol")
|
|
120
|
+
.addOption(option("--depth <count>", "call-flow depth", "integer"))
|
|
121
|
+
.addOption(option("--max-bytes <count>", "hard artifact budget", "integer"));
|
|
122
|
+
leaf(program, "search <query> [limit]", "hybrid symbol search", capture)
|
|
123
|
+
.option("--languages <values>", "comma-separated languages")
|
|
124
|
+
.option("--extensions <values>", "comma-separated extensions")
|
|
125
|
+
.option("--kinds <values>", "comma-separated symbol kinds")
|
|
126
|
+
.option("--path <prefix>", "repo-relative path prefix")
|
|
127
|
+
.option("--tests-only", "search test symbols only")
|
|
128
|
+
.option("--production-only", "search production symbols only")
|
|
129
|
+
.option("--no-source", "omit source snippets")
|
|
130
|
+
.addOption(option("--offset <count>", "pagination offset", "integer"))
|
|
131
|
+
.addOption(option("--limit <count>", "result limit", "integer"));
|
|
132
|
+
leaf(program, "query <pattern> [targets...]", "run one structured graph query", capture)
|
|
133
|
+
.option("--impact-mode <mode>", "symbol or file impact")
|
|
134
|
+
.option("--direction <direction>", "upstream, downstream, or both")
|
|
135
|
+
.addOption(option("--depth <count>", "bounded traversal depth", "integer"))
|
|
136
|
+
.option("--relations <kinds>", "comma-separated relationship kinds")
|
|
137
|
+
.addOption(option("--min-confidence <value>", "minimum edge confidence", "number"))
|
|
138
|
+
.option("--exclude-tests", "exclude tests from impact")
|
|
139
|
+
.option("--data-flow", "include bounded data-flow evidence")
|
|
140
|
+
.option("--limit <count>", "result limit")
|
|
141
|
+
.addOption(option("--min-lines <count>", "minimum line count", "integer"))
|
|
142
|
+
.addOption(option("--min-complexity <count>", "minimum complexity", "integer"))
|
|
143
|
+
.option("--kinds <values>", "comma-separated symbol kinds")
|
|
144
|
+
.option("--path <prefix>", "repo-relative path prefix")
|
|
145
|
+
.option("--variable <name>", "flow-analysis variable")
|
|
146
|
+
.option("--facets <values>", "comma-separated architecture facets")
|
|
147
|
+
.addOption(option("--top <count>", "maximum ranked results", "integer"))
|
|
148
|
+
.option("--sort <mode>", "result ordering");
|
|
149
|
+
leaf(program, "rename <old> <new>", "preview or apply an ambiguity-safe rename", capture)
|
|
150
|
+
.option("--apply", "apply the verified edit")
|
|
151
|
+
.option("--no-verify", "skip post-apply typecheck");
|
|
152
|
+
}
|
|
153
|
+
function addArtifactCommands(program, capture) {
|
|
154
|
+
leaf(program, "evidence <level> <file>", "deliver progressive source evidence", capture)
|
|
155
|
+
.option("--continuation <handle>", "resume an exact prior response")
|
|
156
|
+
.option("--baseline-hash <sha256>", "complete baseline SHA-256")
|
|
157
|
+
.addOption(option("--baseline-bytes <count>", "complete baseline UTF-8 bytes", "integer"))
|
|
158
|
+
.addOption(option("--start <line>", "first source line", "integer"))
|
|
159
|
+
.addOption(option("--end <line>", "last source line", "integer"));
|
|
160
|
+
const pack = program.command("pack").description("export bounded portable context");
|
|
161
|
+
pack
|
|
162
|
+
.argument("[input]")
|
|
163
|
+
.option("--format <format>", "markdown, json, or xml")
|
|
164
|
+
.option("--include <globs>", "comma-separated include globs")
|
|
165
|
+
.option("--exclude <globs>", "comma-separated exclude globs")
|
|
166
|
+
.option("--policy <assignments>", "comma-separated glob policies")
|
|
167
|
+
.option("--already-present <paths>", "comma-separated paths already in context")
|
|
168
|
+
.option("--chat-files <paths>", "comma-separated chat paths")
|
|
169
|
+
.option("--line-numbers", "include source line numbers")
|
|
170
|
+
.option("--tree", "include repository tree")
|
|
171
|
+
.option("--output <path>", "write a repo-relative retained artifact")
|
|
172
|
+
.option("--diff-scope <scope>", "unstaged, staged, all, or compare")
|
|
173
|
+
.option("--from <ref>", "comparison start revision")
|
|
174
|
+
.option("--to <ref>", "comparison end revision")
|
|
175
|
+
.addOption(option("--log <count>", "include recent commits", "integer"))
|
|
176
|
+
.action((...values) => capture(values.at(-1)));
|
|
177
|
+
leaf(pack, "read <artifact>", "read a retained context artifact", capture)
|
|
178
|
+
.addOption(option("--start <line>", "first line", "integer"))
|
|
179
|
+
.addOption(option("--end <line>", "last line", "integer"));
|
|
180
|
+
leaf(pack, "grep <artifact> <regex>", "search an artifact with a linear-time regex", capture)
|
|
181
|
+
.option("--flags <flags>", "regular-expression flags")
|
|
182
|
+
.addOption(option("--limit <count>", "match limit", "integer"));
|
|
183
|
+
const compress = program.command("compress").description("compress already-produced output");
|
|
184
|
+
compress
|
|
185
|
+
.argument("[input]")
|
|
186
|
+
.option("--strategy <strategy>", "smart, head-tail, or errors-only")
|
|
187
|
+
.option("--adapter <adapter>", "structured output adapter")
|
|
188
|
+
.addOption(option("--lines <count>", "hard line budget", "integer"))
|
|
189
|
+
.addOption(option("--max-output-bytes <count>", "hard output byte budget", "integer"))
|
|
190
|
+
.addOption(option("--context <count>", "signal context lines", "integer"))
|
|
191
|
+
.addOption(option("--exit-code <code>", "source process exit code", "integer"))
|
|
192
|
+
.option("--signal <name>", "source process termination signal")
|
|
193
|
+
.addOption(option("--max-input-bytes <count>", "hard input byte limit", "integer"))
|
|
194
|
+
.option("--no-retain", "do not retain raw drill-down data")
|
|
195
|
+
.option("--no-redact", "disable secret redaction")
|
|
196
|
+
.action((...values) => capture(values.at(-1)));
|
|
197
|
+
for (const action of ["read", "diagnose"]) {
|
|
198
|
+
leaf(compress, `${action} <artifact>`, `${action} a retained output artifact`, capture)
|
|
199
|
+
.addOption(option("--start <line>", "first line", "integer"))
|
|
200
|
+
.addOption(option("--end <line>", "last line", "integer"))
|
|
201
|
+
.addOption(option("--max-output-bytes <count>", "hard output byte budget", "integer"))
|
|
202
|
+
.addOption(option("--context <count>", "diagnostic context lines", "integer"))
|
|
203
|
+
.addOption(option("--limit <count>", "diagnostic result limit", "integer"))
|
|
204
|
+
.addOption(option("--offset <count>", "resume retained diagnostics at offset", "integer"))
|
|
205
|
+
.option("--raw", "return unredacted retained bytes");
|
|
206
|
+
}
|
|
207
|
+
leaf(compress, "delete <artifact>", "delete a retained output artifact", capture);
|
|
208
|
+
}
|
|
209
|
+
function createCliProgram(capture = () => { }) {
|
|
210
|
+
const program = new Command("knodin")
|
|
211
|
+
.description("knodin — source-evidenced local code intelligence with known bounds")
|
|
212
|
+
.showHelpAfterError()
|
|
213
|
+
.showSuggestionAfterError()
|
|
214
|
+
.passThroughOptions(false)
|
|
215
|
+
.allowExcessArguments(false)
|
|
216
|
+
.exitOverride();
|
|
217
|
+
program.configureOutput({ writeErr: () => { } });
|
|
218
|
+
addGlobalOptions(program);
|
|
219
|
+
program.addHelpText("after", "\nMCP callers use the `prs` operation and corresponding operations on the single `knodin` gateway.\nknodin update status|check|explain|apply|rollback consumes only threshold-signed metadata.\n");
|
|
220
|
+
leaf(program, "init", "initialize graph, lifecycle hooks, and agent integration", capture)
|
|
221
|
+
.option("--scope <scope>", "personal, team, or cli-only")
|
|
222
|
+
.addHelpText("after", "\nUsage: knodin init [--scope personal|team|cli-only] [--json]\n\npersonal recommended; local/excluded agent adapters and a clean Git status\nteam commit-ready shared agent configuration\ncli-only no agent discovery; AI agents will not know to invoke knodin\n\nChange later with `knodin configure --scope <scope>`.\nTracked files are never added to Git exclude files.\n");
|
|
223
|
+
leaf(program, "configure", "change or inspect agent integration", capture)
|
|
224
|
+
.option("--scope <scope>", "personal, team, or cli-only")
|
|
225
|
+
.option("--status", "inspect configuration without changing it");
|
|
226
|
+
leaf(program, "index [files...]", "index a repository or selected files", capture)
|
|
227
|
+
.option("--clean", "rebuild selected index state")
|
|
228
|
+
.option("--force", "force clean indexing")
|
|
229
|
+
.option("--scip <file>", "opt in to a bounded local SCIP protobuf import")
|
|
230
|
+
.addOption(option("--scip-max-bytes <count>", "raise the SCIP input size ceiling", "integer"))
|
|
231
|
+
.addOption(option("--scip-max-files <count>", "raise the SCIP document ceiling", "integer"))
|
|
232
|
+
.addOption(option("--scip-max-facts <count>", "raise the SCIP fact ceiling", "integer"))
|
|
233
|
+
.addOption(option("--scip-timeout-ms <count>", "raise the SCIP read budget", "integer"))
|
|
234
|
+
.option("--sarif <file>", "import bounded local analyzer findings from a SARIF log")
|
|
235
|
+
.addOption(option("--sarif-max-bytes <count>", "raise the SARIF log size ceiling", "integer"))
|
|
236
|
+
.addOption(option("--sarif-max-findings <count>", "raise the imported findings ceiling", "integer"))
|
|
237
|
+
.addOption(option("--sarif-timeout-ms <count>", "raise the SARIF read budget", "integer"));
|
|
238
|
+
leaf(program, "doctor", "diagnose installation, clients, hooks, graph, and updates", capture).option("--client <client>", "claude, codex, gemini, or antigravity");
|
|
239
|
+
leaf(program, "status", "report graph, lifecycle, integration, and update state", capture)
|
|
240
|
+
.option("--deep", "run a full graph audit")
|
|
241
|
+
.option("--watch", "stream status snapshots")
|
|
242
|
+
.addOption(option("--interval <seconds>", "watch interval", "number"));
|
|
243
|
+
leaf(program, "wait", "wait for current graph evidence", capture)
|
|
244
|
+
.option("--fresh", "wait for a fresh graph")
|
|
245
|
+
.addOption(option("--timeout <seconds>", "deadline", "number"));
|
|
246
|
+
leaf(program, "repair", "audit and reconcile graph state", capture)
|
|
247
|
+
.option("--plan", "report the repair plan without mutation")
|
|
248
|
+
.option("--jsonl", "stream JSONL progress")
|
|
249
|
+
.option("--progress <mode>", "tty, jsonl, plain, or none")
|
|
250
|
+
.option("--progress-interval <duration>", "progress interval such as 750ms, 30s, or 2m");
|
|
251
|
+
leaf(program, "serve", "run the one-tool MCP gateway on stdio", capture);
|
|
252
|
+
leaf(program, "version", "print the installed knodin version", capture);
|
|
253
|
+
leaf(program, "hook-refresh <kind> [values...]", "internal Git lifecycle refresh", capture);
|
|
254
|
+
leaf(program, "refresh-artifacts [event]", "refresh external graph artifacts", capture);
|
|
255
|
+
addRepositoryCommands(program, capture);
|
|
256
|
+
addRemoteCommands(program, capture);
|
|
257
|
+
addGraphCommands(program, capture);
|
|
258
|
+
addArtifactCommands(program, capture);
|
|
259
|
+
const system = program.command("system").description("inspect declared multi-repository systems");
|
|
260
|
+
leaf(system, "list", "list configured systems", capture);
|
|
261
|
+
for (const action of ["show", "validate", "query"]) {
|
|
262
|
+
const command = leaf(system, `${action} <system-id>`, `${action} one configured system`, capture);
|
|
263
|
+
if (action === "query")
|
|
264
|
+
command.option("--allow-partial", "return healthy components");
|
|
265
|
+
}
|
|
266
|
+
const update = program
|
|
267
|
+
.command("update")
|
|
268
|
+
.description("inspect or apply threshold-signed updates")
|
|
269
|
+
.allowExcessArguments(false)
|
|
270
|
+
.addArgument(new Argument("<action>").choices(["status", "check", "explain", "apply", "rollback"]));
|
|
271
|
+
update.action((...values) => capture(values.at(-1)));
|
|
272
|
+
leaf(program, "docs <topic>", "read canonical product documentation", capture);
|
|
273
|
+
leaf(program, "prs [action]", "audit pull requests, reviews, and checks", capture)
|
|
274
|
+
.option("--state <state>", "open, merged, closed, or all")
|
|
275
|
+
.addOption(option("--limit <count>", "pull-request limit", "integer"))
|
|
276
|
+
.option("--branches <pattern>", "branch-name pattern")
|
|
277
|
+
.option("--range <range>", "pull-request number range")
|
|
278
|
+
.option("--base <ref>", "audit base revision")
|
|
279
|
+
.option("--head <ref>", "audit head revision")
|
|
280
|
+
.option("--expected-login <login>", "expected GitHub identity");
|
|
281
|
+
leaf(program, "worktrees [action] [path]", "inspect, reconcile, or remove managed worktrees", capture).option("--dry-run", "report removal without mutation");
|
|
282
|
+
const telemetry = program
|
|
283
|
+
.command("telemetry")
|
|
284
|
+
.description("manage local opt-in ROI telemetry")
|
|
285
|
+
.allowExcessArguments(false)
|
|
286
|
+
.addArgument(new Argument("<action>").choices(["status", "report", "export", "clear"]));
|
|
287
|
+
telemetry
|
|
288
|
+
.option("--input <path>", "telemetry input path")
|
|
289
|
+
.option("--output <path>", "dashboard or evidence-bundle output path")
|
|
290
|
+
.addOption(option("--retention-days <count>", "retention window in days", "integer"))
|
|
291
|
+
.action((...values) => capture(values.at(-1)));
|
|
292
|
+
const diagnostics = program
|
|
293
|
+
.command("diagnostics")
|
|
294
|
+
.description("manage local troubleshooting diagnostics and support bundles")
|
|
295
|
+
.allowExcessArguments(false)
|
|
296
|
+
.addArgument(new Argument("<action>").choices([
|
|
297
|
+
"enable",
|
|
298
|
+
"status",
|
|
299
|
+
"preview",
|
|
300
|
+
"archive",
|
|
301
|
+
"collect",
|
|
302
|
+
"inspect",
|
|
303
|
+
"clear",
|
|
304
|
+
"disable",
|
|
305
|
+
]))
|
|
306
|
+
.addArgument(new Argument("[bundle]"));
|
|
307
|
+
diagnostics
|
|
308
|
+
.addOption(option("--retention-days <count>", "local event retention in days", "integer"))
|
|
309
|
+
.option("--since <duration>", "collection window, such as 24h or 7d")
|
|
310
|
+
.option("--output <path>", "repository-contained .json.gz bundle path")
|
|
311
|
+
.option("--preview-id <id>", "exact prior preview to archive")
|
|
312
|
+
.action((...values) => capture(values.at(-1)));
|
|
313
|
+
return program;
|
|
314
|
+
}
|
|
315
|
+
function flattenPositionals(values) {
|
|
316
|
+
return values
|
|
317
|
+
.flatMap((value) => (Array.isArray(value) ? value : [value]))
|
|
318
|
+
.filter((value) => typeof value === "string");
|
|
319
|
+
}
|
|
320
|
+
/** Parse and validate one invocation without running a product operation. */
|
|
321
|
+
export function parseCliInvocation(argv) {
|
|
322
|
+
let selected;
|
|
323
|
+
const program = createCliProgram((command) => {
|
|
324
|
+
selected = command;
|
|
325
|
+
});
|
|
326
|
+
program.parse([process.execPath, "knodin", ...argv]);
|
|
327
|
+
if (!selected)
|
|
328
|
+
return { commandPath: [], positionals: [], options: program.opts() };
|
|
329
|
+
const command = selected;
|
|
330
|
+
const commandPath = [];
|
|
331
|
+
for (let current = command; current?.parent; current = current.parent) {
|
|
332
|
+
commandPath.unshift(current.name());
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
commandPath,
|
|
336
|
+
positionals: flattenPositionals(command.processedArgs),
|
|
337
|
+
options: command.optsWithGlobals(),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/** Render root or nested help from the exact parser model. */
|
|
341
|
+
export function renderCliHelp(commandPath, columns) {
|
|
342
|
+
const program = createCliProgram();
|
|
343
|
+
let selected = program;
|
|
344
|
+
for (const name of commandPath) {
|
|
345
|
+
const child = selected.commands.find((command) => command.name() === name);
|
|
346
|
+
if (!child)
|
|
347
|
+
throw new Error(`knodin: unknown help command ${commandPath.join(" ")}`);
|
|
348
|
+
selected = child;
|
|
349
|
+
}
|
|
350
|
+
selected.configureHelp({ helpWidth: Math.max(60, Math.floor(columns ?? 100) - 1) });
|
|
351
|
+
let output = "";
|
|
352
|
+
selected.configureOutput({ writeOut: (value) => (output += value), writeErr: () => { } });
|
|
353
|
+
selected.outputHelp();
|
|
354
|
+
return formatTerminalHelp(output, columns);
|
|
355
|
+
}
|
|
356
|
+
/** Resolve the deepest declared command before a help flag or positional. */
|
|
357
|
+
export function helpCommandPath(argv) {
|
|
358
|
+
const program = createCliProgram();
|
|
359
|
+
const path = [];
|
|
360
|
+
let selected = program;
|
|
361
|
+
for (let index = 0; index < argv.length; index++) {
|
|
362
|
+
const value = argv[index];
|
|
363
|
+
if (value === "-h" || value === "--help")
|
|
364
|
+
break;
|
|
365
|
+
if (value.startsWith("-")) {
|
|
366
|
+
const optionName = value.split("=", 1)[0];
|
|
367
|
+
let cursor = selected;
|
|
368
|
+
let declared;
|
|
369
|
+
while (cursor && !declared) {
|
|
370
|
+
declared = cursor.options.find((candidate) => candidate.short === optionName || candidate.long === optionName);
|
|
371
|
+
cursor = cursor.parent;
|
|
372
|
+
}
|
|
373
|
+
if (declared?.required && !value.includes("="))
|
|
374
|
+
index++;
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
const child = selected.commands.find((command) => command.name() === value);
|
|
378
|
+
if (!child)
|
|
379
|
+
break;
|
|
380
|
+
selected = child;
|
|
381
|
+
path.push(value);
|
|
382
|
+
}
|
|
383
|
+
return path;
|
|
384
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { compareBytes } from "./compare.js";
|
|
2
|
+
export const EXPECTED_RELATIONSHIPS = [
|
|
3
|
+
"packages/api/route.ts->packages/auth/service.ts",
|
|
4
|
+
"packages/api/route.ts->packages/audit/store.ts",
|
|
5
|
+
"packages/auth/service.ts->packages/data/users.ts",
|
|
6
|
+
];
|
|
7
|
+
export function normalizeCodeFlowRelationships(connections) {
|
|
8
|
+
return connections.map((value) => {
|
|
9
|
+
const edge = value;
|
|
10
|
+
return {
|
|
11
|
+
// CodeFlow exports callee/definition as source and caller as target.
|
|
12
|
+
from: String(edge.target ?? ""),
|
|
13
|
+
to: String(edge.source ?? ""),
|
|
14
|
+
kind: typeof edge.kind === "string" ? edge.kind : null,
|
|
15
|
+
extractor: typeof edge.extractor === "string" ? edge.extractor : null,
|
|
16
|
+
confidence: edge.confidence === "exact" || edge.confidence === "heuristic" ? edge.confidence : null,
|
|
17
|
+
sourceFile: typeof edge.sourceFile === "string" ? edge.sourceFile : null,
|
|
18
|
+
sourceLine: typeof edge.sourceLine === "number" ? edge.sourceLine : null,
|
|
19
|
+
evidence: typeof edge.evidence === "string" ? edge.evidence : null,
|
|
20
|
+
raw: value,
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function scoreRelationships(relationships) {
|
|
25
|
+
const actual = new Set(relationships.map((edge) => `${edge.from}->${edge.to}`));
|
|
26
|
+
const expected = new Set(EXPECTED_RELATIONSHIPS);
|
|
27
|
+
const truePositives = [...expected].filter((edge) => actual.has(edge));
|
|
28
|
+
const falseNegatives = [...expected].filter((edge) => !actual.has(edge));
|
|
29
|
+
const falsePositives = [...actual].filter((edge) => !expected.has(edge));
|
|
30
|
+
const malformed = relationships.filter((edge) => !edge.from || !edge.to);
|
|
31
|
+
return {
|
|
32
|
+
truePositives,
|
|
33
|
+
falsePositives,
|
|
34
|
+
falseNegatives,
|
|
35
|
+
precision: actual.size === 0 ? 0 : truePositives.length / actual.size,
|
|
36
|
+
recall: truePositives.length / expected.size,
|
|
37
|
+
unsupportedEdgeRate: falseNegatives.length / expected.size,
|
|
38
|
+
malformedRelationships: malformed.length,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function blastRadiusCompleteness(relationships) {
|
|
42
|
+
const origin = "packages/data/users.ts";
|
|
43
|
+
const expected = new Set(["packages/auth/service.ts", "packages/api/route.ts"]);
|
|
44
|
+
const adjacency = new Map();
|
|
45
|
+
for (const edge of relationships) {
|
|
46
|
+
if (!adjacency.has(edge.to))
|
|
47
|
+
adjacency.set(edge.to, new Set());
|
|
48
|
+
adjacency.get(edge.to)?.add(edge.from);
|
|
49
|
+
}
|
|
50
|
+
const found = new Set();
|
|
51
|
+
const queue = [origin];
|
|
52
|
+
while (queue.length) {
|
|
53
|
+
const current = queue.shift();
|
|
54
|
+
for (const dependent of adjacency.get(current) ?? []) {
|
|
55
|
+
if (dependent === origin || found.has(dependent))
|
|
56
|
+
continue;
|
|
57
|
+
found.add(dependent);
|
|
58
|
+
queue.push(dependent);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
origin,
|
|
63
|
+
expected: [...expected],
|
|
64
|
+
found: [...found].sort(compareBytes),
|
|
65
|
+
missing: [...expected].filter((file) => !found.has(file)),
|
|
66
|
+
completeness: [...expected].filter((file) => found.has(file)).length / expected.size,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function provenanceCoverage(relationships) {
|
|
70
|
+
const count = relationships.length;
|
|
71
|
+
const rate = (predicate) => count === 0 ? 0 : relationships.filter(predicate).length / count;
|
|
72
|
+
return {
|
|
73
|
+
relationships: count,
|
|
74
|
+
relationKind: rate((edge) => edge.kind !== null),
|
|
75
|
+
extractorIdentity: rate((edge) => edge.extractor !== null),
|
|
76
|
+
exactOrHeuristicConfidence: rate((edge) => edge.confidence !== null),
|
|
77
|
+
sourceFile: rate((edge) => edge.sourceFile !== null),
|
|
78
|
+
sourceLine: rate((edge) => edge.sourceLine !== null),
|
|
79
|
+
boundedSourceEvidence: rate((edge) => edge.evidence !== null),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const IDENTITY_PREFIX = "sym_";
|
|
2
|
+
/** A stable, repository-scoped shorthand. Prefix collisions remain ambiguity-safe
|
|
3
|
+
* because selectors are expanded as prefixes rather than guessed as one symbol. */
|
|
4
|
+
export function compactIdentity(identity) {
|
|
5
|
+
if (!identity)
|
|
6
|
+
return "~?";
|
|
7
|
+
const body = identity.startsWith(IDENTITY_PREFIX)
|
|
8
|
+
? identity.slice(IDENTITY_PREFIX.length)
|
|
9
|
+
: identity;
|
|
10
|
+
return `~${body.slice(0, 6)}`;
|
|
11
|
+
}
|
|
12
|
+
export function expandCompactIdentity(identity) {
|
|
13
|
+
return identity?.startsWith("~") ? `${IDENTITY_PREFIX}${identity.slice(1)}*` : identity;
|
|
14
|
+
}
|
|
15
|
+
function freshness(value) {
|
|
16
|
+
if (value === "reconciled")
|
|
17
|
+
return "reconciled";
|
|
18
|
+
if (value === "unknown")
|
|
19
|
+
return "unknown";
|
|
20
|
+
return "fresh";
|
|
21
|
+
}
|
|
22
|
+
function terseSignature(row) {
|
|
23
|
+
const signature = row.signature?.replace(/\s+/g, " ").trim() ?? "";
|
|
24
|
+
if (!signature)
|
|
25
|
+
return "";
|
|
26
|
+
const python = /^(.*?:)(?:\s|$)/.exec(signature)?.[1];
|
|
27
|
+
return python ?? signature;
|
|
28
|
+
}
|
|
29
|
+
function rowLine(row, repeatFile) {
|
|
30
|
+
const nested = row.parent?.symbol ? `${row.parent.symbol}/` : "";
|
|
31
|
+
const filePrefix = repeatFile ? `${row.file ?? "?"}:` : "";
|
|
32
|
+
const location = `${filePrefix}${row.line ?? "?"}`;
|
|
33
|
+
const signature = terseSignature(row);
|
|
34
|
+
const signatureSuffix = signature ? ` ${signature}` : "";
|
|
35
|
+
return `${compactIdentity(row.identity)} ${row.kind ?? "?"} ${nested}${row.symbol ?? "?"}@${location}${signatureSuffix}`;
|
|
36
|
+
}
|
|
37
|
+
function withinBytes(header, rows, byteBudget) {
|
|
38
|
+
let selected = rows;
|
|
39
|
+
if (byteBudget !== undefined) {
|
|
40
|
+
while (selected.length > 0 &&
|
|
41
|
+
Buffer.byteLength([header(selected.length), ...selected].join("\n"), "utf8") > byteBudget)
|
|
42
|
+
selected = selected.slice(0, -1);
|
|
43
|
+
}
|
|
44
|
+
const output = [header(selected.length), ...selected].join("\n");
|
|
45
|
+
return byteBudget !== undefined && Buffer.byteLength(output, "utf8") > byteBudget ? "" : output;
|
|
46
|
+
}
|
|
47
|
+
export function compactQueryResult(result, byteBudget) {
|
|
48
|
+
if (result.ambiguity)
|
|
49
|
+
return compactAmbiguity(result.ambiguity.candidates, result.staleness);
|
|
50
|
+
const total = result.count;
|
|
51
|
+
const state = freshness(result.staleness);
|
|
52
|
+
if (result.pattern === "project_overview") {
|
|
53
|
+
const rows = result.results.map((row) => `${row.symbol ?? row.file ?? "."} ${row.fileCount ?? 0} ${row.sizeBytes ?? 0}B`);
|
|
54
|
+
return withinBytes((n) => `${state} ${n}/${total}`, rows, byteBudget);
|
|
55
|
+
}
|
|
56
|
+
const repeatFile = result.pattern === "batch_outline";
|
|
57
|
+
const rows = result.results.map((row) => rowLine(row, repeatFile));
|
|
58
|
+
return withinBytes((n) => {
|
|
59
|
+
const continuation = result.hasMore ? "+" : "";
|
|
60
|
+
const target = result.target ? ` ${result.target}` : "";
|
|
61
|
+
return `${state} ${n}/${total}${continuation}${target}`;
|
|
62
|
+
}, rows, byteBudget);
|
|
63
|
+
}
|
|
64
|
+
export function compactSearchResult(page, query, byteBudget) {
|
|
65
|
+
const normalize = (value) => value.toLocaleLowerCase().replace(/[^a-z0-9]/g, "");
|
|
66
|
+
const normalized = normalize(query);
|
|
67
|
+
const matches = page.results.filter((row) => normalize(row.symbol).includes(normalized));
|
|
68
|
+
const state = freshness(matches[0]?.staleness ?? page.results[0]?.staleness);
|
|
69
|
+
const rows = matches.map((row) => {
|
|
70
|
+
const source = row.source ? ` ${row.source}` : "";
|
|
71
|
+
return `${compactIdentity(row.identity)} ${row.kind} @${row.filePath}${source}`;
|
|
72
|
+
});
|
|
73
|
+
return withinBytes((n) => `${state} ${n}/${matches.length}${page.hasMore ? "+" : ""} ${query}`, rows, byteBudget);
|
|
74
|
+
}
|
|
75
|
+
export function compactExplainResult(result, selectorFile, byteBudget) {
|
|
76
|
+
if (result.ambiguity)
|
|
77
|
+
return compactAmbiguity(result.ambiguity.candidates, result.staleness);
|
|
78
|
+
const numbered = result.source ?? "";
|
|
79
|
+
const firstLine = Number(/^(\d+): /.exec(numbered)?.[1] ?? 0);
|
|
80
|
+
const source = numbered
|
|
81
|
+
.split("\n")
|
|
82
|
+
.map((line) => line.replace(/^\d+: /, ""))
|
|
83
|
+
.join("\n");
|
|
84
|
+
const header = `${freshness(result.staleness)} ${compactIdentity(result.identity)} ${selectorFile ?? "?"}:${firstLine || "?"}`;
|
|
85
|
+
const output = `${header}\n${source}`;
|
|
86
|
+
if (byteBudget === undefined || Buffer.byteLength(output, "utf8") <= byteBudget)
|
|
87
|
+
return output;
|
|
88
|
+
const omitted = `${header}\nomitted ${Buffer.byteLength(source, "utf8")}B`;
|
|
89
|
+
return Buffer.byteLength(omitted, "utf8") <= byteBudget ? omitted : "";
|
|
90
|
+
}
|
|
91
|
+
function compactAmbiguity(candidates, staleness) {
|
|
92
|
+
return [
|
|
93
|
+
`ambiguous ${freshness(staleness)} ${candidates.length}`,
|
|
94
|
+
...candidates.map((candidate) => `${compactIdentity(candidate.identity)} ${candidate.kind} ${candidate.file}:${candidate.line}`),
|
|
95
|
+
].join("\n");
|
|
96
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordering primitives for values that get persisted or compared across machines.
|
|
3
|
+
*
|
|
4
|
+
* `Array.prototype.sort()` with no comparator coerces elements to strings and
|
|
5
|
+
* compares UTF-16 code units. For an array of strings that is already the order
|
|
6
|
+
* these helpers produce — so adopting them is behaviour-preserving. For anything
|
|
7
|
+
* else it is not: `[10, 9].sort()` yields `[10, 9]`, which is the bug the rule
|
|
8
|
+
* exists to catch.
|
|
9
|
+
*
|
|
10
|
+
* The reason these are hand-written rather than `String.localeCompare`:
|
|
11
|
+
* collation is machine-dependent. It varies with the host locale and with the
|
|
12
|
+
* ICU data the runtime was built against, so two machines can order identical
|
|
13
|
+
* input differently. knodin persists sort order into the graph and compares it
|
|
14
|
+
* across checkouts, and a SARIF import shipped with exactly that bug earlier
|
|
15
|
+
* today. Byte order is a function of the bytes alone.
|
|
16
|
+
*/
|
|
17
|
+
/** Lexicographic by UTF-16 code unit — identical to the default sort order. */
|
|
18
|
+
export function compareBytes(left, right) {
|
|
19
|
+
if (left < right)
|
|
20
|
+
return -1;
|
|
21
|
+
return left > right ? 1 : 0;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Byte order, descending.
|
|
25
|
+
*
|
|
26
|
+
* Written out rather than expressed as `compareBytes(right, left)` so call
|
|
27
|
+
* sites do not have to swap their arguments. A swap reads as a bug — and is
|
|
28
|
+
* reported as one, since a reversed call whose parameters share the callee's
|
|
29
|
+
* names is indistinguishable from a genuine mistake.
|
|
30
|
+
*/
|
|
31
|
+
export function compareBytesDescending(left, right) {
|
|
32
|
+
if (left > right)
|
|
33
|
+
return -1;
|
|
34
|
+
return left < right ? 1 : 0;
|
|
35
|
+
}
|
|
36
|
+
/** Numeric ascending. The default sort would order these lexicographically. */
|
|
37
|
+
export function compareNumbers(left, right) {
|
|
38
|
+
return left - right;
|
|
39
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
|
+
import { sampleOperation } from "./competitive-measurement.js";
|
|
5
|
+
function textOf(value) {
|
|
6
|
+
return (value.content ?? [])
|
|
7
|
+
.filter((item) => item.type === "text")
|
|
8
|
+
.map((item) => item.text ?? "")
|
|
9
|
+
.join("\n");
|
|
10
|
+
}
|
|
11
|
+
/** Measure five cold calls, rebuilding the MCP process before every recorded call. */
|
|
12
|
+
export async function measureColdMcp(command, args, cwd, side, clientName) {
|
|
13
|
+
let last;
|
|
14
|
+
let error;
|
|
15
|
+
const { measurement } = await sampleOperation(async () => {
|
|
16
|
+
const transport = new StdioClientTransport({ command, args, cwd, stderr: "pipe" });
|
|
17
|
+
const client = new Client({ name: clientName, version: "1" }, { capabilities: {} });
|
|
18
|
+
try {
|
|
19
|
+
await client.connect(transport);
|
|
20
|
+
last = await client.callTool({ name: side.tool, arguments: side.args });
|
|
21
|
+
error = undefined;
|
|
22
|
+
}
|
|
23
|
+
catch (cause) {
|
|
24
|
+
error = cause instanceof Error ? cause.message : String(cause);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
await transport.close().catch(() => undefined);
|
|
28
|
+
}
|
|
29
|
+
return last;
|
|
30
|
+
}, { mode: "cold" });
|
|
31
|
+
const response = last ? textOf(last) : "";
|
|
32
|
+
return {
|
|
33
|
+
ok: !error && !last?.isError,
|
|
34
|
+
error,
|
|
35
|
+
...measurement,
|
|
36
|
+
responseBytes: Buffer.byteLength(response),
|
|
37
|
+
responseSha256: createHash("sha256").update(response).digest("hex"),
|
|
38
|
+
response,
|
|
39
|
+
};
|
|
40
|
+
}
|