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.
Files changed (44) hide show
  1. package/.claude/hooks/send-event-wsl.py +339 -0
  2. package/.claude/hooks/send-event.py +334 -0
  3. package/CHANGELOG.md +66 -0
  4. package/CONTRIBUTING.md +70 -0
  5. package/EULA.md +223 -0
  6. package/INSTALL.md +193 -0
  7. package/LICENSE +287 -0
  8. package/LICENSE-COMMERCIAL +241 -0
  9. package/PRIVACY.md +115 -0
  10. package/README.md +182 -0
  11. package/SECURITY.md +63 -0
  12. package/TERMS.md +233 -0
  13. package/install-service.ps1 +302 -0
  14. package/installer/cli.js +177 -0
  15. package/installer/detect.js +355 -0
  16. package/installer/install.js +195 -0
  17. package/installer/manifest.js +140 -0
  18. package/installer/package.json +12 -0
  19. package/installer/steps/api-keys.js +59 -0
  20. package/installer/steps/directory.js +41 -0
  21. package/installer/steps/env-report.js +48 -0
  22. package/installer/steps/hooks.js +149 -0
  23. package/installer/steps/service.js +159 -0
  24. package/installer/steps/tls.js +104 -0
  25. package/installer/steps/verify.js +117 -0
  26. package/installer/steps/welcome.js +46 -0
  27. package/installer/ui.js +133 -0
  28. package/installer/uninstall.js +233 -0
  29. package/installer/upgrade.js +289 -0
  30. package/package.json +58 -0
  31. package/public/index.html +13953 -0
  32. package/server/fixtures/allowlist-profiles.json +185 -0
  33. package/server/package.json +34 -0
  34. package/server/platform.js +270 -0
  35. package/server/rules/gitleaks.toml +3214 -0
  36. package/server/rules/security.yara +579 -0
  37. package/server/start.js +178 -0
  38. package/service/agent-recon.service +30 -0
  39. package/service/com.agent-recon.server.plist +56 -0
  40. package/setup-linux.sh +259 -0
  41. package/setup-macos.sh +264 -0
  42. package/setup-wsl.sh +248 -0
  43. package/setup.ps1 +171 -0
  44. package/start-agent-recon.bat +4 -0
@@ -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 "────────────────────────────────────────────────────────────────────────"