@cueai/omni-reader-mcp 1.0.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 +49 -0
- package/dist/artifact-store.d.ts +65 -0
- package/dist/artifact-store.js +720 -0
- package/dist/cli/agent-config.d.ts +28 -0
- package/dist/cli/agent-config.js +335 -0
- package/dist/cli/clean.d.ts +9 -0
- package/dist/cli/clean.js +93 -0
- package/dist/cli/doctor.d.ts +17 -0
- package/dist/cli/doctor.js +157 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +59 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +7 -0
- package/dist/cube-client.d.ts +48 -0
- package/dist/cube-client.js +161 -0
- package/dist/cursor.d.ts +14 -0
- package/dist/cursor.js +101 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +26 -0
- package/dist/iiis-client.d.ts +43 -0
- package/dist/iiis-client.js +694 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +174 -0
- package/dist/multipart-body.d.ts +12 -0
- package/dist/multipart-body.js +90 -0
- package/dist/operation-journal.d.ts +28 -0
- package/dist/operation-journal.js +351 -0
- package/dist/path-security.d.ts +22 -0
- package/dist/path-security.js +240 -0
- package/dist/progress.d.ts +4 -0
- package/dist/progress.js +3 -0
- package/dist/protocol.d.ts +39 -0
- package/dist/protocol.js +27 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +10 -0
- package/dist/tools.d.ts +24 -0
- package/dist/tools.js +224 -0
- package/package.json +35 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type AgentTarget = "cursor" | "claude-desktop" | "generic";
|
|
2
|
+
export interface AgentConfigEnvironment {
|
|
3
|
+
readonly homeDirectory: string;
|
|
4
|
+
readonly platform: NodeJS.Platform;
|
|
5
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
6
|
+
}
|
|
7
|
+
export type AgentConfigStatus = "configured" | "not configured" | "invalid or unreadable";
|
|
8
|
+
export interface PreparedAgentConfig {
|
|
9
|
+
readonly target: AgentTarget;
|
|
10
|
+
readonly configPath?: string;
|
|
11
|
+
readonly displayPath: string;
|
|
12
|
+
readonly existed: boolean;
|
|
13
|
+
readonly before: Record<string, unknown>;
|
|
14
|
+
readonly after: Record<string, unknown>;
|
|
15
|
+
readonly entry: Record<string, unknown>;
|
|
16
|
+
readonly sourceFingerprint?: string;
|
|
17
|
+
readonly environment?: AgentConfigEnvironment;
|
|
18
|
+
}
|
|
19
|
+
export declare function parseAgentTarget(value: string): AgentTarget;
|
|
20
|
+
export declare function agentConfigPath(target: AgentTarget, environment: AgentConfigEnvironment): string | undefined;
|
|
21
|
+
export declare function displayConfigPath(configPath: string, environment: AgentConfigEnvironment): string;
|
|
22
|
+
export declare function buildAgentEntry(extraRoots: readonly string[], platform: NodeJS.Platform): Record<string, unknown>;
|
|
23
|
+
export declare function prepareAgentConfig(target: AgentTarget, extraRoots: readonly string[], environment: AgentConfigEnvironment): Promise<PreparedAgentConfig>;
|
|
24
|
+
export declare function verifyPreparedAgentConfig(prepared: PreparedAgentConfig): Promise<void>;
|
|
25
|
+
export declare function writePreparedAgentConfig(prepared: PreparedAgentConfig): Promise<void>;
|
|
26
|
+
export declare function inspectAgentConfig(configPath: string, environment?: AgentConfigEnvironment): Promise<AgentConfigStatus>;
|
|
27
|
+
export declare function detectAgentTargets(environment: AgentConfigEnvironment): Promise<Array<Exclude<AgentTarget, "generic">>>;
|
|
28
|
+
export declare function configContainsOmni(configPath: string): Promise<boolean>;
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
3
|
+
import { chmod, lstat, mkdir, open, rename, } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function fileError(message) {
|
|
9
|
+
const error = new Error(message);
|
|
10
|
+
error.name = "AgentConfigError";
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
function pathModule(platform) {
|
|
14
|
+
return platform === "win32" ? path.win32 : path.posix;
|
|
15
|
+
}
|
|
16
|
+
export function parseAgentTarget(value) {
|
|
17
|
+
const normalized = value.trim().toLowerCase();
|
|
18
|
+
if (normalized === "cursor")
|
|
19
|
+
return "cursor";
|
|
20
|
+
if (normalized === "claude desktop" || normalized === "claude" || normalized === "claude-desktop") {
|
|
21
|
+
return "claude-desktop";
|
|
22
|
+
}
|
|
23
|
+
return "generic";
|
|
24
|
+
}
|
|
25
|
+
export function agentConfigPath(target, environment) {
|
|
26
|
+
const paths = pathModule(environment.platform);
|
|
27
|
+
if (target === "cursor") {
|
|
28
|
+
return paths.join(environment.homeDirectory, ".cursor", "mcp.json");
|
|
29
|
+
}
|
|
30
|
+
if (target === "claude-desktop") {
|
|
31
|
+
if (environment.platform === "darwin") {
|
|
32
|
+
return paths.join(environment.homeDirectory, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
33
|
+
}
|
|
34
|
+
if (environment.platform === "win32") {
|
|
35
|
+
const appData = environment.env?.APPDATA?.trim()
|
|
36
|
+
|| paths.join(environment.homeDirectory, "AppData", "Roaming");
|
|
37
|
+
return paths.join(appData, "Claude", "claude_desktop_config.json");
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
export function displayConfigPath(configPath, environment) {
|
|
44
|
+
const paths = pathModule(environment.platform);
|
|
45
|
+
const relative = paths.relative(environment.homeDirectory, configPath);
|
|
46
|
+
if (relative !== "" && relative !== ".." && !relative.startsWith(`..${paths.sep}`)) {
|
|
47
|
+
return `~/${relative.split(paths.sep).join("/")}`;
|
|
48
|
+
}
|
|
49
|
+
return configPath;
|
|
50
|
+
}
|
|
51
|
+
export function buildAgentEntry(extraRoots, platform) {
|
|
52
|
+
const entry = {
|
|
53
|
+
command: "npx",
|
|
54
|
+
args: ["-y", "@cue/omni-reader-mcp"],
|
|
55
|
+
};
|
|
56
|
+
if (extraRoots.length > 0) {
|
|
57
|
+
entry.env = {
|
|
58
|
+
OMNI_ALLOWED_ROOTS: extraRoots.join(platform === "win32" ? ";" : ":"),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return entry;
|
|
62
|
+
}
|
|
63
|
+
function containsPath(parent, child, paths) {
|
|
64
|
+
const relative = paths.relative(parent, child);
|
|
65
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${paths.sep}`));
|
|
66
|
+
}
|
|
67
|
+
async function validateUserConfigPath(configPath, environment) {
|
|
68
|
+
const paths = pathModule(environment.platform);
|
|
69
|
+
const homeDirectory = paths.resolve(environment.homeDirectory);
|
|
70
|
+
const userRoots = [homeDirectory];
|
|
71
|
+
if (environment.platform === "win32" && environment.env?.APPDATA?.trim()) {
|
|
72
|
+
userRoots.push(paths.resolve(environment.env.APPDATA));
|
|
73
|
+
}
|
|
74
|
+
const resolvedConfigPath = paths.resolve(configPath);
|
|
75
|
+
const userRoot = userRoots.find((root) => containsPath(root, resolvedConfigPath, paths));
|
|
76
|
+
if (userRoot === undefined) {
|
|
77
|
+
throw fileError("The Agent configuration path is outside the user profile; no changes were written.");
|
|
78
|
+
}
|
|
79
|
+
const relativeParent = paths.relative(userRoot, paths.dirname(resolvedConfigPath));
|
|
80
|
+
const components = relativeParent === "" ? [] : relativeParent.split(paths.sep);
|
|
81
|
+
let current = userRoot;
|
|
82
|
+
for (const component of ["", ...components]) {
|
|
83
|
+
if (component !== "")
|
|
84
|
+
current = paths.join(current, component);
|
|
85
|
+
try {
|
|
86
|
+
const details = await lstat(current);
|
|
87
|
+
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
88
|
+
throw fileError("The Agent configuration path contains an unsafe directory; no changes were written.");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
if (error.code === "ENOENT")
|
|
93
|
+
break;
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const details = await lstat(resolvedConfigPath);
|
|
99
|
+
if (!details.isFile() || details.isSymbolicLink()) {
|
|
100
|
+
throw fileError("The existing Agent configuration is unsafe; no changes were written.");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error.code !== "ENOENT")
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function configFingerprint(loaded) {
|
|
109
|
+
if (!loaded.existed || loaded.serialized === undefined)
|
|
110
|
+
return "missing";
|
|
111
|
+
return `sha256:${createHash("sha256").update(loaded.serialized).digest("hex")}`;
|
|
112
|
+
}
|
|
113
|
+
async function readConfig(configPath) {
|
|
114
|
+
let handle;
|
|
115
|
+
try {
|
|
116
|
+
handle = await open(configPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
const code = error.code;
|
|
120
|
+
if (code === "ENOENT")
|
|
121
|
+
return { existed: false, value: {} };
|
|
122
|
+
if (code === "ELOOP" || code === "EMLINK") {
|
|
123
|
+
throw fileError("The existing Agent configuration is unsafe; no changes were written.");
|
|
124
|
+
}
|
|
125
|
+
throw fileError("The existing Agent configuration could not be read.");
|
|
126
|
+
}
|
|
127
|
+
let serialized;
|
|
128
|
+
try {
|
|
129
|
+
const details = await handle.stat();
|
|
130
|
+
if (!details.isFile()) {
|
|
131
|
+
throw fileError("The existing Agent configuration is unsafe; no changes were written.");
|
|
132
|
+
}
|
|
133
|
+
serialized = await handle.readFile("utf8");
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
await handle.close();
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(serialized);
|
|
140
|
+
if (!isRecord(parsed))
|
|
141
|
+
throw new Error("configuration is not an object");
|
|
142
|
+
return { existed: true, value: parsed, serialized };
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw fileError("The existing Agent configuration is malformed JSON; no changes were written.");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export async function prepareAgentConfig(target, extraRoots, environment) {
|
|
149
|
+
const configPath = agentConfigPath(target, environment);
|
|
150
|
+
const entry = buildAgentEntry(extraRoots, environment.platform);
|
|
151
|
+
if (configPath === undefined) {
|
|
152
|
+
return {
|
|
153
|
+
target: "generic",
|
|
154
|
+
displayPath: "Generic user-scope configuration",
|
|
155
|
+
existed: false,
|
|
156
|
+
before: {},
|
|
157
|
+
after: { mcpServers: { "omni-reader": entry } },
|
|
158
|
+
entry,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
await validateUserConfigPath(configPath, environment);
|
|
162
|
+
const loaded = await readConfig(configPath);
|
|
163
|
+
const existingServers = loaded.value.mcpServers;
|
|
164
|
+
if (existingServers !== undefined && !isRecord(existingServers)) {
|
|
165
|
+
throw fileError("The existing mcpServers setting is not an object; no changes were written.");
|
|
166
|
+
}
|
|
167
|
+
const after = {
|
|
168
|
+
...loaded.value,
|
|
169
|
+
mcpServers: {
|
|
170
|
+
...(existingServers ?? {}),
|
|
171
|
+
"omni-reader": entry,
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
target,
|
|
176
|
+
configPath,
|
|
177
|
+
displayPath: displayConfigPath(configPath, environment),
|
|
178
|
+
existed: loaded.existed,
|
|
179
|
+
before: loaded.value,
|
|
180
|
+
after,
|
|
181
|
+
entry,
|
|
182
|
+
sourceFingerprint: configFingerprint(loaded),
|
|
183
|
+
environment,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async function syncDirectory(directory) {
|
|
187
|
+
let handle;
|
|
188
|
+
try {
|
|
189
|
+
handle = await open(directory, "r");
|
|
190
|
+
await handle.sync();
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
const code = error.code;
|
|
194
|
+
if (code !== "EINVAL" && code !== "ENOTSUP" && code !== "EISDIR")
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
await handle?.close();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function atomicReplace(filePath, content) {
|
|
202
|
+
const directory = path.dirname(filePath);
|
|
203
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
204
|
+
await chmod(directory, 0o700);
|
|
205
|
+
const temporaryPath = path.join(directory, `.${path.basename(filePath)}.${randomBytes(18).toString("base64url")}.tmp`);
|
|
206
|
+
let handle;
|
|
207
|
+
try {
|
|
208
|
+
handle = await open(temporaryPath, "wx", 0o600);
|
|
209
|
+
await handle.writeFile(content, "utf8");
|
|
210
|
+
await handle.sync();
|
|
211
|
+
await handle.close();
|
|
212
|
+
handle = undefined;
|
|
213
|
+
await rename(temporaryPath, filePath);
|
|
214
|
+
await chmod(filePath, 0o600);
|
|
215
|
+
await syncDirectory(directory);
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
await handle?.close();
|
|
219
|
+
await import("node:fs/promises").then(({ unlink }) => unlink(temporaryPath).catch(() => undefined));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function withConfigLock(configPath, action) {
|
|
223
|
+
const directory = path.dirname(configPath);
|
|
224
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
225
|
+
await chmod(directory, 0o700);
|
|
226
|
+
const lockPath = `${configPath}.omni-reader.lock`;
|
|
227
|
+
let handle;
|
|
228
|
+
try {
|
|
229
|
+
handle = await open(lockPath, "wx", 0o600);
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
if (error.code === "EEXIST") {
|
|
233
|
+
throw fileError("Another Omni setup is updating this Agent configuration; retry after it finishes.");
|
|
234
|
+
}
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
await handle.writeFile(`${process.pid}\n`, "utf8");
|
|
239
|
+
await handle.sync();
|
|
240
|
+
return await action();
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
await handle.close();
|
|
244
|
+
await import("node:fs/promises").then(({ unlink }) => unlink(lockPath).catch(() => undefined));
|
|
245
|
+
await syncDirectory(directory);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
export async function verifyPreparedAgentConfig(prepared) {
|
|
249
|
+
if (prepared.configPath === undefined
|
|
250
|
+
|| prepared.environment === undefined
|
|
251
|
+
|| prepared.sourceFingerprint === undefined)
|
|
252
|
+
return;
|
|
253
|
+
await validateUserConfigPath(prepared.configPath, prepared.environment);
|
|
254
|
+
const current = await readConfig(prepared.configPath);
|
|
255
|
+
if (configFingerprint(current) !== prepared.sourceFingerprint) {
|
|
256
|
+
throw fileError("The Agent configuration changed after preview; no changes were written.");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
export async function writePreparedAgentConfig(prepared) {
|
|
260
|
+
if (prepared.configPath === undefined)
|
|
261
|
+
return;
|
|
262
|
+
await withConfigLock(prepared.configPath, async () => {
|
|
263
|
+
await verifyPreparedAgentConfig(prepared);
|
|
264
|
+
const current = await readConfig(prepared.configPath);
|
|
265
|
+
if (prepared.existed && current.serialized !== undefined) {
|
|
266
|
+
await atomicReplace(`${prepared.configPath}.bak`, current.serialized);
|
|
267
|
+
}
|
|
268
|
+
await atomicReplace(prepared.configPath, `${JSON.stringify(prepared.after, null, 2)}\n`);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function isExpectedOmniEntry(value) {
|
|
272
|
+
if (!isRecord(value))
|
|
273
|
+
return false;
|
|
274
|
+
const keys = Object.keys(value).sort();
|
|
275
|
+
const args = value.args;
|
|
276
|
+
if (value.command !== "npx"
|
|
277
|
+
|| !Array.isArray(args)
|
|
278
|
+
|| args.length !== 2
|
|
279
|
+
|| args[0] !== "-y"
|
|
280
|
+
|| args[1] !== "@cue/omni-reader-mcp")
|
|
281
|
+
return false;
|
|
282
|
+
if (value.env === undefined) {
|
|
283
|
+
return keys.length === 2 && keys[0] === "args" && keys[1] === "command";
|
|
284
|
+
}
|
|
285
|
+
if (!isRecord(value.env))
|
|
286
|
+
return false;
|
|
287
|
+
const envKeys = Object.keys(value.env);
|
|
288
|
+
return keys.length === 3
|
|
289
|
+
&& keys[0] === "args"
|
|
290
|
+
&& keys[1] === "command"
|
|
291
|
+
&& keys[2] === "env"
|
|
292
|
+
&& envKeys.length === 1
|
|
293
|
+
&& envKeys[0] === "OMNI_ALLOWED_ROOTS"
|
|
294
|
+
&& typeof value.env.OMNI_ALLOWED_ROOTS === "string"
|
|
295
|
+
&& value.env.OMNI_ALLOWED_ROOTS.length > 0;
|
|
296
|
+
}
|
|
297
|
+
export async function inspectAgentConfig(configPath, environment) {
|
|
298
|
+
try {
|
|
299
|
+
if (environment !== undefined)
|
|
300
|
+
await validateUserConfigPath(configPath, environment);
|
|
301
|
+
const loaded = await readConfig(configPath);
|
|
302
|
+
if (!loaded.existed)
|
|
303
|
+
return "not configured";
|
|
304
|
+
if (!isRecord(loaded.value.mcpServers))
|
|
305
|
+
return "not configured";
|
|
306
|
+
const entry = loaded.value.mcpServers["omni-reader"];
|
|
307
|
+
if (entry === undefined)
|
|
308
|
+
return "not configured";
|
|
309
|
+
return isExpectedOmniEntry(entry) ? "configured" : "invalid or unreadable";
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return "invalid or unreadable";
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export async function detectAgentTargets(environment) {
|
|
316
|
+
const detected = [];
|
|
317
|
+
for (const target of ["cursor", "claude-desktop"]) {
|
|
318
|
+
const configPath = agentConfigPath(target, environment);
|
|
319
|
+
if (configPath === undefined)
|
|
320
|
+
continue;
|
|
321
|
+
try {
|
|
322
|
+
await validateUserConfigPath(configPath, environment);
|
|
323
|
+
const details = await lstat(configPath);
|
|
324
|
+
if (details.isFile() && !details.isSymbolicLink())
|
|
325
|
+
detected.push(target);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// Unsafe or absent candidates are not advertised as detected.
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return detected;
|
|
332
|
+
}
|
|
333
|
+
export async function configContainsOmni(configPath) {
|
|
334
|
+
return await inspectAgentConfig(configPath) === "configured";
|
|
335
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface CleanOptions {
|
|
2
|
+
readonly artifactRoot: string;
|
|
3
|
+
readonly projectDirectory: string;
|
|
4
|
+
readonly now?: () => Date;
|
|
5
|
+
}
|
|
6
|
+
export interface CleanResult {
|
|
7
|
+
readonly removed: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function runClean(options: CleanOptions): Promise<CleanResult>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { lstat, mkdir, readFile, readdir, realpath, unlink, } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ARTIFACT_TTL_MS } from "../constants.js";
|
|
4
|
+
function containsPath(parent, child) {
|
|
5
|
+
const relative = path.relative(parent, child);
|
|
6
|
+
return relative === "" || (relative !== ".." && !relative.startsWith(`..${path.sep}`));
|
|
7
|
+
}
|
|
8
|
+
async function remove(filePath) {
|
|
9
|
+
try {
|
|
10
|
+
await unlink(filePath);
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code === "ENOENT")
|
|
15
|
+
return false;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
export async function runClean(options) {
|
|
23
|
+
const requestedRoot = path.resolve(options.artifactRoot);
|
|
24
|
+
const project = await realpath(options.projectDirectory);
|
|
25
|
+
await mkdir(requestedRoot, { recursive: true, mode: 0o700 });
|
|
26
|
+
const rootDetails = await lstat(requestedRoot);
|
|
27
|
+
if (!rootDetails.isDirectory() || rootDetails.isSymbolicLink()) {
|
|
28
|
+
throw new Error("The Bridge cache root is unsafe; nothing was deleted.");
|
|
29
|
+
}
|
|
30
|
+
const root = await realpath(requestedRoot);
|
|
31
|
+
if (containsPath(project, root) || containsPath(root, project)) {
|
|
32
|
+
throw new Error("The Bridge cache root overlaps the project; nothing was deleted.");
|
|
33
|
+
}
|
|
34
|
+
let removed = 0;
|
|
35
|
+
const now = (options.now ?? (() => new Date()))().getTime();
|
|
36
|
+
const results = path.join(root, "results");
|
|
37
|
+
try {
|
|
38
|
+
const resultDetails = await lstat(results);
|
|
39
|
+
if (!resultDetails.isDirectory() || resultDetails.isSymbolicLink()) {
|
|
40
|
+
throw new Error("The Bridge results directory is unsafe; nothing was deleted.");
|
|
41
|
+
}
|
|
42
|
+
for (const entry of await readdir(results, { withFileTypes: true })) {
|
|
43
|
+
if (!entry.isFile() && !entry.isSymbolicLink())
|
|
44
|
+
continue;
|
|
45
|
+
const entryPath = path.join(results, entry.name);
|
|
46
|
+
if (/^result_[A-Za-z0-9_-]{16,64}\.(?:data|json)$/u.test(entry.name)) {
|
|
47
|
+
if (await remove(entryPath))
|
|
48
|
+
removed += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (/^\.tmp-[A-Za-z0-9_-]+$/u.test(entry.name)) {
|
|
52
|
+
const details = await lstat(entryPath);
|
|
53
|
+
if (details.mtimeMs + ARTIFACT_TTL_MS <= now && await remove(entryPath))
|
|
54
|
+
removed += 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error.code !== "ENOENT")
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
63
|
+
if (!entry.isFile() && !entry.isSymbolicLink())
|
|
64
|
+
continue;
|
|
65
|
+
const entryPath = path.join(root, entry.name);
|
|
66
|
+
if (/^\..+\.tmp$/u.test(entry.name)) {
|
|
67
|
+
const details = await lstat(entryPath);
|
|
68
|
+
if (details.mtimeMs + ARTIFACT_TTL_MS <= now && await remove(entryPath))
|
|
69
|
+
removed += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const match = /^([0-9a-f]{64})\.issued\.json$/u.exec(entry.name);
|
|
73
|
+
if (match === null || entry.isSymbolicLink())
|
|
74
|
+
continue;
|
|
75
|
+
try {
|
|
76
|
+
const value = JSON.parse(await readFile(entryPath, "utf8"));
|
|
77
|
+
if (!isRecord(value) || value.state !== "GRANT_ISSUED" || typeof value.expiresAt !== "string") {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const expiresAt = Date.parse(value.expiresAt);
|
|
81
|
+
if (Number.isFinite(expiresAt) && expiresAt + ARTIFACT_TTL_MS <= now) {
|
|
82
|
+
if (await remove(entryPath))
|
|
83
|
+
removed += 1;
|
|
84
|
+
if (await remove(path.join(root, `${match[1]}.json`)))
|
|
85
|
+
removed += 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// Preserve malformed or unreadable journals for explicit diagnosis.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { removed };
|
|
93
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type AgentConfigEnvironment } from "./agent-config.js";
|
|
2
|
+
export interface HealthResult {
|
|
3
|
+
readonly cubeProtocol: string;
|
|
4
|
+
readonly iiisProtocol: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DoctorOptions extends AgentConfigEnvironment {
|
|
7
|
+
readonly env: NodeJS.ProcessEnv;
|
|
8
|
+
readonly artifactRoot: string;
|
|
9
|
+
readonly fetchImpl: typeof fetch;
|
|
10
|
+
readonly nodeVersion?: string;
|
|
11
|
+
readonly npmVersion?: string;
|
|
12
|
+
readonly packageVersion?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function checkCubeHealth(fetchImpl: typeof fetch, apiKey: string): Promise<string>;
|
|
15
|
+
export declare function checkIiisHealth(fetchImpl: typeof fetch): Promise<string>;
|
|
16
|
+
export declare function checkHealth(fetchImpl: typeof fetch, apiKey: string): Promise<HealthResult>;
|
|
17
|
+
export declare function runDoctor(options: DoctorOptions): Promise<string[]>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import { lstat, open, readdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DEFAULT_CUBE_BASE_URL, GRANTED_STREAM_PROTOCOL_VERSION, MAX_FILE_BYTES, PROTOCOL_VERSION, } from "../constants.js";
|
|
5
|
+
import { agentConfigPath, inspectAgentConfig, } from "./agent-config.js";
|
|
6
|
+
const CUBE_HEALTH_PATH = "/api/omni-reader/direct-upload/v1/health";
|
|
7
|
+
const IIIS_HEALTH_URL = "https://cubefile.ai.iiis.co:9443/omni/granted/health";
|
|
8
|
+
function isHealthBody(value) {
|
|
9
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
10
|
+
return false;
|
|
11
|
+
const record = value;
|
|
12
|
+
return typeof record.enabled === "boolean"
|
|
13
|
+
&& typeof record.protocol_version === "string"
|
|
14
|
+
&& record.max_bytes === MAX_FILE_BYTES;
|
|
15
|
+
}
|
|
16
|
+
async function fetchHealth(fetchImpl, url, authorization) {
|
|
17
|
+
const response = await fetchImpl(url, {
|
|
18
|
+
method: "GET",
|
|
19
|
+
headers: authorization === undefined ? undefined : { authorization },
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok)
|
|
22
|
+
throw new Error(`health endpoint returned ${response.status}`);
|
|
23
|
+
const body = await response.json();
|
|
24
|
+
if (!isHealthBody(body))
|
|
25
|
+
throw new Error("health endpoint returned an invalid response");
|
|
26
|
+
return body;
|
|
27
|
+
}
|
|
28
|
+
export async function checkCubeHealth(fetchImpl, apiKey) {
|
|
29
|
+
const cubeUrl = new URL(CUBE_HEALTH_PATH, DEFAULT_CUBE_BASE_URL).toString();
|
|
30
|
+
const cube = await fetchHealth(fetchImpl, cubeUrl, `Bearer ${apiKey}`);
|
|
31
|
+
if (!cube.enabled || cube.protocol_version !== PROTOCOL_VERSION) {
|
|
32
|
+
throw new Error("Cube direct upload is disabled or incompatible");
|
|
33
|
+
}
|
|
34
|
+
return cube.protocol_version;
|
|
35
|
+
}
|
|
36
|
+
export async function checkIiisHealth(fetchImpl) {
|
|
37
|
+
const iiis = await fetchHealth(fetchImpl, IIIS_HEALTH_URL);
|
|
38
|
+
if (!iiis.enabled || iiis.protocol_version !== GRANTED_STREAM_PROTOCOL_VERSION) {
|
|
39
|
+
throw new Error("IIIS granted upload is disabled or incompatible");
|
|
40
|
+
}
|
|
41
|
+
return iiis.protocol_version;
|
|
42
|
+
}
|
|
43
|
+
export async function checkHealth(fetchImpl, apiKey) {
|
|
44
|
+
const [cubeProtocol, iiisProtocol] = await Promise.all([
|
|
45
|
+
checkCubeHealth(fetchImpl, apiKey),
|
|
46
|
+
checkIiisHealth(fetchImpl),
|
|
47
|
+
]);
|
|
48
|
+
return { cubeProtocol, iiisProtocol };
|
|
49
|
+
}
|
|
50
|
+
async function artifactExpiry(metadataPath, resultId) {
|
|
51
|
+
let handle;
|
|
52
|
+
try {
|
|
53
|
+
handle = await open(metadataPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const details = await handle.stat();
|
|
60
|
+
if (!details.isFile())
|
|
61
|
+
return undefined;
|
|
62
|
+
const value = JSON.parse(await handle.readFile("utf8"));
|
|
63
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
64
|
+
return undefined;
|
|
65
|
+
const metadata = value;
|
|
66
|
+
if (metadata.resultId !== resultId || typeof metadata.expiresAt !== "string")
|
|
67
|
+
return undefined;
|
|
68
|
+
const expiresAt = Date.parse(metadata.expiresAt);
|
|
69
|
+
return Number.isFinite(expiresAt) ? expiresAt : undefined;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await handle.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function artifactFacts(artifactRoot) {
|
|
79
|
+
const resultsDirectory = path.join(artifactRoot, "results");
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = await readdir(resultsDirectory, { withFileTypes: true });
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error.code === "ENOENT") {
|
|
86
|
+
return { count: 0, bytes: 0, expiry: "none" };
|
|
87
|
+
}
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
let count = 0;
|
|
91
|
+
let bytes = 0;
|
|
92
|
+
let earliestExpiry;
|
|
93
|
+
let unknownExpiry = false;
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
const match = /^(result_[A-Za-z0-9_-]{16,64})\.data$/u.exec(entry.name);
|
|
96
|
+
if (!entry.isFile() || match === null)
|
|
97
|
+
continue;
|
|
98
|
+
const details = await lstat(path.join(resultsDirectory, entry.name));
|
|
99
|
+
if (!details.isFile() || details.isSymbolicLink())
|
|
100
|
+
continue;
|
|
101
|
+
count += 1;
|
|
102
|
+
bytes += details.size;
|
|
103
|
+
const expiresAt = await artifactExpiry(path.join(resultsDirectory, `${match[1]}.json`), match[1]);
|
|
104
|
+
if (expiresAt === undefined) {
|
|
105
|
+
unknownExpiry = true;
|
|
106
|
+
}
|
|
107
|
+
else if (earliestExpiry === undefined || expiresAt < earliestExpiry) {
|
|
108
|
+
earliestExpiry = expiresAt;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const expiry = count === 0
|
|
112
|
+
? "none"
|
|
113
|
+
: unknownExpiry || earliestExpiry === undefined
|
|
114
|
+
? "unknown"
|
|
115
|
+
: new Date(earliestExpiry).toISOString();
|
|
116
|
+
return { count, bytes, expiry };
|
|
117
|
+
}
|
|
118
|
+
export async function runDoctor(options) {
|
|
119
|
+
const lines = [
|
|
120
|
+
`Node: ${options.nodeVersion ?? process.version}`,
|
|
121
|
+
`npm: ${options.npmVersion ?? "unavailable"}`,
|
|
122
|
+
`Package: ${options.packageVersion ?? "unknown"}`,
|
|
123
|
+
`Cue API Key: ${options.env.CUE_API_KEY ? "present" : "absent"}`,
|
|
124
|
+
`Allowed roots: ${options.env.OMNI_ALLOWED_ROOTS || "none"}`,
|
|
125
|
+
];
|
|
126
|
+
for (const [label, target] of [
|
|
127
|
+
["Cursor", "cursor"],
|
|
128
|
+
["Claude Desktop", "claude-desktop"],
|
|
129
|
+
]) {
|
|
130
|
+
const configPath = agentConfigPath(target, options);
|
|
131
|
+
const status = configPath === undefined
|
|
132
|
+
? "not configured"
|
|
133
|
+
: await inspectAgentConfig(configPath, options);
|
|
134
|
+
lines.push(`${label} config: ${status}`);
|
|
135
|
+
}
|
|
136
|
+
if (options.env.CUE_API_KEY) {
|
|
137
|
+
try {
|
|
138
|
+
lines.push(`Cube protocol: ${await checkCubeHealth(options.fetchImpl, options.env.CUE_API_KEY)}`);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
lines.push("Cube health: unavailable or incompatible");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
lines.push("Cube health: skipped (Cue API Key absent)");
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
lines.push(`IIIS protocol: ${await checkIiisHealth(options.fetchImpl)}`);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
lines.push("IIIS health: unavailable or incompatible");
|
|
152
|
+
}
|
|
153
|
+
const artifacts = await artifactFacts(options.artifactRoot);
|
|
154
|
+
lines.push(`Artifacts: ${artifacts.count} file(s), ${artifacts.bytes} byte(s)`);
|
|
155
|
+
lines.push(`Artifact expiry: ${artifacts.expiry}`);
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type AgentConfigEnvironment } from "./agent-config.js";
|
|
2
|
+
export interface SetupOptions extends AgentConfigEnvironment {
|
|
3
|
+
readonly env: NodeJS.ProcessEnv;
|
|
4
|
+
readonly fetchImpl: typeof fetch;
|
|
5
|
+
readonly ask: (question: string) => Promise<string>;
|
|
6
|
+
readonly write: (text: string) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function runSetup(options: SetupOptions): Promise<void>;
|