@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/import.js
CHANGED
|
@@ -1,34 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* synap import — "capture, but for files and URLs".
|
|
2
|
+
* synap import — "capture, but for files and URLs" (personal OS intake).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* POST /capture/structure
|
|
7
|
-
*
|
|
4
|
+
* Routing (INTAKE-CONSOLIDATION-PLAN Wave 1):
|
|
5
|
+
* - Superwhisper store-first → lib/superwhisper-import (unchanged)
|
|
6
|
+
* - Single file OR single URL → POST /capture/structure + /capture/execute
|
|
7
|
+
* - Any .csv (even ONE) → its own `source: csv` batch on the table
|
|
8
|
+
* door below (a CSV is a table, never prose)
|
|
9
|
+
* - 2 text-like files
|
|
10
|
+
* (md / markdown / txt / csv) → ONE batch via SYNCHRONOUS REST import:
|
|
11
|
+
* POST /import/analyze → human review (or --yes / --dry-run)
|
|
12
|
+
* POST /import/apply → materialize the EXACT previewed ops
|
|
13
|
+
* - ≥CORPUS_THRESHOLD (3) such
|
|
14
|
+
* files → ONE BACKGROUND job (the sync door times out):
|
|
15
|
+
* POST /import/enqueue-corpus → { jobId }, returns immediately
|
|
16
|
+
* GET /import/corpus-job/{id} → poll to "completed"
|
|
17
|
+
* On completion the result is ONE pending governed `import.graph`
|
|
18
|
+
* proposal — NOT stored entities. `--dry-run` never takes this door
|
|
19
|
+
* (enqueueing is itself a write).
|
|
20
|
+
* - Mixed binary/URL + text → capture path for URLs/binaries;
|
|
21
|
+
* batch text via /import/* when N≥2
|
|
8
22
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
23
|
+
* Directory walks cap at DIR_FILE_CAP (sync analyze max); truncate is warned,
|
|
24
|
+
* never silent. After expansion, batch-eligible text is also capped at
|
|
25
|
+
* GLOBAL_BATCH_CAP (same ceiling) across all inputs — not only per-directory.
|
|
26
|
+
* Workspace/project from --workspace / --project (or cfg);
|
|
27
|
+
* session from --session (active session id).
|
|
11
28
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
29
|
+
* Optional --home-map rewrites batch item paths so the backend path heuristic
|
|
30
|
+
* (workspace **name** as a path segment) pins multi-home destinations. Example:
|
|
31
|
+
* --home-map "Projects=Builder,Posts=Content OS"
|
|
32
|
+
* rewrites `5. Projects/foo.md` → `Builder/5. Projects/foo.md`. No product
|
|
33
|
+
* defaults — user-supplied pairs only. It applies to the corpus door too: the
|
|
34
|
+
* background job forwards `items` verbatim into the SAME
|
|
35
|
+
* `ImportOrchestrator.analyzeLarge` the sync door reaches, so the path
|
|
36
|
+
* heuristic is identical on both.
|
|
16
37
|
*
|
|
17
|
-
*
|
|
18
|
-
* AI-proposed targetWorkspaceId / targetProjectId from /capture/structure win.
|
|
38
|
+
* No Company OS coupling — pure personal intake doors.
|
|
19
39
|
*/
|
|
20
40
|
import chalk from "chalk";
|
|
21
41
|
import { readFileSync, statSync, readdirSync, existsSync } from "node:fs";
|
|
22
|
-
import { extname, basename, join } from "node:path";
|
|
42
|
+
import { extname, basename, join, relative } from "node:path";
|
|
23
43
|
import * as readline from "node:readline/promises";
|
|
24
44
|
import { stdin as input, stdout as output } from "node:process";
|
|
25
45
|
import { log } from "../utils/logger.js";
|
|
26
|
-
import { resolveHubConfig, resolveUserId, hubGet, hubPost,
|
|
46
|
+
import { resolveHubConfig, resolveUserId, hubGet, hubPost, resolveActiveSessionId, renderHubError, } from "../lib/hub-client.js";
|
|
27
47
|
import { unwrapList } from "../lib/unwrapList.js";
|
|
48
|
+
import { pollForApproval } from "../lib/approval-poll.js";
|
|
49
|
+
import { FLOW, renderNextSteps } from "../lib/next-steps.js";
|
|
50
|
+
import { fetchEffectiveIdentity, isAgentReviewRejection, proposalWebLink, } from "../lib/credential-class.js";
|
|
28
51
|
import { formatLaneLine, resolveWorkspaceName } from "../lib/capture-lane.js";
|
|
29
|
-
import { isDegraded, degradedMessage, readCaptureExecute, } from "../lib/capture-structure.js";
|
|
52
|
+
import { isDegraded, degradedMessage, readCaptureExecute, formatWorkspaceRouting, formatPendingWorkspaceSwitch, headlessPlacementFields, projectRoutingHints, workspaceRoutingHints, } from "../lib/capture-structure.js";
|
|
30
53
|
// ─── input classification ─────────────────────────────────────────────────────
|
|
31
54
|
const TEXT_LIKE = new Set([".md", ".markdown", ".txt", ".csv", ".json", ".html", ".htm"]);
|
|
55
|
+
/** Extensions eligible for the multi-file REST /import/analyze batch path. */
|
|
56
|
+
const BATCH_TEXT_EXTS = new Set([".md", ".markdown", ".txt", ".csv"]);
|
|
32
57
|
const MIME = {
|
|
33
58
|
".md": "text/markdown",
|
|
34
59
|
".markdown": "text/markdown",
|
|
@@ -55,26 +80,72 @@ const MIME = {
|
|
|
55
80
|
};
|
|
56
81
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
57
82
|
const URL_RE = /^https?:\/\//i;
|
|
58
|
-
/**
|
|
59
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Cap on files pulled from a single directory — log + truncate, never silently drop.
|
|
85
|
+
* Aligned with sync /import/analyze max items (2000).
|
|
86
|
+
*/
|
|
87
|
+
const DIR_FILE_CAP = 2000;
|
|
88
|
+
/**
|
|
89
|
+
* Global cap on batch-eligible text items across ALL inputs (not only one dir).
|
|
90
|
+
* Same ceiling as sync /import/analyze max items.
|
|
91
|
+
*/
|
|
92
|
+
const GLOBAL_BATCH_CAP = 2000;
|
|
93
|
+
/** Generous timeout: deep vault structure can take minutes on large corpora. */
|
|
94
|
+
const IMPORT_ANALYZE_TIMEOUT_MS = 300_000;
|
|
95
|
+
const IMPORT_APPLY_TIMEOUT_MS = 120_000;
|
|
96
|
+
/**
|
|
97
|
+
* At or above this many batch-eligible text files, route to the BACKGROUND
|
|
98
|
+
* corpus door (`POST /import/enqueue-corpus`) instead of the synchronous
|
|
99
|
+
* `/import/analyze`.
|
|
100
|
+
*
|
|
101
|
+
* Why 3, measured rather than guessed: 3 markdown files complete in ~2m01s on
|
|
102
|
+
* the background worker, and the SAME 3 files fail 0/5 through /import/analyze,
|
|
103
|
+
* each dying at 45–48s against the pod's 45s request timeout. So 3 is the
|
|
104
|
+
* smallest N at which the synchronous door is observed to be unusable.
|
|
105
|
+
*
|
|
106
|
+
* Below it (1–2 files) the synchronous path stays: it is proven to work at that
|
|
107
|
+
* size and it is strictly more interactive — it renders the operation list, the
|
|
108
|
+
* multi-home placement and the quality report, then applies on a y/N confirm.
|
|
109
|
+
* The corpus door can offer none of that; it returns a job handle and files ONE
|
|
110
|
+
* governed proposal for later review. Routing everything through it would trade
|
|
111
|
+
* a working interactive preview for a background job on inputs that never
|
|
112
|
+
* needed one.
|
|
113
|
+
*/
|
|
114
|
+
export const CORPUS_THRESHOLD = 3;
|
|
115
|
+
/** Gap between corpus-job polls. 12 req/min — well under the hub RPM budget. */
|
|
116
|
+
const CORPUS_POLL_INTERVAL_MS = 5_000;
|
|
117
|
+
/** Heartbeat cadence for the human progress line (poll ticks are quieter). */
|
|
118
|
+
const CORPUS_PROGRESS_EVERY_MS = 30_000;
|
|
119
|
+
/** Observed worker throughput: 3 files → ~2m01s ≈ 40s/file. Rounded up. */
|
|
120
|
+
const CORPUS_MS_PER_ITEM = 45_000;
|
|
121
|
+
/** Never wait less than this, even for a 3-file corpus. */
|
|
122
|
+
const CORPUS_MIN_WAIT_MS = 10 * 60_000;
|
|
123
|
+
/**
|
|
124
|
+
* Never block a terminal longer than this. Hitting it is NOT a failure — the
|
|
125
|
+
* job keeps running server-side and the CLI prints how to re-poll.
|
|
126
|
+
*/
|
|
127
|
+
const CORPUS_MAX_WAIT_MS = 2 * 60 * 60_000;
|
|
60
128
|
function mimeFor(path) {
|
|
61
129
|
return MIME[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
62
130
|
}
|
|
63
131
|
/** Read a single file into an ImportItem (utf8 for text-like, base64 otherwise). */
|
|
64
|
-
function fileToItem(path) {
|
|
132
|
+
function fileToItem(path, displayPath) {
|
|
65
133
|
const ext = extname(path).toLowerCase();
|
|
66
134
|
const filename = basename(path);
|
|
67
135
|
const mimeType = mimeFor(path);
|
|
136
|
+
const rel = displayPath ?? path;
|
|
68
137
|
if (TEXT_LIKE.has(ext)) {
|
|
69
138
|
return {
|
|
70
139
|
kind: "file",
|
|
71
|
-
label:
|
|
140
|
+
label: rel,
|
|
141
|
+
path: rel,
|
|
72
142
|
file: { content: readFileSync(path, "utf8"), mimeType, filename, encoding: "utf8" },
|
|
73
143
|
};
|
|
74
144
|
}
|
|
75
145
|
return {
|
|
76
146
|
kind: "file",
|
|
77
|
-
label:
|
|
147
|
+
label: rel,
|
|
148
|
+
path: rel,
|
|
78
149
|
file: { content: readFileSync(path).toString("base64"), mimeType, filename, encoding: "base64" },
|
|
79
150
|
};
|
|
80
151
|
}
|
|
@@ -138,7 +209,9 @@ function expandInputs(inputs) {
|
|
|
138
209
|
truncated = true;
|
|
139
210
|
for (const f of files) {
|
|
140
211
|
try {
|
|
141
|
-
|
|
212
|
+
// Preserve vault-relative paths so wikilink/relation resolution works.
|
|
213
|
+
const rel = relative(raw, f) || basename(f);
|
|
214
|
+
items.push(fileToItem(f, rel));
|
|
142
215
|
}
|
|
143
216
|
catch (e) {
|
|
144
217
|
failures.push({ label: f, error: e.message });
|
|
@@ -156,8 +229,151 @@ function expandInputs(inputs) {
|
|
|
156
229
|
}
|
|
157
230
|
return { items, failures, truncated };
|
|
158
231
|
}
|
|
159
|
-
|
|
160
|
-
|
|
232
|
+
/** True when this item can join a multi-file /import/analyze batch. */
|
|
233
|
+
function isBatchTextItem(item) {
|
|
234
|
+
if (item.kind !== "file")
|
|
235
|
+
return false;
|
|
236
|
+
if (item.file.encoding !== "utf8")
|
|
237
|
+
return false;
|
|
238
|
+
const ext = extname(item.path).toLowerCase() || extname(item.file.filename).toLowerCase();
|
|
239
|
+
return BATCH_TEXT_EXTS.has(ext);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Source heuristic for the batch door:
|
|
243
|
+
* - any .csv → "csv"
|
|
244
|
+
* - mostly .md/.markdown → "markdown"
|
|
245
|
+
* - default → "markdown"
|
|
246
|
+
*/
|
|
247
|
+
function sourceForBatchItems(items) {
|
|
248
|
+
if (items.some((i) => i.path.toLowerCase().endsWith(".csv")))
|
|
249
|
+
return "csv";
|
|
250
|
+
const mdCount = items.filter((i) => /\.(md|markdown)$/i.test(i.path)).length;
|
|
251
|
+
if (mdCount * 2 >= items.length)
|
|
252
|
+
return "markdown";
|
|
253
|
+
return "markdown";
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Which batch-eligible files go through the table/batch door, and in which
|
|
257
|
+
* batches:
|
|
258
|
+
* - every CSV, even a lone one, in ONE `source: csv` batch. A CSV is a TABLE:
|
|
259
|
+
* the batch door builds a column→kind plan for it, while the single-file
|
|
260
|
+
* capture door reads it as free text (a 1,500-row contacts export came back
|
|
261
|
+
* as one degraded note filed in the wrong space). Keeping CSVs apart also
|
|
262
|
+
* stops a mixed md + csv batch from parsing the markdown as CSV.
|
|
263
|
+
* - the other text files in ONE batch when there are ≥2; a lone one stays on
|
|
264
|
+
* the capture door (unchanged).
|
|
265
|
+
*/
|
|
266
|
+
export function groupBatchFiles(files) {
|
|
267
|
+
const isCsv = (f) => f.path.toLowerCase().endsWith(".csv");
|
|
268
|
+
const csv = files.filter(isCsv);
|
|
269
|
+
const text = files.filter((f) => !isCsv(f));
|
|
270
|
+
const batches = [];
|
|
271
|
+
if (csv.length > 0)
|
|
272
|
+
batches.push(csv);
|
|
273
|
+
if (text.length >= 2)
|
|
274
|
+
batches.push(text);
|
|
275
|
+
return { batches, single: text.length >= 2 ? [] : text };
|
|
276
|
+
}
|
|
277
|
+
function toBatchTextItem(item) {
|
|
278
|
+
return {
|
|
279
|
+
path: item.path,
|
|
280
|
+
content: item.file.content,
|
|
281
|
+
label: item.label,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Parse `--home-map "Projects=Builder,Posts=Content OS"` into pairs.
|
|
286
|
+
* Values may be workspace names or UUIDs (resolved later).
|
|
287
|
+
*/
|
|
288
|
+
export function parseHomeMap(raw) {
|
|
289
|
+
const pairs = [];
|
|
290
|
+
// Split on commas that separate pairs. Values may contain spaces but not commas.
|
|
291
|
+
for (const part of raw.split(",")) {
|
|
292
|
+
const trimmed = part.trim();
|
|
293
|
+
if (!trimmed)
|
|
294
|
+
continue;
|
|
295
|
+
const eq = trimmed.indexOf("=");
|
|
296
|
+
if (eq <= 0 || eq === trimmed.length - 1) {
|
|
297
|
+
throw new Error(`Invalid --home-map entry '${trimmed}'. Expected pathSubstring=workspaceNameOrId ` +
|
|
298
|
+
`(e.g. Projects=Builder,Posts=Content OS).`);
|
|
299
|
+
}
|
|
300
|
+
const pathSubstring = trimmed.slice(0, eq).trim();
|
|
301
|
+
const workspace = trimmed.slice(eq + 1).trim();
|
|
302
|
+
if (!pathSubstring || !workspace) {
|
|
303
|
+
throw new Error(`Invalid --home-map entry '${trimmed}'. Both pathSubstring and workspace are required.`);
|
|
304
|
+
}
|
|
305
|
+
pairs.push({ pathSubstring, workspace });
|
|
306
|
+
}
|
|
307
|
+
if (pairs.length === 0) {
|
|
308
|
+
throw new Error(`Empty --home-map. Expected pathSubstring=workspaceNameOrId pairs ` +
|
|
309
|
+
`(e.g. Projects=Builder,Posts=Content OS).`);
|
|
310
|
+
}
|
|
311
|
+
return pairs;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Resolve home-map values to workspace **names** (id → name when UUID given).
|
|
315
|
+
* Backend path heuristic matches segment equality against workspace names.
|
|
316
|
+
*/
|
|
317
|
+
async function resolveHomeMap(pairs, cfg) {
|
|
318
|
+
const res = (await hubGet("/workspaces", {}, cfg));
|
|
319
|
+
const list = unwrapList(res, ["workspaces"]);
|
|
320
|
+
const names = list.map((w) => String(w.name ?? w.id)).join(", ");
|
|
321
|
+
return pairs.map(({ pathSubstring, workspace }) => {
|
|
322
|
+
if (UUID_RE.test(workspace)) {
|
|
323
|
+
const match = list.find((w) => String(w.id) === workspace);
|
|
324
|
+
if (!match) {
|
|
325
|
+
throw new Error(`Home-map workspace id '${workspace}' not found.${names ? ` Available: ${names}` : ""}`);
|
|
326
|
+
}
|
|
327
|
+
const workspaceName = String(match.name ?? "").trim();
|
|
328
|
+
if (!workspaceName) {
|
|
329
|
+
throw new Error(`Home-map workspace '${workspace}' has no name — cannot rewrite paths.`);
|
|
330
|
+
}
|
|
331
|
+
return { pathSubstring, workspaceName };
|
|
332
|
+
}
|
|
333
|
+
const match = list.find((w) => String(w.name ?? "").toLowerCase() === workspace.toLowerCase());
|
|
334
|
+
if (!match) {
|
|
335
|
+
throw new Error(`Home-map workspace '${workspace}' not found.${names ? ` Available: ${names}` : ""}`);
|
|
336
|
+
}
|
|
337
|
+
return { pathSubstring, workspaceName: String(match.name) };
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* If `path` contains any map pathSubstring (case-insensitive), prefix with the
|
|
342
|
+
* resolved workspace name so deep-structure segment match pins that home.
|
|
343
|
+
* First matching pair wins. Skips rewrite when the path already starts with the
|
|
344
|
+
* workspace name segment.
|
|
345
|
+
*/
|
|
346
|
+
export function applyHomeMapToPath(path, map) {
|
|
347
|
+
if (map.length === 0 || !path)
|
|
348
|
+
return path;
|
|
349
|
+
const lower = path.toLowerCase();
|
|
350
|
+
for (const { pathSubstring, workspaceName } of map) {
|
|
351
|
+
if (!pathSubstring)
|
|
352
|
+
continue;
|
|
353
|
+
if (!lower.includes(pathSubstring.toLowerCase()))
|
|
354
|
+
continue;
|
|
355
|
+
const prefix = workspaceName + "/";
|
|
356
|
+
// Already rewritten or vault already uses workspace name as root.
|
|
357
|
+
if (path === workspaceName ||
|
|
358
|
+
path.startsWith(prefix) ||
|
|
359
|
+
path.toLowerCase().startsWith(prefix.toLowerCase())) {
|
|
360
|
+
return path;
|
|
361
|
+
}
|
|
362
|
+
return `${workspaceName}/${path}`;
|
|
363
|
+
}
|
|
364
|
+
return path;
|
|
365
|
+
}
|
|
366
|
+
/** Apply home-map path rewrite to batch text items (labels stay original). */
|
|
367
|
+
function applyHomeMapToBatchItems(items, map) {
|
|
368
|
+
if (map.length === 0)
|
|
369
|
+
return items;
|
|
370
|
+
return items.map((i) => {
|
|
371
|
+
const next = applyHomeMapToPath(i.path, map);
|
|
372
|
+
if (next === i.path)
|
|
373
|
+
return i;
|
|
374
|
+
return { ...i, path: next };
|
|
375
|
+
});
|
|
376
|
+
}
|
|
161
377
|
// ─── workspace / project name → id resolution ─────────────────────────────────
|
|
162
378
|
/** Resolve a --workspace value (id or name) to a workspace id. Throws if unknown. */
|
|
163
379
|
async function resolveWorkspaceArg(value, cfg) {
|
|
@@ -221,17 +437,176 @@ function pct(c) {
|
|
|
221
437
|
}
|
|
222
438
|
/** Print the AI's proposed workspace/project routing for this item. */
|
|
223
439
|
function renderRouting(result, wsOverride, projOverride) {
|
|
224
|
-
|
|
440
|
+
// The pod's NAME for its pick, with its confidence once (never a raw id
|
|
441
|
+
// when the name is known). An override is the user's own id, shown as given.
|
|
442
|
+
const ws = wsOverride ? `${wsOverride} (override)` : formatWorkspaceRouting(result);
|
|
225
443
|
const proj = projOverride ?? result.targetProjectId ?? null;
|
|
226
|
-
const wsSrc = wsOverride ? " (override)" : pct(result.targetWorkspaceConfidence);
|
|
227
444
|
const projSrc = projOverride ? " (override)" : pct(result.targetProjectConfidence);
|
|
228
|
-
log.dim(` → workspace: ${ws ?? "pod-wide / unclear"}
|
|
445
|
+
log.dim(` → workspace: ${ws ?? "pod-wide / unclear"}`);
|
|
229
446
|
if (result.targetWorkspaceReason && !wsOverride)
|
|
230
447
|
log.dim(` ${result.targetWorkspaceReason.slice(0, 90)}`);
|
|
231
448
|
log.dim(` → project: ${proj ?? "none / unclear"}${projSrc}`);
|
|
232
449
|
if (result.targetProjectReason && !projOverride)
|
|
233
450
|
log.dim(` ${result.targetProjectReason.slice(0, 90)}`);
|
|
234
451
|
}
|
|
452
|
+
function renderBatchAnalyze(result, fileCount, source) {
|
|
453
|
+
const ops = result.operations ?? [];
|
|
454
|
+
log.heading(`▸ Batch import (${fileCount} file${fileCount !== 1 ? "s" : ""}, source=${source})`);
|
|
455
|
+
if (result.summary)
|
|
456
|
+
log.info(` ${result.summary}`);
|
|
457
|
+
log.dim(` ${ops.length} operation${ops.length !== 1 ? "s" : ""}` +
|
|
458
|
+
(result.mode ? ` · mode ${result.mode}` : "") +
|
|
459
|
+
(result.proposalId ? ` · proposal ${result.proposalId}` : "") +
|
|
460
|
+
(result.sessionId ? ` · session ${result.sessionId}` : ""));
|
|
461
|
+
if (result.stats && typeof result.stats === "object") {
|
|
462
|
+
const s = result.stats;
|
|
463
|
+
const bits = [];
|
|
464
|
+
if (s.entityCount != null)
|
|
465
|
+
bits.push(`${s.entityCount} entities`);
|
|
466
|
+
if (s.relationCount != null)
|
|
467
|
+
bits.push(`${s.relationCount} relations`);
|
|
468
|
+
if (s.itemCount != null)
|
|
469
|
+
bits.push(`${s.itemCount} items`);
|
|
470
|
+
if (s.typeCount != null)
|
|
471
|
+
bits.push(`${s.typeCount} types`);
|
|
472
|
+
if (s.itemsProcessed != null)
|
|
473
|
+
bits.push(`${s.itemsProcessed} processed`);
|
|
474
|
+
if (bits.length > 0)
|
|
475
|
+
log.dim(` stats: ${bits.join(" · ")}`);
|
|
476
|
+
}
|
|
477
|
+
// Multi-home: show where create_entity ops will land (per-op targetWorkspaceId).
|
|
478
|
+
const homes = result.homes;
|
|
479
|
+
if (homes && typeof homes === "object") {
|
|
480
|
+
const parts = [];
|
|
481
|
+
for (const [wid, n] of Object.entries(homes.byWorkspace ?? {})) {
|
|
482
|
+
parts.push(`${wid.slice(0, 8)}…×${n}`);
|
|
483
|
+
}
|
|
484
|
+
if (homes.podWide && homes.podWide > 0)
|
|
485
|
+
parts.push(`pod-wide×${homes.podWide}`);
|
|
486
|
+
if (parts.length > 0) {
|
|
487
|
+
log.info(` homes${homes.multiHome ? " (multi)" : ""}: ${parts.join(" · ")}`);
|
|
488
|
+
}
|
|
489
|
+
const proj = homes.byProject ?? {};
|
|
490
|
+
const projParts = Object.entries(proj).map(([pid, n]) => `${pid.slice(0, 8)}…×${n}`);
|
|
491
|
+
if (projParts.length > 0)
|
|
492
|
+
log.dim(` projects: ${projParts.join(" · ")}`);
|
|
493
|
+
}
|
|
494
|
+
else if (result.workspaceId) {
|
|
495
|
+
log.dim(` → workspace: ${result.workspaceId}`);
|
|
496
|
+
}
|
|
497
|
+
// Quality report — refuse / improve / apply loop
|
|
498
|
+
const q = result.quality;
|
|
499
|
+
if (q && typeof q.score === "number") {
|
|
500
|
+
log.info(` ${q.summary ?? `quality ${q.score}/100`}`);
|
|
501
|
+
const findings = q.findings ?? [];
|
|
502
|
+
for (const f of findings.slice(0, 8)) {
|
|
503
|
+
const tag = (f.severity ?? "info").toUpperCase();
|
|
504
|
+
log.dim(` [${tag}] ${f.message ?? f.id}`);
|
|
505
|
+
}
|
|
506
|
+
const ups = q.nextUpgrades ?? [];
|
|
507
|
+
if (ups.length > 0) {
|
|
508
|
+
log.info(" next upgrades:");
|
|
509
|
+
for (const u of ups.slice(0, 5))
|
|
510
|
+
log.dim(` → ${u}`);
|
|
511
|
+
}
|
|
512
|
+
if (result.proposalId) {
|
|
513
|
+
log.dim(` proposal ${result.proposalId} stores quality+ops — refuse in UI, re-import with upgrades, apply when score looks good`);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Build the outcome for an item the IS structurer could not structure.
|
|
519
|
+
*
|
|
520
|
+
* Carries `degradedReason` so `--json` reports the same cause the human path
|
|
521
|
+
* already prints via `degradedMessage()`. Without it a scripted import saw
|
|
522
|
+
* `{"status":"degraded"}` with no way to tell an auth error from an empty
|
|
523
|
+
* result — the reason existed on the wire and was dropped at this one seam.
|
|
524
|
+
*/
|
|
525
|
+
export function degradedOutcome(label, res, workspaceId, projectId) {
|
|
526
|
+
return {
|
|
527
|
+
label,
|
|
528
|
+
status: "degraded",
|
|
529
|
+
workspaceId,
|
|
530
|
+
projectId,
|
|
531
|
+
...(res.degradedReason ? { degradedReason: res.degradedReason } : {}),
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* The exit code for a finished import run.
|
|
536
|
+
*
|
|
537
|
+
* A degraded item produced ZERO entities — the IS structurer was down and the
|
|
538
|
+
* CLI deliberately created nothing. Exiting 0 on that told a scripted backfill
|
|
539
|
+
* "success" while most of its input was silently lost, so `degraded` is a
|
|
540
|
+
* non-zero outcome exactly like `failed`.
|
|
541
|
+
*
|
|
542
|
+
* Everything else stays 0, including a clean `--dry-run` (nothing was meant to
|
|
543
|
+
* be written), a `skipped` item (the user declined) and an `empty` one (the
|
|
544
|
+
* structurer ran fine and honestly found nothing to create).
|
|
545
|
+
*
|
|
546
|
+
* Scoped to `import` on purpose: this is `import`'s own final line, not a
|
|
547
|
+
* shared helper, so no other command's exit semantics change.
|
|
548
|
+
*/
|
|
549
|
+
export function importExitCode(outcomes) {
|
|
550
|
+
return outcomes.some((o) => o.status === "failed" || o.status === "degraded") ? 1 : 0;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* The `keepRaw` half of a `/capture/execute` body — `{}` unless `--keep-raw`
|
|
554
|
+
* was passed for a FILE item.
|
|
555
|
+
*
|
|
556
|
+
* Three things it must get right, all of which the pod silently tolerates:
|
|
557
|
+
* • `content` MUST be base64. `fileToItem` reads text-like files as utf8, and
|
|
558
|
+
* the pod does `Buffer.from(content, "base64")` unconditionally — sending
|
|
559
|
+
* utf8 there stores a mangled blob with no error.
|
|
560
|
+
* • `extractedText` is echoed from the structure response so the stored
|
|
561
|
+
* document lands with a real v1 body. Skipping it leaves
|
|
562
|
+
* `document_versions.content` empty, which blinds the embedding worker, the
|
|
563
|
+
* retrieval body join and Typesense enrichment.
|
|
564
|
+
* • A URL item has no bytes to keep, so `keepRaw` is simply not sent — the pod
|
|
565
|
+
* would accept `keepRaw: true` with no file and do nothing.
|
|
566
|
+
*/
|
|
567
|
+
export function buildKeepRawPayload(opts, item, structureRes) {
|
|
568
|
+
if (!opts.keepRaw || item.kind !== "file")
|
|
569
|
+
return {};
|
|
570
|
+
const content = item.file.encoding === "base64"
|
|
571
|
+
? item.file.content
|
|
572
|
+
: Buffer.from(item.file.content, "utf8").toString("base64");
|
|
573
|
+
const extractedText = structureRes.extraction?.text;
|
|
574
|
+
return {
|
|
575
|
+
keepRaw: true,
|
|
576
|
+
file: {
|
|
577
|
+
content,
|
|
578
|
+
mimeType: item.file.mimeType,
|
|
579
|
+
filename: item.file.filename,
|
|
580
|
+
...(extractedText ? { extractedText } : {}),
|
|
581
|
+
...(structureRes.extraction?.textTruncated
|
|
582
|
+
? { extractedTextTruncated: true }
|
|
583
|
+
: {}),
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* One line describing what ACTUALLY happened to a kept original. The pod
|
|
589
|
+
* reports four dispositions and they are not interchangeable: a
|
|
590
|
+
* governance-parked attach is not a save, and a policy denial is not a storage
|
|
591
|
+
* failure. Returns null when nothing was kept (so nothing is claimed).
|
|
592
|
+
*/
|
|
593
|
+
export function keepRawLine(res) {
|
|
594
|
+
const sf = res.sourceFile;
|
|
595
|
+
if (!sf)
|
|
596
|
+
return null;
|
|
597
|
+
switch (sf.status) {
|
|
598
|
+
case "stored":
|
|
599
|
+
return "original kept and linked to the entity";
|
|
600
|
+
case "proposed":
|
|
601
|
+
return `original awaiting review${sf.reviewUrl ? ` — ${sf.reviewUrl}` : ""}`;
|
|
602
|
+
case "denied":
|
|
603
|
+
return `original NOT kept — policy declined it${sf.reason ? `: ${sf.reason}` : ""}`;
|
|
604
|
+
case "failed":
|
|
605
|
+
return "original NOT kept — the pod could not store it";
|
|
606
|
+
default:
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
235
610
|
async function processItem(item, opts, cfg, userId, wsOverride, projOverride, sessionId, rl) {
|
|
236
611
|
// 1. Structure — workspace hint = override ?? active (the AI may still re-route).
|
|
237
612
|
const structureBody = {
|
|
@@ -247,6 +622,8 @@ async function processItem(item, opts, cfg, userId, wsOverride, projOverride, se
|
|
|
247
622
|
// give structure a generous timeout (capture's free-text path is fast).
|
|
248
623
|
const structureRes = (await hubPost("/capture/structure", structureBody, cfg, 120_000));
|
|
249
624
|
const wsTarget = wsOverride ?? structureRes.targetWorkspaceId ?? cfg.workspaceId ?? null;
|
|
625
|
+
// What the receipt reports for the project: the user's pin, else the AI's
|
|
626
|
+
// suggestion. Only the PIN is sent as `projectId` (see the execute body).
|
|
250
627
|
const projTarget = projOverride ?? structureRes.targetProjectId ?? null;
|
|
251
628
|
const proposals = structureRes.proposals ?? [];
|
|
252
629
|
// Degraded: the IS structurer is down. Create nothing (same as `synap
|
|
@@ -257,7 +634,7 @@ async function processItem(item, opts, cfg, userId, wsOverride, projOverride, se
|
|
|
257
634
|
log.heading(`▸ ${item.label}`);
|
|
258
635
|
log.warn(` ${degradedMessage(structureRes)}`);
|
|
259
636
|
}
|
|
260
|
-
return
|
|
637
|
+
return degradedOutcome(item.label, structureRes, wsTarget, projTarget);
|
|
261
638
|
}
|
|
262
639
|
if (!opts.json) {
|
|
263
640
|
log.heading(`▸ ${item.label}`);
|
|
@@ -294,9 +671,12 @@ async function processItem(item, opts, cfg, userId, wsOverride, projOverride, se
|
|
|
294
671
|
// forward the ambient workspace + the AI's routing signal and let the
|
|
295
672
|
// backend decide (auto = AI target wins over ambient when confidence is
|
|
296
673
|
// high enough AND the user is a member).
|
|
297
|
-
//
|
|
298
|
-
//
|
|
299
|
-
//
|
|
674
|
+
// The AI's advice goes through the ONE structure → execute mapper
|
|
675
|
+
// (`captureExecuteRoutingHints`, mirrored in capture-structure.ts) as
|
|
676
|
+
// ADVISORY hints. Project: only an explicit --project is a `projectId`
|
|
677
|
+
// pin; the AI's project pick is the advisory `aiProjectId` — promoting it
|
|
678
|
+
// to `projectId` linked it outright (widening cross-workspace access)
|
|
679
|
+
// before the user ever confirmed it.
|
|
300
680
|
const executeBody = {
|
|
301
681
|
userId,
|
|
302
682
|
...(wsOverride
|
|
@@ -304,14 +684,20 @@ async function processItem(item, opts, cfg, userId, wsOverride, projOverride, se
|
|
|
304
684
|
: {
|
|
305
685
|
...(cfg.workspaceId ? { workspaceId: cfg.workspaceId } : {}),
|
|
306
686
|
workspaceRouting: "auto",
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
687
|
+
// Headless door: the structure step's PLACEMENT decides what is
|
|
688
|
+
// pinned (the ONE derivation, shared with the pod's headless doors).
|
|
689
|
+
// An AI suggestion stays a proposal (`workspaceChoice: "ignored"`).
|
|
690
|
+
...headlessPlacementFields(structureRes),
|
|
691
|
+
...workspaceRoutingHints(structureRes),
|
|
310
692
|
}),
|
|
311
|
-
...(
|
|
693
|
+
...(projOverride ? { projectId: projOverride } : projectRoutingHints(structureRes)),
|
|
312
694
|
...(sessionId ? { sessionId } : {}),
|
|
313
695
|
entities: proposals,
|
|
314
696
|
relations: structureRes.relations ?? [],
|
|
697
|
+
// --keep-raw: preserve the ORIGINAL artifact, not just what the AI read out
|
|
698
|
+
// of it. Without this the imported bytes are discarded, so the created
|
|
699
|
+
// entity has no `documentId` and no source document at all.
|
|
700
|
+
...buildKeepRawPayload(opts, item, structureRes),
|
|
315
701
|
};
|
|
316
702
|
const executeRes = (await hubPost("/capture/execute", executeBody, cfg, 60_000));
|
|
317
703
|
// Report what the pod ACTUALLY returned. A workspace policy can queue the
|
|
@@ -331,9 +717,10 @@ async function processItem(item, opts, cfg, userId, wsOverride, projOverride, se
|
|
|
331
717
|
}
|
|
332
718
|
const entityCount = outcome.entitiesCreated;
|
|
333
719
|
const relCount = outcome.relationsCreated;
|
|
334
|
-
// Final resting workspace: the
|
|
335
|
-
//
|
|
336
|
-
|
|
720
|
+
// Final resting workspace: the pre-execute target. The ladder NEVER moves a
|
|
721
|
+
// capture on an AI guess (rung 5 proposes), so there is nothing to override
|
|
722
|
+
// it with — a pending suggestion is reported separately, below.
|
|
723
|
+
const finalWorkspaceId = wsTarget;
|
|
337
724
|
if (!opts.json) {
|
|
338
725
|
const report = {
|
|
339
726
|
lane: "work",
|
|
@@ -345,11 +732,21 @@ async function processItem(item, opts, cfg, userId, wsOverride, projOverride, se
|
|
|
345
732
|
if (outcome.entitiesLinked) {
|
|
346
733
|
log.dim(` ${outcome.entitiesLinked} linked to existing entit${outcome.entitiesLinked !== 1 ? "ies" : "y"}`);
|
|
347
734
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
735
|
+
// Only a --project pin is filed; the AI's pick is a suggestion to confirm.
|
|
736
|
+
const projTag = projOverride
|
|
737
|
+
? ` [project ${projOverride.slice(0, 8)}]`
|
|
738
|
+
: projTarget
|
|
739
|
+
? ` [project ${projTarget.slice(0, 8)} suggested]`
|
|
740
|
+
: "";
|
|
741
|
+
console.log(" " + formatLaneLine(report) + (projTag ? chalk.dim(projTag) : ""));
|
|
742
|
+
const pending = formatPendingWorkspaceSwitch(outcome);
|
|
743
|
+
if (pending)
|
|
744
|
+
log.dim(` ${pending}`);
|
|
745
|
+
const kept = keepRawLine(executeRes);
|
|
746
|
+
if (kept)
|
|
747
|
+
log.dim(` ${kept}`);
|
|
352
748
|
}
|
|
749
|
+
const keptStatus = executeRes.sourceFile?.status;
|
|
353
750
|
return {
|
|
354
751
|
label: item.label,
|
|
355
752
|
status: "stored",
|
|
@@ -357,8 +754,605 @@ async function processItem(item, opts, cfg, userId, wsOverride, projOverride, se
|
|
|
357
754
|
relations: relCount,
|
|
358
755
|
workspaceId: finalWorkspaceId,
|
|
359
756
|
projectId: projTarget,
|
|
757
|
+
// --json parity with the human line above: a scripted import must be able
|
|
758
|
+
// to tell a kept original from a denied or failed one.
|
|
759
|
+
...(keptStatus ? { sourceFile: keptStatus } : {}),
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Judge a completed job's `output`.
|
|
764
|
+
*
|
|
765
|
+
* The ONE place absence is turned into `known: false`. A pod that predates the
|
|
766
|
+
* result-carrying worker returns no output; so does a job whose handler reported
|
|
767
|
+
* nothing. Neither is evidence that every file landed, so neither may collapse
|
|
768
|
+
* into `filesFailed: 0`.
|
|
769
|
+
*/
|
|
770
|
+
export function readCorpusFileOutcome(output) {
|
|
771
|
+
if (!output || typeof output.filesFailed !== "number")
|
|
772
|
+
return { known: false };
|
|
773
|
+
return {
|
|
774
|
+
known: true,
|
|
775
|
+
filesProcessed: typeof output.filesProcessed === "number" ? output.filesProcessed : null,
|
|
776
|
+
filesFailed: output.filesFailed,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* The outcome status a COMPLETED corpus job earns.
|
|
781
|
+
*
|
|
782
|
+
* The ONE decision point: files were dropped → `degraded`, which `importExitCode`
|
|
783
|
+
* already treats as non-zero (no second exit-code path). An unknown outcome stays
|
|
784
|
+
* `proposed` — the proposal genuinely exists — but the caller must print
|
|
785
|
+
* CORPUS_OUTCOME_UNAVAILABLE so the unknown is never read as "0 failed".
|
|
786
|
+
*/
|
|
787
|
+
export function corpusCompletionOutcome(output) {
|
|
788
|
+
const outcome = readCorpusFileOutcome(output);
|
|
789
|
+
if (outcome.known && outcome.filesFailed > 0) {
|
|
790
|
+
return {
|
|
791
|
+
status: "degraded",
|
|
792
|
+
degradedReason: `corpus_files_failed:${outcome.filesFailed}`,
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
return { status: "proposed" };
|
|
796
|
+
}
|
|
797
|
+
/** The message shown when the pod cannot tell us what happened per file. */
|
|
798
|
+
export const CORPUS_OUTCOME_UNAVAILABLE = "file-level outcome unavailable from this pod — it did not report how many files were structured or dropped. " +
|
|
799
|
+
"This is NOT a confirmation that every file landed: open the import.graph proposal and read quality.counts.";
|
|
800
|
+
/**
|
|
801
|
+
* Human lines for a completed corpus run's file-level outcome.
|
|
802
|
+
*
|
|
803
|
+
* Exported so the run summary, the `--job-status` re-poll and the tests all read
|
|
804
|
+
* the SAME judgement — a second copy is how "0 failed" survived on one path.
|
|
805
|
+
*/
|
|
806
|
+
export function corpusOutcomeLines(output, itemCount) {
|
|
807
|
+
const outcome = readCorpusFileOutcome(output);
|
|
808
|
+
if (!outcome.known)
|
|
809
|
+
return [CORPUS_OUTCOME_UNAVAILABLE];
|
|
810
|
+
const processed = outcome.filesProcessed ?? "?";
|
|
811
|
+
const total = typeof itemCount === "number" ? ` of ${itemCount}` : "";
|
|
812
|
+
const lines = [
|
|
813
|
+
`files: ${processed}${total} structured · ${outcome.filesFailed} failed`,
|
|
814
|
+
];
|
|
815
|
+
if (outcome.filesFailed > 0) {
|
|
816
|
+
lines.push(`${outcome.filesFailed} file(s) produced NOTHING and are absent from the proposal — re-import them alone once the cause below is addressed.`);
|
|
817
|
+
}
|
|
818
|
+
for (const f of (output?.findings ?? []).slice(0, 8)) {
|
|
819
|
+
if (!f?.message)
|
|
820
|
+
continue;
|
|
821
|
+
lines.push(`[${(f.severity ?? "info").toUpperCase()}] ${f.message}`);
|
|
822
|
+
}
|
|
823
|
+
return lines;
|
|
824
|
+
}
|
|
825
|
+
/** pg-boss states that mean "this job is over and it did not succeed". */
|
|
826
|
+
const CORPUS_DEAD_STATES = new Set(["failed", "cancelled", "expired"]);
|
|
827
|
+
/**
|
|
828
|
+
* Which batch door to use. The ONE place the threshold is applied — the human
|
|
829
|
+
* message, the --json payload and the actual POST all read this.
|
|
830
|
+
*
|
|
831
|
+
* `--dry-run` always stays on `/import/analyze`: the corpus door has no preview
|
|
832
|
+
* mode (`previewOnly` is an /import/analyze field), and enqueueing is itself a
|
|
833
|
+
* durable write. A dry run that queued a background job would be a dry run that
|
|
834
|
+
* writes — the exact thing the flag promises not to do.
|
|
835
|
+
*/
|
|
836
|
+
export function chooseBatchDoor(args) {
|
|
837
|
+
if (args.dryRun)
|
|
838
|
+
return "analyze";
|
|
839
|
+
return args.fileCount >= CORPUS_THRESHOLD ? "enqueue-corpus" : "analyze";
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* How long to wait for a corpus job before handing the terminal back.
|
|
843
|
+
*
|
|
844
|
+
* Scaled from measured throughput (~45s/file) so a small corpus is not waited on
|
|
845
|
+
* for an hour and a 257-file backfill is not abandoned after five minutes, then
|
|
846
|
+
* clamped: never under 10 minutes, never over 2 hours. The ceiling is safe
|
|
847
|
+
* precisely because expiry is not failure — see corpusTimeoutLines().
|
|
848
|
+
*/
|
|
849
|
+
export function corpusPollTimeoutMs(itemCount) {
|
|
850
|
+
const scaled = itemCount * CORPUS_MS_PER_ITEM;
|
|
851
|
+
return Math.min(Math.max(scaled, CORPUS_MIN_WAIT_MS), CORPUS_MAX_WAIT_MS);
|
|
852
|
+
}
|
|
853
|
+
function humanDuration(ms) {
|
|
854
|
+
const totalMin = Math.round(ms / 60_000);
|
|
855
|
+
if (totalMin < 60)
|
|
856
|
+
return `${Math.max(totalMin, 1)}m`;
|
|
857
|
+
const h = Math.floor(totalMin / 60);
|
|
858
|
+
const m = totalMin % 60;
|
|
859
|
+
return m ? `${h}h${m}m` : `${h}h`;
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* What we print when the wait window elapses.
|
|
863
|
+
*
|
|
864
|
+
* The job is STILL RUNNING — pg-boss holds it, the worker keeps chunking, and
|
|
865
|
+
* the governed `import.graph` proposal will still be written. Saying "failed"
|
|
866
|
+
* (or exiting non-zero) here would send a user to re-run a 257-file import that
|
|
867
|
+
* is already half-done. So: state plainly that nothing was lost, and give the
|
|
868
|
+
* exact command to re-poll.
|
|
869
|
+
*/
|
|
870
|
+
export function corpusTimeoutLines(jobId, waitedMs) {
|
|
871
|
+
return [
|
|
872
|
+
`Still running after ${humanDuration(waitedMs)} — this is not a failure.`,
|
|
873
|
+
`The job is queued on the pod and keeps going after this command exits; nothing was lost and nothing was imported twice.`,
|
|
874
|
+
`Re-check it with: synap import --job-status ${jobId}`,
|
|
875
|
+
`Or watch the review inbox — the finished import lands there as ONE import.graph proposal: synap proposals list`,
|
|
876
|
+
];
|
|
877
|
+
}
|
|
878
|
+
/** Poll a corpus job to completion. Returns the last observed status. */
|
|
879
|
+
async function pollCorpusJob(jobId, cfg, userId, itemCount, quiet) {
|
|
880
|
+
const timeoutMs = corpusPollTimeoutMs(itemCount);
|
|
881
|
+
const url = new URL(`${cfg.podUrl}/api/hub/import/corpus-job/${jobId}`);
|
|
882
|
+
url.searchParams.set("userId", userId);
|
|
883
|
+
let lastState = "created";
|
|
884
|
+
let lastProgressAt = 0;
|
|
885
|
+
const startedAt = Date.now();
|
|
886
|
+
try {
|
|
887
|
+
// REUSE the shared approval poll — same create→poll shape as CP login and
|
|
888
|
+
// agent-key approval. No second loop, no second backoff policy.
|
|
889
|
+
const status = await pollForApproval({
|
|
890
|
+
url: url.toString(),
|
|
891
|
+
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
892
|
+
intervalMs: CORPUS_POLL_INTERVAL_MS,
|
|
893
|
+
timeoutMs,
|
|
894
|
+
isApproved: (d) => d?.state === "completed",
|
|
895
|
+
isRejected: (d) => {
|
|
896
|
+
const state = String(d?.state ?? "");
|
|
897
|
+
// Record the terminal state here too, not only in onTick: the rejected
|
|
898
|
+
// path throws, and the catch below reports `lastState`. Reading it only
|
|
899
|
+
// from onTick would make a failed job report the state BEFORE it failed.
|
|
900
|
+
if (state)
|
|
901
|
+
lastState = state;
|
|
902
|
+
return CORPUS_DEAD_STATES.has(state);
|
|
903
|
+
},
|
|
904
|
+
onApproved: (d) => d,
|
|
905
|
+
rejectedError: "corpus import job did not complete",
|
|
906
|
+
timeoutError: "__corpus_timeout__",
|
|
907
|
+
onTick: ({ data, elapsedMs }) => {
|
|
908
|
+
const state = data?.state;
|
|
909
|
+
if (state)
|
|
910
|
+
lastState = state;
|
|
911
|
+
if (quiet)
|
|
912
|
+
return;
|
|
913
|
+
if (elapsedMs - lastProgressAt < CORPUS_PROGRESS_EVERY_MS)
|
|
914
|
+
return;
|
|
915
|
+
lastProgressAt = elapsedMs;
|
|
916
|
+
log.dim(` … ${lastState} · ${humanDuration(elapsedMs)} elapsed (waiting up to ${humanDuration(timeoutMs)})`);
|
|
917
|
+
},
|
|
918
|
+
});
|
|
919
|
+
return { status, timedOut: false, waitedMs: Date.now() - startedAt };
|
|
920
|
+
}
|
|
921
|
+
catch (e) {
|
|
922
|
+
const waitedMs = Date.now() - startedAt;
|
|
923
|
+
if (e.message === "__corpus_timeout__") {
|
|
924
|
+
return { status: { jobId, state: lastState }, timedOut: true, waitedMs };
|
|
925
|
+
}
|
|
926
|
+
// Rejected: the job reached a terminal non-success state. Report the state
|
|
927
|
+
// we actually saw rather than a generic "failed".
|
|
928
|
+
return { status: { jobId, state: lastState }, timedOut: false, waitedMs };
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Enqueue a text corpus on the background door and wait for the worker.
|
|
933
|
+
*
|
|
934
|
+
* The door is ADDITIVE onto the same `ImportOrchestrator.analyzeLarge` that
|
|
935
|
+
* `/import/analyze` reaches — chunking, cross-chunk dedup and the single
|
|
936
|
+
* governed `import.graph` proposal all live server-side. This function only
|
|
937
|
+
* posts, polls, and reports.
|
|
938
|
+
*/
|
|
939
|
+
async function processCorpusImport(mappedItems, source, opts, cfg, userId, workspaceId, projOverride, sessionId) {
|
|
940
|
+
const label = `corpus:${source} (${mappedItems.length} files)`;
|
|
941
|
+
const itemCount = mappedItems.length;
|
|
942
|
+
// The enqueue payload the pod builds is {userId, workspaceId, source, items}
|
|
943
|
+
// (hub-protocol/rest/capture.ts) — projectId and sessionId are accepted by the
|
|
944
|
+
// schema and then DROPPED before the job. Warn rather than let a passed flag
|
|
945
|
+
// vanish: on this path they genuinely do not apply.
|
|
946
|
+
if (!opts.json && (projOverride || sessionId)) {
|
|
947
|
+
const dropped = [projOverride ? "--project" : null, sessionId ? "--session" : null]
|
|
948
|
+
.filter(Boolean)
|
|
949
|
+
.join(" and ");
|
|
950
|
+
log.warn(` ${dropped} ${projOverride && sessionId ? "do" : "does"} not apply to the background corpus import — ` +
|
|
951
|
+
`the pod's enqueue payload carries only userId, workspaceId, source and items. ` +
|
|
952
|
+
`Set the project/session on the resulting proposal when you review it.`);
|
|
953
|
+
}
|
|
954
|
+
if (!opts.json) {
|
|
955
|
+
log.info(`Enqueuing ${itemCount} text file${itemCount !== 1 ? "s" : ""} on the background corpus door ` +
|
|
956
|
+
`(source=${source}) — the synchronous /import/analyze door times out at this size.`);
|
|
957
|
+
}
|
|
958
|
+
const enqueueRes = (await hubPost("/import/enqueue-corpus", {
|
|
959
|
+
userId,
|
|
960
|
+
source,
|
|
961
|
+
items: mappedItems.map((i) => ({ path: i.path, content: i.content })),
|
|
962
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
963
|
+
}, cfg, 60_000));
|
|
964
|
+
const jobId = enqueueRes.jobId ?? null;
|
|
965
|
+
const finalWs = enqueueRes.workspaceId ?? workspaceId ?? null;
|
|
966
|
+
const corpusBase = { jobId, itemCount, waitedMs: 0 };
|
|
967
|
+
// pg-boss can accept a send and still return no id (dedup/throttle). We were
|
|
968
|
+
// told "queued", so nothing failed — but we cannot poll a job we cannot name.
|
|
969
|
+
if (!jobId) {
|
|
970
|
+
if (!opts.json) {
|
|
971
|
+
log.warn(" The pod accepted the corpus but returned no job id — it is running, but this command cannot follow it.");
|
|
972
|
+
log.dim(" Watch for the import.graph proposal with: synap proposals list");
|
|
973
|
+
}
|
|
974
|
+
return {
|
|
975
|
+
label,
|
|
976
|
+
status: "queued",
|
|
977
|
+
workspaceId: finalWs,
|
|
978
|
+
projectId: null,
|
|
979
|
+
batch: {
|
|
980
|
+
source,
|
|
981
|
+
fileCount: itemCount,
|
|
982
|
+
proposalId: null,
|
|
983
|
+
sessionId: null,
|
|
984
|
+
corpus: { ...corpusBase, state: "unknown", timedOut: false },
|
|
985
|
+
},
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
if (!opts.json) {
|
|
989
|
+
log.dim(` job ${jobId} · polling every ${CORPUS_POLL_INTERVAL_MS / 1000}s`);
|
|
990
|
+
}
|
|
991
|
+
const { status, timedOut, waitedMs } = await pollCorpusJob(jobId, cfg, userId, itemCount, Boolean(opts.json));
|
|
992
|
+
const state = status?.state ?? "unknown";
|
|
993
|
+
const corpus = { jobId, itemCount, state, timedOut, waitedMs };
|
|
994
|
+
const batchMeta = {
|
|
995
|
+
source,
|
|
996
|
+
fileCount: itemCount,
|
|
997
|
+
proposalId: null,
|
|
998
|
+
sessionId: null,
|
|
999
|
+
corpus,
|
|
1000
|
+
};
|
|
1001
|
+
if (timedOut) {
|
|
1002
|
+
if (!opts.json) {
|
|
1003
|
+
log.blank();
|
|
1004
|
+
for (const line of corpusTimeoutLines(jobId, waitedMs))
|
|
1005
|
+
log.info(` ${line}`);
|
|
1006
|
+
}
|
|
1007
|
+
return { label, status: "queued", workspaceId: finalWs, projectId: null, batch: batchMeta };
|
|
1008
|
+
}
|
|
1009
|
+
if (state !== "completed") {
|
|
1010
|
+
if (!opts.json) {
|
|
1011
|
+
log.error(` corpus job ${jobId} ended in state "${state}" — nothing was imported.`);
|
|
1012
|
+
log.hint("Check the pod's job logs, then re-run the import.");
|
|
1013
|
+
}
|
|
1014
|
+
return {
|
|
1015
|
+
label,
|
|
1016
|
+
status: "failed",
|
|
1017
|
+
error: `corpus job ended in state "${state}"`,
|
|
1018
|
+
workspaceId: finalWs,
|
|
1019
|
+
projectId: null,
|
|
1020
|
+
batch: batchMeta,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
// Completed. The result is a PENDING governed proposal — not stored entities.
|
|
1024
|
+
// Say exactly that; "created N entities" here would be a lie.
|
|
1025
|
+
//
|
|
1026
|
+
// And "completed" is NOT "every file landed": the pod structures each file
|
|
1027
|
+
// separately and records `filesFailed` on the proposal. Read that number here
|
|
1028
|
+
// or the run reports success over an unknown amount of loss.
|
|
1029
|
+
const output = status?.output ?? null;
|
|
1030
|
+
const fileOutcome = readCorpusFileOutcome(output);
|
|
1031
|
+
const lostFiles = fileOutcome.known && fileOutcome.filesFailed > 0;
|
|
1032
|
+
const corpusFull = {
|
|
1033
|
+
...corpus,
|
|
1034
|
+
outcomeKnown: fileOutcome.known,
|
|
1035
|
+
filesProcessed: fileOutcome.known ? fileOutcome.filesProcessed : null,
|
|
1036
|
+
filesFailed: fileOutcome.known ? fileOutcome.filesFailed : null,
|
|
1037
|
+
...(output?.findings ? { findings: output.findings } : {}),
|
|
1038
|
+
proposalId: output?.proposalId ?? null,
|
|
1039
|
+
};
|
|
1040
|
+
const fullBatchMeta = {
|
|
1041
|
+
...batchMeta,
|
|
1042
|
+
proposalId: output?.proposalId ?? null,
|
|
1043
|
+
corpus: corpusFull,
|
|
1044
|
+
};
|
|
1045
|
+
if (!opts.json) {
|
|
1046
|
+
const headline = ` Corpus import finished in ${humanDuration(waitedMs)}${lostFiles ? " with FILES MISSING" : ""} — ONE import.graph proposal is waiting for review.`;
|
|
1047
|
+
if (lostFiles)
|
|
1048
|
+
log.warn(headline);
|
|
1049
|
+
else
|
|
1050
|
+
log.success(headline);
|
|
1051
|
+
for (const line of corpusOutcomeLines(output, itemCount)) {
|
|
1052
|
+
if (fileOutcome.known && !lostFiles)
|
|
1053
|
+
log.dim(` ${line}`);
|
|
1054
|
+
else
|
|
1055
|
+
log.warn(` ${line}`);
|
|
1056
|
+
}
|
|
1057
|
+
log.dim(" Nothing is in the graph yet: approving the proposal is what materializes it.");
|
|
1058
|
+
renderNextSteps(FLOW.afterCorpusImport());
|
|
1059
|
+
}
|
|
1060
|
+
// Files dropped → `degraded`, the existing non-zero outcome (importExitCode);
|
|
1061
|
+
// otherwise `proposed`, the same meaning the capture path already carries.
|
|
1062
|
+
// The judgement lives in corpusCompletionOutcome so the tests exercise the
|
|
1063
|
+
// real decision rather than a copy of it.
|
|
1064
|
+
const completion = corpusCompletionOutcome(output);
|
|
1065
|
+
return {
|
|
1066
|
+
label,
|
|
1067
|
+
status: completion.status,
|
|
1068
|
+
...(completion.degradedReason
|
|
1069
|
+
? { degradedReason: completion.degradedReason }
|
|
1070
|
+
: {}),
|
|
1071
|
+
entities: 0,
|
|
1072
|
+
relations: 0,
|
|
1073
|
+
workspaceId: finalWs,
|
|
1074
|
+
projectId: null,
|
|
1075
|
+
batch: fullBatchMeta,
|
|
360
1076
|
};
|
|
361
1077
|
}
|
|
1078
|
+
// ─── multi-file batch via /import/analyze + /import/apply ─────────────────────
|
|
1079
|
+
async function processBatchImport(batchItems, opts, cfg, userId, wsOverride, projOverride, sessionId, rl, homeMap = []) {
|
|
1080
|
+
// Path rewrite for multi-home: backend matches workspace **names** in path
|
|
1081
|
+
// segments. --home-map prefixes matched paths with the resolved name.
|
|
1082
|
+
const mappedItems = applyHomeMapToBatchItems(batchItems, homeMap);
|
|
1083
|
+
const homeMapRewrites = homeMap.length > 0
|
|
1084
|
+
? mappedItems.filter((m, i) => m.path !== batchItems[i]?.path).length
|
|
1085
|
+
: 0;
|
|
1086
|
+
const source = sourceForBatchItems(mappedItems);
|
|
1087
|
+
const label = `batch:${source} (${mappedItems.length} files)`;
|
|
1088
|
+
// Only an explicit --workspace pins an import. The saved default space is
|
|
1089
|
+
// not a choice the person made for THIS import: sent as a pin it filed a
|
|
1090
|
+
// 2,827-entity, all pod-wide contacts import under "Brand" (live 0728053b,
|
|
1091
|
+
// 2026-10-22). Without a pin the pod places the graph by its kinds and
|
|
1092
|
+
// roles (one space when they all point at it, else pod-wide).
|
|
1093
|
+
const workspaceId = wsOverride;
|
|
1094
|
+
// Above CORPUS_THRESHOLD the synchronous door cannot finish inside the pod's
|
|
1095
|
+
// request timeout — hand the SAME mapped items (home-map already applied, caps
|
|
1096
|
+
// already enforced, source already resolved) to the background door instead.
|
|
1097
|
+
if (chooseBatchDoor({ fileCount: mappedItems.length, dryRun: opts.dryRun }) === "enqueue-corpus") {
|
|
1098
|
+
if (!opts.json && homeMapRewrites > 0) {
|
|
1099
|
+
log.dim(` home-map: rewrote ${homeMapRewrites} path${homeMapRewrites !== 1 ? "s" : ""} ` +
|
|
1100
|
+
`(workspace name as first segment for multi-home placement)`);
|
|
1101
|
+
}
|
|
1102
|
+
return processCorpusImport(mappedItems, source, opts, cfg, userId, workspaceId, projOverride, sessionId);
|
|
1103
|
+
}
|
|
1104
|
+
const analyzeBody = {
|
|
1105
|
+
userId,
|
|
1106
|
+
source,
|
|
1107
|
+
items: mappedItems.map((i) => ({ path: i.path, content: i.content })),
|
|
1108
|
+
aiStructure: true,
|
|
1109
|
+
// --dry-run must not file a durable import.graph proposal (inbox spam).
|
|
1110
|
+
// Interactive / --yes still create a real proposal for HITL apply.
|
|
1111
|
+
...(opts.dryRun ? { previewOnly: true } : {}),
|
|
1112
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
1113
|
+
...(projOverride ? { projectId: projOverride } : {}),
|
|
1114
|
+
...(sessionId ? { sessionId } : {}),
|
|
1115
|
+
};
|
|
1116
|
+
if (!opts.json) {
|
|
1117
|
+
log.info(`Analyzing ${mappedItems.length} text file${mappedItems.length !== 1 ? "s" : ""} via /import/analyze (source=${source})…`);
|
|
1118
|
+
if (homeMapRewrites > 0) {
|
|
1119
|
+
log.dim(` home-map: rewrote ${homeMapRewrites} path${homeMapRewrites !== 1 ? "s" : ""} ` +
|
|
1120
|
+
`(workspace name as first segment for multi-home placement)`);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
const analyzeRes = (await hubPost("/import/analyze", analyzeBody, cfg, IMPORT_ANALYZE_TIMEOUT_MS));
|
|
1124
|
+
const ops = analyzeRes.operations ?? [];
|
|
1125
|
+
const batchMeta = {
|
|
1126
|
+
source,
|
|
1127
|
+
fileCount: mappedItems.length,
|
|
1128
|
+
proposalId: analyzeRes.proposalId ?? null,
|
|
1129
|
+
sessionId: analyzeRes.sessionId ?? sessionId ?? null,
|
|
1130
|
+
analyze: analyzeRes,
|
|
1131
|
+
};
|
|
1132
|
+
if (!opts.json) {
|
|
1133
|
+
renderBatchAnalyze(analyzeRes, mappedItems.length, source);
|
|
1134
|
+
}
|
|
1135
|
+
if (ops.length === 0) {
|
|
1136
|
+
return {
|
|
1137
|
+
label,
|
|
1138
|
+
status: "empty",
|
|
1139
|
+
workspaceId: analyzeRes.workspaceId ?? workspaceId ?? null,
|
|
1140
|
+
projectId: projOverride ?? null,
|
|
1141
|
+
batch: batchMeta,
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
// Dry-run (or --json without --yes): analyze only — no apply.
|
|
1145
|
+
if (opts.dryRun) {
|
|
1146
|
+
return {
|
|
1147
|
+
label,
|
|
1148
|
+
status: "dry-run",
|
|
1149
|
+
entities: ops.filter((o) => o.op === "create_entity").length || ops.length,
|
|
1150
|
+
relations: ops.filter((o) => o.op === "create_relation").length,
|
|
1151
|
+
workspaceId: analyzeRes.workspaceId ?? workspaceId ?? null,
|
|
1152
|
+
projectId: projOverride ?? null,
|
|
1153
|
+
batch: batchMeta,
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
// Human apply default: confirm y/N unless --yes.
|
|
1157
|
+
// --json without --yes skips apply (same as per-item capture path).
|
|
1158
|
+
let confirmed = Boolean(opts.yes);
|
|
1159
|
+
if (!confirmed && !opts.json && rl) {
|
|
1160
|
+
const answer = await rl.question(chalk.bold(` Apply this import (${ops.length} operation${ops.length !== 1 ? "s" : ""})? [y/N] `));
|
|
1161
|
+
confirmed = answer.trim().toLowerCase() === "y";
|
|
1162
|
+
}
|
|
1163
|
+
if (!confirmed) {
|
|
1164
|
+
if (!opts.json)
|
|
1165
|
+
log.dim(" Skipped.");
|
|
1166
|
+
return {
|
|
1167
|
+
label,
|
|
1168
|
+
status: "skipped",
|
|
1169
|
+
workspaceId: analyzeRes.workspaceId ?? workspaceId ?? null,
|
|
1170
|
+
projectId: projOverride ?? null,
|
|
1171
|
+
batch: batchMeta,
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
// Applying a NAMED import proposal is approving it, and approval is the
|
|
1175
|
+
// human step: the pod refuses an agent credential (HTTP 403, correct
|
|
1176
|
+
// governance). So an agent's import ends at the proposal — reported as
|
|
1177
|
+
// `proposed` with the review link, never as `failed`. `/users/me` `isAgent`
|
|
1178
|
+
// is the pod's own answer; when it is unknown the apply runs and a 403
|
|
1179
|
+
// agent refusal is read the same way.
|
|
1180
|
+
const filedProposalId = analyzeRes.proposalId ?? null;
|
|
1181
|
+
if (filedProposalId && (await callerIsAgent(cfg))) {
|
|
1182
|
+
return proposedForReview(label, filedProposalId, cfg, opts, {
|
|
1183
|
+
workspaceId: analyzeRes.workspaceId ?? workspaceId ?? null,
|
|
1184
|
+
projectId: projOverride ?? null,
|
|
1185
|
+
batch: batchMeta,
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
const applySessionId = analyzeRes.sessionId ?? sessionId;
|
|
1189
|
+
// Prefer analyze-resolved placement so apply lands where the proposal was filed.
|
|
1190
|
+
const applyWorkspaceId = analyzeRes.workspaceId ?? workspaceId ?? undefined;
|
|
1191
|
+
const applyBody = {
|
|
1192
|
+
userId,
|
|
1193
|
+
source,
|
|
1194
|
+
operations: ops,
|
|
1195
|
+
...(applyWorkspaceId ? { workspaceId: applyWorkspaceId } : {}),
|
|
1196
|
+
...(projOverride ? { projectId: projOverride } : {}),
|
|
1197
|
+
...(applySessionId ? { sessionId: applySessionId } : {}),
|
|
1198
|
+
// Client-stable idempotency namespace (U1): analyze proposalId.
|
|
1199
|
+
...(analyzeRes.proposalId
|
|
1200
|
+
? {
|
|
1201
|
+
idempotencyKey: analyzeRes.proposalId,
|
|
1202
|
+
proposalId: analyzeRes.proposalId,
|
|
1203
|
+
}
|
|
1204
|
+
: {}),
|
|
1205
|
+
};
|
|
1206
|
+
let applyRes;
|
|
1207
|
+
try {
|
|
1208
|
+
applyRes = (await hubPost("/import/apply", applyBody, cfg, IMPORT_APPLY_TIMEOUT_MS));
|
|
1209
|
+
}
|
|
1210
|
+
catch (e) {
|
|
1211
|
+
if (filedProposalId && isAgentReviewRejection(e)) {
|
|
1212
|
+
return proposedForReview(label, filedProposalId, cfg, opts, {
|
|
1213
|
+
workspaceId: analyzeRes.workspaceId ?? workspaceId ?? null,
|
|
1214
|
+
projectId: projOverride ?? null,
|
|
1215
|
+
batch: batchMeta,
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
throw e;
|
|
1219
|
+
}
|
|
1220
|
+
const created = applyRes.created ?? 0;
|
|
1221
|
+
const linked = applyRes.linked ?? 0;
|
|
1222
|
+
const finalWs = applyRes.workspaceId ?? analyzeRes.workspaceId ?? workspaceId ?? null;
|
|
1223
|
+
if (!opts.json) {
|
|
1224
|
+
log.success(` ${created} entit${created !== 1 ? "ies" : "y"} created` +
|
|
1225
|
+
(linked ? `, ${linked} linked` : ""));
|
|
1226
|
+
if (finalWs) {
|
|
1227
|
+
const report = {
|
|
1228
|
+
lane: "work",
|
|
1229
|
+
workspaceId: finalWs,
|
|
1230
|
+
workspaceName: await resolveWorkspaceName(finalWs, cfg),
|
|
1231
|
+
governance: "auto",
|
|
1232
|
+
};
|
|
1233
|
+
console.log(" " +
|
|
1234
|
+
formatLaneLine(report) +
|
|
1235
|
+
(projOverride ? chalk.dim(` [project ${projOverride.slice(0, 8)}]`) : ""));
|
|
1236
|
+
}
|
|
1237
|
+
if (applySessionId)
|
|
1238
|
+
log.dim(` session: ${applySessionId}`);
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
label,
|
|
1242
|
+
status: "stored",
|
|
1243
|
+
entities: created,
|
|
1244
|
+
relations: linked,
|
|
1245
|
+
workspaceId: finalWs,
|
|
1246
|
+
projectId: projOverride ?? null,
|
|
1247
|
+
batch: { ...batchMeta, apply: applyRes },
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Is the calling credential an agent? The pod's own `/users/me` `isAgent`.
|
|
1252
|
+
* Unknown (older pod, network) is `false`: the apply then runs and the pod's
|
|
1253
|
+
* 403 agent refusal is translated after the fact — never a guess that blocks
|
|
1254
|
+
* a human.
|
|
1255
|
+
*/
|
|
1256
|
+
async function callerIsAgent(cfg) {
|
|
1257
|
+
try {
|
|
1258
|
+
return (await fetchEffectiveIdentity(cfg)).isAgent === true;
|
|
1259
|
+
}
|
|
1260
|
+
catch {
|
|
1261
|
+
return false;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
/** The import was filed for review and not applied: say so, with the door. */
|
|
1265
|
+
function proposedForReview(label, proposalId, cfg, opts, rest) {
|
|
1266
|
+
const reviewUrl = proposalWebLink(cfg.podUrl, proposalId);
|
|
1267
|
+
if (!opts.json) {
|
|
1268
|
+
log.info(" Proposed — waiting for your approval. Nothing is stored until a person approves it.");
|
|
1269
|
+
log.dim(` proposal: ${proposalId}`);
|
|
1270
|
+
log.dim(` review: ${reviewUrl}`);
|
|
1271
|
+
log.dim(` or run: synap open proposal ${proposalId}`);
|
|
1272
|
+
}
|
|
1273
|
+
return {
|
|
1274
|
+
label,
|
|
1275
|
+
status: "proposed",
|
|
1276
|
+
entities: 0,
|
|
1277
|
+
relations: 0,
|
|
1278
|
+
reviewUrl,
|
|
1279
|
+
...rest,
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
// ─── `synap import --job-status <jobId>` ──────────────────────────────────────
|
|
1283
|
+
/**
|
|
1284
|
+
* Re-poll a corpus job started by an earlier `synap import`.
|
|
1285
|
+
*
|
|
1286
|
+
* This is the command `corpusTimeoutLines()` tells the user to run, so it has to
|
|
1287
|
+
* exist: a timeout message naming a command that does not resolve is worse than
|
|
1288
|
+
* no message. One GET, no waiting — it reports the current state and exits.
|
|
1289
|
+
*/
|
|
1290
|
+
export async function importJobStatus(jobId, opts) {
|
|
1291
|
+
try {
|
|
1292
|
+
if (!UUID_RE.test(jobId)) {
|
|
1293
|
+
console.error(chalk.red(`'${jobId}' is not a job id — pass the UUID printed when the import was enqueued.`));
|
|
1294
|
+
process.exit(1);
|
|
1295
|
+
}
|
|
1296
|
+
const cfg = await resolveHubConfig(opts);
|
|
1297
|
+
const userId = await resolveUserId(cfg);
|
|
1298
|
+
const res = (await hubGet(`/import/corpus-job/${jobId}`, { userId }, cfg));
|
|
1299
|
+
const state = res.state ?? "unknown";
|
|
1300
|
+
const output = res.output ?? null;
|
|
1301
|
+
const fileOutcome = readCorpusFileOutcome(output);
|
|
1302
|
+
const lostFiles = fileOutcome.known && fileOutcome.filesFailed > 0;
|
|
1303
|
+
if (opts.json) {
|
|
1304
|
+
console.log(JSON.stringify({
|
|
1305
|
+
jobId,
|
|
1306
|
+
state,
|
|
1307
|
+
createdOn: res.createdOn ?? null,
|
|
1308
|
+
completedOn: res.completedOn ?? null,
|
|
1309
|
+
// Same UNKNOWN-vs-zero distinction the run path makes.
|
|
1310
|
+
outcomeKnown: state === "completed" ? fileOutcome.known : false,
|
|
1311
|
+
filesProcessed: fileOutcome.known ? fileOutcome.filesProcessed : null,
|
|
1312
|
+
filesFailed: fileOutcome.known ? fileOutcome.filesFailed : null,
|
|
1313
|
+
...(output?.findings ? { findings: output.findings } : {}),
|
|
1314
|
+
proposalId: output?.proposalId ?? null,
|
|
1315
|
+
}, null, 2));
|
|
1316
|
+
if (state === "completed" && lostFiles)
|
|
1317
|
+
process.exit(1);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
log.heading(`Corpus import job ${jobId}`);
|
|
1321
|
+
log.info(` state: ${state}`);
|
|
1322
|
+
if (res.createdOn)
|
|
1323
|
+
log.dim(` created: ${res.createdOn}`);
|
|
1324
|
+
if (res.completedOn)
|
|
1325
|
+
log.dim(` completed: ${res.completedOn}`);
|
|
1326
|
+
if (state === "completed") {
|
|
1327
|
+
if (lostFiles) {
|
|
1328
|
+
log.warn(" Finished with FILES MISSING — ONE import.graph proposal is waiting for review.");
|
|
1329
|
+
}
|
|
1330
|
+
else {
|
|
1331
|
+
log.success(" Finished — ONE import.graph proposal is waiting for review.");
|
|
1332
|
+
}
|
|
1333
|
+
for (const line of corpusOutcomeLines(output)) {
|
|
1334
|
+
if (fileOutcome.known && !lostFiles)
|
|
1335
|
+
log.dim(` ${line}`);
|
|
1336
|
+
else
|
|
1337
|
+
log.warn(` ${line}`);
|
|
1338
|
+
}
|
|
1339
|
+
renderNextSteps(FLOW.afterCorpusImport());
|
|
1340
|
+
// A run that lost files is not a success on the re-poll path either.
|
|
1341
|
+
if (lostFiles)
|
|
1342
|
+
process.exit(1);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
if (CORPUS_DEAD_STATES.has(state)) {
|
|
1346
|
+
log.error(` The job ended in state "${state}" — nothing was imported.`);
|
|
1347
|
+
process.exit(1);
|
|
1348
|
+
}
|
|
1349
|
+
log.info(" Still running — nothing was lost. Re-run this command later.");
|
|
1350
|
+
}
|
|
1351
|
+
catch (e) {
|
|
1352
|
+
renderHubError(e);
|
|
1353
|
+
process.exit(1);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
362
1356
|
// ─── command entrypoint ─────────────────────────────────────────────────────────
|
|
363
1357
|
export async function importData(inputs, opts) {
|
|
364
1358
|
try {
|
|
@@ -394,10 +1388,17 @@ export async function importData(inputs, opts) {
|
|
|
394
1388
|
// Resolve routing overrides (id or name) up front so a bad value fails fast.
|
|
395
1389
|
const wsOverride = opts.workspace ? await resolveWorkspaceArg(opts.workspace, cfg) : undefined;
|
|
396
1390
|
const projOverride = opts.project ? await resolveProjectArg(opts.project, cfg) : undefined;
|
|
397
|
-
const sessionId = opts.session ?
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
1391
|
+
const sessionId = opts.session ? resolveActiveSessionId(cfg.podUrl) : undefined;
|
|
1392
|
+
// Optional multi-home path map (no product defaults — user pairs only).
|
|
1393
|
+
let homeMap = [];
|
|
1394
|
+
if (opts.homeMap) {
|
|
1395
|
+
homeMap = await resolveHomeMap(parseHomeMap(opts.homeMap), cfg);
|
|
1396
|
+
}
|
|
1397
|
+
const { items, failures, truncated: dirTruncated } = expandInputs(inputs);
|
|
1398
|
+
let truncated = dirTruncated;
|
|
1399
|
+
if (dirTruncated && !opts.json) {
|
|
1400
|
+
log.warn(`A directory exceeded ${DIR_FILE_CAP} files — only the first ${DIR_FILE_CAP} were taken ` +
|
|
1401
|
+
`(sync /import/analyze max). Narrow the path, split the import, or use a smaller folder.`);
|
|
401
1402
|
}
|
|
402
1403
|
if (items.length === 0) {
|
|
403
1404
|
if (opts.json) {
|
|
@@ -410,15 +1411,71 @@ export async function importData(inputs, opts) {
|
|
|
410
1411
|
}
|
|
411
1412
|
process.exit(failures.length > 0 ? 1 : 0);
|
|
412
1413
|
}
|
|
1414
|
+
// Partition: N≥2 batch-eligible text → /import/*; rest → per-item capture.
|
|
1415
|
+
// Single text file stays on capture structure/execute (same as a lone URL).
|
|
1416
|
+
const batchFileItems = [];
|
|
1417
|
+
const captureItems = [];
|
|
1418
|
+
// `--keep-raw` is a PER-FILE disposition and the batch door
|
|
1419
|
+
// (/import/analyze → /import/apply) has no `keepRaw` half at all. Routing a
|
|
1420
|
+
// kept-raw text file through it would accept the flag and silently discard
|
|
1421
|
+
// every original — an inert flag rendered as a live one. Send those items
|
|
1422
|
+
// down the per-item capture path instead, which is where `keepRaw` exists.
|
|
1423
|
+
for (const item of items) {
|
|
1424
|
+
if (isBatchTextItem(item) && !opts.keepRaw) {
|
|
1425
|
+
batchFileItems.push(item);
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
captureItems.push(item);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (opts.keepRaw && !opts.json && captureItems.length > 1) {
|
|
1432
|
+
log.dim(`--keep-raw: importing ${captureItems.length} items one by one (the batch door cannot keep originals).`);
|
|
1433
|
+
}
|
|
1434
|
+
// Global batch cap: multi-dir / multi-file inputs can exceed the sync max
|
|
1435
|
+
// even when no single directory hit DIR_FILE_CAP.
|
|
1436
|
+
if (batchFileItems.length > GLOBAL_BATCH_CAP) {
|
|
1437
|
+
truncated = true;
|
|
1438
|
+
if (!opts.json) {
|
|
1439
|
+
log.warn(`Batch has ${batchFileItems.length} text files — truncating to ${GLOBAL_BATCH_CAP} ` +
|
|
1440
|
+
`(sync /import/analyze max). Split the import or narrow the paths.`);
|
|
1441
|
+
}
|
|
1442
|
+
batchFileItems.length = GLOBAL_BATCH_CAP;
|
|
1443
|
+
}
|
|
1444
|
+
const grouped = groupBatchFiles(batchFileItems);
|
|
1445
|
+
const batchGroups = grouped.batches.map((group) => group.map(toBatchTextItem));
|
|
1446
|
+
captureItems.push(...grouped.single);
|
|
413
1447
|
if (!opts.json) {
|
|
414
|
-
|
|
1448
|
+
const parts = [];
|
|
1449
|
+
for (const group of batchGroups) {
|
|
1450
|
+
parts.push(`${group.length} ${sourceForBatchItems(group) === "csv" ? "CSV" : "text"} file${group.length !== 1 ? "s" : ""} (batch import)`);
|
|
1451
|
+
}
|
|
1452
|
+
if (captureItems.length > 0) {
|
|
1453
|
+
parts.push(`${captureItems.length} item${captureItems.length !== 1 ? "s" : ""} (capture pipeline)`);
|
|
1454
|
+
}
|
|
1455
|
+
log.info(`Importing ${parts.join(" + ") || `${items.length} item${items.length !== 1 ? "s" : ""}`}` +
|
|
1456
|
+
`${opts.dryRun ? " (dry run)" : ""}…`);
|
|
415
1457
|
}
|
|
416
1458
|
const outcomes = [];
|
|
417
1459
|
// One shared readline for all interactive confirmations (human, non-json).
|
|
418
1460
|
const interactive = !opts.json && !opts.dryRun && !opts.yes;
|
|
419
1461
|
const rl = interactive ? readline.createInterface({ input, output }) : undefined;
|
|
420
1462
|
try {
|
|
421
|
-
for (const
|
|
1463
|
+
for (const batchCandidates of batchGroups) {
|
|
1464
|
+
try {
|
|
1465
|
+
outcomes.push(await processBatchImport(batchCandidates, opts, cfg, userId, wsOverride, projOverride, sessionId, rl, homeMap));
|
|
1466
|
+
}
|
|
1467
|
+
catch (e) {
|
|
1468
|
+
const error = e.message;
|
|
1469
|
+
if (!opts.json)
|
|
1470
|
+
log.error(` batch import: ${error}`);
|
|
1471
|
+
outcomes.push({
|
|
1472
|
+
label: `batch (${batchCandidates.length} files)`,
|
|
1473
|
+
status: "failed",
|
|
1474
|
+
error,
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
for (const item of captureItems) {
|
|
422
1479
|
try {
|
|
423
1480
|
outcomes.push(await processItem(item, opts, cfg, userId, wsOverride, projOverride, sessionId, rl));
|
|
424
1481
|
}
|
|
@@ -438,7 +1495,19 @@ export async function importData(inputs, opts) {
|
|
|
438
1495
|
for (const f of failures)
|
|
439
1496
|
outcomes.push({ label: f.label, status: "failed", error: f.error });
|
|
440
1497
|
if (opts.json) {
|
|
441
|
-
|
|
1498
|
+
// Surface analyze/apply payloads for the batch path (agents need ops + receipt).
|
|
1499
|
+
const batchOutcome = outcomes.find((o) => o.batch);
|
|
1500
|
+
console.log(JSON.stringify({
|
|
1501
|
+
imported: outcomes,
|
|
1502
|
+
truncated,
|
|
1503
|
+
...(batchOutcome?.batch?.analyze ? { analyze: batchOutcome.batch.analyze } : {}),
|
|
1504
|
+
...(batchOutcome?.batch?.apply ? { apply: batchOutcome.batch.apply } : {}),
|
|
1505
|
+
// Background corpus path: a script needs the jobId + final state to
|
|
1506
|
+
// decide whether to re-poll, and nextSteps to chain the review.
|
|
1507
|
+
...(batchOutcome?.batch?.corpus
|
|
1508
|
+
? { corpus: batchOutcome.batch.corpus, nextSteps: FLOW.afterCorpusImport() }
|
|
1509
|
+
: {}),
|
|
1510
|
+
}, null, 2));
|
|
442
1511
|
}
|
|
443
1512
|
else {
|
|
444
1513
|
const stored = outcomes.filter((o) => o.status === "stored");
|
|
@@ -448,20 +1517,47 @@ export async function importData(inputs, opts) {
|
|
|
448
1517
|
log.heading("Import summary");
|
|
449
1518
|
const proposed = outcomes.filter((o) => o.status === "proposed").length;
|
|
450
1519
|
const degraded = outcomes.filter((o) => o.status === "degraded").length;
|
|
451
|
-
|
|
1520
|
+
const queued = outcomes.filter((o) => o.status === "queued").length;
|
|
1521
|
+
// Background corpus path: ONE outcome represents ONE JOB covering N files.
|
|
1522
|
+
// So this line's counters are denominated in JOBS, and the file-level truth
|
|
1523
|
+
// is a separate line below.
|
|
1524
|
+
const corpusMeta = outcomes.find((o) => o.batch?.corpus)?.batch?.corpus;
|
|
1525
|
+
const corpusLost = (corpusMeta?.filesFailed ?? 0) > 0;
|
|
1526
|
+
const noun = corpusMeta ? "job" : "item";
|
|
1527
|
+
log.info(`${outcomes.length} ${noun}${outcomes.length !== 1 ? "s" : ""} · ` +
|
|
452
1528
|
`${stored.length} stored (${totalEntities} entit${totalEntities !== 1 ? "ies" : "y"}) · ` +
|
|
453
1529
|
(proposed ? `${proposed} proposed · ` : "") +
|
|
1530
|
+
(queued ? `${queued} still running · ` : "") +
|
|
454
1531
|
`${outcomes.filter((o) => o.status === "dry-run").length} dry-run · ` +
|
|
455
1532
|
`${outcomes.filter((o) => o.status === "skipped").length} skipped · ` +
|
|
456
1533
|
`${outcomes.filter((o) => o.status === "empty").length} empty · ` +
|
|
457
1534
|
(degraded ? `${degraded} degraded · ` : "") +
|
|
458
|
-
|
|
1535
|
+
// A corpus run that LOST files expresses that loss one line below, in
|
|
1536
|
+
// files. Printing "0 failed" here too — same word, different
|
|
1537
|
+
// denominator, three lines apart — restates the exact sentence this
|
|
1538
|
+
// whole wave existed to delete. Job-level failure is already carried
|
|
1539
|
+
// by `degraded`, so the counter is dropped rather than qualified.
|
|
1540
|
+
(corpusLost ? "" : `${failed.length} failed`));
|
|
459
1541
|
for (const f of failed)
|
|
460
1542
|
log.dim(` ✗ ${f.label}: ${f.error ?? "unknown error"}`);
|
|
1543
|
+
if (corpusMeta && !corpusMeta.timedOut) {
|
|
1544
|
+
if (corpusMeta.outcomeKnown) {
|
|
1545
|
+
const line = `files: ${corpusMeta.filesProcessed ?? "?"} of ${corpusMeta.itemCount} structured · ${corpusMeta.filesFailed ?? 0} failed`;
|
|
1546
|
+
if (corpusLost)
|
|
1547
|
+
log.warn(` ${line}`);
|
|
1548
|
+
else
|
|
1549
|
+
log.info(` ${line}`);
|
|
1550
|
+
}
|
|
1551
|
+
else if (corpusMeta.state === "completed") {
|
|
1552
|
+
log.warn(` ${CORPUS_OUTCOME_UNAVAILABLE}`);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
461
1555
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
1556
|
+
// Only hard-exit on a non-zero code: a plain `return` lets Node flush the
|
|
1557
|
+
// (possibly large) --json payload it just wrote to stdout.
|
|
1558
|
+
const code = importExitCode(outcomes);
|
|
1559
|
+
if (code !== 0)
|
|
1560
|
+
process.exit(code);
|
|
465
1561
|
}
|
|
466
1562
|
catch (e) {
|
|
467
1563
|
renderHubError(e);
|