vellum 0.2.1 → 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 +133 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- 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 +4 -3
- 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 +130 -4
- 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-store.test.ts +216 -1
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
- 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 +299 -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__/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 +62 -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 +5 -3
- 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 +8 -4
- package/src/__tests__/run-orchestrator.test.ts +4 -4
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
- package/src/__tests__/runtime-runs-http.test.ts +4 -4
- package/src/__tests__/runtime-runs.test.ts +4 -4
- 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 +71 -48
- 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 +218 -3
- 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 +186 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +12 -8
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +43 -5
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +43 -17
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-store.ts +192 -5
- package/src/calls/relay-server.ts +41 -4
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-provider.ts +10 -6
- package/src/calls/twilio-routes.ts +90 -76
- package/src/calls/types.ts +1 -1
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +251 -1
- 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 +82 -23
- 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/media-generate-image.ts +1 -23
- 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 +33 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +161 -1
- package/src/config/system-prompt.ts +61 -16
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +4 -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 +163 -5
- 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 -277
- 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 +384 -68
- package/src/daemon/ipc-contract-inventory.json +28 -4
- package/src/daemon/ipc-contract.ts +133 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +21 -0
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +74 -10
- package/src/daemon/server.ts +143 -26
- 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 +216 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1404
- 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 -1153
- 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 +96 -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 +23 -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 +108 -20
- package/src/runtime/routes/attachment-routes.ts +2 -3
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +5 -10
- package/src/runtime/routes/conversation-routes.ts +5 -5
- package/src/runtime/routes/run-routes.ts +2 -2
- package/src/runtime/run-orchestrator.ts +9 -3
- 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 +17 -67
- package/src/tools/calls/call-start.ts +24 -85
- package/src/tools/calls/call-status.ts +35 -51
- 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 +20 -4
- 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/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 -8
- package/src/tools/tasks/task-delete.ts +60 -88
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +72 -108
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +183 -215
- package/src/tools/tasks/work-item-list.ts +33 -63
- package/src/tools/tasks/work-item-remove.ts +45 -97
- package/src/tools/tasks/work-item-update.ts +91 -163
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +0 -62
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +3 -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 +27 -2
- package/src/workspace/commit-message-enrichment-service.ts +31 -7
- package/src/workspace/git-service.ts +87 -22
- package/src/workspace/provider-commit-message-generator.ts +242 -0
- package/src/workspace/turn-commit.ts +62 -3
- 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
|
@@ -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
|
// =========================================================================
|
|
@@ -695,6 +856,7 @@ async function ensureChromeWithCDP(): Promise<void> {
|
|
|
695
856
|
`--remote-debugging-port=9222`,
|
|
696
857
|
`--force-renderer-accessibility`,
|
|
697
858
|
`--user-data-dir=${CHROME_DATA_DIR}`,
|
|
859
|
+
`https://www.doordash.com/consumer/login/`,
|
|
698
860
|
], {
|
|
699
861
|
detached: true,
|
|
700
862
|
stdio: 'ignore',
|
|
@@ -708,6 +870,94 @@ async function ensureChromeWithCDP(): Promise<void> {
|
|
|
708
870
|
throw new Error('Chrome started but CDP endpoint not responding after 15s');
|
|
709
871
|
}
|
|
710
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
|
+
|
|
711
961
|
// ---------------------------------------------------------------------------
|
|
712
962
|
// Ride Shotgun learn session helper
|
|
713
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
|
+
}
|