@cueai/omni-reader-mcp 1.2.1 → 1.3.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 +19 -12
- package/dist/artifact-store.d.ts +3 -0
- package/dist/artifact-store.js +36 -0
- package/dist/cli/agent-config.d.ts +5 -0
- package/dist/cli/agent-config.js +121 -41
- package/dist/cli/doctor.js +5 -9
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/iiis-client.d.ts +1 -1
- package/dist/operation-manager.d.ts +5 -4
- package/dist/operation-manager.js +56 -4
- package/dist/outline.d.ts +15 -0
- package/dist/outline.js +79 -0
- package/dist/protocol.d.ts +11 -0
- package/dist/protocol.js +16 -1
- package/dist/remote-client.js +27 -3
- package/dist/result-contract.d.ts +23 -1
- package/dist/result-contract.js +86 -6
- package/dist/tools.d.ts +3 -0
- package/dist/tools.js +75 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ differ from the numbers above, report the live values.
|
|
|
60
60
|
Always use an audited exact version, never an implicit `latest`:
|
|
61
61
|
|
|
62
62
|
```sh
|
|
63
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
63
|
+
npx -y @cueai/omni-reader-mcp@1.3.0 setup
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
The interactive setup supports Hermes, Cursor, Claude Desktop, and generic stdio
|
|
@@ -68,9 +68,9 @@ configuration. Non-interactive installation uses the same argument parsing and w
|
|
|
68
68
|
logic:
|
|
69
69
|
|
|
70
70
|
```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.
|
|
71
|
+
npx -y @cueai/omni-reader-mcp@1.3.0 setup --client hermes --allowed-root /absolute/minimum/root --yes --json
|
|
72
|
+
npx -y @cueai/omni-reader-mcp@1.3.0 setup --client cursor --add-root /absolute/minimum/root --yes --json
|
|
73
|
+
npx -y @cueai/omni-reader-mcp@1.3.0 setup --client claude-desktop --allowed-root /absolute/minimum/root --yes --json
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
When an agent or script runs under a pty (stdin is still a TTY), declare non-interactive
|
|
@@ -78,7 +78,7 @@ mode explicitly with `--headless` (alias `--non-interactive`): no `--yes` is req
|
|
|
78
78
|
stdin is never read:
|
|
79
79
|
|
|
80
80
|
```sh
|
|
81
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
81
|
+
npx -y @cueai/omni-reader-mcp@1.3.0 setup --client cursor --allowed-root /absolute/minimum/root --headless --json
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
## Cache and journal isolation
|
|
@@ -127,6 +127,7 @@ The public tools are fixed:
|
|
|
127
127
|
- `get_parse_status`
|
|
128
128
|
- `cancel_parse`
|
|
129
129
|
- `read_result`
|
|
130
|
+
- `read_outline`
|
|
130
131
|
- `discard_result`
|
|
131
132
|
|
|
132
133
|
Every tool returns `structuredContent` with a strict `outputSchema`, plus an equivalent
|
|
@@ -138,6 +139,12 @@ fallback for clients that only read legacy MCP `content[].text`:
|
|
|
138
139
|
compact JSON equivalent to the `structuredContent` fields;
|
|
139
140
|
- the `read_result` text JSON contains the current `result.text` and an optional
|
|
140
141
|
`next_cursor`; clients must exhaust all cursors before concatenating the body;
|
|
142
|
+
- `read_outline(result_id)` returns the result's heading tree (from Markdown ATX headings
|
|
143
|
+
or, for a PDF source, its font-size-calibrated heading structure) without returning the
|
|
144
|
+
full body to the caller; `read_outline(result_id, node_id)` mints a `read_result`-compatible cursor
|
|
145
|
+
anchored at that heading, so a long result can be jumped into directly instead of only
|
|
146
|
+
advancing sequentially through `next_cursor`. An empty or absent outline is reported
|
|
147
|
+
explicitly, never silently — it never blocks reading the result itself with `read_result`;
|
|
141
148
|
- the `discard_result` text JSON explicitly returns `discarded`; never claim deletion on
|
|
142
149
|
call success alone.
|
|
143
150
|
|
|
@@ -198,16 +205,16 @@ new source that satisfies the constraints.
|
|
|
198
205
|
## Commands
|
|
199
206
|
|
|
200
207
|
```sh
|
|
201
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
202
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
203
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
204
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
208
|
+
npx -y @cueai/omni-reader-mcp@1.3.0 doctor
|
|
209
|
+
npx -y @cueai/omni-reader-mcp@1.3.0 doctor --json
|
|
210
|
+
npx -y @cueai/omni-reader-mcp@1.3.0 clean
|
|
211
|
+
npx -y @cueai/omni-reader-mcp@1.3.0 uninstall --yes --json
|
|
205
212
|
```
|
|
206
213
|
|
|
207
214
|
Running the pinned version without a command starts the stdio MCP server:
|
|
208
215
|
|
|
209
216
|
```sh
|
|
210
|
-
npx -y @cueai/omni-reader-mcp@1.
|
|
217
|
+
npx -y @cueai/omni-reader-mcp@1.3.0
|
|
211
218
|
```
|
|
212
219
|
|
|
213
220
|
`doctor --json` returns package/npm/client adapter, Key present/absent, allowed-root
|
|
@@ -216,12 +223,12 @@ status; it never prints the Key, private source paths, or content.
|
|
|
216
223
|
|
|
217
224
|
## Uninstall and rollback
|
|
218
225
|
|
|
219
|
-
`uninstall --yes --json` removes only
|
|
226
|
+
`uninstall --yes --json` removes only a trusted 1.2.2 or 1.3.0 Bridge entry; when a
|
|
220
227
|
matching trusted backup exists, it restores the original URL-only `omni-reader` entry.
|
|
221
228
|
Uninstall never deletes user source files and never silently removes unexpired local
|
|
222
229
|
results.
|
|
223
230
|
|
|
224
|
-
To roll back from 1.
|
|
231
|
+
To roll back from 1.3.0:
|
|
225
232
|
|
|
226
233
|
1. stop recommending or installing that version;
|
|
227
234
|
2. run `uninstall --yes --json` to restore the trusted URL-only entry;
|
package/dist/artifact-store.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FileHandle } from "node:fs/promises";
|
|
2
2
|
import type { ReleasedMetadata, ResultRetentionSink, ResultRetentionStart } from "./iiis-client.js";
|
|
3
|
+
import { type OutlineResult } from "./outline.js";
|
|
3
4
|
import { GROUNDING_SCHEMA_VERSION, RESULT_BUNDLE_PROTOCOL_VERSION } from "./protocol.js";
|
|
4
5
|
import { type VerifiedBundle } from "./result-bundle.js";
|
|
5
6
|
export interface ArtifactStoreOptions {
|
|
@@ -109,6 +110,8 @@ export declare class ArtifactStore {
|
|
|
109
110
|
get rootDirectory(): string;
|
|
110
111
|
createRetention(): LocalResultRetention;
|
|
111
112
|
read(resultId: string, cursor?: string, maxBytes?: number): Promise<ArtifactReadResult>;
|
|
113
|
+
readOutline(resultId: string): Promise<OutlineResult>;
|
|
114
|
+
mintOutlineCursor(resultId: string, byteOffset: number): Promise<string>;
|
|
112
115
|
readBundlePart(resultId: string, cursor: string, maxBytes?: number): Promise<BundlePartReadChunk>;
|
|
113
116
|
discard(resultId: string): Promise<boolean>;
|
|
114
117
|
cleanupExpired(): Promise<number>;
|
package/dist/artifact-store.js
CHANGED
|
@@ -7,6 +7,7 @@ import { z } from "zod";
|
|
|
7
7
|
import { ARTIFACT_TTL_MS, INLINE_RESULT_MAX_BYTES, RESULT_CHUNK_MAX_BYTES } from "./constants.js";
|
|
8
8
|
import { CursorCodec } from "./cursor.js";
|
|
9
9
|
import { OmniBridgeError } from "./errors.js";
|
|
10
|
+
import { extractOutline } from "./outline.js";
|
|
10
11
|
import { GROUNDING_SCHEMA_VERSION, RESULT_BUNDLE_PROTOCOL_VERSION } from "./protocol.js";
|
|
11
12
|
// The D2-A source scan (test/protocol.test.ts) rejects any import specifier
|
|
12
13
|
// mentioning the bundle module: it was written when nothing imported it. D2-D
|
|
@@ -535,6 +536,41 @@ export class ArtifactStore {
|
|
|
535
536
|
: {}),
|
|
536
537
|
};
|
|
537
538
|
}
|
|
539
|
+
async readOutline(resultId) {
|
|
540
|
+
this.#requireOpen();
|
|
541
|
+
const metadata = await this.#loadMetadata(resultId);
|
|
542
|
+
if (Date.parse(metadata.expiresAt) <= this.#now().getTime()) {
|
|
543
|
+
await this.discard(resultId);
|
|
544
|
+
throw artifactError("RESULT_EXPIRED", "The local result artifact has expired.");
|
|
545
|
+
}
|
|
546
|
+
const artifactPath = path.join(this.#resultsDirectory, metadata.artifactName);
|
|
547
|
+
const handle = await openArtifactForRead(artifactPath);
|
|
548
|
+
let fullText;
|
|
549
|
+
try {
|
|
550
|
+
const artifactStat = await handle.stat();
|
|
551
|
+
if (!artifactStat.isFile() || artifactStat.size !== metadata.resultBytes) {
|
|
552
|
+
throw artifactError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result artifact is invalid.");
|
|
553
|
+
}
|
|
554
|
+
const slice = await readUtf8Slice(handle, 0, metadata.resultBytes, metadata.resultBytes);
|
|
555
|
+
fullText = slice.text;
|
|
556
|
+
}
|
|
557
|
+
finally {
|
|
558
|
+
await handle.close();
|
|
559
|
+
}
|
|
560
|
+
return extractOutline(fullText);
|
|
561
|
+
}
|
|
562
|
+
async mintOutlineCursor(resultId, byteOffset) {
|
|
563
|
+
this.#requireOpen();
|
|
564
|
+
const metadata = await this.#loadMetadata(resultId);
|
|
565
|
+
if (Date.parse(metadata.expiresAt) <= this.#now().getTime()) {
|
|
566
|
+
await this.discard(resultId);
|
|
567
|
+
throw artifactError("RESULT_EXPIRED", "The local result artifact has expired.");
|
|
568
|
+
}
|
|
569
|
+
if (!Number.isSafeInteger(byteOffset) || byteOffset < 0 || byteOffset > metadata.resultBytes) {
|
|
570
|
+
throw artifactError("INVALID_RESULT_CURSOR", "The outline node offset is invalid.");
|
|
571
|
+
}
|
|
572
|
+
return this.#cursor.encode({ resultId, offset: byteOffset, expiresAt: metadata.expiresAt });
|
|
573
|
+
}
|
|
538
574
|
// Read one UTF-8-safe chunk of one named part of a retained bundle. The
|
|
539
575
|
// opaque v2 cursor binds resultId/part/detail/schema/bundle protocol/
|
|
540
576
|
// offset/expiry; every bound field is re-verified against the closed
|
|
@@ -7,6 +7,10 @@ export interface AgentConfigEnvironment {
|
|
|
7
7
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
8
8
|
}
|
|
9
9
|
export type AgentConfigStatus = "configured" | "not configured" | "invalid or unreadable";
|
|
10
|
+
export interface AgentConfigInspection {
|
|
11
|
+
readonly status: AgentConfigStatus;
|
|
12
|
+
readonly version?: string;
|
|
13
|
+
}
|
|
10
14
|
interface JsonPreviousEntry {
|
|
11
15
|
readonly format: "json";
|
|
12
16
|
readonly value: unknown;
|
|
@@ -40,6 +44,7 @@ export declare function buildAgentEntry(target: AgentTarget, extraRoots: readonl
|
|
|
40
44
|
export declare function prepareAgentConfig(target: AgentTarget, extraRoots: readonly string[], environment: AgentConfigEnvironment): Promise<PreparedAgentConfig>;
|
|
41
45
|
export declare function verifyPreparedAgentConfig(prepared: PreparedAgentConfig): Promise<void>;
|
|
42
46
|
export declare function writePreparedAgentConfig(prepared: PreparedAgentConfig): Promise<void>;
|
|
47
|
+
export declare function inspectAgentConfigDetails(configPath: string, environment?: AgentConfigEnvironment, target?: NativeAgentTarget): Promise<AgentConfigInspection>;
|
|
43
48
|
export declare function inspectAgentConfig(configPath: string, environment?: AgentConfigEnvironment, target?: NativeAgentTarget): Promise<AgentConfigStatus>;
|
|
44
49
|
export declare function detectAgentTargets(environment: AgentConfigEnvironment): Promise<NativeAgentTarget[]>;
|
|
45
50
|
export declare function configContainsOmni(configPath: string): Promise<boolean>;
|
package/dist/cli/agent-config.js
CHANGED
|
@@ -4,7 +4,9 @@ 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
|
+
const PREVIOUS_PACKAGE_SPEC = "@cueai/omni-reader-mcp@1.2.2";
|
|
7
8
|
const LEGACY_PACKAGE_SPEC = "@cueai/omni-reader-mcp";
|
|
9
|
+
const TRUSTED_EXACT_PACKAGE_SPECS = new Set([PREVIOUS_PACKAGE_SPEC, PACKAGE_SPEC]);
|
|
8
10
|
function isRecord(value) {
|
|
9
11
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10
12
|
}
|
|
@@ -543,7 +545,8 @@ function isExpectedOmniEntry(target, value) {
|
|
|
543
545
|
!Array.isArray(args) ||
|
|
544
546
|
args.length !== 2 ||
|
|
545
547
|
args[0] !== "-y" ||
|
|
546
|
-
args[1] !==
|
|
548
|
+
typeof args[1] !== "string" ||
|
|
549
|
+
!TRUSTED_EXACT_PACKAGE_SPECS.has(args[1]))
|
|
547
550
|
return false;
|
|
548
551
|
const keys = Object.keys(value).sort();
|
|
549
552
|
if (value.env === undefined) {
|
|
@@ -579,6 +582,12 @@ function isExpectedOmniEntry(target, value) {
|
|
|
579
582
|
&& keys[1] === "command"
|
|
580
583
|
&& keys[2] === "env";
|
|
581
584
|
}
|
|
585
|
+
function expectedBridgeVersion(target, value) {
|
|
586
|
+
if (!isExpectedOmniEntry(target, value))
|
|
587
|
+
return undefined;
|
|
588
|
+
const packageSpec = value.args[1];
|
|
589
|
+
return packageSpec === PREVIOUS_PACKAGE_SPEC ? "1.2.2" : BRIDGE_RELEASE_VERSION;
|
|
590
|
+
}
|
|
582
591
|
function isLegacyBridgeEntry(target, value) {
|
|
583
592
|
if (!isRecord(value) || value.command !== "npx")
|
|
584
593
|
return false;
|
|
@@ -586,7 +595,8 @@ function isLegacyBridgeEntry(target, value) {
|
|
|
586
595
|
if (!Array.isArray(args) ||
|
|
587
596
|
args.length !== 2 ||
|
|
588
597
|
args[0] !== "-y" ||
|
|
589
|
-
|
|
598
|
+
typeof args[1] !== "string" ||
|
|
599
|
+
(args[1] !== LEGACY_PACKAGE_SPEC && !TRUSTED_EXACT_PACKAGE_SPECS.has(args[1])))
|
|
590
600
|
return false;
|
|
591
601
|
if (value.env === undefined)
|
|
592
602
|
return target !== "hermes";
|
|
@@ -742,6 +752,7 @@ export async function prepareAgentConfig(target, extraRoots, environment) {
|
|
|
742
752
|
environment,
|
|
743
753
|
reload: reloadInstruction("hermes"),
|
|
744
754
|
format,
|
|
755
|
+
serializedBefore: loaded.serialized,
|
|
745
756
|
serializedAfter,
|
|
746
757
|
previousEntry,
|
|
747
758
|
};
|
|
@@ -786,9 +797,11 @@ async function atomicReplace(filePath, content) {
|
|
|
786
797
|
await handle.sync();
|
|
787
798
|
await handle.close();
|
|
788
799
|
handle = undefined;
|
|
800
|
+
await chmod(temporaryPath, 0o600);
|
|
789
801
|
await rename(temporaryPath, filePath);
|
|
790
|
-
|
|
791
|
-
|
|
802
|
+
// Rename is the commit point. Do not throw afterward: callers cannot safely
|
|
803
|
+
// roll back a committed user-config write without racing an external editor.
|
|
804
|
+
await syncDirectory(directory).catch(() => undefined);
|
|
792
805
|
}
|
|
793
806
|
finally {
|
|
794
807
|
await handle?.close();
|
|
@@ -912,6 +925,17 @@ export async function verifyPreparedAgentConfig(prepared) {
|
|
|
912
925
|
throw fileError("The Agent configuration changed after preview; no changes were written.");
|
|
913
926
|
}
|
|
914
927
|
}
|
|
928
|
+
async function restoreTrustedBackup(backupPath, backup) {
|
|
929
|
+
if (backup !== undefined) {
|
|
930
|
+
await atomicReplace(backupPath, `${JSON.stringify(backup, null, 2)}\n`);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
await unlink(backupPath).catch((error) => {
|
|
934
|
+
if (error.code !== "ENOENT")
|
|
935
|
+
throw error;
|
|
936
|
+
});
|
|
937
|
+
await syncDirectory(path.dirname(backupPath));
|
|
938
|
+
}
|
|
915
939
|
export async function writePreparedAgentConfig(prepared) {
|
|
916
940
|
if (prepared.configPath === undefined ||
|
|
917
941
|
prepared.environment === undefined ||
|
|
@@ -919,36 +943,56 @@ export async function writePreparedAgentConfig(prepared) {
|
|
|
919
943
|
prepared.serializedAfter === undefined)
|
|
920
944
|
return;
|
|
921
945
|
await withConfigLock(prepared.configPath, prepared.environment, async () => {
|
|
922
|
-
const
|
|
946
|
+
const configPath = prepared.configPath;
|
|
947
|
+
const current = await readConfig(configPath);
|
|
923
948
|
if (current.fingerprint !== prepared.sourceFingerprint) {
|
|
924
949
|
throw fileError("The Agent configuration changed after preview; no changes were written.");
|
|
925
950
|
}
|
|
926
951
|
const backup = trustedBackup(prepared);
|
|
927
|
-
const backupPath = agentBackupPath(
|
|
928
|
-
const existingBackup = await readTrustedBackup(
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
952
|
+
const backupPath = agentBackupPath(configPath);
|
|
953
|
+
const existingBackup = await readTrustedBackup(configPath);
|
|
954
|
+
let replacementBackup = backup;
|
|
955
|
+
if (existingBackup !== undefined) {
|
|
956
|
+
const currentEntry = expectedConfigEntry(prepared.target, current);
|
|
957
|
+
if (currentEntry !== undefined && existingBackup.target === prepared.target) {
|
|
958
|
+
if (existingBackup.bridge_entry_digest !== entryDigest(currentEntry)) {
|
|
959
|
+
throw fileError("A conflicting trusted Omni backup already exists; no changes were written.");
|
|
960
|
+
}
|
|
961
|
+
replacementBackup = {
|
|
936
962
|
...existingBackup,
|
|
937
963
|
bridge_entry_digest: entryDigest(prepared.entry),
|
|
938
964
|
};
|
|
939
|
-
if (JSON.stringify(updatedBackup) !== JSON.stringify(existingBackup)) {
|
|
940
|
-
await atomicReplace(backupPath, `${JSON.stringify(updatedBackup, null, 2)}\n`);
|
|
941
|
-
}
|
|
942
965
|
}
|
|
943
966
|
else if (JSON.stringify(existingBackup) !== JSON.stringify(backup)) {
|
|
944
967
|
throw fileError("A conflicting trusted Omni backup already exists; no changes were written.");
|
|
945
968
|
}
|
|
969
|
+
else {
|
|
970
|
+
replacementBackup = existingBackup;
|
|
971
|
+
}
|
|
946
972
|
}
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
973
|
+
const backupNeedsWrite = existingBackup === undefined ||
|
|
974
|
+
JSON.stringify(replacementBackup) !== JSON.stringify(existingBackup);
|
|
975
|
+
try {
|
|
976
|
+
if (backupNeedsWrite) {
|
|
977
|
+
await atomicReplace(backupPath, `${JSON.stringify(replacementBackup, null, 2)}\n`);
|
|
978
|
+
}
|
|
979
|
+
const immediatelyBeforeWrite = await readConfig(configPath);
|
|
980
|
+
if (immediatelyBeforeWrite.fingerprint !== current.fingerprint) {
|
|
981
|
+
throw fileError("The Agent configuration changed during setup; no changes were written.");
|
|
982
|
+
}
|
|
983
|
+
await atomicReplace(configPath, prepared.serializedAfter);
|
|
984
|
+
}
|
|
985
|
+
catch (error) {
|
|
986
|
+
try {
|
|
987
|
+
if (backupNeedsWrite) {
|
|
988
|
+
await restoreTrustedBackup(backupPath, existingBackup);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
catch {
|
|
992
|
+
throw fileError("The Agent configuration write failed and trusted rollback state could not be restored.");
|
|
993
|
+
}
|
|
994
|
+
throw error;
|
|
950
995
|
}
|
|
951
|
-
await atomicReplace(prepared.configPath, prepared.serializedAfter);
|
|
952
996
|
});
|
|
953
997
|
}
|
|
954
998
|
function jsonConfigEntry(value) {
|
|
@@ -956,49 +1000,64 @@ function jsonConfigEntry(value) {
|
|
|
956
1000
|
return undefined;
|
|
957
1001
|
return onlyOmniEntry(value.mcpServers).value;
|
|
958
1002
|
}
|
|
959
|
-
|
|
1003
|
+
function expectedConfigEntry(target, loaded) {
|
|
960
1004
|
if (target === "hermes") {
|
|
961
1005
|
const document = parseHermesDocument(loaded.serialized);
|
|
962
1006
|
const entry = hermesOmniEntry(document);
|
|
963
|
-
|
|
1007
|
+
if (entry === undefined)
|
|
1008
|
+
return undefined;
|
|
1009
|
+
const value = parseHermesEntry(entry);
|
|
1010
|
+
return isExpectedOmniEntry(target, value) ? value : undefined;
|
|
964
1011
|
}
|
|
965
1012
|
assertNoDuplicateJsonOmniEntries(loaded.serialized);
|
|
966
|
-
const value = parseJsonConfig(loaded);
|
|
967
|
-
return isExpectedOmniEntry(target,
|
|
1013
|
+
const value = jsonConfigEntry(parseJsonConfig(loaded));
|
|
1014
|
+
return isExpectedOmniEntry(target, value) ? value : undefined;
|
|
968
1015
|
}
|
|
969
|
-
|
|
1016
|
+
async function configHasExpectedEntry(target, loaded) {
|
|
1017
|
+
return expectedConfigEntry(target, loaded) !== undefined;
|
|
1018
|
+
}
|
|
1019
|
+
export async function inspectAgentConfigDetails(configPath, environment, target) {
|
|
970
1020
|
try {
|
|
971
1021
|
if (environment !== undefined)
|
|
972
1022
|
await validateUserConfigPath(configPath, environment);
|
|
973
1023
|
const loaded = await readConfig(configPath);
|
|
974
1024
|
if (!loaded.existed)
|
|
975
|
-
return "not configured";
|
|
1025
|
+
return { status: "not configured" };
|
|
976
1026
|
const resolvedTarget = target ?? (configPath.endsWith("config.yaml") ? "hermes" : "cursor");
|
|
977
1027
|
if (resolvedTarget === "hermes") {
|
|
978
1028
|
const document = parseHermesDocument(loaded.serialized);
|
|
979
1029
|
const entry = hermesOmniEntry(document);
|
|
980
1030
|
if (entry === undefined)
|
|
981
|
-
return "not configured";
|
|
1031
|
+
return { status: "not configured" };
|
|
982
1032
|
const value = parseHermesEntry(entry);
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1033
|
+
const version = expectedBridgeVersion("hermes", value);
|
|
1034
|
+
if (version !== undefined)
|
|
1035
|
+
return { status: "configured", version };
|
|
1036
|
+
return {
|
|
1037
|
+
status: isCanonicalRemoteEntry(value) ? "not configured" : "invalid or unreadable",
|
|
1038
|
+
};
|
|
986
1039
|
}
|
|
987
1040
|
assertNoDuplicateJsonOmniEntries(loaded.serialized);
|
|
988
1041
|
const value = parseJsonConfig(loaded);
|
|
989
1042
|
if (!isRecord(value.mcpServers))
|
|
990
|
-
return "not configured";
|
|
1043
|
+
return { status: "not configured" };
|
|
991
1044
|
const entry = onlyOmniEntry(value.mcpServers).value;
|
|
992
1045
|
if (entry === undefined)
|
|
993
|
-
return "not configured";
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1046
|
+
return { status: "not configured" };
|
|
1047
|
+
const version = expectedBridgeVersion(resolvedTarget, entry);
|
|
1048
|
+
if (version !== undefined)
|
|
1049
|
+
return { status: "configured", version };
|
|
1050
|
+
return {
|
|
1051
|
+
status: isCanonicalRemoteEntry(entry) ? "not configured" : "invalid or unreadable",
|
|
1052
|
+
};
|
|
997
1053
|
}
|
|
998
1054
|
catch {
|
|
999
|
-
return "invalid or unreadable";
|
|
1055
|
+
return { status: "invalid or unreadable" };
|
|
1000
1056
|
}
|
|
1001
1057
|
}
|
|
1058
|
+
export async function inspectAgentConfig(configPath, environment, target) {
|
|
1059
|
+
return (await inspectAgentConfigDetails(configPath, environment, target)).status;
|
|
1060
|
+
}
|
|
1002
1061
|
export async function detectAgentTargets(environment) {
|
|
1003
1062
|
const detected = [];
|
|
1004
1063
|
for (const target of ["hermes", "cursor", "claude-desktop"]) {
|
|
@@ -1056,6 +1115,15 @@ export async function rollbackPreparedAgentConfig(prepared) {
|
|
|
1056
1115
|
if (!current.existed || current.serialized !== prepared.serializedAfter) {
|
|
1057
1116
|
throw fileError("The Agent configuration changed before rollback; automatic rollback stopped.");
|
|
1058
1117
|
}
|
|
1118
|
+
const backupPath = agentBackupPath(prepared.configPath);
|
|
1119
|
+
const backup = await readTrustedBackup(prepared.configPath);
|
|
1120
|
+
const predecessor = prepared.existed
|
|
1121
|
+
? expectedConfigEntry(prepared.target, {
|
|
1122
|
+
existed: true,
|
|
1123
|
+
serialized: prepared.serializedBefore,
|
|
1124
|
+
fingerprint: prepared.sourceFingerprint ?? "unused",
|
|
1125
|
+
})
|
|
1126
|
+
: undefined;
|
|
1059
1127
|
if (prepared.existed) {
|
|
1060
1128
|
await atomicReplace(prepared.configPath, prepared.serializedBefore);
|
|
1061
1129
|
}
|
|
@@ -1063,10 +1131,22 @@ export async function rollbackPreparedAgentConfig(prepared) {
|
|
|
1063
1131
|
await unlink(prepared.configPath);
|
|
1064
1132
|
await syncDirectory(path.dirname(prepared.configPath));
|
|
1065
1133
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1134
|
+
if (backup !== undefined &&
|
|
1135
|
+
predecessor !== undefined &&
|
|
1136
|
+
backup.target === prepared.target &&
|
|
1137
|
+
backup.source_fingerprint !== prepared.sourceFingerprint) {
|
|
1138
|
+
const restoredBackup = {
|
|
1139
|
+
...backup,
|
|
1140
|
+
bridge_entry_digest: entryDigest(predecessor),
|
|
1141
|
+
};
|
|
1142
|
+
await atomicReplace(backupPath, `${JSON.stringify(restoredBackup, null, 2)}\n`);
|
|
1143
|
+
}
|
|
1144
|
+
else {
|
|
1145
|
+
await unlink(backupPath).catch((error) => {
|
|
1146
|
+
if (error.code !== "ENOENT")
|
|
1147
|
+
throw error;
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1070
1150
|
await syncDirectory(path.dirname(prepared.configPath));
|
|
1071
1151
|
});
|
|
1072
1152
|
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { constants as fsConstants } from "node:fs";
|
|
2
2
|
import { lstat, open, readdir } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { DEFAULT_CUBE_BASE_URL, GRANTED_STREAM_PROTOCOL_VERSION, MAX_FILE_BYTES, CUBE_GRANT_PROTOCOL_VERSION, } from "../constants.js";
|
|
5
5
|
import { API_KEY_URL, getOnboardingPolicyWithTimeout, onboardingGuidance, } from "../onboarding-policy.js";
|
|
6
|
-
import { agentConfigPath,
|
|
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
9
|
function isHealthBody(value) {
|
|
@@ -137,13 +137,9 @@ async function clientAdapterFacts(options) {
|
|
|
137
137
|
["claude_desktop", "claude-desktop"],
|
|
138
138
|
]) {
|
|
139
139
|
const configPath = agentConfigPath(target, options);
|
|
140
|
-
|
|
141
|
-
? "not configured"
|
|
142
|
-
: await
|
|
143
|
-
result[label] = {
|
|
144
|
-
status,
|
|
145
|
-
...(status === "configured" ? { version: BRIDGE_RELEASE_VERSION } : {}),
|
|
146
|
-
};
|
|
140
|
+
result[label] = configPath === undefined
|
|
141
|
+
? { status: "not configured" }
|
|
142
|
+
: await inspectAgentConfigDetails(configPath, options, target);
|
|
147
143
|
}
|
|
148
144
|
return result;
|
|
149
145
|
}
|
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.3.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.3.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/iiis-client.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface ResultRetentionStart {
|
|
|
4
4
|
readonly operationId: string;
|
|
5
5
|
readonly resultBytes: number;
|
|
6
6
|
readonly mediaType: string;
|
|
7
|
-
readonly source: "sse" | "recovery";
|
|
7
|
+
readonly source: "sse" | "recovery" | "remote_hydration";
|
|
8
8
|
}
|
|
9
9
|
export interface ReleasedMetadata extends ResultRetentionStart {
|
|
10
10
|
readonly resultDigest: string;
|
|
@@ -5,7 +5,7 @@ import type { JournalPatch, JournalRecord, JournalState, OperationJournal } from
|
|
|
5
5
|
import { type OpenAllowedFileOptions, type OpenedAllowedFile } from "./path-security.js";
|
|
6
6
|
import { type RepresentationIntent } from "./protocol.js";
|
|
7
7
|
import type { RemoteOmniClient } from "./remote-client.js";
|
|
8
|
-
import type
|
|
8
|
+
import { type ParseResult } from "./result-contract.js";
|
|
9
9
|
export interface SubmitOperationInput {
|
|
10
10
|
readonly sourceKind: "local" | "url";
|
|
11
11
|
readonly sourceFacts: Readonly<Record<string, unknown>>;
|
|
@@ -55,11 +55,11 @@ export declare class OperationManager {
|
|
|
55
55
|
status(operationId: string, waitMs?: number, signal?: AbortSignal): Promise<JournalRecord>;
|
|
56
56
|
cancel(operationId: string, signal?: AbortSignal): Promise<JournalRecord>;
|
|
57
57
|
}
|
|
58
|
-
interface LocalRetention extends ResultRetentionSink {
|
|
58
|
+
export interface LocalRetention extends ResultRetentionSink {
|
|
59
59
|
result(): LocalResult;
|
|
60
60
|
bundleResult?(): BundleLocalResult;
|
|
61
61
|
}
|
|
62
|
-
interface LocalArtifactStore {
|
|
62
|
+
export interface LocalArtifactStore {
|
|
63
63
|
createRetention(): LocalRetention;
|
|
64
64
|
read(resultId: string, cursor?: string, maxBytes?: number): Promise<ArtifactReadResult>;
|
|
65
65
|
readBundleDescriptor?(resultId: string): Promise<BundleLocalResult | null>;
|
|
@@ -76,5 +76,6 @@ export interface LocalParseOperationManagerOptions {
|
|
|
76
76
|
readonly now?: () => Date;
|
|
77
77
|
readonly sleep?: (milliseconds: number) => Promise<void>;
|
|
78
78
|
}
|
|
79
|
+
export declare function hydrateInlineUrlResult(result: ParseResult, artifactStore: Pick<LocalArtifactStore, "createRetention">): Promise<ParseResult>;
|
|
80
|
+
export declare function hydrateInlineUrlResultSafely(result: ParseResult, artifactStore: Pick<LocalArtifactStore, "createRetention">): Promise<ParseResult>;
|
|
79
81
|
export declare function createLocalParseOperationManager(options: LocalParseOperationManagerOptions): OperationManager;
|
|
80
|
-
export {};
|
|
@@ -5,6 +5,7 @@ import { LEGACY_RECOVERY_FAILURE_CODE } from "./operation-journal.js";
|
|
|
5
5
|
import { openAllowedFile, } from "./path-security.js";
|
|
6
6
|
import { NOOP_PROGRESS } from "./progress.js";
|
|
7
7
|
import { normalizeRepresentation } from "./protocol.js";
|
|
8
|
+
import { localResultToResultField } from "./result-contract.js";
|
|
8
9
|
const TERMINAL_STATES = new Set([
|
|
9
10
|
"COMPLETED",
|
|
10
11
|
"FAILED",
|
|
@@ -633,6 +634,50 @@ function remoteContext(value) {
|
|
|
633
634
|
}
|
|
634
635
|
return context;
|
|
635
636
|
}
|
|
637
|
+
// A completed (or cleanup_pending -- also directly caller-visible with usable
|
|
638
|
+
// result content, per the local-file path's own existing handling of that
|
|
639
|
+
// status) URL parse's `kind:"inline"` result already carries its full text --
|
|
640
|
+
// hydrating it into Bridge's own local ArtifactStore is a pure local
|
|
641
|
+
// operation (zero extra network I/O) that makes it a completely ordinary
|
|
642
|
+
// local result afterward, so read_result/read_outline work on it exactly as
|
|
643
|
+
// they do for a local-file parse. `kind:"artifact"`/bundle-shaped results
|
|
644
|
+
// are deliberately left untouched -- see this plan's Non-goals.
|
|
645
|
+
export async function hydrateInlineUrlResult(result, artifactStore) {
|
|
646
|
+
if (result.status !== "completed" && result.status !== "cleanup_pending") {
|
|
647
|
+
return result;
|
|
648
|
+
}
|
|
649
|
+
if (result.result === undefined || result.result.kind !== "inline") {
|
|
650
|
+
return result;
|
|
651
|
+
}
|
|
652
|
+
const bytes = new TextEncoder().encode(result.result.text);
|
|
653
|
+
const start = {
|
|
654
|
+
operationId: result.operation_id,
|
|
655
|
+
resultBytes: bytes.byteLength,
|
|
656
|
+
mediaType: "text/markdown; charset=utf-8",
|
|
657
|
+
source: "remote_hydration",
|
|
658
|
+
};
|
|
659
|
+
const retention = artifactStore.createRetention();
|
|
660
|
+
await retention.begin(start);
|
|
661
|
+
await retention.write(bytes);
|
|
662
|
+
await retention.complete({
|
|
663
|
+
...start,
|
|
664
|
+
resultDigest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
|
|
665
|
+
});
|
|
666
|
+
return { ...result, result: localResultToResultField(retention.result()) };
|
|
667
|
+
}
|
|
668
|
+
// hydrateInlineUrlResult is a pure local-storage optimization on top of content the caller
|
|
669
|
+
// already has in full -- a local-disk failure here (e.g. disk full, permission error) must never
|
|
670
|
+
// turn an otherwise-successful URL parse into a failure. This wrapper swallows any hydration
|
|
671
|
+
// error and falls back to the original, un-hydrated result: the caller still gets their content;
|
|
672
|
+
// they just lose local read_result/read_outline re-read capability for that particular result.
|
|
673
|
+
export async function hydrateInlineUrlResultSafely(result, artifactStore) {
|
|
674
|
+
try {
|
|
675
|
+
return await hydrateInlineUrlResult(result, artifactStore);
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
return result;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
636
681
|
function _userFacingMessage(code, result) {
|
|
637
682
|
const error = result.error;
|
|
638
683
|
if (!error.file_uploaded && !error.parser_started) {
|
|
@@ -1419,7 +1464,8 @@ export function createLocalParseOperationManager(options) {
|
|
|
1419
1464
|
}
|
|
1420
1465
|
const context = remoteContext(input.context);
|
|
1421
1466
|
const result = await options.remoteClient.parse(context.source, input.clientRequestId, input.signal ?? new AbortController().signal, representation.detail);
|
|
1422
|
-
|
|
1467
|
+
const hydrated = await hydrateInlineUrlResultSafely(result, options.artifactStore);
|
|
1468
|
+
return remoteResultUpdate(hydrated, recovery?.operationId ?? undefined);
|
|
1423
1469
|
}
|
|
1424
1470
|
const driver = {
|
|
1425
1471
|
create: (input) => input.sourceKind === "url"
|
|
@@ -1434,7 +1480,9 @@ export function createLocalParseOperationManager(options) {
|
|
|
1434
1480
|
if (record.sourceKind === "url") {
|
|
1435
1481
|
if (options.remoteClient === undefined)
|
|
1436
1482
|
return undefined;
|
|
1437
|
-
|
|
1483
|
+
const result = await options.remoteClient.status(record.operationId, waitMs, signal);
|
|
1484
|
+
const hydrated = await hydrateInlineUrlResultSafely(result, options.artifactStore);
|
|
1485
|
+
return remoteResultUpdate(hydrated, record.operationId);
|
|
1438
1486
|
}
|
|
1439
1487
|
const order = STATE_ORDER.get(record.state);
|
|
1440
1488
|
if (order === undefined || order < STATE_ORDER.get("UPLOADING"))
|
|
@@ -1454,7 +1502,9 @@ export function createLocalParseOperationManager(options) {
|
|
|
1454
1502
|
if (record.sourceKind === "url") {
|
|
1455
1503
|
if (options.remoteClient === undefined)
|
|
1456
1504
|
return undefined;
|
|
1457
|
-
|
|
1505
|
+
const result = await options.remoteClient.cancel(record.operationId, signal);
|
|
1506
|
+
const hydrated = await hydrateInlineUrlResultSafely(result, options.artifactStore);
|
|
1507
|
+
return remoteResultUpdate(hydrated, record.operationId);
|
|
1458
1508
|
}
|
|
1459
1509
|
if (record.operationToken === null)
|
|
1460
1510
|
return undefined;
|
|
@@ -1535,7 +1585,9 @@ export function createLocalParseOperationManager(options) {
|
|
|
1535
1585
|
return completedLocalParse(await recoverLocalArtifact(record));
|
|
1536
1586
|
}
|
|
1537
1587
|
if (record.sourceKind === "url" && options.remoteClient !== undefined) {
|
|
1538
|
-
|
|
1588
|
+
const result = await options.remoteClient.status(record.operationId, 0, signal);
|
|
1589
|
+
const hydrated = await hydrateInlineUrlResultSafely(result, options.artifactStore);
|
|
1590
|
+
return hydrated;
|
|
1539
1591
|
}
|
|
1540
1592
|
return undefined;
|
|
1541
1593
|
},
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface OutlineNode {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
readonly level: number;
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly preview: string;
|
|
6
|
+
readonly byteOffset: number;
|
|
7
|
+
}
|
|
8
|
+
export interface OutlineResult {
|
|
9
|
+
readonly coverage: "complete" | "partial" | "none";
|
|
10
|
+
readonly nodes: readonly OutlineNode[];
|
|
11
|
+
}
|
|
12
|
+
export interface ExtractOutlineOptions {
|
|
13
|
+
readonly coverage?: "complete" | "partial";
|
|
14
|
+
}
|
|
15
|
+
export declare function extractOutline(content: string, options?: ExtractOutlineOptions): OutlineResult;
|
package/dist/outline.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// 0-3 leading spaces (CommonMark ATX rule: 4+ leading spaces is a code block, not a heading),
|
|
2
|
+
// 1-6 '#' characters, required whitespace, required non-empty title.
|
|
3
|
+
const ATX_HEADING_PATTERN = /^ {0,3}(#{1,6})\s+(\S.*)$/;
|
|
4
|
+
// Any run of 3+ backticks or tildes is a fence-looking line. Matches CommonMark's exact
|
|
5
|
+
// fence-matching rule: a fence only closes when the closing marker uses the SAME character
|
|
6
|
+
// (backtick vs tilde) as the opening marker, its length is >= the opening marker's length, AND
|
|
7
|
+
// the line -- after trimming BOTH leading and trailing whitespace -- is exactly the marker with
|
|
8
|
+
// nothing else (a closing fence line may still carry the same 0-3 leading spaces and any amount
|
|
9
|
+
// of trailing whitespace that fence lines are always allowed, but may not carry an info string).
|
|
10
|
+
// The character/length/exact-match comparisons are done in plain JS after this regex match (see
|
|
11
|
+
// below); a fence-looking line that does not satisfy all three conditions while already inside a
|
|
12
|
+
// fence is just fence-looking content inside the fence, not a real close. An OPENING fence line
|
|
13
|
+
// has no such restriction -- trailing content there is a legitimate info string (e.g. ```python)
|
|
14
|
+
// and is allowed and ignored.
|
|
15
|
+
const FENCE_PATTERN = /^ {0,3}(`{3,}|~{3,})/;
|
|
16
|
+
const TRAILING_CLOSING_HASHES = /\s+#+\s*$/;
|
|
17
|
+
export function extractOutline(content, options = {}) {
|
|
18
|
+
const lines = content.split("\n");
|
|
19
|
+
const nodes = [];
|
|
20
|
+
let fenceMarker = null;
|
|
21
|
+
let byteOffset = 0;
|
|
22
|
+
let nodeCounter = 0;
|
|
23
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
24
|
+
const line = lines[i];
|
|
25
|
+
const isLastLine = i === lines.length - 1;
|
|
26
|
+
// split("\n") consumes the newline itself; re-add its one byte for every line except a
|
|
27
|
+
// trailing line with no final newline (matches how `content.split("\n")` behaves).
|
|
28
|
+
const lineByteLength = Buffer.byteLength(line, "utf8") + (isLastLine ? 0 : 1);
|
|
29
|
+
const fenceMatch = FENCE_PATTERN.exec(line);
|
|
30
|
+
if (fenceMatch) {
|
|
31
|
+
const marker = fenceMatch[1];
|
|
32
|
+
const char = marker[0];
|
|
33
|
+
const length = marker.length;
|
|
34
|
+
if (fenceMarker === null) {
|
|
35
|
+
fenceMarker = { char, length };
|
|
36
|
+
}
|
|
37
|
+
else if (char === fenceMarker.char && length >= fenceMarker.length && line.trim() === marker) {
|
|
38
|
+
fenceMarker = null;
|
|
39
|
+
}
|
|
40
|
+
// Otherwise: fence-looking line while already inside a fence that doesn't satisfy the
|
|
41
|
+
// closing condition -- just fence-looking content inside the fence, state stays "in fence".
|
|
42
|
+
byteOffset += lineByteLength;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (fenceMarker === null) {
|
|
46
|
+
const match = ATX_HEADING_PATTERN.exec(line);
|
|
47
|
+
if (match) {
|
|
48
|
+
const hashes = match[1];
|
|
49
|
+
const rawTitle = match[2].replace(TRAILING_CLOSING_HASHES, "").trim();
|
|
50
|
+
const title = /^#+$/.test(rawTitle) ? "" : rawTitle;
|
|
51
|
+
if (title) {
|
|
52
|
+
nodeCounter += 1;
|
|
53
|
+
nodes.push({
|
|
54
|
+
id: `node_${String(nodeCounter).padStart(6, "0")}`,
|
|
55
|
+
level: hashes.length,
|
|
56
|
+
title,
|
|
57
|
+
preview: firstLinePreview(lines, i + 1),
|
|
58
|
+
byteOffset,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
byteOffset += lineByteLength;
|
|
64
|
+
}
|
|
65
|
+
const coverage = options.coverage ?? (nodes.length > 0 ? "complete" : "none");
|
|
66
|
+
return { coverage, nodes };
|
|
67
|
+
}
|
|
68
|
+
function firstLinePreview(lines, startIndex, maxChars = 120) {
|
|
69
|
+
for (let i = startIndex; i < lines.length; i += 1) {
|
|
70
|
+
const raw = lines[i];
|
|
71
|
+
const trimmed = raw.trim();
|
|
72
|
+
if (!trimmed)
|
|
73
|
+
continue;
|
|
74
|
+
if (ATX_HEADING_PATTERN.test(raw))
|
|
75
|
+
return ""; // next content is another heading -> no preview
|
|
76
|
+
return trimmed.length > maxChars ? `${trimmed.slice(0, maxChars)}…` : trimmed;
|
|
77
|
+
}
|
|
78
|
+
return "";
|
|
79
|
+
}
|
package/dist/protocol.d.ts
CHANGED
|
@@ -51,6 +51,16 @@ export declare const readResultSchema: z.ZodObject<{
|
|
|
51
51
|
cursor?: string | undefined;
|
|
52
52
|
max_bytes?: number | undefined;
|
|
53
53
|
}>;
|
|
54
|
+
export declare const readOutlineSchema: z.ZodObject<{
|
|
55
|
+
result_id: z.ZodString;
|
|
56
|
+
node_id: z.ZodOptional<z.ZodString>;
|
|
57
|
+
}, "strict", z.ZodTypeAny, {
|
|
58
|
+
result_id: string;
|
|
59
|
+
node_id?: string | undefined;
|
|
60
|
+
}, {
|
|
61
|
+
result_id: string;
|
|
62
|
+
node_id?: string | undefined;
|
|
63
|
+
}>;
|
|
54
64
|
export declare const discardResultSchema: z.ZodObject<{
|
|
55
65
|
result_id: z.ZodString;
|
|
56
66
|
}, "strict", z.ZodTypeAny, {
|
|
@@ -63,3 +73,4 @@ export type GetParseStatusArguments = z.infer<typeof getParseStatusSchema>;
|
|
|
63
73
|
export type CancelParseArguments = z.infer<typeof cancelParseSchema>;
|
|
64
74
|
export type ReadResultArguments = z.infer<typeof readResultSchema>;
|
|
65
75
|
export type DiscardResultArguments = z.infer<typeof discardResultSchema>;
|
|
76
|
+
export type ReadOutlineArguments = z.infer<typeof readOutlineSchema>;
|
package/dist/protocol.js
CHANGED
|
@@ -64,10 +64,25 @@ export const cancelParseSchema = z
|
|
|
64
64
|
export const readResultSchema = z
|
|
65
65
|
.object({
|
|
66
66
|
result_id: resultIdSchema,
|
|
67
|
-
cursor: z
|
|
67
|
+
cursor: z
|
|
68
|
+
.string()
|
|
69
|
+
.min(1)
|
|
70
|
+
.max(2048)
|
|
71
|
+
.describe("Pass the previous result's next_cursor as this cursor.")
|
|
72
|
+
.optional(),
|
|
68
73
|
max_bytes: z.number().int().min(1).max(RESULT_CHUNK_MAX_BYTES).optional(),
|
|
69
74
|
})
|
|
70
75
|
.strict();
|
|
76
|
+
export const readOutlineSchema = z
|
|
77
|
+
.object({
|
|
78
|
+
result_id: resultIdSchema,
|
|
79
|
+
node_id: z
|
|
80
|
+
.string()
|
|
81
|
+
.regex(/^node_[0-9]{6}$/u)
|
|
82
|
+
.describe("From a prior read_outline call's nodes[].id. Mints a read_result-compatible cursor for that section.")
|
|
83
|
+
.optional(),
|
|
84
|
+
})
|
|
85
|
+
.strict();
|
|
71
86
|
export const discardResultSchema = z
|
|
72
87
|
.object({
|
|
73
88
|
result_id: resultIdSchema,
|
package/dist/remote-client.js
CHANGED
|
@@ -186,8 +186,29 @@ function decodeEnvelope(value, requestId) {
|
|
|
186
186
|
throw protocolError();
|
|
187
187
|
}
|
|
188
188
|
const result = envelope.result;
|
|
189
|
+
let candidate = result.structuredContent;
|
|
190
|
+
if (candidate === undefined) {
|
|
191
|
+
const content = result.content;
|
|
192
|
+
if (!Array.isArray(content) || content.length !== 1) {
|
|
193
|
+
throw protocolError();
|
|
194
|
+
}
|
|
195
|
+
const item = content[0];
|
|
196
|
+
if (item === null
|
|
197
|
+
|| typeof item !== "object"
|
|
198
|
+
|| Array.isArray(item)
|
|
199
|
+
|| item.type !== "text"
|
|
200
|
+
|| typeof item.text !== "string") {
|
|
201
|
+
throw protocolError();
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
candidate = JSON.parse(item.text);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
throw protocolError();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
189
210
|
try {
|
|
190
|
-
return parseResultSchema.parse(
|
|
211
|
+
return parseResultSchema.parse(candidate);
|
|
191
212
|
}
|
|
192
213
|
catch {
|
|
193
214
|
throw protocolError();
|
|
@@ -302,7 +323,10 @@ export class HttpRemoteOmniClient {
|
|
|
302
323
|
retryable: false,
|
|
303
324
|
});
|
|
304
325
|
}
|
|
305
|
-
let args = {
|
|
326
|
+
let args = {
|
|
327
|
+
source: classified.source,
|
|
328
|
+
wait: false,
|
|
329
|
+
};
|
|
306
330
|
if (detail === "grounded" || detail === "layout") {
|
|
307
331
|
// D2-D Task 14: the URL profile is obtained and selected BEFORE any
|
|
308
332
|
// non-text tools/call is constructed. Detail is sent only for a valid
|
|
@@ -310,7 +334,7 @@ export class HttpRemoteOmniClient {
|
|
|
310
334
|
// UNSUPPORTED_DETAIL and zero tools/call requests.
|
|
311
335
|
const capabilities = await this.initializeCapabilities(signal);
|
|
312
336
|
selectUrlProfile(capabilities, detail);
|
|
313
|
-
args = { source: classified.source, detail };
|
|
337
|
+
args = { source: classified.source, detail, wait: false };
|
|
314
338
|
}
|
|
315
339
|
return this.#call("parse", args, clientRequestId, signal);
|
|
316
340
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import type {
|
|
3
|
+
import type { LocalResult } from "./artifact-store.js";
|
|
4
|
+
import { type OmniBridgeErrorPayload } from "./errors.js";
|
|
4
5
|
export interface DataHandling {
|
|
5
6
|
processing_copy: "in_use" | "pending" | "deleted";
|
|
6
7
|
temporary_data: "in_use" | "pending" | "deleted";
|
|
@@ -70,11 +71,30 @@ export type ParseResult = {
|
|
|
70
71
|
status: "canceled";
|
|
71
72
|
operation_id: string;
|
|
72
73
|
cleanup_deadline?: string;
|
|
74
|
+
billing?: {
|
|
75
|
+
credits_charged: number;
|
|
76
|
+
credits_remaining: number;
|
|
77
|
+
};
|
|
73
78
|
data_handling: DataHandling;
|
|
74
79
|
} | {
|
|
75
80
|
status: "expired";
|
|
76
81
|
operation_id: string;
|
|
77
82
|
requires_user_confirmation: true;
|
|
83
|
+
billing?: {
|
|
84
|
+
credits_charged: number;
|
|
85
|
+
credits_remaining: number;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
export declare function localResultToResultField(local: LocalResult): {
|
|
89
|
+
kind: "inline";
|
|
90
|
+
text: string;
|
|
91
|
+
} | {
|
|
92
|
+
kind: "artifact";
|
|
93
|
+
result_id: string;
|
|
94
|
+
result_bytes: number;
|
|
95
|
+
expires_at: string;
|
|
96
|
+
preview: string;
|
|
97
|
+
next_cursor: string;
|
|
78
98
|
};
|
|
79
99
|
export declare const stableErrorSchema: z.ZodObject<{
|
|
80
100
|
ok: z.ZodLiteral<false>;
|
|
@@ -586,6 +606,8 @@ export declare const bundlePartReadSchema: z.ZodObject<{
|
|
|
586
606
|
};
|
|
587
607
|
}>;
|
|
588
608
|
export declare const readResultOutputSchema: z.ZodTypeAny;
|
|
609
|
+
export declare const readOutlineOutputSchema: z.ZodTypeAny;
|
|
610
|
+
export declare const readOutlineToolOutputSchema: z.ZodTypeAny;
|
|
589
611
|
export declare const discardResultOutputSchema: z.ZodTypeAny;
|
|
590
612
|
export declare const parseToolOutputSchema: z.ZodTypeAny;
|
|
591
613
|
export declare const readResultToolOutputSchema: z.ZodTypeAny;
|
package/dist/result-contract.js
CHANGED
|
@@ -1,7 +1,45 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { OmniBridgeError } from "./errors.js";
|
|
2
3
|
import { RESULT_BUNDLE_PROTOCOL_VERSION } from "./protocol.js";
|
|
4
|
+
// Maps a retained LocalResult (ArtifactStore's own representation) into the
|
|
5
|
+
// wire ParseResult["result"] shape. Used both for local-file parses
|
|
6
|
+
// (tools.ts's completedLocalResult, which additionally sets its own
|
|
7
|
+
// local-upload-specific operation_id/data_handling) and for hydrated URL
|
|
8
|
+
// parses (operation-manager.ts's hydrateInlineUrlResult, which preserves
|
|
9
|
+
// Cube's own original operation_id/data_handling instead).
|
|
10
|
+
export function localResultToResultField(local) {
|
|
11
|
+
if (local.kind === "inline") {
|
|
12
|
+
return { kind: "inline", text: local.text };
|
|
13
|
+
}
|
|
14
|
+
if (local.nextCursor === undefined) {
|
|
15
|
+
throw new OmniBridgeError({
|
|
16
|
+
code: "LOCAL_RESULT_INTEGRITY_FAILED",
|
|
17
|
+
failureScope: "bridge",
|
|
18
|
+
message: "The local result artifact cursor is missing.",
|
|
19
|
+
operationCreated: true,
|
|
20
|
+
fileUploaded: true,
|
|
21
|
+
parserStarted: true,
|
|
22
|
+
billed: false,
|
|
23
|
+
contentReleased: true,
|
|
24
|
+
retryable: false,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
kind: "artifact",
|
|
29
|
+
result_id: local.resultId,
|
|
30
|
+
result_bytes: local.resultBytes,
|
|
31
|
+
expires_at: local.expiresAt,
|
|
32
|
+
preview: local.preview,
|
|
33
|
+
next_cursor: local.nextCursor,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
3
36
|
const operationIdSchema = z.string().min(1).max(128);
|
|
4
37
|
const resultIdSchema = z.string().regex(/^result_[A-Za-z0-9_-]{16,64}$/u);
|
|
38
|
+
const nextCursorSchema = z
|
|
39
|
+
.string()
|
|
40
|
+
.min(1)
|
|
41
|
+
.max(2048)
|
|
42
|
+
.describe("Pass this value as cursor in the next read_result call.");
|
|
5
43
|
const constraintsSchema = z
|
|
6
44
|
.object({
|
|
7
45
|
max_bytes: z.number().int().nonnegative().optional(),
|
|
@@ -76,7 +114,7 @@ const artifactResultSchema = z
|
|
|
76
114
|
result_bytes: z.number().int().nonnegative(),
|
|
77
115
|
expires_at: z.string().datetime({ offset: true }),
|
|
78
116
|
preview: z.string(),
|
|
79
|
-
next_cursor:
|
|
117
|
+
next_cursor: nextCursorSchema,
|
|
80
118
|
})
|
|
81
119
|
.strict();
|
|
82
120
|
const billingSchema = z
|
|
@@ -105,7 +143,7 @@ const contentStorageSchema = z.discriminatedUnion("kind", [
|
|
|
105
143
|
z
|
|
106
144
|
.object({
|
|
107
145
|
kind: z.literal("artifact"),
|
|
108
|
-
next_cursor:
|
|
146
|
+
next_cursor: nextCursorSchema,
|
|
109
147
|
preview: z.string().optional(),
|
|
110
148
|
})
|
|
111
149
|
.strict(),
|
|
@@ -120,7 +158,7 @@ const groundingStorageSchema = z.discriminatedUnion("kind", [
|
|
|
120
158
|
z
|
|
121
159
|
.object({
|
|
122
160
|
kind: z.literal("artifact"),
|
|
123
|
-
next_cursor:
|
|
161
|
+
next_cursor: nextCursorSchema,
|
|
124
162
|
})
|
|
125
163
|
.strict(),
|
|
126
164
|
]);
|
|
@@ -218,6 +256,7 @@ export const parseResultSchema = z.discriminatedUnion("status", [
|
|
|
218
256
|
status: z.literal("canceled"),
|
|
219
257
|
operation_id: operationIdSchema,
|
|
220
258
|
cleanup_deadline: z.string().datetime({ offset: true }).optional(),
|
|
259
|
+
billing: billingSchema.optional(),
|
|
221
260
|
data_handling: dataHandlingSchema,
|
|
222
261
|
})
|
|
223
262
|
.strict(),
|
|
@@ -226,6 +265,7 @@ export const parseResultSchema = z.discriminatedUnion("status", [
|
|
|
226
265
|
status: z.literal("expired"),
|
|
227
266
|
operation_id: operationIdSchema,
|
|
228
267
|
requires_user_confirmation: z.literal(true),
|
|
268
|
+
billing: billingSchema.optional(),
|
|
229
269
|
})
|
|
230
270
|
.strict(),
|
|
231
271
|
]);
|
|
@@ -235,7 +275,7 @@ const resultChunkSchema = z
|
|
|
235
275
|
result_bytes: z.number().int().nonnegative(),
|
|
236
276
|
expires_at: z.string().datetime({ offset: true }),
|
|
237
277
|
text: z.string(),
|
|
238
|
-
next_cursor:
|
|
278
|
+
next_cursor: nextCursorSchema.optional(),
|
|
239
279
|
})
|
|
240
280
|
.strict();
|
|
241
281
|
// v3 part read (D2-D item 9): closed chunk with part/media type/offset/
|
|
@@ -253,7 +293,7 @@ const bundlePartReadChunkSchema = z
|
|
|
253
293
|
offset: z.number().int().nonnegative(),
|
|
254
294
|
decoded_bytes: z.number().int().nonnegative(),
|
|
255
295
|
text: z.string(),
|
|
256
|
-
next_cursor:
|
|
296
|
+
next_cursor: nextCursorSchema.optional(),
|
|
257
297
|
expires_at: z.string().datetime({ offset: true }),
|
|
258
298
|
})
|
|
259
299
|
.strict(),
|
|
@@ -266,7 +306,7 @@ const bundlePartReadChunkSchema = z
|
|
|
266
306
|
offset: z.number().int().nonnegative(),
|
|
267
307
|
decoded_bytes: z.number().int().nonnegative(),
|
|
268
308
|
text: z.string(),
|
|
269
|
-
next_cursor:
|
|
309
|
+
next_cursor: nextCursorSchema.optional(),
|
|
270
310
|
expires_at: z.string().datetime({ offset: true }),
|
|
271
311
|
})
|
|
272
312
|
.strict(),
|
|
@@ -287,6 +327,46 @@ export const readResultOutputSchema = z.discriminatedUnion("status", [
|
|
|
287
327
|
.strict(),
|
|
288
328
|
failedResultSchema,
|
|
289
329
|
]);
|
|
330
|
+
const outlineNodeSchema = z
|
|
331
|
+
.object({
|
|
332
|
+
id: z.string().regex(/^node_[0-9]{6}$/u),
|
|
333
|
+
level: z.number().int().min(1).max(6),
|
|
334
|
+
title: z.string().min(1).max(512),
|
|
335
|
+
preview: z.string().max(512),
|
|
336
|
+
})
|
|
337
|
+
.strict();
|
|
338
|
+
export const readOutlineOutputSchema = z.discriminatedUnion("status", [
|
|
339
|
+
z
|
|
340
|
+
.object({
|
|
341
|
+
status: z.literal("outline"),
|
|
342
|
+
coverage: z.enum(["complete", "partial", "none"]),
|
|
343
|
+
nodes: z.array(outlineNodeSchema),
|
|
344
|
+
})
|
|
345
|
+
.strict(),
|
|
346
|
+
z
|
|
347
|
+
.object({
|
|
348
|
+
status: z.literal("cursor"),
|
|
349
|
+
cursor: z.string().min(1).max(2048),
|
|
350
|
+
})
|
|
351
|
+
.strict(),
|
|
352
|
+
z
|
|
353
|
+
.object({
|
|
354
|
+
status: z.literal("unavailable"),
|
|
355
|
+
reason: z.literal("OUTLINE_NOT_SUPPORTED"),
|
|
356
|
+
})
|
|
357
|
+
.strict(),
|
|
358
|
+
failedResultSchema,
|
|
359
|
+
]);
|
|
360
|
+
export const readOutlineToolOutputSchema = z
|
|
361
|
+
.object({
|
|
362
|
+
status: z.enum(["outline", "cursor", "unavailable", "failed"]),
|
|
363
|
+
coverage: z.enum(["complete", "partial", "none"]).optional(),
|
|
364
|
+
nodes: z.array(outlineNodeSchema).optional(),
|
|
365
|
+
cursor: z.string().min(1).max(2048).optional(),
|
|
366
|
+
reason: z.literal("OUTLINE_NOT_SUPPORTED").optional(),
|
|
367
|
+
error: stableErrorSchema.optional(),
|
|
368
|
+
})
|
|
369
|
+
.strict();
|
|
290
370
|
export const discardResultOutputSchema = z.discriminatedUnion("status", [
|
|
291
371
|
z
|
|
292
372
|
.object({
|
package/dist/tools.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { ReaderCapabilitiesV1 } from "./capabilities.js";
|
|
|
5
5
|
import { type CubeGrantClient } from "./cube-client.js";
|
|
6
6
|
import type { IiisClient, ResultRetentionSink } from "./iiis-client.js";
|
|
7
7
|
import { type OpenAllowedFileOptions, type OpenedAllowedFile } from "./path-security.js";
|
|
8
|
+
import type { OutlineResult } from "./outline.js";
|
|
8
9
|
import type { RemoteOmniClient } from "./remote-client.js";
|
|
9
10
|
import { type ParseResult } from "./result-contract.js";
|
|
10
11
|
interface ToolRetention extends ResultRetentionSink {
|
|
@@ -14,6 +15,8 @@ interface ToolArtifactStore {
|
|
|
14
15
|
createRetention(): ToolRetention;
|
|
15
16
|
read(resultId: string, cursor?: string, maxBytes?: number): Promise<ArtifactReadResult>;
|
|
16
17
|
readBundlePart?(resultId: string, cursor: string, maxBytes?: number): Promise<BundlePartReadChunk>;
|
|
18
|
+
readOutline?(resultId: string): Promise<OutlineResult>;
|
|
19
|
+
mintOutlineCursor?(resultId: string, byteOffset: number): Promise<string>;
|
|
17
20
|
discard(resultId: string): Promise<boolean>;
|
|
18
21
|
}
|
|
19
22
|
export interface ParseOperationController {
|
package/dist/tools.js
CHANGED
|
@@ -3,10 +3,11 @@ import { CallToolRequestSchema, CancelTaskRequestSchema, ErrorCode, McpError, }
|
|
|
3
3
|
import { selectDirectProfile, selectUrlProfile, } from "./capabilities.js";
|
|
4
4
|
import { createClientRequestId as newClientRequestId, } from "./cube-client.js";
|
|
5
5
|
import { OmniBridgeError } from "./errors.js";
|
|
6
|
+
import { hydrateInlineUrlResultSafely } from "./operation-manager.js";
|
|
6
7
|
import { openAllowedFile, } from "./path-security.js";
|
|
7
8
|
import { NOOP_PROGRESS } from "./progress.js";
|
|
8
|
-
import { MACHINE_INSTRUCTIONS, cancelParseSchema, discardResultSchema, getParseStatusSchema, normalizeRepresentation, parseSchema, readResultSchema, } from "./protocol.js";
|
|
9
|
-
import { discardResultOutputSchema, discardResultToolOutputSchema, parseResultSchema, parseToolOutputSchema, readResultOutputSchema, readResultToolOutputSchema, structuredResult, } from "./result-contract.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";
|
|
10
11
|
import { classifySource } from "./source.js";
|
|
11
12
|
import { TaskRuntime } from "./task-runtime.js";
|
|
12
13
|
function bridgeError(code, message, facts = {}) {
|
|
@@ -51,38 +52,15 @@ function completedLocalResult(local) {
|
|
|
51
52
|
original_source: "unchanged",
|
|
52
53
|
remote_content_retained: false,
|
|
53
54
|
};
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
status: "completed",
|
|
57
|
-
operation_id: local.operationId,
|
|
58
|
-
result: { kind: "inline", text: local.text },
|
|
59
|
-
data_handling: dataHandling,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
if (local.nextCursor === undefined) {
|
|
63
|
-
throw bridgeError("LOCAL_RESULT_INTEGRITY_FAILED", "The local result artifact cursor is missing.", {
|
|
64
|
-
operationCreated: true,
|
|
65
|
-
fileUploaded: true,
|
|
66
|
-
parserStarted: true,
|
|
67
|
-
contentReleased: true,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
55
|
+
const result = localResultToResultField(local);
|
|
70
56
|
return {
|
|
71
57
|
status: "completed",
|
|
72
58
|
operation_id: local.operationId,
|
|
73
|
-
result
|
|
74
|
-
kind: "artifact",
|
|
75
|
-
result_id: local.resultId,
|
|
76
|
-
result_bytes: local.resultBytes,
|
|
77
|
-
expires_at: local.expiresAt,
|
|
78
|
-
preview: local.preview,
|
|
79
|
-
next_cursor: local.nextCursor,
|
|
80
|
-
},
|
|
59
|
+
result,
|
|
81
60
|
data_handling: dataHandling,
|
|
82
|
-
|
|
83
|
-
expires_at: local.expiresAt,
|
|
84
|
-
|
|
85
|
-
},
|
|
61
|
+
...(result.kind === "artifact"
|
|
62
|
+
? { local_result_cache: { expires_at: local.expiresAt, discard_action: "discard_result" } }
|
|
63
|
+
: {}),
|
|
86
64
|
};
|
|
87
65
|
}
|
|
88
66
|
async function parseLocal(args, clientRequestId, signal, progress, dependencies) {
|
|
@@ -229,7 +207,8 @@ async function parseValue(args, signal, progress, dependencies) {
|
|
|
229
207
|
if (dependencies.remoteClient === undefined) {
|
|
230
208
|
throw bridgeError("REMOTE_PARSE_UNAVAILABLE", "Remote URL parsing is not available in this Bridge build.", { retryable: true });
|
|
231
209
|
}
|
|
232
|
-
|
|
210
|
+
const remoteResult = await dependencies.remoteClient.parse(source.source, clientRequestId, signal);
|
|
211
|
+
return await hydrateInlineUrlResultSafely(remoteResult, dependencies.artifactStore);
|
|
233
212
|
}
|
|
234
213
|
return await parseLocal(args, clientRequestId, signal, progress, dependencies);
|
|
235
214
|
}
|
|
@@ -256,7 +235,8 @@ async function statusValue(operationId, waitMs, signal, dependencies) {
|
|
|
256
235
|
}
|
|
257
236
|
}
|
|
258
237
|
if (dependencies.remoteClient !== undefined) {
|
|
259
|
-
|
|
238
|
+
const remoteResult = await dependencies.remoteClient.status(operationId, waitMs, signal);
|
|
239
|
+
return await hydrateInlineUrlResultSafely(remoteResult, dependencies.artifactStore);
|
|
260
240
|
}
|
|
261
241
|
throw bridgeError("OPERATION_NOT_FOUND", "The requested parse operation is not available.", { operationCreated: true });
|
|
262
242
|
}
|
|
@@ -283,7 +263,8 @@ async function cancelValue(operationId, signal, dependencies) {
|
|
|
283
263
|
}
|
|
284
264
|
}
|
|
285
265
|
if (dependencies.remoteClient !== undefined) {
|
|
286
|
-
|
|
266
|
+
const remoteResult = await dependencies.remoteClient.cancel(operationId, signal);
|
|
267
|
+
return await hydrateInlineUrlResultSafely(remoteResult, dependencies.artifactStore);
|
|
287
268
|
}
|
|
288
269
|
throw bridgeError("OPERATION_NOT_FOUND", "The requested parse operation is not available.", { operationCreated: true });
|
|
289
270
|
}
|
|
@@ -354,12 +335,55 @@ async function callDiscardResult(resultId, dependencies) {
|
|
|
354
335
|
return structuredResult(discardResultOutputSchema, failed(error));
|
|
355
336
|
}
|
|
356
337
|
}
|
|
338
|
+
async function callReadOutline(resultId, nodeId, dependencies) {
|
|
339
|
+
if (dependencies.artifactStore.readOutline === undefined
|
|
340
|
+
|| dependencies.artifactStore.mintOutlineCursor === undefined) {
|
|
341
|
+
return structuredResult(readOutlineOutputSchema, {
|
|
342
|
+
status: "unavailable",
|
|
343
|
+
reason: "OUTLINE_NOT_SUPPORTED",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
const outline = await dependencies.artifactStore.readOutline(resultId);
|
|
348
|
+
if (nodeId === undefined) {
|
|
349
|
+
return structuredResult(readOutlineOutputSchema, {
|
|
350
|
+
status: "outline",
|
|
351
|
+
coverage: outline.coverage,
|
|
352
|
+
// Strip byteOffset: it is an internal detail the caller mints a cursor for,
|
|
353
|
+
// never a value it reads or supplies back directly.
|
|
354
|
+
nodes: outline.nodes.map((node) => ({
|
|
355
|
+
id: node.id,
|
|
356
|
+
level: node.level,
|
|
357
|
+
title: node.title,
|
|
358
|
+
preview: node.preview,
|
|
359
|
+
})),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
const node = outline.nodes.find((candidate) => candidate.id === nodeId);
|
|
363
|
+
if (node === undefined) {
|
|
364
|
+
throw bridgeError("OUTLINE_NODE_NOT_FOUND", "The requested outline node does not exist.", {
|
|
365
|
+
operationCreated: true,
|
|
366
|
+
fileUploaded: true,
|
|
367
|
+
parserStarted: true,
|
|
368
|
+
billed: true,
|
|
369
|
+
contentReleased: true,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
const cursor = await dependencies.artifactStore.mintOutlineCursor(resultId, node.byteOffset);
|
|
373
|
+
return structuredResult(readOutlineOutputSchema, { status: "cursor", cursor });
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
return structuredResult(readOutlineOutputSchema, failed(error));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
357
379
|
function invalidArgumentsResult(name) {
|
|
358
380
|
const error = failed(bridgeError("INVALID_TOOL_ARGUMENTS", "The Omni tool arguments are invalid."));
|
|
359
381
|
if (name === "read_result")
|
|
360
382
|
return structuredResult(readResultOutputSchema, error);
|
|
361
383
|
if (name === "discard_result")
|
|
362
384
|
return structuredResult(discardResultOutputSchema, error);
|
|
385
|
+
if (name === "read_outline")
|
|
386
|
+
return structuredResult(readOutlineOutputSchema, error);
|
|
363
387
|
return structuredResult(parseResultSchema, error);
|
|
364
388
|
}
|
|
365
389
|
async function dispatchTool(name, rawArguments, extra, dependencies) {
|
|
@@ -393,6 +417,12 @@ async function dispatchTool(name, rawArguments, extra, dependencies) {
|
|
|
393
417
|
? callDiscardResult(parsed.data.result_id, dependencies)
|
|
394
418
|
: invalidArgumentsResult(name);
|
|
395
419
|
}
|
|
420
|
+
if (name === "read_outline") {
|
|
421
|
+
const parsed = readOutlineSchema.safeParse(rawArguments ?? {});
|
|
422
|
+
return parsed.success
|
|
423
|
+
? callReadOutline(parsed.data.result_id, parsed.data.node_id, dependencies)
|
|
424
|
+
: invalidArgumentsResult(name);
|
|
425
|
+
}
|
|
396
426
|
return structuredResult(parseResultSchema, failed(bridgeError("TOOL_NOT_FOUND", "The requested Omni tool is not available.")));
|
|
397
427
|
}
|
|
398
428
|
export function registerOmniTools(server, dependencies, taskStore) {
|
|
@@ -452,7 +482,7 @@ export function registerOmniTools(server, dependencies, taskStore) {
|
|
|
452
482
|
},
|
|
453
483
|
}, (args, extra) => callCancel(args.operation_id, extra, dependencies));
|
|
454
484
|
server.registerTool("read_result", {
|
|
455
|
-
description: "Read one UTF-8 chunk from a Bridge-created local result artifact.",
|
|
485
|
+
description: "Read one UTF-8 chunk from a Bridge-created local result artifact. Pass each returned next_cursor as cursor in the next call until it is absent.",
|
|
456
486
|
inputSchema: readResultSchema,
|
|
457
487
|
outputSchema: readResultToolOutputSchema,
|
|
458
488
|
annotations: {
|
|
@@ -473,6 +503,17 @@ export function registerOmniTools(server, dependencies, taskStore) {
|
|
|
473
503
|
openWorldHint: false,
|
|
474
504
|
},
|
|
475
505
|
}, (args) => callDiscardResult(args.result_id, dependencies));
|
|
506
|
+
server.registerTool("read_outline", {
|
|
507
|
+
description: "Return a local result's heading outline. Pass node_id from a returned node's id to mint a read_result-compatible cursor that jumps straight to that section, instead of reading sequentially from the start.",
|
|
508
|
+
inputSchema: readOutlineSchema,
|
|
509
|
+
outputSchema: readOutlineToolOutputSchema,
|
|
510
|
+
annotations: {
|
|
511
|
+
readOnlyHint: true,
|
|
512
|
+
destructiveHint: false,
|
|
513
|
+
idempotentHint: true,
|
|
514
|
+
openWorldHint: false,
|
|
515
|
+
},
|
|
516
|
+
}, (args) => callReadOutline(args.result_id, args.node_id, dependencies));
|
|
476
517
|
server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
477
518
|
if (request.params.task !== undefined) {
|
|
478
519
|
if (request.params.name !== "parse") {
|