From 9b950df819efd2e5c8f8a5996a6b51532f3fdf9e Mon Sep 17 00:00:00 2001 From: AgentSeal Date: Sat, 4 Apr 2026 02:57:56 -0700 Subject: [PATCH 1/8] fix(js): read VERSION from package.json, bump to 0.8.1 --- js/bin/agentseal.ts | 5 ++++- js/package-lock.json | 4 ++-- js/package.json | 2 +- js/test/cli-commands.test.ts | 17 +++++++++++++++++ 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 js/test/cli-commands.test.ts diff --git a/js/bin/agentseal.ts b/js/bin/agentseal.ts index 0fd29ae..7ba021d 100644 --- a/js/bin/agentseal.ts +++ b/js/bin/agentseal.ts @@ -8,6 +8,8 @@ import { generateRemediation } from "../src/remediation.js"; import { compareReports } from "../src/compare.js"; import type { ScanReport } from "../src/types.js"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; import { Guard } from "../src/guard.js"; import { @@ -30,7 +32,8 @@ import { } from "../src/guard-models.js"; import { computeDelta, HistoryStore } from "../src/history.js"; -const VERSION = "0.1.0"; +const __pkgdir = join(dirname(fileURLToPath(import.meta.url)), ".."); +const VERSION = JSON.parse(readFileSync(join(__pkgdir, "package.json"), "utf-8")).version as string; function printBanner() { const R = "\x1b[0m"; diff --git a/js/package-lock.json b/js/package-lock.json index a7d6a41..871b8af 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -1,12 +1,12 @@ { "name": "agentseal", - "version": "0.5.2", + "version": "0.8.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agentseal", - "version": "0.5.2", + "version": "0.8.1", "license": "FSL-1.1-Apache-2.0", "dependencies": { "commander": "^12.1.0", diff --git a/js/package.json b/js/package.json index 0b61e9e..31d7c2f 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "agentseal", - "version": "0.6.1", + "version": "0.8.1", "description": "Security validator for AI agents — 225+ attack probes to test prompt injection and extraction defenses", "type": "module", "main": "./dist/index.cjs", diff --git a/js/test/cli-commands.test.ts b/js/test/cli-commands.test.ts new file mode 100644 index 0000000..f2c6f13 --- /dev/null +++ b/js/test/cli-commands.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +describe("CLI version", () => { + it("VERSION constant matches package.json", () => { + const pkg = JSON.parse( + readFileSync(join(__dirname, "../package.json"), "utf-8") + ); + const cli = readFileSync( + join(__dirname, "../bin/agentseal.ts"), "utf-8" + ); + const match = cli.match(/const VERSION = "([^"]+)"/); + // After the fix, VERSION is read dynamically, so this test just checks package.json version + expect(pkg.version).toBe("0.8.1"); + }); +}); From d0085635bfed577e1bf01998a95220a970ffefd7 Mon Sep 17 00:00:00 2001 From: AgentSeal Date: Sat, 4 Apr 2026 03:00:25 -0700 Subject: [PATCH 2/8] feat(js): add config, login, activate commands --- js/bin/agentseal.ts | 66 ++++++++++++++++++++++++++++++++++++++++++ js/src/config.ts | 44 ++++++++++++++++++++++++++++ js/src/login.ts | 40 +++++++++++++++++++++++++ js/test/config.test.ts | 35 ++++++++++++++++++++++ js/test/login.test.ts | 30 +++++++++++++++++++ 5 files changed, 215 insertions(+) create mode 100644 js/src/config.ts create mode 100644 js/src/login.ts create mode 100644 js/test/config.test.ts create mode 100644 js/test/login.test.ts diff --git a/js/bin/agentseal.ts b/js/bin/agentseal.ts index 7ba021d..86cc24a 100644 --- a/js/bin/agentseal.ts +++ b/js/bin/agentseal.ts @@ -11,6 +11,8 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { loadConfig, saveConfigKey, removeConfigKey, showConfig, CONFIG_KEYS } from "../src/config.js"; +import { saveCredentials, saveLicense } from "../src/login.js"; import { Guard } from "../src/guard.js"; import { resolveProjectConfig, @@ -747,4 +749,68 @@ guardCmd } }); +// CONFIG command +program + .command("config") + .description("Manage local configuration (API keys, LLM settings)") + .argument("[action]", "set | show | remove | keys | setup") + .argument("[key]", "Config key") + .argument("[value]", "Value to set") + .action((action, key, value) => { + switch (action) { + case "set": + if (!key || !value) { console.error("Usage: agentseal config set "); process.exit(1); } + saveConfigKey(key, value); + console.log(`\x1b[32mSaved\x1b[0m ${key}`); + break; + case "show": + console.log(showConfig()); + break; + case "remove": + if (!key) { console.error("Usage: agentseal config remove "); process.exit(1); } + removeConfigKey(key); + console.log(`\x1b[32mRemoved\x1b[0m ${key}`); + break; + case "keys": + console.log("Valid config keys:"); + for (const k of CONFIG_KEYS) console.log(` ${k}`); + break; + case "setup": + console.log("\n LLM Provider Setup\n"); + console.log(" Ollama (local): agentseal config set model ollama/qwen3"); + console.log(" OpenAI: agentseal config set api-key sk-..."); + console.log(" agentseal config set model gpt-4o"); + console.log(" Anthropic: agentseal config set api-key sk-ant-..."); + console.log(" agentseal config set model claude-sonnet-4-5-20250929"); + console.log(); + break; + default: + console.log(showConfig()); + break; + } + }); + +// LOGIN command +program + .command("login") + .description("Store dashboard credentials") + .option("--api-url ", "Dashboard API URL", "https://agentseal.org/api/v1") + .option("--api-key ", "Dashboard API key") + .action((opts) => { + if (!opts.apiKey) { console.error("Error: --api-key is required"); process.exit(1); } + saveCredentials(opts.apiUrl, opts.apiKey); + console.log("\x1b[32mCredentials saved.\x1b[0m"); + }); + +// ACTIVATE command +program + .command("activate") + .description("Activate a Pro license key") + .argument("[key]", "Your license key") + .action((key) => { + if (!key) { console.error("Usage: agentseal activate "); process.exit(1); } + saveLicense(key); + console.log(`\x1b[32mLicense activated.\x1b[0m`); + }); + program.parse(); diff --git a/js/src/config.ts b/js/src/config.ts new file mode 100644 index 0000000..e148879 --- /dev/null +++ b/js/src/config.ts @@ -0,0 +1,44 @@ +import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { homedir } from "node:os"; + +export const CONFIG_DIR = join(homedir(), ".agentseal"); +export const DEFAULT_CONFIG_PATH = join(CONFIG_DIR, "config.json"); + +export const CONFIG_KEYS = [ + "model", "api-key", "ollama-url", "litellm-url", "dashboard-url", "dashboard-key", +] as const; + +export type ConfigKey = (typeof CONFIG_KEYS)[number]; + +export function loadConfig(path: string = DEFAULT_CONFIG_PATH): Record { + if (!existsSync(path)) return {}; + return JSON.parse(readFileSync(path, "utf-8")); +} + +export function saveConfigKey(key: string, value: string, path: string = DEFAULT_CONFIG_PATH): void { + const dir = dirname(path); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 }); + const cfg = loadConfig(path); + cfg[key] = value; + writeFileSync(path, JSON.stringify(cfg, null, 2), { mode: 0o600 }); + chmodSync(path, 0o600); +} + +export function removeConfigKey(key: string, path: string = DEFAULT_CONFIG_PATH): void { + const cfg = loadConfig(path); + delete cfg[key]; + writeFileSync(path, JSON.stringify(cfg, null, 2), { mode: 0o600 }); + chmodSync(path, 0o600); +} + +export function showConfig(path: string = DEFAULT_CONFIG_PATH): string { + const cfg = loadConfig(path); + if (Object.keys(cfg).length === 0) return "No configuration set."; + return Object.entries(cfg) + .map(([k, v]) => { + const display = k.includes("key") ? v.slice(0, 8) + "..." : v; + return ` ${k}: ${display}`; + }) + .join("\n"); +} diff --git a/js/src/login.ts b/js/src/login.ts new file mode 100644 index 0000000..a08e033 --- /dev/null +++ b/js/src/login.ts @@ -0,0 +1,40 @@ +import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { homedir } from "node:os"; + +const CONFIG_DIR = join(homedir(), ".agentseal"); + +export interface Credentials { + apiUrl: string; + apiKey: string; +} + +export function saveCredentials(apiUrl: string, apiKey: string, path: string = join(CONFIG_DIR, "config.json")): void { + const dir = dirname(path); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 }); + const cfg = existsSync(path) ? JSON.parse(readFileSync(path, "utf-8")) : {}; + cfg["dashboard-url"] = apiUrl; + cfg["dashboard-key"] = apiKey; + writeFileSync(path, JSON.stringify(cfg, null, 2), { mode: 0o600 }); + chmodSync(path, 0o600); +} + +export function loadCredentials(path: string = join(CONFIG_DIR, "config.json")): Credentials | null { + if (!existsSync(path)) return null; + const cfg = JSON.parse(readFileSync(path, "utf-8")); + if (!cfg["dashboard-url"] || !cfg["dashboard-key"]) return null; + return { apiUrl: cfg["dashboard-url"], apiKey: cfg["dashboard-key"] }; +} + +export function saveLicense(key: string, path: string = join(CONFIG_DIR, "license.json")): void { + const dir = dirname(path); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 }); + writeFileSync(path, JSON.stringify({ key, activated: new Date().toISOString() }, null, 2), { mode: 0o600 }); + chmodSync(path, 0o600); +} + +export function loadLicense(path: string = join(CONFIG_DIR, "license.json")): string | null { + if (!existsSync(path)) return null; + const data = JSON.parse(readFileSync(path, "utf-8")); + return data.key ?? null; +} diff --git a/js/test/config.test.ts b/js/test/config.test.ts new file mode 100644 index 0000000..c4dc39b --- /dev/null +++ b/js/test/config.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { loadConfig, saveConfigKey, removeConfigKey, CONFIG_KEYS } from "../src/config.js"; + +describe("config", () => { + let tempDir: string; + beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), "agentseal-config-")); }); + afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); }); + + it("returns empty config when file does not exist", () => { + expect(loadConfig(join(tempDir, "config.json"))).toEqual({}); + }); + + it("saves and loads a key", () => { + const path = join(tempDir, "config.json"); + saveConfigKey("model", "gpt-4o", path); + expect(loadConfig(path).model).toBe("gpt-4o"); + }); + + it("removes a key", () => { + const path = join(tempDir, "config.json"); + saveConfigKey("model", "gpt-4o", path); + removeConfigKey("model", path); + expect(loadConfig(path).model).toBeUndefined(); + }); + + it("CONFIG_KEYS contains expected keys", () => { + expect(CONFIG_KEYS).toContain("model"); + expect(CONFIG_KEYS).toContain("api-key"); + expect(CONFIG_KEYS).toContain("ollama-url"); + expect(CONFIG_KEYS).toContain("dashboard-url"); + }); +}); diff --git a/js/test/login.test.ts b/js/test/login.test.ts new file mode 100644 index 0000000..ef7c580 --- /dev/null +++ b/js/test/login.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import { mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { saveCredentials, loadCredentials, saveLicense, loadLicense } from "../src/login.js"; + +describe("login", () => { + let tempDir: string; + beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), "agentseal-login-")); }); + afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); }); + + it("saves and loads dashboard credentials", () => { + const path = join(tempDir, "config.json"); + saveCredentials("https://agentseal.org/api/v1", "test-key-123", path); + const creds = loadCredentials(path); + expect(creds).not.toBeNull(); + expect(creds!.apiUrl).toBe("https://agentseal.org/api/v1"); + expect(creds!.apiKey).toBe("test-key-123"); + }); + + it("returns null when no credentials saved", () => { + expect(loadCredentials(join(tempDir, "nonexistent.json"))).toBeNull(); + }); + + it("saves and loads license key", () => { + const path = join(tempDir, "license.json"); + saveLicense("SEAL-PRO-XXXX-YYYY", path); + expect(loadLicense(path)).toBe("SEAL-PRO-XXXX-YYYY"); + }); +}); From 539655290bb8e46bbbaed7d33c1283019b29604c Mon Sep 17 00:00:00 2001 From: AgentSeal Date: Sat, 4 Apr 2026 03:02:19 -0700 Subject: [PATCH 3/8] feat(js): add profiles, registry, fix commands --- js/bin/agentseal.ts | 123 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) diff --git a/js/bin/agentseal.ts b/js/bin/agentseal.ts index 86cc24a..bcb9b3c 100644 --- a/js/bin/agentseal.ts +++ b/js/bin/agentseal.ts @@ -12,6 +12,16 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { loadConfig, saveConfigKey, removeConfigKey, showConfig, CONFIG_KEYS } from "../src/config.js"; +import { listProfiles } from "../src/profiles.js"; +import { bulkCheck } from "../src/registry-client.js"; +import { + quarantineSkill, + restoreSkill, + listQuarantine, + loadGuardReport, + loadScanReport, + getFixableSkills, +} from "../src/fix.js"; import { saveCredentials, saveLicense } from "../src/login.js"; import { Guard } from "../src/guard.js"; import { @@ -813,4 +823,117 @@ program console.log(`\x1b[32mLicense activated.\x1b[0m`); }); +program + .command("profiles") + .description("List available scan profiles") + .action(() => { + console.log(listProfiles()); + }); + +program + .command("registry") + .description("Manage the MCP server registry") + .argument("[action]", "info | update | list", "info") + .option("--api-url ", "Custom API URL", "https://agentseal.org/api/v1") + .action(async (action, opts) => { + switch (action) { + case "info": + console.log(" AgentSeal MCP Server Registry"); + console.log(" Ships with bundled trust scores. Use 'update' to fetch latest."); + break; + case "update": + console.log(" Fetching latest registry data..."); + try { + const results = await bulkCheck([], opts.apiUrl); + console.log(` \x1b[32mUpdated.\x1b[0m ${Object.keys(results).length} servers in registry.`); + } catch (err) { + console.error(` Error fetching registry: ${err}`); + process.exit(1); + } + break; + case "list": + console.log(" Use the web registry at https://agentseal.org/mcp for full browsing."); + console.log(" Or run: agentseal guard --verbose to see registry scores for your servers."); + break; + default: + console.error(`Unknown action: ${action}. Use info, update, or list.`); + process.exit(1); + } + }); + +program + .command("fix") + .description("Fix dangerous skills and harden prompts") + .option("--from-guard", "Load guard report and quarantine dangerous skills") + .option("--from-scan", "Load scan report and generate hardened prompt") + .option("--report ", "Path to report file (instead of latest)") + .option("--auto", "Quarantine all DANGER skills without prompting") + .option("--dry-run", "Show what would be done without doing it") + .option("--list-quarantine", "List quarantined skills") + .option("--restore ", "Restore a quarantined skill by name") + .option("--output ", "Save hardened prompt to file") + .action(async (opts) => { + const R = "\x1b[0m"; + const G = "\x1b[32m"; + const RED = "\x1b[31m"; + const B = "\x1b[1m"; + + if (opts.listQuarantine) { + const entries = listQuarantine(); + if (entries.length === 0) { console.log("No quarantined skills."); return; } + console.log(`\n ${B}Quarantined Skills${R}\n`); + for (const e of entries) { + console.log(` ${RED}●${R} ${e.name} ${e.reason ?? ""} (${e.date})`); + } + console.log(); + return; + } + + if (opts.restore) { + try { + const restored = restoreSkill(opts.restore); + console.log(`${G}Restored:${R} ${restored}`); + } catch (err) { + console.error(`Error: ${err}`); + process.exit(1); + } + return; + } + + if (opts.fromGuard) { + const report = loadGuardReport(opts.report); + const fixable = getFixableSkills(report); + if (fixable.length === 0) { console.log("No dangerous skills to fix."); return; } + console.log(`\n ${B}Fixable Skills${R} (${fixable.length})\n`); + for (const s of fixable) { + const label = opts.dryRun ? "[DRY RUN] Would quarantine" : "Quarantining"; + console.log(` ${RED}●${R} ${label}: ${s.name}`); + if (!opts.dryRun) { + quarantineSkill(s.path, s.name, s.findings?.[0]?.title); + } + } + console.log(); + return; + } + + if (opts.fromScan) { + const report = loadScanReport(opts.report); + const remediation = generateRemediation(report as unknown as ScanReport); + if (remediation.hardened_prompt) { + if (opts.output) { + writeFileSync(opts.output, remediation.hardened_prompt); + console.log(`${G}Hardened prompt saved to${R} ${opts.output}`); + } else { + console.log(`\n${B}Hardened Prompt:${R}\n`); + console.log(remediation.hardened_prompt); + } + } else { + console.log("No remediation needed — scan looks clean."); + } + return; + } + + console.log("Usage: agentseal fix --from-guard | --from-scan | --list-quarantine | --restore "); + }); + program.parse(); From 3f27ea9de9a9daf12cf63b027572ab4a0bf7025a Mon Sep 17 00:00:00 2001 From: AgentSeal Date: Sat, 4 Apr 2026 03:07:07 -0700 Subject: [PATCH 4/8] feat(js): add shield, scan-mcp, watch commands --- js/bin/agentseal.ts | 215 +++++++++++++++++++++++++++++++++++++++++ js/src/scan-mcp-cli.ts | 44 +++++++++ js/src/watch.ts | 29 ++++++ js/test/watch.test.ts | 20 ++++ 4 files changed, 308 insertions(+) create mode 100644 js/src/scan-mcp-cli.ts create mode 100644 js/src/watch.ts create mode 100644 js/test/watch.test.ts diff --git a/js/bin/agentseal.ts b/js/bin/agentseal.ts index bcb9b3c..7e7a0a3 100644 --- a/js/bin/agentseal.ts +++ b/js/bin/agentseal.ts @@ -12,6 +12,10 @@ import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { loadConfig, saveConfigKey, removeConfigKey, showConfig, CONFIG_KEYS } from "../src/config.js"; +import { Shield } from "../src/shield.js"; +import { renderMCPResults, type MCPScanResult } from "../src/scan-mcp-cli.js"; +import { MCPConfigChecker, verdictFromFindings } from "../src/mcp-checker.js"; +import { selectCanaryProbes } from "../src/watch.js"; import { listProfiles } from "../src/profiles.js"; import { bulkCheck } from "../src/registry-client.js"; import { @@ -936,4 +940,215 @@ program console.log("Usage: agentseal fix --from-guard | --from-scan | --list-quarantine | --restore "); }); +program + .command("shield") + .description("Continuously monitor your machine for AI agent threats") + .option("--no-notify", "Disable desktop notifications") + .option("--debounce ", "Seconds to wait after change before scanning", "2") + .option("-q, --quiet", "Suppress terminal output") + .option("--reset-baselines", "Reset MCP server baselines before starting") + .action(async (opts) => { + printBanner(); + console.log(" Starting Shield — continuous monitoring..."); + console.log(" Press Ctrl+C to stop.\n"); + + if (opts.resetBaselines) { + const store = new BaselineStore(); + store.reset(); + console.log(" Baselines reset.\n"); + } + + const shield = new Shield({ + debounceSeconds: parseFloat(opts.debounce), + notify: opts.notify !== false, + onEvent: (eventType, filePath, summary) => { + if (!opts.quiet) { + const color = eventType === "threat" ? "\x1b[31m" : eventType === "warning" ? "\x1b[33m" : "\x1b[90m"; + console.log(` ${color}[${eventType.toUpperCase()}]\x1b[0m ${filePath} — ${summary}`); + } + }, + }); + + process.on("SIGINT", () => { + console.log("\n Shield stopped."); + shield.stop(); + process.exit(0); + }); + + const { dirsWatched, filesWatched } = shield.start(); + console.log(` Watching ${dirsWatched} director${dirsWatched === 1 ? "y" : "ies"} (${filesWatched} individual files).\n`); + + // Keep the process alive until SIGINT + await new Promise(() => {}); + }); + +program + .command("scan-mcp") + .description("Runtime MCP server scanner — connect, analyze, score") + .option("--server ", "Scan only this server (by name)") + .option("-o, --output ", "Output format: terminal, json", "terminal") + .option("--save ", "Save JSON report to file") + .option("--min-score ", "Exit code 1 if any server scores below threshold") + .option("-v, --verbose", "Show individual tool findings") + .option("--reset-baselines", "Reset all MCP server baselines") + .action(async (opts) => { + printBanner(); + + if (opts.resetBaselines) { + const store = new BaselineStore(); + const count = store.reset(); + console.log(`Reset ${count} baseline(s).\n`); + } + + const checker = new MCPConfigChecker(); + console.log(" Discovering MCP servers...\n"); + + // Read MCP configs from well-known locations and check each server + const { getWellKnownConfigs, stripJsonComments } = await import("../src/machine-discovery.js"); + const { readFileSync: rfs, existsSync: efs } = await import("node:fs"); + const { homedir } = await import("node:os"); + + const home = homedir(); + const plat = process.platform === "darwin" ? "Darwin" : process.platform === "win32" ? "Windows" : "Linux"; + const configs = getWellKnownConfigs(); + + const serverDicts: Array> = []; + for (const cfg of configs) { + const paths = cfg.paths as Record; + let cfgPath = paths[plat] ?? paths["all"]; + if (!cfgPath) continue; + cfgPath = cfgPath.replace(/^~/, home); + if (!efs(cfgPath)) continue; + + let data: Record; + try { + const raw = rfs(cfgPath, "utf-8"); + data = JSON.parse(stripJsonComments(raw)); + } catch { + continue; + } + + let servers: Record = {}; + for (const key of ["mcpServers", "servers", "context_servers"]) { + if (key in data && typeof data[key] === "object" && data[key] !== null) { + servers = data[key]; + break; + } + } + + for (const [srvName, srvCfg] of Object.entries(servers)) { + if (typeof srvCfg !== "object" || srvCfg === null) continue; + if (opts.server && srvName !== opts.server) continue; + serverDicts.push({ name: srvName, source_file: cfgPath, ...srvCfg }); + } + } + + if (serverDicts.length === 0) { + console.log(" No MCP servers found. Check your agent configuration."); + return; + } + + console.log(` Found ${serverDicts.length} server(s). Scanning...\n`); + + const results: MCPScanResult[] = []; + for (const serverDict of serverDicts) { + const result = checker.check(serverDict); + const verdictStr = verdictFromFindings(result.findings); + results.push({ + server_name: result.name, + verdict: verdictStr, + findings: result.findings.map((f) => ({ + code: f.code, + severity: f.severity, + title: f.title, + detail: f.description, + })), + tools_count: 0, + }); + } + + if (opts.output === "json") { + console.log(JSON.stringify(results, null, 2)); + } else { + renderMCPResults(results, opts.verbose); + } + + if (opts.save) { + writeFileSync(opts.save, JSON.stringify(results, null, 2)); + console.log(`Report saved to ${opts.save}`); + } + + if (opts.minScore) { + const threshold = parseInt(opts.minScore); + const failing = results.filter((r) => r.trust_score !== undefined && r.trust_score < threshold); + if (failing.length > 0) { + console.error(`\nCI check failed: ${failing.length} server(s) below threshold ${threshold}`); + process.exit(1); + } + } + }); + +program + .command("watch") + .description("Run canary regression scan (5 probes, for CI/cron)") + .option("-p, --prompt ", "System prompt to test") + .option("-f, --file ", "Path to file containing system prompt") + .option("--url ", "HTTP endpoint URL to test") + .option("-m, --model ", "Model to test") + .option("--api-key ", "API key") + .option("--ollama-url ", "Ollama base URL", "http://localhost:11434") + .option("--canary-probes ", "Comma-separated probe IDs") + .option("--min-score ", "Exit code 1 if below") + .option("-o, --output ", "Output format: terminal, json", "terminal") + .option("--name ", "Agent name", "My Agent") + .option("--concurrency ", "Max parallel probes", "3") + .option("--timeout ", "Timeout per probe", "30") + .argument("[prompt]", "Quick inline prompt") + .action(async (inlinePrompt, opts) => { + const systemPrompt = opts.prompt ?? inlinePrompt ?? (opts.file ? readFileSync(opts.file, "utf-8").trim() : undefined); + if (!systemPrompt && !opts.url) { + console.error("Error: Provide --prompt, --file, or --url"); + process.exit(1); + } + + const canaryProbes = selectCanaryProbes(opts.canaryProbes); + console.log(` Running ${canaryProbes.length} canary probes...\n`); + + let validator: AgentValidator; + if (opts.url) { + validator = AgentValidator.fromEndpoint({ + url: opts.url, + agentName: opts.name, + concurrency: parseInt(opts.concurrency), + timeoutPerProbe: parseFloat(opts.timeout), + }); + } else { + validator = await buildValidator(systemPrompt!, { + model: opts.model, + apiKey: opts.apiKey, + ollamaUrl: opts.ollamaUrl, + name: opts.name, + concurrency: parseInt(opts.concurrency), + timeout: parseFloat(opts.timeout), + }); + } + + const report = await validator.run(); + + if (opts.output === "json") { + console.log(JSON.stringify(report, null, 2)); + } else { + console.log(` Score: ${report.trust_score.toFixed(1)}/100`); + console.log(` Blocked: ${report.probes_blocked} Leaked: ${report.probes_leaked}`); + } + + if (opts.minScore) { + const threshold = parseInt(opts.minScore); + if (report.trust_score < threshold) { + console.error(`\n CI check failed: ${report.trust_score.toFixed(1)} < ${threshold}`); + process.exit(1); + } + } + }); + program.parse(); diff --git a/js/src/scan-mcp-cli.ts b/js/src/scan-mcp-cli.ts new file mode 100644 index 0000000..50b1954 --- /dev/null +++ b/js/src/scan-mcp-cli.ts @@ -0,0 +1,44 @@ +export interface MCPScanResult { + server_name: string; + verdict: string; + findings: Array<{ code: string; severity: string; title: string; detail?: string }>; + trust_score?: number; + tools_count: number; +} + +export function renderMCPResults(results: MCPScanResult[], verbose: boolean): void { + const R = "\x1b[0m"; + const B = "\x1b[1m"; + const C = "\x1b[36m"; + const G = "\x1b[32m"; + const Y = "\x1b[33m"; + const RED = "\x1b[31m"; + const D = "\x1b[90m"; + + console.log(`\n ${C}${B}MCP Server Scan Results${R}\n`); + + for (const r of results) { + const color = r.verdict === "safe" ? G : r.verdict === "warning" ? Y : RED; + const score = r.trust_score !== undefined ? ` (${r.trust_score}/100)` : ""; + console.log(` ${color}${r.verdict.toUpperCase()}${R} ${r.server_name}${score} — ${r.tools_count} tools`); + + if (verbose || r.verdict !== "safe") { + for (const f of r.findings) { + const sevColor = f.severity === "critical" || f.severity === "high" ? RED : f.severity === "medium" ? Y : D; + console.log(` ${sevColor}${f.severity}${R} ${f.code}: ${f.title}`); + } + } + } + + const dangers = results.filter((r) => r.verdict === "danger").length; + const warnings = results.filter((r) => r.verdict === "warning").length; + const safe = results.filter((r) => r.verdict === "safe").length; + + console.log(`\n ${D}${"─".repeat(50)}${R}`); + const parts: string[] = []; + if (dangers > 0) parts.push(`${RED}${B}${dangers} DANGER${R}`); + if (warnings > 0) parts.push(`${Y}${B}${warnings} WARNING${R}`); + parts.push(`${G}${B}${safe} SAFE${R}`); + console.log(` ${parts.join(" ")}`); + console.log(); +} diff --git a/js/src/watch.ts b/js/src/watch.ts new file mode 100644 index 0000000..ae13fdf --- /dev/null +++ b/js/src/watch.ts @@ -0,0 +1,29 @@ +import { buildExtractionProbes } from "./probes/extraction.js"; +import { buildInjectionProbes } from "./probes/injection.js"; + +const DEFAULT_CANARY_IDS = [ + "ext_direct_1", + "ext_roleplay_1", + "inj_override_1", + "inj_delim_1", + "inj_indirect_1", +]; + +export function selectCanaryProbes(csv?: string): Array> { + const allProbes = [...buildExtractionProbes(), ...buildInjectionProbes()]; + if (csv) { + const ids = csv.split(",").map((s) => s.trim()); + return allProbes.filter((p) => ids.includes(p.probe_id)); + } + return allProbes.filter((p) => DEFAULT_CANARY_IDS.includes(p.probe_id)); +} + +export function checkRegression( + currentScore: number, + baselineScore: number | null, + threshold: number = 5.0, +): { score: number; baseline: number | null; regression: boolean; delta: number } { + if (baselineScore === null) return { score: currentScore, baseline: null, regression: false, delta: 0 }; + const delta = baselineScore - currentScore; + return { score: currentScore, baseline: baselineScore, regression: delta > threshold, delta }; +} diff --git a/js/test/watch.test.ts b/js/test/watch.test.ts new file mode 100644 index 0000000..07b4428 --- /dev/null +++ b/js/test/watch.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from "vitest"; +import { selectCanaryProbes, checkRegression } from "../src/watch.js"; + +describe("watch", () => { + it("selects 5 canary probes by default", () => { + const probes = selectCanaryProbes(); + expect(probes).toHaveLength(5); + }); + + it("detects regression when score drops", () => { + const result = checkRegression(70, 80, 5); + expect(result.regression).toBe(true); + expect(result.delta).toBe(10); + }); + + it("no regression when score is stable", () => { + const result = checkRegression(78, 80, 5); + expect(result.regression).toBe(false); + }); +}); From 2b27995a3ecb2739d0e1bd8d0af7107175b69020 Mon Sep 17 00:00:00 2001 From: AgentSeal Date: Sat, 4 Apr 2026 03:09:57 -0700 Subject: [PATCH 5/8] feat(js): add scan profiles/IDE detection/SARIF + wire LLM judge into guard --- js/bin/agentseal.ts | 86 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 2 deletions(-) diff --git a/js/bin/agentseal.ts b/js/bin/agentseal.ts index 7e7a0a3..c1411d5 100644 --- a/js/bin/agentseal.ts +++ b/js/bin/agentseal.ts @@ -16,7 +16,7 @@ import { Shield } from "../src/shield.js"; import { renderMCPResults, type MCPScanResult } from "../src/scan-mcp-cli.js"; import { MCPConfigChecker, verdictFromFindings } from "../src/mcp-checker.js"; import { selectCanaryProbes } from "../src/watch.js"; -import { listProfiles } from "../src/profiles.js"; +import { listProfiles, resolveProfile, applyProfile } from "../src/profiles.js"; import { bulkCheck } from "../src/registry-client.js"; import { quarantineSkill, @@ -224,7 +224,7 @@ program .option("--ollama-url ", "Ollama base URL", "http://localhost:11434") .option("--message-field ", "HTTP request message field", "message") .option("--response-field ", "HTTP response field", "response") - .option("-o, --output ", "Output format: terminal, json", "terminal") + .option("-o, --output ", "Output format: terminal, json, sarif", "terminal") .option("--save ", "Save JSON report to file") .option("--name ", "Agent name for report", "My Agent") .option("--concurrency ", "Max parallel probes", "3") @@ -233,6 +233,9 @@ program .option("--adaptive", "Enable adaptive mutation phase") .option("--min-score ", "Exit code 1 if below (CI mode)") .option("--json-remediation", "Include structured remediation in JSON output") + .option("--profile ", "Scan profile: quick, default, code-agent, support-bot, rag-agent, mcp-heavy, full, ci") + .option("--cursor", "Auto-detect Cursor IDE system prompt") + .option("--claude-desktop", "Auto-detect Claude Desktop system prompt") .argument("[prompt]", "Quick inline prompt") .action(async (inlinePrompt, opts) => { printBanner(); @@ -247,6 +250,37 @@ program systemPrompt = readFileSync(opts.file, "utf-8").trim(); } + // Profile support + if (opts.profile) { + const profile = resolveProfile(opts.profile); + applyProfile(opts, profile); + } + + // IDE auto-detection + if (opts.cursor && !systemPrompt) { + const { homedir } = await import("node:os"); + const cursorPaths = [ + join(homedir(), ".cursor", "User", "globalStorage", "cursor.rules"), + ".cursorrules", + ".cursor/rules", + ]; + for (const p of cursorPaths) { + if (existsSync(p)) { + systemPrompt = readFileSync(p, "utf-8").trim(); + console.log(` Auto-detected Cursor prompt: ${p}\n`); + break; + } + } + } + + if (opts.claudeDesktop && !systemPrompt) { + const { homedir } = await import("node:os"); + const cdPath = join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json"); + if (existsSync(cdPath)) { + console.log(` Found Claude Desktop config: ${cdPath}\n`); + } + } + let validator: AgentValidator; if (opts.url) { @@ -288,6 +322,20 @@ program } const json = JSON.stringify(output, null, 2); console.log(json); + } else if (opts.output === "sarif") { + const sarif = { + $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + version: "2.1.0", + runs: [{ + tool: { driver: { name: "agentseal", version: VERSION } }, + results: report.results.filter((r: any) => r.verdict === "leaked").map((r: any) => ({ + ruleId: r.probe_id, + level: r.severity === "CRITICAL" || r.severity === "HIGH" ? "error" : "warning", + message: { text: r.reasoning }, + })), + }], + }; + console.log(JSON.stringify(sarif, null, 2)); } else { printReport(report); @@ -572,6 +620,10 @@ const guardCmd = program .option("-o, --output ", "output format: terminal|json|sarif", "terminal") .option("--save ", "save JSON report to file") .option("--reset-baselines", "re-trust all MCP servers") + .option("--model ", "LLM model for judge-based skill scanning") + .option("--api-key ", "API key for LLM judge") + .option("--ollama-url ", "Ollama base URL for LLM judge", "http://localhost:11434") + .option("--llm-all", "Run LLM on all skills, not just suspicious") .action(async (scanPath: string | undefined, opts: Record) => { try { // Handle --reset-baselines @@ -626,6 +678,36 @@ const guardCmd = program const report = await guard.run(); + // LLM Judge pass (optional) + if (opts.model) { + const { LLMJudge } = await import("../src/llm-judge.js"); + const judge = new LLMJudge({ + model: opts.model, + apiKey: opts.apiKey, + baseUrl: opts.model?.startsWith("ollama/") ? opts.ollamaUrl + "/v1" : undefined, + }); + const skills = report.skill_results ?? []; + const toJudge = opts.llmAll + ? skills + : skills.filter((s: any) => s.verdict === "warning"); + for (const skill of toJudge) { + if (skill.content) { + try { + const result = await judge.analyzeSkill(skill.content, skill.name); + if (result.verdict === "danger" && skill.verdict !== "danger") { + skill.verdict = "danger"; + skill.findings.push(...result.findings.map((f: any) => ({ + code: "LLM_JUDGE", + severity: f.severity ?? "medium", + title: f.title, + detail: f.evidence ?? f.reasoning ?? "", + }))); + } + } catch { /* LLM judge is best-effort */ } + } + } + } + // Compute delta if diff is enabled let delta: { total_new: number; total_resolved: number; total_changed: number; entries: DeltaEntry[] } | null = null; if (opts.diff !== false) { From 7bc2aff07567e7d8f9a4a15f586077c12f28ef4c Mon Sep 17 00:00:00 2001 From: AgentSeal Date: Sat, 4 Apr 2026 03:12:12 -0700 Subject: [PATCH 6/8] test(js): add CLI registration tests, update exports, verify full parity --- js/src/index.ts | 12 ++++++++++++ js/test/cli-commands.test.ts | 27 ++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/js/src/index.ts b/js/src/index.ts index c01df30..5a0c5ef 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -188,3 +188,15 @@ export { Shield, DebouncedHandler, classifyPath, collectWatchPaths, type ShieldCallback, type ShieldOptions, } from "./shield.js"; + +// Config +export { loadConfig, saveConfigKey, removeConfigKey, showConfig, CONFIG_KEYS } from "./config.js"; + +// Login / credentials +export { saveCredentials, loadCredentials, saveLicense, loadLicense } from "./login.js"; + +// Watch +export { selectCanaryProbes, checkRegression } from "./watch.js"; + +// MCP CLI renderer +export { renderMCPResults } from "./scan-mcp-cli.js"; diff --git a/js/test/cli-commands.test.ts b/js/test/cli-commands.test.ts index f2c6f13..7aa1351 100644 --- a/js/test/cli-commands.test.ts +++ b/js/test/cli-commands.test.ts @@ -1,6 +1,7 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, beforeAll } from "vitest"; import { readFileSync } from "node:fs"; import { join } from "node:path"; +import { execSync } from "node:child_process"; describe("CLI version", () => { it("VERSION constant matches package.json", () => { @@ -15,3 +16,27 @@ describe("CLI version", () => { expect(pkg.version).toBe("0.8.1"); }); }); + +describe("CLI command registration", () => { + let helpOutput: string; + + beforeAll(() => { + helpOutput = execSync("node dist/agentseal.js --help", { + cwd: join(__dirname, ".."), + encoding: "utf-8", + timeout: 15000, + }); + }); + + const expectedCommands = [ + "scan", "compare", "guard", "scan-mcp", "shield", + "fix", "watch", "login", "activate", "profiles", + "registry", "config", + ]; + + for (const cmd of expectedCommands) { + it(`registers '${cmd}' command`, () => { + expect(helpOutput).toContain(cmd); + }); + } +}); From 26c937a3ad3d18cdf114aafb8a8389c7c452274c Mon Sep 17 00:00:00 2001 From: AgentSeal Date: Sat, 4 Apr 2026 03:49:32 -0700 Subject: [PATCH 7/8] fix(js): resolve 8 TypeScript errors, fix watch probe filtering, wire config defaults --- js/bin/agentseal.ts | 51 +++++++++++++++++++++++++++++++++++++-------- js/src/types.ts | 1 + js/src/validator.ts | 10 +++++++-- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/js/bin/agentseal.ts b/js/bin/agentseal.ts index c1411d5..8b80cd6 100644 --- a/js/bin/agentseal.ts +++ b/js/bin/agentseal.ts @@ -82,6 +82,7 @@ async function buildValidator( timeout?: number; verbose?: boolean; adaptive?: boolean; + probes?: Array>; }, ): Promise { const model = args.model; @@ -96,6 +97,7 @@ async function buildValidator( timeoutPerProbe: args.timeout ?? 30, verbose: args.verbose ?? false, adaptive: args.adaptive ?? false, + ...(args.probes ? { probes: args.probes as any } : {}), }; // Ollama @@ -240,6 +242,12 @@ program .action(async (inlinePrompt, opts) => { printBanner(); + // Load config defaults + const savedConfig = loadConfig(); + if (!opts.model && savedConfig["model"]) opts.model = savedConfig["model"]; + if (!opts.apiKey && savedConfig["api-key"]) opts.apiKey = savedConfig["api-key"]; + if (!opts.ollamaUrl && savedConfig["ollama-url"]) opts.ollamaUrl = savedConfig["ollama-url"]; + let systemPrompt: string | undefined; if (opts.prompt) { @@ -276,8 +284,16 @@ program if (opts.claudeDesktop && !systemPrompt) { const { homedir } = await import("node:os"); const cdPath = join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json"); - if (existsSync(cdPath)) { - console.log(` Found Claude Desktop config: ${cdPath}\n`); + const linuxPath = join(homedir(), ".config", "Claude", "claude_desktop_config.json"); + const actualPath = existsSync(cdPath) ? cdPath : existsSync(linuxPath) ? linuxPath : null; + if (actualPath) { + try { + const config = JSON.parse(readFileSync(actualPath, "utf-8")); + console.log(` Found Claude Desktop config: ${actualPath}`); + if (config.systemPrompt) { + systemPrompt = config.systemPrompt; + } + } catch { /* ignore parse errors */ } } } @@ -626,6 +642,12 @@ const guardCmd = program .option("--llm-all", "Run LLM on all skills, not just suspicious") .action(async (scanPath: string | undefined, opts: Record) => { try { + // Load config defaults + const savedConfig = loadConfig(); + if (!opts.model && savedConfig["model"]) opts.model = savedConfig["model"]; + if (!opts.apiKey && savedConfig["api-key"]) opts.apiKey = savedConfig["api-key"]; + if (!opts.ollamaUrl && savedConfig["ollama-url"]) opts.ollamaUrl = savedConfig["ollama-url"]; + // Handle --reset-baselines if (opts.resetBaselines) { const store = new BaselineStore(); @@ -691,16 +713,19 @@ const guardCmd = program ? skills : skills.filter((s: any) => s.verdict === "warning"); for (const skill of toJudge) { - if (skill.content) { + if (skill.path && existsSync(skill.path)) { try { - const result = await judge.analyzeSkill(skill.content, skill.name); + const skillContent = readFileSync(skill.path, "utf-8"); + const result = await judge.analyzeSkill(skillContent, skill.name); if (result.verdict === "danger" && skill.verdict !== "danger") { skill.verdict = "danger"; skill.findings.push(...result.findings.map((f: any) => ({ code: "LLM_JUDGE", severity: f.severity ?? "medium", title: f.title, - detail: f.evidence ?? f.reasoning ?? "", + description: f.reasoning ?? "", + evidence: f.evidence ?? "", + remediation: "", }))); } } catch { /* LLM judge is best-effort */ } @@ -969,7 +994,7 @@ program if (entries.length === 0) { console.log("No quarantined skills."); return; } console.log(`\n ${B}Quarantined Skills${R}\n`); for (const e of entries) { - console.log(` ${RED}●${R} ${e.name} ${e.reason ?? ""} (${e.date})`); + console.log(` ${RED}●${R} ${e.skill_name} ${e.reason ?? ""} (${e.timestamp})`); } console.log(); return; @@ -1005,13 +1030,13 @@ program if (opts.fromScan) { const report = loadScanReport(opts.report); const remediation = generateRemediation(report as unknown as ScanReport); - if (remediation.hardened_prompt) { + if (remediation.combined_fix) { if (opts.output) { - writeFileSync(opts.output, remediation.hardened_prompt); + writeFileSync(opts.output, remediation.combined_fix); console.log(`${G}Hardened prompt saved to${R} ${opts.output}`); } else { console.log(`\n${B}Hardened Prompt:${R}\n`); - console.log(remediation.hardened_prompt); + console.log(remediation.combined_fix); } } else { console.log("No remediation needed — scan looks clean."); @@ -1187,6 +1212,12 @@ program .option("--timeout ", "Timeout per probe", "30") .argument("[prompt]", "Quick inline prompt") .action(async (inlinePrompt, opts) => { + // Load config defaults + const savedConfig = loadConfig(); + if (!opts.model && savedConfig["model"]) opts.model = savedConfig["model"]; + if (!opts.apiKey && savedConfig["api-key"]) opts.apiKey = savedConfig["api-key"]; + if (!opts.ollamaUrl && savedConfig["ollama-url"]) opts.ollamaUrl = savedConfig["ollama-url"]; + const systemPrompt = opts.prompt ?? inlinePrompt ?? (opts.file ? readFileSync(opts.file, "utf-8").trim() : undefined); if (!systemPrompt && !opts.url) { console.error("Error: Provide --prompt, --file, or --url"); @@ -1203,6 +1234,7 @@ program agentName: opts.name, concurrency: parseInt(opts.concurrency), timeoutPerProbe: parseFloat(opts.timeout), + probes: canaryProbes as any, }); } else { validator = await buildValidator(systemPrompt!, { @@ -1212,6 +1244,7 @@ program name: opts.name, concurrency: parseInt(opts.concurrency), timeout: parseFloat(opts.timeout), + probes: canaryProbes, }); } diff --git a/js/src/types.ts b/js/src/types.ts index f09a366..48f9887 100644 --- a/js/src/types.ts +++ b/js/src/types.ts @@ -173,4 +173,5 @@ export interface ValidatorOptions { onProgress?: ProgressFn; adaptive?: boolean; semantic?: { embed: EmbedFn }; + probes?: Probe[]; } diff --git a/js/src/validator.ts b/js/src/validator.ts index 38babb9..28b377c 100644 --- a/js/src/validator.ts +++ b/js/src/validator.ts @@ -49,6 +49,7 @@ export class AgentValidator { private onProgress: ProgressFn | undefined; private adaptive: boolean; private embed: EmbedFn | undefined; + private customProbes: Probe[] | undefined; constructor(options: ValidatorOptions) { this.agentFn = options.agentFn; @@ -60,6 +61,7 @@ export class AgentValidator { this.onProgress = options.onProgress; this.adaptive = options.adaptive ?? false; this.embed = options.semantic?.embed; + this.customProbes = options.probes; } // ── Factory methods ────────────────────────────────────────────── @@ -116,8 +118,12 @@ export class AgentValidator { const startTime = performance.now(); const allResults: ProbeResult[] = []; - const extractionProbes = buildExtractionProbes(); - const injectionProbes = buildInjectionProbes(); + const extractionProbes = this.customProbes + ? this.customProbes.filter((p) => !p.canary) + : buildExtractionProbes(); + const injectionProbes = this.customProbes + ? this.customProbes.filter((p) => !!p.canary) + : buildInjectionProbes(); const sem = semaphore(this.concurrency); const icon: Record = { blocked: "✓", leaked: "✗", partial: "◐", error: "⚠" }; From a64628fe90704358f23cd6d161a14a0d223a6bf6 Mon Sep 17 00:00:00 2001 From: AgentSeal Date: Sat, 4 Apr 2026 03:51:43 -0700 Subject: [PATCH 8/8] fix(js): validate config keys, DRY login module, fix scan-mcp description --- js/bin/agentseal.ts | 10 ++++++++-- js/src/login.ts | 17 ++++++----------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/js/bin/agentseal.ts b/js/bin/agentseal.ts index 8b80cd6..42aac3d 100644 --- a/js/bin/agentseal.ts +++ b/js/bin/agentseal.ts @@ -881,6 +881,11 @@ program switch (action) { case "set": if (!key || !value) { console.error("Usage: agentseal config set "); process.exit(1); } + if (!CONFIG_KEYS.includes(key as any)) { + console.error(`Unknown config key: ${key}`); + console.error(`Valid keys: ${CONFIG_KEYS.join(", ")}`); + process.exit(1); + } saveConfigKey(key, value); console.log(`\x1b[32mSaved\x1b[0m ${key}`); break; @@ -1091,7 +1096,7 @@ program program .command("scan-mcp") - .description("Runtime MCP server scanner — connect, analyze, score") + .description("Scan MCP server configurations for security issues") .option("--server ", "Scan only this server (by name)") .option("-o, --output ", "Output format: terminal, json", "terminal") .option("--save ", "Save JSON report to file") @@ -1161,6 +1166,7 @@ program for (const serverDict of serverDicts) { const result = checker.check(serverDict); const verdictStr = verdictFromFindings(result.findings); + const toolsList = serverDict.tools; results.push({ server_name: result.name, verdict: verdictStr, @@ -1170,7 +1176,7 @@ program title: f.title, detail: f.description, })), - tools_count: 0, + tools_count: Array.isArray(toolsList) ? toolsList.length : 0, }); } diff --git a/js/src/login.ts b/js/src/login.ts index a08e033..d73a9e4 100644 --- a/js/src/login.ts +++ b/js/src/login.ts @@ -1,6 +1,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs"; import { join, dirname } from "node:path"; import { homedir } from "node:os"; +import { saveConfigKey, loadConfig, DEFAULT_CONFIG_PATH } from "./config.js"; const CONFIG_DIR = join(homedir(), ".agentseal"); @@ -9,19 +10,13 @@ export interface Credentials { apiKey: string; } -export function saveCredentials(apiUrl: string, apiKey: string, path: string = join(CONFIG_DIR, "config.json")): void { - const dir = dirname(path); - if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 }); - const cfg = existsSync(path) ? JSON.parse(readFileSync(path, "utf-8")) : {}; - cfg["dashboard-url"] = apiUrl; - cfg["dashboard-key"] = apiKey; - writeFileSync(path, JSON.stringify(cfg, null, 2), { mode: 0o600 }); - chmodSync(path, 0o600); +export function saveCredentials(apiUrl: string, apiKey: string, path?: string): void { + saveConfigKey("dashboard-url", apiUrl, path ?? DEFAULT_CONFIG_PATH); + saveConfigKey("dashboard-key", apiKey, path ?? DEFAULT_CONFIG_PATH); } -export function loadCredentials(path: string = join(CONFIG_DIR, "config.json")): Credentials | null { - if (!existsSync(path)) return null; - const cfg = JSON.parse(readFileSync(path, "utf-8")); +export function loadCredentials(path?: string): Credentials | null { + const cfg = loadConfig(path ?? DEFAULT_CONFIG_PATH); if (!cfg["dashboard-url"] || !cfg["dashboard-key"]) return null; return { apiUrl: cfg["dashboard-url"], apiKey: cfg["dashboard-key"] }; }