agent-recon 1.0.1
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/.claude/hooks/send-event-wsl.py +339 -0
- package/.claude/hooks/send-event.py +334 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +70 -0
- package/EULA.md +223 -0
- package/INSTALL.md +193 -0
- package/LICENSE +287 -0
- package/LICENSE-COMMERCIAL +241 -0
- package/PRIVACY.md +115 -0
- package/README.md +182 -0
- package/SECURITY.md +63 -0
- package/TERMS.md +233 -0
- package/install-service.ps1 +302 -0
- package/installer/cli.js +177 -0
- package/installer/detect.js +355 -0
- package/installer/install.js +195 -0
- package/installer/manifest.js +140 -0
- package/installer/package.json +12 -0
- package/installer/steps/api-keys.js +59 -0
- package/installer/steps/directory.js +41 -0
- package/installer/steps/env-report.js +48 -0
- package/installer/steps/hooks.js +149 -0
- package/installer/steps/service.js +159 -0
- package/installer/steps/tls.js +104 -0
- package/installer/steps/verify.js +117 -0
- package/installer/steps/welcome.js +46 -0
- package/installer/ui.js +133 -0
- package/installer/uninstall.js +233 -0
- package/installer/upgrade.js +289 -0
- package/package.json +58 -0
- package/public/index.html +13953 -0
- package/server/fixtures/allowlist-profiles.json +185 -0
- package/server/package.json +34 -0
- package/server/platform.js +270 -0
- package/server/rules/gitleaks.toml +3214 -0
- package/server/rules/security.yara +579 -0
- package/server/start.js +178 -0
- package/service/agent-recon.service +30 -0
- package/service/com.agent-recon.server.plist +56 -0
- package/setup-linux.sh +259 -0
- package/setup-macos.sh +264 -0
- package/setup-wsl.sh +248 -0
- package/setup.ps1 +171 -0
- package/start-agent-recon.bat +4 -0
package/server/start.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// Copyright 2026 PNW Great Loop LLC. All rights reserved.
|
|
2
|
+
// Licensed under the Apache License, Version 2.0 — see LICENSE for terms.
|
|
3
|
+
|
|
4
|
+
'use strict';
|
|
5
|
+
/**
|
|
6
|
+
* Agent Recon — cross-platform startup wrapper
|
|
7
|
+
*
|
|
8
|
+
* Checks whether the better-sqlite3 native binary matches the current
|
|
9
|
+
* platform (Linux ELF, Win32 PE, or macOS Mach-O). If it doesn't,
|
|
10
|
+
* the wrong binary is renamed to a platform-tagged backup and
|
|
11
|
+
* `npm rebuild better-sqlite3` downloads the correct prebuilt binary.
|
|
12
|
+
*
|
|
13
|
+
* This means you never have to manually run npm rebuild when switching
|
|
14
|
+
* between a WSL terminal, Windows PowerShell/CMD, or macOS.
|
|
15
|
+
*
|
|
16
|
+
* Additionally migrates the SQLite database off NTFS/DrvFs on Linux/WSL
|
|
17
|
+
* (WAL mode requires mmap, which DrvFs does not support) and checks for
|
|
18
|
+
* YARA-X platform binary availability.
|
|
19
|
+
*
|
|
20
|
+
* Usage (from server/):
|
|
21
|
+
* node start.js — normal start (auto-fixes binary if needed)
|
|
22
|
+
* npm start — same (package.json "start" points here)
|
|
23
|
+
*
|
|
24
|
+
* @module start
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const { execSync } = require('child_process');
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const platform = require('./platform');
|
|
31
|
+
|
|
32
|
+
const BINARY = path.join(
|
|
33
|
+
__dirname,
|
|
34
|
+
'node_modules', 'better-sqlite3', 'build', 'Release', 'better_sqlite3.node'
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/** Human-readable name for each binary type (used in log messages). */
|
|
38
|
+
const BINARY_TYPE_NAMES = {
|
|
39
|
+
pe: 'Win32 PE',
|
|
40
|
+
elf: 'Linux ELF',
|
|
41
|
+
'macho-x64': 'Mach-O x64',
|
|
42
|
+
'macho-arm64': 'Mach-O arm64',
|
|
43
|
+
'macho-universal': 'Mach-O universal',
|
|
44
|
+
unknown: 'unknown',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Human-readable target platform name for log messages. */
|
|
48
|
+
function targetPlatformName() {
|
|
49
|
+
const os = platform.detectOS();
|
|
50
|
+
switch (os) {
|
|
51
|
+
case 'windows': return 'Windows';
|
|
52
|
+
case 'macos': return 'macOS';
|
|
53
|
+
case 'wsl': return 'Linux (WSL)';
|
|
54
|
+
case 'linux': return 'Linux';
|
|
55
|
+
default: return os;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Binary platform check ──────────────────────────────────────────────────
|
|
60
|
+
// This solves the cross-platform scenario where node_modules is shared or
|
|
61
|
+
// synced between environments (e.g. NTFS mounts visible from both Windows
|
|
62
|
+
// and WSL, or a checkout copied between macOS and Linux). The native
|
|
63
|
+
// better-sqlite3 .node addon compiled for one OS won't load on another,
|
|
64
|
+
// so we detect the mismatch proactively and rebuild before server.js tries
|
|
65
|
+
// to require() it.
|
|
66
|
+
if (fs.existsSync(BINARY)) {
|
|
67
|
+
try {
|
|
68
|
+
// We only need 5 bytes: the first 4 are the file format magic number
|
|
69
|
+
// (MZ for PE, 7F ELF for ELF, CF FA ED FE for Mach-O). Byte 5 is the
|
|
70
|
+
// Mach-O cputype field, needed because both x64 and arm64 macOS use the
|
|
71
|
+
// same 4-byte little-endian magic (CF FA ED FE) — the cputype at offset 4
|
|
72
|
+
// distinguishes them (0x07 = CPU_TYPE_X86_64, 0x0C = CPU_TYPE_ARM64).
|
|
73
|
+
const hdr = fs.readFileSync(BINARY).slice(0, 5);
|
|
74
|
+
|
|
75
|
+
const binaryType = platform.detectBinaryType(hdr);
|
|
76
|
+
|
|
77
|
+
if (platform.needsRebuild(binaryType)) {
|
|
78
|
+
const fromDesc = BINARY_TYPE_NAMES[binaryType] || binaryType;
|
|
79
|
+
const toDesc = targetPlatformName();
|
|
80
|
+
const backupExt = platform.backupSuffix(binaryType);
|
|
81
|
+
|
|
82
|
+
console.log(`\n[start] better-sqlite3: ${fromDesc} binary detected — rebuilding for ${toDesc}...`);
|
|
83
|
+
|
|
84
|
+
// renameSync instead of unlinkSync: on NTFS (Windows or DrvFs mounts),
|
|
85
|
+
// a loaded .node file has its handle held open by the process that
|
|
86
|
+
// loaded it. unlinkSync would fail with EBUSY because NTFS requires
|
|
87
|
+
// all handles to be closed before deletion. renameSync works because
|
|
88
|
+
// it only updates the directory entry — the file data remains accessible
|
|
89
|
+
// via the existing handle until the last reference is closed.
|
|
90
|
+
try {
|
|
91
|
+
fs.renameSync(BINARY, BINARY + backupExt);
|
|
92
|
+
} catch (_) {
|
|
93
|
+
// Rename failed (e.g. permissions) — try unlinking instead
|
|
94
|
+
try { fs.unlinkSync(BINARY); } catch (_) {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
execSync('npm rebuild better-sqlite3', {
|
|
98
|
+
cwd: __dirname,
|
|
99
|
+
stdio: 'inherit',
|
|
100
|
+
timeout: 120_000,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
console.log(`[start] Binary switched to ${toDesc}.\n`);
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
// Don't abort — let server.js surface a cleaner error if the binary
|
|
107
|
+
// is still wrong after this point.
|
|
108
|
+
console.warn(`[start] Binary check skipped: ${err.message}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── YARA-X binary availability check ──────────────────────────────────────
|
|
113
|
+
{
|
|
114
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
115
|
+
const YARA_PLATFORM_PKGS = {
|
|
116
|
+
'win32-x64': '@litko/yara-x-win32-x64-msvc',
|
|
117
|
+
'linux-x64': '@litko/yara-x-linux-x64-gnu',
|
|
118
|
+
'linux-arm64': '@litko/yara-x-linux-arm64-gnu',
|
|
119
|
+
'darwin-x64': '@litko/yara-x-darwin-x64',
|
|
120
|
+
'darwin-arm64': '@litko/yara-x-darwin-arm64',
|
|
121
|
+
};
|
|
122
|
+
const expectedPkg = YARA_PLATFORM_PKGS[platformKey];
|
|
123
|
+
if (expectedPkg) {
|
|
124
|
+
let yaraResolved = false;
|
|
125
|
+
try { require.resolve(expectedPkg); yaraResolved = true; } catch (_) {}
|
|
126
|
+
if (!yaraResolved) {
|
|
127
|
+
console.warn(
|
|
128
|
+
`\n[start] YARA-X: missing platform binary for ${platformKey}.` +
|
|
129
|
+
`\n Expected package: ${expectedPkg}` +
|
|
130
|
+
`\n Install it: npm pack ${expectedPkg}@0.5.0 && ` +
|
|
131
|
+
`mkdir -p node_modules/${expectedPkg} && ` +
|
|
132
|
+
`tar -xzf *.tgz -C node_modules/${expectedPkg} --strip-components=1` +
|
|
133
|
+
`\n (Security classification will use regex fallback until fixed.)\n`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── DB path: migrate off NTFS/DrvFs on Linux (WAL mode needs mmap) ────────
|
|
140
|
+
if (process.platform !== 'win32' && !process.env.DB_PATH) {
|
|
141
|
+
const defaultDb = path.join(__dirname, '..', 'data', 'agent-recon.db');
|
|
142
|
+
|
|
143
|
+
if (platform.isNtfs(defaultDb)) {
|
|
144
|
+
const homeDb = path.join(
|
|
145
|
+
platform.homedir(),
|
|
146
|
+
'.agent-recon', 'agent-recon.db'
|
|
147
|
+
);
|
|
148
|
+
fs.mkdirSync(path.dirname(homeDb), { recursive: true });
|
|
149
|
+
// Use the ext4 copy if it exists and passes an integrity check.
|
|
150
|
+
// If it's missing or corrupt, copy fresh from NTFS (source of truth for hooks).
|
|
151
|
+
// WAL files are cleaned on copy to avoid opening a stale WAL.
|
|
152
|
+
let needsCopy = !fs.existsSync(homeDb);
|
|
153
|
+
if (!needsCopy) {
|
|
154
|
+
try {
|
|
155
|
+
const Database = require('better-sqlite3');
|
|
156
|
+
const testDb = new Database(homeDb, { readonly: true });
|
|
157
|
+
const ok = testDb.pragma('integrity_check', { simple: true });
|
|
158
|
+
testDb.close();
|
|
159
|
+
if (ok !== 'ok') { needsCopy = true; console.warn('[start] ext4 DB failed integrity check — replacing from NTFS'); }
|
|
160
|
+
} catch (_) { needsCopy = true; console.warn('[start] ext4 DB unreadable — replacing from NTFS'); }
|
|
161
|
+
}
|
|
162
|
+
if (needsCopy && fs.existsSync(defaultDb)) {
|
|
163
|
+
try {
|
|
164
|
+
console.log(`[start] Copying NTFS DB to ext4: ${homeDb}`);
|
|
165
|
+
fs.copyFileSync(defaultDb, homeDb);
|
|
166
|
+
try { fs.unlinkSync(homeDb + '-shm'); } catch (_) {}
|
|
167
|
+
try { fs.unlinkSync(homeDb + '-wal'); } catch (_) {}
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.warn(`[start] DB migration warning: ${e.message}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
process.env.DB_PATH = homeDb;
|
|
173
|
+
console.log(`[start] Using ext4 DB path: ${homeDb}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── Start the server ───────────────────────────────────────────────────────
|
|
178
|
+
require('./server');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Agent Recon — systemd user service
|
|
2
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
# Installation (after running setup-linux.sh):
|
|
4
|
+
#
|
|
5
|
+
# mkdir -p ~/.config/systemd/user
|
|
6
|
+
# cp agent-recon.service ~/.config/systemd/user/
|
|
7
|
+
# systemctl --user daemon-reload
|
|
8
|
+
# systemctl --user enable agent-recon
|
|
9
|
+
# systemctl --user start agent-recon
|
|
10
|
+
#
|
|
11
|
+
# Logs:
|
|
12
|
+
# journalctl --user -u agent-recon -f
|
|
13
|
+
#
|
|
14
|
+
# The {{INSTALL_DIR}} placeholder is replaced by setup-linux.sh during install.
|
|
15
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
[Unit]
|
|
18
|
+
Description=Agent Recon — Claude Code Observability Server
|
|
19
|
+
After=network.target
|
|
20
|
+
|
|
21
|
+
[Service]
|
|
22
|
+
Type=simple
|
|
23
|
+
ExecStart=/usr/bin/node {{INSTALL_DIR}}/server/start.js
|
|
24
|
+
WorkingDirectory={{INSTALL_DIR}}/server
|
|
25
|
+
Restart=on-failure
|
|
26
|
+
RestartSec=5
|
|
27
|
+
Environment=NODE_ENV=production
|
|
28
|
+
|
|
29
|
+
[Install]
|
|
30
|
+
WantedBy=default.target
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!--
|
|
3
|
+
Agent Recon — macOS launchd service
|
|
4
|
+
====================================
|
|
5
|
+
Auto-starts the Agent Recon server on login and restarts on crash.
|
|
6
|
+
|
|
7
|
+
Install:
|
|
8
|
+
cp service/com.agent-recon.server.plist ~/Library/LaunchAgents/
|
|
9
|
+
# Edit the plist to replace {{INSTALL_DIR}} with your actual path, e.g.:
|
|
10
|
+
# sed -i '' 's|{{INSTALL_DIR}}|/Users/you/agent-recon|g' \
|
|
11
|
+
# ~/Library/LaunchAgents/com.agent-recon.server.plist
|
|
12
|
+
launchctl load ~/Library/LaunchAgents/com.agent-recon.server.plist
|
|
13
|
+
|
|
14
|
+
Uninstall:
|
|
15
|
+
launchctl unload ~/Library/LaunchAgents/com.agent-recon.server.plist
|
|
16
|
+
rm ~/Library/LaunchAgents/com.agent-recon.server.plist
|
|
17
|
+
|
|
18
|
+
View logs:
|
|
19
|
+
tail -f ~/Library/Logs/agent-recon.log
|
|
20
|
+
tail -f ~/Library/Logs/agent-recon.error.log
|
|
21
|
+
-->
|
|
22
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
23
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
24
|
+
<plist version="1.0">
|
|
25
|
+
<dict>
|
|
26
|
+
<key>Label</key>
|
|
27
|
+
<string>com.agent-recon.server</string>
|
|
28
|
+
|
|
29
|
+
<key>ProgramArguments</key>
|
|
30
|
+
<array>
|
|
31
|
+
<string>node</string>
|
|
32
|
+
<string>start.js</string>
|
|
33
|
+
</array>
|
|
34
|
+
|
|
35
|
+
<key>WorkingDirectory</key>
|
|
36
|
+
<string>{{INSTALL_DIR}}/server</string>
|
|
37
|
+
|
|
38
|
+
<key>RunAtLoad</key>
|
|
39
|
+
<true/>
|
|
40
|
+
|
|
41
|
+
<key>KeepAlive</key>
|
|
42
|
+
<true/>
|
|
43
|
+
|
|
44
|
+
<key>StandardOutPath</key>
|
|
45
|
+
<string>{{HOME}}/Library/Logs/agent-recon.log</string>
|
|
46
|
+
|
|
47
|
+
<key>StandardErrorPath</key>
|
|
48
|
+
<string>{{HOME}}/Library/Logs/agent-recon.error.log</string>
|
|
49
|
+
|
|
50
|
+
<key>EnvironmentVariables</key>
|
|
51
|
+
<dict>
|
|
52
|
+
<key>PATH</key>
|
|
53
|
+
<string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string>
|
|
54
|
+
</dict>
|
|
55
|
+
</dict>
|
|
56
|
+
</plist>
|
package/setup-linux.sh
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
3
|
+
# Agent Recon — Native Linux Setup
|
|
4
|
+
# Run this script on a native Linux machine (not WSL) to wire the Agent Recon
|
|
5
|
+
# hooks into your Claude Code installation.
|
|
6
|
+
#
|
|
7
|
+
# bash setup-linux.sh
|
|
8
|
+
#
|
|
9
|
+
# What it does:
|
|
10
|
+
# 1. Creates ~/.claude/hooks/
|
|
11
|
+
# 2. Copies (or updates) send-event.py there (with sha256 drift detection)
|
|
12
|
+
# 3. Verifies python3 is available
|
|
13
|
+
# 4. Tests connectivity to localhost:3131
|
|
14
|
+
# 5. Writes (or merges) ~/.claude/settings.json with all 13 hook registrations
|
|
15
|
+
# 6. Checks for libsecret credential backend (secret-tool)
|
|
16
|
+
# 7. Optionally installs a systemd user service for auto-start
|
|
17
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
21
|
+
HOOK_SRC="$SCRIPT_DIR/.claude/hooks/send-event.py"
|
|
22
|
+
HOOK_DST="$HOME/.claude/hooks/send-event.py"
|
|
23
|
+
SETTINGS="$HOME/.claude/settings.json"
|
|
24
|
+
HOOK_CMD="python3 $HOOK_DST"
|
|
25
|
+
SERVICE_SRC="$SCRIPT_DIR/service/agent-recon.service"
|
|
26
|
+
|
|
27
|
+
# ── Validate ─────────────────────────────────────────────────────────────────
|
|
28
|
+
if [[ ! -f "$HOOK_SRC" ]]; then
|
|
29
|
+
echo "ERROR: Cannot find hook source at $HOOK_SRC"
|
|
30
|
+
echo " Run this script from the agent-recon project root directory."
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
if ! command -v python3 &>/dev/null; then
|
|
35
|
+
echo "ERROR: python3 not found. Install it with your package manager:"
|
|
36
|
+
echo " Debian/Ubuntu : sudo apt install python3"
|
|
37
|
+
echo " Fedora : sudo dnf install python3"
|
|
38
|
+
echo " Arch : sudo pacman -S python"
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
echo "✓ python3 found: $(python3 --version 2>&1)"
|
|
43
|
+
|
|
44
|
+
# ── Install / sync hook script ───────────────────────────────────────────────
|
|
45
|
+
mkdir -p "$HOME/.claude/hooks"
|
|
46
|
+
|
|
47
|
+
if [[ -f "$HOOK_DST" ]]; then
|
|
48
|
+
SRC_HASH=$(sha256sum "$HOOK_SRC" | cut -d' ' -f1)
|
|
49
|
+
DST_HASH=$(sha256sum "$HOOK_DST" | cut -d' ' -f1)
|
|
50
|
+
if [[ "$SRC_HASH" != "$DST_HASH" ]]; then
|
|
51
|
+
echo "↻ Hook script updated — syncing to $HOOK_DST"
|
|
52
|
+
cp "$HOOK_SRC" "$HOOK_DST"
|
|
53
|
+
chmod +x "$HOOK_DST"
|
|
54
|
+
echo "✓ Synced"
|
|
55
|
+
else
|
|
56
|
+
echo "✓ Hook script already up-to-date: $HOOK_DST"
|
|
57
|
+
fi
|
|
58
|
+
else
|
|
59
|
+
cp "$HOOK_SRC" "$HOOK_DST"
|
|
60
|
+
chmod +x "$HOOK_DST"
|
|
61
|
+
echo "✓ Installed hook forwarder → $HOOK_DST"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# ── Test Agent Recon server connectivity ─────────────────────────────────────
|
|
65
|
+
echo ""
|
|
66
|
+
echo "── Connectivity check ──────────────────────────────────────────────────"
|
|
67
|
+
|
|
68
|
+
if curl -sf --max-time 2 "http://localhost:3131/health" > /dev/null 2>&1; then
|
|
69
|
+
echo " ✓ localhost:3131 REACHABLE"
|
|
70
|
+
else
|
|
71
|
+
echo " ✗ localhost:3131 not reachable"
|
|
72
|
+
echo ""
|
|
73
|
+
echo " ⚠ Agent Recon server is not running."
|
|
74
|
+
echo " Start it with:"
|
|
75
|
+
echo " cd $SCRIPT_DIR/server && node start.js"
|
|
76
|
+
echo " Then re-run this script to verify connectivity."
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# ── Write settings.json ─────────────────────────────────────────────────────
|
|
80
|
+
echo ""
|
|
81
|
+
echo "── Hook registration ─────────────────────────────────────────────────"
|
|
82
|
+
|
|
83
|
+
if [[ -f "$SETTINGS" ]]; then
|
|
84
|
+
# Merge hooks into existing settings.json using Python so we don't clobber
|
|
85
|
+
# other fields (e.g. skipDangerousModePermissionPrompt).
|
|
86
|
+
python3 - "$SETTINGS" "$HOOK_CMD" <<'PYEOF'
|
|
87
|
+
import json, sys
|
|
88
|
+
|
|
89
|
+
settings_path = sys.argv[1]
|
|
90
|
+
hook_cmd = sys.argv[2]
|
|
91
|
+
|
|
92
|
+
with open(settings_path) as f:
|
|
93
|
+
cfg = json.load(f)
|
|
94
|
+
|
|
95
|
+
hooks = cfg.setdefault("hooks", {})
|
|
96
|
+
|
|
97
|
+
def hook_entry(matcher=False):
|
|
98
|
+
entry = {"type": "command", "command": hook_cmd, "async": True, "timeout": 10}
|
|
99
|
+
if matcher:
|
|
100
|
+
return {"matcher": "", "hooks": [entry]}
|
|
101
|
+
return {"hooks": [entry]}
|
|
102
|
+
|
|
103
|
+
EVENTS_PLAIN = ["SessionStart","SessionEnd","UserPromptSubmit",
|
|
104
|
+
"SubagentStart","SubagentStop","Stop","TeammateIdle","TaskCompleted"]
|
|
105
|
+
EVENTS_MATCHER = ["PreToolUse","PostToolUse","PostToolUseFailure","Notification","PreCompact"]
|
|
106
|
+
|
|
107
|
+
def already_registered(event_hooks):
|
|
108
|
+
for group in event_hooks:
|
|
109
|
+
for h in group.get("hooks", []):
|
|
110
|
+
if h.get("command","") == hook_cmd:
|
|
111
|
+
return True
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
added = []
|
|
115
|
+
for ev in EVENTS_PLAIN:
|
|
116
|
+
if ev not in hooks:
|
|
117
|
+
hooks[ev] = [hook_entry(False)]
|
|
118
|
+
added.append(ev)
|
|
119
|
+
elif not already_registered(hooks[ev]):
|
|
120
|
+
hooks[ev].append(hook_entry(False))
|
|
121
|
+
added.append(ev)
|
|
122
|
+
|
|
123
|
+
for ev in EVENTS_MATCHER:
|
|
124
|
+
if ev not in hooks:
|
|
125
|
+
hooks[ev] = [hook_entry(True)]
|
|
126
|
+
added.append(ev)
|
|
127
|
+
elif not already_registered(hooks[ev]):
|
|
128
|
+
hooks[ev].append(hook_entry(True))
|
|
129
|
+
added.append(ev)
|
|
130
|
+
|
|
131
|
+
with open(settings_path, "w") as f:
|
|
132
|
+
json.dump(cfg, f, indent=2)
|
|
133
|
+
f.write("\n")
|
|
134
|
+
|
|
135
|
+
if added:
|
|
136
|
+
print("✓ Merged hooks into", settings_path)
|
|
137
|
+
print(" Added:", ", ".join(added))
|
|
138
|
+
else:
|
|
139
|
+
print("✓ All hooks already present in", settings_path, "— nothing changed")
|
|
140
|
+
PYEOF
|
|
141
|
+
else
|
|
142
|
+
cat > "$SETTINGS" <<SETTINGSEOF
|
|
143
|
+
{
|
|
144
|
+
"hooks": {
|
|
145
|
+
"SessionStart": [{"hooks": [{"type": "command","command": "$HOOK_CMD","async": true,"timeout": 10}]}],
|
|
146
|
+
"SessionEnd": [{"hooks": [{"type": "command","command": "$HOOK_CMD","async": true,"timeout": 10}]}],
|
|
147
|
+
"UserPromptSubmit": [{"hooks": [{"type": "command","command": "$HOOK_CMD","async": true,"timeout": 10}]}],
|
|
148
|
+
"PreToolUse": [{"matcher": "","hooks": [{"type": "command","command": "$HOOK_CMD","async": true,"timeout": 10}]}],
|
|
149
|
+
"PostToolUse": [{"matcher": "","hooks": [{"type": "command","command": "$HOOK_CMD","async": true,"timeout": 10}]}],
|
|
150
|
+
"PostToolUseFailure": [{"matcher": "","hooks": [{"type": "command","command": "$HOOK_CMD","async": true,"timeout": 10}]}],
|
|
151
|
+
"SubagentStart": [{"hooks": [{"type": "command","command": "$HOOK_CMD","async": true,"timeout": 10}]}],
|
|
152
|
+
"SubagentStop": [{"hooks": [{"type": "command","command": "$HOOK_CMD","async": true,"timeout": 10}]}],
|
|
153
|
+
"Stop": [{"hooks": [{"type": "command","command": "$HOOK_CMD","async": true,"timeout": 10}]}],
|
|
154
|
+
"Notification": [{"matcher": "","hooks": [{"type": "command","command": "$HOOK_CMD","async": true,"timeout": 10}]}],
|
|
155
|
+
"TeammateIdle": [{"hooks": [{"type": "command","command": "$HOOK_CMD","async": true,"timeout": 10}]}],
|
|
156
|
+
"TaskCompleted": [{"hooks": [{"type": "command","command": "$HOOK_CMD","async": true,"timeout": 10}]}],
|
|
157
|
+
"PreCompact": [{"matcher": "","hooks": [{"type": "command","command": "$HOOK_CMD","async": true,"timeout": 10}]}]
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
SETTINGSEOF
|
|
161
|
+
echo "✓ Created $SETTINGS"
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
# ── Check libsecret credential backend ──────────────────────────────────────
|
|
165
|
+
echo ""
|
|
166
|
+
echo "── Credential backend check ──────────────────────────────────────────"
|
|
167
|
+
|
|
168
|
+
if command -v secret-tool &>/dev/null; then
|
|
169
|
+
echo " ✓ secret-tool found (libsecret credential backend available)"
|
|
170
|
+
|
|
171
|
+
# Test store/lookup/clear cycle
|
|
172
|
+
TEST_KEY="agent-recon-setup-test-$$"
|
|
173
|
+
SECRET_OK=true
|
|
174
|
+
|
|
175
|
+
if ! echo -n "test-value" | secret-tool store --label="Agent Recon test" agent-recon-key "$TEST_KEY" 2>/dev/null; then
|
|
176
|
+
SECRET_OK=false
|
|
177
|
+
fi
|
|
178
|
+
|
|
179
|
+
if $SECRET_OK; then
|
|
180
|
+
LOOKUP=$(secret-tool lookup agent-recon-key "$TEST_KEY" 2>/dev/null || true)
|
|
181
|
+
if [[ "$LOOKUP" == "test-value" ]]; then
|
|
182
|
+
echo " ✓ secret-tool store/lookup cycle passed"
|
|
183
|
+
else
|
|
184
|
+
SECRET_OK=false
|
|
185
|
+
fi
|
|
186
|
+
# Clean up test entry
|
|
187
|
+
secret-tool clear agent-recon-key "$TEST_KEY" 2>/dev/null || true
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
if ! $SECRET_OK; then
|
|
191
|
+
echo " ⚠ secret-tool is installed but the test cycle failed."
|
|
192
|
+
echo " This may happen if no keyring daemon is running (e.g. headless server)."
|
|
193
|
+
echo " PBKDF2 fallback will be used for credential storage."
|
|
194
|
+
fi
|
|
195
|
+
else
|
|
196
|
+
echo " ⚠ secret-tool not found — PBKDF2 fallback will be used for credentials."
|
|
197
|
+
echo " To install libsecret for native keyring support:"
|
|
198
|
+
echo " Debian/Ubuntu : sudo apt install libsecret-tools"
|
|
199
|
+
echo " Fedora : sudo dnf install libsecret"
|
|
200
|
+
echo " Arch : sudo pacman -S libsecret"
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
# ── Optional systemd user service ───────────────────────────────────────────
|
|
204
|
+
echo ""
|
|
205
|
+
echo "── Systemd user service ────────────────────────────────────────────────"
|
|
206
|
+
|
|
207
|
+
if [[ ! -f "$SERVICE_SRC" ]]; then
|
|
208
|
+
echo " ⚠ Service unit file not found at $SERVICE_SRC — skipping."
|
|
209
|
+
else
|
|
210
|
+
echo " A systemd user service can auto-start Agent Recon on login."
|
|
211
|
+
echo ""
|
|
212
|
+
read -r -p " Install systemd user service? [y/N] " INSTALL_SERVICE
|
|
213
|
+
if [[ "${INSTALL_SERVICE,,}" == "y" ]]; then
|
|
214
|
+
SERVICE_DIR="$HOME/.config/systemd/user"
|
|
215
|
+
mkdir -p "$SERVICE_DIR"
|
|
216
|
+
|
|
217
|
+
# Replace {{INSTALL_DIR}} placeholder with the actual project path
|
|
218
|
+
sed "s|{{INSTALL_DIR}}|$SCRIPT_DIR|g" "$SERVICE_SRC" > "$SERVICE_DIR/agent-recon.service"
|
|
219
|
+
|
|
220
|
+
systemctl --user daemon-reload
|
|
221
|
+
systemctl --user enable agent-recon
|
|
222
|
+
echo " ✓ Service installed and enabled."
|
|
223
|
+
echo ""
|
|
224
|
+
read -r -p " Start the service now? [y/N] " START_NOW
|
|
225
|
+
if [[ "${START_NOW,,}" == "y" ]]; then
|
|
226
|
+
systemctl --user start agent-recon
|
|
227
|
+
echo " ✓ Service started."
|
|
228
|
+
echo " Check status: systemctl --user status agent-recon"
|
|
229
|
+
echo " View logs : journalctl --user -u agent-recon -f"
|
|
230
|
+
else
|
|
231
|
+
echo " Service enabled but not started. Start it later with:"
|
|
232
|
+
echo " systemctl --user start agent-recon"
|
|
233
|
+
fi
|
|
234
|
+
else
|
|
235
|
+
echo " Skipped. You can install it manually later:"
|
|
236
|
+
echo " mkdir -p ~/.config/systemd/user"
|
|
237
|
+
echo " sed 's|{{INSTALL_DIR}}|$SCRIPT_DIR|g' $SERVICE_SRC > ~/.config/systemd/user/agent-recon.service"
|
|
238
|
+
echo " systemctl --user daemon-reload"
|
|
239
|
+
echo " systemctl --user enable agent-recon"
|
|
240
|
+
echo " systemctl --user start agent-recon"
|
|
241
|
+
fi
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
# ── Summary ──────────────────────────────────────────────────────────────────
|
|
245
|
+
echo ""
|
|
246
|
+
echo "────────────────────────────────────────────────────────────────────────"
|
|
247
|
+
echo " Linux setup complete!"
|
|
248
|
+
echo ""
|
|
249
|
+
echo " Hook script : $HOOK_DST"
|
|
250
|
+
echo " Server URL : http://localhost:3131"
|
|
251
|
+
echo ""
|
|
252
|
+
echo " Start server: cd $SCRIPT_DIR/server && node start.js"
|
|
253
|
+
echo " (start.js auto-rebuilds the SQLite binary if platform mismatches)"
|
|
254
|
+
echo ""
|
|
255
|
+
echo " Start a Claude session in any directory and events will stream"
|
|
256
|
+
echo " to http://localhost:3131"
|
|
257
|
+
echo ""
|
|
258
|
+
echo " Debug mode : AGENT_RECON_DEBUG=1 claude → ~/.claude/agent-recon-debug.log"
|
|
259
|
+
echo "────────────────────────────────────────────────────────────────────────"
|