@hybridaione/hybridclaw 0.2.3 → 0.2.7

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 (285) hide show
  1. package/.github/workflows/ci.yml +70 -0
  2. package/.husky/pre-commit +1 -0
  3. package/CHANGELOG.md +95 -0
  4. package/CONTRIBUTING.md +33 -0
  5. package/README.md +57 -19
  6. package/SECURITY.md +17 -0
  7. package/biome.json +35 -0
  8. package/config.example.json +72 -7
  9. package/container/package-lock.json +2 -2
  10. package/container/package.json +1 -1
  11. package/container/src/approval-policy.ts +1405 -0
  12. package/container/src/browser-tools.ts +672 -136
  13. package/container/src/extensions.ts +36 -12
  14. package/container/src/hybridai-client.ts +65 -16
  15. package/container/src/index.ts +463 -110
  16. package/container/src/ipc.ts +5 -3
  17. package/container/src/token-usage.ts +107 -18
  18. package/container/src/tools.ts +600 -225
  19. package/container/src/types.ts +49 -2
  20. package/container/src/web-fetch.ts +89 -32
  21. package/dist/agent.d.ts.map +1 -1
  22. package/dist/agent.js +15 -4
  23. package/dist/agent.js.map +1 -1
  24. package/dist/audit-cli.d.ts.map +1 -1
  25. package/dist/audit-cli.js +4 -2
  26. package/dist/audit-cli.js.map +1 -1
  27. package/dist/audit-events.d.ts.map +1 -1
  28. package/dist/audit-events.js +53 -3
  29. package/dist/audit-events.js.map +1 -1
  30. package/dist/audit-trail.d.ts.map +1 -1
  31. package/dist/audit-trail.js +17 -8
  32. package/dist/audit-trail.js.map +1 -1
  33. package/dist/channels/discord/attachments.d.ts.map +1 -1
  34. package/dist/channels/discord/attachments.js +14 -7
  35. package/dist/channels/discord/attachments.js.map +1 -1
  36. package/dist/channels/discord/debounce.d.ts +9 -0
  37. package/dist/channels/discord/debounce.d.ts.map +1 -0
  38. package/dist/channels/discord/debounce.js +20 -0
  39. package/dist/channels/discord/debounce.js.map +1 -0
  40. package/dist/channels/discord/delivery.d.ts +4 -1
  41. package/dist/channels/discord/delivery.d.ts.map +1 -1
  42. package/dist/channels/discord/delivery.js +24 -4
  43. package/dist/channels/discord/delivery.js.map +1 -1
  44. package/dist/channels/discord/human-delay.d.ts +16 -0
  45. package/dist/channels/discord/human-delay.d.ts.map +1 -0
  46. package/dist/channels/discord/human-delay.js +29 -0
  47. package/dist/channels/discord/human-delay.js.map +1 -0
  48. package/dist/channels/discord/inbound.d.ts +31 -0
  49. package/dist/channels/discord/inbound.d.ts.map +1 -1
  50. package/dist/channels/discord/inbound.js +156 -6
  51. package/dist/channels/discord/inbound.js.map +1 -1
  52. package/dist/channels/discord/mentions.d.ts.map +1 -1
  53. package/dist/channels/discord/mentions.js +16 -4
  54. package/dist/channels/discord/mentions.js.map +1 -1
  55. package/dist/channels/discord/presence.d.ts +33 -0
  56. package/dist/channels/discord/presence.d.ts.map +1 -0
  57. package/dist/channels/discord/presence.js +111 -0
  58. package/dist/channels/discord/presence.js.map +1 -0
  59. package/dist/channels/discord/rate-limiter.d.ts +14 -0
  60. package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
  61. package/dist/channels/discord/rate-limiter.js +49 -0
  62. package/dist/channels/discord/rate-limiter.js.map +1 -0
  63. package/dist/channels/discord/reactions.d.ts +38 -0
  64. package/dist/channels/discord/reactions.d.ts.map +1 -0
  65. package/dist/channels/discord/reactions.js +151 -0
  66. package/dist/channels/discord/reactions.js.map +1 -0
  67. package/dist/channels/discord/runtime.d.ts +7 -4
  68. package/dist/channels/discord/runtime.d.ts.map +1 -1
  69. package/dist/channels/discord/runtime.js +755 -150
  70. package/dist/channels/discord/runtime.js.map +1 -1
  71. package/dist/channels/discord/stream.d.ts +4 -1
  72. package/dist/channels/discord/stream.d.ts.map +1 -1
  73. package/dist/channels/discord/stream.js +19 -12
  74. package/dist/channels/discord/stream.js.map +1 -1
  75. package/dist/channels/discord/tool-actions.d.ts.map +1 -1
  76. package/dist/channels/discord/tool-actions.js +24 -12
  77. package/dist/channels/discord/tool-actions.js.map +1 -1
  78. package/dist/channels/discord/typing.d.ts +15 -0
  79. package/dist/channels/discord/typing.d.ts.map +1 -0
  80. package/dist/channels/discord/typing.js +106 -0
  81. package/dist/channels/discord/typing.js.map +1 -0
  82. package/dist/chunk.d.ts.map +1 -1
  83. package/dist/chunk.js +4 -2
  84. package/dist/chunk.js.map +1 -1
  85. package/dist/cli.js +77 -23
  86. package/dist/cli.js.map +1 -1
  87. package/dist/config.d.ts +24 -0
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/config.js +113 -18
  90. package/dist/config.js.map +1 -1
  91. package/dist/container-runner.d.ts.map +1 -1
  92. package/dist/container-runner.js +59 -26
  93. package/dist/container-runner.js.map +1 -1
  94. package/dist/container-setup.d.ts.map +1 -1
  95. package/dist/container-setup.js +10 -9
  96. package/dist/container-setup.js.map +1 -1
  97. package/dist/conversation.d.ts +2 -2
  98. package/dist/conversation.d.ts.map +1 -1
  99. package/dist/conversation.js +1 -1
  100. package/dist/conversation.js.map +1 -1
  101. package/dist/db.d.ts +118 -2
  102. package/dist/db.d.ts.map +1 -1
  103. package/dist/db.js +1568 -50
  104. package/dist/db.js.map +1 -1
  105. package/dist/delegation-manager.d.ts.map +1 -1
  106. package/dist/delegation-manager.js +3 -2
  107. package/dist/delegation-manager.js.map +1 -1
  108. package/dist/gateway-client.d.ts +2 -2
  109. package/dist/gateway-client.d.ts.map +1 -1
  110. package/dist/gateway-client.js +10 -4
  111. package/dist/gateway-client.js.map +1 -1
  112. package/dist/gateway-service.d.ts +3 -3
  113. package/dist/gateway-service.d.ts.map +1 -1
  114. package/dist/gateway-service.js +624 -87
  115. package/dist/gateway-service.js.map +1 -1
  116. package/dist/gateway-types.d.ts +24 -0
  117. package/dist/gateway-types.d.ts.map +1 -1
  118. package/dist/gateway-types.js.map +1 -1
  119. package/dist/gateway.js +303 -26
  120. package/dist/gateway.js.map +1 -1
  121. package/dist/health.d.ts.map +1 -1
  122. package/dist/health.js +20 -10
  123. package/dist/health.js.map +1 -1
  124. package/dist/heartbeat.d.ts +4 -0
  125. package/dist/heartbeat.d.ts.map +1 -1
  126. package/dist/heartbeat.js +62 -20
  127. package/dist/heartbeat.js.map +1 -1
  128. package/dist/hybridai-bots.d.ts.map +1 -1
  129. package/dist/hybridai-bots.js +4 -2
  130. package/dist/hybridai-bots.js.map +1 -1
  131. package/dist/hybridai-models.d.ts +8 -0
  132. package/dist/hybridai-models.d.ts.map +1 -0
  133. package/dist/hybridai-models.js +94 -0
  134. package/dist/hybridai-models.js.map +1 -0
  135. package/dist/instruction-approval-audit.d.ts.map +1 -1
  136. package/dist/instruction-approval-audit.js.map +1 -1
  137. package/dist/instruction-integrity.d.ts.map +1 -1
  138. package/dist/instruction-integrity.js +8 -2
  139. package/dist/instruction-integrity.js.map +1 -1
  140. package/dist/ipc.d.ts.map +1 -1
  141. package/dist/ipc.js +6 -1
  142. package/dist/ipc.js.map +1 -1
  143. package/dist/logger.js.map +1 -1
  144. package/dist/memory-consolidation.d.ts +17 -0
  145. package/dist/memory-consolidation.d.ts.map +1 -0
  146. package/dist/memory-consolidation.js +25 -0
  147. package/dist/memory-consolidation.js.map +1 -0
  148. package/dist/memory-service.d.ts +200 -0
  149. package/dist/memory-service.d.ts.map +1 -0
  150. package/dist/memory-service.js +294 -0
  151. package/dist/memory-service.js.map +1 -0
  152. package/dist/mount-security.d.ts.map +1 -1
  153. package/dist/mount-security.js +31 -7
  154. package/dist/mount-security.js.map +1 -1
  155. package/dist/observability-ingest.d.ts.map +1 -1
  156. package/dist/observability-ingest.js +32 -11
  157. package/dist/observability-ingest.js.map +1 -1
  158. package/dist/onboarding.d.ts.map +1 -1
  159. package/dist/onboarding.js +32 -9
  160. package/dist/onboarding.js.map +1 -1
  161. package/dist/proactive-policy.d.ts.map +1 -1
  162. package/dist/proactive-policy.js +2 -1
  163. package/dist/proactive-policy.js.map +1 -1
  164. package/dist/prompt-hooks.d.ts.map +1 -1
  165. package/dist/prompt-hooks.js +11 -7
  166. package/dist/prompt-hooks.js.map +1 -1
  167. package/dist/runtime-config.d.ts +104 -1
  168. package/dist/runtime-config.d.ts.map +1 -1
  169. package/dist/runtime-config.js +503 -24
  170. package/dist/runtime-config.js.map +1 -1
  171. package/dist/scheduled-task-runner.d.ts +1 -0
  172. package/dist/scheduled-task-runner.d.ts.map +1 -1
  173. package/dist/scheduled-task-runner.js +43 -10
  174. package/dist/scheduled-task-runner.js.map +1 -1
  175. package/dist/scheduler.d.ts +43 -4
  176. package/dist/scheduler.d.ts.map +1 -1
  177. package/dist/scheduler.js +530 -56
  178. package/dist/scheduler.js.map +1 -1
  179. package/dist/session-export.d.ts +26 -0
  180. package/dist/session-export.d.ts.map +1 -0
  181. package/dist/session-export.js +149 -0
  182. package/dist/session-export.js.map +1 -0
  183. package/dist/session-maintenance.d.ts.map +1 -1
  184. package/dist/session-maintenance.js +75 -13
  185. package/dist/session-maintenance.js.map +1 -1
  186. package/dist/session-transcripts.d.ts.map +1 -1
  187. package/dist/session-transcripts.js.map +1 -1
  188. package/dist/side-effects.d.ts.map +1 -1
  189. package/dist/side-effects.js +14 -2
  190. package/dist/side-effects.js.map +1 -1
  191. package/dist/skills-guard.d.ts.map +1 -1
  192. package/dist/skills-guard.js +893 -130
  193. package/dist/skills-guard.js.map +1 -1
  194. package/dist/skills.d.ts +5 -0
  195. package/dist/skills.d.ts.map +1 -1
  196. package/dist/skills.js +29 -15
  197. package/dist/skills.js.map +1 -1
  198. package/dist/token-efficiency.d.ts.map +1 -1
  199. package/dist/token-efficiency.js.map +1 -1
  200. package/dist/tui.js +92 -11
  201. package/dist/tui.js.map +1 -1
  202. package/dist/types.d.ts +150 -0
  203. package/dist/types.d.ts.map +1 -1
  204. package/dist/types.js +24 -1
  205. package/dist/types.js.map +1 -1
  206. package/dist/update.d.ts.map +1 -1
  207. package/dist/update.js +42 -14
  208. package/dist/update.js.map +1 -1
  209. package/dist/workspace.d.ts.map +1 -1
  210. package/dist/workspace.js +49 -9
  211. package/dist/workspace.js.map +1 -1
  212. package/docs/chat.html +9 -3
  213. package/docs/index.html +72 -13
  214. package/package.json +8 -2
  215. package/src/agent.ts +31 -4
  216. package/src/audit-cli.ts +44 -16
  217. package/src/audit-events.ts +69 -5
  218. package/src/audit-trail.ts +41 -15
  219. package/src/channels/discord/attachments.ts +81 -27
  220. package/src/channels/discord/debounce.ts +25 -0
  221. package/src/channels/discord/delivery.ts +65 -14
  222. package/src/channels/discord/human-delay.ts +48 -0
  223. package/src/channels/discord/inbound.ts +190 -8
  224. package/src/channels/discord/mentions.ts +42 -18
  225. package/src/channels/discord/presence.ts +148 -0
  226. package/src/channels/discord/rate-limiter.ts +58 -0
  227. package/src/channels/discord/reactions.ts +211 -0
  228. package/src/channels/discord/runtime.ts +1161 -205
  229. package/src/channels/discord/stream.ts +85 -31
  230. package/src/channels/discord/tool-actions.ts +78 -37
  231. package/src/channels/discord/typing.ts +140 -0
  232. package/src/chunk.ts +12 -4
  233. package/src/cli.ts +174 -57
  234. package/src/config.ts +208 -36
  235. package/src/container-runner.ts +134 -42
  236. package/src/container-setup.ts +57 -22
  237. package/src/conversation.ts +9 -7
  238. package/src/db.ts +2217 -84
  239. package/src/delegation-manager.ts +6 -2
  240. package/src/gateway-client.ts +41 -17
  241. package/src/gateway-service.ts +996 -220
  242. package/src/gateway-types.ts +33 -0
  243. package/src/gateway.ts +487 -52
  244. package/src/health.ts +66 -26
  245. package/src/heartbeat.ts +99 -22
  246. package/src/hybridai-bots.ts +14 -5
  247. package/src/hybridai-models.ts +158 -0
  248. package/src/instruction-approval-audit.ts +4 -1
  249. package/src/instruction-integrity.ts +30 -9
  250. package/src/ipc.ts +23 -5
  251. package/src/logger.ts +4 -1
  252. package/src/memory-consolidation.ts +41 -0
  253. package/src/memory-service.ts +606 -0
  254. package/src/mount-security.ts +58 -13
  255. package/src/observability-ingest.ts +134 -35
  256. package/src/onboarding.ts +126 -35
  257. package/src/proactive-policy.ts +3 -1
  258. package/src/prompt-hooks.ts +42 -17
  259. package/src/runtime-config.ts +1112 -122
  260. package/src/scheduled-task-runner.ts +77 -11
  261. package/src/scheduler.ts +683 -60
  262. package/src/session-export.ts +196 -0
  263. package/src/session-maintenance.ts +125 -22
  264. package/src/session-transcripts.ts +12 -3
  265. package/src/side-effects.ts +28 -5
  266. package/src/skills-guard.ts +1067 -219
  267. package/src/skills.ts +163 -65
  268. package/src/token-efficiency.ts +31 -9
  269. package/src/tui.ts +166 -25
  270. package/src/types.ts +199 -2
  271. package/src/update.ts +79 -23
  272. package/src/workspace.ts +63 -11
  273. package/tests/approval-policy.test.ts +335 -0
  274. package/tests/discord.basic.test.ts +213 -4
  275. package/tests/discord.human-presence.test.ts +85 -0
  276. package/tests/gateway-service.media-routing.test.ts +8 -2
  277. package/tests/hybridai-client.test.ts +112 -0
  278. package/tests/hybridai-models.test.ts +46 -0
  279. package/tests/memory-service.test.ts +1114 -0
  280. package/tests/token-efficiency.basic.test.ts +8 -2
  281. package/tests/token-usage.cache.test.ts +128 -0
  282. package/vitest.e2e.config.ts +3 -1
  283. package/vitest.integration.config.ts +3 -1
  284. package/vitest.live.config.ts +3 -1
  285. package/vitest.unit.config.ts +9 -0
@@ -1,8 +1,12 @@
1
- import { execSync } from 'child_process';
2
- import fs from 'fs';
3
- import path from 'path';
4
-
5
- import { BROWSER_TOOL_DEFINITIONS, executeBrowserTool, setBrowserModelContext } from './browser-tools.js';
1
+ import { execSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ import {
6
+ BROWSER_TOOL_DEFINITIONS,
7
+ executeBrowserTool,
8
+ setBrowserModelContext,
9
+ } from './browser-tools.js';
6
10
  import type {
7
11
  DelegationSideEffect,
8
12
  DelegationTaskSpec,
@@ -15,24 +19,24 @@ import { webFetch } from './web-fetch.js';
15
19
  // --- Exec safety deny-list (defense-in-depth, adapted from PicoClaw) ---
16
20
 
17
21
  const DENY_PATTERNS: RegExp[] = [
18
- /\brm\s+-[rf]{1,2}\b/, // rm -r, rm -f, rm -rf
22
+ /\brm\s+-[rf]{1,2}\b/, // rm -r, rm -f, rm -rf
19
23
  /(^|[;&|]\s*)mkfs(?:\.[a-z0-9_+-]+)?\b/, // mkfs command at segment start
20
- /(^|[;&|]\s*)format(?:\.com|\.exe)?\b/, // format command at segment start (Windows)
21
- /\bdd\s+if=/, // raw disk I/O
22
- /:\(\)\s*\{.*\};\s*:/, // fork bomb :(){ :|:& };:
23
- /\|\s*(sh|bash|zsh)\b/, // pipe to shell
24
- /;\s*rm\s+-[rf]/, // chained rm after semicolon
25
- /&&\s*rm\s+-[rf]/, // chained rm after &&
26
- /\|\|\s*rm\s+-[rf]/, // chained rm after ||
27
- /\bcurl\b.*\|\s*(sh|bash)/, // curl | sh
28
- /\bwget\b.*\|\s*(sh|bash)/, // wget | sh
29
- /\beval\b/, // eval execution
30
- /\bsource\s+.*\.sh\b/, // source shell scripts
31
- /\bpkill\b/, // process killing
32
- /\bkillall\b/, // process killing
33
- /\bkill\s+-9\b/, // force kill
34
- /\b(shutdown|reboot|poweroff)\b/, // system power control
35
- />\s*\/dev\/sd[a-z]\b/, // write to block devices
24
+ /(^|[;&|]\s*)format(?:\.com|\.exe)?\b/, // format command at segment start (Windows)
25
+ /\bdd\s+if=/, // raw disk I/O
26
+ /:\(\)\s*\{.*\};\s*:/, // fork bomb :(){ :|:& };:
27
+ /\|\s*(sh|bash|zsh)\b/, // pipe to shell
28
+ /;\s*rm\s+-[rf]/, // chained rm after semicolon
29
+ /&&\s*rm\s+-[rf]/, // chained rm after &&
30
+ /\|\|\s*rm\s+-[rf]/, // chained rm after ||
31
+ /\bcurl\b.*\|\s*(sh|bash)/, // curl | sh
32
+ /\bwget\b.*\|\s*(sh|bash)/, // wget | sh
33
+ /\beval\b/, // eval execution
34
+ /\bsource\s+.*\.sh\b/, // source shell scripts
35
+ /\bpkill\b/, // process killing
36
+ /\bkillall\b/, // process killing
37
+ /\bkill\s+-9\b/, // force kill
38
+ /\b(shutdown|reboot|poweroff)\b/, // system power control
39
+ />\s*\/dev\/sd[a-z]\b/, // write to block devices
36
40
  ];
37
41
 
38
42
  function guardCommand(command: string): string | null {
@@ -47,7 +51,16 @@ function guardCommand(command: string): string | null {
47
51
 
48
52
  // --- Side-effect accumulator for host-processed actions ---
49
53
 
50
- type ScheduledTaskInfo = { id: number; cronExpr: string; runAt: string | null; everyMs: number | null; prompt: string; enabled: number; lastRun: string | null; createdAt: string };
54
+ type ScheduledTaskInfo = {
55
+ id: number;
56
+ cronExpr: string;
57
+ runAt: string | null;
58
+ everyMs: number | null;
59
+ prompt: string;
60
+ enabled: number;
61
+ lastRun: string | null;
62
+ createdAt: string;
63
+ };
51
64
 
52
65
  let pendingSchedules: ScheduleSideEffect[] = [];
53
66
  let pendingDelegations: DelegationSideEffect[] = [];
@@ -80,18 +93,23 @@ export function resetSideEffects(): void {
80
93
  pendingDelegations = [];
81
94
  }
82
95
 
83
- export function getPendingSideEffects(): {
84
- schedules?: ScheduleSideEffect[];
85
- delegations?: DelegationSideEffect[];
86
- } | undefined {
87
- if (pendingSchedules.length === 0 && pendingDelegations.length === 0) return undefined;
96
+ export function getPendingSideEffects():
97
+ | {
98
+ schedules?: ScheduleSideEffect[];
99
+ delegations?: DelegationSideEffect[];
100
+ }
101
+ | undefined {
102
+ if (pendingSchedules.length === 0 && pendingDelegations.length === 0)
103
+ return undefined;
88
104
  return {
89
105
  schedules: pendingSchedules.length > 0 ? pendingSchedules : undefined,
90
106
  delegations: pendingDelegations.length > 0 ? pendingDelegations : undefined,
91
107
  };
92
108
  }
93
109
 
94
- export function setScheduledTasks(tasks: ScheduledTaskInfo[] | undefined): void {
110
+ export function setScheduledTasks(
111
+ tasks: ScheduledTaskInfo[] | undefined,
112
+ ): void {
95
113
  injectedTasks = tasks || [];
96
114
  }
97
115
 
@@ -99,7 +117,11 @@ export function setSessionContext(sessionId: string): void {
99
117
  currentSessionId = String(sessionId || '');
100
118
  }
101
119
 
102
- export function setGatewayContext(baseUrl?: string, apiToken?: string, channelId?: string): void {
120
+ export function setGatewayContext(
121
+ baseUrl?: string,
122
+ apiToken?: string,
123
+ channelId?: string,
124
+ ): void {
103
125
  gatewayBaseUrl = String(baseUrl || '').trim();
104
126
  gatewayApiToken = String(apiToken || '').trim();
105
127
  gatewayChannelId = String(channelId || '').trim();
@@ -128,12 +150,16 @@ function readStringValue(value: unknown): string | undefined {
128
150
  return trimmed.length > 0 ? trimmed : undefined;
129
151
  }
130
152
 
131
- function resolveDiscordMessageAction(rawAction: unknown): DiscordMessageToolAction | null {
153
+ function resolveDiscordMessageAction(
154
+ rawAction: unknown,
155
+ ): DiscordMessageToolAction | null {
132
156
  const normalized = readStringValue(rawAction)?.toLowerCase();
133
157
  if (!normalized) return null;
134
158
  if (normalized === 'read' || normalized === 'readmessages') return 'read';
135
- if (normalized === 'member-info' || normalized === 'memberinfo') return 'member-info';
136
- if (normalized === 'channel-info' || normalized === 'channelinfo') return 'channel-info';
159
+ if (normalized === 'member-info' || normalized === 'memberinfo')
160
+ return 'member-info';
161
+ if (normalized === 'channel-info' || normalized === 'channelinfo')
162
+ return 'channel-info';
137
163
  return null;
138
164
  }
139
165
 
@@ -143,7 +169,9 @@ function resolveGatewayDiscordActionUrl(): string | null {
143
169
  return `${base}/api/discord/action`;
144
170
  }
145
171
 
146
- async function callGatewayDiscordAction(payload: Record<string, unknown>): Promise<string> {
172
+ async function callGatewayDiscordAction(
173
+ payload: Record<string, unknown>,
174
+ ): Promise<string> {
147
175
  const url = resolveGatewayDiscordActionUrl();
148
176
  if (!url) {
149
177
  return 'Error: Discord actions are unavailable because gatewayBaseUrl is not configured.';
@@ -179,9 +207,10 @@ async function callGatewayDiscordAction(payload: Record<string, unknown>): Promi
179
207
  }
180
208
 
181
209
  if (!response.ok) {
182
- const detail = typeof parsed?.error === 'string'
183
- ? parsed.error
184
- : rawText || `HTTP ${response.status}`;
210
+ const detail =
211
+ typeof parsed?.error === 'string'
212
+ ? parsed.error
213
+ : rawText || `HTTP ${response.status}`;
185
214
  return `Error: Discord action failed (${response.status}): ${detail}`;
186
215
  }
187
216
 
@@ -189,7 +218,10 @@ async function callGatewayDiscordAction(payload: Record<string, unknown>): Promi
189
218
  return rawText || JSON.stringify({ ok: true }, null, 2);
190
219
  }
191
220
 
192
- function normalizeDelegationTask(raw: unknown, fallbackModel?: string): DelegationTaskSpec | null {
221
+ function normalizeDelegationTask(
222
+ raw: unknown,
223
+ fallbackModel?: string,
224
+ ): DelegationTaskSpec | null {
193
225
  if (typeof raw === 'string') {
194
226
  const prompt = raw.trim();
195
227
  if (!prompt) return null;
@@ -202,7 +234,8 @@ function normalizeDelegationTask(raw: unknown, fallbackModel?: string): Delegati
202
234
  if (!prompt) return null;
203
235
 
204
236
  const label = typeof task.label === 'string' ? task.label.trim() : '';
205
- const model = typeof task.model === 'string' ? task.model.trim() : (fallbackModel || '');
237
+ const model =
238
+ typeof task.model === 'string' ? task.model.trim() : fallbackModel || '';
206
239
  const normalized: DelegationTaskSpec = { prompt };
207
240
  if (label) normalized.label = label;
208
241
  if (model) normalized.model = model;
@@ -217,10 +250,16 @@ function normalizeDelegationTaskList(params: {
217
250
  const { raw, fallbackModel, fieldName } = params;
218
251
  if (raw == null) return { tasks: [] };
219
252
  if (!Array.isArray(raw)) {
220
- return { tasks: [], error: `Error: "${fieldName}" must be an array of task objects.` };
253
+ return {
254
+ tasks: [],
255
+ error: `Error: "${fieldName}" must be an array of task objects.`,
256
+ };
221
257
  }
222
258
  if (raw.length === 0) {
223
- return { tasks: [], error: `Error: "${fieldName}" must contain at least one task.` };
259
+ return {
260
+ tasks: [],
261
+ error: `Error: "${fieldName}" must contain at least one task.`,
262
+ };
224
263
  }
225
264
  if (raw.length > MAX_DELEGATION_BATCH_ITEMS) {
226
265
  return {
@@ -274,7 +313,9 @@ function isSafeDiscordCdnUrl(raw: string): boolean {
274
313
  return false;
275
314
  }
276
315
  if (parsed.protocol !== 'https:') return false;
277
- return DISCORD_CDN_HOST_PATTERNS.some((pattern) => pattern.test(parsed.hostname));
316
+ return DISCORD_CDN_HOST_PATTERNS.some((pattern) =>
317
+ pattern.test(parsed.hostname),
318
+ );
278
319
  }
279
320
 
280
321
  function normalizeVisionLocalPath(rawPath: string): string | null {
@@ -286,8 +327,13 @@ function normalizeVisionLocalPath(rawPath: string): string | null {
286
327
  ? path.posix.normalize(normalizedInput)
287
328
  : path.posix.normalize(path.posix.join(WORKSPACE_ROOT, normalizedInput));
288
329
  if (
289
- !(candidate === WORKSPACE_ROOT || candidate.startsWith(`${WORKSPACE_ROOT}/`))
290
- && !(candidate === DISCORD_MEDIA_CACHE_ROOT || candidate.startsWith(`${DISCORD_MEDIA_CACHE_ROOT}/`))
330
+ !(
331
+ candidate === WORKSPACE_ROOT || candidate.startsWith(`${WORKSPACE_ROOT}/`)
332
+ ) &&
333
+ !(
334
+ candidate === DISCORD_MEDIA_CACHE_ROOT ||
335
+ candidate.startsWith(`${DISCORD_MEDIA_CACHE_ROOT}/`)
336
+ )
291
337
  ) {
292
338
  return null;
293
339
  }
@@ -304,8 +350,13 @@ function isKnownDiscordMediaPath(localPath: string): boolean {
304
350
  return knownPaths.includes(localPath);
305
351
  }
306
352
 
307
- function inferImageMimeTypeFromPath(localPath: string, fallbackMime?: string | null): string {
308
- const normalizedFallback = String(fallbackMime || '').trim().toLowerCase();
353
+ function inferImageMimeTypeFromPath(
354
+ localPath: string,
355
+ fallbackMime?: string | null,
356
+ ): string {
357
+ const normalizedFallback = String(fallbackMime || '')
358
+ .trim()
359
+ .toLowerCase();
309
360
  if (normalizedFallback.startsWith('image/')) return normalizedFallback;
310
361
  const ext = path.posix.extname(localPath).toLowerCase();
311
362
  if (ext === '.png') return 'image/png';
@@ -318,13 +369,22 @@ function inferImageMimeTypeFromPath(localPath: string, fallbackMime?: string | n
318
369
  return 'image/png';
319
370
  }
320
371
 
321
- async function readVisionImageFromLocalPath(localPath: string): Promise<{ buffer: Buffer; mimeType: string; source: string }> {
372
+ async function readVisionImageFromLocalPath(
373
+ localPath: string,
374
+ ): Promise<{ buffer: Buffer; mimeType: string; source: string }> {
322
375
  const normalizedPath = normalizeVisionLocalPath(localPath);
323
376
  if (!normalizedPath) {
324
- throw new Error('local image path must be under /workspace or /discord-media-cache');
377
+ throw new Error(
378
+ 'local image path must be under /workspace or /discord-media-cache',
379
+ );
325
380
  }
326
- if (normalizedPath.startsWith(`${DISCORD_MEDIA_CACHE_ROOT}/`) && !isKnownDiscordMediaPath(normalizedPath)) {
327
- throw new Error('requested local image is not part of current media context');
381
+ if (
382
+ normalizedPath.startsWith(`${DISCORD_MEDIA_CACHE_ROOT}/`) &&
383
+ !isKnownDiscordMediaPath(normalizedPath)
384
+ ) {
385
+ throw new Error(
386
+ 'requested local image is not part of current media context',
387
+ );
328
388
  }
329
389
  if (!fs.existsSync(normalizedPath)) {
330
390
  throw new Error(`local image not found: ${normalizedPath}`);
@@ -337,14 +397,21 @@ async function readVisionImageFromLocalPath(localPath: string): Promise<{ buffer
337
397
  throw new Error(`local image is empty: ${normalizedPath}`);
338
398
  }
339
399
  if (stat.size > VISION_IMAGE_MAX_BYTES) {
340
- throw new Error(`local image exceeds max size (${VISION_IMAGE_MAX_BYTES} bytes)`);
400
+ throw new Error(
401
+ `local image exceeds max size (${VISION_IMAGE_MAX_BYTES} bytes)`,
402
+ );
341
403
  }
342
404
  const buffer = fs.readFileSync(normalizedPath);
343
405
  const mediaHint = currentMediaContext.find((entry) => {
344
- const normalizedEntryPath = entry.path ? normalizeVisionLocalPath(entry.path) : null;
406
+ const normalizedEntryPath = entry.path
407
+ ? normalizeVisionLocalPath(entry.path)
408
+ : null;
345
409
  return normalizedEntryPath === normalizedPath;
346
410
  });
347
- const mimeType = inferImageMimeTypeFromPath(normalizedPath, mediaHint?.mimeType);
411
+ const mimeType = inferImageMimeTypeFromPath(
412
+ normalizedPath,
413
+ mediaHint?.mimeType,
414
+ );
348
415
  if (!mimeType.startsWith('image/')) {
349
416
  throw new Error(`unsupported local image type: ${mimeType}`);
350
417
  }
@@ -355,9 +422,13 @@ async function readVisionImageFromLocalPath(localPath: string): Promise<{ buffer
355
422
  };
356
423
  }
357
424
 
358
- async function readVisionImageFromUrl(rawUrl: string): Promise<{ buffer: Buffer; mimeType: string; source: string }> {
425
+ async function readVisionImageFromUrl(
426
+ rawUrl: string,
427
+ ): Promise<{ buffer: Buffer; mimeType: string; source: string }> {
359
428
  if (!isSafeDiscordCdnUrl(rawUrl)) {
360
- throw new Error('remote image URL is blocked (only Discord CDN HTTPS URLs are allowed)');
429
+ throw new Error(
430
+ 'remote image URL is blocked (only Discord CDN HTTPS URLs are allowed)',
431
+ );
361
432
  }
362
433
  const controller = new AbortController();
363
434
  const timer = setTimeout(() => controller.abort(), VISION_FETCH_TIMEOUT_MS);
@@ -373,13 +444,23 @@ async function readVisionImageFromUrl(rawUrl: string): Promise<{ buffer: Buffer;
373
444
  if (!mimeType.startsWith('image/')) {
374
445
  throw new Error(`remote URL is not an image (${mimeType || 'unknown'})`);
375
446
  }
376
- const contentLength = Number.parseInt(response.headers.get('content-length') || '', 10);
377
- if (Number.isFinite(contentLength) && contentLength > VISION_IMAGE_MAX_BYTES) {
378
- throw new Error(`remote image exceeds max size (${VISION_IMAGE_MAX_BYTES} bytes)`);
447
+ const contentLength = Number.parseInt(
448
+ response.headers.get('content-length') || '',
449
+ 10,
450
+ );
451
+ if (
452
+ Number.isFinite(contentLength) &&
453
+ contentLength > VISION_IMAGE_MAX_BYTES
454
+ ) {
455
+ throw new Error(
456
+ `remote image exceeds max size (${VISION_IMAGE_MAX_BYTES} bytes)`,
457
+ );
379
458
  }
380
459
  const buffer = Buffer.from(await response.arrayBuffer());
381
460
  if (buffer.length > VISION_IMAGE_MAX_BYTES) {
382
- throw new Error(`remote image exceeds max size (${VISION_IMAGE_MAX_BYTES} bytes)`);
461
+ throw new Error(
462
+ `remote image exceeds max size (${VISION_IMAGE_MAX_BYTES} bytes)`,
463
+ );
383
464
  }
384
465
  return {
385
466
  buffer,
@@ -392,14 +473,21 @@ async function readVisionImageFromUrl(rawUrl: string): Promise<{ buffer: Buffer;
392
473
  }
393
474
 
394
475
  function visionModelContextError(): string | null {
395
- if (!currentModelApiKey) return 'vision_analyze is not configured: missing API key context.';
396
- if (!currentModelBaseUrl) return 'vision_analyze is not configured: missing base URL context.';
397
- if (!currentModelName) return 'vision_analyze is not configured: missing model context.';
398
- if (!currentChatbotId) return 'vision_analyze is not configured: missing chatbot_id context.';
476
+ if (!currentModelApiKey)
477
+ return 'vision_analyze is not configured: missing API key context.';
478
+ if (!currentModelBaseUrl)
479
+ return 'vision_analyze is not configured: missing base URL context.';
480
+ if (!currentModelName)
481
+ return 'vision_analyze is not configured: missing model context.';
482
+ if (!currentChatbotId)
483
+ return 'vision_analyze is not configured: missing chatbot_id context.';
399
484
  return null;
400
485
  }
401
486
 
402
- async function callVisionModel(question: string, imageDataUrl: string): Promise<{ model: string; analysis: string }> {
487
+ async function callVisionModel(
488
+ question: string,
489
+ imageDataUrl: string,
490
+ ): Promise<{ model: string; analysis: string }> {
403
491
  const contextError = visionModelContextError();
404
492
  if (contextError) throw new Error(contextError);
405
493
 
@@ -427,8 +515,11 @@ async function callVisionModel(question: string, imageDataUrl: string): Promise<
427
515
 
428
516
  const rawText = await response.text();
429
517
  if (!response.ok) {
430
- const detail = rawText.length > 600 ? `${rawText.slice(0, 600)}...` : rawText;
431
- throw new Error(`vision API request failed (${response.status}): ${detail}`);
518
+ const detail =
519
+ rawText.length > 600 ? `${rawText.slice(0, 600)}...` : rawText;
520
+ throw new Error(
521
+ `vision API request failed (${response.status}): ${detail}`,
522
+ );
432
523
  }
433
524
 
434
525
  let parsed: unknown;
@@ -454,15 +545,25 @@ async function callVisionModel(question: string, imageDataUrl: string): Promise<
454
545
  };
455
546
  }
456
547
 
457
- async function runVisionAnalyze(args: Record<string, unknown>): Promise<string> {
548
+ async function runVisionAnalyze(
549
+ args: Record<string, unknown>,
550
+ ): Promise<string> {
458
551
  const question = readStringValue(args.question);
459
552
  if (!question) return 'Error: question is required';
460
553
 
461
- const imageRef = readStringValue(args.image_url) || readStringValue(args.imageUrl) || readStringValue(args.path);
462
- const fallbackUrl = readStringValue(args.fallback_url) || readStringValue(args.fallbackUrl) || readStringValue(args.original_url);
554
+ const imageRef =
555
+ readStringValue(args.image_url) ||
556
+ readStringValue(args.imageUrl) ||
557
+ readStringValue(args.path);
558
+ const fallbackUrl =
559
+ readStringValue(args.fallback_url) ||
560
+ readStringValue(args.fallbackUrl) ||
561
+ readStringValue(args.original_url);
463
562
  if (!imageRef) return 'Error: image_url is required';
464
563
 
465
- const candidates = [imageRef, fallbackUrl].filter((value): value is string => Boolean(value));
564
+ const candidates = [imageRef, fallbackUrl].filter((value): value is string =>
565
+ Boolean(value),
566
+ );
466
567
  const errors: string[] = [];
467
568
  for (const candidate of candidates) {
468
569
  try {
@@ -472,14 +573,18 @@ async function runVisionAnalyze(args: Record<string, unknown>): Promise<string>
472
573
  : await readVisionImageFromLocalPath(candidate);
473
574
  const dataUrl = `data:${image.mimeType};base64,${image.buffer.toString('base64')}`;
474
575
  const vision = await callVisionModel(question, dataUrl);
475
- return JSON.stringify({
476
- success: true,
477
- model: vision.model,
478
- analysis: vision.analysis,
479
- source: image.source,
480
- mime_type: image.mimeType,
481
- size_bytes: image.buffer.length,
482
- }, null, 2);
576
+ return JSON.stringify(
577
+ {
578
+ success: true,
579
+ model: vision.model,
580
+ analysis: vision.analysis,
581
+ source: image.source,
582
+ mime_type: image.mimeType,
583
+ size_bytes: image.buffer.length,
584
+ },
585
+ null,
586
+ 2,
587
+ );
483
588
  } catch (err) {
484
589
  const detail = err instanceof Error ? err.message : String(err);
485
590
  errors.push(`${candidate}: ${detail}`);
@@ -502,11 +607,17 @@ const READ_MAX_BYTES = 50 * 1024;
502
607
 
503
608
  function abbreviatePreview(text: string): string {
504
609
  const lines = text.split('\n');
505
- const truncated = lines.slice(0, PREVIEW_MAX_OUTPUT_LINES).map((line) =>
506
- line.length > PREVIEW_MAX_LINE_LENGTH ? line.slice(0, PREVIEW_MAX_LINE_LENGTH) + '...' : line
507
- );
610
+ const truncated = lines
611
+ .slice(0, PREVIEW_MAX_OUTPUT_LINES)
612
+ .map((line) =>
613
+ line.length > PREVIEW_MAX_LINE_LENGTH
614
+ ? `${line.slice(0, PREVIEW_MAX_LINE_LENGTH)}...`
615
+ : line,
616
+ );
508
617
  if (lines.length > PREVIEW_MAX_OUTPUT_LINES) {
509
- truncated.push(`... (${lines.length - PREVIEW_MAX_OUTPUT_LINES} more lines)`);
618
+ truncated.push(
619
+ `... (${lines.length - PREVIEW_MAX_OUTPUT_LINES} more lines)`,
620
+ );
510
621
  }
511
622
  return truncated.join('\n');
512
623
  }
@@ -525,7 +636,11 @@ function formatBytes(bytes: number): string {
525
636
  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
526
637
  }
527
638
 
528
- function truncateReadContent(content: string, maxLines = READ_MAX_LINES, maxBytes = READ_MAX_BYTES): ReadTruncationResult {
639
+ function truncateReadContent(
640
+ content: string,
641
+ maxLines = READ_MAX_LINES,
642
+ maxBytes = READ_MAX_BYTES,
643
+ ): ReadTruncationResult {
529
644
  const lines = content.split('\n');
530
645
  const totalBytes = Buffer.byteLength(content, 'utf-8');
531
646
  if (lines.length <= maxLines && totalBytes <= maxBytes) {
@@ -576,7 +691,11 @@ function truncateReadContent(content: string, maxLines = READ_MAX_LINES, maxByte
576
691
  function formatBashOutput(content: string): string {
577
692
  const raw = content || '(no output)';
578
693
  const totalLines = raw.split('\n').length;
579
- const truncation = truncateReadContent(raw, BASH_MAX_OUTPUT_LINES, BASH_MAX_OUTPUT_BYTES);
694
+ const truncation = truncateReadContent(
695
+ raw,
696
+ BASH_MAX_OUTPUT_LINES,
697
+ BASH_MAX_OUTPUT_BYTES,
698
+ );
580
699
  if (!truncation.truncated) return raw;
581
700
 
582
701
  if (truncation.firstLineExceedsLimit) {
@@ -675,11 +794,13 @@ function resolveMemoryFilePath(args: Record<string, unknown>): string | null {
675
794
  normalizeMemoryFilePath(args.path);
676
795
  if (direct) return direct;
677
796
 
678
- const target = typeof args.target === 'string' ? args.target.trim().toLowerCase() : '';
797
+ const target =
798
+ typeof args.target === 'string' ? args.target.trim().toLowerCase() : '';
679
799
  if (target === 'memory') return 'MEMORY.md';
680
800
  if (target === 'user') return 'USER.md';
681
801
  if (target === 'daily') {
682
- const date = typeof args.date === 'string' ? normalizeDateStamp(args.date) : null;
802
+ const date =
803
+ typeof args.date === 'string' ? normalizeDateStamp(args.date) : null;
683
804
  return `memory/${date || currentDateStamp()}.md`;
684
805
  }
685
806
 
@@ -773,12 +894,14 @@ function collectTranscriptRows(filePath: string): TranscriptRow[] {
773
894
  }
774
895
  parsed.push({
775
896
  sessionId: row.sessionId,
776
- channelId: typeof row.channelId === 'string' ? row.channelId : undefined,
897
+ channelId:
898
+ typeof row.channelId === 'string' ? row.channelId : undefined,
777
899
  role: row.role,
778
900
  userId: typeof row.userId === 'string' ? row.userId : undefined,
779
901
  username: row.username == null ? null : String(row.username),
780
902
  content: row.content,
781
- createdAt: typeof row.createdAt === 'string' ? row.createdAt : undefined,
903
+ createdAt:
904
+ typeof row.createdAt === 'string' ? row.createdAt : undefined,
782
905
  });
783
906
  } catch {
784
907
  // Skip malformed row
@@ -787,7 +910,11 @@ function collectTranscriptRows(filePath: string): TranscriptRow[] {
787
910
  return parsed;
788
911
  }
789
912
 
790
- function scoreTranscript(rows: TranscriptRow[], query: string, roleFilter: Set<string> | null): number {
913
+ function scoreTranscript(
914
+ rows: TranscriptRow[],
915
+ query: string,
916
+ roleFilter: Set<string> | null,
917
+ ): number {
791
918
  const terms = query
792
919
  .toLowerCase()
793
920
  .split(/\s+/)
@@ -808,7 +935,11 @@ function scoreTranscript(rows: TranscriptRow[], query: string, roleFilter: Set<s
808
935
  return score;
809
936
  }
810
937
 
811
- function findMatchIndexes(rows: TranscriptRow[], query: string, roleFilter: Set<string> | null): number[] {
938
+ function findMatchIndexes(
939
+ rows: TranscriptRow[],
940
+ query: string,
941
+ roleFilter: Set<string> | null,
942
+ ): number[] {
812
943
  const lower = query.toLowerCase();
813
944
  const terms = lower
814
945
  .split(/\s+/)
@@ -821,14 +952,20 @@ function findMatchIndexes(rows: TranscriptRow[], query: string, roleFilter: Set<
821
952
  const role = rows[i].role.toLowerCase();
822
953
  if (roleFilter && !roleFilter.has(role)) continue;
823
954
  const content = rows[i].content.toLowerCase();
824
- if (content.includes(lower) || terms.some((term) => content.includes(term))) {
955
+ if (
956
+ content.includes(lower) ||
957
+ terms.some((term) => content.includes(term))
958
+ ) {
825
959
  indexes.push(i);
826
960
  }
827
961
  }
828
962
  return indexes;
829
963
  }
830
964
 
831
- function summarizeSessionCandidate(candidate: SessionSearchCandidate, query: string): Record<string, unknown> {
965
+ function summarizeSessionCandidate(
966
+ candidate: SessionSearchCandidate,
967
+ query: string,
968
+ ): Record<string, unknown> {
832
969
  const rows = candidate.rows;
833
970
  const snippets: string[] = [];
834
971
  const seenIndexes = new Set<number>();
@@ -858,12 +995,17 @@ function summarizeSessionCandidate(candidate: SessionSearchCandidate, query: str
858
995
  }
859
996
  }
860
997
 
861
- const firstTs = rows.find((row) => typeof row.createdAt === 'string')?.createdAt || null;
862
- const lastTs = [...rows].reverse().find((row) => typeof row.createdAt === 'string')?.createdAt || null;
998
+ const firstTs =
999
+ rows.find((row) => typeof row.createdAt === 'string')?.createdAt || null;
1000
+ const lastTs =
1001
+ [...rows].reverse().find((row) => typeof row.createdAt === 'string')
1002
+ ?.createdAt || null;
863
1003
  const summaryParts = [
864
1004
  `Matched ${candidate.matchIndexes.length} turn(s) for "${query}".`,
865
1005
  userMatches.length > 0 ? `User focus: ${userMatches.join(' | ')}` : '',
866
- assistantMatches.length > 0 ? `Assistant outcomes: ${assistantMatches.join(' | ')}` : '',
1006
+ assistantMatches.length > 0
1007
+ ? `Assistant outcomes: ${assistantMatches.join(' | ')}`
1008
+ : '',
867
1009
  ].filter(Boolean);
868
1010
 
869
1011
  return {
@@ -876,7 +1018,10 @@ function summarizeSessionCandidate(candidate: SessionSearchCandidate, query: str
876
1018
  };
877
1019
  }
878
1020
 
879
- export async function executeTool(name: string, argsJson: string): Promise<string> {
1021
+ export async function executeTool(
1022
+ name: string,
1023
+ argsJson: string,
1024
+ ): Promise<string> {
880
1025
  try {
881
1026
  const args = JSON.parse(argsJson);
882
1027
 
@@ -886,19 +1031,25 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
886
1031
  return 'Error: path is required';
887
1032
  }
888
1033
  const filePath = safeJoin(args.path);
889
- if (!fs.existsSync(filePath)) return `Error: File not found: ${args.path}`;
1034
+ if (!fs.existsSync(filePath))
1035
+ return `Error: File not found: ${args.path}`;
890
1036
  const content = fs.readFileSync(filePath, 'utf-8');
891
1037
  const lines = content.split('\n');
892
1038
  const totalFileLines = lines.length;
893
1039
 
894
- const rawOffset = typeof args.offset === 'number' && Number.isFinite(args.offset) ? args.offset : 1;
1040
+ const rawOffset =
1041
+ typeof args.offset === 'number' && Number.isFinite(args.offset)
1042
+ ? args.offset
1043
+ : 1;
895
1044
  const startLine = Math.max(1, Math.floor(rawOffset));
896
1045
  if (startLine > totalFileLines) {
897
1046
  return `Error: Offset ${startLine} is beyond end of file (${totalFileLines} lines total)`;
898
1047
  }
899
1048
 
900
1049
  const rawLimit =
901
- typeof args.limit === 'number' && Number.isFinite(args.limit) && args.limit > 0
1050
+ typeof args.limit === 'number' &&
1051
+ Number.isFinite(args.limit) &&
1052
+ args.limit > 0
902
1053
  ? Math.floor(args.limit)
903
1054
  : undefined;
904
1055
 
@@ -913,7 +1064,9 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
913
1064
  const truncation = truncateReadContent(selectedContent);
914
1065
  if (truncation.firstLineExceedsLimit) {
915
1066
  const firstSelectedLine = selected[0] ?? '';
916
- const firstLineSize = formatBytes(Buffer.byteLength(firstSelectedLine, 'utf-8'));
1067
+ const firstLineSize = formatBytes(
1068
+ Buffer.byteLength(firstSelectedLine, 'utf-8'),
1069
+ );
917
1070
  return `[Line ${startLine} is ${firstLineSize}, exceeds ${formatBytes(READ_MAX_BYTES)} limit. Use bash: sed -n '${startLine}p' ${args.path} | head -c ${READ_MAX_BYTES}]`;
918
1071
  }
919
1072
 
@@ -947,7 +1100,8 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
947
1100
 
948
1101
  case 'edit': {
949
1102
  const filePath = safeJoin(args.path);
950
- if (!fs.existsSync(filePath)) return `Error: File not found: ${args.path}`;
1103
+ if (!fs.existsSync(filePath))
1104
+ return `Error: File not found: ${args.path}`;
951
1105
  let content = fs.readFileSync(filePath, 'utf-8');
952
1106
  const count = args.count || 1;
953
1107
  for (let i = 0; i < count; i++) {
@@ -956,7 +1110,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
956
1110
  if (i === 0) return `Error: Text not found in ${args.path}`;
957
1111
  break;
958
1112
  }
959
- content = content.slice(0, idx) + args.new + content.slice(idx + args.old.length);
1113
+ content =
1114
+ content.slice(0, idx) +
1115
+ args.new +
1116
+ content.slice(idx + args.old.length);
960
1117
  }
961
1118
  fs.writeFileSync(filePath, content);
962
1119
  return `Edited ${args.path} (${count} replacement${count > 1 ? 's' : ''})`;
@@ -964,7 +1121,8 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
964
1121
 
965
1122
  case 'delete': {
966
1123
  const filePath = safeJoin(args.path);
967
- if (!fs.existsSync(filePath)) return `Error: File not found: ${args.path}`;
1124
+ if (!fs.existsSync(filePath))
1125
+ return `Error: File not found: ${args.path}`;
968
1126
  fs.unlinkSync(filePath);
969
1127
  return `Deleted ${args.path}`;
970
1128
  }
@@ -977,7 +1135,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
977
1135
  const result = execSync(cmd, { timeout: 10000, encoding: 'utf-8' });
978
1136
  if (!result.trim()) return 'No files found.';
979
1137
  // Convert absolute paths to relative
980
- const files = result.trim().split('\n').map((f) => f.replace('/workspace/', ''));
1138
+ const files = result
1139
+ .trim()
1140
+ .split('\n')
1141
+ .map((f) => f.replace('/workspace/', ''));
981
1142
  return abbreviatePreview(files.join('\n'));
982
1143
  } catch {
983
1144
  return 'No files found.';
@@ -1022,23 +1183,28 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1022
1183
  message?: string;
1023
1184
  };
1024
1185
 
1025
- const stdout = typeof execErr.stdout === 'string'
1026
- ? execErr.stdout
1027
- : Buffer.isBuffer(execErr.stdout)
1028
- ? execErr.stdout.toString('utf-8')
1029
- : '';
1030
- const stderr = typeof execErr.stderr === 'string'
1031
- ? execErr.stderr
1032
- : Buffer.isBuffer(execErr.stderr)
1033
- ? execErr.stderr.toString('utf-8')
1034
- : '';
1035
- const combinedOutput = [stdout, stderr].filter(Boolean).join('\n').trim();
1186
+ const stdout =
1187
+ typeof execErr.stdout === 'string'
1188
+ ? execErr.stdout
1189
+ : Buffer.isBuffer(execErr.stdout)
1190
+ ? execErr.stdout.toString('utf-8')
1191
+ : '';
1192
+ const stderr =
1193
+ typeof execErr.stderr === 'string'
1194
+ ? execErr.stderr
1195
+ : Buffer.isBuffer(execErr.stderr)
1196
+ ? execErr.stderr.toString('utf-8')
1197
+ : '';
1198
+ const combinedOutput = [stdout, stderr]
1199
+ .filter(Boolean)
1200
+ .join('\n')
1201
+ .trim();
1036
1202
 
1037
1203
  const errorMessage = execErr.message || 'Command failed';
1038
1204
  const timeoutLikely =
1039
- execErr.code === 'ETIMEDOUT'
1040
- || /ETIMEDOUT|timed out/i.test(errorMessage)
1041
- || (execErr.signal === 'SIGTERM' && /spawnSync/i.test(errorMessage));
1205
+ execErr.code === 'ETIMEDOUT' ||
1206
+ /ETIMEDOUT|timed out/i.test(errorMessage) ||
1207
+ (execErr.signal === 'SIGTERM' && /spawnSync/i.test(errorMessage));
1042
1208
  const summary = timeoutLikely
1043
1209
  ? `Command timed out after ${timeoutMs}ms`
1044
1210
  : errorMessage;
@@ -1049,7 +1215,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1049
1215
  }
1050
1216
 
1051
1217
  case 'memory': {
1052
- const action = typeof args.action === 'string' ? args.action.trim().toLowerCase() : 'read';
1218
+ const action =
1219
+ typeof args.action === 'string'
1220
+ ? args.action.trim().toLowerCase()
1221
+ : 'read';
1053
1222
  const relativePath = resolveMemoryFilePath(args);
1054
1223
  if (!relativePath) {
1055
1224
  return 'Error: memory file_path must be MEMORY.md, USER.md, or memory/YYYY-MM-DD.md';
@@ -1065,7 +1234,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1065
1234
  }
1066
1235
 
1067
1236
  if (action === 'search') {
1068
- const query = typeof args.query === 'string' ? args.query.trim().toLowerCase() : '';
1237
+ const query =
1238
+ typeof args.query === 'string'
1239
+ ? args.query.trim().toLowerCase()
1240
+ : '';
1069
1241
  if (!query) return 'Error: query is required for memory search';
1070
1242
  const files = listMemoryFiles();
1071
1243
  const matches: string[] = [];
@@ -1085,7 +1257,9 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1085
1257
  }
1086
1258
  if (matches.length >= 40) break;
1087
1259
  }
1088
- return matches.length > 0 ? matches.join('\n') : `No memory matches for "${query}".`;
1260
+ return matches.length > 0
1261
+ ? matches.join('\n')
1262
+ : `No memory matches for "${query}".`;
1089
1263
  }
1090
1264
 
1091
1265
  if (action === 'read') {
@@ -1097,11 +1271,14 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1097
1271
  }
1098
1272
 
1099
1273
  if (action === 'append') {
1100
- const content = typeof args.content === 'string' ? args.content.trim() : '';
1274
+ const content =
1275
+ typeof args.content === 'string' ? args.content.trim() : '';
1101
1276
  if (!content) return 'Error: content is required for memory append';
1102
1277
 
1103
1278
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
1104
- const existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : '';
1279
+ const existing = fs.existsSync(filePath)
1280
+ ? fs.readFileSync(filePath, 'utf-8')
1281
+ : '';
1105
1282
  let next = existing.replace(/\s+$/, '');
1106
1283
  if (next.length > 0) next += '\n\n';
1107
1284
  next += `${content}\n`;
@@ -1125,12 +1302,16 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1125
1302
  }
1126
1303
 
1127
1304
  if (action === 'replace') {
1128
- const oldText = typeof args.old_text === 'string' ? args.old_text : '';
1129
- const newText = typeof args.new_text === 'string' ? args.new_text : '';
1305
+ const oldText =
1306
+ typeof args.old_text === 'string' ? args.old_text : '';
1307
+ const newText =
1308
+ typeof args.new_text === 'string' ? args.new_text : '';
1130
1309
  if (!oldText) return 'Error: old_text is required for memory replace';
1131
- if (!fs.existsSync(filePath)) return `Error: File not found: ${relativePath}`;
1310
+ if (!fs.existsSync(filePath))
1311
+ return `Error: File not found: ${relativePath}`;
1132
1312
  const content = fs.readFileSync(filePath, 'utf-8');
1133
- if (!content.includes(oldText)) return `Error: old_text not found in ${relativePath}`;
1313
+ if (!content.includes(oldText))
1314
+ return `Error: old_text not found in ${relativePath}`;
1134
1315
  const next = content.replace(oldText, newText);
1135
1316
  const limit = memoryCharLimit(relativePath);
1136
1317
  if (next.length > limit) {
@@ -1141,11 +1322,14 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1141
1322
  }
1142
1323
 
1143
1324
  if (action === 'remove') {
1144
- const oldText = typeof args.old_text === 'string' ? args.old_text : '';
1325
+ const oldText =
1326
+ typeof args.old_text === 'string' ? args.old_text : '';
1145
1327
  if (!oldText) return 'Error: old_text is required for memory remove';
1146
- if (!fs.existsSync(filePath)) return `Error: File not found: ${relativePath}`;
1328
+ if (!fs.existsSync(filePath))
1329
+ return `Error: File not found: ${relativePath}`;
1147
1330
  const content = fs.readFileSync(filePath, 'utf-8');
1148
- if (!content.includes(oldText)) return `Error: old_text not found in ${relativePath}`;
1331
+ if (!content.includes(oldText))
1332
+ return `Error: old_text not found in ${relativePath}`;
1149
1333
  fs.writeFileSync(filePath, content.replace(oldText, ''), 'utf-8');
1150
1334
  return `Removed matching text from ${relativePath}`;
1151
1335
  }
@@ -1163,10 +1347,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1163
1347
 
1164
1348
  if (action === 'read') {
1165
1349
  const channelId =
1166
- readStringValue(args.channelId)
1167
- || readStringValue(args.to)
1168
- || readStringValue(args.target)
1169
- || gatewayChannelId;
1350
+ readStringValue(args.channelId) ||
1351
+ readStringValue(args.to) ||
1352
+ readStringValue(args.target) ||
1353
+ gatewayChannelId;
1170
1354
  if (!channelId) {
1171
1355
  return 'Error: channelId is required for message action "read".';
1172
1356
  }
@@ -1186,10 +1370,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1186
1370
  if (action === 'member-info') {
1187
1371
  const guildId = readStringValue(args.guildId);
1188
1372
  const userId =
1189
- readStringValue(args.userId)
1190
- || readStringValue(args.memberId)
1191
- || readStringValue(args.user)
1192
- || readStringValue(args.username);
1373
+ readStringValue(args.userId) ||
1374
+ readStringValue(args.memberId) ||
1375
+ readStringValue(args.user) ||
1376
+ readStringValue(args.username);
1193
1377
  if (!guildId) {
1194
1378
  return 'Error: guildId is required for message action "member-info".';
1195
1379
  }
@@ -1202,10 +1386,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1202
1386
 
1203
1387
  if (action === 'channel-info') {
1204
1388
  const channelId =
1205
- readStringValue(args.channelId)
1206
- || readStringValue(args.to)
1207
- || readStringValue(args.target)
1208
- || gatewayChannelId;
1389
+ readStringValue(args.channelId) ||
1390
+ readStringValue(args.to) ||
1391
+ readStringValue(args.target) ||
1392
+ gatewayChannelId;
1209
1393
  if (!channelId) {
1210
1394
  return 'Error: channelId is required for message action "channel-info".';
1211
1395
  }
@@ -1223,19 +1407,26 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1223
1407
  typeof args.limit === 'number' && Number.isFinite(args.limit)
1224
1408
  ? Math.floor(args.limit)
1225
1409
  : 3;
1226
- const limit = Math.max(1, Math.min(requestedLimit, SESSION_SEARCH_MAX_RESULTS));
1410
+ const limit = Math.max(
1411
+ 1,
1412
+ Math.min(requestedLimit, SESSION_SEARCH_MAX_RESULTS),
1413
+ );
1227
1414
  const includeCurrent = args.include_current === true;
1228
1415
  const roleFilter = parseRoleFilter(args.role_filter);
1229
1416
 
1230
1417
  const transcriptDir = safeJoin(SESSION_TRANSCRIPTS_DIR);
1231
1418
  if (!fs.existsSync(transcriptDir)) {
1232
- return JSON.stringify({
1233
- success: true,
1234
- query,
1235
- count: 0,
1236
- results: [],
1237
- message: 'No historical transcripts found yet.',
1238
- }, null, 2);
1419
+ return JSON.stringify(
1420
+ {
1421
+ success: true,
1422
+ query,
1423
+ count: 0,
1424
+ results: [],
1425
+ message: 'No historical transcripts found yet.',
1426
+ },
1427
+ null,
1428
+ 2,
1429
+ );
1239
1430
  }
1240
1431
 
1241
1432
  const files = fs
@@ -1249,8 +1440,14 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1249
1440
  const rows = collectTranscriptRows(filePath);
1250
1441
  if (rows.length === 0) continue;
1251
1442
 
1252
- const sessionId = rows[0].sessionId || filename.replace(/\.jsonl$/, '');
1253
- if (!includeCurrent && currentSessionId && sessionId === currentSessionId) continue;
1443
+ const sessionId =
1444
+ rows[0].sessionId || filename.replace(/\.jsonl$/, '');
1445
+ if (
1446
+ !includeCurrent &&
1447
+ currentSessionId &&
1448
+ sessionId === currentSessionId
1449
+ )
1450
+ continue;
1254
1451
 
1255
1452
  const matchIndexes = findMatchIndexes(rows, query, roleFilter);
1256
1453
  if (matchIndexes.length === 0) continue;
@@ -1273,15 +1470,21 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1273
1470
  });
1274
1471
 
1275
1472
  const top = candidates.slice(0, limit);
1276
- const results = top.map((candidate) => summarizeSessionCandidate(candidate, query));
1473
+ const results = top.map((candidate) =>
1474
+ summarizeSessionCandidate(candidate, query),
1475
+ );
1277
1476
 
1278
- return JSON.stringify({
1279
- success: true,
1280
- query,
1281
- count: results.length,
1282
- sessions_searched: candidates.length,
1283
- results,
1284
- }, null, 2);
1477
+ return JSON.stringify(
1478
+ {
1479
+ success: true,
1480
+ query,
1481
+ count: results.length,
1482
+ sessions_searched: candidates.length,
1483
+ results,
1484
+ },
1485
+ null,
1486
+ 2,
1487
+ );
1285
1488
  }
1286
1489
 
1287
1490
  case 'web_fetch': {
@@ -1294,7 +1497,9 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1294
1497
  const meta = `[${result.extractor}] ${result.finalUrl} (${result.status}, ${result.tookMs}ms)`;
1295
1498
  const lines = [meta];
1296
1499
  if (result.escalationHint) {
1297
- lines.push(`Escalation hint: ${result.escalationHint} (retry with browser_navigate for this URL).`);
1500
+ lines.push(
1501
+ `Escalation hint: ${result.escalationHint} (retry with browser_navigate for this URL).`,
1502
+ );
1298
1503
  }
1299
1504
  if (result.warning) {
1300
1505
  lines.push(`Warning: ${result.warning}`);
@@ -1311,6 +1516,7 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1311
1516
  case 'browser_snapshot':
1312
1517
  case 'browser_click':
1313
1518
  case 'browser_type':
1519
+ case 'browser_upload':
1314
1520
  case 'browser_press':
1315
1521
  case 'browser_scroll':
1316
1522
  case 'browser_back':
@@ -1321,7 +1527,11 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1321
1527
  case 'browser_console':
1322
1528
  case 'browser_network':
1323
1529
  case 'browser_close': {
1324
- return await executeBrowserTool(name, args, currentSessionId || 'default');
1530
+ return await executeBrowserTool(
1531
+ name,
1532
+ args,
1533
+ currentSessionId || 'default',
1534
+ );
1325
1535
  }
1326
1536
 
1327
1537
  case 'cron': {
@@ -1335,7 +1545,8 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1335
1545
  else if (t.everyMs) {
1336
1546
  const secs = t.everyMs / 1000;
1337
1547
  if (secs < 120) schedule = `every ${secs}s`;
1338
- else if (secs < 7200) schedule = `every ${Math.round(secs / 60)}m`;
1548
+ else if (secs < 7200)
1549
+ schedule = `every ${Math.round(secs / 60)}m`;
1339
1550
  else schedule = `every ${Math.round(secs / 3600)}h`;
1340
1551
  } else schedule = t.cronExpr;
1341
1552
  const status = t.enabled ? 'enabled' : 'disabled';
@@ -1349,22 +1560,37 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1349
1560
 
1350
1561
  if (args.at) {
1351
1562
  const runAt = new Date(args.at);
1352
- if (isNaN(runAt.getTime())) return `Error: invalid ISO-8601 timestamp: ${args.at}`;
1353
- if (runAt.getTime() <= Date.now()) return `Error: timestamp must be in the future: ${args.at}`;
1354
- pendingSchedules.push({ action: 'add', runAt: runAt.toISOString(), prompt: args.prompt });
1563
+ if (Number.isNaN(runAt.getTime()))
1564
+ return `Error: invalid ISO-8601 timestamp: ${args.at}`;
1565
+ if (runAt.getTime() <= Date.now())
1566
+ return `Error: timestamp must be in the future: ${args.at}`;
1567
+ pendingSchedules.push({
1568
+ action: 'add',
1569
+ runAt: runAt.toISOString(),
1570
+ prompt: args.prompt,
1571
+ });
1355
1572
  return `Scheduled one-shot task at ${runAt.toISOString()}: ${args.prompt}`;
1356
1573
  }
1357
1574
 
1358
1575
  if (args.cron) {
1359
- pendingSchedules.push({ action: 'add', cronExpr: args.cron, prompt: args.prompt });
1576
+ pendingSchedules.push({
1577
+ action: 'add',
1578
+ cronExpr: args.cron,
1579
+ prompt: args.prompt,
1580
+ });
1360
1581
  return `Scheduled recurring task with cron "${args.cron}": ${args.prompt}`;
1361
1582
  }
1362
1583
 
1363
1584
  if (args.every) {
1364
1585
  const secs = Number(args.every);
1365
- if (isNaN(secs) || secs < 10) return 'Error: "every" must be a number of seconds >= 10';
1586
+ if (Number.isNaN(secs) || secs < 10)
1587
+ return 'Error: "every" must be a number of seconds >= 10';
1366
1588
  const everyMs = Math.round(secs * 1000);
1367
- pendingSchedules.push({ action: 'add', everyMs, prompt: args.prompt });
1589
+ pendingSchedules.push({
1590
+ action: 'add',
1591
+ everyMs,
1592
+ prompt: args.prompt,
1593
+ });
1368
1594
  return `Scheduled interval task every ${secs}s: ${args.prompt}`;
1369
1595
  }
1370
1596
 
@@ -1385,14 +1611,21 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1385
1611
  return `Error: delegation limit reached for this turn (${MAX_PENDING_DELEGATIONS}).`;
1386
1612
  }
1387
1613
 
1388
- const modeRaw = typeof args.mode === 'string' ? args.mode.trim().toLowerCase() : '';
1389
- if (modeRaw && modeRaw !== 'single' && modeRaw !== 'parallel' && modeRaw !== 'chain') {
1614
+ const modeRaw =
1615
+ typeof args.mode === 'string' ? args.mode.trim().toLowerCase() : '';
1616
+ if (
1617
+ modeRaw &&
1618
+ modeRaw !== 'single' &&
1619
+ modeRaw !== 'parallel' &&
1620
+ modeRaw !== 'chain'
1621
+ ) {
1390
1622
  return 'Error: mode must be one of "single", "parallel", or "chain".';
1391
1623
  }
1392
1624
 
1393
1625
  const label = typeof args.label === 'string' ? args.label.trim() : '';
1394
1626
  const model = typeof args.model === 'string' ? args.model.trim() : '';
1395
- const prompt = typeof args.prompt === 'string' ? args.prompt.trim() : '';
1627
+ const prompt =
1628
+ typeof args.prompt === 'string' ? args.prompt.trim() : '';
1396
1629
  const tasksResult = normalizeDelegationTaskList({
1397
1630
  raw: args.tasks,
1398
1631
  fallbackModel: model || undefined,
@@ -1421,7 +1654,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1421
1654
  mode = 'single';
1422
1655
  }
1423
1656
 
1424
- if ((hasTasks ? 1 : 0) + (hasChain ? 1 : 0) + (hasPrompt ? 1 : 0) > 1 && !modeRaw) {
1657
+ if (
1658
+ (hasTasks ? 1 : 0) + (hasChain ? 1 : 0) + (hasPrompt ? 1 : 0) > 1 &&
1659
+ !modeRaw
1660
+ ) {
1425
1661
  return 'Error: provide one delegation mode payload: "prompt", "tasks", or "chain".';
1426
1662
  }
1427
1663
 
@@ -1439,8 +1675,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1439
1675
  };
1440
1676
  summary = label ? `${label}: ${prompt}` : prompt;
1441
1677
  } else if (mode === 'parallel') {
1442
- if (!hasTasks) return 'Error: tasks are required for mode="parallel".';
1443
- if (hasPrompt || hasChain) return 'Error: mode="parallel" accepts only "tasks" plus optional label/model.';
1678
+ if (!hasTasks)
1679
+ return 'Error: tasks are required for mode="parallel".';
1680
+ if (hasPrompt || hasChain)
1681
+ return 'Error: mode="parallel" accepts only "tasks" plus optional label/model.';
1444
1682
  effect = {
1445
1683
  action: 'delegate',
1446
1684
  mode,
@@ -1451,7 +1689,8 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1451
1689
  summary = `${tasksResult.tasks.length} parallel task(s)`;
1452
1690
  } else {
1453
1691
  if (!hasChain) return 'Error: chain is required for mode="chain".';
1454
- if (hasPrompt || hasTasks) return 'Error: mode="chain" accepts only "chain" plus optional label/model.';
1692
+ if (hasPrompt || hasTasks)
1693
+ return 'Error: mode="chain" accepts only "chain" plus optional label/model.';
1455
1694
  effect = {
1456
1695
  action: 'delegate',
1457
1696
  mode,
@@ -1486,8 +1725,16 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1486
1725
  type: 'object',
1487
1726
  properties: {
1488
1727
  path: { type: 'string', description: 'Path to the file to read' },
1489
- offset: { type: 'number', description: 'Line number to start reading from (1-indexed, default: 1)' },
1490
- limit: { type: 'number', description: 'Maximum number of lines to read before truncation logic (optional)' },
1728
+ offset: {
1729
+ type: 'number',
1730
+ description:
1731
+ 'Line number to start reading from (1-indexed, default: 1)',
1732
+ },
1733
+ limit: {
1734
+ type: 'number',
1735
+ description:
1736
+ 'Maximum number of lines to read before truncation logic (optional)',
1737
+ },
1491
1738
  },
1492
1739
  required: ['path'],
1493
1740
  },
@@ -1521,7 +1768,10 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1521
1768
  path: { type: 'string', description: 'Path to the file to edit' },
1522
1769
  old: { type: 'string', description: 'Text to find and replace' },
1523
1770
  new: { type: 'string', description: 'Replacement text' },
1524
- count: { type: 'number', description: 'Number of replacements (default: 1)' },
1771
+ count: {
1772
+ type: 'number',
1773
+ description: 'Number of replacements (default: 1)',
1774
+ },
1525
1775
  },
1526
1776
  required: ['path', 'old', 'new'],
1527
1777
  },
@@ -1549,7 +1799,10 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1549
1799
  parameters: {
1550
1800
  type: 'object',
1551
1801
  properties: {
1552
- pattern: { type: 'string', description: 'Glob pattern to match files' },
1802
+ pattern: {
1803
+ type: 'string',
1804
+ description: 'Glob pattern to match files',
1805
+ },
1553
1806
  },
1554
1807
  required: ['pattern'],
1555
1808
  },
@@ -1563,8 +1816,15 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1563
1816
  parameters: {
1564
1817
  type: 'object',
1565
1818
  properties: {
1566
- pattern: { type: 'string', description: 'Regex pattern to search for' },
1567
- path: { type: 'string', description: 'Directory or file to search in (default: workspace root)' },
1819
+ pattern: {
1820
+ type: 'string',
1821
+ description: 'Regex pattern to search for',
1822
+ },
1823
+ path: {
1824
+ type: 'string',
1825
+ description:
1826
+ 'Directory or file to search in (default: workspace root)',
1827
+ },
1568
1828
  },
1569
1829
  required: ['pattern'],
1570
1830
  },
@@ -1580,8 +1840,16 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1580
1840
  type: 'object',
1581
1841
  properties: {
1582
1842
  command: { type: 'string', description: 'Shell command to execute' },
1583
- timeoutMs: { type: 'number', description: 'Optional command timeout in milliseconds (default 240000, max 900000)' },
1584
- timeoutSeconds: { type: 'number', description: 'Optional command timeout in seconds (used when timeoutMs is omitted)' },
1843
+ timeoutMs: {
1844
+ type: 'number',
1845
+ description:
1846
+ 'Optional command timeout in milliseconds (default 240000, max 900000)',
1847
+ },
1848
+ timeoutSeconds: {
1849
+ type: 'number',
1850
+ description:
1851
+ 'Optional command timeout in seconds (used when timeoutMs is omitted)',
1852
+ },
1585
1853
  },
1586
1854
  required: ['command'],
1587
1855
  },
@@ -1596,14 +1864,42 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1596
1864
  parameters: {
1597
1865
  type: 'object',
1598
1866
  properties: {
1599
- action: { type: 'string', description: 'Action: "read", "append", "write", "replace", "remove", "list", or "search"' },
1600
- file_path: { type: 'string', description: 'Target file path. Allowed: MEMORY.md, USER.md, memory/YYYY-MM-DD.md' },
1601
- target: { type: 'string', description: 'Optional shorthand target: "memory", "user", or "daily"' },
1602
- date: { type: 'string', description: 'Date for target="daily" in YYYY-MM-DD format (defaults to today)' },
1603
- content: { type: 'string', description: 'Text payload for append/write' },
1604
- old_text: { type: 'string', description: 'Existing substring for replace/remove' },
1605
- new_text: { type: 'string', description: 'Replacement text for replace' },
1606
- query: { type: 'string', description: 'Case-insensitive query string for search' },
1867
+ action: {
1868
+ type: 'string',
1869
+ description:
1870
+ 'Action: "read", "append", "write", "replace", "remove", "list", or "search"',
1871
+ },
1872
+ file_path: {
1873
+ type: 'string',
1874
+ description:
1875
+ 'Target file path. Allowed: MEMORY.md, USER.md, memory/YYYY-MM-DD.md',
1876
+ },
1877
+ target: {
1878
+ type: 'string',
1879
+ description:
1880
+ 'Optional shorthand target: "memory", "user", or "daily"',
1881
+ },
1882
+ date: {
1883
+ type: 'string',
1884
+ description:
1885
+ 'Date for target="daily" in YYYY-MM-DD format (defaults to today)',
1886
+ },
1887
+ content: {
1888
+ type: 'string',
1889
+ description: 'Text payload for append/write',
1890
+ },
1891
+ old_text: {
1892
+ type: 'string',
1893
+ description: 'Existing substring for replace/remove',
1894
+ },
1895
+ new_text: {
1896
+ type: 'string',
1897
+ description: 'Replacement text for replace',
1898
+ },
1899
+ query: {
1900
+ type: 'string',
1901
+ description: 'Case-insensitive query string for search',
1902
+ },
1607
1903
  },
1608
1904
  required: ['action'],
1609
1905
  },
@@ -1620,12 +1916,14 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1620
1916
  properties: {
1621
1917
  action: {
1622
1918
  type: 'string',
1623
- description: 'Action to perform: "read", "member-info", or "channel-info".',
1919
+ description:
1920
+ 'Action to perform: "read", "member-info", or "channel-info".',
1624
1921
  enum: ['read', 'member-info', 'channel-info'],
1625
1922
  },
1626
1923
  channelId: {
1627
1924
  type: 'string',
1628
- description: 'Discord channel id. Defaults to current channel for read/channel-info.',
1925
+ description:
1926
+ 'Discord channel id. Defaults to current channel for read/channel-info.',
1629
1927
  },
1630
1928
  guildId: {
1631
1929
  type: 'string',
@@ -1641,7 +1939,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1641
1939
  },
1642
1940
  username: {
1643
1941
  type: 'string',
1644
- description: 'Discord username/display name/@handle to resolve for member-info.',
1942
+ description:
1943
+ 'Discord username/display name/@handle to resolve for member-info.',
1645
1944
  },
1646
1945
  user: {
1647
1946
  type: 'string',
@@ -1651,9 +1950,18 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1651
1950
  type: 'number',
1652
1951
  description: 'Read limit for action="read" (default 20, max 100).',
1653
1952
  },
1654
- before: { type: 'string', description: 'Read messages before this message id.' },
1655
- after: { type: 'string', description: 'Read messages after this message id.' },
1656
- around: { type: 'string', description: 'Read messages around this message id.' },
1953
+ before: {
1954
+ type: 'string',
1955
+ description: 'Read messages before this message id.',
1956
+ },
1957
+ after: {
1958
+ type: 'string',
1959
+ description: 'Read messages after this message id.',
1960
+ },
1961
+ around: {
1962
+ type: 'string',
1963
+ description: 'Read messages around this message id.',
1964
+ },
1657
1965
  target: { type: 'string', description: 'Alias for channelId.' },
1658
1966
  to: { type: 'string', description: 'Alias for channelId.' },
1659
1967
  },
@@ -1670,10 +1978,25 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1670
1978
  parameters: {
1671
1979
  type: 'object',
1672
1980
  properties: {
1673
- query: { type: 'string', description: 'Search query over prior session transcripts' },
1674
- limit: { type: 'number', description: 'Maximum number of sessions to summarize (default 3, max 5)' },
1675
- role_filter: { type: 'string', description: 'Optional comma-separated roles to match (e.g. "user,assistant")' },
1676
- include_current: { type: 'boolean', description: 'Include the current session in results (default false)' },
1981
+ query: {
1982
+ type: 'string',
1983
+ description: 'Search query over prior session transcripts',
1984
+ },
1985
+ limit: {
1986
+ type: 'number',
1987
+ description:
1988
+ 'Maximum number of sessions to summarize (default 3, max 5)',
1989
+ },
1990
+ role_filter: {
1991
+ type: 'string',
1992
+ description:
1993
+ 'Optional comma-separated roles to match (e.g. "user,assistant")',
1994
+ },
1995
+ include_current: {
1996
+ type: 'boolean',
1997
+ description:
1998
+ 'Include the current session in results (default false)',
1999
+ },
1677
2000
  },
1678
2001
  required: ['query'],
1679
2002
  },
@@ -1695,7 +2018,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1695
2018
  },
1696
2019
  maxChars: {
1697
2020
  type: 'number',
1698
- description: 'Maximum characters to return (default 50000, max 50000)',
2021
+ description:
2022
+ 'Maximum characters to return (default 50000, max 50000)',
1699
2023
  },
1700
2024
  },
1701
2025
  required: ['url'],
@@ -1713,7 +2037,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1713
2037
  properties: {
1714
2038
  image_url: {
1715
2039
  type: 'string',
1716
- description: 'Local image path (preferred) or Discord CDN HTTPS URL.',
2040
+ description:
2041
+ 'Local image path (preferred) or Discord CDN HTTPS URL.',
1717
2042
  },
1718
2043
  question: {
1719
2044
  type: 'string',
@@ -1721,7 +2046,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1721
2046
  },
1722
2047
  fallback_url: {
1723
2048
  type: 'string',
1724
- description: 'Optional fallback Discord CDN URL if image_url cannot be read.',
2049
+ description:
2050
+ 'Optional fallback Discord CDN URL if image_url cannot be read.',
1725
2051
  },
1726
2052
  original_url: {
1727
2053
  type: 'string',
@@ -1736,14 +2062,14 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1736
2062
  type: 'function',
1737
2063
  function: {
1738
2064
  name: 'image',
1739
- description:
1740
- 'Alias of vision_analyze for image analysis.',
2065
+ description: 'Alias of vision_analyze for image analysis.',
1741
2066
  parameters: {
1742
2067
  type: 'object',
1743
2068
  properties: {
1744
2069
  image_url: {
1745
2070
  type: 'string',
1746
- description: 'Local image path (preferred) or Discord CDN HTTPS URL.',
2071
+ description:
2072
+ 'Local image path (preferred) or Discord CDN HTTPS URL.',
1747
2073
  },
1748
2074
  question: {
1749
2075
  type: 'string',
@@ -1751,7 +2077,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1751
2077
  },
1752
2078
  fallback_url: {
1753
2079
  type: 'string',
1754
- description: 'Optional fallback Discord CDN URL if image_url cannot be read.',
2080
+ description:
2081
+ 'Optional fallback Discord CDN URL if image_url cannot be read.',
1755
2082
  },
1756
2083
  original_url: {
1757
2084
  type: 'string',
@@ -1774,38 +2101,65 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1774
2101
  properties: {
1775
2102
  mode: {
1776
2103
  type: 'string',
1777
- description: 'Optional explicit mode: "single", "parallel", or "chain". Inferred automatically when omitted.',
2104
+ description:
2105
+ 'Optional explicit mode: "single", "parallel", or "chain". Inferred automatically when omitted.',
1778
2106
  enum: ['single', 'parallel', 'chain'],
1779
2107
  },
1780
- prompt: { type: 'string', description: 'Single-mode task instructions. Must be self-contained and specific.' },
1781
- label: { type: 'string', description: 'Optional short label for completion messages' },
1782
- model: { type: 'string', description: 'Optional model override for delegated run(s)' },
2108
+ prompt: {
2109
+ type: 'string',
2110
+ description:
2111
+ 'Single-mode task instructions. Must be self-contained and specific.',
2112
+ },
2113
+ label: {
2114
+ type: 'string',
2115
+ description: 'Optional short label for completion messages',
2116
+ },
2117
+ model: {
2118
+ type: 'string',
2119
+ description: 'Optional model override for delegated run(s)',
2120
+ },
1783
2121
  tasks: {
1784
2122
  type: 'array',
1785
- description: 'Parallel-mode independent tasks (1-6 items). Each task must be self-contained.',
2123
+ description:
2124
+ 'Parallel-mode independent tasks (1-6 items). Each task must be self-contained.',
1786
2125
  minItems: 1,
1787
2126
  maxItems: 6,
1788
2127
  items: {
1789
2128
  type: 'object',
1790
2129
  properties: {
1791
- prompt: { type: 'string', description: 'Task instructions with explicit goal/scope/constraints.' },
2130
+ prompt: {
2131
+ type: 'string',
2132
+ description:
2133
+ 'Task instructions with explicit goal/scope/constraints.',
2134
+ },
1792
2135
  label: { type: 'string', description: 'Optional task label' },
1793
- model: { type: 'string', description: 'Optional per-task model override' },
2136
+ model: {
2137
+ type: 'string',
2138
+ description: 'Optional per-task model override',
2139
+ },
1794
2140
  },
1795
2141
  required: ['prompt'],
1796
2142
  },
1797
2143
  },
1798
2144
  chain: {
1799
2145
  type: 'array',
1800
- description: 'Chain-mode dependent steps (1-6 items). Use `{previous}` to inject prior step output.',
2146
+ description:
2147
+ 'Chain-mode dependent steps (1-6 items). Use `{previous}` to inject prior step output.',
1801
2148
  minItems: 1,
1802
2149
  maxItems: 6,
1803
2150
  items: {
1804
2151
  type: 'object',
1805
2152
  properties: {
1806
- prompt: { type: 'string', description: 'Step instructions (supports `{previous}`) with expected output.' },
2153
+ prompt: {
2154
+ type: 'string',
2155
+ description:
2156
+ 'Step instructions (supports `{previous}`) with expected output.',
2157
+ },
1807
2158
  label: { type: 'string', description: 'Optional step label' },
1808
- model: { type: 'string', description: 'Optional per-step model override' },
2159
+ model: {
2160
+ type: 'string',
2161
+ description: 'Optional per-step model override',
2162
+ },
1809
2163
  },
1810
2164
  required: ['prompt'],
1811
2165
  },
@@ -1828,12 +2182,33 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1828
2182
  parameters: {
1829
2183
  type: 'object',
1830
2184
  properties: {
1831
- action: { type: 'string', description: 'Action to perform: "list", "add", or "remove"' },
1832
- prompt: { type: 'string', description: 'Task prompt / reminder text (required for "add")' },
1833
- at: { type: 'string', description: 'ISO-8601 timestamp for one-shot schedule (e.g. "2025-01-15T14:30:00Z")' },
1834
- cron: { type: 'string', description: 'Cron expression for recurring schedule (e.g. "0 9 * * *")' },
1835
- every: { type: 'number', description: 'Interval in seconds for simple recurring schedule (minimum 10)' },
1836
- taskId: { type: 'number', description: 'Task ID to remove (required for "remove")' },
2185
+ action: {
2186
+ type: 'string',
2187
+ description: 'Action to perform: "list", "add", or "remove"',
2188
+ },
2189
+ prompt: {
2190
+ type: 'string',
2191
+ description: 'Task prompt / reminder text (required for "add")',
2192
+ },
2193
+ at: {
2194
+ type: 'string',
2195
+ description:
2196
+ 'ISO-8601 timestamp for one-shot schedule (e.g. "2025-01-15T14:30:00Z")',
2197
+ },
2198
+ cron: {
2199
+ type: 'string',
2200
+ description:
2201
+ 'Cron expression for recurring schedule (e.g. "0 9 * * *")',
2202
+ },
2203
+ every: {
2204
+ type: 'number',
2205
+ description:
2206
+ 'Interval in seconds for simple recurring schedule (minimum 10)',
2207
+ },
2208
+ taskId: {
2209
+ type: 'number',
2210
+ description: 'Task ID to remove (required for "remove")',
2211
+ },
1837
2212
  },
1838
2213
  required: ['action'],
1839
2214
  },