@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
package/src/update.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import { spawn, spawnSync } from 'child_process';
1
2
  import fs from 'fs';
2
3
  import path from 'path';
3
- import { spawn, spawnSync } from 'child_process';
4
4
  import readline from 'readline/promises';
5
5
 
6
6
  const DEFAULT_PACKAGE_NAME = '@hybridaione/hybridclaw';
@@ -52,8 +52,14 @@ function readPackageInfo(packageJsonPath: string): PackageInfo {
52
52
  try {
53
53
  const raw = fs.readFileSync(packageJsonPath, 'utf-8');
54
54
  const parsed = JSON.parse(raw) as PackageManifest;
55
- const name = typeof parsed.name === 'string' && parsed.name.trim() ? parsed.name.trim() : null;
56
- const version = typeof parsed.version === 'string' && parsed.version.trim() ? parsed.version.trim() : null;
55
+ const name =
56
+ typeof parsed.name === 'string' && parsed.name.trim()
57
+ ? parsed.name.trim()
58
+ : null;
59
+ const version =
60
+ typeof parsed.version === 'string' && parsed.version.trim()
61
+ ? parsed.version.trim()
62
+ : null;
57
63
  return { name, version };
58
64
  } catch {
59
65
  return { name: null, version: null };
@@ -88,9 +94,10 @@ function findNearestPackageRoot(startPath: string | undefined): string | null {
88
94
  let current: string;
89
95
  try {
90
96
  const resolved = path.resolve(startPath);
91
- current = fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()
92
- ? resolved
93
- : path.dirname(resolved);
97
+ current =
98
+ fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()
99
+ ? resolved
100
+ : path.dirname(resolved);
94
101
  } catch {
95
102
  return null;
96
103
  }
@@ -134,14 +141,24 @@ function detectPackageManager(): PackageManager {
134
141
  return 'npm';
135
142
  }
136
143
 
137
- function detectInstallContext(packageName: string, entryPath: string | undefined): InstallContext {
144
+ function detectInstallContext(
145
+ packageName: string,
146
+ entryPath: string | undefined,
147
+ ): InstallContext {
138
148
  const preferredManager = detectPackageManager();
139
149
  const entryRoot = findNearestPackageRoot(entryPath);
140
150
  const cwdRoot = findNearestPackageRoot(process.cwd());
141
151
  const cwdInfo = readPackageInfo(path.join(process.cwd(), 'package.json'));
142
152
 
143
- if (cwdInfo.name === packageName && fs.existsSync(path.join(process.cwd(), '.git'))) {
144
- return { kind: 'source', root: process.cwd(), packageManager: preferredManager };
153
+ if (
154
+ cwdInfo.name === packageName &&
155
+ fs.existsSync(path.join(process.cwd(), '.git'))
156
+ ) {
157
+ return {
158
+ kind: 'source',
159
+ root: process.cwd(),
160
+ packageManager: preferredManager,
161
+ };
145
162
  }
146
163
 
147
164
  if (!entryRoot) {
@@ -157,15 +174,27 @@ function detectInstallContext(packageName: string, entryPath: string | undefined
157
174
  packageManager: preferredManager,
158
175
  };
159
176
  }
160
- return { kind: 'unknown', root: entryRoot, packageManager: preferredManager };
177
+ return {
178
+ kind: 'unknown',
179
+ root: entryRoot,
180
+ packageManager: preferredManager,
181
+ };
161
182
  }
162
183
 
163
184
  if (fs.existsSync(path.join(entryRoot, '.git'))) {
164
- return { kind: 'source', root: entryRoot, packageManager: preferredManager };
185
+ return {
186
+ kind: 'source',
187
+ root: entryRoot,
188
+ packageManager: preferredManager,
189
+ };
165
190
  }
166
191
 
167
192
  if (entryRoot.includes(`${path.sep}node_modules${path.sep}`)) {
168
- return { kind: 'package', root: entryRoot, packageManager: preferredManager };
193
+ return {
194
+ kind: 'package',
195
+ root: entryRoot,
196
+ packageManager: preferredManager,
197
+ };
169
198
  }
170
199
 
171
200
  return { kind: 'unknown', root: entryRoot, packageManager: preferredManager };
@@ -179,7 +208,12 @@ function parseSemver(value: string): ParsedSemver | null {
179
208
  const major = Number.parseInt(match[1], 10);
180
209
  const minor = Number.parseInt(match[2], 10);
181
210
  const patch = Number.parseInt(match[3], 10);
182
- if (!Number.isFinite(major) || !Number.isFinite(minor) || !Number.isFinite(patch)) return null;
211
+ if (
212
+ !Number.isFinite(major) ||
213
+ !Number.isFinite(minor) ||
214
+ !Number.isFinite(patch)
215
+ )
216
+ return null;
183
217
 
184
218
  return {
185
219
  major,
@@ -239,7 +273,9 @@ function commandAvailable(command: string): boolean {
239
273
  return result.status === 0;
240
274
  }
241
275
 
242
- function resolveAvailablePackageManager(preferred: PackageManager): PackageManager | null {
276
+ function resolveAvailablePackageManager(
277
+ preferred: PackageManager,
278
+ ): PackageManager | null {
243
279
  const order: PackageManager[] = [preferred, 'npm', 'pnpm', 'yarn', 'bun'];
244
280
  const checked = new Set<PackageManager>();
245
281
  for (const candidate of order) {
@@ -250,7 +286,10 @@ function resolveAvailablePackageManager(preferred: PackageManager): PackageManag
250
286
  return null;
251
287
  }
252
288
 
253
- function buildUpdateCommand(packageManager: PackageManager, packageName: string): UpdateCommand {
289
+ function buildUpdateCommand(
290
+ packageManager: PackageManager,
291
+ packageName: string,
292
+ ): UpdateCommand {
254
293
  switch (packageManager) {
255
294
  case 'pnpm': {
256
295
  const args = ['add', '-g', `${packageName}@latest`];
@@ -277,7 +316,9 @@ async function askForConfirmation(message: string): Promise<boolean> {
277
316
  output: process.stdout,
278
317
  });
279
318
  try {
280
- const answer = (await rl.question(`${message} [y/N] `)).trim().toLowerCase();
319
+ const answer = (await rl.question(`${message} [y/N] `))
320
+ .trim()
321
+ .toLowerCase();
281
322
  return answer === 'y' || answer === 'yes';
282
323
  } finally {
283
324
  rl.close();
@@ -304,7 +345,10 @@ Options:
304
345
  --yes, -y Skip confirmation prompt before install`);
305
346
  }
306
347
 
307
- export async function runUpdateCommand(args: string[], currentVersion: string): Promise<void> {
348
+ export async function runUpdateCommand(
349
+ args: string[],
350
+ currentVersion: string,
351
+ ): Promise<void> {
308
352
  const options = parseUpdateArgs(args);
309
353
  if (options.help) {
310
354
  printUpdateUsage();
@@ -314,7 +358,9 @@ export async function runUpdateCommand(args: string[], currentVersion: string):
314
358
  const packageName = resolvePackageName(process.argv[1]);
315
359
  const install = detectInstallContext(packageName, process.argv[1]);
316
360
  const latest = fetchLatestVersion(packageName);
317
- const comparison = latest.version ? compareSemver(currentVersion, latest.version) : null;
361
+ const comparison = latest.version
362
+ ? compareSemver(currentVersion, latest.version)
363
+ : null;
318
364
 
319
365
  console.log(`Current version: ${currentVersion}`);
320
366
  if (latest.version) {
@@ -325,7 +371,9 @@ export async function runUpdateCommand(args: string[], currentVersion: string):
325
371
 
326
372
  if (install.kind === 'source') {
327
373
  console.log('');
328
- console.log(`Source checkout detected at ${install.root || process.cwd()}.`);
374
+ console.log(
375
+ `Source checkout detected at ${install.root || process.cwd()}.`,
376
+ );
329
377
  console.log('To update, run:');
330
378
  console.log(' git pull --rebase');
331
379
  console.log(' npm install');
@@ -342,14 +390,20 @@ export async function runUpdateCommand(args: string[], currentVersion: string):
342
390
  } else if (latest.version && comparison === 0) {
343
391
  console.log('HybridClaw is already up to date.');
344
392
  } else if (latest.version && comparison === 1) {
345
- console.log('Installed version is newer than npm latest; skipping automatic update.');
393
+ console.log(
394
+ 'Installed version is newer than npm latest; skipping automatic update.',
395
+ );
346
396
  } else if (latest.version) {
347
- console.log('Version comparison unavailable; semver format not recognized.');
397
+ console.log(
398
+ 'Version comparison unavailable; semver format not recognized.',
399
+ );
348
400
  }
349
401
 
350
402
  const manager = resolveAvailablePackageManager(install.packageManager);
351
403
  if (!manager) {
352
- throw new Error('No supported package manager found (npm, pnpm, yarn, bun).');
404
+ throw new Error(
405
+ 'No supported package manager found (npm, pnpm, yarn, bun).',
406
+ );
353
407
  }
354
408
  const updateCommand = buildUpdateCommand(manager, packageName);
355
409
 
@@ -370,7 +424,9 @@ export async function runUpdateCommand(args: string[], currentVersion: string):
370
424
  console.log(`Update command: ${updateCommand.display}`);
371
425
  if (!options.yes) {
372
426
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
373
- console.log('Non-interactive shell detected. Re-run with `--yes` to apply the update.');
427
+ console.log(
428
+ 'Non-interactive shell detected. Re-run with `--yes` to apply the update.',
429
+ );
374
430
  return;
375
431
  }
376
432
  const confirmed = await askForConfirmation('Proceed with update now?');
package/src/workspace.ts CHANGED
@@ -3,11 +3,10 @@
3
3
  * TOOLS.md, MEMORY.md, HEARTBEAT.md from the agent workspace
4
4
  * and injects them into the system prompt (like OpenClaw).
5
5
  */
6
- import fs from 'fs';
7
- import path from 'path';
8
-
9
- import { logger } from './logger.js';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
10
8
  import { agentWorkspaceDir } from './ipc.js';
9
+ import { logger } from './logger.js';
11
10
  import { truncateHeadTailText } from './token-efficiency.js';
12
11
 
13
12
  const BOOTSTRAP_FILES = [
@@ -21,6 +20,21 @@ const BOOTSTRAP_FILES = [
21
20
  'BOOTSTRAP.md',
22
21
  'BOOT.md',
23
22
  ] as const;
23
+ const POLICY_RELATIVE_PATH = path.join('.hybridclaw', 'policy.yaml');
24
+ const DEFAULT_POLICY_TEMPLATE = `approval:
25
+ pinned_red:
26
+ - pattern: "rm -rf /"
27
+ - paths: ["~/.ssh/**", "/etc/**", ".env*"]
28
+ - tools: ["force_push"]
29
+
30
+ workspace_fence: true
31
+ max_pending_approvals: 3
32
+ approval_timeout_secs: 120
33
+
34
+ audit:
35
+ log_all_red: true
36
+ log_denials: true
37
+ `;
24
38
 
25
39
  const MAX_FILE_CHARS = 20_000;
26
40
  const TEMPLATES_DIR = path.join(process.cwd(), 'templates');
@@ -47,6 +61,25 @@ export function ensureBootstrapFiles(agentId: string): void {
47
61
  logger.debug({ agentId, file: filename }, 'Copied bootstrap template');
48
62
  }
49
63
  }
64
+
65
+ const policyDestPath = path.join(wsDir, POLICY_RELATIVE_PATH);
66
+ if (!fs.existsSync(policyDestPath)) {
67
+ fs.mkdirSync(path.dirname(policyDestPath), { recursive: true });
68
+ const repoPolicyPath = path.join(process.cwd(), POLICY_RELATIVE_PATH);
69
+ if (fs.existsSync(repoPolicyPath)) {
70
+ fs.copyFileSync(repoPolicyPath, policyDestPath);
71
+ logger.debug(
72
+ { agentId, file: POLICY_RELATIVE_PATH },
73
+ 'Copied approval policy from repository',
74
+ );
75
+ } else {
76
+ fs.writeFileSync(policyDestPath, DEFAULT_POLICY_TEMPLATE, 'utf-8');
77
+ logger.debug(
78
+ { agentId, file: POLICY_RELATIVE_PATH },
79
+ 'Wrote default approval policy template',
80
+ );
81
+ }
82
+ }
50
83
  }
51
84
 
52
85
  /**
@@ -71,7 +104,10 @@ export function loadBootstrapFiles(agentId: string): ContextFile[] {
71
104
 
72
105
  files.push({ name: filename, content });
73
106
  } catch (err) {
74
- logger.warn({ agentId, file: filename, err }, 'Failed to read bootstrap file');
107
+ logger.warn(
108
+ { agentId, file: filename, err },
109
+ 'Failed to read bootstrap file',
110
+ );
75
111
  }
76
112
  }
77
113
 
@@ -83,7 +119,10 @@ export function loadBootstrapFiles(agentId: string): ContextFile[] {
83
119
  * e.g. "Tuesday, February 24th, 2026 — 14:32"
84
120
  */
85
121
  function formatCurrentTime(timezone?: string): string {
86
- const tz = timezone?.trim() || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
122
+ const tz =
123
+ timezone?.trim() ||
124
+ Intl.DateTimeFormat().resolvedOptions().timeZone ||
125
+ 'UTC';
87
126
  const now = new Date();
88
127
  try {
89
128
  const parts = new Intl.DateTimeFormat('en-US', {
@@ -100,14 +139,27 @@ function formatCurrentTime(timezone?: string): string {
100
139
  for (const part of parts) {
101
140
  if (part.type !== 'literal') map[part.type] = part.value;
102
141
  }
103
- if (!map.weekday || !map.year || !map.month || !map.day || !map.hour || !map.minute) {
142
+ if (
143
+ !map.weekday ||
144
+ !map.year ||
145
+ !map.month ||
146
+ !map.day ||
147
+ !map.hour ||
148
+ !map.minute
149
+ ) {
104
150
  return now.toISOString();
105
151
  }
106
152
  const dayNum = parseInt(map.day, 10);
107
- const suffix = dayNum >= 11 && dayNum <= 13 ? 'th'
108
- : dayNum % 10 === 1 ? 'st'
109
- : dayNum % 10 === 2 ? 'nd'
110
- : dayNum % 10 === 3 ? 'rd' : 'th';
153
+ const suffix =
154
+ dayNum >= 11 && dayNum <= 13
155
+ ? 'th'
156
+ : dayNum % 10 === 1
157
+ ? 'st'
158
+ : dayNum % 10 === 2
159
+ ? 'nd'
160
+ : dayNum % 10 === 3
161
+ ? 'rd'
162
+ : 'th';
111
163
  return `${map.weekday}, ${map.month} ${dayNum}${suffix}, ${map.year} — ${map.hour}:${map.minute} (${tz})`;
112
164
  } catch {
113
165
  return now.toISOString();
@@ -0,0 +1,224 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { describe, expect, test } from 'vitest';
5
+
6
+ import { TrustedCoworkerApprovalRuntime } from '../container/src/approval-policy.js';
7
+ import type { ChatMessage } from '../container/src/types.js';
8
+
9
+ function userMessage(text: string): ChatMessage {
10
+ return { role: 'user', content: text };
11
+ }
12
+
13
+ function tempTrustStorePath(name: string): string {
14
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hybridclaw-approval-'));
15
+ return path.join(dir, `${name}.json`);
16
+ }
17
+
18
+ describe('TrustedCoworkerApprovalRuntime', () => {
19
+ test('yellow actions promote to green after successful repeat', () => {
20
+ const runtime = new TrustedCoworkerApprovalRuntime(
21
+ '/tmp/hybridclaw-missing-policy.yaml',
22
+ );
23
+
24
+ const first = runtime.evaluateToolCall({
25
+ toolName: 'bash',
26
+ argsJson: JSON.stringify({ command: 'npm install' }),
27
+ latestUserPrompt: 'Install dependencies',
28
+ });
29
+ expect(first.tier).toBe('yellow');
30
+ expect(first.decision).toBe('implicit');
31
+
32
+ runtime.afterToolExecution(first, true);
33
+
34
+ const second = runtime.evaluateToolCall({
35
+ toolName: 'bash',
36
+ argsJson: JSON.stringify({ command: 'npm install' }),
37
+ latestUserPrompt: 'Install dependencies',
38
+ });
39
+ expect(second.tier).toBe('green');
40
+ });
41
+
42
+ test('sensitive paths stay pinned red and require explicit approval', () => {
43
+ const runtime = new TrustedCoworkerApprovalRuntime(
44
+ '/tmp/hybridclaw-missing-policy.yaml',
45
+ );
46
+
47
+ const evaluation = runtime.evaluateToolCall({
48
+ toolName: 'write',
49
+ argsJson: JSON.stringify({ path: '.env', contents: 'API_KEY=abc' }),
50
+ latestUserPrompt: 'Write env file',
51
+ });
52
+
53
+ expect(evaluation.baseTier).toBe('red');
54
+ expect(evaluation.decision).toBe('required');
55
+ expect(evaluation.pinned).toBe(true);
56
+ expect(evaluation.requestId).toBeTruthy();
57
+ });
58
+
59
+ test('yes response approves once and replays original prompt', () => {
60
+ const runtime = new TrustedCoworkerApprovalRuntime(
61
+ '/tmp/hybridclaw-missing-policy.yaml',
62
+ );
63
+ const originalPrompt = 'Delete dist and rebuild cleanly';
64
+
65
+ const pending = runtime.evaluateToolCall({
66
+ toolName: 'bash',
67
+ argsJson: JSON.stringify({ command: 'rm -rf dist' }),
68
+ latestUserPrompt: originalPrompt,
69
+ });
70
+ expect(pending.decision).toBe('required');
71
+
72
+ const prelude = runtime.handleApprovalResponse([userMessage('yes')]);
73
+ expect(prelude?.replayPrompt).toBe(originalPrompt);
74
+ expect(prelude?.approvalMode).toBe('once');
75
+
76
+ const approved = runtime.evaluateToolCall({
77
+ toolName: 'bash',
78
+ argsJson: JSON.stringify({ command: 'rm -rf dist' }),
79
+ latestUserPrompt: originalPrompt,
80
+ });
81
+ expect(approved.decision).toBe('approved_once');
82
+ });
83
+
84
+ test('yes for session persists trust for repeated action key', () => {
85
+ const runtime = new TrustedCoworkerApprovalRuntime(
86
+ '/tmp/hybridclaw-missing-policy.yaml',
87
+ );
88
+ const originalPrompt = 'Fetch from example.com';
89
+
90
+ const first = runtime.evaluateToolCall({
91
+ toolName: 'web_fetch',
92
+ argsJson: JSON.stringify({ url: 'https://example.com' }),
93
+ latestUserPrompt: originalPrompt,
94
+ });
95
+ expect(first.decision).toBe('required');
96
+
97
+ const prelude = runtime.handleApprovalResponse([
98
+ userMessage('yes for session'),
99
+ ]);
100
+ expect(prelude?.approvalMode).toBe('session');
101
+
102
+ const second = runtime.evaluateToolCall({
103
+ toolName: 'web_fetch',
104
+ argsJson: JSON.stringify({ url: 'https://example.com' }),
105
+ latestUserPrompt: originalPrompt,
106
+ });
107
+ expect(second.decision).toBe('approved_session');
108
+ });
109
+
110
+ test('pinned red cannot be session-trusted across runs', () => {
111
+ const runtime = new TrustedCoworkerApprovalRuntime(
112
+ '/tmp/hybridclaw-missing-policy.yaml',
113
+ );
114
+ const prompt = 'Append token to .env';
115
+
116
+ const first = runtime.evaluateToolCall({
117
+ toolName: 'write',
118
+ argsJson: JSON.stringify({ path: '.env', contents: 'TOKEN=x' }),
119
+ latestUserPrompt: prompt,
120
+ });
121
+ expect(first.decision).toBe('required');
122
+ expect(first.pinned).toBe(true);
123
+
124
+ const prelude = runtime.handleApprovalResponse([
125
+ userMessage('yes for session'),
126
+ ]);
127
+ expect(prelude?.approvalMode).toBe('once');
128
+
129
+ const second = runtime.evaluateToolCall({
130
+ toolName: 'write',
131
+ argsJson: JSON.stringify({ path: '.env', contents: 'TOKEN=x' }),
132
+ latestUserPrompt: prompt,
133
+ });
134
+ expect(second.decision).toBe('approved_once');
135
+
136
+ const third = runtime.evaluateToolCall({
137
+ toolName: 'write',
138
+ argsJson: JSON.stringify({ path: '.env', contents: 'TOKEN=x' }),
139
+ latestUserPrompt: prompt,
140
+ });
141
+ expect(third.decision).toBe('required');
142
+ });
143
+
144
+ test('yes for agent persists trust across runtime restarts', () => {
145
+ const trustStorePath = tempTrustStorePath('agent-trust');
146
+ const policyPath = '/tmp/hybridclaw-missing-policy.yaml';
147
+ const prompt = 'Fetch from example.com';
148
+ const argsJson = JSON.stringify({ url: 'https://example.com' });
149
+
150
+ const runtime = new TrustedCoworkerApprovalRuntime(
151
+ policyPath,
152
+ trustStorePath,
153
+ );
154
+ const first = runtime.evaluateToolCall({
155
+ toolName: 'web_fetch',
156
+ argsJson,
157
+ latestUserPrompt: prompt,
158
+ });
159
+ expect(first.decision).toBe('required');
160
+
161
+ const prelude = runtime.handleApprovalResponse([
162
+ userMessage('yes for agent'),
163
+ ]);
164
+ expect(prelude?.approvalMode).toBe('agent');
165
+
166
+ const second = runtime.evaluateToolCall({
167
+ toolName: 'web_fetch',
168
+ argsJson,
169
+ latestUserPrompt: prompt,
170
+ });
171
+ expect(second.decision).toBe('approved_agent');
172
+
173
+ const restarted = new TrustedCoworkerApprovalRuntime(
174
+ policyPath,
175
+ trustStorePath,
176
+ );
177
+ const third = restarted.evaluateToolCall({
178
+ toolName: 'web_fetch',
179
+ argsJson,
180
+ latestUserPrompt: prompt,
181
+ });
182
+ expect(third.decision).toBe('approved_agent');
183
+ });
184
+
185
+ test('pinned red cannot be agent-trusted across restarts', () => {
186
+ const trustStorePath = tempTrustStorePath('pinned-agent');
187
+ const policyPath = '/tmp/hybridclaw-missing-policy.yaml';
188
+ const prompt = 'Write .env';
189
+ const argsJson = JSON.stringify({ path: '.env', contents: 'TOKEN=abc' });
190
+
191
+ const runtime = new TrustedCoworkerApprovalRuntime(
192
+ policyPath,
193
+ trustStorePath,
194
+ );
195
+ const first = runtime.evaluateToolCall({
196
+ toolName: 'write',
197
+ argsJson,
198
+ latestUserPrompt: prompt,
199
+ });
200
+ expect(first.decision).toBe('required');
201
+ expect(first.pinned).toBe(true);
202
+
203
+ const prelude = runtime.handleApprovalResponse([userMessage('3')]);
204
+ expect(prelude?.approvalMode).toBe('once');
205
+
206
+ const second = runtime.evaluateToolCall({
207
+ toolName: 'write',
208
+ argsJson,
209
+ latestUserPrompt: prompt,
210
+ });
211
+ expect(second.decision).toBe('approved_once');
212
+
213
+ const restarted = new TrustedCoworkerApprovalRuntime(
214
+ policyPath,
215
+ trustStorePath,
216
+ );
217
+ const third = restarted.evaluateToolCall({
218
+ toolName: 'write',
219
+ argsJson,
220
+ latestUserPrompt: prompt,
221
+ });
222
+ expect(third.decision).toBe('required');
223
+ });
224
+ });
@@ -1,7 +1,11 @@
1
1
  import { expect, test } from 'vitest';
2
2
 
3
3
  import { buildResponseText } from '../src/channels/discord/delivery.js';
4
- import { rewriteUserMentions, type MentionLookup } from '../src/channels/discord/mentions.js';
4
+ import { isTrigger, parseCommand } from '../src/channels/discord/inbound.js';
5
+ import {
6
+ type MentionLookup,
7
+ rewriteUserMentions,
8
+ } from '../src/channels/discord/mentions.js';
5
9
 
6
10
  function createLookup(entries: Record<string, string[]>): MentionLookup {
7
11
  const byAlias = new Map<string, Set<string>>();
@@ -18,7 +22,9 @@ test('rewriteUserMentions rewrites a uniquely-resolved @alias', () => {
18
22
  });
19
23
 
20
24
  test('rewriteUserMentions does not rewrite ambiguous aliases', () => {
21
- const lookup = createLookup({ bob: ['111111111111111111', '222222222222222222'] });
25
+ const lookup = createLookup({
26
+ bob: ['111111111111111111', '222222222222222222'],
27
+ });
22
28
  const output = rewriteUserMentions('hi @bob', lookup);
23
29
  expect(output).toBe('hi @bob');
24
30
  });
@@ -41,3 +47,77 @@ test('buildResponseText leaves text unchanged when no tools were used', () => {
41
47
  const output = buildResponseText('Done.');
42
48
  expect(output).toBe('Done.');
43
49
  });
50
+
51
+ test('isTrigger blocks non-command chatter when channel mode is off', () => {
52
+ const shouldTrigger = isTrigger({
53
+ content: 'hello',
54
+ isDm: false,
55
+ commandsOnly: false,
56
+ respondToAllMessages: false,
57
+ guildMessageMode: 'off',
58
+ prefix: '!claw',
59
+ botMentionRegex: null,
60
+ hasBotMention: false,
61
+ });
62
+ expect(shouldTrigger).toBe(false);
63
+ });
64
+
65
+ test('isTrigger still allows prefixed commands when channel mode is off', () => {
66
+ const shouldTrigger = isTrigger({
67
+ content: '!claw status',
68
+ isDm: false,
69
+ commandsOnly: false,
70
+ respondToAllMessages: false,
71
+ guildMessageMode: 'off',
72
+ prefix: '!claw',
73
+ botMentionRegex: null,
74
+ hasBotMention: false,
75
+ });
76
+ expect(shouldTrigger).toBe(true);
77
+ });
78
+
79
+ test('isTrigger allows free-response mode in guild channels', () => {
80
+ const shouldTrigger = isTrigger({
81
+ content: 'Can you review this patch?',
82
+ isDm: false,
83
+ commandsOnly: false,
84
+ respondToAllMessages: false,
85
+ guildMessageMode: 'free',
86
+ prefix: '!claw',
87
+ botMentionRegex: null,
88
+ hasBotMention: false,
89
+ });
90
+ expect(shouldTrigger).toBe(true);
91
+ });
92
+
93
+ test('isTrigger keeps mention mode even when respondToAllMessages is enabled', () => {
94
+ const shouldTrigger = isTrigger({
95
+ content: 'hello',
96
+ isDm: false,
97
+ commandsOnly: false,
98
+ respondToAllMessages: true,
99
+ guildMessageMode: 'mention',
100
+ prefix: '!claw',
101
+ botMentionRegex: null,
102
+ hasBotMention: false,
103
+ });
104
+ expect(shouldTrigger).toBe(false);
105
+ });
106
+
107
+ test('parseCommand recognizes channel command namespace', () => {
108
+ const parsed = parseCommand('!claw channel mode free', null, '!claw');
109
+ expect(parsed).toEqual({
110
+ isCommand: true,
111
+ command: 'channel',
112
+ args: ['mode', 'free'],
113
+ });
114
+ });
115
+
116
+ test('parseCommand recognizes usage command namespace', () => {
117
+ const parsed = parseCommand('!claw usage monthly', null, '!claw');
118
+ expect(parsed).toEqual({
119
+ isCommand: true,
120
+ command: 'usage',
121
+ args: ['monthly'],
122
+ });
123
+ });