@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,879 @@
1
+ import { spawn, type ChildProcess } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import type { ApprovalRequestPayload } from '@vibelet/shared';
4
+ import type { Driver, MessageHandler } from './types.js';
5
+ import { config } from '../config.js';
6
+ import { logger as rootLogger } from '../logger.js';
7
+ import { metrics } from '../metrics.js';
8
+ import { audit } from '../audit.js';
9
+ import { CLI_VERSION } from '../cli-version.js';
10
+
11
+ const log = rootLogger.child({ module: 'codex' });
12
+
13
+ type CodexApprovalRequest =
14
+ | {
15
+ kind: 'command-execution';
16
+ responseKind: 'v2';
17
+ rpcId: number;
18
+ toolName: string;
19
+ input: Record<string, unknown>;
20
+ }
21
+ | {
22
+ kind: 'file-change';
23
+ responseKind: 'v2';
24
+ rpcId: number;
25
+ toolName: string;
26
+ input: Record<string, unknown>;
27
+ }
28
+ | {
29
+ kind: 'request-user-input-approval';
30
+ rpcId: number;
31
+ questionId: string;
32
+ approveLabel: string;
33
+ denyLabel: string;
34
+ toolName: string;
35
+ input: Record<string, unknown>;
36
+ }
37
+ | {
38
+ kind: 'exec-command-legacy';
39
+ rpcId: number;
40
+ toolName: string;
41
+ input: Record<string, unknown>;
42
+ }
43
+ | {
44
+ kind: 'apply-patch-legacy';
45
+ rpcId: number;
46
+ toolName: string;
47
+ input: Record<string, unknown>;
48
+ };
49
+
50
+ type CodexToolContext = {
51
+ toolName: string;
52
+ input: Record<string, unknown>;
53
+ };
54
+
55
+ function serializeApprovalContext(request: CodexApprovalRequest): NonNullable<ApprovalRequestPayload['approvalContext']> {
56
+ switch (request.kind) {
57
+ case 'request-user-input-approval':
58
+ return {
59
+ provider: 'codex',
60
+ kind: request.kind,
61
+ rpcId: request.rpcId,
62
+ questionId: request.questionId,
63
+ approveLabel: request.approveLabel,
64
+ denyLabel: request.denyLabel,
65
+ };
66
+ default:
67
+ return {
68
+ provider: 'codex',
69
+ kind: request.kind,
70
+ rpcId: request.rpcId,
71
+ };
72
+ }
73
+ }
74
+
75
+ function restoreApprovalRequest(approval: ApprovalRequestPayload): CodexApprovalRequest | null {
76
+ const context = approval.approvalContext;
77
+ if (!context || context.provider !== 'codex') {
78
+ return null;
79
+ }
80
+
81
+ switch (context.kind) {
82
+ case 'command-execution':
83
+ return {
84
+ kind: context.kind,
85
+ responseKind: 'v2',
86
+ rpcId: context.rpcId,
87
+ toolName: approval.toolName,
88
+ input: approval.input,
89
+ };
90
+ case 'file-change':
91
+ return {
92
+ kind: context.kind,
93
+ responseKind: 'v2',
94
+ rpcId: context.rpcId,
95
+ toolName: approval.toolName,
96
+ input: approval.input,
97
+ };
98
+ case 'request-user-input-approval':
99
+ if (!context.questionId || !context.approveLabel || !context.denyLabel) {
100
+ return null;
101
+ }
102
+ return {
103
+ kind: context.kind,
104
+ rpcId: context.rpcId,
105
+ questionId: context.questionId,
106
+ approveLabel: context.approveLabel,
107
+ denyLabel: context.denyLabel,
108
+ toolName: approval.toolName,
109
+ input: approval.input,
110
+ };
111
+ case 'exec-command-legacy':
112
+ return {
113
+ kind: context.kind,
114
+ rpcId: context.rpcId,
115
+ toolName: approval.toolName,
116
+ input: approval.input,
117
+ };
118
+ case 'apply-patch-legacy':
119
+ return {
120
+ kind: context.kind,
121
+ rpcId: context.rpcId,
122
+ toolName: approval.toolName,
123
+ input: approval.input,
124
+ };
125
+ default:
126
+ return null;
127
+ }
128
+ }
129
+
130
+ function asRecord(value: unknown): Record<string, unknown> | null {
131
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
132
+ return value as Record<string, unknown>;
133
+ }
134
+
135
+ function readString(value: unknown): string | null {
136
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
137
+ }
138
+
139
+ const INTERRUPTED_TURN_STATUSES = new Set(['aborted', 'interrupted', 'cancelled', 'canceled']);
140
+
141
+ function normalizeTurnStatus(value: unknown): string | null {
142
+ const text = readString(value);
143
+ return text ? text.toLowerCase() : null;
144
+ }
145
+
146
+ function normalizeItemType(value: unknown): string | null {
147
+ const text = readString(value);
148
+ return text ? text.replace(/[^a-z0-9]/gi, '').toLowerCase() : null;
149
+ }
150
+
151
+ function readItemId(record: Record<string, unknown> | null | undefined): string | null {
152
+ if (!record) return null;
153
+ return readString(record.itemId) ?? readString(record.id) ?? readString(record.callId) ?? readString(record.call_id);
154
+ }
155
+
156
+ function omitKeys(record: Record<string, unknown>, keys: string[]): Record<string, unknown> {
157
+ const next: Record<string, unknown> = {};
158
+ for (const [key, value] of Object.entries(record)) {
159
+ if (!keys.includes(key)) next[key] = value;
160
+ }
161
+ return next;
162
+ }
163
+
164
+ function isApproveLabel(label: string): boolean {
165
+ return /\bapprove\b|\ballow\b|\baccept\b|\byes\b|\bcontinue\b|\bproceed\b|\brun\b|\bapply\b/i.test(label);
166
+ }
167
+
168
+ function isDenyLabel(label: string): boolean {
169
+ return /\bdeny\b|\breject\b|\bdecline\b|\bno\b|\bcancel\b|\babort\b|\bstop\b/i.test(label);
170
+ }
171
+
172
+ function resolveApprovalQuestion(questions: unknown): {
173
+ questionId: string;
174
+ approveLabel: string;
175
+ denyLabel: string;
176
+ } | null {
177
+ if (!Array.isArray(questions)) return null;
178
+ for (const question of questions) {
179
+ const record = asRecord(question);
180
+ if (!record) continue;
181
+
182
+ const questionId = readString(record.id);
183
+ const options = Array.isArray(record.options)
184
+ ? record.options.map((option) => asRecord(option)).filter((option): option is Record<string, unknown> => Boolean(option))
185
+ : [];
186
+ if (!questionId || options.length === 0) continue;
187
+
188
+ const labels = options
189
+ .map((option) => readString(option.label))
190
+ .filter((label): label is string => Boolean(label));
191
+ if (labels.length < 2) continue;
192
+
193
+ const approveLabel = labels.find((label) => isApproveLabel(label)) ?? labels[0];
194
+ const denyLabel = labels.find((label) => isDenyLabel(label)) ?? labels[labels.length - 1];
195
+
196
+ if (!approveLabel || !denyLabel || approveLabel === denyLabel) {
197
+ continue;
198
+ }
199
+
200
+ return {
201
+ questionId,
202
+ approveLabel,
203
+ denyLabel,
204
+ };
205
+ }
206
+
207
+ return null;
208
+ }
209
+
210
+ function looksLikeApprovalRequestUserInput(questions: unknown): boolean {
211
+ if (!Array.isArray(questions) || questions.length === 0) return false;
212
+ const labels = questions
213
+ .map((question) => asRecord(question))
214
+ .filter((question): question is Record<string, unknown> => Boolean(question))
215
+ .flatMap((question) => {
216
+ const options = Array.isArray(question.options) ? question.options : [];
217
+ return options
218
+ .map((option) => asRecord(option))
219
+ .filter((option): option is Record<string, unknown> => Boolean(option))
220
+ .map((option) => readString(option.label))
221
+ .filter((label): label is string => Boolean(label));
222
+ });
223
+ const hasApproval = labels.some((label) => isApproveLabel(label));
224
+ const hasDeny = labels.some((label) => isDenyLabel(label));
225
+ return hasApproval && hasDeny;
226
+ }
227
+
228
+ function extractToolContext(item: Record<string, unknown>): CodexToolContext | null {
229
+ const itemType = normalizeItemType(item.type ?? item.itemType);
230
+ if (itemType === 'commandexecution') {
231
+ return {
232
+ toolName: 'Bash',
233
+ input: omitKeys(item, ['id', 'itemId', 'type', 'itemType', 'stdout', 'stderr', 'exitCode', 'exit_code', 'status', 'success', 'error']),
234
+ };
235
+ }
236
+ if (itemType === 'filechange') {
237
+ return {
238
+ toolName: 'Patch',
239
+ input: omitKeys(item, ['id', 'itemId', 'type', 'itemType', 'stdout', 'stderr', 'exitCode', 'exit_code', 'status', 'success', 'error']),
240
+ };
241
+ }
242
+ if (itemType === 'mcptoolcall') {
243
+ const server = readString(item.server);
244
+ const tool = readString(item.tool) ?? readString(item.name);
245
+ if (!tool) return null;
246
+ return {
247
+ toolName: server ? `mcp__${server}__${tool}` : tool,
248
+ input: (asRecord(item.arguments) ?? asRecord(item.input) ?? {}) as Record<string, unknown>,
249
+ };
250
+ }
251
+ return null;
252
+ }
253
+
254
+ function readRequestUserInputDescription(questions: unknown): string {
255
+ if (!Array.isArray(questions)) return '';
256
+ for (const question of questions) {
257
+ const record = asRecord(question);
258
+ if (!record) continue;
259
+ const questionText = readString(record.question) ?? readString(record.header);
260
+ if (questionText) return questionText;
261
+ }
262
+ return '';
263
+ }
264
+
265
+ function resolveCodexPolicy(approvalMode: string | undefined, cwd: string): {
266
+ approvalPolicy: 'on-request' | 'never';
267
+ sandbox: 'workspace-write' | 'danger-full-access';
268
+ sandboxPolicy: Record<string, unknown>;
269
+ } {
270
+ if (approvalMode === 'autoApprove') {
271
+ return {
272
+ approvalPolicy: 'never',
273
+ sandbox: 'danger-full-access',
274
+ sandboxPolicy: { type: 'dangerFullAccess' },
275
+ };
276
+ }
277
+ if (approvalMode === 'plan') {
278
+ return {
279
+ approvalPolicy: 'on-request',
280
+ sandbox: 'workspace-write',
281
+ sandboxPolicy: {
282
+ type: 'workspaceWrite',
283
+ writableRoots: [],
284
+ readOnlyAccess: { type: 'fullAccess' },
285
+ networkAccess: true,
286
+ excludeTmpdirEnvVar: false,
287
+ excludeSlashTmp: false,
288
+ },
289
+ };
290
+ }
291
+ return {
292
+ approvalPolicy: 'on-request',
293
+ sandbox: 'workspace-write',
294
+ sandboxPolicy: {
295
+ type: 'workspaceWrite',
296
+ writableRoots: [cwd],
297
+ readOnlyAccess: { type: 'fullAccess' },
298
+ networkAccess: true,
299
+ excludeTmpdirEnvVar: false,
300
+ excludeSlashTmp: false,
301
+ },
302
+ };
303
+ }
304
+
305
+ export class CodexDriver implements Driver {
306
+ private proc: ChildProcess | null = null;
307
+ private handler: MessageHandler | null = null;
308
+ private exitHandler: ((code: number | null) => void) | null = null;
309
+ private buffer = '';
310
+ private rpcId = 0;
311
+ private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
312
+ private threadId = '';
313
+ private lastStderr: string[] = [];
314
+ private approvalRequests = new Map<string, CodexApprovalRequest>();
315
+ private toolContextByCallId = new Map<string, CodexToolContext>();
316
+ private turnStartInFlight = false;
317
+ private interruptRequestedDuringTurnStart = false;
318
+
319
+ private approvalMode?: string;
320
+ private cwd = '';
321
+
322
+ async start(cwd: string, resumeSessionId?: string, approvalMode?: string): Promise<string> {
323
+ this.approvalMode = approvalMode;
324
+ this.cwd = cwd;
325
+ this.approvalRequests.clear();
326
+ this.toolContextByCallId.clear();
327
+ this.turnStartInFlight = false;
328
+ this.interruptRequestedDuringTurnStart = false;
329
+ const codexPath = config.codexPath;
330
+ let spawnCmd: string;
331
+ let spawnArgs = this.buildSpawnArgs();
332
+
333
+ if (config.isTransientPath(codexPath)) {
334
+ const resolved = config.execViaLoginShell('codex', spawnArgs);
335
+ spawnCmd = resolved.command;
336
+ spawnArgs = resolved.args;
337
+ log.info({ spawnCmd, argsPreview: spawnArgs.slice(0, 2) }, 'spawning via login shell');
338
+ } else {
339
+ spawnCmd = codexPath;
340
+ log.info({ spawnCmd, args: spawnArgs }, 'spawning');
341
+ }
342
+
343
+ audit.emit('driver.spawn', { agent: 'codex', cwd, resumeSessionId });
344
+ this.lastStderr = [];
345
+
346
+ this.proc = spawn(spawnCmd, spawnArgs, {
347
+ cwd: cwd || undefined,
348
+ stdio: ['pipe', 'pipe', 'pipe'],
349
+ env: config.buildSanitizedEnv(),
350
+ ...(process.platform === 'win32' ? { shell: true } : {}),
351
+ });
352
+
353
+ this.proc.on('error', (err) => {
354
+ let message = err.message;
355
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT' && cwd && !existsSync(cwd)) {
356
+ message = `Working directory does not exist: ${cwd}`;
357
+ }
358
+ log.error({ error: message, cwd }, 'spawn error');
359
+ audit.emit('driver.error', { agent: 'codex', error: message });
360
+ });
361
+
362
+ this.proc.stdout!.on('data', (chunk: Buffer) => {
363
+ this.buffer += chunk.toString();
364
+ const lines = this.buffer.split('\n');
365
+ this.buffer = lines.pop()!;
366
+ for (const line of lines) {
367
+ if (!line.trim()) continue;
368
+ try {
369
+ this.handleRaw(JSON.parse(line));
370
+ } catch {
371
+ log.warn({ linePreview: line.slice(0, 200) }, 'failed to parse stdout line');
372
+ }
373
+ }
374
+ });
375
+
376
+ this.proc.stderr!.on('data', (chunk: Buffer) => {
377
+ const text = chunk.toString().trim();
378
+ if (text) {
379
+ log.debug({ stderr: text }, 'stderr');
380
+ this.lastStderr.push(text);
381
+ if (this.lastStderr.length > 10) this.lastStderr.shift();
382
+ }
383
+ });
384
+
385
+ this.proc.on('exit', (code) => {
386
+ log.info({ threadId: this.threadId, exitCode: code }, 'process exited');
387
+ if (code && code !== 0) {
388
+ log.error({ threadId: this.threadId, exitCode: code, lastStderr: this.lastStderr.slice(-3) }, 'abnormal exit');
389
+ }
390
+ this.proc = null;
391
+ this.approvalRequests.clear();
392
+ this.toolContextByCallId.clear();
393
+ this.turnStartInFlight = false;
394
+ this.interruptRequestedDuringTurnStart = false;
395
+ for (const [, { reject }] of this.pending) {
396
+ reject(new Error(`Codex process exited with code ${code}`));
397
+ }
398
+ this.pending.clear();
399
+ this.exitHandler?.(code);
400
+ if (code && this.handler && this.threadId) {
401
+ this.handler({ type: 'error', sessionId: this.threadId, message: `Codex process exited with code ${code}` });
402
+ }
403
+ });
404
+
405
+ const endInit = metrics.startTimer('rpc.duration');
406
+ await this.rpc('initialize', {
407
+ clientInfo: { name: '@vibelet/cli', version: CLI_VERSION },
408
+ capabilities: {
409
+ experimentalApi: true,
410
+ },
411
+ });
412
+ endInit();
413
+ this.rpcNotify('initialized', {});
414
+
415
+ const policy = resolveCodexPolicy(this.approvalMode, cwd);
416
+
417
+ if (resumeSessionId) {
418
+ const res = await this.rpc('thread/resume', {
419
+ threadId: resumeSessionId,
420
+ approvalPolicy: policy.approvalPolicy,
421
+ sandbox: policy.sandbox,
422
+ persistExtendedHistory: true,
423
+ }) as any;
424
+ this.threadId = res?.thread?.id ?? resumeSessionId;
425
+ log.info({ threadId: this.threadId }, 'thread resumed');
426
+ audit.emit('driver.init', { agent: 'codex', sessionId: this.threadId });
427
+ } else {
428
+ const res = await this.rpc('thread/start', {
429
+ cwd,
430
+ approvalPolicy: policy.approvalPolicy,
431
+ sandbox: policy.sandbox,
432
+ experimentalRawEvents: true,
433
+ persistExtendedHistory: true,
434
+ }) as any;
435
+ this.threadId = res?.thread?.id ?? res?.threadId ?? '';
436
+ log.info({ threadId: this.threadId }, 'thread created');
437
+ audit.emit('driver.init', { agent: 'codex', sessionId: this.threadId });
438
+ }
439
+
440
+ return this.threadId;
441
+ }
442
+
443
+ private buildSpawnArgs(): string[] {
444
+ return ['app-server', '--listen', 'stdio://'];
445
+ }
446
+
447
+ sendPrompt(text: string): void {
448
+ const policy = resolveCodexPolicy(this.approvalMode, this.cwd || process.cwd());
449
+ this.turnStartInFlight = true;
450
+ this.interruptRequestedDuringTurnStart = false;
451
+ log.info({ threadId: this.threadId, promptPreview: text.slice(0, 50) }, 'turn/start');
452
+ this.rpc('turn/start', {
453
+ threadId: this.threadId,
454
+ input: [{ type: 'text', text }],
455
+ approvalPolicy: policy.approvalPolicy,
456
+ sandboxPolicy: policy.sandboxPolicy,
457
+ })
458
+ .then((res) => {
459
+ log.debug({ resultPreview: JSON.stringify(res).slice(0, 200) }, 'turn/start result');
460
+ this.turnStartInFlight = false;
461
+ if (this.interruptRequestedDuringTurnStart) {
462
+ this.interruptRequestedDuringTurnStart = false;
463
+ this.requestTurnInterrupt();
464
+ }
465
+ })
466
+ .catch((e) => {
467
+ this.turnStartInFlight = false;
468
+ this.interruptRequestedDuringTurnStart = false;
469
+ log.error({ threadId: this.threadId, error: e.message }, 'sendPrompt error');
470
+ this.handler?.({ type: 'error', sessionId: this.threadId, message: e.message });
471
+ });
472
+ }
473
+
474
+ respondApproval(requestId: string, approved: boolean): boolean {
475
+ if (!this.proc?.stdin?.writable) return false;
476
+ const tracked = this.approvalRequests.get(requestId);
477
+ const rpcId = tracked?.rpcId ?? Number(requestId);
478
+ if (!Number.isFinite(rpcId)) return false;
479
+
480
+ let result: Record<string, unknown>;
481
+ if (!tracked) {
482
+ result = approved
483
+ ? { decision: 'approve' }
484
+ : { decision: 'deny', reason: 'User denied from Vibelet' };
485
+ } else {
486
+ switch (tracked.kind) {
487
+ case 'command-execution':
488
+ case 'file-change':
489
+ result = { decision: approved ? 'accept' : 'decline' };
490
+ break;
491
+ case 'request-user-input-approval':
492
+ result = {
493
+ answers: {
494
+ [tracked.questionId]: {
495
+ answers: [approved ? tracked.approveLabel : tracked.denyLabel],
496
+ },
497
+ },
498
+ };
499
+ break;
500
+ case 'exec-command-legacy':
501
+ case 'apply-patch-legacy':
502
+ result = { decision: approved ? 'approved' : 'denied' };
503
+ break;
504
+ default:
505
+ result = { decision: approved ? 'accept' : 'decline' };
506
+ break;
507
+ }
508
+ this.approvalRequests.delete(requestId);
509
+ }
510
+
511
+ log.info({ threadId: this.threadId, rpcId, approved }, 'approval response');
512
+ this.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: rpcId, result }) + '\n');
513
+ return true;
514
+ }
515
+
516
+ restorePendingApproval(approval: ApprovalRequestPayload): void {
517
+ const request = restoreApprovalRequest(approval);
518
+ if (!request) {
519
+ return;
520
+ }
521
+ this.approvalRequests.set(approval.requestId, request);
522
+ }
523
+
524
+ interrupt(): void {
525
+ if (this.turnStartInFlight) {
526
+ this.interruptRequestedDuringTurnStart = true;
527
+ }
528
+ this.requestTurnInterrupt();
529
+ }
530
+
531
+ setApprovalMode(mode: string): void {
532
+ this.approvalMode = mode;
533
+ log.info({ approvalMode: mode }, 'approval mode updated (takes effect on subsequent turns)');
534
+ }
535
+
536
+ stop(): void {
537
+ if (!this.proc) return;
538
+ this.proc.kill('SIGTERM');
539
+ const p = this.proc;
540
+ setTimeout(() => {
541
+ if (!p.killed) p.kill('SIGKILL');
542
+ }, 5000).unref();
543
+ this.proc = null;
544
+ this.approvalRequests.clear();
545
+ this.toolContextByCallId.clear();
546
+ this.turnStartInFlight = false;
547
+ this.interruptRequestedDuringTurnStart = false;
548
+ for (const [, { reject }] of this.pending) {
549
+ reject(new Error('Process stopped'));
550
+ }
551
+ this.pending.clear();
552
+ }
553
+
554
+ onMessage(handler: MessageHandler): void {
555
+ this.handler = handler;
556
+ }
557
+
558
+ onExit(handler: (code: number | null) => void): void {
559
+ this.exitHandler = handler;
560
+ }
561
+
562
+ private rpc(method: string, params: unknown): Promise<unknown> {
563
+ const id = ++this.rpcId;
564
+ const endTimer = metrics.startTimer('rpc.duration');
565
+ return new Promise((resolve, reject) => {
566
+ this.pending.set(id, {
567
+ resolve: (v) => { endTimer(); resolve(v); },
568
+ reject: (e) => { endTimer(); reject(e); },
569
+ });
570
+ if (!this.proc?.stdin?.writable) {
571
+ this.pending.delete(id);
572
+ endTimer();
573
+ reject(new Error('Process not available'));
574
+ return;
575
+ }
576
+ this.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, id, params }) + '\n');
577
+ setTimeout(() => {
578
+ if (this.pending.has(id)) {
579
+ this.pending.delete(id);
580
+ log.error({ method, rpcId: id, threadId: this.threadId }, 'RPC timeout');
581
+ endTimer();
582
+ reject(new Error(`RPC timeout: ${method}`));
583
+ }
584
+ }, 30000).unref();
585
+ });
586
+ }
587
+
588
+ private requestTurnInterrupt(): void {
589
+ this.rpc('turn/interrupt', { threadId: this.threadId }).catch(() => {});
590
+ }
591
+
592
+ private rpcNotify(method: string, params: unknown): void {
593
+ if (!this.proc?.stdin?.writable) return;
594
+ this.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n');
595
+ }
596
+
597
+ private writeServerRequestError(rpcId: number, message: string): void {
598
+ if (!this.proc?.stdin?.writable) return;
599
+ this.proc.stdin.write(JSON.stringify({
600
+ jsonrpc: '2.0',
601
+ id: rpcId,
602
+ error: { code: -32601, message },
603
+ }) + '\n');
604
+ }
605
+
606
+ private resolveToolContext(params: Record<string, unknown> | undefined): {
607
+ callId: string;
608
+ toolContext: CodexToolContext;
609
+ } | null {
610
+ const item = asRecord(params?.item) ?? params ?? null;
611
+ const callId = readItemId(item);
612
+ if (!callId) return null;
613
+
614
+ const extracted = item ? extractToolContext(item) : null;
615
+ if (extracted) {
616
+ this.toolContextByCallId.set(callId, extracted);
617
+ return { callId, toolContext: extracted };
618
+ }
619
+
620
+ const remembered = this.toolContextByCallId.get(callId);
621
+ if (!remembered) return null;
622
+ return { callId, toolContext: remembered };
623
+ }
624
+
625
+ private createFallbackInput(params: Record<string, unknown> | undefined, excludedKeys: string[]): Record<string, unknown> {
626
+ return params ? omitKeys(params, ['threadId', 'turnId', ...excludedKeys]) : {};
627
+ }
628
+
629
+ private queueApprovalRequest(requestId: string, request: CodexApprovalRequest, description: string): void {
630
+ this.approvalRequests.set(requestId, request);
631
+ audit.emit('approval.request', { agent: 'codex', sessionId: this.threadId, toolName: request.toolName });
632
+
633
+ if (!this.handler) {
634
+ log.warn({ threadId: this.threadId, requestId, toolName: request.toolName }, 'approval request received without handler');
635
+ if (!this.respondApproval(requestId, false)) {
636
+ this.writeServerRequestError(request.rpcId, 'Approval handler unavailable');
637
+ }
638
+ return;
639
+ }
640
+
641
+ this.handler({
642
+ type: 'approval.request',
643
+ sessionId: this.threadId,
644
+ requestId,
645
+ toolName: request.toolName,
646
+ input: request.input,
647
+ description,
648
+ approvalContext: serializeApprovalContext(request),
649
+ });
650
+ }
651
+
652
+ private handleServerRequest(method: string, rpcId: number, params: Record<string, unknown> | undefined): void {
653
+ const requestId = String(rpcId);
654
+ const resolved = this.resolveToolContext(params);
655
+
656
+ if (method === 'item/commandExecution/requestApproval') {
657
+ this.queueApprovalRequest(requestId, {
658
+ kind: 'command-execution',
659
+ responseKind: 'v2',
660
+ rpcId,
661
+ toolName: resolved?.toolContext.toolName ?? 'Bash',
662
+ input: resolved?.toolContext.input ?? this.createFallbackInput(params, ['itemId', 'reason', 'description']),
663
+ }, readString(params?.reason) ?? readString(params?.description) ?? '');
664
+ return;
665
+ }
666
+
667
+ if (method === 'item/fileChange/requestApproval') {
668
+ this.queueApprovalRequest(requestId, {
669
+ kind: 'file-change',
670
+ responseKind: 'v2',
671
+ rpcId,
672
+ toolName: resolved?.toolContext.toolName ?? 'Patch',
673
+ input: resolved?.toolContext.input ?? this.createFallbackInput(params, ['itemId', 'reason', 'description']),
674
+ }, readString(params?.reason) ?? readString(params?.description) ?? '');
675
+ return;
676
+ }
677
+
678
+ if (method === 'item/tool/requestUserInput') {
679
+ const questions = params?.questions;
680
+ if (!looksLikeApprovalRequestUserInput(questions)) {
681
+ log.warn({ threadId: this.threadId, rpcId }, 'unsupported requestUserInput request');
682
+ this.writeServerRequestError(rpcId, 'Unsupported interactive request from Codex');
683
+ return;
684
+ }
685
+
686
+ const question = resolveApprovalQuestion(questions);
687
+ if (!question) {
688
+ this.writeServerRequestError(rpcId, 'Malformed requestUserInput approval request');
689
+ return;
690
+ }
691
+
692
+ this.queueApprovalRequest(requestId, {
693
+ kind: 'request-user-input-approval',
694
+ rpcId,
695
+ questionId: question.questionId,
696
+ approveLabel: question.approveLabel,
697
+ denyLabel: question.denyLabel,
698
+ toolName: resolved?.toolContext.toolName ?? 'Bash',
699
+ input: resolved?.toolContext.input ?? this.createFallbackInput(params, ['itemId', 'questions']),
700
+ }, readRequestUserInputDescription(questions));
701
+ return;
702
+ }
703
+
704
+ if (method === 'execCommandApproval') {
705
+ this.queueApprovalRequest(requestId, {
706
+ kind: 'exec-command-legacy',
707
+ rpcId,
708
+ toolName: resolved?.toolContext.toolName ?? 'Bash',
709
+ input: resolved?.toolContext.input ?? this.createFallbackInput(params, []),
710
+ }, readString(params?.reason) ?? readString(params?.description) ?? '');
711
+ return;
712
+ }
713
+
714
+ if (method === 'applyPatchApproval') {
715
+ this.queueApprovalRequest(requestId, {
716
+ kind: 'apply-patch-legacy',
717
+ rpcId,
718
+ toolName: resolved?.toolContext.toolName ?? 'Patch',
719
+ input: resolved?.toolContext.input ?? this.createFallbackInput(params, []),
720
+ }, readString(params?.reason) ?? readString(params?.description) ?? '');
721
+ return;
722
+ }
723
+
724
+ if (method === 'item/permissions/requestApproval') {
725
+ log.warn({ threadId: this.threadId, rpcId }, 'unsupported permissions approval request');
726
+ this.writeServerRequestError(rpcId, 'Unsupported permissions approval request');
727
+ return;
728
+ }
729
+
730
+ log.warn({ threadId: this.threadId, rpcId, method }, 'unsupported server request');
731
+ this.writeServerRequestError(rpcId, `Unsupported server request: ${method}`);
732
+ }
733
+
734
+ private handleRaw(msg: Record<string, unknown>): void {
735
+ log.debug({ method: msg.method ?? '(response)', rpcId: msg.id ?? '-' }, 'handleRaw');
736
+
737
+ if (msg.id != null && !msg.method && this.pending.has(msg.id as number)) {
738
+ const p = this.pending.get(msg.id as number)!;
739
+ this.pending.delete(msg.id as number);
740
+ if (msg.error) {
741
+ p.reject(new Error((msg.error as any).message ?? 'RPC error'));
742
+ } else {
743
+ p.resolve(msg.result);
744
+ }
745
+ return;
746
+ }
747
+
748
+ const method = typeof msg.method === 'string' ? msg.method : undefined;
749
+ const params = asRecord(msg.params);
750
+
751
+ if (method && msg.id != null) {
752
+ this.handleServerRequest(method, msg.id as number, params ?? undefined);
753
+ return;
754
+ }
755
+
756
+ if (method) {
757
+ log.debug({ method, paramsPreview: JSON.stringify(params ?? {}).slice(0, 300) }, 'notification');
758
+ }
759
+
760
+ switch (method) {
761
+ case 'item/agentMessage/delta': {
762
+ const deltaText = (params?.text ?? params?.delta ?? params?.content ?? '') as string;
763
+ if (deltaText && this.handler) {
764
+ this.handler({
765
+ type: 'text.delta',
766
+ sessionId: this.threadId,
767
+ content: deltaText,
768
+ });
769
+ } else if (!deltaText) {
770
+ log.debug({ paramsKeys: Object.keys(params ?? {}) }, 'agentMessage/delta with empty text');
771
+ }
772
+ break;
773
+ }
774
+
775
+ case 'item/commandExecution/outputDelta':
776
+ if (params?.output && this.handler) {
777
+ this.handler({
778
+ type: 'tool.result',
779
+ sessionId: this.threadId,
780
+ toolCallId: (params.itemId as string) ?? '',
781
+ output: String(params.output),
782
+ });
783
+ }
784
+ break;
785
+
786
+ case 'item/started': {
787
+ const item = asRecord(params?.item);
788
+ const callId = readItemId(item);
789
+ const toolContext = item ? extractToolContext(item) : null;
790
+ if (callId && toolContext) {
791
+ this.toolContextByCallId.set(callId, toolContext);
792
+ if (this.handler) {
793
+ this.handler({
794
+ type: 'tool.call',
795
+ sessionId: this.threadId,
796
+ toolName: toolContext.toolName,
797
+ input: toolContext.input,
798
+ toolCallId: callId,
799
+ });
800
+ }
801
+ }
802
+ break;
803
+ }
804
+
805
+ case 'item/completed': {
806
+ const item = asRecord(params?.item) ?? params;
807
+ const callId = readItemId(item);
808
+ if (callId) this.toolContextByCallId.delete(callId);
809
+ break;
810
+ }
811
+
812
+ case 'turn/aborted': {
813
+ this.turnStartInFlight = false;
814
+ this.interruptRequestedDuringTurnStart = false;
815
+ const reason = readString(params?.reason) ?? readString(asRecord(params?.turn)?.reason);
816
+ log.info({ threadId: this.threadId, reason }, 'turn interrupted');
817
+ audit.emit('session.interrupted', {
818
+ agent: 'codex',
819
+ sessionId: this.threadId,
820
+ ...(reason ? { reason } : {}),
821
+ });
822
+ if (this.handler) {
823
+ this.handler({
824
+ type: 'session.interrupted',
825
+ sessionId: this.threadId,
826
+ });
827
+ }
828
+ break;
829
+ }
830
+
831
+ case 'turn/completed': {
832
+ this.turnStartInFlight = false;
833
+ this.interruptRequestedDuringTurnStart = false;
834
+ const turn = asRecord(params?.turn);
835
+ const turnStatus = normalizeTurnStatus(turn?.status);
836
+ const rawUsage = asRecord(params?.usage);
837
+ const usage = rawUsage
838
+ ? {
839
+ inputTokens: Number(rawUsage.input_tokens ?? rawUsage.inputTokens ?? 0),
840
+ outputTokens: Number(rawUsage.output_tokens ?? rawUsage.outputTokens ?? 0),
841
+ }
842
+ : undefined;
843
+ if (turnStatus && INTERRUPTED_TURN_STATUSES.has(turnStatus)) {
844
+ log.info({ threadId: this.threadId, status: turnStatus }, 'turn interrupted');
845
+ audit.emit('session.interrupted', {
846
+ agent: 'codex',
847
+ sessionId: this.threadId,
848
+ status: turnStatus,
849
+ ...(usage ? { usage } : {}),
850
+ });
851
+ if (this.handler) {
852
+ this.handler({
853
+ type: 'session.interrupted',
854
+ sessionId: this.threadId,
855
+ });
856
+ }
857
+ break;
858
+ }
859
+
860
+ log.info({ threadId: this.threadId, status: turnStatus ?? 'completed' }, 'turn completed');
861
+ audit.emit('session.done', { agent: 'codex', sessionId: this.threadId, usage });
862
+ if (this.handler) {
863
+ this.handler({
864
+ type: 'session.done',
865
+ sessionId: this.threadId,
866
+ usage,
867
+ });
868
+ }
869
+ break;
870
+ }
871
+
872
+ default:
873
+ if (method) {
874
+ log.debug({ method }, 'unhandled notification');
875
+ }
876
+ break;
877
+ }
878
+ }
879
+ }