sootsim 0.1.83 → 0.1.85

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 (209) hide show
  1. package/README.md +0 -1
  2. package/detox/colors.ts +54 -0
  3. package/detox/config-loader.ts +135 -0
  4. package/detox/element-types.ts +36 -0
  5. package/detox/expectations.ts +477 -0
  6. package/detox/gestures.ts +442 -0
  7. package/detox/index.ts +1403 -0
  8. package/detox/jest-environment.ts +86 -0
  9. package/detox/jest-preset.cjs +50 -0
  10. package/detox/matchers.ts +29 -0
  11. package/detox/navigation.ts +43 -0
  12. package/detox/run-test.ts +113 -0
  13. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  14. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  15. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  16. package/detox/screenshots/theme-blue-toggle.png +0 -0
  17. package/detox/screenshots/theme-blue.png +0 -0
  18. package/detox/screenshots/theme-red-toggle.png +0 -0
  19. package/detox/screenshots/theme-red.png +0 -0
  20. package/dist-cli/bin.js +3 -3
  21. package/dist-cli/chunks/{agent-MQ7GLVIB.js → agent-T3DUH5YJ.js} +2 -2
  22. package/dist-cli/chunks/{agent-wrapper-7KAFDQCN.js → agent-wrapper-NSBF4THI.js} +2 -2
  23. package/dist-cli/chunks/{assert-TV46GUNU.js → assert-X3F7TRCZ.js} +2 -2
  24. package/dist-cli/chunks/auto-bootstrap-47RN2V5G.js +2 -0
  25. package/dist-cli/chunks/beta-BRCGAF2N.js +2 -0
  26. package/dist-cli/chunks/chunk-36RPD6JI.js +2 -0
  27. package/dist-cli/chunks/{chunk-PM5NVKLP.js → chunk-3WGHC7JN.js} +2 -2
  28. package/dist-cli/chunks/chunk-4DBPNLGI.js +1 -0
  29. package/dist-cli/chunks/{chunk-J2GYISVJ.js → chunk-4EVSIUNB.js} +2 -2
  30. package/dist-cli/chunks/{chunk-JHJNODXN.js → chunk-4QZHZ6BC.js} +2 -2
  31. package/dist-cli/chunks/{chunk-F3HP444U.js → chunk-5DIGWOY7.js} +1 -1
  32. package/dist-cli/chunks/{chunk-DP7O5MHK.js → chunk-5N3V7OCG.js} +2 -2
  33. package/dist-cli/chunks/{chunk-Y4BUVURT.js → chunk-5S6D7K4L.js} +2 -2
  34. package/dist-cli/chunks/{chunk-ECJBV65H.js → chunk-7LKUN46F.js} +2 -2
  35. package/dist-cli/chunks/{chunk-WTKTOL3C.js → chunk-AC6QGW22.js} +2 -2
  36. package/dist-cli/chunks/{chunk-IBNRRAES.js → chunk-AFNDVS4E.js} +2 -2
  37. package/dist-cli/chunks/{chunk-6TNANCQC.js → chunk-BESAZ2HA.js} +2 -2
  38. package/dist-cli/chunks/{chunk-WN7M3QON.js → chunk-BHZJ6RIH.js} +2 -2
  39. package/dist-cli/chunks/{chunk-277XAALA.js → chunk-BZL6D4TV.js} +3 -3
  40. package/dist-cli/chunks/{chunk-CYV6Y6YV.js → chunk-CF2LPRXD.js} +2 -2
  41. package/dist-cli/chunks/chunk-DWTLRPEN.js +79 -0
  42. package/dist-cli/chunks/{chunk-CJY3AVI7.js → chunk-E2QE5FFP.js} +1 -1
  43. package/dist-cli/chunks/chunk-EBEL6TTJ.js +4 -0
  44. package/dist-cli/chunks/{chunk-DM6WT7QM.js → chunk-EFM53PZ5.js} +1 -1
  45. package/dist-cli/chunks/{chunk-YUELRHGB.js → chunk-EKXK3SWK.js} +2 -2
  46. package/dist-cli/chunks/{chunk-4LS5MZAI.js → chunk-G7CIZ5S3.js} +3 -3
  47. package/dist-cli/chunks/{chunk-6NN2D4EJ.js → chunk-GTAD6IUV.js} +1 -1
  48. package/dist-cli/chunks/{chunk-OYMFNU3M.js → chunk-H44IQHKZ.js} +1 -1
  49. package/dist-cli/chunks/{chunk-IP3QJLRH.js → chunk-HQDJ5BOF.js} +1 -1
  50. package/dist-cli/chunks/{chunk-5DJXZIFZ.js → chunk-KUSQ4NNJ.js} +1 -1
  51. package/dist-cli/chunks/{chunk-HAWOAQAG.js → chunk-MAO7F5PH.js} +3 -3
  52. package/dist-cli/chunks/{chunk-572VSFNP.js → chunk-NVTL3JQG.js} +1 -1
  53. package/dist-cli/chunks/{chunk-6XZOEBTZ.js → chunk-O6N2CEET.js} +2 -2
  54. package/dist-cli/chunks/{chunk-HNWEELAE.js → chunk-OISHLFON.js} +1 -1
  55. package/dist-cli/chunks/{chunk-2PY3UZVO.js → chunk-OUNLJM56.js} +2 -2
  56. package/dist-cli/chunks/chunk-OXOARRKR.js +67 -0
  57. package/dist-cli/chunks/{chunk-NXATOWWF.js → chunk-PHPXGLME.js} +1 -1
  58. package/dist-cli/chunks/{chunk-JQ7ZXOXJ.js → chunk-PQFFUJR6.js} +2 -2
  59. package/dist-cli/chunks/{chunk-KASUZ5XV.js → chunk-QLJNSOS7.js} +1 -1
  60. package/dist-cli/chunks/chunk-QQAECG5B.js +2 -0
  61. package/dist-cli/chunks/{chunk-FJYT7XL2.js → chunk-RZHREO3M.js} +2 -2
  62. package/dist-cli/chunks/{chunk-FRM355UL.js → chunk-SBGOUA6F.js} +2 -2
  63. package/dist-cli/chunks/chunk-SSCA2AEA.js +1 -0
  64. package/dist-cli/chunks/{chunk-Y2VJBRSP.js → chunk-UYRGCJ4N.js} +1 -1
  65. package/dist-cli/chunks/{chunk-2AWQ7OB2.js → chunk-WGDL5V6C.js} +1 -1
  66. package/dist-cli/chunks/{chunk-VMXWC2JO.js → chunk-Y5PLPEEU.js} +2 -2
  67. package/dist-cli/chunks/chunk-ZFAM4N5B.js +1 -0
  68. package/dist-cli/chunks/{chunk-RH4F2TF7.js → chunk-ZO3VHP6W.js} +1 -1
  69. package/dist-cli/chunks/cli-version-WPFDM2A6.js +2 -0
  70. package/dist-cli/chunks/{compat-QLLWBTS3.js → compat-PCXGGZBZ.js} +3 -3
  71. package/dist-cli/chunks/{config-2DSLDCXV.js → config-LULEVEYL.js} +2 -2
  72. package/dist-cli/chunks/control-6P6HY7UF.js +2 -0
  73. package/dist-cli/chunks/{cpu-profile-GEIKHCPC.js → cpu-profile-NOK73ZYW.js} +2 -2
  74. package/dist-cli/chunks/{daemon-4EBUFN4D.js → daemon-4A3DMUYL.js} +2 -2
  75. package/dist-cli/chunks/{debug-WGD6XWOF.js → debug-74BWB2ZG.js} +3 -3
  76. package/dist-cli/chunks/{detox-LNKGRZU6.js → detox-HEOMINSC.js} +2 -2
  77. package/dist-cli/chunks/{device-AYKXKVIQ.js → device-TTXXBJFZ.js} +2 -2
  78. package/dist-cli/chunks/{diagnose-TMXSDOOC.js → diagnose-QZ3GOHSE.js} +2 -2
  79. package/dist-cli/chunks/drivers-QRPWNOIT.js +2 -0
  80. package/dist-cli/chunks/{electron-QFPF7TBY.js → electron-QVOWV44R.js} +3 -3
  81. package/dist-cli/chunks/flow-QMA7GVN6.js +2 -0
  82. package/dist-cli/chunks/{hints-MXKRR4TG.js → hints-YKWRNMJC.js} +2 -2
  83. package/dist-cli/chunks/{home-paths-REMWQDAO.js → home-paths-SFADSTJM.js} +2 -2
  84. package/dist-cli/chunks/{inspect-XGSQNFV7.js → inspect-LEWGQCIU.js} +3 -3
  85. package/dist-cli/chunks/install-7N2N7Q32.js +2 -0
  86. package/dist-cli/chunks/{install-desktop-NQG3RZSA.js → install-desktop-22HYQZ2G.js} +3 -3
  87. package/dist-cli/chunks/{keys-5QZWXL3F.js → keys-3ZT3MICU.js} +2 -2
  88. package/dist-cli/chunks/{launch-SBXOZWKO.js → launch-ZXW2NFLG.js} +3 -3
  89. package/dist-cli/chunks/{login-EACQXE24.js → login-NJKJ7GZO.js} +4 -4
  90. package/dist-cli/chunks/{logout-IBQLMUML.js → logout-VMMQL7CB.js} +2 -2
  91. package/dist-cli/chunks/{maestro-LFYXUX7O.js → maestro-OJY4MTI7.js} +2 -2
  92. package/dist-cli/chunks/{preview-U4SBOEGQ.js → preview-QU2GXTEV.js} +2 -2
  93. package/dist-cli/chunks/{profile-GWS5ECMY.js → profile-7APWK47T.js} +2 -2
  94. package/dist-cli/chunks/{react-QDHLMVYL.js → react-RSVO5JZZ.js} +2 -2
  95. package/dist-cli/chunks/{record-BUEUWPDI.js → record-UWH4MDEO.js} +2 -2
  96. package/dist-cli/chunks/runtime-3FUENRHM.js +2 -0
  97. package/dist-cli/chunks/{runtime-delivery-G7L6RVZ7.js → runtime-delivery-QMKGRV7N.js} +2 -2
  98. package/dist-cli/chunks/{screenshot-T2HBA3VI.js → screenshot-43M27ALE.js} +2 -2
  99. package/dist-cli/chunks/{screenshot-mode-EG5HMIH3.js → screenshot-mode-EBYYN6TY.js} +2 -2
  100. package/dist-cli/chunks/{screenshots-S52AFHTV.js → screenshots-7TQZL6Z6.js} +2 -2
  101. package/dist-cli/chunks/{server-MFFVYUGG.js → server-VCFM25Z6.js} +2 -2
  102. package/dist-cli/chunks/setup-repo-HFH4VKJQ.js +2 -0
  103. package/dist-cli/chunks/{skills-HQGWBS2O.js → skills-RQA6EJQL.js} +2 -2
  104. package/dist-cli/chunks/{start-E3DRYY7W.js → start-ZT6MBYND.js} +4 -4
  105. package/dist-cli/chunks/store-BJBTDSZE.js +2 -0
  106. package/dist-cli/chunks/telemetry-ZZZKTILZ.js +2 -0
  107. package/dist-cli/chunks/{test-ZY3EF62K.js → test-RNRX5SWV.js} +3 -3
  108. package/dist-cli/chunks/{three-mode-WSPKQCJ5.js → three-mode-TQZH25ZO.js} +2 -2
  109. package/dist-cli/chunks/{timeline-3XAB5EWZ.js → timeline-GGN3AY6P.js} +2 -2
  110. package/dist-cli/chunks/{upgrade-WNENPFM5.js → upgrade-XT22D67C.js} +2 -2
  111. package/dist-cli/chunks/upload-NC2AYLC5.js +2 -0
  112. package/dist-cli/chunks/{web-D2AOZY44.js → web-KEHVF5MB.js} +2 -2
  113. package/dist-cli/chunks/{what-happened-F43KNSG6.js → what-happened-PATQRJ5T.js} +2 -2
  114. package/dist-cli/chunks/{whoami-T22VBR7C.js → whoami-CXVY26VV.js} +2 -2
  115. package/dist-lib/agent-daemon-client.cjs +1 -1
  116. package/dist-lib/agent-events.cjs +1 -1
  117. package/dist-lib/agent-sessions.cjs +1 -1
  118. package/dist-lib/attached-projects.cjs +1 -1
  119. package/dist-lib/auth/shared-session.cjs +1 -1
  120. package/dist-lib/backend-origin.cjs +1 -1
  121. package/dist-lib/beta.cjs +44 -0
  122. package/dist-lib/bridge-constants.cjs +1 -1
  123. package/dist-lib/cli-constants.cjs +1 -1
  124. package/dist-lib/config.cjs +1 -1
  125. package/dist-lib/detox/index.cjs +1770 -0
  126. package/dist-lib/detox/jest-preset.cjs +50 -0
  127. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  128. package/dist-lib/home-paths.cjs +1 -1
  129. package/dist-lib/host/bridge-host.cjs +1 -1
  130. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  131. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  132. package/dist-lib/index.cjs +136 -138
  133. package/dist-lib/metro.cjs +31 -26
  134. package/dist-lib/profiles.cjs +1 -1
  135. package/dist-lib/render-mode.cjs +1 -1
  136. package/dist-lib/scripts/demo-app-registry.cjs +809 -0
  137. package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
  138. package/dist-lib/skills.cjs +17766 -0
  139. package/dist-lib/vite.cjs +129 -39
  140. package/package.json +39 -14
  141. package/scripts/demo-app-registry.ts +989 -0
  142. package/scripts/dev-server-scanner.ts +674 -0
  143. package/src/agent-daemon-client.ts +390 -0
  144. package/src/agent-events.ts +71 -0
  145. package/src/agent-prompt.ts +71 -0
  146. package/src/agent-sessions.ts +572 -0
  147. package/src/attached-projects.ts +536 -0
  148. package/src/auth/shared-session.ts +199 -0
  149. package/src/backend-origin.ts +49 -0
  150. package/src/beta.ts +21 -0
  151. package/src/bridge-constants.ts +10 -0
  152. package/src/cli-constants.ts +1 -0
  153. package/src/cli-version.ts +30 -0
  154. package/src/codex-client.ts +215 -0
  155. package/src/config.ts +110 -0
  156. package/src/dev-bundle-resolution.ts +180 -0
  157. package/src/home-paths.ts +382 -0
  158. package/src/host/agent-host.ts +576 -0
  159. package/src/host/bridge-host.ts +2293 -0
  160. package/src/host/fetch-proxy-handler.ts +288 -0
  161. package/src/host/fetch-proxy-overrides.ts +39 -0
  162. package/src/host/open-url.ts +234 -0
  163. package/src/index.ts +9 -0
  164. package/src/metro-plugin.ts +139 -0
  165. package/src/native-dev-bundle-url.ts +62 -0
  166. package/src/native-seam-manifest.ts +313 -0
  167. package/src/profiles.ts +179 -0
  168. package/src/render-mode.ts +27 -0
  169. package/src/runtime-assets.ts +84 -0
  170. package/src/runtime-delivery.ts +334 -0
  171. package/src/screenshots/compose.ts +422 -0
  172. package/src/screenshots/frame-compose.ts +438 -0
  173. package/src/screenshots/orchestrate.ts +244 -0
  174. package/src/screenshots/registry.ts +58 -0
  175. package/src/screenshots/schema.ts +364 -0
  176. package/src/skills/builtin/a11y-review.ts +126 -0
  177. package/src/skills/builtin/compat-check.ts +104 -0
  178. package/src/skills/builtin/perf-profile.ts +84 -0
  179. package/src/skills/builtin/screenshot-all.ts +46 -0
  180. package/src/skills/builtin/test-flow.ts +118 -0
  181. package/src/skills/builtin/visual-diff.ts +94 -0
  182. package/src/skills/registry.ts +107 -0
  183. package/src/skills/types.ts +41 -0
  184. package/src/vite-plugin-one.ts +189 -0
  185. package/src/vite-plugin.ts +1381 -0
  186. package/src/worklets-babel.ts +132 -0
  187. package/dist-cli/chunks/auto-bootstrap-FQS4ZD2K.js +0 -2
  188. package/dist-cli/chunks/beta-VG7CDY2U.js +0 -2
  189. package/dist-cli/chunks/chunk-2OIBDYHW.js +0 -1
  190. package/dist-cli/chunks/chunk-6BNLVMXA.js +0 -1
  191. package/dist-cli/chunks/chunk-6XD6CBJM.js +0 -2
  192. package/dist-cli/chunks/chunk-CHQTO426.js +0 -1
  193. package/dist-cli/chunks/chunk-FAPYGVIU.js +0 -4
  194. package/dist-cli/chunks/chunk-PEHFE3LG.js +0 -64
  195. package/dist-cli/chunks/chunk-RXH2SLKF.js +0 -2
  196. package/dist-cli/chunks/chunk-UXQWC5ZR.js +0 -79
  197. package/dist-cli/chunks/chunk-XFQL74PF.js +0 -5
  198. package/dist-cli/chunks/cli-version-PWF6I6LY.js +0 -2
  199. package/dist-cli/chunks/control-UIOXGYXU.js +0 -2
  200. package/dist-cli/chunks/demo-app-registry-G3BDOFWC.js +0 -2
  201. package/dist-cli/chunks/drivers-IDQF34HP.js +0 -2
  202. package/dist-cli/chunks/flow-3JN3Y7RF.js +0 -2
  203. package/dist-cli/chunks/install-2N3YOOSN.js +0 -2
  204. package/dist-cli/chunks/runtime-PVB4VGUH.js +0 -2
  205. package/dist-cli/chunks/setup-repo-YOF7NV5D.js +0 -2
  206. package/dist-cli/chunks/store-MAI6D3UO.js +0 -2
  207. package/dist-cli/chunks/telemetry-RCQKCJTH.js +0 -2
  208. package/dist-cli/chunks/upload-YLJ4RA73.js +0 -2
  209. package/dist-lib/vite-base.cjs +0 -6937
@@ -0,0 +1,390 @@
1
+ // node-side client for the daemon's agent:* ws protocol.
2
+ //
3
+ // used by electron main (translates IPC → daemon) and by the CLI
4
+ // `sootsim agent …` subcommands. the browser shell has its own shim that
5
+ // uses window.WebSocket; this file is node-only.
6
+ //
7
+ // single long-lived connection per client. if the daemon goes away, the
8
+ // client surfaces the disconnect to callers via pending rejections and
9
+ // `onDisconnect`, and the caller can rebuild. auto-reconnect is not built
10
+ // in — the two callers (electron main, CLI) each have their own
11
+ // lifecycle and decide when to reconnect.
12
+
13
+ import { spawn } from 'node:child_process'
14
+ import net from 'node:net'
15
+ import { WebSocket } from 'ws'
16
+ import { resolveSootsimInvocation, type Provider } from './agent-sessions.ts'
17
+ import { DEFAULT_SOOTSIM_BRIDGE_PORT } from './bridge-constants.ts'
18
+ import type { AgentEvent } from './agent-events.ts'
19
+ import type { AgentPromptEnvelope } from './agent-prompt.ts'
20
+ import type { AgentSession, AttachedProject } from './attached-projects.ts'
21
+
22
+ export interface AgentDaemonClientOptions {
23
+ port?: number
24
+ commandTimeoutMs?: number
25
+ /** label forwarded in logs; helps identify the daemon caller. */
26
+ clientLabel?: string
27
+ }
28
+
29
+ export interface AgentDaemonPaths {
30
+ userDataDir: string
31
+ storeFile: string
32
+ sessionsDir: string
33
+ transcriptsDir: string
34
+ }
35
+
36
+ export interface AgentStartSessionInput {
37
+ projectId: string
38
+ provider?: Provider
39
+ codexBin?: string
40
+ claudeBin?: string
41
+ freshThread?: boolean
42
+ }
43
+
44
+ export interface AgentStartSessionResult {
45
+ session: AgentSession
46
+ wrapperPid: number
47
+ }
48
+
49
+ export interface AgentUpsertProjectInput {
50
+ cwd: string
51
+ name?: string
52
+ preferredProvider?: Provider
53
+ sourceRoots?: string[]
54
+ knownBundleUrls?: string[]
55
+ framework?: 'expo' | 'one' | 'rock' | 'unknown'
56
+ bundleId?: string
57
+ }
58
+
59
+ export interface AgentAutoAttachInput {
60
+ bundleUrl: string
61
+ provider?: Provider
62
+ }
63
+
64
+ export class AgentDaemonError extends Error {
65
+ code?: string
66
+ constructor(message: string, code?: string) {
67
+ super(message)
68
+ this.name = 'AgentDaemonError'
69
+ this.code = code
70
+ }
71
+ }
72
+
73
+ type PendingEntry = {
74
+ resolve: (value: unknown) => void
75
+ reject: (err: Error) => void
76
+ timer: ReturnType<typeof setTimeout>
77
+ }
78
+
79
+ type EventCallback = (payload: { sessionId: string; event: AgentEvent }) => void
80
+ type StatusCallback = (session: AgentSession) => void
81
+
82
+ export class AgentDaemonClient {
83
+ private ws: WebSocket
84
+ private port: number
85
+ private commandTimeoutMs: number
86
+ private ready: Promise<void>
87
+ private closed = false
88
+ private nextId = 1
89
+ private pending = new Map<number, PendingEntry>()
90
+ private eventListeners = new Set<EventCallback>()
91
+ private statusListeners = new Set<StatusCallback>()
92
+ private disconnectListeners = new Set<() => void>()
93
+
94
+ constructor(opts: AgentDaemonClientOptions = {}) {
95
+ this.port = opts.port ?? DEFAULT_SOOTSIM_BRIDGE_PORT
96
+ this.commandTimeoutMs = opts.commandTimeoutMs ?? 15_000
97
+ this.ws = new WebSocket(`ws://localhost:${this.port}`)
98
+ this.ready = new Promise<void>((resolve, reject) => {
99
+ const onOpen = () => {
100
+ this.ws.off('error', onError)
101
+ resolve()
102
+ }
103
+ const onError = (err: Error) => {
104
+ this.ws.off('open', onOpen)
105
+ reject(
106
+ new AgentDaemonError(
107
+ `could not connect to sootsim daemon on port ${this.port}: ${err.message}`,
108
+ 'NO_DAEMON',
109
+ ),
110
+ )
111
+ }
112
+ this.ws.once('open', onOpen)
113
+ this.ws.once('error', onError)
114
+ })
115
+ this.ws.on('message', (data) => this.handleMessage(data))
116
+ this.ws.on('close', () => this.handleClose())
117
+ // ws emits 'error' for late socket faults (peer dropped, write EIO after
118
+ // sleep/wake, etc). without a listener, node's EventEmitter rethrows as
119
+ // uncaughtException. 'close' fires right after and runs the cleanup.
120
+ this.ws.on('error', () => {})
121
+ }
122
+
123
+ async waitReady(): Promise<void> {
124
+ return this.ready
125
+ }
126
+
127
+ // --- public api ---
128
+
129
+ async listProjects(): Promise<AttachedProject[]> {
130
+ return this.send<AttachedProject[]>('agent:list-projects')
131
+ }
132
+
133
+ async upsertProject(input: AgentUpsertProjectInput): Promise<AttachedProject> {
134
+ return this.send<AttachedProject>('agent:upsert-project', { input })
135
+ }
136
+
137
+ async deleteProject(projectId: string): Promise<{ ok: true }> {
138
+ return this.send<{ ok: true }>('agent:delete-project', { projectId })
139
+ }
140
+
141
+ async autoAttachForUrl(
142
+ input: AgentAutoAttachInput,
143
+ ): Promise<{ project: AttachedProject | null }> {
144
+ return this.send<{ project: AttachedProject | null }>('agent:auto-attach-for-url', {
145
+ input,
146
+ })
147
+ }
148
+
149
+ async listSessions(projectId?: string): Promise<AgentSession[]> {
150
+ return this.send<AgentSession[]>('agent:list-sessions', { projectId })
151
+ }
152
+
153
+ async startSession(input: AgentStartSessionInput): Promise<AgentStartSessionResult> {
154
+ return this.send<AgentStartSessionResult>('agent:start-session', { input })
155
+ }
156
+
157
+ async sendPrompt(
158
+ sessionId: string,
159
+ prompt: AgentPromptEnvelope,
160
+ ): Promise<{ ok: true }> {
161
+ return this.send<{ ok: true }>('agent:send-prompt', {
162
+ sessionId,
163
+ prompt,
164
+ })
165
+ }
166
+
167
+ async endSession(sessionId: string): Promise<{ ok: true }> {
168
+ return this.send<{ ok: true }>('agent:end-session', { sessionId })
169
+ }
170
+
171
+ async getTranscript(
172
+ sessionId: string,
173
+ ): Promise<string | { error: string; code: string }> {
174
+ return this.send<string | { error: string; code: string }>('agent:get-transcript', {
175
+ sessionId,
176
+ })
177
+ }
178
+
179
+ async getPaths(): Promise<AgentDaemonPaths> {
180
+ return this.send<AgentDaemonPaths>('agent:get-paths')
181
+ }
182
+
183
+ async subscribeEvents(sessionId: string): Promise<{ ok: true; refCount: number }> {
184
+ return this.send<{ ok: true; refCount: number }>('agent:subscribe-events', {
185
+ sessionId,
186
+ })
187
+ }
188
+
189
+ async unsubscribeEvents(sessionId: string): Promise<{ ok: true; refCount: number }> {
190
+ return this.send<{ ok: true; refCount: number }>('agent:unsubscribe-events', {
191
+ sessionId,
192
+ })
193
+ }
194
+
195
+ onAgentEvent(cb: EventCallback): () => void {
196
+ this.eventListeners.add(cb)
197
+ return () => this.eventListeners.delete(cb)
198
+ }
199
+
200
+ onSessionStatusChange(cb: StatusCallback): () => void {
201
+ this.statusListeners.add(cb)
202
+ return () => this.statusListeners.delete(cb)
203
+ }
204
+
205
+ onDisconnect(cb: () => void): () => void {
206
+ this.disconnectListeners.add(cb)
207
+ return () => this.disconnectListeners.delete(cb)
208
+ }
209
+
210
+ close(): void {
211
+ if (this.closed) return
212
+ this.closed = true
213
+ try {
214
+ this.ws.close()
215
+ } catch {}
216
+ }
217
+
218
+ // --- internals ---
219
+
220
+ private async send<T>(type: string, payload: Record<string, unknown> = {}): Promise<T> {
221
+ await this.ready
222
+ if (this.closed || this.ws.readyState !== WebSocket.OPEN) {
223
+ throw new AgentDaemonError('daemon connection is closed', 'NO_DAEMON')
224
+ }
225
+ const id = this.nextId++
226
+ return new Promise<T>((resolve, reject) => {
227
+ const timer = setTimeout(() => {
228
+ this.pending.delete(id)
229
+ reject(
230
+ new AgentDaemonError(
231
+ `${type} timed out after ${Math.round(this.commandTimeoutMs / 1000)}s`,
232
+ 'TIMEOUT',
233
+ ),
234
+ )
235
+ }, this.commandTimeoutMs)
236
+ this.pending.set(id, {
237
+ resolve: resolve as (value: unknown) => void,
238
+ reject,
239
+ timer,
240
+ })
241
+ try {
242
+ this.ws.send(JSON.stringify({ id, type, ...payload }))
243
+ } catch (err) {
244
+ clearTimeout(timer)
245
+ this.pending.delete(id)
246
+ reject(err instanceof Error ? err : new Error(String(err)))
247
+ }
248
+ })
249
+ }
250
+
251
+ private handleMessage(data: unknown): void {
252
+ let msg: any
253
+ try {
254
+ msg = JSON.parse(String(data))
255
+ } catch {
256
+ return
257
+ }
258
+ if (!msg || typeof msg !== 'object') return
259
+ if (msg.type === 'agent:event') {
260
+ for (const cb of this.eventListeners) {
261
+ try {
262
+ cb({ sessionId: msg.sessionId, event: msg.event })
263
+ } catch {}
264
+ }
265
+ return
266
+ }
267
+ if (msg.type === 'agent:session-status') {
268
+ for (const cb of this.statusListeners) {
269
+ try {
270
+ cb(msg.session)
271
+ } catch {}
272
+ }
273
+ return
274
+ }
275
+ // non-agent server pushes (bridge:welcome, bridge:client-state, …) are
276
+ // harmless — we just ignore them. we only opened this socket for the
277
+ // agent protocol.
278
+ if (typeof msg.id !== 'number') return
279
+ const entry = this.pending.get(msg.id)
280
+ if (!entry) return
281
+ this.pending.delete(msg.id)
282
+ clearTimeout(entry.timer)
283
+ if (msg.error) {
284
+ entry.reject(new AgentDaemonError(msg.error, msg.code))
285
+ } else {
286
+ entry.resolve(msg.result)
287
+ }
288
+ }
289
+
290
+ private handleClose(): void {
291
+ if (this.closed) return
292
+ this.closed = true
293
+ for (const [, entry] of this.pending) {
294
+ clearTimeout(entry.timer)
295
+ entry.reject(new AgentDaemonError('daemon disconnected', 'DISCONNECT'))
296
+ }
297
+ this.pending.clear()
298
+ for (const cb of this.disconnectListeners) {
299
+ try {
300
+ cb()
301
+ } catch {}
302
+ }
303
+ }
304
+ }
305
+
306
+ // --- daemon lifecycle helpers ---
307
+
308
+ /** raw TCP probe — matches the shape electron main.ts already uses. */
309
+ export function isBridgeUp(
310
+ port: number = DEFAULT_SOOTSIM_BRIDGE_PORT,
311
+ timeoutMs = 400,
312
+ ): Promise<boolean> {
313
+ return new Promise((resolve) => {
314
+ const socket = new net.Socket()
315
+ let settled = false
316
+ const done = (ok: boolean) => {
317
+ if (settled) return
318
+ settled = true
319
+ socket.destroy()
320
+ resolve(ok)
321
+ }
322
+ socket.setTimeout(timeoutMs)
323
+ socket.once('connect', () => done(true))
324
+ socket.once('timeout', () => done(false))
325
+ socket.once('error', () => done(false))
326
+ socket.connect(port, '127.0.0.1')
327
+ })
328
+ }
329
+
330
+ export interface EnsureDaemonOptions {
331
+ port?: number
332
+ /** how long to wait for the spawned daemon to start listening before
333
+ * giving up. defaults to 5s, which is generous for a local process
334
+ * spawn even on cold hw. */
335
+ startupTimeoutMs?: number
336
+ }
337
+
338
+ /** returns { alreadyRunning: true } if something was already bound to the
339
+ * port, or { alreadyRunning: false, pid } if we spawned a daemon. throws
340
+ * if a spawn attempt fails to reach a ready state in time. */
341
+ export async function ensureDaemonRunning(
342
+ opts: EnsureDaemonOptions = {},
343
+ ): Promise<{ alreadyRunning: boolean; pid?: number }> {
344
+ const port = opts.port ?? DEFAULT_SOOTSIM_BRIDGE_PORT
345
+ if (await isBridgeUp(port)) {
346
+ return { alreadyRunning: true }
347
+ }
348
+ const { cmd, prefixArgs } = resolveSootsimInvocation()
349
+ const args = [...prefixArgs, 'server', '--quiet']
350
+ if (port !== DEFAULT_SOOTSIM_BRIDGE_PORT) {
351
+ args.push('--port', String(port))
352
+ }
353
+ const child = spawn(cmd, args, {
354
+ detached: true,
355
+ stdio: 'ignore',
356
+ env: process.env,
357
+ cwd: process.cwd(),
358
+ })
359
+ child.unref()
360
+ // poll with a modest budget. the daemon binds synchronously once its
361
+ // event loop is alive, so first-connection usually lands well under 1s.
362
+ const deadline = Date.now() + (opts.startupTimeoutMs ?? 5_000)
363
+ while (Date.now() < deadline) {
364
+ if (await isBridgeUp(port)) return { alreadyRunning: false, pid: child.pid }
365
+ await new Promise((r) => setTimeout(r, 100))
366
+ }
367
+ throw new AgentDaemonError(
368
+ `spawned sootsim daemon on port ${port} but it did not come up in time. ` +
369
+ `run \`sootsim server\` manually to diagnose.`,
370
+ 'SPAWN_TIMEOUT',
371
+ )
372
+ }
373
+
374
+ /** convenience: ensureDaemon + open a client + wait until ready. */
375
+ export async function connectToDaemon(
376
+ opts: AgentDaemonClientOptions & EnsureDaemonOptions = {},
377
+ ): Promise<AgentDaemonClient> {
378
+ await ensureDaemonRunning({
379
+ port: opts.port,
380
+ startupTimeoutMs: opts.startupTimeoutMs,
381
+ })
382
+ const client = new AgentDaemonClient(opts)
383
+ try {
384
+ await client.waitReady()
385
+ } catch (err) {
386
+ client.close()
387
+ throw err
388
+ }
389
+ return client
390
+ }
@@ -0,0 +1,71 @@
1
+ // wire format emitted by the agent-wrapper CLI subcommand on the events.out
2
+ // FIFO. every line on the FIFO is exactly one JSON object — parse line-by-line.
3
+ //
4
+ // this file is the source of truth for the sootsim ↔ agent channel. it's
5
+ // imported by electron main (cockpit bridge), the CLI watch/transcript tools,
6
+ // and the wrapper implementation itself, so it must stay runtime-safe (no
7
+ // node-specific or electron-specific imports).
8
+
9
+ export type AgentEvent =
10
+ | {
11
+ type: 'ready'
12
+ sessionId: string
13
+ projectId: string
14
+ provider: 'codex' | 'claude'
15
+ cwd: string
16
+ ts: number
17
+ }
18
+ | {
19
+ type: 'prompt-received'
20
+ text: string
21
+ inspectSummary?: string
22
+ inspectTrace?: string
23
+ ts: number
24
+ }
25
+ | { type: 'turn-started'; turnId?: string; ts: number }
26
+ | { type: 'turn-reasoning'; delta: string; ts: number }
27
+ | { type: 'turn-message'; delta: string; ts: number }
28
+ | {
29
+ type: 'turn-plan'
30
+ steps: Array<{ id: string; title: string; status: string }>
31
+ ts: number
32
+ }
33
+ | { type: 'tool-call'; name: string; args: unknown; ts: number }
34
+ | {
35
+ type: 'file-edited'
36
+ path: string
37
+ kind: 'add' | 'modify' | 'delete'
38
+ diff?: string
39
+ ts: number
40
+ }
41
+ | { type: 'file-diff-delta'; path: string; delta: string; ts: number }
42
+ | { type: 'approval-needed'; kind: string; detail: unknown; ts: number }
43
+ | {
44
+ type: 'turn-completed'
45
+ turnId?: string
46
+ filesTouched: string[]
47
+ durationMs: number
48
+ costUsd?: number
49
+ ts: number
50
+ }
51
+ | { type: 'error'; message: string; ts: number }
52
+ | { type: 'exited'; code: number | null; ts: number }
53
+
54
+ export type AgentEventType = AgentEvent['type']
55
+
56
+ export function isAgentEvent(value: unknown): value is AgentEvent {
57
+ if (!value || typeof value !== 'object') return false
58
+ const v = value as { type?: unknown; ts?: unknown }
59
+ return typeof v.type === 'string' && typeof v.ts === 'number'
60
+ }
61
+
62
+ export function parseAgentEventLine(line: string): AgentEvent | null {
63
+ const trimmed = line.trim()
64
+ if (!trimmed) return null
65
+ try {
66
+ const parsed = JSON.parse(trimmed) as unknown
67
+ return isAgentEvent(parsed) ? parsed : null
68
+ } catch {
69
+ return null
70
+ }
71
+ }
@@ -0,0 +1,71 @@
1
+ export interface AgentPromptEnvelope {
2
+ text: string
3
+ displayText?: string
4
+ inspectSummary?: string
5
+ inspectTrace?: string
6
+ }
7
+
8
+ const ENVELOPE_MARKER = 'sootsim-agent-prompt-v1'
9
+
10
+ function cleanPromptText(value: string | undefined): string {
11
+ return typeof value === 'string' ? value.trim() : ''
12
+ }
13
+
14
+ export function encodeAgentPromptEnvelope(input: AgentPromptEnvelope): string {
15
+ const text = cleanPromptText(input.text)
16
+ if (!text) return ''
17
+ const displayText = cleanPromptText(input.displayText)
18
+ const inspectSummary = cleanPromptText(input.inspectSummary)
19
+ const inspectTrace = cleanPromptText(input.inspectTrace)
20
+ const needsEnvelope =
21
+ !!inspectSummary ||
22
+ !!inspectTrace ||
23
+ /[\r\n]/.test(text) ||
24
+ (!!displayText && displayText !== text)
25
+ if (!needsEnvelope) return text
26
+ return JSON.stringify({
27
+ __sootsimAgentPrompt: ENVELOPE_MARKER,
28
+ text,
29
+ displayText,
30
+ inspectSummary,
31
+ inspectTrace,
32
+ })
33
+ }
34
+
35
+ export function decodeAgentPromptEnvelope(line: string): AgentPromptEnvelope | null {
36
+ const trimmed = cleanPromptText(line)
37
+ if (!trimmed) return null
38
+ try {
39
+ const parsed = JSON.parse(trimmed) as {
40
+ __sootsimAgentPrompt?: unknown
41
+ text?: unknown
42
+ displayText?: unknown
43
+ inspectSummary?: unknown
44
+ inspectTrace?: unknown
45
+ }
46
+ if (parsed.__sootsimAgentPrompt !== ENVELOPE_MARKER) {
47
+ return { text: trimmed }
48
+ }
49
+ const text = cleanPromptText(
50
+ typeof parsed.text === 'string' ? parsed.text : undefined,
51
+ )
52
+ if (!text) return null
53
+ const displayText = cleanPromptText(
54
+ typeof parsed.displayText === 'string' ? parsed.displayText : undefined,
55
+ )
56
+ const inspectSummary = cleanPromptText(
57
+ typeof parsed.inspectSummary === 'string' ? parsed.inspectSummary : undefined,
58
+ )
59
+ const inspectTrace = cleanPromptText(
60
+ typeof parsed.inspectTrace === 'string' ? parsed.inspectTrace : undefined,
61
+ )
62
+ return {
63
+ text,
64
+ ...(displayText ? { displayText } : {}),
65
+ ...(inspectSummary ? { inspectSummary } : {}),
66
+ ...(inspectTrace ? { inspectTrace } : {}),
67
+ }
68
+ } catch {
69
+ return { text: trimmed }
70
+ }
71
+ }