@phenx-inc/ctlsurf 0.3.9 → 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/out/main/index.js CHANGED
@@ -9902,13 +9902,13 @@ function requireWebsocketServer() {
9902
9902
  return websocketServer;
9903
9903
  }
9904
9904
  requireWebsocketServer();
9905
- const WS = typeof WebSocket !== "undefined" ? WebSocket : WebSocket$1;
9906
- function log$3(...args) {
9905
+ function log$2(...args) {
9907
9906
  try {
9908
9907
  console.log(...args);
9909
9908
  } catch {
9910
9909
  }
9911
9910
  }
9911
+ const WS = typeof WebSocket !== "undefined" ? WebSocket : WebSocket$1;
9912
9912
  const HEARTBEAT_INTERVAL_MS = 3e4;
9913
9913
  const RECONNECT_DELAY_MS = 5e3;
9914
9914
  const MAX_RECONNECT_DELAY_MS = 6e4;
@@ -9999,7 +9999,7 @@ class WorkerWsClient {
9999
9999
  }
10000
10000
  doConnect() {
10001
10001
  if (!this.apiKey || !this.registration) {
10002
- log$3("[worker-ws] No API key or registration, skipping connect");
10002
+ log$2("[worker-ws] No API key or registration, skipping connect");
10003
10003
  return;
10004
10004
  }
10005
10005
  this.clearTimers();
@@ -10022,22 +10022,22 @@ class WorkerWsClient {
10022
10022
  doConnectNow() {
10023
10023
  if (!this.apiKey || !this.registration) return;
10024
10024
  if (!this.shouldReconnect) {
10025
- log$3("[worker-ws] shouldReconnect is false, aborting connect");
10025
+ log$2("[worker-ws] shouldReconnect is false, aborting connect");
10026
10026
  return;
10027
10027
  }
10028
10028
  this.setStatus("connecting");
10029
10029
  const wsBase = this.baseUrl.replace(/^http/, "ws");
10030
10030
  const url = `${wsBase}/api/ws/worker?token=${encodeURIComponent(this.apiKey)}`;
10031
- log$3(`[worker-ws] Connecting to ${url.replace(/token=.*/, "token=***")}...`);
10031
+ log$2(`[worker-ws] Connecting to ${url.replace(/token=.*/, "token=***")}...`);
10032
10032
  try {
10033
10033
  this.ws = new WS(url);
10034
10034
  } catch (err) {
10035
- log$3("[worker-ws] Failed to create WebSocket:", err);
10035
+ log$2("[worker-ws] Failed to create WebSocket:", err);
10036
10036
  this.scheduleReconnect();
10037
10037
  return;
10038
10038
  }
10039
10039
  this.ws.onopen = () => {
10040
- log$3("[worker-ws] Connected, sending register");
10040
+ log$2("[worker-ws] Connected, sending register");
10041
10041
  this.reconnectDelay = RECONNECT_DELAY_MS;
10042
10042
  this.send({
10043
10043
  type: "register",
@@ -10050,11 +10050,11 @@ class WorkerWsClient {
10050
10050
  const data = JSON.parse(String(event.data));
10051
10051
  this.handleMessage(data);
10052
10052
  } catch (err) {
10053
- log$3("[worker-ws] Failed to parse message:", err);
10053
+ log$2("[worker-ws] Failed to parse message:", err);
10054
10054
  }
10055
10055
  };
10056
10056
  this.ws.onclose = (event) => {
10057
- log$3(`[worker-ws] Disconnected: ${event.code} ${event.reason}`);
10057
+ log$2(`[worker-ws] Disconnected: ${event.code} ${event.reason}`);
10058
10058
  this.ws = null;
10059
10059
  this.clearHeartbeat();
10060
10060
  this.setStatus("disconnected");
@@ -10063,7 +10063,7 @@ class WorkerWsClient {
10063
10063
  }
10064
10064
  };
10065
10065
  this.ws.onerror = () => {
10066
- log$3("[worker-ws] WebSocket error");
10066
+ log$2("[worker-ws] WebSocket error");
10067
10067
  };
10068
10068
  }
10069
10069
  handleMessage(data) {
@@ -10072,7 +10072,7 @@ class WorkerWsClient {
10072
10072
  case "registered": {
10073
10073
  this.workerId = data.worker_id;
10074
10074
  const workerStatus = data.status;
10075
- console.log(`[worker-ws] Registered as ${this.workerId}, status: ${workerStatus}`);
10075
+ log$2(`[worker-ws] Registered as ${this.workerId}, status: ${workerStatus}`);
10076
10076
  if (workerStatus === "pending_approval") {
10077
10077
  this.setStatus("pending_approval");
10078
10078
  } else {
@@ -10091,14 +10091,14 @@ class WorkerWsClient {
10091
10091
  break;
10092
10092
  }
10093
10093
  case "approved": {
10094
- log$3("[worker-ws] Worker approved!");
10094
+ log$2("[worker-ws] Worker approved!");
10095
10095
  this.setStatus("connected");
10096
10096
  break;
10097
10097
  }
10098
10098
  case "message": {
10099
10099
  const msg = data.message;
10100
10100
  if (msg) {
10101
- console.log(`[worker-ws] Received message: ${msg.id}`);
10101
+ log$2(`[worker-ws] Received message: ${msg.id}`);
10102
10102
  this.events.onMessage(msg);
10103
10103
  }
10104
10104
  break;
@@ -10113,7 +10113,7 @@ class WorkerWsClient {
10113
10113
  case "heartbeat_ack":
10114
10114
  break;
10115
10115
  default:
10116
- console.log(`[worker-ws] Unknown message type: ${msgType}`);
10116
+ log$2(`[worker-ws] Unknown message type: ${msgType}`);
10117
10117
  }
10118
10118
  }
10119
10119
  send(data) {
@@ -10135,7 +10135,7 @@ class WorkerWsClient {
10135
10135
  }
10136
10136
  scheduleReconnect() {
10137
10137
  if (!this.shouldReconnect) return;
10138
- console.log(`[worker-ws] Reconnecting in ${this.reconnectDelay / 1e3}s...`);
10138
+ log$2(`[worker-ws] Reconnecting in ${this.reconnectDelay / 1e3}s...`);
10139
10139
  this.reconnectTimer = setTimeout(() => {
10140
10140
  this.doConnect();
10141
10141
  }, this.reconnectDelay);
@@ -10178,7 +10178,7 @@ function isSameLocalDay(a, b2) {
10178
10178
  const db = new Date(b2);
10179
10179
  return da.getFullYear() === db.getFullYear() && da.getMonth() === db.getMonth() && da.getDate() === db.getDate();
10180
10180
  }
10181
- function log$2(...args) {
10181
+ function log$1(...args) {
10182
10182
  try {
10183
10183
  console.log("[time-tracker]", ...args);
10184
10184
  } catch {
@@ -10234,7 +10234,7 @@ class TimeTracker {
10234
10234
  }
10235
10235
  }, FIRST_CHECKPOINT_DELAY_MS);
10236
10236
  const pending = !state.blockId || !state.rowId;
10237
- log$2(`Started tracking tab=${tabId} agent="${agentName}" cwd=${cwd}${pending ? " (pending datastore — will retry on each checkpoint)" : ""}`);
10237
+ log$1(`Started tracking tab=${tabId} agent="${agentName}" cwd=${cwd}${pending ? " (pending datastore — will retry on each checkpoint)" : ""}`);
10238
10238
  }
10239
10239
  /** Attempts to locate (or create) the datastore + add the session row.
10240
10240
  * Returns true once the session is resolved (blockId + rowId set).
@@ -10258,15 +10258,15 @@ class TimeTracker {
10258
10258
  });
10259
10259
  const rowId = row?.id;
10260
10260
  if (!rowId) {
10261
- log$2(`addRow returned no id for tab=${tabId}; will retry on next checkpoint`);
10261
+ log$1(`addRow returned no id for tab=${tabId}; will retry on next checkpoint`);
10262
10262
  return false;
10263
10263
  }
10264
10264
  s.blockId = blockId;
10265
10265
  s.rowId = rowId;
10266
- log$2(`Resolved datastore for tab=${tabId} (cwd=${s.cwd})`);
10266
+ log$1(`Resolved datastore for tab=${tabId} (cwd=${s.cwd})`);
10267
10267
  return true;
10268
10268
  } catch (err) {
10269
- log$2(`tryResolve failed for tab=${tabId}: ${err?.message || err}`);
10269
+ log$1(`tryResolve failed for tab=${tabId}: ${err?.message || err}`);
10270
10270
  return false;
10271
10271
  }
10272
10272
  }
@@ -10294,12 +10294,12 @@ class TimeTracker {
10294
10294
  if (!s || s.ended) return;
10295
10295
  this.rollingOver.add(tabId);
10296
10296
  const { cwd, agentName, idleTimeoutMin } = s;
10297
- log$2(`Day rolled over for tab=${tabId}; ending session and starting fresh`);
10297
+ log$1(`Day rolled over for tab=${tabId}; ending session and starting fresh`);
10298
10298
  try {
10299
10299
  await this.endSession(tabId);
10300
10300
  await this.startSession(tabId, cwd, agentName, idleTimeoutMin);
10301
10301
  } catch (err) {
10302
- log$2(`rollover failed: ${err?.message || err}`);
10302
+ log$1(`rollover failed: ${err?.message || err}`);
10303
10303
  } finally {
10304
10304
  this.rollingOver.delete(tabId);
10305
10305
  }
@@ -10316,10 +10316,10 @@ class TimeTracker {
10316
10316
  if (s.blockId && s.rowId) {
10317
10317
  await this.writeRow(s, Date.now());
10318
10318
  } else {
10319
- log$2(`endSession for tab=${tabId}: never resolved datastore; ${Math.round(s.activeMs / 6e4)}min not recorded`);
10319
+ log$1(`endSession for tab=${tabId}: never resolved datastore; ${Math.round(s.activeMs / 6e4)}min not recorded`);
10320
10320
  }
10321
10321
  } catch (err) {
10322
- log$2(`endSession write failed: ${err?.message || err}`);
10322
+ log$1(`endSession write failed: ${err?.message || err}`);
10323
10323
  }
10324
10324
  s.ended = true;
10325
10325
  this.sessions.delete(tabId);
@@ -10337,12 +10337,12 @@ class TimeTracker {
10337
10337
  try {
10338
10338
  await this.writeRow(s, Date.now());
10339
10339
  } catch (err) {
10340
- log$2(`checkpoint failed: ${err?.message || err}; retrying in 2s`);
10340
+ log$1(`checkpoint failed: ${err?.message || err}; retrying in 2s`);
10341
10341
  setTimeout(() => {
10342
10342
  const live = this.sessions.get(tabId);
10343
10343
  if (!live || live.ended || !live.blockId || !live.rowId) return;
10344
10344
  this.writeRow(live, Date.now()).catch((err2) => {
10345
- log$2(`checkpoint retry failed: ${err2?.message || err2}`);
10345
+ log$1(`checkpoint retry failed: ${err2?.message || err2}`);
10346
10346
  });
10347
10347
  }, 2e3);
10348
10348
  }
@@ -10381,7 +10381,7 @@ class TimeTracker {
10381
10381
  props: { columns, system_key: SYSTEM_KEY }
10382
10382
  });
10383
10383
  if (created?.id) {
10384
- log$2(`Created "${DATASTORE_TITLE}" datastore on Agent Datastore page for ${cwd}`);
10384
+ log$1(`Created "${DATASTORE_TITLE}" datastore on Agent Datastore page for ${cwd}`);
10385
10385
  this.blockCache.set(cwd, created.id);
10386
10386
  return created.id;
10387
10387
  }
@@ -10408,7 +10408,7 @@ class TimeTracker {
10408
10408
  titleFallbackProps = props;
10409
10409
  }
10410
10410
  } catch (err) {
10411
- log$2(`getBlock(${s.id}) failed during lookup: ${err?.message || err}`);
10411
+ log$1(`getBlock(${s.id}) failed during lookup: ${err?.message || err}`);
10412
10412
  }
10413
10413
  }
10414
10414
  if (titleFallbackId) {
@@ -10416,9 +10416,9 @@ class TimeTracker {
10416
10416
  await this.api.updateBlock(titleFallbackId, {
10417
10417
  props: { ...titleFallbackProps || {}, system_key: SYSTEM_KEY }
10418
10418
  });
10419
- log$2(`Backfilled system_key on legacy Time Tracking block ${titleFallbackId}`);
10419
+ log$1(`Backfilled system_key on legacy Time Tracking block ${titleFallbackId}`);
10420
10420
  } catch (err) {
10421
- log$2(`backfill system_key failed on ${titleFallbackId}: ${err?.message || err}`);
10421
+ log$1(`backfill system_key failed on ${titleFallbackId}: ${err?.message || err}`);
10422
10422
  }
10423
10423
  return titleFallbackId;
10424
10424
  }
@@ -10441,18 +10441,12 @@ class TimeTracker {
10441
10441
  });
10442
10442
  const merged = [...existingCols, ...appended];
10443
10443
  await this.api.updateDatastoreSchema(blockId, merged);
10444
- log$2(`Added ${missing.length} missing column(s) to existing Time Tracking datastore: ${missing.map((c) => c.name).join(", ")}`);
10444
+ log$1(`Added ${missing.length} missing column(s) to existing Time Tracking datastore: ${missing.map((c) => c.name).join(", ")}`);
10445
10445
  } catch (err) {
10446
- log$2(`ensureColumns failed: ${err?.message || err}`);
10446
+ log$1(`ensureColumns failed: ${err?.message || err}`);
10447
10447
  }
10448
10448
  }
10449
10449
  }
10450
- function log$1(...args) {
10451
- try {
10452
- console.log(...args);
10453
- } catch {
10454
- }
10455
- }
10456
10450
  const DEFAULT_IDLE_TIMEOUT_MIN = 15;
10457
10451
  const DEFAULT_PROFILES = {
10458
10452
  production: {
@@ -10491,11 +10485,11 @@ class Orchestrator {
10491
10485
  this.events = events;
10492
10486
  this.workerWs = new WorkerWsClient({
10493
10487
  onStatusChange: (status) => {
10494
- log$1(`[worker-ws] Status: ${status}`);
10488
+ log$2(`[worker-ws] Status: ${status}`);
10495
10489
  events.onWorkerStatus(status);
10496
10490
  },
10497
10491
  onMessage: (message) => {
10498
- log$1(`[worker-ws] Incoming message: ${message.id} (${message.type})`);
10492
+ log$2(`[worker-ws] Incoming message: ${message.id} (${message.type})`);
10499
10493
  events.onWorkerMessage(message);
10500
10494
  this.workerWs.sendAck(message.id);
10501
10495
  if (message.type === "prompt" || message.type === "task_dispatch") {
@@ -10507,7 +10501,7 @@ class Orchestrator {
10507
10501
  }
10508
10502
  },
10509
10503
  onRegistered: (data) => {
10510
- log$1(`[worker-ws] Registered: worker_id=${data.worker_id}, folder_id=${data.folder_id}, status=${data.status}`);
10504
+ log$2(`[worker-ws] Registered: worker_id=${data.worker_id}, folder_id=${data.folder_id}, status=${data.status}`);
10511
10505
  events.onWorkerRegistered(data);
10512
10506
  if (!data.folder_id) {
10513
10507
  events.onWorkerStatus("no_project");
@@ -10565,7 +10559,7 @@ class Orchestrator {
10565
10559
  const baseUrl = profile.baseUrl || process.env.CTLSURF_BASE_URL || "https://app.ctlsurf.com";
10566
10560
  this.ctlsurfApi.setBaseUrl(baseUrl);
10567
10561
  this.workerWs.setBaseUrl(baseUrl);
10568
- log$1(`[settings] Profile applied: ${profile.name} (${baseUrl})`);
10562
+ log$2(`[settings] Profile applied: ${profile.name} (${baseUrl})`);
10569
10563
  }
10570
10564
  loadSettings() {
10571
10565
  try {
@@ -10590,7 +10584,7 @@ class Orchestrator {
10590
10584
  logChat: !!raw.logChat
10591
10585
  };
10592
10586
  this.saveSettings();
10593
- log$1("[settings] Migrated legacy settings to profiles");
10587
+ log$2("[settings] Migrated legacy settings to profiles");
10594
10588
  } else {
10595
10589
  this.settings = raw;
10596
10590
  if (!this.settings.profiles.production) {
@@ -10617,7 +10611,7 @@ class Orchestrator {
10617
10611
  fs.mkdirSync(this.settingsDir, { recursive: true });
10618
10612
  fs.writeFileSync(settingsPath, JSON.stringify(this.settings, null, 2));
10619
10613
  } catch (err) {
10620
- log$1("[settings] Failed to save:", err.message);
10614
+ log$2("[settings] Failed to save:", err.message);
10621
10615
  }
10622
10616
  }
10623
10617
  overrideApiKey(key) {
@@ -10827,7 +10821,7 @@ class Orchestrator {
10827
10821
  const profile = this.getActiveProfile();
10828
10822
  const apiKey = profile.apiKey || process.env.CTLSURF_API_KEY;
10829
10823
  if (!apiKey) {
10830
- log$1("[worker-ws] No API key, skipping WS connect");
10824
+ log$2("[worker-ws] No API key, skipping WS connect");
10831
10825
  return;
10832
10826
  }
10833
10827
  this.stopNoProjectPolling();
@@ -10841,7 +10835,7 @@ class Orchestrator {
10841
10835
  if (this.noProjectPollTimer && this.noProjectPollCwd === cwd) return;
10842
10836
  this.stopNoProjectPolling();
10843
10837
  this.noProjectPollCwd = cwd;
10844
- log$1(`[worker-ws] Polling for project folder at ${cwd}`);
10838
+ log$2(`[worker-ws] Polling for project folder at ${cwd}`);
10845
10839
  this.noProjectPollTimer = setInterval(() => {
10846
10840
  void this.checkForProjectFolder(cwd);
10847
10841
  }, NO_PROJECT_POLL_MS);
@@ -10862,7 +10856,7 @@ class Orchestrator {
10862
10856
  try {
10863
10857
  const folder = await this.ctlsurfApi.findFolderByPath(cwd);
10864
10858
  if (folder?.id && this.currentCwd === cwd && this.currentAgent) {
10865
- log$1(`[worker-ws] Project folder appeared (${folder.id}); reconnecting`);
10859
+ log$2(`[worker-ws] Project folder appeared (${folder.id}); reconnecting`);
10866
10860
  const agent = this.currentAgent;
10867
10861
  this.stopNoProjectPolling();
10868
10862
  this.workerWs.disconnect();
@@ -10912,6 +10906,8 @@ class Orchestrator {
10912
10906
  this.workerWs.disconnect();
10913
10907
  }
10914
10908
  }
10909
+ electron.app.setName("ctlsurf");
10910
+ process.title = "ctlsurf";
10915
10911
  process.stdout?.on?.("error", () => {
10916
10912
  });
10917
10913
  process.stderr?.on?.("error", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phenx-inc/ctlsurf",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Agent-agnostic terminal and desktop app for ctlsurf — run Claude Code, Codex, or any coding agent with live session logging and remote control",
5
5
  "main": "out/main/index.js",
6
6
  "bin": {
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Rebrand the Electron binary inside node_modules so the app shows up as
5
+ * "ctlsurf" (with our icon) in macOS Activity Monitor / Windows Task Manager
6
+ * instead of "Electron".
7
+ *
8
+ * Idempotent: writes a sentinel file and skips if already done.
9
+ * Re-runs automatically after `npm update` (which wipes node_modules/electron).
10
+ */
11
+
12
+ const fs = require('fs')
13
+ const path = require('path')
14
+ const { execFileSync } = require('child_process')
15
+
16
+ const ROOT = path.resolve(__dirname, '..')
17
+ const PRODUCT_NAME = 'ctlsurf'
18
+ const ELECTRON_DIR = path.join(ROOT, 'node_modules', 'electron')
19
+ const SENTINEL = path.join(ELECTRON_DIR, '.ctlsurf-rebranded')
20
+
21
+ function log(msg) {
22
+ if (!process.env.CTLSURF_QUIET) console.log(`[ctlsurf:rebrand] ${msg}`)
23
+ }
24
+
25
+ function rebrand() {
26
+ if (!fs.existsSync(ELECTRON_DIR)) return false
27
+
28
+ if (fs.existsSync(SENTINEL)) return true
29
+
30
+ let pathTxtPath = path.join(ELECTRON_DIR, 'path.txt')
31
+ if (!fs.existsSync(pathTxtPath)) return false
32
+
33
+ const relBin = fs.readFileSync(pathTxtPath, 'utf-8').trim()
34
+ const electronBinary = path.join(ELECTRON_DIR, relBin)
35
+
36
+ try {
37
+ if (process.platform === 'darwin') {
38
+ rebrandDarwin(electronBinary, pathTxtPath)
39
+ } else if (process.platform === 'linux') {
40
+ rebrandLinux(electronBinary, pathTxtPath)
41
+ } else if (process.platform === 'win32') {
42
+ rebrandWin32(electronBinary, pathTxtPath)
43
+ } else {
44
+ return false
45
+ }
46
+ } catch (err) {
47
+ log(`failed: ${err.message}`)
48
+ return false
49
+ }
50
+
51
+ fs.writeFileSync(SENTINEL, new Date().toISOString())
52
+ log('done')
53
+ return true
54
+ }
55
+
56
+ // ─── macOS ──────────────────────────────────────────
57
+
58
+ function rebrandDarwin(electronBinary, pathTxtPath) {
59
+ // electronBinary = .../node_modules/electron/dist/Electron.app/Contents/MacOS/Electron
60
+ const macosDir = path.dirname(electronBinary)
61
+ const contentsDir = path.dirname(macosDir)
62
+ const appDir = path.dirname(contentsDir)
63
+ const distDir = path.dirname(appDir)
64
+ const newAppDir = path.join(distDir, `${PRODUCT_NAME}.app`)
65
+
66
+ // 1) Rename main executable inside MacOS/
67
+ const newBinaryPath = path.join(macosDir, PRODUCT_NAME)
68
+ if (fs.existsSync(electronBinary) && electronBinary !== newBinaryPath) {
69
+ fs.renameSync(electronBinary, newBinaryPath)
70
+ }
71
+
72
+ // 2) Replace icon
73
+ const ourIcon = path.join(ROOT, 'resources', 'icon.icns')
74
+ const electronIcon = path.join(contentsDir, 'Resources', 'electron.icns')
75
+ if (fs.existsSync(ourIcon) && fs.existsSync(electronIcon)) {
76
+ fs.copyFileSync(ourIcon, electronIcon)
77
+ }
78
+
79
+ // 3) Patch main Info.plist
80
+ patchInfoPlist(path.join(contentsDir, 'Info.plist'), {
81
+ CFBundleName: PRODUCT_NAME,
82
+ CFBundleDisplayName: PRODUCT_NAME,
83
+ CFBundleExecutable: PRODUCT_NAME,
84
+ })
85
+
86
+ // 4) Rebrand helper apps so renderer/GPU subprocesses also show as "ctlsurf Helper"
87
+ const frameworksDir = path.join(contentsDir, 'Frameworks')
88
+ if (fs.existsSync(frameworksDir)) {
89
+ for (const entry of fs.readdirSync(frameworksDir)) {
90
+ if (entry.startsWith('Electron Helper') && entry.endsWith('.app')) {
91
+ rebrandHelper(path.join(frameworksDir, entry))
92
+ }
93
+ }
94
+ }
95
+
96
+ // 5) Rename .app bundle directory
97
+ let finalAppDir = appDir
98
+ if (appDir !== newAppDir && !fs.existsSync(newAppDir)) {
99
+ fs.renameSync(appDir, newAppDir)
100
+ finalAppDir = newAppDir
101
+ }
102
+
103
+ // 6) Update path.txt to point at new bundle/binary
104
+ const newRel = path.join('dist', `${PRODUCT_NAME}.app`, 'Contents', 'MacOS', PRODUCT_NAME)
105
+ fs.writeFileSync(pathTxtPath, newRel)
106
+
107
+ // 7) Re-sign ad-hoc — original signature is invalidated by our edits, and
108
+ // Gatekeeper / hardened-runtime macOS configs will refuse to load the
109
+ // helper frameworks otherwise.
110
+ try {
111
+ execFileSync('codesign', ['--remove-signature', finalAppDir], { stdio: 'ignore' })
112
+ } catch { /* ignore */ }
113
+ try {
114
+ execFileSync('codesign', ['--force', '--deep', '--sign', '-', finalAppDir], { stdio: 'ignore' })
115
+ } catch { /* ignore — best-effort */ }
116
+ }
117
+
118
+ function rebrandHelper(helperAppDir) {
119
+ // e.g. helperAppDir = ".../Electron Helper (Renderer).app"
120
+ const oldName = path.basename(helperAppDir, '.app') // "Electron Helper (Renderer)"
121
+ const suffix = oldName.replace(/^Electron Helper/, '').trim() // "(Renderer)" or ""
122
+ const newName = suffix ? `${PRODUCT_NAME} Helper ${suffix}` : `${PRODUCT_NAME} Helper`
123
+
124
+ const macosDir = path.join(helperAppDir, 'Contents', 'MacOS')
125
+ const oldBinary = path.join(macosDir, oldName)
126
+ const newBinary = path.join(macosDir, newName)
127
+ if (fs.existsSync(oldBinary) && oldBinary !== newBinary) {
128
+ fs.renameSync(oldBinary, newBinary)
129
+ }
130
+
131
+ patchInfoPlist(path.join(helperAppDir, 'Contents', 'Info.plist'), {
132
+ CFBundleName: newName,
133
+ CFBundleDisplayName: newName,
134
+ CFBundleExecutable: newName,
135
+ })
136
+
137
+ const newAppDir = path.join(path.dirname(helperAppDir), `${newName}.app`)
138
+ if (helperAppDir !== newAppDir && !fs.existsSync(newAppDir)) {
139
+ fs.renameSync(helperAppDir, newAppDir)
140
+ }
141
+ }
142
+
143
+ function patchInfoPlist(plistPath, kv) {
144
+ if (!fs.existsSync(plistPath)) return
145
+ for (const [key, val] of Object.entries(kv)) {
146
+ try {
147
+ execFileSync('plutil', ['-replace', key, '-string', val, plistPath], { stdio: 'ignore' })
148
+ } catch {
149
+ try {
150
+ execFileSync('plutil', ['-insert', key, '-string', val, plistPath], { stdio: 'ignore' })
151
+ } catch { /* ignore */ }
152
+ }
153
+ }
154
+ }
155
+
156
+ // ─── Linux ──────────────────────────────────────────
157
+
158
+ function rebrandLinux(electronBinary, pathTxtPath) {
159
+ // dist/electron → dist/ctlsurf (process title comes from argv[0]/comm on linux)
160
+ const dir = path.dirname(electronBinary)
161
+ const newBinary = path.join(dir, PRODUCT_NAME)
162
+ if (fs.existsSync(electronBinary) && !fs.existsSync(newBinary)) {
163
+ fs.renameSync(electronBinary, newBinary)
164
+ }
165
+ fs.writeFileSync(pathTxtPath, path.join('dist', PRODUCT_NAME))
166
+ }
167
+
168
+ // ─── Windows ────────────────────────────────────────
169
+
170
+ function rebrandWin32(electronBinary, pathTxtPath) {
171
+ // dist/electron.exe → dist/ctlsurf.exe
172
+ // (Embedding our icon into the exe resource table requires rcedit; out of
173
+ // scope for npm-install distribution. Renaming gives correct Task Manager
174
+ // process name; icon in tray/window comes from BrowserWindow.icon.)
175
+ const dir = path.dirname(electronBinary)
176
+ const newBinary = path.join(dir, `${PRODUCT_NAME}.exe`)
177
+ if (fs.existsSync(electronBinary) && !fs.existsSync(newBinary)) {
178
+ fs.renameSync(electronBinary, newBinary)
179
+ }
180
+ fs.writeFileSync(pathTxtPath, path.join('dist', `${PRODUCT_NAME}.exe`))
181
+ }
182
+
183
+ // ─── Entry ──────────────────────────────────────────
184
+
185
+ if (require.main === module) {
186
+ const ok = rebrand()
187
+ if (!ok && !fs.existsSync(ELECTRON_DIR)) {
188
+ // Electron not installed yet — silent no-op; launcher will rerun us
189
+ // after just-in-time install.
190
+ process.exit(0)
191
+ }
192
+ process.exit(0)
193
+ }
194
+
195
+ module.exports = { rebrand }
@@ -21,6 +21,12 @@ process.on('uncaughtException', (err) => {
21
21
  try { console.error('[uncaught]', err) } catch { /* ignore */ }
22
22
  })
23
23
 
24
+ import { setSilent } from './logger'
25
+
26
+ // PTY owns the screen in TUI mode — silence background logs so they don't
27
+ // corrupt the agent's display.
28
+ setSilent(true)
29
+
24
30
  import { Orchestrator } from './orchestrator'
25
31
  import { getSettingsDir } from './settingsDir'
26
32
  import { getBuiltinAgents, isCodingAgent, type AgentConfig } from './agents'
package/src/main/index.ts CHANGED
@@ -4,6 +4,12 @@ import fs from 'fs'
4
4
 
5
5
  import { fetchLatestNpmVersion, compareSemver } from './updateCheck'
6
6
 
7
+ // Set app name early — drives the macOS menu bar title, dock label, and the
8
+ // CFBundleName fallback. The Activity Monitor process name is set by the
9
+ // rebranded Electron.app bundle (see scripts/rebrand-electron.js).
10
+ app.setName('ctlsurf')
11
+ process.title = 'ctlsurf'
12
+
7
13
  // Prevent EPIPE crashes when stdout pipe is closed
8
14
  process.stdout?.on?.('error', () => {})
9
15
  process.stderr?.on?.('error', () => {})
@@ -0,0 +1,10 @@
1
+ let silent = false
2
+
3
+ export function setSilent(value: boolean): void {
4
+ silent = value
5
+ }
6
+
7
+ export function log(...args: unknown[]): void {
8
+ if (silent) return
9
+ try { console.log(...args) } catch { /* EPIPE safe */ }
10
+ }
@@ -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
 
@@ -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)