@cueai/omni-reader-mcp 1.3.2 → 1.4.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/README.md +25 -18
- package/dist/artifact-store.d.ts +4 -0
- package/dist/artifact-store.js +80 -1
- package/dist/cli/agent-config.js +38 -27
- package/dist/cli/doctor.d.ts +19 -0
- package/dist/cli/doctor.js +93 -3
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/protocol.d.ts +8 -0
- package/dist/protocol.js +5 -0
- package/dist/remote-client.js +7 -2
- package/dist/result-contract.d.ts +2 -0
- package/dist/result-contract.js +27 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +41 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,10 +15,14 @@ Parse https://example.com/report.pdf with Omni
|
|
|
15
15
|
Parse ./report.pdf with Omni and summarize the revenue changes
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
a
|
|
21
|
-
|
|
18
|
+
Install Bridge as the default: it handles a URL and a local file through the same call, so
|
|
19
|
+
neither the agent nor the user has to reason about which kind of source it is. The only
|
|
20
|
+
exception is a deployment that will genuinely never need local files — that case is covered
|
|
21
|
+
by a remote-only MCP connection instead, with nothing to install. Otherwise, before
|
|
22
|
+
installing Bridge the agent should first explain the minimal directory authorization and
|
|
23
|
+
data handling and obtain confirmation, then install a pinned Bridge version. After parsing
|
|
24
|
+
completes, the agent continues the original task — summarization, Q&A, field extraction —
|
|
25
|
+
rather than stopping at "parse done".
|
|
22
26
|
|
|
23
27
|
## Requirements
|
|
24
28
|
|
|
@@ -60,7 +64,7 @@ differ from the numbers above, report the live values.
|
|
|
60
64
|
Always use an audited exact version, never an implicit `latest`:
|
|
61
65
|
|
|
62
66
|
```sh
|
|
63
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
67
|
+
npx -y @cueai/omni-reader-mcp@1.4.0 setup
|
|
64
68
|
```
|
|
65
69
|
|
|
66
70
|
The interactive setup supports Hermes, Cursor, Claude Desktop, and generic stdio
|
|
@@ -68,9 +72,9 @@ configuration. Non-interactive installation uses the same argument parsing and w
|
|
|
68
72
|
logic:
|
|
69
73
|
|
|
70
74
|
```sh
|
|
71
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
72
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
73
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
75
|
+
npx -y @cueai/omni-reader-mcp@1.4.0 setup --client hermes --allowed-root /absolute/minimum/root --yes --json
|
|
76
|
+
npx -y @cueai/omni-reader-mcp@1.4.0 setup --client cursor --add-root /absolute/minimum/root --yes --json
|
|
77
|
+
npx -y @cueai/omni-reader-mcp@1.4.0 setup --client claude-desktop --allowed-root /absolute/minimum/root --yes --json
|
|
74
78
|
```
|
|
75
79
|
|
|
76
80
|
When an agent or script runs under a pty (stdin is still a TTY), declare non-interactive
|
|
@@ -78,7 +82,7 @@ mode explicitly with `--headless` (alias `--non-interactive`): no `--yes` is req
|
|
|
78
82
|
stdin is never read:
|
|
79
83
|
|
|
80
84
|
```sh
|
|
81
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
85
|
+
npx -y @cueai/omni-reader-mcp@1.4.0 setup --client cursor --allowed-root /absolute/minimum/root --headless --json
|
|
82
86
|
```
|
|
83
87
|
|
|
84
88
|
## Cache and journal isolation
|
|
@@ -199,22 +203,25 @@ Bridge validates the local source before grant, upload, and billing:
|
|
|
199
203
|
- `UNSUPPORTED_MEDIA_TYPE`: unsupported media type, with the current authoritative
|
|
200
204
|
`supported_extensions`.
|
|
201
205
|
|
|
202
|
-
|
|
203
|
-
|
|
206
|
+
Do not upload the source to a public site to work around a constraint. Splitting or
|
|
207
|
+
transcoding the source yourself (with your own tools, outside Bridge) is fine, but only
|
|
208
|
+
after explicitly telling the user what you intend to do to it and getting their
|
|
209
|
+
confirmation — never do this silently. Otherwise, ask the user for a new source that
|
|
210
|
+
satisfies the constraints.
|
|
204
211
|
|
|
205
212
|
## Commands
|
|
206
213
|
|
|
207
214
|
```sh
|
|
208
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
209
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
210
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
211
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
215
|
+
npx -y @cueai/omni-reader-mcp@1.4.0 doctor
|
|
216
|
+
npx -y @cueai/omni-reader-mcp@1.4.0 doctor --json
|
|
217
|
+
npx -y @cueai/omni-reader-mcp@1.4.0 clean
|
|
218
|
+
npx -y @cueai/omni-reader-mcp@1.4.0 uninstall --yes --json
|
|
212
219
|
```
|
|
213
220
|
|
|
214
221
|
Running the pinned version without a command starts the stdio MCP server:
|
|
215
222
|
|
|
216
223
|
```sh
|
|
217
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
224
|
+
npx -y @cueai/omni-reader-mcp@1.4.0
|
|
218
225
|
```
|
|
219
226
|
|
|
220
227
|
`doctor --json` returns package/npm/client adapter, Key present/absent, allowed-root
|
|
@@ -223,12 +230,12 @@ status; it never prints the Key, private source paths, or content.
|
|
|
223
230
|
|
|
224
231
|
## Uninstall and rollback
|
|
225
232
|
|
|
226
|
-
`uninstall --yes --json` removes only a trusted 1.3.
|
|
233
|
+
`uninstall --yes --json` removes only a trusted 1.3.3 or 1.4.0 Bridge entry; when a
|
|
227
234
|
matching trusted backup exists, it restores the original URL-only `omni-reader` entry.
|
|
228
235
|
Uninstall never deletes user source files and never silently removes unexpired local
|
|
229
236
|
results.
|
|
230
237
|
|
|
231
|
-
To roll back from 1.
|
|
238
|
+
To roll back from 1.4.0:
|
|
232
239
|
|
|
233
240
|
1. stop recommending or installing that version;
|
|
234
241
|
2. run `uninstall --yes --json` to restore the trusted URL-only entry;
|
package/dist/artifact-store.d.ts
CHANGED
|
@@ -112,6 +112,10 @@ export declare class ArtifactStore {
|
|
|
112
112
|
read(resultId: string, cursor?: string, maxBytes?: number): Promise<ArtifactReadResult>;
|
|
113
113
|
readOutline(resultId: string): Promise<OutlineResult>;
|
|
114
114
|
mintOutlineCursor(resultId: string, byteOffset: number): Promise<string>;
|
|
115
|
+
exportResult(resultId: string): Promise<{
|
|
116
|
+
path: string;
|
|
117
|
+
bytes: number;
|
|
118
|
+
}>;
|
|
115
119
|
readBundlePart(resultId: string, cursor: string, maxBytes?: number): Promise<BundlePartReadChunk>;
|
|
116
120
|
discard(resultId: string): Promise<boolean>;
|
|
117
121
|
cleanupExpired(): Promise<number>;
|
package/dist/artifact-store.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { constants as fsConstants } from "node:fs";
|
|
3
|
-
import { chmod, link, lstat, mkdir, open, readdir, realpath, rename, rmdir, unlink, } from "node:fs/promises";
|
|
3
|
+
import { chmod, link, lstat, mkdir, open, readdir, realpath, rename, rmdir, unlink, writeFile, } from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
@@ -571,6 +571,85 @@ export class ArtifactStore {
|
|
|
571
571
|
}
|
|
572
572
|
return this.#cursor.encode({ resultId, offset: byteOffset, expiresAt: metadata.expiresAt });
|
|
573
573
|
}
|
|
574
|
+
// The full content text in one read, regardless of storage shape (a flat
|
|
575
|
+
// v1 artifact, or a v2 bundle's "content" part). save_result's only
|
|
576
|
+
// consumer: read_result's chunked/cursor contract stays the single way an
|
|
577
|
+
// MCP caller streams a result; this exists purely to materialize the same
|
|
578
|
+
// bytes onto disk in one pass instead of looping next_cursor internally.
|
|
579
|
+
async #readFullText(resultId) {
|
|
580
|
+
this.#requireOpen();
|
|
581
|
+
let metadata;
|
|
582
|
+
try {
|
|
583
|
+
metadata = await this.#loadMetadata(resultId);
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
if (error instanceof OmniBridgeError
|
|
587
|
+
&& error.code === "RESULT_NOT_FOUND"
|
|
588
|
+
&& await this.#hasBundleDirectory(resultId)) {
|
|
589
|
+
return this.#readFullBundleContent(resultId);
|
|
590
|
+
}
|
|
591
|
+
throw error;
|
|
592
|
+
}
|
|
593
|
+
if (Date.parse(metadata.expiresAt) <= this.#now().getTime()) {
|
|
594
|
+
await this.discard(resultId);
|
|
595
|
+
throw artifactError("RESULT_EXPIRED", "The local result artifact has expired.");
|
|
596
|
+
}
|
|
597
|
+
const artifactPath = path.join(this.#resultsDirectory, metadata.artifactName);
|
|
598
|
+
const handle = await openArtifactForRead(artifactPath);
|
|
599
|
+
try {
|
|
600
|
+
const artifactStat = await handle.stat();
|
|
601
|
+
if (!artifactStat.isFile() || artifactStat.size !== metadata.resultBytes) {
|
|
602
|
+
throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result artifact is invalid.");
|
|
603
|
+
}
|
|
604
|
+
const slice = await readUtf8Slice(handle, 0, metadata.resultBytes, metadata.resultBytes);
|
|
605
|
+
return { text: slice.text, resultBytes: metadata.resultBytes, expiresAt: metadata.expiresAt };
|
|
606
|
+
}
|
|
607
|
+
finally {
|
|
608
|
+
await handle.close();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
async #readFullBundleContent(resultId) {
|
|
612
|
+
const manifest = await this.#loadBundleManifest(resultId);
|
|
613
|
+
if (Date.parse(manifest.expires_at) <= this.#now().getTime()) {
|
|
614
|
+
await this.discard(resultId);
|
|
615
|
+
throw artifactError("RESULT_EXPIRED", "The local result artifact has expired.");
|
|
616
|
+
}
|
|
617
|
+
const part = manifest.parts.content;
|
|
618
|
+
if (part === undefined) {
|
|
619
|
+
throw artifactError("RESULT_NOT_FOUND", "The requested local result was not found.");
|
|
620
|
+
}
|
|
621
|
+
const source = part.storage.kind === "inline"
|
|
622
|
+
? Buffer.from(part.storage.text, "utf8")
|
|
623
|
+
: await readPrivateFile(path.join(this.#resultsDirectory, resultId, "content.data"), "LOCAL_RESULT_INTEGRITY_FAILED", "The local bundle part is missing.");
|
|
624
|
+
if (source.length !== part.result_bytes) {
|
|
625
|
+
throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local bundle part is invalid.");
|
|
626
|
+
}
|
|
627
|
+
const computedDigest = `sha256:${createHash("sha256").update(source).digest("hex")}`;
|
|
628
|
+
if (computedDigest !== part.digest) {
|
|
629
|
+
throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local bundle part is invalid.");
|
|
630
|
+
}
|
|
631
|
+
return { text: source.toString("utf8"), resultBytes: part.result_bytes, expiresAt: manifest.expires_at };
|
|
632
|
+
}
|
|
633
|
+
// Materializes a result's complete content onto a stable, predictable
|
|
634
|
+
// path -- no caller-supplied destination, deliberately: result_id is
|
|
635
|
+
// already validated against RESULT_ID_PATTERN, so using it verbatim as the
|
|
636
|
+
// export filename carries no path-traversal or injection surface, unlike a
|
|
637
|
+
// free-form path argument would. Overwrites on repeat calls (idempotent,
|
|
638
|
+
// not an error) so re-exporting after a result changes never leaves a
|
|
639
|
+
// stale file silently in place.
|
|
640
|
+
async exportResult(resultId) {
|
|
641
|
+
this.#requireOpen();
|
|
642
|
+
if (!RESULT_ID_PATTERN.test(resultId)) {
|
|
643
|
+
throw artifactError("RESULT_NOT_FOUND", "The requested local result was not found.");
|
|
644
|
+
}
|
|
645
|
+
const { text } = await this.#readFullText(resultId);
|
|
646
|
+
const exportsDirectory = path.join(this.#rootDirectory, "exports");
|
|
647
|
+
await mkdir(exportsDirectory, { recursive: true, mode: 0o700 });
|
|
648
|
+
const exportPath = path.join(exportsDirectory, `${resultId}.md`);
|
|
649
|
+
const bytes = Buffer.from(text, "utf8");
|
|
650
|
+
await writeFile(exportPath, bytes, { mode: 0o600 });
|
|
651
|
+
return { path: exportPath, bytes: bytes.byteLength };
|
|
652
|
+
}
|
|
574
653
|
// Read one UTF-8-safe chunk of one named part of a retained bundle. The
|
|
575
654
|
// opaque v2 cursor binds resultId/part/detail/schema/bundle protocol/
|
|
576
655
|
// offset/expiry; every bound field is re-verified against the closed
|
package/dist/cli/agent-config.js
CHANGED
|
@@ -4,7 +4,11 @@ import { chmod, lstat, mkdir, open, realpath, rename, unlink, } from "node:fs/pr
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { BRIDGE_RELEASE_VERSION, REMOTE_OMNI_MCP_URL, } from "../constants.js";
|
|
6
6
|
const PACKAGE_SPEC = `@cueai/omni-reader-mcp@${BRIDGE_RELEASE_VERSION}`;
|
|
7
|
-
|
|
7
|
+
// The release that the current version is a trusted upgrade from: uninstall and
|
|
8
|
+
// rollback recognize exactly {previous, current} and nothing else. Bump this to
|
|
9
|
+
// the version we just published when BRIDGE_RELEASE_VERSION advances.
|
|
10
|
+
const PREVIOUS_RELEASE_VERSION = "1.3.3";
|
|
11
|
+
const PREVIOUS_PACKAGE_SPEC = `@cueai/omni-reader-mcp@${PREVIOUS_RELEASE_VERSION}`;
|
|
8
12
|
const LEGACY_PACKAGE_SPEC = "@cueai/omni-reader-mcp";
|
|
9
13
|
const TRUSTED_EXACT_PACKAGE_SPECS = new Set([PREVIOUS_PACKAGE_SPEC, PACKAGE_SPEC]);
|
|
10
14
|
function isRecord(value) {
|
|
@@ -586,7 +590,7 @@ function expectedBridgeVersion(target, value) {
|
|
|
586
590
|
if (!isExpectedOmniEntry(target, value))
|
|
587
591
|
return undefined;
|
|
588
592
|
const packageSpec = value.args[1];
|
|
589
|
-
return packageSpec === PREVIOUS_PACKAGE_SPEC ?
|
|
593
|
+
return packageSpec === PREVIOUS_PACKAGE_SPEC ? PREVIOUS_RELEASE_VERSION : BRIDGE_RELEASE_VERSION;
|
|
590
594
|
}
|
|
591
595
|
function isLegacyBridgeEntry(target, value) {
|
|
592
596
|
if (!isRecord(value) || value.command !== "npx")
|
|
@@ -614,36 +618,38 @@ function isCanonicalRemoteEntry(value) {
|
|
|
614
618
|
&& value.args === undefined
|
|
615
619
|
&& safeCredentialReferences(value);
|
|
616
620
|
}
|
|
617
|
-
function onlyOmniEntry(servers) {
|
|
621
|
+
function onlyOmniEntry(servers, displayPath) {
|
|
618
622
|
const keys = Object.keys(servers).filter(normalizedOmniName);
|
|
619
623
|
if (keys.length > 1) {
|
|
620
|
-
throw fileError(
|
|
624
|
+
throw fileError(`${displayPath} contains duplicate Omni entries; no changes were written.`);
|
|
621
625
|
}
|
|
622
626
|
const key = keys[0];
|
|
623
627
|
if (key !== undefined && key !== "omni-reader") {
|
|
624
|
-
throw fileError(
|
|
628
|
+
throw fileError(`${displayPath} contains a conflicting Omni entry named ${JSON.stringify(key)}; no changes were written.`);
|
|
625
629
|
}
|
|
626
630
|
return key === undefined ? {} : { key, value: servers[key] };
|
|
627
631
|
}
|
|
628
|
-
function hermesOmniEntry(document) {
|
|
632
|
+
function hermesOmniEntry(document, displayPath) {
|
|
629
633
|
const entries = document.entries.filter((entry) => normalizedOmniName(entry.name));
|
|
630
634
|
if (entries.length > 1) {
|
|
631
|
-
throw fileError(
|
|
635
|
+
throw fileError(`${displayPath} contains duplicate Omni entries; no changes were written.`);
|
|
632
636
|
}
|
|
633
637
|
const entry = entries[0];
|
|
634
638
|
if (entry !== undefined && entry.name !== "omni-reader") {
|
|
635
|
-
throw fileError(
|
|
639
|
+
throw fileError(`${displayPath} contains a conflicting Omni entry named ${JSON.stringify(entry.name)}; no changes were written.`);
|
|
636
640
|
}
|
|
637
641
|
return entry;
|
|
638
642
|
}
|
|
639
|
-
function previousJsonEntry(value) {
|
|
643
|
+
function previousJsonEntry(value, displayPath) {
|
|
640
644
|
if (value === undefined)
|
|
641
645
|
return null;
|
|
642
646
|
if (isCanonicalRemoteEntry(value))
|
|
643
647
|
return { format: "json", value };
|
|
644
648
|
if (isLegacyBridgeEntry("cursor", value))
|
|
645
649
|
return null;
|
|
646
|
-
throw fileError(
|
|
650
|
+
throw fileError(`The existing "omni-reader" entry in ${displayPath} is not a trusted remote or Bridge `
|
|
651
|
+
+ "configuration; no changes were written. Remove that entry manually, or restore a backup, "
|
|
652
|
+
+ "then run setup again.");
|
|
647
653
|
}
|
|
648
654
|
function hermesCredentialsAreReferences(serialized) {
|
|
649
655
|
for (const line of sourceLines(serialized)) {
|
|
@@ -657,19 +663,23 @@ function hermesCredentialsAreReferences(serialized) {
|
|
|
657
663
|
}
|
|
658
664
|
return true;
|
|
659
665
|
}
|
|
660
|
-
function previousHermesEntry(entry) {
|
|
666
|
+
function previousHermesEntry(entry, displayPath) {
|
|
661
667
|
if (entry === undefined)
|
|
662
668
|
return null;
|
|
663
669
|
const value = parseHermesEntry(entry);
|
|
664
670
|
if (isCanonicalRemoteEntry(value)) {
|
|
665
671
|
if (!hermesCredentialsAreReferences(entry.serialized)) {
|
|
666
|
-
throw fileError(
|
|
672
|
+
throw fileError(`The "omni-reader" entry in ${displayPath} contains a literal credential; no changes `
|
|
673
|
+
+ "were written. Replace the value with a ${CUE_API_KEY} reference, or remove the "
|
|
674
|
+
+ "entry, then run setup again.");
|
|
667
675
|
}
|
|
668
676
|
return { format: "hermes-yaml", serialized: entry.serialized };
|
|
669
677
|
}
|
|
670
678
|
if (isLegacyBridgeEntry("hermes", value))
|
|
671
679
|
return null;
|
|
672
|
-
throw fileError(
|
|
680
|
+
throw fileError(`The existing "omni-reader" entry in ${displayPath} is not a trusted remote or Bridge `
|
|
681
|
+
+ "configuration; no changes were written. Remove that entry manually, or restore a backup, "
|
|
682
|
+
+ "then run setup again.");
|
|
673
683
|
}
|
|
674
684
|
export async function prepareAgentConfig(target, extraRoots, environment) {
|
|
675
685
|
const entry = buildAgentEntry(target, extraRoots, environment.platform);
|
|
@@ -690,16 +700,17 @@ export async function prepareAgentConfig(target, extraRoots, environment) {
|
|
|
690
700
|
const loaded = await readConfig(configPath);
|
|
691
701
|
assertNoLiteralApiKey(loaded.serialized, environment);
|
|
692
702
|
const format = configFormat(nativeTarget);
|
|
703
|
+
const displayPath = displayConfigPath(configPath, environment);
|
|
693
704
|
if (format === "json") {
|
|
694
705
|
assertNoDuplicateJsonOmniEntries(loaded.serialized);
|
|
695
706
|
const value = parseJsonConfig(loaded);
|
|
696
707
|
const existingServers = value.mcpServers;
|
|
697
708
|
if (existingServers !== undefined && !isRecord(existingServers)) {
|
|
698
|
-
throw fileError(
|
|
709
|
+
throw fileError(`The existing mcpServers setting in ${displayPath} is not an object; no changes were written.`);
|
|
699
710
|
}
|
|
700
711
|
const servers = existingServers ?? {};
|
|
701
|
-
const existing = onlyOmniEntry(servers);
|
|
702
|
-
const previousEntry = previousJsonEntry(existing.value);
|
|
712
|
+
const existing = onlyOmniEntry(servers, displayPath);
|
|
713
|
+
const previousEntry = previousJsonEntry(existing.value, displayPath);
|
|
703
714
|
const afterValue = {
|
|
704
715
|
...value,
|
|
705
716
|
mcpServers: {
|
|
@@ -710,7 +721,7 @@ export async function prepareAgentConfig(target, extraRoots, environment) {
|
|
|
710
721
|
return {
|
|
711
722
|
target: nativeTarget,
|
|
712
723
|
configPath,
|
|
713
|
-
displayPath
|
|
724
|
+
displayPath,
|
|
714
725
|
existed: loaded.existed,
|
|
715
726
|
before: redactSecrets(value),
|
|
716
727
|
after: redactSecrets(afterValue),
|
|
@@ -725,13 +736,13 @@ export async function prepareAgentConfig(target, extraRoots, environment) {
|
|
|
725
736
|
};
|
|
726
737
|
}
|
|
727
738
|
const document = parseHermesDocument(loaded.serialized);
|
|
728
|
-
const existing = hermesOmniEntry(document);
|
|
729
|
-
const previousEntry = previousHermesEntry(existing);
|
|
739
|
+
const existing = hermesOmniEntry(document, displayPath);
|
|
740
|
+
const previousEntry = previousHermesEntry(existing, displayPath);
|
|
730
741
|
const serializedAfter = replaceHermesEntry(loaded.serialized, document, yamlEntry(entry, document.newline));
|
|
731
742
|
return {
|
|
732
743
|
target: "hermes",
|
|
733
744
|
configPath,
|
|
734
|
-
displayPath
|
|
745
|
+
displayPath,
|
|
735
746
|
existed: loaded.existed,
|
|
736
747
|
before: {
|
|
737
748
|
format: "hermes-yaml",
|
|
@@ -998,12 +1009,12 @@ export async function writePreparedAgentConfig(prepared) {
|
|
|
998
1009
|
function jsonConfigEntry(value) {
|
|
999
1010
|
if (!isRecord(value.mcpServers))
|
|
1000
1011
|
return undefined;
|
|
1001
|
-
return onlyOmniEntry(value.mcpServers).value;
|
|
1012
|
+
return onlyOmniEntry(value.mcpServers, "the Agent configuration").value;
|
|
1002
1013
|
}
|
|
1003
1014
|
function expectedConfigEntry(target, loaded) {
|
|
1004
1015
|
if (target === "hermes") {
|
|
1005
1016
|
const document = parseHermesDocument(loaded.serialized);
|
|
1006
|
-
const entry = hermesOmniEntry(document);
|
|
1017
|
+
const entry = hermesOmniEntry(document, "the Hermes configuration");
|
|
1007
1018
|
if (entry === undefined)
|
|
1008
1019
|
return undefined;
|
|
1009
1020
|
const value = parseHermesEntry(entry);
|
|
@@ -1026,7 +1037,7 @@ export async function inspectAgentConfigDetails(configPath, environment, target)
|
|
|
1026
1037
|
const resolvedTarget = target ?? (configPath.endsWith("config.yaml") ? "hermes" : "cursor");
|
|
1027
1038
|
if (resolvedTarget === "hermes") {
|
|
1028
1039
|
const document = parseHermesDocument(loaded.serialized);
|
|
1029
|
-
const entry = hermesOmniEntry(document);
|
|
1040
|
+
const entry = hermesOmniEntry(document, configPath);
|
|
1030
1041
|
if (entry === undefined)
|
|
1031
1042
|
return { status: "not configured" };
|
|
1032
1043
|
const value = parseHermesEntry(entry);
|
|
@@ -1041,7 +1052,7 @@ export async function inspectAgentConfigDetails(configPath, environment, target)
|
|
|
1041
1052
|
const value = parseJsonConfig(loaded);
|
|
1042
1053
|
if (!isRecord(value.mcpServers))
|
|
1043
1054
|
return { status: "not configured" };
|
|
1044
|
-
const entry = onlyOmniEntry(value.mcpServers).value;
|
|
1055
|
+
const entry = onlyOmniEntry(value.mcpServers, configPath).value;
|
|
1045
1056
|
if (entry === undefined)
|
|
1046
1057
|
return { status: "not configured" };
|
|
1047
1058
|
const version = expectedBridgeVersion(resolvedTarget, entry);
|
|
@@ -1089,7 +1100,7 @@ export async function configuredAllowedRoots(target, environment) {
|
|
|
1089
1100
|
return [];
|
|
1090
1101
|
let entry;
|
|
1091
1102
|
if (target === "hermes") {
|
|
1092
|
-
const hermesEntry = hermesOmniEntry(parseHermesDocument(loaded.serialized));
|
|
1103
|
+
const hermesEntry = hermesOmniEntry(parseHermesDocument(loaded.serialized), configPath);
|
|
1093
1104
|
entry = hermesEntry === undefined ? undefined : parseHermesEntry(hermesEntry);
|
|
1094
1105
|
}
|
|
1095
1106
|
else {
|
|
@@ -1170,7 +1181,7 @@ function trustedRemotePrevious(previous) {
|
|
|
1170
1181
|
return isCanonicalRemoteEntry(previous.value) ? previous : null;
|
|
1171
1182
|
}
|
|
1172
1183
|
const document = parseHermesDocument(`mcp_servers:\n${previous.serialized}`);
|
|
1173
|
-
const entry = hermesOmniEntry(document);
|
|
1184
|
+
const entry = hermesOmniEntry(document, "the Hermes configuration");
|
|
1174
1185
|
return entry !== undefined && isCanonicalRemoteEntry(parseHermesEntry(entry))
|
|
1175
1186
|
? previous
|
|
1176
1187
|
: null;
|
|
@@ -1189,7 +1200,7 @@ export async function uninstallAgentConfig(target, environment) {
|
|
|
1189
1200
|
throw fileError("The Agent configuration changed before uninstall; no changes were written.");
|
|
1190
1201
|
}
|
|
1191
1202
|
const currentEntry = target === "hermes"
|
|
1192
|
-
? parseHermesEntry(hermesOmniEntry(parseHermesDocument(current.serialized)))
|
|
1203
|
+
? parseHermesEntry(hermesOmniEntry(parseHermesDocument(current.serialized), configPath))
|
|
1193
1204
|
: jsonConfigEntry(parseJsonConfig(current));
|
|
1194
1205
|
const backup = await readTrustedBackup(configPath);
|
|
1195
1206
|
const matchingBackup = backup !== undefined
|
package/dist/cli/doctor.d.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { type AgentConfigEnvironment } from "./agent-config.js";
|
|
2
|
+
export type VersionCheck = {
|
|
3
|
+
readonly status: "current";
|
|
4
|
+
readonly installed: string;
|
|
5
|
+
readonly latest: string;
|
|
6
|
+
} | {
|
|
7
|
+
readonly status: "outdated";
|
|
8
|
+
readonly installed: string;
|
|
9
|
+
readonly latest: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly status: "ahead";
|
|
12
|
+
readonly installed: string;
|
|
13
|
+
readonly latest: string;
|
|
14
|
+
} | {
|
|
15
|
+
readonly status: "unavailable";
|
|
16
|
+
readonly installed: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function fetchLatestPublishedVersion(fetchImpl: typeof fetch, timeoutMs?: number): Promise<string | undefined>;
|
|
19
|
+
export declare function versionCheckFacts(installed: string, latest: string | undefined): VersionCheck;
|
|
2
20
|
export interface HealthResult {
|
|
3
21
|
readonly cubeProtocol: string;
|
|
4
22
|
readonly iiisProtocol: string;
|
|
@@ -14,6 +32,7 @@ export interface DoctorOptions extends AgentConfigEnvironment {
|
|
|
14
32
|
}
|
|
15
33
|
export interface DoctorReport {
|
|
16
34
|
readonly package_version: string;
|
|
35
|
+
readonly version_check: VersionCheck;
|
|
17
36
|
readonly node_version: string;
|
|
18
37
|
readonly npm_version: string;
|
|
19
38
|
readonly client_adapters: Record<string, {
|
package/dist/cli/doctor.js
CHANGED
|
@@ -6,6 +6,67 @@ import { API_KEY_URL, getOnboardingPolicyWithTimeout, onboardingGuidance, } from
|
|
|
6
6
|
import { agentConfigPath, inspectAgentConfigDetails, } from "./agent-config.js";
|
|
7
7
|
const CUBE_HEALTH_PATH = "/api/omni-reader/direct-upload/v1/health";
|
|
8
8
|
const GRANTED_UPLOAD_HEALTH_URL = "https://cubefile.ai.iiis.co:9443/omni/granted/health";
|
|
9
|
+
const NPM_LATEST_URL = "https://registry.npmjs.org/@cueai/omni-reader-mcp/latest";
|
|
10
|
+
const SEMVER_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
11
|
+
// Numeric semver comparison, not string equality: "1.3.9" < "1.3.10" lexically reverses
|
|
12
|
+
// (string comparison would see "1.3.9" > "1.3.10"), and a local/dev build can legitimately
|
|
13
|
+
// be numerically ahead of whatever npm currently has published as "latest".
|
|
14
|
+
function compareSemver(a, b) {
|
|
15
|
+
const partsA = a.split(".").map(Number);
|
|
16
|
+
const partsB = b.split(".").map(Number);
|
|
17
|
+
for (let index = 0; index < 3; index += 1) {
|
|
18
|
+
const diff = partsA[index] - partsB[index];
|
|
19
|
+
if (diff !== 0)
|
|
20
|
+
return diff;
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
function isNpmLatestBody(value) {
|
|
25
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
26
|
+
return false;
|
|
27
|
+
const record = value;
|
|
28
|
+
return typeof record.version === "string" && SEMVER_PATTERN.test(record.version);
|
|
29
|
+
}
|
|
30
|
+
// Mirrors getOnboardingPolicyWithTimeout's shape exactly: bounded timeout, strict
|
|
31
|
+
// response validation, silent degrade to undefined on any failure -- a stale or
|
|
32
|
+
// unreachable registry must never surface as a crash or a false version claim.
|
|
33
|
+
export async function fetchLatestPublishedVersion(fetchImpl, timeoutMs = 3_000) {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
36
|
+
timer.unref?.();
|
|
37
|
+
try {
|
|
38
|
+
const response = await fetchImpl(NPM_LATEST_URL, {
|
|
39
|
+
method: "GET",
|
|
40
|
+
headers: { accept: "application/json" },
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok)
|
|
44
|
+
return undefined;
|
|
45
|
+
const body = await response.json();
|
|
46
|
+
return isNpmLatestBody(body) ? body.version : undefined;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// "unavailable" is its own status, not a fallback to "current": the absence of a
|
|
56
|
+
// reachable signal must never manufacture a false "you're up to date" claim. Likewise, an
|
|
57
|
+
// installed version that isn't valid semver (e.g. this file's own "unknown" fallback) has
|
|
58
|
+
// nothing meaningful to compare numerically -- report unavailable rather than guessing.
|
|
59
|
+
export function versionCheckFacts(installed, latest) {
|
|
60
|
+
if (latest === undefined || !SEMVER_PATTERN.test(installed)) {
|
|
61
|
+
return { status: "unavailable", installed };
|
|
62
|
+
}
|
|
63
|
+
const comparison = compareSemver(installed, latest);
|
|
64
|
+
if (comparison === 0)
|
|
65
|
+
return { status: "current", installed, latest };
|
|
66
|
+
return comparison < 0
|
|
67
|
+
? { status: "outdated", installed, latest }
|
|
68
|
+
: { status: "ahead", installed, latest };
|
|
69
|
+
}
|
|
9
70
|
function isHealthBody(value) {
|
|
10
71
|
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
11
72
|
return false;
|
|
@@ -144,10 +205,18 @@ async function clientAdapterFacts(options) {
|
|
|
144
205
|
return result;
|
|
145
206
|
}
|
|
146
207
|
export async function runDoctor(options) {
|
|
147
|
-
const
|
|
208
|
+
const packageVersion = options.packageVersion ?? "unknown";
|
|
209
|
+
const [clientAdapters, artifacts, onboarding, latestPublishedVersion] = await Promise.all([
|
|
148
210
|
clientAdapterFacts(options),
|
|
149
211
|
artifactFacts(options.artifactRoot),
|
|
150
212
|
getOnboardingPolicyWithTimeout(options.fetchImpl),
|
|
213
|
+
// A non-semver packageVersion (the "unknown" fallback) has nothing meaningful to
|
|
214
|
+
// compare against -- skip the network round trip entirely rather than fetch a latest
|
|
215
|
+
// version we'd have no valid installed version to check it against. A valid-semver
|
|
216
|
+
// local/dev build IS worth checking (it may legitimately be ahead of npm's latest).
|
|
217
|
+
SEMVER_PATTERN.test(packageVersion)
|
|
218
|
+
? fetchLatestPublishedVersion(options.fetchImpl)
|
|
219
|
+
: Promise.resolve(undefined),
|
|
151
220
|
]);
|
|
152
221
|
let urlControl = "skipped (Cue API Key absent)";
|
|
153
222
|
if (options.env.CUE_API_KEY) {
|
|
@@ -166,7 +235,10 @@ export async function runDoctor(options) {
|
|
|
166
235
|
directUpload = "unavailable or incompatible";
|
|
167
236
|
}
|
|
168
237
|
return {
|
|
169
|
-
package_version:
|
|
238
|
+
package_version: packageVersion,
|
|
239
|
+
version_check: SEMVER_PATTERN.test(packageVersion)
|
|
240
|
+
? versionCheckFacts(packageVersion, latestPublishedVersion)
|
|
241
|
+
: { status: "unavailable", installed: packageVersion },
|
|
170
242
|
node_version: options.nodeVersion ?? process.version,
|
|
171
243
|
npm_version: options.npmVersion ?? "unavailable",
|
|
172
244
|
client_adapters: clientAdapters,
|
|
@@ -192,11 +264,23 @@ export async function runDoctor(options) {
|
|
|
192
264
|
reload_required: false,
|
|
193
265
|
};
|
|
194
266
|
}
|
|
267
|
+
function packageLine(versionCheck) {
|
|
268
|
+
if (versionCheck.status === "current")
|
|
269
|
+
return `Package: ${versionCheck.installed} (up to date)`;
|
|
270
|
+
if (versionCheck.status === "outdated") {
|
|
271
|
+
return `Package: ${versionCheck.installed} (latest published: ${versionCheck.latest} — `
|
|
272
|
+
+ `npx -y @cueai/omni-reader-mcp@${versionCheck.latest} setup to upgrade)`;
|
|
273
|
+
}
|
|
274
|
+
if (versionCheck.status === "ahead") {
|
|
275
|
+
return `Package: ${versionCheck.installed} (ahead of latest published ${versionCheck.latest} — local or dev build)`;
|
|
276
|
+
}
|
|
277
|
+
return `Package: ${versionCheck.installed} (latest published version unavailable)`;
|
|
278
|
+
}
|
|
195
279
|
export function renderDoctor(report) {
|
|
196
280
|
const lines = [
|
|
197
281
|
`Node: ${report.node_version}`,
|
|
198
282
|
`npm: ${report.npm_version}`,
|
|
199
|
-
|
|
283
|
+
packageLine(report.version_check),
|
|
200
284
|
`Cue API Key: ${report.api_key.status}`,
|
|
201
285
|
`Allowed roots: ${report.allowed_roots.count} (${report.allowed_roots.safe ? "safe" : "invalid"})`,
|
|
202
286
|
];
|
|
@@ -208,7 +292,13 @@ export function renderDoctor(report) {
|
|
|
208
292
|
lines.push(`${label} config: ${report.client_adapters[key].status}`);
|
|
209
293
|
}
|
|
210
294
|
lines.push(`Omni control protocol: ${report.endpoints.url_control}`);
|
|
295
|
+
if (report.endpoints.url_control === "unavailable or incompatible") {
|
|
296
|
+
lines.push(" WARNING: URL parsing will fail until this is reachable.");
|
|
297
|
+
}
|
|
211
298
|
lines.push(`Omni direct upload protocol: ${report.endpoints.direct_upload}`);
|
|
299
|
+
if (report.endpoints.direct_upload === "unavailable or incompatible") {
|
|
300
|
+
lines.push(" WARNING: local file parsing will fail until this is reachable.");
|
|
301
|
+
}
|
|
212
302
|
lines.push(`Artifacts: ${report.artifacts.count} file(s), ${report.artifacts.bytes} byte(s)`);
|
|
213
303
|
lines.push(`Artifact expiry: ${report.artifacts.earliest_expiry ?? (report.artifacts.count === 0 ? "none" : "unknown")}`);
|
|
214
304
|
lines.push(`Cache: ${report.cache.mode}`);
|
package/dist/constants.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare const CUBE_GRANT_PROTOCOL_VERSION = "omni.parse_grant.v1";
|
|
|
6
6
|
export declare const GRANTED_STREAM_PROTOCOL_VERSION = "omni.granted_parse_stream.v1";
|
|
7
7
|
export declare const DEFAULT_CUBE_BASE_URL = "https://mcp.cuecue.cn";
|
|
8
8
|
export declare const DEFAULT_IIIS_GRANTED_BASE_URL = "https://cubefile.ai.iiis.co:9443/omni/granted/";
|
|
9
|
-
export declare const BRIDGE_RELEASE_VERSION = "1.
|
|
9
|
+
export declare const BRIDGE_RELEASE_VERSION = "1.4.0";
|
|
10
10
|
export declare const REMOTE_OMNI_MCP_URL = "https://mcp.cuecue.cn/api/omni-reader/mcp/";
|
|
11
11
|
export declare const FOREGROUND_BUDGET_MS = 15000;
|
|
12
12
|
export declare const STATUS_LONG_POLL_MAX_MS = 20000;
|
package/dist/constants.js
CHANGED
|
@@ -6,7 +6,7 @@ export const CUBE_GRANT_PROTOCOL_VERSION = "omni.parse_grant.v1";
|
|
|
6
6
|
export const GRANTED_STREAM_PROTOCOL_VERSION = "omni.granted_parse_stream.v1";
|
|
7
7
|
export const DEFAULT_CUBE_BASE_URL = "https://mcp.cuecue.cn";
|
|
8
8
|
export const DEFAULT_IIIS_GRANTED_BASE_URL = "https://cubefile.ai.iiis.co:9443/omni/granted/";
|
|
9
|
-
export const BRIDGE_RELEASE_VERSION = "1.
|
|
9
|
+
export const BRIDGE_RELEASE_VERSION = "1.4.0";
|
|
10
10
|
export const REMOTE_OMNI_MCP_URL = "https://mcp.cuecue.cn/api/omni-reader/mcp/";
|
|
11
11
|
export const FOREGROUND_BUDGET_MS = 15_000;
|
|
12
12
|
export const STATUS_LONG_POLL_MAX_MS = 20_000;
|
package/dist/protocol.d.ts
CHANGED
|
@@ -68,9 +68,17 @@ export declare const discardResultSchema: z.ZodObject<{
|
|
|
68
68
|
}, {
|
|
69
69
|
result_id: string;
|
|
70
70
|
}>;
|
|
71
|
+
export declare const saveResultSchema: z.ZodObject<{
|
|
72
|
+
result_id: z.ZodString;
|
|
73
|
+
}, "strict", z.ZodTypeAny, {
|
|
74
|
+
result_id: string;
|
|
75
|
+
}, {
|
|
76
|
+
result_id: string;
|
|
77
|
+
}>;
|
|
71
78
|
export type ParseArguments = z.infer<typeof parseSchema>;
|
|
72
79
|
export type GetParseStatusArguments = z.infer<typeof getParseStatusSchema>;
|
|
73
80
|
export type CancelParseArguments = z.infer<typeof cancelParseSchema>;
|
|
74
81
|
export type ReadResultArguments = z.infer<typeof readResultSchema>;
|
|
75
82
|
export type DiscardResultArguments = z.infer<typeof discardResultSchema>;
|
|
76
83
|
export type ReadOutlineArguments = z.infer<typeof readOutlineSchema>;
|
|
84
|
+
export type SaveResultArguments = z.infer<typeof saveResultSchema>;
|
package/dist/protocol.js
CHANGED
package/dist/remote-client.js
CHANGED
|
@@ -35,9 +35,14 @@ function responseError(response) {
|
|
|
35
35
|
if (response.status === 401) {
|
|
36
36
|
return remoteError({
|
|
37
37
|
code: "API_KEY_INVALID",
|
|
38
|
-
|
|
38
|
+
// A 401 alone does not distinguish "this key value was checked and rejected" from
|
|
39
|
+
// "no key (or a malformed/unexpanded one) was actually sent" -- the client only knows
|
|
40
|
+
// the request was rejected as unauthorized, not why. Asserting the key itself "is
|
|
41
|
+
// invalid or expired" overclaims a diagnosis the client cannot make, and has misled a
|
|
42
|
+
// caller into telling a user to replace a key that was valid but never actually sent.
|
|
43
|
+
message: "Authentication was rejected (HTTP 401). This can mean the Cue API Key itself is invalid or expired, or that a valid key was not actually sent with the request.",
|
|
39
44
|
failureScope: "authentication",
|
|
40
|
-
userAction: "
|
|
45
|
+
userAction: "Confirm a valid Cue API Key is actually being sent with this request (not just configured) before assuming the key value itself is wrong.",
|
|
41
46
|
operationCreated: false,
|
|
42
47
|
retryable: false,
|
|
43
48
|
});
|
|
@@ -609,7 +609,9 @@ export declare const readResultOutputSchema: z.ZodTypeAny;
|
|
|
609
609
|
export declare const readOutlineOutputSchema: z.ZodTypeAny;
|
|
610
610
|
export declare const readOutlineToolOutputSchema: z.ZodTypeAny;
|
|
611
611
|
export declare const discardResultOutputSchema: z.ZodTypeAny;
|
|
612
|
+
export declare const saveResultOutputSchema: z.ZodTypeAny;
|
|
612
613
|
export declare const parseToolOutputSchema: z.ZodTypeAny;
|
|
613
614
|
export declare const readResultToolOutputSchema: z.ZodTypeAny;
|
|
614
615
|
export declare const discardResultToolOutputSchema: z.ZodTypeAny;
|
|
616
|
+
export declare const saveResultToolOutputSchema: z.ZodTypeAny;
|
|
615
617
|
export declare function structuredResult(schema: z.ZodTypeAny, value: unknown): CallToolResult;
|
package/dist/result-contract.js
CHANGED
|
@@ -377,6 +377,23 @@ export const discardResultOutputSchema = z.discriminatedUnion("status", [
|
|
|
377
377
|
.strict(),
|
|
378
378
|
failedResultSchema,
|
|
379
379
|
]);
|
|
380
|
+
export const saveResultOutputSchema = z.discriminatedUnion("status", [
|
|
381
|
+
z
|
|
382
|
+
.object({
|
|
383
|
+
status: z.literal("completed"),
|
|
384
|
+
result_id: resultIdSchema,
|
|
385
|
+
path: z.string().min(1),
|
|
386
|
+
bytes: z.number().int().nonnegative(),
|
|
387
|
+
})
|
|
388
|
+
.strict(),
|
|
389
|
+
z
|
|
390
|
+
.object({
|
|
391
|
+
status: z.literal("unavailable"),
|
|
392
|
+
reason: z.literal("SAVE_NOT_SUPPORTED"),
|
|
393
|
+
})
|
|
394
|
+
.strict(),
|
|
395
|
+
failedResultSchema,
|
|
396
|
+
]);
|
|
380
397
|
export const parseToolOutputSchema = z
|
|
381
398
|
.object({
|
|
382
399
|
status: z.enum([
|
|
@@ -422,6 +439,16 @@ export const discardResultToolOutputSchema = z
|
|
|
422
439
|
error: stableErrorSchema.optional(),
|
|
423
440
|
})
|
|
424
441
|
.strict();
|
|
442
|
+
export const saveResultToolOutputSchema = z
|
|
443
|
+
.object({
|
|
444
|
+
status: z.enum(["completed", "unavailable", "failed"]),
|
|
445
|
+
result_id: resultIdSchema.optional(),
|
|
446
|
+
path: z.string().min(1).optional(),
|
|
447
|
+
bytes: z.number().int().nonnegative().optional(),
|
|
448
|
+
reason: z.literal("SAVE_NOT_SUPPORTED").optional(),
|
|
449
|
+
error: stableErrorSchema.optional(),
|
|
450
|
+
})
|
|
451
|
+
.strict();
|
|
425
452
|
function storageText(result) {
|
|
426
453
|
const parts = result.parts;
|
|
427
454
|
if (parts === null || typeof parts !== "object" || Array.isArray(parts)) {
|
package/dist/tools.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ interface ToolArtifactStore {
|
|
|
18
18
|
readOutline?(resultId: string): Promise<OutlineResult>;
|
|
19
19
|
mintOutlineCursor?(resultId: string, byteOffset: number): Promise<string>;
|
|
20
20
|
discard(resultId: string): Promise<boolean>;
|
|
21
|
+
exportResult?(resultId: string): Promise<{
|
|
22
|
+
path: string;
|
|
23
|
+
bytes: number;
|
|
24
|
+
}>;
|
|
21
25
|
}
|
|
22
26
|
export interface ParseOperationController {
|
|
23
27
|
submitResult(input: {
|
package/dist/tools.js
CHANGED
|
@@ -6,8 +6,8 @@ import { OmniBridgeError } from "./errors.js";
|
|
|
6
6
|
import { hydrateInlineUrlResultSafely } from "./operation-manager.js";
|
|
7
7
|
import { openAllowedFile, } from "./path-security.js";
|
|
8
8
|
import { NOOP_PROGRESS } from "./progress.js";
|
|
9
|
-
import { MACHINE_INSTRUCTIONS, cancelParseSchema, discardResultSchema, getParseStatusSchema, normalizeRepresentation, parseSchema, readOutlineSchema, readResultSchema, } from "./protocol.js";
|
|
10
|
-
import { discardResultOutputSchema, discardResultToolOutputSchema, localResultToResultField, parseResultSchema, parseToolOutputSchema, readOutlineOutputSchema, readOutlineToolOutputSchema, readResultOutputSchema, readResultToolOutputSchema, structuredResult, } from "./result-contract.js";
|
|
9
|
+
import { MACHINE_INSTRUCTIONS, cancelParseSchema, discardResultSchema, getParseStatusSchema, normalizeRepresentation, parseSchema, readOutlineSchema, readResultSchema, saveResultSchema, } from "./protocol.js";
|
|
10
|
+
import { discardResultOutputSchema, discardResultToolOutputSchema, localResultToResultField, parseResultSchema, parseToolOutputSchema, readOutlineOutputSchema, readOutlineToolOutputSchema, readResultOutputSchema, readResultToolOutputSchema, saveResultOutputSchema, saveResultToolOutputSchema, structuredResult, } from "./result-contract.js";
|
|
11
11
|
import { classifySource } from "./source.js";
|
|
12
12
|
import { TaskRuntime } from "./task-runtime.js";
|
|
13
13
|
function bridgeError(code, message, facts = {}) {
|
|
@@ -376,6 +376,26 @@ async function callReadOutline(resultId, nodeId, dependencies) {
|
|
|
376
376
|
return structuredResult(readOutlineOutputSchema, failed(error));
|
|
377
377
|
}
|
|
378
378
|
}
|
|
379
|
+
async function callSaveResult(resultId, dependencies) {
|
|
380
|
+
if (dependencies.artifactStore.exportResult === undefined) {
|
|
381
|
+
return structuredResult(saveResultOutputSchema, {
|
|
382
|
+
status: "unavailable",
|
|
383
|
+
reason: "SAVE_NOT_SUPPORTED",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
const { path: exportPath, bytes } = await dependencies.artifactStore.exportResult(resultId);
|
|
388
|
+
return structuredResult(saveResultOutputSchema, {
|
|
389
|
+
status: "completed",
|
|
390
|
+
result_id: resultId,
|
|
391
|
+
path: exportPath,
|
|
392
|
+
bytes,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
catch (error) {
|
|
396
|
+
return structuredResult(saveResultOutputSchema, failed(error));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
379
399
|
function invalidArgumentsResult(name) {
|
|
380
400
|
const error = failed(bridgeError("INVALID_TOOL_ARGUMENTS", "The Omni tool arguments are invalid."));
|
|
381
401
|
if (name === "read_result")
|
|
@@ -384,6 +404,8 @@ function invalidArgumentsResult(name) {
|
|
|
384
404
|
return structuredResult(discardResultOutputSchema, error);
|
|
385
405
|
if (name === "read_outline")
|
|
386
406
|
return structuredResult(readOutlineOutputSchema, error);
|
|
407
|
+
if (name === "save_result")
|
|
408
|
+
return structuredResult(saveResultOutputSchema, error);
|
|
387
409
|
return structuredResult(parseResultSchema, error);
|
|
388
410
|
}
|
|
389
411
|
async function dispatchTool(name, rawArguments, extra, dependencies) {
|
|
@@ -423,6 +445,12 @@ async function dispatchTool(name, rawArguments, extra, dependencies) {
|
|
|
423
445
|
? callReadOutline(parsed.data.result_id, parsed.data.node_id, dependencies)
|
|
424
446
|
: invalidArgumentsResult(name);
|
|
425
447
|
}
|
|
448
|
+
if (name === "save_result") {
|
|
449
|
+
const parsed = saveResultSchema.safeParse(rawArguments ?? {});
|
|
450
|
+
return parsed.success
|
|
451
|
+
? callSaveResult(parsed.data.result_id, dependencies)
|
|
452
|
+
: invalidArgumentsResult(name);
|
|
453
|
+
}
|
|
426
454
|
return structuredResult(parseResultSchema, failed(bridgeError("TOOL_NOT_FOUND", "The requested Omni tool is not available.")));
|
|
427
455
|
}
|
|
428
456
|
export function registerOmniTools(server, dependencies, taskStore) {
|
|
@@ -514,6 +542,17 @@ export function registerOmniTools(server, dependencies, taskStore) {
|
|
|
514
542
|
openWorldHint: false,
|
|
515
543
|
},
|
|
516
544
|
}, (args) => callReadOutline(args.result_id, args.node_id, dependencies));
|
|
545
|
+
server.registerTool("save_result", {
|
|
546
|
+
description: "Write a local result's complete content to a stable file on disk and return its path. There is no path argument: the file is always named from result_id inside the Bridge cache directory, so this cannot be pointed at an arbitrary location. Calling it again for the same result overwrites the same file.",
|
|
547
|
+
inputSchema: saveResultSchema,
|
|
548
|
+
outputSchema: saveResultToolOutputSchema,
|
|
549
|
+
annotations: {
|
|
550
|
+
readOnlyHint: false,
|
|
551
|
+
destructiveHint: false,
|
|
552
|
+
idempotentHint: true,
|
|
553
|
+
openWorldHint: false,
|
|
554
|
+
},
|
|
555
|
+
}, (args) => callSaveResult(args.result_id, dependencies));
|
|
517
556
|
server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
518
557
|
if (request.params.task !== undefined) {
|
|
519
558
|
if (request.params.name !== "parse") {
|