@phenx-inc/ctlsurf 0.11.0 → 0.12.1
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 +115 -5
- package/out/headless/index.mjs.map +3 -3
- package/out/main/index.js +316 -8
- package/out/preload/index.js +9 -0
- package/out/renderer/assets/{cssMode-Bmzy9hKX.js → cssMode-D7r66wvO.js} +3 -3
- package/out/renderer/assets/{freemarker2-CDAsExTa.js → freemarker2-jSWJIjPG.js} +1 -1
- package/out/renderer/assets/{handlebars-CvpBYG1L.js → handlebars-BnFyz5RH.js} +1 -1
- package/out/renderer/assets/{html-CNhEQqr8.js → html-Bl5UFavp.js} +1 -1
- package/out/renderer/assets/{htmlMode-DD7YPgga.js → htmlMode-D4VByBT6.js} +3 -3
- package/out/renderer/assets/{index-CGHeDwZa.css → index-Cc_TqOCL.css} +318 -0
- package/out/renderer/assets/{index-BDlCPzCG.js → index-DxhW1emk.js} +558 -85
- package/out/renderer/assets/{javascript-CsGLsswK.js → javascript-CMN1OmwG.js} +2 -2
- package/out/renderer/assets/{jsonMode-CTuGgRvC.js → jsonMode-CJ5AYSPV.js} +3 -3
- package/out/renderer/assets/{liquid-BrrN4uul.js → liquid-BGMIDTEm.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Bhdn2Eqy.js → lspLanguageFeatures-DGifVYB5.js} +1 -1
- package/out/renderer/assets/{mdx-DJo-gax4.js → mdx-B2Z7R10Q.js} +1 -1
- package/out/renderer/assets/{python-DYHeg5b2.js → python-LMDMSSdn.js} +1 -1
- package/out/renderer/assets/{razor-Cqcks14K.js → razor-BZSrziPu.js} +1 -1
- package/out/renderer/assets/{tsMode-YTLgCayh.js → tsMode-DF9m6_3n.js} +1 -1
- package/out/renderer/assets/{typescript-oxBn_Vcw.js → typescript-rZeCMf0a.js} +1 -1
- package/out/renderer/assets/{xml-BEqGMCTc.js → xml-fNSZ-7so.js} +1 -1
- package/out/renderer/assets/{yaml-BGSrk5HC.js → yaml-1wxIuvzh.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 +62 -4
- package/src/main/orchestrator.ts +112 -4
- package/src/main/ssh.ts +348 -0
- package/src/preload/index.ts +26 -0
- package/src/renderer/App.tsx +25 -1
- 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/components/SpeakControls.tsx +8 -2
- package/src/renderer/components/TerminalPanel.tsx +134 -23
- package/src/renderer/components/TicketPanel.tsx +67 -26
- package/src/renderer/styles.css +318 -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.1",
|
|
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);
|
|
@@ -7834,12 +7911,16 @@ var Orchestrator = class {
|
|
|
7834
7911
|
}
|
|
7835
7912
|
}
|
|
7836
7913
|
setActiveTab(tabId) {
|
|
7914
|
+
const prevProjectPath = this.getActiveTabProjectPath();
|
|
7837
7915
|
this.activeTabId = tabId;
|
|
7838
7916
|
const tab = this.tabs.get(tabId);
|
|
7839
7917
|
if (tab) {
|
|
7840
7918
|
this.currentAgent = tab.agent;
|
|
7841
7919
|
this.currentCwd = tab.cwd;
|
|
7842
7920
|
}
|
|
7921
|
+
if (this.getActiveTabProjectPath() !== prevProjectPath) {
|
|
7922
|
+
this.events.onCwdChanged();
|
|
7923
|
+
}
|
|
7843
7924
|
}
|
|
7844
7925
|
getTabIds() {
|
|
7845
7926
|
return [...this.tabs.keys()];
|
|
@@ -7872,8 +7953,18 @@ var Orchestrator = class {
|
|
|
7872
7953
|
if (!this.activeTabId) return null;
|
|
7873
7954
|
return this.tabs.get(this.activeTabId)?.cwd ?? null;
|
|
7874
7955
|
}
|
|
7956
|
+
/**
|
|
7957
|
+
* Path used to resolve the active tab's ctlsurf project. For a remote session
|
|
7958
|
+
* bound to a project this is the project's root_path; otherwise the local cwd.
|
|
7959
|
+
*/
|
|
7960
|
+
getActiveTabProjectPath() {
|
|
7961
|
+
if (!this.activeTabId) return null;
|
|
7962
|
+
const tab = this.tabs.get(this.activeTabId);
|
|
7963
|
+
if (!tab) return null;
|
|
7964
|
+
return tab.projectRootPath ?? tab.cwd;
|
|
7965
|
+
}
|
|
7875
7966
|
async addTicketForActiveTab(input) {
|
|
7876
|
-
const cwd = this.
|
|
7967
|
+
const cwd = this.getActiveTabProjectPath();
|
|
7877
7968
|
if (!cwd) return { ok: false, error: "No active terminal tab" };
|
|
7878
7969
|
if (!this.ctlsurfApi.getApiKey()) {
|
|
7879
7970
|
return { ok: false, error: "ctlsurf API key not configured" };
|
|
@@ -7881,7 +7972,7 @@ var Orchestrator = class {
|
|
|
7881
7972
|
return this.ticketStore.addTicket(cwd, input);
|
|
7882
7973
|
}
|
|
7883
7974
|
async updateTicketForActiveTab(rowId, input) {
|
|
7884
|
-
const cwd = this.
|
|
7975
|
+
const cwd = this.getActiveTabProjectPath();
|
|
7885
7976
|
if (!cwd) return { ok: false, error: "No active terminal tab" };
|
|
7886
7977
|
if (!this.ctlsurfApi.getApiKey()) {
|
|
7887
7978
|
return { ok: false, error: "ctlsurf API key not configured" };
|
|
@@ -7889,7 +7980,7 @@ var Orchestrator = class {
|
|
|
7889
7980
|
return this.ticketStore.updateTicket(cwd, rowId, input);
|
|
7890
7981
|
}
|
|
7891
7982
|
async listTicketsForActiveTab() {
|
|
7892
|
-
const cwd = this.
|
|
7983
|
+
const cwd = this.getActiveTabProjectPath();
|
|
7893
7984
|
if (!cwd) return { ok: false, tickets: [], error: "No active terminal tab" };
|
|
7894
7985
|
if (!this.ctlsurfApi.getApiKey()) {
|
|
7895
7986
|
return { ok: false, tickets: [], error: "ctlsurf API key not configured" };
|
|
@@ -7897,6 +7988,25 @@ var Orchestrator = class {
|
|
|
7897
7988
|
return this.ticketStore.listTickets(cwd);
|
|
7898
7989
|
}
|
|
7899
7990
|
// ─── Worker WebSocket ───────────────────────────
|
|
7991
|
+
// Bind the active tab (typically a remote SSH session, whose local cwd is
|
|
7992
|
+
// meaningless) to a chosen ctlsurf project. We register the worker WS using
|
|
7993
|
+
// the project's root_path so the backend resolves that folder — the same
|
|
7994
|
+
// mechanism create-project relies on. This lights up chat logging, tickets,
|
|
7995
|
+
// remote-control, and the project header/panel for the remote session.
|
|
7996
|
+
bindActiveTabToProject(rootPath) {
|
|
7997
|
+
if (!rootPath) return { ok: false, error: "No project path" };
|
|
7998
|
+
if (!this.activeTabId) return { ok: false, error: "No active tab" };
|
|
7999
|
+
const tab = this.tabs.get(this.activeTabId);
|
|
8000
|
+
if (!tab) return { ok: false, error: "No active tab" };
|
|
8001
|
+
if (!this.ctlsurfApi.getApiKey()) return { ok: false, error: "No ctlsurf API key configured" };
|
|
8002
|
+
log(`[ctlsurf] Binding tab agent="${tab.agent.name}" to project root_path=${rootPath}`);
|
|
8003
|
+
tab.projectRootPath = rootPath;
|
|
8004
|
+
this.stopNoProjectPolling();
|
|
8005
|
+
this.workerWs.disconnect();
|
|
8006
|
+
this.connectWorkerWs(tab.agent, rootPath);
|
|
8007
|
+
this.events.onCwdChanged();
|
|
8008
|
+
return { ok: true };
|
|
8009
|
+
}
|
|
7900
8010
|
connectWorkerWs(agent, cwd) {
|
|
7901
8011
|
const profile = this.getActiveProfile();
|
|
7902
8012
|
const apiKey = profile.apiKey || process.env.CTLSURF_API_KEY;
|