@phenx-inc/ctlsurf 0.1.0

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 (133) hide show
  1. package/bin/ctlsurf-worker.js +173 -0
  2. package/electron-vite.config.ts +34 -0
  3. package/out/headless/index.mjs +1364 -0
  4. package/out/headless/index.mjs.map +7 -0
  5. package/out/main/index.js +1131 -0
  6. package/out/preload/index.js +67 -0
  7. package/out/renderer/assets/abap-D5KwWAsZ.js +1399 -0
  8. package/out/renderer/assets/apex-DVGUZ64i.js +331 -0
  9. package/out/renderer/assets/azcli-BEAhqcuE.js +69 -0
  10. package/out/renderer/assets/bat-Bqkp9Cfu.js +101 -0
  11. package/out/renderer/assets/bicep-DIlfshcM.js +110 -0
  12. package/out/renderer/assets/cameligo-CLaaYNMV.js +175 -0
  13. package/out/renderer/assets/clojure-fcgFaMHx.js +762 -0
  14. package/out/renderer/assets/codicon-ngg6Pgfi.ttf +0 -0
  15. package/out/renderer/assets/coffee-CzJ5oEdj.js +233 -0
  16. package/out/renderer/assets/cpp-CcN6f0ik.js +390 -0
  17. package/out/renderer/assets/csharp-BJeIuvde.js +327 -0
  18. package/out/renderer/assets/csp-D_3BK2Wp.js +54 -0
  19. package/out/renderer/assets/css-i3rI3_64.js +186 -0
  20. package/out/renderer/assets/css.worker-umuuUiIb.js +53567 -0
  21. package/out/renderer/assets/cssMode-DL0XItGB.js +208 -0
  22. package/out/renderer/assets/cypher-D0--_GAN.js +264 -0
  23. package/out/renderer/assets/dart-vLMHv35g.js +282 -0
  24. package/out/renderer/assets/dockerfile--oxj0cAH.js +131 -0
  25. package/out/renderer/assets/ecl-CeuUgzaZ.js +457 -0
  26. package/out/renderer/assets/editor.worker-CNgWLVu7.js +13695 -0
  27. package/out/renderer/assets/elixir-eLfY1jWH.js +570 -0
  28. package/out/renderer/assets/flow9-ZSTChSMd.js +143 -0
  29. package/out/renderer/assets/freemarker2-CrOEuDcF.js +995 -0
  30. package/out/renderer/assets/fsharp-D2uoxuLH.js +218 -0
  31. package/out/renderer/assets/go-brnMpFrj.js +219 -0
  32. package/out/renderer/assets/graphql-BeiGgjIU.js +152 -0
  33. package/out/renderer/assets/handlebars-D4QYaBof.js +414 -0
  34. package/out/renderer/assets/hcl-CrX1Es2W.js +184 -0
  35. package/out/renderer/assets/html-B2Dqk2ai.js +303 -0
  36. package/out/renderer/assets/html.worker-BT47iy49.js +29777 -0
  37. package/out/renderer/assets/htmlMode-CdZ0Prhd.js +224 -0
  38. package/out/renderer/assets/index-CJ6RsQWP.css +8108 -0
  39. package/out/renderer/assets/index-pZmE1QXB.js +211777 -0
  40. package/out/renderer/assets/ini-BcQysCTb.js +72 -0
  41. package/out/renderer/assets/java-Dt3iMn2o.js +233 -0
  42. package/out/renderer/assets/javascript-CK8zNQXj.js +72 -0
  43. package/out/renderer/assets/json.worker-D4JVmXIe.js +21424 -0
  44. package/out/renderer/assets/jsonMode-Cewaellc.js +931 -0
  45. package/out/renderer/assets/julia-Cm3ItYL_.js +512 -0
  46. package/out/renderer/assets/kotlin-Ddo1SjA5.js +253 -0
  47. package/out/renderer/assets/less-B7Qaxw-O.js +162 -0
  48. package/out/renderer/assets/lexon-C1U0m2n9.js +158 -0
  49. package/out/renderer/assets/liquid-Bd3GPNs2.js +235 -0
  50. package/out/renderer/assets/lspLanguageFeatures-DSDH7BnA.js +1841 -0
  51. package/out/renderer/assets/lua-hNsuGJkO.js +163 -0
  52. package/out/renderer/assets/m3-6ko6q9-_.js +211 -0
  53. package/out/renderer/assets/markdown-B0YTnTxW.js +230 -0
  54. package/out/renderer/assets/mdx-CCPVCrXC.js +159 -0
  55. package/out/renderer/assets/mips-CJm71dS3.js +199 -0
  56. package/out/renderer/assets/msdax-BBeIktCY.js +376 -0
  57. package/out/renderer/assets/mysql-BWiizXSn.js +879 -0
  58. package/out/renderer/assets/objective-c-B1L1C5EC.js +184 -0
  59. package/out/renderer/assets/pascal-DMQyD4Xk.js +252 -0
  60. package/out/renderer/assets/pascaligo-VA_LQ1oU.js +165 -0
  61. package/out/renderer/assets/perl-DC0Z0tlO.js +627 -0
  62. package/out/renderer/assets/pgsql-DaSGFTLp.js +852 -0
  63. package/out/renderer/assets/php-Bkx1qpkQ.js +501 -0
  64. package/out/renderer/assets/pla-DEV89yYj.js +138 -0
  65. package/out/renderer/assets/postiats-CVVurEnu.js +908 -0
  66. package/out/renderer/assets/powerquery-BQ_t1ZiQ.js +891 -0
  67. package/out/renderer/assets/powershell-BXiKvz7Z.js +240 -0
  68. package/out/renderer/assets/protobuf-CndvAUGu.js +421 -0
  69. package/out/renderer/assets/pug-BxCXwerb.js +403 -0
  70. package/out/renderer/assets/python-34jOtlcC.js +295 -0
  71. package/out/renderer/assets/qsharp-BWK6YLKm.js +302 -0
  72. package/out/renderer/assets/r-CtqYUQ6l.js +244 -0
  73. package/out/renderer/assets/razor-DXRw694z.js +545 -0
  74. package/out/renderer/assets/redis-O7gSt3oh.js +303 -0
  75. package/out/renderer/assets/redshift-CvYMMYZY.js +810 -0
  76. package/out/renderer/assets/restructuredtext-B-KQCVu_.js +175 -0
  77. package/out/renderer/assets/ruby-DCd4DmAr.js +512 -0
  78. package/out/renderer/assets/rust-B1c0VCeq.js +344 -0
  79. package/out/renderer/assets/sb-Chfc_wZF.js +116 -0
  80. package/out/renderer/assets/scala-DbVzH-3O.js +371 -0
  81. package/out/renderer/assets/scheme-D7PxodDG.js +109 -0
  82. package/out/renderer/assets/scss-B42qMyAu.js +261 -0
  83. package/out/renderer/assets/shell-vZEubQ82.js +222 -0
  84. package/out/renderer/assets/solidity-yHOxYChb.js +1368 -0
  85. package/out/renderer/assets/sophia-D7pU0Y1d.js +200 -0
  86. package/out/renderer/assets/sparql-DxuVdnRl.js +202 -0
  87. package/out/renderer/assets/sql-BAGepFCR.js +854 -0
  88. package/out/renderer/assets/st-C-b0Dh53.js +417 -0
  89. package/out/renderer/assets/swift-BmOZGynf.js +313 -0
  90. package/out/renderer/assets/systemverilog-BOC0OOdC.js +577 -0
  91. package/out/renderer/assets/tcl-Bb4GCwBr.js +233 -0
  92. package/out/renderer/assets/ts.worker-C7hW3aY-.js +225330 -0
  93. package/out/renderer/assets/tsMode-CmND5_wB.js +1265 -0
  94. package/out/renderer/assets/twig-DvgEGWAV.js +393 -0
  95. package/out/renderer/assets/typescript-BNNI0Euv.js +337 -0
  96. package/out/renderer/assets/typespec-R77Ln7Jb.js +128 -0
  97. package/out/renderer/assets/vb-Bm6ESA0Q.js +373 -0
  98. package/out/renderer/assets/wgsl-_KPae5vw.js +454 -0
  99. package/out/renderer/assets/xml-CgdndrNB.js +89 -0
  100. package/out/renderer/assets/yaml-DNWPIf1s.js +200 -0
  101. package/out/renderer/index.html +13 -0
  102. package/package.json +67 -0
  103. package/resources/icon.icns +0 -0
  104. package/resources/icon.ico +0 -0
  105. package/resources/icon.png +0 -0
  106. package/src/main/agents.ts +46 -0
  107. package/src/main/bridge.ts +180 -0
  108. package/src/main/ctlsurfApi.ts +142 -0
  109. package/src/main/detectMode.ts +17 -0
  110. package/src/main/headless.ts +182 -0
  111. package/src/main/index.ts +300 -0
  112. package/src/main/orchestrator.ts +404 -0
  113. package/src/main/pty.ts +65 -0
  114. package/src/main/settingsDir.ts +17 -0
  115. package/src/main/tui.ts +366 -0
  116. package/src/main/workerWs.ts +312 -0
  117. package/src/preload/index.ts +114 -0
  118. package/src/renderer/App.tsx +275 -0
  119. package/src/renderer/components/CtlsurfPanel.tsx +49 -0
  120. package/src/renderer/components/EditorPanel.tsx +232 -0
  121. package/src/renderer/components/MultiSplitPane.tsx +251 -0
  122. package/src/renderer/components/PaneLayout.tsx +419 -0
  123. package/src/renderer/components/SettingsDialog.tsx +204 -0
  124. package/src/renderer/components/SplitPane.tsx +82 -0
  125. package/src/renderer/components/StatusBar.tsx +73 -0
  126. package/src/renderer/components/TerminalPanel.tsx +140 -0
  127. package/src/renderer/index.html +12 -0
  128. package/src/renderer/main.tsx +10 -0
  129. package/src/renderer/styles.css +722 -0
  130. package/tsconfig.json +8 -0
  131. package/tsconfig.main.json +15 -0
  132. package/tsconfig.preload.json +14 -0
  133. package/tsconfig.renderer.json +15 -0
@@ -0,0 +1,404 @@
1
+ import path from 'path'
2
+ import fs from 'fs'
3
+ import os from 'os'
4
+
5
+ import { PtyManager } from './pty'
6
+ import { AgentConfig, isCodingAgent } from './agents'
7
+ import { CtlsurfApi } from './ctlsurfApi'
8
+ import { ConversationBridge } from './bridge'
9
+ import { WorkerWsClient, type WorkerWsStatus, type IncomingMessage } from './workerWs'
10
+
11
+ function log(...args: unknown[]): void {
12
+ try { console.log(...args) } catch { /* EPIPE safe */ }
13
+ }
14
+
15
+ // ─── Types ────────────────────────────────────────
16
+
17
+ export interface Profile {
18
+ name: string
19
+ apiKey: string
20
+ baseUrl: string
21
+ dataspacePageId: string
22
+ }
23
+
24
+ export interface SettingsData {
25
+ activeProfile: string
26
+ profiles: Record<string, Profile>
27
+ ctlsurfApiKey?: string
28
+ ctlsurfBaseUrl?: string
29
+ ctlsurfDataspacePageId?: string
30
+ }
31
+
32
+ export interface OrchestratorEvents {
33
+ onPtyData: (data: string) => void
34
+ onPtyExit: (code: number) => void
35
+ onWorkerStatus: (status: string) => void
36
+ onWorkerMessage: (message: IncomingMessage) => void
37
+ onWorkerRegistered: (data: { worker_id: string; folder_id: string | null; status: string }) => void
38
+ onCwdChanged: () => void
39
+ }
40
+
41
+ // ─── Orchestrator ─────────────────────────────────
42
+
43
+ const DEFAULT_PROFILES: Record<string, Profile> = {
44
+ production: {
45
+ name: 'Production',
46
+ apiKey: '',
47
+ baseUrl: 'https://app.ctlsurf.com',
48
+ dataspacePageId: '',
49
+ },
50
+ }
51
+
52
+ const TERM_STREAM_INTERVAL_MS = 50
53
+
54
+ export class Orchestrator {
55
+ private settingsDir: string
56
+ private events: OrchestratorEvents
57
+
58
+ // Core services
59
+ readonly ctlsurfApi = new CtlsurfApi()
60
+ readonly bridge = new ConversationBridge(this.ctlsurfApi)
61
+ readonly workerWs: WorkerWsClient
62
+
63
+ // State
64
+ private ptyManager: PtyManager | null = null
65
+ private currentAgent: AgentConfig | null = null
66
+ private currentCwd: string | null = null
67
+ private settings: SettingsData = {
68
+ activeProfile: 'production',
69
+ profiles: { ...DEFAULT_PROFILES },
70
+ }
71
+
72
+ // Terminal stream batching
73
+ private termStreamBuffer = ''
74
+ private termStreamTimer: ReturnType<typeof setTimeout> | null = null
75
+
76
+ constructor(settingsDir: string, events: OrchestratorEvents) {
77
+ this.settingsDir = settingsDir
78
+ this.events = events
79
+
80
+ this.workerWs = new WorkerWsClient({
81
+ onStatusChange: (status: WorkerWsStatus) => {
82
+ log(`[worker-ws] Status: ${status}`)
83
+ events.onWorkerStatus(status)
84
+ },
85
+ onMessage: (message: IncomingMessage) => {
86
+ log(`[worker-ws] Incoming message: ${message.id} (${message.type})`)
87
+ events.onWorkerMessage(message)
88
+ this.workerWs.sendAck(message.id)
89
+
90
+ if (message.type === 'prompt' || message.type === 'task_dispatch') {
91
+ if (this.ptyManager) {
92
+ this.ptyManager.write(message.content + '\r')
93
+ this.bridge.feedInput(message.content)
94
+ }
95
+ }
96
+ },
97
+ onRegistered: (data) => {
98
+ log(`[worker-ws] Registered: worker_id=${data.worker_id}, folder_id=${data.folder_id}, status=${data.status}`)
99
+ events.onWorkerRegistered(data)
100
+ if (!data.folder_id) {
101
+ events.onWorkerStatus('no_project')
102
+ }
103
+ },
104
+ onTerminalInput: (data: string) => {
105
+ this.ptyManager?.write(data)
106
+ },
107
+ })
108
+ }
109
+
110
+ // ─── Settings ───────────────────────────────────
111
+
112
+ getActiveProfile(): Profile {
113
+ return this.settings.profiles[this.settings.activeProfile] || this.settings.profiles.production || DEFAULT_PROFILES.production
114
+ }
115
+
116
+ get settingsData(): SettingsData {
117
+ return this.settings
118
+ }
119
+
120
+ get cwd(): string | null {
121
+ return this.currentCwd
122
+ }
123
+
124
+ get agent(): AgentConfig | null {
125
+ return this.currentAgent
126
+ }
127
+
128
+ applyProfile(profile: Profile): void {
129
+ const apiKey = profile.apiKey || process.env.CTLSURF_API_KEY || ''
130
+ if (apiKey) {
131
+ this.ctlsurfApi.setApiKey(apiKey)
132
+ this.workerWs.setApiKey(apiKey)
133
+ } else {
134
+ this.ctlsurfApi.setApiKey('')
135
+ this.workerWs.setApiKey(null)
136
+ }
137
+
138
+ const baseUrl = profile.baseUrl || process.env.CTLSURF_BASE_URL || 'https://app.ctlsurf.com'
139
+ this.ctlsurfApi.setBaseUrl(baseUrl)
140
+ this.workerWs.setBaseUrl(baseUrl)
141
+
142
+ log(`[settings] Profile applied: ${profile.name} (${baseUrl})`)
143
+ }
144
+
145
+ loadSettings(): void {
146
+ // Ensure settings dir exists
147
+ try { fs.mkdirSync(this.settingsDir, { recursive: true }) } catch { /* ignore */ }
148
+
149
+ const settingsPath = path.join(this.settingsDir, 'settings.json')
150
+ try {
151
+ if (fs.existsSync(settingsPath)) {
152
+ const raw = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
153
+
154
+ if (!raw.profiles) {
155
+ this.settings = {
156
+ activeProfile: 'production',
157
+ profiles: {
158
+ production: {
159
+ name: 'Production',
160
+ apiKey: raw.ctlsurfApiKey || '',
161
+ baseUrl: raw.ctlsurfBaseUrl || 'https://app.ctlsurf.com',
162
+ dataspacePageId: raw.ctlsurfDataspacePageId || '',
163
+ },
164
+ },
165
+ }
166
+ this.saveSettings()
167
+ log('[settings] Migrated legacy settings to profiles')
168
+ } else {
169
+ this.settings = raw as SettingsData
170
+ if (!this.settings.profiles.production) {
171
+ this.settings.profiles.production = { ...DEFAULT_PROFILES.production }
172
+ }
173
+ }
174
+ }
175
+ } catch {
176
+ this.settings = {
177
+ activeProfile: 'production',
178
+ profiles: { ...DEFAULT_PROFILES },
179
+ }
180
+ }
181
+
182
+ this.applyProfile(this.getActiveProfile())
183
+ }
184
+
185
+ saveSettings(): void {
186
+ const settingsPath = path.join(this.settingsDir, 'settings.json')
187
+ try {
188
+ fs.mkdirSync(this.settingsDir, { recursive: true })
189
+ fs.writeFileSync(settingsPath, JSON.stringify(this.settings, null, 2))
190
+ } catch (err: any) {
191
+ log('[settings] Failed to save:', err.message)
192
+ }
193
+ }
194
+
195
+ overrideApiKey(key: string): void {
196
+ this.ctlsurfApi.setApiKey(key)
197
+ this.workerWs.setApiKey(key)
198
+ }
199
+
200
+ overrideBaseUrl(url: string): void {
201
+ this.ctlsurfApi.setBaseUrl(url)
202
+ this.workerWs.setBaseUrl(url)
203
+ }
204
+
205
+ // ─── Profile CRUD ───────────────────────────────
206
+
207
+ listProfiles() {
208
+ return {
209
+ activeProfile: this.settings.activeProfile,
210
+ profiles: Object.entries(this.settings.profiles).map(([id, p]) => ({
211
+ id,
212
+ name: p.name,
213
+ baseUrl: p.baseUrl,
214
+ hasApiKey: !!p.apiKey,
215
+ dataspacePageId: p.dataspacePageId || null,
216
+ })),
217
+ }
218
+ }
219
+
220
+ getProfile(profileId: string) {
221
+ const p = this.settings.profiles[profileId]
222
+ if (!p) return null
223
+ return {
224
+ id: profileId,
225
+ name: p.name,
226
+ baseUrl: p.baseUrl,
227
+ hasApiKey: !!p.apiKey,
228
+ dataspacePageId: p.dataspacePageId || '',
229
+ }
230
+ }
231
+
232
+ saveProfile(profileId: string, data: { name: string; apiKey?: string; baseUrl: string; dataspacePageId: string }) {
233
+ const existing = this.settings.profiles[profileId]
234
+ this.settings.profiles[profileId] = {
235
+ name: data.name,
236
+ apiKey: data.apiKey !== undefined ? data.apiKey : (existing?.apiKey || ''),
237
+ baseUrl: data.baseUrl || 'https://app.ctlsurf.com',
238
+ dataspacePageId: data.dataspacePageId || '',
239
+ }
240
+ this.saveSettings()
241
+
242
+ if (profileId === this.settings.activeProfile) {
243
+ this.applyProfile(this.settings.profiles[profileId])
244
+ if (this.currentAgent && this.currentCwd) {
245
+ this.workerWs.disconnect()
246
+ this.connectWorkerWs(this.currentAgent, this.currentCwd)
247
+ }
248
+ }
249
+ }
250
+
251
+ switchProfile(profileId: string): { ok: boolean; error?: string } {
252
+ if (!this.settings.profiles[profileId]) return { ok: false, error: 'Profile not found' }
253
+ this.workerWs.disconnect()
254
+ this.settings.activeProfile = profileId
255
+ this.saveSettings()
256
+ this.applyProfile(this.getActiveProfile())
257
+ if (this.currentAgent && this.currentCwd) {
258
+ this.connectWorkerWs(this.currentAgent, this.currentCwd)
259
+ }
260
+ return { ok: true }
261
+ }
262
+
263
+ deleteProfile(profileId: string): { ok: boolean; error?: string } {
264
+ if (profileId === 'production') return { ok: false, error: 'Cannot delete Production profile' }
265
+ if (!this.settings.profiles[profileId]) return { ok: false, error: 'Profile not found' }
266
+
267
+ if (this.settings.activeProfile === profileId) {
268
+ this.workerWs.disconnect()
269
+ this.settings.activeProfile = 'production'
270
+ this.applyProfile(this.getActiveProfile())
271
+ if (this.currentAgent && this.currentCwd) {
272
+ this.connectWorkerWs(this.currentAgent, this.currentCwd)
273
+ }
274
+ }
275
+
276
+ delete this.settings.profiles[profileId]
277
+ this.saveSettings()
278
+ return { ok: true }
279
+ }
280
+
281
+ // ─── PTY & Agent ────────────────────────────────
282
+
283
+ async spawnAgent(agent: AgentConfig, cwd: string): Promise<void> {
284
+ if (this.ptyManager) {
285
+ await this.bridge.endSession()
286
+ this.ptyManager.kill()
287
+ }
288
+
289
+ this.currentAgent = agent
290
+ const prevCwd = this.currentCwd
291
+ this.currentCwd = cwd
292
+ if (prevCwd !== cwd) {
293
+ this.events.onCwdChanged()
294
+ }
295
+
296
+ this.ptyManager = new PtyManager(agent, cwd)
297
+
298
+ this.ptyManager.onData((data: string) => {
299
+ this.events.onPtyData(data)
300
+ this.bridge.feedOutput(data)
301
+ this.streamTerminalData(data)
302
+ })
303
+
304
+ const thisPtyManager = this.ptyManager
305
+
306
+ this.ptyManager.onExit(async (exitCode: number) => {
307
+ this.events.onPtyExit(exitCode)
308
+ await this.bridge.endSession(exitCode)
309
+ if (thisPtyManager === this.ptyManager && this.currentAgent && isCodingAgent(this.currentAgent)) {
310
+ this.workerWs.disconnect()
311
+ }
312
+ })
313
+
314
+ const profile = this.getActiveProfile()
315
+ const dataspacePageId = profile.dataspacePageId || process.env.CTLSURF_DATASPACE_PAGE_ID || ''
316
+ if (dataspacePageId && this.ctlsurfApi.getApiKey()) {
317
+ await this.bridge.startSession(dataspacePageId, agent.name, cwd)
318
+ }
319
+
320
+ if (isCodingAgent(agent)) {
321
+ this.connectWorkerWs(agent, cwd)
322
+ } else {
323
+ this.workerWs.disconnect()
324
+ this.checkProjectStatus(cwd)
325
+ }
326
+ }
327
+
328
+ writePty(data: string): void {
329
+ this.ptyManager?.write(data)
330
+ this.bridge.feedInput(data)
331
+ }
332
+
333
+ resizePty(cols: number, rows: number): void {
334
+ this.ptyManager?.resize(cols, rows)
335
+ this.workerWs.sendTerminalResize(cols, rows)
336
+ }
337
+
338
+ async killAgent(): Promise<void> {
339
+ await this.bridge.endSession()
340
+ this.ptyManager?.kill()
341
+ this.ptyManager = null
342
+ if (this.currentAgent && isCodingAgent(this.currentAgent)) {
343
+ this.workerWs.disconnect()
344
+ }
345
+ }
346
+
347
+ // ─── Worker WebSocket ───────────────────────────
348
+
349
+ connectWorkerWs(agent: AgentConfig, cwd: string): void {
350
+ const profile = this.getActiveProfile()
351
+ const apiKey = profile.apiKey || process.env.CTLSURF_API_KEY
352
+ if (!apiKey) {
353
+ log('[worker-ws] No API key, skipping WS connect')
354
+ return
355
+ }
356
+
357
+ this.workerWs.connect({
358
+ machine: os.hostname(),
359
+ cwd,
360
+ agent: agent.name,
361
+ })
362
+ }
363
+
364
+ private async checkProjectStatus(cwd: string): Promise<void> {
365
+ if (!this.ctlsurfApi.getApiKey()) {
366
+ this.events.onWorkerStatus('no_project')
367
+ return
368
+ }
369
+ try {
370
+ const folder = await this.ctlsurfApi.findFolderByPath(cwd)
371
+ if (!folder?.id) {
372
+ this.events.onWorkerStatus('no_project')
373
+ }
374
+ } catch {
375
+ this.events.onWorkerStatus('no_project')
376
+ }
377
+ }
378
+
379
+ private streamTerminalData(data: string): void {
380
+ this.termStreamBuffer += data
381
+ if (!this.termStreamTimer) {
382
+ this.termStreamTimer = setTimeout(() => {
383
+ if (this.termStreamBuffer) {
384
+ this.workerWs.sendTerminalData(this.termStreamBuffer)
385
+ this.termStreamBuffer = ''
386
+ }
387
+ this.termStreamTimer = null
388
+ }, TERM_STREAM_INTERVAL_MS)
389
+ }
390
+ }
391
+
392
+ // ─── Shutdown ───────────────────────────────────
393
+
394
+ async shutdown(): Promise<void> {
395
+ await this.bridge.endSession()
396
+ this.ptyManager?.kill()
397
+ this.ptyManager = null
398
+ this.workerWs.disconnect()
399
+ if (this.termStreamTimer) {
400
+ clearTimeout(this.termStreamTimer)
401
+ this.termStreamTimer = null
402
+ }
403
+ }
404
+ }
@@ -0,0 +1,65 @@
1
+ import { createRequire } from 'module'
2
+ import { AgentConfig } from './agents'
3
+
4
+ // Use createRequire to load native module at runtime, bypassing bundler
5
+ const require = createRequire(import.meta.url)
6
+ const pty = require('node-pty')
7
+
8
+ export class PtyManager {
9
+ private process: any | null = null
10
+ private dataCallbacks: ((data: string) => void)[] = []
11
+ private exitCallbacks: ((code: number) => void)[] = []
12
+
13
+ constructor(agent: AgentConfig, cwd: string) {
14
+ const shell = agent.command
15
+ const args = agent.args || []
16
+
17
+ try {
18
+ console.log(`[pty] Spawning: ${shell} ${args.join(' ')} in ${cwd}`)
19
+ } catch {
20
+ // Ignore EPIPE errors when stdout is closed
21
+ }
22
+
23
+ this.process = pty.spawn(shell, args, {
24
+ name: 'xterm-256color',
25
+ cwd,
26
+ env: process.env as Record<string, string>,
27
+ cols: 80,
28
+ rows: 24
29
+ })
30
+
31
+ this.process.onData((data: string) => {
32
+ for (const cb of this.dataCallbacks) {
33
+ cb(data)
34
+ }
35
+ })
36
+
37
+ this.process.onExit(({ exitCode }: { exitCode: number }) => {
38
+ for (const cb of this.exitCallbacks) {
39
+ cb(exitCode)
40
+ }
41
+ this.process = null
42
+ })
43
+ }
44
+
45
+ write(data: string): void {
46
+ this.process?.write(data)
47
+ }
48
+
49
+ resize(cols: number, rows: number): void {
50
+ this.process?.resize(cols, rows)
51
+ }
52
+
53
+ kill(): void {
54
+ this.process?.kill()
55
+ this.process = null
56
+ }
57
+
58
+ onData(cb: (data: string) => void): void {
59
+ this.dataCallbacks.push(cb)
60
+ }
61
+
62
+ onExit(cb: (code: number) => void): void {
63
+ this.exitCallbacks.push(cb)
64
+ }
65
+ }
@@ -0,0 +1,17 @@
1
+ import path from 'path'
2
+ import os from 'os'
3
+
4
+ export function getSettingsDir(useElectron: boolean): string {
5
+ if (useElectron) {
6
+ const { app } = require('electron')
7
+ return app.getPath('userData')
8
+ }
9
+
10
+ if (process.platform === 'darwin') {
11
+ return path.join(os.homedir(), 'Library', 'Application Support', 'ctlsurf-worker')
12
+ }
13
+ return path.join(
14
+ process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'),
15
+ 'ctlsurf-worker'
16
+ )
17
+ }