@ag-eco/agentplate-cli 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +462 -0
- package/agents/ap-co-creation.md +90 -0
- package/agents/builder.md +144 -0
- package/agents/coordinator.md +377 -0
- package/agents/lead.md +435 -0
- package/agents/merger.md +164 -0
- package/agents/monitor.md +214 -0
- package/agents/orchestrator.md +239 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +125 -0
- package/agents/supervisor.md +427 -0
- package/package.json +66 -0
- package/src/agents/capabilities.test.ts +85 -0
- package/src/agents/capabilities.ts +125 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/copilot-hooks-deployer.test.ts +162 -0
- package/src/agents/copilot-hooks-deployer.ts +93 -0
- package/src/agents/guard-rules.test.ts +372 -0
- package/src/agents/guard-rules.ts +97 -0
- package/src/agents/headless-mail-injector.test.ts +709 -0
- package/src/agents/headless-mail-injector.ts +377 -0
- package/src/agents/headless-prompt.test.ts +102 -0
- package/src/agents/headless-prompt.ts +68 -0
- package/src/agents/hooks-deployer.test.ts +3119 -0
- package/src/agents/hooks-deployer.ts +804 -0
- package/src/agents/identity.test.ts +604 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/mail-poll-detect.test.ts +153 -0
- package/src/agents/mail-poll-detect.ts +73 -0
- package/src/agents/manifest.test.ts +1026 -0
- package/src/agents/manifest.ts +376 -0
- package/src/agents/overlay.test.ts +1058 -0
- package/src/agents/overlay.ts +490 -0
- package/src/agents/scope-detect.test.ts +190 -0
- package/src/agents/scope-detect.ts +146 -0
- package/src/agents/turn-lock.test.ts +181 -0
- package/src/agents/turn-lock.ts +235 -0
- package/src/agents/turn-runner-dispatch.test.ts +182 -0
- package/src/agents/turn-runner-dispatch.ts +105 -0
- package/src/agents/turn-runner.test.ts +2312 -0
- package/src/agents/turn-runner.ts +1383 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +230 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +328 -0
- package/src/commands/agents.ts +299 -0
- package/src/commands/clean.test.ts +797 -0
- package/src/commands/clean.ts +791 -0
- package/src/commands/completions.test.ts +348 -0
- package/src/commands/completions.ts +981 -0
- package/src/commands/coordinator.test.ts +2975 -0
- package/src/commands/coordinator.ts +1841 -0
- package/src/commands/costs.test.ts +1183 -0
- package/src/commands/costs.ts +599 -0
- package/src/commands/dashboard.test.ts +954 -0
- package/src/commands/dashboard.ts +1212 -0
- package/src/commands/discover.test.ts +288 -0
- package/src/commands/discover.ts +202 -0
- package/src/commands/doctor.test.ts +303 -0
- package/src/commands/doctor.ts +311 -0
- package/src/commands/ecosystem.test.ts +226 -0
- package/src/commands/ecosystem.ts +248 -0
- package/src/commands/errors.test.ts +654 -0
- package/src/commands/errors.ts +197 -0
- package/src/commands/feed.test.ts +709 -0
- package/src/commands/feed.ts +260 -0
- package/src/commands/group.test.ts +475 -0
- package/src/commands/group.ts +546 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +263 -0
- package/src/commands/init.test.ts +1011 -0
- package/src/commands/init.ts +967 -0
- package/src/commands/inspect.test.ts +1239 -0
- package/src/commands/inspect.ts +648 -0
- package/src/commands/log.test.ts +1913 -0
- package/src/commands/log.ts +958 -0
- package/src/commands/logs.test.ts +801 -0
- package/src/commands/logs.ts +483 -0
- package/src/commands/mail.test.ts +1501 -0
- package/src/commands/mail.ts +848 -0
- package/src/commands/merge.test.ts +864 -0
- package/src/commands/merge.ts +381 -0
- package/src/commands/metrics.test.ts +458 -0
- package/src/commands/metrics.ts +129 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +409 -0
- package/src/commands/nudge.test.ts +579 -0
- package/src/commands/nudge.ts +646 -0
- package/src/commands/orchestrator.ts +42 -0
- package/src/commands/prime.test.ts +612 -0
- package/src/commands/prime.ts +359 -0
- package/src/commands/replay.test.ts +757 -0
- package/src/commands/replay.ts +231 -0
- package/src/commands/run.test.ts +469 -0
- package/src/commands/run.ts +353 -0
- package/src/commands/serve/agent-actions.test.ts +210 -0
- package/src/commands/serve/agent-actions.ts +192 -0
- package/src/commands/serve/build.test.ts +202 -0
- package/src/commands/serve/build.ts +206 -0
- package/src/commands/serve/coordinator-actions.test.ts +339 -0
- package/src/commands/serve/coordinator-actions.ts +410 -0
- package/src/commands/serve/dev.test.ts +168 -0
- package/src/commands/serve/dev.ts +117 -0
- package/src/commands/serve/mail-actions.test.ts +312 -0
- package/src/commands/serve/mail-actions.ts +167 -0
- package/src/commands/serve/rest.test.ts +1680 -0
- package/src/commands/serve/rest.ts +1130 -0
- package/src/commands/serve/static.ts +51 -0
- package/src/commands/serve/ws.test.ts +361 -0
- package/src/commands/serve/ws.ts +332 -0
- package/src/commands/serve.test.ts +459 -0
- package/src/commands/serve.ts +654 -0
- package/src/commands/sling.test.ts +1583 -0
- package/src/commands/sling.ts +1351 -0
- package/src/commands/spec.test.ts +179 -0
- package/src/commands/spec.ts +105 -0
- package/src/commands/status.test.ts +614 -0
- package/src/commands/status.ts +403 -0
- package/src/commands/stop.test.ts +964 -0
- package/src/commands/stop.ts +319 -0
- package/src/commands/supervisor.test.ts +185 -0
- package/src/commands/supervisor.ts +537 -0
- package/src/commands/trace.test.ts +762 -0
- package/src/commands/trace.ts +205 -0
- package/src/commands/update.test.ts +466 -0
- package/src/commands/update.ts +263 -0
- package/src/commands/upgrade.test.ts +48 -0
- package/src/commands/upgrade.ts +240 -0
- package/src/commands/watch.test.ts +257 -0
- package/src/commands/watch.ts +308 -0
- package/src/commands/worktree.test.ts +1297 -0
- package/src/commands/worktree.ts +451 -0
- package/src/config.test.ts +1535 -0
- package/src/config.ts +1064 -0
- package/src/doctor/agents.test.ts +523 -0
- package/src/doctor/agents.ts +399 -0
- package/src/doctor/config-check.test.ts +191 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +807 -0
- package/src/doctor/consistency.ts +347 -0
- package/src/doctor/databases.test.ts +350 -0
- package/src/doctor/databases.ts +243 -0
- package/src/doctor/dependencies.test.ts +296 -0
- package/src/doctor/dependencies.ts +272 -0
- package/src/doctor/ecosystem.test.ts +308 -0
- package/src/doctor/ecosystem.ts +156 -0
- package/src/doctor/logs.test.ts +253 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +315 -0
- package/src/doctor/merge-queue.ts +167 -0
- package/src/doctor/providers.test.ts +409 -0
- package/src/doctor/providers.ts +250 -0
- package/src/doctor/serve.test.ts +95 -0
- package/src/doctor/serve.ts +86 -0
- package/src/doctor/structure.test.ts +423 -0
- package/src/doctor/structure.ts +285 -0
- package/src/doctor/types.ts +43 -0
- package/src/doctor/version.test.ts +241 -0
- package/src/doctor/version.ts +132 -0
- package/src/doctor/watchdog.test.ts +167 -0
- package/src/doctor/watchdog.ts +214 -0
- package/src/e2e/init-sling-lifecycle.test.ts +283 -0
- package/src/errors.test.ts +350 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tailer.test.ts +719 -0
- package/src/events/tailer.ts +332 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +533 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/insights/quality-gates.test.ts +141 -0
- package/src/insights/quality-gates.ts +156 -0
- package/src/json.test.ts +72 -0
- package/src/json.ts +53 -0
- package/src/loam/client.test.ts +752 -0
- package/src/loam/client.ts +664 -0
- package/src/logging/color.test.ts +252 -0
- package/src/logging/color.ts +105 -0
- package/src/logging/format.test.ts +110 -0
- package/src/logging/format.ts +255 -0
- package/src/logging/logger.test.ts +814 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +110 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/logging/theme.ts +140 -0
- package/src/mail/broadcast.test.ts +204 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +774 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +898 -0
- package/src/mail/store.ts +425 -0
- package/src/merge/lock.test.ts +149 -0
- package/src/merge/lock.ts +140 -0
- package/src/merge/predict.test.ts +387 -0
- package/src/merge/predict.ts +249 -0
- package/src/merge/queue.test.ts +426 -0
- package/src/merge/queue.ts +246 -0
- package/src/merge/resolver.test.ts +1993 -0
- package/src/merge/resolver.ts +926 -0
- package/src/metrics/pricing.test.ts +258 -0
- package/src/metrics/pricing.ts +135 -0
- package/src/metrics/store.test.ts +978 -0
- package/src/metrics/store.ts +501 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +483 -0
- package/src/metrics/transcript.ts +114 -0
- package/src/runtimes/__fixtures__/claude-stream-fixture.ts +22 -0
- package/src/runtimes/aider.test.ts +124 -0
- package/src/runtimes/aider.ts +147 -0
- package/src/runtimes/amp.test.ts +164 -0
- package/src/runtimes/amp.ts +154 -0
- package/src/runtimes/claude.test.ts +1474 -0
- package/src/runtimes/claude.ts +579 -0
- package/src/runtimes/codex.test.ts +805 -0
- package/src/runtimes/codex.ts +273 -0
- package/src/runtimes/connections.test.ts +214 -0
- package/src/runtimes/connections.ts +103 -0
- package/src/runtimes/copilot.test.ts +707 -0
- package/src/runtimes/copilot.ts +316 -0
- package/src/runtimes/cursor.test.ts +497 -0
- package/src/runtimes/cursor.ts +205 -0
- package/src/runtimes/gemini.test.ts +537 -0
- package/src/runtimes/gemini.ts +243 -0
- package/src/runtimes/goose.test.ts +133 -0
- package/src/runtimes/goose.ts +157 -0
- package/src/runtimes/headless-connection.test.ts +264 -0
- package/src/runtimes/headless-connection.ts +158 -0
- package/src/runtimes/opencode.test.ts +325 -0
- package/src/runtimes/opencode.ts +188 -0
- package/src/runtimes/pi-guards.test.ts +486 -0
- package/src/runtimes/pi-guards.ts +367 -0
- package/src/runtimes/pi.test.ts +789 -0
- package/src/runtimes/pi.ts +305 -0
- package/src/runtimes/registry.test.ts +196 -0
- package/src/runtimes/registry.ts +99 -0
- package/src/runtimes/sapling.test.ts +1267 -0
- package/src/runtimes/sapling.ts +710 -0
- package/src/runtimes/types.ts +266 -0
- package/src/schema-consistency.test.ts +246 -0
- package/src/sessions/compat.test.ts +281 -0
- package/src/sessions/compat.ts +105 -0
- package/src/sessions/store.test.ts +1748 -0
- package/src/sessions/store.ts +858 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +145 -0
- package/src/test-setup.test.ts +31 -0
- package/src/test-setup.ts +28 -0
- package/src/tools/loam/api.ts +368 -0
- package/src/tools/loam/cli.ts +278 -0
- package/src/tools/loam/commands/add.ts +52 -0
- package/src/tools/loam/commands/archive.ts +214 -0
- package/src/tools/loam/commands/audit.ts +276 -0
- package/src/tools/loam/commands/compact.ts +1062 -0
- package/src/tools/loam/commands/completions.ts +79 -0
- package/src/tools/loam/commands/config.ts +381 -0
- package/src/tools/loam/commands/delete-domain.ts +121 -0
- package/src/tools/loam/commands/delete.ts +316 -0
- package/src/tools/loam/commands/diff.ts +200 -0
- package/src/tools/loam/commands/doctor.ts +1113 -0
- package/src/tools/loam/commands/edit.ts +226 -0
- package/src/tools/loam/commands/init.ts +31 -0
- package/src/tools/loam/commands/learn.ts +179 -0
- package/src/tools/loam/commands/move.ts +323 -0
- package/src/tools/loam/commands/onboard.ts +374 -0
- package/src/tools/loam/commands/outcome.ts +185 -0
- package/src/tools/loam/commands/prime.ts +688 -0
- package/src/tools/loam/commands/prune.ts +614 -0
- package/src/tools/loam/commands/query.ts +218 -0
- package/src/tools/loam/commands/rank.ts +180 -0
- package/src/tools/loam/commands/ready.ts +189 -0
- package/src/tools/loam/commands/record.ts +1210 -0
- package/src/tools/loam/commands/restore.ts +166 -0
- package/src/tools/loam/commands/search.ts +327 -0
- package/src/tools/loam/commands/setup.ts +887 -0
- package/src/tools/loam/commands/status.ts +103 -0
- package/src/tools/loam/commands/sync.ts +298 -0
- package/src/tools/loam/commands/update.ts +19 -0
- package/src/tools/loam/commands/upgrade.ts +93 -0
- package/src/tools/loam/commands/validate.ts +190 -0
- package/src/tools/loam/index.ts +62 -0
- package/src/tools/loam/log.ts +127 -0
- package/src/tools/loam/registry/builtins.ts +409 -0
- package/src/tools/loam/registry/custom.ts +431 -0
- package/src/tools/loam/registry/init.ts +55 -0
- package/src/tools/loam/registry/template.ts +40 -0
- package/src/tools/loam/registry/type-registry.ts +113 -0
- package/src/tools/loam/schemas/config-schema.ts +489 -0
- package/src/tools/loam/schemas/config.ts +245 -0
- package/src/tools/loam/schemas/index.ts +18 -0
- package/src/tools/loam/schemas/record-schema.ts +191 -0
- package/src/tools/loam/schemas/record.ts +115 -0
- package/src/tools/loam/utils/active-work.ts +205 -0
- package/src/tools/loam/utils/anchor-validity.ts +80 -0
- package/src/tools/loam/utils/archive.ts +146 -0
- package/src/tools/loam/utils/audit.ts +667 -0
- package/src/tools/loam/utils/bm25.ts +238 -0
- package/src/tools/loam/utils/budget.ts +142 -0
- package/src/tools/loam/utils/config.ts +344 -0
- package/src/tools/loam/utils/dir-anchors.ts +62 -0
- package/src/tools/loam/utils/domain-rules.ts +114 -0
- package/src/tools/loam/utils/expertise.ts +393 -0
- package/src/tools/loam/utils/format-helpers.ts +96 -0
- package/src/tools/loam/utils/format.ts +1234 -0
- package/src/tools/loam/utils/git-context.ts +50 -0
- package/src/tools/loam/utils/git.ts +183 -0
- package/src/tools/loam/utils/hooks.ts +299 -0
- package/src/tools/loam/utils/index.ts +52 -0
- package/src/tools/loam/utils/json-output.ts +13 -0
- package/src/tools/loam/utils/lock.ts +76 -0
- package/src/tools/loam/utils/markers.ts +48 -0
- package/src/tools/loam/utils/numeric-flags.ts +20 -0
- package/src/tools/loam/utils/palette.ts +44 -0
- package/src/tools/loam/utils/prime-ranking.ts +135 -0
- package/src/tools/loam/utils/recipe-discovery.ts +195 -0
- package/src/tools/loam/utils/runtime-flags.ts +28 -0
- package/src/tools/loam/utils/scoring.ts +94 -0
- package/src/tools/loam/utils/version.ts +116 -0
- package/src/tools/sprout/commands/block.ts +64 -0
- package/src/tools/sprout/commands/blocked.ts +86 -0
- package/src/tools/sprout/commands/close.ts +129 -0
- package/src/tools/sprout/commands/completions.ts +198 -0
- package/src/tools/sprout/commands/config.ts +238 -0
- package/src/tools/sprout/commands/create.ts +164 -0
- package/src/tools/sprout/commands/dep.ts +148 -0
- package/src/tools/sprout/commands/doctor.ts +979 -0
- package/src/tools/sprout/commands/init.ts +83 -0
- package/src/tools/sprout/commands/label.ts +178 -0
- package/src/tools/sprout/commands/list.ts +210 -0
- package/src/tools/sprout/commands/migrate.ts +133 -0
- package/src/tools/sprout/commands/onboard.ts +207 -0
- package/src/tools/sprout/commands/plan-show.ts +278 -0
- package/src/tools/sprout/commands/plan.ts +2526 -0
- package/src/tools/sprout/commands/prime.ts +399 -0
- package/src/tools/sprout/commands/ready.ts +245 -0
- package/src/tools/sprout/commands/search.ts +221 -0
- package/src/tools/sprout/commands/show.ts +277 -0
- package/src/tools/sprout/commands/stats.ts +146 -0
- package/src/tools/sprout/commands/sync.ts +134 -0
- package/src/tools/sprout/commands/tpl.ts +364 -0
- package/src/tools/sprout/commands/unblock.ts +115 -0
- package/src/tools/sprout/commands/update.ts +257 -0
- package/src/tools/sprout/commands/upgrade.ts +91 -0
- package/src/tools/sprout/config-schema.ts +152 -0
- package/src/tools/sprout/config.ts +355 -0
- package/src/tools/sprout/filter.ts +107 -0
- package/src/tools/sprout/format.ts +43 -0
- package/src/tools/sprout/id.ts +22 -0
- package/src/tools/sprout/index.ts +204 -0
- package/src/tools/sprout/log.ts +76 -0
- package/src/tools/sprout/markers.ts +22 -0
- package/src/tools/sprout/output.ts +121 -0
- package/src/tools/sprout/plan-backref.ts +93 -0
- package/src/tools/sprout/plan-context.ts +81 -0
- package/src/tools/sprout/plan-domain.ts +139 -0
- package/src/tools/sprout/plan-lifecycle.ts +65 -0
- package/src/tools/sprout/plan-loam.ts +207 -0
- package/src/tools/sprout/plan-schema.ts +209 -0
- package/src/tools/sprout/sort.ts +31 -0
- package/src/tools/sprout/store.ts +172 -0
- package/src/tools/sprout/types.ts +118 -0
- package/src/tools/sprout/validation.ts +119 -0
- package/src/tools/sprout/version.ts +1 -0
- package/src/tools/sprout/yaml.ts +387 -0
- package/src/tools/trellis/commands/archive.ts +87 -0
- package/src/tools/trellis/commands/completions.ts +610 -0
- package/src/tools/trellis/commands/config.ts +382 -0
- package/src/tools/trellis/commands/create.ts +252 -0
- package/src/tools/trellis/commands/diff.ts +150 -0
- package/src/tools/trellis/commands/doctor.ts +771 -0
- package/src/tools/trellis/commands/emit.ts +365 -0
- package/src/tools/trellis/commands/history.ts +83 -0
- package/src/tools/trellis/commands/import.ts +198 -0
- package/src/tools/trellis/commands/init.ts +81 -0
- package/src/tools/trellis/commands/list.ts +103 -0
- package/src/tools/trellis/commands/onboard.ts +156 -0
- package/src/tools/trellis/commands/pin.ts +172 -0
- package/src/tools/trellis/commands/prime.ts +193 -0
- package/src/tools/trellis/commands/render.ts +122 -0
- package/src/tools/trellis/commands/schema.ts +353 -0
- package/src/tools/trellis/commands/show.ts +115 -0
- package/src/tools/trellis/commands/stats.ts +65 -0
- package/src/tools/trellis/commands/sync.ts +112 -0
- package/src/tools/trellis/commands/tree.ts +123 -0
- package/src/tools/trellis/commands/update.ts +330 -0
- package/src/tools/trellis/commands/upgrade.ts +95 -0
- package/src/tools/trellis/commands/validate.ts +166 -0
- package/src/tools/trellis/config-schema.ts +81 -0
- package/src/tools/trellis/config.ts +108 -0
- package/src/tools/trellis/frontmatter.ts +348 -0
- package/src/tools/trellis/id.ts +24 -0
- package/src/tools/trellis/index.ts +209 -0
- package/src/tools/trellis/markers.ts +28 -0
- package/src/tools/trellis/output.ts +84 -0
- package/src/tools/trellis/render.ts +212 -0
- package/src/tools/trellis/store.ts +144 -0
- package/src/tools/trellis/types.ts +82 -0
- package/src/tools/trellis/validate.ts +199 -0
- package/src/tools/trellis/yaml.ts +309 -0
- package/src/tracker/beads.test.ts +454 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +90 -0
- package/src/tracker/factory.ts +65 -0
- package/src/tracker/sprout.test.ts +461 -0
- package/src/tracker/sprout.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/trellis/client.test.ts +107 -0
- package/src/trellis/client.ts +179 -0
- package/src/types.ts +970 -0
- package/src/utils/bin.test.ts +10 -0
- package/src/utils/bin.ts +37 -0
- package/src/utils/browser.test.ts +49 -0
- package/src/utils/browser.ts +48 -0
- package/src/utils/fs.test.ts +119 -0
- package/src/utils/fs.ts +62 -0
- package/src/utils/pid.test.ts +152 -0
- package/src/utils/pid.ts +130 -0
- package/src/utils/process-scan.test.ts +53 -0
- package/src/utils/process-scan.ts +76 -0
- package/src/utils/time.test.ts +43 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/version.test.ts +33 -0
- package/src/utils/version.ts +70 -0
- package/src/version.ts +5 -0
- package/src/watchdog/daemon.test.ts +3721 -0
- package/src/watchdog/daemon.ts +1257 -0
- package/src/watchdog/health.test.ts +830 -0
- package/src/watchdog/health.ts +434 -0
- package/src/watchdog/triage.test.ts +205 -0
- package/src/watchdog/triage.ts +205 -0
- package/src/worktree/manager.test.ts +720 -0
- package/src/worktree/manager.ts +405 -0
- package/src/worktree/process.test.ts +172 -0
- package/src/worktree/process.ts +131 -0
- package/src/worktree/tmux.test.ts +1616 -0
- package/src/worktree/tmux.ts +721 -0
- package/templates/CLAUDE.md.tmpl +100 -0
- package/templates/copilot-hooks.json.tmpl +13 -0
- package/templates/hooks.json.tmpl +109 -0
- package/templates/overlay.md.tmpl +88 -0
- package/ui/dist/apple-touch-icon-bdy6teep.png +0 -0
- package/ui/dist/chunk-8s31f05k.css +1 -0
- package/ui/dist/chunk-vm5rz679.js +300 -0
- package/ui/dist/favicon-nzb39vza.svg +4 -0
- package/ui/dist/index.html +17 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import type { Command } from "commander";
|
|
6
|
+
import { getRegistry } from "../registry/type-registry.ts";
|
|
7
|
+
import type { ExpertiseRecord } from "../schemas/record.ts";
|
|
8
|
+
import { getExpertiseDir, getExpertisePath, readConfig } from "../utils/config.ts";
|
|
9
|
+
import {
|
|
10
|
+
findMissingDomainFields,
|
|
11
|
+
getAllowedTypes,
|
|
12
|
+
getRequiredFields,
|
|
13
|
+
} from "../utils/domain-rules.ts";
|
|
14
|
+
import {
|
|
15
|
+
appendRecord,
|
|
16
|
+
readExpertiseFile,
|
|
17
|
+
resolveRecordId,
|
|
18
|
+
writeExpertiseFile,
|
|
19
|
+
} from "../utils/expertise.ts";
|
|
20
|
+
import { getRecordSummary } from "../utils/format.ts";
|
|
21
|
+
import { runHooks } from "../utils/hooks.ts";
|
|
22
|
+
import { outputJson, outputJsonError } from "../utils/json-output.ts";
|
|
23
|
+
import { withFileLock } from "../utils/lock.ts";
|
|
24
|
+
import { accent, brand, isQuiet } from "../utils/palette.ts";
|
|
25
|
+
|
|
26
|
+
interface MoveOptions {
|
|
27
|
+
dryRun: boolean;
|
|
28
|
+
force: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ReferenceHit {
|
|
32
|
+
domain: string;
|
|
33
|
+
id: string | null;
|
|
34
|
+
field: "relates_to" | "supersedes";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Scan every other expertise file for references (relates_to / supersedes) to
|
|
38
|
+
// the moved record's ID. We don't rewrite — the ID is preserved across the
|
|
39
|
+
// move, so existing links still resolve. The scan exists purely as an
|
|
40
|
+
// informational warning so users can audit the link graph if they care.
|
|
41
|
+
async function findIncomingReferences(
|
|
42
|
+
movedId: string,
|
|
43
|
+
cwd: string,
|
|
44
|
+
skipFiles: Set<string>,
|
|
45
|
+
): Promise<ReferenceHit[]> {
|
|
46
|
+
const expertiseDir = getExpertiseDir(cwd);
|
|
47
|
+
if (!existsSync(expertiseDir)) return [];
|
|
48
|
+
const entries = await readdir(expertiseDir).catch(() => [] as string[]);
|
|
49
|
+
const hits: ReferenceHit[] = [];
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
52
|
+
const filePath = join(expertiseDir, entry);
|
|
53
|
+
if (skipFiles.has(filePath)) continue;
|
|
54
|
+
const domain = entry.slice(0, -".jsonl".length);
|
|
55
|
+
const records = await readExpertiseFile(filePath, { allowUnknownTypes: true }).catch(
|
|
56
|
+
() => [] as ExpertiseRecord[],
|
|
57
|
+
);
|
|
58
|
+
for (const r of records) {
|
|
59
|
+
if (r.relates_to?.includes(movedId)) {
|
|
60
|
+
hits.push({ domain, id: r.id ?? null, field: "relates_to" });
|
|
61
|
+
}
|
|
62
|
+
if (r.supersedes?.includes(movedId)) {
|
|
63
|
+
hits.push({ domain, id: r.id ?? null, field: "supersedes" });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return hits;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function registerMoveCommand(program: Command): void {
|
|
71
|
+
program
|
|
72
|
+
.command("move")
|
|
73
|
+
.argument("<source-domain>", "current domain")
|
|
74
|
+
.argument("<id>", "record ID (e.g. mx-abc123, abc123, or abc)")
|
|
75
|
+
.argument("<target-domain>", "destination domain")
|
|
76
|
+
.description("Move a record from one domain to another, preserving its ID and metadata")
|
|
77
|
+
.option("--dry-run", "preview the move without making changes", false)
|
|
78
|
+
.option(
|
|
79
|
+
"--force",
|
|
80
|
+
"bypass target domain's allowed_types gate (required_fields still enforced)",
|
|
81
|
+
false,
|
|
82
|
+
)
|
|
83
|
+
.action(
|
|
84
|
+
async (sourceDomain: string, id: string, targetDomain: string, options: MoveOptions) => {
|
|
85
|
+
const jsonMode = program.opts().json === true;
|
|
86
|
+
try {
|
|
87
|
+
const config = await readConfig();
|
|
88
|
+
const availableDomains = Object.keys(config.domains);
|
|
89
|
+
|
|
90
|
+
if (sourceDomain === targetDomain) {
|
|
91
|
+
const msg = "Source and target domain are the same — nothing to move.";
|
|
92
|
+
if (jsonMode) outputJsonError("move", msg);
|
|
93
|
+
else console.error(chalk.red(`Error: ${msg}`));
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const d of [sourceDomain, targetDomain]) {
|
|
99
|
+
if (!(d in config.domains)) {
|
|
100
|
+
const msg = `Domain "${d}" not found in config. Available domains: ${availableDomains.join(", ") || "(none)"}`;
|
|
101
|
+
if (jsonMode) outputJsonError("move", msg);
|
|
102
|
+
else console.error(chalk.red(`Error: ${msg}`));
|
|
103
|
+
process.exitCode = 1;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const sourcePath = getExpertisePath(sourceDomain);
|
|
109
|
+
const targetPath = getExpertisePath(targetDomain);
|
|
110
|
+
|
|
111
|
+
// Read source under its lock to resolve the record. We release
|
|
112
|
+
// the lock before validating so we don't hold it across hooks /
|
|
113
|
+
// target lock acquisition — the record is re-fetched under lock
|
|
114
|
+
// at write time below.
|
|
115
|
+
const sourceRecords = await withFileLock(sourcePath, async () =>
|
|
116
|
+
readExpertiseFile(sourcePath),
|
|
117
|
+
);
|
|
118
|
+
const resolved = resolveRecordId(sourceRecords, id);
|
|
119
|
+
if (!resolved.ok) {
|
|
120
|
+
if (jsonMode) outputJsonError("move", resolved.error);
|
|
121
|
+
else console.error(chalk.red(`Error: ${resolved.error}`));
|
|
122
|
+
process.exitCode = 1;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const record = resolved.record;
|
|
126
|
+
|
|
127
|
+
// Reject archived records. The .loam/archive/ store is walked by
|
|
128
|
+
// `lm restore` and `lm search --archived`; live expertise files
|
|
129
|
+
// shouldn't carry archived rows, but a stray status field would.
|
|
130
|
+
if (record.status === "archived") {
|
|
131
|
+
const msg = `Record ${record.id ?? id} is archived. Run \`lm restore ${record.id ?? id}\` first.`;
|
|
132
|
+
if (jsonMode) outputJsonError("move", msg);
|
|
133
|
+
else console.error(chalk.red(`Error: ${msg}`));
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Validate type against target allowed_types
|
|
139
|
+
const allowedTypes = getAllowedTypes(config, targetDomain);
|
|
140
|
+
if (allowedTypes && !allowedTypes.includes(record.type) && !options.force) {
|
|
141
|
+
const msg = `Type "${record.type}" is not in target domain "${targetDomain}" allowed_types (${allowedTypes.join(", ")}). Pass --force to override, or adjust loam.config.yaml.`;
|
|
142
|
+
if (jsonMode) outputJsonError("move", msg);
|
|
143
|
+
else console.error(chalk.red(`Error: ${msg}`));
|
|
144
|
+
process.exitCode = 1;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Validate required_fields for target
|
|
149
|
+
const requiredFields = getRequiredFields(config, targetDomain);
|
|
150
|
+
if (requiredFields) {
|
|
151
|
+
const missing = findMissingDomainFields(
|
|
152
|
+
record as unknown as Record<string, unknown>,
|
|
153
|
+
requiredFields,
|
|
154
|
+
);
|
|
155
|
+
if (missing.length > 0) {
|
|
156
|
+
const fieldList = missing.map((f) => `"${f}"`).join(", ");
|
|
157
|
+
const msg = `Record is missing field(s) required by target domain "${targetDomain}": ${fieldList}. Edit the record (\`lm edit ${record.id ?? id}\`) before moving.`;
|
|
158
|
+
if (jsonMode) outputJsonError("move", msg);
|
|
159
|
+
else console.error(chalk.red(`Error: ${msg}`));
|
|
160
|
+
process.exitCode = 1;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Re-validate against the JSON schema. The record is presumed
|
|
166
|
+
// valid (it was written before), but a registry change since
|
|
167
|
+
// then could have tightened the schema. This is a cheap
|
|
168
|
+
// safeguard against silently writing a stale row into the new
|
|
169
|
+
// domain.
|
|
170
|
+
const validate = getRegistry().validator;
|
|
171
|
+
if (!validate(record)) {
|
|
172
|
+
const errs = (validate.errors ?? [])
|
|
173
|
+
.map((e) => `${e.instancePath} ${e.message}`)
|
|
174
|
+
.join("; ");
|
|
175
|
+
const msg = `Record fails schema validation: ${errs}. Edit the record before moving.`;
|
|
176
|
+
if (jsonMode) outputJsonError("move", msg);
|
|
177
|
+
else console.error(chalk.red(`Error: ${msg}`));
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Informational scan for inbound references. Skips both source
|
|
183
|
+
// and target files (target hasn't been written yet; source
|
|
184
|
+
// references to the record itself are not interesting here).
|
|
185
|
+
const incomingRefs = await findIncomingReferences(
|
|
186
|
+
record.id ?? "",
|
|
187
|
+
process.cwd(),
|
|
188
|
+
new Set([sourcePath, targetPath]),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (options.dryRun) {
|
|
192
|
+
if (jsonMode) {
|
|
193
|
+
outputJson({
|
|
194
|
+
success: true,
|
|
195
|
+
command: "move",
|
|
196
|
+
dryRun: true,
|
|
197
|
+
sourceDomain,
|
|
198
|
+
targetDomain,
|
|
199
|
+
record: {
|
|
200
|
+
id: record.id ?? null,
|
|
201
|
+
type: record.type,
|
|
202
|
+
summary: getRecordSummary(record),
|
|
203
|
+
},
|
|
204
|
+
incomingReferences: incomingRefs,
|
|
205
|
+
});
|
|
206
|
+
} else if (!isQuiet()) {
|
|
207
|
+
const rid = record.id ? ` ${accent(record.id)}` : "";
|
|
208
|
+
console.log(
|
|
209
|
+
`${chalk.yellow("[DRY RUN]")} ${brand(`Would move ${record.type}`)}${rid} ${brand(
|
|
210
|
+
`from ${sourceDomain} → ${targetDomain}`,
|
|
211
|
+
)}: ${getRecordSummary(record)}`,
|
|
212
|
+
);
|
|
213
|
+
if (incomingRefs.length > 0) {
|
|
214
|
+
console.log(
|
|
215
|
+
chalk.dim(
|
|
216
|
+
` ${incomingRefs.length} inbound reference(s) detected; ID is preserved so links remain valid.`,
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Fire pre-record against the target domain. A blocked hook
|
|
225
|
+
// aborts the move (record stays in source intact).
|
|
226
|
+
const preResult = await runHooks<{ domain: string; record: ExpertiseRecord }>(
|
|
227
|
+
"pre-record",
|
|
228
|
+
{ domain: targetDomain, record },
|
|
229
|
+
);
|
|
230
|
+
if (preResult.blocked) {
|
|
231
|
+
const reason = preResult.blockReason ?? "pre-record hook blocked the move";
|
|
232
|
+
if (jsonMode) outputJsonError("move", reason);
|
|
233
|
+
else console.error(chalk.red(`Error: ${reason}`));
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
let recordToWrite = record;
|
|
238
|
+
if (preResult.ranAny && preResult.payload?.record) {
|
|
239
|
+
const mutated = preResult.payload.record;
|
|
240
|
+
if (mutated !== record) {
|
|
241
|
+
if (!validate(mutated)) {
|
|
242
|
+
const errs = (validate.errors ?? [])
|
|
243
|
+
.map((e) => `${e.instancePath} ${e.message}`)
|
|
244
|
+
.join("; ");
|
|
245
|
+
const msg = `pre-record hook produced an invalid record: ${errs}`;
|
|
246
|
+
if (jsonMode) outputJsonError("move", msg);
|
|
247
|
+
else console.error(chalk.red(`Error: ${msg}`));
|
|
248
|
+
process.exitCode = 1;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
recordToWrite = mutated;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Append to target, then remove from source. Locks are nested
|
|
256
|
+
// in a fixed order (target before source) to keep concurrent
|
|
257
|
+
// moves deadlock-free. If a crash lands between the append and
|
|
258
|
+
// the source rewrite, the record appears in both domains —
|
|
259
|
+
// recoverable by hand and strictly better than losing it.
|
|
260
|
+
await withFileLock(targetPath, async () => {
|
|
261
|
+
await appendRecord(targetPath, recordToWrite);
|
|
262
|
+
await withFileLock(sourcePath, async () => {
|
|
263
|
+
const currentSource = await readExpertiseFile(sourcePath);
|
|
264
|
+
const filtered = currentSource.filter((r) => r.id !== recordToWrite.id);
|
|
265
|
+
await writeExpertiseFile(sourcePath, filtered);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const postResult = await runHooks("post-record", {
|
|
270
|
+
domain: targetDomain,
|
|
271
|
+
record: recordToWrite,
|
|
272
|
+
action: "created",
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const warnings = [...preResult.warnings, ...postResult.warnings];
|
|
276
|
+
|
|
277
|
+
if (jsonMode) {
|
|
278
|
+
outputJson({
|
|
279
|
+
success: true,
|
|
280
|
+
command: "move",
|
|
281
|
+
sourceDomain,
|
|
282
|
+
targetDomain,
|
|
283
|
+
record: {
|
|
284
|
+
id: recordToWrite.id ?? null,
|
|
285
|
+
type: recordToWrite.type,
|
|
286
|
+
summary: getRecordSummary(recordToWrite),
|
|
287
|
+
},
|
|
288
|
+
incomingReferences: incomingRefs,
|
|
289
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
290
|
+
});
|
|
291
|
+
} else if (!isQuiet()) {
|
|
292
|
+
const rid = recordToWrite.id ? ` ${accent(recordToWrite.id)}` : "";
|
|
293
|
+
console.log(
|
|
294
|
+
`${brand("✓")} ${brand(`Moved ${recordToWrite.type}`)}${rid} ${brand(
|
|
295
|
+
`from ${sourceDomain} → ${targetDomain}`,
|
|
296
|
+
)}: ${getRecordSummary(recordToWrite)}`,
|
|
297
|
+
);
|
|
298
|
+
if (incomingRefs.length > 0) {
|
|
299
|
+
console.log(
|
|
300
|
+
chalk.yellow(
|
|
301
|
+
` ${incomingRefs.length} inbound reference(s) found; ID preserved so existing links still resolve:`,
|
|
302
|
+
),
|
|
303
|
+
);
|
|
304
|
+
for (const ref of incomingRefs) {
|
|
305
|
+
console.log(chalk.dim(` ${ref.domain}/${ref.id ?? "(no id)"} via ${ref.field}`));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
for (const w of warnings) console.log(chalk.yellow(` warning: ${w}`));
|
|
309
|
+
}
|
|
310
|
+
} catch (err) {
|
|
311
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
312
|
+
const msg = "No .loam/ directory found. Run `loam init` first.";
|
|
313
|
+
if (jsonMode) outputJsonError("move", msg);
|
|
314
|
+
else console.error(chalk.red(`Error: ${msg}`));
|
|
315
|
+
} else {
|
|
316
|
+
if (jsonMode) outputJsonError("move", (err as Error).message);
|
|
317
|
+
else console.error(chalk.red(`Error: ${(err as Error).message}`));
|
|
318
|
+
}
|
|
319
|
+
process.exitCode = 1;
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
);
|
|
323
|
+
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import type { Command } from "commander";
|
|
5
|
+
import type { SessionCloseConfig } from "../schemas/config.ts";
|
|
6
|
+
import { readConfig } from "../utils/config.ts";
|
|
7
|
+
import { getSessionEndReminder } from "../utils/format.ts";
|
|
8
|
+
import { outputJson, outputJsonError } from "../utils/json-output.ts";
|
|
9
|
+
import { hasMarkerSection, replaceMarkerSection, wrapInMarkers } from "../utils/markers.ts";
|
|
10
|
+
import { isQuiet } from "../utils/palette.ts";
|
|
11
|
+
import { getCurrentVersion } from "../utils/version.ts";
|
|
12
|
+
|
|
13
|
+
// Single marker carries both display and detection: snippets are considered
|
|
14
|
+
// current iff they include the marker for the running CLI's package version.
|
|
15
|
+
// Patch bumps therefore prompt re-run, which is the desired UX (the visible
|
|
16
|
+
// version in CLAUDE.md should track the installed Loam).
|
|
17
|
+
export function getVersionMarker(): string {
|
|
18
|
+
return `<!-- loam-onboard:v${getCurrentVersion()} -->`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildSnippet(sessionClose?: SessionCloseConfig): string {
|
|
22
|
+
const pkgVersion = getCurrentVersion();
|
|
23
|
+
return `## Project Expertise (Loam)
|
|
24
|
+
<!-- loam-onboard:v${pkgVersion} -->
|
|
25
|
+
|
|
26
|
+
This project uses [Loam](https://github.com/hgoudat/loam) v${pkgVersion} for structured expertise management.
|
|
27
|
+
|
|
28
|
+
**At the start of every session**, run:
|
|
29
|
+
\`\`\`bash
|
|
30
|
+
lm prime
|
|
31
|
+
\`\`\`
|
|
32
|
+
|
|
33
|
+
Injects project-specific conventions, patterns, decisions, failures, references, and guides into
|
|
34
|
+
your context. Run \`lm prime --files src/foo.ts\` before editing a file to load only records
|
|
35
|
+
relevant to that path (per-file framing, classification age, and confirmation scores included).
|
|
36
|
+
|
|
37
|
+
For monolith projects where dumping every record wastes context, set
|
|
38
|
+
\`prime.default_mode: manifest\` in \`.loam/loam.config.yaml\` (or pass \`--manifest\`) to emit a
|
|
39
|
+
quick reference + domain index. Agents then scope-load with \`lm prime <domain>\` or
|
|
40
|
+
\`lm prime --files <path>\`.
|
|
41
|
+
|
|
42
|
+
**Before completing your task**, record insights worth preserving — conventions discovered,
|
|
43
|
+
patterns applied, failures encountered, or decisions made:
|
|
44
|
+
\`\`\`bash
|
|
45
|
+
lm record <domain> --type <convention|pattern|failure|decision|reference|guide> --description "..."
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
Evidence auto-populates from git (current commit + changed files). Link explicitly with
|
|
49
|
+
\`--evidence-sprout <id>\` / \`--evidence-gh <id>\` / \`--evidence-linear <id>\` / \`--evidence-bead <id>\`,
|
|
50
|
+
\`--evidence-commit <sha>\`, or \`--relates-to <mx-id>\`. Upserts of named records merge outcomes
|
|
51
|
+
instead of replacing them; validation failures print a copy-paste retry hint with missing fields
|
|
52
|
+
pre-filled.
|
|
53
|
+
|
|
54
|
+
Run \`lm status\` for domain health, \`lm doctor\` to check record integrity (add \`--fix\` to strip
|
|
55
|
+
broken file anchors), \`lm --help\` for the full command list. Write commands use file locking and
|
|
56
|
+
atomic writes, so multiple agents can record concurrently. Expertise survives \`git worktree\`
|
|
57
|
+
cleanup — \`.loam/\` resolves to the main repo.
|
|
58
|
+
|
|
59
|
+
\`lm prune\` soft-archives stale records to \`.loam/archive/\` instead of deleting them; pass
|
|
60
|
+
\`--hard\` for true deletion. Restore an archived record with \`lm restore <id>\`. Do not read
|
|
61
|
+
\`.loam/archive/\` directly — those records are stale by definition. If you need historical
|
|
62
|
+
context, run \`lm search --archived <query>\`.
|
|
63
|
+
|
|
64
|
+
${getSessionEndReminder("embedded", sessionClose)}
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const LEGACY_HEADER = "## Project Expertise (Loam)";
|
|
69
|
+
const LEGACY_TAIL = 'loam validate && git add .loam/ && git commit -m "loam: record learnings"';
|
|
70
|
+
|
|
71
|
+
function getSnippet(_provider: string | undefined, sessionClose?: SessionCloseConfig): string {
|
|
72
|
+
// All providers use the same standardized snippet.
|
|
73
|
+
return buildSnippet(sessionClose);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// `lm onboard` may run before `lm init`, so reading the config gracefully
|
|
77
|
+
// degrades to "no preset configured" rather than failing the command.
|
|
78
|
+
async function readSessionCloseConfig(cwd: string): Promise<SessionCloseConfig | undefined> {
|
|
79
|
+
try {
|
|
80
|
+
const config = await readConfig(cwd);
|
|
81
|
+
return config.prime?.session_close;
|
|
82
|
+
} catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
88
|
+
try {
|
|
89
|
+
await access(filePath);
|
|
90
|
+
return true;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface OnboardTarget {
|
|
97
|
+
path: string;
|
|
98
|
+
fileName: string;
|
|
99
|
+
exists: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hasLegacySnippet(content: string): boolean {
|
|
103
|
+
return content.includes(LEGACY_HEADER);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function replaceLegacySnippet(content: string, newSection: string): string {
|
|
107
|
+
const headerIdx = content.indexOf(LEGACY_HEADER);
|
|
108
|
+
if (headerIdx === -1) return content;
|
|
109
|
+
|
|
110
|
+
const tailIdx = content.indexOf(LEGACY_TAIL, headerIdx);
|
|
111
|
+
|
|
112
|
+
let endIdx: number;
|
|
113
|
+
if (tailIdx !== -1) {
|
|
114
|
+
// Find the closing ``` after the tail line
|
|
115
|
+
const afterTail = content.indexOf("```", tailIdx + LEGACY_TAIL.length);
|
|
116
|
+
if (afterTail !== -1) {
|
|
117
|
+
endIdx = afterTail + 3;
|
|
118
|
+
// Consume trailing newlines
|
|
119
|
+
while (endIdx < content.length && content[endIdx] === "\n") {
|
|
120
|
+
endIdx++;
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
endIdx = content.length;
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
// Tail not found (user edited the snippet): take from header to EOF
|
|
127
|
+
endIdx = content.length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const before = content.substring(0, headerIdx);
|
|
131
|
+
const after = content.substring(endIdx);
|
|
132
|
+
|
|
133
|
+
return before + newSection + after;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isSnippetCurrent(content: string): boolean {
|
|
137
|
+
if (!hasMarkerSection(content)) return false;
|
|
138
|
+
return content.includes(getVersionMarker());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function findSnippetLocations(cwd: string): Promise<OnboardTarget[]> {
|
|
142
|
+
const candidates = [
|
|
143
|
+
{ fileName: "CLAUDE.md", path: join(cwd, "CLAUDE.md") },
|
|
144
|
+
{ fileName: ".claude/CLAUDE.md", path: join(cwd, ".claude", "CLAUDE.md") },
|
|
145
|
+
{ fileName: "AGENTS.md", path: join(cwd, "AGENTS.md") },
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
const results: OnboardTarget[] = [];
|
|
149
|
+
for (const c of candidates) {
|
|
150
|
+
const exists = await fileExists(c.path);
|
|
151
|
+
if (exists) {
|
|
152
|
+
const content = await readFile(c.path, "utf-8");
|
|
153
|
+
if (hasMarkerSection(content) || hasLegacySnippet(content)) {
|
|
154
|
+
results.push({ ...c, exists: true });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return results;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function resolveTargetFile(cwd: string): Promise<{
|
|
162
|
+
target: OnboardTarget;
|
|
163
|
+
duplicates: OnboardTarget[];
|
|
164
|
+
}> {
|
|
165
|
+
const withSnippet = await findSnippetLocations(cwd);
|
|
166
|
+
|
|
167
|
+
// If snippet found in one or more locations, use the first; others are duplicates
|
|
168
|
+
const [firstSnippet, ...restSnippets] = withSnippet;
|
|
169
|
+
if (firstSnippet !== undefined) {
|
|
170
|
+
return {
|
|
171
|
+
target: firstSnippet,
|
|
172
|
+
duplicates: restSnippets,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// No snippet found anywhere. Prefer existing CLAUDE.md, else AGENTS.md
|
|
177
|
+
if (await fileExists(join(cwd, "CLAUDE.md"))) {
|
|
178
|
+
return {
|
|
179
|
+
target: {
|
|
180
|
+
fileName: "CLAUDE.md",
|
|
181
|
+
path: join(cwd, "CLAUDE.md"),
|
|
182
|
+
exists: true,
|
|
183
|
+
},
|
|
184
|
+
duplicates: [],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// If AGENTS.md already exists (no snippet), append there to respect Codex-style projects.
|
|
189
|
+
// Only create a new file when neither exists — prefer CLAUDE.md over AGENTS.md in that case.
|
|
190
|
+
const agentsExists = await fileExists(join(cwd, "AGENTS.md"));
|
|
191
|
+
if (agentsExists) {
|
|
192
|
+
return {
|
|
193
|
+
target: {
|
|
194
|
+
fileName: "AGENTS.md",
|
|
195
|
+
path: join(cwd, "AGENTS.md"),
|
|
196
|
+
exists: true,
|
|
197
|
+
},
|
|
198
|
+
duplicates: [],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
target: {
|
|
204
|
+
fileName: "CLAUDE.md",
|
|
205
|
+
path: join(cwd, "CLAUDE.md"),
|
|
206
|
+
exists: false,
|
|
207
|
+
},
|
|
208
|
+
duplicates: [],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
type OnboardAction =
|
|
213
|
+
| "created"
|
|
214
|
+
| "appended"
|
|
215
|
+
| "updated"
|
|
216
|
+
| "migrated"
|
|
217
|
+
| "up_to_date"
|
|
218
|
+
| "not_installed"
|
|
219
|
+
| "outdated"
|
|
220
|
+
| "legacy";
|
|
221
|
+
|
|
222
|
+
export async function runOnboard(options: {
|
|
223
|
+
stdout?: boolean;
|
|
224
|
+
provider?: string;
|
|
225
|
+
check?: boolean;
|
|
226
|
+
cwd?: string;
|
|
227
|
+
jsonMode?: boolean;
|
|
228
|
+
}): Promise<void> {
|
|
229
|
+
const cwd = options.cwd ?? process.cwd();
|
|
230
|
+
const sessionClose = await readSessionCloseConfig(cwd);
|
|
231
|
+
const snippet = getSnippet(options.provider, sessionClose);
|
|
232
|
+
const wrappedSnippet = wrapInMarkers(snippet);
|
|
233
|
+
|
|
234
|
+
if (options.stdout) {
|
|
235
|
+
process.stdout.write(wrappedSnippet);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const { target, duplicates } = await resolveTargetFile(cwd);
|
|
240
|
+
|
|
241
|
+
// --check: read-only inspection
|
|
242
|
+
if (options.check) {
|
|
243
|
+
let action: OnboardAction;
|
|
244
|
+
|
|
245
|
+
if (!target.exists) {
|
|
246
|
+
action = "not_installed";
|
|
247
|
+
} else {
|
|
248
|
+
const content = await readFile(target.path, "utf-8");
|
|
249
|
+
if (hasMarkerSection(content)) {
|
|
250
|
+
action = isSnippetCurrent(content) ? "up_to_date" : "outdated";
|
|
251
|
+
} else if (hasLegacySnippet(content)) {
|
|
252
|
+
action = "legacy";
|
|
253
|
+
} else {
|
|
254
|
+
action = "not_installed";
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (options.jsonMode) {
|
|
259
|
+
outputJson({
|
|
260
|
+
success: true,
|
|
261
|
+
command: "onboard",
|
|
262
|
+
file: target.fileName,
|
|
263
|
+
action,
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
const messages: Record<string, string> = {
|
|
267
|
+
not_installed: `Loam snippet is not installed in ${target.fileName}.`,
|
|
268
|
+
up_to_date: `Loam snippet in ${target.fileName} is up to date.`,
|
|
269
|
+
outdated: `Loam snippet in ${target.fileName} is outdated. Run \`lm onboard\` to update.`,
|
|
270
|
+
legacy: `Loam snippet in ${target.fileName} uses legacy format (no markers). Run \`lm onboard\` to migrate.`,
|
|
271
|
+
};
|
|
272
|
+
const colors: Record<string, (s: string) => string> = {
|
|
273
|
+
not_installed: chalk.yellow,
|
|
274
|
+
up_to_date: chalk.green,
|
|
275
|
+
outdated: chalk.yellow,
|
|
276
|
+
legacy: chalk.yellow,
|
|
277
|
+
};
|
|
278
|
+
const colorFn = colors[action] ?? chalk.white;
|
|
279
|
+
const msg = messages[action] ?? action;
|
|
280
|
+
if (!isQuiet()) console.log(colorFn(msg));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (duplicates.length > 0) {
|
|
284
|
+
const names = duplicates.map((d) => d.fileName).join(", ");
|
|
285
|
+
if (!options.jsonMode && !isQuiet()) {
|
|
286
|
+
console.log(chalk.yellow(`Warning: loam snippet also found in: ${names}`));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Write path
|
|
293
|
+
let action: OnboardAction;
|
|
294
|
+
|
|
295
|
+
if (!target.exists) {
|
|
296
|
+
// Create new file
|
|
297
|
+
await mkdir(dirname(target.path), { recursive: true });
|
|
298
|
+
await writeFile(target.path, `${wrappedSnippet}\n`, "utf-8");
|
|
299
|
+
action = "created";
|
|
300
|
+
} else {
|
|
301
|
+
const content = await readFile(target.path, "utf-8");
|
|
302
|
+
|
|
303
|
+
if (hasMarkerSection(content)) {
|
|
304
|
+
// Check if current
|
|
305
|
+
if (isSnippetCurrent(content)) {
|
|
306
|
+
action = "up_to_date";
|
|
307
|
+
} else {
|
|
308
|
+
// Replace marker section
|
|
309
|
+
const updated = replaceMarkerSection(content, wrappedSnippet);
|
|
310
|
+
if (updated !== null) {
|
|
311
|
+
await writeFile(target.path, updated, "utf-8");
|
|
312
|
+
}
|
|
313
|
+
action = "updated";
|
|
314
|
+
}
|
|
315
|
+
} else if (hasLegacySnippet(content)) {
|
|
316
|
+
// Migrate legacy snippet
|
|
317
|
+
const migrated = replaceLegacySnippet(content, `${wrappedSnippet}\n`);
|
|
318
|
+
await writeFile(target.path, migrated, "utf-8");
|
|
319
|
+
action = "migrated";
|
|
320
|
+
} else {
|
|
321
|
+
// Append to existing file
|
|
322
|
+
await writeFile(target.path, `${content.trimEnd()}\n\n${wrappedSnippet}\n`, "utf-8");
|
|
323
|
+
action = "appended";
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (options.jsonMode) {
|
|
328
|
+
outputJson({
|
|
329
|
+
success: true,
|
|
330
|
+
command: "onboard",
|
|
331
|
+
file: target.fileName,
|
|
332
|
+
action,
|
|
333
|
+
});
|
|
334
|
+
} else {
|
|
335
|
+
const messages: Record<string, string> = {
|
|
336
|
+
created: `Loam onboarding snippet written to ${target.fileName}.`,
|
|
337
|
+
appended: `Loam onboarding snippet appended to ${target.fileName}.`,
|
|
338
|
+
updated: `Loam onboarding snippet updated in ${target.fileName}.`,
|
|
339
|
+
migrated: `Loam onboarding snippet migrated to marker format in ${target.fileName}.`,
|
|
340
|
+
up_to_date: `Loam snippet in ${target.fileName} is already up to date. No changes made.`,
|
|
341
|
+
};
|
|
342
|
+
const color = action === "up_to_date" ? chalk.yellow : chalk.green;
|
|
343
|
+
if (!isQuiet()) console.log(color(messages[action]));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (duplicates.length > 0) {
|
|
347
|
+
const names = duplicates.map((d) => d.fileName).join(", ");
|
|
348
|
+
if (!options.jsonMode && !isQuiet()) {
|
|
349
|
+
console.log(chalk.yellow(`Warning: loam snippet also found in: ${names}`));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function registerOnboardCommand(program: Command): void {
|
|
355
|
+
program
|
|
356
|
+
.command("onboard")
|
|
357
|
+
.description("Generate or update a CLAUDE.md/AGENTS.md snippet pointing to lm prime")
|
|
358
|
+
.option("--stdout", "print snippet to stdout instead of writing to file")
|
|
359
|
+
.option("--provider <provider>", "customize snippet for a specific provider (e.g. claude)")
|
|
360
|
+
.option("--check", "check if onboarding snippet is installed and up to date")
|
|
361
|
+
.action(async (options: { stdout?: boolean; provider?: string; check?: boolean }) => {
|
|
362
|
+
const jsonMode = program.opts().json === true;
|
|
363
|
+
try {
|
|
364
|
+
await runOnboard({ ...options, jsonMode });
|
|
365
|
+
} catch (err) {
|
|
366
|
+
if (jsonMode) {
|
|
367
|
+
outputJsonError("onboard", (err as Error).message);
|
|
368
|
+
} else {
|
|
369
|
+
console.error(chalk.red(`Error: ${(err as Error).message}`));
|
|
370
|
+
}
|
|
371
|
+
process.exitCode = 1;
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|