@phenx-inc/ctlsurf 0.3.8 → 0.3.10
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/bin/ctlsurf-worker.js +46 -43
- package/out/headless/index.mjs +71 -24
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +86 -45
- package/package.json +1 -1
- package/scripts/rebrand-electron.js +195 -0
- package/src/main/headless.ts +6 -0
- package/src/main/index.ts +6 -0
- package/src/main/logger.ts +10 -0
- package/src/main/orchestrator.ts +46 -4
- package/src/main/workerWs.ts +5 -8
package/src/main/orchestrator.ts
CHANGED
|
@@ -8,10 +8,7 @@ import { CtlsurfApi } from './ctlsurfApi'
|
|
|
8
8
|
import { ConversationBridge } from './bridge'
|
|
9
9
|
import { WorkerWsClient, type WorkerWsStatus, type IncomingMessage } from './workerWs'
|
|
10
10
|
import { TimeTracker } from './timeTracker'
|
|
11
|
-
|
|
12
|
-
function log(...args: unknown[]): void {
|
|
13
|
-
try { console.log(...args) } catch { /* EPIPE safe */ }
|
|
14
|
-
}
|
|
11
|
+
import { log } from './logger'
|
|
15
12
|
|
|
16
13
|
// ─── Types ────────────────────────────────────────
|
|
17
14
|
|
|
@@ -66,6 +63,7 @@ const DEFAULT_PROFILES: Record<string, Profile> = {
|
|
|
66
63
|
}
|
|
67
64
|
|
|
68
65
|
const TERM_STREAM_INTERVAL_MS = 50
|
|
66
|
+
const NO_PROJECT_POLL_MS = 5_000
|
|
69
67
|
|
|
70
68
|
export class Orchestrator {
|
|
71
69
|
private settingsDir: string
|
|
@@ -87,6 +85,8 @@ export class Orchestrator {
|
|
|
87
85
|
profiles: { ...DEFAULT_PROFILES },
|
|
88
86
|
logChat: false,
|
|
89
87
|
}
|
|
88
|
+
private noProjectPollTimer: ReturnType<typeof setInterval> | null = null
|
|
89
|
+
private noProjectPollCwd: string | null = null
|
|
90
90
|
|
|
91
91
|
constructor(settingsDir: string, events: OrchestratorEvents) {
|
|
92
92
|
this.settingsDir = settingsDir
|
|
@@ -115,6 +115,11 @@ export class Orchestrator {
|
|
|
115
115
|
events.onWorkerRegistered(data)
|
|
116
116
|
if (!data.folder_id) {
|
|
117
117
|
events.onWorkerStatus('no_project')
|
|
118
|
+
if (this.currentCwd && data.status !== 'pending_approval') {
|
|
119
|
+
this.startNoProjectPolling(this.currentCwd)
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
this.stopNoProjectPolling()
|
|
118
123
|
}
|
|
119
124
|
},
|
|
120
125
|
onTerminalInput: (data: string) => {
|
|
@@ -398,6 +403,7 @@ export class Orchestrator {
|
|
|
398
403
|
if (isCodingAgent(agent)) {
|
|
399
404
|
this.connectWorkerWs(agent, cwd)
|
|
400
405
|
} else {
|
|
406
|
+
this.stopNoProjectPolling()
|
|
401
407
|
this.workerWs.disconnect()
|
|
402
408
|
this.checkProjectStatus(cwd)
|
|
403
409
|
}
|
|
@@ -484,6 +490,7 @@ export class Orchestrator {
|
|
|
484
490
|
return
|
|
485
491
|
}
|
|
486
492
|
|
|
493
|
+
this.stopNoProjectPolling()
|
|
487
494
|
this.workerWs.connect({
|
|
488
495
|
machine: os.hostname(),
|
|
489
496
|
cwd,
|
|
@@ -491,6 +498,40 @@ export class Orchestrator {
|
|
|
491
498
|
})
|
|
492
499
|
}
|
|
493
500
|
|
|
501
|
+
private startNoProjectPolling(cwd: string): void {
|
|
502
|
+
if (this.noProjectPollTimer && this.noProjectPollCwd === cwd) return
|
|
503
|
+
this.stopNoProjectPolling()
|
|
504
|
+
this.noProjectPollCwd = cwd
|
|
505
|
+
log(`[worker-ws] Polling for project folder at ${cwd}`)
|
|
506
|
+
this.noProjectPollTimer = setInterval(() => { void this.checkForProjectFolder(cwd) }, NO_PROJECT_POLL_MS)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private stopNoProjectPolling(): void {
|
|
510
|
+
if (this.noProjectPollTimer) {
|
|
511
|
+
clearInterval(this.noProjectPollTimer)
|
|
512
|
+
this.noProjectPollTimer = null
|
|
513
|
+
this.noProjectPollCwd = null
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private async checkForProjectFolder(cwd: string): Promise<void> {
|
|
518
|
+
if (this.currentCwd !== cwd || !this.currentAgent) {
|
|
519
|
+
this.stopNoProjectPolling()
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
if (!this.ctlsurfApi.getApiKey()) return
|
|
523
|
+
try {
|
|
524
|
+
const folder = await this.ctlsurfApi.findFolderByPath(cwd)
|
|
525
|
+
if (folder?.id && this.currentCwd === cwd && this.currentAgent) {
|
|
526
|
+
log(`[worker-ws] Project folder appeared (${folder.id}); reconnecting`)
|
|
527
|
+
const agent = this.currentAgent
|
|
528
|
+
this.stopNoProjectPolling()
|
|
529
|
+
this.workerWs.disconnect()
|
|
530
|
+
this.connectWorkerWs(agent, cwd)
|
|
531
|
+
}
|
|
532
|
+
} catch { /* ignore — retry on next tick */ }
|
|
533
|
+
}
|
|
534
|
+
|
|
494
535
|
private async checkProjectStatus(cwd: string): Promise<void> {
|
|
495
536
|
if (!this.ctlsurfApi.getApiKey()) {
|
|
496
537
|
this.events.onWorkerStatus('no_project')
|
|
@@ -524,6 +565,7 @@ export class Orchestrator {
|
|
|
524
565
|
// ─── Shutdown ───────────────────────────────────
|
|
525
566
|
|
|
526
567
|
async shutdown(): Promise<void> {
|
|
568
|
+
this.stopNoProjectPolling()
|
|
527
569
|
this.bridge.endSession()
|
|
528
570
|
await this.timeTracker.endAll()
|
|
529
571
|
for (const [, tab] of this.tabs) {
|
package/src/main/workerWs.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import os from 'os'
|
|
2
2
|
import crypto from 'crypto'
|
|
3
3
|
import WsModule from 'ws'
|
|
4
|
+
import { log } from './logger'
|
|
4
5
|
|
|
5
6
|
// Use native WebSocket if available (Node 22+), otherwise fall back to ws package
|
|
6
7
|
const WS: typeof WebSocket = typeof WebSocket !== 'undefined' ? WebSocket : WsModule as any
|
|
7
8
|
|
|
8
|
-
function log(...args: unknown[]): void {
|
|
9
|
-
try { console.log(...args) } catch { /* EPIPE safe */ }
|
|
10
|
-
}
|
|
11
|
-
|
|
12
9
|
const HEARTBEAT_INTERVAL_MS = 30_000
|
|
13
10
|
const RECONNECT_DELAY_MS = 5_000
|
|
14
11
|
const MAX_RECONNECT_DELAY_MS = 60_000
|
|
@@ -225,7 +222,7 @@ export class WorkerWsClient {
|
|
|
225
222
|
case 'registered': {
|
|
226
223
|
this.workerId = data.worker_id as string
|
|
227
224
|
const workerStatus = data.status as string
|
|
228
|
-
|
|
225
|
+
log(`[worker-ws] Registered as ${this.workerId}, status: ${workerStatus}`)
|
|
229
226
|
|
|
230
227
|
if (workerStatus === 'pending_approval') {
|
|
231
228
|
this.setStatus('pending_approval')
|
|
@@ -257,7 +254,7 @@ export class WorkerWsClient {
|
|
|
257
254
|
case 'message': {
|
|
258
255
|
const msg = data.message as IncomingMessage
|
|
259
256
|
if (msg) {
|
|
260
|
-
|
|
257
|
+
log(`[worker-ws] Received message: ${msg.id}`)
|
|
261
258
|
this.events.onMessage(msg)
|
|
262
259
|
}
|
|
263
260
|
break
|
|
@@ -275,7 +272,7 @@ export class WorkerWsClient {
|
|
|
275
272
|
break
|
|
276
273
|
|
|
277
274
|
default:
|
|
278
|
-
|
|
275
|
+
log(`[worker-ws] Unknown message type: ${msgType}`)
|
|
279
276
|
}
|
|
280
277
|
}
|
|
281
278
|
|
|
@@ -302,7 +299,7 @@ export class WorkerWsClient {
|
|
|
302
299
|
|
|
303
300
|
private scheduleReconnect(): void {
|
|
304
301
|
if (!this.shouldReconnect) return
|
|
305
|
-
|
|
302
|
+
log(`[worker-ws] Reconnecting in ${this.reconnectDelay / 1000}s...`)
|
|
306
303
|
this.reconnectTimer = setTimeout(() => {
|
|
307
304
|
this.doConnect()
|
|
308
305
|
}, this.reconnectDelay)
|