@infimind-next/ai-hive-mcp 0.4.2 → 0.5.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.
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { chmod, mkdir, readFile, rename, stat, unlink, writeFile, } from "node:fs/promises";
3
- import { basename, dirname, join } from "node:path";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
4
4
  import { detectTarget } from "./detect.js";
5
5
  import { mergeJsonMcpServers, mergeOpenCodeServer, readJsonServerEnv, upsertCodexServer, } from "./merge.js";
6
6
  import { readBundledSkills } from "./skills.js";
@@ -127,7 +127,11 @@ async function detectAll(targets, dependencies) {
127
127
  }
128
128
  function buildServerEnv(config, key) {
129
129
  return {
130
- ...(key ? { AI_HIVE_MCP_KEY: key } : {}),
130
+ ...(key
131
+ ? { AI_HIVE_MCP_KEY: key }
132
+ : {
133
+ AI_HIVE_WORKBUDDY_CREDENTIALS_FILE: resolve(config.credentialsFile),
134
+ }),
131
135
  ...(config.endpointExplicit
132
136
  ? { AI_HIVE_API_ENDPOINT: config.endpoint }
133
137
  : {}),
@@ -245,7 +249,8 @@ async function configureClaudeCode(input) {
245
249
  };
246
250
  }
247
251
  // 重复执行时 claude 会因为同名服务器报错; 先移除再添加, 保证 setup 可重复运行.
248
- const runner = input.dependencies.runCommand ?? defaultRunner;
252
+ const runner = input.dependencies.runCommand ??
253
+ ((command, args) => defaultRunner(command, args, input.dependencies));
249
254
  await runner("claude", ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
250
255
  const result = await runner("claude", args);
251
256
  if (!result.ok) {
@@ -270,15 +275,129 @@ function maskedCommand(args) {
270
275
  .map((part) => part.startsWith("AI_HIVE_MCP_KEY=") ? "AI_HIVE_MCP_KEY=***" : part)
271
276
  .join(" ");
272
277
  }
273
- async function defaultRunner(command, args) {
274
- const result = spawnSync(command, args, {
278
+ async function defaultRunner(command, args, input) {
279
+ const executable = input.platform === "win32"
280
+ ? await resolveWindowsCommand(command, input.env)
281
+ : { command, prefix: [] };
282
+ if (!executable)
283
+ return { ok: false };
284
+ const result = spawnSync(executable.command, [...executable.prefix, ...args], {
275
285
  stdio: "ignore",
276
- // Windows 上 npm 安装的是 claude.cmd, Node 必须经 shell 才能执行; 参数只含常量和已校验的密钥.
277
- shell: process.platform === "win32",
286
+ env: input.env,
287
+ shell: false,
278
288
  timeout: 30_000,
279
289
  });
280
290
  return { ok: result.status === 0 };
281
291
  }
292
+ async function resolveWindowsCommand(command, env) {
293
+ const pathKey = Object.keys(env)
294
+ .sort()
295
+ .find((key) => key.toLowerCase() === "path");
296
+ const pathExtKey = Object.keys(env)
297
+ .sort()
298
+ .find((key) => key.toLowerCase() === "pathext");
299
+ const extensions = (pathExtKey ? env[pathExtKey] : undefined)?.split(";") ?? [
300
+ ".com",
301
+ ".exe",
302
+ ".bat",
303
+ ".cmd",
304
+ ];
305
+ for (const rawDirectory of (pathKey ? (env[pathKey] ?? "") : "").split(";")) {
306
+ const directory = rawDirectory.replace(/^"(.*)"$/, "$1");
307
+ if (!directory)
308
+ continue;
309
+ for (const rawExtension of extensions) {
310
+ const extension = rawExtension.toLowerCase();
311
+ if (![".exe", ".com", ".cmd", ".bat"].includes(extension))
312
+ continue;
313
+ const file = join(directory, `${command}${extension}`);
314
+ if (!(await stat(file).catch(() => null))?.isFile())
315
+ continue;
316
+ if (extension === ".exe" || extension === ".com") {
317
+ return { command: file, prefix: [] };
318
+ }
319
+ // 凭据路径不能交给 cmd 解释; npm shim 只用于定位同一安装的真实入口.
320
+ const packageRoot = join(directory, "node_modules/@anthropic-ai/claude-code");
321
+ try {
322
+ const metadata = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
323
+ const bin = typeof metadata.bin === "string"
324
+ ? metadata.bin
325
+ : metadata.bin?.[command];
326
+ if (metadata.name !== "@anthropic-ai/claude-code" ||
327
+ typeof bin !== "string")
328
+ return null;
329
+ const entry = resolve(packageRoot, bin);
330
+ const local = relative(packageRoot, entry);
331
+ if (local === ".." ||
332
+ local.startsWith(`..${sep}`) ||
333
+ isAbsolute(local) ||
334
+ !(await stat(entry)).isFile())
335
+ return null;
336
+ const javascript = /\.(?:c|m)?js$/i.test(entry);
337
+ if (!javascript && !/\.(?:exe|com)$/i.test(entry))
338
+ return null;
339
+ // 邻接官方包不能证明 wrapper 没有自定义账号环境或参数, 必须同时校验 shim 本身.
340
+ if (!isStandardNpmShim(await readFile(file, "utf8"), relative(directory, entry).replaceAll("/", "\\"), javascript))
341
+ return null;
342
+ return javascript
343
+ ? { command: process.execPath, prefix: [entry] }
344
+ : { command: entry, prefix: [] };
345
+ }
346
+ catch {
347
+ // 已命中的 shim 无法安全解析时停止, 不切到 PATH 中另一份客户端安装.
348
+ }
349
+ return null;
350
+ }
351
+ }
352
+ return null;
353
+ }
354
+ function isStandardNpmShim(source, target, javascript) {
355
+ const lines = source
356
+ .replaceAll("\r\n", "\n")
357
+ .trim()
358
+ .split("\n")
359
+ .map((line) => line.trim().replace(/[ \t]+(?=%\*$)/, " "))
360
+ .filter(Boolean);
361
+ const head = [
362
+ "@ECHO off",
363
+ "GOTO start",
364
+ ":find_dp0",
365
+ "SET dp0=%~dp0",
366
+ "EXIT /b",
367
+ ":start",
368
+ "SETLOCAL",
369
+ "CALL :find_dp0",
370
+ ];
371
+ const nodeBranch = [
372
+ 'IF EXIST "%dp0%\\node.exe" (',
373
+ 'SET "_prog=%dp0%\\node.exe"',
374
+ ") ELSE (",
375
+ 'SET "_prog=node"',
376
+ ];
377
+ const finish = "endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & ";
378
+ const invoke = `"%_prog%" "%dp0%\\${target}" %*`;
379
+ // npm cmd-shim 8 与 9 将 PATHEXT 放在不同位置, 只接受已实测生成的两种标准结构.
380
+ const bodies = javascript
381
+ ? [
382
+ [
383
+ ...nodeBranch,
384
+ "SET PATHEXT=%PATHEXT:;.JS;=;%",
385
+ ")",
386
+ `${finish}${invoke}`,
387
+ ],
388
+ [
389
+ ...nodeBranch,
390
+ ")",
391
+ `${finish}set PATHEXT=%PATHEXT:;.JS;=;% & ${invoke}`,
392
+ ],
393
+ ]
394
+ : [[`"%dp0%\\${target}" %*`]];
395
+ return bodies.some((body) => {
396
+ const expected = [...head, ...body];
397
+ return (lines.length === expected.length &&
398
+ lines.every((line, index) => line === expected[index]));
399
+ });
400
+ }
282
401
  async function installSkills(input) {
283
402
  const errors = [];
284
403
  let changed = 0;
@@ -1,3 +1,47 @@
1
+ import { z } from "zod";
2
+ export declare const noticeSchema: z.ZodObject<{
3
+ id: z.ZodString;
4
+ revision: z.ZodString;
5
+ kind: z.ZodEnum<["RECHARGE_PROMOTION", "ANNOUNCEMENT"]>;
6
+ title: z.ZodString;
7
+ message: z.ZodString;
8
+ details: z.ZodString;
9
+ expiresAt: z.ZodNullable<z.ZodString>;
10
+ action: z.ZodOptional<z.ZodObject<{
11
+ label: z.ZodString;
12
+ url: z.ZodEffects<z.ZodString, string, string>;
13
+ }, "strip", z.ZodTypeAny, {
14
+ label: string;
15
+ url: string;
16
+ }, {
17
+ label: string;
18
+ url: string;
19
+ }>>;
20
+ }, "strip", z.ZodTypeAny, {
21
+ id: string;
22
+ revision: string;
23
+ kind: "RECHARGE_PROMOTION" | "ANNOUNCEMENT";
24
+ message: string;
25
+ title: string;
26
+ details: string;
27
+ expiresAt: string | null;
28
+ action?: {
29
+ label: string;
30
+ url: string;
31
+ } | undefined;
32
+ }, {
33
+ id: string;
34
+ revision: string;
35
+ kind: "RECHARGE_PROMOTION" | "ANNOUNCEMENT";
36
+ message: string;
37
+ title: string;
38
+ details: string;
39
+ expiresAt: string | null;
40
+ action?: {
41
+ label: string;
42
+ url: string;
43
+ } | undefined;
44
+ }>;
1
45
  export declare function toolResultContent(value: unknown): {
2
46
  type: "text";
3
47
  text: string;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  // 独立 npm 包不能依赖私有 workspace, 与 contracts 的通知文本通过跨包用例校验.
3
- const noticeSchema = z.object({
3
+ export const noticeSchema = z.object({
4
4
  id: z.string().min(1),
5
5
  revision: z.string().min(1),
6
6
  kind: z.enum(["RECHARGE_PROMOTION", "ANNOUNCEMENT"]),
@@ -18,16 +18,9 @@ const noticeSchema = z.object({
18
18
  })
19
19
  .optional(),
20
20
  });
21
- export function toolResultContent(value) {
22
- if (!value ||
23
- typeof value !== "object" ||
24
- Array.isArray(value) ||
25
- !("userNotices" in value)) {
26
- return [{ type: "text", text: JSON.stringify(value) }];
27
- }
28
- const { userNotices, ...businessResult } = value;
29
- const notices = Array.isArray(userNotices)
30
- ? userNotices.flatMap((raw) => {
21
+ function noticeContent(value, deduplicate = false) {
22
+ const valid = Array.isArray(value)
23
+ ? value.flatMap((raw) => {
31
24
  const parsed = noticeSchema.safeParse(raw);
32
25
  return parsed.success &&
33
26
  (parsed.data.expiresAt === null ||
@@ -36,17 +29,42 @@ export function toolResultContent(value) {
36
29
  : [];
37
30
  })
38
31
  : [];
32
+ const notices = deduplicate
33
+ ? [
34
+ ...new Map(valid.map((notice) => [`${notice.id}:${notice.revision}`, notice])).values(),
35
+ ]
36
+ : valid;
37
+ return notices.map((notice) => ({
38
+ type: "text",
39
+ text: [
40
+ `${notice.title}:${notice.message}`,
41
+ ...(notice.action
42
+ ? [`${notice.action.label}:${notice.action.url}`]
43
+ : []),
44
+ ...(notice.details ? [`充值档位:\n${notice.details}`] : []),
45
+ ].join("\n"),
46
+ }));
47
+ }
48
+ export function toolResultContent(value) {
49
+ if (value &&
50
+ typeof value === "object" &&
51
+ !Array.isArray(value) &&
52
+ "schemaVersion" in value &&
53
+ value.schemaVersion === 2 &&
54
+ "notices" in value) {
55
+ return [
56
+ { type: "text", text: JSON.stringify(value) },
57
+ ...noticeContent(value.notices, true),
58
+ ];
59
+ }
60
+ if (!value ||
61
+ typeof value !== "object" ||
62
+ Array.isArray(value) ||
63
+ !("userNotices" in value))
64
+ return [{ type: "text", text: JSON.stringify(value) }];
65
+ const { userNotices, ...businessResult } = value;
39
66
  return [
40
67
  { type: "text", text: JSON.stringify(businessResult) },
41
- ...notices.map((notice) => ({
42
- type: "text",
43
- text: [
44
- `${notice.title}:${notice.message}`,
45
- ...(notice.action
46
- ? [`${notice.action.label}:${notice.action.url}`]
47
- : []),
48
- ...(notice.details ? [`充值档位:\n${notice.details}`] : []),
49
- ].join("\n"),
50
- })),
68
+ ...noticeContent(userNotices),
51
69
  ];
52
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infimind-next/ai-hive-mcp",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -13,13 +13,6 @@
13
13
  "dist",
14
14
  "skills"
15
15
  ],
16
- "scripts": {
17
- "build": "tsc -p tsconfig.json",
18
- "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
19
- "lint": "eslint \"src/**/*.ts\"",
20
- "prepack": "pnpm run build",
21
- "test": "vitest run"
22
- },
23
16
  "dependencies": {
24
17
  "@modelcontextprotocol/sdk": "^1.29.0",
25
18
  "file-type": "^21.3.4",
@@ -30,5 +23,11 @@
30
23
  "typescript": "^5.0.0",
31
24
  "typescript-eslint": "^8.0.0",
32
25
  "vitest": "^3.0.0"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.json",
29
+ "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput",
30
+ "lint": "eslint \"src/**/*.ts\"",
31
+ "test": "vitest run"
33
32
  }
34
- }
33
+ }
@@ -31,3 +31,5 @@ MCP 工具:`get_billing_summary`、`list_billing_records`、`get_billing_recor
31
31
  ## 账户信息
32
32
 
33
33
  需要账户资料或充值入口时用 `get_user_info`。该工具同时返回当前活动,有活动时可以把活动信息转达给用户。
34
+
35
+ 新版包模型查询默认 V2。先按类型或关键词查轻目录,找到候选即可 get_model,默认读取参数和价格;已知模型 ID 直接 get_model。无需遍历所有分页,完整目录需求才读取 nextCursor。正式调用前预检并确认费用;未知提交先查原任务,不自动付费重试。
@@ -39,3 +39,5 @@ description: 用蜂巢生成图片或视频。用户要出图、图生图、文
39
39
 
40
40
  - 成功:给结果链接或 mediaId,并说明本次消费(单位是蜂蜜,充值实付才是元)。
41
41
  - 失败:说明失败原因和用户能做什么,不展示上游响应体。
42
+
43
+ 新版包模型查询默认 V2。先按类型或关键词查轻目录,找到候选即可 get_model,默认读取参数和价格;已知模型 ID 直接 get_model。无需遍历所有分页,完整目录需求才读取 nextCursor。正式调用前预检并确认费用;未知提交先查原任务,不自动付费重试。
@@ -31,3 +31,5 @@ description: 把本机图片、视频、音频或文档传给蜂巢,换取可
31
31
  - `complete_media_upload` 失败时不要直接拿去生成,先重试确认或重新上传。
32
32
  - 单文件大小和格式限制以 `get_model` 返回的媒体要求为准。
33
33
  - 不要把文件内容贴进对话文本,必须走上传换取 `mediaId`。
34
+
35
+ 新版包模型查询默认 V2。先按类型或关键词查轻目录,找到候选即可 get_model,默认读取参数和价格;已知模型 ID 直接 get_model。无需遍历所有分页,完整目录需求才读取 nextCursor。正式调用前预检并确认费用;未知提交先查原任务,不自动付费重试。
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ai-hive-model-selection
3
- description: 在蜂巢 AI 开放平台选择模型。用户要调用文字、图片或视频模型,但还没指定模型时使用;先列出可用模型和售价,再读取目标模型的参数契约,避免按记忆猜参数。也可用于比较同类模型的价格和线路。
3
+ description: 在蜂巢 AI 开放平台选择模型。用户要调用文字、图片或视频模型,但还没指定模型时使用;先筛选轻目录,再读取候选模型的参数和价格,避免按记忆猜参数。也可用于比较同类模型的价格和线路。
4
4
  ---
5
5
 
6
6
  # 选择模型
@@ -14,7 +14,7 @@ description: 在蜂巢 AI 开放平台选择模型。用户要调用文字、图
14
14
 
15
15
  ## 步骤
16
16
 
17
- 1. 列出候选模型(`detail=lite` 只返回选型需要的字段):
17
+ 1. 按类型或关键词列出候选模型(目录每行仅 ID、名称、类型和素材模式):
18
18
 
19
19
  ```
20
20
  ai-hive-mcp models --type IMAGE --json
@@ -27,7 +27,7 @@ description: 在蜂巢 AI 开放平台选择模型。用户要调用文字、图
27
27
  - 图片:`IMAGE`
28
28
  - 视频:`VIDEO`
29
29
 
30
- 3. 比较 `pricingSummary` 里的 `saleMicros`。这是客户实际结算价,单位是蜂蜜微单位(1,000,000 微单位 = 1 蜂蜜),不要引用 `originalMicros`。只有充值实付金额才是元。
30
+ 3. 找到候选即可读取 `get_model` 默认参数与价格;已知 ID 可直接读取。比较 `pricingSnapshot` 里的 `saleMicros`。这是客户实际结算价,单位是蜂蜜微单位(1,000,000 微单位 = 1 蜂蜜),不要引用 `originalMicros`。只有充值实付金额才是元。
31
31
  4. 用 `get_model` / `ai-hive-mcp model <模型 ID> --json` 读取:
32
32
  - 支持的 `routingModes`
33
33
  - 参数 schema
@@ -46,5 +46,8 @@ description: 在蜂巢 AI 开放平台选择模型。用户要调用文字、图
46
46
  ## 失败处理
47
47
 
48
48
  - 返回"模型不存在或当前不可用":重新列一遍模型,不要重试同一个 ID。
49
- - 返回没有价格:该模型暂不可售,换一个模型。
49
+ - V2 轻目录不包含价格是正常行为:使用目录返回的 `publicModelId` 调用 `get_model`,读取价格和参数后再比较,不因此判定模型不可售。
50
+ - `get_model` 详情价格缺失或不可用:暂停调用并核验详情的 pricing 章节与服务端状态,不自动换模型或发起生成。恢复后仍按原流程预检并确认费用。
50
51
  - 不要猜测参数名。`get_model` 没有给出的字段一律不要传。
52
+
53
+ 新版包模型查询默认 V2。先按类型或关键词查轻目录,找到候选即可 get_model,默认读取参数和价格;已知模型 ID 直接 get_model。无需遍历所有分页,完整目录需求才读取 nextCursor。正式调用前预检并确认费用;未知提交先查原任务,不自动付费重试。
@@ -20,7 +20,7 @@ ai-hive-mcp run <模型 ID> "你的提示词" --json
20
20
  ## 必须遵守
21
21
 
22
22
  1. **每次付费调用都带 `clientRequestId`**,同一次业务请求重试必须复用同一个值。改参数后必须换新值,否则会被当成同一次请求去重。
23
- 2. `pricingSnapshot` 可选。省略时客户端会自动向服务端取当前快照;手动传时必须是 `list_models` 返回的对应对象,且 `publicModelId`、`routingMode` 一致。
23
+ 2. `pricingSnapshot` 可选。省略时客户端会自动向服务端取当前快照;手动传时必须是 `get_model` 返回的对应快照对象,且 `publicModelId`、`routingMode` 一致。
24
24
  3. 多轮对话时把历史消息按 `system` / `user` / `assistant` 顺序传入。
25
25
  4. 图片理解:先把图片上传为 `mediaId`(见 `ai-hive-media-upload`),再放进 `messages[].mediaIds`。
26
26
 
@@ -37,3 +37,5 @@ ai-hive-mcp run <模型 ID> "你的提示词" --json
37
37
  - 给出生成结果。
38
38
  - 结果里包含 `chargeMicros` 时,换算成蜂蜜(除以 1,000,000)告诉用户本次消费;只有充值实付金额才是元。
39
39
  - 不要展示内部错误码或上游响应体。
40
+
41
+ 新版包模型查询默认 V2。先按类型或关键词查轻目录,找到候选即可 get_model,默认读取参数和价格;已知模型 ID 直接 get_model。无需遍历所有分页,完整目录需求才读取 nextCursor。正式调用前预检并确认费用;未知提交先查原任务,不自动付费重试。