trail-code 0.0.0-stage → 0.28.0-semantic.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +567 -2
- package/dist/ai/crux.d.ts +88 -0
- package/dist/ai/crux.d.ts.map +1 -0
- package/dist/ai/crux.js +219 -0
- package/dist/ai/crux.js.map +1 -0
- package/dist/ai/failure.d.ts +60 -0
- package/dist/ai/failure.d.ts.map +1 -0
- package/dist/ai/failure.js +86 -0
- package/dist/ai/failure.js.map +1 -0
- package/dist/ai/llm/anthropic.d.ts +37 -0
- package/dist/ai/llm/anthropic.d.ts.map +1 -0
- package/dist/ai/llm/anthropic.js +144 -0
- package/dist/ai/llm/anthropic.js.map +1 -0
- package/dist/ai/llm/factory.d.ts +28 -0
- package/dist/ai/llm/factory.d.ts.map +1 -0
- package/dist/ai/llm/factory.js +36 -0
- package/dist/ai/llm/factory.js.map +1 -0
- package/dist/ai/llm/litellm.d.ts +34 -0
- package/dist/ai/llm/litellm.d.ts.map +1 -0
- package/dist/ai/llm/litellm.js +45 -0
- package/dist/ai/llm/litellm.js.map +1 -0
- package/dist/ai/llm/openai.d.ts +145 -0
- package/dist/ai/llm/openai.d.ts.map +1 -0
- package/dist/ai/llm/openai.js +629 -0
- package/dist/ai/llm/openai.js.map +1 -0
- package/dist/ai/llm/orcarouter.d.ts +34 -0
- package/dist/ai/llm/orcarouter.d.ts.map +1 -0
- package/dist/ai/llm/orcarouter.js +45 -0
- package/dist/ai/llm/orcarouter.js.map +1 -0
- package/dist/ai/llm/recover-tool.d.ts +21 -0
- package/dist/ai/llm/recover-tool.d.ts.map +1 -0
- package/dist/ai/llm/recover-tool.js +109 -0
- package/dist/ai/llm/recover-tool.js.map +1 -0
- package/dist/ai/llm/types.d.ts +122 -0
- package/dist/ai/llm/types.d.ts.map +1 -0
- package/dist/ai/llm/types.js +33 -0
- package/dist/ai/llm/types.js.map +1 -0
- package/dist/ai/providers.d.ts +59 -0
- package/dist/ai/providers.d.ts.map +1 -0
- package/dist/ai/providers.js +54 -0
- package/dist/ai/providers.js.map +1 -0
- package/dist/ai/summarize.d.ts +25 -0
- package/dist/ai/summarize.d.ts.map +1 -0
- package/dist/ai/summarize.js +34 -0
- package/dist/ai/summarize.js.map +1 -0
- package/dist/ai/synthesize.d.ts +40 -0
- package/dist/ai/synthesize.d.ts.map +1 -0
- package/dist/ai/synthesize.js +230 -0
- package/dist/ai/synthesize.js.map +1 -0
- package/dist/ask/ask.d.ts +199 -0
- package/dist/ask/ask.d.ts.map +1 -0
- package/dist/ask/ask.js +1291 -0
- package/dist/ask/ask.js.map +1 -0
- package/dist/ask/file-rank.d.ts +72 -0
- package/dist/ask/file-rank.d.ts.map +1 -0
- package/dist/ask/file-rank.js +160 -0
- package/dist/ask/file-rank.js.map +1 -0
- package/dist/ask/file-selection.d.ts +22 -0
- package/dist/ask/file-selection.d.ts.map +1 -0
- package/dist/ask/file-selection.js +66 -0
- package/dist/ask/file-selection.js.map +1 -0
- package/dist/ask/fuse.d.ts +195 -0
- package/dist/ask/fuse.d.ts.map +1 -0
- package/dist/ask/fuse.js +344 -0
- package/dist/ask/fuse.js.map +1 -0
- package/dist/ask/graphrank.d.ts +61 -0
- package/dist/ask/graphrank.d.ts.map +1 -0
- package/dist/ask/graphrank.js +130 -0
- package/dist/ask/graphrank.js.map +1 -0
- package/dist/ask/index-file.d.ts +44 -0
- package/dist/ask/index-file.d.ts.map +1 -0
- package/dist/ask/index-file.js +128 -0
- package/dist/ask/index-file.js.map +1 -0
- package/dist/ask/semantic.d.ts +42 -0
- package/dist/ask/semantic.d.ts.map +1 -0
- package/dist/ask/semantic.js +293 -0
- package/dist/ask/semantic.js.map +1 -0
- package/dist/bin/graft.d.ts +3 -0
- package/dist/bin/graft.d.ts.map +1 -0
- package/dist/bin/graft.js +14 -0
- package/dist/bin/graft.js.map +1 -0
- package/dist/bin/trail.d.ts +3 -0
- package/dist/bin/trail.d.ts.map +1 -0
- package/dist/bin/trail.js +13 -0
- package/dist/bin/trail.js.map +1 -0
- package/dist/blast/blast-cli.d.ts +35 -0
- package/dist/blast/blast-cli.d.ts.map +1 -0
- package/dist/blast/blast-cli.js +155 -0
- package/dist/blast/blast-cli.js.map +1 -0
- package/dist/blast/blast.d.ts +128 -0
- package/dist/blast/blast.d.ts.map +1 -0
- package/dist/blast/blast.js +383 -0
- package/dist/blast/blast.js.map +1 -0
- package/dist/blast/diff.d.ts +53 -0
- package/dist/blast/diff.d.ts.map +1 -0
- package/dist/blast/diff.js +193 -0
- package/dist/blast/diff.js.map +1 -0
- package/dist/blast/evidence.d.ts +51 -0
- package/dist/blast/evidence.d.ts.map +1 -0
- package/dist/blast/evidence.js +162 -0
- package/dist/blast/evidence.js.map +1 -0
- package/dist/blast/modules.d.ts +33 -0
- package/dist/blast/modules.d.ts.map +1 -0
- package/dist/blast/modules.js +83 -0
- package/dist/blast/modules.js.map +1 -0
- package/dist/blast/name.d.ts +74 -0
- package/dist/blast/name.d.ts.map +1 -0
- package/dist/blast/name.js +269 -0
- package/dist/blast/name.js.map +1 -0
- package/dist/blast/owners.d.ts +85 -0
- package/dist/blast/owners.d.ts.map +1 -0
- package/dist/blast/owners.js +272 -0
- package/dist/blast/owners.js.map +1 -0
- package/dist/blast/render.d.ts +46 -0
- package/dist/blast/render.d.ts.map +1 -0
- package/dist/blast/render.js +422 -0
- package/dist/blast/render.js.map +1 -0
- package/dist/blast/viz.d.ts +37 -0
- package/dist/blast/viz.d.ts.map +1 -0
- package/dist/blast/viz.js +180 -0
- package/dist/blast/viz.js.map +1 -0
- package/dist/brand.d.ts +111 -0
- package/dist/brand.d.ts.map +1 -0
- package/dist/brand.js +282 -0
- package/dist/brand.js.map +1 -0
- package/dist/claude/build-progress.d.ts +30 -0
- package/dist/claude/build-progress.d.ts.map +1 -0
- package/dist/claude/build-progress.js +81 -0
- package/dist/claude/build-progress.js.map +1 -0
- package/dist/claude/hermes-skill-template.d.ts +16 -0
- package/dist/claude/hermes-skill-template.d.ts.map +1 -0
- package/dist/claude/hermes-skill-template.js +154 -0
- package/dist/claude/hermes-skill-template.js.map +1 -0
- package/dist/claude/init.d.ts +65 -0
- package/dist/claude/init.d.ts.map +1 -0
- package/dist/claude/init.js +258 -0
- package/dist/claude/init.js.map +1 -0
- package/dist/claude/paths.d.ts +9 -0
- package/dist/claude/paths.d.ts.map +1 -0
- package/dist/claude/paths.js +22 -0
- package/dist/claude/paths.js.map +1 -0
- package/dist/claude/session-metrics.d.ts +73 -0
- package/dist/claude/session-metrics.d.ts.map +1 -0
- package/dist/claude/session-metrics.js +209 -0
- package/dist/claude/session-metrics.js.map +1 -0
- package/dist/claude/settings-merge.d.ts +76 -0
- package/dist/claude/settings-merge.d.ts.map +1 -0
- package/dist/claude/settings-merge.js +283 -0
- package/dist/claude/settings-merge.js.map +1 -0
- package/dist/claude/skill-template.d.ts +13 -0
- package/dist/claude/skill-template.d.ts.map +1 -0
- package/dist/claude/skill-template.js +290 -0
- package/dist/claude/skill-template.js.map +1 -0
- package/dist/claude/state.d.ts +85 -0
- package/dist/claude/state.d.ts.map +1 -0
- package/dist/claude/state.js +88 -0
- package/dist/claude/state.js.map +1 -0
- package/dist/claude/stats.d.ts +5 -0
- package/dist/claude/stats.d.ts.map +1 -0
- package/dist/claude/stats.js +24 -0
- package/dist/claude/stats.js.map +1 -0
- package/dist/cli-epilogue.d.ts +12 -0
- package/dist/cli-epilogue.d.ts.map +1 -0
- package/dist/cli-epilogue.js +72 -0
- package/dist/cli-epilogue.js.map +1 -0
- package/dist/cli-meta.d.ts +82 -0
- package/dist/cli-meta.d.ts.map +1 -0
- package/dist/cli-meta.js +238 -0
- package/dist/cli-meta.js.map +1 -0
- package/dist/cli-picker.d.ts +139 -0
- package/dist/cli-picker.d.ts.map +1 -0
- package/dist/cli-picker.js +439 -0
- package/dist/cli-picker.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1638 -0
- package/dist/cli.js.map +1 -0
- package/dist/context/batches.d.ts +29 -0
- package/dist/context/batches.d.ts.map +1 -0
- package/dist/context/batches.js +130 -0
- package/dist/context/batches.js.map +1 -0
- package/dist/context/build.d.ts +57 -0
- package/dist/context/build.d.ts.map +1 -0
- package/dist/context/build.js +396 -0
- package/dist/context/build.js.map +1 -0
- package/dist/context/check.d.ts +42 -0
- package/dist/context/check.d.ts.map +1 -0
- package/dist/context/check.js +162 -0
- package/dist/context/check.js.map +1 -0
- package/dist/context/map-dir.d.ts +13 -0
- package/dist/context/map-dir.d.ts.map +1 -0
- package/dist/context/map-dir.js +81 -0
- package/dist/context/map-dir.js.map +1 -0
- package/dist/context/node-file.d.ts +109 -0
- package/dist/context/node-file.d.ts.map +1 -0
- package/dist/context/node-file.js +390 -0
- package/dist/context/node-file.js.map +1 -0
- package/dist/context/price.d.ts +49 -0
- package/dist/context/price.d.ts.map +1 -0
- package/dist/context/price.js +83 -0
- package/dist/context/price.js.map +1 -0
- package/dist/context/savings.d.ts +67 -0
- package/dist/context/savings.d.ts.map +1 -0
- package/dist/context/savings.js +125 -0
- package/dist/context/savings.js.map +1 -0
- package/dist/engine.d.ts +85 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +122 -0
- package/dist/engine.js.map +1 -0
- package/dist/graph/bindings.d.ts +64 -0
- package/dist/graph/bindings.d.ts.map +1 -0
- package/dist/graph/bindings.js +713 -0
- package/dist/graph/bindings.js.map +1 -0
- package/dist/graph/build-run.d.ts +25 -0
- package/dist/graph/build-run.d.ts.map +1 -0
- package/dist/graph/build-run.js +121 -0
- package/dist/graph/build-run.js.map +1 -0
- package/dist/graph/build.d.ts +88 -0
- package/dist/graph/build.d.ts.map +1 -0
- package/dist/graph/build.js +386 -0
- package/dist/graph/build.js.map +1 -0
- package/dist/graph/cards.d.ts +46 -0
- package/dist/graph/cards.d.ts.map +1 -0
- package/dist/graph/cards.js +280 -0
- package/dist/graph/cards.js.map +1 -0
- package/dist/graph/check.d.ts +25 -0
- package/dist/graph/check.d.ts.map +1 -0
- package/dist/graph/check.js +192 -0
- package/dist/graph/check.js.map +1 -0
- package/dist/graph/container.d.ts +63 -0
- package/dist/graph/container.d.ts.map +1 -0
- package/dist/graph/container.js +191 -0
- package/dist/graph/container.js.map +1 -0
- package/dist/graph/depth-parser.d.ts +61 -0
- package/dist/graph/depth-parser.d.ts.map +1 -0
- package/dist/graph/depth-parser.js +153 -0
- package/dist/graph/depth-parser.js.map +1 -0
- package/dist/graph/enrich.d.ts +62 -0
- package/dist/graph/enrich.d.ts.map +1 -0
- package/dist/graph/enrich.js +258 -0
- package/dist/graph/enrich.js.map +1 -0
- package/dist/graph/extract-cache.d.ts +101 -0
- package/dist/graph/extract-cache.d.ts.map +1 -0
- package/dist/graph/extract-cache.js +199 -0
- package/dist/graph/extract-cache.js.map +1 -0
- package/dist/graph/extract.d.ts +85 -0
- package/dist/graph/extract.d.ts.map +1 -0
- package/dist/graph/extract.js +2377 -0
- package/dist/graph/extract.js.map +1 -0
- package/dist/graph/fingerprint.d.ts +76 -0
- package/dist/graph/fingerprint.d.ts.map +1 -0
- package/dist/graph/fingerprint.js +160 -0
- package/dist/graph/fingerprint.js.map +1 -0
- package/dist/graph/generic.d.ts +74 -0
- package/dist/graph/generic.d.ts.map +1 -0
- package/dist/graph/generic.js +773 -0
- package/dist/graph/generic.js.map +1 -0
- package/dist/graph/invariants.d.ts +33 -0
- package/dist/graph/invariants.d.ts.map +1 -0
- package/dist/graph/invariants.js +52 -0
- package/dist/graph/invariants.js.map +1 -0
- package/dist/graph/load.d.ts +29 -0
- package/dist/graph/load.d.ts.map +1 -0
- package/dist/graph/load.js +97 -0
- package/dist/graph/load.js.map +1 -0
- package/dist/graph/lsp/client.d.ts +61 -0
- package/dist/graph/lsp/client.d.ts.map +1 -0
- package/dist/graph/lsp/client.js +217 -0
- package/dist/graph/lsp/client.js.map +1 -0
- package/dist/graph/lsp/enrich.d.ts +11 -0
- package/dist/graph/lsp/enrich.d.ts.map +1 -0
- package/dist/graph/lsp/enrich.js +163 -0
- package/dist/graph/lsp/enrich.js.map +1 -0
- package/dist/graph/lsp/registry.d.ts +22 -0
- package/dist/graph/lsp/registry.d.ts.map +1 -0
- package/dist/graph/lsp/registry.js +71 -0
- package/dist/graph/lsp/registry.js.map +1 -0
- package/dist/graph/map.d.ts +107 -0
- package/dist/graph/map.d.ts.map +1 -0
- package/dist/graph/map.js +239 -0
- package/dist/graph/map.js.map +1 -0
- package/dist/graph/markdown.d.ts +22 -0
- package/dist/graph/markdown.d.ts.map +1 -0
- package/dist/graph/markdown.js +143 -0
- package/dist/graph/markdown.js.map +1 -0
- package/dist/graph/native.d.ts +46 -0
- package/dist/graph/native.d.ts.map +1 -0
- package/dist/graph/native.js +169 -0
- package/dist/graph/native.js.map +1 -0
- package/dist/graph/queries/c.scm +13 -0
- package/dist/graph/queries/c_sharp.scm +19 -0
- package/dist/graph/queries/clojure.scm +58 -0
- package/dist/graph/queries/cpp.scm +23 -0
- package/dist/graph/queries/dart.scm +42 -0
- package/dist/graph/queries/elixir.scm +54 -0
- package/dist/graph/queries/gleam.scm +51 -0
- package/dist/graph/queries/java.scm +20 -0
- package/dist/graph/queries/kotlin.scm +43 -0
- package/dist/graph/queries/lua.scm +36 -0
- package/dist/graph/queries/nix.scm +32 -0
- package/dist/graph/queries/php.scm +40 -0
- package/dist/graph/queries/ruby.scm +65 -0
- package/dist/graph/queries/rust.scm +41 -0
- package/dist/graph/queries/scala.scm +66 -0
- package/dist/graph/queries/solidity.scm +43 -0
- package/dist/graph/queries/swift.scm +51 -0
- package/dist/graph/refresh.d.ts +106 -0
- package/dist/graph/refresh.d.ts.map +1 -0
- package/dist/graph/refresh.js +430 -0
- package/dist/graph/refresh.js.map +1 -0
- package/dist/graph/relations.d.ts +14 -0
- package/dist/graph/relations.d.ts.map +1 -0
- package/dist/graph/relations.js +8 -0
- package/dist/graph/relations.js.map +1 -0
- package/dist/graph/resolve.d.ts +17 -0
- package/dist/graph/resolve.d.ts.map +1 -0
- package/dist/graph/resolve.js +716 -0
- package/dist/graph/resolve.js.map +1 -0
- package/dist/graph/root.d.ts +11 -0
- package/dist/graph/root.d.ts.map +1 -0
- package/dist/graph/root.js +40 -0
- package/dist/graph/root.js.map +1 -0
- package/dist/graph/scopes.d.ts +40 -0
- package/dist/graph/scopes.d.ts.map +1 -0
- package/dist/graph/scopes.js +383 -0
- package/dist/graph/scopes.js.map +1 -0
- package/dist/graph/seed.d.ts +38 -0
- package/dist/graph/seed.d.ts.map +1 -0
- package/dist/graph/seed.js +229 -0
- package/dist/graph/seed.js.map +1 -0
- package/dist/graph/source-files.d.ts +46 -0
- package/dist/graph/source-files.d.ts.map +1 -0
- package/dist/graph/source-files.js +118 -0
- package/dist/graph/source-files.js.map +1 -0
- package/dist/graph/traverse-cli.d.ts +55 -0
- package/dist/graph/traverse-cli.d.ts.map +1 -0
- package/dist/graph/traverse-cli.js +189 -0
- package/dist/graph/traverse-cli.js.map +1 -0
- package/dist/graph/traverse.d.ts +112 -0
- package/dist/graph/traverse.d.ts.map +1 -0
- package/dist/graph/traverse.js +192 -0
- package/dist/graph/traverse.js.map +1 -0
- package/dist/graph/types.d.ts +78 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +14 -0
- package/dist/graph/types.js.map +1 -0
- package/dist/graph/wasm-runtime.d.ts +19 -0
- package/dist/graph/wasm-runtime.d.ts.map +1 -0
- package/dist/graph/wasm-runtime.js +38 -0
- package/dist/graph/wasm-runtime.js.map +1 -0
- package/dist/graph/workspace-cli.d.ts +56 -0
- package/dist/graph/workspace-cli.d.ts.map +1 -0
- package/dist/graph/workspace-cli.js +111 -0
- package/dist/graph/workspace-cli.js.map +1 -0
- package/dist/graph/workspace.d.ts +172 -0
- package/dist/graph/workspace.d.ts.map +1 -0
- package/dist/graph/workspace.js +714 -0
- package/dist/graph/workspace.js.map +1 -0
- package/dist/graph/write.d.ts +13 -0
- package/dist/graph/write.d.ts.map +1 -0
- package/dist/graph/write.js +86 -0
- package/dist/graph/write.js.map +1 -0
- package/dist/hosts/antigravity.d.ts +14 -0
- package/dist/hosts/antigravity.d.ts.map +1 -0
- package/dist/hosts/antigravity.js +59 -0
- package/dist/hosts/antigravity.js.map +1 -0
- package/dist/hosts/auto.d.ts +37 -0
- package/dist/hosts/auto.d.ts.map +1 -0
- package/dist/hosts/auto.js +141 -0
- package/dist/hosts/auto.js.map +1 -0
- package/dist/hosts/claude-global.d.ts +72 -0
- package/dist/hosts/claude-global.d.ts.map +1 -0
- package/dist/hosts/claude-global.js +230 -0
- package/dist/hosts/claude-global.js.map +1 -0
- package/dist/hosts/config-write.d.ts +36 -0
- package/dist/hosts/config-write.d.ts.map +1 -0
- package/dist/hosts/config-write.js +64 -0
- package/dist/hosts/config-write.js.map +1 -0
- package/dist/hosts/init.d.ts +28 -0
- package/dist/hosts/init.d.ts.map +1 -0
- package/dist/hosts/init.js +89 -0
- package/dist/hosts/init.js.map +1 -0
- package/dist/hosts/instructions.d.ts +14 -0
- package/dist/hosts/instructions.d.ts.map +1 -0
- package/dist/hosts/instructions.js +95 -0
- package/dist/hosts/instructions.js.map +1 -0
- package/dist/hosts/mcp-config.d.ts +78 -0
- package/dist/hosts/mcp-config.d.ts.map +1 -0
- package/dist/hosts/mcp-config.js +251 -0
- package/dist/hosts/mcp-config.js.map +1 -0
- package/dist/hosts/plan.d.ts +32 -0
- package/dist/hosts/plan.d.ts.map +1 -0
- package/dist/hosts/plan.js +73 -0
- package/dist/hosts/plan.js.map +1 -0
- package/dist/hosts/registry.d.ts +41 -0
- package/dist/hosts/registry.d.ts.map +1 -0
- package/dist/hosts/registry.js +164 -0
- package/dist/hosts/registry.js.map +1 -0
- package/dist/hosts/retract.d.ts +47 -0
- package/dist/hosts/retract.d.ts.map +1 -0
- package/dist/hosts/retract.js +520 -0
- package/dist/hosts/retract.js.map +1 -0
- package/dist/hosts/sections.d.ts +47 -0
- package/dist/hosts/sections.d.ts.map +1 -0
- package/dist/hosts/sections.js +127 -0
- package/dist/hosts/sections.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest/fs.d.ts +79 -0
- package/dist/ingest/fs.d.ts.map +1 -0
- package/dist/ingest/fs.js +306 -0
- package/dist/ingest/fs.js.map +1 -0
- package/dist/init-guard.d.ts +16 -0
- package/dist/init-guard.d.ts.map +1 -0
- package/dist/init-guard.js +27 -0
- package/dist/init-guard.js.map +1 -0
- package/dist/mcp/instructions.d.ts +5 -0
- package/dist/mcp/instructions.d.ts.map +1 -0
- package/dist/mcp/instructions.js +64 -0
- package/dist/mcp/instructions.js.map +1 -0
- package/dist/mcp/kept-listing.d.ts +27 -0
- package/dist/mcp/kept-listing.d.ts.map +1 -0
- package/dist/mcp/kept-listing.js +63 -0
- package/dist/mcp/kept-listing.js.map +1 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +147 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tool-names.d.ts +27 -0
- package/dist/mcp/tool-names.d.ts.map +1 -0
- package/dist/mcp/tool-names.js +47 -0
- package/dist/mcp/tool-names.js.map +1 -0
- package/dist/mcp/tools.d.ts +22 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +440 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/node-version.d.ts +15 -0
- package/dist/node-version.d.ts.map +1 -0
- package/dist/node-version.js +23 -0
- package/dist/node-version.js.map +1 -0
- package/dist/search/grep-cli.d.ts +35 -0
- package/dist/search/grep-cli.d.ts.map +1 -0
- package/dist/search/grep-cli.js +115 -0
- package/dist/search/grep-cli.js.map +1 -0
- package/dist/search/grep.d.ts +60 -0
- package/dist/search/grep.d.ts.map +1 -0
- package/dist/search/grep.js +143 -0
- package/dist/search/grep.js.map +1 -0
- package/dist/search/ignored-code.d.ts +9 -0
- package/dist/search/ignored-code.d.ts.map +1 -0
- package/dist/search/ignored-code.js +73 -0
- package/dist/search/ignored-code.js.map +1 -0
- package/dist/search/user-regexp.d.ts +26 -0
- package/dist/search/user-regexp.d.ts.map +1 -0
- package/dist/search/user-regexp.js +150 -0
- package/dist/search/user-regexp.js.map +1 -0
- package/dist/telemetry/contract.d.ts +117 -0
- package/dist/telemetry/contract.d.ts.map +1 -0
- package/dist/telemetry/contract.js +341 -0
- package/dist/telemetry/contract.js.map +1 -0
- package/dist/telemetry/flush.d.ts +21 -0
- package/dist/telemetry/flush.d.ts.map +1 -0
- package/dist/telemetry/flush.js +85 -0
- package/dist/telemetry/flush.js.map +1 -0
- package/dist/telemetry/gate.d.ts +30 -0
- package/dist/telemetry/gate.d.ts.map +1 -0
- package/dist/telemetry/gate.js +81 -0
- package/dist/telemetry/gate.js.map +1 -0
- package/dist/telemetry/identity.d.ts +40 -0
- package/dist/telemetry/identity.d.ts.map +1 -0
- package/dist/telemetry/identity.js +80 -0
- package/dist/telemetry/identity.js.map +1 -0
- package/dist/telemetry/index.d.ts +18 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +16 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/telemetry/key.d.ts +21 -0
- package/dist/telemetry/key.d.ts.map +1 -0
- package/dist/telemetry/key.js +49 -0
- package/dist/telemetry/key.js.map +1 -0
- package/dist/telemetry/notice.d.ts +19 -0
- package/dist/telemetry/notice.d.ts.map +1 -0
- package/dist/telemetry/notice.js +93 -0
- package/dist/telemetry/notice.js.map +1 -0
- package/dist/telemetry/queue.d.ts +35 -0
- package/dist/telemetry/queue.d.ts.map +1 -0
- package/dist/telemetry/queue.js +162 -0
- package/dist/telemetry/queue.js.map +1 -0
- package/dist/telemetry/send.d.ts +19 -0
- package/dist/telemetry/send.d.ts.map +1 -0
- package/dist/telemetry/send.js +84 -0
- package/dist/telemetry/send.js.map +1 -0
- package/dist/telemetry/sessions.d.ts +25 -0
- package/dist/telemetry/sessions.d.ts.map +1 -0
- package/dist/telemetry/sessions.js +104 -0
- package/dist/telemetry/sessions.js.map +1 -0
- package/dist/telemetry/track.d.ts +62 -0
- package/dist/telemetry/track.d.ts.map +1 -0
- package/dist/telemetry/track.js +143 -0
- package/dist/telemetry/track.js.map +1 -0
- package/dist/upkeep-run.d.ts +22 -0
- package/dist/upkeep-run.d.ts.map +1 -0
- package/dist/upkeep-run.js +85 -0
- package/dist/upkeep-run.js.map +1 -0
- package/dist/upkeep.d.ts +127 -0
- package/dist/upkeep.d.ts.map +1 -0
- package/dist/upkeep.js +252 -0
- package/dist/upkeep.js.map +1 -0
- package/dist/util/actions-folder.d.ts +2 -0
- package/dist/util/actions-folder.d.ts.map +1 -0
- package/dist/util/actions-folder.js +9 -0
- package/dist/util/actions-folder.js.map +1 -0
- package/dist/util/browser.d.ts +3 -0
- package/dist/util/browser.d.ts.map +1 -0
- package/dist/util/browser.js +14 -0
- package/dist/util/browser.js.map +1 -0
- package/dist/util/child-process.d.ts +15 -0
- package/dist/util/child-process.d.ts.map +1 -0
- package/dist/util/child-process.js +15 -0
- package/dist/util/child-process.js.map +1 -0
- package/dist/util/front-matter.d.ts +46 -0
- package/dist/util/front-matter.d.ts.map +1 -0
- package/dist/util/front-matter.js +113 -0
- package/dist/util/front-matter.js.map +1 -0
- package/dist/util/id.d.ts +7 -0
- package/dist/util/id.d.ts.map +1 -0
- package/dist/util/id.js +14 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/paths.d.ts +29 -0
- package/dist/util/paths.d.ts.map +1 -0
- package/dist/util/paths.js +71 -0
- package/dist/util/paths.js.map +1 -0
- package/dist/util/private-file.d.ts +14 -0
- package/dist/util/private-file.d.ts.map +1 -0
- package/dist/util/private-file.js +82 -0
- package/dist/util/private-file.js.map +1 -0
- package/dist/util/source.d.ts +4 -0
- package/dist/util/source.d.ts.map +1 -0
- package/dist/util/source.js +12 -0
- package/dist/util/source.js.map +1 -0
- package/dist/util/spinner.d.ts +15 -0
- package/dist/util/spinner.d.ts.map +1 -0
- package/dist/util/spinner.js +52 -0
- package/dist/util/spinner.js.map +1 -0
- package/dist/util/state.d.ts +120 -0
- package/dist/util/state.d.ts.map +1 -0
- package/dist/util/state.js +264 -0
- package/dist/util/state.js.map +1 -0
- package/dist/vendor/grammars/tree-sitter-go.LICENSE +21 -0
- package/dist/vendor/grammars/tree-sitter-go.wasm +0 -0
- package/dist/vendor/grammars/tree-sitter-java.LICENSE +21 -0
- package/dist/vendor/grammars/tree-sitter-java.wasm +0 -0
- package/dist/vendor/grammars/tree-sitter-kotlin.LICENSE +9 -0
- package/dist/vendor/grammars/tree-sitter-kotlin.wasm +0 -0
- package/dist/vendor/grammars/tree-sitter-php.LICENSE +22 -0
- package/dist/vendor/grammars/tree-sitter-php.wasm +0 -0
- package/dist/vendor/grammars/tree-sitter-python.LICENSE +21 -0
- package/dist/vendor/grammars/tree-sitter-python.wasm +0 -0
- package/dist/vendor/grammars/tree-sitter-r.LICENSE +9 -0
- package/dist/vendor/grammars/tree-sitter-r.wasm +0 -0
- package/dist/vendor/grammars/tree-sitter-swift.LICENSE +21 -0
- package/dist/vendor/grammars/tree-sitter-swift.wasm +0 -0
- package/dist/vendor/grammars/tree-sitter-tsx.wasm +0 -0
- package/dist/vendor/grammars/tree-sitter-typescript.LICENSE +21 -0
- package/dist/vendor/grammars/tree-sitter-typescript.wasm +0 -0
- package/dist/viewer/app.js +2 -0
- package/dist/viewer/index.html +49 -0
- package/dist/viewer/style.css +154 -0
- package/dist/viz/assemble.d.ts +83 -0
- package/dist/viz/assemble.d.ts.map +1 -0
- package/dist/viz/assemble.js +101 -0
- package/dist/viz/assemble.js.map +1 -0
- package/dist/viz/export.d.ts +39 -0
- package/dist/viz/export.d.ts.map +1 -0
- package/dist/viz/export.js +98 -0
- package/dist/viz/export.js.map +1 -0
- package/dist/viz/serve.d.ts +13 -0
- package/dist/viz/serve.d.ts.map +1 -0
- package/dist/viz/serve.js +130 -0
- package/dist/viz/serve.js.map +1 -0
- package/package.json +115 -4
- package/scripts/build-grammar-wasm.mjs +112 -0
- package/scripts/build-viewer.mjs +44 -0
- package/scripts/graph-quality.mjs +117 -0
- package/scripts/pack-trail.mjs +53 -0
- package/scripts/postinstall.mjs +65 -0
- package/scripts/run-tests.mjs +72 -0
- package/scripts/stamp-telemetry-key.mjs +56 -0
- package/scripts/tally-audit.mjs +140 -0
- package/scripts/vendor-grammars.mjs +53 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1638 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { join, relative, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { Graft } from "./engine.js";
|
|
8
|
+
import { resolveConfig } from "./ai/providers.js";
|
|
9
|
+
import { formatCheckReport } from "./context/check.js";
|
|
10
|
+
import { formatGraphCheckReport } from "./graph/check.js";
|
|
11
|
+
import { buildGraphIfMissing, runInit } from "./claude/init.js";
|
|
12
|
+
import { statuslineWanted } from "./claude/settings-merge.js";
|
|
13
|
+
import { runHostsInit } from "./hosts/init.js";
|
|
14
|
+
import { hostIds } from "./hosts/registry.js";
|
|
15
|
+
import { agentList, autoSelection } from "./hosts/auto.js";
|
|
16
|
+
import { asksForTrail, brand, cmd, graftNotice, setBrand, strandedRepoNotice, tag } from "./brand.js";
|
|
17
|
+
import { contextDirFor } from "./context/node-file.js";
|
|
18
|
+
import { loadGraphCached } from "./graph/load.js";
|
|
19
|
+
import { ensureFreshChildren, ensureFreshGraph, refreshNote } from "./graph/refresh.js";
|
|
20
|
+
import { isWorkspaceBuildRoot, loadWorkspaceGraphs, readWorkspace } from "./graph/workspace.js";
|
|
21
|
+
import { homeFolderRefusal } from "./init-guard.js";
|
|
22
|
+
import { nearestGraftRoot } from "./graph/root.js";
|
|
23
|
+
import { unsupportedExtensions, supportedExtensions } from "./graph/source-files.js";
|
|
24
|
+
import { discoverWorkspaceChildren } from "./graph/scopes.js";
|
|
25
|
+
import { runWorkspaceAsk, runWorkspaceBuild, runWorkspaceCallers, runWorkspaceCheck, runWorkspaceGrep, runWorkspaceMap, } from "./graph/workspace-cli.js";
|
|
26
|
+
import { planInit, selectedWrites } from "./hosts/plan.js";
|
|
27
|
+
import { planRetract, runRetract, changed } from "./hosts/retract.js";
|
|
28
|
+
import { formatNonInteractiveHelp, formatPlan, runPicker } from "./cli-picker.js";
|
|
29
|
+
import { homedir } from "node:os";
|
|
30
|
+
import { formatUpgradeReport, formatVersionReport, getNpmViewVersion, readCurrentVersion, readPackageName, runUpgrade } from "./cli-meta.js";
|
|
31
|
+
import { nativeFallbackNotice } from "./graph/native.js";
|
|
32
|
+
import { patchBuildConfig } from "./util/state.js";
|
|
33
|
+
import { normalizePathPrefix } from "./util/paths.js";
|
|
34
|
+
import { latestSession, formatSessionStats, sessionInputRate } from "./claude/session-metrics.js";
|
|
35
|
+
import { listSessionIds, readSession, sessionDir } from "./claude/state.js";
|
|
36
|
+
import { setInputRate } from "./context/savings.js";
|
|
37
|
+
import { formatUpdateNudge, maybeRefreshInBackground, readUpdateCache, refreshUpdateCache, writeStamp } from "./upkeep.js";
|
|
38
|
+
import { errorCode, filesBucket, durationBucket, formatDebug, formatStatus, firstRunNotice, isTrackedCommand, langsValue, offReason, maybeFlushInBackground, patchState, runFlush, track, trackFirstRunIfNew, } from "./telemetry/index.js";
|
|
39
|
+
const program = new Command();
|
|
40
|
+
const currentVersion = readCurrentVersion(import.meta.url);
|
|
41
|
+
// When Commander prints an error (unknown option, missing required option, etc.)
|
|
42
|
+
// append a hint so users know how to discover the available flags.
|
|
43
|
+
program.configureOutput({
|
|
44
|
+
writeErr: (str) => {
|
|
45
|
+
process.stderr.write(str);
|
|
46
|
+
},
|
|
47
|
+
outputError: (str, write) => {
|
|
48
|
+
write(str);
|
|
49
|
+
// Append a --help hint when Commander exits with a parse error, so users
|
|
50
|
+
// know how to discover available flags instead of having to guess.
|
|
51
|
+
if (str.startsWith('error:')) {
|
|
52
|
+
const sub = process.argv[2];
|
|
53
|
+
const hint = sub && !sub.startsWith('-')
|
|
54
|
+
? `Run 'trail ${sub} --help' to see available options.\n`
|
|
55
|
+
: `Run 'trail --help' to see available commands.\n`;
|
|
56
|
+
process.stderr.write(hint);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* Started as `trail` rather than `graft`. Decided once, before any command is
|
|
62
|
+
* registered, because it changes which commands exist: under trail the old
|
|
63
|
+
* `graft trail …` group moves to the top level, `check` hands its name to the
|
|
64
|
+
* team check (the freshness check is `build --check`), and `stats` and
|
|
65
|
+
* `trail status` become one `status`. Under graft nothing moves. Trail's setup
|
|
66
|
+
* prompt runs as trail whichever name started it (`asksForTrail`).
|
|
67
|
+
*/
|
|
68
|
+
if (asksForTrail(process.argv.slice(2)))
|
|
69
|
+
setBrand("trail");
|
|
70
|
+
const TRAIL = brand() === "trail";
|
|
71
|
+
/** Help-screen groups under trail; graft keeps its one flat list. */
|
|
72
|
+
const GROUP = {
|
|
73
|
+
find: "Find code · your agent runs these:",
|
|
74
|
+
setup: "Setup:",
|
|
75
|
+
ci: "CI:",
|
|
76
|
+
};
|
|
77
|
+
/** Put a command in a help group, under trail only. */
|
|
78
|
+
function grouped(c, heading) {
|
|
79
|
+
if (TRAIL)
|
|
80
|
+
c.helpGroup(heading);
|
|
81
|
+
return c;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* What the `query` telemetry event will say, filled in by the command as it runs
|
|
85
|
+
* and emitted once from the `postAction` hook below.
|
|
86
|
+
*
|
|
87
|
+
* A module-level slot rather than a threaded parameter because the repo root is
|
|
88
|
+
* resolved deep inside each action (`queryRoot`) while the event is emitted
|
|
89
|
+
* centrally — and because a command that calls `process.exit` should simply
|
|
90
|
+
* report nothing, which falls out of never emitting until postAction.
|
|
91
|
+
*/
|
|
92
|
+
let queryNote = {};
|
|
93
|
+
/** Record the repo a query ran against, and pass it straight through so call
|
|
94
|
+
* sites stay one line. */
|
|
95
|
+
function noteQuery(dir) {
|
|
96
|
+
queryNote.repo = dir;
|
|
97
|
+
// Every retrieval command funnels through here, which makes it the one place
|
|
98
|
+
// that can price this session's tokens before a formatter needs the number.
|
|
99
|
+
setInputRate(sessionInputRate(dir));
|
|
100
|
+
return dir;
|
|
101
|
+
}
|
|
102
|
+
/** Whether a query found anything. Only the commands that have a result count in
|
|
103
|
+
* hand call this; the property is simply absent for the others. */
|
|
104
|
+
function noteHit(found) {
|
|
105
|
+
queryNote.hit = found ? "yes" : "no";
|
|
106
|
+
}
|
|
107
|
+
program
|
|
108
|
+
.name(brand())
|
|
109
|
+
.description(TRAIL
|
|
110
|
+
? "A code map for your coding agents: exact file:line spans, a call graph,\n" +
|
|
111
|
+
"and a semantic index, kept in sync with the working tree as you query it."
|
|
112
|
+
: "Build a repo's context graph as linked markdown, and keep it in sync with the code.")
|
|
113
|
+
.version(currentVersion, "-v, --version")
|
|
114
|
+
.option("--dir <path>", "context graph directory (default: <repo>/graft)")
|
|
115
|
+
.option("--provider <name>", "LLM wire format: openai | anthropic | litellm | orcarouter (env GRAFT_PROVIDER)")
|
|
116
|
+
.option("--model <id>", "model id for the LLM pass (env GRAFT_MODEL)")
|
|
117
|
+
.option("--api-key <key>", "provider API key (env GRAFT_API_KEY)")
|
|
118
|
+
.option("--base-url <url>", "OpenAI-compatible endpoint URL (env GRAFT_BASE_URL)");
|
|
119
|
+
/** Config drawn from the global CLI flags (env + defaults fill the rest). */
|
|
120
|
+
function cliConfig() {
|
|
121
|
+
const o = program.opts();
|
|
122
|
+
return {
|
|
123
|
+
contextDir: o.dir,
|
|
124
|
+
provider: o.provider,
|
|
125
|
+
model: o.model,
|
|
126
|
+
apiKey: o.apiKey,
|
|
127
|
+
baseUrl: o.baseUrl,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const engineFrom = () => new Graft(cliConfig());
|
|
131
|
+
/**
|
|
132
|
+
* Warn (never fail) when a user's `-e` extension has no parser, so it is never a silent
|
|
133
|
+
* no-op — `graft build -e ".vue"` used to accept it, index nothing, and exit 0. The
|
|
134
|
+
* supported set is listed so `-e` also answers "what is actually supported".
|
|
135
|
+
*/
|
|
136
|
+
function warnUnsupportedExtensions(exts) {
|
|
137
|
+
if (!exts?.length)
|
|
138
|
+
return;
|
|
139
|
+
const bad = unsupportedExtensions(exts);
|
|
140
|
+
if (bad.length === 0)
|
|
141
|
+
return;
|
|
142
|
+
for (const e of bad) {
|
|
143
|
+
const shown = e.trim().startsWith(".") ? e.trim() : `.${e.trim()}`;
|
|
144
|
+
console.error(`⚠ -e "${shown}": no parser registered for this extension, so it is ignored.`);
|
|
145
|
+
}
|
|
146
|
+
console.error(` supported: ${supportedExtensions().join(" ")}`);
|
|
147
|
+
}
|
|
148
|
+
/** Text for the omitted-`[dir]` case, shared by every query command's help. */
|
|
149
|
+
const DIR_ARG = ["[dir]", "repository root (default: nearest ancestor with a graft/ index)"];
|
|
150
|
+
/**
|
|
151
|
+
* The root a query runs against: the dir the user named, else the nearest
|
|
152
|
+
* ancestor holding a graft index (`graph/root.ts`) so a shell or agent session
|
|
153
|
+
* in a subdirectory still finds the graph. The walk is announced on stderr —
|
|
154
|
+
* answering from an ancestor's graph must never be silent.
|
|
155
|
+
*/
|
|
156
|
+
function queryRoot(dir) {
|
|
157
|
+
if (dir !== undefined)
|
|
158
|
+
return resolve(dir);
|
|
159
|
+
const { root, levels } = nearestGraftRoot(process.cwd(), program.opts().dir);
|
|
160
|
+
if (levels > 0)
|
|
161
|
+
console.error(`${tag()} no code map here: answering from ${contextDirFor(root, program.opts().dir)}`);
|
|
162
|
+
return root;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Bring the graph up to date with the working tree before a query answers from it
|
|
166
|
+
* — the same gate the MCP tools run (see `graph/refresh.ts`). Cheap when nothing
|
|
167
|
+
* moved; a structural, $0 rebuild when it did. The note goes to stderr so `--json`
|
|
168
|
+
* stdout stays machine-readable.
|
|
169
|
+
*/
|
|
170
|
+
async function refreshBefore(dir, opts) {
|
|
171
|
+
const globalDir = program.opts().dir;
|
|
172
|
+
const root = resolve(dir);
|
|
173
|
+
const disabled = opts.refresh === false;
|
|
174
|
+
const ws = readWorkspace(root, globalDir);
|
|
175
|
+
const r = ws
|
|
176
|
+
? await ensureFreshChildren(root, ws.children, { contextDir: globalDir, disabled })
|
|
177
|
+
: await ensureFreshGraph(root, { contextDir: globalDir, disabled });
|
|
178
|
+
const note = refreshNote(r);
|
|
179
|
+
if (note)
|
|
180
|
+
console.error(note);
|
|
181
|
+
}
|
|
182
|
+
/** Attached to every query command: `--no-refresh` answers from the graph exactly
|
|
183
|
+
* as it is on disk, no rebuild. */
|
|
184
|
+
const NO_REFRESH_FLAG = ["--no-refresh", "skip the freshness check: answer from the graph as-is"];
|
|
185
|
+
const VIZ_TABS = ["context", "code", "outline"];
|
|
186
|
+
/**
|
|
187
|
+
* `--tabs context,code` → the tabs to write into an exported page.
|
|
188
|
+
*
|
|
189
|
+
* An unknown name is a caller mistake worth failing on rather than silently
|
|
190
|
+
* dropping: a page exported with a typo'd tab list would be missing a tab and
|
|
191
|
+
* nothing would say why. Undefined means "all of them", which is the default the
|
|
192
|
+
* exporter already applies.
|
|
193
|
+
*/
|
|
194
|
+
function parseTabs(raw) {
|
|
195
|
+
if (raw === undefined)
|
|
196
|
+
return undefined;
|
|
197
|
+
const want = raw.split(",").map((t) => t.trim()).filter(Boolean);
|
|
198
|
+
const bad = want.filter((t) => !VIZ_TABS.includes(t));
|
|
199
|
+
if (bad.length > 0 || want.length === 0) {
|
|
200
|
+
console.error(`✗ --tabs takes a comma-separated subset of ${VIZ_TABS.join(", ")}${bad.length ? `, not "${bad.join('", "')}"` : ""}`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
return want;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Commands that own the upgrade story themselves (`version`, `upgrade`) or must
|
|
207
|
+
* not editorialize on stderr at startup (`mcp` runs its own upkeep at boot, and
|
|
208
|
+
* `_update-check` IS the fetch).
|
|
209
|
+
*/
|
|
210
|
+
const UPKEEP_SKIP = new Set(["version", "upgrade", "_update-check", "mcp"]);
|
|
211
|
+
/**
|
|
212
|
+
* Every other command: top up the cached registry answer in the background and,
|
|
213
|
+
* if a newer graft is out, say so once on stderr. This is what makes the CLI the
|
|
214
|
+
* cache filler for the hooks, which are not allowed to touch the network.
|
|
215
|
+
*/
|
|
216
|
+
program.hook("preAction", (_parent, action) => {
|
|
217
|
+
if (UPKEEP_SKIP.has(action.name()))
|
|
218
|
+
return;
|
|
219
|
+
// Someone typing graft by hand learns its new name, once a day. Agents,
|
|
220
|
+
// hooks, CI and pipes never see it (see graftNotice).
|
|
221
|
+
const renamed = graftNotice({ ran: spelling(action), tty: Boolean(process.stderr.isTTY) });
|
|
222
|
+
if (renamed)
|
|
223
|
+
console.error(renamed);
|
|
224
|
+
// Someone typing trail in a repo whose graft-era hooks can't reach it learns
|
|
225
|
+
// the one command that moves it (init itself is that command).
|
|
226
|
+
const stranded = action.name() === "init" ? null : strandedRepoNotice({ cwd: process.cwd(), tty: Boolean(process.stderr.isTTY) });
|
|
227
|
+
if (stranded)
|
|
228
|
+
console.error(stranded);
|
|
229
|
+
maybeRefreshInBackground();
|
|
230
|
+
const nudge = formatUpdateNudge(currentVersion, readUpdateCache()?.latest);
|
|
231
|
+
if (nudge)
|
|
232
|
+
console.error(nudge);
|
|
233
|
+
// Telemetry, in the order a user should experience it: disclose first, then
|
|
234
|
+
// record, then (at most once a day, detached) send. Every step is a no-op in a
|
|
235
|
+
// fork, in CI, under DO_NOT_TRACK, or after `graft telemetry disable`.
|
|
236
|
+
const notice = firstRunNotice();
|
|
237
|
+
if (notice)
|
|
238
|
+
console.error(notice);
|
|
239
|
+
trackFirstRunIfNew();
|
|
240
|
+
maybeFlushInBackground();
|
|
241
|
+
});
|
|
242
|
+
/**
|
|
243
|
+
* The `query` event, emitted after the command rather than before it, so it can
|
|
244
|
+
* carry what the query actually did. A command that exits early via
|
|
245
|
+
* `process.exit` never reaches here and is simply not counted — under-reporting
|
|
246
|
+
* is the right failure mode for a metric.
|
|
247
|
+
*/
|
|
248
|
+
program.hook("postAction", (_parent, action) => {
|
|
249
|
+
// `trail build --check` is graft's `check`, and counts as it always has.
|
|
250
|
+
const name = queryNote.command ?? action.name();
|
|
251
|
+
if (!isTrackedCommand(name))
|
|
252
|
+
return;
|
|
253
|
+
track("query", { command: name, surface: "cli", hit: queryNote.hit, notes: queryNote.notes }, { repo: queryNote.repo });
|
|
254
|
+
});
|
|
255
|
+
/** `graft ask`, `graft build`: a command as graft spells it, whatever ran it. */
|
|
256
|
+
function spelling(action) {
|
|
257
|
+
const words = [];
|
|
258
|
+
for (let c = action; c && c.parent; c = c.parent)
|
|
259
|
+
words.unshift(c.name());
|
|
260
|
+
return ["graft", ...words].join(" ");
|
|
261
|
+
}
|
|
262
|
+
program
|
|
263
|
+
.command("_update-check", { hidden: true })
|
|
264
|
+
.description("internal: refresh the cached latest-version answer")
|
|
265
|
+
.action(() => {
|
|
266
|
+
refreshUpdateCache();
|
|
267
|
+
});
|
|
268
|
+
// Hidden for the same reason as _update-check: only ever spawned detached, by
|
|
269
|
+
// maybeFlushInBackground. Running it by hand is harmless — it drains the queue.
|
|
270
|
+
program
|
|
271
|
+
.command("_telemetry-flush", { hidden: true })
|
|
272
|
+
.description("internal: POST the queued anonymous usage events")
|
|
273
|
+
.action(async () => {
|
|
274
|
+
await runFlush();
|
|
275
|
+
});
|
|
276
|
+
grouped(program.command("telemetry"), GROUP.setup)
|
|
277
|
+
.description("Show, inspect, or turn off the anonymous usage stats (see TELEMETRY.md)")
|
|
278
|
+
.argument("[action]", "status (default) | enable | disable | debug", "status")
|
|
279
|
+
.action((action) => {
|
|
280
|
+
switch (action) {
|
|
281
|
+
case "status":
|
|
282
|
+
console.log(formatStatus());
|
|
283
|
+
return;
|
|
284
|
+
case "enable":
|
|
285
|
+
patchState({ enabled: true });
|
|
286
|
+
console.log(`telemetry: on, anonymous and aggregate-only. \`${cmd("graft telemetry status")}\` for details.`);
|
|
287
|
+
return;
|
|
288
|
+
case "disable":
|
|
289
|
+
// Also stamp the notice as shown: someone who has just opted out should
|
|
290
|
+
// not be told about telemetry again the next time they run a command.
|
|
291
|
+
patchState({ enabled: false, noticeShownAt: new Date().toISOString() });
|
|
292
|
+
console.log("telemetry: off. Nothing further will be recorded or sent.");
|
|
293
|
+
return;
|
|
294
|
+
case "debug":
|
|
295
|
+
console.log(formatDebug());
|
|
296
|
+
return;
|
|
297
|
+
default:
|
|
298
|
+
console.error(`✗ unknown action "${action}": expected status, enable, disable, or debug`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
// Hidden under trail, where -v says the same; listed under graft as it was.
|
|
303
|
+
program
|
|
304
|
+
.command("version", { hidden: TRAIL })
|
|
305
|
+
.description("Print the installed version and the latest published on npm")
|
|
306
|
+
.action(() => {
|
|
307
|
+
const latest = getNpmViewVersion();
|
|
308
|
+
console.log(formatVersionReport(currentVersion, latest));
|
|
309
|
+
});
|
|
310
|
+
grouped(program.command("upgrade"), GROUP.setup)
|
|
311
|
+
.description(`Upgrade the globally installed ${brand()} to the latest version on npm`)
|
|
312
|
+
.action(() => {
|
|
313
|
+
const result = runUpgrade(import.meta.url);
|
|
314
|
+
console.log(formatUpgradeReport(result));
|
|
315
|
+
if (result.ran && !result.ok)
|
|
316
|
+
process.exit(1);
|
|
317
|
+
});
|
|
318
|
+
grouped(program.command("config", { hidden: !TRAIL }), GROUP.setup)
|
|
319
|
+
.description("Read or update workspace or global trail configuration");
|
|
320
|
+
grouped(program.command("build"), GROUP.setup)
|
|
321
|
+
.description(TRAIL
|
|
322
|
+
? "Rebuild the code map in .trail/cache/ ($0, no key). --check fails if it's stale; --deep adds LLM summaries."
|
|
323
|
+
: "Build graft/ from your code: wiring graph + per-file cards ($0, no key). " +
|
|
324
|
+
"Add --deep for the LLM concept map + per-symbol summaries/crux.")
|
|
325
|
+
.argument("[dir]", "repository root", ".")
|
|
326
|
+
.option("--check", "build nothing: fail if graft/ is stale relative to the code (for CI)")
|
|
327
|
+
.option("--json", "with --check: output the drift as JSON")
|
|
328
|
+
.option("--deep", "run the LLM pass: concept nodes (graft/*.md) + per-symbol summary/crux")
|
|
329
|
+
.option("-e, --extensions <exts...>", 'code extensions to include (e.g. ".ts" ".py"); an extension with no parser is ignored with a warning that lists the supported set')
|
|
330
|
+
.option("-j, --concurrency <n>", "files summarized in parallel during --deep (default 5)")
|
|
331
|
+
.option("--synth-concurrency <n>", "concept synthesis batches in flight at once during --deep (default 4). Separate from -j: " +
|
|
332
|
+
"each synthesis call carries a whole batch of summaries, not one file")
|
|
333
|
+
.option("--no-reuse", "re-parse every file instead of replaying unchanged ones from the extraction cache")
|
|
334
|
+
.option("--lsp", "add compiler-grade call edges via a language server if one is installed (opt-in, slower; e.g. rust-analyzer, clangd)")
|
|
335
|
+
.option("--allow-partial", "with --deep: exit 0 even when some files' summaries failed (default: a degraded meaning tier exits 1)")
|
|
336
|
+
.option("--follow-submodules", "include initialized Git submodules recursively; persisted for later builds and automatic refreshes")
|
|
337
|
+
.option("--no-follow-submodules", "exclude Git submodules; persisted for later builds and automatic refreshes (default)")
|
|
338
|
+
.option("--follow-nested-repos", "include nested Git clones the index does not track (a multi-repo manifest checkout, or any repo cloned into the tree) " +
|
|
339
|
+
"as ONE graph, so imports across them resolve; persisted for later builds and automatic refreshes")
|
|
340
|
+
.option("--no-follow-nested-repos", "exclude untracked nested Git clones; persisted for later builds and automatic refreshes (default)")
|
|
341
|
+
.option("--include-dir <name>", "override SKIP_DIRS for this repo's walks, repeatable (e.g. --include-dir build --include-dir tools); " +
|
|
342
|
+
"persisted, so a later build (and the automatic refresh before a query) include it without the flag; dot-dirs are never overridable", (val, prev) => [...prev, val], [])
|
|
343
|
+
.option("--only-dir <path>", "only index files under this repo-relative path, repeatable; the wiring walk and the --deep " +
|
|
344
|
+
"concept pass both honor it. Recorded in the graph fingerprint so a later build " +
|
|
345
|
+
"(and the automatic refresh before a query) walks the same set; everything outside the list is skipped", (val, prev) => [...prev, val], [])
|
|
346
|
+
.option("--no-gitignore", "skip writing graft/ into .gitignore (same as GRAFT_NO_GITIGNORE=1)")
|
|
347
|
+
.option("--no-ignore", "skip writing .ignore for ripgrep re-admit (same as GRAFT_NO_IGNORE=1)")
|
|
348
|
+
.option("--no-semantic", "skip embedding nodes for semantic search after building the graph")
|
|
349
|
+
.option("--include-md", "index .md files as graph nodes (headings → symbols); persisted for later builds and refreshes")
|
|
350
|
+
.option("--no-include-md", "stop indexing .md files (revert a previous --include-md); persisted")
|
|
351
|
+
.option("--md-only-dir <path>", "limit .md indexing to this repo-relative dir, repeatable (e.g. --md-only-dir specs); persisted", (val, prev) => [...prev, val], [])
|
|
352
|
+
.option("--include-openspec-archive", "also index openspec/changes/archive/ .md files (excluded by default); persisted")
|
|
353
|
+
.action(async (dir, opts, command) => {
|
|
354
|
+
if (opts.check) {
|
|
355
|
+
// The freshness check, which was `graft check`. Counted as `check` so the
|
|
356
|
+
// metric carries on across the rename; the dir defaults like a query's.
|
|
357
|
+
queryNote.command = "check";
|
|
358
|
+
await runCheckCommand(command.args[0], { extensions: opts.extensions, json: opts.json }, cmd("graft check"));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const buildStartedAt = Date.now();
|
|
362
|
+
if (opts.gitignore === false)
|
|
363
|
+
process.env.GRAFT_NO_GITIGNORE = "1";
|
|
364
|
+
if (opts.ignore === false)
|
|
365
|
+
process.env.GRAFT_NO_IGNORE = "1";
|
|
366
|
+
const concurrency = opts.concurrency ? Math.max(1, Number(opts.concurrency)) : undefined;
|
|
367
|
+
if (opts.concurrency && !Number.isFinite(concurrency)) {
|
|
368
|
+
console.error(`✗ --concurrency must be a number, got "${opts.concurrency}"`);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
const synthConcurrency = opts.synthConcurrency ? Math.max(1, Number(opts.synthConcurrency)) : undefined;
|
|
372
|
+
if (opts.synthConcurrency && !Number.isFinite(synthConcurrency)) {
|
|
373
|
+
console.error(`✗ --synth-concurrency must be a number, got "${opts.synthConcurrency}"`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
warnUnsupportedExtensions(opts.extensions);
|
|
377
|
+
// Persisted BEFORE the build itself runs, so this invocation's walks (and
|
|
378
|
+
// every later no-flag build / hooks refresh) see it identically — the
|
|
379
|
+
// walkDir call sites read it from state, not from a threaded option.
|
|
380
|
+
const buildConfigPatch = {};
|
|
381
|
+
if (opts.includeDir && opts.includeDir.length > 0) {
|
|
382
|
+
// --include-dir takes bare SKIP_DIRS-style directory NAMES (shouldSkipDir
|
|
383
|
+
// compares a single path segment), never paths, and dot-dirs are never
|
|
384
|
+
// overridable at all (see the option's own help text) — reject anything
|
|
385
|
+
// else up front instead of silently persisting a value that can never
|
|
386
|
+
// match a real directory name.
|
|
387
|
+
for (const name of opts.includeDir) {
|
|
388
|
+
if (name.startsWith(".")) {
|
|
389
|
+
console.error(`✗ --include-dir "${name}": dot-directories are never overridable`);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
if (name.includes("/") || name.includes("\\")) {
|
|
393
|
+
console.error(`✗ --include-dir "${name}": expected a bare directory name, not a path`);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
buildConfigPatch.includeDirs = opts.includeDir;
|
|
398
|
+
}
|
|
399
|
+
// The whitelist is NOT persisted to `.graft/config.json`: it belongs with the
|
|
400
|
+
// graph (the fingerprint records it at build time), never in the source repo,
|
|
401
|
+
// so a `--only-dir` build leaves no trace under the repo being indexed.
|
|
402
|
+
let onlyDirs;
|
|
403
|
+
if (opts.onlyDir && opts.onlyDir.length > 0) {
|
|
404
|
+
// --only-dir takes a repo-relative path prefix, normalized to the same
|
|
405
|
+
// posix, no-`./`, no-trailing-slash form `--in` uses, so the prefix match
|
|
406
|
+
// is exact. A prefix that normalizes to "" (a bare "/" or ".") is rejected:
|
|
407
|
+
// it would mean "match nothing" or "match everything", neither of which is
|
|
408
|
+
// a deliberate whitelist.
|
|
409
|
+
const normalized = opts.onlyDir.map((p) => normalizePathPrefix(p)).filter((p) => p !== "");
|
|
410
|
+
if (normalized.length === 0) {
|
|
411
|
+
console.error("✗ --only-dir: expected a non-empty repo-relative path");
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
onlyDirs = normalized;
|
|
415
|
+
}
|
|
416
|
+
const followSubmodulesWasExplicit = command.getOptionValueSource("followSubmodules") === "cli";
|
|
417
|
+
if (followSubmodulesWasExplicit && typeof opts.followSubmodules === "boolean") {
|
|
418
|
+
buildConfigPatch.followSubmodules = opts.followSubmodules;
|
|
419
|
+
}
|
|
420
|
+
const followNestedReposWasExplicit = command.getOptionValueSource("followNestedRepos") === "cli";
|
|
421
|
+
if (followNestedReposWasExplicit && typeof opts.followNestedRepos === "boolean") {
|
|
422
|
+
buildConfigPatch.followNestedRepos = opts.followNestedRepos;
|
|
423
|
+
}
|
|
424
|
+
const includeMdWasExplicit = command.getOptionValueSource("includeMd") === "cli";
|
|
425
|
+
if (includeMdWasExplicit && typeof opts.includeMd === "boolean") {
|
|
426
|
+
buildConfigPatch.includeMd = opts.includeMd;
|
|
427
|
+
}
|
|
428
|
+
if (opts.mdOnlyDir && opts.mdOnlyDir.length > 0) {
|
|
429
|
+
buildConfigPatch.mdOnlyDirs = opts.mdOnlyDir;
|
|
430
|
+
}
|
|
431
|
+
const includeOpenSpecArchiveWasExplicit = command.getOptionValueSource("includeOpenSpecArchive") === "cli";
|
|
432
|
+
if (includeOpenSpecArchiveWasExplicit && opts.includeOpenSpecArchive !== undefined) {
|
|
433
|
+
buildConfigPatch.includeOpenSpecArchive = opts.includeOpenSpecArchive;
|
|
434
|
+
}
|
|
435
|
+
if (Object.keys(buildConfigPatch).length > 0) {
|
|
436
|
+
patchBuildConfig(resolve(dir), buildConfigPatch);
|
|
437
|
+
}
|
|
438
|
+
const engine = engineFrom();
|
|
439
|
+
const fmt = (o) => Object.entries(o)
|
|
440
|
+
.sort((a, b) => b[1] - a[1])
|
|
441
|
+
.map(([k, n]) => `${n} ${k}`)
|
|
442
|
+
.join(", ");
|
|
443
|
+
// --deep needs a key; without one, degrade to the $0 structural build.
|
|
444
|
+
let deep = opts.deep;
|
|
445
|
+
const resolved = resolveConfig(cliConfig());
|
|
446
|
+
if (deep && !resolved.apiKey) {
|
|
447
|
+
deep = false;
|
|
448
|
+
console.error("⚠ no API key set, so falling back to the structural build (no LLM summaries).\n" +
|
|
449
|
+
" Set GRAFT_API_KEY (and GRAFT_PROVIDER / GRAFT_BASE_URL / GRAFT_MODEL for your\n" +
|
|
450
|
+
` provider) and re-run \`${cmd("graft build --deep")}\` to add concept nodes and summaries.`);
|
|
451
|
+
}
|
|
452
|
+
if (deep && resolved.usedLegacyEnv) {
|
|
453
|
+
console.error("⚠ using OPENROUTER_API_KEY (deprecated): prefer GRAFT_API_KEY + GRAFT_BASE_URL.");
|
|
454
|
+
}
|
|
455
|
+
// Workspace parent: build each child into its OWN graft/ + a workspace index.
|
|
456
|
+
const buildRoot = resolve(dir);
|
|
457
|
+
const buildGlobalDir = program.opts().dir;
|
|
458
|
+
if (isWorkspaceBuildRoot(buildRoot, buildGlobalDir)) {
|
|
459
|
+
await runWorkspaceBuild(buildRoot, {
|
|
460
|
+
deep: !!deep,
|
|
461
|
+
extensions: opts.extensions,
|
|
462
|
+
concurrency,
|
|
463
|
+
synthConcurrency,
|
|
464
|
+
childConfig: cliConfig(),
|
|
465
|
+
override: buildGlobalDir,
|
|
466
|
+
includeDirs: opts.includeDir,
|
|
467
|
+
followSubmodules: followSubmodulesWasExplicit ? opts.followSubmodules : undefined,
|
|
468
|
+
followNestedRepos: followNestedReposWasExplicit ? opts.followNestedRepos : undefined,
|
|
469
|
+
includeMd: includeMdWasExplicit ? opts.includeMd : undefined,
|
|
470
|
+
mdOnlyDirs: opts.mdOnlyDir && opts.mdOnlyDir.length > 0 ? opts.mdOnlyDir : undefined,
|
|
471
|
+
includeOpenSpecArchive: includeOpenSpecArchiveWasExplicit ? opts.includeOpenSpecArchive : undefined,
|
|
472
|
+
packageName: readPackageName(import.meta.url),
|
|
473
|
+
});
|
|
474
|
+
// --semantic: embed each child repo after workspace build.
|
|
475
|
+
if (opts.semantic) {
|
|
476
|
+
const { embedNodes: doEmbedWs } = await import('./ask/semantic.js');
|
|
477
|
+
const { join: pathJoinWs } = await import('node:path');
|
|
478
|
+
const { contextDirFor: ctxDirForWs } = await import('./context/node-file.js');
|
|
479
|
+
const { loadGraphCached: loadGraphWs } = await import('./graph/load.js');
|
|
480
|
+
const ws = readWorkspace(buildRoot, buildGlobalDir);
|
|
481
|
+
if (ws) {
|
|
482
|
+
for (const child of ws.children) {
|
|
483
|
+
const childRoot = pathJoinWs(buildRoot, child);
|
|
484
|
+
const childCtx = ctxDirForWs(childRoot);
|
|
485
|
+
const loaded = loadGraphWs(childCtx);
|
|
486
|
+
if (loaded) {
|
|
487
|
+
const dbPath = pathJoinWs(childCtx, '.cache', 'embeddings.db');
|
|
488
|
+
await doEmbedWs(loaded.nodes, dbPath);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
process.stderr.write('[semantic] Workspace embedding complete\n');
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// --deep: concept nodes first, then the wiring graph links cards up to them.
|
|
497
|
+
let conceptErrors = [];
|
|
498
|
+
let conceptFatal;
|
|
499
|
+
if (deep) {
|
|
500
|
+
const c = await engine.init(dir, {
|
|
501
|
+
extensions: opts.extensions,
|
|
502
|
+
onlyDirs,
|
|
503
|
+
synthConcurrency,
|
|
504
|
+
onProgress: ({ phase, index, total, file }) => process.stderr.write(`\r${phase === "summarize" ? "reading" : "writing"} concepts ${index + 1}/${total}: ${file.slice(0, 40).padEnd(40)}`),
|
|
505
|
+
}).catch((err) => {
|
|
506
|
+
// Only the stage and a code enum; the message stays on this machine.
|
|
507
|
+
track("build_failed", { stage: "summarize", code: errorCode(err) }, { repo: buildRoot });
|
|
508
|
+
throw err;
|
|
509
|
+
});
|
|
510
|
+
process.stderr.write("\n");
|
|
511
|
+
const conceptLine = `concepts: ${c.nodes} nodes, ${c.links} links from ${c.files} files (${c.summarized} read, ${c.cached} cached)`;
|
|
512
|
+
if (c.nodes === 0)
|
|
513
|
+
console.error(`⚠ ${conceptLine}`);
|
|
514
|
+
else
|
|
515
|
+
console.log(`✓ ${conceptLine}`);
|
|
516
|
+
for (const e of c.errors)
|
|
517
|
+
console.error(`✗ ${e}`);
|
|
518
|
+
conceptErrors = c.errors;
|
|
519
|
+
conceptFatal = c.fatal;
|
|
520
|
+
}
|
|
521
|
+
// Wiring graph — always; LLM meaning only with --deep.
|
|
522
|
+
const g = await engine.graph(dir, {
|
|
523
|
+
llm: deep,
|
|
524
|
+
concurrency,
|
|
525
|
+
reuse: opts.reuse,
|
|
526
|
+
lsp: opts.lsp,
|
|
527
|
+
onlyDirs,
|
|
528
|
+
onProgress: ({ phase, index, total, file }) => process.stderr.write(`\r${phase === "enrich" ? "summarizing" : "parsing"} ${index + 1}/${total}: ${file.slice(0, 50).padEnd(50)}`),
|
|
529
|
+
}).catch((err) => {
|
|
530
|
+
track("build_failed", { stage: "graph", code: errorCode(err) }, { repo: buildRoot });
|
|
531
|
+
throw err;
|
|
532
|
+
});
|
|
533
|
+
process.stderr.write("\n");
|
|
534
|
+
console.log(`✓ wiring: ${g.nodes} nodes (${fmt(g.byKind)}), ${g.edges} edges, ${g.cards} cards [${g.languages.join(", ")}]`);
|
|
535
|
+
console.log(` parsed: ${g.parsed} of ${g.files} files (${g.reused} replayed from cache)`);
|
|
536
|
+
// Worth one line: this build started from a graph the user never built *here*.
|
|
537
|
+
if (g.seededFrom)
|
|
538
|
+
console.log(` seeded: copied a starting graph from ${g.seededFrom} (git worktree)`);
|
|
539
|
+
if (deep) {
|
|
540
|
+
const m = g.meaning;
|
|
541
|
+
console.log(` meaning: ${m.computed} computed, ${m.cached} cached, ${m.stale} stale, ${m.pending} pending`);
|
|
542
|
+
}
|
|
543
|
+
console.log(` → ${g.contextDir}`);
|
|
544
|
+
// The activation event. Everything here is a bucket or a fixed label: repo
|
|
545
|
+
// scale rather than a file count, a language set rather than file names.
|
|
546
|
+
track("build_completed", {
|
|
547
|
+
files_bucket: filesBucket(g.files),
|
|
548
|
+
langs: langsValue(g.languages),
|
|
549
|
+
mode: deep ? "deep" : "fast",
|
|
550
|
+
duration_bucket: durationBucket(Date.now() - buildStartedAt),
|
|
551
|
+
incremental: String(g.reused > 0),
|
|
552
|
+
}, { repo: buildRoot });
|
|
553
|
+
for (const e of g.errors)
|
|
554
|
+
console.error(`✗ ${e}`);
|
|
555
|
+
// A language whose depth grammar didn't load was parsed by its breadth stand-in:
|
|
556
|
+
// say which, and the install that fixes it. Nothing when everything loaded.
|
|
557
|
+
const fallbackNotice = nativeFallbackNotice(g.nativeFallback, readPackageName(import.meta.url));
|
|
558
|
+
if (fallbackNotice)
|
|
559
|
+
console.error(fallbackNotice);
|
|
560
|
+
const rel = relative(process.cwd(), g.contextDir) || "graft";
|
|
561
|
+
if (process.env.GRAFT_NO_GITIGNORE) {
|
|
562
|
+
console.log(` ${rel}/ is a local cache: add it to your gitignore if you want it untracked.`);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
console.log(` ${rel}/ is git-ignored (added automatically), a local cache; teammates run \`${cmd("graft build")}\` to get their own.`);
|
|
566
|
+
}
|
|
567
|
+
// --semantic: embed all nodes for semantic search after a successful build.
|
|
568
|
+
if (opts.semantic) {
|
|
569
|
+
const { embedNodes: doEmbed } = await import('./ask/semantic.js');
|
|
570
|
+
const { join: pathJoin } = await import('node:path');
|
|
571
|
+
const { contextDirFor } = await import('./context/node-file.js');
|
|
572
|
+
const { loadGraphCached } = await import('./graph/load.js');
|
|
573
|
+
const contextDir = contextDirFor(buildRoot, program.opts().dir);
|
|
574
|
+
const loaded = loadGraphCached(contextDir);
|
|
575
|
+
if (loaded) {
|
|
576
|
+
const dbPath = pathJoin(contextDir, '.cache', 'embeddings.db');
|
|
577
|
+
await doEmbed(loaded.nodes, dbPath);
|
|
578
|
+
process.stderr.write('[semantic] Embedding complete\n');
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// #127: a --deep run whose LLM calls failed used to print the same success
|
|
582
|
+
// footer and exit 0, so a quota-exhausted build looked identical to a clean
|
|
583
|
+
// one and `graft check` still said "in sync" (it only ever checked Tier-1).
|
|
584
|
+
// The structural graph IS still written and every successful summary is
|
|
585
|
+
// cached, so this is a loud warning about a degraded tier, not a rollback.
|
|
586
|
+
if (deep) {
|
|
587
|
+
const m = g.meaning;
|
|
588
|
+
const failed = m.failedFiles > 0 || m.fatal !== undefined || conceptErrors.length > 0 || conceptFatal !== undefined;
|
|
589
|
+
if (failed) {
|
|
590
|
+
const ready = m.computed + m.cached;
|
|
591
|
+
const total = ready + m.stale + m.pending;
|
|
592
|
+
const pct = total > 0 ? Math.round((ready / total) * 100) : 0;
|
|
593
|
+
console.error("");
|
|
594
|
+
console.error(`✗ the deep pass did not complete, so the meaning tier is incomplete.`);
|
|
595
|
+
if (conceptFatal)
|
|
596
|
+
console.error(` concepts: ${conceptFatal}`);
|
|
597
|
+
if (m.fatal)
|
|
598
|
+
console.error(` summaries: ${m.fatal}`);
|
|
599
|
+
if (m.failedFiles > 0) {
|
|
600
|
+
const skipped = m.skippedFiles > 0 ? `, ${m.skippedFiles} never attempted` : "";
|
|
601
|
+
console.error(` ${m.failedFiles} file(s) failed to summarize${skipped}.`);
|
|
602
|
+
}
|
|
603
|
+
if (conceptErrors.length > 0)
|
|
604
|
+
console.error(` ${conceptErrors.length} concept-pass error(s).`);
|
|
605
|
+
console.error(` meaning coverage: ${ready}/${total} symbols (${pct}%).`);
|
|
606
|
+
console.error(` Nothing computed was lost: re-run \`${cmd("graft build --deep")}\` to resume from what is cached.\n` +
|
|
607
|
+
" Pass --allow-partial to accept a degraded meaning tier and exit 0.");
|
|
608
|
+
if (!opts.allowPartial)
|
|
609
|
+
process.exitCode = 1;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
/**
|
|
614
|
+
* After `ask`, under trail: which shown learnings are a teammate's, and the
|
|
615
|
+
* lookup recorded (the learnings shown are a use, which is what the team page
|
|
616
|
+
* counts as reuse; the tokens saved ride along). Then the background upkeep
|
|
617
|
+
* that keeps branches and the workspace current. Spawns no git itself. Teammates' branches come up after an edit
|
|
618
|
+
* instead, once there's a function to compare (notes/edit-overlap.ts).
|
|
619
|
+
*/
|
|
620
|
+
function afterAsk(r, repo) {
|
|
621
|
+
// Note: learnings tracking removed in trail-solo-grafo.
|
|
622
|
+
void r;
|
|
623
|
+
void repo;
|
|
624
|
+
}
|
|
625
|
+
grouped(program.command("ask"), GROUP.find)
|
|
626
|
+
.description("Query the graft/ graph: returns ranked nodes + exact file:line, routed to prose or wiring ($0, no key)")
|
|
627
|
+
.argument("<query>", "what you want to understand, in plain words (- reads it from stdin)")
|
|
628
|
+
.argument(...DIR_ARG)
|
|
629
|
+
.option("-n, --limit <n>", "max results", "8")
|
|
630
|
+
.option("--source", "inline the source at each file:line hit (retriever mode: the pack IS the answer, no need to re-open files)")
|
|
631
|
+
.option("--full", "with --source: inline whole definition spans instead of the default ≤8-line crux excerpts")
|
|
632
|
+
.option("--in <path>", "narrow to nodes under this path prefix, filtered before scoring (segment-aware, like scopeOf)")
|
|
633
|
+
.option("--json", "output the result as JSON")
|
|
634
|
+
.option("--no-graph-rank", "rank by lexical relevance only, without the graph-connectivity re-rank (ablation/eval)")
|
|
635
|
+
.option("--semantic", "use semantic (embedding) search instead of lexical — finds conceptually related code even without exact keywords; fallback when MCP trail_search_semantic is not available")
|
|
636
|
+
.option(...NO_REFRESH_FLAG)
|
|
637
|
+
.action(async (query, dirArg, opts) => {
|
|
638
|
+
// The prompt hook passes what the person typed on stdin, out of sight of `ps`.
|
|
639
|
+
if (query === "-")
|
|
640
|
+
query = readFileSync(0, "utf8").trim();
|
|
641
|
+
const dir = noteQuery(queryRoot(dirArg));
|
|
642
|
+
await refreshBefore(dir, opts);
|
|
643
|
+
const askGlobalDir = program.opts().dir;
|
|
644
|
+
// --semantic: vector search fallback (equivalent to MCP trail_search_semantic).
|
|
645
|
+
if (opts.semantic) {
|
|
646
|
+
const limit = Number(opts.limit);
|
|
647
|
+
const { searchSemantic } = await import('./ask/semantic.js');
|
|
648
|
+
const { contextDirFor } = await import('./context/node-file.js');
|
|
649
|
+
const ws = readWorkspace(dir, askGlobalDir);
|
|
650
|
+
const results = [];
|
|
651
|
+
const children = ws?.children ?? [];
|
|
652
|
+
if (children.length > 0) {
|
|
653
|
+
// Workspace: search each child's DB.
|
|
654
|
+
const { join: pjoin } = await import('node:path');
|
|
655
|
+
for (const child of children) {
|
|
656
|
+
const childCtx = contextDirFor(join(dir, child));
|
|
657
|
+
const db = pjoin(childCtx, '.cache', 'embeddings.db');
|
|
658
|
+
if (!existsSync(db))
|
|
659
|
+
continue;
|
|
660
|
+
try {
|
|
661
|
+
const hits = await searchSemantic(query, db, limit);
|
|
662
|
+
hits.forEach(h => results.push({ ...h, file_path: h.file_path.startsWith(child) ? h.file_path : `${child}/${h.file_path}` }));
|
|
663
|
+
}
|
|
664
|
+
catch { /* DB not ready */ }
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
// Single repo.
|
|
669
|
+
const ctx = contextDirFor(dir, askGlobalDir);
|
|
670
|
+
const db = join(ctx, '.cache', 'embeddings.db');
|
|
671
|
+
if (existsSync(db)) {
|
|
672
|
+
try {
|
|
673
|
+
(await searchSemantic(query, db, limit)).forEach(h => results.push(h));
|
|
674
|
+
}
|
|
675
|
+
catch { /* DB not ready */ }
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (results.length === 0) {
|
|
679
|
+
console.error('no semantic matches found — run `trail build` to build the semantic index');
|
|
680
|
+
process.exitCode = 1;
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
results.sort((a, b) => b.score - a.score);
|
|
684
|
+
results.slice(0, limit).filter(h => h.score > 0.05).forEach((h, i) => {
|
|
685
|
+
console.log(`${i + 1}. ${h.file_path} · ${h.symbol} (score: ${h.score.toFixed(3)})\n ${h.snippet}`);
|
|
686
|
+
});
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (readWorkspace(dir, askGlobalDir)) {
|
|
690
|
+
runWorkspaceAsk(dir, askGlobalDir, query, {
|
|
691
|
+
limit: Number(opts.limit), source: opts.source, full: opts.full, in: opts.in, json: opts.json,
|
|
692
|
+
});
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const engine = engineFrom();
|
|
696
|
+
let r;
|
|
697
|
+
try {
|
|
698
|
+
r = engine.ask(dir, query, { limit: Number(opts.limit), source: opts.source, full: opts.full, in: opts.in, graphRank: opts.graphRank });
|
|
699
|
+
}
|
|
700
|
+
catch (err) {
|
|
701
|
+
console.error(`✗ ${err instanceof Error ? err.message : String(err)}`);
|
|
702
|
+
process.exit(1);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
noteHit(r.hits.length > 0);
|
|
706
|
+
// Teammates' branches on the same files, and the learnings shown, counted as a use.
|
|
707
|
+
if (TRAIL)
|
|
708
|
+
afterAsk(r, dir);
|
|
709
|
+
if (opts.json) {
|
|
710
|
+
console.log(JSON.stringify(r, null, 2));
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
const { formatAsk } = await import("./ask/ask.js");
|
|
714
|
+
process.stdout.write(formatAsk(r));
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
grouped(program.command("skeleton"), GROUP.find)
|
|
718
|
+
.description("Signatures-only view of one file from the wiring graph: the cheapest way to see a file's API surface")
|
|
719
|
+
.argument("<file>", "repo-relative path (or unique basename) of the file")
|
|
720
|
+
.argument(...DIR_ARG)
|
|
721
|
+
.option("--json", "output the result as JSON")
|
|
722
|
+
.option(...NO_REFRESH_FLAG)
|
|
723
|
+
.action(async (file, dirArg, opts) => {
|
|
724
|
+
const dir = noteQuery(queryRoot(dirArg));
|
|
725
|
+
await refreshBefore(dir, opts);
|
|
726
|
+
const { skeleton, formatSkeleton } = await import("./ask/ask.js");
|
|
727
|
+
const globalOpts = program.opts();
|
|
728
|
+
const r = skeleton(dir, file, { contextDir: globalOpts.dir });
|
|
729
|
+
if (opts.json)
|
|
730
|
+
console.log(JSON.stringify(r, null, 2));
|
|
731
|
+
else
|
|
732
|
+
process.stdout.write(formatSkeleton(r));
|
|
733
|
+
});
|
|
734
|
+
if (!TRAIL) {
|
|
735
|
+
program
|
|
736
|
+
.command("check")
|
|
737
|
+
.description("Fail if graft/ is stale relative to the code (for CI)")
|
|
738
|
+
.argument(...DIR_ARG)
|
|
739
|
+
.option("-e, --extensions <exts...>", "code extensions to include")
|
|
740
|
+
.option("--json", "output the drift as JSON")
|
|
741
|
+
.action(async (dirArg, opts) => {
|
|
742
|
+
await runCheckCommand(dirArg, opts, "graft check");
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
// Under trail, `check` is the team check (teamCheckCommand, with the cloud
|
|
746
|
+
// commands below); the freshness check is `trail build --check`.
|
|
747
|
+
/**
|
|
748
|
+
* The freshness check: `graft check`, and `trail build --check`. `label` is how
|
|
749
|
+
* the report names the command, so each name prints its own spelling.
|
|
750
|
+
*/
|
|
751
|
+
async function runCheckCommand(dirArg, opts, label) {
|
|
752
|
+
warnUnsupportedExtensions(opts.extensions);
|
|
753
|
+
const dir = noteQuery(queryRoot(dirArg));
|
|
754
|
+
const checkGlobalDir = program.opts().dir;
|
|
755
|
+
if (readWorkspace(dir, checkGlobalDir)) {
|
|
756
|
+
await runWorkspaceCheck(dir, checkGlobalDir);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const engine = engineFrom();
|
|
760
|
+
const r = engine.check(dir, { extensions: opts.extensions });
|
|
761
|
+
const g = await engine.checkGraph(dir); // graph.json is only judged when it exists
|
|
762
|
+
// A layer that IS present must be in sync; a never-built layer (keyless
|
|
763
|
+
// build skips the markdown layer) is informational, not a failure.
|
|
764
|
+
const bothMissing = r.missing && g.missing;
|
|
765
|
+
const markdownFail = !r.missing && !r.ok;
|
|
766
|
+
const wiringFail = !g.missing && !g.ok;
|
|
767
|
+
if (opts.json) {
|
|
768
|
+
console.log(JSON.stringify({ context: r, graph: g.missing ? null : g }, null, 2));
|
|
769
|
+
}
|
|
770
|
+
else if (bothMissing) {
|
|
771
|
+
console.log(`${label}: NO GRAPH\n\nNo code map found. Run \`${cmd("graft build")}\` first.`);
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
if (r.missing) {
|
|
775
|
+
console.log(`deep layer: not built (run \`${cmd("graft build --deep")}\` for concept nodes); the wiring graph is the source of truth`);
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
console.log(formatCheckReport(r, label));
|
|
779
|
+
}
|
|
780
|
+
if (!g.missing)
|
|
781
|
+
console.log("\n" + formatGraphCheckReport(g));
|
|
782
|
+
}
|
|
783
|
+
if (bothMissing || markdownFail || wiringFail)
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
// Under trail this is part of `status`; the old name still runs, unlisted.
|
|
787
|
+
program
|
|
788
|
+
.command("stats", { hidden: TRAIL })
|
|
789
|
+
.description(`Show this agent session's ${brand()}-vs-source usage mix and tokens saved`)
|
|
790
|
+
.argument(...DIR_ARG)
|
|
791
|
+
.option("--json", "output the session stats as JSON")
|
|
792
|
+
.action((dirArg, opts) => {
|
|
793
|
+
// Reads local session JSON only — no graph, no network. This is how a Cursor
|
|
794
|
+
// user (no statusline) sees the numbers the Claude Code bar would show, and it
|
|
795
|
+
// works under DO_NOT_TRACK because it never touches telemetry.
|
|
796
|
+
const dir = queryRoot(dirArg);
|
|
797
|
+
const s = latestSession(dir);
|
|
798
|
+
if (opts.json) {
|
|
799
|
+
console.log(JSON.stringify(s, null, 2));
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
console.log(formatSessionStats(s));
|
|
803
|
+
});
|
|
804
|
+
program
|
|
805
|
+
.command("viz", { hidden: TRAIL })
|
|
806
|
+
.description("Serve an interactive visualization of the context graph (and graph.json when present)")
|
|
807
|
+
.argument(...DIR_ARG)
|
|
808
|
+
.option("-p, --port <port>", "port to serve on", "4400")
|
|
809
|
+
.option("--no-open", "don't open the browser")
|
|
810
|
+
.option("--repos <list...>", "workspace: show only these child repos (space or comma separated, e.g. frontend backend)")
|
|
811
|
+
.option("--skip <list...>", "workspace: exclude these child repos (space or comma separated)")
|
|
812
|
+
.option("--include-tests", "include test files in the graph (excluded by default)")
|
|
813
|
+
.option("--export <dir>", "write one self-contained index.html instead of serving (for CI, GitHub Pages, or a build artifact)")
|
|
814
|
+
.option("--title <text>", "subtitle shown beside the repo name in an exported page (e.g. \"PR #151\")")
|
|
815
|
+
.option("--tabs <list>", "tabs the exported page offers, comma separated: context,code,outline (default: all three)")
|
|
816
|
+
.action(async (dirArg, opts) => {
|
|
817
|
+
// Flags are checked before the repository is: a typo'd `--tabs` is a mistake
|
|
818
|
+
// in the command the caller just typed, and telling them to go build an index
|
|
819
|
+
// first sends them off to fix the wrong thing.
|
|
820
|
+
const tabs = parseTabs(opts.tabs);
|
|
821
|
+
const dir = noteQuery(queryRoot(dirArg));
|
|
822
|
+
const { existsSync } = await import("node:fs");
|
|
823
|
+
const { resolve, basename } = await import("node:path");
|
|
824
|
+
const { spawn } = await import("node:child_process");
|
|
825
|
+
const { fileURLToPath } = await import("node:url");
|
|
826
|
+
const { contextDirFor } = await import("./context/node-file.js");
|
|
827
|
+
const { startVizServer } = await import("./viz/serve.js");
|
|
828
|
+
const root = resolve(dir);
|
|
829
|
+
const globalOpts = program.opts();
|
|
830
|
+
let contextDir = contextDirFor(root, globalOpts.dir);
|
|
831
|
+
let federatedWiringPath = null;
|
|
832
|
+
// Workspace: federate all children's wiring.json into a single in-memory file for viz.
|
|
833
|
+
const { readWorkspace: readWs } = await import('./graph/workspace.js');
|
|
834
|
+
const ws = readWs(root);
|
|
835
|
+
if (ws && ws.children.length > 0) {
|
|
836
|
+
const { join: pj } = await import('node:path');
|
|
837
|
+
const { writeFileSync, mkdirSync, readFileSync: rfs } = await import('node:fs');
|
|
838
|
+
// Apply --only / --exclude filters.
|
|
839
|
+
const onlySet = opts.repos?.length ? new Set(opts.repos.flatMap((s) => s.split(/[,\s]+/)).map((s) => s.trim()).filter(Boolean)) : null;
|
|
840
|
+
const excludeSet = opts.skip?.length ? new Set(opts.skip.flatMap((s) => s.split(/[,\s]+/)).map((s) => s.trim()).filter(Boolean)) : null;
|
|
841
|
+
// [DEBUG removed]
|
|
842
|
+
const children = ws.children.filter(c => {
|
|
843
|
+
const name = c.replace(/\//g, '/'); // keep as-is for matching
|
|
844
|
+
if (onlySet && !onlySet.has(c) && !onlySet.has(c.split('/').pop()))
|
|
845
|
+
return false;
|
|
846
|
+
if (excludeSet && (excludeSet.has(c) || excludeSet.has(c.split('/').pop())))
|
|
847
|
+
return false;
|
|
848
|
+
return true;
|
|
849
|
+
});
|
|
850
|
+
if (onlySet || excludeSet)
|
|
851
|
+
console.error(`· workspace viz: showing ${children.length}/${ws.children.length} repos: ${children.join(', ')}`);
|
|
852
|
+
const allNodes = [];
|
|
853
|
+
const allEdges = [];
|
|
854
|
+
const allLangs = new Set();
|
|
855
|
+
let childrenWithGraph = 0;
|
|
856
|
+
for (const child of children) {
|
|
857
|
+
const childCtx = contextDirFor(pj(root, child));
|
|
858
|
+
const wiringPath = pj(childCtx, '.graph', 'wiring.json');
|
|
859
|
+
if (!existsSync(wiringPath))
|
|
860
|
+
continue;
|
|
861
|
+
try {
|
|
862
|
+
const w = JSON.parse(rfs(wiringPath, 'utf8'));
|
|
863
|
+
if (w?.meta?.version !== 1)
|
|
864
|
+
continue;
|
|
865
|
+
const isTestPath = (p) => /\.(test|spec)\.[jt]sx?$|[\\/](__tests__|tests?|specs?)[\\/]|\.Tests\.cs$/i.test(p);
|
|
866
|
+
const filterNode = (n) => opts.includeTests || !isTestPath(n.path ?? n.id ?? '');
|
|
867
|
+
const filteredNodes = (w.nodes ?? []).filter(filterNode);
|
|
868
|
+
const filteredIds = new Set(filteredNodes.map((n) => `${child}/${n.id}`));
|
|
869
|
+
// Prefix node ids and paths with child name to avoid collisions.
|
|
870
|
+
for (const n of filteredNodes)
|
|
871
|
+
allNodes.push({ ...n, id: `${child}/${n.id}`, path: n.path ? `${child}/${n.path}` : n.path });
|
|
872
|
+
for (const e of (w.edges ?? [])) {
|
|
873
|
+
const src = `${child}/${e.source}`, tgt = `${child}/${e.target}`;
|
|
874
|
+
if (filteredIds.has(src) && filteredIds.has(tgt))
|
|
875
|
+
allEdges.push({ ...e, source: src, target: tgt });
|
|
876
|
+
}
|
|
877
|
+
for (const l of (w.meta?.languages ?? []))
|
|
878
|
+
allLangs.add(l);
|
|
879
|
+
childrenWithGraph++;
|
|
880
|
+
}
|
|
881
|
+
catch { /* skip broken child */ }
|
|
882
|
+
}
|
|
883
|
+
if (childrenWithGraph > 0) {
|
|
884
|
+
const federated = { meta: { version: 1, nodeCount: allNodes.length, edgeCount: allEdges.length, languages: [...allLangs] }, nodes: allNodes, edges: allEdges };
|
|
885
|
+
const tmpDir = pj(contextDir, '.graph');
|
|
886
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
887
|
+
// Write as wiring.json so startVizServer finds it at the expected path (Code tab).
|
|
888
|
+
const tmpPath = pj(tmpDir, 'wiring.json');
|
|
889
|
+
writeFileSync(tmpPath, JSON.stringify(federated));
|
|
890
|
+
federatedWiringPath = tmpPath;
|
|
891
|
+
// Federation of context cards (.md) for the Context tab.
|
|
892
|
+
const { readdirSync, copyFileSync } = await import('node:fs');
|
|
893
|
+
for (const child of children) {
|
|
894
|
+
const childCtx = contextDirFor(pj(root, child));
|
|
895
|
+
if (!existsSync(childCtx))
|
|
896
|
+
continue;
|
|
897
|
+
try {
|
|
898
|
+
for (const f of readdirSync(childCtx)) {
|
|
899
|
+
if (!f.endsWith('.md'))
|
|
900
|
+
continue;
|
|
901
|
+
copyFileSync(pj(childCtx, f), pj(contextDir, `${child.replace(/\//g, '__')}__${f}`));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
catch { /* skip */ }
|
|
905
|
+
}
|
|
906
|
+
console.error(`· workspace viz: federating ${childrenWithGraph} repos (${allNodes.length} nodes, ${allEdges.length} edges)`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (!existsSync(contextDir)) {
|
|
910
|
+
console.error(`✗ no context graph at ${contextDir}: run \`${cmd("graft build --deep")}\` first`);
|
|
911
|
+
process.exit(1);
|
|
912
|
+
}
|
|
913
|
+
const viewerDir = fileURLToPath(new URL("./viewer/", import.meta.url)); // prebuilt
|
|
914
|
+
// Filter tests from wiring.json unless --include-tests (applies to single repo and workspace).
|
|
915
|
+
const isTestPath = (p) => /\.(test|spec)\.[jt]sx?$|[\\/](__tests__|tests?|specs?)[\\/]|\.Tests\.cs$/i.test(p ?? '');
|
|
916
|
+
let wiringOverride;
|
|
917
|
+
if (!opts.includeTests) {
|
|
918
|
+
const { readFileSync: rfs2, writeFileSync: wfs2, mkdirSync: mks2 } = await import('node:fs');
|
|
919
|
+
const { join: pj2 } = await import('node:path');
|
|
920
|
+
const srcWiring = pj2(contextDir, '.graph', 'wiring.json');
|
|
921
|
+
if (existsSync(srcWiring)) {
|
|
922
|
+
try {
|
|
923
|
+
const w = JSON.parse(rfs2(srcWiring, 'utf8'));
|
|
924
|
+
if (w?.meta?.version === 1) {
|
|
925
|
+
const filteredNodes = (w.nodes ?? []).filter((n) => !isTestPath(n.path ?? n.id ?? ''));
|
|
926
|
+
const filteredIds = new Set(filteredNodes.map((n) => n.id));
|
|
927
|
+
const filteredEdges = (w.edges ?? []).filter((e) => filteredIds.has(e.source) && filteredIds.has(e.target));
|
|
928
|
+
const filtered = { ...w, meta: { ...w.meta, nodeCount: filteredNodes.length, edgeCount: filteredEdges.length }, nodes: filteredNodes, edges: filteredEdges };
|
|
929
|
+
const tmpPath = pj2(contextDir, '.graph', 'wiring-viz.json');
|
|
930
|
+
mks2(pj2(contextDir, '.graph'), { recursive: true });
|
|
931
|
+
wfs2(tmpPath, JSON.stringify(filtered));
|
|
932
|
+
wiringOverride = tmpPath;
|
|
933
|
+
const removed = (w.nodes ?? []).length - filteredNodes.length;
|
|
934
|
+
if (removed > 0)
|
|
935
|
+
console.error(`· viz: ${removed} test nodes hidden (use --include-tests to show)`);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
catch { /* skip — serve original */ }
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
if (opts.export) {
|
|
942
|
+
const { exportViz } = await import("./viz/export.js");
|
|
943
|
+
const out = exportViz({
|
|
944
|
+
contextDir,
|
|
945
|
+
viewerDir,
|
|
946
|
+
outDir: resolve(opts.export),
|
|
947
|
+
repoName: basename(root),
|
|
948
|
+
subtitle: opts.title,
|
|
949
|
+
tabs,
|
|
950
|
+
});
|
|
951
|
+
const kb = Math.round(out.bytes / 1024);
|
|
952
|
+
console.log(`${cmd("graft viz")} → ${out.file} (${kb} kB, ${out.contextNodes} concept nodes, ${out.codeNodes} code nodes)`);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const srv = await startVizServer({
|
|
956
|
+
contextDir,
|
|
957
|
+
viewerDir,
|
|
958
|
+
port: Number(opts.port),
|
|
959
|
+
repoName: basename(root),
|
|
960
|
+
wiringOverride,
|
|
961
|
+
});
|
|
962
|
+
console.log(`${cmd("graft viz")} → ${srv.url} (ctrl-c to stop)`);
|
|
963
|
+
if (opts.open) {
|
|
964
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
965
|
+
spawn(opener, [srv.url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
program
|
|
969
|
+
.command("mcp", { hidden: TRAIL })
|
|
970
|
+
.description("Serve the graph over MCP (stdio): exposes graft_find_code, graft_trace_calls, graft_find_all, graft_file_api, graft_repo_map, graft_check_freshness and graft_search_semantic as tools")
|
|
971
|
+
.argument(...DIR_ARG)
|
|
972
|
+
.option("--brains", "serve only the paired Trail workspace's tools and skills (what Claude Code gets after init --connect)")
|
|
973
|
+
.action(async (dirArg, opts) => {
|
|
974
|
+
const { startMcpServer } = await import("./mcp/server.js");
|
|
975
|
+
if (opts.brains) {
|
|
976
|
+
startMcpServer(process.cwd(), undefined, currentVersion, { brainsOnly: true });
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
const dir = noteQuery(queryRoot(dirArg));
|
|
980
|
+
const globalOpts = program.opts();
|
|
981
|
+
startMcpServer(dir, globalOpts.dir, currentVersion);
|
|
982
|
+
});
|
|
983
|
+
grouped(program.command("callers"), GROUP.find)
|
|
984
|
+
.description("Who calls/references a symbol ($0, no LLM). --direction out gives callees (what it calls); --depth N (or all) walks transitively for full blast radius")
|
|
985
|
+
.argument("<symbol>", "bare name, qualified (Class.method), or package-qualified (pkg.Fn)")
|
|
986
|
+
.argument(...DIR_ARG)
|
|
987
|
+
.option("--direction <in|out>", 'edge direction: "in" = callers (default), "out" = callees')
|
|
988
|
+
.option("-d, --depth <n>", 'walk transitively up to N hops for blast radius, or "all" for the full connected closure (default 1)')
|
|
989
|
+
.option("--in <path>", "narrow matches to nodes at or under this path prefix")
|
|
990
|
+
.option("--json", "output as JSON")
|
|
991
|
+
.option(...NO_REFRESH_FLAG)
|
|
992
|
+
.action(async (symbol, dirArg, opts) => {
|
|
993
|
+
const dir = noteQuery(queryRoot(dirArg));
|
|
994
|
+
await refreshBefore(dir, opts);
|
|
995
|
+
const globalOpts = program.opts();
|
|
996
|
+
if (!opts.json && readWorkspace(dir, globalOpts.dir)) {
|
|
997
|
+
runWorkspaceCallers(dir, globalOpts.dir, symbol, {
|
|
998
|
+
direction: opts.direction === "out" ? "out" : "in",
|
|
999
|
+
depth: opts.depth
|
|
1000
|
+
? (/^(all|full|max)$/i.test(opts.depth) ? Number.POSITIVE_INFINITY : Number(opts.depth))
|
|
1001
|
+
: undefined,
|
|
1002
|
+
in: opts.in,
|
|
1003
|
+
});
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const { runCallersCommand } = await import("./graph/traverse-cli.js");
|
|
1007
|
+
runCallersCommand(symbol, dir, {
|
|
1008
|
+
direction: opts.direction,
|
|
1009
|
+
depth: opts.depth,
|
|
1010
|
+
in: opts.in,
|
|
1011
|
+
json: opts.json,
|
|
1012
|
+
globalDir: globalOpts.dir,
|
|
1013
|
+
});
|
|
1014
|
+
});
|
|
1015
|
+
grouped(program.command("blast"), GROUP.ci)
|
|
1016
|
+
.description("Blast radius of a diff: what depends on the lines this change touched ($0, no LLM). " +
|
|
1017
|
+
"Built for CI: `--format markdown` is a PR comment with a Mermaid diagram.")
|
|
1018
|
+
.argument(...DIR_ARG)
|
|
1019
|
+
.option("--base <ref>", "diff against this ref's merge base with HEAD (e.g. origin/main); default: the working tree vs HEAD")
|
|
1020
|
+
.option("-d, --depth <n>", 'hops to walk over incoming edges, or "all" for the full closure (default 2)')
|
|
1021
|
+
.option("--format <fmt>", "text (default) | markdown | mermaid | json")
|
|
1022
|
+
.option("--name", "name the affected areas with one cached LLM call (needs GRAFT_API_KEY); without it, areas are named after their hub symbol")
|
|
1023
|
+
.option("--export-viz <dir>", "also write the interactive page for this radius (one self-contained index.html, for CI, GitHub Pages, or an artifact)")
|
|
1024
|
+
.option("--title <text>", "subtitle beside the repo name on the exported page (e.g. \"PR #171\")")
|
|
1025
|
+
.option("--no-owners", "do not suggest who to tag (by default, git history names the people behind each affected area)")
|
|
1026
|
+
.option("--pr-author <who...>", "GitHub login, git name or email of the PR author, so they are left out of their own suggestions")
|
|
1027
|
+
.option(...NO_REFRESH_FLAG)
|
|
1028
|
+
.action(async (dirArg, opts) => {
|
|
1029
|
+
const dir = noteQuery(queryRoot(dirArg));
|
|
1030
|
+
await refreshBefore(dir, opts);
|
|
1031
|
+
const { runBlastCommand } = await import("./blast/blast-cli.js");
|
|
1032
|
+
await runBlastCommand(dir, {
|
|
1033
|
+
base: opts.base,
|
|
1034
|
+
depth: opts.depth,
|
|
1035
|
+
format: opts.format,
|
|
1036
|
+
name: opts.name,
|
|
1037
|
+
exportViz: opts.exportViz,
|
|
1038
|
+
title: opts.title,
|
|
1039
|
+
owners: opts.owners,
|
|
1040
|
+
prAuthor: opts.prAuthor,
|
|
1041
|
+
globalDir: program.opts().dir,
|
|
1042
|
+
});
|
|
1043
|
+
});
|
|
1044
|
+
grouped(program.command("grep"), GROUP.find)
|
|
1045
|
+
.description("Regex search over indexed files, hits grouped by enclosing symbol and ranked by coupling ($0, no LLM)")
|
|
1046
|
+
.argument("<pattern>", "regex pattern (or literal string with --fixed)")
|
|
1047
|
+
.argument(...DIR_ARG)
|
|
1048
|
+
.option("-i, --ignore-case", "case-insensitive match")
|
|
1049
|
+
.option("--fixed", "treat pattern as a literal string, not a regex")
|
|
1050
|
+
.option("--in <path>", "narrow to files at or under this path prefix")
|
|
1051
|
+
.option("--json", "output as JSON")
|
|
1052
|
+
.option(...NO_REFRESH_FLAG)
|
|
1053
|
+
.action(async (pattern, dirArg, opts) => {
|
|
1054
|
+
const dir = noteQuery(queryRoot(dirArg));
|
|
1055
|
+
await refreshBefore(dir, opts);
|
|
1056
|
+
const globalOpts = program.opts();
|
|
1057
|
+
if (readWorkspace(dir, globalOpts.dir)) {
|
|
1058
|
+
runWorkspaceGrep(dir, globalOpts.dir, pattern, {
|
|
1059
|
+
ignoreCase: opts.ignoreCase, fixed: opts.fixed, json: opts.json,
|
|
1060
|
+
});
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
const { runGrepCommand } = await import("./search/grep-cli.js");
|
|
1064
|
+
runGrepCommand(pattern, dir, {
|
|
1065
|
+
ignoreCase: opts.ignoreCase,
|
|
1066
|
+
fixed: opts.fixed,
|
|
1067
|
+
in: opts.in,
|
|
1068
|
+
json: opts.json,
|
|
1069
|
+
globalDir: globalOpts.dir,
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
grouped(program.command("map"), GROUP.find)
|
|
1073
|
+
.description("Token-budgeted repo orientation: directory clusters, per-directory hubs, and global hotspots from the wiring graph ($0, no LLM)")
|
|
1074
|
+
.argument(...DIR_ARG)
|
|
1075
|
+
.option("--max-dirs <n>", "max directory entries shown, rest counted into dropped (default 16)")
|
|
1076
|
+
.option("--json", "output as JSON")
|
|
1077
|
+
.option(...NO_REFRESH_FLAG)
|
|
1078
|
+
.action(async (dirArg, opts) => {
|
|
1079
|
+
const dir = noteQuery(queryRoot(dirArg));
|
|
1080
|
+
const root = resolve(dir);
|
|
1081
|
+
const globalOpts = program.opts();
|
|
1082
|
+
let maxDirsW;
|
|
1083
|
+
if (opts.maxDirs !== undefined) {
|
|
1084
|
+
const n = parseInt(opts.maxDirs, 10);
|
|
1085
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
1086
|
+
console.error(`✗ --max-dirs must be a positive integer, got "${opts.maxDirs}"`);
|
|
1087
|
+
process.exit(1);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
maxDirsW = n;
|
|
1091
|
+
}
|
|
1092
|
+
await refreshBefore(dir, opts); // after arg validation: a bad flag shouldn't cost a rebuild
|
|
1093
|
+
if (!opts.json && readWorkspace(root, globalOpts.dir)) {
|
|
1094
|
+
runWorkspaceMap(root, globalOpts.dir, { maxDirs: maxDirsW });
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
const { buildRepoMap, formatRepoMap } = await import("./graph/map.js");
|
|
1098
|
+
const contextDir = contextDirFor(root, globalOpts.dir);
|
|
1099
|
+
const graph = loadGraphCached(contextDir);
|
|
1100
|
+
if (!graph) {
|
|
1101
|
+
console.error(`✗ no graph: run ${cmd("graft build")} first`);
|
|
1102
|
+
process.exit(1);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
const map = buildRepoMap(graph, { maxDirs: maxDirsW });
|
|
1106
|
+
if (opts.json) {
|
|
1107
|
+
console.log(JSON.stringify(map, null, 2));
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
process.stdout.write(formatRepoMap(map));
|
|
1111
|
+
});
|
|
1112
|
+
// `trail note`: what a session worked out, saved for the next person. Listed
|
|
1113
|
+
// under trail; under graft it still runs, unlisted, for a teammate on graft in
|
|
1114
|
+
// a repo where someone else set up trail.
|
|
1115
|
+
/** Lines that close a command's output, with a blank line before them. */
|
|
1116
|
+
function printLines(lines) {
|
|
1117
|
+
if (lines.length)
|
|
1118
|
+
console.log(`\n${lines.join("\n")}`);
|
|
1119
|
+
}
|
|
1120
|
+
/** Repo-relative with forward slashes, for printing. */
|
|
1121
|
+
function shown(repo, path) {
|
|
1122
|
+
return relative(repo, path).split("\\").join("/");
|
|
1123
|
+
}
|
|
1124
|
+
/** Agents init knows by another id: both read AGENTS.md. */
|
|
1125
|
+
const AGENT_ALIASES = { codex: "agents", opencode: "agents" };
|
|
1126
|
+
/** `graft init`. */
|
|
1127
|
+
async function runInitCommand(dir, opts, progress) {
|
|
1128
|
+
if (opts.listAgents) {
|
|
1129
|
+
for (const id of [...hostIds(), "claude"])
|
|
1130
|
+
console.log(id);
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
const repo = resolve(dir);
|
|
1134
|
+
// Codex and OpenCode read AGENTS.md: `--agents codex` means that wiring.
|
|
1135
|
+
// The name as given is kept for Trail's onboarding page.
|
|
1136
|
+
const asked = Array.isArray(opts.agents) ? opts.agents : undefined;
|
|
1137
|
+
// `--agents auto`: the website's one setup prompt, run by an agent with no one to ask.
|
|
1138
|
+
const auto = asked?.includes("auto") === true;
|
|
1139
|
+
if (auto && asked.length > 1) {
|
|
1140
|
+
console.error("✗ --agents auto picks the agents itself: give it on its own, or name the agents instead");
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
const explicit = asked && !auto ? [...new Set(asked.map((id) => AGENT_ALIASES[id] ?? id))] : undefined;
|
|
1144
|
+
// Checked before anything is written: run from the home folder, init wired
|
|
1145
|
+
// it, mapped every code file under it, then said to commit.
|
|
1146
|
+
const refusal = homeFolderRefusal(repo);
|
|
1147
|
+
if (refusal) {
|
|
1148
|
+
console.error(refusal);
|
|
1149
|
+
process.exitCode = 1;
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
// Trail's page shares a repo with the team: removed in trail-solo-grafo.
|
|
1153
|
+
if (explicit) {
|
|
1154
|
+
const validIds = [...hostIds(), "claude"];
|
|
1155
|
+
const unknown = explicit.filter((id) => !validIds.includes(id));
|
|
1156
|
+
if (unknown.length) {
|
|
1157
|
+
console.error(`✗ unknown agent id(s): ${unknown.join(", ")}; valid: ${validIds.join(", ")}`);
|
|
1158
|
+
process.exit(1);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
// Which agents to wire, decided before anything is written. Explicit flags
|
|
1162
|
+
// win; otherwise prompt on a TTY, and on a pipe write nothing rather than
|
|
1163
|
+
// guessing (pre-0.8 this silently wired every agent the machine had ever
|
|
1164
|
+
// installed — see --yes to get that back).
|
|
1165
|
+
const home = homedir();
|
|
1166
|
+
const plan = planInit(repo, { home });
|
|
1167
|
+
const detectedIds = plan.filter((p) => p.detected).map((p) => p.id);
|
|
1168
|
+
const noAgents = opts.agents === false;
|
|
1169
|
+
// auto: the main agents' repo files plus the laptop's; out-of-repo writes only for `laptop`, the agents installed here and the caller.
|
|
1170
|
+
const autoSel = auto ? autoSelection({ home, repo }) : null;
|
|
1171
|
+
const laptop = autoSel?.laptop;
|
|
1172
|
+
let ids;
|
|
1173
|
+
// The consent answer from the picker. Undefined everywhere else — a scripted
|
|
1174
|
+
// or flag-driven init never asked, so it must not silently answer.
|
|
1175
|
+
let consent;
|
|
1176
|
+
if (autoSel)
|
|
1177
|
+
ids = autoSel.repo;
|
|
1178
|
+
else if (explicit)
|
|
1179
|
+
ids = explicit;
|
|
1180
|
+
else if (opts.allAgents)
|
|
1181
|
+
ids = plan.map((p) => p.id);
|
|
1182
|
+
else if (noAgents)
|
|
1183
|
+
ids = ["claude"];
|
|
1184
|
+
else if (opts.yes || opts.dryRun)
|
|
1185
|
+
ids = detectedIds;
|
|
1186
|
+
else if (process.stdin.isTTY && process.stderr.isTTY) {
|
|
1187
|
+
// Only offer the row when telemetry could actually run. `disabled` still
|
|
1188
|
+
// counts: someone who turned it off should be able to turn it back on here.
|
|
1189
|
+
const reason = offReason();
|
|
1190
|
+
const picked = await runPicker(plan, repo, home, {
|
|
1191
|
+
offerTelemetry: reason === null || reason === "disabled",
|
|
1192
|
+
});
|
|
1193
|
+
if (picked === null) {
|
|
1194
|
+
console.error("· cancelled, nothing written");
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
ids = picked.hosts;
|
|
1198
|
+
consent = picked.telemetry;
|
|
1199
|
+
}
|
|
1200
|
+
else {
|
|
1201
|
+
console.error(formatNonInteractiveHelp(detectedIds));
|
|
1202
|
+
return null;
|
|
1203
|
+
}
|
|
1204
|
+
// The picker's answer, recorded before anything is wired: a user who
|
|
1205
|
+
// unchecked the row must not have this run's init_completed sent.
|
|
1206
|
+
if (consent !== undefined) {
|
|
1207
|
+
patchState({ enabled: consent, noticeShownAt: new Date().toISOString() });
|
|
1208
|
+
}
|
|
1209
|
+
// Knowledge repo prompt removed in trail-solo-grafo.
|
|
1210
|
+
// Workspace parent: every child repo gets its OWN wiring too.
|
|
1211
|
+
// opens at a repo root, not at the parent, and reads `.claude/` from there —
|
|
1212
|
+
// wiring only the parent leaves each child with no skill, hooks, or MCP.
|
|
1213
|
+
// The parent's own wiring stays (queries there federate across children).
|
|
1214
|
+
// Knowledge repo cloning removed in trail-solo-grafo.
|
|
1215
|
+
const children = isWorkspaceBuildRoot(repo, program.opts().dir)
|
|
1216
|
+
? discoverWorkspaceChildren(repo)
|
|
1217
|
+
: [];
|
|
1218
|
+
// Parent FIRST: its build is the workspace build, which builds every child's
|
|
1219
|
+
// graph, so each child's own `buildGraphIfMissing` then finds one and no-ops.
|
|
1220
|
+
const targets = [repo, ...children.map((c) => join(repo, c))];
|
|
1221
|
+
if (opts.dryRun) {
|
|
1222
|
+
if (autoSel)
|
|
1223
|
+
console.error(`· --agents auto: repo files for Claude Code, Cursor, Codex and OpenCode; laptop settings for ${autoSel.agents.length ? agentList(autoSel.agents) : "none (no agent found here)"}\n`);
|
|
1224
|
+
console.error(formatPlan(laptopOnly(plan, laptop), ids, repo, home));
|
|
1225
|
+
for (const child of children)
|
|
1226
|
+
console.error(`\n· ${child}/ (workspace child)\n` + formatPlan(laptopOnly(planInit(join(repo, child), { home }), laptop), ids, join(repo, child), home));
|
|
1227
|
+
return null;
|
|
1228
|
+
}
|
|
1229
|
+
if (ids.length === 0) {
|
|
1230
|
+
console.error("· no agents selected, nothing written");
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
const wantClaude = ids.includes("claude");
|
|
1234
|
+
const cliPath = fileURLToPath(import.meta.url);
|
|
1235
|
+
// Say it out loud: wiring a workspace parent writes into every child repo, and
|
|
1236
|
+
// silence here reads as "only the parent was touched".
|
|
1237
|
+
if (children.length > 0)
|
|
1238
|
+
console.error(`· workspace: wiring ${repo} and ${children.length} child repo(s): ${children.join(", ")}`);
|
|
1239
|
+
// Wire each target repo.
|
|
1240
|
+
for (const target of targets) {
|
|
1241
|
+
const targetPlan = target === repo ? plan : planInit(target, { home });
|
|
1242
|
+
const wr = wireTarget(target, ids, {
|
|
1243
|
+
home,
|
|
1244
|
+
cliPath,
|
|
1245
|
+
plan: targetPlan,
|
|
1246
|
+
wantClaude,
|
|
1247
|
+
opts: { build: opts.build, mcp: opts.mcp, global: opts.global },
|
|
1248
|
+
quiet: targets.length > 1,
|
|
1249
|
+
laptop,
|
|
1250
|
+
});
|
|
1251
|
+
for (const w of wr.warnings)
|
|
1252
|
+
console.error(w);
|
|
1253
|
+
}
|
|
1254
|
+
// Build graph (builds all targets in workspace mode).
|
|
1255
|
+
if (opts.build !== false) {
|
|
1256
|
+
const { buildGraphWithProgress: bgp } = await import('./claude/build-progress.js');
|
|
1257
|
+
await bgp(repo, { build: opts.build, cliPath, childCount: children.length });
|
|
1258
|
+
}
|
|
1259
|
+
// teamSaved/LEARNINGS_DIR removed in trail-solo-grafo.
|
|
1260
|
+
// Sorted so `claude,cursor` and `cursor,claude` aggregate as one value.
|
|
1261
|
+
track("init_completed", { agents: [...ids].sort().join(","), consent: consent === undefined ? "unasked" : String(consent) }, { repo });
|
|
1262
|
+
return { ids, laptop: laptop ?? ids };
|
|
1263
|
+
}
|
|
1264
|
+
/** A plan with the out-of-repo writes only for `laptop`'s hosts; the plan itself when every host may write there. */
|
|
1265
|
+
function laptopOnly(plan, laptop) {
|
|
1266
|
+
if (!laptop)
|
|
1267
|
+
return plan;
|
|
1268
|
+
return plan.map((p) => (laptop.includes(p.id) ? p : { ...p, writes: p.writes.filter((w) => w.scope === "repo") }));
|
|
1269
|
+
}
|
|
1270
|
+
/** One repo's worth of `init` writes — the parent, then each workspace child. */
|
|
1271
|
+
function wireTarget(repo, ids, ctx) {
|
|
1272
|
+
const { home, cliPath, plan, wantClaude } = ctx;
|
|
1273
|
+
const quiet = ctx.quiet === true;
|
|
1274
|
+
const opts = quiet ? { ...ctx.opts, build: false } : ctx.opts;
|
|
1275
|
+
const report = { retracted: [], warnings: [] };
|
|
1276
|
+
// Every per-file line goes through here; in quiet mode only ⚠ lines survive.
|
|
1277
|
+
const say = (line) => {
|
|
1278
|
+
if (!quiet)
|
|
1279
|
+
console.error(line);
|
|
1280
|
+
else if (line.startsWith("⚠"))
|
|
1281
|
+
report.warnings.push(line);
|
|
1282
|
+
};
|
|
1283
|
+
const wantStatusline = statuslineWanted();
|
|
1284
|
+
// Converge, don't just add. init writes the selected hosts; without this it
|
|
1285
|
+
// never touches the rest, so a repo wired by an older version (or by the same
|
|
1286
|
+
// version with different --agents) keeps that run's files forever — and
|
|
1287
|
+
// `reconcileWiring` then keeps them *up to date*, which is worse than stale.
|
|
1288
|
+
// Retract every host NOT being written now; `exclude` spares the ones about to
|
|
1289
|
+
// be rewritten, and the graph cache is kept (init is one step from using it).
|
|
1290
|
+
const retracted = changed(runRetract(repo, { home, apply: true, global: opts.global, cache: false, exclude: ids })).filter((r) => r.action !== "skipped-unparseable");
|
|
1291
|
+
for (const r of retracted)
|
|
1292
|
+
say(`- removed ${r.path} (${r.what}): agent not selected`);
|
|
1293
|
+
report.retracted = [...new Set(retracted.map((r) => r.hostId))];
|
|
1294
|
+
if (wantClaude) {
|
|
1295
|
+
// `global`/`home` are threaded through alongside `statusline`: the claude layer
|
|
1296
|
+
// writes under `~/.claude` now (hosts/claude-global.ts), so --no-global has to
|
|
1297
|
+
// reach it or the flag would silently mean "no out-of-repo writes, except three".
|
|
1298
|
+
const claudeGlobal = opts.global !== false && (!ctx.laptop || ctx.laptop.includes("claude"));
|
|
1299
|
+
const res = runInit(repo, { build: opts.build, cliPath, statusline: wantStatusline, global: claudeGlobal, home, brand: brand() });
|
|
1300
|
+
if (res.settingsPath)
|
|
1301
|
+
say(`✓ wrote ${res.settingsPath}`);
|
|
1302
|
+
for (const s of res.shims)
|
|
1303
|
+
say(`✓ wrote ${s}`);
|
|
1304
|
+
if (res.skill)
|
|
1305
|
+
say(`✓ wrote ${res.skill}`);
|
|
1306
|
+
if (!res.mcp) {
|
|
1307
|
+
// trail registers no server for Claude Code: the hooks and the `trail` command cover it.
|
|
1308
|
+
}
|
|
1309
|
+
else if (res.mcp.action === "skipped-unparseable")
|
|
1310
|
+
say(`⚠ .mcp.json: ${res.mcp.path} left unchanged (not valid JSON); add the graft server manually`);
|
|
1311
|
+
else if (res.mcp.action === "unchanged")
|
|
1312
|
+
say(`· mcp claude: ${res.mcp.path} (already registered)`);
|
|
1313
|
+
else
|
|
1314
|
+
say(`✓ mcp claude: ${res.mcp.path} (${res.mcp.action}); restart Claude Code to load the graft MCP server`);
|
|
1315
|
+
say(res.built ? `✓ built the graph (${cmd("graft build")})` : "· skipped graph build");
|
|
1316
|
+
if (!wantStatusline)
|
|
1317
|
+
say("· skipped Claude Code statusLine (GRAFT_NO_STATUSLINE)");
|
|
1318
|
+
for (const w of res.warnings)
|
|
1319
|
+
say(`⚠ ${w}`);
|
|
1320
|
+
}
|
|
1321
|
+
// `ids` is already resolved, so hosts init is always driven by an explicit
|
|
1322
|
+
// list — never by its own detection fallback.
|
|
1323
|
+
// trail-solo-grafo: only 'hermes' is permitted as a non-claude agent;
|
|
1324
|
+
// gemini, antigravity, cursor, codex etc. are excluded (capa eliminada).
|
|
1325
|
+
const others = ids.filter((id) => id === "hermes");
|
|
1326
|
+
if (others.length > 0) {
|
|
1327
|
+
const r = runHostsInit(repo, {
|
|
1328
|
+
agents: others,
|
|
1329
|
+
home,
|
|
1330
|
+
mcp: opts.mcp,
|
|
1331
|
+
global: opts.global,
|
|
1332
|
+
laptop: ctx.laptop,
|
|
1333
|
+
brand: brand(),
|
|
1334
|
+
});
|
|
1335
|
+
for (const w of r.written)
|
|
1336
|
+
say(`✓ ${w.id}: ${w.path} (${w.action})`);
|
|
1337
|
+
for (const m of r.mcp)
|
|
1338
|
+
say(`✓ mcp ${m.id}: ${m.path} (${m.action})`);
|
|
1339
|
+
for (const h of r.hooks)
|
|
1340
|
+
say(`✓ hook ${h.id}: ${h.path} (${h.action})`);
|
|
1341
|
+
// Only worth saying when there was actually something out-of-repo to skip.
|
|
1342
|
+
if (opts.global === false && selectedWrites(plan, ids).some((w) => w.scope === "global"))
|
|
1343
|
+
say("· skipped out-of-repo writes (--no-global)");
|
|
1344
|
+
}
|
|
1345
|
+
// Record WHICH graft wrote this repo's agent files, and under which flags.
|
|
1346
|
+
// Every entry point compares this against the running binary and re-writes
|
|
1347
|
+
// them on a mismatch, so an `npm i -g` upgrade reaches the hooks/skill/rules
|
|
1348
|
+
// too — not just the binary. The flags ride along so a refresh replays the
|
|
1349
|
+
// user's choices (notably --no-global) instead of overriding them.
|
|
1350
|
+
writeStamp(repo, currentVersion, ids, {
|
|
1351
|
+
global: opts.global !== false,
|
|
1352
|
+
mcp: opts.mcp !== false,
|
|
1353
|
+
hooks: false,
|
|
1354
|
+
statusline: wantStatusline,
|
|
1355
|
+
...(ctx.laptop ? { laptop: ctx.laptop } : {}),
|
|
1356
|
+
});
|
|
1357
|
+
// Every host's wiring points at graft/, so the graph is built whatever was
|
|
1358
|
+
// selected — not only when Claude Code is in the list (runInit does its own).
|
|
1359
|
+
if (!wantClaude) {
|
|
1360
|
+
if (!quiet)
|
|
1361
|
+
say(buildGraphIfMissing(repo, { build: opts.build, cliPath })
|
|
1362
|
+
? `✓ built the graph (${cmd("graft build")})`
|
|
1363
|
+
: "· skipped graph build");
|
|
1364
|
+
}
|
|
1365
|
+
return report;
|
|
1366
|
+
}
|
|
1367
|
+
/** Group a retraction report by host, so the output reads as "what leaves each agent". */
|
|
1368
|
+
function formatRetractions(rs, apply) {
|
|
1369
|
+
const hit = changed(rs);
|
|
1370
|
+
if (hit.length === 0)
|
|
1371
|
+
return "· nothing to remove: no graft wiring found here";
|
|
1372
|
+
const verb = apply ? "removed" : "would remove";
|
|
1373
|
+
const lines = [];
|
|
1374
|
+
const byHost = new Map();
|
|
1375
|
+
for (const r of hit) {
|
|
1376
|
+
const k = byHost.get(r.hostId) ?? [];
|
|
1377
|
+
k.push(r);
|
|
1378
|
+
byHost.set(r.hostId, k);
|
|
1379
|
+
}
|
|
1380
|
+
for (const [host, items] of byHost) {
|
|
1381
|
+
lines.push(`\n${host}:`);
|
|
1382
|
+
for (const r of items) {
|
|
1383
|
+
const mark = r.action === "skipped-unparseable"
|
|
1384
|
+
? "⚠"
|
|
1385
|
+
: r.action === "deleted"
|
|
1386
|
+
? "-"
|
|
1387
|
+
: "~";
|
|
1388
|
+
const note = r.action === "skipped-unparseable"
|
|
1389
|
+
? ": not valid JSON, left untouched (remove the graft entry by hand)"
|
|
1390
|
+
: r.action === "deleted"
|
|
1391
|
+
? ` (${r.what}, deleted)`
|
|
1392
|
+
: ` (${r.what})`;
|
|
1393
|
+
const scope = r.scope === "global" ? " [machine-wide]" : "";
|
|
1394
|
+
lines.push(` ${mark} ${verb}: ${r.path}${scope}${note}`);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
return lines.join("\n").replace(/^\n/, "");
|
|
1398
|
+
}
|
|
1399
|
+
grouped(program.command("init"), GROUP.setup)
|
|
1400
|
+
.description(TRAIL
|
|
1401
|
+
? "Wire trail into your agents: AGENTS.md, .trail/ directory and Hermes skill"
|
|
1402
|
+
: "Wire Graft into the AI coding agents used with this repo")
|
|
1403
|
+
.argument("[dir]", "target repo directory", ".")
|
|
1404
|
+
.option("--agents <ids...>", "agent ids to wire (claude, cursor, codex, opencode, gemini, antigravity, agents)")
|
|
1405
|
+
.option("--all-agents", "wire every supported agent")
|
|
1406
|
+
.option("--list-agents", "print the supported agent ids and exit")
|
|
1407
|
+
.option("--no-build", "skip the graph build (wire only)")
|
|
1408
|
+
.option("--no-mcp", "skip MCP server registration")
|
|
1409
|
+
.option("--yes", "accept all defaults without prompting")
|
|
1410
|
+
.option("--dry-run", "print what would be written and exit")
|
|
1411
|
+
.option("--no-global", "skip out-of-repo writes (~/.claude, etc.)")
|
|
1412
|
+
.option("--verbose", "print the graph-build report when done")
|
|
1413
|
+
.action(async (dir, opts) => {
|
|
1414
|
+
const result = await runInitCommand(dir, opts);
|
|
1415
|
+
if (!result)
|
|
1416
|
+
return;
|
|
1417
|
+
if (opts.dryRun)
|
|
1418
|
+
return;
|
|
1419
|
+
console.error(`· restart your agents so a new session picks up ${brand()}`);
|
|
1420
|
+
if (TRAIL)
|
|
1421
|
+
console.error("· run `trail build` from this folder to update the code graph and semantic index");
|
|
1422
|
+
});
|
|
1423
|
+
grouped(program.command("uninstall"), GROUP.setup)
|
|
1424
|
+
.description(`Remove every file and config entry ${TRAIL ? "trail or graft" : "graft"} has written to this repo (the inverse of init)`)
|
|
1425
|
+
.argument("[dir]", "target repo directory", ".")
|
|
1426
|
+
.option("-y, --yes", "actually remove (without this, prints what it would remove and exits)")
|
|
1427
|
+
.option("--keep-cache", "keep graft/ and the .gitignore entries: wiring only")
|
|
1428
|
+
.option("--no-global", "leave out-of-repo files alone (~/.codex, ~/.gemini)")
|
|
1429
|
+
.action((dir, opts) => {
|
|
1430
|
+
const repo = resolve(dir);
|
|
1431
|
+
const home = homedir();
|
|
1432
|
+
const common = { home, global: opts.global, cache: opts.keepCache ? false : true };
|
|
1433
|
+
if (!opts.yes) {
|
|
1434
|
+
console.error(formatRetractions(planRetract(repo, common), false));
|
|
1435
|
+
console.error("\nDry run: nothing was touched. Re-run with -y to remove.");
|
|
1436
|
+
if (opts.global !== false)
|
|
1437
|
+
console.error("Entries marked [machine-wide] affect every project; --no-global skips them.");
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
const done = runRetract(repo, { ...common, apply: true });
|
|
1441
|
+
console.error(formatRetractions(done, true));
|
|
1442
|
+
const bad = changed(done).filter((r) => r.action === "skipped-unparseable");
|
|
1443
|
+
console.error(bad.length
|
|
1444
|
+
? `\n⚠ ${bad.length} file(s) could not be parsed and were left as-is: see above.`
|
|
1445
|
+
: `\n✓ ${brand()} fully removed. \`${cmd("graft init")}\` re-wires from scratch.`);
|
|
1446
|
+
});
|
|
1447
|
+
/* -------------------------------------------------------------------------- */
|
|
1448
|
+
/* trail login · logout · workspace */
|
|
1449
|
+
/* -------------------------------------------------------------------------- */
|
|
1450
|
+
/**
|
|
1451
|
+
* `trail login`: sign this laptop in to a Trail workspace through the
|
|
1452
|
+
* browser. No code to copy: the link carries a fresh state, the person signs
|
|
1453
|
+
* in and allows it there, and this laptop claims its own token. In a
|
|
1454
|
+
* terminal it opens the browser and waits. Run by an agent, it prints the
|
|
1455
|
+
* link for the agent's reply and leaves a background wait to finish it, so
|
|
1456
|
+
* the agent never sits blocked. `--workspace` (or the repo's
|
|
1457
|
+
* `.trail/workspace.json`) picks the workspace on that page. A laptop
|
|
1458
|
+
* already signed in links another repo the same way, to the same
|
|
1459
|
+
* workspace or another one: each repo goes to one workspace.
|
|
1460
|
+
*
|
|
1461
|
+
* `--code` skips the browser: Trail's web app made the one-time code for the
|
|
1462
|
+
* person already signed in there, so the laptop trades it for its token
|
|
1463
|
+
* straight away.
|
|
1464
|
+
*/
|
|
1465
|
+
function savedThisWeek(repo, now = Date.now()) {
|
|
1466
|
+
const since = now - 7 * 24 * 60 * 60 * 1000;
|
|
1467
|
+
let total = 0;
|
|
1468
|
+
for (const id of listSessionIds(repo)) {
|
|
1469
|
+
try {
|
|
1470
|
+
if (statSync(join(sessionDir(repo), `${id}.json`)).mtimeMs < since)
|
|
1471
|
+
continue;
|
|
1472
|
+
}
|
|
1473
|
+
catch {
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
total += readSession(repo, id).savedTokens ?? 0;
|
|
1477
|
+
}
|
|
1478
|
+
return total;
|
|
1479
|
+
}
|
|
1480
|
+
/** 212345 → `212k`, 1_250_000 → `1.3M`. */
|
|
1481
|
+
function shortCount(n) {
|
|
1482
|
+
if (n >= 1_000_000)
|
|
1483
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
1484
|
+
if (n >= 1_000)
|
|
1485
|
+
return `${Math.round(n / 1_000)}k`;
|
|
1486
|
+
return String(n);
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* `trail status`: what was `graft stats`, on one
|
|
1490
|
+
* screen. The code map and whether it matches the code, the semantic index, the
|
|
1491
|
+
* workspace, and what the agents saved this week.
|
|
1492
|
+
*/
|
|
1493
|
+
function statusCommand(name = "status") {
|
|
1494
|
+
return new Command(name)
|
|
1495
|
+
.description("Code map, freshness and tokens saved, on one screen")
|
|
1496
|
+
.argument(...DIR_ARG)
|
|
1497
|
+
.option("--json", "machine-readable output")
|
|
1498
|
+
.action(async (dirArg, opts) => {
|
|
1499
|
+
const repo = queryRoot(dirArg);
|
|
1500
|
+
const globalDir = program.opts().dir;
|
|
1501
|
+
const ws = readWorkspace(repo, globalDir);
|
|
1502
|
+
// ── Code-map: workspace aggregates across children, single-repo reads directly ──
|
|
1503
|
+
let totalNodes = 0;
|
|
1504
|
+
let builtRepos = 0;
|
|
1505
|
+
let totalRepos = 0;
|
|
1506
|
+
let missingRepos = [];
|
|
1507
|
+
let fresh = null;
|
|
1508
|
+
if (ws) {
|
|
1509
|
+
const wg = loadWorkspaceGraphs(repo, globalDir);
|
|
1510
|
+
builtRepos = wg.loaded.length;
|
|
1511
|
+
missingRepos = wg.missing;
|
|
1512
|
+
totalRepos = builtRepos + missingRepos.length;
|
|
1513
|
+
totalNodes = wg.loaded.reduce((s, l) => s + l.graph.meta.nodeCount, 0);
|
|
1514
|
+
// No per-child staleness check in status — treat built children as ok.
|
|
1515
|
+
fresh = builtRepos > 0 ? true : null;
|
|
1516
|
+
}
|
|
1517
|
+
else {
|
|
1518
|
+
const graph = loadGraphCached(contextDirFor(repo, globalDir));
|
|
1519
|
+
const g = graph ? await engineFrom().checkGraph(repo) : null;
|
|
1520
|
+
fresh = g && !g.missing ? g.ok : null;
|
|
1521
|
+
totalRepos = 1;
|
|
1522
|
+
if (graph) {
|
|
1523
|
+
totalNodes = graph.meta.nodeCount;
|
|
1524
|
+
builtRepos = 1;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
// Learnings and cloud sections removed in trail-solo-grafo.
|
|
1528
|
+
const saved = savedThisWeek(repo);
|
|
1529
|
+
// ── Semantic index: iterate children in workspace mode ─────────────────
|
|
1530
|
+
let semanticSymbols = 0;
|
|
1531
|
+
let semanticRepos = 0;
|
|
1532
|
+
{
|
|
1533
|
+
const { existsSync: fsExists } = await import('node:fs');
|
|
1534
|
+
const { join: pj } = await import('node:path');
|
|
1535
|
+
const { contextDirFor: ctxFor } = await import('./context/node-file.js');
|
|
1536
|
+
const { createRequire } = await import('node:module');
|
|
1537
|
+
const req = createRequire(import.meta.url);
|
|
1538
|
+
let Database = null;
|
|
1539
|
+
try {
|
|
1540
|
+
Database = req('better-sqlite3');
|
|
1541
|
+
}
|
|
1542
|
+
catch { /* sqlite not available */ }
|
|
1543
|
+
if (Database) {
|
|
1544
|
+
const ctxDirs = ws
|
|
1545
|
+
? ws.children.map((c) => ctxFor(join(repo, c)))
|
|
1546
|
+
: [ctxFor(repo, globalDir)];
|
|
1547
|
+
for (const ctxDir of ctxDirs) {
|
|
1548
|
+
const dbPath = pj(ctxDir, '.cache', 'embeddings.db');
|
|
1549
|
+
if (!fsExists(dbPath))
|
|
1550
|
+
continue;
|
|
1551
|
+
try {
|
|
1552
|
+
const db = Database(dbPath, { readonly: true });
|
|
1553
|
+
const { c } = db.prepare('SELECT COUNT(*) AS c FROM embed_nodes').get();
|
|
1554
|
+
db.close();
|
|
1555
|
+
semanticSymbols += c;
|
|
1556
|
+
semanticRepos++;
|
|
1557
|
+
}
|
|
1558
|
+
catch { /* DB not readable — ignore */ }
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
// ── Output ─────────────────────────────────────────────────────────────
|
|
1563
|
+
if (opts.json) {
|
|
1564
|
+
console.log(JSON.stringify({
|
|
1565
|
+
codeMap: builtRepos > 0 ? { nodes: totalNodes, repos: ws ? { built: builtRepos, total: totalRepos } : undefined, inSync: fresh } : null,
|
|
1566
|
+
week: { savedTokens: saved },
|
|
1567
|
+
semantic: semanticRepos > 0 ? { symbols: semanticSymbols, repos: semanticRepos, total: totalRepos } : null,
|
|
1568
|
+
}, null, 2));
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
const fmt = (x) => x.toLocaleString("en-US");
|
|
1572
|
+
const row = (k, v) => console.log(`${k.padEnd(11)} ${v}`);
|
|
1573
|
+
// Code-map row
|
|
1574
|
+
if (builtRepos === 0) {
|
|
1575
|
+
row("code map", `✗ not built · run ${cmd("trail build")}`);
|
|
1576
|
+
}
|
|
1577
|
+
else if (ws) {
|
|
1578
|
+
const repoLabel = `${builtRepos} repo${builtRepos === 1 ? "" : "s"}`;
|
|
1579
|
+
if (missingRepos.length > 0) {
|
|
1580
|
+
row("code map", `⚠ ${repoLabel} of ${totalRepos} built · ${fmt(totalNodes)} nodes · ${cmd("trail build")} to cover ${missingRepos.join(", ")}`);
|
|
1581
|
+
}
|
|
1582
|
+
else {
|
|
1583
|
+
row("code map", `✓ ${repoLabel} · ${fmt(totalNodes)} nodes`);
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
else if (fresh === false) {
|
|
1587
|
+
row("code map", `⚠ ${fmt(totalNodes)} nodes · behind the code, refreshed on the next query`);
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
row("code map", `✓ ${fmt(totalNodes)} nodes · in sync with the code`);
|
|
1591
|
+
}
|
|
1592
|
+
row("this week", saved > 0 ? `~${shortCount(saved)} tokens saved` : "nothing saved yet");
|
|
1593
|
+
// Semantic row
|
|
1594
|
+
if (semanticRepos === 0) {
|
|
1595
|
+
row("semantic", `✗ not built · run \`trail build\` to enable vector search`);
|
|
1596
|
+
}
|
|
1597
|
+
else if (ws) {
|
|
1598
|
+
row("semantic", `✓ ${fmt(semanticSymbols)} symbols · ${semanticRepos} of ${totalRepos} repos indexed`);
|
|
1599
|
+
}
|
|
1600
|
+
else {
|
|
1601
|
+
row("semantic", `✓ ${fmt(semanticSymbols)} symbols indexed · run \`trail build\` to refresh`);
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
if (TRAIL) {
|
|
1606
|
+
program.addCommand(statusCommand().helpGroup(GROUP.setup));
|
|
1607
|
+
// Help lists the groups in this order, and the commands within each.
|
|
1608
|
+
const order = ["ask", "grep", "skeleton", "callers", "map", "note", "check", "team", "skills", "init", "build", "status", "upgrade", "uninstall", "telemetry", "workspace", "login", "logout", "blast"];
|
|
1609
|
+
const rank = (c) => {
|
|
1610
|
+
const i = order.indexOf(c.name());
|
|
1611
|
+
return i === -1 ? order.length : i;
|
|
1612
|
+
};
|
|
1613
|
+
program.commands.sort((a, b) => rank(a) - rank(b));
|
|
1614
|
+
// One line each in the list; `trail <command> --help` keeps the long text.
|
|
1615
|
+
const summaries = {
|
|
1616
|
+
ask: "ranked answer, with the code inlined at each file:line",
|
|
1617
|
+
grep: "every occurrence, grouped by enclosing symbol",
|
|
1618
|
+
skeleton: "a file's whole API in ~200 tokens",
|
|
1619
|
+
callers: "who calls it, or what it calls with --direction out",
|
|
1620
|
+
map: "directory clusters, hubs and hotspots",
|
|
1621
|
+
init: "wire trail into your agents",
|
|
1622
|
+
build: "rebuild the code map · --check fails if it's stale",
|
|
1623
|
+
status: "code map and semantic index",
|
|
1624
|
+
upgrade: "install the latest trail",
|
|
1625
|
+
uninstall: "remove everything trail or graft wrote to this repo",
|
|
1626
|
+
telemetry: "show or turn off the anonymous usage stats",
|
|
1627
|
+
blast: "what depends on the lines this diff touched",
|
|
1628
|
+
};
|
|
1629
|
+
for (const c of program.commands)
|
|
1630
|
+
if (summaries[c.name()])
|
|
1631
|
+
c.summary(summaries[c.name()]);
|
|
1632
|
+
program.addHelpText("after", "\ntrail <command> --help for flags · the same command set runs as `graft`");
|
|
1633
|
+
}
|
|
1634
|
+
program.parseAsync(process.argv).catch(async (err) => {
|
|
1635
|
+
console.error(err instanceof Error ? err.message : err);
|
|
1636
|
+
process.exit(1);
|
|
1637
|
+
});
|
|
1638
|
+
//# sourceMappingURL=cli.js.map
|