@synap-core/cli 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +40 -198
- package/dist/build-info.json +6 -0
- package/dist/commands/agent-run.js +3 -2
- package/dist/commands/agent-run.js.map +1 -1
- package/dist/commands/agents.d.ts +32 -17
- package/dist/commands/agents.js +203 -237
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/app.d.ts +143 -0
- package/dist/commands/app.js +524 -0
- package/dist/commands/app.js.map +1 -0
- package/dist/commands/automate.d.ts +21 -0
- package/dist/commands/automate.js +42 -0
- package/dist/commands/automate.js.map +1 -0
- package/dist/commands/automation-schema.test.d.ts +16 -0
- package/dist/commands/automation-schema.test.js +77 -0
- package/dist/commands/automation-schema.test.js.map +1 -0
- package/dist/commands/automation.d.ts +12 -0
- package/dist/commands/automation.js +116 -66
- package/dist/commands/automation.js.map +1 -1
- package/dist/commands/box.d.ts +42 -0
- package/dist/commands/box.js +89 -0
- package/dist/commands/box.js.map +1 -0
- package/dist/commands/bridge-setup.d.ts +14 -1
- package/dist/commands/bridge-setup.js +395 -129
- package/dist/commands/bridge-setup.js.map +1 -1
- package/dist/commands/cap-connections.d.ts +4 -4
- package/dist/commands/cap-connections.js +14 -14
- package/dist/commands/cap-connections.js.map +1 -1
- package/dist/commands/capability.d.ts +3 -1
- package/dist/commands/capability.js +249 -176
- package/dist/commands/capability.js.map +1 -1
- package/dist/commands/catalog.d.ts +275 -0
- package/dist/commands/catalog.js +481 -0
- package/dist/commands/catalog.js.map +1 -0
- package/dist/commands/cell.d.ts +155 -8
- package/dist/commands/cell.js +416 -40
- package/dist/commands/cell.js.map +1 -1
- package/dist/commands/centrality.js +1 -13
- package/dist/commands/centrality.js.map +1 -1
- package/dist/commands/close-questions.d.ts +27 -0
- package/dist/commands/close-questions.js +49 -0
- package/dist/commands/close-questions.js.map +1 -0
- package/dist/commands/close-questions.test.d.ts +1 -0
- package/dist/commands/close-questions.test.js +52 -0
- package/dist/commands/close-questions.test.js.map +1 -0
- package/dist/commands/connect.d.ts +36 -13
- package/dist/commands/connect.js +68 -167
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/connections.d.ts +3 -2
- package/dist/commands/connections.js +95 -25
- package/dist/commands/connections.js.map +1 -1
- package/dist/commands/context.js +79 -44
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/data-extra.d.ts +34 -0
- package/dist/commands/data-extra.js +203 -25
- package/dist/commands/data-extra.js.map +1 -1
- package/dist/commands/data.d.ts +120 -2
- package/dist/commands/data.js +295 -21
- package/dist/commands/data.js.map +1 -1
- package/dist/commands/diagnose.d.ts +66 -4
- package/dist/commands/diagnose.js +285 -28
- package/dist/commands/diagnose.js.map +1 -1
- package/dist/commands/doc.d.ts +11 -4
- package/dist/commands/doc.js +39 -14
- package/dist/commands/doc.js.map +1 -1
- package/dist/commands/doctor.d.ts +133 -3
- package/dist/commands/doctor.js +761 -41
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/explain.js +5 -5
- package/dist/commands/explain.js.map +1 -1
- package/dist/commands/hooks.d.ts +19 -0
- package/dist/commands/hooks.js +70 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/import.d.ts +393 -13
- package/dist/commands/import.js +1150 -54
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts +34 -9
- package/dist/commands/init.js +481 -981
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/keys.d.ts +1 -0
- package/dist/commands/keys.js +15 -2
- package/dist/commands/keys.js.map +1 -1
- package/dist/commands/knowledge.d.ts +22 -6
- package/dist/commands/knowledge.js +121 -57
- package/dist/commands/knowledge.js.map +1 -1
- package/dist/commands/lens.d.ts +37 -1
- package/dist/commands/lens.js +194 -72
- package/dist/commands/lens.js.map +1 -1
- package/dist/commands/market-authoring.d.ts +29 -2
- package/dist/commands/market-authoring.js +348 -7
- package/dist/commands/market-authoring.js.map +1 -1
- package/dist/commands/market-official.d.ts +58 -0
- package/dist/commands/market-official.js +84 -0
- package/dist/commands/market-official.js.map +1 -0
- package/dist/commands/market.d.ts +400 -16
- package/dist/commands/market.js +1802 -146
- package/dist/commands/market.js.map +1 -1
- package/dist/commands/mcp.d.ts +60 -25
- package/dist/commands/mcp.js +251 -64
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/observe.category.test.d.ts +1 -0
- package/dist/commands/observe.category.test.js +59 -0
- package/dist/commands/observe.category.test.js.map +1 -0
- package/dist/commands/observe.d.ts +3 -0
- package/dist/commands/observe.js +7 -5
- package/dist/commands/observe.js.map +1 -1
- package/dist/commands/observe.title.test.d.ts +1 -0
- package/dist/commands/observe.title.test.js +37 -0
- package/dist/commands/observe.title.test.js.map +1 -0
- package/dist/commands/open.d.ts +39 -9
- package/dist/commands/open.js +80 -25
- package/dist/commands/open.js.map +1 -1
- package/dist/commands/playbook.d.ts +42 -0
- package/dist/commands/playbook.js +171 -0
- package/dist/commands/playbook.js.map +1 -0
- package/dist/commands/pod.js +1 -1
- package/dist/commands/pod.js.map +1 -1
- package/dist/commands/pods.d.ts +11 -1
- package/dist/commands/pods.js +129 -131
- package/dist/commands/pods.js.map +1 -1
- package/dist/commands/proposals-actions.d.ts +62 -0
- package/dist/commands/proposals-actions.js +143 -8
- package/dist/commands/proposals-actions.js.map +1 -1
- package/dist/commands/proposals-await.d.ts +68 -0
- package/dist/commands/proposals-await.js +248 -0
- package/dist/commands/proposals-await.js.map +1 -0
- package/dist/commands/proton-setup.d.ts +73 -0
- package/dist/commands/proton-setup.js +579 -0
- package/dist/commands/proton-setup.js.map +1 -0
- package/dist/commands/providers.d.ts +23 -0
- package/dist/commands/providers.js +126 -1
- package/dist/commands/providers.js.map +1 -1
- package/dist/commands/raycast.js +6 -2
- package/dist/commands/raycast.js.map +1 -1
- package/dist/commands/rule.d.ts +150 -0
- package/dist/commands/rule.js +447 -0
- package/dist/commands/rule.js.map +1 -0
- package/dist/commands/session-feed-wait.d.ts +48 -0
- package/dist/commands/session-feed-wait.js +261 -0
- package/dist/commands/session-feed-wait.js.map +1 -0
- package/dist/commands/session-post.d.ts +40 -0
- package/dist/commands/session-post.js +96 -0
- package/dist/commands/session-post.js.map +1 -0
- package/dist/commands/session-wait.d.ts +116 -0
- package/dist/commands/session-wait.js +224 -0
- package/dist/commands/session-wait.js.map +1 -0
- package/dist/commands/sessions.d.ts +54 -0
- package/dist/commands/sessions.js +501 -58
- package/dist/commands/sessions.js.map +1 -1
- package/dist/commands/skill-manage.d.ts +21 -0
- package/dist/commands/skill-manage.js +136 -19
- package/dist/commands/skill-manage.js.map +1 -1
- package/dist/commands/skills-commands.d.ts +2 -0
- package/dist/commands/skills-commands.js +9 -1
- package/dist/commands/skills-commands.js.map +1 -1
- package/dist/commands/skills-commands.test.d.ts +1 -0
- package/dist/commands/skills-commands.test.js +57 -0
- package/dist/commands/skills-commands.test.js.map +1 -0
- package/dist/commands/skills.js +16 -0
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/status.d.ts +1 -4
- package/dist/commands/status.js +6 -35
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/statusline.backoff.test.d.ts +13 -0
- package/dist/commands/statusline.backoff.test.js +100 -0
- package/dist/commands/statusline.backoff.test.js.map +1 -0
- package/dist/commands/statusline.js +16 -9
- package/dist/commands/statusline.js.map +1 -1
- package/dist/commands/sync-status.d.ts +12 -0
- package/dist/commands/sync-status.js +125 -0
- package/dist/commands/sync-status.js.map +1 -0
- package/dist/commands/templates-retire.d.ts +34 -0
- package/dist/commands/templates-retire.js +192 -0
- package/dist/commands/templates-retire.js.map +1 -0
- package/dist/commands/tools.js +6 -21
- package/dist/commands/tools.js.map +1 -1
- package/dist/commands/tracks.d.ts +112 -0
- package/dist/commands/tracks.js +543 -0
- package/dist/commands/tracks.js.map +1 -0
- package/dist/commands/update.d.ts +3 -11
- package/dist/commands/update.js +5 -134
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/upload.d.ts +41 -3
- package/dist/commands/upload.js +120 -18
- package/dist/commands/upload.js.map +1 -1
- package/dist/commands/vendor.d.ts +28 -0
- package/dist/commands/vendor.js +110 -0
- package/dist/commands/vendor.js.map +1 -0
- package/dist/commands/versions.js +6 -0
- package/dist/commands/versions.js.map +1 -1
- package/dist/commands/view.js +10 -5
- package/dist/commands/view.js.map +1 -1
- package/dist/commands/whoami.d.ts +30 -3
- package/dist/commands/whoami.js +186 -23
- package/dist/commands/whoami.js.map +1 -1
- package/dist/commands/worker-accounts.d.ts +149 -0
- package/dist/commands/worker-accounts.js +339 -0
- package/dist/commands/worker-accounts.js.map +1 -0
- package/dist/commands/worker-accounts.test.d.ts +1 -0
- package/dist/commands/worker-accounts.test.js +224 -0
- package/dist/commands/worker-accounts.test.js.map +1 -0
- package/dist/commands/worker.d.ts +331 -0
- package/dist/commands/worker.js +1067 -0
- package/dist/commands/worker.js.map +1 -0
- package/dist/commands/worker.test.d.ts +1 -0
- package/dist/commands/worker.test.js +427 -0
- package/dist/commands/worker.test.js.map +1 -0
- package/dist/commands/workspace-ops.d.ts +35 -0
- package/dist/commands/workspace-ops.js +153 -0
- package/dist/commands/workspace-ops.js.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +1043 -186
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-connections.d.ts +31 -0
- package/dist/lib/agent-connections.js +33 -0
- package/dist/lib/agent-connections.js.map +1 -0
- package/dist/lib/agent-detect.d.ts +27 -0
- package/dist/lib/agent-detect.js +71 -0
- package/dist/lib/agent-detect.js.map +1 -0
- package/dist/lib/agent-door.d.ts +136 -0
- package/dist/lib/agent-door.js +429 -0
- package/dist/lib/agent-door.js.map +1 -0
- package/dist/lib/agents-config.d.ts +2 -0
- package/dist/lib/agents-config.js +4 -9
- package/dist/lib/agents-config.js.map +1 -1
- package/dist/lib/approval-poll.d.ts +31 -2
- package/dist/lib/approval-poll.js +20 -7
- package/dist/lib/approval-poll.js.map +1 -1
- package/dist/lib/atomic-file.d.ts +48 -0
- package/dist/lib/atomic-file.js +129 -0
- package/dist/lib/atomic-file.js.map +1 -0
- package/dist/lib/auth.d.ts +17 -11
- package/dist/lib/auth.js +47 -64
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/batch-approval.d.ts +85 -0
- package/dist/lib/batch-approval.js +170 -0
- package/dist/lib/batch-approval.js.map +1 -0
- package/dist/lib/box/egress.d.ts +82 -0
- package/dist/lib/box/egress.js +264 -0
- package/dist/lib/box/egress.js.map +1 -0
- package/dist/lib/box/evidence.d.ts +86 -0
- package/dist/lib/box/evidence.js +121 -0
- package/dist/lib/box/evidence.js.map +1 -0
- package/dist/lib/box/job-server.d.ts +72 -0
- package/dist/lib/box/job-server.js +246 -0
- package/dist/lib/box/job-server.js.map +1 -0
- package/dist/lib/box/provisioner.d.ts +83 -0
- package/dist/lib/box/provisioner.js +312 -0
- package/dist/lib/box/provisioner.js.map +1 -0
- package/dist/lib/box/proxmox.d.ts +112 -0
- package/dist/lib/box/proxmox.js +211 -0
- package/dist/lib/box/proxmox.js.map +1 -0
- package/dist/lib/box/wire.d.ts +98 -0
- package/dist/lib/box/wire.js +70 -0
- package/dist/lib/box/wire.js.map +1 -0
- package/dist/lib/capture-lane.d.ts +6 -5
- package/dist/lib/capture-lane.js +6 -5
- package/dist/lib/capture-lane.js.map +1 -1
- package/dist/lib/capture-structure.d.ts +230 -11
- package/dist/lib/capture-structure.js +251 -13
- package/dist/lib/capture-structure.js.map +1 -1
- package/dist/lib/catalog-templates.d.ts +59 -0
- package/dist/lib/catalog-templates.js +126 -0
- package/dist/lib/catalog-templates.js.map +1 -0
- package/dist/lib/cli-build.d.ts +19 -0
- package/dist/lib/cli-build.js +37 -0
- package/dist/lib/cli-build.js.map +1 -0
- package/dist/lib/client-header.test.d.ts +1 -0
- package/dist/lib/client-header.test.js +85 -0
- package/dist/lib/client-header.test.js.map +1 -0
- package/dist/lib/conversation-headers.d.ts +33 -0
- package/dist/lib/conversation-headers.js +69 -0
- package/dist/lib/conversation-headers.js.map +1 -0
- package/dist/lib/cp-packages.d.ts +167 -11
- package/dist/lib/cp-packages.js +253 -21
- package/dist/lib/cp-packages.js.map +1 -1
- package/dist/lib/credential-class.d.ts +144 -0
- package/dist/lib/credential-class.js +138 -0
- package/dist/lib/credential-class.js.map +1 -0
- package/dist/lib/describe-lens.d.ts +35 -0
- package/dist/lib/describe-lens.js +40 -0
- package/dist/lib/describe-lens.js.map +1 -1
- package/dist/lib/device-connect.d.ts +125 -0
- package/dist/lib/device-connect.js +256 -0
- package/dist/lib/device-connect.js.map +1 -0
- package/dist/lib/directory-lens.d.ts +99 -0
- package/dist/lib/directory-lens.js +187 -0
- package/dist/lib/directory-lens.js.map +1 -0
- package/dist/lib/exporter-coverage.d.ts +71 -0
- package/dist/lib/exporter-coverage.js +97 -0
- package/dist/lib/exporter-coverage.js.map +1 -0
- package/dist/lib/goal-words.d.ts +38 -0
- package/dist/lib/goal-words.js +112 -0
- package/dist/lib/goal-words.js.map +1 -0
- package/dist/lib/hub-client.d.ts +57 -15
- package/dist/lib/hub-client.js +396 -82
- package/dist/lib/hub-client.js.map +1 -1
- package/dist/lib/init-connect.d.ts +41 -0
- package/dist/lib/init-connect.js +75 -0
- package/dist/lib/init-connect.js.map +1 -0
- package/dist/lib/init-exit.d.ts +24 -0
- package/dist/lib/init-exit.js +41 -0
- package/dist/lib/init-exit.js.map +1 -0
- package/dist/lib/init-flow.d.ts +25 -0
- package/dist/lib/init-flow.js +40 -0
- package/dist/lib/init-flow.js.map +1 -0
- package/dist/lib/init-handoff.d.ts +24 -0
- package/dist/lib/init-handoff.js +26 -0
- package/dist/lib/init-handoff.js.map +1 -0
- package/dist/lib/init-output.d.ts +18 -0
- package/dist/lib/init-output.js +43 -0
- package/dist/lib/init-output.js.map +1 -0
- package/dist/lib/init-pod.d.ts +45 -0
- package/dist/lib/init-pod.js +36 -0
- package/dist/lib/init-pod.js.map +1 -0
- package/dist/lib/installed.d.ts +210 -11
- package/dist/lib/installed.js +234 -36
- package/dist/lib/installed.js.map +1 -1
- package/dist/lib/key-approval.d.ts +57 -0
- package/dist/lib/key-approval.js +117 -0
- package/dist/lib/key-approval.js.map +1 -0
- package/dist/lib/key-diagnosis.d.ts +56 -0
- package/dist/lib/key-diagnosis.js +227 -0
- package/dist/lib/key-diagnosis.js.map +1 -0
- package/dist/lib/key-doctor.d.ts +136 -0
- package/dist/lib/key-doctor.js +286 -0
- package/dist/lib/key-doctor.js.map +1 -0
- package/dist/lib/key-locations.d.ts +120 -0
- package/dist/lib/key-locations.js +326 -0
- package/dist/lib/key-locations.js.map +1 -0
- package/dist/lib/key-source.d.ts +68 -0
- package/dist/lib/key-source.js +109 -0
- package/dist/lib/key-source.js.map +1 -0
- package/dist/lib/key-state.d.ts +70 -0
- package/dist/lib/key-state.js +115 -0
- package/dist/lib/key-state.js.map +1 -0
- package/dist/lib/kind-package.d.ts +181 -0
- package/dist/lib/kind-package.js +679 -0
- package/dist/lib/kind-package.js.map +1 -0
- package/dist/lib/knowledge-title.d.ts +25 -0
- package/dist/lib/knowledge-title.js +47 -0
- package/dist/lib/knowledge-title.js.map +1 -0
- package/dist/lib/knowledge-title.test.d.ts +1 -0
- package/dist/lib/knowledge-title.test.js +34 -0
- package/dist/lib/knowledge-title.test.js.map +1 -0
- package/dist/lib/machine-instance.d.ts +64 -0
- package/dist/lib/machine-instance.js +142 -0
- package/dist/lib/machine-instance.js.map +1 -0
- package/dist/lib/mcp-bridge.d.ts +49 -0
- package/dist/lib/mcp-bridge.js +180 -0
- package/dist/lib/mcp-bridge.js.map +1 -0
- package/dist/lib/mcp-bridge.test.d.ts +1 -0
- package/dist/lib/mcp-bridge.test.js +118 -0
- package/dist/lib/mcp-bridge.test.js.map +1 -0
- package/dist/lib/mcp-call.d.ts +30 -0
- package/dist/lib/mcp-call.js +107 -0
- package/dist/lib/mcp-call.js.map +1 -0
- package/dist/lib/mcp-snippets.d.ts +53 -0
- package/dist/lib/mcp-snippets.js +82 -0
- package/dist/lib/mcp-snippets.js.map +1 -0
- package/dist/lib/next-steps.d.ts +8 -0
- package/dist/lib/next-steps.js +31 -1
- package/dist/lib/next-steps.js.map +1 -1
- package/dist/lib/openclaw.js +3 -2
- package/dist/lib/openclaw.js.map +1 -1
- package/dist/lib/package-wire.d.ts +179 -0
- package/dist/lib/package-wire.js +68 -0
- package/dist/lib/package-wire.js.map +1 -0
- package/dist/lib/pair-link.d.ts +38 -0
- package/dist/lib/pair-link.js +68 -0
- package/dist/lib/pair-link.js.map +1 -0
- package/dist/lib/pod-moved.d.ts +70 -0
- package/dist/lib/pod-moved.js +146 -0
- package/dist/lib/pod-moved.js.map +1 -0
- package/dist/lib/pod-reach.d.ts +39 -0
- package/dist/lib/pod-reach.js +117 -0
- package/dist/lib/pod-reach.js.map +1 -0
- package/dist/lib/pod-reach.test.d.ts +1 -0
- package/dist/lib/pod-reach.test.js +67 -0
- package/dist/lib/pod-reach.test.js.map +1 -0
- package/dist/lib/pod-wide.d.ts +40 -0
- package/dist/lib/pod-wide.js +46 -0
- package/dist/lib/pod-wide.js.map +1 -0
- package/dist/lib/pod.d.ts +74 -112
- package/dist/lib/pod.js +146 -415
- package/dist/lib/pod.js.map +1 -1
- package/dist/lib/project-ref.d.ts +9 -3
- package/dist/lib/project-ref.js +10 -4
- package/dist/lib/project-ref.js.map +1 -1
- package/dist/lib/register-claude-code-mcp.test.d.ts +1 -0
- package/dist/lib/register-claude-code-mcp.test.js +78 -0
- package/dist/lib/register-claude-code-mcp.test.js.map +1 -0
- package/dist/lib/review-credential.d.ts +119 -0
- package/dist/lib/review-credential.js +149 -0
- package/dist/lib/review-credential.js.map +1 -0
- package/dist/lib/safe-open.d.ts +49 -0
- package/dist/lib/safe-open.js +119 -0
- package/dist/lib/safe-open.js.map +1 -0
- package/dist/lib/session-feed.d.ts +185 -0
- package/dist/lib/session-feed.js +257 -0
- package/dist/lib/session-feed.js.map +1 -0
- package/dist/lib/session-lens.d.ts +40 -1
- package/dist/lib/session-lens.js +54 -4
- package/dist/lib/session-lens.js.map +1 -1
- package/dist/lib/superwhisper-import.d.ts +8 -0
- package/dist/lib/superwhisper-import.js +14 -7
- package/dist/lib/superwhisper-import.js.map +1 -1
- package/dist/lib/superwhisper-title.test.d.ts +1 -0
- package/dist/lib/superwhisper-title.test.js +18 -0
- package/dist/lib/superwhisper-title.test.js.map +1 -0
- package/dist/lib/targets.d.ts +152 -10
- package/dist/lib/targets.js +974 -325
- package/dist/lib/targets.js.map +1 -1
- package/dist/lib/template-file.d.ts +4 -0
- package/dist/lib/template-file.js.map +1 -1
- package/dist/lib/template-lifecycle.d.ts +217 -0
- package/dist/lib/template-lifecycle.js +433 -0
- package/dist/lib/template-lifecycle.js.map +1 -0
- package/dist/lib/template-steps.d.ts +88 -0
- package/dist/lib/template-steps.js +118 -0
- package/dist/lib/template-steps.js.map +1 -0
- package/dist/lib/time-ago.d.ts +2 -0
- package/dist/lib/time-ago.js +15 -0
- package/dist/lib/time-ago.js.map +1 -0
- package/dist/lib/wake-hook-settings.d.ts +57 -0
- package/dist/lib/wake-hook-settings.js +153 -0
- package/dist/lib/wake-hook-settings.js.map +1 -0
- package/dist/lib/wake-state.d.ts +65 -0
- package/dist/lib/wake-state.js +122 -0
- package/dist/lib/wake-state.js.map +1 -0
- package/dist/utils/logger.js +5 -2
- package/dist/utils/logger.js.map +1 -1
- package/package.json +26 -17
- package/skills/README.md +13 -11
- package/skills/agent-os/SKILL.md +214 -99
- package/skills/manifest.json +1 -1
- package/skills/onboard/SKILL.md +55 -26
- package/skills/synap/README.md +5 -8
- package/skills/synap/SKILL.md +1149 -289
- package/skills/synap/_order.txt +7 -0
- package/skills/synap/_skill.yaml +1 -1
- package/skills/synap/automations.md +15 -0
- package/skills/synap/capture.md +5 -0
- package/skills/synap/cli-operations.md +24 -18
- package/skills/synap/code-skills.md +70 -0
- package/skills/synap/concepts.md +36 -0
- package/skills/synap/content-os.md +10 -1
- package/skills/synap/creative-loop.md +15 -10
- package/skills/synap/crm.md +21 -52
- package/skills/synap/document-embeds.md +143 -47
- package/skills/synap/escalation-ladder.md +20 -10
- package/skills/synap/focus-sessions.md +56 -21
- package/skills/synap/from-intent.md +158 -0
- package/skills/synap/governance-rules.md +5 -5
- package/skills/synap/governance.md +10 -7
- package/skills/synap/hand-off.md +33 -0
- package/skills/synap/inline-patterns.md +15 -7
- package/skills/synap/lenses.md +34 -16
- package/skills/synap/linking.md +42 -41
- package/skills/synap/mcp-remote-servers.md +64 -0
- package/skills/synap/mental-model.md +17 -15
- package/skills/synap/operating-mode-rules.md +2 -2
- package/skills/synap/processes.md +118 -0
- package/skills/synap/quick-reference.md +7 -2
- package/skills/synap/reading.md +3 -1
- package/skills/synap/reflexes.md +12 -42
- package/skills/synap/viewframe-cells.md +38 -5
- package/skills/synap/work-flow.md +1 -1
- package/skills/synap/work-model.md +110 -0
- package/skills/synap/worked-examples.md +4 -5
- package/skills/synap/workspace-design.md +15 -11
- package/skills/synap/writes.md +105 -25
- package/skills/synap/writing-titles.md +63 -0
- package/skills/synap-app/SKILL.md +149 -67
- package/skills/synap-app/_skill.yaml +1 -1
- package/skills/synap-app/creating-the-base-app.md +12 -7
- package/skills/synap-app/governance.md +27 -16
- package/skills/synap-app/install-commands.md +18 -9
- package/skills/synap-app/overview.md +16 -9
- package/skills/synap-app/using-the-sdk.md +75 -25
- package/skills/synap-design/SKILL.md +474 -0
- package/skills/synap-design/_order.txt +4 -0
- package/skills/synap-design/_skill.yaml +21 -0
- package/skills/synap-design/loop.md +134 -0
- package/skills/synap-design/overview.md +44 -0
- package/skills/synap-design/principles.md +130 -0
- package/skills/synap-design/system.md +133 -0
- package/skills/synap-market/SKILL.md +306 -16
- package/skills/synap-market/_skill.yaml +1 -1
- package/skills/synap-market/author-and-publish.md +145 -8
- package/skills/synap-market/governance-and-catalog.md +7 -0
- package/skills/synap-market/install.md +79 -1
- package/skills/synap-market/overview.md +74 -6
- package/skills/synap-schema/SKILL.md +172 -10
- package/skills/synap-schema/_order.txt +2 -0
- package/skills/synap-schema/_skill.yaml +4 -2
- package/skills/synap-schema/common-mistakes.md +4 -2
- package/skills/synap-schema/discover-profiles.md +1 -1
- package/skills/synap-schema/entity-scope.md +15 -3
- package/skills/synap-schema/extend-first.md +29 -0
- package/skills/synap-schema/extend-vs-create.md +1 -1
- package/skills/synap-schema/intent-vocabulary-ssot.md +111 -0
- package/skills/synap-schema/property-types.md +1 -1
- package/skills/synap-schema/relations.md +1 -1
- package/skills/synap-ui/SKILL.md +19 -12
- package/skills/synap-ui/_skill.yaml +1 -1
- package/skills/synap-ui/ai-companion.md +1 -1
- package/skills/synap-ui/arranging-bento.md +2 -0
- package/skills/synap-ui/bento-layouts.md +10 -10
- package/skills/synap-ui/bento-recipes.md +313 -543
- package/skills/synap-ui/four-builds.md +5 -0
- package/skills/synap-ui/view-types.md +2 -2
- package/skills/synap-ui/widget-catalog.md +22 -278
- package/templates/automations/bookmark-enrichment.automation.json +290 -13
- package/templates/automations/url-bookmark-capture.automation.json +73 -11
- package/templates/box/runner.py +206 -0
- package/templates/skills/authoring-playbooks/SKILL.md +6 -3
- package/templates/skills/authoring-playbooks/reference/stages-contract.md +15 -0
- package/templates/skills/authoring-playbooks/reference/worked-example.md +3 -0
- package/dist/commands/infra.d.ts +0 -21
- package/dist/commands/infra.js +0 -262
- package/dist/commands/infra.js.map +0 -1
- package/node_modules/@synap/hub-rest-client/dist/index.cjs +0 -1261
- package/node_modules/@synap/hub-rest-client/dist/index.d.cts +0 -1647
- package/node_modules/@synap/hub-rest-client/dist/index.d.ts +0 -1647
- package/node_modules/@synap/hub-rest-client/dist/index.js +0 -1231
- package/node_modules/@synap/hub-rest-client/package.json +0 -45
- package/node_modules/@synap/hub-rest-client/src/client.ts +0 -1631
- package/node_modules/@synap/hub-rest-client/src/errors.ts +0 -30
- package/node_modules/@synap/hub-rest-client/src/index.ts +0 -153
- package/node_modules/@synap/hub-rest-client/src/setup.ts +0 -133
- package/node_modules/@synap/hub-rest-client/src/types.ts +0 -1299
package/dist/commands/init.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* synap init
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Laptop/desktop (the default): find or create a pod → connect the agents on
|
|
5
|
+
* this machine (Claude Code, Codex, Cursor — one row each) → pair
|
|
6
|
+
* the phone → hand the person one real task to give their agent.
|
|
7
|
+
*
|
|
8
|
+
* One flow for every machine, a server running a pod included: Synap is
|
|
9
|
+
* agent-agnostic, so init connects the agents found here and installs none.
|
|
8
10
|
*/
|
|
9
11
|
import prompts from "prompts";
|
|
10
12
|
import ora from "ora";
|
|
@@ -12,955 +14,498 @@ import chalk from "chalk";
|
|
|
12
14
|
import { execSync } from "child_process";
|
|
13
15
|
import fs from "fs";
|
|
14
16
|
import { log, banner } from "../utils/logger.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
17
|
+
import { checkPodHealth, getLocalPodConfig, listPodProfiles, addPodProfile, setActivePod, } from "../lib/pod.js";
|
|
18
|
+
import { CP_URL, login, isLoggedIn, listPods, podUrlOf, getStoredToken, waitForPodCallback } from "../lib/auth.js";
|
|
19
|
+
import { detectAgents } from "../lib/agent-detect.js";
|
|
20
|
+
import { reachChecklist, unreachableMessage } from "../lib/pod-reach.js";
|
|
21
|
+
import { createPodInBrowser } from "../lib/init-pod.js";
|
|
22
|
+
import { buildPairLink, renderTerminalQr } from "../lib/pair-link.js";
|
|
23
|
+
import { buildHandoff } from "../lib/init-handoff.js";
|
|
24
|
+
import { connectAgentsBatch } from "../lib/agent-door.js";
|
|
25
|
+
import { INIT_EXIT, InitExit } from "../lib/init-exit.js";
|
|
26
|
+
import { askControlPlaneWherePodIs, podMovedMessage } from "../lib/pod-moved.js";
|
|
27
|
+
import { reserveStdoutForJson } from "../lib/init-output.js";
|
|
28
|
+
import { isManagedPod, keyChoices } from "../lib/init-flow.js";
|
|
29
|
+
import { installWakeHookForProject } from "./hooks.js";
|
|
30
|
+
import { computerKeyNeedsRenewal, deviceConnect, DeviceStartError, saveDeviceGrant, wireApprovedAgents, } from "../lib/device-connect.js";
|
|
31
|
+
/** Where a phone without Relay gets it (synap-landing/app/download/relay). */
|
|
32
|
+
const RELAY_DOWNLOAD_URL = "https://synap.live/download/relay";
|
|
33
|
+
const AGENT_CHOICES = ["claude-code", "codex", "cursor"];
|
|
20
34
|
export async function init(opts) {
|
|
35
|
+
const emitJson = opts.json ? reserveStdoutForJson() : null;
|
|
21
36
|
banner();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
// A --pod-url flag that's explicitly unreachable is a separate diagnostic
|
|
30
|
-
// (handled below once checkPodHealth has confirmed).
|
|
31
|
-
if (!opts.podUrl) {
|
|
32
|
-
if (await hasAnyPodSignal()) {
|
|
33
|
-
// fall through into the normal flow
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
printNoPodInstructions();
|
|
37
|
-
process.exit(2);
|
|
38
|
-
}
|
|
37
|
+
let summary = null;
|
|
38
|
+
let exitCode = INIT_EXIT.ok;
|
|
39
|
+
let error;
|
|
40
|
+
try {
|
|
41
|
+
summary = await runInit(opts);
|
|
42
|
+
if (summary && summary.connected.length === 0)
|
|
43
|
+
exitCode = INIT_EXIT.noAgent;
|
|
39
44
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
catch (err) {
|
|
46
|
+
if (!(err instanceof InitExit))
|
|
47
|
+
throw err;
|
|
48
|
+
exitCode = err.code;
|
|
49
|
+
error = err.message;
|
|
50
|
+
if (!err.shown)
|
|
51
|
+
log.error(error);
|
|
52
|
+
}
|
|
53
|
+
emitJson?.(JSON.stringify(summary ? { ...summary, exitCode } : { exitCode, error }) + "\n");
|
|
54
|
+
if (exitCode !== INIT_EXIT.ok)
|
|
55
|
+
process.exit(exitCode);
|
|
56
|
+
}
|
|
57
|
+
/** `--agents claude-code,codex` → the targets, or a usage error naming the bad one. */
|
|
58
|
+
function parseAgents(list) {
|
|
59
|
+
const names = list.split(",").map((a) => a.trim()).filter(Boolean);
|
|
60
|
+
const bad = names.filter((a) => !AGENT_CHOICES.includes(a));
|
|
61
|
+
if (bad.length > 0 || names.length === 0) {
|
|
62
|
+
throw new InitExit(INIT_EXIT.usage, `Unknown agent${bad.length > 1 ? "s" : ""}: ${bad.join(", ") || "(none)"}. Use ${AGENT_CHOICES.join(", ")}.`);
|
|
63
|
+
}
|
|
64
|
+
return [...new Set(names)];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The one line for a pod address that redirects to another host: the new
|
|
68
|
+
* address when the Control Plane knows it (never the redirect's), else what
|
|
69
|
+
* to run to find it.
|
|
70
|
+
*/
|
|
71
|
+
async function movedPodLine(podUrl, next, saved = false) {
|
|
72
|
+
const answer = await askControlPlaneWherePodIs({
|
|
73
|
+
podUrl,
|
|
74
|
+
cpUrl: CP_URL,
|
|
75
|
+
cpToken: getStoredToken()?.token,
|
|
76
|
+
});
|
|
77
|
+
if (answer.kind === "moved")
|
|
78
|
+
return `The pod at ${podUrl} moved to ${answer.podUrl}. ${next(answer.podUrl)}`;
|
|
79
|
+
if (saved)
|
|
80
|
+
return podMovedMessage(podUrl);
|
|
81
|
+
return answer.kind === "signed-out"
|
|
82
|
+
? `The pod at ${podUrl} has a new address. Run: synap login — then run init again to look it up.`
|
|
83
|
+
: `The pod at ${podUrl} has a new address. Use the address shown in your account.`;
|
|
84
|
+
}
|
|
85
|
+
async function runInit(opts) {
|
|
86
|
+
const tty = Boolean(process.stdin.isTTY);
|
|
87
|
+
if (!tty && (!opts.podUrl || !opts.agents)) {
|
|
88
|
+
throw new InitExit(INIT_EXIT.usage, "Not a terminal. Pass --pod-url and --agents claude-code,codex");
|
|
89
|
+
}
|
|
90
|
+
opts.unattended = !tty || Boolean(opts.yes);
|
|
91
|
+
const agents = opts.agents ? parseAgents(opts.agents) : undefined;
|
|
92
|
+
// An explicit --pod-url is probed before anything assumes reachability: a
|
|
93
|
+
// typo'd URL fails loud with a checklist instead of silent 500s later.
|
|
94
|
+
if (opts.podUrl) {
|
|
43
95
|
const probe = await checkPodHealth(opts.podUrl);
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
process.exit(2);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// ── Auto-detect environment ─────────────────────────────────────────────
|
|
50
|
-
// Pod URL is always the primary decision axis. OpenClaw, if present, is an
|
|
51
|
-
// optional enhancement that gets wired after the pod connection is set up —
|
|
52
|
-
// it is no longer the gating condition for which path runs.
|
|
53
|
-
const oc = detectOpenClaw();
|
|
54
|
-
const isServer = detectServer();
|
|
55
|
-
if (oc.found) {
|
|
56
|
-
log.info(`OpenClaw detected${oc.version ? ` v${oc.version}` : ""} — will wire it after pod setup`);
|
|
57
|
-
}
|
|
58
|
-
if (isServer) {
|
|
59
|
-
// Before assuming fresh install, check if a pod is already running
|
|
60
|
-
const spinner = ora("Scanning for existing Synap pod...").start();
|
|
61
|
-
const existingPodUrl = opts.podUrl ?? (await detectLocalPod());
|
|
62
|
-
spinner.stop();
|
|
63
|
-
if (existingPodUrl) {
|
|
64
|
-
log.success(`Found existing pod at ${existingPodUrl}`);
|
|
65
|
-
if (oc.found) {
|
|
66
|
-
log.info("Server + OpenClaw — connecting pod then wiring OpenClaw");
|
|
67
|
-
await pathA(opts, oc);
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
log.info("Server detected — connecting to existing pod");
|
|
71
|
-
await pathBExisting(opts, existingPodUrl);
|
|
72
|
-
}
|
|
96
|
+
if (probe.moved) {
|
|
97
|
+
throw new InitExit(INIT_EXIT.noPod, await movedPodLine(opts.podUrl, (to) => `Run: synap init --pod-url ${to}`));
|
|
73
98
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
99
|
+
if (!probe.healthy) {
|
|
100
|
+
printUnreachablePodInstructions(opts.podUrl, probe.failure);
|
|
101
|
+
throw new InitExit(INIT_EXIT.noPod, unreachableMessage(opts.podUrl, probe.failure), true);
|
|
77
102
|
}
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
if (oc.found) {
|
|
81
|
-
// Desktop with OpenClaw — use pathA (OpenClaw connect mode)
|
|
82
|
-
await pathA(opts, oc);
|
|
83
|
-
return;
|
|
84
103
|
}
|
|
85
|
-
|
|
86
|
-
await pathC(opts);
|
|
104
|
+
return desktopFlow(opts, agents);
|
|
87
105
|
}
|
|
88
106
|
// =============================================================================
|
|
89
|
-
//
|
|
107
|
+
// DESKTOP: pod → agents → phone → first task
|
|
90
108
|
// =============================================================================
|
|
91
|
-
async function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
await
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
async function desktopFlow(opts, agents) {
|
|
110
|
+
log.heading("Your pod");
|
|
111
|
+
const pod = await resolveDesktopPod(opts);
|
|
112
|
+
// The agents asked for WITH the computer: --agents, else the ones found here.
|
|
113
|
+
const wanted = agents ?? detectAgents().filter((r) => r.found).map((r) => r.target);
|
|
114
|
+
const { key: humanKey, grant } = await ensurePodKey(pod.podUrl, opts, pod.podId, wanted);
|
|
115
|
+
// Connected just now: the person already chose the agents on the approval
|
|
116
|
+
// page, so connect exactly those.
|
|
117
|
+
const picked = grant
|
|
118
|
+
? grant.agents.map((a) => a.agentType).filter((t) => AGENT_CHOICES.includes(t))
|
|
119
|
+
: agents;
|
|
120
|
+
const outcome = await connectAgentsStep(pod.podUrl, humanKey, opts, picked);
|
|
121
|
+
// Claude Code connected: let another session's agent (or the person, in the
|
|
122
|
+
// room) wake this project's terminals — the asyncRewake Stop hook.
|
|
123
|
+
if (outcome.connected.includes("claude-code"))
|
|
124
|
+
installWakeHookForProject();
|
|
125
|
+
const pairLink = await pairPhoneStep(pod.podUrl);
|
|
126
|
+
printClosing(outcome.connectedLabels);
|
|
127
|
+
return {
|
|
128
|
+
podUrl: pod.podUrl,
|
|
129
|
+
connected: outcome.connected,
|
|
130
|
+
notApproved: outcome.notApproved,
|
|
131
|
+
failed: outcome.failed,
|
|
132
|
+
pairLink,
|
|
133
|
+
};
|
|
113
134
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
spinner.succeed(`Pod healthy${health.version ? ` (v${health.version})` : ""}`);
|
|
136
|
-
// Connect (generate Hub API key)
|
|
137
|
-
const apiKey = await connectStep(podUrl, opts, false);
|
|
138
|
-
if (!apiKey)
|
|
139
|
-
return;
|
|
140
|
-
// OpenClaw — always Docker on a server with an existing compose stack
|
|
141
|
-
log.heading("Step 2: OpenClaw");
|
|
142
|
-
const deployDir = findSynapDeployDir();
|
|
143
|
-
if (!deployDir) {
|
|
144
|
-
log.warn("Could not find your Synap deploy directory automatically.");
|
|
145
|
-
log.dim("Start OpenClaw manually from your synap-backend dir:");
|
|
146
|
-
log.dim(" docker compose --profile openclaw up -d openclaw");
|
|
147
|
-
log.dim("Then run: synap update");
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
log.info(`Deploy dir: ${deployDir}`);
|
|
151
|
-
log.blank();
|
|
152
|
-
const localConfig = getLocalPodConfig();
|
|
153
|
-
// Stop any spinner before running docker compose — its output goes to stdout
|
|
154
|
-
// and will conflict with ora. We print progress directly.
|
|
155
|
-
log.info("Running: docker compose --profile openclaw up -d openclaw");
|
|
156
|
-
log.dim("(This may take a few minutes on first run — pulling image ~1GB)");
|
|
157
|
-
log.blank();
|
|
158
|
-
let ocStarted = false;
|
|
159
|
-
try {
|
|
160
|
-
startOpenClawOnServer(apiKey, localConfig?.agentUserId ?? "", localConfig?.workspaceId ?? "", podUrl, getActiveProjectId());
|
|
161
|
-
ocStarted = true;
|
|
162
|
-
log.success("OpenClaw container started");
|
|
163
|
-
}
|
|
164
|
-
catch (err) {
|
|
165
|
-
log.warn(err instanceof Error ? err.message : String(err));
|
|
166
|
-
}
|
|
167
|
-
log.blank();
|
|
168
|
-
if (ocStarted) {
|
|
169
|
-
log.info("OpenClaw is initializing (first boot takes 1-2 min).");
|
|
170
|
-
log.info("Once it's ready, run:");
|
|
171
|
-
console.log(chalk.cyan("\n synap update\n"));
|
|
172
|
-
log.dim("This will install the skill, seed your workspace, and configure AI routing.");
|
|
173
|
-
log.dim("Check progress at any time: synap status");
|
|
135
|
+
/**
|
|
136
|
+
* The pod this machine talks to. In order: --pod-url, a saved profile that
|
|
137
|
+
* answers, a pod running on this machine — and when there is none, create one
|
|
138
|
+
* instead of exiting.
|
|
139
|
+
*/
|
|
140
|
+
async function resolveDesktopPod(opts) {
|
|
141
|
+
if (opts.podUrl)
|
|
142
|
+
return { podUrl: opts.podUrl };
|
|
143
|
+
const saved = getLocalPodConfig();
|
|
144
|
+
if (saved?.podUrl) {
|
|
145
|
+
const spinner = ora(`Checking ${saved.podUrl}…`).start();
|
|
146
|
+
const health = await checkPodHealth(saved.podUrl);
|
|
147
|
+
if (health.healthy) {
|
|
148
|
+
spinner.succeed(`Pod: ${saved.podUrl}`);
|
|
149
|
+
return { podUrl: saved.podUrl, podId: saved.podId };
|
|
150
|
+
}
|
|
151
|
+
if (health.moved) {
|
|
152
|
+
spinner.fail();
|
|
153
|
+
throw new InitExit(INIT_EXIT.noPod, await movedPodLine(saved.podUrl, () => "Run: synap doctor --fix", true));
|
|
154
|
+
}
|
|
155
|
+
spinner.warn(`Your saved pod at ${saved.podUrl} is not answering.`);
|
|
174
156
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
157
|
+
const local = await detectLocalPod();
|
|
158
|
+
if (local) {
|
|
159
|
+
log.success(`Pod running on this machine: ${local}`);
|
|
160
|
+
return { podUrl: local };
|
|
179
161
|
}
|
|
162
|
+
if (opts.unattended)
|
|
163
|
+
throw new InitExit(INIT_EXIT.noPod, "No pod found. Pass --pod-url <url>.");
|
|
164
|
+
return noPodStep();
|
|
180
165
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
await pathBExisting(opts, existingUrl);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
// Resource check
|
|
195
|
-
const resources = checkServerResources();
|
|
196
|
-
log.info(`RAM: ${resources.ramTotal}MB total, ${resources.ramFree}MB free`);
|
|
197
|
-
log.info(`Disk: ${resources.diskFree} free`);
|
|
198
|
-
if (resources.ramFree < 1500) {
|
|
199
|
-
log.warn("Low RAM — Synap + OpenClaw need ~1.5GB. Performance may be affected.");
|
|
200
|
-
}
|
|
201
|
-
// What to install
|
|
202
|
-
const { installChoice } = await prompts({
|
|
166
|
+
/** Sign-in first (and the default): a person with a pod on synap.live must not land on "Create". */
|
|
167
|
+
export const NO_POD_CHOICES = [
|
|
168
|
+
{ title: "I have one on synap.live (sign in)", value: "login" },
|
|
169
|
+
{ title: "Create in browser", value: "create" },
|
|
170
|
+
{ title: "I have a URL", value: "url" },
|
|
171
|
+
{ title: "Self-host", value: "self-host" },
|
|
172
|
+
];
|
|
173
|
+
async function noPodStep() {
|
|
174
|
+
log.blank();
|
|
175
|
+
const { choice } = await prompts({
|
|
203
176
|
type: "select",
|
|
204
|
-
name: "
|
|
205
|
-
message: "
|
|
206
|
-
choices: [
|
|
207
|
-
|
|
208
|
-
title: "Synap pod + OpenClaw (full stack)",
|
|
209
|
-
description: "Recommended — everything on this server",
|
|
210
|
-
value: "bundle",
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
title: "Synap pod only",
|
|
214
|
-
description: "Add OpenClaw later",
|
|
215
|
-
value: "pod-only",
|
|
216
|
-
},
|
|
217
|
-
],
|
|
177
|
+
name: "choice",
|
|
178
|
+
message: "No Synap pod yet. Create one, or point me at yours?",
|
|
179
|
+
choices: [...NO_POD_CHOICES],
|
|
180
|
+
initial: 0,
|
|
218
181
|
});
|
|
219
|
-
if (!
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
182
|
+
if (!choice)
|
|
183
|
+
throw new InitExit(INIT_EXIT.cancelled, "Cancelled.");
|
|
184
|
+
if (choice === "create")
|
|
185
|
+
return createPodStep();
|
|
186
|
+
if (choice === "login") {
|
|
187
|
+
const picked = await loginAndSelectPod();
|
|
188
|
+
if (!picked)
|
|
189
|
+
throw new InitExit(INIT_EXIT.noPod, "No pod selected.");
|
|
190
|
+
return { podUrl: picked.url, podId: picked.podId || undefined };
|
|
191
|
+
}
|
|
192
|
+
if (choice === "url") {
|
|
193
|
+
const { url } = await prompts({ type: "text", name: "url", message: "Pod URL:" });
|
|
194
|
+
if (!url)
|
|
195
|
+
throw new InitExit(INIT_EXIT.cancelled, "Cancelled.");
|
|
196
|
+
const spinner = ora("Checking pod health...").start();
|
|
197
|
+
const health = await checkPodHealth(url);
|
|
198
|
+
if (health.healthy) {
|
|
199
|
+
spinner.succeed(`Pod: ${url}`);
|
|
200
|
+
return { podUrl: url };
|
|
238
201
|
}
|
|
239
|
-
|
|
240
|
-
|
|
202
|
+
if (health.moved) {
|
|
203
|
+
spinner.fail();
|
|
204
|
+
throw new InitExit(INIT_EXIT.noPod, await movedPodLine(url, (to) => `Run: synap init --pod-url ${to}`));
|
|
241
205
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
console.log(chalk.cyan("\n synap update\n"));
|
|
245
|
-
log.dim("Check progress: synap status");
|
|
246
|
-
return;
|
|
206
|
+
spinner.fail(unreachableMessage(url, health.failure));
|
|
207
|
+
throw new InitExit(INIT_EXIT.noPod, unreachableMessage(url, health.failure), true);
|
|
247
208
|
}
|
|
248
|
-
|
|
209
|
+
log.blank();
|
|
210
|
+
log.info("On your server, run:");
|
|
211
|
+
console.log(chalk.cyan("\n curl -fsSL https://synap.live/install.sh | bash\n"));
|
|
212
|
+
log.info("Then here: " + chalk.cyan("npx @synap-core/cli init --pod-url <your-pod-url>"));
|
|
213
|
+
throw new InitExit(INIT_EXIT.noPod, "No pod yet: self-host one, then run init with --pod-url.", true);
|
|
249
214
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
description: "Sign in via browser — auto-detect your pods",
|
|
263
|
-
value: "login",
|
|
264
|
-
},
|
|
265
|
-
{
|
|
266
|
-
title: "Connect to an existing pod (enter URL)",
|
|
267
|
-
description: "Self-hosted or managed — enter the URL directly",
|
|
268
|
-
value: "existing",
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
title: "I don't have a pod yet",
|
|
272
|
-
value: "none",
|
|
273
|
-
},
|
|
274
|
-
],
|
|
215
|
+
/** Open synap.live to create a pod, then continue here once it answers. */
|
|
216
|
+
async function createPodStep() {
|
|
217
|
+
log.info("Opening synap.live to create your pod…");
|
|
218
|
+
const spinner = ora("Waiting for you in the browser…").start();
|
|
219
|
+
const result = await createPodInBrowser({
|
|
220
|
+
waitForPodCallback: async () => {
|
|
221
|
+
const created = await waitForPodCallback();
|
|
222
|
+
if (created)
|
|
223
|
+
spinner.text = "Your pod is starting (usually ~2 min). I'll continue here.";
|
|
224
|
+
return created;
|
|
225
|
+
},
|
|
226
|
+
checkPodHealth,
|
|
275
227
|
});
|
|
276
|
-
if (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
choices: [
|
|
289
|
-
{
|
|
290
|
-
title: "Managed by Synap — €15/mo",
|
|
291
|
-
description: "We host it, zero ops",
|
|
292
|
-
value: "managed",
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
title: "Self-hosted on my VPS — FREE",
|
|
296
|
-
value: "vps",
|
|
297
|
-
},
|
|
298
|
-
],
|
|
299
|
-
});
|
|
300
|
-
if (createChoice === "managed") {
|
|
301
|
-
log.blank();
|
|
302
|
-
log.info("Opening synap.live to provision your pod...");
|
|
303
|
-
log.info(chalk.dim("(Waiting up to 5 minutes for provisioning to complete)"));
|
|
304
|
-
log.blank();
|
|
305
|
-
const spinner = ora("Waiting for pod provisioning...").start();
|
|
306
|
-
const result = await waitForPodCallback();
|
|
307
|
-
if (result) {
|
|
308
|
-
spinner.succeed(`Pod provisioned at ${chalk.cyan(result.podUrl)}`);
|
|
309
|
-
await connectExistingPod(result.podUrl, opts, "managed");
|
|
310
|
-
}
|
|
311
|
-
else {
|
|
312
|
-
spinner.fail("Pod provisioning timed out or was cancelled.");
|
|
313
|
-
log.blank();
|
|
314
|
-
log.info("You can resume at any time by running: " + chalk.cyan("synap init"));
|
|
315
|
-
log.info("Or connect manually: " + chalk.cyan(`synap connect --pod-url <your-pod-url>`));
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
else if (createChoice === "vps") {
|
|
319
|
-
log.blank();
|
|
320
|
-
log.info("SSH into your server and run:");
|
|
321
|
-
console.log(chalk.cyan("\n curl -fsSL https://raw.githubusercontent.com/Synap-core/backend/main/install.sh | bash\n"));
|
|
322
|
-
log.info("Then on that server: npx @synap/cli init");
|
|
323
|
-
}
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
if (hostChoice === "existing") {
|
|
327
|
-
const { url } = await prompts({
|
|
328
|
-
type: "text",
|
|
329
|
-
name: "url",
|
|
330
|
-
message: "Pod URL:",
|
|
331
|
-
initial: "https://pod.synap.live",
|
|
332
|
-
});
|
|
333
|
-
if (!url)
|
|
334
|
-
return;
|
|
335
|
-
const spinner = ora("Checking pod health...").start();
|
|
336
|
-
const status = await checkPodHealth(url);
|
|
337
|
-
if (!status.healthy) {
|
|
338
|
-
spinner.fail(`Pod not reachable at ${url}`);
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
spinner.succeed(`Pod healthy at ${url}`);
|
|
342
|
-
// Detect if self-hosted or managed by checking URL
|
|
343
|
-
const isSynapLive = url.includes("synap.live");
|
|
344
|
-
await connectExistingPod(url, opts, isSynapLive ? "managed" : "self-hosted");
|
|
345
|
-
}
|
|
228
|
+
if (result.ok) {
|
|
229
|
+
spinner.succeed(`Pod ready: ${result.podUrl}`);
|
|
230
|
+
return { podUrl: result.podUrl };
|
|
231
|
+
}
|
|
232
|
+
if (result.reason === "not-reachable") {
|
|
233
|
+
spinner.warn(`Your pod at ${result.podUrl} is still starting.`);
|
|
234
|
+
log.info("Pick up where you left off: " + chalk.cyan(`synap init --pod-url ${result.podUrl}`));
|
|
235
|
+
throw new InitExit(INIT_EXIT.noPod, `Your pod at ${result.podUrl} is still starting.`, true);
|
|
236
|
+
}
|
|
237
|
+
spinner.fail("No pod was created (the browser window timed out or was closed).");
|
|
238
|
+
log.info("Try again any time: " + chalk.cyan("synap init"));
|
|
239
|
+
throw new InitExit(INIT_EXIT.noPod, "No pod was created.", true);
|
|
346
240
|
}
|
|
347
241
|
/**
|
|
348
|
-
*
|
|
242
|
+
* This computer's key for this pod — the one `/setup/agent` accepts to mint an
|
|
243
|
+
* agent's key, and the CLI's own identity there. Reuses a saved profile for
|
|
244
|
+
* this pod while it works and is not about to expire; otherwise connects this
|
|
245
|
+
* computer through the pod's device flow (never without a person at the
|
|
246
|
+
* terminal: unattended runs pass --api-key).
|
|
247
|
+
*
|
|
248
|
+
* `grant` is set when the device flow ran now: its agents were approved with
|
|
249
|
+
* the computer, and their keys are already saved for their installers.
|
|
349
250
|
*/
|
|
350
|
-
async function
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const { ocChoice } = await prompts({
|
|
365
|
-
type: "select",
|
|
366
|
-
name: "ocChoice",
|
|
367
|
-
message: "OpenClaw not detected locally. What would you like to do?",
|
|
368
|
-
choices: [
|
|
369
|
-
{
|
|
370
|
-
title: "Enable OpenClaw on my pod server (free addon)",
|
|
371
|
-
description: "Runs alongside your pod via Docker — zero extra cost",
|
|
372
|
-
value: "addon",
|
|
373
|
-
},
|
|
374
|
-
{
|
|
375
|
-
title: "Install OpenClaw on this computer",
|
|
376
|
-
description: "npm i -g openclaw",
|
|
377
|
-
value: "local",
|
|
378
|
-
},
|
|
379
|
-
{ title: "Skip OpenClaw for now", value: "skip" },
|
|
380
|
-
],
|
|
381
|
-
});
|
|
382
|
-
if (ocChoice === "addon") {
|
|
383
|
-
if (podType === "self-hosted") {
|
|
384
|
-
log.blank();
|
|
385
|
-
log.info("SSH into your pod server and run:");
|
|
386
|
-
log.blank();
|
|
387
|
-
console.log(chalk.cyan(" cd /srv/synap && ./deploy/setup-openclaw.sh"));
|
|
388
|
-
log.blank();
|
|
389
|
-
log.info("This will start OpenClaw as a Docker addon on your pod.");
|
|
390
|
-
log.info("Then re-run: synap init --pod-url " + podUrl);
|
|
391
|
-
}
|
|
392
|
-
else {
|
|
393
|
-
// Managed pod — activate via CP (requires user session, not PROVISIONING_TOKEN)
|
|
394
|
-
log.info("Activating OpenClaw addon on your managed pod...");
|
|
395
|
-
const creds = getStoredToken();
|
|
396
|
-
try {
|
|
397
|
-
if (!creds)
|
|
398
|
-
throw new Error("Not logged in to CP");
|
|
399
|
-
await enableOpenClawAddonManaged(creds.token, podUrl);
|
|
400
|
-
log.success("OpenClaw addon provisioning started — may take a minute");
|
|
401
|
-
}
|
|
402
|
-
catch (err) {
|
|
403
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
404
|
-
if (msg.includes("422") || msg.includes("provisioned pod server")) {
|
|
405
|
-
// Pod doesn't have a managed server — OpenClaw can't run server-side
|
|
406
|
-
log.blank();
|
|
407
|
-
log.info("Your pod doesn't have a managed server for addons.");
|
|
408
|
-
log.info("Install OpenClaw locally instead:");
|
|
409
|
-
log.dim(" npm i -g openclaw && openclaw onboard");
|
|
410
|
-
log.info("Then re-run: " + chalk.cyan(`synap init --pod-url ${podUrl}`));
|
|
411
|
-
}
|
|
412
|
-
else {
|
|
413
|
-
log.warn("Could not activate OpenClaw automatically.");
|
|
414
|
-
log.dim(msg);
|
|
415
|
-
log.info("Enable it from: https://synap.live/account/pods");
|
|
416
|
-
}
|
|
417
|
-
}
|
|
251
|
+
async function ensurePodKey(podUrl, opts, podId, agents = []) {
|
|
252
|
+
if (opts.apiKey)
|
|
253
|
+
return { key: opts.apiKey, grant: null };
|
|
254
|
+
const norm = (u) => u.replace(/\/+$/, "");
|
|
255
|
+
const saved = listPodProfiles().find((p) => norm(p.config.podUrl) === norm(podUrl));
|
|
256
|
+
if (saved?.config.hubApiKey) {
|
|
257
|
+
const res = await fetch(`${norm(podUrl)}/api/hub/auth/status`, {
|
|
258
|
+
headers: { Authorization: `Bearer ${saved.config.hubApiKey}` },
|
|
259
|
+
signal: AbortSignal.timeout(15_000),
|
|
260
|
+
}).catch(() => null);
|
|
261
|
+
const renew = computerKeyNeedsRenewal(saved.config);
|
|
262
|
+
if (res?.ok && (!renew || opts.unattended)) {
|
|
263
|
+
if (renew) {
|
|
264
|
+
log.warn(`This computer's key for this pod expires ${saved.config.keyExpiresAt}. Renew it: ${chalk.cyan("synap login")}`);
|
|
418
265
|
}
|
|
266
|
+
if (!saved.active)
|
|
267
|
+
setActivePod(saved.name);
|
|
268
|
+
return { key: saved.config.hubApiKey, grant: null };
|
|
419
269
|
}
|
|
420
|
-
|
|
421
|
-
log.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
log.
|
|
425
|
-
|
|
270
|
+
if (res?.ok) {
|
|
271
|
+
log.info(`This computer's key for this pod expires ${saved.config.keyExpiresAt}. Renewing it.`);
|
|
272
|
+
}
|
|
273
|
+
else if (res && (res.status === 401 || res.status === 403)) {
|
|
274
|
+
log.warn("Your saved key for this pod no longer works. Connecting this computer again.");
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
throw new InitExit(INIT_EXIT.noKey, `Could not check your saved key for this pod (${res ? `HTTP ${res.status}` : "unreachable"}).`);
|
|
426
278
|
}
|
|
427
279
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
280
|
+
const { key, grant } = await connectStep(podUrl, opts, podId, agents);
|
|
281
|
+
if (key)
|
|
282
|
+
return { key, grant };
|
|
283
|
+
throw new InitExit(INIT_EXIT.noKey, opts.unattended
|
|
284
|
+
? "No key for this pod. Pass --api-key <key>: without a person at the terminal, nobody can approve this computer."
|
|
285
|
+
: "No key for this pod, so no agent can connect yet.");
|
|
286
|
+
}
|
|
287
|
+
/** A pod's profile name: its host name. Never another pod's profile. */
|
|
288
|
+
function podProfileName(podUrl) {
|
|
289
|
+
try {
|
|
290
|
+
return new URL(podUrl).hostname;
|
|
435
291
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
log.info("Pod connected. Once OpenClaw is running:");
|
|
439
|
-
log.dim(" Local: openclaw skills install synap");
|
|
440
|
-
log.dim(" Docker: docker exec openclaw openclaw skills install synap");
|
|
441
|
-
log.dim(" Or run: synap update");
|
|
292
|
+
catch {
|
|
293
|
+
return "default";
|
|
442
294
|
}
|
|
443
|
-
printSummary(podUrl, ocAfter.found);
|
|
444
295
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
296
|
+
/** Save the pod as a named profile (host name) and make it active. */
|
|
297
|
+
function rememberPod(config) {
|
|
298
|
+
const name = podProfileName(config.podUrl);
|
|
299
|
+
addPodProfile(name, config);
|
|
300
|
+
setActivePod(name);
|
|
301
|
+
}
|
|
302
|
+
async function connectAgentsStep(podUrl, humanKey, opts, requested) {
|
|
303
|
+
log.heading("Your agents");
|
|
304
|
+
const rows = detectAgents();
|
|
305
|
+
const found = rows.filter((r) => r.found);
|
|
306
|
+
const labelOf = (t) => rows.find((r) => r.target === t)?.label ?? t;
|
|
307
|
+
let picked;
|
|
308
|
+
if (requested) {
|
|
309
|
+
picked = requested;
|
|
310
|
+
}
|
|
311
|
+
else if (opts.unattended) {
|
|
312
|
+
picked = found.map((r) => r.target);
|
|
313
|
+
if (picked.length === 0)
|
|
314
|
+
log.info("No agent found on this machine.");
|
|
455
315
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
316
|
+
else {
|
|
317
|
+
log.info(found.length > 0
|
|
318
|
+
? `Found: ${found.map((r) => r.label).join(", ")}.`
|
|
319
|
+
: "No agent found on this machine. Pick one to set up anyway, or skip.");
|
|
320
|
+
const { targets } = await prompts({
|
|
321
|
+
type: "multiselect",
|
|
322
|
+
name: "targets",
|
|
323
|
+
message: found.length > 1 ? `Connect ${found.length === 2 ? "both" : "them"}?` : "Connect:",
|
|
324
|
+
choices: rows.map((r) => ({
|
|
325
|
+
title: r.label,
|
|
326
|
+
description: r.found ? r.evidence : "not found on this machine",
|
|
327
|
+
value: r.target,
|
|
328
|
+
selected: r.found,
|
|
329
|
+
})),
|
|
330
|
+
hint: "space to toggle, enter to confirm",
|
|
331
|
+
instructions: false,
|
|
332
|
+
});
|
|
333
|
+
picked = (targets ?? []);
|
|
461
334
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
{
|
|
468
|
-
title: "Login to Synap — connect to your existing pod",
|
|
469
|
-
description: "Sign in via browser and find your pod automatically",
|
|
470
|
-
value: "login",
|
|
471
|
-
},
|
|
472
|
-
{
|
|
473
|
-
title: "This machine (docker-compose) — FREE",
|
|
474
|
-
description: "Runs alongside OpenClaw, ~1.5GB RAM",
|
|
475
|
-
value: "local",
|
|
476
|
-
},
|
|
477
|
-
{
|
|
478
|
-
title: "Managed by Synap — €15/mo",
|
|
479
|
-
description: "We host it, you connect",
|
|
480
|
-
value: "managed",
|
|
481
|
-
},
|
|
482
|
-
{
|
|
483
|
-
title: "I already have a pod (enter URL)",
|
|
484
|
-
value: "existing",
|
|
485
|
-
},
|
|
486
|
-
],
|
|
335
|
+
// One click for every key that needs approval (V1 D5): mint them all first,
|
|
336
|
+
// open ONE review page, then install with the approved keys. The same batch
|
|
337
|
+
// door `synap agents add` uses — pod-wide, no scope.
|
|
338
|
+
const result = await connectAgentsBatch({ podUrl, apiKey: humanKey }, picked, {}, {
|
|
339
|
+
labelOf,
|
|
487
340
|
});
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const { url } = await prompts({
|
|
504
|
-
type: "text",
|
|
505
|
-
name: "url",
|
|
506
|
-
message: "Pod URL:",
|
|
507
|
-
});
|
|
508
|
-
if (!url)
|
|
509
|
-
return null;
|
|
510
|
-
const spinner = ora("Checking pod health...").start();
|
|
511
|
-
const status = await checkPodHealth(url);
|
|
512
|
-
if (status.healthy) {
|
|
513
|
-
spinner.succeed(`Pod healthy at ${url}`);
|
|
514
|
-
return url;
|
|
341
|
+
return { ...result, connectedLabels: result.connected.map(labelOf) };
|
|
342
|
+
}
|
|
343
|
+
/** Prints the QR for Relay; returns the link, or null when a phone can't use this pod. */
|
|
344
|
+
async function pairPhoneStep(podUrl) {
|
|
345
|
+
log.heading("Your phone");
|
|
346
|
+
const pair = buildPairLink({ podUrl, email: getStoredToken()?.email });
|
|
347
|
+
if (!pair.ok) {
|
|
348
|
+
if (pair.reason === "loopback") {
|
|
349
|
+
log.info("Pair later, once the pod has a public URL: " + chalk.cyan("synap init --pod-url https://…"));
|
|
350
|
+
}
|
|
351
|
+
else if (pair.reason === "insecure") {
|
|
352
|
+
log.info("Pair later, once the pod is served over https: " + chalk.cyan("synap init --pod-url https://…"));
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
log.info(`Can't make a pairing link for ${podUrl}.`);
|
|
515
356
|
}
|
|
516
|
-
spinner.fail(`Pod not reachable at ${url}`);
|
|
517
357
|
return null;
|
|
518
358
|
}
|
|
519
|
-
|
|
359
|
+
log.info("Scan with Relay so your agents can reach you. That's where their questions arrive.");
|
|
360
|
+
log.dim(`No Relay yet? ${RELAY_DOWNLOAD_URL}`);
|
|
361
|
+
console.log("\n" + (await renderTerminalQr(pair.link)));
|
|
362
|
+
log.dim(pair.link);
|
|
363
|
+
return pair.link;
|
|
520
364
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
log.
|
|
526
|
-
|
|
527
|
-
const { installDomain } = await prompts({
|
|
528
|
-
type: "text",
|
|
529
|
-
name: "installDomain",
|
|
530
|
-
message: "Domain for this pod (use localhost for local setup):",
|
|
531
|
-
initial: "localhost",
|
|
532
|
-
});
|
|
533
|
-
if (!installDomain)
|
|
534
|
-
return null;
|
|
535
|
-
let installEmail = "";
|
|
536
|
-
if (installDomain !== "localhost") {
|
|
537
|
-
const emailPrompt = await prompts({
|
|
538
|
-
type: "text",
|
|
539
|
-
name: "installEmail",
|
|
540
|
-
message: "Email for Let's Encrypt SSL certificates:",
|
|
541
|
-
});
|
|
542
|
-
installEmail = emailPrompt.installEmail ?? "";
|
|
543
|
-
if (!installEmail) {
|
|
544
|
-
log.error("Email is required for non-localhost domains.");
|
|
545
|
-
return null;
|
|
546
|
-
}
|
|
365
|
+
function printClosing(connected) {
|
|
366
|
+
log.blank();
|
|
367
|
+
if (connected.length === 0) {
|
|
368
|
+
console.log(" No agent connected yet. When you're ready: " + chalk.cyan("synap connect --target=claude-code"));
|
|
369
|
+
log.blank();
|
|
370
|
+
return;
|
|
547
371
|
}
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
const installCmd = installDomain === "localhost"
|
|
551
|
-
? `curl -fsSL https://raw.githubusercontent.com/Synap-core/backend/main/install.sh | bash -s -- --domain '${escapedDomain}'`
|
|
552
|
-
: `curl -fsSL https://raw.githubusercontent.com/Synap-core/backend/main/install.sh | bash -s -- --domain '${escapedDomain}' --email '${escapedEmail}'`;
|
|
372
|
+
const h = buildHandoff(connected);
|
|
373
|
+
console.log(chalk.green.bold(" ✓ Done.") + ` Try it now. In ${h.agentLabel}, say:`);
|
|
553
374
|
log.blank();
|
|
554
|
-
log.
|
|
375
|
+
console.log(chalk.bold(` "${h.prompt}"`));
|
|
555
376
|
log.blank();
|
|
556
|
-
|
|
377
|
+
log.dim("More:");
|
|
378
|
+
for (const m of h.more)
|
|
379
|
+
log.dim(` "${m}"`);
|
|
557
380
|
log.blank();
|
|
558
|
-
const { proceed } = await prompts({
|
|
559
|
-
type: "confirm",
|
|
560
|
-
name: "proceed",
|
|
561
|
-
message: "Run the installer now?",
|
|
562
|
-
initial: true,
|
|
563
|
-
});
|
|
564
|
-
if (proceed) {
|
|
565
|
-
try {
|
|
566
|
-
execSync(installCmd, { stdio: "inherit" });
|
|
567
|
-
return "http://localhost:4000";
|
|
568
|
-
}
|
|
569
|
-
catch {
|
|
570
|
-
log.error("Installation failed. Check the output above.");
|
|
571
|
-
return null;
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
log.dim("Run the command above manually, then: synap init");
|
|
575
|
-
return null;
|
|
576
381
|
}
|
|
577
|
-
|
|
578
|
-
|
|
382
|
+
/**
|
|
383
|
+
* This computer's own key for this pod: it mints each agent's key and is
|
|
384
|
+
* saved as a named profile.
|
|
385
|
+
*/
|
|
386
|
+
async function connectStep(podUrl, opts, podId,
|
|
387
|
+
/** Agents to ask the pod for along with this computer. */
|
|
388
|
+
agents = []) {
|
|
579
389
|
let apiKey = opts.apiKey;
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
try {
|
|
594
|
-
({ sessionToken } = await provisionUserOnPod(podUrl, creds.token));
|
|
595
|
-
}
|
|
596
|
-
catch (err) {
|
|
597
|
-
// Non-fatal here — the missing-session guard below decides what to do.
|
|
598
|
-
log.warn(`Could not provision user on pod: ${err instanceof Error ? err.message : String(err)}`);
|
|
599
|
-
}
|
|
600
|
-
if (!sessionToken) {
|
|
601
|
-
throw new Error("Could not establish a pod session to issue an agent key.");
|
|
602
|
-
}
|
|
603
|
-
// Canonical key issuance: mint a scoped Hub Protocol key via the pod's
|
|
604
|
-
// apiKeys.connectIntegration tRPC procedure — the same path the pod-admin
|
|
605
|
-
// /connect page and the browser use (replaces the removed CP relay).
|
|
606
|
-
const result = await setupAgentViaPod(podUrl, sessionToken, "openclaw");
|
|
607
|
-
apiKey = result.hubApiKey;
|
|
608
|
-
opts.apiKey = apiKey;
|
|
609
|
-
spinner.succeed("API key generated");
|
|
610
|
-
if (result.agentUserId) {
|
|
611
|
-
log.dim(`Agent user: ${result.agentUserId}`);
|
|
612
|
-
}
|
|
613
|
-
log.dim(`Workspace: ${result.workspaceId}`);
|
|
614
|
-
log.blank();
|
|
615
|
-
log.info("This key lets OpenClaw read/write your knowledge graph.");
|
|
616
|
-
log.info("It's scoped to Hub Protocol operations only.");
|
|
617
|
-
// Always save to ~/.synap/pod-config.json (works even without OpenClaw)
|
|
618
|
-
saveLocalPodConfig({
|
|
619
|
-
podUrl,
|
|
620
|
-
podId: podId ?? undefined,
|
|
621
|
-
workspaceId: result.workspaceId,
|
|
622
|
-
agentUserId: result.agentUserId,
|
|
623
|
-
hubApiKey: result.hubApiKey,
|
|
624
|
-
savedAt: new Date().toISOString(),
|
|
625
|
-
});
|
|
626
|
-
if (openclawFound) {
|
|
627
|
-
const config = readOpenClawConfig() ?? {};
|
|
628
|
-
setConfigValue(config, "synap.podUrl", podUrl);
|
|
629
|
-
setConfigValue(config, "synap.workspaceId", result.workspaceId);
|
|
630
|
-
setConfigValue(config, "synap.agentUserId", result.agentUserId);
|
|
631
|
-
writeOpenClawConfig(config);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
catch (err) {
|
|
635
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
636
|
-
spinner.fail(`Auto-generation failed: ${msg}`);
|
|
637
|
-
// Fall through to manual options below
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
if (!apiKey) {
|
|
641
|
-
// Manual path — not authenticated or auto-gen failed
|
|
642
|
-
const { keyChoice } = await prompts({
|
|
643
|
-
type: "select",
|
|
644
|
-
name: "keyChoice",
|
|
645
|
-
message: "How to get an API key?",
|
|
646
|
-
choices: [
|
|
647
|
-
{
|
|
648
|
-
title: "Generate via PROVISIONING_TOKEN",
|
|
649
|
-
description: "Use the token from your pod's .env file",
|
|
650
|
-
value: "generate",
|
|
651
|
-
},
|
|
652
|
-
{
|
|
653
|
-
title: "Paste an existing API key",
|
|
654
|
-
description: "From pod Settings → API Keys",
|
|
655
|
-
value: "paste",
|
|
656
|
-
},
|
|
657
|
-
],
|
|
658
|
-
});
|
|
659
|
-
if (keyChoice === "paste") {
|
|
660
|
-
const { key } = await prompts({
|
|
661
|
-
type: "password",
|
|
662
|
-
name: "key",
|
|
663
|
-
message: "Hub Protocol API key:",
|
|
664
|
-
});
|
|
665
|
-
apiKey = key;
|
|
666
|
-
}
|
|
667
|
-
else {
|
|
668
|
-
const { token } = await prompts({
|
|
669
|
-
type: "password",
|
|
670
|
-
name: "token",
|
|
671
|
-
message: "PROVISIONING_TOKEN (from pod .env):",
|
|
672
|
-
});
|
|
673
|
-
if (token) {
|
|
674
|
-
const spinner = ora("Creating agent credentials...").start();
|
|
675
|
-
try {
|
|
676
|
-
// Agents are pod-wide singletons now — setupAgent() never passed a
|
|
677
|
-
// workspaceId in its request either, so the response's workspaceId
|
|
678
|
-
// was already always null/"" in practice. Provision via the shared
|
|
679
|
-
// wrapper (no enrollAgentIfNeeded: a PROVISIONING_TOKEN can't
|
|
680
|
-
// authorize /workspaces/enroll-agent, same as before).
|
|
681
|
-
const result = await provisionAgentKey(podUrl, token, "openclaw");
|
|
682
|
-
apiKey = result.hubApiKey;
|
|
683
|
-
opts.apiKey = apiKey;
|
|
684
|
-
spinner.succeed("Credentials created");
|
|
685
|
-
log.dim(`Agent: ${result.agentUserId}`);
|
|
686
|
-
saveLocalPodConfig({
|
|
687
|
-
podUrl,
|
|
688
|
-
podId: podId ?? undefined,
|
|
689
|
-
workspaceId: "",
|
|
690
|
-
agentUserId: result.agentUserId,
|
|
691
|
-
hubApiKey: result.hubApiKey,
|
|
692
|
-
savedAt: new Date().toISOString(),
|
|
693
|
-
});
|
|
694
|
-
if (openclawFound) {
|
|
695
|
-
const config = readOpenClawConfig() ?? {};
|
|
696
|
-
setConfigValue(config, "synap.podUrl", podUrl);
|
|
697
|
-
setConfigValue(config, "synap.agentUserId", result.agentUserId);
|
|
698
|
-
writeOpenClawConfig(config);
|
|
699
|
-
}
|
|
700
|
-
if (result.agentUserId) {
|
|
701
|
-
try {
|
|
702
|
-
await configureAgentContext(podUrl, apiKey, "openclaw", result.agentUserId);
|
|
703
|
-
}
|
|
704
|
-
catch (err) {
|
|
705
|
-
log.warn(`Agent context wizard failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
catch (err) {
|
|
710
|
-
spinner.fail(err instanceof Error ? err.message : String(err));
|
|
711
|
-
return null;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
}
|
|
390
|
+
let grant = null;
|
|
391
|
+
// Nobody at the terminal to approve a code or paste a key.
|
|
392
|
+
if (!apiKey && opts.unattended)
|
|
393
|
+
return { key: null, grant: null };
|
|
394
|
+
// The pod's device flow: a code the person approves on any device. This
|
|
395
|
+
// computer gets its own key; the agents asked for get theirs in the same step.
|
|
396
|
+
let deviceTried = false;
|
|
397
|
+
let deviceAvailable = true;
|
|
398
|
+
const connectComputer = async () => {
|
|
399
|
+
deviceTried = true;
|
|
400
|
+
let outcome;
|
|
401
|
+
try {
|
|
402
|
+
outcome = await deviceConnect(podUrl, { agents });
|
|
715
403
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
return apiKey ?? null;
|
|
722
|
-
}
|
|
723
|
-
export async function skillStep(openclawFound, oc) {
|
|
724
|
-
if (!openclawFound)
|
|
725
|
-
return;
|
|
726
|
-
log.heading("Install Skill");
|
|
727
|
-
const isDocker = oc?.runtime === "docker";
|
|
728
|
-
const containerName = oc?.containerName;
|
|
729
|
-
if (isDocker) {
|
|
730
|
-
log.dim(`Installing via docker exec ${containerName ?? "openclaw"}...`);
|
|
731
|
-
}
|
|
732
|
-
const spinner = ora("Installing synap skill...").start();
|
|
733
|
-
try {
|
|
734
|
-
installSynapSkill(isDocker ? (containerName ?? "openclaw") : undefined);
|
|
735
|
-
spinner.succeed("Synap skill installed");
|
|
736
|
-
}
|
|
737
|
-
catch (err) {
|
|
738
|
-
spinner.fail(err instanceof Error ? err.message : "Failed");
|
|
739
|
-
if (isDocker) {
|
|
740
|
-
log.dim(`Install manually: docker exec ${containerName ?? "openclaw"} openclaw skills install synap`);
|
|
404
|
+
catch (err) {
|
|
405
|
+
if (err instanceof DeviceStartError && err.status === 404)
|
|
406
|
+
deviceAvailable = false;
|
|
407
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
408
|
+
return;
|
|
741
409
|
}
|
|
742
|
-
|
|
743
|
-
log.
|
|
410
|
+
if (outcome.status !== "approved") {
|
|
411
|
+
log.error(outcome.message);
|
|
412
|
+
return;
|
|
744
413
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
const statusRes = await fetch(`${podUrl}/api/provision/status`, {
|
|
766
|
-
signal: AbortSignal.timeout(5000),
|
|
767
|
-
}).catch(() => null);
|
|
768
|
-
if (statusRes?.ok) {
|
|
769
|
-
const data = (await statusRes.json());
|
|
770
|
-
const svc = data?.intelligenceService;
|
|
771
|
-
isActive = svc?.status === "active";
|
|
772
|
-
isUrl = svc?.url;
|
|
414
|
+
grant = outcome.grant;
|
|
415
|
+
apiKey = grant.computer.apiKey;
|
|
416
|
+
opts.apiKey = apiKey;
|
|
417
|
+
await saveDeviceGrant(podUrl, grant, { name: podProfileName(podUrl), podId });
|
|
418
|
+
setActivePod(podProfileName(podUrl));
|
|
419
|
+
log.success(`${grant.computer.name} connected`);
|
|
420
|
+
};
|
|
421
|
+
if (!apiKey)
|
|
422
|
+
await connectComputer();
|
|
423
|
+
// No key yet: connecting this computer again first, the advanced ways after.
|
|
424
|
+
const managed = isManagedPod(podUrl);
|
|
425
|
+
while (!apiKey) {
|
|
426
|
+
const { keyChoice } = (await prompts({
|
|
427
|
+
type: "select",
|
|
428
|
+
name: "keyChoice",
|
|
429
|
+
message: "How do you want to connect to this pod?",
|
|
430
|
+
choices: keyChoices({ managed, deviceTried, deviceAvailable }),
|
|
431
|
+
}));
|
|
432
|
+
if (keyChoice === "device") {
|
|
433
|
+
await connectComputer();
|
|
773
434
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
778
|
-
if (isActive) {
|
|
779
|
-
spinner.succeed(`Intelligence Service active${isUrl ? ` (${isUrl})` : ""}`);
|
|
780
|
-
}
|
|
781
|
-
else {
|
|
782
|
-
spinner.stop();
|
|
783
|
-
}
|
|
784
|
-
// 2. If IS is not active, try to provision via CP (if user is logged in)
|
|
785
|
-
if (!isActive) {
|
|
786
|
-
const creds = getStoredToken();
|
|
787
|
-
if (creds) {
|
|
788
|
-
const cpUrl = process.env.SYNAP_CP_URL ?? "https://api.synap.live";
|
|
789
|
-
try {
|
|
790
|
-
const pods = await listPods(creds.token);
|
|
791
|
-
const matchingPod = pods.find((p) => (p.podUrl ?? p.url ?? "").replace(/\/+$/, "") === podUrl.replace(/\/+$/, ""));
|
|
792
|
-
if (matchingPod) {
|
|
793
|
-
const podId = matchingPod.id;
|
|
794
|
-
const provStatus = await fetch(`${cpUrl}/intelligence/provision/status/${podId}`, { headers: { Authorization: `Bearer ${creds.token}` } }).catch(() => null);
|
|
795
|
-
const provData = provStatus?.ok
|
|
796
|
-
? (await provStatus.json())
|
|
797
|
-
: null;
|
|
798
|
-
if (provData?.subscribed && !provData?.cpProvisioned) {
|
|
799
|
-
const { provision } = await prompts({
|
|
800
|
-
type: "confirm",
|
|
801
|
-
name: "provision",
|
|
802
|
-
message: "Your subscription includes AI. Provision Intelligence Service on this pod?",
|
|
803
|
-
initial: true,
|
|
804
|
-
});
|
|
805
|
-
if (provision) {
|
|
806
|
-
const provSpinner = ora("Provisioning Intelligence Service...").start();
|
|
807
|
-
try {
|
|
808
|
-
const res = await fetch(`${cpUrl}/intelligence/provision/${podId}`, {
|
|
809
|
-
method: "POST",
|
|
810
|
-
headers: {
|
|
811
|
-
Authorization: `Bearer ${creds.token}`,
|
|
812
|
-
"Content-Type": "application/json",
|
|
813
|
-
},
|
|
814
|
-
});
|
|
815
|
-
if (res.ok) {
|
|
816
|
-
provSpinner.succeed("Intelligence Service provisioned successfully");
|
|
817
|
-
isActive = true;
|
|
818
|
-
}
|
|
819
|
-
else {
|
|
820
|
-
const err = (await res.json().catch(() => null));
|
|
821
|
-
provSpinner.fail(`Provisioning failed: ${err?.error ?? res.status}`);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
catch (e) {
|
|
825
|
-
provSpinner.fail(`Provisioning error: ${e instanceof Error ? e.message : "unknown"}`);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
else if (!provData?.subscribed) {
|
|
830
|
-
log.dim("No AI subscription — subscribe at https://synap.live/pricing");
|
|
831
|
-
log.dim("Skip this step with: synap update --skip-is");
|
|
832
|
-
}
|
|
833
|
-
else if (provData?.cpProvisioned) {
|
|
834
|
-
log.dim("IS provisioned on CP but not yet confirmed on pod.");
|
|
835
|
-
log.dim("Re-run 'synap update' in a minute, or reprovision from Browser Settings.");
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
else {
|
|
839
|
-
// Logged in but pod not found on CP account — self-hosted pod
|
|
840
|
-
log.dim("Intelligence Service not configured on this pod.");
|
|
841
|
-
log.dim("To enable: connect this pod to your Synap account at https://synap.live/account/pods");
|
|
842
|
-
log.dim("Then subscribe to a plan with AI and provision from Browser Settings > Add-ons.");
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
catch {
|
|
846
|
-
log.dim("Could not reach Synap to check IS status — skipping.");
|
|
847
|
-
}
|
|
435
|
+
else if (keyChoice === "paste") {
|
|
436
|
+
apiKey = await pasteKey(podUrl, podId);
|
|
437
|
+
break;
|
|
848
438
|
}
|
|
849
439
|
else {
|
|
850
|
-
|
|
851
|
-
log.dim("Intelligence Service not active on this pod.");
|
|
852
|
-
log.blank();
|
|
853
|
-
log.dim("To enable AI routing via Synap:");
|
|
854
|
-
log.dim(" 1. Log in: synap login --token <token> (get token at synap.live/account/tokens)");
|
|
855
|
-
log.dim(" 2. Re-run: synap update");
|
|
856
|
-
log.blank();
|
|
857
|
-
log.dim("Or skip AI for now: synap update --skip-is");
|
|
440
|
+
break;
|
|
858
441
|
}
|
|
859
442
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
443
|
+
return { key: apiKey ?? null, grant };
|
|
444
|
+
}
|
|
445
|
+
/** Ask for a key the person copies from their pod; saved as this pod's profile. */
|
|
446
|
+
async function pasteKey(podUrl, podId) {
|
|
447
|
+
const { key } = await prompts({
|
|
448
|
+
type: "password",
|
|
449
|
+
name: "key",
|
|
450
|
+
message: "API key:",
|
|
451
|
+
});
|
|
452
|
+
if (key) {
|
|
453
|
+
rememberPod({
|
|
454
|
+
podUrl,
|
|
455
|
+
podId: podId ?? undefined,
|
|
456
|
+
workspaceId: "",
|
|
457
|
+
agentUserId: "",
|
|
458
|
+
hubApiKey: key,
|
|
459
|
+
savedAt: new Date().toISOString(),
|
|
868
460
|
});
|
|
869
|
-
if (configureOc) {
|
|
870
|
-
// Fetch real model IDs from the pod so OpenClaw shows actual provider names
|
|
871
|
-
const { fetchPodProviders } = await import("./providers.js");
|
|
872
|
-
const podProviders = await fetchPodProviders(podUrl, apiKey);
|
|
873
|
-
const models = podProviders.flatMap((p) => p.models.map((m) => ({
|
|
874
|
-
id: `${p.providerId}/${m.id}`,
|
|
875
|
-
name: `${p.name} — ${m.id}`,
|
|
876
|
-
...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
|
|
877
|
-
})));
|
|
878
|
-
if (models.length === 0) {
|
|
879
|
-
log.warn("No model IDs found — configure models in pod admin → Intelligence → Providers.");
|
|
880
|
-
log.dim("Skipping OpenClaw AI provider setup. Re-run: synap update");
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
const synapProvider = {
|
|
884
|
-
baseUrl: `${podUrl}/v1`,
|
|
885
|
-
api: "openai-completions",
|
|
886
|
-
apiKey,
|
|
887
|
-
models,
|
|
888
|
-
};
|
|
889
|
-
if (ocRuntime.runtime === "docker") {
|
|
890
|
-
const containerName = ocRuntime.containerName ?? "openclaw";
|
|
891
|
-
const spinner = ora("Writing Synap IS provider to OpenClaw config...").start();
|
|
892
|
-
try {
|
|
893
|
-
execSync(`docker exec ${containerName} openclaw config set models.providers.synap ${JSON.stringify(JSON.stringify(synapProvider))}`, { stdio: "pipe", timeout: 15000 });
|
|
894
|
-
spinner.succeed("Synap IS registered as OpenClaw provider");
|
|
895
|
-
log.dim(`Models: ${models.slice(0, 3).map((m) => m.id).join(", ")}${models.length > 3 ? ` +${models.length - 3} more` : ""}`);
|
|
896
|
-
try {
|
|
897
|
-
execSync(`docker restart ${containerName}`, { stdio: "pipe", timeout: 30000 });
|
|
898
|
-
log.dim("Container restarted to pick up new config");
|
|
899
|
-
}
|
|
900
|
-
catch {
|
|
901
|
-
log.warn("Restart failed — run manually: docker restart openclaw");
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
catch (err) {
|
|
905
|
-
const stderr = err.stderr?.toString().trim();
|
|
906
|
-
spinner.fail("Failed to set Synap IS provider");
|
|
907
|
-
if (stderr)
|
|
908
|
-
log.dim(stderr);
|
|
909
|
-
log.dim(` docker exec ${containerName} openclaw config set models.providers.synap.baseUrl ${podUrl}/v1`);
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
else {
|
|
913
|
-
const config = readOpenClawConfig() ?? {};
|
|
914
|
-
setConfigValue(config, "models.providers.synap", synapProvider);
|
|
915
|
-
writeOpenClawConfig(config);
|
|
916
|
-
log.success("Synap IS configured as OpenClaw provider — restart OpenClaw to apply");
|
|
917
|
-
log.dim(`Models: ${models.slice(0, 3).map((m) => m.id).join(", ")}${models.length > 3 ? ` +${models.length - 3} more` : ""}`);
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
461
|
}
|
|
462
|
+
return key || undefined;
|
|
921
463
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
464
|
+
/**
|
|
465
|
+
* `synap login` with no pod saved: sign in to the Synap account, pick one of
|
|
466
|
+
* its pods, issue this machine's key and save the profile — the same steps as
|
|
467
|
+
* init's "sign in" path. Returns the pod URL, or null when no pod came of it.
|
|
468
|
+
*/
|
|
469
|
+
export async function signInAndSavePod() {
|
|
470
|
+
try {
|
|
471
|
+
const pod = await loginAndSelectPod();
|
|
472
|
+
if (!pod)
|
|
473
|
+
return null;
|
|
474
|
+
const wanted = detectAgents().filter((r) => r.found).map((r) => r.target);
|
|
475
|
+
const { key, grant } = await ensurePodKey(pod.url, {}, pod.podId || undefined, wanted);
|
|
476
|
+
// Approved with the computer: write each agent's own key into its config.
|
|
477
|
+
if (grant)
|
|
478
|
+
await wireApprovedAgents(pod.url, key, grant);
|
|
479
|
+
return pod.url;
|
|
936
480
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
log.info("Once it's ready, run:");
|
|
944
|
-
log.blank();
|
|
945
|
-
console.log(chalk.cyan(" synap update"));
|
|
946
|
-
log.blank();
|
|
947
|
-
log.dim("This will install the skill, seed your workspace, and configure AI routing.");
|
|
948
|
-
log.dim("Check progress: synap status");
|
|
481
|
+
catch (err) {
|
|
482
|
+
if (!(err instanceof InitExit))
|
|
483
|
+
throw err;
|
|
484
|
+
if (!err.shown)
|
|
485
|
+
log.error(err.message);
|
|
486
|
+
return null;
|
|
949
487
|
}
|
|
950
488
|
}
|
|
951
489
|
async function loginAndSelectPod() {
|
|
952
490
|
// Check if already logged in
|
|
953
491
|
const authStatus = await isLoggedIn();
|
|
954
|
-
if (
|
|
955
|
-
log.
|
|
956
|
-
|
|
492
|
+
if (authStatus.valid) {
|
|
493
|
+
log.success(`Already logged in as ${authStatus.email}`);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
log.info("You are not currently logged in to Synap.");
|
|
497
|
+
}
|
|
498
|
+
let storedToken = getStoredToken();
|
|
499
|
+
if (!storedToken || !authStatus.valid) {
|
|
500
|
+
// No token or not valid — need to login
|
|
501
|
+
log.info("Sign in to Synap and pick your pod.");
|
|
957
502
|
const creds = await login();
|
|
958
503
|
if (!creds) {
|
|
959
|
-
|
|
504
|
+
log.error("Sign-in timed out or failed.");
|
|
960
505
|
log.dim("Try again or use --pod-url to connect directly");
|
|
961
506
|
return null;
|
|
962
507
|
}
|
|
963
|
-
|
|
508
|
+
log.success(`Signed in as ${creds.email}`);
|
|
964
509
|
// If the web flow already performed pod selection, use that result directly.
|
|
965
510
|
if (creds.podUrl && creds.podId) {
|
|
966
511
|
const healthSpinner = ora("Checking pod health...").start();
|
|
@@ -969,39 +514,58 @@ async function loginAndSelectPod() {
|
|
|
969
514
|
healthSpinner.succeed(`Pod ready at ${creds.podUrl}`);
|
|
970
515
|
return { url: creds.podUrl, podId: creds.podId };
|
|
971
516
|
}
|
|
972
|
-
healthSpinner.warn(`Pod selected (${creds.podUrl}) but not yet reachable
|
|
517
|
+
healthSpinner.warn(`Pod selected (${creds.podUrl}) but not yet reachable. It may still be provisioning.`);
|
|
973
518
|
return { url: creds.podUrl, podId: creds.podId };
|
|
974
519
|
}
|
|
520
|
+
// After login, re-fetch stored token (login() writes credentials)
|
|
521
|
+
storedToken = getStoredToken();
|
|
975
522
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
const token = getStoredToken();
|
|
980
|
-
if (!token)
|
|
523
|
+
// We have a valid token
|
|
524
|
+
if (!storedToken) {
|
|
525
|
+
log.warn("No stored credentials found. Please sign in.");
|
|
981
526
|
return null;
|
|
527
|
+
}
|
|
982
528
|
// Short-circuit if credentials already carry a pod from a previous web-based selection.
|
|
983
|
-
if (
|
|
984
|
-
|
|
529
|
+
if (storedToken.podUrl && storedToken.podId) {
|
|
530
|
+
log.info(`You have a previously selected pod: ${storedToken.podUrl}`);
|
|
531
|
+
const { useExisting } = await prompts({
|
|
532
|
+
type: "confirm",
|
|
533
|
+
name: "useExisting",
|
|
534
|
+
message: "Use this pod?",
|
|
535
|
+
initial: true,
|
|
536
|
+
});
|
|
537
|
+
if (useExisting) {
|
|
538
|
+
const healthSpinner = ora("Checking pod health...").start();
|
|
539
|
+
const status = await checkPodHealth(storedToken.podUrl);
|
|
540
|
+
if (status.healthy) {
|
|
541
|
+
healthSpinner.succeed(`Pod healthy at ${storedToken.podUrl}`);
|
|
542
|
+
return { url: storedToken.podUrl, podId: storedToken.podId };
|
|
543
|
+
}
|
|
544
|
+
healthSpinner.fail(`Pod not reachable at ${storedToken.podUrl}`);
|
|
545
|
+
log.blank();
|
|
546
|
+
}
|
|
547
|
+
// Fall through to pod selection if user doesn't want to use existing pod or pod is unreachable
|
|
985
548
|
}
|
|
986
549
|
// List pods
|
|
987
550
|
const podsSpinner = ora("Fetching your pods...").start();
|
|
988
551
|
try {
|
|
989
|
-
const pods = await listPods(
|
|
552
|
+
const pods = await listPods(storedToken.token);
|
|
990
553
|
if (pods.length === 0) {
|
|
991
|
-
podsSpinner.info("No pods
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
log.info("Then re-run: " + chalk.dim("synap init"));
|
|
995
|
-
return null;
|
|
554
|
+
podsSpinner.info("No pods on your account yet");
|
|
555
|
+
const created = await createPodStep();
|
|
556
|
+
return created ? { url: created.podUrl, podId: "" } : null;
|
|
996
557
|
}
|
|
997
558
|
podsSpinner.succeed(`Found ${pods.length} pod(s)`);
|
|
559
|
+
log.blank();
|
|
560
|
+
log.info("Your available pods:");
|
|
998
561
|
if (pods.length === 1) {
|
|
999
562
|
const pod = pods[0];
|
|
1000
|
-
const podUrl = pod
|
|
563
|
+
const podUrl = podUrlOf(pod);
|
|
564
|
+
log.dim(` • ${podUrl} (${pod.status})`);
|
|
1001
565
|
const { connect } = await prompts({
|
|
1002
566
|
type: "confirm",
|
|
1003
567
|
name: "connect",
|
|
1004
|
-
message: `Connect to
|
|
568
|
+
message: `Connect to this pod?`,
|
|
1005
569
|
initial: true,
|
|
1006
570
|
});
|
|
1007
571
|
if (!connect)
|
|
@@ -1021,17 +585,16 @@ async function loginAndSelectPod() {
|
|
|
1021
585
|
name: "selectedPodUrl",
|
|
1022
586
|
message: "Which pod do you want to connect to?",
|
|
1023
587
|
choices: pods.map((pod) => {
|
|
1024
|
-
const podUrl = pod
|
|
588
|
+
const podUrl = podUrlOf(pod);
|
|
1025
589
|
return {
|
|
1026
|
-
title: `${pod.subdomain}
|
|
1027
|
-
description: podUrl,
|
|
590
|
+
title: `${pod.subdomain} · ${new URL(podUrl).host} (${pod.status})`,
|
|
1028
591
|
value: podUrl,
|
|
1029
592
|
};
|
|
1030
593
|
}),
|
|
1031
594
|
});
|
|
1032
595
|
if (!selectedPodUrl)
|
|
1033
596
|
return null;
|
|
1034
|
-
const selectedPod = pods.find((p) => (p
|
|
597
|
+
const selectedPod = pods.find((p) => podUrlOf(p) === selectedPodUrl);
|
|
1035
598
|
const healthSpinner = ora("Checking pod health...").start();
|
|
1036
599
|
const status = await checkPodHealth(selectedPodUrl);
|
|
1037
600
|
if (status.healthy) {
|
|
@@ -1049,83 +612,13 @@ async function loginAndSelectPod() {
|
|
|
1049
612
|
// =============================================================================
|
|
1050
613
|
// HELPERS
|
|
1051
614
|
// =============================================================================
|
|
1052
|
-
function
|
|
1053
|
-
try {
|
|
1054
|
-
const platform = process.platform;
|
|
1055
|
-
if (platform !== "linux")
|
|
1056
|
-
return false;
|
|
1057
|
-
execSync("docker info >/dev/null 2>&1", { timeout: 5000 });
|
|
1058
|
-
return true;
|
|
1059
|
-
}
|
|
1060
|
-
catch {
|
|
1061
|
-
return false;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
/**
|
|
1065
|
-
* Does the environment carry ANY signal that a Synap pod exists somewhere?
|
|
1066
|
-
* Used by `init` to decide whether to run the full flow or short-circuit
|
|
1067
|
-
* with a "no pod detected" instruction message.
|
|
1068
|
-
*
|
|
1069
|
-
* Treat these as signals (in cheap-to-expensive order):
|
|
1070
|
-
* 1. SYNAP_POD_URL env var
|
|
1071
|
-
* 2. A stored ~/.synap/pod-config.json from a previous run
|
|
1072
|
-
* 3. A running pod detected on this machine
|
|
1073
|
-
* 4. An OpenClaw install that already has synap.podUrl configured
|
|
1074
|
-
*/
|
|
1075
|
-
async function hasAnyPodSignal() {
|
|
1076
|
-
if (process.env.SYNAP_POD_URL)
|
|
1077
|
-
return true;
|
|
1078
|
-
if (getLocalPodConfig()?.podUrl)
|
|
1079
|
-
return true;
|
|
1080
|
-
// OpenClaw may already carry a pod URL in its config — respect that.
|
|
1081
|
-
try {
|
|
1082
|
-
const oc = detectOpenClaw();
|
|
1083
|
-
const ocSynap = oc.config?.synap;
|
|
1084
|
-
if (typeof ocSynap?.podUrl === "string" && ocSynap.podUrl.length > 0)
|
|
1085
|
-
return true;
|
|
1086
|
-
}
|
|
1087
|
-
catch {
|
|
1088
|
-
// ignore — detection is best-effort
|
|
1089
|
-
}
|
|
1090
|
-
// Last: probe the local machine. This is the expensive step.
|
|
1091
|
-
const local = await detectLocalPod();
|
|
1092
|
-
return Boolean(local);
|
|
1093
|
-
}
|
|
1094
|
-
function printNoPodInstructions() {
|
|
1095
|
-
const line = "─".repeat(63);
|
|
1096
|
-
console.log(`
|
|
1097
|
-
┌${line}┐
|
|
1098
|
-
│ No Synap pod detected. │
|
|
1099
|
-
│ │
|
|
1100
|
-
│ You need a running pod before using @synap/cli init. │
|
|
1101
|
-
│ │
|
|
1102
|
-
│ Options: │
|
|
1103
|
-
│ │
|
|
1104
|
-
│ 1. Self-host on a server (free): │
|
|
1105
|
-
│ On your server, run: │
|
|
1106
|
-
│ curl -fsSL https://synap.live/install.sh | bash │
|
|
1107
|
-
│ Then come back and run: │
|
|
1108
|
-
│ npx @synap-core/cli init --pod-url https://your-pod... │
|
|
1109
|
-
│ │
|
|
1110
|
-
│ 2. Use a hosted pod ($): │
|
|
1111
|
-
│ Sign up at https://synap.live │
|
|
1112
|
-
│ Then run: │
|
|
1113
|
-
│ npx @synap-core/cli init --pod-url <your-pod-url> │
|
|
1114
|
-
│ │
|
|
1115
|
-
│ 3. Already running locally? Pass --pod-url: │
|
|
1116
|
-
│ npx @synap-core/cli init --pod-url http://localhost:4000 │
|
|
1117
|
-
└${line}┘
|
|
1118
|
-
`);
|
|
1119
|
-
}
|
|
1120
|
-
function printUnreachablePodInstructions(podUrl) {
|
|
615
|
+
function printUnreachablePodInstructions(podUrl, failure) {
|
|
1121
616
|
console.log("");
|
|
1122
|
-
log.error(
|
|
617
|
+
log.error(unreachableMessage(podUrl, failure));
|
|
1123
618
|
log.blank();
|
|
1124
619
|
log.info("Checklist:");
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
log.dim(" - Is there a firewall or reverse proxy in the way?");
|
|
1128
|
-
log.dim(" - For localhost setups, did you start the stack? ./synap start");
|
|
620
|
+
for (const line of reachChecklist(failure))
|
|
621
|
+
log.dim(` ${line}`);
|
|
1129
622
|
log.blank();
|
|
1130
623
|
log.info("Once the pod is reachable, re-run:");
|
|
1131
624
|
log.dim(` npx @synap-core/cli init --pod-url ${podUrl}`);
|
|
@@ -1145,6 +638,13 @@ async function detectLocalPod() {
|
|
|
1145
638
|
// ── 1. Env var ──────────────────────────────────────────────────────────
|
|
1146
639
|
if (process.env.SYNAP_POD_URL)
|
|
1147
640
|
return process.env.SYNAP_POD_URL;
|
|
641
|
+
return probeThisMachineForPod();
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Steps 2-4 of detectLocalPod: a pod actually running on this machine. The
|
|
645
|
+
* env var is left out on purpose: it points at a pod, often a remote one.
|
|
646
|
+
*/
|
|
647
|
+
async function probeThisMachineForPod() {
|
|
1148
648
|
const candidates = [];
|
|
1149
649
|
// ── 2. Deploy dir scan ──────────────────────────────────────────────────
|
|
1150
650
|
// Look for a .env that has PUBLIC_URL or DOMAIN, which tells us the
|