rnxsim 0.0.0 → 0.1.313

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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +512 -0
  3. package/detox/colors.ts +54 -0
  4. package/detox/config-loader.ts +135 -0
  5. package/detox/element-types.ts +39 -0
  6. package/detox/expectations.ts +537 -0
  7. package/detox/gestures.ts +480 -0
  8. package/detox/index.ts +2118 -0
  9. package/detox/jest-preset.cjs +51 -0
  10. package/detox/jest-setup-after-env.cjs +18 -0
  11. package/detox/matchers.ts +30 -0
  12. package/detox/motion-change.cjs +109 -0
  13. package/detox/motion-change.d.cts +35 -0
  14. package/detox/navigation.ts +43 -0
  15. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  16. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  17. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  18. package/detox/screenshots/theme-blue-toggle.png +0 -0
  19. package/detox/screenshots/theme-blue.png +0 -0
  20. package/detox/screenshots/theme-red-toggle.png +0 -0
  21. package/detox/screenshots/theme-red.png +0 -0
  22. package/dist-cli/bin.js +15 -0
  23. package/dist-cli/chunks/agent-XZ2KTPCU.js +48 -0
  24. package/dist-cli/chunks/agent-wrapper-JJYYW2WH.js +17 -0
  25. package/dist-cli/chunks/app-fonts-IXRNQG6B.js +4 -0
  26. package/dist-cli/chunks/assert-54T5SK5F.js +49 -0
  27. package/dist-cli/chunks/auth-FI5UDI45.js +12 -0
  28. package/dist-cli/chunks/beta-JV6UKADW.js +4 -0
  29. package/dist-cli/chunks/camera-VL73YIKP.js +22 -0
  30. package/dist-cli/chunks/chunk-2D2UPBBR.js +4 -0
  31. package/dist-cli/chunks/chunk-2YR5BGA5.js +5 -0
  32. package/dist-cli/chunks/chunk-3NV2NCNX.js +4 -0
  33. package/dist-cli/chunks/chunk-46EUUFJ5.js +4 -0
  34. package/dist-cli/chunks/chunk-4NPPOV2N.js +5 -0
  35. package/dist-cli/chunks/chunk-5DHC6KHQ.js +7 -0
  36. package/dist-cli/chunks/chunk-5TEF3ET3.js +74 -0
  37. package/dist-cli/chunks/chunk-5TPRP5QT.js +4 -0
  38. package/dist-cli/chunks/chunk-5YJCOWCH.js +4 -0
  39. package/dist-cli/chunks/chunk-7GN3LVWB.js +4 -0
  40. package/dist-cli/chunks/chunk-BBULZ7CG.js +568 -0
  41. package/dist-cli/chunks/chunk-BTWORNNG.js +71 -0
  42. package/dist-cli/chunks/chunk-D4FFVGI5.js +5 -0
  43. package/dist-cli/chunks/chunk-DCEMHR2Y.js +4 -0
  44. package/dist-cli/chunks/chunk-DZS6WPUI.js +4 -0
  45. package/dist-cli/chunks/chunk-F5ZRSS3C.js +3 -0
  46. package/dist-cli/chunks/chunk-FSUYIVJ6.js +9 -0
  47. package/dist-cli/chunks/chunk-G2WW6L2C.js +23 -0
  48. package/dist-cli/chunks/chunk-GASE6UBA.js +4 -0
  49. package/dist-cli/chunks/chunk-GGRX24GF.js +142 -0
  50. package/dist-cli/chunks/chunk-HI5TFJWN.js +5 -0
  51. package/dist-cli/chunks/chunk-IJ5CAZZC.js +5 -0
  52. package/dist-cli/chunks/chunk-IJO63TDP.js +4 -0
  53. package/dist-cli/chunks/chunk-KTHV3RUS.js +26 -0
  54. package/dist-cli/chunks/chunk-LF2ZVT7O.js +6 -0
  55. package/dist-cli/chunks/chunk-MJRLLB4R.js +38 -0
  56. package/dist-cli/chunks/chunk-NFK7T35W.js +4 -0
  57. package/dist-cli/chunks/chunk-NMF2ZMZQ.js +15 -0
  58. package/dist-cli/chunks/chunk-OHAZNXLK.js +4 -0
  59. package/dist-cli/chunks/chunk-OVFJFXUD.js +101 -0
  60. package/dist-cli/chunks/chunk-OZSSI4WN.js +28 -0
  61. package/dist-cli/chunks/chunk-QKDWYITG.js +25 -0
  62. package/dist-cli/chunks/chunk-RSZWCKNT.js +30 -0
  63. package/dist-cli/chunks/chunk-RTN5C5RL.js +4 -0
  64. package/dist-cli/chunks/chunk-TIVZIMMW.js +4 -0
  65. package/dist-cli/chunks/chunk-TZFFR3SD.js +7 -0
  66. package/dist-cli/chunks/chunk-UC6U3MML.js +5 -0
  67. package/dist-cli/chunks/chunk-VFCMSYZK.js +6 -0
  68. package/dist-cli/chunks/chunk-VNQEB4L7.js +5 -0
  69. package/dist-cli/chunks/chunk-VZXWHRUZ.js +983 -0
  70. package/dist-cli/chunks/chunk-W6K4EFPH.js +107 -0
  71. package/dist-cli/chunks/chunk-WEXDAC74.js +4 -0
  72. package/dist-cli/chunks/chunk-WF3T4SVI.js +14 -0
  73. package/dist-cli/chunks/chunk-WINYQ44O.js +4 -0
  74. package/dist-cli/chunks/chunk-WMIIKMGK.js +10 -0
  75. package/dist-cli/chunks/chunk-WUSWBCWA.js +507 -0
  76. package/dist-cli/chunks/chunk-WWZIXIRD.js +4 -0
  77. package/dist-cli/chunks/chunk-XEVZYVIW.js +766 -0
  78. package/dist-cli/chunks/chunk-YDGQTMQL.js +4 -0
  79. package/dist-cli/chunks/chunk-YIFT42WN.js +4 -0
  80. package/dist-cli/chunks/chunk-ZMJD5GEC.js +125 -0
  81. package/dist-cli/chunks/cleanup-P27PA6JI.js +25 -0
  82. package/dist-cli/chunks/cli-version-WWLPBDQ7.js +4 -0
  83. package/dist-cli/chunks/compat-ZD65FED3.js +20 -0
  84. package/dist-cli/chunks/config-XMJRNM2A.js +13 -0
  85. package/dist-cli/chunks/control-KMIQT3QP.js +4 -0
  86. package/dist-cli/chunks/daemon-G2ME7NLB.js +4 -0
  87. package/dist-cli/chunks/debug-PT4HOP7N.js +200 -0
  88. package/dist-cli/chunks/desktop-S3FG72AK.js +31 -0
  89. package/dist-cli/chunks/detox-B3D4IFCN.js +52 -0
  90. package/dist-cli/chunks/device-XBNDSB2R.js +18 -0
  91. package/dist-cli/chunks/diagnose-HMQXJE5N.js +43 -0
  92. package/dist-cli/chunks/disk-cleanup-BLCZ5BSZ.js +4 -0
  93. package/dist-cli/chunks/drivers-LDECZGP2.js +4 -0
  94. package/dist-cli/chunks/film-BJGTBYZB.js +74 -0
  95. package/dist-cli/chunks/flow-UEQNVTU7.js +4 -0
  96. package/dist-cli/chunks/help-T5FYSVGB.js +4 -0
  97. package/dist-cli/chunks/hidden-runtime-alias-ANOYADHM.js +4 -0
  98. package/dist-cli/chunks/home-paths-GT3LFNOR.js +4 -0
  99. package/dist-cli/chunks/inspect-ZA6XF5LD.js +4 -0
  100. package/dist-cli/chunks/install-desktop-TIMUDHPL.js +4 -0
  101. package/dist-cli/chunks/login-FJ737MWG.js +28 -0
  102. package/dist-cli/chunks/logout-ZCNMMHMY.js +4 -0
  103. package/dist-cli/chunks/maestro-SZTNKLDF.js +104 -0
  104. package/dist-cli/chunks/maestro-generate-DCFAIZ4H.js +36 -0
  105. package/dist-cli/chunks/mode-GRMQCRXR.js +26 -0
  106. package/dist-cli/chunks/optional-demo-registry-W36EWFFB.js +4 -0
  107. package/dist-cli/chunks/perf-QYBAAUZG.js +48 -0
  108. package/dist-cli/chunks/permissions-3QCQ6VF4.js +13 -0
  109. package/dist-cli/chunks/record-QPPC2S4E.js +72 -0
  110. package/dist-cli/chunks/report-issue-7NMFP4HK.js +29 -0
  111. package/dist-cli/chunks/runtime-XOAXMSTU.js +4 -0
  112. package/dist-cli/chunks/screenshot-command-67AECJFB.js +159 -0
  113. package/dist-cli/chunks/screenshot-layers-ASWBYPJL.js +20 -0
  114. package/dist-cli/chunks/screenshots-capture-PXA3HFQK.js +42 -0
  115. package/dist-cli/chunks/serve-BI2NBAXG.js +44 -0
  116. package/dist-cli/chunks/setup-7DWPMRSB.js +32 -0
  117. package/dist-cli/chunks/skills-S3Y22TUA.js +50 -0
  118. package/dist-cli/chunks/storage-XUIMJWAJ.js +37 -0
  119. package/dist-cli/chunks/store-JTHEJLAZ.js +4 -0
  120. package/dist-cli/chunks/telemetry-ZYJGD2DB.js +4 -0
  121. package/dist-cli/chunks/timeline-TMPLQPSP.js +24 -0
  122. package/dist-cli/chunks/upgrade-7HDSIM7K.js +7 -0
  123. package/dist-cli/chunks/upload-GMSZPWM6.js +4 -0
  124. package/dist-cli/chunks/version-HOCHZ37L.js +6 -0
  125. package/dist-cli/chunks/web-DG3WBYD3.js +4 -0
  126. package/dist-cli/chunks/what-happened-XFVUTZR7.js +25 -0
  127. package/dist-lib/agent-daemon-client.cjs +468 -0
  128. package/dist-lib/agent-events.cjs +48 -0
  129. package/dist-lib/agent-identity.cjs +53 -0
  130. package/dist-lib/agent-sessions.cjs +725 -0
  131. package/dist-lib/attached-projects.cjs +526 -0
  132. package/dist-lib/auth/shared-session.cjs +195 -0
  133. package/dist-lib/backend-origin.cjs +70 -0
  134. package/dist-lib/beta.cjs +41 -0
  135. package/dist-lib/beta.mjs +13 -0
  136. package/dist-lib/bridge-constants.cjs +161 -0
  137. package/dist-lib/cli-constants.cjs +32 -0
  138. package/dist-lib/config.cjs +96 -0
  139. package/dist-lib/detox/index.cjs +2863 -0
  140. package/dist-lib/detox/jest-preset.cjs +51 -0
  141. package/dist-lib/dev-bundle-resolution.cjs +223 -0
  142. package/dist-lib/home-paths.cjs +421 -0
  143. package/dist-lib/host/bridge-host.cjs +5600 -0
  144. package/dist-lib/host/fetch-proxy-handler.cjs +373 -0
  145. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  146. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  147. package/dist-lib/host/websocket-proxy.cjs +214 -0
  148. package/dist-lib/index.cjs +513 -0
  149. package/dist-lib/jump-to-source-babel.cjs +93 -0
  150. package/dist-lib/menu.cjs +620 -0
  151. package/dist-lib/menu.mjs +581 -0
  152. package/dist-lib/metro.cjs +297 -0
  153. package/dist-lib/profiles.cjs +246 -0
  154. package/dist-lib/public-brand.cjs +39 -0
  155. package/dist-lib/render-mode.cjs +54 -0
  156. package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
  157. package/dist-lib/sdk.cjs +2333 -0
  158. package/dist-lib/sdk.mjs +2240 -0
  159. package/dist-lib/skills.cjs +23922 -0
  160. package/dist-lib/vite.cjs +358 -0
  161. package/package.json +189 -18
  162. package/scripts/dev-server-scanner.ts +941 -0
  163. package/scripts/optional-demo-registry.ts +113 -0
  164. package/scripts/postinstall.cjs +3 -0
  165. package/scripts/preuninstall.cjs +118 -0
  166. package/skills/contrast/SKILL.md +171 -0
  167. package/skills/rnx-debug/SKILL.md +425 -0
  168. package/skills/rnx-perf/SKILL.md +295 -0
  169. package/skills/rnx-setup/SKILL.md +160 -0
  170. package/skills/rnx-test/SKILL.md +292 -0
  171. package/skills/rnx-visual/SKILL.md +122 -0
  172. package/src/agent-daemon-client.ts +410 -0
  173. package/src/agent-events.ts +71 -0
  174. package/src/agent-identity.ts +23 -0
  175. package/src/agent-prompt.ts +71 -0
  176. package/src/agent-sessions.ts +573 -0
  177. package/src/attached-projects.ts +539 -0
  178. package/src/auth/shared-session.ts +204 -0
  179. package/src/backend-origin.ts +49 -0
  180. package/src/beta.ts +19 -0
  181. package/src/bridge-constants.ts +171 -0
  182. package/src/browser-cache.ts +16 -0
  183. package/src/cli-constants.ts +1 -0
  184. package/src/cli-version.ts +32 -0
  185. package/src/codex-client.ts +215 -0
  186. package/src/config.ts +134 -0
  187. package/src/dev-bundle-resolution.ts +236 -0
  188. package/src/disk-cleanup.ts +466 -0
  189. package/src/home-paths.ts +552 -0
  190. package/src/host/agent-host.ts +666 -0
  191. package/src/host/bridge-host.ts +2626 -0
  192. package/src/host/fetch-proxy-handler.ts +397 -0
  193. package/src/host/fetch-proxy-overrides.ts +51 -0
  194. package/src/host/open-url.ts +89 -0
  195. package/src/host/websocket-proxy.ts +210 -0
  196. package/src/index.ts +12 -705
  197. package/src/jump-to-source-babel.ts +135 -0
  198. package/src/menu.ts +734 -0
  199. package/src/metro-plugin.ts +139 -0
  200. package/src/native-dev-bundle-url.ts +96 -0
  201. package/src/native-seam-manifest.ts +341 -0
  202. package/src/plist.ts +27 -0
  203. package/src/poll-until.ts +92 -0
  204. package/src/profiles.ts +179 -0
  205. package/src/public-brand.ts +9 -0
  206. package/src/render-mode.ts +27 -0
  207. package/src/runtime-assets.ts +228 -0
  208. package/src/runtime-delivery.ts +54 -0
  209. package/src/screenshots/compose.ts +422 -0
  210. package/src/screenshots/frame-compose.ts +454 -0
  211. package/src/screenshots/orchestrate.ts +256 -0
  212. package/src/screenshots/org-deck.ts +91 -0
  213. package/src/screenshots/registry.ts +58 -0
  214. package/src/screenshots/schema.ts +364 -0
  215. package/src/sdk.ts +7 -0
  216. package/src/settings-bundle.ts +418 -0
  217. package/src/skills/builtin/a11y-review.ts +132 -0
  218. package/src/skills/builtin/compat-check.ts +71 -0
  219. package/src/skills/builtin/maestro-test.ts +121 -0
  220. package/src/skills/builtin/perf-profile.ts +90 -0
  221. package/src/skills/builtin/screenshot-all.ts +52 -0
  222. package/src/skills/builtin/visual-diff.ts +100 -0
  223. package/src/skills/registry.ts +107 -0
  224. package/src/skills/types.ts +41 -0
  225. package/src/team-machine-prompt.ts +73 -0
  226. package/src/vite-plugin-one.ts +190 -0
  227. package/src/vite-plugin.ts +1750 -0
  228. package/src/worklets-babel.ts +134 -0
@@ -0,0 +1,410 @@
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 { shouldSkipPersistentDaemon } from './home-paths.ts'
19
+ import type { AgentEvent } from './agent-events.ts'
20
+ import type { AgentPromptEnvelope } from './agent-prompt.ts'
21
+ import type { AgentSession, AttachedProject } from './attached-projects.ts'
22
+
23
+ export interface AgentDaemonClientOptions {
24
+ port?: number
25
+ commandTimeoutMs?: number
26
+ /** label forwarded in logs; helps identify the daemon caller. */
27
+ clientLabel?: string
28
+ }
29
+
30
+ export interface AgentDaemonPaths {
31
+ userDataDir: string
32
+ storeFile: string
33
+ sessionsDir: string
34
+ transcriptsDir: string
35
+ }
36
+
37
+ export interface AgentStartSessionInput {
38
+ projectId: string
39
+ provider?: Provider
40
+ codexBin?: string
41
+ claudeBin?: string
42
+ freshThread?: boolean
43
+ }
44
+
45
+ export interface AgentStartSessionResult {
46
+ session: AgentSession
47
+ wrapperPid: number
48
+ }
49
+
50
+ export interface AgentUpsertProjectInput {
51
+ cwd: string
52
+ name?: string
53
+ preferredProvider?: Provider
54
+ sourceRoots?: string[]
55
+ knownBundleUrls?: string[]
56
+ framework?: 'expo' | 'one' | 'rock' | 'unknown'
57
+ bundleId?: string
58
+ }
59
+
60
+ export interface AgentAutoAttachInput {
61
+ bundleUrl: string
62
+ provider?: Provider
63
+ }
64
+
65
+ export class AgentDaemonError extends Error {
66
+ code?: string
67
+ constructor(message: string, code?: string) {
68
+ super(message)
69
+ this.name = 'AgentDaemonError'
70
+ this.code = code
71
+ }
72
+ }
73
+
74
+ type PendingEntry = {
75
+ resolve: (value: unknown) => void
76
+ reject: (err: Error) => void
77
+ timer: ReturnType<typeof setTimeout>
78
+ }
79
+
80
+ type EventCallback = (payload: { sessionId: string; event: AgentEvent }) => void
81
+ type StatusCallback = (session: AgentSession) => void
82
+
83
+ export class AgentDaemonClient {
84
+ private ws: WebSocket
85
+ private port: number
86
+ private commandTimeoutMs: number
87
+ private ready: Promise<void>
88
+ private closed = false
89
+ private nextId = 1
90
+ private pending = new Map<number, PendingEntry>()
91
+ private eventListeners = new Set<EventCallback>()
92
+ private statusListeners = new Set<StatusCallback>()
93
+ private disconnectListeners = new Set<() => void>()
94
+
95
+ constructor(opts: AgentDaemonClientOptions = {}) {
96
+ this.port = opts.port ?? DEFAULT_SOOTSIM_BRIDGE_PORT
97
+ this.commandTimeoutMs = opts.commandTimeoutMs ?? 15_000
98
+ this.ws = new WebSocket(`ws://localhost:${this.port}`)
99
+ this.ready = new Promise<void>((resolve, reject) => {
100
+ const onOpen = () => {
101
+ this.ws.off('error', onError)
102
+ resolve()
103
+ }
104
+ const onError = (err: Error) => {
105
+ this.ws.off('open', onOpen)
106
+ reject(
107
+ new AgentDaemonError(
108
+ `could not connect to rnx daemon on port ${this.port}: ${err.message}`,
109
+ 'NO_DAEMON',
110
+ ),
111
+ )
112
+ }
113
+ this.ws.once('open', onOpen)
114
+ this.ws.once('error', onError)
115
+ })
116
+ this.ws.on('message', (data) => this.handleMessage(data))
117
+ this.ws.on('close', () => this.handleClose())
118
+ // ws emits 'error' for late socket faults (peer dropped, write EIO after
119
+ // sleep/wake, etc). without a listener, node's EventEmitter rethrows as
120
+ // uncaughtException. 'close' fires right after and runs the cleanup.
121
+ this.ws.on('error', () => {})
122
+ }
123
+
124
+ async waitReady(): Promise<void> {
125
+ return this.ready
126
+ }
127
+
128
+ // --- public api ---
129
+
130
+ async listProjects(): Promise<AttachedProject[]> {
131
+ return this.send<AttachedProject[]>('agent:list-projects')
132
+ }
133
+
134
+ async upsertProject(input: AgentUpsertProjectInput): Promise<AttachedProject> {
135
+ return this.send<AttachedProject>('agent:upsert-project', { input })
136
+ }
137
+
138
+ async deleteProject(projectId: string): Promise<{ ok: true }> {
139
+ return this.send<{ ok: true }>('agent:delete-project', { projectId })
140
+ }
141
+
142
+ async autoAttachForUrl(
143
+ input: AgentAutoAttachInput,
144
+ ): Promise<{ project: AttachedProject | null }> {
145
+ return this.send<{ project: AttachedProject | null }>('agent:auto-attach-for-url', {
146
+ input,
147
+ })
148
+ }
149
+
150
+ async listSessions(projectId?: string): Promise<AgentSession[]> {
151
+ return this.send<AgentSession[]>('agent:list-sessions', { projectId })
152
+ }
153
+
154
+ async startSession(input: AgentStartSessionInput): Promise<AgentStartSessionResult> {
155
+ return this.send<AgentStartSessionResult>('agent:start-session', { input })
156
+ }
157
+
158
+ async sendPrompt(
159
+ sessionId: string,
160
+ prompt: AgentPromptEnvelope,
161
+ ): Promise<{ ok: true }> {
162
+ return this.send<{ ok: true }>('agent:send-prompt', {
163
+ sessionId,
164
+ prompt,
165
+ })
166
+ }
167
+
168
+ async sendClaimedPrompt(
169
+ simId: string,
170
+ prompt: AgentPromptEnvelope,
171
+ ): Promise<{ ok: true; routed: 'team-machine'; sessionId: string }> {
172
+ return this.send<{ ok: true; routed: 'team-machine'; sessionId: string }>(
173
+ 'agent:send-claimed-prompt',
174
+ {
175
+ simId,
176
+ prompt,
177
+ },
178
+ )
179
+ }
180
+
181
+ async endSession(sessionId: string): Promise<{ ok: true }> {
182
+ return this.send<{ ok: true }>('agent:end-session', { sessionId })
183
+ }
184
+
185
+ async getTranscript(
186
+ sessionId: string,
187
+ ): Promise<string | { error: string; code: string }> {
188
+ return this.send<string | { error: string; code: string }>('agent:get-transcript', {
189
+ sessionId,
190
+ })
191
+ }
192
+
193
+ async getPaths(): Promise<AgentDaemonPaths> {
194
+ return this.send<AgentDaemonPaths>('agent:get-paths')
195
+ }
196
+
197
+ async subscribeEvents(sessionId: string): Promise<{ ok: true; refCount: number }> {
198
+ return this.send<{ ok: true; refCount: number }>('agent:subscribe-events', {
199
+ sessionId,
200
+ })
201
+ }
202
+
203
+ async unsubscribeEvents(sessionId: string): Promise<{ ok: true; refCount: number }> {
204
+ return this.send<{ ok: true; refCount: number }>('agent:unsubscribe-events', {
205
+ sessionId,
206
+ })
207
+ }
208
+
209
+ onAgentEvent(cb: EventCallback): () => void {
210
+ this.eventListeners.add(cb)
211
+ return () => this.eventListeners.delete(cb)
212
+ }
213
+
214
+ onSessionStatusChange(cb: StatusCallback): () => void {
215
+ this.statusListeners.add(cb)
216
+ return () => this.statusListeners.delete(cb)
217
+ }
218
+
219
+ onDisconnect(cb: () => void): () => void {
220
+ this.disconnectListeners.add(cb)
221
+ return () => this.disconnectListeners.delete(cb)
222
+ }
223
+
224
+ close(): void {
225
+ if (this.closed) return
226
+ this.closed = true
227
+ try {
228
+ this.ws.close()
229
+ } catch {}
230
+ }
231
+
232
+ // --- internals ---
233
+
234
+ private async send<T>(type: string, payload: Record<string, unknown> = {}): Promise<T> {
235
+ await this.ready
236
+ if (this.closed || this.ws.readyState !== WebSocket.OPEN) {
237
+ throw new AgentDaemonError('daemon connection is closed', 'NO_DAEMON')
238
+ }
239
+ const id = this.nextId++
240
+ return new Promise<T>((resolve, reject) => {
241
+ const timer = setTimeout(() => {
242
+ this.pending.delete(id)
243
+ reject(
244
+ new AgentDaemonError(
245
+ `${type} timed out after ${Math.round(this.commandTimeoutMs / 1000)}s`,
246
+ 'TIMEOUT',
247
+ ),
248
+ )
249
+ }, this.commandTimeoutMs)
250
+ this.pending.set(id, {
251
+ resolve: resolve as (value: unknown) => void,
252
+ reject,
253
+ timer,
254
+ })
255
+ try {
256
+ this.ws.send(JSON.stringify({ id, type, ...payload }))
257
+ } catch (err) {
258
+ clearTimeout(timer)
259
+ this.pending.delete(id)
260
+ reject(err instanceof Error ? err : new Error(String(err)))
261
+ }
262
+ })
263
+ }
264
+
265
+ private handleMessage(data: unknown): void {
266
+ let msg: any
267
+ try {
268
+ msg = JSON.parse(String(data))
269
+ } catch {
270
+ return
271
+ }
272
+ if (!msg || typeof msg !== 'object') return
273
+ if (msg.type === 'agent:event') {
274
+ for (const cb of this.eventListeners) {
275
+ try {
276
+ cb({ sessionId: msg.sessionId, event: msg.event })
277
+ } catch {}
278
+ }
279
+ return
280
+ }
281
+ if (msg.type === 'agent:session-status') {
282
+ for (const cb of this.statusListeners) {
283
+ try {
284
+ cb(msg.session)
285
+ } catch {}
286
+ }
287
+ return
288
+ }
289
+ // non-agent server pushes (bridge:welcome, bridge:client-state, …) are
290
+ // harmless — we just ignore them. we only opened this socket for the
291
+ // agent protocol.
292
+ if (typeof msg.id !== 'number') return
293
+ const entry = this.pending.get(msg.id)
294
+ if (!entry) return
295
+ this.pending.delete(msg.id)
296
+ clearTimeout(entry.timer)
297
+ if (msg.error) {
298
+ entry.reject(new AgentDaemonError(msg.error, msg.code))
299
+ } else {
300
+ entry.resolve(msg.result)
301
+ }
302
+ }
303
+
304
+ private handleClose(): void {
305
+ if (this.closed) return
306
+ this.closed = true
307
+ for (const [, entry] of this.pending) {
308
+ clearTimeout(entry.timer)
309
+ entry.reject(new AgentDaemonError('daemon disconnected', 'DISCONNECT'))
310
+ }
311
+ this.pending.clear()
312
+ for (const cb of this.disconnectListeners) {
313
+ try {
314
+ cb()
315
+ } catch {}
316
+ }
317
+ }
318
+ }
319
+
320
+ // --- daemon lifecycle helpers ---
321
+
322
+ /** raw TCP probe — matches the shape electron main.ts already uses. */
323
+ export function isBridgeUp(
324
+ port: number = DEFAULT_SOOTSIM_BRIDGE_PORT,
325
+ timeoutMs = 400,
326
+ ): Promise<boolean> {
327
+ return new Promise((resolve) => {
328
+ const socket = new net.Socket()
329
+ let settled = false
330
+ const done = (ok: boolean) => {
331
+ if (settled) return
332
+ settled = true
333
+ socket.destroy()
334
+ resolve(ok)
335
+ }
336
+ socket.setTimeout(timeoutMs)
337
+ socket.once('connect', () => done(true))
338
+ socket.once('timeout', () => done(false))
339
+ socket.once('error', () => done(false))
340
+ socket.connect(port, '127.0.0.1')
341
+ })
342
+ }
343
+
344
+ export interface EnsureDaemonOptions {
345
+ port?: number
346
+ /** how long to wait for the spawned daemon to start listening before
347
+ * giving up. defaults to 5s, which is generous for a local process
348
+ * spawn even on cold hw. */
349
+ startupTimeoutMs?: number
350
+ }
351
+
352
+ /** returns { alreadyRunning: true } if something was already bound to the
353
+ * port, or { alreadyRunning: false, pid } if we spawned a daemon. throws
354
+ * if a spawn attempt fails to reach a ready state in time. */
355
+ export async function ensureDaemonRunning(
356
+ opts: EnsureDaemonOptions = {},
357
+ ): Promise<{ alreadyRunning: boolean; pid?: number }> {
358
+ const port = opts.port ?? DEFAULT_SOOTSIM_BRIDGE_PORT
359
+ if (await isBridgeUp(port)) {
360
+ return { alreadyRunning: true }
361
+ }
362
+ if (shouldSkipPersistentDaemon()) {
363
+ throw new AgentDaemonError(
364
+ `no rnx bridge on port ${port}. run \`rnxsim setup\` to install the background daemon, or \`rnxsim serve\` in another shell.`,
365
+ 'DEV_HOST_REFUSED',
366
+ )
367
+ }
368
+ const { cmd, prefixArgs } = resolveSootsimInvocation()
369
+ const args = [...prefixArgs, 'serve', '--quiet']
370
+ if (port !== DEFAULT_SOOTSIM_BRIDGE_PORT) {
371
+ args.push('--port', String(port))
372
+ }
373
+ const child = spawn(cmd, args, {
374
+ detached: true,
375
+ stdio: 'ignore',
376
+ env: process.env,
377
+ cwd: process.cwd(),
378
+ })
379
+ child.unref()
380
+ // poll with a modest budget. the daemon binds synchronously once its
381
+ // event loop is alive, so first-connection usually lands well under 1s.
382
+ const deadline = Date.now() + (opts.startupTimeoutMs ?? 5_000)
383
+ while (Date.now() < deadline) {
384
+ if (await isBridgeUp(port)) return { alreadyRunning: false, pid: child.pid }
385
+ await new Promise((r) => setTimeout(r, 100))
386
+ }
387
+ throw new AgentDaemonError(
388
+ `spawned rnx daemon on port ${port} but it did not come up in time. ` +
389
+ `run \`rnxsim serve\` manually to diagnose.`,
390
+ 'SPAWN_TIMEOUT',
391
+ )
392
+ }
393
+
394
+ /** convenience: ensureDaemon + open a client + wait until ready. */
395
+ export async function connectToDaemon(
396
+ opts: AgentDaemonClientOptions & EnsureDaemonOptions = {},
397
+ ): Promise<AgentDaemonClient> {
398
+ await ensureDaemonRunning({
399
+ port: opts.port,
400
+ startupTimeoutMs: opts.startupTimeoutMs,
401
+ })
402
+ const client = new AgentDaemonClient(opts)
403
+ try {
404
+ await client.waitReady()
405
+ } catch (err) {
406
+ client.close()
407
+ throw err
408
+ }
409
+ return client
410
+ }
@@ -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,23 @@
1
+ const TEAM_MACHINE_SESSION_RE = /^[A-Za-z0-9._:-]{1,160}$/
2
+
3
+ export function agentSessionIdFromCliIdentity(identity: string | null): string | null {
4
+ if (!identity) return null
5
+ const colon = identity.indexOf(':')
6
+ if (colon < 0) return identity.trim() || null
7
+ const source = identity.slice(0, colon)
8
+ const value = identity.slice(colon + 1).trim()
9
+ return source === 'SOOTSIM_CLI_IDENTITY' && value ? value : null
10
+ }
11
+
12
+ export function teamMachineSessionIdFromCliIdentity(
13
+ identity: string | null,
14
+ ): string | null {
15
+ if (!identity?.startsWith('TM_SESSION:')) return null
16
+ const sessionId = identity.slice('TM_SESSION:'.length).trim()
17
+ if (!TEAM_MACHINE_SESSION_RE.test(sessionId)) return null
18
+ return sessionId
19
+ }
20
+
21
+ export function isValidTeamMachineSessionId(sessionId: string): boolean {
22
+ return TEAM_MACHINE_SESSION_RE.test(sessionId)
23
+ }
@@ -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
+ }