harnery 0.40.0 → 0.42.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/README.md +1 -1
- package/dist/commander.js +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +34 -20
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +113 -12
- package/dist/commands/browse-ai.d.ts.map +1 -1
- package/dist/commands/browse-ai.js +4 -0
- package/dist/commands/browse.d.ts +164 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +388 -45
- package/dist/commands/checkpoint.js +8 -12
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +52 -0
- package/dist/commands/deinit.d.ts +1 -1
- package/dist/commands/deinit.d.ts.map +1 -1
- package/dist/commands/deinit.js +8 -4
- package/dist/commands/disk.d.ts +6 -0
- package/dist/commands/disk.d.ts.map +1 -0
- package/dist/commands/disk.js +80 -0
- package/dist/commands/docs.d.ts.map +1 -1
- package/dist/commands/docs.js +2 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +58 -3
- package/dist/commands/fetch.d.ts.map +1 -1
- package/dist/commands/fetch.js +10 -0
- package/dist/commands/files.d.ts +16 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -2
- package/dist/commands/init.d.ts +21 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +271 -109
- package/dist/commands/instructions.js +5 -4
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +2 -1
- package/dist/commands/rm.d.ts +4 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +87 -0
- package/dist/commands/servers.d.ts +7 -0
- package/dist/commands/servers.d.ts.map +1 -0
- package/dist/commands/servers.js +394 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +82 -2
- package/dist/commands/webview.d.ts +4 -0
- package/dist/commands/webview.d.ts.map +1 -0
- package/dist/commands/webview.js +94 -0
- package/dist/core/adapter.d.ts +50 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +59 -1
- package/dist/core/adapters/attestation.d.ts.map +1 -1
- package/dist/core/adapters/attestation.js +3 -2
- package/dist/core/adapters/profiles.d.ts +29 -3
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +53 -1
- package/dist/core/adapters/registry.d.ts.map +1 -1
- package/dist/core/adapters/registry.js +33 -0
- package/dist/core/adapters/types.d.ts +9 -0
- package/dist/core/adapters/types.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.d.ts +1 -1
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -7
- package/dist/core/agents/cli.js +25 -16
- package/dist/core/agents/coord-client.d.ts.map +1 -1
- package/dist/core/agents/coord-client.js +7 -2
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +2 -0
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +3 -9
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +2 -8
- package/dist/core/agents/rules/stop-hook.d.ts +2 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +1 -1
- package/dist/core/agents/session-events.d.ts +6 -2
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +105 -26
- package/dist/core/agents/session-finalization-state-v3.d.ts.map +1 -1
- package/dist/core/agents/session-finalization-state-v3.js +2 -1
- package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
- package/dist/core/agents/session-finalizer-v3.js +5 -4
- package/dist/core/agents/session-name-display.d.ts +28 -0
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +43 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +10 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +21 -2
- package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +61 -0
- package/dist/core/agents/state/live-coordination-writer.js +3 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +2 -10
- package/dist/core/agents/turn-changes.d.ts +17 -0
- package/dist/core/agents/turn-changes.d.ts.map +1 -0
- package/dist/core/agents/turn-changes.js +76 -0
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +11 -3
- package/dist/core/artifacts/index.d.ts +140 -4
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +526 -52
- package/dist/core/config.d.ts +73 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +142 -3
- package/dist/core/context/index.d.ts.map +1 -1
- package/dist/core/context/index.js +2 -1
- package/dist/core/conversations/archive.d.ts.map +1 -1
- package/dist/core/conversations/archive.js +4 -3
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +5 -4
- package/dist/core/events/legacy-storage/canary.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/canary.js +5 -4
- package/dist/core/events/legacy-storage/compression.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/compression.js +2 -1
- package/dist/core/events/v3/adapter-id.d.ts +10 -2
- package/dist/core/events/v3/adapter-id.d.ts.map +1 -1
- package/dist/core/events/v3/adapter-id.js +16 -0
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +2 -1
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +5 -4
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +17 -0
- package/dist/core/events/v3/contract.d.ts +5 -0
- package/dist/core/events/v3/contract.d.ts.map +1 -1
- package/dist/core/events/v3/contract.js +7 -1
- package/dist/core/events/v3/control-witness.d.ts.map +1 -1
- package/dist/core/events/v3/control-witness.js +3 -2
- package/dist/core/events/v3/coordination-transaction-recovery.d.ts.map +1 -1
- package/dist/core/events/v3/coordination-transaction-recovery.js +4 -3
- package/dist/core/events/v3/fingerprint-keys.d.ts.map +1 -1
- package/dist/core/events/v3/fingerprint-keys.js +7 -6
- package/dist/core/events/v3/generated.d.ts +1 -1
- package/dist/core/events/v3/generated.js +1 -1
- package/dist/core/events/v3/live-feed.d.ts.map +1 -1
- package/dist/core/events/v3/live-feed.js +5 -4
- package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/command-recorder.js +8 -7
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +8 -7
- package/dist/core/events/v3/producers/diagnostic-summaries.d.ts.map +1 -1
- package/dist/core/events/v3/producers/diagnostic-summaries.js +5 -4
- package/dist/core/events/v3/producers/hook-base.d.ts +1 -0
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +10 -4
- package/dist/core/events/v3/producers/hook.d.ts +2 -0
- package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook.js +16 -1
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +16 -6
- package/dist/core/events/v3/producers/recorder.d.ts +2 -4
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +125 -101
- package/dist/core/events/v3/projection.js +9 -1
- package/dist/core/events/v3/recovery.d.ts.map +1 -1
- package/dist/core/events/v3/recovery.js +4 -3
- package/dist/core/events/v3/support-storage/inventory.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/inventory.js +2 -1
- package/dist/core/events/v3/support-storage/maintenance.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/maintenance.js +4 -3
- package/dist/core/events/v3/support-storage/pack-reader.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-reader.js +4 -3
- package/dist/core/events/v3/support-storage/pack-writer.d.ts.map +1 -1
- package/dist/core/events/v3/support-storage/pack-writer.js +6 -5
- package/dist/core/events/v3/tool-line-changes.d.ts +8 -0
- package/dist/core/events/v3/tool-line-changes.d.ts.map +1 -0
- package/dist/core/events/v3/tool-line-changes.js +135 -0
- package/dist/core/events/v3/writer.d.ts.map +1 -1
- package/dist/core/events/v3/writer.js +8 -7
- package/dist/core/governor/planning.d.ts.map +1 -1
- package/dist/core/governor/planning.js +6 -5
- package/dist/core/governor/service.d.ts.map +1 -1
- package/dist/core/governor/service.js +12 -11
- package/dist/core/governor/state.d.ts.map +1 -1
- package/dist/core/governor/state.js +7 -6
- package/dist/core/guard/evaluator.js +1 -0
- package/dist/core/guard/storage.d.ts.map +1 -1
- package/dist/core/guard/storage.js +3 -2
- package/dist/core/hooks/adapter/behaviors/claude-code.d.ts +4 -0
- package/dist/core/hooks/adapter/behaviors/claude-code.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/claude-code.js +31 -0
- package/dist/core/hooks/adapter/behaviors/codex.d.ts +15 -0
- package/dist/core/hooks/adapter/behaviors/codex.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/codex.js +82 -0
- package/dist/core/hooks/adapter/behaviors/cursor.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/cursor.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/cursor.js +35 -0
- package/dist/core/hooks/adapter/behaviors/index.d.ts +11 -0
- package/dist/core/hooks/adapter/behaviors/index.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/index.js +20 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/openclaw.js +34 -0
- package/dist/core/hooks/adapter/behaviors/opencode.d.ts +7 -0
- package/dist/core/hooks/adapter/behaviors/opencode.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/opencode.js +34 -0
- package/dist/core/hooks/adapter/behaviors/types.d.ts +89 -0
- package/dist/core/hooks/adapter/behaviors/types.d.ts.map +1 -0
- package/dist/core/hooks/adapter/behaviors/types.js +1 -0
- package/dist/core/hooks/adapter/detect.d.ts +1 -1
- package/dist/core/hooks/adapter/detect.d.ts.map +1 -1
- package/dist/core/hooks/adapter/detect.js +2 -3
- package/dist/core/hooks/adapter/events.d.ts +46 -1
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +36 -0
- package/dist/core/hooks/adapter/opencode-plugin.d.ts +60 -0
- package/dist/core/hooks/adapter/opencode-plugin.d.ts.map +1 -0
- package/dist/core/hooks/adapter/opencode-plugin.js +129 -0
- package/dist/core/hooks/adapter/output.d.ts +7 -0
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +8 -1
- package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
- package/dist/core/hooks/adapter/wiring.d.ts +52 -8
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +133 -12
- package/dist/core/hooks/cli.js +676 -531
- package/dist/core/hooks/effects/image-capture.d.ts.map +1 -1
- package/dist/core/hooks/effects/image-capture.js +2 -1
- package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -1
- package/dist/core/hooks/prompt-context/contract.js +1 -0
- package/dist/core/hooks/resolve/transcript.d.ts +1 -1
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +35 -4
- package/dist/core/hooks/run-markers.d.ts +37 -0
- package/dist/core/hooks/run-markers.d.ts.map +1 -0
- package/dist/core/hooks/run-markers.js +109 -0
- package/dist/core/hooks/session-name-presence.d.ts +3 -1
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +7 -2
- package/dist/core/hooks/shell-waiter.d.ts +2 -0
- package/dist/core/hooks/shell-waiter.d.ts.map +1 -0
- package/dist/core/hooks/shell-waiter.js +173 -0
- package/dist/core/resources/service.d.ts.map +1 -1
- package/dist/core/resources/service.js +5 -4
- package/dist/core/semantic/review.d.ts.map +1 -1
- package/dist/core/semantic/review.js +5 -4
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +15 -14
- package/dist/core/semantic/storage.d.ts.map +1 -1
- package/dist/core/semantic/storage.js +5 -4
- package/dist/core/servers/index.d.ts +211 -0
- package/dist/core/servers/index.d.ts.map +1 -0
- package/dist/core/servers/index.js +560 -0
- package/dist/core/servers/net.d.ts +58 -0
- package/dist/core/servers/net.d.ts.map +1 -0
- package/dist/core/servers/net.js +270 -0
- package/dist/core/servers/tunnels.d.ts +14 -0
- package/dist/core/servers/tunnels.d.ts.map +1 -0
- package/dist/core/servers/tunnels.js +33 -0
- package/dist/core/storage/atomic-json.d.ts.map +1 -1
- package/dist/core/storage/atomic-json.js +7 -3
- package/dist/core/storage/builtins.js +1 -0
- package/dist/core/storage/durable-history.d.ts.map +1 -1
- package/dist/core/storage/durable-history.js +6 -5
- package/dist/core/storage/maintenance-providers.d.ts.map +1 -1
- package/dist/core/storage/maintenance-providers.js +2 -1
- package/dist/core/storage/maintenance.d.ts.map +1 -1
- package/dist/core/storage/maintenance.js +7 -6
- package/dist/core/storage/metrics.d.ts.map +1 -1
- package/dist/core/storage/metrics.js +5 -1
- package/dist/core/storage/modes.d.ts +17 -0
- package/dist/core/storage/modes.d.ts.map +1 -0
- package/dist/core/storage/modes.js +47 -0
- package/dist/core/storage/process-log.d.ts.map +1 -1
- package/dist/core/storage/process-log.js +3 -2
- package/dist/core/storage/receipts.d.ts.map +1 -1
- package/dist/core/storage/receipts.js +4 -3
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +7 -6
- package/dist/core/supervisor/hook-health.d.ts +2 -1
- package/dist/core/supervisor/hook-health.d.ts.map +1 -1
- package/dist/core/supervisor/hook-health.js +4 -3
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +7 -6
- package/dist/core/work/state.d.ts.map +1 -1
- package/dist/core/work/state.js +8 -7
- package/dist/core/workflow/adapters.d.ts +7 -0
- package/dist/core/workflow/adapters.d.ts.map +1 -1
- package/dist/core/workflow/adapters.js +9 -0
- package/dist/core/workflow/approvals.d.ts.map +1 -1
- package/dist/core/workflow/approvals.js +6 -5
- package/dist/core/workflow/billing.d.ts.map +1 -1
- package/dist/core/workflow/billing.js +33 -4
- package/dist/core/workflow/durable-record.d.ts.map +1 -1
- package/dist/core/workflow/durable-record.js +4 -3
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +30 -1
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +2 -3
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +3 -2
- package/dist/core/workflow/sandbox-projection.d.ts +13 -2
- package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
- package/dist/core/workflow/sandbox-projection.js +23 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +6 -1
- package/dist/core/workflow/spawn-codex.d.ts +2 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +20 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +6 -1
- package/dist/core/workflow/spawn-opencode.d.ts +36 -0
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -0
- package/dist/core/workflow/spawn-opencode.js +135 -0
- package/dist/core/workflow/transcript.d.ts.map +1 -1
- package/dist/core/workflow/transcript.js +3 -2
- package/dist/core/workflow/types.d.ts +47 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/worker-access.d.ts +92 -0
- package/dist/core/workflow/worker-access.d.ts.map +1 -0
- package/dist/core/workflow/worker-access.js +450 -0
- package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/leases.js +5 -4
- package/dist/core/workflow/workspaces/local-git.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/local-git.js +3 -2
- package/dist/core/workflow/workspaces/paths.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/paths.js +2 -1
- package/dist/core/workflow/workspaces/state.d.ts.map +1 -1
- package/dist/core/workflow/workspaces/state.js +10 -9
- package/dist/lazy-commands.js +8 -1
- package/dist/lib/agent-browser/client.d.ts +9 -0
- package/dist/lib/agent-browser/client.d.ts.map +1 -1
- package/dist/lib/agent-browser/client.js +13 -0
- package/dist/lib/browser/batch-steps.d.ts +18 -0
- package/dist/lib/browser/batch-steps.d.ts.map +1 -0
- package/dist/lib/browser/batch-steps.js +82 -0
- package/dist/lib/browser/client.d.ts +35 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +117 -8
- package/dist/lib/browser/collect/collector.d.ts +99 -0
- package/dist/lib/browser/collect/collector.d.ts.map +1 -0
- package/dist/lib/browser/collect/collector.js +202 -0
- package/dist/lib/browser/collect/fields.d.ts +31 -0
- package/dist/lib/browser/collect/fields.d.ts.map +1 -0
- package/dist/lib/browser/collect/fields.js +75 -0
- package/dist/lib/browser/collect/index.d.ts +6 -0
- package/dist/lib/browser/collect/index.d.ts.map +1 -0
- package/dist/lib/browser/collect/index.js +5 -0
- package/dist/lib/browser/collect/page-driver.d.ts +14 -0
- package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
- package/dist/lib/browser/collect/page-driver.js +306 -0
- package/dist/lib/browser/collect/presets.d.ts +52 -0
- package/dist/lib/browser/collect/presets.d.ts.map +1 -0
- package/dist/lib/browser/collect/presets.js +176 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
- package/dist/lib/browser/collect/scroll-plan.js +118 -0
- package/dist/lib/browser/identity.d.ts +25 -0
- package/dist/lib/browser/identity.d.ts.map +1 -0
- package/dist/lib/browser/identity.js +66 -0
- package/dist/lib/browser/index.d.ts +3 -2
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -1
- package/dist/lib/browser/launch-args.d.ts +26 -0
- package/dist/lib/browser/launch-args.d.ts.map +1 -1
- package/dist/lib/browser/launch-args.js +41 -1
- package/dist/lib/browser/page-review-judge.d.ts +2 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +13 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +1 -0
- package/dist/lib/browser/user-agent.d.ts +53 -0
- package/dist/lib/browser/user-agent.d.ts.map +1 -0
- package/dist/lib/browser/user-agent.js +79 -0
- package/dist/lib/browser/windows-chrome.d.ts +59 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
- package/dist/lib/browser/windows-chrome.js +179 -0
- package/dist/lib/claude-desktop-share.d.ts +136 -0
- package/dist/lib/claude-desktop-share.d.ts.map +1 -0
- package/dist/lib/claude-desktop-share.js +394 -0
- package/dist/lib/disk-usage.d.ts +50 -0
- package/dist/lib/disk-usage.d.ts.map +1 -0
- package/dist/lib/disk-usage.js +303 -0
- package/dist/lib/docs-lint.d.ts +6 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +8 -3
- package/dist/lib/docs-metadata-managed.d.ts +1 -0
- package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
- package/dist/lib/docs-metadata-managed.js +17 -1
- package/dist/lib/guarded-remove.d.ts +34 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -0
- package/dist/lib/guarded-remove.js +310 -0
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +4 -9
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +2 -0
- package/dist/lib/instructions/templates.js +8 -8
- package/dist/lib/pace/index.d.ts +2 -0
- package/dist/lib/pace/index.d.ts.map +1 -0
- package/dist/lib/pace/index.js +1 -0
- package/dist/lib/pace/pace.d.ts +95 -0
- package/dist/lib/pace/pace.d.ts.map +1 -0
- package/dist/lib/pace/pace.js +313 -0
- package/dist/lib/tunnel/error-page.d.ts +1 -1
- package/dist/lib/tunnel/error-page.d.ts.map +1 -1
- package/dist/lib/tunnel/error-page.js +19 -8
- package/dist/lib/tunnel/forward-headers.d.ts +11 -0
- package/dist/lib/tunnel/forward-headers.d.ts.map +1 -0
- package/dist/lib/tunnel/forward-headers.js +17 -0
- package/dist/lib/tunnel/gate.js +34 -10
- package/dist/lib/tunnel/path-scope.d.ts +22 -0
- package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
- package/dist/lib/tunnel/path-scope.js +62 -0
- package/dist/lib/tunnel/state.d.ts +5 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +17 -3
- package/dist/lib/webview/index.d.ts +2 -0
- package/dist/lib/webview/index.d.ts.map +1 -0
- package/dist/lib/webview/index.js +1 -0
- package/dist/lib/webview/webview.d.ts +116 -0
- package/dist/lib/webview/webview.d.ts.map +1 -0
- package/dist/lib/webview/webview.js +289 -0
- package/opencode-plugin/package.json +19 -0
- package/opencode-plugin/src/index.ts +791 -0
- package/package.json +16 -4
- package/schemas/config.schema.json +104 -0
- package/schemas/event-v3.schema.json +164 -0
- package/src/commander.ts +26 -0
- package/src/commands/agents.ts +47 -18
- package/src/commands/artifacts.ts +171 -19
- package/src/commands/browse-ai.ts +8 -0
- package/src/commands/browse.ts +570 -48
- package/src/commands/checkpoint.ts +7 -9
- package/src/commands/claude-desktop.ts +61 -0
- package/src/commands/deinit.ts +9 -4
- package/src/commands/disk.ts +114 -0
- package/src/commands/docs.ts +2 -0
- package/src/commands/doctor.ts +63 -3
- package/src/commands/fetch.ts +14 -0
- package/src/commands/files.ts +93 -2
- package/src/commands/init.ts +334 -122
- package/src/commands/instructions.ts +8 -4
- package/src/commands/ledger-v3.ts +2 -1
- package/src/commands/rm.ts +115 -0
- package/src/commands/servers.ts +506 -0
- package/src/commands/tunnel.ts +102 -2
- package/src/commands/webview.ts +159 -0
- package/src/core/adapter.ts +102 -1
- package/src/core/adapters/attestation.ts +3 -2
- package/src/core/adapters/profiles.ts +63 -1
- package/src/core/adapters/registry.ts +37 -0
- package/src/core/adapters/types.ts +10 -0
- package/src/core/agents/canonical-emit.ts +2 -5
- package/src/core/agents/cli.ts +26 -17
- package/src/core/agents/coord-client.ts +9 -3
- package/src/core/agents/render/session-context.ts +1 -0
- package/src/core/agents/rules/claim-conflict.ts +3 -8
- package/src/core/agents/rules/commit-conflict.ts +2 -7
- package/src/core/agents/rules/stop-hook.ts +3 -1
- package/src/core/agents/session-events.ts +120 -26
- package/src/core/agents/session-finalization-state-v3.ts +2 -1
- package/src/core/agents/session-finalizer-v3.ts +5 -4
- package/src/core/agents/session-name-display.ts +48 -0
- package/src/core/agents/state/heartbeat-reader.ts +7 -0
- package/src/core/agents/state/heartbeat-writer.ts +24 -2
- package/src/core/agents/state/live-coordination-view.ts +74 -0
- package/src/core/agents/state/live-coordination-writer.ts +3 -0
- package/src/core/agents/state/stale-sweep.ts +2 -9
- package/src/core/agents/turn-changes.ts +97 -0
- package/src/core/artifacts/delivery-card.ts +12 -3
- package/src/core/artifacts/index.ts +701 -47
- package/src/core/config.ts +206 -4
- package/src/core/context/index.ts +2 -1
- package/src/core/conversations/archive.ts +4 -3
- package/src/core/diagnostics/bundle.ts +5 -3
- package/src/core/events/legacy-storage/canary.ts +5 -4
- package/src/core/events/legacy-storage/compression.ts +2 -1
- package/src/core/events/v3/adapter-id.ts +29 -1
- package/src/core/events/v3/authority-outbox.ts +2 -1
- package/src/core/events/v3/bootstrap.ts +5 -4
- package/src/core/events/v3/capabilities.ts +17 -0
- package/src/core/events/v3/contract.ts +9 -1
- package/src/core/events/v3/control-witness.ts +3 -2
- package/src/core/events/v3/coordination-transaction-recovery.ts +4 -3
- package/src/core/events/v3/fingerprint-keys.ts +7 -6
- package/src/core/events/v3/generated.ts +1 -1
- package/src/core/events/v3/live-feed.ts +5 -4
- package/src/core/events/v3/producers/command-recorder.ts +8 -6
- package/src/core/events/v3/producers/coordination-recorder.ts +8 -7
- package/src/core/events/v3/producers/diagnostic-summaries.ts +5 -4
- package/src/core/events/v3/producers/hook-base.ts +11 -4
- package/src/core/events/v3/producers/hook.ts +19 -1
- package/src/core/events/v3/producers/intake.ts +18 -6
- package/src/core/events/v3/producers/recorder.ts +139 -114
- package/src/core/events/v3/projection.ts +11 -1
- package/src/core/events/v3/recovery.ts +4 -3
- package/src/core/events/v3/support-storage/inventory.ts +2 -1
- package/src/core/events/v3/support-storage/maintenance.ts +8 -3
- package/src/core/events/v3/support-storage/pack-reader.ts +4 -3
- package/src/core/events/v3/support-storage/pack-writer.ts +6 -5
- package/src/core/events/v3/tool-line-changes.ts +139 -0
- package/src/core/events/v3/writer.ts +8 -7
- package/src/core/governor/planning.ts +6 -5
- package/src/core/governor/service.ts +12 -11
- package/src/core/governor/state.ts +7 -6
- package/src/core/guard/evaluator.ts +1 -0
- package/src/core/guard/storage.ts +3 -2
- package/src/core/hooks/adapter/behaviors/claude-code.ts +33 -0
- package/src/core/hooks/adapter/behaviors/codex.ts +96 -0
- package/src/core/hooks/adapter/behaviors/cursor.ts +37 -0
- package/src/core/hooks/adapter/behaviors/index.ts +31 -0
- package/src/core/hooks/adapter/behaviors/openclaw.ts +36 -0
- package/src/core/hooks/adapter/behaviors/opencode.ts +36 -0
- package/src/core/hooks/adapter/behaviors/types.ts +110 -0
- package/src/core/hooks/adapter/detect.ts +2 -3
- package/src/core/hooks/adapter/events.ts +72 -1
- package/src/core/hooks/adapter/opencode-plugin.ts +177 -0
- package/src/core/hooks/adapter/output.ts +8 -1
- package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
- package/src/core/hooks/adapter/wiring.ts +198 -16
- package/src/core/hooks/cli.ts +776 -539
- package/src/core/hooks/effects/image-capture.ts +2 -1
- package/src/core/hooks/prompt-context/contract.ts +1 -0
- package/src/core/hooks/resolve/transcript.ts +42 -5
- package/src/core/hooks/run-markers.ts +132 -0
- package/src/core/hooks/session-name-presence.ts +10 -3
- package/src/core/hooks/shell-waiter.ts +166 -0
- package/src/core/resources/service.ts +5 -4
- package/src/core/semantic/review.ts +5 -4
- package/src/core/semantic/service.ts +15 -14
- package/src/core/semantic/storage.ts +5 -4
- package/src/core/servers/index.ts +712 -0
- package/src/core/servers/net.ts +283 -0
- package/src/core/servers/tunnels.ts +39 -0
- package/src/core/storage/atomic-json.ts +7 -3
- package/src/core/storage/builtins.ts +1 -0
- package/src/core/storage/durable-history.ts +6 -5
- package/src/core/storage/maintenance-providers.ts +2 -1
- package/src/core/storage/maintenance.ts +7 -6
- package/src/core/storage/metrics.ts +5 -1
- package/src/core/storage/modes.ts +53 -0
- package/src/core/storage/process-log.ts +3 -2
- package/src/core/storage/receipts.ts +4 -3
- package/src/core/storage/segments.ts +7 -6
- package/src/core/supervisor/hook-health.ts +4 -4
- package/src/core/supervisor/service.ts +7 -6
- package/src/core/work/state.ts +8 -7
- package/src/core/workflow/adapters.ts +15 -0
- package/src/core/workflow/approvals.ts +6 -5
- package/src/core/workflow/billing.ts +34 -4
- package/src/core/workflow/durable-record.ts +4 -3
- package/src/core/workflow/engine.ts +38 -1
- package/src/core/workflow/live-session-v3.ts +2 -3
- package/src/core/workflow/run-state.ts +3 -2
- package/src/core/workflow/sandbox-projection.ts +38 -2
- package/src/core/workflow/spawn-claude.ts +5 -1
- package/src/core/workflow/spawn-codex.ts +18 -1
- package/src/core/workflow/spawn-cursor.ts +5 -1
- package/src/core/workflow/spawn-opencode.ts +153 -0
- package/src/core/workflow/transcript.ts +3 -2
- package/src/core/workflow/types.ts +62 -2
- package/src/core/workflow/worker-access.ts +525 -0
- package/src/core/workflow/workspaces/leases.ts +5 -4
- package/src/core/workflow/workspaces/local-git.ts +3 -2
- package/src/core/workflow/workspaces/paths.ts +2 -1
- package/src/core/workflow/workspaces/state.ts +10 -9
- package/src/lazy-commands.ts +11 -1
- package/src/lib/agent-browser/client.ts +18 -0
- package/src/lib/browser/batch-steps.ts +79 -0
- package/src/lib/browser/client.ts +147 -9
- package/src/lib/browser/collect/collector.ts +306 -0
- package/src/lib/browser/collect/fields.ts +87 -0
- package/src/lib/browser/collect/index.ts +47 -0
- package/src/lib/browser/collect/page-driver.ts +327 -0
- package/src/lib/browser/collect/presets.ts +237 -0
- package/src/lib/browser/collect/scroll-plan.ts +179 -0
- package/src/lib/browser/identity.ts +64 -0
- package/src/lib/browser/index.ts +16 -1
- package/src/lib/browser/launch-args.ts +53 -1
- package/src/lib/browser/page-review-judge.ts +16 -0
- package/src/lib/browser/qa-run-contracts.ts +11 -0
- package/src/lib/browser/qa-run.ts +1 -0
- package/src/lib/browser/user-agent.ts +130 -0
- package/src/lib/browser/windows-chrome.ts +227 -0
- package/src/lib/claude-desktop-share.ts +511 -0
- package/src/lib/disk-usage.ts +347 -0
- package/src/lib/docs-lint.ts +8 -3
- package/src/lib/docs-metadata-managed.ts +21 -0
- package/src/lib/guarded-remove.ts +376 -0
- package/src/lib/identities/assume.ts +7 -7
- package/src/lib/instructions/apply.ts +2 -0
- package/src/lib/instructions/templates.ts +8 -8
- package/src/lib/pace/index.ts +14 -0
- package/src/lib/pace/pace.ts +370 -0
- package/src/lib/tunnel/error-page.ts +25 -9
- package/src/lib/tunnel/forward-headers.ts +16 -0
- package/src/lib/tunnel/gate.ts +39 -10
- package/src/lib/tunnel/path-scope.ts +73 -0
- package/src/lib/tunnel/state.ts +20 -3
- package/src/lib/webview/index.ts +13 -0
- package/src/lib/webview/webview.ts +457 -0
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local server registry.
|
|
3
|
+
*
|
|
4
|
+
* Every command that starts a long-lived local server (a preview UI, a tuning
|
|
5
|
+
* panel, a tunnel gate, a dashboard) writes one record under
|
|
6
|
+
* `.harnery/servers/<id>.json` and removes it when the server stops. One
|
|
7
|
+
* reader can then list every running server, stop any of them, and clean up
|
|
8
|
+
* session servers whose starting agent is gone.
|
|
9
|
+
*
|
|
10
|
+
* Two kinds:
|
|
11
|
+
* - `service`: one long-running copy (a dashboard, a tunnel). Listed and
|
|
12
|
+
* stoppable by hand; never stopped automatically.
|
|
13
|
+
* - `session`: one copy per piece of work (a preview for one scene). Eligible
|
|
14
|
+
* for `gc` once its starting agent has ended or been unobserved for a long
|
|
15
|
+
* time AND nobody has used the server for the idle window.
|
|
16
|
+
*
|
|
17
|
+
* Fail-closed rules: a record whose pid was recycled is treated as dead, never
|
|
18
|
+
* signalled; an unreadable coordination view makes every owner `unknown`,
|
|
19
|
+
* which `gc` never stops; a record without an owner is never stopped
|
|
20
|
+
* automatically.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { spawnSync } from "node:child_process";
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
25
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
26
|
+
import { hostname } from "node:os";
|
|
27
|
+
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
28
|
+
import { listStates } from "../../lib/tunnel/state.ts";
|
|
29
|
+
import { resolveCoordRoot } from "../agents/coord-client.ts";
|
|
30
|
+
import { readLiveCoordinationRow } from "../agents/state/live-coordination-view.ts";
|
|
31
|
+
import { checkPidToken, processStartToken } from "../agents/state/proc-start.ts";
|
|
32
|
+
import { serverPortRange } from "../config.ts";
|
|
33
|
+
import {
|
|
34
|
+
readCoordinationViewV3,
|
|
35
|
+
requireAuthoritySafeCoordinationViewV3,
|
|
36
|
+
} from "../events/v3/coordination-view.ts";
|
|
37
|
+
import { liveInstanceIdV3 } from "../events/v3/live-route-observer.ts";
|
|
38
|
+
import { resolveOwner } from "../hooks/resolve/owner.ts";
|
|
39
|
+
import { writePrivateJsonAtomic } from "../storage/atomic-json.ts";
|
|
40
|
+
import {
|
|
41
|
+
ancestry,
|
|
42
|
+
establishedConnectionCounts,
|
|
43
|
+
listListeningPorts,
|
|
44
|
+
listListeningSockets,
|
|
45
|
+
processStartedAt,
|
|
46
|
+
readProcessInfo,
|
|
47
|
+
type ScanSupport,
|
|
48
|
+
scanSupport,
|
|
49
|
+
} from "./net.ts";
|
|
50
|
+
import { tunnelServerId, tunnelServerInput } from "./tunnels.ts";
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
ancestry,
|
|
54
|
+
establishedConnectionCounts,
|
|
55
|
+
listListeningPorts,
|
|
56
|
+
listListeningSockets,
|
|
57
|
+
lsofConnectionCounts,
|
|
58
|
+
lsofListeningSockets,
|
|
59
|
+
parseElapsed,
|
|
60
|
+
parseLsofListen,
|
|
61
|
+
parseProcNetTcp,
|
|
62
|
+
processStartedAt,
|
|
63
|
+
psProcessInfo,
|
|
64
|
+
readProcessInfo,
|
|
65
|
+
scanSupport,
|
|
66
|
+
} from "./net.ts";
|
|
67
|
+
export { tunnelServerId, tunnelServerInput } from "./tunnels.ts";
|
|
68
|
+
|
|
69
|
+
export const SERVER_RECORD_SCHEMA_VERSION = 1;
|
|
70
|
+
export const SERVER_DEFAULT_IDLE_HOURS = 2;
|
|
71
|
+
export const SERVER_DEFAULT_OWNER_STALE_HOURS = 24;
|
|
72
|
+
const TOUCH_INTERVAL_MS = 60_000;
|
|
73
|
+
|
|
74
|
+
export type ServerKind = "service" | "session";
|
|
75
|
+
|
|
76
|
+
export interface ServerRecord {
|
|
77
|
+
schema_version: typeof SERVER_RECORD_SCHEMA_VERSION;
|
|
78
|
+
id: string;
|
|
79
|
+
kind: ServerKind;
|
|
80
|
+
/** Free-form family name: `preview`, `tunnel`, `dashboard`, ... */
|
|
81
|
+
type: string;
|
|
82
|
+
label: string;
|
|
83
|
+
url?: string;
|
|
84
|
+
port?: number;
|
|
85
|
+
/** The process that owns the server's lifetime (signalled on stop). */
|
|
86
|
+
pid: number;
|
|
87
|
+
start_token?: string;
|
|
88
|
+
/** Further processes that belong to this server (helpers, children). */
|
|
89
|
+
pids?: number[];
|
|
90
|
+
host: string;
|
|
91
|
+
/** What the server serves, usually a directory. */
|
|
92
|
+
scope?: string;
|
|
93
|
+
cwd?: string;
|
|
94
|
+
log?: string;
|
|
95
|
+
/** Graceful stop command; signals are the fallback. */
|
|
96
|
+
stop_argv?: string[];
|
|
97
|
+
owner?: { instance_id: string; name?: string };
|
|
98
|
+
started_at: string;
|
|
99
|
+
registered_at: string;
|
|
100
|
+
last_active_at?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface RegisterServerInput {
|
|
104
|
+
id?: string;
|
|
105
|
+
kind: ServerKind;
|
|
106
|
+
type: string;
|
|
107
|
+
label?: string;
|
|
108
|
+
url?: string;
|
|
109
|
+
port?: number;
|
|
110
|
+
pid?: number;
|
|
111
|
+
pids?: number[];
|
|
112
|
+
scope?: string;
|
|
113
|
+
cwd?: string;
|
|
114
|
+
log?: string;
|
|
115
|
+
stop_argv?: string[];
|
|
116
|
+
started_at?: string;
|
|
117
|
+
/** `null` records no owner; omitted resolves the calling agent session. */
|
|
118
|
+
owner?: { instance_id: string; name?: string } | null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ServerOptions {
|
|
122
|
+
coordRoot?: string | null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type ServerState = "running" | "dead" | "other-host";
|
|
126
|
+
export type OwnerState = "live" | "abandoned" | "ended" | "unknown" | "none";
|
|
127
|
+
|
|
128
|
+
export interface ServerView {
|
|
129
|
+
record: ServerRecord;
|
|
130
|
+
state: ServerState;
|
|
131
|
+
owner_state: OwnerState;
|
|
132
|
+
connections: number | null;
|
|
133
|
+
last_active_at: string;
|
|
134
|
+
idle_ms: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface UnregisteredListener {
|
|
138
|
+
pid: number;
|
|
139
|
+
port: number;
|
|
140
|
+
address: string;
|
|
141
|
+
command: string;
|
|
142
|
+
cwd: string | null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface ServersReport {
|
|
146
|
+
schema_version: 1;
|
|
147
|
+
coord_root: string;
|
|
148
|
+
scan: ScanSupport;
|
|
149
|
+
servers: ServerView[];
|
|
150
|
+
unregistered: UnregisteredListener[];
|
|
151
|
+
pruned: string[];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function serversDir(coordRoot: string): string {
|
|
155
|
+
return join(coordRoot, ".harnery", "servers");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function rootOf(options?: ServerOptions): string | null {
|
|
159
|
+
return options?.coordRoot === undefined ? resolveCoordRoot() : options.coordRoot;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A stable id for one server family serving one scope. */
|
|
163
|
+
export function serverId(type: string, scope?: string): string {
|
|
164
|
+
const family = slug(type) || "server";
|
|
165
|
+
if (!scope) return family;
|
|
166
|
+
const digest = createHash("sha256").update(resolve(scope)).digest("hex").slice(0, 6);
|
|
167
|
+
const name = slug(basename(resolve(scope))).slice(0, 32);
|
|
168
|
+
return [family, name, digest].filter(Boolean).join("-");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function slug(value: string): string {
|
|
172
|
+
return value
|
|
173
|
+
.toLowerCase()
|
|
174
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
175
|
+
.replace(/^-+|-+$/g, "");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function isValidServerId(id: string): boolean {
|
|
179
|
+
return /^[a-z0-9][a-z0-9._-]{0,95}$/.test(id);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function recordPath(coordRoot: string, id: string): string {
|
|
183
|
+
if (!isValidServerId(id)) throw new Error(`invalid server id: ${id}`);
|
|
184
|
+
return join(serversDir(coordRoot), `${id}.json`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Record a running server. Returns null when no coordination root resolves
|
|
189
|
+
* (a tool running outside any Harnery project), so callers never fail
|
|
190
|
+
* because registration was impossible.
|
|
191
|
+
*/
|
|
192
|
+
export function registerServer(
|
|
193
|
+
input: RegisterServerInput,
|
|
194
|
+
options?: ServerOptions,
|
|
195
|
+
): ServerRecord | null {
|
|
196
|
+
const coordRoot = rootOf(options);
|
|
197
|
+
if (!coordRoot) return null;
|
|
198
|
+
const pid = input.pid ?? process.pid;
|
|
199
|
+
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error(`invalid server pid: ${pid}`);
|
|
200
|
+
if (
|
|
201
|
+
input.port !== undefined &&
|
|
202
|
+
!(Number.isInteger(input.port) && input.port > 0 && input.port < 65536)
|
|
203
|
+
)
|
|
204
|
+
throw new Error(`invalid server port: ${input.port}`);
|
|
205
|
+
const id = input.id ?? serverId(input.type, input.scope);
|
|
206
|
+
const now = new Date().toISOString();
|
|
207
|
+
const startToken = processStartToken(pid);
|
|
208
|
+
const owner = input.owner === undefined ? callingOwner(coordRoot) : (input.owner ?? undefined);
|
|
209
|
+
const record: ServerRecord = {
|
|
210
|
+
schema_version: SERVER_RECORD_SCHEMA_VERSION,
|
|
211
|
+
id,
|
|
212
|
+
kind: input.kind,
|
|
213
|
+
type: input.type,
|
|
214
|
+
label: input.label ?? id,
|
|
215
|
+
...(input.url ? { url: input.url } : {}),
|
|
216
|
+
...(input.port ? { port: input.port } : {}),
|
|
217
|
+
pid,
|
|
218
|
+
...(startToken ? { start_token: startToken } : {}),
|
|
219
|
+
...(input.pids?.length
|
|
220
|
+
? { pids: input.pids.filter((p) => Number.isSafeInteger(p) && p > 0) }
|
|
221
|
+
: {}),
|
|
222
|
+
host: hostname(),
|
|
223
|
+
...(input.scope ? { scope: resolve(input.scope) } : {}),
|
|
224
|
+
...(input.cwd ? { cwd: resolve(input.cwd) } : {}),
|
|
225
|
+
...(input.log ? { log: resolve(input.log) } : {}),
|
|
226
|
+
...(input.stop_argv?.length ? { stop_argv: input.stop_argv } : {}),
|
|
227
|
+
...(owner ? { owner } : {}),
|
|
228
|
+
started_at: input.started_at ?? now,
|
|
229
|
+
registered_at: now,
|
|
230
|
+
last_active_at: now,
|
|
231
|
+
};
|
|
232
|
+
writePrivateJsonAtomic(recordPath(coordRoot, id), record);
|
|
233
|
+
return record;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Best-effort registration that never throws (for server start paths). */
|
|
237
|
+
export function tryRegisterServer(
|
|
238
|
+
input: RegisterServerInput,
|
|
239
|
+
options?: ServerOptions,
|
|
240
|
+
): ServerRecord | null {
|
|
241
|
+
try {
|
|
242
|
+
return registerServer(input, options);
|
|
243
|
+
} catch {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function callingOwner(coordRoot: string): ServerRecord["owner"] {
|
|
249
|
+
try {
|
|
250
|
+
const owner = resolveOwner({ payload: null, coordRoot });
|
|
251
|
+
if (!owner) return undefined;
|
|
252
|
+
const name = readLiveCoordinationRow(coordRoot, owner.instance_id)?.name;
|
|
253
|
+
return { instance_id: owner.instance_id, ...(name ? { name } : {}) };
|
|
254
|
+
} catch {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Remove a server's record. With `pid`, only a record still naming that pid
|
|
261
|
+
* is removed, so a replacement that re-registered the same id survives.
|
|
262
|
+
*/
|
|
263
|
+
export function unregisterServer(id: string, options?: ServerOptions & { pid?: number }): boolean {
|
|
264
|
+
const coordRoot = rootOf(options);
|
|
265
|
+
if (!coordRoot || !isValidServerId(id)) return false;
|
|
266
|
+
const path = recordPath(coordRoot, id);
|
|
267
|
+
if (options?.pid !== undefined) {
|
|
268
|
+
const record = readRecord(path);
|
|
269
|
+
if (record && record.pid !== options.pid) return false;
|
|
270
|
+
}
|
|
271
|
+
if (!existsSync(path)) return false;
|
|
272
|
+
rmSync(path, { force: true });
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function readServer(id: string, options?: ServerOptions): ServerRecord | null {
|
|
277
|
+
const coordRoot = rootOf(options);
|
|
278
|
+
if (!coordRoot || !isValidServerId(id)) return null;
|
|
279
|
+
return readRecord(recordPath(coordRoot, id));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Mark a server as used now. */
|
|
283
|
+
export function touchServer(id: string, options?: ServerOptions & { now?: Date }): void {
|
|
284
|
+
const coordRoot = rootOf(options);
|
|
285
|
+
if (!coordRoot || !isValidServerId(id)) return;
|
|
286
|
+
const path = recordPath(coordRoot, id);
|
|
287
|
+
const record = readRecord(path);
|
|
288
|
+
if (!record) return;
|
|
289
|
+
writePrivateJsonAtomic(path, {
|
|
290
|
+
...record,
|
|
291
|
+
last_active_at: (options?.now ?? new Date()).toISOString(),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* A request hook for servers that want precise idle tracking: call the
|
|
297
|
+
* returned function on every request; it writes at most once a minute.
|
|
298
|
+
*/
|
|
299
|
+
export function createServerToucher(id: string, options?: ServerOptions): () => void {
|
|
300
|
+
let last = 0;
|
|
301
|
+
return () => {
|
|
302
|
+
const now = Date.now();
|
|
303
|
+
if (now - last < TOUCH_INTERVAL_MS) return;
|
|
304
|
+
last = now;
|
|
305
|
+
try {
|
|
306
|
+
touchServer(id, options);
|
|
307
|
+
} catch {
|
|
308
|
+
// Activity tracking must never break the server.
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function readRecord(path: string): ServerRecord | null {
|
|
314
|
+
try {
|
|
315
|
+
if (!existsSync(path) || statSync(path).size > 64 * 1024) return null;
|
|
316
|
+
const value = JSON.parse(readFileSync(path, "utf8")) as ServerRecord;
|
|
317
|
+
if (
|
|
318
|
+
value?.schema_version !== SERVER_RECORD_SCHEMA_VERSION ||
|
|
319
|
+
typeof value.id !== "string" ||
|
|
320
|
+
!Number.isSafeInteger(value.pid) ||
|
|
321
|
+
(value.kind !== "service" && value.kind !== "session")
|
|
322
|
+
)
|
|
323
|
+
return null;
|
|
324
|
+
return value;
|
|
325
|
+
} catch {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function readServerRecords(options?: ServerOptions): ServerRecord[] {
|
|
331
|
+
const coordRoot = rootOf(options);
|
|
332
|
+
if (!coordRoot) return [];
|
|
333
|
+
const dir = serversDir(coordRoot);
|
|
334
|
+
if (!existsSync(dir)) return [];
|
|
335
|
+
return readdirSync(dir)
|
|
336
|
+
.filter((name) => name.endsWith(".json"))
|
|
337
|
+
.map((name) => readRecord(join(dir, name)))
|
|
338
|
+
.filter((record): record is ServerRecord => record !== null)
|
|
339
|
+
.sort(
|
|
340
|
+
(a, b) =>
|
|
341
|
+
a.kind.localeCompare(b.kind) || a.type.localeCompare(b.type) || a.id.localeCompare(b.id),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function pidAlive(pid: number): boolean {
|
|
346
|
+
try {
|
|
347
|
+
process.kill(pid, 0);
|
|
348
|
+
return true;
|
|
349
|
+
} catch (error) {
|
|
350
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function serverState(record: ServerRecord): ServerState {
|
|
355
|
+
if (record.host !== hostname()) return "other-host";
|
|
356
|
+
if (!pidAlive(record.pid)) return "dead";
|
|
357
|
+
return checkPidToken(record.pid, record.start_token) === "mismatch" ? "dead" : "running";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
type OwnerReader = (instanceId: string) => OwnerState;
|
|
361
|
+
|
|
362
|
+
function ownerReader(coordRoot: string, ownerStaleMs: number, nowMs: number): OwnerReader {
|
|
363
|
+
let view: ReturnType<typeof readCoordinationViewV3> | null = null;
|
|
364
|
+
let failed = false;
|
|
365
|
+
try {
|
|
366
|
+
view = requireAuthoritySafeCoordinationViewV3(readCoordinationViewV3(coordRoot));
|
|
367
|
+
} catch {
|
|
368
|
+
failed = true;
|
|
369
|
+
}
|
|
370
|
+
return (instanceId) => {
|
|
371
|
+
if (failed || !view) return "unknown";
|
|
372
|
+
const generation = view.instances[liveInstanceIdV3(instanceId)];
|
|
373
|
+
if (generation?.phase !== "live") return "ended";
|
|
374
|
+
const observed = Date.parse(generation.last_observed_at);
|
|
375
|
+
if (!Number.isFinite(observed)) return "unknown";
|
|
376
|
+
return nowMs - observed > ownerStaleMs ? "abandoned" : "live";
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export interface ListServersOptions extends ServerOptions {
|
|
381
|
+
/** Look for listening processes inside the project that never registered. */
|
|
382
|
+
scan?: boolean;
|
|
383
|
+
/** Record observed connections as activity (default true). */
|
|
384
|
+
sample?: boolean;
|
|
385
|
+
/** Delete records of servers that are no longer running (default true). */
|
|
386
|
+
prune?: boolean;
|
|
387
|
+
ownerStaleHours?: number;
|
|
388
|
+
now?: Date;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Read every record, observe it, and optionally find unregistered listeners. */
|
|
392
|
+
export function listServers(options?: ListServersOptions): ServersReport {
|
|
393
|
+
const coordRoot = rootOf(options);
|
|
394
|
+
if (!coordRoot) throw new Error("no Harnery project found (coordination root did not resolve)");
|
|
395
|
+
const now = options?.now ?? new Date();
|
|
396
|
+
const nowMs = now.getTime();
|
|
397
|
+
const ownerStaleMs = hours(options?.ownerStaleHours ?? SERVER_DEFAULT_OWNER_STALE_HOURS);
|
|
398
|
+
adoptRunningTunnels(coordRoot);
|
|
399
|
+
const records = readServerRecords({ coordRoot });
|
|
400
|
+
const ownerOf = ownerReader(coordRoot, ownerStaleMs, nowMs);
|
|
401
|
+
const states = records.map((record) => serverState(record));
|
|
402
|
+
const counts = establishedConnectionCounts(
|
|
403
|
+
records.filter((_, i) => states[i] === "running").map((r) => r.port ?? 0),
|
|
404
|
+
);
|
|
405
|
+
const support = scanSupport();
|
|
406
|
+
const pruned: string[] = [];
|
|
407
|
+
const servers: ServerView[] = [];
|
|
408
|
+
records.forEach((record, i) => {
|
|
409
|
+
const state = states[i]!;
|
|
410
|
+
if (state === "dead" && options?.prune !== false) {
|
|
411
|
+
unregisterServer(record.id, { coordRoot, pid: record.pid });
|
|
412
|
+
pruned.push(record.id);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const connections =
|
|
416
|
+
state === "running" && record.port && support !== "unsupported"
|
|
417
|
+
? (counts.get(record.port) ?? 0)
|
|
418
|
+
: null;
|
|
419
|
+
let lastActive = record.last_active_at ?? record.registered_at;
|
|
420
|
+
if (connections && options?.sample !== false) {
|
|
421
|
+
lastActive = now.toISOString();
|
|
422
|
+
try {
|
|
423
|
+
touchServer(record.id, { coordRoot, now });
|
|
424
|
+
} catch {
|
|
425
|
+
// Observation stays correct for this read even if the write fails.
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const lastMs = Date.parse(lastActive);
|
|
429
|
+
servers.push({
|
|
430
|
+
record,
|
|
431
|
+
state,
|
|
432
|
+
owner_state: record.owner ? ownerOf(record.owner.instance_id) : "none",
|
|
433
|
+
connections,
|
|
434
|
+
last_active_at: lastActive,
|
|
435
|
+
idle_ms: Number.isFinite(lastMs) ? Math.max(0, nowMs - lastMs) : 0,
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
const unregistered = options?.scan === false ? [] : findUnregisteredListeners(coordRoot, servers);
|
|
439
|
+
return {
|
|
440
|
+
schema_version: 1,
|
|
441
|
+
coord_root: coordRoot,
|
|
442
|
+
scan: support,
|
|
443
|
+
servers,
|
|
444
|
+
unregistered,
|
|
445
|
+
pruned,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** Register running tunnels that have no current record. Returns adopted ids. */
|
|
450
|
+
export function adoptRunningTunnels(coordRoot: string): string[] {
|
|
451
|
+
const adopted: string[] = [];
|
|
452
|
+
let states: ReturnType<typeof listStates>;
|
|
453
|
+
try {
|
|
454
|
+
states = listStates(coordRoot);
|
|
455
|
+
} catch {
|
|
456
|
+
return adopted;
|
|
457
|
+
}
|
|
458
|
+
for (const state of states) {
|
|
459
|
+
if (!state.gate_pid || !pidAlive(state.gate_pid)) continue;
|
|
460
|
+
const id = tunnelServerId(state.name);
|
|
461
|
+
if (readServer(id, { coordRoot })?.pid === state.gate_pid) continue;
|
|
462
|
+
if (tryRegisterServer(tunnelServerInput(state, coordRoot), { coordRoot })) adopted.push(id);
|
|
463
|
+
}
|
|
464
|
+
return adopted;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Processes that browser automation and editors start; never project servers. */
|
|
468
|
+
const IGNORED_LISTENER_COMMANDS = [
|
|
469
|
+
/(^|\/)(chrome|chromium|chrome-headless-shell|headless_shell|msedge|firefox)( |$)/i,
|
|
470
|
+
/\/(\.vscode-server|\.cursor-server|\.windsurf-server)\//,
|
|
471
|
+
/--type=(renderer|gpu-process|utility)/,
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Listening processes that run inside the project (working directory or
|
|
476
|
+
* command line within the coordination root) but are not covered by any
|
|
477
|
+
* registered server: not a recorded pid, not a descendant of one, and not on
|
|
478
|
+
* a recorded port.
|
|
479
|
+
*/
|
|
480
|
+
export function findUnregisteredListeners(
|
|
481
|
+
coordRoot: string,
|
|
482
|
+
servers: Array<Pick<ServerView, "record" | "state">>,
|
|
483
|
+
): UnregisteredListener[] {
|
|
484
|
+
const running = servers.filter((server) => server.state === "running");
|
|
485
|
+
const covered = new Set<number>();
|
|
486
|
+
const ports = new Set<number>();
|
|
487
|
+
for (const { record } of running) {
|
|
488
|
+
covered.add(record.pid);
|
|
489
|
+
for (const pid of record.pids ?? []) covered.add(pid);
|
|
490
|
+
if (record.port) ports.add(record.port);
|
|
491
|
+
}
|
|
492
|
+
const results: UnregisteredListener[] = [];
|
|
493
|
+
const seenPids = new Map<number, boolean>();
|
|
494
|
+
for (const socket of listListeningSockets()) {
|
|
495
|
+
if (ports.has(socket.port) || socket.pid === process.pid) continue;
|
|
496
|
+
let inside = seenPids.get(socket.pid);
|
|
497
|
+
const info = readProcessInfo(socket.pid);
|
|
498
|
+
if (!info) continue;
|
|
499
|
+
if (inside === undefined) {
|
|
500
|
+
inside =
|
|
501
|
+
!IGNORED_LISTENER_COMMANDS.some((pattern) => pattern.test(info.command)) &&
|
|
502
|
+
(within(coordRoot, info.cwd) || mentions(info.command, coordRoot)) &&
|
|
503
|
+
!ancestry(socket.pid).some((pid) => covered.has(pid));
|
|
504
|
+
seenPids.set(socket.pid, inside);
|
|
505
|
+
}
|
|
506
|
+
if (!inside) continue;
|
|
507
|
+
results.push({
|
|
508
|
+
pid: socket.pid,
|
|
509
|
+
port: socket.port,
|
|
510
|
+
address: socket.address,
|
|
511
|
+
command: info.command.slice(0, 300),
|
|
512
|
+
cwd: info.cwd,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return results;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function within(root: string, path: string | null): boolean {
|
|
519
|
+
if (!path) return false;
|
|
520
|
+
const rel = relative(root, path);
|
|
521
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function mentions(command: string, root: string): boolean {
|
|
525
|
+
return command.split(/\s+/).some((arg) => arg === root || arg.startsWith(`${root}/`));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function hours(value: number): number {
|
|
529
|
+
return value * 3_600_000;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export interface GcPolicy {
|
|
533
|
+
idleHours?: number;
|
|
534
|
+
ownerStaleHours?: number;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export interface GcDecision {
|
|
538
|
+
id: string;
|
|
539
|
+
action: "stop" | "keep";
|
|
540
|
+
reason: string;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Decide which session servers to stop. Pure: the caller supplies the views.
|
|
545
|
+
* A server is stopped only when all hold: kind `session`, running on this
|
|
546
|
+
* host, its owner ended or went unobserved past the stale window, and it has
|
|
547
|
+
* been idle for the idle window.
|
|
548
|
+
*/
|
|
549
|
+
export function planServerGc(views: ServerView[], policy?: GcPolicy): GcDecision[] {
|
|
550
|
+
const idleMs = hours(policy?.idleHours ?? SERVER_DEFAULT_IDLE_HOURS);
|
|
551
|
+
return views.map(({ record, state, owner_state, idle_ms }) => {
|
|
552
|
+
const keep = (reason: string): GcDecision => ({ id: record.id, action: "keep", reason });
|
|
553
|
+
if (state !== "running") return keep(`server is ${state}`);
|
|
554
|
+
if (record.kind !== "session") return keep("services are only stopped by hand");
|
|
555
|
+
if (owner_state === "none") return keep("no starting agent was recorded");
|
|
556
|
+
if (owner_state === "unknown") return keep("the starting agent's state could not be read");
|
|
557
|
+
if (owner_state === "live") return keep("the starting agent is still active");
|
|
558
|
+
if (idle_ms < idleMs)
|
|
559
|
+
return keep(`used within the last ${policy?.idleHours ?? SERVER_DEFAULT_IDLE_HOURS}h`);
|
|
560
|
+
return {
|
|
561
|
+
id: record.id,
|
|
562
|
+
action: "stop",
|
|
563
|
+
reason: `starting agent ${owner_state === "ended" ? "has ended" : "has been unobserved past the stale window"} and the server has been idle ${Math.round(idle_ms / 60_000)} min`,
|
|
564
|
+
};
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export interface StopResult {
|
|
569
|
+
id: string;
|
|
570
|
+
stopped: boolean;
|
|
571
|
+
method: "not-running" | "stop-command" | "signal" | "refused";
|
|
572
|
+
detail?: string;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Stop one server: its graceful stop command first when recorded, then
|
|
577
|
+
* SIGTERM to its process group, then SIGKILL after the grace period. A pid
|
|
578
|
+
* whose start token no longer matches is never signalled.
|
|
579
|
+
*/
|
|
580
|
+
export async function stopServer(
|
|
581
|
+
record: ServerRecord,
|
|
582
|
+
options?: ServerOptions & { graceMs?: number },
|
|
583
|
+
): Promise<StopResult> {
|
|
584
|
+
const coordRoot = rootOf(options);
|
|
585
|
+
const graceMs = options?.graceMs ?? 5_000;
|
|
586
|
+
const finish = (result: StopResult): StopResult => {
|
|
587
|
+
if (result.stopped || result.method === "not-running")
|
|
588
|
+
unregisterServer(record.id, { coordRoot, pid: record.pid });
|
|
589
|
+
return result;
|
|
590
|
+
};
|
|
591
|
+
const state = serverState(record);
|
|
592
|
+
if (state === "other-host")
|
|
593
|
+
return { id: record.id, stopped: false, method: "refused", detail: `runs on ${record.host}` };
|
|
594
|
+
if (state === "dead") return finish({ id: record.id, stopped: false, method: "not-running" });
|
|
595
|
+
|
|
596
|
+
if (record.stop_argv?.length) {
|
|
597
|
+
const [command, ...args] = record.stop_argv;
|
|
598
|
+
spawnSync(command!, args, { stdio: "ignore", timeout: 30_000, cwd: record.cwd });
|
|
599
|
+
if (await exited(record, graceMs))
|
|
600
|
+
return finish({ id: record.id, stopped: true, method: "stop-command" });
|
|
601
|
+
}
|
|
602
|
+
signal(record.pid, "SIGTERM");
|
|
603
|
+
for (const pid of record.pids ?? []) signal(pid, "SIGTERM");
|
|
604
|
+
if (await exited(record, graceMs))
|
|
605
|
+
return finish({ id: record.id, stopped: true, method: "signal" });
|
|
606
|
+
if (serverState(record) === "running") {
|
|
607
|
+
signal(record.pid, "SIGKILL");
|
|
608
|
+
for (const pid of record.pids ?? []) signal(pid, "SIGKILL");
|
|
609
|
+
}
|
|
610
|
+
const stopped = await exited(record, 2_000);
|
|
611
|
+
return finish({
|
|
612
|
+
id: record.id,
|
|
613
|
+
stopped,
|
|
614
|
+
method: "signal",
|
|
615
|
+
...(stopped ? {} : { detail: "process survived SIGKILL" }),
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function signal(pid: number, name: NodeJS.Signals): void {
|
|
620
|
+
// Detached servers lead their own process group; signal the group so a
|
|
621
|
+
// wrapper (npm, bunx) does not orphan the real listener.
|
|
622
|
+
try {
|
|
623
|
+
process.kill(-pid, name);
|
|
624
|
+
return;
|
|
625
|
+
} catch {
|
|
626
|
+
// Not a group leader, or already gone.
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
process.kill(pid, name);
|
|
630
|
+
} catch {
|
|
631
|
+
// Already gone.
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function exited(record: ServerRecord, waitMs: number): Promise<boolean> {
|
|
636
|
+
const deadline = Date.now() + waitMs;
|
|
637
|
+
while (Date.now() < deadline) {
|
|
638
|
+
if (serverState(record) !== "running") return true;
|
|
639
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
640
|
+
}
|
|
641
|
+
return serverState(record) !== "running";
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* The first free port in the configured range for `type`
|
|
646
|
+
* (`.harnery/config.jsonc` `servers.port_ranges`). Skips ports any process is
|
|
647
|
+
* listening on and ports a registered server claims. Returns null when the
|
|
648
|
+
* type has no configured range; throws when the range is exhausted.
|
|
649
|
+
*/
|
|
650
|
+
export function allocateServerPort(type: string, options?: ServerOptions): number | null {
|
|
651
|
+
const coordRoot = rootOf(options);
|
|
652
|
+
const range = serverPortRange(type, coordRoot);
|
|
653
|
+
if (!range) return null;
|
|
654
|
+
const taken = listListeningPorts();
|
|
655
|
+
for (const record of readServerRecords({ coordRoot })) {
|
|
656
|
+
if (record.port && serverState(record) === "running") taken.add(record.port);
|
|
657
|
+
}
|
|
658
|
+
for (let port = range[0]; port <= range[1]; port++) if (!taken.has(port)) return port;
|
|
659
|
+
throw new Error(`no free port for ${type} in ${range[0]}-${range[1]}`);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export interface AdoptOptions extends ServerOptions {
|
|
663
|
+
type?: string;
|
|
664
|
+
kind?: ServerKind;
|
|
665
|
+
label?: string;
|
|
666
|
+
scope?: string;
|
|
667
|
+
/** `self` (default) records the adopting agent as owner; `none` records no owner. */
|
|
668
|
+
owner?: "self" | "none";
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Register a running server that never registered itself. Only a pid the scan
|
|
673
|
+
* reports as an unregistered listener inside the project can be adopted.
|
|
674
|
+
*/
|
|
675
|
+
export function adoptServer(pid: number, options?: AdoptOptions): ServerRecord {
|
|
676
|
+
const coordRoot = rootOf(options);
|
|
677
|
+
if (!coordRoot) throw new Error("no Harnery project found (coordination root did not resolve)");
|
|
678
|
+
const report = listServers({ coordRoot, sample: false });
|
|
679
|
+
const listeners = report.unregistered.filter((item) => item.pid === pid);
|
|
680
|
+
if (!listeners.length)
|
|
681
|
+
throw new Error(`pid ${pid} is not an unregistered listener inside this project`);
|
|
682
|
+
const ports = listeners.map((item) => item.port).sort((a, b) => a - b);
|
|
683
|
+
const first = listeners[0]!;
|
|
684
|
+
const type = options?.type ?? "adopted";
|
|
685
|
+
const scope = options?.scope ?? first.cwd ?? undefined;
|
|
686
|
+
const started = processStartedAt(pid);
|
|
687
|
+
const record = registerServer(
|
|
688
|
+
{
|
|
689
|
+
id: serverId(type, `${scope ?? ""}#${pid}`),
|
|
690
|
+
kind: options?.kind ?? "session",
|
|
691
|
+
type,
|
|
692
|
+
label: options?.label ?? `Adopted: ${first.command.slice(0, 80)}`,
|
|
693
|
+
url: `http://127.0.0.1:${ports[0]}/`,
|
|
694
|
+
port: ports[0],
|
|
695
|
+
pid,
|
|
696
|
+
...(scope ? { scope } : {}),
|
|
697
|
+
...(first.cwd ? { cwd: first.cwd } : {}),
|
|
698
|
+
...(options?.owner === "none" ? { owner: null } : {}),
|
|
699
|
+
...(started ? { started_at: started } : {}),
|
|
700
|
+
},
|
|
701
|
+
{ coordRoot },
|
|
702
|
+
);
|
|
703
|
+
if (!record) throw new Error("registration failed");
|
|
704
|
+
return record;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/** Ensure the registry directory exists (for hosts that write records directly). */
|
|
708
|
+
export function ensureServersDir(coordRoot: string): string {
|
|
709
|
+
const dir = serversDir(coordRoot);
|
|
710
|
+
mkdirSync(dir, { recursive: true });
|
|
711
|
+
return dir;
|
|
712
|
+
}
|