@solongate/proxy 0.82.41 → 0.82.43

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.
package/dist/index.js CHANGED
@@ -6187,907 +6187,1406 @@ var init_cli_utils = __esm({
6187
6187
  }
6188
6188
  });
6189
6189
 
6190
- // src/self-update.ts
6191
- var self_update_exports = {};
6192
- __export(self_update_exports, {
6193
- currentVersion: () => currentVersion,
6194
- latestVersion: () => latestVersion,
6195
- maybeSelfUpdate: () => maybeSelfUpdate,
6196
- newerThan: () => newerThan,
6197
- tuiUpdateFlow: () => tuiUpdateFlow
6190
+ // src/global-install.ts
6191
+ var global_install_exports = {};
6192
+ __export(global_install_exports, {
6193
+ clearGuardUpdateCheck: () => clearGuardUpdateCheck,
6194
+ globalPaths: () => globalPaths,
6195
+ guardHookOutdated: () => guardHookOutdated,
6196
+ installClaudeShim: () => installClaudeShim,
6197
+ installGlobalQuiet: () => installGlobalQuiet,
6198
+ installGlobalWithKey: () => installGlobalWithKey,
6199
+ installedGuardVersion: () => installedGuardVersion,
6200
+ isGuardInstalled: () => isGuardInstalled,
6201
+ lockProtected: () => lockProtected,
6202
+ removeClaudeShim: () => removeClaudeShim,
6203
+ runGlobalInstall: () => runGlobalInstall,
6204
+ runGlobalRestore: () => runGlobalRestore,
6205
+ uninstallGlobalQuiet: () => uninstallGlobalQuiet,
6206
+ unlockProtected: () => unlockProtected
6198
6207
  });
6199
- import { execFile, spawn } from "child_process";
6200
- import { mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
6208
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync3, rmSync as rmSync2 } from "fs";
6209
+ import { resolve as resolve3, join as join4, dirname } from "path";
6201
6210
  import { homedir as homedir2 } from "os";
6202
- import { dirname, join as join4 } from "path";
6203
6211
  import { fileURLToPath } from "url";
6204
- function readState() {
6212
+ import { createInterface } from "readline";
6213
+ import { execFileSync as execFileSync2 } from "child_process";
6214
+ function lockFile(file) {
6215
+ if (!existsSync3(file)) return;
6205
6216
  try {
6206
- const s = JSON.parse(readFileSync4(STATE_FILE, "utf-8"));
6207
- return s && typeof s === "object" ? s : {};
6217
+ if (process.platform === "win32") {
6218
+ try {
6219
+ execFileSync2("icacls", [file, "/deny", "*S-1-1-0:(WD,AD,DC,DE)"], { stdio: "ignore" });
6220
+ } catch {
6221
+ }
6222
+ try {
6223
+ execFileSync2("attrib", ["+R", file], { stdio: "ignore" });
6224
+ } catch {
6225
+ }
6226
+ } else if (process.platform === "darwin") {
6227
+ try {
6228
+ execFileSync2("chflags", ["uchg", file], { stdio: "ignore" });
6229
+ } catch {
6230
+ }
6231
+ } else {
6232
+ try {
6233
+ execFileSync2("chattr", ["+i", file], { stdio: "ignore" });
6234
+ } catch {
6235
+ }
6236
+ }
6208
6237
  } catch {
6209
- return {};
6210
6238
  }
6211
6239
  }
6212
- function writeState(s) {
6240
+ function unlockFile(file) {
6241
+ if (!existsSync3(file)) return;
6213
6242
  try {
6214
- mkdirSync3(join4(homedir2(), ".solongate"), { recursive: true });
6215
- writeFileSync3(STATE_FILE, JSON.stringify(s));
6243
+ if (process.platform === "win32") {
6244
+ try {
6245
+ execFileSync2("icacls", [file, "/remove:d", "*S-1-1-0"], { stdio: "ignore" });
6246
+ } catch {
6247
+ }
6248
+ try {
6249
+ execFileSync2("icacls", [file, "/reset"], { stdio: "ignore" });
6250
+ } catch {
6251
+ }
6252
+ try {
6253
+ execFileSync2("attrib", ["-R", file], { stdio: "ignore" });
6254
+ } catch {
6255
+ }
6256
+ } else if (process.platform === "darwin") {
6257
+ try {
6258
+ execFileSync2("chflags", ["nouchg", file], { stdio: "ignore" });
6259
+ } catch {
6260
+ }
6261
+ } else {
6262
+ try {
6263
+ execFileSync2("chattr", ["-i", file], { stdio: "ignore" });
6264
+ } catch {
6265
+ }
6266
+ }
6216
6267
  } catch {
6217
6268
  }
6218
6269
  }
6219
- function currentVersion() {
6220
- try {
6221
- const pkg = JSON.parse(readFileSync4(join4(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8"));
6222
- return pkg.version ?? "0.0.0";
6223
- } catch {
6224
- return "0.0.0";
6225
- }
6270
+ function protectedTargets() {
6271
+ const p = globalPaths();
6272
+ return [
6273
+ join4(p.hooksDir, "guard.mjs"),
6274
+ join4(p.hooksDir, "audit.mjs"),
6275
+ join4(p.hooksDir, "stop.mjs"),
6276
+ join4(p.hooksDir, "shield.mjs"),
6277
+ p.configPath,
6278
+ p.settingsPath,
6279
+ p.antigravityHooksPath
6280
+ ];
6226
6281
  }
6227
- function newerThan(b, a) {
6228
- const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
6229
- const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
6230
- for (let i = 0; i < 3; i++) {
6231
- if ((pb[i] ?? 0) > (pa[i] ?? 0)) return true;
6232
- if ((pb[i] ?? 0) < (pa[i] ?? 0)) return false;
6233
- }
6234
- return false;
6282
+ function lockProtected() {
6283
+ for (const f of protectedTargets()) lockFile(f);
6235
6284
  }
6236
- async function latestVersion() {
6237
- return fetchLatest();
6285
+ function unlockProtected() {
6286
+ for (const f of protectedTargets()) unlockFile(f);
6238
6287
  }
6239
- async function fetchLatest() {
6240
- try {
6241
- const res = await fetch(`https://registry.npmjs.org/${PKG}`, {
6242
- headers: { accept: "application/vnd.npm.install-v1+json" },
6243
- signal: AbortSignal.timeout(3e3)
6244
- });
6245
- if (!res.ok) return null;
6246
- const j = await res.json();
6247
- const latest = j["dist-tags"]?.latest;
6248
- if (typeof latest !== "string") return null;
6249
- if (!j.versions || !(latest in j.versions)) return null;
6250
- return latest;
6251
- } catch {
6252
- return null;
6253
- }
6288
+ function globalPaths() {
6289
+ const home = homedir2();
6290
+ const sgDir = join4(home, ".solongate");
6291
+ const hooksDir = join4(sgDir, "hooks");
6292
+ const claudeDir = join4(home, ".claude");
6293
+ const antigravityDir = join4(home, ".gemini", "config");
6294
+ return {
6295
+ home,
6296
+ sgDir,
6297
+ hooksDir,
6298
+ claudeDir,
6299
+ antigravityDir,
6300
+ settingsPath: join4(claudeDir, "settings.json"),
6301
+ backupPath: join4(claudeDir, "settings.solongate.bak"),
6302
+ configPath: join4(sgDir, "cloud-guard.json"),
6303
+ // Antigravity reads global hooks from ~/.gemini/config/hooks.json. Only the
6304
+ // guard is registered there (PreToolUse); Antigravity's ALLOW-path audit is
6305
+ // covered by the passive session-log collector, not a hook.
6306
+ antigravityHooksPath: join4(antigravityDir, "hooks.json"),
6307
+ antigravityBackupPath: join4(antigravityDir, "hooks.solongate.bak")
6308
+ };
6254
6309
  }
6255
- function spawnGlobalInstall(version) {
6310
+ function clearGuardUpdateCheck() {
6256
6311
  try {
6257
- mkdirSync3(join4(homedir2(), ".solongate"), { recursive: true });
6258
- const log3 = openSync(LOG_FILE, "a");
6259
- const p = spawn("npm", ["install", "-g", `${PKG}@${version}`], {
6260
- stdio: ["ignore", log3, log3],
6261
- detached: true,
6262
- windowsHide: true,
6263
- shell: process.platform === "win32"
6264
- });
6265
- p.on("error", () => {
6266
- });
6267
- p.unref();
6312
+ rmSync2(join4(globalPaths().sgDir, ".hook-update-check"), { force: true });
6268
6313
  return true;
6269
6314
  } catch {
6270
6315
  return false;
6271
6316
  }
6272
6317
  }
6273
- function runGlobalInstall(version) {
6274
- return new Promise((resolve6) => {
6275
- try {
6276
- mkdirSync3(join4(homedir2(), ".solongate"), { recursive: true });
6277
- execFile(
6278
- "npm",
6279
- ["install", "-g", `${PKG}@${version}`],
6280
- { timeout: 3e5, windowsHide: true, shell: process.platform === "win32" },
6281
- (err2, stdout, stderr) => {
6282
- try {
6283
- writeFileSync3(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} install ${version}: ${err2 ? "FAILED" : "ok"}
6284
- ${stdout}
6285
- ${stderr}
6286
- `, { flag: "a" });
6287
- } catch {
6288
- }
6289
- resolve6(!err2);
6290
- }
6291
- );
6292
- } catch {
6293
- resolve6(false);
6294
- }
6295
- });
6318
+ function readHook(filename) {
6319
+ return readFileSync4(join4(HOOKS_DIR, filename), "utf-8");
6296
6320
  }
6297
- async function tuiUpdateFlow(onStatus) {
6321
+ function firstAccountCredential() {
6298
6322
  try {
6299
- const current = currentVersion();
6300
- const latest = await fetchLatest();
6301
- const state = readState();
6302
- if (latest) writeState({ ...state, lastCheckAt: Date.now(), latestSeen: latest });
6303
- if (!latest || !newerThan(latest, current)) return;
6304
- if (readState().installed === latest) {
6305
- onStatus({ kind: "updated", version: latest });
6306
- return;
6307
- }
6308
- onStatus({ kind: "updating", version: latest });
6309
- if (await runGlobalInstall(latest)) {
6310
- writeState({ ...readState(), installed: latest });
6311
- onStatus({ kind: "updated", version: latest });
6323
+ const raw = JSON.parse(readFileSync4(join4(homedir2(), ".solongate", "accounts.json"), "utf-8"));
6324
+ if (Array.isArray(raw)) {
6325
+ const acc = raw.find((a) => a && typeof a.apiKey === "string" && a.apiKey);
6326
+ if (acc) return { apiKey: acc.apiKey, apiUrl: typeof acc.apiUrl === "string" ? acc.apiUrl : void 0 };
6312
6327
  }
6313
6328
  } catch {
6314
6329
  }
6330
+ return {};
6315
6331
  }
6316
- function maybeSelfUpdate(notify = (l) => process.stderr.write(l + "\n")) {
6317
- void (async () => {
6332
+ function readGuard() {
6333
+ const bundled = join4(HOOKS_DIR, "guard.bundled.mjs");
6334
+ return existsSync3(bundled) ? readFileSync4(bundled, "utf-8") : readHook("guard.mjs");
6335
+ }
6336
+ function antigravityHookCommand(guardAbs) {
6337
+ const nodeBin = process.execPath.replace(/\\/g, "/");
6338
+ const call = process.platform === "win32" ? "& " : "";
6339
+ return `${call}"${nodeBin}" "${guardAbs.replace(/\\/g, "/")}" antigravity "Antigravity"`;
6340
+ }
6341
+ function installAntigravityGuard(p, guardAbs) {
6342
+ mkdirSync3(p.antigravityDir, { recursive: true });
6343
+ let existing = {};
6344
+ if (existsSync3(p.antigravityHooksPath)) {
6345
+ const raw = readFileSync4(p.antigravityHooksPath, "utf-8");
6346
+ if (!existsSync3(p.antigravityBackupPath)) writeFileSync3(p.antigravityBackupPath, raw);
6318
6347
  try {
6319
- const state = readState();
6320
- const now = Date.now();
6321
- const current = currentVersion();
6322
- let latest = state.latestSeen ?? null;
6323
- if (now - (state.lastCheckAt ?? 0) >= CHECK_EVERY_MS) {
6324
- latest = await fetchLatest();
6325
- if (latest) writeState({ ...state, lastCheckAt: now, latestSeen: latest });
6326
- else writeState({ ...state, lastCheckAt: now });
6327
- }
6328
- if (!latest || !newerThan(latest, current)) return;
6329
- const attempts = readState().attempts ?? {};
6330
- if (now - (attempts[latest] ?? 0) >= ATTEMPT_EVERY_MS) {
6331
- writeState({ ...readState(), attempts: { [latest]: now } });
6332
- if (spawnGlobalInstall(latest)) {
6333
- notify(`${c.dim}\u2191 solongate v${latest} available (running v${current}) \u2014 updating in the background, next run uses it${c.reset}`);
6334
- }
6335
- }
6348
+ existing = JSON.parse(raw);
6336
6349
  } catch {
6350
+ existing = {};
6337
6351
  }
6338
- })();
6339
- }
6340
- var PKG, CHECK_EVERY_MS, ATTEMPT_EVERY_MS, STATE_FILE, LOG_FILE;
6341
- var init_self_update = __esm({
6342
- "src/self-update.ts"() {
6343
- "use strict";
6344
- init_cli_utils();
6345
- PKG = "@solongate/proxy";
6346
- CHECK_EVERY_MS = 30 * 60 * 1e3;
6347
- ATTEMPT_EVERY_MS = 6 * 60 * 60 * 1e3;
6348
- STATE_FILE = join4(homedir2(), ".solongate", ".self-update.json");
6349
- LOG_FILE = join4(homedir2(), ".solongate", "self-update.log");
6350
- }
6351
- });
6352
-
6353
- // src/logs-server-daemon.ts
6354
- var logs_server_daemon_exports = {};
6355
- __export(logs_server_daemon_exports, {
6356
- LOGS_SERVER_PORT: () => LOGS_SERVER_PORT,
6357
- ensureLogsServerDaemon: () => ensureLogsServerDaemon,
6358
- logsServerStatus: () => logsServerStatus,
6359
- recordLogsServerStarted: () => recordLogsServerStarted,
6360
- startLogsServerDaemon: () => startLogsServerDaemon,
6361
- stopLogsServerDaemon: () => stopLogsServerDaemon
6362
- });
6363
- import { spawn as spawn2 } from "child_process";
6364
- import { mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
6365
- import { homedir as homedir3 } from "os";
6366
- import { dirname as dirname2, join as join5 } from "path";
6367
- import { fileURLToPath as fileURLToPath2 } from "url";
6368
- function readState2() {
6369
- try {
6370
- const s = JSON.parse(readFileSync5(STATE_FILE2, "utf-8"));
6371
- return s && typeof s === "object" ? s : {};
6372
- } catch {
6373
- return {};
6374
6352
  }
6353
+ const merged = {
6354
+ ...existing,
6355
+ [ANTIGRAVITY_GROUP]: {
6356
+ PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: antigravityHookCommand(guardAbs) }] }]
6357
+ }
6358
+ };
6359
+ writeFileSync3(p.antigravityHooksPath, JSON.stringify(merged, null, 2) + "\n");
6375
6360
  }
6376
- function writeState2(s) {
6361
+ function removeAntigravityGuard(p) {
6362
+ if (!existsSync3(p.antigravityHooksPath)) return;
6377
6363
  try {
6378
- mkdirSync4(DIR, { recursive: true });
6379
- writeFileSync4(STATE_FILE2, JSON.stringify(s));
6364
+ const s = JSON.parse(readFileSync4(p.antigravityHooksPath, "utf-8"));
6365
+ if (!(ANTIGRAVITY_GROUP in s)) return;
6366
+ delete s[ANTIGRAVITY_GROUP];
6367
+ writeFileSync3(p.antigravityHooksPath, JSON.stringify(s, null, 2) + "\n");
6380
6368
  } catch {
6381
6369
  }
6382
6370
  }
6383
- function pidAlive(pid) {
6384
- if (!pid || pid <= 0) return false;
6385
- try {
6386
- process.kill(pid, 0);
6387
- return true;
6388
- } catch (e) {
6389
- return e.code === "EPERM";
6390
- }
6371
+ function ask(question) {
6372
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
6373
+ return new Promise((res) => rl.question(question, (a) => {
6374
+ rl.close();
6375
+ res(a.trim());
6376
+ }));
6391
6377
  }
6392
- function logsServerStatus() {
6393
- const s = readState2();
6394
- const running = pidAlive(s.pid);
6395
- return { desired: s.desired === "on" ? "on" : "off", running, pid: running ? s.pid : void 0, port: s.port || LOGS_SERVER_PORT };
6378
+ function runGlobalRestore() {
6379
+ const p = globalPaths();
6380
+ unlockProtected();
6381
+ removeClaudeShim();
6382
+ removeAntigravityGuard(p);
6383
+ if (existsSync3(p.backupPath)) {
6384
+ writeFileSync3(p.settingsPath, readFileSync4(p.backupPath, "utf-8"));
6385
+ console.log(` Restored ${p.settingsPath} from backup.`);
6386
+ } else if (existsSync3(p.settingsPath)) {
6387
+ try {
6388
+ const s = JSON.parse(readFileSync4(p.settingsPath, "utf-8"));
6389
+ delete s.hooks;
6390
+ writeFileSync3(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
6391
+ console.log(` Removed SolonGate hooks from ${p.settingsPath}.`);
6392
+ } catch {
6393
+ }
6394
+ } else {
6395
+ console.log(" Nothing to restore \u2014 no global Claude Code settings found.");
6396
+ }
6397
+ console.log(" Global SolonGate enforcement uninstalled. Restart Claude Code.");
6396
6398
  }
6397
- function recordLogsServerStarted(port) {
6398
- writeState2({ desired: "on", pid: process.pid, port, startedAt: Date.now() });
6399
+ function installGlobalQuiet() {
6400
+ try {
6401
+ const p = globalPaths();
6402
+ let apiKey = process.env["SOLONGATE_API_KEY"] || "";
6403
+ let apiUrl = process.env["SOLONGATE_API_URL"] || "https://api.solongate.com";
6404
+ try {
6405
+ const cfg = JSON.parse(readFileSync4(p.configPath, "utf-8"));
6406
+ if (cfg && typeof cfg.apiKey === "string") apiKey = apiKey || cfg.apiKey;
6407
+ if (cfg && typeof cfg.apiUrl === "string") apiUrl = cfg.apiUrl;
6408
+ } catch {
6409
+ }
6410
+ if (!apiKey) {
6411
+ const acc = firstAccountCredential();
6412
+ if (acc.apiKey) {
6413
+ apiKey = acc.apiKey;
6414
+ if (acc.apiUrl) apiUrl = acc.apiUrl;
6415
+ }
6416
+ }
6417
+ if (!apiKey) return { ok: false, message: "no login on this device \u2014 add an account first (Accounts \u2192 + add)" };
6418
+ mkdirSync3(p.hooksDir, { recursive: true });
6419
+ mkdirSync3(p.claudeDir, { recursive: true });
6420
+ unlockProtected();
6421
+ writeFileSync3(join4(p.hooksDir, "guard.mjs"), readGuard());
6422
+ writeFileSync3(join4(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
6423
+ writeFileSync3(join4(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
6424
+ writeFileSync3(join4(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
6425
+ writeFileSync3(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
6426
+ let existing = {};
6427
+ if (existsSync3(p.settingsPath)) {
6428
+ const raw = readFileSync4(p.settingsPath, "utf-8");
6429
+ if (!existsSync3(p.backupPath)) writeFileSync3(p.backupPath, raw);
6430
+ try {
6431
+ existing = JSON.parse(raw);
6432
+ } catch {
6433
+ existing = {};
6434
+ }
6435
+ }
6436
+ const nodeBin = process.execPath.replace(/\\/g, "/");
6437
+ const call = process.platform === "win32" ? "& " : "";
6438
+ const hookCmd = (script) => `${call}"${nodeBin}" "${join4(p.hooksDir, script).replace(/\\/g, "/")}" claude-code "Claude Code"`;
6439
+ const merged = {
6440
+ ...existing,
6441
+ hooks: {
6442
+ PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("guard.mjs") }] }],
6443
+ PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("audit.mjs") }] }],
6444
+ Stop: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("stop.mjs") }] }]
6445
+ }
6446
+ };
6447
+ writeFileSync3(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
6448
+ try {
6449
+ installAntigravityGuard(p, join4(p.hooksDir, "guard.mjs").replace(/\\/g, "/"));
6450
+ } catch {
6451
+ }
6452
+ if (process.env["SOLONGATE_OS_LOCK"] === "1") lockProtected();
6453
+ return { ok: true, message: "guard installed (open a new session)" };
6454
+ } catch (e) {
6455
+ return { ok: false, message: e instanceof Error ? e.message : String(e) };
6456
+ }
6399
6457
  }
6400
- function startLogsServerDaemon() {
6401
- const cur = logsServerStatus();
6402
- if (cur.running) {
6403
- writeState2({ ...readState2(), desired: "on" });
6404
- return { ...cur, desired: "on" };
6458
+ function installedGuardVersion() {
6459
+ try {
6460
+ const p = globalPaths();
6461
+ const s = readFileSync4(join4(p.hooksDir, "guard.mjs"), "utf-8");
6462
+ const m = s.match(/HOOK_VERSION\s*=\s*(\d+)/);
6463
+ return m ? parseInt(m[1], 10) : null;
6464
+ } catch {
6465
+ return null;
6405
6466
  }
6467
+ }
6468
+ function guardHookOutdated() {
6406
6469
  try {
6407
- mkdirSync4(DIR, { recursive: true });
6408
- const log3 = openSync2(LOG_FILE2, "a");
6409
- const cli = join5(dirname2(fileURLToPath2(import.meta.url)), "index.js");
6410
- const p = spawn2(process.execPath, [cli, "logs-server"], {
6411
- detached: true,
6412
- stdio: ["ignore", log3, log3],
6413
- windowsHide: true
6414
- });
6415
- p.on("error", () => {
6416
- });
6417
- p.unref();
6418
- writeState2({ desired: "on", pid: p.pid, port: LOGS_SERVER_PORT, startedAt: Date.now() });
6419
- return { desired: "on", running: true, pid: p.pid, port: LOGS_SERVER_PORT };
6470
+ const p = globalPaths();
6471
+ return readFileSync4(join4(p.hooksDir, "guard.mjs"), "utf-8") !== readGuard();
6420
6472
  } catch {
6421
- return { ...cur, desired: "on", running: false };
6473
+ return false;
6422
6474
  }
6423
6475
  }
6424
- function stopLogsServerDaemon() {
6425
- const s = readState2();
6426
- if (pidAlive(s.pid)) {
6476
+ function isGuardInstalled() {
6477
+ try {
6478
+ const p = globalPaths();
6479
+ if (!existsSync3(p.settingsPath)) return false;
6480
+ const s = JSON.parse(readFileSync4(p.settingsPath, "utf-8"));
6481
+ return !!s.hooks && JSON.stringify(s.hooks).includes(".solongate");
6482
+ } catch {
6483
+ return false;
6484
+ }
6485
+ }
6486
+ function uninstallGlobalQuiet() {
6487
+ try {
6488
+ const p = globalPaths();
6489
+ unlockProtected();
6490
+ removeClaudeShim();
6427
6491
  try {
6428
- process.kill(s.pid);
6492
+ removeAntigravityGuard(p);
6429
6493
  } catch {
6430
6494
  }
6495
+ if (!existsSync3(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
6496
+ const s = JSON.parse(readFileSync4(p.settingsPath, "utf-8"));
6497
+ delete s.hooks;
6498
+ writeFileSync3(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
6499
+ return { ok: true, message: "guard removed (open a new session)" };
6500
+ } catch (e) {
6501
+ return { ok: false, message: e instanceof Error ? e.message : String(e) };
6431
6502
  }
6432
- writeState2({ desired: "off", port: s.port });
6433
- return { desired: "off", running: false, port: s.port || LOGS_SERVER_PORT };
6434
6503
  }
6435
- function ensureLogsServerDaemon() {
6504
+ function escapeRe(s) {
6505
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6506
+ }
6507
+ function resolveRealClaude() {
6436
6508
  try {
6437
- const s = logsServerStatus();
6438
- if (s.desired === "on" && !s.running) startLogsServerDaemon();
6509
+ const finder = process.platform === "win32" ? "where" : "which";
6510
+ const out2 = execFileSync2(finder, ["claude"], { encoding: "utf-8" }).split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
6511
+ if (process.platform === "win32") {
6512
+ const low = (s) => s.toLowerCase();
6513
+ return out2.find((l) => low(l).endsWith(".cmd")) || out2.find((l) => low(l).endsWith(".exe")) || out2.find((l) => low(l).endsWith(".bat")) || out2[0] || null;
6514
+ }
6515
+ return out2[0] || null;
6439
6516
  } catch {
6517
+ return null;
6440
6518
  }
6441
6519
  }
6442
- var DIR, STATE_FILE2, LOG_FILE2, LOGS_SERVER_PORT;
6443
- var init_logs_server_daemon = __esm({
6444
- "src/logs-server-daemon.ts"() {
6445
- "use strict";
6446
- DIR = join5(homedir3(), ".solongate");
6447
- STATE_FILE2 = join5(DIR, ".logs-server.json");
6448
- LOG_FILE2 = join5(DIR, "logs-server.log");
6449
- LOGS_SERVER_PORT = 8788;
6520
+ function shimTargets() {
6521
+ if (process.platform === "win32") {
6522
+ try {
6523
+ const prof = execFileSync2("powershell", ["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"], { encoding: "utf-8" }).trim();
6524
+ return prof ? [prof] : [];
6525
+ } catch {
6526
+ return [];
6527
+ }
6450
6528
  }
6451
- });
6452
-
6453
- // src/tui/config.ts
6454
- import { readFileSync as readFileSync6 } from "fs";
6455
- import { homedir as homedir4 } from "os";
6456
- import { join as join6 } from "path";
6457
- function loadConfig() {
6458
- if (cached) return cached;
6459
- const defaults = { notifications: true };
6529
+ return [".bashrc", ".zshrc", ".profile"].map((f) => join4(homedir2(), f)).filter((f) => existsSync3(f));
6530
+ }
6531
+ function writeShimBlock(file, block2) {
6532
+ const re = new RegExp(escapeRe(SHIM_BEGIN) + "[\\s\\S]*?" + escapeRe(SHIM_END) + "\\r?\\n?", "g");
6533
+ let content = existsSync3(file) ? readFileSync4(file, "utf-8") : "";
6534
+ content = content.replace(re, "");
6535
+ if (block2) {
6536
+ if (content.length && !content.endsWith("\n")) content += "\n";
6537
+ content += block2 + "\n";
6538
+ }
6539
+ mkdirSync3(dirname(file), { recursive: true });
6540
+ writeFileSync3(file, content);
6541
+ }
6542
+ function installClaudeShim(shieldPath) {
6543
+ const real = resolveRealClaude();
6544
+ if (!real) {
6545
+ console.log(" (Claude Code not found on PATH \u2014 skipped auto-shield. Install it, then re-open `solongate` \u2192 Settings \u2192 guard.)");
6546
+ return;
6547
+ }
6548
+ const node = process.execPath.replace(/\\/g, "/");
6549
+ const shield = shieldPath.replace(/\\/g, "/");
6550
+ const win = process.platform === "win32";
6551
+ const block2 = win ? `${SHIM_BEGIN}
6552
+ function claude { & "${node}" "${shield}" -- "${real}" @args }
6553
+ ${SHIM_END}` : `${SHIM_BEGIN}
6554
+ claude() { "${node}" "${shield}" -- "${real}" "$@"; }
6555
+ ${SHIM_END}`;
6556
+ const targets = shimTargets();
6557
+ if (targets.length === 0) return;
6558
+ for (const file of targets) {
6559
+ try {
6560
+ writeShimBlock(file, block2);
6561
+ } catch {
6562
+ }
6563
+ }
6564
+ }
6565
+ function removeClaudeShim() {
6566
+ for (const file of shimTargets()) {
6567
+ try {
6568
+ writeShimBlock(file, null);
6569
+ } catch {
6570
+ }
6571
+ }
6572
+ }
6573
+ async function runGlobalInstall(opts = {}) {
6574
+ const p = globalPaths();
6575
+ let apiKey = opts.apiKey || process.env["SOLONGATE_API_KEY"] || "";
6576
+ if (!apiKey || apiKey === "sg_live_your_key_here") {
6577
+ try {
6578
+ const cfg = JSON.parse(readFileSync4(p.configPath, "utf-8"));
6579
+ if (cfg && typeof cfg.apiKey === "string") apiKey = cfg.apiKey;
6580
+ } catch {
6581
+ }
6582
+ }
6583
+ if (!apiKey || apiKey === "sg_live_your_key_here") {
6584
+ const acc = firstAccountCredential();
6585
+ if (acc.apiKey) apiKey = acc.apiKey;
6586
+ }
6587
+ if (!apiKey || apiKey === "sg_live_your_key_here") {
6588
+ apiKey = await ask(" Enter your SolonGate API key (sg_live_\u2026 from https://dashboard.solongate.com): ");
6589
+ }
6590
+ if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
6591
+ console.log(" Invalid API key. Must start with sg_live_ or sg_test_");
6592
+ process.exit(1);
6593
+ }
6594
+ const apiUrl = opts.apiUrl || process.env["SOLONGATE_API_URL"] || "https://api.solongate.com";
6595
+ mkdirSync3(p.hooksDir, { recursive: true });
6596
+ mkdirSync3(p.claudeDir, { recursive: true });
6597
+ unlockProtected();
6598
+ writeFileSync3(join4(p.hooksDir, "guard.mjs"), readGuard());
6599
+ writeFileSync3(join4(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
6600
+ writeFileSync3(join4(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
6601
+ writeFileSync3(join4(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
6602
+ console.log(` Installed hooks \u2192 ${p.hooksDir}`);
6603
+ installClaudeShim(join4(p.hooksDir, "shield.mjs"));
6604
+ writeFileSync3(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
6605
+ console.log(` Wrote ${p.configPath}`);
6606
+ let existing = {};
6607
+ if (existsSync3(p.settingsPath)) {
6608
+ const raw = readFileSync4(p.settingsPath, "utf-8");
6609
+ if (!existsSync3(p.backupPath)) {
6610
+ writeFileSync3(p.backupPath, raw);
6611
+ console.log(` Backed up existing settings \u2192 ${p.backupPath}`);
6612
+ }
6613
+ try {
6614
+ existing = JSON.parse(raw);
6615
+ } catch {
6616
+ existing = {};
6617
+ }
6618
+ }
6619
+ const guardAbs = join4(p.hooksDir, "guard.mjs").replace(/\\/g, "/");
6620
+ const auditAbs = join4(p.hooksDir, "audit.mjs").replace(/\\/g, "/");
6621
+ const stopAbs = join4(p.hooksDir, "stop.mjs").replace(/\\/g, "/");
6622
+ const nodeBin = process.execPath.replace(/\\/g, "/");
6623
+ const call = process.platform === "win32" ? "& " : "";
6624
+ const hookCmd = (script) => `${call}"${nodeBin}" "${script}" claude-code "Claude Code"`;
6625
+ const merged = {
6626
+ ...existing,
6627
+ hooks: {
6628
+ PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(guardAbs) }] }],
6629
+ PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(auditAbs) }] }],
6630
+ Stop: [{ matcher: "", hooks: [{ type: "command", command: hookCmd(stopAbs) }] }]
6631
+ }
6632
+ };
6633
+ writeFileSync3(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
6634
+ console.log(` Registered global hooks \u2192 ${p.settingsPath}`);
6460
6635
  try {
6461
- const raw = readFileSync6(join6(homedir4(), ".solongate", "tui-config.json"), "utf-8");
6462
- const j = JSON.parse(raw);
6463
- cached = {
6464
- notifications: j.notifications !== false,
6465
- accent: typeof j.accent === "string" ? j.accent : void 0,
6466
- pollMs: typeof j.pollMs === "number" ? j.pollMs : void 0
6467
- };
6636
+ installAntigravityGuard(p, guardAbs);
6637
+ console.log(` Registered Antigravity CLI guard \u2192 ${p.antigravityHooksPath}`);
6468
6638
  } catch {
6469
- cached = defaults;
6470
6639
  }
6471
- return cached;
6640
+ if (process.env["SOLONGATE_OS_LOCK"] === "1") {
6641
+ lockProtected();
6642
+ console.log(" Locked protection files (OS-level read-only/immutable).");
6643
+ }
6472
6644
  }
6473
- var cached;
6474
- var init_config = __esm({
6475
- "src/tui/config.ts"() {
6645
+ async function installGlobalWithKey(apiKey, apiUrl) {
6646
+ await runGlobalInstall({ apiKey, apiUrl });
6647
+ }
6648
+ var __dirname, HOOKS_DIR, ANTIGRAVITY_GROUP, SHIM_BEGIN, SHIM_END;
6649
+ var init_global_install = __esm({
6650
+ "src/global-install.ts"() {
6476
6651
  "use strict";
6477
- cached = null;
6652
+ __dirname = dirname(fileURLToPath(import.meta.url));
6653
+ HOOKS_DIR = resolve3(__dirname, "..", "hooks");
6654
+ ANTIGRAVITY_GROUP = "solongate-guard";
6655
+ SHIM_BEGIN = "# >>> SolonGate shield (auto secret redaction) >>>";
6656
+ SHIM_END = "# <<< SolonGate shield <<<";
6478
6657
  }
6479
6658
  });
6480
6659
 
6481
- // src/tui/theme.ts
6482
- function decisionColor(decision) {
6483
- const d = (decision || "").toUpperCase();
6484
- if (d === "ALLOW") return theme.ok;
6485
- if (d === "DENY" || d === "DENIED") return theme.bad;
6486
- return theme.dim;
6660
+ // src/self-update.ts
6661
+ var self_update_exports = {};
6662
+ __export(self_update_exports, {
6663
+ currentVersion: () => currentVersion,
6664
+ latestVersion: () => latestVersion,
6665
+ maybeSelfUpdate: () => maybeSelfUpdate,
6666
+ newerThan: () => newerThan,
6667
+ runUpdateCommand: () => runUpdateCommand,
6668
+ tuiUpdateFlow: () => tuiUpdateFlow
6669
+ });
6670
+ import { execFile, spawn } from "child_process";
6671
+ import { mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
6672
+ import { homedir as homedir3 } from "os";
6673
+ import { dirname as dirname2, join as join5 } from "path";
6674
+ import { fileURLToPath as fileURLToPath2 } from "url";
6675
+ function readState() {
6676
+ try {
6677
+ const s = JSON.parse(readFileSync5(STATE_FILE, "utf-8"));
6678
+ return s && typeof s === "object" ? s : {};
6679
+ } catch {
6680
+ return {};
6681
+ }
6487
6682
  }
6488
- function modeColor(m) {
6489
- if (m === "block" || m === "active" || m === "on") return theme.ok;
6490
- if (m === "detect" || m === "idle") return theme.warn;
6491
- return theme.dim;
6683
+ function writeState(s) {
6684
+ try {
6685
+ mkdirSync4(join5(homedir3(), ".solongate"), { recursive: true });
6686
+ writeFileSync4(STATE_FILE, JSON.stringify(s));
6687
+ } catch {
6688
+ }
6492
6689
  }
6493
- function prettyJson(s) {
6690
+ function currentVersion() {
6494
6691
  try {
6495
- return JSON.stringify(JSON.parse(s), null, 2);
6692
+ const pkg = JSON.parse(readFileSync5(join5(dirname2(fileURLToPath2(import.meta.url)), "..", "package.json"), "utf-8"));
6693
+ return pkg.version ?? "0.0.0";
6496
6694
  } catch {
6497
- return s;
6695
+ return "0.0.0";
6498
6696
  }
6499
6697
  }
6500
- function wrapLines(s, width2) {
6501
- const w = Math.max(8, width2);
6502
- const out2 = [];
6503
- for (const raw of (s ?? "").split("\n")) {
6504
- if (raw.length <= w) {
6505
- out2.push(raw);
6506
- continue;
6507
- }
6508
- for (let i = 0; i < raw.length; i += w) out2.push(raw.slice(i, i + w));
6698
+ function newerThan(b, a) {
6699
+ const pa = a.split(".").map((n) => parseInt(n, 10) || 0);
6700
+ const pb = b.split(".").map((n) => parseInt(n, 10) || 0);
6701
+ for (let i = 0; i < 3; i++) {
6702
+ if ((pb[i] ?? 0) > (pa[i] ?? 0)) return true;
6703
+ if ((pb[i] ?? 0) < (pa[i] ?? 0)) return false;
6509
6704
  }
6510
- return out2;
6705
+ return false;
6511
6706
  }
6512
- function truncate2(s, n) {
6513
- if (!s) return "";
6514
- return s.length <= n ? s : s.slice(0, Math.max(0, n - 1)) + "\u2026";
6707
+ async function latestVersion() {
6708
+ return fetchLatest();
6515
6709
  }
6516
- function ago(ts) {
6517
- const t = typeof ts === "number" ? ts : Date.parse(ts);
6518
- if (Number.isNaN(t)) return String(ts ?? "");
6519
- const s = Math.max(0, (Date.now() - t) / 1e3);
6520
- if (s < 60) return `${Math.floor(s)}s`;
6521
- if (s < 3600) return `${Math.floor(s / 60)}m`;
6522
- if (s < 86400) return `${Math.floor(s / 3600)}h`;
6523
- return `${Math.floor(s / 86400)}d`;
6524
- }
6525
- var accent, theme;
6526
- var init_theme = __esm({
6527
- "src/tui/theme.ts"() {
6528
- "use strict";
6529
- init_config();
6530
- accent = loadConfig().accent;
6531
- theme = {
6532
- accent: accent || "white",
6533
- accentBright: accent || "white",
6534
- ok: "green",
6535
- warn: "yellow",
6536
- bad: "red",
6537
- dim: "gray"
6538
- };
6539
- }
6540
- });
6541
-
6542
- // src/tui/components.tsx
6543
- import { Box, Text } from "ink";
6544
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6545
- function PaneTitle({ label, extra, width: width2 }) {
6546
- const tail = extra ? ` ${extra} ` : " ";
6547
- const used = 2 + 1 + label.length + tail.length;
6548
- return /* @__PURE__ */ jsxs(Text, { wrap: "truncate", children: [
6549
- /* @__PURE__ */ jsx(Text, { color: theme.dim, children: "\u2500 " }),
6550
- /* @__PURE__ */ jsxs(Text, { color: theme.accentBright, bold: true, children: [
6551
- "\u258E",
6552
- label
6553
- ] }),
6554
- /* @__PURE__ */ jsx(Text, { color: theme.dim, children: tail + "\u2500".repeat(Math.max(0, width2 - used)) })
6555
- ] });
6556
- }
6557
- function StreamLine({ e, loc, selected, date }) {
6558
- return /* @__PURE__ */ jsxs(Text, { wrap: "truncate", backgroundColor: selected ? "#1c2f63" : void 0, children: [
6559
- /* @__PURE__ */ jsx(Text, { color: selected ? theme.accentBright : theme.dim, children: selected ? "\u25B8" : " " }),
6560
- /* @__PURE__ */ jsxs(Text, { color: theme.dim, children: [
6561
- "[",
6562
- date ? ymd(e.at) + " " : "",
6563
- hhmmss(e.at),
6564
- " "
6565
- ] }),
6566
- /* @__PURE__ */ jsx(Text, { color: loc ? theme.ok : "white", children: loc ? "LOC" : "CLD" }),
6567
- /* @__PURE__ */ jsx(Text, { color: theme.dim, children: "] " }),
6568
- /* @__PURE__ */ jsx(Text, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
6569
- /* @__PURE__ */ jsx(Text, { color: theme.accent, children: truncate2(e.tool, 12).padEnd(13) }),
6570
- /* @__PURE__ */ jsx(Text, { color: theme.dim, children: (e.permission ?? "").padEnd(5) }),
6571
- /* @__PURE__ */ jsx(Text, { color: e.evalMs != null && e.evalMs > 500 ? theme.warn : theme.dim, children: (e.evalMs != null ? `${e.evalMs}ms` : "\u2014").padEnd(7) }),
6572
- /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncate2(e.agent ?? "-", 11).padEnd(12) }),
6573
- /* @__PURE__ */ jsx(Text, { color: e.dlp ? theme.bad : theme.dim, bold: e.dlp, children: ("dlp:" + (e.dlp ? "yes" : "no")).padEnd(8) }),
6574
- /* @__PURE__ */ jsx(Text, { color: e.burst ? theme.warn : theme.dim, bold: e.burst, children: ("rl:" + (e.burst ? "yes" : "no")).padEnd(7) }),
6575
- e.rule && e.decision !== "ALLOW" ? /* @__PURE__ */ jsx(Text, { color: theme.bad, children: truncate2(e.rule, 14) + " " }) : null,
6576
- /* @__PURE__ */ jsx(Text, { color: theme.dim, children: e.detail })
6577
- ] });
6578
- }
6579
- function DataView({
6580
- loading,
6581
- error,
6582
- empty,
6583
- emptyText,
6584
- children
6585
- }) {
6586
- if (error) return /* @__PURE__ */ jsxs(Text, { color: theme.bad, children: [
6587
- "\u2717 ",
6588
- error
6589
- ] });
6590
- if (loading) return /* @__PURE__ */ jsx(Text, { color: theme.warn, children: "\u27F3 loading\u2026" });
6591
- if (empty) return /* @__PURE__ */ jsx(Text, { color: theme.dim, children: emptyText ?? "No data." });
6592
- return /* @__PURE__ */ jsx(Fragment, { children });
6593
- }
6594
- function Table({ columns, rows }) {
6595
- const fit = (s, w) => {
6596
- const v = s ?? "";
6597
- if (v.length > w) return v.slice(0, Math.max(0, w - 1)) + "\u2026";
6598
- return v.padEnd(w);
6599
- };
6600
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
6601
- /* @__PURE__ */ jsx(Box, { children: columns.map((c2, i) => /* @__PURE__ */ jsx(Text, { color: theme.dim, children: fit(c2.header, c2.width) + " " }, i)) }),
6602
- rows.map((row, ri) => /* @__PURE__ */ jsx(Box, { children: row.map((cell, ci) => /* @__PURE__ */ jsx(Text, { color: cell.color, dimColor: cell.dim, bold: cell.bold, children: fit(cell.value, columns[ci]?.width ?? 10) + " " }, ci)) }, ri))
6603
- ] });
6604
- }
6605
- function KeyHints({ hints }) {
6606
- return /* @__PURE__ */ jsx(Box, { children: hints.map(([key, label], i) => /* @__PURE__ */ jsxs(Text, { color: theme.dim, children: [
6607
- /* @__PURE__ */ jsx(Text, { color: theme.accent, children: key }),
6608
- " ",
6609
- label,
6610
- i < hints.length - 1 ? " " : ""
6611
- ] }, i)) });
6612
- }
6613
- var hhmmss, ymd;
6614
- var init_components = __esm({
6615
- "src/tui/components.tsx"() {
6616
- "use strict";
6617
- init_theme();
6618
- hhmmss = (ts) => {
6619
- const d = new Date(ts);
6620
- return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
6621
- };
6622
- ymd = (ts) => {
6623
- const d = new Date(ts);
6624
- if (Number.isNaN(d.getTime())) return "----------";
6625
- const p = (n) => String(n).padStart(2, "0");
6626
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
6627
- };
6628
- }
6629
- });
6630
-
6631
- // src/tui/local-log.ts
6632
- import { closeSync, openSync as openSync3, readFileSync as readFileSync7, readSync, statSync, writeFileSync as writeFileSync5 } from "fs";
6633
- import { homedir as homedir5 } from "os";
6634
- import { join as join7 } from "path";
6635
- function tailLines(file, maxBytes = 131072) {
6710
+ async function fetchLatest() {
6636
6711
  try {
6637
- const size = statSync(file).size;
6638
- const start = Math.max(0, size - maxBytes);
6639
- const fd = openSync3(file, "r");
6640
- const buf = Buffer.alloc(size - start);
6641
- readSync(fd, buf, 0, buf.length, start);
6642
- closeSync(fd);
6643
- const lines = buf.toString("utf-8").split("\n").filter(Boolean);
6644
- if (start > 0) lines.shift();
6645
- return lines;
6712
+ const res = await fetch(`https://registry.npmjs.org/${PKG}`, {
6713
+ headers: { accept: "application/vnd.npm.install-v1+json" },
6714
+ signal: AbortSignal.timeout(3e3)
6715
+ });
6716
+ if (!res.ok) return null;
6717
+ const j = await res.json();
6718
+ const latest = j["dist-tags"]?.latest;
6719
+ if (typeof latest !== "string") return null;
6720
+ if (!j.versions || !(latest in j.versions)) return null;
6721
+ return latest;
6646
6722
  } catch {
6647
- return [];
6723
+ return null;
6648
6724
  }
6649
6725
  }
6650
- function deleteLocalEntry(at, tool, session) {
6726
+ function spawnGlobalInstall(version) {
6651
6727
  try {
6652
- const lines = readFileSync7(LOCAL_LOG, "utf-8").split("\n");
6653
- let removed = 0;
6654
- const kept = lines.filter((line) => {
6655
- if (!line.trim()) return false;
6656
- if (removed) return true;
6657
- try {
6658
- const j = JSON.parse(line);
6659
- const hit = Date.parse(j.ts ?? "") === at && (j.tool ?? "?") === tool && (!session || j.session_id === session);
6660
- if (hit) {
6661
- removed++;
6662
- return false;
6663
- }
6664
- } catch {
6665
- }
6666
- return true;
6728
+ mkdirSync4(join5(homedir3(), ".solongate"), { recursive: true });
6729
+ const log3 = openSync(LOG_FILE, "a");
6730
+ const p = spawn("npm", ["install", "-g", `${PKG}@${version}`], {
6731
+ stdio: ["ignore", log3, log3],
6732
+ detached: true,
6733
+ windowsHide: true,
6734
+ shell: process.platform === "win32"
6667
6735
  });
6668
- if (removed) writeFileSync5(LOCAL_LOG, kept.length ? kept.join("\n") + "\n" : "");
6669
- return removed;
6736
+ p.on("error", () => {
6737
+ });
6738
+ p.unref();
6739
+ return true;
6670
6740
  } catch {
6671
- return 0;
6741
+ return false;
6672
6742
  }
6673
6743
  }
6674
- function clearLocalLog() {
6744
+ async function runUpdateCommand() {
6745
+ const out2 = (s) => void process.stderr.write(s + "\n");
6746
+ const cur = currentVersion();
6747
+ out2(` current ${cur}`);
6748
+ const latest = await latestVersion();
6749
+ if (!latest) {
6750
+ out2(" could not reach the npm registry. Try again, or: npm i -g @solongate/proxy@latest");
6751
+ return 1;
6752
+ }
6753
+ if (newerThan(latest, cur)) {
6754
+ out2(` updating ${cur} -> ${latest} ...`);
6755
+ const ok = await runGlobalInstall2(latest);
6756
+ if (!ok) {
6757
+ out2(" update failed. Try: npm i -g @solongate/proxy@latest");
6758
+ return 1;
6759
+ }
6760
+ out2(` \u2713 updated to ${latest} (open a new session to use it)`);
6761
+ } else {
6762
+ out2(` \u2713 already up to date`);
6763
+ }
6675
6764
  try {
6676
- const n = readFileSync7(LOCAL_LOG, "utf-8").split("\n").filter(Boolean).length;
6677
- writeFileSync5(LOCAL_LOG, "");
6678
- return n;
6765
+ const { installGlobalQuiet: installGlobalQuiet2, installedGuardVersion: installedGuardVersion2 } = await Promise.resolve().then(() => (init_global_install(), global_install_exports));
6766
+ const r = installGlobalQuiet2();
6767
+ out2(r.ok ? ` \u2713 guard hooks refreshed (v${installedGuardVersion2() ?? "?"})` : ` guard: ${r.message}`);
6679
6768
  } catch {
6680
- return 0;
6681
- }
6682
- }
6683
- function reasonSignals(reason) {
6684
- const r = reason || "";
6685
- const dlp = [];
6686
- if (DLP_REASON.test(r)) {
6687
- const m = r.match(/contain(?:s)? (?:a |an )?(.+?)(?:\.|$)/i);
6688
- dlp.push(m ? m[1].trim() : "DLP");
6689
6769
  }
6690
- return { dlp, burst: RL_REASON.test(r) };
6770
+ return 0;
6691
6771
  }
6692
- function parseLocalLines(lines) {
6693
- const out2 = [];
6694
- for (const line of lines) {
6772
+ function runGlobalInstall2(version) {
6773
+ return new Promise((resolve6) => {
6695
6774
  try {
6696
- const j = JSON.parse(line);
6697
- const at = Date.parse(j.ts ?? "");
6698
- if (Number.isFinite(at)) out2.push({ ...j, at });
6775
+ mkdirSync4(join5(homedir3(), ".solongate"), { recursive: true });
6776
+ execFile(
6777
+ "npm",
6778
+ ["install", "-g", `${PKG}@${version}`],
6779
+ { timeout: 3e5, windowsHide: true, shell: process.platform === "win32" },
6780
+ (err2, stdout, stderr) => {
6781
+ try {
6782
+ writeFileSync4(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} install ${version}: ${err2 ? "FAILED" : "ok"}
6783
+ ${stdout}
6784
+ ${stderr}
6785
+ `, { flag: "a" });
6786
+ } catch {
6787
+ }
6788
+ resolve6(!err2);
6789
+ }
6790
+ );
6699
6791
  } catch {
6792
+ resolve6(false);
6700
6793
  }
6701
- }
6702
- return out2;
6794
+ });
6703
6795
  }
6704
- var LOCAL_LOG, DLP_REASON, RL_REASON;
6705
- var init_local_log = __esm({
6706
- "src/tui/local-log.ts"() {
6707
- "use strict";
6708
- LOCAL_LOG = join7(homedir5(), ".solongate", "local-logs", "solongate-audit.jsonl");
6709
- DLP_REASON = /security layer \(dlp\)/i;
6710
- RL_REASON = /security layer \(rate limit\)|rate[- ]?limit(?:ed)?\b.*exceed|exceeded \d+ calls/i;
6711
- }
6712
- });
6713
-
6714
- // src/api-client/client.ts
6715
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync3 } from "fs";
6716
- import { resolve as resolve3, join as join8 } from "path";
6717
- import { homedir as homedir6 } from "os";
6718
- function listAccounts() {
6719
- let list5 = [];
6796
+ async function tuiUpdateFlow(onStatus) {
6720
6797
  try {
6721
- const raw = JSON.parse(readFileSync8(accountsFile(), "utf-8"));
6722
- if (Array.isArray(raw)) list5 = raw.filter((a) => a && typeof a.apiKey === "string");
6798
+ const current = currentVersion();
6799
+ const latest = await fetchLatest();
6800
+ const state = readState();
6801
+ if (latest) writeState({ ...state, lastCheckAt: Date.now(), latestSeen: latest });
6802
+ if (!latest || !newerThan(latest, current)) return;
6803
+ if (readState().installed === latest) {
6804
+ onStatus({ kind: "updated", version: latest });
6805
+ return;
6806
+ }
6807
+ onStatus({ kind: "updating", version: latest });
6808
+ if (await runGlobalInstall2(latest)) {
6809
+ writeState({ ...readState(), installed: latest });
6810
+ onStatus({ kind: "updated", version: latest });
6811
+ }
6723
6812
  } catch {
6724
6813
  }
6725
- const active2 = loginCredentialFile();
6726
- if (active2.apiKey && !list5.some((a) => a.apiKey === active2.apiKey)) {
6727
- list5.unshift({ apiKey: active2.apiKey, apiUrl: active2.apiUrl || DEFAULT_API_URL2 });
6728
- }
6729
- return list5;
6730
6814
  }
6731
- function saveAccount(acc) {
6732
- try {
6733
- const list5 = (() => {
6734
- try {
6735
- const raw = JSON.parse(readFileSync8(accountsFile(), "utf-8"));
6736
- return Array.isArray(raw) ? raw.filter((a) => a && a.apiKey) : [];
6737
- } catch {
6738
- return [];
6739
- }
6740
- })();
6741
- const next = list5.filter((a) => a.apiKey !== acc.apiKey);
6742
- next.unshift({ ...acc, addedAt: acc.addedAt ?? Date.now() });
6743
- mkdirSync5(join8(homedir6(), ".solongate"), { recursive: true });
6744
- writeFileSync6(accountsFile(), JSON.stringify(next, null, 2));
6815
+ function maybeSelfUpdate(notify = (l) => process.stderr.write(l + "\n")) {
6816
+ void (async () => {
6817
+ try {
6818
+ const state = readState();
6819
+ const now = Date.now();
6820
+ const current = currentVersion();
6821
+ let latest = state.latestSeen ?? null;
6822
+ if (now - (state.lastCheckAt ?? 0) >= CHECK_EVERY_MS) {
6823
+ latest = await fetchLatest();
6824
+ if (latest) writeState({ ...state, lastCheckAt: now, latestSeen: latest });
6825
+ else writeState({ ...state, lastCheckAt: now });
6826
+ }
6827
+ if (!latest || !newerThan(latest, current)) return;
6828
+ const attempts = readState().attempts ?? {};
6829
+ if (now - (attempts[latest] ?? 0) >= ATTEMPT_EVERY_MS) {
6830
+ writeState({ ...readState(), attempts: { [latest]: now } });
6831
+ if (spawnGlobalInstall(latest)) {
6832
+ notify(`${c.dim}\u2191 solongate v${latest} available (running v${current}) \u2014 updating in the background, next run uses it${c.reset}`);
6833
+ }
6834
+ }
6835
+ } catch {
6836
+ }
6837
+ })();
6838
+ }
6839
+ var PKG, CHECK_EVERY_MS, ATTEMPT_EVERY_MS, STATE_FILE, LOG_FILE;
6840
+ var init_self_update = __esm({
6841
+ "src/self-update.ts"() {
6842
+ "use strict";
6843
+ init_cli_utils();
6844
+ PKG = "@solongate/proxy";
6845
+ CHECK_EVERY_MS = 30 * 60 * 1e3;
6846
+ ATTEMPT_EVERY_MS = 6 * 60 * 60 * 1e3;
6847
+ STATE_FILE = join5(homedir3(), ".solongate", ".self-update.json");
6848
+ LOG_FILE = join5(homedir3(), ".solongate", "self-update.log");
6849
+ }
6850
+ });
6851
+
6852
+ // src/logs-server-daemon.ts
6853
+ var logs_server_daemon_exports = {};
6854
+ __export(logs_server_daemon_exports, {
6855
+ LOGS_SERVER_PORT: () => LOGS_SERVER_PORT,
6856
+ ensureLogsServerDaemon: () => ensureLogsServerDaemon,
6857
+ logsServerStatus: () => logsServerStatus,
6858
+ recordLogsServerStarted: () => recordLogsServerStarted,
6859
+ startLogsServerDaemon: () => startLogsServerDaemon,
6860
+ stopLogsServerDaemon: () => stopLogsServerDaemon
6861
+ });
6862
+ import { spawn as spawn2 } from "child_process";
6863
+ import { mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
6864
+ import { homedir as homedir4 } from "os";
6865
+ import { dirname as dirname3, join as join6 } from "path";
6866
+ import { fileURLToPath as fileURLToPath3 } from "url";
6867
+ function readState2() {
6868
+ try {
6869
+ const s = JSON.parse(readFileSync6(STATE_FILE2, "utf-8"));
6870
+ return s && typeof s === "object" ? s : {};
6745
6871
  } catch {
6872
+ return {};
6746
6873
  }
6747
6874
  }
6748
- function removeAccount(apiKey) {
6875
+ function writeState2(s) {
6749
6876
  try {
6750
- const list5 = (() => {
6751
- try {
6752
- const raw = JSON.parse(readFileSync8(accountsFile(), "utf-8"));
6753
- return Array.isArray(raw) ? raw.filter((a) => a && a.apiKey) : [];
6754
- } catch {
6755
- return [];
6756
- }
6757
- })();
6758
- writeFileSync6(accountsFile(), JSON.stringify(list5.filter((a) => a.apiKey !== apiKey), null, 2));
6877
+ mkdirSync5(DIR, { recursive: true });
6878
+ writeFileSync5(STATE_FILE2, JSON.stringify(s));
6759
6879
  } catch {
6760
6880
  }
6761
6881
  }
6762
- function setViewCredentials(creds) {
6763
- viewOverride = creds;
6764
- cached2 = null;
6882
+ function pidAlive(pid) {
6883
+ if (!pid || pid <= 0) return false;
6884
+ try {
6885
+ process.kill(pid, 0);
6886
+ return true;
6887
+ } catch (e) {
6888
+ return e.code === "EPERM";
6889
+ }
6765
6890
  }
6766
- function isActiveAccount(apiKey) {
6767
- return loginCredentialFile().apiKey === apiKey;
6891
+ function logsServerStatus() {
6892
+ const s = readState2();
6893
+ const running = pidAlive(s.pid);
6894
+ return { desired: s.desired === "on" ? "on" : "off", running, pid: running ? s.pid : void 0, port: s.port || LOGS_SERVER_PORT };
6768
6895
  }
6769
- function setActiveAccount(creds) {
6896
+ function recordLogsServerStarted(port) {
6897
+ writeState2({ desired: "on", pid: process.pid, port, startedAt: Date.now() });
6898
+ }
6899
+ function startLogsServerDaemon() {
6900
+ const cur = logsServerStatus();
6901
+ if (cur.running) {
6902
+ writeState2({ ...readState2(), desired: "on" });
6903
+ return { ...cur, desired: "on" };
6904
+ }
6770
6905
  try {
6771
- const dir = join8(homedir6(), ".solongate");
6772
- mkdirSync5(dir, { recursive: true });
6773
- const p = join8(dir, ["cloud", "guard.json"].join("-"));
6774
- let existing = {};
6775
- try {
6776
- existing = JSON.parse(readFileSync8(p, "utf-8"));
6777
- } catch {
6778
- }
6779
- writeFileSync6(p, JSON.stringify({ ...existing, apiKey: creds.apiKey, apiUrl: creds.apiUrl }, null, 2));
6780
- cached2 = null;
6781
- return true;
6906
+ mkdirSync5(DIR, { recursive: true });
6907
+ const log3 = openSync2(LOG_FILE2, "a");
6908
+ const cli = join6(dirname3(fileURLToPath3(import.meta.url)), "index.js");
6909
+ const p = spawn2(process.execPath, [cli, "logs-server"], {
6910
+ detached: true,
6911
+ stdio: ["ignore", log3, log3],
6912
+ windowsHide: true
6913
+ });
6914
+ p.on("error", () => {
6915
+ });
6916
+ p.unref();
6917
+ writeState2({ desired: "on", pid: p.pid, port: LOGS_SERVER_PORT, startedAt: Date.now() });
6918
+ return { desired: "on", running: true, pid: p.pid, port: LOGS_SERVER_PORT };
6782
6919
  } catch {
6783
- return false;
6920
+ return { ...cur, desired: "on", running: false };
6784
6921
  }
6785
6922
  }
6786
- function clearActiveCredential() {
6787
- try {
6788
- const p = join8(homedir6(), ".solongate", ["cloud", "guard.json"].join("-"));
6789
- if (!existsSync3(p)) {
6790
- cached2 = null;
6791
- return true;
6792
- }
6793
- let existing = {};
6923
+ function stopLogsServerDaemon() {
6924
+ const s = readState2();
6925
+ if (pidAlive(s.pid)) {
6794
6926
  try {
6795
- existing = JSON.parse(readFileSync8(p, "utf-8"));
6927
+ process.kill(s.pid);
6796
6928
  } catch {
6797
6929
  }
6798
- delete existing.apiKey;
6799
- delete existing.apiUrl;
6800
- writeFileSync6(p, JSON.stringify(existing, null, 2));
6801
- cached2 = null;
6802
- viewOverride = null;
6803
- return true;
6804
- } catch {
6805
- return false;
6806
6930
  }
6931
+ writeState2({ desired: "off", port: s.port });
6932
+ return { desired: "off", running: false, port: s.port || LOGS_SERVER_PORT };
6807
6933
  }
6808
- function loginCredentialFile() {
6934
+ function ensureLogsServerDaemon() {
6809
6935
  try {
6810
- const p = join8(homedir6(), ".solongate", "cloud-guard.json");
6811
- if (!existsSync3(p)) return {};
6812
- const c2 = JSON.parse(readFileSync8(p, "utf-8"));
6813
- return c2 && typeof c2 === "object" ? c2 : {};
6936
+ const s = logsServerStatus();
6937
+ if (s.desired === "on" && !s.running) startLogsServerDaemon();
6814
6938
  } catch {
6815
- return {};
6816
6939
  }
6817
6940
  }
6818
- function dotenvApiKey() {
6941
+ var DIR, STATE_FILE2, LOG_FILE2, LOGS_SERVER_PORT;
6942
+ var init_logs_server_daemon = __esm({
6943
+ "src/logs-server-daemon.ts"() {
6944
+ "use strict";
6945
+ DIR = join6(homedir4(), ".solongate");
6946
+ STATE_FILE2 = join6(DIR, ".logs-server.json");
6947
+ LOG_FILE2 = join6(DIR, "logs-server.log");
6948
+ LOGS_SERVER_PORT = 8788;
6949
+ }
6950
+ });
6951
+
6952
+ // src/tui/config.ts
6953
+ import { readFileSync as readFileSync7 } from "fs";
6954
+ import { homedir as homedir5 } from "os";
6955
+ import { join as join7 } from "path";
6956
+ function loadConfig() {
6957
+ if (cached) return cached;
6958
+ const defaults = { notifications: true };
6819
6959
  try {
6820
- const envPath = resolve3(".env");
6821
- if (!existsSync3(envPath)) return void 0;
6822
- for (const line of readFileSync8(envPath, "utf-8").split("\n")) {
6823
- const trimmed = line.trim();
6824
- if (!trimmed || trimmed.startsWith("#")) continue;
6825
- const eq = trimmed.indexOf("=");
6826
- if (eq === -1) continue;
6827
- const key = trimmed.slice(0, eq).trim();
6828
- if (key !== "SOLONGATE_API_KEY") continue;
6829
- return trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
6830
- }
6960
+ const raw = readFileSync7(join7(homedir5(), ".solongate", "tui-config.json"), "utf-8");
6961
+ const j = JSON.parse(raw);
6962
+ cached = {
6963
+ notifications: j.notifications !== false,
6964
+ accent: typeof j.accent === "string" ? j.accent : void 0,
6965
+ pollMs: typeof j.pollMs === "number" ? j.pollMs : void 0
6966
+ };
6831
6967
  } catch {
6968
+ cached = defaults;
6832
6969
  }
6833
- return void 0;
6970
+ return cached;
6834
6971
  }
6835
- function isAuthenticated() {
6836
- try {
6837
- resolveCredentials();
6838
- return true;
6839
- } catch {
6840
- return false;
6972
+ var cached;
6973
+ var init_config = __esm({
6974
+ "src/tui/config.ts"() {
6975
+ "use strict";
6976
+ cached = null;
6841
6977
  }
6978
+ });
6979
+
6980
+ // src/tui/theme.ts
6981
+ function decisionColor(decision) {
6982
+ const d = (decision || "").toUpperCase();
6983
+ if (d === "ALLOW") return theme.ok;
6984
+ if (d === "DENY" || d === "DENIED") return theme.bad;
6985
+ return theme.dim;
6842
6986
  }
6843
- function resolveCredentials(apiUrlOverride) {
6844
- if (viewOverride && !apiUrlOverride) return viewOverride;
6845
- if (cached2 && !apiUrlOverride) return cached2;
6846
- const file = loginCredentialFile();
6847
- const apiKey = process.env["SOLONGATE_API_KEY"] || file.apiKey || dotenvApiKey();
6848
- if (!apiKey) throw new NotAuthenticatedError();
6849
- const apiUrl = apiUrlOverride || process.env["SOLONGATE_API_URL"] || file.apiUrl || DEFAULT_API_URL2;
6850
- const creds = { apiKey, apiUrl: apiUrl.replace(/\/$/, "") };
6851
- if (!apiUrlOverride) cached2 = creds;
6852
- return creds;
6987
+ function modeColor(m) {
6988
+ if (m === "block" || m === "active" || m === "on") return theme.ok;
6989
+ if (m === "detect" || m === "idle") return theme.warn;
6990
+ return theme.dim;
6853
6991
  }
6854
- function buildUrl(base, path, query) {
6855
- const url = new URL(`${base}/api/v1${path}`);
6856
- if (query) {
6857
- for (const [k, v] of Object.entries(query)) {
6858
- if (v === void 0) continue;
6859
- url.searchParams.set(k, String(v));
6860
- }
6992
+ function prettyJson(s) {
6993
+ try {
6994
+ return JSON.stringify(JSON.parse(s), null, 2);
6995
+ } catch {
6996
+ return s;
6861
6997
  }
6862
- return url.toString();
6863
6998
  }
6864
- async function request(method, path, opts = {}) {
6865
- const creds = resolveCredentials(opts.apiUrl);
6866
- const url = buildUrl(creds.apiUrl, path, opts.query);
6867
- const headers = {
6868
- Authorization: `Bearer ${creds.apiKey}`
6869
- };
6870
- let bodyInit;
6871
- if (opts.body !== void 0) {
6872
- headers["Content-Type"] = "application/json";
6873
- bodyInit = JSON.stringify(opts.body);
6874
- }
6875
- const attempt = () => fetch(url, {
6876
- method,
6877
- headers: { ...headers, Connection: "close" },
6878
- body: bodyInit,
6879
- signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
6880
- });
6881
- const maxTries = method === "GET" ? 3 : 1;
6882
- let res;
6883
- let lastErr;
6884
- for (let i = 0; i < maxTries; i++) {
6885
- try {
6886
- res = await attempt();
6887
- break;
6888
- } catch (err2) {
6889
- lastErr = err2;
6890
- if (i < maxTries - 1) await new Promise((r) => setTimeout(r, 600 * (i + 1)));
6999
+ function wrapLines(s, width2) {
7000
+ const w = Math.max(8, width2);
7001
+ const out2 = [];
7002
+ for (const raw of (s ?? "").split("\n")) {
7003
+ if (raw.length <= w) {
7004
+ out2.push(raw);
7005
+ continue;
6891
7006
  }
7007
+ for (let i = 0; i < raw.length; i += w) out2.push(raw.slice(i, i + w));
6892
7008
  }
6893
- if (!res) {
6894
- const msg = lastErr instanceof Error ? lastErr.message : String(lastErr);
6895
- throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
6896
- }
6897
- const text = await res.text().catch(() => "");
6898
- let json = void 0;
6899
- if (text) {
6900
- try {
6901
- json = JSON.parse(text);
6902
- } catch {
6903
- }
6904
- }
6905
- if (!res.ok) {
6906
- const envelope = json?.error;
6907
- if (envelope && typeof envelope === "object") {
6908
- throw new ApiError(res.status, envelope.code || "ERROR", envelope.message || res.statusText);
6909
- }
6910
- if (typeof envelope === "string") {
6911
- throw new ApiError(res.status, "ERROR", envelope);
6912
- }
6913
- if (res.status === 401) throw new ApiError(401, "AUTHENTICATION_ERROR", "Invalid API key. Run `solongate` and log in from the Accounts panel.");
6914
- if (res.status === 429) throw new ApiError(429, "RATE_LIMITED", "Rate limited by the API. Slow down and retry.");
6915
- if (res.status >= 500) throw new ApiError(res.status, "SERVER_ERROR", text || "SolonGate API had a problem (server error). Please try again in a moment.");
6916
- throw new ApiError(res.status, "ERROR", text || res.statusText || `HTTP ${res.status}`);
6917
- }
6918
- return json;
7009
+ return out2;
6919
7010
  }
6920
- var DEFAULT_API_URL2, accountsFile, viewOverride, ApiError, NotAuthenticatedError, cached2;
6921
- var init_client = __esm({
6922
- "src/api-client/client.ts"() {
7011
+ function truncate2(s, n) {
7012
+ if (!s) return "";
7013
+ return s.length <= n ? s : s.slice(0, Math.max(0, n - 1)) + "\u2026";
7014
+ }
7015
+ function ago(ts) {
7016
+ const t = typeof ts === "number" ? ts : Date.parse(ts);
7017
+ if (Number.isNaN(t)) return String(ts ?? "");
7018
+ const s = Math.max(0, (Date.now() - t) / 1e3);
7019
+ if (s < 60) return `${Math.floor(s)}s`;
7020
+ if (s < 3600) return `${Math.floor(s / 60)}m`;
7021
+ if (s < 86400) return `${Math.floor(s / 3600)}h`;
7022
+ return `${Math.floor(s / 86400)}d`;
7023
+ }
7024
+ var accent, theme;
7025
+ var init_theme = __esm({
7026
+ "src/tui/theme.ts"() {
6923
7027
  "use strict";
6924
- DEFAULT_API_URL2 = "https://api.solongate.com";
6925
- accountsFile = () => join8(homedir6(), ".solongate", "accounts.json");
6926
- viewOverride = null;
6927
- ApiError = class extends Error {
6928
- status;
6929
- code;
6930
- constructor(status, code, message) {
6931
- super(message);
6932
- this.name = "ApiError";
6933
- this.status = status;
6934
- this.code = code;
6935
- }
6936
- };
6937
- NotAuthenticatedError = class extends Error {
6938
- constructor() {
6939
- super("Not logged in. Run `solongate` and log in from the Accounts panel.");
6940
- this.name = "NotAuthenticatedError";
6941
- }
7028
+ init_config();
7029
+ accent = loadConfig().accent;
7030
+ theme = {
7031
+ accent: accent || "white",
7032
+ accentBright: accent || "white",
7033
+ ok: "green",
7034
+ warn: "yellow",
7035
+ bad: "red",
7036
+ dim: "gray"
6942
7037
  };
6943
- cached2 = null;
6944
7038
  }
6945
7039
  });
6946
7040
 
6947
- // src/api-client/types.ts
6948
- var init_types = __esm({
6949
- "src/api-client/types.ts"() {
7041
+ // src/tui/components.tsx
7042
+ import { Box, Text } from "ink";
7043
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7044
+ function PaneTitle({ label, extra, width: width2 }) {
7045
+ const tail = extra ? ` ${extra} ` : " ";
7046
+ const used = 2 + 1 + label.length + tail.length;
7047
+ return /* @__PURE__ */ jsxs(Text, { wrap: "truncate", children: [
7048
+ /* @__PURE__ */ jsx(Text, { color: theme.dim, children: "\u2500 " }),
7049
+ /* @__PURE__ */ jsxs(Text, { color: theme.accentBright, bold: true, children: [
7050
+ "\u258E",
7051
+ label
7052
+ ] }),
7053
+ /* @__PURE__ */ jsx(Text, { color: theme.dim, children: tail + "\u2500".repeat(Math.max(0, width2 - used)) })
7054
+ ] });
7055
+ }
7056
+ function StreamLine({ e, loc, selected, date }) {
7057
+ return /* @__PURE__ */ jsxs(Text, { wrap: "truncate", backgroundColor: selected ? "#1c2f63" : void 0, children: [
7058
+ /* @__PURE__ */ jsx(Text, { color: selected ? theme.accentBright : theme.dim, children: selected ? "\u25B8" : " " }),
7059
+ /* @__PURE__ */ jsxs(Text, { color: theme.dim, children: [
7060
+ "[",
7061
+ date ? ymd(e.at) + " " : "",
7062
+ hhmmss(e.at),
7063
+ " "
7064
+ ] }),
7065
+ /* @__PURE__ */ jsx(Text, { color: loc ? theme.ok : "white", children: loc ? "LOC" : "CLD" }),
7066
+ /* @__PURE__ */ jsx(Text, { color: theme.dim, children: "] " }),
7067
+ /* @__PURE__ */ jsx(Text, { color: decisionColor(e.decision), bold: e.decision !== "ALLOW", children: e.decision.padEnd(6) }),
7068
+ /* @__PURE__ */ jsx(Text, { color: theme.accent, children: truncate2(e.tool, 12).padEnd(13) }),
7069
+ /* @__PURE__ */ jsx(Text, { color: theme.dim, children: (e.permission ?? "").padEnd(5) }),
7070
+ /* @__PURE__ */ jsx(Text, { color: e.evalMs != null && e.evalMs > 500 ? theme.warn : theme.dim, children: (e.evalMs != null ? `${e.evalMs}ms` : "\u2014").padEnd(7) }),
7071
+ /* @__PURE__ */ jsx(Text, { color: theme.dim, children: truncate2(e.agent ?? "-", 11).padEnd(12) }),
7072
+ /* @__PURE__ */ jsx(Text, { color: e.dlp ? theme.bad : theme.dim, bold: e.dlp, children: ("dlp:" + (e.dlp ? "yes" : "no")).padEnd(8) }),
7073
+ /* @__PURE__ */ jsx(Text, { color: e.burst ? theme.warn : theme.dim, bold: e.burst, children: ("rl:" + (e.burst ? "yes" : "no")).padEnd(7) }),
7074
+ e.rule && e.decision !== "ALLOW" ? /* @__PURE__ */ jsx(Text, { color: theme.bad, children: truncate2(e.rule, 14) + " " }) : null,
7075
+ /* @__PURE__ */ jsx(Text, { color: theme.dim, children: e.detail })
7076
+ ] });
7077
+ }
7078
+ function DataView({
7079
+ loading,
7080
+ error,
7081
+ empty,
7082
+ emptyText,
7083
+ children
7084
+ }) {
7085
+ if (error) return /* @__PURE__ */ jsxs(Text, { color: theme.bad, children: [
7086
+ "\u2717 ",
7087
+ error
7088
+ ] });
7089
+ if (loading) return /* @__PURE__ */ jsx(Text, { color: theme.warn, children: "\u27F3 loading\u2026" });
7090
+ if (empty) return /* @__PURE__ */ jsx(Text, { color: theme.dim, children: emptyText ?? "No data." });
7091
+ return /* @__PURE__ */ jsx(Fragment, { children });
7092
+ }
7093
+ function Table({ columns, rows }) {
7094
+ const fit = (s, w) => {
7095
+ const v = s ?? "";
7096
+ if (v.length > w) return v.slice(0, Math.max(0, w - 1)) + "\u2026";
7097
+ return v.padEnd(w);
7098
+ };
7099
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
7100
+ /* @__PURE__ */ jsx(Box, { children: columns.map((c2, i) => /* @__PURE__ */ jsx(Text, { color: theme.dim, children: fit(c2.header, c2.width) + " " }, i)) }),
7101
+ rows.map((row, ri) => /* @__PURE__ */ jsx(Box, { children: row.map((cell, ci) => /* @__PURE__ */ jsx(Text, { color: cell.color, dimColor: cell.dim, bold: cell.bold, children: fit(cell.value, columns[ci]?.width ?? 10) + " " }, ci)) }, ri))
7102
+ ] });
7103
+ }
7104
+ function KeyHints({ hints }) {
7105
+ return /* @__PURE__ */ jsx(Box, { children: hints.map(([key, label], i) => /* @__PURE__ */ jsxs(Text, { color: theme.dim, children: [
7106
+ /* @__PURE__ */ jsx(Text, { color: theme.accent, children: key }),
7107
+ " ",
7108
+ label,
7109
+ i < hints.length - 1 ? " " : ""
7110
+ ] }, i)) });
7111
+ }
7112
+ var hhmmss, ymd;
7113
+ var init_components = __esm({
7114
+ "src/tui/components.tsx"() {
6950
7115
  "use strict";
7116
+ init_theme();
7117
+ hhmmss = (ts) => {
7118
+ const d = new Date(ts);
7119
+ return Number.isNaN(d.getTime()) ? "--:--:--" : d.toTimeString().slice(0, 8);
7120
+ };
7121
+ ymd = (ts) => {
7122
+ const d = new Date(ts);
7123
+ if (Number.isNaN(d.getTime())) return "----------";
7124
+ const p = (n) => String(n).padStart(2, "0");
7125
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
7126
+ };
6951
7127
  }
6952
7128
  });
6953
7129
 
6954
- // src/api-client/device-login.ts
6955
- import { spawn as spawn3 } from "child_process";
6956
- function openBrowser(url) {
7130
+ // src/tui/local-log.ts
7131
+ import { closeSync, openSync as openSync3, readFileSync as readFileSync8, readSync, statSync, writeFileSync as writeFileSync6 } from "fs";
7132
+ import { homedir as homedir6 } from "os";
7133
+ import { join as join8 } from "path";
7134
+ function tailLines(file, maxBytes = 131072) {
6957
7135
  try {
6958
- const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
6959
- const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
6960
- const child = spawn3(cmd, args, { stdio: "ignore", detached: true });
6961
- child.on("error", () => {
6962
- });
6963
- child.unref();
7136
+ const size = statSync(file).size;
7137
+ const start = Math.max(0, size - maxBytes);
7138
+ const fd = openSync3(file, "r");
7139
+ const buf = Buffer.alloc(size - start);
7140
+ readSync(fd, buf, 0, buf.length, start);
7141
+ closeSync(fd);
7142
+ const lines = buf.toString("utf-8").split("\n").filter(Boolean);
7143
+ if (start > 0) lines.shift();
7144
+ return lines;
6964
7145
  } catch {
7146
+ return [];
6965
7147
  }
6966
7148
  }
6967
- async function startDeviceLogin(apiUrl = DEFAULT_API_URL2) {
6968
- const res = await fetch(`${apiUrl}/api/v1/auth/device/start`, { method: "POST" });
6969
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
6970
- const j = await res.json();
6971
- return {
6972
- deviceCode: j.device_code,
6973
- verifyUrl: j.verification_uri_complete || j.verification_uri || "",
6974
- intervalMs: Math.max(2, Number(j.interval) || 3) * 1e3,
6975
- expiresAt: Date.now() + (Number(j.expires_in) || 600) * 1e3
6976
- };
6977
- }
6978
- async function pollDeviceLogin(apiUrl, deviceCode) {
7149
+ function deleteLocalEntry(at, tool, session) {
6979
7150
  try {
6980
- const res = await fetch(`${apiUrl}/api/v1/auth/device/poll`, {
6981
- method: "POST",
6982
- headers: { "Content-Type": "application/json" },
6983
- body: JSON.stringify({ device_code: deviceCode })
7151
+ const lines = readFileSync8(LOCAL_LOG, "utf-8").split("\n");
7152
+ let removed = 0;
7153
+ const kept = lines.filter((line) => {
7154
+ if (!line.trim()) return false;
7155
+ if (removed) return true;
7156
+ try {
7157
+ const j = JSON.parse(line);
7158
+ const hit = Date.parse(j.ts ?? "") === at && (j.tool ?? "?") === tool && (!session || j.session_id === session);
7159
+ if (hit) {
7160
+ removed++;
7161
+ return false;
7162
+ }
7163
+ } catch {
7164
+ }
7165
+ return true;
6984
7166
  });
6985
- const j = await res.json().catch(() => ({}));
6986
- if (j.status === "approved" && j.api_key) {
6987
- return { status: "approved", apiKey: j.api_key, project: j.project?.name, user: j.user?.name || j.user?.email, email: j.user?.email };
6988
- }
6989
- if (j.status === "expired" || j.status === "not_found") return { status: j.status };
6990
- return { status: "pending" };
7167
+ if (removed) writeFileSync6(LOCAL_LOG, kept.length ? kept.join("\n") + "\n" : "");
7168
+ return removed;
6991
7169
  } catch {
6992
- return { status: "pending" };
7170
+ return 0;
6993
7171
  }
6994
7172
  }
6995
- var init_device_login = __esm({
6996
- "src/api-client/device-login.ts"() {
7173
+ function clearLocalLog() {
7174
+ try {
7175
+ const n = readFileSync8(LOCAL_LOG, "utf-8").split("\n").filter(Boolean).length;
7176
+ writeFileSync6(LOCAL_LOG, "");
7177
+ return n;
7178
+ } catch {
7179
+ return 0;
7180
+ }
7181
+ }
7182
+ function reasonSignals(reason) {
7183
+ const r = reason || "";
7184
+ const dlp = [];
7185
+ if (DLP_REASON.test(r)) {
7186
+ const m = r.match(/contain(?:s)? (?:a |an )?(.+?)(?:\.|$)/i);
7187
+ dlp.push(m ? m[1].trim() : "DLP");
7188
+ }
7189
+ return { dlp, burst: RL_REASON.test(r) };
7190
+ }
7191
+ function parseLocalLines(lines) {
7192
+ const out2 = [];
7193
+ for (const line of lines) {
7194
+ try {
7195
+ const j = JSON.parse(line);
7196
+ const at = Date.parse(j.ts ?? "");
7197
+ if (Number.isFinite(at)) out2.push({ ...j, at });
7198
+ } catch {
7199
+ }
7200
+ }
7201
+ return out2;
7202
+ }
7203
+ var LOCAL_LOG, DLP_REASON, RL_REASON;
7204
+ var init_local_log = __esm({
7205
+ "src/tui/local-log.ts"() {
6997
7206
  "use strict";
6998
- init_client();
7207
+ LOCAL_LOG = join8(homedir6(), ".solongate", "local-logs", "solongate-audit.jsonl");
7208
+ DLP_REASON = /security layer \(dlp\)/i;
7209
+ RL_REASON = /security layer \(rate limit\)|rate[- ]?limit(?:ed)?\b.*exceed|exceeded \d+ calls/i;
6999
7210
  }
7000
7211
  });
7001
7212
 
7002
- // src/api-client/auth.ts
7003
- var auth_exports = {};
7004
- __export(auth_exports, {
7005
- me: () => me
7006
- });
7007
- function me() {
7008
- return request("GET", "/auth/me");
7009
- }
7010
- var init_auth = __esm({
7011
- "src/api-client/auth.ts"() {
7012
- "use strict";
7013
- init_client();
7213
+ // src/api-client/client.ts
7214
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync6, existsSync as existsSync4 } from "fs";
7215
+ import { resolve as resolve4, join as join9 } from "path";
7216
+ import { homedir as homedir7 } from "os";
7217
+ function listAccounts() {
7218
+ let list5 = [];
7219
+ try {
7220
+ const raw = JSON.parse(readFileSync9(accountsFile(), "utf-8"));
7221
+ if (Array.isArray(raw)) list5 = raw.filter((a) => a && typeof a.apiKey === "string");
7222
+ } catch {
7014
7223
  }
7015
- });
7016
-
7017
- // src/api-client/policies.ts
7018
- var policies_exports = {};
7019
- __export(policies_exports, {
7020
- active: () => active,
7021
- addRule: () => addRule,
7022
- backtest: () => backtest,
7023
- create: () => create,
7024
- dryRun: () => dryRun,
7025
- get: () => get,
7026
- list: () => list,
7027
- remove: () => remove,
7028
- revokeRule: () => revokeRule,
7029
- rollback: () => rollback,
7030
- setActive: () => setActive,
7031
- update: () => update,
7032
- versions: () => versions
7033
- });
7034
- function list() {
7035
- return request("GET", "/policies");
7224
+ const active2 = loginCredentialFile();
7225
+ if (active2.apiKey && !list5.some((a) => a.apiKey === active2.apiKey)) {
7226
+ list5.unshift({ apiKey: active2.apiKey, apiUrl: active2.apiUrl || DEFAULT_API_URL2 });
7227
+ }
7228
+ return list5;
7036
7229
  }
7037
- function get(id, version) {
7038
- return request("GET", `/policies/${encodeURIComponent(id)}`, {
7039
- query: version !== void 0 ? { version } : void 0
7040
- });
7230
+ function saveAccount(acc) {
7231
+ try {
7232
+ const list5 = (() => {
7233
+ try {
7234
+ const raw = JSON.parse(readFileSync9(accountsFile(), "utf-8"));
7235
+ return Array.isArray(raw) ? raw.filter((a) => a && a.apiKey) : [];
7236
+ } catch {
7237
+ return [];
7238
+ }
7239
+ })();
7240
+ const next = list5.filter((a) => a.apiKey !== acc.apiKey);
7241
+ next.unshift({ ...acc, addedAt: acc.addedAt ?? Date.now() });
7242
+ mkdirSync6(join9(homedir7(), ".solongate"), { recursive: true });
7243
+ writeFileSync7(accountsFile(), JSON.stringify(next, null, 2));
7244
+ } catch {
7245
+ }
7041
7246
  }
7042
- function create(policy) {
7043
- return request("POST", "/policies", { body: policy });
7247
+ function removeAccount(apiKey) {
7248
+ try {
7249
+ const list5 = (() => {
7250
+ try {
7251
+ const raw = JSON.parse(readFileSync9(accountsFile(), "utf-8"));
7252
+ return Array.isArray(raw) ? raw.filter((a) => a && a.apiKey) : [];
7253
+ } catch {
7254
+ return [];
7255
+ }
7256
+ })();
7257
+ writeFileSync7(accountsFile(), JSON.stringify(list5.filter((a) => a.apiKey !== apiKey), null, 2));
7258
+ } catch {
7259
+ }
7044
7260
  }
7045
- function update(id, policy) {
7046
- return request("PUT", `/policies/${encodeURIComponent(id)}`, { body: policy });
7261
+ function setViewCredentials(creds) {
7262
+ viewOverride = creds;
7263
+ cached2 = null;
7047
7264
  }
7048
- function remove(id) {
7049
- return request("DELETE", `/policies/${encodeURIComponent(id)}`);
7265
+ function isActiveAccount(apiKey) {
7266
+ return loginCredentialFile().apiKey === apiKey;
7050
7267
  }
7051
- function addRule(id, spec) {
7052
- return request("POST", `/policies/${encodeURIComponent(id)}/rules`, { body: spec });
7268
+ function setActiveAccount(creds) {
7269
+ try {
7270
+ const dir = join9(homedir7(), ".solongate");
7271
+ mkdirSync6(dir, { recursive: true });
7272
+ const p = join9(dir, ["cloud", "guard.json"].join("-"));
7273
+ let existing = {};
7274
+ try {
7275
+ existing = JSON.parse(readFileSync9(p, "utf-8"));
7276
+ } catch {
7277
+ }
7278
+ writeFileSync7(p, JSON.stringify({ ...existing, apiKey: creds.apiKey, apiUrl: creds.apiUrl }, null, 2));
7279
+ cached2 = null;
7280
+ return true;
7281
+ } catch {
7282
+ return false;
7283
+ }
7053
7284
  }
7054
- function revokeRule(id, ruleId) {
7055
- return request("DELETE", `/policies/${encodeURIComponent(id)}/rules/${encodeURIComponent(ruleId)}`);
7285
+ function clearActiveCredential() {
7286
+ try {
7287
+ const p = join9(homedir7(), ".solongate", ["cloud", "guard.json"].join("-"));
7288
+ if (!existsSync4(p)) {
7289
+ cached2 = null;
7290
+ return true;
7291
+ }
7292
+ let existing = {};
7293
+ try {
7294
+ existing = JSON.parse(readFileSync9(p, "utf-8"));
7295
+ } catch {
7296
+ }
7297
+ delete existing.apiKey;
7298
+ delete existing.apiUrl;
7299
+ writeFileSync7(p, JSON.stringify(existing, null, 2));
7300
+ cached2 = null;
7301
+ viewOverride = null;
7302
+ return true;
7303
+ } catch {
7304
+ return false;
7305
+ }
7056
7306
  }
7057
- function versions(id, opts = {}) {
7058
- return request("GET", `/policies/${encodeURIComponent(id)}/versions`, { query: opts });
7307
+ function loginCredentialFile() {
7308
+ try {
7309
+ const p = join9(homedir7(), ".solongate", "cloud-guard.json");
7310
+ if (!existsSync4(p)) return {};
7311
+ const c2 = JSON.parse(readFileSync9(p, "utf-8"));
7312
+ return c2 && typeof c2 === "object" ? c2 : {};
7313
+ } catch {
7314
+ return {};
7315
+ }
7059
7316
  }
7060
- function rollback(id, version) {
7061
- return request("POST", `/policies/${encodeURIComponent(id)}/rollback`, { body: { version } });
7317
+ function dotenvApiKey() {
7318
+ try {
7319
+ const envPath = resolve4(".env");
7320
+ if (!existsSync4(envPath)) return void 0;
7321
+ for (const line of readFileSync9(envPath, "utf-8").split("\n")) {
7322
+ const trimmed = line.trim();
7323
+ if (!trimmed || trimmed.startsWith("#")) continue;
7324
+ const eq = trimmed.indexOf("=");
7325
+ if (eq === -1) continue;
7326
+ const key = trimmed.slice(0, eq).trim();
7327
+ if (key !== "SOLONGATE_API_KEY") continue;
7328
+ return trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
7329
+ }
7330
+ } catch {
7331
+ }
7332
+ return void 0;
7062
7333
  }
7063
- function active(agentId) {
7064
- return request("GET", "/policies/active", { query: agentId ? { agent_id: agentId } : void 0 });
7334
+ function isAuthenticated() {
7335
+ try {
7336
+ resolveCredentials();
7337
+ return true;
7338
+ } catch {
7339
+ return false;
7340
+ }
7065
7341
  }
7066
- function setActive(policyId) {
7067
- return request("POST", "/policies/active", { body: { policyId: policyId ?? "" } });
7342
+ function resolveCredentials(apiUrlOverride) {
7343
+ if (viewOverride && !apiUrlOverride) return viewOverride;
7344
+ if (cached2 && !apiUrlOverride) return cached2;
7345
+ const file = loginCredentialFile();
7346
+ const apiKey = process.env["SOLONGATE_API_KEY"] || file.apiKey || dotenvApiKey();
7347
+ if (!apiKey) throw new NotAuthenticatedError();
7348
+ const apiUrl = apiUrlOverride || process.env["SOLONGATE_API_URL"] || file.apiUrl || DEFAULT_API_URL2;
7349
+ const creds = { apiKey, apiUrl: apiUrl.replace(/\/$/, "") };
7350
+ if (!apiUrlOverride) cached2 = creds;
7351
+ return creds;
7068
7352
  }
7069
- function dryRun(body) {
7070
- return request("POST", "/policies/dry-run", { body });
7353
+ function buildUrl(base, path, query) {
7354
+ const url = new URL(`${base}/api/v1${path}`);
7355
+ if (query) {
7356
+ for (const [k, v] of Object.entries(query)) {
7357
+ if (v === void 0) continue;
7358
+ url.searchParams.set(k, String(v));
7359
+ }
7360
+ }
7361
+ return url.toString();
7071
7362
  }
7072
- function backtest(body) {
7073
- return request("POST", "/policies/backtest", { body });
7363
+ async function request(method, path, opts = {}) {
7364
+ const creds = resolveCredentials(opts.apiUrl);
7365
+ const url = buildUrl(creds.apiUrl, path, opts.query);
7366
+ const headers = {
7367
+ Authorization: `Bearer ${creds.apiKey}`
7368
+ };
7369
+ let bodyInit;
7370
+ if (opts.body !== void 0) {
7371
+ headers["Content-Type"] = "application/json";
7372
+ bodyInit = JSON.stringify(opts.body);
7373
+ }
7374
+ const attempt = () => fetch(url, {
7375
+ method,
7376
+ headers: { ...headers, Connection: "close" },
7377
+ body: bodyInit,
7378
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
7379
+ });
7380
+ const maxTries = method === "GET" ? 3 : 1;
7381
+ let res;
7382
+ let lastErr;
7383
+ for (let i = 0; i < maxTries; i++) {
7384
+ try {
7385
+ res = await attempt();
7386
+ break;
7387
+ } catch (err2) {
7388
+ lastErr = err2;
7389
+ if (i < maxTries - 1) await new Promise((r) => setTimeout(r, 600 * (i + 1)));
7390
+ }
7391
+ }
7392
+ if (!res) {
7393
+ const msg = lastErr instanceof Error ? lastErr.message : String(lastErr);
7394
+ throw new ApiError(0, "NETWORK_ERROR", `Cannot reach SolonGate API: ${msg}`);
7395
+ }
7396
+ const text = await res.text().catch(() => "");
7397
+ let json = void 0;
7398
+ if (text) {
7399
+ try {
7400
+ json = JSON.parse(text);
7401
+ } catch {
7402
+ }
7403
+ }
7404
+ if (!res.ok) {
7405
+ const envelope = json?.error;
7406
+ if (envelope && typeof envelope === "object") {
7407
+ throw new ApiError(res.status, envelope.code || "ERROR", envelope.message || res.statusText);
7408
+ }
7409
+ if (typeof envelope === "string") {
7410
+ throw new ApiError(res.status, "ERROR", envelope);
7411
+ }
7412
+ if (res.status === 401) throw new ApiError(401, "AUTHENTICATION_ERROR", "Invalid API key. Run `solongate` and log in from the Accounts panel.");
7413
+ if (res.status === 429) throw new ApiError(429, "RATE_LIMITED", "Rate limited by the API. Slow down and retry.");
7414
+ if (res.status >= 500) throw new ApiError(res.status, "SERVER_ERROR", text || "SolonGate API had a problem (server error). Please try again in a moment.");
7415
+ throw new ApiError(res.status, "ERROR", text || res.statusText || `HTTP ${res.status}`);
7416
+ }
7417
+ return json;
7074
7418
  }
7075
- var init_policies = __esm({
7076
- "src/api-client/policies.ts"() {
7419
+ var DEFAULT_API_URL2, accountsFile, viewOverride, ApiError, NotAuthenticatedError, cached2;
7420
+ var init_client = __esm({
7421
+ "src/api-client/client.ts"() {
7077
7422
  "use strict";
7078
- init_client();
7423
+ DEFAULT_API_URL2 = "https://api.solongate.com";
7424
+ accountsFile = () => join9(homedir7(), ".solongate", "accounts.json");
7425
+ viewOverride = null;
7426
+ ApiError = class extends Error {
7427
+ status;
7428
+ code;
7429
+ constructor(status, code, message) {
7430
+ super(message);
7431
+ this.name = "ApiError";
7432
+ this.status = status;
7433
+ this.code = code;
7434
+ }
7435
+ };
7436
+ NotAuthenticatedError = class extends Error {
7437
+ constructor() {
7438
+ super("Not logged in. Run `solongate` and log in from the Accounts panel.");
7439
+ this.name = "NotAuthenticatedError";
7440
+ }
7441
+ };
7442
+ cached2 = null;
7079
7443
  }
7080
7444
  });
7081
7445
 
7082
- // src/api-client/settings.ts
7083
- var settings_exports = {};
7084
- __export(settings_exports, {
7085
- clearRateLimitHistory: () => clearRateLimitHistory,
7086
- createAlert: () => createAlert,
7087
- createWebhook: () => createWebhook,
7088
- deleteAlert: () => deleteAlert,
7089
- deleteWebhook: () => deleteWebhook,
7090
- getAlerts: () => getAlerts,
7446
+ // src/api-client/types.ts
7447
+ var init_types = __esm({
7448
+ "src/api-client/types.ts"() {
7449
+ "use strict";
7450
+ }
7451
+ });
7452
+
7453
+ // src/api-client/device-login.ts
7454
+ import { spawn as spawn3 } from "child_process";
7455
+ function openBrowser(url) {
7456
+ try {
7457
+ const cmd = process.platform === "win32" ? "cmd" : process.platform === "darwin" ? "open" : "xdg-open";
7458
+ const args = process.platform === "win32" ? ["/c", "start", '""', url] : [url];
7459
+ const child = spawn3(cmd, args, { stdio: "ignore", detached: true });
7460
+ child.on("error", () => {
7461
+ });
7462
+ child.unref();
7463
+ } catch {
7464
+ }
7465
+ }
7466
+ async function startDeviceLogin(apiUrl = DEFAULT_API_URL2) {
7467
+ const res = await fetch(`${apiUrl}/api/v1/auth/device/start`, { method: "POST" });
7468
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
7469
+ const j = await res.json();
7470
+ return {
7471
+ deviceCode: j.device_code,
7472
+ verifyUrl: j.verification_uri_complete || j.verification_uri || "",
7473
+ intervalMs: Math.max(2, Number(j.interval) || 3) * 1e3,
7474
+ expiresAt: Date.now() + (Number(j.expires_in) || 600) * 1e3
7475
+ };
7476
+ }
7477
+ async function pollDeviceLogin(apiUrl, deviceCode) {
7478
+ try {
7479
+ const res = await fetch(`${apiUrl}/api/v1/auth/device/poll`, {
7480
+ method: "POST",
7481
+ headers: { "Content-Type": "application/json" },
7482
+ body: JSON.stringify({ device_code: deviceCode })
7483
+ });
7484
+ const j = await res.json().catch(() => ({}));
7485
+ if (j.status === "approved" && j.api_key) {
7486
+ return { status: "approved", apiKey: j.api_key, project: j.project?.name, user: j.user?.name || j.user?.email, email: j.user?.email };
7487
+ }
7488
+ if (j.status === "expired" || j.status === "not_found") return { status: j.status };
7489
+ return { status: "pending" };
7490
+ } catch {
7491
+ return { status: "pending" };
7492
+ }
7493
+ }
7494
+ var init_device_login = __esm({
7495
+ "src/api-client/device-login.ts"() {
7496
+ "use strict";
7497
+ init_client();
7498
+ }
7499
+ });
7500
+
7501
+ // src/api-client/auth.ts
7502
+ var auth_exports = {};
7503
+ __export(auth_exports, {
7504
+ me: () => me
7505
+ });
7506
+ function me() {
7507
+ return request("GET", "/auth/me");
7508
+ }
7509
+ var init_auth = __esm({
7510
+ "src/api-client/auth.ts"() {
7511
+ "use strict";
7512
+ init_client();
7513
+ }
7514
+ });
7515
+
7516
+ // src/api-client/policies.ts
7517
+ var policies_exports = {};
7518
+ __export(policies_exports, {
7519
+ active: () => active,
7520
+ addRule: () => addRule,
7521
+ backtest: () => backtest,
7522
+ create: () => create,
7523
+ dryRun: () => dryRun,
7524
+ get: () => get,
7525
+ list: () => list,
7526
+ remove: () => remove,
7527
+ revokeRule: () => revokeRule,
7528
+ rollback: () => rollback,
7529
+ setActive: () => setActive,
7530
+ update: () => update,
7531
+ versions: () => versions
7532
+ });
7533
+ function list() {
7534
+ return request("GET", "/policies");
7535
+ }
7536
+ function get(id, version) {
7537
+ return request("GET", `/policies/${encodeURIComponent(id)}`, {
7538
+ query: version !== void 0 ? { version } : void 0
7539
+ });
7540
+ }
7541
+ function create(policy) {
7542
+ return request("POST", "/policies", { body: policy });
7543
+ }
7544
+ function update(id, policy) {
7545
+ return request("PUT", `/policies/${encodeURIComponent(id)}`, { body: policy });
7546
+ }
7547
+ function remove(id) {
7548
+ return request("DELETE", `/policies/${encodeURIComponent(id)}`);
7549
+ }
7550
+ function addRule(id, spec) {
7551
+ return request("POST", `/policies/${encodeURIComponent(id)}/rules`, { body: spec });
7552
+ }
7553
+ function revokeRule(id, ruleId) {
7554
+ return request("DELETE", `/policies/${encodeURIComponent(id)}/rules/${encodeURIComponent(ruleId)}`);
7555
+ }
7556
+ function versions(id, opts = {}) {
7557
+ return request("GET", `/policies/${encodeURIComponent(id)}/versions`, { query: opts });
7558
+ }
7559
+ function rollback(id, version) {
7560
+ return request("POST", `/policies/${encodeURIComponent(id)}/rollback`, { body: { version } });
7561
+ }
7562
+ function active(agentId) {
7563
+ return request("GET", "/policies/active", { query: agentId ? { agent_id: agentId } : void 0 });
7564
+ }
7565
+ function setActive(policyId) {
7566
+ return request("POST", "/policies/active", { body: { policyId: policyId ?? "" } });
7567
+ }
7568
+ function dryRun(body) {
7569
+ return request("POST", "/policies/dry-run", { body });
7570
+ }
7571
+ function backtest(body) {
7572
+ return request("POST", "/policies/backtest", { body });
7573
+ }
7574
+ var init_policies = __esm({
7575
+ "src/api-client/policies.ts"() {
7576
+ "use strict";
7577
+ init_client();
7578
+ }
7579
+ });
7580
+
7581
+ // src/api-client/settings.ts
7582
+ var settings_exports = {};
7583
+ __export(settings_exports, {
7584
+ clearRateLimitHistory: () => clearRateLimitHistory,
7585
+ createAlert: () => createAlert,
7586
+ createWebhook: () => createWebhook,
7587
+ deleteAlert: () => deleteAlert,
7588
+ deleteWebhook: () => deleteWebhook,
7589
+ getAlerts: () => getAlerts,
7091
7590
  getGuardStatus: () => getGuardStatus,
7092
7591
  getLocalLogs: () => getLocalLogs,
7093
7592
  getRateLimitHistory: () => getRateLimitHistory,
@@ -7413,9 +7912,9 @@ var init_hooks = __esm({
7413
7912
  // src/tui/panels/Live.tsx
7414
7913
  import { Box as Box2, Text as Text2, useInput } from "ink";
7415
7914
  import TextInput from "ink-text-input";
7416
- import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
7417
- import { homedir as homedir7 } from "os";
7418
- import { join as join9 } from "path";
7915
+ import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
7916
+ import { homedir as homedir8 } from "os";
7917
+ import { join as join10 } from "path";
7419
7918
  import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
7420
7919
  import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
7421
7920
  function extractTarget(detail) {
@@ -7973,10 +8472,10 @@ function LivePanel({ active: active2 }) {
7973
8472
  else if (input === "x") toggleSignal("dlp");
7974
8473
  else if (input === "r") toggleSignal("ratelimit");
7975
8474
  else if (input === "e") {
7976
- const file = join9(homedir7(), ".solongate", "live-export.jsonl");
8475
+ const file = join10(homedir8(), ".solongate", "live-export.jsonl");
7977
8476
  try {
7978
- mkdirSync6(join9(homedir7(), ".solongate"), { recursive: true });
7979
- writeFileSync7(file, visibleDesc.map((x) => JSON.stringify(x)).join("\n") + "\n");
8477
+ mkdirSync7(join10(homedir8(), ".solongate"), { recursive: true });
8478
+ writeFileSync8(file, visibleDesc.map((x) => JSON.stringify(x)).join("\n") + "\n");
7980
8479
  setActionMsg({ text: `\u2713 exported ${visibleDesc.length} lines \u2192 ${file}`, level: "ok", until: Date.now() + 6e3 });
7981
8480
  } catch (err2) {
7982
8481
  setActionMsg({ text: "\u2717 export failed: " + (err2 instanceof Error ? err2.message : String(err2)), level: "bad", until: Date.now() + 6e3 });
@@ -8387,7 +8886,7 @@ var init_Live = __esm({
8387
8886
  SPIN = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
8388
8887
  BG = "#12234f";
8389
8888
  DIM_FLOOR = "#233457";
8390
- RING = join9(process.cwd(), ".solongate", ".eval-ring.jsonl");
8889
+ RING = join10(process.cwd(), ".solongate", ".eval-ring.jsonl");
8391
8890
  fmtUp = (ms) => {
8392
8891
  const s = Math.floor(ms / 1e3);
8393
8892
  const p = (n) => String(n).padStart(2, "0");
@@ -9390,9 +9889,9 @@ var init_Dlp = __esm({
9390
9889
  // src/tui/panels/Audit.tsx
9391
9890
  import { Box as Box7, Text as Text7, useInput as useInput6 } from "ink";
9392
9891
  import TextInput5 from "ink-text-input";
9393
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync8 } from "fs";
9394
- import { homedir as homedir8 } from "os";
9395
- import { join as join10 } from "path";
9892
+ import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync9 } from "fs";
9893
+ import { homedir as homedir9 } from "os";
9894
+ import { join as join11 } from "path";
9396
9895
  import { useState as useState7 } from "react";
9397
9896
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
9398
9897
  function loadLocalRows() {
@@ -9542,16 +10041,16 @@ function AuditPanel({ active: active2, focused }) {
9542
10041
  const doExport = (kind) => {
9543
10042
  setMsg({ text: "exporting\u2026", level: "ok" });
9544
10043
  const run10 = async () => {
9545
- const dir = join10(homedir8(), ".solongate");
9546
- const file = join10(dir, `audit-export-${source}.jsonl`);
10044
+ const dir = join11(homedir9(), ".solongate");
10045
+ const file = join11(dir, `audit-export-${source}.jsonl`);
9547
10046
  let rows2;
9548
10047
  if (kind === "page") rows2 = pageRows;
9549
10048
  else if (source === "cloud") {
9550
10049
  const r = await api.audit.list({ ...query, limit: 1e4, offset: 0 });
9551
10050
  rows2 = r.entries.map(cloudRow).sort((a, b) => b.at - a.at);
9552
10051
  } else rows2 = localFiltered;
9553
- mkdirSync7(dir, { recursive: true });
9554
- writeFileSync8(file, rows2.map((x) => JSON.stringify(x)).join("\n") + (rows2.length ? "\n" : ""));
10052
+ mkdirSync8(dir, { recursive: true });
10053
+ writeFileSync9(file, rows2.map((x) => JSON.stringify(x)).join("\n") + (rows2.length ? "\n" : ""));
9555
10054
  return { n: rows2.length, file };
9556
10055
  };
9557
10056
  run10().then(({ n, file }) => setMsg({ text: `\u2713 exported ${n} rows \u2192 ${file}`, level: "ok" })).catch((e) => setMsg({ text: "\u2717 export failed: " + (e instanceof Error ? e.message : String(e)), level: "bad" }));
@@ -9780,551 +10279,245 @@ function AuditPanel({ active: active2, focused }) {
9780
10279
  /* @__PURE__ */ jsx7(Text7, { children: e.trust || "\u2014" }),
9781
10280
  /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2502 eval " }),
9782
10281
  /* @__PURE__ */ jsx7(Text7, { color: e.evalMs != null && e.evalMs > 500 ? theme.warn : void 0, children: e.evalMs != null ? `${e.evalMs}ms` : "\u2014" }),
9783
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2502 agent " }),
9784
- /* @__PURE__ */ jsx7(Text7, { children: e.agent ?? "\u2014" }),
9785
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2502" })
9786
- ] }),
9787
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
9788
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2502 session " }),
9789
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: e.session ?? "\u2014" }),
9790
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2502 rule " }),
9791
- /* @__PURE__ */ jsx7(Text7, { color: e.rule && e.decision !== "ALLOW" ? theme.bad : theme.dim, children: e.rule ?? "\u2014" }),
9792
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2502" })
9793
- ] }),
9794
- /* @__PURE__ */ jsx7(PaneTitle, { label: "FULL CONTENT", extra: `${contentLines.length} lines${maxScroll ? ` \xB7 \u25BC${maxScroll - off} more \xB7 \u2191\u2193 scroll` : ""} \xB7 space copy \xB7 \u2190 back`, width: innerW }),
9795
- /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", height: bodyRows, overflow: "hidden", children: win.map((l, i) => /* @__PURE__ */ jsx7(Text7, { wrap: "truncate", children: l || " " }, off + i)) }),
9796
- /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
9797
- /* @__PURE__ */ jsx7(Text7, { backgroundColor: BG2, color: "white", bold: true, children: " ENTRY " }),
9798
- /* @__PURE__ */ jsx7(Text7, { backgroundColor: "#0b1530", color: "white", children: ` ${e.id} ` }),
9799
- /* @__PURE__ */ jsx7(Text7, { backgroundColor: BG2, color: "white", children: " \u2191\u2193 scroll \xB7 space copy \xB7 ? all keys \xB7 \u2190 back \xB7 esc menu " })
9800
- ] })
9801
- ] });
9802
- }
9803
- const chip = (label, val, on) => /* @__PURE__ */ jsxs7(Text7, { children: [
9804
- /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
9805
- label,
9806
- ":"
9807
- ] }),
9808
- /* @__PURE__ */ jsx7(Text7, { color: on ? theme.accentBright : theme.dim, children: val }),
9809
- /* @__PURE__ */ jsx7(Text7, { children: " " })
9810
- ] });
9811
- if (view === "sessions") {
9812
- const headerRows2 = 5 + (editing ? 1 : 0);
9813
- const listRows2 = Math.max(4, rows - headerRows2);
9814
- const selC = Math.min(sessSel, Math.max(0, sessionsFiltered.length - 1));
9815
- const start2 = Math.min(Math.max(0, selC - Math.floor((listRows2 - 1) / 2)), Math.max(0, sessionsFiltered.length - listRows2));
9816
- const win = sessionsFiltered.slice(start2, start2 + listRows2);
9817
- return /* @__PURE__ */ jsx7(DataView, { loading: sessLoading && !frozen, error: source === "cloud" ? agentsQ.error : localQ.error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
9818
- strip,
9819
- copyBanner,
9820
- /* @__PURE__ */ jsxs7(Box7, { children: [
9821
- srcChip,
9822
- /* @__PURE__ */ jsx7(Text7, { color: theme.accentBright, bold: true, children: "SESSIONS" }),
9823
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \xB7 logs (v) " }),
9824
- chip("status", SESS_STATUS[si] ?? "all", si !== 0),
9825
- chip("search", sessSearch || "\xB7", !!sessSearch)
9826
- ] }),
9827
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, wrap: "truncate", children: focused ? "\u2191\u2193 select \xB7 enter \u2192 session logs \xB7 v logs \xB7 ^R refresh \xB7 ? all keys" : "press \u2192 to browse" }),
9828
- editing === "sess-search" ? /* @__PURE__ */ jsxs7(Box7, { children: [
9829
- /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: "search: " }),
9830
- /* @__PURE__ */ jsx7(
9831
- TextInput5,
9832
- {
9833
- value: sessSearch,
9834
- onChange: (v) => {
9835
- setSessSearch(v);
9836
- setSessSel(0);
9837
- },
9838
- onSubmit: () => setEditing(null)
9839
- }
9840
- )
9841
- ] }) : null,
9842
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, wrap: "truncate", children: `${sessionsFiltered.length} sessions \xB7 ${selC + 1}/${sessionsFiltered.length}${start2 ? ` \xB7 \u25B2${start2}` : ""}${start2 + listRows2 < sessionsFiltered.length ? ` \xB7 \u25BC${sessionsFiltered.length - start2 - listRows2}` : ""}` }),
9843
- /* @__PURE__ */ jsx7(
9844
- Table,
9845
- {
9846
- columns: [
9847
- { header: "", width: 2 },
9848
- { header: "STATUS", width: 8 },
9849
- { header: "AGENT", width: 18 },
9850
- { header: "SESSION", width: 10 },
9851
- { header: "CALLS", width: 6 },
9852
- { header: "DENY", width: 5 },
9853
- { header: "DLP", width: 4 },
9854
- { header: "TRUST", width: 5 },
9855
- { header: "LAST", width: 6 }
9856
- ],
9857
- rows: win.map((r, i) => {
9858
- const st = STATUS_DOT[r.status];
9859
- const isSel = start2 + i === selC && focused;
9860
- return [
9861
- { value: isSel ? "\u25B8" : "", color: theme.accentBright },
9862
- { value: `${st.ch} ${r.status}`, color: st.color },
9863
- { value: truncate2(r.agent, 18), color: theme.accent, bold: isSel },
9864
- { value: r.id.slice(0, 8), dim: true },
9865
- { value: String(r.calls) },
9866
- { value: String(r.denies), color: r.denies ? theme.bad : void 0, dim: !r.denies },
9867
- { value: String(r.dlp), color: r.dlp ? theme.bad : void 0, dim: !r.dlp },
9868
- { value: r.trust != null ? String(r.trust) : "\u2014", dim: true },
9869
- { value: ago(r.lastAt), dim: true }
9870
- ];
9871
- })
9872
- }
9873
- ),
9874
- sessionsFiltered.length === 0 ? /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
9875
- "(no sessions",
9876
- source === "local" ? " in the local file" : "",
9877
- ")"
9878
- ] }) : null
9879
- ] }) });
9880
- }
9881
- const headerRows = 6 + (editing && editing !== "sess-search" ? 1 : 0) + (msg ? 1 : 0) + (frozen ? 1 : 0);
9882
- const listRows = Math.max(4, rows - headerRows);
9883
- const selClamped = Math.min(sel, Math.max(0, pageRows.length - 1));
9884
- const maxStart = Math.max(0, pageRows.length - listRows);
9885
- const start = Math.min(Math.max(0, selClamped - Math.floor((listRows - 1) / 2)), maxStart);
9886
- const windowed = pageRows.slice(start, start + listRows);
9887
- return /* @__PURE__ */ jsx7(DataView, { loading: logsLoading && !frozen, error: source === "cloud" ? cloudQ.error : localQ.error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
9888
- strip,
9889
- copyBanner,
9890
- /* @__PURE__ */ jsxs7(Box7, { children: [
9891
- srcChip,
9892
- /* @__PURE__ */ jsx7(Text7, { color: theme.accentBright, bold: true, children: "LOGS" }),
9893
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \xB7 sessions (v) " }),
9894
- chip("dec", DECISIONS[di] ?? "all", di !== 0),
9895
- chip("sig", SIGNALS[gi] ?? "all", gi !== 0),
9896
- chip("tool", tool || "\xB7", !!tool),
9897
- chip("agent", agent || "\xB7", !!agent),
9898
- chip("search", search || "\xB7", !!search),
9899
- sessFilter ? chip("sess", sessFilter.slice(0, 8), true) : null
9900
- ] }),
9901
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, wrap: "truncate", children: focused ? "\u2191\u2193 select \xB7 enter full entry \xB7 \u2190\u2192 page \xB7 v sessions \xB7 ^R refresh \xB7 ? all keys" : "press \u2192 to browse" }),
9902
- msg ? /* @__PURE__ */ jsx7(Text7, { color: msg.level === "bad" ? theme.bad : theme.ok, children: truncate2(msg.text, cols) }) : null,
9903
- editing && editing !== "sess-search" ? /* @__PURE__ */ jsxs7(Box7, { children: [
9904
- /* @__PURE__ */ jsxs7(Text7, { color: theme.warn, children: [
9905
- editing,
9906
- ": "
9907
- ] }),
9908
- /* @__PURE__ */ jsx7(
9909
- TextInput5,
9910
- {
9911
- value: editing === "tool" ? tool : editing === "agent" ? agent : search,
9912
- onChange: (v) => {
9913
- (editing === "tool" ? setTool : editing === "agent" ? setAgent : setSearch)(v);
9914
- setPage(0);
9915
- toTop();
9916
- },
9917
- onSubmit: () => setEditing(null)
9918
- }
9919
- )
9920
- ] }) : null,
9921
- /* @__PURE__ */ jsx7(Text7, { color: theme.dim, wrap: "truncate", children: `${total} matched \xB7 page ${Math.min(page + 1, pages)}/${pages} \xB7 ${PAGE}/page \xB7 ${pageRows.length ? selClamped + 1 : 0}/${pageRows.length}${start ? ` \xB7 \u25B2${start} newer` : ""}${start + listRows < pageRows.length ? ` \xB7 \u25BC${pageRows.length - start - listRows} older` : ""}` }),
9922
- /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", overflow: "hidden", children: windowed.map((e, i) => /* @__PURE__ */ jsx7(StreamLine, { e: toStream(e), loc: source === "local", selected: start + i === selClamped && focused, date: true }, e.id)) }),
9923
- pageRows.length === 0 ? /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
9924
- "(no entries",
9925
- source === "local" ? " in the local file" : "",
9926
- " \u2014 c clears filters)"
9927
- ] }) : null
9928
- ] }) });
9929
- }
9930
- var DECISIONS, SIGNALS, SESS_STATUS, PAGE, LOCAL_MAX_BYTES, BG2, toStream, cloudRow, AUDIT_HELP, sessStatus2, STATUS_DOT;
9931
- var init_Audit = __esm({
9932
- "src/tui/panels/Audit.tsx"() {
9933
- "use strict";
9934
- init_api_client();
9935
- init_components();
9936
- init_hooks();
9937
- init_local_log();
9938
- init_theme();
9939
- DECISIONS = [void 0, "DENY", "ALLOW"];
9940
- SIGNALS = [void 0, "dlp", "ratelimit"];
9941
- SESS_STATUS = [void 0, "active", "idle", "ended"];
9942
- PAGE = 500;
9943
- LOCAL_MAX_BYTES = 16 * 1024 * 1024;
9944
- BG2 = "#12234f";
9945
- toStream = (e) => ({
9946
- at: e.at,
9947
- tool: e.tool,
9948
- decision: e.decision,
9949
- permission: (e.permission ?? "").slice(0, 4),
9950
- detail: (e.args ?? e.reason ?? "").replace(/\s+/g, " "),
9951
- dlp: e.dlp.length > 0,
9952
- burst: e.burst,
9953
- agent: e.agent,
9954
- evalMs: e.evalMs,
9955
- rule: e.rule
9956
- });
9957
- cloudRow = (e) => {
9958
- const rs = reasonSignals(e.reason);
9959
- return {
9960
- id: e.id,
9961
- at: Date.parse(e.created_at),
9962
- tool: e.tool_name,
9963
- decision: e.decision,
9964
- permission: e.permission,
9965
- trust: e.trust_level,
9966
- agent: e.agent_name,
9967
- session: e.session_id,
9968
- reason: e.reason,
9969
- rule: e.matched_rule_id,
9970
- evalMs: e.evaluation_time_ms,
9971
- dlp: e.dlp_matches?.length ? e.dlp_matches : rs.dlp,
9972
- burst: !!e.rate_limit_burst || rs.burst,
9973
- args: e.arguments_summary ? JSON.stringify(e.arguments_summary) : null
9974
- };
9975
- };
9976
- AUDIT_HELP = [
9977
- [
9978
- "Logs",
9979
- [
9980
- ["\u2191\u2193 / PgUp PgDn", "select a row (window follows)"],
9981
- ["enter", "open the FULL entry (reason + arguments)"],
9982
- ["\u2190 \u2192", "previous / next page (500 per page, jumps to top)"],
9983
- ["f", "decision filter: all \u2192 DENY \u2192 ALLOW"],
9984
- ["g", "signal filter: all \u2192 dlp \u2192 ratelimit"],
9985
- ["t / n", "tool / agent filter (type, enter done)"],
9986
- ["/", "free-text search"],
9987
- ["x", "delete ONLY the selected entry (press x twice)"],
9988
- ["X", "delete ALL matched logs of the source (press X twice)"],
9989
- ["e", "export this page \u2192 ~/.solongate/audit-export-<src>.jsonl"],
9990
- ["E", "export ALL matched rows (cloud: up to 10k)"],
9991
- ["c", "clear every filter (incl. session)"]
9992
- ]
9993
- ],
9994
- [
9995
- "Sessions",
9996
- [
9997
- ["\u2191\u2193", "select a session"],
9998
- ["enter", "open that session's logs"],
9999
- ["f", "status filter: all \u2192 active \u2192 idle \u2192 ended"],
10000
- ["/", "search agent / session id"],
10001
- ["c", "clear session filters"]
10002
- ]
10003
- ],
10004
- [
10005
- "Anywhere in Audit",
10006
- [
10007
- ["v", "switch logs \u2194 sessions"],
10008
- ["s", "switch source cloud \u2194 local file"],
10009
- ["space", "copy mode: freeze screen for mouse selection"],
10010
- ["?", "this help \xB7 any key closes"],
10011
- ["esc", "back to the menu"]
10012
- ]
10013
- ],
10014
- ["Entry (full content)", [["\u2191\u2193 / PgUp PgDn", "scroll the reason + arguments"], ["space", "copy mode (freeze, then select)"], ["\u2190 / esc", "back to the list"]]]
10015
- ];
10016
- sessStatus2 = (lastAt) => Date.now() - lastAt < 6e4 ? "active" : Date.now() - lastAt < 3e5 ? "idle" : "ended";
10017
- STATUS_DOT = {
10018
- active: { ch: "\u25CF", color: theme.ok },
10019
- idle: { ch: "\u25D0", color: theme.warn },
10020
- ended: { ch: "\u25CB", color: theme.dim }
10021
- };
10022
- }
10023
- });
10024
-
10025
- // src/global-install.ts
10026
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync4, mkdirSync as mkdirSync8, rmSync as rmSync2 } from "fs";
10027
- import { resolve as resolve4, join as join11, dirname as dirname3 } from "path";
10028
- import { homedir as homedir9 } from "os";
10029
- import { fileURLToPath as fileURLToPath3 } from "url";
10030
- import { createInterface } from "readline";
10031
- import { execFileSync as execFileSync2 } from "child_process";
10032
- function lockFile(file) {
10033
- if (!existsSync4(file)) return;
10034
- try {
10035
- if (process.platform === "win32") {
10036
- try {
10037
- execFileSync2("icacls", [file, "/deny", "*S-1-1-0:(WD,AD,DC,DE)"], { stdio: "ignore" });
10038
- } catch {
10039
- }
10040
- try {
10041
- execFileSync2("attrib", ["+R", file], { stdio: "ignore" });
10042
- } catch {
10043
- }
10044
- } else if (process.platform === "darwin") {
10045
- try {
10046
- execFileSync2("chflags", ["uchg", file], { stdio: "ignore" });
10047
- } catch {
10048
- }
10049
- } else {
10050
- try {
10051
- execFileSync2("chattr", ["+i", file], { stdio: "ignore" });
10052
- } catch {
10053
- }
10054
- }
10055
- } catch {
10056
- }
10057
- }
10058
- function unlockFile(file) {
10059
- if (!existsSync4(file)) return;
10060
- try {
10061
- if (process.platform === "win32") {
10062
- try {
10063
- execFileSync2("icacls", [file, "/remove:d", "*S-1-1-0"], { stdio: "ignore" });
10064
- } catch {
10065
- }
10066
- try {
10067
- execFileSync2("icacls", [file, "/reset"], { stdio: "ignore" });
10068
- } catch {
10069
- }
10070
- try {
10071
- execFileSync2("attrib", ["-R", file], { stdio: "ignore" });
10072
- } catch {
10073
- }
10074
- } else if (process.platform === "darwin") {
10075
- try {
10076
- execFileSync2("chflags", ["nouchg", file], { stdio: "ignore" });
10077
- } catch {
10078
- }
10079
- } else {
10080
- try {
10081
- execFileSync2("chattr", ["-i", file], { stdio: "ignore" });
10082
- } catch {
10083
- }
10084
- }
10085
- } catch {
10086
- }
10087
- }
10088
- function protectedTargets() {
10089
- const p = globalPaths();
10090
- return [
10091
- join11(p.hooksDir, "guard.mjs"),
10092
- join11(p.hooksDir, "audit.mjs"),
10093
- join11(p.hooksDir, "stop.mjs"),
10094
- join11(p.hooksDir, "shield.mjs"),
10095
- p.configPath,
10096
- p.settingsPath,
10097
- p.antigravityHooksPath
10098
- ];
10099
- }
10100
- function lockProtected() {
10101
- for (const f of protectedTargets()) lockFile(f);
10102
- }
10103
- function unlockProtected() {
10104
- for (const f of protectedTargets()) unlockFile(f);
10105
- }
10106
- function globalPaths() {
10107
- const home = homedir9();
10108
- const sgDir = join11(home, ".solongate");
10109
- const hooksDir = join11(sgDir, "hooks");
10110
- const claudeDir = join11(home, ".claude");
10111
- const antigravityDir = join11(home, ".gemini", "config");
10112
- return {
10113
- home,
10114
- sgDir,
10115
- hooksDir,
10116
- claudeDir,
10117
- antigravityDir,
10118
- settingsPath: join11(claudeDir, "settings.json"),
10119
- backupPath: join11(claudeDir, "settings.solongate.bak"),
10120
- configPath: join11(sgDir, "cloud-guard.json"),
10121
- // Antigravity reads global hooks from ~/.gemini/config/hooks.json. Only the
10122
- // guard is registered there (PreToolUse); Antigravity's ALLOW-path audit is
10123
- // covered by the passive session-log collector, not a hook.
10124
- antigravityHooksPath: join11(antigravityDir, "hooks.json"),
10125
- antigravityBackupPath: join11(antigravityDir, "hooks.solongate.bak")
10126
- };
10127
- }
10128
- function readHook(filename) {
10129
- return readFileSync9(join11(HOOKS_DIR, filename), "utf-8");
10130
- }
10131
- function firstAccountCredential() {
10132
- try {
10133
- const raw = JSON.parse(readFileSync9(join11(homedir9(), ".solongate", "accounts.json"), "utf-8"));
10134
- if (Array.isArray(raw)) {
10135
- const acc = raw.find((a) => a && typeof a.apiKey === "string" && a.apiKey);
10136
- if (acc) return { apiKey: acc.apiKey, apiUrl: typeof acc.apiUrl === "string" ? acc.apiUrl : void 0 };
10137
- }
10138
- } catch {
10139
- }
10140
- return {};
10141
- }
10142
- function readGuard() {
10143
- const bundled = join11(HOOKS_DIR, "guard.bundled.mjs");
10144
- return existsSync4(bundled) ? readFileSync9(bundled, "utf-8") : readHook("guard.mjs");
10145
- }
10146
- function antigravityHookCommand(guardAbs) {
10147
- const nodeBin = process.execPath.replace(/\\/g, "/");
10148
- const call = process.platform === "win32" ? "& " : "";
10149
- return `${call}"${nodeBin}" "${guardAbs.replace(/\\/g, "/")}" antigravity "Antigravity"`;
10150
- }
10151
- function installAntigravityGuard(p, guardAbs) {
10152
- mkdirSync8(p.antigravityDir, { recursive: true });
10153
- let existing = {};
10154
- if (existsSync4(p.antigravityHooksPath)) {
10155
- const raw = readFileSync9(p.antigravityHooksPath, "utf-8");
10156
- if (!existsSync4(p.antigravityBackupPath)) writeFileSync9(p.antigravityBackupPath, raw);
10157
- try {
10158
- existing = JSON.parse(raw);
10159
- } catch {
10160
- existing = {};
10161
- }
10162
- }
10163
- const merged = {
10164
- ...existing,
10165
- [ANTIGRAVITY_GROUP]: {
10166
- PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: antigravityHookCommand(guardAbs) }] }]
10167
- }
10168
- };
10169
- writeFileSync9(p.antigravityHooksPath, JSON.stringify(merged, null, 2) + "\n");
10170
- }
10171
- function removeAntigravityGuard(p) {
10172
- if (!existsSync4(p.antigravityHooksPath)) return;
10173
- try {
10174
- const s = JSON.parse(readFileSync9(p.antigravityHooksPath, "utf-8"));
10175
- if (!(ANTIGRAVITY_GROUP in s)) return;
10176
- delete s[ANTIGRAVITY_GROUP];
10177
- writeFileSync9(p.antigravityHooksPath, JSON.stringify(s, null, 2) + "\n");
10178
- } catch {
10179
- }
10180
- }
10181
- function installGlobalQuiet() {
10182
- try {
10183
- const p = globalPaths();
10184
- let apiKey = process.env["SOLONGATE_API_KEY"] || "";
10185
- let apiUrl = process.env["SOLONGATE_API_URL"] || "https://api.solongate.com";
10186
- try {
10187
- const cfg = JSON.parse(readFileSync9(p.configPath, "utf-8"));
10188
- if (cfg && typeof cfg.apiKey === "string") apiKey = apiKey || cfg.apiKey;
10189
- if (cfg && typeof cfg.apiUrl === "string") apiUrl = cfg.apiUrl;
10190
- } catch {
10191
- }
10192
- if (!apiKey) {
10193
- const acc = firstAccountCredential();
10194
- if (acc.apiKey) {
10195
- apiKey = acc.apiKey;
10196
- if (acc.apiUrl) apiUrl = acc.apiUrl;
10197
- }
10198
- }
10199
- if (!apiKey) return { ok: false, message: "no login on this device \u2014 add an account first (Accounts \u2192 + add)" };
10200
- mkdirSync8(p.hooksDir, { recursive: true });
10201
- mkdirSync8(p.claudeDir, { recursive: true });
10202
- unlockProtected();
10203
- writeFileSync9(join11(p.hooksDir, "guard.mjs"), readGuard());
10204
- writeFileSync9(join11(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
10205
- writeFileSync9(join11(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
10206
- writeFileSync9(join11(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
10207
- writeFileSync9(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
10208
- let existing = {};
10209
- if (existsSync4(p.settingsPath)) {
10210
- const raw = readFileSync9(p.settingsPath, "utf-8");
10211
- if (!existsSync4(p.backupPath)) writeFileSync9(p.backupPath, raw);
10212
- try {
10213
- existing = JSON.parse(raw);
10214
- } catch {
10215
- existing = {};
10216
- }
10217
- }
10218
- const nodeBin = process.execPath.replace(/\\/g, "/");
10219
- const call = process.platform === "win32" ? "& " : "";
10220
- const hookCmd = (script) => `${call}"${nodeBin}" "${join11(p.hooksDir, script).replace(/\\/g, "/")}" claude-code "Claude Code"`;
10221
- const merged = {
10222
- ...existing,
10223
- hooks: {
10224
- PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("guard.mjs") }] }],
10225
- PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("audit.mjs") }] }],
10226
- Stop: [{ matcher: "", hooks: [{ type: "command", command: hookCmd("stop.mjs") }] }]
10227
- }
10228
- };
10229
- writeFileSync9(p.settingsPath, JSON.stringify(merged, null, 2) + "\n");
10230
- try {
10231
- installAntigravityGuard(p, join11(p.hooksDir, "guard.mjs").replace(/\\/g, "/"));
10232
- } catch {
10233
- }
10234
- if (process.env["SOLONGATE_OS_LOCK"] === "1") lockProtected();
10235
- return { ok: true, message: "guard installed (open a new session)" };
10236
- } catch (e) {
10237
- return { ok: false, message: e instanceof Error ? e.message : String(e) };
10238
- }
10239
- }
10240
- function installedGuardVersion() {
10241
- try {
10242
- const p = globalPaths();
10243
- const s = readFileSync9(join11(p.hooksDir, "guard.mjs"), "utf-8");
10244
- const m = s.match(/HOOK_VERSION\s*=\s*(\d+)/);
10245
- return m ? parseInt(m[1], 10) : null;
10246
- } catch {
10247
- return null;
10248
- }
10249
- }
10250
- function guardHookOutdated() {
10251
- try {
10252
- const p = globalPaths();
10253
- return readFileSync9(join11(p.hooksDir, "guard.mjs"), "utf-8") !== readGuard();
10254
- } catch {
10255
- return false;
10256
- }
10257
- }
10258
- function isGuardInstalled() {
10259
- try {
10260
- const p = globalPaths();
10261
- if (!existsSync4(p.settingsPath)) return false;
10262
- const s = JSON.parse(readFileSync9(p.settingsPath, "utf-8"));
10263
- return !!s.hooks && JSON.stringify(s.hooks).includes(".solongate");
10264
- } catch {
10265
- return false;
10266
- }
10267
- }
10268
- function uninstallGlobalQuiet() {
10269
- try {
10270
- const p = globalPaths();
10271
- unlockProtected();
10272
- removeClaudeShim();
10273
- try {
10274
- removeAntigravityGuard(p);
10275
- } catch {
10276
- }
10277
- if (!existsSync4(p.settingsPath)) return { ok: true, message: "guard removed (open a new session)" };
10278
- const s = JSON.parse(readFileSync9(p.settingsPath, "utf-8"));
10279
- delete s.hooks;
10280
- writeFileSync9(p.settingsPath, JSON.stringify(s, null, 2) + "\n");
10281
- return { ok: true, message: "guard removed (open a new session)" };
10282
- } catch (e) {
10283
- return { ok: false, message: e instanceof Error ? e.message : String(e) };
10284
- }
10285
- }
10286
- function escapeRe(s) {
10287
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10288
- }
10289
- function shimTargets() {
10290
- if (process.platform === "win32") {
10291
- try {
10292
- const prof = execFileSync2("powershell", ["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"], { encoding: "utf-8" }).trim();
10293
- return prof ? [prof] : [];
10294
- } catch {
10295
- return [];
10296
- }
10297
- }
10298
- return [".bashrc", ".zshrc", ".profile"].map((f) => join11(homedir9(), f)).filter((f) => existsSync4(f));
10299
- }
10300
- function writeShimBlock(file, block2) {
10301
- const re = new RegExp(escapeRe(SHIM_BEGIN) + "[\\s\\S]*?" + escapeRe(SHIM_END) + "\\r?\\n?", "g");
10302
- let content = existsSync4(file) ? readFileSync9(file, "utf-8") : "";
10303
- content = content.replace(re, "");
10304
- if (block2) {
10305
- if (content.length && !content.endsWith("\n")) content += "\n";
10306
- content += block2 + "\n";
10282
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2502 agent " }),
10283
+ /* @__PURE__ */ jsx7(Text7, { children: e.agent ?? "\u2014" }),
10284
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2502" })
10285
+ ] }),
10286
+ /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10287
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: "\u2502 session " }),
10288
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: e.session ?? "\u2014" }),
10289
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2502 rule " }),
10290
+ /* @__PURE__ */ jsx7(Text7, { color: e.rule && e.decision !== "ALLOW" ? theme.bad : theme.dim, children: e.rule ?? "\u2014" }),
10291
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \u2502" })
10292
+ ] }),
10293
+ /* @__PURE__ */ jsx7(PaneTitle, { label: "FULL CONTENT", extra: `${contentLines.length} lines${maxScroll ? ` \xB7 \u25BC${maxScroll - off} more \xB7 \u2191\u2193 scroll` : ""} \xB7 space copy \xB7 \u2190 back`, width: innerW }),
10294
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", height: bodyRows, overflow: "hidden", children: win.map((l, i) => /* @__PURE__ */ jsx7(Text7, { wrap: "truncate", children: l || " " }, off + i)) }),
10295
+ /* @__PURE__ */ jsxs7(Text7, { wrap: "truncate", children: [
10296
+ /* @__PURE__ */ jsx7(Text7, { backgroundColor: BG2, color: "white", bold: true, children: " ENTRY " }),
10297
+ /* @__PURE__ */ jsx7(Text7, { backgroundColor: "#0b1530", color: "white", children: ` ${e.id} ` }),
10298
+ /* @__PURE__ */ jsx7(Text7, { backgroundColor: BG2, color: "white", children: " \u2191\u2193 scroll \xB7 space copy \xB7 ? all keys \xB7 \u2190 back \xB7 esc menu " })
10299
+ ] })
10300
+ ] });
10307
10301
  }
10308
- mkdirSync8(dirname3(file), { recursive: true });
10309
- writeFileSync9(file, content);
10310
- }
10311
- function removeClaudeShim() {
10312
- for (const file of shimTargets()) {
10313
- try {
10314
- writeShimBlock(file, null);
10315
- } catch {
10316
- }
10302
+ const chip = (label, val, on) => /* @__PURE__ */ jsxs7(Text7, { children: [
10303
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
10304
+ label,
10305
+ ":"
10306
+ ] }),
10307
+ /* @__PURE__ */ jsx7(Text7, { color: on ? theme.accentBright : theme.dim, children: val }),
10308
+ /* @__PURE__ */ jsx7(Text7, { children: " " })
10309
+ ] });
10310
+ if (view === "sessions") {
10311
+ const headerRows2 = 5 + (editing ? 1 : 0);
10312
+ const listRows2 = Math.max(4, rows - headerRows2);
10313
+ const selC = Math.min(sessSel, Math.max(0, sessionsFiltered.length - 1));
10314
+ const start2 = Math.min(Math.max(0, selC - Math.floor((listRows2 - 1) / 2)), Math.max(0, sessionsFiltered.length - listRows2));
10315
+ const win = sessionsFiltered.slice(start2, start2 + listRows2);
10316
+ return /* @__PURE__ */ jsx7(DataView, { loading: sessLoading && !frozen, error: source === "cloud" ? agentsQ.error : localQ.error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
10317
+ strip,
10318
+ copyBanner,
10319
+ /* @__PURE__ */ jsxs7(Box7, { children: [
10320
+ srcChip,
10321
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accentBright, bold: true, children: "SESSIONS" }),
10322
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \xB7 logs (v) " }),
10323
+ chip("status", SESS_STATUS[si] ?? "all", si !== 0),
10324
+ chip("search", sessSearch || "\xB7", !!sessSearch)
10325
+ ] }),
10326
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, wrap: "truncate", children: focused ? "\u2191\u2193 select \xB7 enter \u2192 session logs \xB7 v logs \xB7 ^R refresh \xB7 ? all keys" : "press \u2192 to browse" }),
10327
+ editing === "sess-search" ? /* @__PURE__ */ jsxs7(Box7, { children: [
10328
+ /* @__PURE__ */ jsx7(Text7, { color: theme.warn, children: "search: " }),
10329
+ /* @__PURE__ */ jsx7(
10330
+ TextInput5,
10331
+ {
10332
+ value: sessSearch,
10333
+ onChange: (v) => {
10334
+ setSessSearch(v);
10335
+ setSessSel(0);
10336
+ },
10337
+ onSubmit: () => setEditing(null)
10338
+ }
10339
+ )
10340
+ ] }) : null,
10341
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, wrap: "truncate", children: `${sessionsFiltered.length} sessions \xB7 ${selC + 1}/${sessionsFiltered.length}${start2 ? ` \xB7 \u25B2${start2}` : ""}${start2 + listRows2 < sessionsFiltered.length ? ` \xB7 \u25BC${sessionsFiltered.length - start2 - listRows2}` : ""}` }),
10342
+ /* @__PURE__ */ jsx7(
10343
+ Table,
10344
+ {
10345
+ columns: [
10346
+ { header: "", width: 2 },
10347
+ { header: "STATUS", width: 8 },
10348
+ { header: "AGENT", width: 18 },
10349
+ { header: "SESSION", width: 10 },
10350
+ { header: "CALLS", width: 6 },
10351
+ { header: "DENY", width: 5 },
10352
+ { header: "DLP", width: 4 },
10353
+ { header: "TRUST", width: 5 },
10354
+ { header: "LAST", width: 6 }
10355
+ ],
10356
+ rows: win.map((r, i) => {
10357
+ const st = STATUS_DOT[r.status];
10358
+ const isSel = start2 + i === selC && focused;
10359
+ return [
10360
+ { value: isSel ? "\u25B8" : "", color: theme.accentBright },
10361
+ { value: `${st.ch} ${r.status}`, color: st.color },
10362
+ { value: truncate2(r.agent, 18), color: theme.accent, bold: isSel },
10363
+ { value: r.id.slice(0, 8), dim: true },
10364
+ { value: String(r.calls) },
10365
+ { value: String(r.denies), color: r.denies ? theme.bad : void 0, dim: !r.denies },
10366
+ { value: String(r.dlp), color: r.dlp ? theme.bad : void 0, dim: !r.dlp },
10367
+ { value: r.trust != null ? String(r.trust) : "\u2014", dim: true },
10368
+ { value: ago(r.lastAt), dim: true }
10369
+ ];
10370
+ })
10371
+ }
10372
+ ),
10373
+ sessionsFiltered.length === 0 ? /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
10374
+ "(no sessions",
10375
+ source === "local" ? " in the local file" : "",
10376
+ ")"
10377
+ ] }) : null
10378
+ ] }) });
10317
10379
  }
10380
+ const headerRows = 6 + (editing && editing !== "sess-search" ? 1 : 0) + (msg ? 1 : 0) + (frozen ? 1 : 0);
10381
+ const listRows = Math.max(4, rows - headerRows);
10382
+ const selClamped = Math.min(sel, Math.max(0, pageRows.length - 1));
10383
+ const maxStart = Math.max(0, pageRows.length - listRows);
10384
+ const start = Math.min(Math.max(0, selClamped - Math.floor((listRows - 1) / 2)), maxStart);
10385
+ const windowed = pageRows.slice(start, start + listRows);
10386
+ return /* @__PURE__ */ jsx7(DataView, { loading: logsLoading && !frozen, error: source === "cloud" ? cloudQ.error : localQ.error, children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
10387
+ strip,
10388
+ copyBanner,
10389
+ /* @__PURE__ */ jsxs7(Box7, { children: [
10390
+ srcChip,
10391
+ /* @__PURE__ */ jsx7(Text7, { color: theme.accentBright, bold: true, children: "LOGS" }),
10392
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, children: " \xB7 sessions (v) " }),
10393
+ chip("dec", DECISIONS[di] ?? "all", di !== 0),
10394
+ chip("sig", SIGNALS[gi] ?? "all", gi !== 0),
10395
+ chip("tool", tool || "\xB7", !!tool),
10396
+ chip("agent", agent || "\xB7", !!agent),
10397
+ chip("search", search || "\xB7", !!search),
10398
+ sessFilter ? chip("sess", sessFilter.slice(0, 8), true) : null
10399
+ ] }),
10400
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, wrap: "truncate", children: focused ? "\u2191\u2193 select \xB7 enter full entry \xB7 \u2190\u2192 page \xB7 v sessions \xB7 ^R refresh \xB7 ? all keys" : "press \u2192 to browse" }),
10401
+ msg ? /* @__PURE__ */ jsx7(Text7, { color: msg.level === "bad" ? theme.bad : theme.ok, children: truncate2(msg.text, cols) }) : null,
10402
+ editing && editing !== "sess-search" ? /* @__PURE__ */ jsxs7(Box7, { children: [
10403
+ /* @__PURE__ */ jsxs7(Text7, { color: theme.warn, children: [
10404
+ editing,
10405
+ ": "
10406
+ ] }),
10407
+ /* @__PURE__ */ jsx7(
10408
+ TextInput5,
10409
+ {
10410
+ value: editing === "tool" ? tool : editing === "agent" ? agent : search,
10411
+ onChange: (v) => {
10412
+ (editing === "tool" ? setTool : editing === "agent" ? setAgent : setSearch)(v);
10413
+ setPage(0);
10414
+ toTop();
10415
+ },
10416
+ onSubmit: () => setEditing(null)
10417
+ }
10418
+ )
10419
+ ] }) : null,
10420
+ /* @__PURE__ */ jsx7(Text7, { color: theme.dim, wrap: "truncate", children: `${total} matched \xB7 page ${Math.min(page + 1, pages)}/${pages} \xB7 ${PAGE}/page \xB7 ${pageRows.length ? selClamped + 1 : 0}/${pageRows.length}${start ? ` \xB7 \u25B2${start} newer` : ""}${start + listRows < pageRows.length ? ` \xB7 \u25BC${pageRows.length - start - listRows} older` : ""}` }),
10421
+ /* @__PURE__ */ jsx7(Box7, { flexDirection: "column", overflow: "hidden", children: windowed.map((e, i) => /* @__PURE__ */ jsx7(StreamLine, { e: toStream(e), loc: source === "local", selected: start + i === selClamped && focused, date: true }, e.id)) }),
10422
+ pageRows.length === 0 ? /* @__PURE__ */ jsxs7(Text7, { color: theme.dim, children: [
10423
+ "(no entries",
10424
+ source === "local" ? " in the local file" : "",
10425
+ " \u2014 c clears filters)"
10426
+ ] }) : null
10427
+ ] }) });
10318
10428
  }
10319
- var __dirname, HOOKS_DIR, ANTIGRAVITY_GROUP, SHIM_BEGIN, SHIM_END;
10320
- var init_global_install = __esm({
10321
- "src/global-install.ts"() {
10429
+ var DECISIONS, SIGNALS, SESS_STATUS, PAGE, LOCAL_MAX_BYTES, BG2, toStream, cloudRow, AUDIT_HELP, sessStatus2, STATUS_DOT;
10430
+ var init_Audit = __esm({
10431
+ "src/tui/panels/Audit.tsx"() {
10322
10432
  "use strict";
10323
- __dirname = dirname3(fileURLToPath3(import.meta.url));
10324
- HOOKS_DIR = resolve4(__dirname, "..", "hooks");
10325
- ANTIGRAVITY_GROUP = "solongate-guard";
10326
- SHIM_BEGIN = "# >>> SolonGate shield (auto secret redaction) >>>";
10327
- SHIM_END = "# <<< SolonGate shield <<<";
10433
+ init_api_client();
10434
+ init_components();
10435
+ init_hooks();
10436
+ init_local_log();
10437
+ init_theme();
10438
+ DECISIONS = [void 0, "DENY", "ALLOW"];
10439
+ SIGNALS = [void 0, "dlp", "ratelimit"];
10440
+ SESS_STATUS = [void 0, "active", "idle", "ended"];
10441
+ PAGE = 500;
10442
+ LOCAL_MAX_BYTES = 16 * 1024 * 1024;
10443
+ BG2 = "#12234f";
10444
+ toStream = (e) => ({
10445
+ at: e.at,
10446
+ tool: e.tool,
10447
+ decision: e.decision,
10448
+ permission: (e.permission ?? "").slice(0, 4),
10449
+ detail: (e.args ?? e.reason ?? "").replace(/\s+/g, " "),
10450
+ dlp: e.dlp.length > 0,
10451
+ burst: e.burst,
10452
+ agent: e.agent,
10453
+ evalMs: e.evalMs,
10454
+ rule: e.rule
10455
+ });
10456
+ cloudRow = (e) => {
10457
+ const rs = reasonSignals(e.reason);
10458
+ return {
10459
+ id: e.id,
10460
+ at: Date.parse(e.created_at),
10461
+ tool: e.tool_name,
10462
+ decision: e.decision,
10463
+ permission: e.permission,
10464
+ trust: e.trust_level,
10465
+ agent: e.agent_name,
10466
+ session: e.session_id,
10467
+ reason: e.reason,
10468
+ rule: e.matched_rule_id,
10469
+ evalMs: e.evaluation_time_ms,
10470
+ dlp: e.dlp_matches?.length ? e.dlp_matches : rs.dlp,
10471
+ burst: !!e.rate_limit_burst || rs.burst,
10472
+ args: e.arguments_summary ? JSON.stringify(e.arguments_summary) : null
10473
+ };
10474
+ };
10475
+ AUDIT_HELP = [
10476
+ [
10477
+ "Logs",
10478
+ [
10479
+ ["\u2191\u2193 / PgUp PgDn", "select a row (window follows)"],
10480
+ ["enter", "open the FULL entry (reason + arguments)"],
10481
+ ["\u2190 \u2192", "previous / next page (500 per page, jumps to top)"],
10482
+ ["f", "decision filter: all \u2192 DENY \u2192 ALLOW"],
10483
+ ["g", "signal filter: all \u2192 dlp \u2192 ratelimit"],
10484
+ ["t / n", "tool / agent filter (type, enter done)"],
10485
+ ["/", "free-text search"],
10486
+ ["x", "delete ONLY the selected entry (press x twice)"],
10487
+ ["X", "delete ALL matched logs of the source (press X twice)"],
10488
+ ["e", "export this page \u2192 ~/.solongate/audit-export-<src>.jsonl"],
10489
+ ["E", "export ALL matched rows (cloud: up to 10k)"],
10490
+ ["c", "clear every filter (incl. session)"]
10491
+ ]
10492
+ ],
10493
+ [
10494
+ "Sessions",
10495
+ [
10496
+ ["\u2191\u2193", "select a session"],
10497
+ ["enter", "open that session's logs"],
10498
+ ["f", "status filter: all \u2192 active \u2192 idle \u2192 ended"],
10499
+ ["/", "search agent / session id"],
10500
+ ["c", "clear session filters"]
10501
+ ]
10502
+ ],
10503
+ [
10504
+ "Anywhere in Audit",
10505
+ [
10506
+ ["v", "switch logs \u2194 sessions"],
10507
+ ["s", "switch source cloud \u2194 local file"],
10508
+ ["space", "copy mode: freeze screen for mouse selection"],
10509
+ ["?", "this help \xB7 any key closes"],
10510
+ ["esc", "back to the menu"]
10511
+ ]
10512
+ ],
10513
+ ["Entry (full content)", [["\u2191\u2193 / PgUp PgDn", "scroll the reason + arguments"], ["space", "copy mode (freeze, then select)"], ["\u2190 / esc", "back to the list"]]]
10514
+ ];
10515
+ sessStatus2 = (lastAt) => Date.now() - lastAt < 6e4 ? "active" : Date.now() - lastAt < 3e5 ? "idle" : "ended";
10516
+ STATUS_DOT = {
10517
+ active: { ch: "\u25CF", color: theme.ok },
10518
+ idle: { ch: "\u25D0", color: theme.warn },
10519
+ ended: { ch: "\u25CB", color: theme.dim }
10520
+ };
10328
10521
  }
10329
10522
  });
10330
10523
 
@@ -11394,6 +11587,22 @@ async function run(argv) {
11394
11587
  printRules(p.rules);
11395
11588
  return 0;
11396
11589
  }
11590
+ case "create": {
11591
+ const name = positionals.slice(1).join(" ").trim();
11592
+ if (!name) return err(" Usage: policy create <name>"), 1;
11593
+ const res = await api.policies.create({ id: `policy-${Date.now()}`, name, rules: [], mode: "denylist" });
11594
+ if (json) return printJson(res), 0;
11595
+ err(green(` \u2713 Created "${name}"`) + dim(` (${res.id})`));
11596
+ return 0;
11597
+ }
11598
+ case "delete": {
11599
+ const id = positionals[1];
11600
+ if (!id) return err(" Usage: policy delete <id>"), 1;
11601
+ const res = await api.policies.remove(id);
11602
+ if (json) return printJson(res), 0;
11603
+ err(green(` \u2713 Deleted ${res.policy_id}`));
11604
+ return 0;
11605
+ }
11397
11606
  case "allow":
11398
11607
  case "deny": {
11399
11608
  const effect = sub === "allow" ? "ALLOW" : "DENY";
@@ -11508,6 +11717,8 @@ var init_policy = __esm({
11508
11717
  init_format();
11509
11718
  USAGE = usage("solongate policy", "manage cloud policies", [
11510
11719
  ["policy list", "list all policies"],
11720
+ ["policy create <name>", "create a new empty policy"],
11721
+ ["policy delete <id>", "delete a policy"],
11511
11722
  ["policy show <id>", "show one policy (rules, mode)"],
11512
11723
  ["policy allow <id> [--command|--path|--url <val>]", "append an ALLOW rule"],
11513
11724
  ["policy deny <id> [--command|--path|--url <val>]", "append a DENY rule"],
@@ -15844,7 +16055,7 @@ ${msg.content.text}`;
15844
16055
 
15845
16056
  // src/index.ts
15846
16057
  init_cli_utils();
15847
- var CLI_SUBCOMMANDS = /* @__PURE__ */ new Set(["logs-server", "local-logs", "policy", "ratelimit", "dlp", "stats", "audit", "sessions", "session", "doctor", "watch", "alerts", "webhooks", "dataroom"]);
16058
+ var CLI_SUBCOMMANDS = /* @__PURE__ */ new Set(["update", "logs-server", "local-logs", "policy", "ratelimit", "dlp", "stats", "audit", "sessions", "session", "doctor", "watch", "alerts", "webhooks", "dataroom"]);
15848
16059
  var CLI_INFO_ARGS = /* @__PURE__ */ new Set(["login", "help", "--help", "-h", "--version", "-v", "version"]);
15849
16060
  var IS_HUMAN_CLI = process.argv.length <= 2 || CLI_SUBCOMMANDS.has(process.argv[2] ?? "") || CLI_INFO_ARGS.has(process.argv[2] ?? "");
15850
16061
  if (!IS_HUMAN_CLI) {
@@ -15905,6 +16116,7 @@ function printHelp() {
15905
16116
  console.log(` ${c.dim}Usage:${c.reset} ${c.cyan}solongate${c.reset} ${c.dim}[command]${c.reset} ${c.dim}(no command opens the dataroom: all of this in a terminal UI)${c.reset}`);
15906
16117
  head("Setup & status");
15907
16118
  cmd("solongate", "open the dataroom UI (login, policies, audit, settings)");
16119
+ cmd("update", "update SolonGate to the newest version and refresh the guard");
15908
16120
  cmd("doctor", "health check: login, policy, guard, local logs");
15909
16121
  cmd("doctor --json", "the same health check as machine-readable JSON");
15910
16122
  cmd("logs-server start", "start the local audit-log service for the dashboard (background)");
@@ -15912,6 +16124,8 @@ function printHelp() {
15912
16124
  cmd("logs-server status", "show the local audit-log service status");
15913
16125
  head("Policies");
15914
16126
  cmd("policy list", "list all policies");
16127
+ cmd("policy create <name>", "create a new empty policy");
16128
+ cmd("policy delete <id>", "delete a policy");
15915
16129
  cmd("policy show <id>", "show one policy (rules, mode)");
15916
16130
  cmd("policy allow <id> [--command|--path|--url <val>]", "add an ALLOW rule");
15917
16131
  cmd("policy deny <id> [--command|--path|--url <val>]", "add a DENY rule");
@@ -15998,6 +16212,10 @@ async function main() {
15998
16212
  console.log("");
15999
16213
  return;
16000
16214
  }
16215
+ if (subcommand === "update") {
16216
+ const { runUpdateCommand: runUpdateCommand2 } = await Promise.resolve().then(() => (init_self_update(), self_update_exports));
16217
+ process.exit(await runUpdateCommand2());
16218
+ }
16001
16219
  if (subcommand === "logs-server" || subcommand === "local-logs") {
16002
16220
  const { runLogsServer: runLogsServer2 } = await Promise.resolve().then(() => (init_logs_server(), logs_server_exports));
16003
16221
  await runLogsServer2();