failproofai 0.0.6-beta.1 → 0.0.6-beta.3

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 (177) hide show
  1. package/.next/standalone/.failproofai/policies/review-policies.mjs +4 -3
  2. package/.next/standalone/.next/BUILD_ID +1 -1
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/required-server-files.json +1 -1
  6. package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +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.nft.json +1 -1
  18. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.html +2 -2
  20. package/.next/standalone/.next/server/app/_not-found.rsc +15 -15
  21. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +15 -15
  22. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
  23. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +10 -10
  24. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  25. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
  26. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  27. package/.next/standalone/.next/server/app/index.html +1 -1
  28. package/.next/standalone/.next/server/app/index.rsc +15 -15
  29. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  30. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +15 -15
  31. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
  32. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +10 -10
  33. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  34. package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
  35. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  37. package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
  38. package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
  39. package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
  40. package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
  41. package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
  42. package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
  43. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
  44. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
  45. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
  46. package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
  47. package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
  48. package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
  49. package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
  50. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +1 -1
  51. package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
  52. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +2 -2
  53. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0~kmh8w._.js → [root-of-the-server]__096k.db._.js} +2 -2
  54. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__09icjsf._.js +2 -2
  55. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +2 -2
  56. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +2 -2
  57. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0rh.18_._.js → [root-of-the-server]__0kyh86x._.js} +2 -2
  58. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0okos0k._.js +2 -2
  59. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0w6l33k._.js +2 -2
  60. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +2 -2
  61. package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +2 -2
  62. package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
  63. package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
  64. package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +1 -1
  65. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  66. package/.next/standalone/.next/server/pages/404.html +2 -2
  67. package/.next/standalone/.next/server/pages/500.html +1 -1
  68. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  69. package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
  70. package/.next/standalone/.next/static/chunks/{0gbf4cphy8ksq.js → 0-dm_9a6nsc2l.js} +1 -1
  71. package/.next/standalone/.next/static/chunks/{12~yi9oj8av8p.js → 01pmw1-asbek~.js} +2 -2
  72. package/.next/standalone/.next/static/chunks/{0v.yd0kg_ld3r.js → 051m32nx~n5yr.js} +1 -1
  73. package/.next/standalone/.next/static/chunks/{09_k80d~cq2wg.js → 0a-yctdwn368y.js} +1 -1
  74. package/.next/standalone/.next/static/chunks/{0bvhsa6zva2o..js → 0ksdlt_1hucdm.js} +1 -1
  75. package/.next/standalone/.next/static/chunks/{01b~z8f1ws0rk.js → 0l-mu4okl-cj1.js} +1 -1
  76. package/.next/standalone/.next/static/chunks/{08t08igdql9yt.js → 0mazj-p-~2kc6.js} +1 -1
  77. package/.next/standalone/.next/static/chunks/0qakntsrpc~1j.js +6 -0
  78. package/.next/standalone/.next/static/chunks/{03rz6ykw-a2xi.js → 156zca6aewyr-.js} +1 -1
  79. package/.next/standalone/CHANGELOG.md +18 -0
  80. package/.next/standalone/bin/failproofai.mjs +91 -4
  81. package/.next/standalone/dist/cli.mjs +1156 -55
  82. package/.next/standalone/docs/ar/built-in-policies.mdx +140 -103
  83. package/.next/standalone/docs/ar/custom-policies.mdx +72 -72
  84. package/.next/standalone/docs/ar/examples.mdx +86 -33
  85. package/.next/standalone/docs/ar/getting-started.mdx +82 -29
  86. package/.next/standalone/docs/built-in-policies.mdx +3 -3
  87. package/.next/standalone/docs/de/built-in-policies.mdx +97 -60
  88. package/.next/standalone/docs/de/custom-policies.mdx +56 -56
  89. package/.next/standalone/docs/de/examples.mdx +72 -18
  90. package/.next/standalone/docs/de/getting-started.mdx +72 -20
  91. package/.next/standalone/docs/es/built-in-policies.mdx +91 -54
  92. package/.next/standalone/docs/es/custom-policies.mdx +55 -55
  93. package/.next/standalone/docs/es/examples.mdx +73 -19
  94. package/.next/standalone/docs/es/getting-started.mdx +72 -20
  95. package/.next/standalone/docs/fr/built-in-policies.mdx +99 -62
  96. package/.next/standalone/docs/fr/custom-policies.mdx +51 -51
  97. package/.next/standalone/docs/fr/examples.mdx +78 -24
  98. package/.next/standalone/docs/fr/getting-started.mdx +65 -13
  99. package/.next/standalone/docs/he/built-in-policies.mdx +139 -99
  100. package/.next/standalone/docs/he/custom-policies.mdx +75 -75
  101. package/.next/standalone/docs/he/examples.mdx +87 -33
  102. package/.next/standalone/docs/he/getting-started.mdx +84 -33
  103. package/.next/standalone/docs/hi/built-in-policies.mdx +203 -166
  104. package/.next/standalone/docs/hi/custom-policies.mdx +71 -70
  105. package/.next/standalone/docs/hi/examples.mdx +90 -36
  106. package/.next/standalone/docs/hi/getting-started.mdx +80 -27
  107. package/.next/standalone/docs/i18n/README.ar.md +69 -69
  108. package/.next/standalone/docs/i18n/README.de.md +46 -46
  109. package/.next/standalone/docs/i18n/README.es.md +42 -42
  110. package/.next/standalone/docs/i18n/README.fr.md +39 -39
  111. package/.next/standalone/docs/i18n/README.he.md +83 -83
  112. package/.next/standalone/docs/i18n/README.hi.md +69 -69
  113. package/.next/standalone/docs/i18n/README.it.md +72 -72
  114. package/.next/standalone/docs/i18n/README.ja.md +71 -71
  115. package/.next/standalone/docs/i18n/README.ko.md +52 -52
  116. package/.next/standalone/docs/i18n/README.pt-br.md +44 -44
  117. package/.next/standalone/docs/i18n/README.ru.md +66 -66
  118. package/.next/standalone/docs/i18n/README.tr.md +82 -83
  119. package/.next/standalone/docs/i18n/README.vi.md +70 -71
  120. package/.next/standalone/docs/i18n/README.zh.md +51 -51
  121. package/.next/standalone/docs/it/built-in-policies.mdx +115 -78
  122. package/.next/standalone/docs/it/custom-policies.mdx +69 -69
  123. package/.next/standalone/docs/it/examples.mdx +93 -39
  124. package/.next/standalone/docs/it/getting-started.mdx +73 -21
  125. package/.next/standalone/docs/ja/built-in-policies.mdx +155 -118
  126. package/.next/standalone/docs/ja/custom-policies.mdx +71 -71
  127. package/.next/standalone/docs/ja/examples.mdx +76 -22
  128. package/.next/standalone/docs/ja/getting-started.mdx +65 -13
  129. package/.next/standalone/docs/ko/built-in-policies.mdx +103 -66
  130. package/.next/standalone/docs/ko/custom-policies.mdx +67 -67
  131. package/.next/standalone/docs/ko/examples.mdx +87 -33
  132. package/.next/standalone/docs/ko/getting-started.mdx +61 -9
  133. package/.next/standalone/docs/pt-br/built-in-policies.mdx +72 -35
  134. package/.next/standalone/docs/pt-br/custom-policies.mdx +56 -56
  135. package/.next/standalone/docs/pt-br/examples.mdx +78 -24
  136. package/.next/standalone/docs/pt-br/getting-started.mdx +64 -12
  137. package/.next/standalone/docs/ru/built-in-policies.mdx +135 -98
  138. package/.next/standalone/docs/ru/custom-policies.mdx +82 -81
  139. package/.next/standalone/docs/ru/examples.mdx +77 -22
  140. package/.next/standalone/docs/ru/getting-started.mdx +74 -22
  141. package/.next/standalone/docs/tr/built-in-policies.mdx +126 -89
  142. package/.next/standalone/docs/tr/custom-policies.mdx +59 -60
  143. package/.next/standalone/docs/tr/examples.mdx +97 -42
  144. package/.next/standalone/docs/tr/getting-started.mdx +75 -23
  145. package/.next/standalone/docs/vi/built-in-policies.mdx +116 -81
  146. package/.next/standalone/docs/vi/custom-policies.mdx +68 -68
  147. package/.next/standalone/docs/vi/examples.mdx +93 -38
  148. package/.next/standalone/docs/vi/getting-started.mdx +74 -22
  149. package/.next/standalone/docs/zh/built-in-policies.mdx +117 -82
  150. package/.next/standalone/docs/zh/custom-policies.mdx +49 -49
  151. package/.next/standalone/docs/zh/examples.mdx +90 -36
  152. package/.next/standalone/docs/zh/getting-started.mdx +73 -21
  153. package/.next/standalone/package.json +1 -1
  154. package/.next/standalone/server.js +1 -1
  155. package/.next/standalone/src/auth/login.ts +104 -0
  156. package/.next/standalone/src/auth/logout.ts +50 -0
  157. package/.next/standalone/src/auth/token-store.ts +64 -0
  158. package/.next/standalone/src/hooks/builtin-policies.ts +27 -21
  159. package/.next/standalone/src/hooks/handler.ts +35 -15
  160. package/.next/standalone/src/relay/daemon.ts +362 -0
  161. package/.next/standalone/src/relay/pid.ts +76 -0
  162. package/.next/standalone/src/relay/queue.ts +225 -0
  163. package/bin/failproofai.mjs +91 -4
  164. package/dist/cli.mjs +1156 -55
  165. package/package.json +1 -1
  166. package/src/auth/login.ts +104 -0
  167. package/src/auth/logout.ts +50 -0
  168. package/src/auth/token-store.ts +64 -0
  169. package/src/hooks/builtin-policies.ts +27 -21
  170. package/src/hooks/handler.ts +35 -15
  171. package/src/relay/daemon.ts +362 -0
  172. package/src/relay/pid.ts +76 -0
  173. package/src/relay/queue.ts +225 -0
  174. package/.next/standalone/.next/static/chunks/0wlyoif4_kj_t.js +0 -6
  175. /package/.next/standalone/.next/static/{CkmOT-ZvDN-sVULinGVKT → r-wX0MuAfCjbhJm3phQc8}/_buildManifest.js +0 -0
  176. /package/.next/standalone/.next/static/{CkmOT-ZvDN-sVULinGVKT → r-wX0MuAfCjbhJm3phQc8}/_clientMiddlewareManifest.js +0 -0
  177. /package/.next/standalone/.next/static/{CkmOT-ZvDN-sVULinGVKT → r-wX0MuAfCjbhJm3phQc8}/_ssgManifest.js +0 -0
@@ -0,0 +1,225 @@
1
+ import {
2
+ appendFileSync,
3
+ mkdirSync,
4
+ existsSync,
5
+ readFileSync,
6
+ statSync,
7
+ renameSync,
8
+ unlinkSync,
9
+ readdirSync,
10
+ chmodSync,
11
+ } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { homedir } from "node:os";
14
+ import { createHash, randomUUID } from "node:crypto";
15
+ import { isLoggedIn } from "../auth/token-store";
16
+
17
+ const QUEUE_DIR = join(homedir(), ".failproofai", "cache", "server-queue");
18
+ const PENDING_FILE = join(QUEUE_DIR, "pending.jsonl");
19
+ const PROCESSING_PREFIX = "processing-";
20
+
21
+ // Cap — if the queue file exceeds this, `appendToServerQueue` is a no-op.
22
+ // Prevents unbounded growth when the daemon is down for a long time or the
23
+ // user installed the CLI but never logged in.
24
+ const MAX_QUEUE_BYTES = 50 * 1024 * 1024; // 50 MB
25
+
26
+ export interface RawEntry {
27
+ timestamp: number;
28
+ eventType: string;
29
+ toolName?: string | null;
30
+ policyName?: string | null;
31
+ policyNames?: string[];
32
+ decision: string;
33
+ reason?: string | null;
34
+ durationMs: number;
35
+ sessionId?: string | null;
36
+ transcriptPath?: string | null;
37
+ cwd?: string | null;
38
+ permissionMode?: string | null;
39
+ hookEventName?: string | null;
40
+ toolInput?: Record<string, unknown>;
41
+ }
42
+
43
+ /**
44
+ * What actually gets persisted and sent to the server. Intentionally a
45
+ * narrower shape than RawEntry — we drop / hash anything that could leak
46
+ * secrets or paths:
47
+ * - toolInput: dropped entirely (can contain credentials, file contents, commands)
48
+ * - cwd: replaced with cwd_hash (SHA-256) so the server can group by project
49
+ * - transcriptPath: dropped (local-only filesystem path)
50
+ * - reason: passed through a redactor for common credential patterns
51
+ */
52
+ export interface QueueEntry {
53
+ client_event_id: string;
54
+ timestamp: number;
55
+ event_type: string;
56
+ tool_name: string | null;
57
+ policy_name: string | null;
58
+ policy_names: string[];
59
+ decision: string;
60
+ reason: string | null;
61
+ duration_ms: number;
62
+ session_id: string | null;
63
+ cwd_hash: string | null;
64
+ permission_mode: string | null;
65
+ hook_event_name: string | null;
66
+ }
67
+
68
+ function hashCwd(cwd: string | null | undefined): string | null {
69
+ if (!cwd) return null;
70
+ return createHash("sha256").update(cwd).digest("hex");
71
+ }
72
+
73
+ function redactReason(reason: string | null | undefined): string | null {
74
+ if (!reason) return reason ?? null;
75
+ return reason
76
+ .replace(/AKIA[0-9A-Z]{16}/g, "[REDACTED-AWS-KEY]")
77
+ .replace(/eyJ[A-Za-z0-9_=-]+\.[A-Za-z0-9_=-]+\.[A-Za-z0-9_=-]+/g, "[REDACTED-JWT]")
78
+ .replace(/ghp_[A-Za-z0-9]{36,}/g, "[REDACTED-GH-TOKEN]")
79
+ .replace(/sk-[A-Za-z0-9]{20,}/g, "[REDACTED-API-KEY]")
80
+ .replace(/Bearer\s+[A-Za-z0-9_.=+-]+/gi, "Bearer [REDACTED]");
81
+ }
82
+
83
+ function sanitize(entry: RawEntry): QueueEntry {
84
+ return {
85
+ client_event_id: randomUUID(),
86
+ timestamp: entry.timestamp,
87
+ event_type: entry.eventType,
88
+ tool_name: entry.toolName ?? null,
89
+ policy_name: entry.policyName ?? null,
90
+ policy_names: entry.policyNames ?? [],
91
+ decision: entry.decision,
92
+ reason: redactReason(entry.reason),
93
+ duration_ms: entry.durationMs,
94
+ session_id: entry.sessionId ?? null,
95
+ cwd_hash: hashCwd(entry.cwd),
96
+ permission_mode: entry.permissionMode ?? null,
97
+ hook_event_name: entry.hookEventName ?? null,
98
+ };
99
+ }
100
+
101
+ function ensureDir(): void {
102
+ if (!existsSync(QUEUE_DIR)) {
103
+ mkdirSync(QUEUE_DIR, { recursive: true, mode: 0o700 });
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Hook-side API — append one event to the pending queue.
109
+ *
110
+ * Uses `appendFileSync` (O_APPEND) which is atomic for small writes, so
111
+ * concurrent hook processes interleave lines correctly without clobbering
112
+ * each other. Sanitizes sensitive fields before persisting.
113
+ *
114
+ * No-op cases (keeps hook path fast and safe):
115
+ * - User not logged in (no auth.json exists)
116
+ * - Queue file already exceeds MAX_QUEUE_BYTES (prevents unbounded growth)
117
+ */
118
+ export function appendToServerQueue(entry: RawEntry): void {
119
+ if (!isLoggedIn()) return;
120
+ ensureDir();
121
+
122
+ try {
123
+ if (existsSync(PENDING_FILE) && statSync(PENDING_FILE).size > MAX_QUEUE_BYTES) {
124
+ return;
125
+ }
126
+ } catch {
127
+ // existsSync/statSync races are fine; proceed
128
+ }
129
+
130
+ const sanitized = sanitize(entry);
131
+ appendFileSync(PENDING_FILE, JSON.stringify(sanitized) + "\n", { mode: 0o600 });
132
+
133
+ // Tighten perms on first create in case the umask allowed wider access
134
+ try {
135
+ chmodSync(PENDING_FILE, 0o600);
136
+ } catch {
137
+ // Windows or non-critical; skip
138
+ }
139
+ }
140
+
141
+ export function queueSizeBytes(): number {
142
+ try {
143
+ return statSync(PENDING_FILE).size;
144
+ } catch {
145
+ return 0;
146
+ }
147
+ }
148
+
149
+ interface ClaimError extends Error {
150
+ code?: string;
151
+ }
152
+
153
+ /**
154
+ * Daemon-side API — atomically claim all pending events into a new
155
+ * processing file. Returns the processing file path, or null ONLY when
156
+ * there's nothing to claim (ENOENT). Other errors throw so we don't
157
+ * silently strand events.
158
+ */
159
+ export function claimPendingBatch(): string | null {
160
+ if (!existsSync(PENDING_FILE)) return null;
161
+ try {
162
+ const size = statSync(PENDING_FILE).size;
163
+ if (size === 0) return null;
164
+ } catch {
165
+ return null;
166
+ }
167
+
168
+ const seq = `${Date.now()}-${process.pid}`;
169
+ const processingFile = join(QUEUE_DIR, `${PROCESSING_PREFIX}${seq}.jsonl`);
170
+ try {
171
+ renameSync(PENDING_FILE, processingFile);
172
+ try {
173
+ chmodSync(processingFile, 0o600);
174
+ } catch {
175
+ // non-critical
176
+ }
177
+ return processingFile;
178
+ } catch (err) {
179
+ const e = err as ClaimError;
180
+ if (e?.code === "ENOENT") return null;
181
+ // Real failure (EACCES, EIO, etc.) — surface to caller; don't lose events
182
+ throw err;
183
+ }
184
+ }
185
+
186
+ export function findOrphanProcessingFiles(): string[] {
187
+ ensureDir();
188
+ try {
189
+ return readdirSync(QUEUE_DIR)
190
+ .filter((n) => n.startsWith(PROCESSING_PREFIX) && n.endsWith(".jsonl"))
191
+ .map((n) => join(QUEUE_DIR, n))
192
+ .sort();
193
+ } catch {
194
+ return [];
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Parse a processing file into structured events, skipping (and logging
200
+ * via stderr) any malformed JSON lines so one bad entry doesn't wedge
201
+ * the entire file forever.
202
+ */
203
+ export function readProcessingFile(path: string): QueueEntry[] {
204
+ if (!existsSync(path)) return [];
205
+ const content = readFileSync(path, "utf8");
206
+ const out: QueueEntry[] = [];
207
+ for (const line of content.split("\n")) {
208
+ const trimmed = line.trim();
209
+ if (!trimmed) continue;
210
+ try {
211
+ out.push(JSON.parse(trimmed) as QueueEntry);
212
+ } catch {
213
+ // Skip malformed line — we can't recover it
214
+ }
215
+ }
216
+ return out;
217
+ }
218
+
219
+ export function deleteProcessingFile(path: string): void {
220
+ try {
221
+ unlinkSync(path);
222
+ } catch {
223
+ // best-effort; stale processing files are cleaned up on next run
224
+ }
225
+ }
@@ -57,6 +57,20 @@ if (hookIdx >= 0) {
57
57
  }
58
58
  }
59
59
 
60
+ // --relay-daemon — internal: long-running background process started by
61
+ // ensureRelayRunning(). Streams queued events to the server via WebSocket.
62
+ if (args.includes("--relay-daemon")) {
63
+ try {
64
+ const { runDaemon } = await import("../src/relay/daemon");
65
+ await runDaemon();
66
+ process.exit(0);
67
+ } catch (err) {
68
+ const msg = err instanceof Error ? err.message : String(err);
69
+ console.error(`Relay daemon error: ${msg}`);
70
+ process.exit(1);
71
+ }
72
+ }
73
+
60
74
  /**
61
75
  * Centralised error handler for all CLI subcommands.
62
76
  * CliError → clean message, no stack trace, exit exitCode (1 or 2)
@@ -64,7 +78,7 @@ if (hookIdx >= 0) {
64
78
  */
65
79
  async function runCli() {
66
80
  // --help / -h (only when not inside a subcommand that handles its own --help)
67
- const SUBCOMMANDS = ["policies"];
81
+ const SUBCOMMANDS = ["policies", "login", "logout", "whoami", "relay", "sync"];
68
82
  if ((args.includes("--help") || args.includes("-h")) && !SUBCOMMANDS.includes(args[0])) {
69
83
  const extraArgs = args.filter((a) => a !== "--help" && a !== "-h");
70
84
  if (extraArgs.length > 0) {
@@ -94,6 +108,12 @@ COMMANDS
94
108
 
95
109
  policies --help, -h Show this help for the policies command
96
110
 
111
+ login Authenticate with the failproofai cloud (Google OAuth)
112
+ logout Clear local auth tokens and stop relay daemon
113
+ whoami Print current logged-in user
114
+ relay start|stop|status Manage the event relay daemon
115
+ sync One-shot flush of pending events to the server
116
+
97
117
  --version, -v Print version and exit
98
118
  --help, -h Show this help message
99
119
 
@@ -288,6 +308,73 @@ EXAMPLES
288
308
  process.exit(0);
289
309
  }
290
310
 
311
+ // login — authenticate with failproofai server via Google OAuth
312
+ if (args[0] === "login") {
313
+ const { login } = await import("../src/auth/login");
314
+ await login();
315
+ process.exit(0);
316
+ }
317
+
318
+ // logout — clear local tokens and stop relay daemon
319
+ if (args[0] === "logout") {
320
+ const { logout } = await import("../src/auth/logout");
321
+ await logout();
322
+ process.exit(0);
323
+ }
324
+
325
+ // whoami — print current user and auth status
326
+ if (args[0] === "whoami") {
327
+ const { whoami } = await import("../src/auth/logout");
328
+ whoami();
329
+ process.exit(0);
330
+ }
331
+
332
+ // relay start|stop|status — manage the event relay daemon
333
+ if (args[0] === "relay") {
334
+ const subcmd = args[1];
335
+ const { relayStatus, stopRelay } = await import("../src/relay/pid");
336
+
337
+ if (subcmd === "status") {
338
+ const s = relayStatus();
339
+ if (s.running) console.log(`Relay daemon running (pid ${s.pid})`);
340
+ else if (s.pid !== null) console.log(`Stale PID file (${s.pid}); daemon not running`);
341
+ else console.log("Relay daemon not running");
342
+ process.exit(0);
343
+ }
344
+
345
+ if (subcmd === "stop") {
346
+ const stopped = stopRelay();
347
+ console.log(stopped ? "Relay daemon stopped" : "Relay daemon was not running");
348
+ process.exit(0);
349
+ }
350
+
351
+ if (subcmd === "start") {
352
+ const { ensureRelayRunning, waitForRelayAlive } = await import("../src/relay/daemon");
353
+ ensureRelayRunning();
354
+ // Spawn is async — give the child a moment to write its PID file
355
+ const alive = await waitForRelayAlive();
356
+ const s = relayStatus();
357
+ if (alive && s.running) {
358
+ console.log(`Relay daemon started (pid ${s.pid})`);
359
+ process.exit(0);
360
+ }
361
+ console.log("Failed to start daemon");
362
+ process.exit(1);
363
+ }
364
+
365
+ throw new CliError(
366
+ `Usage: failproofai relay <start|stop|status>`
367
+ );
368
+ }
369
+
370
+ // sync — one-shot flush of pending events to server (fallback for no daemon)
371
+ if (args[0] === "sync") {
372
+ const { runOneShotSync } = await import("../src/relay/daemon");
373
+ const count = await runOneShotSync();
374
+ console.log(`Synced ${count} event${count === 1 ? "" : "s"} to server`);
375
+ process.exit(0);
376
+ }
377
+
291
378
  // Unknown flag guard — must appear after all known-flag branches
292
379
  const knownFlags = ["--version", "-v", "--help", "-h", "--hook"];
293
380
  const unknownFlag = args.find(a => a.startsWith("-") && !knownFlags.includes(a));
@@ -306,7 +393,7 @@ EXAMPLES
306
393
  return dp[m][n];
307
394
  }
308
395
 
309
- const primary = ["--version", "--help", "--hook", "policies"];
396
+ const primary = ["--version", "--help", "--hook", "policies", "login", "logout", "whoami", "relay", "sync"];
310
397
  const closest = primary.reduce((best, flag) => {
311
398
  const dist = levenshtein(unknownFlag, flag);
312
399
  return dist < best.dist ? { flag, dist } : best;
@@ -319,8 +406,8 @@ EXAMPLES
319
406
  );
320
407
  }
321
408
 
322
- // Unknown subcommand guard (non-flag args that aren't "policies")
323
- const unknownSubcommand = args.find(a => !a.startsWith("-") && a !== "policies");
409
+ // Unknown subcommand guard (non-flag args that aren't a known subcommand)
410
+ const unknownSubcommand = args.find(a => !a.startsWith("-") && !SUBCOMMANDS.includes(a));
324
411
  if (unknownSubcommand) {
325
412
  throw new CliError(
326
413
  `Unknown command: ${unknownSubcommand}\n` +