sootsim 0.1.83 → 0.1.84
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.
- package/README.md +0 -1
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/expectations.ts +477 -0
- package/detox/gestures.ts +442 -0
- package/detox/index.ts +1436 -0
- package/detox/jest-environment.ts +86 -0
- package/detox/jest-preset.cjs +50 -0
- package/detox/matchers.ts +29 -0
- package/detox/navigation.ts +43 -0
- package/detox/run-test.ts +113 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-cli/bin.js +3 -3
- package/dist-cli/chunks/{agent-MQ7GLVIB.js → agent-2CWD6W6P.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-7KAFDQCN.js → agent-wrapper-5W3LOX6S.js} +2 -2
- package/dist-cli/chunks/{assert-TV46GUNU.js → assert-ZOMAMKRT.js} +2 -2
- package/dist-cli/chunks/auto-bootstrap-NYYSMTIM.js +2 -0
- package/dist-cli/chunks/beta-4K2SQACK.js +2 -0
- package/dist-cli/chunks/chunk-3HXQ7MJK.js +79 -0
- package/dist-cli/chunks/{chunk-4LS5MZAI.js → chunk-4K7BH2D4.js} +3 -3
- package/dist-cli/chunks/{chunk-FJYT7XL2.js → chunk-4OPRODFA.js} +2 -2
- package/dist-cli/chunks/{chunk-DP7O5MHK.js → chunk-4OWVPRZV.js} +2 -2
- package/dist-cli/chunks/{chunk-PM5NVKLP.js → chunk-5XCXOLG2.js} +2 -2
- package/dist-cli/chunks/chunk-67ZZ2CM5.js +1 -0
- package/dist-cli/chunks/{chunk-WN7M3QON.js → chunk-73UZXB4B.js} +2 -2
- package/dist-cli/chunks/{chunk-5DJXZIFZ.js → chunk-7NWNTUJF.js} +1 -1
- package/dist-cli/chunks/{chunk-Y2VJBRSP.js → chunk-7YHDJLO2.js} +1 -1
- package/dist-cli/chunks/{chunk-6NN2D4EJ.js → chunk-AJVTY6KY.js} +1 -1
- package/dist-cli/chunks/chunk-AWSQUOAS.js +67 -0
- package/dist-cli/chunks/{chunk-CJY3AVI7.js → chunk-BCBNVJVG.js} +1 -1
- package/dist-cli/chunks/{chunk-OYMFNU3M.js → chunk-BKBL6K2G.js} +1 -1
- package/dist-cli/chunks/{chunk-IBNRRAES.js → chunk-C3DPQZ4J.js} +2 -2
- package/dist-cli/chunks/chunk-D3ZSBIIY.js +2 -0
- package/dist-cli/chunks/{chunk-2AWQ7OB2.js → chunk-D4HUVLZR.js} +1 -1
- package/dist-cli/chunks/{chunk-F3HP444U.js → chunk-DUUSJDES.js} +1 -1
- package/dist-cli/chunks/{chunk-277XAALA.js → chunk-ELJLF4SG.js} +3 -3
- package/dist-cli/chunks/{chunk-RH4F2TF7.js → chunk-EQ7TFQ2F.js} +1 -1
- package/dist-cli/chunks/{chunk-HNWEELAE.js → chunk-EQCKGC4B.js} +1 -1
- package/dist-cli/chunks/chunk-FUCGLWNN.js +1 -0
- package/dist-cli/chunks/{chunk-FRM355UL.js → chunk-HYPJW65U.js} +2 -2
- package/dist-cli/chunks/chunk-IILJQCZA.js +2 -0
- package/dist-cli/chunks/{chunk-Y4BUVURT.js → chunk-KU6MSPAH.js} +2 -2
- package/dist-cli/chunks/{chunk-DM6WT7QM.js → chunk-OOOR7NT2.js} +1 -1
- package/dist-cli/chunks/{chunk-HAWOAQAG.js → chunk-P7WDNKOS.js} +3 -3
- package/dist-cli/chunks/{chunk-6TNANCQC.js → chunk-PPKKA5VW.js} +2 -2
- package/dist-cli/chunks/{chunk-JQ7ZXOXJ.js → chunk-PS2G44GT.js} +2 -2
- package/dist-cli/chunks/{chunk-ECJBV65H.js → chunk-QMSJR5R2.js} +2 -2
- package/dist-cli/chunks/{chunk-J2GYISVJ.js → chunk-RF4R2U46.js} +2 -2
- package/dist-cli/chunks/{chunk-VMXWC2JO.js → chunk-RIXUH3NK.js} +2 -2
- package/dist-cli/chunks/{chunk-2PY3UZVO.js → chunk-SFGUPL2X.js} +2 -2
- package/dist-cli/chunks/{chunk-572VSFNP.js → chunk-SQX5CAYG.js} +1 -1
- package/dist-cli/chunks/{chunk-NXATOWWF.js → chunk-SQZAC7C4.js} +1 -1
- package/dist-cli/chunks/{chunk-WTKTOL3C.js → chunk-SV7FOGJ3.js} +2 -2
- package/dist-cli/chunks/{chunk-JHJNODXN.js → chunk-TK3OJSEO.js} +2 -2
- package/dist-cli/chunks/{chunk-KASUZ5XV.js → chunk-TL7SIZ7S.js} +1 -1
- package/dist-cli/chunks/{chunk-6XZOEBTZ.js → chunk-V2GQ4WXJ.js} +2 -2
- package/dist-cli/chunks/{chunk-IP3QJLRH.js → chunk-VH7F45CN.js} +1 -1
- package/dist-cli/chunks/chunk-WNVNU2OW.js +4 -0
- package/dist-cli/chunks/{chunk-YUELRHGB.js → chunk-XQ2OBHBE.js} +2 -2
- package/dist-cli/chunks/{chunk-CYV6Y6YV.js → chunk-YCIA4BHJ.js} +2 -2
- package/dist-cli/chunks/chunk-ZSMMJMPA.js +1 -0
- package/dist-cli/chunks/cli-version-QB4VH24H.js +2 -0
- package/dist-cli/chunks/{compat-QLLWBTS3.js → compat-FWSEEGEH.js} +3 -3
- package/dist-cli/chunks/{config-2DSLDCXV.js → config-CYI2WAGP.js} +2 -2
- package/dist-cli/chunks/control-UXY7YQVX.js +2 -0
- package/dist-cli/chunks/{cpu-profile-GEIKHCPC.js → cpu-profile-IKAE3KTY.js} +2 -2
- package/dist-cli/chunks/{daemon-4EBUFN4D.js → daemon-ZUMF53YB.js} +2 -2
- package/dist-cli/chunks/{debug-WGD6XWOF.js → debug-P6KULKKS.js} +3 -3
- package/dist-cli/chunks/{detox-LNKGRZU6.js → detox-SPWAZCYG.js} +2 -2
- package/dist-cli/chunks/{device-AYKXKVIQ.js → device-JWEPK6I2.js} +2 -2
- package/dist-cli/chunks/{diagnose-TMXSDOOC.js → diagnose-IZODTXV2.js} +2 -2
- package/dist-cli/chunks/drivers-MK6WJKBC.js +2 -0
- package/dist-cli/chunks/{electron-QFPF7TBY.js → electron-R5GP6RVB.js} +3 -3
- package/dist-cli/chunks/flow-6O4GEOPJ.js +2 -0
- package/dist-cli/chunks/{hints-MXKRR4TG.js → hints-DYDNYX7N.js} +2 -2
- package/dist-cli/chunks/{home-paths-REMWQDAO.js → home-paths-GLMX5OKL.js} +2 -2
- package/dist-cli/chunks/{inspect-XGSQNFV7.js → inspect-FJOPCTY2.js} +3 -3
- package/dist-cli/chunks/install-A3TUGGHN.js +2 -0
- package/dist-cli/chunks/{install-desktop-NQG3RZSA.js → install-desktop-YPJZMZM5.js} +3 -3
- package/dist-cli/chunks/{keys-5QZWXL3F.js → keys-GSYPHWNY.js} +2 -2
- package/dist-cli/chunks/{launch-SBXOZWKO.js → launch-4G2PKW5X.js} +3 -3
- package/dist-cli/chunks/{login-EACQXE24.js → login-KJQGHA64.js} +4 -4
- package/dist-cli/chunks/{logout-IBQLMUML.js → logout-XM2SYH5C.js} +2 -2
- package/dist-cli/chunks/{maestro-LFYXUX7O.js → maestro-EOWGI7DG.js} +2 -2
- package/dist-cli/chunks/{preview-U4SBOEGQ.js → preview-F73TKK37.js} +2 -2
- package/dist-cli/chunks/{profile-GWS5ECMY.js → profile-22FDKBUO.js} +2 -2
- package/dist-cli/chunks/{react-QDHLMVYL.js → react-5L6VPFUP.js} +2 -2
- package/dist-cli/chunks/{record-BUEUWPDI.js → record-JZXCQ4IN.js} +2 -2
- package/dist-cli/chunks/runtime-EEBX7CFV.js +2 -0
- package/dist-cli/chunks/{runtime-delivery-G7L6RVZ7.js → runtime-delivery-LXUM3R4A.js} +2 -2
- package/dist-cli/chunks/{screenshot-T2HBA3VI.js → screenshot-HDRRG33Q.js} +2 -2
- package/dist-cli/chunks/{screenshot-mode-EG5HMIH3.js → screenshot-mode-WY63LZIX.js} +2 -2
- package/dist-cli/chunks/{screenshots-S52AFHTV.js → screenshots-MPV2ENL5.js} +2 -2
- package/dist-cli/chunks/{server-MFFVYUGG.js → server-5LBMCJ3G.js} +2 -2
- package/dist-cli/chunks/setup-repo-SZSYNKNI.js +2 -0
- package/dist-cli/chunks/{skills-HQGWBS2O.js → skills-BQ73YOBF.js} +2 -2
- package/dist-cli/chunks/{start-E3DRYY7W.js → start-2WU4W6ZU.js} +4 -4
- package/dist-cli/chunks/store-RE45SUBF.js +2 -0
- package/dist-cli/chunks/telemetry-DG6GJLCP.js +2 -0
- package/dist-cli/chunks/{test-ZY3EF62K.js → test-OVO4CQTG.js} +3 -3
- package/dist-cli/chunks/{three-mode-WSPKQCJ5.js → three-mode-BKM3KFM7.js} +2 -2
- package/dist-cli/chunks/{timeline-3XAB5EWZ.js → timeline-MDXGEDQL.js} +2 -2
- package/dist-cli/chunks/{upgrade-WNENPFM5.js → upgrade-JGQABWVF.js} +2 -2
- package/dist-cli/chunks/upload-UJNUA4ZV.js +2 -0
- package/dist-cli/chunks/{web-D2AOZY44.js → web-WYFAYQ72.js} +2 -2
- package/dist-cli/chunks/{what-happened-F43KNSG6.js → what-happened-PZW2KW6A.js} +2 -2
- package/dist-cli/chunks/{whoami-T22VBR7C.js → whoami-7ATWJQS6.js} +2 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +44 -0
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1770 -0
- package/dist-lib/detox/jest-preset.cjs +50 -0
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/demo-app-registry.cjs +809 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
- package/dist-lib/skills.cjs +8322 -0
- package/dist-lib/vite-base.cjs +3 -3
- package/dist-lib/vite.cjs +1 -1
- package/package.json +39 -10
- package/scripts/demo-app-registry.ts +989 -0
- package/scripts/dev-server-scanner.ts +674 -0
- package/src/agent-daemon-client.ts +390 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +572 -0
- package/src/attached-projects.ts +536 -0
- package/src/auth/shared-session.ts +199 -0
- package/src/backend-origin.ts +49 -0
- package/src/beta.ts +21 -0
- package/src/bridge-constants.ts +10 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +30 -0
- package/src/codex-client.ts +215 -0
- package/src/config.ts +110 -0
- package/src/dev-bundle-resolution.ts +180 -0
- package/src/home-paths.ts +382 -0
- package/src/host/agent-host.ts +576 -0
- package/src/host/bridge-host.ts +2293 -0
- package/src/host/fetch-proxy-handler.ts +288 -0
- package/src/host/fetch-proxy-overrides.ts +39 -0
- package/src/host/open-url.ts +234 -0
- package/src/index.ts +9 -0
- package/src/metro-plugin.ts +207 -0
- package/src/native-dev-bundle-url.ts +62 -0
- package/src/native-seam-manifest.ts +313 -0
- package/src/profiles.ts +179 -0
- package/src/render-mode.ts +27 -0
- package/src/runtime-delivery.ts +334 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +438 -0
- package/src/screenshots/orchestrate.ts +244 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/skills/builtin/a11y-review.ts +126 -0
- package/src/skills/builtin/compat-check.ts +104 -0
- package/src/skills/builtin/perf-profile.ts +84 -0
- package/src/skills/builtin/screenshot-all.ts +46 -0
- package/src/skills/builtin/test-flow.ts +118 -0
- package/src/skills/builtin/visual-diff.ts +94 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/vite-plugin-one.ts +187 -0
- package/src/vite-plugin.ts +1381 -0
- package/src/worklets-babel.ts +132 -0
- package/dist-cli/chunks/auto-bootstrap-FQS4ZD2K.js +0 -2
- package/dist-cli/chunks/beta-VG7CDY2U.js +0 -2
- package/dist-cli/chunks/chunk-2OIBDYHW.js +0 -1
- package/dist-cli/chunks/chunk-6BNLVMXA.js +0 -1
- package/dist-cli/chunks/chunk-6XD6CBJM.js +0 -2
- package/dist-cli/chunks/chunk-CHQTO426.js +0 -1
- package/dist-cli/chunks/chunk-FAPYGVIU.js +0 -4
- package/dist-cli/chunks/chunk-PEHFE3LG.js +0 -64
- package/dist-cli/chunks/chunk-RXH2SLKF.js +0 -2
- package/dist-cli/chunks/chunk-UXQWC5ZR.js +0 -79
- package/dist-cli/chunks/chunk-XFQL74PF.js +0 -5
- package/dist-cli/chunks/cli-version-PWF6I6LY.js +0 -2
- package/dist-cli/chunks/control-UIOXGYXU.js +0 -2
- package/dist-cli/chunks/demo-app-registry-G3BDOFWC.js +0 -2
- package/dist-cli/chunks/drivers-IDQF34HP.js +0 -2
- package/dist-cli/chunks/flow-3JN3Y7RF.js +0 -2
- package/dist-cli/chunks/install-2N3YOOSN.js +0 -2
- package/dist-cli/chunks/runtime-PVB4VGUH.js +0 -2
- package/dist-cli/chunks/setup-repo-YOF7NV5D.js +0 -2
- package/dist-cli/chunks/store-MAI6D3UO.js +0 -2
- package/dist-cli/chunks/telemetry-RCQKCJTH.js +0 -2
- package/dist-cli/chunks/upload-YLJ4RA73.js +0 -2
|
@@ -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
|
+
}
|