@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.
Files changed (35) hide show
  1. package/out/headless/index.mjs +111 -5
  2. package/out/headless/index.mjs.map +3 -3
  3. package/out/main/index.js +311 -7
  4. package/out/preload/index.js +9 -0
  5. package/out/renderer/assets/{cssMode-Bmzy9hKX.js → cssMode-R8fTFLae.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-CDAsExTa.js → freemarker2-CyjWzWoP.js} +1 -1
  7. package/out/renderer/assets/{handlebars-CvpBYG1L.js → handlebars-CrhCGIRM.js} +1 -1
  8. package/out/renderer/assets/{html-CNhEQqr8.js → html-Cc3dPNZr.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-DD7YPgga.js → htmlMode-BWtG6ywk.js} +3 -3
  10. package/out/renderer/assets/{index-CGHeDwZa.css → index-BL9njXIL.css} +201 -0
  11. package/out/renderer/assets/{index-BDlCPzCG.js → index-kkG56aKv.js} +407 -34
  12. package/out/renderer/assets/{javascript-CsGLsswK.js → javascript-C8T_SdLC.js} +2 -2
  13. package/out/renderer/assets/{jsonMode-CTuGgRvC.js → jsonMode-D1zR4VVU.js} +3 -3
  14. package/out/renderer/assets/{liquid-BrrN4uul.js → liquid-COSMe9Uf.js} +1 -1
  15. package/out/renderer/assets/{lspLanguageFeatures-Bhdn2Eqy.js → lspLanguageFeatures-CXGfrtzl.js} +1 -1
  16. package/out/renderer/assets/{mdx-DJo-gax4.js → mdx-RfS44T-L.js} +1 -1
  17. package/out/renderer/assets/{python-DYHeg5b2.js → python-DFSWKHUu.js} +1 -1
  18. package/out/renderer/assets/{razor-Cqcks14K.js → razor-DtH2_uvo.js} +1 -1
  19. package/out/renderer/assets/{tsMode-YTLgCayh.js → tsMode-BYIQ-S9Z.js} +1 -1
  20. package/out/renderer/assets/{typescript-oxBn_Vcw.js → typescript-HhQUr1Rb.js} +1 -1
  21. package/out/renderer/assets/{xml-BEqGMCTc.js → xml-KuhgdKfD.js} +1 -1
  22. package/out/renderer/assets/{yaml-BGSrk5HC.js → yaml-W_kzCFH7.js} +1 -1
  23. package/out/renderer/index.html +2 -2
  24. package/package.json +1 -1
  25. package/src/main/agents.ts +9 -1
  26. package/src/main/ctlsurfApi.ts +7 -0
  27. package/src/main/index.ts +58 -3
  28. package/src/main/orchestrator.ts +105 -4
  29. package/src/main/ssh.ts +348 -0
  30. package/src/preload/index.ts +26 -0
  31. package/src/renderer/App.tsx +20 -0
  32. package/src/renderer/components/AgentPicker.tsx +18 -0
  33. package/src/renderer/components/ProjectPicker.tsx +121 -0
  34. package/src/renderer/components/RemoteConnect.tsx +315 -0
  35. package/src/renderer/styles.css +201 -0
@@ -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.11.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.getActiveTabCwd();
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.getActiveTabCwd();
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.getActiveTabCwd();
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;