@loompad/cli 0.1.0 → 1.1.1
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/ARCHITECTURE.md +96 -11
- package/CHANGELOG.md +1042 -0
- package/README.md +1473 -44
- package/THIRD_PARTY_NOTICES.md +275 -0
- package/dist/adapters/antigravity-cli.d.ts +62 -0
- package/dist/adapters/antigravity-cli.js +258 -0
- package/dist/adapters/antigravity-cli.js.map +1 -0
- package/dist/adapters/base.d.ts +61 -9
- package/dist/adapters/base.js +266 -15
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/bridges/antigravity.d.ts +9 -12
- package/dist/adapters/bridges/antigravity.js +9 -12
- package/dist/adapters/bridges/antigravity.js.map +1 -1
- package/dist/adapters/echo.d.ts +1 -0
- package/dist/adapters/echo.js +59 -3
- package/dist/adapters/echo.js.map +1 -1
- package/dist/adapters/grok.d.ts +7 -0
- package/dist/adapters/grok.js +20 -10
- package/dist/adapters/grok.js.map +1 -1
- package/dist/adapters/index.d.ts +5 -1
- package/dist/adapters/index.js +53 -7
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/model.d.ts +137 -0
- package/dist/adapters/model.js +527 -0
- package/dist/adapters/model.js.map +1 -0
- package/dist/adapters/opencode.d.ts +101 -1
- package/dist/adapters/opencode.js +874 -98
- package/dist/adapters/opencode.js.map +1 -1
- package/dist/cli/doctor.d.ts +32 -0
- package/dist/cli/doctor.js +89 -4
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.js +2651 -87
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/tui-model.js +3 -1
- package/dist/cli/tui-model.js.map +1 -1
- package/dist/cli/tui.js +44 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/cli/ui.d.ts +2 -0
- package/dist/cli/ui.js +13 -0
- package/dist/cli/ui.js.map +1 -1
- package/dist/core/ades.d.ts +32 -16
- package/dist/core/ades.js +57 -20
- package/dist/core/ades.js.map +1 -1
- package/dist/core/agent-state.d.ts +14 -0
- package/dist/core/agent-state.js +32 -0
- package/dist/core/agent-state.js.map +1 -0
- package/dist/core/approvals.d.ts +33 -0
- package/dist/core/approvals.js +29 -0
- package/dist/core/approvals.js.map +1 -0
- package/dist/core/backup.d.ts +19 -0
- package/dist/core/backup.js +86 -0
- package/dist/core/backup.js.map +1 -0
- package/dist/core/baton.d.ts +2 -2
- package/dist/core/baton.js.map +1 -1
- package/dist/core/brain-extract.d.ts +17 -0
- package/dist/core/brain-extract.js +28 -2
- package/dist/core/brain-extract.js.map +1 -1
- package/dist/core/brain-index.d.ts +71 -0
- package/dist/core/brain-index.js +204 -5
- package/dist/core/brain-index.js.map +1 -1
- package/dist/core/brain.d.ts +58 -2
- package/dist/core/brain.js +79 -1
- package/dist/core/brain.js.map +1 -1
- package/dist/core/chat-import.d.ts +71 -0
- package/dist/core/chat-import.js +419 -0
- package/dist/core/chat-import.js.map +1 -0
- package/dist/core/checkpoint.d.ts +107 -0
- package/dist/core/checkpoint.js +259 -0
- package/dist/core/checkpoint.js.map +1 -0
- package/dist/core/claude-cli.js +1 -1
- package/dist/core/claude-cli.js.map +1 -1
- package/dist/core/continuity/artifacts.d.ts +14 -0
- package/dist/core/continuity/artifacts.js +118 -0
- package/dist/core/continuity/artifacts.js.map +1 -0
- package/dist/core/continuity/capabilities.d.ts +65 -0
- package/dist/core/continuity/capabilities.js +129 -0
- package/dist/core/continuity/capabilities.js.map +1 -0
- package/dist/core/continuity/contracts.d.ts +352 -0
- package/dist/core/continuity/contracts.js +117 -0
- package/dist/core/continuity/contracts.js.map +1 -0
- package/dist/core/continuity/engine.d.ts +55 -0
- package/dist/core/continuity/engine.js +504 -0
- package/dist/core/continuity/engine.js.map +1 -0
- package/dist/core/continuity/store.d.ts +73 -0
- package/dist/core/continuity/store.js +438 -0
- package/dist/core/continuity/store.js.map +1 -0
- package/dist/core/conversations.d.ts +19 -0
- package/dist/core/conversations.js +87 -0
- package/dist/core/conversations.js.map +1 -0
- package/dist/core/crew-coordination.d.ts +6 -0
- package/dist/core/crew-coordination.js +24 -0
- package/dist/core/crew-coordination.js.map +1 -0
- package/dist/core/crew-policy.d.ts +18 -0
- package/dist/core/crew-policy.js +40 -0
- package/dist/core/crew-policy.js.map +1 -0
- package/dist/core/crew-protocol.d.ts +64 -0
- package/dist/core/crew-protocol.js +146 -0
- package/dist/core/crew-protocol.js.map +1 -0
- package/dist/core/crew-requests.d.ts +14 -0
- package/dist/core/crew-requests.js +22 -0
- package/dist/core/crew-requests.js.map +1 -0
- package/dist/core/crew.d.ts +395 -0
- package/dist/core/crew.js +1857 -0
- package/dist/core/crew.js.map +1 -0
- package/dist/core/digest.d.ts +40 -0
- package/dist/core/digest.js +94 -0
- package/dist/core/digest.js.map +1 -0
- package/dist/core/distill.js +3 -5
- package/dist/core/distill.js.map +1 -1
- package/dist/core/eventlog.d.ts +27 -25
- package/dist/core/eventlog.js +97 -150
- package/dist/core/eventlog.js.map +1 -1
- package/dist/core/events/contracts.d.ts +51 -0
- package/dist/core/events/contracts.js +2 -0
- package/dist/core/events/contracts.js.map +1 -0
- package/dist/core/events/jsonl-store.d.ts +23 -0
- package/dist/core/events/jsonl-store.js +90 -0
- package/dist/core/events/jsonl-store.js.map +1 -0
- package/dist/core/events/sqlite-store.d.ts +37 -0
- package/dist/core/events/sqlite-store.js +215 -0
- package/dist/core/events/sqlite-store.js.map +1 -0
- package/dist/core/forecast.d.ts +44 -0
- package/dist/core/forecast.js +46 -0
- package/dist/core/forecast.js.map +1 -0
- package/dist/core/free-pool.d.ts +65 -0
- package/dist/core/free-pool.js +171 -0
- package/dist/core/free-pool.js.map +1 -0
- package/dist/core/git.d.ts +30 -0
- package/dist/core/git.js +50 -2
- package/dist/core/git.js.map +1 -1
- package/dist/core/github-events.d.ts +53 -0
- package/dist/core/github-events.js +227 -0
- package/dist/core/github-events.js.map +1 -0
- package/dist/core/goal-lanes.d.ts +53 -0
- package/dist/core/goal-lanes.js +99 -0
- package/dist/core/goal-lanes.js.map +1 -0
- package/dist/core/hosted.d.ts +27 -0
- package/dist/core/hosted.js +50 -0
- package/dist/core/hosted.js.map +1 -0
- package/dist/core/http-server.d.ts +5 -0
- package/dist/core/http-server.js +8 -0
- package/dist/core/http-server.js.map +1 -0
- package/dist/core/invite-link.d.ts +31 -0
- package/dist/core/invite-link.js +45 -0
- package/dist/core/invite-link.js.map +1 -0
- package/dist/core/loops.d.ts +37 -0
- package/dist/core/loops.js +81 -0
- package/dist/core/loops.js.map +1 -0
- package/dist/core/mcp-catalog.d.ts +182 -0
- package/dist/core/mcp-catalog.js +431 -0
- package/dist/core/mcp-catalog.js.map +1 -0
- package/dist/core/mcp.d.ts +87 -0
- package/dist/core/mcp.js +181 -0
- package/dist/core/mcp.js.map +1 -0
- package/dist/core/memory.d.ts +1 -0
- package/dist/core/memory.js +5 -2
- package/dist/core/memory.js.map +1 -1
- package/dist/core/model-tools.d.ts +97 -0
- package/dist/core/model-tools.js +388 -0
- package/dist/core/model-tools.js.map +1 -0
- package/dist/core/notify.js +19 -1
- package/dist/core/notify.js.map +1 -1
- package/dist/core/orchestra.d.ts +635 -0
- package/dist/core/orchestra.js +2120 -0
- package/dist/core/orchestra.js.map +1 -0
- package/dist/core/permissions.d.ts +56 -0
- package/dist/core/permissions.js +122 -0
- package/dist/core/permissions.js.map +1 -0
- package/dist/core/preview-proxy.d.ts +70 -0
- package/dist/core/preview-proxy.js +563 -0
- package/dist/core/preview-proxy.js.map +1 -0
- package/dist/core/preview-shot.d.ts +46 -0
- package/dist/core/preview-shot.js +109 -0
- package/dist/core/preview-shot.js.map +1 -0
- package/dist/core/process.d.ts +36 -0
- package/dist/core/process.js +168 -0
- package/dist/core/process.js.map +1 -0
- package/dist/core/prompt-queue.d.ts +136 -0
- package/dist/core/prompt-queue.js +269 -0
- package/dist/core/prompt-queue.js.map +1 -0
- package/dist/core/prompts.d.ts +53 -0
- package/dist/core/prompts.js +127 -0
- package/dist/core/prompts.js.map +1 -0
- package/dist/core/proof.d.ts +82 -0
- package/dist/core/proof.js +216 -0
- package/dist/core/proof.js.map +1 -0
- package/dist/core/provider-text.d.ts +23 -0
- package/dist/core/provider-text.js +66 -0
- package/dist/core/provider-text.js.map +1 -0
- package/dist/core/providers.d.ts +152 -0
- package/dist/core/providers.js +343 -0
- package/dist/core/providers.js.map +1 -0
- package/dist/core/quota.d.ts +33 -0
- package/dist/core/quota.js +74 -0
- package/dist/core/quota.js.map +1 -0
- package/dist/core/recipes.d.ts +50 -0
- package/dist/core/recipes.js +99 -0
- package/dist/core/recipes.js.map +1 -0
- package/dist/core/registry.d.ts +54 -0
- package/dist/core/registry.js +62 -5
- package/dist/core/registry.js.map +1 -1
- package/dist/core/relay-client.d.ts +67 -0
- package/dist/core/relay-client.js +184 -0
- package/dist/core/relay-client.js.map +1 -0
- package/dist/core/relay-protocol.d.ts +140 -0
- package/dist/core/relay-protocol.js +189 -0
- package/dist/core/relay-protocol.js.map +1 -0
- package/dist/core/replay.d.ts +18 -0
- package/dist/core/replay.js +191 -0
- package/dist/core/replay.js.map +1 -0
- package/dist/core/review-triage.d.ts +41 -0
- package/dist/core/review-triage.js +124 -0
- package/dist/core/review-triage.js.map +1 -0
- package/dist/core/router.js +2 -2
- package/dist/core/router.js.map +1 -1
- package/dist/core/routes.d.ts +39 -3
- package/dist/core/routes.js +137 -4
- package/dist/core/routes.js.map +1 -1
- package/dist/core/run-command.d.ts +14 -0
- package/dist/core/run-command.js +45 -0
- package/dist/core/run-command.js.map +1 -0
- package/dist/core/runner-setup.d.ts +36 -0
- package/dist/core/runner-setup.js +114 -0
- package/dist/core/runner-setup.js.map +1 -0
- package/dist/core/search.js +0 -0
- package/dist/core/search.js.map +1 -1
- package/dist/core/semantic.d.ts +92 -0
- package/dist/core/semantic.js +227 -0
- package/dist/core/semantic.js.map +1 -0
- package/dist/core/servers.d.ts +115 -0
- package/dist/core/servers.js +305 -0
- package/dist/core/servers.js.map +1 -0
- package/dist/core/setup.js +64 -6
- package/dist/core/setup.js.map +1 -1
- package/dist/core/shield.d.ts +17 -0
- package/dist/core/shield.js +81 -0
- package/dist/core/shield.js.map +1 -0
- package/dist/core/shift.d.ts +69 -0
- package/dist/core/shift.js +120 -0
- package/dist/core/shift.js.map +1 -0
- package/dist/core/skill-install.d.ts +125 -0
- package/dist/core/skill-install.js +328 -0
- package/dist/core/skill-install.js.map +1 -0
- package/dist/core/skills.d.ts +116 -0
- package/dist/core/skills.js +223 -0
- package/dist/core/skills.js.map +1 -0
- package/dist/core/step-conditions.d.ts +65 -0
- package/dist/core/step-conditions.js +102 -0
- package/dist/core/step-conditions.js.map +1 -0
- package/dist/core/subagents.d.ts +45 -0
- package/dist/core/subagents.js +90 -0
- package/dist/core/subagents.js.map +1 -0
- package/dist/core/team-canon.d.ts +42 -0
- package/dist/core/team-canon.js +121 -0
- package/dist/core/team-canon.js.map +1 -0
- package/dist/core/team-crypto.d.ts +97 -0
- package/dist/core/team-crypto.js +175 -0
- package/dist/core/team-crypto.js.map +1 -0
- package/dist/core/team-hub.d.ts +514 -0
- package/dist/core/team-hub.js +964 -0
- package/dist/core/team-hub.js.map +1 -0
- package/dist/core/team-landing.d.ts +239 -0
- package/dist/core/team-landing.js +504 -0
- package/dist/core/team-landing.js.map +1 -0
- package/dist/core/team-leases.d.ts +44 -0
- package/dist/core/team-leases.js +145 -0
- package/dist/core/team-leases.js.map +1 -0
- package/dist/core/team-memory.d.ts +42 -0
- package/dist/core/team-memory.js +84 -0
- package/dist/core/team-memory.js.map +1 -0
- package/dist/core/team-policy.d.ts +85 -0
- package/dist/core/team-policy.js +191 -0
- package/dist/core/team-policy.js.map +1 -0
- package/dist/core/thread-md.d.ts +6 -0
- package/dist/core/thread-md.js +42 -0
- package/dist/core/thread-md.js.map +1 -0
- package/dist/core/turn-stats.d.ts +61 -0
- package/dist/core/turn-stats.js +124 -0
- package/dist/core/turn-stats.js.map +1 -0
- package/dist/core/updater.d.ts +73 -0
- package/dist/core/updater.js +153 -0
- package/dist/core/updater.js.map +1 -0
- package/dist/core/usage.d.ts +75 -0
- package/dist/core/usage.js +163 -0
- package/dist/core/usage.js.map +1 -0
- package/dist/core/windows-jobs.d.ts +15 -0
- package/dist/core/windows-jobs.js +170 -0
- package/dist/core/windows-jobs.js.map +1 -0
- package/dist/core/worktree-merge.d.ts +57 -0
- package/dist/core/worktree-merge.js +123 -0
- package/dist/core/worktree-merge.js.map +1 -0
- package/dist/core/worktree.d.ts +5 -0
- package/dist/core/worktree.js +45 -5
- package/dist/core/worktree.js.map +1 -1
- package/dist/core/worktrees.d.ts +6 -0
- package/dist/core/worktrees.js +21 -0
- package/dist/core/worktrees.js.map +1 -0
- package/dist/daemon/app-page.d.ts +0 -17
- package/dist/daemon/app-page.js +10 -6346
- package/dist/daemon/app-page.js.map +1 -1
- package/dist/daemon/auth.d.ts +24 -3
- package/dist/daemon/auth.js +66 -7
- package/dist/daemon/auth.js.map +1 -1
- package/dist/daemon/board.d.ts +7 -0
- package/dist/daemon/board.js +12 -1
- package/dist/daemon/board.js.map +1 -1
- package/dist/daemon/brand-icons.d.ts +6 -1
- package/dist/daemon/brand-icons.js +11 -3
- package/dist/daemon/brand-icons.js.map +1 -1
- package/dist/daemon/client.d.ts +665 -1
- package/dist/daemon/client.js +381 -3
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/delivery.d.ts +34 -0
- package/dist/daemon/delivery.js +63 -0
- package/dist/daemon/delivery.js.map +1 -0
- package/dist/daemon/deploys.d.ts +62 -0
- package/dist/daemon/deploys.js +159 -0
- package/dist/daemon/deploys.js.map +1 -0
- package/dist/daemon/landing.d.ts +192 -0
- package/dist/daemon/landing.js +992 -0
- package/dist/daemon/landing.js.map +1 -0
- package/dist/daemon/onboard.d.ts +150 -0
- package/dist/daemon/onboard.js +390 -0
- package/dist/daemon/onboard.js.map +1 -0
- package/dist/daemon/proof.d.ts +70 -0
- package/dist/daemon/proof.js +172 -0
- package/dist/daemon/proof.js.map +1 -0
- package/dist/daemon/push.d.ts +19 -1
- package/dist/daemon/push.js +90 -3
- package/dist/daemon/push.js.map +1 -1
- package/dist/daemon/relay.d.ts +82 -0
- package/dist/daemon/relay.js +351 -0
- package/dist/daemon/relay.js.map +1 -0
- package/dist/daemon/routes/agent-discovery.d.ts +4 -0
- package/dist/daemon/routes/agent-discovery.js +29 -0
- package/dist/daemon/routes/agent-discovery.js.map +1 -0
- package/dist/daemon/routes/agent-settings.d.ts +4 -0
- package/dist/daemon/routes/agent-settings.js +94 -0
- package/dist/daemon/routes/agent-settings.js.map +1 -0
- package/dist/daemon/routes/agents.d.ts +4 -0
- package/dist/daemon/routes/agents.js +105 -0
- package/dist/daemon/routes/agents.js.map +1 -0
- package/dist/daemon/routes/alerts.d.ts +4 -0
- package/dist/daemon/routes/alerts.js +137 -0
- package/dist/daemon/routes/alerts.js.map +1 -0
- package/dist/daemon/routes/approvals.d.ts +4 -0
- package/dist/daemon/routes/approvals.js +40 -0
- package/dist/daemon/routes/approvals.js.map +1 -0
- package/dist/daemon/routes/assets.d.ts +3 -0
- package/dist/daemon/routes/assets.js +64 -0
- package/dist/daemon/routes/assets.js.map +1 -0
- package/dist/daemon/routes/board.d.ts +4 -0
- package/dist/daemon/routes/board.js +147 -0
- package/dist/daemon/routes/board.js.map +1 -0
- package/dist/daemon/routes/brain.d.ts +4 -0
- package/dist/daemon/routes/brain.js +270 -0
- package/dist/daemon/routes/brain.js.map +1 -0
- package/dist/daemon/routes/chats.d.ts +4 -0
- package/dist/daemon/routes/chats.js +249 -0
- package/dist/daemon/routes/chats.js.map +1 -0
- package/dist/daemon/routes/clients.d.ts +4 -0
- package/dist/daemon/routes/clients.js +49 -0
- package/dist/daemon/routes/clients.js.map +1 -0
- package/dist/daemon/routes/cloud.d.ts +4 -0
- package/dist/daemon/routes/cloud.js +46 -0
- package/dist/daemon/routes/cloud.js.map +1 -0
- package/dist/daemon/routes/config.d.ts +4 -0
- package/dist/daemon/routes/config.js +35 -0
- package/dist/daemon/routes/config.js.map +1 -0
- package/dist/daemon/routes/context.d.ts +51 -0
- package/dist/daemon/routes/context.js +2 -0
- package/dist/daemon/routes/context.js.map +1 -0
- package/dist/daemon/routes/crews.d.ts +4 -0
- package/dist/daemon/routes/crews.js +149 -0
- package/dist/daemon/routes/crews.js.map +1 -0
- package/dist/daemon/routes/files.d.ts +3 -0
- package/dist/daemon/routes/files.js +253 -0
- package/dist/daemon/routes/files.js.map +1 -0
- package/dist/daemon/routes/git.d.ts +4 -0
- package/dist/daemon/routes/git.js +96 -0
- package/dist/daemon/routes/git.js.map +1 -0
- package/dist/daemon/routes/history.d.ts +4 -0
- package/dist/daemon/routes/history.js +101 -0
- package/dist/daemon/routes/history.js.map +1 -0
- package/dist/daemon/routes/integrations-project.d.ts +3 -0
- package/dist/daemon/routes/integrations-project.js +103 -0
- package/dist/daemon/routes/integrations-project.js.map +1 -0
- package/dist/daemon/routes/integrations.d.ts +3 -0
- package/dist/daemon/routes/integrations.js +111 -0
- package/dist/daemon/routes/integrations.js.map +1 -0
- package/dist/daemon/routes/logs.d.ts +3 -0
- package/dist/daemon/routes/logs.js +40 -0
- package/dist/daemon/routes/logs.js.map +1 -0
- package/dist/daemon/routes/messages.d.ts +4 -0
- package/dist/daemon/routes/messages.js +60 -0
- package/dist/daemon/routes/messages.js.map +1 -0
- package/dist/daemon/routes/observability.d.ts +4 -0
- package/dist/daemon/routes/observability.js +247 -0
- package/dist/daemon/routes/observability.js.map +1 -0
- package/dist/daemon/routes/onboard.d.ts +12 -0
- package/dist/daemon/routes/onboard.js +67 -0
- package/dist/daemon/routes/onboard.js.map +1 -0
- package/dist/daemon/routes/orchestra.d.ts +4 -0
- package/dist/daemon/routes/orchestra.js +110 -0
- package/dist/daemon/routes/orchestra.js.map +1 -0
- package/dist/daemon/routes/pairing.d.ts +4 -0
- package/dist/daemon/routes/pairing.js +155 -0
- package/dist/daemon/routes/pairing.js.map +1 -0
- package/dist/daemon/routes/preview.d.ts +24 -0
- package/dist/daemon/routes/preview.js +122 -0
- package/dist/daemon/routes/preview.js.map +1 -0
- package/dist/daemon/routes/project-actions.d.ts +4 -0
- package/dist/daemon/routes/project-actions.js +53 -0
- package/dist/daemon/routes/project-actions.js.map +1 -0
- package/dist/daemon/routes/project-team.d.ts +4 -0
- package/dist/daemon/routes/project-team.js +201 -0
- package/dist/daemon/routes/project-team.js.map +1 -0
- package/dist/daemon/routes/projects.d.ts +14 -0
- package/dist/daemon/routes/projects.js +138 -0
- package/dist/daemon/routes/projects.js.map +1 -0
- package/dist/daemon/routes/prompts.d.ts +4 -0
- package/dist/daemon/routes/prompts.js +44 -0
- package/dist/daemon/routes/prompts.js.map +1 -0
- package/dist/daemon/routes/proof.d.ts +4 -0
- package/dist/daemon/routes/proof.js +22 -0
- package/dist/daemon/routes/proof.js.map +1 -0
- package/dist/daemon/routes/providers.d.ts +3 -0
- package/dist/daemon/routes/providers.js +60 -0
- package/dist/daemon/routes/providers.js.map +1 -0
- package/dist/daemon/routes/public.d.ts +4 -0
- package/dist/daemon/routes/public.js +64 -0
- package/dist/daemon/routes/public.js.map +1 -0
- package/dist/daemon/routes/queue.d.ts +4 -0
- package/dist/daemon/routes/queue.js +140 -0
- package/dist/daemon/routes/queue.js.map +1 -0
- package/dist/daemon/routes/routing.d.ts +4 -0
- package/dist/daemon/routes/routing.js +61 -0
- package/dist/daemon/routes/routing.js.map +1 -0
- package/dist/daemon/routes/runtime-handler.d.ts +2 -0
- package/dist/daemon/routes/runtime-handler.js +63 -0
- package/dist/daemon/routes/runtime-handler.js.map +1 -0
- package/dist/daemon/routes/search.d.ts +4 -0
- package/dist/daemon/routes/search.js +17 -0
- package/dist/daemon/routes/search.js.map +1 -0
- package/dist/daemon/routes/servers.d.ts +4 -0
- package/dist/daemon/routes/servers.js +133 -0
- package/dist/daemon/routes/servers.js.map +1 -0
- package/dist/daemon/routes/setup.d.ts +3 -0
- package/dist/daemon/routes/setup.js +50 -0
- package/dist/daemon/routes/setup.js.map +1 -0
- package/dist/daemon/routes/shift.d.ts +4 -0
- package/dist/daemon/routes/shift.js +30 -0
- package/dist/daemon/routes/shift.js.map +1 -0
- package/dist/daemon/routes/specs.d.ts +4 -0
- package/dist/daemon/routes/specs.js +57 -0
- package/dist/daemon/routes/specs.js.map +1 -0
- package/dist/daemon/routes/task-delivery.d.ts +4 -0
- package/dist/daemon/routes/task-delivery.js +38 -0
- package/dist/daemon/routes/task-delivery.js.map +1 -0
- package/dist/daemon/routes/team.d.ts +4 -0
- package/dist/daemon/routes/team.js +193 -0
- package/dist/daemon/routes/team.js.map +1 -0
- package/dist/daemon/routes/terminals.d.ts +4 -0
- package/dist/daemon/routes/terminals.js +87 -0
- package/dist/daemon/routes/terminals.js.map +1 -0
- package/dist/daemon/routes/tools.d.ts +4 -0
- package/dist/daemon/routes/tools.js +180 -0
- package/dist/daemon/routes/tools.js.map +1 -0
- package/dist/daemon/routes/updates.d.ts +4 -0
- package/dist/daemon/routes/updates.js +89 -0
- package/dist/daemon/routes/updates.js.map +1 -0
- package/dist/daemon/routes/voice.d.ts +4 -0
- package/dist/daemon/routes/voice.js +53 -0
- package/dist/daemon/routes/voice.js.map +1 -0
- package/dist/daemon/runner.d.ts +186 -0
- package/dist/daemon/runner.js +612 -0
- package/dist/daemon/runner.js.map +1 -0
- package/dist/daemon/runtime/accounting.d.ts +130 -0
- package/dist/daemon/runtime/accounting.js +303 -0
- package/dist/daemon/runtime/accounting.js.map +1 -0
- package/dist/daemon/runtime/agents.d.ts +16 -0
- package/dist/daemon/runtime/agents.js +71 -0
- package/dist/daemon/runtime/agents.js.map +1 -0
- package/dist/daemon/runtime/briefings.d.ts +134 -0
- package/dist/daemon/runtime/briefings.js +329 -0
- package/dist/daemon/runtime/briefings.js.map +1 -0
- package/dist/daemon/runtime/native-usage.d.ts +21 -0
- package/dist/daemon/runtime/native-usage.js +71 -0
- package/dist/daemon/runtime/native-usage.js.map +1 -0
- package/dist/daemon/runtime/queue.d.ts +94 -0
- package/dist/daemon/runtime/queue.js +222 -0
- package/dist/daemon/runtime/queue.js.map +1 -0
- package/dist/daemon/runtime/relay.d.ts +93 -0
- package/dist/daemon/runtime/relay.js +240 -0
- package/dist/daemon/runtime/relay.js.map +1 -0
- package/dist/daemon/runtime/turns.d.ts +202 -0
- package/dist/daemon/runtime/turns.js +663 -0
- package/dist/daemon/runtime/turns.js.map +1 -0
- package/dist/daemon/runtime-support.d.ts +76 -0
- package/dist/daemon/runtime-support.js +134 -0
- package/dist/daemon/runtime-support.js.map +1 -0
- package/dist/daemon/runtime.d.ts +739 -70
- package/dist/daemon/runtime.js +2751 -470
- package/dist/daemon/runtime.js.map +1 -1
- package/dist/daemon/server.d.ts +82 -55
- package/dist/daemon/server.js +674 -1435
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/shift.d.ts +70 -0
- package/dist/daemon/shift.js +170 -0
- package/dist/daemon/shift.js.map +1 -0
- package/dist/daemon/specs.d.ts +92 -0
- package/dist/daemon/specs.js +285 -0
- package/dist/daemon/specs.js.map +1 -0
- package/dist/daemon/system.d.ts +208 -0
- package/dist/daemon/system.js +557 -0
- package/dist/daemon/system.js.map +1 -0
- package/dist/daemon/tasks.js +1 -1
- package/dist/daemon/team-brain.d.ts +136 -0
- package/dist/daemon/team-brain.js +659 -0
- package/dist/daemon/team-brain.js.map +1 -0
- package/dist/daemon/team-coordinator.d.ts +78 -0
- package/dist/daemon/team-coordinator.js +408 -0
- package/dist/daemon/team-coordinator.js.map +1 -0
- package/dist/daemon/team.d.ts +339 -0
- package/dist/daemon/team.js +1433 -0
- package/dist/daemon/team.js.map +1 -0
- package/dist/daemon/terminals.d.ts +22 -3
- package/dist/daemon/terminals.js +236 -16
- package/dist/daemon/terminals.js.map +1 -1
- package/dist/hub/client.d.ts +120 -0
- package/dist/hub/client.js +228 -0
- package/dist/hub/client.js.map +1 -0
- package/dist/hub/server.d.ts +32 -0
- package/dist/hub/server.js +180 -0
- package/dist/hub/server.js.map +1 -0
- package/dist/hub/supabase-client.d.ts +220 -0
- package/dist/hub/supabase-client.js +843 -0
- package/dist/hub/supabase-client.js.map +1 -0
- package/dist/observability/ask.d.ts +91 -0
- package/dist/observability/ask.js +179 -0
- package/dist/observability/ask.js.map +1 -0
- package/dist/observability/clickhouse.d.ts +33 -0
- package/dist/observability/clickhouse.js +48 -0
- package/dist/observability/clickhouse.js.map +1 -0
- package/dist/observability/decisions.d.ts +176 -0
- package/dist/observability/decisions.js +377 -0
- package/dist/observability/decisions.js.map +1 -0
- package/dist/observability/index.d.ts +89 -0
- package/dist/observability/index.js +348 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/insights.d.ts +151 -0
- package/dist/observability/insights.js +319 -0
- package/dist/observability/insights.js.map +1 -0
- package/dist/observability/logs-query.d.ts +82 -0
- package/dist/observability/logs-query.js +102 -0
- package/dist/observability/logs-query.js.map +1 -0
- package/dist/observability/logs.d.ts +72 -0
- package/dist/observability/logs.js +295 -0
- package/dist/observability/logs.js.map +1 -0
- package/dist/observability/metrics.d.ts +86 -0
- package/dist/observability/metrics.js +303 -0
- package/dist/observability/metrics.js.map +1 -0
- package/dist/observability/otlp.d.ts +127 -0
- package/dist/observability/otlp.js +242 -0
- package/dist/observability/otlp.js.map +1 -0
- package/dist/observability/snapshots.d.ts +43 -0
- package/dist/observability/snapshots.js +129 -0
- package/dist/observability/snapshots.js.map +1 -0
- package/dist/observability/triage.d.ts +49 -0
- package/dist/observability/triage.js +284 -0
- package/dist/observability/triage.js.map +1 -0
- package/dist/providers/adapter.d.ts +50 -0
- package/dist/providers/adapter.js +38 -0
- package/dist/providers/adapter.js.map +1 -0
- package/dist/providers/agent.d.ts +110 -0
- package/dist/providers/agent.js +487 -0
- package/dist/providers/agent.js.map +1 -0
- package/dist/providers/approvals.d.ts +24 -0
- package/dist/providers/approvals.js +78 -0
- package/dist/providers/approvals.js.map +1 -0
- package/dist/providers/attachments.d.ts +20 -0
- package/dist/providers/attachments.js +51 -0
- package/dist/providers/attachments.js.map +1 -0
- package/dist/providers/claude/adapter.d.ts +99 -0
- package/dist/providers/claude/adapter.js +790 -0
- package/dist/providers/claude/adapter.js.map +1 -0
- package/dist/providers/codex/adapter.d.ts +113 -0
- package/dist/providers/codex/adapter.js +881 -0
- package/dist/providers/codex/adapter.js.map +1 -0
- package/dist/providers/codex/instructions.d.ts +11 -0
- package/dist/providers/codex/instructions.js +154 -0
- package/dist/providers/codex/instructions.js.map +1 -0
- package/dist/providers/codex/rpc.d.ts +36 -0
- package/dist/providers/codex/rpc.js +104 -0
- package/dist/providers/codex/rpc.js.map +1 -0
- package/dist/providers/contracts.d.ts +339 -0
- package/dist/providers/contracts.js +19 -0
- package/dist/providers/contracts.js.map +1 -0
- package/dist/providers/directory.d.ts +70 -0
- package/dist/providers/directory.js +95 -0
- package/dist/providers/directory.js.map +1 -0
- package/dist/providers/errors.d.ts +47 -0
- package/dist/providers/errors.js +24 -0
- package/dist/providers/errors.js.map +1 -0
- package/dist/providers/index.d.ts +18 -0
- package/dist/providers/index.js +19 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/ingestion.d.ts +80 -0
- package/dist/providers/ingestion.js +330 -0
- package/dist/providers/ingestion.js.map +1 -0
- package/dist/providers/live.d.ts +22 -0
- package/dist/providers/live.js +56 -0
- package/dist/providers/live.js.map +1 -0
- package/dist/providers/process.d.ts +29 -0
- package/dist/providers/process.js +71 -0
- package/dist/providers/process.js.map +1 -0
- package/dist/providers/reaper.d.ts +33 -0
- package/dist/providers/reaper.js +65 -0
- package/dist/providers/reaper.js.map +1 -0
- package/dist/providers/service.d.ts +87 -0
- package/dist/providers/service.js +256 -0
- package/dist/providers/service.js.map +1 -0
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +1 -1
- package/dist/sdk.js.map +1 -1
- package/dist/types.d.ts +385 -6
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +7 -0
- package/dist/version.js +16 -0
- package/dist/version.js.map +1 -0
- package/dist/web/app.css +4643 -0
- package/dist/web/app.js +22640 -0
- package/dist/web/app.js.map +7 -0
- package/dist/web/shell.html +39 -0
- package/docs/README.md +21 -0
- package/docs/SETUP.md +10 -0
- package/docs/adapters.md +21 -8
- package/docs/brain-continuity.md +186 -0
- package/docs/cloud.md +108 -0
- package/docs/crews.md +62 -0
- package/docs/design-system.md +31 -2
- package/docs/integration-notes.md +128 -20
- package/docs/loompad.md +105 -0
- package/docs/observability.md +220 -0
- package/docs/play-store.md +123 -0
- package/docs/proposals/agent-teams.md +249 -0
- package/docs/proposals/brain-continuity-implementation.md +468 -0
- package/docs/proposals/brain-session-continuity.md +623 -0
- package/docs/proposals/t3code-port.md +81 -0
- package/docs/proposals/t3code-reference.md +95 -0
- package/docs/refactoring/AGENT-TEAMS-PROGRESS.md +88 -0
- package/docs/refactoring/BOUNDARIES.md +120 -0
- package/docs/refactoring/BRAIN-AUDIT.md +176 -0
- package/docs/refactoring/BRAIN-NOTES.md +390 -0
- package/docs/refactoring/BRAIN-TODO.md +214 -0
- package/docs/refactoring/NOTES.md +129 -0
- package/docs/refactoring/T3-PORT-NOTES.md +165 -0
- package/docs/refactoring/T3-PORT-TODO.md +123 -0
- package/docs/refactoring/TODO.md +46 -0
- package/docs/refactoring/brain-benchmark-2026-09-28.json +48 -0
- package/docs/refactoring/brain-benchmark-audit-2026-09-28.json +48 -0
- package/docs/release-notes/v0.2.0.md +35 -0
- package/docs/release-notes/v0.2.1.md +59 -0
- package/docs/release-notes/v0.2.3.md +77 -0
- package/docs/release-notes/v0.2.4.md +128 -0
- package/docs/release-notes/v0.2.5.md +153 -0
- package/docs/release-notes/v0.2.6.md +62 -0
- package/docs/release-notes/v1.0.0.md +120 -0
- package/docs/release-notes/v1.0.1.md +77 -0
- package/docs/release-notes/v1.1.0.md +71 -0
- package/docs/release-notes/v1.1.1.md +38 -0
- package/docs/teams-architecture.md +697 -0
- package/docs/teams.md +171 -0
- package/docs/windows.md +85 -0
- package/package.json +22 -9
- package/dist/adapters/claude-code.d.ts +0 -26
- package/dist/adapters/claude-code.js +0 -209
- package/dist/adapters/claude-code.js.map +0 -1
- package/dist/adapters/codex.d.ts +0 -51
- package/dist/adapters/codex.js +0 -291
- package/dist/adapters/codex.js.map +0 -1
- package/docs/img/board-linear.png +0 -0
- package/docs/img/board-projects.png +0 -0
- package/docs/img/brain.png +0 -0
- package/docs/img/settings.png +0 -0
- package/docs/img/workspace.png +0 -0
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SupabaseHubClient — a daemon's connection to the hosted Loom Team Hub.
|
|
3
|
+
*
|
|
4
|
+
* Same `HubClient` contract as MemoryHub (the reference) and HttpHubClient
|
|
5
|
+
* (a self-hosted `loom hub`), against the Supabase schema in
|
|
6
|
+
* supabase/migrations/0002–0007: reads are RLS-guarded selects, writes are the
|
|
7
|
+
* SECURITY DEFINER rule functions called through `rpc`, and live events are
|
|
8
|
+
* Realtime `postgres_changes` (RLS decides who hears what).
|
|
9
|
+
*
|
|
10
|
+
* Sessions: the client holds a Supabase session (access + refresh token) and
|
|
11
|
+
* supabase-js refreshes it on its own. Refresh tokens rotate — the one that
|
|
12
|
+
* was just used is dead — so whoever persists the session (Team Link, in
|
|
13
|
+
* team.json) must listen with `onSession` and store every new one.
|
|
14
|
+
*
|
|
15
|
+
* Sign-in (D65) is GitHub OAuth through Supabase, PKCE with a loopback
|
|
16
|
+
* redirect: `hostedSignIn`.
|
|
17
|
+
*/
|
|
18
|
+
import { spawn } from "../core/process.js";
|
|
19
|
+
import http from "node:http";
|
|
20
|
+
import { HubError, LEASE_TTL_MS, normalizeRepo, } from "../core/team-hub.js";
|
|
21
|
+
function toHostedSession(s) {
|
|
22
|
+
return {
|
|
23
|
+
accessToken: s.access_token,
|
|
24
|
+
refreshToken: s.refresh_token,
|
|
25
|
+
expiresAt: (s.expires_at ?? Math.floor(Date.now() / 1000) + (s.expires_in ?? 3600)) * 1000,
|
|
26
|
+
userId: s.user.id,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** supabase-js wants somewhere to keep its session (and the PKCE verifier); memory is right for a daemon. */
|
|
30
|
+
function memoryStorage() {
|
|
31
|
+
const m = new Map();
|
|
32
|
+
return {
|
|
33
|
+
getItem: (k) => m.get(k) ?? null,
|
|
34
|
+
setItem: (k, v) => void m.set(k, v),
|
|
35
|
+
removeItem: (k) => void m.delete(k),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async function newSupabase(supabaseUrl, publishableKey) {
|
|
39
|
+
const { createClient } = await import("@supabase/supabase-js");
|
|
40
|
+
return createClient(supabaseUrl, publishableKey, {
|
|
41
|
+
auth: {
|
|
42
|
+
persistSession: true,
|
|
43
|
+
storage: memoryStorage(),
|
|
44
|
+
storageKey: "loom-team-hub",
|
|
45
|
+
autoRefreshToken: true,
|
|
46
|
+
detectSessionInUrl: false,
|
|
47
|
+
flowType: "pkce",
|
|
48
|
+
},
|
|
49
|
+
realtime: { params: { eventsPerSecond: 20 } },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/** A Postgres / PostgREST error as the HubError MemoryHub would have thrown. */
|
|
53
|
+
export function hubErrorFrom(err, httpStatus) {
|
|
54
|
+
const code = err?.code ?? "";
|
|
55
|
+
const message = err?.message || `hosted hub request failed${httpStatus ? ` (${httpStatus})` : ""}`;
|
|
56
|
+
let status = 400;
|
|
57
|
+
if (code === "42501")
|
|
58
|
+
status = 403;
|
|
59
|
+
else if (code === "P0002")
|
|
60
|
+
status = 404;
|
|
61
|
+
else if (code === "40001")
|
|
62
|
+
status = 409;
|
|
63
|
+
else if (/^PT[45]\d\d$/.test(code))
|
|
64
|
+
status = Number(code.slice(2)); // PostgREST's "PTxyz" = HTTP xyz
|
|
65
|
+
else if (code === "PGRST301" || code === "PGRST302" || code === "PGRST303" || httpStatus === 401)
|
|
66
|
+
status = 401;
|
|
67
|
+
return new HubError(message, status);
|
|
68
|
+
}
|
|
69
|
+
/** Postgres timestamps (PostgREST's ISO, or the text form Realtime sends) → epoch ms. */
|
|
70
|
+
export function ms(v) {
|
|
71
|
+
if (typeof v === "number")
|
|
72
|
+
return v;
|
|
73
|
+
if (typeof v !== "string" || !v)
|
|
74
|
+
return 0;
|
|
75
|
+
const iso = v
|
|
76
|
+
.replace(" ", "T")
|
|
77
|
+
.replace(/(\.\d{3})\d+/, "$1")
|
|
78
|
+
.replace(/([+-]\d\d)$/, "$1:00");
|
|
79
|
+
const t = Date.parse(iso);
|
|
80
|
+
return Number.isFinite(t) ? t : 0;
|
|
81
|
+
}
|
|
82
|
+
const str = (v) => (v == null ? "" : String(v));
|
|
83
|
+
const arr = (v) => (Array.isArray(v) ? v.map(String) : []);
|
|
84
|
+
/** Only the optional fields that are set, the way MemoryHub spreads its input. */
|
|
85
|
+
function opt(o) {
|
|
86
|
+
const out = {};
|
|
87
|
+
for (const [k, v] of Object.entries(o))
|
|
88
|
+
if (v !== null && v !== undefined)
|
|
89
|
+
out[k] = v;
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
export function mapTeam(r) {
|
|
93
|
+
return { id: str(r.id), name: str(r.name), createdAt: ms(r.created_at), keyVersion: Number(r.key_version ?? 1) };
|
|
94
|
+
}
|
|
95
|
+
export function mapDevice(r) {
|
|
96
|
+
return {
|
|
97
|
+
id: str(r.id),
|
|
98
|
+
userId: str(r.user_id),
|
|
99
|
+
label: str(r.label),
|
|
100
|
+
sealPub: str(r.seal_pub),
|
|
101
|
+
signPub: str(r.sign_pub),
|
|
102
|
+
createdAt: ms(r.created_at),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function mapPresence(r, github) {
|
|
106
|
+
return {
|
|
107
|
+
deviceId: str(r.device_id),
|
|
108
|
+
repo: str(r.repo),
|
|
109
|
+
...opt({ runId: r.run_id, taskId: r.task_id }),
|
|
110
|
+
agent: str(r.agent),
|
|
111
|
+
kind: str(r.kind),
|
|
112
|
+
...opt({ branch: r.branch }),
|
|
113
|
+
touches: arr(r.touches),
|
|
114
|
+
state: str(r.state),
|
|
115
|
+
since: ms(r.since),
|
|
116
|
+
...opt({ sealed: r.sealed, sig: r.sig }),
|
|
117
|
+
teamId: str(r.team_id),
|
|
118
|
+
userId: str(r.user_id),
|
|
119
|
+
github,
|
|
120
|
+
ts: ms(r.ts),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function mapLease(r, github, now = Date.now()) {
|
|
124
|
+
const ts = ms(r.ts);
|
|
125
|
+
return {
|
|
126
|
+
globs: arr(r.globs),
|
|
127
|
+
files: arr(r.files),
|
|
128
|
+
prefixes: arr(r.prefixes),
|
|
129
|
+
id: str(r.id),
|
|
130
|
+
teamId: str(r.team_id),
|
|
131
|
+
userId: str(r.user_id),
|
|
132
|
+
github,
|
|
133
|
+
deviceId: str(r.device_id),
|
|
134
|
+
repo: str(r.repo),
|
|
135
|
+
runId: str(r.run_id),
|
|
136
|
+
taskId: str(r.task_id),
|
|
137
|
+
state: (str(r.state) || "active"),
|
|
138
|
+
...opt({ sealed: r.sealed }),
|
|
139
|
+
since: ms(r.since),
|
|
140
|
+
ts,
|
|
141
|
+
stale: now - ts > LEASE_TTL_MS,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function mapMemory(r) {
|
|
145
|
+
return {
|
|
146
|
+
id: str(r.id),
|
|
147
|
+
teamId: str(r.team_id),
|
|
148
|
+
repo: str(r.repo),
|
|
149
|
+
authorId: str(r.author_id),
|
|
150
|
+
author: str(r.author),
|
|
151
|
+
hmac: str(r.hmac),
|
|
152
|
+
sealed: r.sealed,
|
|
153
|
+
state: str(r.state),
|
|
154
|
+
...opt({
|
|
155
|
+
supersedes: r.supersedes,
|
|
156
|
+
supersededBy: r.superseded_by,
|
|
157
|
+
resolvedBy: r.resolved_by,
|
|
158
|
+
resolvedReason: r.resolved_reason,
|
|
159
|
+
}),
|
|
160
|
+
confirmedBy: arr(r.confirmed_by),
|
|
161
|
+
createdAt: ms(r.created_at),
|
|
162
|
+
updatedAt: ms(r.updated_at),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function mapFeed(r, github) {
|
|
166
|
+
return {
|
|
167
|
+
...opt({ repo: r.repo }),
|
|
168
|
+
type: str(r.type),
|
|
169
|
+
meta: r.meta ?? {},
|
|
170
|
+
...opt({
|
|
171
|
+
sealed: r.sealed,
|
|
172
|
+
deviceId: r.device_id,
|
|
173
|
+
sig: r.sig,
|
|
174
|
+
dedupeKey: r.dedupe_key,
|
|
175
|
+
}),
|
|
176
|
+
id: Number(r.id),
|
|
177
|
+
teamId: str(r.team_id),
|
|
178
|
+
userId: r.user_id ?? null,
|
|
179
|
+
github,
|
|
180
|
+
ts: ms(r.ts),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** A `jobs` row (PostgREST or Realtime) → the Job MemoryHub returns. */
|
|
184
|
+
export function mapJob(r) {
|
|
185
|
+
return {
|
|
186
|
+
id: str(r.id),
|
|
187
|
+
teamId: str(r.team_id),
|
|
188
|
+
repo: str(r.repo),
|
|
189
|
+
kind: str(r.kind),
|
|
190
|
+
userId: str(r.user_id),
|
|
191
|
+
github: str(r.github),
|
|
192
|
+
...opt({ target: r.target }),
|
|
193
|
+
state: str(r.state),
|
|
194
|
+
...opt({
|
|
195
|
+
runnerId: r.runner_id,
|
|
196
|
+
runnerGithub: r.runner_github,
|
|
197
|
+
claimedAt: r.claimed_at == null ? undefined : ms(r.claimed_at),
|
|
198
|
+
heartbeatAt: r.heartbeat_at == null ? undefined : ms(r.heartbeat_at),
|
|
199
|
+
}),
|
|
200
|
+
sealed: r.sealed,
|
|
201
|
+
...opt({
|
|
202
|
+
progress: r.progress,
|
|
203
|
+
result: r.result,
|
|
204
|
+
error: r.error,
|
|
205
|
+
}),
|
|
206
|
+
createdAt: ms(r.created_at),
|
|
207
|
+
updatedAt: ms(r.updated_at),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/** team_runners / register_runner JSON (already camelCase) → Runner. */
|
|
211
|
+
export function mapRunner(r) {
|
|
212
|
+
return {
|
|
213
|
+
deviceId: str(r.deviceId),
|
|
214
|
+
userId: str(r.userId),
|
|
215
|
+
github: str(r.github),
|
|
216
|
+
label: str(r.label),
|
|
217
|
+
kinds: arr(r.kinds),
|
|
218
|
+
shared: Boolean(r.shared),
|
|
219
|
+
capacity: Number(r.capacity ?? 1),
|
|
220
|
+
lastSeen: ms(r.lastSeen),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
export class SupabaseHubClient {
|
|
224
|
+
opts;
|
|
225
|
+
sbP = null;
|
|
226
|
+
current;
|
|
227
|
+
listeners = new Set();
|
|
228
|
+
githubs = new Map(); // user id → GitHub login
|
|
229
|
+
closed = false;
|
|
230
|
+
constructor(opts) {
|
|
231
|
+
this.opts = opts;
|
|
232
|
+
if (!opts.session && !opts.refreshToken)
|
|
233
|
+
throw new HubError("the hosted hub needs a session — sign in again", 401);
|
|
234
|
+
this.current = opts.session ?? null;
|
|
235
|
+
if (opts.onSession)
|
|
236
|
+
this.listeners.add(opts.onSession);
|
|
237
|
+
}
|
|
238
|
+
/** Hear every new session (refreshes rotate the refresh token). Returns an unsubscribe. */
|
|
239
|
+
onSession(cb) {
|
|
240
|
+
this.listeners.add(cb);
|
|
241
|
+
return () => this.listeners.delete(cb);
|
|
242
|
+
}
|
|
243
|
+
/** The session in effect right now (null until the first call has restored it). */
|
|
244
|
+
session() {
|
|
245
|
+
return this.current;
|
|
246
|
+
}
|
|
247
|
+
get supabaseUrl() {
|
|
248
|
+
return this.opts.supabaseUrl;
|
|
249
|
+
}
|
|
250
|
+
/** Stop refreshing and drop every live subscription. */
|
|
251
|
+
async close() {
|
|
252
|
+
this.closed = true;
|
|
253
|
+
const p = this.sbP;
|
|
254
|
+
this.sbP = null;
|
|
255
|
+
if (!p)
|
|
256
|
+
return;
|
|
257
|
+
const sb = await p.catch(() => null);
|
|
258
|
+
if (!sb)
|
|
259
|
+
return;
|
|
260
|
+
await sb.auth.stopAutoRefresh().catch(() => { });
|
|
261
|
+
await sb.removeAllChannels().catch(() => { });
|
|
262
|
+
sb.realtime.disconnect();
|
|
263
|
+
}
|
|
264
|
+
emitSession(s) {
|
|
265
|
+
const hs = toHostedSession(s);
|
|
266
|
+
const changed = !this.current || this.current.refreshToken !== hs.refreshToken || this.current.accessToken !== hs.accessToken;
|
|
267
|
+
this.current = hs;
|
|
268
|
+
if (!changed)
|
|
269
|
+
return;
|
|
270
|
+
for (const l of this.listeners) {
|
|
271
|
+
try {
|
|
272
|
+
l(hs);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
/* a listener's failure never breaks the session */
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** The supabase-js client, with the session restored (once). */
|
|
280
|
+
sb() {
|
|
281
|
+
if (this.closed)
|
|
282
|
+
return Promise.reject(new HubError("this hub connection is closed", 400));
|
|
283
|
+
if (!this.sbP) {
|
|
284
|
+
this.sbP = (async () => {
|
|
285
|
+
const sb = await newSupabase(this.opts.supabaseUrl, this.opts.publishableKey);
|
|
286
|
+
sb.auth.onAuthStateChange((event, session) => {
|
|
287
|
+
// Synchronous on purpose: awaiting auth calls in here deadlocks supabase-js.
|
|
288
|
+
if (session && (event === "SIGNED_IN" || event === "TOKEN_REFRESHED" || event === "USER_UPDATED")) {
|
|
289
|
+
this.emitSession(session);
|
|
290
|
+
void sb.realtime.setAuth(session.access_token);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
const s = this.current;
|
|
294
|
+
const { data, error } = s
|
|
295
|
+
? await sb.auth.setSession({ access_token: s.accessToken, refresh_token: s.refreshToken })
|
|
296
|
+
: await sb.auth.refreshSession({ refresh_token: this.opts.refreshToken });
|
|
297
|
+
if (error || !data.session) {
|
|
298
|
+
const status = error?.status;
|
|
299
|
+
// 4xx: the refresh token is spent or revoked — only a new sign-in helps
|
|
300
|
+
if (status && status < 500)
|
|
301
|
+
throw new HubError(`the hosted hub session expired — sign in again (${error.message})`, 401);
|
|
302
|
+
throw new HubError(error?.message ?? "couldn't restore the hosted hub session", 503);
|
|
303
|
+
}
|
|
304
|
+
this.emitSession(data.session);
|
|
305
|
+
await sb.realtime.setAuth(data.session.access_token);
|
|
306
|
+
return sb;
|
|
307
|
+
})();
|
|
308
|
+
// a failed restore can be retried (network); a spent token will just fail again with 401
|
|
309
|
+
this.sbP.catch(() => {
|
|
310
|
+
this.sbP = null;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return this.sbP;
|
|
314
|
+
}
|
|
315
|
+
uid() {
|
|
316
|
+
if (!this.current)
|
|
317
|
+
throw new HubError("not signed in to the hosted hub", 401);
|
|
318
|
+
return this.current.userId;
|
|
319
|
+
}
|
|
320
|
+
async rpc(fn, args) {
|
|
321
|
+
const sb = await this.sb();
|
|
322
|
+
const res = await sb.rpc(fn, args);
|
|
323
|
+
if (res.error)
|
|
324
|
+
throw hubErrorFrom(res.error, res.status);
|
|
325
|
+
return res.data;
|
|
326
|
+
}
|
|
327
|
+
async select(build) {
|
|
328
|
+
const sb = await this.sb();
|
|
329
|
+
const res = await build(sb);
|
|
330
|
+
if (res.error)
|
|
331
|
+
throw hubErrorFrom(res.error, res.status);
|
|
332
|
+
return res.data ?? [];
|
|
333
|
+
}
|
|
334
|
+
/** GitHub logins for user ids, cached (profiles are readable for self and teammates). */
|
|
335
|
+
async logins(ids) {
|
|
336
|
+
const missing = [...new Set(ids.filter((x) => Boolean(x) && !this.githubs.has(x)))];
|
|
337
|
+
if (missing.length) {
|
|
338
|
+
const rows = await this.select((sb) => sb.from("profiles").select("user_id, github").in("user_id", missing));
|
|
339
|
+
for (const r of rows)
|
|
340
|
+
this.githubs.set(str(r.user_id), str(r.github));
|
|
341
|
+
}
|
|
342
|
+
return this.githubs;
|
|
343
|
+
}
|
|
344
|
+
async login(id) {
|
|
345
|
+
if (!id)
|
|
346
|
+
return "";
|
|
347
|
+
return (await this.logins([id])).get(id) ?? "";
|
|
348
|
+
}
|
|
349
|
+
async leasesOf(rows) {
|
|
350
|
+
const g = await this.logins(rows.map((r) => r.user_id));
|
|
351
|
+
const now = Date.now();
|
|
352
|
+
return rows.map((r) => mapLease(r, g.get(str(r.user_id)) ?? "", now));
|
|
353
|
+
}
|
|
354
|
+
async claimResult(raw) {
|
|
355
|
+
const rows = [raw.lease, ...(raw.overlaps ?? []).map((o) => o.lease), raw.blockedBy?.lease].filter((x) => Boolean(x));
|
|
356
|
+
const g = await this.logins(rows.map((r) => r.user_id));
|
|
357
|
+
const now = Date.now();
|
|
358
|
+
const lease = (r) => mapLease(r, g.get(str(r.user_id)) ?? "", now);
|
|
359
|
+
return {
|
|
360
|
+
lease: raw.lease ? lease(raw.lease) : null,
|
|
361
|
+
overlaps: (raw.overlaps ?? []).map((o) => ({ lease: lease(o.lease), paths: arr(o.paths) })),
|
|
362
|
+
...(raw.blockedBy ? { blockedBy: { lease: lease(raw.blockedBy.lease), zone: raw.blockedBy.zone } } : {}),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
// ── Phase 1 ──
|
|
366
|
+
async me() {
|
|
367
|
+
await this.sb();
|
|
368
|
+
const rows = await this.select((sb) => sb.from("profiles").select("user_id, github, name").eq("user_id", this.uid()));
|
|
369
|
+
const r = rows[0];
|
|
370
|
+
if (!r)
|
|
371
|
+
throw new HubError("no such user", 404);
|
|
372
|
+
this.githubs.set(str(r.user_id), str(r.github));
|
|
373
|
+
return { id: str(r.user_id), github: str(r.github), name: str(r.name) || str(r.github) };
|
|
374
|
+
}
|
|
375
|
+
async createTeam(name) {
|
|
376
|
+
const clean = name.trim().slice(0, 80);
|
|
377
|
+
if (!clean)
|
|
378
|
+
throw new HubError("a team needs a name");
|
|
379
|
+
return mapTeam(await this.rpc("create_team", { p_name: clean }));
|
|
380
|
+
}
|
|
381
|
+
async teams() {
|
|
382
|
+
await this.sb();
|
|
383
|
+
const rows = await this.select((sb) => sb.from("team_members").select("role, teams(*)").eq("user_id", this.uid()));
|
|
384
|
+
return rows.filter((r) => r.teams).map((r) => ({ ...mapTeam(r.teams), role: str(r.role) }));
|
|
385
|
+
}
|
|
386
|
+
async registerDevice(input) {
|
|
387
|
+
if (!input.sealPub || !input.signPub)
|
|
388
|
+
throw new HubError("a device needs both public keys");
|
|
389
|
+
return mapDevice(await this.rpc("register_device", { p_label: input.label, p_seal_pub: input.sealPub, p_sign_pub: input.signPub }));
|
|
390
|
+
}
|
|
391
|
+
async createInvite(teamId, ttlMs) {
|
|
392
|
+
const args = { p_team: teamId };
|
|
393
|
+
if (typeof ttlMs === "number" && Number.isFinite(ttlMs))
|
|
394
|
+
args.p_ttl_seconds = Math.round(ttlMs / 1000);
|
|
395
|
+
const out = await this.rpc("create_invite", args);
|
|
396
|
+
return { invite: out.invite, expiresAt: Number(out.expiresAt) };
|
|
397
|
+
}
|
|
398
|
+
async redeemInvite(invite) {
|
|
399
|
+
const out = await this.rpc("redeem_invite", { p_invite: invite });
|
|
400
|
+
return { team: mapTeam(out.team), role: out.role };
|
|
401
|
+
}
|
|
402
|
+
async members(teamId) {
|
|
403
|
+
const list = await this.rpc("team_member_list", { p_team: teamId });
|
|
404
|
+
for (const m of list)
|
|
405
|
+
this.githubs.set(m.user.id, m.user.github);
|
|
406
|
+
return list.map((m) => ({ ...m, user: { ...m.user, name: m.user.name || m.user.github } }));
|
|
407
|
+
}
|
|
408
|
+
async removeMember(teamId, userId) {
|
|
409
|
+
await this.rpc("remove_member", { p_team: teamId, p_user: userId });
|
|
410
|
+
}
|
|
411
|
+
async putKeyEnvelopes(teamId, version, envelopes) {
|
|
412
|
+
await this.rpc("put_key_envelopes", { p_team: teamId, p_version: version, p_envelopes: envelopes });
|
|
413
|
+
}
|
|
414
|
+
async keyEnvelopes(teamId, deviceId) {
|
|
415
|
+
const rows = await this.select((sb) => sb.from("key_envelopes").select("version, box").eq("team_id", teamId).eq("device_id", deviceId).order("version"));
|
|
416
|
+
return rows.map((r) => ({ version: Number(r.version), box: str(r.box) }));
|
|
417
|
+
}
|
|
418
|
+
async shareRepo(teamId, repo) {
|
|
419
|
+
await this.rpc("share_repo", { p_team: teamId, p_repo: normalizeRepo(repo) });
|
|
420
|
+
}
|
|
421
|
+
async repos(teamId) {
|
|
422
|
+
const rows = await this.select((sb) => sb.from("team_repos").select("repo").eq("team_id", teamId).order("repo"));
|
|
423
|
+
return rows.map((r) => str(r.repo));
|
|
424
|
+
}
|
|
425
|
+
async heartbeat(teamId, p) {
|
|
426
|
+
await this.rpc("heartbeat", { p_team: teamId, p });
|
|
427
|
+
}
|
|
428
|
+
async clearPresence(teamId, key) {
|
|
429
|
+
await this.rpc("clear_presence", { p_team: teamId, p_device: key.deviceId, p_repo: key.repo, p_agent: key.agent });
|
|
430
|
+
}
|
|
431
|
+
async presence(teamId) {
|
|
432
|
+
const rows = await this.select((sb) => sb.from("presence").select("*").eq("team_id", teamId));
|
|
433
|
+
const g = await this.logins(rows.map((r) => r.user_id));
|
|
434
|
+
return rows.map((r) => mapPresence(r, g.get(str(r.user_id)) ?? ""));
|
|
435
|
+
}
|
|
436
|
+
async appendFeed(teamId, e) {
|
|
437
|
+
const row = await this.rpc("append_feed", { p_team: teamId, e });
|
|
438
|
+
if (!row || row.id == null)
|
|
439
|
+
return null; // deduped
|
|
440
|
+
return mapFeed(row, await this.login(row.user_id));
|
|
441
|
+
}
|
|
442
|
+
async feed(teamId, opts = {}) {
|
|
443
|
+
const limit = Math.min(Math.max(opts.limit ?? 200, 1), 1000);
|
|
444
|
+
const rows = await this.select((sb) => sb
|
|
445
|
+
.from("feed")
|
|
446
|
+
.select("*")
|
|
447
|
+
.eq("team_id", teamId)
|
|
448
|
+
.gt("id", opts.since ?? 0)
|
|
449
|
+
.order("id", { ascending: false })
|
|
450
|
+
.limit(limit));
|
|
451
|
+
const g = await this.logins(rows.map((r) => r.user_id));
|
|
452
|
+
return rows.reverse().map((r) => mapFeed(r, r.user_id ? (g.get(str(r.user_id)) ?? null) : null));
|
|
453
|
+
}
|
|
454
|
+
// ── Phase 2: leases ──
|
|
455
|
+
async claimLease(teamId, c) {
|
|
456
|
+
return this.claimResult(await this.rpc("claim_lease", { p_team: teamId, c }));
|
|
457
|
+
}
|
|
458
|
+
async extendLease(teamId, leaseId, scope, hardZones) {
|
|
459
|
+
const s = { globs: scope.globs, files: scope.files, prefixes: scope.prefixes };
|
|
460
|
+
return this.claimResult(await this.rpc("extend_lease", { p_team: teamId, p_lease: leaseId, scope: s, hard_zones: hardZones }));
|
|
461
|
+
}
|
|
462
|
+
async renewLeases(teamId, deviceId) {
|
|
463
|
+
return Number(await this.rpc("renew_leases", { p_team: teamId, p_device: deviceId }));
|
|
464
|
+
}
|
|
465
|
+
async setRunLeaseState(teamId, runId, state) {
|
|
466
|
+
return Number(await this.rpc("set_run_lease_state", { p_team: teamId, p_run: runId, p_state: state }));
|
|
467
|
+
}
|
|
468
|
+
async releaseLeases(teamId, runId, reason) {
|
|
469
|
+
return Number(await this.rpc("release_leases", { p_team: teamId, p_run: runId, p_reason: reason }));
|
|
470
|
+
}
|
|
471
|
+
async leases(teamId, repo) {
|
|
472
|
+
const r = repo ? normalizeRepo(repo) : null;
|
|
473
|
+
const rows = await this.select((sb) => {
|
|
474
|
+
const q = sb.from("leases").select("*").eq("team_id", teamId);
|
|
475
|
+
return r ? q.eq("repo", r) : q;
|
|
476
|
+
});
|
|
477
|
+
return this.leasesOf(rows);
|
|
478
|
+
}
|
|
479
|
+
// ── Phase 3: the team brain ──
|
|
480
|
+
async publishMemory(teamId, m) {
|
|
481
|
+
const out = await this.rpc("publish_memory", { p_team: teamId, m });
|
|
482
|
+
return { memory: mapMemory(out.memory), merged: Boolean(out.merged) };
|
|
483
|
+
}
|
|
484
|
+
async updateTeamMemory(teamId, id, patch) {
|
|
485
|
+
return mapMemory(await this.rpc("update_team_memory", { p_team: teamId, p_id: id, p_hmac: patch.hmac, p_sealed: patch.sealed }));
|
|
486
|
+
}
|
|
487
|
+
async forgetTeamMemory(teamId, id, reason) {
|
|
488
|
+
await this.rpc("forget_team_memory", { p_team: teamId, p_id: id, p_reason: reason });
|
|
489
|
+
}
|
|
490
|
+
async resolveMemories(teamId, winnerId, loserId, reason) {
|
|
491
|
+
return mapMemory(await this.rpc("resolve_memories", { p_team: teamId, p_winner: winnerId, p_loser: loserId, p_reason: reason }));
|
|
492
|
+
}
|
|
493
|
+
async teamMemories(teamId, repo, opts = {}) {
|
|
494
|
+
const r = normalizeRepo(repo);
|
|
495
|
+
const rows = await this.select((sb) => {
|
|
496
|
+
const q = sb.from("team_memories").select("*").eq("team_id", teamId).eq("repo", r).order("created_at");
|
|
497
|
+
return opts.history ? q : q.eq("state", "live");
|
|
498
|
+
});
|
|
499
|
+
return rows.map(mapMemory);
|
|
500
|
+
}
|
|
501
|
+
// ── Phase 5: runners and jobs ──
|
|
502
|
+
async registerRunner(input) {
|
|
503
|
+
return mapRunner(await this.rpc("register_runner", {
|
|
504
|
+
p_device: input.deviceId,
|
|
505
|
+
p_kinds: input.kinds.map(String),
|
|
506
|
+
p_shared: Boolean(input.shared),
|
|
507
|
+
p_capacity: Math.floor(input.capacity ?? 1),
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
510
|
+
async runners(teamId) {
|
|
511
|
+
return ((await this.rpc("team_runners", { p_team: teamId })) ?? []).map(mapRunner);
|
|
512
|
+
}
|
|
513
|
+
async revokeDevice(deviceId) {
|
|
514
|
+
await this.rpc("revoke_device", { p_device: deviceId });
|
|
515
|
+
}
|
|
516
|
+
async createJob(teamId, j) {
|
|
517
|
+
return mapJob(await this.rpc("create_job", { p_team: teamId, j }));
|
|
518
|
+
}
|
|
519
|
+
async claimJob(teamId, runnerDeviceId, instantOnly = false) {
|
|
520
|
+
const row = await this.rpc("claim_job", { p_team: teamId, p_runner: runnerDeviceId, ...(instantOnly ? { p_instant_only: true } : {}) });
|
|
521
|
+
// a composite-returning function answers "none" as a row of nulls
|
|
522
|
+
return !row || row.id == null ? null : mapJob(row);
|
|
523
|
+
}
|
|
524
|
+
async heartbeatJob(teamId, jobId, runnerDeviceId, progress) {
|
|
525
|
+
return mapJob(await this.rpc("heartbeat_job", { p_team: teamId, p_job: jobId, p_runner: runnerDeviceId, ...(progress ? { p_progress: progress } : {}) }));
|
|
526
|
+
}
|
|
527
|
+
async finishJob(teamId, jobId, runnerDeviceId, outcome) {
|
|
528
|
+
return mapJob(await this.rpc("finish_job", {
|
|
529
|
+
p_team: teamId,
|
|
530
|
+
p_job: jobId,
|
|
531
|
+
p_runner: runnerDeviceId,
|
|
532
|
+
p_state: outcome.state,
|
|
533
|
+
...(outcome.result ? { p_result: outcome.result } : {}),
|
|
534
|
+
...(outcome.error ? { p_error: outcome.error } : {}),
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
async cancelJob(teamId, jobId) {
|
|
538
|
+
return mapJob(await this.rpc("cancel_job", { p_team: teamId, p_job: jobId }));
|
|
539
|
+
}
|
|
540
|
+
async jobs(teamId, opts = {}) {
|
|
541
|
+
const rows = await this.select((sb) => {
|
|
542
|
+
const q = sb.from("jobs").select("*").eq("team_id", teamId).order("created_at").order("id");
|
|
543
|
+
return opts.active ? q.in("state", ["queued", "claimed"]) : q;
|
|
544
|
+
});
|
|
545
|
+
return rows.map(mapJob);
|
|
546
|
+
}
|
|
547
|
+
/** Owners only (0009_phase6.sql); the Edge Function verifies deliveries with it (D83). */
|
|
548
|
+
async webhookSecret(teamId, rotate = false) {
|
|
549
|
+
return { secret: String(await this.rpc("webhook_secret", { p_team: teamId, p_rotate: rotate })) };
|
|
550
|
+
}
|
|
551
|
+
// ── live events ──
|
|
552
|
+
/**
|
|
553
|
+
* Realtime `postgres_changes` on feed, presence, leases, team_memories and jobs,
|
|
554
|
+
* as the HubEvents MemoryHub emits. Inserts and updates are filtered to the
|
|
555
|
+
* team and RLS-checked per member. Deletes can't be filtered and carry only
|
|
556
|
+
* the primary key, so: presence's key includes the team, and lease ids are
|
|
557
|
+
* matched against the leases this subscription has seen.
|
|
558
|
+
*/
|
|
559
|
+
async subscribe(teamId, cb) {
|
|
560
|
+
const sb = await this.sb();
|
|
561
|
+
// MemoryHub refuses non-members; RLS would just stay silent.
|
|
562
|
+
const mine = await this.select((s) => s.from("team_members").select("role").eq("team_id", teamId).eq("user_id", this.uid()));
|
|
563
|
+
if (!mine.length)
|
|
564
|
+
throw new HubError("not a member of this team", 403);
|
|
565
|
+
const runOf = new Map(); // lease id → run id
|
|
566
|
+
for (const l of await this.leases(teamId).catch(() => []))
|
|
567
|
+
runOf.set(l.id, l.runId);
|
|
568
|
+
// one event at a time, in arrival order, even when a login lookup has to wait
|
|
569
|
+
let chain = Promise.resolve();
|
|
570
|
+
let closed = false;
|
|
571
|
+
const emit = (make) => {
|
|
572
|
+
chain = chain
|
|
573
|
+
.then(async () => {
|
|
574
|
+
const e = await make();
|
|
575
|
+
if (e && !closed)
|
|
576
|
+
cb(e);
|
|
577
|
+
})
|
|
578
|
+
.catch(() => {
|
|
579
|
+
/* one bad event never breaks the stream */
|
|
580
|
+
});
|
|
581
|
+
};
|
|
582
|
+
const filter = `team_id=eq.${teamId}`;
|
|
583
|
+
const ch = sb.channel(`loom-team:${teamId}:${Math.random().toString(36).slice(2, 10)}`);
|
|
584
|
+
const on = (table, event, fn) => ch.on("postgres_changes", { event, schema: "public", table, ...(event === "DELETE" ? {} : { filter }) }, ((p) => fn(p)));
|
|
585
|
+
on("feed", "INSERT", (p) => emit(async () => ({ type: "feed", teamId, event: mapFeed(p.new, p.new.user_id ? await this.login(str(p.new.user_id)) : null) })));
|
|
586
|
+
const presence = (p) => emit(async () => ({ type: "presence", teamId, presence: mapPresence(p.new, await this.login(str(p.new.user_id))) }));
|
|
587
|
+
on("presence", "INSERT", presence);
|
|
588
|
+
on("presence", "UPDATE", presence);
|
|
589
|
+
on("presence", "DELETE", (p) => {
|
|
590
|
+
if (str(p.old.team_id) !== teamId)
|
|
591
|
+
return;
|
|
592
|
+
emit(async () => ({
|
|
593
|
+
type: "presence_gone",
|
|
594
|
+
teamId,
|
|
595
|
+
userId: str(p.old.user_id),
|
|
596
|
+
deviceId: str(p.old.device_id),
|
|
597
|
+
agent: str(p.old.agent),
|
|
598
|
+
repo: str(p.old.repo),
|
|
599
|
+
}));
|
|
600
|
+
});
|
|
601
|
+
const lease = (p) => {
|
|
602
|
+
runOf.set(str(p.new.id), str(p.new.run_id));
|
|
603
|
+
emit(async () => ({ type: "lease", teamId, lease: mapLease(p.new, await this.login(str(p.new.user_id))) }));
|
|
604
|
+
};
|
|
605
|
+
on("leases", "INSERT", lease);
|
|
606
|
+
on("leases", "UPDATE", lease);
|
|
607
|
+
on("leases", "DELETE", (p) => {
|
|
608
|
+
const id = str(p.old.id);
|
|
609
|
+
const runId = runOf.get(id);
|
|
610
|
+
if (runId === undefined)
|
|
611
|
+
return; // another team's lease
|
|
612
|
+
runOf.delete(id);
|
|
613
|
+
emit(async () => ({ type: "lease_gone", teamId, leaseIds: [id], runId }));
|
|
614
|
+
});
|
|
615
|
+
const memory = (p) => emit(async () => ({ type: "memory", teamId, memory: mapMemory(p.new) }));
|
|
616
|
+
on("team_memories", "INSERT", memory);
|
|
617
|
+
on("team_memories", "UPDATE", memory);
|
|
618
|
+
const job = (p) => emit(async () => ({ type: "job", teamId, job: mapJob(p.new) }));
|
|
619
|
+
on("jobs", "INSERT", job);
|
|
620
|
+
on("jobs", "UPDATE", job);
|
|
621
|
+
// Ready means both: the channel joined (SUBSCRIBED) *and* Realtime says the
|
|
622
|
+
// Postgres subscription is live — that system message comes later, and a
|
|
623
|
+
// change in between would be lost.
|
|
624
|
+
await new Promise((resolve, reject) => {
|
|
625
|
+
let settled = false;
|
|
626
|
+
let joined = false;
|
|
627
|
+
let pgReady = false;
|
|
628
|
+
const ready = () => {
|
|
629
|
+
if (settled || !joined || !pgReady)
|
|
630
|
+
return;
|
|
631
|
+
settled = true;
|
|
632
|
+
clearTimeout(timer);
|
|
633
|
+
resolve();
|
|
634
|
+
};
|
|
635
|
+
ch.on("system", {}, ((m) => {
|
|
636
|
+
if (m?.extension !== "postgres_changes" || settled)
|
|
637
|
+
return;
|
|
638
|
+
if (m.status === "ok") {
|
|
639
|
+
pgReady = true;
|
|
640
|
+
ready();
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
settled = true;
|
|
644
|
+
clearTimeout(timer);
|
|
645
|
+
reject(new HubError(`hosted hub realtime: ${m.message ?? "postgres changes unavailable"}`, 503));
|
|
646
|
+
}
|
|
647
|
+
}));
|
|
648
|
+
const timer = setTimeout(() => {
|
|
649
|
+
if (settled)
|
|
650
|
+
return;
|
|
651
|
+
settled = true;
|
|
652
|
+
reject(new HubError("the hosted hub's realtime didn't answer", 503));
|
|
653
|
+
}, 20_000);
|
|
654
|
+
timer.unref?.();
|
|
655
|
+
ch.subscribe((status, err) => {
|
|
656
|
+
if (settled)
|
|
657
|
+
return;
|
|
658
|
+
if (status === "SUBSCRIBED") {
|
|
659
|
+
joined = true;
|
|
660
|
+
ready();
|
|
661
|
+
}
|
|
662
|
+
else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
663
|
+
settled = true;
|
|
664
|
+
clearTimeout(timer);
|
|
665
|
+
reject(new HubError(`hosted hub realtime: ${err?.message ?? status.toLowerCase()}`, 503));
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
}).catch(async (err) => {
|
|
669
|
+
await sb.removeChannel(ch).catch(() => { });
|
|
670
|
+
throw err;
|
|
671
|
+
});
|
|
672
|
+
// After the initial join, realtime-js rejoins on its own after a drop.
|
|
673
|
+
return () => {
|
|
674
|
+
closed = true;
|
|
675
|
+
void sb.removeChannel(ch).catch(() => { });
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
// Sign-in: GitHub OAuth through Supabase, PKCE, loopback redirect (D65)
|
|
681
|
+
// ---------------------------------------------------------------------------
|
|
682
|
+
export const SIGN_IN_TIMEOUT_MS = 5 * 60_000;
|
|
683
|
+
function escapeHtml(s) {
|
|
684
|
+
return s.replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
685
|
+
}
|
|
686
|
+
function page(title, body) {
|
|
687
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title>
|
|
688
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
689
|
+
<style>body{font:16px/1.5 system-ui,sans-serif;display:grid;place-items:center;min-height:100vh;margin:0;background:#f7f7f5;color:#1a1a1a}
|
|
690
|
+
@media (prefers-color-scheme:dark){body{background:#161616;color:#eee}}main{max-width:28rem;padding:2rem;text-align:center}
|
|
691
|
+
h1{font-size:1.25rem;margin:0 0 .5rem}p{margin:0;opacity:.75}</style></head>
|
|
692
|
+
<body><main><h1>${escapeHtml(title)}</h1><p>${escapeHtml(body)}</p></main></body></html>`;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* The loopback half of an OAuth sign-in: listen on 127.0.0.1 (random port),
|
|
696
|
+
* hand the redirect URL to `start` (build the authorize URL, open a browser),
|
|
697
|
+
* wait for `/callback?code=…`, trade the code with `exchange`, and tell the
|
|
698
|
+
* browser tab it can close. Rejects on an OAuth error, or after `timeoutMs`.
|
|
699
|
+
*/
|
|
700
|
+
export async function loopbackSignIn(opts) {
|
|
701
|
+
const server = http.createServer();
|
|
702
|
+
await new Promise((resolve, reject) => {
|
|
703
|
+
server.once("error", reject);
|
|
704
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
705
|
+
});
|
|
706
|
+
const port = server.address().port;
|
|
707
|
+
const redirectTo = `http://127.0.0.1:${port}/callback`;
|
|
708
|
+
let timer;
|
|
709
|
+
const done = new Promise((resolve, reject) => {
|
|
710
|
+
let finished = false;
|
|
711
|
+
const finish = (err, value) => {
|
|
712
|
+
if (finished)
|
|
713
|
+
return;
|
|
714
|
+
finished = true;
|
|
715
|
+
if (err)
|
|
716
|
+
reject(err);
|
|
717
|
+
else
|
|
718
|
+
resolve(value);
|
|
719
|
+
};
|
|
720
|
+
timer = setTimeout(() => finish(new HubError("sign-in timed out — nothing came back from the browser in 5 minutes", 408)), opts.timeoutMs ?? SIGN_IN_TIMEOUT_MS);
|
|
721
|
+
server.on("request", (req, res) => {
|
|
722
|
+
const url = new URL(req.url ?? "/", redirectTo);
|
|
723
|
+
const reply = (status, title, body) => {
|
|
724
|
+
res.writeHead(status, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", connection: "close" });
|
|
725
|
+
res.end(page(title, body));
|
|
726
|
+
};
|
|
727
|
+
if (url.pathname !== "/callback")
|
|
728
|
+
return reply(404, "Not found", "This is Loom's sign-in listener.");
|
|
729
|
+
if (finished)
|
|
730
|
+
return reply(410, "Already done", "This sign-in has finished. You can close this tab.");
|
|
731
|
+
const oauthError = url.searchParams.get("error_description") || url.searchParams.get("error");
|
|
732
|
+
if (oauthError) {
|
|
733
|
+
reply(400, "Sign-in failed", `${oauthError}. You can close this tab and try again.`);
|
|
734
|
+
return finish(new HubError(`sign-in failed: ${oauthError}`, 401));
|
|
735
|
+
}
|
|
736
|
+
const code = url.searchParams.get("code");
|
|
737
|
+
if (!code)
|
|
738
|
+
return reply(400, "No sign-in code", "The redirect didn't carry a code. Try signing in again.");
|
|
739
|
+
opts.exchange(code).then((value) => {
|
|
740
|
+
reply(200, "Signed in to Loom", "You can close this tab and go back to your terminal.");
|
|
741
|
+
finish(null, value);
|
|
742
|
+
}, (err) => {
|
|
743
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
744
|
+
reply(400, "Sign-in failed", `${msg}. You can close this tab and try again.`);
|
|
745
|
+
finish(err instanceof HubError ? err : new HubError(`sign-in failed: ${msg}`, 401));
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
try {
|
|
750
|
+
await opts.start(redirectTo);
|
|
751
|
+
return await done;
|
|
752
|
+
}
|
|
753
|
+
finally {
|
|
754
|
+
clearTimeout(timer);
|
|
755
|
+
server.closeAllConnections?.();
|
|
756
|
+
server.close();
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/** Open a URL in the user's browser. */
|
|
760
|
+
export function openInBrowser(url) {
|
|
761
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
762
|
+
try {
|
|
763
|
+
const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
|
|
764
|
+
child.on("error", () => { });
|
|
765
|
+
child.unref();
|
|
766
|
+
}
|
|
767
|
+
catch {
|
|
768
|
+
/* the caller also prints the URL */
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Sign in to the hosted hub with GitHub: PKCE through Supabase Auth, the
|
|
773
|
+
* redirect landing on a loopback listener. Returns the new session.
|
|
774
|
+
*/
|
|
775
|
+
export async function hostedSignIn(opts) {
|
|
776
|
+
const sb = await newSupabase(opts.supabaseUrl, opts.publishableKey);
|
|
777
|
+
try {
|
|
778
|
+
return await loopbackSignIn({
|
|
779
|
+
timeoutMs: opts.timeoutMs,
|
|
780
|
+
start: async (redirectTo) => {
|
|
781
|
+
const { data, error } = await sb.auth.signInWithOAuth({
|
|
782
|
+
provider: "github",
|
|
783
|
+
options: { redirectTo, skipBrowserRedirect: true },
|
|
784
|
+
});
|
|
785
|
+
if (error || !data?.url)
|
|
786
|
+
throw hubErrorFrom(error, 400);
|
|
787
|
+
await (opts.openBrowser ?? openInBrowser)(data.url);
|
|
788
|
+
},
|
|
789
|
+
exchange: async (code) => {
|
|
790
|
+
const { data, error } = await sb.auth.exchangeCodeForSession(code);
|
|
791
|
+
if (error || !data.session)
|
|
792
|
+
throw new HubError(error?.message ?? "no session came back", 401);
|
|
793
|
+
return toHostedSession(data.session);
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
finally {
|
|
798
|
+
// This client only existed to sign in; the hub client takes the session from here.
|
|
799
|
+
await sb.auth.stopAutoRefresh().catch(() => { });
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
/** The authorization code in whatever the user pasted: a full URL, a query string, or the bare code. */
|
|
803
|
+
export function codeFromPasted(pasted) {
|
|
804
|
+
const t = pasted.trim();
|
|
805
|
+
if (!t)
|
|
806
|
+
return null;
|
|
807
|
+
const m = /[?&#]code=([^&#\s]+)/.exec(t);
|
|
808
|
+
if (m)
|
|
809
|
+
return decodeURIComponent(m[1]);
|
|
810
|
+
const err = /[?&#]error_description=([^&#\s]+)/.exec(t);
|
|
811
|
+
if (err)
|
|
812
|
+
throw new HubError(decodeURIComponent(err[1].replace(/\+/g, " ")), 401);
|
|
813
|
+
return /^[A-Za-z0-9._~-]{8,}$/.test(t) ? t : null;
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Hosted sign-in on a machine with no browser — a runner box over SSH (Loom
|
|
817
|
+
* Teams D74). Same PKCE flow as hostedSignIn, but the redirect goes to a
|
|
818
|
+
* loopback address nothing listens on: the user opens the URL anywhere, signs
|
|
819
|
+
* in with GitHub, lands on an error page, and pastes that page's address back.
|
|
820
|
+
* The code is useless without this process's verifier, so pasting it is safe.
|
|
821
|
+
*/
|
|
822
|
+
export async function pasteSignIn(opts) {
|
|
823
|
+
const sb = await newSupabase(opts.supabaseUrl, opts.publishableKey);
|
|
824
|
+
try {
|
|
825
|
+
const { data, error } = await sb.auth.signInWithOAuth({
|
|
826
|
+
provider: "github",
|
|
827
|
+
options: { redirectTo: "http://127.0.0.1:1/loom-signin", skipBrowserRedirect: true },
|
|
828
|
+
});
|
|
829
|
+
if (error || !data?.url)
|
|
830
|
+
throw hubErrorFrom(error, 400);
|
|
831
|
+
const code = codeFromPasted(await opts.ask(data.url));
|
|
832
|
+
if (!code)
|
|
833
|
+
throw new HubError("that didn't contain a sign-in code — paste the whole address you landed on", 400);
|
|
834
|
+
const ex = await sb.auth.exchangeCodeForSession(code);
|
|
835
|
+
if (ex.error || !ex.data.session)
|
|
836
|
+
throw new HubError(ex.error?.message ?? "no session came back", 401);
|
|
837
|
+
return toHostedSession(ex.data.session);
|
|
838
|
+
}
|
|
839
|
+
finally {
|
|
840
|
+
await sb.auth.stopAutoRefresh().catch(() => { });
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
//# sourceMappingURL=supabase-client.js.map
|