mercury-agent 0.4.5
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/LICENSE +22 -0
- package/README.md +438 -0
- package/container/Dockerfile +127 -0
- package/container/Dockerfile.base +109 -0
- package/container/Dockerfile.power +17 -0
- package/container/agent-package.json +8 -0
- package/container/build.sh +54 -0
- package/docs/TODOS.md +147 -0
- package/docs/auth/dashboard.md +28 -0
- package/docs/auth/overview.md +109 -0
- package/docs/auth/whatsapp.md +173 -0
- package/docs/configuration.md +54 -0
- package/docs/container-lifecycle.md +349 -0
- package/docs/context-architecture.md +87 -0
- package/docs/deployment.md +199 -0
- package/docs/extensions.md +375 -0
- package/docs/graceful-shutdown.md +62 -0
- package/docs/kb-distillation.md +77 -0
- package/docs/media/overview.md +140 -0
- package/docs/media/whatsapp.md +171 -0
- package/docs/memory.md +137 -0
- package/docs/permissions.md +217 -0
- package/docs/pipeline.md +228 -0
- package/docs/prd-chat-memory.md +76 -0
- package/docs/prd-config-load.md +82 -0
- package/docs/rate-limiting.md +166 -0
- package/docs/scheduler.md +288 -0
- package/docs/setup-discord.md +100 -0
- package/docs/setup-slack.md +119 -0
- package/docs/setup-whatsapp.md +94 -0
- package/docs/subagents.md +166 -0
- package/docs/web-search.md +62 -0
- package/examples/extensions/README.md +12 -0
- package/examples/extensions/charts/index.ts +13 -0
- package/examples/extensions/charts/skill/SKILL.md +98 -0
- package/examples/extensions/gws/README.md +52 -0
- package/examples/extensions/gws/index.ts +106 -0
- package/examples/extensions/gws/skill/SKILL.md +57 -0
- package/examples/extensions/gws/skill/references/calendar.md +101 -0
- package/examples/extensions/gws/skill/references/docs.md +65 -0
- package/examples/extensions/gws/skill/references/drive.md +79 -0
- package/examples/extensions/gws/skill/references/gmail.md +85 -0
- package/examples/extensions/gws/skill/references/sheets.md +60 -0
- package/examples/extensions/napkin/index.ts +821 -0
- package/examples/extensions/napkin/prompts/consolidation-monthly.md +73 -0
- package/examples/extensions/napkin/prompts/consolidation-weekly.md +67 -0
- package/examples/extensions/napkin/prompts/kb-distillation.md +176 -0
- package/examples/extensions/napkin/skill/SKILL.md +728 -0
- package/examples/extensions/pdf/index.ts +23 -0
- package/examples/extensions/pdf/skill/LICENSE.txt +30 -0
- package/examples/extensions/pdf/skill/SKILL.md +314 -0
- package/examples/extensions/pdf/skill/forms.md +294 -0
- package/examples/extensions/pdf/skill/reference.md +612 -0
- package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -0
- package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -0
- package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -0
- package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -0
- package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -0
- package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -0
- package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -0
- package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -0
- package/examples/extensions/permission-guard/index.ts +65 -0
- package/examples/extensions/pinchtab/index.ts +199 -0
- package/examples/extensions/pinchtab/lib/session-injector.ts +144 -0
- package/examples/extensions/pinchtab/skill/SKILL.md +224 -0
- package/examples/extensions/pinchtab/skill/TRUST.md +69 -0
- package/examples/extensions/pinchtab/skill/references/api.md +297 -0
- package/examples/extensions/pinchtab/skill/references/env.md +45 -0
- package/examples/extensions/pinchtab/skill/references/profiles.md +107 -0
- package/examples/extensions/tradestation/host/refresh.ts +102 -0
- package/examples/extensions/tradestation/index.ts +153 -0
- package/examples/extensions/tradestation/skill/SKILL.md +67 -0
- package/examples/extensions/tradestation/skill/scripts/ts-cli.ts +111 -0
- package/examples/extensions/voice-synth/index.ts +94 -0
- package/examples/extensions/voice-synth/skill/SKILL.md +38 -0
- package/examples/extensions/voice-transcribe/index.ts +381 -0
- package/examples/extensions/voice-transcribe/requirements.txt +8 -0
- package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -0
- package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -0
- package/examples/extensions/web-search/index.ts +22 -0
- package/examples/extensions/web-search/skill/SKILL.md +114 -0
- package/examples/extensions/web-search/skill/references/apartments.md +178 -0
- package/examples/extensions/web-search/skill/references/car-purchase.md +132 -0
- package/examples/extensions/web-search/skill/references/car-rental.md +113 -0
- package/examples/extensions/web-search/skill/references/flights.md +133 -0
- package/examples/extensions/web-search/skill/references/hotels.md +148 -0
- package/examples/extensions/yahoo-mail/cli/bun.lock +66 -0
- package/examples/extensions/yahoo-mail/cli/package.json +13 -0
- package/examples/extensions/yahoo-mail/cli/ymail.mjs +353 -0
- package/examples/extensions/yahoo-mail/index.ts +57 -0
- package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -0
- package/package.json +106 -0
- package/resources/agents/explore.md +50 -0
- package/resources/agents/worker.md +24 -0
- package/resources/builtin-extensions.txt +3 -0
- package/resources/connection-env-vars.json +25 -0
- package/resources/extensions/.gitkeep +0 -0
- package/resources/pi-extensions/subagent/agents.ts +126 -0
- package/resources/pi-extensions/subagent/index.ts +964 -0
- package/resources/profiles/coding/AGENTS.md +43 -0
- package/resources/profiles/coding/mercury-profile.yaml +15 -0
- package/resources/profiles/general/AGENTS.md +31 -0
- package/resources/profiles/general/mercury-profile.yaml +15 -0
- package/resources/profiles/research/AGENTS.md +40 -0
- package/resources/profiles/research/mercury-profile.yaml +15 -0
- package/resources/skills/config/SKILL.md +25 -0
- package/resources/skills/context/SKILL.md +33 -0
- package/resources/skills/conversation-recap/SKILL.md +19 -0
- package/resources/skills/media/SKILL.md +27 -0
- package/resources/skills/mutes/SKILL.md +31 -0
- package/resources/skills/permissions/SKILL.md +19 -0
- package/resources/skills/preferences/SKILL.md +31 -0
- package/resources/skills/recall/SKILL.md +24 -0
- package/resources/skills/roles/SKILL.md +18 -0
- package/resources/skills/spaces/SKILL.md +18 -0
- package/resources/skills/tasks/SKILL.md +45 -0
- package/resources/templates/AGENTS.md +157 -0
- package/resources/templates/env.template +34 -0
- package/resources/templates/mercury.example.yaml +75 -0
- package/src/adapters/discord-native.ts +534 -0
- package/src/adapters/discord.ts +38 -0
- package/src/adapters/setup.ts +89 -0
- package/src/adapters/slack.ts +9 -0
- package/src/adapters/whatsapp-media.ts +337 -0
- package/src/adapters/whatsapp.ts +629 -0
- package/src/agent/api-socket.ts +127 -0
- package/src/agent/container-entry.ts +967 -0
- package/src/agent/container-error.ts +49 -0
- package/src/agent/container-runner.ts +1272 -0
- package/src/agent/model-capabilities-core.ts +23 -0
- package/src/agent/model-capabilities.ts +231 -0
- package/src/agent/pi-failure-class.ts +83 -0
- package/src/agent/pi-jsonl-parser.ts +306 -0
- package/src/agent/preferences-prompt.ts +20 -0
- package/src/agent/user-error-messages.ts +78 -0
- package/src/bridges/discord.ts +171 -0
- package/src/bridges/slack.ts +177 -0
- package/src/bridges/teams.ts +160 -0
- package/src/bridges/telegram.ts +571 -0
- package/src/bridges/whatsapp.ts +290 -0
- package/src/chat-shim.ts +259 -0
- package/src/cli/mercury.ts +2508 -0
- package/src/cli/mrctl-http.ts +27 -0
- package/src/cli/mrctl.ts +611 -0
- package/src/cli/whatsapp-auth.ts +260 -0
- package/src/config-file.ts +397 -0
- package/src/config-model-chain.ts +30 -0
- package/src/config.ts +316 -0
- package/src/core/api-types.ts +58 -0
- package/src/core/api.ts +105 -0
- package/src/core/commands.ts +76 -0
- package/src/core/conversation.ts +47 -0
- package/src/core/handler.ts +206 -0
- package/src/core/media.ts +200 -0
- package/src/core/mute-duration.ts +22 -0
- package/src/core/outbox.ts +76 -0
- package/src/core/permissions.ts +192 -0
- package/src/core/profiles.ts +245 -0
- package/src/core/rate-limiter.ts +127 -0
- package/src/core/router.ts +191 -0
- package/src/core/routes/chat.ts +172 -0
- package/src/core/routes/config-builtin.ts +107 -0
- package/src/core/routes/config.ts +81 -0
- package/src/core/routes/connections.ts +190 -0
- package/src/core/routes/console.ts +668 -0
- package/src/core/routes/control.ts +46 -0
- package/src/core/routes/conversations.ts +66 -0
- package/src/core/routes/dashboard.ts +2491 -0
- package/src/core/routes/extensions.ts +37 -0
- package/src/core/routes/index.ts +14 -0
- package/src/core/routes/media.ts +72 -0
- package/src/core/routes/messages.ts +37 -0
- package/src/core/routes/mutes.ts +89 -0
- package/src/core/routes/prefs.ts +95 -0
- package/src/core/routes/roles.ts +125 -0
- package/src/core/routes/spaces.ts +60 -0
- package/src/core/routes/storage.ts +126 -0
- package/src/core/routes/tasks.ts +189 -0
- package/src/core/routes/tradestation.ts +268 -0
- package/src/core/routes/tts.ts +51 -0
- package/src/core/runtime.ts +1140 -0
- package/src/core/space-queue.ts +103 -0
- package/src/core/storage-cleanup.ts +140 -0
- package/src/core/storage-guard.ts +24 -0
- package/src/core/task-scheduler.ts +132 -0
- package/src/core/telegram-format.ts +178 -0
- package/src/core/trigger.ts +142 -0
- package/src/dashboard/index.html +729 -0
- package/src/dashboard/tokens.css +53 -0
- package/src/extensions/api.ts +252 -0
- package/src/extensions/catalog.ts +117 -0
- package/src/extensions/config-registry.ts +83 -0
- package/src/extensions/context.ts +36 -0
- package/src/extensions/hooks.ts +156 -0
- package/src/extensions/image-builder.ts +617 -0
- package/src/extensions/installer.ts +306 -0
- package/src/extensions/jobs.ts +122 -0
- package/src/extensions/loader.ts +271 -0
- package/src/extensions/permission-guard.ts +52 -0
- package/src/extensions/reserved.ts +28 -0
- package/src/extensions/skills.ts +123 -0
- package/src/extensions/types.ts +462 -0
- package/src/logger.ts +174 -0
- package/src/main.ts +586 -0
- package/src/server.ts +391 -0
- package/src/storage/db.ts +1624 -0
- package/src/storage/memory.ts +45 -0
- package/src/storage/pi-auth.ts +95 -0
- package/src/text/markdown.ts +117 -0
- package/src/text/rtl.ts +38 -0
- package/src/tradestation/host-api.ts +77 -0
- package/src/tradestation/pending-orders.ts +69 -0
- package/src/tts/azure.ts +52 -0
- package/src/tts/google.ts +128 -0
- package/src/tts/index.ts +8 -0
- package/src/tts/language.ts +20 -0
- package/src/tts/synthesize.ts +133 -0
- package/src/types.ts +295 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Minimal TradeStation v3 CLI for the Mercury agent container.
|
|
4
|
+
* Uses TRADESTATION_ACCESS_TOKEN and TRADESTATION_API_BASE from the host hook.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const token = process.env.TRADESTATION_ACCESS_TOKEN;
|
|
8
|
+
const authErr = process.env.TRADESTATION_AUTH_ERROR;
|
|
9
|
+
const base = (
|
|
10
|
+
process.env.TRADESTATION_API_BASE || "https://api.tradestation.com/v3"
|
|
11
|
+
).replace(/\/$/, "");
|
|
12
|
+
|
|
13
|
+
function usage(): never {
|
|
14
|
+
console.error(`Usage:
|
|
15
|
+
ts-cli.ts accounts
|
|
16
|
+
ts-cli.ts balances <accountKey>
|
|
17
|
+
ts-cli.ts positions <accountKey>
|
|
18
|
+
ts-cli.ts bars <symbol> [barsback]
|
|
19
|
+
|
|
20
|
+
Environment: TRADESTATION_ACCESS_TOKEN (and optional TRADESTATION_API_BASE)`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function apiGet(
|
|
25
|
+
path: string,
|
|
26
|
+
query?: Record<string, string>,
|
|
27
|
+
): Promise<unknown> {
|
|
28
|
+
const url = new URL(path.replace(/^\//, ""), `${base}/`);
|
|
29
|
+
if (query) {
|
|
30
|
+
for (const [k, v] of Object.entries(query)) {
|
|
31
|
+
if (v !== undefined && v !== "") url.searchParams.set(k, v);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const res = await fetch(url.toString(), {
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: `Bearer ${token}`,
|
|
37
|
+
Accept: "application/json",
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const text = await res.text();
|
|
41
|
+
let body: unknown;
|
|
42
|
+
try {
|
|
43
|
+
body = text ? JSON.parse(text) : null;
|
|
44
|
+
} catch {
|
|
45
|
+
body = text;
|
|
46
|
+
}
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
console.error(res.status, body);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
return body;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function main() {
|
|
55
|
+
const [, , cmd, ...rest] = process.argv;
|
|
56
|
+
if (!cmd) usage();
|
|
57
|
+
|
|
58
|
+
if (authErr) {
|
|
59
|
+
console.error("TradeStation auth error:", authErr);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
if (!token) {
|
|
63
|
+
console.error(
|
|
64
|
+
"Missing TRADESTATION_ACCESS_TOKEN. This TradeStation integration is admin-only; check Mercury host configuration.",
|
|
65
|
+
);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
switch (cmd) {
|
|
70
|
+
case "accounts": {
|
|
71
|
+
const data = await apiGet("/brokerage/accounts");
|
|
72
|
+
console.log(JSON.stringify(data, null, 2));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case "balances": {
|
|
76
|
+
const accountKey = rest[0];
|
|
77
|
+
if (!accountKey) usage();
|
|
78
|
+
const data = await apiGet(
|
|
79
|
+
`/brokerage/accounts/${encodeURIComponent(accountKey)}/balances`,
|
|
80
|
+
);
|
|
81
|
+
console.log(JSON.stringify(data, null, 2));
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "positions": {
|
|
85
|
+
const accountKey = rest[0];
|
|
86
|
+
if (!accountKey) usage();
|
|
87
|
+
const data = await apiGet(
|
|
88
|
+
`/brokerage/accounts/${encodeURIComponent(accountKey)}/positions`,
|
|
89
|
+
);
|
|
90
|
+
console.log(JSON.stringify(data, null, 2));
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "bars": {
|
|
94
|
+
const symbol = rest[0];
|
|
95
|
+
if (!symbol) usage();
|
|
96
|
+
const barsback = rest[1] ?? "20";
|
|
97
|
+
const data = await apiGet(
|
|
98
|
+
`/marketdata/barcharts/${encodeURIComponent(symbol)}`,
|
|
99
|
+
{
|
|
100
|
+
barsback,
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
console.log(JSON.stringify(data, null, 2));
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
default:
|
|
107
|
+
usage();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await main();
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: voice-synth
|
|
3
|
+
description: Cloud text-to-speech (Google or Azure) for English and Hebrew — on-demand via mrctl or optional auto voice attachments per space.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Voice synthesis (TTS)
|
|
7
|
+
|
|
8
|
+
**`mrctl tts synthesize` is available in this environment.** When the user asks for a voice message, audio reply, or TTS, you MUST run the command below. Do not assume it is unavailable — always try it and report any error verbatim.
|
|
9
|
+
|
|
10
|
+
**Do not paste** `mrctl`, shell snippets, or JSON tool-call blobs into your **visible** reply — use the **bash tool** only for the command. The user sees normal text plus the audio attachment.
|
|
11
|
+
|
|
12
|
+
## How to send a voice message
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
mrctl tts synthesize --text "Your spoken reply here" --out outbox/reply.mp3
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This writes an MP3 file to `outbox/` which Mercury automatically attaches to your reply. Do **not** create `.txt` files or use misleading names — chat apps decide how to render attachments from the file extension and MIME type.
|
|
19
|
+
|
|
20
|
+
Optional flags:
|
|
21
|
+
|
|
22
|
+
- `--language` — `auto` (default), `he-IL`, or `en-US`. `auto` picks Hebrew if the text contains Hebrew script.
|
|
23
|
+
- `--provider` — `google`, `azure`, or `auto` (host default).
|
|
24
|
+
|
|
25
|
+
Requires the caller to have **`tts.synthesize`** permission (admins have it by default).
|
|
26
|
+
|
|
27
|
+
### Telegram / WhatsApp delivery
|
|
28
|
+
|
|
29
|
+
**Telegram** uses `sendAudio` for MP3 (in-chat player) and `sendVoice` for OGG voice notes. **WhatsApp** treats audio as a voice note when the filename matches **`voice-*.ogg`** (case-insensitive); otherwise it sends as normal audio (`ptt: false`).
|
|
30
|
+
|
|
31
|
+
## Automatic mode (optional)
|
|
32
|
+
|
|
33
|
+
| Setting | Behavior |
|
|
34
|
+
|--------|----------|
|
|
35
|
+
| **`voice-synth.mode=on_demand`** (default) | TTS runs only when you call `mrctl tts synthesize`. |
|
|
36
|
+
| **`voice-synth.mode=auto`** | The host attaches a TTS MP3 to every assistant reply automatically. |
|
|
37
|
+
|
|
38
|
+
Set with `mrctl config set voice-synth.mode on_demand` or `auto`.
|