@termhub/agent 0.1.4 → 0.1.5

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 +79 -38
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -63,8 +63,13 @@ var RPC = {
63
63
  "ai.credential": def(z.object({ provider: aiProvider, config_dir: machinePath.nullable() }), z.object({ stdout: z.string() }), 1e4),
64
64
  "file.paste": def(z.object({ name: pasteName, data_b64: z.string().min(1).max(28 * 1024 * 1024) }), z.object({ path: z.string() }), 6e4),
65
65
  /** Monitor hooks (see @termhub/machine-ops hooks.ts): the agent writes the script, env and config entries under its own $HOME. */
66
- "hooks.install": def(z.object({ hooks_url: z.string().min(1).max(2048).regex(/^https?:\/\/[^\s'"]+$/), token: z.string().min(1).max(256).regex(/^[A-Za-z0-9_-]+$/) }), z.object({ home: z.string(), claude: z.enum(["installed", "skipped"]), codex: z.enum(["installed", "skipped"]) }), 15e3),
67
- "hooks.uninstall": def(z.object({}), z.object({ removed: z.boolean() }), 15e3)
66
+ /** `claude_dirs`: Claude config dirs besides ~/.claude (accounts with CLAUDE_CONFIG_DIR), hooked when they exist; since agent 0.1.5. */
67
+ "hooks.install": def(z.object({
68
+ hooks_url: z.string().min(1).max(2048).regex(/^https?:\/\/[^\s'"]+$/),
69
+ token: z.string().min(1).max(256).regex(/^[A-Za-z0-9_-]+$/),
70
+ claude_dirs: z.array(machinePath).max(16).optional()
71
+ }), z.object({ home: z.string(), claude: z.enum(["installed", "skipped"]), codex: z.enum(["installed", "skipped"]), claude_dirs: z.array(z.string()).optional() }), 15e3),
72
+ "hooks.uninstall": def(z.object({ claude_dirs: z.array(machinePath).max(16).optional() }), z.object({ removed: z.boolean() }), 15e3)
68
73
  };
69
74
  var RPC_METHODS = Object.keys(RPC);
70
75
  var rpcMethod = z.enum(RPC_METHODS);
@@ -534,6 +539,23 @@ function ptyEnv(base, shell) {
534
539
  var HOOK_SCRIPT_REL = ".termhub/bin/termhub-hook";
535
540
  var HOOK_ENV_REL = ".termhub/hook.env";
536
541
  var HOOK_MARK = "termhub-hook";
542
+ var CLAUDE_DEFAULT_DIR = "~/.claude";
543
+ function claudeConfigDirs(accountDirs) {
544
+ const out = [CLAUDE_DEFAULT_DIR];
545
+ for (const raw of accountDirs) {
546
+ let d = (raw ?? "").trim().replace(/\/+$/, "");
547
+ if (!d || d === "~" || /[\0\n\r]/.test(d))
548
+ continue;
549
+ if (!d.startsWith("~/") && !d.startsWith("/"))
550
+ d = `~/${d}`;
551
+ if (!out.includes(d))
552
+ out.push(d);
553
+ }
554
+ return out;
555
+ }
556
+ function expandHome(dir, home) {
557
+ return dir.startsWith("~/") ? `${home}/${dir.slice(2)}` : dir;
558
+ }
537
559
  var CLAUDE_HOOK_EVENTS = ["SessionStart", "UserPromptSubmit", "Notification", "Stop", "SessionEnd"];
538
560
  var HOOK_SCRIPT = `#!/bin/sh
539
561
  # termhub monitor hook \u2014 installed by termhub; forwards Claude Code / Codex hook events to
@@ -730,7 +752,7 @@ function createDispatcher(deps) {
730
752
  // src/pty.ts
731
753
  import fs2 from "fs";
732
754
  import path2 from "path";
733
- function expandHome(rawCwd, home) {
755
+ function expandHome2(rawCwd, home) {
734
756
  if (rawCwd === "~") return home;
735
757
  if (rawCwd.startsWith("~/")) return path2.join(home, rawCwd.slice(2));
736
758
  return rawCwd;
@@ -744,7 +766,7 @@ function existsDir(p) {
744
766
  }
745
767
  function resolveCwd(rawCwd) {
746
768
  const home = process.env.HOME || "/";
747
- const expanded = expandHome(rawCwd, home);
769
+ const expanded = expandHome2(rawCwd, home);
748
770
  return existsDir(expanded) ? expanded : home;
749
771
  }
750
772
  function isEnoent(err) {
@@ -935,7 +957,6 @@ async function mkdir(params) {
935
957
  import { chmod, mkdir as mkdir2, readFile, rename, rm, stat, writeFile } from "fs/promises";
936
958
  import os3 from "os";
937
959
  import path4 from "path";
938
- var CLAUDE_SETTINGS_REL = ".claude/settings.json";
939
960
  var CODEX_DIR_REL = ".codex";
940
961
  var CODEX_CONFIG_REL = ".codex/config.toml";
941
962
  var isEnoent2 = (err) => err?.code === "ENOENT";
@@ -960,65 +981,85 @@ async function writeAtomic(file, body, mode) {
960
981
  await chmod(tmp, mode);
961
982
  await rename(tmp, file);
962
983
  }
963
- function fsFailure(err, rel) {
984
+ var relPath = (shown) => shown.startsWith("~/") ? shown.slice(2) : shown;
985
+ function fsFailure(err, shown) {
964
986
  const code = err?.code;
965
- if (code === "EACCES" || code === "EPERM") return new RpcFailure("eperm", `sem permiss\xE3o em ~/${rel}`, rel);
966
- return new RpcFailure("failed", `n\xE3o foi poss\xEDvel escrever ~/${rel}: ${err instanceof Error ? err.message : String(err)}`, rel);
987
+ if (code === "EACCES" || code === "EPERM") return new RpcFailure("eperm", `sem permiss\xE3o em ${shown}`, relPath(shown));
988
+ return new RpcFailure("failed", `n\xE3o foi poss\xEDvel escrever ${shown}: ${err instanceof Error ? err.message : String(err)}`, relPath(shown));
989
+ }
990
+ async function claudeTargets(dirs, home) {
991
+ const out = [];
992
+ for (const d of claudeConfigDirs(dirs ?? [])) {
993
+ const dir = expandHome(d, home);
994
+ if (d !== CLAUDE_DEFAULT_DIR && !await isDir(dir)) continue;
995
+ out.push({ dir, file: path4.join(dir, "settings.json"), shown: `${d}/settings.json` });
996
+ }
997
+ return out;
967
998
  }
968
999
  async function install(params, home = os3.homedir()) {
969
1000
  const scriptPath = path4.join(home, HOOK_SCRIPT_REL);
970
- const claudeFile = path4.join(home, CLAUDE_SETTINGS_REL);
971
1001
  const codexFile = path4.join(home, CODEX_CONFIG_REL);
972
- const claudeSettings = await readOrEmpty(claudeFile);
973
- let mergedClaude;
974
- try {
975
- mergedClaude = mergeClaudeSettings(claudeSettings, scriptPath);
976
- } catch (err) {
977
- throw new RpcFailure("failed", err instanceof SyntaxError ? "~/.claude/settings.json n\xE3o \xE9 JSON v\xE1lido" : err instanceof Error ? err.message : String(err), CLAUDE_SETTINGS_REL);
1002
+ const targets = await claudeTargets(params.claude_dirs, home);
1003
+ const merged = [];
1004
+ for (const target of targets) {
1005
+ try {
1006
+ merged.push({ target, body: mergeClaudeSettings(await readOrEmpty(target.file), scriptPath) });
1007
+ } catch (err) {
1008
+ const message = err instanceof SyntaxError || err instanceof Error && err.message.includes("n\xE3o \xE9 um objeto JSON") ? `${target.shown} n\xE3o \xE9 JSON v\xE1lido` : err instanceof Error ? err.message : String(err);
1009
+ throw new RpcFailure("failed", message, relPath(target.shown));
1010
+ }
978
1011
  }
979
1012
  const hasCodex = await isDir(path4.join(home, CODEX_DIR_REL));
980
1013
  const mergedCodex = hasCodex ? mergeCodexConfig(await readOrEmpty(codexFile), scriptPath) : null;
981
- let current = HOOK_SCRIPT_REL;
1014
+ let current = `~/${HOOK_SCRIPT_REL}`;
982
1015
  try {
983
1016
  await mkdir2(path4.dirname(scriptPath), { recursive: true });
984
- await mkdir2(path4.dirname(claudeFile), { recursive: true });
985
- current = HOOK_ENV_REL;
1017
+ current = `~/${HOOK_ENV_REL}`;
986
1018
  await writeAtomic(path4.join(home, HOOK_ENV_REL), hookEnvFile(params.hooks_url, params.token), 384);
987
- current = HOOK_SCRIPT_REL;
1019
+ current = `~/${HOOK_SCRIPT_REL}`;
988
1020
  await writeAtomic(scriptPath, HOOK_SCRIPT, 493);
989
- current = CLAUDE_SETTINGS_REL;
990
- await writeAtomic(claudeFile, mergedClaude, 420);
1021
+ for (const { target, body } of merged) {
1022
+ current = target.shown;
1023
+ await mkdir2(target.dir, { recursive: true });
1024
+ await writeAtomic(target.file, body, 420);
1025
+ }
991
1026
  if (mergedCodex !== null) {
992
- current = CODEX_CONFIG_REL;
1027
+ current = `~/${CODEX_CONFIG_REL}`;
993
1028
  await writeAtomic(codexFile, mergedCodex, 420);
994
1029
  }
995
1030
  } catch (err) {
996
1031
  throw fsFailure(err, current);
997
1032
  }
998
- return { home, claude: "installed", codex: mergedCodex !== null ? "installed" : "skipped" };
1033
+ return {
1034
+ home,
1035
+ claude: "installed",
1036
+ codex: mergedCodex !== null ? "installed" : "skipped",
1037
+ claude_dirs: merged.map(({ target }) => target.shown.replace(/\/settings\.json$/, ""))
1038
+ };
999
1039
  }
1000
- async function uninstall(_params, home = os3.homedir()) {
1001
- const claudeFile = path4.join(home, CLAUDE_SETTINGS_REL);
1040
+ async function uninstall(params, home = os3.homedir()) {
1002
1041
  const codexFile = path4.join(home, CODEX_CONFIG_REL);
1003
- const claudeSettings = await readOrEmpty(claudeFile);
1004
- let stripped = null;
1005
- try {
1006
- stripped = claudeSettings.trim() ? stripClaudeSettings(claudeSettings) : null;
1007
- } catch {
1008
- stripped = null;
1042
+ const stripped = [];
1043
+ for (const target of await claudeTargets(params.claude_dirs, home)) {
1044
+ const current2 = await readOrEmpty(target.file);
1045
+ try {
1046
+ const body = current2.trim() ? stripClaudeSettings(current2) : current2;
1047
+ if (body !== current2) stripped.push({ target, body });
1048
+ } catch {
1049
+ }
1009
1050
  }
1010
1051
  const codexConfig = await isDir(path4.join(home, CODEX_DIR_REL)) ? await readOrEmpty(codexFile) : "";
1011
- let current = HOOK_SCRIPT_REL;
1052
+ let current = `~/${HOOK_SCRIPT_REL}`;
1012
1053
  try {
1013
1054
  await rm(path4.join(home, HOOK_SCRIPT_REL), { force: true });
1014
- current = HOOK_ENV_REL;
1055
+ current = `~/${HOOK_ENV_REL}`;
1015
1056
  await rm(path4.join(home, HOOK_ENV_REL), { force: true });
1016
- if (stripped !== null && stripped !== claudeSettings) {
1017
- current = CLAUDE_SETTINGS_REL;
1018
- await writeAtomic(claudeFile, stripped, 420);
1057
+ for (const { target, body } of stripped) {
1058
+ current = target.shown;
1059
+ await writeAtomic(target.file, body, 420);
1019
1060
  }
1020
1061
  if (codexConfig.includes(HOOK_MARK)) {
1021
- current = CODEX_CONFIG_REL;
1062
+ current = `~/${CODEX_CONFIG_REL}`;
1022
1063
  await writeAtomic(codexFile, stripCodexConfig(codexConfig), 420);
1023
1064
  }
1024
1065
  } catch (err) {
@@ -1186,7 +1227,7 @@ async function status(deps = {}) {
1186
1227
  }
1187
1228
 
1188
1229
  // src/version.ts
1189
- var AGENT_VERSION = "0.1.4";
1230
+ var AGENT_VERSION = "0.1.5";
1190
1231
 
1191
1232
  // src/run.ts
1192
1233
  function detectOs(platform = process.platform) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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,