@numa-tech/numa 1.26.8 → 1.26.9

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.
Files changed (63) hide show
  1. package/README.md +13 -2
  2. package/dist/ai-registry/commands.js +192 -129
  3. package/dist/ai-registry/commands.js.map +1 -1
  4. package/dist/ai-registry/install-conflicts.d.ts +9 -0
  5. package/dist/ai-registry/install-conflicts.js +59 -0
  6. package/dist/ai-registry/install-conflicts.js.map +1 -0
  7. package/dist/ai-registry/install-defaults.js +3 -2
  8. package/dist/ai-registry/install-defaults.js.map +1 -1
  9. package/dist/cli.js +39 -28
  10. package/dist/cli.js.map +1 -1
  11. package/dist/command-catalog.js +10 -3
  12. package/dist/command-catalog.js.map +1 -1
  13. package/dist/interactive/entry.js +1 -1
  14. package/dist/interactive/entry.js.map +1 -1
  15. package/dist/interactive/prompts.js +1 -1
  16. package/dist/interactive/prompts.js.map +1 -1
  17. package/dist/interactive/registry.d.ts +15 -0
  18. package/dist/interactive/registry.js +51 -2
  19. package/dist/interactive/registry.js.map +1 -1
  20. package/dist/jev/client.d.ts +17 -0
  21. package/dist/jev/client.js +127 -0
  22. package/dist/jev/client.js.map +1 -0
  23. package/dist/jev/commands.d.ts +8 -0
  24. package/dist/jev/commands.js +40 -0
  25. package/dist/jev/commands.js.map +1 -0
  26. package/dist/jev/errors.d.ts +39 -0
  27. package/dist/jev/errors.js +48 -0
  28. package/dist/jev/errors.js.map +1 -0
  29. package/dist/jev/input.d.ts +11 -0
  30. package/dist/jev/input.js +126 -0
  31. package/dist/jev/input.js.map +1 -0
  32. package/dist/jev/profile.d.ts +3 -0
  33. package/dist/jev/profile.js +26 -0
  34. package/dist/jev/profile.js.map +1 -0
  35. package/dist/jev/response.d.ts +3 -0
  36. package/dist/jev/response.js +38 -0
  37. package/dist/jev/response.js.map +1 -0
  38. package/dist/kb/web-page.js +70 -13
  39. package/dist/kb/web-page.js.map +1 -1
  40. package/dist/kb/web-queue.d.ts +3 -0
  41. package/dist/kb/web-queue.js +21 -1
  42. package/dist/kb/web-queue.js.map +1 -1
  43. package/dist/kb/web.js +12 -0
  44. package/dist/kb/web.js.map +1 -1
  45. package/dist/mcp-gateway/runtime.d.ts +95 -0
  46. package/dist/mcp-gateway/runtime.js +25 -3
  47. package/dist/mcp-gateway/runtime.js.map +1 -1
  48. package/docs/design/kb-web-workspace.md +19 -1
  49. package/docs/module-maintenance-and-skills-updates.md +18 -0
  50. package/examples/jev/README.md +6 -2
  51. package/examples/oss/__pycache__/roundtrip.cpython-314.pyc +0 -0
  52. package/examples/oss/__pycache__/test_roundtrip.cpython-314.pyc +0 -0
  53. package/package.json +4 -4
  54. package/skills/numa-cli/SKILL.md +10 -6
  55. package/skills/numa-cli/evals/evals.json +27 -0
  56. package/skills/numa-cli/numa-skill.json +1 -1
  57. package/skills/numa-cli/references/authoring.md +29 -0
  58. package/skills/numa-cli/references/cli-bootstrap.md +3 -3
  59. package/skills/numa-jev/SKILL.md +11 -9
  60. package/skills/numa-jev/numa-skill.json +5 -2
  61. package/skills/numa-jev/references/application-integration.md +2 -2
  62. package/skills/numa-jev/references/cli-usage.md +62 -0
  63. package/skills/numa-jev/references/mcp-usage.md +3 -1
package/README.md CHANGED
@@ -38,7 +38,15 @@ numa notion page create --grant 41 --file page.json --json
38
38
 
39
39
  ## Jev 企业决策(开发中)
40
40
 
41
- Jev 企业接入已开始实施:定制 Skill 指导 Agent 调用平台原生 MCP,平台统一授权并经指定代理访问 OpenRouter。终端 Agent、ChatGPT 和 K8s Agent 的接口、身份及验收边界见 [Jev 详细设计](docs/design/jev-integration.md),源码包见 [numa-jev Skill](skills/numa-jev/SKILL.md) 和 [应用示例](examples/jev/README.md)。当前是未发布源码;ChatGPT 远程 OAuth 仍按设计待实施,Skill 安装不等于 MCP 已连接。
41
+ `numa jev` 直接调用平台 HTTP API,复用当前平台身份。正常任务一次提交业务请求;输入、权限、可用性和结果校验由 CLI 处理,无需先查帮助、capabilities 或执行试探性推理。
42
+
43
+ ```sh
44
+ numa jev decide --file request.json --json
45
+ ```
46
+
47
+ 输出单个 JSON,包含 `status`、`availability` 和结果或 `next_action`。输入错误返回 `needs_input`,身份/服务问题返回 `blocked`;请求中断或响应异常返回 `pending`,可能已计费,不自动重放。`--check` 与 `capabilities` 仅供主动诊断。
48
+
49
+ 目前是未发布源码,不能假定 npm latest 已包含该命令。用法见 [CLI 指引](skills/numa-jev/references/cli-usage.md),三类接入边界见 [Jev 设计](docs/design/jev-integration.md) 与 [numa-jev Skill](skills/numa-jev/SKILL.md)。平台管理上游密钥和代理;ChatGPT 远程 MCP/OAuth 为独立接入路径,CLI 不回退到 MCP。
42
50
 
43
51
  ## 显式用户授权
44
52
 
@@ -615,7 +623,10 @@ numa maintenance run --force --json
615
623
  未下发默认列表时给出 profile 配置提示;不会猜测或硬编码默认客户端。
616
624
  `skillBootstrap.enabled` 只控制登录后的自动初始化,不影响用户主动执行 install。
617
625
  多客户端共用一次下载,同目录目标去重,已有文件保留并报告 `SKIPPED_EXISTING`;
618
- 这不表示已有版本已核对。使用 `--force` 才备份替换。显式单客户端保留原有 JSON 结果结构。
626
+ 已登记且未修改的 Skill 会提示使用更新;本地自建或修改过的同名 Skill 在交互模式下询问
627
+ 是否备份覆盖,非交互需显式 `--force`。显式单客户端保留原有 JSON 成功结果结构。
628
+ 安装向导支持同时勾选多个 Skills 和 Agents;命令行也可使用
629
+ `numa skills install numa-cli numa-notion --agent claude,codex --interactive`。
619
630
 
620
631
  | 客户端 | Skill | MCP Server |
621
632
  | --- | --- | --- |
@@ -1,17 +1,20 @@
1
1
  import { addGatewayOptions, executeGatewayCommand, registerGatewayCommands, useGatewayMode } from "../mcp-gateway/commands.js";
2
2
  import { appendEnvironment, collectEnvironment, parseEnvironment, safeMcpOutput } from "../mcp/environment.js";
3
- import { completeRegistryInstall } from "../interactive/registry.js";
3
+ import { completeSkillBatchInstall, completeRegistryInstall } from "../interactive/registry.js";
4
4
  import { terminalUi, useInteractive, requireInput, confirmInstall, InstallCancelled, chooseEnvironment } from "../interactive/prompts.js";
5
- import { lstat, readFile } from "node:fs/promises";
5
+ import { readFile } from "node:fs/promises";
6
6
  import { resolve } from "node:path";
7
7
  import process from "node:process";
8
8
  import { loadAppConfig } from "../app-config.js";
9
+ import { requireProfileService } from "../platform-profile.js";
10
+ import { prepareRegistryInstanceMigration, applyRegistryInstanceMigration } from "../compatibility/index.js";
9
11
  import { loadConfig as loadPlatformConfig } from "../config.js";
10
12
  import { AiRegistryClient } from "./client.js";
11
13
  import { readMcpSpecification } from "./mcp-management.js";
12
14
  import { registerMcpRuntimeCommands } from "./mcp-runtime-commands.js";
13
15
  import { runMcpBridge } from "./bridge.js";
14
16
  import { bootstrapSkills } from "./bootstrap.js";
17
+ import { decideSkillInstall } from "./install-conflicts.js";
15
18
  import { resolveSkillInstallTargets } from "./install-defaults.js";
16
19
  import { chooseSkillUpdates } from "./update-prompt.js";
17
20
  import { recordManagedSkill, updateSkillPreferences } from "./updates.js";
@@ -23,7 +26,7 @@ import { maintenanceIdentity } from "../maintenance/identity.js";
23
26
  import { AiRegistryError, sanitizeRegistryOutput } from "./errors.js";
24
27
  import { installMcpClientDefinition, mcpDefinitionFromNacos } from "./mcp-installer.js";
25
28
  import { resolveRegistryNamespace } from "./namespace.js";
26
- import { installSkillArchive, skillTargetRoot } from "./skill-installer.js";
29
+ import { installSkillArchive, safeSkillName, skillTargetRoot } from "./skill-installer.js";
27
30
  import { assertConsumersCompatible, assertInstalledDependencies, confirmSkillDependencies, resolveSkillDependencies } from "./skill-dependencies.js";
28
31
  import { assertRouterHealth, lockRegistryWorkspace, portableInstallPath, resolveVersion, routerMcpDefinition, syncRegistryWorkspace } from "./sync.js";
29
32
  import { runRegistryTui } from "./tui.js";
@@ -275,10 +278,10 @@ export function registerAiRegistryCommands(program, dependencies = {}) {
275
278
  const result = await client.getSkill(targetNamespace.id, name);
276
279
  await output(command, { ok: true, namespace: targetNamespace, skill: result }, JSON.stringify(sanitizeRegistryOutput(result), null, 2));
277
280
  }), ["numa skills inspect release-helper --namespace team-ai --json"]);
278
- addExamples(skills.command("install [name]")
281
+ addExamples(skills.command("install [names...]")
279
282
  .option("-n, --namespace <key>", "namespaceKey")
280
283
  .option("-i, --instance <id-or-code>", "同名 namespace 的实例消歧")
281
- .option("-a, --agent <agent>", "覆盖 profile 默认客户端:chatgpt|codex|claude|cursor|workbuddy|openclaw|agents|custom")
284
+ .option("-a, --agent <agent>", "逗号分隔的目标客户端,覆盖 profile 默认:chatgpt|codex|claude|cursor|workbuddy|openclaw|agents|custom")
282
285
  .option("--scope <scope>", "user(所有工作空间)|project(当前项目)", "user")
283
286
  .option("--version <semver>", "精确版本")
284
287
  .option("--label <label>", "路由标签,例如 latest")
@@ -291,146 +294,191 @@ export function registerAiRegistryCommands(program, dependencies = {}) {
291
294
  .option("--adopt-dependencies", "与 --with-dependencies 配合,确认接管已核验的未登记旧依赖")
292
295
  .option("--interactive", "交互选择来源、Skill、版本、Agent 和安装范围")
293
296
  .option("--no-interactive", "不询问;依赖安装须显式 --with-dependencies")
294
- .action(async (name, options, command) => {
297
+ .action(async (names, options, command) => {
295
298
  const json = jsonEnabled(command, (await loadAppConfig()).output), ui = dependencies.ui ?? terminalUi;
296
- const interactive = useInteractive(options, !name || !options.agent || options.agent === "custom" && !options.customRoot, json, dependencies.tty);
299
+ const interactive = useInteractive(options, !names.length || !options.agent || Boolean(options.agent?.split(",").map(value => value.trim()).includes("custom") && !options.customRoot), json, dependencies.tty);
297
300
  if (!interactive)
298
- requireInput(name, "缺少 Skill 名称");
301
+ requireInput(names.length, "缺少 Skill 名称");
299
302
  await dependencies.initializeProfile?.();
300
303
  const client = dependencies.clientFactory?.() ?? new AiRegistryClient(loadConfig("aiRegistry"));
301
- if (interactive) {
302
- const selected = await completeRegistryInstall("skill", name, { ...options, scope: command.getOptionValueSource("scope") === "default" ? undefined : options.scope }, client, ui);
303
- name = selected.name;
304
- options = { ...options, ...selected.options, scope: selected.options.scope ?? options.scope };
305
- }
306
- requireInput(name, "缺少 Skill 名称");
307
- const targets = await resolveSkillInstallTargets(options);
308
- if (targets.scope === "project" && options.updates === "current")
309
- throw new Error("Project Skills remain locked; use registry upgrade.");
310
- const context = await resolveContext(client, options, "install");
311
- const detail = await client.getSkill(context.namespace.id, name);
312
- const trackedLabel = options.version ? undefined : options.label ?? "latest";
313
- const version = resolveVersion(detail, options.version, trackedLabel, name);
314
- const download = await client.downloadSkill(context.namespace.id, name, { version });
315
- const graph = await resolveSkillDependencies(client, context.namespace.id, { name, version, download, dependencies: [] });
316
- const dependencyNodes = graph.filter(node => node.name !== name);
317
- const source = { namespace: context.namespace.namespaceKey, instance: context.namespace.instanceCode,
318
- namespaceId: context.namespace.id, instanceId: context.namespace.instanceId };
319
- const identity = await maintenanceIdentity(loadConfig("aiRegistry"));
320
- const dependencyPlans = new Map();
321
- for (const agent of targets.agents) {
322
- const target = { agent, scope: targets.scope, workspace: options.workspace, customRoot: options.customRoot, skillName: name };
323
- const rootPath = resolve(skillTargetRoot(target), name);
324
- await assertConsumersCompatible(name, version, rootPath);
325
- if (dependencyNodes.length) {
326
- const existing = await lstat(rootPath).catch((e) => { if (e.code === "ENOENT")
327
- return undefined; throw e; });
328
- if (existing && !options.force && !options.plan)
329
- throw new AiRegistryError("Root Skill already exists; inspect local changes and explicitly approve --force before installing dependencies.", "ai_registry_install_exists");
330
- const root = { ...source, identity: identity ?? "", name, version, agent, scope: targets.scope,
331
- path: rootPath, workspace: options.workspace, customRoot: options.customRoot, id: "", archiveSha256: download.sha256,
332
- contentDigest: "", updatedAt: 0, policy: "pinned" };
333
- dependencyPlans.set(agent, { root, prepared: await prepareSkillGraph(root, dependencyNodes, client, undefined, graph.find(n => n.name === name)) });
304
+ const selections = interactive
305
+ ? await completeSkillBatchInstall(names, { ...options, scope: command.getOptionValueSource("scope") === "default" ? undefined : options.scope }, client, ui)
306
+ : [...new Set(names)].map(name => ({ name, options }));
307
+ const results = [];
308
+ const installOne = async (name, options) => {
309
+ const emit = async (_command, result, textValue) => {
310
+ if (selections.length === 1)
311
+ await output(command, result, textValue);
312
+ else
313
+ results.push({ name, result, text: textValue });
314
+ };
315
+ name = safeSkillName(name);
316
+ const targets = await resolveSkillInstallTargets(options);
317
+ if (targets.scope === "project" && options.updates === "current")
318
+ throw new Error("Project Skills remain locked; use registry upgrade.");
319
+ const context = await resolveContext(client, options, "install");
320
+ const detail = await client.getSkill(context.namespace.id, name);
321
+ const trackedLabel = options.version ? undefined : options.label ?? "latest";
322
+ const version = resolveVersion(detail, options.version, trackedLabel, name);
323
+ const decisions = new Map();
324
+ const conflictUi = !options.plan && useInteractive(options, true, json, dependencies.tty) ? ui : undefined;
325
+ for (const agent of targets.agents) {
326
+ const target = { agent, scope: targets.scope, workspace: options.workspace, skillName: name, force: options.force };
327
+ decisions.set(agent, options.plan ? { force: Boolean(options.force) }
328
+ : await decideSkillInstall(resolve(skillTargetRoot({ ...target, customRoot: options.customRoot }), name), target, conflictUi));
334
329
  }
335
- }
336
- const plan = { source, scope: targets.scope, agents: targets.agents, packages: graph.map(node => ({
337
- name: node.name, version: node.version, sha256: node.download.sha256, dependencies: node.dependencies,
338
- targets: targets.agents.map(agent => resolve(skillTargetRoot({ agent, scope: targets.scope, workspace: options.workspace, customRoot: options.customRoot }), node.name))
339
- })), dependencyTargets: [...dependencyPlans.values()].map(p => p.prepared.plan) };
340
- if (options.plan) {
341
- await output(command, { ok: true, plan }, JSON.stringify(plan, null, 2));
342
- return;
343
- }
344
- const adoptsDependencies = plan.dependencyTargets.some(p => p.packages.some(d => d.adopt));
345
- // Complete arguments skip discovery, but a TTY can still confirm dependencies.
346
- const promptDependencies = dependencyNodes.length > 0 && (!options.withDependencies || adoptsDependencies && !options.adoptDependencies)
347
- && useInteractive(options, true, json, dependencies.tty);
348
- if (interactive || promptDependencies) {
349
- if (dependencyNodes.length) {
350
- ui.note(plan.dependencyTargets.map(formatSkillUpdatePlan).join("\n")
351
- + `\n${options.force ? "备份并替换" : "安装"} ${name}@${version} · ${targets.scope}\n${plan.packages.find(p => p.name === name).targets.join("\n")}`, "安装计划");
352
- if (!(await ui.confirm(adoptsDependencies ? "确认接管上述旧依赖并安装此 Skill?" : "是否安装上述 Skill 及其依赖?")))
353
- throw new InstallCancelled();
330
+ const activeAgents = targets.agents.filter(agent => !decisions.get(agent)?.skip);
331
+ if (!activeAgents.length) {
332
+ const installations = targets.agents.map(agent => ({ agent, status: "SKIPPED_EXISTING", message: decisions.get(agent).skip, updateCommand: decisions.get(agent).updateCommand }));
333
+ await emit(command, { ok: true, installations }, installations.map(item => `${item.agent}: ${item.message}`).join("\n"));
334
+ return;
354
335
  }
355
- else
356
- await confirmInstall(ui, { ...plan, overwrite: Boolean(options.force) });
357
- options.withDependencies = true;
358
- options.adoptDependencies = true;
359
- }
360
- if (dependencyNodes.length)
361
- await confirmSkillDependencies(plan, { confirmed: options.withDependencies, json: true });
362
- if (adoptsDependencies && !options.adoptDependencies)
363
- throw new AiRegistryError("检测到未登记的旧依赖,已核验其发布版本;先查看 --plan,确认接管后使用 --with-dependencies --adopt-dependencies。", "skills_dependency_adoption_required", undefined, undefined, { plan });
364
- const choice = await chooseSkillUpdates({ choice: options.updates,
365
- eligible: targets.scope === "user" && !options.version && (!options.label || options.label === "latest"),
366
- json: !interactive || json });
367
- if (dependencyNodes.length && !identity)
368
- throw new AiRegistryError("Dependency provenance needs a verified current identity; login and retry.", "skills_dependency_identity_required");
369
- if (choice && !identity)
370
- throw new Error("Cannot associate update settings with the current identity; run numa login and retry.");
371
- const installations = [];
372
- for (const agent of targets.agents) {
373
- const dependencyInstallations = [];
374
- try {
336
+ const download = await client.downloadSkill(context.namespace.id, name, { version });
337
+ const graph = await resolveSkillDependencies(client, context.namespace.id, { name, version, download, dependencies: [] });
338
+ const dependencyNodes = graph.filter(node => node.name !== name);
339
+ const source = { namespace: context.namespace.namespaceKey, instance: context.namespace.instanceCode,
340
+ namespaceId: context.namespace.id, instanceId: context.namespace.instanceId };
341
+ const identity = await maintenanceIdentity(loadConfig("aiRegistry"));
342
+ const dependencyPlans = new Map();
343
+ for (const agent of activeAgents) {
375
344
  const target = { agent, scope: targets.scope, workspace: options.workspace, customRoot: options.customRoot, skillName: name };
376
- const dependencyPlan = dependencyPlans.get(agent);
377
- if (dependencyPlan) {
378
- await applySkillUpdate(dependencyPlan.prepared, dependencyPlan.root, undefined, {});
379
- for (const dep of dependencyPlan.prepared.plan.packages) {
380
- dependencyInstallations.push({ name: dep.name, version: dep.version, path: dep.path, backupPath: dep.backupPath,
381
- status: dep.adopt ? dep.action === "REUSE" ? "ADOPTED" : "ADOPTED_UPDATED" : dep.action === "REUSE" ? "REUSED" : dep.action === "UPDATE" ? "UPDATED" : "INSTALLED" });
382
- await recordSkillInstallation(context, options.workspace, { name: dep.name, version: dep.version, agent,
383
- scope: targets.scope, customRoot: options.customRoot }, dep.version, dep.sha256, dep.path);
345
+ const rootPath = resolve(skillTargetRoot(target), name);
346
+ await assertConsumersCompatible(name, version, rootPath);
347
+ if (dependencyNodes.length) {
348
+ const root = { ...source, identity: identity ?? "", name, version, agent, scope: targets.scope,
349
+ path: rootPath, workspace: options.workspace, customRoot: options.customRoot, id: "", archiveSha256: download.sha256,
350
+ contentDigest: "", updatedAt: 0, policy: "pinned" };
351
+ dependencyPlans.set(agent, { root, prepared: await prepareSkillGraph(root, dependencyNodes, client, undefined, graph.find(n => n.name === name)) });
352
+ }
353
+ }
354
+ const plan = { source, scope: targets.scope, agents: activeAgents, packages: graph.map(node => ({
355
+ name: node.name, version: node.version, sha256: node.download.sha256, dependencies: node.dependencies,
356
+ targets: activeAgents.map(agent => resolve(skillTargetRoot({ agent, scope: targets.scope, workspace: options.workspace, customRoot: options.customRoot }), node.name))
357
+ })), dependencyTargets: [...dependencyPlans.values()].map(p => p.prepared.plan) };
358
+ if (options.plan) {
359
+ await emit(command, { ok: true, plan }, JSON.stringify(plan, null, 2));
360
+ return;
361
+ }
362
+ // Validate/migrate registration before any Skill or dependency is written.
363
+ await registryProjectForWrite(context, options.workspace);
364
+ const adoptsDependencies = plan.dependencyTargets.some(p => p.packages.some(d => d.adopt));
365
+ // Complete arguments skip discovery, but a TTY can still confirm dependencies.
366
+ const promptDependencies = dependencyNodes.length > 0 && (!options.withDependencies || adoptsDependencies && !options.adoptDependencies)
367
+ && useInteractive(options, true, json, dependencies.tty);
368
+ if (interactive || promptDependencies) {
369
+ if (dependencyNodes.length) {
370
+ ui.note(plan.dependencyTargets.map(formatSkillUpdatePlan).join("\n")
371
+ + `\n${activeAgents.some(agent => decisions.get(agent)?.force) ? "备份并替换已有目录并安装" : "安装"} ${name}@${version} · ${targets.scope}\n${plan.packages.find(p => p.name === name).targets.join("\n")}`, "安装计划");
372
+ if (!(await ui.confirm(adoptsDependencies ? "确认接管上述旧依赖并安装此 Skill?" : "是否安装上述 Skill 及其依赖?")))
373
+ throw new InstallCancelled();
374
+ }
375
+ else
376
+ await confirmInstall(ui, { ...plan, overwriteTargets: activeAgents.filter(agent => decisions.get(agent)?.force) });
377
+ options.withDependencies = true;
378
+ options.adoptDependencies = true;
379
+ }
380
+ if (dependencyNodes.length)
381
+ await confirmSkillDependencies(plan, { confirmed: options.withDependencies, json: true });
382
+ if (adoptsDependencies && !options.adoptDependencies)
383
+ throw new AiRegistryError("检测到未登记的旧依赖,已核验其发布版本;先查看 --plan,确认接管后使用 --with-dependencies --adopt-dependencies。", "skills_dependency_adoption_required", undefined, undefined, { plan });
384
+ const choice = await chooseSkillUpdates({ choice: options.updates,
385
+ eligible: targets.scope === "user" && !options.version && (!options.label || options.label === "latest"),
386
+ json: !interactive || json });
387
+ if (dependencyNodes.length && !identity)
388
+ throw new AiRegistryError("Dependency provenance needs a verified current identity; login and retry.", "skills_dependency_identity_required");
389
+ if (choice && !identity)
390
+ throw new Error("Cannot associate update settings with the current identity; run numa login and retry.");
391
+ const installations = [];
392
+ for (const agent of targets.agents) {
393
+ const decision = decisions.get(agent);
394
+ if (decision.skip) {
395
+ installations.push({ agent, status: "SKIPPED_EXISTING", message: decision.skip, updateCommand: decision.updateCommand });
396
+ continue;
397
+ }
398
+ const dependencyInstallations = [];
399
+ try {
400
+ const target = { agent, scope: targets.scope, workspace: options.workspace, customRoot: options.customRoot, skillName: name };
401
+ const dependencyPlan = dependencyPlans.get(agent);
402
+ if (dependencyPlan) {
403
+ await applySkillUpdate(dependencyPlan.prepared, dependencyPlan.root, undefined, {});
404
+ for (const dep of dependencyPlan.prepared.plan.packages) {
405
+ dependencyInstallations.push({ name: dep.name, version: dep.version, path: dep.path, backupPath: dep.backupPath,
406
+ status: dep.adopt ? dep.action === "REUSE" ? "ADOPTED" : "ADOPTED_UPDATED" : dep.action === "REUSE" ? "REUSED" : dep.action === "UPDATE" ? "UPDATED" : "INSTALLED" });
407
+ await recordSkillInstallation(context, options.workspace, { name: dep.name, version: dep.version, agent,
408
+ scope: targets.scope, customRoot: options.customRoot }, dep.version, dep.sha256, dep.path);
409
+ }
410
+ }
411
+ await assertInstalledDependencies(download.bytes, resolve(skillTargetRoot(target), name), source);
412
+ const result = await installSkillArchive(download.bytes, {
413
+ dependencyPlanVerified: true,
414
+ agent, scope: targets.scope, skillName: name,
415
+ workspace: options.workspace, customRoot: options.customRoot, force: decision.force
416
+ });
417
+ const lock = await recordSkillInstallation(context, options.workspace, {
418
+ name, version: options.version, label: trackedLabel, agent,
419
+ scope: targets.scope, customRoot: options.customRoot
420
+ }, version, download.sha256, result.installedPath);
421
+ const tracking = identity ? await recordManagedSkill({ identity, name, namespace: context.namespace.namespaceKey,
422
+ instance: context.namespace.instanceCode, namespaceId: context.namespace.id, instanceId: context.namespace.instanceId,
423
+ agent, scope: targets.scope, path: result.installedPath, workspace: options.workspace, customRoot: options.customRoot,
424
+ version, archiveSha256: download.sha256, contentDigest: result.contentDigest,
425
+ preservePolicy: !choice && !options.version && (!options.label || options.label === "latest"),
426
+ policy: choice === "current" ? "latest" : choice === "never" ? "off"
427
+ : options.version || options.label && options.label !== "latest" ? "pinned" : "inherit" }) : undefined;
428
+ if (choice === "all" || choice === "never")
429
+ await updateSkillPreferences(choice);
430
+ if (targets.source === "explicit" && targets.agents.length === 1) {
431
+ await emit(command, { ok: true, namespace: context.namespace, resolvedVersion: version, dependencyPlan: plan, dependencyInstallations, download: { sha256: download.sha256, fileName: download.fileName }, installation: result, lock,
432
+ updateTracking: tracking ? { id: tracking.id, policy: tracking.policy, choice } : { policy: "unregistered" } }, `已安装 ${name}@${version}: ${result.installedPath}${dependencyInstallations.map(d => `\n依赖 ${d.name}@${d.version}: ${{ REUSED: "复用", INSTALLED: "已安装", UPDATED: "已升级", ADOPTED: "已接管", ADOPTED_UPDATED: "已接管并升级" }[d.status]}${d.backupPath ? `(备份:${d.backupPath})` : ""}`).join("")}${result.backupPath ? `\n原版本备份: ${result.backupPath}` : ""}\n更新策略: ${tracking?.policy ?? "未登记"}${choice ? ` (${choice})` : ""};numa skills updates status 查看。`);
433
+ return;
384
434
  }
435
+ installations.push({ agent, status: "INSTALLED", installation: result, lock, dependencyInstallations,
436
+ updateTracking: tracking ? { id: tracking.id, policy: tracking.policy, choice } : { policy: "unregistered" } });
385
437
  }
386
- await assertInstalledDependencies(download.bytes, resolve(skillTargetRoot(target), name), source);
387
- const result = await installSkillArchive(download.bytes, {
388
- dependencyPlanVerified: true,
389
- agent, scope: targets.scope, skillName: name,
390
- workspace: options.workspace, customRoot: options.customRoot, force: options.force
391
- });
392
- const lock = await recordSkillInstallation(context, options.workspace, {
393
- name, version: options.version, label: trackedLabel, agent,
394
- scope: targets.scope, customRoot: options.customRoot
395
- }, version, download.sha256, result.installedPath);
396
- const tracking = identity ? await recordManagedSkill({ identity, name, namespace: context.namespace.namespaceKey,
397
- instance: context.namespace.instanceCode, namespaceId: context.namespace.id, instanceId: context.namespace.instanceId,
398
- agent, scope: targets.scope, path: result.installedPath, workspace: options.workspace, customRoot: options.customRoot,
399
- version, archiveSha256: download.sha256, contentDigest: result.contentDigest,
400
- preservePolicy: !choice && !options.version && (!options.label || options.label === "latest"),
401
- policy: choice === "current" ? "latest" : choice === "never" ? "off"
402
- : options.version || options.label && options.label !== "latest" ? "pinned" : "inherit" }) : undefined;
403
- if (choice === "all" || choice === "never")
404
- await updateSkillPreferences(choice);
405
- if (targets.source === "explicit") {
406
- await output(command, { ok: true, namespace: context.namespace, resolvedVersion: version, dependencyPlan: plan, dependencyInstallations, download: { sha256: download.sha256, fileName: download.fileName }, installation: result, lock,
407
- updateTracking: tracking ? { id: tracking.id, policy: tracking.policy, choice } : { policy: "unregistered" } }, `已安装 ${name}@${version}: ${result.installedPath}${dependencyInstallations.map(d => `\n依赖 ${d.name}@${d.version}: ${{ REUSED: "复用", INSTALLED: "已安装", UPDATED: "已升级", ADOPTED: "已接管", ADOPTED_UPDATED: "已接管并升级" }[d.status]}${d.backupPath ? `(备份:${d.backupPath})` : ""}`).join("")}${result.backupPath ? `\n原版本备份: ${result.backupPath}` : ""}\n更新策略: ${tracking?.policy ?? "未登记"}${choice ? ` (${choice})` : ""};numa skills updates status 查看。`);
408
- return;
438
+ catch (error) {
439
+ if (targets.source === "explicit" && targets.agents.length === 1) {
440
+ if (!dependencyInstallations.length)
441
+ throw error;
442
+ throw new AiRegistryError(error instanceof AiRegistryError ? error.message : "Skill installation failed; some dependencies may already be installed.", error instanceof AiRegistryError ? error.code : "skills_install_failed", undefined, undefined, { plan, dependencyInstallations, partial: true });
443
+ }
444
+ const existing = error instanceof AiRegistryError && error.code === "ai_registry_install_exists";
445
+ installations.push({ agent, status: existing ? "SKIPPED_EXISTING" : "FAILED", dependencyInstallations,
446
+ code: error instanceof AiRegistryError ? error.code : "skills_install_failed",
447
+ message: existing ? "已保留已有文件,未验证其版本;使用 --force 可备份后替换。"
448
+ : error instanceof AiRegistryError ? error.message : "安装失败,请检查目标目录后重试。" });
409
449
  }
410
- installations.push({ agent, status: "INSTALLED", installation: result, lock, dependencyInstallations,
411
- updateTracking: tracking ? { id: tracking.id, policy: tracking.policy, choice } : { policy: "unregistered" } });
450
+ }
451
+ const ok = installations.every((item) => item.status !== "FAILED");
452
+ await emit(command, { ok, namespace: context.namespace, resolvedVersion: version, dependencyPlan: plan,
453
+ download: { sha256: download.sha256, fileName: download.fileName }, source: targets.source,
454
+ scope: targets.scope, installations }, `${name}@${version}(${targets.source === "profile" ? "profile 默认客户端" : "所选客户端"},${targets.scope})\n${installations.map((item) => `${item.agent}\t${item.status}\t${"installation" in item ? item.installation?.installedPath : item.message}`).join("\n")}`);
455
+ if (!ok)
456
+ process.exitCode = 1;
457
+ };
458
+ for (const selected of selections) {
459
+ try {
460
+ await installOne(selected.name, { ...options, ...selected.options, scope: selected.options.scope ?? options.scope });
412
461
  }
413
462
  catch (error) {
414
- if (targets.source === "explicit") {
415
- if (!dependencyInstallations.length)
416
- throw error;
417
- throw new AiRegistryError(error instanceof AiRegistryError ? error.message : "Skill installation failed; some dependencies may already be installed.", error instanceof AiRegistryError ? error.code : "skills_install_failed", undefined, undefined, { plan, dependencyInstallations, partial: true });
463
+ if (selections.length === 1)
464
+ throw error;
465
+ if (error instanceof InstallCancelled) {
466
+ results.push({ name: selected.name, result: { ok: false, code: error.code, message: error.message }, text: `${selected.name}: ${error.message}` });
467
+ process.exitCode = 1;
468
+ break;
418
469
  }
419
- const existing = error instanceof AiRegistryError && error.code === "ai_registry_install_exists";
420
- installations.push({ agent, status: existing ? "SKIPPED_EXISTING" : "FAILED", dependencyInstallations,
421
- code: error instanceof AiRegistryError ? error.code : "skills_install_failed",
422
- message: existing ? "已保留已有文件,未验证其版本;使用 --force 可备份后替换。"
423
- : error instanceof AiRegistryError ? error.message : "安装失败,请检查目标目录后重试。" });
470
+ const message = error instanceof AiRegistryError ? error.message : "安装失败,请检查目标目录后重试。";
471
+ results.push({ name: selected.name, result: { ok: false, code: error instanceof AiRegistryError ? error.code : "skills_install_failed", message }, text: `${selected.name}: ${message}` });
472
+ process.exitCode = 1;
424
473
  }
425
474
  }
426
- const ok = installations.every((item) => item.status !== "FAILED");
427
- await output(command, { ok, namespace: context.namespace, resolvedVersion: version, dependencyPlan: plan,
428
- download: { sha256: download.sha256, fileName: download.fileName }, source: targets.source,
429
- scope: targets.scope, installations }, `${name}@${version}(profile 默认客户端,${targets.scope})\n${installations.map((item) => `${item.agent}\t${item.status}\t${"installation" in item ? item.installation?.installedPath : item.message}`).join("\n")}`);
430
- if (!ok)
431
- process.exitCode = 1;
475
+ if (selections.length > 1) {
476
+ const ok = results.every(item => item.result.ok !== false);
477
+ await output(command, { ok, results: results.map(({ name, result }) => ({ name, result })) }, results.map(item => item.text).join("\n\n"));
478
+ }
432
479
  }), [
433
480
  "numa skills install numa-cli",
481
+ "numa skills install numa-cli numa-notion --agent claude,codex --interactive",
434
482
  "numa skills install release-helper --namespace team-ai --agent claude",
435
483
  "numa skills install release-helper --namespace team-ai --instance cluster-b --agent cursor --scope user --label latest --json"
436
484
  ]);
@@ -635,6 +683,7 @@ export function registerAiRegistryCommands(program, dependencies = {}) {
635
683
  await output(command, safeMcpOutput({ ok: true, plan }), JSON.stringify(safeMcpOutput(plan), null, 2));
636
684
  return;
637
685
  }
686
+ await registryProjectForWrite(context, options.workspace);
638
687
  if (interactive)
639
688
  await confirmInstall(ui, plan);
640
689
  const result = await installMcpClientDefinition(name, definition, {
@@ -918,7 +967,21 @@ async function registryProjectForWrite(context, workspace) {
918
967
  mcpServers: []
919
968
  };
920
969
  if (manifest.namespaceKey !== context.namespace.namespaceKey || (manifest.instanceCode && manifest.instanceCode !== context.namespace.instanceCode)) {
921
- throw new AiRegistryError("Existing registry manifest points to a different namespace binding.", "ai_registry_manifest_namespace_mismatch");
970
+ // Explicit/interactive selection bypasses project-source resolution. Still
971
+ // migrate a confirmed historical alias when it matches this authorized target.
972
+ const migration = manifest.namespaceKey === context.namespace.namespaceKey
973
+ ? prepareRegistryInstanceMigration({
974
+ baseUrl: requireProfileService("aiRegistry")?.service.baseUrl,
975
+ namespace: manifest.namespaceKey, instance: manifest.instanceCode,
976
+ source: "project", capabilities: context.capabilities
977
+ }) : undefined;
978
+ if (existing && migration && migration.to === context.namespace.instanceCode) {
979
+ await applyRegistryInstanceMigration(existing.files, context.namespace, migration);
980
+ return registryProjectForWrite(context, workspace);
981
+ }
982
+ throw new AiRegistryError(`Registry manifest ${files.manifestPath} binds namespace=${manifest.namespaceKey}, instance=${manifest.instanceCode ?? "(unspecified)"}; ` +
983
+ `selected namespace=${context.namespace.namespaceKey}, instance=${context.namespace.instanceCode}. ` +
984
+ "No verified legacy migration matches; existing configuration was preserved.", "ai_registry_manifest_namespace_mismatch");
922
985
  }
923
986
  const lock = await readRegistryLock(files, false) ?? {
924
987
  schemaVersion: 1,