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
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Twitter API client.
|
|
3
|
+
* Executes GraphQL queries through Chrome's CDP (Runtime.evaluate) so requests
|
|
4
|
+
* go through the browser's authenticated session.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
loadSession,
|
|
9
|
+
type TwitterSession,
|
|
10
|
+
} from './session.js';
|
|
11
|
+
|
|
12
|
+
const CDP_BASE = 'http://localhost:9222';
|
|
13
|
+
|
|
14
|
+
/** Static bearer token used by x.com for all GraphQL requests. */
|
|
15
|
+
const BEARER_TOKEN =
|
|
16
|
+
'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
17
|
+
|
|
18
|
+
// ─── Query IDs (captured from x.com) ─────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const QUERY_IDS = {
|
|
21
|
+
CreateTweet: 'Ah3G_byjEDs_HSlgU0PyZw',
|
|
22
|
+
UserByScreenName: 'AWbeRIdkLtqTRN7yL_H8yw',
|
|
23
|
+
UserTweets: 'N2tFDY-MlrLxXJ9F_ZxJGA',
|
|
24
|
+
TweetDetail: 'YCNdW_ZytXfV9YR3cJK9kw',
|
|
25
|
+
SearchTimeline: 'ML-n2SfAxx5S_9QMqNejbg',
|
|
26
|
+
Bookmarks: 'toTC7lB_mQm5fuBE5yyEJw',
|
|
27
|
+
HomeTimeline: 'nn16KxqX3E1OdE7WlHB5LA',
|
|
28
|
+
NotificationsTimeline: 'saZw4lppu6QzMEiRUCYurg',
|
|
29
|
+
Likes: 'Pcw-j9lrSeDMmkgnIejJiQ',
|
|
30
|
+
Followers: 'P7m4Qr-rJEB8KUluOenU6A',
|
|
31
|
+
Following: 'T5wihsMTYHncY7BB4YxHSg',
|
|
32
|
+
UserMedia: 'xLCC9bG_VqHfXXgq8jPoCg',
|
|
33
|
+
} as const;
|
|
34
|
+
|
|
35
|
+
/** Feature flags shared by all GraphQL endpoints. */
|
|
36
|
+
const FEATURES: Record<string, boolean> = {
|
|
37
|
+
rweb_video_screen_enabled: false,
|
|
38
|
+
profile_label_improvements_pcf_label_in_post_enabled: true,
|
|
39
|
+
responsive_web_profile_redirect_enabled: false,
|
|
40
|
+
rweb_tipjar_consumption_enabled: false,
|
|
41
|
+
verified_phone_label_enabled: false,
|
|
42
|
+
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
43
|
+
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
44
|
+
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
45
|
+
premium_content_api_read_enabled: false,
|
|
46
|
+
communities_web_enable_tweet_community_results_fetch: true,
|
|
47
|
+
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
|
48
|
+
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
|
49
|
+
responsive_web_grok_analyze_post_followups_enabled: true,
|
|
50
|
+
responsive_web_jetfuel_frame: true,
|
|
51
|
+
responsive_web_grok_share_attachment_enabled: true,
|
|
52
|
+
responsive_web_grok_annotations_enabled: true,
|
|
53
|
+
articles_preview_enabled: true,
|
|
54
|
+
responsive_web_edit_tweet_api_enabled: true,
|
|
55
|
+
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
|
56
|
+
view_counts_everywhere_api_enabled: true,
|
|
57
|
+
longform_notetweets_consumption_enabled: true,
|
|
58
|
+
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
|
59
|
+
tweet_awards_web_tipping_enabled: false,
|
|
60
|
+
responsive_web_grok_show_grok_translated_post: true,
|
|
61
|
+
responsive_web_grok_analysis_button_from_backend: true,
|
|
62
|
+
post_ctas_fetch_enabled: true,
|
|
63
|
+
freedom_of_speech_not_reach_fetch_enabled: true,
|
|
64
|
+
standardized_nudges_misinfo: true,
|
|
65
|
+
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
|
66
|
+
longform_notetweets_rich_text_read_enabled: true,
|
|
67
|
+
longform_notetweets_inline_media_enabled: true,
|
|
68
|
+
responsive_web_grok_image_annotation_enabled: true,
|
|
69
|
+
responsive_web_grok_imagine_annotation_enabled: true,
|
|
70
|
+
responsive_web_grok_community_note_auto_translation_is_enabled: false,
|
|
71
|
+
responsive_web_enhance_cards_enabled: false,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// ─── Errors ──────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/** Thrown when the session is missing or expired. */
|
|
77
|
+
export class SessionExpiredError extends Error {
|
|
78
|
+
constructor(reason: string) {
|
|
79
|
+
super(reason);
|
|
80
|
+
this.name = 'SessionExpiredError';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function requireSession(): TwitterSession {
|
|
85
|
+
const session = loadSession();
|
|
86
|
+
if (!session) {
|
|
87
|
+
throw new SessionExpiredError('No Twitter session found.');
|
|
88
|
+
}
|
|
89
|
+
return session;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── CDP transport ───────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
async function findTwitterTab(): Promise<string> {
|
|
95
|
+
const res = await fetch(`${CDP_BASE}/json/list`).catch(() => null);
|
|
96
|
+
if (!res?.ok) {
|
|
97
|
+
throw new SessionExpiredError('Chrome CDP not available. Run `vellum twitter refresh` first.');
|
|
98
|
+
}
|
|
99
|
+
const targets = (await res.json()) as Array<{ type: string; url: string; webSocketDebuggerUrl: string }>;
|
|
100
|
+
const tab = targets.find(
|
|
101
|
+
t => t.type === 'page' && (t.url.includes('x.com') || t.url.includes('twitter.com')),
|
|
102
|
+
);
|
|
103
|
+
if (!tab?.webSocketDebuggerUrl) {
|
|
104
|
+
throw new SessionExpiredError('No x.com tab found in Chrome. Open x.com and try again.');
|
|
105
|
+
}
|
|
106
|
+
return tab.webSocketDebuggerUrl;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Standard headers for X API requests (as a JS expression for Runtime.evaluate). */
|
|
110
|
+
const API_HEADERS_GET = `{
|
|
111
|
+
'authorization': 'Bearer ${BEARER_TOKEN}',
|
|
112
|
+
'x-csrf-token': csrf,
|
|
113
|
+
'x-twitter-auth-type': 'OAuth2Session',
|
|
114
|
+
'x-twitter-active-user': 'yes',
|
|
115
|
+
'x-twitter-client-language': 'en',
|
|
116
|
+
}`;
|
|
117
|
+
|
|
118
|
+
const API_HEADERS_POST = `{
|
|
119
|
+
'Content-Type': 'application/json',
|
|
120
|
+
'authorization': 'Bearer ${BEARER_TOKEN}',
|
|
121
|
+
'x-csrf-token': csrf,
|
|
122
|
+
'x-twitter-auth-type': 'OAuth2Session',
|
|
123
|
+
'x-twitter-active-user': 'yes',
|
|
124
|
+
'x-twitter-client-language': 'en',
|
|
125
|
+
}`;
|
|
126
|
+
|
|
127
|
+
/** Execute a POST fetch inside Chrome via CDP Runtime.evaluate. */
|
|
128
|
+
async function cdpFetch(wsUrl: string, url: string, body: string): Promise<unknown> {
|
|
129
|
+
return cdpEval(wsUrl, `
|
|
130
|
+
(function() {
|
|
131
|
+
var csrf = (document.cookie.match(/ct0=([^;]+)/) || [])[1] || '';
|
|
132
|
+
return fetch(${JSON.stringify(url)}, {
|
|
133
|
+
method: 'POST',
|
|
134
|
+
headers: ${API_HEADERS_POST},
|
|
135
|
+
body: ${JSON.stringify(body)},
|
|
136
|
+
credentials: 'include',
|
|
137
|
+
})
|
|
138
|
+
.then(function(r) {
|
|
139
|
+
if (!r.ok) return r.text().then(function(t) {
|
|
140
|
+
return JSON.stringify({ __status: r.status, __error: true, __body: t.substring(0, 500) });
|
|
141
|
+
});
|
|
142
|
+
return r.text();
|
|
143
|
+
})
|
|
144
|
+
.catch(function(e) { return JSON.stringify({ __error: true, __message: e.message }); });
|
|
145
|
+
})()
|
|
146
|
+
`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Execute a GET fetch inside Chrome via CDP Runtime.evaluate. */
|
|
150
|
+
async function cdpGet(wsUrl: string, url: string): Promise<unknown> {
|
|
151
|
+
return cdpEval(wsUrl, `
|
|
152
|
+
(function() {
|
|
153
|
+
var csrf = (document.cookie.match(/ct0=([^;]+)/) || [])[1] || '';
|
|
154
|
+
return fetch(${JSON.stringify(url)}, {
|
|
155
|
+
method: 'GET',
|
|
156
|
+
headers: ${API_HEADERS_GET},
|
|
157
|
+
credentials: 'include',
|
|
158
|
+
})
|
|
159
|
+
.then(function(r) {
|
|
160
|
+
if (!r.ok) return r.text().then(function(t) {
|
|
161
|
+
return JSON.stringify({ __status: r.status, __error: true, __body: t.substring(0, 500) });
|
|
162
|
+
});
|
|
163
|
+
return r.text();
|
|
164
|
+
})
|
|
165
|
+
.catch(function(e) { return JSON.stringify({ __error: true, __message: e.message }); });
|
|
166
|
+
})()
|
|
167
|
+
`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Navigate Chrome to a URL and capture the response body of a specific GraphQL query.
|
|
172
|
+
* This works for endpoints that require X's client-generated transaction ID (e.g. Search, Followers)
|
|
173
|
+
* because the browser's own JavaScript generates the correct headers.
|
|
174
|
+
*/
|
|
175
|
+
async function cdpNavigateAndCapture(wsUrl: string, pageUrl: string, queryName: string): Promise<unknown> {
|
|
176
|
+
return new Promise((resolve, reject) => {
|
|
177
|
+
const ws = new WebSocket(wsUrl);
|
|
178
|
+
let nextId = 1;
|
|
179
|
+
const callbacks = new Map<number, (v: unknown) => void>();
|
|
180
|
+
const pendingRequestIds = new Set<string>();
|
|
181
|
+
|
|
182
|
+
const timeout = setTimeout(() => {
|
|
183
|
+
ws.close();
|
|
184
|
+
reject(new Error(`CDP navigate+capture timed out waiting for ${queryName}`));
|
|
185
|
+
}, 30000);
|
|
186
|
+
|
|
187
|
+
function send(method: string, params?: Record<string, unknown>): Promise<unknown> {
|
|
188
|
+
const id = nextId++;
|
|
189
|
+
return new Promise(r => {
|
|
190
|
+
callbacks.set(id, r);
|
|
191
|
+
ws.send(JSON.stringify({ id, method, params }));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
ws.onmessage = (event) => {
|
|
196
|
+
const msg = JSON.parse(typeof event.data === 'string' ? event.data : '');
|
|
197
|
+
|
|
198
|
+
// Handle command responses
|
|
199
|
+
if (msg.id != null && callbacks.has(msg.id)) {
|
|
200
|
+
callbacks.get(msg.id)!(msg.result ?? msg.error);
|
|
201
|
+
callbacks.delete(msg.id);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Track GraphQL requests matching our query name
|
|
206
|
+
if (msg.method === 'Network.requestWillBeSent') {
|
|
207
|
+
const req = msg.params?.request;
|
|
208
|
+
const url = req?.url as string | undefined;
|
|
209
|
+
if (url?.includes(`/graphql/`) && url?.includes(`/${queryName}`)) {
|
|
210
|
+
pendingRequestIds.add(msg.params.requestId as string);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Capture response when loading finishes
|
|
215
|
+
if (msg.method === 'Network.loadingFinished') {
|
|
216
|
+
const requestId = msg.params?.requestId as string;
|
|
217
|
+
if (!pendingRequestIds.has(requestId)) return;
|
|
218
|
+
pendingRequestIds.delete(requestId);
|
|
219
|
+
|
|
220
|
+
send('Network.getResponseBody', { requestId }).then(result => {
|
|
221
|
+
const body = (result as Record<string, unknown>)?.body as string;
|
|
222
|
+
if (!body) return;
|
|
223
|
+
try {
|
|
224
|
+
const json = JSON.parse(body);
|
|
225
|
+
clearTimeout(timeout);
|
|
226
|
+
ws.close();
|
|
227
|
+
if (json.errors?.length) {
|
|
228
|
+
reject(new Error(`X API errors: ${json.errors.map((e: { message: string }) => e.message).join('; ')}`));
|
|
229
|
+
} else {
|
|
230
|
+
resolve(json);
|
|
231
|
+
}
|
|
232
|
+
} catch { /* not JSON, skip */ }
|
|
233
|
+
}).catch(() => { /* ignore */ });
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
ws.onerror = () => {
|
|
238
|
+
clearTimeout(timeout);
|
|
239
|
+
reject(new SessionExpiredError('CDP connection failed.'));
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
ws.onopen = async () => {
|
|
243
|
+
await send('Network.enable');
|
|
244
|
+
await send('Page.enable');
|
|
245
|
+
await send('Page.navigate', { url: pageUrl });
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Shared CDP evaluate helper. */
|
|
251
|
+
async function cdpEval(wsUrl: string, expression: string): Promise<unknown> {
|
|
252
|
+
return new Promise((resolve, reject) => {
|
|
253
|
+
const ws = new WebSocket(wsUrl);
|
|
254
|
+
const id = 1;
|
|
255
|
+
|
|
256
|
+
const timeout = setTimeout(() => {
|
|
257
|
+
ws.close();
|
|
258
|
+
reject(new Error('CDP fetch timed out after 30s'));
|
|
259
|
+
}, 30000);
|
|
260
|
+
|
|
261
|
+
ws.onopen = () => {
|
|
262
|
+
ws.send(JSON.stringify({
|
|
263
|
+
id,
|
|
264
|
+
method: 'Runtime.evaluate',
|
|
265
|
+
params: { expression, awaitPromise: true, returnByValue: true },
|
|
266
|
+
}));
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
ws.onmessage = (event) => {
|
|
270
|
+
try {
|
|
271
|
+
const msg = JSON.parse(typeof event.data === 'string' ? event.data : '');
|
|
272
|
+
if (msg.id === id) {
|
|
273
|
+
clearTimeout(timeout);
|
|
274
|
+
ws.close();
|
|
275
|
+
|
|
276
|
+
if (msg.error) {
|
|
277
|
+
reject(new Error(`CDP error: ${msg.error.message}`));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const value = msg.result?.result?.value;
|
|
282
|
+
if (!value) {
|
|
283
|
+
reject(new Error('Empty CDP response'));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
|
288
|
+
if (parsed.__error) {
|
|
289
|
+
if (parsed.__status === 403 || parsed.__status === 401) {
|
|
290
|
+
reject(new SessionExpiredError('Twitter session has expired.'));
|
|
291
|
+
} else {
|
|
292
|
+
reject(new Error(parsed.__message ?? `HTTP ${parsed.__status}: ${parsed.__body ?? ''}`));
|
|
293
|
+
}
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
resolve(parsed);
|
|
297
|
+
}
|
|
298
|
+
} catch (err) {
|
|
299
|
+
clearTimeout(timeout);
|
|
300
|
+
ws.close();
|
|
301
|
+
reject(err);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
ws.onerror = () => {
|
|
306
|
+
clearTimeout(timeout);
|
|
307
|
+
reject(new SessionExpiredError('CDP connection failed.'));
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ─── GraphQL helpers ─────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
/** Build a GraphQL GET URL with encoded variables and features. */
|
|
315
|
+
function graphqlUrl(queryId: string, queryName: string, variables: Record<string, unknown>): string {
|
|
316
|
+
const v = encodeURIComponent(JSON.stringify(variables));
|
|
317
|
+
const f = encodeURIComponent(JSON.stringify(FEATURES));
|
|
318
|
+
return `https://x.com/i/api/graphql/${queryId}/${queryName}?variables=${v}&features=${f}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Execute a GraphQL GET query and return the parsed response. */
|
|
322
|
+
async function graphqlGet(queryId: string, queryName: string, variables: Record<string, unknown>): Promise<unknown> {
|
|
323
|
+
requireSession();
|
|
324
|
+
const wsUrl = await findTwitterTab();
|
|
325
|
+
const url = graphqlUrl(queryId, queryName, variables);
|
|
326
|
+
const json = await cdpGet(wsUrl, url) as { errors?: Array<{ message: string }> };
|
|
327
|
+
if (json.errors?.length) {
|
|
328
|
+
throw new Error(`X API errors: ${json.errors.map(e => e.message).join('; ')}`);
|
|
329
|
+
}
|
|
330
|
+
return json;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ─── Tweet extraction helpers ────────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
336
|
+
type AnyJson = any;
|
|
337
|
+
|
|
338
|
+
function extractScreenName(tweetResult: AnyJson): string {
|
|
339
|
+
return (
|
|
340
|
+
tweetResult?.core?.user_results?.result?.legacy?.screen_name ??
|
|
341
|
+
tweetResult?.core?.user_results?.result?.core?.screen_name ??
|
|
342
|
+
'i'
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Extract tweets from a timeline instructions array (shared by most endpoints). */
|
|
347
|
+
function extractTweetsFromInstructions(instructions: AnyJson[]): TweetEntry[] {
|
|
348
|
+
const tweets: TweetEntry[] = [];
|
|
349
|
+
for (const instruction of instructions) {
|
|
350
|
+
// Handle both array-style entries and direct entries
|
|
351
|
+
const entries = instruction.entries ?? [];
|
|
352
|
+
for (const entry of entries) {
|
|
353
|
+
// Standard tweet entry
|
|
354
|
+
let tweetResult = entry.content?.itemContent?.tweet_results?.result;
|
|
355
|
+
// Some results wrap in __typename: "TweetWithVisibilityResults"
|
|
356
|
+
if (tweetResult?.__typename === 'TweetWithVisibilityResults') {
|
|
357
|
+
tweetResult = tweetResult.tweet;
|
|
358
|
+
}
|
|
359
|
+
if (tweetResult?.rest_id) {
|
|
360
|
+
tweets.push({
|
|
361
|
+
tweetId: tweetResult.rest_id,
|
|
362
|
+
text: tweetResult.legacy?.full_text ?? '',
|
|
363
|
+
url: `https://x.com/${extractScreenName(tweetResult)}/status/${tweetResult.rest_id}`,
|
|
364
|
+
createdAt: tweetResult.legacy?.created_at ?? '',
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Search results can have TimelineTimelineModule with nested items
|
|
369
|
+
if (entry.content?.items) {
|
|
370
|
+
for (const item of entry.content.items) {
|
|
371
|
+
let tr = item.item?.itemContent?.tweet_results?.result;
|
|
372
|
+
if (tr?.__typename === 'TweetWithVisibilityResults') tr = tr.tweet;
|
|
373
|
+
if (tr?.rest_id) {
|
|
374
|
+
tweets.push({
|
|
375
|
+
tweetId: tr.rest_id,
|
|
376
|
+
text: tr.legacy?.full_text ?? '',
|
|
377
|
+
url: `https://x.com/${extractScreenName(tr)}/status/${tr.rest_id}`,
|
|
378
|
+
createdAt: tr.legacy?.created_at ?? '',
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return tweets;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** Extract users from a timeline instructions array (Followers/Following). */
|
|
389
|
+
function extractUsersFromInstructions(instructions: AnyJson[]): UserInfo[] {
|
|
390
|
+
const users: UserInfo[] = [];
|
|
391
|
+
for (const instruction of instructions) {
|
|
392
|
+
for (const entry of instruction.entries ?? []) {
|
|
393
|
+
const userResult = entry.content?.itemContent?.user_results?.result;
|
|
394
|
+
if (userResult?.rest_id) {
|
|
395
|
+
users.push({
|
|
396
|
+
userId: userResult.rest_id,
|
|
397
|
+
screenName: userResult.legacy?.screen_name ?? userResult.core?.screen_name ?? '',
|
|
398
|
+
name: userResult.legacy?.name ?? userResult.core?.name ?? '',
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return users;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ─── Public types ────────────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
export interface PostTweetResult {
|
|
409
|
+
tweetId: string;
|
|
410
|
+
text: string;
|
|
411
|
+
url: string;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export interface UserInfo {
|
|
415
|
+
userId: string;
|
|
416
|
+
screenName: string;
|
|
417
|
+
name: string;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export interface TweetEntry {
|
|
421
|
+
tweetId: string;
|
|
422
|
+
text: string;
|
|
423
|
+
url: string;
|
|
424
|
+
createdAt: string;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export interface NotificationEntry {
|
|
428
|
+
id: string;
|
|
429
|
+
message: string;
|
|
430
|
+
timestamp: string;
|
|
431
|
+
url?: string;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ─── Write operations ────────────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
export async function postTweet(text: string, opts?: { inReplyToTweetId?: string }): Promise<PostTweetResult> {
|
|
437
|
+
requireSession();
|
|
438
|
+
|
|
439
|
+
const wsUrl = await findTwitterTab();
|
|
440
|
+
const url = `https://x.com/i/api/graphql/${QUERY_IDS.CreateTweet}/CreateTweet`;
|
|
441
|
+
const variables: Record<string, unknown> = {
|
|
442
|
+
tweet_text: text,
|
|
443
|
+
dark_request: false,
|
|
444
|
+
media: {
|
|
445
|
+
media_entities: [],
|
|
446
|
+
possibly_sensitive: false,
|
|
447
|
+
},
|
|
448
|
+
semantic_annotation_ids: [],
|
|
449
|
+
disallowed_reply_options: null,
|
|
450
|
+
};
|
|
451
|
+
if (opts?.inReplyToTweetId) {
|
|
452
|
+
variables.reply = {
|
|
453
|
+
in_reply_to_tweet_id: opts.inReplyToTweetId,
|
|
454
|
+
exclude_reply_user_ids: [],
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
const body = JSON.stringify({
|
|
458
|
+
variables,
|
|
459
|
+
features: FEATURES,
|
|
460
|
+
queryId: QUERY_IDS.CreateTweet,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const json = (await cdpFetch(wsUrl, url, body)) as AnyJson;
|
|
464
|
+
|
|
465
|
+
if (json.errors?.length) {
|
|
466
|
+
throw new Error(`X API errors: ${json.errors.map((e: AnyJson) => e.message).join('; ')}`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const tweetResults = json.data?.create_tweet?.tweet_results;
|
|
470
|
+
const result = tweetResults?.result;
|
|
471
|
+
if (!result?.rest_id) {
|
|
472
|
+
if (tweetResults && !result) {
|
|
473
|
+
throw new Error('X rejected this post — it may be a duplicate of a recent post. Try different text.');
|
|
474
|
+
}
|
|
475
|
+
throw new Error(`Unexpected response from X API. Response: ${JSON.stringify(json).slice(0, 500)}`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
tweetId: result.rest_id,
|
|
480
|
+
text,
|
|
481
|
+
url: `https://x.com/${extractScreenName(result)}/status/${result.rest_id}`,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ─── User lookup ─────────────────────────────────────────────────────────────
|
|
486
|
+
|
|
487
|
+
export async function getUserByScreenName(screenName: string): Promise<UserInfo> {
|
|
488
|
+
const json = await graphqlGet(QUERY_IDS.UserByScreenName, 'UserByScreenName', {
|
|
489
|
+
screen_name: screenName,
|
|
490
|
+
withGrokTranslatedBio: true,
|
|
491
|
+
}) as AnyJson;
|
|
492
|
+
|
|
493
|
+
const user = json.data?.user?.result;
|
|
494
|
+
if (!user?.rest_id) {
|
|
495
|
+
throw new Error(`User @${screenName} not found`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
userId: user.rest_id,
|
|
500
|
+
screenName: user.legacy?.screen_name ?? screenName,
|
|
501
|
+
name: user.legacy?.name ?? screenName,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ─── User tweets ─────────────────────────────────────────────────────────────
|
|
506
|
+
|
|
507
|
+
export async function getUserTweets(userId: string, count = 20): Promise<TweetEntry[]> {
|
|
508
|
+
const json = await graphqlGet(QUERY_IDS.UserTweets, 'UserTweets', {
|
|
509
|
+
userId,
|
|
510
|
+
count,
|
|
511
|
+
includePromotedContent: true,
|
|
512
|
+
withQuickPromoteEligibilityTweetFields: true,
|
|
513
|
+
withVoice: true,
|
|
514
|
+
}) as AnyJson;
|
|
515
|
+
|
|
516
|
+
// Response path: data.user.result.timeline_v2.timeline.instructions[]
|
|
517
|
+
// Fallback to data.user.result.timeline.timeline.instructions[]
|
|
518
|
+
const timelineData = json.data?.user?.result?.timeline_v2 ?? json.data?.user?.result?.timeline;
|
|
519
|
+
const instructions = timelineData?.timeline?.instructions ?? [];
|
|
520
|
+
return extractTweetsFromInstructions(instructions);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ─── Tweet detail ────────────────────────────────────────────────────────────
|
|
524
|
+
|
|
525
|
+
export async function getTweetDetail(tweetId: string): Promise<TweetEntry[]> {
|
|
526
|
+
const json = await graphqlGet(QUERY_IDS.TweetDetail, 'TweetDetail', {
|
|
527
|
+
focalTweetId: tweetId,
|
|
528
|
+
referrer: 'tweet',
|
|
529
|
+
with_rux_injections: false,
|
|
530
|
+
rankingMode: 'Relevance',
|
|
531
|
+
includePromotedContent: true,
|
|
532
|
+
withCommunity: true,
|
|
533
|
+
withQuickPromoteEligibilityTweetFields: true,
|
|
534
|
+
withBirdwatchNotes: true,
|
|
535
|
+
withVoice: true,
|
|
536
|
+
}) as AnyJson;
|
|
537
|
+
|
|
538
|
+
// Response path: data.threaded_conversation_with_injections_v2.instructions[]
|
|
539
|
+
const instructions = json.data?.threaded_conversation_with_injections_v2?.instructions ?? [];
|
|
540
|
+
return extractTweetsFromInstructions(instructions);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ─── Search ──────────────────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
export async function searchTweets(
|
|
546
|
+
query: string,
|
|
547
|
+
product: 'Top' | 'Latest' | 'People' | 'Media' = 'Top',
|
|
548
|
+
): Promise<TweetEntry[]> {
|
|
549
|
+
requireSession();
|
|
550
|
+
const wsUrl = await findTwitterTab();
|
|
551
|
+
|
|
552
|
+
// Search requires X's client-generated transaction ID, so we navigate Chrome
|
|
553
|
+
// to the search page and capture the response from network events.
|
|
554
|
+
const productParam = product === 'Top' ? '' : `&f=${product.toLowerCase()}`;
|
|
555
|
+
const pageUrl = `https://x.com/search?q=${encodeURIComponent(query)}&src=typed_query${productParam}`;
|
|
556
|
+
const json = await cdpNavigateAndCapture(wsUrl, pageUrl, 'SearchTimeline') as AnyJson;
|
|
557
|
+
|
|
558
|
+
const instructions = json.data?.search_by_raw_query?.search_timeline?.timeline?.instructions ?? [];
|
|
559
|
+
return extractTweetsFromInstructions(instructions);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ─── Bookmarks ───────────────────────────────────────────────────────────────
|
|
563
|
+
|
|
564
|
+
export async function getBookmarks(count = 20): Promise<TweetEntry[]> {
|
|
565
|
+
const json = await graphqlGet(QUERY_IDS.Bookmarks, 'Bookmarks', {
|
|
566
|
+
count,
|
|
567
|
+
includePromotedContent: true,
|
|
568
|
+
}) as AnyJson;
|
|
569
|
+
|
|
570
|
+
// Response path: data.bookmark_timeline_v2.timeline.instructions[]
|
|
571
|
+
const instructions = json.data?.bookmark_timeline_v2?.timeline?.instructions ?? [];
|
|
572
|
+
return extractTweetsFromInstructions(instructions);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ─── Home timeline ───────────────────────────────────────────────────────────
|
|
576
|
+
|
|
577
|
+
export async function getHomeTimeline(count = 20): Promise<TweetEntry[]> {
|
|
578
|
+
const json = await graphqlGet(QUERY_IDS.HomeTimeline, 'HomeTimeline', {
|
|
579
|
+
count,
|
|
580
|
+
includePromotedContent: true,
|
|
581
|
+
requestContext: 'launch',
|
|
582
|
+
withCommunity: true,
|
|
583
|
+
}) as AnyJson;
|
|
584
|
+
|
|
585
|
+
// Response path: data.home.home_timeline_urt.instructions[]
|
|
586
|
+
const instructions = json.data?.home?.home_timeline_urt?.instructions ?? [];
|
|
587
|
+
return extractTweetsFromInstructions(instructions);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ─── Notifications ───────────────────────────────────────────────────────────
|
|
591
|
+
|
|
592
|
+
export async function getNotifications(count = 20): Promise<NotificationEntry[]> {
|
|
593
|
+
const json = await graphqlGet(QUERY_IDS.NotificationsTimeline, 'NotificationsTimeline', {
|
|
594
|
+
timeline_type: 'All',
|
|
595
|
+
count,
|
|
596
|
+
}) as AnyJson;
|
|
597
|
+
|
|
598
|
+
// Response path: data.viewer_v2.user_results.result.notification_timeline.timeline.instructions[]
|
|
599
|
+
const instructions =
|
|
600
|
+
json.data?.viewer_v2?.user_results?.result?.notification_timeline?.timeline?.instructions ?? [];
|
|
601
|
+
|
|
602
|
+
const notifications: NotificationEntry[] = [];
|
|
603
|
+
for (const instruction of instructions) {
|
|
604
|
+
for (const entry of instruction.entries ?? []) {
|
|
605
|
+
const ic = entry.content?.itemContent;
|
|
606
|
+
if (ic?.__typename !== 'TimelineNotification') continue;
|
|
607
|
+
notifications.push({
|
|
608
|
+
id: ic.id ?? entry.entryId ?? '',
|
|
609
|
+
message: ic.rich_message?.text ?? ic.notification_text?.text ?? '',
|
|
610
|
+
timestamp: ic.timestamp_ms ?? '',
|
|
611
|
+
url: ic.notification_url?.url,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return notifications;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ─── Likes ───────────────────────────────────────────────────────────────────
|
|
619
|
+
|
|
620
|
+
export async function getLikes(userId: string, count = 20): Promise<TweetEntry[]> {
|
|
621
|
+
const json = await graphqlGet(QUERY_IDS.Likes, 'Likes', {
|
|
622
|
+
userId,
|
|
623
|
+
count,
|
|
624
|
+
includePromotedContent: false,
|
|
625
|
+
withClientEventToken: false,
|
|
626
|
+
withBirdwatchNotes: false,
|
|
627
|
+
withVoice: true,
|
|
628
|
+
}) as AnyJson;
|
|
629
|
+
|
|
630
|
+
// Response path: data.user.result.timeline.timeline.instructions[]
|
|
631
|
+
const instructions = json.data?.user?.result?.timeline?.timeline?.instructions ?? [];
|
|
632
|
+
return extractTweetsFromInstructions(instructions);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ─── Followers ───────────────────────────────────────────────────────────────
|
|
636
|
+
|
|
637
|
+
export async function getFollowers(userId: string, screenName?: string): Promise<UserInfo[]> {
|
|
638
|
+
// Followers requires X's client-generated transaction ID.
|
|
639
|
+
// Navigate to the followers page and capture via CDP.
|
|
640
|
+
if (screenName) {
|
|
641
|
+
requireSession();
|
|
642
|
+
const wsUrl = await findTwitterTab();
|
|
643
|
+
const json = await cdpNavigateAndCapture(wsUrl, `https://x.com/${screenName}/followers`, 'Followers') as AnyJson;
|
|
644
|
+
const instructions = json.data?.user?.result?.timeline?.timeline?.instructions ?? [];
|
|
645
|
+
return extractUsersFromInstructions(instructions);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const json = await graphqlGet(QUERY_IDS.Followers, 'Followers', {
|
|
649
|
+
userId,
|
|
650
|
+
count: 20,
|
|
651
|
+
includePromotedContent: false,
|
|
652
|
+
withGrokTranslatedBio: false,
|
|
653
|
+
}) as AnyJson;
|
|
654
|
+
|
|
655
|
+
const instructions = json.data?.user?.result?.timeline?.timeline?.instructions ?? [];
|
|
656
|
+
return extractUsersFromInstructions(instructions);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ─── Following ───────────────────────────────────────────────────────────────
|
|
660
|
+
|
|
661
|
+
export async function getFollowing(userId: string, count = 20): Promise<UserInfo[]> {
|
|
662
|
+
const json = await graphqlGet(QUERY_IDS.Following, 'Following', {
|
|
663
|
+
userId,
|
|
664
|
+
count,
|
|
665
|
+
includePromotedContent: false,
|
|
666
|
+
withGrokTranslatedBio: false,
|
|
667
|
+
}) as AnyJson;
|
|
668
|
+
|
|
669
|
+
// Response path: data.user.result.timeline.timeline.instructions[]
|
|
670
|
+
const instructions = json.data?.user?.result?.timeline?.timeline?.instructions ?? [];
|
|
671
|
+
return extractUsersFromInstructions(instructions);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ─── User media ──────────────────────────────────────────────────────────────
|
|
675
|
+
|
|
676
|
+
export async function getUserMedia(userId: string, count = 20): Promise<TweetEntry[]> {
|
|
677
|
+
const json = await graphqlGet(QUERY_IDS.UserMedia, 'UserMedia', {
|
|
678
|
+
userId,
|
|
679
|
+
count,
|
|
680
|
+
includePromotedContent: false,
|
|
681
|
+
withClientEventToken: false,
|
|
682
|
+
withBirdwatchNotes: false,
|
|
683
|
+
withVoice: true,
|
|
684
|
+
}) as AnyJson;
|
|
685
|
+
|
|
686
|
+
// Response path: data.user.result.timeline.timeline.instructions[]
|
|
687
|
+
// (same as Likes — contains tweets that have media)
|
|
688
|
+
const instructions = json.data?.user?.result?.timeline?.timeline?.instructions ?? [];
|
|
689
|
+
return extractTweetsFromInstructions(instructions);
|
|
690
|
+
}
|