@vibelet/cli 0.1.34 → 0.1.36

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 (323) hide show
  1. package/app.json +5 -0
  2. package/dist/advertised-hosts.d.ts +34 -0
  3. package/dist/advertised-hosts.d.ts.map +1 -0
  4. package/dist/advertised-hosts.js +176 -0
  5. package/dist/advertised-hosts.js.map +1 -0
  6. package/dist/advertised-hosts.test.d.ts +2 -0
  7. package/dist/advertised-hosts.test.d.ts.map +1 -0
  8. package/dist/advertised-hosts.test.js +96 -0
  9. package/dist/advertised-hosts.test.js.map +1 -0
  10. package/dist/audit.d.ts +30 -0
  11. package/dist/audit.d.ts.map +1 -0
  12. package/dist/audit.js +73 -0
  13. package/dist/audit.js.map +1 -0
  14. package/dist/audit.test.d.ts +2 -0
  15. package/dist/audit.test.d.ts.map +1 -0
  16. package/dist/audit.test.js +33 -0
  17. package/dist/audit.test.js.map +1 -0
  18. package/dist/auth.d.ts +6 -0
  19. package/dist/auth.d.ts.map +1 -0
  20. package/dist/auth.js +27 -0
  21. package/dist/auth.js.map +1 -0
  22. package/dist/claude-hooks.d.ts +58 -0
  23. package/dist/claude-hooks.d.ts.map +1 -0
  24. package/dist/claude-hooks.js +129 -0
  25. package/dist/claude-hooks.js.map +1 -0
  26. package/dist/cli-version.d.ts +3 -0
  27. package/dist/cli-version.d.ts.map +1 -0
  28. package/dist/cli-version.js +35 -0
  29. package/dist/cli-version.js.map +1 -0
  30. package/dist/cli-version.test.d.ts +2 -0
  31. package/dist/cli-version.test.d.ts.map +1 -0
  32. package/dist/cli-version.test.js +38 -0
  33. package/dist/cli-version.test.js.map +1 -0
  34. package/dist/config.d.ts +30 -0
  35. package/dist/config.d.ts.map +1 -0
  36. package/dist/config.js +327 -0
  37. package/dist/config.js.map +1 -0
  38. package/dist/config.test.d.ts +2 -0
  39. package/dist/config.test.d.ts.map +1 -0
  40. package/dist/config.test.js +184 -0
  41. package/dist/config.test.js.map +1 -0
  42. package/dist/dev-auth.test.d.ts +2 -0
  43. package/dist/dev-auth.test.d.ts.map +1 -0
  44. package/dist/dev-auth.test.js +154 -0
  45. package/dist/dev-auth.test.js.map +1 -0
  46. package/dist/dev-script.test.d.ts +2 -0
  47. package/dist/dev-script.test.d.ts.map +1 -0
  48. package/dist/dev-script.test.js +412 -0
  49. package/dist/dev-script.test.js.map +1 -0
  50. package/dist/drivers/claude.d.ts +34 -0
  51. package/dist/drivers/claude.d.ts.map +1 -0
  52. package/dist/drivers/claude.js +413 -0
  53. package/dist/drivers/claude.js.map +1 -0
  54. package/dist/drivers/claude.test.d.ts +2 -0
  55. package/dist/drivers/claude.test.d.ts.map +1 -0
  56. package/dist/drivers/claude.test.js +951 -0
  57. package/dist/drivers/claude.test.js.map +1 -0
  58. package/dist/drivers/codex.d.ts +38 -0
  59. package/dist/drivers/codex.d.ts.map +1 -0
  60. package/dist/drivers/codex.js +771 -0
  61. package/dist/drivers/codex.js.map +1 -0
  62. package/dist/drivers/codex.test.d.ts +2 -0
  63. package/dist/drivers/codex.test.d.ts.map +1 -0
  64. package/dist/drivers/codex.test.js +939 -0
  65. package/dist/drivers/codex.test.js.map +1 -0
  66. package/dist/drivers/types.d.ts +14 -0
  67. package/dist/drivers/types.d.ts.map +1 -0
  68. package/dist/drivers/types.js +2 -0
  69. package/dist/drivers/types.js.map +1 -0
  70. package/dist/e2e.test.d.ts +2 -0
  71. package/dist/e2e.test.d.ts.map +1 -0
  72. package/dist/e2e.test.js +111 -0
  73. package/dist/e2e.test.js.map +1 -0
  74. package/dist/identity.d.ts +10 -0
  75. package/dist/identity.d.ts.map +1 -0
  76. package/dist/identity.js +66 -0
  77. package/dist/identity.js.map +1 -0
  78. package/dist/identity.test.d.ts +2 -0
  79. package/dist/identity.test.d.ts.map +1 -0
  80. package/dist/identity.test.js +25 -0
  81. package/dist/identity.test.js.map +1 -0
  82. package/dist/index-entry.test.d.ts +2 -0
  83. package/dist/index-entry.test.d.ts.map +1 -0
  84. package/dist/index-entry.test.js +272 -0
  85. package/dist/index-entry.test.js.map +1 -0
  86. package/dist/index.d.ts +2 -0
  87. package/dist/index.d.ts.map +1 -0
  88. package/dist/index.js +707 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/logger.d.ts +31 -0
  91. package/dist/logger.d.ts.map +1 -0
  92. package/dist/logger.js +75 -0
  93. package/dist/logger.js.map +1 -0
  94. package/dist/metrics.d.ts +52 -0
  95. package/dist/metrics.d.ts.map +1 -0
  96. package/dist/metrics.js +89 -0
  97. package/dist/metrics.js.map +1 -0
  98. package/dist/pairing-store.d.ts +29 -0
  99. package/dist/pairing-store.d.ts.map +1 -0
  100. package/dist/pairing-store.js +131 -0
  101. package/dist/pairing-store.js.map +1 -0
  102. package/dist/pairing-store.test.d.ts +2 -0
  103. package/dist/pairing-store.test.d.ts.map +1 -0
  104. package/dist/pairing-store.test.js +47 -0
  105. package/dist/pairing-store.test.js.map +1 -0
  106. package/dist/paths.d.ts +16 -0
  107. package/dist/paths.d.ts.map +1 -0
  108. package/dist/paths.js +18 -0
  109. package/dist/paths.js.map +1 -0
  110. package/dist/perf-compare.d.ts +13 -0
  111. package/dist/perf-compare.d.ts.map +1 -0
  112. package/dist/perf-compare.js +125 -0
  113. package/dist/perf-compare.js.map +1 -0
  114. package/dist/port-conflict.d.ts +9 -0
  115. package/dist/port-conflict.d.ts.map +1 -0
  116. package/dist/port-conflict.js +33 -0
  117. package/dist/port-conflict.js.map +1 -0
  118. package/dist/port-conflict.test.d.ts +2 -0
  119. package/dist/port-conflict.test.d.ts.map +1 -0
  120. package/dist/port-conflict.test.js +38 -0
  121. package/dist/port-conflict.test.js.map +1 -0
  122. package/dist/process-scanner.d.ts +43 -0
  123. package/dist/process-scanner.d.ts.map +1 -0
  124. package/dist/process-scanner.js +453 -0
  125. package/dist/process-scanner.js.map +1 -0
  126. package/dist/process-scanner.perf.test.d.ts +2 -0
  127. package/dist/process-scanner.perf.test.d.ts.map +1 -0
  128. package/dist/process-scanner.perf.test.js +186 -0
  129. package/dist/process-scanner.perf.test.js.map +1 -0
  130. package/dist/process-scanner.test.d.ts +2 -0
  131. package/dist/process-scanner.test.d.ts.map +1 -0
  132. package/dist/process-scanner.test.js +399 -0
  133. package/dist/process-scanner.test.js.map +1 -0
  134. package/dist/push-protocol.d.ts +15 -0
  135. package/dist/push-protocol.d.ts.map +1 -0
  136. package/dist/push-protocol.js +23 -0
  137. package/dist/push-protocol.js.map +1 -0
  138. package/dist/push-protocol.test.d.ts +2 -0
  139. package/dist/push-protocol.test.d.ts.map +1 -0
  140. package/dist/push-protocol.test.js +57 -0
  141. package/dist/push-protocol.test.js.map +1 -0
  142. package/dist/push-store.d.ts +22 -0
  143. package/dist/push-store.d.ts.map +1 -0
  144. package/dist/push-store.js +103 -0
  145. package/dist/push-store.js.map +1 -0
  146. package/dist/push-store.test.d.ts +2 -0
  147. package/dist/push-store.test.d.ts.map +1 -0
  148. package/dist/push-store.test.js +79 -0
  149. package/dist/push-store.test.js.map +1 -0
  150. package/dist/push.d.ts +65 -0
  151. package/dist/push.d.ts.map +1 -0
  152. package/dist/push.js +202 -0
  153. package/dist/push.js.map +1 -0
  154. package/dist/push.test.d.ts +2 -0
  155. package/dist/push.test.d.ts.map +1 -0
  156. package/dist/push.test.js +199 -0
  157. package/dist/push.test.js.map +1 -0
  158. package/dist/safe-stdio.d.ts +3 -0
  159. package/dist/safe-stdio.d.ts.map +1 -0
  160. package/dist/safe-stdio.js +46 -0
  161. package/dist/safe-stdio.js.map +1 -0
  162. package/dist/scanner.d.ts +30 -0
  163. package/dist/scanner.d.ts.map +1 -0
  164. package/dist/scanner.js +859 -0
  165. package/dist/scanner.js.map +1 -0
  166. package/dist/scanner.perf.test.d.ts +2 -0
  167. package/dist/scanner.perf.test.d.ts.map +1 -0
  168. package/dist/scanner.perf.test.js +320 -0
  169. package/dist/scanner.perf.test.js.map +1 -0
  170. package/dist/scanner.test.d.ts +2 -0
  171. package/dist/scanner.test.d.ts.map +1 -0
  172. package/dist/scanner.test.js +948 -0
  173. package/dist/scanner.test.js.map +1 -0
  174. package/dist/session-inventory.d.ts +63 -0
  175. package/dist/session-inventory.d.ts.map +1 -0
  176. package/dist/session-inventory.js +525 -0
  177. package/dist/session-inventory.js.map +1 -0
  178. package/dist/session-inventory.perf.test.d.ts +2 -0
  179. package/dist/session-inventory.perf.test.d.ts.map +1 -0
  180. package/dist/session-inventory.perf.test.js +220 -0
  181. package/dist/session-inventory.perf.test.js.map +1 -0
  182. package/dist/session-inventory.test.d.ts +2 -0
  183. package/dist/session-inventory.test.d.ts.map +1 -0
  184. package/dist/session-inventory.test.js +712 -0
  185. package/dist/session-inventory.test.js.map +1 -0
  186. package/dist/session-manager.d.ts +75 -0
  187. package/dist/session-manager.d.ts.map +1 -0
  188. package/dist/session-manager.js +1515 -0
  189. package/dist/session-manager.js.map +1 -0
  190. package/dist/session-manager.test.d.ts +2 -0
  191. package/dist/session-manager.test.d.ts.map +1 -0
  192. package/dist/session-manager.test.js +2861 -0
  193. package/dist/session-manager.test.js.map +1 -0
  194. package/dist/session-store.d.ts +42 -0
  195. package/dist/session-store.d.ts.map +1 -0
  196. package/dist/session-store.js +163 -0
  197. package/dist/session-store.js.map +1 -0
  198. package/dist/session-store.test.d.ts +2 -0
  199. package/dist/session-store.test.d.ts.map +1 -0
  200. package/dist/session-store.test.js +236 -0
  201. package/dist/session-store.test.js.map +1 -0
  202. package/dist/session-title.d.ts +6 -0
  203. package/dist/session-title.d.ts.map +1 -0
  204. package/dist/session-title.js +105 -0
  205. package/dist/session-title.js.map +1 -0
  206. package/dist/session-title.perf.test.d.ts +2 -0
  207. package/dist/session-title.perf.test.d.ts.map +1 -0
  208. package/dist/session-title.perf.test.js +99 -0
  209. package/dist/session-title.perf.test.js.map +1 -0
  210. package/dist/session-title.test.d.ts +2 -0
  211. package/dist/session-title.test.d.ts.map +1 -0
  212. package/dist/session-title.test.js +199 -0
  213. package/dist/session-title.test.js.map +1 -0
  214. package/dist/shutdown-endpoint.test.d.ts +2 -0
  215. package/dist/shutdown-endpoint.test.d.ts.map +1 -0
  216. package/dist/shutdown-endpoint.test.js +93 -0
  217. package/dist/shutdown-endpoint.test.js.map +1 -0
  218. package/dist/storage-housekeeping.d.ts +28 -0
  219. package/dist/storage-housekeeping.d.ts.map +1 -0
  220. package/dist/storage-housekeeping.js +76 -0
  221. package/dist/storage-housekeeping.js.map +1 -0
  222. package/dist/storage-housekeeping.test.d.ts +2 -0
  223. package/dist/storage-housekeeping.test.d.ts.map +1 -0
  224. package/dist/storage-housekeeping.test.js +65 -0
  225. package/dist/storage-housekeeping.test.js.map +1 -0
  226. package/dist/test-daemon-harness.d.ts +31 -0
  227. package/dist/test-daemon-harness.d.ts.map +1 -0
  228. package/dist/test-daemon-harness.js +337 -0
  229. package/dist/test-daemon-harness.js.map +1 -0
  230. package/dist/token-auth.test.d.ts +2 -0
  231. package/dist/token-auth.test.d.ts.map +1 -0
  232. package/dist/token-auth.test.js +52 -0
  233. package/dist/token-auth.test.js.map +1 -0
  234. package/dist/utils.d.ts +4 -0
  235. package/dist/utils.d.ts.map +1 -0
  236. package/dist/utils.js +40 -0
  237. package/dist/utils.js.map +1 -0
  238. package/dist/utils.test.d.ts +2 -0
  239. package/dist/utils.test.d.ts.map +1 -0
  240. package/dist/utils.test.js +54 -0
  241. package/dist/utils.test.js.map +1 -0
  242. package/dist/ws-data.d.ts +4 -0
  243. package/dist/ws-data.d.ts.map +1 -0
  244. package/dist/ws-data.js +20 -0
  245. package/dist/ws-data.js.map +1 -0
  246. package/dist/ws-data.test.d.ts +2 -0
  247. package/dist/ws-data.test.d.ts.map +1 -0
  248. package/dist/ws-data.test.js +17 -0
  249. package/dist/ws-data.test.js.map +1 -0
  250. package/package.json +24 -27
  251. package/perf-reporter.mjs +138 -0
  252. package/scripts/build-release.mjs +41 -0
  253. package/scripts/dev.mjs +537 -0
  254. package/src/advertised-hosts.test.ts +125 -0
  255. package/src/advertised-hosts.ts +225 -0
  256. package/src/audit.test.ts +38 -0
  257. package/src/audit.ts +117 -0
  258. package/src/auth.ts +31 -0
  259. package/src/claude-hooks.ts +195 -0
  260. package/src/cli-version.test.ts +36 -0
  261. package/src/cli-version.ts +46 -0
  262. package/src/config.test.ts +254 -0
  263. package/src/config.ts +324 -0
  264. package/src/dev-auth.test.ts +183 -0
  265. package/src/dev-script.test.ts +511 -0
  266. package/src/drivers/claude.test.ts +1186 -0
  267. package/src/drivers/claude.ts +443 -0
  268. package/src/drivers/codex.test.ts +1096 -0
  269. package/src/drivers/codex.ts +879 -0
  270. package/src/drivers/types.ts +15 -0
  271. package/src/e2e.test.ts +139 -0
  272. package/src/identity.test.ts +26 -0
  273. package/src/identity.ts +82 -0
  274. package/src/index-entry.test.ts +336 -0
  275. package/src/index.ts +781 -0
  276. package/src/logger.ts +112 -0
  277. package/src/metrics.ts +117 -0
  278. package/src/pairing-store.test.ts +53 -0
  279. package/src/pairing-store.ts +154 -0
  280. package/src/paths.ts +19 -0
  281. package/src/perf-compare.ts +164 -0
  282. package/src/port-conflict.test.ts +45 -0
  283. package/src/port-conflict.ts +44 -0
  284. package/src/process-scanner.perf.test.ts +222 -0
  285. package/src/process-scanner.test.ts +575 -0
  286. package/src/process-scanner.ts +514 -0
  287. package/src/push-protocol.test.ts +74 -0
  288. package/src/push-protocol.ts +36 -0
  289. package/src/push-store.test.ts +89 -0
  290. package/src/push-store.ts +126 -0
  291. package/src/push.test.ts +234 -0
  292. package/src/push.ts +318 -0
  293. package/src/safe-stdio.ts +51 -0
  294. package/src/scanner.perf.test.ts +359 -0
  295. package/src/scanner.test.ts +1045 -0
  296. package/src/scanner.ts +924 -0
  297. package/src/session-inventory.perf.test.ts +250 -0
  298. package/src/session-inventory.test.ts +1002 -0
  299. package/src/session-inventory.ts +721 -0
  300. package/src/session-manager.test.ts +3430 -0
  301. package/src/session-manager.ts +1775 -0
  302. package/src/session-store.test.ts +276 -0
  303. package/src/session-store.ts +202 -0
  304. package/src/session-title.perf.test.ts +118 -0
  305. package/src/session-title.test.ts +286 -0
  306. package/src/session-title.ts +108 -0
  307. package/src/shutdown-endpoint.test.ts +95 -0
  308. package/src/storage-housekeeping.test.ts +78 -0
  309. package/src/storage-housekeeping.ts +111 -0
  310. package/src/test-daemon-harness.ts +410 -0
  311. package/src/token-auth.test.ts +67 -0
  312. package/src/utils.test.ts +65 -0
  313. package/src/utils.ts +47 -0
  314. package/src/ws-data.test.ts +20 -0
  315. package/src/ws-data.ts +26 -0
  316. package/tsconfig.json +12 -0
  317. package/README.md +0 -80
  318. package/bin/cloudflared-quick-tunnel.mjs +0 -11
  319. package/bin/cloudflared-resolver.mjs +0 -68
  320. package/bin/vibelet-runtime-policy.mjs +0 -36
  321. package/bin/vibelet.cjs +0 -12
  322. package/bin/vibelet.mjs +0 -1019
  323. package/dist/index.cjs +0 -123
@@ -0,0 +1,443 @@
1
+ import { spawn, type ChildProcess } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import type { Driver, MessageHandler } from './types.js';
4
+ import { config } from '../config.js';
5
+ import { logger as rootLogger } from '../logger.js';
6
+ import { audit } from '../audit.js';
7
+ import { cleanupClaudeHookFiles, createClaudeHookFiles, type ClaudeHookFiles } from '../claude-hooks.js';
8
+ import { writeStderrSafe } from '../safe-stdio.js';
9
+
10
+ const log = rootLogger.child({ module: 'claude' });
11
+ export const CLAUDE_SYNTHETIC_APPROVAL_PREFIX = 'claude-permission-denial:';
12
+ const ANSI_ESCAPE_PATTERN = /\u001B\[[0-?]*[ -/]*[@-~]/g;
13
+
14
+ function shouldLogRemote(): boolean {
15
+ return process.env.VIBE_TEST !== '1' && !process.argv.includes('--test');
16
+ }
17
+
18
+ function logRemote(message: string): void {
19
+ if (!shouldLogRemote()) return;
20
+ writeStderrSafe(`\x1b[33m⚡ [Vibelet] ${message}\x1b[0m\n`);
21
+ }
22
+
23
+ function extractStructuredToolResultText(content: unknown): string {
24
+ if (!Array.isArray(content)) return '';
25
+ return content
26
+ .map((item) => {
27
+ if (!item || typeof item !== 'object') return '';
28
+ if (typeof (item as any).text === 'string') return (item as any).text;
29
+ if (typeof (item as any).content === 'string') return (item as any).content;
30
+ return '';
31
+ })
32
+ .filter(Boolean)
33
+ .join('\n');
34
+ }
35
+
36
+ function isToolReferenceOnlyContent(content: unknown): boolean {
37
+ return Array.isArray(content)
38
+ && content.length > 0
39
+ && content.every((item) => item && typeof item === 'object' && (item as any).type === 'tool_reference');
40
+ }
41
+
42
+ function stringifyToolResult(content: unknown): string {
43
+ if (typeof content === 'string') return content;
44
+ const extracted = extractStructuredToolResultText(content);
45
+ if (extracted) return extracted;
46
+ if (isToolReferenceOnlyContent(content)) return '';
47
+ return JSON.stringify(content);
48
+ }
49
+
50
+ function looksLikePermissionDenial(message: string): boolean {
51
+ return /requested permissions|haven't granted/i.test(message);
52
+ }
53
+
54
+ function sanitizeClaudeErrorText(text: string): string {
55
+ return text
56
+ .replace(ANSI_ESCAPE_PATTERN, '')
57
+ .replace(/\s+/g, ' ')
58
+ .trim();
59
+ }
60
+
61
+ function isGenericClaudeErrorText(text: string): boolean {
62
+ const normalized = text.toLowerCase();
63
+ return normalized === 'error' || normalized === 'failed' || normalized === 'unknown error';
64
+ }
65
+
66
+ function looksLikeClaudeUsageLimit(text: string): boolean {
67
+ return (
68
+ /\brate limit\b/i.test(text) ||
69
+ /\busage limit\b/i.test(text) ||
70
+ /\bquota\b/i.test(text) ||
71
+ /\btoo many requests\b/i.test(text) ||
72
+ /\bcredit balance\b/i.test(text) ||
73
+ /\bcredits? remaining\b/i.test(text) ||
74
+ /\bmax(?:imum)? usage\b/i.test(text)
75
+ );
76
+ }
77
+
78
+ function formatClaudeUsageLimitMessage(detail: string): string {
79
+ return detail.toLowerCase().startsWith('claude ')
80
+ ? detail
81
+ : `Claude usage limit reached. ${detail}`;
82
+ }
83
+
84
+ function formatClaudeFailureMessage(options: {
85
+ resultText?: string;
86
+ stderrLines?: string[];
87
+ exitCode?: number | null;
88
+ }): string {
89
+ const normalizedResult = sanitizeClaudeErrorText(options.resultText ?? '');
90
+ const stderrDetails = (options.stderrLines ?? [])
91
+ .map(sanitizeClaudeErrorText)
92
+ .filter(Boolean)
93
+ .slice(-3);
94
+ const limitDetail = [normalizedResult, ...stderrDetails].find(
95
+ (text) => Boolean(text) && looksLikeClaudeUsageLimit(text),
96
+ );
97
+
98
+ if (limitDetail) {
99
+ return formatClaudeUsageLimitMessage(limitDetail);
100
+ }
101
+
102
+ const detail = [normalizedResult, ...stderrDetails].find(
103
+ (text) => Boolean(text) && !isGenericClaudeErrorText(text),
104
+ );
105
+ if (detail) {
106
+ if (options.exitCode != null && detail !== normalizedResult) {
107
+ return `Claude exited with code ${options.exitCode}: ${detail}`;
108
+ }
109
+ return detail;
110
+ }
111
+
112
+ if (options.exitCode != null) {
113
+ return `Claude exited with code ${options.exitCode}`;
114
+ }
115
+ return 'Claude returned an error result.';
116
+ }
117
+
118
+ export function isClaudeSyntheticApprovalRequestId(requestId: string): boolean {
119
+ return requestId.startsWith(CLAUDE_SYNTHETIC_APPROVAL_PREFIX);
120
+ }
121
+
122
+ export class ClaudeDriver implements Driver {
123
+ private proc: ChildProcess | null = null;
124
+ private handler: MessageHandler | null = null;
125
+ private sessionId = '';
126
+ private buffer = '';
127
+ private cwd = '';
128
+ private approvalMode?: string;
129
+ private sawFinalResult = false;
130
+ private interrupted = false;
131
+ private exitHandler: ((code: number | null) => void) | null = null;
132
+ private lastStderr: string[] = [];
133
+ private pendingPermissionDescriptions = new Map<string, string>();
134
+ private emittedToolCallIds = new Set<string>();
135
+ /** True while the CLI is replaying old messages before the new response starts. */
136
+ private replayPhase = false;
137
+ private hookPort: number | null = null;
138
+ private hookSecret: string | null = null;
139
+ private hookFiles: ClaudeHookFiles | null = null;
140
+
141
+ private buildClaudeEnv(): Record<string, string> {
142
+ const env = config.buildSanitizedEnv();
143
+ for (const key of Object.keys(env)) {
144
+ if (key.startsWith('CMUX_')) {
145
+ delete env[key];
146
+ }
147
+ }
148
+ return env;
149
+ }
150
+
151
+ async start(cwd: string, resumeSessionId?: string, approvalMode?: string): Promise<string> {
152
+ this.cwd = cwd;
153
+ this.approvalMode = approvalMode;
154
+ if (resumeSessionId) this.sessionId = resumeSessionId;
155
+ if (!this.sessionId) this.sessionId = `pending_${Date.now()}`;
156
+ log.info({ sessionId: this.sessionId, cwd }, 'session initialized');
157
+ audit.emit('driver.spawn', { agent: 'claude', sessionId: this.sessionId, cwd });
158
+ return this.sessionId;
159
+ }
160
+
161
+ configureHookBridge(port: number, secret: string): void {
162
+ this.hookPort = port;
163
+ this.hookSecret = secret;
164
+ }
165
+
166
+ sendPrompt(text: string): void {
167
+ const args = [
168
+ '-p', text,
169
+ '--output-format', 'stream-json',
170
+ '--verbose',
171
+ '--include-partial-messages',
172
+ ];
173
+
174
+ if (this.sessionId && !this.sessionId.startsWith('pending_')) {
175
+ args.push('--resume', this.sessionId);
176
+ }
177
+
178
+ cleanupClaudeHookFiles(this.hookFiles);
179
+ this.hookFiles = null;
180
+
181
+ if (this.approvalMode !== 'plan' && this.approvalMode !== 'acceptEdits' && this.approvalMode !== 'autoApprove' && this.hookPort && this.hookSecret) {
182
+ this.hookFiles = createClaudeHookFiles(this.hookPort, this.hookSecret);
183
+ args.push('--settings', this.hookFiles.settingsPath);
184
+ }
185
+
186
+ if (this.approvalMode === 'plan') args.push('--permission-mode', 'plan');
187
+ if (this.approvalMode === 'acceptEdits') args.push('--permission-mode', 'acceptEdits');
188
+ if (this.approvalMode === 'autoApprove') args.push('--dangerously-skip-permissions');
189
+
190
+ const label = this.sessionId.startsWith('pending_') ? '(new)' : `(resume ${this.sessionId.slice(0, 8)})`;
191
+ log.info({ sessionId: this.sessionId, label, promptPreview: text.slice(0, 50) }, 'running claude');
192
+ logRemote( `New message: ${text.slice(0, 60)}`);
193
+ this.sawFinalResult = false;
194
+ this.interrupted = false;
195
+ this.lastStderr = [];
196
+ this.pendingPermissionDescriptions.clear();
197
+ this.emittedToolCallIds.clear();
198
+ this.replayPhase = true;
199
+
200
+ this.proc = spawn(config.claudePath, args, {
201
+ cwd: this.cwd || undefined,
202
+ stdio: ['ignore', 'pipe', 'pipe'],
203
+ env: this.buildClaudeEnv(),
204
+ ...(process.platform === 'win32' ? { shell: true } : {}),
205
+ });
206
+
207
+ this.proc.on('error', (err) => {
208
+ let message = err.message;
209
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT' && this.cwd && !existsSync(this.cwd)) {
210
+ message = `Working directory does not exist: ${this.cwd}`;
211
+ }
212
+ log.error({ sessionId: this.sessionId, error: message, cwd: this.cwd }, 'spawn error');
213
+ audit.emit('driver.error', { agent: 'claude', sessionId: this.sessionId, error: message });
214
+ this.handler?.({ type: 'error', sessionId: this.sessionId, message });
215
+ });
216
+
217
+ this.buffer = '';
218
+ this.proc.stdout!.on('data', (chunk: Buffer) => {
219
+ this.buffer += chunk.toString();
220
+ const lines = this.buffer.split('\n');
221
+ this.buffer = lines.pop()!;
222
+ for (const line of lines) {
223
+ if (!line.trim()) continue;
224
+ try {
225
+ this.handleRaw(JSON.parse(line));
226
+ } catch {
227
+ log.warn({ sessionId: this.sessionId, linePreview: line.slice(0, 100) }, 'failed to parse stdout line');
228
+ }
229
+ }
230
+ });
231
+
232
+ this.proc.stderr!.on('data', (chunk: Buffer) => {
233
+ const t = chunk.toString().trim();
234
+ if (t) {
235
+ log.debug({ sessionId: this.sessionId, stderr: t }, 'stderr');
236
+ this.lastStderr.push(t);
237
+ if (this.lastStderr.length > 10) this.lastStderr.shift();
238
+ }
239
+ });
240
+
241
+ this.proc.on('exit', (code, signal) => {
242
+ log.info({ sessionId: this.sessionId, exitCode: code, signal }, 'process exited');
243
+ const wasInterrupted = this.interrupted;
244
+ this.proc = null;
245
+ cleanupClaudeHookFiles(this.hookFiles);
246
+ this.hookFiles = null;
247
+ this.interrupted = false;
248
+ if (wasInterrupted && !this.sawFinalResult) {
249
+ this.handler?.({ type: 'session.interrupted', sessionId: this.sessionId });
250
+ } else if (code && code !== 0 && !this.sawFinalResult) {
251
+ log.error({ sessionId: this.sessionId, exitCode: code, lastStderr: this.lastStderr.slice(-3) }, 'abnormal exit');
252
+ this.handler?.({
253
+ type: 'error',
254
+ sessionId: this.sessionId,
255
+ message: formatClaudeFailureMessage({ stderrLines: this.lastStderr, exitCode: code }),
256
+ });
257
+ }
258
+ this.exitHandler?.(code);
259
+ });
260
+ }
261
+
262
+ respondApproval(requestId: string, approved: boolean): boolean {
263
+ log.info({ sessionId: this.sessionId, requestId, approved }, 'approval response');
264
+ if (!this.proc?.stdin?.writable) {
265
+ log.error({ sessionId: this.sessionId }, 'cannot send approval: stdin not writable');
266
+ return false;
267
+ }
268
+ const response = JSON.stringify({
269
+ type: 'control_response',
270
+ request_id: requestId,
271
+ permission_granted: approved,
272
+ });
273
+ this.proc.stdin.write(response + '\n');
274
+ return true;
275
+ }
276
+
277
+ setApprovalMode(mode: string): void {
278
+ this.approvalMode = mode;
279
+ log.info({ sessionId: this.sessionId, approvalMode: mode }, 'approval mode updated');
280
+ }
281
+
282
+ interrupt(): void {
283
+ if (this.proc && !this.proc.killed) {
284
+ this.interrupted = true;
285
+ this.proc.kill('SIGTERM');
286
+ log.info({ sessionId: this.sessionId }, 'interrupted');
287
+ }
288
+ }
289
+
290
+ stop(): void {
291
+ if (!this.proc) return;
292
+ this.proc.kill('SIGTERM');
293
+ const p = this.proc;
294
+ setTimeout(() => { if (!p.killed) p.kill('SIGKILL'); }, 5000).unref();
295
+ this.proc = null;
296
+ cleanupClaudeHookFiles(this.hookFiles);
297
+ this.hookFiles = null;
298
+ }
299
+
300
+ onMessage(handler: MessageHandler): void {
301
+ this.handler = handler;
302
+ }
303
+
304
+ onExit(handler: (code: number | null) => void): void {
305
+ this.exitHandler = handler;
306
+ }
307
+
308
+ private handleRaw(raw: Record<string, unknown>): void {
309
+ const type = raw.type as string;
310
+
311
+ if (type === 'system' && (raw as any).subtype === 'init') {
312
+ const newId = (raw as any).session_id ?? '';
313
+ if (newId && newId !== this.sessionId) {
314
+ log.info({ oldSessionId: this.sessionId, newSessionId: newId }, 'session ID resolved');
315
+ audit.emit('driver.init', { agent: 'claude', sessionId: newId });
316
+ this.sessionId = newId;
317
+ }
318
+ return;
319
+ }
320
+
321
+ if (!this.handler) return;
322
+
323
+ switch (type) {
324
+ case 'assistant': {
325
+ if (this.replayPhase) break; // skip replayed history from --include-partial-messages
326
+ const content = (raw as any).message?.content;
327
+ if (!Array.isArray(content)) break;
328
+ for (const block of content) {
329
+ if (block.type === 'tool_use' && !this.emittedToolCallIds.has(block.id)) {
330
+ this.emittedToolCallIds.add(block.id);
331
+ this.handler({
332
+ type: 'tool.call', sessionId: this.sessionId,
333
+ toolName: block.name, input: block.input ?? {}, toolCallId: block.id,
334
+ });
335
+ }
336
+ }
337
+ break;
338
+ }
339
+
340
+ case 'user': {
341
+ if (this.replayPhase) break; // skip replayed history from --include-partial-messages
342
+ const content = (raw as any).message?.content;
343
+ if (!Array.isArray(content)) break;
344
+ for (const block of content) {
345
+ if (block.type === 'tool_result') {
346
+ const output = stringifyToolResult(block.content);
347
+ if (!output) continue;
348
+ if (block.is_error === true && typeof block.tool_use_id === 'string' && looksLikePermissionDenial(output)) {
349
+ this.pendingPermissionDescriptions.set(block.tool_use_id, output);
350
+ continue;
351
+ }
352
+ this.handler({
353
+ type: 'tool.result', sessionId: this.sessionId,
354
+ toolCallId: block.tool_use_id, output,
355
+ });
356
+ }
357
+ }
358
+ break;
359
+ }
360
+
361
+ case 'control_request': {
362
+ this.replayPhase = false;
363
+ const req = (raw as any).request;
364
+ if (req?.subtype === 'can_use_tool') {
365
+ audit.emit('approval.request', { agent: 'claude', sessionId: this.sessionId, toolName: req.tool_name });
366
+ this.handler({
367
+ type: 'approval.request', sessionId: this.sessionId,
368
+ requestId: (raw as any).request_id,
369
+ toolName: req.tool_name ?? 'unknown',
370
+ input: req.input ?? {},
371
+ description: req.description ?? req.title ?? '',
372
+ });
373
+ }
374
+ break;
375
+ }
376
+
377
+ case 'stream_event': {
378
+ this.replayPhase = false; // stream events only come from the new response
379
+ const event = (raw as any).event;
380
+ if (event?.type === 'content_block_delta' && event?.delta?.type === 'text_delta' && event?.delta?.text) {
381
+ this.handler({ type: 'text.delta', sessionId: this.sessionId, content: event.delta.text });
382
+ }
383
+ break;
384
+ }
385
+
386
+ case 'result': {
387
+ this.replayPhase = false;
388
+ this.sawFinalResult = true;
389
+ const resultText = typeof (raw as any).result === 'string'
390
+ ? (raw as any).result
391
+ : '';
392
+ if ((raw as any).is_error) {
393
+ logRemote( 'Claude failed.');
394
+ this.handler({
395
+ type: 'error',
396
+ sessionId: this.sessionId,
397
+ message: formatClaudeFailureMessage({ resultText, stderrLines: this.lastStderr }),
398
+ });
399
+ break;
400
+ }
401
+ const permissionDenials = Array.isArray((raw as any).permission_denials)
402
+ ? (raw as any).permission_denials as Array<Record<string, unknown>>
403
+ : [];
404
+ if (permissionDenials.length > 0) {
405
+ const firstDenial = permissionDenials[0] ?? {};
406
+ const toolUseId = typeof firstDenial.tool_use_id === 'string'
407
+ ? firstDenial.tool_use_id
408
+ : `missing_${Date.now()}`;
409
+ const toolName = typeof firstDenial.tool_name === 'string'
410
+ ? firstDenial.tool_name
411
+ : 'unknown';
412
+ const input = firstDenial.tool_input && typeof firstDenial.tool_input === 'object'
413
+ ? firstDenial.tool_input as Record<string, unknown>
414
+ : {};
415
+ const description = this.pendingPermissionDescriptions.get(toolUseId)
416
+ ?? `Claude requested permissions to use ${toolName}.`;
417
+ audit.emit('approval.request', { agent: 'claude', sessionId: this.sessionId, toolName });
418
+ this.handler({
419
+ type: 'approval.request',
420
+ sessionId: this.sessionId,
421
+ requestId: `${CLAUDE_SYNTHETIC_APPROVAL_PREFIX}${toolUseId}`,
422
+ toolName,
423
+ input,
424
+ description,
425
+ });
426
+ }
427
+ this.pendingPermissionDescriptions.clear();
428
+ const cost = (raw as any).total_cost_usd;
429
+ const usage = (raw as any).usage
430
+ ? { inputTokens: (raw as any).usage.input_tokens, outputTokens: (raw as any).usage.output_tokens }
431
+ : undefined;
432
+ logRemote( 'Claude finished. Run `claude --continue` to continue on desktop.');
433
+ audit.emit('session.done', { agent: 'claude', sessionId: this.sessionId, cost, usage });
434
+ this.handler({
435
+ type: 'session.done', sessionId: this.sessionId,
436
+ cost,
437
+ usage,
438
+ });
439
+ break;
440
+ }
441
+ }
442
+ }
443
+ }