@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.
@@ -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) {
@@ -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
- console.log(`[worker-ws] Registered as ${this.workerId}, status: ${workerStatus}`)
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
- console.log(`[worker-ws] Received message: ${msg.id}`)
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
- console.log(`[worker-ws] Unknown message type: ${msgType}`)
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
- console.log(`[worker-ws] Reconnecting in ${this.reconnectDelay / 1000}s...`)
302
+ log(`[worker-ws] Reconnecting in ${this.reconnectDelay / 1000}s...`)
306
303
  this.reconnectTimer = setTimeout(() => {
307
304
  this.doConnect()
308
305
  }, this.reconnectDelay)