@termhub/agent 0.1.6 → 0.2.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 (2) hide show
  1. package/dist/cli.js +64 -4
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -39,6 +39,9 @@ var sessionName = z.string().min(1).max(128).regex(SESSION_RE);
39
39
  var machinePath = z.string().min(1).max(4096).refine((p) => (p === "~" || p.startsWith("~/") || p.startsWith("/")) && !/[\0\n\r]/.test(p), "invalid path");
40
40
  var pasteName = z.string().min(1).max(255).regex(/^[A-Za-z0-9._-]+$/);
41
41
  var aiProvider = z.enum(["claude", "chatgpt", "gemini", "antigravity"]);
42
+ var TMUX_KEYS = ["Enter", "Escape", "C-c", "Up", "Down", "Tab", "y", "n", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
43
+ var tmuxKey = z.enum(TMUX_KEYS);
44
+ var TEXT_MAX_CHARS = 4e3;
42
45
  var rpcErrorSchema = z.object({
43
46
  /** `failed`: the operation ran on the machine and `message` says why it failed, in words meant for the user. */
44
47
  code: z.enum(["eperm", "notfound", "no_tmux", "timeout", "invalid", "internal", "failed"]),
@@ -51,6 +54,11 @@ var RPC = {
51
54
  "tmux.list": def(z.object({}), z.object({ sessions: z.array(sessionName) })),
52
55
  "tmux.kill": def(z.object({ session: sessionName }), z.object({ killed: z.boolean() })),
53
56
  "tmux.capture": def(z.object({ session: sessionName, lines: z.number().int().min(1).max(5e3) }), z.object({ text: z.string() })),
57
+ /** Idempotent: creates the detached session in `cwd` when it is missing. `created` says whether it had to. */
58
+ "tmux.ensure": def(z.object({ session: sessionName, cwd: machinePath }), z.object({ created: z.boolean() }), 1e4),
59
+ /** Types `text` literally, then (with `enter`) presses Enter on its own after a short pause. */
60
+ "tmux.sendText": def(z.object({ session: sessionName, text: z.string().max(TEXT_MAX_CHARS), enter: z.boolean() }), z.object({ sent: z.literal(true) }), 1e4),
61
+ "tmux.sendKey": def(z.object({ session: sessionName, key: tmuxKey }), z.object({ sent: z.literal(true) }), 1e4),
54
62
  "tools.detect": def(z.object({}), z.object({ os: z.string().nullable(), tools: z.array(z.string().max(32)) })),
55
63
  "hw.probe": def(z.object({}), z.object({ stdout: z.string() }), 15e3),
56
64
  "fs.list": def(z.object({ path: machinePath }), z.object({ stdout: z.string() })),
@@ -490,14 +498,26 @@ function buildPasteScript(name) {
490
498
  }
491
499
 
492
500
  // ../../packages/machine-ops/dist/ai-credentials.js
501
+ var CREDENTIAL_SEPARATOR = "---termhub-credential---";
493
502
  function credentialScript(provider) {
494
503
  switch (provider) {
495
504
  case "claude":
496
505
  return [
497
- `if [ -f "$D/.credentials.json" ]; then cat "$D/.credentials.json"`,
498
- `elif [ "$(uname -s)" = Darwin ]; then security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null || true`,
506
+ `SEP=${shellQuote(CREDENTIAL_SEPARATOR)}`,
507
+ // 1. the on-disk copy (Linux, and macOS when the keychain entry is missing/disabled)
508
+ `if [ -f "$D/.credentials.json" ]; then cat "$D/.credentials.json" 2>/dev/null || true; fi`,
509
+ `printf '\\n%s\\n' "$SEP"`,
510
+ `if [ "$(uname -s 2>/dev/null)" = Darwin ]; then`,
511
+ ` D=\${D%/}`,
512
+ // 2a. the per-config-dir keychain item (CLAUDE_CONFIG_DIR accounts)
513
+ ` H=$(printf %s "$D" | shasum -a 256 2>/dev/null | cut -c1-8) || true`,
514
+ ` if [ -n "$H" ]; then security find-generic-password -s "Claude Code-credentials-$H" -w 2>/dev/null || true; fi`,
515
+ ` printf '\\n%s\\n' "$SEP"`,
516
+ // 2b. the unsuffixed item, only for the default dir (it belongs to a different account otherwise)
517
+ ` if [ "$D" = "$HOME/.claude" ]; then security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null || true; fi`,
518
+ ` printf '\\n%s\\n' "$SEP"`,
499
519
  `fi`
500
- ].join("; ");
520
+ ].join("\n");
501
521
  case "chatgpt":
502
522
  return `if [ -f "$D/auth.json" ]; then cat "$D/auth.json"; fi`;
503
523
  case "gemini":
@@ -1105,6 +1125,7 @@ async function pasteFile(params) {
1105
1125
  }
1106
1126
 
1107
1127
  // src/rpc/tmux.ts
1128
+ var ENTER_PAUSE_MS = 300;
1108
1129
  function processFailure2(r) {
1109
1130
  if (r.timedOut) return new RpcFailure("timeout", "tmux timed out");
1110
1131
  if (r.error === "enoent") return new RpcFailure("no_tmux", "tmux not found");
@@ -1132,6 +1153,42 @@ async function capture(params) {
1132
1153
  if (r.code !== 0) throw new RpcFailure("notfound", "session not found");
1133
1154
  return { text: r.stdout };
1134
1155
  }
1156
+ var pane = (session) => `=${session}:`;
1157
+ var why = (stderr, fallback) => stderr.trim().split("\n")[0] || fallback;
1158
+ async function ensure(params) {
1159
+ const has = await run(tmuxPath(), ["has-session", "-t", `=${params.session}`]);
1160
+ const hasFailure = processFailure2(has);
1161
+ if (hasFailure) throw hasFailure;
1162
+ if (has.code === 0) return { created: false };
1163
+ const made = await run(tmuxPath(), ["new-session", "-d", "-s", params.session, "-c", params.cwd]);
1164
+ const madeFailure = processFailure2(made);
1165
+ if (madeFailure) throw madeFailure;
1166
+ if (made.code !== 0) throw new RpcFailure("failed", why(made.stderr, "tmux new-session falhou"), params.cwd);
1167
+ return { created: true };
1168
+ }
1169
+ async function sendText(params) {
1170
+ if (params.text) {
1171
+ const typed = await run(tmuxPath(), ["send-keys", "-t", pane(params.session), "-l", "--", params.text]);
1172
+ const failure = processFailure2(typed);
1173
+ if (failure) throw failure;
1174
+ if (typed.code !== 0) throw new RpcFailure("notfound", why(typed.stderr, "session not found"));
1175
+ if (params.enter) await new Promise((r) => setTimeout(r, ENTER_PAUSE_MS));
1176
+ }
1177
+ if (params.enter) {
1178
+ const entered = await run(tmuxPath(), ["send-keys", "-t", pane(params.session), "Enter"]);
1179
+ const failure = processFailure2(entered);
1180
+ if (failure) throw failure;
1181
+ if (entered.code !== 0) throw new RpcFailure("notfound", why(entered.stderr, "session not found"));
1182
+ }
1183
+ return { sent: true };
1184
+ }
1185
+ async function sendKey(params) {
1186
+ const r = await run(tmuxPath(), ["send-keys", "-t", pane(params.session), params.key]);
1187
+ const failure = processFailure2(r);
1188
+ if (failure) throw failure;
1189
+ if (r.code !== 0) throw new RpcFailure("notfound", why(r.stderr, "session not found"));
1190
+ return { sent: true };
1191
+ }
1135
1192
 
1136
1193
  // src/rpc/tools.ts
1137
1194
  async function detect(_params) {
@@ -1147,6 +1204,9 @@ var handlers = {
1147
1204
  "tmux.list": list2,
1148
1205
  "tmux.kill": kill,
1149
1206
  "tmux.capture": capture,
1207
+ "tmux.ensure": ensure,
1208
+ "tmux.sendText": sendText,
1209
+ "tmux.sendKey": sendKey,
1150
1210
  "tools.detect": detect,
1151
1211
  "hw.probe": probe,
1152
1212
  "fs.list": list,
@@ -1242,7 +1302,7 @@ async function status(deps = {}) {
1242
1302
  }
1243
1303
 
1244
1304
  // src/version.ts
1245
- var AGENT_VERSION = "0.1.6";
1305
+ var AGENT_VERSION = "0.2.0";
1246
1306
 
1247
1307
  // src/run.ts
1248
1308
  function detectOs(platform = process.platform) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.1.6",
3
+ "version": "0.2.0",
4
4
  "description": "Agente do termhub: conecta esta máquina ao servidor por WebSocket de saída (sem SSH) e expõe os terminais tmux no navegador.",
5
5
  "license": "MIT",
6
6
  "private": false,