@hybridaione/hybridclaw 0.2.3 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/.github/workflows/ci.yml +70 -0
  2. package/.husky/pre-commit +1 -0
  3. package/CHANGELOG.md +95 -0
  4. package/CONTRIBUTING.md +33 -0
  5. package/README.md +57 -19
  6. package/SECURITY.md +17 -0
  7. package/biome.json +35 -0
  8. package/config.example.json +72 -7
  9. package/container/package-lock.json +2 -2
  10. package/container/package.json +1 -1
  11. package/container/src/approval-policy.ts +1405 -0
  12. package/container/src/browser-tools.ts +672 -136
  13. package/container/src/extensions.ts +36 -12
  14. package/container/src/hybridai-client.ts +65 -16
  15. package/container/src/index.ts +463 -110
  16. package/container/src/ipc.ts +5 -3
  17. package/container/src/token-usage.ts +107 -18
  18. package/container/src/tools.ts +600 -225
  19. package/container/src/types.ts +49 -2
  20. package/container/src/web-fetch.ts +89 -32
  21. package/dist/agent.d.ts.map +1 -1
  22. package/dist/agent.js +15 -4
  23. package/dist/agent.js.map +1 -1
  24. package/dist/audit-cli.d.ts.map +1 -1
  25. package/dist/audit-cli.js +4 -2
  26. package/dist/audit-cli.js.map +1 -1
  27. package/dist/audit-events.d.ts.map +1 -1
  28. package/dist/audit-events.js +53 -3
  29. package/dist/audit-events.js.map +1 -1
  30. package/dist/audit-trail.d.ts.map +1 -1
  31. package/dist/audit-trail.js +17 -8
  32. package/dist/audit-trail.js.map +1 -1
  33. package/dist/channels/discord/attachments.d.ts.map +1 -1
  34. package/dist/channels/discord/attachments.js +14 -7
  35. package/dist/channels/discord/attachments.js.map +1 -1
  36. package/dist/channels/discord/debounce.d.ts +9 -0
  37. package/dist/channels/discord/debounce.d.ts.map +1 -0
  38. package/dist/channels/discord/debounce.js +20 -0
  39. package/dist/channels/discord/debounce.js.map +1 -0
  40. package/dist/channels/discord/delivery.d.ts +4 -1
  41. package/dist/channels/discord/delivery.d.ts.map +1 -1
  42. package/dist/channels/discord/delivery.js +24 -4
  43. package/dist/channels/discord/delivery.js.map +1 -1
  44. package/dist/channels/discord/human-delay.d.ts +16 -0
  45. package/dist/channels/discord/human-delay.d.ts.map +1 -0
  46. package/dist/channels/discord/human-delay.js +29 -0
  47. package/dist/channels/discord/human-delay.js.map +1 -0
  48. package/dist/channels/discord/inbound.d.ts +31 -0
  49. package/dist/channels/discord/inbound.d.ts.map +1 -1
  50. package/dist/channels/discord/inbound.js +156 -6
  51. package/dist/channels/discord/inbound.js.map +1 -1
  52. package/dist/channels/discord/mentions.d.ts.map +1 -1
  53. package/dist/channels/discord/mentions.js +16 -4
  54. package/dist/channels/discord/mentions.js.map +1 -1
  55. package/dist/channels/discord/presence.d.ts +33 -0
  56. package/dist/channels/discord/presence.d.ts.map +1 -0
  57. package/dist/channels/discord/presence.js +111 -0
  58. package/dist/channels/discord/presence.js.map +1 -0
  59. package/dist/channels/discord/rate-limiter.d.ts +14 -0
  60. package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
  61. package/dist/channels/discord/rate-limiter.js +49 -0
  62. package/dist/channels/discord/rate-limiter.js.map +1 -0
  63. package/dist/channels/discord/reactions.d.ts +38 -0
  64. package/dist/channels/discord/reactions.d.ts.map +1 -0
  65. package/dist/channels/discord/reactions.js +151 -0
  66. package/dist/channels/discord/reactions.js.map +1 -0
  67. package/dist/channels/discord/runtime.d.ts +7 -4
  68. package/dist/channels/discord/runtime.d.ts.map +1 -1
  69. package/dist/channels/discord/runtime.js +755 -150
  70. package/dist/channels/discord/runtime.js.map +1 -1
  71. package/dist/channels/discord/stream.d.ts +4 -1
  72. package/dist/channels/discord/stream.d.ts.map +1 -1
  73. package/dist/channels/discord/stream.js +19 -12
  74. package/dist/channels/discord/stream.js.map +1 -1
  75. package/dist/channels/discord/tool-actions.d.ts.map +1 -1
  76. package/dist/channels/discord/tool-actions.js +24 -12
  77. package/dist/channels/discord/tool-actions.js.map +1 -1
  78. package/dist/channels/discord/typing.d.ts +15 -0
  79. package/dist/channels/discord/typing.d.ts.map +1 -0
  80. package/dist/channels/discord/typing.js +106 -0
  81. package/dist/channels/discord/typing.js.map +1 -0
  82. package/dist/chunk.d.ts.map +1 -1
  83. package/dist/chunk.js +4 -2
  84. package/dist/chunk.js.map +1 -1
  85. package/dist/cli.js +77 -23
  86. package/dist/cli.js.map +1 -1
  87. package/dist/config.d.ts +24 -0
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/config.js +113 -18
  90. package/dist/config.js.map +1 -1
  91. package/dist/container-runner.d.ts.map +1 -1
  92. package/dist/container-runner.js +59 -26
  93. package/dist/container-runner.js.map +1 -1
  94. package/dist/container-setup.d.ts.map +1 -1
  95. package/dist/container-setup.js +10 -9
  96. package/dist/container-setup.js.map +1 -1
  97. package/dist/conversation.d.ts +2 -2
  98. package/dist/conversation.d.ts.map +1 -1
  99. package/dist/conversation.js +1 -1
  100. package/dist/conversation.js.map +1 -1
  101. package/dist/db.d.ts +118 -2
  102. package/dist/db.d.ts.map +1 -1
  103. package/dist/db.js +1568 -50
  104. package/dist/db.js.map +1 -1
  105. package/dist/delegation-manager.d.ts.map +1 -1
  106. package/dist/delegation-manager.js +3 -2
  107. package/dist/delegation-manager.js.map +1 -1
  108. package/dist/gateway-client.d.ts +2 -2
  109. package/dist/gateway-client.d.ts.map +1 -1
  110. package/dist/gateway-client.js +10 -4
  111. package/dist/gateway-client.js.map +1 -1
  112. package/dist/gateway-service.d.ts +3 -3
  113. package/dist/gateway-service.d.ts.map +1 -1
  114. package/dist/gateway-service.js +624 -87
  115. package/dist/gateway-service.js.map +1 -1
  116. package/dist/gateway-types.d.ts +24 -0
  117. package/dist/gateway-types.d.ts.map +1 -1
  118. package/dist/gateway-types.js.map +1 -1
  119. package/dist/gateway.js +303 -26
  120. package/dist/gateway.js.map +1 -1
  121. package/dist/health.d.ts.map +1 -1
  122. package/dist/health.js +20 -10
  123. package/dist/health.js.map +1 -1
  124. package/dist/heartbeat.d.ts +4 -0
  125. package/dist/heartbeat.d.ts.map +1 -1
  126. package/dist/heartbeat.js +62 -20
  127. package/dist/heartbeat.js.map +1 -1
  128. package/dist/hybridai-bots.d.ts.map +1 -1
  129. package/dist/hybridai-bots.js +4 -2
  130. package/dist/hybridai-bots.js.map +1 -1
  131. package/dist/hybridai-models.d.ts +8 -0
  132. package/dist/hybridai-models.d.ts.map +1 -0
  133. package/dist/hybridai-models.js +94 -0
  134. package/dist/hybridai-models.js.map +1 -0
  135. package/dist/instruction-approval-audit.d.ts.map +1 -1
  136. package/dist/instruction-approval-audit.js.map +1 -1
  137. package/dist/instruction-integrity.d.ts.map +1 -1
  138. package/dist/instruction-integrity.js +8 -2
  139. package/dist/instruction-integrity.js.map +1 -1
  140. package/dist/ipc.d.ts.map +1 -1
  141. package/dist/ipc.js +6 -1
  142. package/dist/ipc.js.map +1 -1
  143. package/dist/logger.js.map +1 -1
  144. package/dist/memory-consolidation.d.ts +17 -0
  145. package/dist/memory-consolidation.d.ts.map +1 -0
  146. package/dist/memory-consolidation.js +25 -0
  147. package/dist/memory-consolidation.js.map +1 -0
  148. package/dist/memory-service.d.ts +200 -0
  149. package/dist/memory-service.d.ts.map +1 -0
  150. package/dist/memory-service.js +294 -0
  151. package/dist/memory-service.js.map +1 -0
  152. package/dist/mount-security.d.ts.map +1 -1
  153. package/dist/mount-security.js +31 -7
  154. package/dist/mount-security.js.map +1 -1
  155. package/dist/observability-ingest.d.ts.map +1 -1
  156. package/dist/observability-ingest.js +32 -11
  157. package/dist/observability-ingest.js.map +1 -1
  158. package/dist/onboarding.d.ts.map +1 -1
  159. package/dist/onboarding.js +32 -9
  160. package/dist/onboarding.js.map +1 -1
  161. package/dist/proactive-policy.d.ts.map +1 -1
  162. package/dist/proactive-policy.js +2 -1
  163. package/dist/proactive-policy.js.map +1 -1
  164. package/dist/prompt-hooks.d.ts.map +1 -1
  165. package/dist/prompt-hooks.js +11 -7
  166. package/dist/prompt-hooks.js.map +1 -1
  167. package/dist/runtime-config.d.ts +104 -1
  168. package/dist/runtime-config.d.ts.map +1 -1
  169. package/dist/runtime-config.js +503 -24
  170. package/dist/runtime-config.js.map +1 -1
  171. package/dist/scheduled-task-runner.d.ts +1 -0
  172. package/dist/scheduled-task-runner.d.ts.map +1 -1
  173. package/dist/scheduled-task-runner.js +43 -10
  174. package/dist/scheduled-task-runner.js.map +1 -1
  175. package/dist/scheduler.d.ts +43 -4
  176. package/dist/scheduler.d.ts.map +1 -1
  177. package/dist/scheduler.js +530 -56
  178. package/dist/scheduler.js.map +1 -1
  179. package/dist/session-export.d.ts +26 -0
  180. package/dist/session-export.d.ts.map +1 -0
  181. package/dist/session-export.js +149 -0
  182. package/dist/session-export.js.map +1 -0
  183. package/dist/session-maintenance.d.ts.map +1 -1
  184. package/dist/session-maintenance.js +75 -13
  185. package/dist/session-maintenance.js.map +1 -1
  186. package/dist/session-transcripts.d.ts.map +1 -1
  187. package/dist/session-transcripts.js.map +1 -1
  188. package/dist/side-effects.d.ts.map +1 -1
  189. package/dist/side-effects.js +14 -2
  190. package/dist/side-effects.js.map +1 -1
  191. package/dist/skills-guard.d.ts.map +1 -1
  192. package/dist/skills-guard.js +893 -130
  193. package/dist/skills-guard.js.map +1 -1
  194. package/dist/skills.d.ts +5 -0
  195. package/dist/skills.d.ts.map +1 -1
  196. package/dist/skills.js +29 -15
  197. package/dist/skills.js.map +1 -1
  198. package/dist/token-efficiency.d.ts.map +1 -1
  199. package/dist/token-efficiency.js.map +1 -1
  200. package/dist/tui.js +92 -11
  201. package/dist/tui.js.map +1 -1
  202. package/dist/types.d.ts +150 -0
  203. package/dist/types.d.ts.map +1 -1
  204. package/dist/types.js +24 -1
  205. package/dist/types.js.map +1 -1
  206. package/dist/update.d.ts.map +1 -1
  207. package/dist/update.js +42 -14
  208. package/dist/update.js.map +1 -1
  209. package/dist/workspace.d.ts.map +1 -1
  210. package/dist/workspace.js +49 -9
  211. package/dist/workspace.js.map +1 -1
  212. package/docs/chat.html +9 -3
  213. package/docs/index.html +72 -13
  214. package/package.json +8 -2
  215. package/src/agent.ts +31 -4
  216. package/src/audit-cli.ts +44 -16
  217. package/src/audit-events.ts +69 -5
  218. package/src/audit-trail.ts +41 -15
  219. package/src/channels/discord/attachments.ts +81 -27
  220. package/src/channels/discord/debounce.ts +25 -0
  221. package/src/channels/discord/delivery.ts +65 -14
  222. package/src/channels/discord/human-delay.ts +48 -0
  223. package/src/channels/discord/inbound.ts +190 -8
  224. package/src/channels/discord/mentions.ts +42 -18
  225. package/src/channels/discord/presence.ts +148 -0
  226. package/src/channels/discord/rate-limiter.ts +58 -0
  227. package/src/channels/discord/reactions.ts +211 -0
  228. package/src/channels/discord/runtime.ts +1161 -205
  229. package/src/channels/discord/stream.ts +85 -31
  230. package/src/channels/discord/tool-actions.ts +78 -37
  231. package/src/channels/discord/typing.ts +140 -0
  232. package/src/chunk.ts +12 -4
  233. package/src/cli.ts +174 -57
  234. package/src/config.ts +208 -36
  235. package/src/container-runner.ts +134 -42
  236. package/src/container-setup.ts +57 -22
  237. package/src/conversation.ts +9 -7
  238. package/src/db.ts +2217 -84
  239. package/src/delegation-manager.ts +6 -2
  240. package/src/gateway-client.ts +41 -17
  241. package/src/gateway-service.ts +996 -220
  242. package/src/gateway-types.ts +33 -0
  243. package/src/gateway.ts +487 -52
  244. package/src/health.ts +66 -26
  245. package/src/heartbeat.ts +99 -22
  246. package/src/hybridai-bots.ts +14 -5
  247. package/src/hybridai-models.ts +158 -0
  248. package/src/instruction-approval-audit.ts +4 -1
  249. package/src/instruction-integrity.ts +30 -9
  250. package/src/ipc.ts +23 -5
  251. package/src/logger.ts +4 -1
  252. package/src/memory-consolidation.ts +41 -0
  253. package/src/memory-service.ts +606 -0
  254. package/src/mount-security.ts +58 -13
  255. package/src/observability-ingest.ts +134 -35
  256. package/src/onboarding.ts +126 -35
  257. package/src/proactive-policy.ts +3 -1
  258. package/src/prompt-hooks.ts +42 -17
  259. package/src/runtime-config.ts +1112 -122
  260. package/src/scheduled-task-runner.ts +77 -11
  261. package/src/scheduler.ts +683 -60
  262. package/src/session-export.ts +196 -0
  263. package/src/session-maintenance.ts +125 -22
  264. package/src/session-transcripts.ts +12 -3
  265. package/src/side-effects.ts +28 -5
  266. package/src/skills-guard.ts +1067 -219
  267. package/src/skills.ts +163 -65
  268. package/src/token-efficiency.ts +31 -9
  269. package/src/tui.ts +166 -25
  270. package/src/types.ts +199 -2
  271. package/src/update.ts +79 -23
  272. package/src/workspace.ts +63 -11
  273. package/tests/approval-policy.test.ts +335 -0
  274. package/tests/discord.basic.test.ts +213 -4
  275. package/tests/discord.human-presence.test.ts +85 -0
  276. package/tests/gateway-service.media-routing.test.ts +8 -2
  277. package/tests/hybridai-client.test.ts +112 -0
  278. package/tests/hybridai-models.test.ts +46 -0
  279. package/tests/memory-service.test.ts +1114 -0
  280. package/tests/token-efficiency.basic.test.ts +8 -2
  281. package/tests/token-usage.cache.test.ts +128 -0
  282. package/vitest.e2e.config.ts +3 -1
  283. package/vitest.integration.config.ts +3 -1
  284. package/vitest.live.config.ts +3 -1
  285. package/vitest.unit.config.ts +9 -0
package/src/cli.ts CHANGED
@@ -1,20 +1,59 @@
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 { fileURLToPath } from 'url';
8
+ import {
9
+ APP_VERSION,
10
+ DATA_DIR,
11
+ GATEWAY_BASE_URL,
12
+ MissingRequiredEnvVarError,
13
+ } from './config.js';
8
14
  import { ensureHybridAICredentials } from './onboarding.js';
9
- import { APP_VERSION, DATA_DIR, GATEWAY_BASE_URL, MissingRequiredEnvVarError } from './config.js';
10
15
  import { printUpdateUsage, runUpdateCommand } from './update.js';
11
16
 
12
- async function ensureRuntimeContainer(commandName: string, required = true): Promise<void> {
17
+ const PACKAGE_NAME = '@hybridaione/hybridclaw';
18
+ let cachedInstallRoot: string | null = null;
19
+
20
+ function resolveInstallRoot(): string {
21
+ if (cachedInstallRoot) return cachedInstallRoot;
22
+ let current = path.dirname(fileURLToPath(import.meta.url));
23
+
24
+ while (true) {
25
+ const packageJsonPath = path.join(current, 'package.json');
26
+ if (fs.existsSync(packageJsonPath)) {
27
+ try {
28
+ const parsed = JSON.parse(
29
+ fs.readFileSync(packageJsonPath, 'utf-8'),
30
+ ) as Partial<{ name: string }>;
31
+ if (parsed.name === PACKAGE_NAME) {
32
+ cachedInstallRoot = current;
33
+ return cachedInstallRoot;
34
+ }
35
+ } catch {
36
+ // ignore parse errors and continue searching upward
37
+ }
38
+ }
39
+ const parent = path.dirname(current);
40
+ if (parent === current) break;
41
+ current = parent;
42
+ }
43
+
44
+ cachedInstallRoot = process.cwd();
45
+ return cachedInstallRoot;
46
+ }
47
+
48
+ async function ensureRuntimeContainer(
49
+ commandName: string,
50
+ required = true,
51
+ ): Promise<void> {
13
52
  const { ensureContainerImageReady } = await import('./container-setup.js');
14
53
  await ensureContainerImageReady({
15
54
  commandName,
16
55
  required,
17
- cwd: process.cwd(),
56
+ cwd: resolveInstallRoot(),
18
57
  });
19
58
  }
20
59
 
@@ -43,13 +82,15 @@ async function ensureGatewayForTui(commandName: string): Promise<void> {
43
82
  return;
44
83
  }
45
84
 
46
- console.log(`${commandName}: Gateway not found. Starting gateway backend at ${GATEWAY_BASE_URL}.`);
85
+ console.log(
86
+ `${commandName}: Gateway not found. Starting gateway backend at ${GATEWAY_BASE_URL}.`,
87
+ );
47
88
  await startGatewayBackend(commandName, true);
48
89
 
49
90
  if (!(await isGatewayReachable())) {
50
91
  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.',
92
+ `Gateway did not become available at ${GATEWAY_BASE_URL} after startup.` +
93
+ ' Please run `hybridclaw gateway start --foreground` in another terminal and try again.',
53
94
  );
54
95
  }
55
96
  }
@@ -81,17 +122,17 @@ function formatInstructionDiffLine(file: {
81
122
  ];
82
123
  }
83
124
 
84
- async function ensureTuiInstructionApproval(commandName: string): Promise<void> {
125
+ async function ensureTuiInstructionApproval(
126
+ commandName: string,
127
+ ): Promise<void> {
85
128
  const {
86
129
  approveInstructionBaseline,
87
130
  INSTRUCTION_BASELINE_PATH,
88
131
  summarizeInstructionIntegrity,
89
132
  verifyInstructionBaseline,
90
133
  } = await import('./instruction-integrity.js');
91
- const {
92
- beginInstructionApprovalAudit,
93
- completeInstructionApprovalAudit,
94
- } = await import('./instruction-approval-audit.js');
134
+ const { beginInstructionApprovalAudit, completeInstructionApprovalAudit } =
135
+ await import('./instruction-approval-audit.js');
95
136
 
96
137
  const result = verifyInstructionBaseline();
97
138
  if (result.ok) return;
@@ -107,7 +148,9 @@ async function ensureTuiInstructionApproval(commandName: string): Promise<void>
107
148
  console.error(`Instruction baseline is invalid: ${result.baselineError}`);
108
149
  console.error(`Baseline path: ${INSTRUCTION_BASELINE_PATH}`);
109
150
  } else if (!result.baseline) {
110
- console.error(`No approved instruction baseline found at ${INSTRUCTION_BASELINE_PATH}.`);
151
+ console.error(
152
+ `No approved instruction baseline found at ${INSTRUCTION_BASELINE_PATH}.`,
153
+ );
111
154
  }
112
155
 
113
156
  const changed = result.files.filter((file) => file.status !== 'ok');
@@ -133,10 +176,17 @@ async function ensureTuiInstructionApproval(commandName: string): Promise<void>
133
176
  );
134
177
  }
135
178
 
136
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
179
+ const rl = readline.createInterface({
180
+ input: process.stdin,
181
+ output: process.stdout,
182
+ });
137
183
  let answer = '';
138
184
  try {
139
- answer = (await rl.question('Approve current instruction changes now? [y/N] ')).trim().toLowerCase();
185
+ answer = (
186
+ await rl.question('Approve current instruction changes now? [y/N] ')
187
+ )
188
+ .trim()
189
+ .toLowerCase();
140
190
  } finally {
141
191
  rl.close();
142
192
  }
@@ -156,7 +206,9 @@ async function ensureTuiInstructionApproval(commandName: string): Promise<void>
156
206
 
157
207
  try {
158
208
  const baseline = approveInstructionBaseline();
159
- console.log(`Approved instruction baseline at ${INSTRUCTION_BASELINE_PATH} (${baseline.approvedAt}).`);
209
+ console.log(
210
+ `Approved instruction baseline at ${INSTRUCTION_BASELINE_PATH} (${baseline.approvedAt}).`,
211
+ );
160
212
  completeInstructionApprovalAudit({
161
213
  context: auditContext,
162
214
  approved: true,
@@ -186,7 +238,10 @@ function printMainUsage(): void {
186
238
  onboarding Run HybridAI account/API key onboarding
187
239
  update Check and apply HybridClaw CLI updates
188
240
  audit Inspect/verify structured audit trail
189
- help Show general or topic-specific help (e.g. \`hybridclaw help gateway\`)`);
241
+ help Show general or topic-specific help (e.g. \`hybridclaw help gateway\`)
242
+
243
+ Options:
244
+ --version, -v Show HybridClaw CLI version`);
190
245
  }
191
246
 
192
247
  function printGatewayUsage(): void {
@@ -309,12 +364,19 @@ function readGatewayPid(): GatewayPidState | null {
309
364
  try {
310
365
  const raw = fs.readFileSync(GATEWAY_PID_PATH, 'utf-8');
311
366
  const parsed = JSON.parse(raw) as Partial<GatewayPidState>;
312
- if (!parsed || typeof parsed.pid !== 'number' || !Number.isFinite(parsed.pid)) return null;
367
+ if (
368
+ !parsed ||
369
+ typeof parsed.pid !== 'number' ||
370
+ !Number.isFinite(parsed.pid)
371
+ )
372
+ return null;
313
373
  return {
314
374
  pid: parsed.pid,
315
375
  startedAt: typeof parsed.startedAt === 'string' ? parsed.startedAt : '',
316
376
  cwd: typeof parsed.cwd === 'string' ? parsed.cwd : '',
317
- command: Array.isArray(parsed.command) ? parsed.command.map((item) => String(item)) : [],
377
+ command: Array.isArray(parsed.command)
378
+ ? parsed.command.map((item) => String(item))
379
+ : [],
318
380
  };
319
381
  } catch {
320
382
  return null;
@@ -364,7 +426,11 @@ function parseGatewayBaseUrl(): URL | null {
364
426
 
365
427
  function isLocalGatewayHost(hostname: string): boolean {
366
428
  const normalized = hostname.trim().toLowerCase();
367
- return normalized === '127.0.0.1' || normalized === 'localhost' || normalized === '::1';
429
+ return (
430
+ normalized === '127.0.0.1' ||
431
+ normalized === 'localhost' ||
432
+ normalized === '::1'
433
+ );
368
434
  }
369
435
 
370
436
  function resolveGatewayListenPort(url: URL): number {
@@ -380,9 +446,13 @@ function findGatewayPidByPort(): number | null {
380
446
  if (!parsed || !isLocalGatewayHost(parsed.hostname)) return null;
381
447
  const port = resolveGatewayListenPort(parsed);
382
448
 
383
- const result = spawnSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'], {
384
- encoding: 'utf-8',
385
- });
449
+ const result = spawnSync(
450
+ 'lsof',
451
+ ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'],
452
+ {
453
+ encoding: 'utf-8',
454
+ },
455
+ );
386
456
  if (result.error) return null;
387
457
  const output = (result.stdout || '').trim();
388
458
  if (!output) return null;
@@ -408,12 +478,14 @@ function adoptGatewayPid(pid: number, source: string): boolean {
408
478
  async function adoptReachableGatewayIfPossible(): Promise<boolean> {
409
479
  const { gatewayStatus } = await import('./gateway-client.js');
410
480
  const status = await gatewayStatus();
411
- const pid = typeof status.pid === 'number' && Number.isFinite(status.pid)
412
- ? Math.floor(status.pid)
413
- : 0;
481
+ const pid =
482
+ typeof status.pid === 'number' && Number.isFinite(status.pid)
483
+ ? Math.floor(status.pid)
484
+ : 0;
414
485
  if (pid > 0 && adoptGatewayPid(pid, 'api-status')) return true;
415
486
  const fallbackPid = findGatewayPidByPort();
416
- if (fallbackPid && adoptGatewayPid(fallbackPid, 'lsof-port-probe')) return true;
487
+ if (fallbackPid && adoptGatewayPid(fallbackPid, 'lsof-port-probe'))
488
+ return true;
417
489
  return false;
418
490
  }
419
491
 
@@ -423,11 +495,16 @@ async function runGatewayForeground(commandName: string): Promise<void> {
423
495
  await import('./gateway.js');
424
496
  }
425
497
 
426
- async function startGatewayBackend(commandName: string, waitForHealthy = false): Promise<void> {
498
+ async function startGatewayBackend(
499
+ commandName: string,
500
+ waitForHealthy = false,
501
+ ): Promise<void> {
427
502
  if (await isGatewayReachable()) {
428
503
  const existing = readGatewayPid();
429
504
  if (existing && isPidRunning(existing.pid)) {
430
- console.log(`Gateway already running in backend mode (pid ${existing.pid}).`);
505
+ console.log(
506
+ `Gateway already running in backend mode (pid ${existing.pid}).`,
507
+ );
431
508
  } else {
432
509
  let adopted = false;
433
510
  try {
@@ -437,9 +514,13 @@ async function startGatewayBackend(commandName: string, waitForHealthy = false):
437
514
  }
438
515
  if (adopted) {
439
516
  const adoptedState = readGatewayPid();
440
- console.log(`Gateway already reachable at ${GATEWAY_BASE_URL}; adopted pid ${adoptedState?.pid || '(unknown)'}.`);
517
+ console.log(
518
+ `Gateway already reachable at ${GATEWAY_BASE_URL}; adopted pid ${adoptedState?.pid || '(unknown)'}.`,
519
+ );
441
520
  } else {
442
- console.log(`Gateway already reachable at ${GATEWAY_BASE_URL} (unmanaged by CLI PID file).`);
521
+ console.log(
522
+ `Gateway already reachable at ${GATEWAY_BASE_URL} (unmanaged by CLI PID file).`,
523
+ );
443
524
  }
444
525
  }
445
526
  return;
@@ -451,12 +532,14 @@ async function startGatewayBackend(commandName: string, waitForHealthy = false):
451
532
  const healthy = await waitForGatewayReachable(15_000);
452
533
  if (!healthy) {
453
534
  throw new Error(
454
- `Gateway process ${existing.pid} exists but did not become reachable at ${GATEWAY_BASE_URL}.`
455
- + ` Check logs: ${GATEWAY_LOG_PATH}`,
535
+ `Gateway process ${existing.pid} exists but did not become reachable at ${GATEWAY_BASE_URL}.` +
536
+ ` Check logs: ${GATEWAY_LOG_PATH}`,
456
537
  );
457
538
  }
458
539
  }
459
- console.log(`Gateway already running in backend mode (pid ${existing.pid}).`);
540
+ console.log(
541
+ `Gateway already running in backend mode (pid ${existing.pid}).`,
542
+ );
460
543
  return;
461
544
  }
462
545
  if (existing && !isPidRunning(existing.pid)) {
@@ -494,8 +577,8 @@ async function startGatewayBackend(commandName: string, waitForHealthy = false):
494
577
  const healthy = await waitForGatewayReachable(20_000);
495
578
  if (!healthy) {
496
579
  throw new Error(
497
- `Gateway backend started (pid ${child.pid}) but not reachable at ${GATEWAY_BASE_URL}.`
498
- + ` Check logs: ${GATEWAY_LOG_PATH}`,
580
+ `Gateway backend started (pid ${child.pid}) but not reachable at ${GATEWAY_BASE_URL}.` +
581
+ ` Check logs: ${GATEWAY_LOG_PATH}`,
499
582
  );
500
583
  }
501
584
  }
@@ -514,7 +597,9 @@ async function stopManagedGatewayByPid(state: GatewayPidState): Promise<void> {
514
597
  process.kill(state.pid, 'SIGTERM');
515
598
  } catch (err) {
516
599
  removeGatewayPidFile();
517
- throw new Error(`Failed to stop gateway pid ${state.pid}: ${err instanceof Error ? err.message : String(err)}`);
600
+ throw new Error(
601
+ `Failed to stop gateway pid ${state.pid}: ${err instanceof Error ? err.message : String(err)}`,
602
+ );
518
603
  }
519
604
 
520
605
  const deadline = Date.now() + 10_000;
@@ -538,7 +623,9 @@ async function stopManagedGatewayByPid(state: GatewayPidState): Promise<void> {
538
623
  console.log(`Gateway stop timed out; sent SIGKILL to pid ${state.pid}.`);
539
624
  }
540
625
 
541
- async function stopUnmanagedGatewayGracefully(mode: 'stop' | 'restart'): Promise<void> {
626
+ async function stopUnmanagedGatewayGracefully(
627
+ mode: 'stop' | 'restart',
628
+ ): Promise<void> {
542
629
  const suffix = mode === 'restart' ? ' before restart' : '';
543
630
  console.log('Gateway is reachable but unmanaged by CLI PID file.');
544
631
  console.log(`Requesting graceful shutdown over API${suffix}...`);
@@ -548,8 +635,8 @@ async function stopUnmanagedGatewayGracefully(mode: 'stop' | 'restart'): Promise
548
635
  const stopped = await waitForGatewayUnreachable(10_000);
549
636
  if (!stopped) {
550
637
  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.',
638
+ `Gateway remained reachable at ${GATEWAY_BASE_URL} after shutdown request.` +
639
+ ' Stop it from its owning process or use your system process manager.',
553
640
  );
554
641
  }
555
642
  console.log('Unmanaged gateway stopped via API request.');
@@ -559,18 +646,24 @@ async function stopUnmanagedGatewayGracefully(mode: 'stop' | 'restart'): Promise
559
646
  }
560
647
 
561
648
  const discoveredPid = findGatewayPidByPort();
562
- if (discoveredPid && discoveredPid !== process.pid && adoptGatewayPid(discoveredPid, 'lsof-port-probe')) {
649
+ if (
650
+ discoveredPid &&
651
+ discoveredPid !== process.pid &&
652
+ adoptGatewayPid(discoveredPid, 'lsof-port-probe')
653
+ ) {
563
654
  const adoptedState = readGatewayPid();
564
655
  if (adoptedState && isPidRunning(adoptedState.pid)) {
565
- console.log(`Shutdown API unavailable; stopping gateway pid ${adoptedState.pid} via local signal.`);
656
+ console.log(
657
+ `Shutdown API unavailable; stopping gateway pid ${adoptedState.pid} via local signal.`,
658
+ );
566
659
  await stopManagedGatewayByPid(adoptedState);
567
660
  return;
568
661
  }
569
662
  }
570
663
 
571
664
  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.',
665
+ `Gateway shutdown endpoint is unavailable at ${GATEWAY_BASE_URL} and PID ownership could not be recovered.` +
666
+ ' Stop the process manually once, then retry.',
574
667
  );
575
668
  }
576
669
 
@@ -588,11 +681,15 @@ async function stopGatewayBackend(): Promise<void> {
588
681
  if (!isPidRunning(state.pid)) {
589
682
  removeGatewayPidFile();
590
683
  if (await isGatewayReachable()) {
591
- console.log(`Removed stale gateway PID file (pid ${state.pid} not running).`);
684
+ console.log(
685
+ `Removed stale gateway PID file (pid ${state.pid} not running).`,
686
+ );
592
687
  await stopUnmanagedGatewayGracefully('stop');
593
688
  return;
594
689
  }
595
- console.log(`Removed stale gateway PID file (pid ${state.pid} not running).`);
690
+ console.log(
691
+ `Removed stale gateway PID file (pid ${state.pid} not running).`,
692
+ );
596
693
  return;
597
694
  }
598
695
 
@@ -611,21 +708,29 @@ async function printGatewayLifecycleStatus(): Promise<void> {
611
708
  } else {
612
709
  console.log('PID file: not found');
613
710
  }
614
- console.log(`Gateway API reachable: ${reachable ? 'yes' : 'no'} (${GATEWAY_BASE_URL})`);
711
+ console.log(
712
+ `Gateway API reachable: ${reachable ? 'yes' : 'no'} (${GATEWAY_BASE_URL})`,
713
+ );
615
714
 
616
715
  if (reachable) {
617
716
  try {
618
717
  const { gatewayStatus } = await import('./gateway-client.js');
619
718
  const status = await gatewayStatus();
620
- console.log(`Uptime: ${status.uptime}s | Sessions: ${status.sessions} | Containers: ${status.activeContainers}`);
719
+ console.log(
720
+ `Uptime: ${status.uptime}s | Sessions: ${status.sessions} | Containers: ${status.activeContainers}`,
721
+ );
621
722
  } catch (err) {
622
- console.log(`Gateway status fetch failed: ${err instanceof Error ? err.message : String(err)}`);
723
+ console.log(
724
+ `Gateway status fetch failed: ${err instanceof Error ? err.message : String(err)}`,
725
+ );
623
726
  }
624
727
  }
625
728
  }
626
729
 
627
730
  async function runGatewayApiCommand(args: string[]): Promise<void> {
628
- const { gatewayCommand, renderGatewayCommand } = await import('./gateway-client.js');
731
+ const { gatewayCommand, renderGatewayCommand } = await import(
732
+ './gateway-client.js'
733
+ );
629
734
  const result = await gatewayCommand({
630
735
  sessionId: 'cli:gateway',
631
736
  guildId: null,
@@ -703,6 +808,10 @@ async function main(): Promise<void> {
703
808
  const subargs = process.argv.slice(3);
704
809
 
705
810
  switch (command) {
811
+ case '--version':
812
+ case '-v':
813
+ console.log(APP_VERSION);
814
+ break;
706
815
  case 'gateway':
707
816
  await handleGatewayCommand(subargs);
708
817
  break;
@@ -722,7 +831,10 @@ async function main(): Promise<void> {
722
831
  printOnboardingUsage();
723
832
  break;
724
833
  }
725
- await ensureHybridAICredentials({ force: true, commandName: 'hybridclaw onboarding' });
834
+ await ensureHybridAICredentials({
835
+ force: true,
836
+ commandName: 'hybridclaw onboarding',
837
+ });
726
838
  await ensureRuntimeContainer('hybridclaw onboarding', false);
727
839
  break;
728
840
  case 'update': {
@@ -772,16 +884,21 @@ const envVarHint: Record<string, string> = {
772
884
  };
773
885
 
774
886
  function printMissingEnvVarError(message: string, envVar?: string): void {
775
- const hint = envVar ? envVarHint[envVar] : 'Set this variable and rerun the command.';
887
+ const hint = envVar
888
+ ? envVarHint[envVar]
889
+ : 'Set this variable and rerun the command.';
776
890
  console.error(`hybridclaw error: ${message}`);
777
891
  console.error(`Hint: ${hint}`);
778
- console.error('Make sure you run `hybridclaw` from the directory that contains your .env file.');
892
+ console.error(
893
+ 'Make sure you run `hybridclaw` from the directory that contains your .env file.',
894
+ );
779
895
  }
780
896
 
781
897
  main().catch((err) => {
782
- const missingEnvVarMatch = err instanceof Error
783
- ? err.message.match(/^Missing required env var:\s*([A-Za-z0-9_]+)/)
784
- : null;
898
+ const missingEnvVarMatch =
899
+ err instanceof Error
900
+ ? err.message.match(/^Missing required env var:\s*([A-Za-z0-9_]+)/)
901
+ : null;
785
902
  if (missingEnvVarMatch) {
786
903
  printMissingEnvVarError(err.message, missingEnvVarMatch[1]);
787
904
  } else if (err instanceof MissingRequiredEnvVarError) {