@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,887 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, join, relative } from "node:path";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import type { Command } from "commander";
|
|
7
|
+
import { getLoamDir, readConfig } from "../utils/config.ts";
|
|
8
|
+
import { getSessionEndReminder } from "../utils/format.ts";
|
|
9
|
+
import { outputJson, outputJsonError } from "../utils/json-output.ts";
|
|
10
|
+
import {
|
|
11
|
+
hasMarkerSection,
|
|
12
|
+
MARKER_END,
|
|
13
|
+
MARKER_START,
|
|
14
|
+
removeMarkerSection,
|
|
15
|
+
} from "../utils/markers.ts";
|
|
16
|
+
import {
|
|
17
|
+
listFilesystemRecipes,
|
|
18
|
+
NPM_RECIPE_PREFIX,
|
|
19
|
+
type ProviderRecipe,
|
|
20
|
+
type RecipeResult,
|
|
21
|
+
type RecipeWithSource,
|
|
22
|
+
resolveRecipe,
|
|
23
|
+
} from "../utils/recipe-discovery.ts";
|
|
24
|
+
|
|
25
|
+
// Read prime.session_close from .loam/loam.config.yaml. Returns undefined
|
|
26
|
+
// when loam isn't initialized yet (setup may run before `lm init`) so the
|
|
27
|
+
// caller falls back to the default preset rather than failing.
|
|
28
|
+
async function readSessionCloseConfig(cwd: string) {
|
|
29
|
+
try {
|
|
30
|
+
const config = await readConfig(cwd);
|
|
31
|
+
return config.prime?.session_close;
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ────────────────────────────────────────────────────────────
|
|
38
|
+
// Git hook helpers
|
|
39
|
+
// ────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const HOOK_MARKER_START = "# loam:start";
|
|
42
|
+
const HOOK_MARKER_END = "# loam:end";
|
|
43
|
+
|
|
44
|
+
const LOAM_HOOK_SECTION = `${HOOK_MARKER_START}
|
|
45
|
+
# Run loam validate before committing
|
|
46
|
+
if command -v loam >/dev/null 2>&1; then
|
|
47
|
+
loam validate
|
|
48
|
+
if [ $? -ne 0 ]; then
|
|
49
|
+
echo "loam validate failed. Commit aborted."
|
|
50
|
+
exit 1
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
${HOOK_MARKER_END}`;
|
|
54
|
+
|
|
55
|
+
async function installGitHook(cwd: string): Promise<RecipeResult> {
|
|
56
|
+
const gitDir = join(cwd, ".git");
|
|
57
|
+
if (!existsSync(gitDir)) {
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
message: "Not a git repository — .git directory not found.",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const hooksDir = join(gitDir, "hooks");
|
|
65
|
+
await mkdir(hooksDir, { recursive: true });
|
|
66
|
+
|
|
67
|
+
const hookPath = join(hooksDir, "pre-commit");
|
|
68
|
+
let content = "";
|
|
69
|
+
|
|
70
|
+
if (existsSync(hookPath)) {
|
|
71
|
+
content = await readFile(hookPath, "utf-8");
|
|
72
|
+
if (content.includes(HOOK_MARKER_START)) {
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
message: "Git pre-commit hook already installed.",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (content) {
|
|
81
|
+
content = `${content.trimEnd()}\n\n${LOAM_HOOK_SECTION}\n`;
|
|
82
|
+
} else {
|
|
83
|
+
content = `#!/bin/sh\n\n${LOAM_HOOK_SECTION}\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await writeFile(hookPath, content, "utf-8");
|
|
87
|
+
await chmod(hookPath, 0o755);
|
|
88
|
+
|
|
89
|
+
return { success: true, message: "Installed loam pre-commit git hook." };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function checkGitHook(cwd: string): Promise<RecipeResult> {
|
|
93
|
+
const hookPath = join(cwd, ".git", "hooks", "pre-commit");
|
|
94
|
+
if (!existsSync(hookPath)) {
|
|
95
|
+
return { success: false, message: "Git pre-commit hook not found." };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const content = await readFile(hookPath, "utf-8");
|
|
99
|
+
if (!content.includes(HOOK_MARKER_START)) {
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
message: "Git pre-commit hook exists but has no loam section.",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { success: true, message: "Git pre-commit hook is installed." };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function removeGitHook(cwd: string): Promise<RecipeResult> {
|
|
110
|
+
const hookPath = join(cwd, ".git", "hooks", "pre-commit");
|
|
111
|
+
if (!existsSync(hookPath)) {
|
|
112
|
+
return {
|
|
113
|
+
success: true,
|
|
114
|
+
message: "Git pre-commit hook not found; nothing to remove.",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const content = await readFile(hookPath, "utf-8");
|
|
119
|
+
if (!content.includes(HOOK_MARKER_START)) {
|
|
120
|
+
return {
|
|
121
|
+
success: true,
|
|
122
|
+
message: "No loam section in pre-commit hook; nothing to remove.",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const startIdx = content.indexOf(HOOK_MARKER_START);
|
|
127
|
+
const endIdx = content.indexOf(HOOK_MARKER_END);
|
|
128
|
+
const before = content.substring(0, startIdx);
|
|
129
|
+
const after = content.substring(endIdx + HOOK_MARKER_END.length);
|
|
130
|
+
const cleaned = (before + after).replace(/\n{3,}/g, "\n\n").trim();
|
|
131
|
+
|
|
132
|
+
// If only the shebang (or nothing) remains, delete the file
|
|
133
|
+
if (!cleaned || cleaned === "#!/bin/sh") {
|
|
134
|
+
await unlink(hookPath);
|
|
135
|
+
return {
|
|
136
|
+
success: true,
|
|
137
|
+
message: "Removed loam pre-commit hook (file deleted).",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await writeFile(hookPath, `${cleaned}\n`, "utf-8");
|
|
142
|
+
return {
|
|
143
|
+
success: true,
|
|
144
|
+
message: "Removed loam section from pre-commit hook.",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ────────────────────────────────────────────────────────────
|
|
149
|
+
// Built-in provider recipes
|
|
150
|
+
// ────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
// ── Claude ──────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
interface ClaudeHookEntry {
|
|
155
|
+
type: string;
|
|
156
|
+
command: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface ClaudeHookGroup {
|
|
160
|
+
matcher: string;
|
|
161
|
+
hooks: ClaudeHookEntry[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface ClaudeSettings {
|
|
165
|
+
hooks?: {
|
|
166
|
+
[event: string]: ClaudeHookGroup[];
|
|
167
|
+
};
|
|
168
|
+
[key: string]: unknown;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const CLAUDE_HOOK_COMMAND = "lm prime";
|
|
172
|
+
|
|
173
|
+
function claudeSettingsPath(cwd: string): string {
|
|
174
|
+
return join(cwd, ".claude", "settings.json");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function hasLoamHook(groups: ClaudeHookGroup[]): boolean {
|
|
178
|
+
return groups.some((g) => g.hooks.some((h) => h.command === CLAUDE_HOOK_COMMAND));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function removeLoamHookGroups(groups: ClaudeHookGroup[]): ClaudeHookGroup[] {
|
|
182
|
+
return groups.filter((g) => !g.hooks.some((h) => h.command === CLAUDE_HOOK_COMMAND));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function createLoamHookGroup(): ClaudeHookGroup {
|
|
186
|
+
return {
|
|
187
|
+
matcher: "",
|
|
188
|
+
hooks: [{ type: "command", command: CLAUDE_HOOK_COMMAND }],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function parseClaudeSettings(raw: string, settingsPath: string): ClaudeSettings {
|
|
193
|
+
try {
|
|
194
|
+
return JSON.parse(raw) as ClaudeSettings;
|
|
195
|
+
} catch (err) {
|
|
196
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
197
|
+
throw new Error(`Failed to parse Claude settings at ${settingsPath}: ${msg}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const claudeRecipe: ProviderRecipe = {
|
|
202
|
+
async install(cwd) {
|
|
203
|
+
const settingsPath = claudeSettingsPath(cwd);
|
|
204
|
+
let settings: ClaudeSettings = {};
|
|
205
|
+
|
|
206
|
+
if (existsSync(settingsPath)) {
|
|
207
|
+
const raw = await readFile(settingsPath, "utf-8");
|
|
208
|
+
settings = parseClaudeSettings(raw, settingsPath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!settings.hooks) {
|
|
212
|
+
settings.hooks = {};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// SessionStart with empty matcher already covers startup, resume, clear,
|
|
216
|
+
// and post-compact reload. PreCompact's stdout never reaches the model
|
|
217
|
+
// after compaction so registering there is dead weight.
|
|
218
|
+
const event = "SessionStart";
|
|
219
|
+
if (!settings.hooks[event]) {
|
|
220
|
+
settings.hooks[event] = [];
|
|
221
|
+
}
|
|
222
|
+
if (hasLoamHook(settings.hooks[event])) {
|
|
223
|
+
return { success: true, message: "Claude hooks already installed." };
|
|
224
|
+
}
|
|
225
|
+
settings.hooks[event].push(createLoamHookGroup());
|
|
226
|
+
|
|
227
|
+
await mkdir(dirname(settingsPath), { recursive: true });
|
|
228
|
+
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
success: true,
|
|
232
|
+
message: "Installed Claude SessionStart hook.",
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
async check(cwd) {
|
|
237
|
+
const settingsPath = claudeSettingsPath(cwd);
|
|
238
|
+
if (!existsSync(settingsPath)) {
|
|
239
|
+
return { success: false, message: "Claude settings.json not found." };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const raw = await readFile(settingsPath, "utf-8");
|
|
243
|
+
const settings = parseClaudeSettings(raw, settingsPath);
|
|
244
|
+
|
|
245
|
+
if (!settings.hooks) {
|
|
246
|
+
return {
|
|
247
|
+
success: false,
|
|
248
|
+
message: "No hooks configured in Claude settings.",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const event = "SessionStart";
|
|
253
|
+
if (!settings.hooks[event] || !hasLoamHook(settings.hooks[event])) {
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
message: `Missing hooks for: ${event}.`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
success: true,
|
|
261
|
+
message: "Claude hooks are installed and correct.",
|
|
262
|
+
};
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
async remove(cwd) {
|
|
266
|
+
const settingsPath = claudeSettingsPath(cwd);
|
|
267
|
+
if (!existsSync(settingsPath)) {
|
|
268
|
+
return {
|
|
269
|
+
success: true,
|
|
270
|
+
message: "Claude settings.json not found; nothing to remove.",
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const raw = await readFile(settingsPath, "utf-8");
|
|
275
|
+
const settings = parseClaudeSettings(raw, settingsPath);
|
|
276
|
+
|
|
277
|
+
if (!settings.hooks) {
|
|
278
|
+
return {
|
|
279
|
+
success: true,
|
|
280
|
+
message: "No hooks in Claude settings; nothing to remove.",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let removed = false;
|
|
285
|
+
for (const event of Object.keys(settings.hooks)) {
|
|
286
|
+
const hookGroup = settings.hooks[event];
|
|
287
|
+
if (!hookGroup) continue;
|
|
288
|
+
const before = hookGroup.length;
|
|
289
|
+
const updated = removeLoamHookGroups(hookGroup);
|
|
290
|
+
settings.hooks[event] = updated;
|
|
291
|
+
if (updated.length < before) {
|
|
292
|
+
removed = true;
|
|
293
|
+
}
|
|
294
|
+
if (updated.length === 0) {
|
|
295
|
+
delete settings.hooks[event];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
300
|
+
settings.hooks = undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
success: true,
|
|
307
|
+
message: removed
|
|
308
|
+
? "Removed loam hooks from Claude settings."
|
|
309
|
+
: "No loam hooks found in Claude settings.",
|
|
310
|
+
};
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// ── Cursor ──────────────────────────────────────────────────
|
|
315
|
+
|
|
316
|
+
function cursorRulePath(cwd: string): string {
|
|
317
|
+
return join(cwd, ".cursor", "rules", "loam.mdc");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Built from config at install/check time so projects can swap the session-close
|
|
321
|
+
// preset (prime.session_close.style) without forking the recipe.
|
|
322
|
+
async function buildCursorRuleContent(cwd: string): Promise<string> {
|
|
323
|
+
const sessionClose = await readSessionCloseConfig(cwd);
|
|
324
|
+
const footer = getSessionEndReminder("embedded", sessionClose);
|
|
325
|
+
return `---
|
|
326
|
+
description: Loam expertise integration
|
|
327
|
+
globs: *
|
|
328
|
+
alwaysApply: true
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
# Loam Expertise
|
|
332
|
+
|
|
333
|
+
At the start of every session, run the following command to load project expertise:
|
|
334
|
+
|
|
335
|
+
\`\`\`
|
|
336
|
+
lm prime
|
|
337
|
+
\`\`\`
|
|
338
|
+
|
|
339
|
+
This injects project-specific conventions, patterns, decisions, and other learnings into your context.
|
|
340
|
+
Use \`lm prime --files src/foo.ts\` to load only records relevant to specific files.
|
|
341
|
+
|
|
342
|
+
Evidence auto-populates from git (current commit + changed files). Link trackers explicitly with \`--evidence-sprout <id>\` / \`--evidence-gh <id>\` / \`--evidence-linear <id>\` / \`--evidence-bead <id>\`, or \`--relates-to <mx-id>\`.
|
|
343
|
+
|
|
344
|
+
${footer}
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const cursorRecipe: ProviderRecipe = {
|
|
349
|
+
async install(cwd) {
|
|
350
|
+
const rulePath = cursorRulePath(cwd);
|
|
351
|
+
const content = await buildCursorRuleContent(cwd);
|
|
352
|
+
|
|
353
|
+
if (existsSync(rulePath)) {
|
|
354
|
+
const existing = await readFile(rulePath, "utf-8");
|
|
355
|
+
if (existing === content) {
|
|
356
|
+
return { success: true, message: "Cursor rule already installed." };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
await mkdir(dirname(rulePath), { recursive: true });
|
|
361
|
+
await writeFile(rulePath, content, "utf-8");
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
success: true,
|
|
365
|
+
message: "Installed Cursor rule at .cursor/rules/loam.mdc.",
|
|
366
|
+
};
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
async check(cwd) {
|
|
370
|
+
const rulePath = cursorRulePath(cwd);
|
|
371
|
+
if (!existsSync(rulePath)) {
|
|
372
|
+
return { success: false, message: "Cursor rule file not found." };
|
|
373
|
+
}
|
|
374
|
+
const content = await readFile(rulePath, "utf-8");
|
|
375
|
+
const expected = await buildCursorRuleContent(cwd);
|
|
376
|
+
if (content !== expected) {
|
|
377
|
+
return {
|
|
378
|
+
success: false,
|
|
379
|
+
message: "Cursor rule file exists but has been modified.",
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return { success: true, message: "Cursor rule is installed and correct." };
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
async remove(cwd) {
|
|
386
|
+
const rulePath = cursorRulePath(cwd);
|
|
387
|
+
if (!existsSync(rulePath)) {
|
|
388
|
+
return {
|
|
389
|
+
success: true,
|
|
390
|
+
message: "Cursor rule not found; nothing to remove.",
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
await unlink(rulePath);
|
|
394
|
+
return { success: true, message: "Removed Cursor rule file." };
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
// ── Codex ───────────────────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
function codexAgentsPath(cwd: string): string {
|
|
401
|
+
return join(cwd, "AGENTS.md");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function buildCodexSection(cwd: string): Promise<string> {
|
|
405
|
+
const sessionClose = await readSessionCloseConfig(cwd);
|
|
406
|
+
const footer = getSessionEndReminder("embedded", sessionClose);
|
|
407
|
+
return `${MARKER_START}
|
|
408
|
+
## Loam Expertise
|
|
409
|
+
|
|
410
|
+
At the start of every session, run \`lm prime\` to load project expertise.
|
|
411
|
+
|
|
412
|
+
This injects project-specific conventions, patterns, decisions, and other learnings into your context.
|
|
413
|
+
Use \`lm prime --files src/foo.ts\` to load only records relevant to specific files.
|
|
414
|
+
|
|
415
|
+
Evidence auto-populates from git (current commit + changed files). Link trackers explicitly with \`--evidence-sprout <id>\` / \`--evidence-gh <id>\` / \`--evidence-linear <id>\` / \`--evidence-bead <id>\`, or \`--relates-to <mx-id>\`.
|
|
416
|
+
|
|
417
|
+
${footer}
|
|
418
|
+
${MARKER_END}`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Codex hooks via .codex/config.toml — Codex 0.124.0+ (April 2026) supports
|
|
422
|
+
// SessionStart hooks whose stdout JSON `additionalContext` is injected as
|
|
423
|
+
// extra developer context. Schema reference:
|
|
424
|
+
// https://developers.openai.com/codex/hooks
|
|
425
|
+
// We fence the managed block in marker comments so we can re-find it for
|
|
426
|
+
// idempotent install and clean removal without disturbing user-added entries.
|
|
427
|
+
|
|
428
|
+
const CODEX_TOML_MARKER_START = "# loam:start — managed by `lm setup codex`";
|
|
429
|
+
const CODEX_TOML_MARKER_END = "# loam:end";
|
|
430
|
+
|
|
431
|
+
const CODEX_TOML_BLOCK = `${CODEX_TOML_MARKER_START}
|
|
432
|
+
[features]
|
|
433
|
+
codex_hooks = true
|
|
434
|
+
|
|
435
|
+
[[hooks.SessionStart]]
|
|
436
|
+
|
|
437
|
+
[[hooks.SessionStart.hooks]]
|
|
438
|
+
type = "command"
|
|
439
|
+
command = "lm prime"
|
|
440
|
+
statusMessage = "Loading loam expertise"
|
|
441
|
+
${CODEX_TOML_MARKER_END}`;
|
|
442
|
+
|
|
443
|
+
function codexConfigPath(cwd: string): string {
|
|
444
|
+
return join(cwd, ".codex", "config.toml");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function hasCodexTomlBlock(content: string): boolean {
|
|
448
|
+
return content.includes(CODEX_TOML_MARKER_START);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function removeCodexTomlBlock(content: string): string {
|
|
452
|
+
const startIdx = content.indexOf(CODEX_TOML_MARKER_START);
|
|
453
|
+
if (startIdx === -1) return content;
|
|
454
|
+
const endMarkerIdx = content.indexOf(CODEX_TOML_MARKER_END, startIdx);
|
|
455
|
+
if (endMarkerIdx === -1) return content;
|
|
456
|
+
const endLineIdx = content.indexOf("\n", endMarkerIdx);
|
|
457
|
+
const cutEnd = endLineIdx === -1 ? content.length : endLineIdx + 1;
|
|
458
|
+
|
|
459
|
+
const before = content.substring(0, startIdx);
|
|
460
|
+
const after = content.substring(cutEnd);
|
|
461
|
+
const cleaned = (before + after).replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
462
|
+
return cleaned ? `${cleaned}\n` : "";
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const codexRecipe: ProviderRecipe = {
|
|
466
|
+
async install(cwd) {
|
|
467
|
+
const agentsPath = codexAgentsPath(cwd);
|
|
468
|
+
const tomlPath = codexConfigPath(cwd);
|
|
469
|
+
|
|
470
|
+
// 1. AGENTS.md — fallback prose for Codex versions without hook support
|
|
471
|
+
let agentsContent = "";
|
|
472
|
+
let agentsAlready = false;
|
|
473
|
+
if (existsSync(agentsPath)) {
|
|
474
|
+
agentsContent = await readFile(agentsPath, "utf-8");
|
|
475
|
+
agentsAlready = hasMarkerSection(agentsContent);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (!agentsAlready) {
|
|
479
|
+
const codexSection = await buildCodexSection(cwd);
|
|
480
|
+
const newAgents = agentsContent
|
|
481
|
+
? `${agentsContent.trimEnd()}\n\n${codexSection}\n`
|
|
482
|
+
: `${codexSection}\n`;
|
|
483
|
+
await writeFile(agentsPath, newAgents, "utf-8");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 2. .codex/config.toml — SessionStart hook running `lm prime`
|
|
487
|
+
let tomlContent = "";
|
|
488
|
+
let tomlAlready = false;
|
|
489
|
+
if (existsSync(tomlPath)) {
|
|
490
|
+
tomlContent = await readFile(tomlPath, "utf-8");
|
|
491
|
+
tomlAlready = hasCodexTomlBlock(tomlContent);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (!tomlAlready) {
|
|
495
|
+
await mkdir(dirname(tomlPath), { recursive: true });
|
|
496
|
+
const newToml = tomlContent.trim()
|
|
497
|
+
? `${tomlContent.trimEnd()}\n\n${CODEX_TOML_BLOCK}\n`
|
|
498
|
+
: `${CODEX_TOML_BLOCK}\n`;
|
|
499
|
+
await writeFile(tomlPath, newToml, "utf-8");
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (agentsAlready && tomlAlready) {
|
|
503
|
+
return {
|
|
504
|
+
success: true,
|
|
505
|
+
message: "Codex integration already installed (AGENTS.md + .codex/config.toml).",
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const installed: string[] = [];
|
|
510
|
+
if (!agentsAlready) installed.push("AGENTS.md loam section");
|
|
511
|
+
if (!tomlAlready) installed.push(".codex/config.toml SessionStart hook");
|
|
512
|
+
return { success: true, message: `Installed Codex integration: ${installed.join(", ")}.` };
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
async check(cwd) {
|
|
516
|
+
const agentsPath = codexAgentsPath(cwd);
|
|
517
|
+
const tomlPath = codexConfigPath(cwd);
|
|
518
|
+
|
|
519
|
+
if (!existsSync(agentsPath)) {
|
|
520
|
+
return { success: false, message: "AGENTS.md not found." };
|
|
521
|
+
}
|
|
522
|
+
const agentsContent = await readFile(agentsPath, "utf-8");
|
|
523
|
+
if (!hasMarkerSection(agentsContent)) {
|
|
524
|
+
return {
|
|
525
|
+
success: false,
|
|
526
|
+
message: "AGENTS.md exists but has no loam section.",
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!existsSync(tomlPath)) {
|
|
531
|
+
return { success: false, message: ".codex/config.toml not found." };
|
|
532
|
+
}
|
|
533
|
+
const tomlContent = await readFile(tomlPath, "utf-8");
|
|
534
|
+
if (!hasCodexTomlBlock(tomlContent)) {
|
|
535
|
+
return {
|
|
536
|
+
success: false,
|
|
537
|
+
message: ".codex/config.toml exists but has no loam SessionStart hook.",
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
success: true,
|
|
543
|
+
message: "Codex integration installed (AGENTS.md + .codex/config.toml).",
|
|
544
|
+
};
|
|
545
|
+
},
|
|
546
|
+
|
|
547
|
+
async remove(cwd) {
|
|
548
|
+
const agentsPath = codexAgentsPath(cwd);
|
|
549
|
+
const tomlPath = codexConfigPath(cwd);
|
|
550
|
+
const removed: string[] = [];
|
|
551
|
+
|
|
552
|
+
if (existsSync(agentsPath)) {
|
|
553
|
+
const content = await readFile(agentsPath, "utf-8");
|
|
554
|
+
if (hasMarkerSection(content)) {
|
|
555
|
+
const cleaned = removeMarkerSection(content);
|
|
556
|
+
await writeFile(agentsPath, cleaned, "utf-8");
|
|
557
|
+
removed.push("AGENTS.md loam section");
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (existsSync(tomlPath)) {
|
|
562
|
+
const content = await readFile(tomlPath, "utf-8");
|
|
563
|
+
if (hasCodexTomlBlock(content)) {
|
|
564
|
+
const cleaned = removeCodexTomlBlock(content);
|
|
565
|
+
if (cleaned) {
|
|
566
|
+
await writeFile(tomlPath, cleaned, "utf-8");
|
|
567
|
+
} else {
|
|
568
|
+
// Block was the whole file — leave an empty file rather than
|
|
569
|
+
// deleting, so user-managed `[features]` etc. coming back
|
|
570
|
+
// later doesn't fight a recreate race. Empty file is harmless.
|
|
571
|
+
await writeFile(tomlPath, "", "utf-8");
|
|
572
|
+
}
|
|
573
|
+
removed.push(".codex/config.toml SessionStart hook");
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (removed.length === 0) {
|
|
578
|
+
return {
|
|
579
|
+
success: true,
|
|
580
|
+
message: "No Codex integration found; nothing to remove.",
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
return { success: true, message: `Removed Codex integration: ${removed.join(", ")}.` };
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// ── Recipe registry ─────────────────────────────────────────
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Built-in recipes shipped with loam. Filesystem (`.loam/recipes/<name>.{ts,sh}`)
|
|
591
|
+
* and npm (`loam-recipe-<name>`) discovery is handled by `resolveRecipe` in
|
|
592
|
+
* `utils/recipe-discovery.ts` and takes precedence over these in that order.
|
|
593
|
+
*/
|
|
594
|
+
const BUILTIN_RECIPES = {
|
|
595
|
+
claude: claudeRecipe,
|
|
596
|
+
cursor: cursorRecipe,
|
|
597
|
+
codex: codexRecipe,
|
|
598
|
+
} as const satisfies Record<string, ProviderRecipe>;
|
|
599
|
+
|
|
600
|
+
/** @deprecated kept as alias for `BUILTIN_RECIPES` — used by tests. */
|
|
601
|
+
const recipes = BUILTIN_RECIPES;
|
|
602
|
+
|
|
603
|
+
const BUILTIN_PROVIDER_NAMES = Object.keys(BUILTIN_RECIPES).sort();
|
|
604
|
+
|
|
605
|
+
// ── Exported helpers for testing ────────────────────────────
|
|
606
|
+
|
|
607
|
+
export {
|
|
608
|
+
BUILTIN_RECIPES,
|
|
609
|
+
recipes,
|
|
610
|
+
BUILTIN_PROVIDER_NAMES,
|
|
611
|
+
buildCursorRuleContent,
|
|
612
|
+
buildCodexSection,
|
|
613
|
+
CLAUDE_HOOK_COMMAND,
|
|
614
|
+
LOAM_HOOK_SECTION,
|
|
615
|
+
installGitHook,
|
|
616
|
+
checkGitHook,
|
|
617
|
+
removeGitHook,
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
export type { ProviderRecipe };
|
|
621
|
+
|
|
622
|
+
// ── Command registration ────────────────────────────────────
|
|
623
|
+
|
|
624
|
+
export function registerSetupCommand(program: Command): void {
|
|
625
|
+
program
|
|
626
|
+
.command("setup")
|
|
627
|
+
.argument(
|
|
628
|
+
"[provider]",
|
|
629
|
+
`agent provider (built-in: ${BUILTIN_PROVIDER_NAMES.join(", ")}; or any name resolvable from .loam/recipes/ or ${NPM_RECIPE_PREFIX}*)`,
|
|
630
|
+
)
|
|
631
|
+
.description("Set up loam integration for a specific agent provider")
|
|
632
|
+
.option("--check", "verify provider integration is installed")
|
|
633
|
+
.option("--remove", "remove provider integration")
|
|
634
|
+
.option("--hooks", "install a pre-commit git hook running loam validate")
|
|
635
|
+
.option("--list", "list discovered providers (built-in, .loam/recipes/, loam-recipe-* npm)")
|
|
636
|
+
.action(
|
|
637
|
+
async (
|
|
638
|
+
provider: string | undefined,
|
|
639
|
+
options: {
|
|
640
|
+
check?: boolean;
|
|
641
|
+
remove?: boolean;
|
|
642
|
+
hooks?: boolean;
|
|
643
|
+
list?: boolean;
|
|
644
|
+
},
|
|
645
|
+
) => {
|
|
646
|
+
const jsonMode = program.opts().json === true;
|
|
647
|
+
|
|
648
|
+
// Verify .loam/ exists
|
|
649
|
+
const loamDir = getLoamDir();
|
|
650
|
+
if (!existsSync(loamDir)) {
|
|
651
|
+
if (jsonMode) {
|
|
652
|
+
outputJsonError("setup", "No .loam/ directory found. Run `loam init` first.");
|
|
653
|
+
} else {
|
|
654
|
+
console.error(chalk.red("Error: No .loam/ directory found. Run `loam init` first."));
|
|
655
|
+
}
|
|
656
|
+
process.exitCode = 1;
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Handle --list (no provider needed)
|
|
661
|
+
if (options.list) {
|
|
662
|
+
await runList(process.cwd(), jsonMode);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (!provider && !options.hooks) {
|
|
667
|
+
if (jsonMode) {
|
|
668
|
+
outputJsonError("setup", "Specify a provider, use --hooks, or use --list.");
|
|
669
|
+
} else {
|
|
670
|
+
console.error(chalk.red("Error: specify a provider, use --hooks, or use --list."));
|
|
671
|
+
}
|
|
672
|
+
process.exitCode = 1;
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Handle --hooks
|
|
677
|
+
if (options.hooks) {
|
|
678
|
+
const cwd = process.cwd();
|
|
679
|
+
let hookResult: RecipeResult;
|
|
680
|
+
const action = options.check ? "check" : options.remove ? "remove" : "install";
|
|
681
|
+
if (options.check) {
|
|
682
|
+
hookResult = await checkGitHook(cwd);
|
|
683
|
+
} else if (options.remove) {
|
|
684
|
+
hookResult = await removeGitHook(cwd);
|
|
685
|
+
} else {
|
|
686
|
+
hookResult = await installGitHook(cwd);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (jsonMode) {
|
|
690
|
+
outputJson({
|
|
691
|
+
success: hookResult.success,
|
|
692
|
+
command: "setup",
|
|
693
|
+
target: "hooks",
|
|
694
|
+
action,
|
|
695
|
+
message: hookResult.message,
|
|
696
|
+
});
|
|
697
|
+
} else if (hookResult.success) {
|
|
698
|
+
console.log(chalk.green(`\u2714 ${hookResult.message}`));
|
|
699
|
+
} else {
|
|
700
|
+
console.error(chalk.red(`\u2716 ${hookResult.message}`));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (!hookResult.success) {
|
|
704
|
+
process.exitCode = 1;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// If no provider, stop here
|
|
708
|
+
if (!provider) return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Handle provider
|
|
712
|
+
if (!provider) return;
|
|
713
|
+
|
|
714
|
+
const cwd = process.cwd();
|
|
715
|
+
let resolved: RecipeWithSource | null;
|
|
716
|
+
try {
|
|
717
|
+
resolved = await resolveRecipe(provider, cwd, BUILTIN_RECIPES);
|
|
718
|
+
} catch (err) {
|
|
719
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
720
|
+
if (jsonMode) {
|
|
721
|
+
outputJsonError("setup", msg);
|
|
722
|
+
} else {
|
|
723
|
+
console.error(chalk.red(`Error: ${msg}`));
|
|
724
|
+
}
|
|
725
|
+
process.exitCode = 1;
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (!resolved) {
|
|
730
|
+
const hint = `Unknown provider "${provider}". Run \`lm setup --list\` to see discovered providers, or add a recipe at .loam/recipes/${provider}.{ts,sh} or install ${NPM_RECIPE_PREFIX}${provider}.`;
|
|
731
|
+
if (jsonMode) {
|
|
732
|
+
outputJsonError("setup", hint);
|
|
733
|
+
} else {
|
|
734
|
+
console.error(chalk.red(`Error: ${hint}`));
|
|
735
|
+
}
|
|
736
|
+
process.exitCode = 1;
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const action = options.check ? "check" : options.remove ? "remove" : "install";
|
|
741
|
+
let result: RecipeResult;
|
|
742
|
+
try {
|
|
743
|
+
if (options.check) {
|
|
744
|
+
result = await resolved.recipe.check(cwd);
|
|
745
|
+
} else if (options.remove) {
|
|
746
|
+
result = await resolved.recipe.remove(cwd);
|
|
747
|
+
} else {
|
|
748
|
+
result = await resolved.recipe.install(cwd);
|
|
749
|
+
}
|
|
750
|
+
} catch (err) {
|
|
751
|
+
// A recipe that throws (instead of returning a RecipeResult) would
|
|
752
|
+
// otherwise surface as a raw Bun stack trace from a top-level
|
|
753
|
+
// awaited action. Convert to the same shape as a returned failure
|
|
754
|
+
// so users see a one-line, formatted error.
|
|
755
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
756
|
+
const sourceLabel =
|
|
757
|
+
resolved.source === "builtin" ? "built-in" : (resolved.path ?? resolved.source);
|
|
758
|
+
result = {
|
|
759
|
+
success: false,
|
|
760
|
+
message: `recipe "${provider}" ${action} threw (${sourceLabel}): ${msg}`,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (jsonMode) {
|
|
765
|
+
outputJson({
|
|
766
|
+
success: result.success,
|
|
767
|
+
command: "setup",
|
|
768
|
+
provider,
|
|
769
|
+
source: resolved.source,
|
|
770
|
+
...(resolved.path ? { path: resolved.path } : {}),
|
|
771
|
+
action,
|
|
772
|
+
message: result.message,
|
|
773
|
+
});
|
|
774
|
+
} else if (result.success) {
|
|
775
|
+
console.log(chalk.green(`\u2714 ${result.message}`));
|
|
776
|
+
} else if (options.check) {
|
|
777
|
+
console.log(chalk.yellow(`\u2716 ${result.message}`));
|
|
778
|
+
} else {
|
|
779
|
+
console.error(chalk.red(`Error: ${result.message}`));
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (!result.success) {
|
|
783
|
+
process.exitCode = 1;
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
interface ProviderListing {
|
|
790
|
+
name: string;
|
|
791
|
+
source: "builtin" | "filesystem-ts" | "filesystem-sh" | "npm";
|
|
792
|
+
path?: string;
|
|
793
|
+
shadowedBy?: "filesystem-ts" | "filesystem-sh" | "npm";
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function npmShadowExists(name: string, cwd: string): boolean {
|
|
797
|
+
// resolveRecipe prefers filesystem \u2192 npm \u2192 built-in, so an installed
|
|
798
|
+
// `loam-recipe-<name>` package shadows the built-in (and is itself shadowed
|
|
799
|
+
// by a filesystem recipe of the same name). Probe per-builtin rather than
|
|
800
|
+
// enumerating node_modules \u2014 fast, and avoids rummaging through unrelated
|
|
801
|
+
// packages.
|
|
802
|
+
try {
|
|
803
|
+
const requireFn = createRequire(import.meta.url);
|
|
804
|
+
requireFn.resolve(`${NPM_RECIPE_PREFIX}${name}`, { paths: [cwd] });
|
|
805
|
+
return true;
|
|
806
|
+
} catch {
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async function gatherProviderListings(cwd: string): Promise<ProviderListing[]> {
|
|
812
|
+
const fsRecipes = await listFilesystemRecipes(cwd);
|
|
813
|
+
|
|
814
|
+
const listings: ProviderListing[] = [];
|
|
815
|
+
|
|
816
|
+
for (const name of BUILTIN_PROVIDER_NAMES) {
|
|
817
|
+
// Resolution order is filesystem \u2192 npm \u2192 built-in, so a filesystem
|
|
818
|
+
// shadow wins over npm. Report the actual winner so the marker isn't a
|
|
819
|
+
// lie about what `lm setup <name>` would run.
|
|
820
|
+
const fsShadow = fsRecipes.find((r) => r.name === name);
|
|
821
|
+
const shadowedBy: ProviderListing["shadowedBy"] | undefined = fsShadow
|
|
822
|
+
? fsShadow.source
|
|
823
|
+
: npmShadowExists(name, cwd)
|
|
824
|
+
? "npm"
|
|
825
|
+
: undefined;
|
|
826
|
+
listings.push({
|
|
827
|
+
name,
|
|
828
|
+
source: "builtin",
|
|
829
|
+
...(shadowedBy ? { shadowedBy } : {}),
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
for (const fs of fsRecipes) {
|
|
834
|
+
listings.push({ name: fs.name, source: fs.source, path: fs.path });
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
listings.sort((a, b) => {
|
|
838
|
+
if (a.name !== b.name) return a.name.localeCompare(b.name);
|
|
839
|
+
// Filesystem before builtin so the active recipe sorts first.
|
|
840
|
+
const order = { "filesystem-ts": 0, "filesystem-sh": 1, npm: 2, builtin: 3 } as const;
|
|
841
|
+
return order[a.source] - order[b.source];
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
return listings;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async function runList(cwd: string, jsonMode: boolean): Promise<void> {
|
|
848
|
+
const listings = await gatherProviderListings(cwd);
|
|
849
|
+
|
|
850
|
+
if (jsonMode) {
|
|
851
|
+
outputJson({
|
|
852
|
+
success: true,
|
|
853
|
+
command: "setup",
|
|
854
|
+
action: "list",
|
|
855
|
+
providers: listings.map((l) => ({
|
|
856
|
+
name: l.name,
|
|
857
|
+
source: l.source,
|
|
858
|
+
...(l.path ? { path: relative(cwd, l.path) } : {}),
|
|
859
|
+
...(l.shadowedBy ? { shadowed_by: l.shadowedBy } : {}),
|
|
860
|
+
})),
|
|
861
|
+
});
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
console.log(chalk.bold("Available providers:"));
|
|
866
|
+
const labelWidth = Math.max(...listings.map((l) => l.name.length), 6);
|
|
867
|
+
for (const l of listings) {
|
|
868
|
+
const sourceLabel =
|
|
869
|
+
l.source === "builtin"
|
|
870
|
+
? l.shadowedBy
|
|
871
|
+
? chalk.dim(`built-in (shadowed by ${l.shadowedBy})`)
|
|
872
|
+
: "built-in"
|
|
873
|
+
: l.source === "filesystem-ts"
|
|
874
|
+
? `filesystem-ts: ${relative(cwd, l.path ?? "")}`
|
|
875
|
+
: l.source === "filesystem-sh"
|
|
876
|
+
? `filesystem-sh: ${relative(cwd, l.path ?? "")}`
|
|
877
|
+
: `npm: ${NPM_RECIPE_PREFIX}${l.name}`;
|
|
878
|
+
const marker = l.shadowedBy ? chalk.dim("\u00b7") : chalk.green("\u2713");
|
|
879
|
+
console.log(` ${marker} ${l.name.padEnd(labelWidth)} ${sourceLabel}`);
|
|
880
|
+
}
|
|
881
|
+
console.log("");
|
|
882
|
+
console.log(
|
|
883
|
+
chalk.dim(
|
|
884
|
+
`Resolution order: filesystem (.loam/recipes/<name>.{ts,sh}) \u2192 npm (${NPM_RECIPE_PREFIX}<name>) \u2192 built-in.`,
|
|
885
|
+
),
|
|
886
|
+
);
|
|
887
|
+
}
|