@10et/cli 1.0.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 +79 -0
- package/clawdbot-plugin/clawdbot.plugin.json +20 -0
- package/clawdbot-plugin/index.js +555 -0
- package/clawdbot-plugin/index.ts +582 -0
- package/clawdbot-skill/README.md +328 -0
- package/clawdbot-skill/SKILL.md +59 -0
- package/clawdbot-skill/index.ts +683 -0
- package/clawdbot-skill/package.json +28 -0
- package/clawdbot-skill/skill.json +19 -0
- package/dist/commands/agent.d.ts +7 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +170 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/agents.d.ts +5 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +400 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/ci-setup.d.ts +5 -0
- package/dist/commands/ci-setup.d.ts.map +1 -0
- package/dist/commands/ci-setup.js +82 -0
- package/dist/commands/ci-setup.js.map +1 -0
- package/dist/commands/clawdbot.d.ts +11 -0
- package/dist/commands/clawdbot.d.ts.map +1 -0
- package/dist/commands/clawdbot.js +215 -0
- package/dist/commands/clawdbot.js.map +1 -0
- package/dist/commands/context-hub.d.ts +22 -0
- package/dist/commands/context-hub.d.ts.map +1 -0
- package/dist/commands/context-hub.js +3505 -0
- package/dist/commands/context-hub.js.map +1 -0
- package/dist/commands/deploy.d.ts +5 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +371 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/digest.d.ts +12 -0
- package/dist/commands/digest.d.ts.map +1 -0
- package/dist/commands/digest.js +128 -0
- package/dist/commands/digest.js.map +1 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +265 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/eval.d.ts +46 -0
- package/dist/commands/eval.d.ts.map +1 -0
- package/dist/commands/eval.js +427 -0
- package/dist/commands/eval.js.map +1 -0
- package/dist/commands/feedback.d.ts +2 -0
- package/dist/commands/feedback.d.ts.map +1 -0
- package/dist/commands/feedback.js +179 -0
- package/dist/commands/feedback.js.map +1 -0
- package/dist/commands/findings.d.ts +13 -0
- package/dist/commands/findings.d.ts.map +1 -0
- package/dist/commands/findings.js +203 -0
- package/dist/commands/findings.js.map +1 -0
- package/dist/commands/flows.d.ts +10 -0
- package/dist/commands/flows.d.ts.map +1 -0
- package/dist/commands/flows.js +423 -0
- package/dist/commands/flows.js.map +1 -0
- package/dist/commands/gtm-process-update.d.ts +10 -0
- package/dist/commands/gtm-process-update.d.ts.map +1 -0
- package/dist/commands/gtm-process-update.js +101 -0
- package/dist/commands/gtm-process-update.js.map +1 -0
- package/dist/commands/hooks.d.ts +13 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +304 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/hud.d.ts +4 -0
- package/dist/commands/hud.d.ts.map +1 -0
- package/dist/commands/hud.js +300 -0
- package/dist/commands/hud.js.map +1 -0
- package/dist/commands/ide.d.ts +28 -0
- package/dist/commands/ide.d.ts.map +1 -0
- package/dist/commands/ide.js +628 -0
- package/dist/commands/ide.js.map +1 -0
- package/dist/commands/improve.d.ts +11 -0
- package/dist/commands/improve.d.ts.map +1 -0
- package/dist/commands/improve.js +77 -0
- package/dist/commands/improve.js.map +1 -0
- package/dist/commands/init-from-service.d.ts +15 -0
- package/dist/commands/init-from-service.d.ts.map +1 -0
- package/dist/commands/init-from-service.js +540 -0
- package/dist/commands/init-from-service.js.map +1 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +940 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/kanban.d.ts +34 -0
- package/dist/commands/kanban.d.ts.map +1 -0
- package/dist/commands/kanban.js +225 -0
- package/dist/commands/kanban.js.map +1 -0
- package/dist/commands/linear.d.ts +41 -0
- package/dist/commands/linear.d.ts.map +1 -0
- package/dist/commands/linear.js +740 -0
- package/dist/commands/linear.js.map +1 -0
- package/dist/commands/login.d.ts +12 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +780 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/memory.d.ts +38 -0
- package/dist/commands/memory.d.ts.map +1 -0
- package/dist/commands/memory.js +229 -0
- package/dist/commands/memory.js.map +1 -0
- package/dist/commands/migrate-services.d.ts +8 -0
- package/dist/commands/migrate-services.d.ts.map +1 -0
- package/dist/commands/migrate-services.js +182 -0
- package/dist/commands/migrate-services.js.map +1 -0
- package/dist/commands/migrate-tenet.d.ts +25 -0
- package/dist/commands/migrate-tenet.d.ts.map +1 -0
- package/dist/commands/migrate-tenet.js +252 -0
- package/dist/commands/migrate-tenet.js.map +1 -0
- package/dist/commands/onboard.d.ts +24 -0
- package/dist/commands/onboard.d.ts.map +1 -0
- package/dist/commands/onboard.js +873 -0
- package/dist/commands/onboard.js.map +1 -0
- package/dist/commands/openclaw.d.ts +59 -0
- package/dist/commands/openclaw.d.ts.map +1 -0
- package/dist/commands/openclaw.js +775 -0
- package/dist/commands/openclaw.js.map +1 -0
- package/dist/commands/orchestrate.d.ts +14 -0
- package/dist/commands/orchestrate.d.ts.map +1 -0
- package/dist/commands/orchestrate.js +270 -0
- package/dist/commands/orchestrate.js.map +1 -0
- package/dist/commands/organize.d.ts +16 -0
- package/dist/commands/organize.d.ts.map +1 -0
- package/dist/commands/organize.js +334 -0
- package/dist/commands/organize.js.map +1 -0
- package/dist/commands/peter.d.ts +21 -0
- package/dist/commands/peter.d.ts.map +1 -0
- package/dist/commands/peter.js +2778 -0
- package/dist/commands/peter.js.map +1 -0
- package/dist/commands/pi-fleet.d.ts +18 -0
- package/dist/commands/pi-fleet.d.ts.map +1 -0
- package/dist/commands/pi-fleet.js +382 -0
- package/dist/commands/pi-fleet.js.map +1 -0
- package/dist/commands/pi.d.ts +25 -0
- package/dist/commands/pi.d.ts.map +1 -0
- package/dist/commands/pi.js +218 -0
- package/dist/commands/pi.js.map +1 -0
- package/dist/commands/pivot.d.ts +28 -0
- package/dist/commands/pivot.d.ts.map +1 -0
- package/dist/commands/pivot.js +216 -0
- package/dist/commands/pivot.js.map +1 -0
- package/dist/commands/portfolio.d.ts +11 -0
- package/dist/commands/portfolio.d.ts.map +1 -0
- package/dist/commands/portfolio.js +239 -0
- package/dist/commands/portfolio.js.map +1 -0
- package/dist/commands/predict.d.ts +25 -0
- package/dist/commands/predict.d.ts.map +1 -0
- package/dist/commands/predict.js +234 -0
- package/dist/commands/predict.js.map +1 -0
- package/dist/commands/profile.d.ts +46 -0
- package/dist/commands/profile.d.ts.map +1 -0
- package/dist/commands/profile.js +498 -0
- package/dist/commands/profile.js.map +1 -0
- package/dist/commands/ralph.d.ts +11 -0
- package/dist/commands/ralph.d.ts.map +1 -0
- package/dist/commands/ralph.js +102 -0
- package/dist/commands/ralph.js.map +1 -0
- package/dist/commands/repair.d.ts +7 -0
- package/dist/commands/repair.d.ts.map +1 -0
- package/dist/commands/repair.js +356 -0
- package/dist/commands/repair.js.map +1 -0
- package/dist/commands/scope.d.ts +8 -0
- package/dist/commands/scope.d.ts.map +1 -0
- package/dist/commands/scope.js +503 -0
- package/dist/commands/scope.js.map +1 -0
- package/dist/commands/service-agent.d.ts +16 -0
- package/dist/commands/service-agent.d.ts.map +1 -0
- package/dist/commands/service-agent.js +375 -0
- package/dist/commands/service-agent.js.map +1 -0
- package/dist/commands/service-manager.d.ts +12 -0
- package/dist/commands/service-manager.d.ts.map +1 -0
- package/dist/commands/service-manager.js +969 -0
- package/dist/commands/service-manager.js.map +1 -0
- package/dist/commands/service-validate.d.ts +12 -0
- package/dist/commands/service-validate.d.ts.map +1 -0
- package/dist/commands/service-validate.js +619 -0
- package/dist/commands/service-validate.js.map +1 -0
- package/dist/commands/services-create.d.ts +15 -0
- package/dist/commands/services-create.d.ts.map +1 -0
- package/dist/commands/services-create.js +1800 -0
- package/dist/commands/services-create.js.map +1 -0
- package/dist/commands/services-scan.d.ts +13 -0
- package/dist/commands/services-scan.d.ts.map +1 -0
- package/dist/commands/services-scan.js +251 -0
- package/dist/commands/services-scan.js.map +1 -0
- package/dist/commands/services-sync-agents.d.ts +23 -0
- package/dist/commands/services-sync-agents.d.ts.map +1 -0
- package/dist/commands/services-sync-agents.js +207 -0
- package/dist/commands/services-sync-agents.js.map +1 -0
- package/dist/commands/services.d.ts +19 -0
- package/dist/commands/services.d.ts.map +1 -0
- package/dist/commands/services.js +906 -0
- package/dist/commands/services.js.map +1 -0
- package/dist/commands/session.d.ts +7 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +597 -0
- package/dist/commands/session.js.map +1 -0
- package/dist/commands/setup.d.ts +12 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +727 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/skills.d.ts +31 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +314 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/start.d.ts +25 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +251 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +163 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/synopsis.d.ts +54 -0
- package/dist/commands/synopsis.d.ts.map +1 -0
- package/dist/commands/synopsis.js +277 -0
- package/dist/commands/synopsis.js.map +1 -0
- package/dist/commands/telemetry-monitor.d.ts +11 -0
- package/dist/commands/telemetry-monitor.d.ts.map +1 -0
- package/dist/commands/telemetry-monitor.js +224 -0
- package/dist/commands/telemetry-monitor.js.map +1 -0
- package/dist/commands/telemetry-test.d.ts +11 -0
- package/dist/commands/telemetry-test.d.ts.map +1 -0
- package/dist/commands/telemetry-test.js +67 -0
- package/dist/commands/telemetry-test.js.map +1 -0
- package/dist/commands/tenet-agents.d.ts +13 -0
- package/dist/commands/tenet-agents.d.ts.map +1 -0
- package/dist/commands/tenet-agents.js +191 -0
- package/dist/commands/tenet-agents.js.map +1 -0
- package/dist/commands/tenet-setup.d.ts +20 -0
- package/dist/commands/tenet-setup.d.ts.map +1 -0
- package/dist/commands/tenet-setup.js +135 -0
- package/dist/commands/tenet-setup.js.map +1 -0
- package/dist/commands/train.d.ts +51 -0
- package/dist/commands/train.d.ts.map +1 -0
- package/dist/commands/train.js +692 -0
- package/dist/commands/train.js.map +1 -0
- package/dist/commands/update.d.ts +12 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +559 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/validate-settings.d.ts +37 -0
- package/dist/commands/validate-settings.d.ts.map +1 -0
- package/dist/commands/validate-settings.js +197 -0
- package/dist/commands/validate-settings.js.map +1 -0
- package/dist/commands/verify.d.ts +14 -0
- package/dist/commands/verify.d.ts.map +1 -0
- package/dist/commands/verify.js +304 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/viz.d.ts +40 -0
- package/dist/commands/viz.d.ts.map +1 -0
- package/dist/commands/viz.js +877 -0
- package/dist/commands/viz.js.map +1 -0
- package/dist/commands/voice.d.ts +409 -0
- package/dist/commands/voice.d.ts.map +1 -0
- package/dist/commands/voice.js +4765 -0
- package/dist/commands/voice.js.map +1 -0
- package/dist/commands/whoami.d.ts +2 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +24 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/dashboard/index.d.ts +11 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +70 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/dashboard-static/assets/index-BVrmW-ZI.js +154 -0
- package/dist/dashboard-static/assets/index-DtruPD44.css +1 -0
- package/dist/dashboard-static/index.html +16 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1707 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/advanced-setup.d.ts +78 -0
- package/dist/lib/advanced-setup.d.ts.map +1 -0
- package/dist/lib/advanced-setup.js +433 -0
- package/dist/lib/advanced-setup.js.map +1 -0
- package/dist/lib/agent-config.d.ts +86 -0
- package/dist/lib/agent-config.d.ts.map +1 -0
- package/dist/lib/agent-config.js +281 -0
- package/dist/lib/agent-config.js.map +1 -0
- package/dist/lib/agent-generator.d.ts +36 -0
- package/dist/lib/agent-generator.d.ts.map +1 -0
- package/dist/lib/agent-generator.js +400 -0
- package/dist/lib/agent-generator.js.map +1 -0
- package/dist/lib/agent-guards.d.ts +67 -0
- package/dist/lib/agent-guards.d.ts.map +1 -0
- package/dist/lib/agent-guards.js +229 -0
- package/dist/lib/agent-guards.js.map +1 -0
- package/dist/lib/agent-manifest.d.ts +35 -0
- package/dist/lib/agent-manifest.d.ts.map +1 -0
- package/dist/lib/agent-manifest.js +75 -0
- package/dist/lib/agent-manifest.js.map +1 -0
- package/dist/lib/agent-runtime-api.d.ts +32 -0
- package/dist/lib/agent-runtime-api.d.ts.map +1 -0
- package/dist/lib/agent-runtime-api.js +270 -0
- package/dist/lib/agent-runtime-api.js.map +1 -0
- package/dist/lib/agent-session.d.ts +104 -0
- package/dist/lib/agent-session.d.ts.map +1 -0
- package/dist/lib/agent-session.js +954 -0
- package/dist/lib/agent-session.js.map +1 -0
- package/dist/lib/build-supervisor.d.ts +44 -0
- package/dist/lib/build-supervisor.d.ts.map +1 -0
- package/dist/lib/build-supervisor.js +79 -0
- package/dist/lib/build-supervisor.js.map +1 -0
- package/dist/lib/connectors/index.d.ts +19 -0
- package/dist/lib/connectors/index.d.ts.map +1 -0
- package/dist/lib/connectors/index.js +23 -0
- package/dist/lib/connectors/index.js.map +1 -0
- package/dist/lib/counterfactual-engine.d.ts +136 -0
- package/dist/lib/counterfactual-engine.d.ts.map +1 -0
- package/dist/lib/counterfactual-engine.js +417 -0
- package/dist/lib/counterfactual-engine.js.map +1 -0
- package/dist/lib/counterfactual-training-bridge.d.ts +114 -0
- package/dist/lib/counterfactual-training-bridge.d.ts.map +1 -0
- package/dist/lib/counterfactual-training-bridge.js +322 -0
- package/dist/lib/counterfactual-training-bridge.js.map +1 -0
- package/dist/lib/discovery-agent.d.ts +48 -0
- package/dist/lib/discovery-agent.d.ts.map +1 -0
- package/dist/lib/discovery-agent.js +111 -0
- package/dist/lib/discovery-agent.js.map +1 -0
- package/dist/lib/domain/engine.d.ts +11 -0
- package/dist/lib/domain/engine.d.ts.map +1 -0
- package/dist/lib/domain/engine.js +23 -0
- package/dist/lib/domain/engine.js.map +1 -0
- package/dist/lib/domain/index.d.ts +3 -0
- package/dist/lib/domain/index.d.ts.map +1 -0
- package/dist/lib/domain/index.js +3 -0
- package/dist/lib/domain/index.js.map +1 -0
- package/dist/lib/domain/template-loader.d.ts +9 -0
- package/dist/lib/domain/template-loader.d.ts.map +1 -0
- package/dist/lib/domain/template-loader.js +29 -0
- package/dist/lib/domain/template-loader.js.map +1 -0
- package/dist/lib/domain/types.d.ts +29 -0
- package/dist/lib/domain/types.d.ts.map +1 -0
- package/dist/lib/domain/types.js +2 -0
- package/dist/lib/domain/types.js.map +1 -0
- package/dist/lib/dynamics-model.d.ts +107 -0
- package/dist/lib/dynamics-model.d.ts.map +1 -0
- package/dist/lib/dynamics-model.js +363 -0
- package/dist/lib/dynamics-model.js.map +1 -0
- package/dist/lib/eval-snapshot.d.ts +47 -0
- package/dist/lib/eval-snapshot.d.ts.map +1 -0
- package/dist/lib/eval-snapshot.js +326 -0
- package/dist/lib/eval-snapshot.js.map +1 -0
- package/dist/lib/eval-store.d.ts +20 -0
- package/dist/lib/eval-store.d.ts.map +1 -0
- package/dist/lib/eval-store.js +209 -0
- package/dist/lib/eval-store.js.map +1 -0
- package/dist/lib/findings-engine.d.ts +51 -0
- package/dist/lib/findings-engine.d.ts.map +1 -0
- package/dist/lib/findings-engine.js +338 -0
- package/dist/lib/findings-engine.js.map +1 -0
- package/dist/lib/flow-engine.d.ts +57 -0
- package/dist/lib/flow-engine.d.ts.map +1 -0
- package/dist/lib/flow-engine.js +717 -0
- package/dist/lib/flow-engine.js.map +1 -0
- package/dist/lib/gtm-generator.d.ts +29 -0
- package/dist/lib/gtm-generator.d.ts.map +1 -0
- package/dist/lib/gtm-generator.js +250 -0
- package/dist/lib/gtm-generator.js.map +1 -0
- package/dist/lib/hook-transformer.d.ts +11 -0
- package/dist/lib/hook-transformer.d.ts.map +1 -0
- package/dist/lib/hook-transformer.js +74 -0
- package/dist/lib/hook-transformer.js.map +1 -0
- package/dist/lib/hub-client.d.ts +81 -0
- package/dist/lib/hub-client.d.ts.map +1 -0
- package/dist/lib/hub-client.js +73 -0
- package/dist/lib/hub-client.js.map +1 -0
- package/dist/lib/hub-health.d.ts +40 -0
- package/dist/lib/hub-health.d.ts.map +1 -0
- package/dist/lib/hub-health.js +101 -0
- package/dist/lib/hub-health.js.map +1 -0
- package/dist/lib/ide-panes.d.ts +58 -0
- package/dist/lib/ide-panes.d.ts.map +1 -0
- package/dist/lib/ide-panes.js +508 -0
- package/dist/lib/ide-panes.js.map +1 -0
- package/dist/lib/invariant-monitor.d.ts +54 -0
- package/dist/lib/invariant-monitor.d.ts.map +1 -0
- package/dist/lib/invariant-monitor.js +487 -0
- package/dist/lib/invariant-monitor.js.map +1 -0
- package/dist/lib/journal-analyzer.d.ts +71 -0
- package/dist/lib/journal-analyzer.d.ts.map +1 -0
- package/dist/lib/journal-analyzer.js +306 -0
- package/dist/lib/journal-analyzer.js.map +1 -0
- package/dist/lib/kanban-github.d.ts +81 -0
- package/dist/lib/kanban-github.d.ts.map +1 -0
- package/dist/lib/kanban-github.js +318 -0
- package/dist/lib/kanban-github.js.map +1 -0
- package/dist/lib/kanban.d.ts +131 -0
- package/dist/lib/kanban.d.ts.map +1 -0
- package/dist/lib/kanban.js +340 -0
- package/dist/lib/kanban.js.map +1 -0
- package/dist/lib/kuva.d.ts +45 -0
- package/dist/lib/kuva.d.ts.map +1 -0
- package/dist/lib/kuva.js +131 -0
- package/dist/lib/kuva.js.map +1 -0
- package/dist/lib/linear-client.d.ts +73 -0
- package/dist/lib/linear-client.d.ts.map +1 -0
- package/dist/lib/linear-client.js +112 -0
- package/dist/lib/linear-client.js.map +1 -0
- package/dist/lib/linear-id-map.d.ts +20 -0
- package/dist/lib/linear-id-map.d.ts.map +1 -0
- package/dist/lib/linear-id-map.js +59 -0
- package/dist/lib/linear-id-map.js.map +1 -0
- package/dist/lib/linear-kanban.d.ts +66 -0
- package/dist/lib/linear-kanban.d.ts.map +1 -0
- package/dist/lib/linear-kanban.js +175 -0
- package/dist/lib/linear-kanban.js.map +1 -0
- package/dist/lib/linear-webhook.d.ts +50 -0
- package/dist/lib/linear-webhook.d.ts.map +1 -0
- package/dist/lib/linear-webhook.js +92 -0
- package/dist/lib/linear-webhook.js.map +1 -0
- package/dist/lib/map-event-bus.d.ts +50 -0
- package/dist/lib/map-event-bus.d.ts.map +1 -0
- package/dist/lib/map-event-bus.js +366 -0
- package/dist/lib/map-event-bus.js.map +1 -0
- package/dist/lib/memory-db.d.ts +136 -0
- package/dist/lib/memory-db.d.ts.map +1 -0
- package/dist/lib/memory-db.js +429 -0
- package/dist/lib/memory-db.js.map +1 -0
- package/dist/lib/memory-indexer.d.ts +61 -0
- package/dist/lib/memory-indexer.d.ts.map +1 -0
- package/dist/lib/memory-indexer.js +418 -0
- package/dist/lib/memory-indexer.js.map +1 -0
- package/dist/lib/memory-search.d.ts +185 -0
- package/dist/lib/memory-search.d.ts.map +1 -0
- package/dist/lib/memory-search.js +678 -0
- package/dist/lib/memory-search.js.map +1 -0
- package/dist/lib/meta-orchestrator.d.ts +141 -0
- package/dist/lib/meta-orchestrator.d.ts.map +1 -0
- package/dist/lib/meta-orchestrator.js +552 -0
- package/dist/lib/meta-orchestrator.js.map +1 -0
- package/dist/lib/model-pricing.d.ts +11 -0
- package/dist/lib/model-pricing.d.ts.map +1 -0
- package/dist/lib/model-pricing.js +27 -0
- package/dist/lib/model-pricing.js.map +1 -0
- package/dist/lib/onboarding.d.ts +40 -0
- package/dist/lib/onboarding.d.ts.map +1 -0
- package/dist/lib/onboarding.js +213 -0
- package/dist/lib/onboarding.js.map +1 -0
- package/dist/lib/openclaw-registry.d.ts +48 -0
- package/dist/lib/openclaw-registry.d.ts.map +1 -0
- package/dist/lib/openclaw-registry.js +181 -0
- package/dist/lib/openclaw-registry.js.map +1 -0
- package/dist/lib/openclaw-sdk.d.ts +115 -0
- package/dist/lib/openclaw-sdk.d.ts.map +1 -0
- package/dist/lib/openclaw-sdk.js +220 -0
- package/dist/lib/openclaw-sdk.js.map +1 -0
- package/dist/lib/peer-agent-generator.d.ts +44 -0
- package/dist/lib/peer-agent-generator.d.ts.map +1 -0
- package/dist/lib/peer-agent-generator.js +310 -0
- package/dist/lib/peer-agent-generator.js.map +1 -0
- package/dist/lib/peter-parker-bridge.d.ts +70 -0
- package/dist/lib/peter-parker-bridge.d.ts.map +1 -0
- package/dist/lib/peter-parker-bridge.js +345 -0
- package/dist/lib/peter-parker-bridge.js.map +1 -0
- package/dist/lib/peter-parker-config.d.ts +13 -0
- package/dist/lib/peter-parker-config.d.ts.map +1 -0
- package/dist/lib/peter-parker-config.js +86 -0
- package/dist/lib/peter-parker-config.js.map +1 -0
- package/dist/lib/physical-world-model.d.ts +50 -0
- package/dist/lib/physical-world-model.d.ts.map +1 -0
- package/dist/lib/physical-world-model.js +251 -0
- package/dist/lib/physical-world-model.js.map +1 -0
- package/dist/lib/pi-sky/bridge.d.ts +55 -0
- package/dist/lib/pi-sky/bridge.d.ts.map +1 -0
- package/dist/lib/pi-sky/bridge.js +264 -0
- package/dist/lib/pi-sky/bridge.js.map +1 -0
- package/dist/lib/pi-sky/cost-monitor.d.ts +21 -0
- package/dist/lib/pi-sky/cost-monitor.d.ts.map +1 -0
- package/dist/lib/pi-sky/cost-monitor.js +126 -0
- package/dist/lib/pi-sky/cost-monitor.js.map +1 -0
- package/dist/lib/pi-sky/eval-sweep.d.ts +27 -0
- package/dist/lib/pi-sky/eval-sweep.d.ts.map +1 -0
- package/dist/lib/pi-sky/eval-sweep.js +141 -0
- package/dist/lib/pi-sky/eval-sweep.js.map +1 -0
- package/dist/lib/pi-sky/event-router.d.ts +32 -0
- package/dist/lib/pi-sky/event-router.d.ts.map +1 -0
- package/dist/lib/pi-sky/event-router.js +176 -0
- package/dist/lib/pi-sky/event-router.js.map +1 -0
- package/dist/lib/pi-sky/experiment.d.ts +9 -0
- package/dist/lib/pi-sky/experiment.d.ts.map +1 -0
- package/dist/lib/pi-sky/experiment.js +83 -0
- package/dist/lib/pi-sky/experiment.js.map +1 -0
- package/dist/lib/pi-sky/index.d.ts +16 -0
- package/dist/lib/pi-sky/index.d.ts.map +1 -0
- package/dist/lib/pi-sky/index.js +16 -0
- package/dist/lib/pi-sky/index.js.map +1 -0
- package/dist/lib/pi-sky/stratus-gate.d.ts +28 -0
- package/dist/lib/pi-sky/stratus-gate.d.ts.map +1 -0
- package/dist/lib/pi-sky/stratus-gate.js +61 -0
- package/dist/lib/pi-sky/stratus-gate.js.map +1 -0
- package/dist/lib/pi-sky/swarm.d.ts +28 -0
- package/dist/lib/pi-sky/swarm.d.ts.map +1 -0
- package/dist/lib/pi-sky/swarm.js +208 -0
- package/dist/lib/pi-sky/swarm.js.map +1 -0
- package/dist/lib/pi-sky/types.d.ts +139 -0
- package/dist/lib/pi-sky/types.d.ts.map +1 -0
- package/dist/lib/pi-sky/types.js +2 -0
- package/dist/lib/pi-sky/types.js.map +1 -0
- package/dist/lib/pi-sky/voice-bridge.d.ts +20 -0
- package/dist/lib/pi-sky/voice-bridge.d.ts.map +1 -0
- package/dist/lib/pi-sky/voice-bridge.js +91 -0
- package/dist/lib/pi-sky/voice-bridge.js.map +1 -0
- package/dist/lib/planning-loop.d.ts +157 -0
- package/dist/lib/planning-loop.d.ts.map +1 -0
- package/dist/lib/planning-loop.js +537 -0
- package/dist/lib/planning-loop.js.map +1 -0
- package/dist/lib/policy-head.d.ts +53 -0
- package/dist/lib/policy-head.d.ts.map +1 -0
- package/dist/lib/policy-head.js +400 -0
- package/dist/lib/policy-head.js.map +1 -0
- package/dist/lib/predictor.d.ts +109 -0
- package/dist/lib/predictor.d.ts.map +1 -0
- package/dist/lib/predictor.js +433 -0
- package/dist/lib/predictor.js.map +1 -0
- package/dist/lib/replay-buffer.d.ts +93 -0
- package/dist/lib/replay-buffer.d.ts.map +1 -0
- package/dist/lib/replay-buffer.js +302 -0
- package/dist/lib/replay-buffer.js.map +1 -0
- package/dist/lib/resource-optimizer-middleware.d.ts +39 -0
- package/dist/lib/resource-optimizer-middleware.d.ts.map +1 -0
- package/dist/lib/resource-optimizer-middleware.js +228 -0
- package/dist/lib/resource-optimizer-middleware.js.map +1 -0
- package/dist/lib/resource-optimizer.d.ts +71 -0
- package/dist/lib/resource-optimizer.d.ts.map +1 -0
- package/dist/lib/resource-optimizer.js +228 -0
- package/dist/lib/resource-optimizer.js.map +1 -0
- package/dist/lib/rewards/index.d.ts +14 -0
- package/dist/lib/rewards/index.d.ts.map +1 -0
- package/dist/lib/rewards/index.js +15 -0
- package/dist/lib/rewards/index.js.map +1 -0
- package/dist/lib/rl-manager.d.ts +74 -0
- package/dist/lib/rl-manager.d.ts.map +1 -0
- package/dist/lib/rl-manager.js +245 -0
- package/dist/lib/rl-manager.js.map +1 -0
- package/dist/lib/sentinel-rl.d.ts +97 -0
- package/dist/lib/sentinel-rl.d.ts.map +1 -0
- package/dist/lib/sentinel-rl.js +430 -0
- package/dist/lib/sentinel-rl.js.map +1 -0
- package/dist/lib/service-analyzer.d.ts +76 -0
- package/dist/lib/service-analyzer.d.ts.map +1 -0
- package/dist/lib/service-analyzer.js +704 -0
- package/dist/lib/service-analyzer.js.map +1 -0
- package/dist/lib/service-dependencies.d.ts +44 -0
- package/dist/lib/service-dependencies.d.ts.map +1 -0
- package/dist/lib/service-dependencies.js +314 -0
- package/dist/lib/service-dependencies.js.map +1 -0
- package/dist/lib/service-detector.d.ts +61 -0
- package/dist/lib/service-detector.d.ts.map +1 -0
- package/dist/lib/service-detector.js +541 -0
- package/dist/lib/service-detector.js.map +1 -0
- package/dist/lib/service-gtm.d.ts +208 -0
- package/dist/lib/service-gtm.d.ts.map +1 -0
- package/dist/lib/service-gtm.js +1006 -0
- package/dist/lib/service-gtm.js.map +1 -0
- package/dist/lib/service-mcp-base.d.ts +103 -0
- package/dist/lib/service-mcp-base.d.ts.map +1 -0
- package/dist/lib/service-mcp-base.js +263 -0
- package/dist/lib/service-mcp-base.js.map +1 -0
- package/dist/lib/service-questionnaire.d.ts +11 -0
- package/dist/lib/service-questionnaire.d.ts.map +1 -0
- package/dist/lib/service-questionnaire.js +89 -0
- package/dist/lib/service-questionnaire.js.map +1 -0
- package/dist/lib/service-utils.d.ts +103 -0
- package/dist/lib/service-utils.d.ts.map +1 -0
- package/dist/lib/service-utils.js +385 -0
- package/dist/lib/service-utils.js.map +1 -0
- package/dist/lib/session-lock.d.ts +61 -0
- package/dist/lib/session-lock.d.ts.map +1 -0
- package/dist/lib/session-lock.js +438 -0
- package/dist/lib/session-lock.js.map +1 -0
- package/dist/lib/setup/agent-generator.d.ts +25 -0
- package/dist/lib/setup/agent-generator.d.ts.map +1 -0
- package/dist/lib/setup/agent-generator.js +444 -0
- package/dist/lib/setup/agent-generator.js.map +1 -0
- package/dist/lib/setup/context-analyzer.d.ts +16 -0
- package/dist/lib/setup/context-analyzer.d.ts.map +1 -0
- package/dist/lib/setup/context-analyzer.js +112 -0
- package/dist/lib/setup/context-analyzer.js.map +1 -0
- package/dist/lib/setup/doc-auditor.d.ts +54 -0
- package/dist/lib/setup/doc-auditor.d.ts.map +1 -0
- package/dist/lib/setup/doc-auditor.js +629 -0
- package/dist/lib/setup/doc-auditor.js.map +1 -0
- package/dist/lib/setup/domain-generator.d.ts +7 -0
- package/dist/lib/setup/domain-generator.d.ts.map +1 -0
- package/dist/lib/setup/domain-generator.js +58 -0
- package/dist/lib/setup/domain-generator.js.map +1 -0
- package/dist/lib/setup/flow-generator.d.ts +10 -0
- package/dist/lib/setup/flow-generator.d.ts.map +1 -0
- package/dist/lib/setup/flow-generator.js +113 -0
- package/dist/lib/setup/flow-generator.js.map +1 -0
- package/dist/lib/setup/invariant-bridge.d.ts +91 -0
- package/dist/lib/setup/invariant-bridge.d.ts.map +1 -0
- package/dist/lib/setup/invariant-bridge.js +384 -0
- package/dist/lib/setup/invariant-bridge.js.map +1 -0
- package/dist/lib/setup/smart-eval-generator.d.ts +38 -0
- package/dist/lib/setup/smart-eval-generator.d.ts.map +1 -0
- package/dist/lib/setup/smart-eval-generator.js +378 -0
- package/dist/lib/setup/smart-eval-generator.js.map +1 -0
- package/dist/lib/setup/smart-recommender.d.ts +63 -0
- package/dist/lib/setup/smart-recommender.d.ts.map +1 -0
- package/dist/lib/setup/smart-recommender.js +329 -0
- package/dist/lib/setup/smart-recommender.js.map +1 -0
- package/dist/lib/setup/spec-generator.d.ts +99 -0
- package/dist/lib/setup/spec-generator.d.ts.map +1 -0
- package/dist/lib/setup/spec-generator.js +784 -0
- package/dist/lib/setup/spec-generator.js.map +1 -0
- package/dist/lib/setup/starter-intelligence.d.ts +25 -0
- package/dist/lib/setup/starter-intelligence.d.ts.map +1 -0
- package/dist/lib/setup/starter-intelligence.js +309 -0
- package/dist/lib/setup/starter-intelligence.js.map +1 -0
- package/dist/lib/setup/violation-agent-generator.d.ts +32 -0
- package/dist/lib/setup/violation-agent-generator.d.ts.map +1 -0
- package/dist/lib/setup/violation-agent-generator.js +255 -0
- package/dist/lib/setup/violation-agent-generator.js.map +1 -0
- package/dist/lib/skill-generator.d.ts +21 -0
- package/dist/lib/skill-generator.d.ts.map +1 -0
- package/dist/lib/skill-generator.js +253 -0
- package/dist/lib/skill-generator.js.map +1 -0
- package/dist/lib/state-capture.d.ts +36 -0
- package/dist/lib/state-capture.d.ts.map +1 -0
- package/dist/lib/state-capture.js +541 -0
- package/dist/lib/state-capture.js.map +1 -0
- package/dist/lib/stealth-onboarding.d.ts +40 -0
- package/dist/lib/stealth-onboarding.d.ts.map +1 -0
- package/dist/lib/stealth-onboarding.js +213 -0
- package/dist/lib/stealth-onboarding.js.map +1 -0
- package/dist/lib/storage/cloud.d.ts +27 -0
- package/dist/lib/storage/cloud.d.ts.map +1 -0
- package/dist/lib/storage/cloud.js +75 -0
- package/dist/lib/storage/cloud.js.map +1 -0
- package/dist/lib/storage/index.d.ts +15 -0
- package/dist/lib/storage/index.d.ts.map +1 -0
- package/dist/lib/storage/index.js +15 -0
- package/dist/lib/storage/index.js.map +1 -0
- package/dist/lib/storage/interface.d.ts +56 -0
- package/dist/lib/storage/interface.d.ts.map +1 -0
- package/dist/lib/storage/interface.js +2 -0
- package/dist/lib/storage/interface.js.map +1 -0
- package/dist/lib/storage/local.d.ts +26 -0
- package/dist/lib/storage/local.d.ts.map +1 -0
- package/dist/lib/storage/local.js +164 -0
- package/dist/lib/storage/local.js.map +1 -0
- package/dist/lib/stratus-client.d.ts +178 -0
- package/dist/lib/stratus-client.d.ts.map +1 -0
- package/dist/lib/stratus-client.js +739 -0
- package/dist/lib/stratus-client.js.map +1 -0
- package/dist/lib/stratus-rollout-test.d.ts +10 -0
- package/dist/lib/stratus-rollout-test.d.ts.map +1 -0
- package/dist/lib/stratus-rollout-test.js +412 -0
- package/dist/lib/stratus-rollout-test.js.map +1 -0
- package/dist/lib/surface-agent.d.ts +78 -0
- package/dist/lib/surface-agent.d.ts.map +1 -0
- package/dist/lib/surface-agent.js +105 -0
- package/dist/lib/surface-agent.js.map +1 -0
- package/dist/lib/surface-coordination-example.d.ts +30 -0
- package/dist/lib/surface-coordination-example.d.ts.map +1 -0
- package/dist/lib/surface-coordination-example.js +164 -0
- package/dist/lib/surface-coordination-example.js.map +1 -0
- package/dist/lib/telemetry/physical-world-collector.d.ts +15 -0
- package/dist/lib/telemetry/physical-world-collector.d.ts.map +1 -0
- package/dist/lib/telemetry/physical-world-collector.js +177 -0
- package/dist/lib/telemetry/physical-world-collector.js.map +1 -0
- package/dist/lib/telemetry/training-bridge.d.ts +51 -0
- package/dist/lib/telemetry/training-bridge.d.ts.map +1 -0
- package/dist/lib/telemetry/training-bridge.js +185 -0
- package/dist/lib/telemetry/training-bridge.js.map +1 -0
- package/dist/lib/telemetry-agent-v2.d.ts +128 -0
- package/dist/lib/telemetry-agent-v2.d.ts.map +1 -0
- package/dist/lib/telemetry-agent-v2.js +1043 -0
- package/dist/lib/telemetry-agent-v2.js.map +1 -0
- package/dist/lib/telemetry-agent.d.ts +57 -0
- package/dist/lib/telemetry-agent.d.ts.map +1 -0
- package/dist/lib/telemetry-agent.js +289 -0
- package/dist/lib/telemetry-agent.js.map +1 -0
- package/dist/lib/telemetry-digest.d.ts +10 -0
- package/dist/lib/telemetry-digest.d.ts.map +1 -0
- package/dist/lib/telemetry-digest.js +381 -0
- package/dist/lib/telemetry-digest.js.map +1 -0
- package/dist/lib/telemetry.d.ts +37 -0
- package/dist/lib/telemetry.d.ts.map +1 -0
- package/dist/lib/telemetry.js +376 -0
- package/dist/lib/telemetry.js.map +1 -0
- package/dist/lib/tenet-board-agent.d.ts +52 -0
- package/dist/lib/tenet-board-agent.d.ts.map +1 -0
- package/dist/lib/tenet-board-agent.js +226 -0
- package/dist/lib/tenet-board-agent.js.map +1 -0
- package/dist/lib/tenet-ide-agent.d.ts +40 -0
- package/dist/lib/tenet-ide-agent.d.ts.map +1 -0
- package/dist/lib/tenet-ide-agent.js +199 -0
- package/dist/lib/tenet-ide-agent.js.map +1 -0
- package/dist/lib/text-preprocessing.d.ts +83 -0
- package/dist/lib/text-preprocessing.d.ts.map +1 -0
- package/dist/lib/text-preprocessing.js +261 -0
- package/dist/lib/text-preprocessing.js.map +1 -0
- package/dist/lib/tool-schemas.d.ts +35 -0
- package/dist/lib/tool-schemas.d.ts.map +1 -0
- package/dist/lib/tool-schemas.js +246 -0
- package/dist/lib/tool-schemas.js.map +1 -0
- package/dist/lib/training-buffer.d.ts +86 -0
- package/dist/lib/training-buffer.d.ts.map +1 -0
- package/dist/lib/training-buffer.js +139 -0
- package/dist/lib/training-buffer.js.map +1 -0
- package/dist/lib/training-tuples.d.ts +33 -0
- package/dist/lib/training-tuples.d.ts.map +1 -0
- package/dist/lib/training-tuples.js +273 -0
- package/dist/lib/training-tuples.js.map +1 -0
- package/dist/lib/trajectory-loader.d.ts +82 -0
- package/dist/lib/trajectory-loader.d.ts.map +1 -0
- package/dist/lib/trajectory-loader.js +406 -0
- package/dist/lib/trajectory-loader.js.map +1 -0
- package/dist/lib/tuple-miner.d.ts +30 -0
- package/dist/lib/tuple-miner.d.ts.map +1 -0
- package/dist/lib/tuple-miner.js +427 -0
- package/dist/lib/tuple-miner.js.map +1 -0
- package/dist/lib/vm-backend.d.ts +72 -0
- package/dist/lib/vm-backend.d.ts.map +1 -0
- package/dist/lib/vm-backend.js +175 -0
- package/dist/lib/vm-backend.js.map +1 -0
- package/dist/lib/workspace/backend.d.ts +53 -0
- package/dist/lib/workspace/backend.d.ts.map +1 -0
- package/dist/lib/workspace/backend.js +37 -0
- package/dist/lib/workspace/backend.js.map +1 -0
- package/dist/lib/workspace/cmux-adapter.d.ts +46 -0
- package/dist/lib/workspace/cmux-adapter.d.ts.map +1 -0
- package/dist/lib/workspace/cmux-adapter.js +261 -0
- package/dist/lib/workspace/cmux-adapter.js.map +1 -0
- package/dist/lib/workspace/data-pipeline.d.ts +35 -0
- package/dist/lib/workspace/data-pipeline.d.ts.map +1 -0
- package/dist/lib/workspace/data-pipeline.js +494 -0
- package/dist/lib/workspace/data-pipeline.js.map +1 -0
- package/dist/lib/workspace/engine.d.ts +65 -0
- package/dist/lib/workspace/engine.d.ts.map +1 -0
- package/dist/lib/workspace/engine.js +407 -0
- package/dist/lib/workspace/engine.js.map +1 -0
- package/dist/lib/workspace/notifications.d.ts +14 -0
- package/dist/lib/workspace/notifications.d.ts.map +1 -0
- package/dist/lib/workspace/notifications.js +41 -0
- package/dist/lib/workspace/notifications.js.map +1 -0
- package/dist/lib/workspace/sidebar-runner.d.ts +13 -0
- package/dist/lib/workspace/sidebar-runner.d.ts.map +1 -0
- package/dist/lib/workspace/sidebar-runner.js +419 -0
- package/dist/lib/workspace/sidebar-runner.js.map +1 -0
- package/dist/lib/workspace/surface-registry.d.ts +49 -0
- package/dist/lib/workspace/surface-registry.d.ts.map +1 -0
- package/dist/lib/workspace/surface-registry.js +225 -0
- package/dist/lib/workspace/surface-registry.js.map +1 -0
- package/dist/lib/workspace/surface-type.d.ts +153 -0
- package/dist/lib/workspace/surface-type.d.ts.map +1 -0
- package/dist/lib/workspace/surface-type.js +9 -0
- package/dist/lib/workspace/surface-type.js.map +1 -0
- package/dist/lib/workspace/surfaces/agent-overview.d.ts +16 -0
- package/dist/lib/workspace/surfaces/agent-overview.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/agent-overview.js +116 -0
- package/dist/lib/workspace/surfaces/agent-overview.js.map +1 -0
- package/dist/lib/workspace/surfaces/agent.d.ts +16 -0
- package/dist/lib/workspace/surfaces/agent.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/agent.js +112 -0
- package/dist/lib/workspace/surfaces/agent.js.map +1 -0
- package/dist/lib/workspace/surfaces/claude.d.ts +15 -0
- package/dist/lib/workspace/surfaces/claude.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/claude.js +23 -0
- package/dist/lib/workspace/surfaces/claude.js.map +1 -0
- package/dist/lib/workspace/surfaces/dashboard.d.ts +21 -0
- package/dist/lib/workspace/surfaces/dashboard.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/dashboard.js +32 -0
- package/dist/lib/workspace/surfaces/dashboard.js.map +1 -0
- package/dist/lib/workspace/surfaces/eval.d.ts +15 -0
- package/dist/lib/workspace/surfaces/eval.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/eval.js +42 -0
- package/dist/lib/workspace/surfaces/eval.js.map +1 -0
- package/dist/lib/workspace/surfaces/event-stream.d.ts +16 -0
- package/dist/lib/workspace/surfaces/event-stream.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/event-stream.js +40 -0
- package/dist/lib/workspace/surfaces/event-stream.js.map +1 -0
- package/dist/lib/workspace/surfaces/flow.d.ts +16 -0
- package/dist/lib/workspace/surfaces/flow.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/flow.js +49 -0
- package/dist/lib/workspace/surfaces/flow.js.map +1 -0
- package/dist/lib/workspace/surfaces/index.d.ts +19 -0
- package/dist/lib/workspace/surfaces/index.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/index.js +19 -0
- package/dist/lib/workspace/surfaces/index.js.map +1 -0
- package/dist/lib/workspace/surfaces/kanban.d.ts +15 -0
- package/dist/lib/workspace/surfaces/kanban.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/kanban.js +43 -0
- package/dist/lib/workspace/surfaces/kanban.js.map +1 -0
- package/dist/lib/workspace/surfaces/physical-world.d.ts +15 -0
- package/dist/lib/workspace/surfaces/physical-world.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/physical-world.js +37 -0
- package/dist/lib/workspace/surfaces/physical-world.js.map +1 -0
- package/dist/lib/workspace/surfaces/portfolio.d.ts +16 -0
- package/dist/lib/workspace/surfaces/portfolio.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/portfolio.js +102 -0
- package/dist/lib/workspace/surfaces/portfolio.js.map +1 -0
- package/dist/lib/workspace/surfaces/service.d.ts +16 -0
- package/dist/lib/workspace/surfaces/service.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/service.js +45 -0
- package/dist/lib/workspace/surfaces/service.js.map +1 -0
- package/dist/lib/workspace/surfaces/shell.d.ts +15 -0
- package/dist/lib/workspace/surfaces/shell.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/shell.js +19 -0
- package/dist/lib/workspace/surfaces/shell.js.map +1 -0
- package/dist/lib/workspace/surfaces/sidebar.d.ts +22 -0
- package/dist/lib/workspace/surfaces/sidebar.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/sidebar.js +94 -0
- package/dist/lib/workspace/surfaces/sidebar.js.map +1 -0
- package/dist/lib/workspace/surfaces/telemetry.d.ts +16 -0
- package/dist/lib/workspace/surfaces/telemetry.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/telemetry.js +48 -0
- package/dist/lib/workspace/surfaces/telemetry.js.map +1 -0
- package/dist/lib/workspace/surfaces/topology.d.ts +15 -0
- package/dist/lib/workspace/surfaces/topology.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/topology.js +19 -0
- package/dist/lib/workspace/surfaces/topology.js.map +1 -0
- package/dist/lib/workspace/surfaces/training.d.ts +16 -0
- package/dist/lib/workspace/surfaces/training.d.ts.map +1 -0
- package/dist/lib/workspace/surfaces/training.js +22 -0
- package/dist/lib/workspace/surfaces/training.js.map +1 -0
- package/dist/lib/workspace/tmux-adapter.d.ts +30 -0
- package/dist/lib/workspace/tmux-adapter.d.ts.map +1 -0
- package/dist/lib/workspace/tmux-adapter.js +137 -0
- package/dist/lib/workspace/tmux-adapter.js.map +1 -0
- package/dist/lib/workspace/tmux-sidebar.d.ts +14 -0
- package/dist/lib/workspace/tmux-sidebar.d.ts.map +1 -0
- package/dist/lib/workspace/tmux-sidebar.js +230 -0
- package/dist/lib/workspace/tmux-sidebar.js.map +1 -0
- package/dist/lib/world-model-store.d.ts +172 -0
- package/dist/lib/world-model-store.d.ts.map +1 -0
- package/dist/lib/world-model-store.js +487 -0
- package/dist/lib/world-model-store.js.map +1 -0
- package/dist/mcp/context-hub-mcp.d.ts +11 -0
- package/dist/mcp/context-hub-mcp.d.ts.map +1 -0
- package/dist/mcp/context-hub-mcp.js +797 -0
- package/dist/mcp/context-hub-mcp.js.map +1 -0
- package/dist/mcp/service-mcp-server.d.ts +12 -0
- package/dist/mcp/service-mcp-server.d.ts.map +1 -0
- package/dist/mcp/service-mcp-server.js +434 -0
- package/dist/mcp/service-mcp-server.js.map +1 -0
- package/dist/mcp/service-peer-mcp.d.ts +36 -0
- package/dist/mcp/service-peer-mcp.d.ts.map +1 -0
- package/dist/mcp/service-peer-mcp.js +220 -0
- package/dist/mcp/service-peer-mcp.js.map +1 -0
- package/dist/mcp/service-registry-mcp.d.ts +13 -0
- package/dist/mcp/service-registry-mcp.d.ts.map +1 -0
- package/dist/mcp/service-registry-mcp.js +330 -0
- package/dist/mcp/service-registry-mcp.js.map +1 -0
- package/dist/telegram/voice.d.ts +146 -0
- package/dist/telegram/voice.d.ts.map +1 -0
- package/dist/telegram/voice.js +351 -0
- package/dist/telegram/voice.js.map +1 -0
- package/dist/types/eval.d.ts +18 -0
- package/dist/types/eval.d.ts.map +1 -0
- package/dist/types/eval.js +5 -0
- package/dist/types/eval.js.map +1 -0
- package/dist/types/flows.d.ts +72 -0
- package/dist/types/flows.d.ts.map +1 -0
- package/dist/types/flows.js +10 -0
- package/dist/types/flows.js.map +1 -0
- package/dist/types/ide.d.ts +49 -0
- package/dist/types/ide.d.ts.map +1 -0
- package/dist/types/ide.js +5 -0
- package/dist/types/ide.js.map +1 -0
- package/dist/types/journal.d.ts +133 -0
- package/dist/types/journal.d.ts.map +1 -0
- package/dist/types/journal.js +59 -0
- package/dist/types/journal.js.map +1 -0
- package/dist/types/map.d.ts +42 -0
- package/dist/types/map.d.ts.map +1 -0
- package/dist/types/map.js +39 -0
- package/dist/types/map.js.map +1 -0
- package/dist/types/physical-world-model.d.ts +65 -0
- package/dist/types/physical-world-model.d.ts.map +1 -0
- package/dist/types/physical-world-model.js +43 -0
- package/dist/types/physical-world-model.js.map +1 -0
- package/dist/types/platform-digest.d.ts +228 -0
- package/dist/types/platform-digest.d.ts.map +1 -0
- package/dist/types/platform-digest.js +5 -0
- package/dist/types/platform-digest.js.map +1 -0
- package/dist/types/skills.d.ts +44 -0
- package/dist/types/skills.d.ts.map +1 -0
- package/dist/types/skills.js +5 -0
- package/dist/types/skills.js.map +1 -0
- package/dist/types/telemetry-digest.d.ts +75 -0
- package/dist/types/telemetry-digest.d.ts.map +1 -0
- package/dist/types/telemetry-digest.js +5 -0
- package/dist/types/telemetry-digest.js.map +1 -0
- package/dist/types/telemetry.d.ts +107 -0
- package/dist/types/telemetry.d.ts.map +1 -0
- package/dist/types/telemetry.js +5 -0
- package/dist/types/telemetry.js.map +1 -0
- package/dist/types/world-model.d.ts +478 -0
- package/dist/types/world-model.d.ts.map +1 -0
- package/dist/types/world-model.js +87 -0
- package/dist/types/world-model.js.map +1 -0
- package/dist/ui/banner.d.ts +18 -0
- package/dist/ui/banner.d.ts.map +1 -0
- package/dist/ui/banner.js +323 -0
- package/dist/ui/banner.js.map +1 -0
- package/dist/ui/context-hub-logs.d.ts +10 -0
- package/dist/ui/context-hub-logs.d.ts.map +1 -0
- package/dist/ui/context-hub-logs.js +175 -0
- package/dist/ui/context-hub-logs.js.map +1 -0
- package/dist/ui/event-dashboard.d.ts +12 -0
- package/dist/ui/event-dashboard.d.ts.map +1 -0
- package/dist/ui/event-dashboard.js +342 -0
- package/dist/ui/event-dashboard.js.map +1 -0
- package/dist/ui/index.d.ts +8 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +8 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/prompts.d.ts +52 -0
- package/dist/ui/prompts.d.ts.map +1 -0
- package/dist/ui/prompts.js +72 -0
- package/dist/ui/prompts.js.map +1 -0
- package/dist/ui/service-dashboard.d.ts +11 -0
- package/dist/ui/service-dashboard.d.ts.map +1 -0
- package/dist/ui/service-dashboard.js +357 -0
- package/dist/ui/service-dashboard.js.map +1 -0
- package/dist/ui/services-manager.d.ts +11 -0
- package/dist/ui/services-manager.d.ts.map +1 -0
- package/dist/ui/services-manager.js +507 -0
- package/dist/ui/services-manager.js.map +1 -0
- package/dist/ui/theme.d.ts +82 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +142 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/utils/auth-guard.d.ts +66 -0
- package/dist/utils/auth-guard.d.ts.map +1 -0
- package/dist/utils/auth-guard.js +347 -0
- package/dist/utils/auth-guard.js.map +1 -0
- package/dist/utils/auth-status.d.ts +21 -0
- package/dist/utils/auth-status.d.ts.map +1 -0
- package/dist/utils/auth-status.js +53 -0
- package/dist/utils/auth-status.js.map +1 -0
- package/dist/utils/claude-md-generator.d.ts +10 -0
- package/dist/utils/claude-md-generator.d.ts.map +1 -0
- package/dist/utils/claude-md-generator.js +215 -0
- package/dist/utils/claude-md-generator.js.map +1 -0
- package/dist/utils/context-hub-port.d.ts +33 -0
- package/dist/utils/context-hub-port.d.ts.map +1 -0
- package/dist/utils/context-hub-port.js +118 -0
- package/dist/utils/context-hub-port.js.map +1 -0
- package/dist/utils/ensure-context-hub.d.ts +20 -0
- package/dist/utils/ensure-context-hub.d.ts.map +1 -0
- package/dist/utils/ensure-context-hub.js +66 -0
- package/dist/utils/ensure-context-hub.js.map +1 -0
- package/dist/utils/ensure-project.d.ts +12 -0
- package/dist/utils/ensure-project.d.ts.map +1 -0
- package/dist/utils/ensure-project.js +81 -0
- package/dist/utils/ensure-project.js.map +1 -0
- package/dist/utils/git.d.ts +73 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +222 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/github-auth.d.ts +54 -0
- package/dist/utils/github-auth.d.ts.map +1 -0
- package/dist/utils/github-auth.js +376 -0
- package/dist/utils/github-auth.js.map +1 -0
- package/dist/utils/github-repo.d.ts +30 -0
- package/dist/utils/github-repo.d.ts.map +1 -0
- package/dist/utils/github-repo.js +219 -0
- package/dist/utils/github-repo.js.map +1 -0
- package/dist/utils/jfl-config.d.ts +30 -0
- package/dist/utils/jfl-config.d.ts.map +1 -0
- package/dist/utils/jfl-config.js +153 -0
- package/dist/utils/jfl-config.js.map +1 -0
- package/dist/utils/jfl-migration.d.ts +29 -0
- package/dist/utils/jfl-migration.d.ts.map +1 -0
- package/dist/utils/jfl-migration.js +142 -0
- package/dist/utils/jfl-migration.js.map +1 -0
- package/dist/utils/jfl-paths.d.ts +71 -0
- package/dist/utils/jfl-paths.d.ts.map +1 -0
- package/dist/utils/jfl-paths.js +157 -0
- package/dist/utils/jfl-paths.js.map +1 -0
- package/dist/utils/platform-auth.d.ts +81 -0
- package/dist/utils/platform-auth.d.ts.map +1 -0
- package/dist/utils/platform-auth.js +192 -0
- package/dist/utils/platform-auth.js.map +1 -0
- package/dist/utils/project-config.d.ts +43 -0
- package/dist/utils/project-config.d.ts.map +1 -0
- package/dist/utils/project-config.js +97 -0
- package/dist/utils/project-config.js.map +1 -0
- package/dist/utils/provenance.d.ts +65 -0
- package/dist/utils/provenance.d.ts.map +1 -0
- package/dist/utils/provenance.js +213 -0
- package/dist/utils/provenance.js.map +1 -0
- package/dist/utils/settings-validator.d.ts +74 -0
- package/dist/utils/settings-validator.d.ts.map +1 -0
- package/dist/utils/settings-validator.js +241 -0
- package/dist/utils/settings-validator.js.map +1 -0
- package/dist/utils/skill-registry.d.ts +49 -0
- package/dist/utils/skill-registry.d.ts.map +1 -0
- package/dist/utils/skill-registry.js +192 -0
- package/dist/utils/skill-registry.js.map +1 -0
- package/dist/utils/tenet-env.d.ts +34 -0
- package/dist/utils/tenet-env.d.ts.map +1 -0
- package/dist/utils/tenet-env.js +42 -0
- package/dist/utils/tenet-env.js.map +1 -0
- package/dist/utils/wallet.d.ts +62 -0
- package/dist/utils/wallet.d.ts.map +1 -0
- package/dist/utils/wallet.js +253 -0
- package/dist/utils/wallet.js.map +1 -0
- package/dist/utils/x402-client.d.ts +86 -0
- package/dist/utils/x402-client.d.ts.map +1 -0
- package/dist/utils/x402-client.js +266 -0
- package/dist/utils/x402-client.js.map +1 -0
- package/package.json +105 -0
- package/packages/pi/AGENTS.md +112 -0
- package/packages/pi/assets/boot.mp3 +0 -0
- package/packages/pi/dist/agent-grid.d.ts +24 -0
- package/packages/pi/dist/agent-grid.d.ts.map +1 -0
- package/packages/pi/dist/agent-grid.js +162 -0
- package/packages/pi/dist/agent-grid.js.map +1 -0
- package/packages/pi/dist/agent-names.d.ts +43 -0
- package/packages/pi/dist/agent-names.d.ts.map +1 -0
- package/packages/pi/dist/agent-names.js +156 -0
- package/packages/pi/dist/agent-names.js.map +1 -0
- package/packages/pi/dist/autoresearch.d.ts +15 -0
- package/packages/pi/dist/autoresearch.d.ts.map +1 -0
- package/packages/pi/dist/autoresearch.js +372 -0
- package/packages/pi/dist/autoresearch.js.map +1 -0
- package/packages/pi/dist/bookmarks.d.ts +15 -0
- package/packages/pi/dist/bookmarks.d.ts.map +1 -0
- package/packages/pi/dist/bookmarks.js +77 -0
- package/packages/pi/dist/bookmarks.js.map +1 -0
- package/packages/pi/dist/context.d.ts +17 -0
- package/packages/pi/dist/context.d.ts.map +1 -0
- package/packages/pi/dist/context.js +152 -0
- package/packages/pi/dist/context.js.map +1 -0
- package/packages/pi/dist/crm-tool.d.ts +12 -0
- package/packages/pi/dist/crm-tool.d.ts.map +1 -0
- package/packages/pi/dist/crm-tool.js +58 -0
- package/packages/pi/dist/crm-tool.js.map +1 -0
- package/packages/pi/dist/eval-tool.d.ts +11 -0
- package/packages/pi/dist/eval-tool.d.ts.map +1 -0
- package/packages/pi/dist/eval-tool.js +188 -0
- package/packages/pi/dist/eval-tool.js.map +1 -0
- package/packages/pi/dist/eval.d.ts +12 -0
- package/packages/pi/dist/eval.d.ts.map +1 -0
- package/packages/pi/dist/eval.js +43 -0
- package/packages/pi/dist/eval.js.map +1 -0
- package/packages/pi/dist/footer.d.ts +20 -0
- package/packages/pi/dist/footer.d.ts.map +1 -0
- package/packages/pi/dist/footer.js +222 -0
- package/packages/pi/dist/footer.js.map +1 -0
- package/packages/pi/dist/header.d.ts +17 -0
- package/packages/pi/dist/header.d.ts.map +1 -0
- package/packages/pi/dist/header.js +156 -0
- package/packages/pi/dist/header.js.map +1 -0
- package/packages/pi/dist/hub-resolver.d.ts +11 -0
- package/packages/pi/dist/hub-resolver.d.ts.map +1 -0
- package/packages/pi/dist/hub-resolver.js +58 -0
- package/packages/pi/dist/hub-resolver.js.map +1 -0
- package/packages/pi/dist/hub-tools.d.ts +14 -0
- package/packages/pi/dist/hub-tools.d.ts.map +1 -0
- package/packages/pi/dist/hub-tools.js +266 -0
- package/packages/pi/dist/hub-tools.js.map +1 -0
- package/packages/pi/dist/hud-tool.d.ts +17 -0
- package/packages/pi/dist/hud-tool.d.ts.map +1 -0
- package/packages/pi/dist/hud-tool.js +297 -0
- package/packages/pi/dist/hud-tool.js.map +1 -0
- package/packages/pi/dist/index.d.ts +12 -0
- package/packages/pi/dist/index.d.ts.map +1 -0
- package/packages/pi/dist/index.js +556 -0
- package/packages/pi/dist/index.js.map +1 -0
- package/packages/pi/dist/jfl-resolve.d.ts +29 -0
- package/packages/pi/dist/jfl-resolve.d.ts.map +1 -0
- package/packages/pi/dist/jfl-resolve.js +89 -0
- package/packages/pi/dist/jfl-resolve.js.map +1 -0
- package/packages/pi/dist/journal.d.ts +23 -0
- package/packages/pi/dist/journal.d.ts.map +1 -0
- package/packages/pi/dist/journal.js +250 -0
- package/packages/pi/dist/journal.js.map +1 -0
- package/packages/pi/dist/map-bridge.d.ts +20 -0
- package/packages/pi/dist/map-bridge.d.ts.map +1 -0
- package/packages/pi/dist/map-bridge.js +181 -0
- package/packages/pi/dist/map-bridge.js.map +1 -0
- package/packages/pi/dist/memory-tool.d.ts +11 -0
- package/packages/pi/dist/memory-tool.d.ts.map +1 -0
- package/packages/pi/dist/memory-tool.js +162 -0
- package/packages/pi/dist/memory-tool.js.map +1 -0
- package/packages/pi/dist/notifications.d.ts +15 -0
- package/packages/pi/dist/notifications.d.ts.map +1 -0
- package/packages/pi/dist/notifications.js +65 -0
- package/packages/pi/dist/notifications.js.map +1 -0
- package/packages/pi/dist/onboarding-v1.d.ts +15 -0
- package/packages/pi/dist/onboarding-v1.d.ts.map +1 -0
- package/packages/pi/dist/onboarding-v1.js +417 -0
- package/packages/pi/dist/onboarding-v1.js.map +1 -0
- package/packages/pi/dist/onboarding-v2.d.ts +18 -0
- package/packages/pi/dist/onboarding-v2.d.ts.map +1 -0
- package/packages/pi/dist/onboarding-v2.js +402 -0
- package/packages/pi/dist/onboarding-v2.js.map +1 -0
- package/packages/pi/dist/onboarding-v3.d.ts +13 -0
- package/packages/pi/dist/onboarding-v3.d.ts.map +1 -0
- package/packages/pi/dist/onboarding-v3.js +581 -0
- package/packages/pi/dist/onboarding-v3.js.map +1 -0
- package/packages/pi/dist/peter-parker.d.ts +12 -0
- package/packages/pi/dist/peter-parker.d.ts.map +1 -0
- package/packages/pi/dist/peter-parker.js +162 -0
- package/packages/pi/dist/peter-parker.js.map +1 -0
- package/packages/pi/dist/pivot-tool.d.ts +11 -0
- package/packages/pi/dist/pivot-tool.d.ts.map +1 -0
- package/packages/pi/dist/pivot-tool.js +56 -0
- package/packages/pi/dist/pivot-tool.js.map +1 -0
- package/packages/pi/dist/policy-head-tool.d.ts +15 -0
- package/packages/pi/dist/policy-head-tool.d.ts.map +1 -0
- package/packages/pi/dist/policy-head-tool.js +220 -0
- package/packages/pi/dist/policy-head-tool.js.map +1 -0
- package/packages/pi/dist/portfolio-bridge.d.ts +12 -0
- package/packages/pi/dist/portfolio-bridge.d.ts.map +1 -0
- package/packages/pi/dist/portfolio-bridge.js +81 -0
- package/packages/pi/dist/portfolio-bridge.js.map +1 -0
- package/packages/pi/dist/service-skills.d.ts +15 -0
- package/packages/pi/dist/service-skills.d.ts.map +1 -0
- package/packages/pi/dist/service-skills.js +198 -0
- package/packages/pi/dist/service-skills.js.map +1 -0
- package/packages/pi/dist/session.d.ts +28 -0
- package/packages/pi/dist/session.d.ts.map +1 -0
- package/packages/pi/dist/session.js +649 -0
- package/packages/pi/dist/session.js.map +1 -0
- package/packages/pi/dist/shortcuts.d.ts +11 -0
- package/packages/pi/dist/shortcuts.d.ts.map +1 -0
- package/packages/pi/dist/shortcuts.js +231 -0
- package/packages/pi/dist/shortcuts.js.map +1 -0
- package/packages/pi/dist/startup-briefing.d.ts +13 -0
- package/packages/pi/dist/startup-briefing.d.ts.map +1 -0
- package/packages/pi/dist/startup-briefing.js +432 -0
- package/packages/pi/dist/startup-briefing.js.map +1 -0
- package/packages/pi/dist/stratus-bridge.d.ts +14 -0
- package/packages/pi/dist/stratus-bridge.d.ts.map +1 -0
- package/packages/pi/dist/stratus-bridge.js +104 -0
- package/packages/pi/dist/stratus-bridge.js.map +1 -0
- package/packages/pi/dist/subway-mesh.d.ts +88 -0
- package/packages/pi/dist/subway-mesh.d.ts.map +1 -0
- package/packages/pi/dist/subway-mesh.js +813 -0
- package/packages/pi/dist/subway-mesh.js.map +1 -0
- package/packages/pi/dist/synopsis-tool.d.ts +12 -0
- package/packages/pi/dist/synopsis-tool.d.ts.map +1 -0
- package/packages/pi/dist/synopsis-tool.js +84 -0
- package/packages/pi/dist/synopsis-tool.js.map +1 -0
- package/packages/pi/dist/tool-renderers.d.ts +55 -0
- package/packages/pi/dist/tool-renderers.d.ts.map +1 -0
- package/packages/pi/dist/tool-renderers.js +349 -0
- package/packages/pi/dist/tool-renderers.js.map +1 -0
- package/packages/pi/dist/training-buffer-tool.d.ts +16 -0
- package/packages/pi/dist/training-buffer-tool.d.ts.map +1 -0
- package/packages/pi/dist/training-buffer-tool.js +319 -0
- package/packages/pi/dist/training-buffer-tool.js.map +1 -0
- package/packages/pi/dist/types.d.ts +195 -0
- package/packages/pi/dist/types.d.ts.map +1 -0
- package/packages/pi/dist/types.js +11 -0
- package/packages/pi/dist/types.js.map +1 -0
- package/packages/pi/extensions/agent-grid.ts +191 -0
- package/packages/pi/extensions/agent-names.ts +178 -0
- package/packages/pi/extensions/autoresearch.ts +428 -0
- package/packages/pi/extensions/bookmarks.ts +85 -0
- package/packages/pi/extensions/context.ts +158 -0
- package/packages/pi/extensions/crm-tool.ts +61 -0
- package/packages/pi/extensions/eval-tool.ts +224 -0
- package/packages/pi/extensions/eval.ts +61 -0
- package/packages/pi/extensions/footer.ts +239 -0
- package/packages/pi/extensions/header.ts +171 -0
- package/packages/pi/extensions/hub-resolver.ts +63 -0
- package/packages/pi/extensions/hub-tools.ts +267 -0
- package/packages/pi/extensions/hud-tool.ts +294 -0
- package/packages/pi/extensions/index.ts +601 -0
- package/packages/pi/extensions/jfl-resolve.ts +98 -0
- package/packages/pi/extensions/journal.ts +309 -0
- package/packages/pi/extensions/map-bridge.ts +209 -0
- package/packages/pi/extensions/memory-tool.ts +170 -0
- package/packages/pi/extensions/notifications.ts +73 -0
- package/packages/pi/extensions/onboarding-v1.ts +455 -0
- package/packages/pi/extensions/onboarding-v2.ts +374 -0
- package/packages/pi/extensions/onboarding-v3.ts +686 -0
- package/packages/pi/extensions/peter-parker.ts +203 -0
- package/packages/pi/extensions/pivot-tool.ts +59 -0
- package/packages/pi/extensions/policy-head-tool.ts +277 -0
- package/packages/pi/extensions/portfolio-bridge.ts +89 -0
- package/packages/pi/extensions/service-skills.ts +219 -0
- package/packages/pi/extensions/session.ts +684 -0
- package/packages/pi/extensions/shortcuts.ts +259 -0
- package/packages/pi/extensions/startup-briefing.ts +482 -0
- package/packages/pi/extensions/stratus-bridge.ts +116 -0
- package/packages/pi/extensions/subway-mesh.ts +893 -0
- package/packages/pi/extensions/synopsis-tool.ts +88 -0
- package/packages/pi/extensions/tool-renderers.ts +366 -0
- package/packages/pi/extensions/training-buffer-tool.ts +376 -0
- package/packages/pi/extensions/types.ts +169 -0
- package/packages/pi/package-lock.json +346 -0
- package/packages/pi/package.json +42 -0
- package/packages/pi/skills/agent-browser/SKILL.md +116 -0
- package/packages/pi/skills/brand-architect/SKILL.md +240 -0
- package/packages/pi/skills/brand-architect/config.yaml +137 -0
- package/packages/pi/skills/campaign-hud/config.yaml +112 -0
- package/packages/pi/skills/content-creator/SKILL.md +294 -0
- package/packages/pi/skills/context/SKILL.md +65 -0
- package/packages/pi/skills/debug/MULTI_AGENT.md +360 -0
- package/packages/pi/skills/debug/SKILL.md +554 -0
- package/packages/pi/skills/end/SKILL.md +1790 -0
- package/packages/pi/skills/eval/SKILL.md +75 -0
- package/packages/pi/skills/fly-deploy/SKILL.md +676 -0
- package/packages/pi/skills/founder-video/SKILL.md +467 -0
- package/packages/pi/skills/hud/SKILL.md +160 -0
- package/packages/pi/skills/orchestrate/SKILL.md +74 -0
- package/packages/pi/skills/pi-agents/SKILL.md +78 -0
- package/packages/pi/skills/pivot/SKILL.md +91 -0
- package/packages/pi/skills/react-best-practices/AGENTS.md +2249 -0
- package/packages/pi/skills/react-best-practices/README.md +123 -0
- package/packages/pi/skills/react-best-practices/SKILL.md +125 -0
- package/packages/pi/skills/react-best-practices/metadata.json +15 -0
- package/packages/pi/skills/react-best-practices/rules/_sections.md +46 -0
- package/packages/pi/skills/react-best-practices/rules/_template.md +28 -0
- package/packages/pi/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/packages/pi/skills/react-best-practices/rules/advanced-use-latest.md +49 -0
- package/packages/pi/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/packages/pi/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/packages/pi/skills/react-best-practices/rules/async-dependencies.md +36 -0
- package/packages/pi/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/packages/pi/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/packages/pi/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/packages/pi/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/packages/pi/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/packages/pi/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/packages/pi/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/packages/pi/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/packages/pi/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/packages/pi/skills/react-best-practices/rules/js-batch-dom-css.md +82 -0
- package/packages/pi/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/packages/pi/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/packages/pi/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/packages/pi/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/packages/pi/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/packages/pi/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/packages/pi/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/packages/pi/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/packages/pi/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/packages/pi/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/packages/pi/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/packages/pi/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/packages/pi/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/packages/pi/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/packages/pi/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/packages/pi/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/packages/pi/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/packages/pi/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/packages/pi/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/packages/pi/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/packages/pi/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/packages/pi/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/packages/pi/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/packages/pi/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/packages/pi/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/packages/pi/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/packages/pi/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/packages/pi/skills/react-best-practices/rules/server-cache-react.md +26 -0
- package/packages/pi/skills/react-best-practices/rules/server-parallel-fetching.md +79 -0
- package/packages/pi/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/packages/pi/skills/remotion-best-practices/SKILL.md +43 -0
- package/packages/pi/skills/remotion-best-practices/rules/3d.md +86 -0
- package/packages/pi/skills/remotion-best-practices/rules/animations.md +29 -0
- package/packages/pi/skills/remotion-best-practices/rules/assets/charts-bar-chart.tsx +173 -0
- package/packages/pi/skills/remotion-best-practices/rules/assets/text-animations-typewriter.tsx +100 -0
- package/packages/pi/skills/remotion-best-practices/rules/assets/text-animations-word-highlight.tsx +108 -0
- package/packages/pi/skills/remotion-best-practices/rules/assets.md +78 -0
- package/packages/pi/skills/remotion-best-practices/rules/audio.md +172 -0
- package/packages/pi/skills/remotion-best-practices/rules/calculate-metadata.md +104 -0
- package/packages/pi/skills/remotion-best-practices/rules/can-decode.md +75 -0
- package/packages/pi/skills/remotion-best-practices/rules/charts.md +58 -0
- package/packages/pi/skills/remotion-best-practices/rules/compositions.md +146 -0
- package/packages/pi/skills/remotion-best-practices/rules/display-captions.md +126 -0
- package/packages/pi/skills/remotion-best-practices/rules/extract-frames.md +229 -0
- package/packages/pi/skills/remotion-best-practices/rules/fonts.md +152 -0
- package/packages/pi/skills/remotion-best-practices/rules/get-audio-duration.md +58 -0
- package/packages/pi/skills/remotion-best-practices/rules/get-video-dimensions.md +68 -0
- package/packages/pi/skills/remotion-best-practices/rules/get-video-duration.md +58 -0
- package/packages/pi/skills/remotion-best-practices/rules/gifs.md +138 -0
- package/packages/pi/skills/remotion-best-practices/rules/images.md +130 -0
- package/packages/pi/skills/remotion-best-practices/rules/import-srt-captions.md +67 -0
- package/packages/pi/skills/remotion-best-practices/rules/lottie.md +68 -0
- package/packages/pi/skills/remotion-best-practices/rules/measuring-dom-nodes.md +35 -0
- package/packages/pi/skills/remotion-best-practices/rules/measuring-text.md +143 -0
- package/packages/pi/skills/remotion-best-practices/rules/sequencing.md +106 -0
- package/packages/pi/skills/remotion-best-practices/rules/tailwind.md +11 -0
- package/packages/pi/skills/remotion-best-practices/rules/text-animations.md +20 -0
- package/packages/pi/skills/remotion-best-practices/rules/timing.md +179 -0
- package/packages/pi/skills/remotion-best-practices/rules/transcribe-captions.md +19 -0
- package/packages/pi/skills/remotion-best-practices/rules/transitions.md +122 -0
- package/packages/pi/skills/remotion-best-practices/rules/trimming.md +53 -0
- package/packages/pi/skills/remotion-best-practices/rules/videos.md +171 -0
- package/packages/pi/skills/search/SKILL.md +220 -0
- package/packages/pi/skills/spec/SKILL.md +377 -0
- package/packages/pi/skills/startup/SKILL.md +315 -0
- package/packages/pi/skills/subway-browser/SKILL.md +292 -0
- package/packages/pi/skills/viz/SKILL.md +204 -0
- package/packages/pi/skills/web-architect/SKILL.md +309 -0
- package/packages/pi/skills/x-algorithm/SKILL.md +305 -0
- package/packages/pi/teams/dev-team.yaml +63 -0
- package/packages/pi/teams/gtm-team.yaml +79 -0
- package/packages/pi/themes/jfl.theme.json +76 -0
- package/packages/pi/tsconfig.json +21 -0
- package/scripts/__pycache__/train-policy-head.cpython-314.pyc +0 -0
- package/scripts/collect-tuples.sh +124 -0
- package/scripts/commit-gtm.sh +56 -0
- package/scripts/commit-product.sh +68 -0
- package/scripts/context-query.sh +45 -0
- package/scripts/destroy-fleet.sh +37 -0
- package/scripts/generate-changesets.sh +113 -0
- package/scripts/jfl-ide.sh +48 -0
- package/scripts/migrate-to-branch-sessions.sh +201 -0
- package/scripts/postinstall.js +146 -0
- package/scripts/pp-branch-pr.sh +133 -0
- package/scripts/pp-branch-pr.sh.bak +115 -0
- package/scripts/session/auto-commit.sh +297 -0
- package/scripts/session/fix-tracked-logs.sh +97 -0
- package/scripts/session/jfl-doctor.sh +707 -0
- package/scripts/session/session-cleanup.sh +292 -0
- package/scripts/session/session-end.sh +198 -0
- package/scripts/session/session-init.sh +356 -0
- package/scripts/session/session-init.sh.backup +292 -0
- package/scripts/session/session-sync.sh +192 -0
- package/scripts/session/test-context-preservation.sh +160 -0
- package/scripts/session/test-critical-infrastructure.sh +293 -0
- package/scripts/session/test-experience-level.sh +336 -0
- package/scripts/session/test-session-cleanup.sh +268 -0
- package/scripts/session/test-session-sync.sh +320 -0
- package/scripts/setup-branch-protection.sh +106 -0
- package/scripts/spawn-fleet.sh +144 -0
- package/scripts/telemetry-dashboard.sh +44 -0
- package/scripts/test-map-eventbus.sh +357 -0
- package/scripts/test-onboarding.sh +121 -0
- package/scripts/test-planning-loop-e2e.ts +181 -0
- package/scripts/test-server-inference.ts +49 -0
- package/scripts/test-state-sensitivity.ts +32 -0
- package/scripts/train/requirements.txt +5 -0
- package/scripts/train/train-policy-head.py +477 -0
- package/scripts/train/v2/__pycache__/dataset.cpython-314.pyc +0 -0
- package/scripts/train/v2/__pycache__/eval.cpython-314.pyc +0 -0
- package/scripts/train/v2/__pycache__/generate_data.cpython-314.pyc +0 -0
- package/scripts/train/v2/__pycache__/infer.cpython-314.pyc +0 -0
- package/scripts/train/v2/__pycache__/model.cpython-314.pyc +0 -0
- package/scripts/train/v2/__pycache__/precompute.cpython-314.pyc +0 -0
- package/scripts/train/v2/__pycache__/train.cpython-314.pyc +0 -0
- package/scripts/train/v2/__pycache__/transform_buffer.cpython-314.pyc +0 -0
- package/scripts/train/v2/__pycache__/validate_data.cpython-314.pyc +0 -0
- package/scripts/train/v2/benchmark.py +661 -0
- package/scripts/train/v2/dataset.py +81 -0
- package/scripts/train/v2/domain.json +66 -0
- package/scripts/train/v2/eval.py +196 -0
- package/scripts/train/v2/generate_balanced.py +439 -0
- package/scripts/train/v2/generate_data.py +219 -0
- package/scripts/train/v2/generate_hard_negatives.py +219 -0
- package/scripts/train/v2/infer.py +301 -0
- package/scripts/train/v2/infer_server.py +224 -0
- package/scripts/train/v2/model.py +112 -0
- package/scripts/train/v2/online_train.py +576 -0
- package/scripts/train/v2/precompute.py +150 -0
- package/scripts/train/v2/train.py +302 -0
- package/scripts/train/v2/transform_buffer.py +227 -0
- package/scripts/train/v2/validate_data.py +115 -0
- package/scripts/train-policy-head.py +434 -0
- package/scripts/vm-swarm/README.md +301 -0
- package/scripts/vm-swarm/collect-tuples.sh +331 -0
- package/scripts/vm-swarm/create-base-template.sh +339 -0
- package/scripts/vm-swarm/kill-fleet.sh +204 -0
- package/scripts/vm-swarm/monitor-fleet.sh +346 -0
- package/scripts/vm-swarm/spawn-fleet.sh +304 -0
- package/scripts/voice-start.sh +156 -0
- package/scripts/voice-stop.sh +33 -0
- package/scripts/where-am-i.sh +78 -0
- package/templates/QUICKSTART_SKILL_TO_PRODUCT.md +242 -0
- package/templates/brand/BRAND_BRIEF.md +124 -0
- package/templates/brand/BRAND_DECISIONS.md +168 -0
- package/templates/brand/BRAND_GUIDELINES.md +251 -0
- package/templates/brand/VOICE_AND_TONE.md +146 -0
- package/templates/brand/global.css +240 -0
- package/templates/collaboration/CONTRIBUTOR.md +74 -0
- package/templates/collaboration/CRM.md +97 -0
- package/templates/collaboration/TASKS.md +83 -0
- package/templates/dating/FUNNEL.md +29 -0
- package/templates/dating/REWARDS.md +24 -0
- package/templates/dating/SIGNALS.md +18 -0
- package/templates/dating/anti_patterns.md +23 -0
- package/templates/dating/connectors/index.md +16 -0
- package/templates/dating/modes.md +21 -0
- package/templates/dating/psychology.md +18 -0
- package/templates/default/README.md +19 -0
- package/templates/service-agent/.claude/settings.json +32 -0
- package/templates/service-agent/CLAUDE.md +334 -0
- package/templates/service-agent/knowledge/ARCHITECTURE.md +115 -0
- package/templates/service-agent/knowledge/DEPLOYMENT.md +199 -0
- package/templates/service-agent/knowledge/RUNBOOK.md +412 -0
- package/templates/service-agent/knowledge/SERVICE_SPEC.md +77 -0
- package/templates/service-mcp-template.js +325 -0
- package/templates/strategic/NARRATIVE.md +114 -0
- package/templates/strategic/ROADMAP.md +128 -0
- package/templates/strategic/THESIS.md +108 -0
- package/templates/strategic/VISION.md +74 -0
|
@@ -0,0 +1,3505 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jfl context-hub - Unified context layer for AI agents
|
|
3
|
+
*
|
|
4
|
+
* Provides context from journal, knowledge docs, and code to any AI.
|
|
5
|
+
* Works locally (CLI) and hosted (platform).
|
|
6
|
+
*
|
|
7
|
+
* @purpose CLI command for Context Hub daemon management
|
|
8
|
+
*/
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import ora from "ora";
|
|
11
|
+
import { execSync, spawn } from "child_process";
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import * as http from "http";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
import { initializeDatabase, getMemoryStats, insertMemory, getLinksFrom, getLinksTo, getMemoriesByIds, addLink, } from "../lib/memory-db.js";
|
|
18
|
+
import { searchMemories } from "../lib/memory-search.js";
|
|
19
|
+
import { indexJournalEntries, startPeriodicIndexing, backfillEmbeddings, indexCodeHeaders } from "../lib/memory-indexer.js";
|
|
20
|
+
import { getProjectPort } from "../utils/context-hub-port.js";
|
|
21
|
+
import { getConfigValue, setConfig } from "../utils/jfl-config.js";
|
|
22
|
+
import Conf from "conf";
|
|
23
|
+
import { MAPEventBus } from "../lib/map-event-bus.js";
|
|
24
|
+
import { FlowEngine } from "../lib/flow-engine.js";
|
|
25
|
+
import { WebSocketServer } from "ws";
|
|
26
|
+
import { telemetry } from "../lib/telemetry.js";
|
|
27
|
+
import { transformHookPayload } from "../lib/hook-transformer.js";
|
|
28
|
+
import { loadAllAgentConfigs } from "../lib/agent-config.js";
|
|
29
|
+
import { TrainingBuffer } from "../lib/training-buffer.js";
|
|
30
|
+
import { FindingsEngine } from "../lib/findings-engine.js";
|
|
31
|
+
const PID_FILE = ".jfl/context-hub.pid";
|
|
32
|
+
const LOG_FILE = ".jfl/logs/context-hub.log";
|
|
33
|
+
const TOKEN_FILE = ".jfl/context-hub.token";
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Security
|
|
36
|
+
// ============================================================================
|
|
37
|
+
function generateToken() {
|
|
38
|
+
return Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join('');
|
|
39
|
+
}
|
|
40
|
+
function getTokenFile(projectRoot) {
|
|
41
|
+
return path.join(projectRoot, TOKEN_FILE);
|
|
42
|
+
}
|
|
43
|
+
function getOrCreateToken(projectRoot) {
|
|
44
|
+
const tokenFile = getTokenFile(projectRoot);
|
|
45
|
+
if (fs.existsSync(tokenFile)) {
|
|
46
|
+
return fs.readFileSync(tokenFile, 'utf-8').trim();
|
|
47
|
+
}
|
|
48
|
+
const token = generateToken();
|
|
49
|
+
fs.writeFileSync(tokenFile, token, { mode: 0o600 }); // Owner read/write only
|
|
50
|
+
return token;
|
|
51
|
+
}
|
|
52
|
+
function validateAuth(req, projectRoot, url) {
|
|
53
|
+
const tokenFile = getTokenFile(projectRoot);
|
|
54
|
+
// If no token file exists, allow access (backwards compatibility during migration)
|
|
55
|
+
if (!fs.existsSync(tokenFile)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const expectedToken = fs.readFileSync(tokenFile, 'utf-8').trim();
|
|
59
|
+
// Check Authorization header first
|
|
60
|
+
const authHeader = req.headers['authorization'];
|
|
61
|
+
if (authHeader) {
|
|
62
|
+
const providedToken = authHeader.startsWith('Bearer ')
|
|
63
|
+
? authHeader.slice(7)
|
|
64
|
+
: authHeader;
|
|
65
|
+
if (providedToken === expectedToken)
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
// Fall back to ?token= query param (needed for SSE/EventSource which can't set headers)
|
|
69
|
+
if (url) {
|
|
70
|
+
const queryToken = url.searchParams.get('token');
|
|
71
|
+
if (queryToken && queryToken === expectedToken)
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function isPortInUse(port) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const server = http.createServer();
|
|
79
|
+
server.once('error', (err) => {
|
|
80
|
+
if (err.code === 'EADDRINUSE') {
|
|
81
|
+
resolve(true);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
resolve(false);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
server.once('listening', () => {
|
|
88
|
+
server.close();
|
|
89
|
+
resolve(false);
|
|
90
|
+
});
|
|
91
|
+
server.listen(port);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// Journal Reader
|
|
96
|
+
// ============================================================================
|
|
97
|
+
function readJournalEntries(projectRoot, limit = 50) {
|
|
98
|
+
const journalDir = path.join(projectRoot, ".jfl", "journal");
|
|
99
|
+
const items = [];
|
|
100
|
+
if (!fs.existsSync(journalDir)) {
|
|
101
|
+
return items;
|
|
102
|
+
}
|
|
103
|
+
// Sort by modification time (newest first) so recent entries aren't buried
|
|
104
|
+
const files = fs.readdirSync(journalDir)
|
|
105
|
+
.filter(f => f.endsWith(".jsonl"))
|
|
106
|
+
.map(f => ({
|
|
107
|
+
name: f,
|
|
108
|
+
mtime: fs.statSync(path.join(journalDir, f)).mtimeMs
|
|
109
|
+
}))
|
|
110
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
111
|
+
.map(f => f.name);
|
|
112
|
+
// Read all entries from all files, then sort globally by timestamp
|
|
113
|
+
const allEntries = [];
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
const filePath = path.join(journalDir, file);
|
|
116
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
117
|
+
const lines = content.trim().split("\n").filter(l => l.trim());
|
|
118
|
+
for (const line of lines) {
|
|
119
|
+
try {
|
|
120
|
+
const entry = JSON.parse(line);
|
|
121
|
+
allEntries.push({
|
|
122
|
+
source: "journal",
|
|
123
|
+
type: entry.type || "entry",
|
|
124
|
+
title: entry.title || "Untitled",
|
|
125
|
+
content: entry.summary || entry.detail || "",
|
|
126
|
+
timestamp: entry.ts,
|
|
127
|
+
path: filePath
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Skip malformed lines
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Sort all entries by timestamp descending, then take the limit
|
|
136
|
+
allEntries.sort((a, b) => {
|
|
137
|
+
const ta = a.timestamp || "";
|
|
138
|
+
const tb = b.timestamp || "";
|
|
139
|
+
return tb.localeCompare(ta);
|
|
140
|
+
});
|
|
141
|
+
return allEntries.slice(0, limit);
|
|
142
|
+
}
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// Knowledge Reader
|
|
145
|
+
// ============================================================================
|
|
146
|
+
function readKnowledgeDocs(projectRoot) {
|
|
147
|
+
const knowledgeDir = path.join(projectRoot, "knowledge");
|
|
148
|
+
const items = [];
|
|
149
|
+
if (!fs.existsSync(knowledgeDir)) {
|
|
150
|
+
return items;
|
|
151
|
+
}
|
|
152
|
+
const priorityFiles = [
|
|
153
|
+
"VISION.md",
|
|
154
|
+
"ROADMAP.md",
|
|
155
|
+
"NARRATIVE.md",
|
|
156
|
+
"THESIS.md",
|
|
157
|
+
"BRAND_DECISIONS.md",
|
|
158
|
+
"TASKS.md",
|
|
159
|
+
"ARCHITECTURE.md",
|
|
160
|
+
"SERVICE_SPEC.md",
|
|
161
|
+
"DEPLOYMENT.md",
|
|
162
|
+
"RUNBOOK.md",
|
|
163
|
+
];
|
|
164
|
+
for (const filename of priorityFiles) {
|
|
165
|
+
const filePath = path.join(knowledgeDir, filename);
|
|
166
|
+
if (fs.existsSync(filePath)) {
|
|
167
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
168
|
+
const title = filename.replace(".md", "").replace(/_/g, " ");
|
|
169
|
+
items.push({
|
|
170
|
+
source: "knowledge",
|
|
171
|
+
type: "doc",
|
|
172
|
+
title,
|
|
173
|
+
content: content.slice(0, 2000),
|
|
174
|
+
path: filePath
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return items;
|
|
179
|
+
}
|
|
180
|
+
function discoverServices(projectRoot) {
|
|
181
|
+
const services = {};
|
|
182
|
+
// Look for .jfl/services.json in current project and parent directories
|
|
183
|
+
// Also search sibling GTM directories
|
|
184
|
+
const searchPaths = [
|
|
185
|
+
projectRoot,
|
|
186
|
+
path.join(projectRoot, ".."),
|
|
187
|
+
path.join(projectRoot, "../.."),
|
|
188
|
+
];
|
|
189
|
+
// Add sibling directories (e.g., if in jfl-cli, check ../JFL-GTM)
|
|
190
|
+
const parentDir = path.join(projectRoot, "..");
|
|
191
|
+
if (fs.existsSync(parentDir)) {
|
|
192
|
+
try {
|
|
193
|
+
const siblings = fs.readdirSync(parentDir, { withFileTypes: true });
|
|
194
|
+
for (const sibling of siblings) {
|
|
195
|
+
if (sibling.isDirectory() && (sibling.name.includes("GTM") || sibling.name.includes("gtm"))) {
|
|
196
|
+
searchPaths.push(path.join(parentDir, sibling.name));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Ignore read errors
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
for (const searchPath of searchPaths) {
|
|
205
|
+
if (!fs.existsSync(searchPath))
|
|
206
|
+
continue;
|
|
207
|
+
const servicesFile = path.join(searchPath, ".jfl/services.json");
|
|
208
|
+
if (fs.existsSync(servicesFile)) {
|
|
209
|
+
try {
|
|
210
|
+
const servicesData = JSON.parse(fs.readFileSync(servicesFile, "utf-8"));
|
|
211
|
+
for (const [serviceName, serviceData] of Object.entries(servicesData)) {
|
|
212
|
+
const data = serviceData;
|
|
213
|
+
// Check if service has service.json with MCP info
|
|
214
|
+
let mcpInfo;
|
|
215
|
+
const serviceJsonPath = path.join(data.path, "service.json");
|
|
216
|
+
if (fs.existsSync(serviceJsonPath)) {
|
|
217
|
+
try {
|
|
218
|
+
const serviceJson = JSON.parse(fs.readFileSync(serviceJsonPath, "utf-8"));
|
|
219
|
+
if (serviceJson.mcp) {
|
|
220
|
+
mcpInfo = {
|
|
221
|
+
enabled: serviceJson.mcp.enabled || false,
|
|
222
|
+
transport: serviceJson.mcp.transport || "stdio"
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Ignore parse errors
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Determine service status (basic check - could be enhanced)
|
|
231
|
+
let status = "unknown";
|
|
232
|
+
if (data.port) {
|
|
233
|
+
// Could check if port is in use
|
|
234
|
+
status = "stopped";
|
|
235
|
+
}
|
|
236
|
+
services[serviceName] = {
|
|
237
|
+
name: serviceName,
|
|
238
|
+
type: data.type || "unknown",
|
|
239
|
+
description: data.description || "",
|
|
240
|
+
path: data.path,
|
|
241
|
+
status,
|
|
242
|
+
mcp: mcpInfo,
|
|
243
|
+
commands: data.commands,
|
|
244
|
+
healthcheck: data.healthcheck,
|
|
245
|
+
port: data.port,
|
|
246
|
+
dependencies: data.dependencies
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
console.error(`Failed to read services from ${servicesFile}:`, err);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return services;
|
|
256
|
+
}
|
|
257
|
+
// ============================================================================
|
|
258
|
+
// Code Context Reader
|
|
259
|
+
// ============================================================================
|
|
260
|
+
function extractPurpose(content) {
|
|
261
|
+
const match = content.match(/@purpose\s+(.+?)(?:\n|\*)/i);
|
|
262
|
+
return match ? match[1].trim() : null;
|
|
263
|
+
}
|
|
264
|
+
function readCodeContext(projectRoot, limit = 30) {
|
|
265
|
+
const items = [];
|
|
266
|
+
// Look for files with @purpose in common locations
|
|
267
|
+
const searchDirs = [
|
|
268
|
+
path.join(projectRoot, "src"),
|
|
269
|
+
path.join(projectRoot, "app"),
|
|
270
|
+
path.join(projectRoot, "lib"),
|
|
271
|
+
path.join(projectRoot, "components"),
|
|
272
|
+
path.join(projectRoot, "product", "src"),
|
|
273
|
+
path.join(projectRoot, "product", "packages")
|
|
274
|
+
];
|
|
275
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
276
|
+
function scanDir(dir, depth = 0) {
|
|
277
|
+
if (depth > 4 || items.length >= limit)
|
|
278
|
+
return;
|
|
279
|
+
if (!fs.existsSync(dir))
|
|
280
|
+
return;
|
|
281
|
+
try {
|
|
282
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
283
|
+
for (const entry of entries) {
|
|
284
|
+
if (items.length >= limit)
|
|
285
|
+
break;
|
|
286
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
287
|
+
continue;
|
|
288
|
+
const fullPath = path.join(dir, entry.name);
|
|
289
|
+
if (entry.isDirectory()) {
|
|
290
|
+
scanDir(fullPath, depth + 1);
|
|
291
|
+
}
|
|
292
|
+
else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
293
|
+
try {
|
|
294
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
295
|
+
const purpose = extractPurpose(content);
|
|
296
|
+
if (purpose) {
|
|
297
|
+
items.push({
|
|
298
|
+
source: "code",
|
|
299
|
+
type: "file",
|
|
300
|
+
title: entry.name,
|
|
301
|
+
content: purpose,
|
|
302
|
+
path: fullPath
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// Skip unreadable files
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// Skip unreadable directories
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
for (const dir of searchDirs) {
|
|
317
|
+
scanDir(dir);
|
|
318
|
+
}
|
|
319
|
+
return items;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Read GTM content artifacts — drafts, specs, content, scripts.
|
|
323
|
+
* Used instead of code indexing for GTM-type projects.
|
|
324
|
+
*/
|
|
325
|
+
function readGtmContent(projectRoot, limit = 40) {
|
|
326
|
+
const items = [];
|
|
327
|
+
const gtmDirs = [
|
|
328
|
+
{ dir: "content", type: "content" },
|
|
329
|
+
{ dir: "drafts", type: "draft" },
|
|
330
|
+
{ dir: "specs", type: "spec" },
|
|
331
|
+
{ dir: "scripts", type: "script" },
|
|
332
|
+
];
|
|
333
|
+
const mdExtensions = [".md", ".txt"];
|
|
334
|
+
const scriptExtensions = [".sh", ".py", ".mjs", ".js"];
|
|
335
|
+
for (const { dir, type } of gtmDirs) {
|
|
336
|
+
const fullDir = path.join(projectRoot, dir);
|
|
337
|
+
if (!fs.existsSync(fullDir))
|
|
338
|
+
continue;
|
|
339
|
+
function scanDir(d, depth = 0) {
|
|
340
|
+
if (depth > 3 || items.length >= limit)
|
|
341
|
+
return;
|
|
342
|
+
try {
|
|
343
|
+
const entries = fs.readdirSync(d, { withFileTypes: true });
|
|
344
|
+
for (const entry of entries) {
|
|
345
|
+
if (items.length >= limit)
|
|
346
|
+
break;
|
|
347
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
348
|
+
continue;
|
|
349
|
+
const fullPath = path.join(d, entry.name);
|
|
350
|
+
if (entry.isDirectory()) {
|
|
351
|
+
scanDir(fullPath, depth + 1);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
const isMarkdown = mdExtensions.some(ext => entry.name.endsWith(ext));
|
|
355
|
+
const isScript = type === "script" && scriptExtensions.some(ext => entry.name.endsWith(ext));
|
|
356
|
+
if (isMarkdown || isScript) {
|
|
357
|
+
try {
|
|
358
|
+
const raw = fs.readFileSync(fullPath, "utf-8");
|
|
359
|
+
// Extract first heading + first paragraph as summary
|
|
360
|
+
const lines = raw.split("\n").filter(l => l.trim());
|
|
361
|
+
const heading = lines.find(l => l.startsWith("#"))?.replace(/^#+\s*/, "") || entry.name;
|
|
362
|
+
const bodyLines = lines.filter(l => !l.startsWith("#") && !l.startsWith("---"));
|
|
363
|
+
const summary = bodyLines.slice(0, 5).join(" ").slice(0, 500);
|
|
364
|
+
if (summary.length > 20) {
|
|
365
|
+
items.push({
|
|
366
|
+
source: "content",
|
|
367
|
+
type,
|
|
368
|
+
title: heading,
|
|
369
|
+
content: summary,
|
|
370
|
+
path: fullPath
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
// Skip unreadable files
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
// Skip unreadable directories
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
scanDir(fullDir);
|
|
386
|
+
}
|
|
387
|
+
return items;
|
|
388
|
+
}
|
|
389
|
+
// ============================================================================
|
|
390
|
+
// Search & Scoring (TF-IDF style)
|
|
391
|
+
// ============================================================================
|
|
392
|
+
function tokenize(text) {
|
|
393
|
+
return text
|
|
394
|
+
.toLowerCase()
|
|
395
|
+
.replace(/[^\w\s]/g, " ")
|
|
396
|
+
.split(/\s+/)
|
|
397
|
+
.filter(t => t.length > 2);
|
|
398
|
+
}
|
|
399
|
+
function computeTF(tokens) {
|
|
400
|
+
const tf = new Map();
|
|
401
|
+
for (const token of tokens) {
|
|
402
|
+
tf.set(token, (tf.get(token) || 0) + 1);
|
|
403
|
+
}
|
|
404
|
+
// Normalize by document length
|
|
405
|
+
for (const [term, count] of tf) {
|
|
406
|
+
tf.set(term, count / tokens.length);
|
|
407
|
+
}
|
|
408
|
+
return tf;
|
|
409
|
+
}
|
|
410
|
+
function computeIDF(documents) {
|
|
411
|
+
const idf = new Map();
|
|
412
|
+
const N = documents.length;
|
|
413
|
+
// Count documents containing each term
|
|
414
|
+
const docCount = new Map();
|
|
415
|
+
for (const doc of documents) {
|
|
416
|
+
const uniqueTerms = new Set(doc);
|
|
417
|
+
for (const term of uniqueTerms) {
|
|
418
|
+
docCount.set(term, (docCount.get(term) || 0) + 1);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// Compute IDF
|
|
422
|
+
for (const [term, count] of docCount) {
|
|
423
|
+
idf.set(term, Math.log((N + 1) / (count + 1)) + 1);
|
|
424
|
+
}
|
|
425
|
+
return idf;
|
|
426
|
+
}
|
|
427
|
+
function scoreItem(item, queryTokens, idf) {
|
|
428
|
+
const text = `${item.title} ${item.content}`;
|
|
429
|
+
const tokens = tokenize(text);
|
|
430
|
+
const tf = computeTF(tokens);
|
|
431
|
+
let score = 0;
|
|
432
|
+
for (const queryTerm of queryTokens) {
|
|
433
|
+
const termTF = tf.get(queryTerm) || 0;
|
|
434
|
+
const termIDF = idf.get(queryTerm) || 1;
|
|
435
|
+
score += termTF * termIDF;
|
|
436
|
+
}
|
|
437
|
+
// Boost title matches
|
|
438
|
+
const titleTokens = new Set(tokenize(item.title));
|
|
439
|
+
for (const queryTerm of queryTokens) {
|
|
440
|
+
if (titleTokens.has(queryTerm)) {
|
|
441
|
+
score *= 1.5;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// Boost recent items (journal)
|
|
445
|
+
if (item.source === "journal" && item.timestamp) {
|
|
446
|
+
const age = Date.now() - new Date(item.timestamp).getTime();
|
|
447
|
+
const daysSinceUpdate = age / (1000 * 60 * 60 * 24);
|
|
448
|
+
if (daysSinceUpdate < 7) {
|
|
449
|
+
score *= 1.3; // Boost recent entries
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return score;
|
|
453
|
+
}
|
|
454
|
+
function semanticSearch(items, query) {
|
|
455
|
+
const queryTokens = tokenize(query);
|
|
456
|
+
if (queryTokens.length === 0)
|
|
457
|
+
return items;
|
|
458
|
+
// Build corpus for IDF
|
|
459
|
+
const documents = items.map(item => tokenize(`${item.title} ${item.content}`));
|
|
460
|
+
const idf = computeIDF(documents);
|
|
461
|
+
// Score and sort
|
|
462
|
+
for (const item of items) {
|
|
463
|
+
item.relevance = scoreItem(item, queryTokens, idf);
|
|
464
|
+
}
|
|
465
|
+
return items
|
|
466
|
+
.filter(item => (item.relevance || 0) > 0)
|
|
467
|
+
.sort((a, b) => (b.relevance || 0) - (a.relevance || 0));
|
|
468
|
+
}
|
|
469
|
+
// ============================================================================
|
|
470
|
+
// Orchestrator
|
|
471
|
+
// ============================================================================
|
|
472
|
+
function getProjectType(projectRoot) {
|
|
473
|
+
const configPath = path.join(projectRoot, ".jfl", "config.json");
|
|
474
|
+
if (fs.existsSync(configPath)) {
|
|
475
|
+
try {
|
|
476
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
477
|
+
return config.type || "standalone";
|
|
478
|
+
}
|
|
479
|
+
catch { }
|
|
480
|
+
}
|
|
481
|
+
return "standalone";
|
|
482
|
+
}
|
|
483
|
+
function getUnifiedContext(projectRoot, query, taskType) {
|
|
484
|
+
const journalItems = readJournalEntries(projectRoot);
|
|
485
|
+
const knowledgeItems = readKnowledgeDocs(projectRoot);
|
|
486
|
+
const projectType = getProjectType(projectRoot);
|
|
487
|
+
const codeItems = readCodeContext(projectRoot);
|
|
488
|
+
const contentItems = projectType === "gtm" ? readGtmContent(projectRoot) : [];
|
|
489
|
+
let items = [...journalItems, ...knowledgeItems, ...codeItems, ...contentItems];
|
|
490
|
+
// Apply semantic search if query provided
|
|
491
|
+
if (query) {
|
|
492
|
+
items = semanticSearch(items, query);
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
items,
|
|
496
|
+
sources: {
|
|
497
|
+
journal: journalItems.length > 0,
|
|
498
|
+
knowledge: knowledgeItems.length > 0,
|
|
499
|
+
code: codeItems.length > 0,
|
|
500
|
+
content: contentItems.length > 0,
|
|
501
|
+
memory: fs.existsSync(path.join(projectRoot, ".jfl", "memory.db")),
|
|
502
|
+
},
|
|
503
|
+
query,
|
|
504
|
+
taskType
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function getChildHubs(projectRoot) {
|
|
508
|
+
const configPath = path.join(projectRoot, ".jfl", "config.json");
|
|
509
|
+
if (!fs.existsSync(configPath))
|
|
510
|
+
return [];
|
|
511
|
+
try {
|
|
512
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
513
|
+
if (config.type !== "portfolio")
|
|
514
|
+
return [];
|
|
515
|
+
const children = [];
|
|
516
|
+
for (const service of config.registered_services || []) {
|
|
517
|
+
if (service.type === "gtm" && service.path) {
|
|
518
|
+
const childPort = getProjectPort(service.path);
|
|
519
|
+
const tokenPath = path.join(service.path, ".jfl", "context-hub.token");
|
|
520
|
+
const token = fs.existsSync(tokenPath)
|
|
521
|
+
? fs.readFileSync(tokenPath, "utf-8").trim()
|
|
522
|
+
: undefined;
|
|
523
|
+
children.push({ name: service.name, path: service.path, port: childPort, token });
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return children;
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async function fetchChildContext(child, endpoint, body) {
|
|
533
|
+
try {
|
|
534
|
+
const headers = { "Content-Type": "application/json" };
|
|
535
|
+
if (child.token)
|
|
536
|
+
headers["Authorization"] = `Bearer ${child.token}`;
|
|
537
|
+
const response = await fetch(`http://localhost:${child.port}${endpoint}`, {
|
|
538
|
+
method: "POST",
|
|
539
|
+
headers,
|
|
540
|
+
body: JSON.stringify(body),
|
|
541
|
+
signal: AbortSignal.timeout(5000),
|
|
542
|
+
});
|
|
543
|
+
if (!response.ok)
|
|
544
|
+
return [];
|
|
545
|
+
const data = (await response.json());
|
|
546
|
+
return (data.items || []).map((item) => ({
|
|
547
|
+
...item,
|
|
548
|
+
title: `[${child.name}] ${item.title}`,
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
return readJournalEntries(child.path, 10).map((item) => ({
|
|
553
|
+
...item,
|
|
554
|
+
title: `[${child.name}] ${item.title}`,
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async function getPortfolioContext(projectRoot, query, taskType, maxItems) {
|
|
559
|
+
const local = getUnifiedContext(projectRoot, query, taskType);
|
|
560
|
+
const children = getChildHubs(projectRoot);
|
|
561
|
+
if (children.length === 0) {
|
|
562
|
+
if (maxItems)
|
|
563
|
+
local.items = local.items.slice(0, maxItems);
|
|
564
|
+
return local;
|
|
565
|
+
}
|
|
566
|
+
const endpoint = query ? "/api/context/search" : "/api/context";
|
|
567
|
+
const body = { maxItems: maxItems || 20 };
|
|
568
|
+
if (query)
|
|
569
|
+
body.query = query;
|
|
570
|
+
if (taskType)
|
|
571
|
+
body.taskType = taskType;
|
|
572
|
+
const childResults = await Promise.all(children.map((child) => fetchChildContext(child, endpoint, body)));
|
|
573
|
+
let merged = [...local.items, ...childResults.flat()];
|
|
574
|
+
if (query) {
|
|
575
|
+
merged = semanticSearch(merged, query);
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
items: maxItems ? merged.slice(0, maxItems) : merged,
|
|
579
|
+
sources: {
|
|
580
|
+
...local.sources,
|
|
581
|
+
journal: true,
|
|
582
|
+
knowledge: true,
|
|
583
|
+
},
|
|
584
|
+
query,
|
|
585
|
+
taskType,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
// ============================================================================
|
|
589
|
+
// HTTP Server
|
|
590
|
+
// ============================================================================
|
|
591
|
+
function createServer(projectRoot, port, eventBus, flowEngine) {
|
|
592
|
+
const server = http.createServer(async (req, res) => {
|
|
593
|
+
const requestStart = Date.now();
|
|
594
|
+
const pathname = new URL(req.url || "/", `http://localhost:${port}`).pathname;
|
|
595
|
+
// Intercept writeHead to capture status code for telemetry
|
|
596
|
+
let capturedStatus = 200;
|
|
597
|
+
const originalWriteHead = res.writeHead.bind(res);
|
|
598
|
+
res.writeHead = function (statusCode, ...args) {
|
|
599
|
+
capturedStatus = statusCode;
|
|
600
|
+
return originalWriteHead(statusCode, ...args);
|
|
601
|
+
};
|
|
602
|
+
// Track request on response finish (skip health/OPTIONS/dashboard)
|
|
603
|
+
const shouldTrack = req.method !== "OPTIONS"
|
|
604
|
+
&& pathname !== "/health"
|
|
605
|
+
&& !pathname.startsWith("/dashboard");
|
|
606
|
+
if (shouldTrack) {
|
|
607
|
+
res.on('finish', () => {
|
|
608
|
+
telemetry.track({
|
|
609
|
+
category: 'context_hub',
|
|
610
|
+
event: 'context_hub:request',
|
|
611
|
+
endpoint: pathname,
|
|
612
|
+
method: req.method,
|
|
613
|
+
status_code: capturedStatus,
|
|
614
|
+
duration_ms: Date.now() - requestStart,
|
|
615
|
+
hub_port: port,
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
// CORS - include Authorization header
|
|
620
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
621
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
622
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
623
|
+
if (req.method === "OPTIONS") {
|
|
624
|
+
res.writeHead(200);
|
|
625
|
+
res.end();
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
629
|
+
// Health check - no auth required (for monitoring)
|
|
630
|
+
if (url.pathname === "/health" && req.method === "GET") {
|
|
631
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
632
|
+
res.end(JSON.stringify({ status: "ok", port }));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
// Setup report — serves .jfl/setup-report.json for dashboard consumption
|
|
636
|
+
if (url.pathname === "/api/setup-report" && req.method === "GET") {
|
|
637
|
+
const reportPath = path.join(projectRoot, ".jfl", "setup-report.json");
|
|
638
|
+
if (fs.existsSync(reportPath)) {
|
|
639
|
+
const report = fs.readFileSync(reportPath, "utf-8");
|
|
640
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
641
|
+
res.end(report);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
645
|
+
res.end(JSON.stringify({ error: "No setup report. Run: jfl setup" }));
|
|
646
|
+
}
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
// Dashboard - served without API auth (has its own token flow in JS)
|
|
650
|
+
if (url.pathname.startsWith("/dashboard")) {
|
|
651
|
+
import("../dashboard/index.js").then(({ handleDashboardRoutes }) => {
|
|
652
|
+
if (!handleDashboardRoutes(req, res, projectRoot, port)) {
|
|
653
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
654
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
655
|
+
}
|
|
656
|
+
}).catch(() => {
|
|
657
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
658
|
+
res.end(JSON.stringify({ error: "Dashboard module failed to load" }));
|
|
659
|
+
});
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
// Hook ingestion (Claude Code HTTP hooks)
|
|
663
|
+
// No auth required — localhost-only. Always returns 200.
|
|
664
|
+
if (url.pathname === "/api/hooks" && req.method === "POST") {
|
|
665
|
+
const remoteAddr = req.socket.remoteAddress || "";
|
|
666
|
+
const isLocalhost = remoteAddr === "127.0.0.1" || remoteAddr === "::1" || remoteAddr === "::ffff:127.0.0.1";
|
|
667
|
+
if (!isLocalhost) {
|
|
668
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
669
|
+
res.end(JSON.stringify({ error: "localhost only" }));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
let body = "";
|
|
673
|
+
let bodySize = 0;
|
|
674
|
+
const MAX_BODY = 64 * 1024;
|
|
675
|
+
req.on("data", (chunk) => {
|
|
676
|
+
bodySize += typeof chunk === "string" ? chunk.length : chunk.byteLength;
|
|
677
|
+
if (bodySize <= MAX_BODY) {
|
|
678
|
+
body += chunk;
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
req.on("end", () => {
|
|
682
|
+
try {
|
|
683
|
+
if (bodySize > MAX_BODY) {
|
|
684
|
+
console.warn(`[hooks] Dropped oversized payload: ${bodySize} bytes`);
|
|
685
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
686
|
+
res.end(JSON.stringify({ ok: true, dropped: true }));
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const payload = JSON.parse(body);
|
|
690
|
+
const partial = transformHookPayload(payload);
|
|
691
|
+
if (eventBus) {
|
|
692
|
+
eventBus.emit(partial);
|
|
693
|
+
}
|
|
694
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
695
|
+
res.end(JSON.stringify({ ok: true }));
|
|
696
|
+
}
|
|
697
|
+
catch (err) {
|
|
698
|
+
console.warn(`[hooks] Parse error: ${err.message}`);
|
|
699
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
700
|
+
res.end(JSON.stringify({ ok: true, parse_error: true }));
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
// All other endpoints require auth
|
|
706
|
+
if (!validateAuth(req, projectRoot, url)) {
|
|
707
|
+
telemetry.track({
|
|
708
|
+
category: 'context_hub',
|
|
709
|
+
event: 'context_hub:auth_failed',
|
|
710
|
+
endpoint: pathname,
|
|
711
|
+
});
|
|
712
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
713
|
+
res.end(JSON.stringify({
|
|
714
|
+
error: "Unauthorized",
|
|
715
|
+
message: "Provide token via Authorization header: Bearer <token>",
|
|
716
|
+
tokenFile: ".jfl/context-hub.token"
|
|
717
|
+
}));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
// Status (includes child hub health for portfolio)
|
|
721
|
+
if (url.pathname === "/api/context/status" && req.method === "GET") {
|
|
722
|
+
const context = getUnifiedContext(projectRoot);
|
|
723
|
+
const children = getChildHubs(projectRoot);
|
|
724
|
+
// Read actual workspace type from config
|
|
725
|
+
let workspaceType = "standalone";
|
|
726
|
+
let workspaceConfig = {};
|
|
727
|
+
const configPath = path.join(projectRoot, ".jfl", "config.json");
|
|
728
|
+
if (fs.existsSync(configPath)) {
|
|
729
|
+
try {
|
|
730
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
731
|
+
if (cfg.type === "portfolio" || cfg.type === "gtm" || cfg.type === "service") {
|
|
732
|
+
workspaceType = cfg.type;
|
|
733
|
+
}
|
|
734
|
+
workspaceConfig = {
|
|
735
|
+
name: cfg.name,
|
|
736
|
+
type: cfg.type,
|
|
737
|
+
description: cfg.description,
|
|
738
|
+
scope: cfg.context_scope || null,
|
|
739
|
+
registered_services: (cfg.registered_services || []).map((s) => ({
|
|
740
|
+
name: s.name,
|
|
741
|
+
path: s.path,
|
|
742
|
+
type: s.type,
|
|
743
|
+
status: s.status,
|
|
744
|
+
context_scope: s.context_scope || null,
|
|
745
|
+
})),
|
|
746
|
+
openclaw_agents: (cfg.openclaw_agents || []).map((a) => ({
|
|
747
|
+
id: a.id,
|
|
748
|
+
runtime: a.runtime,
|
|
749
|
+
registered_at: a.registered_at,
|
|
750
|
+
})),
|
|
751
|
+
gtm_parent: cfg.gtm_parent || null,
|
|
752
|
+
portfolio_parent: cfg.portfolio_parent || null,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
catch { }
|
|
756
|
+
}
|
|
757
|
+
const childStatus = await Promise.all(children.map(async (child) => {
|
|
758
|
+
try {
|
|
759
|
+
const resp = await fetch(`http://localhost:${child.port}/health`, {
|
|
760
|
+
signal: AbortSignal.timeout(2000),
|
|
761
|
+
});
|
|
762
|
+
return { name: child.name, port: child.port, status: resp.ok ? "ok" : "error" };
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
return { name: child.name, port: child.port, status: "down" };
|
|
766
|
+
}
|
|
767
|
+
}));
|
|
768
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
769
|
+
res.end(JSON.stringify({
|
|
770
|
+
status: "running",
|
|
771
|
+
port,
|
|
772
|
+
type: workspaceType,
|
|
773
|
+
config: workspaceConfig,
|
|
774
|
+
sources: context.sources,
|
|
775
|
+
itemCount: context.items.length,
|
|
776
|
+
...(children.length > 0 ? { children: childStatus } : {}),
|
|
777
|
+
}));
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
// Get context (portfolio-aware: fans out to child hubs)
|
|
781
|
+
if (url.pathname === "/api/context" && req.method === "POST") {
|
|
782
|
+
let body = "";
|
|
783
|
+
req.on("data", chunk => body += chunk);
|
|
784
|
+
req.on("end", async () => {
|
|
785
|
+
try {
|
|
786
|
+
const { query, taskType, maxItems } = JSON.parse(body || "{}");
|
|
787
|
+
const context = await getPortfolioContext(projectRoot, query, taskType, maxItems);
|
|
788
|
+
telemetry.track({
|
|
789
|
+
category: 'context_hub',
|
|
790
|
+
event: 'context_hub:context_loaded',
|
|
791
|
+
item_count: context.items.length,
|
|
792
|
+
journal_count: context.items.filter(i => i.source === 'journal').length,
|
|
793
|
+
knowledge_count: context.items.filter(i => i.source === 'knowledge').length,
|
|
794
|
+
code_count: context.items.filter(i => i.source === 'code').length,
|
|
795
|
+
content_count: context.items.filter(i => i.source === 'content').length,
|
|
796
|
+
query_length: query ? query.length : 0,
|
|
797
|
+
});
|
|
798
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
799
|
+
res.end(JSON.stringify(context));
|
|
800
|
+
}
|
|
801
|
+
catch (err) {
|
|
802
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
803
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
// Search (portfolio-aware: fans out to child hubs)
|
|
809
|
+
if (url.pathname === "/api/context/search" && req.method === "POST") {
|
|
810
|
+
let body = "";
|
|
811
|
+
req.on("data", chunk => body += chunk);
|
|
812
|
+
req.on("end", async () => {
|
|
813
|
+
try {
|
|
814
|
+
const { query, maxItems = 20 } = JSON.parse(body || "{}");
|
|
815
|
+
if (!query) {
|
|
816
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
817
|
+
res.end(JSON.stringify({ error: "query required" }));
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const searchStart = Date.now();
|
|
821
|
+
const context = await getPortfolioContext(projectRoot, query, undefined, maxItems);
|
|
822
|
+
context.items = context.items
|
|
823
|
+
.filter(item => item.relevance && item.relevance > 0)
|
|
824
|
+
.slice(0, maxItems);
|
|
825
|
+
telemetry.track({
|
|
826
|
+
category: 'context_hub',
|
|
827
|
+
event: 'context_hub:search',
|
|
828
|
+
result_count: context.items.length,
|
|
829
|
+
duration_ms: Date.now() - searchStart,
|
|
830
|
+
has_query: true,
|
|
831
|
+
query_length: query.length,
|
|
832
|
+
});
|
|
833
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
834
|
+
res.end(JSON.stringify(context));
|
|
835
|
+
}
|
|
836
|
+
catch (err) {
|
|
837
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
838
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
// Services registry
|
|
844
|
+
if (url.pathname === "/api/services" && req.method === "GET") {
|
|
845
|
+
try {
|
|
846
|
+
const services = discoverServices(projectRoot);
|
|
847
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
848
|
+
res.end(JSON.stringify(services));
|
|
849
|
+
}
|
|
850
|
+
catch (err) {
|
|
851
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
852
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
853
|
+
}
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
// Get specific service
|
|
857
|
+
if (url.pathname.startsWith("/api/services/") && req.method === "GET") {
|
|
858
|
+
try {
|
|
859
|
+
const serviceName = url.pathname.replace("/api/services/", "");
|
|
860
|
+
const services = discoverServices(projectRoot);
|
|
861
|
+
const service = services[serviceName];
|
|
862
|
+
if (!service) {
|
|
863
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
864
|
+
res.end(JSON.stringify({ error: "Service not found" }));
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
868
|
+
res.end(JSON.stringify(service));
|
|
869
|
+
}
|
|
870
|
+
catch (err) {
|
|
871
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
872
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
873
|
+
}
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
// RAG Chat — search context + memory, then stream LLM response
|
|
877
|
+
if (url.pathname === "/api/chat" && req.method === "POST") {
|
|
878
|
+
let body = "";
|
|
879
|
+
req.on("data", chunk => body += chunk);
|
|
880
|
+
req.on("end", async () => {
|
|
881
|
+
try {
|
|
882
|
+
const { message, history = [] } = JSON.parse(body || "{}");
|
|
883
|
+
if (!message) {
|
|
884
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
885
|
+
res.end(JSON.stringify({ error: "message required" }));
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const [memRaw, ctxResult] = await Promise.allSettled([
|
|
889
|
+
searchMemories(message, { maxItems: 5 }),
|
|
890
|
+
getPortfolioContext(projectRoot, message, undefined, 5),
|
|
891
|
+
]);
|
|
892
|
+
const memResults = memRaw.status === "fulfilled"
|
|
893
|
+
? memRaw.value.map(r => ({ title: r.memory.title, content: r.memory.content, type: r.memory.type, relevance: r.relevance }))
|
|
894
|
+
: [];
|
|
895
|
+
const ctxResults = ctxResult.status === "fulfilled"
|
|
896
|
+
? (ctxResult.value.items || []).slice(0, 5).map((i) => ({ title: i.title || i.path, content: i.content?.slice(0, 500), type: i.type }))
|
|
897
|
+
: [];
|
|
898
|
+
const sources = [...memResults, ...ctxResults];
|
|
899
|
+
let contextBlock = "";
|
|
900
|
+
if (sources.length > 0) {
|
|
901
|
+
contextBlock = "Here is relevant context from the project:\n\n" +
|
|
902
|
+
sources.map((s, i) => `[${i + 1}] ${s.title} (${s.type})\n${s.content}`).join("\n\n") +
|
|
903
|
+
"\n\n---\n\n";
|
|
904
|
+
}
|
|
905
|
+
// Load .env from project root for API keys
|
|
906
|
+
const envPath = path.join(projectRoot, ".env");
|
|
907
|
+
if (fs.existsSync(envPath)) {
|
|
908
|
+
for (const line of fs.readFileSync(envPath, "utf-8").split("\n")) {
|
|
909
|
+
const match = line.match(/^([A-Z_]+)=(.+)$/);
|
|
910
|
+
if (match && !process.env[match[1]])
|
|
911
|
+
process.env[match[1]] = match[2].trim();
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
const apiKey = process.env.STRATUS_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY;
|
|
915
|
+
if (!apiKey) {
|
|
916
|
+
res.writeHead(200, {
|
|
917
|
+
"Content-Type": "text/event-stream",
|
|
918
|
+
"Cache-Control": "no-cache",
|
|
919
|
+
"Connection": "keep-alive",
|
|
920
|
+
});
|
|
921
|
+
const fallback = sources.length > 0
|
|
922
|
+
? "I found relevant context but no LLM API key is configured. Set STRATUS_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY in your .env.\n\n**Sources found:**\n" +
|
|
923
|
+
sources.map((s, i) => `${i + 1}. **${s.title}** \`${s.type}\`\n ${s.content?.slice(0, 150)}`).join("\n")
|
|
924
|
+
: "No API key configured and no relevant context found.";
|
|
925
|
+
res.write(`data: ${JSON.stringify({ sources })}\n\n`);
|
|
926
|
+
res.write(`data: ${JSON.stringify({ delta: fallback })}\n\n`);
|
|
927
|
+
res.write("data: [DONE]\n\n");
|
|
928
|
+
res.end();
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const useStratus = apiKey === process.env.STRATUS_API_KEY;
|
|
932
|
+
const baseURL = useStratus ? "https://api.stratus.run/v1" : undefined;
|
|
933
|
+
const model = useStratus ? "stratus-x1ac-huge-claude-sonnet-4-6" : "gpt-4o-mini";
|
|
934
|
+
const OpenAI = (await import("openai")).default;
|
|
935
|
+
const client = new OpenAI({ apiKey, baseURL });
|
|
936
|
+
const messages = [
|
|
937
|
+
{
|
|
938
|
+
role: "system",
|
|
939
|
+
content: `You are JFL Assistant, helping the user understand their project context. Answer questions based on the provided context from journal entries, knowledge docs, and code files. Be concise and direct. If the context doesn't contain enough information, say so. Reference specific sources when possible.\n\n${contextBlock}`
|
|
940
|
+
},
|
|
941
|
+
...history.slice(-6).map((m) => ({ role: m.role, content: m.content })),
|
|
942
|
+
{ role: "user", content: message },
|
|
943
|
+
];
|
|
944
|
+
res.writeHead(200, {
|
|
945
|
+
"Content-Type": "text/event-stream",
|
|
946
|
+
"Cache-Control": "no-cache",
|
|
947
|
+
"Connection": "keep-alive",
|
|
948
|
+
});
|
|
949
|
+
res.write(`data: ${JSON.stringify({ sources })}\n\n`);
|
|
950
|
+
try {
|
|
951
|
+
const stream = await client.chat.completions.create({
|
|
952
|
+
model,
|
|
953
|
+
messages,
|
|
954
|
+
stream: true,
|
|
955
|
+
max_tokens: 1024,
|
|
956
|
+
});
|
|
957
|
+
for await (const chunk of stream) {
|
|
958
|
+
const delta = chunk.choices?.[0]?.delta?.content;
|
|
959
|
+
if (delta) {
|
|
960
|
+
res.write(`data: ${JSON.stringify({ delta })}\n\n`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
catch (llmErr) {
|
|
965
|
+
res.write(`data: ${JSON.stringify({ delta: `\n\nLLM error: ${llmErr.message}` })}\n\n`);
|
|
966
|
+
}
|
|
967
|
+
res.write("data: [DONE]\n\n");
|
|
968
|
+
res.end();
|
|
969
|
+
}
|
|
970
|
+
catch (err) {
|
|
971
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
972
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
// Memory search
|
|
978
|
+
if (url.pathname === "/api/memory/search" && req.method === "POST") {
|
|
979
|
+
let body = "";
|
|
980
|
+
req.on("data", chunk => body += chunk);
|
|
981
|
+
req.on("end", async () => {
|
|
982
|
+
try {
|
|
983
|
+
const { query, type, maxItems = 10, since } = JSON.parse(body || "{}");
|
|
984
|
+
if (!query) {
|
|
985
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
986
|
+
res.end(JSON.stringify({ error: "query required" }));
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const raw = await searchMemories(query, { type, maxItems, since });
|
|
990
|
+
const results = raw.map(r => ({
|
|
991
|
+
id: r.memory.id,
|
|
992
|
+
title: r.memory.title,
|
|
993
|
+
content: r.memory.content,
|
|
994
|
+
summary: r.memory.summary,
|
|
995
|
+
type: r.memory.type,
|
|
996
|
+
source: r.memory.source,
|
|
997
|
+
ts: r.memory.created_at,
|
|
998
|
+
score: r.score,
|
|
999
|
+
relevance: r.relevance,
|
|
1000
|
+
}));
|
|
1001
|
+
// Surface linked memories (teacup ↔ insight connections)
|
|
1002
|
+
const linkedResults = [];
|
|
1003
|
+
for (const r of results) {
|
|
1004
|
+
if (r.id) {
|
|
1005
|
+
try {
|
|
1006
|
+
const linksFrom = await getLinksFrom(r.id);
|
|
1007
|
+
const linksTo = await getLinksTo(r.id);
|
|
1008
|
+
const linkedIds = [
|
|
1009
|
+
...linksFrom.map(l => l.to_memory_id),
|
|
1010
|
+
...linksTo.map(l => l.from_memory_id)
|
|
1011
|
+
].filter(id => !results.some(existing => existing.id === id));
|
|
1012
|
+
for (const linkedId of linkedIds.slice(0, 3)) {
|
|
1013
|
+
try {
|
|
1014
|
+
const [linked] = await getMemoriesByIds([linkedId]);
|
|
1015
|
+
if (linked) {
|
|
1016
|
+
const linkType = linksFrom.find(l => l.to_memory_id === linkedId)?.link_type
|
|
1017
|
+
|| linksTo.find(l => l.from_memory_id === linkedId)?.link_type
|
|
1018
|
+
|| 'related_to';
|
|
1019
|
+
linkedResults.push({
|
|
1020
|
+
id: linked.id,
|
|
1021
|
+
title: `🔗 ${linkType === 'leads_to' ? '🫖→' : '←🫖'} ${linked.title}`,
|
|
1022
|
+
content: linked.content,
|
|
1023
|
+
summary: linked.summary,
|
|
1024
|
+
type: linked.type,
|
|
1025
|
+
source: linked.source,
|
|
1026
|
+
ts: linked.created_at,
|
|
1027
|
+
score: r.score * 0.8,
|
|
1028
|
+
relevance: 'medium',
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
catch { /* skip broken links */ }
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
catch { /* links optional */ }
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1039
|
+
res.end(JSON.stringify({ results: [...results, ...linkedResults] }));
|
|
1040
|
+
}
|
|
1041
|
+
catch (err) {
|
|
1042
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1043
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
// Memory status
|
|
1049
|
+
if (url.pathname === "/api/memory/status" && req.method === "GET") {
|
|
1050
|
+
getMemoryStats()
|
|
1051
|
+
.then(stats => {
|
|
1052
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1053
|
+
res.end(JSON.stringify(stats));
|
|
1054
|
+
})
|
|
1055
|
+
.catch((err) => {
|
|
1056
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1057
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1058
|
+
});
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
// Memory add
|
|
1062
|
+
if (url.pathname === "/api/memory/add" && req.method === "POST") {
|
|
1063
|
+
let body = "";
|
|
1064
|
+
req.on("data", chunk => body += chunk);
|
|
1065
|
+
req.on("end", async () => {
|
|
1066
|
+
try {
|
|
1067
|
+
const { title, content, tags, type, linked_to } = JSON.parse(body || "{}");
|
|
1068
|
+
if (!title || !content) {
|
|
1069
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1070
|
+
res.end(JSON.stringify({ error: "title and content required" }));
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const id = await insertMemory({
|
|
1074
|
+
source: 'manual',
|
|
1075
|
+
type: type || undefined,
|
|
1076
|
+
title,
|
|
1077
|
+
content,
|
|
1078
|
+
created_at: new Date().toISOString()
|
|
1079
|
+
});
|
|
1080
|
+
// If this teacup/insight links to another memory, create the link
|
|
1081
|
+
if (linked_to && typeof linked_to === 'number') {
|
|
1082
|
+
try {
|
|
1083
|
+
const linkType = type === 'teacup' ? 'leads_to' : 'related_to';
|
|
1084
|
+
await addLink(id, linked_to, linkType);
|
|
1085
|
+
}
|
|
1086
|
+
catch { /* link is optional, don't fail the add */ }
|
|
1087
|
+
}
|
|
1088
|
+
res.writeHead(201, { "Content-Type": "application/json" });
|
|
1089
|
+
res.end(JSON.stringify({ id }));
|
|
1090
|
+
}
|
|
1091
|
+
catch (err) {
|
|
1092
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1093
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
// POST /api/memory/index — trigger journal→memory indexing + embedding backfill
|
|
1099
|
+
if (url.pathname === "/api/memory/index" && req.method === "POST") {
|
|
1100
|
+
let body = "";
|
|
1101
|
+
req.on("data", (chunk) => body += chunk);
|
|
1102
|
+
req.on("end", async () => {
|
|
1103
|
+
try {
|
|
1104
|
+
const { force, backfill } = JSON.parse(body || "{}");
|
|
1105
|
+
const stats = await indexJournalEntries(force ?? false);
|
|
1106
|
+
let embeddingStats = { updated: 0, errors: 0 };
|
|
1107
|
+
if (backfill !== false) {
|
|
1108
|
+
embeddingStats = await backfillEmbeddings();
|
|
1109
|
+
}
|
|
1110
|
+
const codeStats = await indexCodeHeaders();
|
|
1111
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1112
|
+
res.end(JSON.stringify({ ok: true, ...stats, embeddings_backfilled: embeddingStats.updated, embedding_errors: embeddingStats.errors, code_headers_added: codeStats.added, code_headers_updated: codeStats.updated }));
|
|
1113
|
+
}
|
|
1114
|
+
catch (err) {
|
|
1115
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1116
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
// POST /api/memory/link — add a graph edge between two memories
|
|
1122
|
+
if (url.pathname === "/api/memory/link" && req.method === "POST") {
|
|
1123
|
+
let body = "";
|
|
1124
|
+
req.on("data", (chunk) => body += chunk);
|
|
1125
|
+
req.on("end", async () => {
|
|
1126
|
+
try {
|
|
1127
|
+
const { from, to, type } = JSON.parse(body || "{}");
|
|
1128
|
+
if (!from || !to || !type) {
|
|
1129
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1130
|
+
res.end(JSON.stringify({ error: "from, to, and type are required" }));
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const { addLink } = await import("../lib/memory-db.js");
|
|
1134
|
+
const id = await addLink(from, to, type);
|
|
1135
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1136
|
+
res.end(JSON.stringify({ ok: true, id }));
|
|
1137
|
+
}
|
|
1138
|
+
catch (err) {
|
|
1139
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1140
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
// Eval trajectory
|
|
1146
|
+
if (url.pathname === "/api/eval/trajectory" && req.method === "GET") {
|
|
1147
|
+
try {
|
|
1148
|
+
const { getTrajectory } = await import("../lib/eval-store.js");
|
|
1149
|
+
const agent = url.searchParams.get("agent") || "";
|
|
1150
|
+
const metric = url.searchParams.get("metric") || "composite";
|
|
1151
|
+
if (!agent) {
|
|
1152
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1153
|
+
res.end(JSON.stringify({ error: "agent query param required" }));
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const points = getTrajectory(agent, metric, projectRoot);
|
|
1157
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1158
|
+
res.end(JSON.stringify({ agent, metric, points }));
|
|
1159
|
+
}
|
|
1160
|
+
catch (err) {
|
|
1161
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1162
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1163
|
+
}
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
// Eval entries (all raw entries for cycle display)
|
|
1167
|
+
if (url.pathname === "/api/eval/entries" && req.method === "GET") {
|
|
1168
|
+
try {
|
|
1169
|
+
const { readEvals } = await import("../lib/eval-store.js");
|
|
1170
|
+
const limit = parseInt(url.searchParams.get("limit") || "100", 10);
|
|
1171
|
+
const entries = readEvals(projectRoot)
|
|
1172
|
+
.sort((a, b) => b.ts.localeCompare(a.ts))
|
|
1173
|
+
.slice(0, limit);
|
|
1174
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1175
|
+
res.end(JSON.stringify({ entries }));
|
|
1176
|
+
}
|
|
1177
|
+
catch (err) {
|
|
1178
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1179
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1180
|
+
}
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
// Eval leaderboard
|
|
1184
|
+
if (url.pathname === "/api/eval/leaderboard" && req.method === "GET") {
|
|
1185
|
+
try {
|
|
1186
|
+
const { readEvals, listAgents, getLatestEval, getTrajectory } = await import("../lib/eval-store.js");
|
|
1187
|
+
const agents = listAgents(projectRoot);
|
|
1188
|
+
const leaderboard = agents.map(agent => {
|
|
1189
|
+
const latest = getLatestEval(agent, projectRoot);
|
|
1190
|
+
const trajectory = getTrajectory(agent, "composite", projectRoot);
|
|
1191
|
+
const prevPoint = trajectory.length >= 2 ? trajectory[trajectory.length - 2] : null;
|
|
1192
|
+
const delta = latest?.composite != null && prevPoint
|
|
1193
|
+
? latest.composite - prevPoint.value
|
|
1194
|
+
: null;
|
|
1195
|
+
return {
|
|
1196
|
+
agent,
|
|
1197
|
+
composite: latest?.composite ?? null,
|
|
1198
|
+
metrics: latest?.metrics ?? {},
|
|
1199
|
+
delta,
|
|
1200
|
+
model_version: latest?.model_version ?? null,
|
|
1201
|
+
lastTs: latest?.ts ?? null,
|
|
1202
|
+
trajectory: trajectory.slice(-20).map(p => p.value),
|
|
1203
|
+
};
|
|
1204
|
+
}).sort((a, b) => (b.composite ?? 0) - (a.composite ?? 0));
|
|
1205
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1206
|
+
res.end(JSON.stringify(leaderboard));
|
|
1207
|
+
}
|
|
1208
|
+
catch (err) {
|
|
1209
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1210
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1211
|
+
}
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
// Synopsis (work summary)
|
|
1215
|
+
if (url.pathname === "/api/synopsis" && req.method === "GET") {
|
|
1216
|
+
try {
|
|
1217
|
+
const hours = parseInt(url.searchParams.get("hours") || "24", 10);
|
|
1218
|
+
const author = url.searchParams.get("author") || undefined;
|
|
1219
|
+
const { generateSynopsis } = await import("./synopsis.js");
|
|
1220
|
+
const synopsis = generateSynopsis(projectRoot, hours, author);
|
|
1221
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1222
|
+
res.end(JSON.stringify(synopsis));
|
|
1223
|
+
}
|
|
1224
|
+
catch (err) {
|
|
1225
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1226
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1227
|
+
}
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
// Prediction accuracy (Stratus)
|
|
1231
|
+
if (url.pathname === "/api/eval/predictions" && req.method === "GET") {
|
|
1232
|
+
try {
|
|
1233
|
+
const { Predictor } = await import("../lib/predictor.js");
|
|
1234
|
+
const predictor = new Predictor({ projectRoot });
|
|
1235
|
+
const accuracy = predictor.getAccuracy();
|
|
1236
|
+
const recent = predictor.getHistory(20).reverse();
|
|
1237
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1238
|
+
res.end(JSON.stringify({ accuracy, recent }));
|
|
1239
|
+
}
|
|
1240
|
+
catch (err) {
|
|
1241
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1242
|
+
res.end(JSON.stringify({ accuracy: { total: 0, resolved: 0, direction_accuracy: 0, mean_delta_error: 0, calibration: 0 }, recent: [] }));
|
|
1243
|
+
}
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
// Cross-project health
|
|
1247
|
+
if (url.pathname === "/api/projects" && req.method === "GET") {
|
|
1248
|
+
const tracked = getTrackedProjects();
|
|
1249
|
+
Promise.all(tracked.map(async (p) => {
|
|
1250
|
+
// Self-check: if this is our own port, we know we're OK
|
|
1251
|
+
if (p.port === port) {
|
|
1252
|
+
return {
|
|
1253
|
+
name: p.path.split("/").pop() || p.path,
|
|
1254
|
+
path: p.path,
|
|
1255
|
+
port: p.port,
|
|
1256
|
+
status: "OK",
|
|
1257
|
+
pid: process.pid,
|
|
1258
|
+
message: "This instance",
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
const result = await diagnoseProject(p.path, p.port);
|
|
1262
|
+
return {
|
|
1263
|
+
name: p.path.split("/").pop() || p.path,
|
|
1264
|
+
path: p.path,
|
|
1265
|
+
port: p.port,
|
|
1266
|
+
status: result.status,
|
|
1267
|
+
pid: result.pid,
|
|
1268
|
+
message: result.message,
|
|
1269
|
+
};
|
|
1270
|
+
}))
|
|
1271
|
+
.then((results) => {
|
|
1272
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1273
|
+
res.end(JSON.stringify(results));
|
|
1274
|
+
})
|
|
1275
|
+
.catch((err) => {
|
|
1276
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1277
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1278
|
+
});
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
// Publish event
|
|
1282
|
+
if (url.pathname === "/api/events" && req.method === "POST") {
|
|
1283
|
+
if (!eventBus) {
|
|
1284
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1285
|
+
res.end(JSON.stringify({ error: "Event bus not initialized" }));
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
let body = "";
|
|
1289
|
+
req.on("data", chunk => body += chunk);
|
|
1290
|
+
req.on("end", () => {
|
|
1291
|
+
try {
|
|
1292
|
+
const { type, source, target, session, data, ttl } = JSON.parse(body || "{}");
|
|
1293
|
+
if (!type || !source) {
|
|
1294
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1295
|
+
res.end(JSON.stringify({ error: "type and source required" }));
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
const event = eventBus.emit({
|
|
1299
|
+
type: type,
|
|
1300
|
+
source,
|
|
1301
|
+
target,
|
|
1302
|
+
session,
|
|
1303
|
+
data: data || {},
|
|
1304
|
+
ttl,
|
|
1305
|
+
});
|
|
1306
|
+
res.writeHead(201, { "Content-Type": "application/json" });
|
|
1307
|
+
res.end(JSON.stringify(event));
|
|
1308
|
+
}
|
|
1309
|
+
catch (err) {
|
|
1310
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1311
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
// Get recent events
|
|
1317
|
+
if (url.pathname === "/api/events" && req.method === "GET") {
|
|
1318
|
+
if (!eventBus) {
|
|
1319
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1320
|
+
res.end(JSON.stringify({ error: "Event bus not initialized" }));
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
const since = url.searchParams.get("since") || undefined;
|
|
1324
|
+
const pattern = url.searchParams.get("pattern") || undefined;
|
|
1325
|
+
const limit = url.searchParams.get("limit") ? parseInt(url.searchParams.get("limit"), 10) : 50;
|
|
1326
|
+
const events = eventBus.getEvents({ since, pattern, limit });
|
|
1327
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1328
|
+
res.end(JSON.stringify({ events, count: events.length }));
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
// SSE event stream
|
|
1332
|
+
if (url.pathname === "/api/events/stream" && req.method === "GET") {
|
|
1333
|
+
if (!eventBus) {
|
|
1334
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1335
|
+
res.end(JSON.stringify({ error: "Event bus not initialized" }));
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
const patterns = (url.searchParams.get("patterns") || "*").split(",");
|
|
1339
|
+
res.writeHead(200, {
|
|
1340
|
+
"Content-Type": "text/event-stream",
|
|
1341
|
+
"Cache-Control": "no-cache",
|
|
1342
|
+
Connection: "keep-alive",
|
|
1343
|
+
"Access-Control-Allow-Origin": "*",
|
|
1344
|
+
});
|
|
1345
|
+
res.write("retry: 3000\n\n");
|
|
1346
|
+
const sub = eventBus.subscribe({
|
|
1347
|
+
clientId: `sse-${Date.now()}`,
|
|
1348
|
+
patterns,
|
|
1349
|
+
transport: "sse",
|
|
1350
|
+
callback: (event) => {
|
|
1351
|
+
res.write(`id: ${event.id}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
|
|
1352
|
+
},
|
|
1353
|
+
});
|
|
1354
|
+
req.on("close", () => {
|
|
1355
|
+
eventBus.unsubscribe(sub.id);
|
|
1356
|
+
});
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
// Telemetry digest
|
|
1360
|
+
if (url.pathname === "/api/telemetry/digest" && req.method === "GET") {
|
|
1361
|
+
try {
|
|
1362
|
+
const { loadLocalEvents, analyzeEvents } = await import("../lib/telemetry-digest.js");
|
|
1363
|
+
const hours = parseInt(url.searchParams.get("hours") || "168", 10);
|
|
1364
|
+
const events = loadLocalEvents();
|
|
1365
|
+
const digest = analyzeEvents(events, hours);
|
|
1366
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1367
|
+
res.end(JSON.stringify(digest));
|
|
1368
|
+
}
|
|
1369
|
+
catch (err) {
|
|
1370
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1371
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1372
|
+
}
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
// Telemetry agent status
|
|
1376
|
+
if (url.pathname === "/api/telemetry/agent" && req.method === "GET") {
|
|
1377
|
+
const agent = server.__telemetryAgent;
|
|
1378
|
+
if (agent) {
|
|
1379
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1380
|
+
res.end(JSON.stringify(agent.getStatus()));
|
|
1381
|
+
}
|
|
1382
|
+
else {
|
|
1383
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1384
|
+
res.end(JSON.stringify({ running: false, lastRun: '', runCount: 0, lastInsights: [] }));
|
|
1385
|
+
}
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
// Telemetry agent: trigger manual run
|
|
1389
|
+
if (url.pathname === "/api/telemetry/agent/run" && req.method === "POST") {
|
|
1390
|
+
const agent = server.__telemetryAgent;
|
|
1391
|
+
if (agent) {
|
|
1392
|
+
try {
|
|
1393
|
+
const insights = await agent.run();
|
|
1394
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1395
|
+
res.end(JSON.stringify({ ok: true, insights }));
|
|
1396
|
+
}
|
|
1397
|
+
catch (err) {
|
|
1398
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1399
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
else {
|
|
1403
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1404
|
+
res.end(JSON.stringify({ error: "Telemetry agent not running" }));
|
|
1405
|
+
}
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
// ── Autoresearch / Experiments API ──────────────────────────────
|
|
1409
|
+
// Agent configs
|
|
1410
|
+
if (url.pathname === "/api/v1/agents" && req.method === "GET") {
|
|
1411
|
+
try {
|
|
1412
|
+
const { listAgentConfigs, loadAgentConfig } = await import("../lib/agent-config.js");
|
|
1413
|
+
const names = listAgentConfigs(projectRoot);
|
|
1414
|
+
const agents = names.map(name => {
|
|
1415
|
+
try {
|
|
1416
|
+
return loadAgentConfig(projectRoot, name);
|
|
1417
|
+
}
|
|
1418
|
+
catch {
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
}).filter(Boolean);
|
|
1422
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1423
|
+
res.end(JSON.stringify({ agents }));
|
|
1424
|
+
}
|
|
1425
|
+
catch (err) {
|
|
1426
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1427
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1428
|
+
}
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
// Replay buffer (experiment history)
|
|
1432
|
+
if (url.pathname === "/api/v1/experiments" && req.method === "GET") {
|
|
1433
|
+
try {
|
|
1434
|
+
const bufferPath = path.join(projectRoot, ".jfl", "replay-buffer.jsonl");
|
|
1435
|
+
const trainingPath = path.join(projectRoot, ".jfl", "training-buffer.jsonl");
|
|
1436
|
+
const experiments = [];
|
|
1437
|
+
for (const p of [bufferPath, trainingPath]) {
|
|
1438
|
+
if (fs.existsSync(p)) {
|
|
1439
|
+
const lines = fs.readFileSync(p, "utf-8").trim().split("\n").filter(Boolean);
|
|
1440
|
+
for (const line of lines.slice(-100)) {
|
|
1441
|
+
try {
|
|
1442
|
+
experiments.push(JSON.parse(line));
|
|
1443
|
+
}
|
|
1444
|
+
catch { }
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
const agent = url.searchParams.get("agent");
|
|
1449
|
+
const filtered = agent ? experiments.filter(e => e.agent === agent) : experiments;
|
|
1450
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1451
|
+
res.end(JSON.stringify({ experiments: filtered, total: filtered.length }));
|
|
1452
|
+
}
|
|
1453
|
+
catch (err) {
|
|
1454
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1455
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1456
|
+
}
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
// Session results
|
|
1460
|
+
if (url.pathname === "/api/v1/sessions" && req.method === "GET") {
|
|
1461
|
+
try {
|
|
1462
|
+
const sessionsDir = path.join(projectRoot, ".jfl", "sessions");
|
|
1463
|
+
const sessions = [];
|
|
1464
|
+
if (fs.existsSync(sessionsDir)) {
|
|
1465
|
+
for (const dir of fs.readdirSync(sessionsDir)) {
|
|
1466
|
+
const resultsPath = path.join(sessionsDir, dir, "results.tsv");
|
|
1467
|
+
if (fs.existsSync(resultsPath)) {
|
|
1468
|
+
const content = fs.readFileSync(resultsPath, "utf-8").trim();
|
|
1469
|
+
const lines = content.split("\n").slice(1); // skip header
|
|
1470
|
+
const rounds = lines.filter(l => l.trim()).map(line => {
|
|
1471
|
+
const [round, task, baseline, metric, delta, kept, duration, error, timestamp] = line.split("\t");
|
|
1472
|
+
return { round: +round, task, baseline: +baseline, metric: +metric, delta: +delta, kept: kept === "1", duration_ms: +duration, error, timestamp };
|
|
1473
|
+
});
|
|
1474
|
+
if (rounds.length > 0) {
|
|
1475
|
+
sessions.push({ id: dir, rounds, agent: dir.replace(/-[a-f0-9]{8}-\d+$/, "") });
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1481
|
+
res.end(JSON.stringify({ sessions }));
|
|
1482
|
+
}
|
|
1483
|
+
catch (err) {
|
|
1484
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1485
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1486
|
+
}
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
// Product context
|
|
1490
|
+
if (url.pathname === "/api/v1/product-context" && req.method === "GET") {
|
|
1491
|
+
const contextPath = path.join(projectRoot, ".jfl", "product-context.md");
|
|
1492
|
+
if (fs.existsSync(contextPath)) {
|
|
1493
|
+
const content = fs.readFileSync(contextPath, "utf-8");
|
|
1494
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1495
|
+
res.end(JSON.stringify({ context: content, updatedAt: fs.statSync(contextPath).mtime.toISOString() }));
|
|
1496
|
+
}
|
|
1497
|
+
else {
|
|
1498
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1499
|
+
res.end(JSON.stringify({ context: null, updatedAt: null }));
|
|
1500
|
+
}
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
// Product analysis (telemetry agent v2)
|
|
1504
|
+
if (url.pathname === "/api/v1/product-analysis" && req.method === "GET") {
|
|
1505
|
+
try {
|
|
1506
|
+
const { TelemetryAgentV2 } = await import("../lib/telemetry-agent-v2.js");
|
|
1507
|
+
const agent = new TelemetryAgentV2({ projectRoot });
|
|
1508
|
+
const analysis = await agent.analyzeProduct();
|
|
1509
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1510
|
+
res.end(JSON.stringify(analysis));
|
|
1511
|
+
}
|
|
1512
|
+
catch (err) {
|
|
1513
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1514
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1515
|
+
}
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
// Kanban board
|
|
1519
|
+
// ── Kanban API (GitHub Issues-backed) ──────────────────────
|
|
1520
|
+
if (url.pathname === "/api/kanban" && req.method === "GET") {
|
|
1521
|
+
try {
|
|
1522
|
+
const { GitHubKanban } = await import("../lib/kanban-github.js");
|
|
1523
|
+
const scope = url.searchParams.get("scope") ?? undefined;
|
|
1524
|
+
const source = url.searchParams.get("source") ?? undefined;
|
|
1525
|
+
const filter = (scope || source) ? { scope, source } : undefined;
|
|
1526
|
+
const kb = new GitHubKanban(projectRoot);
|
|
1527
|
+
const board = await kb.getBoard(filter);
|
|
1528
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1529
|
+
res.end(JSON.stringify(board));
|
|
1530
|
+
}
|
|
1531
|
+
catch (err) {
|
|
1532
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1533
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1534
|
+
}
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
if (url.pathname === "/api/kanban/cards" && req.method === "GET") {
|
|
1538
|
+
try {
|
|
1539
|
+
const { GitHubKanban } = await import("../lib/kanban-github.js");
|
|
1540
|
+
const scope = url.searchParams.get("scope") ?? undefined;
|
|
1541
|
+
const source = url.searchParams.get("source") ?? undefined;
|
|
1542
|
+
const filter = (scope || source) ? { scope, source } : undefined;
|
|
1543
|
+
const kb = new GitHubKanban(projectRoot);
|
|
1544
|
+
const cards = await kb.getCards(filter);
|
|
1545
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1546
|
+
res.end(JSON.stringify(cards));
|
|
1547
|
+
}
|
|
1548
|
+
catch (err) {
|
|
1549
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1550
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1551
|
+
}
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
if (url.pathname === "/api/kanban/scopes" && req.method === "GET") {
|
|
1555
|
+
try {
|
|
1556
|
+
const { GitHubKanban } = await import("../lib/kanban-github.js");
|
|
1557
|
+
const kb = new GitHubKanban(projectRoot);
|
|
1558
|
+
const workspace = kb.getWorkspaceInfo();
|
|
1559
|
+
const services = kb.getRegisteredServices();
|
|
1560
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1561
|
+
res.end(JSON.stringify({ workspace, services }));
|
|
1562
|
+
}
|
|
1563
|
+
catch (err) {
|
|
1564
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1565
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1566
|
+
}
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
if (url.pathname === "/api/kanban" && req.method === "POST") {
|
|
1570
|
+
let body = "";
|
|
1571
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
1572
|
+
req.on("end", async () => {
|
|
1573
|
+
try {
|
|
1574
|
+
const data = JSON.parse(body || "{}");
|
|
1575
|
+
const { GitHubKanban } = await import("../lib/kanban-github.js");
|
|
1576
|
+
const kb = new GitHubKanban(projectRoot);
|
|
1577
|
+
if (data.action === "create") {
|
|
1578
|
+
const number = kb.create({
|
|
1579
|
+
title: data.title,
|
|
1580
|
+
description: data.description,
|
|
1581
|
+
source: data.source ?? "human",
|
|
1582
|
+
priority: data.priority ?? 0,
|
|
1583
|
+
scope: data.scope,
|
|
1584
|
+
labels: data.labels,
|
|
1585
|
+
});
|
|
1586
|
+
res.writeHead(201, { "Content-Type": "application/json" });
|
|
1587
|
+
res.end(JSON.stringify({ id: `#${number}`, number }));
|
|
1588
|
+
}
|
|
1589
|
+
else if (data.action === "move") {
|
|
1590
|
+
// Accept both #8 and 8 format
|
|
1591
|
+
const num = typeof data.id === "string" ? parseInt(data.id.replace("#", "")) : data.id;
|
|
1592
|
+
kb.move(num, data.column);
|
|
1593
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1594
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1595
|
+
}
|
|
1596
|
+
else if (data.action === "pick") {
|
|
1597
|
+
const num = typeof data.id === "string" ? parseInt(data.id.replace("#", "")) : data.id;
|
|
1598
|
+
kb.pick(num, data.agent);
|
|
1599
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1600
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1601
|
+
}
|
|
1602
|
+
else {
|
|
1603
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1604
|
+
res.end(JSON.stringify({ error: "Unknown action" }));
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
catch (err) {
|
|
1608
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1609
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
// Flow definitions
|
|
1615
|
+
if (url.pathname === "/api/flows" && req.method === "GET") {
|
|
1616
|
+
if (!flowEngine) {
|
|
1617
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1618
|
+
res.end(JSON.stringify([]));
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1622
|
+
res.end(JSON.stringify(flowEngine.getFlows()));
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
// Flow executions
|
|
1626
|
+
if (url.pathname === "/api/flows/executions" && req.method === "GET") {
|
|
1627
|
+
if (!flowEngine) {
|
|
1628
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1629
|
+
res.end(JSON.stringify({ executions: [] }));
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1633
|
+
res.end(JSON.stringify({ executions: flowEngine.getExecutions() }));
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
// Flow approval
|
|
1637
|
+
if (url.pathname.match(/^\/api\/flows\/[^/]+\/approve$/) && req.method === "POST") {
|
|
1638
|
+
if (!flowEngine) {
|
|
1639
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1640
|
+
res.end(JSON.stringify({ error: "Flow engine not initialized" }));
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
const flowName = decodeURIComponent(url.pathname.split("/")[3]);
|
|
1644
|
+
let body = "";
|
|
1645
|
+
req.on("data", chunk => body += chunk);
|
|
1646
|
+
req.on("end", async () => {
|
|
1647
|
+
try {
|
|
1648
|
+
const { trigger_event_id } = JSON.parse(body || "{}");
|
|
1649
|
+
if (!trigger_event_id) {
|
|
1650
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1651
|
+
res.end(JSON.stringify({ error: "trigger_event_id required" }));
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
const result = await flowEngine.approveGated(flowName, trigger_event_id);
|
|
1655
|
+
if (!result) {
|
|
1656
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1657
|
+
res.end(JSON.stringify({ error: "Gated execution not found" }));
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1661
|
+
res.end(JSON.stringify(result));
|
|
1662
|
+
}
|
|
1663
|
+
catch (err) {
|
|
1664
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1665
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1666
|
+
}
|
|
1667
|
+
});
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
if (url.pathname.match(/^\/api\/flows\/[^/]+\/toggle$/) && req.method === "POST") {
|
|
1671
|
+
if (!flowEngine) {
|
|
1672
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1673
|
+
res.end(JSON.stringify({ error: "Flow engine not initialized" }));
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
const flowName = decodeURIComponent(url.pathname.split("/")[3]);
|
|
1677
|
+
let body = "";
|
|
1678
|
+
req.on("data", chunk => body += chunk);
|
|
1679
|
+
req.on("end", () => {
|
|
1680
|
+
try {
|
|
1681
|
+
const { enabled } = JSON.parse(body || "{}");
|
|
1682
|
+
const result = flowEngine.toggleFlow(flowName, enabled);
|
|
1683
|
+
if (!result) {
|
|
1684
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1685
|
+
res.end(JSON.stringify({ error: "Flow not found" }));
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1689
|
+
res.end(JSON.stringify({ ok: true, flow: flowName, enabled: result.enabled }));
|
|
1690
|
+
}
|
|
1691
|
+
catch (err) {
|
|
1692
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1693
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
// Topology — returns nodes/edges for agent topology visualization
|
|
1699
|
+
if (url.pathname === "/api/v1/topology" && req.method === "GET") {
|
|
1700
|
+
try {
|
|
1701
|
+
const configPath = path.join(projectRoot, ".jfl", "config.json");
|
|
1702
|
+
let registeredServices = [];
|
|
1703
|
+
let workspaceType = "standalone";
|
|
1704
|
+
let workspaceName = "workspace";
|
|
1705
|
+
if (fs.existsSync(configPath)) {
|
|
1706
|
+
try {
|
|
1707
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
1708
|
+
registeredServices = config.registered_services || [];
|
|
1709
|
+
workspaceType = config.type || "standalone";
|
|
1710
|
+
workspaceName = config.name || "workspace";
|
|
1711
|
+
}
|
|
1712
|
+
catch { }
|
|
1713
|
+
}
|
|
1714
|
+
// Read event counts from map-events.jsonl
|
|
1715
|
+
const mapEventsPath = path.join(projectRoot, ".jfl", "map-events.jsonl");
|
|
1716
|
+
const eventCounts = {};
|
|
1717
|
+
const edgeEventCounts = {}; // "source:target:eventType" -> count
|
|
1718
|
+
const recentWindow = 24 * 60 * 60 * 1000; // 24 hours
|
|
1719
|
+
if (fs.existsSync(mapEventsPath)) {
|
|
1720
|
+
try {
|
|
1721
|
+
const lines = fs.readFileSync(mapEventsPath, "utf-8").trim().split("\n");
|
|
1722
|
+
const now = Date.now();
|
|
1723
|
+
for (const line of lines.slice(-1000)) { // Last 1000 events
|
|
1724
|
+
if (!line)
|
|
1725
|
+
continue;
|
|
1726
|
+
try {
|
|
1727
|
+
const evt = JSON.parse(line);
|
|
1728
|
+
const ts = new Date(evt.ts).getTime();
|
|
1729
|
+
if (now - ts > recentWindow)
|
|
1730
|
+
continue;
|
|
1731
|
+
// Count events by source
|
|
1732
|
+
if (evt.source) {
|
|
1733
|
+
const srcId = evt.source.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1734
|
+
eventCounts[srcId] = (eventCounts[srcId] || 0) + 1;
|
|
1735
|
+
}
|
|
1736
|
+
// Count events by type prefix (for edge matching)
|
|
1737
|
+
if (evt.type) {
|
|
1738
|
+
const prefix = evt.type.split(":")[0];
|
|
1739
|
+
eventCounts[prefix] = (eventCounts[prefix] || 0) + 1;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
catch { }
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
catch { }
|
|
1746
|
+
}
|
|
1747
|
+
// System agents (always present in a JFL installation)
|
|
1748
|
+
// Only Peter Parker is a real system agent — others come from .jfl/agents/*.toml configs
|
|
1749
|
+
// Stratus, eval-engine, telemetry-agent are either external infra or RL agents now
|
|
1750
|
+
const systemAgents = [
|
|
1751
|
+
{ id: "peter-parker", label: "Peter Parker", type: "orchestrator", status: "running", produces: ["peter:task-completed", "peter:rollout-request", "peter:experiment-start"], consumes: ["telemetry:insight", "telemetry:metric-alert", "eval:scored", "sentinel:recommendation"] },
|
|
1752
|
+
];
|
|
1753
|
+
const nodes = [
|
|
1754
|
+
...systemAgents.map(a => ({
|
|
1755
|
+
id: a.id,
|
|
1756
|
+
label: a.label,
|
|
1757
|
+
type: a.type,
|
|
1758
|
+
status: a.status,
|
|
1759
|
+
eventCount: eventCounts[a.id] || eventCounts[a.id.split("-")[0]] || 0,
|
|
1760
|
+
produces: a.produces,
|
|
1761
|
+
consumes: a.consumes,
|
|
1762
|
+
})),
|
|
1763
|
+
];
|
|
1764
|
+
// Add registered services as nodes
|
|
1765
|
+
for (const service of registeredServices) {
|
|
1766
|
+
if (!nodes.find(n => n.id === service.name)) {
|
|
1767
|
+
const nodeType = service.type === "agent" ? "agent"
|
|
1768
|
+
: service.type === "eval" ? "eval"
|
|
1769
|
+
: service.type === "gtm" ? "gtm"
|
|
1770
|
+
: "service";
|
|
1771
|
+
nodes.push({
|
|
1772
|
+
id: service.name,
|
|
1773
|
+
label: service.name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
|
|
1774
|
+
type: nodeType,
|
|
1775
|
+
status: service.status === "running" ? "running" : service.status === "idle" ? "idle" : "stopped",
|
|
1776
|
+
eventCount: eventCounts[service.name] || 0,
|
|
1777
|
+
produces: service.context_scope?.produces,
|
|
1778
|
+
consumes: service.context_scope?.consumes,
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
// Add RL agent nodes from .jfl/agents/*.toml configs
|
|
1783
|
+
try {
|
|
1784
|
+
const rlAgentConfigs = loadAllAgentConfigs(projectRoot);
|
|
1785
|
+
const trainingBuffer = new TrainingBuffer(projectRoot);
|
|
1786
|
+
const trainingEntries = trainingBuffer.read();
|
|
1787
|
+
for (const config of rlAgentConfigs) {
|
|
1788
|
+
const nodeId = `rl-agent-${config.name}`;
|
|
1789
|
+
// Skip if node already exists (e.g., matches a registered service name)
|
|
1790
|
+
if (nodes.find(n => n.id === nodeId || n.id === config.name)) {
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
// Check for recent training data (within last 24h)
|
|
1794
|
+
const now = Date.now();
|
|
1795
|
+
const recentWindow = 24 * 60 * 60 * 1000;
|
|
1796
|
+
const recentEntries = trainingEntries.filter(e => {
|
|
1797
|
+
if (e.agent !== config.name)
|
|
1798
|
+
return false;
|
|
1799
|
+
const ts = new Date(e.ts).getTime();
|
|
1800
|
+
return now - ts < recentWindow;
|
|
1801
|
+
});
|
|
1802
|
+
const status = recentEntries.length > 0 ? "running" : "idle";
|
|
1803
|
+
// Convert name to proper label (e.g., "cli-speed" -> "Cli Speed")
|
|
1804
|
+
const label = config.name
|
|
1805
|
+
.split("-")
|
|
1806
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1807
|
+
.join(" ");
|
|
1808
|
+
nodes.push({
|
|
1809
|
+
id: nodeId,
|
|
1810
|
+
label,
|
|
1811
|
+
type: "agent",
|
|
1812
|
+
status,
|
|
1813
|
+
eventCount: recentEntries.length,
|
|
1814
|
+
produces: config.context_scope?.produces,
|
|
1815
|
+
consumes: config.context_scope?.consumes,
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
catch (err) {
|
|
1820
|
+
// Non-fatal: RL agents are optional
|
|
1821
|
+
}
|
|
1822
|
+
// For portfolio mode, fetch child GTM services and their registered services
|
|
1823
|
+
const childHubs = getChildHubs(projectRoot);
|
|
1824
|
+
const hierarchy = {
|
|
1825
|
+
gtms: [],
|
|
1826
|
+
};
|
|
1827
|
+
if (workspaceType === "portfolio" && childHubs.length > 0) {
|
|
1828
|
+
hierarchy.portfolio = workspaceName;
|
|
1829
|
+
for (const child of childHubs) {
|
|
1830
|
+
const gtmServices = [];
|
|
1831
|
+
// Try to read child's config for its registered services
|
|
1832
|
+
const childConfigPath = path.join(child.path, ".jfl", "config.json");
|
|
1833
|
+
if (fs.existsSync(childConfigPath)) {
|
|
1834
|
+
try {
|
|
1835
|
+
const childConfig = JSON.parse(fs.readFileSync(childConfigPath, "utf-8"));
|
|
1836
|
+
const childServices = childConfig.registered_services || [];
|
|
1837
|
+
for (const svc of childServices) {
|
|
1838
|
+
const svcId = `${child.name}/${svc.name}`;
|
|
1839
|
+
gtmServices.push(svc.name);
|
|
1840
|
+
// Add child services as nodes (if not already present)
|
|
1841
|
+
if (!nodes.find(n => n.id === svcId)) {
|
|
1842
|
+
nodes.push({
|
|
1843
|
+
id: svcId,
|
|
1844
|
+
label: `${svc.name}`,
|
|
1845
|
+
type: svc.type === "agent" ? "agent" : "service",
|
|
1846
|
+
status: "idle", // We don't know remote status
|
|
1847
|
+
eventCount: 0,
|
|
1848
|
+
produces: svc.context_scope?.produces,
|
|
1849
|
+
consumes: svc.context_scope?.consumes,
|
|
1850
|
+
parent: child.name,
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
catch { }
|
|
1856
|
+
}
|
|
1857
|
+
hierarchy.gtms.push({
|
|
1858
|
+
name: child.name,
|
|
1859
|
+
port: child.port,
|
|
1860
|
+
services: gtmServices,
|
|
1861
|
+
});
|
|
1862
|
+
// Add GTM node if not present
|
|
1863
|
+
if (!nodes.find(n => n.id === child.name)) {
|
|
1864
|
+
nodes.push({
|
|
1865
|
+
id: child.name,
|
|
1866
|
+
label: child.name.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
|
|
1867
|
+
type: "gtm",
|
|
1868
|
+
status: "running",
|
|
1869
|
+
eventCount: 0,
|
|
1870
|
+
children: gtmServices,
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
// Build edges from produces/consumes relationships
|
|
1876
|
+
const edges = [];
|
|
1877
|
+
let edgeId = 0;
|
|
1878
|
+
// Create edges between services based on scope patterns
|
|
1879
|
+
for (const producer of nodes) {
|
|
1880
|
+
if (!producer.produces)
|
|
1881
|
+
continue;
|
|
1882
|
+
for (const eventPattern of producer.produces) {
|
|
1883
|
+
for (const consumer of nodes) {
|
|
1884
|
+
if (producer.id === consumer.id || !consumer.consumes)
|
|
1885
|
+
continue;
|
|
1886
|
+
for (const consumePattern of consumer.consumes) {
|
|
1887
|
+
// Check if patterns match (simple glob matching)
|
|
1888
|
+
const patternMatches = (prod, cons) => {
|
|
1889
|
+
if (cons.endsWith(":*")) {
|
|
1890
|
+
return prod.startsWith(cons.slice(0, -1));
|
|
1891
|
+
}
|
|
1892
|
+
if (cons === "*")
|
|
1893
|
+
return true;
|
|
1894
|
+
return prod === cons || prod.startsWith(cons.split(":")[0] + ":");
|
|
1895
|
+
};
|
|
1896
|
+
if (patternMatches(eventPattern, consumePattern)) {
|
|
1897
|
+
const existing = edges.find(e => e.source === producer.id && e.target === consumer.id && e.eventType === eventPattern);
|
|
1898
|
+
if (!existing) {
|
|
1899
|
+
const edgeKey = `${producer.id}:${consumer.id}:${eventPattern}`;
|
|
1900
|
+
const prefix = eventPattern.split(":")[0];
|
|
1901
|
+
edges.push({
|
|
1902
|
+
id: `e${++edgeId}`,
|
|
1903
|
+
source: producer.id,
|
|
1904
|
+
target: consumer.id,
|
|
1905
|
+
eventType: eventPattern,
|
|
1906
|
+
category: eventPattern.includes("eval") || eventPattern.includes("scored") ? "success"
|
|
1907
|
+
: eventPattern.includes("peter") || eventPattern.includes("rollout") ? "rl"
|
|
1908
|
+
: "data",
|
|
1909
|
+
recentEvents: edgeEventCounts[edgeKey] || eventCounts[prefix] || 0,
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
// Add known system flow edges that might not be captured by scope
|
|
1918
|
+
const systemEdges = [
|
|
1919
|
+
{ source: "telemetry-agent", target: "peter-parker", eventType: "telemetry:insight", category: "data" },
|
|
1920
|
+
{ source: "peter-parker", target: "eval-engine", eventType: "peter:task-completed", category: "rl" },
|
|
1921
|
+
{ source: "eval-engine", target: "telemetry-agent", eventType: "eval:scored", category: "success" },
|
|
1922
|
+
{ source: "peter-parker", target: "stratus", eventType: "peter:rollout-request", category: "rl" },
|
|
1923
|
+
{ source: "stratus", target: "eval-engine", eventType: "stratus:prediction", category: "success" },
|
|
1924
|
+
];
|
|
1925
|
+
for (const sysEdge of systemEdges) {
|
|
1926
|
+
const exists = edges.find(e => e.source === sysEdge.source && e.target === sysEdge.target);
|
|
1927
|
+
if (!exists && nodes.find(n => n.id === sysEdge.source) && nodes.find(n => n.id === sysEdge.target)) {
|
|
1928
|
+
const prefix = sysEdge.eventType.split(":")[0];
|
|
1929
|
+
edges.push({
|
|
1930
|
+
id: `e${++edgeId}`,
|
|
1931
|
+
...sysEdge,
|
|
1932
|
+
recentEvents: eventCounts[prefix] || 0,
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1937
|
+
res.end(JSON.stringify({
|
|
1938
|
+
nodes,
|
|
1939
|
+
edges,
|
|
1940
|
+
hierarchy: workspaceType === "portfolio" ? hierarchy : undefined,
|
|
1941
|
+
workspaceType,
|
|
1942
|
+
workspaceName,
|
|
1943
|
+
}));
|
|
1944
|
+
}
|
|
1945
|
+
catch (err) {
|
|
1946
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1947
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1948
|
+
}
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
// Autoresearch status — returns current autoresearch run state
|
|
1952
|
+
if (url.pathname === "/api/v1/autoresearch/status" && req.method === "GET") {
|
|
1953
|
+
try {
|
|
1954
|
+
const status = {
|
|
1955
|
+
running: false,
|
|
1956
|
+
currentRound: 0,
|
|
1957
|
+
totalRounds: 0,
|
|
1958
|
+
baselineComposite: null,
|
|
1959
|
+
proposals: [],
|
|
1960
|
+
dimensions: {},
|
|
1961
|
+
history: [],
|
|
1962
|
+
lastUpdate: null,
|
|
1963
|
+
};
|
|
1964
|
+
// Try to read from log files
|
|
1965
|
+
const logPaths = [
|
|
1966
|
+
path.join(projectRoot, ".jfl", "autoresearch-continuous.log"),
|
|
1967
|
+
path.join(projectRoot, ".jfl", "autoresearch-overnight.log"),
|
|
1968
|
+
];
|
|
1969
|
+
let logContent = "";
|
|
1970
|
+
let logPath = "";
|
|
1971
|
+
for (const p of logPaths) {
|
|
1972
|
+
if (fs.existsSync(p)) {
|
|
1973
|
+
const stat = fs.statSync(p);
|
|
1974
|
+
// Use the most recently modified log
|
|
1975
|
+
if (!logPath || stat.mtimeMs > fs.statSync(logPath).mtimeMs) {
|
|
1976
|
+
logPath = p;
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
if (logPath) {
|
|
1981
|
+
logContent = fs.readFileSync(logPath, "utf-8");
|
|
1982
|
+
status.lastUpdate = fs.statSync(logPath).mtime.toISOString();
|
|
1983
|
+
// Parse total rounds from header
|
|
1984
|
+
const roundsMatch = logContent.match(/Autoresearch Mode \((\d+) rounds\)/);
|
|
1985
|
+
if (roundsMatch) {
|
|
1986
|
+
status.totalRounds = parseInt(roundsMatch[1], 10);
|
|
1987
|
+
}
|
|
1988
|
+
// Parse baseline
|
|
1989
|
+
const baselineMatch = logContent.match(/Baseline composite: ([\d.]+)/);
|
|
1990
|
+
if (baselineMatch) {
|
|
1991
|
+
status.baselineComposite = parseFloat(baselineMatch[1]);
|
|
1992
|
+
}
|
|
1993
|
+
// Parse latest round number
|
|
1994
|
+
const roundMatches = [...logContent.matchAll(/── Round (\d+)\/\d+ ──/g)];
|
|
1995
|
+
if (roundMatches.length > 0) {
|
|
1996
|
+
status.currentRound = parseInt(roundMatches[roundMatches.length - 1][1], 10);
|
|
1997
|
+
}
|
|
1998
|
+
// Parse policy head proposals (get the latest set)
|
|
1999
|
+
const proposalBlocks = [...logContent.matchAll(/Policy head re-ranked 3 proposals.*?\n([\s\S]*?)(?=\n\s*Task:|$)/g)];
|
|
2000
|
+
if (proposalBlocks.length > 0) {
|
|
2001
|
+
const latestBlock = proposalBlocks[proposalBlocks.length - 1][1];
|
|
2002
|
+
const proposalMatches = [...latestBlock.matchAll(/#(\d+) \[pred=([-\d.]+)\] ([^\n]+)/g)];
|
|
2003
|
+
status.proposals = proposalMatches.map(m => ({
|
|
2004
|
+
rank: parseInt(m[1], 10),
|
|
2005
|
+
predicted: parseFloat(m[2]),
|
|
2006
|
+
description: m[3].trim(),
|
|
2007
|
+
}));
|
|
2008
|
+
}
|
|
2009
|
+
// Parse dimensions from latest eval
|
|
2010
|
+
const dimMatches = [...logContent.matchAll(/Dimensions: (tests=[\d.]+ tsc=[\d.]+ lint=[\d.]+ telemetry=[\d.]+ newTests=[\d.]+)/g)];
|
|
2011
|
+
if (dimMatches.length > 0) {
|
|
2012
|
+
const latest = dimMatches[dimMatches.length - 1][1];
|
|
2013
|
+
for (const [, key, val] of latest.matchAll(/(tests|tsc|lint|telemetry|newTests)=([\d.]+)/g)) {
|
|
2014
|
+
status.dimensions[key] = parseFloat(val);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
// Parse round results for history
|
|
2018
|
+
const resultMatches = [...logContent.matchAll(/Round (\d+) result: ([\d.]+) \(([-=+][\d.]+)\)\s*\n\s*Tests: (\d+\/\d+)/g)];
|
|
2019
|
+
status.history = resultMatches.map(m => ({
|
|
2020
|
+
round: parseInt(m[1], 10),
|
|
2021
|
+
composite: parseFloat(m[2]),
|
|
2022
|
+
delta: m[3].startsWith("=") ? 0 : parseFloat(m[3]),
|
|
2023
|
+
tests: m[4],
|
|
2024
|
+
}));
|
|
2025
|
+
// Check if running (log modified in last 5 minutes and no completion message)
|
|
2026
|
+
const fiveMinAgo = Date.now() - 5 * 60 * 1000;
|
|
2027
|
+
const logMtime = fs.statSync(logPath).mtimeMs;
|
|
2028
|
+
const hasCompletionMsg = logContent.includes("All rounds complete") || logContent.includes("Autoresearch finished");
|
|
2029
|
+
status.running = logMtime > fiveMinAgo && !hasCompletionMsg && status.currentRound > 0;
|
|
2030
|
+
}
|
|
2031
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2032
|
+
res.end(JSON.stringify(status));
|
|
2033
|
+
}
|
|
2034
|
+
catch (err) {
|
|
2035
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2036
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2037
|
+
}
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
if (url.pathname === "/api/actions/spawn" && req.method === "POST") {
|
|
2041
|
+
let body = "";
|
|
2042
|
+
req.on("data", chunk => body += chunk);
|
|
2043
|
+
req.on("end", () => {
|
|
2044
|
+
try {
|
|
2045
|
+
const { command, args, cwd, event_type, event_data } = JSON.parse(body || "{}");
|
|
2046
|
+
if (!command) {
|
|
2047
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2048
|
+
res.end(JSON.stringify({ error: "command required" }));
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
const env = { ...process.env };
|
|
2052
|
+
delete env.ANTHROPIC_API_KEY;
|
|
2053
|
+
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
2054
|
+
const child = spawn(command, args || [], {
|
|
2055
|
+
cwd: cwd || projectRoot,
|
|
2056
|
+
detached: true,
|
|
2057
|
+
stdio: "ignore",
|
|
2058
|
+
env,
|
|
2059
|
+
});
|
|
2060
|
+
child.unref();
|
|
2061
|
+
if (event_type && eventBus) {
|
|
2062
|
+
eventBus.emit({
|
|
2063
|
+
type: event_type,
|
|
2064
|
+
source: "dashboard:action",
|
|
2065
|
+
data: event_data || { command, args, pid: child.pid },
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2069
|
+
res.end(JSON.stringify({ ok: true, pid: child.pid }));
|
|
2070
|
+
}
|
|
2071
|
+
catch (err) {
|
|
2072
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2073
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
// ── Session Parity API ────────────────────────────────────────────
|
|
2079
|
+
// These endpoints provide runtime-agnostic session lifecycle.
|
|
2080
|
+
// Both Claude Code hooks and Pi extensions call these instead of
|
|
2081
|
+
// duplicating logic. Single source of truth for session init/end.
|
|
2082
|
+
// POST /api/session/init — sync repos, create branch, run doctor
|
|
2083
|
+
//
|
|
2084
|
+
// CONCURRENCY: Multiple Pi sessions can call this simultaneously.
|
|
2085
|
+
// We use a file lock + serialization to prevent branch race conditions.
|
|
2086
|
+
// The Hub creates the branch with `git branch` (no checkout) and returns
|
|
2087
|
+
// the name. The Pi extension does the actual checkout in its own context.
|
|
2088
|
+
if (url.pathname === "/api/session/init" && req.method === "POST") {
|
|
2089
|
+
let body = "";
|
|
2090
|
+
req.on("data", (chunk) => body += chunk);
|
|
2091
|
+
req.on("end", async () => {
|
|
2092
|
+
// Serialize session init calls to prevent git race conditions
|
|
2093
|
+
const lockFile = path.join(projectRoot, ".jfl", ".session-init.lock");
|
|
2094
|
+
const maxWait = 15000;
|
|
2095
|
+
const waitStart = Date.now();
|
|
2096
|
+
// Wait for any concurrent session init to finish
|
|
2097
|
+
while (fs.existsSync(lockFile) && Date.now() - waitStart < maxWait) {
|
|
2098
|
+
try {
|
|
2099
|
+
// Check if lock is stale (older than 30s)
|
|
2100
|
+
const lockStat = fs.statSync(lockFile);
|
|
2101
|
+
if (Date.now() - lockStat.mtimeMs > 30000) {
|
|
2102
|
+
fs.unlinkSync(lockFile);
|
|
2103
|
+
break;
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
catch { }
|
|
2107
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
2108
|
+
}
|
|
2109
|
+
// Acquire lock
|
|
2110
|
+
try {
|
|
2111
|
+
fs.mkdirSync(path.join(projectRoot, ".jfl"), { recursive: true });
|
|
2112
|
+
fs.writeFileSync(lockFile, `${process.pid}:${Date.now()}`);
|
|
2113
|
+
}
|
|
2114
|
+
catch { }
|
|
2115
|
+
try {
|
|
2116
|
+
const { runtime } = JSON.parse(body || "{}");
|
|
2117
|
+
const warnings = [];
|
|
2118
|
+
const scriptDir = path.join(projectRoot, "scripts", "session");
|
|
2119
|
+
// Step 1: Sync repos
|
|
2120
|
+
const syncScript = path.join(scriptDir, "session-sync.sh");
|
|
2121
|
+
let syncOk = true;
|
|
2122
|
+
if (fs.existsSync(syncScript)) {
|
|
2123
|
+
try {
|
|
2124
|
+
execSync(`bash "${syncScript}"`, { cwd: projectRoot, timeout: 60000, stdio: ["pipe", "pipe", "pipe"] });
|
|
2125
|
+
}
|
|
2126
|
+
catch (err) {
|
|
2127
|
+
syncOk = false;
|
|
2128
|
+
warnings.push(`Sync warning: ${(err.message || "").split("\n")[0]}`);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
// Step 2: Doctor check
|
|
2132
|
+
const doctorScript = path.join(scriptDir, "jfl-doctor.sh");
|
|
2133
|
+
let doctorErrors = 0;
|
|
2134
|
+
let doctorWarnings = 0;
|
|
2135
|
+
if (fs.existsSync(doctorScript)) {
|
|
2136
|
+
try {
|
|
2137
|
+
const doctorOut = execSync(`bash "${doctorScript}"`, { cwd: projectRoot, timeout: 30000, stdio: ["pipe", "pipe", "pipe"] }).toString();
|
|
2138
|
+
const em = doctorOut.match(/(\d+) error\(s\)/);
|
|
2139
|
+
const wm = doctorOut.match(/(\d+) warning\(s\)/);
|
|
2140
|
+
doctorErrors = em ? parseInt(em[1]) : 0;
|
|
2141
|
+
doctorWarnings = wm ? parseInt(wm[1]) : 0;
|
|
2142
|
+
if (doctorErrors > 0)
|
|
2143
|
+
warnings.push(`Doctor: ${doctorErrors} errors`);
|
|
2144
|
+
}
|
|
2145
|
+
catch { }
|
|
2146
|
+
}
|
|
2147
|
+
// Step 3: Create session branch (always create a fresh one)
|
|
2148
|
+
let currentBranch = "";
|
|
2149
|
+
try {
|
|
2150
|
+
currentBranch = execSync("git branch --show-current", { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
2151
|
+
}
|
|
2152
|
+
catch { }
|
|
2153
|
+
// Generate session branch name first (needed regardless of path)
|
|
2154
|
+
const user = (() => {
|
|
2155
|
+
try {
|
|
2156
|
+
return execSync("git config user.name", { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"] })
|
|
2157
|
+
.toString().trim().replace(/\s+/g, "-").toLowerCase().replace(/[^a-z0-9-]/g, "").slice(0, 30) || "user";
|
|
2158
|
+
}
|
|
2159
|
+
catch {
|
|
2160
|
+
return "user";
|
|
2161
|
+
}
|
|
2162
|
+
})();
|
|
2163
|
+
const now = new Date();
|
|
2164
|
+
const dateStr = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
2165
|
+
const timeStr = now.toISOString().slice(11, 16).replace(":", "");
|
|
2166
|
+
const randomId = Math.random().toString(16).slice(2, 8);
|
|
2167
|
+
let sessionBranch = `session-${user}-${dateStr}-${timeStr}-${randomId}`;
|
|
2168
|
+
// Determine working branch from config or default to main
|
|
2169
|
+
let workingBranch = "main";
|
|
2170
|
+
const configPath = path.join(projectRoot, ".jfl", "config.json");
|
|
2171
|
+
if (fs.existsSync(configPath)) {
|
|
2172
|
+
try {
|
|
2173
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
2174
|
+
if (cfg.working_branch)
|
|
2175
|
+
workingBranch = cfg.working_branch;
|
|
2176
|
+
}
|
|
2177
|
+
catch { }
|
|
2178
|
+
}
|
|
2179
|
+
// Save any dirty state before branch operations — git add -A catches
|
|
2180
|
+
// both modified and untracked files. Without this, checkout fails on
|
|
2181
|
+
// dirty working trees (the #1 cause of "Could not create session branch").
|
|
2182
|
+
try {
|
|
2183
|
+
const status = execSync("git status --porcelain", { cwd: projectRoot, timeout: 5000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
2184
|
+
if (status) {
|
|
2185
|
+
execSync("git add -A && git commit -m 'auto: session-init save before branch switch' --no-verify", { cwd: projectRoot, timeout: 10000, stdio: ["pipe", "pipe", "pipe"] });
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
catch (commitErr) {
|
|
2189
|
+
// If commit fails (e.g. nothing to commit after add), try stash as fallback
|
|
2190
|
+
try {
|
|
2191
|
+
execSync("git stash push -m 'jfl-session-init-stash' --include-untracked", { cwd: projectRoot, timeout: 10000, stdio: ["pipe", "pipe", "pipe"] });
|
|
2192
|
+
warnings.push("Stashed dirty state (commit failed)");
|
|
2193
|
+
}
|
|
2194
|
+
catch {
|
|
2195
|
+
// Last resort: force checkout will carry dirty files to new branch
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
// If on a stale session branch, merge it first (prevent data loss)
|
|
2199
|
+
if (currentBranch.startsWith("session-")) {
|
|
2200
|
+
try {
|
|
2201
|
+
// Check for unique commits
|
|
2202
|
+
const uniqueCommits = execSync(`git log --oneline "${workingBranch}".."${currentBranch}" 2>/dev/null | wc -l`, { cwd: projectRoot, encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
2203
|
+
if (parseInt(uniqueCommits) > 0) {
|
|
2204
|
+
// Merge stale branch to working branch
|
|
2205
|
+
execSync(`git checkout "${workingBranch}"`, { cwd: projectRoot, timeout: 10000, stdio: ["pipe", "pipe", "pipe"] });
|
|
2206
|
+
try {
|
|
2207
|
+
execSync(`git merge "${currentBranch}" --no-edit -X ours`, { cwd: projectRoot, timeout: 15000, stdio: ["pipe", "pipe", "pipe"] });
|
|
2208
|
+
try {
|
|
2209
|
+
execSync(`git branch -d "${currentBranch}"`, { cwd: projectRoot, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
2210
|
+
}
|
|
2211
|
+
catch { }
|
|
2212
|
+
warnings.push(`Merged stale session ${currentBranch} (${uniqueCommits} commits) → ${workingBranch}`);
|
|
2213
|
+
}
|
|
2214
|
+
catch {
|
|
2215
|
+
try {
|
|
2216
|
+
execSync("git merge --abort", { cwd: projectRoot, timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
2217
|
+
}
|
|
2218
|
+
catch { }
|
|
2219
|
+
warnings.push(`Could not merge stale branch ${currentBranch} — preserved for manual merge`);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
else {
|
|
2223
|
+
execSync(`git checkout "${workingBranch}"`, { cwd: projectRoot, timeout: 10000, stdio: ["pipe", "pipe", "pipe"] });
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
catch {
|
|
2227
|
+
warnings.push(`Could not switch from stale session branch ${currentBranch}`);
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
// Ensure we're on working branch before creating session branch
|
|
2231
|
+
try {
|
|
2232
|
+
const nowBranch = execSync("git branch --show-current", { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
2233
|
+
if (nowBranch !== workingBranch) {
|
|
2234
|
+
execSync(`git checkout "${workingBranch}"`, { cwd: projectRoot, timeout: 10000, stdio: ["pipe", "pipe", "pipe"] });
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
catch { }
|
|
2238
|
+
// Create the new session branch
|
|
2239
|
+
try {
|
|
2240
|
+
execSync(`git checkout -b "${sessionBranch}"`, { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"] });
|
|
2241
|
+
}
|
|
2242
|
+
catch {
|
|
2243
|
+
// If branch creation fails (name collision?), try with longer random
|
|
2244
|
+
const fallbackId = Math.random().toString(16).slice(2, 14);
|
|
2245
|
+
sessionBranch = `session-${user}-${dateStr}-${timeStr}-${fallbackId}`;
|
|
2246
|
+
try {
|
|
2247
|
+
execSync(`git checkout -b "${sessionBranch}"`, { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"] });
|
|
2248
|
+
}
|
|
2249
|
+
catch {
|
|
2250
|
+
sessionBranch = currentBranch || "main";
|
|
2251
|
+
warnings.push("Could not create session branch");
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
// Pop stash if we stashed earlier
|
|
2255
|
+
try {
|
|
2256
|
+
const stashList = execSync("git stash list", { cwd: projectRoot, timeout: 5000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
2257
|
+
if (stashList.includes("jfl-session-init-stash")) {
|
|
2258
|
+
execSync("git stash pop", { cwd: projectRoot, timeout: 10000, stdio: ["pipe", "pipe", "pipe"] });
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
catch { }
|
|
2262
|
+
// Step 4: Save session info
|
|
2263
|
+
const jflDir = path.join(projectRoot, ".jfl");
|
|
2264
|
+
fs.mkdirSync(path.join(jflDir, "logs"), { recursive: true });
|
|
2265
|
+
fs.mkdirSync(path.join(jflDir, "journal"), { recursive: true });
|
|
2266
|
+
fs.writeFileSync(path.join(jflDir, "current-session-branch.txt"), sessionBranch);
|
|
2267
|
+
fs.writeFileSync(path.join(jflDir, "current-worktree.txt"), "direct");
|
|
2268
|
+
// Emit session start event
|
|
2269
|
+
if (eventBus) {
|
|
2270
|
+
eventBus.emit({
|
|
2271
|
+
type: "session:started",
|
|
2272
|
+
source: `hub:${runtime || "unknown"}`,
|
|
2273
|
+
data: { branch: sessionBranch, runtime, warnings },
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2277
|
+
res.end(JSON.stringify({
|
|
2278
|
+
ok: true,
|
|
2279
|
+
branch: sessionBranch,
|
|
2280
|
+
syncOk,
|
|
2281
|
+
doctor: { errors: doctorErrors, warnings: doctorWarnings },
|
|
2282
|
+
warnings,
|
|
2283
|
+
}));
|
|
2284
|
+
}
|
|
2285
|
+
catch (err) {
|
|
2286
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2287
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2288
|
+
}
|
|
2289
|
+
finally {
|
|
2290
|
+
// Release session init lock
|
|
2291
|
+
try {
|
|
2292
|
+
fs.unlinkSync(lockFile);
|
|
2293
|
+
}
|
|
2294
|
+
catch { }
|
|
2295
|
+
}
|
|
2296
|
+
});
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
// POST /api/prompt — get system prompt injection (CLAUDE.md + context)
|
|
2300
|
+
if (url.pathname === "/api/prompt" && req.method === "POST") {
|
|
2301
|
+
let body = "";
|
|
2302
|
+
req.on("data", (chunk) => body += chunk);
|
|
2303
|
+
req.on("end", async () => {
|
|
2304
|
+
try {
|
|
2305
|
+
const { taskType, maxItems } = JSON.parse(body || "{}");
|
|
2306
|
+
const parts = [];
|
|
2307
|
+
// 1. Load CLAUDE.md
|
|
2308
|
+
const claudeMdPath = path.join(projectRoot, "CLAUDE.md");
|
|
2309
|
+
if (fs.existsSync(claudeMdPath)) {
|
|
2310
|
+
const content = fs.readFileSync(claudeMdPath, "utf-8");
|
|
2311
|
+
if (content.length > 50000) {
|
|
2312
|
+
const sections = content.split(/^## /m);
|
|
2313
|
+
const critical = sections.filter((s) => /CRITICAL|Session Sync|Journal Protocol|Immediate Decision|Working Mode|Core Architecture/i.test(s.slice(0, 100)));
|
|
2314
|
+
if (critical.length > 0) {
|
|
2315
|
+
parts.push("# Project Instructions (CLAUDE.md — critical sections)\n");
|
|
2316
|
+
parts.push("## " + critical.join("\n\n## "));
|
|
2317
|
+
}
|
|
2318
|
+
else {
|
|
2319
|
+
parts.push("# Project Instructions (CLAUDE.md — truncated)\n");
|
|
2320
|
+
parts.push(content.slice(0, 30000));
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
else {
|
|
2324
|
+
parts.push("# Project Instructions (CLAUDE.md)\n");
|
|
2325
|
+
parts.push(content);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
// 2. Load recent context
|
|
2329
|
+
try {
|
|
2330
|
+
const contextItems = readJournalEntries(projectRoot, maxItems ?? 20);
|
|
2331
|
+
if (contextItems.length > 0) {
|
|
2332
|
+
parts.push("\n## Recent Project Context\n");
|
|
2333
|
+
parts.push(contextItems.map(item => {
|
|
2334
|
+
const prefix = item.source ? `[${item.source}] ` : "";
|
|
2335
|
+
return `${prefix}${item.content}`;
|
|
2336
|
+
}).join("\n\n"));
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
catch { }
|
|
2340
|
+
// 3. Load knowledge docs summaries
|
|
2341
|
+
const knowledgeDir = path.join(projectRoot, "knowledge");
|
|
2342
|
+
if (fs.existsSync(knowledgeDir)) {
|
|
2343
|
+
const knowledgeDocs = ["VISION.md", "ROADMAP.md", "NARRATIVE.md", "THESIS.md"];
|
|
2344
|
+
const summaries = [];
|
|
2345
|
+
for (const doc of knowledgeDocs) {
|
|
2346
|
+
const docPath = path.join(knowledgeDir, doc);
|
|
2347
|
+
if (fs.existsSync(docPath)) {
|
|
2348
|
+
const content = fs.readFileSync(docPath, "utf-8");
|
|
2349
|
+
if (content.length > 100) {
|
|
2350
|
+
summaries.push(`### ${doc}\n${content.slice(0, 500)}${content.length > 500 ? "\n..." : ""}`);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
if (summaries.length > 0) {
|
|
2355
|
+
parts.push("\n## Knowledge Documents\n");
|
|
2356
|
+
parts.push(summaries.join("\n\n"));
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2360
|
+
res.end(JSON.stringify({
|
|
2361
|
+
prompt: parts.join("\n"),
|
|
2362
|
+
claudeMdSize: fs.existsSync(path.join(projectRoot, "CLAUDE.md"))
|
|
2363
|
+
? fs.statSync(path.join(projectRoot, "CLAUDE.md")).size : 0,
|
|
2364
|
+
taskType: taskType ?? "general",
|
|
2365
|
+
}));
|
|
2366
|
+
}
|
|
2367
|
+
catch (err) {
|
|
2368
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2369
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2370
|
+
}
|
|
2371
|
+
});
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
// POST /api/session/pivot — checkpoint work without ending session
|
|
2375
|
+
if (url.pathname === "/api/session/pivot" && req.method === "POST") {
|
|
2376
|
+
let body = "";
|
|
2377
|
+
req.on("data", (chunk) => body += chunk);
|
|
2378
|
+
req.on("end", async () => {
|
|
2379
|
+
try {
|
|
2380
|
+
const { summary, push } = JSON.parse(body || "{}");
|
|
2381
|
+
const { pivotCommand } = await import("./pivot.js");
|
|
2382
|
+
const result = await pivotCommand({ summary, push, json: false });
|
|
2383
|
+
if (eventBus) {
|
|
2384
|
+
eventBus.emit({
|
|
2385
|
+
type: "session:pivoted",
|
|
2386
|
+
source: "hub:pivot",
|
|
2387
|
+
data: { branch: result.branch, pivotNumber: result.pivotNumber, summary: summary || "", filesChanged: result.filesChanged },
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2391
|
+
res.end(JSON.stringify(result));
|
|
2392
|
+
}
|
|
2393
|
+
catch (err) {
|
|
2394
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2395
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2396
|
+
}
|
|
2397
|
+
});
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
// POST /api/session/end — cleanup session (merge, commit, etc)
|
|
2401
|
+
if (url.pathname === "/api/session/end" && req.method === "POST") {
|
|
2402
|
+
let body = "";
|
|
2403
|
+
req.on("data", (chunk) => body += chunk);
|
|
2404
|
+
req.on("end", async () => {
|
|
2405
|
+
try {
|
|
2406
|
+
const { runtime, skipCleanup } = JSON.parse(body || "{}");
|
|
2407
|
+
// Check journal exists
|
|
2408
|
+
let hasJournal = false;
|
|
2409
|
+
try {
|
|
2410
|
+
const branch = execSync("git branch --show-current", { cwd: projectRoot, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
|
|
2411
|
+
const journalPath = path.join(projectRoot, ".jfl", "journal", `${branch}.jsonl`);
|
|
2412
|
+
hasJournal = fs.existsSync(journalPath) && fs.statSync(journalPath).size > 0;
|
|
2413
|
+
}
|
|
2414
|
+
catch { }
|
|
2415
|
+
let cleanupResult = "skipped";
|
|
2416
|
+
if (!skipCleanup) {
|
|
2417
|
+
const cleanupScript = path.join(projectRoot, "scripts", "session", "session-cleanup.sh");
|
|
2418
|
+
if (fs.existsSync(cleanupScript)) {
|
|
2419
|
+
try {
|
|
2420
|
+
execSync(`bash "${cleanupScript}"`, { cwd: projectRoot, timeout: 60000, stdio: ["pipe", "pipe", "pipe"] });
|
|
2421
|
+
cleanupResult = "ok";
|
|
2422
|
+
}
|
|
2423
|
+
catch (err) {
|
|
2424
|
+
cleanupResult = `error: ${(err.message || "").split("\n")[0]}`;
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
else {
|
|
2428
|
+
cleanupResult = "no cleanup script";
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
// Emit session end event
|
|
2432
|
+
if (eventBus) {
|
|
2433
|
+
eventBus.emit({
|
|
2434
|
+
type: "session:ended",
|
|
2435
|
+
source: `hub:${runtime || "unknown"}`,
|
|
2436
|
+
data: { hasJournal, cleanupResult, runtime },
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2440
|
+
res.end(JSON.stringify({
|
|
2441
|
+
ok: true,
|
|
2442
|
+
hasJournal,
|
|
2443
|
+
cleanupResult,
|
|
2444
|
+
warnings: hasJournal ? [] : ["No journal entry for this session"],
|
|
2445
|
+
}));
|
|
2446
|
+
}
|
|
2447
|
+
catch (err) {
|
|
2448
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2449
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2450
|
+
}
|
|
2451
|
+
});
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
// ── Findings API ──────────────────────────────────────────────────
|
|
2455
|
+
// GET /api/v1/findings — list current findings
|
|
2456
|
+
if (url.pathname === "/api/v1/findings" && req.method === "GET") {
|
|
2457
|
+
try {
|
|
2458
|
+
const engine = new FindingsEngine(projectRoot);
|
|
2459
|
+
const refresh = url.searchParams.get("refresh") === "true";
|
|
2460
|
+
let findings;
|
|
2461
|
+
if (refresh) {
|
|
2462
|
+
findings = await engine.analyze();
|
|
2463
|
+
}
|
|
2464
|
+
else {
|
|
2465
|
+
findings = engine.getFindings();
|
|
2466
|
+
// Auto-analyze if no findings exist
|
|
2467
|
+
if (findings.length === 0) {
|
|
2468
|
+
findings = await engine.analyze();
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
// Filter out dismissed unless ?include_dismissed=true
|
|
2472
|
+
const includeDismissed = url.searchParams.get("include_dismissed") === "true";
|
|
2473
|
+
if (!includeDismissed) {
|
|
2474
|
+
findings = findings.filter(f => !f.dismissed);
|
|
2475
|
+
}
|
|
2476
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2477
|
+
res.end(JSON.stringify({ findings, total: findings.length }));
|
|
2478
|
+
}
|
|
2479
|
+
catch (err) {
|
|
2480
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2481
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2482
|
+
}
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
// POST /api/v1/findings/:id/dismiss — dismiss a finding
|
|
2486
|
+
if (url.pathname.match(/^\/api\/v1\/findings\/[^/]+\/dismiss$/) && req.method === "POST") {
|
|
2487
|
+
try {
|
|
2488
|
+
const findingId = decodeURIComponent(url.pathname.split("/")[4]);
|
|
2489
|
+
const engine = new FindingsEngine(projectRoot);
|
|
2490
|
+
const success = engine.dismissFinding(findingId);
|
|
2491
|
+
if (success) {
|
|
2492
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2493
|
+
res.end(JSON.stringify({ ok: true, dismissed: findingId }));
|
|
2494
|
+
}
|
|
2495
|
+
else {
|
|
2496
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2497
|
+
res.end(JSON.stringify({ error: "Finding not found" }));
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
catch (err) {
|
|
2501
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2502
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2503
|
+
}
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2506
|
+
// POST /api/v1/findings/:id/spawn — spawn an agent from a finding
|
|
2507
|
+
if (url.pathname.match(/^\/api\/v1\/findings\/[^/]+\/spawn$/) && req.method === "POST") {
|
|
2508
|
+
try {
|
|
2509
|
+
const findingId = decodeURIComponent(url.pathname.split("/")[4]);
|
|
2510
|
+
const engine = new FindingsEngine(projectRoot);
|
|
2511
|
+
const findings = engine.getFindings();
|
|
2512
|
+
const finding = findings.find(f => f.id === findingId);
|
|
2513
|
+
if (!finding) {
|
|
2514
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2515
|
+
res.end(JSON.stringify({ error: "Finding not found" }));
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
if (!finding.agent_config) {
|
|
2519
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2520
|
+
res.end(JSON.stringify({ error: "Finding has no agent config" }));
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
// Spawn peter-parker to fix the issue
|
|
2524
|
+
const env = { ...process.env };
|
|
2525
|
+
delete env.ANTHROPIC_API_KEY;
|
|
2526
|
+
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
2527
|
+
const agentConfig = finding.agent_config;
|
|
2528
|
+
const prompt = `Fix this issue: ${finding.title}\n\n${finding.description}\n\nTarget metric: ${agentConfig.metric} >= ${agentConfig.target}\nScope files: ${agentConfig.scope_files.join(", ")}`;
|
|
2529
|
+
const child = spawn("jfl", ["peter", "run", "--prompt", prompt], {
|
|
2530
|
+
cwd: projectRoot,
|
|
2531
|
+
detached: true,
|
|
2532
|
+
stdio: "ignore",
|
|
2533
|
+
env,
|
|
2534
|
+
});
|
|
2535
|
+
child.unref();
|
|
2536
|
+
// Emit event for tracking
|
|
2537
|
+
if (eventBus) {
|
|
2538
|
+
eventBus.emit({
|
|
2539
|
+
type: "findings:agent-spawned",
|
|
2540
|
+
source: "findings-engine",
|
|
2541
|
+
data: {
|
|
2542
|
+
finding_id: findingId,
|
|
2543
|
+
finding_type: finding.type,
|
|
2544
|
+
finding_title: finding.title,
|
|
2545
|
+
pid: child.pid,
|
|
2546
|
+
},
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2550
|
+
res.end(JSON.stringify({
|
|
2551
|
+
ok: true,
|
|
2552
|
+
pid: child.pid,
|
|
2553
|
+
finding_id: findingId,
|
|
2554
|
+
agent_config: agentConfig,
|
|
2555
|
+
}));
|
|
2556
|
+
}
|
|
2557
|
+
catch (err) {
|
|
2558
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2559
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2560
|
+
}
|
|
2561
|
+
return;
|
|
2562
|
+
}
|
|
2563
|
+
// POST /api/v1/findings/analyze — force re-analyze
|
|
2564
|
+
if (url.pathname === "/api/v1/findings/analyze" && req.method === "POST") {
|
|
2565
|
+
try {
|
|
2566
|
+
const engine = new FindingsEngine(projectRoot);
|
|
2567
|
+
const findings = await engine.analyze();
|
|
2568
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2569
|
+
res.end(JSON.stringify({ findings, total: findings.length }));
|
|
2570
|
+
}
|
|
2571
|
+
catch (err) {
|
|
2572
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2573
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2574
|
+
}
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
// POST /api/webhooks/linear — handle Linear webhook events
|
|
2578
|
+
if (url.pathname === "/api/webhooks/linear" && req.method === "POST") {
|
|
2579
|
+
let body = "";
|
|
2580
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
2581
|
+
req.on("end", async () => {
|
|
2582
|
+
try {
|
|
2583
|
+
const payload = JSON.parse(body || "{}");
|
|
2584
|
+
const { handleLinearWebhook } = await import("../lib/linear-webhook.js");
|
|
2585
|
+
const result = handleLinearWebhook(payload, projectRoot);
|
|
2586
|
+
if (result.handled && eventBus) {
|
|
2587
|
+
eventBus.emit({
|
|
2588
|
+
type: "linear:document-sync",
|
|
2589
|
+
source: "linear-webhook",
|
|
2590
|
+
data: {
|
|
2591
|
+
action: payload.action,
|
|
2592
|
+
documentTitle: payload.data?.title,
|
|
2593
|
+
journalFile: result.journalFile,
|
|
2594
|
+
},
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2598
|
+
res.end(JSON.stringify(result));
|
|
2599
|
+
}
|
|
2600
|
+
catch (err) {
|
|
2601
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2602
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
2603
|
+
}
|
|
2604
|
+
});
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
// 404
|
|
2608
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2609
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
2610
|
+
});
|
|
2611
|
+
// WebSocket upgrade for event streaming
|
|
2612
|
+
if (eventBus) {
|
|
2613
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
2614
|
+
server.on("upgrade", (request, socket, head) => {
|
|
2615
|
+
const reqUrl = new URL(request.url || "/", `http://localhost:${port}`);
|
|
2616
|
+
if (reqUrl.pathname !== "/ws/events") {
|
|
2617
|
+
socket.destroy();
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (!validateAuth(request, projectRoot, reqUrl)) {
|
|
2621
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
2622
|
+
socket.destroy();
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
2626
|
+
const patterns = (reqUrl.searchParams.get("patterns") || "*").split(",");
|
|
2627
|
+
const sub = eventBus.subscribe({
|
|
2628
|
+
clientId: `ws-${Date.now()}`,
|
|
2629
|
+
patterns,
|
|
2630
|
+
transport: "websocket",
|
|
2631
|
+
callback: (event) => {
|
|
2632
|
+
if (ws.readyState === ws.OPEN) {
|
|
2633
|
+
ws.send(JSON.stringify(event));
|
|
2634
|
+
}
|
|
2635
|
+
},
|
|
2636
|
+
});
|
|
2637
|
+
ws.on("close", () => {
|
|
2638
|
+
eventBus.unsubscribe(sub.id);
|
|
2639
|
+
});
|
|
2640
|
+
ws.on("error", () => {
|
|
2641
|
+
eventBus.unsubscribe(sub.id);
|
|
2642
|
+
});
|
|
2643
|
+
});
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
return server;
|
|
2647
|
+
}
|
|
2648
|
+
// ============================================================================
|
|
2649
|
+
// Daemon Management
|
|
2650
|
+
// ============================================================================
|
|
2651
|
+
function getPidFile(projectRoot) {
|
|
2652
|
+
return path.join(projectRoot, PID_FILE);
|
|
2653
|
+
}
|
|
2654
|
+
function getLogFile(projectRoot) {
|
|
2655
|
+
const logFile = path.join(projectRoot, LOG_FILE);
|
|
2656
|
+
const logDir = path.dirname(logFile);
|
|
2657
|
+
if (!fs.existsSync(logDir)) {
|
|
2658
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
2659
|
+
}
|
|
2660
|
+
return logFile;
|
|
2661
|
+
}
|
|
2662
|
+
export function isRunning(projectRoot) {
|
|
2663
|
+
const pidFile = getPidFile(projectRoot);
|
|
2664
|
+
if (!fs.existsSync(pidFile)) {
|
|
2665
|
+
return { running: false };
|
|
2666
|
+
}
|
|
2667
|
+
const pid = parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
|
|
2668
|
+
try {
|
|
2669
|
+
process.kill(pid, 0); // Check if process exists
|
|
2670
|
+
return { running: true, pid };
|
|
2671
|
+
}
|
|
2672
|
+
catch {
|
|
2673
|
+
// Process doesn't exist, clean up stale PID file
|
|
2674
|
+
fs.unlinkSync(pidFile);
|
|
2675
|
+
return { running: false };
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
// ============================================================================
|
|
2679
|
+
// Cross-Project Helpers
|
|
2680
|
+
// ============================================================================
|
|
2681
|
+
function getTrackedProjects() {
|
|
2682
|
+
// Read from both config sources (Conf library + XDG config)
|
|
2683
|
+
const confStore = new Conf({ projectName: "jfl" });
|
|
2684
|
+
const confProjects = confStore.get("projects") || [];
|
|
2685
|
+
const xdgProjects = getConfigValue("projects") || [];
|
|
2686
|
+
// Deduplicate
|
|
2687
|
+
const allPaths = [...new Set([...confProjects, ...xdgProjects])];
|
|
2688
|
+
return allPaths
|
|
2689
|
+
.filter(p => fs.existsSync(path.join(p, ".jfl")))
|
|
2690
|
+
.map(p => ({ path: p, port: getProjectPort(p) }));
|
|
2691
|
+
}
|
|
2692
|
+
async function ensureForProject(projectRoot, port, quiet = false) {
|
|
2693
|
+
// Rule: ensure ONLY starts hubs, NEVER kills them.
|
|
2694
|
+
// If something is on the port, leave it alone.
|
|
2695
|
+
const status = isRunning(projectRoot);
|
|
2696
|
+
if (status.running) {
|
|
2697
|
+
return { status: "running", message: `Already running (PID: ${status.pid})` };
|
|
2698
|
+
}
|
|
2699
|
+
const portInUse = await isPortInUse(port);
|
|
2700
|
+
if (portInUse) {
|
|
2701
|
+
// Something is on this port. Don't kill it — could be a healthy hub
|
|
2702
|
+
// whose PID file was lost, or a hub started by another process.
|
|
2703
|
+
return { status: "running", message: `Port ${port} in use (assuming healthy)` };
|
|
2704
|
+
}
|
|
2705
|
+
// Nothing running, nothing on port — safe to start
|
|
2706
|
+
const result = await startDaemon(projectRoot, port);
|
|
2707
|
+
if (result.success) {
|
|
2708
|
+
return { status: "started", message: result.message };
|
|
2709
|
+
}
|
|
2710
|
+
return { status: "failed", message: result.message };
|
|
2711
|
+
}
|
|
2712
|
+
async function diagnoseProject(projectPath, port) {
|
|
2713
|
+
if (!fs.existsSync(projectPath)) {
|
|
2714
|
+
return { path: projectPath, port, status: "STALE", message: "Directory does not exist" };
|
|
2715
|
+
}
|
|
2716
|
+
const pidStatus = isRunning(projectPath);
|
|
2717
|
+
if (!pidStatus.running) {
|
|
2718
|
+
return { path: projectPath, port, status: "DOWN", pid: undefined };
|
|
2719
|
+
}
|
|
2720
|
+
try {
|
|
2721
|
+
const response = await fetch(`http://localhost:${port}/health`, {
|
|
2722
|
+
signal: AbortSignal.timeout(2000)
|
|
2723
|
+
});
|
|
2724
|
+
if (response.ok) {
|
|
2725
|
+
return { path: projectPath, port, status: "OK", pid: pidStatus.pid };
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
catch {
|
|
2729
|
+
// Not responding
|
|
2730
|
+
}
|
|
2731
|
+
return { path: projectPath, port, status: "ZOMBIE", pid: pidStatus.pid, message: "PID exists but not responding" };
|
|
2732
|
+
}
|
|
2733
|
+
async function startDaemon(projectRoot, port) {
|
|
2734
|
+
const status = isRunning(projectRoot);
|
|
2735
|
+
if (status.running) {
|
|
2736
|
+
return { success: true, message: `Context Hub already running (PID: ${status.pid})` };
|
|
2737
|
+
}
|
|
2738
|
+
// Check if port is in use by another process
|
|
2739
|
+
const portInUse = await isPortInUse(port);
|
|
2740
|
+
if (portInUse) {
|
|
2741
|
+
return { success: false, message: `Port ${port} is already in use by another process` };
|
|
2742
|
+
}
|
|
2743
|
+
const logFile = getLogFile(projectRoot);
|
|
2744
|
+
const pidFile = getPidFile(projectRoot);
|
|
2745
|
+
// Generate auth token before starting
|
|
2746
|
+
const token = getOrCreateToken(projectRoot);
|
|
2747
|
+
// Find jfl command (prefer global install)
|
|
2748
|
+
let jflCmd = "jfl";
|
|
2749
|
+
try {
|
|
2750
|
+
// Try to find jfl in PATH
|
|
2751
|
+
execSync("which jfl", { encoding: "utf-8" }).trim();
|
|
2752
|
+
}
|
|
2753
|
+
catch {
|
|
2754
|
+
// Fall back to current process
|
|
2755
|
+
jflCmd = process.argv[1];
|
|
2756
|
+
}
|
|
2757
|
+
// Start as detached process with CONTEXT_HUB_DAEMON=1 so the serve
|
|
2758
|
+
// action knows to ignore SIGTERM during its startup grace period
|
|
2759
|
+
const child = spawn(jflCmd, ["context-hub", "serve", "--port", String(port), "--project-root", projectRoot], {
|
|
2760
|
+
cwd: projectRoot,
|
|
2761
|
+
detached: true,
|
|
2762
|
+
stdio: ["ignore", fs.openSync(logFile, "a"), fs.openSync(logFile, "a")],
|
|
2763
|
+
env: { ...process.env, NODE_ENV: "production", CONTEXT_HUB_DAEMON: "1" }
|
|
2764
|
+
});
|
|
2765
|
+
child.unref();
|
|
2766
|
+
// Wait a moment to ensure process started
|
|
2767
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
2768
|
+
// Write PID file EARLY to avoid race conditions
|
|
2769
|
+
if (child.pid) {
|
|
2770
|
+
// Write PID file immediately
|
|
2771
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
2772
|
+
// Then verify process is still running
|
|
2773
|
+
try {
|
|
2774
|
+
process.kill(child.pid, 0);
|
|
2775
|
+
// Give it a bit more time to be ready
|
|
2776
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
2777
|
+
return { success: true, message: `Started (PID: ${child.pid}). Token: ${token.slice(0, 8)}...` };
|
|
2778
|
+
}
|
|
2779
|
+
catch {
|
|
2780
|
+
// Process died, clean up PID file
|
|
2781
|
+
fs.unlinkSync(pidFile);
|
|
2782
|
+
return { success: false, message: "Process started but immediately exited" };
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
return { success: false, message: "Failed to spawn daemon process" };
|
|
2786
|
+
}
|
|
2787
|
+
async function stopDaemon(projectRoot) {
|
|
2788
|
+
const status = isRunning(projectRoot);
|
|
2789
|
+
if (!status.running || !status.pid) {
|
|
2790
|
+
return { success: true, message: "Context Hub is not running" };
|
|
2791
|
+
}
|
|
2792
|
+
const pidFile = getPidFile(projectRoot);
|
|
2793
|
+
try {
|
|
2794
|
+
// Send SIGTERM first (graceful)
|
|
2795
|
+
process.kill(status.pid, "SIGTERM");
|
|
2796
|
+
// Wait up to 3 seconds for graceful shutdown
|
|
2797
|
+
let attempts = 0;
|
|
2798
|
+
while (attempts < 6) {
|
|
2799
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
2800
|
+
try {
|
|
2801
|
+
process.kill(status.pid, 0); // Check if still running
|
|
2802
|
+
attempts++;
|
|
2803
|
+
}
|
|
2804
|
+
catch {
|
|
2805
|
+
// Process is gone
|
|
2806
|
+
break;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
// If still running after 3 seconds, force kill
|
|
2810
|
+
try {
|
|
2811
|
+
process.kill(status.pid, 0);
|
|
2812
|
+
process.kill(status.pid, "SIGKILL");
|
|
2813
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
2814
|
+
}
|
|
2815
|
+
catch {
|
|
2816
|
+
// Process is gone, that's fine
|
|
2817
|
+
}
|
|
2818
|
+
// Clean up PID file (preserve token for seamless restart)
|
|
2819
|
+
if (fs.existsSync(pidFile)) {
|
|
2820
|
+
fs.unlinkSync(pidFile);
|
|
2821
|
+
}
|
|
2822
|
+
return { success: true, message: "Context Hub stopped" };
|
|
2823
|
+
}
|
|
2824
|
+
catch (err) {
|
|
2825
|
+
return { success: false, message: `Failed to stop daemon: ${err}` };
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
// ============================================================================
|
|
2829
|
+
// Auto-Install Daemon
|
|
2830
|
+
// ============================================================================
|
|
2831
|
+
export async function ensureDaemonInstalled(opts) {
|
|
2832
|
+
const quiet = opts?.quiet ?? false;
|
|
2833
|
+
if (process.platform !== "darwin") {
|
|
2834
|
+
return false;
|
|
2835
|
+
}
|
|
2836
|
+
const plistLabel = "com.jfl.context-hub";
|
|
2837
|
+
const plistDir = path.join(homedir(), "Library", "LaunchAgents");
|
|
2838
|
+
const plistPath = path.join(plistDir, `${plistLabel}.plist`);
|
|
2839
|
+
const logPath = path.join(homedir(), ".config", "jfl", "context-hub-agent.log");
|
|
2840
|
+
// If plist exists, check if loaded
|
|
2841
|
+
if (fs.existsSync(plistPath)) {
|
|
2842
|
+
try {
|
|
2843
|
+
const output = execSync("launchctl list", { encoding: "utf-8" });
|
|
2844
|
+
if (output.includes(plistLabel)) {
|
|
2845
|
+
return true;
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
catch {
|
|
2849
|
+
// launchctl failed, try to reload
|
|
2850
|
+
}
|
|
2851
|
+
// Exists but not loaded — reload it
|
|
2852
|
+
try {
|
|
2853
|
+
execSync(`launchctl load "${plistPath}"`, { stdio: "ignore" });
|
|
2854
|
+
if (!quiet) {
|
|
2855
|
+
console.log(chalk.green(`\n Daemon reloaded.`));
|
|
2856
|
+
console.log(chalk.gray(` Plist: ${plistPath}\n`));
|
|
2857
|
+
}
|
|
2858
|
+
return true;
|
|
2859
|
+
}
|
|
2860
|
+
catch {
|
|
2861
|
+
// Fall through to full install
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
// Full install
|
|
2865
|
+
let jflPath = "";
|
|
2866
|
+
try {
|
|
2867
|
+
jflPath = execSync("which jfl", { encoding: "utf-8" }).trim();
|
|
2868
|
+
}
|
|
2869
|
+
catch {
|
|
2870
|
+
jflPath = process.argv[1] || "jfl";
|
|
2871
|
+
}
|
|
2872
|
+
const logDir = path.dirname(logPath);
|
|
2873
|
+
if (!fs.existsSync(logDir)) {
|
|
2874
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
2875
|
+
}
|
|
2876
|
+
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
2877
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2878
|
+
<plist version="1.0">
|
|
2879
|
+
<dict>
|
|
2880
|
+
<key>Label</key>
|
|
2881
|
+
<string>${plistLabel}</string>
|
|
2882
|
+
<key>ProgramArguments</key>
|
|
2883
|
+
<array>
|
|
2884
|
+
<string>${jflPath}</string>
|
|
2885
|
+
<string>context-hub</string>
|
|
2886
|
+
<string>ensure-all</string>
|
|
2887
|
+
<string>--quiet</string>
|
|
2888
|
+
</array>
|
|
2889
|
+
<key>RunAtLoad</key>
|
|
2890
|
+
<true/>
|
|
2891
|
+
<key>StartInterval</key>
|
|
2892
|
+
<integer>300</integer>
|
|
2893
|
+
<key>ProcessType</key>
|
|
2894
|
+
<string>Background</string>
|
|
2895
|
+
<key>LowPriorityIO</key>
|
|
2896
|
+
<true/>
|
|
2897
|
+
<key>Nice</key>
|
|
2898
|
+
<integer>10</integer>
|
|
2899
|
+
<key>EnvironmentVariables</key>
|
|
2900
|
+
<dict>
|
|
2901
|
+
<key>PATH</key>
|
|
2902
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
2903
|
+
</dict>
|
|
2904
|
+
<key>StandardOutPath</key>
|
|
2905
|
+
<string>${logPath}</string>
|
|
2906
|
+
<key>StandardErrorPath</key>
|
|
2907
|
+
<string>${logPath}</string>
|
|
2908
|
+
</dict>
|
|
2909
|
+
</plist>
|
|
2910
|
+
`;
|
|
2911
|
+
if (!fs.existsSync(plistDir)) {
|
|
2912
|
+
fs.mkdirSync(plistDir, { recursive: true });
|
|
2913
|
+
}
|
|
2914
|
+
// Unload if partially loaded
|
|
2915
|
+
try {
|
|
2916
|
+
execSync(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "ignore" });
|
|
2917
|
+
}
|
|
2918
|
+
catch {
|
|
2919
|
+
// Not loaded, fine
|
|
2920
|
+
}
|
|
2921
|
+
fs.writeFileSync(plistPath, plistContent);
|
|
2922
|
+
try {
|
|
2923
|
+
execSync(`launchctl load "${plistPath}"`);
|
|
2924
|
+
if (!quiet) {
|
|
2925
|
+
console.log(chalk.green(`\n Daemon installed and loaded.`));
|
|
2926
|
+
console.log(chalk.gray(` Plist: ${plistPath}`));
|
|
2927
|
+
console.log(chalk.gray(` Log: ${logPath}`));
|
|
2928
|
+
console.log(chalk.gray(` Runs ensure-all every 5 minutes + on login.\n`));
|
|
2929
|
+
}
|
|
2930
|
+
return true;
|
|
2931
|
+
}
|
|
2932
|
+
catch {
|
|
2933
|
+
if (!quiet) {
|
|
2934
|
+
console.log(chalk.red(`\n Failed to load daemon.\n`));
|
|
2935
|
+
}
|
|
2936
|
+
return false;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
// ============================================================================
|
|
2940
|
+
// CLI Command
|
|
2941
|
+
// ============================================================================
|
|
2942
|
+
export async function contextHubCommand(action, options = {}) {
|
|
2943
|
+
const isGlobal = options.global || false;
|
|
2944
|
+
const projectRoot = options.projectRoot || (isGlobal ? homedir() : process.cwd());
|
|
2945
|
+
const port = options.port || getProjectPort(projectRoot);
|
|
2946
|
+
// Ensure directories exist (skip for actions that don't need local project root)
|
|
2947
|
+
const globalActions = ["ensure-all", "doctor", "install-daemon", "uninstall-daemon"];
|
|
2948
|
+
if (!globalActions.includes(action || "")) {
|
|
2949
|
+
if (isGlobal) {
|
|
2950
|
+
const { JFL_PATHS, ensureJflDirs } = await import("../utils/jfl-paths.js");
|
|
2951
|
+
ensureJflDirs();
|
|
2952
|
+
}
|
|
2953
|
+
else {
|
|
2954
|
+
const jflDir = path.join(projectRoot, ".jfl");
|
|
2955
|
+
if (!fs.existsSync(jflDir)) {
|
|
2956
|
+
fs.mkdirSync(jflDir, { recursive: true });
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
switch (action) {
|
|
2961
|
+
case "start": {
|
|
2962
|
+
const spinner = ora("Starting Context Hub...").start();
|
|
2963
|
+
const result = await startDaemon(projectRoot, port);
|
|
2964
|
+
if (result.success) {
|
|
2965
|
+
// Check if it was already running
|
|
2966
|
+
if (result.message.includes("already running")) {
|
|
2967
|
+
spinner.info(result.message);
|
|
2968
|
+
}
|
|
2969
|
+
else {
|
|
2970
|
+
// Wait for server to be ready
|
|
2971
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
2972
|
+
const status = isRunning(projectRoot);
|
|
2973
|
+
spinner.succeed(`Context Hub started on port ${port} (PID: ${status.pid})`);
|
|
2974
|
+
console.log(chalk.gray(` Token file: .jfl/context-hub.token`));
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
else {
|
|
2978
|
+
spinner.fail(result.message);
|
|
2979
|
+
}
|
|
2980
|
+
break;
|
|
2981
|
+
}
|
|
2982
|
+
case "stop": {
|
|
2983
|
+
const spinner = ora("Stopping Context Hub...").start();
|
|
2984
|
+
const result = await stopDaemon(projectRoot);
|
|
2985
|
+
if (result.success) {
|
|
2986
|
+
spinner.succeed(result.message);
|
|
2987
|
+
}
|
|
2988
|
+
else {
|
|
2989
|
+
spinner.fail(result.message);
|
|
2990
|
+
}
|
|
2991
|
+
break;
|
|
2992
|
+
}
|
|
2993
|
+
case "restart": {
|
|
2994
|
+
await contextHubCommand("stop", options);
|
|
2995
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
2996
|
+
await contextHubCommand("start", options);
|
|
2997
|
+
break;
|
|
2998
|
+
}
|
|
2999
|
+
case "status": {
|
|
3000
|
+
const status = isRunning(projectRoot);
|
|
3001
|
+
if (status.running) {
|
|
3002
|
+
console.log(chalk.green(`\n Context Hub is running`));
|
|
3003
|
+
console.log(chalk.gray(` PID: ${status.pid}`));
|
|
3004
|
+
console.log(chalk.gray(` Port: ${port}`));
|
|
3005
|
+
// Try to get more info from the API
|
|
3006
|
+
try {
|
|
3007
|
+
const response = await fetch(`http://localhost:${port}/api/context/status`);
|
|
3008
|
+
const data = await response.json();
|
|
3009
|
+
console.log(chalk.gray(` Sources: ${Object.entries(data.sources).filter(([, v]) => v).map(([k]) => k).join(", ")}`));
|
|
3010
|
+
console.log(chalk.gray(` Items: ${data.itemCount}`));
|
|
3011
|
+
}
|
|
3012
|
+
catch {
|
|
3013
|
+
// Server might not be responding yet
|
|
3014
|
+
}
|
|
3015
|
+
console.log();
|
|
3016
|
+
}
|
|
3017
|
+
else {
|
|
3018
|
+
console.log(chalk.yellow("\n Context Hub is not running"));
|
|
3019
|
+
console.log(chalk.gray(" Run: jfl context-hub start\n"));
|
|
3020
|
+
}
|
|
3021
|
+
break;
|
|
3022
|
+
}
|
|
3023
|
+
case "ensure": {
|
|
3024
|
+
await ensureForProject(projectRoot, port, true);
|
|
3025
|
+
break;
|
|
3026
|
+
}
|
|
3027
|
+
case "ensure-all": {
|
|
3028
|
+
const tracked = getTrackedProjects();
|
|
3029
|
+
if (tracked.length === 0) {
|
|
3030
|
+
if (!options.quiet) {
|
|
3031
|
+
console.log(chalk.yellow("\n No tracked projects found.\n"));
|
|
3032
|
+
}
|
|
3033
|
+
break;
|
|
3034
|
+
}
|
|
3035
|
+
const results = [];
|
|
3036
|
+
for (const project of tracked) {
|
|
3037
|
+
const name = path.basename(project.path);
|
|
3038
|
+
const result = await ensureForProject(project.path, project.port, true);
|
|
3039
|
+
results.push({ name, result });
|
|
3040
|
+
}
|
|
3041
|
+
if (!options.quiet) {
|
|
3042
|
+
console.log(chalk.bold("\n Context Hub - ensure-all\n"));
|
|
3043
|
+
for (const { name, result } of results) {
|
|
3044
|
+
const icon = result.status === "failed" ? chalk.red("✗") : chalk.green("✓");
|
|
3045
|
+
const label = result.status === "started" ? chalk.cyan("started") :
|
|
3046
|
+
result.status === "running" ? chalk.green("running") :
|
|
3047
|
+
chalk.red("failed");
|
|
3048
|
+
console.log(` ${icon} ${chalk.bold(name)} — ${label}`);
|
|
3049
|
+
}
|
|
3050
|
+
const ok = results.filter(r => r.result.status !== "failed").length;
|
|
3051
|
+
const fail = results.filter(r => r.result.status === "failed").length;
|
|
3052
|
+
console.log(chalk.gray(`\n ${ok} running, ${fail} failed\n`));
|
|
3053
|
+
}
|
|
3054
|
+
break;
|
|
3055
|
+
}
|
|
3056
|
+
case "doctor": {
|
|
3057
|
+
const confStoreDoctor = new Conf({ projectName: "jfl" });
|
|
3058
|
+
const confProjectsDoctor = confStoreDoctor.get("projects") || [];
|
|
3059
|
+
const xdgProjectsDoctor = getConfigValue("projects") || [];
|
|
3060
|
+
const allProjects = [...new Set([...confProjectsDoctor, ...xdgProjectsDoctor])];
|
|
3061
|
+
if (allProjects.length === 0) {
|
|
3062
|
+
console.log(chalk.yellow("\n No tracked projects found.\n"));
|
|
3063
|
+
break;
|
|
3064
|
+
}
|
|
3065
|
+
const cleanMode = process.argv.includes("--clean");
|
|
3066
|
+
if (cleanMode) {
|
|
3067
|
+
const before = allProjects.length;
|
|
3068
|
+
const valid = allProjects.filter(p => fs.existsSync(p));
|
|
3069
|
+
// Write cleaned list back to both stores
|
|
3070
|
+
confStoreDoctor.set("projects", valid.filter(p => confProjectsDoctor.includes(p)));
|
|
3071
|
+
setConfig("projects", valid.filter(p => xdgProjectsDoctor.includes(p)));
|
|
3072
|
+
const removed = before - valid.length;
|
|
3073
|
+
if (removed > 0) {
|
|
3074
|
+
console.log(chalk.green(`\n Removed ${removed} stale project${removed > 1 ? "s" : ""} from tracker.\n`));
|
|
3075
|
+
}
|
|
3076
|
+
else {
|
|
3077
|
+
console.log(chalk.green("\n No stale projects found.\n"));
|
|
3078
|
+
}
|
|
3079
|
+
break;
|
|
3080
|
+
}
|
|
3081
|
+
console.log(chalk.bold("\n Context Hub - doctor\n"));
|
|
3082
|
+
let staleCount = 0;
|
|
3083
|
+
let downCount = 0;
|
|
3084
|
+
let zombieCount = 0;
|
|
3085
|
+
let okCount = 0;
|
|
3086
|
+
for (const projectPath of allProjects) {
|
|
3087
|
+
const projectPort = getProjectPort(projectPath);
|
|
3088
|
+
const result = await diagnoseProject(projectPath, projectPort);
|
|
3089
|
+
const name = path.basename(result.path);
|
|
3090
|
+
switch (result.status) {
|
|
3091
|
+
case "OK":
|
|
3092
|
+
console.log(` ${chalk.green("OK")} ${chalk.bold(name)} — PID ${result.pid}, port ${result.port}`);
|
|
3093
|
+
okCount++;
|
|
3094
|
+
break;
|
|
3095
|
+
case "ZOMBIE":
|
|
3096
|
+
console.log(` ${chalk.red("ZOMBIE")} ${chalk.bold(name)} — PID ${result.pid} not responding on port ${result.port}`);
|
|
3097
|
+
zombieCount++;
|
|
3098
|
+
break;
|
|
3099
|
+
case "DOWN":
|
|
3100
|
+
console.log(` ${chalk.red("DOWN")} ${chalk.bold(name)} — not running (port ${result.port})`);
|
|
3101
|
+
downCount++;
|
|
3102
|
+
break;
|
|
3103
|
+
case "STALE":
|
|
3104
|
+
console.log(` ${chalk.yellow("STALE")} ${chalk.gray(result.path)} — directory missing`);
|
|
3105
|
+
staleCount++;
|
|
3106
|
+
break;
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
console.log();
|
|
3110
|
+
if (downCount > 0 || zombieCount > 0) {
|
|
3111
|
+
console.log(chalk.gray(` Hint: run ${chalk.cyan("jfl context-hub ensure-all")} to start all hubs`));
|
|
3112
|
+
}
|
|
3113
|
+
if (staleCount > 0) {
|
|
3114
|
+
console.log(chalk.gray(` Hint: run ${chalk.cyan("jfl context-hub doctor --clean")} to remove stale entries`));
|
|
3115
|
+
}
|
|
3116
|
+
if (okCount === allProjects.length) {
|
|
3117
|
+
console.log(chalk.green(" All projects healthy."));
|
|
3118
|
+
}
|
|
3119
|
+
console.log();
|
|
3120
|
+
break;
|
|
3121
|
+
}
|
|
3122
|
+
case "install-daemon": {
|
|
3123
|
+
const result = await ensureDaemonInstalled({ quiet: false });
|
|
3124
|
+
if (!result) {
|
|
3125
|
+
console.log(chalk.yellow("\n Daemon install skipped (non-macOS or failed).\n"));
|
|
3126
|
+
}
|
|
3127
|
+
break;
|
|
3128
|
+
}
|
|
3129
|
+
case "uninstall-daemon": {
|
|
3130
|
+
const plistLabel = "com.jfl.context-hub";
|
|
3131
|
+
const plistPath = path.join(homedir(), "Library", "LaunchAgents", `${plistLabel}.plist`);
|
|
3132
|
+
if (!fs.existsSync(plistPath)) {
|
|
3133
|
+
console.log(chalk.yellow("\n Daemon not installed.\n"));
|
|
3134
|
+
break;
|
|
3135
|
+
}
|
|
3136
|
+
try {
|
|
3137
|
+
execSync(`launchctl unload "${plistPath}"`, { stdio: "ignore" });
|
|
3138
|
+
}
|
|
3139
|
+
catch {
|
|
3140
|
+
// Already unloaded
|
|
3141
|
+
}
|
|
3142
|
+
fs.unlinkSync(plistPath);
|
|
3143
|
+
console.log(chalk.green("\n Daemon uninstalled.\n"));
|
|
3144
|
+
break;
|
|
3145
|
+
}
|
|
3146
|
+
case "serve": {
|
|
3147
|
+
// Load .env files for API keys (hub runs as detached process)
|
|
3148
|
+
for (const envFile of [
|
|
3149
|
+
path.join(projectRoot, ".env"),
|
|
3150
|
+
path.join(projectRoot, ".env.local"),
|
|
3151
|
+
path.join(process.env.HOME || "/tmp", ".env"),
|
|
3152
|
+
]) {
|
|
3153
|
+
if (fs.existsSync(envFile)) {
|
|
3154
|
+
const envContent = fs.readFileSync(envFile, "utf-8");
|
|
3155
|
+
for (const line of envContent.split("\n")) {
|
|
3156
|
+
const trimmed = line.trim();
|
|
3157
|
+
if (!trimmed || trimmed.startsWith("#"))
|
|
3158
|
+
continue;
|
|
3159
|
+
const eqIdx = trimmed.indexOf("=");
|
|
3160
|
+
if (eqIdx === -1)
|
|
3161
|
+
continue;
|
|
3162
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
3163
|
+
let val = trimmed.slice(eqIdx + 1).trim();
|
|
3164
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
3165
|
+
val = val.slice(1, -1);
|
|
3166
|
+
}
|
|
3167
|
+
if (!process.env[key]) {
|
|
3168
|
+
process.env[key] = val;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
// Run server in foreground (used by daemon)
|
|
3174
|
+
const serviceEventsPath = path.join(projectRoot, ".jfl", "service-events.jsonl");
|
|
3175
|
+
const mapPersistPath = path.join(projectRoot, ".jfl", "map-events.jsonl");
|
|
3176
|
+
const journalDir = path.join(projectRoot, ".jfl", "journal");
|
|
3177
|
+
const eventBus = new MAPEventBus({
|
|
3178
|
+
maxSize: 1000,
|
|
3179
|
+
persistPath: mapPersistPath,
|
|
3180
|
+
serviceEventsPath,
|
|
3181
|
+
journalDir: fs.existsSync(journalDir) ? journalDir : null,
|
|
3182
|
+
});
|
|
3183
|
+
const flowEngine = new FlowEngine(eventBus, projectRoot);
|
|
3184
|
+
const server = createServer(projectRoot, port, eventBus, flowEngine);
|
|
3185
|
+
let isListening = false;
|
|
3186
|
+
// Cross-service scope impact detection (GTM/portfolio level)
|
|
3187
|
+
// When eval:scored fires with improved=true, detect which other services
|
|
3188
|
+
// are affected and emit scope:impact events for each
|
|
3189
|
+
const configPath = path.join(projectRoot, ".jfl", "config.json");
|
|
3190
|
+
const hubConfig = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf-8")) : {};
|
|
3191
|
+
if (hubConfig.type === "gtm" || hubConfig.type === "portfolio") {
|
|
3192
|
+
eventBus.subscribe({
|
|
3193
|
+
clientId: "scope-detector",
|
|
3194
|
+
patterns: ["eval:scored"],
|
|
3195
|
+
transport: "poll",
|
|
3196
|
+
callback: (event) => {
|
|
3197
|
+
if (event.data?.improved !== "true" && event.data?.improved !== true)
|
|
3198
|
+
return;
|
|
3199
|
+
const serviceName = event.data?.service || event.source || "unknown";
|
|
3200
|
+
const registeredServices = hubConfig.registered_services || [];
|
|
3201
|
+
// Find source service's produces
|
|
3202
|
+
const sourceReg = registeredServices.find((s) => s.name === serviceName);
|
|
3203
|
+
if (!sourceReg?.path)
|
|
3204
|
+
return;
|
|
3205
|
+
try {
|
|
3206
|
+
const svcConfigPath = path.join(sourceReg.path, ".jfl", "config.json");
|
|
3207
|
+
if (!fs.existsSync(svcConfigPath))
|
|
3208
|
+
return;
|
|
3209
|
+
const svcConfig = JSON.parse(fs.readFileSync(svcConfigPath, "utf-8"));
|
|
3210
|
+
const produces = svcConfig.context_scope?.produces || [];
|
|
3211
|
+
if (produces.length === 0)
|
|
3212
|
+
return;
|
|
3213
|
+
// Check each other service for consuming matches
|
|
3214
|
+
for (const otherSvc of registeredServices) {
|
|
3215
|
+
if (otherSvc.name === serviceName || !otherSvc.path)
|
|
3216
|
+
continue;
|
|
3217
|
+
const otherConfigPath = path.join(otherSvc.path, ".jfl", "config.json");
|
|
3218
|
+
if (!fs.existsSync(otherConfigPath))
|
|
3219
|
+
continue;
|
|
3220
|
+
const otherConfig = JSON.parse(fs.readFileSync(otherConfigPath, "utf-8"));
|
|
3221
|
+
const consumes = otherConfig.context_scope?.consumes || [];
|
|
3222
|
+
// Match produces against consumes
|
|
3223
|
+
const matched = [];
|
|
3224
|
+
for (const p of produces) {
|
|
3225
|
+
for (const c of consumes) {
|
|
3226
|
+
if (c === "*" || p === c || (c.endsWith(":*") && p.startsWith(c.slice(0, -1))) || (c.endsWith("*") && p.startsWith(c.slice(0, -1)))) {
|
|
3227
|
+
matched.push(`${p} → ${c}`);
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
if (matched.length > 0) {
|
|
3232
|
+
const ts = new Date().toISOString();
|
|
3233
|
+
console.log(`[${ts}] scope:impact — ${serviceName} → ${otherSvc.name} (${matched.length} patterns)`);
|
|
3234
|
+
eventBus.emit({
|
|
3235
|
+
type: "scope:impact",
|
|
3236
|
+
source: "scope-detector",
|
|
3237
|
+
data: {
|
|
3238
|
+
source_service: serviceName,
|
|
3239
|
+
affected_service: otherSvc.name,
|
|
3240
|
+
affected_service_path: otherSvc.path,
|
|
3241
|
+
scope_patterns: matched,
|
|
3242
|
+
source_pr: event.data?.pr_number || "",
|
|
3243
|
+
change_description: event.data?.branch || "eval improvement",
|
|
3244
|
+
source_delta: event.data?.delta || "0",
|
|
3245
|
+
},
|
|
3246
|
+
});
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
catch (err) {
|
|
3251
|
+
console.error(`[scope-detector] Error checking impact for ${serviceName}:`, err.message);
|
|
3252
|
+
}
|
|
3253
|
+
},
|
|
3254
|
+
});
|
|
3255
|
+
console.log(`[scope-detector] Cross-service impact detection enabled for ${hubConfig.type} hub`);
|
|
3256
|
+
}
|
|
3257
|
+
// When spawned as daemon, ignore SIGTERM during startup grace period.
|
|
3258
|
+
// The parent process (hook runner) may exit and send SIGTERM to the
|
|
3259
|
+
// process group before we're fully detached. After grace period,
|
|
3260
|
+
// re-enable normal shutdown handling.
|
|
3261
|
+
const isDaemon = process.env.CONTEXT_HUB_DAEMON === "1";
|
|
3262
|
+
let startupGrace = isDaemon;
|
|
3263
|
+
if (isDaemon) {
|
|
3264
|
+
setTimeout(() => {
|
|
3265
|
+
startupGrace = false;
|
|
3266
|
+
}, 5000);
|
|
3267
|
+
}
|
|
3268
|
+
// Error handling - keep process alive
|
|
3269
|
+
process.on("uncaughtException", (err) => {
|
|
3270
|
+
console.error(`Uncaught exception: ${err.message}`);
|
|
3271
|
+
console.error(err.stack);
|
|
3272
|
+
telemetry.track({
|
|
3273
|
+
category: 'error',
|
|
3274
|
+
event: 'error:hub_crash',
|
|
3275
|
+
error_type: err.constructor.name,
|
|
3276
|
+
error_code: err.code || undefined,
|
|
3277
|
+
hub_port: port,
|
|
3278
|
+
hub_uptime_s: isListening ? Math.floor((Date.now() - hubStartTime) / 1000) : 0,
|
|
3279
|
+
});
|
|
3280
|
+
});
|
|
3281
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
3282
|
+
console.error(`Unhandled rejection at ${promise}: ${reason}`);
|
|
3283
|
+
// Don't exit - log and continue
|
|
3284
|
+
});
|
|
3285
|
+
server.on("error", (err) => {
|
|
3286
|
+
console.error(`Server error: ${err.message}`);
|
|
3287
|
+
telemetry.track({
|
|
3288
|
+
category: 'error',
|
|
3289
|
+
event: 'error:hub_server',
|
|
3290
|
+
error_type: err.constructor.name,
|
|
3291
|
+
error_code: err.code || undefined,
|
|
3292
|
+
hub_port: port,
|
|
3293
|
+
});
|
|
3294
|
+
if (err.code === "EADDRINUSE") {
|
|
3295
|
+
console.error(`Port ${port} is already in use. Exiting.`);
|
|
3296
|
+
process.exit(1);
|
|
3297
|
+
}
|
|
3298
|
+
});
|
|
3299
|
+
const hubStartTime = Date.now();
|
|
3300
|
+
server.listen(port, async () => {
|
|
3301
|
+
isListening = true;
|
|
3302
|
+
const timestamp = new Date().toISOString();
|
|
3303
|
+
console.log(`[${timestamp}] Context Hub listening on port ${port}`);
|
|
3304
|
+
console.log(`[${timestamp}] PID: ${process.pid}`);
|
|
3305
|
+
telemetry.track({
|
|
3306
|
+
category: 'context_hub',
|
|
3307
|
+
event: 'context_hub:started',
|
|
3308
|
+
hub_port: port,
|
|
3309
|
+
duration_ms: Date.now() - hubStartTime,
|
|
3310
|
+
});
|
|
3311
|
+
// Initialize memory system
|
|
3312
|
+
try {
|
|
3313
|
+
await initializeDatabase();
|
|
3314
|
+
console.log(`[${timestamp}] Memory database initialized`);
|
|
3315
|
+
// Index existing journal entries
|
|
3316
|
+
const stats = await indexJournalEntries();
|
|
3317
|
+
if (stats.added > 0) {
|
|
3318
|
+
console.log(`[${timestamp}] Indexed ${stats.added} new journal entries`);
|
|
3319
|
+
}
|
|
3320
|
+
// Start periodic indexing (every 60 seconds)
|
|
3321
|
+
startPeriodicIndexing(60000);
|
|
3322
|
+
console.log(`[${timestamp}] Periodic memory indexing started`);
|
|
3323
|
+
}
|
|
3324
|
+
catch (err) {
|
|
3325
|
+
console.error(`[${timestamp}] Failed to initialize memory system:`, err.message);
|
|
3326
|
+
// Don't exit - memory is optional
|
|
3327
|
+
}
|
|
3328
|
+
// Start flow engine (with child hub connections for portfolio mode)
|
|
3329
|
+
try {
|
|
3330
|
+
const children = getChildHubs(projectRoot);
|
|
3331
|
+
if (children.length > 0) {
|
|
3332
|
+
flowEngine.setChildren(children);
|
|
3333
|
+
console.log(`[${timestamp}] Portfolio mode: connecting to ${children.length} child hub(s)`);
|
|
3334
|
+
}
|
|
3335
|
+
const flowCount = await flowEngine.start();
|
|
3336
|
+
if (flowCount > 0) {
|
|
3337
|
+
console.log(`[${timestamp}] Flow engine started with ${flowCount} active flow(s)`);
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
catch (err) {
|
|
3341
|
+
console.error(`[${timestamp}] Failed to start flow engine:`, err.message);
|
|
3342
|
+
}
|
|
3343
|
+
// Start telemetry agent (periodic pattern detection)
|
|
3344
|
+
try {
|
|
3345
|
+
const { TelemetryAgent } = await import("../lib/telemetry-agent.js");
|
|
3346
|
+
const telemetryAgent = new TelemetryAgent({
|
|
3347
|
+
projectRoot,
|
|
3348
|
+
intervalMs: 30 * 60 * 1000,
|
|
3349
|
+
emitEvent: (type, data, source) => {
|
|
3350
|
+
eventBus.emit({ type: type, data, source: source || 'telemetry-agent' });
|
|
3351
|
+
},
|
|
3352
|
+
});
|
|
3353
|
+
telemetryAgent.start();
|
|
3354
|
+
server.__telemetryAgent = telemetryAgent;
|
|
3355
|
+
console.log(`[${timestamp}] Telemetry agent started (interval: 30m)`);
|
|
3356
|
+
}
|
|
3357
|
+
catch (err) {
|
|
3358
|
+
console.error(`[${timestamp}] Failed to start telemetry agent:`, err.message);
|
|
3359
|
+
}
|
|
3360
|
+
console.log(`[${timestamp}] MAP event bus initialized (buffer: 1000, subscribers: ${eventBus.getSubscriberCount()})`);
|
|
3361
|
+
console.log(`[${timestamp}] Ready to serve requests`);
|
|
3362
|
+
});
|
|
3363
|
+
// Handle shutdown gracefully
|
|
3364
|
+
const shutdown = (signal) => {
|
|
3365
|
+
// During startup grace period (daemon mode), ignore SIGTERM from
|
|
3366
|
+
// parent process cleanup. This prevents the hook runner from
|
|
3367
|
+
// killing the hub before it's fully detached.
|
|
3368
|
+
if (startupGrace && signal === "SIGTERM") {
|
|
3369
|
+
const ts = new Date().toISOString();
|
|
3370
|
+
console.log(`[${ts}] Ignoring ${signal} during startup grace period (PID: ${process.pid}, Parent: ${process.ppid})`);
|
|
3371
|
+
return;
|
|
3372
|
+
}
|
|
3373
|
+
if (!isListening) {
|
|
3374
|
+
// Server never started, just exit
|
|
3375
|
+
process.exit(0);
|
|
3376
|
+
return;
|
|
3377
|
+
}
|
|
3378
|
+
// Log who sent the signal for debugging
|
|
3379
|
+
const timestamp = new Date().toISOString();
|
|
3380
|
+
console.log(`[${timestamp}] Received ${signal}`);
|
|
3381
|
+
console.log(`[${timestamp}] PID: ${process.pid}, Parent PID: ${process.ppid}`);
|
|
3382
|
+
console.log(`[${timestamp}] Shutting down...`);
|
|
3383
|
+
telemetry.track({
|
|
3384
|
+
category: 'context_hub',
|
|
3385
|
+
event: 'context_hub:stopped',
|
|
3386
|
+
hub_port: port,
|
|
3387
|
+
hub_uptime_s: Math.floor((Date.now() - hubStartTime) / 1000),
|
|
3388
|
+
});
|
|
3389
|
+
server.close(() => {
|
|
3390
|
+
eventBus.destroy();
|
|
3391
|
+
console.log(`[${new Date().toISOString()}] Server closed`);
|
|
3392
|
+
process.exit(0);
|
|
3393
|
+
});
|
|
3394
|
+
// Force exit after 5s if server doesn't close
|
|
3395
|
+
setTimeout(() => {
|
|
3396
|
+
console.log(`[${new Date().toISOString()}] Force exit after timeout`);
|
|
3397
|
+
process.exit(1);
|
|
3398
|
+
}, 5000);
|
|
3399
|
+
};
|
|
3400
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
3401
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
3402
|
+
// Keep process alive with heartbeat
|
|
3403
|
+
const heartbeat = setInterval(() => {
|
|
3404
|
+
// Heartbeat - ensures event loop stays active
|
|
3405
|
+
// Also log periodically so we know it's alive
|
|
3406
|
+
if (isListening && Date.now() % 300000 < 60000) { // Every 5 minutes
|
|
3407
|
+
const timestamp = new Date().toISOString();
|
|
3408
|
+
console.log(`[${timestamp}] Heartbeat: Still running (PID: ${process.pid})`);
|
|
3409
|
+
}
|
|
3410
|
+
}, 60000);
|
|
3411
|
+
// Cleanup heartbeat on exit
|
|
3412
|
+
process.on("exit", () => {
|
|
3413
|
+
clearInterval(heartbeat);
|
|
3414
|
+
});
|
|
3415
|
+
break;
|
|
3416
|
+
}
|
|
3417
|
+
case "query": {
|
|
3418
|
+
// Quick query for testing
|
|
3419
|
+
const context = getUnifiedContext(projectRoot);
|
|
3420
|
+
console.log(chalk.bold("\n Context Hub Query\n"));
|
|
3421
|
+
console.log(chalk.gray(` Sources: ${Object.entries(context.sources).filter(([, v]) => v).map(([k]) => k).join(", ")}`));
|
|
3422
|
+
console.log(chalk.gray(` Items: ${context.items.length}\n`));
|
|
3423
|
+
for (const item of context.items.slice(0, 10)) {
|
|
3424
|
+
console.log(chalk.cyan(` [${item.source}] ${item.title}`));
|
|
3425
|
+
console.log(chalk.gray(` ${item.content.slice(0, 100)}${item.content.length > 100 ? "..." : ""}`));
|
|
3426
|
+
}
|
|
3427
|
+
console.log();
|
|
3428
|
+
break;
|
|
3429
|
+
}
|
|
3430
|
+
case "logs": {
|
|
3431
|
+
// Launch TUI log viewer
|
|
3432
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
3433
|
+
const __dirname = path.dirname(__filename);
|
|
3434
|
+
const logViewer = spawn(process.execPath, [
|
|
3435
|
+
path.join(__dirname, "../ui/context-hub-logs.js")
|
|
3436
|
+
], {
|
|
3437
|
+
stdio: "inherit",
|
|
3438
|
+
cwd: projectRoot
|
|
3439
|
+
});
|
|
3440
|
+
logViewer.on("exit", (code) => {
|
|
3441
|
+
process.exit(code || 0);
|
|
3442
|
+
});
|
|
3443
|
+
break;
|
|
3444
|
+
}
|
|
3445
|
+
case "dashboard": {
|
|
3446
|
+
const token = getOrCreateToken(projectRoot);
|
|
3447
|
+
const dashUrl = `http://localhost:${port}/dashboard?token=${token}`;
|
|
3448
|
+
try {
|
|
3449
|
+
execSync(`open "${dashUrl}"`);
|
|
3450
|
+
}
|
|
3451
|
+
catch {
|
|
3452
|
+
// open not available — print URL instead
|
|
3453
|
+
}
|
|
3454
|
+
console.log(chalk.gray(` Opening ${dashUrl}`));
|
|
3455
|
+
break;
|
|
3456
|
+
}
|
|
3457
|
+
case "clear-logs": {
|
|
3458
|
+
// Clear both global and local log files
|
|
3459
|
+
const { JFL_FILES } = await import("../utils/jfl-paths.js");
|
|
3460
|
+
const globalLogFile = path.join(JFL_FILES.servicesLogs, "context-hub.log");
|
|
3461
|
+
const localLogFile = getLogFile(projectRoot);
|
|
3462
|
+
let cleared = 0;
|
|
3463
|
+
if (fs.existsSync(globalLogFile)) {
|
|
3464
|
+
fs.writeFileSync(globalLogFile, '');
|
|
3465
|
+
console.log(chalk.green('✓ Global Context Hub logs cleared'));
|
|
3466
|
+
console.log(chalk.gray(` File: ${globalLogFile}`));
|
|
3467
|
+
cleared++;
|
|
3468
|
+
}
|
|
3469
|
+
if (fs.existsSync(localLogFile) && localLogFile !== globalLogFile) {
|
|
3470
|
+
fs.writeFileSync(localLogFile, '');
|
|
3471
|
+
console.log(chalk.green('✓ Local Context Hub logs cleared'));
|
|
3472
|
+
console.log(chalk.gray(` File: ${localLogFile}`));
|
|
3473
|
+
cleared++;
|
|
3474
|
+
}
|
|
3475
|
+
if (cleared === 0) {
|
|
3476
|
+
console.log(chalk.yellow('No log files found'));
|
|
3477
|
+
}
|
|
3478
|
+
break;
|
|
3479
|
+
}
|
|
3480
|
+
default: {
|
|
3481
|
+
console.log(chalk.bold("\n Context Hub - Unified context for AI agents\n"));
|
|
3482
|
+
console.log(chalk.gray(" Commands:"));
|
|
3483
|
+
console.log(" jfl context-hub start Start the daemon");
|
|
3484
|
+
console.log(" jfl context-hub stop Stop the daemon");
|
|
3485
|
+
console.log(" jfl context-hub restart Restart the daemon");
|
|
3486
|
+
console.log(" jfl context-hub status Check if running");
|
|
3487
|
+
console.log(" jfl context-hub ensure Start if not running (for hooks)");
|
|
3488
|
+
console.log(" jfl context-hub ensure-all Ensure all tracked projects are running");
|
|
3489
|
+
console.log(" jfl context-hub doctor Diagnose all tracked projects");
|
|
3490
|
+
console.log(" jfl context-hub doctor --clean Remove stale project entries");
|
|
3491
|
+
console.log(" jfl context-hub dashboard Open web dashboard in browser");
|
|
3492
|
+
console.log(" jfl context-hub install-daemon Install macOS launchd keepalive");
|
|
3493
|
+
console.log(" jfl context-hub uninstall-daemon Remove macOS launchd keepalive");
|
|
3494
|
+
console.log(" jfl context-hub logs Show real-time logs (TUI)");
|
|
3495
|
+
console.log(" jfl context-hub clear-logs Clear log file");
|
|
3496
|
+
console.log(" jfl context-hub query Quick context query");
|
|
3497
|
+
console.log();
|
|
3498
|
+
console.log(chalk.gray(" Options:"));
|
|
3499
|
+
console.log(" --port <port> Port to run on (default: per-project)");
|
|
3500
|
+
console.log(" --quiet Suppress output (for daemon use)");
|
|
3501
|
+
console.log();
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
//# sourceMappingURL=context-hub.js.map
|