@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/cli.ts CHANGED
@@ -1,15 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { spawn, spawnSync } from 'child_process';
3
4
  import fs from 'fs';
4
5
  import path from 'path';
5
- import { spawn, spawnSync } from 'child_process';
6
6
  import readline from 'readline/promises';
7
-
7
+ import {
8
+ APP_VERSION,
9
+ DATA_DIR,
10
+ GATEWAY_BASE_URL,
11
+ MissingRequiredEnvVarError,
12
+ } from './config.js';
8
13
  import { ensureHybridAICredentials } from './onboarding.js';
9
- import { APP_VERSION, DATA_DIR, GATEWAY_BASE_URL, MissingRequiredEnvVarError } from './config.js';
10
14
  import { printUpdateUsage, runUpdateCommand } from './update.js';
11
15
 
12
- async function ensureRuntimeContainer(commandName: string, required = true): Promise<void> {
16
+ async function ensureRuntimeContainer(
17
+ commandName: string,
18
+ required = true,
19
+ ): Promise<void> {
13
20
  const { ensureContainerImageReady } = await import('./container-setup.js');
14
21
  await ensureContainerImageReady({
15
22
  commandName,
@@ -43,13 +50,15 @@ async function ensureGatewayForTui(commandName: string): Promise<void> {
43
50
  return;
44
51
  }
45
52
 
46
- console.log(`${commandName}: Gateway not found. Starting gateway backend at ${GATEWAY_BASE_URL}.`);
53
+ console.log(
54
+ `${commandName}: Gateway not found. Starting gateway backend at ${GATEWAY_BASE_URL}.`,
55
+ );
47
56
  await startGatewayBackend(commandName, true);
48
57
 
49
58
  if (!(await isGatewayReachable())) {
50
59
  throw new Error(
51
- `Gateway did not become available at ${GATEWAY_BASE_URL} after startup.`
52
- + ' Please run `hybridclaw gateway start --foreground` in another terminal and try again.',
60
+ `Gateway did not become available at ${GATEWAY_BASE_URL} after startup.` +
61
+ ' Please run `hybridclaw gateway start --foreground` in another terminal and try again.',
53
62
  );
54
63
  }
55
64
  }
@@ -81,17 +90,17 @@ function formatInstructionDiffLine(file: {
81
90
  ];
82
91
  }
83
92
 
84
- async function ensureTuiInstructionApproval(commandName: string): Promise<void> {
93
+ async function ensureTuiInstructionApproval(
94
+ commandName: string,
95
+ ): Promise<void> {
85
96
  const {
86
97
  approveInstructionBaseline,
87
98
  INSTRUCTION_BASELINE_PATH,
88
99
  summarizeInstructionIntegrity,
89
100
  verifyInstructionBaseline,
90
101
  } = await import('./instruction-integrity.js');
91
- const {
92
- beginInstructionApprovalAudit,
93
- completeInstructionApprovalAudit,
94
- } = await import('./instruction-approval-audit.js');
102
+ const { beginInstructionApprovalAudit, completeInstructionApprovalAudit } =
103
+ await import('./instruction-approval-audit.js');
95
104
 
96
105
  const result = verifyInstructionBaseline();
97
106
  if (result.ok) return;
@@ -107,7 +116,9 @@ async function ensureTuiInstructionApproval(commandName: string): Promise<void>
107
116
  console.error(`Instruction baseline is invalid: ${result.baselineError}`);
108
117
  console.error(`Baseline path: ${INSTRUCTION_BASELINE_PATH}`);
109
118
  } else if (!result.baseline) {
110
- console.error(`No approved instruction baseline found at ${INSTRUCTION_BASELINE_PATH}.`);
119
+ console.error(
120
+ `No approved instruction baseline found at ${INSTRUCTION_BASELINE_PATH}.`,
121
+ );
111
122
  }
112
123
 
113
124
  const changed = result.files.filter((file) => file.status !== 'ok');
@@ -133,10 +144,17 @@ async function ensureTuiInstructionApproval(commandName: string): Promise<void>
133
144
  );
134
145
  }
135
146
 
136
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
147
+ const rl = readline.createInterface({
148
+ input: process.stdin,
149
+ output: process.stdout,
150
+ });
137
151
  let answer = '';
138
152
  try {
139
- answer = (await rl.question('Approve current instruction changes now? [y/N] ')).trim().toLowerCase();
153
+ answer = (
154
+ await rl.question('Approve current instruction changes now? [y/N] ')
155
+ )
156
+ .trim()
157
+ .toLowerCase();
140
158
  } finally {
141
159
  rl.close();
142
160
  }
@@ -156,7 +174,9 @@ async function ensureTuiInstructionApproval(commandName: string): Promise<void>
156
174
 
157
175
  try {
158
176
  const baseline = approveInstructionBaseline();
159
- console.log(`Approved instruction baseline at ${INSTRUCTION_BASELINE_PATH} (${baseline.approvedAt}).`);
177
+ console.log(
178
+ `Approved instruction baseline at ${INSTRUCTION_BASELINE_PATH} (${baseline.approvedAt}).`,
179
+ );
160
180
  completeInstructionApprovalAudit({
161
181
  context: auditContext,
162
182
  approved: true,
@@ -186,7 +206,10 @@ function printMainUsage(): void {
186
206
  onboarding Run HybridAI account/API key onboarding
187
207
  update Check and apply HybridClaw CLI updates
188
208
  audit Inspect/verify structured audit trail
189
- help Show general or topic-specific help (e.g. \`hybridclaw help gateway\`)`);
209
+ help Show general or topic-specific help (e.g. \`hybridclaw help gateway\`)
210
+
211
+ Options:
212
+ --version, -v Show HybridClaw CLI version`);
190
213
  }
191
214
 
192
215
  function printGatewayUsage(): void {
@@ -309,12 +332,19 @@ function readGatewayPid(): GatewayPidState | null {
309
332
  try {
310
333
  const raw = fs.readFileSync(GATEWAY_PID_PATH, 'utf-8');
311
334
  const parsed = JSON.parse(raw) as Partial<GatewayPidState>;
312
- if (!parsed || typeof parsed.pid !== 'number' || !Number.isFinite(parsed.pid)) return null;
335
+ if (
336
+ !parsed ||
337
+ typeof parsed.pid !== 'number' ||
338
+ !Number.isFinite(parsed.pid)
339
+ )
340
+ return null;
313
341
  return {
314
342
  pid: parsed.pid,
315
343
  startedAt: typeof parsed.startedAt === 'string' ? parsed.startedAt : '',
316
344
  cwd: typeof parsed.cwd === 'string' ? parsed.cwd : '',
317
- command: Array.isArray(parsed.command) ? parsed.command.map((item) => String(item)) : [],
345
+ command: Array.isArray(parsed.command)
346
+ ? parsed.command.map((item) => String(item))
347
+ : [],
318
348
  };
319
349
  } catch {
320
350
  return null;
@@ -364,7 +394,11 @@ function parseGatewayBaseUrl(): URL | null {
364
394
 
365
395
  function isLocalGatewayHost(hostname: string): boolean {
366
396
  const normalized = hostname.trim().toLowerCase();
367
- return normalized === '127.0.0.1' || normalized === 'localhost' || normalized === '::1';
397
+ return (
398
+ normalized === '127.0.0.1' ||
399
+ normalized === 'localhost' ||
400
+ normalized === '::1'
401
+ );
368
402
  }
369
403
 
370
404
  function resolveGatewayListenPort(url: URL): number {
@@ -380,9 +414,13 @@ function findGatewayPidByPort(): number | null {
380
414
  if (!parsed || !isLocalGatewayHost(parsed.hostname)) return null;
381
415
  const port = resolveGatewayListenPort(parsed);
382
416
 
383
- const result = spawnSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'], {
384
- encoding: 'utf-8',
385
- });
417
+ const result = spawnSync(
418
+ 'lsof',
419
+ ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'],
420
+ {
421
+ encoding: 'utf-8',
422
+ },
423
+ );
386
424
  if (result.error) return null;
387
425
  const output = (result.stdout || '').trim();
388
426
  if (!output) return null;
@@ -408,12 +446,14 @@ function adoptGatewayPid(pid: number, source: string): boolean {
408
446
  async function adoptReachableGatewayIfPossible(): Promise<boolean> {
409
447
  const { gatewayStatus } = await import('./gateway-client.js');
410
448
  const status = await gatewayStatus();
411
- const pid = typeof status.pid === 'number' && Number.isFinite(status.pid)
412
- ? Math.floor(status.pid)
413
- : 0;
449
+ const pid =
450
+ typeof status.pid === 'number' && Number.isFinite(status.pid)
451
+ ? Math.floor(status.pid)
452
+ : 0;
414
453
  if (pid > 0 && adoptGatewayPid(pid, 'api-status')) return true;
415
454
  const fallbackPid = findGatewayPidByPort();
416
- if (fallbackPid && adoptGatewayPid(fallbackPid, 'lsof-port-probe')) return true;
455
+ if (fallbackPid && adoptGatewayPid(fallbackPid, 'lsof-port-probe'))
456
+ return true;
417
457
  return false;
418
458
  }
419
459
 
@@ -423,11 +463,16 @@ async function runGatewayForeground(commandName: string): Promise<void> {
423
463
  await import('./gateway.js');
424
464
  }
425
465
 
426
- async function startGatewayBackend(commandName: string, waitForHealthy = false): Promise<void> {
466
+ async function startGatewayBackend(
467
+ commandName: string,
468
+ waitForHealthy = false,
469
+ ): Promise<void> {
427
470
  if (await isGatewayReachable()) {
428
471
  const existing = readGatewayPid();
429
472
  if (existing && isPidRunning(existing.pid)) {
430
- console.log(`Gateway already running in backend mode (pid ${existing.pid}).`);
473
+ console.log(
474
+ `Gateway already running in backend mode (pid ${existing.pid}).`,
475
+ );
431
476
  } else {
432
477
  let adopted = false;
433
478
  try {
@@ -437,9 +482,13 @@ async function startGatewayBackend(commandName: string, waitForHealthy = false):
437
482
  }
438
483
  if (adopted) {
439
484
  const adoptedState = readGatewayPid();
440
- console.log(`Gateway already reachable at ${GATEWAY_BASE_URL}; adopted pid ${adoptedState?.pid || '(unknown)'}.`);
485
+ console.log(
486
+ `Gateway already reachable at ${GATEWAY_BASE_URL}; adopted pid ${adoptedState?.pid || '(unknown)'}.`,
487
+ );
441
488
  } else {
442
- console.log(`Gateway already reachable at ${GATEWAY_BASE_URL} (unmanaged by CLI PID file).`);
489
+ console.log(
490
+ `Gateway already reachable at ${GATEWAY_BASE_URL} (unmanaged by CLI PID file).`,
491
+ );
443
492
  }
444
493
  }
445
494
  return;
@@ -451,12 +500,14 @@ async function startGatewayBackend(commandName: string, waitForHealthy = false):
451
500
  const healthy = await waitForGatewayReachable(15_000);
452
501
  if (!healthy) {
453
502
  throw new Error(
454
- `Gateway process ${existing.pid} exists but did not become reachable at ${GATEWAY_BASE_URL}.`
455
- + ` Check logs: ${GATEWAY_LOG_PATH}`,
503
+ `Gateway process ${existing.pid} exists but did not become reachable at ${GATEWAY_BASE_URL}.` +
504
+ ` Check logs: ${GATEWAY_LOG_PATH}`,
456
505
  );
457
506
  }
458
507
  }
459
- console.log(`Gateway already running in backend mode (pid ${existing.pid}).`);
508
+ console.log(
509
+ `Gateway already running in backend mode (pid ${existing.pid}).`,
510
+ );
460
511
  return;
461
512
  }
462
513
  if (existing && !isPidRunning(existing.pid)) {
@@ -494,8 +545,8 @@ async function startGatewayBackend(commandName: string, waitForHealthy = false):
494
545
  const healthy = await waitForGatewayReachable(20_000);
495
546
  if (!healthy) {
496
547
  throw new Error(
497
- `Gateway backend started (pid ${child.pid}) but not reachable at ${GATEWAY_BASE_URL}.`
498
- + ` Check logs: ${GATEWAY_LOG_PATH}`,
548
+ `Gateway backend started (pid ${child.pid}) but not reachable at ${GATEWAY_BASE_URL}.` +
549
+ ` Check logs: ${GATEWAY_LOG_PATH}`,
499
550
  );
500
551
  }
501
552
  }
@@ -514,7 +565,9 @@ async function stopManagedGatewayByPid(state: GatewayPidState): Promise<void> {
514
565
  process.kill(state.pid, 'SIGTERM');
515
566
  } catch (err) {
516
567
  removeGatewayPidFile();
517
- throw new Error(`Failed to stop gateway pid ${state.pid}: ${err instanceof Error ? err.message : String(err)}`);
568
+ throw new Error(
569
+ `Failed to stop gateway pid ${state.pid}: ${err instanceof Error ? err.message : String(err)}`,
570
+ );
518
571
  }
519
572
 
520
573
  const deadline = Date.now() + 10_000;
@@ -538,7 +591,9 @@ async function stopManagedGatewayByPid(state: GatewayPidState): Promise<void> {
538
591
  console.log(`Gateway stop timed out; sent SIGKILL to pid ${state.pid}.`);
539
592
  }
540
593
 
541
- async function stopUnmanagedGatewayGracefully(mode: 'stop' | 'restart'): Promise<void> {
594
+ async function stopUnmanagedGatewayGracefully(
595
+ mode: 'stop' | 'restart',
596
+ ): Promise<void> {
542
597
  const suffix = mode === 'restart' ? ' before restart' : '';
543
598
  console.log('Gateway is reachable but unmanaged by CLI PID file.');
544
599
  console.log(`Requesting graceful shutdown over API${suffix}...`);
@@ -548,8 +603,8 @@ async function stopUnmanagedGatewayGracefully(mode: 'stop' | 'restart'): Promise
548
603
  const stopped = await waitForGatewayUnreachable(10_000);
549
604
  if (!stopped) {
550
605
  throw new Error(
551
- `Gateway remained reachable at ${GATEWAY_BASE_URL} after shutdown request.`
552
- + ' Stop it from its owning process or use your system process manager.',
606
+ `Gateway remained reachable at ${GATEWAY_BASE_URL} after shutdown request.` +
607
+ ' Stop it from its owning process or use your system process manager.',
553
608
  );
554
609
  }
555
610
  console.log('Unmanaged gateway stopped via API request.');
@@ -559,18 +614,24 @@ async function stopUnmanagedGatewayGracefully(mode: 'stop' | 'restart'): Promise
559
614
  }
560
615
 
561
616
  const discoveredPid = findGatewayPidByPort();
562
- if (discoveredPid && discoveredPid !== process.pid && adoptGatewayPid(discoveredPid, 'lsof-port-probe')) {
617
+ if (
618
+ discoveredPid &&
619
+ discoveredPid !== process.pid &&
620
+ adoptGatewayPid(discoveredPid, 'lsof-port-probe')
621
+ ) {
563
622
  const adoptedState = readGatewayPid();
564
623
  if (adoptedState && isPidRunning(adoptedState.pid)) {
565
- console.log(`Shutdown API unavailable; stopping gateway pid ${adoptedState.pid} via local signal.`);
624
+ console.log(
625
+ `Shutdown API unavailable; stopping gateway pid ${adoptedState.pid} via local signal.`,
626
+ );
566
627
  await stopManagedGatewayByPid(adoptedState);
567
628
  return;
568
629
  }
569
630
  }
570
631
 
571
632
  throw new Error(
572
- `Gateway shutdown endpoint is unavailable at ${GATEWAY_BASE_URL} and PID ownership could not be recovered.`
573
- + ' Stop the process manually once, then retry.',
633
+ `Gateway shutdown endpoint is unavailable at ${GATEWAY_BASE_URL} and PID ownership could not be recovered.` +
634
+ ' Stop the process manually once, then retry.',
574
635
  );
575
636
  }
576
637
 
@@ -588,11 +649,15 @@ async function stopGatewayBackend(): Promise<void> {
588
649
  if (!isPidRunning(state.pid)) {
589
650
  removeGatewayPidFile();
590
651
  if (await isGatewayReachable()) {
591
- console.log(`Removed stale gateway PID file (pid ${state.pid} not running).`);
652
+ console.log(
653
+ `Removed stale gateway PID file (pid ${state.pid} not running).`,
654
+ );
592
655
  await stopUnmanagedGatewayGracefully('stop');
593
656
  return;
594
657
  }
595
- console.log(`Removed stale gateway PID file (pid ${state.pid} not running).`);
658
+ console.log(
659
+ `Removed stale gateway PID file (pid ${state.pid} not running).`,
660
+ );
596
661
  return;
597
662
  }
598
663
 
@@ -611,21 +676,29 @@ async function printGatewayLifecycleStatus(): Promise<void> {
611
676
  } else {
612
677
  console.log('PID file: not found');
613
678
  }
614
- console.log(`Gateway API reachable: ${reachable ? 'yes' : 'no'} (${GATEWAY_BASE_URL})`);
679
+ console.log(
680
+ `Gateway API reachable: ${reachable ? 'yes' : 'no'} (${GATEWAY_BASE_URL})`,
681
+ );
615
682
 
616
683
  if (reachable) {
617
684
  try {
618
685
  const { gatewayStatus } = await import('./gateway-client.js');
619
686
  const status = await gatewayStatus();
620
- console.log(`Uptime: ${status.uptime}s | Sessions: ${status.sessions} | Containers: ${status.activeContainers}`);
687
+ console.log(
688
+ `Uptime: ${status.uptime}s | Sessions: ${status.sessions} | Containers: ${status.activeContainers}`,
689
+ );
621
690
  } catch (err) {
622
- console.log(`Gateway status fetch failed: ${err instanceof Error ? err.message : String(err)}`);
691
+ console.log(
692
+ `Gateway status fetch failed: ${err instanceof Error ? err.message : String(err)}`,
693
+ );
623
694
  }
624
695
  }
625
696
  }
626
697
 
627
698
  async function runGatewayApiCommand(args: string[]): Promise<void> {
628
- const { gatewayCommand, renderGatewayCommand } = await import('./gateway-client.js');
699
+ const { gatewayCommand, renderGatewayCommand } = await import(
700
+ './gateway-client.js'
701
+ );
629
702
  const result = await gatewayCommand({
630
703
  sessionId: 'cli:gateway',
631
704
  guildId: null,
@@ -703,6 +776,10 @@ async function main(): Promise<void> {
703
776
  const subargs = process.argv.slice(3);
704
777
 
705
778
  switch (command) {
779
+ case '--version':
780
+ case '-v':
781
+ console.log(APP_VERSION);
782
+ break;
706
783
  case 'gateway':
707
784
  await handleGatewayCommand(subargs);
708
785
  break;
@@ -722,7 +799,10 @@ async function main(): Promise<void> {
722
799
  printOnboardingUsage();
723
800
  break;
724
801
  }
725
- await ensureHybridAICredentials({ force: true, commandName: 'hybridclaw onboarding' });
802
+ await ensureHybridAICredentials({
803
+ force: true,
804
+ commandName: 'hybridclaw onboarding',
805
+ });
726
806
  await ensureRuntimeContainer('hybridclaw onboarding', false);
727
807
  break;
728
808
  case 'update': {
@@ -772,16 +852,21 @@ const envVarHint: Record<string, string> = {
772
852
  };
773
853
 
774
854
  function printMissingEnvVarError(message: string, envVar?: string): void {
775
- const hint = envVar ? envVarHint[envVar] : 'Set this variable and rerun the command.';
855
+ const hint = envVar
856
+ ? envVarHint[envVar]
857
+ : 'Set this variable and rerun the command.';
776
858
  console.error(`hybridclaw error: ${message}`);
777
859
  console.error(`Hint: ${hint}`);
778
- console.error('Make sure you run `hybridclaw` from the directory that contains your .env file.');
860
+ console.error(
861
+ 'Make sure you run `hybridclaw` from the directory that contains your .env file.',
862
+ );
779
863
  }
780
864
 
781
865
  main().catch((err) => {
782
- const missingEnvVarMatch = err instanceof Error
783
- ? err.message.match(/^Missing required env var:\s*([A-Za-z0-9_]+)/)
784
- : null;
866
+ const missingEnvVarMatch =
867
+ err instanceof Error
868
+ ? err.message.match(/^Missing required env var:\s*([A-Za-z0-9_]+)/)
869
+ : null;
785
870
  if (missingEnvVarMatch) {
786
871
  printMissingEnvVarError(err.message, missingEnvVarMatch[1]);
787
872
  } else if (err instanceof MissingRequiredEnvVarError) {