knodin 0.7.4 → 0.7.6

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.
Files changed (104) hide show
  1. package/README.md +4 -1
  2. package/dist/bin/cli.js +108 -1859
  3. package/dist/bin/launcher.js +3 -10
  4. package/dist/chunks/chunk-DMQAGX77.js +654 -0
  5. package/dist/chunks/chunk-F4Z3Z766.js +4 -0
  6. package/dist/chunks/chunk-SIJAQVSX.js +3 -0
  7. package/dist/chunks/chunk-X6M4HUUE.js +2 -0
  8. package/dist/chunks/chunk-YPRMY2LP.js +8 -0
  9. package/dist/chunks/pure-compression-cli-4TA2TQD5.js +5 -0
  10. package/dist/chunks/server-7EDF4CBY.js +14 -0
  11. package/dist/chunks/structural-fast-path-KD5KQSPX.js +4 -0
  12. package/dist/src/init-progress-worker.js +2 -106
  13. package/dist/src/repair-progress-worker.js +2 -120
  14. package/docs/INSTALLATION.md +17 -1
  15. package/docs/releases/0.7.6.md +25 -0
  16. package/package.json +1 -110
  17. package/dist/src/agent-integration.js +0 -238
  18. package/dist/src/artifact-refresh.js +0 -81
  19. package/dist/src/cli-args.js +0 -274
  20. package/dist/src/cli-model.js +0 -358
  21. package/dist/src/codeflow-replay.js +0 -80
  22. package/dist/src/compact-structural.js +0 -96
  23. package/dist/src/competitive-cold-mcp.js +0 -40
  24. package/dist/src/competitive-constraints.js +0 -20
  25. package/dist/src/competitive-manifest.js +0 -411
  26. package/dist/src/competitive-measurement.js +0 -183
  27. package/dist/src/competitive-runner.js +0 -487
  28. package/dist/src/competitive-sandbox.js +0 -108
  29. package/dist/src/context-export.js +0 -422
  30. package/dist/src/context.js +0 -102
  31. package/dist/src/diagnostics.js +0 -449
  32. package/dist/src/docs-sections.js +0 -141
  33. package/dist/src/doctor.js +0 -380
  34. package/dist/src/engine/ann-hnsw.js +0 -271
  35. package/dist/src/engine/embeddings.js +0 -193
  36. package/dist/src/engine/file-walker.js +0 -43
  37. package/dist/src/engine/git-history.js +0 -289
  38. package/dist/src/engine/index.js +0 -13377
  39. package/dist/src/engine/perf.js +0 -115
  40. package/dist/src/engine/prune.js +0 -112
  41. package/dist/src/engine/scip-import.js +0 -408
  42. package/dist/src/engine/source-policy.js +0 -69
  43. package/dist/src/engine/sqlite.js +0 -71
  44. package/dist/src/engine/symbol-delete.js +0 -58
  45. package/dist/src/execution-profile.js +0 -203
  46. package/dist/src/failure-diagnosis.js +0 -649
  47. package/dist/src/fleet.js +0 -7
  48. package/dist/src/git-executable.js +0 -31
  49. package/dist/src/graph-query-health.js +0 -115
  50. package/dist/src/hook-manager-integration.js +0 -156
  51. package/dist/src/index-activity.js +0 -125
  52. package/dist/src/init-progress.js +0 -155
  53. package/dist/src/init.js +0 -1254
  54. package/dist/src/lifecycle-health.js +0 -250
  55. package/dist/src/lsp-readonly.js +0 -217
  56. package/dist/src/output-compression.js +0 -629
  57. package/dist/src/output-telemetry.js +0 -363
  58. package/dist/src/pr-triage.js +0 -638
  59. package/dist/src/progressive-evidence.js +0 -473
  60. package/dist/src/pure-compression-cli.js +0 -101
  61. package/dist/src/relationship-adapters.js +0 -370
  62. package/dist/src/release-attestation.js +0 -533
  63. package/dist/src/release-preflight.js +0 -510
  64. package/dist/src/repair-progress.js +0 -262
  65. package/dist/src/repository-init-process.js +0 -173
  66. package/dist/src/repository-management.js +0 -1231
  67. package/dist/src/response-budget.js +0 -194
  68. package/dist/src/server.js +0 -73
  69. package/dist/src/structural-fast-path.js +0 -338
  70. package/dist/src/structural-snapshot.js +0 -33
  71. package/dist/src/system-config.js +0 -615
  72. package/dist/src/terminal-help.js +0 -83
  73. package/dist/src/tools/knodin-tools.js +0 -1529
  74. package/dist/src/update-ceremony.js +0 -158
  75. package/dist/src/update-policy.js +0 -944
  76. package/dist/src/update-trust.js +0 -503
  77. package/dist/src/version.js +0 -13
  78. package/dist/src/visualization.js +0 -162
  79. package/dist/src/wait-for-fresh.js +0 -98
  80. package/dist/src/worktree-lifecycle.js +0 -231
  81. package/docs/CONTAINED-EXECUTION.md +0 -77
  82. package/docs/DIAGNOSTICS.md +0 -45
  83. package/docs/GIT-HISTORY-REVIEW.md +0 -39
  84. package/docs/PROGRESSIVE-EVIDENCE.md +0 -37
  85. package/docs/PT-ACCESS-RECOMMENDATION.md +0 -91
  86. package/docs/RELEASE-0.3-EVIDENCE.md +0 -73
  87. package/docs/SCIP-IMPORT.md +0 -57
  88. package/docs/SIGNED-UPDATES.md +0 -151
  89. package/docs/TELEMETRY.md +0 -46
  90. package/docs/assets/knodin-favicon.svg +0 -4
  91. package/docs/releases/0.3.0.md +0 -46
  92. package/docs/releases/0.4.0.md +0 -68
  93. package/docs/releases/0.4.1.md +0 -28
  94. package/docs/releases/0.4.2.md +0 -27
  95. package/docs/releases/0.4.3.md +0 -23
  96. package/docs/releases/0.5.0.md +0 -29
  97. package/docs/releases/0.5.1.md +0 -17
  98. package/docs/releases/0.6.0.md +0 -18
  99. package/docs/releases/0.7.0.md +0 -24
  100. package/docs/releases/0.7.1.md +0 -21
  101. package/docs/releases/0.7.2.md +0 -21
  102. package/docs/releases/0.7.3.md +0 -23
  103. package/docs/releases/0.7.4.md +0 -17
  104. package/schemas/release-attestation-v1.schema.json +0 -210
@@ -1,274 +0,0 @@
1
- /**
2
- * CLI argument & repo-target resolution for the `knodin` CLI (R19).
3
- *
4
- * The MCP gateway takes an explicit `repoPath`; the CLI historically fell back
5
- * to `process.cwd()` with no equivalent flag, so `knodin index /other/repo` from
6
- * the wrong cwd silently indexed nothing and exited 0. These pure helpers give
7
- * the CLI one unambiguous way to target a repo (`--repo`) and make every no-op
8
- * fail loudly. Kept side-effect-free (only stat the filesystem) so bin/cli.ts is
9
- * a thin caller and the logic is unit-testable without an engine or database.
10
- */
11
- import fs from "node:fs";
12
- import { createRequire } from "node:module";
13
- import path from "node:path";
14
- const GLOBAL_VALUE_FLAGS = new Set([
15
- "--identity",
16
- "--file",
17
- "--kind",
18
- "--to-identity",
19
- "--to-file",
20
- "--to-kind",
21
- "--bytes",
22
- "--tokens",
23
- "--items",
24
- "--impact-mode",
25
- "--direction",
26
- "--depth",
27
- "--relations",
28
- "--min-confidence",
29
- "--limit",
30
- ]);
31
- const GLOBAL_BOOLEAN_FLAGS = new Set(["--exclude-tests", "--data-flow", "--json"]);
32
- const isDir = (p) => {
33
- try {
34
- return fs.statSync(p).isDirectory();
35
- }
36
- catch {
37
- return false;
38
- }
39
- };
40
- const RUNTIME_MODULE_FLAGS = new Set([
41
- "--import",
42
- "--loader",
43
- "--experimental-loader",
44
- "--require",
45
- "-r",
46
- ]);
47
- function resolveRuntimeModuleReference(value, cwd, resolveModule) {
48
- if (path.isAbsolute(value) || /^[a-zA-Z][a-zA-Z\d+.-]*:/.test(value))
49
- return value;
50
- if (value.startsWith("."))
51
- return path.resolve(cwd, value);
52
- return resolveModule(value, cwd);
53
- }
54
- /**
55
- * Reconstruct the executable command that launched this CLI. Source-checkout
56
- * invocations need loader arguments such as `node --import tsx`; dropping
57
- * `execArgv` creates a background hook that exists but cannot start. Bare
58
- * loader names are resolved now because the hook later runs from another repo.
59
- */
60
- export function resolveCliRuntimeCommand(runtime, resolveModule = (specifier, cwd) => createRequire(path.join(cwd, "package.json")).resolve(specifier)) {
61
- const entry = runtime.argv[1];
62
- if (!entry)
63
- throw new Error("knodin: CLI entry path is unavailable");
64
- const cwd = runtime.cwd();
65
- const runtimeArguments = [];
66
- for (let index = 0; index < runtime.execArgv.length; index++) {
67
- const argument = runtime.execArgv[index];
68
- if (RUNTIME_MODULE_FLAGS.has(argument)) {
69
- const value = runtime.execArgv[index + 1];
70
- if (!value)
71
- throw new Error(`knodin: ${argument} requires a runtime module`);
72
- runtimeArguments.push(argument, resolveRuntimeModuleReference(value, cwd, resolveModule));
73
- index++;
74
- continue;
75
- }
76
- const equalsFlag = [...RUNTIME_MODULE_FLAGS].find((flag) => argument.startsWith(`${flag}=`));
77
- if (equalsFlag) {
78
- const value = argument.slice(equalsFlag.length + 1);
79
- if (!value)
80
- throw new Error(`knodin: ${equalsFlag} requires a runtime module`);
81
- runtimeArguments.push(`${equalsFlag}=${resolveRuntimeModuleReference(value, cwd, resolveModule)}`);
82
- continue;
83
- }
84
- runtimeArguments.push(argument);
85
- }
86
- return [path.resolve(cwd, runtime.execPath), ...runtimeArguments, path.resolve(cwd, entry)];
87
- }
88
- /**
89
- * Pull a global `--repo <path>` / `--repo=<path>` out of an argv slice and
90
- * move any other documented global flags before the subcommand to immediately
91
- * after it. This preserves the existing command parsers while accepting both
92
- * conventional `knodin --file x explain y` and `knodin explain y --file x`.
93
- */
94
- export function extractRepoFlag(argv) {
95
- const rest = [];
96
- const leadingGlobalOptions = [];
97
- let repoFlag;
98
- let commandSeen = false;
99
- for (let i = 0; i < argv.length; i++) {
100
- const a = argv[i];
101
- if (a === "--repo") {
102
- repoFlag = argv[i + 1] ?? "";
103
- i++; // consume the value
104
- }
105
- else if (a.startsWith("--repo=")) {
106
- repoFlag = a.slice("--repo=".length);
107
- }
108
- else if (!commandSeen && GLOBAL_VALUE_FLAGS.has(a)) {
109
- leadingGlobalOptions.push(a);
110
- if (argv[i + 1] !== undefined)
111
- leadingGlobalOptions.push(argv[++i]);
112
- }
113
- else if (!commandSeen && GLOBAL_BOOLEAN_FLAGS.has(a)) {
114
- leadingGlobalOptions.push(a);
115
- }
116
- else {
117
- rest.push(a);
118
- commandSeen = true;
119
- }
120
- }
121
- rest.push(...leadingGlobalOptions);
122
- return { repoFlag, rest };
123
- }
124
- /** Return positional arguments without mistaking recognized option values for paths. */
125
- export function extractPositionals(args) {
126
- const positionals = [];
127
- for (let index = 0; index < args.length; index++) {
128
- const argument = args[index];
129
- if (argument.startsWith("--")) {
130
- if (!argument.includes("=") && GLOBAL_VALUE_FLAGS.has(argument))
131
- index++;
132
- continue;
133
- }
134
- positionals.push(argument);
135
- }
136
- return positionals;
137
- }
138
- /**
139
- * Resolve the target repo: an explicit `--repo` (validated as an existing
140
- * directory) wins over `cwd`. An absent flag falls back to `cwd`.
141
- */
142
- export function resolveRepo(repoFlag, cwd) {
143
- if (repoFlag === undefined)
144
- return { ok: true, repo: cwd };
145
- if (repoFlag === "")
146
- return { ok: false, error: "knodin: --repo requires a <path>" };
147
- const repo = path.resolve(cwd, repoFlag);
148
- if (!fs.existsSync(repo)) {
149
- return { ok: false, error: `knodin: --repo path does not exist: ${repo}` };
150
- }
151
- if (!isDir(repo)) {
152
- return { ok: false, error: `knodin: --repo path is not a directory: ${repo}` };
153
- }
154
- return { ok: true, repo };
155
- }
156
- /**
157
- * Decide what `knodin index [positionals...]` actually targets, closing the
158
- * silent-no-op hole:
159
- * - a lone directory positional with no `--repo` IS the repo (full reindex) —
160
- * this is the observed-bug fix: `knodin index /other/repo` now indexes it;
161
- * - any other directory positional is rejected, pointing at `--repo`;
162
- * - file positionals resolving outside the target repo are rejected loudly;
163
- * - otherwise the positionals are files to index within the resolved repo.
164
- */
165
- export function planIndex(repoFlag, positionals, cwd) {
166
- // Case A: no --repo and a single directory positional => that directory is
167
- // the repo to index, not a file. Absorb it as the target.
168
- if (repoFlag === undefined && positionals.length === 1) {
169
- const abs = path.resolve(cwd, positionals[0]);
170
- if (isDir(abs)) {
171
- return { ok: true, repo: abs, files: undefined };
172
- }
173
- }
174
- const resolved = resolveRepo(repoFlag, cwd);
175
- if (!resolved.ok)
176
- return resolved;
177
- const repo = resolved.repo;
178
- if (positionals.length === 0) {
179
- return { ok: true, repo, files: undefined }; // full reindex
180
- }
181
- // Case B: positionals are files. Validate each is a within-repo non-directory.
182
- const rejectedDirs = [];
183
- const rejectedOutside = [];
184
- const files = [];
185
- for (const p of positionals) {
186
- const abs = path.resolve(repo, p);
187
- const rel = path.relative(repo, abs);
188
- if (rel === "" || rel === ".." || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
189
- rejectedOutside.push(p);
190
- continue;
191
- }
192
- if (isDir(abs)) {
193
- rejectedDirs.push(p);
194
- continue;
195
- }
196
- files.push(p);
197
- }
198
- if (rejectedOutside.length > 0 || rejectedDirs.length > 0) {
199
- const parts = [];
200
- if (rejectedOutside.length > 0) {
201
- parts.push(`outside repo (${rejectedOutside.join(", ")})`);
202
- }
203
- if (rejectedDirs.length > 0) {
204
- const hint = repoFlag === undefined
205
- ? "use --repo to index a whole repo"
206
- : "pass files, not directories, alongside --repo";
207
- parts.push(`directories — ${hint} (${rejectedDirs.join(", ")})`);
208
- }
209
- return {
210
- ok: false,
211
- error: `knodin index: refusing to index against ${repo}; rejected ${parts.join("; ")}`,
212
- };
213
- }
214
- return { ok: true, repo, files };
215
- }
216
- /**
217
- * Guard against reporting success for a no-op: an index run that touched zero
218
- * files must exit non-zero with a diagnostic naming the repo it actually used.
219
- */
220
- export function checkIndexed(indexed, repo) {
221
- if (!indexed || indexed.length === 0) {
222
- return { ok: false, error: `knodin index: indexed 0 files in ${repo} (nothing matched)` };
223
- }
224
- return { ok: true };
225
- }
226
- /** Pure parser for `knodin review`, shared by the executable and CLI contract tests. */
227
- export function parseReviewArgs(args) {
228
- const value = (flag) => {
229
- const index = args.indexOf(flag);
230
- if (index < 0)
231
- return undefined;
232
- const candidate = args[index + 1];
233
- if (!candidate || candidate.startsWith("--")) {
234
- throw new Error(`knodin review: ${flag} requires a value`);
235
- }
236
- return candidate;
237
- };
238
- const rawScope = value("--scope");
239
- const scopes = ["unstaged", "staged", "all", "compare"];
240
- if (rawScope && !scopes.includes(rawScope)) {
241
- throw new Error(`invalid review scope: ${rawScope}`);
242
- }
243
- const optionValues = new Set([
244
- ...GLOBAL_VALUE_FLAGS,
245
- "--scope",
246
- "--from",
247
- "--to",
248
- "--files",
249
- "--scip",
250
- ]);
251
- const positionals = args.filter((arg, index) => {
252
- if (arg.startsWith("--"))
253
- return false;
254
- return index === 0 || !optionValues.has(args[index - 1]);
255
- });
256
- const filesValue = value("--files");
257
- const files = filesValue
258
- ?.split(",")
259
- .map((file) => file.trim())
260
- .filter(Boolean);
261
- if (filesValue !== undefined && files?.length === 0) {
262
- throw new Error("knodin review: --files requires at least one repo-relative path");
263
- }
264
- return {
265
- base: positionals.find((item) => item !== "minimal") ?? "HEAD~1",
266
- detailLevel: positionals.includes("minimal") ? "minimal" : "standard",
267
- options: {
268
- scope: rawScope,
269
- from: value("--from"),
270
- to: value("--to"),
271
- files,
272
- },
273
- };
274
- }
@@ -1,358 +0,0 @@
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
- if (action === "discover") {
70
- command
71
- .option("--signals", "include bounded repository applicability signals")
72
- .addOption(option("--items <count>", "bound returned repositories and signal arrays", "integer"))
73
- .addOption(option("--bytes <count>", "bound serialized response bytes", "integer"))
74
- .addOption(option("--tokens <count>", "bound estimated response tokens", "integer"))
75
- .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");
76
- }
77
- }
78
- leaf(repos, "search <query>", "search selected repositories sequentially", capture)
79
- .addOption(option("--root <path>", "portfolio root", "collect"))
80
- .addOption(option("--include <selector>", "include a repository id or path", "collect"))
81
- .addOption(option("--exclude <selector>", "exclude a repository id or path", "collect"))
82
- .option("--cursor <cursor>", "resume from an opaque cursor")
83
- .option("--allow-partial", "return healthy repository results when another degrades");
84
- const fleet = program.command("fleet").description("deprecated repository-fleet compatibility");
85
- leaf(fleet, "init <roots...>", "initialize a repository fleet", capture)
86
- .addOption(option("--depth <count>", "maximum discovery depth", "integer"))
87
- .option("--worktrees <mode>", "skip or include linked worktrees")
88
- .option("--dry-run", "report actions without mutation");
89
- }
90
- function addGraphCommands(program, capture) {
91
- leaf(program, "context <task> [base]", "build compact task orientation", capture);
92
- leaf(program, "explain <symbol> [detail]", "explain one ambiguity-safe symbol", capture);
93
- leaf(program, "review [base] [detail]", "review an explicit Git diff scope", capture)
94
- .option("--scope <scope>", "unstaged, staged, all, or compare")
95
- .option("--from <ref>", "comparison start revision")
96
- .option("--to <ref>", "comparison end revision")
97
- .option("--files <paths>", "comma-separated repo-relative paths");
98
- leaf(program, "map", "show architecture communities and edges", capture)
99
- .option("--standard", "include full map detail")
100
- .addOption(option("--top <count>", "maximum ranked communities", "integer"))
101
- .option("--sort <mode>", "relevance, name, size, degree, or complexity")
102
- .option("--relations <kinds>", "comma-separated relationship kinds");
103
- leaf(program, "wiki", "write local architecture wiki pages", capture).option("--force", "rewrite unchanged pages");
104
- leaf(program, "visualize <entry>", "write a local architecture/call-flow HTML artifact", capture)
105
- .requiredOption("--output <path>", "repo-relative HTML output path")
106
- .addOption(option("--depth <count>", "call-flow depth", "integer"))
107
- .addOption(option("--max-bytes <count>", "hard artifact budget", "integer"));
108
- leaf(program, "search <query> [limit]", "hybrid symbol search", capture)
109
- .option("--languages <values>", "comma-separated languages")
110
- .option("--extensions <values>", "comma-separated extensions")
111
- .option("--kinds <values>", "comma-separated symbol kinds")
112
- .option("--path <prefix>", "repo-relative path prefix")
113
- .option("--tests-only", "search test symbols only")
114
- .option("--production-only", "search production symbols only")
115
- .option("--no-source", "omit source snippets")
116
- .addOption(option("--offset <count>", "pagination offset", "integer"))
117
- .addOption(option("--limit <count>", "result limit", "integer"));
118
- leaf(program, "query <pattern> [targets...]", "run one structured graph query", capture)
119
- .option("--impact-mode <mode>", "symbol or file impact")
120
- .option("--direction <direction>", "upstream, downstream, or both")
121
- .addOption(option("--depth <count>", "bounded traversal depth", "integer"))
122
- .option("--relations <kinds>", "comma-separated relationship kinds")
123
- .addOption(option("--min-confidence <value>", "minimum edge confidence", "number"))
124
- .option("--exclude-tests", "exclude tests from impact")
125
- .option("--data-flow", "include bounded data-flow evidence")
126
- .option("--limit <count>", "result limit")
127
- .addOption(option("--min-lines <count>", "minimum line count", "integer"))
128
- .addOption(option("--min-complexity <count>", "minimum complexity", "integer"))
129
- .option("--kinds <values>", "comma-separated symbol kinds")
130
- .option("--path <prefix>", "repo-relative path prefix")
131
- .option("--variable <name>", "flow-analysis variable")
132
- .option("--facets <values>", "comma-separated architecture facets")
133
- .addOption(option("--top <count>", "maximum ranked results", "integer"))
134
- .option("--sort <mode>", "result ordering");
135
- leaf(program, "rename <old> <new>", "preview or apply an ambiguity-safe rename", capture)
136
- .option("--apply", "apply the verified edit")
137
- .option("--no-verify", "skip post-apply typecheck");
138
- }
139
- function addArtifactCommands(program, capture) {
140
- leaf(program, "evidence <level> <file>", "deliver progressive source evidence", capture)
141
- .option("--continuation <handle>", "resume an exact prior response")
142
- .option("--baseline-hash <sha256>", "complete baseline SHA-256")
143
- .addOption(option("--baseline-bytes <count>", "complete baseline UTF-8 bytes", "integer"))
144
- .addOption(option("--start <line>", "first source line", "integer"))
145
- .addOption(option("--end <line>", "last source line", "integer"));
146
- const pack = program.command("pack").description("export bounded portable context");
147
- pack
148
- .argument("[input]")
149
- .option("--format <format>", "markdown, json, or xml")
150
- .option("--include <globs>", "comma-separated include globs")
151
- .option("--exclude <globs>", "comma-separated exclude globs")
152
- .option("--policy <assignments>", "comma-separated glob policies")
153
- .option("--already-present <paths>", "comma-separated paths already in context")
154
- .option("--chat-files <paths>", "comma-separated chat paths")
155
- .option("--line-numbers", "include source line numbers")
156
- .option("--tree", "include repository tree")
157
- .option("--output <path>", "write a repo-relative retained artifact")
158
- .option("--diff-scope <scope>", "unstaged, staged, all, or compare")
159
- .option("--from <ref>", "comparison start revision")
160
- .option("--to <ref>", "comparison end revision")
161
- .addOption(option("--log <count>", "include recent commits", "integer"))
162
- .action((...values) => capture(values.at(-1)));
163
- leaf(pack, "read <artifact>", "read a retained context artifact", capture)
164
- .addOption(option("--start <line>", "first line", "integer"))
165
- .addOption(option("--end <line>", "last line", "integer"));
166
- leaf(pack, "grep <artifact> <regex>", "search an artifact with a linear-time regex", capture)
167
- .option("--flags <flags>", "regular-expression flags")
168
- .addOption(option("--limit <count>", "match limit", "integer"));
169
- const compress = program.command("compress").description("compress already-produced output");
170
- compress
171
- .argument("[input]")
172
- .option("--strategy <strategy>", "smart, head-tail, or errors-only")
173
- .option("--adapter <adapter>", "structured output adapter")
174
- .addOption(option("--lines <count>", "hard line budget", "integer"))
175
- .addOption(option("--max-output-bytes <count>", "hard output byte budget", "integer"))
176
- .addOption(option("--context <count>", "signal context lines", "integer"))
177
- .addOption(option("--exit-code <code>", "source process exit code", "integer"))
178
- .option("--signal <name>", "source process termination signal")
179
- .addOption(option("--max-input-bytes <count>", "hard input byte limit", "integer"))
180
- .option("--no-retain", "do not retain raw drill-down data")
181
- .option("--no-redact", "disable secret redaction")
182
- .action((...values) => capture(values.at(-1)));
183
- for (const action of ["read", "diagnose"]) {
184
- leaf(compress, `${action} <artifact>`, `${action} a retained output artifact`, capture)
185
- .addOption(option("--start <line>", "first line", "integer"))
186
- .addOption(option("--end <line>", "last line", "integer"))
187
- .addOption(option("--max-output-bytes <count>", "hard output byte budget", "integer"))
188
- .addOption(option("--context <count>", "diagnostic context lines", "integer"))
189
- .addOption(option("--limit <count>", "diagnostic result limit", "integer"))
190
- .addOption(option("--offset <count>", "resume retained diagnostics at offset", "integer"))
191
- .option("--raw", "return unredacted retained bytes");
192
- }
193
- leaf(compress, "delete <artifact>", "delete a retained output artifact", capture);
194
- }
195
- function createCliProgram(capture = () => { }) {
196
- const program = new Command("knodin")
197
- .description("knodin — source-evidenced local code intelligence with known bounds")
198
- .showHelpAfterError()
199
- .showSuggestionAfterError()
200
- .passThroughOptions(false)
201
- .allowExcessArguments(false)
202
- .exitOverride();
203
- program.configureOutput({ writeErr: () => { } });
204
- addGlobalOptions(program);
205
- 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");
206
- leaf(program, "init", "initialize graph, lifecycle hooks, and agent integration", capture)
207
- .option("--scope <scope>", "personal, team, or cli-only")
208
- .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");
209
- leaf(program, "configure", "change or inspect agent integration", capture)
210
- .option("--scope <scope>", "personal, team, or cli-only")
211
- .option("--status", "inspect configuration without changing it");
212
- leaf(program, "index [files...]", "index a repository or selected files", capture)
213
- .option("--clean", "rebuild selected index state")
214
- .option("--force", "force clean indexing")
215
- .option("--scip <file>", "opt in to a bounded local SCIP protobuf import");
216
- leaf(program, "doctor", "diagnose installation, clients, hooks, graph, and updates", capture).option("--client <client>", "claude, codex, gemini, or antigravity");
217
- leaf(program, "status", "report graph, lifecycle, integration, and update state", capture)
218
- .option("--deep", "run a full graph audit")
219
- .option("--watch", "stream status snapshots")
220
- .addOption(option("--interval <seconds>", "watch interval", "number"));
221
- leaf(program, "wait", "wait for current graph evidence", capture)
222
- .option("--fresh", "wait for a fresh graph")
223
- .addOption(option("--timeout <seconds>", "deadline", "number"));
224
- leaf(program, "repair", "audit and reconcile graph state", capture)
225
- .option("--plan", "report the repair plan without mutation")
226
- .option("--jsonl", "stream JSONL progress")
227
- .option("--progress <mode>", "tty, jsonl, plain, or none")
228
- .option("--progress-interval <duration>", "progress interval such as 750ms, 30s, or 2m");
229
- leaf(program, "serve", "run the one-tool MCP gateway on stdio", capture);
230
- leaf(program, "version", "print the installed knodin version", capture);
231
- leaf(program, "hook-refresh <kind> [values...]", "internal Git lifecycle refresh", capture);
232
- leaf(program, "refresh-artifacts [event]", "refresh external graph artifacts", capture);
233
- addRepositoryCommands(program, capture);
234
- addGraphCommands(program, capture);
235
- addArtifactCommands(program, capture);
236
- const system = program.command("system").description("inspect declared multi-repository systems");
237
- leaf(system, "list", "list configured systems", capture);
238
- for (const action of ["show", "validate", "query"]) {
239
- const command = leaf(system, `${action} <system-id>`, `${action} one configured system`, capture);
240
- if (action === "query")
241
- command.option("--allow-partial", "return healthy components");
242
- }
243
- const update = program
244
- .command("update")
245
- .description("inspect or apply threshold-signed updates")
246
- .allowExcessArguments(false)
247
- .addArgument(new Argument("<action>").choices(["status", "check", "explain", "apply", "rollback"]));
248
- update.action((...values) => capture(values.at(-1)));
249
- leaf(program, "docs <topic>", "read canonical product documentation", capture);
250
- leaf(program, "prs [action]", "audit pull requests, reviews, and checks", capture)
251
- .option("--state <state>", "open, merged, closed, or all")
252
- .addOption(option("--limit <count>", "pull-request limit", "integer"))
253
- .option("--branches <pattern>", "branch-name pattern")
254
- .option("--range <range>", "pull-request number range")
255
- .option("--base <ref>", "audit base revision")
256
- .option("--head <ref>", "audit head revision")
257
- .option("--expected-login <login>", "expected GitHub identity");
258
- leaf(program, "worktrees [action] [path]", "inspect, reconcile, or remove managed worktrees", capture).option("--dry-run", "report removal without mutation");
259
- const telemetry = program
260
- .command("telemetry")
261
- .description("manage local opt-in ROI telemetry")
262
- .allowExcessArguments(false)
263
- .addArgument(new Argument("<action>").choices(["status", "report", "export", "clear"]));
264
- telemetry
265
- .option("--input <path>", "telemetry input path")
266
- .option("--output <path>", "dashboard or evidence-bundle output path")
267
- .addOption(option("--retention-days <count>", "retention window in days", "integer"))
268
- .action((...values) => capture(values.at(-1)));
269
- const diagnostics = program
270
- .command("diagnostics")
271
- .description("manage local troubleshooting diagnostics and support bundles")
272
- .allowExcessArguments(false)
273
- .addArgument(new Argument("<action>").choices([
274
- "enable",
275
- "status",
276
- "collect",
277
- "inspect",
278
- "clear",
279
- "disable",
280
- ]))
281
- .addArgument(new Argument("[bundle]"));
282
- diagnostics
283
- .addOption(option("--retention-days <count>", "local event retention in days", "integer"))
284
- .option("--since <duration>", "collection window, such as 24h or 7d")
285
- .option("--output <path>", "repository-contained .json.gz bundle path")
286
- .action((...values) => capture(values.at(-1)));
287
- return program;
288
- }
289
- function flattenPositionals(values) {
290
- return values
291
- .flatMap((value) => (Array.isArray(value) ? value : [value]))
292
- .filter((value) => typeof value === "string");
293
- }
294
- /** Parse and validate one invocation without running a product operation. */
295
- export function parseCliInvocation(argv) {
296
- let selected;
297
- const program = createCliProgram((command) => {
298
- selected = command;
299
- });
300
- program.parse([process.execPath, "knodin", ...argv]);
301
- if (!selected)
302
- return { commandPath: [], positionals: [], options: program.opts() };
303
- const command = selected;
304
- const commandPath = [];
305
- for (let current = command; current?.parent; current = current.parent) {
306
- commandPath.unshift(current.name());
307
- }
308
- return {
309
- commandPath,
310
- positionals: flattenPositionals(command.processedArgs),
311
- options: command.optsWithGlobals(),
312
- };
313
- }
314
- /** Render root or nested help from the exact parser model. */
315
- export function renderCliHelp(commandPath, columns) {
316
- const program = createCliProgram();
317
- let selected = program;
318
- for (const name of commandPath) {
319
- const child = selected.commands.find((command) => command.name() === name);
320
- if (!child)
321
- throw new Error(`knodin: unknown help command ${commandPath.join(" ")}`);
322
- selected = child;
323
- }
324
- selected.configureHelp({ helpWidth: Math.max(60, Math.floor(columns ?? 100) - 1) });
325
- let output = "";
326
- selected.configureOutput({ writeOut: (value) => (output += value), writeErr: () => { } });
327
- selected.outputHelp();
328
- return formatTerminalHelp(output, columns);
329
- }
330
- /** Resolve the deepest declared command before a help flag or positional. */
331
- export function helpCommandPath(argv) {
332
- const program = createCliProgram();
333
- const path = [];
334
- let selected = program;
335
- for (let index = 0; index < argv.length; index++) {
336
- const value = argv[index];
337
- if (value === "-h" || value === "--help")
338
- break;
339
- if (value.startsWith("-")) {
340
- const optionName = value.split("=", 1)[0];
341
- let cursor = selected;
342
- let declared;
343
- while (cursor && !declared) {
344
- declared = cursor.options.find((candidate) => candidate.short === optionName || candidate.long === optionName);
345
- cursor = cursor.parent;
346
- }
347
- if (declared?.required && !value.includes("="))
348
- index++;
349
- continue;
350
- }
351
- const child = selected.commands.find((command) => command.name() === value);
352
- if (!child)
353
- break;
354
- selected = child;
355
- path.push(value);
356
- }
357
- return path;
358
- }