@cairn-tool/cairn 3.3.0 → 3.4.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/dist/cli.js CHANGED
@@ -2,35 +2,6 @@
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { Command } from "commander";
4
4
  import { TARGETS } from "./agent/types.js";
5
- import { lintAction } from "./commands/lint.js";
6
- import { lintDirAction } from "./commands/lint-dir.js";
7
- import { refsAction } from "./commands/refs.js";
8
- import { refsToAction } from "./commands/refs-to.js";
9
- import { headersAction } from "./commands/headers.js";
10
- import { outlineAction } from "./commands/outline.js";
11
- import { tocAction } from "./commands/toc.js";
12
- import { statsAction } from "./commands/stats.js";
13
- import { codeBlocksAction } from "./commands/code-blocks.js";
14
- import { structureAction } from "./commands/structure.js";
15
- import { linksAction } from "./commands/links.js";
16
- import { sectionAction } from "./commands/section.js";
17
- import { frontmatterAction } from "./commands/frontmatter.js";
18
- import { tasksAction } from "./commands/tasks.js";
19
- import { tablesAction } from "./commands/tables.js";
20
- import { checkUrlsAction } from "./commands/check-urls.js";
21
- import { orphansAction } from "./commands/orphans.js";
22
- import { renameHeadingAction } from "./commands/rename-heading.js";
23
- import { renameFileAction } from "./commands/rename-file.js";
24
- import { graphAction } from "./commands/graph.js";
25
- import { validateFrontmatterAction } from "./commands/validate-frontmatter.js";
26
- import { auditAction } from "./commands/audit.js";
27
- import { queryAction } from "./commands/query.js";
28
- import { contextAction } from "./commands/context.js";
29
- import { diffAction } from "./commands/diff.js";
30
- import { fixAction } from "./commands/fix.js";
31
- import { checkSnippetsAction } from "./commands/check-snippets.js";
32
- import { indexAction } from "./commands/index.js";
33
- import { checkUpdateAction, refreshUpdateCacheAction } from "./commands/update-check.js";
34
5
  import { installUpdateNotifier, CHECK_COMMAND, REFRESH_COMMAND } from "./update-notifier.js";
35
6
  import { loadConfig, selectConfig, selectRoot, defaultLintConcurrency } from "./config.js";
36
7
  import { commandOptions, initializeRuntime, runtime } from "./runtime.js";
@@ -38,29 +9,6 @@ import { CommandExit } from "./command-result.js";
38
9
  import { collect } from "./option-utils.js";
39
10
  import { formatsFor } from "./formats.js";
40
11
  import { packageName, packageVersion as version } from "./version.js";
41
- import { agentCompatAction, agentConvertAction, agentInspectAction, agentValidateAction, agentActionBoundary, } from "./commands/agent.js";
42
- import { agentSpecsAction } from "./commands/agent-specs.js";
43
- import { agentAddAction, agentInitAction } from "./commands/agent-scaffold.js";
44
- import { agentUpgradeAction } from "./commands/agent-upgrade.js";
45
- import { agentImportAction } from "./commands/agent-import.js";
46
- import { agentPackageAction } from "./commands/agent-package.js";
47
- import { agentMarketplaceAction } from "./commands/agent-marketplace.js";
48
- import { agentAuditAction } from "./commands/agent-audit.js";
49
- import { agentTestAction } from "./commands/agent-test.js";
50
- import { agentDoctorAction } from "./commands/agent-doctor.js";
51
- import { agentInstallAction } from "./commands/agent-install.js";
52
- import { agentUninstallAction } from "./commands/agent-uninstall.js";
53
- import { agentInstalledAction } from "./commands/agent-installed.js";
54
- import { agentVerifyAction } from "./commands/agent-verify.js";
55
- import { describeAction } from "./commands/describe.js";
56
- import { schemaAction } from "./commands/schema.js";
57
- import { completionAction } from "./commands/completion.js";
58
- import { serveAction } from "./commands/serve.js";
59
- import { scriptsListAction, scriptsRunAction, scriptsWhichAction, } from "./commands/scripts.js";
60
- import { archiveExtractAction, archiveListAction, archiveMigrateAction, archiveRunAction, archiveStatusAction, archiveVerifyAction, } from "./commands/archive.js";
61
- import { adfActionBoundary, adfFromMarkdownAction, adfInspectAction, adfToMarkdownAction, adfValidateAction, } from "./commands/jira.js";
62
- import { pdfActionBoundary, pdfAttachmentsAction, pdfFormsAction, pdfInspectAction, pdfOutlineAction, pdfTextAction, pdfToMarkdownAction, pdfValidateAction, } from "./commands/pdf.js";
63
- import { usageAgentsAction, usageCommandsAction, usageHooksAction, usageImportAction, usageIndexAction, usageMigrateAction, usageProjectsAction, usageProvidersAction, usageSessionsAction, usageSkillsAction, usageSummaryAction, usageTokensAction, usageToolsAction, } from "./commands/usage.js";
64
12
  // Pre-process argv to expand -fh/-fj shorthands into --format values
65
13
  // before Commander sees them (Commander doesn't support multi-char short flags).
66
14
  // Bounded to the tokens before the first `--`: everything after it is forwarded
@@ -155,7 +103,10 @@ agent
155
103
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
156
104
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
157
105
  .addHelpText("after", "\n--report writes the same document as conversion-report.json, provenance included, to\nan arbitrary path, so CI can keep the report without keeping the rendered tree. It is\nwritten in every mode, including --dry-run, --check, and a strict failure, and is never\nlisted in the artifacts. It must not be inside the source tree or the output directory.\n\nExit codes:\n 0 Successful and lossless\n 1 Invocation or I/O error\n 2 Validation, compatibility, strict, or stale-output finding")
158
- .action((source, opts) => agentActionBoundary("convert", opts, () => agentConvertAction(source, opts)));
106
+ .action(async (source, opts) => {
107
+ const { agentActionBoundary, agentConvertAction } = await import("./commands/agent.js");
108
+ return agentActionBoundary("convert", opts, () => agentConvertAction(source, opts));
109
+ });
159
110
  agent
160
111
  .command("validate")
161
112
  .description("Validate an agent bundle without generating output")
@@ -164,7 +115,10 @@ agent
164
115
  .option("--strict", "Treat approximations as blocking findings")
165
116
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
166
117
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
167
- .action((source, opts) => agentActionBoundary("validate", opts, () => agentValidateAction(source, opts)));
118
+ .action(async (source, opts) => {
119
+ const { agentActionBoundary, agentValidateAction } = await import("./commands/agent.js");
120
+ return agentActionBoundary("validate", opts, () => agentValidateAction(source, opts));
121
+ });
168
122
  agent
169
123
  .command("inspect")
170
124
  .description("Show the normalized bundle, references, overrides, and graph")
@@ -174,7 +128,10 @@ agent
174
128
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
175
129
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
176
130
  .addHelpText("after", "\n--target narrows a large bundle to the components that reach the selected targets,\nusing the same predicate the renderer uses, and reports what it excluded under\n`filter`. --profile drops the sections a profile never emits, such as hooks and MCP\noutside the plugin profile, and requires --target. Without either flag the output is\nunchanged.\n\nExit codes:\n 0 Bundle inspected\n 1 Invocation or I/O error\n 2 Bundle findings")
177
- .action((source, opts) => agentActionBoundary("inspect", opts, () => agentInspectAction(source, opts)));
131
+ .action(async (source, opts) => {
132
+ const { agentActionBoundary, agentInspectAction } = await import("./commands/agent.js");
133
+ return agentActionBoundary("inspect", opts, () => agentInspectAction(source, opts));
134
+ });
178
135
  agent
179
136
  .command("compat")
180
137
  .description("Show platform compatibility or analyze a bundle")
@@ -183,7 +140,10 @@ agent
183
140
  .option("--strict", "Treat approximations as blocking findings")
184
141
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
185
142
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
186
- .action((source, opts) => agentActionBoundary("compat", opts, () => agentCompatAction(source, opts)));
143
+ .action(async (source, opts) => {
144
+ const { agentActionBoundary, agentCompatAction } = await import("./commands/agent.js");
145
+ return agentActionBoundary("compat", opts, () => agentCompatAction(source, opts));
146
+ });
187
147
  agent
188
148
  .command("doctor")
189
149
  .description("Check a bundle and generated output against the target conformance profiles")
@@ -196,7 +156,13 @@ agent
196
156
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
197
157
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
198
158
  .addHelpText("after", "\nRuns without a bundle: profile self-checks and host version reporting still apply.\nNever executes a host's own tooling, so results do not depend on what is installed.\n\nExit codes:\n 0 No blocking conformance findings\n 1 Invocation or I/O error\n 2 Profile, drift, host, or strict finding")
199
- .action((source, opts) => agentActionBoundary("doctor", opts, () => agentDoctorAction(source, opts)));
159
+ .action(async (source, opts) => {
160
+ const [{ agentActionBoundary }, { agentDoctorAction }] = await Promise.all([
161
+ import("./commands/agent.js"),
162
+ import("./commands/agent-doctor.js"),
163
+ ]);
164
+ return agentActionBoundary("doctor", opts, () => agentDoctorAction(source, opts));
165
+ });
200
166
  agent
201
167
  .command("init")
202
168
  .description("Scaffold a new portable agent bundle")
@@ -215,7 +181,13 @@ agent
215
181
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
216
182
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
217
183
  .addHelpText("after", "\nNever prompts. Placeholder marketplace metadata is valid here; publish\nreadiness is checked by agent package.\n\nExit codes:\n 0 Bundle scaffolded, or dry run completed\n 1 Invocation or I/O error\n 2 --check found a missing or differing scaffold")
218
- .action((name, opts) => agentActionBoundary("init", opts, () => agentInitAction(name, opts)));
184
+ .action(async (name, opts) => {
185
+ const [{ agentActionBoundary }, { agentInitAction }] = await Promise.all([
186
+ import("./commands/agent.js"),
187
+ import("./commands/agent-scaffold.js"),
188
+ ]);
189
+ return agentActionBoundary("init", opts, () => agentInitAction(name, opts));
190
+ });
219
191
  agent
220
192
  .command("add")
221
193
  .description("Add one component to an existing bundle")
@@ -235,7 +207,13 @@ agent
235
207
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
236
208
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
237
209
  .addHelpText("after", "\nagent-bundle.yaml is edited through a comment-preserving YAML document and is\nleft byte-untouched when no manifest change is needed.\n\nExit codes:\n 0 Component added, or dry run completed\n 1 Invocation or I/O error\n 2 --check found a missing or differing component")
238
- .action((kind, name, bundle, opts) => agentActionBoundary("add", opts, () => agentAddAction(kind, name, bundle, opts)));
210
+ .action(async (kind, name, bundle, opts) => {
211
+ const [{ agentActionBoundary }, { agentAddAction }] = await Promise.all([
212
+ import("./commands/agent.js"),
213
+ import("./commands/agent-scaffold.js"),
214
+ ]);
215
+ return agentActionBoundary("add", opts, () => agentAddAction(kind, name, bundle, opts));
216
+ });
239
217
  agent
240
218
  .command("import")
241
219
  .description("Import an existing native plugin or project into a portable bundle")
@@ -252,7 +230,13 @@ agent
252
230
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
253
231
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
254
232
  .addHelpText("after", "\nDetection is driven by the target conformance profiles, so it cannot drift\nfrom what agent convert emits. Untranslatable pieces are preserved under\nnative/<target>/ rather than dropped.\n\nExit codes:\n 0 Imported, or dry run completed\n 1 Invocation or I/O error\n 2 Blocking finding, or --check found drift")
255
- .action((source, opts) => agentActionBoundary("import", opts, () => agentImportAction(source, opts)));
233
+ .action(async (source, opts) => {
234
+ const [{ agentActionBoundary }, { agentImportAction }] = await Promise.all([
235
+ import("./commands/agent.js"),
236
+ import("./commands/agent-import.js"),
237
+ ]);
238
+ return agentActionBoundary("import", opts, () => agentImportAction(source, opts));
239
+ });
256
240
  agent
257
241
  .command("upgrade")
258
242
  .description("Migrate a bundle between neutral schema versions")
@@ -263,7 +247,13 @@ agent
263
247
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
264
248
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
265
249
  .addHelpText("after", "\nOnly agent-bundle.yaml is rewritten; no component file is touched. The\nmigration is verified in memory to produce byte-identical generated output\nbefore it writes.\n\nExit codes:\n 0 Migrated, already current, or dry run completed\n 1 Invocation or I/O error\n 2 --check found a bundle below the target schema, or a blocking finding")
266
- .action((source, opts) => agentActionBoundary("upgrade", opts, () => agentUpgradeAction(source, opts)));
250
+ .action(async (source, opts) => {
251
+ const [{ agentActionBoundary }, { agentUpgradeAction }] = await Promise.all([
252
+ import("./commands/agent.js"),
253
+ import("./commands/agent-upgrade.js"),
254
+ ]);
255
+ return agentActionBoundary("upgrade", opts, () => agentUpgradeAction(source, opts));
256
+ });
267
257
  agent
268
258
  .command("package")
269
259
  .description("Build a distributable package with catalogs, checksums, and archives")
@@ -281,7 +271,13 @@ agent
281
271
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
282
272
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
283
273
  .addHelpText("after", "\nRenders the bundle itself, so a package can never certify a stale tree.\nNever contacts the network and never publishes.\n\nExit codes:\n 0 Package written, or checks passed\n 1 Invocation or I/O error\n 2 Publish-readiness, integrity, or stale finding")
284
- .action((source, opts) => agentActionBoundary("package", opts, () => agentPackageAction(source, opts)));
274
+ .action(async (source, opts) => {
275
+ const [{ agentActionBoundary }, { agentPackageAction }] = await Promise.all([
276
+ import("./commands/agent.js"),
277
+ import("./commands/agent-package.js"),
278
+ ]);
279
+ return agentActionBoundary("package", opts, () => agentPackageAction(source, opts));
280
+ });
285
281
  agent
286
282
  .command("marketplace")
287
283
  .description("Build a marketplace of several bundles from a collection spec")
@@ -302,7 +298,13 @@ agent
302
298
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
303
299
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
304
300
  .addHelpText("after", "\nRenders every bundle itself, so a catalog can never certify a stale tree.\nOne aggregated catalog per target, not one per bundle. Never contacts the\nnetwork and never publishes.\n\n--install registers one marketplace offering every plugin, where installing\neach bundle separately would register one marketplace per bundle.\n\nExit codes:\n 0 Collection written or installed, or checks passed\n 1 Invocation or I/O error\n 2 Spec, publish-readiness, install, or stale finding")
305
- .action((spec, opts) => agentActionBoundary("marketplace", opts, () => agentMarketplaceAction(spec, opts)));
301
+ .action(async (spec, opts) => {
302
+ const [{ agentActionBoundary }, { agentMarketplaceAction }] = await Promise.all([
303
+ import("./commands/agent.js"),
304
+ import("./commands/agent-marketplace.js"),
305
+ ]);
306
+ return agentActionBoundary("marketplace", opts, () => agentMarketplaceAction(spec, opts));
307
+ });
306
308
  agent
307
309
  .command("audit")
308
310
  .description("Review a bundle's executable surface, permissions, and supply chain")
@@ -314,7 +316,13 @@ agent
314
316
  .option("--format <fmt>", "Output format: llm, human, json, sarif", "llm")
315
317
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
316
318
  .addHelpText("after", "\nExplainable static analysis: nothing is executed and no network request is\nmade. Exit 2 means findings to review, not proof that a bundle is malicious.\n\nExit codes:\n 0 No blocking review findings\n 1 Invocation or I/O error\n 2 Review findings")
317
- .action((source, opts) => agentActionBoundary("audit", opts, () => agentAuditAction(source, opts)));
319
+ .action(async (source, opts) => {
320
+ const [{ agentActionBoundary }, { agentAuditAction }] = await Promise.all([
321
+ import("./commands/agent.js"),
322
+ import("./commands/agent-audit.js"),
323
+ ]);
324
+ return agentActionBoundary("audit", opts, () => agentAuditAction(source, opts));
325
+ });
318
326
  agent
319
327
  .command("test")
320
328
  .description("Run the model-free contract tests stored with a bundle")
@@ -327,7 +335,13 @@ agent
327
335
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
328
336
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
329
337
  .addHelpText("after", "\nEvery expectation is evaluated against the same in-memory render agent convert\nwould write. Nothing is executed, no model is called, and no file is written; a\nchanged golden digest is reported with both the expected and the actual value.\n--target and --profile narrow each case's own selection rather than widening it.\n\nExit codes:\n 0 Every selected case passed\n 1 Invocation or I/O error\n 2 A failing case, an invalid test file, or a warning under --strict")
330
- .action((source, opts) => agentActionBoundary("test", opts, () => agentTestAction(source, opts)));
338
+ .action(async (source, opts) => {
339
+ const [{ agentActionBoundary }, { agentTestAction }] = await Promise.all([
340
+ import("./commands/agent.js"),
341
+ import("./commands/agent-test.js"),
342
+ ]);
343
+ return agentActionBoundary("test", opts, () => agentTestAction(source, opts));
344
+ });
331
345
  agent
332
346
  .command("install")
333
347
  .description("Install one or more bundles into a host plugin or project directory")
@@ -347,7 +361,13 @@ agent
347
361
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
348
362
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
349
363
  .addHelpText("after", "\nRenders and packages in memory, so an install is always derived from the\nbundle rather than from a possibly-drifted dist tree. Destinations come from\nthe target profiles. --register is the only flag that edits host config.\n\n--target is repeatable, and one destination may hold several installs: they are\ntold apart by bundle, target, profile and scope. A run is planned in full before\nanything is written, so a blocked plan writes nothing at all. --target all covers\nevery target declaring a location for the scope.\n\n--config installs the agent.install block a repository declares, and --target\nthere narrows that block rather than adding to it.\n\nExit codes:\n 0 Installed, or checks passed\n 1 Invocation or I/O error\n 2 Install finding, or --check found drift")
350
- .action((source, opts) => agentActionBoundary("install", opts, () => agentInstallAction(source, opts)));
364
+ .action(async (source, opts) => {
365
+ const [{ agentActionBoundary }, { agentInstallAction }] = await Promise.all([
366
+ import("./commands/agent.js"),
367
+ import("./commands/agent-install.js"),
368
+ ]);
369
+ return agentActionBoundary("install", opts, () => agentInstallAction(source, opts));
370
+ });
351
371
  agent
352
372
  .command("uninstall")
353
373
  .description("Remove a previously installed bundle")
@@ -360,7 +380,13 @@ agent
360
380
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
361
381
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
362
382
  .addHelpText("after", "\nRemoves exactly the inventory recorded in .cairn-install.json and\nnothing else. --scope is optional: both scopes are searched, and two matches\nis an error rather than a guess.\n\nExit codes:\n 0 Removed, already absent under --check, or dry run completed\n 1 Invocation or I/O error\n 2 Manifest missing or malformed, or --check found the install still present")
363
- .action((name, opts) => agentActionBoundary("uninstall", opts, () => agentUninstallAction(name, opts)));
383
+ .action(async (name, opts) => {
384
+ const [{ agentActionBoundary }, { agentUninstallAction }] = await Promise.all([
385
+ import("./commands/agent.js"),
386
+ import("./commands/agent-uninstall.js"),
387
+ ]);
388
+ return agentActionBoundary("uninstall", opts, () => agentUninstallAction(name, opts));
389
+ });
364
390
  agent
365
391
  .command("installed")
366
392
  .description("List bundles installed by this CLI")
@@ -370,7 +396,13 @@ agent
370
396
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
371
397
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
372
398
  .addHelpText("after", "\nScans the install roots declared on the target profiles and lists every\n.cairn-install.json it finds.\n\nExit codes:\n 0 Listing written to stdout\n 1 Invocation error")
373
- .action((opts) => agentActionBoundary("installed", opts, () => agentInstalledAction(opts)));
399
+ .action(async (opts) => {
400
+ const [{ agentActionBoundary }, { agentInstalledAction }] = await Promise.all([
401
+ import("./commands/agent.js"),
402
+ import("./commands/agent-installed.js"),
403
+ ]);
404
+ return agentActionBoundary("installed", opts, () => agentInstalledAction(opts));
405
+ });
374
406
  agent
375
407
  .command("verify")
376
408
  .description("Check committed agent trees against the bundles they were generated from")
@@ -380,7 +412,13 @@ agent
380
412
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
381
413
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
382
414
  .addHelpText("after", "\nReads what to verify from the agent.verify block of a cairn configuration\ndocument, so a CI pipeline can run it with no arguments. Each declared bundle\nis rendered in memory and compared against the committed tree, and the pinned\nCLI and target profile versions are asserted against the running build.\n\nExit codes:\n 0 Every entry matches and every pin is satisfied\n 1 Invocation, configuration, or I/O error\n 2 Drift, an orphaned file, or a violated pin")
383
- .action((opts) => agentActionBoundary("verify", opts, () => agentVerifyAction(opts)));
415
+ .action(async (opts) => {
416
+ const [{ agentActionBoundary }, { agentVerifyAction }] = await Promise.all([
417
+ import("./commands/agent.js"),
418
+ import("./commands/agent-verify.js"),
419
+ ]);
420
+ return agentActionBoundary("verify", opts, () => agentVerifyAction(opts));
421
+ });
384
422
  agent
385
423
  .command("specs")
386
424
  .description("Print the versioned target conformance profiles")
@@ -388,43 +426,61 @@ agent
388
426
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
389
427
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
390
428
  .addHelpText("after", "\nThe profiles are the source of truth for target behavior; --format json is the\nform to depend on.\n\nExit codes:\n 0 Profiles written to stdout\n 1 Invocation error")
391
- .action((opts) => agentActionBoundary("specs", opts, () => agentSpecsAction(opts)));
429
+ .action(async (opts) => {
430
+ const [{ agentActionBoundary }, { agentSpecsAction }] = await Promise.all([
431
+ import("./commands/agent.js"),
432
+ import("./commands/agent-specs.js"),
433
+ ]);
434
+ return agentActionBoundary("specs", opts, () => agentSpecsAction(opts));
435
+ });
392
436
  program
393
437
  .command(CHECK_COMMAND)
394
438
  .description("Check whether a newer version of this CLI has been published")
395
439
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
396
440
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
397
441
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json\n\nQueries the registry directly rather than using the 24h cache.\n\nExit codes:\n 0 Already on the latest version\n 1 Could not reach the registry\n 2 A newer version is available")
398
- .action((opts) => checkUpdateAction(packageName, version, opts));
442
+ .action(async (opts) => {
443
+ const { checkUpdateAction } = await import("./commands/update-check.js");
444
+ return checkUpdateAction(packageName, version, opts);
445
+ });
399
446
  program
400
447
  .command("describe")
401
448
  .description("Describe the CLI contract: commands, options, exit codes, and output schemas")
402
449
  .argument("[command...]", "Optional command path, for example: md graph")
403
450
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
404
451
  .addHelpText("after", "\nExamples:\n cairn describe --format json\n cairn describe md graph --format json\n\nReports the static contract; project configuration is not applied.\n\nExit codes:\n 0 Description written to stdout\n 1 Unknown command path or invalid format")
405
- .action((commandPath, opts) => describeAction(program, commandPath, {
406
- ...opts,
407
- toolName: packageName,
408
- toolVersion: version,
409
- }));
452
+ .action(async (commandPath, opts) => {
453
+ const { describeAction } = await import("./commands/describe.js");
454
+ return describeAction(program, commandPath, {
455
+ ...opts,
456
+ toolName: packageName,
457
+ toolVersion: version,
458
+ });
459
+ });
410
460
  program
411
461
  .command("schema")
412
462
  .description("Print a published output schema, or list the available schemas")
413
463
  .argument("[id]", "Schema id, for example: agent-result")
414
464
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
415
465
  .addHelpText("after", "\nWith an id, the schema document is written regardless of --format.\nSchema ids are identifiers, not fetchable URLs.\n\nExit codes:\n 0 Schema or index written to stdout\n 1 Unknown schema id or invalid format")
416
- .action((id, opts) => schemaAction(id, opts));
466
+ .action(async (id, opts) => {
467
+ const { schemaAction } = await import("./commands/schema.js");
468
+ return schemaAction(id, opts);
469
+ });
417
470
  program
418
471
  .command("completion")
419
472
  .description("Print a shell completion script for bash, zsh, fish, or powershell")
420
473
  .argument("<shell>", "Shell: bash, zsh, fish, or powershell")
421
474
  .option("--format <fmt>", "Output format: llm, human, json", "llm")
422
475
  .addHelpText("after", "\nThe script is written to stdout regardless of --format, and is generated from the\nsame command tree `describe` walks, so it cannot drift from the real options.\n\nInstall:\n cairn completion bash >> ~/.bashrc (or a bash-completion.d file)\n cairn completion zsh > ~/.zfunc/_cairn (a directory on $fpath)\n cairn completion fish > ~/.config/fish/completions/cairn.fish\n cairn completion powershell >> $PROFILE\n\nRegenerate after upgrading; the script embeds the command tree rather than calling\nback into the CLI, so a shell never pays a process spawn per keystroke.\n\nExit codes:\n 0 Script written to stdout\n 1 Unknown shell or invalid format")
423
- .action((shell, opts) => completionAction(program, shell, {
424
- ...opts,
425
- toolName: packageName,
426
- toolVersion: version,
427
- }));
476
+ .action(async (shell, opts) => {
477
+ const { completionAction } = await import("./commands/completion.js");
478
+ return completionAction(program, shell, {
479
+ ...opts,
480
+ toolName: packageName,
481
+ toolVersion: version,
482
+ });
483
+ });
428
484
  program
429
485
  .command("serve")
430
486
  .description("Serve the workspace engine over a machine protocol")
@@ -435,7 +491,10 @@ program
435
491
  .option("--max-documents <n>", "Parsed documents held in memory before eviction")
436
492
  .option("--concurrency <n>", "Parallel lints during audit_markdown")
437
493
  .addHelpText("after", "\nSpeaks the Model Context Protocol over stdio, exposing the Markdown workspace\nengine as read-only tools. stdout carries JSON-RPC frames rather than a payload,\nso --format does not apply; diagnostics go to stderr.\n\nEvery tool is read-only and every path argument is confined to --root, resolved\nthrough symlinks. Configuration is discovered from --root, so a tool answers the\nsame as the equivalent md command in that workspace.\n\nRegister with a host:\n claude mcp add markdown -- cairn serve mcp --root docs\n\nExit codes:\n 0 The client closed the connection\n 1 Unknown protocol, unreadable root, or invalid configuration")
438
- .action((protocol, opts) => serveAction(protocol, opts));
494
+ .action(async (protocol, opts) => {
495
+ const { serveAction } = await import("./commands/serve.js");
496
+ return serveAction(protocol, opts);
497
+ });
439
498
  const jira = program
440
499
  .command("jira")
441
500
  .description("Work with Jira and Confluence content formats")
@@ -454,20 +513,32 @@ const adfConverter = (command) => adfCommon(command)
454
513
  adfConverter(adf.command("to-markdown"))
455
514
  .description("Convert an ADF document to Markdown")
456
515
  .addHelpText("after", "\nEmits no frontmatter: an ADF document carries no title, key, status, or author, so\nthere is nothing to put there.\n\nExit codes:\n 0 Converted; read diagnostics to learn what was approximated\n 1 Invocation or I/O error, or the input is not an ADF document\n 2 An error, or any approximation under --strict")
457
- .action((source, opts) => adfActionBoundary("to-markdown", opts, () => adfToMarkdownAction(source, opts)));
516
+ .action(async (source, opts) => {
517
+ const { adfActionBoundary, adfToMarkdownAction } = await import("./commands/jira.js");
518
+ return adfActionBoundary("to-markdown", opts, () => adfToMarkdownAction(source, opts));
519
+ });
458
520
  adfConverter(adf.command("from-markdown"))
459
521
  .description("Convert a Markdown document to ADF")
460
522
  .addHelpText("after", "\nThe default format already emits pure ADF JSON, so --format json wraps that document\nin the result envelope rather than changing its encoding. Frontmatter is dropped with\na finding rather than becoming body content.\n\nExit codes:\n 0 Converted; read diagnostics to learn what was approximated\n 1 Invocation or I/O error\n 2 An error, or any approximation under --strict")
461
- .action((source, opts) => adfActionBoundary("from-markdown", opts, () => adfFromMarkdownAction(source, opts)));
523
+ .action(async (source, opts) => {
524
+ const { adfActionBoundary, adfFromMarkdownAction } = await import("./commands/jira.js");
525
+ return adfActionBoundary("from-markdown", opts, () => adfFromMarkdownAction(source, opts));
526
+ });
462
527
  adfCommon(adf.command("validate"))
463
528
  .description("Check an ADF document's structure without converting it")
464
529
  .option("--strict", "Treat an unrecognized node or mark type as a blocking finding")
465
530
  .addHelpText("after", "\nChecks nesting, required content, and attribute constraints against this tool's own\ncontent model. It is not a wrapper around Atlassian's schema: a node type the model\ndoes not know reports AD100 rather than being judged.\n\nExit codes:\n 0 No structural errors\n 1 Invocation or I/O error, or the input is not an ADF document\n 2 Invalid structure, or an unknown node type under --strict")
466
- .action((source, opts) => adfActionBoundary("validate", opts, () => adfValidateAction(source, opts)));
531
+ .action(async (source, opts) => {
532
+ const { adfActionBoundary, adfValidateAction } = await import("./commands/jira.js");
533
+ return adfActionBoundary("validate", opts, () => adfValidateAction(source, opts));
534
+ });
467
535
  adfCommon(adf.command("inspect"))
468
536
  .description("List the node and mark types in an ADF document, with per-type fidelity")
469
537
  .addHelpText("after", "\nAnswers what a conversion will cost before paying it. A type this tool does not model\nis listed as unsupported rather than omitted.\n\nExit codes:\n 0 Inventory written to stdout\n 1 Invocation or I/O error, or the input is not an ADF document")
470
- .action((source, opts) => adfActionBoundary("inspect", opts, () => adfInspectAction(source, opts)));
538
+ .action(async (source, opts) => {
539
+ const { adfActionBoundary, adfInspectAction } = await import("./commands/jira.js");
540
+ return adfActionBoundary("inspect", opts, () => adfInspectAction(source, opts));
541
+ });
471
542
  const pdf = program
472
543
  .command("pdf")
473
544
  .description("Read PDF documents: text, structure, embedded files, and conversion to Markdown")
@@ -486,35 +557,56 @@ const pdfDocument = (command) => pdfCommon(command)
486
557
  pdfCommon(pdf.command("inspect"))
487
558
  .description("Report page count, metadata, tagging, and a per-page text-layer forecast")
488
559
  .addHelpText("after", "\nAnswers what a conversion will cost before paying it, and whether a document needs OCR at\nall. Each page is classified present, sparse, or absent by glyph count per square inch; an\nabsent text layer means the page is an image and `pdf text` returns nothing for it. The\ncharacter count and density are reported beside the label, so a caller who disagrees with\nthe threshold can re-classify from the evidence.\n\ndocument.tagged is the field to read first: a tagged document carries a structure tree that\nnames its own paragraphs, headings, and lists, so `pdf to-markdown` infers almost nothing.\ndocument.structured is the measured version of that claim — some producers declare tagging\nand ship an empty tree.\n\nExit codes:\n 0 Inventory written to stdout\n 1 Invocation or I/O error, or the input is not a PDF\n 2 A page could not be analyzed, leaving the inventory incomplete")
489
- .action((file, opts) => pdfActionBoundary("inspect", file, opts, () => pdfInspectAction(file, opts)));
560
+ .action(async (file, opts) => {
561
+ const { pdfActionBoundary, pdfInspectAction } = await import("./commands/pdf.js");
562
+ return pdfActionBoundary("inspect", file, opts, () => pdfInspectAction(file, opts));
563
+ });
490
564
  pdfDocument(pdf.command("text"))
491
565
  .description("Extract the text layer, page by page")
492
566
  .addHelpText("after", "\nExtracts the text a document already carries. It does not recognize text in an image: a\nscanned page has no text layer and reports AP050 rather than returning an empty string with\nno explanation. Run `pdf inspect` first to see which pages have one.\n\nPages are separated by a form feed on stdout, as pdftotext does. Under --format json they\nare a per-page array instead, and a page that could not be decoded is absent from it rather\nthan present and empty.\n\nExamples:\n cairn pdf text report.pdf --pages 1,4-6\n cairn pdf text scan.pdf --strict # fail if any page is an image\n\nExit codes:\n 0 Text written to stdout\n 1 Invocation or I/O error, or the input is not a PDF\n 2 A page could not be decoded, or a page has no text layer under --strict")
493
- .action((file, opts) => pdfActionBoundary("text", file, opts, () => pdfTextAction(file, opts)));
567
+ .action(async (file, opts) => {
568
+ const { pdfActionBoundary, pdfTextAction } = await import("./commands/pdf.js");
569
+ return pdfActionBoundary("text", file, opts, () => pdfTextAction(file, opts));
570
+ });
494
571
  pdfCommon(pdf.command("outline"))
495
572
  .description("Read the document outline (bookmarks) as a heading tree")
496
573
  .addHelpText("after", "\nReports the outline the document declares, not one inferred from its text. A document with\nno /Outlines returns an empty tree and exits 0: that is an answer, not a failure. An entry\nwhose destination does not resolve keeps its title with a null page and reports AP080,\nrather than being dropped.\n\nURLs are recorded and never followed. An entry whose scheme the parser refused carries no\nurl at all, rather than presenting a javascript: or file: URI as though it were clickable.\n\nExit codes:\n 0 Outline written to stdout, possibly empty\n 1 Invocation or I/O error, or the input is not a PDF\n 2 An outline entry could not be resolved")
497
- .action((file, opts) => pdfActionBoundary("outline", file, opts, () => pdfOutlineAction(file, opts)));
574
+ .action(async (file, opts) => {
575
+ const { pdfActionBoundary, pdfOutlineAction } = await import("./commands/pdf.js");
576
+ return pdfActionBoundary("outline", file, opts, () => pdfOutlineAction(file, opts));
577
+ });
498
578
  pdfCommon(pdf.command("validate"))
499
579
  .description("Check a PDF's structural integrity without converting it")
500
580
  .option("--strict", "Treat an unsupported construct as a blocking finding")
501
581
  .addHelpText("after", "\nReports what the parser itself can see: a damaged cross-reference table, a content stream\nit could not decode, a font it could not resolve, a page tree cycle, an unsupported filter.\n\nIt is deliberately not a PDF/A or PDF/UA conformance checker. Full conformance validation is\nveraPDF's job and is a Java program; claiming it here would be a lie. It also does not\nverify signatures or judge whether a document renders, neither of which is reachable\nwithout rasterizing. This is the same line `jira adf validate` draws when it reports AD100\nfor a node type it does not model.\n\nA cross-reference table that was damaged but successfully rebuilt reports AP101 and still\nparses, so a finding here does not mean the document is unreadable.\n\nExit codes:\n 0 No structural errors\n 1 Invocation or I/O error, or the input is not a PDF\n 2 Invalid structure, or an unsupported construct under --strict")
502
- .action((file, opts) => pdfActionBoundary("validate", file, opts, () => pdfValidateAction(file, opts)));
582
+ .action(async (file, opts) => {
583
+ const { pdfActionBoundary, pdfValidateAction } = await import("./commands/pdf.js");
584
+ return pdfActionBoundary("validate", file, opts, () => pdfValidateAction(file, opts));
585
+ });
503
586
  pdfDocument(pdf.command("to-markdown"))
504
587
  .description("Convert a PDF's content to Markdown, reporting what was inferred")
505
588
  .addHelpText("after", "\nA PDF has no paragraphs, no headings, and no lists — only positioned glyph runs. On an\nuntagged page every block boundary is inferred from geometry and font metrics, so the\nconversion is approximate by construction; a tagged page uses the structure tree instead and\nis close to exact. The path is chosen per page, and AP200 always reports which was used.\n\nRead document.tagged before trusting the structure, and run `pdf inspect` before running\nthis at all.\n\nTabular content is flattened to one paragraph per row and reported. A real table is only\nemitted for a tagged document: a geometric reconstruction gets merged and wrapped cells\nwrong and produces a confidently wrong table that cannot be told from a right one.\n\n--pages restricts which pages are emitted, not what the inference saw — the modal body font\nand repeated-header detection still run over the whole document, so a page range is a true\nsubset of the full conversion.\n\nEmits no frontmatter: a PDF's metadata is `pdf inspect`'s answer.\n\nExit codes:\n 0 Converted; read diagnostics to learn what was inferred or lost\n 1 Invocation or I/O error, or the input is not a PDF\n 2 An error, or any approximation under --strict")
506
- .action((file, opts) => pdfActionBoundary("to-markdown", file, opts, () => pdfToMarkdownAction(file, opts)));
589
+ .action(async (file, opts) => {
590
+ const { pdfActionBoundary, pdfToMarkdownAction } = await import("./commands/pdf.js");
591
+ return pdfActionBoundary("to-markdown", file, opts, () => pdfToMarkdownAction(file, opts));
592
+ });
507
593
  pdfCommon(pdf.command("attachments"))
508
594
  .description("List the files embedded in a PDF, and optionally write them out")
509
595
  .option("--extract <dir>", "Write the embedded files into this directory")
510
596
  .option("--strict", "Treat a sanitized name or an unreadable file as a blocking finding")
511
597
  .addHelpText("after", "\nEmbedded files are files carried inside the document. Without --extract this only\ninventories them — name, size, SHA-256 — which is what makes it safe to reach for and\nsafe to expose over MCP; writing is the opt-in.\n\nBinary never goes to stdout under any format. --extract is the only way bytes leave this\ncommand.\n\nA stored file name is attacker-controlled and is sanitized before it is used as a path. The\npayload reports both the raw stored name and the name actually written, so a rename is\nvisible. Extraction is planned in full before anything is written: one refused destination\nmeans no file is written at all, and a name that collides is written under a resolved name\nrather than overwriting anything.\n\nNothing embedded is ever executed or opened.\n\nExamples:\n cairn pdf attachments report.pdf\n cairn pdf attachments report.pdf --extract ./out\n\nExit codes:\n 0 Listed, and written when --extract was given\n 1 Invocation or I/O error, or the input is not a PDF\n 2 A destination was refused, an embedded file could not be decoded, or any\n name had to be sanitized under --strict")
512
- .action((file, opts) => pdfActionBoundary("attachments", file, opts, () => pdfAttachmentsAction(file, opts)));
598
+ .action(async (file, opts) => {
599
+ const { pdfActionBoundary, pdfAttachmentsAction } = await import("./commands/pdf.js");
600
+ return pdfActionBoundary("attachments", file, opts, () => pdfAttachmentsAction(file, opts));
601
+ });
513
602
  pdfCommon(pdf.command("forms"))
514
603
  .description("List AcroForm fields and their current values")
515
604
  .option("--strict", "Treat a form this cannot fully read as a blocking finding")
516
605
  .addHelpText("after", "\nReads and never writes. Filling a form is manipulation, which this toolset does not do, so\nthere is no flag that sets a value.\n\nOne field can render as several widgets across pages; they are folded into one row carrying\na count. A field's page is reported 1-based, matching every other page number here.\n\nA field marked as a password field is reported with its value and the flag. The same bytes\nare already reachable through `pdf text`, so withholding them would be theatre.\n\nAn XFA-only document reports type:xfa with no fields and AP311, never a silently empty\nlist: its values live in an XML packet this does not read.\n\nExit codes:\n 0 Reported, including a document that carries no form\n 1 Invocation or I/O error, or the input is not a PDF\n 2 Under --strict, a form this cannot fully read — an XFA form, or a field\n that resolves to no page")
517
- .action((file, opts) => pdfActionBoundary("forms", file, opts, () => pdfFormsAction(file, opts)));
606
+ .action(async (file, opts) => {
607
+ const { pdfActionBoundary, pdfFormsAction } = await import("./commands/pdf.js");
608
+ return pdfActionBoundary("forms", file, opts, () => pdfFormsAction(file, opts));
609
+ });
518
610
  const scripts = program
519
611
  .command("scripts")
520
612
  .description("Resolve and run named scripts declared in .cairn.yml")
@@ -529,17 +621,27 @@ scriptsCommon(scripts.command("run"))
529
621
  .description("Run a named script from anywhere in the tree")
530
622
  .argument("<name>", "Script name declared under scripts: in a .cairn.yml")
531
623
  .argument("[args...]", "Arguments forwarded to the script, after --")
532
- .addHelpText("after", "\nExamples:\n cairn scripts run gather-context\n cairn scripts run lint-changed -- --since main\n\nIn llm and human formats the script's streams pass through untouched and its exit\nstatus becomes this process's exit status, so a hook reads the real code. With\n--format json the streams are captured into the payload instead.\n\nRefuses to run outside a Git repository unless --root sets the boundary explicitly.\n\nExit codes:\n * llm and human: the script's own exit status, verbatim\n 0 --format json: the script exited 0\n 1 Unresolvable name, or the script could not be started\n 2 --format json: the script exited non-zero or was killed by a signal")
533
- .action((name, args, opts) => scriptsRunAction(name, args, opts));
624
+ .option("--ignore-exit-code", "Exit 0 whatever the script did; its real status stays in the payload")
625
+ .addHelpText("after", "\nExamples:\n cairn scripts run gather-context\n cairn scripts run lint-changed -- --since main\n\nIn llm and human formats the script's streams pass through untouched and its exit\nstatus becomes this process's exit status, so a hook reads the real code. With\n--format json the streams are captured into the payload instead.\n\n--ignore-exit-code discards that status and exits 0 instead, for an invocation\ninline in a skill document, where any non-zero status is read as a failure to load.\nThe script's own code is still reported as exit.status under --format json.\n\nRefuses to run outside a Git repository unless --root sets the boundary explicitly.\n\nExit codes:\n * llm and human: the script's own exit status, verbatim\n 0 --format json: the script exited 0\n 0 --ignore-exit-code: always, whatever the script did\n 1 Unresolvable name, or the script could not be started\n 2 --format json: the script exited non-zero or was killed by a signal")
626
+ .action(async (name, args, opts) => {
627
+ const { scriptsRunAction } = await import("./commands/scripts.js");
628
+ return scriptsRunAction(name, args, opts);
629
+ });
534
630
  scriptsCommon(scripts.command("which"))
535
631
  .description("Show which registry defines a script, without running it")
536
632
  .argument("<name>", "Script name")
537
633
  .addHelpText("after", "\nReports the winning .cairn.yml, the working directory the script would run in,\nand any same-named definitions it shadows.\n\nExit codes:\n 0 The name resolved\n 1 Invocation error\n 2 No script by that name")
538
- .action((name, opts) => scriptsWhichAction(name, opts));
634
+ .action(async (name, opts) => {
635
+ const { scriptsWhichAction } = await import("./commands/scripts.js");
636
+ return scriptsWhichAction(name, opts);
637
+ });
539
638
  scriptsCommon(scripts.command("list"))
540
639
  .description("List every script visible from the working directory")
541
640
  .addHelpText("after", "\nNearest definition wins, so a name declared in a nested registry hides the one above\nit. Files that could not be parsed are reported rather than skipped silently.\n\nExit codes:\n 0 Listing written to stdout\n 1 Invocation error\n 2 A consulted configuration file could not be read")
542
- .action((opts) => scriptsListAction(opts));
641
+ .action(async (opts) => {
642
+ const { scriptsListAction } = await import("./commands/scripts.js");
643
+ return scriptsListAction(opts);
644
+ });
543
645
  const usage = program
544
646
  .command("usage")
545
647
  .description("Report on Claude Code usage from its own session logs")
@@ -569,52 +671,79 @@ usageCommon(usage.command("summary"))
569
671
  .description("Headline totals: sessions, tokens, tools, and features")
570
672
  .addHelpText("after", "\nToken counts deduplicate the per-response fan-out in the source transcripts, where\none API response is written as several lines each carrying an identical copy of its\nusage. Subagent transcripts are included; --no-subagents excludes them." +
571
673
  usageExitCodes)
572
- .action((opts) => usageSummaryAction(opts));
674
+ .action(async (opts) => {
675
+ const { usageSummaryAction } = await import("./commands/usage.js");
676
+ return usageSummaryAction(opts);
677
+ });
573
678
  usageCommon(usage.command("tokens"))
574
679
  .description("Token usage rolled up by model, time, project, or session")
575
680
  .option("--by <dimension>", "model, day, week, month, project, session, provider", "model")
576
681
  .addHelpText("after", "\nCache writes report an authoritative total alongside a best-effort split by TTL,\nwhich the oldest records do not carry." +
577
682
  usageExitCodes)
578
- .action((opts) => usageTokensAction(opts));
683
+ .action(async (opts) => {
684
+ const { usageTokensAction } = await import("./commands/usage.js");
685
+ return usageTokensAction(opts);
686
+ });
579
687
  usageCommon(usage.command("tools"))
580
688
  .description("Tool calls rolled up by name, kind, server, day, or session")
581
689
  .option("--by <dimension>", "name, kind, server, day, session, provider", "name")
582
690
  .option("--kind <kind>", "Limit to builtin, mcp, agent, or skill calls")
583
691
  .addHelpText("after", "\nAn MCP tool named mcp__<server>__<tool> is split into its server and tool halves,\nso --by server and --kind mcp are how that surface is queried." +
584
692
  usageExitCodes)
585
- .action((opts) => usageToolsAction(opts));
693
+ .action(async (opts) => {
694
+ const { usageToolsAction } = await import("./commands/usage.js");
695
+ return usageToolsAction(opts);
696
+ });
586
697
  usageCommon(usage.command("sessions"))
587
698
  .description("One row per session, with its subagent transcripts folded in")
588
699
  .option("--sort <order>", "recent, tokens, tools, duration", "recent")
589
700
  .addHelpText("after", "\n--last n selects the n most recently active sessions rather than the n most recent\nfiles, so a session's subagent spend is never dropped from its own row." +
590
701
  usageExitCodes)
591
- .action((opts) => usageSessionsAction(opts));
702
+ .action(async (opts) => {
703
+ const { usageSessionsAction } = await import("./commands/usage.js");
704
+ return usageSessionsAction(opts);
705
+ });
592
706
  usageCommon(usage.command("projects"))
593
707
  .description("Usage rolled up by the directory each session ran in")
594
708
  .addHelpText("after", "\nProject identity is the working directory recorded inside the transcripts, not the\nlog directory name, whose separator substitution is not reliably invertible." +
595
709
  usageExitCodes)
596
- .action((opts) => usageProjectsAction(opts));
710
+ .action(async (opts) => {
711
+ const { usageProjectsAction } = await import("./commands/usage.js");
712
+ return usageProjectsAction(opts);
713
+ });
597
714
  usageCommon(usage.command("skills"))
598
715
  .description("Skill invocations by name")
599
716
  .addHelpText("after", "\nCounted from every surface that records one: the Skill tool, the invoked-skill\nattachments, and the slash-command form." +
600
717
  usageExitCodes)
601
- .action((opts) => usageSkillsAction(opts));
718
+ .action(async (opts) => {
719
+ const { usageSkillsAction } = await import("./commands/usage.js");
720
+ return usageSkillsAction(opts);
721
+ });
602
722
  usageCommon(usage.command("agents"))
603
723
  .description("Subagent activity by agent type, with real token cost")
604
724
  .option("--by <dimension>", "role, path", "role")
605
725
  .addHelpText("after", "\nSpawn counts come from the parent's tool calls; tokens come from the subagent\ntranscripts themselves. The parent's own tool result records only the subagent's\nfinal message and understates its spend several-fold, so it is not used.\n\n--by role groups by the reusable agent type; --by path groups by the task-specific\nidentifier, which only some providers record." +
606
726
  usageExitCodes)
607
- .action((opts) => usageAgentsAction(opts));
727
+ .action(async (opts) => {
728
+ const { usageAgentsAction } = await import("./commands/usage.js");
729
+ return usageAgentsAction(opts);
730
+ });
608
731
  usageCommon(usage.command("hooks"))
609
732
  .description("Hook executions by event and tool, with failures and latency")
610
733
  .addHelpText("after", "\nKeyed by <Event>:<Tool>. Stop hooks report through a session summary record rather\nthan a per-execution one and are counted under Stop." +
611
734
  usageExitCodes)
612
- .action((opts) => usageHooksAction(opts));
735
+ .action(async (opts) => {
736
+ const { usageHooksAction } = await import("./commands/usage.js");
737
+ return usageHooksAction(opts);
738
+ });
613
739
  usageCommon(usage.command("commands"))
614
740
  .description("Slash command usage by name")
615
741
  .addHelpText("after", "\nSlash commands are not a field in the logs; they are a marker block inside the\nuser's message text, and are extracted from it." +
616
742
  usageExitCodes)
617
- .action((opts) => usageCommandsAction(opts));
743
+ .action(async (opts) => {
744
+ const { usageCommandsAction } = await import("./commands/usage.js");
745
+ return usageCommandsAction(opts);
746
+ });
618
747
  usage
619
748
  .command("providers")
620
749
  .description("List the log sources usage can report on")
@@ -622,18 +751,27 @@ usage
622
751
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
623
752
  .option("--logs <dir>", "Test discovery against this directory")
624
753
  .addHelpText("after", "\nReports whether each provider has left anything on this machine and what its logs\ncan answer. Reports read those capabilities rather than branching on a provider\nname, so registering a second assistant is one module and one registry line.\n\nExit codes:\n 0 Listing written to stdout\n 1 Invocation error")
625
- .action((opts) => usageProvidersAction(opts));
754
+ .action(async (opts) => {
755
+ const { usageProvidersAction } = await import("./commands/usage.js");
756
+ return usageProvidersAction(opts);
757
+ });
626
758
  usageCommon(usage.command("index"))
627
759
  .description("Show, rebuild, or clear the usage store")
628
760
  .option("--rebuild", "Re-parse every transcript and rewrite its rows")
629
761
  .option("--clear", "Drop the selected providers' rows")
630
762
  .addHelpText("after", "\nThe store keys on each transcript's path, size, and modification time. Transcripts\nare append-only, so an unchanged file cannot hold a record the stored aggregate is\nmissing, and only files that grew are reopened.\n\nOne SQLite store under XDG_DATA_HOME holds every provider, so --clear is scoped by\n--provider and the size it reports is the whole file's.\n\nExit codes:\n 0 Status written, or the store was rebuilt or cleared\n 1 Invocation error")
631
- .action((opts) => usageIndexAction(opts));
763
+ .action(async (opts) => {
764
+ const { usageIndexAction } = await import("./commands/usage.js");
765
+ return usageIndexAction(opts);
766
+ });
632
767
  usageCommon(usage.command("import"))
633
768
  .description("Import transcripts into the usage store")
634
769
  .option("--rebuild", "Re-parse every transcript, not only the ones that changed")
635
770
  .addHelpText("after", "\nReports populate the store on first use, so this is never required. It exists to do\nthat work deliberately: to warm a cold store before a timed report, to run on a\nschedule, and to see the import counters without a report wrapped around them.\n\nThe store keeps two grains. Day buckets answer every report this tool offers; the\nevent rows answer what a day bucket cannot, and are there for anything querying the\nSQLite file directly.\n\nExit codes:\n 0 Import completed\n 1 Invocation error, or no logs found\n 2 --strict was given and a transcript could not be fully read")
636
- .action((opts) => usageImportAction(opts));
771
+ .action(async (opts) => {
772
+ const { usageImportAction } = await import("./commands/usage.js");
773
+ return usageImportAction(opts);
774
+ });
637
775
  usage
638
776
  .command("migrate")
639
777
  .description("Apply pending usage store migrations")
@@ -641,7 +779,10 @@ usage
641
779
  .option("--envelope", "Wrap --format json output in the versioned result envelope")
642
780
  .option("--check", "Report what is pending without writing")
643
781
  .addHelpText("after", "\nEvery command that opens the store migrates it, so this is needed only to migrate\ndeliberately, or with --check to see what is pending first.\n\nThe store is migrated rather than discarded. Once transcripts have been archived and\npruned it may be the only record of that usage left, so a version bump carries the\ndata forward instead of throwing it away. A store written by a newer cairn is\nrefused rather than guessed at.\n\nExit codes:\n 0 Store is current, or was migrated\n 1 Invocation error, or the store is newer than this build understands")
644
- .action((opts) => usageMigrateAction(opts));
782
+ .action(async (opts) => {
783
+ const { usageMigrateAction } = await import("./commands/usage.js");
784
+ return usageMigrateAction(opts);
785
+ });
645
786
  const archive = program
646
787
  .command("archive")
647
788
  .description("Archive plans, artifacts, and logs into long-term compressed storage")
@@ -661,11 +802,17 @@ archiveCommon(archive.command("run"))
661
802
  .option("-v, --verbose", "Print one line per artifact to stderr")
662
803
  .option("--no-progress", "Suppress the progress line")
663
804
  .addHelpText("after", "\nplans and artifacts are archived by default; transcripts and logs are opt-in because\nthey are three orders of magnitude larger.\n\nIncremental twice over: a file whose size and modification time already match the index\nis never opened, and a file whose content is already stored is never written again, so a\nsecond run over an unchanged corpus costs one stat per file.\n\nA file that changes gets a new row against a new blob, so the archive keeps every version\nit ever saw.\n\nProgress:\n A run over a full corpus is tens of thousands of files and takes minutes, so it draws a\n progress line on stderr. That line rewrites itself in place, so it appears only when\n stderr is a terminal, --format is not json, and CI is unset; --no-progress suppresses it.\n\n -v prints one durable line per artifact instead, giving its disposition (stored,\n duplicate, unchanged, skipped), size, hash, and path. It is not gated on a terminal,\n so `cairn archive run -v 2> archive.log` is the way to keep a record of a long run.\n\nExit codes:\n 0 Run completed\n 1 Invocation error, or no logs found")
664
- .action((opts) => archiveRunAction(opts));
805
+ .action(async (opts) => {
806
+ const { archiveRunAction } = await import("./commands/archive.js");
807
+ return archiveRunAction(opts);
808
+ });
665
809
  archiveCommon(archive.command("status"))
666
810
  .description("Report what the archive holds")
667
811
  .addHelpText("after", "\nExit codes:\n 0 Status written\n 1 Invocation error")
668
- .action((opts) => archiveStatusAction(opts));
812
+ .action(async (opts) => {
813
+ const { archiveStatusAction } = await import("./commands/archive.js");
814
+ return archiveStatusAction(opts);
815
+ });
669
816
  archiveCommon(archive.command("list"))
670
817
  .description("List archived artifacts")
671
818
  .option("--provider <name>", "Limit to one log source, or all", "all")
@@ -673,28 +820,43 @@ archiveCommon(archive.command("list"))
673
820
  .option("--since <day>", "Only artifacts last seen on or after this ISO day")
674
821
  .option("--top <n>", "Rows to show; 0 for all", "20")
675
822
  .addHelpText("after", "\nOne row per archived path, newest first. A path the archive holds several versions of is\nlisted once, with the count.\n\nExit codes:\n 0 Listing written\n 1 Invocation error")
676
- .action((opts) => archiveListAction(opts));
823
+ .action(async (opts) => {
824
+ const { archiveListAction } = await import("./commands/archive.js");
825
+ return archiveListAction(opts);
826
+ });
677
827
  archiveCommon(archive.command("extract"))
678
828
  .argument("<target>", "Original path, or a sha256 prefix")
679
829
  .description("Write an archived file back out")
680
830
  .option("--out <dir>", "Directory to write into", ".")
681
831
  .addHelpText("after", "\nA path resolves to its newest version; name a hash to reach an older one. The content is\nre-hashed on the way out, so an archive whose index and bytes disagree reports that rather\nthan handing back the wrong file.\n\nExit codes:\n 0 File written\n 1 Invocation error, or nothing matched")
682
- .action((target, opts) => archiveExtractAction(target, opts));
832
+ .action(async (target, opts) => {
833
+ const { archiveExtractAction } = await import("./commands/archive.js");
834
+ return archiveExtractAction(target, opts);
835
+ });
683
836
  archiveCommon(archive.command("verify"))
684
837
  .description("Check the archive against its index")
685
838
  .option("--deep", "Also decompress every segment and re-hash each blob")
686
839
  .addHelpText("after", "\nThe default pass hashes each segment file, catching truncation and corruption for the cost\nof reading it. --deep additionally re-hashes every member, which catches an index whose\noffsets no longer point where it claims.\n\nExit codes:\n 0 Archive matches its index\n 1 Invocation error\n 2 The archive and its index disagree")
687
- .action((opts) => archiveVerifyAction(opts));
840
+ .action(async (opts) => {
841
+ const { archiveVerifyAction } = await import("./commands/archive.js");
842
+ return archiveVerifyAction(opts);
843
+ });
688
844
  archiveCommon(archive.command("migrate"))
689
845
  .description("Apply pending archive index migrations")
690
846
  .option("--check", "Report what is pending without writing")
691
847
  .addHelpText("after", "\nThe index is migrated rather than discarded: it is the only map from an original path to\nthe segment holding that file's bytes. An index written by a newer cairn is refused rather\nthan guessed at.\n\nExit codes:\n 0 Index is current, or was migrated\n 1 Invocation error, or the index is newer than this build understands")
692
- .action((opts) => archiveMigrateAction(opts));
848
+ .action(async (opts) => {
849
+ const { archiveMigrateAction } = await import("./commands/archive.js");
850
+ return archiveMigrateAction(opts);
851
+ });
693
852
  // Internal: refreshes the cached latest version. Spawned detached by the notifier.
694
853
  program
695
854
  .command(REFRESH_COMMAND, { hidden: true })
696
855
  .description("Internal: refresh the cached latest-version check")
697
- .action(() => refreshUpdateCacheAction(packageName));
856
+ .action(async () => {
857
+ const { refreshUpdateCacheAction } = await import("./commands/update-check.js");
858
+ return refreshUpdateCacheAction(packageName);
859
+ });
698
860
  const md = program
699
861
  .command("md")
700
862
  .description("Agent-agnostic Markdown validation and analysis commands")
@@ -724,14 +886,17 @@ common(md.command("lint"))
724
886
  .option("--include <glob>", "Markdown include glob (repeatable)", collect)
725
887
  .option("--exclude <glob>", "Markdown exclude glob (repeatable)", collect)
726
888
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json\n\nExit codes:\n 0 All checks pass\n 2 One or more issues found")
727
- .action((files, opts) => lintAction(files, commandOptions("lint", {
728
- style: projectConfig.checks.markdownlint,
729
- mermaid: projectConfig.checks.mermaid,
730
- katex: projectConfig.checks.katex,
731
- references: projectConfig.checks.references,
732
- include: projectConfig.files.include,
733
- exclude: projectConfig.files.exclude,
734
- }, opts)));
889
+ .action(async (files, opts) => {
890
+ const { lintAction } = await import("./commands/lint.js");
891
+ return lintAction(files, commandOptions("lint", {
892
+ style: projectConfig.checks.markdownlint,
893
+ mermaid: projectConfig.checks.mermaid,
894
+ katex: projectConfig.checks.katex,
895
+ references: projectConfig.checks.references,
896
+ include: projectConfig.files.include,
897
+ exclude: projectConfig.files.exclude,
898
+ }, opts));
899
+ });
735
900
  common(md.command("lint-dir"))
736
901
  .description("Run all checks on all markdown files in a directory")
737
902
  .argument("[directory]", "Path to the directory to scan (default: workspace root)")
@@ -750,16 +915,19 @@ common(md.command("lint-dir"))
750
915
  .option("--exclude <glob>", "Markdown exclude glob (repeatable)", collect)
751
916
  .option("--changed-since <revision>", "Only files changed since a Git revision")
752
917
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json\n\nExit codes:\n 0 All files pass all checks\n 2 One or more issues found in any file")
753
- .action((directory, opts) => lintDirAction(directory ?? projectConfig.root, commandOptions("lint-dir", {
754
- style: projectConfig.checks.markdownlint,
755
- summary: false,
756
- concurrency: String(defaultLintConcurrency()),
757
- include: projectConfig.files.include,
758
- exclude: projectConfig.files.exclude,
759
- mermaid: projectConfig.checks.mermaid,
760
- katex: projectConfig.checks.katex,
761
- references: projectConfig.checks.references,
762
- }, opts)));
918
+ .action(async (directory, opts) => {
919
+ const { lintDirAction } = await import("./commands/lint-dir.js");
920
+ return lintDirAction(directory ?? projectConfig.root, commandOptions("lint-dir", {
921
+ style: projectConfig.checks.markdownlint,
922
+ summary: false,
923
+ concurrency: String(defaultLintConcurrency()),
924
+ include: projectConfig.files.include,
925
+ exclude: projectConfig.files.exclude,
926
+ mermaid: projectConfig.checks.mermaid,
927
+ katex: projectConfig.checks.katex,
928
+ references: projectConfig.checks.references,
929
+ }, opts));
930
+ });
763
931
  common(md.command("refs"))
764
932
  .description("List all references from a markdown file and check if targets exist")
765
933
  .argument("<file>", "Path to the markdown file to inspect")
@@ -770,7 +938,10 @@ common(md.command("refs"))
770
938
  .option("-i, --images", "Include image references")
771
939
  .option("--no-images", "Exclude image references")
772
940
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json\n\nExit codes:\n 0 All referenced targets exist\n 2 One or more targets missing")
773
- .action((file, opts) => refsAction(file, commandOptions("refs", { external: false, anchors: false, images: false }, opts)));
941
+ .action(async (file, opts) => {
942
+ const { refsAction } = await import("./commands/refs.js");
943
+ return refsAction(file, commandOptions("refs", { external: false, anchors: false, images: false }, opts));
944
+ });
774
945
  common(md.command("refs-to"))
775
946
  .description("Find all markdown files that reference a given file")
776
947
  .argument("<file>", "Path to the file to find references to")
@@ -778,19 +949,28 @@ common(md.command("refs-to"))
778
949
  .option("--include <glob>", "Markdown include glob (repeatable)", collect)
779
950
  .option("--exclude <glob>", "Markdown exclude glob (repeatable)", collect)
780
951
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json")
781
- .action((file, directory, opts) => refsToAction(file, directory ?? projectConfig.root, commandOptions("refs-to", { include: projectConfig.files.include, exclude: projectConfig.files.exclude }, opts)));
952
+ .action(async (file, directory, opts) => {
953
+ const { refsToAction } = await import("./commands/refs-to.js");
954
+ return refsToAction(file, directory ?? projectConfig.root, commandOptions("refs-to", { include: projectConfig.files.include, exclude: projectConfig.files.exclude }, opts));
955
+ });
782
956
  common(md.command("headers"))
783
957
  .description("Extract headings from a markdown file with line numbers")
784
958
  .argument("<file>", "Path to the markdown file")
785
959
  .option("--max-depth <n>", "Maximum heading depth to include (1-6)")
786
960
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json")
787
- .action((file, opts) => headersAction(file, commandOptions("headers", { maxDepth: "6" }, opts)));
961
+ .action(async (file, opts) => {
962
+ const { headersAction } = await import("./commands/headers.js");
963
+ return headersAction(file, commandOptions("headers", { maxDepth: "6" }, opts));
964
+ });
788
965
  common(md.command("outline"))
789
966
  .description("Show headings in an indented outline format")
790
967
  .argument("<file>", "Path to the markdown file")
791
968
  .option("--max-depth <n>", "Maximum heading depth to include (1-6)")
792
969
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json")
793
- .action((file, opts) => outlineAction(file, commandOptions("outline", { maxDepth: "6" }, opts)));
970
+ .action(async (file, opts) => {
971
+ const { outlineAction } = await import("./commands/outline.js");
972
+ return outlineAction(file, commandOptions("outline", { maxDepth: "6" }, opts));
973
+ });
794
974
  common(md.command("toc"))
795
975
  .description("Generate a markdown table of contents from headings")
796
976
  .argument("<file>", "Path to the markdown file")
@@ -802,7 +982,10 @@ common(md.command("toc"))
802
982
  .option("--write", "Update the content between TOC markers")
803
983
  .option("--dry-run", "Print the proposed marker block without writing")
804
984
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json")
805
- .action((file, opts) => tocAction(file, commandOptions("toc", { maxDepth: "6", minDepth: "1", ordered: false, check: false, write: false, dryRun: false }, opts)));
985
+ .action(async (file, opts) => {
986
+ const { tocAction } = await import("./commands/toc.js");
987
+ return tocAction(file, commandOptions("toc", { maxDepth: "6", minDepth: "1", ordered: false, check: false, write: false, dryRun: false }, opts));
988
+ });
806
989
  common(md.command("graph"))
807
990
  .description("Analyze the workspace Markdown document graph")
808
991
  .argument("[directory]", "Directory to scan (default: workspace root)")
@@ -813,14 +996,17 @@ common(md.command("graph"))
813
996
  .option("--include <glob>", "Markdown include glob (repeatable)", collect)
814
997
  .option("--exclude <glob>", "Markdown exclude glob (repeatable)", collect)
815
998
  .addHelpText("after", "\n--focus narrows the report and the mermaid/dot diagrams to the documents within\n--depth undirected hops, so backlinks are included. The graph is analyzed in full\nfirst, so inbound/outbound counts, components, and cycles remain whole-workspace\nfacts rather than artifacts of the narrowing.\n\nExit codes:\n 0 No broken or unreachable documents\n 2 Broken or unreachable documents found")
816
- .action((directory, opts) => graphAction(directory ?? projectConfig.root, commandOptions("graph", {
817
- output: "report",
818
- depth: "1",
819
- focus: [],
820
- entry: projectConfig.files.entryPoints,
821
- include: projectConfig.files.include,
822
- exclude: projectConfig.files.exclude,
823
- }, opts)));
999
+ .action(async (directory, opts) => {
1000
+ const { graphAction } = await import("./commands/graph.js");
1001
+ return graphAction(directory ?? projectConfig.root, commandOptions("graph", {
1002
+ output: "report",
1003
+ depth: "1",
1004
+ focus: [],
1005
+ entry: projectConfig.files.entryPoints,
1006
+ include: projectConfig.files.include,
1007
+ exclude: projectConfig.files.exclude,
1008
+ }, opts));
1009
+ });
824
1010
  common(md.command("validate-frontmatter"))
825
1011
  .description("Validate Markdown frontmatter with schema and workspace rules")
826
1012
  .argument("<paths...>", "Markdown files, directories, or globs")
@@ -829,11 +1015,14 @@ common(md.command("validate-frontmatter"))
829
1015
  .option("--exclude <glob>", "Markdown exclude glob (repeatable)", collect)
830
1016
  .option("--changed-since <revision>", "Only files changed since a Git revision")
831
1017
  .addHelpText("after", "\nExit codes:\n 0 Frontmatter is valid\n 1 Configuration or schema error\n 2 Validation findings")
832
- .action((target, opts) => validateFrontmatterAction(target, commandOptions("validate-frontmatter", {
833
- schema: projectConfig.frontmatter.schema,
834
- include: projectConfig.files.include,
835
- exclude: projectConfig.files.exclude,
836
- }, opts)));
1018
+ .action(async (target, opts) => {
1019
+ const { validateFrontmatterAction } = await import("./commands/validate-frontmatter.js");
1020
+ return validateFrontmatterAction(target, commandOptions("validate-frontmatter", {
1021
+ schema: projectConfig.frontmatter.schema,
1022
+ include: projectConfig.files.include,
1023
+ exclude: projectConfig.files.exclude,
1024
+ }, opts));
1025
+ });
837
1026
  common(md.command("audit"))
838
1027
  .description("Run composable checks across a Markdown workspace")
839
1028
  .argument("[directory]", "Directory to scan (default: workspace root)")
@@ -867,32 +1056,38 @@ common(md.command("audit"))
867
1056
  .option("--include <glob>", "Markdown include glob (repeatable)", collect)
868
1057
  .option("--exclude <glob>", "Markdown exclude glob (repeatable)", collect)
869
1058
  .addHelpText("after", "\nA baseline suppresses findings it already records, so only regressions fail. Entries\nare keyed on checker, workspace-relative path, and message — not line number — so\nediting prose above a known finding does not resurface it. Recording is explicit:\n--write-baseline writes the file and exits 0, and the two flags cannot be combined.\n\nExit codes:\n 0 Audit passed, or a baseline was written\n 1 Operational error\n 2 Actionable findings")
870
- .action((directory, opts) => auditAction(directory ?? projectConfig.root, commandOptions("audit", {
871
- summary: false,
872
- external: projectConfig.checks.external,
873
- frontmatter: projectConfig.checks.frontmatter,
874
- graph: projectConfig.checks.graph,
875
- toc: projectConfig.checks.toc,
876
- snippets: projectConfig.checks.snippets,
877
- style: projectConfig.checks.markdownlint,
878
- mermaid: projectConfig.checks.mermaid,
879
- katex: projectConfig.checks.katex,
880
- references: projectConfig.checks.references,
881
- concurrency: String(defaultLintConcurrency()),
882
- timeout: "5000",
883
- retry: "1",
884
- entry: projectConfig.files.entryPoints,
885
- include: projectConfig.files.include,
886
- exclude: projectConfig.files.exclude,
887
- maxDepth: String(projectConfig.commands.toc?.maxDepth ?? "6"),
888
- minDepth: String(projectConfig.commands.toc?.minDepth ?? "1"),
889
- ordered: Boolean(projectConfig.commands.toc?.ordered ?? false),
890
- }, opts)));
1059
+ .action(async (directory, opts) => {
1060
+ const { auditAction } = await import("./commands/audit.js");
1061
+ return auditAction(directory ?? projectConfig.root, commandOptions("audit", {
1062
+ summary: false,
1063
+ external: projectConfig.checks.external,
1064
+ frontmatter: projectConfig.checks.frontmatter,
1065
+ graph: projectConfig.checks.graph,
1066
+ toc: projectConfig.checks.toc,
1067
+ snippets: projectConfig.checks.snippets,
1068
+ style: projectConfig.checks.markdownlint,
1069
+ mermaid: projectConfig.checks.mermaid,
1070
+ katex: projectConfig.checks.katex,
1071
+ references: projectConfig.checks.references,
1072
+ concurrency: String(defaultLintConcurrency()),
1073
+ timeout: "5000",
1074
+ retry: "1",
1075
+ entry: projectConfig.files.entryPoints,
1076
+ include: projectConfig.files.include,
1077
+ exclude: projectConfig.files.exclude,
1078
+ maxDepth: String(projectConfig.commands.toc?.maxDepth ?? "6"),
1079
+ minDepth: String(projectConfig.commands.toc?.minDepth ?? "1"),
1080
+ ordered: Boolean(projectConfig.commands.toc?.ordered ?? false),
1081
+ }, opts));
1082
+ });
891
1083
  common(md.command("stats"))
892
1084
  .description("Show document statistics (words, headings, links, code blocks)")
893
1085
  .argument("<file>", "Path to the markdown file")
894
1086
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json")
895
- .action((file, opts) => statsAction(file, commandOptions("stats", {}, opts)));
1087
+ .action(async (file, opts) => {
1088
+ const { statsAction } = await import("./commands/stats.js");
1089
+ return statsAction(file, commandOptions("stats", {}, opts));
1090
+ });
896
1091
  common(md.command("code-blocks"))
897
1092
  .description("List fenced code blocks with language and line ranges")
898
1093
  .argument("<file>", "Path to the markdown file")
@@ -900,12 +1095,18 @@ common(md.command("code-blocks"))
900
1095
  .option("--content", "Include code block content in output")
901
1096
  .option("--no-content", "Exclude code block content from output")
902
1097
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json")
903
- .action((file, opts) => codeBlocksAction(file, commandOptions("code-blocks", { content: false }, opts)));
1098
+ .action(async (file, opts) => {
1099
+ const { codeBlocksAction } = await import("./commands/code-blocks.js");
1100
+ return codeBlocksAction(file, commandOptions("code-blocks", { content: false }, opts));
1101
+ });
904
1102
  common(md.command("structure"))
905
1103
  .description("Show document structure skeleton (headings, code blocks, lists, math)")
906
1104
  .argument("<file>", "Path to the markdown file")
907
1105
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json")
908
- .action((file, opts) => structureAction(file, commandOptions("structure", {}, opts)));
1106
+ .action(async (file, opts) => {
1107
+ const { structureAction } = await import("./commands/structure.js");
1108
+ return structureAction(file, commandOptions("structure", {}, opts));
1109
+ });
909
1110
  common(md.command("links"))
910
1111
  .description("List all links with context, grouped by type")
911
1112
  .argument("<file>", "Path to the markdown file")
@@ -913,7 +1114,10 @@ common(md.command("links"))
913
1114
  .option("--no-broken-only", "Include valid links")
914
1115
  .option("--type <type>", "Filter by type: internal, external, image, anchor")
915
1116
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json\n\nExit codes:\n 0 All link targets exist (or not checked)\n 2 One or more broken links found")
916
- .action((file, opts) => linksAction(file, commandOptions("links", { brokenOnly: false }, opts)));
1117
+ .action(async (file, opts) => {
1118
+ const { linksAction } = await import("./commands/links.js");
1119
+ return linksAction(file, commandOptions("links", { brokenOnly: false }, opts));
1120
+ });
917
1121
  common(md.command("section"))
918
1122
  .description("Extract content of a section by heading text or slug")
919
1123
  .argument("<file>", "Path to the markdown file")
@@ -925,7 +1129,10 @@ common(md.command("section"))
925
1129
  .option("--raw", "Output raw markdown only (no metadata)")
926
1130
  .option("--no-raw", "Include section metadata")
927
1131
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json\n\nExit codes:\n 0 Section found and extracted\n 1 File not found or heading not found")
928
- .action((file, heading, opts) => sectionAction(file, heading, commandOptions("section", { includeHeading: true, children: true, raw: false }, opts)));
1132
+ .action(async (file, heading, opts) => {
1133
+ const { sectionAction } = await import("./commands/section.js");
1134
+ return sectionAction(file, heading, commandOptions("section", { includeHeading: true, children: true, raw: false }, opts));
1135
+ });
929
1136
  common(md.command("context"))
930
1137
  .description("Assemble a reproducible context pack from the workspace graph")
931
1138
  .argument("[seeds...]", "Markdown files, directories, or globs to start from")
@@ -948,16 +1155,19 @@ common(md.command("context"))
948
1155
  "Exit codes:\n" +
949
1156
  " 0 Pack written to stdout, whether or not it was truncated\n" +
950
1157
  " 1 No seeds given, or a --section heading matched nothing")
951
- .action((seeds, opts) => contextAction(seeds, commandOptions("context", {
952
- depth: "1",
953
- section: [],
954
- budget: "0",
955
- backlinks: false,
956
- children: true,
957
- frontmatter: false,
958
- include: projectConfig.files.include,
959
- exclude: projectConfig.files.exclude,
960
- }, opts)));
1158
+ .action(async (seeds, opts) => {
1159
+ const { contextAction } = await import("./commands/context.js");
1160
+ return contextAction(seeds, commandOptions("context", {
1161
+ depth: "1",
1162
+ section: [],
1163
+ budget: "0",
1164
+ backlinks: false,
1165
+ children: true,
1166
+ frontmatter: false,
1167
+ include: projectConfig.files.include,
1168
+ exclude: projectConfig.files.exclude,
1169
+ }, opts));
1170
+ });
961
1171
  common(md.command("diff"))
962
1172
  .description("Summarize Markdown changes by structure rather than by text")
963
1173
  .argument("[a]", "First file, or the directory to scan with --since")
@@ -978,17 +1188,23 @@ common(md.command("diff"))
978
1188
  "Exit codes:\n" +
979
1189
  " 0 Report written to stdout, whether or not anything changed\n" +
980
1190
  " 1 Bad invocation, a missing file, or an unreadable revision")
981
- .action((a, b, opts) => diffAction(a, b, commandOptions("diff", {
982
- summary: false,
983
- include: projectConfig.files.include,
984
- exclude: projectConfig.files.exclude,
985
- }, opts)));
1191
+ .action(async (a, b, opts) => {
1192
+ const { diffAction } = await import("./commands/diff.js");
1193
+ return diffAction(a, b, commandOptions("diff", {
1194
+ summary: false,
1195
+ include: projectConfig.files.include,
1196
+ exclude: projectConfig.files.exclude,
1197
+ }, opts));
1198
+ });
986
1199
  common(md.command("frontmatter"))
987
1200
  .description("Parse and display YAML frontmatter from a markdown file")
988
1201
  .argument("<file>", "Path to the markdown file")
989
1202
  .option("--key <key>", "Extract a specific key (dot notation for nested keys)")
990
1203
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json\n\nExit codes:\n 0 Frontmatter found (or no frontmatter)\n 1 File not found or key not found")
991
- .action((file, opts) => frontmatterAction(file, commandOptions("frontmatter", {}, opts)));
1204
+ .action(async (file, opts) => {
1205
+ const { frontmatterAction } = await import("./commands/frontmatter.js");
1206
+ return frontmatterAction(file, commandOptions("frontmatter", {}, opts));
1207
+ });
992
1208
  common(md.command("tasks"))
993
1209
  .description("Extract GFM task list items with completion status")
994
1210
  .argument("<file>", "Path to the markdown file")
@@ -996,7 +1212,10 @@ common(md.command("tasks"))
996
1212
  .option("--summary", "Show only summary counts")
997
1213
  .option("--no-summary", "Show individual tasks")
998
1214
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json")
999
- .action((file, opts) => tasksAction(file, commandOptions("tasks", { summary: false }, opts)));
1215
+ .action(async (file, opts) => {
1216
+ const { tasksAction } = await import("./commands/tasks.js");
1217
+ return tasksAction(file, commandOptions("tasks", { summary: false }, opts));
1218
+ });
1000
1219
  common(md.command("tables"))
1001
1220
  .description("List or extract GFM tables with location and dimensions")
1002
1221
  .argument("<file>", "Path to the markdown file")
@@ -1004,7 +1223,10 @@ common(md.command("tables"))
1004
1223
  .option("--no-content", "Exclude table content from output")
1005
1224
  .option("--index <n>", "Extract only the nth table (1-based)")
1006
1225
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json")
1007
- .action((file, opts) => tablesAction(file, commandOptions("tables", { content: false }, opts)));
1226
+ .action(async (file, opts) => {
1227
+ const { tablesAction } = await import("./commands/tables.js");
1228
+ return tablesAction(file, commandOptions("tables", { content: false }, opts));
1229
+ });
1008
1230
  common(md.command("check-urls"))
1009
1231
  .description("Validate external URLs across Markdown inputs")
1010
1232
  .argument("<inputs...>", "Markdown files, directories, globs, or -")
@@ -1026,21 +1248,24 @@ common(md.command("check-urls"))
1026
1248
  .option("--report-redirects", "Report redirects and final destinations")
1027
1249
  .option("--no-report-redirects", "Do not report redirects")
1028
1250
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json\n\nExit codes:\n 0 All URLs reachable (or no external URLs)\n 2 One or more URLs are broken")
1029
- .action((file, opts) => checkUrlsAction(file, commandOptions("check-urls", {
1030
- timeout: "5000",
1031
- concurrency: "5",
1032
- retry: "1",
1033
- includeOk: false,
1034
- include: projectConfig.files.include,
1035
- exclude: projectConfig.files.exclude,
1036
- ignore: projectConfig.urls.ignore,
1037
- ignoreDomain: projectConfig.urls.ignoreDomains,
1038
- allowedStatus: projectConfig.urls.allowedStatuses,
1039
- cache: projectConfig.urls.cache,
1040
- cacheTtl: String(projectConfig.urls.cacheTtl),
1041
- headFallbackStatus: projectConfig.urls.headFallbackStatuses,
1042
- reportRedirects: projectConfig.urls.reportRedirects,
1043
- }, opts)));
1251
+ .action(async (file, opts) => {
1252
+ const { checkUrlsAction } = await import("./commands/check-urls.js");
1253
+ return checkUrlsAction(file, commandOptions("check-urls", {
1254
+ timeout: "5000",
1255
+ concurrency: "5",
1256
+ retry: "1",
1257
+ includeOk: false,
1258
+ include: projectConfig.files.include,
1259
+ exclude: projectConfig.files.exclude,
1260
+ ignore: projectConfig.urls.ignore,
1261
+ ignoreDomain: projectConfig.urls.ignoreDomains,
1262
+ allowedStatus: projectConfig.urls.allowedStatuses,
1263
+ cache: projectConfig.urls.cache,
1264
+ cacheTtl: String(projectConfig.urls.cacheTtl),
1265
+ headFallbackStatus: projectConfig.urls.headFallbackStatuses,
1266
+ reportRedirects: projectConfig.urls.reportRedirects,
1267
+ }, opts));
1268
+ });
1044
1269
  common(md.command("orphans"))
1045
1270
  .description("Find markdown files not referenced by any other markdown file")
1046
1271
  .argument("[directory]", "Directory to scan (default: workspace root)")
@@ -1049,12 +1274,15 @@ common(md.command("orphans"))
1049
1274
  .option("--ignore <glob>", "Glob pattern to exclude (repeatable)", collect, [])
1050
1275
  .option("--entry <file>", "Entry-point file not considered orphan (repeatable)", collect, [])
1051
1276
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json\n\nExit codes:\n 0 No orphans found\n 2 One or more orphans found")
1052
- .action((directory, opts) => orphansAction(directory ?? projectConfig.root, commandOptions("orphans", {
1053
- ignore: [],
1054
- include: projectConfig.files.include,
1055
- exclude: projectConfig.files.exclude,
1056
- entry: projectConfig.files.entryPoints,
1057
- }, opts)));
1277
+ .action(async (directory, opts) => {
1278
+ const { orphansAction } = await import("./commands/orphans.js");
1279
+ return orphansAction(directory ?? projectConfig.root, commandOptions("orphans", {
1280
+ ignore: [],
1281
+ include: projectConfig.files.include,
1282
+ exclude: projectConfig.files.exclude,
1283
+ entry: projectConfig.files.entryPoints,
1284
+ }, opts));
1285
+ });
1058
1286
  common(md.command("query"))
1059
1287
  .description("Run a focused query across the Markdown workspace")
1060
1288
  .argument("<kind>", "Query kind: links-to, duplicates, unused-assets, code-blocks, tasks, missing-h1, frontmatter-keys")
@@ -1085,20 +1313,23 @@ common(md.command("query"))
1085
1313
  " md query links --where links-to:docs/api.md --select file,line\n" +
1086
1314
  " md query tasks --where status=pending --group-by frontmatter.owner\n\n" +
1087
1315
  "An unknown field, predicate, or operator exits 1 rather than matching nothing.")
1088
- .action((kind, directory, opts) => queryAction(kind, directory ?? projectConfig.root, commandOptions("query", {
1089
- include: projectConfig.files.include,
1090
- exclude: projectConfig.files.exclude,
1091
- field: "title",
1092
- content: false,
1093
- status: "all",
1094
- summary: false,
1095
- assetExtension: projectConfig.assets.extensions,
1096
- // Predicates are per-question by nature, so they are deliberately not
1097
- // configurable: a checked-in `commands.query.where` would silently
1098
- // filter every query anyone ran in the workspace.
1099
- where: [],
1100
- select: [],
1101
- }, opts)));
1316
+ .action(async (kind, directory, opts) => {
1317
+ const { queryAction } = await import("./commands/query.js");
1318
+ return queryAction(kind, directory ?? projectConfig.root, commandOptions("query", {
1319
+ include: projectConfig.files.include,
1320
+ exclude: projectConfig.files.exclude,
1321
+ field: "title",
1322
+ content: false,
1323
+ status: "all",
1324
+ summary: false,
1325
+ assetExtension: projectConfig.assets.extensions,
1326
+ // Predicates are per-question by nature, so they are deliberately not
1327
+ // configurable: a checked-in `commands.query.where` would silently
1328
+ // filter every query anyone ran in the workspace.
1329
+ where: [],
1330
+ select: [],
1331
+ }, opts));
1332
+ });
1102
1333
  common(md.command("index"))
1103
1334
  .description("Inspect or manage the persistent workspace index")
1104
1335
  .argument("<action>", "Index action: status, build, clear")
@@ -1106,7 +1337,10 @@ common(md.command("index"))
1106
1337
  .option("--include <glob>", "Markdown include glob (repeatable)", collect)
1107
1338
  .option("--exclude <glob>", "Markdown exclude glob (repeatable)", collect)
1108
1339
  .addHelpText("after", "\nActions:\n status Inspect cache coverage\n build Force a rebuild\n clear Clear this workspace cache")
1109
- .action((action, directory, opts) => indexAction(action, directory ?? projectConfig.root, commandOptions("index", { include: projectConfig.files.include, exclude: projectConfig.files.exclude }, opts)));
1340
+ .action(async (action, directory, opts) => {
1341
+ const { indexAction } = await import("./commands/index.js");
1342
+ return indexAction(action, directory ?? projectConfig.root, commandOptions("index", { include: projectConfig.files.include, exclude: projectConfig.files.exclude }, opts));
1343
+ });
1110
1344
  common(md.command("fix"))
1111
1345
  .description("Plan and apply deterministic Markdown fixes")
1112
1346
  .argument("<inputs...>", "Markdown files, directories, or globs")
@@ -1126,16 +1360,19 @@ common(md.command("fix"))
1126
1360
  "Exit codes:\n" +
1127
1361
  " 0 No pending fixes, or --write/--dry-run completed\n" +
1128
1362
  " 2 --check found pending fixes, or any mode found a conflict")
1129
- .action((inputs, opts) => fixAction(inputs, commandOptions("fix", {
1130
- rule: [],
1131
- include: projectConfig.files.include,
1132
- exclude: projectConfig.files.exclude,
1133
- // Config may supply these as numbers; the fixer parses strings, so
1134
- // coerce here exactly as `md audit` does.
1135
- maxDepth: String(projectConfig.commands.toc?.maxDepth ?? "6"),
1136
- minDepth: String(projectConfig.commands.toc?.minDepth ?? "1"),
1137
- ordered: Boolean(projectConfig.commands.toc?.ordered ?? false),
1138
- }, opts)));
1363
+ .action(async (inputs, opts) => {
1364
+ const { fixAction } = await import("./commands/fix.js");
1365
+ return fixAction(inputs, commandOptions("fix", {
1366
+ rule: [],
1367
+ include: projectConfig.files.include,
1368
+ exclude: projectConfig.files.exclude,
1369
+ // Config may supply these as numbers; the fixer parses strings, so
1370
+ // coerce here exactly as `md audit` does.
1371
+ maxDepth: String(projectConfig.commands.toc?.maxDepth ?? "6"),
1372
+ minDepth: String(projectConfig.commands.toc?.minDepth ?? "1"),
1373
+ ordered: Boolean(projectConfig.commands.toc?.ordered ?? false),
1374
+ }, opts));
1375
+ });
1139
1376
  common(md.command("check-snippets"))
1140
1377
  .description("Compare fenced code blocks against the source regions they declare")
1141
1378
  .argument("[inputs...]", "Markdown files, directories, or globs (default: workspace root)")
@@ -1161,11 +1398,14 @@ common(md.command("check-snippets"))
1161
1398
  " 2 --check or --dry-run found drift, or any mode found a link it\n" +
1162
1399
  " could not resolve, a malformed link, a fence it cannot rewrite,\n" +
1163
1400
  " or an edit-plan conflict")
1164
- .action((inputs, opts) => checkSnippetsAction(inputs.length ? inputs : [projectConfig.root], commandOptions("check-snippets", {
1165
- includeOk: false,
1166
- include: projectConfig.files.include,
1167
- exclude: projectConfig.files.exclude,
1168
- }, opts)));
1401
+ .action(async (inputs, opts) => {
1402
+ const { checkSnippetsAction } = await import("./commands/check-snippets.js");
1403
+ return checkSnippetsAction(inputs.length ? inputs : [projectConfig.root], commandOptions("check-snippets", {
1404
+ includeOk: false,
1405
+ include: projectConfig.files.include,
1406
+ exclude: projectConfig.files.exclude,
1407
+ }, opts));
1408
+ });
1169
1409
  common(md.command("rename-heading"))
1170
1410
  .description("Rename a heading and update all internal anchor references")
1171
1411
  .argument("<file>", "Path to the markdown file containing the heading")
@@ -1177,11 +1417,14 @@ common(md.command("rename-heading"))
1177
1417
  .option("--dry-run", "Show what would change without modifying files")
1178
1418
  .option("--no-dry-run", "Apply changes")
1179
1419
  .addHelpText("after", "\nFormat shorthands:\n -fh Shorthand for --format=human\n -fj Shorthand for --format=json\n\nExit codes:\n 0 Heading renamed successfully (or dry-run completed)\n 1 File/heading not found or new heading slug already exists")
1180
- .action((file, oldHeading, newHeading, opts) => renameHeadingAction(file, oldHeading, newHeading, commandOptions("rename-heading", {
1181
- dryRun: false,
1182
- include: projectConfig.files.include,
1183
- exclude: projectConfig.files.exclude,
1184
- }, opts)));
1420
+ .action(async (file, oldHeading, newHeading, opts) => {
1421
+ const { renameHeadingAction } = await import("./commands/rename-heading.js");
1422
+ return renameHeadingAction(file, oldHeading, newHeading, commandOptions("rename-heading", {
1423
+ dryRun: false,
1424
+ include: projectConfig.files.include,
1425
+ exclude: projectConfig.files.exclude,
1426
+ }, opts));
1427
+ });
1185
1428
  common(md.command("rename-file"))
1186
1429
  .description("Move a workspace file and update Markdown references")
1187
1430
  .argument("<source>", "Existing Markdown document or referenced asset")
@@ -1190,11 +1433,14 @@ common(md.command("rename-file"))
1190
1433
  .option("--exclude <glob>", "Markdown exclude glob (repeatable)", collect)
1191
1434
  .option("--dry-run", "Show changes without modifying files")
1192
1435
  .option("--no-dry-run", "Apply changes")
1193
- .action((source, destination, opts) => renameFileAction(source, destination, commandOptions("rename-file", {
1194
- dryRun: false,
1195
- include: projectConfig.files.include,
1196
- exclude: projectConfig.files.exclude,
1197
- }, opts)));
1436
+ .action(async (source, destination, opts) => {
1437
+ const { renameFileAction } = await import("./commands/rename-file.js");
1438
+ return renameFileAction(source, destination, commandOptions("rename-file", {
1439
+ dryRun: false,
1440
+ include: projectConfig.files.include,
1441
+ exclude: projectConfig.files.exclude,
1442
+ }, opts));
1443
+ });
1198
1444
  try {
1199
1445
  await program.parseAsync(argv);
1200
1446
  }