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
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Copyright 2026 PNW Great Loop LLC. All rights reserved.
|
|
2
|
+
// Licensed under the Agent Recon™ Commercial License — see LICENSE-COMMERCIAL.
|
|
3
|
+
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { confirm, password } = require('@inquirer/prompts');
|
|
8
|
+
const ui = require('../ui');
|
|
9
|
+
|
|
10
|
+
async function run(ctx) {
|
|
11
|
+
const wantKeys = await confirm({
|
|
12
|
+
message: 'Configure LLM API keys for analysis features (security chains, insights, prompt coaching)?',
|
|
13
|
+
default: true,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (!wantKeys) {
|
|
17
|
+
ui.info('Skipping API key configuration');
|
|
18
|
+
return { success: true, skipped: true, configured: [] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const configured = [];
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const credStore = require(path.join(ctx.installDir, 'server', 'credential-store'));
|
|
25
|
+
|
|
26
|
+
const anthropicKey = await password({
|
|
27
|
+
message: 'Anthropic API key (sk-ant-...):',
|
|
28
|
+
mask: '*',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (anthropicKey && anthropicKey.trim()) {
|
|
32
|
+
credStore.setSecret('agent-recon', 'anthropic-api-key', anthropicKey.trim());
|
|
33
|
+
configured.push('anthropic');
|
|
34
|
+
ui.ok('Anthropic API key stored');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const openaiKey = await password({
|
|
38
|
+
message: 'OpenAI API key (sk-...) [optional, press Enter to skip]:',
|
|
39
|
+
mask: '*',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (openaiKey && openaiKey.trim()) {
|
|
43
|
+
credStore.setSecret('agent-recon', 'openai-api-key', openaiKey.trim());
|
|
44
|
+
configured.push('openai');
|
|
45
|
+
ui.ok('OpenAI API key stored');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (configured.length === 0) {
|
|
49
|
+
ui.info('No API keys provided — analysis features will be disabled');
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
ui.warn(`Could not configure API keys: ${err.message}`);
|
|
53
|
+
return { success: true, configured, error: err.message };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { success: true, configured };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { run };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Copyright 2026 PNW Great Loop LLC. All rights reserved.
|
|
2
|
+
// Licensed under the Agent Recon™ Commercial License — see LICENSE-COMMERCIAL.
|
|
3
|
+
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { input } = require('@inquirer/prompts');
|
|
9
|
+
const ui = require('../ui');
|
|
10
|
+
const platform = require('../../server/platform');
|
|
11
|
+
|
|
12
|
+
function isValidInstallDir(dir) {
|
|
13
|
+
try {
|
|
14
|
+
return fs.existsSync(path.join(dir, 'server', 'server.js'));
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function run(ctx) {
|
|
21
|
+
let dir = ctx.installDir || path.resolve(__dirname, '..', '..');
|
|
22
|
+
|
|
23
|
+
if (!isValidInstallDir(dir)) {
|
|
24
|
+
ui.warn(`Default directory not valid: ${dir}`);
|
|
25
|
+
dir = await input({
|
|
26
|
+
message: 'Enter the Agent Recon install directory:',
|
|
27
|
+
validate: val => isValidInstallDir(val) || 'Directory must contain server/server.js',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
dir = path.resolve(dir);
|
|
32
|
+
|
|
33
|
+
if (platform.detectOS() === 'wsl' && platform.isNtfs(dir)) {
|
|
34
|
+
ui.warn('Install directory is on NTFS — native addon builds and file watching may be slower');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ui.ok(`Install directory: ${dir}`);
|
|
38
|
+
return { success: true, installDir: dir };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { run };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Copyright 2026 PNW Great Loop LLC. All rights reserved.
|
|
2
|
+
// Licensed under the Agent Recon™ Commercial License — see LICENSE-COMMERCIAL.
|
|
3
|
+
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const { confirm } = require('@inquirer/prompts');
|
|
7
|
+
const ui = require('../ui');
|
|
8
|
+
|
|
9
|
+
async function run(ctx) {
|
|
10
|
+
ui.printEnvReport(ctx.envReport);
|
|
11
|
+
|
|
12
|
+
const blockers = [];
|
|
13
|
+
const warnings = [];
|
|
14
|
+
|
|
15
|
+
if (!ctx.envReport.pythonVersion) {
|
|
16
|
+
blockers.push('Python 3 not found — hook scripts require Python');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const nodeMajor = parseInt(ctx.envReport.nodeVersion, 10);
|
|
20
|
+
if (!nodeMajor || nodeMajor < 22) {
|
|
21
|
+
blockers.push(`Node.js ${ctx.envReport.nodeVersion || 'not found'} — version 22+ required`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!ctx.envReport.claudeSettingsExists) {
|
|
25
|
+
warnings.push('Claude settings.json not found — hooks cannot be registered until Claude Code is run once');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!ctx.envReport.existingInstall || !ctx.envReport.existingInstall.serverHealthy) {
|
|
29
|
+
warnings.push('Agent Recon server is not currently running');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const w of warnings) ui.warn(w);
|
|
33
|
+
for (const b of blockers) ui.error(b);
|
|
34
|
+
|
|
35
|
+
if (blockers.length > 0) {
|
|
36
|
+
const proceed = await confirm({
|
|
37
|
+
message: 'Critical issues detected. Continue anyway?',
|
|
38
|
+
default: false,
|
|
39
|
+
});
|
|
40
|
+
if (!proceed) {
|
|
41
|
+
return { success: false, error: 'Blocked by environment issues' };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { success: true, blockers, warnings };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { run };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Copyright 2026 PNW Great Loop LLC. All rights reserved.
|
|
2
|
+
// Licensed under the Agent Recon™ Commercial License — see LICENSE-COMMERCIAL.
|
|
3
|
+
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const ui = require('../ui');
|
|
10
|
+
const platform = require('../../server/platform');
|
|
11
|
+
|
|
12
|
+
const EVENTS_PLAIN = [
|
|
13
|
+
'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart',
|
|
14
|
+
'SubagentStop', 'Stop', 'TeammateIdle', 'TaskCompleted',
|
|
15
|
+
// Added in Claude Code v2.1.x
|
|
16
|
+
'StopFailure', 'PostCompact', 'InstructionsLoaded', 'ConfigChange',
|
|
17
|
+
'CwdChanged', 'FileChanged', 'WorktreeCreate', 'WorktreeRemove',
|
|
18
|
+
'TaskCreated', 'ElicitationResult',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const EVENTS_MATCHER = [
|
|
22
|
+
'PreToolUse', 'PostToolUse', 'PostToolUseFailure', 'Notification', 'PreCompact',
|
|
23
|
+
// Added in Claude Code v2.1.x
|
|
24
|
+
'PermissionRequest', 'Elicitation',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function _alreadyRegistered(groups, hookCommand) {
|
|
28
|
+
if (!Array.isArray(groups)) return false;
|
|
29
|
+
for (const group of groups) {
|
|
30
|
+
for (const h of (group.hooks || [])) {
|
|
31
|
+
if (h.command && h.command === hookCommand) return true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _mergeHooks(settings, hookCommand) {
|
|
38
|
+
const result = JSON.parse(JSON.stringify(settings || {}));
|
|
39
|
+
if (!result.hooks) result.hooks = {};
|
|
40
|
+
|
|
41
|
+
const hookEntry = { type: 'command', command: hookCommand, async: true, timeout: 10 };
|
|
42
|
+
|
|
43
|
+
for (const event of EVENTS_PLAIN) {
|
|
44
|
+
if (!result.hooks[event]) result.hooks[event] = [];
|
|
45
|
+
if (!_alreadyRegistered(result.hooks[event], hookCommand)) {
|
|
46
|
+
result.hooks[event].push({ hooks: [{ ...hookEntry }] });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const event of EVENTS_MATCHER) {
|
|
51
|
+
if (!result.hooks[event]) result.hooks[event] = [];
|
|
52
|
+
if (!_alreadyRegistered(result.hooks[event], hookCommand)) {
|
|
53
|
+
result.hooks[event].push({ matcher: '', hooks: [{ ...hookEntry }] });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function _removeHooks(settings, hookCommand) {
|
|
61
|
+
const result = JSON.parse(JSON.stringify(settings || {}));
|
|
62
|
+
if (!result.hooks) return result;
|
|
63
|
+
|
|
64
|
+
for (const event of Object.keys(result.hooks)) {
|
|
65
|
+
if (!Array.isArray(result.hooks[event])) continue;
|
|
66
|
+
result.hooks[event] = result.hooks[event].filter(group => {
|
|
67
|
+
const hooks = group.hooks || [];
|
|
68
|
+
return !hooks.some(h => h.command && h.command === hookCommand);
|
|
69
|
+
});
|
|
70
|
+
if (result.hooks[event].length === 0) {
|
|
71
|
+
delete result.hooks[event];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sha256(filePath) {
|
|
79
|
+
try {
|
|
80
|
+
const buf = fs.readFileSync(filePath);
|
|
81
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function run(ctx) {
|
|
88
|
+
const osType = ctx.envReport.os;
|
|
89
|
+
const scriptName = osType === 'wsl' ? 'send-event-wsl.py' : 'send-event.py';
|
|
90
|
+
const source = path.join(ctx.installDir, '.claude', 'hooks', scriptName);
|
|
91
|
+
const destDir = path.join(ctx.envReport.home, '.claude', 'hooks');
|
|
92
|
+
const destination = path.join(destDir, scriptName);
|
|
93
|
+
|
|
94
|
+
if (!fs.existsSync(source)) {
|
|
95
|
+
return { success: false, error: `Hook script not found: ${source}` };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
99
|
+
|
|
100
|
+
const srcHash = sha256(source);
|
|
101
|
+
const dstHash = sha256(destination);
|
|
102
|
+
if (srcHash !== dstHash) {
|
|
103
|
+
fs.copyFileSync(source, destination);
|
|
104
|
+
ui.ok(`Copied ${scriptName} to ${destDir}`);
|
|
105
|
+
} else {
|
|
106
|
+
ui.ok(`Hook script already up to date`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (osType !== 'windows') {
|
|
110
|
+
try { fs.chmodSync(destination, 0o755); } catch { /* best effort */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const pythonPath = ctx.envReport.pythonPath || 'python3';
|
|
114
|
+
const hookCommand = pythonPath + ' ' + destination;
|
|
115
|
+
|
|
116
|
+
const settingsPath = path.join(ctx.envReport.home, '.claude', 'settings.json');
|
|
117
|
+
let settings = {};
|
|
118
|
+
try {
|
|
119
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
120
|
+
} catch { /* no existing settings */ }
|
|
121
|
+
|
|
122
|
+
const merged = _mergeHooks(settings, hookCommand);
|
|
123
|
+
const tmpPath = settingsPath + '.tmp';
|
|
124
|
+
fs.writeFileSync(tmpPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
|
|
125
|
+
fs.renameSync(tmpPath, settingsPath);
|
|
126
|
+
ui.ok('Claude settings.json updated with hooks');
|
|
127
|
+
|
|
128
|
+
const content = fs.readFileSync(destination, 'utf8');
|
|
129
|
+
if (content.includes('\r\n')) {
|
|
130
|
+
ui.warn('Hook script contains CRLF line endings — this may break on Unix. Consider running dos2unix.');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const allEvents = [...EVENTS_PLAIN, ...EVENTS_MATCHER];
|
|
134
|
+
return {
|
|
135
|
+
success: true,
|
|
136
|
+
hookCommand,
|
|
137
|
+
hookScriptDest: destination,
|
|
138
|
+
hooksAdded: allEvents,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
run,
|
|
144
|
+
_mergeHooks,
|
|
145
|
+
_removeHooks,
|
|
146
|
+
_alreadyRegistered,
|
|
147
|
+
EVENTS_PLAIN,
|
|
148
|
+
EVENTS_MATCHER,
|
|
149
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Copyright 2026 PNW Great Loop LLC. All rights reserved.
|
|
2
|
+
// Licensed under the Agent Recon™ Commercial License — see LICENSE-COMMERCIAL.
|
|
3
|
+
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execFileSync } = require('child_process');
|
|
9
|
+
const { confirm } = require('@inquirer/prompts');
|
|
10
|
+
const ui = require('../ui');
|
|
11
|
+
|
|
12
|
+
async function run(ctx) {
|
|
13
|
+
const wantService = await confirm({
|
|
14
|
+
message: 'Install auto-start service so Agent Recon runs on login?',
|
|
15
|
+
default: true,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (!wantService) {
|
|
19
|
+
ui.info('Skipping service installation');
|
|
20
|
+
return { success: true, skipped: true, serviceType: 'none' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const os = ctx.envReport.os;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
if (os === 'macos') {
|
|
27
|
+
return await installMacos(ctx);
|
|
28
|
+
} else if (os === 'linux') {
|
|
29
|
+
return await installLinux(ctx);
|
|
30
|
+
} else if (os === 'windows') {
|
|
31
|
+
return await installWindows(ctx);
|
|
32
|
+
} else if (os === 'wsl') {
|
|
33
|
+
return await handleWsl(ctx);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ui.warn(`Unsupported platform for service install: ${os}`);
|
|
37
|
+
return { success: true, skipped: true, serviceType: 'unknown' };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
ui.warn(`Service installation failed: ${err.message}`);
|
|
40
|
+
return { success: true, serviceType: 'failed', error: err.message };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function installMacos(ctx) {
|
|
45
|
+
const templatePath = path.join(ctx.installDir, 'service', 'com.agent-recon.server.plist');
|
|
46
|
+
let template = fs.readFileSync(templatePath, 'utf8');
|
|
47
|
+
template = template.replace(/\{\{INSTALL_DIR\}\}/g, ctx.installDir);
|
|
48
|
+
template = template.replace(/\{\{HOME\}\}/g, ctx.envReport.home);
|
|
49
|
+
|
|
50
|
+
const destDir = path.join(ctx.envReport.home, 'Library', 'LaunchAgents');
|
|
51
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
52
|
+
const destPath = path.join(destDir, 'com.agent-recon.server.plist');
|
|
53
|
+
fs.writeFileSync(destPath, template, 'utf8');
|
|
54
|
+
ui.ok(`Wrote launchd plist to ${destPath}`);
|
|
55
|
+
|
|
56
|
+
const loadNow = await confirm({ message: 'Load the service now?' });
|
|
57
|
+
if (loadNow) {
|
|
58
|
+
try {
|
|
59
|
+
execFileSync('launchctl', ['load', destPath], { stdio: 'pipe' });
|
|
60
|
+
ui.ok('Service loaded via launchctl');
|
|
61
|
+
} catch (err) {
|
|
62
|
+
ui.warn(`launchctl load failed: ${err.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { success: true, serviceType: 'launchd', servicePath: destPath };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function installLinux(ctx) {
|
|
70
|
+
const templatePath = path.join(ctx.installDir, 'service', 'agent-recon.service');
|
|
71
|
+
let template = fs.readFileSync(templatePath, 'utf8');
|
|
72
|
+
template = template.replace(/\{\{INSTALL_DIR\}\}/g, ctx.installDir);
|
|
73
|
+
|
|
74
|
+
const destDir = path.join(ctx.envReport.home, '.config', 'systemd', 'user');
|
|
75
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
76
|
+
const destPath = path.join(destDir, 'agent-recon.service');
|
|
77
|
+
fs.writeFileSync(destPath, template, 'utf8');
|
|
78
|
+
ui.ok(`Wrote systemd unit to ${destPath}`);
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
execFileSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'pipe' });
|
|
82
|
+
execFileSync('systemctl', ['--user', 'enable', 'agent-recon'], { stdio: 'pipe' });
|
|
83
|
+
ui.ok('Service enabled via systemctl --user');
|
|
84
|
+
} catch (err) {
|
|
85
|
+
ui.warn(`systemctl enable failed: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const startNow = await confirm({ message: 'Start the service now?' });
|
|
89
|
+
if (startNow) {
|
|
90
|
+
try {
|
|
91
|
+
execFileSync('systemctl', ['--user', 'start', 'agent-recon'], { stdio: 'pipe' });
|
|
92
|
+
ui.ok('Service started');
|
|
93
|
+
} catch (err) {
|
|
94
|
+
ui.warn(`systemctl start failed: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { success: true, serviceType: 'systemd', servicePath: destPath, serviceUnit: 'agent-recon.service' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function installWindows(ctx) {
|
|
102
|
+
if (!ctx.envReport.hasNssm) {
|
|
103
|
+
ui.warn('NSSM not found — install NSSM (https://nssm.cc) or configure Task Scheduler manually');
|
|
104
|
+
ui.info('To create a scheduled task: run install-service.ps1 from an admin PowerShell');
|
|
105
|
+
return { success: true, serviceType: 'manual', servicePath: null };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const serverDir = path.join(ctx.installDir, 'server');
|
|
110
|
+
const nodePath = ctx.envReport.nodePath;
|
|
111
|
+
const startJs = path.join(serverDir, 'start.js');
|
|
112
|
+
execFileSync('nssm', ['install', 'AgentRecon', nodePath, startJs], { stdio: 'pipe' });
|
|
113
|
+
execFileSync('nssm', ['set', 'AgentRecon', 'AppDirectory', serverDir], { stdio: 'pipe' });
|
|
114
|
+
execFileSync('nssm', ['set', 'AgentRecon', 'Start', 'SERVICE_AUTO_START'], { stdio: 'pipe' });
|
|
115
|
+
ui.ok('Windows service installed via NSSM');
|
|
116
|
+
|
|
117
|
+
const startNow = await confirm({ message: 'Start the service now?' });
|
|
118
|
+
if (startNow) {
|
|
119
|
+
try {
|
|
120
|
+
execFileSync('nssm', ['start', 'AgentRecon'], { stdio: 'pipe' });
|
|
121
|
+
ui.ok('Service started');
|
|
122
|
+
} catch (err) {
|
|
123
|
+
ui.warn(`Service start failed: ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { success: true, serviceType: 'nssm', servicePath: 'AgentRecon' };
|
|
128
|
+
} catch (err) {
|
|
129
|
+
ui.warn(`NSSM service creation failed: ${err.message}`);
|
|
130
|
+
return { success: true, serviceType: 'failed', error: err.message };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function handleWsl(ctx) {
|
|
135
|
+
ui.info('On WSL, the Agent Recon server should run on the Windows side');
|
|
136
|
+
ui.info('Use install-service.ps1 in a Windows PowerShell to set up the Windows service');
|
|
137
|
+
|
|
138
|
+
const checkStatus = await confirm({ message: 'Check Windows service status?', default: false });
|
|
139
|
+
if (checkStatus) {
|
|
140
|
+
try {
|
|
141
|
+
const ps = '/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe';
|
|
142
|
+
const result = execFileSync(ps, ['-NoProfile', '-Command',
|
|
143
|
+
'Get-Service AgentRecon -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Status'],
|
|
144
|
+
{ encoding: 'utf8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
145
|
+
const status = result.trim();
|
|
146
|
+
if (status) {
|
|
147
|
+
ui.ok(`Windows AgentRecon service status: ${status}`);
|
|
148
|
+
} else {
|
|
149
|
+
ui.info('AgentRecon service not found on Windows');
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
ui.info('Could not query Windows service status');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { success: true, serviceType: 'wsl-deferred' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { run };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Copyright 2026 PNW Great Loop LLC. All rights reserved.
|
|
2
|
+
// Licensed under the Agent Recon™ Commercial License — see LICENSE-COMMERCIAL.
|
|
3
|
+
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const ui = require('../ui');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Installer step — TLS / HTTPS choice.
|
|
10
|
+
*
|
|
11
|
+
* Presents three options:
|
|
12
|
+
* 1. HTTP only (default) — no encryption
|
|
13
|
+
* 2. Browser-trusted HTTPS via mkcert
|
|
14
|
+
* 3. Custom certificate — user supplies cert/key paths
|
|
15
|
+
*
|
|
16
|
+
* Returns { success, tlsChoice, tlsEnabled, tlsMode, tlsCertPath?, tlsKeyPath? }
|
|
17
|
+
*/
|
|
18
|
+
async function run(ctx) {
|
|
19
|
+
const { select } = require('@inquirer/prompts');
|
|
20
|
+
const choice = await select({
|
|
21
|
+
message: 'How would you like to secure the dashboard?',
|
|
22
|
+
choices: [
|
|
23
|
+
{ name: 'HTTP only — localhost traffic, no encryption needed', value: 'http' },
|
|
24
|
+
{ name: 'Browser-trusted HTTPS — uses mkcert (one-time admin prompt)', value: 'mkcert' },
|
|
25
|
+
{ name: 'Custom certificate — provide your own CA-signed cert/key paths', value: 'custom' },
|
|
26
|
+
],
|
|
27
|
+
default: ctx.tlsPreselect || 'http',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (choice === 'http') {
|
|
31
|
+
ui.info('Dashboard will run on HTTP (http://localhost:3131)');
|
|
32
|
+
return { success: true, tlsChoice: 'http', tlsEnabled: false, tlsMode: 'mkcert' };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (choice === 'mkcert') {
|
|
36
|
+
return await _handleMkcert();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// custom
|
|
40
|
+
return await _handleCustom();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function _handleMkcert() {
|
|
44
|
+
// Check if mkcert is available
|
|
45
|
+
let mkcertOk = false;
|
|
46
|
+
try {
|
|
47
|
+
const { checkMkcert } = require('../../server/tls-setup');
|
|
48
|
+
const result = checkMkcert();
|
|
49
|
+
mkcertOk = result.installed;
|
|
50
|
+
if (mkcertOk) {
|
|
51
|
+
ui.ok(`mkcert ${result.version} detected`);
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// tls-setup module not loadable — treat as not installed
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!mkcertOk) {
|
|
58
|
+
const { MKCERT_INSTALL_HINT } = require('../../server/tls-setup');
|
|
59
|
+
ui.warn('mkcert is not installed.');
|
|
60
|
+
ui.info(`Install it: ${MKCERT_INSTALL_HINT}`);
|
|
61
|
+
ui.info('Then run: mkcert -install (one-time, requires admin/sudo)');
|
|
62
|
+
ui.info('TLS will be enabled in settings — install mkcert and restart the server to activate.');
|
|
63
|
+
return {
|
|
64
|
+
success: true,
|
|
65
|
+
tlsChoice: 'mkcert',
|
|
66
|
+
tlsEnabled: true,
|
|
67
|
+
tlsMode: 'mkcert',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
ui.ok('mkcert is installed. The server will generate browser-trusted certificates on first start.');
|
|
72
|
+
ui.info('Run "mkcert -install" if you haven\'t already (one-time, requires admin/sudo).');
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
tlsChoice: 'mkcert',
|
|
76
|
+
tlsEnabled: true,
|
|
77
|
+
tlsMode: 'mkcert',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function _handleCustom() {
|
|
82
|
+
const { input } = require('@inquirer/prompts');
|
|
83
|
+
const certPath = await input({
|
|
84
|
+
message: 'Path to certificate file (PEM):',
|
|
85
|
+
validate: v => v.trim().length > 0 || 'Certificate path is required',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const keyPath = await input({
|
|
89
|
+
message: 'Path to private key file (PEM):',
|
|
90
|
+
validate: v => v.trim().length > 0 || 'Key path is required',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
ui.ok('Custom certificate paths saved. The server will use these on next start.');
|
|
94
|
+
return {
|
|
95
|
+
success: true,
|
|
96
|
+
tlsChoice: 'custom',
|
|
97
|
+
tlsEnabled: true,
|
|
98
|
+
tlsMode: 'custom',
|
|
99
|
+
tlsCertPath: certPath.trim(),
|
|
100
|
+
tlsKeyPath: keyPath.trim(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { run };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Copyright 2026 PNW Great Loop LLC. All rights reserved.
|
|
2
|
+
// Licensed under the Agent Recon™ Commercial License — see LICENSE-COMMERCIAL.
|
|
3
|
+
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const http = require('http');
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
const ui = require('../ui');
|
|
10
|
+
|
|
11
|
+
function httpPost(url, body) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const parsed = new URL(url);
|
|
14
|
+
const data = JSON.stringify(body);
|
|
15
|
+
const req = http.request({
|
|
16
|
+
hostname: parsed.hostname,
|
|
17
|
+
port: parsed.port,
|
|
18
|
+
path: parsed.pathname,
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
|
|
21
|
+
timeout: 3000,
|
|
22
|
+
}, res => {
|
|
23
|
+
let buf = '';
|
|
24
|
+
res.on('data', chunk => { buf += chunk; });
|
|
25
|
+
res.on('end', () => resolve({ status: res.statusCode, body: buf }));
|
|
26
|
+
});
|
|
27
|
+
req.on('error', reject);
|
|
28
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
29
|
+
req.write(data);
|
|
30
|
+
req.end();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function httpGet(url) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const parsed = new URL(url);
|
|
37
|
+
const req = http.get({
|
|
38
|
+
hostname: parsed.hostname,
|
|
39
|
+
port: parsed.port,
|
|
40
|
+
path: parsed.pathname,
|
|
41
|
+
timeout: 2000,
|
|
42
|
+
}, res => {
|
|
43
|
+
let buf = '';
|
|
44
|
+
res.on('data', chunk => { buf += chunk; });
|
|
45
|
+
res.on('end', () => resolve({ status: res.statusCode, body: buf }));
|
|
46
|
+
});
|
|
47
|
+
req.on('error', reject);
|
|
48
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sleep(ms) {
|
|
53
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function isServerRunning() {
|
|
57
|
+
try {
|
|
58
|
+
const res = await httpGet('http://localhost:3131/health');
|
|
59
|
+
return res.status === 200;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function run(ctx) {
|
|
66
|
+
let serverRunning = await isServerRunning();
|
|
67
|
+
|
|
68
|
+
if (!serverRunning) {
|
|
69
|
+
ui.info('Server not running — attempting to start...');
|
|
70
|
+
const serverDir = path.join(ctx.installDir, 'server');
|
|
71
|
+
try {
|
|
72
|
+
const child = spawn('node', ['start.js'], {
|
|
73
|
+
cwd: serverDir,
|
|
74
|
+
detached: true,
|
|
75
|
+
stdio: 'ignore',
|
|
76
|
+
});
|
|
77
|
+
child.unref();
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < 5; i++) {
|
|
80
|
+
await sleep(1000);
|
|
81
|
+
serverRunning = await isServerRunning();
|
|
82
|
+
if (serverRunning) break;
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
ui.warn(`Could not start server: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!serverRunning) {
|
|
90
|
+
ui.warn('Server did not start — verification skipped');
|
|
91
|
+
return { success: true, serverRunning: false, error: 'Server not reachable' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ui.ok('Server is running');
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const testPayload = {
|
|
98
|
+
session_id: 'installer-verify',
|
|
99
|
+
hook_event_name: 'SessionStart',
|
|
100
|
+
payload: { type: 'installer-test' },
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const res = await httpPost('http://localhost:3131/event', testPayload);
|
|
104
|
+
if (res.status === 200) {
|
|
105
|
+
ui.ok('Test event sent and accepted');
|
|
106
|
+
return { success: true, serverRunning: true };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
ui.warn(`Test event returned status ${res.status}`);
|
|
110
|
+
return { success: true, serverRunning: true, error: `Unexpected status: ${res.status}` };
|
|
111
|
+
} catch (err) {
|
|
112
|
+
ui.warn(`Test event failed: ${err.message}`);
|
|
113
|
+
return { success: true, serverRunning: true, error: err.message };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { run };
|