@node9/proxy 1.0.15 → 1.0.17

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/README.md CHANGED
@@ -44,6 +44,10 @@ Node9 initiates a **Concurrent Race** across all enabled channels. The first cha
44
44
 
45
45
  Node9 records every tool call your AI agent makes in real-time — no polling, no log files, no refresh. Two ways to watch:
46
46
 
47
+ <p align="center">
48
+ <img src="docs/flight-recorder.jpeg" width="100%">
49
+ </p>
50
+
47
51
  **Browser Dashboard** (`node9 daemon start` → `localhost:7391`)
48
52
 
49
53
  A live 3-column dashboard. The left column streams every tool call as it happens, updating in-place from `● PENDING` to `✓ ALLOW` or `✗ BLOCK`. The center handles pending approvals. The right sidebar controls shields and persistent decisions — all without ever causing a browser scrollbar.
@@ -284,29 +288,35 @@ Use `node9 explain <tool> <args>` to dry-run any tool call and see exactly which
284
288
 
285
289
  ```json
286
290
  {
291
+ "version": "1.0",
287
292
  "settings": {
288
- "mode": "standard",
293
+ "mode": "audit",
289
294
  "enableUndo": true,
295
+ "flightRecorder": true,
290
296
  "approvalTimeoutMs": 30000,
291
297
  "approvers": {
292
298
  "native": true,
293
299
  "browser": true,
294
- "cloud": true,
300
+ "cloud": false,
295
301
  "terminal": true
296
302
  }
297
303
  }
298
304
  }
299
305
  ```
300
306
 
301
- | Key | Default | Description |
302
- | :------------------- | :----------- | :----------------------------------------------------------- |
303
- | `mode` | `"standard"` | `standard` \| `strict` \| `audit` |
304
- | `enableUndo` | `true` | Take git snapshots before every AI file edit |
305
- | `approvalTimeoutMs` | `0` | Auto-deny after N ms if no human responds (0 = wait forever) |
306
- | `approvers.native` | `true` | OS-native popup |
307
- | `approvers.browser` | `true` | Browser dashboard (`node9 daemon`) |
308
- | `approvers.cloud` | `true` | Slack / SaaS approval |
309
- | `approvers.terminal` | `true` | `[Y/n]` prompt in terminal |
307
+ | Key | Default | Description |
308
+ | :------------------- | :-------- | :------------------------------------------------------------------------------ |
309
+ | `mode` | `"audit"` | `audit` (log-only) \| `standard` (approve/block) \| `strict` (deny by default) |
310
+ | `enableUndo` | `true` | Take git snapshots before every AI file edit |
311
+ | `flightRecorder` | `true` | Record tool call activity to the flight recorder ring buffer for the browser UI |
312
+ | `approvalTimeoutMs` | `30000` | Auto-deny after N ms if no human responds (`0` = wait forever) |
313
+ | `approvers.native` | `true` | OS-native popup |
314
+ | `approvers.browser` | `true` | Browser dashboard (`node9 daemon`) |
315
+ | `approvers.cloud` | `false` | Slack / SaaS approval — requires `node9 login`; opt-in only |
316
+ | `approvers.terminal` | `true` | `[Y/n]` prompt in terminal |
317
+
318
+ > **Tip — choosing a mode:**
319
+ > Start with the default `audit` to observe what your agent does without blocking anything. Once you understand its behaviour, switch to `standard` (blocks dangerous commands with human approval) or `strict` (denies anything not explicitly allowed) in your `~/.node9/config.json` or project `node9.config.json`.
310
320
 
311
321
  ---
312
322
 
@@ -369,7 +379,7 @@ Verdict: BLOCK (dangerous word: rm -rf)
369
379
  ## 🔧 Troubleshooting
370
380
 
371
381
  **`node9 check` exits immediately / Claude is never blocked**
372
- Node9 fails open by design to prevent breaking your agent. Check debug logs: `NODE9_DEBUG=1 claude`.
382
+ Node9 fails open by design to prevent breaking your agent. Check debug logs: `NODE9_DEBUG=1 claude`. Also verify you are in `standard` or `strict` mode — the default `audit` mode approves everything and only logs.
373
383
 
374
384
  **Terminal prompt never appears during Claude/Gemini sessions**
375
385
  Interactive agents run hooks in a "Headless" subprocess. You **must** enable `native: true` or `browser: true` in your config to see approval prompts.
@@ -377,6 +387,9 @@ Interactive agents run hooks in a "Headless" subprocess. You **must** enable `na
377
387
  **"Blocked by Organization (SaaS)"**
378
388
  A corporate policy has locked this action. You must click the "Approve" button in your company's Slack channel to proceed.
379
389
 
390
+ **`node9 tail --history` says "Daemon failed to start" even though the daemon is running**
391
+ This can happen when the daemon's PID file (`~/.node9/daemon.pid`) is missing — for example after a crash or a botched restart left a daemon running without a PID file. Node9 now detects this automatically: it performs an HTTP health probe and a live port check before deciding the daemon is gone. If you hit this on an older version, run `node9 daemon stop` then `node9 daemon -b` to create a clean PID file.
392
+
380
393
  ---
381
394
 
382
395
  ## 🗺️ Roadmap
package/dist/cli.js CHANGED
@@ -418,7 +418,13 @@ var init_config_schema = __esm({
418
418
  ),
419
419
  value: import_zod.z.string().optional(),
420
420
  flags: import_zod.z.string().optional()
421
- });
421
+ }).refine(
422
+ (c) => {
423
+ if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
424
+ return true;
425
+ },
426
+ { message: "matchesGlob and notMatchesGlob conditions require a value field" }
427
+ );
422
428
  SmartRuleSchema = import_zod.z.object({
423
429
  name: import_zod.z.string().optional(),
424
430
  tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
@@ -437,6 +443,7 @@ var init_config_schema = __esm({
437
443
  enableUndo: import_zod.z.boolean().optional(),
438
444
  enableHookLogDebug: import_zod.z.boolean().optional(),
439
445
  approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
446
+ flightRecorder: import_zod.z.boolean().optional(),
440
447
  approvers: import_zod.z.object({
441
448
  native: import_zod.z.boolean().optional(),
442
449
  browser: import_zod.z.boolean().optional(),
@@ -927,7 +934,7 @@ function evaluateSmartConditions(args, rule) {
927
934
  case "matchesGlob":
928
935
  return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
929
936
  case "notMatchesGlob":
930
- return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : true;
937
+ return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
931
938
  default:
932
939
  return false;
933
940
  }
@@ -1040,7 +1047,7 @@ function getGlobalSettings() {
1040
1047
  const parsed = JSON.parse(import_fs2.default.readFileSync(globalConfigPath, "utf-8"));
1041
1048
  const settings = parsed.settings || {};
1042
1049
  return {
1043
- mode: settings.mode || "standard",
1050
+ mode: settings.mode || "audit",
1044
1051
  autoStartDaemon: settings.autoStartDaemon !== false,
1045
1052
  slackEnabled: settings.slackEnabled !== false,
1046
1053
  enableTrustSessions: settings.enableTrustSessions === true,
@@ -1050,7 +1057,7 @@ function getGlobalSettings() {
1050
1057
  } catch {
1051
1058
  }
1052
1059
  return {
1053
- mode: "standard",
1060
+ mode: "audit",
1054
1061
  autoStartDaemon: true,
1055
1062
  slackEnabled: true,
1056
1063
  enableTrustSessions: false,
@@ -1437,13 +1444,23 @@ function isIgnoredTool(toolName) {
1437
1444
  return matchesPattern(toolName, config.policy.ignoredTools);
1438
1445
  }
1439
1446
  function isDaemonRunning() {
1447
+ const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1448
+ if (import_fs2.default.existsSync(pidFile)) {
1449
+ try {
1450
+ const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1451
+ if (port !== DAEMON_PORT) return false;
1452
+ process.kill(pid, 0);
1453
+ return true;
1454
+ } catch {
1455
+ return false;
1456
+ }
1457
+ }
1440
1458
  try {
1441
- const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1442
- if (!import_fs2.default.existsSync(pidFile)) return false;
1443
- const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1444
- if (port !== DAEMON_PORT) return false;
1445
- process.kill(pid, 0);
1446
- return true;
1459
+ const r = (0, import_child_process2.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
1460
+ encoding: "utf8",
1461
+ timeout: 500
1462
+ });
1463
+ return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
1447
1464
  } catch {
1448
1465
  return false;
1449
1466
  }
@@ -2245,7 +2262,7 @@ async function resolveNode9SaaS(requestId, creds, approved) {
2245
2262
  } catch {
2246
2263
  }
2247
2264
  }
2248
- var import_chalk2, import_prompts, import_fs2, import_path4, import_os2, import_net, import_crypto2, import_picomatch, import_sh_syntax, PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
2265
+ var import_chalk2, import_prompts, import_fs2, import_path4, import_os2, import_net, import_crypto2, import_child_process2, import_picomatch, import_sh_syntax, PAUSED_FILE, TRUST_FILE, LOCAL_AUDIT_LOG, HOOK_DEBUG_LOG, SQL_DML_KEYWORDS, DANGEROUS_WORDS, DEFAULT_CONFIG, ADVISORY_SMART_RULES, cachedConfig, DAEMON_PORT, DAEMON_HOST, ACTIVITY_SOCKET_PATH;
2249
2266
  var init_core = __esm({
2250
2267
  "src/core.ts"() {
2251
2268
  "use strict";
@@ -2256,6 +2273,7 @@ var init_core = __esm({
2256
2273
  import_os2 = __toESM(require("os"));
2257
2274
  import_net = __toESM(require("net"));
2258
2275
  import_crypto2 = require("crypto");
2276
+ import_child_process2 = require("child_process");
2259
2277
  import_picomatch = __toESM(require("picomatch"));
2260
2278
  import_sh_syntax = require("sh-syntax");
2261
2279
  init_native();
@@ -2275,15 +2293,17 @@ var init_core = __esm({
2275
2293
  // permanently overwrites file contents (unrecoverable)
2276
2294
  ];
2277
2295
  DEFAULT_CONFIG = {
2296
+ version: "1.0",
2278
2297
  settings: {
2279
- mode: "standard",
2298
+ mode: "audit",
2280
2299
  autoStartDaemon: true,
2281
2300
  enableUndo: true,
2282
2301
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
2283
- enableHookLogDebug: false,
2284
- approvalTimeoutMs: 0,
2285
- // 0 = disabled; set e.g. 30000 for 30-second auto-deny
2286
- approvers: { native: true, browser: true, cloud: true, terminal: true }
2302
+ enableHookLogDebug: true,
2303
+ approvalTimeoutMs: 3e4,
2304
+ // 30-second auto-deny timeout
2305
+ flightRecorder: true,
2306
+ approvers: { native: true, browser: true, cloud: false, terminal: true }
2287
2307
  },
2288
2308
  policy: {
2289
2309
  sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
@@ -4056,7 +4076,7 @@ data: ${JSON.stringify(data)}
4056
4076
  function openBrowser(url) {
4057
4077
  try {
4058
4078
  const args = process.platform === "darwin" ? ["open", url] : process.platform === "win32" ? ["cmd", "/c", "start", "", url] : ["xdg-open", url];
4059
- (0, import_child_process2.spawn)(args[0], args.slice(1), { detached: true, stdio: "ignore" }).unref();
4079
+ (0, import_child_process3.spawn)(args[0], args.slice(1), { detached: true, stdio: "ignore" }).unref();
4060
4080
  } catch {
4061
4081
  }
4062
4082
  }
@@ -4437,6 +4457,11 @@ data: ${JSON.stringify(item.data)}
4437
4457
  res.writeHead(400).end();
4438
4458
  }
4439
4459
  }
4460
+ if (req.method === "POST" && pathname === "/events/clear") {
4461
+ activityRing.length = 0;
4462
+ res.writeHead(200, { "Content-Type": "application/json" });
4463
+ return res.end(JSON.stringify({ ok: true }));
4464
+ }
4440
4465
  if (req.method === "GET" && pathname === "/audit") {
4441
4466
  res.writeHead(200, { "Content-Type": "application/json" });
4442
4467
  return res.end(JSON.stringify(getAuditHistory()));
@@ -4492,6 +4517,35 @@ data: ${JSON.stringify(item.data)}
4492
4517
  server.listen(DAEMON_PORT2, DAEMON_HOST2);
4493
4518
  return;
4494
4519
  }
4520
+ fetch(`http://${DAEMON_HOST2}:${DAEMON_PORT2}/settings`, {
4521
+ signal: AbortSignal.timeout(1e3)
4522
+ }).then((res) => {
4523
+ if (res.ok) {
4524
+ try {
4525
+ const r = (0, import_child_process3.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT2}`], {
4526
+ encoding: "utf8",
4527
+ timeout: 1e3
4528
+ });
4529
+ const match = r.stdout?.match(/pid=(\d+)/);
4530
+ if (match) {
4531
+ const orphanPid = parseInt(match[1], 10);
4532
+ process.kill(orphanPid, 0);
4533
+ atomicWriteSync2(
4534
+ DAEMON_PID_FILE,
4535
+ JSON.stringify({ pid: orphanPid, port: DAEMON_PORT2, internalToken, autoStarted }),
4536
+ { mode: 384 }
4537
+ );
4538
+ }
4539
+ } catch {
4540
+ }
4541
+ process.exit(0);
4542
+ } else {
4543
+ server.listen(DAEMON_PORT2, DAEMON_HOST2);
4544
+ }
4545
+ }).catch(() => {
4546
+ server.listen(DAEMON_PORT2, DAEMON_HOST2);
4547
+ });
4548
+ return;
4495
4549
  }
4496
4550
  console.error(import_chalk4.default.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
4497
4551
  process.exit(1);
@@ -4571,17 +4625,28 @@ function stopDaemon() {
4571
4625
  }
4572
4626
  }
4573
4627
  function daemonStatus() {
4574
- if (!import_fs4.default.existsSync(DAEMON_PID_FILE))
4575
- return console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
4576
- try {
4577
- const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4578
- process.kill(pid, 0);
4579
- console.log(import_chalk4.default.green("Node9 daemon: running"));
4580
- } catch {
4581
- console.log(import_chalk4.default.yellow("Node9 daemon: not running (stale PID)"));
4628
+ if (import_fs4.default.existsSync(DAEMON_PID_FILE)) {
4629
+ try {
4630
+ const { pid } = JSON.parse(import_fs4.default.readFileSync(DAEMON_PID_FILE, "utf-8"));
4631
+ process.kill(pid, 0);
4632
+ console.log(import_chalk4.default.green("Node9 daemon: running"));
4633
+ return;
4634
+ } catch {
4635
+ console.log(import_chalk4.default.yellow("Node9 daemon: not running (stale PID)"));
4636
+ return;
4637
+ }
4638
+ }
4639
+ const r = (0, import_child_process3.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT2}`], {
4640
+ encoding: "utf8",
4641
+ timeout: 500
4642
+ });
4643
+ if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT2}`)) {
4644
+ console.log(import_chalk4.default.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
4645
+ } else {
4646
+ console.log(import_chalk4.default.yellow("Node9 daemon: not running"));
4582
4647
  }
4583
4648
  }
4584
- var import_http, import_net2, import_fs4, import_path6, import_os4, import_child_process2, import_crypto3, import_chalk4, ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
4649
+ var import_http, import_net2, import_fs4, import_path6, import_os4, import_child_process3, import_crypto3, import_chalk4, ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
4585
4650
  var init_daemon = __esm({
4586
4651
  "src/daemon/index.ts"() {
4587
4652
  "use strict";
@@ -4591,7 +4656,7 @@ var init_daemon = __esm({
4591
4656
  import_fs4 = __toESM(require("fs"));
4592
4657
  import_path6 = __toESM(require("path"));
4593
4658
  import_os4 = __toESM(require("os"));
4594
- import_child_process2 = require("child_process");
4659
+ import_child_process3 = require("child_process");
4595
4660
  import_crypto3 = require("crypto");
4596
4661
  import_chalk4 = __toESM(require("chalk"));
4597
4662
  init_core();
@@ -4672,8 +4737,15 @@ async function ensureDaemon() {
4672
4737
  } catch {
4673
4738
  }
4674
4739
  }
4740
+ try {
4741
+ const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
4742
+ signal: AbortSignal.timeout(500)
4743
+ });
4744
+ if (res.ok) return DAEMON_PORT2;
4745
+ } catch {
4746
+ }
4675
4747
  console.log(import_chalk5.default.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
4676
- const child = (0, import_child_process4.spawn)(process.execPath, [process.argv[1], "daemon"], {
4748
+ const child = (0, import_child_process5.spawn)(process.execPath, [process.argv[1], "daemon"], {
4677
4749
  detached: true,
4678
4750
  stdio: "ignore",
4679
4751
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
@@ -4681,15 +4753,11 @@ async function ensureDaemon() {
4681
4753
  child.unref();
4682
4754
  for (let i = 0; i < 20; i++) {
4683
4755
  await new Promise((r) => setTimeout(r, 250));
4684
- if (!import_fs6.default.existsSync(PID_FILE)) continue;
4685
4756
  try {
4686
4757
  const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
4687
4758
  signal: AbortSignal.timeout(500)
4688
4759
  });
4689
- if (res.ok) {
4690
- const { port } = JSON.parse(import_fs6.default.readFileSync(PID_FILE, "utf-8"));
4691
- return port;
4692
- }
4760
+ if (res.ok) return DAEMON_PORT2;
4693
4761
  } catch {
4694
4762
  }
4695
4763
  }
@@ -4698,11 +4766,26 @@ async function ensureDaemon() {
4698
4766
  }
4699
4767
  async function startTail(options = {}) {
4700
4768
  const port = await ensureDaemon();
4769
+ if (options.clear) {
4770
+ await new Promise((resolve) => {
4771
+ const req2 = import_http2.default.request(
4772
+ { method: "POST", hostname: "127.0.0.1", port, path: "/events/clear" },
4773
+ (res) => {
4774
+ res.resume();
4775
+ res.on("end", resolve);
4776
+ }
4777
+ );
4778
+ req2.on("error", resolve);
4779
+ req2.end();
4780
+ });
4781
+ }
4701
4782
  const connectionTime = Date.now();
4702
4783
  const pending2 = /* @__PURE__ */ new Map();
4703
4784
  console.log(import_chalk5.default.cyan.bold(`
4704
4785
  \u{1F6F0}\uFE0F Node9 tail `) + import_chalk5.default.dim(`\u2192 localhost:${port}`));
4705
- if (options.history) {
4786
+ if (options.clear) {
4787
+ console.log(import_chalk5.default.dim("History cleared. Showing live events. Press Ctrl+C to exit.\n"));
4788
+ } else if (options.history) {
4706
4789
  console.log(import_chalk5.default.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
4707
4790
  } else {
4708
4791
  console.log(
@@ -4783,7 +4866,7 @@ async function startTail(options = {}) {
4783
4866
  process.exit(1);
4784
4867
  });
4785
4868
  }
4786
- var import_http2, import_chalk5, import_fs6, import_os6, import_path8, import_readline, import_child_process4, PID_FILE, ICONS;
4869
+ var import_http2, import_chalk5, import_fs6, import_os6, import_path8, import_readline, import_child_process5, PID_FILE, ICONS;
4787
4870
  var init_tail = __esm({
4788
4871
  "src/tui/tail.ts"() {
4789
4872
  "use strict";
@@ -4793,7 +4876,7 @@ var init_tail = __esm({
4793
4876
  import_os6 = __toESM(require("os"));
4794
4877
  import_path8 = __toESM(require("path"));
4795
4878
  import_readline = __toESM(require("readline"));
4796
- import_child_process4 = require("child_process");
4879
+ import_child_process5 = require("child_process");
4797
4880
  init_daemon();
4798
4881
  PID_FILE = import_path8.default.join(import_os6.default.homedir(), ".node9", "daemon.pid");
4799
4882
  ICONS = {
@@ -5068,7 +5151,7 @@ async function setupCursor() {
5068
5151
 
5069
5152
  // src/cli.ts
5070
5153
  init_daemon();
5071
- var import_child_process5 = require("child_process");
5154
+ var import_child_process6 = require("child_process");
5072
5155
  var import_execa = require("execa");
5073
5156
  var import_execa2 = require("execa");
5074
5157
  var import_chalk6 = __toESM(require("chalk"));
@@ -5078,7 +5161,7 @@ var import_path9 = __toESM(require("path"));
5078
5161
  var import_os7 = __toESM(require("os"));
5079
5162
 
5080
5163
  // src/undo.ts
5081
- var import_child_process3 = require("child_process");
5164
+ var import_child_process4 = require("child_process");
5082
5165
  var import_fs5 = __toESM(require("fs"));
5083
5166
  var import_path7 = __toESM(require("path"));
5084
5167
  var import_os5 = __toESM(require("os"));
@@ -5115,12 +5198,12 @@ async function createShadowSnapshot(tool = "unknown", args = {}) {
5115
5198
  if (!import_fs5.default.existsSync(import_path7.default.join(cwd, ".git"))) return null;
5116
5199
  const tempIndex = import_path7.default.join(cwd, ".git", `node9_index_${Date.now()}`);
5117
5200
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
5118
- (0, import_child_process3.spawnSync)("git", ["add", "-A"], { env });
5119
- const treeRes = (0, import_child_process3.spawnSync)("git", ["write-tree"], { env });
5201
+ (0, import_child_process4.spawnSync)("git", ["add", "-A"], { env });
5202
+ const treeRes = (0, import_child_process4.spawnSync)("git", ["write-tree"], { env });
5120
5203
  const treeHash = treeRes.stdout.toString().trim();
5121
5204
  if (import_fs5.default.existsSync(tempIndex)) import_fs5.default.unlinkSync(tempIndex);
5122
5205
  if (!treeHash || treeRes.status !== 0) return null;
5123
- const commitRes = (0, import_child_process3.spawnSync)("git", [
5206
+ const commitRes = (0, import_child_process4.spawnSync)("git", [
5124
5207
  "commit-tree",
5125
5208
  treeHash,
5126
5209
  "-m",
@@ -5151,10 +5234,10 @@ function getSnapshotHistory() {
5151
5234
  }
5152
5235
  function computeUndoDiff(hash, cwd) {
5153
5236
  try {
5154
- const result = (0, import_child_process3.spawnSync)("git", ["diff", hash, "--stat", "--", "."], { cwd });
5237
+ const result = (0, import_child_process4.spawnSync)("git", ["diff", hash, "--stat", "--", "."], { cwd });
5155
5238
  const stat = result.stdout.toString().trim();
5156
5239
  if (!stat) return null;
5157
- const diff = (0, import_child_process3.spawnSync)("git", ["diff", hash, "--", "."], { cwd });
5240
+ const diff = (0, import_child_process4.spawnSync)("git", ["diff", hash, "--", "."], { cwd });
5158
5241
  const raw = diff.stdout.toString();
5159
5242
  if (!raw) return null;
5160
5243
  const lines = raw.split("\n").filter(
@@ -5168,14 +5251,14 @@ function computeUndoDiff(hash, cwd) {
5168
5251
  function applyUndo(hash, cwd) {
5169
5252
  try {
5170
5253
  const dir = cwd ?? process.cwd();
5171
- const restore = (0, import_child_process3.spawnSync)("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
5254
+ const restore = (0, import_child_process4.spawnSync)("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
5172
5255
  cwd: dir
5173
5256
  });
5174
5257
  if (restore.status !== 0) return false;
5175
- const lsTree = (0, import_child_process3.spawnSync)("git", ["ls-tree", "-r", "--name-only", hash], { cwd: dir });
5258
+ const lsTree = (0, import_child_process4.spawnSync)("git", ["ls-tree", "-r", "--name-only", hash], { cwd: dir });
5176
5259
  const snapshotFiles = new Set(lsTree.stdout.toString().trim().split("\n").filter(Boolean));
5177
- const tracked = (0, import_child_process3.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5178
- const untracked = (0, import_child_process3.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5260
+ const tracked = (0, import_child_process4.spawnSync)("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5261
+ const untracked = (0, import_child_process4.spawnSync)("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5179
5262
  for (const file of [...tracked, ...untracked]) {
5180
5263
  const fullPath = import_path7.default.join(dir, file);
5181
5264
  if (!snapshotFiles.has(file) && import_fs5.default.existsSync(fullPath)) {
@@ -5280,15 +5363,15 @@ function openBrowserLocal() {
5280
5363
  const url = `http://${DAEMON_HOST2}:${DAEMON_PORT2}/`;
5281
5364
  try {
5282
5365
  const opts = { stdio: "ignore" };
5283
- if (process.platform === "darwin") (0, import_child_process5.execSync)(`open "${url}"`, opts);
5284
- else if (process.platform === "win32") (0, import_child_process5.execSync)(`cmd /c start "" "${url}"`, opts);
5285
- else (0, import_child_process5.execSync)(`xdg-open "${url}"`, opts);
5366
+ if (process.platform === "darwin") (0, import_child_process6.execSync)(`open "${url}"`, opts);
5367
+ else if (process.platform === "win32") (0, import_child_process6.execSync)(`cmd /c start "" "${url}"`, opts);
5368
+ else (0, import_child_process6.execSync)(`xdg-open "${url}"`, opts);
5286
5369
  } catch {
5287
5370
  }
5288
5371
  }
5289
5372
  async function autoStartDaemonAndWait() {
5290
5373
  try {
5291
- const child = (0, import_child_process5.spawn)("node9", ["daemon"], {
5374
+ const child = (0, import_child_process6.spawn)("node9", ["daemon"], {
5292
5375
  detached: true,
5293
5376
  stdio: "ignore",
5294
5377
  env: { ...process.env, NODE9_AUTO_STARTED: "1" }
@@ -5325,7 +5408,7 @@ async function runProxy(targetCommand) {
5325
5408
  } catch {
5326
5409
  }
5327
5410
  console.log(import_chalk6.default.green(`\u{1F680} Node9 Proxy Active: Monitoring [${targetCommand}]`));
5328
- const child = (0, import_child_process5.spawn)(executable, args, {
5411
+ const child = (0, import_child_process6.spawn)(executable, args, {
5329
5412
  stdio: ["pipe", "pipe", "inherit"],
5330
5413
  // We control STDIN and STDOUT
5331
5414
  shell: false,
@@ -5497,7 +5580,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
5497
5580
  `));
5498
5581
  section("Binary");
5499
5582
  try {
5500
- const which = (0, import_child_process5.execSync)("which node9", { encoding: "utf-8" }).trim();
5583
+ const which = (0, import_child_process6.execSync)("which node9", { encoding: "utf-8" }).trim();
5501
5584
  pass(`node9 found at ${which}`);
5502
5585
  } catch {
5503
5586
  warn("node9 not found in $PATH \u2014 hooks may not find it", "Run: npm install -g @node9/proxy");
@@ -5512,7 +5595,7 @@ program.command("doctor").description("Check that Node9 is installed and configu
5512
5595
  );
5513
5596
  }
5514
5597
  try {
5515
- const gitVersion = (0, import_child_process5.execSync)("git --version", { encoding: "utf-8" }).trim();
5598
+ const gitVersion = (0, import_child_process6.execSync)("git --version", { encoding: "utf-8" }).trim();
5516
5599
  pass(gitVersion);
5517
5600
  } catch {
5518
5601
  warn(
@@ -5863,7 +5946,7 @@ program.command("daemon").description("Run the local approval server").argument(
5863
5946
  console.log(import_chalk6.default.green(`\u{1F310} Opened browser: http://${DAEMON_HOST2}:${DAEMON_PORT2}/`));
5864
5947
  process.exit(0);
5865
5948
  }
5866
- const child = (0, import_child_process5.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
5949
+ const child = (0, import_child_process6.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
5867
5950
  child.unref();
5868
5951
  for (let i = 0; i < 12; i++) {
5869
5952
  await new Promise((r) => setTimeout(r, 250));
@@ -5875,7 +5958,7 @@ program.command("daemon").description("Run the local approval server").argument(
5875
5958
  process.exit(0);
5876
5959
  }
5877
5960
  if (options.background) {
5878
- const child = (0, import_child_process5.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
5961
+ const child = (0, import_child_process6.spawn)("node9", ["daemon"], { detached: true, stdio: "ignore" });
5879
5962
  child.unref();
5880
5963
  console.log(import_chalk6.default.green(`
5881
5964
  \u{1F6E1}\uFE0F Node9 daemon started in background (PID ${child.pid})`));
@@ -5884,7 +5967,7 @@ program.command("daemon").description("Run the local approval server").argument(
5884
5967
  startDaemon();
5885
5968
  }
5886
5969
  );
5887
- program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).action(async (options) => {
5970
+ program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).option("--clear", "Clear history buffer and stream live events fresh", false).action(async (options) => {
5888
5971
  const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
5889
5972
  await startTail2(options);
5890
5973
  });
package/dist/cli.mjs CHANGED
@@ -397,7 +397,13 @@ var init_config_schema = __esm({
397
397
  ),
398
398
  value: z.string().optional(),
399
399
  flags: z.string().optional()
400
- });
400
+ }).refine(
401
+ (c) => {
402
+ if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
403
+ return true;
404
+ },
405
+ { message: "matchesGlob and notMatchesGlob conditions require a value field" }
406
+ );
401
407
  SmartRuleSchema = z.object({
402
408
  name: z.string().optional(),
403
409
  tool: z.string().min(1, "Smart rule tool must not be empty"),
@@ -416,6 +422,7 @@ var init_config_schema = __esm({
416
422
  enableUndo: z.boolean().optional(),
417
423
  enableHookLogDebug: z.boolean().optional(),
418
424
  approvalTimeoutMs: z.number().nonnegative().optional(),
425
+ flightRecorder: z.boolean().optional(),
419
426
  approvers: z.object({
420
427
  native: z.boolean().optional(),
421
428
  browser: z.boolean().optional(),
@@ -751,6 +758,7 @@ import path4 from "path";
751
758
  import os2 from "os";
752
759
  import net from "net";
753
760
  import { randomUUID } from "crypto";
761
+ import { spawnSync } from "child_process";
754
762
  import pm from "picomatch";
755
763
  import { parse } from "sh-syntax";
756
764
  function checkPause() {
@@ -915,7 +923,7 @@ function evaluateSmartConditions(args, rule) {
915
923
  case "matchesGlob":
916
924
  return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
917
925
  case "notMatchesGlob":
918
- return val !== null && cond.value ? !pm.isMatch(val, cond.value) : true;
926
+ return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
919
927
  default:
920
928
  return false;
921
929
  }
@@ -1028,7 +1036,7 @@ function getGlobalSettings() {
1028
1036
  const parsed = JSON.parse(fs2.readFileSync(globalConfigPath, "utf-8"));
1029
1037
  const settings = parsed.settings || {};
1030
1038
  return {
1031
- mode: settings.mode || "standard",
1039
+ mode: settings.mode || "audit",
1032
1040
  autoStartDaemon: settings.autoStartDaemon !== false,
1033
1041
  slackEnabled: settings.slackEnabled !== false,
1034
1042
  enableTrustSessions: settings.enableTrustSessions === true,
@@ -1038,7 +1046,7 @@ function getGlobalSettings() {
1038
1046
  } catch {
1039
1047
  }
1040
1048
  return {
1041
- mode: "standard",
1049
+ mode: "audit",
1042
1050
  autoStartDaemon: true,
1043
1051
  slackEnabled: true,
1044
1052
  enableTrustSessions: false,
@@ -1425,13 +1433,23 @@ function isIgnoredTool(toolName) {
1425
1433
  return matchesPattern(toolName, config.policy.ignoredTools);
1426
1434
  }
1427
1435
  function isDaemonRunning() {
1436
+ const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1437
+ if (fs2.existsSync(pidFile)) {
1438
+ try {
1439
+ const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1440
+ if (port !== DAEMON_PORT) return false;
1441
+ process.kill(pid, 0);
1442
+ return true;
1443
+ } catch {
1444
+ return false;
1445
+ }
1446
+ }
1428
1447
  try {
1429
- const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1430
- if (!fs2.existsSync(pidFile)) return false;
1431
- const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1432
- if (port !== DAEMON_PORT) return false;
1433
- process.kill(pid, 0);
1434
- return true;
1448
+ const r = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
1449
+ encoding: "utf8",
1450
+ timeout: 500
1451
+ });
1452
+ return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
1435
1453
  } catch {
1436
1454
  return false;
1437
1455
  }
@@ -2254,15 +2272,17 @@ var init_core = __esm({
2254
2272
  // permanently overwrites file contents (unrecoverable)
2255
2273
  ];
2256
2274
  DEFAULT_CONFIG = {
2275
+ version: "1.0",
2257
2276
  settings: {
2258
- mode: "standard",
2277
+ mode: "audit",
2259
2278
  autoStartDaemon: true,
2260
2279
  enableUndo: true,
2261
2280
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
2262
- enableHookLogDebug: false,
2263
- approvalTimeoutMs: 0,
2264
- // 0 = disabled; set e.g. 30000 for 30-second auto-deny
2265
- approvers: { native: true, browser: true, cloud: true, terminal: true }
2281
+ enableHookLogDebug: true,
2282
+ approvalTimeoutMs: 3e4,
2283
+ // 30-second auto-deny timeout
2284
+ flightRecorder: true,
2285
+ approvers: { native: true, browser: true, cloud: false, terminal: true }
2266
2286
  },
2267
2287
  policy: {
2268
2288
  sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
@@ -3913,7 +3933,7 @@ import net2 from "net";
3913
3933
  import fs4 from "fs";
3914
3934
  import path6 from "path";
3915
3935
  import os4 from "os";
3916
- import { spawn as spawn2 } from "child_process";
3936
+ import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
3917
3937
  import { randomUUID as randomUUID2 } from "crypto";
3918
3938
  import chalk4 from "chalk";
3919
3939
  function atomicWriteSync2(filePath, data, options) {
@@ -4424,6 +4444,11 @@ data: ${JSON.stringify(item.data)}
4424
4444
  res.writeHead(400).end();
4425
4445
  }
4426
4446
  }
4447
+ if (req.method === "POST" && pathname === "/events/clear") {
4448
+ activityRing.length = 0;
4449
+ res.writeHead(200, { "Content-Type": "application/json" });
4450
+ return res.end(JSON.stringify({ ok: true }));
4451
+ }
4427
4452
  if (req.method === "GET" && pathname === "/audit") {
4428
4453
  res.writeHead(200, { "Content-Type": "application/json" });
4429
4454
  return res.end(JSON.stringify(getAuditHistory()));
@@ -4479,6 +4504,35 @@ data: ${JSON.stringify(item.data)}
4479
4504
  server.listen(DAEMON_PORT2, DAEMON_HOST2);
4480
4505
  return;
4481
4506
  }
4507
+ fetch(`http://${DAEMON_HOST2}:${DAEMON_PORT2}/settings`, {
4508
+ signal: AbortSignal.timeout(1e3)
4509
+ }).then((res) => {
4510
+ if (res.ok) {
4511
+ try {
4512
+ const r = spawnSync2("ss", ["-Htnp", `sport = :${DAEMON_PORT2}`], {
4513
+ encoding: "utf8",
4514
+ timeout: 1e3
4515
+ });
4516
+ const match = r.stdout?.match(/pid=(\d+)/);
4517
+ if (match) {
4518
+ const orphanPid = parseInt(match[1], 10);
4519
+ process.kill(orphanPid, 0);
4520
+ atomicWriteSync2(
4521
+ DAEMON_PID_FILE,
4522
+ JSON.stringify({ pid: orphanPid, port: DAEMON_PORT2, internalToken, autoStarted }),
4523
+ { mode: 384 }
4524
+ );
4525
+ }
4526
+ } catch {
4527
+ }
4528
+ process.exit(0);
4529
+ } else {
4530
+ server.listen(DAEMON_PORT2, DAEMON_HOST2);
4531
+ }
4532
+ }).catch(() => {
4533
+ server.listen(DAEMON_PORT2, DAEMON_HOST2);
4534
+ });
4535
+ return;
4482
4536
  }
4483
4537
  console.error(chalk4.red("\n\u{1F6D1} Node9 Daemon Error:"), e.message);
4484
4538
  process.exit(1);
@@ -4558,14 +4612,25 @@ function stopDaemon() {
4558
4612
  }
4559
4613
  }
4560
4614
  function daemonStatus() {
4561
- if (!fs4.existsSync(DAEMON_PID_FILE))
4562
- return console.log(chalk4.yellow("Node9 daemon: not running"));
4563
- try {
4564
- const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4565
- process.kill(pid, 0);
4566
- console.log(chalk4.green("Node9 daemon: running"));
4567
- } catch {
4568
- console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
4615
+ if (fs4.existsSync(DAEMON_PID_FILE)) {
4616
+ try {
4617
+ const { pid } = JSON.parse(fs4.readFileSync(DAEMON_PID_FILE, "utf-8"));
4618
+ process.kill(pid, 0);
4619
+ console.log(chalk4.green("Node9 daemon: running"));
4620
+ return;
4621
+ } catch {
4622
+ console.log(chalk4.yellow("Node9 daemon: not running (stale PID)"));
4623
+ return;
4624
+ }
4625
+ }
4626
+ const r = spawnSync2("ss", ["-Htnp", `sport = :${DAEMON_PORT2}`], {
4627
+ encoding: "utf8",
4628
+ timeout: 500
4629
+ });
4630
+ if (r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT2}`)) {
4631
+ console.log(chalk4.yellow("Node9 daemon: running (no PID file \u2014 orphaned)"));
4632
+ } else {
4633
+ console.log(chalk4.yellow("Node9 daemon: not running"));
4569
4634
  }
4570
4635
  }
4571
4636
  var ACTIVITY_SOCKET_PATH2, DAEMON_PORT2, DAEMON_HOST2, homeDir, DAEMON_PID_FILE, DECISIONS_FILE, GLOBAL_CONFIG_FILE, CREDENTIALS_FILE, AUDIT_LOG_FILE, TRUST_FILE2, TRUST_DURATIONS, SECRET_KEY_RE, AUTO_DENY_MS, autoStarted, pending, sseClients, abandonTimer, daemonServer, hadBrowserClient, ACTIVITY_RING_SIZE, activityRing;
@@ -4658,6 +4723,13 @@ async function ensureDaemon() {
4658
4723
  } catch {
4659
4724
  }
4660
4725
  }
4726
+ try {
4727
+ const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
4728
+ signal: AbortSignal.timeout(500)
4729
+ });
4730
+ if (res.ok) return DAEMON_PORT2;
4731
+ } catch {
4732
+ }
4661
4733
  console.log(chalk5.dim("\u{1F6E1}\uFE0F Starting Node9 daemon..."));
4662
4734
  const child = spawn3(process.execPath, [process.argv[1], "daemon"], {
4663
4735
  detached: true,
@@ -4667,15 +4739,11 @@ async function ensureDaemon() {
4667
4739
  child.unref();
4668
4740
  for (let i = 0; i < 20; i++) {
4669
4741
  await new Promise((r) => setTimeout(r, 250));
4670
- if (!fs6.existsSync(PID_FILE)) continue;
4671
4742
  try {
4672
4743
  const res = await fetch(`http://127.0.0.1:${DAEMON_PORT2}/settings`, {
4673
4744
  signal: AbortSignal.timeout(500)
4674
4745
  });
4675
- if (res.ok) {
4676
- const { port } = JSON.parse(fs6.readFileSync(PID_FILE, "utf-8"));
4677
- return port;
4678
- }
4746
+ if (res.ok) return DAEMON_PORT2;
4679
4747
  } catch {
4680
4748
  }
4681
4749
  }
@@ -4684,11 +4752,26 @@ async function ensureDaemon() {
4684
4752
  }
4685
4753
  async function startTail(options = {}) {
4686
4754
  const port = await ensureDaemon();
4755
+ if (options.clear) {
4756
+ await new Promise((resolve) => {
4757
+ const req2 = http2.request(
4758
+ { method: "POST", hostname: "127.0.0.1", port, path: "/events/clear" },
4759
+ (res) => {
4760
+ res.resume();
4761
+ res.on("end", resolve);
4762
+ }
4763
+ );
4764
+ req2.on("error", resolve);
4765
+ req2.end();
4766
+ });
4767
+ }
4687
4768
  const connectionTime = Date.now();
4688
4769
  const pending2 = /* @__PURE__ */ new Map();
4689
4770
  console.log(chalk5.cyan.bold(`
4690
4771
  \u{1F6F0}\uFE0F Node9 tail `) + chalk5.dim(`\u2192 localhost:${port}`));
4691
- if (options.history) {
4772
+ if (options.clear) {
4773
+ console.log(chalk5.dim("History cleared. Showing live events. Press Ctrl+C to exit.\n"));
4774
+ } else if (options.history) {
4692
4775
  console.log(chalk5.dim("Showing history + live events. Press Ctrl+C to exit.\n"));
4693
4776
  } else {
4694
4777
  console.log(
@@ -5057,7 +5140,7 @@ import path9 from "path";
5057
5140
  import os7 from "os";
5058
5141
 
5059
5142
  // src/undo.ts
5060
- import { spawnSync } from "child_process";
5143
+ import { spawnSync as spawnSync3 } from "child_process";
5061
5144
  import fs5 from "fs";
5062
5145
  import path7 from "path";
5063
5146
  import os5 from "os";
@@ -5094,12 +5177,12 @@ async function createShadowSnapshot(tool = "unknown", args = {}) {
5094
5177
  if (!fs5.existsSync(path7.join(cwd, ".git"))) return null;
5095
5178
  const tempIndex = path7.join(cwd, ".git", `node9_index_${Date.now()}`);
5096
5179
  const env = { ...process.env, GIT_INDEX_FILE: tempIndex };
5097
- spawnSync("git", ["add", "-A"], { env });
5098
- const treeRes = spawnSync("git", ["write-tree"], { env });
5180
+ spawnSync3("git", ["add", "-A"], { env });
5181
+ const treeRes = spawnSync3("git", ["write-tree"], { env });
5099
5182
  const treeHash = treeRes.stdout.toString().trim();
5100
5183
  if (fs5.existsSync(tempIndex)) fs5.unlinkSync(tempIndex);
5101
5184
  if (!treeHash || treeRes.status !== 0) return null;
5102
- const commitRes = spawnSync("git", [
5185
+ const commitRes = spawnSync3("git", [
5103
5186
  "commit-tree",
5104
5187
  treeHash,
5105
5188
  "-m",
@@ -5130,10 +5213,10 @@ function getSnapshotHistory() {
5130
5213
  }
5131
5214
  function computeUndoDiff(hash, cwd) {
5132
5215
  try {
5133
- const result = spawnSync("git", ["diff", hash, "--stat", "--", "."], { cwd });
5216
+ const result = spawnSync3("git", ["diff", hash, "--stat", "--", "."], { cwd });
5134
5217
  const stat = result.stdout.toString().trim();
5135
5218
  if (!stat) return null;
5136
- const diff = spawnSync("git", ["diff", hash, "--", "."], { cwd });
5219
+ const diff = spawnSync3("git", ["diff", hash, "--", "."], { cwd });
5137
5220
  const raw = diff.stdout.toString();
5138
5221
  if (!raw) return null;
5139
5222
  const lines = raw.split("\n").filter(
@@ -5147,14 +5230,14 @@ function computeUndoDiff(hash, cwd) {
5147
5230
  function applyUndo(hash, cwd) {
5148
5231
  try {
5149
5232
  const dir = cwd ?? process.cwd();
5150
- const restore = spawnSync("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
5233
+ const restore = spawnSync3("git", ["restore", "--source", hash, "--staged", "--worktree", "."], {
5151
5234
  cwd: dir
5152
5235
  });
5153
5236
  if (restore.status !== 0) return false;
5154
- const lsTree = spawnSync("git", ["ls-tree", "-r", "--name-only", hash], { cwd: dir });
5237
+ const lsTree = spawnSync3("git", ["ls-tree", "-r", "--name-only", hash], { cwd: dir });
5155
5238
  const snapshotFiles = new Set(lsTree.stdout.toString().trim().split("\n").filter(Boolean));
5156
- const tracked = spawnSync("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5157
- const untracked = spawnSync("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5239
+ const tracked = spawnSync3("git", ["ls-files"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5240
+ const untracked = spawnSync3("git", ["ls-files", "--others", "--exclude-standard"], { cwd: dir }).stdout.toString().trim().split("\n").filter(Boolean);
5158
5241
  for (const file of [...tracked, ...untracked]) {
5159
5242
  const fullPath = path7.join(dir, file);
5160
5243
  if (!snapshotFiles.has(file) && fs5.existsSync(fullPath)) {
@@ -5863,7 +5946,7 @@ program.command("daemon").description("Run the local approval server").argument(
5863
5946
  startDaemon();
5864
5947
  }
5865
5948
  );
5866
- program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).action(async (options) => {
5949
+ program.command("tail").description("Stream live agent activity to the terminal").option("--history", "Include recent history on connect", false).option("--clear", "Clear history buffer and stream live events fresh", false).action(async (options) => {
5867
5950
  const { startTail: startTail2 } = await Promise.resolve().then(() => (init_tail(), tail_exports));
5868
5951
  await startTail2(options);
5869
5952
  });
package/dist/index.js CHANGED
@@ -42,6 +42,7 @@ var import_path4 = __toESM(require("path"));
42
42
  var import_os2 = __toESM(require("os"));
43
43
  var import_net = __toESM(require("net"));
44
44
  var import_crypto = require("crypto");
45
+ var import_child_process2 = require("child_process");
45
46
  var import_picomatch = __toESM(require("picomatch"));
46
47
  var import_sh_syntax = require("sh-syntax");
47
48
 
@@ -392,7 +393,13 @@ var SmartConditionSchema = import_zod.z.object({
392
393
  ),
393
394
  value: import_zod.z.string().optional(),
394
395
  flags: import_zod.z.string().optional()
395
- });
396
+ }).refine(
397
+ (c) => {
398
+ if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
399
+ return true;
400
+ },
401
+ { message: "matchesGlob and notMatchesGlob conditions require a value field" }
402
+ );
396
403
  var SmartRuleSchema = import_zod.z.object({
397
404
  name: import_zod.z.string().optional(),
398
405
  tool: import_zod.z.string().min(1, "Smart rule tool must not be empty"),
@@ -411,6 +418,7 @@ var ConfigFileSchema = import_zod.z.object({
411
418
  enableUndo: import_zod.z.boolean().optional(),
412
419
  enableHookLogDebug: import_zod.z.boolean().optional(),
413
420
  approvalTimeoutMs: import_zod.z.number().nonnegative().optional(),
421
+ flightRecorder: import_zod.z.boolean().optional(),
414
422
  approvers: import_zod.z.object({
415
423
  native: import_zod.z.boolean().optional(),
416
424
  browser: import_zod.z.boolean().optional(),
@@ -885,7 +893,7 @@ function evaluateSmartConditions(args, rule) {
885
893
  case "matchesGlob":
886
894
  return val !== null && cond.value ? import_picomatch.default.isMatch(val, cond.value) : false;
887
895
  case "notMatchesGlob":
888
- return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : true;
896
+ return val !== null && cond.value ? !import_picomatch.default.isMatch(val, cond.value) : false;
889
897
  default:
890
898
  return false;
891
899
  }
@@ -996,15 +1004,17 @@ var DANGEROUS_WORDS = [
996
1004
  // permanently overwrites file contents (unrecoverable)
997
1005
  ];
998
1006
  var DEFAULT_CONFIG = {
1007
+ version: "1.0",
999
1008
  settings: {
1000
- mode: "standard",
1009
+ mode: "audit",
1001
1010
  autoStartDaemon: true,
1002
1011
  enableUndo: true,
1003
1012
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
1004
- enableHookLogDebug: false,
1005
- approvalTimeoutMs: 0,
1006
- // 0 = disabled; set e.g. 30000 for 30-second auto-deny
1007
- approvers: { native: true, browser: true, cloud: true, terminal: true }
1013
+ enableHookLogDebug: true,
1014
+ approvalTimeoutMs: 3e4,
1015
+ // 30-second auto-deny timeout
1016
+ flightRecorder: true,
1017
+ approvers: { native: true, browser: true, cloud: false, terminal: true }
1008
1018
  },
1009
1019
  policy: {
1010
1020
  sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
@@ -1315,13 +1325,23 @@ function isIgnoredTool(toolName) {
1315
1325
  var DAEMON_PORT = 7391;
1316
1326
  var DAEMON_HOST = "127.0.0.1";
1317
1327
  function isDaemonRunning() {
1328
+ const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1329
+ if (import_fs2.default.existsSync(pidFile)) {
1330
+ try {
1331
+ const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1332
+ if (port !== DAEMON_PORT) return false;
1333
+ process.kill(pid, 0);
1334
+ return true;
1335
+ } catch {
1336
+ return false;
1337
+ }
1338
+ }
1318
1339
  try {
1319
- const pidFile = import_path4.default.join(import_os2.default.homedir(), ".node9", "daemon.pid");
1320
- if (!import_fs2.default.existsSync(pidFile)) return false;
1321
- const { pid, port } = JSON.parse(import_fs2.default.readFileSync(pidFile, "utf-8"));
1322
- if (port !== DAEMON_PORT) return false;
1323
- process.kill(pid, 0);
1324
- return true;
1340
+ const r = (0, import_child_process2.spawnSync)("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
1341
+ encoding: "utf8",
1342
+ timeout: 500
1343
+ });
1344
+ return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
1325
1345
  } catch {
1326
1346
  return false;
1327
1347
  }
package/dist/index.mjs CHANGED
@@ -6,6 +6,7 @@ import path4 from "path";
6
6
  import os2 from "os";
7
7
  import net from "net";
8
8
  import { randomUUID } from "crypto";
9
+ import { spawnSync } from "child_process";
9
10
  import pm from "picomatch";
10
11
  import { parse } from "sh-syntax";
11
12
 
@@ -356,7 +357,13 @@ var SmartConditionSchema = z.object({
356
357
  ),
357
358
  value: z.string().optional(),
358
359
  flags: z.string().optional()
359
- });
360
+ }).refine(
361
+ (c) => {
362
+ if (c.op === "matchesGlob" || c.op === "notMatchesGlob") return c.value !== void 0;
363
+ return true;
364
+ },
365
+ { message: "matchesGlob and notMatchesGlob conditions require a value field" }
366
+ );
360
367
  var SmartRuleSchema = z.object({
361
368
  name: z.string().optional(),
362
369
  tool: z.string().min(1, "Smart rule tool must not be empty"),
@@ -375,6 +382,7 @@ var ConfigFileSchema = z.object({
375
382
  enableUndo: z.boolean().optional(),
376
383
  enableHookLogDebug: z.boolean().optional(),
377
384
  approvalTimeoutMs: z.number().nonnegative().optional(),
385
+ flightRecorder: z.boolean().optional(),
378
386
  approvers: z.object({
379
387
  native: z.boolean().optional(),
380
388
  browser: z.boolean().optional(),
@@ -849,7 +857,7 @@ function evaluateSmartConditions(args, rule) {
849
857
  case "matchesGlob":
850
858
  return val !== null && cond.value ? pm.isMatch(val, cond.value) : false;
851
859
  case "notMatchesGlob":
852
- return val !== null && cond.value ? !pm.isMatch(val, cond.value) : true;
860
+ return val !== null && cond.value ? !pm.isMatch(val, cond.value) : false;
853
861
  default:
854
862
  return false;
855
863
  }
@@ -960,15 +968,17 @@ var DANGEROUS_WORDS = [
960
968
  // permanently overwrites file contents (unrecoverable)
961
969
  ];
962
970
  var DEFAULT_CONFIG = {
971
+ version: "1.0",
963
972
  settings: {
964
- mode: "standard",
973
+ mode: "audit",
965
974
  autoStartDaemon: true,
966
975
  enableUndo: true,
967
976
  // 🔥 ALWAYS TRUE BY DEFAULT for the safety net
968
- enableHookLogDebug: false,
969
- approvalTimeoutMs: 0,
970
- // 0 = disabled; set e.g. 30000 for 30-second auto-deny
971
- approvers: { native: true, browser: true, cloud: true, terminal: true }
977
+ enableHookLogDebug: true,
978
+ approvalTimeoutMs: 3e4,
979
+ // 30-second auto-deny timeout
980
+ flightRecorder: true,
981
+ approvers: { native: true, browser: true, cloud: false, terminal: true }
972
982
  },
973
983
  policy: {
974
984
  sandboxPaths: ["/tmp/**", "**/sandbox/**", "**/test-results/**"],
@@ -1279,13 +1289,23 @@ function isIgnoredTool(toolName) {
1279
1289
  var DAEMON_PORT = 7391;
1280
1290
  var DAEMON_HOST = "127.0.0.1";
1281
1291
  function isDaemonRunning() {
1292
+ const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1293
+ if (fs2.existsSync(pidFile)) {
1294
+ try {
1295
+ const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1296
+ if (port !== DAEMON_PORT) return false;
1297
+ process.kill(pid, 0);
1298
+ return true;
1299
+ } catch {
1300
+ return false;
1301
+ }
1302
+ }
1282
1303
  try {
1283
- const pidFile = path4.join(os2.homedir(), ".node9", "daemon.pid");
1284
- if (!fs2.existsSync(pidFile)) return false;
1285
- const { pid, port } = JSON.parse(fs2.readFileSync(pidFile, "utf-8"));
1286
- if (port !== DAEMON_PORT) return false;
1287
- process.kill(pid, 0);
1288
- return true;
1304
+ const r = spawnSync("ss", ["-Htnp", `sport = :${DAEMON_PORT}`], {
1305
+ encoding: "utf8",
1306
+ timeout: 500
1307
+ });
1308
+ return r.status === 0 && (r.stdout ?? "").includes(`:${DAEMON_PORT}`);
1289
1309
  } catch {
1290
1310
  return false;
1291
1311
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",