@proagentstore/cli 0.4.18 → 0.4.20

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.
@@ -72,6 +72,47 @@ export function listSessions() {
72
72
  return [];
73
73
  }
74
74
  }
75
+ /**
76
+ * Rich listing of every live session with the signal a UI/agent needs to tell them
77
+ * apart: window count, attach state, and what's running in the active pane. Returns
78
+ * an empty array when no tmux server is running (never throws).
79
+ */
80
+ export function listSessionsDetailed() {
81
+ try {
82
+ // Tab-separated so a session name containing spaces can't split a field.
83
+ const fmt = "#{session_name}\t#{session_windows}\t#{session_attached}\t#{pane_current_command}\t#{window_name}\t#{session_created}";
84
+ const out = execFileSync("tmux", ["list-sessions", "-F", fmt], { encoding: "utf8", stdio: "pipe" });
85
+ return out
86
+ .split("\n")
87
+ .map((l) => l.trim())
88
+ .filter(Boolean)
89
+ .map((line) => {
90
+ const [name, windows, attached, activeCommand, activeWindow, created] = line.split("\t");
91
+ return {
92
+ name: name ?? "",
93
+ windows: Number(windows) || 0,
94
+ attached: attached === "1",
95
+ activeCommand: activeCommand ?? "",
96
+ activeWindow: activeWindow ?? "",
97
+ created: created ?? "",
98
+ };
99
+ })
100
+ .filter((s) => s.name);
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ }
106
+ /**
107
+ * Type a command line into a session's active pane and press Enter — the convenience
108
+ * wrapper over sendText + sendKey for "run this shell/git command". The text is sent
109
+ * literally (`-l`), so it is never shell-interpreted by tmux; the receiving pane's
110
+ * shell/CLI runs it exactly as a human would have typed it.
111
+ */
112
+ export function runCommand(target, command) {
113
+ sendText(target, command);
114
+ sendKey(target, "Enter");
115
+ }
75
116
  // biome-ignore lint/suspicious/noControlCharactersInRegex: matching ANSI escape codes from tmux output.
76
117
  const ANSI = /\x1B\[[0-?]*[ -/]*[@-~]/g;
77
118
  /** Strip ANSI escape codes. */
@@ -25,6 +25,7 @@ const CAPABILITIES = [
25
25
  "file.upload",
26
26
  "human.approval",
27
27
  "human.takeover",
28
+ "tmux.control",
28
29
  ];
29
30
  /** How many times a task may attempt an action before handing off to a human. */
30
31
  export const MAX_AUTONOMOUS_ATTEMPTS = 3;
@@ -282,6 +282,72 @@ async function route(runner, req, res) {
282
282
  if (req.method === "POST" && codingEndMatch) {
283
283
  return json(res, 200, runner.coding.endTakeover(codingEndMatch[1]));
284
284
  }
285
+ // ── tmux connector ──────────────────────────────────────────────────────
286
+ // The machine-global terminal surface any permitted agent can drive over the
287
+ // relay: list EVERY live session (not just pags-* ones), read a pane, and —
288
+ // gated by write-consent in the cloud — send keys / run a command. Reuses the
289
+ // coding runtime's tmux primitives.
290
+ if ((req.method === "GET" || req.method === "POST") && path === "/tmux/list") {
291
+ const { listSessionsDetailed } = await import("./coding/tmux.js");
292
+ return json(res, 200, { sessions: listSessionsDetailed() });
293
+ }
294
+ if (req.method === "POST" && path === "/tmux/capture") {
295
+ const { capturePane, sessionExists } = await import("./coding/tmux.js");
296
+ const b = await readJson(req);
297
+ const session = String(b.session || "").trim();
298
+ if (!session)
299
+ return json(res, 400, { error: "A `session` name is required." });
300
+ if (!sessionExists(session))
301
+ return json(res, 404, { error: `No tmux session "${session}".` });
302
+ const lines = Math.min(Math.max(Number(b.lines) || 200, 1), 2000);
303
+ return json(res, 200, { session, pane: capturePane(session, lines) });
304
+ }
305
+ if (req.method === "POST" && path === "/tmux/send") {
306
+ const { sendText, sendKey, capturePane, sessionExists } = await import("./coding/tmux.js");
307
+ const b = await readJson(req);
308
+ const session = String(b.session || "").trim();
309
+ if (!session)
310
+ return json(res, 400, { error: "A `session` name is required." });
311
+ if (!sessionExists(session))
312
+ return json(res, 404, { error: `No tmux session "${session}".` });
313
+ if (b.text != null)
314
+ sendText(session, String(b.text));
315
+ for (const k of b.keys ?? [])
316
+ sendKey(session, String(k));
317
+ return json(res, 200, { session, pane: capturePane(session, 200) });
318
+ }
319
+ if (req.method === "POST" && path === "/tmux/run") {
320
+ const { runCommand, capturePane, sessionExists } = await import("./coding/tmux.js");
321
+ const b = await readJson(req);
322
+ const session = String(b.session || "").trim();
323
+ const command = String(b.command ?? "");
324
+ if (!session)
325
+ return json(res, 400, { error: "A `session` name is required." });
326
+ if (!command.trim())
327
+ return json(res, 400, { error: "A `command` is required." });
328
+ if (!sessionExists(session))
329
+ return json(res, 404, { error: `No tmux session "${session}".` });
330
+ runCommand(session, command);
331
+ return json(res, 200, { session, command, pane: capturePane(session, 200) });
332
+ }
333
+ if (req.method === "POST" && path === "/tmux/session") {
334
+ const { createSession, killSession, sessionExists } = await import("./coding/tmux.js");
335
+ const { homedir } = await import("node:os");
336
+ const b = await readJson(req);
337
+ const session = String(b.session || "").trim();
338
+ if (!session)
339
+ return json(res, 400, { error: "A `session` name is required." });
340
+ if (b.action === "kill") {
341
+ return json(res, 200, { session, killed: killSession(session) });
342
+ }
343
+ // default: create
344
+ if (sessionExists(session))
345
+ return json(res, 200, { session, created: false, existed: true });
346
+ const { resolve } = await import("node:path");
347
+ const workDir = resolve(String(b.workDir || "~").replace(/^~(?=$|\/)/, homedir()));
348
+ createSession(session, workDir, b.command ? String(b.command) : undefined);
349
+ return json(res, 200, { session, created: true, workDir });
350
+ }
285
351
  return json(res, 404, { error: "Not found" });
286
352
  }
287
353
  function authorize(req, config) {
package/dist/index.js CHANGED
@@ -1244,11 +1244,20 @@ var upCommand = new Command7("up").description("Start the browser runner for all
1244
1244
  process.exit(1);
1245
1245
  }
1246
1246
  const data = await res.json();
1247
- let instances = (data.instances || []).filter((i) => i.status === "active");
1247
+ const active = (data.instances || []).filter((i) => i.status === "active");
1248
+ let instances = active;
1248
1249
  if (opts.instance) {
1249
1250
  instances = instances.filter((i) => i.id === opts.instance || i.slug === opts.instance);
1251
+ } else {
1252
+ instances = instances.filter((i) => i.capabilities?.runtime != null);
1250
1253
  }
1251
1254
  if (instances.length === 0) {
1255
+ if (!opts.instance && active.length > 0) {
1256
+ printStep("None of your agents need a local runner", "ok");
1257
+ writeLine(" They run in the cloud (chat, knowledge, connectors) \u2014 nothing to connect here.");
1258
+ writeLine(" Only coding (Coder) and browser agents (e.g. Job Application Assistant) use `pags up`.");
1259
+ process.exit(0);
1260
+ }
1252
1261
  printStep("No active instances found", "fail");
1253
1262
  writeLine(" Subscribe to an agent at https://proagentstore.online");
1254
1263
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.18",
3
+ "version": "0.4.20",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",