@hybridaione/hybridclaw 0.2.2 → 0.2.6

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 (277) hide show
  1. package/.github/workflows/ci.yml +70 -0
  2. package/.husky/pre-commit +1 -0
  3. package/CHANGELOG.md +85 -0
  4. package/CONTRIBUTING.md +33 -0
  5. package/README.md +41 -16
  6. package/SECURITY.md +17 -0
  7. package/biome.json +35 -0
  8. package/config.example.json +71 -8
  9. package/container/package-lock.json +2 -2
  10. package/container/package.json +1 -1
  11. package/container/src/approval-policy.ts +1303 -0
  12. package/container/src/browser-tools.ts +431 -136
  13. package/container/src/extensions.ts +36 -12
  14. package/container/src/hybridai-client.ts +34 -13
  15. package/container/src/index.ts +451 -109
  16. package/container/src/ipc.ts +5 -3
  17. package/container/src/token-usage.ts +20 -10
  18. package/container/src/tools.ts +599 -225
  19. package/container/src/types.ts +32 -2
  20. package/container/src/web-fetch.ts +89 -32
  21. package/dist/agent.d.ts.map +1 -1
  22. package/dist/agent.js +10 -2
  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 +19 -3
  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 +4 -0
  49. package/dist/channels/discord/inbound.d.ts.map +1 -1
  50. package/dist/channels/discord/inbound.js +45 -4
  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 +6 -3
  68. package/dist/channels/discord/runtime.d.ts.map +1 -1
  69. package/dist/channels/discord/runtime.js +621 -125
  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 +16 -8
  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 +47 -22
  86. package/dist/cli.js.map +1 -1
  87. package/dist/config.d.ts +19 -0
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/config.js +103 -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 +58 -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 +563 -73
  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 +179 -24
  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 +48 -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/instruction-approval-audit.d.ts.map +1 -1
  132. package/dist/instruction-approval-audit.js.map +1 -1
  133. package/dist/instruction-integrity.d.ts.map +1 -1
  134. package/dist/instruction-integrity.js +8 -2
  135. package/dist/instruction-integrity.js.map +1 -1
  136. package/dist/ipc.d.ts.map +1 -1
  137. package/dist/ipc.js +6 -1
  138. package/dist/ipc.js.map +1 -1
  139. package/dist/logger.js.map +1 -1
  140. package/dist/memory-consolidation.d.ts +17 -0
  141. package/dist/memory-consolidation.d.ts.map +1 -0
  142. package/dist/memory-consolidation.js +25 -0
  143. package/dist/memory-consolidation.js.map +1 -0
  144. package/dist/memory-service.d.ts +200 -0
  145. package/dist/memory-service.d.ts.map +1 -0
  146. package/dist/memory-service.js +294 -0
  147. package/dist/memory-service.js.map +1 -0
  148. package/dist/mount-security.d.ts.map +1 -1
  149. package/dist/mount-security.js +31 -7
  150. package/dist/mount-security.js.map +1 -1
  151. package/dist/observability-ingest.d.ts.map +1 -1
  152. package/dist/observability-ingest.js +32 -11
  153. package/dist/observability-ingest.js.map +1 -1
  154. package/dist/onboarding.d.ts.map +1 -1
  155. package/dist/onboarding.js +32 -9
  156. package/dist/onboarding.js.map +1 -1
  157. package/dist/proactive-policy.d.ts.map +1 -1
  158. package/dist/proactive-policy.js +2 -1
  159. package/dist/proactive-policy.js.map +1 -1
  160. package/dist/prompt-hooks.d.ts.map +1 -1
  161. package/dist/prompt-hooks.js +9 -7
  162. package/dist/prompt-hooks.js.map +1 -1
  163. package/dist/runtime-config.d.ts +98 -1
  164. package/dist/runtime-config.d.ts.map +1 -1
  165. package/dist/runtime-config.js +477 -23
  166. package/dist/runtime-config.js.map +1 -1
  167. package/dist/scheduled-task-runner.d.ts +1 -0
  168. package/dist/scheduled-task-runner.d.ts.map +1 -1
  169. package/dist/scheduled-task-runner.js +29 -10
  170. package/dist/scheduled-task-runner.js.map +1 -1
  171. package/dist/scheduler.d.ts +43 -4
  172. package/dist/scheduler.d.ts.map +1 -1
  173. package/dist/scheduler.js +530 -56
  174. package/dist/scheduler.js.map +1 -1
  175. package/dist/session-export.d.ts +26 -0
  176. package/dist/session-export.d.ts.map +1 -0
  177. package/dist/session-export.js +149 -0
  178. package/dist/session-export.js.map +1 -0
  179. package/dist/session-maintenance.d.ts.map +1 -1
  180. package/dist/session-maintenance.js +75 -13
  181. package/dist/session-maintenance.js.map +1 -1
  182. package/dist/session-transcripts.d.ts.map +1 -1
  183. package/dist/session-transcripts.js.map +1 -1
  184. package/dist/side-effects.d.ts.map +1 -1
  185. package/dist/side-effects.js +14 -2
  186. package/dist/side-effects.js.map +1 -1
  187. package/dist/skills-guard.d.ts.map +1 -1
  188. package/dist/skills-guard.js +893 -130
  189. package/dist/skills-guard.js.map +1 -1
  190. package/dist/skills.d.ts +5 -0
  191. package/dist/skills.d.ts.map +1 -1
  192. package/dist/skills.js +29 -15
  193. package/dist/skills.js.map +1 -1
  194. package/dist/token-efficiency.d.ts.map +1 -1
  195. package/dist/token-efficiency.js.map +1 -1
  196. package/dist/tui.js +92 -11
  197. package/dist/tui.js.map +1 -1
  198. package/dist/types.d.ts +146 -0
  199. package/dist/types.d.ts.map +1 -1
  200. package/dist/types.js +24 -1
  201. package/dist/types.js.map +1 -1
  202. package/dist/update.d.ts.map +1 -1
  203. package/dist/update.js +42 -14
  204. package/dist/update.js.map +1 -1
  205. package/dist/workspace.d.ts.map +1 -1
  206. package/dist/workspace.js +49 -9
  207. package/dist/workspace.js.map +1 -1
  208. package/docs/chat.html +9 -3
  209. package/docs/index.html +37 -13
  210. package/package.json +8 -2
  211. package/src/agent.ts +16 -3
  212. package/src/audit-cli.ts +44 -16
  213. package/src/audit-events.ts +69 -5
  214. package/src/audit-trail.ts +41 -15
  215. package/src/channels/discord/attachments.ts +81 -27
  216. package/src/channels/discord/debounce.ts +25 -0
  217. package/src/channels/discord/delivery.ts +57 -13
  218. package/src/channels/discord/human-delay.ts +48 -0
  219. package/src/channels/discord/inbound.ts +66 -7
  220. package/src/channels/discord/mentions.ts +42 -18
  221. package/src/channels/discord/presence.ts +148 -0
  222. package/src/channels/discord/rate-limiter.ts +58 -0
  223. package/src/channels/discord/reactions.ts +211 -0
  224. package/src/channels/discord/runtime.ts +1048 -182
  225. package/src/channels/discord/stream.ts +73 -27
  226. package/src/channels/discord/tool-actions.ts +78 -37
  227. package/src/channels/discord/typing.ts +140 -0
  228. package/src/chunk.ts +12 -4
  229. package/src/cli.ts +141 -56
  230. package/src/config.ts +192 -34
  231. package/src/container-runner.ts +132 -42
  232. package/src/container-setup.ts +57 -22
  233. package/src/conversation.ts +9 -7
  234. package/src/db.ts +2217 -84
  235. package/src/delegation-manager.ts +6 -2
  236. package/src/gateway-client.ts +41 -17
  237. package/src/gateway-service.ts +1019 -201
  238. package/src/gateway-types.ts +33 -0
  239. package/src/gateway.ts +321 -48
  240. package/src/health.ts +66 -26
  241. package/src/heartbeat.ts +84 -22
  242. package/src/hybridai-bots.ts +14 -5
  243. package/src/instruction-approval-audit.ts +4 -1
  244. package/src/instruction-integrity.ts +30 -9
  245. package/src/ipc.ts +23 -5
  246. package/src/logger.ts +4 -1
  247. package/src/memory-consolidation.ts +41 -0
  248. package/src/memory-service.ts +606 -0
  249. package/src/mount-security.ts +58 -13
  250. package/src/observability-ingest.ts +134 -35
  251. package/src/onboarding.ts +126 -35
  252. package/src/proactive-policy.ts +3 -1
  253. package/src/prompt-hooks.ts +40 -17
  254. package/src/runtime-config.ts +1114 -99
  255. package/src/scheduled-task-runner.ts +63 -11
  256. package/src/scheduler.ts +683 -60
  257. package/src/session-export.ts +196 -0
  258. package/src/session-maintenance.ts +125 -22
  259. package/src/session-transcripts.ts +12 -3
  260. package/src/side-effects.ts +28 -5
  261. package/src/skills-guard.ts +1067 -219
  262. package/src/skills.ts +163 -65
  263. package/src/token-efficiency.ts +31 -9
  264. package/src/tui.ts +166 -25
  265. package/src/types.ts +195 -2
  266. package/src/update.ts +79 -23
  267. package/src/workspace.ts +63 -11
  268. package/tests/approval-policy.test.ts +224 -0
  269. package/tests/discord.basic.test.ts +82 -2
  270. package/tests/discord.human-presence.test.ts +85 -0
  271. package/tests/gateway-service.media-routing.test.ts +8 -2
  272. package/tests/memory-service.test.ts +1114 -0
  273. package/tests/token-efficiency.basic.test.ts +8 -2
  274. package/vitest.e2e.config.ts +3 -1
  275. package/vitest.integration.config.ts +3 -1
  276. package/vitest.live.config.ts +3 -1
  277. 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}`);
@@ -1321,7 +1526,11 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1321
1526
  case 'browser_console':
1322
1527
  case 'browser_network':
1323
1528
  case 'browser_close': {
1324
- return await executeBrowserTool(name, args, currentSessionId || 'default');
1529
+ return await executeBrowserTool(
1530
+ name,
1531
+ args,
1532
+ currentSessionId || 'default',
1533
+ );
1325
1534
  }
1326
1535
 
1327
1536
  case 'cron': {
@@ -1335,7 +1544,8 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1335
1544
  else if (t.everyMs) {
1336
1545
  const secs = t.everyMs / 1000;
1337
1546
  if (secs < 120) schedule = `every ${secs}s`;
1338
- else if (secs < 7200) schedule = `every ${Math.round(secs / 60)}m`;
1547
+ else if (secs < 7200)
1548
+ schedule = `every ${Math.round(secs / 60)}m`;
1339
1549
  else schedule = `every ${Math.round(secs / 3600)}h`;
1340
1550
  } else schedule = t.cronExpr;
1341
1551
  const status = t.enabled ? 'enabled' : 'disabled';
@@ -1349,22 +1559,37 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1349
1559
 
1350
1560
  if (args.at) {
1351
1561
  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 });
1562
+ if (Number.isNaN(runAt.getTime()))
1563
+ return `Error: invalid ISO-8601 timestamp: ${args.at}`;
1564
+ if (runAt.getTime() <= Date.now())
1565
+ return `Error: timestamp must be in the future: ${args.at}`;
1566
+ pendingSchedules.push({
1567
+ action: 'add',
1568
+ runAt: runAt.toISOString(),
1569
+ prompt: args.prompt,
1570
+ });
1355
1571
  return `Scheduled one-shot task at ${runAt.toISOString()}: ${args.prompt}`;
1356
1572
  }
1357
1573
 
1358
1574
  if (args.cron) {
1359
- pendingSchedules.push({ action: 'add', cronExpr: args.cron, prompt: args.prompt });
1575
+ pendingSchedules.push({
1576
+ action: 'add',
1577
+ cronExpr: args.cron,
1578
+ prompt: args.prompt,
1579
+ });
1360
1580
  return `Scheduled recurring task with cron "${args.cron}": ${args.prompt}`;
1361
1581
  }
1362
1582
 
1363
1583
  if (args.every) {
1364
1584
  const secs = Number(args.every);
1365
- if (isNaN(secs) || secs < 10) return 'Error: "every" must be a number of seconds >= 10';
1585
+ if (Number.isNaN(secs) || secs < 10)
1586
+ return 'Error: "every" must be a number of seconds >= 10';
1366
1587
  const everyMs = Math.round(secs * 1000);
1367
- pendingSchedules.push({ action: 'add', everyMs, prompt: args.prompt });
1588
+ pendingSchedules.push({
1589
+ action: 'add',
1590
+ everyMs,
1591
+ prompt: args.prompt,
1592
+ });
1368
1593
  return `Scheduled interval task every ${secs}s: ${args.prompt}`;
1369
1594
  }
1370
1595
 
@@ -1385,14 +1610,21 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1385
1610
  return `Error: delegation limit reached for this turn (${MAX_PENDING_DELEGATIONS}).`;
1386
1611
  }
1387
1612
 
1388
- const modeRaw = typeof args.mode === 'string' ? args.mode.trim().toLowerCase() : '';
1389
- if (modeRaw && modeRaw !== 'single' && modeRaw !== 'parallel' && modeRaw !== 'chain') {
1613
+ const modeRaw =
1614
+ typeof args.mode === 'string' ? args.mode.trim().toLowerCase() : '';
1615
+ if (
1616
+ modeRaw &&
1617
+ modeRaw !== 'single' &&
1618
+ modeRaw !== 'parallel' &&
1619
+ modeRaw !== 'chain'
1620
+ ) {
1390
1621
  return 'Error: mode must be one of "single", "parallel", or "chain".';
1391
1622
  }
1392
1623
 
1393
1624
  const label = typeof args.label === 'string' ? args.label.trim() : '';
1394
1625
  const model = typeof args.model === 'string' ? args.model.trim() : '';
1395
- const prompt = typeof args.prompt === 'string' ? args.prompt.trim() : '';
1626
+ const prompt =
1627
+ typeof args.prompt === 'string' ? args.prompt.trim() : '';
1396
1628
  const tasksResult = normalizeDelegationTaskList({
1397
1629
  raw: args.tasks,
1398
1630
  fallbackModel: model || undefined,
@@ -1421,7 +1653,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1421
1653
  mode = 'single';
1422
1654
  }
1423
1655
 
1424
- if ((hasTasks ? 1 : 0) + (hasChain ? 1 : 0) + (hasPrompt ? 1 : 0) > 1 && !modeRaw) {
1656
+ if (
1657
+ (hasTasks ? 1 : 0) + (hasChain ? 1 : 0) + (hasPrompt ? 1 : 0) > 1 &&
1658
+ !modeRaw
1659
+ ) {
1425
1660
  return 'Error: provide one delegation mode payload: "prompt", "tasks", or "chain".';
1426
1661
  }
1427
1662
 
@@ -1439,8 +1674,10 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1439
1674
  };
1440
1675
  summary = label ? `${label}: ${prompt}` : prompt;
1441
1676
  } 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.';
1677
+ if (!hasTasks)
1678
+ return 'Error: tasks are required for mode="parallel".';
1679
+ if (hasPrompt || hasChain)
1680
+ return 'Error: mode="parallel" accepts only "tasks" plus optional label/model.';
1444
1681
  effect = {
1445
1682
  action: 'delegate',
1446
1683
  mode,
@@ -1451,7 +1688,8 @@ export async function executeTool(name: string, argsJson: string): Promise<strin
1451
1688
  summary = `${tasksResult.tasks.length} parallel task(s)`;
1452
1689
  } else {
1453
1690
  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.';
1691
+ if (hasPrompt || hasTasks)
1692
+ return 'Error: mode="chain" accepts only "chain" plus optional label/model.';
1455
1693
  effect = {
1456
1694
  action: 'delegate',
1457
1695
  mode,
@@ -1486,8 +1724,16 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1486
1724
  type: 'object',
1487
1725
  properties: {
1488
1726
  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)' },
1727
+ offset: {
1728
+ type: 'number',
1729
+ description:
1730
+ 'Line number to start reading from (1-indexed, default: 1)',
1731
+ },
1732
+ limit: {
1733
+ type: 'number',
1734
+ description:
1735
+ 'Maximum number of lines to read before truncation logic (optional)',
1736
+ },
1491
1737
  },
1492
1738
  required: ['path'],
1493
1739
  },
@@ -1521,7 +1767,10 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1521
1767
  path: { type: 'string', description: 'Path to the file to edit' },
1522
1768
  old: { type: 'string', description: 'Text to find and replace' },
1523
1769
  new: { type: 'string', description: 'Replacement text' },
1524
- count: { type: 'number', description: 'Number of replacements (default: 1)' },
1770
+ count: {
1771
+ type: 'number',
1772
+ description: 'Number of replacements (default: 1)',
1773
+ },
1525
1774
  },
1526
1775
  required: ['path', 'old', 'new'],
1527
1776
  },
@@ -1549,7 +1798,10 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1549
1798
  parameters: {
1550
1799
  type: 'object',
1551
1800
  properties: {
1552
- pattern: { type: 'string', description: 'Glob pattern to match files' },
1801
+ pattern: {
1802
+ type: 'string',
1803
+ description: 'Glob pattern to match files',
1804
+ },
1553
1805
  },
1554
1806
  required: ['pattern'],
1555
1807
  },
@@ -1563,8 +1815,15 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1563
1815
  parameters: {
1564
1816
  type: 'object',
1565
1817
  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)' },
1818
+ pattern: {
1819
+ type: 'string',
1820
+ description: 'Regex pattern to search for',
1821
+ },
1822
+ path: {
1823
+ type: 'string',
1824
+ description:
1825
+ 'Directory or file to search in (default: workspace root)',
1826
+ },
1568
1827
  },
1569
1828
  required: ['pattern'],
1570
1829
  },
@@ -1580,8 +1839,16 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1580
1839
  type: 'object',
1581
1840
  properties: {
1582
1841
  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)' },
1842
+ timeoutMs: {
1843
+ type: 'number',
1844
+ description:
1845
+ 'Optional command timeout in milliseconds (default 240000, max 900000)',
1846
+ },
1847
+ timeoutSeconds: {
1848
+ type: 'number',
1849
+ description:
1850
+ 'Optional command timeout in seconds (used when timeoutMs is omitted)',
1851
+ },
1585
1852
  },
1586
1853
  required: ['command'],
1587
1854
  },
@@ -1596,14 +1863,42 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1596
1863
  parameters: {
1597
1864
  type: 'object',
1598
1865
  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' },
1866
+ action: {
1867
+ type: 'string',
1868
+ description:
1869
+ 'Action: "read", "append", "write", "replace", "remove", "list", or "search"',
1870
+ },
1871
+ file_path: {
1872
+ type: 'string',
1873
+ description:
1874
+ 'Target file path. Allowed: MEMORY.md, USER.md, memory/YYYY-MM-DD.md',
1875
+ },
1876
+ target: {
1877
+ type: 'string',
1878
+ description:
1879
+ 'Optional shorthand target: "memory", "user", or "daily"',
1880
+ },
1881
+ date: {
1882
+ type: 'string',
1883
+ description:
1884
+ 'Date for target="daily" in YYYY-MM-DD format (defaults to today)',
1885
+ },
1886
+ content: {
1887
+ type: 'string',
1888
+ description: 'Text payload for append/write',
1889
+ },
1890
+ old_text: {
1891
+ type: 'string',
1892
+ description: 'Existing substring for replace/remove',
1893
+ },
1894
+ new_text: {
1895
+ type: 'string',
1896
+ description: 'Replacement text for replace',
1897
+ },
1898
+ query: {
1899
+ type: 'string',
1900
+ description: 'Case-insensitive query string for search',
1901
+ },
1607
1902
  },
1608
1903
  required: ['action'],
1609
1904
  },
@@ -1620,12 +1915,14 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1620
1915
  properties: {
1621
1916
  action: {
1622
1917
  type: 'string',
1623
- description: 'Action to perform: "read", "member-info", or "channel-info".',
1918
+ description:
1919
+ 'Action to perform: "read", "member-info", or "channel-info".',
1624
1920
  enum: ['read', 'member-info', 'channel-info'],
1625
1921
  },
1626
1922
  channelId: {
1627
1923
  type: 'string',
1628
- description: 'Discord channel id. Defaults to current channel for read/channel-info.',
1924
+ description:
1925
+ 'Discord channel id. Defaults to current channel for read/channel-info.',
1629
1926
  },
1630
1927
  guildId: {
1631
1928
  type: 'string',
@@ -1641,7 +1938,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1641
1938
  },
1642
1939
  username: {
1643
1940
  type: 'string',
1644
- description: 'Discord username/display name/@handle to resolve for member-info.',
1941
+ description:
1942
+ 'Discord username/display name/@handle to resolve for member-info.',
1645
1943
  },
1646
1944
  user: {
1647
1945
  type: 'string',
@@ -1651,9 +1949,18 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1651
1949
  type: 'number',
1652
1950
  description: 'Read limit for action="read" (default 20, max 100).',
1653
1951
  },
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.' },
1952
+ before: {
1953
+ type: 'string',
1954
+ description: 'Read messages before this message id.',
1955
+ },
1956
+ after: {
1957
+ type: 'string',
1958
+ description: 'Read messages after this message id.',
1959
+ },
1960
+ around: {
1961
+ type: 'string',
1962
+ description: 'Read messages around this message id.',
1963
+ },
1657
1964
  target: { type: 'string', description: 'Alias for channelId.' },
1658
1965
  to: { type: 'string', description: 'Alias for channelId.' },
1659
1966
  },
@@ -1670,10 +1977,25 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1670
1977
  parameters: {
1671
1978
  type: 'object',
1672
1979
  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)' },
1980
+ query: {
1981
+ type: 'string',
1982
+ description: 'Search query over prior session transcripts',
1983
+ },
1984
+ limit: {
1985
+ type: 'number',
1986
+ description:
1987
+ 'Maximum number of sessions to summarize (default 3, max 5)',
1988
+ },
1989
+ role_filter: {
1990
+ type: 'string',
1991
+ description:
1992
+ 'Optional comma-separated roles to match (e.g. "user,assistant")',
1993
+ },
1994
+ include_current: {
1995
+ type: 'boolean',
1996
+ description:
1997
+ 'Include the current session in results (default false)',
1998
+ },
1677
1999
  },
1678
2000
  required: ['query'],
1679
2001
  },
@@ -1695,7 +2017,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1695
2017
  },
1696
2018
  maxChars: {
1697
2019
  type: 'number',
1698
- description: 'Maximum characters to return (default 50000, max 50000)',
2020
+ description:
2021
+ 'Maximum characters to return (default 50000, max 50000)',
1699
2022
  },
1700
2023
  },
1701
2024
  required: ['url'],
@@ -1713,7 +2036,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1713
2036
  properties: {
1714
2037
  image_url: {
1715
2038
  type: 'string',
1716
- description: 'Local image path (preferred) or Discord CDN HTTPS URL.',
2039
+ description:
2040
+ 'Local image path (preferred) or Discord CDN HTTPS URL.',
1717
2041
  },
1718
2042
  question: {
1719
2043
  type: 'string',
@@ -1721,7 +2045,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1721
2045
  },
1722
2046
  fallback_url: {
1723
2047
  type: 'string',
1724
- description: 'Optional fallback Discord CDN URL if image_url cannot be read.',
2048
+ description:
2049
+ 'Optional fallback Discord CDN URL if image_url cannot be read.',
1725
2050
  },
1726
2051
  original_url: {
1727
2052
  type: 'string',
@@ -1736,14 +2061,14 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1736
2061
  type: 'function',
1737
2062
  function: {
1738
2063
  name: 'image',
1739
- description:
1740
- 'Alias of vision_analyze for image analysis.',
2064
+ description: 'Alias of vision_analyze for image analysis.',
1741
2065
  parameters: {
1742
2066
  type: 'object',
1743
2067
  properties: {
1744
2068
  image_url: {
1745
2069
  type: 'string',
1746
- description: 'Local image path (preferred) or Discord CDN HTTPS URL.',
2070
+ description:
2071
+ 'Local image path (preferred) or Discord CDN HTTPS URL.',
1747
2072
  },
1748
2073
  question: {
1749
2074
  type: 'string',
@@ -1751,7 +2076,8 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1751
2076
  },
1752
2077
  fallback_url: {
1753
2078
  type: 'string',
1754
- description: 'Optional fallback Discord CDN URL if image_url cannot be read.',
2079
+ description:
2080
+ 'Optional fallback Discord CDN URL if image_url cannot be read.',
1755
2081
  },
1756
2082
  original_url: {
1757
2083
  type: 'string',
@@ -1774,38 +2100,65 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1774
2100
  properties: {
1775
2101
  mode: {
1776
2102
  type: 'string',
1777
- description: 'Optional explicit mode: "single", "parallel", or "chain". Inferred automatically when omitted.',
2103
+ description:
2104
+ 'Optional explicit mode: "single", "parallel", or "chain". Inferred automatically when omitted.',
1778
2105
  enum: ['single', 'parallel', 'chain'],
1779
2106
  },
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)' },
2107
+ prompt: {
2108
+ type: 'string',
2109
+ description:
2110
+ 'Single-mode task instructions. Must be self-contained and specific.',
2111
+ },
2112
+ label: {
2113
+ type: 'string',
2114
+ description: 'Optional short label for completion messages',
2115
+ },
2116
+ model: {
2117
+ type: 'string',
2118
+ description: 'Optional model override for delegated run(s)',
2119
+ },
1783
2120
  tasks: {
1784
2121
  type: 'array',
1785
- description: 'Parallel-mode independent tasks (1-6 items). Each task must be self-contained.',
2122
+ description:
2123
+ 'Parallel-mode independent tasks (1-6 items). Each task must be self-contained.',
1786
2124
  minItems: 1,
1787
2125
  maxItems: 6,
1788
2126
  items: {
1789
2127
  type: 'object',
1790
2128
  properties: {
1791
- prompt: { type: 'string', description: 'Task instructions with explicit goal/scope/constraints.' },
2129
+ prompt: {
2130
+ type: 'string',
2131
+ description:
2132
+ 'Task instructions with explicit goal/scope/constraints.',
2133
+ },
1792
2134
  label: { type: 'string', description: 'Optional task label' },
1793
- model: { type: 'string', description: 'Optional per-task model override' },
2135
+ model: {
2136
+ type: 'string',
2137
+ description: 'Optional per-task model override',
2138
+ },
1794
2139
  },
1795
2140
  required: ['prompt'],
1796
2141
  },
1797
2142
  },
1798
2143
  chain: {
1799
2144
  type: 'array',
1800
- description: 'Chain-mode dependent steps (1-6 items). Use `{previous}` to inject prior step output.',
2145
+ description:
2146
+ 'Chain-mode dependent steps (1-6 items). Use `{previous}` to inject prior step output.',
1801
2147
  minItems: 1,
1802
2148
  maxItems: 6,
1803
2149
  items: {
1804
2150
  type: 'object',
1805
2151
  properties: {
1806
- prompt: { type: 'string', description: 'Step instructions (supports `{previous}`) with expected output.' },
2152
+ prompt: {
2153
+ type: 'string',
2154
+ description:
2155
+ 'Step instructions (supports `{previous}`) with expected output.',
2156
+ },
1807
2157
  label: { type: 'string', description: 'Optional step label' },
1808
- model: { type: 'string', description: 'Optional per-step model override' },
2158
+ model: {
2159
+ type: 'string',
2160
+ description: 'Optional per-step model override',
2161
+ },
1809
2162
  },
1810
2163
  required: ['prompt'],
1811
2164
  },
@@ -1828,12 +2181,33 @@ export const TOOL_DEFINITIONS: ToolDefinition[] = [
1828
2181
  parameters: {
1829
2182
  type: 'object',
1830
2183
  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")' },
2184
+ action: {
2185
+ type: 'string',
2186
+ description: 'Action to perform: "list", "add", or "remove"',
2187
+ },
2188
+ prompt: {
2189
+ type: 'string',
2190
+ description: 'Task prompt / reminder text (required for "add")',
2191
+ },
2192
+ at: {
2193
+ type: 'string',
2194
+ description:
2195
+ 'ISO-8601 timestamp for one-shot schedule (e.g. "2025-01-15T14:30:00Z")',
2196
+ },
2197
+ cron: {
2198
+ type: 'string',
2199
+ description:
2200
+ 'Cron expression for recurring schedule (e.g. "0 9 * * *")',
2201
+ },
2202
+ every: {
2203
+ type: 'number',
2204
+ description:
2205
+ 'Interval in seconds for simple recurring schedule (minimum 10)',
2206
+ },
2207
+ taskId: {
2208
+ type: 'number',
2209
+ description: 'Task ID to remove (required for "remove")',
2210
+ },
1837
2211
  },
1838
2212
  required: ['action'],
1839
2213
  },