@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/bin/ctlsurf-worker.js +46 -43
- package/out/headless/index.mjs +28 -26
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +43 -47
- 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 +1 -4
- package/src/main/workerWs.ts +5 -8
package/out/main/index.js
CHANGED
|
@@ -9902,13 +9902,13 @@ function requireWebsocketServer() {
|
|
|
9902
9902
|
return websocketServer;
|
|
9903
9903
|
}
|
|
9904
9904
|
requireWebsocketServer();
|
|
9905
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
10053
|
+
log$2("[worker-ws] Failed to parse message:", err);
|
|
10054
10054
|
}
|
|
10055
10055
|
};
|
|
10056
10056
|
this.ws.onclose = (event) => {
|
|
10057
|
-
log$
|
|
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$
|
|
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
|
-
|
|
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$
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
10266
|
+
log$1(`Resolved datastore for tab=${tabId} (cwd=${s.cwd})`);
|
|
10267
10267
|
return true;
|
|
10268
10268
|
} catch (err) {
|
|
10269
|
-
log$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
10419
|
+
log$1(`Backfilled system_key on legacy Time Tracking block ${titleFallbackId}`);
|
|
10420
10420
|
} catch (err) {
|
|
10421
|
-
log$
|
|
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$
|
|
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$
|
|
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$
|
|
10488
|
+
log$2(`[worker-ws] Status: ${status}`);
|
|
10495
10489
|
events.onWorkerStatus(status);
|
|
10496
10490
|
},
|
|
10497
10491
|
onMessage: (message) => {
|
|
10498
|
-
log$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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.
|
|
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 }
|
package/src/main/headless.ts
CHANGED
|
@@ -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', () => {})
|
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
|
|
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)
|