@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 +25 -12
- package/dist/cli.js +140 -57
- package/dist/cli.mjs +124 -41
- package/dist/index.js +33 -13
- package/dist/index.mjs +33 -13
- package/package.json +1 -1
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": "
|
|
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":
|
|
300
|
+
"cloud": false,
|
|
295
301
|
"terminal": true
|
|
296
302
|
}
|
|
297
303
|
}
|
|
298
304
|
}
|
|
299
305
|
```
|
|
300
306
|
|
|
301
|
-
| Key | Default
|
|
302
|
-
| :------------------- |
|
|
303
|
-
| `mode` | `"
|
|
304
|
-
| `enableUndo` | `true`
|
|
305
|
-
| `
|
|
306
|
-
| `
|
|
307
|
-
| `approvers.
|
|
308
|
-
| `approvers.
|
|
309
|
-
| `approvers.
|
|
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) :
|
|
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 || "
|
|
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: "
|
|
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
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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: "
|
|
2298
|
+
mode: "audit",
|
|
2280
2299
|
autoStartDaemon: true,
|
|
2281
2300
|
enableUndo: true,
|
|
2282
2301
|
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
2283
|
-
enableHookLogDebug:
|
|
2284
|
-
approvalTimeoutMs:
|
|
2285
|
-
//
|
|
2286
|
-
|
|
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,
|
|
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 (
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
5119
|
-
const treeRes = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
5178
|
-
const untracked = (0,
|
|
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,
|
|
5284
|
-
else if (process.platform === "win32") (0,
|
|
5285
|
-
else (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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) :
|
|
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 || "
|
|
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: "
|
|
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
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
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: "
|
|
2277
|
+
mode: "audit",
|
|
2259
2278
|
autoStartDaemon: true,
|
|
2260
2279
|
enableUndo: true,
|
|
2261
2280
|
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
2262
|
-
enableHookLogDebug:
|
|
2263
|
-
approvalTimeoutMs:
|
|
2264
|
-
//
|
|
2265
|
-
|
|
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 (
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
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.
|
|
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
|
-
|
|
5098
|
-
const treeRes =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
5157
|
-
const untracked =
|
|
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) :
|
|
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: "
|
|
1009
|
+
mode: "audit",
|
|
1001
1010
|
autoStartDaemon: true,
|
|
1002
1011
|
enableUndo: true,
|
|
1003
1012
|
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
1004
|
-
enableHookLogDebug:
|
|
1005
|
-
approvalTimeoutMs:
|
|
1006
|
-
//
|
|
1007
|
-
|
|
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
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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) :
|
|
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: "
|
|
973
|
+
mode: "audit",
|
|
965
974
|
autoStartDaemon: true,
|
|
966
975
|
enableUndo: true,
|
|
967
976
|
// 🔥 ALWAYS TRUE BY DEFAULT for the safety net
|
|
968
|
-
enableHookLogDebug:
|
|
969
|
-
approvalTimeoutMs:
|
|
970
|
-
//
|
|
971
|
-
|
|
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
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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
|
}
|