mercury-agent 0.5.0 → 0.5.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.
Files changed (125) hide show
  1. package/README.md +452 -451
  2. package/container/Dockerfile +127 -127
  3. package/container/Dockerfile.base +109 -109
  4. package/container/Dockerfile.power +17 -17
  5. package/container/agent-package.json +8 -8
  6. package/container/build.sh +54 -54
  7. package/docs/ROADMAP.md +3 -4
  8. package/docs/archive/summarization/2026-07-02-recent-dev-summary.md +71 -0
  9. package/docs/auth/dashboard.md +28 -28
  10. package/docs/auth/overview.md +109 -109
  11. package/docs/auth/whatsapp.md +173 -173
  12. package/docs/authoring-profiles.md +174 -174
  13. package/docs/configuration.md +54 -54
  14. package/docs/container-lifecycle.md +349 -349
  15. package/docs/{bugs/bwrap-privileged-linux-docker.md → debug/major/2026-07-02-bwrap-privileged-linux-docker.md} +21 -1
  16. package/docs/{bugs/e2big-prompt-delivery-fix.md → debug/major/2026-07-02-e2big-prompt-delivery-fix.md} +27 -1
  17. package/docs/{bugs/registry-pull-no-local-fallback.md → debug/major/2026-07-02-registry-pull-no-local-fallback.md} +27 -1
  18. package/docs/debug/summarization/2026-07-02-common-bug-patterns.md +72 -0
  19. package/docs/deployment.md +199 -199
  20. package/docs/extensions.md +375 -375
  21. package/docs/graceful-shutdown.md +62 -62
  22. package/docs/kb-distillation.md +77 -77
  23. package/docs/media/overview.md +140 -140
  24. package/docs/media/whatsapp.md +171 -171
  25. package/docs/memory.md +137 -137
  26. package/docs/permissions.md +217 -217
  27. package/docs/pipeline.md +228 -228
  28. package/docs/prd-chat-memory.md +76 -76
  29. package/docs/prd-config-load.md +82 -82
  30. package/docs/rate-limiting.md +229 -229
  31. package/docs/runbooks/publish-checklist.md +9 -1
  32. package/docs/scheduler.md +288 -288
  33. package/docs/setup-discord.md +100 -100
  34. package/docs/setup-slack.md +119 -119
  35. package/docs/setup-whatsapp.md +94 -94
  36. package/docs/subagents.md +166 -166
  37. package/docs/web-search.md +62 -62
  38. package/examples/extensions/README.md +12 -12
  39. package/examples/extensions/charts/index.ts +13 -13
  40. package/examples/extensions/charts/skill/SKILL.md +98 -98
  41. package/examples/extensions/gws/README.md +52 -52
  42. package/examples/extensions/gws/skill/SKILL.md +57 -57
  43. package/examples/extensions/gws/skill/references/calendar.md +101 -101
  44. package/examples/extensions/gws/skill/references/docs.md +65 -65
  45. package/examples/extensions/gws/skill/references/drive.md +79 -79
  46. package/examples/extensions/gws/skill/references/gmail.md +85 -85
  47. package/examples/extensions/gws/skill/references/sheets.md +60 -60
  48. package/examples/extensions/napkin/skill/SKILL.md +728 -728
  49. package/examples/extensions/pdf/skill/LICENSE.txt +30 -30
  50. package/examples/extensions/pdf/skill/SKILL.md +314 -314
  51. package/examples/extensions/pdf/skill/forms.md +294 -294
  52. package/examples/extensions/pdf/skill/reference.md +611 -611
  53. package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -65
  54. package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -11
  55. package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -33
  56. package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -37
  57. package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -122
  58. package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -115
  59. package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -98
  60. package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -107
  61. package/examples/extensions/permission-guard/index.ts +65 -65
  62. package/examples/extensions/pinchtab/skill/SKILL.md +224 -224
  63. package/examples/extensions/pinchtab/skill/TRUST.md +69 -69
  64. package/examples/extensions/pinchtab/skill/references/api.md +297 -297
  65. package/examples/extensions/pinchtab/skill/references/env.md +45 -45
  66. package/examples/extensions/pinchtab/skill/references/profiles.md +107 -107
  67. package/examples/extensions/tradestation/host/refresh.ts +102 -102
  68. package/examples/extensions/tradestation/index.ts +153 -153
  69. package/examples/extensions/tradestation/skill/SKILL.md +67 -67
  70. package/examples/extensions/voice-synth/index.ts +94 -94
  71. package/examples/extensions/voice-synth/skill/SKILL.md +38 -38
  72. package/examples/extensions/voice-transcribe/requirements.txt +8 -8
  73. package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -179
  74. package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -53
  75. package/examples/extensions/yahoo-mail/cli/package.json +13 -13
  76. package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -78
  77. package/package.json +106 -106
  78. package/resources/agents/explore.md +50 -50
  79. package/resources/agents/worker.md +24 -24
  80. package/resources/connection-env-vars.json +25 -25
  81. package/resources/pi-extensions/subagent/agents.ts +126 -126
  82. package/resources/pi-extensions/subagent/index.ts +964 -964
  83. package/resources/profiles/coding/AGENTS.md +43 -43
  84. package/resources/profiles/coding/mercury-profile.yaml +15 -15
  85. package/resources/profiles/general/AGENTS.md +31 -31
  86. package/resources/profiles/general/mercury-profile.yaml +15 -15
  87. package/resources/profiles/research/AGENTS.md +40 -40
  88. package/resources/profiles/research/mercury-profile.yaml +15 -15
  89. package/resources/skills/config/SKILL.md +25 -25
  90. package/resources/skills/context/SKILL.md +33 -33
  91. package/resources/skills/conversation-recap/SKILL.md +19 -19
  92. package/resources/skills/mutes/SKILL.md +31 -31
  93. package/resources/skills/permissions/SKILL.md +19 -19
  94. package/resources/skills/preferences/SKILL.md +31 -31
  95. package/resources/skills/recall/SKILL.md +24 -24
  96. package/resources/skills/roles/SKILL.md +18 -18
  97. package/resources/skills/spaces/SKILL.md +18 -18
  98. package/resources/skills/tasks/SKILL.md +45 -45
  99. package/resources/templates/AGENTS.md +157 -157
  100. package/resources/templates/env.template +38 -38
  101. package/resources/templates/mercury.example.yaml +99 -99
  102. package/src/agent/container-entry.ts +1 -1
  103. package/src/agent/container-runner.ts +1345 -1346
  104. package/src/cli/mercury.ts +39 -7
  105. package/src/cli/mrctl.ts +636 -636
  106. package/src/config-file.ts +540 -540
  107. package/src/config.ts +339 -339
  108. package/src/core/api.ts +125 -125
  109. package/src/core/caller-token.ts +101 -101
  110. package/src/core/permissions.ts +228 -228
  111. package/src/core/profiles.ts +271 -271
  112. package/src/core/routes/capability.ts +70 -70
  113. package/src/core/routes/index.ts +15 -15
  114. package/src/core/runtime.ts +1530 -1530
  115. package/src/dashboard/index.html +729 -729
  116. package/src/extensions/api.ts +273 -273
  117. package/src/extensions/loader.ts +286 -286
  118. package/src/extensions/types.ts +517 -517
  119. package/src/main.ts +605 -605
  120. package/docs/pending-updates/applicative-profiles.md +0 -15
  121. package/docs/pending-updates/caller-bound-capability-token.md +0 -15
  122. package/docs/pending-updates/dm-auto-space.md +0 -15
  123. /package/docs/archive/{2026-07-01-applicative-profiles.md → applicative-profiles/2026-07-01-applicative-profiles.md} +0 -0
  124. /package/docs/archive/{2026-07-01-caller-bound-capability-token.md → applicative-profiles/2026-07-01-caller-bound-capability-token.md} +0 -0
  125. /package/docs/archive/{2026-07-01-dm-auto-space.md → applicative-profiles/2026-07-01-dm-auto-space.md} +0 -0
@@ -1,45 +1,45 @@
1
- # Pinchtab Environment Variables
2
-
3
- ## Core runtime
4
-
5
- | Var | Default | Description |
6
- |---|---|---|
7
- | `BRIDGE_BIND` | `127.0.0.1` | Bind address. Set `0.0.0.0` for network access |
8
- | `BRIDGE_PORT` | `9867` | HTTP port |
9
- | `BRIDGE_HEADLESS` | `true` | Run Chrome headless |
10
- | `BRIDGE_TOKEN` | (none) | Bearer auth token (recommended with `0.0.0.0`) |
11
- | `BRIDGE_PROFILE` | `~/.pinchtab/chrome-profile` | Chrome profile dir |
12
- | `BRIDGE_STATE_DIR` | `~/.pinchtab` | State/session storage |
13
- | `BRIDGE_NO_RESTORE` | `false` | Skip tab restore on startup |
14
- | `BRIDGE_STEALTH` | `light` | Stealth level: `light` or `full` |
15
- | `BRIDGE_MAX_TABS` | `20` | Max open tabs (0 = unlimited) |
16
- | `BRIDGE_BLOCK_IMAGES` | `false` | Block image loading |
17
- | `BRIDGE_BLOCK_MEDIA` | `false` | Block all media (images + fonts + CSS + video) |
18
- | `BRIDGE_NO_ANIMATIONS` | `false` | Disable CSS animations/transitions |
19
- | `BRIDGE_TIMEZONE` | (none) | Force browser timezone (IANA tz) |
20
- | `BRIDGE_CHROME_VERSION` | `144.0.7559.133` | Chrome version for fingerprint rotation |
21
- | `BRIDGE_USER_AGENT` | (none) | Custom User-Agent string; also overrides Sec-Ch-Ua client hints via CDP |
22
- | `CHROME_BINARY` | (auto) | Path to Chrome/Chromium binary |
23
- | `CHROME_FLAGS` | (none) | Extra Chrome flags (space-separated) |
24
- | `BRIDGE_CONFIG` | `~/.pinchtab/config.json` | Path to config JSON file |
25
- | `BRIDGE_TIMEOUT` | `15` | Action timeout (seconds) |
26
- | `BRIDGE_NAV_TIMEOUT` | `30` | Navigation timeout (seconds) |
27
- | `CDP_URL` | (none) | Connect to existing Chrome DevTools |
28
- | `BRIDGE_NO_DASHBOARD` | `false` | Disable dashboard endpoints on instance processes |
29
-
30
- ## CLI client
31
-
32
- | Var | Default | Description |
33
- |---|---|---|
34
- | `PINCHTAB_URL` | `http://localhost:9867` | Pinchtab server URL for CLI commands |
35
- | `PINCHTAB_TOKEN` | (none) | Auth token for CLI (sent as `Authorization: Bearer`) |
36
-
37
- ## Dashboard (`pinchtab`)
38
-
39
- | Var | Default | Description |
40
- |---|---|---|
41
- | `PINCHTAB_AUTO_LAUNCH` | `false` | Auto-launch default profile at startup |
42
- | `PINCHTAB_DEFAULT_PROFILE` | `default` | Profile name for auto-launch |
43
- | `PINCHTAB_DEFAULT_PORT` | `9867` | Port for auto-launched profile |
44
- | `PINCHTAB_HEADED` | (unset) | If set, auto-launched profile is headed |
45
- | `PINCHTAB_DASHBOARD_URL` | `http://localhost:$BRIDGE_PORT` | Base URL for `pinchtab connect` |
1
+ # Pinchtab Environment Variables
2
+
3
+ ## Core runtime
4
+
5
+ | Var | Default | Description |
6
+ |---|---|---|
7
+ | `BRIDGE_BIND` | `127.0.0.1` | Bind address. Set `0.0.0.0` for network access |
8
+ | `BRIDGE_PORT` | `9867` | HTTP port |
9
+ | `BRIDGE_HEADLESS` | `true` | Run Chrome headless |
10
+ | `BRIDGE_TOKEN` | (none) | Bearer auth token (recommended with `0.0.0.0`) |
11
+ | `BRIDGE_PROFILE` | `~/.pinchtab/chrome-profile` | Chrome profile dir |
12
+ | `BRIDGE_STATE_DIR` | `~/.pinchtab` | State/session storage |
13
+ | `BRIDGE_NO_RESTORE` | `false` | Skip tab restore on startup |
14
+ | `BRIDGE_STEALTH` | `light` | Stealth level: `light` or `full` |
15
+ | `BRIDGE_MAX_TABS` | `20` | Max open tabs (0 = unlimited) |
16
+ | `BRIDGE_BLOCK_IMAGES` | `false` | Block image loading |
17
+ | `BRIDGE_BLOCK_MEDIA` | `false` | Block all media (images + fonts + CSS + video) |
18
+ | `BRIDGE_NO_ANIMATIONS` | `false` | Disable CSS animations/transitions |
19
+ | `BRIDGE_TIMEZONE` | (none) | Force browser timezone (IANA tz) |
20
+ | `BRIDGE_CHROME_VERSION` | `144.0.7559.133` | Chrome version for fingerprint rotation |
21
+ | `BRIDGE_USER_AGENT` | (none) | Custom User-Agent string; also overrides Sec-Ch-Ua client hints via CDP |
22
+ | `CHROME_BINARY` | (auto) | Path to Chrome/Chromium binary |
23
+ | `CHROME_FLAGS` | (none) | Extra Chrome flags (space-separated) |
24
+ | `BRIDGE_CONFIG` | `~/.pinchtab/config.json` | Path to config JSON file |
25
+ | `BRIDGE_TIMEOUT` | `15` | Action timeout (seconds) |
26
+ | `BRIDGE_NAV_TIMEOUT` | `30` | Navigation timeout (seconds) |
27
+ | `CDP_URL` | (none) | Connect to existing Chrome DevTools |
28
+ | `BRIDGE_NO_DASHBOARD` | `false` | Disable dashboard endpoints on instance processes |
29
+
30
+ ## CLI client
31
+
32
+ | Var | Default | Description |
33
+ |---|---|---|
34
+ | `PINCHTAB_URL` | `http://localhost:9867` | Pinchtab server URL for CLI commands |
35
+ | `PINCHTAB_TOKEN` | (none) | Auth token for CLI (sent as `Authorization: Bearer`) |
36
+
37
+ ## Dashboard (`pinchtab`)
38
+
39
+ | Var | Default | Description |
40
+ |---|---|---|
41
+ | `PINCHTAB_AUTO_LAUNCH` | `false` | Auto-launch default profile at startup |
42
+ | `PINCHTAB_DEFAULT_PROFILE` | `default` | Profile name for auto-launch |
43
+ | `PINCHTAB_DEFAULT_PORT` | `9867` | Port for auto-launched profile |
44
+ | `PINCHTAB_HEADED` | (unset) | If set, auto-launched profile is headed |
45
+ | `PINCHTAB_DASHBOARD_URL` | `http://localhost:$BRIDGE_PORT` | Base URL for `pinchtab connect` |
@@ -1,107 +1,107 @@
1
- # Profile Management
2
-
3
- When running `pinchtab`, profiles are managed via the dashboard API on port 9867.
4
-
5
- ## List profiles
6
-
7
- ```bash
8
- curl http://localhost:9867/profiles
9
- ```
10
-
11
- Returns array of profiles with `id`, `name`, `accountEmail`, `useWhen`, etc.
12
-
13
- ## Start a profile
14
-
15
- ```bash
16
- # Auto-allocate port (recommended)
17
- curl -X POST http://localhost:9867/profiles/<ID>/start
18
-
19
- # With specific port and headless mode
20
- curl -X POST http://localhost:9867/profiles/<ID>/start \
21
- -H 'Content-Type: application/json' \
22
- -d '{"port": "9868", "headless": true}'
23
-
24
- # Short alias
25
- curl -X POST http://localhost:9867/start/<ID>
26
- ```
27
-
28
- Returns instance info including allocated `port`. Use that port for all subsequent API calls.
29
-
30
- ## Stop a profile
31
-
32
- ```bash
33
- curl -X POST http://localhost:9867/profiles/<ID>/stop
34
-
35
- # Short alias
36
- curl -X POST http://localhost:9867/stop/<ID>
37
- ```
38
-
39
- ## Check instance status
40
-
41
- ```bash
42
- # By profile ID (recommended)
43
- curl http://localhost:9867/profiles/<ID>/instance
44
-
45
- # By profile name
46
- curl http://localhost:9867/profiles/My%20Profile/instance
47
- ```
48
-
49
- ## Launch by name
50
-
51
- ```bash
52
- curl -X POST http://localhost:9867/instances/launch \
53
- -H 'Content-Type: application/json' \
54
- -d '{"name": "work", "port": "9868"}'
55
- ```
56
-
57
- ## CLI usage with profiles
58
-
59
- The CLI doesn't have profile subcommands yet — use `curl` for profile management.
60
- Once a profile instance is running, point the CLI at it:
61
-
62
- ```bash
63
- # Get the instance port, then use CLI
64
- PINCHTAB_URL=http://localhost:9868 pinchtab snap -i
65
- ```
66
-
67
- ## Typical agent flow
68
-
69
- ```bash
70
- # 1. List profiles
71
- PROFILES=$(curl -s http://localhost:9867/profiles)
72
-
73
- # 2. Start profile (auto-allocates port)
74
- INSTANCE=$(curl -s -X POST http://localhost:9867/profiles/$PROFILE_ID/start)
75
- PORT=$(echo $INSTANCE | jq -r .port)
76
-
77
- # 3. Use the instance
78
- curl -X POST http://localhost:$PORT/navigate -H 'Content-Type: application/json' \
79
- -d '{"url": "https://mail.google.com"}'
80
- curl http://localhost:$PORT/snapshot?maxTokens=4000
81
-
82
- # 4. Stop when done
83
- curl -s -X POST http://localhost:9867/profiles/$PROFILE_ID/stop
84
- ```
85
-
86
- ## Profile IDs
87
-
88
- Each profile gets a stable 12-char hex ID (SHA-256 of name, truncated) stored in `profile.json`. IDs are URL-safe and never change — use them instead of names in automation.
89
-
90
- ## Headed mode
91
-
92
- Headed mode = real visible Chrome window managed by Pinchtab.
93
-
94
- - Human can log in, pass 2FA/captcha, validate state
95
- - Agent calls HTTP APIs against the same running instance
96
- - Session state persists in profile directory (cookies/storage carry over)
97
-
98
- Recommended human + agent flow:
99
-
100
- ```bash
101
- # Human starts dashboard and sets up profile
102
- pinchtab
103
-
104
- # Agent resolves the profile endpoint
105
- PINCHTAB_BASE_URL="$(pinchtab connect <profile-name>)"
106
- curl "$PINCHTAB_BASE_URL/health"
107
- ```
1
+ # Profile Management
2
+
3
+ When running `pinchtab`, profiles are managed via the dashboard API on port 9867.
4
+
5
+ ## List profiles
6
+
7
+ ```bash
8
+ curl http://localhost:9867/profiles
9
+ ```
10
+
11
+ Returns array of profiles with `id`, `name`, `accountEmail`, `useWhen`, etc.
12
+
13
+ ## Start a profile
14
+
15
+ ```bash
16
+ # Auto-allocate port (recommended)
17
+ curl -X POST http://localhost:9867/profiles/<ID>/start
18
+
19
+ # With specific port and headless mode
20
+ curl -X POST http://localhost:9867/profiles/<ID>/start \
21
+ -H 'Content-Type: application/json' \
22
+ -d '{"port": "9868", "headless": true}'
23
+
24
+ # Short alias
25
+ curl -X POST http://localhost:9867/start/<ID>
26
+ ```
27
+
28
+ Returns instance info including allocated `port`. Use that port for all subsequent API calls.
29
+
30
+ ## Stop a profile
31
+
32
+ ```bash
33
+ curl -X POST http://localhost:9867/profiles/<ID>/stop
34
+
35
+ # Short alias
36
+ curl -X POST http://localhost:9867/stop/<ID>
37
+ ```
38
+
39
+ ## Check instance status
40
+
41
+ ```bash
42
+ # By profile ID (recommended)
43
+ curl http://localhost:9867/profiles/<ID>/instance
44
+
45
+ # By profile name
46
+ curl http://localhost:9867/profiles/My%20Profile/instance
47
+ ```
48
+
49
+ ## Launch by name
50
+
51
+ ```bash
52
+ curl -X POST http://localhost:9867/instances/launch \
53
+ -H 'Content-Type: application/json' \
54
+ -d '{"name": "work", "port": "9868"}'
55
+ ```
56
+
57
+ ## CLI usage with profiles
58
+
59
+ The CLI doesn't have profile subcommands yet — use `curl` for profile management.
60
+ Once a profile instance is running, point the CLI at it:
61
+
62
+ ```bash
63
+ # Get the instance port, then use CLI
64
+ PINCHTAB_URL=http://localhost:9868 pinchtab snap -i
65
+ ```
66
+
67
+ ## Typical agent flow
68
+
69
+ ```bash
70
+ # 1. List profiles
71
+ PROFILES=$(curl -s http://localhost:9867/profiles)
72
+
73
+ # 2. Start profile (auto-allocates port)
74
+ INSTANCE=$(curl -s -X POST http://localhost:9867/profiles/$PROFILE_ID/start)
75
+ PORT=$(echo $INSTANCE | jq -r .port)
76
+
77
+ # 3. Use the instance
78
+ curl -X POST http://localhost:$PORT/navigate -H 'Content-Type: application/json' \
79
+ -d '{"url": "https://mail.google.com"}'
80
+ curl http://localhost:$PORT/snapshot?maxTokens=4000
81
+
82
+ # 4. Stop when done
83
+ curl -s -X POST http://localhost:9867/profiles/$PROFILE_ID/stop
84
+ ```
85
+
86
+ ## Profile IDs
87
+
88
+ Each profile gets a stable 12-char hex ID (SHA-256 of name, truncated) stored in `profile.json`. IDs are URL-safe and never change — use them instead of names in automation.
89
+
90
+ ## Headed mode
91
+
92
+ Headed mode = real visible Chrome window managed by Pinchtab.
93
+
94
+ - Human can log in, pass 2FA/captcha, validate state
95
+ - Agent calls HTTP APIs against the same running instance
96
+ - Session state persists in profile directory (cookies/storage carry over)
97
+
98
+ Recommended human + agent flow:
99
+
100
+ ```bash
101
+ # Human starts dashboard and sets up profile
102
+ pinchtab
103
+
104
+ # Agent resolves the profile endpoint
105
+ PINCHTAB_BASE_URL="$(pinchtab connect <profile-name>)"
106
+ curl "$PINCHTAB_BASE_URL/health"
107
+ ```
@@ -1,102 +1,102 @@
1
- export const TRADESTATION_EXT = "tradestation" as const;
2
-
3
- type Db = {
4
- getExtState(ext: string, key: string): string | null;
5
- setExtState(ext: string, key: string, value: string): void;
6
- deleteExtState(ext: string, key: string): boolean;
7
- };
8
-
9
- const TRADESTATION_TOKEN_URL = "https://signin.tradestation.com/oauth/token";
10
-
11
- export async function runTradeStationTokenRefresh(
12
- db: Db,
13
- log: { info(msg: string): void; warn(msg: string): void; error(msg: string): void },
14
- fetchImpl: typeof fetch = fetch,
15
- ): Promise<void> {
16
- const clientId =
17
- process.env.MERCURY_TS_CLIENT_ID?.trim() ||
18
- process.env.TS_CLIENT_ID?.trim() ||
19
- null;
20
- if (!clientId) {
21
- log.warn("[ts-refresh] no client_id — skipping");
22
- return;
23
- }
24
-
25
- const clientSecret = process.env.MERCURY_TS_CLIENT_SECRET?.trim() || null;
26
-
27
- let refreshToken =
28
- db.getExtState(TRADESTATION_EXT, "refresh_token") ??
29
- process.env.MERCURY_TRADESTATION_REFRESH_TOKEN?.trim() ??
30
- process.env.TS_REFRESH_TOKEN?.trim() ??
31
- null;
32
-
33
- if (!refreshToken) {
34
- log.warn("[ts-refresh] no refresh_token — skipping");
35
- return;
36
- }
37
-
38
- const accessToken = db.getExtState(TRADESTATION_EXT, "access_token");
39
- const expiryRaw = db.getExtState(TRADESTATION_EXT, "token_expiry_ms");
40
- if (accessToken && expiryRaw) {
41
- const expiry = Number(expiryRaw);
42
- if (expiry > Date.now() + 60_000) {
43
- return;
44
- }
45
- }
46
-
47
- const bodyParams: Record<string, string> = {
48
- grant_type: "refresh_token",
49
- client_id: clientId,
50
- refresh_token: refreshToken,
51
- };
52
- if (clientSecret) bodyParams.client_secret = clientSecret;
53
- const body = new URLSearchParams(bodyParams);
54
-
55
- let res: Response;
56
- try {
57
- res = await fetchImpl(TRADESTATION_TOKEN_URL, {
58
- method: "POST",
59
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
60
- body: body.toString(),
61
- });
62
- } catch (err) {
63
- const msg = err instanceof Error ? err.message : String(err);
64
- log.error(`[ts-refresh] fetch error: ${msg}`);
65
- db.setExtState(TRADESTATION_EXT, "auth_error", "refresh_failed:network");
66
- return;
67
- }
68
-
69
- if (!res.ok) {
70
- log.warn(`[ts-refresh] token refresh failed: ${res.status}`);
71
- db.setExtState(TRADESTATION_EXT, "auth_error", `refresh_failed:${res.status}`);
72
- return;
73
- }
74
-
75
- let data: Record<string, unknown>;
76
- try {
77
- data = (await res.json()) as Record<string, unknown>;
78
- } catch {
79
- db.setExtState(TRADESTATION_EXT, "auth_error", "refresh_failed:invalid_json");
80
- return;
81
- }
82
-
83
- const newAccess = typeof data.access_token === "string" ? data.access_token : null;
84
- if (!newAccess) {
85
- db.setExtState(TRADESTATION_EXT, "auth_error", "refresh_failed:no_access_token");
86
- return;
87
- }
88
-
89
- db.setExtState(TRADESTATION_EXT, "access_token", newAccess);
90
- if (typeof data.refresh_token === "string" && data.refresh_token) {
91
- db.setExtState(TRADESTATION_EXT, "refresh_token", data.refresh_token);
92
- }
93
- if (typeof data.expires_in === "number") {
94
- db.setExtState(
95
- TRADESTATION_EXT,
96
- "token_expiry_ms",
97
- String(Date.now() + data.expires_in * 1000),
98
- );
99
- }
100
- db.deleteExtState(TRADESTATION_EXT, "auth_error");
101
- log.info("[ts-refresh] tokens refreshed");
102
- }
1
+ export const TRADESTATION_EXT = "tradestation" as const;
2
+
3
+ type Db = {
4
+ getExtState(ext: string, key: string): string | null;
5
+ setExtState(ext: string, key: string, value: string): void;
6
+ deleteExtState(ext: string, key: string): boolean;
7
+ };
8
+
9
+ const TRADESTATION_TOKEN_URL = "https://signin.tradestation.com/oauth/token";
10
+
11
+ export async function runTradeStationTokenRefresh(
12
+ db: Db,
13
+ log: { info(msg: string): void; warn(msg: string): void; error(msg: string): void },
14
+ fetchImpl: typeof fetch = fetch,
15
+ ): Promise<void> {
16
+ const clientId =
17
+ process.env.MERCURY_TS_CLIENT_ID?.trim() ||
18
+ process.env.TS_CLIENT_ID?.trim() ||
19
+ null;
20
+ if (!clientId) {
21
+ log.warn("[ts-refresh] no client_id — skipping");
22
+ return;
23
+ }
24
+
25
+ const clientSecret = process.env.MERCURY_TS_CLIENT_SECRET?.trim() || null;
26
+
27
+ let refreshToken =
28
+ db.getExtState(TRADESTATION_EXT, "refresh_token") ??
29
+ process.env.MERCURY_TRADESTATION_REFRESH_TOKEN?.trim() ??
30
+ process.env.TS_REFRESH_TOKEN?.trim() ??
31
+ null;
32
+
33
+ if (!refreshToken) {
34
+ log.warn("[ts-refresh] no refresh_token — skipping");
35
+ return;
36
+ }
37
+
38
+ const accessToken = db.getExtState(TRADESTATION_EXT, "access_token");
39
+ const expiryRaw = db.getExtState(TRADESTATION_EXT, "token_expiry_ms");
40
+ if (accessToken && expiryRaw) {
41
+ const expiry = Number(expiryRaw);
42
+ if (expiry > Date.now() + 60_000) {
43
+ return;
44
+ }
45
+ }
46
+
47
+ const bodyParams: Record<string, string> = {
48
+ grant_type: "refresh_token",
49
+ client_id: clientId,
50
+ refresh_token: refreshToken,
51
+ };
52
+ if (clientSecret) bodyParams.client_secret = clientSecret;
53
+ const body = new URLSearchParams(bodyParams);
54
+
55
+ let res: Response;
56
+ try {
57
+ res = await fetchImpl(TRADESTATION_TOKEN_URL, {
58
+ method: "POST",
59
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
60
+ body: body.toString(),
61
+ });
62
+ } catch (err) {
63
+ const msg = err instanceof Error ? err.message : String(err);
64
+ log.error(`[ts-refresh] fetch error: ${msg}`);
65
+ db.setExtState(TRADESTATION_EXT, "auth_error", "refresh_failed:network");
66
+ return;
67
+ }
68
+
69
+ if (!res.ok) {
70
+ log.warn(`[ts-refresh] token refresh failed: ${res.status}`);
71
+ db.setExtState(TRADESTATION_EXT, "auth_error", `refresh_failed:${res.status}`);
72
+ return;
73
+ }
74
+
75
+ let data: Record<string, unknown>;
76
+ try {
77
+ data = (await res.json()) as Record<string, unknown>;
78
+ } catch {
79
+ db.setExtState(TRADESTATION_EXT, "auth_error", "refresh_failed:invalid_json");
80
+ return;
81
+ }
82
+
83
+ const newAccess = typeof data.access_token === "string" ? data.access_token : null;
84
+ if (!newAccess) {
85
+ db.setExtState(TRADESTATION_EXT, "auth_error", "refresh_failed:no_access_token");
86
+ return;
87
+ }
88
+
89
+ db.setExtState(TRADESTATION_EXT, "access_token", newAccess);
90
+ if (typeof data.refresh_token === "string" && data.refresh_token) {
91
+ db.setExtState(TRADESTATION_EXT, "refresh_token", data.refresh_token);
92
+ }
93
+ if (typeof data.expires_in === "number") {
94
+ db.setExtState(
95
+ TRADESTATION_EXT,
96
+ "token_expiry_ms",
97
+ String(Date.now() + data.expires_in * 1000),
98
+ );
99
+ }
100
+ db.deleteExtState(TRADESTATION_EXT, "auth_error");
101
+ log.info("[ts-refresh] tokens refreshed");
102
+ }