canfar-lab 0.7.0__py3-none-any.whl

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 (269) hide show
  1. canfar_lab/__init__.py +20 -0
  2. canfar_lab/__main__.py +4 -0
  3. canfar_lab/agent/__init__.py +5 -0
  4. canfar_lab/agent/addons.py +230 -0
  5. canfar_lab/agent/agent_config.py +593 -0
  6. canfar_lab/agent/agent_targets.py +285 -0
  7. canfar_lab/agent/bin_root.py +383 -0
  8. canfar_lab/agent/bundle_path.py +48 -0
  9. canfar_lab/agent/clean_agent.py +97 -0
  10. canfar_lab/agent/fix.py +236 -0
  11. canfar_lab/agent/install.py +440 -0
  12. canfar_lab/agent/install_bins.py +407 -0
  13. canfar_lab/agent/install_fetch.py +577 -0
  14. canfar_lab/agent/interact.py +130 -0
  15. canfar_lab/agent/inventory.py +194 -0
  16. canfar_lab/agent/keys.py +338 -0
  17. canfar_lab/agent/opencode_config.py +89 -0
  18. canfar_lab/agent/openscience.py +275 -0
  19. canfar_lab/agent/plugins.py +620 -0
  20. canfar_lab/agent/reconcile.py +236 -0
  21. canfar_lab/agent/registry.py +72 -0
  22. canfar_lab/agent/registry_lifecycle.py +718 -0
  23. canfar_lab/agent/registry_probe.py +376 -0
  24. canfar_lab/agent/registry_store.py +78 -0
  25. canfar_lab/agent/review_bench.py +535 -0
  26. canfar_lab/agent/setup.py +921 -0
  27. canfar_lab/agent/setup_state.py +264 -0
  28. canfar_lab/agent/support.py +251 -0
  29. canfar_lab/agent/upstream.py +87 -0
  30. canfar_lab/agent/wipe.py +119 -0
  31. canfar_lab/cli/__init__.py +0 -0
  32. canfar_lab/cli/agent_cmd.py +378 -0
  33. canfar_lab/cli/agent_format.py +251 -0
  34. canfar_lab/cli/agent_install.py +603 -0
  35. canfar_lab/cli/agent_keys.py +105 -0
  36. canfar_lab/cli/agent_plugins.py +210 -0
  37. canfar_lab/cli/agent_setup.py +408 -0
  38. canfar_lab/cli/banner.py +76 -0
  39. canfar_lab/cli/clean.py +140 -0
  40. canfar_lab/cli/config.py +63 -0
  41. canfar_lab/cli/context.py +33 -0
  42. canfar_lab/cli/desk.py +218 -0
  43. canfar_lab/cli/dispatch.py +27 -0
  44. canfar_lab/cli/env.py +69 -0
  45. canfar_lab/cli/help_cmd.py +242 -0
  46. canfar_lab/cli/init_clone_env.py +506 -0
  47. canfar_lab/cli/kernel.py +109 -0
  48. canfar_lab/cli/main.py +113 -0
  49. canfar_lab/cli/open_cmd.py +134 -0
  50. canfar_lab/cli/panel.py +199 -0
  51. canfar_lab/cli/status.py +176 -0
  52. canfar_lab/cli/studio.py +522 -0
  53. canfar_lab/cli/sync.py +509 -0
  54. canfar_lab/config/__init__.py +3 -0
  55. canfar_lab/config/settings.py +118 -0
  56. canfar_lab/core/arc_permissions.py +109 -0
  57. canfar_lab/core/cadc_auth.py +21 -0
  58. canfar_lab/core/disk_usage.py +158 -0
  59. canfar_lab/core/git.py +156 -0
  60. canfar_lab/core/home_clean.py +144 -0
  61. canfar_lab/core/home_layout.py +410 -0
  62. canfar_lab/core/kernel.py +171 -0
  63. canfar_lab/core/pathlock.py +177 -0
  64. canfar_lab/core/paths.py +70 -0
  65. canfar_lab/core/project.py +361 -0
  66. canfar_lab/core/session_common.py +122 -0
  67. canfar_lab/core/session_resources.py +166 -0
  68. canfar_lab/core/storage.py +303 -0
  69. canfar_lab/core/vospace_status.py +270 -0
  70. canfar_lab/data/__init__.py +0 -0
  71. canfar_lab/data/agent/VERSION +1 -0
  72. canfar_lab/data/agent/agents/agy.yaml +27 -0
  73. canfar_lab/data/agent/agents/augment.yaml +28 -0
  74. canfar_lab/data/agent/agents/claude.yaml +26 -0
  75. canfar_lab/data/agent/agents/cline.yaml +19 -0
  76. canfar_lab/data/agent/agents/codewhale.yaml +29 -0
  77. canfar_lab/data/agent/agents/codex.yaml +28 -0
  78. canfar_lab/data/agent/agents/copilot.yaml +26 -0
  79. canfar_lab/data/agent/agents/cursor.yaml +23 -0
  80. canfar_lab/data/agent/agents/droid.yaml +30 -0
  81. canfar_lab/data/agent/agents/dsh.yaml +23 -0
  82. canfar_lab/data/agent/agents/freebuff.yaml +28 -0
  83. canfar_lab/data/agent/agents/goose.yaml +24 -0
  84. canfar_lab/data/agent/agents/hermes.yaml +27 -0
  85. canfar_lab/data/agent/agents/junie.yaml +29 -0
  86. canfar_lab/data/agent/agents/kilo.yaml +25 -0
  87. canfar_lab/data/agent/agents/muse.yaml +27 -0
  88. canfar_lab/data/agent/agents/omp.yaml +29 -0
  89. canfar_lab/data/agent/agents/openclaw.yaml +22 -0
  90. canfar_lab/data/agent/agents/opencode.yaml +25 -0
  91. canfar_lab/data/agent/agents/openscience.yaml +33 -0
  92. canfar_lab/data/agent/agents/pi.yaml +31 -0
  93. canfar_lab/data/agent/agents/qoder.yaml +25 -0
  94. canfar_lab/data/agent/agents/swival.yaml +23 -0
  95. canfar_lab/data/agent/agents/zcode.yaml +25 -0
  96. canfar_lab/data/agent/claude/mcp.json +33 -0
  97. canfar_lab/data/agent/claude/settings.json +17 -0
  98. canfar_lab/data/agent/cli/agent-tools.sh +17 -0
  99. canfar_lab/data/agent/cline/cline-notes.md +7 -0
  100. canfar_lab/data/agent/codewhale/config.toml +12 -0
  101. canfar_lab/data/agent/codex/config.toml +30 -0
  102. canfar_lab/data/agent/copilot/mcp-config.json +42 -0
  103. canfar_lab/data/agent/cursor/mcp.json +33 -0
  104. canfar_lab/data/agent/cursor/rules/astroai-lab-paths.mdc +18 -0
  105. canfar_lab/data/agent/cursor/rules/cpp.mdc +13 -0
  106. canfar_lab/data/agent/cursor/rules/env-hygiene.mdc +44 -0
  107. canfar_lab/data/agent/cursor/rules/harness.mdc +17 -0
  108. canfar_lab/data/agent/cursor/rules/latex.mdc +15 -0
  109. canfar_lab/data/agent/cursor/rules/ponytail.mdc +36 -0
  110. canfar_lab/data/agent/cursor/rules/python-pixi.mdc +13 -0
  111. canfar_lab/data/agent/cursor/rules/python.mdc +15 -0
  112. canfar_lab/data/agent/cursor/rules/shell.mdc +14 -0
  113. canfar_lab/data/agent/cursor/rules/token-efficient.mdc +15 -0
  114. canfar_lab/data/agent/cursor/rules/writing.mdc +15 -0
  115. canfar_lab/data/agent/freebuff/astroai-notes.md +6 -0
  116. canfar_lab/data/agent/goose/extensions.yaml +43 -0
  117. canfar_lab/data/agent/goose/goosehints +6 -0
  118. canfar_lab/data/agent/kilo/kilo.jsonc +12 -0
  119. canfar_lab/data/agent/manifest.json +86 -0
  120. canfar_lab/data/agent/marimo/marimo.toml +17 -0
  121. canfar_lab/data/agent/muse/settings.json +3 -0
  122. canfar_lab/data/agent/omp/astroai-notes.md +23 -0
  123. canfar_lab/data/agent/opencode/opencode.json +42 -0
  124. canfar_lab/data/agent/openscience/agent/astronomy.md +25 -0
  125. canfar_lab/data/agent/openscience/canfar.md +81 -0
  126. canfar_lab/data/agent/openscience/command/astro-check.md +15 -0
  127. canfar_lab/data/agent/openscience/command/find-data.md +10 -0
  128. canfar_lab/data/agent/openscience/command/save-results.md +10 -0
  129. canfar_lab/data/agent/openscience/command/scale-out.md +12 -0
  130. canfar_lab/data/agent/openscience/skills/astro-literature/SKILL.md +55 -0
  131. canfar_lab/data/agent/openscience/skills/cadc-archive-search/SKILL.md +52 -0
  132. canfar_lab/data/agent/openscience/skills/canfar-data-management/SKILL.md +63 -0
  133. canfar_lab/data/agent/openscience/skills/canfar-ray-scaling/SKILL.md +76 -0
  134. canfar_lab/data/agent/openscience/skills/catalog-crossmatch/SKILL.md +79 -0
  135. canfar_lab/data/agent/openscience/skills/ccd-image-reduction/SKILL.md +79 -0
  136. canfar_lab/data/agent/openscience/skills/fits-wcs-cutouts/SKILL.md +74 -0
  137. canfar_lab/data/agent/openscience/skills/gaia-astrometry/SKILL.md +95 -0
  138. canfar_lab/data/agent/openscience/skills/lightcurve-periodogram/SKILL.md +53 -0
  139. canfar_lab/data/agent/openscience/skills/obscore-tap/SKILL.md +55 -0
  140. canfar_lab/data/agent/openscience/skills/observation-planning/SKILL.md +69 -0
  141. canfar_lab/data/agent/openscience/skills/photometric-calibration/SKILL.md +75 -0
  142. canfar_lab/data/agent/openscience/skills/photometry-aperture/SKILL.md +65 -0
  143. canfar_lab/data/agent/openscience/skills/spectra-specutils/SKILL.md +58 -0
  144. canfar_lab/data/agent/openscience/skills/spectral-cubes/SKILL.md +68 -0
  145. canfar_lab/data/agent/openscience/skills/vo-cutouts-datalink/SKILL.md +76 -0
  146. canfar_lab/data/agent/pi/settings.json +4 -0
  147. canfar_lab/data/agent/plugins/ast-grep-cli.yaml +15 -0
  148. canfar_lab/data/agent/plugins/git-mcp.yaml +22 -0
  149. canfar_lab/data/agent/plugins/mcp-context7.yaml +12 -0
  150. canfar_lab/data/agent/plugins/mcp-memory.yaml +12 -0
  151. canfar_lab/data/agent/plugins/ponytail-rule.yaml +14 -0
  152. canfar_lab/data/agent/plugins/ray-manager-mcp.yaml +20 -0
  153. canfar_lab/data/agent/plugins/skore-cli.yaml +16 -0
  154. canfar_lab/data/agent/plugins/sqlite.yaml +26 -0
  155. canfar_lab/data/agent/plugins/time.yaml +23 -0
  156. canfar_lab/data/agent/plugins/token-efficient.yaml +13 -0
  157. canfar_lab/data/agent/project/.cursor/mcp.json +14 -0
  158. canfar_lab/data/agent/project/.cursor/rules/project-defaults.mdc +11 -0
  159. canfar_lab/data/agent/project/AGENTS.md +62 -0
  160. canfar_lab/data/agent/qoder/astroai-notes.md +6 -0
  161. canfar_lab/data/agent/rules/env-hygiene.md +39 -0
  162. canfar_lab/data/agent/rules/harness.md +10 -0
  163. canfar_lab/data/agent/rules/latex.md +7 -0
  164. canfar_lab/data/agent/rules/ponytail.md +24 -0
  165. canfar_lab/data/agent/rules/python-pixi.md +6 -0
  166. canfar_lab/data/agent/rules/shell.md +6 -0
  167. canfar_lab/data/agent/rules/token-efficient.md +3 -0
  168. canfar_lab/data/agent/rules/writing.md +7 -0
  169. canfar_lab/data/agent/support.yaml +87 -0
  170. canfar_lab/data/agent/swival/astroai-notes.md +6 -0
  171. canfar_lab/data/agent/zcode/astroai-notes.md +6 -0
  172. canfar_lab/data/brand/README.md +11 -0
  173. canfar_lab/data/brand/astroai-logo.png +0 -0
  174. canfar_lab/data/desk/agents.html +159 -0
  175. canfar_lab/data/desk/dock.html +230 -0
  176. canfar_lab/data/notebooks/ray_train.ipynb +43 -0
  177. canfar_lab/data/notebooks/starter.ipynb +460 -0
  178. canfar_lab/data/notebooks/starter.py +423 -0
  179. canfar_lab/data/review-bench/HOWTO.md +294 -0
  180. canfar_lab/data/review-bench/README.md +225 -0
  181. canfar_lab/data/review-bench/VERSION +1 -0
  182. canfar_lab/data/review-bench/bin/dsh-web.sh +40 -0
  183. canfar_lab/data/review-bench/bin/panel.sh +108 -0
  184. canfar_lab/data/review-bench/cordis.bench.patch.yml +12 -0
  185. canfar_lab/data/review-bench/presets/review-bench/agent.cordis.yml +682 -0
  186. canfar_lab/data/review-bench/presets/review-bench/preset.yml +8 -0
  187. canfar_lab/data/review-bench/project-template/.dsh/README.md +95 -0
  188. canfar_lab/data/review-bench/project-template/.dsh/cordis.patch.yml +21 -0
  189. canfar_lab/data/review-bench/skills/astroai-team-charter/SKILL.md +167 -0
  190. canfar_lab/data/review-bench/skills/canfar-session/SKILL.md +44 -0
  191. canfar_lab/data/review-bench/skills/review-panel/SKILL.md +258 -0
  192. canfar_lab/data/review-bench/skills/review-panel/references/brief-template.md +42 -0
  193. canfar_lab/data/review-bench/skills/review-panel/references/panel-round1.js +130 -0
  194. canfar_lab/data/review-bench/skills/review-panel/references/report-template.md +49 -0
  195. canfar_lab/data/review-bench/skills/review-panel/references/rubric.md +128 -0
  196. canfar_lab/data/review-bench/validate.mjs +314 -0
  197. canfar_lab/data/shell/hooks.sh +68 -0
  198. canfar_lab/data/shell/profile.sh +105 -0
  199. canfar_lab/data/studio/plugins/dsh-astroai-brand/cordis.patch.yml +8 -0
  200. canfar_lab/data/studio/plugins/dsh-astroai-brand/lib/client.js +81 -0
  201. canfar_lab/data/studio/plugins/dsh-astroai-brand/lib/index.js +2 -0
  202. canfar_lab/data/studio/plugins/dsh-astroai-brand/package.json +30 -0
  203. canfar_lab/data/studio/plugins/dsh-astroai-settings/cordis.patch.yml +4 -0
  204. canfar_lab/data/studio/plugins/dsh-astroai-settings/lib/client.js +308 -0
  205. canfar_lab/data/studio/plugins/dsh-astroai-settings/lib/index.js +111 -0
  206. canfar_lab/data/studio/plugins/dsh-astroai-settings/package.json +29 -0
  207. canfar_lab/data/studio/plugins/dsh-opencode-session/README.md +135 -0
  208. canfar_lab/data/studio/plugins/dsh-opencode-session/cordis.patch.yml +29 -0
  209. canfar_lab/data/studio/plugins/dsh-opencode-session/lib/index.js +216 -0
  210. canfar_lab/data/studio/plugins/dsh-opencode-session/package.json +53 -0
  211. canfar_lab/desk/__init__.py +15 -0
  212. canfar_lab/desk/actions.py +281 -0
  213. canfar_lab/desk/agents_page.py +84 -0
  214. canfar_lab/desk/apps.py +384 -0
  215. canfar_lab/desk/proxy.py +440 -0
  216. canfar_lab/desk/serve.py +79 -0
  217. canfar_lab/desk/state.py +155 -0
  218. canfar_lab/dsh_release.py +158 -0
  219. canfar_lab/errors.py +15 -0
  220. canfar_lab/models/__init__.py +3 -0
  221. canfar_lab/models/manifest.py +19 -0
  222. canfar_lab/panel.py +274 -0
  223. canfar_lab/session_urls.py +66 -0
  224. canfar_lab/shell/__init__.py +1 -0
  225. canfar_lab/shell/session_env.py +416 -0
  226. canfar_lab/studio.py +557 -0
  227. canfar_lab/studio_bundles.py +218 -0
  228. canfar_lab/studio_doctor.py +578 -0
  229. canfar_lab/studio_layer.py +968 -0
  230. canfar_lab/studio_paths.py +225 -0
  231. canfar_lab/studio_profile.py +177 -0
  232. canfar_lab/sync/__init__.py +15 -0
  233. canfar_lab/sync/arc_data.py +155 -0
  234. canfar_lab/sync/bus.py +236 -0
  235. canfar_lab/sync/catalog.py +221 -0
  236. canfar_lab/sync/clock.py +30 -0
  237. canfar_lab/sync/engine.py +512 -0
  238. canfar_lab/sync/watch.py +167 -0
  239. canfar_lab/ui.py +265 -0
  240. canfar_lab/utils/__init__.py +0 -0
  241. canfar_lab/utils/console.py +5 -0
  242. canfar_lab/utils/json_utils.py +141 -0
  243. canfar_lab/utils/subprocess.py +66 -0
  244. canfar_lab/utils/timing.py +61 -0
  245. canfar_lab/utils/toml_compat.py +12 -0
  246. canfar_lab/version.py +57 -0
  247. canfar_lab-0.7.0.dist-info/METADATA +156 -0
  248. canfar_lab-0.7.0.dist-info/RECORD +269 -0
  249. canfar_lab-0.7.0.dist-info/WHEEL +4 -0
  250. canfar_lab-0.7.0.dist-info/entry_points.txt +5 -0
  251. canfar_lab-0.7.0.dist-info/licenses/LICENSE +21 -0
  252. canfar_workload/__init__.py +30 -0
  253. canfar_workload/astro_data.py +561 -0
  254. canfar_workload/autoscaler.py +748 -0
  255. canfar_workload/canfar_ops.py +324 -0
  256. canfar_workload/cli.py +1095 -0
  257. canfar_workload/cluster.py +600 -0
  258. canfar_workload/dashboard.py +317 -0
  259. canfar_workload/executor.py +238 -0
  260. canfar_workload/manager_client.py +179 -0
  261. canfar_workload/mcp.py +626 -0
  262. canfar_workload/models.py +301 -0
  263. canfar_workload/preflight.py +196 -0
  264. canfar_workload/ray_cluster.py +119 -0
  265. canfar_workload/reconcile.py +211 -0
  266. canfar_workload/settings.py +96 -0
  267. canfar_workload/state_store.py +180 -0
  268. canfar_workload/worker_logs.py +45 -0
  269. canfar_workload/workers.py +231 -0
canfar_lab/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ """canfar-lab — in-session workbench for AstroAI sessions on CANFAR."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from canfar_lab.version import __version__, display_version, version_info
8
+
9
+
10
+ def config_dir() -> Path:
11
+ """Workbench config directory (~/.astroai/lab)."""
12
+ return Path.home() / ".astroai" / "lab"
13
+
14
+
15
+ def saves_dir() -> Path:
16
+ """Default directory for saved lockfile environments."""
17
+ return config_dir() / "saves"
18
+
19
+
20
+ __all__ = ["__version__", "config_dir", "display_version", "saves_dir", "version_info"]
canfar_lab/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from canfar_lab.cli.main import main_entry
2
+
3
+ if __name__ == "__main__":
4
+ main_entry()
@@ -0,0 +1,5 @@
1
+ """AI agent setup and tool installation for CANFAR sessions."""
2
+
3
+ from canfar_lab.agent.bundle_path import bundle_root
4
+
5
+ __all__ = ["bundle_root"]
@@ -0,0 +1,230 @@
1
+ """Install transports for agent plugins (MCP, rules, tools).
2
+
3
+ Plugin YAML under ``data/agent/plugins/*.yaml`` is the catalog. This module
4
+ applies those entries (bundled / github-rule / mcp-snippet / cli-tool) via
5
+ ``_apply_addon``, also used by ``agent plugins install``.
6
+
7
+ Skills (SKILL.md) are not installed here — use ``npx skills``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import shutil
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from canfar_lab.agent.agent_targets import (
18
+ MCP_TARGETS,
19
+ mcp_server_present,
20
+ mcp_target,
21
+ merge_mcp_server,
22
+ translate_mcp_entry,
23
+ )
24
+ from canfar_lab.agent.install import install_tool, tool_on_path
25
+ from canfar_lab.agent.upstream import (
26
+ _refresh_upstream_repo,
27
+ _upstream_cache_root,
28
+ )
29
+ from canfar_lab.errors import LabError
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class AddonResult:
34
+ id: str
35
+ status: str
36
+ detail: str = ""
37
+
38
+
39
+ def plugin_as_addon(plugin: dict[str, Any]) -> dict[str, Any]:
40
+ """Map a plugin registry entry to the addon dict shape used by transports."""
41
+ install = dict(plugin.get("install") or {})
42
+ return {
43
+ "id": plugin["id"],
44
+ "kind": plugin["kind"],
45
+ "tags": plugin.get("tags", []),
46
+ "summary": plugin.get("summary", ""),
47
+ "homepage": plugin.get("homepage", ""),
48
+ "default": bool(plugin.get("default")),
49
+ "agents": list(plugin.get("agents", [])),
50
+ "install": install,
51
+ }
52
+
53
+
54
+ def addon_installed(item: dict[str, Any], home: Path, agent: str | None = None) -> bool:
55
+ install = item.get("install") or {}
56
+ itype = install.get("type")
57
+ addon_id = item["id"]
58
+
59
+ if itype == "bundled":
60
+ if addon_id == "token-efficient":
61
+ return (home / ".cursor" / "rules" / "token-efficient.mdc").is_file()
62
+ if addon_id.startswith("mcp-"):
63
+ server = addon_id.removeprefix("mcp-")
64
+ return _mcp_server_present(home, server, agent=agent)
65
+ return False
66
+
67
+ if itype == "github-rule":
68
+ rule = Path(install.get("path", "")).name
69
+ return (home / ".cursor" / "rules" / rule).is_file()
70
+
71
+ if itype == "mcp-snippet":
72
+ server = install.get("server", "")
73
+ agents = [agent] if agent else list(item.get("agents") or [])
74
+ hosts = [a for a in agents if mcp_target(a)]
75
+ if not server or not hosts:
76
+ return False
77
+ return all(mcp_server_present(home, a, server) for a in hosts)
78
+
79
+ if itype == "cli-tool":
80
+ return tool_on_path(install.get("tool", addon_id))
81
+
82
+ return False
83
+
84
+
85
+ def _mcp_server_present(home: Path, server: str, agent: str | None = None) -> bool:
86
+ """True when the given agent (or every MCP host, if omitted) has ``server``."""
87
+ if not server:
88
+ return False
89
+ agents = [agent] if agent else list(MCP_TARGETS)
90
+ return all(mcp_server_present(home, a, server) for a in agents)
91
+
92
+
93
+ def add_addon(
94
+ addon_id: str,
95
+ *,
96
+ home: Path | None = None,
97
+ force: bool = False,
98
+ dry_run: bool = False,
99
+ ) -> AddonResult:
100
+ from canfar_lab.agent.plugins import get_plugin
101
+
102
+ plugin = get_plugin(addon_id)
103
+ if plugin is None:
104
+ raise LabError(
105
+ f"Unknown addon: {addon_id}",
106
+ hint="canfar lab agent plugins list",
107
+ )
108
+ return _apply_addon(plugin_as_addon(plugin), home=home, force=force, dry_run=dry_run)
109
+
110
+
111
+ def _apply_addon(
112
+ item: dict[str, Any],
113
+ *,
114
+ home: Path | None = None,
115
+ force: bool = False,
116
+ dry_run: bool = False,
117
+ agent: str | None = None,
118
+ ) -> AddonResult:
119
+ """Apply one addon (plugin-as-addon dict) via its install transport.
120
+
121
+ ``agent`` scopes mcp-snippet writes to one support-matrix agent; omit it
122
+ to apply every agent listed on the plugin.
123
+ """
124
+ addon_id = item["id"]
125
+ home = home or Path.home()
126
+ install = item.get("install") or {}
127
+ itype = install.get("type")
128
+
129
+ if itype == "bundled":
130
+ return AddonResult(
131
+ addon_id,
132
+ "skipped",
133
+ install.get("note") or "bundled — run: canfar lab agent setup",
134
+ )
135
+
136
+ if not force and addon_installed(item, home, agent=agent):
137
+ return AddonResult(addon_id, "skipped", "already installed")
138
+
139
+ if itype == "github-rule":
140
+ return _install_github_rule(item, home=home, force=force, dry_run=dry_run)
141
+
142
+ if itype == "mcp-snippet":
143
+ return _install_mcp_snippet(item, home=home, force=force, dry_run=dry_run, agent=agent)
144
+
145
+ if itype == "cli-tool":
146
+ tool = install.get("tool")
147
+ if not tool:
148
+ raise LabError(f"Addon {addon_id} missing install.tool")
149
+ if dry_run:
150
+ return AddonResult(addon_id, "dry-run", f"would install CLI {tool}")
151
+ install_tool(tool, dry_run=False)
152
+ return AddonResult(addon_id, "installed", tool)
153
+
154
+ raise LabError(f"Addon {addon_id} has unsupported install type: {itype}")
155
+
156
+
157
+ def _install_github_rule(
158
+ item: dict[str, Any],
159
+ *,
160
+ home: Path,
161
+ force: bool,
162
+ dry_run: bool,
163
+ ) -> AddonResult:
164
+ install = item["install"]
165
+ repo = install["repo"]
166
+ path = install["path"]
167
+ if dry_run:
168
+ return AddonResult(item["id"], "dry-run", path)
169
+
170
+ cache_root = _upstream_cache_root(home, repo)
171
+ status, detail = _refresh_upstream_repo(cache_root, repo, path)
172
+ if status == "failed":
173
+ return AddonResult(item["id"], "failed", detail)
174
+
175
+ src = cache_root / path
176
+ if not src.is_file():
177
+ return AddonResult(item["id"], "failed", f"missing rule at {path}")
178
+ dst = home / ".cursor" / "rules" / src.name
179
+ if dst.is_file() and not force:
180
+ return AddonResult(item["id"], "skipped", "already installed")
181
+ dst.parent.mkdir(parents=True, exist_ok=True)
182
+ shutil.copy2(src, dst)
183
+ return AddonResult(item["id"], status, str(dst))
184
+
185
+
186
+ def _install_mcp_snippet(
187
+ item: dict[str, Any],
188
+ *,
189
+ home: Path,
190
+ force: bool,
191
+ dry_run: bool,
192
+ agent: str | None = None,
193
+ ) -> AddonResult:
194
+ install = item["install"]
195
+ server = install["server"]
196
+ cursor_cfg = install.get("cursor") or {}
197
+ opencode_cfg = install.get("opencode") or {}
198
+ agents = list(item.get("agents") or [])
199
+ if not agents:
200
+ raise LabError(
201
+ f"Addon {item['id']} mcp-snippet requires a non-empty agents support matrix",
202
+ hint="Declare agents: [cursor, ...] on the plugin YAML",
203
+ )
204
+ if agent is not None:
205
+ if agent not in agents:
206
+ return AddonResult(
207
+ item["id"], "skipped", f"{agent} not in support matrix ({', '.join(agents)})"
208
+ )
209
+ agents = [agent]
210
+
211
+ if dry_run:
212
+ return AddonResult(item["id"], "dry-run", f"mcp:{server} → {', '.join(agents)}")
213
+
214
+ written: list[str] = []
215
+ for ag in agents:
216
+ if mcp_target(ag) is None:
217
+ continue
218
+ entry = (
219
+ opencode_cfg
220
+ if ag == "opencode" and opencode_cfg
221
+ else translate_mcp_entry(ag, cursor_cfg)
222
+ )
223
+ if not entry:
224
+ continue
225
+ if merge_mcp_server(home, ag, server, entry, force=force):
226
+ written.append(ag)
227
+ if not written and not force:
228
+ return AddonResult(item["id"], "skipped", f"mcp:{server} already present")
229
+ detail = f"mcp:{server}" + (f" → {', '.join(written)}" if written else "")
230
+ return AddonResult(item["id"], "installed", detail)