@phenx-inc/ctlsurf 0.11.0 → 0.12.0
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/headless/index.mjs +111 -5
- package/out/headless/index.mjs.map +3 -3
- package/out/main/index.js +311 -7
- package/out/preload/index.js +9 -0
- package/out/renderer/assets/{cssMode-Bmzy9hKX.js → cssMode-R8fTFLae.js} +3 -3
- package/out/renderer/assets/{freemarker2-CDAsExTa.js → freemarker2-CyjWzWoP.js} +1 -1
- package/out/renderer/assets/{handlebars-CvpBYG1L.js → handlebars-CrhCGIRM.js} +1 -1
- package/out/renderer/assets/{html-CNhEQqr8.js → html-Cc3dPNZr.js} +1 -1
- package/out/renderer/assets/{htmlMode-DD7YPgga.js → htmlMode-BWtG6ywk.js} +3 -3
- package/out/renderer/assets/{index-CGHeDwZa.css → index-BL9njXIL.css} +201 -0
- package/out/renderer/assets/{index-BDlCPzCG.js → index-kkG56aKv.js} +407 -34
- package/out/renderer/assets/{javascript-CsGLsswK.js → javascript-C8T_SdLC.js} +2 -2
- package/out/renderer/assets/{jsonMode-CTuGgRvC.js → jsonMode-D1zR4VVU.js} +3 -3
- package/out/renderer/assets/{liquid-BrrN4uul.js → liquid-COSMe9Uf.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Bhdn2Eqy.js → lspLanguageFeatures-CXGfrtzl.js} +1 -1
- package/out/renderer/assets/{mdx-DJo-gax4.js → mdx-RfS44T-L.js} +1 -1
- package/out/renderer/assets/{python-DYHeg5b2.js → python-DFSWKHUu.js} +1 -1
- package/out/renderer/assets/{razor-Cqcks14K.js → razor-DtH2_uvo.js} +1 -1
- package/out/renderer/assets/{tsMode-YTLgCayh.js → tsMode-BYIQ-S9Z.js} +1 -1
- package/out/renderer/assets/{typescript-oxBn_Vcw.js → typescript-HhQUr1Rb.js} +1 -1
- package/out/renderer/assets/{xml-BEqGMCTc.js → xml-KuhgdKfD.js} +1 -1
- package/out/renderer/assets/{yaml-BGSrk5HC.js → yaml-W_kzCFH7.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/agents.ts +9 -1
- package/src/main/ctlsurfApi.ts +7 -0
- package/src/main/index.ts +58 -3
- package/src/main/orchestrator.ts +105 -4
- package/src/main/ssh.ts +348 -0
- package/src/preload/index.ts +26 -0
- package/src/renderer/App.tsx +20 -0
- package/src/renderer/components/AgentPicker.tsx +18 -0
- package/src/renderer/components/ProjectPicker.tsx +121 -0
- package/src/renderer/components/RemoteConnect.tsx +315 -0
- package/src/renderer/styles.css +201 -0
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.
|
|
5474
|
+
version: "0.12.0",
|
|
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: {
|
|
@@ -5673,8 +5673,20 @@ function getAvailableAgents() {
|
|
|
5673
5673
|
const shell = all.filter((a) => !isCodingAgent(a));
|
|
5674
5674
|
return [...coding, ...shell];
|
|
5675
5675
|
}
|
|
5676
|
+
function isRemoteAgent(agent) {
|
|
5677
|
+
return agent.id.startsWith("remote-");
|
|
5678
|
+
}
|
|
5676
5679
|
function isCodingAgent(agent) {
|
|
5677
|
-
return agent.id !== "shell";
|
|
5680
|
+
return agent.id !== "shell" && !isRemoteAgent(agent);
|
|
5681
|
+
}
|
|
5682
|
+
|
|
5683
|
+
// src/main/ssh.ts
|
|
5684
|
+
function stripAnsiSeq(s) {
|
|
5685
|
+
return s.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
|
|
5686
|
+
}
|
|
5687
|
+
var PASSWORD_PROMPT_RE = /(?:password|passphrase)[^\n]*:\s*$/i;
|
|
5688
|
+
function isPasswordPrompt(tail) {
|
|
5689
|
+
return PASSWORD_PROMPT_RE.test(tail);
|
|
5678
5690
|
}
|
|
5679
5691
|
|
|
5680
5692
|
// src/main/ctlsurfApi.ts
|
|
@@ -5738,6 +5750,12 @@ var CtlsurfApi = class {
|
|
|
5738
5750
|
async createFolder(params) {
|
|
5739
5751
|
return this.request("POST", "/folders", params);
|
|
5740
5752
|
}
|
|
5753
|
+
// All folders (projects) the API key can see. Used to let a remote session
|
|
5754
|
+
// pick which ctlsurf project to bind to.
|
|
5755
|
+
async listFolders() {
|
|
5756
|
+
const folders = await this.request("GET", "/folders");
|
|
5757
|
+
return Array.isArray(folders) ? folders : [];
|
|
5758
|
+
}
|
|
5741
5759
|
// ─── Workers ────────────────────────────────────────
|
|
5742
5760
|
async getAuthCode() {
|
|
5743
5761
|
return this.request("POST", "/workers/token-exchange");
|
|
@@ -7448,6 +7466,10 @@ var Orchestrator = class {
|
|
|
7448
7466
|
currentProjectName = null;
|
|
7449
7467
|
transcriptTailer = null;
|
|
7450
7468
|
stateMonitor;
|
|
7469
|
+
// One-shot SSH passwords entered in-app, keyed by target. Held only in memory
|
|
7470
|
+
// until the next remote spawn for that target consumes them — never persisted.
|
|
7471
|
+
pendingSshPasswords = /* @__PURE__ */ new Map();
|
|
7472
|
+
pendingSshPasswordTimers = /* @__PURE__ */ new Map();
|
|
7451
7473
|
constructor(settingsDir, events) {
|
|
7452
7474
|
this.settingsDir = settingsDir;
|
|
7453
7475
|
this.events = events;
|
|
@@ -7743,6 +7765,53 @@ var Orchestrator = class {
|
|
|
7743
7765
|
return { ok: true };
|
|
7744
7766
|
}
|
|
7745
7767
|
// ─── PTY & Agent (multi-tab) ─────────────────────
|
|
7768
|
+
// Stash an in-app SSH password for the next remote spawn to `target`. Kept
|
|
7769
|
+
// only in memory and auto-expired so a typed-but-unused password doesn't
|
|
7770
|
+
// linger. Never written to disk or logged.
|
|
7771
|
+
setPendingSshPassword(target, password) {
|
|
7772
|
+
if (!target || !password) return;
|
|
7773
|
+
const prev = this.pendingSshPasswordTimers.get(target);
|
|
7774
|
+
if (prev) clearTimeout(prev);
|
|
7775
|
+
this.pendingSshPasswords.set(target, password);
|
|
7776
|
+
this.pendingSshPasswordTimers.set(target, setTimeout(() => {
|
|
7777
|
+
this.pendingSshPasswords.delete(target);
|
|
7778
|
+
this.pendingSshPasswordTimers.delete(target);
|
|
7779
|
+
}, 12e4));
|
|
7780
|
+
}
|
|
7781
|
+
consumePendingSshPassword(target) {
|
|
7782
|
+
const pw = this.pendingSshPasswords.get(target);
|
|
7783
|
+
if (pw === void 0) return void 0;
|
|
7784
|
+
this.pendingSshPasswords.delete(target);
|
|
7785
|
+
const timer = this.pendingSshPasswordTimers.get(target);
|
|
7786
|
+
if (timer) {
|
|
7787
|
+
clearTimeout(timer);
|
|
7788
|
+
this.pendingSshPasswordTimers.delete(target);
|
|
7789
|
+
}
|
|
7790
|
+
return pw;
|
|
7791
|
+
}
|
|
7792
|
+
// Watch a freshly-spawned ssh PTY for its password/passphrase prompt and write
|
|
7793
|
+
// the password once. We write straight to the PTY (not through writePty) so it
|
|
7794
|
+
// never reaches the bridge/chat-logging path; ssh keeps echo off, so it never
|
|
7795
|
+
// renders to the terminal or scrollback either.
|
|
7796
|
+
armPasswordInjector(ptyManager, password) {
|
|
7797
|
+
let pw = password;
|
|
7798
|
+
let tail = "";
|
|
7799
|
+
const giveUp = setTimeout(() => {
|
|
7800
|
+
pw = null;
|
|
7801
|
+
tail = "";
|
|
7802
|
+
}, 3e4);
|
|
7803
|
+
ptyManager.onData((data) => {
|
|
7804
|
+
if (pw === null) return;
|
|
7805
|
+
tail = (tail + stripAnsiSeq(data)).slice(-200);
|
|
7806
|
+
if (isPasswordPrompt(tail)) {
|
|
7807
|
+
log("[ssh] password prompt detected \u2014 injecting in-app password (one-shot)");
|
|
7808
|
+
ptyManager.write(pw + "\r");
|
|
7809
|
+
pw = null;
|
|
7810
|
+
tail = "";
|
|
7811
|
+
clearTimeout(giveUp);
|
|
7812
|
+
}
|
|
7813
|
+
});
|
|
7814
|
+
}
|
|
7746
7815
|
async spawnAgent(tabId, agent, cwd, opts) {
|
|
7747
7816
|
const existing = this.tabs.get(tabId);
|
|
7748
7817
|
if (existing) {
|
|
@@ -7761,6 +7830,14 @@ var Orchestrator = class {
|
|
|
7761
7830
|
const tab = { ptyManager, agent, cwd, termStreamBuffer: "", termStreamTimer: null };
|
|
7762
7831
|
this.tabs.set(tabId, tab);
|
|
7763
7832
|
this.stateMonitor.trackTab(tabId, agent.id, cwd);
|
|
7833
|
+
if (isRemoteAgent(agent)) {
|
|
7834
|
+
const target = agent.args[1];
|
|
7835
|
+
const pw = target ? this.consumePendingSshPassword(target) : void 0;
|
|
7836
|
+
if (pw) {
|
|
7837
|
+
log(`[ssh] remote spawn for ${target}: in-app password armed (will inject at prompt)`);
|
|
7838
|
+
this.armPasswordInjector(ptyManager, pw);
|
|
7839
|
+
}
|
|
7840
|
+
}
|
|
7764
7841
|
ptyManager.onData((data) => {
|
|
7765
7842
|
this.events.onPtyData(tabId, data);
|
|
7766
7843
|
this.timeTracker.recordActivity(tabId);
|
|
@@ -7872,8 +7949,18 @@ var Orchestrator = class {
|
|
|
7872
7949
|
if (!this.activeTabId) return null;
|
|
7873
7950
|
return this.tabs.get(this.activeTabId)?.cwd ?? null;
|
|
7874
7951
|
}
|
|
7952
|
+
/**
|
|
7953
|
+
* Path used to resolve the active tab's ctlsurf project. For a remote session
|
|
7954
|
+
* bound to a project this is the project's root_path; otherwise the local cwd.
|
|
7955
|
+
*/
|
|
7956
|
+
getActiveTabProjectPath() {
|
|
7957
|
+
if (!this.activeTabId) return null;
|
|
7958
|
+
const tab = this.tabs.get(this.activeTabId);
|
|
7959
|
+
if (!tab) return null;
|
|
7960
|
+
return tab.projectRootPath ?? tab.cwd;
|
|
7961
|
+
}
|
|
7875
7962
|
async addTicketForActiveTab(input) {
|
|
7876
|
-
const cwd = this.
|
|
7963
|
+
const cwd = this.getActiveTabProjectPath();
|
|
7877
7964
|
if (!cwd) return { ok: false, error: "No active terminal tab" };
|
|
7878
7965
|
if (!this.ctlsurfApi.getApiKey()) {
|
|
7879
7966
|
return { ok: false, error: "ctlsurf API key not configured" };
|
|
@@ -7881,7 +7968,7 @@ var Orchestrator = class {
|
|
|
7881
7968
|
return this.ticketStore.addTicket(cwd, input);
|
|
7882
7969
|
}
|
|
7883
7970
|
async updateTicketForActiveTab(rowId, input) {
|
|
7884
|
-
const cwd = this.
|
|
7971
|
+
const cwd = this.getActiveTabProjectPath();
|
|
7885
7972
|
if (!cwd) return { ok: false, error: "No active terminal tab" };
|
|
7886
7973
|
if (!this.ctlsurfApi.getApiKey()) {
|
|
7887
7974
|
return { ok: false, error: "ctlsurf API key not configured" };
|
|
@@ -7889,7 +7976,7 @@ var Orchestrator = class {
|
|
|
7889
7976
|
return this.ticketStore.updateTicket(cwd, rowId, input);
|
|
7890
7977
|
}
|
|
7891
7978
|
async listTicketsForActiveTab() {
|
|
7892
|
-
const cwd = this.
|
|
7979
|
+
const cwd = this.getActiveTabProjectPath();
|
|
7893
7980
|
if (!cwd) return { ok: false, tickets: [], error: "No active terminal tab" };
|
|
7894
7981
|
if (!this.ctlsurfApi.getApiKey()) {
|
|
7895
7982
|
return { ok: false, tickets: [], error: "ctlsurf API key not configured" };
|
|
@@ -7897,6 +7984,25 @@ var Orchestrator = class {
|
|
|
7897
7984
|
return this.ticketStore.listTickets(cwd);
|
|
7898
7985
|
}
|
|
7899
7986
|
// ─── Worker WebSocket ───────────────────────────
|
|
7987
|
+
// Bind the active tab (typically a remote SSH session, whose local cwd is
|
|
7988
|
+
// meaningless) to a chosen ctlsurf project. We register the worker WS using
|
|
7989
|
+
// the project's root_path so the backend resolves that folder — the same
|
|
7990
|
+
// mechanism create-project relies on. This lights up chat logging, tickets,
|
|
7991
|
+
// remote-control, and the project header/panel for the remote session.
|
|
7992
|
+
bindActiveTabToProject(rootPath) {
|
|
7993
|
+
if (!rootPath) return { ok: false, error: "No project path" };
|
|
7994
|
+
if (!this.activeTabId) return { ok: false, error: "No active tab" };
|
|
7995
|
+
const tab = this.tabs.get(this.activeTabId);
|
|
7996
|
+
if (!tab) return { ok: false, error: "No active tab" };
|
|
7997
|
+
if (!this.ctlsurfApi.getApiKey()) return { ok: false, error: "No ctlsurf API key configured" };
|
|
7998
|
+
log(`[ctlsurf] Binding tab agent="${tab.agent.name}" to project root_path=${rootPath}`);
|
|
7999
|
+
tab.projectRootPath = rootPath;
|
|
8000
|
+
this.stopNoProjectPolling();
|
|
8001
|
+
this.workerWs.disconnect();
|
|
8002
|
+
this.connectWorkerWs(tab.agent, rootPath);
|
|
8003
|
+
this.events.onCwdChanged();
|
|
8004
|
+
return { ok: true };
|
|
8005
|
+
}
|
|
7900
8006
|
connectWorkerWs(agent, cwd) {
|
|
7901
8007
|
const profile = this.getActiveProfile();
|
|
7902
8008
|
const apiKey = profile.apiKey || process.env.CTLSURF_API_KEY;
|