@cosyte/cli 0.0.5 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +416 -0
- package/README.md +111 -45
- package/dist/bin/cosyte-mcp.cjs +461 -106
- package/dist/bin/cosyte-mcp.cjs.map +1 -1
- package/dist/bin/cosyte-mcp.mjs +461 -106
- package/dist/bin/cosyte-mcp.mjs.map +1 -1
- package/dist/bin/cosyte.cjs +824 -140
- package/dist/bin/cosyte.cjs.map +1 -1
- package/dist/bin/cosyte.mjs +825 -141
- package/dist/bin/cosyte.mjs.map +1 -1
- package/dist/index.cjs +592 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +515 -281
- package/dist/index.d.ts +515 -281
- package/dist/index.mjs +574 -104
- package/dist/index.mjs.map +1 -1
- package/dist/io-C9bSG2zF.d.cts +381 -0
- package/dist/io-C9bSG2zF.d.ts +381 -0
- package/dist/mcp.cjs +435 -103
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.d.cts +70 -9
- package/dist/mcp.d.ts +70 -9
- package/dist/mcp.mjs +435 -103
- package/dist/mcp.mjs.map +1 -1
- package/package.json +61 -37
- package/dist/io-B2EobRzC.d.cts +0 -57
- package/dist/io-B2EobRzC.d.ts +0 -57
package/dist/bin/cosyte.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import { parseArgs } from 'util';
|
|
|
3
3
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
5
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
-
import {
|
|
6
|
+
import { open } from 'fs/promises';
|
|
7
|
+
import { randomBytes } from 'crypto';
|
|
7
8
|
|
|
8
9
|
var __defProp = Object.defineProperty;
|
|
9
10
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -27,21 +28,29 @@ var init_exit_codes = __esm({
|
|
|
27
28
|
EXIT = {
|
|
28
29
|
/** Success: the operation completed; `validate` found the input **valid**. */
|
|
29
30
|
OK: 0,
|
|
30
|
-
/** Operation-level failure: `validate` found the input **invalid** (parseable but non-conformant)
|
|
31
|
-
* a real, expected CI signal that the
|
|
32
|
-
* unparseable input (that is `DATAERR`). */
|
|
31
|
+
/** Operation-level failure: `validate` found the input **invalid** (parseable but non-conformant),
|
|
32
|
+
* or `redact` could not de-identify every locus: a real, expected CI signal that the tool worked
|
|
33
|
+
* and the operation did not complete. Never emitted for unparseable input (that is `DATAERR`). */
|
|
33
34
|
INVALID: 1,
|
|
34
|
-
/** Usage error: unknown command, bad flag, missing argument
|
|
35
|
+
/** Usage error: unknown command, bad flag, missing argument. A value specific to this CLI, not a
|
|
36
|
+
* `sysexits.h` constant. */
|
|
35
37
|
USAGE: 2,
|
|
36
38
|
/** Data error: input could not be parsed or its format could not be detected (`EX_DATAERR`). */
|
|
37
39
|
DATAERR: 65,
|
|
38
40
|
/** No input: the named file does not exist or is unreadable (`EX_NOINPUT`). */
|
|
39
41
|
NOINPUT: 66,
|
|
40
|
-
/** Unavailable: a required capability is not
|
|
41
|
-
*
|
|
42
|
+
/** Unavailable: a required capability is not wired for this input (e.g. `redact` on a format
|
|
43
|
+
* `@cosyte/deid` has no adapter for, or with that optional library absent), a distinct non-zero
|
|
44
|
+
* signal that is never a fake success (`EX_UNAVAILABLE`). */
|
|
42
45
|
UNAVAILABLE: 69,
|
|
43
46
|
/** Internal error: an unexpected exception (a bug), distinct from a handled bad input (`EX_SOFTWARE`). */
|
|
44
|
-
SOFTWARE: 70
|
|
47
|
+
SOFTWARE: 70,
|
|
48
|
+
/** Output error: the stream the CLI was writing to closed before the output had been delivered, so
|
|
49
|
+
* the consumer holds nothing or holds only part of the answer. The shape a downstream consumer
|
|
50
|
+
* that exits early produces (a pager quit, a `head` that has seen enough). A handled condition the
|
|
51
|
+
* consumer owns, never a defect in the CLI, so it is deliberately distinct from `SOFTWARE`
|
|
52
|
+
* (`EX_IOERR`). */
|
|
53
|
+
IOERR: 74
|
|
45
54
|
};
|
|
46
55
|
}
|
|
47
56
|
});
|
|
@@ -86,12 +95,27 @@ var init_diagnostics = __esm({
|
|
|
86
95
|
CLI_PARSER_UNAVAILABLE: "CLI_PARSER_UNAVAILABLE",
|
|
87
96
|
/** The wrapped parser rejected the input. Positional context only, never the offending bytes. Exit `65`. */
|
|
88
97
|
CLI_PARSE_FAILED: "CLI_PARSE_FAILED",
|
|
98
|
+
/** The input is larger than the documented size limit the CLI reads in one invocation. Raised
|
|
99
|
+
* against the running byte count while the input is read, so the refusal always arrives before an
|
|
100
|
+
* allocation ceiling could. Names the limit, never the input. Exit `65`, never `70`. */
|
|
101
|
+
CLI_INPUT_TOO_LARGE: "CLI_INPUT_TOO_LARGE",
|
|
102
|
+
/** The output stream could not be written: a consumer that closes the pipe ends the invocation
|
|
103
|
+
* here rather than as an unhandled platform error, whether it went away part way through a record
|
|
104
|
+
* stream or before a single-result write was delivered. Names no input, and never the platform's
|
|
105
|
+
* own error text. Exit `74`. */
|
|
106
|
+
CLI_OUTPUT_WRITE_FAILED: "CLI_OUTPUT_WRITE_FAILED",
|
|
89
107
|
/** The BYO ConceptMap supplied to `map-codes` is not valid JSON or not a loadable FHIR ConceptMap.
|
|
90
108
|
* Names the stable terminology-loader code, never the map's bytes. Exit `65`. */
|
|
91
109
|
CLI_MAP_INVALID: "CLI_MAP_INVALID",
|
|
92
|
-
/** A command whose ground-layer library
|
|
93
|
-
*
|
|
110
|
+
/** A command whose ground-layer library does not cover the resolved format (e.g. `redact` on a
|
|
111
|
+
* format `@cosyte/deid` ships no adapter for). Never a fake success: a distinct, value-free
|
|
112
|
+
* "unavailable" signal. Exit `69`. */
|
|
94
113
|
CLI_NOT_IMPLEMENTED: "CLI_NOT_IMPLEMENTED",
|
|
114
|
+
/** The delegated de-identifier reported that it could not handle every element of the input (a
|
|
115
|
+
* fail-closed `blocked` locus, or its own fatal). The run is **not** a de-identified copy, so no
|
|
116
|
+
* output is emitted: an operation-level failure, never a bug. Names loci and codes, never a
|
|
117
|
+
* value. Exit `1`. */
|
|
118
|
+
CLI_DEID_INCOMPLETE: "CLI_DEID_INCOMPLETE",
|
|
95
119
|
/** An unexpected internal error (a bug). Exit `70`. */
|
|
96
120
|
CLI_INTERNAL: "CLI_INTERNAL"
|
|
97
121
|
};
|
|
@@ -115,6 +139,28 @@ var init_diagnostics = __esm({
|
|
|
115
139
|
}
|
|
116
140
|
});
|
|
117
141
|
|
|
142
|
+
// src/core/limits.ts
|
|
143
|
+
function describeByteLimit(limit = MAX_INPUT_BYTES) {
|
|
144
|
+
const mib = limit / BYTES_PER_MIB;
|
|
145
|
+
return Number.isInteger(mib) ? `${String(limit)} bytes (${String(mib)} MiB)` : `${String(limit)} bytes`;
|
|
146
|
+
}
|
|
147
|
+
function inputTooLargeError(limit = MAX_INPUT_BYTES) {
|
|
148
|
+
return new CliError(
|
|
149
|
+
CLI_CODES.CLI_INPUT_TOO_LARGE,
|
|
150
|
+
EXIT.DATAERR,
|
|
151
|
+
`input is larger than the ${describeByteLimit(limit)} this command reads in one invocation; split the input and re-run`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
var BYTES_PER_MIB, MAX_INPUT_BYTES;
|
|
155
|
+
var init_limits = __esm({
|
|
156
|
+
"src/core/limits.ts"() {
|
|
157
|
+
init_diagnostics();
|
|
158
|
+
init_exit_codes();
|
|
159
|
+
BYTES_PER_MIB = 1024 * 1024;
|
|
160
|
+
MAX_INPUT_BYTES = 64 * BYTES_PER_MIB;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
118
164
|
// src/core/format.ts
|
|
119
165
|
function leadingText(bytes) {
|
|
120
166
|
const slice = bytes.subarray(0, SNIFF_BYTES);
|
|
@@ -190,12 +236,13 @@ function detectionError(detected) {
|
|
|
190
236
|
function asCosyteFormat(value) {
|
|
191
237
|
return KNOWN_FORMATS.includes(value) ? value : null;
|
|
192
238
|
}
|
|
193
|
-
var SNIFF_BYTES, SIGNATURES, DETECTABLE_FORMATS, KNOWN_FORMATS;
|
|
239
|
+
var DETECT_PREFIX_BYTES, SNIFF_BYTES, SIGNATURES, DETECTABLE_FORMATS, KNOWN_FORMATS;
|
|
194
240
|
var init_format = __esm({
|
|
195
241
|
"src/core/format.ts"() {
|
|
196
242
|
init_diagnostics();
|
|
197
243
|
init_exit_codes();
|
|
198
|
-
|
|
244
|
+
DETECT_PREFIX_BYTES = 512;
|
|
245
|
+
SNIFF_BYTES = DETECT_PREFIX_BYTES;
|
|
199
246
|
SIGNATURES = [
|
|
200
247
|
{ format: "hl7", match: (prefix) => looksLikeHl7(prefix) },
|
|
201
248
|
{ format: "mllp", match: (prefix, bytes) => looksLikeMllp(prefix, bytes) },
|
|
@@ -561,11 +608,7 @@ async function validateFormat(format, bytes) {
|
|
|
561
608
|
}
|
|
562
609
|
async function deframeMllp(bytes) {
|
|
563
610
|
if (bytes.lastIndexOf(11) > bytes.lastIndexOf(28)) {
|
|
564
|
-
throw
|
|
565
|
-
CLI_CODES.CLI_PARSE_FAILED,
|
|
566
|
-
EXIT.DATAERR,
|
|
567
|
-
"truncated MLLP stream: an unterminated frame at the end of input (no closing FS/CR)"
|
|
568
|
-
);
|
|
611
|
+
throw truncatedMllpError();
|
|
569
612
|
}
|
|
570
613
|
const { FrameReader } = await loadOptional("mllp", () => import('@cosyte/mllp'));
|
|
571
614
|
const payloads = [];
|
|
@@ -577,6 +620,13 @@ async function deframeMllp(bytes) {
|
|
|
577
620
|
reader.push(Buffer.from(bytes));
|
|
578
621
|
return { payloads, warningCount };
|
|
579
622
|
}
|
|
623
|
+
function truncatedMllpError() {
|
|
624
|
+
return new CliError(
|
|
625
|
+
CLI_CODES.CLI_PARSE_FAILED,
|
|
626
|
+
EXIT.DATAERR,
|
|
627
|
+
"truncated MLLP stream: an unterminated frame at the end of input (no closing FS/CR)"
|
|
628
|
+
);
|
|
629
|
+
}
|
|
580
630
|
function decode(bytes) {
|
|
581
631
|
return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
582
632
|
}
|
|
@@ -612,6 +662,91 @@ var init_parsers = __esm({
|
|
|
612
662
|
}
|
|
613
663
|
});
|
|
614
664
|
|
|
665
|
+
// src/core/records.ts
|
|
666
|
+
function asBuffer(chunk) {
|
|
667
|
+
return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk.buffer, chunk.byteOffset, chunk.length);
|
|
668
|
+
}
|
|
669
|
+
async function* oneChunk(bytes) {
|
|
670
|
+
await Promise.resolve();
|
|
671
|
+
if (bytes.length > 0) yield bytes;
|
|
672
|
+
}
|
|
673
|
+
async function* withinLimit(chunks, limit = MAX_INPUT_BYTES) {
|
|
674
|
+
let total = 0;
|
|
675
|
+
for await (const chunk of chunks) {
|
|
676
|
+
total += chunk.length;
|
|
677
|
+
if (total > limit) throw inputTooLargeError(limit);
|
|
678
|
+
yield chunk;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
async function collectChunks(chunks) {
|
|
682
|
+
const parts = [];
|
|
683
|
+
for await (const chunk of chunks) parts.push(asBuffer(chunk));
|
|
684
|
+
return Buffer.concat(parts);
|
|
685
|
+
}
|
|
686
|
+
function isBlank(line) {
|
|
687
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(line).trim().length === 0;
|
|
688
|
+
}
|
|
689
|
+
function stripCarriageReturn(line) {
|
|
690
|
+
return line.length > 0 && line[line.length - 1] === CARRIAGE_RETURN ? line.subarray(0, line.length - 1) : line;
|
|
691
|
+
}
|
|
692
|
+
async function* ndjsonRecords(chunks) {
|
|
693
|
+
let carry = Buffer.alloc(0);
|
|
694
|
+
for await (const chunk of chunks) {
|
|
695
|
+
const buf = carry.length > 0 ? Buffer.concat([carry, asBuffer(chunk)]) : asBuffer(chunk);
|
|
696
|
+
let start = 0;
|
|
697
|
+
for (; ; ) {
|
|
698
|
+
const end = buf.indexOf(LINE_FEED, start);
|
|
699
|
+
if (end < 0) break;
|
|
700
|
+
const line = stripCarriageReturn(buf.subarray(start, end));
|
|
701
|
+
if (!isBlank(line)) yield line;
|
|
702
|
+
start = end + 1;
|
|
703
|
+
}
|
|
704
|
+
carry = start === 0 ? asBuffer(buf) : Buffer.from(buf.subarray(start));
|
|
705
|
+
}
|
|
706
|
+
const last = stripCarriageReturn(carry);
|
|
707
|
+
if (!isBlank(last)) yield last;
|
|
708
|
+
}
|
|
709
|
+
async function* mllpFrames(chunks) {
|
|
710
|
+
const { FrameReader } = await loadOptional("mllp", () => import('@cosyte/mllp'));
|
|
711
|
+
const ready = [];
|
|
712
|
+
const reader = new FrameReader({
|
|
713
|
+
onFrame: (payload) => ready.push(Buffer.from(payload)),
|
|
714
|
+
onWarning: () => void 0,
|
|
715
|
+
// The CLI's own documented input limit is the binding one, so the reader's smaller default frame
|
|
716
|
+
// ceiling is raised to it: an over-limit MLLP stream is refused by the limit the help output and
|
|
717
|
+
// the command reference state, not by an undocumented one underneath it.
|
|
718
|
+
maxFrameSizeBytes: MAX_INPUT_BYTES
|
|
719
|
+
});
|
|
720
|
+
let lastStart = -1;
|
|
721
|
+
let lastEnd = -1;
|
|
722
|
+
let offset = 0;
|
|
723
|
+
for await (const chunk of chunks) {
|
|
724
|
+
const buf = asBuffer(chunk);
|
|
725
|
+
const start = buf.lastIndexOf(VERTICAL_TAB);
|
|
726
|
+
if (start >= 0) lastStart = offset + start;
|
|
727
|
+
const end = buf.lastIndexOf(FILE_SEPARATOR);
|
|
728
|
+
if (end >= 0) lastEnd = offset + end;
|
|
729
|
+
offset += buf.length;
|
|
730
|
+
reader.push(buf);
|
|
731
|
+
while (ready.length > 0) {
|
|
732
|
+
const frame = ready.shift();
|
|
733
|
+
if (frame !== void 0) yield frame;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
if (lastStart > lastEnd) throw truncatedMllpError();
|
|
737
|
+
}
|
|
738
|
+
var LINE_FEED, CARRIAGE_RETURN, VERTICAL_TAB, FILE_SEPARATOR;
|
|
739
|
+
var init_records = __esm({
|
|
740
|
+
"src/core/records.ts"() {
|
|
741
|
+
init_limits();
|
|
742
|
+
init_parsers();
|
|
743
|
+
LINE_FEED = 10;
|
|
744
|
+
CARRIAGE_RETURN = 13;
|
|
745
|
+
VERTICAL_TAB = 11;
|
|
746
|
+
FILE_SEPARATOR = 28;
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
|
|
615
750
|
// src/core/input.ts
|
|
616
751
|
async function resolveInput(source, formatOverride, deps2, op) {
|
|
617
752
|
if (source === void 0) {
|
|
@@ -633,34 +768,91 @@ async function resolveInput(source, formatOverride, deps2, op) {
|
|
|
633
768
|
if (bytes.length === 0) {
|
|
634
769
|
return fail(new CliError(CLI_CODES.CLI_EMPTY_INPUT, EXIT.DATAERR, "input is empty"));
|
|
635
770
|
}
|
|
771
|
+
const format = resolveFormat(bytes, formatOverride, op);
|
|
772
|
+
if (format instanceof CliError) return fail(format);
|
|
773
|
+
return { ok: true, input: { format, bytes } };
|
|
774
|
+
}
|
|
775
|
+
async function resolveInputStream(source, formatOverride, deps2, op, limit = MAX_INPUT_BYTES) {
|
|
776
|
+
if (source === void 0) {
|
|
777
|
+
return fail(
|
|
778
|
+
new CliError(
|
|
779
|
+
CLI_CODES.CLI_USAGE,
|
|
780
|
+
EXIT.USAGE,
|
|
781
|
+
"missing <file> argument; pass a path or `-` to read stdin"
|
|
782
|
+
)
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
const iterator = withinLimit(openChunks(source, deps2), limit)[Symbol.asyncIterator]();
|
|
786
|
+
const need = formatOverride === void 0 ? DETECT_PREFIX_BYTES : 1;
|
|
787
|
+
const head = [];
|
|
788
|
+
let headBytes = 0;
|
|
789
|
+
try {
|
|
790
|
+
while (headBytes < need) {
|
|
791
|
+
const step = await iterator.next();
|
|
792
|
+
if (step.done === true) break;
|
|
793
|
+
head.push(step.value);
|
|
794
|
+
headBytes += step.value.length;
|
|
795
|
+
}
|
|
796
|
+
} catch (e) {
|
|
797
|
+
if (e instanceof CliError) return fail(e);
|
|
798
|
+
throw e;
|
|
799
|
+
}
|
|
800
|
+
if (headBytes === 0) {
|
|
801
|
+
return fail(new CliError(CLI_CODES.CLI_EMPTY_INPUT, EXIT.DATAERR, "input is empty"));
|
|
802
|
+
}
|
|
803
|
+
const prefix = Buffer.concat(
|
|
804
|
+
head.map((c) => Buffer.from(c.buffer, c.byteOffset, c.length)),
|
|
805
|
+
Math.min(headBytes, DETECT_PREFIX_BYTES)
|
|
806
|
+
);
|
|
807
|
+
const format = resolveFormat(prefix, formatOverride, op);
|
|
808
|
+
if (format instanceof CliError) {
|
|
809
|
+
await iterator.return(void 0);
|
|
810
|
+
return fail(format);
|
|
811
|
+
}
|
|
812
|
+
return { ok: true, input: { format, chunks: rejoin(head, iterator) } };
|
|
813
|
+
}
|
|
814
|
+
function openChunks(source, deps2) {
|
|
815
|
+
if (source === "-") {
|
|
816
|
+
return deps2.openStdin === void 0 ? readerAsChunks(() => deps2.readStdin()) : deps2.openStdin();
|
|
817
|
+
}
|
|
818
|
+
return deps2.openFile === void 0 ? readerAsChunks(() => deps2.readFile(source)) : deps2.openFile(source);
|
|
819
|
+
}
|
|
820
|
+
async function* readerAsChunks(read) {
|
|
821
|
+
yield* oneChunk(await read());
|
|
822
|
+
}
|
|
823
|
+
async function* rejoin(head, rest) {
|
|
824
|
+
for (const chunk of head) yield chunk;
|
|
825
|
+
for (; ; ) {
|
|
826
|
+
const step = await rest.next();
|
|
827
|
+
if (step.done === true) return;
|
|
828
|
+
yield step.value;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function resolveFormat(prefix, formatOverride, op) {
|
|
636
832
|
let format;
|
|
637
833
|
if (formatOverride !== void 0) {
|
|
638
834
|
const narrowed = asCosyteFormat(formatOverride);
|
|
639
835
|
if (narrowed === null) {
|
|
640
|
-
return
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
"unknown --format value; expected one of hl7, fhir, dicom, x12, ccda, ncpdp, astm, mllp"
|
|
645
|
-
)
|
|
836
|
+
return new CliError(
|
|
837
|
+
CLI_CODES.CLI_USAGE,
|
|
838
|
+
EXIT.USAGE,
|
|
839
|
+
"unknown --format value; expected one of hl7, fhir, dicom, x12, ccda, ncpdp, astm, mllp"
|
|
646
840
|
);
|
|
647
841
|
}
|
|
648
842
|
format = narrowed;
|
|
649
843
|
} else {
|
|
650
|
-
const detected = detectFormat(
|
|
651
|
-
if (detected.format === null) return
|
|
844
|
+
const detected = detectFormat(prefix);
|
|
845
|
+
if (detected.format === null) return detectionError(detected);
|
|
652
846
|
format = detected.format;
|
|
653
847
|
}
|
|
654
|
-
if (!supportsOp(format, op)) {
|
|
655
|
-
return
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
`format '${format}' does not support \`${op}\` in this CLI build (${op} supports: ${formatsSupporting(op).join(", ")})`
|
|
660
|
-
)
|
|
848
|
+
if (op !== null && !supportsOp(format, op)) {
|
|
849
|
+
return new CliError(
|
|
850
|
+
CLI_CODES.CLI_FORMAT_UNSUPPORTED,
|
|
851
|
+
EXIT.DATAERR,
|
|
852
|
+
`format '${format}' does not support \`${op}\` in this CLI build (${op} supports: ${formatsSupporting(op).join(", ")})`
|
|
661
853
|
);
|
|
662
854
|
}
|
|
663
|
-
return
|
|
855
|
+
return format;
|
|
664
856
|
}
|
|
665
857
|
function fail(e) {
|
|
666
858
|
return { ok: false, result: errorResult(e) };
|
|
@@ -670,7 +862,9 @@ var init_input = __esm({
|
|
|
670
862
|
init_format();
|
|
671
863
|
init_diagnostics();
|
|
672
864
|
init_exit_codes();
|
|
865
|
+
init_limits();
|
|
673
866
|
init_parsers();
|
|
867
|
+
init_records();
|
|
674
868
|
}
|
|
675
869
|
});
|
|
676
870
|
|
|
@@ -972,11 +1166,24 @@ async function parseCommand(args, deps2, posture = VALUE_FREE) {
|
|
|
972
1166
|
)
|
|
973
1167
|
);
|
|
974
1168
|
}
|
|
975
|
-
const resolved = await
|
|
1169
|
+
const resolved = await resolveInputStream(positionals[0], values.format, deps2, "parse");
|
|
976
1170
|
if (!resolved.ok) return resolved.result;
|
|
977
|
-
const { format,
|
|
1171
|
+
const { format, chunks } = resolved.input;
|
|
978
1172
|
if (format === "mllp" || values.ndjson === true) {
|
|
979
|
-
return await parseMulti(
|
|
1173
|
+
return await parseMulti(
|
|
1174
|
+
format,
|
|
1175
|
+
chunks,
|
|
1176
|
+
values.ndjson === true,
|
|
1177
|
+
values.quiet === true,
|
|
1178
|
+
deps2.writeStdout
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
let bytes;
|
|
1182
|
+
try {
|
|
1183
|
+
bytes = await collectChunks(chunks);
|
|
1184
|
+
} catch (e) {
|
|
1185
|
+
if (e instanceof CliError) return errorResult(e);
|
|
1186
|
+
throw e;
|
|
980
1187
|
}
|
|
981
1188
|
return await parseSingle(format, bytes, values.json === true, values.quiet === true, posture);
|
|
982
1189
|
}
|
|
@@ -995,23 +1202,57 @@ async function parseSingle(format, bytes, json, quiet, posture) {
|
|
|
995
1202
|
` : "";
|
|
996
1203
|
return { stdout, stderr, exit: EXIT.OK };
|
|
997
1204
|
}
|
|
998
|
-
async function parseMulti(format,
|
|
999
|
-
|
|
1000
|
-
|
|
1205
|
+
async function parseMulti(format, chunks, ndjson, quiet, writeStdout) {
|
|
1206
|
+
const recordFormat = format === "mllp" ? "hl7" : format;
|
|
1207
|
+
const records = format === "mllp" ? mllpFrames(chunks) : ndjsonRecords(chunks);
|
|
1208
|
+
const held = [];
|
|
1209
|
+
const emit2 = (line) => {
|
|
1210
|
+
if (writeStdout === void 0) {
|
|
1211
|
+
held.push(line);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
try {
|
|
1215
|
+
writeStdout(line);
|
|
1216
|
+
} catch {
|
|
1217
|
+
throw new CliError(
|
|
1218
|
+
CLI_CODES.CLI_OUTPUT_WRITE_FAILED,
|
|
1219
|
+
EXIT.IOERR,
|
|
1220
|
+
"could not write to the output stream; it closed before the record stream finished"
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
const written = () => held.join("");
|
|
1225
|
+
let total = 0;
|
|
1226
|
+
let failed = 0;
|
|
1227
|
+
let warnings = 0;
|
|
1001
1228
|
try {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1229
|
+
for await (const record of records) {
|
|
1230
|
+
let line;
|
|
1231
|
+
try {
|
|
1232
|
+
const { model, warnings: ws } = await parseFormat(recordFormat, record);
|
|
1233
|
+
warnings += ws.length;
|
|
1234
|
+
line = { record: total, format: recordFormat, model, warnings: ws };
|
|
1235
|
+
} catch (e) {
|
|
1236
|
+
if (e instanceof CliError) throw e;
|
|
1237
|
+
failed += 1;
|
|
1238
|
+
line = {
|
|
1239
|
+
record: total,
|
|
1240
|
+
format: recordFormat,
|
|
1241
|
+
error: extractStableCode(e) ?? "CLI_PARSE_FAILED"
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
total += 1;
|
|
1245
|
+
emit2(JSON.stringify(line) + "\n");
|
|
1009
1246
|
}
|
|
1010
1247
|
} catch (e) {
|
|
1011
|
-
if (e instanceof CliError)
|
|
1012
|
-
|
|
1248
|
+
if (e instanceof CliError) {
|
|
1249
|
+
return { stdout: written(), stderr: `${formatDiagnostic(e)}
|
|
1250
|
+
`, exit: e.exit };
|
|
1251
|
+
}
|
|
1252
|
+
const rejected = parseFailureResult(format, new Uint8Array(), VALUE_FREE, e);
|
|
1253
|
+
return { stdout: written(), stderr: rejected.stderr, exit: rejected.exit };
|
|
1013
1254
|
}
|
|
1014
|
-
if (
|
|
1255
|
+
if (total === 0) {
|
|
1015
1256
|
return errorResult(
|
|
1016
1257
|
new CliError(
|
|
1017
1258
|
CLI_CODES.CLI_PARSE_FAILED,
|
|
@@ -1020,34 +1261,9 @@ async function parseMulti(format, bytes, ndjson, quiet) {
|
|
|
1020
1261
|
)
|
|
1021
1262
|
);
|
|
1022
1263
|
}
|
|
1023
|
-
const lines = [];
|
|
1024
|
-
let failed = 0;
|
|
1025
|
-
let warnings = 0;
|
|
1026
|
-
for (let i = 0; i < records.length; i += 1) {
|
|
1027
|
-
const rec = records[i];
|
|
1028
|
-
try {
|
|
1029
|
-
const { model, warnings: ws } = await parseFormat(recordFormat, rec);
|
|
1030
|
-
warnings += ws.length;
|
|
1031
|
-
lines.push({ record: i, format: recordFormat, model, warnings: ws });
|
|
1032
|
-
} catch (e) {
|
|
1033
|
-
if (e instanceof CliError) return errorResult(e);
|
|
1034
|
-
failed += 1;
|
|
1035
|
-
lines.push({
|
|
1036
|
-
record: i,
|
|
1037
|
-
format: recordFormat,
|
|
1038
|
-
error: extractStableCode(e) ?? "CLI_PARSE_FAILED"
|
|
1039
|
-
});
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
const stdout = lines.map((l) => JSON.stringify(l)).join("\n") + "\n";
|
|
1043
1264
|
const exit = failed > 0 ? EXIT.DATAERR : EXIT.OK;
|
|
1044
|
-
const stderr = quiet ? "" : `cosyte: parsed ${String(
|
|
1045
|
-
return { stdout, stderr, exit };
|
|
1046
|
-
}
|
|
1047
|
-
function splitLines(bytes) {
|
|
1048
|
-
const text = new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
1049
|
-
const enc = new TextEncoder();
|
|
1050
|
-
return text.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => enc.encode(line));
|
|
1265
|
+
const stderr = quiet ? "" : `cosyte: parsed ${String(total)} ${recordFormat} record(s) (${String(warnings)} warning(s), ${String(failed)} failed)` + (ndjson ? " [ndjson]" : format === "mllp" ? " [mllp]" : "") + "\n";
|
|
1266
|
+
return { stdout: written(), stderr, exit };
|
|
1051
1267
|
}
|
|
1052
1268
|
var PARSE_OPTIONS;
|
|
1053
1269
|
var init_parse = __esm({
|
|
@@ -1057,6 +1273,7 @@ var init_parse = __esm({
|
|
|
1057
1273
|
init_input();
|
|
1058
1274
|
init_parsers();
|
|
1059
1275
|
init_phi();
|
|
1276
|
+
init_records();
|
|
1060
1277
|
init_wrap();
|
|
1061
1278
|
PARSE_OPTIONS = {
|
|
1062
1279
|
format: { type: "string" },
|
|
@@ -1149,11 +1366,19 @@ var init_validate = __esm({
|
|
|
1149
1366
|
var VERSION;
|
|
1150
1367
|
var init_version = __esm({
|
|
1151
1368
|
"src/core/version.ts"() {
|
|
1152
|
-
VERSION = "0.0
|
|
1369
|
+
VERSION = "0.1.0";
|
|
1153
1370
|
}
|
|
1154
1371
|
});
|
|
1155
1372
|
|
|
1156
1373
|
// src/mcp/tools.ts
|
|
1374
|
+
function outputSchema(data) {
|
|
1375
|
+
return {
|
|
1376
|
+
type: "object",
|
|
1377
|
+
properties: { ...OUTCOME_PROPS, data },
|
|
1378
|
+
required: [...OUTCOME_REQUIRED],
|
|
1379
|
+
additionalProperties: false
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1157
1382
|
function inlineDeps(content) {
|
|
1158
1383
|
const bytes = new TextEncoder().encode(content);
|
|
1159
1384
|
const noFile = new CliError(
|
|
@@ -1172,37 +1397,65 @@ function stringArg(args, key) {
|
|
|
1172
1397
|
const v = args[key];
|
|
1173
1398
|
return typeof v === "string" ? v : null;
|
|
1174
1399
|
}
|
|
1175
|
-
function
|
|
1176
|
-
const hardError = result.stdout === "";
|
|
1177
|
-
const text = hardError ? result.stderr.trim() : result.stdout.trim();
|
|
1400
|
+
function structuredResult(structuredContent) {
|
|
1178
1401
|
return {
|
|
1179
|
-
content: [
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
isError: hardError,
|
|
1183
|
-
structuredContent: { exit: result.exit, ok: !hardError }
|
|
1402
|
+
content: [{ type: "text", text: JSON.stringify(structuredContent) }],
|
|
1403
|
+
isError: structuredContent.status === "failed",
|
|
1404
|
+
structuredContent
|
|
1184
1405
|
};
|
|
1185
1406
|
}
|
|
1186
|
-
function
|
|
1187
|
-
return
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1407
|
+
function isRecordLine(value) {
|
|
1408
|
+
return typeof value === "object" && value !== null && "record" in value;
|
|
1409
|
+
}
|
|
1410
|
+
function parsePayload(stdout) {
|
|
1411
|
+
const values = [];
|
|
1412
|
+
for (const line of stdout.split("\n")) {
|
|
1413
|
+
if (line.trim().length === 0) continue;
|
|
1414
|
+
try {
|
|
1415
|
+
values.push(JSON.parse(line));
|
|
1416
|
+
} catch {
|
|
1417
|
+
return void 0;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
const only = values.length === 1 ? values[0] : void 0;
|
|
1421
|
+
return only !== void 0 && !isRecordLine(only) ? only : { records: values };
|
|
1422
|
+
}
|
|
1423
|
+
function diagnosticCode(stderr) {
|
|
1424
|
+
return Object.values(CLI_CODES).find((code) => stderr.includes(`cosyte: ${code}: `));
|
|
1425
|
+
}
|
|
1426
|
+
function toToolResult(result) {
|
|
1427
|
+
if (result.stdout === "") {
|
|
1428
|
+
const code = diagnosticCode(result.stderr);
|
|
1429
|
+
return structuredResult({
|
|
1430
|
+
ok: false,
|
|
1431
|
+
status: "failed",
|
|
1432
|
+
exit: result.exit,
|
|
1433
|
+
...code !== void 0 ? { code } : {}
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
const data = parsePayload(result.stdout);
|
|
1437
|
+
return structuredResult({
|
|
1438
|
+
ok: true,
|
|
1439
|
+
status: result.exit === EXIT.OK ? "success" : "verdict",
|
|
1440
|
+
exit: result.exit,
|
|
1441
|
+
...data !== void 0 ? { data } : {}
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
function usageError() {
|
|
1445
|
+
return structuredResult({
|
|
1446
|
+
ok: false,
|
|
1447
|
+
status: "failed",
|
|
1448
|
+
exit: EXIT.USAGE,
|
|
1449
|
+
code: CLI_CODES.CLI_USAGE
|
|
1450
|
+
});
|
|
1192
1451
|
}
|
|
1193
1452
|
function internalError(e) {
|
|
1194
1453
|
const err = toCliError(e);
|
|
1195
|
-
return {
|
|
1196
|
-
content: [{ type: "text", text: `cosyte: ${err.code}: ${err.message}` }],
|
|
1197
|
-
isError: true,
|
|
1198
|
-
structuredContent: { exit: err.exit, ok: false }
|
|
1199
|
-
};
|
|
1454
|
+
return structuredResult({ ok: false, status: "failed", exit: err.exit, code: err.code });
|
|
1200
1455
|
}
|
|
1201
1456
|
async function dispatchTool(name, args, deps2) {
|
|
1202
1457
|
const content = stringArg(args, "content");
|
|
1203
|
-
if (content === null)
|
|
1204
|
-
return usageError("missing required 'content' argument (the message text to operate on)");
|
|
1205
|
-
}
|
|
1458
|
+
if (content === null) return usageError();
|
|
1206
1459
|
const runDeps = inlineDeps(content);
|
|
1207
1460
|
const format = stringArg(args, "format");
|
|
1208
1461
|
const fmtFlag = format !== null ? ["--format", format] : [];
|
|
@@ -1221,13 +1474,13 @@ async function dispatchTool(name, args, deps2) {
|
|
|
1221
1474
|
return toToolResult(await convertCommand(["-", "--to", to, "--json"], runDeps, VALUE_FREE));
|
|
1222
1475
|
}
|
|
1223
1476
|
default:
|
|
1224
|
-
return usageError(
|
|
1477
|
+
return usageError();
|
|
1225
1478
|
}
|
|
1226
1479
|
} catch (e) {
|
|
1227
1480
|
return internalError(e);
|
|
1228
1481
|
}
|
|
1229
1482
|
}
|
|
1230
|
-
var CONTENT_PROP, FORMAT_PROP, TOOL_DEFS;
|
|
1483
|
+
var CONTENT_PROP, FORMAT_PROP, OUTCOME_PROPS, OUTCOME_REQUIRED, TOOL_DEFS;
|
|
1231
1484
|
var init_tools = __esm({
|
|
1232
1485
|
"src/mcp/tools.ts"() {
|
|
1233
1486
|
init_convert();
|
|
@@ -1250,39 +1503,108 @@ var init_tools = __esm({
|
|
|
1250
1503
|
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)."
|
|
1251
1504
|
}
|
|
1252
1505
|
};
|
|
1506
|
+
OUTCOME_PROPS = {
|
|
1507
|
+
ok: {
|
|
1508
|
+
type: "boolean",
|
|
1509
|
+
description: "True iff the tool call produced data. A negative verdict about the message is still true (the tool worked); only a call that produced no data at all is false."
|
|
1510
|
+
},
|
|
1511
|
+
status: {
|
|
1512
|
+
type: "string",
|
|
1513
|
+
enum: ["success", "verdict", "failed"],
|
|
1514
|
+
description: "The outcome to branch on without parsing text: 'success' the operation completed cleanly, 'verdict' the tool ran and reports a negative finding about the message, 'failed' the call produced no data (a usage mistake, unparseable input, an unavailable parser, an internal error)."
|
|
1515
|
+
},
|
|
1516
|
+
exit: {
|
|
1517
|
+
type: "integer",
|
|
1518
|
+
enum: Object.values(EXIT),
|
|
1519
|
+
description: "The exit code the underlying command resolved to, from the documented exit-code contract."
|
|
1520
|
+
},
|
|
1521
|
+
code: {
|
|
1522
|
+
type: "string",
|
|
1523
|
+
enum: Object.values(CLI_CODES),
|
|
1524
|
+
description: "The stable diagnostic code on a failed call, drawn from a fixed registry so it never carries an input value. Absent on a call that produced data."
|
|
1525
|
+
}
|
|
1526
|
+
};
|
|
1527
|
+
OUTCOME_REQUIRED = ["ok", "status", "exit"];
|
|
1253
1528
|
TOOL_DEFS = [
|
|
1254
1529
|
{
|
|
1255
1530
|
name: "parse",
|
|
1531
|
+
title: "Parse a healthcare message",
|
|
1256
1532
|
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.",
|
|
1257
1533
|
inputSchema: {
|
|
1258
1534
|
type: "object",
|
|
1259
1535
|
properties: { ...CONTENT_PROP, ...FORMAT_PROP },
|
|
1260
1536
|
required: ["content"],
|
|
1261
1537
|
additionalProperties: false
|
|
1262
|
-
}
|
|
1538
|
+
},
|
|
1539
|
+
outputSchema: outputSchema({
|
|
1540
|
+
type: "object",
|
|
1541
|
+
description: "The parse payload, present on a call that produced data. A single message parses to format + model + warnings. A multi-record input (an MLLP stream) parses to `records`, one entry per record: a parsed record, or a value-free per-record error code.",
|
|
1542
|
+
properties: {
|
|
1543
|
+
format: { type: "string", description: "The format the input was parsed as." },
|
|
1544
|
+
model: { description: "The wrapped library's parsed model, verbatim: the data channel." },
|
|
1545
|
+
warnings: {
|
|
1546
|
+
type: "array",
|
|
1547
|
+
description: "Value-free parse warnings: a stable code plus positional context."
|
|
1548
|
+
},
|
|
1549
|
+
records: {
|
|
1550
|
+
type: "array",
|
|
1551
|
+
description: "One entry per record of a multi-record input, in stream order."
|
|
1552
|
+
}
|
|
1553
|
+
},
|
|
1554
|
+
additionalProperties: false
|
|
1555
|
+
})
|
|
1263
1556
|
},
|
|
1264
1557
|
{
|
|
1265
1558
|
name: "validate",
|
|
1559
|
+
title: "Validate a healthcare message",
|
|
1266
1560
|
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).",
|
|
1267
1561
|
inputSchema: {
|
|
1268
1562
|
type: "object",
|
|
1269
1563
|
properties: { ...CONTENT_PROP, ...FORMAT_PROP },
|
|
1270
1564
|
required: ["content"],
|
|
1271
1565
|
additionalProperties: false
|
|
1272
|
-
}
|
|
1566
|
+
},
|
|
1567
|
+
outputSchema: outputSchema({
|
|
1568
|
+
type: "object",
|
|
1569
|
+
description: "The validation payload, present on a call that produced data (a valid result and a negative verdict alike). Value-free throughout: findings are codes, severities and positional locators, never a field value.",
|
|
1570
|
+
properties: {
|
|
1571
|
+
format: { type: "string", description: "The format the input was validated as." },
|
|
1572
|
+
valid: {
|
|
1573
|
+
type: "boolean",
|
|
1574
|
+
description: "The verdict: false means parsed-but-non-conformant, not a failed call."
|
|
1575
|
+
},
|
|
1576
|
+
findings: {
|
|
1577
|
+
type: "array",
|
|
1578
|
+
description: "Value-free findings: a stable code, a severity, and a positional locator."
|
|
1579
|
+
}
|
|
1580
|
+
},
|
|
1581
|
+
required: ["format", "valid", "findings"],
|
|
1582
|
+
additionalProperties: false
|
|
1583
|
+
})
|
|
1273
1584
|
},
|
|
1274
1585
|
{
|
|
1275
1586
|
name: "inspect",
|
|
1587
|
+
title: "Inspect a healthcare message's structure",
|
|
1276
1588
|
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.",
|
|
1277
1589
|
inputSchema: {
|
|
1278
1590
|
type: "object",
|
|
1279
1591
|
properties: { ...CONTENT_PROP, ...FORMAT_PROP },
|
|
1280
1592
|
required: ["content"],
|
|
1281
1593
|
additionalProperties: false
|
|
1282
|
-
}
|
|
1594
|
+
},
|
|
1595
|
+
outputSchema: outputSchema({
|
|
1596
|
+
type: "object",
|
|
1597
|
+
description: "The value-free structural summary, present on a call that produced data. `format` names the shape of the rest, which is that format's own counts and structural type codes (a message type, a resource type, a transaction-set id): classification, never a value.",
|
|
1598
|
+
properties: {
|
|
1599
|
+
format: { type: "string", description: "The format the summary describes." }
|
|
1600
|
+
},
|
|
1601
|
+
required: ["format"],
|
|
1602
|
+
additionalProperties: true
|
|
1603
|
+
})
|
|
1283
1604
|
},
|
|
1284
1605
|
{
|
|
1285
1606
|
name: "convert",
|
|
1607
|
+
title: "Convert HL7 v2 to a FHIR R4 Bundle",
|
|
1286
1608
|
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.",
|
|
1287
1609
|
inputSchema: {
|
|
1288
1610
|
type: "object",
|
|
@@ -1296,7 +1618,21 @@ var init_tools = __esm({
|
|
|
1296
1618
|
},
|
|
1297
1619
|
required: ["content"],
|
|
1298
1620
|
additionalProperties: false
|
|
1299
|
-
}
|
|
1621
|
+
},
|
|
1622
|
+
outputSchema: outputSchema({
|
|
1623
|
+
type: "object",
|
|
1624
|
+
description: "The conversion payload, present on a call that produced data. An error-severity finding is a negative verdict (the Bundle is still returned), not a failed call.",
|
|
1625
|
+
properties: {
|
|
1626
|
+
format: { type: "string", description: "The conversion target the Bundle is in." },
|
|
1627
|
+
bundle: { type: "object", description: "The converted FHIR R4 Bundle: the data channel." },
|
|
1628
|
+
findings: {
|
|
1629
|
+
type: "array",
|
|
1630
|
+
description: "Value-free conversion findings: a code, a severity, and a positional locator."
|
|
1631
|
+
}
|
|
1632
|
+
},
|
|
1633
|
+
required: ["format", "bundle", "findings"],
|
|
1634
|
+
additionalProperties: false
|
|
1635
|
+
})
|
|
1300
1636
|
}
|
|
1301
1637
|
];
|
|
1302
1638
|
}
|
|
@@ -1314,12 +1650,21 @@ function createMcpServer() {
|
|
|
1314
1650
|
server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
1315
1651
|
tools: TOOL_DEFS.map((t) => ({
|
|
1316
1652
|
name: t.name,
|
|
1653
|
+
title: t.title,
|
|
1317
1654
|
description: t.description,
|
|
1318
1655
|
inputSchema: {
|
|
1319
1656
|
type: t.inputSchema.type,
|
|
1320
1657
|
properties: { ...t.inputSchema.properties },
|
|
1321
1658
|
...t.inputSchema.required !== void 0 ? { required: [...t.inputSchema.required] } : {},
|
|
1322
1659
|
...t.inputSchema.additionalProperties !== void 0 ? { additionalProperties: t.inputSchema.additionalProperties } : {}
|
|
1660
|
+
},
|
|
1661
|
+
// Every advertised tool publishes its result schema; a client validates a reply against it
|
|
1662
|
+
// instead of pattern-matching the text block. Declared, so it is copied unconditionally.
|
|
1663
|
+
outputSchema: {
|
|
1664
|
+
type: t.outputSchema.type,
|
|
1665
|
+
properties: { ...t.outputSchema.properties },
|
|
1666
|
+
required: [...t.outputSchema.required],
|
|
1667
|
+
additionalProperties: t.outputSchema.additionalProperties
|
|
1323
1668
|
}
|
|
1324
1669
|
}))
|
|
1325
1670
|
}));
|
|
@@ -1329,7 +1674,9 @@ function createMcpServer() {
|
|
|
1329
1674
|
return {
|
|
1330
1675
|
content: result.content.map((c) => ({ type: c.type, text: c.text })),
|
|
1331
1676
|
isError: result.isError,
|
|
1332
|
-
|
|
1677
|
+
// The whole structured value reaches the wire. A field-by-field copy here would silently
|
|
1678
|
+
// truncate the payload the called tool's own schema promises.
|
|
1679
|
+
structuredContent: { ...result.structuredContent }
|
|
1333
1680
|
};
|
|
1334
1681
|
});
|
|
1335
1682
|
return server;
|
|
@@ -1347,27 +1694,73 @@ var init_server = __esm({
|
|
|
1347
1694
|
}
|
|
1348
1695
|
});
|
|
1349
1696
|
|
|
1697
|
+
// src/bin/cosyte.ts
|
|
1698
|
+
init_diagnostics();
|
|
1699
|
+
init_exit_codes();
|
|
1700
|
+
|
|
1350
1701
|
// src/core/io.ts
|
|
1351
1702
|
init_diagnostics();
|
|
1352
1703
|
init_exit_codes();
|
|
1353
|
-
|
|
1704
|
+
init_limits();
|
|
1705
|
+
function noInputError(path) {
|
|
1706
|
+
return new CliError(
|
|
1707
|
+
CLI_CODES.CLI_NO_INPUT,
|
|
1708
|
+
EXIT.NOINPUT,
|
|
1709
|
+
`cannot read input file: ${path} (does it exist and is it readable?)`
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
async function readFileBytes(path, limit = MAX_INPUT_BYTES) {
|
|
1713
|
+
let handle;
|
|
1354
1714
|
try {
|
|
1355
|
-
|
|
1715
|
+
handle = await open(path, "r");
|
|
1356
1716
|
} catch {
|
|
1357
|
-
throw
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
);
|
|
1717
|
+
throw noInputError(path);
|
|
1718
|
+
}
|
|
1719
|
+
try {
|
|
1720
|
+
const info = await handle.stat();
|
|
1721
|
+
if (info.isFile() && info.size > limit) throw inputTooLargeError(limit);
|
|
1722
|
+
return await handle.readFile();
|
|
1723
|
+
} catch (e) {
|
|
1724
|
+
if (e instanceof CliError) throw e;
|
|
1725
|
+
throw noInputError(path);
|
|
1726
|
+
} finally {
|
|
1727
|
+
await handle.close();
|
|
1362
1728
|
}
|
|
1363
1729
|
}
|
|
1364
|
-
async function readStreamBytes(stream) {
|
|
1730
|
+
async function readStreamBytes(stream, limit = MAX_INPUT_BYTES) {
|
|
1365
1731
|
const chunks = [];
|
|
1732
|
+
let total = 0;
|
|
1366
1733
|
for await (const chunk of stream) {
|
|
1367
|
-
|
|
1734
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk, "utf-8") : chunk;
|
|
1735
|
+
total += buf.length;
|
|
1736
|
+
if (total > limit) throw inputTooLargeError(limit);
|
|
1737
|
+
chunks.push(buf);
|
|
1368
1738
|
}
|
|
1369
1739
|
return Buffer.concat(chunks);
|
|
1370
1740
|
}
|
|
1741
|
+
async function* streamChunks(stream) {
|
|
1742
|
+
for await (const chunk of stream) {
|
|
1743
|
+
yield typeof chunk === "string" ? Buffer.from(chunk, "utf-8") : chunk;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
async function* fileChunks(path) {
|
|
1747
|
+
let handle;
|
|
1748
|
+
try {
|
|
1749
|
+
handle = await open(path, "r");
|
|
1750
|
+
} catch {
|
|
1751
|
+
throw noInputError(path);
|
|
1752
|
+
}
|
|
1753
|
+
try {
|
|
1754
|
+
const stream = handle.createReadStream();
|
|
1755
|
+
try {
|
|
1756
|
+
for await (const chunk of stream) yield chunk;
|
|
1757
|
+
} catch {
|
|
1758
|
+
throw noInputError(path);
|
|
1759
|
+
}
|
|
1760
|
+
} finally {
|
|
1761
|
+
await handle.close();
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1371
1764
|
|
|
1372
1765
|
// src/commands/completion.ts
|
|
1373
1766
|
init_diagnostics();
|
|
@@ -1628,47 +2021,248 @@ function renderNote(result) {
|
|
|
1628
2021
|
// src/core/run.ts
|
|
1629
2022
|
init_parse();
|
|
1630
2023
|
|
|
1631
|
-
// src/commands/redact.ts
|
|
1632
|
-
init_diagnostics();
|
|
1633
|
-
|
|
1634
2024
|
// src/core/deid.ts
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
2025
|
+
init_diagnostics();
|
|
2026
|
+
init_exit_codes();
|
|
2027
|
+
init_parsers();
|
|
2028
|
+
init_wrap();
|
|
2029
|
+
var DEID_PACKAGE = "@cosyte/deid";
|
|
2030
|
+
var DEID_COVERAGE = {
|
|
2031
|
+
hl7: "covered",
|
|
2032
|
+
fhir: "covered",
|
|
2033
|
+
x12: "covered",
|
|
2034
|
+
ccda: "covered",
|
|
2035
|
+
// The delegate's NCPDP adapter is Telecom; this CLI resolves SCRIPT.
|
|
2036
|
+
ncpdp: "no-adapter",
|
|
2037
|
+
astm: "no-adapter",
|
|
2038
|
+
mllp: "no-adapter",
|
|
2039
|
+
// Covered by the delegate, but its result is Part 10 bytes and stdout here is text.
|
|
2040
|
+
dicom: "unserializable"
|
|
2041
|
+
};
|
|
2042
|
+
function deidCoverage(format) {
|
|
2043
|
+
return DEID_COVERAGE[format];
|
|
2044
|
+
}
|
|
2045
|
+
function isDeidCovered(format) {
|
|
2046
|
+
return DEID_COVERAGE[format] === "covered";
|
|
2047
|
+
}
|
|
2048
|
+
function deidCoveredFormats() {
|
|
2049
|
+
return Object.keys(DEID_COVERAGE).filter(isDeidCovered).sort();
|
|
2050
|
+
}
|
|
2051
|
+
var DEID_BLOCKED = "blocked";
|
|
2052
|
+
var DEID_UNAVAILABLE_REASON = `the ${DEID_PACKAGE} de-identification library is not installed; install it to use redact/deid (it is an optional dependency). 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`;
|
|
2053
|
+
function adapterUnavailable(format) {
|
|
2054
|
+
return `the ${DEID_PACKAGE}/${format} de-identification adapter could not be loaded; it needs both ${DEID_PACKAGE} and the @cosyte/${format} parser to be installed`;
|
|
2055
|
+
}
|
|
2056
|
+
function parseOrFail(format, parse) {
|
|
2057
|
+
try {
|
|
2058
|
+
return parse();
|
|
2059
|
+
} catch (e) {
|
|
2060
|
+
const code = extractStableCode(e);
|
|
2061
|
+
throw new CliError(
|
|
2062
|
+
CLI_CODES.CLI_PARSE_FAILED,
|
|
2063
|
+
EXIT.DATAERR,
|
|
2064
|
+
`the ${format} parser rejected the input${code === null ? "" : ` (${code})`}`
|
|
2065
|
+
);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
function deidentifyOrFail(run2) {
|
|
2069
|
+
try {
|
|
2070
|
+
return run2();
|
|
2071
|
+
} catch (e) {
|
|
2072
|
+
const code = extractStableCode(e);
|
|
2073
|
+
if (code === null) throw e;
|
|
2074
|
+
throw new CliError(
|
|
2075
|
+
CLI_CODES.CLI_DEID_INCOMPLETE,
|
|
2076
|
+
EXIT.INVALID,
|
|
2077
|
+
`the ${DEID_PACKAGE} de-identifier could not complete this input (${code}); no output was emitted`
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
function decode2(bytes) {
|
|
2082
|
+
return new TextDecoder("utf-8", { fatal: false }).decode(bytes);
|
|
2083
|
+
}
|
|
2084
|
+
function toRecords(manifest) {
|
|
2085
|
+
return manifest.map((e) => ({
|
|
2086
|
+
category: e.category,
|
|
2087
|
+
transform: e.transform,
|
|
2088
|
+
locus: e.locus,
|
|
2089
|
+
count: e.count,
|
|
2090
|
+
disposition: e.disposition,
|
|
2091
|
+
code: e.code
|
|
2092
|
+
}));
|
|
2093
|
+
}
|
|
2094
|
+
async function loadDeidDelegate(importCore = () => import('@cosyte/deid')) {
|
|
2095
|
+
const core = await loadOptionalPackage(DEID_UNAVAILABLE_REASON, importCore);
|
|
2096
|
+
const context = core.createDeidContext({ key: randomBytes(32) });
|
|
2097
|
+
return {
|
|
2098
|
+
label: core.OUTPUT_LABEL,
|
|
2099
|
+
version: core.VERSION,
|
|
2100
|
+
redact: async (format, bytes) => {
|
|
2101
|
+
switch (format) {
|
|
2102
|
+
case "hl7": {
|
|
2103
|
+
const { parseHL7 } = await import('@cosyte/hl7');
|
|
2104
|
+
const { deidentifyHl7 } = await loadOptionalPackage(
|
|
2105
|
+
adapterUnavailable("hl7"),
|
|
2106
|
+
() => import('@cosyte/deid/hl7')
|
|
2107
|
+
);
|
|
2108
|
+
const msg = parseOrFail("hl7", () => parseHL7(Buffer.from(bytes)));
|
|
2109
|
+
return deidentifyOrFail(() => {
|
|
2110
|
+
const { document, manifest } = deidentifyHl7(msg, { context });
|
|
2111
|
+
return { output: document.toString(), manifest: toRecords(manifest) };
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
case "fhir": {
|
|
2115
|
+
const { parseResource, serializeResource } = await loadFhir();
|
|
2116
|
+
const { deidentifyFhir } = await loadOptionalPackage(
|
|
2117
|
+
adapterUnavailable("fhir"),
|
|
2118
|
+
() => import('@cosyte/deid/fhir')
|
|
2119
|
+
);
|
|
2120
|
+
const resource = parseOrFail("fhir", () => parseResource(decode2(bytes)).resource);
|
|
2121
|
+
return deidentifyOrFail(() => {
|
|
2122
|
+
const { document, manifest } = deidentifyFhir(resource, { context });
|
|
2123
|
+
return { output: serializeResource(document), manifest: toRecords(manifest) };
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
case "x12": {
|
|
2127
|
+
const { parseX12 } = await loadOptional("x12", () => import('@cosyte/x12'));
|
|
2128
|
+
const { deidentifyX12 } = await loadOptionalPackage(
|
|
2129
|
+
adapterUnavailable("x12"),
|
|
2130
|
+
() => import('@cosyte/deid/x12')
|
|
2131
|
+
);
|
|
2132
|
+
const interchange = parseOrFail("x12", () => parseX12(Buffer.from(bytes)));
|
|
2133
|
+
return deidentifyOrFail(() => {
|
|
2134
|
+
const { x12, manifest } = deidentifyX12(interchange, { context });
|
|
2135
|
+
return { output: x12, manifest: toRecords(manifest) };
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
case "ccda": {
|
|
2139
|
+
const { parseCcda, serializeCcda } = await loadOptional(
|
|
2140
|
+
"ccda",
|
|
2141
|
+
() => import('@cosyte/ccda')
|
|
2142
|
+
);
|
|
2143
|
+
const { deidentifyCcda } = await loadOptionalPackage(
|
|
2144
|
+
adapterUnavailable("ccda"),
|
|
2145
|
+
() => import('@cosyte/deid/ccda')
|
|
2146
|
+
);
|
|
2147
|
+
const doc = parseOrFail("ccda", () => parseCcda(decode2(bytes)));
|
|
2148
|
+
return deidentifyOrFail(() => {
|
|
2149
|
+
const { document, manifest } = deidentifyCcda(doc, { context });
|
|
2150
|
+
return { output: serializeCcda(document), manifest: toRecords(manifest) };
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
};
|
|
1638
2156
|
}
|
|
1639
2157
|
|
|
1640
2158
|
// src/commands/redact.ts
|
|
2159
|
+
init_diagnostics();
|
|
1641
2160
|
init_exit_codes();
|
|
2161
|
+
init_input();
|
|
1642
2162
|
var REDACT_OPTIONS = {
|
|
1643
2163
|
format: { type: "string" }
|
|
1644
2164
|
};
|
|
1645
|
-
function
|
|
2165
|
+
function manifestLine(entry) {
|
|
2166
|
+
return `cosyte: redact: ${entry.category} ${entry.transform} ${entry.locus} x${String(entry.count)} ${entry.disposition} ${entry.code}
|
|
2167
|
+
`;
|
|
2168
|
+
}
|
|
2169
|
+
function attribution(delegate) {
|
|
2170
|
+
return `cosyte: redact: delegated to ${DEID_PACKAGE} ${delegate.version}: ${delegate.label}
|
|
2171
|
+
`;
|
|
2172
|
+
}
|
|
2173
|
+
function manifestLines(outcome) {
|
|
2174
|
+
return outcome.manifest.map(manifestLine).join("");
|
|
2175
|
+
}
|
|
2176
|
+
function countOf(outcome, disposition) {
|
|
2177
|
+
return outcome.manifest.filter((e) => e.disposition === disposition).length;
|
|
2178
|
+
}
|
|
2179
|
+
function tally(format, outcome) {
|
|
2180
|
+
const transformed = countOf(outcome, "transformed");
|
|
2181
|
+
const removed = countOf(outcome, "removed");
|
|
2182
|
+
const blocked = countOf(outcome, DEID_BLOCKED);
|
|
2183
|
+
return `cosyte: redact: ${format}: ${String(outcome.manifest.length)} loci acted on (${String(transformed)} transformed, ${String(removed)} removed, ${String(blocked)} blocked)
|
|
2184
|
+
`;
|
|
2185
|
+
}
|
|
2186
|
+
var EPHEMERAL_KEY_NOTE = "cosyte: redact: identifier surrogates are keyed with a per-invocation ephemeral key: consistent within this output, not stable across runs\n";
|
|
2187
|
+
async function redactCommand(args, deps2, loadDelegate = loadDeidDelegate) {
|
|
2188
|
+
let values;
|
|
2189
|
+
let positionals;
|
|
1646
2190
|
try {
|
|
1647
|
-
parseArgs({ args, options: REDACT_OPTIONS, allowPositionals: true });
|
|
2191
|
+
const parsed = parseArgs({ args, options: REDACT_OPTIONS, allowPositionals: true });
|
|
2192
|
+
values = parsed.values;
|
|
2193
|
+
positionals = parsed.positionals;
|
|
1648
2194
|
} catch {
|
|
2195
|
+
return errorResult(
|
|
2196
|
+
new CliError(
|
|
2197
|
+
CLI_CODES.CLI_USAGE,
|
|
2198
|
+
EXIT.USAGE,
|
|
2199
|
+
"invalid arguments to `redact` (see `cosyte --help`)"
|
|
2200
|
+
)
|
|
2201
|
+
);
|
|
2202
|
+
}
|
|
2203
|
+
let delegate;
|
|
2204
|
+
try {
|
|
2205
|
+
delegate = await loadDelegate();
|
|
2206
|
+
} catch (e) {
|
|
2207
|
+
if (e instanceof CliError) return errorResult(e);
|
|
2208
|
+
throw e;
|
|
2209
|
+
}
|
|
2210
|
+
const resolved = await resolveInput(positionals[0], values.format, deps2, null);
|
|
2211
|
+
if (!resolved.ok) return resolved.result;
|
|
2212
|
+
const { format, bytes } = resolved.input;
|
|
2213
|
+
if (!isDeidCovered(format)) return errorResult(uncovered(format));
|
|
2214
|
+
let outcome;
|
|
2215
|
+
try {
|
|
2216
|
+
outcome = await delegate.redact(format, bytes);
|
|
2217
|
+
} catch (e) {
|
|
2218
|
+
if (e instanceof CliError) return errorResult(e);
|
|
2219
|
+
throw e;
|
|
2220
|
+
}
|
|
2221
|
+
const blocked = outcome.manifest.filter((e) => e.disposition === DEID_BLOCKED);
|
|
2222
|
+
if (blocked.length > 0) {
|
|
2223
|
+
const incomplete = new CliError(
|
|
2224
|
+
CLI_CODES.CLI_DEID_INCOMPLETE,
|
|
2225
|
+
EXIT.INVALID,
|
|
2226
|
+
`${DEID_PACKAGE} blocked ${String(blocked.length)} of ${String(outcome.manifest.length)} loci in this ${format} input; no output was emitted`
|
|
2227
|
+
);
|
|
1649
2228
|
return {
|
|
1650
2229
|
stdout: "",
|
|
1651
|
-
stderr: `cosyte: ${
|
|
2230
|
+
stderr: attribution(delegate) + manifestLines(outcome) + tally(format, outcome) + `cosyte: ${incomplete.code}: ${incomplete.message}
|
|
1652
2231
|
`,
|
|
1653
|
-
exit:
|
|
2232
|
+
exit: incomplete.exit
|
|
1654
2233
|
};
|
|
1655
2234
|
}
|
|
1656
|
-
const status = deidStatus();
|
|
1657
2235
|
return {
|
|
1658
|
-
stdout:
|
|
1659
|
-
stderr: `cosyte: ${CLI_CODES.CLI_NOT_IMPLEMENTED}: ${status.reason}
|
|
2236
|
+
stdout: `${outcome.output}
|
|
1660
2237
|
`,
|
|
1661
|
-
|
|
2238
|
+
stderr: attribution(delegate) + manifestLines(outcome) + tally(format, outcome) + EPHEMERAL_KEY_NOTE,
|
|
2239
|
+
exit: EXIT.OK
|
|
1662
2240
|
};
|
|
1663
2241
|
}
|
|
2242
|
+
function uncovered(format) {
|
|
2243
|
+
if (deidCoverage(format) === "unserializable") {
|
|
2244
|
+
return new CliError(
|
|
2245
|
+
CLI_CODES.CLI_FORMAT_UNSUPPORTED,
|
|
2246
|
+
EXIT.DATAERR,
|
|
2247
|
+
`format '${format}' cannot be redacted onto this CLI's text output channel (its de-identified form is a binary stream, so \`parse\` and \`fmt\` are unwired too)`
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
return new CliError(
|
|
2251
|
+
CLI_CODES.CLI_NOT_IMPLEMENTED,
|
|
2252
|
+
EXIT.UNAVAILABLE,
|
|
2253
|
+
`${DEID_PACKAGE} does not de-identify the ${format} standard this CLI resolves, and the CLI ships no redactor of its own, so nothing is emitted`
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
1664
2256
|
|
|
1665
2257
|
// src/core/run.ts
|
|
1666
2258
|
init_validate();
|
|
1667
2259
|
init_diagnostics();
|
|
1668
2260
|
init_exit_codes();
|
|
2261
|
+
init_limits();
|
|
1669
2262
|
init_phi();
|
|
1670
2263
|
init_version();
|
|
1671
|
-
|
|
2264
|
+
function helpText(limitBytes = MAX_INPUT_BYTES) {
|
|
2265
|
+
return `cosyte: a PHI-safe developer CLI over the @cosyte/* healthcare parsers
|
|
1672
2266
|
|
|
1673
2267
|
Usage:
|
|
1674
2268
|
cosyte <command> [options]
|
|
@@ -1680,7 +2274,7 @@ Commands:
|
|
|
1680
2274
|
fmt <file|-> Canonically re-serialize via the parser's spec-clean serializer
|
|
1681
2275
|
convert <file|-> Convert HL7 v2 \u2192 FHIR R4 via @cosyte/transform (use --to fhir)
|
|
1682
2276
|
map-codes <cmap|-> Translate a code through a BYO FHIR ConceptMap via @cosyte/terminology
|
|
1683
|
-
redact <file|-> De-identify a message (alias: deid):
|
|
2277
|
+
redact <file|-> De-identify a message via @cosyte/deid (alias: deid): ${deidCoveredFormats().join(" ")}
|
|
1684
2278
|
mcp Start the stdio MCP server (agent front door; also the cosyte-mcp bin)
|
|
1685
2279
|
completion <shell> Print a shell completion script (bash | zsh | fish)
|
|
1686
2280
|
|
|
@@ -1701,22 +2295,44 @@ Common options (parse / validate / inspect / fmt):
|
|
|
1701
2295
|
convert options:
|
|
1702
2296
|
--to fhir The conversion target (required; only HL7 v2 \u2192 FHIR R4 today)
|
|
1703
2297
|
|
|
2298
|
+
redact / deid (de-identification is delegated to @cosyte/deid; the CLI adds no policy of its own):
|
|
2299
|
+
Covered formats: ${deidCoveredFormats().join(", ")}. Every other format is refused rather than
|
|
2300
|
+
approximated: astm, mllp and ncpdp have no adapter (69), and dicom is covered by the library but
|
|
2301
|
+
its de-identified form is a binary stream this text stdout cannot carry (65). If the library
|
|
2302
|
+
reports it could not handle an element, the run exits 1 and emits no output at all: a partial
|
|
2303
|
+
scrub presented as de-identified is the one outcome this command exists to prevent.
|
|
2304
|
+
stdout carries the de-identified document; stderr carries the library's own value-free manifest
|
|
2305
|
+
(category, transform, locus, count, disposition code) plus its published label and version.
|
|
2306
|
+
Identifier surrogates are keyed with a per-invocation ephemeral key: consistent within one
|
|
2307
|
+
output, not stable across runs.
|
|
2308
|
+
|
|
1704
2309
|
map-codes options (the positional is a BYO FHIR ConceptMap; a code is not PHI):
|
|
1705
2310
|
--code <code> The source code to translate (required)
|
|
1706
2311
|
--system <uri> The source code system (optional; selects the ConceptMap group)
|
|
1707
2312
|
--version <v> The source code system version (optional)
|
|
1708
2313
|
--display <d> The source display (optional)
|
|
1709
2314
|
|
|
2315
|
+
Input size:
|
|
2316
|
+
parse reads at most ${describeByteLimit(limitBytes)} of input per invocation. A larger input is
|
|
2317
|
+
refused with CLI_INPUT_TOO_LARGE and the data-error exit code (65), never the internal-error
|
|
2318
|
+
code; split the input and re-run. Multi-record output (--ndjson, MLLP) is written record by
|
|
2319
|
+
record as it is parsed, so a refusal can arrive after some records have already been emitted:
|
|
2320
|
+
the exit code, not the output, is the signal that the run did not complete.
|
|
2321
|
+
|
|
1710
2322
|
Exit codes:
|
|
1711
|
-
0 success / valid 65 data error (unparseable / undetected format)
|
|
2323
|
+
0 success / valid 65 data error (unparseable / undetected format / input too large)
|
|
1712
2324
|
1 invalid (validate) 66 no input (missing/unreadable file)
|
|
1713
|
-
|
|
2325
|
+
or de-identification incomplete (redact)
|
|
2326
|
+
2 usage error 69 unavailable (a capability is not wired here, e.g. redact on a
|
|
2327
|
+
format @cosyte/deid has no adapter for)
|
|
1714
2328
|
70 internal error
|
|
1715
2329
|
|
|
1716
2330
|
PHI posture: the parsed model goes to stdout (the data channel you chose); every diagnostic on
|
|
1717
2331
|
stderr is value-free (codes and positions only, never a field value) unless you pass the loud,
|
|
1718
2332
|
opt-in --unsafe-show-values (which permits a bounded input excerpt in a failure diagnostic).
|
|
1719
2333
|
`;
|
|
2334
|
+
}
|
|
2335
|
+
var HELP = helpText();
|
|
1720
2336
|
function wantsHelp(argv) {
|
|
1721
2337
|
return argv.includes("--help") || argv.includes("-h");
|
|
1722
2338
|
}
|
|
@@ -1746,7 +2362,7 @@ async function run(argv, deps2) {
|
|
|
1746
2362
|
return await mapCodesCommand(rest, deps2);
|
|
1747
2363
|
case "redact":
|
|
1748
2364
|
case "deid":
|
|
1749
|
-
return redactCommand(rest);
|
|
2365
|
+
return await redactCommand(rest, deps2);
|
|
1750
2366
|
case "completion":
|
|
1751
2367
|
return completionCommand(rest);
|
|
1752
2368
|
default:
|
|
@@ -1768,22 +2384,90 @@ function resolve(e) {
|
|
|
1768
2384
|
}
|
|
1769
2385
|
|
|
1770
2386
|
// src/bin/cosyte.ts
|
|
2387
|
+
var serverMode = process.argv[2] === "mcp";
|
|
2388
|
+
var stdoutOpen = true;
|
|
2389
|
+
var stderrOpen = true;
|
|
2390
|
+
var reported = false;
|
|
2391
|
+
process.stderr.on("error", () => {
|
|
2392
|
+
stderrOpen = false;
|
|
2393
|
+
});
|
|
2394
|
+
function note(line) {
|
|
2395
|
+
if (!stderrOpen) return;
|
|
2396
|
+
try {
|
|
2397
|
+
process.stderr.write(line);
|
|
2398
|
+
} catch {
|
|
2399
|
+
stderrOpen = false;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
function reportOutputClosed() {
|
|
2403
|
+
if (reported) return;
|
|
2404
|
+
reported = true;
|
|
2405
|
+
note(
|
|
2406
|
+
`cosyte: ${CLI_CODES.CLI_OUTPUT_WRITE_FAILED}: could not write to the output stream; it closed before the output was delivered
|
|
2407
|
+
`
|
|
2408
|
+
);
|
|
2409
|
+
process.exitCode = EXIT.IOERR;
|
|
2410
|
+
}
|
|
2411
|
+
process.stdout.on("error", () => {
|
|
2412
|
+
stdoutOpen = false;
|
|
2413
|
+
if (!serverMode) return;
|
|
2414
|
+
reportOutputClosed();
|
|
2415
|
+
process.stdin.destroy();
|
|
2416
|
+
});
|
|
2417
|
+
var produced = false;
|
|
2418
|
+
var delivered = true;
|
|
2419
|
+
var queued = 0;
|
|
2420
|
+
var onFlushed;
|
|
2421
|
+
function emit(chunk) {
|
|
2422
|
+
produced = true;
|
|
2423
|
+
if (!stdoutOpen) {
|
|
2424
|
+
delivered = false;
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
queued += 1;
|
|
2428
|
+
process.stdout.write(chunk, (err) => {
|
|
2429
|
+
if (err !== null && err !== void 0) delivered = false;
|
|
2430
|
+
queued -= 1;
|
|
2431
|
+
if (queued > 0 || onFlushed === void 0) return;
|
|
2432
|
+
const resume = onFlushed;
|
|
2433
|
+
onFlushed = void 0;
|
|
2434
|
+
resume();
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
function flushed() {
|
|
2438
|
+
if (queued === 0) return Promise.resolve();
|
|
2439
|
+
return new Promise((resolve2) => {
|
|
2440
|
+
onFlushed = resolve2;
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
1771
2443
|
var deps = {
|
|
1772
2444
|
readFile: (path) => readFileBytes(path),
|
|
1773
|
-
readStdin: () => readStreamBytes(process.stdin)
|
|
2445
|
+
readStdin: () => readStreamBytes(process.stdin),
|
|
2446
|
+
openFile: (path) => fileChunks(path),
|
|
2447
|
+
openStdin: () => streamChunks(process.stdin),
|
|
2448
|
+
writeStdout: (chunk) => {
|
|
2449
|
+
if (!stdoutOpen) throw new Error("stdout closed");
|
|
2450
|
+
emit(chunk);
|
|
2451
|
+
}
|
|
1774
2452
|
};
|
|
1775
|
-
if (
|
|
2453
|
+
if (serverMode) {
|
|
1776
2454
|
Promise.resolve().then(() => (init_server(), server_exports)).then(({ startStdioServer: startStdioServer2 }) => startStdioServer2()).catch(() => {
|
|
1777
|
-
|
|
2455
|
+
note("cosyte: CLI_INTERNAL: the MCP server failed to start\n");
|
|
1778
2456
|
process.exitCode = 70;
|
|
1779
2457
|
});
|
|
1780
2458
|
} else {
|
|
1781
|
-
run(process.argv.slice(2), deps).then((result) => {
|
|
1782
|
-
if (result.stdout)
|
|
1783
|
-
if (
|
|
2459
|
+
run(process.argv.slice(2), deps).then(async (result) => {
|
|
2460
|
+
if (result.stdout !== "") emit(result.stdout);
|
|
2461
|
+
if (produced) process.exitCode = EXIT.IOERR;
|
|
2462
|
+
await flushed();
|
|
2463
|
+
if (produced && !(delivered && stdoutOpen)) {
|
|
2464
|
+
reportOutputClosed();
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
if (result.stderr) note(result.stderr);
|
|
1784
2468
|
process.exitCode = result.exit;
|
|
1785
2469
|
}).catch(() => {
|
|
1786
|
-
|
|
2470
|
+
note("cosyte: CLI_INTERNAL: an unexpected internal error occurred\n");
|
|
1787
2471
|
process.exitCode = 70;
|
|
1788
2472
|
});
|
|
1789
2473
|
}
|