vellum 0.2.0 → 0.2.2
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 +15 -2
- package/bun.lock +5 -2
- package/package.json +4 -2
- package/scripts/capture-x-graphql.ts +562 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
- package/scripts/test.sh +5 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +161 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/app-bundler.test.ts +12 -33
- package/src/__tests__/asset-materialize-tool.test.ts +16 -15
- package/src/__tests__/asset-search-tool.test.ts +23 -22
- package/src/__tests__/attachments-store.test.ts +56 -127
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
- package/src/__tests__/browser-skill-endstate.test.ts +5 -8
- package/src/__tests__/call-bridge.test.ts +385 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-orchestrator.test.ts +454 -0
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +459 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-state.test.ts +133 -0
- package/src/__tests__/call-store.test.ts +691 -0
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +348 -3
- package/src/__tests__/conflict-store.test.ts +2 -1
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/conversation-store.test.ts +30 -32
- package/src/__tests__/credential-security-invariants.test.ts +4 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/doordash-session.test.ts +9 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/handlers-twitter-config.test.ts +718 -0
- package/src/__tests__/intent-routing.test.ts +64 -57
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +96 -28
- package/src/__tests__/llm-usage-store.test.ts +3 -8
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/playbook-tools.test.ts +342 -0
- package/src/__tests__/profile-compiler.test.ts +2 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
- package/src/__tests__/recurrence-engine.test.ts +69 -0
- package/src/__tests__/recurrence-types.test.ts +71 -0
- package/src/__tests__/registry.test.ts +17 -10
- package/src/__tests__/relay-server.test.ts +633 -0
- package/src/__tests__/reminder-store.test.ts +6 -3
- package/src/__tests__/reminder.test.ts +43 -77
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +222 -0
- package/src/__tests__/run-orchestrator.test.ts +7 -7
- package/src/__tests__/runtime-attachment-metadata.test.ts +19 -20
- package/src/__tests__/runtime-runs-http.test.ts +5 -23
- package/src/__tests__/runtime-runs.test.ts +11 -11
- package/src/__tests__/schedule-store.test.ts +482 -0
- package/src/__tests__/schedule-tools.test.ts +700 -0
- package/src/__tests__/scheduler-recurrence.test.ts +329 -0
- package/src/__tests__/server-history-render.test.ts +14 -13
- package/src/__tests__/session-error.test.ts +28 -0
- package/src/__tests__/session-init.benchmark.test.ts +462 -0
- package/src/__tests__/session-queue.test.ts +89 -16
- package/src/__tests__/session-runtime-assembly.test.ts +161 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/signup-e2e.test.ts +2 -1
- package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/subagent-manager-notify.test.ts +42 -10
- package/src/__tests__/subagent-tools.test.ts +141 -41
- package/src/__tests__/task-compiler.test.ts +2 -1
- package/src/__tests__/task-runner.test.ts +2 -1
- package/src/__tests__/task-scheduler.test.ts +2 -1
- package/src/__tests__/task-tools.test.ts +49 -56
- package/src/__tests__/tool-audit-listener.test.ts +1 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor.test.ts +13 -17
- package/src/__tests__/turn-commit.test.ts +273 -2
- package/src/__tests__/twilio-provider.test.ts +143 -0
- package/src/__tests__/twilio-routes.test.ts +789 -0
- package/src/__tests__/twitter-auth-handler.test.ts +581 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/workspace-git-service.test.ts +403 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +141 -2
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +35 -14
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +48 -0
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +390 -0
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-state.ts +64 -0
- package/src/calls/call-store.ts +416 -0
- package/src/calls/relay-server.ts +335 -0
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-config.ts +34 -0
- package/src/calls/twilio-provider.ts +173 -0
- package/src/calls/twilio-routes.ts +250 -0
- package/src/calls/types.ts +37 -0
- package/src/calls/voice-provider.ts +14 -0
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +256 -25
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/map.ts +246 -0
- package/src/cli/twitter.ts +575 -0
- package/src/cli.ts +7 -5
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
- package/src/config/bundled-skills/document/SKILL.md +18 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +163 -0
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -24
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/twitter/SKILL.md +134 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/defaults.ts +44 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +218 -1
- package/src/config/system-prompt.ts +100 -6
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +5 -0
- package/src/contacts/contact-store.ts +4 -4
- package/src/daemon/assistant-attachments.ts +10 -0
- package/src/daemon/classifier.ts +3 -1
- package/src/daemon/computer-use-session.ts +3 -1
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +16 -1
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +7 -1
- package/src/daemon/handlers/config.ts +192 -4
- package/src/daemon/handlers/diagnostics.ts +5 -1
- package/src/daemon/handlers/documents.ts +18 -29
- package/src/daemon/handlers/home-base.ts +5 -1
- package/src/daemon/handlers/index.ts +40 -271
- package/src/daemon/handlers/misc.ts +9 -1
- package/src/daemon/handlers/publish.ts +6 -1
- package/src/daemon/handlers/sessions.ts +65 -12
- package/src/daemon/handlers/shared.ts +36 -1
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +20 -6
- package/src/daemon/handlers/subagents.ts +8 -3
- package/src/daemon/handlers/twitter-auth.ts +169 -0
- package/src/daemon/handlers/work-items.ts +495 -39
- package/src/daemon/ipc-contract-inventory.json +40 -4
- package/src/daemon/ipc-contract.ts +185 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +48 -5
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +74 -10
- package/src/daemon/server.ts +144 -29
- package/src/daemon/session-agent-loop.ts +887 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-error.ts +24 -3
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +164 -0
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-queue-manager.ts +1 -0
- package/src/daemon/session-runtime-assembly.ts +52 -0
- package/src/daemon/session-skill-tools.ts +124 -5
- package/src/daemon/session-slash.ts +3 -0
- package/src/daemon/session-surfaces.ts +77 -2
- package/src/daemon/session-tool-setup.ts +222 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1365
- package/src/daemon/video-thumbnail.ts +60 -0
- package/src/doordash/client.ts +121 -27
- package/src/doordash/queries.ts +1 -2
- package/src/export/formatter.ts +3 -1
- package/src/followups/followup-store.ts +4 -2
- package/src/followups/types.ts +6 -0
- package/src/hooks/templates.ts +1 -1
- package/src/index.ts +32 -1151
- package/src/media/gemini-image-service.ts +1 -1
- package/src/memory/attachments-store.ts +28 -83
- package/src/memory/channel-delivery-store.ts +7 -21
- package/src/memory/clarification-resolver.ts +6 -5
- package/src/memory/contradiction-checker.ts +3 -2
- package/src/memory/conversation-key-store.ts +10 -29
- package/src/memory/conversation-store.ts +2 -1
- package/src/memory/db.ts +362 -2
- package/src/memory/entity-extractor.ts +6 -3
- package/src/memory/items-extractor.ts +5 -4
- package/src/memory/jobs-store.ts +3 -2
- package/src/memory/llm-usage-store.ts +1 -2
- package/src/memory/runs-store.ts +1 -2
- package/src/memory/schema.ts +65 -2
- package/src/messaging/style-analyzer.ts +3 -2
- package/src/messaging/thread-summarizer.ts +8 -12
- package/src/messaging/triage-engine.ts +4 -2
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/registry.ts +8 -0
- package/src/runtime/http-server.ts +277 -25
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/routes/attachment-routes.ts +5 -6
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +12 -19
- package/src/runtime/routes/conversation-routes.ts +5 -9
- package/src/runtime/routes/run-routes.ts +4 -8
- package/src/runtime/run-orchestrator.ts +39 -6
- package/src/schedule/recurrence-engine.ts +138 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +102 -57
- package/src/schedule/scheduler.ts +9 -6
- package/src/security/oauth2.ts +29 -4
- package/src/security/secret-allowlist.ts +46 -0
- package/src/skills/clawhub.ts +1 -1
- package/src/subagent/manager.ts +40 -8
- package/src/swarm/backend-claude-code.ts +64 -9
- package/src/swarm/worker-prompts.ts +2 -1
- package/src/tasks/SPEC.md +34 -28
- package/src/tasks/ephemeral-permissions.ts +16 -7
- package/src/tasks/task-compiler.ts +5 -4
- package/src/tasks/task-runner.ts +10 -5
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/assets/search.ts +4 -4
- package/src/tools/browser/api-map.ts +220 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +2 -1
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/browser/network-recorder.ts +5 -4
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +67 -0
- package/src/tools/calls/call-start.ts +73 -0
- package/src/tools/calls/call-status.ts +81 -0
- package/src/tools/claude-code/claude-code.ts +77 -11
- package/src/tools/contacts/contact-merge.ts +46 -78
- package/src/tools/contacts/contact-search.ts +35 -79
- package/src/tools/contacts/contact-upsert.ts +35 -108
- package/src/tools/credentials/vault.ts +21 -5
- package/src/tools/document/document-tool.ts +71 -144
- package/src/tools/executor.ts +129 -10
- package/src/tools/followups/followup_create.ts +46 -88
- package/src/tools/followups/followup_list.ts +34 -74
- package/src/tools/followups/followup_resolve.ts +31 -66
- package/src/tools/host-terminal/cli-discover.ts +2 -1
- package/src/tools/host-terminal/host-shell.ts +10 -0
- package/src/tools/memory/handlers.ts +5 -4
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/web-fetch.ts +18 -6
- package/src/tools/playbooks/index.ts +4 -5
- package/src/tools/playbooks/playbook-create.ts +3 -47
- package/src/tools/playbooks/playbook-delete.ts +1 -25
- package/src/tools/playbooks/playbook-list.ts +1 -28
- package/src/tools/playbooks/playbook-update.ts +3 -51
- package/src/tools/registry.ts +2 -4
- package/src/tools/reminder/reminder.ts +5 -78
- package/src/tools/schedule/create.ts +69 -74
- package/src/tools/schedule/delete.ts +21 -47
- package/src/tools/schedule/list.ts +55 -74
- package/src/tools/schedule/update.ts +77 -84
- package/src/tools/subagent/abort.ts +29 -58
- package/src/tools/subagent/message.ts +30 -63
- package/src/tools/subagent/read.ts +53 -84
- package/src/tools/subagent/spawn.ts +43 -82
- package/src/tools/subagent/status.ts +42 -71
- package/src/tools/swarm/delegate.ts +2 -1
- package/src/tools/tasks/index.ts +8 -6
- package/src/tools/tasks/task-delete.ts +69 -56
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +74 -102
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +192 -134
- package/src/tools/tasks/work-item-list.ts +33 -78
- package/src/tools/tasks/work-item-remove.ts +60 -0
- package/src/tools/tasks/work-item-update.ts +114 -0
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +20 -74
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +6 -1
- package/src/tools/watch/screen-watch.ts +3 -1
- package/src/tools/watcher/create.ts +52 -98
- package/src/tools/watcher/delete.ts +20 -46
- package/src/tools/watcher/digest.ts +36 -70
- package/src/tools/watcher/list.ts +49 -79
- package/src/tools/watcher/update.ts +45 -91
- package/src/twitter/client.ts +690 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/types.ts +0 -1
- package/src/util/truncate.ts +6 -0
- package/src/watcher/providers/slack.ts +2 -1
- package/src/watcher/watcher-store.ts +3 -2
- package/src/work-items/work-item-store.ts +236 -2
- package/src/workspace/commit-message-enrichment-service.ts +284 -0
- package/src/workspace/commit-message-provider.ts +95 -0
- package/src/workspace/git-service.ts +272 -52
- package/src/workspace/heartbeat-service.ts +70 -13
- package/src/workspace/provider-commit-message-generator.ts +242 -0
- package/src/workspace/turn-commit.ts +100 -51
- package/src/tools/contacts/index.ts +0 -4
- package/src/tools/document/index.ts +0 -5
- package/src/tools/followups/index.ts +0 -3
- package/src/tools/subagent/index.ts +0 -5
- /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
package/src/cli/doordash.ts
CHANGED
|
@@ -139,8 +139,8 @@ export function registerDoordashCommand(program: Command): void {
|
|
|
139
139
|
dd.command('refresh')
|
|
140
140
|
.description(
|
|
141
141
|
'Start a Ride Shotgun learn session to capture fresh DoorDash cookies. ' +
|
|
142
|
-
'Opens doordash.com in Chrome — sign in when prompted. ' +
|
|
143
|
-
'
|
|
142
|
+
'Opens doordash.com in a separate Chrome window — sign in when prompted. ' +
|
|
143
|
+
'Your existing Chrome and tabs are not affected.',
|
|
144
144
|
)
|
|
145
145
|
.option('--duration <seconds>', 'Recording duration in seconds', '180')
|
|
146
146
|
.action(async (opts: { duration: string }, cmd: Command) => {
|
|
@@ -148,6 +148,9 @@ export function registerDoordashCommand(program: Command): void {
|
|
|
148
148
|
const duration = parseInt(opts.duration, 10);
|
|
149
149
|
|
|
150
150
|
try {
|
|
151
|
+
// Restore minimized Chrome window so user can see the login page
|
|
152
|
+
try { await restoreChromeWindow(); } catch { /* best-effort */ }
|
|
153
|
+
|
|
151
154
|
const result = await startLearnSession(duration);
|
|
152
155
|
if (result.recordingPath) {
|
|
153
156
|
const session = importFromRecording(result.recordingPath);
|
|
@@ -167,6 +170,14 @@ export function registerDoordashCommand(program: Command): void {
|
|
|
167
170
|
// Non-fatal: query extraction is best-effort
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
// Best-effort: minimize Chrome window after capturing session
|
|
174
|
+
try {
|
|
175
|
+
await minimizeChromeWindow();
|
|
176
|
+
process.stderr.write('[doordash] Chrome window minimized\n');
|
|
177
|
+
} catch {
|
|
178
|
+
// Non-fatal: minimizing is best-effort
|
|
179
|
+
}
|
|
180
|
+
|
|
170
181
|
output(
|
|
171
182
|
{
|
|
172
183
|
ok: true,
|
|
@@ -326,7 +337,8 @@ export function registerDoordashCommand(program: Command): void {
|
|
|
326
337
|
.description('Inspect GraphQL operations in a recording')
|
|
327
338
|
.argument('<recordingId>', 'Recording ID or path to recording JSON file')
|
|
328
339
|
.option('--op <operationName>', 'Filter to a specific operation name')
|
|
329
|
-
.
|
|
340
|
+
.option('--extract-options', 'Extract item customization options from updateCartItem operations')
|
|
341
|
+
.action(async (recordingIdOrPath: string, opts: { op?: string; extractOptions?: boolean }, cmd: Command) => {
|
|
330
342
|
const json = getJson(cmd);
|
|
331
343
|
|
|
332
344
|
try {
|
|
@@ -352,6 +364,43 @@ export function registerDoordashCommand(program: Command): void {
|
|
|
352
364
|
|
|
353
365
|
const queries = extractQueries(recording);
|
|
354
366
|
|
|
367
|
+
if (opts.extractOptions) {
|
|
368
|
+
const cartOps = queries.filter(q => q.operationName === 'updateCartItem');
|
|
369
|
+
if (cartOps.length === 0) {
|
|
370
|
+
outputError('No updateCartItem operations found in this recording');
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const extracted = cartOps.map(q => {
|
|
375
|
+
const vars = (q.exampleVariables ?? {}) as Record<string, unknown>;
|
|
376
|
+
const params = (vars.updateCartItemApiParams ?? {}) as Record<string, unknown>;
|
|
377
|
+
return {
|
|
378
|
+
itemId: params.itemId as string | undefined,
|
|
379
|
+
itemName: params.itemName as string | undefined,
|
|
380
|
+
nestedOptions: params.nestedOptions as string | undefined,
|
|
381
|
+
specialInstructions: params.specialInstructions as string | undefined,
|
|
382
|
+
unitPrice: params.unitPrice as number | undefined,
|
|
383
|
+
menuId: params.menuId as string | undefined,
|
|
384
|
+
storeId: params.storeId as string | undefined,
|
|
385
|
+
};
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (json) {
|
|
389
|
+
output({ ok: true, items: extracted, count: extracted.length }, true);
|
|
390
|
+
} else {
|
|
391
|
+
for (const item of extracted) {
|
|
392
|
+
process.stderr.write(`\nItem: ${item.itemName ?? 'unknown'} (${item.itemId ?? '?'})\n`);
|
|
393
|
+
process.stderr.write(` Store: ${item.storeId ?? '?'}, Menu: ${item.menuId ?? '?'}\n`);
|
|
394
|
+
process.stderr.write(` Unit Price: ${item.unitPrice ?? '?'}\n`);
|
|
395
|
+
if (item.specialInstructions) {
|
|
396
|
+
process.stderr.write(` Special Instructions: ${item.specialInstructions}\n`);
|
|
397
|
+
}
|
|
398
|
+
process.stderr.write(` Options: ${item.nestedOptions ?? '[]'}\n`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
355
404
|
if (opts.op) {
|
|
356
405
|
const match = queries.find(q => q.operationName === opts.op);
|
|
357
406
|
if (!match) {
|
|
@@ -511,6 +560,7 @@ export function registerDoordashCommand(program: Command): void {
|
|
|
511
560
|
.option('--quantity <n>', 'Quantity', '1')
|
|
512
561
|
.option('--cart-id <cartId>', 'Existing cart ID (creates new if omitted)')
|
|
513
562
|
.option('--special-instructions <text>', 'Special instructions')
|
|
563
|
+
.option('--options <json>', 'Item customization options as JSON array (from item details or recording)')
|
|
514
564
|
.action(
|
|
515
565
|
async (
|
|
516
566
|
opts: {
|
|
@@ -522,6 +572,7 @@ export function registerDoordashCommand(program: Command): void {
|
|
|
522
572
|
quantity: string;
|
|
523
573
|
cartId?: string;
|
|
524
574
|
specialInstructions?: string;
|
|
575
|
+
options?: string;
|
|
525
576
|
},
|
|
526
577
|
cmd: Command,
|
|
527
578
|
) => {
|
|
@@ -535,6 +586,7 @@ export function registerDoordashCommand(program: Command): void {
|
|
|
535
586
|
quantity: parseInt(opts.quantity, 10),
|
|
536
587
|
cartId: opts.cartId,
|
|
537
588
|
specialInstructions: opts.specialInstructions,
|
|
589
|
+
nestedOptions: opts.options,
|
|
538
590
|
});
|
|
539
591
|
return { cart: result };
|
|
540
592
|
});
|
|
@@ -580,6 +632,115 @@ export function registerDoordashCommand(program: Command): void {
|
|
|
580
632
|
});
|
|
581
633
|
});
|
|
582
634
|
|
|
635
|
+
// cart learn — capture customization options via CDP recording
|
|
636
|
+
cart
|
|
637
|
+
.command('learn')
|
|
638
|
+
.description(
|
|
639
|
+
'Learn item customization options by recording a browser interaction. ' +
|
|
640
|
+
'Opens Chrome and watches you customize an item — when you add it to cart, ' +
|
|
641
|
+
'the nestedOptions and specialInstructions are extracted and output.',
|
|
642
|
+
)
|
|
643
|
+
.option('--duration <seconds>', 'Max recording duration in seconds', '120')
|
|
644
|
+
.action(async (opts: { duration: string }, cmd: Command) => {
|
|
645
|
+
const json = getJson(cmd);
|
|
646
|
+
const duration = parseInt(opts.duration, 10);
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
await ensureChromeWithCDP();
|
|
650
|
+
|
|
651
|
+
const startTime = Date.now() / 1000;
|
|
652
|
+
const recorder = new NetworkRecorder('doordash.com');
|
|
653
|
+
await recorder.startDirect('http://localhost:9222');
|
|
654
|
+
|
|
655
|
+
process.stderr.write('Recording... Navigate to an item, customize it, and add it to cart.\n');
|
|
656
|
+
process.stderr.write(`Will auto-stop when "updateCartItem" is detected. Timeout: ${duration}s.\n`);
|
|
657
|
+
|
|
658
|
+
await new Promise<void>((resolve) => {
|
|
659
|
+
const timer = setTimeout(() => {
|
|
660
|
+
if (poll) clearInterval(poll);
|
|
661
|
+
process.stderr.write(`\nTimeout reached (${duration}s).\n`);
|
|
662
|
+
resolve();
|
|
663
|
+
}, duration * 1000);
|
|
664
|
+
|
|
665
|
+
process.on('SIGINT', () => {
|
|
666
|
+
if (poll) clearInterval(poll);
|
|
667
|
+
clearTimeout(timer);
|
|
668
|
+
resolve();
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
const poll = setInterval(() => {
|
|
672
|
+
const entries = recorder.getEntries();
|
|
673
|
+
const found = entries.some(e => {
|
|
674
|
+
if (!e.request.postData) return false;
|
|
675
|
+
try {
|
|
676
|
+
const body = JSON.parse(e.request.postData) as { operationName?: string };
|
|
677
|
+
return body.operationName === 'updateCartItem';
|
|
678
|
+
} catch { return false; }
|
|
679
|
+
});
|
|
680
|
+
if (found) {
|
|
681
|
+
clearInterval(poll);
|
|
682
|
+
clearTimeout(timer);
|
|
683
|
+
process.stderr.write('\nDetected "updateCartItem" operation.\n');
|
|
684
|
+
setTimeout(() => resolve(), 3000);
|
|
685
|
+
}
|
|
686
|
+
}, 500);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
process.stderr.write('Stopping recording...\n');
|
|
690
|
+
const cookies = await recorder.extractCookies('doordash.com');
|
|
691
|
+
const entries = await recorder.stop();
|
|
692
|
+
|
|
693
|
+
const recording: SessionRecording = {
|
|
694
|
+
id: crypto.randomUUID(),
|
|
695
|
+
startedAt: startTime,
|
|
696
|
+
endedAt: Date.now() / 1000,
|
|
697
|
+
targetDomain: 'doordash.com',
|
|
698
|
+
networkEntries: entries,
|
|
699
|
+
cookies,
|
|
700
|
+
observations: [],
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// Extract updateCartItem operations
|
|
704
|
+
const queries = extractQueries(recording);
|
|
705
|
+
const cartOps = queries.filter(q => q.operationName === 'updateCartItem');
|
|
706
|
+
|
|
707
|
+
if (cartOps.length === 0) {
|
|
708
|
+
outputError('No updateCartItem operations captured. Did you add an item to cart?');
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const extracted = cartOps.map(q => {
|
|
713
|
+
const vars = (q.exampleVariables ?? {}) as Record<string, unknown>;
|
|
714
|
+
const params = (vars.updateCartItemApiParams ?? {}) as Record<string, unknown>;
|
|
715
|
+
return {
|
|
716
|
+
itemId: params.itemId as string | undefined,
|
|
717
|
+
itemName: params.itemName as string | undefined,
|
|
718
|
+
nestedOptions: params.nestedOptions as string | undefined,
|
|
719
|
+
specialInstructions: params.specialInstructions as string | undefined,
|
|
720
|
+
unitPrice: params.unitPrice as number | undefined,
|
|
721
|
+
menuId: params.menuId as string | undefined,
|
|
722
|
+
storeId: params.storeId as string | undefined,
|
|
723
|
+
};
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
// Also save the recording for future reference
|
|
727
|
+
const recordingPath = saveRecording(recording);
|
|
728
|
+
|
|
729
|
+
output(
|
|
730
|
+
{
|
|
731
|
+
ok: true,
|
|
732
|
+
items: extracted,
|
|
733
|
+
count: extracted.length,
|
|
734
|
+
recordingId: recording.id,
|
|
735
|
+
recordingPath,
|
|
736
|
+
},
|
|
737
|
+
json,
|
|
738
|
+
);
|
|
739
|
+
} catch (err) {
|
|
740
|
+
outputError(err instanceof Error ? err.message : String(err));
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
583
744
|
// =========================================================================
|
|
584
745
|
// checkout — get checkout / dropoff options
|
|
585
746
|
// =========================================================================
|
|
@@ -664,7 +825,7 @@ export function registerDoordashCommand(program: Command): void {
|
|
|
664
825
|
// Chrome CDP restart helper
|
|
665
826
|
// ---------------------------------------------------------------------------
|
|
666
827
|
|
|
667
|
-
import {
|
|
828
|
+
import { spawn as spawnChild } from 'node:child_process';
|
|
668
829
|
import { homedir } from 'node:os';
|
|
669
830
|
import { join as pathJoin } from 'node:path';
|
|
670
831
|
|
|
@@ -687,33 +848,15 @@ async function ensureChromeWithCDP(): Promise<void> {
|
|
|
687
848
|
// Already running with CDP?
|
|
688
849
|
if (await isCdpReady()) return;
|
|
689
850
|
|
|
690
|
-
//
|
|
691
|
-
|
|
692
|
-
execSync('osascript -e \'tell application "Google Chrome" to quit\'', {
|
|
693
|
-
timeout: 5000,
|
|
694
|
-
stdio: 'ignore',
|
|
695
|
-
});
|
|
696
|
-
} catch {
|
|
697
|
-
// Chrome might not be running
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// Wait for Chrome to quit
|
|
701
|
-
for (let i = 0; i < 30; i++) {
|
|
702
|
-
try {
|
|
703
|
-
execSync('pgrep -x "Google Chrome"', { stdio: 'ignore' });
|
|
704
|
-
await new Promise(r => setTimeout(r, 200));
|
|
705
|
-
} catch {
|
|
706
|
-
break; // Not running
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
// Relaunch Chrome with CDP flags
|
|
851
|
+
// Launch a separate Chrome instance with CDP flags alongside any existing Chrome.
|
|
852
|
+
// Using a dedicated --user-data-dir allows coexistence without killing the user's browser.
|
|
711
853
|
const chromeApp =
|
|
712
854
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
713
855
|
spawnChild(chromeApp, [
|
|
714
856
|
`--remote-debugging-port=9222`,
|
|
715
857
|
`--force-renderer-accessibility`,
|
|
716
858
|
`--user-data-dir=${CHROME_DATA_DIR}`,
|
|
859
|
+
`https://www.doordash.com/consumer/login/`,
|
|
717
860
|
], {
|
|
718
861
|
detached: true,
|
|
719
862
|
stdio: 'ignore',
|
|
@@ -727,6 +870,94 @@ async function ensureChromeWithCDP(): Promise<void> {
|
|
|
727
870
|
throw new Error('Chrome started but CDP endpoint not responding after 15s');
|
|
728
871
|
}
|
|
729
872
|
|
|
873
|
+
async function minimizeChromeWindow(): Promise<void> {
|
|
874
|
+
const res = await fetch(`${CDP_BASE}/json/list`);
|
|
875
|
+
const targets = (await res.json()) as Array<{ type: string; webSocketDebuggerUrl: string }>;
|
|
876
|
+
const pageTarget = targets.find(t => t.type === 'page');
|
|
877
|
+
if (!pageTarget) return;
|
|
878
|
+
|
|
879
|
+
const ws = new WebSocket(pageTarget.webSocketDebuggerUrl);
|
|
880
|
+
|
|
881
|
+
await new Promise<void>((resolve, reject) => {
|
|
882
|
+
const timeout = setTimeout(() => {
|
|
883
|
+
ws.close();
|
|
884
|
+
reject(new Error('CDP minimize timed out'));
|
|
885
|
+
}, 5000);
|
|
886
|
+
|
|
887
|
+
ws.addEventListener('open', () => {
|
|
888
|
+
ws.send(JSON.stringify({ id: 1, method: 'Browser.getWindowForTarget' }));
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
ws.addEventListener('message', (event) => {
|
|
892
|
+
const msg = JSON.parse(String(event.data)) as { id: number; result?: { windowId: number } };
|
|
893
|
+
if (msg.id === 1 && msg.result) {
|
|
894
|
+
ws.send(JSON.stringify({
|
|
895
|
+
id: 2,
|
|
896
|
+
method: 'Browser.setWindowBounds',
|
|
897
|
+
params: { windowId: msg.result.windowId, bounds: { windowState: 'minimized' } },
|
|
898
|
+
}));
|
|
899
|
+
} else if (msg.id === 1) {
|
|
900
|
+
clearTimeout(timeout);
|
|
901
|
+
ws.close();
|
|
902
|
+
reject(new Error('Browser.getWindowForTarget failed'));
|
|
903
|
+
} else if (msg.id === 2) {
|
|
904
|
+
clearTimeout(timeout);
|
|
905
|
+
ws.close();
|
|
906
|
+
resolve();
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
ws.addEventListener('error', (err) => {
|
|
911
|
+
clearTimeout(timeout);
|
|
912
|
+
reject(err);
|
|
913
|
+
});
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async function restoreChromeWindow(): Promise<void> {
|
|
918
|
+
const res = await fetch(`${CDP_BASE}/json/list`);
|
|
919
|
+
const targets = (await res.json()) as Array<{ type: string; webSocketDebuggerUrl: string }>;
|
|
920
|
+
const pageTarget = targets.find(t => t.type === 'page');
|
|
921
|
+
if (!pageTarget) return;
|
|
922
|
+
|
|
923
|
+
const ws = new WebSocket(pageTarget.webSocketDebuggerUrl);
|
|
924
|
+
|
|
925
|
+
await new Promise<void>((resolve, reject) => {
|
|
926
|
+
const timeout = setTimeout(() => {
|
|
927
|
+
ws.close();
|
|
928
|
+
reject(new Error('CDP restore timed out'));
|
|
929
|
+
}, 5000);
|
|
930
|
+
|
|
931
|
+
ws.addEventListener('open', () => {
|
|
932
|
+
ws.send(JSON.stringify({ id: 1, method: 'Browser.getWindowForTarget' }));
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
ws.addEventListener('message', (event) => {
|
|
936
|
+
const msg = JSON.parse(String(event.data)) as { id: number; result?: { windowId: number } };
|
|
937
|
+
if (msg.id === 1 && msg.result) {
|
|
938
|
+
ws.send(JSON.stringify({
|
|
939
|
+
id: 2,
|
|
940
|
+
method: 'Browser.setWindowBounds',
|
|
941
|
+
params: { windowId: msg.result.windowId, bounds: { windowState: 'normal' } },
|
|
942
|
+
}));
|
|
943
|
+
} else if (msg.id === 1) {
|
|
944
|
+
clearTimeout(timeout);
|
|
945
|
+
ws.close();
|
|
946
|
+
reject(new Error('Browser.getWindowForTarget failed'));
|
|
947
|
+
} else if (msg.id === 2) {
|
|
948
|
+
clearTimeout(timeout);
|
|
949
|
+
ws.close();
|
|
950
|
+
resolve();
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
ws.addEventListener('error', (err) => {
|
|
955
|
+
clearTimeout(timeout);
|
|
956
|
+
reject(err);
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
|
|
730
961
|
// ---------------------------------------------------------------------------
|
|
731
962
|
// Ride Shotgun learn session helper
|
|
732
963
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as net from 'node:net';
|
|
2
|
+
import { getSocketPath, readSessionToken } from '../util/platform.js';
|
|
3
|
+
import {
|
|
4
|
+
serialize,
|
|
5
|
+
createMessageParser,
|
|
6
|
+
type ClientMessage,
|
|
7
|
+
type ServerMessage,
|
|
8
|
+
} from '../daemon/ipc-protocol.js';
|
|
9
|
+
import { IpcError } from '../util/errors.js';
|
|
10
|
+
|
|
11
|
+
export function sendOneMessage(
|
|
12
|
+
msg: ClientMessage,
|
|
13
|
+
): Promise<ServerMessage> {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const socket = net.createConnection(getSocketPath());
|
|
16
|
+
const parser = createMessageParser();
|
|
17
|
+
let resolved = false;
|
|
18
|
+
let authenticated = false;
|
|
19
|
+
|
|
20
|
+
socket.on('connect', () => {
|
|
21
|
+
// Authenticate first — the daemon requires a valid session token
|
|
22
|
+
// before it will accept any other messages.
|
|
23
|
+
const token = readSessionToken();
|
|
24
|
+
if (!token) {
|
|
25
|
+
resolved = true;
|
|
26
|
+
reject(new IpcError('Session token not found — is the daemon running?'));
|
|
27
|
+
socket.destroy();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
socket.write(serialize({ type: 'auth', token }));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
socket.on('data', (data) => {
|
|
34
|
+
const messages = parser.feed(data.toString()) as ServerMessage[];
|
|
35
|
+
for (const m of messages) {
|
|
36
|
+
// Handle auth handshake
|
|
37
|
+
if (!authenticated) {
|
|
38
|
+
if (m.type === 'auth_result') {
|
|
39
|
+
if ((m as { success: boolean }).success) {
|
|
40
|
+
authenticated = true;
|
|
41
|
+
// Now send the actual message
|
|
42
|
+
socket.write(serialize(msg));
|
|
43
|
+
} else {
|
|
44
|
+
resolved = true;
|
|
45
|
+
reject(new IpcError((m as { message?: string }).message ?? 'Authentication failed'));
|
|
46
|
+
socket.destroy();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Skip push messages that aren't responses to our request
|
|
53
|
+
if (m.type === 'daemon_status') {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// On auto-auth sockets the server may send a second auth_result
|
|
57
|
+
// in response to the client's auth message after we're already
|
|
58
|
+
// authenticated — ignore it so it doesn't resolve as the response.
|
|
59
|
+
if (m.type === 'auth_result') {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (m.type === 'session_info' && msg.type !== 'session_create') {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
resolved = true;
|
|
66
|
+
socket.end();
|
|
67
|
+
resolve(m);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
socket.on('error', (err) => {
|
|
73
|
+
if (!resolved) reject(err);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
socket.on('close', () => {
|
|
77
|
+
if (!resolved) {
|
|
78
|
+
reject(new IpcError('Socket closed before receiving a response'));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
package/src/cli/map.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI command: `vellum map <domain>`
|
|
3
|
+
*
|
|
4
|
+
* Launches Chrome with CDP, starts a Ride Shotgun learn session to auto-navigate
|
|
5
|
+
* the given domain, then analyzes captured network traffic into a deduplicated API map.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as net from 'node:net';
|
|
9
|
+
import { spawn as spawnChild } from 'node:child_process';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { join as pathJoin } from 'node:path';
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import { getSocketPath, readSessionToken } from '../util/platform.js';
|
|
14
|
+
import {
|
|
15
|
+
serialize,
|
|
16
|
+
createMessageParser,
|
|
17
|
+
} from '../daemon/ipc-protocol.js';
|
|
18
|
+
import { loadRecording } from '../tools/browser/recording-store.js';
|
|
19
|
+
import { analyzeApiMap, saveApiMap, printApiMapTable } from '../tools/browser/api-map.js';
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
function output(data: unknown, json: boolean): void {
|
|
26
|
+
process.stdout.write(
|
|
27
|
+
json ? JSON.stringify(data) + '\n' : JSON.stringify(data, null, 2) + '\n',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function outputError(message: string, code = 1): void {
|
|
32
|
+
output({ ok: false, error: message }, true);
|
|
33
|
+
process.exitCode = code;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getJson(cmd: Command): boolean {
|
|
37
|
+
let c: Command | null = cmd;
|
|
38
|
+
while (c) {
|
|
39
|
+
if ((c.opts() as { json?: boolean }).json) return true;
|
|
40
|
+
c = c.parent;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Chrome CDP helpers
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const CDP_BASE = 'http://localhost:9222';
|
|
50
|
+
const CHROME_DATA_DIR = pathJoin(
|
|
51
|
+
homedir(),
|
|
52
|
+
'Library/Application Support/Google/Chrome-CDP',
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
async function isCdpReady(): Promise<boolean> {
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(`${CDP_BASE}/json/version`);
|
|
58
|
+
return res.ok;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function ensureChromeWithCDP(domain: string): Promise<void> {
|
|
65
|
+
// Already running with CDP?
|
|
66
|
+
if (await isCdpReady()) return;
|
|
67
|
+
|
|
68
|
+
// Launch a separate Chrome instance with CDP flags alongside any existing Chrome.
|
|
69
|
+
const chromeApp =
|
|
70
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
71
|
+
spawnChild(chromeApp, [
|
|
72
|
+
`--remote-debugging-port=9222`,
|
|
73
|
+
`--force-renderer-accessibility`,
|
|
74
|
+
`--user-data-dir=${CHROME_DATA_DIR}`,
|
|
75
|
+
`https://${domain}/`,
|
|
76
|
+
], {
|
|
77
|
+
detached: true,
|
|
78
|
+
stdio: 'ignore',
|
|
79
|
+
}).unref();
|
|
80
|
+
|
|
81
|
+
// Wait for CDP to be ready
|
|
82
|
+
for (let i = 0; i < 30; i++) {
|
|
83
|
+
await new Promise(r => setTimeout(r, 500));
|
|
84
|
+
if (await isCdpReady()) return;
|
|
85
|
+
}
|
|
86
|
+
throw new Error('Chrome started but CDP endpoint not responding after 15s');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Ride Shotgun learn session helper
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
interface LearnResult {
|
|
94
|
+
recordingId?: string;
|
|
95
|
+
recordingPath?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function startLearnSession(domain: string, durationSeconds: number): Promise<LearnResult> {
|
|
99
|
+
await ensureChromeWithCDP(domain);
|
|
100
|
+
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const socketPath = getSocketPath();
|
|
103
|
+
const sessionToken = readSessionToken();
|
|
104
|
+
const socket = net.createConnection(socketPath);
|
|
105
|
+
const parser = createMessageParser();
|
|
106
|
+
|
|
107
|
+
socket.on('error', (err) => {
|
|
108
|
+
reject(new Error(`Cannot connect to daemon: ${err.message}. Is the daemon running?`));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const timeoutHandle = setTimeout(() => {
|
|
112
|
+
socket.destroy();
|
|
113
|
+
reject(new Error(`Learn session timed out after ${durationSeconds + 30}s`));
|
|
114
|
+
}, (durationSeconds + 30) * 1000);
|
|
115
|
+
timeoutHandle.unref();
|
|
116
|
+
|
|
117
|
+
let authenticated = !sessionToken;
|
|
118
|
+
|
|
119
|
+
const sendStartCommand = () => {
|
|
120
|
+
socket.write(
|
|
121
|
+
serialize({
|
|
122
|
+
type: 'ride_shotgun_start',
|
|
123
|
+
durationSeconds,
|
|
124
|
+
intervalSeconds: 5,
|
|
125
|
+
mode: 'learn',
|
|
126
|
+
targetDomain: domain,
|
|
127
|
+
autoNavigate: true,
|
|
128
|
+
} as unknown as import('../daemon/ipc-protocol.js').ClientMessage),
|
|
129
|
+
);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
socket.on('data', (chunk) => {
|
|
133
|
+
const messages = parser.feed(chunk.toString('utf-8'));
|
|
134
|
+
for (const msg of messages) {
|
|
135
|
+
const m = msg as unknown as Record<string, unknown>;
|
|
136
|
+
|
|
137
|
+
if (!authenticated && m.type === 'auth_result') {
|
|
138
|
+
if ((m as { success: boolean }).success) {
|
|
139
|
+
authenticated = true;
|
|
140
|
+
sendStartCommand();
|
|
141
|
+
} else {
|
|
142
|
+
clearTimeout(timeoutHandle);
|
|
143
|
+
socket.destroy();
|
|
144
|
+
reject(new Error('Daemon authentication failed'));
|
|
145
|
+
}
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (m.type === 'auth_result') {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (m.type === 'ride_shotgun_result') {
|
|
154
|
+
clearTimeout(timeoutHandle);
|
|
155
|
+
socket.destroy();
|
|
156
|
+
resolve({
|
|
157
|
+
recordingId: m.recordingId as string | undefined,
|
|
158
|
+
recordingPath: m.recordingPath as string | undefined,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
socket.on('connect', () => {
|
|
165
|
+
if (sessionToken) {
|
|
166
|
+
socket.write(
|
|
167
|
+
serialize({
|
|
168
|
+
type: 'auth',
|
|
169
|
+
token: sessionToken,
|
|
170
|
+
} as unknown as import('../daemon/ipc-protocol.js').ClientMessage),
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
sendStartCommand();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// Command registration
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
export function registerMapCommand(program: Command): void {
|
|
184
|
+
program
|
|
185
|
+
.command('map')
|
|
186
|
+
.description(
|
|
187
|
+
'Auto-navigate a domain and produce a deduplicated API map. ' +
|
|
188
|
+
'Launches Chrome with CDP, starts a Ride Shotgun learn session, ' +
|
|
189
|
+
'then analyzes captured network traffic.',
|
|
190
|
+
)
|
|
191
|
+
.argument('<domain>', 'Domain to map (e.g., example.com)')
|
|
192
|
+
.option('--duration <seconds>', 'Recording duration in seconds', '120')
|
|
193
|
+
.option('--json', 'Machine-readable JSON output')
|
|
194
|
+
.action(async (domain: string, opts: { duration: string; json?: boolean }, cmd: Command) => {
|
|
195
|
+
const json = getJson(cmd);
|
|
196
|
+
const duration = parseInt(opts.duration, 10);
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
// 1. Start learn session (launches Chrome + auto-navigates)
|
|
200
|
+
if (!json) {
|
|
201
|
+
console.log(`Starting API map session for ${domain} (${duration}s)...`);
|
|
202
|
+
}
|
|
203
|
+
const result = await startLearnSession(domain, duration);
|
|
204
|
+
|
|
205
|
+
if (!result.recordingId) {
|
|
206
|
+
outputError('Recording completed but no recording ID returned');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 2. Load the recording
|
|
211
|
+
const recording = loadRecording(result.recordingId);
|
|
212
|
+
if (!recording) {
|
|
213
|
+
outputError(`Failed to load recording ${result.recordingId}`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 3. Analyze the API map
|
|
218
|
+
const apiMap = analyzeApiMap(recording.networkEntries, domain);
|
|
219
|
+
|
|
220
|
+
// 4. Save the API map
|
|
221
|
+
const savedPath = saveApiMap(domain, apiMap);
|
|
222
|
+
|
|
223
|
+
// 5. Display results
|
|
224
|
+
if (!json) {
|
|
225
|
+
printApiMapTable(apiMap);
|
|
226
|
+
console.log(`API map saved to: ${savedPath}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 6. Output JSON result
|
|
230
|
+
output(
|
|
231
|
+
{
|
|
232
|
+
ok: true,
|
|
233
|
+
domain,
|
|
234
|
+
recordingId: result.recordingId,
|
|
235
|
+
savedPath,
|
|
236
|
+
totalRequests: apiMap.totalRequests,
|
|
237
|
+
endpointCount: apiMap.endpoints.length,
|
|
238
|
+
apiMap,
|
|
239
|
+
},
|
|
240
|
+
json,
|
|
241
|
+
);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
outputError(err instanceof Error ? err.message : String(err));
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|