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.
Files changed (218) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +438 -0
  3. package/container/Dockerfile +127 -0
  4. package/container/Dockerfile.base +109 -0
  5. package/container/Dockerfile.power +17 -0
  6. package/container/agent-package.json +8 -0
  7. package/container/build.sh +54 -0
  8. package/docs/TODOS.md +147 -0
  9. package/docs/auth/dashboard.md +28 -0
  10. package/docs/auth/overview.md +109 -0
  11. package/docs/auth/whatsapp.md +173 -0
  12. package/docs/configuration.md +54 -0
  13. package/docs/container-lifecycle.md +349 -0
  14. package/docs/context-architecture.md +87 -0
  15. package/docs/deployment.md +199 -0
  16. package/docs/extensions.md +375 -0
  17. package/docs/graceful-shutdown.md +62 -0
  18. package/docs/kb-distillation.md +77 -0
  19. package/docs/media/overview.md +140 -0
  20. package/docs/media/whatsapp.md +171 -0
  21. package/docs/memory.md +137 -0
  22. package/docs/permissions.md +217 -0
  23. package/docs/pipeline.md +228 -0
  24. package/docs/prd-chat-memory.md +76 -0
  25. package/docs/prd-config-load.md +82 -0
  26. package/docs/rate-limiting.md +166 -0
  27. package/docs/scheduler.md +288 -0
  28. package/docs/setup-discord.md +100 -0
  29. package/docs/setup-slack.md +119 -0
  30. package/docs/setup-whatsapp.md +94 -0
  31. package/docs/subagents.md +166 -0
  32. package/docs/web-search.md +62 -0
  33. package/examples/extensions/README.md +12 -0
  34. package/examples/extensions/charts/index.ts +13 -0
  35. package/examples/extensions/charts/skill/SKILL.md +98 -0
  36. package/examples/extensions/gws/README.md +52 -0
  37. package/examples/extensions/gws/index.ts +106 -0
  38. package/examples/extensions/gws/skill/SKILL.md +57 -0
  39. package/examples/extensions/gws/skill/references/calendar.md +101 -0
  40. package/examples/extensions/gws/skill/references/docs.md +65 -0
  41. package/examples/extensions/gws/skill/references/drive.md +79 -0
  42. package/examples/extensions/gws/skill/references/gmail.md +85 -0
  43. package/examples/extensions/gws/skill/references/sheets.md +60 -0
  44. package/examples/extensions/napkin/index.ts +821 -0
  45. package/examples/extensions/napkin/prompts/consolidation-monthly.md +73 -0
  46. package/examples/extensions/napkin/prompts/consolidation-weekly.md +67 -0
  47. package/examples/extensions/napkin/prompts/kb-distillation.md +176 -0
  48. package/examples/extensions/napkin/skill/SKILL.md +728 -0
  49. package/examples/extensions/pdf/index.ts +23 -0
  50. package/examples/extensions/pdf/skill/LICENSE.txt +30 -0
  51. package/examples/extensions/pdf/skill/SKILL.md +314 -0
  52. package/examples/extensions/pdf/skill/forms.md +294 -0
  53. package/examples/extensions/pdf/skill/reference.md +612 -0
  54. package/examples/extensions/pdf/skill/scripts/check_bounding_boxes.py +65 -0
  55. package/examples/extensions/pdf/skill/scripts/check_fillable_fields.py +11 -0
  56. package/examples/extensions/pdf/skill/scripts/convert_pdf_to_images.py +33 -0
  57. package/examples/extensions/pdf/skill/scripts/create_validation_image.py +37 -0
  58. package/examples/extensions/pdf/skill/scripts/extract_form_field_info.py +122 -0
  59. package/examples/extensions/pdf/skill/scripts/extract_form_structure.py +115 -0
  60. package/examples/extensions/pdf/skill/scripts/fill_fillable_fields.py +98 -0
  61. package/examples/extensions/pdf/skill/scripts/fill_pdf_form_with_annotations.py +107 -0
  62. package/examples/extensions/permission-guard/index.ts +65 -0
  63. package/examples/extensions/pinchtab/index.ts +199 -0
  64. package/examples/extensions/pinchtab/lib/session-injector.ts +144 -0
  65. package/examples/extensions/pinchtab/skill/SKILL.md +224 -0
  66. package/examples/extensions/pinchtab/skill/TRUST.md +69 -0
  67. package/examples/extensions/pinchtab/skill/references/api.md +297 -0
  68. package/examples/extensions/pinchtab/skill/references/env.md +45 -0
  69. package/examples/extensions/pinchtab/skill/references/profiles.md +107 -0
  70. package/examples/extensions/tradestation/host/refresh.ts +102 -0
  71. package/examples/extensions/tradestation/index.ts +153 -0
  72. package/examples/extensions/tradestation/skill/SKILL.md +67 -0
  73. package/examples/extensions/tradestation/skill/scripts/ts-cli.ts +111 -0
  74. package/examples/extensions/voice-synth/index.ts +94 -0
  75. package/examples/extensions/voice-synth/skill/SKILL.md +38 -0
  76. package/examples/extensions/voice-transcribe/index.ts +381 -0
  77. package/examples/extensions/voice-transcribe/requirements.txt +8 -0
  78. package/examples/extensions/voice-transcribe/scripts/transcribe.py +179 -0
  79. package/examples/extensions/voice-transcribe/skill/SKILL.md +53 -0
  80. package/examples/extensions/web-search/index.ts +22 -0
  81. package/examples/extensions/web-search/skill/SKILL.md +114 -0
  82. package/examples/extensions/web-search/skill/references/apartments.md +178 -0
  83. package/examples/extensions/web-search/skill/references/car-purchase.md +132 -0
  84. package/examples/extensions/web-search/skill/references/car-rental.md +113 -0
  85. package/examples/extensions/web-search/skill/references/flights.md +133 -0
  86. package/examples/extensions/web-search/skill/references/hotels.md +148 -0
  87. package/examples/extensions/yahoo-mail/cli/bun.lock +66 -0
  88. package/examples/extensions/yahoo-mail/cli/package.json +13 -0
  89. package/examples/extensions/yahoo-mail/cli/ymail.mjs +353 -0
  90. package/examples/extensions/yahoo-mail/index.ts +57 -0
  91. package/examples/extensions/yahoo-mail/skill/SKILL.md +78 -0
  92. package/package.json +106 -0
  93. package/resources/agents/explore.md +50 -0
  94. package/resources/agents/worker.md +24 -0
  95. package/resources/builtin-extensions.txt +3 -0
  96. package/resources/connection-env-vars.json +25 -0
  97. package/resources/extensions/.gitkeep +0 -0
  98. package/resources/pi-extensions/subagent/agents.ts +126 -0
  99. package/resources/pi-extensions/subagent/index.ts +964 -0
  100. package/resources/profiles/coding/AGENTS.md +43 -0
  101. package/resources/profiles/coding/mercury-profile.yaml +15 -0
  102. package/resources/profiles/general/AGENTS.md +31 -0
  103. package/resources/profiles/general/mercury-profile.yaml +15 -0
  104. package/resources/profiles/research/AGENTS.md +40 -0
  105. package/resources/profiles/research/mercury-profile.yaml +15 -0
  106. package/resources/skills/config/SKILL.md +25 -0
  107. package/resources/skills/context/SKILL.md +33 -0
  108. package/resources/skills/conversation-recap/SKILL.md +19 -0
  109. package/resources/skills/media/SKILL.md +27 -0
  110. package/resources/skills/mutes/SKILL.md +31 -0
  111. package/resources/skills/permissions/SKILL.md +19 -0
  112. package/resources/skills/preferences/SKILL.md +31 -0
  113. package/resources/skills/recall/SKILL.md +24 -0
  114. package/resources/skills/roles/SKILL.md +18 -0
  115. package/resources/skills/spaces/SKILL.md +18 -0
  116. package/resources/skills/tasks/SKILL.md +45 -0
  117. package/resources/templates/AGENTS.md +157 -0
  118. package/resources/templates/env.template +34 -0
  119. package/resources/templates/mercury.example.yaml +75 -0
  120. package/src/adapters/discord-native.ts +534 -0
  121. package/src/adapters/discord.ts +38 -0
  122. package/src/adapters/setup.ts +89 -0
  123. package/src/adapters/slack.ts +9 -0
  124. package/src/adapters/whatsapp-media.ts +337 -0
  125. package/src/adapters/whatsapp.ts +629 -0
  126. package/src/agent/api-socket.ts +127 -0
  127. package/src/agent/container-entry.ts +967 -0
  128. package/src/agent/container-error.ts +49 -0
  129. package/src/agent/container-runner.ts +1272 -0
  130. package/src/agent/model-capabilities-core.ts +23 -0
  131. package/src/agent/model-capabilities.ts +231 -0
  132. package/src/agent/pi-failure-class.ts +83 -0
  133. package/src/agent/pi-jsonl-parser.ts +306 -0
  134. package/src/agent/preferences-prompt.ts +20 -0
  135. package/src/agent/user-error-messages.ts +78 -0
  136. package/src/bridges/discord.ts +171 -0
  137. package/src/bridges/slack.ts +177 -0
  138. package/src/bridges/teams.ts +160 -0
  139. package/src/bridges/telegram.ts +571 -0
  140. package/src/bridges/whatsapp.ts +290 -0
  141. package/src/chat-shim.ts +259 -0
  142. package/src/cli/mercury.ts +2508 -0
  143. package/src/cli/mrctl-http.ts +27 -0
  144. package/src/cli/mrctl.ts +611 -0
  145. package/src/cli/whatsapp-auth.ts +260 -0
  146. package/src/config-file.ts +397 -0
  147. package/src/config-model-chain.ts +30 -0
  148. package/src/config.ts +316 -0
  149. package/src/core/api-types.ts +58 -0
  150. package/src/core/api.ts +105 -0
  151. package/src/core/commands.ts +76 -0
  152. package/src/core/conversation.ts +47 -0
  153. package/src/core/handler.ts +206 -0
  154. package/src/core/media.ts +200 -0
  155. package/src/core/mute-duration.ts +22 -0
  156. package/src/core/outbox.ts +76 -0
  157. package/src/core/permissions.ts +192 -0
  158. package/src/core/profiles.ts +245 -0
  159. package/src/core/rate-limiter.ts +127 -0
  160. package/src/core/router.ts +191 -0
  161. package/src/core/routes/chat.ts +172 -0
  162. package/src/core/routes/config-builtin.ts +107 -0
  163. package/src/core/routes/config.ts +81 -0
  164. package/src/core/routes/connections.ts +190 -0
  165. package/src/core/routes/console.ts +668 -0
  166. package/src/core/routes/control.ts +46 -0
  167. package/src/core/routes/conversations.ts +66 -0
  168. package/src/core/routes/dashboard.ts +2491 -0
  169. package/src/core/routes/extensions.ts +37 -0
  170. package/src/core/routes/index.ts +14 -0
  171. package/src/core/routes/media.ts +72 -0
  172. package/src/core/routes/messages.ts +37 -0
  173. package/src/core/routes/mutes.ts +89 -0
  174. package/src/core/routes/prefs.ts +95 -0
  175. package/src/core/routes/roles.ts +125 -0
  176. package/src/core/routes/spaces.ts +60 -0
  177. package/src/core/routes/storage.ts +126 -0
  178. package/src/core/routes/tasks.ts +189 -0
  179. package/src/core/routes/tradestation.ts +268 -0
  180. package/src/core/routes/tts.ts +51 -0
  181. package/src/core/runtime.ts +1140 -0
  182. package/src/core/space-queue.ts +103 -0
  183. package/src/core/storage-cleanup.ts +140 -0
  184. package/src/core/storage-guard.ts +24 -0
  185. package/src/core/task-scheduler.ts +132 -0
  186. package/src/core/telegram-format.ts +178 -0
  187. package/src/core/trigger.ts +142 -0
  188. package/src/dashboard/index.html +729 -0
  189. package/src/dashboard/tokens.css +53 -0
  190. package/src/extensions/api.ts +252 -0
  191. package/src/extensions/catalog.ts +117 -0
  192. package/src/extensions/config-registry.ts +83 -0
  193. package/src/extensions/context.ts +36 -0
  194. package/src/extensions/hooks.ts +156 -0
  195. package/src/extensions/image-builder.ts +617 -0
  196. package/src/extensions/installer.ts +306 -0
  197. package/src/extensions/jobs.ts +122 -0
  198. package/src/extensions/loader.ts +271 -0
  199. package/src/extensions/permission-guard.ts +52 -0
  200. package/src/extensions/reserved.ts +28 -0
  201. package/src/extensions/skills.ts +123 -0
  202. package/src/extensions/types.ts +462 -0
  203. package/src/logger.ts +174 -0
  204. package/src/main.ts +586 -0
  205. package/src/server.ts +391 -0
  206. package/src/storage/db.ts +1624 -0
  207. package/src/storage/memory.ts +45 -0
  208. package/src/storage/pi-auth.ts +95 -0
  209. package/src/text/markdown.ts +117 -0
  210. package/src/text/rtl.ts +38 -0
  211. package/src/tradestation/host-api.ts +77 -0
  212. package/src/tradestation/pending-orders.ts +69 -0
  213. package/src/tts/azure.ts +52 -0
  214. package/src/tts/google.ts +128 -0
  215. package/src/tts/index.ts +8 -0
  216. package/src/tts/language.ts +20 -0
  217. package/src/tts/synthesize.ts +133 -0
  218. 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
+ });