@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/bin/ctlsurf-worker.js
CHANGED
|
@@ -238,50 +238,36 @@ if (mode === 'desktop') {
|
|
|
238
238
|
try {
|
|
239
239
|
electronPath = require('electron')
|
|
240
240
|
} catch {
|
|
241
|
-
// Electron not installed
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
console.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const readline = require('readline')
|
|
250
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
251
|
-
rl.question(` Choice ${D}(1/2/3)${R}: `, (answer) => {
|
|
252
|
-
rl.close()
|
|
253
|
-
answer = answer.trim()
|
|
254
|
-
if (answer === '1') {
|
|
255
|
-
console.log(`\n${D}Installing Electron (this may take a minute)...${R}\n`)
|
|
256
|
-
try {
|
|
257
|
-
execSync('npm install electron --no-save', { cwd: ROOT, stdio: 'inherit' })
|
|
258
|
-
console.log(`\n${G}✓${R} Electron installed. Launching desktop mode...\n`)
|
|
259
|
-
try {
|
|
260
|
-
const ep = require('electron')
|
|
261
|
-
execFileSync(String(ep), [ROOT, ...args], {
|
|
262
|
-
stdio: 'inherit',
|
|
263
|
-
env: { ...process.env, CTLSURF_WORKER_CWD: process.env.CTLSURF_WORKER_CWD || process.cwd() }
|
|
264
|
-
})
|
|
265
|
-
} catch (err) {
|
|
266
|
-
process.exit(err.status || 0)
|
|
267
|
-
}
|
|
268
|
-
} catch (err) {
|
|
269
|
-
console.error(`\n${Y}!${R} Electron install failed: ${err.message}`)
|
|
270
|
-
console.error(` Try manually: cd ${ROOT} && npm install electron\n`)
|
|
271
|
-
process.exit(1)
|
|
272
|
-
}
|
|
273
|
-
} else if (answer === '2') {
|
|
274
|
-
console.log('')
|
|
275
|
-
runTerminal()
|
|
276
|
-
} else {
|
|
277
|
-
process.exit(0)
|
|
278
|
-
}
|
|
279
|
-
})
|
|
280
|
-
return
|
|
241
|
+
// Electron not installed (devDependency, or wiped by `npm update`).
|
|
242
|
+
// Auto-install silently so npm-installed users don't have to think about it.
|
|
243
|
+
if (!installElectron()) {
|
|
244
|
+
// Install failed — fall back to terminal mode
|
|
245
|
+
console.error(`\n${Y}!${R} Falling back to terminal mode.\n`)
|
|
246
|
+
runTerminal()
|
|
247
|
+
process.exit(0)
|
|
281
248
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
249
|
+
try {
|
|
250
|
+
// Bust require cache (electron's index.js reads path.txt at require time)
|
|
251
|
+
delete require.cache[require.resolve('electron')]
|
|
252
|
+
electronPath = require('electron')
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error(`\n${Y}!${R} Electron resolution failed after install: ${err.message}\n`)
|
|
255
|
+
process.exit(1)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Rebrand Electron.app → ctlsurf.app (idempotent; sentinel file inside
|
|
260
|
+
// node_modules/electron). Re-runs automatically after npm update wipes it.
|
|
261
|
+
try {
|
|
262
|
+
const { rebrand } = require(path.join(ROOT, 'scripts', 'rebrand-electron.js'))
|
|
263
|
+
if (rebrand()) {
|
|
264
|
+
// Path may have changed — re-resolve
|
|
265
|
+
delete require.cache[require.resolve('electron')]
|
|
266
|
+
electronPath = require('electron')
|
|
267
|
+
}
|
|
268
|
+
} catch (err) {
|
|
269
|
+
// Non-fatal — app still launches, just shows as "Electron"
|
|
270
|
+
console.error(`${D}[ctlsurf] rebrand skipped: ${err.message}${R}`)
|
|
285
271
|
}
|
|
286
272
|
|
|
287
273
|
try {
|
|
@@ -296,6 +282,23 @@ if (mode === 'desktop') {
|
|
|
296
282
|
runTerminal()
|
|
297
283
|
}
|
|
298
284
|
|
|
285
|
+
function installElectron() {
|
|
286
|
+
console.log(`\n${B} ctlsurf${R} ${D}— first-run setup${R}\n`)
|
|
287
|
+
console.log(` ${D}Installing Electron (one-time, ~100 MB)...${R}`)
|
|
288
|
+
try {
|
|
289
|
+
execSync('npm install electron --no-save --silent', {
|
|
290
|
+
cwd: ROOT,
|
|
291
|
+
stdio: ['ignore', 'ignore', 'inherit']
|
|
292
|
+
})
|
|
293
|
+
console.log(` ${G}✓${R} Electron installed.\n`)
|
|
294
|
+
return true
|
|
295
|
+
} catch (err) {
|
|
296
|
+
console.error(`\n${Y}!${R} Electron install failed: ${err.message}`)
|
|
297
|
+
console.error(` Try manually: cd ${ROOT} && npm install electron\n`)
|
|
298
|
+
return false
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
299
302
|
function runTerminal() {
|
|
300
303
|
const terminalPath = path.join(ROOT, 'out/headless/index.mjs')
|
|
301
304
|
if (!fs.existsSync(terminalPath)) {
|
package/out/headless/index.mjs
CHANGED
|
@@ -5471,7 +5471,7 @@ var require_package = __commonJS({
|
|
|
5471
5471
|
"package.json"(exports, module) {
|
|
5472
5472
|
module.exports = {
|
|
5473
5473
|
name: "@phenx-inc/ctlsurf",
|
|
5474
|
-
version: "0.3.
|
|
5474
|
+
version: "0.3.10",
|
|
5475
5475
|
description: "Agent-agnostic terminal and desktop app for ctlsurf \u2014 run Claude Code, Codex, or any coding agent with live session logging and remote control",
|
|
5476
5476
|
main: "out/main/index.js",
|
|
5477
5477
|
bin: {
|
|
@@ -5548,6 +5548,19 @@ var require_package = __commonJS({
|
|
|
5548
5548
|
}
|
|
5549
5549
|
});
|
|
5550
5550
|
|
|
5551
|
+
// src/main/logger.ts
|
|
5552
|
+
var silent = false;
|
|
5553
|
+
function setSilent(value) {
|
|
5554
|
+
silent = value;
|
|
5555
|
+
}
|
|
5556
|
+
function log(...args) {
|
|
5557
|
+
if (silent) return;
|
|
5558
|
+
try {
|
|
5559
|
+
console.log(...args);
|
|
5560
|
+
} catch {
|
|
5561
|
+
}
|
|
5562
|
+
}
|
|
5563
|
+
|
|
5551
5564
|
// src/main/orchestrator.ts
|
|
5552
5565
|
import path from "path";
|
|
5553
5566
|
import fs from "fs";
|
|
@@ -6051,12 +6064,6 @@ import os from "os";
|
|
|
6051
6064
|
import crypto from "crypto";
|
|
6052
6065
|
import WsModule from "ws";
|
|
6053
6066
|
var WS = typeof WebSocket !== "undefined" ? WebSocket : WsModule;
|
|
6054
|
-
function log(...args) {
|
|
6055
|
-
try {
|
|
6056
|
-
console.log(...args);
|
|
6057
|
-
} catch {
|
|
6058
|
-
}
|
|
6059
|
-
}
|
|
6060
6067
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
6061
6068
|
var RECONNECT_DELAY_MS = 5e3;
|
|
6062
6069
|
var MAX_RECONNECT_DELAY_MS = 6e4;
|
|
@@ -6220,7 +6227,7 @@ var WorkerWsClient = class {
|
|
|
6220
6227
|
case "registered": {
|
|
6221
6228
|
this.workerId = data.worker_id;
|
|
6222
6229
|
const workerStatus = data.status;
|
|
6223
|
-
|
|
6230
|
+
log(`[worker-ws] Registered as ${this.workerId}, status: ${workerStatus}`);
|
|
6224
6231
|
if (workerStatus === "pending_approval") {
|
|
6225
6232
|
this.setStatus("pending_approval");
|
|
6226
6233
|
} else {
|
|
@@ -6246,7 +6253,7 @@ var WorkerWsClient = class {
|
|
|
6246
6253
|
case "message": {
|
|
6247
6254
|
const msg = data.message;
|
|
6248
6255
|
if (msg) {
|
|
6249
|
-
|
|
6256
|
+
log(`[worker-ws] Received message: ${msg.id}`);
|
|
6250
6257
|
this.events.onMessage(msg);
|
|
6251
6258
|
}
|
|
6252
6259
|
break;
|
|
@@ -6261,7 +6268,7 @@ var WorkerWsClient = class {
|
|
|
6261
6268
|
case "heartbeat_ack":
|
|
6262
6269
|
break;
|
|
6263
6270
|
default:
|
|
6264
|
-
|
|
6271
|
+
log(`[worker-ws] Unknown message type: ${msgType}`);
|
|
6265
6272
|
}
|
|
6266
6273
|
}
|
|
6267
6274
|
send(data) {
|
|
@@ -6283,7 +6290,7 @@ var WorkerWsClient = class {
|
|
|
6283
6290
|
}
|
|
6284
6291
|
scheduleReconnect() {
|
|
6285
6292
|
if (!this.shouldReconnect) return;
|
|
6286
|
-
|
|
6293
|
+
log(`[worker-ws] Reconnecting in ${this.reconnectDelay / 1e3}s...`);
|
|
6287
6294
|
this.reconnectTimer = setTimeout(() => {
|
|
6288
6295
|
this.doConnect();
|
|
6289
6296
|
}, this.reconnectDelay);
|
|
@@ -6601,12 +6608,6 @@ var TimeTracker = class {
|
|
|
6601
6608
|
};
|
|
6602
6609
|
|
|
6603
6610
|
// src/main/orchestrator.ts
|
|
6604
|
-
function log3(...args) {
|
|
6605
|
-
try {
|
|
6606
|
-
console.log(...args);
|
|
6607
|
-
} catch {
|
|
6608
|
-
}
|
|
6609
|
-
}
|
|
6610
6611
|
var DEFAULT_IDLE_TIMEOUT_MIN = 15;
|
|
6611
6612
|
var DEFAULT_PROFILES = {
|
|
6612
6613
|
production: {
|
|
@@ -6619,6 +6620,7 @@ var DEFAULT_PROFILES = {
|
|
|
6619
6620
|
}
|
|
6620
6621
|
};
|
|
6621
6622
|
var TERM_STREAM_INTERVAL_MS = 50;
|
|
6623
|
+
var NO_PROJECT_POLL_MS = 5e3;
|
|
6622
6624
|
var Orchestrator = class {
|
|
6623
6625
|
settingsDir;
|
|
6624
6626
|
events;
|
|
@@ -6637,16 +6639,18 @@ var Orchestrator = class {
|
|
|
6637
6639
|
profiles: { ...DEFAULT_PROFILES },
|
|
6638
6640
|
logChat: false
|
|
6639
6641
|
};
|
|
6642
|
+
noProjectPollTimer = null;
|
|
6643
|
+
noProjectPollCwd = null;
|
|
6640
6644
|
constructor(settingsDir, events) {
|
|
6641
6645
|
this.settingsDir = settingsDir;
|
|
6642
6646
|
this.events = events;
|
|
6643
6647
|
this.workerWs = new WorkerWsClient({
|
|
6644
6648
|
onStatusChange: (status) => {
|
|
6645
|
-
|
|
6649
|
+
log(`[worker-ws] Status: ${status}`);
|
|
6646
6650
|
events.onWorkerStatus(status);
|
|
6647
6651
|
},
|
|
6648
6652
|
onMessage: (message) => {
|
|
6649
|
-
|
|
6653
|
+
log(`[worker-ws] Incoming message: ${message.id} (${message.type})`);
|
|
6650
6654
|
events.onWorkerMessage(message);
|
|
6651
6655
|
this.workerWs.sendAck(message.id);
|
|
6652
6656
|
if (message.type === "prompt" || message.type === "task_dispatch") {
|
|
@@ -6658,10 +6662,15 @@ var Orchestrator = class {
|
|
|
6658
6662
|
}
|
|
6659
6663
|
},
|
|
6660
6664
|
onRegistered: (data) => {
|
|
6661
|
-
|
|
6665
|
+
log(`[worker-ws] Registered: worker_id=${data.worker_id}, folder_id=${data.folder_id}, status=${data.status}`);
|
|
6662
6666
|
events.onWorkerRegistered(data);
|
|
6663
6667
|
if (!data.folder_id) {
|
|
6664
6668
|
events.onWorkerStatus("no_project");
|
|
6669
|
+
if (this.currentCwd && data.status !== "pending_approval") {
|
|
6670
|
+
this.startNoProjectPolling(this.currentCwd);
|
|
6671
|
+
}
|
|
6672
|
+
} else {
|
|
6673
|
+
this.stopNoProjectPolling();
|
|
6665
6674
|
}
|
|
6666
6675
|
},
|
|
6667
6676
|
onTerminalInput: (data) => {
|
|
@@ -6711,7 +6720,7 @@ var Orchestrator = class {
|
|
|
6711
6720
|
const baseUrl = profile.baseUrl || process.env.CTLSURF_BASE_URL || "https://app.ctlsurf.com";
|
|
6712
6721
|
this.ctlsurfApi.setBaseUrl(baseUrl);
|
|
6713
6722
|
this.workerWs.setBaseUrl(baseUrl);
|
|
6714
|
-
|
|
6723
|
+
log(`[settings] Profile applied: ${profile.name} (${baseUrl})`);
|
|
6715
6724
|
}
|
|
6716
6725
|
loadSettings() {
|
|
6717
6726
|
try {
|
|
@@ -6736,7 +6745,7 @@ var Orchestrator = class {
|
|
|
6736
6745
|
logChat: !!raw.logChat
|
|
6737
6746
|
};
|
|
6738
6747
|
this.saveSettings();
|
|
6739
|
-
|
|
6748
|
+
log("[settings] Migrated legacy settings to profiles");
|
|
6740
6749
|
} else {
|
|
6741
6750
|
this.settings = raw;
|
|
6742
6751
|
if (!this.settings.profiles.production) {
|
|
@@ -6763,7 +6772,7 @@ var Orchestrator = class {
|
|
|
6763
6772
|
fs.mkdirSync(this.settingsDir, { recursive: true });
|
|
6764
6773
|
fs.writeFileSync(settingsPath, JSON.stringify(this.settings, null, 2));
|
|
6765
6774
|
} catch (err) {
|
|
6766
|
-
|
|
6775
|
+
log("[settings] Failed to save:", err.message);
|
|
6767
6776
|
}
|
|
6768
6777
|
}
|
|
6769
6778
|
overrideApiKey(key) {
|
|
@@ -6901,6 +6910,7 @@ var Orchestrator = class {
|
|
|
6901
6910
|
if (isCodingAgent(agent)) {
|
|
6902
6911
|
this.connectWorkerWs(agent, cwd);
|
|
6903
6912
|
} else {
|
|
6913
|
+
this.stopNoProjectPolling();
|
|
6904
6914
|
this.workerWs.disconnect();
|
|
6905
6915
|
this.checkProjectStatus(cwd);
|
|
6906
6916
|
}
|
|
@@ -6972,15 +6982,50 @@ var Orchestrator = class {
|
|
|
6972
6982
|
const profile = this.getActiveProfile();
|
|
6973
6983
|
const apiKey = profile.apiKey || process.env.CTLSURF_API_KEY;
|
|
6974
6984
|
if (!apiKey) {
|
|
6975
|
-
|
|
6985
|
+
log("[worker-ws] No API key, skipping WS connect");
|
|
6976
6986
|
return;
|
|
6977
6987
|
}
|
|
6988
|
+
this.stopNoProjectPolling();
|
|
6978
6989
|
this.workerWs.connect({
|
|
6979
6990
|
machine: os3.hostname(),
|
|
6980
6991
|
cwd,
|
|
6981
6992
|
agent: agent.name
|
|
6982
6993
|
});
|
|
6983
6994
|
}
|
|
6995
|
+
startNoProjectPolling(cwd) {
|
|
6996
|
+
if (this.noProjectPollTimer && this.noProjectPollCwd === cwd) return;
|
|
6997
|
+
this.stopNoProjectPolling();
|
|
6998
|
+
this.noProjectPollCwd = cwd;
|
|
6999
|
+
log(`[worker-ws] Polling for project folder at ${cwd}`);
|
|
7000
|
+
this.noProjectPollTimer = setInterval(() => {
|
|
7001
|
+
void this.checkForProjectFolder(cwd);
|
|
7002
|
+
}, NO_PROJECT_POLL_MS);
|
|
7003
|
+
}
|
|
7004
|
+
stopNoProjectPolling() {
|
|
7005
|
+
if (this.noProjectPollTimer) {
|
|
7006
|
+
clearInterval(this.noProjectPollTimer);
|
|
7007
|
+
this.noProjectPollTimer = null;
|
|
7008
|
+
this.noProjectPollCwd = null;
|
|
7009
|
+
}
|
|
7010
|
+
}
|
|
7011
|
+
async checkForProjectFolder(cwd) {
|
|
7012
|
+
if (this.currentCwd !== cwd || !this.currentAgent) {
|
|
7013
|
+
this.stopNoProjectPolling();
|
|
7014
|
+
return;
|
|
7015
|
+
}
|
|
7016
|
+
if (!this.ctlsurfApi.getApiKey()) return;
|
|
7017
|
+
try {
|
|
7018
|
+
const folder = await this.ctlsurfApi.findFolderByPath(cwd);
|
|
7019
|
+
if (folder?.id && this.currentCwd === cwd && this.currentAgent) {
|
|
7020
|
+
log(`[worker-ws] Project folder appeared (${folder.id}); reconnecting`);
|
|
7021
|
+
const agent = this.currentAgent;
|
|
7022
|
+
this.stopNoProjectPolling();
|
|
7023
|
+
this.workerWs.disconnect();
|
|
7024
|
+
this.connectWorkerWs(agent, cwd);
|
|
7025
|
+
}
|
|
7026
|
+
} catch {
|
|
7027
|
+
}
|
|
7028
|
+
}
|
|
6984
7029
|
async checkProjectStatus(cwd) {
|
|
6985
7030
|
if (!this.ctlsurfApi.getApiKey()) {
|
|
6986
7031
|
this.events.onWorkerStatus("no_project");
|
|
@@ -7011,6 +7056,7 @@ var Orchestrator = class {
|
|
|
7011
7056
|
}
|
|
7012
7057
|
// ─── Shutdown ───────────────────────────────────
|
|
7013
7058
|
async shutdown() {
|
|
7059
|
+
this.stopNoProjectPolling();
|
|
7014
7060
|
this.bridge.endSession();
|
|
7015
7061
|
await this.timeTracker.endAll();
|
|
7016
7062
|
for (const [, tab] of this.tabs) {
|
|
@@ -7348,6 +7394,7 @@ process.on("uncaughtException", (err) => {
|
|
|
7348
7394
|
} catch {
|
|
7349
7395
|
}
|
|
7350
7396
|
});
|
|
7397
|
+
setSilent(true);
|
|
7351
7398
|
function getCurrentVersion() {
|
|
7352
7399
|
try {
|
|
7353
7400
|
const pkg = require_package();
|