@cosyte/cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +326 -0
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/dist/bin/cosyte-mcp.cjs +1160 -0
- package/dist/bin/cosyte-mcp.cjs.map +1 -0
- package/dist/bin/cosyte-mcp.d.cts +1 -0
- package/dist/bin/cosyte-mcp.d.ts +1 -0
- package/dist/bin/cosyte-mcp.mjs +1158 -0
- package/dist/bin/cosyte-mcp.mjs.map +1 -0
- package/dist/bin/cosyte.cjs +1785 -0
- package/dist/bin/cosyte.cjs.map +1 -0
- package/dist/bin/cosyte.d.cts +1 -0
- package/dist/bin/cosyte.d.ts +1 -0
- package/dist/bin/cosyte.mjs +1783 -0
- package/dist/bin/cosyte.mjs.map +1 -0
- package/dist/index.cjs +1462 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1360 -0
- package/dist/index.d.ts +1360 -0
- package/dist/index.mjs +1414 -0
- package/dist/index.mjs.map +1 -0
- package/dist/io-B2EobRzC.d.cts +57 -0
- package/dist/io-B2EobRzC.d.ts +57 -0
- package/dist/mcp.cjs +1161 -0
- package/dist/mcp.cjs.map +1 -0
- package/dist/mcp.d.cts +143 -0
- package/dist/mcp.d.ts +143 -0
- package/dist/mcp.mjs +1155 -0
- package/dist/mcp.mjs.map +1 -0
- package/package.json +131 -0
|
@@ -0,0 +1,1783 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'util';
|
|
3
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { readFile } from 'fs/promises';
|
|
7
|
+
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __esm = (fn, res, err) => function __init() {
|
|
11
|
+
if (err) throw err[0];
|
|
12
|
+
try {
|
|
13
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
throw err = [e], e;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var __export = (target, all) => {
|
|
19
|
+
for (var name in all)
|
|
20
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/core/exit-codes.ts
|
|
24
|
+
var EXIT;
|
|
25
|
+
var init_exit_codes = __esm({
|
|
26
|
+
"src/core/exit-codes.ts"() {
|
|
27
|
+
EXIT = {
|
|
28
|
+
/** Success: the operation completed; `validate` found the input **valid**. */
|
|
29
|
+
OK: 0,
|
|
30
|
+
/** Operation-level failure: `validate` found the input **invalid** (parseable but non-conformant):
|
|
31
|
+
* a real, expected CI signal that the message is bad and the tool worked. Never emitted for
|
|
32
|
+
* unparseable input (that is `DATAERR`). */
|
|
33
|
+
INVALID: 1,
|
|
34
|
+
/** Usage error: unknown command, bad flag, missing argument (`EX_USAGE`). */
|
|
35
|
+
USAGE: 2,
|
|
36
|
+
/** Data error: input could not be parsed or its format could not be detected (`EX_DATAERR`). */
|
|
37
|
+
DATAERR: 65,
|
|
38
|
+
/** No input: the named file does not exist or is unreadable (`EX_NOINPUT`). */
|
|
39
|
+
NOINPUT: 66,
|
|
40
|
+
/** Unavailable: a required capability is not yet built (e.g. `redact` before `@cosyte/deid`), a
|
|
41
|
+
* distinct non-zero signal that is never a fake success (`EX_UNAVAILABLE`). */
|
|
42
|
+
UNAVAILABLE: 69,
|
|
43
|
+
/** Internal error: an unexpected exception (a bug), distinct from a handled bad input (`EX_SOFTWARE`). */
|
|
44
|
+
SOFTWARE: 70
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// src/core/diagnostics.ts
|
|
50
|
+
function formatDiagnostic(err) {
|
|
51
|
+
return `cosyte: ${err.code}: ${err.message}`;
|
|
52
|
+
}
|
|
53
|
+
function errorResult(err) {
|
|
54
|
+
return { stdout: "", stderr: `${formatDiagnostic(err)}
|
|
55
|
+
`, exit: err.exit };
|
|
56
|
+
}
|
|
57
|
+
function toCliError(err) {
|
|
58
|
+
if (err instanceof CliError) return err;
|
|
59
|
+
return new CliError(
|
|
60
|
+
CLI_CODES.CLI_INTERNAL,
|
|
61
|
+
EXIT.SOFTWARE,
|
|
62
|
+
"an unexpected internal error occurred (no input is echoed)"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
var CLI_CODES, CliError;
|
|
66
|
+
var init_diagnostics = __esm({
|
|
67
|
+
"src/core/diagnostics.ts"() {
|
|
68
|
+
init_exit_codes();
|
|
69
|
+
CLI_CODES = {
|
|
70
|
+
/** A required argument was missing or a flag was invalid. Exit `2`. */
|
|
71
|
+
CLI_USAGE: "CLI_USAGE",
|
|
72
|
+
/** The named file does not exist or could not be read. Exit `66`. */
|
|
73
|
+
CLI_NO_INPUT: "CLI_NO_INPUT",
|
|
74
|
+
/** The input was empty, no bytes to detect or parse. Exit `65`. */
|
|
75
|
+
CLI_EMPTY_INPUT: "CLI_EMPTY_INPUT",
|
|
76
|
+
/** No format signature matched; the CLI will not guess. Names the candidates, never the bytes. Exit `65`. */
|
|
77
|
+
CLI_FORMAT_UNDETECTED: "CLI_FORMAT_UNDETECTED",
|
|
78
|
+
/** More than one format signature matched; the CLI will not guess. Names the candidates, never the bytes. Exit `65`. */
|
|
79
|
+
CLI_FORMAT_AMBIGUOUS: "CLI_FORMAT_AMBIGUOUS",
|
|
80
|
+
/** A recognised format whose parser does not support the requested operation (e.g. `parse` on a
|
|
81
|
+
* format wired only for `inspect`). Names the format + operation, never the bytes. Exit `65`. */
|
|
82
|
+
CLI_FORMAT_UNSUPPORTED: "CLI_FORMAT_UNSUPPORTED",
|
|
83
|
+
/** The optional parser package for a recognised format is not installed. The six breadth parsers
|
|
84
|
+
* (dicom/x12/ccda/ncpdp/astm/mllp) are `optionalDependencies`: normally installed, but if
|
|
85
|
+
* one is absent the CLI degrades to this value-free signal rather than crashing. Exit `69`. */
|
|
86
|
+
CLI_PARSER_UNAVAILABLE: "CLI_PARSER_UNAVAILABLE",
|
|
87
|
+
/** The wrapped parser rejected the input. Positional context only, never the offending bytes. Exit `65`. */
|
|
88
|
+
CLI_PARSE_FAILED: "CLI_PARSE_FAILED",
|
|
89
|
+
/** The BYO ConceptMap supplied to `map-codes` is not valid JSON or not a loadable FHIR ConceptMap.
|
|
90
|
+
* Names the stable terminology-loader code, never the map's bytes. Exit `65`. */
|
|
91
|
+
CLI_MAP_INVALID: "CLI_MAP_INVALID",
|
|
92
|
+
/** A command whose ground-layer library is not yet built (e.g. `redact` before `@cosyte/deid`
|
|
93
|
+
* ships). Never a fake success: a distinct, value-free "unavailable" signal. Exit `69`. */
|
|
94
|
+
CLI_NOT_IMPLEMENTED: "CLI_NOT_IMPLEMENTED",
|
|
95
|
+
/** An unexpected internal error (a bug). Exit `70`. */
|
|
96
|
+
CLI_INTERNAL: "CLI_INTERNAL"
|
|
97
|
+
};
|
|
98
|
+
CliError = class extends Error {
|
|
99
|
+
/** The stable diagnostic code. */
|
|
100
|
+
code;
|
|
101
|
+
/** The exit code this error resolves the invocation to. */
|
|
102
|
+
exit;
|
|
103
|
+
/**
|
|
104
|
+
* @param code - A stable {@link CliCode}.
|
|
105
|
+
* @param exit - The {@link ExitCode} to resolve to.
|
|
106
|
+
* @param message - A **value-free** explanation (positional/structural context only, never PHI).
|
|
107
|
+
*/
|
|
108
|
+
constructor(code, exit, message) {
|
|
109
|
+
super(message);
|
|
110
|
+
this.name = "CliError";
|
|
111
|
+
this.code = code;
|
|
112
|
+
this.exit = exit;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// src/core/format.ts
|
|
119
|
+
function leadingText(bytes) {
|
|
120
|
+
const slice = bytes.subarray(0, SNIFF_BYTES);
|
|
121
|
+
let text = new TextDecoder("utf-8", { fatal: false }).decode(slice);
|
|
122
|
+
if (text.charCodeAt(0) === 65279) text = text.slice(1);
|
|
123
|
+
return text;
|
|
124
|
+
}
|
|
125
|
+
function trimLeading(prefix) {
|
|
126
|
+
return prefix.replace(/^[ \t\r\n\f]+/, "");
|
|
127
|
+
}
|
|
128
|
+
function isDelimiter(ch) {
|
|
129
|
+
if (ch === void 0) return false;
|
|
130
|
+
const code = ch.charCodeAt(0);
|
|
131
|
+
return code > 32 && code < 127 && !/[A-Za-z0-9]/.test(ch);
|
|
132
|
+
}
|
|
133
|
+
function looksLikeHl7(prefix) {
|
|
134
|
+
const s = trimLeading(prefix);
|
|
135
|
+
return s.startsWith("MSH") && isDelimiter(s[3]);
|
|
136
|
+
}
|
|
137
|
+
function looksLikeMllp(_prefix, bytes) {
|
|
138
|
+
return bytes[0] === 11;
|
|
139
|
+
}
|
|
140
|
+
function looksLikeFhir(prefix) {
|
|
141
|
+
const s = trimLeading(prefix);
|
|
142
|
+
if (!s.startsWith("{")) return false;
|
|
143
|
+
return /"resourceType"\s*:\s*"/.test(s);
|
|
144
|
+
}
|
|
145
|
+
function looksLikeX12(prefix) {
|
|
146
|
+
const s = trimLeading(prefix);
|
|
147
|
+
return s.startsWith("ISA") && isDelimiter(s[3]);
|
|
148
|
+
}
|
|
149
|
+
function looksLikeAstm(prefix) {
|
|
150
|
+
const s = trimLeading(prefix);
|
|
151
|
+
if (!s.startsWith("H") || s.length < 5) return false;
|
|
152
|
+
return isDelimiter(s[1]) && isDelimiter(s[2]) && isDelimiter(s[3]) && isDelimiter(s[4]);
|
|
153
|
+
}
|
|
154
|
+
function looksLikeCcda(prefix) {
|
|
155
|
+
return /<ClinicalDocument[\s>]/.test(prefix);
|
|
156
|
+
}
|
|
157
|
+
function looksLikeNcpdp(prefix) {
|
|
158
|
+
return /<Message[\s>]/.test(prefix) && /ncpdp/i.test(prefix);
|
|
159
|
+
}
|
|
160
|
+
function looksLikeDicom(_prefix, bytes) {
|
|
161
|
+
if (bytes.length < 132) return false;
|
|
162
|
+
return bytes[128] === 68 && bytes[129] === 73 && bytes[130] === 67 && bytes[131] === 77;
|
|
163
|
+
}
|
|
164
|
+
function detectFormat(bytes) {
|
|
165
|
+
if (bytes.length === 0) return classifyCandidates([]);
|
|
166
|
+
const prefix = leadingText(bytes);
|
|
167
|
+
const candidates = SIGNATURES.filter((sig) => sig.match(prefix, bytes)).map((sig) => sig.format);
|
|
168
|
+
return classifyCandidates(candidates);
|
|
169
|
+
}
|
|
170
|
+
function classifyCandidates(candidates) {
|
|
171
|
+
const [first, second] = candidates;
|
|
172
|
+
if (second !== void 0) return { format: null, confidence: "ambiguous", candidates };
|
|
173
|
+
if (first !== void 0) return { format: first, confidence: "certain", candidates };
|
|
174
|
+
return { format: null, confidence: "none", candidates: [] };
|
|
175
|
+
}
|
|
176
|
+
function detectionError(detected) {
|
|
177
|
+
if (detected.confidence === "ambiguous") {
|
|
178
|
+
return new CliError(
|
|
179
|
+
CLI_CODES.CLI_FORMAT_AMBIGUOUS,
|
|
180
|
+
EXIT.DATAERR,
|
|
181
|
+
`input matched multiple formats (${detected.candidates.join(", ")}); disambiguate with --format`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
return new CliError(
|
|
185
|
+
CLI_CODES.CLI_FORMAT_UNDETECTED,
|
|
186
|
+
EXIT.DATAERR,
|
|
187
|
+
`could not detect the input format; re-run with --format (detectable: ${DETECTABLE_FORMATS.join(", ")})`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
function asCosyteFormat(value) {
|
|
191
|
+
return KNOWN_FORMATS.includes(value) ? value : null;
|
|
192
|
+
}
|
|
193
|
+
var SNIFF_BYTES, SIGNATURES, DETECTABLE_FORMATS, KNOWN_FORMATS;
|
|
194
|
+
var init_format = __esm({
|
|
195
|
+
"src/core/format.ts"() {
|
|
196
|
+
init_diagnostics();
|
|
197
|
+
init_exit_codes();
|
|
198
|
+
SNIFF_BYTES = 512;
|
|
199
|
+
SIGNATURES = [
|
|
200
|
+
{ format: "hl7", match: (prefix) => looksLikeHl7(prefix) },
|
|
201
|
+
{ format: "mllp", match: (prefix, bytes) => looksLikeMllp(prefix, bytes) },
|
|
202
|
+
{ format: "fhir", match: (prefix) => looksLikeFhir(prefix) },
|
|
203
|
+
{ format: "x12", match: (prefix) => looksLikeX12(prefix) },
|
|
204
|
+
{ format: "astm", match: (prefix) => looksLikeAstm(prefix) },
|
|
205
|
+
{ format: "ccda", match: (prefix) => looksLikeCcda(prefix) },
|
|
206
|
+
{ format: "ncpdp", match: (prefix) => looksLikeNcpdp(prefix) },
|
|
207
|
+
{ format: "dicom", match: (prefix, bytes) => looksLikeDicom(prefix, bytes) }
|
|
208
|
+
];
|
|
209
|
+
DETECTABLE_FORMATS = SIGNATURES.map((s) => s.format);
|
|
210
|
+
KNOWN_FORMATS = [
|
|
211
|
+
"hl7",
|
|
212
|
+
"fhir",
|
|
213
|
+
"dicom",
|
|
214
|
+
"x12",
|
|
215
|
+
"ccda",
|
|
216
|
+
"ncpdp",
|
|
217
|
+
"astm",
|
|
218
|
+
"mllp"
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// src/core/findings.ts
|
|
224
|
+
function formatHl7Position(pos) {
|
|
225
|
+
let out = `seg[${String(pos.segmentIndex)}]`;
|
|
226
|
+
if (pos.fieldIndex !== void 0) out += `.field[${String(pos.fieldIndex)}]`;
|
|
227
|
+
if (pos.repetitionIndex !== void 0) out += `.rep[${String(pos.repetitionIndex)}]`;
|
|
228
|
+
if (pos.componentIndex !== void 0) out += `.comp[${String(pos.componentIndex)}]`;
|
|
229
|
+
if (pos.subcomponentIndex !== void 0) out += `.sub[${String(pos.subcomponentIndex)}]`;
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
var init_findings = __esm({
|
|
233
|
+
"src/core/findings.ts"() {
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// src/core/parsers.ts
|
|
238
|
+
function supportsOp(format, op) {
|
|
239
|
+
return OP_SUPPORT[format].has(op);
|
|
240
|
+
}
|
|
241
|
+
function formatsSupporting(op) {
|
|
242
|
+
return Object.keys(OP_SUPPORT).filter((f) => OP_SUPPORT[f].has(op)).sort();
|
|
243
|
+
}
|
|
244
|
+
function valueFreeLocator(pos) {
|
|
245
|
+
if (typeof pos !== "object" || pos === null) return "?";
|
|
246
|
+
const parts = [];
|
|
247
|
+
for (const [k, v] of Object.entries(pos)) {
|
|
248
|
+
if (typeof v === "number") parts.push(`${k}[${String(v)}]`);
|
|
249
|
+
}
|
|
250
|
+
return parts.length > 0 ? parts.join(".") : "?";
|
|
251
|
+
}
|
|
252
|
+
function anyError(findings) {
|
|
253
|
+
return findings.some((f) => ERROR_SEVERITIES.has(f.severity));
|
|
254
|
+
}
|
|
255
|
+
async function loadOptional(format, load) {
|
|
256
|
+
try {
|
|
257
|
+
return await load();
|
|
258
|
+
} catch (e) {
|
|
259
|
+
if (isModuleNotFound(e)) {
|
|
260
|
+
throw new CliError(
|
|
261
|
+
CLI_CODES.CLI_PARSER_UNAVAILABLE,
|
|
262
|
+
EXIT.UNAVAILABLE,
|
|
263
|
+
`the @cosyte/${format} parser is not installed; install it to use this format (it is an optional dependency)`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
throw e;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function isModuleNotFound(e) {
|
|
270
|
+
if (typeof e !== "object" || e === null) return false;
|
|
271
|
+
const code = "code" in e ? e.code : void 0;
|
|
272
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") return true;
|
|
273
|
+
const message = "message" in e && typeof e.message === "string" ? e.message : "";
|
|
274
|
+
return /Cannot find (package|module)|Failed to (load|resolve)/i.test(message);
|
|
275
|
+
}
|
|
276
|
+
async function parseFormat(format, bytes) {
|
|
277
|
+
switch (format) {
|
|
278
|
+
case "hl7": {
|
|
279
|
+
const { parseHL7 } = await import('@cosyte/hl7');
|
|
280
|
+
const msg = parseHL7(Buffer.from(bytes));
|
|
281
|
+
return {
|
|
282
|
+
model: msg.toJSON(),
|
|
283
|
+
warnings: msg.warnings.map((w) => ({ code: w.code, position: w.position }))
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
case "fhir": {
|
|
287
|
+
const { parseResource, serializeResource } = await import('@cosyte/fhir');
|
|
288
|
+
const text = decode(bytes);
|
|
289
|
+
const { resource, issues } = parseResource(text);
|
|
290
|
+
const model = JSON.parse(serializeResource(resource));
|
|
291
|
+
return {
|
|
292
|
+
model,
|
|
293
|
+
warnings: issues.map((i) => ({
|
|
294
|
+
code: i.code,
|
|
295
|
+
severity: i.severity,
|
|
296
|
+
expression: i.expression
|
|
297
|
+
}))
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
case "x12": {
|
|
301
|
+
const { parseX12 } = await loadOptional("x12", () => import('@cosyte/x12'));
|
|
302
|
+
const ix = parseX12(Buffer.from(bytes));
|
|
303
|
+
return {
|
|
304
|
+
model: ix,
|
|
305
|
+
warnings: ix.warnings.map((w) => ({ code: w.code, position: w.position }))
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
case "astm": {
|
|
309
|
+
const { parseAstmRecords } = await loadOptional("astm", () => import('@cosyte/astm'));
|
|
310
|
+
const msg = parseAstmRecords(decode(bytes));
|
|
311
|
+
return {
|
|
312
|
+
model: msg,
|
|
313
|
+
warnings: msg.warnings.map((w) => ({ code: w.code, position: w.position }))
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
case "ncpdp": {
|
|
317
|
+
const { parseScript } = await loadOptional("ncpdp", () => import('@cosyte/ncpdp'));
|
|
318
|
+
const msg = parseScript(decode(bytes));
|
|
319
|
+
return {
|
|
320
|
+
model: msg,
|
|
321
|
+
warnings: msg.warnings.map((w) => ({ code: w.code, position: w.position }))
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
case "dicom":
|
|
325
|
+
case "ccda":
|
|
326
|
+
case "mllp":
|
|
327
|
+
throw unsupportedInternal(format, "parse");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async function inspectFormat(format, bytes) {
|
|
331
|
+
switch (format) {
|
|
332
|
+
case "hl7": {
|
|
333
|
+
const { parseHL7 } = await import('@cosyte/hl7');
|
|
334
|
+
const msg = parseHL7(Buffer.from(bytes));
|
|
335
|
+
const segments = {};
|
|
336
|
+
const all = msg.allSegments();
|
|
337
|
+
for (const seg of all) segments[seg.type] = (segments[seg.type] ?? 0) + 1;
|
|
338
|
+
return {
|
|
339
|
+
format: "hl7",
|
|
340
|
+
messageType: msg.meta.type ?? null,
|
|
341
|
+
version: msg.meta.version ?? null,
|
|
342
|
+
segmentCount: all.length,
|
|
343
|
+
segments,
|
|
344
|
+
warningCount: msg.warnings.length
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
case "fhir": {
|
|
348
|
+
const { parseResource, resourceType, readBundle } = await import('@cosyte/fhir');
|
|
349
|
+
const { resource, issues } = parseResource(decode(bytes));
|
|
350
|
+
const rt = resourceType(resource) ?? null;
|
|
351
|
+
if (rt === "Bundle") {
|
|
352
|
+
const bundle = readBundle(resource);
|
|
353
|
+
const entryResourceTypes = {};
|
|
354
|
+
for (const e of bundle.entries) {
|
|
355
|
+
const t = e.resourceType ?? "(none)";
|
|
356
|
+
entryResourceTypes[t] = (entryResourceTypes[t] ?? 0) + 1;
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
format: "fhir",
|
|
360
|
+
resourceType: rt,
|
|
361
|
+
bundleType: bundle.type ?? null,
|
|
362
|
+
entryCount: bundle.entries.length,
|
|
363
|
+
entryResourceTypes,
|
|
364
|
+
issueCount: issues.length
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
return { format: "fhir", resourceType: rt, issueCount: issues.length };
|
|
368
|
+
}
|
|
369
|
+
case "x12": {
|
|
370
|
+
const { parseX12 } = await loadOptional("x12", () => import('@cosyte/x12'));
|
|
371
|
+
const ix = parseX12(Buffer.from(bytes));
|
|
372
|
+
const transactionSetIds = {};
|
|
373
|
+
let transactionCount = 0;
|
|
374
|
+
let segmentCount = 0;
|
|
375
|
+
for (const g of ix.groups) {
|
|
376
|
+
for (const t of g.transactions) {
|
|
377
|
+
transactionCount += 1;
|
|
378
|
+
segmentCount += t.segments.length;
|
|
379
|
+
const id = t.st.elements[1] ?? "(unknown)";
|
|
380
|
+
transactionSetIds[id] = (transactionSetIds[id] ?? 0) + 1;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
format: "x12",
|
|
385
|
+
groupCount: ix.groups.length,
|
|
386
|
+
transactionCount,
|
|
387
|
+
transactionSetIds,
|
|
388
|
+
segmentCount,
|
|
389
|
+
warningCount: ix.warnings.length
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
case "astm": {
|
|
393
|
+
const { parseAstmRecords } = await loadOptional("astm", () => import('@cosyte/astm'));
|
|
394
|
+
const msg = parseAstmRecords(decode(bytes));
|
|
395
|
+
const recordTypes = {};
|
|
396
|
+
for (const r of msg.records) recordTypes[r.type] = (recordTypes[r.type] ?? 0) + 1;
|
|
397
|
+
return {
|
|
398
|
+
format: "astm",
|
|
399
|
+
messageKind: msg.classification.kind,
|
|
400
|
+
recordCount: msg.records.length,
|
|
401
|
+
recordTypes,
|
|
402
|
+
warningCount: msg.warnings.length
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
case "ccda": {
|
|
406
|
+
const { parseCcda } = await loadOptional("ccda", () => import('@cosyte/ccda'));
|
|
407
|
+
const doc = parseCcda(decode(bytes));
|
|
408
|
+
const sectionCodes = [];
|
|
409
|
+
for (const s of doc.sections) {
|
|
410
|
+
const code = s.code?.code;
|
|
411
|
+
if (typeof code === "string") sectionCodes.push(code);
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
format: "ccda",
|
|
415
|
+
documentType: doc.documentType ?? null,
|
|
416
|
+
sectionCount: doc.sections.length,
|
|
417
|
+
sectionCodes,
|
|
418
|
+
warningCount: doc.warnings.length
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
case "dicom": {
|
|
422
|
+
const { parseDicom } = await loadOptional("dicom", () => import('@cosyte/dicom'));
|
|
423
|
+
let warningCount = 0;
|
|
424
|
+
const ds = parseDicom(Buffer.from(bytes), { onWarning: () => warningCount += 1 });
|
|
425
|
+
return {
|
|
426
|
+
format: "dicom",
|
|
427
|
+
sopClassUid: ds.fileMeta?.mediaStorageSOPClassUID ?? null,
|
|
428
|
+
transferSyntaxUid: ds.fileMeta?.transferSyntaxUID ?? null,
|
|
429
|
+
elementCount: ds.elements().length,
|
|
430
|
+
warningCount
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
case "ncpdp": {
|
|
434
|
+
const { parseScript } = await loadOptional("ncpdp", () => import('@cosyte/ncpdp'));
|
|
435
|
+
const msg = parseScript(decode(bytes));
|
|
436
|
+
return {
|
|
437
|
+
format: "ncpdp",
|
|
438
|
+
standard: "SCRIPT",
|
|
439
|
+
messageType: msg.body.kind ?? null,
|
|
440
|
+
version: msg.header.version ?? null,
|
|
441
|
+
warningCount: msg.warnings.length
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
case "mllp": {
|
|
445
|
+
const { payloads, warningCount } = await deframeMllp(bytes);
|
|
446
|
+
return { format: "mllp", frameCount: payloads.length, warningCount };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async function fmtFormat(format, bytes) {
|
|
451
|
+
switch (format) {
|
|
452
|
+
case "hl7": {
|
|
453
|
+
const { parseHL7 } = await import('@cosyte/hl7');
|
|
454
|
+
const msg = parseHL7(Buffer.from(bytes));
|
|
455
|
+
return { output: msg.toString(), warningCount: msg.warnings.length };
|
|
456
|
+
}
|
|
457
|
+
case "fhir": {
|
|
458
|
+
const { parseResource, serializeResource } = await import('@cosyte/fhir');
|
|
459
|
+
const { resource, issues } = parseResource(decode(bytes));
|
|
460
|
+
return { output: serializeResource(resource), warningCount: issues.length };
|
|
461
|
+
}
|
|
462
|
+
case "x12": {
|
|
463
|
+
const { parseX12, serializeX12 } = await loadOptional("x12", () => import('@cosyte/x12'));
|
|
464
|
+
const ix = parseX12(Buffer.from(bytes));
|
|
465
|
+
return { output: serializeX12(ix), warningCount: ix.warnings.length };
|
|
466
|
+
}
|
|
467
|
+
case "astm": {
|
|
468
|
+
const { parseAstmRecords, serializeAstmRecords } = await loadOptional(
|
|
469
|
+
"astm",
|
|
470
|
+
() => import('@cosyte/astm')
|
|
471
|
+
);
|
|
472
|
+
const msg = parseAstmRecords(decode(bytes));
|
|
473
|
+
return { output: serializeAstmRecords(msg), warningCount: msg.warnings.length };
|
|
474
|
+
}
|
|
475
|
+
case "ccda": {
|
|
476
|
+
const { parseCcda, serializeCcda } = await loadOptional("ccda", () => import('@cosyte/ccda'));
|
|
477
|
+
const doc = parseCcda(decode(bytes));
|
|
478
|
+
return { output: serializeCcda(doc), warningCount: doc.warnings.length };
|
|
479
|
+
}
|
|
480
|
+
case "ncpdp": {
|
|
481
|
+
const { parseScript, serializeScript } = await loadOptional(
|
|
482
|
+
"ncpdp",
|
|
483
|
+
() => import('@cosyte/ncpdp')
|
|
484
|
+
);
|
|
485
|
+
const msg = parseScript(decode(bytes));
|
|
486
|
+
return { output: serializeScript(msg), warningCount: msg.warnings.length };
|
|
487
|
+
}
|
|
488
|
+
case "dicom":
|
|
489
|
+
case "mllp":
|
|
490
|
+
throw unsupportedInternal(format, "fmt");
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
async function validateFormat(format, bytes) {
|
|
494
|
+
switch (format) {
|
|
495
|
+
case "fhir": {
|
|
496
|
+
const { parseResource, validateResource } = await import('@cosyte/fhir');
|
|
497
|
+
const { resource, issues } = parseResource(decode(bytes));
|
|
498
|
+
const validation = validateResource(resource);
|
|
499
|
+
const findings = [
|
|
500
|
+
...issues.map((i) => finding(i.code, i.severity, i.expression)),
|
|
501
|
+
...validation.issues.map((i) => finding(i.code, i.severity, i.expression, i.constraint))
|
|
502
|
+
];
|
|
503
|
+
return { findings, valid: !anyError(findings) };
|
|
504
|
+
}
|
|
505
|
+
case "hl7": {
|
|
506
|
+
const { parseHL7 } = await import('@cosyte/hl7');
|
|
507
|
+
const msg = parseHL7(Buffer.from(bytes));
|
|
508
|
+
const findings = msg.warnings.map(
|
|
509
|
+
(w) => finding(w.code, "warning", formatHl7Position(w.position))
|
|
510
|
+
);
|
|
511
|
+
return { findings, valid: !anyError(findings) };
|
|
512
|
+
}
|
|
513
|
+
case "x12": {
|
|
514
|
+
const { parseX12 } = await loadOptional("x12", () => import('@cosyte/x12'));
|
|
515
|
+
const ix = parseX12(Buffer.from(bytes));
|
|
516
|
+
const findings = ix.warnings.map(
|
|
517
|
+
(w) => finding(w.code, "warning", valueFreeLocator(w.position))
|
|
518
|
+
);
|
|
519
|
+
return { findings, valid: !anyError(findings) };
|
|
520
|
+
}
|
|
521
|
+
case "astm": {
|
|
522
|
+
const { parseAstmRecords } = await loadOptional("astm", () => import('@cosyte/astm'));
|
|
523
|
+
const msg = parseAstmRecords(decode(bytes));
|
|
524
|
+
const findings = msg.warnings.map(
|
|
525
|
+
(w) => finding(w.code, "warning", valueFreeLocator(w.position))
|
|
526
|
+
);
|
|
527
|
+
return { findings, valid: !anyError(findings) };
|
|
528
|
+
}
|
|
529
|
+
case "ccda": {
|
|
530
|
+
const { parseCcda } = await loadOptional("ccda", () => import('@cosyte/ccda'));
|
|
531
|
+
const doc = parseCcda(decode(bytes));
|
|
532
|
+
const findings = doc.warnings.map(
|
|
533
|
+
(w) => finding(w.code, "warning", valueFreeLocator(w.position))
|
|
534
|
+
);
|
|
535
|
+
return { findings, valid: !anyError(findings) };
|
|
536
|
+
}
|
|
537
|
+
case "dicom": {
|
|
538
|
+
const { parseDicom } = await loadOptional("dicom", () => import('@cosyte/dicom'));
|
|
539
|
+
const findings = [];
|
|
540
|
+
parseDicom(Buffer.from(bytes), {
|
|
541
|
+
onWarning: (w) => findings.push(finding(w.code, "warning", valueFreeLocator(w.position)))
|
|
542
|
+
});
|
|
543
|
+
return { findings, valid: !anyError(findings) };
|
|
544
|
+
}
|
|
545
|
+
case "ncpdp": {
|
|
546
|
+
const { parseScript } = await loadOptional("ncpdp", () => import('@cosyte/ncpdp'));
|
|
547
|
+
const msg = parseScript(decode(bytes));
|
|
548
|
+
const findings = msg.warnings.map(
|
|
549
|
+
(w) => finding(w.code, "warning", valueFreeLocator(w.position))
|
|
550
|
+
);
|
|
551
|
+
return { findings, valid: !anyError(findings) };
|
|
552
|
+
}
|
|
553
|
+
case "mllp":
|
|
554
|
+
throw unsupportedInternal(format, "validate");
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
async function deframeMllp(bytes) {
|
|
558
|
+
if (bytes.lastIndexOf(11) > bytes.lastIndexOf(28)) {
|
|
559
|
+
throw new CliError(
|
|
560
|
+
CLI_CODES.CLI_PARSE_FAILED,
|
|
561
|
+
EXIT.DATAERR,
|
|
562
|
+
"truncated MLLP stream: an unterminated frame at the end of input (no closing FS/CR)"
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
const { FrameReader } = await loadOptional("mllp", () => import('@cosyte/mllp'));
|
|
566
|
+
const payloads = [];
|
|
567
|
+
let warningCount = 0;
|
|
568
|
+
const reader = new FrameReader({
|
|
569
|
+
onFrame: (payload) => payloads.push(Buffer.from(payload)),
|
|
570
|
+
onWarning: () => warningCount += 1
|
|
571
|
+
});
|
|
572
|
+
reader.push(Buffer.from(bytes));
|
|
573
|
+
return { payloads, warningCount };
|
|
574
|
+
}
|
|
575
|
+
function decode(bytes) {
|
|
576
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
577
|
+
}
|
|
578
|
+
function finding(code, severity, location, constraint) {
|
|
579
|
+
const loc = constraint !== void 0 ? `${location} (${constraint})` : location;
|
|
580
|
+
return { code, severity, location: loc };
|
|
581
|
+
}
|
|
582
|
+
function unsupportedInternal(format, op) {
|
|
583
|
+
return new CliError(
|
|
584
|
+
CLI_CODES.CLI_FORMAT_UNSUPPORTED,
|
|
585
|
+
EXIT.DATAERR,
|
|
586
|
+
`format '${format}' does not support \`${op}\``
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
var OP_SUPPORT, ERROR_SEVERITIES;
|
|
590
|
+
var init_parsers = __esm({
|
|
591
|
+
"src/core/parsers.ts"() {
|
|
592
|
+
init_diagnostics();
|
|
593
|
+
init_exit_codes();
|
|
594
|
+
init_findings();
|
|
595
|
+
OP_SUPPORT = {
|
|
596
|
+
hl7: /* @__PURE__ */ new Set(["parse", "inspect", "fmt", "validate"]),
|
|
597
|
+
fhir: /* @__PURE__ */ new Set(["parse", "inspect", "fmt", "validate"]),
|
|
598
|
+
x12: /* @__PURE__ */ new Set(["parse", "inspect", "fmt", "validate"]),
|
|
599
|
+
astm: /* @__PURE__ */ new Set(["parse", "inspect", "fmt", "validate"]),
|
|
600
|
+
ncpdp: /* @__PURE__ */ new Set(["parse", "inspect", "fmt", "validate"]),
|
|
601
|
+
ccda: /* @__PURE__ */ new Set(["inspect", "fmt", "validate"]),
|
|
602
|
+
dicom: /* @__PURE__ */ new Set(["inspect", "validate"]),
|
|
603
|
+
mllp: /* @__PURE__ */ new Set(["parse", "inspect"])
|
|
604
|
+
};
|
|
605
|
+
ERROR_SEVERITIES = /* @__PURE__ */ new Set(["error", "fatal"]);
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// src/core/input.ts
|
|
610
|
+
async function resolveInput(source, formatOverride, deps2, op) {
|
|
611
|
+
if (source === void 0) {
|
|
612
|
+
return fail(
|
|
613
|
+
new CliError(
|
|
614
|
+
CLI_CODES.CLI_USAGE,
|
|
615
|
+
EXIT.USAGE,
|
|
616
|
+
"missing <file> argument; pass a path or `-` to read stdin"
|
|
617
|
+
)
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
let bytes;
|
|
621
|
+
try {
|
|
622
|
+
bytes = source === "-" ? await deps2.readStdin() : await deps2.readFile(source);
|
|
623
|
+
} catch (e) {
|
|
624
|
+
if (e instanceof CliError) return fail(e);
|
|
625
|
+
throw e;
|
|
626
|
+
}
|
|
627
|
+
if (bytes.length === 0) {
|
|
628
|
+
return fail(new CliError(CLI_CODES.CLI_EMPTY_INPUT, EXIT.DATAERR, "input is empty"));
|
|
629
|
+
}
|
|
630
|
+
let format;
|
|
631
|
+
if (formatOverride !== void 0) {
|
|
632
|
+
const narrowed = asCosyteFormat(formatOverride);
|
|
633
|
+
if (narrowed === null) {
|
|
634
|
+
return fail(
|
|
635
|
+
new CliError(
|
|
636
|
+
CLI_CODES.CLI_USAGE,
|
|
637
|
+
EXIT.USAGE,
|
|
638
|
+
"unknown --format value; expected one of hl7, fhir, dicom, x12, ccda, ncpdp, astm, mllp"
|
|
639
|
+
)
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
format = narrowed;
|
|
643
|
+
} else {
|
|
644
|
+
const detected = detectFormat(bytes);
|
|
645
|
+
if (detected.format === null) return fail(detectionError(detected));
|
|
646
|
+
format = detected.format;
|
|
647
|
+
}
|
|
648
|
+
if (!supportsOp(format, op)) {
|
|
649
|
+
return fail(
|
|
650
|
+
new CliError(
|
|
651
|
+
CLI_CODES.CLI_FORMAT_UNSUPPORTED,
|
|
652
|
+
EXIT.DATAERR,
|
|
653
|
+
`format '${format}' does not support \`${op}\` in this CLI build (${op} supports: ${formatsSupporting(op).join(", ")})`
|
|
654
|
+
)
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
return { ok: true, input: { format, bytes } };
|
|
658
|
+
}
|
|
659
|
+
function fail(e) {
|
|
660
|
+
return { ok: false, result: errorResult(e) };
|
|
661
|
+
}
|
|
662
|
+
var init_input = __esm({
|
|
663
|
+
"src/core/input.ts"() {
|
|
664
|
+
init_format();
|
|
665
|
+
init_diagnostics();
|
|
666
|
+
init_exit_codes();
|
|
667
|
+
init_parsers();
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// src/core/phi.ts
|
|
672
|
+
function extractPhiPosture(argv) {
|
|
673
|
+
const showValues = argv.includes(UNSAFE_SHOW_VALUES_FLAG);
|
|
674
|
+
const cleaned = argv.filter((a) => a !== UNSAFE_SHOW_VALUES_FLAG);
|
|
675
|
+
return { posture: showValues ? SHOW_VALUES : VALUE_FREE, argv: cleaned };
|
|
676
|
+
}
|
|
677
|
+
function unsafeInputSuffix(bytes, posture) {
|
|
678
|
+
if (!posture.showValues) return "";
|
|
679
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(0, UNSAFE_EXCERPT_MAX)).replace(/[\r\n\t]+/g, " ").trim();
|
|
680
|
+
if (text.length === 0) return "";
|
|
681
|
+
return ` [unsafe-show-values] offending input (first ${String(UNSAFE_EXCERPT_MAX)} bytes): ${text}`;
|
|
682
|
+
}
|
|
683
|
+
var VALUE_FREE, SHOW_VALUES, UNSAFE_SHOW_VALUES_FLAG, UNSAFE_EXCERPT_MAX;
|
|
684
|
+
var init_phi = __esm({
|
|
685
|
+
"src/core/phi.ts"() {
|
|
686
|
+
VALUE_FREE = { showValues: false };
|
|
687
|
+
SHOW_VALUES = { showValues: true };
|
|
688
|
+
UNSAFE_SHOW_VALUES_FLAG = "--unsafe-show-values";
|
|
689
|
+
UNSAFE_EXCERPT_MAX = 200;
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// src/core/wrap.ts
|
|
694
|
+
function extractStableCode(e) {
|
|
695
|
+
if (typeof e === "object" && e !== null && "code" in e) {
|
|
696
|
+
const raw = e.code;
|
|
697
|
+
if (typeof raw === "string" && /^[A-Z][A-Z0-9_]*$/.test(raw)) return raw;
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
function parseFailureResult(format, bytes, posture, e) {
|
|
702
|
+
const code = extractStableCode(e);
|
|
703
|
+
const detail = code === null ? "" : ` (${code})`;
|
|
704
|
+
const suffix = unsafeInputSuffix(bytes, posture);
|
|
705
|
+
return {
|
|
706
|
+
stdout: "",
|
|
707
|
+
stderr: `cosyte: ${CLI_CODES.CLI_PARSE_FAILED}: the ${format} parser rejected the input${detail}${suffix}
|
|
708
|
+
`,
|
|
709
|
+
exit: EXIT.DATAERR
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
var init_wrap = __esm({
|
|
713
|
+
"src/core/wrap.ts"() {
|
|
714
|
+
init_diagnostics();
|
|
715
|
+
init_exit_codes();
|
|
716
|
+
init_phi();
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
async function convertCommand(args, deps2, posture = VALUE_FREE) {
|
|
720
|
+
let values;
|
|
721
|
+
let positionals;
|
|
722
|
+
try {
|
|
723
|
+
const parsed = parseArgs({ args, options: CONVERT_OPTIONS, allowPositionals: true });
|
|
724
|
+
values = parsed.values;
|
|
725
|
+
positionals = parsed.positionals;
|
|
726
|
+
} catch {
|
|
727
|
+
return errorResult(
|
|
728
|
+
new CliError(
|
|
729
|
+
CLI_CODES.CLI_USAGE,
|
|
730
|
+
EXIT.USAGE,
|
|
731
|
+
"invalid arguments to `convert` (see `cosyte convert --help`)"
|
|
732
|
+
)
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
if (values.to === void 0) {
|
|
736
|
+
return errorResult(
|
|
737
|
+
new CliError(CLI_CODES.CLI_USAGE, EXIT.USAGE, "convert requires --to fhir (the only target)")
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
if (values.to !== SUPPORTED_TARGET) {
|
|
741
|
+
return errorResult(
|
|
742
|
+
new CliError(
|
|
743
|
+
CLI_CODES.CLI_USAGE,
|
|
744
|
+
EXIT.USAGE,
|
|
745
|
+
`unsupported --to '${values.to}'; only --to fhir is supported (HL7 v2 \u2192 FHIR R4)`
|
|
746
|
+
)
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
const resolved = await resolveInput(positionals[0], values.format, deps2, "parse");
|
|
750
|
+
if (!resolved.ok) return resolved.result;
|
|
751
|
+
const { format, bytes } = resolved.input;
|
|
752
|
+
if (format !== "hl7") {
|
|
753
|
+
return errorResult(
|
|
754
|
+
new CliError(
|
|
755
|
+
CLI_CODES.CLI_FORMAT_UNSUPPORTED,
|
|
756
|
+
EXIT.DATAERR,
|
|
757
|
+
`convert reads HL7 v2 and writes FHIR R4; the input is '${format}', not an HL7 v2 source`
|
|
758
|
+
)
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
const outcome = await runConvert(bytes, posture);
|
|
762
|
+
if (!outcome.ok) return outcome.result;
|
|
763
|
+
const conversion = outcome.conversion;
|
|
764
|
+
const { hasError, report } = convertOutcome(conversion.findings);
|
|
765
|
+
const exit = hasError ? EXIT.INVALID : EXIT.OK;
|
|
766
|
+
if (values.json === true) {
|
|
767
|
+
const body = {
|
|
768
|
+
format: SUPPORTED_TARGET,
|
|
769
|
+
bundle: JSON.parse(conversion.bundleText),
|
|
770
|
+
findings: conversion.findings
|
|
771
|
+
};
|
|
772
|
+
return { stdout: `${JSON.stringify(body)}
|
|
773
|
+
`, stderr: "", exit };
|
|
774
|
+
}
|
|
775
|
+
const stderr = values.quiet === true ? "" : report;
|
|
776
|
+
return { stdout: `${conversion.bundleText}
|
|
777
|
+
`, stderr, exit };
|
|
778
|
+
}
|
|
779
|
+
async function runConvert(bytes, posture) {
|
|
780
|
+
const [{ parseHL7 }, { toFhir }, { serializeResource }] = await Promise.all([
|
|
781
|
+
import('@cosyte/hl7'),
|
|
782
|
+
import('@cosyte/transform'),
|
|
783
|
+
import('@cosyte/fhir')
|
|
784
|
+
]);
|
|
785
|
+
let msg;
|
|
786
|
+
try {
|
|
787
|
+
msg = parseHL7(Buffer.from(bytes));
|
|
788
|
+
} catch (e) {
|
|
789
|
+
return { ok: false, result: parseFailureResult("hl7", bytes, posture, e) };
|
|
790
|
+
}
|
|
791
|
+
const { bundle, issues } = toFhir(msg);
|
|
792
|
+
return {
|
|
793
|
+
ok: true,
|
|
794
|
+
conversion: {
|
|
795
|
+
bundleText: serializeResource(bundle),
|
|
796
|
+
findings: issues.map((i) => toFinding(i.code, i.severity, i.v2Location, i.fhirPath))
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function toFinding(code, severity, v2Location, fhirPath) {
|
|
801
|
+
const location = fhirPath !== void 0 ? `${v2Location} \u2192 ${fhirPath}` : v2Location;
|
|
802
|
+
return { code, severity, location };
|
|
803
|
+
}
|
|
804
|
+
function convertOutcome(findings) {
|
|
805
|
+
const lines = findings.map((f) => `cosyte: convert: ${f.severity} ${f.code} at ${f.location}
|
|
806
|
+
`);
|
|
807
|
+
const errors = findings.filter((f) => ERROR_SEVERITIES2.has(f.severity)).length;
|
|
808
|
+
const others = findings.length - errors;
|
|
809
|
+
const summary = errors > 0 ? `cosyte: convert: hl7 \u2192 fhir produced ${String(errors)} error(s), ${String(others)} other finding(s)
|
|
810
|
+
` : `cosyte: convert: hl7 \u2192 fhir OK (${String(findings.length)} finding(s))
|
|
811
|
+
`;
|
|
812
|
+
return { hasError: errors > 0, report: lines.join("") + summary };
|
|
813
|
+
}
|
|
814
|
+
var CONVERT_OPTIONS, SUPPORTED_TARGET, ERROR_SEVERITIES2;
|
|
815
|
+
var init_convert = __esm({
|
|
816
|
+
"src/commands/convert.ts"() {
|
|
817
|
+
init_diagnostics();
|
|
818
|
+
init_exit_codes();
|
|
819
|
+
init_input();
|
|
820
|
+
init_phi();
|
|
821
|
+
init_wrap();
|
|
822
|
+
CONVERT_OPTIONS = {
|
|
823
|
+
to: { type: "string" },
|
|
824
|
+
format: { type: "string" },
|
|
825
|
+
json: { type: "boolean", default: false },
|
|
826
|
+
quiet: { type: "boolean", default: false },
|
|
827
|
+
"no-color": { type: "boolean", default: false }
|
|
828
|
+
};
|
|
829
|
+
SUPPORTED_TARGET = "fhir";
|
|
830
|
+
ERROR_SEVERITIES2 = /* @__PURE__ */ new Set(["error", "fatal"]);
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
async function inspectCommand(args, deps2, posture = VALUE_FREE) {
|
|
834
|
+
let values;
|
|
835
|
+
let positionals;
|
|
836
|
+
try {
|
|
837
|
+
const parsed = parseArgs({ args, options: INSPECT_OPTIONS, allowPositionals: true });
|
|
838
|
+
values = parsed.values;
|
|
839
|
+
positionals = parsed.positionals;
|
|
840
|
+
} catch {
|
|
841
|
+
return errorResult(
|
|
842
|
+
new CliError(
|
|
843
|
+
CLI_CODES.CLI_USAGE,
|
|
844
|
+
EXIT.USAGE,
|
|
845
|
+
"invalid arguments to `inspect` (see `cosyte inspect --help`)"
|
|
846
|
+
)
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
const resolved = await resolveInput(positionals[0], values.format, deps2, "inspect");
|
|
850
|
+
if (!resolved.ok) return resolved.result;
|
|
851
|
+
const { format, bytes } = resolved.input;
|
|
852
|
+
let summary;
|
|
853
|
+
try {
|
|
854
|
+
summary = await inspectFormat(format, bytes);
|
|
855
|
+
} catch (e) {
|
|
856
|
+
if (e instanceof CliError) return errorResult(e);
|
|
857
|
+
return parseFailureResult(format, bytes, posture, e);
|
|
858
|
+
}
|
|
859
|
+
const stdout = values.json === true ? `${JSON.stringify(summary)}
|
|
860
|
+
` : renderSummary(summary);
|
|
861
|
+
return { stdout, stderr: "", exit: EXIT.OK };
|
|
862
|
+
}
|
|
863
|
+
function renderSummary(summary) {
|
|
864
|
+
const lines = [];
|
|
865
|
+
const countLines = (label, counts) => {
|
|
866
|
+
if (label.length > 0) lines.push(label);
|
|
867
|
+
for (const key of Object.keys(counts).sort()) lines.push(` ${key}: ${String(counts[key])}`);
|
|
868
|
+
};
|
|
869
|
+
switch (summary.format) {
|
|
870
|
+
case "hl7":
|
|
871
|
+
lines.push(`format: hl7`);
|
|
872
|
+
lines.push(`message type: ${summary.messageType ?? "(unknown)"}`);
|
|
873
|
+
lines.push(`version: ${summary.version ?? "(unknown)"}`);
|
|
874
|
+
lines.push(`segments: ${String(summary.segmentCount)}`);
|
|
875
|
+
countLines("", summary.segments);
|
|
876
|
+
lines.push(`warnings: ${String(summary.warningCount)}`);
|
|
877
|
+
break;
|
|
878
|
+
case "fhir":
|
|
879
|
+
lines.push(`format: fhir`);
|
|
880
|
+
lines.push(`resource type: ${summary.resourceType ?? "(unknown)"}`);
|
|
881
|
+
if (summary.entryCount !== void 0) {
|
|
882
|
+
lines.push(`bundle type: ${summary.bundleType ?? "(unknown)"}`);
|
|
883
|
+
lines.push(`entries: ${String(summary.entryCount)}`);
|
|
884
|
+
countLines("", summary.entryResourceTypes ?? {});
|
|
885
|
+
}
|
|
886
|
+
lines.push(`issues: ${String(summary.issueCount)}`);
|
|
887
|
+
break;
|
|
888
|
+
case "x12":
|
|
889
|
+
lines.push(`format: x12`);
|
|
890
|
+
lines.push(`groups: ${String(summary.groupCount)}`);
|
|
891
|
+
lines.push(`transactions: ${String(summary.transactionCount)}`);
|
|
892
|
+
countLines("transaction sets:", summary.transactionSetIds);
|
|
893
|
+
lines.push(`segments: ${String(summary.segmentCount)}`);
|
|
894
|
+
lines.push(`warnings: ${String(summary.warningCount)}`);
|
|
895
|
+
break;
|
|
896
|
+
case "astm":
|
|
897
|
+
lines.push(`format: astm`);
|
|
898
|
+
lines.push(`message kind: ${summary.messageKind}`);
|
|
899
|
+
lines.push(`records: ${String(summary.recordCount)}`);
|
|
900
|
+
countLines("", summary.recordTypes);
|
|
901
|
+
lines.push(`warnings: ${String(summary.warningCount)}`);
|
|
902
|
+
break;
|
|
903
|
+
case "ccda":
|
|
904
|
+
lines.push(`format: ccda`);
|
|
905
|
+
lines.push(`document type: ${summary.documentType ?? "(unknown)"}`);
|
|
906
|
+
lines.push(`sections: ${String(summary.sectionCount)}`);
|
|
907
|
+
if (summary.sectionCodes.length > 0) {
|
|
908
|
+
lines.push(`section codes: ${summary.sectionCodes.join(", ")}`);
|
|
909
|
+
}
|
|
910
|
+
lines.push(`warnings: ${String(summary.warningCount)}`);
|
|
911
|
+
break;
|
|
912
|
+
case "dicom":
|
|
913
|
+
lines.push(`format: dicom`);
|
|
914
|
+
lines.push(`sop class uid: ${summary.sopClassUid ?? "(unknown)"}`);
|
|
915
|
+
lines.push(`transfer syntax:${summary.transferSyntaxUid ?? " (unknown)"}`);
|
|
916
|
+
lines.push(`elements: ${String(summary.elementCount)}`);
|
|
917
|
+
lines.push(`warnings: ${String(summary.warningCount)}`);
|
|
918
|
+
break;
|
|
919
|
+
case "ncpdp":
|
|
920
|
+
lines.push(`format: ncpdp`);
|
|
921
|
+
lines.push(`standard: SCRIPT`);
|
|
922
|
+
lines.push(`message type: ${summary.messageType ?? "(unknown)"}`);
|
|
923
|
+
lines.push(`version: ${summary.version ?? "(unknown)"}`);
|
|
924
|
+
lines.push(`warnings: ${String(summary.warningCount)}`);
|
|
925
|
+
break;
|
|
926
|
+
case "mllp":
|
|
927
|
+
lines.push(`format: mllp`);
|
|
928
|
+
lines.push(`frames: ${String(summary.frameCount)}`);
|
|
929
|
+
lines.push(`warnings:${String(summary.warningCount)}`);
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
return lines.join("\n") + "\n";
|
|
933
|
+
}
|
|
934
|
+
var INSPECT_OPTIONS;
|
|
935
|
+
var init_inspect = __esm({
|
|
936
|
+
"src/commands/inspect.ts"() {
|
|
937
|
+
init_diagnostics();
|
|
938
|
+
init_exit_codes();
|
|
939
|
+
init_input();
|
|
940
|
+
init_parsers();
|
|
941
|
+
init_phi();
|
|
942
|
+
init_wrap();
|
|
943
|
+
INSPECT_OPTIONS = {
|
|
944
|
+
format: { type: "string" },
|
|
945
|
+
json: { type: "boolean", default: false },
|
|
946
|
+
quiet: { type: "boolean", default: false },
|
|
947
|
+
"no-color": { type: "boolean", default: false }
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
async function parseCommand(args, deps2, posture = VALUE_FREE) {
|
|
952
|
+
let values;
|
|
953
|
+
let positionals;
|
|
954
|
+
try {
|
|
955
|
+
const parsed = parseArgs({ args, options: PARSE_OPTIONS, allowPositionals: true });
|
|
956
|
+
values = parsed.values;
|
|
957
|
+
positionals = parsed.positionals;
|
|
958
|
+
} catch {
|
|
959
|
+
return errorResult(
|
|
960
|
+
new CliError(
|
|
961
|
+
CLI_CODES.CLI_USAGE,
|
|
962
|
+
EXIT.USAGE,
|
|
963
|
+
"invalid arguments to `parse` (see `cosyte parse --help`)"
|
|
964
|
+
)
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
const resolved = await resolveInput(positionals[0], values.format, deps2, "parse");
|
|
968
|
+
if (!resolved.ok) return resolved.result;
|
|
969
|
+
const { format, bytes } = resolved.input;
|
|
970
|
+
if (format === "mllp" || values.ndjson === true) {
|
|
971
|
+
return await parseMulti(format, bytes, values.ndjson === true, values.quiet === true);
|
|
972
|
+
}
|
|
973
|
+
return await parseSingle(format, bytes, values.json === true, values.quiet === true, posture);
|
|
974
|
+
}
|
|
975
|
+
async function parseSingle(format, bytes, json, quiet, posture) {
|
|
976
|
+
let result;
|
|
977
|
+
try {
|
|
978
|
+
result = await parseFormat(format, bytes);
|
|
979
|
+
} catch (e) {
|
|
980
|
+
if (e instanceof CliError) return errorResult(e);
|
|
981
|
+
return parseFailureResult(format, bytes, posture, e);
|
|
982
|
+
}
|
|
983
|
+
const envelope = { format, model: result.model, warnings: result.warnings };
|
|
984
|
+
const stdout = (json ? JSON.stringify(envelope) : JSON.stringify(envelope, null, 2)) + "\n";
|
|
985
|
+
const n = envelope.warnings.length;
|
|
986
|
+
const stderr = n > 0 && !quiet ? `cosyte: parsed ${format} with ${String(n)} warning(s) (see .warnings in output)
|
|
987
|
+
` : "";
|
|
988
|
+
return { stdout, stderr, exit: EXIT.OK };
|
|
989
|
+
}
|
|
990
|
+
async function parseMulti(format, bytes, ndjson, quiet) {
|
|
991
|
+
let records;
|
|
992
|
+
let recordFormat;
|
|
993
|
+
try {
|
|
994
|
+
if (format === "mllp") {
|
|
995
|
+
const { payloads } = await deframeMllp(bytes);
|
|
996
|
+
records = payloads;
|
|
997
|
+
recordFormat = "hl7";
|
|
998
|
+
} else {
|
|
999
|
+
records = splitLines(bytes);
|
|
1000
|
+
recordFormat = format;
|
|
1001
|
+
}
|
|
1002
|
+
} catch (e) {
|
|
1003
|
+
if (e instanceof CliError) return errorResult(e);
|
|
1004
|
+
return parseFailureResult(format, bytes, VALUE_FREE, e);
|
|
1005
|
+
}
|
|
1006
|
+
if (records.length === 0) {
|
|
1007
|
+
return errorResult(
|
|
1008
|
+
new CliError(
|
|
1009
|
+
CLI_CODES.CLI_PARSE_FAILED,
|
|
1010
|
+
EXIT.DATAERR,
|
|
1011
|
+
`no ${format === "mllp" ? "MLLP frame" : "record"} found in the input`
|
|
1012
|
+
)
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
const lines = [];
|
|
1016
|
+
let failed = 0;
|
|
1017
|
+
let warnings = 0;
|
|
1018
|
+
for (let i = 0; i < records.length; i += 1) {
|
|
1019
|
+
const rec = records[i];
|
|
1020
|
+
try {
|
|
1021
|
+
const { model, warnings: ws } = await parseFormat(recordFormat, rec);
|
|
1022
|
+
warnings += ws.length;
|
|
1023
|
+
lines.push({ record: i, format: recordFormat, model, warnings: ws });
|
|
1024
|
+
} catch (e) {
|
|
1025
|
+
if (e instanceof CliError) return errorResult(e);
|
|
1026
|
+
failed += 1;
|
|
1027
|
+
lines.push({
|
|
1028
|
+
record: i,
|
|
1029
|
+
format: recordFormat,
|
|
1030
|
+
error: extractStableCode(e) ?? "CLI_PARSE_FAILED"
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const stdout = lines.map((l) => JSON.stringify(l)).join("\n") + "\n";
|
|
1035
|
+
const exit = failed > 0 ? EXIT.DATAERR : EXIT.OK;
|
|
1036
|
+
const stderr = quiet ? "" : `cosyte: parsed ${String(records.length)} ${recordFormat} record(s) (${String(warnings)} warning(s), ${String(failed)} failed)` + (ndjson ? " [ndjson]" : format === "mllp" ? " [mllp]" : "") + "\n";
|
|
1037
|
+
return { stdout, stderr, exit };
|
|
1038
|
+
}
|
|
1039
|
+
function splitLines(bytes) {
|
|
1040
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
1041
|
+
const enc = new TextEncoder();
|
|
1042
|
+
return text.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => enc.encode(line));
|
|
1043
|
+
}
|
|
1044
|
+
var PARSE_OPTIONS;
|
|
1045
|
+
var init_parse = __esm({
|
|
1046
|
+
"src/commands/parse.ts"() {
|
|
1047
|
+
init_diagnostics();
|
|
1048
|
+
init_exit_codes();
|
|
1049
|
+
init_input();
|
|
1050
|
+
init_parsers();
|
|
1051
|
+
init_phi();
|
|
1052
|
+
init_wrap();
|
|
1053
|
+
PARSE_OPTIONS = {
|
|
1054
|
+
format: { type: "string" },
|
|
1055
|
+
ndjson: { type: "boolean", default: false },
|
|
1056
|
+
json: { type: "boolean", default: false },
|
|
1057
|
+
quiet: { type: "boolean", default: false },
|
|
1058
|
+
"no-color": { type: "boolean", default: false }
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
async function validateCommand(args, deps2, posture = VALUE_FREE) {
|
|
1063
|
+
let values;
|
|
1064
|
+
let positionals;
|
|
1065
|
+
try {
|
|
1066
|
+
const parsed = parseArgs({ args, options: VALIDATE_OPTIONS, allowPositionals: true });
|
|
1067
|
+
values = parsed.values;
|
|
1068
|
+
positionals = parsed.positionals;
|
|
1069
|
+
} catch {
|
|
1070
|
+
return errorResult(
|
|
1071
|
+
new CliError(
|
|
1072
|
+
CLI_CODES.CLI_USAGE,
|
|
1073
|
+
EXIT.USAGE,
|
|
1074
|
+
"invalid arguments to `validate` (see `cosyte validate --help`)"
|
|
1075
|
+
)
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
if (values.profile !== void 0) {
|
|
1079
|
+
return errorResult(
|
|
1080
|
+
new CliError(
|
|
1081
|
+
CLI_CODES.CLI_NOT_IMPLEMENTED,
|
|
1082
|
+
EXIT.UNAVAILABLE,
|
|
1083
|
+
"profile-based validation is not yet available: the CLI bundles no profiles and cannot yet load one; validating against `--profile` is gated on that surface. Re-run without --profile for base validation."
|
|
1084
|
+
)
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
const resolved = await resolveInput(positionals[0], values.format, deps2, "validate");
|
|
1088
|
+
if (!resolved.ok) return resolved.result;
|
|
1089
|
+
const { format, bytes } = resolved.input;
|
|
1090
|
+
let verdict;
|
|
1091
|
+
try {
|
|
1092
|
+
verdict = await validateFormat(format, bytes);
|
|
1093
|
+
} catch (e) {
|
|
1094
|
+
if (e instanceof CliError) return errorResult(e);
|
|
1095
|
+
return parseFailureResult(format, bytes, posture, e);
|
|
1096
|
+
}
|
|
1097
|
+
const exit = verdict.valid ? EXIT.OK : EXIT.INVALID;
|
|
1098
|
+
if (values.json === true) {
|
|
1099
|
+
const body = { format, valid: verdict.valid, findings: verdict.findings };
|
|
1100
|
+
return { stdout: `${JSON.stringify(body)}
|
|
1101
|
+
`, stderr: "", exit };
|
|
1102
|
+
}
|
|
1103
|
+
if (values.quiet === true) {
|
|
1104
|
+
return { stdout: "", stderr: "", exit };
|
|
1105
|
+
}
|
|
1106
|
+
return { stdout: "", stderr: renderReport(format, verdict), exit };
|
|
1107
|
+
}
|
|
1108
|
+
function renderReport(format, verdict) {
|
|
1109
|
+
const lines = verdict.findings.map(
|
|
1110
|
+
(f) => `cosyte: validate: ${f.severity} ${f.code} at ${f.location}
|
|
1111
|
+
`
|
|
1112
|
+
);
|
|
1113
|
+
const errors = verdict.findings.filter((f) => ERROR_SEVERITIES3.has(f.severity)).length;
|
|
1114
|
+
const others = verdict.findings.length - errors;
|
|
1115
|
+
const summary = verdict.valid ? `cosyte: validate: ${format} is valid (${String(verdict.findings.length)} finding(s))
|
|
1116
|
+
` : `cosyte: validate: ${format} is INVALID (${String(errors)} error(s), ${String(others)} other finding(s))
|
|
1117
|
+
`;
|
|
1118
|
+
return lines.join("") + summary;
|
|
1119
|
+
}
|
|
1120
|
+
var VALIDATE_OPTIONS, ERROR_SEVERITIES3;
|
|
1121
|
+
var init_validate = __esm({
|
|
1122
|
+
"src/commands/validate.ts"() {
|
|
1123
|
+
init_diagnostics();
|
|
1124
|
+
init_exit_codes();
|
|
1125
|
+
init_input();
|
|
1126
|
+
init_parsers();
|
|
1127
|
+
init_phi();
|
|
1128
|
+
init_wrap();
|
|
1129
|
+
VALIDATE_OPTIONS = {
|
|
1130
|
+
format: { type: "string" },
|
|
1131
|
+
profile: { type: "string" },
|
|
1132
|
+
json: { type: "boolean", default: false },
|
|
1133
|
+
quiet: { type: "boolean", default: false },
|
|
1134
|
+
"no-color": { type: "boolean", default: false }
|
|
1135
|
+
};
|
|
1136
|
+
ERROR_SEVERITIES3 = /* @__PURE__ */ new Set(["error", "fatal"]);
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
// src/core/version.ts
|
|
1141
|
+
var VERSION;
|
|
1142
|
+
var init_version = __esm({
|
|
1143
|
+
"src/core/version.ts"() {
|
|
1144
|
+
VERSION = "0.0.0";
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
// src/mcp/tools.ts
|
|
1149
|
+
function inlineDeps(content) {
|
|
1150
|
+
const bytes = new TextEncoder().encode(content);
|
|
1151
|
+
const noFile = new CliError(
|
|
1152
|
+
CLI_CODES.CLI_NO_INPUT,
|
|
1153
|
+
EXIT.NOINPUT,
|
|
1154
|
+
"the MCP tools operate on inline content, not a file path"
|
|
1155
|
+
);
|
|
1156
|
+
return {
|
|
1157
|
+
readStdin: () => Promise.resolve(bytes),
|
|
1158
|
+
// The MCP tools always feed inline content via stdin (`-`); a file read is defensive and unreachable.
|
|
1159
|
+
/* v8 ignore next -- unreachable: no MCP tool passes a file path, only inline `-` stdin content */
|
|
1160
|
+
readFile: () => Promise.reject(noFile)
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
function stringArg(args, key) {
|
|
1164
|
+
const v = args[key];
|
|
1165
|
+
return typeof v === "string" ? v : null;
|
|
1166
|
+
}
|
|
1167
|
+
function toToolResult(result) {
|
|
1168
|
+
const hardError = result.stdout === "";
|
|
1169
|
+
const text = hardError ? result.stderr.trim() : result.stdout.trim();
|
|
1170
|
+
return {
|
|
1171
|
+
content: [
|
|
1172
|
+
{ type: "text", text: text.length > 0 ? text : `cosyte: exit ${String(result.exit)}` }
|
|
1173
|
+
],
|
|
1174
|
+
isError: hardError,
|
|
1175
|
+
structuredContent: { exit: result.exit, ok: !hardError }
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
function usageError(message) {
|
|
1179
|
+
return {
|
|
1180
|
+
content: [{ type: "text", text: `cosyte: ${CLI_CODES.CLI_USAGE}: ${message}` }],
|
|
1181
|
+
isError: true,
|
|
1182
|
+
structuredContent: { exit: EXIT.USAGE, ok: false }
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
function internalError(e) {
|
|
1186
|
+
const err = toCliError(e);
|
|
1187
|
+
return {
|
|
1188
|
+
content: [{ type: "text", text: `cosyte: ${err.code}: ${err.message}` }],
|
|
1189
|
+
isError: true,
|
|
1190
|
+
structuredContent: { exit: err.exit, ok: false }
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
async function dispatchTool(name, args, deps2) {
|
|
1194
|
+
const content = stringArg(args, "content");
|
|
1195
|
+
if (content === null) {
|
|
1196
|
+
return usageError("missing required 'content' argument (the message text to operate on)");
|
|
1197
|
+
}
|
|
1198
|
+
const runDeps = inlineDeps(content);
|
|
1199
|
+
const format = stringArg(args, "format");
|
|
1200
|
+
const fmtFlag = format !== null ? ["--format", format] : [];
|
|
1201
|
+
try {
|
|
1202
|
+
switch (name) {
|
|
1203
|
+
case "parse":
|
|
1204
|
+
return toToolResult(await parseCommand(["-", "--json", ...fmtFlag], runDeps, VALUE_FREE));
|
|
1205
|
+
case "validate":
|
|
1206
|
+
return toToolResult(
|
|
1207
|
+
await validateCommand(["-", "--json", ...fmtFlag], runDeps, VALUE_FREE)
|
|
1208
|
+
);
|
|
1209
|
+
case "inspect":
|
|
1210
|
+
return toToolResult(await inspectCommand(["-", "--json", ...fmtFlag], runDeps, VALUE_FREE));
|
|
1211
|
+
case "convert": {
|
|
1212
|
+
const to = stringArg(args, "to") ?? "fhir";
|
|
1213
|
+
return toToolResult(await convertCommand(["-", "--to", to, "--json"], runDeps, VALUE_FREE));
|
|
1214
|
+
}
|
|
1215
|
+
default:
|
|
1216
|
+
return usageError(`unknown tool '${name}'`);
|
|
1217
|
+
}
|
|
1218
|
+
} catch (e) {
|
|
1219
|
+
return internalError(e);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
var CONTENT_PROP, FORMAT_PROP, TOOL_DEFS;
|
|
1223
|
+
var init_tools = __esm({
|
|
1224
|
+
"src/mcp/tools.ts"() {
|
|
1225
|
+
init_convert();
|
|
1226
|
+
init_inspect();
|
|
1227
|
+
init_parse();
|
|
1228
|
+
init_validate();
|
|
1229
|
+
init_diagnostics();
|
|
1230
|
+
init_exit_codes();
|
|
1231
|
+
init_phi();
|
|
1232
|
+
CONTENT_PROP = {
|
|
1233
|
+
content: {
|
|
1234
|
+
type: "string",
|
|
1235
|
+
description: "The raw healthcare message to operate on (as text; e.g. an HL7 v2 message or a FHIR JSON resource)."
|
|
1236
|
+
}
|
|
1237
|
+
};
|
|
1238
|
+
FORMAT_PROP = {
|
|
1239
|
+
format: {
|
|
1240
|
+
type: "string",
|
|
1241
|
+
enum: ["hl7", "fhir", "dicom", "x12", "ccda", "ncpdp", "astm", "mllp"],
|
|
1242
|
+
description: "Optional format override; omit to autodetect by content. A format whose parser does not support the requested operation returns a value-free CLI_FORMAT_UNSUPPORTED (never a fake)."
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
TOOL_DEFS = [
|
|
1246
|
+
{
|
|
1247
|
+
name: "parse",
|
|
1248
|
+
description: "Parse a healthcare message (HL7 v2 or FHIR R4) to typed JSON. Format is autodetected by content. Returns the parsed model plus value-free warnings.",
|
|
1249
|
+
inputSchema: {
|
|
1250
|
+
type: "object",
|
|
1251
|
+
properties: { ...CONTENT_PROP, ...FORMAT_PROP },
|
|
1252
|
+
required: ["content"],
|
|
1253
|
+
additionalProperties: false
|
|
1254
|
+
}
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
name: "validate",
|
|
1258
|
+
description: "Validate a message and carry the verdict: ok=true with a valid result, or a result reporting value-free findings. The parsed-but-invalid verdict is a successful call (not a tool error).",
|
|
1259
|
+
inputSchema: {
|
|
1260
|
+
type: "object",
|
|
1261
|
+
properties: { ...CONTENT_PROP, ...FORMAT_PROP },
|
|
1262
|
+
required: ["content"],
|
|
1263
|
+
additionalProperties: false
|
|
1264
|
+
}
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
name: "inspect",
|
|
1268
|
+
description: "Return a value-free structural summary of a message: its type, segment/entry counts, and a warning/issue count. Never includes a field value.",
|
|
1269
|
+
inputSchema: {
|
|
1270
|
+
type: "object",
|
|
1271
|
+
properties: { ...CONTENT_PROP, ...FORMAT_PROP },
|
|
1272
|
+
required: ["content"],
|
|
1273
|
+
additionalProperties: false
|
|
1274
|
+
}
|
|
1275
|
+
},
|
|
1276
|
+
{
|
|
1277
|
+
name: "convert",
|
|
1278
|
+
description: "Convert an HL7 v2 message to a FHIR R4 Bundle via @cosyte/transform. Returns the converted Bundle; an error-severity conversion issue is reported with ok=false.",
|
|
1279
|
+
inputSchema: {
|
|
1280
|
+
type: "object",
|
|
1281
|
+
properties: {
|
|
1282
|
+
...CONTENT_PROP,
|
|
1283
|
+
to: {
|
|
1284
|
+
type: "string",
|
|
1285
|
+
enum: ["fhir"],
|
|
1286
|
+
description: "The conversion target (only FHIR R4 is supported today)."
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
required: ["content"],
|
|
1290
|
+
additionalProperties: false
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
];
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
// src/mcp/server.ts
|
|
1298
|
+
var server_exports = {};
|
|
1299
|
+
__export(server_exports, {
|
|
1300
|
+
SERVER_INFO: () => SERVER_INFO,
|
|
1301
|
+
createMcpServer: () => createMcpServer,
|
|
1302
|
+
startStdioServer: () => startStdioServer
|
|
1303
|
+
});
|
|
1304
|
+
function createMcpServer() {
|
|
1305
|
+
const server = new Server(SERVER_INFO, { capabilities: { tools: {} } });
|
|
1306
|
+
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
1307
|
+
tools: TOOL_DEFS.map((t) => ({
|
|
1308
|
+
name: t.name,
|
|
1309
|
+
description: t.description,
|
|
1310
|
+
inputSchema: {
|
|
1311
|
+
type: t.inputSchema.type,
|
|
1312
|
+
properties: { ...t.inputSchema.properties },
|
|
1313
|
+
...t.inputSchema.required !== void 0 ? { required: [...t.inputSchema.required] } : {},
|
|
1314
|
+
...t.inputSchema.additionalProperties !== void 0 ? { additionalProperties: t.inputSchema.additionalProperties } : {}
|
|
1315
|
+
}
|
|
1316
|
+
}))
|
|
1317
|
+
}));
|
|
1318
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1319
|
+
const { name, arguments: args } = request.params;
|
|
1320
|
+
const result = await dispatchTool(name, args ?? {});
|
|
1321
|
+
return {
|
|
1322
|
+
content: result.content.map((c) => ({ type: c.type, text: c.text })),
|
|
1323
|
+
isError: result.isError,
|
|
1324
|
+
structuredContent: { exit: result.structuredContent.exit, ok: result.structuredContent.ok }
|
|
1325
|
+
};
|
|
1326
|
+
});
|
|
1327
|
+
return server;
|
|
1328
|
+
}
|
|
1329
|
+
async function startStdioServer() {
|
|
1330
|
+
const server = createMcpServer();
|
|
1331
|
+
await server.connect(new StdioServerTransport());
|
|
1332
|
+
}
|
|
1333
|
+
var SERVER_INFO;
|
|
1334
|
+
var init_server = __esm({
|
|
1335
|
+
"src/mcp/server.ts"() {
|
|
1336
|
+
init_version();
|
|
1337
|
+
init_tools();
|
|
1338
|
+
SERVER_INFO = { name: "cosyte", version: VERSION };
|
|
1339
|
+
}
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
// src/core/io.ts
|
|
1343
|
+
init_diagnostics();
|
|
1344
|
+
init_exit_codes();
|
|
1345
|
+
async function readFileBytes(path) {
|
|
1346
|
+
try {
|
|
1347
|
+
return await readFile(path);
|
|
1348
|
+
} catch {
|
|
1349
|
+
throw new CliError(
|
|
1350
|
+
CLI_CODES.CLI_NO_INPUT,
|
|
1351
|
+
EXIT.NOINPUT,
|
|
1352
|
+
`cannot read input file: ${path} (does it exist and is it readable?)`
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
async function readStreamBytes(stream) {
|
|
1357
|
+
const chunks = [];
|
|
1358
|
+
for await (const chunk of stream) {
|
|
1359
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk, "utf-8") : chunk);
|
|
1360
|
+
}
|
|
1361
|
+
return Buffer.concat(chunks);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// src/commands/completion.ts
|
|
1365
|
+
init_diagnostics();
|
|
1366
|
+
init_exit_codes();
|
|
1367
|
+
var COMMANDS = [
|
|
1368
|
+
"parse",
|
|
1369
|
+
"validate",
|
|
1370
|
+
"inspect",
|
|
1371
|
+
"fmt",
|
|
1372
|
+
"convert",
|
|
1373
|
+
"map-codes",
|
|
1374
|
+
"redact",
|
|
1375
|
+
"deid",
|
|
1376
|
+
"mcp",
|
|
1377
|
+
"completion"
|
|
1378
|
+
];
|
|
1379
|
+
var FLAGS = [
|
|
1380
|
+
"--format",
|
|
1381
|
+
"--json",
|
|
1382
|
+
"--ndjson",
|
|
1383
|
+
"--quiet",
|
|
1384
|
+
"--no-color",
|
|
1385
|
+
"--unsafe-show-values",
|
|
1386
|
+
"--help",
|
|
1387
|
+
"--version"
|
|
1388
|
+
];
|
|
1389
|
+
var SHELLS = ["bash", "zsh", "fish"];
|
|
1390
|
+
function completionCommand(args) {
|
|
1391
|
+
const shell = args[0];
|
|
1392
|
+
if (shell === void 0) {
|
|
1393
|
+
return errorResult(
|
|
1394
|
+
new CliError(
|
|
1395
|
+
CLI_CODES.CLI_USAGE,
|
|
1396
|
+
EXIT.USAGE,
|
|
1397
|
+
`missing shell; usage: cosyte completion <${SHELLS.join("|")}>`
|
|
1398
|
+
)
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
if (!isShell(shell)) {
|
|
1402
|
+
return errorResult(
|
|
1403
|
+
new CliError(
|
|
1404
|
+
CLI_CODES.CLI_USAGE,
|
|
1405
|
+
EXIT.USAGE,
|
|
1406
|
+
`unknown shell '${shell}'; supported: ${SHELLS.join(", ")}`
|
|
1407
|
+
)
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
return { stdout: SCRIPTS[shell](), stderr: "", exit: EXIT.OK };
|
|
1411
|
+
}
|
|
1412
|
+
function isShell(value) {
|
|
1413
|
+
return SHELLS.includes(value);
|
|
1414
|
+
}
|
|
1415
|
+
var SCRIPTS = {
|
|
1416
|
+
bash: () => `# cosyte bash completion: source <(cosyte completion bash)
|
|
1417
|
+
_cosyte() {
|
|
1418
|
+
local cur prev cmds flags
|
|
1419
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1420
|
+
cmds="${COMMANDS.join(" ")}"
|
|
1421
|
+
flags="${FLAGS.join(" ")}"
|
|
1422
|
+
if [ "\${COMP_CWORD}" -eq 1 ]; then
|
|
1423
|
+
COMPREPLY=( $(compgen -W "\${cmds}" -- "\${cur}") )
|
|
1424
|
+
else
|
|
1425
|
+
COMPREPLY=( $(compgen -W "\${flags}" -f -- "\${cur}") )
|
|
1426
|
+
fi
|
|
1427
|
+
}
|
|
1428
|
+
complete -F _cosyte cosyte
|
|
1429
|
+
`,
|
|
1430
|
+
zsh: () => `#compdef cosyte
|
|
1431
|
+
# cosyte zsh completion: source <(cosyte completion zsh)
|
|
1432
|
+
_cosyte() {
|
|
1433
|
+
local -a cmds flags
|
|
1434
|
+
cmds=(${COMMANDS.map((c) => `'${c}'`).join(" ")})
|
|
1435
|
+
flags=(${FLAGS.map((f) => `'${f}'`).join(" ")})
|
|
1436
|
+
if (( CURRENT == 2 )); then
|
|
1437
|
+
_describe 'command' cmds
|
|
1438
|
+
else
|
|
1439
|
+
_describe 'flag' flags
|
|
1440
|
+
_files
|
|
1441
|
+
fi
|
|
1442
|
+
}
|
|
1443
|
+
compdef _cosyte cosyte
|
|
1444
|
+
`,
|
|
1445
|
+
fish: () => {
|
|
1446
|
+
const lines = COMMANDS.map(
|
|
1447
|
+
(c) => `complete -c cosyte -n '__fish_use_subcommand' -a '${c}' -d 'cosyte ${c}'`
|
|
1448
|
+
);
|
|
1449
|
+
const flagLines = FLAGS.filter((f) => f.startsWith("--")).map(
|
|
1450
|
+
(f) => `complete -c cosyte -l '${f.slice(2)}'`
|
|
1451
|
+
);
|
|
1452
|
+
return `# cosyte fish completion: cosyte completion fish | source
|
|
1453
|
+
${lines.join("\n")}
|
|
1454
|
+
${flagLines.join("\n")}
|
|
1455
|
+
`;
|
|
1456
|
+
}
|
|
1457
|
+
};
|
|
1458
|
+
|
|
1459
|
+
// src/core/run.ts
|
|
1460
|
+
init_convert();
|
|
1461
|
+
|
|
1462
|
+
// src/commands/fmt.ts
|
|
1463
|
+
init_diagnostics();
|
|
1464
|
+
init_exit_codes();
|
|
1465
|
+
init_input();
|
|
1466
|
+
init_parsers();
|
|
1467
|
+
init_phi();
|
|
1468
|
+
init_wrap();
|
|
1469
|
+
var FMT_OPTIONS = {
|
|
1470
|
+
format: { type: "string" },
|
|
1471
|
+
quiet: { type: "boolean", default: false },
|
|
1472
|
+
"no-color": { type: "boolean", default: false }
|
|
1473
|
+
};
|
|
1474
|
+
async function fmtCommand(args, deps2, posture = VALUE_FREE) {
|
|
1475
|
+
let values;
|
|
1476
|
+
let positionals;
|
|
1477
|
+
try {
|
|
1478
|
+
const parsed = parseArgs({ args, options: FMT_OPTIONS, allowPositionals: true });
|
|
1479
|
+
values = parsed.values;
|
|
1480
|
+
positionals = parsed.positionals;
|
|
1481
|
+
} catch {
|
|
1482
|
+
return errorResult(
|
|
1483
|
+
new CliError(
|
|
1484
|
+
CLI_CODES.CLI_USAGE,
|
|
1485
|
+
EXIT.USAGE,
|
|
1486
|
+
"invalid arguments to `fmt` (see `cosyte fmt --help`)"
|
|
1487
|
+
)
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
const resolved = await resolveInput(positionals[0], values.format, deps2, "fmt");
|
|
1491
|
+
if (!resolved.ok) return resolved.result;
|
|
1492
|
+
const { format, bytes } = resolved.input;
|
|
1493
|
+
let result;
|
|
1494
|
+
try {
|
|
1495
|
+
result = await fmtFormat(format, bytes);
|
|
1496
|
+
} catch (e) {
|
|
1497
|
+
if (e instanceof CliError) return errorResult(e);
|
|
1498
|
+
return parseFailureResult(format, bytes, posture, e);
|
|
1499
|
+
}
|
|
1500
|
+
const stderr = result.warningCount > 0 && values.quiet !== true ? `cosyte: fmt: re-serialized ${format} with ${String(result.warningCount)} parse warning(s)
|
|
1501
|
+
` : "";
|
|
1502
|
+
return { stdout: `${result.output}
|
|
1503
|
+
`, stderr, exit: EXIT.OK };
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// src/core/run.ts
|
|
1507
|
+
init_inspect();
|
|
1508
|
+
|
|
1509
|
+
// src/commands/map-codes.ts
|
|
1510
|
+
init_diagnostics();
|
|
1511
|
+
init_exit_codes();
|
|
1512
|
+
init_wrap();
|
|
1513
|
+
var MAP_CODES_OPTIONS = {
|
|
1514
|
+
code: { type: "string" },
|
|
1515
|
+
system: { type: "string" },
|
|
1516
|
+
version: { type: "string" },
|
|
1517
|
+
display: { type: "string" },
|
|
1518
|
+
json: { type: "boolean", default: false },
|
|
1519
|
+
quiet: { type: "boolean", default: false },
|
|
1520
|
+
"no-color": { type: "boolean", default: false }
|
|
1521
|
+
};
|
|
1522
|
+
async function mapCodesCommand(args, deps2) {
|
|
1523
|
+
let values;
|
|
1524
|
+
let positionals;
|
|
1525
|
+
try {
|
|
1526
|
+
const parsed = parseArgs({ args, options: MAP_CODES_OPTIONS, allowPositionals: true });
|
|
1527
|
+
values = parsed.values;
|
|
1528
|
+
positionals = parsed.positionals;
|
|
1529
|
+
} catch {
|
|
1530
|
+
return errorResult(
|
|
1531
|
+
new CliError(
|
|
1532
|
+
CLI_CODES.CLI_USAGE,
|
|
1533
|
+
EXIT.USAGE,
|
|
1534
|
+
"invalid arguments to `map-codes` (see `cosyte map-codes --help`)"
|
|
1535
|
+
)
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
const source = positionals[0];
|
|
1539
|
+
if (source === void 0) {
|
|
1540
|
+
return errorResult(
|
|
1541
|
+
new CliError(
|
|
1542
|
+
CLI_CODES.CLI_USAGE,
|
|
1543
|
+
EXIT.USAGE,
|
|
1544
|
+
"missing <conceptmap> argument; pass a FHIR ConceptMap path or `-` to read stdin"
|
|
1545
|
+
)
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
if (values.code === void 0) {
|
|
1549
|
+
return errorResult(
|
|
1550
|
+
new CliError(
|
|
1551
|
+
CLI_CODES.CLI_USAGE,
|
|
1552
|
+
EXIT.USAGE,
|
|
1553
|
+
"map-codes requires --code (the source code to translate)"
|
|
1554
|
+
)
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
let bytes;
|
|
1558
|
+
try {
|
|
1559
|
+
bytes = source === "-" ? await deps2.readStdin() : await deps2.readFile(source);
|
|
1560
|
+
} catch (e) {
|
|
1561
|
+
if (e instanceof CliError) return errorResult(e);
|
|
1562
|
+
throw e;
|
|
1563
|
+
}
|
|
1564
|
+
if (bytes.length === 0) {
|
|
1565
|
+
return errorResult(new CliError(CLI_CODES.CLI_EMPTY_INPUT, EXIT.DATAERR, "input is empty"));
|
|
1566
|
+
}
|
|
1567
|
+
const coding = {
|
|
1568
|
+
code: values.code,
|
|
1569
|
+
...values.system !== void 0 ? { system: values.system } : {},
|
|
1570
|
+
...values.version !== void 0 ? { version: values.version } : {},
|
|
1571
|
+
...values.display !== void 0 ? { display: values.display } : {}
|
|
1572
|
+
};
|
|
1573
|
+
const outcome = await translateVia(bytes, coding);
|
|
1574
|
+
if (!outcome.ok) return outcome.result;
|
|
1575
|
+
const { result } = outcome;
|
|
1576
|
+
const exit = result.unmapped ? EXIT.INVALID : EXIT.OK;
|
|
1577
|
+
const body = { source: coding, result };
|
|
1578
|
+
if (values.json === true) {
|
|
1579
|
+
return { stdout: `${JSON.stringify(body)}
|
|
1580
|
+
`, stderr: "", exit };
|
|
1581
|
+
}
|
|
1582
|
+
const stderr = values.quiet === true ? "" : renderNote(result);
|
|
1583
|
+
return { stdout: `${JSON.stringify(body, null, 2)}
|
|
1584
|
+
`, stderr, exit };
|
|
1585
|
+
}
|
|
1586
|
+
async function translateVia(bytes, coding) {
|
|
1587
|
+
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
1588
|
+
let json;
|
|
1589
|
+
try {
|
|
1590
|
+
json = JSON.parse(text);
|
|
1591
|
+
} catch {
|
|
1592
|
+
return mapInvalid("the ConceptMap input is not valid JSON");
|
|
1593
|
+
}
|
|
1594
|
+
const { loadConceptMap, translate } = await import('@cosyte/terminology');
|
|
1595
|
+
let map;
|
|
1596
|
+
try {
|
|
1597
|
+
map = loadConceptMap(json);
|
|
1598
|
+
} catch (e) {
|
|
1599
|
+
const code = extractStableCode(e);
|
|
1600
|
+
const detail = code === null ? "" : ` (${code})`;
|
|
1601
|
+
return mapInvalid(`the supplied ConceptMap could not be loaded${detail}`);
|
|
1602
|
+
}
|
|
1603
|
+
return { ok: true, result: translate(coding, map) };
|
|
1604
|
+
}
|
|
1605
|
+
function mapInvalid(message) {
|
|
1606
|
+
return {
|
|
1607
|
+
ok: false,
|
|
1608
|
+
result: errorResult(new CliError(CLI_CODES.CLI_MAP_INVALID, EXIT.DATAERR, message))
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
function renderNote(result) {
|
|
1612
|
+
if (result.unmapped) {
|
|
1613
|
+
return `cosyte: map-codes: ${result.code} (fallback mode: ${result.mode})
|
|
1614
|
+
`;
|
|
1615
|
+
}
|
|
1616
|
+
return `cosyte: map-codes: ${String(result.matches.length)} match(es)
|
|
1617
|
+
`;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/core/run.ts
|
|
1621
|
+
init_parse();
|
|
1622
|
+
|
|
1623
|
+
// src/commands/redact.ts
|
|
1624
|
+
init_diagnostics();
|
|
1625
|
+
|
|
1626
|
+
// src/core/deid.ts
|
|
1627
|
+
var DEID_UNAVAILABLE_REASON = "redact/deid is not yet available. It delegates to @cosyte/deid, which is unpublished. The CLI ships no built-in redactor: a partial Safe-Harbor scrub over only the obvious PHI loci would leave PHI behind and present a false-safety impression, so nothing is emitted. This command will produce a de-identified copy once @cosyte/deid ships and is vetted.";
|
|
1628
|
+
function deidStatus() {
|
|
1629
|
+
return { available: false, reason: DEID_UNAVAILABLE_REASON };
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// src/commands/redact.ts
|
|
1633
|
+
init_exit_codes();
|
|
1634
|
+
var REDACT_OPTIONS = {
|
|
1635
|
+
format: { type: "string" }
|
|
1636
|
+
};
|
|
1637
|
+
function redactCommand(args) {
|
|
1638
|
+
try {
|
|
1639
|
+
parseArgs({ args, options: REDACT_OPTIONS, allowPositionals: true });
|
|
1640
|
+
} catch {
|
|
1641
|
+
return {
|
|
1642
|
+
stdout: "",
|
|
1643
|
+
stderr: `cosyte: ${CLI_CODES.CLI_USAGE}: invalid arguments to \`redact\` (see \`cosyte --help\`)
|
|
1644
|
+
`,
|
|
1645
|
+
exit: EXIT.USAGE
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
const status = deidStatus();
|
|
1649
|
+
return {
|
|
1650
|
+
stdout: "",
|
|
1651
|
+
stderr: `cosyte: ${CLI_CODES.CLI_NOT_IMPLEMENTED}: ${status.reason}
|
|
1652
|
+
`,
|
|
1653
|
+
exit: EXIT.UNAVAILABLE
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// src/core/run.ts
|
|
1658
|
+
init_validate();
|
|
1659
|
+
init_diagnostics();
|
|
1660
|
+
init_exit_codes();
|
|
1661
|
+
init_phi();
|
|
1662
|
+
init_version();
|
|
1663
|
+
var HELP = `cosyte: a PHI-safe developer CLI over the @cosyte/* healthcare parsers
|
|
1664
|
+
|
|
1665
|
+
Usage:
|
|
1666
|
+
cosyte <command> [options]
|
|
1667
|
+
|
|
1668
|
+
Commands:
|
|
1669
|
+
parse <file|-> Parse a healthcare message to typed JSON (format autodetected)
|
|
1670
|
+
validate <file|-> Validate a message; exit code carries the verdict (0 valid / 1 invalid)
|
|
1671
|
+
inspect <file|-> Print a value-free structural summary (segment/resource counts, type)
|
|
1672
|
+
fmt <file|-> Canonically re-serialize via the parser's spec-clean serializer
|
|
1673
|
+
convert <file|-> Convert HL7 v2 \u2192 FHIR R4 via @cosyte/transform (use --to fhir)
|
|
1674
|
+
map-codes <cmap|-> Translate a code through a BYO FHIR ConceptMap via @cosyte/terminology
|
|
1675
|
+
redact <file|-> De-identify a message (alias: deid): gated on @cosyte/deid, not yet available
|
|
1676
|
+
mcp Start the stdio MCP server (agent front door; also the cosyte-mcp bin)
|
|
1677
|
+
completion <shell> Print a shell completion script (bash | zsh | fish)
|
|
1678
|
+
|
|
1679
|
+
Global:
|
|
1680
|
+
-h, --help Show this help
|
|
1681
|
+
-V, --version Show the CLI version
|
|
1682
|
+
--unsafe-show-values Permit input values on stderr/diagnostics (PHI-exposing; off by default)
|
|
1683
|
+
|
|
1684
|
+
Common options (parse / validate / inspect / fmt):
|
|
1685
|
+
--format <fmt> Override autodetection: hl7 | fhir | dicom | x12 | ccda | ncpdp | astm | mllp
|
|
1686
|
+
(parse: hl7 fhir x12 astm ncpdp mllp \xB7 inspect: all \xB7 fmt: hl7 fhir x12 astm
|
|
1687
|
+
ccda ncpdp \xB7 validate: all but mllp; dicom/ccda parse & mllp fmt are deferred)
|
|
1688
|
+
--json Machine-readable JSON output (parse / validate / inspect / convert / map-codes)
|
|
1689
|
+
--ndjson Treat each non-empty input line as a record; emit NDJSON (parse; FHIR bulk data)
|
|
1690
|
+
--quiet Suppress value-free notes on stderr
|
|
1691
|
+
--no-color Disable ANSI colour
|
|
1692
|
+
|
|
1693
|
+
convert options:
|
|
1694
|
+
--to fhir The conversion target (required; only HL7 v2 \u2192 FHIR R4 today)
|
|
1695
|
+
|
|
1696
|
+
map-codes options (the positional is a BYO FHIR ConceptMap; a code is not PHI):
|
|
1697
|
+
--code <code> The source code to translate (required)
|
|
1698
|
+
--system <uri> The source code system (optional; selects the ConceptMap group)
|
|
1699
|
+
--version <v> The source code system version (optional)
|
|
1700
|
+
--display <d> The source display (optional)
|
|
1701
|
+
|
|
1702
|
+
Exit codes:
|
|
1703
|
+
0 success / valid 65 data error (unparseable / undetected format)
|
|
1704
|
+
1 invalid (validate) 66 no input (missing/unreadable file)
|
|
1705
|
+
2 usage error 69 unavailable (a capability is not yet built, e.g. redact)
|
|
1706
|
+
70 internal error
|
|
1707
|
+
|
|
1708
|
+
PHI posture: the parsed model goes to stdout (the data channel you chose); every diagnostic on
|
|
1709
|
+
stderr is value-free (codes and positions only, never a field value) unless you pass the loud,
|
|
1710
|
+
opt-in --unsafe-show-values (which permits a bounded input excerpt in a failure diagnostic).
|
|
1711
|
+
`;
|
|
1712
|
+
function wantsHelp(argv) {
|
|
1713
|
+
return argv.includes("--help") || argv.includes("-h");
|
|
1714
|
+
}
|
|
1715
|
+
async function run(argv, deps2) {
|
|
1716
|
+
const { posture, argv: rest0 } = extractPhiPosture(argv);
|
|
1717
|
+
const [command, ...rest] = rest0;
|
|
1718
|
+
if (command === void 0 || wantsHelp(rest0)) {
|
|
1719
|
+
return { stdout: HELP, stderr: "", exit: EXIT.OK };
|
|
1720
|
+
}
|
|
1721
|
+
if (command === "--version" || command === "-V") {
|
|
1722
|
+
return { stdout: `${VERSION}
|
|
1723
|
+
`, stderr: "", exit: EXIT.OK };
|
|
1724
|
+
}
|
|
1725
|
+
try {
|
|
1726
|
+
switch (command) {
|
|
1727
|
+
case "parse":
|
|
1728
|
+
return await parseCommand(rest, deps2, posture);
|
|
1729
|
+
case "validate":
|
|
1730
|
+
return await validateCommand(rest, deps2, posture);
|
|
1731
|
+
case "inspect":
|
|
1732
|
+
return await inspectCommand(rest, deps2, posture);
|
|
1733
|
+
case "fmt":
|
|
1734
|
+
return await fmtCommand(rest, deps2, posture);
|
|
1735
|
+
case "convert":
|
|
1736
|
+
return await convertCommand(rest, deps2, posture);
|
|
1737
|
+
case "map-codes":
|
|
1738
|
+
return await mapCodesCommand(rest, deps2);
|
|
1739
|
+
case "redact":
|
|
1740
|
+
case "deid":
|
|
1741
|
+
return redactCommand(rest);
|
|
1742
|
+
case "completion":
|
|
1743
|
+
return completionCommand(rest);
|
|
1744
|
+
default:
|
|
1745
|
+
return resolve(
|
|
1746
|
+
new CliError(
|
|
1747
|
+
CLI_CODES.CLI_USAGE,
|
|
1748
|
+
EXIT.USAGE,
|
|
1749
|
+
`unknown command '${command}'; run \`cosyte --help\``
|
|
1750
|
+
)
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
} catch (e) {
|
|
1754
|
+
return resolve(toCliError(e));
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
function resolve(e) {
|
|
1758
|
+
return { stdout: "", stderr: `cosyte: ${e.code}: ${e.message}
|
|
1759
|
+
`, exit: e.exit };
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// src/bin/cosyte.ts
|
|
1763
|
+
var deps = {
|
|
1764
|
+
readFile: (path) => readFileBytes(path),
|
|
1765
|
+
readStdin: () => readStreamBytes(process.stdin)
|
|
1766
|
+
};
|
|
1767
|
+
if (process.argv[2] === "mcp") {
|
|
1768
|
+
Promise.resolve().then(() => (init_server(), server_exports)).then(({ startStdioServer: startStdioServer2 }) => startStdioServer2()).catch(() => {
|
|
1769
|
+
process.stderr.write("cosyte: CLI_INTERNAL: the MCP server failed to start\n");
|
|
1770
|
+
process.exitCode = 70;
|
|
1771
|
+
});
|
|
1772
|
+
} else {
|
|
1773
|
+
run(process.argv.slice(2), deps).then((result) => {
|
|
1774
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
1775
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
1776
|
+
process.exitCode = result.exit;
|
|
1777
|
+
}).catch(() => {
|
|
1778
|
+
process.stderr.write("cosyte: CLI_INTERNAL: an unexpected internal error occurred\n");
|
|
1779
|
+
process.exitCode = 70;
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
//# sourceMappingURL=cosyte.mjs.map
|
|
1783
|
+
//# sourceMappingURL=cosyte.mjs.map
|