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,153 +1,153 @@
1
- import {
2
- runTradeStationTokenRefresh,
3
- TRADESTATION_EXT,
4
- } from "./host/refresh.js";
5
-
6
- const API_BASE_DEFAULT = "https://api.tradestation.com/v3";
7
-
8
- function apiBaseFromHost(): string {
9
- const raw = process.env.MERCURY_TS_API_BASE?.trim();
10
- if (raw) return raw.replace(/\/$/, "");
11
- const env = process.env.MERCURY_TS_ENVIRONMENT?.trim().toUpperCase();
12
- if (env === "LIVE" || env === "SIM") {
13
- return API_BASE_DEFAULT;
14
- }
15
- return API_BASE_DEFAULT;
16
- }
17
-
18
- type ConnectionStatus = "connected" | "needs-reauth" | "broken" | "unknown";
19
-
20
- type ConnectionStatusCtx = {
21
- db: {
22
- getExtState(e: string, k: string): string | null;
23
- };
24
- };
25
-
26
- type ExtCtx = {
27
- db: {
28
- getExtState(e: string, k: string): string | null;
29
- setExtState(e: string, k: string, v: string): void;
30
- deleteExtState(e: string, k: string): boolean;
31
- };
32
- log: {
33
- info(m: string): void;
34
- warn(m: string): void;
35
- error(m: string): void;
36
- };
37
- };
38
-
39
- /** Structural match for `MercuryExtensionAPI` (avoid package subpath imports). */
40
- type MercuryExt = {
41
- permission(opts: { defaultRoles: string[] }): void;
42
- env(def: { from: string; as?: string }): void;
43
- requires(
44
- capabilities: (
45
- | "tools"
46
- | "vision"
47
- | "audio_input"
48
- | "audio_output"
49
- | "extended_thinking"
50
- )[],
51
- ): void;
52
- job(name: string, def: { interval: number; run: (ctx: ExtCtx) => Promise<void> }): void;
53
- on(event: "startup", handler: (event: Record<string, never>, ctx: ExtCtx) => Promise<void>): void;
54
- on(
55
- event: "before_container",
56
- handler: (
57
- event: { spaceId: string; callerId: string },
58
- ctx: ExtCtx & {
59
- hasCallerPermission(spaceId: string, callerId: string, permission: string): boolean;
60
- },
61
- ) => Promise<{ env?: Record<string, string> } | undefined>,
62
- ): void;
63
- skill(relativePath: string): void;
64
- connection(def: {
65
- displayName: string;
66
- iconUrl?: string;
67
- category:
68
- | "email"
69
- | "drive"
70
- | "calendar"
71
- | "finance"
72
- | "messaging"
73
- | "docs"
74
- | "workspace"
75
- | "other";
76
- authType:
77
- | "oauth2"
78
- | "apikey"
79
- | "app-password"
80
- | "credentials-file"
81
- | "form"
82
- | "custom";
83
- credentialEnvVar?: string;
84
- scopes?: string[];
85
- statusCheck?: (ctx: ConnectionStatusCtx) => Promise<{ status: ConnectionStatus; detail?: string }>;
86
- }): void;
87
- };
88
-
89
- export default function (mercury: MercuryExt) {
90
- mercury.permission({ defaultRoles: [] });
91
- mercury.requires(["tools"]);
92
- mercury.env({ from: "MERCURY_TS_CLIENT_ID" });
93
- mercury.env({ from: "MERCURY_TS_CLIENT_SECRET" });
94
- mercury.env({ from: "MERCURY_TRADESTATION_REFRESH_TOKEN" });
95
-
96
- mercury.connection({
97
- displayName: "TradeStation",
98
- category: "finance",
99
- authType: "oauth2",
100
- scopes: ["openid", "offline_access", "MarketData", "ReadAccount", "Trade"],
101
- // No credentialEnvVar — credentials live in extension_state, not env.
102
- statusCheck: async (ctx) => {
103
- const authError = ctx.db.getExtState(TRADESTATION_EXT, "auth_error");
104
- if (authError) {
105
- return { status: "needs-reauth", detail: authError };
106
- }
107
- const token = ctx.db.getExtState(TRADESTATION_EXT, "access_token");
108
- if (token) {
109
- return { status: "connected" };
110
- }
111
- return { status: "unknown" };
112
- },
113
- });
114
-
115
- mercury.on("startup", async (_, ctx) => {
116
- await runTradeStationTokenRefresh(ctx.db, ctx.log);
117
- });
118
-
119
- mercury.job("ts-token-refresh", {
120
- interval: 10 * 60 * 1000,
121
- run: async (ctx) => {
122
- await runTradeStationTokenRefresh(ctx.db, ctx.log);
123
- },
124
- });
125
-
126
- mercury.on("before_container", async (event, ctx) => {
127
- if (
128
- !ctx.hasCallerPermission(event.spaceId, event.callerId, TRADESTATION_EXT)
129
- ) {
130
- return undefined;
131
- }
132
-
133
- const base = apiBaseFromHost();
134
- const authError = ctx.db.getExtState(TRADESTATION_EXT, "auth_error");
135
- const token = ctx.db.getExtState(TRADESTATION_EXT, "access_token");
136
-
137
- const env: Record<string, string> = {
138
- TRADESTATION_API_BASE: base,
139
- };
140
-
141
- if (authError) {
142
- env.TRADESTATION_AUTH_ERROR = authError;
143
- }
144
-
145
- if (token) {
146
- env.TRADESTATION_ACCESS_TOKEN = token;
147
- }
148
-
149
- return { env };
150
- });
151
-
152
- mercury.skill("./skill");
153
- }
1
+ import {
2
+ runTradeStationTokenRefresh,
3
+ TRADESTATION_EXT,
4
+ } from "./host/refresh.js";
5
+
6
+ const API_BASE_DEFAULT = "https://api.tradestation.com/v3";
7
+
8
+ function apiBaseFromHost(): string {
9
+ const raw = process.env.MERCURY_TS_API_BASE?.trim();
10
+ if (raw) return raw.replace(/\/$/, "");
11
+ const env = process.env.MERCURY_TS_ENVIRONMENT?.trim().toUpperCase();
12
+ if (env === "LIVE" || env === "SIM") {
13
+ return API_BASE_DEFAULT;
14
+ }
15
+ return API_BASE_DEFAULT;
16
+ }
17
+
18
+ type ConnectionStatus = "connected" | "needs-reauth" | "broken" | "unknown";
19
+
20
+ type ConnectionStatusCtx = {
21
+ db: {
22
+ getExtState(e: string, k: string): string | null;
23
+ };
24
+ };
25
+
26
+ type ExtCtx = {
27
+ db: {
28
+ getExtState(e: string, k: string): string | null;
29
+ setExtState(e: string, k: string, v: string): void;
30
+ deleteExtState(e: string, k: string): boolean;
31
+ };
32
+ log: {
33
+ info(m: string): void;
34
+ warn(m: string): void;
35
+ error(m: string): void;
36
+ };
37
+ };
38
+
39
+ /** Structural match for `MercuryExtensionAPI` (avoid package subpath imports). */
40
+ type MercuryExt = {
41
+ permission(opts: { defaultRoles: string[] }): void;
42
+ env(def: { from: string; as?: string }): void;
43
+ requires(
44
+ capabilities: (
45
+ | "tools"
46
+ | "vision"
47
+ | "audio_input"
48
+ | "audio_output"
49
+ | "extended_thinking"
50
+ )[],
51
+ ): void;
52
+ job(name: string, def: { interval: number; run: (ctx: ExtCtx) => Promise<void> }): void;
53
+ on(event: "startup", handler: (event: Record<string, never>, ctx: ExtCtx) => Promise<void>): void;
54
+ on(
55
+ event: "before_container",
56
+ handler: (
57
+ event: { spaceId: string; callerId: string },
58
+ ctx: ExtCtx & {
59
+ hasCallerPermission(spaceId: string, callerId: string, permission: string): boolean;
60
+ },
61
+ ) => Promise<{ env?: Record<string, string> } | undefined>,
62
+ ): void;
63
+ skill(relativePath: string): void;
64
+ connection(def: {
65
+ displayName: string;
66
+ iconUrl?: string;
67
+ category:
68
+ | "email"
69
+ | "drive"
70
+ | "calendar"
71
+ | "finance"
72
+ | "messaging"
73
+ | "docs"
74
+ | "workspace"
75
+ | "other";
76
+ authType:
77
+ | "oauth2"
78
+ | "apikey"
79
+ | "app-password"
80
+ | "credentials-file"
81
+ | "form"
82
+ | "custom";
83
+ credentialEnvVar?: string;
84
+ scopes?: string[];
85
+ statusCheck?: (ctx: ConnectionStatusCtx) => Promise<{ status: ConnectionStatus; detail?: string }>;
86
+ }): void;
87
+ };
88
+
89
+ export default function (mercury: MercuryExt) {
90
+ mercury.permission({ defaultRoles: [] });
91
+ mercury.requires(["tools"]);
92
+ mercury.env({ from: "MERCURY_TS_CLIENT_ID" });
93
+ mercury.env({ from: "MERCURY_TS_CLIENT_SECRET" });
94
+ mercury.env({ from: "MERCURY_TRADESTATION_REFRESH_TOKEN" });
95
+
96
+ mercury.connection({
97
+ displayName: "TradeStation",
98
+ category: "finance",
99
+ authType: "oauth2",
100
+ scopes: ["openid", "offline_access", "MarketData", "ReadAccount", "Trade"],
101
+ // No credentialEnvVar — credentials live in extension_state, not env.
102
+ statusCheck: async (ctx) => {
103
+ const authError = ctx.db.getExtState(TRADESTATION_EXT, "auth_error");
104
+ if (authError) {
105
+ return { status: "needs-reauth", detail: authError };
106
+ }
107
+ const token = ctx.db.getExtState(TRADESTATION_EXT, "access_token");
108
+ if (token) {
109
+ return { status: "connected" };
110
+ }
111
+ return { status: "unknown" };
112
+ },
113
+ });
114
+
115
+ mercury.on("startup", async (_, ctx) => {
116
+ await runTradeStationTokenRefresh(ctx.db, ctx.log);
117
+ });
118
+
119
+ mercury.job("ts-token-refresh", {
120
+ interval: 10 * 60 * 1000,
121
+ run: async (ctx) => {
122
+ await runTradeStationTokenRefresh(ctx.db, ctx.log);
123
+ },
124
+ });
125
+
126
+ mercury.on("before_container", async (event, ctx) => {
127
+ if (
128
+ !ctx.hasCallerPermission(event.spaceId, event.callerId, TRADESTATION_EXT)
129
+ ) {
130
+ return undefined;
131
+ }
132
+
133
+ const base = apiBaseFromHost();
134
+ const authError = ctx.db.getExtState(TRADESTATION_EXT, "auth_error");
135
+ const token = ctx.db.getExtState(TRADESTATION_EXT, "access_token");
136
+
137
+ const env: Record<string, string> = {
138
+ TRADESTATION_API_BASE: base,
139
+ };
140
+
141
+ if (authError) {
142
+ env.TRADESTATION_AUTH_ERROR = authError;
143
+ }
144
+
145
+ if (token) {
146
+ env.TRADESTATION_ACCESS_TOKEN = token;
147
+ }
148
+
149
+ return { env };
150
+ });
151
+
152
+ mercury.skill("./skill");
153
+ }
@@ -1,67 +1,67 @@
1
- ---
2
- name: tradestation
3
- description: Call TradeStation REST API v3 from the agent (accounts, balances, positions, bars) and place orders via mrctl two-step confirmation. Admin-only; requires Mercury host OAuth setup and token refresh job.
4
- allowed-tools: Bash
5
- ---
6
-
7
- # TradeStation (Mercury extension)
8
-
9
- This skill is for **admin** callers only. Mercury injects `TRADESTATION_ACCESS_TOKEN` and `TRADESTATION_API_BASE` only when the user has the `tradestation` permission (default: admins only).
10
-
11
- ## Host setup
12
-
13
- 1. Register an OAuth app with TradeStation (same model as Tagula: `TS_CLIENT_ID`, `TS_CLIENT_SECRET`, redirect URI, scopes such as `openid profile offline_access MarketData ReadAccount`).
14
- - **Orders**: add TradeStation scopes required for order placement (see [TradeStation scopes](https://api.tradestation.com/docs/fundamentals/authentication/scopes/)); re-authenticate after changing scopes.
15
- 2. Complete OAuth once (e.g. Tagula `manage-auth` / your dashboard) and put the same credentials in the Mercury host `.env` as either the `MERCURY_*` names **or** the same Tagula names (`TS_CLIENT_ID`, `TS_CLIENT_SECRET`, `TS_REFRESH_TOKEN`, optional `TS_ACCESS_TOKEN`). The refresh job accepts both.
16
- - Optional: `MERCURY_TS_TOKEN_URL`, `MERCURY_TS_API_BASE`, `MERCURY_TS_ENVIRONMENT` (`SIM` / `LIVE` — documented for parity; v3 base URL is unchanged)
17
- - **Live orders**: by default, only **SIM** accounts (AccountID matching `SIM…`) may place orders via Mercury. To allow non-SIM accounts, set `MERCURY_TS_ALLOW_LIVE_ORDERS=true` on the host (real-money risk).
18
- 3. Restart Mercury so the extension job runs (refresh every **10 minutes**).
19
-
20
- If refresh fails, the container may receive `TRADESTATION_AUTH_ERROR` with a short code (e.g. `refresh_failed:...`, `no_refresh_token`). Tell the user to re-authenticate and update host env / tokens.
21
-
22
- ## CLI — reads (Bun)
23
-
24
- Scripts live next to this skill. In the agent container the skill is typically mounted under `/home/node/.pi/agent/skills/tradestation/`.
25
-
26
- ```bash
27
- bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts accounts
28
- bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts balances ACCOUNT_KEY
29
- bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts positions ACCOUNT_KEY
30
- bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts bars SYMBOL [barsback]
31
- ```
32
-
33
- Examples:
34
-
35
- ```bash
36
- bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts accounts
37
- bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts balances SIM123456789
38
- bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts positions SIM123456789
39
- bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts bars AAPL 20
40
- bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts bars '%40ES' 10
41
- ```
42
-
43
- Use a URL-encoded symbol for futures (e.g. `@ES` as `%40ES`) or pass `@ES` quoted so the shell does not expand it.
44
-
45
- ## Orders — two-step confirmation (`mrctl`)
46
-
47
- Order placement runs on the **Mercury host** (not inside `ts-cli`) so the flow can be audited and gated. Use **`mrctl`** from the agent container (same headers as other `mrctl` commands).
48
-
49
- 1. **Propose** (no `--confirm`): calls TradeStation `orderconfirm`, stores a pending id (~15 minutes), returns a summary and `pendingId`.
50
- 2. **Human check**: show the summary on **any** chat platform. The user can confirm with plain text, e.g. `CONFIRM <pendingId>`, or tell you to run step 3.
51
- 3. **Execute**: same flags as step 1 plus `--confirm --pending-id <pendingId>`.
52
-
53
- Example (SIM account):
54
-
55
- ```bash
56
- mrctl tradestation order --account SIM123456789 --symbol AAPL --quantity 1 --action SELL --type Market --duration DAY
57
- # … user approves …
58
- mrctl tradestation order --account SIM123456789 --symbol AAPL --quantity 1 --action SELL --type Market --duration DAY --confirm --pending-id '<uuid-from-step-1>'
59
- ```
60
-
61
- Optional: `--route Intelligent` (default), `--limit-price`, `--stop-price`, `--expiration-date` (for time-in-force), `--type` (default `Market`).
62
-
63
- **Rules**: Do not skip human confirmation. Do not place live orders unless the user explicitly wants that and `MERCURY_TS_ALLOW_LIVE_ORDERS` is enabled on the host.
64
-
65
- ## Scope
66
-
67
- Read helpers: **accounts**, **balances**, **positions**, **bars**. Orders: **host-side** via `mrctl tradestation order` only. Not a full API mirror; no streaming in-container.
1
+ ---
2
+ name: tradestation
3
+ description: Call TradeStation REST API v3 from the agent (accounts, balances, positions, bars) and place orders via mrctl two-step confirmation. Admin-only; requires Mercury host OAuth setup and token refresh job.
4
+ allowed-tools: Bash
5
+ ---
6
+
7
+ # TradeStation (Mercury extension)
8
+
9
+ This skill is for **admin** callers only. Mercury injects `TRADESTATION_ACCESS_TOKEN` and `TRADESTATION_API_BASE` only when the user has the `tradestation` permission (default: admins only).
10
+
11
+ ## Host setup
12
+
13
+ 1. Register an OAuth app with TradeStation (same model as Tagula: `TS_CLIENT_ID`, `TS_CLIENT_SECRET`, redirect URI, scopes such as `openid profile offline_access MarketData ReadAccount`).
14
+ - **Orders**: add TradeStation scopes required for order placement (see [TradeStation scopes](https://api.tradestation.com/docs/fundamentals/authentication/scopes/)); re-authenticate after changing scopes.
15
+ 2. Complete OAuth once (e.g. Tagula `manage-auth` / your dashboard) and put the same credentials in the Mercury host `.env` as either the `MERCURY_*` names **or** the same Tagula names (`TS_CLIENT_ID`, `TS_CLIENT_SECRET`, `TS_REFRESH_TOKEN`, optional `TS_ACCESS_TOKEN`). The refresh job accepts both.
16
+ - Optional: `MERCURY_TS_TOKEN_URL`, `MERCURY_TS_API_BASE`, `MERCURY_TS_ENVIRONMENT` (`SIM` / `LIVE` — documented for parity; v3 base URL is unchanged)
17
+ - **Live orders**: by default, only **SIM** accounts (AccountID matching `SIM…`) may place orders via Mercury. To allow non-SIM accounts, set `MERCURY_TS_ALLOW_LIVE_ORDERS=true` on the host (real-money risk).
18
+ 3. Restart Mercury so the extension job runs (refresh every **10 minutes**).
19
+
20
+ If refresh fails, the container may receive `TRADESTATION_AUTH_ERROR` with a short code (e.g. `refresh_failed:...`, `no_refresh_token`). Tell the user to re-authenticate and update host env / tokens.
21
+
22
+ ## CLI — reads (Bun)
23
+
24
+ Scripts live next to this skill. In the agent container the skill is typically mounted under `/home/node/.pi/agent/skills/tradestation/`.
25
+
26
+ ```bash
27
+ bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts accounts
28
+ bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts balances ACCOUNT_KEY
29
+ bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts positions ACCOUNT_KEY
30
+ bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts bars SYMBOL [barsback]
31
+ ```
32
+
33
+ Examples:
34
+
35
+ ```bash
36
+ bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts accounts
37
+ bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts balances SIM123456789
38
+ bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts positions SIM123456789
39
+ bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts bars AAPL 20
40
+ bun /home/node/.pi/agent/skills/tradestation/scripts/ts-cli.ts bars '%40ES' 10
41
+ ```
42
+
43
+ Use a URL-encoded symbol for futures (e.g. `@ES` as `%40ES`) or pass `@ES` quoted so the shell does not expand it.
44
+
45
+ ## Orders — two-step confirmation (`mrctl`)
46
+
47
+ Order placement runs on the **Mercury host** (not inside `ts-cli`) so the flow can be audited and gated. Use **`mrctl`** from the agent container (same headers as other `mrctl` commands).
48
+
49
+ 1. **Propose** (no `--confirm`): calls TradeStation `orderconfirm`, stores a pending id (~15 minutes), returns a summary and `pendingId`.
50
+ 2. **Human check**: show the summary on **any** chat platform. The user can confirm with plain text, e.g. `CONFIRM <pendingId>`, or tell you to run step 3.
51
+ 3. **Execute**: same flags as step 1 plus `--confirm --pending-id <pendingId>`.
52
+
53
+ Example (SIM account):
54
+
55
+ ```bash
56
+ mrctl tradestation order --account SIM123456789 --symbol AAPL --quantity 1 --action SELL --type Market --duration DAY
57
+ # … user approves …
58
+ mrctl tradestation order --account SIM123456789 --symbol AAPL --quantity 1 --action SELL --type Market --duration DAY --confirm --pending-id '<uuid-from-step-1>'
59
+ ```
60
+
61
+ Optional: `--route Intelligent` (default), `--limit-price`, `--stop-price`, `--expiration-date` (for time-in-force), `--type` (default `Market`).
62
+
63
+ **Rules**: Do not skip human confirmation. Do not place live orders unless the user explicitly wants that and `MERCURY_TS_ALLOW_LIVE_ORDERS` is enabled on the host.
64
+
65
+ ## Scope
66
+
67
+ Read helpers: **accounts**, **balances**, **positions**, **bars**. Orders: **host-side** via `mrctl tradestation order` only. Not a full API mirror; no streaming in-container.
@@ -1,94 +1,94 @@
1
- import { mkdirSync, statSync, writeFileSync } from "node:fs";
2
- import path from "node:path";
3
- import type {
4
- MercuryExtensionAPI,
5
- MercuryExtensionContext,
6
- } from "mercury-agent/extensions/types";
7
- import type { MercuryTtsConfig } from "mercury-agent/tts";
8
- import { synthesizeSpeech } from "mercury-agent/tts";
9
- import type { EgressFile } from "mercury-agent/types";
10
-
11
- const EXT = "voice-synth";
12
-
13
- /** Prefer `voice-synth.mode`; fall back to legacy `voice-synth.auto`. */
14
- function readVoiceSynthMode(
15
- ctx: MercuryExtensionContext,
16
- spaceId: string,
17
- ): "on_demand" | "auto" {
18
- const mode = ctx.db.getSpaceConfig(spaceId, `${EXT}.mode`);
19
- if (mode === "auto" || mode === "on_demand") return mode;
20
- const legacy = ctx.db.getSpaceConfig(spaceId, `${EXT}.auto`);
21
- return legacy === "true" ? "auto" : "on_demand";
22
- }
23
-
24
- function toTtsConfig(ctx: MercuryExtensionContext): MercuryTtsConfig {
25
- const c = ctx.config;
26
- return {
27
- ttsProvider: c.ttsProvider,
28
- azureSpeechKey: c.azureSpeechKey,
29
- azureSpeechRegion: c.azureSpeechRegion,
30
- googleApplicationCredentials: c.googleApplicationCredentials,
31
- ttsMaxChars: c.ttsMaxChars,
32
- };
33
- }
34
-
35
- export default function setup(mercury: MercuryExtensionAPI) {
36
- mercury.config("mode", {
37
- description:
38
- "on_demand: TTS only when the agent runs `mrctl tts synthesize` (default). auto: attach a TTS MP3 to every assistant reply.",
39
- default: "on_demand",
40
- validate: (v) => v === "on_demand" || v === "auto",
41
- });
42
-
43
- mercury.config("auto", {
44
- description:
45
- "Legacy: prefer voice-synth.mode. Used only when mode is unset; true equals mode=auto.",
46
- default: "false",
47
- validate: (v) => v === "true" || v === "false",
48
- });
49
-
50
- mercury.skill("./skill");
51
-
52
- mercury.on("after_container", async (event, ctx) => {
53
- if (readVoiceSynthMode(ctx, event.spaceId) !== "auto") return undefined;
54
- if (!event.reply?.trim()) return undefined;
55
- if (event.error) return undefined;
56
-
57
- if (
58
- !ctx.hasCallerPermission(event.spaceId, event.callerId, "tts.synthesize")
59
- ) {
60
- ctx.log.warn("voice-synth.auto skipped: caller lacks tts.synthesize", {
61
- extension: EXT,
62
- spaceId: event.spaceId,
63
- });
64
- return undefined;
65
- }
66
-
67
- const outDir = path.join(event.workspace, "outbox");
68
- mkdirSync(outDir, { recursive: true });
69
- const filename = `tts-${Date.now()}.mp3`;
70
- const absPath = path.join(outDir, filename);
71
-
72
- try {
73
- const { buffer } = await synthesizeSpeech(toTtsConfig(ctx), {
74
- text: event.reply,
75
- language: "auto",
76
- });
77
- writeFileSync(absPath, buffer);
78
- const st = statSync(absPath);
79
- const file: EgressFile = {
80
- path: absPath,
81
- filename,
82
- mimeType: "audio/mpeg",
83
- sizeBytes: st.size,
84
- };
85
- return { files: [file] };
86
- } catch (e) {
87
- ctx.log.error("voice-synth TTS failed", {
88
- extension: EXT,
89
- error: e instanceof Error ? e.message : String(e),
90
- });
91
- return undefined;
92
- }
93
- });
94
- }
1
+ import { mkdirSync, statSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type {
4
+ MercuryExtensionAPI,
5
+ MercuryExtensionContext,
6
+ } from "mercury-agent/extensions/types";
7
+ import type { MercuryTtsConfig } from "mercury-agent/tts";
8
+ import { synthesizeSpeech } from "mercury-agent/tts";
9
+ import type { EgressFile } from "mercury-agent/types";
10
+
11
+ const EXT = "voice-synth";
12
+
13
+ /** Prefer `voice-synth.mode`; fall back to legacy `voice-synth.auto`. */
14
+ function readVoiceSynthMode(
15
+ ctx: MercuryExtensionContext,
16
+ spaceId: string,
17
+ ): "on_demand" | "auto" {
18
+ const mode = ctx.db.getSpaceConfig(spaceId, `${EXT}.mode`);
19
+ if (mode === "auto" || mode === "on_demand") return mode;
20
+ const legacy = ctx.db.getSpaceConfig(spaceId, `${EXT}.auto`);
21
+ return legacy === "true" ? "auto" : "on_demand";
22
+ }
23
+
24
+ function toTtsConfig(ctx: MercuryExtensionContext): MercuryTtsConfig {
25
+ const c = ctx.config;
26
+ return {
27
+ ttsProvider: c.ttsProvider,
28
+ azureSpeechKey: c.azureSpeechKey,
29
+ azureSpeechRegion: c.azureSpeechRegion,
30
+ googleApplicationCredentials: c.googleApplicationCredentials,
31
+ ttsMaxChars: c.ttsMaxChars,
32
+ };
33
+ }
34
+
35
+ export default function setup(mercury: MercuryExtensionAPI) {
36
+ mercury.config("mode", {
37
+ description:
38
+ "on_demand: TTS only when the agent runs `mrctl tts synthesize` (default). auto: attach a TTS MP3 to every assistant reply.",
39
+ default: "on_demand",
40
+ validate: (v) => v === "on_demand" || v === "auto",
41
+ });
42
+
43
+ mercury.config("auto", {
44
+ description:
45
+ "Legacy: prefer voice-synth.mode. Used only when mode is unset; true equals mode=auto.",
46
+ default: "false",
47
+ validate: (v) => v === "true" || v === "false",
48
+ });
49
+
50
+ mercury.skill("./skill");
51
+
52
+ mercury.on("after_container", async (event, ctx) => {
53
+ if (readVoiceSynthMode(ctx, event.spaceId) !== "auto") return undefined;
54
+ if (!event.reply?.trim()) return undefined;
55
+ if (event.error) return undefined;
56
+
57
+ if (
58
+ !ctx.hasCallerPermission(event.spaceId, event.callerId, "tts.synthesize")
59
+ ) {
60
+ ctx.log.warn("voice-synth.auto skipped: caller lacks tts.synthesize", {
61
+ extension: EXT,
62
+ spaceId: event.spaceId,
63
+ });
64
+ return undefined;
65
+ }
66
+
67
+ const outDir = path.join(event.workspace, "outbox");
68
+ mkdirSync(outDir, { recursive: true });
69
+ const filename = `tts-${Date.now()}.mp3`;
70
+ const absPath = path.join(outDir, filename);
71
+
72
+ try {
73
+ const { buffer } = await synthesizeSpeech(toTtsConfig(ctx), {
74
+ text: event.reply,
75
+ language: "auto",
76
+ });
77
+ writeFileSync(absPath, buffer);
78
+ const st = statSync(absPath);
79
+ const file: EgressFile = {
80
+ path: absPath,
81
+ filename,
82
+ mimeType: "audio/mpeg",
83
+ sizeBytes: st.size,
84
+ };
85
+ return { files: [file] };
86
+ } catch (e) {
87
+ ctx.log.error("voice-synth TTS failed", {
88
+ extension: EXT,
89
+ error: e instanceof Error ? e.message : String(e),
90
+ });
91
+ return undefined;
92
+ }
93
+ });
94
+ }