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,324 @@
|
|
|
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 addRepositoryCommands(program, capture) {
|
|
57
|
+
const repos = program.command("repos").description("manage a portfolio of repositories");
|
|
58
|
+
for (const action of ["discover", "init", "status", "doctor"]) {
|
|
59
|
+
const command = leaf(repos, `${action} <roots...>`, `${action} repositories beneath one or more roots`, capture)
|
|
60
|
+
.addOption(option("--depth <count>", "maximum discovery depth", "integer"))
|
|
61
|
+
.option("--linked-worktrees <mode>", "skip or include linked worktrees");
|
|
62
|
+
if (action === "init") {
|
|
63
|
+
command
|
|
64
|
+
.addOption(option("--include <selector>", "include a repository id or path", "collect"))
|
|
65
|
+
.addOption(option("--exclude <selector>", "exclude a repository id or path", "collect"))
|
|
66
|
+
.option("--manifest <path>", "write or resume a portfolio manifest")
|
|
67
|
+
.option("--dry-run", "report actions without changing repositories");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
leaf(repos, "search <query>", "search selected repositories sequentially", capture)
|
|
71
|
+
.addOption(option("--root <path>", "portfolio root", "collect"))
|
|
72
|
+
.addOption(option("--include <selector>", "include a repository id or path", "collect"))
|
|
73
|
+
.addOption(option("--exclude <selector>", "exclude a repository id or path", "collect"))
|
|
74
|
+
.option("--cursor <cursor>", "resume from an opaque cursor")
|
|
75
|
+
.option("--allow-partial", "return healthy repository results when another degrades");
|
|
76
|
+
const fleet = program.command("fleet").description("deprecated repository-fleet compatibility");
|
|
77
|
+
leaf(fleet, "init <roots...>", "initialize a repository fleet", capture)
|
|
78
|
+
.addOption(option("--depth <count>", "maximum discovery depth", "integer"))
|
|
79
|
+
.option("--worktrees <mode>", "skip or include linked worktrees")
|
|
80
|
+
.option("--dry-run", "report actions without mutation");
|
|
81
|
+
}
|
|
82
|
+
function addGraphCommands(program, capture) {
|
|
83
|
+
leaf(program, "context <task> [base]", "build compact task orientation", capture);
|
|
84
|
+
leaf(program, "explain <symbol> [detail]", "explain one ambiguity-safe symbol", capture);
|
|
85
|
+
leaf(program, "review [base] [detail]", "review an explicit Git diff scope", capture)
|
|
86
|
+
.option("--scope <scope>", "unstaged, staged, all, or compare")
|
|
87
|
+
.option("--from <ref>", "comparison start revision")
|
|
88
|
+
.option("--to <ref>", "comparison end revision")
|
|
89
|
+
.option("--files <paths>", "comma-separated repo-relative paths");
|
|
90
|
+
leaf(program, "map", "show architecture communities and edges", capture)
|
|
91
|
+
.option("--standard", "include full map detail")
|
|
92
|
+
.addOption(option("--top <count>", "maximum ranked communities", "integer"))
|
|
93
|
+
.option("--sort <mode>", "relevance, name, size, degree, or complexity")
|
|
94
|
+
.option("--relations <kinds>", "comma-separated relationship kinds");
|
|
95
|
+
leaf(program, "wiki", "write local architecture wiki pages", capture).option("--force", "rewrite unchanged pages");
|
|
96
|
+
leaf(program, "visualize <entry>", "write a local architecture/call-flow HTML artifact", capture)
|
|
97
|
+
.requiredOption("--output <path>", "repo-relative HTML output path")
|
|
98
|
+
.addOption(option("--depth <count>", "call-flow depth", "integer"))
|
|
99
|
+
.addOption(option("--max-bytes <count>", "hard artifact budget", "integer"));
|
|
100
|
+
leaf(program, "search <query> [limit]", "hybrid symbol search", capture)
|
|
101
|
+
.option("--languages <values>", "comma-separated languages")
|
|
102
|
+
.option("--extensions <values>", "comma-separated extensions")
|
|
103
|
+
.option("--kinds <values>", "comma-separated symbol kinds")
|
|
104
|
+
.option("--path <prefix>", "repo-relative path prefix")
|
|
105
|
+
.option("--tests-only", "search test symbols only")
|
|
106
|
+
.option("--production-only", "search production symbols only")
|
|
107
|
+
.option("--no-source", "omit source snippets")
|
|
108
|
+
.addOption(option("--offset <count>", "pagination offset", "integer"))
|
|
109
|
+
.addOption(option("--limit <count>", "result limit", "integer"));
|
|
110
|
+
leaf(program, "query <pattern> [targets...]", "run one structured graph query", capture)
|
|
111
|
+
.option("--impact-mode <mode>", "symbol or file impact")
|
|
112
|
+
.option("--direction <direction>", "upstream, downstream, or both")
|
|
113
|
+
.addOption(option("--depth <count>", "bounded traversal depth", "integer"))
|
|
114
|
+
.option("--relations <kinds>", "comma-separated relationship kinds")
|
|
115
|
+
.addOption(option("--min-confidence <value>", "minimum edge confidence", "number"))
|
|
116
|
+
.option("--exclude-tests", "exclude tests from impact")
|
|
117
|
+
.option("--data-flow", "include bounded data-flow evidence")
|
|
118
|
+
.option("--limit <count>", "result limit")
|
|
119
|
+
.addOption(option("--min-lines <count>", "minimum line count", "integer"))
|
|
120
|
+
.addOption(option("--min-complexity <count>", "minimum complexity", "integer"))
|
|
121
|
+
.option("--kinds <values>", "comma-separated symbol kinds")
|
|
122
|
+
.option("--path <prefix>", "repo-relative path prefix")
|
|
123
|
+
.option("--variable <name>", "flow-analysis variable")
|
|
124
|
+
.option("--facets <values>", "comma-separated architecture facets")
|
|
125
|
+
.addOption(option("--top <count>", "maximum ranked results", "integer"))
|
|
126
|
+
.option("--sort <mode>", "result ordering");
|
|
127
|
+
leaf(program, "rename <old> <new>", "preview or apply an ambiguity-safe rename", capture)
|
|
128
|
+
.option("--apply", "apply the verified edit")
|
|
129
|
+
.option("--no-verify", "skip post-apply typecheck");
|
|
130
|
+
}
|
|
131
|
+
function addArtifactCommands(program, capture) {
|
|
132
|
+
const pack = program.command("pack").description("export bounded portable context");
|
|
133
|
+
pack
|
|
134
|
+
.argument("[input]")
|
|
135
|
+
.option("--format <format>", "markdown, json, or xml")
|
|
136
|
+
.option("--include <globs>", "comma-separated include globs")
|
|
137
|
+
.option("--exclude <globs>", "comma-separated exclude globs")
|
|
138
|
+
.option("--policy <assignments>", "comma-separated glob policies")
|
|
139
|
+
.option("--already-present <paths>", "comma-separated paths already in context")
|
|
140
|
+
.option("--chat-files <paths>", "comma-separated chat paths")
|
|
141
|
+
.option("--line-numbers", "include source line numbers")
|
|
142
|
+
.option("--tree", "include repository tree")
|
|
143
|
+
.option("--output <path>", "write a repo-relative retained artifact")
|
|
144
|
+
.option("--diff-scope <scope>", "unstaged, staged, all, or compare")
|
|
145
|
+
.option("--from <ref>", "comparison start revision")
|
|
146
|
+
.option("--to <ref>", "comparison end revision")
|
|
147
|
+
.addOption(option("--log <count>", "include recent commits", "integer"))
|
|
148
|
+
.action((...values) => capture(values.at(-1)));
|
|
149
|
+
leaf(pack, "read <artifact>", "read a retained context artifact", capture)
|
|
150
|
+
.addOption(option("--start <line>", "first line", "integer"))
|
|
151
|
+
.addOption(option("--end <line>", "last line", "integer"));
|
|
152
|
+
leaf(pack, "grep <artifact> <regex>", "search an artifact with a linear-time regex", capture)
|
|
153
|
+
.option("--flags <flags>", "regular-expression flags")
|
|
154
|
+
.addOption(option("--limit <count>", "match limit", "integer"));
|
|
155
|
+
const compress = program.command("compress").description("compress already-produced output");
|
|
156
|
+
compress
|
|
157
|
+
.argument("[input]")
|
|
158
|
+
.option("--strategy <strategy>", "smart, head-tail, or errors-only")
|
|
159
|
+
.option("--adapter <adapter>", "structured output adapter")
|
|
160
|
+
.addOption(option("--lines <count>", "hard line budget", "integer"))
|
|
161
|
+
.addOption(option("--max-output-bytes <count>", "hard output byte budget", "integer"))
|
|
162
|
+
.addOption(option("--context <count>", "signal context lines", "integer"))
|
|
163
|
+
.addOption(option("--exit-code <code>", "source process exit code", "integer"))
|
|
164
|
+
.option("--signal <name>", "source process termination signal")
|
|
165
|
+
.addOption(option("--max-input-bytes <count>", "hard input byte limit", "integer"))
|
|
166
|
+
.option("--no-retain", "do not retain raw drill-down data")
|
|
167
|
+
.option("--no-redact", "disable secret redaction")
|
|
168
|
+
.action((...values) => capture(values.at(-1)));
|
|
169
|
+
for (const action of ["read", "diagnose"]) {
|
|
170
|
+
leaf(compress, `${action} <artifact>`, `${action} a retained output artifact`, capture)
|
|
171
|
+
.addOption(option("--start <line>", "first line", "integer"))
|
|
172
|
+
.addOption(option("--end <line>", "last line", "integer"))
|
|
173
|
+
.addOption(option("--max-output-bytes <count>", "hard output byte budget", "integer"))
|
|
174
|
+
.addOption(option("--context <count>", "diagnostic context lines", "integer"))
|
|
175
|
+
.addOption(option("--limit <count>", "diagnostic result limit", "integer"))
|
|
176
|
+
.option("--raw", "return unredacted retained bytes");
|
|
177
|
+
}
|
|
178
|
+
leaf(compress, "delete <artifact>", "delete a retained output artifact", capture);
|
|
179
|
+
}
|
|
180
|
+
function createCliProgram(capture = () => { }) {
|
|
181
|
+
const program = new Command("knodin")
|
|
182
|
+
.description("knodin — source-evidenced local code intelligence with known bounds")
|
|
183
|
+
.showHelpAfterError()
|
|
184
|
+
.showSuggestionAfterError()
|
|
185
|
+
.passThroughOptions(false)
|
|
186
|
+
.allowExcessArguments(false)
|
|
187
|
+
.exitOverride();
|
|
188
|
+
program.configureOutput({ writeErr: () => { } });
|
|
189
|
+
addGlobalOptions(program);
|
|
190
|
+
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");
|
|
191
|
+
leaf(program, "init", "initialize graph, lifecycle hooks, and agent integration", capture)
|
|
192
|
+
.option("--scope <scope>", "personal, team, or cli-only")
|
|
193
|
+
.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");
|
|
194
|
+
leaf(program, "configure", "change or inspect agent integration", capture)
|
|
195
|
+
.option("--scope <scope>", "personal, team, or cli-only")
|
|
196
|
+
.option("--status", "inspect configuration without changing it");
|
|
197
|
+
leaf(program, "index [files...]", "index a repository or selected files", capture)
|
|
198
|
+
.option("--clean", "rebuild selected index state")
|
|
199
|
+
.option("--force", "force clean indexing");
|
|
200
|
+
leaf(program, "doctor", "diagnose installation, clients, hooks, graph, and updates", capture).option("--client <client>", "claude, codex, gemini, or antigravity");
|
|
201
|
+
leaf(program, "status", "report graph, lifecycle, integration, and update state", capture)
|
|
202
|
+
.option("--deep", "run a full graph audit")
|
|
203
|
+
.option("--watch", "stream status snapshots")
|
|
204
|
+
.addOption(option("--interval <seconds>", "watch interval", "number"));
|
|
205
|
+
leaf(program, "wait", "wait for current graph evidence", capture)
|
|
206
|
+
.option("--fresh", "wait for a fresh graph")
|
|
207
|
+
.addOption(option("--timeout <seconds>", "deadline", "number"));
|
|
208
|
+
leaf(program, "repair", "audit and reconcile graph state", capture)
|
|
209
|
+
.option("--plan", "report the repair plan without mutation")
|
|
210
|
+
.option("--jsonl", "stream JSONL progress")
|
|
211
|
+
.option("--progress <mode>", "tty, jsonl, plain, or none")
|
|
212
|
+
.option("--progress-interval <duration>", "progress interval such as 750ms, 30s, or 2m");
|
|
213
|
+
leaf(program, "serve", "run the one-tool MCP gateway on stdio", capture);
|
|
214
|
+
leaf(program, "version", "print the installed knodin version", capture);
|
|
215
|
+
leaf(program, "hook-refresh <kind> [values...]", "internal Git lifecycle refresh", capture);
|
|
216
|
+
leaf(program, "refresh-artifacts [event]", "refresh external graph artifacts", capture);
|
|
217
|
+
addRepositoryCommands(program, capture);
|
|
218
|
+
addGraphCommands(program, capture);
|
|
219
|
+
addArtifactCommands(program, capture);
|
|
220
|
+
const system = program.command("system").description("inspect declared multi-repository systems");
|
|
221
|
+
leaf(system, "list", "list configured systems", capture);
|
|
222
|
+
for (const action of ["show", "validate", "query"]) {
|
|
223
|
+
const command = leaf(system, `${action} <system-id>`, `${action} one configured system`, capture);
|
|
224
|
+
if (action === "query")
|
|
225
|
+
command.option("--allow-partial", "return healthy components");
|
|
226
|
+
}
|
|
227
|
+
const update = program
|
|
228
|
+
.command("update")
|
|
229
|
+
.description("inspect or apply threshold-signed updates")
|
|
230
|
+
.allowExcessArguments(false)
|
|
231
|
+
.addArgument(new Argument("<action>").choices(["status", "check", "explain", "apply", "rollback"]));
|
|
232
|
+
update.action((...values) => capture(values.at(-1)));
|
|
233
|
+
leaf(program, "docs <topic>", "read canonical product documentation", capture);
|
|
234
|
+
leaf(program, "prs [action]", "audit pull requests, reviews, and checks", capture)
|
|
235
|
+
.option("--state <state>", "open, merged, closed, or all")
|
|
236
|
+
.addOption(option("--limit <count>", "pull-request limit", "integer"))
|
|
237
|
+
.option("--branches <pattern>", "branch-name pattern")
|
|
238
|
+
.option("--range <range>", "pull-request number range")
|
|
239
|
+
.option("--base <ref>", "audit base revision")
|
|
240
|
+
.option("--head <ref>", "audit head revision")
|
|
241
|
+
.option("--expected-login <login>", "expected GitHub identity");
|
|
242
|
+
leaf(program, "worktrees [action] [path]", "inspect, reconcile, or remove managed worktrees", capture).option("--dry-run", "report removal without mutation");
|
|
243
|
+
const telemetry = program
|
|
244
|
+
.command("telemetry")
|
|
245
|
+
.description("manage local opt-in ROI telemetry")
|
|
246
|
+
.allowExcessArguments(false)
|
|
247
|
+
.addArgument(new Argument("<action>").choices(["status", "report", "export", "clear"]));
|
|
248
|
+
telemetry
|
|
249
|
+
.option("--input <path>", "telemetry input path")
|
|
250
|
+
.option("--output <path>", "dashboard or evidence-bundle output path")
|
|
251
|
+
.addOption(option("--retention-days <count>", "retention window in days", "integer"))
|
|
252
|
+
.action((...values) => capture(values.at(-1)));
|
|
253
|
+
return program;
|
|
254
|
+
}
|
|
255
|
+
function flattenPositionals(values) {
|
|
256
|
+
return values
|
|
257
|
+
.flatMap((value) => (Array.isArray(value) ? value : [value]))
|
|
258
|
+
.filter((value) => typeof value === "string");
|
|
259
|
+
}
|
|
260
|
+
/** Parse and validate one invocation without running a product operation. */
|
|
261
|
+
export function parseCliInvocation(argv) {
|
|
262
|
+
let selected;
|
|
263
|
+
const program = createCliProgram((command) => {
|
|
264
|
+
selected = command;
|
|
265
|
+
});
|
|
266
|
+
program.parse([process.execPath, "knodin", ...argv]);
|
|
267
|
+
if (!selected)
|
|
268
|
+
return { commandPath: [], positionals: [], options: program.opts() };
|
|
269
|
+
const command = selected;
|
|
270
|
+
const commandPath = [];
|
|
271
|
+
for (let current = command; current?.parent; current = current.parent) {
|
|
272
|
+
commandPath.unshift(current.name());
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
commandPath,
|
|
276
|
+
positionals: flattenPositionals(command.processedArgs),
|
|
277
|
+
options: command.optsWithGlobals(),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/** Render root or nested help from the exact parser model. */
|
|
281
|
+
export function renderCliHelp(commandPath, columns) {
|
|
282
|
+
const program = createCliProgram();
|
|
283
|
+
let selected = program;
|
|
284
|
+
for (const name of commandPath) {
|
|
285
|
+
const child = selected.commands.find((command) => command.name() === name);
|
|
286
|
+
if (!child)
|
|
287
|
+
throw new Error(`knodin: unknown help command ${commandPath.join(" ")}`);
|
|
288
|
+
selected = child;
|
|
289
|
+
}
|
|
290
|
+
selected.configureHelp({ helpWidth: Math.max(60, Math.floor(columns ?? 100) - 1) });
|
|
291
|
+
let output = "";
|
|
292
|
+
selected.configureOutput({ writeOut: (value) => (output += value), writeErr: () => { } });
|
|
293
|
+
selected.outputHelp();
|
|
294
|
+
return formatTerminalHelp(output, columns);
|
|
295
|
+
}
|
|
296
|
+
/** Resolve the deepest declared command before a help flag or positional. */
|
|
297
|
+
export function helpCommandPath(argv) {
|
|
298
|
+
const program = createCliProgram();
|
|
299
|
+
const path = [];
|
|
300
|
+
let selected = program;
|
|
301
|
+
for (let index = 0; index < argv.length; index++) {
|
|
302
|
+
const value = argv[index];
|
|
303
|
+
if (value === "-h" || value === "--help")
|
|
304
|
+
break;
|
|
305
|
+
if (value.startsWith("-")) {
|
|
306
|
+
const optionName = value.split("=", 1)[0];
|
|
307
|
+
let cursor = selected;
|
|
308
|
+
let declared;
|
|
309
|
+
while (cursor && !declared) {
|
|
310
|
+
declared = cursor.options.find((candidate) => candidate.short === optionName || candidate.long === optionName);
|
|
311
|
+
cursor = cursor.parent;
|
|
312
|
+
}
|
|
313
|
+
if (declared?.required && !value.includes("="))
|
|
314
|
+
index++;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const child = selected.commands.find((command) => command.name() === value);
|
|
318
|
+
if (!child)
|
|
319
|
+
break;
|
|
320
|
+
selected = child;
|
|
321
|
+
path.push(value);
|
|
322
|
+
}
|
|
323
|
+
return path;
|
|
324
|
+
}
|
|
@@ -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,20 @@
|
|
|
1
|
+
/** Verifies the local-only, single-gateway contract that qualifies a competitive replay. */
|
|
2
|
+
export function assessCompetitiveConstraints(tools) {
|
|
3
|
+
const mcpToolNames = tools.map((tool) => tool.name).sort();
|
|
4
|
+
const schemaBytes = Buffer.byteLength(JSON.stringify(tools));
|
|
5
|
+
const violations = [];
|
|
6
|
+
if (tools.length !== 1)
|
|
7
|
+
violations.push(`Expected one MCP tool, found ${tools.length}`);
|
|
8
|
+
if (mcpToolNames.length !== 1 || mcpToolNames[0] !== "knodin")
|
|
9
|
+
violations.push(`Expected only the knodin gateway, found ${mcpToolNames.join(", ") || "none"}`);
|
|
10
|
+
return {
|
|
11
|
+
passed: violations.length === 0,
|
|
12
|
+
mcpToolCount: tools.length,
|
|
13
|
+
mcpToolNames,
|
|
14
|
+
schemaBytes,
|
|
15
|
+
schemaTokens: Math.ceil(schemaBytes / 4),
|
|
16
|
+
localOnly: true,
|
|
17
|
+
optionalServices: ["local GitHub CLI integration (prs operation)"],
|
|
18
|
+
violations,
|
|
19
|
+
};
|
|
20
|
+
}
|