@sakanano-moe/agenthelm 6.5.3 → 6.7.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.
Files changed (112) hide show
  1. package/README.md +68 -0
  2. package/README.zh-CN.md +28 -0
  3. package/bin/agenthelm.js +1 -1
  4. package/package.json +2 -2
  5. package/runtime/index.json +80 -0
  6. package/runtime/plugins/agent-pack/.codex-plugin/plugin.json +26 -0
  7. package/runtime/plugins/agent-pack/.mcp.json +12 -0
  8. package/runtime/plugins/agent-pack/hooks/hooks.json +16 -0
  9. package/runtime/plugins/agent-pack/mcp.json +14 -0
  10. package/runtime/plugins/agent-pack/plugin.json +20 -0
  11. package/runtime/plugins/agent-pack/references/capability-config.md +148 -0
  12. package/runtime/plugins/agent-pack/runtime/agent_pack_claude.py +448 -0
  13. package/runtime/{agent_pack_config.py → plugins/agent-pack/runtime/agent_pack_config.py} +178 -22
  14. package/runtime/{agenthelm.py → plugins/agent-pack/runtime/agenthelm.py} +42 -1
  15. package/runtime/plugins/agent-pack/runtime/agenthelm_runtime.py +388 -0
  16. package/runtime/{mcp_server.py → plugins/agent-pack/runtime/mcp_server.py} +30 -8
  17. package/runtime/plugins/agent-pack/skills/agent-pack/SKILL.md +46 -0
  18. package/runtime/plugins/agents-init/.codex-plugin/plugin.json +22 -0
  19. package/runtime/plugins/agents-init/content.json +31 -0
  20. package/runtime/plugins/agents-init/plugin.json +19 -0
  21. package/runtime/plugins/agents-init/skills/agents-init/AGENT.template.md +376 -0
  22. package/runtime/plugins/agents-init/skills/agents-init/SKILL.md +119 -0
  23. package/runtime/plugins/agents-init/skills/agents-init/index.html +357 -0
  24. package/runtime/plugins/agents-init/skills/agents-init/launcher.sh +93 -0
  25. package/runtime/plugins/agents-init/skills/agents-init/project.schema.json +171 -0
  26. package/runtime/plugins/agents-init/skills/agents-init/scripts/init_project.py +1190 -0
  27. package/runtime/plugins/agents-init/skills/agents-init/scripts/test_init_project.py +678 -0
  28. package/runtime/plugins/agents-init/skills/agents-init/skill.json +122 -0
  29. package/runtime/plugins/opinion-manager/.codex-plugin/plugin.json +26 -0
  30. package/runtime/plugins/opinion-manager/catalog/README.md +15 -0
  31. package/runtime/plugins/opinion-manager/catalog/rules/.gitkeep +0 -0
  32. package/runtime/plugins/opinion-manager/catalog/rules/om.align-requirements/versions/1.0.0.json +11 -0
  33. package/runtime/plugins/opinion-manager/catalog/rules/om.assumptions-authority/versions/1.0.0.json +11 -0
  34. package/runtime/plugins/opinion-manager/catalog/rules/om.batch-questions/versions/1.0.0.json +11 -0
  35. package/runtime/plugins/opinion-manager/catalog/rules/om.bilingual-terms/versions/1.0.0.json +11 -0
  36. package/runtime/plugins/opinion-manager/catalog/rules/om.calculations/versions/1.0.0.json +11 -0
  37. package/runtime/plugins/opinion-manager/catalog/rules/om.chinese-response/versions/1.0.0.json +11 -0
  38. package/runtime/plugins/opinion-manager/catalog/rules/om.clean-deliverable/versions/1.0.0.json +11 -0
  39. package/runtime/plugins/opinion-manager/catalog/rules/om.complete-options/versions/1.0.0.json +11 -0
  40. package/runtime/plugins/opinion-manager/catalog/rules/om.conclusion-first/versions/1.0.0.json +11 -0
  41. package/runtime/plugins/opinion-manager/catalog/rules/om.concrete-titles/versions/1.0.0.json +11 -0
  42. package/runtime/plugins/opinion-manager/catalog/rules/om.consistent-items/versions/1.0.0.json +11 -0
  43. package/runtime/plugins/opinion-manager/catalog/rules/om.device-readable/versions/1.0.0.json +11 -0
  44. package/runtime/plugins/opinion-manager/catalog/rules/om.editable-diagrams/versions/1.0.0.json +11 -0
  45. package/runtime/plugins/opinion-manager/catalog/rules/om.evidence-inference/versions/1.0.0.json +11 -0
  46. package/runtime/plugins/opinion-manager/catalog/rules/om.maintenance-chinese/versions/1.0.0.json +11 -0
  47. package/runtime/plugins/opinion-manager/catalog/rules/om.maintenance-project-language/versions/1.0.0.json +11 -0
  48. package/runtime/plugins/opinion-manager/catalog/rules/om.markdown-structure/versions/1.0.0.json +11 -0
  49. package/runtime/plugins/opinion-manager/catalog/rules/om.portable-files/versions/1.0.0.json +11 -0
  50. package/runtime/plugins/opinion-manager/catalog/rules/om.report-context/versions/1.0.0.json +11 -0
  51. package/runtime/plugins/opinion-manager/catalog/rules/om.report-formats/versions/1.0.0.json +11 -0
  52. package/runtime/plugins/opinion-manager/catalog/rules/om.report-review/versions/1.0.0.json +11 -0
  53. package/runtime/plugins/opinion-manager/catalog/rules/om.response-brief/versions/1.0.0.json +11 -0
  54. package/runtime/plugins/opinion-manager/catalog/rules/om.response-dense/versions/1.0.0.json +11 -0
  55. package/runtime/plugins/opinion-manager/catalog/rules/om.rules-tools/versions/1.0.0.json +11 -0
  56. package/runtime/plugins/opinion-manager/catalog/rules/om.semantic-wrap/versions/1.0.0.json +11 -0
  57. package/runtime/plugins/opinion-manager/catalog/rules/om.solutions-proportional/versions/1.0.0.json +11 -0
  58. package/runtime/plugins/opinion-manager/catalog/rules/om.solutions-three-level/versions/1.0.0.json +11 -0
  59. package/runtime/plugins/opinion-manager/catalog/rules/om.tables-compact/versions/1.0.0.json +11 -0
  60. package/runtime/plugins/opinion-manager/catalog/rules/om.tables-list-first/versions/1.0.0.json +11 -0
  61. package/runtime/plugins/opinion-manager/catalog/rules/om.test-evidence/versions/1.0.0.json +11 -0
  62. package/runtime/plugins/opinion-manager/catalog/rules/om.versioned-files/versions/1.0.0.json +11 -0
  63. package/runtime/plugins/opinion-manager/catalog/templates/.gitkeep +0 -0
  64. package/runtime/plugins/opinion-manager/catalog/templates/delivery-standard/compact-tables/versions/1.0.0.json +21 -0
  65. package/runtime/plugins/opinion-manager/catalog/templates/delivery-standard/list-first/versions/1.0.0.json +21 -0
  66. package/runtime/plugins/opinion-manager/catalog/templates/development-standard/chinese-maintenance/versions/1.0.0.json +15 -0
  67. package/runtime/plugins/opinion-manager/catalog/templates/development-standard/project-language/versions/1.0.0.json +15 -0
  68. package/runtime/plugins/opinion-manager/catalog/templates/response-style/brief-chinese/versions/1.0.0.json +19 -0
  69. package/runtime/plugins/opinion-manager/catalog/templates/response-style/dense-chinese/versions/1.0.0.json +19 -0
  70. package/runtime/plugins/opinion-manager/catalog/templates/scenario/concise-project/versions/1.0.0.json +39 -0
  71. package/runtime/plugins/opinion-manager/catalog/templates/scenario/reviewed-project/versions/1.0.0.json +39 -0
  72. package/runtime/plugins/opinion-manager/catalog/templates/work-method/proportional/versions/1.0.0.json +20 -0
  73. package/runtime/plugins/opinion-manager/catalog/templates/work-method/three-level/versions/1.0.0.json +20 -0
  74. package/runtime/plugins/opinion-manager/content.json +63 -0
  75. package/runtime/plugins/opinion-manager/plugin.json +18 -0
  76. package/runtime/plugins/opinion-manager/references/lock.schema.json +14 -0
  77. package/runtime/plugins/opinion-manager/references/onboarding.md +57 -0
  78. package/runtime/plugins/opinion-manager/references/profile.schema.json +56 -0
  79. package/runtime/plugins/opinion-manager/references/rule.schema.json +16 -0
  80. package/runtime/plugins/opinion-manager/references/template.schema.json +71 -0
  81. package/runtime/plugins/opinion-manager/references/versioned-template.schema.json +19 -0
  82. package/runtime/plugins/opinion-manager/references/versioning.md +263 -0
  83. package/runtime/plugins/opinion-manager/runtime/constants.py +7 -0
  84. package/runtime/plugins/opinion-manager/runtime/context.py +180 -0
  85. package/runtime/plugins/opinion-manager/runtime/lifecycle.py +395 -0
  86. package/runtime/plugins/opinion-manager/runtime/opinion_manager.py +419 -0
  87. package/runtime/plugins/opinion-manager/runtime/versioned.py +705 -0
  88. package/runtime/plugins/opinion-manager/skills/opinion-manager/SKILL.md +62 -0
  89. package/runtime/plugins/opinion-manager/skills/opinion-manager/agents/openai.yaml +4 -0
  90. package/runtime/plugins/opinion-manager/templates/.gitkeep +1 -0
  91. package/runtime/plugins/project-orchestrator/.codex-plugin/plugin.json +24 -0
  92. package/runtime/plugins/project-orchestrator/content.json +37 -0
  93. package/runtime/plugins/project-orchestrator/plugin.json +19 -0
  94. package/runtime/plugins/project-orchestrator/references/asset-synonyms.example.json +8 -0
  95. package/runtime/plugins/project-orchestrator/references/case-workspaces.md +159 -0
  96. package/runtime/plugins/project-orchestrator/references/case.schema.json +31 -0
  97. package/runtime/plugins/project-orchestrator/runtime/assets.py +1288 -0
  98. package/runtime/plugins/project-orchestrator/runtime/case.py +557 -0
  99. package/runtime/plugins/project-orchestrator/runtime/case_workspace.py +350 -0
  100. package/runtime/plugins/project-orchestrator/runtime/memory.py +363 -0
  101. package/runtime/plugins/project-orchestrator/runtime/project_base.py +107 -0
  102. package/runtime/plugins/project-orchestrator/runtime/project_cli.py +599 -0
  103. package/runtime/plugins/project-orchestrator/runtime/project_demo.py +504 -0
  104. package/runtime/plugins/project-orchestrator/runtime/project_maintain.py +470 -0
  105. package/runtime/plugins/project-orchestrator/runtime/project_menu.py +394 -0
  106. package/runtime/plugins/project-orchestrator/runtime/project_real.py +522 -0
  107. package/runtime/plugins/project-orchestrator/runtime/project_write.py +329 -0
  108. package/runtime/plugins/project-orchestrator/runtime/wf.py +439 -0
  109. package/runtime/plugins/project-orchestrator/skills/project-orchestrator/SKILL.md +230 -0
  110. package/runtime/plugins/project-orchestrator/skills/project-orchestrator/agents/openai.yaml +4 -0
  111. package/runtime/plugins/project-orchestrator/templates/CASE.md +39 -0
  112. /package/runtime/{mcp_env_launcher.py → plugins/agent-pack/runtime/mcp_env_launcher.py} +0 -0
@@ -1019,8 +1019,15 @@ def install_plugin(
1019
1019
  apply: bool = False,
1020
1020
  replace: bool = False,
1021
1021
  recovery_root: Path | None = None,
1022
+ skills: Iterable[str] = (),
1023
+ mcps: Iterable[str] = (),
1024
+ reset_overrides: bool = False,
1022
1025
  ) -> dict[str, Any]:
1023
- """Bootstrap a portable plugin into a supported client without overwriting."""
1026
+ """Bootstrap a portable plugin into a supported client without overwriting.
1027
+
1028
+ For Claude Code, ``skills`` / ``mcps`` limit the install to those components
1029
+ and local credential values on MCP entries are kept unless ``reset_overrides``.
1030
+ """
1024
1031
  plugin = plugin.expanduser().resolve()
1025
1032
  manifest, issues = validate_plugin_manifest(plugin)
1026
1033
  if issues:
@@ -1030,8 +1037,11 @@ def install_plugin(
1030
1037
  home = (home or Path.home()).expanduser().resolve()
1031
1038
  if client == "claude":
1032
1039
  return _install_plugin_claude(
1033
- plugin, name, home=home, apply=apply, replace=replace, recovery_root=recovery_root
1040
+ plugin, name, home=home, apply=apply, replace=replace, recovery_root=recovery_root,
1041
+ select_skills=set(skills), select_mcps=set(mcps), reset_overrides=reset_overrides,
1034
1042
  )
1043
+ if skills or mcps:
1044
+ raise AgentPackError("--skill / --mcp selection is supported for the claude client only")
1035
1045
  destination = _client_package_destination(client, home, name)
1036
1046
  action = "install-plugin"
1037
1047
  if destination.exists() or destination.is_symlink():
@@ -1103,6 +1113,9 @@ def _install_plugin_claude(
1103
1113
  apply: bool,
1104
1114
  replace: bool,
1105
1115
  recovery_root: Path | None,
1116
+ select_skills: set[str] | None = None,
1117
+ select_mcps: set[str] | None = None,
1118
+ reset_overrides: bool = False,
1106
1119
  ) -> dict[str, Any]:
1107
1120
  """Install a plugin into Claude Code as live links plus user-scope MCP entries.
1108
1121
 
@@ -1112,24 +1125,42 @@ def _install_plugin_claude(
1112
1125
  record ``~/.claude/agent-pack/plugins/<name>`` (symlink to the source root)
1113
1126
  that inventory uses to report the installed version.
1114
1127
  """
1128
+ import agent_pack_claude # noqa: PLC0415 - shipped next to this file
1129
+
1130
+ select_skills, select_mcps = select_skills or set(), select_mcps or set()
1131
+ selective = bool(select_skills or select_mcps)
1115
1132
  record = _claude_plugin_records(home) / name
1116
1133
  links: list[tuple[str, str, Path, Path]] = [("plugin", name, record, plugin)]
1134
+ available_skills: set[str] = set()
1117
1135
  skills_root = plugin / "skills"
1118
1136
  if skills_root.is_dir() and not skills_root.is_symlink():
1119
1137
  for child in sorted(skills_root.iterdir(), key=lambda item: item.name):
1120
1138
  if child.is_dir() and not child.name.startswith(".") and (child / "SKILL.md").is_file():
1121
- links.append(("skill", child.name, home / ".claude" / "skills" / child.name, child))
1139
+ available_skills.add(child.name)
1140
+ if not selective or child.name in select_skills:
1141
+ links.append(("skill", child.name, home / ".claude" / "skills" / child.name, child))
1142
+ unknown_skills = select_skills - available_skills
1143
+ if unknown_skills:
1144
+ raise AgentPackError("plugin has no such Skill: " + ", ".join(sorted(unknown_skills)))
1122
1145
 
1123
1146
  config_path = _claude_user_config(home)
1124
1147
  servers: dict[str, dict[str, Any]] = {}
1125
1148
  mcp_path = plugin / "mcp.json"
1126
1149
  if mcp_path.is_file():
1127
1150
  data_root = home / ".claude" / "agent-pack" / "data" / name
1128
- for server, raw in sorted(_read_json(mcp_path).get("mcpServers", {}).items()):
1151
+ declared = _read_json(mcp_path).get("mcpServers", {})
1152
+ unknown_mcps = select_mcps - set(declared)
1153
+ if unknown_mcps:
1154
+ raise AgentPackError("plugin has no such MCP server: " + ", ".join(sorted(unknown_mcps)))
1155
+ for server, raw in sorted(declared.items()):
1156
+ if selective and server not in select_mcps:
1157
+ continue
1129
1158
  parsed = _portable_mcp(server, raw)
1130
1159
  if not parsed.portable:
1131
1160
  raise AgentPackError(f"MCP {server} is not portable: " + "; ".join(parsed.issues))
1132
1161
  servers[server] = _claude_mcp_config(parsed.config, record, data_root)
1162
+ elif select_mcps:
1163
+ raise AgentPackError("plugin has no mcp.json: " + ", ".join(sorted(select_mcps)))
1133
1164
 
1134
1165
  config: dict[str, Any] = {}
1135
1166
  if servers and config_path.exists():
@@ -1151,16 +1182,17 @@ def _install_plugin_claude(
1151
1182
  else:
1152
1183
  action = "link"
1153
1184
  operations.append({"action": f"{action}-{kind}", "client": "claude", "name": item_name, "to": str(path)})
1185
+ entries: dict[str, dict[str, Any]] = {}
1154
1186
  for server, rendered in servers.items():
1155
- current = existing_servers.get(server)
1156
- if current is None:
1157
- action = "add"
1158
- elif current == rendered:
1159
- action = "keep"
1160
- else:
1161
- action = "replace"
1187
+ state, entries[server], kept = agent_pack_claude.classify_mcp(
1188
+ rendered, existing_servers.get(server), reset=reset_overrides)
1189
+ action = {"differ": "replace"}.get(state, state)
1190
+ if action == "replace":
1162
1191
  conflicts.append(f"mcp {server}: {config_path}")
1163
- operations.append({"action": f"{action}-mcp", "client": "claude", "name": server, "to": str(config_path)})
1192
+ operation = {"action": f"{action}-mcp", "client": "claude", "name": server, "to": str(config_path)}
1193
+ if kept:
1194
+ operation["kept_overrides"] = kept
1195
+ operations.append(operation)
1164
1196
  if conflicts and not replace:
1165
1197
  raise AgentPackError("Claude client already has different content: " + "; ".join(conflicts))
1166
1198
  result: dict[str, Any] = {"ok": True, "applied": apply, "operations": operations}
@@ -1192,8 +1224,8 @@ def _install_plugin_claude(
1192
1224
  os.symlink(target, path, target_is_directory=True)
1193
1225
  created.append(path)
1194
1226
  changed = {
1195
- server: rendered for server, rendered in servers.items()
1196
- if existing_servers.get(server) != rendered
1227
+ server: entry for server, entry in entries.items()
1228
+ if existing_servers.get(server) != entry
1197
1229
  }
1198
1230
  if changed:
1199
1231
  replaced = {server: existing_servers[server] for server in changed if server in existing_servers}
@@ -1287,12 +1319,23 @@ def reconcile_project(
1287
1319
  recovery_root: Path | None = None,
1288
1320
  apply: bool = False,
1289
1321
  retire_only: bool = False,
1322
+ replace: bool = False,
1323
+ reset_overrides: bool = False,
1290
1324
  ) -> dict[str, Any]:
1291
1325
  """Reconcile one project package and explicitly selected legacy components.
1292
1326
 
1293
1327
  ``retire_only`` archives the selected legacy components without installing
1294
- or replacing the project package itself.
1328
+ or replacing the project package itself. Claude Code is handled by
1329
+ ``agent_pack_claude`` (dedicated package, MCP entries, Skill links);
1330
+ ``replace`` and ``reset_overrides`` only apply there.
1295
1331
  """
1332
+ if client == "claude":
1333
+ import agent_pack_claude # noqa: PLC0415 - shipped next to this file
1334
+
1335
+ return agent_pack_claude.sync_project(
1336
+ project, home=home, apply=apply, replace=replace, reset_overrides=reset_overrides,
1337
+ recovery_root=recovery_root, retire_skills=retire_skills, retire_plugins=retire_plugins,
1338
+ retire_only=retire_only)
1296
1339
  layout = load_project_layout(project)
1297
1340
  authority = inventory_project(layout.root)
1298
1341
  package_name = _package_name(layout.root)
@@ -1402,8 +1445,12 @@ def sync_project(
1402
1445
  *,
1403
1446
  home: Path | None = None,
1404
1447
  apply: bool = False,
1448
+ replace: bool = False,
1449
+ reset_overrides: bool = False,
1450
+ recovery_root: Path | None = None,
1405
1451
  ) -> dict[str, Any]:
1406
- return reconcile_project(project, client, home=home, apply=apply)
1452
+ return reconcile_project(project, client, home=home, apply=apply, replace=replace,
1453
+ reset_overrides=reset_overrides, recovery_root=recovery_root)
1407
1454
 
1408
1455
 
1409
1456
  def _update_personal_marketplace(home: Path, package_name: str) -> None:
@@ -1425,7 +1472,81 @@ def _update_personal_marketplace(home: Path, package_name: str) -> None:
1425
1472
  _write_json_atomic(marketplace, payload)
1426
1473
 
1427
1474
 
1428
- def doctor(project: Path) -> dict[str, Any]:
1475
+ def _skill_fingerprints(package: Path) -> dict[str, str]:
1476
+ root = package / "skills"
1477
+ if not root.is_dir() or root.is_symlink():
1478
+ return {}
1479
+ found = {}
1480
+ for child in sorted(root.iterdir()):
1481
+ if child.is_dir() and not child.is_symlink() and (child / "SKILL.md").is_file():
1482
+ try:
1483
+ found[child.name] = _tree_fingerprint(child)
1484
+ except AgentPackError:
1485
+ found[child.name] = "unreadable"
1486
+ return found
1487
+
1488
+
1489
+ def _package_mcps(package: Path) -> dict[str, Any]:
1490
+ path = package / "mcp.json"
1491
+ if not path.is_file() or path.is_symlink():
1492
+ return {}
1493
+ try:
1494
+ servers = _read_json(path).get("mcpServers", {})
1495
+ except AgentPackError:
1496
+ return {}
1497
+ return servers if isinstance(servers, dict) else {}
1498
+
1499
+
1500
+ def client_drift(project: Path, client: str, home: Path | None = None) -> dict[str, Any]:
1501
+ """Compare what a client has installed for this project with the project's authority.
1502
+
1503
+ Codex and Cursor load the project package directory, so the installed package
1504
+ is compared with a freshly built one (Skills, MCP servers, the rest of the
1505
+ files). Claude Code is compared entry by entry by ``agent_pack_claude``.
1506
+ Same-name MCP servers configured by hand in the client are listed as native.
1507
+ """
1508
+ if client == "claude":
1509
+ import agent_pack_claude # noqa: PLC0415 - shipped next to this file
1510
+
1511
+ return agent_pack_claude.drift(project, home)
1512
+ home = (home or Path.home()).expanduser().resolve()
1513
+ layout = load_project_layout(project)
1514
+ authority = inventory_project(layout.root)
1515
+ name = _package_name(layout.root)
1516
+ destination = _client_package_destination(client, home, name)
1517
+ report: dict[str, Any] = {"client": client, "package": name, "missing": [], "different": [], "overrides": [],
1518
+ "stale": [], "native": [], "excluded": [], "dangling": [], "coupled": None}
1519
+ report["excluded"] = sorted(item.name for item in authority.mcps
1520
+ if item.portable and not _mcp_allowed(layout, item.name, client))
1521
+ with tempfile.TemporaryDirectory(prefix="agent-pack-drift-") as temporary:
1522
+ expected = _build_package(authority, name, Path(temporary), client)
1523
+ if destination.is_symlink() and not destination.exists():
1524
+ report["dangling"].append(f"package {name}: {destination}")
1525
+ elif not destination.is_dir():
1526
+ report["missing"].append(f"package {name}")
1527
+ report["missing"] += [f"skill {skill}" for skill in _skill_fingerprints(expected)]
1528
+ report["missing"] += [f"mcp {server}" for server in _package_mcps(expected)]
1529
+ elif _tree_fingerprint(expected) != _tree_fingerprint(destination):
1530
+ want, have = _skill_fingerprints(expected), _skill_fingerprints(destination)
1531
+ report["missing"] += [f"skill {skill}" for skill in sorted(set(want) - set(have))]
1532
+ report["stale"] += [f"skill {skill}" for skill in sorted(set(have) - set(want))]
1533
+ report["different"] += [f"skill {skill}" for skill in sorted(set(want) & set(have))
1534
+ if want[skill] != have[skill]]
1535
+ want_mcps, have_mcps = _package_mcps(expected), _package_mcps(destination)
1536
+ report["missing"] += [f"mcp {server}" for server in sorted(set(want_mcps) - set(have_mcps))]
1537
+ report["stale"] += [f"mcp {server}" for server in sorted(set(have_mcps) - set(want_mcps))]
1538
+ report["different"] += [f"mcp {server}" for server in sorted(set(want_mcps) & set(have_mcps))
1539
+ if want_mcps[server] != have_mcps[server]]
1540
+ if not (report["missing"] or report["stale"] or report["different"]):
1541
+ report["different"].append(f"package {name}: other files differ")
1542
+ native = _codex_mcps(home) if client == "codex" else _json_client_mcps(home / ".cursor" / "mcp.json")
1543
+ report["native"] = sorted(native)
1544
+ report["in_sync"] = not (report["missing"] or report["different"] or report["stale"] or report["dangling"])
1545
+ return report
1546
+
1547
+
1548
+ def doctor(project: Path, clients: Iterable[str] = (), home: Path | None = None) -> dict[str, Any]:
1549
+ """Validate the project; with ``clients``, also report each client's drift from it."""
1429
1550
  layout = load_project_layout(project)
1430
1551
  inventory = inventory_project(layout.root)
1431
1552
  issues = list(inventory.issues)
@@ -1465,7 +1586,17 @@ def doctor(project: Path) -> dict[str, Any]:
1465
1586
  issues.extend(f"credential env file missing key: {name}" for name in sorted(missing))
1466
1587
  except AgentPackError as exc:
1467
1588
  issues.append(str(exc))
1468
- return {
1589
+ reports: dict[str, Any] = {}
1590
+ for client in dict.fromkeys(clients):
1591
+ report = client_drift(layout.root, client, home)
1592
+ reports[client] = report
1593
+ for kind in ("missing", "different", "stale", "dangling"):
1594
+ issues.extend(f"{client}: {kind} {item}" for item in report[kind])
1595
+ if report["coupled"]:
1596
+ issues.append(f"{client}: install record points into another package directory: {report['coupled']}")
1597
+ warnings.extend(f"{client}: kept local override {item['name']} ({', '.join(item['keys'])})"
1598
+ for item in report["overrides"])
1599
+ result: dict[str, Any] = {
1469
1600
  "ok": not issues,
1470
1601
  "source": inventory.source,
1471
1602
  "counts": {
@@ -1476,6 +1607,9 @@ def doctor(project: Path) -> dict[str, Any]:
1476
1607
  "issues": sorted(set(issues)),
1477
1608
  "warnings": sorted(set(warnings)),
1478
1609
  }
1610
+ if reports:
1611
+ result["clients"] = reports
1612
+ return result
1479
1613
 
1480
1614
 
1481
1615
  def _print(value: dict[str, Any], output: str) -> None:
@@ -1508,6 +1642,9 @@ def build_parser() -> argparse.ArgumentParser:
1508
1642
 
1509
1643
  doctor_parser = sub.add_parser("doctor")
1510
1644
  doctor_parser.add_argument("--project", type=Path, default=Path.cwd())
1645
+ doctor_parser.add_argument("--client", action="append", default=[], choices=("codex", "cursor", "claude"),
1646
+ help="also compare this client's installed capabilities with the project (repeatable)")
1647
+ doctor_parser.add_argument("--home", type=Path)
1511
1648
 
1512
1649
  transfer_parser = sub.add_parser("transfer")
1513
1650
  transfer_parser.add_argument("--from", dest="source", required=True)
@@ -1519,9 +1656,14 @@ def build_parser() -> argparse.ArgumentParser:
1519
1656
 
1520
1657
  sync_parser = sub.add_parser("sync")
1521
1658
  sync_parser.add_argument("--project", type=Path, default=Path.cwd())
1522
- sync_parser.add_argument("--client", choices=("codex", "cursor"), required=True)
1659
+ sync_parser.add_argument("--client", choices=("codex", "cursor", "claude"), required=True)
1523
1660
  sync_parser.add_argument("--home", type=Path)
1524
1661
  sync_parser.add_argument("--apply", action="store_true")
1662
+ sync_parser.add_argument("--replace", action="store_true",
1663
+ help="claude: also replace Skills / MCP entries this package did not install")
1664
+ sync_parser.add_argument("--reset-overrides", action="store_true",
1665
+ help="claude: drop local credential values (headers / env) instead of keeping them")
1666
+ sync_parser.add_argument("--recovery-root", type=Path)
1525
1667
 
1526
1668
  bootstrap_parser = sub.add_parser("bootstrap")
1527
1669
  bootstrap_parser.add_argument("--plugin", type=Path, required=True)
@@ -1530,10 +1672,18 @@ def build_parser() -> argparse.ArgumentParser:
1530
1672
  bootstrap_parser.add_argument("--apply", action="store_true")
1531
1673
  bootstrap_parser.add_argument("--replace", action="store_true")
1532
1674
  bootstrap_parser.add_argument("--recovery-root", type=Path)
1675
+ bootstrap_parser.add_argument("--skill", action="append", default=[], help="claude: install only this Skill")
1676
+ bootstrap_parser.add_argument("--mcp", action="append", default=[], help="claude: install only this MCP server")
1677
+ bootstrap_parser.add_argument("--reset-overrides", action="store_true",
1678
+ help="claude: drop local credential values (headers / env) instead of keeping them")
1533
1679
 
1534
1680
  reconcile_parser = sub.add_parser("reconcile")
1535
1681
  reconcile_parser.add_argument("--project", type=Path, default=Path.cwd())
1536
- reconcile_parser.add_argument("--client", choices=("codex", "cursor"), required=True)
1682
+ reconcile_parser.add_argument("--client", choices=("codex", "cursor", "claude"), required=True)
1683
+ reconcile_parser.add_argument("--replace", action="store_true",
1684
+ help="claude: also replace Skills / MCP entries this package did not install")
1685
+ reconcile_parser.add_argument("--reset-overrides", action="store_true",
1686
+ help="claude: drop local credential values (headers / env) instead of keeping them")
1537
1687
  reconcile_parser.add_argument("--home", type=Path)
1538
1688
  reconcile_parser.add_argument("--retire-skill", action="append", default=[])
1539
1689
  reconcile_parser.add_argument("--retire-plugin", action="append", default=[])
@@ -1554,7 +1704,7 @@ def main(argv: list[str] | None = None) -> int:
1554
1704
  result = inventory_source(args.source, args.home).to_dict()
1555
1705
  result["ok"] = not result["issues"]
1556
1706
  elif args.command == "doctor":
1557
- result = doctor(args.project)
1707
+ result = doctor(args.project, args.client, args.home)
1558
1708
  elif args.command == "transfer":
1559
1709
  if not args.skill and not args.mcp:
1560
1710
  raise AgentPackError("select at least one --skill or --mcp")
@@ -1567,7 +1717,8 @@ def main(argv: list[str] | None = None) -> int:
1567
1717
  )
1568
1718
  elif args.command == "sync":
1569
1719
  result = sync_project(
1570
- args.project, args.client, home=args.home, apply=args.apply
1720
+ args.project, args.client, home=args.home, apply=args.apply, replace=args.replace,
1721
+ reset_overrides=args.reset_overrides, recovery_root=args.recovery_root,
1571
1722
  )
1572
1723
  elif args.command == "bootstrap":
1573
1724
  result = install_plugin(
@@ -1577,6 +1728,9 @@ def main(argv: list[str] | None = None) -> int:
1577
1728
  apply=args.apply,
1578
1729
  replace=args.replace,
1579
1730
  recovery_root=args.recovery_root,
1731
+ skills=args.skill,
1732
+ mcps=args.mcp,
1733
+ reset_overrides=args.reset_overrides,
1580
1734
  )
1581
1735
  else:
1582
1736
  result = reconcile_project(
@@ -1588,6 +1742,8 @@ def main(argv: list[str] | None = None) -> int:
1588
1742
  recovery_root=args.recovery_root,
1589
1743
  apply=args.apply,
1590
1744
  retire_only=args.retire_only,
1745
+ replace=args.replace,
1746
+ reset_overrides=args.reset_overrides,
1591
1747
  )
1592
1748
  _print(result, args.output)
1593
1749
  return 0 if result.get("ok", True) else 1
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env python3
2
2
  """agenthelm:在 Claude Code、Codex、Cursor 间安装、检查与更新 agenthelm 插件。
3
3
 
4
- ``check`` 只读:比较各客户端已安装的插件版本、项目里由模板渲染的文件,
4
+ ``project`` 打开不需要 AI Agent 的项目自助入口(由 project-orchestrator 实现);``runtime``
5
+ 管理用户级运行时。``check`` 只读:比较各客户端已安装的插件版本、项目里由模板渲染的文件,
5
6
  以及 Opinion 来源与云端 ``latest.json``。``install``、``update``、``migrate``
6
7
  默认只预览,``--apply`` 才执行。通过 ``npx @sakanano-moe/agenthelm <命令>`` 或
7
8
  ``python3 <agent-pack>/runtime/agenthelm.py <命令>`` 运行。``--hook`` 供会话开始时静默调用:
@@ -1003,6 +1004,10 @@ def parser() -> argparse.ArgumentParser:
1003
1004
  doctor_parser = sub.add_parser("doctor", help="报告重复安装、版本不一致、旧方式残留与检查开关")
1004
1005
  common(doctor_parser)
1005
1006
 
1007
+ sub.add_parser("runtime", add_help=False,
1008
+ help="用户级运行时:install / update / use / status(日常只需要 Python,不经过 npx)")
1009
+ sub.add_parser("project", add_help=False,
1010
+ help="项目自助入口:找资料、看进展、项目指南、检查(不需要 AI Agent;详见 project --help)")
1006
1011
  locate_parser = sub.add_parser("locate", help="输出插件的已安装位置(最新优先),供下游脚本查找")
1007
1012
  locate_parser.add_argument("plugin")
1008
1013
  locate_parser.add_argument("--version", help="只要这个版本")
@@ -1034,7 +1039,43 @@ def _print_steps(results: list[dict], apply: bool) -> None:
1034
1039
  print("以上为预览;确认后追加 --apply 执行。")
1035
1040
 
1036
1041
 
1042
+ def run_project(argv: list[str]) -> int:
1043
+ """``agenthelm project``: hand over to project-orchestrator's project entry, which owns it."""
1044
+ installed = installed_plugins(current_env(), {"project-orchestrator"})
1045
+ copies = [PLUGIN_ROOT.parent / "project-orchestrator", *(Path(item["path"]) for item in installed)]
1046
+ # Older copies predate the project entry; only those that ship it count.
1047
+ usable = [path for path in copies if (path / "runtime" / "project_cli.py").is_file()]
1048
+ if not usable:
1049
+ stale = any((path / "plugin.json").is_file() for path in copies)
1050
+ found = "(已安装的 project-orchestrator 版本过旧)" if stale else ""
1051
+ print(f"agenthelm project 需要 project-orchestrator 的项目入口,但没有找到{found}。\n"
1052
+ f" · 更新插件:{CLI} update,或从发布包安装运行时(见 INSTALL.md)", file=sys.stderr)
1053
+ return 3
1054
+ # Prefer the copy shipped next to this agent-pack (same release), then the newest installed one.
1055
+ sibling = PLUGIN_ROOT.parent / "project-orchestrator"
1056
+ newest = sibling if sibling in usable else \
1057
+ max(usable, key=lambda path: version_key(_manifest_version(path) or "") or (0,))
1058
+ sys.path.insert(0, str(newest / "runtime"))
1059
+ import project_cli # noqa: PLC0415 located at run time
1060
+
1061
+ project_cli.HOST = sys.modules[__name__]
1062
+ return project_cli.main(argv)
1063
+
1064
+
1065
+ def run_runtime(argv: list[str]) -> int:
1066
+ """``agenthelm runtime``: the user-level runtime that project launchers use without any AI client."""
1067
+ sys.path.insert(0, str(RUNTIME_DIR))
1068
+ import agenthelm_runtime # noqa: PLC0415 - shipped next to this file
1069
+
1070
+ return agenthelm_runtime.main(argv, sys.modules[__name__])
1071
+
1072
+
1037
1073
  def main(argv: list[str] | None = None) -> int:
1074
+ argv = list(sys.argv[1:] if argv is None else argv)
1075
+ if argv[:1] == ["project"]:
1076
+ return run_project(argv[1:])
1077
+ if argv[:1] == ["runtime"]:
1078
+ return run_runtime(argv[1:])
1038
1079
  args = parser().parse_args(argv)
1039
1080
  env = current_env(args.home)
1040
1081
  home = env.home