@the-cascade-protocol/cli 0.2.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 (165) hide show
  1. package/.dockerignore +7 -0
  2. package/.eslintrc.json +23 -0
  3. package/.prettierrc +7 -0
  4. package/DOCKER.md +36 -0
  5. package/Dockerfile +18 -0
  6. package/README.md +69 -0
  7. package/dist/commands/capabilities.d.ts +9 -0
  8. package/dist/commands/capabilities.d.ts.map +1 -0
  9. package/dist/commands/capabilities.js +194 -0
  10. package/dist/commands/capabilities.js.map +1 -0
  11. package/dist/commands/conformance.d.ts +15 -0
  12. package/dist/commands/conformance.d.ts.map +1 -0
  13. package/dist/commands/conformance.js +348 -0
  14. package/dist/commands/conformance.js.map +1 -0
  15. package/dist/commands/convert.d.ts +21 -0
  16. package/dist/commands/convert.d.ts.map +1 -0
  17. package/dist/commands/convert.js +134 -0
  18. package/dist/commands/convert.js.map +1 -0
  19. package/dist/commands/pod/export.d.ts +8 -0
  20. package/dist/commands/pod/export.d.ts.map +1 -0
  21. package/dist/commands/pod/export.js +72 -0
  22. package/dist/commands/pod/export.js.map +1 -0
  23. package/dist/commands/pod/helpers.d.ts +79 -0
  24. package/dist/commands/pod/helpers.d.ts.map +1 -0
  25. package/dist/commands/pod/helpers.js +369 -0
  26. package/dist/commands/pod/helpers.js.map +1 -0
  27. package/dist/commands/pod/index.d.ts +20 -0
  28. package/dist/commands/pod/index.d.ts.map +1 -0
  29. package/dist/commands/pod/index.js +29 -0
  30. package/dist/commands/pod/index.js.map +1 -0
  31. package/dist/commands/pod/info.d.ts +9 -0
  32. package/dist/commands/pod/info.d.ts.map +1 -0
  33. package/dist/commands/pod/info.js +196 -0
  34. package/dist/commands/pod/info.js.map +1 -0
  35. package/dist/commands/pod/init.d.ts +9 -0
  36. package/dist/commands/pod/init.d.ts.map +1 -0
  37. package/dist/commands/pod/init.js +251 -0
  38. package/dist/commands/pod/init.js.map +1 -0
  39. package/dist/commands/pod/query.d.ts +9 -0
  40. package/dist/commands/pod/query.d.ts.map +1 -0
  41. package/dist/commands/pod/query.js +169 -0
  42. package/dist/commands/pod/query.js.map +1 -0
  43. package/dist/commands/pod 2.js +1017 -0
  44. package/dist/commands/pod.d.ts +28 -0
  45. package/dist/commands/pod.d.ts 2.map +1 -0
  46. package/dist/commands/pod.d.ts.map +1 -0
  47. package/dist/commands/pod.js +1031 -0
  48. package/dist/commands/pod.js 2.map +1 -0
  49. package/dist/commands/pod.js.map +1 -0
  50. package/dist/commands/serve.d.ts +33 -0
  51. package/dist/commands/serve.d.ts.map +1 -0
  52. package/dist/commands/serve.js +74 -0
  53. package/dist/commands/serve.js.map +1 -0
  54. package/dist/commands/validate.d.ts +18 -0
  55. package/dist/commands/validate.d.ts.map +1 -0
  56. package/dist/commands/validate.js +275 -0
  57. package/dist/commands/validate.js.map +1 -0
  58. package/dist/index.d.ts +19 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +49 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/lib/fhir-converter/cascade-to-fhir.d.ts +17 -0
  63. package/dist/lib/fhir-converter/cascade-to-fhir.d.ts.map +1 -0
  64. package/dist/lib/fhir-converter/cascade-to-fhir.js +358 -0
  65. package/dist/lib/fhir-converter/cascade-to-fhir.js.map +1 -0
  66. package/dist/lib/fhir-converter/converters-clinical.d.ts +29 -0
  67. package/dist/lib/fhir-converter/converters-clinical.d.ts.map +1 -0
  68. package/dist/lib/fhir-converter/converters-clinical.js +391 -0
  69. package/dist/lib/fhir-converter/converters-clinical.js.map +1 -0
  70. package/dist/lib/fhir-converter/converters-demographics.d.ts +20 -0
  71. package/dist/lib/fhir-converter/converters-demographics.d.ts.map +1 -0
  72. package/dist/lib/fhir-converter/converters-demographics.js +242 -0
  73. package/dist/lib/fhir-converter/converters-demographics.js.map +1 -0
  74. package/dist/lib/fhir-converter/fhir-to-cascade.d.ts +17 -0
  75. package/dist/lib/fhir-converter/fhir-to-cascade.d.ts.map +1 -0
  76. package/dist/lib/fhir-converter/fhir-to-cascade.js +63 -0
  77. package/dist/lib/fhir-converter/fhir-to-cascade.js.map +1 -0
  78. package/dist/lib/fhir-converter/index.d.ts +36 -0
  79. package/dist/lib/fhir-converter/index.d.ts.map +1 -0
  80. package/dist/lib/fhir-converter/index.js +187 -0
  81. package/dist/lib/fhir-converter/index.js.map +1 -0
  82. package/dist/lib/fhir-converter/types.d.ts +77 -0
  83. package/dist/lib/fhir-converter/types.d.ts.map +1 -0
  84. package/dist/lib/fhir-converter/types.js +236 -0
  85. package/dist/lib/fhir-converter/types.js.map +1 -0
  86. package/dist/lib/fhir-converter.d.ts +62 -0
  87. package/dist/lib/fhir-converter.d.ts.map +1 -0
  88. package/dist/lib/fhir-converter.js +1474 -0
  89. package/dist/lib/fhir-converter.js.map +1 -0
  90. package/dist/lib/mcp/audit.d.ts +24 -0
  91. package/dist/lib/mcp/audit.d.ts.map +1 -0
  92. package/dist/lib/mcp/audit.js +85 -0
  93. package/dist/lib/mcp/audit.js.map +1 -0
  94. package/dist/lib/mcp/server.d.ts +38 -0
  95. package/dist/lib/mcp/server.d.ts.map +1 -0
  96. package/dist/lib/mcp/server.js +172 -0
  97. package/dist/lib/mcp/server.js.map +1 -0
  98. package/dist/lib/mcp/tools.d.ts +47 -0
  99. package/dist/lib/mcp/tools.d.ts.map +1 -0
  100. package/dist/lib/mcp/tools.js +547 -0
  101. package/dist/lib/mcp/tools.js.map +1 -0
  102. package/dist/lib/output.d.ts +26 -0
  103. package/dist/lib/output.d.ts.map +1 -0
  104. package/dist/lib/output.js +64 -0
  105. package/dist/lib/output.js.map +1 -0
  106. package/dist/lib/shacl-validator.d.ts +53 -0
  107. package/dist/lib/shacl-validator.d.ts.map +1 -0
  108. package/dist/lib/shacl-validator.js +245 -0
  109. package/dist/lib/shacl-validator.js.map +1 -0
  110. package/dist/lib/turtle-parser.d.ts +64 -0
  111. package/dist/lib/turtle-parser.d.ts.map +1 -0
  112. package/dist/lib/turtle-parser.js +236 -0
  113. package/dist/lib/turtle-parser.js.map +1 -0
  114. package/dist/shapes/checkup.shapes.ttl +1459 -0
  115. package/dist/shapes/clinical.shapes.ttl +1350 -0
  116. package/dist/shapes/clinical.ttl +1369 -0
  117. package/dist/shapes/core.shapes.ttl +450 -0
  118. package/dist/shapes/core.ttl +603 -0
  119. package/dist/shapes/coverage.shapes.ttl +214 -0
  120. package/dist/shapes/coverage.ttl +182 -0
  121. package/dist/shapes/health.shapes.ttl +697 -0
  122. package/dist/shapes/health.ttl +859 -0
  123. package/dist/shapes/pots.shapes.ttl +481 -0
  124. package/package.json +54 -0
  125. package/src/commands/capabilities.ts +235 -0
  126. package/src/commands/conformance.ts +447 -0
  127. package/src/commands/convert.ts +164 -0
  128. package/src/commands/pod/export.ts +85 -0
  129. package/src/commands/pod/helpers.ts +449 -0
  130. package/src/commands/pod/index.ts +32 -0
  131. package/src/commands/pod/info.ts +239 -0
  132. package/src/commands/pod/init.ts +273 -0
  133. package/src/commands/pod/query.ts +224 -0
  134. package/src/commands/serve.ts +92 -0
  135. package/src/commands/validate.ts +303 -0
  136. package/src/index.ts +58 -0
  137. package/src/lib/fhir-converter/cascade-to-fhir.ts +369 -0
  138. package/src/lib/fhir-converter/converters-clinical.ts +446 -0
  139. package/src/lib/fhir-converter/converters-demographics.ts +270 -0
  140. package/src/lib/fhir-converter/fhir-to-cascade.ts +82 -0
  141. package/src/lib/fhir-converter/index.ts +215 -0
  142. package/src/lib/fhir-converter/types.ts +318 -0
  143. package/src/lib/mcp/audit.ts +107 -0
  144. package/src/lib/mcp/server.ts +192 -0
  145. package/src/lib/mcp/tools.ts +668 -0
  146. package/src/lib/output.ts +76 -0
  147. package/src/lib/shacl-validator.ts +314 -0
  148. package/src/lib/turtle-parser.ts +277 -0
  149. package/src/shapes/checkup.shapes.ttl +1459 -0
  150. package/src/shapes/clinical.shapes.ttl +1350 -0
  151. package/src/shapes/clinical.ttl +1369 -0
  152. package/src/shapes/core.shapes.ttl +450 -0
  153. package/src/shapes/core.ttl +603 -0
  154. package/src/shapes/coverage.shapes.ttl +214 -0
  155. package/src/shapes/coverage.ttl +182 -0
  156. package/src/shapes/health.shapes.ttl +697 -0
  157. package/src/shapes/health.ttl +859 -0
  158. package/src/shapes/pots.shapes.ttl +481 -0
  159. package/test-fixtures/fhir-bundle-example.json +216 -0
  160. package/test-fixtures/fhir-medication-example.json +18 -0
  161. package/tests/cli.test.ts +126 -0
  162. package/tests/fhir-converter.test.ts +874 -0
  163. package/tests/mcp-server.test.ts +396 -0
  164. package/tests/pod.test.ts +400 -0
  165. package/tsconfig.json +24 -0
@@ -0,0 +1,224 @@
1
+ /**
2
+ * cascade pod query <pod-dir>
3
+ *
4
+ * Query data within a Cascade Pod by type (medications, conditions, etc.)
5
+ * or across all data types.
6
+ */
7
+
8
+ import type { Command } from 'commander';
9
+ import * as path from 'path';
10
+ import { printResult, printError, printVerbose, type OutputOptions } from '../../lib/output.js';
11
+ import {
12
+ DATA_TYPES,
13
+ resolvePodDir,
14
+ isDirectory,
15
+ fileExists,
16
+ discoverTtlFiles,
17
+ parseDataFile,
18
+ extractLabelFromProps,
19
+ selectKeyProperties,
20
+ } from './helpers.js';
21
+
22
+ export function registerQuerySubcommand(pod: Command, program: Command): void {
23
+ pod
24
+ .command('query')
25
+ .description('Query data within a pod')
26
+ .argument('<pod-dir>', 'Path to the Cascade Pod')
27
+ .option('--medications', 'Query medications')
28
+ .option('--conditions', 'Query conditions')
29
+ .option('--allergies', 'Query allergies')
30
+ .option('--lab-results', 'Query lab results')
31
+ .option('--immunizations', 'Query immunizations')
32
+ .option('--vital-signs', 'Query vital signs')
33
+ .option('--supplements', 'Query supplements')
34
+ .option('--all', 'Query all data')
35
+ .action(
36
+ async (
37
+ podDir: string,
38
+ options: {
39
+ medications?: boolean;
40
+ conditions?: boolean;
41
+ allergies?: boolean;
42
+ labResults?: boolean;
43
+ immunizations?: boolean;
44
+ vitalSigns?: boolean;
45
+ supplements?: boolean;
46
+ all?: boolean;
47
+ },
48
+ ) => {
49
+ const globalOpts = program.opts() as OutputOptions;
50
+ const absDir = resolvePodDir(podDir);
51
+
52
+ printVerbose(`Querying pod: ${absDir}`, globalOpts);
53
+ printVerbose(`Filters: ${JSON.stringify(options)}`, globalOpts);
54
+
55
+ // Validate pod exists
56
+ if (!(await isDirectory(absDir))) {
57
+ printError(`Pod directory not found: ${absDir}`, globalOpts);
58
+ process.exitCode = 1;
59
+ return;
60
+ }
61
+
62
+ try {
63
+ // Determine which data types to query
64
+ let requestedTypes: string[];
65
+
66
+ if (options.all) {
67
+ // Discover all TTL files in the pod
68
+ requestedTypes = Object.keys(DATA_TYPES);
69
+ } else {
70
+ requestedTypes = [];
71
+ if (options.medications) requestedTypes.push('medications');
72
+ if (options.conditions) requestedTypes.push('conditions');
73
+ if (options.allergies) requestedTypes.push('allergies');
74
+ if (options.labResults) requestedTypes.push('lab-results');
75
+ if (options.immunizations) requestedTypes.push('immunizations');
76
+ if (options.vitalSigns) requestedTypes.push('vital-signs');
77
+ if (options.supplements) requestedTypes.push('supplements');
78
+ }
79
+
80
+ if (requestedTypes.length === 0) {
81
+ printError(
82
+ 'No query filter specified. Use --medications, --conditions, --all, etc.',
83
+ globalOpts,
84
+ );
85
+ process.exitCode = 1;
86
+ return;
87
+ }
88
+
89
+ // Process each requested data type
90
+ const queryResults: Record<
91
+ string,
92
+ {
93
+ count: number;
94
+ file: string;
95
+ records: Array<{
96
+ id: string;
97
+ type: string;
98
+ properties: Record<string, string>;
99
+ }>;
100
+ error?: string;
101
+ }
102
+ > = {};
103
+
104
+ // If --all, also discover any TTL files not in the registry
105
+ const extraFiles: string[] = [];
106
+ if (options.all) {
107
+ const allTtlFiles = await discoverTtlFiles(absDir);
108
+ const knownPaths = new Set(
109
+ Object.values(DATA_TYPES).map((dt) =>
110
+ path.join(absDir, dt.directory, dt.filename),
111
+ ),
112
+ );
113
+ // Also exclude index.ttl, manifest.ttl, profile/card.ttl, type indexes
114
+ const excludePaths = new Set([
115
+ path.join(absDir, 'index.ttl'),
116
+ path.join(absDir, 'manifest.ttl'),
117
+ path.join(absDir, 'profile', 'card.ttl'),
118
+ path.join(absDir, 'settings', 'publicTypeIndex.ttl'),
119
+ path.join(absDir, 'settings', 'privateTypeIndex.ttl'),
120
+ ]);
121
+ for (const f of allTtlFiles) {
122
+ if (!knownPaths.has(f) && !excludePaths.has(f)) {
123
+ extraFiles.push(f);
124
+ }
125
+ }
126
+ }
127
+
128
+ for (const typeName of requestedTypes) {
129
+ const typeInfo = DATA_TYPES[typeName];
130
+ if (!typeInfo) continue;
131
+
132
+ const filePath = path.join(absDir, typeInfo.directory, typeInfo.filename);
133
+ if (!(await fileExists(filePath))) {
134
+ printVerbose(`Skipping ${typeName}: file not found at ${filePath}`, globalOpts);
135
+ continue;
136
+ }
137
+
138
+ const { records, error } = await parseDataFile(filePath);
139
+
140
+ queryResults[typeName] = {
141
+ count: records.length,
142
+ file: `${typeInfo.directory}/${typeInfo.filename}`,
143
+ records: records.map((r) => ({
144
+ id: r.id,
145
+ type: r.type,
146
+ properties: r.properties,
147
+ })),
148
+ error,
149
+ };
150
+ }
151
+
152
+ // Process extra files found in --all mode
153
+ for (const extraFile of extraFiles) {
154
+ const relPath = path.relative(absDir, extraFile);
155
+ const baseName = path.basename(extraFile, '.ttl');
156
+
157
+ const { records, error } = await parseDataFile(extraFile);
158
+ if (records.length > 0) {
159
+ queryResults[baseName] = {
160
+ count: records.length,
161
+ file: relPath,
162
+ records: records.map((r) => ({
163
+ id: r.id,
164
+ type: r.type,
165
+ properties: r.properties,
166
+ })),
167
+ error,
168
+ };
169
+ }
170
+ }
171
+
172
+ // Output results
173
+ if (globalOpts.json) {
174
+ printResult(
175
+ {
176
+ pod: podDir,
177
+ dataTypes: queryResults,
178
+ },
179
+ globalOpts,
180
+ );
181
+ } else {
182
+ // Human-readable output
183
+ const typeKeys = Object.keys(queryResults);
184
+ if (typeKeys.length === 0) {
185
+ console.log('No data found for the specified query filters.');
186
+ return;
187
+ }
188
+
189
+ for (const typeName of typeKeys) {
190
+ const data = queryResults[typeName];
191
+ const typeInfo = DATA_TYPES[typeName];
192
+ const displayLabel = typeInfo?.label ?? typeName;
193
+
194
+ console.log(`\n=== ${displayLabel} (${data.count} records) ===`);
195
+ if (data.error) {
196
+ console.log(` Error: ${data.error}`);
197
+ continue;
198
+ }
199
+ console.log(` File: ${data.file}\n`);
200
+
201
+ for (let i = 0; i < data.records.length; i++) {
202
+ const rec = data.records[i];
203
+ const label = extractLabelFromProps(rec.properties);
204
+ const idShort = rec.id.length > 40 ? rec.id.substring(0, 40) + '...' : rec.id;
205
+
206
+ console.log(` ${i + 1}. ${label ?? rec.type} (${idShort})`);
207
+
208
+ // Show key properties
209
+ const keyProps = selectKeyProperties(typeName, rec.properties);
210
+ for (const [key, value] of Object.entries(keyProps)) {
211
+ console.log(` ${key}: ${value}`);
212
+ }
213
+ console.log('');
214
+ }
215
+ }
216
+ }
217
+ } catch (err: unknown) {
218
+ const message = err instanceof Error ? err.message : String(err);
219
+ printError(`Failed to query pod: ${message}`, globalOpts);
220
+ process.exitCode = 1;
221
+ }
222
+ },
223
+ );
224
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * cascade serve
3
+ *
4
+ * Start a local MCP-compatible agent server that exposes Cascade Protocol
5
+ * tools to AI agents.
6
+ *
7
+ * Transports:
8
+ * stdio — Standard input/output (for Claude Desktop, Claude Code)
9
+ * sse — Server-Sent Events over HTTP (for web-based agents)
10
+ *
11
+ * Options:
12
+ * --mcp Enable MCP (Model Context Protocol) mode
13
+ * --transport <transport> Transport type (stdio|sse) [default: stdio]
14
+ * --port <port> Port for SSE transport [default: 3000]
15
+ * --pod <path> Default Pod directory path
16
+ *
17
+ * Environment Variables:
18
+ * CASCADE_POD_PATH Default Pod directory (overridden by --pod)
19
+ *
20
+ * Claude Desktop configuration:
21
+ * {
22
+ * "mcpServers": {
23
+ * "cascade": {
24
+ * "command": "cascade",
25
+ * "args": ["serve", "--mcp"],
26
+ * "env": { "CASCADE_POD_PATH": "/path/to/pod" }
27
+ * }
28
+ * }
29
+ * }
30
+ */
31
+
32
+ import { Command } from 'commander';
33
+ import { printError, printVerbose, type OutputOptions } from '../lib/output.js';
34
+ import { startServer } from '../lib/mcp/server.js';
35
+
36
+ export function registerServeCommand(program: Command): void {
37
+ program
38
+ .command('serve')
39
+ .description('Start local agent server')
40
+ .option('--mcp', 'Enable MCP (Model Context Protocol) mode')
41
+ .option('--transport <transport>', 'Transport type (stdio|sse)', 'stdio')
42
+ .option('--port <port>', 'Port for SSE transport', '3000')
43
+ .option('--pod <path>', 'Default Pod directory path')
44
+ .action(
45
+ async (options: {
46
+ mcp?: boolean;
47
+ transport: string;
48
+ port: string;
49
+ pod?: string;
50
+ }) => {
51
+ const globalOpts = program.opts() as OutputOptions;
52
+
53
+ if (!options.mcp) {
54
+ printError(
55
+ 'The --mcp flag is required. Usage: cascade serve --mcp',
56
+ globalOpts,
57
+ );
58
+ console.error('');
59
+ console.error('Examples:');
60
+ console.error(' cascade serve --mcp # stdio transport');
61
+ console.error(' cascade serve --mcp --transport sse --port 3000 # SSE transport');
62
+ console.error(' cascade serve --mcp --pod ./my-pod # with default Pod');
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+
67
+ const transport = options.transport as 'stdio' | 'sse';
68
+ if (transport !== 'stdio' && transport !== 'sse') {
69
+ printError(
70
+ `Invalid transport: "${options.transport}". Must be "stdio" or "sse".`,
71
+ globalOpts,
72
+ );
73
+ process.exitCode = 1;
74
+ return;
75
+ }
76
+
77
+ printVerbose(`Starting MCP server with transport: ${transport}`, globalOpts);
78
+
79
+ try {
80
+ await startServer({
81
+ transport,
82
+ port: parseInt(options.port, 10),
83
+ podPath: options.pod,
84
+ });
85
+ } catch (err: unknown) {
86
+ const message = err instanceof Error ? err.message : String(err);
87
+ printError(`Failed to start server: ${message}`, globalOpts);
88
+ process.exitCode = 1;
89
+ }
90
+ },
91
+ );
92
+ }
@@ -0,0 +1,303 @@
1
+ /**
2
+ * cascade validate <file-or-dir>
3
+ *
4
+ * Validate Cascade Protocol data against SHACL shapes.
5
+ *
6
+ * Options:
7
+ * --shapes <shapes-dir> Path to custom SHACL shapes directory
8
+ * --json Output results as JSON
9
+ * --verbose Show detailed validation information
10
+ *
11
+ * Exit codes:
12
+ * 0 = all files pass validation
13
+ * 1 = one or more validation failures
14
+ * 2 = errors (file not found, malformed Turtle, etc.)
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { Command } from 'commander';
20
+ import { printVerbose, type OutputOptions } from '../lib/output.js';
21
+ import {
22
+ loadShapes,
23
+ validateFile,
24
+ findTurtleFiles,
25
+ type ValidationResult,
26
+ } from '../lib/shacl-validator.js';
27
+
28
+ /** ANSI color codes for terminal output */
29
+ const colors = {
30
+ reset: '\x1b[0m',
31
+ red: '\x1b[31m',
32
+ green: '\x1b[32m',
33
+ yellow: '\x1b[33m',
34
+ blue: '\x1b[34m',
35
+ cyan: '\x1b[36m',
36
+ dim: '\x1b[2m',
37
+ bold: '\x1b[1m',
38
+ };
39
+
40
+ /** Severity icons for human-readable output */
41
+ const severityIcons: Record<string, string> = {
42
+ violation: `${colors.red}FAIL${colors.reset}`,
43
+ warning: `${colors.yellow}WARN${colors.reset}`,
44
+ info: `${colors.blue}INFO${colors.reset}`,
45
+ };
46
+
47
+ /**
48
+ * Format a single validation result for human-readable output.
49
+ */
50
+ function formatResultHuman(result: ValidationResult, verbose: boolean): string {
51
+ const lines: string[] = [];
52
+ const relPath = result.file;
53
+
54
+ if (result.valid) {
55
+ lines.push(`${colors.green}PASS${colors.reset} ${relPath} (${result.quadCount} triples)`);
56
+ if (verbose && result.shapesUsed.length > 0) {
57
+ lines.push(` Shapes: ${result.shapesUsed.join(', ')}`);
58
+ }
59
+ if (verbose && result.subjects.length > 0) {
60
+ const typesSummary = result.subjects
61
+ .flatMap((s) => s.types.map(uriToLocalName))
62
+ .filter((t, i, a) => a.indexOf(t) === i);
63
+ lines.push(` Types: ${typesSummary.join(', ')}`);
64
+ }
65
+ } else {
66
+ const violations = result.results.filter((r) => r.severity === 'violation');
67
+ const warnings = result.results.filter((r) => r.severity === 'warning');
68
+ const infos = result.results.filter((r) => r.severity === 'info');
69
+
70
+ const countParts: string[] = [];
71
+ if (violations.length > 0) countParts.push(`${violations.length} violation${violations.length !== 1 ? 's' : ''}`);
72
+ if (warnings.length > 0) countParts.push(`${warnings.length} warning${warnings.length !== 1 ? 's' : ''}`);
73
+ if (infos.length > 0) countParts.push(`${infos.length} info`);
74
+
75
+ const statusIcon = violations.length > 0
76
+ ? `${colors.red}FAIL${colors.reset}`
77
+ : `${colors.yellow}WARN${colors.reset}`;
78
+
79
+ lines.push(`${statusIcon} ${relPath} (${result.quadCount} triples, ${countParts.join(', ')})`);
80
+
81
+ if (result.shapesUsed.length > 0) {
82
+ lines.push(` Shapes: ${result.shapesUsed.join(', ')}`);
83
+ }
84
+
85
+ // Group issues by severity
86
+ for (const issue of result.results) {
87
+ const icon = severityIcons[issue.severity] ?? issue.severity;
88
+ const focusInfo = issue.focusNode ? ` [${uriToLocalName(issue.focusNode)}]` : '';
89
+ lines.push(` ${icon} ${issue.message}${focusInfo}`);
90
+ if (issue.property) {
91
+ lines.push(` Property: ${issue.property}`);
92
+ }
93
+ if (issue.shape) {
94
+ lines.push(` Shape: ${issue.shape}`);
95
+ }
96
+ if (issue.value !== undefined) {
97
+ lines.push(` Value: ${issue.value}`);
98
+ }
99
+ if (issue.specLink) {
100
+ lines.push(` Spec: ${colors.cyan}${issue.specLink}${colors.reset}`);
101
+ }
102
+ }
103
+ }
104
+
105
+ return lines.join('\n');
106
+ }
107
+
108
+ /**
109
+ * Extract local name from a URI (fragment or last path segment).
110
+ */
111
+ function uriToLocalName(uri: string): string {
112
+ if (uri.includes('#')) {
113
+ return uri.split('#').pop() ?? uri;
114
+ }
115
+ return uri.split('/').pop() ?? uri;
116
+ }
117
+
118
+ /**
119
+ * Print a summary line for batch validation.
120
+ */
121
+ function printSummary(
122
+ results: ValidationResult[],
123
+ opts: OutputOptions,
124
+ ): void {
125
+ if (opts.json) return; // JSON mode outputs the full array
126
+
127
+ const total = results.length;
128
+ const passed = results.filter((r) => r.valid).length;
129
+ const failed = total - passed;
130
+
131
+ const totalViolations = results.reduce(
132
+ (sum, r) => sum + r.results.filter((i) => i.severity === 'violation').length,
133
+ 0,
134
+ );
135
+ const totalWarnings = results.reduce(
136
+ (sum, r) => sum + r.results.filter((i) => i.severity === 'warning').length,
137
+ 0,
138
+ );
139
+ const totalInfos = results.reduce(
140
+ (sum, r) => sum + r.results.filter((i) => i.severity === 'info').length,
141
+ 0,
142
+ );
143
+
144
+ console.log('');
145
+ console.log(`${colors.bold}Validation Summary${colors.reset}`);
146
+ console.log(` Files: ${total} total, ${colors.green}${passed} passed${colors.reset}, ${failed > 0 ? `${colors.red}${failed} failed${colors.reset}` : '0 failed'}`);
147
+
148
+ const parts: string[] = [];
149
+ if (totalViolations > 0) parts.push(`${colors.red}${totalViolations} violations${colors.reset}`);
150
+ if (totalWarnings > 0) parts.push(`${colors.yellow}${totalWarnings} warnings${colors.reset}`);
151
+ if (totalInfos > 0) parts.push(`${colors.blue}${totalInfos} info${colors.reset}`);
152
+ if (parts.length > 0) {
153
+ console.log(` Issues: ${parts.join(', ')}`);
154
+ }
155
+ }
156
+
157
+ export function registerValidateCommand(program: Command): void {
158
+ program
159
+ .command('validate')
160
+ .description('Validate Cascade data against SHACL shapes')
161
+ .argument('<file-or-dir>', 'Turtle file or directory to validate')
162
+ .option('--shapes <shapes-dir>', 'Path to custom SHACL shapes directory')
163
+ .action(async (fileOrDir: string, options: { shapes?: string }) => {
164
+ const globalOpts = program.opts() as OutputOptions;
165
+
166
+ // Resolve the path
167
+ const targetPath = path.resolve(fileOrDir);
168
+
169
+ // Check if path exists
170
+ if (!fs.existsSync(targetPath)) {
171
+ if (globalOpts.json) {
172
+ console.log(JSON.stringify({ error: `Path not found: ${targetPath}` }, null, 2));
173
+ } else {
174
+ console.error(`${colors.red}ERROR${colors.reset}: Path not found: ${targetPath}`);
175
+ }
176
+ process.exitCode = 2;
177
+ return;
178
+ }
179
+
180
+ // Load SHACL shapes
181
+ printVerbose('Loading SHACL shapes...', globalOpts);
182
+
183
+ let shapesStore;
184
+ let shapeFiles: string[];
185
+ try {
186
+ const loaded = loadShapes(options.shapes);
187
+ shapesStore = loaded.store;
188
+ shapeFiles = loaded.shapeFiles;
189
+ printVerbose(`Loaded ${shapeFiles.length} shape files: ${shapeFiles.join(', ')}`, globalOpts);
190
+ } catch (e: unknown) {
191
+ const msg = e instanceof Error ? e.message : String(e);
192
+ if (globalOpts.json) {
193
+ console.log(JSON.stringify({ error: msg }, null, 2));
194
+ } else {
195
+ console.error(`${colors.red}ERROR${colors.reset}: ${msg}`);
196
+ }
197
+ process.exitCode = 2;
198
+ return;
199
+ }
200
+
201
+ // Determine files to validate
202
+ let filesToValidate: string[];
203
+ const stat = fs.statSync(targetPath);
204
+
205
+ if (stat.isDirectory()) {
206
+ filesToValidate = findTurtleFiles(targetPath);
207
+ if (filesToValidate.length === 0) {
208
+ if (globalOpts.json) {
209
+ console.log(JSON.stringify({ error: `No .ttl files found in ${targetPath}` }, null, 2));
210
+ } else {
211
+ console.error(`${colors.yellow}WARNING${colors.reset}: No .ttl files found in ${targetPath}`);
212
+ }
213
+ process.exitCode = 2;
214
+ return;
215
+ }
216
+ printVerbose(`Found ${filesToValidate.length} Turtle files in ${targetPath}`, globalOpts);
217
+ } else if (stat.isFile()) {
218
+ if (!targetPath.endsWith('.ttl')) {
219
+ if (globalOpts.json) {
220
+ console.log(JSON.stringify({ error: `Not a Turtle file: ${targetPath}` }, null, 2));
221
+ } else {
222
+ console.error(`${colors.red}ERROR${colors.reset}: Not a Turtle file (expected .ttl): ${targetPath}`);
223
+ }
224
+ process.exitCode = 2;
225
+ return;
226
+ }
227
+ filesToValidate = [targetPath];
228
+ } else {
229
+ if (globalOpts.json) {
230
+ console.log(JSON.stringify({ error: `Not a file or directory: ${targetPath}` }, null, 2));
231
+ } else {
232
+ console.error(`${colors.red}ERROR${colors.reset}: Not a file or directory: ${targetPath}`);
233
+ }
234
+ process.exitCode = 2;
235
+ return;
236
+ }
237
+
238
+ // Validate each file
239
+ const results: ValidationResult[] = [];
240
+ let hasErrors = false;
241
+ let hasViolations = false;
242
+
243
+ for (const file of filesToValidate) {
244
+ printVerbose(`Validating: ${file}`, globalOpts);
245
+
246
+ try {
247
+ const result = validateFile(file, shapesStore, shapeFiles);
248
+ results.push(result);
249
+
250
+ if (!result.valid) {
251
+ const violations = result.results.filter((r) => r.severity === 'violation');
252
+ if (violations.length > 0) {
253
+ hasViolations = true;
254
+ }
255
+ }
256
+
257
+ // Print result immediately in human-readable mode
258
+ if (!globalOpts.json) {
259
+ console.log(formatResultHuman(result, globalOpts.verbose));
260
+ }
261
+ } catch (e: unknown) {
262
+ hasErrors = true;
263
+ const msg = e instanceof Error ? e.message : String(e);
264
+ const errorResult: ValidationResult = {
265
+ valid: false,
266
+ file,
267
+ results: [{
268
+ severity: 'violation',
269
+ shape: '',
270
+ property: '',
271
+ message: `Error processing file: ${msg}`,
272
+ }],
273
+ shapesUsed: [],
274
+ quadCount: 0,
275
+ subjects: [],
276
+ };
277
+ results.push(errorResult);
278
+ if (!globalOpts.json) {
279
+ console.log(formatResultHuman(errorResult, globalOpts.verbose));
280
+ }
281
+ }
282
+ }
283
+
284
+ // Output JSON
285
+ if (globalOpts.json) {
286
+ console.log(JSON.stringify(results, null, 2));
287
+ } else {
288
+ // Print summary for multiple files
289
+ if (results.length > 1) {
290
+ printSummary(results, globalOpts);
291
+ }
292
+ }
293
+
294
+ // Set exit code
295
+ if (hasErrors) {
296
+ process.exitCode = 2;
297
+ } else if (hasViolations) {
298
+ process.exitCode = 1;
299
+ } else {
300
+ process.exitCode = 0;
301
+ }
302
+ });
303
+ }
package/src/index.ts ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @the-cascade-protocol/cli
5
+ *
6
+ * Cascade Protocol CLI - Validate, convert, and manage health data.
7
+ *
8
+ * Usage:
9
+ * cascade <command> [options]
10
+ *
11
+ * Commands:
12
+ * validate Validate Cascade data against SHACL shapes
13
+ * convert Convert between health data formats
14
+ * pod Manage Cascade Pod structures
15
+ * conformance Run conformance test suite
16
+ * serve Start local agent server
17
+ * capabilities Show machine-readable tool descriptions
18
+ */
19
+
20
+ import { Command } from 'commander';
21
+ import { registerValidateCommand } from './commands/validate.js';
22
+ import { registerConvertCommand } from './commands/convert.js';
23
+ import { registerPodCommand } from './commands/pod/index.js';
24
+ import { registerConformanceCommand } from './commands/conformance.js';
25
+ import { registerServeCommand } from './commands/serve.js';
26
+ import { registerCapabilitiesCommand } from './commands/capabilities.js';
27
+
28
+ const program = new Command();
29
+
30
+ program
31
+ .name('cascade')
32
+ .description('Cascade Protocol CLI')
33
+ .version('0.2.0')
34
+ .option('--verbose', 'Verbose output', false)
35
+ .option('--json', 'Output results as JSON (machine-readable)', false);
36
+
37
+ // Register all commands
38
+ registerValidateCommand(program);
39
+ registerConvertCommand(program);
40
+ registerPodCommand(program);
41
+ registerConformanceCommand(program);
42
+ registerServeCommand(program);
43
+ registerCapabilitiesCommand(program);
44
+
45
+ // Custom help text with examples
46
+ program.addHelpText(
47
+ 'after',
48
+ `
49
+ Examples:
50
+ cascade validate record.ttl
51
+ cascade convert --from fhir --to cascade patient.json
52
+ cascade pod init ./my-pod
53
+ cascade capabilities
54
+ cascade capabilities --json
55
+ `,
56
+ );
57
+
58
+ program.parse();