vellum 0.2.1 → 0.2.7
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 +71 -100
- package/package.json +5 -3
- 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 +305 -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-twilio-config.test.ts +221 -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 +71 -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-regressions.test.ts +100 -2
- 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-commit-message-generator.test.ts +303 -0
- 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-conflict-gate.test.ts +28 -25
- 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/__tests__/twilio-webhook-urls.test.ts +162 -0
- 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-config.ts +8 -8
- package/src/calls/twilio-provider.ts +13 -9
- package/src/calls/twilio-routes.ts +90 -76
- package/src/calls/twilio-webhook-urls.ts +50 -0
- 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 +270 -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 +34 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +165 -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/config/vellum-skills/telegram-setup/SKILL.md +1 -5
- 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 +205 -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 +32 -4
- package/src/daemon/ipc-contract.ts +156 -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 +75 -10
- package/src/daemon/server.ts +143 -26
- package/src/daemon/session-agent-loop.ts +922 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-conflict-gate.ts +18 -109
- 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/conflict-intent.ts +114 -0
- 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/job-handlers/conflict.ts +23 -1
- 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/gateway-client.ts +36 -0
- package/src/runtime/http-server.ts +166 -22
- 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 +125 -88
- 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 +293 -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 +207 -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 +269 -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
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recording analyzer that processes NetworkRecordedEntry[] into a deduplicated
|
|
3
|
+
* API map. Collapses ID-like path segments into {id} placeholders so repeated
|
|
4
|
+
* calls to the same endpoint are grouped together.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdirSync, writeFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { getDataDir } from '../../util/platform.js';
|
|
10
|
+
import type { NetworkRecordedEntry } from './network-recording-types.js';
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Types
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
export interface ApiEndpoint {
|
|
17
|
+
method: string;
|
|
18
|
+
urlPattern: string;
|
|
19
|
+
exampleUrl: string;
|
|
20
|
+
queryParams: string[];
|
|
21
|
+
requestBodyKeys: string[];
|
|
22
|
+
responseStatus: number[];
|
|
23
|
+
responseBodyKeys: string[];
|
|
24
|
+
count: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ApiMapResult {
|
|
28
|
+
domain: string;
|
|
29
|
+
analyzedAt: number;
|
|
30
|
+
totalRequests: number;
|
|
31
|
+
endpoints: ApiEndpoint[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
39
|
+
const NUMERIC_RE = /^\d+$/;
|
|
40
|
+
const HEX_HASH_RE = /^[0-9a-f]{8,}$/i;
|
|
41
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
42
|
+
|
|
43
|
+
/** URL path patterns that indicate non-API noise. */
|
|
44
|
+
const NOISE_PATH_PATTERNS = [
|
|
45
|
+
/\/web-translations\//,
|
|
46
|
+
/\/cdn-cgi\//,
|
|
47
|
+
/\.properties$/,
|
|
48
|
+
/\.js$/,
|
|
49
|
+
/\.css$/,
|
|
50
|
+
/\.woff2?$/,
|
|
51
|
+
/\.png$/,
|
|
52
|
+
/\.jpg$/,
|
|
53
|
+
/\.svg$/,
|
|
54
|
+
/\.ico$/,
|
|
55
|
+
/\.map$/,
|
|
56
|
+
/\/preference\//,
|
|
57
|
+
/\/userpreference-service\//,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/** Returns true when a path segment looks like a dynamic ID. */
|
|
61
|
+
function isIdSegment(segment: string): boolean {
|
|
62
|
+
if (NUMERIC_RE.test(segment)) return true;
|
|
63
|
+
if (UUID_RE.test(segment)) return true;
|
|
64
|
+
if (HEX_HASH_RE.test(segment)) return true;
|
|
65
|
+
if (DATE_RE.test(segment)) return true;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Replace ID-like path segments with `{id}`. */
|
|
70
|
+
function normalizePathSegments(pathname: string): string {
|
|
71
|
+
return pathname
|
|
72
|
+
.split('/')
|
|
73
|
+
.map((seg) => (isIdSegment(seg) ? '{id}' : seg))
|
|
74
|
+
.join('/');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Safely parse JSON, returning undefined on failure. */
|
|
78
|
+
function tryParseJson(text: string | undefined): Record<string, unknown> | undefined {
|
|
79
|
+
if (!text) return undefined;
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(text);
|
|
82
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
83
|
+
return parsed as Record<string, unknown>;
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// not JSON
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Extract GraphQL operation name from request body. */
|
|
92
|
+
function extractGraphQLOperationName(postData: string | undefined): string | null {
|
|
93
|
+
if (!postData) return null;
|
|
94
|
+
const body = tryParseJson(postData);
|
|
95
|
+
if (!body) return null;
|
|
96
|
+
if (typeof body.operationName === 'string' && body.operationName) return body.operationName;
|
|
97
|
+
// Try extracting from query string: "query FooBar { ..." or "mutation FooBar { ..."
|
|
98
|
+
if (typeof body.query === 'string') {
|
|
99
|
+
const named = body.query.match(/(?:query|mutation|subscription)\s+(\w+)/);
|
|
100
|
+
if (named) return named[1];
|
|
101
|
+
// Unnamed query — extract the first field name: "query{fooBar(" or "query { fooBar {"
|
|
102
|
+
const firstField = body.query.match(/(?:query|mutation|subscription)\s*\{?\s*(\w+)/);
|
|
103
|
+
if (firstField) return firstField[1];
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Core analysis
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
interface GroupData {
|
|
113
|
+
method: string;
|
|
114
|
+
urlPattern: string;
|
|
115
|
+
exampleUrl: string;
|
|
116
|
+
queryParams: Set<string>;
|
|
117
|
+
requestBodyKeys: Set<string>;
|
|
118
|
+
responseStatus: Set<number>;
|
|
119
|
+
responseBodyKeys: Set<string>;
|
|
120
|
+
count: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function analyzeApiMap(
|
|
124
|
+
entries: NetworkRecordedEntry[],
|
|
125
|
+
domain: string,
|
|
126
|
+
): ApiMapResult {
|
|
127
|
+
const groups = new Map<string, GroupData>();
|
|
128
|
+
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
const { request, response } = entry;
|
|
131
|
+
let parsed: URL;
|
|
132
|
+
try {
|
|
133
|
+
parsed = new URL(request.url);
|
|
134
|
+
} catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Skip non-API noise
|
|
139
|
+
if (NOISE_PATH_PATTERNS.some(p => p.test(parsed.pathname))) continue;
|
|
140
|
+
|
|
141
|
+
// Skip non-JSON responses
|
|
142
|
+
const mimeType = response?.mimeType ?? '';
|
|
143
|
+
if (response && !mimeType.includes('json') && !mimeType.includes('graphql')) continue;
|
|
144
|
+
|
|
145
|
+
const method = request.method.toUpperCase();
|
|
146
|
+
const normalizedPath = normalizePathSegments(parsed.pathname);
|
|
147
|
+
const basePattern = `${parsed.hostname}${normalizedPath}`;
|
|
148
|
+
|
|
149
|
+
// For GraphQL endpoints, split by operation name
|
|
150
|
+
let urlPattern = basePattern;
|
|
151
|
+
const isGraphQL = normalizedPath.includes('graphql');
|
|
152
|
+
if (isGraphQL && method === 'POST') {
|
|
153
|
+
const opName = extractGraphQLOperationName(request.postData);
|
|
154
|
+
if (opName) {
|
|
155
|
+
urlPattern = `${basePattern} → ${opName}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const key = `${method} ${urlPattern}`;
|
|
160
|
+
|
|
161
|
+
let group = groups.get(key);
|
|
162
|
+
if (!group) {
|
|
163
|
+
group = {
|
|
164
|
+
method,
|
|
165
|
+
urlPattern,
|
|
166
|
+
exampleUrl: request.url,
|
|
167
|
+
queryParams: new Set(),
|
|
168
|
+
requestBodyKeys: new Set(),
|
|
169
|
+
responseStatus: new Set(),
|
|
170
|
+
responseBodyKeys: new Set(),
|
|
171
|
+
count: 0,
|
|
172
|
+
};
|
|
173
|
+
groups.set(key, group);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
group.count++;
|
|
177
|
+
|
|
178
|
+
for (const paramKey of parsed.searchParams.keys()) {
|
|
179
|
+
group.queryParams.add(paramKey);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (['POST', 'PUT', 'PATCH'].includes(method)) {
|
|
183
|
+
const body = tryParseJson(request.postData);
|
|
184
|
+
if (body) {
|
|
185
|
+
for (const k of Object.keys(body)) {
|
|
186
|
+
if (k !== 'query' && k !== 'operationName' && k !== 'extensions') {
|
|
187
|
+
group.requestBodyKeys.add(k);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (response) {
|
|
194
|
+
group.responseStatus.add(response.status);
|
|
195
|
+
const resBody = tryParseJson(response.body);
|
|
196
|
+
if (resBody) {
|
|
197
|
+
for (const k of Object.keys(resBody)) {
|
|
198
|
+
group.responseBodyKeys.add(k);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const endpoints: ApiEndpoint[] = Array.from(groups.values()).map((g) => ({
|
|
205
|
+
method: g.method,
|
|
206
|
+
urlPattern: g.urlPattern,
|
|
207
|
+
exampleUrl: g.exampleUrl,
|
|
208
|
+
queryParams: Array.from(g.queryParams).sort(),
|
|
209
|
+
requestBodyKeys: Array.from(g.requestBodyKeys).sort(),
|
|
210
|
+
responseStatus: Array.from(g.responseStatus).sort((a, b) => a - b),
|
|
211
|
+
responseBodyKeys: Array.from(g.responseBodyKeys).sort(),
|
|
212
|
+
count: g.count,
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
// Sort: data endpoints first (low count = unique pages), then boilerplate
|
|
216
|
+
// Within each tier, sort alphabetically by pattern for readability
|
|
217
|
+
endpoints.sort((a, b) => {
|
|
218
|
+
const aIsBoilerplate = a.count > 15;
|
|
219
|
+
const bIsBoilerplate = b.count > 15;
|
|
220
|
+
if (aIsBoilerplate !== bIsBoilerplate) return aIsBoilerplate ? 1 : -1;
|
|
221
|
+
return a.urlPattern.localeCompare(b.urlPattern);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const totalApiRequests = endpoints.reduce((sum, ep) => sum + ep.count, 0);
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
domain,
|
|
228
|
+
analyzedAt: Date.now(),
|
|
229
|
+
totalRequests: totalApiRequests,
|
|
230
|
+
endpoints,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Persistence
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
export function saveApiMap(domain: string, result: ApiMapResult): string {
|
|
239
|
+
const dir = join(getDataDir(), 'api-maps');
|
|
240
|
+
mkdirSync(dir, { recursive: true });
|
|
241
|
+
|
|
242
|
+
const timestamp = Date.now();
|
|
243
|
+
const filePath = join(dir, `${domain}-${timestamp}.json`);
|
|
244
|
+
writeFileSync(filePath, JSON.stringify(result, null, 2));
|
|
245
|
+
return filePath;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// Pretty-print
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
export function printApiMapTable(result: ApiMapResult): void {
|
|
253
|
+
const dataEndpoints = result.endpoints.filter(ep => ep.count <= 15);
|
|
254
|
+
const boilerplate = result.endpoints.filter(ep => ep.count > 15);
|
|
255
|
+
|
|
256
|
+
console.log(`\nAPI Map for ${result.domain} — ${result.endpoints.length} endpoints discovered\n`);
|
|
257
|
+
|
|
258
|
+
const stripDomain = (pattern: string) => {
|
|
259
|
+
const idx = pattern.indexOf('/');
|
|
260
|
+
return idx >= 0 ? pattern.slice(idx) : pattern;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const printSection = (title: string, eps: ApiEndpoint[]) => {
|
|
264
|
+
if (eps.length === 0) return;
|
|
265
|
+
console.log(` ${title} (${eps.length})\n`);
|
|
266
|
+
|
|
267
|
+
const header = ['Method', 'Endpoint', 'Hits', 'Response Keys'];
|
|
268
|
+
const rows = eps.map((ep) => [
|
|
269
|
+
ep.method,
|
|
270
|
+
stripDomain(ep.urlPattern),
|
|
271
|
+
String(ep.count),
|
|
272
|
+
ep.responseBodyKeys.slice(0, 5).join(', ') || '-',
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
const widths = header.map((h, i) =>
|
|
276
|
+
Math.min(i === 1 ? 72 : i === 3 ? 50 : 200, Math.max(h.length, ...rows.map((r) => r[i].length))),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const sep = widths.map((w) => '-'.repeat(w)).join(' | ');
|
|
280
|
+
const fmt = (row: string[]) =>
|
|
281
|
+
row.map((cell, i) => cell.slice(0, widths[i]).padEnd(widths[i])).join(' | ');
|
|
282
|
+
|
|
283
|
+
console.log(` ${fmt(header)}`);
|
|
284
|
+
console.log(` ${sep}`);
|
|
285
|
+
for (const row of rows) {
|
|
286
|
+
console.log(` ${fmt(row)}`);
|
|
287
|
+
}
|
|
288
|
+
console.log();
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
printSection('DATA ENDPOINTS', dataEndpoints);
|
|
292
|
+
printSection('PAGE-LOAD BOILERPLATE', boilerplate);
|
|
293
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP-based auto-navigation for any domain.
|
|
3
|
+
*
|
|
4
|
+
* Drives Chrome through a domain's pages by discovering internal links,
|
|
5
|
+
* so the NetworkRecorder captures the API surface without manual browsing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getLogger } from '../../util/logger.js';
|
|
9
|
+
|
|
10
|
+
const log = getLogger('auto-navigate');
|
|
11
|
+
|
|
12
|
+
const CDP_BASE = 'http://localhost:9222';
|
|
13
|
+
const MAX_PAGES = 15;
|
|
14
|
+
const PAGE_WAIT_MS = 3500;
|
|
15
|
+
const SCROLL_WAIT_MS = 2000;
|
|
16
|
+
|
|
17
|
+
/** Minimal CDP client — connects to one page tab. */
|
|
18
|
+
class MiniCDP {
|
|
19
|
+
private ws: WebSocket | null = null;
|
|
20
|
+
private nextId = 1;
|
|
21
|
+
private callbacks = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
|
22
|
+
|
|
23
|
+
async connect(wsUrl: string): Promise<void> {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const ws = new WebSocket(wsUrl);
|
|
26
|
+
ws.onopen = () => { this.ws = ws; resolve(); };
|
|
27
|
+
ws.onerror = (e) => reject(new Error(`CDP error: ${e}`));
|
|
28
|
+
ws.onclose = () => {
|
|
29
|
+
this.ws = null;
|
|
30
|
+
for (const [, cb] of this.callbacks) {
|
|
31
|
+
cb.reject(new Error('WebSocket closed'));
|
|
32
|
+
}
|
|
33
|
+
this.callbacks.clear();
|
|
34
|
+
};
|
|
35
|
+
ws.onmessage = (event) => {
|
|
36
|
+
const msg = JSON.parse(String(event.data));
|
|
37
|
+
if (msg.id != null) {
|
|
38
|
+
const cb = this.callbacks.get(msg.id);
|
|
39
|
+
if (cb) {
|
|
40
|
+
this.callbacks.delete(msg.id);
|
|
41
|
+
msg.error ? cb.reject(new Error(msg.error.message)) : cb.resolve(msg.result);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async send(method: string, params?: Record<string, unknown>): Promise<unknown> {
|
|
49
|
+
if (!this.ws) throw new Error('Not connected');
|
|
50
|
+
const id = this.nextId++;
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
this.callbacks.set(id, { resolve, reject });
|
|
53
|
+
this.ws!.send(JSON.stringify({ id, method, params }));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
close() { this.ws?.close(); }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Navigate Chrome through a domain's pages to trigger API calls.
|
|
62
|
+
* Discovers internal links from the DOM and visits up to ~15 unique paths.
|
|
63
|
+
*
|
|
64
|
+
* @param domain The domain to crawl (e.g. "example.com").
|
|
65
|
+
* @param abortSignal Optional signal to stop navigation early.
|
|
66
|
+
* @returns List of visited page URLs.
|
|
67
|
+
*/
|
|
68
|
+
export async function autoNavigate(domain: string, abortSignal?: { aborted: boolean }): Promise<string[]> {
|
|
69
|
+
let wsUrl: string | null = null;
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch(`${CDP_BASE}/json/list`);
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
log.warn('CDP not available for auto-navigation');
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const targets = (await res.json()) as Array<{ type: string; url: string; webSocketDebuggerUrl: string }>;
|
|
77
|
+
const domainTab = targets.find(t => {
|
|
78
|
+
if (t.type !== 'page') return false;
|
|
79
|
+
try {
|
|
80
|
+
const hostname = new URL(t.url).hostname;
|
|
81
|
+
return hostname === domain || hostname.endsWith('.' + domain);
|
|
82
|
+
} catch { return false; }
|
|
83
|
+
});
|
|
84
|
+
wsUrl = domainTab?.webSocketDebuggerUrl ?? targets.find(t => t.type === 'page')?.webSocketDebuggerUrl ?? null;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
log.warn({ err }, 'Failed to discover Chrome tabs');
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!wsUrl) {
|
|
91
|
+
log.warn('No Chrome tab found for auto-navigation');
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const cdp = new MiniCDP();
|
|
96
|
+
try {
|
|
97
|
+
await cdp.connect(wsUrl);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
log.warn({ err }, 'Failed to connect CDP for auto-navigation');
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await cdp.send('Page.enable').catch(() => {});
|
|
104
|
+
|
|
105
|
+
const rootUrl = `https://${domain}/`;
|
|
106
|
+
const visited = new Set<string>();
|
|
107
|
+
const visitedUrls: string[] = [];
|
|
108
|
+
|
|
109
|
+
// Navigate to the domain root first
|
|
110
|
+
try {
|
|
111
|
+
await cdp.send('Page.navigate', { url: rootUrl });
|
|
112
|
+
await sleep(PAGE_WAIT_MS);
|
|
113
|
+
visited.add('/');
|
|
114
|
+
visitedUrls.push(rootUrl);
|
|
115
|
+
log.info({ url: rootUrl }, 'Visited root page');
|
|
116
|
+
} catch (err) {
|
|
117
|
+
log.warn({ err }, 'Failed to navigate to domain root');
|
|
118
|
+
cdp.close();
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (abortSignal?.aborted) { cdp.close(); return visitedUrls; }
|
|
123
|
+
|
|
124
|
+
// Scroll the root page to trigger lazy content
|
|
125
|
+
await scrollPage(cdp);
|
|
126
|
+
await sleep(SCROLL_WAIT_MS);
|
|
127
|
+
|
|
128
|
+
// Click common interactive elements on the root page
|
|
129
|
+
await clickInteractiveElements(cdp);
|
|
130
|
+
await sleep(SCROLL_WAIT_MS);
|
|
131
|
+
|
|
132
|
+
// Discover internal links from the current page
|
|
133
|
+
let discoveredLinks = await discoverInternalLinks(cdp, domain);
|
|
134
|
+
log.info({ count: discoveredLinks.length }, 'Discovered internal links from root');
|
|
135
|
+
|
|
136
|
+
// Visit discovered pages
|
|
137
|
+
for (const link of discoveredLinks) {
|
|
138
|
+
if (abortSignal?.aborted) break;
|
|
139
|
+
if (visited.size >= MAX_PAGES) break;
|
|
140
|
+
if (visited.has(link.key)) continue;
|
|
141
|
+
|
|
142
|
+
const url = link.url;
|
|
143
|
+
log.info({ url }, 'Auto-navigate visiting page');
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
await cdp.send('Page.navigate', { url });
|
|
147
|
+
await sleep(PAGE_WAIT_MS);
|
|
148
|
+
visited.add(link.key);
|
|
149
|
+
visitedUrls.push(url);
|
|
150
|
+
|
|
151
|
+
// Scroll to trigger lazy-loaded content
|
|
152
|
+
await scrollPage(cdp);
|
|
153
|
+
await sleep(SCROLL_WAIT_MS);
|
|
154
|
+
|
|
155
|
+
// Click interactive elements to trigger more API calls
|
|
156
|
+
await clickInteractiveElements(cdp);
|
|
157
|
+
await sleep(1500);
|
|
158
|
+
|
|
159
|
+
// Discover more links from this page
|
|
160
|
+
const newLinks = await discoverInternalLinks(cdp, domain);
|
|
161
|
+
for (const nl of newLinks) {
|
|
162
|
+
if (!visited.has(nl.key) && !discoveredLinks.some(l => l.key === nl.key)) {
|
|
163
|
+
discoveredLinks.push(nl);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
log.info({ url }, 'Auto-navigate page completed');
|
|
168
|
+
} catch (err) {
|
|
169
|
+
log.warn({ err, url }, 'Auto-navigate page failed');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
cdp.close();
|
|
174
|
+
log.info({ visited: visitedUrls.length, total: discoveredLinks.length + 1 }, 'Auto-navigation finished');
|
|
175
|
+
return visitedUrls;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
interface DiscoveredLink {
|
|
179
|
+
/** Full URL to navigate to (preserves subdomain). */
|
|
180
|
+
url: string;
|
|
181
|
+
/** Deduplication key: origin + pathname. */
|
|
182
|
+
key: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Extract internal links from the current page DOM, preserving subdomains. */
|
|
186
|
+
async function discoverInternalLinks(cdp: MiniCDP, domain: string): Promise<DiscoveredLink[]> {
|
|
187
|
+
try {
|
|
188
|
+
const result = await cdp.send('Runtime.evaluate', {
|
|
189
|
+
expression: `
|
|
190
|
+
(function() {
|
|
191
|
+
const domain = ${JSON.stringify(domain)};
|
|
192
|
+
const seen = new Set();
|
|
193
|
+
const links = [];
|
|
194
|
+
for (const a of document.querySelectorAll('a[href]')) {
|
|
195
|
+
const href = a.getAttribute('href');
|
|
196
|
+
if (!href) continue;
|
|
197
|
+
try {
|
|
198
|
+
const url = new URL(href, location.origin);
|
|
199
|
+
if (url.hostname !== domain && !url.hostname.endsWith('.' + domain)) continue;
|
|
200
|
+
const path = url.pathname;
|
|
201
|
+
// Skip anchors, query-only links, file downloads, and trivial paths
|
|
202
|
+
if (path === '/' || path === '') continue;
|
|
203
|
+
if (path.match(/\\.(png|jpg|jpeg|gif|svg|css|js|woff|pdf|zip)$/i)) continue;
|
|
204
|
+
const key = url.origin + url.pathname;
|
|
205
|
+
if (!seen.has(key)) {
|
|
206
|
+
seen.add(key);
|
|
207
|
+
links.push({ url: url.origin + url.pathname, key });
|
|
208
|
+
}
|
|
209
|
+
} catch { /* skip malformed URLs */ }
|
|
210
|
+
}
|
|
211
|
+
return links;
|
|
212
|
+
})()
|
|
213
|
+
`,
|
|
214
|
+
awaitPromise: false,
|
|
215
|
+
returnByValue: true,
|
|
216
|
+
}) as { result?: { value?: DiscoveredLink[] } };
|
|
217
|
+
return result?.result?.value ?? [];
|
|
218
|
+
} catch {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Scroll the page to trigger lazy-loaded content. */
|
|
224
|
+
async function scrollPage(cdp: MiniCDP): Promise<void> {
|
|
225
|
+
await cdp.send('Runtime.evaluate', {
|
|
226
|
+
expression: 'window.scrollBy(0, 800)',
|
|
227
|
+
awaitPromise: false,
|
|
228
|
+
}).catch(() => {});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Click common interactive elements (tabs, nav buttons) to trigger API calls. */
|
|
232
|
+
async function clickInteractiveElements(cdp: MiniCDP): Promise<void> {
|
|
233
|
+
const selectors = [
|
|
234
|
+
'nav a:not([href="/"])',
|
|
235
|
+
'[role="tab"]',
|
|
236
|
+
'[role="tablist"] button',
|
|
237
|
+
'button[data-tab]',
|
|
238
|
+
'.tab, .nav-tab, .nav-link',
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
for (const selector of selectors) {
|
|
242
|
+
await clickInPage(cdp, selector);
|
|
243
|
+
await sleep(800);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function clickInPage(cdp: MiniCDP, selector: string): Promise<boolean> {
|
|
248
|
+
try {
|
|
249
|
+
const result = await cdp.send('Runtime.evaluate', {
|
|
250
|
+
expression: `
|
|
251
|
+
(function() {
|
|
252
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
253
|
+
if (!el) return false;
|
|
254
|
+
el.scrollIntoView({ block: 'center' });
|
|
255
|
+
el.click();
|
|
256
|
+
return true;
|
|
257
|
+
})()
|
|
258
|
+
`,
|
|
259
|
+
awaitPromise: false,
|
|
260
|
+
returnByValue: true,
|
|
261
|
+
}) as { result?: { value?: boolean } };
|
|
262
|
+
return result?.result?.value === true;
|
|
263
|
+
} catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function sleep(ms: number): Promise<void> {
|
|
269
|
+
return new Promise(r => setTimeout(r, ms));
|
|
270
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ToolContext, ToolExecutionResult } from '../types.js';
|
|
2
2
|
import type { ImageContent } from '../../providers/types.js';
|
|
3
3
|
import { getLogger } from '../../util/logger.js';
|
|
4
|
+
import { truncate } from '../../util/truncate.js';
|
|
4
5
|
import {
|
|
5
6
|
parseUrl,
|
|
6
7
|
isPrivateOrLocalHost,
|
|
@@ -826,7 +827,7 @@ export async function executeBrowserWaitFor(
|
|
|
826
827
|
`document.body?.innerText?.includes(${escaped})`,
|
|
827
828
|
{ timeout },
|
|
828
829
|
);
|
|
829
|
-
return { content: `Text "${text
|
|
830
|
+
return { content: `Text "${truncate(text, 80)}" appeared on page.`, isError: false };
|
|
830
831
|
}
|
|
831
832
|
|
|
832
833
|
// duration mode (milliseconds)
|
|
@@ -433,7 +433,7 @@ class BrowserManager {
|
|
|
433
433
|
if (this.browserCdpSession) {
|
|
434
434
|
try {
|
|
435
435
|
await this.browserCdpSession.detach();
|
|
436
|
-
} catch {}
|
|
436
|
+
} catch (e) { log.debug({ err: e }, 'CDP session detach failed during shutdown'); }
|
|
437
437
|
this.browserCdpSession = null;
|
|
438
438
|
this.browserWindowId = null;
|
|
439
439
|
}
|
|
@@ -489,7 +489,7 @@ class BrowserManager {
|
|
|
489
489
|
try {
|
|
490
490
|
await cdp.send('Page.stopScreencast');
|
|
491
491
|
await cdp.detach();
|
|
492
|
-
} catch {}
|
|
492
|
+
} catch (e) { log.debug({ err: e }, 'Screencast stop / CDP detach failed during cleanup'); }
|
|
493
493
|
this.cdpSessions.delete(sessionId);
|
|
494
494
|
this.screencastCallbacks.delete(sessionId);
|
|
495
495
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getLogger } from '../../util/logger.js';
|
|
9
|
+
import { truncate } from '../../util/truncate.js';
|
|
9
10
|
import type { NetworkRecordedEntry, NetworkRecordedRequest, ExtractedCredential } from './network-recording-types.js';
|
|
10
11
|
|
|
11
12
|
const log = getLogger('network-recorder');
|
|
@@ -54,7 +55,7 @@ class DirectCDPClient {
|
|
|
54
55
|
for (const h of handlers) h(msg.params ?? {});
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
|
-
} catch {}
|
|
58
|
+
} catch (e) { log.debug({ err: e }, 'Failed to parse CDP WebSocket message'); }
|
|
58
59
|
};
|
|
59
60
|
});
|
|
60
61
|
}
|
|
@@ -195,7 +196,7 @@ export class NetworkRecorder {
|
|
|
195
196
|
for (const client of this.pageClients) {
|
|
196
197
|
try {
|
|
197
198
|
await client.send('Network.disable');
|
|
198
|
-
} catch {}
|
|
199
|
+
} catch (e) { log.debug({ err: e }, 'Network.disable failed during cleanup'); }
|
|
199
200
|
client.close();
|
|
200
201
|
}
|
|
201
202
|
this.pageClients = [];
|
|
@@ -273,7 +274,7 @@ export class NetworkRecorder {
|
|
|
273
274
|
const method = (request.method as string) ?? 'GET';
|
|
274
275
|
const postData = request.postData as string | undefined;
|
|
275
276
|
|
|
276
|
-
log.debug({ url: url
|
|
277
|
+
log.debug({ url: truncate(url, 120, ''), method, requestId }, 'Request captured');
|
|
277
278
|
|
|
278
279
|
const recordedRequest: NetworkRecordedRequest = { method, url, headers, postData };
|
|
279
280
|
const entry: NetworkRecordedEntry = {
|
|
@@ -307,7 +308,7 @@ export class NetworkRecorder {
|
|
|
307
308
|
this.loginSignals.some(sig => entry.request.url.includes(sig))
|
|
308
309
|
) {
|
|
309
310
|
this.loginDetectedFired = true;
|
|
310
|
-
log.info({ url: entry.request.url
|
|
311
|
+
log.info({ url: truncate(entry.request.url, 120, '') }, 'Login detected — will auto-stop in 5s');
|
|
311
312
|
// Delay to let remaining network requests (cookies, session data) settle
|
|
312
313
|
setTimeout(() => this.onLoginDetected?.(), 5000);
|
|
313
314
|
}
|