agent-security-lens 0.1.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.
Files changed (81) hide show
  1. package/.env.example +10 -0
  2. package/.mcp/server.json +42 -0
  3. package/CHANGELOG.md +17 -0
  4. package/LICENSE +17 -0
  5. package/PRIVACY.md +37 -0
  6. package/README.md +150 -0
  7. package/RELEASE-MANIFEST.json +449 -0
  8. package/SECURITY.md +24 -0
  9. package/apps/mcp-server/agent-security-lens-mcp.mjs +441 -0
  10. package/bin/agent-security-lens.mjs +117 -0
  11. package/data/ecosystems/agent-candidates.json +230 -0
  12. package/data/intelligence/components.json +22989 -0
  13. package/data/intelligence/security-evaluation-standard.json +221 -0
  14. package/data/recommendations/core/recommendations.json +256 -0
  15. package/data/trust/signal-taxonomy.json +107 -0
  16. package/docs/asl-agent-component-safety-standard-v0.2.md +56 -0
  17. package/examples/dot-hermes/.hermes/config.json +17 -0
  18. package/examples/dot-openclaw/.openclaw/openclaw.json +17 -0
  19. package/examples/hermes-like/.env.example +2 -0
  20. package/examples/hermes-like/config.json +37 -0
  21. package/examples/hermes-like/optional-mcps/github-tools.json +8 -0
  22. package/examples/hermes-like/skills/openclaw-imports/browser-skill/SKILL.md +8 -0
  23. package/examples/openclaw-like/.env.example +2 -0
  24. package/examples/openclaw-like/AGENTS.md +7 -0
  25. package/examples/openclaw-like/openclaw.json +28 -0
  26. package/examples/openclaw-like/workspace/skills/browser-control/SKILL.md +8 -0
  27. package/llms.txt +25 -0
  28. package/package.json +50 -0
  29. package/profiles/generic-agent/profile.json +19 -0
  30. package/profiles/hermes-like/profile.json +23 -0
  31. package/profiles/mcp-server/profile.json +18 -0
  32. package/profiles/openclaw-like/profile.json +22 -0
  33. package/profiles/skill-runtime/profile.json +19 -0
  34. package/rule-packs/core/rules.json +82 -0
  35. package/rule-packs/hermes/rules.json +44 -0
  36. package/rule-packs/mcp/rules.json +65 -0
  37. package/rule-packs/openclaw/rules.json +46 -0
  38. package/rule-packs/skills/rules.json +45 -0
  39. package/schemas/agent-install-decision.schema.json +432 -0
  40. package/schemas/agent-usage-event.schema.json +45 -0
  41. package/schemas/assessment-result.schema.json +361 -0
  42. package/schemas/comparison-result.schema.json +113 -0
  43. package/schemas/component-alternative-graph.schema.json +187 -0
  44. package/schemas/component-intelligence.schema.json +93 -0
  45. package/schemas/decision-feedback.schema.json +49 -0
  46. package/schemas/ecosystem-candidate-registry.schema.json +98 -0
  47. package/schemas/profile.schema.json +65 -0
  48. package/schemas/recommendation-pack.schema.json +114 -0
  49. package/schemas/rule-pack.schema.json +113 -0
  50. package/schemas/trust-signal-taxonomy.schema.json +68 -0
  51. package/scripts/verify-examples.mjs +121 -0
  52. package/scripts/verify-mcp-server.mjs +278 -0
  53. package/scripts/verify-registry.mjs +264 -0
  54. package/server.json +42 -0
  55. package/src/assessment/assess.mjs +108 -0
  56. package/src/assessment/discover-targets.mjs +127 -0
  57. package/src/assessment/risk-domains.mjs +83 -0
  58. package/src/assessment/summarize.mjs +57 -0
  59. package/src/core/files.mjs +74 -0
  60. package/src/intelligence/cloud-client.mjs +260 -0
  61. package/src/intelligence/component-intelligence.mjs +358 -0
  62. package/src/intelligence/decision-engine.mjs +772 -0
  63. package/src/intelligence/finding-context.mjs +180 -0
  64. package/src/intelligence/safety-score-v0.2.mjs +294 -0
  65. package/src/observations/json-observations.mjs +211 -0
  66. package/src/observations/observation-rules.mjs +157 -0
  67. package/src/profiles/load-profiles.mjs +130 -0
  68. package/src/recommendations/component-alternative-graph.mjs +94 -0
  69. package/src/recommendations/load-recommendations.mjs +17 -0
  70. package/src/recommendations/match-recommendations.mjs +79 -0
  71. package/src/report/comparison-console.mjs +71 -0
  72. package/src/report/console.mjs +103 -0
  73. package/src/report/markdown.mjs +145 -0
  74. package/src/results/compare-results.mjs +106 -0
  75. package/src/results/save-result.mjs +29 -0
  76. package/src/rules/load-rules.mjs +22 -0
  77. package/src/rules/match-rules.mjs +99 -0
  78. package/src/rules/supersedes.mjs +39 -0
  79. package/src/store/assessment-store.mjs +78 -0
  80. package/src/trust/derive-trust-signals.mjs +73 -0
  81. package/src/trust/load-trust-signals.mjs +17 -0
@@ -0,0 +1,441 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { assess } from "../../src/assessment/assess.mjs";
4
+ import { discoverTargets } from "../../src/assessment/discover-targets.mjs";
5
+ import { apiEndpoints, cloudIntelligenceEnabled, queryCloudStatus } from "../../src/intelligence/cloud-client.mjs";
6
+ import {
7
+ getResearchStatus,
8
+ recommendAlternatives,
9
+ recordUsageEvent,
10
+ reviewBeforeInstall,
11
+ submitDecisionFeedback,
12
+ submitUnknownComponent
13
+ } from "../../src/intelligence/component-intelligence.mjs";
14
+
15
+ const serverInfo = {
16
+ name: "agent-security-lens-mcp",
17
+ version: "0.1.0"
18
+ };
19
+
20
+ const tools = [
21
+ {
22
+ name: "get_install_policy",
23
+ description:
24
+ "Return the ASL machine-readable policy an autonomous agent should follow before installing MCPs, Skills, tools, prompt bundles, workflows or memory components.",
25
+ inputSchema: {
26
+ type: "object",
27
+ properties: {},
28
+ additionalProperties: false
29
+ }
30
+ },
31
+ {
32
+ name: "get_intelligence_status",
33
+ description:
34
+ "Return whether ASL MCP is using online cloud intelligence or local fallback, and whether the cloud API is reachable.",
35
+ inputSchema: {
36
+ type: "object",
37
+ properties: {},
38
+ additionalProperties: false
39
+ }
40
+ },
41
+ {
42
+ name: "review_before_install",
43
+ description:
44
+ "Query ASL intelligence before an autonomous agent installs or enables an MCP, Skill, tool, prompt bundle, workflow, memory component or install command.",
45
+ inputSchema: {
46
+ type: "object",
47
+ properties: {
48
+ component_name: { type: "string", description: "Component name, package name or display name." },
49
+ component_type: {
50
+ type: "string",
51
+ enum: ["agent", "agent-framework", "mcp", "skill", "tool", "prompt", "prompt-bundle", "workflow", "memory", "unknown"]
52
+ },
53
+ source_url: { type: "string", description: "GitHub, registry, npm, Docker or documentation URL." },
54
+ install_command: { type: "string", description: "Command the agent plans to run, such as npx/uvx/docker." },
55
+ manifest: { type: ["object", "string"], description: "MCP/Skill manifest or config snippet." },
56
+ planned_use: { type: "string", description: "What the agent wants to use this component for." },
57
+ requested_permissions: {
58
+ type: "array",
59
+ items: { type: "string" },
60
+ description: "Permissions the agent expects this component to need, such as shell, filesystem, network or browser."
61
+ },
62
+ submit_if_unknown: { type: "boolean", description: "Queue unknown components for ASL research." }
63
+ },
64
+ additionalProperties: true
65
+ }
66
+ },
67
+ {
68
+ name: "check_component",
69
+ description: "Check ASL online intelligence for a component and return an agent-readable install decision.",
70
+ inputSchema: {
71
+ type: "object",
72
+ properties: {
73
+ component_name: { type: "string" },
74
+ component_type: { type: "string" },
75
+ source_url: { type: "string" },
76
+ install_command: { type: "string" },
77
+ manifest: { type: ["object", "string"] }
78
+ },
79
+ additionalProperties: true
80
+ }
81
+ },
82
+ {
83
+ name: "recommend_alternatives",
84
+ description: "Return safer alternatives and a safe install plan for a component.",
85
+ inputSchema: {
86
+ type: "object",
87
+ properties: {
88
+ component_name: { type: "string" },
89
+ component_type: { type: "string" },
90
+ source_url: { type: "string" },
91
+ install_command: { type: "string" },
92
+ manifest: { type: ["object", "string"] }
93
+ },
94
+ additionalProperties: true
95
+ }
96
+ },
97
+ {
98
+ name: "submit_unknown_component",
99
+ description:
100
+ "Submit public metadata for an unknown component to the ASL research queue. Do not submit private file contents or secrets.",
101
+ inputSchema: {
102
+ type: "object",
103
+ properties: {
104
+ component_name: { type: "string" },
105
+ component_type: { type: "string" },
106
+ source_url: { type: "string" },
107
+ install_command: { type: "string" },
108
+ manifest: { type: ["object", "string"] }
109
+ },
110
+ additionalProperties: true
111
+ }
112
+ },
113
+ {
114
+ name: "report_install_outcome",
115
+ description:
116
+ "Report what happened after an agent followed an ASL decision. Use this to improve ASL adoption metrics and recommendation quality.",
117
+ inputSchema: {
118
+ type: "object",
119
+ properties: {
120
+ component_name: { type: "string" },
121
+ component_type: { type: "string" },
122
+ source_url: { type: "string" },
123
+ decision: { type: "string", enum: ["allow", "allow_with_restrictions", "ask_user", "avoid", "unknown"] },
124
+ outcome: {
125
+ type: "string",
126
+ enum: ["installed", "blocked", "user_approved", "user_rejected", "restriction_applied", "failed", "skipped"]
127
+ },
128
+ restriction_applied: { type: "boolean" },
129
+ alternative_used: { type: "string" },
130
+ error_summary: { type: "string" }
131
+ },
132
+ additionalProperties: true
133
+ }
134
+ },
135
+ {
136
+ name: "submit_decision_feedback",
137
+ description:
138
+ "Submit feedback about whether an ASL decision was useful, wrong, missing an alternative, or too strict. Do not include secrets or private code.",
139
+ inputSchema: {
140
+ type: "object",
141
+ properties: {
142
+ component_name: { type: "string" },
143
+ component_type: { type: "string" },
144
+ decision: { type: "string" },
145
+ feedback_type: {
146
+ type: "string",
147
+ enum: ["helpful", "too_strict", "too_permissive", "missing_component", "missing_alternative", "incorrect_risk", "other"]
148
+ },
149
+ rating: { type: "number", minimum: 1, maximum: 5 },
150
+ comment: { type: "string" }
151
+ },
152
+ additionalProperties: true
153
+ }
154
+ },
155
+ {
156
+ name: "get_research_status",
157
+ description:
158
+ "Check whether an unknown component submitted to ASL has been collected, scanned, reviewed, published, archived or is still pending.",
159
+ inputSchema: {
160
+ type: "object",
161
+ properties: {
162
+ submission_id: { type: "string" }
163
+ },
164
+ required: ["submission_id"],
165
+ additionalProperties: false
166
+ }
167
+ },
168
+ {
169
+ name: "discover_workspace",
170
+ description: "Discover Agent, MCP and Skill environments under a workspace path.",
171
+ inputSchema: {
172
+ type: "object",
173
+ properties: {
174
+ workspace_path: { type: "string" },
175
+ max_depth: { type: "number" }
176
+ },
177
+ required: ["workspace_path"],
178
+ additionalProperties: false
179
+ }
180
+ },
181
+ {
182
+ name: "scan_workspace",
183
+ description:
184
+ "Discover and assess every Agent, MCP and Skill environment under a workspace path. Use this for local context only.",
185
+ inputSchema: {
186
+ type: "object",
187
+ properties: {
188
+ workspace_path: { type: "string" },
189
+ max_depth: { type: "number" }
190
+ },
191
+ required: ["workspace_path"],
192
+ additionalProperties: false
193
+ }
194
+ }
195
+ ];
196
+
197
+ function writeMessage(message) {
198
+ process.stdout.write(`${JSON.stringify(message)}\n`);
199
+ }
200
+
201
+ function toolResult(data) {
202
+ return {
203
+ content: [
204
+ {
205
+ type: "text",
206
+ text: JSON.stringify(data, null, 2)
207
+ }
208
+ ]
209
+ };
210
+ }
211
+
212
+ async function callTool(name, args = {}) {
213
+ if (name === "get_install_policy") {
214
+ return toolResult({
215
+ schema_version: "0.1.0",
216
+ service: "AgentSecurityLens",
217
+ result_type: "agent_install_policy",
218
+ policy: {
219
+ protocol: {
220
+ decision_schema: "https://agentsecuritylens.dev/schemas/agent-install-decision.schema.json",
221
+ decision_contract_version: "asl-agent-decision-contract@0.2.0",
222
+ primary_tool: "review_before_install",
223
+ lifecycle_tools: [
224
+ "review_before_install",
225
+ "submit_unknown_component",
226
+ "get_research_status",
227
+ "report_install_outcome",
228
+ "submit_decision_feedback"
229
+ ]
230
+ },
231
+ before_install: [
232
+ "Call review_before_install before installing or enabling any MCP, Skill, tool, prompt bundle, workflow or memory component.",
233
+ "Read agent_decision_contract. If blocks_install is true, do not run the install command automatically.",
234
+ "Do not install automatically when decision is ask_user or avoid.",
235
+ "Apply every safe_install_plan step before installation when decision is allow_with_restrictions.",
236
+ "Submit unknown public components for ASL review without sending private code, secrets, tokens, cookies or personal data.",
237
+ "Prefer recommended_alternatives when decision is avoid or the user rejects the requested component."
238
+ ],
239
+ after_install: [
240
+ "Call report_install_outcome after the agent installs, blocks, skips or fails to install a component.",
241
+ "Call submit_decision_feedback when the decision was helpful, too strict, too permissive, missing an alternative or incorrect.",
242
+ "Call get_research_status before retrying an unknown component that was previously submitted."
243
+ ],
244
+ expected_component_metadata: [
245
+ "component_name",
246
+ "component_type",
247
+ "source_url",
248
+ "install_command",
249
+ "manifest"
250
+ ],
251
+ decisions: {
252
+ allow: "Install after recording exact source and version.",
253
+ allow_with_restrictions: "Apply restrictions first, then install.",
254
+ ask_user: "Ask the user before installation.",
255
+ avoid: "Do not install automatically."
256
+ },
257
+ machine_contract_fields: {
258
+ agent_decision_contract: "Boolean policy contract for automatic install, user confirmation, unknown submission, retry and reporting.",
259
+ agent_actions: "Ordered action list the agent should execute around installation.",
260
+ one_step_action: "Single highest-priority action for agents that need a compact answer.",
261
+ recommended_alternatives: "Structured alternative choices. Review each alternative with ASL before installing it.",
262
+ unknown_component: "Submission and retry instructions when ASL has no reviewed intelligence record."
263
+ }
264
+ }
265
+ });
266
+ }
267
+ if (name === "get_intelligence_status") {
268
+ const onlineMode = cloudIntelligenceEnabled();
269
+ const cloud = onlineMode ? await queryCloudStatus() : null;
270
+ return toolResult({
271
+ schema_version: "0.1.0",
272
+ service: "AgentSecurityLens",
273
+ result_type: "intelligence_status",
274
+ mode: onlineMode ? "online" : "local",
275
+ configured_api_endpoints: apiEndpoints(),
276
+ cloud: cloud
277
+ ? {
278
+ reachable: cloud.ok,
279
+ api_url: cloud.api_url,
280
+ attempted_api_urls: cloud.attempted_api_urls || [],
281
+ fallback_used: cloud.fallback_used || false,
282
+ reason: cloud.reason || null,
283
+ status: cloud.data || null
284
+ }
285
+ : null,
286
+ agent_instruction: onlineMode
287
+ ? "Use review_before_install before installing components. If cloud is unreachable, treat local fallback decisions as provisional."
288
+ : "ASL is in local mode. Treat decisions as offline fallback and do not assume full ASL intelligence coverage."
289
+ });
290
+ }
291
+ if (name === "review_before_install") {
292
+ const review = await reviewBeforeInstall(args);
293
+ if (
294
+ args.submit_if_unknown &&
295
+ (review.unknown_component?.should_submit || (review.component?.cataloged && !review.component?.reviewed))
296
+ ) {
297
+ const submission = await submitUnknownComponent(args);
298
+ if (review.unknown_component) {
299
+ review.unknown_component.submission = submission;
300
+ } else {
301
+ review.catalog_research_submission = submission;
302
+ }
303
+ }
304
+ return toolResult(review);
305
+ }
306
+ if (name === "check_component") {
307
+ return toolResult(await reviewBeforeInstall(args));
308
+ }
309
+ if (name === "recommend_alternatives") {
310
+ return toolResult(await recommendAlternatives(args));
311
+ }
312
+ if (name === "submit_unknown_component") {
313
+ return toolResult(await submitUnknownComponent(args));
314
+ }
315
+ if (name === "report_install_outcome") {
316
+ return toolResult(
317
+ await recordUsageEvent({
318
+ event_type: "install_outcome",
319
+ recorded_at: new Date().toISOString(),
320
+ ...args
321
+ })
322
+ );
323
+ }
324
+ if (name === "submit_decision_feedback") {
325
+ return toolResult(
326
+ await submitDecisionFeedback({
327
+ feedback_type: args.feedback_type || "other",
328
+ recorded_at: new Date().toISOString(),
329
+ ...args
330
+ })
331
+ );
332
+ }
333
+ if (name === "get_research_status") {
334
+ return toolResult(await getResearchStatus(args.submission_id));
335
+ }
336
+ if (name === "discover_workspace") {
337
+ return toolResult(
338
+ await discoverTargets({
339
+ workspacePath: args.workspace_path,
340
+ maxDepth: Number(args.max_depth || 4)
341
+ })
342
+ );
343
+ }
344
+ if (name === "scan_workspace") {
345
+ const discovery = await discoverTargets({
346
+ workspacePath: args.workspace_path,
347
+ maxDepth: Number(args.max_depth || 4)
348
+ });
349
+ const results = [];
350
+ for (const target of discovery.targets) {
351
+ const result = await assess({ targetPath: target.path, requestedProfile: null });
352
+ results.push({
353
+ target,
354
+ decision_summary: {
355
+ trust_score: result.summary.trust_score,
356
+ total_findings: result.summary.total_findings,
357
+ selected_profile: result.lineage.profile_selection.selected_profile,
358
+ top_risks: result.findings.slice(0, 5).map((finding) => ({
359
+ title: finding.title,
360
+ severity: finding.severity,
361
+ category: finding.category
362
+ }))
363
+ }
364
+ });
365
+ }
366
+ return toolResult({
367
+ workspace_path: discovery.workspace_path,
368
+ targets_found: discovery.targets.length,
369
+ targets_assessed: results.length,
370
+ results
371
+ });
372
+ }
373
+ throw new Error(`Unknown tool: ${name}`);
374
+ }
375
+
376
+ async function handleRequest(message) {
377
+ if (message.method === "initialize") {
378
+ return {
379
+ jsonrpc: "2.0",
380
+ id: message.id,
381
+ result: {
382
+ protocolVersion: message.params?.protocolVersion || "2024-11-05",
383
+ capabilities: {
384
+ tools: {}
385
+ },
386
+ serverInfo
387
+ }
388
+ };
389
+ }
390
+ if (message.method === "tools/list") {
391
+ return {
392
+ jsonrpc: "2.0",
393
+ id: message.id,
394
+ result: { tools }
395
+ };
396
+ }
397
+ if (message.method === "tools/call") {
398
+ const result = await callTool(message.params?.name, message.params?.arguments || {});
399
+ return {
400
+ jsonrpc: "2.0",
401
+ id: message.id,
402
+ result
403
+ };
404
+ }
405
+ if (message.method?.startsWith("notifications/")) return null;
406
+ return {
407
+ jsonrpc: "2.0",
408
+ id: message.id,
409
+ error: {
410
+ code: -32601,
411
+ message: `Method not found: ${message.method}`
412
+ }
413
+ };
414
+ }
415
+
416
+ let buffer = "";
417
+ process.stdin.setEncoding("utf8");
418
+ process.stdin.on("data", (chunk) => {
419
+ buffer += chunk;
420
+ const lines = buffer.split(/\r?\n/);
421
+ buffer = lines.pop() || "";
422
+ for (const line of lines) {
423
+ const trimmed = line.trim();
424
+ if (!trimmed) continue;
425
+ void (async () => {
426
+ try {
427
+ const response = await handleRequest(JSON.parse(trimmed));
428
+ if (response) writeMessage(response);
429
+ } catch (error) {
430
+ writeMessage({
431
+ jsonrpc: "2.0",
432
+ id: null,
433
+ error: {
434
+ code: -32000,
435
+ message: error?.message || String(error)
436
+ }
437
+ });
438
+ }
439
+ })();
440
+ }
441
+ });
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { assess } from "../src/assessment/assess.mjs";
4
+ import { renderComparisonConsole } from "../src/report/comparison-console.mjs";
5
+ import { renderConsole } from "../src/report/console.mjs";
6
+ import { renderMarkdown } from "../src/report/markdown.mjs";
7
+ import { compareAssessmentFiles } from "../src/results/compare-results.mjs";
8
+ import { saveAssessmentResult } from "../src/results/save-result.mjs";
9
+
10
+ function parseArgs(argv) {
11
+ const args = {
12
+ command: argv[2],
13
+ target: argv[3],
14
+ current: argv[4],
15
+ profile: null,
16
+ format: "console",
17
+ save: false,
18
+ outDir: null
19
+ };
20
+
21
+ const firstOptionIndex = args.command === "compare" ? 5 : 4;
22
+ for (let i = firstOptionIndex; i < argv.length; i += 1) {
23
+ const arg = argv[i];
24
+ if (arg === "--profile") {
25
+ args.profile = argv[i + 1];
26
+ i += 1;
27
+ } else if (arg === "--format") {
28
+ args.format = argv[i + 1];
29
+ i += 1;
30
+ } else if (arg === "--save") {
31
+ args.save = true;
32
+ } else if (arg === "--out-dir") {
33
+ args.outDir = argv[i + 1];
34
+ i += 1;
35
+ }
36
+ }
37
+
38
+ return args;
39
+ }
40
+
41
+ function printHelp() {
42
+ console.log(`AgentSecurityLens
43
+
44
+ Usage:
45
+ agent-security-lens assess <target> [--profile <id>] [--format console|json|markdown] [--save] [--out-dir <dir>]
46
+ agent-security-lens compare <previous-result.json> <current-result.json> [--format console|json]
47
+
48
+ Examples:
49
+ agent-security-lens assess .
50
+ agent-security-lens assess ~/.openclaw --profile openclaw-like
51
+ agent-security-lens assess . --format markdown
52
+ agent-security-lens assess . --save --out-dir ./assessment-runs
53
+ agent-security-lens compare ./old.json ./new.json
54
+ `);
55
+ }
56
+
57
+ async function main() {
58
+ const args = parseArgs(process.argv);
59
+
60
+ if (!args.command || args.command === "--help" || args.command === "-h") {
61
+ printHelp();
62
+ return;
63
+ }
64
+
65
+ if (args.command === "compare") {
66
+ if (!args.target || !args.current) {
67
+ console.error("Missing comparison result paths.");
68
+ printHelp();
69
+ process.exitCode = 1;
70
+ return;
71
+ }
72
+ const comparison = await compareAssessmentFiles(args.target, args.current);
73
+ if (args.format === "json") {
74
+ console.log(JSON.stringify(comparison, null, 2));
75
+ } else {
76
+ console.log(renderComparisonConsole(comparison));
77
+ }
78
+ return;
79
+ }
80
+
81
+ if (args.command !== "assess") {
82
+ console.error(`Unknown command: ${args.command}`);
83
+ printHelp();
84
+ process.exitCode = 1;
85
+ return;
86
+ }
87
+
88
+ if (!args.target) {
89
+ console.error("Missing target path.");
90
+ printHelp();
91
+ process.exitCode = 1;
92
+ return;
93
+ }
94
+
95
+ const result = await assess({
96
+ targetPath: args.target,
97
+ requestedProfile: args.profile
98
+ });
99
+ const saved = args.save
100
+ ? await saveAssessmentResult({ result, outDir: args.outDir })
101
+ : null;
102
+
103
+ if (args.format === "json") {
104
+ console.log(JSON.stringify(saved ? { ...result, saved } : result, null, 2));
105
+ } else if (args.format === "markdown") {
106
+ console.log(renderMarkdown(result));
107
+ if (saved) console.log(`\nSaved result: ${saved.path}`);
108
+ } else {
109
+ console.log(renderConsole(result));
110
+ if (saved) console.log(`\nSaved result: ${saved.path}`);
111
+ }
112
+ }
113
+
114
+ main().catch((error) => {
115
+ console.error(error?.stack || String(error));
116
+ process.exitCode = 1;
117
+ });