@cueai/omni-reader-mcp 1.5.5 → 1.7.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 (42) hide show
  1. package/README.md +106 -46
  2. package/dist/artifact-store.d.ts +2 -0
  3. package/dist/artifact-store.js +45 -10
  4. package/dist/capabilities.d.ts +129 -2
  5. package/dist/capabilities.js +122 -19
  6. package/dist/cli/agent-config.js +2 -2
  7. package/dist/cli/arguments.d.ts +8 -4
  8. package/dist/cli/arguments.js +62 -7
  9. package/dist/cli/config-inspection.d.ts +59 -0
  10. package/dist/cli/config-inspection.js +307 -0
  11. package/dist/cli/doctor.d.ts +7 -0
  12. package/dist/cli/doctor.js +37 -2
  13. package/dist/cli/setup.js +13 -2
  14. package/dist/constants.d.ts +6 -1
  15. package/dist/constants.js +9 -4
  16. package/dist/cube-client.d.ts +4 -1
  17. package/dist/cube-client.js +286 -32
  18. package/dist/cursor.d.ts +4 -0
  19. package/dist/cursor.js +11 -13
  20. package/dist/errors.d.ts +1 -0
  21. package/dist/errors.js +15 -0
  22. package/dist/iiis-client.d.ts +33 -2
  23. package/dist/iiis-client.js +368 -40
  24. package/dist/index.js +10 -1
  25. package/dist/operation-journal.d.ts +17 -1
  26. package/dist/operation-journal.js +260 -15
  27. package/dist/operation-manager.d.ts +6 -2
  28. package/dist/operation-manager.js +448 -89
  29. package/dist/path-normalization.d.ts +5 -0
  30. package/dist/path-normalization.js +25 -0
  31. package/dist/path-security.d.ts +2 -1
  32. package/dist/path-security.js +49 -28
  33. package/dist/protocol.d.ts +10 -2
  34. package/dist/protocol.js +23 -7
  35. package/dist/remote-client.js +3 -13
  36. package/dist/result-contract.d.ts +76 -32
  37. package/dist/result-contract.js +121 -5
  38. package/dist/task-runtime.d.ts +3 -2
  39. package/dist/task-runtime.js +2 -2
  40. package/dist/tools.d.ts +4 -0
  41. package/dist/tools.js +41 -31
  42. package/package.json +1 -1
@@ -0,0 +1,5 @@
1
+ export declare class PathNormalizationError extends Error {
2
+ readonly code = "INVALID_WINDOWS_PATH";
3
+ constructor();
4
+ }
5
+ export declare function normalizePlatformPath(value: string, platform?: NodeJS.Platform): string;
@@ -0,0 +1,25 @@
1
+ import path from "node:path";
2
+ const MSYS_DRIVE_PATH = /^\/([A-Za-z])(?:\/|$)/u;
3
+ export class PathNormalizationError extends Error {
4
+ code = "INVALID_WINDOWS_PATH";
5
+ constructor() {
6
+ super("Use an absolute Windows drive path such as C:\\Reports or exact MSYS syntax such as /c/Reports.");
7
+ this.name = "PathNormalizationError";
8
+ }
9
+ }
10
+ export function normalizePlatformPath(value, platform = process.platform) {
11
+ if (platform !== "win32")
12
+ return value;
13
+ if (value.startsWith("//") || value.startsWith("\\\\"))
14
+ return value;
15
+ const match = MSYS_DRIVE_PATH.exec(value);
16
+ if (match !== null) {
17
+ const drive = match[1].toUpperCase();
18
+ const remainder = value.slice(match[0].length).replaceAll("/", "\\");
19
+ return path.win32.normalize(`${drive}:\\${remainder}`);
20
+ }
21
+ if (value.startsWith("/") || value.startsWith("\\")) {
22
+ throw new PathNormalizationError();
23
+ }
24
+ return value;
25
+ }
@@ -9,6 +9,7 @@ export interface OpenAllowedFileOptions {
9
9
  workspace: string;
10
10
  extraRoots?: readonly string[];
11
11
  homeDirectory?: string;
12
+ platform?: NodeJS.Platform;
12
13
  fileSystem?: PathSecurityFileSystem;
13
14
  }
14
15
  export interface OpenedAllowedFile {
@@ -19,5 +20,5 @@ export interface OpenedAllowedFile {
19
20
  createReadStream(): ReadStream;
20
21
  close(): Promise<void>;
21
22
  }
22
- export declare function splitAllowedRoots(value: string | undefined): string[];
23
+ export declare function splitAllowedRoots(value: string | undefined, platform?: NodeJS.Platform): string[];
23
24
  export declare function openAllowedFile(input: string, options: OpenAllowedFileOptions): Promise<OpenedAllowedFile>;
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
5
5
  import path from "node:path";
6
6
  import { MAX_FILE_BYTES } from "./constants.js";
7
7
  import { OmniBridgeError } from "./errors.js";
8
+ import { PathNormalizationError, normalizePlatformPath, } from "./path-normalization.js";
8
9
  const CONTENT_TYPES = {
9
10
  ".pdf": "application/pdf",
10
11
  ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@@ -92,29 +93,44 @@ function bridgeError(code, message, retryable, constraints) {
92
93
  ...(constraints === undefined ? {} : { constraints }),
93
94
  });
94
95
  }
96
+ function pathsFor(platform) {
97
+ return platform === "win32" ? path.win32 : path.posix;
98
+ }
95
99
  function validatePathText(value) {
96
100
  if (value.length === 0 || value.includes("\0")) {
97
101
  throw bridgeError("INVALID_LOCAL_PATH", "Provide one valid local file path.", false);
98
102
  }
99
103
  }
100
- function expandHomeOnly(value, homeDirectory) {
104
+ function normalizedPath(value, platform) {
105
+ validatePathText(value);
106
+ try {
107
+ return normalizePlatformPath(value, platform);
108
+ }
109
+ catch (error) {
110
+ if (error instanceof PathNormalizationError) {
111
+ throw bridgeError(error.code, error.message, false);
112
+ }
113
+ throw error;
114
+ }
115
+ }
116
+ function expandHomeOnly(value, homeDirectory, paths) {
101
117
  if (value === "~") {
102
118
  return homeDirectory;
103
119
  }
104
120
  if (/^~[\\/]/u.test(value)) {
105
- return path.join(homeDirectory, value.slice(2));
121
+ return paths.join(homeDirectory, value.slice(2));
106
122
  }
107
123
  return value;
108
124
  }
109
- function isInsideRoot(candidate, root) {
110
- const relative = path.relative(root, candidate);
125
+ function isInsideRoot(candidate, root, paths) {
126
+ const relative = paths.relative(root, candidate);
111
127
  return (relative === "" ||
112
128
  (relative !== ".." &&
113
- !relative.startsWith(`..${path.sep}`) &&
114
- !path.isAbsolute(relative)));
129
+ !relative.startsWith(`..${paths.sep}`) &&
130
+ !paths.isAbsolute(relative)));
115
131
  }
116
- function requireInsideAnyRoot(candidate, roots) {
117
- if (!roots.some((root) => isInsideRoot(candidate, root))) {
132
+ function requireInsideAnyRoot(candidate, roots, paths) {
133
+ if (!roots.some((root) => isInsideRoot(candidate, root, paths))) {
118
134
  throw bridgeError("PATH_NOT_ALLOWED", "Move the file into the current workspace or add its parent directory to OMNI_ALLOWED_ROOTS.", false);
119
135
  }
120
136
  }
@@ -148,8 +164,8 @@ function redactedSourceFingerprint(fileStat) {
148
164
  ].map((value) => value.toString()).join(":");
149
165
  return `sha256:${createHash("sha256").update(identity, "utf8").digest("hex")}`;
150
166
  }
151
- function safeFileType(resolvedPath) {
152
- const safeExtension = path.extname(resolvedPath).toLowerCase();
167
+ function safeFileType(resolvedPath, paths) {
168
+ const safeExtension = paths.extname(resolvedPath).toLowerCase();
153
169
  if (!/^\.[a-z0-9]{1,10}$/u.test(safeExtension)) {
154
170
  throw bridgeError("UNSUPPORTED_MEDIA_TYPE", "The source media type is not supported.", false, { supported_extensions: SUPPORTED_EXTENSIONS });
155
171
  }
@@ -175,19 +191,20 @@ function fileAccessError(error) {
175
191
  }
176
192
  return bridgeError("FILE_NOT_READABLE", "Omni cannot read the local file. Check its permissions and try again.", false);
177
193
  }
178
- async function resolveAllowedRoots(options, fileSystem, homeDirectory) {
179
- validatePathText(options.workspace);
180
- const workspaceInput = path.resolve(expandHomeOnly(options.workspace, homeDirectory));
194
+ async function resolveAllowedRoots(options, fileSystem, homeDirectory, platform, paths) {
195
+ const workspace = normalizedPath(options.workspace, platform);
196
+ const expandedWorkspace = expandHomeOnly(workspace, homeDirectory, paths);
197
+ const workspaceInput = paths.resolve(normalizedPath(expandedWorkspace, platform));
181
198
  const requestedRoots = [
182
199
  workspaceInput,
183
200
  ...(options.extraRoots ?? [])
184
201
  .filter((root) => root.length > 0)
185
202
  .map((root) => {
186
- validatePathText(root);
187
- const expanded = expandHomeOnly(root, homeDirectory);
188
- return path.isAbsolute(expanded)
203
+ const normalizedRoot = normalizedPath(root, platform);
204
+ const expanded = normalizedPath(expandHomeOnly(normalizedRoot, homeDirectory, paths), platform);
205
+ return paths.isAbsolute(expanded)
189
206
  ? expanded
190
- : path.resolve(workspaceInput, expanded);
207
+ : paths.resolve(workspaceInput, expanded);
191
208
  }),
192
209
  ];
193
210
  try {
@@ -199,7 +216,7 @@ async function resolveAllowedRoots(options, fileSystem, homeDirectory) {
199
216
  }
200
217
  return resolved;
201
218
  }));
202
- return [...new Set(roots)];
219
+ return { roots: [...new Set(roots)], workspaceInput };
203
220
  }
204
221
  catch (error) {
205
222
  if (error instanceof OmniBridgeError) {
@@ -208,21 +225,25 @@ async function resolveAllowedRoots(options, fileSystem, homeDirectory) {
208
225
  throw bridgeError("INVALID_ALLOWED_ROOT", "The workspace or an OMNI_ALLOWED_ROOTS entry is unavailable.", false);
209
226
  }
210
227
  }
211
- export function splitAllowedRoots(value) {
228
+ export function splitAllowedRoots(value, platform = process.platform) {
212
229
  if (value === undefined || value.length === 0) {
213
230
  return [];
214
231
  }
215
- return value.split(path.delimiter).filter((root) => root.length > 0);
232
+ return value
233
+ .split(pathsFor(platform).delimiter)
234
+ .filter((root) => root.length > 0);
216
235
  }
217
236
  export async function openAllowedFile(input, options) {
218
- validatePathText(input);
237
+ const platform = options.platform ?? process.platform;
238
+ const paths = pathsFor(platform);
219
239
  const fileSystem = options.fileSystem ?? NODE_FILE_SYSTEM;
220
240
  const homeDirectory = options.homeDirectory ?? homedir();
221
- const roots = await resolveAllowedRoots(options, fileSystem, homeDirectory);
222
- const expanded = expandHomeOnly(input, homeDirectory);
223
- const candidate = path.isAbsolute(expanded)
241
+ const normalizedInput = normalizedPath(input, platform);
242
+ const expanded = normalizedPath(expandHomeOnly(normalizedInput, homeDirectory, paths), platform);
243
+ const { roots, workspaceInput } = await resolveAllowedRoots(options, fileSystem, homeDirectory, platform, paths);
244
+ const candidate = paths.isAbsolute(expanded)
224
245
  ? expanded
225
- : path.resolve(options.workspace, expanded);
246
+ : paths.resolve(workspaceInput, expanded);
226
247
  let resolved;
227
248
  try {
228
249
  resolved = await fileSystem.realpath(candidate);
@@ -230,7 +251,7 @@ export async function openAllowedFile(input, options) {
230
251
  catch (error) {
231
252
  throw fileAccessError(error);
232
253
  }
233
- requireInsideAnyRoot(resolved, roots);
254
+ requireInsideAnyRoot(resolved, roots, paths);
234
255
  let handle;
235
256
  try {
236
257
  const pathStat = await fileSystem.stat(resolved);
@@ -244,11 +265,11 @@ export async function openAllowedFile(input, options) {
244
265
  requireSameFile(pathStat, before);
245
266
  const size = requireAllowedSize(before);
246
267
  const afterPath = await fileSystem.realpath(resolved);
247
- requireInsideAnyRoot(afterPath, roots);
268
+ requireInsideAnyRoot(afterPath, roots, paths);
248
269
  const after = await fileSystem.stat(afterPath);
249
270
  requireRegularFile(after);
250
271
  requireSameFile(before, after);
251
- const { safeExtension, contentType } = safeFileType(afterPath);
272
+ const { safeExtension, contentType } = safeFileType(afterPath, paths);
252
273
  const opened = new DescriptorBackedFile(handle, size, safeExtension, contentType, redactedSourceFingerprint(before));
253
274
  handle = undefined;
254
275
  return opened;
@@ -1,9 +1,10 @@
1
1
  import { z } from "zod";
2
- export declare const LOCAL_BRIDGE_PROTOCOL_VERSION = "omni.local_bridge_tools.v3";
3
- export declare const OPERATION_JOURNAL_VERSION = 3;
2
+ export declare const LOCAL_BRIDGE_PROTOCOL_VERSION = "omni.local_bridge_tools.v5";
3
+ export declare const OPERATION_JOURNAL_VERSION = 5;
4
4
  export declare const BUNDLE_CURSOR_VERSION = 2;
5
5
  export declare const RESULT_BUNDLE_PROTOCOL_VERSION = "omni.result_bundle.v1";
6
6
  export declare const GROUNDING_SCHEMA_VERSION = "omni.grounding.v1";
7
+ export type ResultDelivery = "auto" | "artifact";
7
8
  export interface RepresentationIntent {
8
9
  readonly detail: "text" | "grounded" | "layout";
9
10
  readonly groundingSchemaVersion: "none" | "omni.grounding.v1";
@@ -15,31 +16,38 @@ export declare const parseSchemaShape: {
15
16
  source: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
16
17
  url: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
17
18
  detail: z.ZodOptional<z.ZodEnum<["text", "grounded", "layout"]>>;
19
+ result_delivery: z.ZodOptional<z.ZodEnum<["auto", "artifact"]>>;
18
20
  };
19
21
  export declare const parseSchema: z.ZodEffects<z.ZodObject<{
20
22
  source: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
21
23
  url: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
22
24
  detail: z.ZodOptional<z.ZodEnum<["text", "grounded", "layout"]>>;
25
+ result_delivery: z.ZodOptional<z.ZodEnum<["auto", "artifact"]>>;
23
26
  }, "strict", z.ZodTypeAny, {
24
27
  source?: string | undefined;
25
28
  url?: string | undefined;
26
29
  detail?: "grounded" | "layout" | "text" | undefined;
30
+ result_delivery?: "auto" | "artifact" | undefined;
27
31
  }, {
28
32
  source?: string | undefined;
29
33
  url?: string | undefined;
30
34
  detail?: "grounded" | "layout" | "text" | undefined;
35
+ result_delivery?: "auto" | "artifact" | undefined;
31
36
  }>, {
32
37
  source?: string | undefined;
33
38
  url?: string | undefined;
34
39
  detail?: "grounded" | "layout" | "text" | undefined;
40
+ result_delivery?: "auto" | "artifact" | undefined;
35
41
  }, {
36
42
  source?: string | undefined;
37
43
  url?: string | undefined;
38
44
  detail?: "grounded" | "layout" | "text" | undefined;
45
+ result_delivery?: "auto" | "artifact" | undefined;
39
46
  }>;
40
47
  export interface NormalizedParseArguments {
41
48
  readonly source: string;
42
49
  readonly detail?: "text" | "grounded" | "layout";
50
+ readonly resultDelivery: ResultDelivery;
43
51
  }
44
52
  export declare function normalizeParseArguments(args: ParseArguments): NormalizedParseArguments;
45
53
  export declare const getParseStatusSchema: z.ZodObject<{
package/dist/protocol.js CHANGED
@@ -1,11 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { RESULT_CHUNK_MAX_BYTES, STATUS_LONG_POLL_MAX_MS } from "./constants.js";
3
- export const LOCAL_BRIDGE_PROTOCOL_VERSION = "omni.local_bridge_tools.v3";
4
- // D2-D item 1: versioned persistence literals frozen with the v3 contract.
5
- // The journal record version and the bundle cursor version are bumped
6
- // together with the tool contract; legacy v2 journal records still recover
7
- // text operations unchanged and are never auto-upgraded.
8
- export const OPERATION_JOURNAL_VERSION = 3;
3
+ export const LOCAL_BRIDGE_PROTOCOL_VERSION = "omni.local_bridge_tools.v5";
4
+ export const OPERATION_JOURNAL_VERSION = 5;
9
5
  export const BUNDLE_CURSOR_VERSION = 2;
10
6
  // Canonical bundle/grounding literals carried by non-text representations.
11
7
  export const RESULT_BUNDLE_PROTOCOL_VERSION = "omni.result_bundle.v1";
@@ -25,6 +21,7 @@ export function normalizeRepresentation(detail) {
25
21
  };
26
22
  }
27
23
  export const MACHINE_INSTRUCTIONS = [
24
+ "Use parse as the only first call for both HTTP(S) URLs and local paths; do not ask the user to choose a local, remote, upload, or URL mode.",
28
25
  "Pass the user-provided source string directly to parse.",
29
26
  "Treat only HTTP(S) as URL; ordinary paths require the local Bridge.",
30
27
  "Never read, attach, base64-encode, or insert local source content before calling Omni.",
@@ -36,6 +33,20 @@ export const MACHINE_INSTRUCTIONS = [
36
33
  "Do not promise background notification when the client lacks task support.",
37
34
  "Do not claim deletion before cleanup is confirmed.",
38
35
  "Report authoritative unit progress when present; never treat partial output as final.",
36
+ "Choose continuation tools from the structured result; do not present the Bridge tool menu to the user.",
37
+ "Text output is Markdown and may retain headings, lists, GFM tables, or raw HTML tables; it lacks grounding/layout sidecars, not all structure.",
38
+ "An empty outline means no recognized headings, not that the text has no structure.",
39
+ "Answer directly → use inline text when present; otherwise read_result",
40
+ "Find one section → read_outline, then pass its cursor to read_result",
41
+ "Read all content → read_result until next_cursor is absent",
42
+ "Deliver a file → save_result",
43
+ "Outline navigation does not require save_result.",
44
+ "Use result_delivery=artifact for saving, section navigation, multiple documents, or strict context control.",
45
+ "Use bounded concurrent independent parse calls for multiple sources; there is no batch_parse tool.",
46
+ "Discover MCP Tasks, MCP Roots, host tool timeout, and process cwd versus active workspace; do not guess client capabilities.",
47
+ "Tasks unknown → use ordinary parse and get_parse_status polling.",
48
+ "Roots unknown → use process cwd and explicitly configured roots only.",
49
+ "Host timeout unknown → retain the bounded 20-second status wait.",
39
50
  "After parsing, continue the user's original task.",
40
51
  ].join("\n");
41
52
  const operationIdSchema = z.string().regex(/^op_[A-Za-z0-9_-]{16,64}$/u);
@@ -60,6 +71,7 @@ const parseSchemaObject = z
60
71
  .refine((value) => !value.includes("\0"))
61
72
  .optional(),
62
73
  detail: z.enum(["text", "grounded", "layout"]).optional(),
74
+ result_delivery: z.enum(["auto", "artifact"]).optional(),
63
75
  })
64
76
  .strict();
65
77
  export const parseSchemaShape = parseSchemaObject.shape;
@@ -71,7 +83,11 @@ export function normalizeParseArguments(args) {
71
83
  // schema-validated input and guards misuse of the exported helper.
72
84
  throw new Error("parse arguments require exactly one of source or url");
73
85
  }
74
- return { source, detail: args.detail };
86
+ return {
87
+ source,
88
+ detail: args.detail,
89
+ resultDelivery: args.result_delivery ?? "auto",
90
+ };
75
91
  }
76
92
  export const getParseStatusSchema = z
77
93
  .object({
@@ -2,7 +2,7 @@ import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js";
2
2
  import { parseReaderCapabilities, selectUrlProfile, } from "./capabilities.js";
3
3
  import { BRIDGE_RELEASE_VERSION, REMOTE_CAPABILITIES_CUSTOM_FIELD, REMOTE_OMNI_MCP_URL, } from "./constants.js";
4
4
  import { API_KEY_URL } from "./onboarding-policy.js";
5
- import { OmniBridgeError } from "./errors.js";
5
+ import { OmniBridgeError, unsupportedDetailError } from "./errors.js";
6
6
  import { parseResultSchema } from "./result-contract.js";
7
7
  import { classifySource } from "./source.js";
8
8
  const INITIALIZE_REQUEST_ID = "initialize";
@@ -108,16 +108,6 @@ function protocolError() {
108
108
  retryable: false,
109
109
  });
110
110
  }
111
- function unsupportedDetail() {
112
- return remoteError({
113
- code: "UNSUPPORTED_DETAIL",
114
- message: "This remote Omni service does not support the requested output detail.",
115
- failureScope: "service",
116
- userAction: "Use plain Markdown output for this source.",
117
- operationCreated: false,
118
- retryable: false,
119
- });
120
- }
121
111
  // Extracts exactly `result.capabilities.experimental["cue.omni-reader"]` from
122
112
  // an MCP initialize envelope; returns undefined when the exact custom field is
123
113
  // absent (server did not declare the capability), throws on contract
@@ -351,7 +341,7 @@ export class HttpRemoteOmniClient {
351
341
  }
352
342
  const custom = initializeCustomCapability(envelope, INITIALIZE_REQUEST_ID);
353
343
  if (custom === undefined) {
354
- throw unsupportedDetail();
344
+ throw unsupportedDetailError("url");
355
345
  }
356
346
  let capabilities;
357
347
  try {
@@ -388,7 +378,7 @@ export class HttpRemoteOmniClient {
388
378
  // non-text v3 call; a missing/mismatched profile fails closed with
389
379
  // UNSUPPORTED_DETAIL and zero tools/call requests.
390
380
  const capabilities = await this.initializeCapabilities(signal);
391
- selectUrlProfile(capabilities, detail);
381
+ selectUrlProfile(capabilities, detail, "url");
392
382
  args = { source: classified.source, detail, wait: false };
393
383
  }
394
384
  return this.#call("parse", args, clientRequestId, signal);