agent-security-scanner-mcp 3.11.0 → 3.13.0
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 +2 -2
- package/index.js +32 -14
- package/openclaw.plugin.json +3 -3
- package/package.json +2 -1
- package/src/cli/audit.js +10 -3
- package/src/cli/demo.js +3 -13
- package/src/cli/doctor.js +15 -13
- package/src/cli/harden.js +10 -3
- package/src/cli/init.js +11 -5
- package/src/config.js +4 -1
- package/src/daemon-client.js +3 -1
- package/src/python.js +54 -0
- package/src/tools/scan-action.js +222 -2
- package/src/tools/scan-prompt.js +34 -0
- package/src/tools/scan-skill.js +438 -80
- package/src/utils.js +71 -12
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Security scanner for AI coding agents and autonomous assistants. Scans code for
|
|
|
28
28
|
| `scan_agent_action` | Pre-execution safety check for agent actions (bash, file ops, HTTP). Returns ALLOW/WARN/BLOCK | Before running any agent-generated shell command or file operation |
|
|
29
29
|
| `scan_mcp_server` | Scan MCP server source for vulnerabilities: unicode poisoning, name spoofing, rug pull detection, manifest analysis. Returns A-F grade | When auditing or installing an MCP server |
|
|
30
30
|
| `scan_skill` | Deep security scan of an OpenClaw skill: prompt injection, AST+taint code analysis, ClawHavoc malware signatures, supply chain, rug pull. Returns A-F grade | Before installing any OpenClaw skill |
|
|
31
|
-
| `
|
|
31
|
+
| `scanner_health` | Check plugin health: engine status, daemon status, package data availability | Diagnostics and plugin status |
|
|
32
32
|
| `list_security_rules` | List available security rules and fix templates | To check rule coverage for a language |
|
|
33
33
|
|
|
34
34
|
## Quick Start
|
|
@@ -1110,7 +1110,7 @@ All MCP tools support a `verbosity` parameter to minimize context window consump
|
|
|
1110
1110
|
### v3.10.0
|
|
1111
1111
|
- **`scan_skill` Tool** — 6-layer deep security scanner for OpenClaw skills: prompt injection (59+ rules), AST+taint code analysis, ClawHavoc malware signatures, package supply chain verification, and SHA-256 rug pull detection. Returns A-F grade with hard-fail on ClawHavoc/rug pull/critical findings
|
|
1112
1112
|
- **ClawHavoc Signature Database** (`rules/clawhavoc.yaml`) — 27 rules, 121 regex patterns across 10 threat categories (reverse shells, crypto miners, info stealers, keyloggers, screen capture, DNS exfiltration, C2 beacons, OpenClaw-specific attacks, campaign patterns, exfil endpoints), mapped to MITRE ATT&CK
|
|
1113
|
-
- **OpenClaw Plugin Skeleton** — Native plugin manifest (`openclaw.plugin.json`), config loader (`~/.openclaw/scanner-config.json`), and health check endpoint (`
|
|
1113
|
+
- **OpenClaw Plugin Skeleton** — Native plugin manifest (`openclaw.plugin.json`), config loader (`~/.openclaw/scanner-config.json`), and health check endpoint (`scanner_health` MCP tool)
|
|
1114
1114
|
- **CLI**: `scan-skill <path>` command with `--baseline` flag; `audit` and `harden` stubs (experimental)
|
|
1115
1115
|
- **Security fixes**: Path containment uses `realpathSync` to prevent symlink bypass; dedup key includes `source` to prevent ClawHavoc findings from being suppressed by same-named code_analysis findings
|
|
1116
1116
|
- **Bug fix**: SQL injection concat detection now covers JavaScript (was C#-only) — single-quoted and template literal strings now detected
|
package/index.js
CHANGED
|
@@ -35,11 +35,18 @@ try {
|
|
|
35
35
|
__dirname = process.cwd();
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// Read version from package.json
|
|
39
|
+
let _pkgVersion = '0.0.0';
|
|
40
|
+
try {
|
|
41
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
|
|
42
|
+
_pkgVersion = pkg.version || '0.0.0';
|
|
43
|
+
} catch { /* fallback */ }
|
|
44
|
+
|
|
38
45
|
// Create MCP Server
|
|
39
46
|
const server = new McpServer(
|
|
40
47
|
{
|
|
41
|
-
name: "security-scanner",
|
|
42
|
-
version:
|
|
48
|
+
name: "agent-security-scanner-mcp",
|
|
49
|
+
version: _pkgVersion,
|
|
43
50
|
},
|
|
44
51
|
{
|
|
45
52
|
capabilities: {
|
|
@@ -163,7 +170,7 @@ server.tool(
|
|
|
163
170
|
// Register scan_agent_action tool
|
|
164
171
|
server.tool(
|
|
165
172
|
"scan_agent_action",
|
|
166
|
-
"Pre-execution security check for agent actions (bash, file_write, file_read, http_request, file_delete). Returns ALLOW/WARN/BLOCK.
|
|
173
|
+
"Pre-execution security check for agent actions (bash, file_write, file_read, http_request, file_delete, cron, process_spawn, git, docker). Returns ALLOW/WARN/BLOCK.",
|
|
167
174
|
scanAgentActionSchema,
|
|
168
175
|
scanAgentAction
|
|
169
176
|
);
|
|
@@ -202,17 +209,27 @@ server.tool(
|
|
|
202
209
|
// PLUGIN HEALTH CHECK
|
|
203
210
|
// ===========================================
|
|
204
211
|
|
|
212
|
+
const _healthHandler = async () => {
|
|
213
|
+
const { getHealthStatus } = await import('./src/plugin-health.js');
|
|
214
|
+
const health = await getHealthStatus();
|
|
215
|
+
return {
|
|
216
|
+
content: [{ type: "text", text: JSON.stringify(health, null, 2) }]
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
|
|
205
220
|
server.tool(
|
|
206
|
-
"
|
|
221
|
+
"scanner_health",
|
|
207
222
|
"Check plugin health: engine status, daemon status, package data availability",
|
|
208
223
|
{},
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
224
|
+
_healthHandler
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Backward-compatible alias (will be removed in a future major version)
|
|
228
|
+
server.tool(
|
|
229
|
+
"clawproof_health",
|
|
230
|
+
"Alias for scanner_health (deprecated, use scanner_health instead)",
|
|
231
|
+
{},
|
|
232
|
+
_healthHandler
|
|
216
233
|
);
|
|
217
234
|
|
|
218
235
|
// ===========================================
|
|
@@ -373,8 +390,9 @@ if (cliArgs[0] === 'init') {
|
|
|
373
390
|
});
|
|
374
391
|
} else if (cliArgs[0] === 'benchmark') {
|
|
375
392
|
// CLI mode: benchmark [--save] [--json-only] [--compare-latest] [--corpus <path>]
|
|
393
|
+
const { resolvePythonCommand, pythonArgs } = await import('./src/python.js');
|
|
376
394
|
const benchmarkPath = join(__dirname, 'benchmarks', 'benchmark_runner.py');
|
|
377
|
-
const benchArgs = [benchmarkPath];
|
|
395
|
+
const benchArgs = [...pythonArgs(), benchmarkPath];
|
|
378
396
|
|
|
379
397
|
// Pass through supported flags
|
|
380
398
|
for (let i = 1; i < cliArgs.length; i++) {
|
|
@@ -387,7 +405,7 @@ if (cliArgs[0] === 'init') {
|
|
|
387
405
|
}
|
|
388
406
|
|
|
389
407
|
try {
|
|
390
|
-
execFileSync(
|
|
408
|
+
execFileSync(resolvePythonCommand(), benchArgs, { stdio: 'inherit', timeout: 300000 });
|
|
391
409
|
} catch (err) {
|
|
392
410
|
if (err.status) process.exit(err.status);
|
|
393
411
|
console.error(`Benchmark error: ${err.message}`);
|
|
@@ -418,7 +436,7 @@ if (cliArgs[0] === 'init') {
|
|
|
418
436
|
const actionValue = cliArgs[2];
|
|
419
437
|
if (!actionType || !actionValue) {
|
|
420
438
|
console.error('Usage: agent-security-scanner-mcp scan-action <type> <value> [--verbosity minimal|compact|full]');
|
|
421
|
-
console.error('Types: bash, file_write, file_read, http_request, file_delete');
|
|
439
|
+
console.error('Types: bash, file_write, file_read, http_request, file_delete, cron, process_spawn, git, docker');
|
|
422
440
|
process.exit(1);
|
|
423
441
|
}
|
|
424
442
|
const verbosityIdx = cliArgs.indexOf('--verbosity');
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "agent-security-scanner",
|
|
3
|
-
"version": "3.
|
|
2
|
+
"name": "agent-security-scanner-mcp",
|
|
3
|
+
"version": "3.10.3",
|
|
4
4
|
"description": "Security scanner for OpenClaw: prompt injection firewall, package hallucination detection, code vulnerability scanning, auto-fix",
|
|
5
5
|
"author": "Sinewave AI",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"description": "Scan an OpenClaw skill for security threats"
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
|
-
"name": "
|
|
34
|
+
"name": "scanner_health",
|
|
35
35
|
"description": "Check plugin health status"
|
|
36
36
|
}
|
|
37
37
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-security-scanner-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.13.0",
|
|
4
4
|
"mcpName": "io.github.sinewaveai/agent-security-scanner-mcp",
|
|
5
5
|
"description": "Security scanner MCP server for AI coding agents. Prompt injection firewall, package hallucination detection (4.3M+ packages), 1000+ vulnerability rules with AST & taint analysis, auto-fix. For Claude Code, Cursor, Windsurf, Cline, OpenClaw.",
|
|
6
6
|
"main": "index.js",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"src/config.js",
|
|
92
92
|
"src/history.js",
|
|
93
93
|
"src/typosquat.js",
|
|
94
|
+
"src/python.js",
|
|
94
95
|
"src/plugin-config.js",
|
|
95
96
|
"src/plugin-health.js",
|
|
96
97
|
"openclaw.plugin.json",
|
package/src/cli/audit.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// src/cli/audit.js — OpenClaw configuration security audit (stub)
|
|
2
2
|
|
|
3
3
|
export async function runAudit(args) {
|
|
4
|
-
|
|
4
|
+
const allowStub = args.includes('--allow-stub');
|
|
5
|
+
|
|
6
|
+
console.log('\n Security Audit [NOT YET IMPLEMENTED]\n');
|
|
5
7
|
console.log(' This command will check your OpenClaw configuration for security issues.');
|
|
6
|
-
console.log('
|
|
7
|
-
console.log(' WARNING: This is an experimental stub. No actual checks are performed.\n');
|
|
8
|
+
console.log(' Implementation is in progress.\n');
|
|
8
9
|
console.log(' Planned checks (60+):');
|
|
9
10
|
console.log(' - Gateway: bind mode, auth, token strength, HTTPS, CORS');
|
|
10
11
|
console.log(' - Permissions: config files, credentials, session transcripts');
|
|
@@ -15,4 +16,10 @@ export async function runAudit(args) {
|
|
|
15
16
|
console.log(' - Plugins: unsigned, permissive, outdated');
|
|
16
17
|
console.log(' - Credentials: plaintext secrets, exposed API keys\n');
|
|
17
18
|
console.log(' OWASP ASI Top 10 mapping for all findings.\n');
|
|
19
|
+
|
|
20
|
+
if (!allowStub) {
|
|
21
|
+
console.error(' ERROR: audit is not yet implemented. No checks were performed.');
|
|
22
|
+
console.error(' Pass --allow-stub to suppress this error in CI.\n');
|
|
23
|
+
throw new Error('audit command is not yet implemented');
|
|
24
|
+
}
|
|
18
25
|
}
|
package/src/cli/demo.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join } from "path";
|
|
|
4
4
|
import { createInterface } from "readline";
|
|
5
5
|
import { dirname } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
+
import { resolvePythonCommand, pythonArgs } from "../python.js";
|
|
7
8
|
|
|
8
9
|
// Handle both ESM and CJS bundling (Smithery bundles to CJS)
|
|
9
10
|
let __dirname;
|
|
@@ -178,22 +179,11 @@ export async function runDemo(args) {
|
|
|
178
179
|
|
|
179
180
|
// Run the analyzer
|
|
180
181
|
const analyzerPath = join(__dirname, '..', '..', 'analyzer.py');
|
|
181
|
-
|
|
182
|
-
const py3 = checkCommand('python3', ['--version']);
|
|
183
|
-
if (!py3.ok) {
|
|
184
|
-
const py = checkCommand('python', ['--version']);
|
|
185
|
-
if (py.ok && py.output.includes('3.')) {
|
|
186
|
-
pythonCmd = 'python';
|
|
187
|
-
} else {
|
|
188
|
-
console.log(` Error: Python 3 not found. Run "npx agent-security-scanner-mcp doctor" to diagnose.\n`);
|
|
189
|
-
unlinkSync(filepath);
|
|
190
|
-
process.exit(1);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
182
|
+
const pythonCmd = resolvePythonCommand();
|
|
193
183
|
|
|
194
184
|
let results;
|
|
195
185
|
try {
|
|
196
|
-
const output = execFileSync(pythonCmd, [analyzerPath, filepath], { timeout: 30000, encoding: 'utf-8' });
|
|
186
|
+
const output = execFileSync(pythonCmd, [...pythonArgs(), analyzerPath, filepath], { timeout: 30000, encoding: 'utf-8' });
|
|
197
187
|
results = JSON.parse(output);
|
|
198
188
|
} catch (e) {
|
|
199
189
|
console.log(` Error running analyzer: ${e.message}\n`);
|
package/src/cli/doctor.js
CHANGED
|
@@ -4,6 +4,7 @@ import { dirname, join } from "path";
|
|
|
4
4
|
import { homedir, platform } from "os";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { getDaemonClient } from '../daemon-client.js';
|
|
7
|
+
import { resolvePythonCommand, pythonArgs } from '../python.js';
|
|
7
8
|
|
|
8
9
|
// Handle both ESM and CJS bundling (Smithery bundles to CJS)
|
|
9
10
|
let __dirname;
|
|
@@ -123,22 +124,23 @@ export async function runDoctor(args) {
|
|
|
123
124
|
issues++;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
// 2. Python 3
|
|
127
|
+
// 2. Python 3 (uses the same resolver as the runtime)
|
|
127
128
|
let pythonCmd = null;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const py = checkCommand('python', ['--version']);
|
|
134
|
-
if (py.ok && py.output.includes('3.')) {
|
|
135
|
-
pythonCmd = 'python';
|
|
136
|
-
console.log(` \u2713 ${py.output}`);
|
|
129
|
+
try {
|
|
130
|
+
pythonCmd = resolvePythonCommand();
|
|
131
|
+
const pyVer = checkCommand(pythonCmd, [...pythonArgs(), '--version']);
|
|
132
|
+
if (pyVer.ok) {
|
|
133
|
+
console.log(` \u2713 ${pyVer.output} (resolved as '${pythonCmd}${pythonArgs().length ? ' ' + pythonArgs().join(' ') : ''}')`);
|
|
137
134
|
} else {
|
|
135
|
+
pythonCmd = null;
|
|
138
136
|
console.log(` \u2717 Python 3 not found`);
|
|
139
137
|
console.log(` Install: https://python.org/downloads/`);
|
|
140
138
|
issues++;
|
|
141
139
|
}
|
|
140
|
+
} catch {
|
|
141
|
+
console.log(` \u2717 Python 3 not found`);
|
|
142
|
+
console.log(` Install: https://python.org/downloads/`);
|
|
143
|
+
issues++;
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
// 3. analyzer.py reachable
|
|
@@ -173,7 +175,7 @@ export async function runDoctor(args) {
|
|
|
173
175
|
|
|
174
176
|
// 4. Python can import yaml (analyzer dependency check)
|
|
175
177
|
if (pythonCmd && existsSync(analyzerPath)) {
|
|
176
|
-
const yamlCheck = checkCommand(pythonCmd, ['-c', 'import yaml; print("ok")']);
|
|
178
|
+
const yamlCheck = checkCommand(pythonCmd, [...pythonArgs(), '-c', 'import yaml; print("ok")']);
|
|
177
179
|
if (yamlCheck.ok && yamlCheck.output === 'ok') {
|
|
178
180
|
console.log(` \u2713 Analyzer engine ready (PyYAML installed)`);
|
|
179
181
|
} else {
|
|
@@ -184,7 +186,7 @@ export async function runDoctor(args) {
|
|
|
184
186
|
|
|
185
187
|
// 5. tree-sitter AST engine (optional but recommended)
|
|
186
188
|
if (pythonCmd) {
|
|
187
|
-
const tsCheck = checkCommand(pythonCmd, ['-c', 'import tree_sitter; print(tree_sitter.__version__)']);
|
|
189
|
+
const tsCheck = checkCommand(pythonCmd, [...pythonArgs(), '-c', 'import tree_sitter; print(tree_sitter.__version__)']);
|
|
188
190
|
if (tsCheck.ok && tsCheck.output) {
|
|
189
191
|
console.log(` \u2713 AST engine ready (tree-sitter ${tsCheck.output})`);
|
|
190
192
|
} else {
|
|
@@ -193,7 +195,7 @@ export async function runDoctor(args) {
|
|
|
193
195
|
console.log(` Installing tree-sitter dependencies...`);
|
|
194
196
|
const requirementsPath = join(__dirname, '..', '..', 'requirements.txt');
|
|
195
197
|
if (existsSync(requirementsPath)) {
|
|
196
|
-
const installResult = checkCommand(pythonCmd, ['-m', 'pip', 'install', '-r', requirementsPath, '--user', '--quiet']);
|
|
198
|
+
const installResult = checkCommand(pythonCmd, [...pythonArgs(), '-m', 'pip', 'install', '-r', requirementsPath, '--user', '--quiet']);
|
|
197
199
|
if (installResult.ok) {
|
|
198
200
|
console.log(` \u2713 Fixed: tree-sitter dependencies installed — AST engine enabled`);
|
|
199
201
|
fixed++;
|
package/src/cli/harden.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// src/cli/harden.js — OpenClaw auto-hardening (stub)
|
|
2
2
|
|
|
3
3
|
export async function runHarden(args) {
|
|
4
|
-
|
|
4
|
+
const allowStub = args.includes('--allow-stub');
|
|
5
|
+
|
|
6
|
+
console.log('\n Auto-Hardening [NOT YET IMPLEMENTED]\n');
|
|
5
7
|
console.log(' This command will automatically fix security issues in your OpenClaw config.');
|
|
6
|
-
console.log('
|
|
7
|
-
console.log(' WARNING: This is an experimental stub. No actions are performed.\n');
|
|
8
|
+
console.log(' Implementation is in progress.\n');
|
|
8
9
|
console.log(' Planned actions:');
|
|
9
10
|
console.log(' - Bind gateway to 127.0.0.1');
|
|
10
11
|
console.log(' - Enable token authentication');
|
|
@@ -12,4 +13,10 @@ export async function runHarden(args) {
|
|
|
12
13
|
console.log(' - Disable mDNS discovery');
|
|
13
14
|
console.log(' - Remove plaintext credentials\n');
|
|
14
15
|
console.log(' Usage: agent-security-scanner-mcp harden --fix [--dry-run]\n');
|
|
16
|
+
|
|
17
|
+
if (!allowStub) {
|
|
18
|
+
console.error(' ERROR: harden is not yet implemented. No actions were performed.');
|
|
19
|
+
console.error(' Pass --allow-stub to suppress this error in CI.\n');
|
|
20
|
+
throw new Error('harden command is not yet implemented');
|
|
21
|
+
}
|
|
15
22
|
}
|
package/src/cli/init.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, existsSync, writeFileSync, copyFileSync, mkdirSync } from "fs";
|
|
2
2
|
import { spawnSync } from "child_process";
|
|
3
3
|
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
4
5
|
import { homedir, platform } from "os";
|
|
5
6
|
import { createInterface } from "readline";
|
|
6
7
|
|
|
@@ -167,8 +168,13 @@ async function installOpenClawSkill(client, flags) {
|
|
|
167
168
|
const skillFile = client.configPath();
|
|
168
169
|
|
|
169
170
|
// Find the source skill file (bundled with the package)
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
let __initDir;
|
|
172
|
+
try {
|
|
173
|
+
__initDir = dirname(fileURLToPath(import.meta.url));
|
|
174
|
+
} catch {
|
|
175
|
+
__initDir = process.cwd();
|
|
176
|
+
}
|
|
177
|
+
const sourceSkill = join(__initDir, '..', '..', 'skills', 'openclaw', 'SKILL.md');
|
|
172
178
|
|
|
173
179
|
console.log(`\n Client: ${client.name}`);
|
|
174
180
|
console.log(` Skill: ${skillDir}`);
|
|
@@ -248,9 +254,9 @@ async function installCodexMCP(flags, serverName) {
|
|
|
248
254
|
console.log(` Config: ~/.codex/config.toml (managed by codex CLI)`);
|
|
249
255
|
console.log(` OS: ${platform()} (${process.arch})\n`);
|
|
250
256
|
|
|
251
|
-
// Check codex CLI is available
|
|
252
|
-
const
|
|
253
|
-
if (
|
|
257
|
+
// Check codex CLI is available (cross-platform: probe directly instead of using which/where)
|
|
258
|
+
const codexCheck = spawnSync('codex', ['--version'], { encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
259
|
+
if (codexCheck.status !== 0 || codexCheck.error) {
|
|
254
260
|
console.error(` ERROR: 'codex' CLI not found in PATH.`);
|
|
255
261
|
console.error(` Install it first: https://github.com/openai/codex\n`);
|
|
256
262
|
process.exit(1);
|
package/src/config.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { existsSync, readFileSync } from 'fs';
|
|
5
5
|
import { dirname, join, resolve, sep } from 'path';
|
|
6
6
|
import { execFileSync } from 'child_process';
|
|
7
|
+
import { resolvePythonCommand, pythonArgs } from './python.js';
|
|
7
8
|
|
|
8
9
|
const DEFAULT_CONFIG = {
|
|
9
10
|
version: 1,
|
|
@@ -86,7 +87,9 @@ function findConfigFile(startPath) {
|
|
|
86
87
|
|
|
87
88
|
function parseYaml(filePath) {
|
|
88
89
|
try {
|
|
89
|
-
const
|
|
90
|
+
const pyCmd = resolvePythonCommand();
|
|
91
|
+
const result = execFileSync(pyCmd, [
|
|
92
|
+
...pythonArgs(),
|
|
90
93
|
'-c',
|
|
91
94
|
'import yaml,json,sys; print(json.dumps(yaml.safe_load(open(sys.argv[1]))))',
|
|
92
95
|
filePath,
|
package/src/daemon-client.js
CHANGED
|
@@ -3,6 +3,7 @@ import { spawn } from 'child_process';
|
|
|
3
3
|
import { createInterface } from 'readline';
|
|
4
4
|
import { dirname, join } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
+
import { resolvePythonCommand, pythonArgs } from './python.js';
|
|
6
7
|
|
|
7
8
|
let __dirname;
|
|
8
9
|
try {
|
|
@@ -64,7 +65,8 @@ class DaemonClient {
|
|
|
64
65
|
// Cleanup any previous process
|
|
65
66
|
this._cleanup();
|
|
66
67
|
|
|
67
|
-
const
|
|
68
|
+
const pyCmd = resolvePythonCommand();
|
|
69
|
+
const proc = spawn(pyCmd, [...pythonArgs(), DAEMON_SCRIPT], {
|
|
68
70
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
69
71
|
env: { ...process.env, PYTHONUNBUFFERED: '1' },
|
|
70
72
|
});
|
package/src/python.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/python.js — Cross-platform Python command resolution.
|
|
2
|
+
// Resolves the correct Python 3 command for the current platform:
|
|
3
|
+
// Windows: py -3 → python3 → python
|
|
4
|
+
// Unix: python3 → python
|
|
5
|
+
|
|
6
|
+
import { execFileSync } from 'child_process';
|
|
7
|
+
import { platform } from 'os';
|
|
8
|
+
|
|
9
|
+
let _cached = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the Python 3 command available on this system.
|
|
13
|
+
* Caches the result for the lifetime of the process.
|
|
14
|
+
* Returns 'python3' as a last-resort default if nothing is found
|
|
15
|
+
* (so callers get a clear "command not found" error rather than silence).
|
|
16
|
+
*/
|
|
17
|
+
export function resolvePythonCommand() {
|
|
18
|
+
if (_cached !== null) return _cached;
|
|
19
|
+
|
|
20
|
+
const candidates = platform() === 'win32'
|
|
21
|
+
? [['py', ['-3', '--version']], ['python3', ['--version']], ['python', ['--version']]]
|
|
22
|
+
: [['python3', ['--version']], ['python', ['--version']]];
|
|
23
|
+
|
|
24
|
+
for (const [cmd, args] of candidates) {
|
|
25
|
+
try {
|
|
26
|
+
const out = execFileSync(cmd, args, {
|
|
27
|
+
encoding: 'utf-8',
|
|
28
|
+
timeout: 5000,
|
|
29
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
30
|
+
});
|
|
31
|
+
if (out.includes('3.')) {
|
|
32
|
+
// For `py -3`, the actual command to use at runtime is ['py', '-3']
|
|
33
|
+
_cached = cmd === 'py' ? 'py' : cmd;
|
|
34
|
+
return _cached;
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// Not available, try next
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Fallback — callers will get "command not found" with a clear error
|
|
42
|
+
_cached = 'python3';
|
|
43
|
+
return _cached;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns the argument array prefix for invoking Python.
|
|
48
|
+
* For Windows `py -3`, returns ['-3']; otherwise returns [].
|
|
49
|
+
* Use: execFileSync(resolvePythonCommand(), [...pythonArgs(), scriptPath, ...])
|
|
50
|
+
*/
|
|
51
|
+
export function pythonArgs() {
|
|
52
|
+
const cmd = resolvePythonCommand();
|
|
53
|
+
return cmd === 'py' ? ['-3'] : [];
|
|
54
|
+
}
|