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,189 @@
|
|
|
1
|
+
import { CronExpressionParser } from "cron-parser";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { logger } from "../../logger.js";
|
|
4
|
+
import { checkPerm, type Env, getApiCtx, getAuth } from "../api-types.js";
|
|
5
|
+
|
|
6
|
+
export const tasks = new Hono<Env>();
|
|
7
|
+
|
|
8
|
+
tasks.get("/", (c) => {
|
|
9
|
+
const { spaceId } = getAuth(c);
|
|
10
|
+
const denied = checkPerm(c, "tasks.list");
|
|
11
|
+
if (denied) return denied;
|
|
12
|
+
|
|
13
|
+
const { db } = getApiCtx(c);
|
|
14
|
+
const taskList = db.listTasks(spaceId);
|
|
15
|
+
return c.json({ tasks: taskList });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
tasks.post("/", async (c) => {
|
|
19
|
+
const { spaceId, callerId } = getAuth(c);
|
|
20
|
+
const denied = checkPerm(c, "tasks.create");
|
|
21
|
+
if (denied) return denied;
|
|
22
|
+
|
|
23
|
+
const { db, config } = getApiCtx(c);
|
|
24
|
+
const body = await c.req.json<{
|
|
25
|
+
cron?: string;
|
|
26
|
+
at?: string;
|
|
27
|
+
prompt?: string;
|
|
28
|
+
silent?: boolean;
|
|
29
|
+
timezone?: string;
|
|
30
|
+
}>();
|
|
31
|
+
|
|
32
|
+
if (!body.prompt) {
|
|
33
|
+
return c.json({ error: "Missing prompt" }, 400);
|
|
34
|
+
}
|
|
35
|
+
if (!body.cron && !body.at) {
|
|
36
|
+
return c.json({ error: "Missing cron or at" }, 400);
|
|
37
|
+
}
|
|
38
|
+
if (body.cron && body.at) {
|
|
39
|
+
return c.json({ error: "Cannot specify both cron and at" }, 400);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const rawTimezone = body.timezone || config.defaultTimezone;
|
|
43
|
+
let timezone: string | undefined;
|
|
44
|
+
if (rawTimezone) {
|
|
45
|
+
try {
|
|
46
|
+
if (!Intl.supportedValuesOf("timeZone").includes(rawTimezone)) {
|
|
47
|
+
return c.json({ error: "Invalid timezone identifier" }, 400);
|
|
48
|
+
}
|
|
49
|
+
timezone = rawTimezone;
|
|
50
|
+
} catch {
|
|
51
|
+
return c.json({ error: "Invalid timezone identifier" }, 400);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const silent = body.silent ?? false;
|
|
56
|
+
let nextRunAt: number;
|
|
57
|
+
let schedule: { cron: string } | { at: string };
|
|
58
|
+
|
|
59
|
+
if (body.cron) {
|
|
60
|
+
try {
|
|
61
|
+
const interval = CronExpressionParser.parse(body.cron, {
|
|
62
|
+
currentDate: new Date(),
|
|
63
|
+
tz: timezone ?? "UTC",
|
|
64
|
+
});
|
|
65
|
+
nextRunAt = interval.next().getTime();
|
|
66
|
+
schedule = { cron: body.cron };
|
|
67
|
+
} catch {
|
|
68
|
+
return c.json({ error: "Invalid cron expression" }, 400);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
const atStr = body.at as string;
|
|
72
|
+
const atTime = new Date(atStr).getTime();
|
|
73
|
+
if (Number.isNaN(atTime)) {
|
|
74
|
+
return c.json({ error: "Invalid at timestamp" }, 400);
|
|
75
|
+
}
|
|
76
|
+
if (atTime <= Date.now()) {
|
|
77
|
+
return c.json({ error: "at timestamp must be in the future" }, 400);
|
|
78
|
+
}
|
|
79
|
+
nextRunAt = atTime;
|
|
80
|
+
schedule = { at: atStr };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const id = db.createTask(
|
|
84
|
+
spaceId,
|
|
85
|
+
schedule,
|
|
86
|
+
body.prompt,
|
|
87
|
+
nextRunAt,
|
|
88
|
+
callerId,
|
|
89
|
+
silent,
|
|
90
|
+
timezone,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return c.json({
|
|
94
|
+
id,
|
|
95
|
+
cron: body.cron ?? null,
|
|
96
|
+
at: body.at ?? null,
|
|
97
|
+
prompt: body.prompt,
|
|
98
|
+
silent,
|
|
99
|
+
timezone: timezone ?? null,
|
|
100
|
+
nextRunAt,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
tasks.post("/:id/pause", (c) => {
|
|
105
|
+
const { spaceId } = getAuth(c);
|
|
106
|
+
const denied = checkPerm(c, "tasks.pause");
|
|
107
|
+
if (denied) return denied;
|
|
108
|
+
|
|
109
|
+
const { db } = getApiCtx(c);
|
|
110
|
+
const taskId = Number(c.req.param("id"));
|
|
111
|
+
if (!Number.isFinite(taskId) || taskId < 1) {
|
|
112
|
+
return c.json({ error: "Invalid task ID" }, 400);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const task = db.getTask(taskId);
|
|
116
|
+
if (!task || task.spaceId !== spaceId) {
|
|
117
|
+
return c.json({ error: "Task not found" }, 404);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
db.setTaskActive(taskId, false);
|
|
121
|
+
return c.json({ id: taskId, active: false });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
tasks.post("/:id/resume", (c) => {
|
|
125
|
+
const { spaceId } = getAuth(c);
|
|
126
|
+
const denied = checkPerm(c, "tasks.resume");
|
|
127
|
+
if (denied) return denied;
|
|
128
|
+
|
|
129
|
+
const { db } = getApiCtx(c);
|
|
130
|
+
const taskId = Number(c.req.param("id"));
|
|
131
|
+
if (!Number.isFinite(taskId) || taskId < 1) {
|
|
132
|
+
return c.json({ error: "Invalid task ID" }, 400);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const task = db.getTask(taskId);
|
|
136
|
+
if (!task || task.spaceId !== spaceId) {
|
|
137
|
+
return c.json({ error: "Task not found" }, 404);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
db.setTaskActive(taskId, true);
|
|
141
|
+
return c.json({ id: taskId, active: true });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
tasks.post("/:id/run", (c) => {
|
|
145
|
+
const { spaceId } = getAuth(c);
|
|
146
|
+
const denied = checkPerm(c, "tasks.create");
|
|
147
|
+
if (denied) return denied;
|
|
148
|
+
|
|
149
|
+
const { db, scheduler } = getApiCtx(c);
|
|
150
|
+
const taskId = Number(c.req.param("id"));
|
|
151
|
+
if (!Number.isFinite(taskId) || taskId < 1) {
|
|
152
|
+
return c.json({ error: "Invalid task ID" }, 400);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const task = db.getTask(taskId);
|
|
156
|
+
if (!task || task.spaceId !== spaceId) {
|
|
157
|
+
return c.json({ error: "Task not found" }, 404);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!task.active) {
|
|
161
|
+
return c.json({ error: "Task is paused" }, 400);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Trigger async - don't wait for completion
|
|
165
|
+
scheduler.triggerTask(taskId).catch((err) => {
|
|
166
|
+
logger.error("Task trigger failed", { taskId, error: String(err) });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return c.json({ id: taskId, triggered: true });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
tasks.delete("/:id", (c) => {
|
|
173
|
+
const { spaceId } = getAuth(c);
|
|
174
|
+
const denied = checkPerm(c, "tasks.delete");
|
|
175
|
+
if (denied) return denied;
|
|
176
|
+
|
|
177
|
+
const { db } = getApiCtx(c);
|
|
178
|
+
const taskId = Number(c.req.param("id"));
|
|
179
|
+
if (!Number.isFinite(taskId) || taskId < 1) {
|
|
180
|
+
return c.json({ error: "Invalid task ID" }, 400);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const deleted = db.deleteTask(taskId, spaceId);
|
|
184
|
+
if (!deleted) {
|
|
185
|
+
return c.json({ error: "Task not found" }, 404);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return c.json({ id: taskId, deleted: true });
|
|
189
|
+
});
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { logger } from "../../logger.js";
|
|
4
|
+
import {
|
|
5
|
+
isLikelySimAccount,
|
|
6
|
+
tradeStationAuthorizedJson,
|
|
7
|
+
} from "../../tradestation/host-api.js";
|
|
8
|
+
import {
|
|
9
|
+
cleanupExpiredTradestationPending,
|
|
10
|
+
createPendingOrderId,
|
|
11
|
+
deletePendingOrder,
|
|
12
|
+
loadPendingOrder,
|
|
13
|
+
PENDING_ORDER_TTL_MS,
|
|
14
|
+
type PendingOrderRecord,
|
|
15
|
+
savePendingOrder,
|
|
16
|
+
type TradeStationOrderRequestJson,
|
|
17
|
+
} from "../../tradestation/pending-orders.js";
|
|
18
|
+
import { checkPerm, type Env, getApiCtx, getAuth } from "../api-types.js";
|
|
19
|
+
|
|
20
|
+
export const tradestation = new Hono<Env>();
|
|
21
|
+
|
|
22
|
+
const tradeActionSchema = z.enum([
|
|
23
|
+
"BUY",
|
|
24
|
+
"SELL",
|
|
25
|
+
"BUYTOCOVER",
|
|
26
|
+
"SELLSHORT",
|
|
27
|
+
"BUYTOOPEN",
|
|
28
|
+
"BUYTOCLOSE",
|
|
29
|
+
"SELLTOOPEN",
|
|
30
|
+
"SELLTOCLOSE",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const orderTypeSchema = z.enum(["Market", "Limit", "StopMarket", "StopLimit"]);
|
|
34
|
+
|
|
35
|
+
const orderBodySchema = z.object({
|
|
36
|
+
confirm: z.boolean().optional(),
|
|
37
|
+
pendingId: z.string().uuid().optional(),
|
|
38
|
+
accountKey: z.string().min(1),
|
|
39
|
+
symbol: z.string().min(1),
|
|
40
|
+
quantity: z.union([z.string(), z.number()]).transform((q) => String(q)),
|
|
41
|
+
tradeAction: tradeActionSchema,
|
|
42
|
+
orderType: orderTypeSchema.default("Market"),
|
|
43
|
+
/** Maps to TimeInForce.Duration (TradeStation v3). */
|
|
44
|
+
timeInForceDuration: z.string().min(1).default("DAY"),
|
|
45
|
+
timeInForceExpirationDate: z.string().optional(),
|
|
46
|
+
route: z.string().min(1).default("Intelligent"),
|
|
47
|
+
limitPrice: z.string().optional(),
|
|
48
|
+
stopPrice: z.string().optional(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function buildOrderRequest(
|
|
52
|
+
parsed: z.infer<typeof orderBodySchema>,
|
|
53
|
+
): TradeStationOrderRequestJson {
|
|
54
|
+
const tif: Record<string, string> = {
|
|
55
|
+
Duration: parsed.timeInForceDuration,
|
|
56
|
+
};
|
|
57
|
+
if (
|
|
58
|
+
parsed.timeInForceExpirationDate &&
|
|
59
|
+
parsed.timeInForceExpirationDate.trim() !== ""
|
|
60
|
+
) {
|
|
61
|
+
tif.ExpirationDate = parsed.timeInForceExpirationDate.trim();
|
|
62
|
+
}
|
|
63
|
+
const body: TradeStationOrderRequestJson = {
|
|
64
|
+
AccountID: parsed.accountKey.trim(),
|
|
65
|
+
Symbol: parsed.symbol.trim(),
|
|
66
|
+
Quantity: parsed.quantity,
|
|
67
|
+
OrderType: parsed.orderType,
|
|
68
|
+
TradeAction: parsed.tradeAction,
|
|
69
|
+
TimeInForce: tif,
|
|
70
|
+
Route: parsed.route.trim(),
|
|
71
|
+
};
|
|
72
|
+
if (parsed.limitPrice !== undefined && parsed.limitPrice.trim() !== "") {
|
|
73
|
+
body.LimitPrice = parsed.limitPrice.trim();
|
|
74
|
+
}
|
|
75
|
+
if (parsed.stopPrice !== undefined && parsed.stopPrice.trim() !== "") {
|
|
76
|
+
body.StopPrice = parsed.stopPrice.trim();
|
|
77
|
+
}
|
|
78
|
+
return body;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function liveOrderBlocked(accountKey: string, allowLive: boolean): boolean {
|
|
82
|
+
if (isLikelySimAccount(accountKey)) return false;
|
|
83
|
+
return !allowLive;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function orderRequestsMatch(
|
|
87
|
+
a: TradeStationOrderRequestJson,
|
|
88
|
+
b: TradeStationOrderRequestJson,
|
|
89
|
+
): boolean {
|
|
90
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
tradestation.post("/orders", async (c) => {
|
|
94
|
+
const denied = checkPerm(c, "tradestation");
|
|
95
|
+
if (denied) return denied;
|
|
96
|
+
|
|
97
|
+
const { spaceId, callerId } = getAuth(c);
|
|
98
|
+
const { db, config, tradeStationFetch } = getApiCtx(c);
|
|
99
|
+
const fetchImpl = tradeStationFetch ?? fetch;
|
|
100
|
+
|
|
101
|
+
cleanupExpiredTradestationPending(db);
|
|
102
|
+
|
|
103
|
+
let bodyRaw: unknown;
|
|
104
|
+
try {
|
|
105
|
+
bodyRaw = await c.req.json();
|
|
106
|
+
} catch {
|
|
107
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const parsed = orderBodySchema.safeParse(bodyRaw);
|
|
111
|
+
if (!parsed.success) {
|
|
112
|
+
return c.json(
|
|
113
|
+
{ error: "Invalid order payload", details: parsed.error.flatten() },
|
|
114
|
+
400,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const input = parsed.data;
|
|
119
|
+
const confirm = input.confirm === true;
|
|
120
|
+
const pendingId = input.pendingId;
|
|
121
|
+
|
|
122
|
+
if (liveOrderBlocked(input.accountKey, config.tsAllowLiveOrders)) {
|
|
123
|
+
return c.json(
|
|
124
|
+
{
|
|
125
|
+
error:
|
|
126
|
+
"Live (non-SIM) account orders are disabled. Set MERCURY_TS_ALLOW_LIVE_ORDERS=true only if you accept real-money risk, or use a SIM account.",
|
|
127
|
+
},
|
|
128
|
+
403,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (confirm) {
|
|
133
|
+
if (!pendingId) {
|
|
134
|
+
return c.json({ error: "confirm requires pendingId" }, 400);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const pending = loadPendingOrder(db, pendingId);
|
|
138
|
+
if (!pending) {
|
|
139
|
+
return c.json(
|
|
140
|
+
{
|
|
141
|
+
error:
|
|
142
|
+
"Unknown or expired pendingId — start again with confirm: false",
|
|
143
|
+
},
|
|
144
|
+
404,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (pending.expiresAt < Date.now()) {
|
|
149
|
+
deletePendingOrder(db, pendingId);
|
|
150
|
+
return c.json({ error: "pendingId expired" }, 410);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (pending.spaceId !== spaceId || pending.callerId !== callerId) {
|
|
154
|
+
return c.json(
|
|
155
|
+
{ error: "pendingId was issued for a different caller or space" },
|
|
156
|
+
403,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const replay = buildOrderRequest(input);
|
|
161
|
+
if (!orderRequestsMatch(replay, pending.orderRequest)) {
|
|
162
|
+
return c.json(
|
|
163
|
+
{
|
|
164
|
+
error:
|
|
165
|
+
"Order fields do not match the pending proposal — use the same parameters as the first request",
|
|
166
|
+
},
|
|
167
|
+
400,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const place = await tradeStationAuthorizedJson(
|
|
172
|
+
db,
|
|
173
|
+
{
|
|
174
|
+
method: "POST",
|
|
175
|
+
path: "/orderexecution/orders",
|
|
176
|
+
body: pending.orderRequest,
|
|
177
|
+
},
|
|
178
|
+
fetchImpl,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
deletePendingOrder(db, pendingId);
|
|
182
|
+
|
|
183
|
+
logger.info("TradeStation order placed", {
|
|
184
|
+
spaceId,
|
|
185
|
+
callerId,
|
|
186
|
+
accountKey: String(pending.orderRequest.AccountID),
|
|
187
|
+
symbol: String(pending.orderRequest.Symbol),
|
|
188
|
+
quantity: String(pending.orderRequest.Quantity),
|
|
189
|
+
tradeAction: String(pending.orderRequest.TradeAction),
|
|
190
|
+
orderType: String(pending.orderRequest.OrderType),
|
|
191
|
+
tsStatus: place.status,
|
|
192
|
+
ok: place.ok,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (!place.ok) {
|
|
196
|
+
const st = place.status >= 400 && place.status < 600 ? place.status : 502;
|
|
197
|
+
return c.json(
|
|
198
|
+
{
|
|
199
|
+
error: "TradeStation order request failed",
|
|
200
|
+
status: place.status,
|
|
201
|
+
tradestation: place.data,
|
|
202
|
+
},
|
|
203
|
+
st as 400 | 401 | 403 | 404 | 409 | 410 | 422 | 429 | 500 | 502,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return c.json({
|
|
208
|
+
placed: true,
|
|
209
|
+
tradestation: place.data,
|
|
210
|
+
summary: pending.summary,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Propose: confirm is false or omitted
|
|
215
|
+
const orderRequest = buildOrderRequest(input);
|
|
216
|
+
|
|
217
|
+
const confirmRes = await tradeStationAuthorizedJson(
|
|
218
|
+
db,
|
|
219
|
+
{
|
|
220
|
+
method: "POST",
|
|
221
|
+
path: "/orderexecution/orderconfirm",
|
|
222
|
+
body: orderRequest,
|
|
223
|
+
},
|
|
224
|
+
fetchImpl,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (!confirmRes.ok) {
|
|
228
|
+
const st =
|
|
229
|
+
confirmRes.status >= 400 && confirmRes.status < 600
|
|
230
|
+
? confirmRes.status
|
|
231
|
+
: 502;
|
|
232
|
+
return c.json(
|
|
233
|
+
{
|
|
234
|
+
error: "TradeStation orderconfirm failed",
|
|
235
|
+
status: confirmRes.status,
|
|
236
|
+
tradestation: confirmRes.data,
|
|
237
|
+
},
|
|
238
|
+
st as 400 | 401 | 403 | 404 | 409 | 410 | 422 | 429 | 500 | 502,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const id = createPendingOrderId();
|
|
243
|
+
const summary = `${input.tradeAction} ${input.quantity} ${input.symbol} on account ${input.accountKey} (${input.orderType}, ${input.timeInForceDuration})`;
|
|
244
|
+
|
|
245
|
+
const record: PendingOrderRecord = {
|
|
246
|
+
v: 1,
|
|
247
|
+
spaceId,
|
|
248
|
+
callerId,
|
|
249
|
+
createdAt: Date.now(),
|
|
250
|
+
expiresAt: Date.now() + PENDING_ORDER_TTL_MS,
|
|
251
|
+
orderRequest,
|
|
252
|
+
summary,
|
|
253
|
+
};
|
|
254
|
+
savePendingOrder(db, id, record);
|
|
255
|
+
|
|
256
|
+
return c.json({
|
|
257
|
+
warning: true,
|
|
258
|
+
pendingId: id,
|
|
259
|
+
summary,
|
|
260
|
+
confirmPreview: confirmRes.data,
|
|
261
|
+
message:
|
|
262
|
+
"STOP AND VERIFY. Only proceed if this order matches user intent. " +
|
|
263
|
+
"Share the summary with the user on any chat platform. " +
|
|
264
|
+
`To execute, send the same JSON fields with confirm: true and pendingId: "${id}" ` +
|
|
265
|
+
`(e.g. mrctl tradestation order ... --confirm --pending-id ${id}). ` +
|
|
266
|
+
`Or ask the user to reply with: CONFIRM ${id}`,
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { logger } from "../../logger.js";
|
|
4
|
+
import { synthesizeSpeech, TtsConfigError } from "../../tts/index.js";
|
|
5
|
+
import { checkPerm, type Env, getApiCtx } from "../api-types.js";
|
|
6
|
+
|
|
7
|
+
export const tts = new Hono<Env>();
|
|
8
|
+
|
|
9
|
+
const bodySchema = z.object({
|
|
10
|
+
text: z.string().min(1).max(10_000),
|
|
11
|
+
language: z.enum(["auto", "he-IL", "en-US"]).optional(),
|
|
12
|
+
provider: z.enum(["google", "azure", "auto"]).optional(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
tts.post("/synthesize", async (c) => {
|
|
16
|
+
const denied = checkPerm(c, "tts.synthesize");
|
|
17
|
+
if (denied) return denied;
|
|
18
|
+
|
|
19
|
+
const { config } = getApiCtx(c);
|
|
20
|
+
|
|
21
|
+
let body: z.infer<typeof bodySchema>;
|
|
22
|
+
try {
|
|
23
|
+
body = bodySchema.parse(await c.req.json());
|
|
24
|
+
} catch {
|
|
25
|
+
return c.json({ error: "Invalid JSON body or validation failed" }, 400);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const { buffer, mimeType, filename } = await synthesizeSpeech(config, {
|
|
30
|
+
text: body.text,
|
|
31
|
+
language: body.language,
|
|
32
|
+
providerOverride: body.provider,
|
|
33
|
+
});
|
|
34
|
+
return c.json({
|
|
35
|
+
mimeType,
|
|
36
|
+
filename,
|
|
37
|
+
dataBase64: buffer.toString("base64"),
|
|
38
|
+
sizeBytes: buffer.length,
|
|
39
|
+
});
|
|
40
|
+
} catch (e) {
|
|
41
|
+
if (e instanceof TtsConfigError) {
|
|
42
|
+
return c.json({ error: e.message }, 503);
|
|
43
|
+
}
|
|
44
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
45
|
+
if (msg.includes("empty") || msg.includes("max length")) {
|
|
46
|
+
return c.json({ error: msg }, 400);
|
|
47
|
+
}
|
|
48
|
+
logger.warn("TTS synthesize failed", { error: msg });
|
|
49
|
+
return c.json({ error: msg }, 502);
|
|
50
|
+
}
|
|
51
|
+
});
|