@cueai/omni-reader-mcp 1.1.1 → 1.1.2

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 CHANGED
@@ -33,17 +33,33 @@ Bridge 运行时只使用以下产品配置:
33
33
  始终使用已审核的精确版本,不使用隐式 `latest`:
34
34
 
35
35
  ```sh
36
- npx -y @cueai/omni-reader-mcp@1.1.1 setup
36
+ npx -y @cueai/omni-reader-mcp@1.1.2 setup
37
37
  ```
38
38
 
39
39
  交互 setup 支持 Hermes、Cursor、Claude Desktop 和通用 stdio 配置。非交互安装使用同一参数解析和写入逻辑:
40
40
 
41
41
  ```sh
42
- npx -y @cueai/omni-reader-mcp@1.1.1 setup --client hermes --allowed-root /absolute/minimum/root --yes --json
43
- npx -y @cueai/omni-reader-mcp@1.1.1 setup --client cursor --add-root /absolute/minimum/root --yes --json
44
- npx -y @cueai/omni-reader-mcp@1.1.1 setup --client claude-desktop --allowed-root /absolute/minimum/root --yes --json
42
+ npx -y @cueai/omni-reader-mcp@1.1.2 setup --client hermes --allowed-root /absolute/minimum/root --yes --json
43
+ npx -y @cueai/omni-reader-mcp@1.1.2 setup --client cursor --add-root /absolute/minimum/root --yes --json
44
+ npx -y @cueai/omni-reader-mcp@1.1.2 setup --client claude-desktop --allowed-root /absolute/minimum/root --yes --json
45
45
  ```
46
46
 
47
+ Agent/脚本在 pty 下运行(stdin 仍是 TTY)时,用 `--headless`(别名 `--non-interactive`)显式声明非交互,无需 `--yes` 且绝不读 stdin:
48
+
49
+ ```sh
50
+ npx -y @cueai/omni-reader-mcp@1.1.2 setup --client cursor --allowed-root /absolute/minimum/root --headless --json
51
+ ```
52
+
53
+ ## 缓存与 journal 隔离
54
+
55
+ 本地解析缓存与操作 journal 默认位于用户级缓存目录(macOS `~/Library/Caches/cue/omni-reader-mcp`、Windows `%LOCALAPPDATA%\Cue\omni-reader-mcp\Cache`、Linux `~/.cache/cue/omni-reader-mcp`)。若 Agent 的文件监视器(如 WorkBuddy)锁定该目录下的文件导致写入失败,设置 `OMNI_CACHE_DIR` 将缓存与 journal 一起隔离到不被监视的目录:
56
+
57
+ ```sh
58
+ export OMNI_CACHE_DIR=/var/cache/omni-reader
59
+ ```
60
+
61
+ 显式 `OMNI_CACHE_DIR` 不会被项目重叠检查改写到临时目录;仍禁止与项目目录互相包含。
62
+
47
63
  setup 会:
48
64
 
49
65
  1. 展示 user-scope 配置目标和变更;
@@ -112,23 +128,23 @@ Agent 不应自动拆分、转码或上传公共站点;应请用户提供符
112
128
  ## 命令
113
129
 
114
130
  ```sh
115
- npx -y @cueai/omni-reader-mcp@1.1.1 doctor
116
- npx -y @cueai/omni-reader-mcp@1.1.1 doctor --json
117
- npx -y @cueai/omni-reader-mcp@1.1.1 clean
118
- npx -y @cueai/omni-reader-mcp@1.1.1 uninstall --yes --json
131
+ npx -y @cueai/omni-reader-mcp@1.1.2 doctor
132
+ npx -y @cueai/omni-reader-mcp@1.1.2 doctor --json
133
+ npx -y @cueai/omni-reader-mcp@1.1.2 clean
134
+ npx -y @cueai/omni-reader-mcp@1.1.2 uninstall --yes --json
119
135
  ```
120
136
 
121
137
  不带命令运行固定版本会启动 stdio MCP server:
122
138
 
123
139
  ```sh
124
- npx -y @cueai/omni-reader-mcp@1.1.1
140
+ npx -y @cueai/omni-reader-mcp@1.1.2
125
141
  ```
126
142
 
127
143
  `doctor --json` 返回 package/npm/client adapter、Key present/absent、allowed-root 安全状态、endpoint compatibility、artifact、cache mode、onboarding 和 reload 状态;不会输出 Key、私有来源路径或内容。
128
144
 
129
145
  ## 卸载与回滚
130
146
 
131
- `uninstall --yes --json` 只删除当前受信的 1.1.1 Bridge entry;存在匹配的可信备份时恢复原 URL-only `omni-reader` entry。卸载不会删除用户源文件,也不会静默删除未过期的本地结果。
147
+ `uninstall --yes --json` 只删除当前受信的 1.1.2 Bridge entry;存在匹配的可信备份时恢复原 URL-only `omni-reader` entry。卸载不会删除用户源文件,也不会静默删除未过期的本地结果。
132
148
 
133
149
  需要回滚 1.1.1 时:
134
150
 
@@ -44,6 +44,7 @@ export interface ArtifactReadResult {
44
44
  interface ArtifactFinalizeInput extends ReleasedMetadata {
45
45
  readonly resultId: string;
46
46
  }
47
+ export declare function syncDirectory(directory: string, platform?: NodeJS.Platform): Promise<void>;
47
48
  export declare function defaultArtifactRoot(options?: DefaultArtifactRootOptions): string;
48
49
  export declare function selectArtifactRoot(options: SelectArtifactRootOptions): Promise<SelectedArtifactRoot>;
49
50
  export declare class ArtifactStore {
@@ -53,14 +53,21 @@ async function removeIfPresent(filePath) {
53
53
  throw error;
54
54
  }
55
55
  }
56
- async function syncDirectory(directory) {
56
+ export async function syncDirectory(directory, platform = process.platform) {
57
+ // Windows cannot fsync a directory handle: opening a directory with
58
+ // O_RDONLY raises EPERM. File fsync still provides primary durability.
59
+ if (platform === "win32")
60
+ return;
57
61
  let handle;
58
62
  try {
59
63
  handle = await open(directory, "r");
60
64
  await handle.sync();
61
65
  }
62
66
  catch (error) {
63
- if (!errno(error, "EINVAL") && !errno(error, "ENOTSUP") && !errno(error, "EISDIR")) {
67
+ if (!errno(error, "EINVAL") &&
68
+ !errno(error, "ENOTSUP") &&
69
+ !errno(error, "EISDIR") &&
70
+ !errno(error, "EPERM")) {
64
71
  throw error;
65
72
  }
66
73
  }
@@ -232,6 +239,14 @@ export function defaultArtifactRoot(options = {}) {
232
239
  const platform = options.platform ?? process.platform;
233
240
  const homeDirectory = options.homeDirectory ?? os.homedir();
234
241
  const env = options.env ?? process.env;
242
+ const paths = platform === "win32" ? path.win32 : path.posix;
243
+ const configured = env.OMNI_CACHE_DIR;
244
+ if (configured !== undefined && configured.length > 0) {
245
+ // Explicit isolation: Agents' file watchers lock files under the default
246
+ // cache (WorkBuddy dogfood). An explicit root skips the overlap fallback;
247
+ // ArtifactStore.open still rejects a project-inside-cache layout.
248
+ return paths.resolve(configured);
249
+ }
235
250
  if (platform === "darwin") {
236
251
  return path.posix.join(homeDirectory, "Library", "Caches", "cue", "omni-reader-mcp");
237
252
  }
@@ -287,12 +302,14 @@ function selectionPathsOverlap(left, right, platform) {
287
302
  export async function selectArtifactRoot(options) {
288
303
  const platform = options.platform ?? process.platform;
289
304
  const rootDirectory = defaultArtifactRoot(options);
305
+ const env = options.env ?? process.env;
306
+ const explicitRoot = env.OMNI_CACHE_DIR !== undefined && env.OMNI_CACHE_DIR.length > 0;
290
307
  const [rootIdentity, projectIdentity, homeIdentity] = await Promise.all([
291
308
  selectionPathIdentity(rootDirectory, platform),
292
309
  selectionPathIdentity(options.projectDirectory, platform),
293
310
  selectionPathIdentity(options.homeDirectory ?? os.homedir(), platform),
294
311
  ]);
295
- if (!selectionPathsOverlap(rootIdentity, projectIdentity, platform)) {
312
+ if (explicitRoot || !selectionPathsOverlap(rootIdentity, projectIdentity, platform)) {
296
313
  return { mode: "default", rootDirectory };
297
314
  }
298
315
  const paths = platform === "win32" ? path.win32 : path.posix;
@@ -4,6 +4,7 @@ export interface SetupArguments {
4
4
  readonly allowedRoots: readonly string[];
5
5
  readonly addRoots: readonly string[];
6
6
  readonly yes: boolean;
7
+ readonly headless: boolean;
7
8
  readonly json: boolean;
8
9
  }
9
10
  export type ParsedCommand = {
@@ -32,12 +32,13 @@ function parseSetupArguments(values, platform) {
32
32
  let allowedRoot;
33
33
  let addRoot;
34
34
  let yes = false;
35
+ let headless = false;
35
36
  let json = false;
36
37
  const seen = new Set();
37
38
  const paths = pathsFor(platform);
38
39
  for (let index = 0; index < values.length; index += 1) {
39
40
  const flag = values[index];
40
- if (!["--client", "--allowed-root", "--add-root", "--yes", "--json"].includes(flag)) {
41
+ if (!["--client", "--allowed-root", "--add-root", "--yes", "--headless", "--non-interactive", "--json"].includes(flag)) {
41
42
  throw new CliUsageError(`Unknown setup flag: ${flag}`);
42
43
  }
43
44
  if (seen.has(flag))
@@ -47,6 +48,10 @@ function parseSetupArguments(values, platform) {
47
48
  yes = true;
48
49
  continue;
49
50
  }
51
+ if (flag === "--headless" || flag === "--non-interactive") {
52
+ headless = true;
53
+ continue;
54
+ }
50
55
  if (flag === "--json") {
51
56
  json = true;
52
57
  continue;
@@ -83,6 +88,7 @@ function parseSetupArguments(values, platform) {
83
88
  allowedRoots: allowedRoot === undefined ? [] : [allowedRoot],
84
89
  addRoots: addRoot === undefined ? [] : [addRoot],
85
90
  yes,
91
+ headless,
86
92
  json,
87
93
  };
88
94
  }
package/dist/cli/setup.js CHANGED
@@ -39,14 +39,20 @@ async function interactiveArguments(options) {
39
39
  }
40
40
  async function selectedTargetAndRoots(options) {
41
41
  const requested = options.arguments;
42
+ // --headless (alias: --non-interactive) declares a non-interactive run
43
+ // explicitly, so setup works even when the caller is attached to a TTY
44
+ // (WorkBuddy runs under a pty). Without the flag, non-TTY is inferred.
45
+ const headless = requested.headless || !options.stdinIsTTY;
42
46
  if (requested.client === undefined) {
43
- if (!options.stdinIsTTY) {
44
- throw new CliUsageError("Non-interactive setup requires: setup --client hermes --allowed-root <absolute-path> --yes --json");
47
+ if (headless) {
48
+ throw new CliUsageError("Headless setup requires: setup --client hermes --allowed-root <absolute-path> --headless --json");
45
49
  }
46
50
  const selected = await interactiveArguments(options);
47
51
  return { ...selected, interactive: true };
48
52
  }
49
- if (!options.stdinIsTTY && !requested.yes) {
53
+ // Only an explicit --headless bypasses the consent gate; a non-TTY
54
+ // without --yes still hard-errors so scripts cannot silently write.
55
+ if (!requested.headless && !options.stdinIsTTY && !requested.yes) {
50
56
  throw new CliUsageError("Non-interactive setup requires --yes.");
51
57
  }
52
58
  const existing = await configuredAllowedRoots(requested.client, options);
@@ -55,7 +61,7 @@ async function selectedTargetAndRoots(options) {
55
61
  : requested.addRoots.length > 0
56
62
  ? [...new Set([...existing, ...requested.addRoots])]
57
63
  : existing;
58
- return { target: requested.client, roots, interactive: !requested.yes };
64
+ return { target: requested.client, roots, interactive: headless ? false : !requested.yes };
59
65
  }
60
66
  function writePreview(options, prepared) {
61
67
  options.write(`Target: ${prepared.displayPath}\n`);
@@ -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.1.1";
9
+ export declare const BRIDGE_RELEASE_VERSION = "1.1.2";
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.1.1";
9
+ export const BRIDGE_RELEASE_VERSION = "1.1.2";
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;
@@ -61,6 +61,14 @@ export interface OperationJournalOptions {
61
61
  readonly rootDirectory?: string;
62
62
  readonly now?: () => Date;
63
63
  }
64
+ /**
65
+ * Rename with a Windows fallback. Node's rename() rejects replacing an
66
+ * existing destination on Windows with EPERM instead of overwriting
67
+ * atomically; WorkBuddy dogfood hit this on every journal update because
68
+ * the record file already exists. The record lock makes the journal
69
+ * single-writer, so remove-then-rename cannot race a concurrent writer.
70
+ */
71
+ export declare function renameWithFallback(sourcePath: string, destinationPath: string, platform?: NodeJS.Platform): Promise<void>;
64
72
  export declare class OperationJournal {
65
73
  #private;
66
74
  constructor(options?: OperationJournalOptions);
@@ -56,6 +56,12 @@ function bridgeError(code, message, retryable) {
56
56
  });
57
57
  }
58
58
  function defaultRootDirectory() {
59
+ const configured = process.env.OMNI_CACHE_DIR;
60
+ if (configured !== undefined && configured.length > 0) {
61
+ return process.platform === "win32"
62
+ ? path.win32.resolve(configured)
63
+ : path.posix.resolve(configured);
64
+ }
59
65
  if (process.platform === "darwin") {
60
66
  return path.join(homedir(), "Library", "Caches", "cue", "omni-reader-mcp");
61
67
  }
@@ -199,6 +205,25 @@ async function closeQuietly(handle) {
199
205
  function sleep(milliseconds) {
200
206
  return new Promise((resolve) => setTimeout(resolve, milliseconds));
201
207
  }
208
+ /**
209
+ * Rename with a Windows fallback. Node's rename() rejects replacing an
210
+ * existing destination on Windows with EPERM instead of overwriting
211
+ * atomically; WorkBuddy dogfood hit this on every journal update because
212
+ * the record file already exists. The record lock makes the journal
213
+ * single-writer, so remove-then-rename cannot race a concurrent writer.
214
+ */
215
+ export async function renameWithFallback(sourcePath, destinationPath, platform = process.platform) {
216
+ try {
217
+ await rename(sourcePath, destinationPath);
218
+ }
219
+ catch (error) {
220
+ if (platform !== "win32" || error.code !== "EPERM") {
221
+ throw error;
222
+ }
223
+ await unlink(destinationPath);
224
+ await rename(sourcePath, destinationPath);
225
+ }
226
+ }
202
227
  function assertStableTransition(current, updated) {
203
228
  for (const [before, after] of [
204
229
  [current.operationId, updated.operationId],
@@ -553,7 +578,7 @@ export class OperationJournal {
553
578
  await handle.sync();
554
579
  await handle.close();
555
580
  handle = undefined;
556
- await rename(temporaryPath, recordPath);
581
+ await renameWithFallback(temporaryPath, recordPath);
557
582
  if (process.platform !== "win32")
558
583
  await chmod(recordPath, 0o600);
559
584
  await this.#syncDirectory();
@@ -541,11 +541,37 @@ function remoteContext(value) {
541
541
  }
542
542
  return context;
543
543
  }
544
+ function _userFacingMessage(code, result) {
545
+ const error = result.error;
546
+ if (!error.file_uploaded && !error.parser_started) {
547
+ if (code === "antibot_blocked" || code === "empty_content") {
548
+ return "目标网站返回了验证码或空白页面,解析器无法提取内容。可尝试用本地 Bridge 下载页面后上传。";
549
+ }
550
+ if (code === "SERVICE_TEMPORARILY_UNAVAILABLE") {
551
+ return "解析服务暂时不可用,请稍后重试。";
552
+ }
553
+ if (code === "ssrf_blocked") {
554
+ return "该链接指向内部/私有地址,已被安全策略拦截。";
555
+ }
556
+ return `文件上传失败(${code})。请检查网络连接,或尝试用本地 Bridge 下载后上传。`;
557
+ }
558
+ if (!error.parser_started) {
559
+ return `文件已上传但解析未能启动(${code})。可能是文件格式不支持或文件损坏。`;
560
+ }
561
+ return `解析失败(${code})。可尝试更换链接或文件格式。`;
562
+ }
544
563
  function remoteFailure(result) {
545
564
  const error = result.error;
565
+ // Map internal error codes to user-facing messages so the caller (and the
566
+ // user) can understand what happened without reading protocol internals.
567
+ const code = error.code || "UNKNOWN_ERROR";
568
+ let message = error.message || "";
569
+ if (!message || message === code) {
570
+ message = _userFacingMessage(code, result);
571
+ }
546
572
  return new OmniBridgeError({
547
- code: error.code,
548
- message: error.message,
573
+ code,
574
+ message,
549
575
  ...(error.failure_scope === undefined ? {} : { failureScope: error.failure_scope }),
550
576
  sourceKind: "url",
551
577
  ...(error.user_action === undefined ? {} : { userAction: error.user_action }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cueai/omni-reader-mcp",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Local stdio MCP bridge for direct Omni document parsing",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",