@bryan-thompson/inspector-assessment-cli 1.30.0 → 1.31.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.
@@ -68,6 +68,15 @@ export function buildConfig(options) {
68
68
  if (options.temporalInvocations) {
69
69
  config.temporalInvocations = options.temporalInvocations;
70
70
  }
71
+ // Official MCP conformance testing (opt-in via --conformance flag)
72
+ // Requires HTTP/SSE transport with serverUrl - STDIO transport will skip gracefully
73
+ if (options.conformanceEnabled) {
74
+ config.assessmentCategories = {
75
+ ...config.assessmentCategories,
76
+ conformance: true,
77
+ };
78
+ console.log("🔍 Official MCP conformance testing enabled");
79
+ }
71
80
  if (options.claudeEnabled) {
72
81
  // Check for HTTP transport via --claude-http flag or environment variables
73
82
  const useHttpTransport = options.claudeHttp || process.env.INSPECTOR_CLAUDE === "true";
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Zod Schemas for Server Configuration Files
3
+ *
4
+ * Runtime validation for MCP server configuration loaded from JSON files.
5
+ * Supports both Claude Desktop config format and standalone config format.
6
+ *
7
+ * @module cli/lib/assessment-runner/server-configSchemas
8
+ *
9
+ * @remarks
10
+ * This module provides type-discriminated schemas for config FILE parsing,
11
+ * with separate schemas for each transport type (HttpSseServerConfigSchema,
12
+ * StdioServerConfigSchema). This enables type-safe handling of transport-specific
13
+ * fields.
14
+ *
15
+ * For flexible CLI argument parsing where transport may not be specified,
16
+ * see cli-parserSchemas.ts (ServerConfigSchema) which uses a single schema
17
+ * with refinement-based validation.
18
+ *
19
+ * **Design Decision (Issue #114):**
20
+ * Both schema approaches are kept because they serve different purposes:
21
+ * - server-configSchemas.ts: Type-safe discriminated unions for file parsing
22
+ * - cli-parserSchemas.ts: Flexible validation for CLI argument parsing
23
+ *
24
+ * @see cli-parserSchemas.ts for CLI argument validation
25
+ * @see sharedSchemas.ts for common schemas (TransportTypeSchema, etc.)
26
+ */
27
+ import { z } from "zod";
28
+ // Import TransportTypeSchema from shared schemas for consistency
29
+ import { TransportTypeSchema } from "../../../../client/lib/lib/assessment/sharedSchemas.js";
30
+ // Re-export for backwards compatibility
31
+ export { TransportTypeSchema };
32
+ /**
33
+ * Schema for HTTP/SSE transport server configuration.
34
+ */
35
+ export const HttpSseServerConfigSchema = z.object({
36
+ transport: z.enum(["http", "sse"]).optional(),
37
+ url: z.string().url("url must be a valid URL"),
38
+ });
39
+ /**
40
+ * Schema for stdio transport server configuration.
41
+ */
42
+ export const StdioServerConfigSchema = z.object({
43
+ transport: z.literal("stdio").optional(),
44
+ command: z.string().min(1, "command is required for stdio transport"),
45
+ args: z.array(z.string()).optional().default([]),
46
+ env: z.record(z.string()).optional().default({}),
47
+ cwd: z.string().optional(),
48
+ });
49
+ /**
50
+ * Schema for a single server entry (either transport type).
51
+ * Used within mcpServers object or as standalone config.
52
+ */
53
+ export const ServerEntrySchema = z.union([
54
+ HttpSseServerConfigSchema,
55
+ StdioServerConfigSchema,
56
+ ]);
57
+ /**
58
+ * Schema for Claude Desktop config format.
59
+ * Contains nested mcpServers object with server configurations.
60
+ */
61
+ export const ClaudeDesktopConfigSchema = z.object({
62
+ mcpServers: z.record(ServerEntrySchema).optional(),
63
+ });
64
+ /**
65
+ * Schema for standalone config file format.
66
+ * Direct server configuration without nesting.
67
+ */
68
+ export const StandaloneConfigSchema = ServerEntrySchema;
69
+ /**
70
+ * Combined schema for any valid config file format.
71
+ */
72
+ export const ConfigFileSchema = z.union([
73
+ ClaudeDesktopConfigSchema,
74
+ StandaloneConfigSchema,
75
+ ]);
76
+ /**
77
+ * Parse a config file's JSON content and validate its structure.
78
+ *
79
+ * @param jsonContent - Raw JSON object from file
80
+ * @returns Validated config file content
81
+ * @throws ZodError if validation fails
82
+ */
83
+ export function parseConfigFile(jsonContent) {
84
+ return ConfigFileSchema.parse(jsonContent);
85
+ }
86
+ /**
87
+ * Safely parse a config file without throwing.
88
+ *
89
+ * @param jsonContent - Raw JSON object from file
90
+ * @returns SafeParseResult with success status and data/error
91
+ */
92
+ export function safeParseConfigFile(jsonContent) {
93
+ return ConfigFileSchema.safeParse(jsonContent);
94
+ }
95
+ /**
96
+ * Validate a server entry matches expected structure.
97
+ *
98
+ * @param entry - Server configuration entry
99
+ * @returns Array of validation error messages (empty if valid)
100
+ */
101
+ export function validateServerEntry(entry) {
102
+ const result = ServerEntrySchema.safeParse(entry);
103
+ if (result.success) {
104
+ return [];
105
+ }
106
+ return result.error.errors.map((e) => {
107
+ const path = e.path.length > 0 ? `${e.path.join(".")}: ` : "";
108
+ return `${path}${e.message}`;
109
+ });
110
+ }
111
+ /**
112
+ * Check if a server entry is HTTP/SSE transport.
113
+ */
114
+ export function isHttpSseConfig(entry) {
115
+ return ("url" in entry || entry.transport === "http" || entry.transport === "sse");
116
+ }
117
+ /**
118
+ * Check if a server entry is stdio transport.
119
+ */
120
+ export function isStdioConfig(entry) {
121
+ return "command" in entry && !("url" in entry);
122
+ }
@@ -9,36 +9,33 @@
9
9
  * @module cli/lib/cli-parser
10
10
  */
11
11
  import { ASSESSMENT_CATEGORY_METADATA, } from "../../../client/lib/lib/assessmentTypes.js";
12
- import { ASSESSMENT_PROFILES, isValidProfileName, getProfileHelpText, TIER_1_CORE_SECURITY, TIER_2_COMPLIANCE, TIER_3_CAPABILITY, TIER_4_EXTENDED, } from "../profiles.js";
12
+ import { ASSESSMENT_PROFILES, getProfileHelpText, TIER_1_CORE_SECURITY, TIER_2_COMPLIANCE, TIER_3_CAPABILITY, TIER_4_EXTENDED, } from "../profiles.js";
13
13
  import packageJson from "../../package.json" with { type: "json" };
14
+ import { safeParseModuleNames, LogLevelSchema, ReportFormatSchema, AssessmentProfileNameSchema, } from "./cli-parserSchemas.js";
14
15
  // ============================================================================
15
16
  // Constants
16
17
  // ============================================================================
17
- // Valid module names derived from ASSESSMENT_CATEGORY_METADATA
18
+ // Valid module names derived from ASSESSMENT_CATEGORY_METADATA (used for help text)
18
19
  const VALID_MODULE_NAMES = Object.keys(ASSESSMENT_CATEGORY_METADATA);
19
20
  // ============================================================================
20
21
  // Validation Functions
21
22
  // ============================================================================
22
23
  /**
23
- * Validate module names from CLI input
24
+ * Validate module names from CLI input using Zod schema.
24
25
  *
25
26
  * @param input - Comma-separated module names
26
27
  * @param flagName - Flag name for error messages (e.g., "--skip-modules")
27
28
  * @returns Array of validated module names, or empty array if invalid
28
29
  */
29
30
  export function validateModuleNames(input, flagName) {
30
- const names = input
31
- .split(",")
32
- .map((n) => n.trim())
33
- .filter(Boolean);
34
- const invalid = names.filter((n) => !VALID_MODULE_NAMES.includes(n));
35
- if (invalid.length > 0) {
36
- console.error(`Error: Invalid module name(s) for ${flagName}: ${invalid.join(", ")}`);
31
+ const result = safeParseModuleNames(input);
32
+ if (result.invalid.length > 0) {
33
+ console.error(`Error: Invalid module name(s) for ${flagName}: ${result.invalid.join(", ")}`);
37
34
  console.error(`Valid modules: ${VALID_MODULE_NAMES.join(", ")}`);
38
35
  setTimeout(() => process.exit(1), 10);
39
36
  return [];
40
37
  }
41
- return names;
38
+ return result.valid;
42
39
  }
43
40
  /**
44
41
  * Validate parsed options for consistency and requirements
@@ -146,20 +143,14 @@ export function parseArgs(argv) {
146
143
  break;
147
144
  case "--log-level": {
148
145
  const levelValue = args[++i];
149
- const validLevels = [
150
- "silent",
151
- "error",
152
- "warn",
153
- "info",
154
- "debug",
155
- ];
156
- if (!validLevels.includes(levelValue)) {
157
- console.error(`Invalid log level: ${levelValue}. Valid options: ${validLevels.join(", ")}`);
146
+ const parseResult = LogLevelSchema.safeParse(levelValue);
147
+ if (!parseResult.success) {
148
+ console.error(`Invalid log level: ${levelValue}. Valid options: silent, error, warn, info, debug`);
158
149
  setTimeout(() => process.exit(1), 10);
159
150
  options.helpRequested = true;
160
151
  return options;
161
152
  }
162
- options.logLevel = levelValue;
153
+ options.logLevel = parseResult.data;
163
154
  break;
164
155
  }
165
156
  case "--json":
@@ -168,13 +159,14 @@ export function parseArgs(argv) {
168
159
  case "--format":
169
160
  case "-f": {
170
161
  const formatValue = args[++i];
171
- if (formatValue !== "json" && formatValue !== "markdown") {
162
+ const parseResult = ReportFormatSchema.safeParse(formatValue);
163
+ if (!parseResult.success) {
172
164
  console.error(`Invalid format: ${formatValue}. Valid options: json, markdown`);
173
165
  setTimeout(() => process.exit(1), 10);
174
166
  options.helpRequested = true;
175
167
  return options;
176
168
  }
177
- options.format = formatValue;
169
+ options.format = parseResult.data;
178
170
  break;
179
171
  }
180
172
  case "--include-policy":
@@ -201,6 +193,10 @@ export function parseArgs(argv) {
201
193
  case "--skip-temporal":
202
194
  options.skipTemporal = true;
203
195
  break;
196
+ case "--conformance":
197
+ // Enable official MCP conformance tests (requires HTTP/SSE transport with serverUrl)
198
+ options.conformanceEnabled = true;
199
+ break;
204
200
  case "--profile": {
205
201
  const profileValue = args[++i];
206
202
  if (!profileValue) {
@@ -210,14 +206,15 @@ export function parseArgs(argv) {
210
206
  options.helpRequested = true;
211
207
  return options;
212
208
  }
213
- if (!isValidProfileName(profileValue)) {
209
+ const parseResult = AssessmentProfileNameSchema.safeParse(profileValue);
210
+ if (!parseResult.success) {
214
211
  console.error(`Error: Invalid profile name: ${profileValue}`);
215
212
  console.error(`Valid profiles: ${Object.keys(ASSESSMENT_PROFILES).join(", ")}`);
216
213
  setTimeout(() => process.exit(1), 10);
217
214
  options.helpRequested = true;
218
215
  return options;
219
216
  }
220
- options.profile = profileValue;
217
+ options.profile = parseResult.data;
221
218
  break;
222
219
  }
223
220
  case "--skip-modules": {
@@ -364,6 +361,7 @@ Options:
364
361
  --profile <name> Use predefined module profile (quick, security, compliance, full)
365
362
  --temporal-invocations <n> Number of invocations per tool for rug pull detection (default: 25)
366
363
  --skip-temporal Skip temporal/rug pull testing (faster assessment)
364
+ --conformance Enable official MCP conformance tests (experimental, requires HTTP/SSE transport)
367
365
  --skip-modules <list> Skip specific modules (comma-separated)
368
366
  --only-modules <list> Run only specific modules (comma-separated)
369
367
  --json Output only JSON path (no console summary)
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Zod Schemas for CLI Argument Parsing
3
+ *
4
+ * Runtime validation schemas for CLI arguments and server configuration.
5
+ * Replaces manual validation in parseArgs() and validateArgs() functions.
6
+ *
7
+ * @module cli/lib/cli-parserSchemas
8
+ */
9
+ import { z } from "zod";
10
+ // Import shared schemas from single source of truth
11
+ import { LogLevelSchema, ReportFormatSchema, TransportTypeSchema, ZOD_SCHEMA_VERSION, } from "../../../client/lib/lib/assessment/sharedSchemas.js";
12
+ // Re-export shared schemas for backwards compatibility
13
+ export { LogLevelSchema, ReportFormatSchema, TransportTypeSchema };
14
+ // Export schema version for consumers
15
+ export { ZOD_SCHEMA_VERSION };
16
+ /**
17
+ * Valid assessment profile names.
18
+ */
19
+ export const AssessmentProfileNameSchema = z.enum([
20
+ "quick",
21
+ "security",
22
+ "compliance",
23
+ "full",
24
+ ]);
25
+ /**
26
+ * Valid assessment module names.
27
+ * Derived from ASSESSMENT_CATEGORY_METADATA in coreTypes.ts.
28
+ */
29
+ export const AssessmentModuleNameSchema = z.enum([
30
+ "functionality",
31
+ "security",
32
+ "documentation",
33
+ "errorHandling",
34
+ "usability",
35
+ "mcpSpecCompliance",
36
+ "aupCompliance",
37
+ "toolAnnotations",
38
+ "prohibitedLibraries",
39
+ "manifestValidation",
40
+ "portability",
41
+ "externalAPIScanner",
42
+ "authentication",
43
+ "temporal",
44
+ "resources",
45
+ "prompts",
46
+ "crossCapability",
47
+ "protocolConformance",
48
+ ]);
49
+ // ============================================================================
50
+ // Server Configuration Schema
51
+ // ============================================================================
52
+ /**
53
+ * Schema for server connection configuration.
54
+ * Validates transport-specific required fields.
55
+ *
56
+ * @remarks
57
+ * This schema provides flexible validation for CLI argument parsing.
58
+ * For type-safe config file parsing with discriminated unions, see
59
+ * server-configSchemas.ts (HttpSseServerConfigSchema, StdioServerConfigSchema).
60
+ */
61
+ export const ServerConfigSchema = z
62
+ .object({
63
+ transport: TransportTypeSchema.optional(),
64
+ command: z.string().optional(),
65
+ args: z.array(z.string()).optional(),
66
+ env: z.record(z.string()).optional(),
67
+ cwd: z.string().optional(),
68
+ url: z.string().optional(),
69
+ })
70
+ .refine((data) => {
71
+ // For http/sse transport, url is required
72
+ if (data.transport === "http" || data.transport === "sse") {
73
+ return !!data.url;
74
+ }
75
+ // For stdio transport, command is required
76
+ if (data.transport === "stdio") {
77
+ return !!data.command;
78
+ }
79
+ // If no transport specified, either url or command must be present
80
+ return !!data.url || !!data.command;
81
+ }, {
82
+ message: "For http/sse transport, 'url' is required. For stdio transport, 'command' is required.",
83
+ });
84
+ // ============================================================================
85
+ // Assessment Options Schema
86
+ // ============================================================================
87
+ /**
88
+ * Schema for assessment CLI options.
89
+ * Validates all command-line arguments and their constraints.
90
+ */
91
+ export const AssessmentOptionsSchema = z
92
+ .object({
93
+ serverName: z.string().min(1, "--server is required"),
94
+ serverConfigPath: z.string().optional(),
95
+ outputPath: z.string().optional(),
96
+ sourceCodePath: z.string().optional(),
97
+ patternConfigPath: z.string().optional(),
98
+ performanceConfigPath: z.string().optional(),
99
+ claudeEnabled: z.boolean().optional(),
100
+ claudeHttp: z.boolean().optional(),
101
+ mcpAuditorUrl: z
102
+ .string()
103
+ .url("--mcp-auditor-url must be a valid URL")
104
+ .optional(),
105
+ fullAssessment: z.boolean().optional(),
106
+ verbose: z.boolean().optional(),
107
+ jsonOnly: z.boolean().optional(),
108
+ helpRequested: z.boolean().optional(),
109
+ versionRequested: z.boolean().optional(),
110
+ format: ReportFormatSchema.optional(),
111
+ includePolicy: z.boolean().optional(),
112
+ preflightOnly: z.boolean().optional(),
113
+ comparePath: z.string().optional(),
114
+ diffOnly: z.boolean().optional(),
115
+ resume: z.boolean().optional(),
116
+ noResume: z.boolean().optional(),
117
+ temporalInvocations: z.number().int().positive().optional(),
118
+ skipTemporal: z.boolean().optional(),
119
+ skipModules: z.array(AssessmentModuleNameSchema).optional(),
120
+ onlyModules: z.array(AssessmentModuleNameSchema).optional(),
121
+ profile: AssessmentProfileNameSchema.optional(),
122
+ logLevel: LogLevelSchema.optional(),
123
+ listModules: z.boolean().optional(),
124
+ })
125
+ .refine((data) => !(data.profile && (data.skipModules?.length || data.onlyModules?.length)), {
126
+ message: "--profile cannot be used with --skip-modules or --only-modules",
127
+ path: ["profile"],
128
+ })
129
+ .refine((data) => !(data.skipModules?.length && data.onlyModules?.length), {
130
+ message: "--skip-modules and --only-modules are mutually exclusive",
131
+ path: ["skipModules"],
132
+ });
133
+ /**
134
+ * Schema for validation result.
135
+ */
136
+ export const ValidationResultSchema = z.object({
137
+ valid: z.boolean(),
138
+ errors: z.array(z.string()),
139
+ });
140
+ // ============================================================================
141
+ // Validation Helpers
142
+ // ============================================================================
143
+ /**
144
+ * Validate assessment options and return error messages.
145
+ *
146
+ * @param options - Options to validate
147
+ * @returns Array of validation error messages (empty if valid)
148
+ */
149
+ export function validateAssessmentOptions(options) {
150
+ const result = AssessmentOptionsSchema.safeParse(options);
151
+ if (result.success) {
152
+ return [];
153
+ }
154
+ return result.error.errors.map((e) => {
155
+ const path = e.path.length > 0 ? `${e.path.join(".")}: ` : "";
156
+ return `${path}${e.message}`;
157
+ });
158
+ }
159
+ /**
160
+ * Validate a server configuration.
161
+ *
162
+ * @param config - Server config to validate
163
+ * @returns Array of validation error messages (empty if valid)
164
+ */
165
+ export function validateServerConfig(config) {
166
+ const result = ServerConfigSchema.safeParse(config);
167
+ if (result.success) {
168
+ return [];
169
+ }
170
+ return result.error.errors.map((e) => {
171
+ const path = e.path.length > 0 ? `${e.path.join(".")}: ` : "";
172
+ return `${path}${e.message}`;
173
+ });
174
+ }
175
+ /**
176
+ * Parse assessment options with validation.
177
+ *
178
+ * @param options - Raw options object
179
+ * @returns Validated options
180
+ * @throws ZodError if validation fails
181
+ */
182
+ export function parseAssessmentOptions(options) {
183
+ return AssessmentOptionsSchema.parse(options);
184
+ }
185
+ /**
186
+ * Safely parse assessment options without throwing.
187
+ *
188
+ * @param options - Raw options object
189
+ * @returns SafeParseResult with success status and data/error
190
+ */
191
+ export function safeParseAssessmentOptions(options) {
192
+ return AssessmentOptionsSchema.safeParse(options);
193
+ }
194
+ /**
195
+ * Validate module names from a comma-separated string.
196
+ *
197
+ * @param input - Comma-separated module names
198
+ * @returns Array of validated module names
199
+ * @throws ZodError if any module name is invalid
200
+ */
201
+ export function parseModuleNames(input) {
202
+ const names = input
203
+ .split(",")
204
+ .map((n) => n.trim())
205
+ .filter(Boolean);
206
+ return z.array(AssessmentModuleNameSchema).parse(names);
207
+ }
208
+ /**
209
+ * Safely validate module names without throwing.
210
+ *
211
+ * @param input - Comma-separated module names
212
+ * @returns Object with valid module names and any invalid names
213
+ */
214
+ export function safeParseModuleNames(input) {
215
+ const names = input
216
+ .split(",")
217
+ .map((n) => n.trim())
218
+ .filter(Boolean);
219
+ const valid = [];
220
+ const invalid = [];
221
+ for (const name of names) {
222
+ const result = AssessmentModuleNameSchema.safeParse(name);
223
+ if (result.success) {
224
+ valid.push(result.data);
225
+ }
226
+ else {
227
+ invalid.push(name);
228
+ }
229
+ }
230
+ return { valid, invalid };
231
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Zod Error Formatting Utilities
3
+ *
4
+ * CLI-friendly error message formatting for Zod validation errors.
5
+ *
6
+ * @module cli/lib/zodErrorFormatter
7
+ */
8
+ /**
9
+ * Format a single Zod issue into a readable string.
10
+ *
11
+ * @param issue - Zod validation issue
12
+ * @returns Formatted error message
13
+ */
14
+ export function formatZodIssue(issue) {
15
+ const path = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
16
+ return `${path}${issue.message}`;
17
+ }
18
+ /**
19
+ * Format a ZodError into a readable string.
20
+ *
21
+ * @param error - Zod validation error
22
+ * @returns Formatted error messages joined by newlines
23
+ */
24
+ export function formatZodError(error) {
25
+ return error.errors.map(formatZodIssue).join("\n");
26
+ }
27
+ /**
28
+ * Format a ZodError with indentation for CLI output.
29
+ *
30
+ * @param error - Zod validation error
31
+ * @param indent - Indentation string (default: " ")
32
+ * @returns Formatted error messages with indentation
33
+ */
34
+ export function formatZodErrorIndented(error, indent = " ") {
35
+ return error.errors.map((e) => `${indent}${formatZodIssue(e)}`).join("\n");
36
+ }
37
+ /**
38
+ * Print a formatted Zod error to stderr with context.
39
+ *
40
+ * @param error - Zod validation error
41
+ * @param context - Context description (e.g., "config file", "CLI arguments")
42
+ */
43
+ export function printZodErrorForCli(error, context) {
44
+ const prefix = context ? `Error in ${context}:\n` : "Validation error:\n";
45
+ console.error(prefix + formatZodErrorIndented(error));
46
+ }
47
+ /**
48
+ * Convert ZodError to an array of error strings.
49
+ * Useful for accumulating errors in a validation result.
50
+ *
51
+ * @param error - Zod validation error
52
+ * @returns Array of formatted error strings
53
+ */
54
+ export function zodErrorToArray(error) {
55
+ return error.errors.map(formatZodIssue);
56
+ }
57
+ /**
58
+ * Format validation errors for JSON output.
59
+ *
60
+ * @param error - Zod validation error
61
+ * @returns Object with structured error information
62
+ */
63
+ export function formatZodErrorForJson(error) {
64
+ return {
65
+ message: "Validation failed",
66
+ errors: error.errors.map((e) => ({
67
+ path: e.path,
68
+ message: e.message,
69
+ code: e.code,
70
+ })),
71
+ };
72
+ }
73
+ /**
74
+ * Create a user-friendly error message for common validation issues.
75
+ *
76
+ * @param error - Zod validation error
77
+ * @param fieldLabels - Optional map of field names to user-friendly labels
78
+ * @returns User-friendly error message
79
+ */
80
+ export function formatUserFriendlyError(error, fieldLabels) {
81
+ const messages = error.errors.map((issue) => {
82
+ const fieldPath = issue.path.join(".");
83
+ const label = fieldLabels?.[fieldPath] || fieldPath;
84
+ const prefix = label ? `${label}: ` : "";
85
+ return `${prefix}${issue.message}`;
86
+ });
87
+ if (messages.length === 1) {
88
+ return messages[0];
89
+ }
90
+ return `Multiple validation errors:\n${messages.map((m) => ` - ${m}`).join("\n")}`;
91
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bryan-thompson/inspector-assessment-cli",
3
- "version": "1.30.0",
3
+ "version": "1.31.0",
4
4
  "description": "CLI for the Enhanced MCP Inspector with assessment capabilities",
5
5
  "license": "MIT",
6
6
  "author": "Bryan Thompson <bryan@triepod.ai>",