@solongate/proxy 0.81.80 → 0.81.82

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,28 @@ 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
+ };
28
+ /** HOOK_VERSION of the guard hook currently installed on THIS device (read from
29
+ * the file), or null when not installed. The LOCAL truth — independent of the
30
+ * cloud guard-status (which only knows what the device last REPORTED). */
31
+ export declare function installedGuardVersion(): number | null;
32
+ /** True when the installed guard hook DIFFERS from the one this CLI ships — i.e.
33
+ * a newer hook is available locally (the CLI self-updated). Cloud-independent,
34
+ * so the dataroom can offer/apply an update even when the API is behind. */
35
+ export declare function guardHookOutdated(): boolean;
14
36
  /**
15
37
  * Are the SolonGate guard hooks currently installed in the global Claude settings
16
38
  * on THIS device? Reflects a local install/remove IMMEDIATELY — unlike the cloud
@@ -137,6 +137,72 @@ 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 (open a new session)" };
184
+ } catch (e) {
185
+ return { ok: false, message: e instanceof Error ? e.message : String(e) };
186
+ }
187
+ }
188
+ function installedGuardVersion() {
189
+ try {
190
+ const p = globalPaths();
191
+ const s = readFileSync(join(p.hooksDir, "guard.mjs"), "utf-8");
192
+ const m = s.match(/HOOK_VERSION\s*=\s*(\d+)/);
193
+ return m ? parseInt(m[1], 10) : null;
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
198
+ function guardHookOutdated() {
199
+ try {
200
+ const p = globalPaths();
201
+ return readFileSync(join(p.hooksDir, "guard.mjs"), "utf-8") !== readGuard();
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
140
206
  function isGuardInstalled() {
141
207
  try {
142
208
  const p = globalPaths();
@@ -152,18 +218,11 @@ function uninstallGlobalQuiet() {
152
218
  const p = globalPaths();
153
219
  unlockProtected();
154
220
  removeClaudeShim();
155
- const applies = "applies to NEW Claude Code sessions \u2014 a session already running keeps its loaded hooks until it ends";
156
- if (existsSync(p.backupPath)) {
157
- writeFileSync(p.settingsPath, readFileSync(p.backupPath, "utf-8"));
158
- return { ok: true, message: `guard removed \u2014 restored the pre-install settings \xB7 ${applies}` };
159
- }
160
- if (existsSync(p.settingsPath)) {
161
- const s = JSON.parse(readFileSync(p.settingsPath, "utf-8"));
162
- delete s.hooks;
163
- writeFileSync(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
164
- return { ok: true, message: `guard removed \u2014 hooks cleared \xB7 ${applies}` };
165
- }
166
- return { ok: true, message: "nothing to remove \u2014 no global Claude Code settings found" };
221
+ if (!existsSync(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
222
+ const s = JSON.parse(readFileSync(p.settingsPath, "utf-8"));
223
+ delete s.hooks;
224
+ writeFileSync(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
225
+ return { ok: true, message: "guard removed (open a new session)" };
167
226
  } catch (e) {
168
227
  return { ok: false, message: e instanceof Error ? e.message : String(e) };
169
228
  }
@@ -308,8 +367,11 @@ async function installGlobalWithKey(apiKey, apiUrl) {
308
367
  export {
309
368
  clearGuardUpdateCheck,
310
369
  globalPaths,
370
+ guardHookOutdated,
311
371
  installClaudeShim,
372
+ installGlobalQuiet,
312
373
  installGlobalWithKey,
374
+ installedGuardVersion,
313
375
  isGuardInstalled,
314
376
  lockProtected,
315
377
  removeClaudeShim,
package/dist/index.js CHANGED
@@ -10091,6 +10091,32 @@ import { homedir as homedir9 } from "os";
10091
10091
  import { fileURLToPath as fileURLToPath3 } from "url";
10092
10092
  import { createInterface } from "readline";
10093
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
+ }
10094
10120
  function unlockFile(file) {
10095
10121
  if (!existsSync4(file)) return;
10096
10122
  try {
@@ -10132,6 +10158,9 @@ function protectedTargets() {
10132
10158
  p.settingsPath
10133
10159
  ];
10134
10160
  }
10161
+ function lockProtected() {
10162
+ for (const f of protectedTargets()) lockFile(f);
10163
+ }
10135
10164
  function unlockProtected() {
10136
10165
  for (const f of protectedTargets()) unlockFile(f);
10137
10166
  }
@@ -10150,10 +10179,75 @@ function globalPaths() {
10150
10179
  configPath: join11(sgDir, "cloud-guard.json")
10151
10180
  };
10152
10181
  }
10153
- 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() {
10154
10190
  try {
10155
- rmSync2(join11(globalPaths().sgDir, ".hook-update-check"), { force: true });
10156
- return true;
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 (open a new session)" };
10233
+ } catch (e) {
10234
+ return { ok: false, message: e instanceof Error ? e.message : String(e) };
10235
+ }
10236
+ }
10237
+ function installedGuardVersion() {
10238
+ try {
10239
+ const p = globalPaths();
10240
+ const s = readFileSync9(join11(p.hooksDir, "guard.mjs"), "utf-8");
10241
+ const m = s.match(/HOOK_VERSION\s*=\s*(\d+)/);
10242
+ return m ? parseInt(m[1], 10) : null;
10243
+ } catch {
10244
+ return null;
10245
+ }
10246
+ }
10247
+ function guardHookOutdated() {
10248
+ try {
10249
+ const p = globalPaths();
10250
+ return readFileSync9(join11(p.hooksDir, "guard.mjs"), "utf-8") !== readGuard();
10157
10251
  } catch {
10158
10252
  return false;
10159
10253
  }
@@ -10173,18 +10267,11 @@ function uninstallGlobalQuiet() {
10173
10267
  const p = globalPaths();
10174
10268
  unlockProtected();
10175
10269
  removeClaudeShim();
10176
- const applies = "applies to NEW Claude Code sessions \u2014 a session already running keeps its loaded hooks until it ends";
10177
- if (existsSync4(p.backupPath)) {
10178
- writeFileSync9(p.settingsPath, readFileSync9(p.backupPath, "utf-8"));
10179
- return { ok: true, message: `guard removed \u2014 restored the pre-install settings \xB7 ${applies}` };
10180
- }
10181
- if (existsSync4(p.settingsPath)) {
10182
- const s = JSON.parse(readFileSync9(p.settingsPath, "utf-8"));
10183
- delete s.hooks;
10184
- writeFileSync9(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
10185
- return { ok: true, message: `guard removed \u2014 hooks cleared \xB7 ${applies}` };
10186
- }
10187
- return { ok: true, message: "nothing to remove \u2014 no global Claude Code settings found" };
10270
+ if (!existsSync4(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
10271
+ const s = JSON.parse(readFileSync9(p.settingsPath, "utf-8"));
10272
+ delete s.hooks;
10273
+ writeFileSync9(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
10274
+ return { ok: true, message: "guard removed (open a new session)" };
10188
10275
  } catch (e) {
10189
10276
  return { ok: false, message: e instanceof Error ? e.message : String(e) };
10190
10277
  }
@@ -10260,6 +10347,13 @@ function SettingsPanel({
10260
10347
  const abort = useRef3(false);
10261
10348
  const refreshAccounts = () => setAccounts(listAccounts());
10262
10349
  const [guardHere, setGuardHere] = useState7(() => isGuardInstalled());
10350
+ const [guardVer, setGuardVer] = useState7(() => installedGuardVersion());
10351
+ const [guardOld, setGuardOld] = useState7(() => guardHookOutdated());
10352
+ const refreshGuard = () => {
10353
+ setGuardHere(isGuardInstalled());
10354
+ setGuardVer(installedGuardVersion());
10355
+ setGuardOld(guardHookOutdated());
10356
+ };
10263
10357
  const locked = accounts.length === 0;
10264
10358
  const [srv, setSrv] = useState7(() => logsServerStatus());
10265
10359
  useEffect6(() => {
@@ -10277,8 +10371,20 @@ function SettingsPanel({
10277
10371
  const guardQ = useLoader(() => listAccounts().length ? api.settings.getGuardStatus() : Promise.resolve(null));
10278
10372
  const selfQ = useLoader(() => listAccounts().length ? api.settings.getSelfProtection() : Promise.resolve(null));
10279
10373
  const local = localQ.data;
10280
- const guard = guardQ.data;
10281
10374
  const selfProt = selfQ.data;
10375
+ const autoInstalledRef = useRef3(false);
10376
+ useEffect6(() => {
10377
+ if (autoInstalledRef.current) return;
10378
+ if (guardHere && guardOld) {
10379
+ autoInstalledRef.current = true;
10380
+ const res = installGlobalQuiet();
10381
+ if (res.ok) {
10382
+ refreshGuard();
10383
+ setMsg({ text: `\u2713 guard updated \u2192 v${installedGuardVersion() ?? "?"} (open a new session)`, level: "ok" });
10384
+ guardQ.reload();
10385
+ }
10386
+ }
10387
+ }, [guardHere, guardOld]);
10282
10388
  const [hidden, setHidden] = useState7(/* @__PURE__ */ new Set());
10283
10389
  const webhooks = (whQ.data?.webhooks ?? []).filter((w) => !hidden.has("wh:" + w.id));
10284
10390
  const alerts = (alertQ.data?.rules ?? []).filter((r) => !hidden.has("alert:" + r.id));
@@ -10404,19 +10510,10 @@ function SettingsPanel({
10404
10510
  } else if (r.kind === "acct-add") {
10405
10511
  beginLogin();
10406
10512
  } else if (r.kind === "guard") {
10407
- if (!guardHere) {
10408
- setMsg({ text: "guard is removed on this device \u2014 log in again (Accounts \u2192 + add) to reinstall it", level: "bad" });
10409
- return;
10410
- }
10411
- if (!guard) return;
10412
- if (guard.up_to_date) {
10413
- setMsg({ text: `guard already on the latest hook (v${guard.latest})`, level: "ok" });
10414
- return;
10415
- }
10416
- const armed = clearGuardUpdateCheck();
10417
- setMsg(
10418
- 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" }
10419
- );
10513
+ const res = installGlobalQuiet();
10514
+ setMsg({ text: (res.ok ? "\u2713 " : "\u2717 ") + res.message, level: res.ok ? "ok" : "bad" });
10515
+ refreshGuard();
10516
+ guardQ.reload();
10420
10517
  } else if (r.kind === "self") {
10421
10518
  if (!selfProt) return;
10422
10519
  run12(selfProt.enabled ? "self-protection disabled" : "self-protection enabled", () => api.settings.setSelfProtection(!selfProt.enabled), selfQ.reload);
@@ -10579,7 +10676,7 @@ function SettingsPanel({
10579
10676
  setConfirmDel(null);
10580
10677
  const res = uninstallGlobalQuiet();
10581
10678
  setMsg({ text: (res.ok ? "\u2713 " : "\u2717 ") + res.message, level: res.ok ? "ok" : "bad" });
10582
- setGuardHere(isGuardInstalled());
10679
+ refreshGuard();
10583
10680
  guardQ.reload();
10584
10681
  } else if (inp === "d" && (cur.kind === "wh" || cur.kind === "alert")) {
10585
10682
  const k = keyOf(cur);
@@ -10688,12 +10785,13 @@ function SettingsPanel({
10688
10785
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "guard".padEnd(11) }),
10689
10786
  !guardHere ? /* @__PURE__ */ jsxs7(Fragment4, { children: [
10690
10787
  /* @__PURE__ */ jsx7(Text7, { color: theme.bad, bold: true, children: "removed" }),
10691
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2014 not installed on this device \xB7 new Claude Code sessions are unguarded" })
10692
- ] }) : guard ? /* @__PURE__ */ jsxs7(Fragment4, { children: [
10693
- /* @__PURE__ */ jsx7(Text7, { color: guard.up_to_date ? theme.ok : theme.warn, children: `v${guard.installed ?? "?"}` }),
10694
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: guard.up_to_date ? " (latest)" : ` \u2192 v${guard.latest} available \xB7 enter update` }),
10695
- /* @__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` })
10696
- ] }) : /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "loading\u2026" })
10788
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \xB7 " }),
10789
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accentBright, children: "enter to install" })
10790
+ ] }) : /* @__PURE__ */ jsxs7(Fragment4, { children: [
10791
+ /* @__PURE__ */ jsx7(Text7, { color: guardOld ? theme.warn : theme.ok, children: `v${guardVer ?? "?"}` }),
10792
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: guardOld ? " \xB7 update available \xB7 enter update" : " (latest) \xB7 enter reinstall" }),
10793
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \xB7 d remove" })
10794
+ ] })
10697
10795
  ] });
10698
10796
  case "self":
10699
10797
  return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
@@ -10757,7 +10855,7 @@ function SettingsPanel({
10757
10855
  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";
10758
10856
  const SECTION_DESC = {
10759
10857
  ACCOUNTS: `on this device (${accounts.length}) \xB7 \u25CF viewing \xB7 ACTIVE = guard key \xB7 x removes`,
10760
- PROTECTION: "guard hook: enter update \xB7 d remove \xB7 + self-protection toggle",
10858
+ PROTECTION: "guard hook: enter install/update (writes the files) \xB7 d remove \xB7 + self-protection",
10761
10859
  "LOCAL LOGS": "mirror every decision to a file + dashboard link",
10762
10860
  WEBHOOKS: `POST events to a URL (${webhooks.length}) \xB7 t tests`,
10763
10861
  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,10 +3236,75 @@ 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;
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 (open a new session)" };
3290
+ } catch (e) {
3291
+ return { ok: false, message: e instanceof Error ? e.message : String(e) };
3292
+ }
3293
+ }
3294
+ function installedGuardVersion() {
3295
+ try {
3296
+ const p = globalPaths();
3297
+ const s = readFileSync4(join6(p.hooksDir, "guard.mjs"), "utf-8");
3298
+ const m = s.match(/HOOK_VERSION\s*=\s*(\d+)/);
3299
+ return m ? parseInt(m[1], 10) : null;
3300
+ } catch {
3301
+ return null;
3302
+ }
3303
+ }
3304
+ function guardHookOutdated() {
3305
+ try {
3306
+ const p = globalPaths();
3307
+ return readFileSync4(join6(p.hooksDir, "guard.mjs"), "utf-8") !== readGuard();
3214
3308
  } catch {
3215
3309
  return false;
3216
3310
  }
@@ -3230,18 +3324,11 @@ function uninstallGlobalQuiet() {
3230
3324
  const p = globalPaths();
3231
3325
  unlockProtected();
3232
3326
  removeClaudeShim();
3233
- const applies = "applies to NEW Claude Code sessions \u2014 a session already running keeps its loaded hooks until it ends";
3234
- if (existsSync2(p.backupPath)) {
3235
- writeFileSync5(p.settingsPath, readFileSync4(p.backupPath, "utf-8"));
3236
- return { ok: true, message: `guard removed \u2014 restored the pre-install settings \xB7 ${applies}` };
3237
- }
3238
- if (existsSync2(p.settingsPath)) {
3239
- const s = JSON.parse(readFileSync4(p.settingsPath, "utf-8"));
3240
- delete s.hooks;
3241
- writeFileSync5(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
3242
- return { ok: true, message: `guard removed \u2014 hooks cleared \xB7 ${applies}` };
3243
- }
3244
- return { ok: true, message: "nothing to remove \u2014 no global Claude Code settings found" };
3327
+ if (!existsSync2(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
3328
+ const s = JSON.parse(readFileSync4(p.settingsPath, "utf-8"));
3329
+ delete s.hooks;
3330
+ writeFileSync5(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
3331
+ return { ok: true, message: "guard removed (open a new session)" };
3245
3332
  } catch (e) {
3246
3333
  return { ok: false, message: e instanceof Error ? e.message : String(e) };
3247
3334
  }
@@ -3397,6 +3484,13 @@ function SettingsPanel({
3397
3484
  const abort = useRef3(false);
3398
3485
  const refreshAccounts = () => setAccounts(listAccounts());
3399
3486
  const [guardHere, setGuardHere] = useState7(() => isGuardInstalled());
3487
+ const [guardVer, setGuardVer] = useState7(() => installedGuardVersion());
3488
+ const [guardOld, setGuardOld] = useState7(() => guardHookOutdated());
3489
+ const refreshGuard = () => {
3490
+ setGuardHere(isGuardInstalled());
3491
+ setGuardVer(installedGuardVersion());
3492
+ setGuardOld(guardHookOutdated());
3493
+ };
3400
3494
  const locked = accounts.length === 0;
3401
3495
  const [srv, setSrv] = useState7(() => logsServerStatus());
3402
3496
  useEffect6(() => {
@@ -3414,8 +3508,20 @@ function SettingsPanel({
3414
3508
  const guardQ = useLoader(() => listAccounts().length ? api.settings.getGuardStatus() : Promise.resolve(null));
3415
3509
  const selfQ = useLoader(() => listAccounts().length ? api.settings.getSelfProtection() : Promise.resolve(null));
3416
3510
  const local = localQ.data;
3417
- const guard = guardQ.data;
3418
3511
  const selfProt = selfQ.data;
3512
+ const autoInstalledRef = useRef3(false);
3513
+ useEffect6(() => {
3514
+ if (autoInstalledRef.current) return;
3515
+ if (guardHere && guardOld) {
3516
+ autoInstalledRef.current = true;
3517
+ const res = installGlobalQuiet();
3518
+ if (res.ok) {
3519
+ refreshGuard();
3520
+ setMsg({ text: `\u2713 guard updated \u2192 v${installedGuardVersion() ?? "?"} (open a new session)`, level: "ok" });
3521
+ guardQ.reload();
3522
+ }
3523
+ }
3524
+ }, [guardHere, guardOld]);
3419
3525
  const [hidden, setHidden] = useState7(/* @__PURE__ */ new Set());
3420
3526
  const webhooks = (whQ.data?.webhooks ?? []).filter((w) => !hidden.has("wh:" + w.id));
3421
3527
  const alerts = (alertQ.data?.rules ?? []).filter((r) => !hidden.has("alert:" + r.id));
@@ -3541,19 +3647,10 @@ function SettingsPanel({
3541
3647
  } else if (r.kind === "acct-add") {
3542
3648
  beginLogin();
3543
3649
  } 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
- );
3650
+ const res = installGlobalQuiet();
3651
+ setMsg({ text: (res.ok ? "\u2713 " : "\u2717 ") + res.message, level: res.ok ? "ok" : "bad" });
3652
+ refreshGuard();
3653
+ guardQ.reload();
3557
3654
  } else if (r.kind === "self") {
3558
3655
  if (!selfProt) return;
3559
3656
  run(selfProt.enabled ? "self-protection disabled" : "self-protection enabled", () => api.settings.setSelfProtection(!selfProt.enabled), selfQ.reload);
@@ -3716,7 +3813,7 @@ function SettingsPanel({
3716
3813
  setConfirmDel(null);
3717
3814
  const res = uninstallGlobalQuiet();
3718
3815
  setMsg({ text: (res.ok ? "\u2713 " : "\u2717 ") + res.message, level: res.ok ? "ok" : "bad" });
3719
- setGuardHere(isGuardInstalled());
3816
+ refreshGuard();
3720
3817
  guardQ.reload();
3721
3818
  } else if (inp === "d" && (cur.kind === "wh" || cur.kind === "alert")) {
3722
3819
  const k = keyOf(cur);
@@ -3825,12 +3922,13 @@ function SettingsPanel({
3825
3922
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "guard".padEnd(11) }),
3826
3923
  !guardHere ? /* @__PURE__ */ jsxs7(Fragment4, { children: [
3827
3924
  /* @__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" })
3829
- ] }) : guard ? /* @__PURE__ */ jsxs7(Fragment4, { children: [
3830
- /* @__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` }),
3832
- /* @__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" })
3925
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \xB7 " }),
3926
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accentBright, children: "enter to install" })
3927
+ ] }) : /* @__PURE__ */ jsxs7(Fragment4, { children: [
3928
+ /* @__PURE__ */ jsx7(Text7, { color: guardOld ? theme.warn : theme.ok, children: `v${guardVer ?? "?"}` }),
3929
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: guardOld ? " \xB7 update available \xB7 enter update" : " (latest) \xB7 enter reinstall" }),
3930
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \xB7 d remove" })
3931
+ ] })
3834
3932
  ] });
3835
3933
  case "self":
3836
3934
  return /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
@@ -3894,7 +3992,7 @@ function SettingsPanel({
3894
3992
  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
3993
  const SECTION_DESC = {
3896
3994
  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",
3995
+ PROTECTION: "guard hook: enter install/update (writes the files) \xB7 d remove \xB7 + self-protection",
3898
3996
  "LOCAL LOGS": "mirror every decision to a file + dashboard link",
3899
3997
  WEBHOOKS: `POST events to a URL (${webhooks.length}) \xB7 t tests`,
3900
3998
  ALERTS: `one email + one telegram (${alerts.length}) \xB7 enter edits \xB7 m on/off`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.81.80",
3
+ "version": "0.81.82",
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": {