@cueai/omni-reader-mcp 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +115 -26
  2. package/dist/artifact-store.d.ts +11 -0
  3. package/dist/artifact-store.js +94 -48
  4. package/dist/cli/agent-config.d.ts +29 -4
  5. package/dist/cli/agent-config.js +910 -107
  6. package/dist/cli/arguments.d.ts +32 -0
  7. package/dist/cli/arguments.js +120 -0
  8. package/dist/cli/doctor.d.ts +42 -1
  9. package/dist/cli/doctor.js +110 -37
  10. package/dist/cli/setup.d.ts +3 -0
  11. package/dist/cli/setup.js +103 -18
  12. package/dist/cli/uninstall.d.ts +6 -0
  13. package/dist/cli/uninstall.js +37 -0
  14. package/dist/constants.d.ts +8 -1
  15. package/dist/constants.js +8 -1
  16. package/dist/cube-client.d.ts +5 -3
  17. package/dist/cube-client.js +17 -12
  18. package/dist/cursor.js +2 -0
  19. package/dist/errors.d.ts +32 -1
  20. package/dist/errors.js +26 -1
  21. package/dist/iiis-client.d.ts +19 -5
  22. package/dist/iiis-client.js +206 -49
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.js +93 -32
  25. package/dist/multipart-body.js +2 -0
  26. package/dist/onboarding-policy.d.ts +10 -0
  27. package/dist/onboarding-policy.js +58 -0
  28. package/dist/operation-journal.d.ts +50 -1
  29. package/dist/operation-journal.js +473 -114
  30. package/dist/operation-manager.d.ts +75 -0
  31. package/dist/operation-manager.js +1311 -0
  32. package/dist/path-security.d.ts +1 -0
  33. package/dist/path-security.js +26 -6
  34. package/dist/progress.d.ts +6 -1
  35. package/dist/protocol.d.ts +26 -13
  36. package/dist/protocol.js +34 -10
  37. package/dist/remote-client.d.ts +17 -0
  38. package/dist/remote-client.js +233 -0
  39. package/dist/result-contract.d.ts +199 -0
  40. package/dist/result-contract.js +235 -0
  41. package/dist/server.js +21 -4
  42. package/dist/source.d.ts +8 -0
  43. package/dist/source.js +37 -0
  44. package/dist/task-runtime.d.ts +13 -0
  45. package/dist/task-runtime.js +94 -0
  46. package/dist/tools.d.ts +19 -1
  47. package/dist/tools.js +317 -112
  48. package/package.json +3 -3
@@ -1,8 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
- import { GRANTED_STREAM_PROTOCOL_VERSION, RESULT_CHUNK_MAX_BYTES, } from "./constants.js";
2
+ import { DEFAULT_IIIS_GRANTED_BASE_URL, GRANTED_STREAM_PROTOCOL_VERSION, RESULT_CHUNK_MAX_BYTES, } from "./constants.js";
3
3
  import { OmniBridgeError } from "./errors.js";
4
4
  import { createMultipartBody } from "./multipart-body.js";
5
- import { NOOP_PROGRESS } from "./progress.js";
5
+ import { NOOP_PROGRESS, } from "./progress.js";
6
6
  class TransportFailure extends Error {
7
7
  }
8
8
  const ACTIVE_STATUSES = new Set(["CLAIMED", "UPLOADING", "PROCESSING", "SETTLING"]);
@@ -14,6 +14,15 @@ const TERMINAL_STATUSES = new Set([
14
14
  "DELIVERY_EXPIRED",
15
15
  "DELIVERED",
16
16
  ]);
17
+ const OPERATION_STATUSES = new Set([
18
+ "ISSUED",
19
+ "CLAIMED",
20
+ "UPLOADING",
21
+ "PROCESSING",
22
+ "SETTLING",
23
+ "RELEASED",
24
+ ...TERMINAL_STATUSES,
25
+ ]);
17
26
  const RECOVERABLE_SETTLEMENT_ERRORS = new Set([
18
27
  "SETTLEMENT_IN_PROGRESS",
19
28
  "SETTLEMENT_RETRYABLE",
@@ -24,11 +33,24 @@ const RECOVERABLE_SETTLEMENT_ERRORS = new Set([
24
33
  ]);
25
34
  const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
26
35
  const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
36
+ const ERROR_CODE_PATTERN = /^[A-Z][A-Z0-9_]{0,127}$/;
37
+ const RFC3339_PATTERN = /^(\d{4})-(\d{2})-(\d{2})[Tt](?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/u;
38
+ const PROGRESS_UNITS = new Set([
39
+ "page",
40
+ "sheet",
41
+ "slide",
42
+ "frame",
43
+ "segment",
44
+ ]);
27
45
  function bridgeError(code, message, facts = {}) {
28
46
  return new OmniBridgeError({
29
47
  code,
30
48
  message,
49
+ operationCreated: facts.operationCreated ?? true,
31
50
  fileUploaded: facts.fileUploaded ?? false,
51
+ parserStarted: (facts.parserStarted ?? false) ||
52
+ facts.billed === true ||
53
+ facts.contentReleased === true,
32
54
  billed: facts.billed ?? false,
33
55
  contentReleased: facts.contentReleased ?? false,
34
56
  retryable: facts.retryable ?? false,
@@ -66,11 +88,17 @@ function throwIfCanceled(input) {
66
88
  if (input.signal?.aborted)
67
89
  throw canceledAfterUpload();
68
90
  }
69
- function operationUrl(input, suffix = "") {
70
- const base = new URL(input.uploadUrl);
71
- if (base.protocol !== "https:" || base.username !== "" || base.password !== "") {
72
- throw bridgeError("INSECURE_IIIS_URL", "The IIIS upload endpoint must use credential-free HTTPS.");
91
+ function secureIiisUrl(value) {
92
+ const url = new URL(value);
93
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "") {
94
+ throw bridgeError("INSECURE_IIIS_URL", "The IIIS endpoint must use credential-free HTTPS.");
73
95
  }
96
+ return url;
97
+ }
98
+ function operationUrl(input, operationBaseUrl, suffix = "") {
99
+ const base = input.uploadUrl === undefined
100
+ ? secureIiisUrl(operationBaseUrl)
101
+ : secureIiisUrl(input.uploadUrl);
74
102
  return new URL(`operations/${encodeURIComponent(input.operationId)}${suffix}`, base).toString();
75
103
  }
76
104
  function operationHeaders(input) {
@@ -84,37 +112,108 @@ function safeInteger(value, minimum = 0) {
84
112
  && Number.isSafeInteger(value)
85
113
  && value >= minimum;
86
114
  }
115
+ function isRfc3339(value) {
116
+ if (typeof value !== "string")
117
+ return false;
118
+ const match = RFC3339_PATTERN.exec(value);
119
+ if (match === null)
120
+ return false;
121
+ const year = Number(match[1]);
122
+ const month = Number(match[2]);
123
+ const day = Number(match[3]);
124
+ if (month < 1 || month > 12 || day < 1)
125
+ return false;
126
+ const monthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
127
+ const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
128
+ const days = month === 2 && leapYear ? 29 : monthDays[month - 1];
129
+ return day <= days;
130
+ }
87
131
  function validateProtocol(event) {
88
132
  if (event.protocol_version !== GRANTED_STREAM_PROTOCOL_VERSION) {
89
133
  throw bridgeError("PROTOCOL_MISMATCH", "IIIS returned an unsupported protocol version.");
90
134
  }
91
135
  }
136
+ function stableErrorCode(value, fallback) {
137
+ return typeof value === "string" && ERROR_CODE_PATTERN.test(value)
138
+ ? value
139
+ : fallback;
140
+ }
141
+ function stableErrorMessage(code) {
142
+ if (code === "SETTLEMENT_DENIED")
143
+ return "Billing did not authorize content release.";
144
+ if (code === "DELIVERY_EXPIRED")
145
+ return "The temporary delivery result has expired.";
146
+ if (code === "CLEANUP_PENDING")
147
+ return "Omni cleanup is still pending.";
148
+ if (code === "CANCELED")
149
+ return "The Omni operation was canceled.";
150
+ return "The Omni parsing service could not complete the operation.";
151
+ }
152
+ function stableProgressMessage(stage) {
153
+ if (stage === "parse" || stage === "parsing")
154
+ return "Parsing document";
155
+ if (stage === "settling" || stage === "settlement")
156
+ return "Finalizing result";
157
+ return "Processing document";
158
+ }
92
159
  function progressValue(event) {
93
160
  validateProtocol(event);
94
- if (!safeInteger(event.done) || !safeInteger(event.total)) {
161
+ if (!safeInteger(event.done) ||
162
+ !safeInteger(event.total) ||
163
+ event.done > event.total) {
95
164
  throw bridgeError("INVALID_PROGRESS_EVENT", "IIIS returned an invalid progress event.");
96
165
  }
166
+ const unit = typeof event.unit === "string" &&
167
+ PROGRESS_UNITS.has(event.unit)
168
+ ? event.unit
169
+ : undefined;
97
170
  return {
98
171
  done: event.done,
99
172
  total: event.total,
100
- message: String(event.message ?? event.stage ?? "Parsing"),
173
+ message: stableProgressMessage(event.stage),
174
+ ...(unit === undefined || event.total <= 0
175
+ ? {}
176
+ : {
177
+ detail: {
178
+ unit,
179
+ completed: Math.min(event.done, event.total),
180
+ total: event.total,
181
+ },
182
+ }),
101
183
  };
102
184
  }
103
185
  function monotonicProgress(sink) {
104
186
  let last = 0;
187
+ let lastDetail;
105
188
  return {
106
- async report(progress, total, message) {
189
+ async report(progress, total, message, detail) {
107
190
  const next = Math.min(total, Math.max(last, progress));
108
191
  last = next;
109
- await sink.report(next, total, message);
192
+ const nextDetail = detail === undefined
193
+ ? undefined
194
+ : lastDetail?.unit === detail.unit
195
+ ? {
196
+ ...detail,
197
+ completed: Math.min(detail.total, Math.max(lastDetail.completed, detail.completed)),
198
+ }
199
+ : detail;
200
+ if (nextDetail !== undefined)
201
+ lastDetail = nextDetail;
202
+ await sink.report(next, total, message, nextDetail);
110
203
  },
111
204
  };
112
205
  }
206
+ function parserProgress(done, total) {
207
+ return total > 0
208
+ ? 40 + Math.floor(Math.min(done / total, 1) * 59)
209
+ : 40;
210
+ }
113
211
  async function errorFromResponse(response, input) {
114
212
  try {
115
213
  const body = await response.json();
116
- if (isRecord(body) && typeof body.code === "string" && typeof body.message === "string") {
117
- return bridgeError(body.code, body.message, {
214
+ if (isRecord(body) && typeof body.code === "string") {
215
+ const code = stableErrorCode(body.code, "IIIS_UNAVAILABLE");
216
+ return bridgeError(code, stableErrorMessage(code), {
118
217
  fileUploaded: body.file_uploaded === true,
119
218
  billed: body.billed === true,
120
219
  contentReleased: body.content_released === true,
@@ -139,6 +238,33 @@ async function protocolJson(response, input, description) {
139
238
  throw bridgeError("PROTOCOL_MISMATCH", `IIIS returned invalid ${description} JSON.`);
140
239
  }
141
240
  }
241
+ function operationStatusValue(value, operationId) {
242
+ if (!isRecord(value)
243
+ || value.protocol_version !== GRANTED_STREAM_PROTOCOL_VERSION
244
+ || value.operation_id !== operationId
245
+ || typeof value.status !== "string"
246
+ || !OPERATION_STATUSES.has(value.status)
247
+ || typeof value.parser_started !== "boolean"
248
+ || typeof value.file_uploaded !== "boolean"
249
+ || typeof value.billed !== "boolean"
250
+ || typeof value.content_released !== "boolean"
251
+ || typeof value.retryable !== "boolean"
252
+ || !(value.expires_at === null || isRfc3339(value.expires_at))) {
253
+ throw bridgeError("PROTOCOL_MISMATCH", "IIIS returned an invalid operation status.");
254
+ }
255
+ return value;
256
+ }
257
+ function operationSnapshot(status) {
258
+ return {
259
+ status: status.status,
260
+ parserStarted: status.parser_started,
261
+ fileUploaded: status.file_uploaded,
262
+ billed: status.billed,
263
+ contentReleased: status.content_released,
264
+ retryable: status.retryable,
265
+ expiresAt: status.expires_at,
266
+ };
267
+ }
142
268
  async function* sseEvents(response) {
143
269
  if (response.body === null)
144
270
  throw new TransportFailure("missing SSE body");
@@ -193,10 +319,17 @@ export class IiisClient {
193
319
  #fetch;
194
320
  #pollIntervalMs;
195
321
  #maxPolls;
322
+ #operationBaseUrl;
196
323
  constructor(options = {}) {
197
324
  this.#fetch = options.fetchImpl ?? fetch;
198
325
  this.#pollIntervalMs = Math.max(0, options.pollIntervalMs ?? 250);
199
326
  this.#maxPolls = Math.max(1, options.maxPolls ?? 240);
327
+ const operationBase = secureIiisUrl(options.operationBaseUrl ?? DEFAULT_IIIS_GRANTED_BASE_URL);
328
+ operationBase.search = "";
329
+ operationBase.hash = "";
330
+ if (!operationBase.pathname.endsWith("/"))
331
+ operationBase.pathname += "/";
332
+ this.#operationBaseUrl = operationBase.toString();
200
333
  }
201
334
  async uploadAndWait(input) {
202
335
  const progress = monotonicProgress(input.progress ?? NOOP_PROGRESS);
@@ -217,7 +350,35 @@ export class IiisClient {
217
350
  return this.#recover(input, true, progress);
218
351
  }
219
352
  }
220
- async downloadResult(input) {
353
+ async recoverAndWait(input) {
354
+ return this.#recover(input, false, monotonicProgress(input.progress ?? NOOP_PROGRESS));
355
+ }
356
+ async inspectOperation(input) {
357
+ return operationSnapshot(await this.#status({ ...input, signal: undefined }));
358
+ }
359
+ async cancelOperation(input) {
360
+ throwIfCanceled(input);
361
+ let response;
362
+ try {
363
+ response = await this.#fetch(operationUrl(input, this.#operationBaseUrl, "/cancel"), {
364
+ method: "POST",
365
+ headers: operationHeaders(input),
366
+ signal: input.signal,
367
+ });
368
+ }
369
+ catch {
370
+ throwIfCanceled(input);
371
+ throw bridgeError("IIIS_UNAVAILABLE", "IIIS operation cancellation is unavailable.", { retryable: true });
372
+ }
373
+ if (!response.ok)
374
+ throw await errorFromResponse(response, input);
375
+ if (response.status === 202 || response.status === 204) {
376
+ return this.inspectOperation(input);
377
+ }
378
+ const value = await protocolJson(response, input, "operation cancellation");
379
+ return operationSnapshot(operationStatusValue(value, input.operationId));
380
+ }
381
+ async downloadResult(input, progress = NOOP_PROGRESS) {
221
382
  throwIfCanceled(input);
222
383
  const confirmedFacts = recoveryFacts({
223
384
  fileUploaded: true,
@@ -244,7 +405,7 @@ export class IiisClient {
244
405
  }
245
406
  let response;
246
407
  try {
247
- response = await this.#fetch(operationUrl(input, "/result"), {
408
+ response = await this.#fetch(operationUrl(input, this.#operationBaseUrl, "/result"), {
248
409
  headers: operationHeaders(input),
249
410
  signal: input.signal,
250
411
  });
@@ -316,6 +477,7 @@ export class IiisClient {
316
477
  };
317
478
  await input.retention.complete(metadata);
318
479
  throwIfCanceled(input);
480
+ await progress.report(100, 100, "Completed");
319
481
  completed = true;
320
482
  return metadata;
321
483
  }
@@ -341,7 +503,7 @@ export class IiisClient {
341
503
  throwIfCanceled(input);
342
504
  let response;
343
505
  try {
344
- response = await this.#fetch(operationUrl(input, "/ack"), {
506
+ response = await this.#fetch(operationUrl(input, this.#operationBaseUrl, "/ack"), {
345
507
  method: "POST",
346
508
  headers: operationHeaders(input),
347
509
  signal: input.signal,
@@ -360,21 +522,27 @@ export class IiisClient {
360
522
  throw await errorFromResponse(response, input);
361
523
  }
362
524
  async #uploadOnce(input, progress) {
363
- const body = createMultipartBody(input.openedFile, {
525
+ const openedFile = input.openedFile;
526
+ const parseGrant = input.parseGrant;
527
+ const uploadUrl = input.uploadUrl;
528
+ if (openedFile === undefined || parseGrant === undefined || uploadUrl === undefined) {
529
+ throw bridgeError("UPLOAD_CONTEXT_UNAVAILABLE", "The local upload context is unavailable for this recovery operation.", { retryable: true });
530
+ }
531
+ const body = createMultipartBody(openedFile, {
364
532
  signal: input.signal,
365
533
  onProgress: async (bytes) => {
366
- const scaled = input.openedFile.size === 0
534
+ const scaled = openedFile.size === 0
367
535
  ? 40
368
- : Math.floor(bytes * 40 / input.openedFile.size);
369
- await progress.report(scaled, 100, `Uploading ${bytes}/${input.openedFile.size} bytes`);
536
+ : Math.floor(bytes * 40 / openedFile.size);
537
+ await progress.report(scaled, 100, `Uploading ${bytes}/${openedFile.size} bytes`);
370
538
  },
371
539
  });
372
540
  let response;
373
541
  try {
374
- response = await this.#fetch(input.uploadUrl, {
542
+ response = await this.#fetch(uploadUrl, {
375
543
  method: "POST",
376
544
  headers: {
377
- authorization: `Bearer ${input.parseGrant}`,
545
+ authorization: `Bearer ${parseGrant}`,
378
546
  "content-type": body.contentType,
379
547
  "content-length": String(body.contentLength),
380
548
  },
@@ -415,14 +583,12 @@ export class IiisClient {
415
583
  }
416
584
  if (event.type === "progress") {
417
585
  const parsed = progressValue(event);
418
- const scaled = parsed.total > 0
419
- ? 40 + Math.floor(Math.min(parsed.done / parsed.total, 1) * 60)
420
- : 40;
421
- await progress.report(scaled, 100, parsed.message);
586
+ await progress.report(parserProgress(parsed.done, parsed.total), 100, parsed.message, parsed.detail);
422
587
  continue;
423
588
  }
424
589
  if (event.type === "error") {
425
- throw bridgeError(String(event.code ?? "IIIS_PARSE_FAILED"), String(event.message ?? "IIIS parsing failed."), {
590
+ const code = stableErrorCode(event.code, "IIIS_PARSE_FAILED");
591
+ throw bridgeError(code, stableErrorMessage(code), {
426
592
  fileUploaded: event.file_uploaded === true,
427
593
  billed: event.billed === true,
428
594
  contentReleased: event.content_released === true,
@@ -519,8 +685,11 @@ export class IiisClient {
519
685
  }
520
686
  throw bridgeError("INVALID_SSE_EVENT", "IIIS returned an unsupported progress event.");
521
687
  }
522
- if (completedMetadata !== undefined)
688
+ if (completedMetadata !== undefined) {
689
+ throwIfCanceled(input);
690
+ await progress.report(100, 100, "Completed");
523
691
  return completedMetadata;
692
+ }
524
693
  throw new TransportFailure("SSE ended before completion");
525
694
  }
526
695
  catch (error) {
@@ -549,7 +718,7 @@ export class IiisClient {
549
718
  throwIfCanceled(input);
550
719
  let response;
551
720
  try {
552
- response = await this.#fetch(operationUrl(input), {
721
+ response = await this.#fetch(operationUrl(input, this.#operationBaseUrl), {
553
722
  headers: operationHeaders(input),
554
723
  signal: input.signal,
555
724
  });
@@ -561,25 +730,13 @@ export class IiisClient {
561
730
  if (!response.ok)
562
731
  throw await errorFromResponse(response, input);
563
732
  const value = await protocolJson(response, input, "operation status");
564
- if (!isRecord(value)
565
- || value.protocol_version !== GRANTED_STREAM_PROTOCOL_VERSION
566
- || value.operation_id !== input.operationId
567
- || typeof value.status !== "string"
568
- || typeof value.parser_started !== "boolean"
569
- || typeof value.file_uploaded !== "boolean"
570
- || typeof value.billed !== "boolean"
571
- || typeof value.content_released !== "boolean"
572
- || typeof value.retryable !== "boolean"
573
- || !(value.expires_at === null || typeof value.expires_at === "string")) {
574
- throw bridgeError("PROTOCOL_MISMATCH", "IIIS returned an invalid operation status.");
575
- }
576
- return value;
733
+ return operationStatusValue(value, input.operationId);
577
734
  }
578
735
  async #events(input, afterSequence) {
579
736
  throwIfCanceled(input);
580
737
  let response;
581
738
  try {
582
- response = await this.#fetch(operationUrl(input, `/events?after_sequence=${afterSequence}`), { headers: operationHeaders(input), signal: input.signal });
739
+ response = await this.#fetch(operationUrl(input, this.#operationBaseUrl, `/events?after_sequence=${afterSequence}`), { headers: operationHeaders(input), signal: input.signal });
583
740
  }
584
741
  catch {
585
742
  throwIfCanceled(input);
@@ -631,7 +788,10 @@ export class IiisClient {
631
788
  await progress.report(45, 100, "Parser started");
632
789
  parserStartedReported = true;
633
790
  }
634
- if (status.status === "ISSUED" && mayRetryUpload && Date.parse(input.expiresAt) > Date.now()) {
791
+ if (status.status === "ISSUED" &&
792
+ mayRetryUpload &&
793
+ input.expiresAt !== undefined &&
794
+ Date.parse(input.expiresAt) > Date.now()) {
635
795
  try {
636
796
  return await this.#uploadOnce(input, progress);
637
797
  }
@@ -643,21 +803,18 @@ export class IiisClient {
643
803
  }
644
804
  }
645
805
  else if (status.status === "RELEASED") {
646
- return await this.downloadResult(input);
806
+ return await this.downloadResult(input, progress);
647
807
  }
648
808
  else if (ACTIVE_STATUSES.has(status.status)) {
649
809
  const page = await this.#events(input, eventCursor);
650
810
  for (const event of page.events) {
651
811
  const parsed = progressValue(event);
652
- const scaled = parsed.total > 0
653
- ? 40 + Math.floor(Math.min(parsed.done / parsed.total, 1) * 60)
654
- : 40;
655
- await progress.report(scaled, 100, parsed.message);
812
+ await progress.report(parserProgress(parsed.done, parsed.total), 100, parsed.message, parsed.detail);
656
813
  }
657
814
  eventCursor = page.nextSequence;
658
815
  }
659
816
  else if (TERMINAL_STATUSES.has(status.status)) {
660
- throw bridgeError(status.status, `The IIIS operation ended with status ${status.status}.`, {
817
+ throw bridgeError(status.status, stableErrorMessage(status.status), {
661
818
  fileUploaded: status.file_uploaded,
662
819
  billed: status.billed,
663
820
  contentReleased: status.content_released,
package/dist/index.d.ts CHANGED
@@ -8,10 +8,13 @@ export interface RunCliOptions {
8
8
  readonly cwd?: string;
9
9
  readonly artifactRoot?: string;
10
10
  readonly platform?: NodeJS.Platform;
11
+ readonly temporaryDirectory?: string;
12
+ readonly stdinIsTTY?: boolean;
11
13
  readonly now?: () => Date;
12
14
  readonly npmVersion?: string;
13
15
  readonly packageVersion?: string;
14
16
  readonly fetchImpl?: typeof fetch;
17
+ readonly iiisOperationBaseUrl?: string;
15
18
  readonly stdout?: WritableLike;
16
19
  readonly stderr?: WritableLike;
17
20
  readonly ask?: (question: string) => Promise<string>;
package/dist/index.js CHANGED
@@ -1,27 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFile as execFileCallback } from "node:child_process";
3
3
  import { readFile, realpath } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { createInterface } from "node:readline/promises";
6
7
  import { stdin, stdout as processStdout } from "node:process";
7
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
- import { ArtifactStore, defaultArtifactRoot } from "./artifact-store.js";
9
+ import { ArtifactStore, selectArtifactRoot, } from "./artifact-store.js";
10
+ import { CliUsageError, parseCliArguments } from "./cli/arguments.js";
9
11
  import { runClean } from "./cli/clean.js";
10
- import { runDoctor } from "./cli/doctor.js";
12
+ import { renderDoctor, runDoctor } from "./cli/doctor.js";
11
13
  import { runSetup } from "./cli/setup.js";
14
+ import { runUninstall } from "./cli/uninstall.js";
12
15
  import { CubeGrantClient } from "./cube-client.js";
13
16
  import { IiisClient } from "./iiis-client.js";
17
+ import { createLocalParseOperationManager } from "./operation-manager.js";
14
18
  import { OperationJournal } from "./operation-journal.js";
15
19
  import { splitAllowedRoots } from "./path-security.js";
20
+ import { HttpRemoteOmniClient } from "./remote-client.js";
16
21
  import { createOmniMcpServer } from "./server.js";
17
22
  function helpText() {
18
23
  return [
19
- "Usage: omni-reader-mcp [setup|doctor|clean|--help|--version]",
24
+ "Usage: omni-reader-mcp [setup|doctor|clean|uninstall|--help|--version]",
20
25
  "",
21
26
  "No arguments start the stdio MCP server.",
22
27
  "setup Configure a supported user-scope Agent",
23
28
  "doctor Check local configuration and protocol health",
24
29
  "clean Delete Bridge-created local artifacts and expired records",
30
+ "uninstall Restore a trusted URL-only Agent entry without deleting artifacts",
25
31
  "",
26
32
  ].join("\n");
27
33
  }
@@ -60,16 +66,26 @@ async function installedNpmVersion(env) {
60
66
  });
61
67
  });
62
68
  }
69
+ async function artifactSelectionFor(options, env, homeDirectory, cwd, platform) {
70
+ if (options.artifactRoot !== undefined) {
71
+ return { mode: "default", rootDirectory: options.artifactRoot };
72
+ }
73
+ return await selectArtifactRoot({
74
+ platform,
75
+ homeDirectory,
76
+ env,
77
+ projectDirectory: cwd,
78
+ temporaryDirectory: options.temporaryDirectory ?? tmpdir(),
79
+ });
80
+ }
63
81
  export async function startStdioServer(options = {}) {
64
82
  const env = options.env ?? process.env;
83
+ const homeDirectory = options.homeDirectory ?? (await import("node:os")).homedir();
65
84
  const cwd = options.cwd ?? process.cwd();
66
- const artifactRoot = options.artifactRoot ?? defaultArtifactRoot({
67
- platform: options.platform,
68
- homeDirectory: options.homeDirectory,
69
- env,
70
- });
85
+ const platform = options.platform ?? process.platform;
86
+ const artifactSelection = await artifactSelectionFor(options, env, homeDirectory, cwd, platform);
71
87
  const artifactStore = await ArtifactStore.open({
72
- rootDirectory: artifactRoot,
88
+ rootDirectory: artifactSelection.rootDirectory,
73
89
  projectDirectory: cwd,
74
90
  now: options.now,
75
91
  });
@@ -79,12 +95,33 @@ export async function startStdioServer(options = {}) {
79
95
  environment: { CUE_API_KEY: env.CUE_API_KEY },
80
96
  fetchImpl: options.fetchImpl,
81
97
  });
98
+ const iiisClient = new IiisClient({
99
+ fetchImpl: options.fetchImpl,
100
+ operationBaseUrl: options.iiisOperationBaseUrl,
101
+ });
102
+ const extraRoots = splitAllowedRoots(env.OMNI_ALLOWED_ROOTS);
103
+ const remoteClient = new HttpRemoteOmniClient({
104
+ apiKey: env.CUE_API_KEY,
105
+ fetchImpl: options.fetchImpl,
106
+ });
107
+ const operationManager = createLocalParseOperationManager({
108
+ journal,
109
+ workspace: cwd,
110
+ extraRoots,
111
+ cubeClient,
112
+ iiisClient,
113
+ artifactStore,
114
+ remoteClient,
115
+ now: options.now,
116
+ });
82
117
  const server = createOmniMcpServer({
83
118
  workspace: cwd,
84
- extraRoots: splitAllowedRoots(env.OMNI_ALLOWED_ROOTS),
119
+ extraRoots,
85
120
  cubeClient,
86
- iiisClient: new IiisClient({ fetchImpl: options.fetchImpl }),
121
+ iiisClient,
87
122
  artifactStore,
123
+ operationManager,
124
+ remoteClient,
88
125
  });
89
126
  const close = async () => {
90
127
  await server.close().catch(() => undefined);
@@ -99,7 +136,6 @@ export async function runCli(args, options = {}) {
99
136
  const homeDirectory = options.homeDirectory ?? (await import("node:os")).homedir();
100
137
  const cwd = options.cwd ?? process.cwd();
101
138
  const platform = options.platform ?? process.platform;
102
- const artifactRoot = options.artifactRoot ?? defaultArtifactRoot({ platform, homeDirectory, env });
103
139
  const fetchImpl = options.fetchImpl ?? fetch;
104
140
  const output = options.stdout ?? process.stdout;
105
141
  const errorOutput = options.stderr ?? process.stderr;
@@ -107,57 +143,82 @@ export async function runCli(args, options = {}) {
107
143
  const write = (text) => { output.write(text); };
108
144
  const packageVersion = options.packageVersion ?? await installedPackageVersion();
109
145
  try {
110
- if (args.length === 0) {
146
+ const parsed = parseCliArguments(args, platform);
147
+ if (parsed.command === "server") {
111
148
  await (options.startServer ?? (() => startStdioServer({
112
149
  ...options,
113
150
  env,
114
151
  homeDirectory,
115
152
  cwd,
116
153
  platform,
117
- artifactRoot,
118
154
  fetchImpl,
119
155
  })))();
120
156
  return 0;
121
157
  }
122
- const command = args[0];
123
- if (command === "--help" || command === "-h" || command === "help") {
158
+ if (parsed.command === "help") {
124
159
  write(helpText());
125
160
  return 0;
126
161
  }
127
- if (command === "--version" || command === "-v") {
162
+ if (parsed.command === "version") {
128
163
  write(`${packageVersion}\n`);
129
164
  return 0;
130
165
  }
131
- if (command === "setup") {
132
- await runSetup({ env, homeDirectory, platform, fetchImpl, ask, write });
166
+ if (parsed.command === "setup") {
167
+ await runSetup({
168
+ env,
169
+ homeDirectory,
170
+ platform,
171
+ fetchImpl,
172
+ ask,
173
+ write,
174
+ stdinIsTTY: options.stdinIsTTY ?? stdin.isTTY === true,
175
+ arguments: parsed.arguments,
176
+ });
177
+ return 0;
178
+ }
179
+ if (parsed.command === "uninstall") {
180
+ await runUninstall({
181
+ args: [
182
+ ...(parsed.yes ? ["--yes"] : []),
183
+ ...(parsed.json ? ["--json"] : []),
184
+ ],
185
+ env,
186
+ homeDirectory,
187
+ platform,
188
+ write,
189
+ });
133
190
  return 0;
134
191
  }
135
- if (command === "doctor") {
136
- const lines = await runDoctor({
192
+ if (parsed.command === "doctor") {
193
+ const artifactSelection = await artifactSelectionFor(options, env, homeDirectory, cwd, platform);
194
+ const report = await runDoctor({
137
195
  env,
138
196
  homeDirectory,
139
197
  platform,
140
- artifactRoot,
198
+ artifactRoot: artifactSelection.rootDirectory,
199
+ cacheMode: artifactSelection.mode,
141
200
  fetchImpl,
142
201
  npmVersion: options.npmVersion ?? await installedNpmVersion(env),
143
202
  packageVersion,
144
203
  });
145
- write(`${lines.join("\n")}\n`);
146
- return 0;
147
- }
148
- if (command === "clean") {
149
- const result = await runClean({ artifactRoot, projectDirectory: cwd, now: options.now });
150
- write(`Removed ${result.removed} Bridge cache item(s).\n`);
204
+ write(parsed.json
205
+ ? `${JSON.stringify(report)}\n`
206
+ : `${renderDoctor(report).join("\n")}\n`);
151
207
  return 0;
152
208
  }
153
- errorOutput.write(`Unknown command: ${command}\n`);
154
- errorOutput.write(helpText());
155
- return 1;
209
+ const artifactSelection = await artifactSelectionFor(options, env, homeDirectory, cwd, platform);
210
+ const result = await runClean({
211
+ artifactRoot: artifactSelection.rootDirectory,
212
+ projectDirectory: cwd,
213
+ now: options.now,
214
+ });
215
+ write(`Removed ${result.removed} Bridge cache item(s).\n`);
216
+ return 0;
156
217
  }
157
218
  catch (error) {
158
219
  const message = error instanceof Error ? error.message : "The Bridge command failed.";
159
220
  errorOutput.write(`Error: ${message}\n`);
160
- return 1;
221
+ return error instanceof CliUsageError ? error.exitCode : 1;
161
222
  }
162
223
  }
163
224
  async function main() {
@@ -4,7 +4,9 @@ function bridgeError(code, message, retryable) {
4
4
  return new OmniBridgeError({
5
5
  code,
6
6
  message,
7
+ operationCreated: true,
7
8
  fileUploaded: false,
9
+ parserStarted: false,
8
10
  billed: false,
9
11
  contentReleased: false,
10
12
  retryable,