@solongate/proxy 0.83.14 → 0.83.16

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.
@@ -10,7 +10,7 @@ import { resolve as resolve2, join as join2 } from "path";
10
10
  import { homedir as homedir2 } from "os";
11
11
 
12
12
  // src/global-install.ts
13
- import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, chmodSync } from "fs";
13
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, rmdirSync, readdirSync, statSync, chmodSync } from "fs";
14
14
  import { resolve, join, dirname } from "path";
15
15
  import { homedir } from "os";
16
16
  import { fileURLToPath } from "url";
@@ -1185,12 +1185,12 @@ async function runAgent(argv) {
1185
1185
  }
1186
1186
 
1187
1187
  // src/commands/doctor.ts
1188
- import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
1188
+ import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
1189
1189
  import { homedir as homedir4 } from "os";
1190
1190
  import { join as join4 } from "path";
1191
1191
 
1192
1192
  // src/tui/local-log.ts
1193
- import { closeSync, existsSync as existsSync3, openSync, readdirSync, readFileSync as readFileSync4, readSync, statSync, writeFileSync as writeFileSync3 } from "fs";
1193
+ import { closeSync, existsSync as existsSync3, openSync, readdirSync as readdirSync2, readFileSync as readFileSync4, readSync, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
1194
1194
  import { homedir as homedir3 } from "os";
1195
1195
  import { createHash } from "crypto";
1196
1196
  import { isAbsolute, join as join3 } from "path";
@@ -1198,11 +1198,11 @@ var DEFAULT_LOCAL_LOG = join3(homedir3(), ".solongate", "local-logs", "solongate
1198
1198
  function localLogFile() {
1199
1199
  try {
1200
1200
  const sgDir = join3(homedir3(), ".solongate");
1201
- const caches = readdirSync(sgDir).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json")).map((f) => {
1201
+ const caches = readdirSync2(sgDir).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json")).map((f) => {
1202
1202
  const p = join3(sgDir, f);
1203
1203
  let mtime = 0;
1204
1204
  try {
1205
- mtime = statSync(p).mtimeMs;
1205
+ mtime = statSync2(p).mtimeMs;
1206
1206
  } catch {
1207
1207
  }
1208
1208
  return { p, mtime };
@@ -1316,7 +1316,7 @@ async function collectChecks() {
1316
1316
  }
1317
1317
  const LOCAL_LOG2 = localLogFile();
1318
1318
  if (existsSync4(LOCAL_LOG2)) {
1319
- const st = statSync2(LOCAL_LOG2);
1319
+ const st = statSync3(LOCAL_LOG2);
1320
1320
  const ageMin = (Date.now() - st.mtimeMs) / 6e4;
1321
1321
  checks.push({ name: "local logs", ok: true, detail: `on \xB7 ${(st.size / 1024).toFixed(0)}KB \xB7 last write ${ageMin < 1 ? "just now" : Math.round(ageMin) + "m ago"}` });
1322
1322
  } else {
@@ -1346,12 +1346,12 @@ async function run6(argv) {
1346
1346
  }
1347
1347
 
1348
1348
  // src/commands/watch.ts
1349
- import { closeSync as closeSync2, existsSync as existsSync5, openSync as openSync2, readSync as readSync2, statSync as statSync3 } from "fs";
1349
+ import { closeSync as closeSync2, existsSync as existsSync5, openSync as openSync2, readSync as readSync2, statSync as statSync4 } from "fs";
1350
1350
  var trunc = (s, n) => s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
1351
1351
  var time = (ms) => new Date(ms).toTimeString().slice(0, 8);
1352
1352
  function tailLocal(file, maxBytes = 131072) {
1353
1353
  try {
1354
- const size = statSync3(file).size;
1354
+ const size = statSync4(file).size;
1355
1355
  const start = Math.max(0, size - maxBytes);
1356
1356
  const fd = openSync2(file, "r");
1357
1357
  const buf = Buffer.alloc(size - start);
@@ -55,6 +55,7 @@ export declare function isOpencodeGuardInstalled(): boolean;
55
55
  /** True when OpenCode looks present on this device, so callers can decide
56
56
  * whether the OpenCode line is worth showing at all. */
57
57
  export declare function opencodeDetected(): boolean;
58
+ export declare function sweepStrayScratchDirs(root?: string, maxDepth?: number, budget?: number): number;
58
59
  export declare function runGlobalRestore(): void;
59
60
  /**
60
61
  * TUI-safe (re)install — the dataroom's one-key "install / update guard" action.
@@ -1,5 +1,5 @@
1
1
  // src/global-install.ts
2
- import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, chmodSync } from "fs";
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, rmdirSync, readdirSync, statSync, chmodSync } from "fs";
3
3
  import { resolve, join, dirname } from "path";
4
4
  import { homedir } from "os";
5
5
  import { fileURLToPath } from "url";
@@ -395,6 +395,57 @@ function opencodeDetected() {
395
395
  return false;
396
396
  }
397
397
  }
398
+ var SCRATCH_FILES = /* @__PURE__ */ new Set([".eval-ring.jsonl", ".last-eval", ".last-deny", ".last-tool-call", ".debug-guard-log"]);
399
+ function sweepStrayScratchDirs(root = homedir(), maxDepth = 6, budget = 4e4) {
400
+ let removed = 0;
401
+ let visited = 0;
402
+ const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build"]);
403
+ const store = join(homedir(), ".solongate");
404
+ const walk = (dir, depth) => {
405
+ if (depth > maxDepth || visited++ > budget) return;
406
+ let entries;
407
+ try {
408
+ entries = readdirSync(dir);
409
+ } catch {
410
+ return;
411
+ }
412
+ for (const name of entries) {
413
+ if (skip.has(name)) continue;
414
+ const full = join(dir, name);
415
+ if (full === store) continue;
416
+ let isDir = false;
417
+ try {
418
+ isDir = statSync(full).isDirectory();
419
+ } catch {
420
+ continue;
421
+ }
422
+ if (!isDir) continue;
423
+ if (name === ".solongate") {
424
+ try {
425
+ const left = [];
426
+ for (const f of readdirSync(full)) {
427
+ if (SCRATCH_FILES.has(f)) {
428
+ try {
429
+ rmSync(join(full, f), { force: true });
430
+ } catch {
431
+ left.push(f);
432
+ }
433
+ } else left.push(f);
434
+ }
435
+ if (left.length === 0) {
436
+ rmdirSync(full);
437
+ removed++;
438
+ }
439
+ } catch {
440
+ }
441
+ continue;
442
+ }
443
+ walk(full, depth + 1);
444
+ }
445
+ };
446
+ walk(root, 0);
447
+ return removed;
448
+ }
398
449
  function ask(question) {
399
450
  const rl = createInterface({ input: process.stdin, output: process.stderr });
400
451
  return new Promise((res) => rl.question(question, (a) => {
@@ -454,6 +505,11 @@ function repairQuiet() {
454
505
  line("OpenCode hooks", isOpencodeGuardInstalled(), "guard registered", "NOT registered")
455
506
  ];
456
507
  const notes = [];
508
+ try {
509
+ const swept = sweepStrayScratchDirs();
510
+ if (swept > 0) notes.push(`Removed ${swept} leftover .solongate scratch folder(s) from earlier versions; per-call flags now live under ~/.solongate/projects.`);
511
+ } catch {
512
+ }
457
513
  const cx = codexHooksStatus();
458
514
  if (cx.registered && !cx.trusted) {
459
515
  notes.push("Codex only: run `/hooks` inside Codex once and trust the SolonGate hooks (Codex skips any hook it has not been told to trust).");
@@ -807,6 +863,7 @@ export {
807
863
  runGlobalInstall,
808
864
  runGlobalRestore,
809
865
  runRepair,
866
+ sweepStrayScratchDirs,
810
867
  uninstallGlobalQuiet,
811
868
  unlockProtected,
812
869
  writeProtectedFile
package/dist/index.js CHANGED
@@ -3043,12 +3043,12 @@ ${ctx.indent}`;
3043
3043
  for (const {
3044
3044
  format,
3045
3045
  test,
3046
- resolve: resolve6
3046
+ resolve: resolve7
3047
3047
  } of tags) {
3048
3048
  if (test) {
3049
3049
  const match = str.match(test);
3050
3050
  if (match) {
3051
- let res = resolve6.apply(null, match);
3051
+ let res = resolve7.apply(null, match);
3052
3052
  if (!(res instanceof Scalar)) res = new Scalar(res);
3053
3053
  if (format) res.format = format;
3054
3054
  return res;
@@ -6209,11 +6209,12 @@ __export(global_install_exports, {
6209
6209
  runGlobalInstall: () => runGlobalInstall,
6210
6210
  runGlobalRestore: () => runGlobalRestore,
6211
6211
  runRepair: () => runRepair,
6212
+ sweepStrayScratchDirs: () => sweepStrayScratchDirs,
6212
6213
  uninstallGlobalQuiet: () => uninstallGlobalQuiet,
6213
6214
  unlockProtected: () => unlockProtected,
6214
6215
  writeProtectedFile: () => writeProtectedFile
6215
6216
  });
6216
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync3, rmSync as rmSync2, chmodSync } from "fs";
6217
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync3, rmSync as rmSync2, rmdirSync, readdirSync, statSync, chmodSync } from "fs";
6217
6218
  import { resolve as resolve3, join as join4, dirname } from "path";
6218
6219
  import { homedir as homedir2 } from "os";
6219
6220
  import { fileURLToPath } from "url";
@@ -6604,6 +6605,56 @@ function opencodeDetected() {
6604
6605
  return false;
6605
6606
  }
6606
6607
  }
6608
+ function sweepStrayScratchDirs(root = homedir2(), maxDepth = 6, budget = 4e4) {
6609
+ let removed = 0;
6610
+ let visited = 0;
6611
+ const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build"]);
6612
+ const store = join4(homedir2(), ".solongate");
6613
+ const walk = (dir, depth) => {
6614
+ if (depth > maxDepth || visited++ > budget) return;
6615
+ let entries;
6616
+ try {
6617
+ entries = readdirSync(dir);
6618
+ } catch {
6619
+ return;
6620
+ }
6621
+ for (const name of entries) {
6622
+ if (skip.has(name)) continue;
6623
+ const full = join4(dir, name);
6624
+ if (full === store) continue;
6625
+ let isDir = false;
6626
+ try {
6627
+ isDir = statSync(full).isDirectory();
6628
+ } catch {
6629
+ continue;
6630
+ }
6631
+ if (!isDir) continue;
6632
+ if (name === ".solongate") {
6633
+ try {
6634
+ const left = [];
6635
+ for (const f of readdirSync(full)) {
6636
+ if (SCRATCH_FILES.has(f)) {
6637
+ try {
6638
+ rmSync2(join4(full, f), { force: true });
6639
+ } catch {
6640
+ left.push(f);
6641
+ }
6642
+ } else left.push(f);
6643
+ }
6644
+ if (left.length === 0) {
6645
+ rmdirSync(full);
6646
+ removed++;
6647
+ }
6648
+ } catch {
6649
+ }
6650
+ continue;
6651
+ }
6652
+ walk(full, depth + 1);
6653
+ }
6654
+ };
6655
+ walk(root, 0);
6656
+ return removed;
6657
+ }
6607
6658
  function ask(question) {
6608
6659
  const rl = createInterface({ input: process.stdin, output: process.stderr });
6609
6660
  return new Promise((res) => rl.question(question, (a) => {
@@ -6663,6 +6714,11 @@ function repairQuiet() {
6663
6714
  line("OpenCode hooks", isOpencodeGuardInstalled(), "guard registered", "NOT registered")
6664
6715
  ];
6665
6716
  const notes = [];
6717
+ try {
6718
+ const swept = sweepStrayScratchDirs();
6719
+ if (swept > 0) notes.push(`Removed ${swept} leftover .solongate scratch folder(s) from earlier versions; per-call flags now live under ~/.solongate/projects.`);
6720
+ } catch {
6721
+ }
6666
6722
  const cx = codexHooksStatus();
6667
6723
  if (cx.registered && !cx.trusted) {
6668
6724
  notes.push("Codex only: run `/hooks` inside Codex once and trust the SolonGate hooks (Codex skips any hook it has not been told to trust).");
@@ -6994,7 +7050,7 @@ async function runGlobalInstall(opts = {}) {
6994
7050
  async function installGlobalWithKey(apiKey, apiUrl) {
6995
7051
  await runGlobalInstall({ apiKey, apiUrl });
6996
7052
  }
6997
- var __dirname, HOOKS_DIR, ANTIGRAVITY_GROUP, CODEX_EVENTS, CODEX_TIMEOUT_SEC, SHIM_BEGIN, SHIM_END;
7053
+ var __dirname, HOOKS_DIR, ANTIGRAVITY_GROUP, CODEX_EVENTS, CODEX_TIMEOUT_SEC, SCRATCH_FILES, SHIM_BEGIN, SHIM_END;
6998
7054
  var init_global_install = __esm({
6999
7055
  "src/global-install.ts"() {
7000
7056
  "use strict";
@@ -7003,6 +7059,7 @@ var init_global_install = __esm({
7003
7059
  ANTIGRAVITY_GROUP = "solongate-guard";
7004
7060
  CODEX_EVENTS = ["PreToolUse", "PostToolUse", "Stop"];
7005
7061
  CODEX_TIMEOUT_SEC = 30;
7062
+ SCRATCH_FILES = /* @__PURE__ */ new Set([".eval-ring.jsonl", ".last-eval", ".last-deny", ".last-tool-call", ".debug-guard-log"]);
7006
7063
  SHIM_BEGIN = "# >>> SolonGate shield (auto secret redaction) >>>";
7007
7064
  SHIM_END = "# <<< SolonGate shield <<<";
7008
7065
  }
@@ -7183,7 +7240,7 @@ async function runUpdateCommand() {
7183
7240
  return 0;
7184
7241
  }
7185
7242
  function runGlobalInstall2(version) {
7186
- return new Promise((resolve6) => {
7243
+ return new Promise((resolve7) => {
7187
7244
  try {
7188
7245
  mkdirSync4(join5(homedir3(), ".solongate"), { recursive: true });
7189
7246
  execFile(
@@ -7199,11 +7256,11 @@ ${output}
7199
7256
  `, { flag: "a" });
7200
7257
  } catch {
7201
7258
  }
7202
- resolve6({ ok: !err2, needsAdmin: !!err2 && NEEDS_ADMIN_RE.test(output) });
7259
+ resolve7({ ok: !err2, needsAdmin: !!err2 && NEEDS_ADMIN_RE.test(output) });
7203
7260
  }
7204
7261
  );
7205
7262
  } catch {
7206
- resolve6({ ok: false, needsAdmin: false });
7263
+ resolve7({ ok: false, needsAdmin: false });
7207
7264
  }
7208
7265
  });
7209
7266
  }
@@ -7224,13 +7281,13 @@ function ownNodeModules() {
7224
7281
  return null;
7225
7282
  }
7226
7283
  function globalInstallCheck() {
7227
- prefixCheck ??= new Promise((resolve6) => {
7284
+ prefixCheck ??= new Promise((resolve7) => {
7228
7285
  try {
7229
7286
  const dir = ownNodeModules();
7230
- if (!dir) return resolve6({ writable: null, dir: null });
7231
- access(dir, FS.W_OK, (e) => resolve6({ writable: !e, dir }));
7287
+ if (!dir) return resolve7({ writable: null, dir: null });
7288
+ access(dir, FS.W_OK, (e) => resolve7({ writable: !e, dir }));
7232
7289
  } catch {
7233
- resolve6({ writable: null, dir: null });
7290
+ resolve7({ writable: null, dir: null });
7234
7291
  }
7235
7292
  });
7236
7293
  return prefixCheck;
@@ -7581,7 +7638,7 @@ function StreamLine({ e, loc, selected, date }) {
7581
7638
  hhmmss(e.at),
7582
7639
  " "
7583
7640
  ] }),
7584
- /* @__PURE__ */ jsx(Text, { color: loc ? theme.ok : "white", children: loc ? "LOC" : "CLD" }),
7641
+ /* @__PURE__ */ jsx(Text, { color: loc ? theme.ok : "white", children: loc ? "HERE" : "AWAY" }),
7585
7642
  /* @__PURE__ */ jsx(Text, { color: theme.dim, children: "] " }),
7586
7643
  /* @__PURE__ */ jsx(Text, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
7587
7644
  /* @__PURE__ */ jsx(Text, { color: theme.accent, children: truncate2(e.tool, 12).padEnd(13) }),
@@ -7647,7 +7704,7 @@ var init_components = __esm({
7647
7704
  });
7648
7705
 
7649
7706
  // src/tui/local-log.ts
7650
- import { closeSync, existsSync as existsSync4, openSync as openSync3, readdirSync, readFileSync as readFileSync8, readSync, statSync, writeFileSync as writeFileSync6 } from "fs";
7707
+ import { closeSync, existsSync as existsSync4, openSync as openSync3, readdirSync as readdirSync2, readFileSync as readFileSync8, readSync, statSync as statSync2, writeFileSync as writeFileSync6 } from "fs";
7651
7708
  import { homedir as homedir6 } from "os";
7652
7709
  import { createHash as createHash2 } from "crypto";
7653
7710
  import { isAbsolute, join as join8 } from "path";
@@ -7676,11 +7733,11 @@ function localLogsSetting() {
7676
7733
  }
7677
7734
  function policyCachesNewestFirst() {
7678
7735
  const sgDir = join8(homedir6(), ".solongate");
7679
- return readdirSync(sgDir).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json")).map((f) => {
7736
+ return readdirSync2(sgDir).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json")).map((f) => {
7680
7737
  const p = join8(sgDir, f);
7681
7738
  let mtime = 0;
7682
7739
  try {
7683
- mtime = statSync(p).mtimeMs;
7740
+ mtime = statSync2(p).mtimeMs;
7684
7741
  } catch {
7685
7742
  }
7686
7743
  return { p, mtime };
@@ -7689,11 +7746,11 @@ function policyCachesNewestFirst() {
7689
7746
  function localLogFile() {
7690
7747
  try {
7691
7748
  const sgDir = join8(homedir6(), ".solongate");
7692
- const caches = readdirSync(sgDir).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json")).map((f) => {
7749
+ const caches = readdirSync2(sgDir).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json")).map((f) => {
7693
7750
  const p = join8(sgDir, f);
7694
7751
  let mtime = 0;
7695
7752
  try {
7696
- mtime = statSync(p).mtimeMs;
7753
+ mtime = statSync2(p).mtimeMs;
7697
7754
  } catch {
7698
7755
  }
7699
7756
  return { p, mtime };
@@ -7737,7 +7794,7 @@ function ensureLocalLogOwner(activeApiKey) {
7737
7794
  }
7738
7795
  function tailLines(file, maxBytes = 131072) {
7739
7796
  try {
7740
- const size = statSync(file).size;
7797
+ const size = statSync2(file).size;
7741
7798
  const start = Math.max(0, size - maxBytes);
7742
7799
  const fd = openSync3(file, "r");
7743
7800
  const buf = Buffer.alloc(size - start);
@@ -8536,7 +8593,7 @@ import { Box as Box2, Text as Text2, useInput } from "ink";
8536
8593
  import TextInput from "ink-text-input";
8537
8594
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
8538
8595
  import { homedir as homedir8 } from "os";
8539
- import { join as join10 } from "path";
8596
+ import { join as join10, resolve as resolve5 } from "path";
8540
8597
  import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
8541
8598
  import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
8542
8599
  function extractTarget(detail) {
@@ -9175,7 +9232,7 @@ function LivePanel({ active: active2 }) {
9175
9232
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "white", bold: true, children: " ENTRY " }),
9176
9233
  /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: true, children: " " + e.decision }),
9177
9234
  /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: " " + e.tool }),
9178
- /* @__PURE__ */ jsx2(Text2, { color: isLoc(e) ? theme.ok : "white", children: " " + (isLoc(e) ? "LOC" : "CLD") }),
9235
+ /* @__PURE__ */ jsx2(Text2, { color: isLoc(e) ? theme.ok : "white", children: " " + (isLoc(e) ? "HERE" : "AWAY") }),
9179
9236
  e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: " DLP!" }) : null,
9180
9237
  e.burst ? /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: " BURST" }) : null,
9181
9238
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2190 back" })
@@ -9500,18 +9557,18 @@ function LivePanel({ active: active2 }) {
9500
9557
  filter === "all" ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
9501
9558
  spin,
9502
9559
  " awaiting traffic\u2026"
9503
- ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `${spin} no ${filter === "local" ? "LOC (this machine)" : "CLD (other devices)"} calls in the buffer \xB7 f switches source` })
9560
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `${spin} no ${filter === "local" ? "HERE (this machine)" : "AWAY (other devices)"} calls in the buffer \xB7 f switches origin` })
9504
9561
  ) : null,
9505
9562
  windowed.map((e, i) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e), selected: clampedScroll + i === selClamped }, e.id))
9506
9563
  ] }),
9507
9564
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
9508
9565
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "white", bold: true, children: " LIVE " }),
9509
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "white", children: ` local ${localOn ? "on" : "off"} \xB7 ${localBuf.length} loc/${cloudBuf.length} cld \xB7 source ${filter === "all" ? "all" : filter === "local" ? "LOC only" : "CLD only"} \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
9510
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "white", children: ` \u2191\u2193 select \xB7 enter full entry \xB7 f source \xB7 / search \xB7 space copy \xB7 ? all keys \xB7 esc menu \xB7 q quit ` })
9566
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "white", children: ` local-log ${localOn ? "on" : "off"} \xB7 ${localBuf.length} here/${cloudBuf.length} away \xB7 origin ${filter === "all" ? "all" : filter === "local" ? "HERE only" : "AWAY only"} \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
9567
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "white", children: ` \u2191\u2193 select \xB7 enter full entry \xB7 f origin \xB7 / search \xB7 space copy \xB7 ? all keys \xB7 esc menu \xB7 q quit ` })
9511
9568
  ] })
9512
9569
  ] });
9513
9570
  }
9514
- var CONFIG, SPIN, BG, DIM_FLOOR, RING, fmtUp, FILTERS, sessStatus, STATUS_STYLE, LIVE_HELP;
9571
+ var CONFIG, SPIN, BG, DIM_FLOOR, projectKey, RING, fmtUp, FILTERS, sessStatus, STATUS_STYLE, LIVE_HELP;
9515
9572
  var init_Live = __esm({
9516
9573
  "src/tui/panels/Live.tsx"() {
9517
9574
  "use strict";
@@ -9526,7 +9583,15 @@ var init_Live = __esm({
9526
9583
  SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
9527
9584
  BG = "#12234f";
9528
9585
  DIM_FLOOR = "#233457";
9529
- RING = join10(process.cwd(), ".solongate", ".eval-ring.jsonl");
9586
+ projectKey = (dir) => {
9587
+ let h = 2166136261;
9588
+ for (let i = 0; i < dir.length; i++) {
9589
+ h ^= dir.charCodeAt(i);
9590
+ h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
9591
+ }
9592
+ return h.toString(16);
9593
+ };
9594
+ RING = join10(homedir8(), ".solongate", "projects", projectKey(resolve5(process.cwd())), ".eval-ring.jsonl");
9530
9595
  fmtUp = (ms) => {
9531
9596
  const s = Math.floor(ms / 1e3);
9532
9597
  const p = (n) => String(n).padStart(2, "0");
@@ -9548,7 +9613,7 @@ var init_Live = __esm({
9548
9613
  ["w", "whitelist the selected DENY (adds ALLOW rule)"],
9549
9614
  ["b", "block the selected ALLOW (adds DENY rule)"],
9550
9615
  ["d / x / r", "filter: denies / dlp hits / rate-limit bursts"],
9551
- ["f", "source: all \u2192 LOC (this machine) \u2192 CLD (other devices)"],
9616
+ ["f", "origin: all \u2192 HERE (this machine) \u2192 AWAY (other devices)"],
9552
9617
  ["/", "live search (tool, agent, command\u2026) \xB7 enter done"],
9553
9618
  ["s", "session picker"],
9554
9619
  ["l", "layers detail (rate limit \xB7 dlp \xB7 ghost \xB7 guard)"],
@@ -11187,7 +11252,7 @@ function AuditPanel({ active: active2, focused }) {
11187
11252
  /* @__PURE__ */ jsx7(Text7, { backgroundColor: BG2, color: "white", bold: true, children: " ENTRY " }),
11188
11253
  /* @__PURE__ */ jsx7(Text7, { color: decisionColor(e.decision), bold: true, children: " " + e.decision }),
11189
11254
  /* @__PURE__ */ jsx7(Text7, { color: theme.accent, bold: true, children: " " + e.tool }),
11190
- /* @__PURE__ */ jsx7(Text7, { color: loc ? theme.ok : "white", children: " " + (loc ? "LOC" : "CLD") }),
11255
+ /* @__PURE__ */ jsx7(Text7, { color: loc ? theme.ok : "white", children: " " + (loc ? "HERE" : "AWAY") }),
11191
11256
  e.dlp.length ? /* @__PURE__ */ jsx7(Text7, { color: theme.bad, children: " DLP!" }) : null,
11192
11257
  e.burst ? /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: " BURST" }) : null,
11193
11258
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2190 back" })
@@ -11563,7 +11628,7 @@ var init_args = __esm({
11563
11628
  });
11564
11629
 
11565
11630
  // src/commands/doctor.ts
11566
- import { existsSync as existsSync6, readFileSync as readFileSync10, statSync as statSync2 } from "fs";
11631
+ import { existsSync as existsSync6, readFileSync as readFileSync10, statSync as statSync3 } from "fs";
11567
11632
  import { homedir as homedir10 } from "os";
11568
11633
  import { join as join12 } from "path";
11569
11634
  async function collectChecks() {
@@ -11654,7 +11719,7 @@ async function collectChecks() {
11654
11719
  }
11655
11720
  const LOCAL_LOG2 = localLogFile();
11656
11721
  if (existsSync6(LOCAL_LOG2)) {
11657
- const st = statSync2(LOCAL_LOG2);
11722
+ const st = statSync3(LOCAL_LOG2);
11658
11723
  const ageMin = (Date.now() - st.mtimeMs) / 6e4;
11659
11724
  checks.push({ name: "local logs", ok: true, detail: `on \xB7 ${(st.size / 1024).toFixed(0)}KB \xB7 last write ${ageMin < 1 ? "just now" : Math.round(ageMin) + "m ago"}` });
11660
11725
  } else {
@@ -13397,10 +13462,10 @@ var init_agents2 = __esm({
13397
13462
  });
13398
13463
 
13399
13464
  // src/commands/watch.ts
13400
- import { closeSync as closeSync2, existsSync as existsSync7, openSync as openSync4, readSync as readSync2, statSync as statSync3 } from "fs";
13465
+ import { closeSync as closeSync2, existsSync as existsSync7, openSync as openSync4, readSync as readSync2, statSync as statSync4 } from "fs";
13401
13466
  function tailLocal(file, maxBytes = 131072) {
13402
13467
  try {
13403
- const size = statSync3(file).size;
13468
+ const size = statSync4(file).size;
13404
13469
  const start = Math.max(0, size - maxBytes);
13405
13470
  const fd = openSync4(file, "r");
13406
13471
  const buf = Buffer.alloc(size - start);
@@ -13708,10 +13773,10 @@ __export(logs_server_exports, {
13708
13773
  runLogsServer: () => runLogsServer
13709
13774
  });
13710
13775
  import { createServer } from "http";
13711
- import { readFileSync as readFileSync12, statSync as statSync4 } from "fs";
13712
- import { resolve as resolve5, join as join14, isAbsolute as isAbsolute2 } from "path";
13776
+ import { readFileSync as readFileSync12, statSync as statSync5 } from "fs";
13777
+ import { resolve as resolve6, join as join14, isAbsolute as isAbsolute2 } from "path";
13713
13778
  import { homedir as homedir12 } from "os";
13714
- import { readdirSync as readdirSync2 } from "fs";
13779
+ import { readdirSync as readdirSync3 } from "fs";
13715
13780
  function allowedOrigins() {
13716
13781
  const base = [
13717
13782
  "https://dashboard.solongate.com",
@@ -13727,12 +13792,12 @@ function resolveLocalLogDir(rawPath) {
13727
13792
  const dir = String(rawPath || "").trim().replace(/[\\/]+$/, "");
13728
13793
  if (!dir) return null;
13729
13794
  if (isAbsolute2(dir)) return dir;
13730
- return resolve5(homedir12(), ".solongate", "local-logs");
13795
+ return resolve6(homedir12(), ".solongate", "local-logs");
13731
13796
  }
13732
13797
  async function findLogDir() {
13733
- const base = resolve5(homedir12(), ".solongate");
13798
+ const base = resolve6(homedir12(), ".solongate");
13734
13799
  try {
13735
- const files = readdirSync2(base).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json"));
13800
+ const files = readdirSync3(base).filter((f) => f.startsWith(".policy-cache-") && f.endsWith(".json"));
13736
13801
  for (const f of files) {
13737
13802
  try {
13738
13803
  const c2 = JSON.parse(readFileSync12(join14(base, f), "utf-8"));
@@ -13775,7 +13840,7 @@ function fileInfo(dir) {
13775
13840
  if (!dir) return { file: null, exists: false, size: 0, mtimeMs: 0 };
13776
13841
  const file = join14(dir, LOG_FILENAME);
13777
13842
  try {
13778
- const st = statSync4(file);
13843
+ const st = statSync5(file);
13779
13844
  return { file, exists: true, size: st.size, mtimeMs: st.mtimeMs };
13780
13845
  } catch {
13781
13846
  return { file, exists: false, size: 0, mtimeMs: 0 };
@@ -16687,7 +16752,7 @@ var Mutex = class {
16687
16752
  this.locked = true;
16688
16753
  return;
16689
16754
  }
16690
- return new Promise((resolve6, reject) => {
16755
+ return new Promise((resolve7, reject) => {
16691
16756
  const timer = setTimeout(() => {
16692
16757
  const idx = this.queue.indexOf(onReady);
16693
16758
  if (idx !== -1) this.queue.splice(idx, 1);
@@ -16695,7 +16760,7 @@ var Mutex = class {
16695
16760
  }, timeoutMs);
16696
16761
  const onReady = () => {
16697
16762
  clearTimeout(timer);
16698
- resolve6();
16763
+ resolve7();
16699
16764
  };
16700
16765
  this.queue.push(onReady);
16701
16766
  });
@@ -11,7 +11,11 @@ export declare function PaneTitle({ label, extra, width }: {
11
11
  }): JSX.Element;
12
12
  /**
13
13
  * One tool-stream row, shared verbatim by Live (rolling buffer) and Audit (full
14
- * history). `loc` tags LOC vs CLD; `selected` highlights + shows the cursor.
14
+ * history). `loc` tags where the call HAPPENED HERE (this machine) vs AWAY
15
+ * (another paired device). Deliberately not LOC/CLD: `local` already names the
16
+ * local-log STORAGE setting shown in the same panel, and one word meaning two
17
+ * things two lines apart had people reading a cloud-stored call as on-disk.
18
+ * `selected` highlights + shows the ▸ cursor.
15
19
  * Columns are fixed-width and NEVER conditional so scrolling can't reshuffle the
16
20
  * line and glitch the frame.
17
21
  */
package/dist/tui/index.js CHANGED
@@ -9,7 +9,7 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/global-install.ts
12
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync, rmSync, chmodSync } from "fs";
12
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync, rmSync, rmdirSync, readdirSync as readdirSync2, statSync as statSync2, chmodSync } from "fs";
13
13
  import { resolve, join as join3, dirname } from "path";
14
14
  import { homedir as homedir3 } from "os";
15
15
  import { fileURLToPath } from "url";
@@ -392,6 +392,56 @@ function opencodeDetected() {
392
392
  return false;
393
393
  }
394
394
  }
395
+ function sweepStrayScratchDirs(root = homedir3(), maxDepth = 6, budget = 4e4) {
396
+ let removed = 0;
397
+ let visited = 0;
398
+ const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build"]);
399
+ const store = join3(homedir3(), ".solongate");
400
+ const walk = (dir, depth) => {
401
+ if (depth > maxDepth || visited++ > budget) return;
402
+ let entries;
403
+ try {
404
+ entries = readdirSync2(dir);
405
+ } catch {
406
+ return;
407
+ }
408
+ for (const name of entries) {
409
+ if (skip.has(name)) continue;
410
+ const full = join3(dir, name);
411
+ if (full === store) continue;
412
+ let isDir = false;
413
+ try {
414
+ isDir = statSync2(full).isDirectory();
415
+ } catch {
416
+ continue;
417
+ }
418
+ if (!isDir) continue;
419
+ if (name === ".solongate") {
420
+ try {
421
+ const left = [];
422
+ for (const f of readdirSync2(full)) {
423
+ if (SCRATCH_FILES.has(f)) {
424
+ try {
425
+ rmSync(join3(full, f), { force: true });
426
+ } catch {
427
+ left.push(f);
428
+ }
429
+ } else left.push(f);
430
+ }
431
+ if (left.length === 0) {
432
+ rmdirSync(full);
433
+ removed++;
434
+ }
435
+ } catch {
436
+ }
437
+ continue;
438
+ }
439
+ walk(full, depth + 1);
440
+ }
441
+ };
442
+ walk(root, 0);
443
+ return removed;
444
+ }
395
445
  function repairQuiet() {
396
446
  const p = globalPaths();
397
447
  const has = (f) => existsSync2(f);
@@ -415,6 +465,11 @@ function repairQuiet() {
415
465
  line("OpenCode hooks", isOpencodeGuardInstalled(), "guard registered", "NOT registered")
416
466
  ];
417
467
  const notes = [];
468
+ try {
469
+ const swept = sweepStrayScratchDirs();
470
+ if (swept > 0) notes.push(`Removed ${swept} leftover .solongate scratch folder(s) from earlier versions; per-call flags now live under ~/.solongate/projects.`);
471
+ } catch {
472
+ }
418
473
  const cx = codexHooksStatus();
419
474
  if (cx.registered && !cx.trusted) {
420
475
  notes.push("Codex only: run `/hooks` inside Codex once and trust the SolonGate hooks (Codex skips any hook it has not been told to trust).");
@@ -579,7 +634,7 @@ function removeClaudeShim() {
579
634
  }
580
635
  }
581
636
  }
582
- var __dirname, HOOKS_DIR, ANTIGRAVITY_GROUP, CODEX_EVENTS, CODEX_TIMEOUT_SEC, SHIM_BEGIN, SHIM_END;
637
+ var __dirname, HOOKS_DIR, ANTIGRAVITY_GROUP, CODEX_EVENTS, CODEX_TIMEOUT_SEC, SCRATCH_FILES, SHIM_BEGIN, SHIM_END;
583
638
  var init_global_install = __esm({
584
639
  "src/global-install.ts"() {
585
640
  "use strict";
@@ -588,6 +643,7 @@ var init_global_install = __esm({
588
643
  ANTIGRAVITY_GROUP = "solongate-guard";
589
644
  CODEX_EVENTS = ["PreToolUse", "PostToolUse", "Stop"];
590
645
  CODEX_TIMEOUT_SEC = 30;
646
+ SCRATCH_FILES = /* @__PURE__ */ new Set([".eval-ring.jsonl", ".last-eval", ".last-deny", ".last-tool-call", ".debug-guard-log"]);
591
647
  SHIM_BEGIN = "# >>> SolonGate shield (auto secret redaction) >>>";
592
648
  SHIM_END = "# <<< SolonGate shield <<<";
593
649
  }
@@ -745,7 +801,7 @@ function StreamLine({ e, loc, selected, date }) {
745
801
  hhmmss(e.at),
746
802
  " "
747
803
  ] }),
748
- /* @__PURE__ */ jsx(Text, { color: loc ? theme.ok : "white", children: loc ? "LOC" : "CLD" }),
804
+ /* @__PURE__ */ jsx(Text, { color: loc ? theme.ok : "white", children: loc ? "HERE" : "AWAY" }),
749
805
  /* @__PURE__ */ jsx(Text, { color: theme.dim, children: "] " }),
750
806
  /* @__PURE__ */ jsx(Text, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
751
807
  /* @__PURE__ */ jsx(Text, { color: theme.accent, children: truncate(e.tool, 12).padEnd(13) }),
@@ -798,7 +854,7 @@ import { Box as Box2, Text as Text2, useInput } from "ink";
798
854
  import TextInput from "ink-text-input";
799
855
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
800
856
  import { homedir as homedir5 } from "os";
801
- import { join as join5 } from "path";
857
+ import { join as join5, resolve as resolve3 } from "path";
802
858
 
803
859
  // src/tui/local-log.ts
804
860
  import { closeSync, existsSync, openSync, readdirSync, readFileSync as readFileSync2, readSync, statSync, writeFileSync } from "fs";
@@ -1594,7 +1650,15 @@ var CONFIG = loadConfig();
1594
1650
  var SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1595
1651
  var BG = "#12234f";
1596
1652
  var DIM_FLOOR = "#233457";
1597
- var RING = join5(process.cwd(), ".solongate", ".eval-ring.jsonl");
1653
+ var projectKey = (dir) => {
1654
+ let h = 2166136261;
1655
+ for (let i = 0; i < dir.length; i++) {
1656
+ h ^= dir.charCodeAt(i);
1657
+ h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
1658
+ }
1659
+ return h.toString(16);
1660
+ };
1661
+ var RING = join5(homedir5(), ".solongate", "projects", projectKey(resolve3(process.cwd())), ".eval-ring.jsonl");
1598
1662
  var fmtUp = (ms) => {
1599
1663
  const s = Math.floor(ms / 1e3);
1600
1664
  const p = (n) => String(n).padStart(2, "0");
@@ -1682,7 +1746,7 @@ var LIVE_HELP = [
1682
1746
  ["w", "whitelist the selected DENY (adds ALLOW rule)"],
1683
1747
  ["b", "block the selected ALLOW (adds DENY rule)"],
1684
1748
  ["d / x / r", "filter: denies / dlp hits / rate-limit bursts"],
1685
- ["f", "source: all \u2192 LOC (this machine) \u2192 CLD (other devices)"],
1749
+ ["f", "origin: all \u2192 HERE (this machine) \u2192 AWAY (other devices)"],
1686
1750
  ["/", "live search (tool, agent, command\u2026) \xB7 enter done"],
1687
1751
  ["s", "session picker"],
1688
1752
  ["l", "layers detail (rate limit \xB7 dlp \xB7 ghost \xB7 guard)"],
@@ -2283,7 +2347,7 @@ function LivePanel({ active: active2 }) {
2283
2347
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "white", bold: true, children: " ENTRY " }),
2284
2348
  /* @__PURE__ */ jsx2(Text2, { color: decisionColor(e.decision), bold: true, children: " " + e.decision }),
2285
2349
  /* @__PURE__ */ jsx2(Text2, { color: theme.accent, bold: true, children: " " + e.tool }),
2286
- /* @__PURE__ */ jsx2(Text2, { color: isLoc(e) ? theme.ok : "white", children: " " + (isLoc(e) ? "LOC" : "CLD") }),
2350
+ /* @__PURE__ */ jsx2(Text2, { color: isLoc(e) ? theme.ok : "white", children: " " + (isLoc(e) ? "HERE" : "AWAY") }),
2287
2351
  e.dlp ? /* @__PURE__ */ jsx2(Text2, { color: theme.bad, children: " DLP!" }) : null,
2288
2352
  e.burst ? /* @__PURE__ */ jsx2(Text2, { color: theme.warn, children: " BURST" }) : null,
2289
2353
  /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: " \u2190 back" })
@@ -2608,14 +2672,14 @@ function LivePanel({ active: active2 }) {
2608
2672
  filter === "all" ? /* @__PURE__ */ jsxs2(Text2, { color: theme.dim, children: [
2609
2673
  spin,
2610
2674
  " awaiting traffic\u2026"
2611
- ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `${spin} no ${filter === "local" ? "LOC (this machine)" : "CLD (other devices)"} calls in the buffer \xB7 f switches source` })
2675
+ ] }) : /* @__PURE__ */ jsx2(Text2, { color: theme.dim, children: `${spin} no ${filter === "local" ? "HERE (this machine)" : "AWAY (other devices)"} calls in the buffer \xB7 f switches origin` })
2612
2676
  ) : null,
2613
2677
  windowed.map((e, i) => /* @__PURE__ */ jsx2(StreamLine, { e, loc: isLoc(e), selected: clampedScroll + i === selClamped }, e.id))
2614
2678
  ] }),
2615
2679
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate", children: [
2616
2680
  /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "white", bold: true, children: " LIVE " }),
2617
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "white", children: ` local ${localOn ? "on" : "off"} \xB7 ${localBuf.length} loc/${cloudBuf.length} cld \xB7 source ${filter === "all" ? "all" : filter === "local" ? "LOC only" : "CLD only"} \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
2618
- /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "white", children: ` \u2191\u2193 select \xB7 enter full entry \xB7 f source \xB7 / search \xB7 space copy \xB7 ? all keys \xB7 esc menu \xB7 q quit ` })
2681
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: "#0b1530", color: "white", children: ` local-log ${localOn ? "on" : "off"} \xB7 ${localBuf.length} here/${cloudBuf.length} away \xB7 origin ${filter === "all" ? "all" : filter === "local" ? "HERE only" : "AWAY only"} \xB7 top ${topTools.map(([t, c2]) => `${t}\xD7${c2}`).join(" ") || "\u2014"} ` }),
2682
+ /* @__PURE__ */ jsx2(Text2, { backgroundColor: BG, color: "white", children: ` \u2191\u2193 select \xB7 enter full entry \xB7 f origin \xB7 / search \xB7 space copy \xB7 ? all keys \xB7 esc menu \xB7 q quit ` })
2619
2683
  ] })
2620
2684
  ] });
2621
2685
  }
@@ -4262,7 +4326,7 @@ function AuditPanel({ active: active2, focused }) {
4262
4326
  /* @__PURE__ */ jsx7(Text7, { backgroundColor: BG2, color: "white", bold: true, children: " ENTRY " }),
4263
4327
  /* @__PURE__ */ jsx7(Text7, { color: decisionColor(e.decision), bold: true, children: " " + e.decision }),
4264
4328
  /* @__PURE__ */ jsx7(Text7, { color: theme.accent, bold: true, children: " " + e.tool }),
4265
- /* @__PURE__ */ jsx7(Text7, { color: loc ? theme.ok : "white", children: " " + (loc ? "LOC" : "CLD") }),
4329
+ /* @__PURE__ */ jsx7(Text7, { color: loc ? theme.ok : "white", children: " " + (loc ? "HERE" : "AWAY") }),
4266
4330
  e.dlp.length ? /* @__PURE__ */ jsx7(Text7, { color: theme.bad, children: " DLP!" }) : null,
4267
4331
  e.burst ? /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: " BURST" }) : null,
4268
4332
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2190 back" })
@@ -4431,7 +4495,7 @@ import { useEffect as useEffect7, useRef as useRef3, useState as useState8 } fro
4431
4495
  init_global_install();
4432
4496
 
4433
4497
  // src/commands/doctor.ts
4434
- import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
4498
+ import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
4435
4499
  import { homedir as homedir7 } from "os";
4436
4500
  import { join as join7 } from "path";
4437
4501
  init_global_install();
@@ -4523,7 +4587,7 @@ async function collectChecks() {
4523
4587
  }
4524
4588
  const LOCAL_LOG2 = localLogFile();
4525
4589
  if (existsSync4(LOCAL_LOG2)) {
4526
- const st = statSync2(LOCAL_LOG2);
4590
+ const st = statSync3(LOCAL_LOG2);
4527
4591
  const ageMin = (Date.now() - st.mtimeMs) / 6e4;
4528
4592
  checks.push({ name: "local logs", ok: true, detail: `on \xB7 ${(st.size / 1024).toFixed(0)}KB \xB7 last write ${ageMin < 1 ? "just now" : Math.round(ageMin) + "m ago"}` });
4529
4593
  } else {
@@ -4688,7 +4752,7 @@ async function fetchLatest() {
4688
4752
  }
4689
4753
  }
4690
4754
  function runGlobalInstall(version) {
4691
- return new Promise((resolve3) => {
4755
+ return new Promise((resolve4) => {
4692
4756
  try {
4693
4757
  mkdirSync6(join9(homedir9(), ".solongate"), { recursive: true });
4694
4758
  execFile(
@@ -4704,11 +4768,11 @@ ${output}
4704
4768
  `, { flag: "a" });
4705
4769
  } catch {
4706
4770
  }
4707
- resolve3({ ok: !err2, needsAdmin: !!err2 && NEEDS_ADMIN_RE.test(output) });
4771
+ resolve4({ ok: !err2, needsAdmin: !!err2 && NEEDS_ADMIN_RE.test(output) });
4708
4772
  }
4709
4773
  );
4710
4774
  } catch {
4711
- resolve3({ ok: false, needsAdmin: false });
4775
+ resolve4({ ok: false, needsAdmin: false });
4712
4776
  }
4713
4777
  });
4714
4778
  }
@@ -4727,13 +4791,13 @@ function ownNodeModules() {
4727
4791
  }
4728
4792
  var prefixCheck = null;
4729
4793
  function globalInstallCheck() {
4730
- prefixCheck ??= new Promise((resolve3) => {
4794
+ prefixCheck ??= new Promise((resolve4) => {
4731
4795
  try {
4732
4796
  const dir = ownNodeModules();
4733
- if (!dir) return resolve3({ writable: null, dir: null });
4734
- access(dir, FS.W_OK, (e) => resolve3({ writable: !e, dir }));
4797
+ if (!dir) return resolve4({ writable: null, dir: null });
4798
+ access(dir, FS.W_OK, (e) => resolve4({ writable: !e, dir }));
4735
4799
  } catch {
4736
- resolve3({ writable: null, dir: null });
4800
+ resolve4({ writable: null, dir: null });
4737
4801
  }
4738
4802
  });
4739
4803
  return prefixCheck;
package/hooks/audit.mjs CHANGED
@@ -8,10 +8,39 @@ import { readFileSync, existsSync, writeFileSync, mkdirSync, appendFileSync } fr
8
8
  import { resolve, join, isAbsolute } from 'node:path';
9
9
  import { homedir } from 'node:os';
10
10
 
11
+ // ── Per-project scratch, kept OUT of the project ─────────────────────────────
12
+ // The guard and the audit hook pass a few small flags to each other (the eval
13
+ // ring, the last-eval measurement, the deny flag, the tool-call marker). These
14
+ // used to be written to `./.solongate/` — relative to whatever directory the
15
+ // agent happened to run in — which scattered a dot-folder through every folder
16
+ // an agent ever touched, config directories included, and put it inside repos
17
+ // that had no ignore rule for it. It also read as "local logging is on" to
18
+ // anyone who found one, which is a different setting entirely.
19
+ //
20
+ // They now live under the user's own ~/.solongate, in a directory named by a
21
+ // hash of the project path, so the per-project separation the dataroom relies
22
+ // on survives while nothing is written where the user works. The hash is
23
+ // duplicated in guard.mjs, audit.mjs and the dataroom: all three must agree for
24
+ // a call's flags to be found, so keep them identical if any one is touched.
25
+ function projectKey(dir) {
26
+ let h = 0x811c9dc5;
27
+ const s = String(dir || '');
28
+ for (let i = 0; i < s.length; i++) {
29
+ h ^= s.charCodeAt(i);
30
+ h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
31
+ }
32
+ return h.toString(16);
33
+ }
34
+
35
+ function projectFlagDir() {
36
+ return join(resolve(homedir(), '.solongate'), 'projects', projectKey(resolve(process.cwd())));
37
+ }
38
+
39
+
11
40
  // Bump on every audit hook change. The cloud serves the newest version; the guard
12
41
  // hook installs it on its next run (no re-login needed). See guard.mjs
13
42
  // fetchAndInstallHook / maybeSelfUpdate.
14
- const HOOK_VERSION = 26;
43
+ const HOOK_VERSION = 27;
15
44
 
16
45
  function loadEnvKey(dir) {
17
46
  try {
@@ -52,7 +81,7 @@ function readLastEvalMs(toolName, sessionId) {
52
81
  // .last-eval flag would be overwritten / half-read under that concurrency). A
53
82
  // lone sequential call has only itself in the window → it reports its own time.
54
83
  try {
55
- const ring = resolve('.solongate', '.eval-ring.jsonl');
84
+ const ring = join(projectFlagDir(), '.eval-ring.jsonl');
56
85
  if (existsSync(ring)) {
57
86
  const now = Date.now();
58
87
  const recent = [];
@@ -69,7 +98,7 @@ function readLastEvalMs(toolName, sessionId) {
69
98
  } catch {}
70
99
  // Fallback: the single-value flag (original behavior).
71
100
  try {
72
- const p = resolve('.solongate', '.last-eval');
101
+ const p = join(projectFlagDir(), '.last-eval');
73
102
  if (!existsSync(p)) return null;
74
103
  const c = JSON.parse(readFileSync(p, 'utf-8'));
75
104
  if (!c || typeof c.ms !== 'number' || typeof c.ts !== 'number') return null;
@@ -587,7 +616,7 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
587
616
  // for a flag written by an older guard (no id, no fp).
588
617
  let guardDenied = false;
589
618
  try {
590
- const denyFlagPath = resolve('.solongate', '.last-deny');
619
+ const denyFlagPath = join(projectFlagDir(), '.last-deny');
591
620
  if (existsSync(denyFlagPath)) {
592
621
  const flag = JSON.parse(readFileSync(denyFlagPath, 'utf-8'));
593
622
  const fresh = flag.ts && Date.now() - flag.ts < 10000 && flag.tool === toolName;
@@ -697,7 +726,7 @@ try { input += readFileSync(0, 'utf-8'); } catch {}
697
726
 
698
727
  // Write flag so stop.mjs knows tool calls happened (skip text-only ALLOW)
699
728
  try {
700
- const flagDir = resolve('.solongate');
729
+ const flagDir = projectFlagDir();
701
730
  mkdirSync(flagDir, { recursive: true });
702
731
  writeFileSync(join(flagDir, '.last-tool-call'), Date.now().toString());
703
732
  } catch {}
@@ -6530,13 +6530,55 @@ var init_src = __esm({
6530
6530
  });
6531
6531
 
6532
6532
  // hooks/guard.mjs
6533
- import { readFileSync, existsSync, statSync, readdirSync, writeFileSync, mkdirSync, chmodSync, renameSync, appendFileSync, rmSync, openSync, readSync, closeSync } from "node:fs";
6533
+ import { readFileSync, existsSync, statSync, readdirSync, writeFileSync, mkdirSync, chmodSync, renameSync, appendFileSync, rmSync, rmdirSync, openSync, readSync, closeSync } from "node:fs";
6534
6534
  import { spawn } from "node:child_process";
6535
6535
  import { resolve, join, dirname, isAbsolute } from "node:path";
6536
6536
  import { homedir } from "node:os";
6537
6537
  import { gunzipSync } from "node:zlib";
6538
6538
  import { createHash } from "node:crypto";
6539
- var HOOK_VERSION = 77;
6539
+ function projectKey(dir) {
6540
+ let h = 2166136261;
6541
+ const s = String(dir || "");
6542
+ for (let i = 0; i < s.length; i++) {
6543
+ h ^= s.charCodeAt(i);
6544
+ h = h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24)) >>> 0;
6545
+ }
6546
+ return h.toString(16);
6547
+ }
6548
+ function projectFlagDir() {
6549
+ return join(resolve(homedir(), ".solongate"), "projects", projectKey(resolve(process.cwd())));
6550
+ }
6551
+ var _legacySwept = false;
6552
+ function sweepLegacyFlagDir() {
6553
+ if (_legacySwept)
6554
+ return;
6555
+ _legacySwept = true;
6556
+ try {
6557
+ const dir = resolve(process.cwd(), ".solongate");
6558
+ if (!existsSync(dir))
6559
+ return;
6560
+ const ours = /* @__PURE__ */ new Set([".eval-ring.jsonl", ".last-eval", ".last-deny", ".last-tool-call", ".debug-guard-log"]);
6561
+ const left = [];
6562
+ for (const f of readdirSync(dir)) {
6563
+ if (ours.has(f)) {
6564
+ try {
6565
+ rmSync(join(dir, f), { force: true });
6566
+ } catch {
6567
+ left.push(f);
6568
+ }
6569
+ } else
6570
+ left.push(f);
6571
+ }
6572
+ if (left.length === 0) {
6573
+ try {
6574
+ rmdirSync(dir);
6575
+ } catch {
6576
+ }
6577
+ }
6578
+ } catch {
6579
+ }
6580
+ }
6581
+ var HOOK_VERSION = 78;
6540
6582
  function localLogsOnly(security) {
6541
6583
  if (security !== void 0) {
6542
6584
  const l = security && security.localLogs;
@@ -7052,7 +7094,7 @@ function callFingerprint(s) {
7052
7094
  }
7053
7095
  function writeDenyFlag(toolName) {
7054
7096
  try {
7055
- const flagDir = resolve(".solongate");
7097
+ const flagDir = projectFlagDir();
7056
7098
  mkdirSync(flagDir, { recursive: true });
7057
7099
  writeFileSync(join(flagDir, ".last-tool-call"), Date.now().toString());
7058
7100
  writeFileSync(join(flagDir, ".last-deny"), JSON.stringify({
@@ -8653,8 +8695,9 @@ if (!REFRESH_MODE) {
8653
8695
  process.stderr.write(`[SolonGate ROUTE] ${opaRoute.toUpperCase()} (${reason ? "block" : "allow"})
8654
8696
  `);
8655
8697
  try {
8656
- const _fd = resolve(".solongate");
8698
+ const _fd = projectFlagDir();
8657
8699
  mkdirSync(_fd, { recursive: true });
8700
+ sweepLegacyFlagDir();
8658
8701
  const _rec = { ms: Date.now() - _evalStart, ts: Date.now(), tool: toolName, session: call.sessionId };
8659
8702
  writeFileSync(join(_fd, ".last-eval"), JSON.stringify(_rec));
8660
8703
  try {
package/hooks/guard.mjs CHANGED
@@ -21,18 +21,67 @@
21
21
  * Logs DENY decisions to SolonGate Cloud. ALLOWs are logged by audit.mjs.
22
22
  * Auto-installed by: npx @solongate/proxy init --global
23
23
  */
24
- import { readFileSync, existsSync, statSync, readdirSync, writeFileSync, mkdirSync, chmodSync, renameSync, appendFileSync, rmSync, openSync, readSync, closeSync } from 'node:fs';
24
+ import { readFileSync, existsSync, statSync, readdirSync, writeFileSync, mkdirSync, chmodSync, renameSync, appendFileSync, rmSync, rmdirSync, openSync, readSync, closeSync } from 'node:fs';
25
25
  import { spawn } from 'node:child_process';
26
26
  import { resolve, join, dirname, isAbsolute } from 'node:path';
27
27
  import { homedir } from 'node:os';
28
28
  import { gunzipSync } from 'node:zlib';
29
+
30
+ // ── Per-project scratch, kept OUT of the project ─────────────────────────────
31
+ // The guard and the audit hook pass a few small flags to each other (the eval
32
+ // ring, the last-eval measurement, the deny flag, the tool-call marker). These
33
+ // used to be written to `./.solongate/` — relative to whatever directory the
34
+ // agent happened to run in — which scattered a dot-folder through every folder
35
+ // an agent ever touched, config directories included, and put it inside repos
36
+ // that had no ignore rule for it. It also read as "local logging is on" to
37
+ // anyone who found one, which is a different setting entirely.
38
+ //
39
+ // They now live under the user's own ~/.solongate, in a directory named by a
40
+ // hash of the project path, so the per-project separation the dataroom relies
41
+ // on survives while nothing is written where the user works. The hash is
42
+ // duplicated in guard.mjs, audit.mjs and the dataroom: all three must agree for
43
+ // a call's flags to be found, so keep them identical if any one is touched.
44
+ function projectKey(dir) {
45
+ let h = 0x811c9dc5;
46
+ const s = String(dir || '');
47
+ for (let i = 0; i < s.length; i++) {
48
+ h ^= s.charCodeAt(i);
49
+ h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
50
+ }
51
+ return h.toString(16);
52
+ }
53
+
54
+ function projectFlagDir() {
55
+ return join(resolve(homedir(), '.solongate'), 'projects', projectKey(resolve(process.cwd())));
56
+ }
57
+
58
+ // Remove the dot-folder older versions left in this working directory. Only our
59
+ // own files go, and the directory only if that empties it — a folder someone
60
+ // else put there, or one with anything unexpected in it, is left alone.
61
+ let _legacySwept = false;
62
+ function sweepLegacyFlagDir() {
63
+ if (_legacySwept) return;
64
+ _legacySwept = true;
65
+ try {
66
+ const dir = resolve(process.cwd(), '.solongate');
67
+ if (!existsSync(dir)) return;
68
+ const ours = new Set(['.eval-ring.jsonl', '.last-eval', '.last-deny', '.last-tool-call', '.debug-guard-log']);
69
+ const left = [];
70
+ for (const f of readdirSync(dir)) {
71
+ if (ours.has(f)) { try { rmSync(join(dir, f), { force: true }); } catch { left.push(f); } }
72
+ else left.push(f);
73
+ }
74
+ if (left.length === 0) { try { rmdirSync(dir); } catch { /* not empty after all */ } }
75
+ } catch { /* never let cleanup disturb a tool call */ }
76
+ }
77
+
29
78
  import { createHash } from 'node:crypto';
30
79
 
31
80
  // Bump on every guard.mjs change. The cloud serves the newest bundle + version;
32
81
  // the installed hook self-updates when the cloud version is higher (see
33
82
  // maybeSelfUpdate). This is what makes guard fixes propagate without a manual
34
83
  // reinstall — the same trust model as the OPA WASM this hook already runs.
35
- const HOOK_VERSION = 77;
84
+ const HOOK_VERSION = 78;
36
85
 
37
86
  // True when local log storage is ON. In that mode logs are kept LOCAL ONLY and
38
87
  // nothing is sent to the cloud audit log — local and cloud are one or the
@@ -780,7 +829,7 @@ function callFingerprint(s) {
780
829
  // Write flag file so stop.mjs knows a tool call (DENY) happened and doesn't log extra ALLOW
781
830
  function writeDenyFlag(toolName) {
782
831
  try {
783
- const flagDir = resolve('.solongate');
832
+ const flagDir = projectFlagDir();
784
833
  mkdirSync(flagDir, { recursive: true });
785
834
  writeFileSync(join(flagDir, '.last-tool-call'), Date.now().toString());
786
835
  // Deny-specific flag: audit.mjs (PostToolUse) reads it to avoid logging a
@@ -2737,7 +2786,8 @@ if (!REFRESH_MODE) { try { input += readFileSync(0, 'utf-8'); } catch {} }
2737
2786
  // Keyed by tool + session so the audit hook can match THIS invocation even
2738
2787
  // when the tool itself runs for minutes (a bare timestamp TTL lost those).
2739
2788
  try {
2740
- const _fd = resolve('.solongate'); mkdirSync(_fd, { recursive: true });
2789
+ const _fd = projectFlagDir(); mkdirSync(_fd, { recursive: true });
2790
+ sweepLegacyFlagDir(); // every call, not just denials
2741
2791
  const _rec = { ms: Date.now() - _evalStart, ts: Date.now(), tool: toolName, session: call.sessionId };
2742
2792
  writeFileSync(join(_fd, '.last-eval'), JSON.stringify(_rec));
2743
2793
  // Also append to a short ring. When many tool calls run in PARALLEL they all
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.83.14",
3
+ "version": "0.83.16",
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": {