@solongate/proxy 0.81.79 → 0.81.81

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.
@@ -11,6 +11,20 @@ export declare function globalPaths(): {
11
11
  };
12
12
  export declare function clearGuardUpdateCheck(): boolean;
13
13
  export declare function runGlobalRestore(): void;
14
+ /**
15
+ * TUI-safe (re)install — the dataroom's one-key "install / update guard" action.
16
+ * Writes the hook files THIS CLI ships (readGuard = the current package's bundle,
17
+ * so a self-updated dataroom installs the newest hooks DIRECTLY — no waiting for a
18
+ * tool call) and registers them in the global Claude settings, reusing the key
19
+ * already stored in cloud-guard.json. No prompt, no stdout, no process.exit — safe
20
+ * to call from Ink. Returns a status; asks the user to log in when no key exists.
21
+ * The auto-shield shell shim is left to `solongate` login (it can print), so this
22
+ * touches only the guard/audit/stop hooks — the primary protection.
23
+ */
24
+ export declare function installGlobalQuiet(): {
25
+ ok: boolean;
26
+ message: string;
27
+ };
14
28
  /**
15
29
  * Are the SolonGate guard hooks currently installed in the global Claude settings
16
30
  * on THIS device? Reflects a local install/remove IMMEDIATELY — unlike the cloud
@@ -137,6 +137,54 @@ function runGlobalRestore() {
137
137
  }
138
138
  console.log(" Global SolonGate enforcement uninstalled. Restart Claude Code.");
139
139
  }
140
+ function installGlobalQuiet() {
141
+ try {
142
+ const p = globalPaths();
143
+ let apiKey = process.env["SOLONGATE_API_KEY"] || "";
144
+ let apiUrl = process.env["SOLONGATE_API_URL"] || "https://api.solongate.com";
145
+ try {
146
+ const cfg = JSON.parse(readFileSync(p.configPath, "utf-8"));
147
+ if (cfg && typeof cfg.apiKey === "string") apiKey = apiKey || cfg.apiKey;
148
+ if (cfg && typeof cfg.apiUrl === "string") apiUrl = cfg.apiUrl;
149
+ } catch {
150
+ }
151
+ if (!apiKey) return { ok: false, message: "no login on this device \u2014 add an account first (Accounts \u2192 + add)" };
152
+ mkdirSync(p.hooksDir, { recursive: true });
153
+ mkdirSync(p.claudeDir, { recursive: true });
154
+ unlockProtected();
155
+ writeFileSync(join(p.hooksDir, "guard.mjs"), readGuard());
156
+ writeFileSync(join(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
157
+ writeFileSync(join(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
158
+ writeFileSync(join(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
159
+ writeFileSync(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
160
+ let existing = {};
161
+ if (existsSync(p.settingsPath)) {
162
+ const raw = readFileSync(p.settingsPath, "utf-8");
163
+ if (!existsSync(p.backupPath)) writeFileSync(p.backupPath, raw);
164
+ try {
165
+ existing = JSON.parse(raw);
166
+ } catch {
167
+ existing = {};
168
+ }
169
+ }
170
+ const nodeBin = process.execPath.replace(/\\/g, "/");
171
+ const call = process.platform === "win32" ? "& " : "";
172
+ const hookCmd = (script) => `${call}"${nodeBin}" "${join(p.hooksDir, script).replace(/\\/g, "/")}" claude-code "Claude Code"`;
173
+ const merged = {
174
+ ...existing,
175
+ hooks: {
176
+ PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("guard.mjs") }] }],
177
+ PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("audit.mjs") }] }],
178
+ Stop: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("stop.mjs") }] }]
179
+ }
180
+ };
181
+ writeFileSync(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
182
+ if (process.env["SOLONGATE_OS_LOCK"] === "1") lockProtected();
183
+ return { ok: true, message: "guard installed \u2014 active in NEW Claude Code sessions (open a new one)" };
184
+ } catch (e) {
185
+ return { ok: false, message: e instanceof Error ? e.message : String(e) };
186
+ }
187
+ }
140
188
  function isGuardInstalled() {
141
189
  try {
142
190
  const p = globalPaths();
@@ -309,6 +357,7 @@ export {
309
357
  clearGuardUpdateCheck,
310
358
  globalPaths,
311
359
  installClaudeShim,
360
+ installGlobalQuiet,
312
361
  installGlobalWithKey,
313
362
  isGuardInstalled,
314
363
  lockProtected,
package/dist/index.js CHANGED
@@ -6562,7 +6562,7 @@ __export(self_update_exports, {
6562
6562
  tuiUpdateFlow: () => tuiUpdateFlow
6563
6563
  });
6564
6564
  import { execFile, spawn } from "child_process";
6565
- import { mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync4, realpathSync, writeFileSync as writeFileSync3 } from "fs";
6565
+ import { mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
6566
6566
  import { homedir as homedir2 } from "os";
6567
6567
  import { dirname, join as join4 } from "path";
6568
6568
  import { fileURLToPath } from "url";
@@ -6614,28 +6614,6 @@ async function fetchLatest() {
6614
6614
  return null;
6615
6615
  }
6616
6616
  }
6617
- function npmGlobalRoot() {
6618
- return new Promise((resolve10) => {
6619
- try {
6620
- execFile("npm", ["root", "-g"], { timeout: 5e3, windowsHide: true, shell: process.platform === "win32" }, (err2, stdout) => {
6621
- resolve10(err2 ? null : stdout.trim() || null);
6622
- });
6623
- } catch {
6624
- resolve10(null);
6625
- }
6626
- });
6627
- }
6628
- async function isGlobalInstall() {
6629
- try {
6630
- const script = realpathSync(process.argv[1] ?? "");
6631
- if (!script) return false;
6632
- const root = await npmGlobalRoot();
6633
- if (!root) return false;
6634
- return script.startsWith(realpathSync(root));
6635
- } catch {
6636
- return false;
6637
- }
6638
- }
6639
6617
  function spawnGlobalInstall(version) {
6640
6618
  try {
6641
6619
  mkdirSync3(join4(homedir2(), ".solongate"), { recursive: true });
@@ -6687,7 +6665,7 @@ async function tuiUpdateFlow(onStatus) {
6687
6665
  if (!latest || !newerThan(latest, current)) return;
6688
6666
  const attempts = readState().attempts ?? {};
6689
6667
  const canRetry = Date.now() - (attempts[latest] ?? 0) >= ATTEMPT_EVERY_MS;
6690
- if (await isGlobalInstall() && canRetry) {
6668
+ if (canRetry) {
6691
6669
  writeState({ ...readState(), attempts: { [latest]: Date.now() } });
6692
6670
  onStatus({ kind: "updating", version: latest });
6693
6671
  if (await runGlobalInstall(latest)) {
@@ -6713,16 +6691,14 @@ function maybeSelfUpdate(notify = (l) => process.stderr.write(l + "\n")) {
6713
6691
  }
6714
6692
  if (!latest || !newerThan(latest, current)) return;
6715
6693
  const attempts = readState().attempts ?? {};
6716
- if (await isGlobalInstall()) {
6717
- if (now - (attempts[latest] ?? 0) >= ATTEMPT_EVERY_MS) {
6718
- writeState({ ...readState(), attempts: { [latest]: now } });
6719
- if (spawnGlobalInstall(latest)) {
6720
- notify(`${c.dim}\u2191 solongate v${latest} available (running v${current}) \u2014 updating in the background, next run uses it${c.reset}`);
6721
- return;
6722
- }
6723
- } else {
6694
+ if (now - (attempts[latest] ?? 0) >= ATTEMPT_EVERY_MS) {
6695
+ writeState({ ...readState(), attempts: { [latest]: now } });
6696
+ if (spawnGlobalInstall(latest)) {
6697
+ notify(`${c.dim}\u2191 solongate v${latest} available (running v${current}) \u2014 updating in the background, next run uses it${c.reset}`);
6724
6698
  return;
6725
6699
  }
6700
+ } else {
6701
+ return;
6726
6702
  }
6727
6703
  notify(`${c.dim}\u2191 solongate v${latest} available (running v${current}) \u2014 update: ${c.reset}${c.cyan}npm i -g ${PKG}@latest${c.reset}`);
6728
6704
  } catch {
@@ -10115,6 +10091,32 @@ import { homedir as homedir9 } from "os";
10115
10091
  import { fileURLToPath as fileURLToPath3 } from "url";
10116
10092
  import { createInterface } from "readline";
10117
10093
  import { execFileSync as execFileSync2 } from "child_process";
10094
+ function lockFile(file) {
10095
+ if (!existsSync4(file)) return;
10096
+ try {
10097
+ if (process.platform === "win32") {
10098
+ try {
10099
+ execFileSync2("icacls", [file, "/deny", "*S-1-1-0:(WD,AD,DC,DE)"], { stdio: "ignore" });
10100
+ } catch {
10101
+ }
10102
+ try {
10103
+ execFileSync2("attrib", ["+R", file], { stdio: "ignore" });
10104
+ } catch {
10105
+ }
10106
+ } else if (process.platform === "darwin") {
10107
+ try {
10108
+ execFileSync2("chflags", ["uchg", file], { stdio: "ignore" });
10109
+ } catch {
10110
+ }
10111
+ } else {
10112
+ try {
10113
+ execFileSync2("chattr", ["+i", file], { stdio: "ignore" });
10114
+ } catch {
10115
+ }
10116
+ }
10117
+ } catch {
10118
+ }
10119
+ }
10118
10120
  function unlockFile(file) {
10119
10121
  if (!existsSync4(file)) return;
10120
10122
  try {
@@ -10156,6 +10158,9 @@ function protectedTargets() {
10156
10158
  p.settingsPath
10157
10159
  ];
10158
10160
  }
10161
+ function lockProtected() {
10162
+ for (const f of protectedTargets()) lockFile(f);
10163
+ }
10159
10164
  function unlockProtected() {
10160
10165
  for (const f of protectedTargets()) unlockFile(f);
10161
10166
  }
@@ -10174,12 +10179,59 @@ function globalPaths() {
10174
10179
  configPath: join11(sgDir, "cloud-guard.json")
10175
10180
  };
10176
10181
  }
10177
- function clearGuardUpdateCheck() {
10182
+ function readHook(filename) {
10183
+ return readFileSync9(join11(HOOKS_DIR, filename), "utf-8");
10184
+ }
10185
+ function readGuard() {
10186
+ const bundled = join11(HOOKS_DIR, "guard.bundled.mjs");
10187
+ return existsSync4(bundled) ? readFileSync9(bundled, "utf-8") : readHook("guard.mjs");
10188
+ }
10189
+ function installGlobalQuiet() {
10178
10190
  try {
10179
- rmSync2(join11(globalPaths().sgDir, ".hook-update-check"), { force: true });
10180
- return true;
10181
- } catch {
10182
- return false;
10191
+ const p = globalPaths();
10192
+ let apiKey = process.env["SOLONGATE_API_KEY"] || "";
10193
+ let apiUrl = process.env["SOLONGATE_API_URL"] || "https://api.solongate.com";
10194
+ try {
10195
+ const cfg = JSON.parse(readFileSync9(p.configPath, "utf-8"));
10196
+ if (cfg && typeof cfg.apiKey === "string") apiKey = apiKey || cfg.apiKey;
10197
+ if (cfg && typeof cfg.apiUrl === "string") apiUrl = cfg.apiUrl;
10198
+ } catch {
10199
+ }
10200
+ if (!apiKey) return { ok: false, message: "no login on this device \u2014 add an account first (Accounts \u2192 + add)" };
10201
+ mkdirSync8(p.hooksDir, { recursive: true });
10202
+ mkdirSync8(p.claudeDir, { recursive: true });
10203
+ unlockProtected();
10204
+ writeFileSync9(join11(p.hooksDir, "guard.mjs"), readGuard());
10205
+ writeFileSync9(join11(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
10206
+ writeFileSync9(join11(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
10207
+ writeFileSync9(join11(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
10208
+ writeFileSync9(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
10209
+ let existing = {};
10210
+ if (existsSync4(p.settingsPath)) {
10211
+ const raw = readFileSync9(p.settingsPath, "utf-8");
10212
+ if (!existsSync4(p.backupPath)) writeFileSync9(p.backupPath, raw);
10213
+ try {
10214
+ existing = JSON.parse(raw);
10215
+ } catch {
10216
+ existing = {};
10217
+ }
10218
+ }
10219
+ const nodeBin = process.execPath.replace(/\\/g, "/");
10220
+ const call = process.platform === "win32" ? "& " : "";
10221
+ const hookCmd = (script) => `${call}"${nodeBin}" "${join11(p.hooksDir, script).replace(/\\/g, "/")}" claude-code "Claude Code"`;
10222
+ const merged = {
10223
+ ...existing,
10224
+ hooks: {
10225
+ PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("guard.mjs") }] }],
10226
+ PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("audit.mjs") }] }],
10227
+ Stop: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("stop.mjs") }] }]
10228
+ }
10229
+ };
10230
+ writeFileSync9(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
10231
+ if (process.env["SOLONGATE_OS_LOCK"] === "1") lockProtected();
10232
+ return { ok: true, message: "guard installed \u2014 active in NEW Claude Code sessions (open a new one)" };
10233
+ } catch (e) {
10234
+ return { ok: false, message: e instanceof Error ? e.message : String(e) };
10183
10235
  }
10184
10236
  }
10185
10237
  function isGuardInstalled() {
@@ -10303,6 +10355,19 @@ function SettingsPanel({
10303
10355
  const local = localQ.data;
10304
10356
  const guard = guardQ.data;
10305
10357
  const selfProt = selfQ.data;
10358
+ const autoInstalledRef = useRef3(false);
10359
+ useEffect6(() => {
10360
+ if (autoInstalledRef.current) return;
10361
+ if (guardHere && guard && guard.installed != null && !guard.up_to_date) {
10362
+ autoInstalledRef.current = true;
10363
+ const res = installGlobalQuiet();
10364
+ if (res.ok) {
10365
+ setGuardHere(isGuardInstalled());
10366
+ setMsg({ text: `\u2713 guard auto-updated \u2192 v${guard.latest} hooks written \xB7 open a new Claude Code session to apply`, level: "ok" });
10367
+ guardQ.reload();
10368
+ }
10369
+ }
10370
+ }, [guardHere, guard]);
10306
10371
  const [hidden, setHidden] = useState7(/* @__PURE__ */ new Set());
10307
10372
  const webhooks = (whQ.data?.webhooks ?? []).filter((w) => !hidden.has("wh:" + w.id));
10308
10373
  const alerts = (alertQ.data?.rules ?? []).filter((r) => !hidden.has("alert:" + r.id));
@@ -10428,19 +10493,10 @@ function SettingsPanel({
10428
10493
  } else if (r.kind === "acct-add") {
10429
10494
  beginLogin();
10430
10495
  } else if (r.kind === "guard") {
10431
- if (!guardHere) {
10432
- setMsg({ text: "guard is removed on this device \u2014 log in again (Accounts \u2192 + add) to reinstall it", level: "bad" });
10433
- return;
10434
- }
10435
- if (!guard) return;
10436
- if (guard.up_to_date) {
10437
- setMsg({ text: `guard already on the latest hook (v${guard.latest})`, level: "ok" });
10438
- return;
10439
- }
10440
- const armed = clearGuardUpdateCheck();
10441
- setMsg(
10442
- armed ? { text: `\u2713 update armed. now make your agent RUN any command (e.g. "echo ok"), not just chat; the guard installs v${guard.latest} on the next executed command`, level: "ok" } : { text: "\u2717 could not arm the update (check ~/.solongate permissions)", level: "bad" }
10443
- );
10496
+ const res = installGlobalQuiet();
10497
+ setMsg({ text: (res.ok ? "\u2713 " : "\u2717 ") + res.message, level: res.ok ? "ok" : "bad" });
10498
+ setGuardHere(isGuardInstalled());
10499
+ guardQ.reload();
10444
10500
  } else if (r.kind === "self") {
10445
10501
  if (!selfProt) return;
10446
10502
  run12(selfProt.enabled ? "self-protection disabled" : "self-protection enabled", () => api.settings.setSelfProtection(!selfProt.enabled), selfQ.reload);
@@ -10712,12 +10768,13 @@ function SettingsPanel({
10712
10768
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "guard".padEnd(11) }),
10713
10769
  !guardHere ? /* @__PURE__ */ jsxs7(Fragment4, { children: [
10714
10770
  /* @__PURE__ */ jsx7(Text7, { color: theme.bad, bold: true, children: "removed" }),
10715
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2014 not installed on this device \xB7 new Claude Code sessions are unguarded" })
10771
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2014 new Claude Code sessions are unguarded \xB7 " }),
10772
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accentBright, children: "enter to install" })
10716
10773
  ] }) : guard ? /* @__PURE__ */ jsxs7(Fragment4, { children: [
10717
10774
  /* @__PURE__ */ jsx7(Text7, { color: guard.up_to_date ? theme.ok : theme.warn, children: `v${guard.installed ?? "?"}` }),
10718
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: guard.up_to_date ? " (latest)" : ` \u2192 v${guard.latest} available \xB7 enter update` }),
10775
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: guard.up_to_date ? " (latest) \xB7 enter reinstall" : ` \u2192 v${guard.latest} available \xB7 enter update` }),
10719
10776
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: ` \xB7 ${guard.device_count} device${guard.device_count === 1 ? "" : "s"}${guard.outdated_count ? ` \xB7 ${guard.outdated_count} outdated` : ""} \xB7 d remove` })
10720
- ] }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "loading\u2026" })
10777
+ ] }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "installed \xB7 enter reinstall/update \xB7 d remove" })
10721
10778
  ] });
10722
10779
  case "self":
10723
10780
  return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
@@ -10781,7 +10838,7 @@ function SettingsPanel({
10781
10838
  const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" ? "PROTECTION" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
10782
10839
  const SECTION_DESC = {
10783
10840
  ACCOUNTS: `on this device (${accounts.length}) \xB7 \u25CF viewing \xB7 ACTIVE = guard key \xB7 x removes`,
10784
- PROTECTION: "guard hook: enter update \xB7 d remove \xB7 + self-protection toggle",
10841
+ PROTECTION: "guard hook: enter install/update (writes the files) \xB7 d remove \xB7 + self-protection",
10785
10842
  "LOCAL LOGS": "mirror every decision to a file + dashboard link",
10786
10843
  WEBHOOKS: `POST events to a URL (${webhooks.length}) \xB7 t tests`,
10787
10844
  ALERTS: `one email + one telegram (${alerts.length}) \xB7 enter edits \xB7 m on/off`
package/dist/tui/index.js CHANGED
@@ -3148,6 +3148,32 @@ import { createInterface } from "readline";
3148
3148
  import { execFileSync } from "child_process";
3149
3149
  var __dirname = dirname(fileURLToPath(import.meta.url));
3150
3150
  var HOOKS_DIR = resolve2(__dirname, "..", "hooks");
3151
+ function lockFile(file) {
3152
+ if (!existsSync2(file)) return;
3153
+ try {
3154
+ if (process.platform === "win32") {
3155
+ try {
3156
+ execFileSync("icacls", [file, "/deny", "*S-1-1-0:(WD,AD,DC,DE)"], { stdio: "ignore" });
3157
+ } catch {
3158
+ }
3159
+ try {
3160
+ execFileSync("attrib", ["+R", file], { stdio: "ignore" });
3161
+ } catch {
3162
+ }
3163
+ } else if (process.platform === "darwin") {
3164
+ try {
3165
+ execFileSync("chflags", ["uchg", file], { stdio: "ignore" });
3166
+ } catch {
3167
+ }
3168
+ } else {
3169
+ try {
3170
+ execFileSync("chattr", ["+i", file], { stdio: "ignore" });
3171
+ } catch {
3172
+ }
3173
+ }
3174
+ } catch {
3175
+ }
3176
+ }
3151
3177
  function unlockFile(file) {
3152
3178
  if (!existsSync2(file)) return;
3153
3179
  try {
@@ -3189,6 +3215,9 @@ function protectedTargets() {
3189
3215
  p.settingsPath
3190
3216
  ];
3191
3217
  }
3218
+ function lockProtected() {
3219
+ for (const f of protectedTargets()) lockFile(f);
3220
+ }
3192
3221
  function unlockProtected() {
3193
3222
  for (const f of protectedTargets()) unlockFile(f);
3194
3223
  }
@@ -3207,12 +3236,59 @@ function globalPaths() {
3207
3236
  configPath: join6(sgDir, "cloud-guard.json")
3208
3237
  };
3209
3238
  }
3210
- function clearGuardUpdateCheck() {
3239
+ function readHook(filename) {
3240
+ return readFileSync4(join6(HOOKS_DIR, filename), "utf-8");
3241
+ }
3242
+ function readGuard() {
3243
+ const bundled = join6(HOOKS_DIR, "guard.bundled.mjs");
3244
+ return existsSync2(bundled) ? readFileSync4(bundled, "utf-8") : readHook("guard.mjs");
3245
+ }
3246
+ function installGlobalQuiet() {
3211
3247
  try {
3212
- rmSync(join6(globalPaths().sgDir, ".hook-update-check"), { force: true });
3213
- return true;
3214
- } catch {
3215
- return false;
3248
+ const p = globalPaths();
3249
+ let apiKey = process.env["SOLONGATE_API_KEY"] || "";
3250
+ let apiUrl = process.env["SOLONGATE_API_URL"] || "https://api.solongate.com";
3251
+ try {
3252
+ const cfg = JSON.parse(readFileSync4(p.configPath, "utf-8"));
3253
+ if (cfg && typeof cfg.apiKey === "string") apiKey = apiKey || cfg.apiKey;
3254
+ if (cfg && typeof cfg.apiUrl === "string") apiUrl = cfg.apiUrl;
3255
+ } catch {
3256
+ }
3257
+ if (!apiKey) return { ok: false, message: "no login on this device \u2014 add an account first (Accounts \u2192 + add)" };
3258
+ mkdirSync4(p.hooksDir, { recursive: true });
3259
+ mkdirSync4(p.claudeDir, { recursive: true });
3260
+ unlockProtected();
3261
+ writeFileSync5(join6(p.hooksDir, "guard.mjs"), readGuard());
3262
+ writeFileSync5(join6(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
3263
+ writeFileSync5(join6(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
3264
+ writeFileSync5(join6(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
3265
+ writeFileSync5(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
3266
+ let existing = {};
3267
+ if (existsSync2(p.settingsPath)) {
3268
+ const raw = readFileSync4(p.settingsPath, "utf-8");
3269
+ if (!existsSync2(p.backupPath)) writeFileSync5(p.backupPath, raw);
3270
+ try {
3271
+ existing = JSON.parse(raw);
3272
+ } catch {
3273
+ existing = {};
3274
+ }
3275
+ }
3276
+ const nodeBin = process.execPath.replace(/\\/g, "/");
3277
+ const call = process.platform === "win32" ? "& " : "";
3278
+ const hookCmd = (script) => `${call}"${nodeBin}" "${join6(p.hooksDir, script).replace(/\\/g, "/")}" claude-code "Claude Code"`;
3279
+ const merged = {
3280
+ ...existing,
3281
+ hooks: {
3282
+ PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("guard.mjs") }] }],
3283
+ PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("audit.mjs") }] }],
3284
+ Stop: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("stop.mjs") }] }]
3285
+ }
3286
+ };
3287
+ writeFileSync5(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
3288
+ if (process.env["SOLONGATE_OS_LOCK"] === "1") lockProtected();
3289
+ return { ok: true, message: "guard installed \u2014 active in NEW Claude Code sessions (open a new one)" };
3290
+ } catch (e) {
3291
+ return { ok: false, message: e instanceof Error ? e.message : String(e) };
3216
3292
  }
3217
3293
  }
3218
3294
  function isGuardInstalled() {
@@ -3416,6 +3492,19 @@ function SettingsPanel({
3416
3492
  const local = localQ.data;
3417
3493
  const guard = guardQ.data;
3418
3494
  const selfProt = selfQ.data;
3495
+ const autoInstalledRef = useRef3(false);
3496
+ useEffect6(() => {
3497
+ if (autoInstalledRef.current) return;
3498
+ if (guardHere && guard && guard.installed != null && !guard.up_to_date) {
3499
+ autoInstalledRef.current = true;
3500
+ const res = installGlobalQuiet();
3501
+ if (res.ok) {
3502
+ setGuardHere(isGuardInstalled());
3503
+ setMsg({ text: `\u2713 guard auto-updated \u2192 v${guard.latest} hooks written \xB7 open a new Claude Code session to apply`, level: "ok" });
3504
+ guardQ.reload();
3505
+ }
3506
+ }
3507
+ }, [guardHere, guard]);
3419
3508
  const [hidden, setHidden] = useState7(/* @__PURE__ */ new Set());
3420
3509
  const webhooks = (whQ.data?.webhooks ?? []).filter((w) => !hidden.has("wh:" + w.id));
3421
3510
  const alerts = (alertQ.data?.rules ?? []).filter((r) => !hidden.has("alert:" + r.id));
@@ -3541,19 +3630,10 @@ function SettingsPanel({
3541
3630
  } else if (r.kind === "acct-add") {
3542
3631
  beginLogin();
3543
3632
  } else if (r.kind === "guard") {
3544
- if (!guardHere) {
3545
- setMsg({ text: "guard is removed on this device \u2014 log in again (Accounts \u2192 + add) to reinstall it", level: "bad" });
3546
- return;
3547
- }
3548
- if (!guard) return;
3549
- if (guard.up_to_date) {
3550
- setMsg({ text: `guard already on the latest hook (v${guard.latest})`, level: "ok" });
3551
- return;
3552
- }
3553
- const armed = clearGuardUpdateCheck();
3554
- setMsg(
3555
- armed ? { text: `\u2713 update armed. now make your agent RUN any command (e.g. "echo ok"), not just chat; the guard installs v${guard.latest} on the next executed command`, level: "ok" } : { text: "\u2717 could not arm the update (check ~/.solongate permissions)", level: "bad" }
3556
- );
3633
+ const res = installGlobalQuiet();
3634
+ setMsg({ text: (res.ok ? "\u2713 " : "\u2717 ") + res.message, level: res.ok ? "ok" : "bad" });
3635
+ setGuardHere(isGuardInstalled());
3636
+ guardQ.reload();
3557
3637
  } else if (r.kind === "self") {
3558
3638
  if (!selfProt) return;
3559
3639
  run(selfProt.enabled ? "self-protection disabled" : "self-protection enabled", () => api.settings.setSelfProtection(!selfProt.enabled), selfQ.reload);
@@ -3825,12 +3905,13 @@ function SettingsPanel({
3825
3905
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "guard".padEnd(11) }),
3826
3906
  !guardHere ? /* @__PURE__ */ jsxs7(Fragment4, { children: [
3827
3907
  /* @__PURE__ */ jsx7(Text7, { color: theme.bad, bold: true, children: "removed" }),
3828
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2014 not installed on this device \xB7 new Claude Code sessions are unguarded" })
3908
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2014 new Claude Code sessions are unguarded \xB7 " }),
3909
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accentBright, children: "enter to install" })
3829
3910
  ] }) : guard ? /* @__PURE__ */ jsxs7(Fragment4, { children: [
3830
3911
  /* @__PURE__ */ jsx7(Text7, { color: guard.up_to_date ? theme.ok : theme.warn, children: `v${guard.installed ?? "?"}` }),
3831
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: guard.up_to_date ? " (latest)" : ` \u2192 v${guard.latest} available \xB7 enter update` }),
3912
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: guard.up_to_date ? " (latest) \xB7 enter reinstall" : ` \u2192 v${guard.latest} available \xB7 enter update` }),
3832
3913
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: ` \xB7 ${guard.device_count} device${guard.device_count === 1 ? "" : "s"}${guard.outdated_count ? ` \xB7 ${guard.outdated_count} outdated` : ""} \xB7 d remove` })
3833
- ] }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "loading\u2026" })
3914
+ ] }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "installed \xB7 enter reinstall/update \xB7 d remove" })
3834
3915
  ] });
3835
3916
  case "self":
3836
3917
  return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
@@ -3894,7 +3975,7 @@ function SettingsPanel({
3894
3975
  const sectionOf = (r) => r.kind === "acct" || r.kind === "acct-add" ? "ACCOUNTS" : r.kind === "guard" || r.kind === "self" ? "PROTECTION" : r.kind === "ll-enabled" || r.kind === "ll-path" || r.kind === "ll-server" ? "LOCAL LOGS" : r.kind === "wh" || r.kind === "wh-add" ? "WEBHOOKS" : "ALERTS";
3895
3976
  const SECTION_DESC = {
3896
3977
  ACCOUNTS: `on this device (${accounts.length}) \xB7 \u25CF viewing \xB7 ACTIVE = guard key \xB7 x removes`,
3897
- PROTECTION: "guard hook: enter update \xB7 d remove \xB7 + self-protection toggle",
3978
+ PROTECTION: "guard hook: enter install/update (writes the files) \xB7 d remove \xB7 + self-protection",
3898
3979
  "LOCAL LOGS": "mirror every decision to a file + dashboard link",
3899
3980
  WEBHOOKS: `POST events to a URL (${webhooks.length}) \xB7 t tests`,
3900
3981
  ALERTS: `one email + one telegram (${alerts.length}) \xB7 enter edits \xB7 m on/off`
@@ -3952,7 +4033,7 @@ function SettingsPanel({
3952
4033
 
3953
4034
  // src/self-update.ts
3954
4035
  import { execFile, spawn as spawn4 } from "child_process";
3955
- import { mkdirSync as mkdirSync6, openSync as openSync3, readFileSync as readFileSync6, realpathSync, writeFileSync as writeFileSync7 } from "fs";
4036
+ import { mkdirSync as mkdirSync6, openSync as openSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync7 } from "fs";
3956
4037
  import { homedir as homedir8 } from "os";
3957
4038
  import { dirname as dirname3, join as join8 } from "path";
3958
4039
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -4009,28 +4090,6 @@ async function fetchLatest() {
4009
4090
  return null;
4010
4091
  }
4011
4092
  }
4012
- function npmGlobalRoot() {
4013
- return new Promise((resolve3) => {
4014
- try {
4015
- execFile("npm", ["root", "-g"], { timeout: 5e3, windowsHide: true, shell: process.platform === "win32" }, (err, stdout) => {
4016
- resolve3(err ? null : stdout.trim() || null);
4017
- });
4018
- } catch {
4019
- resolve3(null);
4020
- }
4021
- });
4022
- }
4023
- async function isGlobalInstall() {
4024
- try {
4025
- const script = realpathSync(process.argv[1] ?? "");
4026
- if (!script) return false;
4027
- const root = await npmGlobalRoot();
4028
- if (!root) return false;
4029
- return script.startsWith(realpathSync(root));
4030
- } catch {
4031
- return false;
4032
- }
4033
- }
4034
4093
  function runGlobalInstall(version) {
4035
4094
  return new Promise((resolve3) => {
4036
4095
  try {
@@ -4064,7 +4123,7 @@ async function tuiUpdateFlow(onStatus) {
4064
4123
  if (!latest || !newerThan(latest, current)) return;
4065
4124
  const attempts = readState2().attempts ?? {};
4066
4125
  const canRetry = Date.now() - (attempts[latest] ?? 0) >= ATTEMPT_EVERY_MS;
4067
- if (await isGlobalInstall() && canRetry) {
4126
+ if (canRetry) {
4068
4127
  writeState2({ ...readState2(), attempts: { [latest]: Date.now() } });
4069
4128
  onStatus({ kind: "updating", version: latest });
4070
4129
  if (await runGlobalInstall(latest)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.81.79",
3
+ "version": "0.81.81",
4
4
  "description": "AI tool security proxy: protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. No code changes required.",
5
5
  "type": "module",
6
6
  "bin": {