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.
- package/README.md +452 -451
- package/container/Dockerfile +127 -127
- package/container/Dockerfile.base +109 -109
- package/container/Dockerfile.power +17 -17
- package/container/agent-package.json +8 -8
- package/container/build.sh +54 -54
- package/docs/ROADMAP.md +3 -4
- package/docs/archive/summarization/2026-07-02-recent-dev-summary.md +71 -0
- package/docs/auth/dashboard.md +28 -28
- package/docs/auth/overview.md +109 -109
- package/docs/auth/whatsapp.md +173 -173
- package/docs/authoring-profiles.md +174 -174
- package/docs/configuration.md +54 -54
- package/docs/container-lifecycle.md +349 -349
- package/docs/{bugs/bwrap-privileged-linux-docker.md → debug/major/2026-07-02-bwrap-privileged-linux-docker.md} +21 -1
- package/docs/{bugs/e2big-prompt-delivery-fix.md → debug/major/2026-07-02-e2big-prompt-delivery-fix.md} +27 -1
- package/docs/{bugs/registry-pull-no-local-fallback.md → debug/major/2026-07-02-registry-pull-no-local-fallback.md} +27 -1
- package/docs/debug/summarization/2026-07-02-common-bug-patterns.md +72 -0
- package/docs/deployment.md +199 -199
- package/docs/extensions.md +375 -375
- package/docs/graceful-shutdown.md +62 -62
- package/docs/kb-distillation.md +77 -77
- package/docs/media/overview.md +140 -140
- package/docs/media/whatsapp.md +171 -171
- package/docs/memory.md +137 -137
- package/docs/permissions.md +217 -217
- package/docs/pipeline.md +228 -228
- package/docs/prd-chat-memory.md +76 -76
- package/docs/prd-config-load.md +82 -82
- package/docs/rate-limiting.md +229 -229
- package/docs/runbooks/publish-checklist.md +9 -1
- package/docs/scheduler.md +288 -288
- package/docs/setup-discord.md +100 -100
- package/docs/setup-slack.md +119 -119
- package/docs/setup-whatsapp.md +94 -94
- package/docs/subagents.md +166 -166
- package/docs/web-search.md +62 -62
- package/examples/extensions/README.md +12 -12
- package/examples/extensions/charts/index.ts +13 -13
- package/examples/extensions/charts/skill/SKILL.md +98 -98
- package/examples/extensions/gws/README.md +52 -52
- package/examples/extensions/gws/skill/SKILL.md +57 -57
- package/examples/extensions/gws/skill/references/calendar.md +101 -101
- package/examples/extensions/gws/skill/references/docs.md +65 -65
- package/examples/extensions/gws/skill/references/drive.md +79 -79
- package/examples/extensions/gws/skill/references/gmail.md +85 -85
- package/examples/extensions/gws/skill/references/sheets.md +60 -60
- package/examples/extensions/napkin/skill/SKILL.md +728 -728
- package/examples/extensions/pdf/skill/LICENSE.txt +30 -30
- package/examples/extensions/pdf/skill/SKILL.md +314 -314
- package/examples/extensions/pdf/skill/forms.md +294 -294
- package/examples/extensions/pdf/skill/reference.md +611 -611
- package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -65
- package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -11
- package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -33
- package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -37
- package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -122
- package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -115
- package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -98
- package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -107
- package/examples/extensions/permission-guard/index.ts +65 -65
- package/examples/extensions/pinchtab/skill/SKILL.md +224 -224
- package/examples/extensions/pinchtab/skill/TRUST.md +69 -69
- package/examples/extensions/pinchtab/skill/references/api.md +297 -297
- package/examples/extensions/pinchtab/skill/references/env.md +45 -45
- package/examples/extensions/pinchtab/skill/references/profiles.md +107 -107
- package/examples/extensions/tradestation/host/refresh.ts +102 -102
- package/examples/extensions/tradestation/index.ts +153 -153
- package/examples/extensions/tradestation/skill/SKILL.md +67 -67
- package/examples/extensions/voice-synth/index.ts +94 -94
- package/examples/extensions/voice-synth/skill/SKILL.md +38 -38
- package/examples/extensions/voice-transcribe/requirements.txt +8 -8
- package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -179
- package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -53
- package/examples/extensions/yahoo-mail/cli/package.json +13 -13
- package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -78
- package/package.json +106 -106
- package/resources/agents/explore.md +50 -50
- package/resources/agents/worker.md +24 -24
- package/resources/connection-env-vars.json +25 -25
- package/resources/pi-extensions/subagent/agents.ts +126 -126
- package/resources/pi-extensions/subagent/index.ts +964 -964
- package/resources/profiles/coding/AGENTS.md +43 -43
- package/resources/profiles/coding/mercury-profile.yaml +15 -15
- package/resources/profiles/general/AGENTS.md +31 -31
- package/resources/profiles/general/mercury-profile.yaml +15 -15
- package/resources/profiles/research/AGENTS.md +40 -40
- package/resources/profiles/research/mercury-profile.yaml +15 -15
- package/resources/skills/config/SKILL.md +25 -25
- package/resources/skills/context/SKILL.md +33 -33
- package/resources/skills/conversation-recap/SKILL.md +19 -19
- package/resources/skills/mutes/SKILL.md +31 -31
- package/resources/skills/permissions/SKILL.md +19 -19
- package/resources/skills/preferences/SKILL.md +31 -31
- package/resources/skills/recall/SKILL.md +24 -24
- package/resources/skills/roles/SKILL.md +18 -18
- package/resources/skills/spaces/SKILL.md +18 -18
- package/resources/skills/tasks/SKILL.md +45 -45
- package/resources/templates/AGENTS.md +157 -157
- package/resources/templates/env.template +38 -38
- package/resources/templates/mercury.example.yaml +99 -99
- package/src/agent/container-entry.ts +1 -1
- package/src/agent/container-runner.ts +1345 -1346
- package/src/cli/mercury.ts +39 -7
- package/src/cli/mrctl.ts +636 -636
- package/src/config-file.ts +540 -540
- package/src/config.ts +339 -339
- package/src/core/api.ts +125 -125
- package/src/core/caller-token.ts +101 -101
- package/src/core/permissions.ts +228 -228
- package/src/core/profiles.ts +271 -271
- package/src/core/routes/capability.ts +70 -70
- package/src/core/routes/index.ts +15 -15
- package/src/core/runtime.ts +1530 -1530
- package/src/dashboard/index.html +729 -729
- package/src/extensions/api.ts +273 -273
- package/src/extensions/loader.ts +286 -286
- package/src/extensions/types.ts +517 -517
- package/src/main.ts +605 -605
- package/docs/pending-updates/applicative-profiles.md +0 -15
- package/docs/pending-updates/caller-bound-capability-token.md +0 -15
- package/docs/pending-updates/dm-auto-space.md +0 -15
- /package/docs/archive/{2026-07-01-applicative-profiles.md → applicative-profiles/2026-07-01-applicative-profiles.md} +0 -0
- /package/docs/archive/{2026-07-01-caller-bound-capability-token.md → applicative-profiles/2026-07-01-caller-bound-capability-token.md} +0 -0
- /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
|
+
}
|