failproofai 0.0.13-beta.0 → 0.0.13-beta.2

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 (198) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +3 -3
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/required-server-files.json +1 -1
  5. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
  7. package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
  8. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
  11. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
  12. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
  13. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
  14. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
  15. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
  17. package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  19. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  21. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  23. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  24. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  26. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  27. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  28. package/.next/standalone/.next/server/app/api/audit/invite/route.js +2 -2
  29. package/.next/standalone/.next/server/app/api/audit/invite/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/audit/run/route.js +3 -2
  31. package/.next/standalone/.next/server/app/api/audit/run/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/auth/login-request/route.js +1 -1
  33. package/.next/standalone/.next/server/app/api/auth/login-request/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js +2 -2
  35. package/.next/standalone/.next/server/app/api/auth/login-verify/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/auth/logout/route.js +2 -2
  37. package/.next/standalone/.next/server/app/api/auth/logout/route.js.nft.json +1 -1
  38. package/.next/standalone/.next/server/app/api/auth/reminder/route.js +2 -2
  39. package/.next/standalone/.next/server/app/api/auth/reminder/route.js.nft.json +1 -1
  40. package/.next/standalone/.next/server/app/api/auth/status/route.js +2 -2
  41. package/.next/standalone/.next/server/app/api/auth/status/route.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +1 -1
  43. package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
  44. package/.next/standalone/.next/server/app/audit/page/server-reference-manifest.json +2 -2
  45. package/.next/standalone/.next/server/app/audit/page.js +1 -1
  46. package/.next/standalone/.next/server/app/audit/page.js.nft.json +1 -1
  47. package/.next/standalone/.next/server/app/audit/page_client-reference-manifest.js +1 -1
  48. package/.next/standalone/.next/server/app/index.html +1 -1
  49. package/.next/standalone/.next/server/app/index.rsc +15 -15
  50. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  51. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  52. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  53. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  54. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  55. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  56. package/.next/standalone/.next/server/app/page.js +1 -1
  57. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  58. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  59. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  60. package/.next/standalone/.next/server/app/policies/page.js +2 -2
  61. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  63. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  64. package/.next/standalone/.next/server/app/project/[name]/page.js +3 -2
  65. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  67. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  68. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  69. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +3 -2
  70. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  73. package/.next/standalone/.next/server/app/projects/page.js +2 -2
  74. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  76. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_0xu5z._.js +1 -1
  77. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0cefjd9._.js +3 -0
  78. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ekj92q._.js +3 -0
  79. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hv08mx._.js +3 -0
  80. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0obaznk._.js +3 -0
  81. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xvsnvt._.js +3 -0
  82. package/.next/standalone/.next/server/chunks/[root-of-the-server]__12v-lma._.js +3 -0
  83. package/.next/standalone/.next/server/chunks/[root-of-the-server]__136y39g._.js +3 -0
  84. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13h8pzr._.js +1 -1
  85. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1fwl2mz._.js +3 -0
  86. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1hkdkmz._.js +3 -0
  87. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1i9rknc._.js +3 -0
  88. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1mrihkj._.js +3 -0
  89. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1myjm-d._.js +3 -0
  90. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0cycwg6._.js → [root-of-the-server]__1tvn4wf._.js} +2 -2
  91. package/.next/standalone/.next/server/chunks/_1z062h5._.js +3 -0
  92. package/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_17k9e3w.js +5 -5
  93. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_1nxcc4v._.js +1 -1
  94. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__015_i4t._.js +4 -0
  95. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__03lv-pe._.js +4 -0
  96. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__04bqvos._.js +3 -0
  97. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0bbpv2d._.js → [root-of-the-server]__09u1pcy._.js} +2 -2
  98. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dqgs96._.js +3 -0
  99. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1uvfwgr._.js → [root-of-the-server]__0hdsupo._.js} +3 -3
  100. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0q5rfxg._.js +3 -0
  101. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qeuy9c._.js +4 -0
  102. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qukut7._.js +4 -0
  103. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s89hoe._.js +4 -0
  104. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0ehe24g._.js → [root-of-the-server]__0ynf7tx._.js} +3 -3
  105. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__170799-._.js +245 -0
  106. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1d8omgc._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ql-mch._.js +3 -0
  108. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__1-m1rhq._.js → [root-of-the-server]__1rlmypf._.js} +2 -2
  109. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1tkjqka._.js +4 -0
  110. package/.next/standalone/.next/server/chunks/ssr/_05whahf._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/ssr/_0ul5rf_._.js +3 -0
  112. package/.next/standalone/.next/server/chunks/ssr/_11_p9y8._.js +1 -1
  113. package/.next/standalone/.next/server/chunks/ssr/_11v0g3z._.js +3 -0
  114. package/.next/standalone/.next/server/chunks/ssr/_1p0-leb._.js +1 -1
  115. package/.next/standalone/.next/server/chunks/ssr/app_audit__components_audit-dashboard_tsx_0p9ud47._.js +1 -1
  116. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_1kp6l3x._.js +1 -1
  117. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_19dqvpc._.js +1 -1
  118. package/.next/standalone/.next/server/chunks/ssr/lib_hermes-projects_ts_05b8-it._.js +3 -0
  119. package/.next/standalone/.next/server/chunks/ssr/{node_modules_html-to-image_es_index_1u91ynr.js → node_modules_html-to-image_es_index_09fe3kb.js} +1 -1
  120. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1ezd2jf._.js +1 -1
  121. package/.next/standalone/.next/server/chunks/ssr/src_hooks_1tnuifj._.js +1 -1
  122. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  123. package/.next/standalone/.next/server/pages/404.html +1 -1
  124. package/.next/standalone/.next/server/pages/500.html +1 -1
  125. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  126. package/.next/standalone/.next/server/server-reference-manifest.json +10 -10
  127. package/.next/standalone/.next/static/chunks/{39rn602wszcbd.js → 06cw8nbn5c4_r.js} +1 -1
  128. package/.next/standalone/.next/static/chunks/0fa7j157kg9_d.js +1 -0
  129. package/.next/standalone/.next/static/chunks/{03zg3ms7uaoq3.js → 0i0joc040j999.js} +1 -1
  130. package/.next/standalone/.next/static/chunks/{1h569gia53ant.js → 0l2n5ftar65ox.js} +1 -1
  131. package/.next/standalone/.next/static/chunks/1c8i78p7t1i-5.js +1 -0
  132. package/.next/standalone/.next/static/chunks/{2ks4pzu5b-l_q.js → 1sg8kx5szx42y.js} +1 -1
  133. package/.next/standalone/.next/static/chunks/{3jka0hvhqouyv.js → 23t-i9s-q--nu.js} +1 -1
  134. package/.next/standalone/.next/static/chunks/{0byua6ng1f_ka.js → 25qvspmcbb_5y.js} +1 -1
  135. package/.next/standalone/.next/static/chunks/25s52un3k_wht.css +2 -0
  136. package/.next/standalone/.next/static/chunks/{41mhnhfkmglpp.js → 2a-k91nsa1lgf.js} +1 -1
  137. package/.next/standalone/.next/static/chunks/{0azkpcy8eb1gr.js → 2oz8ok8fzxaj3.js} +1 -1
  138. package/.next/standalone/.next/static/chunks/{1zxtyef-iqede.js → 2rkvn4znvuy4o.js} +1 -1
  139. package/.next/standalone/app/components/sessions-list.tsx +19 -0
  140. package/.next/standalone/app/project/[name]/page.tsx +9 -4
  141. package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +26 -5
  142. package/.next/standalone/assets/logos/hermes-dark.svg +1 -0
  143. package/.next/standalone/assets/logos/hermes-light.svg +1 -0
  144. package/.next/standalone/lib/cli-registry.ts +6 -1
  145. package/.next/standalone/lib/download-session.ts +16 -0
  146. package/.next/standalone/lib/hermes-projects.ts +164 -0
  147. package/.next/standalone/lib/hermes-sessions.ts +273 -0
  148. package/.next/standalone/lib/projects.ts +14 -3
  149. package/.next/standalone/lib/sqlite-reader.ts +102 -0
  150. package/.next/standalone/package.json +5 -3
  151. package/.next/standalone/server.js +1 -1
  152. package/README.md +10 -1
  153. package/bin/failproofai.mjs +17 -16
  154. package/dist/cli.mjs +7774 -225
  155. package/lib/cli-registry.ts +6 -1
  156. package/lib/download-session.ts +16 -0
  157. package/lib/hermes-projects.ts +164 -0
  158. package/lib/hermes-sessions.ts +273 -0
  159. package/lib/projects.ts +14 -3
  160. package/lib/sql-js-asm.d.ts +26 -0
  161. package/lib/sqlite-reader.ts +102 -0
  162. package/package.json +5 -3
  163. package/src/audit/cli-adapters/hermes.ts +58 -0
  164. package/src/audit/cli-adapters/index.ts +6 -0
  165. package/src/hooks/builtin-policies.ts +16 -0
  166. package/src/hooks/handler.ts +8 -0
  167. package/src/hooks/install-prompt.ts +7 -3
  168. package/src/hooks/integrations.ts +179 -5
  169. package/src/hooks/policy-evaluator.ts +42 -0
  170. package/src/hooks/resolve-transcript-path.ts +4 -0
  171. package/src/hooks/tool-name-canonicalize.ts +6 -0
  172. package/src/hooks/types.ts +67 -1
  173. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0dwpg-h._.js +0 -3
  174. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0lnenda._.js +0 -3
  175. package/.next/standalone/.next/server/chunks/[root-of-the-server]__13i_sva._.js +0 -3
  176. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1_mqemn._.js +0 -3
  177. package/.next/standalone/.next/server/chunks/[root-of-the-server]__1m2_4t0._.js +0 -3
  178. package/.next/standalone/.next/server/chunks/node_modules_0-tu4ot._.js +0 -3
  179. package/.next/standalone/.next/server/chunks/node_modules_0ttxbz7._.js +0 -3
  180. package/.next/standalone/.next/server/chunks/node_modules_1bnh1y0._.js +0 -3
  181. package/.next/standalone/.next/server/chunks/node_modules_1epycqa._.js +0 -3
  182. package/.next/standalone/.next/server/chunks/node_modules_1wpdcgo._.js +0 -3
  183. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0808sha._.js +0 -4
  184. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0e4-6d8._.js +0 -4
  185. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0f62vu9._.js +0 -3
  186. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g253ve._.js +0 -4
  187. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0k65l27._.js +0 -3
  188. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0vxf0_g._.js +0 -4
  189. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12mcauo._.js +0 -4
  190. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1e-x7j4._.js +0 -3
  191. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1mt35_w._.js +0 -221
  192. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1pcxxwg._.js +0 -4
  193. package/.next/standalone/.next/static/chunks/0f7d7hnbh4djs.js +0 -1
  194. package/.next/standalone/.next/static/chunks/1o0xa47736gi9.css +0 -2
  195. package/.next/standalone/.next/static/chunks/2wbuxnsvux4di.js +0 -1
  196. /package/.next/standalone/.next/static/{W6ELw5VwHTFy2amS2px6e → HgG0mizCfOM3XyN1EW-sL}/_buildManifest.js +0 -0
  197. /package/.next/standalone/.next/static/{W6ELw5VwHTFy2amS2px6e → HgG0mizCfOM3XyN1EW-sL}/_clientMiddlewareManifest.js +0 -0
  198. /package/.next/standalone/.next/static/{W6ELw5VwHTFy2amS2px6e → HgG0mizCfOM3XyN1EW-sL}/_ssgManifest.js +0 -0
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Hermes (hermes-agent) session transcript loader + parser.
3
+ *
4
+ * AUDIT-ONLY (Pillar 2). Hermes stores every gateway user's sessions in one
5
+ * SQLite DB (`~/.hermes/state.db`). We read it DIRECTLY via the bundled sql.js
6
+ * driver (lib/sqlite-reader.ts) — a reusable path for SQLite-backed agents.
7
+ * (opencode and the already-shipped CLIs keep their CLI shell-out unchanged.)
8
+ *
9
+ * Message shape is OpenAI Chat-Completions style (verified live): assistant tool
10
+ * calls are `tool_calls[].function.{name, arguments}` (stored as a JSON string
11
+ * column) and results are separate `role:"tool"` rows keyed by `tool_call_id`.
12
+ * `hermesRowsToLogEntries` pairs them (mirrors `lib/codex-sessions.ts`) and is a
13
+ * PURE function of the message rows, so it is unit-testable without a DB.
14
+ *
15
+ * DB path override: set `HERMES_DB_PATH` (used by tests and to point at a copied
16
+ * or remote state.db).
17
+ */
18
+ import { homedir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { openSqliteReadonly } from "./sqlite-reader";
21
+ import { runtimeCache } from "./runtime-cache";
22
+ import {
23
+ baseEntry,
24
+ formatTimestamp,
25
+ type LogEntry,
26
+ type UserEntry,
27
+ type AssistantEntry,
28
+ type GenericEntry,
29
+ type ContentBlock,
30
+ type ToolUseBlock,
31
+ type LogSource,
32
+ } from "./log-entries";
33
+ import { formatDuration } from "./format-duration";
34
+
35
+ /** Absolute path to Hermes's SQLite DB (override with HERMES_DB_PATH). */
36
+ export function hermesDbPath(): string {
37
+ return process.env.HERMES_DB_PATH || join(homedir(), ".hermes", "state.db");
38
+ }
39
+
40
+ /** Coerce a Hermes epoch value (seconds or ms) to epoch ms. Hermes stores
41
+ * `started_at`/`ended_at`/`timestamp` as REAL epoch seconds. */
42
+ export function epochToMs(value: unknown): number {
43
+ if (typeof value === "number" && Number.isFinite(value)) {
44
+ if (value > 1e12) return value; // already ms
45
+ if (value > 1e9) return value * 1000; // seconds
46
+ }
47
+ return Date.now();
48
+ }
49
+
50
+ // ── Parsing helpers ──
51
+
52
+ function safeJsonParse(s: unknown): unknown {
53
+ if (typeof s !== "string" || s.length === 0) return undefined;
54
+ try {
55
+ return JSON.parse(s);
56
+ } catch {
57
+ return undefined;
58
+ }
59
+ }
60
+
61
+ function isPlainObject(v: unknown): v is Record<string, unknown> {
62
+ return !!v && typeof v === "object" && !Array.isArray(v);
63
+ }
64
+
65
+ /** Text from a `content` field that may be a string or an array of
66
+ * `{ type:"text", text }` blocks. */
67
+ function extractText(content: unknown): string {
68
+ if (typeof content === "string") return content;
69
+ if (Array.isArray(content)) {
70
+ return content
71
+ .map((c) => (isPlainObject(c) && typeof c.text === "string" ? (c.text as string) : ""))
72
+ .filter(Boolean)
73
+ .join("\n");
74
+ }
75
+ return "";
76
+ }
77
+
78
+ function toDate(value: unknown, fallbackMs: number): Date {
79
+ if (typeof value === "number" && Number.isFinite(value)) {
80
+ if (value > 1e12) return new Date(value);
81
+ if (value > 1e9) return new Date(value * 1000);
82
+ }
83
+ if (typeof value === "string") {
84
+ const ms = Date.parse(value);
85
+ if (!Number.isNaN(ms)) return new Date(ms);
86
+ }
87
+ return new Date(fallbackMs);
88
+ }
89
+
90
+ interface NormalizedToolCall {
91
+ id: string;
92
+ name: string;
93
+ input: Record<string, unknown>;
94
+ }
95
+
96
+ /** Normalize `tool_calls` (array, or the JSON-string column Hermes stores) into
97
+ * `{ id, name, input }[]`. OpenAI shape: `{ id|call_id, function:{ name,
98
+ * arguments } }` where `arguments` is itself a JSON string. */
99
+ function normalizeToolCalls(raw: unknown): NormalizedToolCall[] {
100
+ const arr = Array.isArray(raw) ? raw : safeJsonParse(raw);
101
+ if (!Array.isArray(arr)) return [];
102
+ const out: NormalizedToolCall[] = [];
103
+ for (const tc of arr) {
104
+ if (!isPlainObject(tc)) continue;
105
+ const fn = isPlainObject(tc.function) ? tc.function : {};
106
+ const name =
107
+ typeof fn.name === "string"
108
+ ? fn.name
109
+ : typeof tc.name === "string"
110
+ ? (tc.name as string)
111
+ : "tool";
112
+ const id =
113
+ (typeof tc.id === "string" && tc.id) ||
114
+ (typeof tc.call_id === "string" && tc.call_id) ||
115
+ `${name}-${out.length}`;
116
+ const parsedArgs = isPlainObject(fn.arguments) ? fn.arguments : safeJsonParse(fn.arguments);
117
+ out.push({ id, name, input: isPlainObject(parsedArgs) ? parsedArgs : {} });
118
+ }
119
+ return out;
120
+ }
121
+
122
+ // ── Pure parser: message rows → LogEntry[] ──
123
+
124
+ /**
125
+ * Convert `messages`-table rows (ordered by timestamp) into `LogEntry[]`.
126
+ * Pairs each assistant `tool_calls` entry with its later `role:"tool"` result
127
+ * by `tool_call_id`. Roles other than user/assistant/tool (e.g. Hermes's
128
+ * `session_meta`) become generic system entries so nothing is dropped. Pure —
129
+ * unit-testable with plain row objects.
130
+ */
131
+ export function hermesRowsToLogEntries(
132
+ rows: Record<string, unknown>[],
133
+ source: LogSource = "session",
134
+ ): LogEntry[] {
135
+ const entries: LogEntry[] = [];
136
+ const toolUseById = new Map<string, ToolUseBlock>();
137
+ const toolUseStartMs = new Map<string, number>();
138
+ const baseMs = Date.now();
139
+
140
+ for (let i = 0; i < rows.length; i++) {
141
+ const m = rows[i];
142
+ if (!isPlainObject(m)) continue;
143
+
144
+ const role = typeof m.role === "string" ? m.role : "system";
145
+ const date = toDate(m.timestamp ?? m.time_created, baseMs + i);
146
+ const timestamp = date.toISOString();
147
+ const raw: Record<string, unknown> = {
148
+ uuid: m.id != null ? String(m.id) : `hermes-${i}`,
149
+ parentUuid: null,
150
+ };
151
+ const base = baseEntry(raw, timestamp, date, source);
152
+
153
+ if (role === "user") {
154
+ entries.push({
155
+ type: "user",
156
+ ...base,
157
+ message: { role: "user", content: extractText(m.content) },
158
+ } satisfies UserEntry);
159
+ continue;
160
+ }
161
+
162
+ if (role === "assistant") {
163
+ const blocks: ContentBlock[] = [];
164
+ const text = extractText(m.content);
165
+ if (text) blocks.push({ type: "text", text });
166
+ for (const tc of normalizeToolCalls(m.tool_calls)) {
167
+ const block: ToolUseBlock = { type: "tool_use", id: tc.id, name: tc.name, input: tc.input };
168
+ blocks.push(block);
169
+ toolUseById.set(tc.id, block);
170
+ toolUseStartMs.set(tc.id, date.getTime());
171
+ }
172
+ if (blocks.length === 0) continue; // empty assistant turn
173
+ entries.push({
174
+ type: "assistant",
175
+ ...base,
176
+ message: { role: "assistant", content: blocks },
177
+ } satisfies AssistantEntry);
178
+ continue;
179
+ }
180
+
181
+ if (role === "tool") {
182
+ const callId = typeof m.tool_call_id === "string" ? m.tool_call_id : undefined;
183
+ const block = callId ? toolUseById.get(callId) : undefined;
184
+ if (block) {
185
+ const startMs = (callId && toolUseStartMs.get(callId)) || date.getTime();
186
+ const durationMs = Math.max(0, date.getTime() - startMs);
187
+ block.result = {
188
+ timestamp,
189
+ timestampFormatted: formatTimestamp(date),
190
+ content: extractText(m.content),
191
+ durationMs,
192
+ durationFormatted: formatDuration(durationMs),
193
+ };
194
+ continue;
195
+ }
196
+ // Orphan tool result — fall through to a system entry.
197
+ }
198
+
199
+ entries.push({ type: "system", ...base, raw } satisfies GenericEntry);
200
+ }
201
+
202
+ entries.sort((a, b) => a.timestampMs - b.timestampMs);
203
+ return entries;
204
+ }
205
+
206
+ // ── DB loader ──
207
+
208
+ export interface HermesSessionLogData {
209
+ entries: LogEntry[];
210
+ rawLines: Record<string, unknown>[];
211
+ cwd?: string;
212
+ filePath: string; // synthetic — hermes keeps sessions in a DB; we use hermes://<id>
213
+ }
214
+
215
+ interface HermesMessageRow {
216
+ id: number | null;
217
+ role: string | null;
218
+ content: string | null;
219
+ tool_call_id: string | null;
220
+ tool_calls: string | null;
221
+ tool_name: string | null;
222
+ timestamp: number | null;
223
+ }
224
+
225
+ /**
226
+ * Load one session by ID from `state.db`. Returns `null` when the DB is
227
+ * unavailable or the session doesn't exist.
228
+ */
229
+ export async function getHermesSessionLog(
230
+ sessionId: string,
231
+ ): Promise<HermesSessionLogData | null> {
232
+ if (!sessionId || !/^[A-Za-z0-9_-]+$/.test(sessionId)) return null;
233
+ const db = await openSqliteReadonly(hermesDbPath());
234
+ if (!db) return null;
235
+ try {
236
+ const sessionRows = db.query<{ source: string | null; cwd: string | null }>(
237
+ "SELECT source, cwd FROM sessions WHERE id = ?",
238
+ [sessionId],
239
+ );
240
+ if (sessionRows.length === 0) return null;
241
+ const { source, cwd: realCwd } = sessionRows[0];
242
+
243
+ const msgRows = db.query<HermesMessageRow>(
244
+ "SELECT id, role, content, tool_call_id, tool_calls, tool_name, timestamp " +
245
+ "FROM messages WHERE session_id = ? ORDER BY timestamp ASC",
246
+ [sessionId],
247
+ );
248
+
249
+ const entries = hermesRowsToLogEntries(
250
+ msgRows as unknown as Record<string, unknown>[],
251
+ "session",
252
+ );
253
+ // Gateway sessions have no cwd → group by source (slack/telegram/cli/cron).
254
+ const cwd =
255
+ realCwd && realCwd.length > 0 ? realCwd : source ? `hermes:${source}` : undefined;
256
+ return {
257
+ entries,
258
+ rawLines: msgRows as unknown as Record<string, unknown>[],
259
+ cwd,
260
+ filePath: `hermes://${sessionId}`,
261
+ };
262
+ } catch {
263
+ return null;
264
+ } finally {
265
+ db.close();
266
+ }
267
+ }
268
+
269
+ export const getCachedHermesSessionLog = runtimeCache(
270
+ (sessionId: string) => getHermesSessionLog(sessionId),
271
+ 2,
272
+ { maxSize: 50 },
273
+ );
@@ -16,7 +16,7 @@ import { formatDate } from "./format-date";
16
16
  export const UUID_RE = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
17
17
  export const PATH_TRAVERSAL_RE = /(^|[\\/])\.\.($|[\\/])/;
18
18
 
19
- export type ProjectCli = "claude" | "codex" | "copilot" | "cursor" | "opencode" | "pi" | "gemini";
19
+ export type ProjectCli = "claude" | "codex" | "copilot" | "cursor" | "opencode" | "pi" | "gemini" | "hermes";
20
20
 
21
21
  export interface ProjectFolder {
22
22
  name: string;
@@ -40,6 +40,11 @@ export interface SessionFile {
40
40
  /** Originating agent CLI. Set when the session list mixes sources from more
41
41
  * than one CLI, so the table can render a per-row CLI badge. */
42
42
  cli?: ProjectCli;
43
+ /** Gateway session metadata (Hermes only today): who drove it and where.
44
+ * Other CLIs leave these undefined; the list renders them only when present. */
45
+ userId?: string;
46
+ channelId?: string;
47
+ channelType?: string; // e.g. "group" | "dm"
43
48
  }
44
49
 
45
50
  /** Stats a path and returns mtime. Falls back to epoch (1970-01-01) on error
@@ -139,6 +144,7 @@ export async function getProjectFolders(): Promise<ProjectFolder[]> {
139
144
  { getOpenCodeProjects },
140
145
  { getPiProjects },
141
146
  { getGeminiProjects },
147
+ { getHermesProjects },
142
148
  ] = await Promise.all([
143
149
  import("./codex-projects"),
144
150
  import("./copilot-projects"),
@@ -146,8 +152,9 @@ export async function getProjectFolders(): Promise<ProjectFolder[]> {
146
152
  import("./opencode-projects"),
147
153
  import("./pi-projects"),
148
154
  import("./gemini-projects"),
155
+ import("./hermes-projects"),
149
156
  ]);
150
- const [claude, codex, copilot, cursor, opencode, pi, gemini] = await Promise.all([
157
+ const [claude, codex, copilot, cursor, opencode, pi, gemini, hermes] = await Promise.all([
151
158
  getClaudeProjectFolders(),
152
159
  getCodexProjects().catch((error) => {
153
160
  logError("Error reading Codex projects:", error);
@@ -173,8 +180,12 @@ export async function getProjectFolders(): Promise<ProjectFolder[]> {
173
180
  logError("Error reading Gemini projects:", error);
174
181
  return [] as ProjectFolder[];
175
182
  }),
183
+ getHermesProjects().catch((error) => {
184
+ logError("Error reading Hermes projects:", error);
185
+ return [] as ProjectFolder[];
186
+ }),
176
187
  ]);
177
- return mergeProjectFolders(claude, codex, copilot, cursor, opencode, pi, gemini);
188
+ return mergeProjectFolders(claude, codex, copilot, cursor, opencode, pi, gemini, hermes);
178
189
  }
179
190
 
180
191
  /**
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Reusable read-only SQLite access for audit adapters of SQLite-backed agents.
3
+ *
4
+ * Tiered so we read LIVE data (including the write-ahead log) wherever possible,
5
+ * and still run anywhere:
6
+ * 1. `node:sqlite` — a real connection that reads main DB + WAL together, so
7
+ * brand-new rows are visible immediately. Built into Node ≥ 22.5 (no native
8
+ * module, no flag on recent 22.x). Preferred.
9
+ * 2. `sql.js` (pure-JS/asm) — portable fallback for older Node. Reads the main
10
+ * DB file's bytes only, so it reflects a snapshot up to the last WAL
11
+ * checkpoint (very recent rows may lag). No native module — survives
12
+ * `npm install --ignore-scripts`.
13
+ *
14
+ * Either way: read-only, and `null` on any failure (fail-open, so an absent or
15
+ * locked DB makes the audit skip that agent rather than crash).
16
+ *
17
+ * NOTE: opencode and every CLI that already ships keep their existing CLI
18
+ * shell-out — this layer is only for new SQLite integrations.
19
+ */
20
+ import { existsSync, readFileSync } from "node:fs";
21
+ import initSqlJs, { type SqlJsStatic } from "sql.js/dist/sql-asm.js";
22
+
23
+ export interface SqliteReader {
24
+ /** Run a parameterized read-only query; returns rows as plain objects. */
25
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[];
26
+ close(): void;
27
+ }
28
+
29
+ // ── Tier 1: node:sqlite (WAL-aware, Node ≥ 22.5) ──
30
+
31
+ interface NodeSqliteStmt {
32
+ all(...params: unknown[]): Record<string, unknown>[];
33
+ }
34
+ interface NodeSqliteDb {
35
+ prepare(sql: string): NodeSqliteStmt;
36
+ close(): void;
37
+ }
38
+ interface NodeSqliteModule {
39
+ DatabaseSync: new (path: string, options?: { readOnly?: boolean }) => NodeSqliteDb;
40
+ }
41
+
42
+ async function tryNodeSqlite(dbPath: string): Promise<SqliteReader | null> {
43
+ try {
44
+ // Dynamic import: on Node < 22.5 this rejects and we fall through to sql.js.
45
+ const mod = (await import("node:sqlite")) as unknown as NodeSqliteModule;
46
+ const db = new mod.DatabaseSync(dbPath, { readOnly: true });
47
+ return {
48
+ query<T = Record<string, unknown>>(sql: string, params: unknown[] = []): T[] {
49
+ const stmt = db.prepare(sql);
50
+ return (params.length ? stmt.all(...params) : stmt.all()) as T[];
51
+ },
52
+ close() {
53
+ db.close();
54
+ },
55
+ };
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ // ── Tier 2: sql.js (portable snapshot) ──
62
+
63
+ let sqlPromise: Promise<SqlJsStatic> | null = null;
64
+ function loadSqlJs(): Promise<SqlJsStatic> {
65
+ if (!sqlPromise) sqlPromise = initSqlJs();
66
+ return sqlPromise;
67
+ }
68
+
69
+ async function trySqlJs(dbPath: string): Promise<SqliteReader | null> {
70
+ try {
71
+ const SQL = await loadSqlJs();
72
+ const db = new SQL.Database(readFileSync(dbPath));
73
+ return {
74
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] {
75
+ const stmt = db.prepare(sql);
76
+ try {
77
+ if (params && params.length) stmt.bind(params);
78
+ const rows: T[] = [];
79
+ while (stmt.step()) rows.push(stmt.getAsObject() as T);
80
+ return rows;
81
+ } finally {
82
+ stmt.free();
83
+ }
84
+ },
85
+ close() {
86
+ db.close();
87
+ },
88
+ };
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Open a SQLite database file READ-ONLY. Prefers a WAL-aware `node:sqlite`
96
+ * connection (live data), falls back to a `sql.js` snapshot. Returns `null` on
97
+ * any failure. Always `close()` the reader when done.
98
+ */
99
+ export async function openSqliteReadonly(dbPath: string): Promise<SqliteReader | null> {
100
+ if (!existsSync(dbPath)) return null;
101
+ return (await tryNodeSqlite(dbPath)) ?? (await trySqlJs(dbPath));
102
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "failproofai",
3
- "version": "0.0.13-beta.0",
3
+ "version": "0.0.13-beta.2",
4
4
  "description": "The easiest way to manage policies that keep your AI agents reliable, on-task, and running autonomously — for Claude Code & the Agents SDK",
5
5
  "bin": {
6
6
  "failproofai": "./dist/cli.mjs"
@@ -22,7 +22,7 @@
22
22
  "scripts": {
23
23
  "predev": "bun run build:cli && bun link",
24
24
  "dev": "FAILPROOFAI_TELEMETRY_DISABLED=1 bun scripts/dev.ts --port 8020",
25
- "build:cli": "bun build --target=node --format=esm --outfile=dist/cli.mjs bin/failproofai.mjs --external posthog-node && node -e \"const fs=require('fs');const c=fs.readFileSync('dist/cli.mjs','utf8');fs.writeFileSync('dist/cli.mjs',c.replace('#!/usr/bin/env bun','#!/usr/bin/env node').replace('// @bun\\n',''))\"",
25
+ "build:cli": "bun build --target=node --format=esm --outfile=dist/cli.mjs bin/failproofai.mjs --external posthog-node --external sql.js && node -e \"const fs=require('fs');const c=fs.readFileSync('dist/cli.mjs','utf8');fs.writeFileSync('dist/cli.mjs',c.replace('#!/usr/bin/env bun','#!/usr/bin/env node').replace('// @bun\\n',''))\"",
26
26
  "build": "bun build --target=node --format=cjs --outfile=dist/index.js src/index.ts && bun run build:cli && bun --bun next build && node -e \"const {cpSync}=require('fs');cpSync('.next/static','.next/standalone/.next/static',{recursive:true});\" && node scripts/prune-standalone.mjs",
27
27
  "prestart": "bun run build:cli && bun link",
28
28
  "start": "FAILPROOFAI_TELEMETRY_DISABLED=1 bun scripts/start.ts",
@@ -99,7 +99,9 @@
99
99
  "dependencies": {
100
100
  "html-to-image": "^1.11.13",
101
101
  "html2canvas": "^1.4.1",
102
- "posthog-node": "^5.37.1"
102
+ "posthog-node": "^5.37.1",
103
+ "sql.js": "^1.14.1",
104
+ "yaml": "^2.9.0"
103
105
  },
104
106
  "overrides": {
105
107
  "postcss": "8.5.14",
@@ -9,7 +9,7 @@ const currentPort = parseInt(process.env.PORT, 10) || 3000
9
9
  const hostname = process.env.HOSTNAME || '0.0.0.0'
10
10
 
11
11
  let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
12
- const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"0.0.13-beta.0"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":false,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"partialFallbacks":false,"dynamicOnHover":false,"varyParams":false,"prefetchInlining":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"imgOptSkipMetadata":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"viewTransition":false,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":false,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":false,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","outputFileTracingExcludes":{"*":["node_modules/@img/**","node_modules/sharp/**"]},"turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"distDirRoot":".next"}
12
+ const nextConfig = {"env":{"NEXT_PUBLIC_APP_VERSION":"0.0.13-beta.2"},"typescript":{"ignoreBuildErrors":false},"typedRoutes":false,"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.ts","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["tsx","ts","jsx","js"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":14400,"formats":["image/webp"],"maximumRedirects":3,"maximumResponseBody":50000000,"dangerouslyAllowLocalIP":false,"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","localPatterns":[{"pathname":"**","search":""}],"remotePatterns":[],"qualities":[75],"unoptimized":true,"customCacheHandler":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{"serverFunctions":true,"browserToTerminal":"warn"},"compiler":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"outputFileTracingRoot":"/home/runner/work/failproofai/failproofai","cacheComponents":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":30,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":31536000}},"cacheHandlers":{},"experimental":{"appNewScrollHandler":false,"useSkewCookie":false,"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"cachedNavigations":false,"partialFallbacks":false,"dynamicOnHover":false,"varyParams":false,"prefetchInlining":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","proxyPrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":3,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"imgOptSkipMetadata":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"strictRouteTypes":false,"viewTransition":false,"removeUncaughtErrorAndRejectionListeners":false,"validateRSCRequestHeaders":false,"staleTimes":{"dynamic":0,"static":300},"reactDebugChannel":true,"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"transitionIndicator":false,"gestureTransition":false,"inlineCss":false,"useCache":false,"globalNotFound":false,"browserDebugInfoInTerminal":"warn","lockDistDir":true,"proxyClientMaxBodySize":10485760,"hideLogsAfterAbort":false,"mcpServer":true,"turbopackFileSystemCacheForDev":true,"turbopackFileSystemCacheForBuild":false,"turbopackInferModuleSideEffects":true,"turbopackPluginRuntimeStrategy":"childProcesses","optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-sqlite-node","@effect/sql-sqlite-bun","@effect/sql-sqlite-wasm","@effect/sql-sqlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"[\\w-]+-Google|Google-[\\w-]+|Chrome-Lighthouse|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview|Yeti|googleweblight","bundlePagesRouterDependencies":false,"configFileName":"next.config.ts","outputFileTracingExcludes":{"*":["node_modules/@img/**","node_modules/sharp/**"]},"turbopack":{"root":"/home/runner/work/failproofai/failproofai"},"distDirRoot":".next"}
13
13
 
14
14
  process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
15
15
 
package/README.md CHANGED
@@ -72,9 +72,18 @@ before they become incidents. Zero latency. Runs locally.
72
72
  <img src="assets/logos/gemini-light.svg" alt="Gemini CLI" width="64" height="64" />
73
73
  </picture>
74
74
  </a>
75
+ &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
76
+ <a href="https://github.com/FailproofAI/failproofai/blob/main/docs/configuration.mdx" title="Hermes (hermes-agent)">
77
+ <picture>
78
+ <source media="(prefers-color-scheme: dark)" srcset="assets/logos/hermes-dark.svg" />
79
+ <img src="assets/logos/hermes-light.svg" alt="Hermes" width="64" height="64" />
80
+ </picture>
81
+ </a>
75
82
  </p>
76
83
 
77
- > Install hooks for one or any combination: `failproofai policies --install --cli opencode pi gemini` (or `--cli claude codex copilot cursor opencode pi gemini`). Omit `--cli` to auto-detect installed CLIs and prompt.
84
+ > Install hooks for one or any combination: `failproofai policies --install --cli opencode pi gemini` (or `--cli claude codex copilot cursor opencode pi gemini hermes`). Omit `--cli` to auto-detect installed CLIs and prompt.
85
+ >
86
+ > **Hermes** (hermes-agent, a Slack/Telegram gateway) is supported for both **live-hook enforcement** (`--cli hermes` — one install intercepts tool calls from every platform and subagent) and offline **audit** replay of its gateway sessions from the single `~/.hermes/state.db`.
78
87
 
79
88
  ---
80
89
 
@@ -65,7 +65,7 @@ const hookIdx = args.indexOf("--hook");
65
65
  if (hookIdx >= 0) {
66
66
  if (!args[hookIdx + 1]) {
67
67
  console.error("Error: Missing event type after --hook");
68
- console.error("Usage: failproofai --hook <event> [--cli <claude|codex|copilot|cursor|opencode|pi|gemini>]");
68
+ console.error("Usage: failproofai --hook <event> [--cli <claude|codex|copilot|cursor|opencode|pi|gemini|hermes>]");
69
69
  process.exit(1);
70
70
  }
71
71
  const eventType = args[hookIdx + 1];
@@ -82,6 +82,7 @@ if (hookIdx >= 0) {
82
82
  || cliArg === "opencode"
83
83
  || cliArg === "pi"
84
84
  || cliArg === "gemini"
85
+ || cliArg === "hermes"
85
86
  )
86
87
  ? cliArg
87
88
  : "claude";
@@ -129,9 +130,9 @@ COMMANDS
129
130
  policies, p List all available policies and their status
130
131
  policies --install, -i Enable policies in agent CLI settings
131
132
  [names...] Specific policy names to enable
132
- --cli claude|codex|copilot|cursor|opencode|pi|gemini
133
+ --cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes
133
134
  Agent CLI(s) to install for; space-separated
134
- (e.g. --cli claude codex copilot cursor opencode pi gemini) or repeated.
135
+ (e.g. --cli claude codex copilot cursor opencode pi gemini hermes) or repeated.
135
136
  Default: detect installed CLIs and prompt.
136
137
  --scope user|project|local Config scope to write to (default: user)
137
138
  (Codex / Copilot / Cursor / OpenCode / Pi / Gemini support user|project only)
@@ -140,7 +141,7 @@ COMMANDS
140
141
 
141
142
  policies --uninstall, -u Disable policies or remove hooks
142
143
  [names...] Specific policy names to disable
143
- --cli claude|codex|copilot|cursor|opencode|pi|gemini
144
+ --cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes
144
145
  Agent CLI(s) to uninstall from
145
146
  --scope user|project|local|all Config scope to remove from (default: user)
146
147
  --beta Remove only beta policies
@@ -176,7 +177,7 @@ EXAMPLES
176
177
  failproofai policies --install --cli opencode --scope project
177
178
  failproofai policies --install --cli pi --scope project
178
179
  failproofai policies --install --cli gemini --scope project
179
- failproofai policies --install --cli claude codex copilot cursor opencode pi gemini
180
+ failproofai policies --install --cli claude codex copilot cursor opencode pi gemini hermes
180
181
  failproofai policies --install --custom ./my-policies.js
181
182
  failproofai policies -i -c ./my-policies.js
182
183
  failproofai policies --uninstall block-sudo
@@ -225,9 +226,9 @@ USAGE
225
226
 
226
227
  OPTIONS (install)
227
228
  [names...] Specific policy names to enable (omit for interactive)
228
- --cli claude|codex|copilot|cursor|opencode|pi|gemini
229
+ --cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes
229
230
  Agent CLI(s) to install for; space-separated
230
- (e.g. --cli claude codex copilot cursor opencode pi gemini) or repeated.
231
+ (e.g. --cli claude codex copilot cursor opencode pi gemini hermes) or repeated.
231
232
  Omit to detect installed CLIs and prompt (or
232
233
  auto-pick if only one is found).
233
234
  --scope user|project|local Config scope to write to (default: user)
@@ -238,7 +239,7 @@ OPTIONS (install)
238
239
 
239
240
  OPTIONS (uninstall)
240
241
  [names...] Specific policy names to disable (omit to remove hooks)
241
- --cli claude|codex|copilot|cursor|opencode|pi|gemini
242
+ --cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes
242
243
  Agent CLI(s) to uninstall from
243
244
  --scope user|project|local|all Config scope to remove from (default: user)
244
245
  --beta Remove only beta policies
@@ -254,7 +255,7 @@ EXAMPLES
254
255
  failproofai policies --install --cli opencode --scope project
255
256
  failproofai policies --install --cli pi --scope project
256
257
  failproofai policies --install --cli gemini --scope project
257
- failproofai policies --install --cli claude codex copilot cursor opencode pi gemini
258
+ failproofai policies --install --cli claude codex copilot cursor opencode pi gemini hermes
258
259
  failproofai policies --install --custom ./my-policies.js
259
260
  failproofai policies -i -c ./my-policies.js
260
261
  failproofai policies --uninstall block-sudo
@@ -295,7 +296,7 @@ EXAMPLES
295
296
  // --cli claude codex copilot
296
297
  // --cli claude --cli codex
297
298
  // Values are consumed greedily until the next flag or end of argv.
298
- const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini"]);
299
+ const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini", "hermes"]);
299
300
  const cliFlagValues = [];
300
301
  const cliConsumedIdxs = new Set();
301
302
  const cliFlagIdxs = subArgs.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0);
@@ -312,7 +313,7 @@ EXAMPLES
312
313
  consumed++;
313
314
  }
314
315
  if (consumed === 0) {
315
- throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi gemini (or any subset)");
316
+ throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi gemini hermes (or any subset)");
316
317
  }
317
318
  }
318
319
 
@@ -384,7 +385,7 @@ EXAMPLES
384
385
  }
385
386
 
386
387
  // --cli accepts one or more space-separated values; same parser as install.
387
- const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini"]);
388
+ const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini", "hermes"]);
388
389
  const cliFlagValues = [];
389
390
  const cliConsumedIdxs = new Set();
390
391
  const cliFlagIdxs = subArgs.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0);
@@ -401,7 +402,7 @@ EXAMPLES
401
402
  consumed++;
402
403
  }
403
404
  if (consumed === 0) {
404
- throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi gemini (or any subset)");
405
+ throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi gemini hermes (or any subset)");
405
406
  }
406
407
  }
407
408
 
@@ -510,7 +511,7 @@ USAGE
510
511
  failproofai policy remove <name> Disable one policy
511
512
 
512
513
  OPTIONS
513
- --cli claude|codex|copilot|cursor|opencode|pi|gemini
514
+ --cli claude|codex|copilot|cursor|opencode|pi|gemini|hermes
514
515
  Agent CLI(s) to apply to; space-separated or repeated.
515
516
  Omit to detect installed CLIs and prompt.
516
517
  --scope user|project|local Config scope (default: user)
@@ -548,7 +549,7 @@ EXAMPLES
548
549
  }
549
550
 
550
551
  // --cli accepts one or more space-separated values, optionally repeated.
551
- const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini"]);
552
+ const VALID_CLIS = new Set(["claude", "codex", "copilot", "cursor", "opencode", "pi", "gemini", "hermes"]);
552
553
  const cliFlagValues = [];
553
554
  const cliConsumedIdxs = new Set();
554
555
  const cliFlagIdxs = rest.map((a, i) => (a === "--cli" ? i : -1)).filter((i) => i >= 0);
@@ -563,7 +564,7 @@ EXAMPLES
563
564
  consumed++;
564
565
  }
565
566
  if (consumed === 0) {
566
- throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi gemini (or any subset)");
567
+ throw new CliError("Missing value(s) for --cli. Usage: --cli claude codex copilot cursor opencode pi gemini hermes (or any subset)");
567
568
  }
568
569
  }
569
570