@oh-my-pi/pi-coding-agent 17.2.9 → 17.2.11

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 (281) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/{CHANGELOG-8yy8h3bc.md → CHANGELOG-d9xenpn9.md} +83 -0
  3. package/dist/cli.js +10208 -12596
  4. package/dist/types/capability/mcp.d.ts +11 -0
  5. package/dist/types/capability/skill.d.ts +6 -0
  6. package/dist/types/cli/args.d.ts +1 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/model-registry.d.ts +4 -0
  14. package/dist/types/config/model-resolver.d.ts +13 -0
  15. package/dist/types/config/settings-schema.d.ts +1 -34
  16. package/dist/types/discovery/agent-plugin-format.d.ts +143 -0
  17. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  18. package/dist/types/discovery/contained-path.d.ts +34 -0
  19. package/dist/types/discovery/index.d.ts +1 -0
  20. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  21. package/dist/types/extensibility/custom-tools/types.d.ts +3 -3
  22. package/dist/types/extensibility/extensions/loader.d.ts +9 -2
  23. package/dist/types/extensibility/extensions/types.d.ts +45 -8
  24. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  25. package/dist/types/extensibility/shared-events.d.ts +3 -2
  26. package/dist/types/extensibility/skills.d.ts +5 -0
  27. package/dist/types/index.d.ts +2 -2
  28. package/dist/types/main.d.ts +1 -1
  29. package/dist/types/mcp/transports/header-policy.d.ts +45 -0
  30. package/dist/types/mcp/types.d.ts +15 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  32. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  33. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  34. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  35. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  36. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  37. package/dist/types/modes/components/agent-hub.d.ts +14 -5
  38. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  39. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  40. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  41. package/dist/types/modes/components/model-browser.d.ts +9 -1
  42. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  43. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  44. package/dist/types/modes/interactive-mode.d.ts +8 -1
  45. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  46. package/dist/types/registry/agent-registry.d.ts +47 -0
  47. package/dist/types/registry/persisted-agents.d.ts +3 -1
  48. package/dist/types/secrets/index.d.ts +23 -1
  49. package/dist/types/session/agent-session-events.d.ts +2 -2
  50. package/dist/types/session/agent-session.d.ts +4 -2
  51. package/dist/types/session/messages.d.ts +1 -0
  52. package/dist/types/session/prewalk.d.ts +3 -1
  53. package/dist/types/session/session-entries.d.ts +10 -0
  54. package/dist/types/session/session-loader.d.ts +11 -1
  55. package/dist/types/session/session-manager.d.ts +9 -1
  56. package/dist/types/session/session-tools.d.ts +18 -1
  57. package/dist/types/session/turn-recovery.d.ts +7 -2
  58. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  59. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  60. package/dist/types/task/executor.d.ts +6 -0
  61. package/dist/types/task/index.d.ts +4 -3
  62. package/dist/types/task/read-only-policy.d.ts +3 -0
  63. package/dist/types/task/structured-subagent.d.ts +2 -0
  64. package/dist/types/task/types.d.ts +4 -0
  65. package/dist/types/tools/bash.d.ts +3 -3
  66. package/dist/types/tools/browser/launch.d.ts +2 -1
  67. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  68. package/dist/types/tools/path-utils.d.ts +1 -0
  69. package/dist/types/tools/read.d.ts +1 -5
  70. package/dist/types/tools/run-scope.d.ts +27 -1
  71. package/dist/types/tools/shell-tokenize.d.ts +16 -0
  72. package/dist/types/tools/terminal-output.d.ts +1 -1
  73. package/dist/types/utils/turndown.d.ts +1 -1
  74. package/dist/types/vibe/runtime.d.ts +2 -3
  75. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  76. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  77. package/examples/custom-tools/README.md +1 -1
  78. package/examples/extensions/README.md +1 -1
  79. package/examples/extensions/api-demo.ts +5 -6
  80. package/examples/extensions/chalk-logger.ts +1 -1
  81. package/examples/extensions/hello.ts +2 -2
  82. package/examples/extensions/reload-runtime.ts +1 -1
  83. package/examples/extensions/tools.ts +2 -2
  84. package/examples/extensions/with-deps/index.ts +1 -1
  85. package/examples/sdk/06-extensions.ts +1 -1
  86. package/package.json +15 -31
  87. package/scripts/bundle-dist.ts +1 -11
  88. package/scripts/legacy-pi-virtual-module.ts +4 -1
  89. package/src/advisor/runtime.ts +54 -0
  90. package/src/capability/mcp.ts +11 -0
  91. package/src/capability/skill.ts +6 -0
  92. package/src/cli/agents-cli.ts +1 -1
  93. package/src/cli/args.ts +24 -2
  94. package/src/cli/auth-broker-cli.ts +1 -1
  95. package/src/cli/auth-gateway-cli.ts +1 -1
  96. package/src/cli/bench-cli.ts +1 -1
  97. package/src/cli/claude-trace-cli.ts +1 -1
  98. package/src/cli/command-help.ts +4 -0
  99. package/src/cli/config-cli.ts +1 -1
  100. package/src/cli/dry-balance-cli.ts +1 -1
  101. package/src/cli/file-processor.ts +1 -1
  102. package/src/cli/flag-tables.ts +4 -0
  103. package/src/cli/gallery-cli.ts +1 -1
  104. package/src/cli/grep-cli.ts +1 -1
  105. package/src/cli/grievances-cli.ts +1 -1
  106. package/src/cli/help-extra.ts +1 -1
  107. package/src/cli/models-cli.ts +1 -1
  108. package/src/cli/plugin-cli.ts +1 -1
  109. package/src/cli/read-cli.ts +1 -1
  110. package/src/cli/setup-cli.ts +1 -1
  111. package/src/cli/shell-cli.ts +1 -1
  112. package/src/cli/ssh-cli.ts +1 -1
  113. package/src/cli/stats-cli.ts +1 -1
  114. package/src/cli/tiny-models-cli.ts +1 -1
  115. package/src/cli/ttsr-cli.ts +1 -1
  116. package/src/cli/update-cli.ts +1 -1
  117. package/src/cli/usage-cli.ts +1 -1
  118. package/src/cli/web-search-cli.ts +1 -1
  119. package/src/cli/worktree-cli.ts +1 -1
  120. package/src/cli-commands.ts +5 -0
  121. package/src/commands/commit.ts +16 -3
  122. package/src/commands/say.ts +1 -1
  123. package/src/commands/share.ts +71 -0
  124. package/src/commands/token.ts +1 -1
  125. package/src/commit/agentic/agent.ts +1 -1
  126. package/src/commit/agentic/index.ts +39 -21
  127. package/src/commit/cli.ts +1 -1
  128. package/src/commit/execute.ts +56 -0
  129. package/src/commit/index.ts +2 -1
  130. package/src/commit/pipeline.ts +20 -7
  131. package/src/config/model-discovery.ts +1 -1
  132. package/src/config/model-registry.ts +21 -1
  133. package/src/config/model-resolver.ts +54 -7
  134. package/src/config/settings-schema.ts +2 -33
  135. package/src/config/settings.ts +46 -0
  136. package/src/discovery/agent-plugin-format.ts +551 -0
  137. package/src/discovery/agent-plugins.ts +341 -0
  138. package/src/discovery/agents.ts +4 -2
  139. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  140. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  141. package/src/discovery/claude-plugins.ts +21 -5
  142. package/src/discovery/contained-path.ts +78 -0
  143. package/src/discovery/helpers.ts +23 -9
  144. package/src/discovery/index.ts +1 -0
  145. package/src/discovery/omp-plugins.ts +20 -7
  146. package/src/eval/py/runner.py +38 -1
  147. package/src/exec/non-interactive-env.ts +1 -0
  148. package/src/extensibility/custom-commands/loader.ts +4 -4
  149. package/src/extensibility/custom-commands/types.ts +5 -5
  150. package/src/extensibility/custom-tools/loader.ts +4 -4
  151. package/src/extensibility/custom-tools/types.ts +3 -3
  152. package/src/extensibility/extensions/loader.ts +85 -20
  153. package/src/extensibility/extensions/runner.ts +6 -0
  154. package/src/extensibility/extensions/types.ts +51 -8
  155. package/src/extensibility/hooks/loader.ts +4 -5
  156. package/src/extensibility/hooks/types.ts +5 -5
  157. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  158. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  159. package/src/extensibility/shared-events.ts +3 -2
  160. package/src/extensibility/skills.ts +9 -0
  161. package/src/index.ts +2 -2
  162. package/src/internal-urls/memory-protocol.ts +5 -1
  163. package/src/internal-urls/skill-protocol.ts +14 -0
  164. package/src/internal-urls/vault-protocol.ts +2 -2
  165. package/src/launch/client.ts +11 -1
  166. package/src/launch/protocol.ts +7 -2
  167. package/src/launch/terminal-output.ts +1 -1
  168. package/src/main.ts +88 -23
  169. package/src/markit/converters/docx.ts +1 -1
  170. package/src/markit/converters/epub.ts +1 -1
  171. package/src/markit/converters/pptx.ts +1 -1
  172. package/src/markit/converters/xlsx.ts +1 -1
  173. package/src/mcp/config.ts +3 -0
  174. package/src/mcp/manager.ts +12 -9
  175. package/src/mcp/transports/header-policy.ts +95 -0
  176. package/src/mcp/transports/http.ts +35 -52
  177. package/src/mcp/transports/sse.ts +20 -27
  178. package/src/mcp/types.ts +15 -0
  179. package/src/modes/acp/acp-agent.ts +20 -9
  180. package/src/modes/acp/acp-client-bridge.ts +1 -1
  181. package/src/modes/acp/acp-event-mapper.ts +1 -1
  182. package/src/modes/acp/acp-mode.ts +1 -1
  183. package/src/modes/components/agent-dashboard.ts +2 -0
  184. package/src/modes/components/agent-hub-projection.ts +248 -0
  185. package/src/modes/components/agent-hub-renderer.ts +194 -0
  186. package/src/modes/components/agent-hub.ts +674 -207
  187. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  188. package/src/modes/components/assistant-message.ts +1 -1
  189. package/src/modes/components/chat-transcript-builder.ts +3 -0
  190. package/src/modes/components/custom-editor.ts +0 -9
  191. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  192. package/src/modes/components/footer.ts +1 -3
  193. package/src/modes/components/model-browser.ts +11 -3
  194. package/src/modes/components/model-hub.ts +4 -1
  195. package/src/modes/components/status-line/component.ts +1 -0
  196. package/src/modes/components/status-line/segments.ts +10 -7
  197. package/src/modes/components/status-line/types.ts +2 -0
  198. package/src/modes/components/tool-execution.ts +13 -16
  199. package/src/modes/components/tree-selector.ts +62 -3
  200. package/src/modes/controllers/command-controller.ts +4 -1
  201. package/src/modes/controllers/event-controller.ts +44 -20
  202. package/src/modes/controllers/extension-ui-controller.ts +2 -2
  203. package/src/modes/controllers/input-controller.ts +20 -2
  204. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  205. package/src/modes/controllers/selector-controller.ts +41 -49
  206. package/src/modes/controllers/streaming-reveal.ts +1 -1
  207. package/src/modes/interactive-mode.ts +13 -2
  208. package/src/modes/rpc/rpc-mode.ts +2 -2
  209. package/src/modes/runtime-init.ts +1 -1
  210. package/src/modes/theme/theme.ts +2 -2
  211. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  212. package/src/modes/utils/ui-helpers.ts +1 -0
  213. package/src/prompts/tools/computer.md +1 -1
  214. package/src/registry/agent-lifecycle.ts +14 -7
  215. package/src/registry/agent-registry.ts +65 -2
  216. package/src/registry/persisted-agents.ts +341 -16
  217. package/src/sdk.ts +6 -49
  218. package/src/secrets/index.ts +52 -1
  219. package/src/session/agent-session-events.ts +2 -2
  220. package/src/session/agent-session.ts +54 -16
  221. package/src/session/messages.ts +1 -0
  222. package/src/session/prewalk.ts +57 -17
  223. package/src/session/session-context.ts +3 -5
  224. package/src/session/session-entries.ts +10 -0
  225. package/src/session/session-handoff.ts +5 -1
  226. package/src/session/session-listing.ts +1 -1
  227. package/src/session/session-loader.ts +74 -8
  228. package/src/session/session-manager.ts +22 -2
  229. package/src/session/session-paths.ts +57 -9
  230. package/src/session/session-tools.ts +65 -4
  231. package/src/session/todo-tracker.ts +11 -1
  232. package/src/session/turn-recovery.ts +96 -74
  233. package/src/slash-commands/acp-builtins.ts +1 -1
  234. package/src/slash-commands/available-commands.ts +1 -1
  235. package/src/slash-commands/builtin-registry.ts +12 -8
  236. package/src/task/executor.ts +70 -20
  237. package/src/task/index.ts +30 -34
  238. package/src/task/isolation-runner.ts +24 -11
  239. package/src/task/persisted-revive.ts +12 -5
  240. package/src/task/read-only-policy.ts +27 -0
  241. package/src/task/structured-subagent.ts +14 -4
  242. package/src/task/types.ts +4 -0
  243. package/src/tools/auto-generated-guard.ts +1 -1
  244. package/src/tools/bash-interactive.ts +4 -4
  245. package/src/tools/bash-skill-urls.ts +15 -0
  246. package/src/tools/bash.ts +16 -17
  247. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  248. package/src/tools/browser/launch.ts +41 -12
  249. package/src/tools/browser/readable.ts +9 -9
  250. package/src/tools/browser/tab-supervisor.ts +12 -3
  251. package/src/tools/browser/tab-worker-entry.ts +1 -1
  252. package/src/tools/browser/tab-worker.ts +101 -12
  253. package/src/tools/debug.ts +1 -1
  254. package/src/tools/fetch.ts +1 -1
  255. package/src/tools/jtd-to-json-schema.ts +123 -21
  256. package/src/tools/path-utils.ts +9 -2
  257. package/src/tools/read.ts +21 -9
  258. package/src/tools/run-scope.ts +290 -4
  259. package/src/tools/shell-tokenize.ts +98 -0
  260. package/src/tools/terminal-output.ts +1 -1
  261. package/src/tools/todo.ts +1 -1
  262. package/src/utils/clipboard.ts +10 -2
  263. package/src/utils/external-editor.ts +4 -2
  264. package/src/utils/jj.ts +1 -1
  265. package/src/utils/turndown.ts +1 -2
  266. package/src/vibe/runtime.ts +5 -5
  267. package/src/web/scrapers/arxiv.ts +1 -1
  268. package/src/web/scrapers/go-pkg.ts +1 -1
  269. package/src/web/scrapers/iacr.ts +1 -1
  270. package/src/web/scrapers/readthedocs.ts +2 -1
  271. package/src/web/scrapers/twitter.ts +2 -2
  272. package/src/web/scrapers/types.ts +1 -1
  273. package/src/web/scrapers/wikipedia.ts +1 -1
  274. package/src/web/search/providers/browser-headers.ts +12 -39
  275. package/src/web/search/providers/codex.ts +3 -26
  276. package/src/web/search/providers/ecosia.ts +1 -1
  277. package/src/web/search/providers/exa.ts +1 -1
  278. package/src/web/search/providers/google.ts +1 -1
  279. package/src/web/search/providers/mojeek.ts +1 -1
  280. package/src/web/search/providers/startpage.ts +1 -1
  281. package/src/markit/converters/mammoth.d.ts +0 -24
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Agent Plugins discovery provider (https://agent-plugins.org).
3
+ *
4
+ * Loads the two portable component types defined by Agent Plugins 1.0.0 from
5
+ * plugin roots whose root `plugin.json` targets the standard:
6
+ *
7
+ * - Skills: immediate children of `skills/` containing a regular `SKILL.md`
8
+ * (spec §7.1), validated against the Agent Skills conventions.
9
+ * - MCP servers: the closed `mcp.json` document at the plugin root (spec §7.2),
10
+ * with `${PLUGIN_ROOT}`/`${PLUGIN_DATA}` expansion and reserved subprocess
11
+ * environment variables (spec §9).
12
+ *
13
+ * Roots come from the shared plugin registries (marketplace installs,
14
+ * `--plugin-dir`) and from configured extension packages. Legacy providers
15
+ * skip roots governed by this standard via `legacyProviderAllowed`.
16
+ */
17
+ import type { Dirent, Stats } from "node:fs";
18
+ import * as fs from "node:fs/promises";
19
+ import * as path from "node:path";
20
+ import { getPluginsDir, isEnoent, normalizeFrontmatterKeys, parseFrontmatter } from "@oh-my-pi/pi-utils";
21
+ import { registerProvider } from "../capability";
22
+ import { readFile } from "../capability/fs";
23
+ import { type MCPServer, mcpCapability } from "../capability/mcp";
24
+ import { type Skill, type SkillFrontmatter, skillCapability } from "../capability/skill";
25
+ import type { LoadContext, LoadResult } from "../capability/types";
26
+ import {
27
+ type AgentPluginManifest,
28
+ classifyAgentPluginRoot,
29
+ parseAgentPluginMcp,
30
+ validateAgentSkillFrontmatter,
31
+ } from "./agent-plugin-format";
32
+ import { resolveContainedPath } from "./contained-path";
33
+ import { compareSkillOrder, createSourceMeta, listClaudePluginRoots } from "./helpers";
34
+ import { listOmpExtensionRoots } from "./omp-extension-roots";
35
+
36
+ const PROVIDER_ID = "agent-plugins";
37
+ const DISPLAY_NAME = "Agent Plugins";
38
+ const DESCRIPTION = "Portable Agent Plugins packages (plugin.json, skills/, mcp.json) per agent-plugins.org";
39
+ // Above claude-plugins (70) so the standard's semantics win for packages that
40
+ // declare it; below claude.ts (80) so user-level .claude/ overrides still apply.
41
+ const PRIORITY = 75;
42
+
43
+ interface CandidateRoot {
44
+ path: string;
45
+ level: "user" | "project";
46
+ /**
47
+ * Stable identity of this installed plugin instance (§9.1). Registry
48
+ * installs key on plugin id + scope so the persistent data directory
49
+ * survives version-path changes across updates; local `--plugin-dir` and
50
+ * configured extension roots key on their configured directory.
51
+ */
52
+ instanceKey: string;
53
+ }
54
+
55
+ /**
56
+ * Every directory that may contain an Agent Plugin: marketplace and
57
+ * `--plugin-dir` roots from the shared registries, plus configured extension
58
+ * package roots. First occurrence wins on duplicates.
59
+ */
60
+ async function listCandidateRoots(ctx: LoadContext): Promise<CandidateRoot[]> {
61
+ const [marketplace, extensionRoots] = await Promise.all([
62
+ listClaudePluginRoots(ctx.home, ctx.cwd),
63
+ listOmpExtensionRoots(ctx),
64
+ ]);
65
+ const seen = new Set<string>();
66
+ const candidates: CandidateRoot[] = [];
67
+ for (const root of marketplace.roots) {
68
+ if (seen.has(root.path)) continue;
69
+ seen.add(root.path);
70
+ candidates.push({
71
+ path: root.path,
72
+ level: root.scope,
73
+ instanceKey: root.marketplace === "__local__" ? `dir:${root.path}` : `${root.id}#${root.scope}`,
74
+ });
75
+ }
76
+ for (const root of extensionRoots) {
77
+ if (seen.has(root.path)) continue;
78
+ seen.add(root.path);
79
+ candidates.push({ path: root.path, level: root.level, instanceKey: `ext:${root.path}` });
80
+ }
81
+ return candidates;
82
+ }
83
+
84
+ /**
85
+ * Client-managed persistent data directory dedicated to one installed plugin
86
+ * instance (§9.1). The manifest name keeps it readable; the SHA-256 digest of
87
+ * the instance identity keeps same-name installs (different marketplaces,
88
+ * scopes, or local directories) from aliasing each other's state.
89
+ */
90
+ function pluginDataDir(home: string, manifestName: string, instanceKey: string): string {
91
+ const digest = new Bun.CryptoHasher("sha256").update(instanceKey).digest("hex").slice(0, 16);
92
+ return path.join(getPluginsDir(home), "data", `${manifestName}-${digest}`);
93
+ }
94
+
95
+ // =============================================================================
96
+ // Skills
97
+ // =============================================================================
98
+
99
+ /**
100
+ * Discover skills per spec §7.1: each immediate child of `skills/` whose
101
+ * `SKILL.md` resolves to a regular file inside the plugin root is one skill;
102
+ * deeper descendants are never searched. Invalid skills are skipped with a
103
+ * warning while the rest keep loading.
104
+ */
105
+ async function scanStandardSkills(realRoot: string, level: "user" | "project"): Promise<LoadResult<Skill>> {
106
+ const items: Skill[] = [];
107
+ const warnings: string[] = [];
108
+
109
+ // §4.1: resolve the fixed location and prove containment BEFORE listing it,
110
+ // so a symlinked skills/ can never lead the scan outside the package.
111
+ const skillsDir = await resolveContainedPath(realRoot, path.join(realRoot, "skills"));
112
+ // §6.2: an absent fixed location is not an error.
113
+ if (skillsDir.status === "missing") return { items, warnings };
114
+ if (skillsDir.status === "outside") {
115
+ warnings.push(`skills/ resolves outside the plugin root`);
116
+ return { items, warnings };
117
+ }
118
+
119
+ let entries: Dirent[];
120
+ try {
121
+ entries = await fs.readdir(skillsDir.realPath, { withFileTypes: true });
122
+ } catch {
123
+ // §6.2: a present location of the wrong filesystem kind invalidates only
124
+ // this component type.
125
+ warnings.push(`skills/ does not resolve to a directory`);
126
+ return { items, warnings };
127
+ }
128
+
129
+ await Promise.all(
130
+ entries.map(async entry => {
131
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) return;
132
+ // Resolve and contain each SKILL.md before any stat/read can follow a
133
+ // symlink out of the package (§4.1 failure boundary 3).
134
+ const resolved = await resolveContainedPath(realRoot, path.join(skillsDir.realPath, entry.name, "SKILL.md"));
135
+ if (resolved.status === "missing") return; // no SKILL.md → not a skill directory
136
+ if (resolved.status === "outside") {
137
+ warnings.push(`Skipping skill "${entry.name}": SKILL.md resolves outside the plugin root`);
138
+ return;
139
+ }
140
+ const skillPath = resolved.realPath;
141
+ let stat: Stats;
142
+ try {
143
+ stat = await fs.stat(skillPath);
144
+ } catch {
145
+ return;
146
+ }
147
+ if (!stat.isFile()) return;
148
+ const content = await readFile(skillPath);
149
+ if (content === null) {
150
+ warnings.push(`Skipping skill "${entry.name}": failed to read SKILL.md`);
151
+ return;
152
+ }
153
+ // Strict parse: malformed YAML must be rejected, not repaired — no
154
+ // scalar quoting, tab replacement, comment stripping, or key aliasing.
155
+ let rawFrontmatter: Record<string, unknown>;
156
+ let body: string;
157
+ try {
158
+ ({ frontmatter: rawFrontmatter, body } = parseFrontmatter(content, {
159
+ source: skillPath,
160
+ level: "fatal",
161
+ repair: false,
162
+ rawKeys: true,
163
+ }));
164
+ } catch {
165
+ warnings.push(`Skipping skill "${entry.name}": malformed YAML frontmatter`);
166
+ return;
167
+ }
168
+ // §7.1: the Agent Skills specification is the source of truth for skill
169
+ // validity; the frontmatter schema is closed per skills-ref, so client
170
+ // conventions like `enabled` reject the skill as an unexpected field.
171
+ // Non-conforming skills are skipped without affecting other components.
172
+ const violation = validateAgentSkillFrontmatter(rawFrontmatter, entry.name);
173
+ if (violation !== null) {
174
+ warnings.push(`Skipping skill "${entry.name}": ${violation}`);
175
+ return;
176
+ }
177
+ // Validation guarantees the frontmatter name matches the directory
178
+ // (NFKC-normalized), so the on-disk directory name is the identity.
179
+ // Store with the codebase's camelCase key convention (skill:// consumers,
180
+ // prompt hiding via disableModelInvocation, …).
181
+ const frontmatter = normalizeFrontmatterKeys(rawFrontmatter) as SkillFrontmatter;
182
+ items.push({
183
+ name: entry.name,
184
+ containRoot: realRoot,
185
+ path: skillPath,
186
+ content: body,
187
+ frontmatter,
188
+ level,
189
+ _source: createSourceMeta(PROVIDER_ID, skillPath, level),
190
+ });
191
+ }),
192
+ );
193
+
194
+ items.sort((a, b) => compareSkillOrder(a.name, a.path, b.name, b.path));
195
+ return { items, warnings };
196
+ }
197
+
198
+ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
199
+ const candidates = await listCandidateRoots(ctx);
200
+ const results = await Promise.all(
201
+ candidates.map(async (candidate): Promise<LoadResult<Skill>> => {
202
+ const status = await classifyAgentPluginRoot(candidate.path);
203
+ if (status.kind === "none") return { items: [] };
204
+ if (status.kind === "invalid") {
205
+ // Fatal manifest violations reject the whole plugin (spec §5.2);
206
+ // reported once here rather than from every capability loader.
207
+ return { items: [], warnings: [`[agent-plugins] Rejected plugin at ${candidate.path}: ${status.reason}`] };
208
+ }
209
+ const scan = await scanStandardSkills(status.realRoot, candidate.level);
210
+ return {
211
+ items: scan.items,
212
+ warnings: [...status.warnings, ...(scan.warnings ?? [])].map(
213
+ warning => `[agent-plugins] ${status.manifest.name}: ${warning}`,
214
+ ),
215
+ };
216
+ }),
217
+ );
218
+ return {
219
+ items: results.flatMap(result => result.items),
220
+ warnings: results.flatMap(result => result.warnings ?? []),
221
+ };
222
+ }
223
+
224
+ // =============================================================================
225
+ // MCP Servers
226
+ // =============================================================================
227
+
228
+ async function loadPluginMCPServers(
229
+ realRoot: string,
230
+ manifest: AgentPluginManifest,
231
+ candidate: CandidateRoot,
232
+ home: string,
233
+ ): Promise<LoadResult<MCPServer>> {
234
+ const items: MCPServer[] = [];
235
+ const warnings: string[] = [];
236
+ // §4.1: resolve and contain mcp.json before any stat/read can follow a
237
+ // symlink out of the package (failure boundary 2).
238
+ const resolved = await resolveContainedPath(realRoot, path.join(realRoot, "mcp.json"));
239
+ if (resolved.status === "missing") return { items, warnings };
240
+ if (resolved.status === "outside") {
241
+ warnings.push(`mcp.json resolves outside the plugin root`);
242
+ return { items, warnings };
243
+ }
244
+ const mcpPath = resolved.realPath;
245
+
246
+ let stat: Stats;
247
+ try {
248
+ stat = await fs.stat(mcpPath);
249
+ } catch (err) {
250
+ if (!isEnoent(err)) warnings.push(`Failed to read mcp.json: ${String(err)}`);
251
+ return { items, warnings };
252
+ }
253
+ if (!stat.isFile()) {
254
+ warnings.push(`mcp.json does not resolve to a regular file`);
255
+ return { items, warnings };
256
+ }
257
+ const raw = await readFile(mcpPath);
258
+ if (raw === null) {
259
+ warnings.push(`Failed to read mcp.json`);
260
+ return { items, warnings };
261
+ }
262
+
263
+ // Client-managed persistent data directory dedicated to this installed
264
+ // plugin instance; contents survive plugin updates (spec §9.1).
265
+ const pluginData = pluginDataDir(home, manifest.name, candidate.instanceKey);
266
+ const result = await parseAgentPluginMcp(raw, { pluginRoot: realRoot, pluginData });
267
+ if (result.status === "disabled") {
268
+ warnings.push(`MCP disabled: ${result.reason}`);
269
+ return { items, warnings };
270
+ }
271
+ warnings.push(...result.warnings);
272
+
273
+ if (result.servers.some(server => server.transport === "stdio")) {
274
+ // §9.1: the data directory must exist and be writable before any plugin
275
+ // subprocess launches.
276
+ await fs.mkdir(pluginData, { recursive: true });
277
+ }
278
+
279
+ for (const server of result.servers) {
280
+ items.push({
281
+ // Namespace by plugin name so servers from different plugins cannot
282
+ // collide in the capability registry (matches claude-plugins).
283
+ name: `${manifest.name}:${server.name}`,
284
+ transport: server.transport,
285
+ ...(server.command !== undefined && { command: server.command }),
286
+ ...(server.args !== undefined && { args: server.args }),
287
+ ...(server.env !== undefined && { env: server.env }),
288
+ // §9.2: env values are literal after the provider's ${PLUGIN_ROOT} /
289
+ // ${PLUGIN_DATA} expansion — exempt from any further resolution.
290
+ ...(server.command !== undefined && { envPolicy: "literal" as const }),
291
+ ...(server.cwd !== undefined && { cwd: server.cwd }),
292
+ ...(server.url !== undefined && { url: server.url }),
293
+ ...(server.headers !== undefined && { headers: server.headers }),
294
+ // §7.2.1: configured headers are literal, origin-locked package data —
295
+ // never expanded and never forwarded to a different origin.
296
+ ...(server.url !== undefined && { headerPolicy: "origin-locked" as const }),
297
+ _source: createSourceMeta(PROVIDER_ID, mcpPath, candidate.level),
298
+ });
299
+ }
300
+ return { items, warnings };
301
+ }
302
+
303
+ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>> {
304
+ const candidates = await listCandidateRoots(ctx);
305
+ const results = await Promise.all(
306
+ candidates.map(async (candidate): Promise<LoadResult<MCPServer>> => {
307
+ const status = await classifyAgentPluginRoot(candidate.path);
308
+ // Invalid roots are rejected and reported by the skills loader.
309
+ if (status.kind !== "standard") return { items: [] };
310
+ const loaded = await loadPluginMCPServers(status.realRoot, status.manifest, candidate, ctx.home);
311
+ return {
312
+ items: loaded.items,
313
+ warnings: (loaded.warnings ?? []).map(warning => `[agent-plugins] ${status.manifest.name}: ${warning}`),
314
+ };
315
+ }),
316
+ );
317
+ return {
318
+ items: results.flatMap(result => result.items),
319
+ warnings: results.flatMap(result => result.warnings ?? []),
320
+ };
321
+ }
322
+
323
+ // =============================================================================
324
+ // Provider Registration
325
+ // =============================================================================
326
+
327
+ registerProvider<Skill>(skillCapability.id, {
328
+ id: PROVIDER_ID,
329
+ displayName: DISPLAY_NAME,
330
+ description: DESCRIPTION,
331
+ priority: PRIORITY,
332
+ load: loadSkills,
333
+ });
334
+
335
+ registerProvider<MCPServer>(mcpCapability.id, {
336
+ id: PROVIDER_ID,
337
+ displayName: DISPLAY_NAME,
338
+ description: DESCRIPTION,
339
+ priority: PRIORITY,
340
+ load: loadMCPServers,
341
+ });
@@ -44,12 +44,14 @@ function isWsl(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): boolean {
44
44
  function convertWindowsPathToDefaultWslMount(windowsPath: string): string | undefined {
45
45
  const trimmed = windowsPath.trim();
46
46
  if (trimmed.length === 0) return undefined;
47
- if (path.isAbsolute(trimmed)) return path.normalize(trimmed);
47
+ // The result is always a WSL (POSIX) path, so build it with posix
48
+ // semantics regardless of the host platform.
49
+ if (path.posix.isAbsolute(trimmed)) return path.posix.normalize(trimmed);
48
50
  const match = WINDOWS_DRIVE_PROFILE_PATTERN.exec(trimmed);
49
51
  if (!match) return undefined;
50
52
  const [, drive, rest] = match;
51
53
  const segments = rest.replace(/\\/g, "/").split("/").filter(Boolean);
52
- return path.join("/mnt", drive.toLowerCase(), ...segments);
54
+ return path.posix.join("/mnt", drive.toLowerCase(), ...segments);
53
55
  }
54
56
 
55
57
  function resolveWithWslPath(windowsPath: string): string | undefined {
@@ -27,14 +27,15 @@ const flag = (opts as { enabled: boolean })["enabled"];
27
27
 
28
28
  ## Use
29
29
 
30
- Prefer a schema parse at the boundary when a validator is available (Zod, Valibot, …) — validate once, then read from a fully typed value. If Zod is already in the project (e.g. Zod v4):
30
+ Prefer a schema parse at the boundary when a validator is available — validate
31
+ once, then read from a fully typed value:
31
32
 
32
33
  ```ts
33
- import { z } from "zod/v4";
34
+ import { type } from "@oh-my-pi/omptype";
34
35
 
35
- const Resp = z.object({ data: z.object({ id: z.string() }) });
36
+ const Resp = type({ data: { id: "string" } });
36
37
 
37
- const resp = Resp.parse(raw); // throws on bad input; resp.data.id is typed string
38
+ const resp = Resp.assert(raw); // throws on bad input; resp.data.id is typed string
38
39
  const id = resp.data.id;
39
40
  ```
40
41
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  description: "Do not extract 1-2 line functions that only wrap an expression — inline them"
3
- condition: "\\{\\s*return [^;{}\\n]+;?\\s*\\}|\\b(?:const|let|var)\\s+[\\w$]+\\s*=\\s*(\\([^)]*\\)|[a-zA-Z_$][\\w$]*)\\s*=>\\s*[^{\\n]+$"
3
+ condition: "(?m)\\{\\s*return [^;{}\\n]+;?\\s*\\}|\\b(?:const|let|var)\\s+[\\w$]+\\s*=\\s*(\\([^)]*\\)|[a-zA-Z_$][\\w$]*)\\s*=>\\s*[^{\\n]+$"
4
4
  scope: "tool:edit(*.ts), tool:edit(*.tsx), tool:write(*.ts), tool:write(*.tsx)"
5
5
  interruptMode: never
6
6
  ---
@@ -15,6 +15,7 @@ import { type Skill, skillCapability } from "../capability/skill";
15
15
  import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
16
16
  import { type CustomTool, toolCapability } from "../capability/tool";
17
17
  import type { LoadContext, LoadResult } from "../capability/types";
18
+ import { legacyProviderAllowed } from "./agent-plugin-format";
18
19
  import {
19
20
  type ClaudePluginRoot,
20
21
  createSourceMeta,
@@ -30,6 +31,21 @@ const PROVIDER_ID = "claude-plugins";
30
31
  const DISPLAY_NAME = "Claude Code Marketplace";
31
32
  const PRIORITY = 70; // Below claude.ts (80) so user .claude/ overrides win
32
33
 
34
+ /**
35
+ * Plugin roots this legacy provider may process for a given surface. Roots
36
+ * whose root `plugin.json` targets the Agent Plugins standard keep their
37
+ * portable components (skills, MCP) exclusive to the `agent-plugins` provider;
38
+ * fatally invalid Agent Plugins packages are skipped entirely.
39
+ */
40
+ async function allowedRoots(
41
+ ctx: LoadContext,
42
+ surface: "skills" | "mcp" | "other",
43
+ ): Promise<{ roots: ClaudePluginRoot[]; warnings: string[] }> {
44
+ const { roots, warnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
45
+ const flags = await Promise.all(roots.map(root => legacyProviderAllowed(root.path, surface)));
46
+ return { roots: roots.filter((_, i) => flags[i]), warnings };
47
+ }
48
+
33
49
  interface ClaudePluginManifest {
34
50
  skills?: string | string[];
35
51
  "slash-commands"?: string | string[];
@@ -188,7 +204,7 @@ async function resolvePluginDir(
188
204
  async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
189
205
  const items: Skill[] = [];
190
206
  const warnings: string[] = [];
191
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
207
+ const { roots, warnings: rootWarnings } = await allowedRoots(ctx, "skills");
192
208
  warnings.push(...rootWarnings);
193
209
  const results = await Promise.all(
194
210
  roots.map(async root => {
@@ -235,7 +251,7 @@ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashComm
235
251
  const items: SlashCommand[] = [];
236
252
  const warnings: string[] = [];
237
253
 
238
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
254
+ const { roots, warnings: rootWarnings } = await allowedRoots(ctx, "other");
239
255
  warnings.push(...rootWarnings);
240
256
 
241
257
  const results = await Promise.all(
@@ -309,7 +325,7 @@ async function loadHooks(ctx: LoadContext): Promise<LoadResult<Hook>> {
309
325
  const items: Hook[] = [];
310
326
  const warnings: string[] = [];
311
327
 
312
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
328
+ const { roots, warnings: rootWarnings } = await allowedRoots(ctx, "other");
313
329
  warnings.push(...rootWarnings);
314
330
 
315
331
  const hookTypes = ["pre", "post"] as const;
@@ -356,7 +372,7 @@ async function loadTools(ctx: LoadContext): Promise<LoadResult<CustomTool>> {
356
372
  const items: CustomTool[] = [];
357
373
  const warnings: string[] = [];
358
374
 
359
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
375
+ const { roots, warnings: rootWarnings } = await allowedRoots(ctx, "other");
360
376
  warnings.push(...rootWarnings);
361
377
 
362
378
  const results = await Promise.all(
@@ -482,7 +498,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
482
498
  const items: MCPServer[] = [];
483
499
  const warnings: string[] = [];
484
500
 
485
- const { roots, warnings: rootWarnings } = await listClaudePluginRoots(ctx.home, ctx.cwd);
501
+ const { roots, warnings: rootWarnings } = await allowedRoots(ctx, "mcp");
486
502
  warnings.push(...rootWarnings);
487
503
 
488
504
  for (const root of roots) {
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Package-boundary containment for plugin directories (Agent Plugins §4.1).
3
+ *
4
+ * A file or directory supplied by a plugin package may only be discovered,
5
+ * read, or executed when its filesystem-resolved path stays within the
6
+ * filesystem-resolved plugin root. These helpers resolve paths WITHOUT reading
7
+ * them, so callers can prove containment before any read, readdir, or
8
+ * symlink-following stat touches content outside the package.
9
+ *
10
+ * Shared by the Agent Plugins format module and the plugin-root helpers;
11
+ * imports nothing from either to stay cycle-free.
12
+ */
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+
16
+ /** Lexical containment: `target` is `base` itself or a descendant of it. */
17
+ export function isContained(base: string, target: string): boolean {
18
+ const relative = path.relative(base, target);
19
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
20
+ }
21
+
22
+ /** Resolve symlinks and equivalents; `null` when the path is missing or unresolvable. */
23
+ export async function realpathIfExists(p: string): Promise<string | null> {
24
+ try {
25
+ return await fs.promises.realpath(p);
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ /** Outcome of resolving a fixed package path without reading it. */
32
+ export type ContainedPathResolution =
33
+ | { status: "missing" }
34
+ | { status: "outside" }
35
+ | { status: "ok"; realPath: string };
36
+
37
+ /**
38
+ * Resolve a fixed package path WITHOUT reading it: symlinks and equivalent
39
+ * mechanisms are resolved first, and a path escaping the filesystem-resolved
40
+ * plugin root is rejected before any I/O could consume outside content.
41
+ */
42
+ export async function resolveContainedPath(realBase: string, target: string): Promise<ContainedPathResolution> {
43
+ if (!isContained(realBase, target)) return { status: "outside" };
44
+ const real = await realpathIfExists(target);
45
+ if (real === null) return { status: "missing" };
46
+ return isContained(realBase, real) ? { status: "ok", realPath: real } : { status: "outside" };
47
+ }
48
+
49
+ /**
50
+ * Verify a package path stays within the filesystem-resolved plugin root:
51
+ * lexically, and — when the target exists — after resolving symlinks. Unlike
52
+ * {@link resolveContainedPath}, a missing target passes. ONLY for configured
53
+ * paths that are validated but never read or executed at load time (stdio
54
+ * `command`, `cwd` — including `${PLUGIN_DATA}` paths created after
55
+ * validation); access paths MUST use {@link resolveContainedPath} instead,
56
+ * which fails closed on unresolvable targets.
57
+ */
58
+ export async function isContainedResolved(realBase: string, target: string): Promise<boolean> {
59
+ if (!isContained(realBase, target)) return false;
60
+ const real = await realpathIfExists(target);
61
+ return real === null || isContained(realBase, real);
62
+ }
63
+
64
+ /**
65
+ * Sync variant of {@link resolveContainedPath} for synchronous callsites
66
+ * (bash `skill://` expansion).
67
+ */
68
+ export function resolveContainedPathSync(realBase: string, target: string): ContainedPathResolution {
69
+ if (!isContained(realBase, target)) return { status: "outside" };
70
+ let real: string | null;
71
+ try {
72
+ real = fs.realpathSync(target);
73
+ } catch {
74
+ real = null;
75
+ }
76
+ if (real === null) return { status: "missing" };
77
+ return isContained(realBase, real) ? { status: "ok", realPath: real } : { status: "outside" };
78
+ }
@@ -20,6 +20,7 @@ import type { MCPRequestIdFormat } from "../mcp/types";
20
20
  import { type ConfiguredThinkingLevel, parseConfiguredThinkingLevel } from "../thinking";
21
21
  import { normalizeToolNames } from "../tools/builtin-names";
22
22
 
23
+ import { realpathIfExists, resolveContainedPath } from "./contained-path";
23
24
  import { buildPluginDirRoot } from "./plugin-dir-roots";
24
25
 
25
26
  /**
@@ -1141,17 +1142,30 @@ export async function injectPluginDirRoots(home: string, dirs: string[], cwd?: s
1141
1142
  const injected: ClaudePluginRoot[] = [];
1142
1143
  for (const dir of dirs) {
1143
1144
  const resolved = path.resolve(dir);
1144
- // Read plugin name from manifest
1145
+ // Read plugin name from manifest: Claude marketplace layout first, then
1146
+ // the Agent Plugins standard root manifest (agent-plugins.org). Each
1147
+ // manifest is resolved and proven inside the plugin directory BEFORE the
1148
+ // read (Agent Plugins §4.1) — an escaping symlink falls back to the
1149
+ // directory basename without consuming outside content.
1145
1150
  let pluginName = path.basename(resolved);
1146
- try {
1147
- const manifestPath = path.join(resolved, ".claude-plugin", "plugin.json");
1148
- const content = await Bun.file(manifestPath).text();
1149
- const manifest = JSON.parse(content);
1150
- if (typeof manifest.name === "string" && manifest.name) {
1151
- pluginName = manifest.name;
1151
+ const realRoot = await realpathIfExists(resolved);
1152
+ if (realRoot !== null) {
1153
+ for (const manifestPath of [
1154
+ path.join(realRoot, ".claude-plugin", "plugin.json"),
1155
+ path.join(realRoot, "plugin.json"),
1156
+ ]) {
1157
+ const contained = await resolveContainedPath(realRoot, manifestPath);
1158
+ if (contained.status !== "ok") continue;
1159
+ try {
1160
+ const manifest = await Bun.file(contained.realPath).json();
1161
+ if (typeof manifest?.name === "string" && manifest.name) {
1162
+ pluginName = manifest.name;
1163
+ break;
1164
+ }
1165
+ } catch {
1166
+ // Invalid manifest — try next, fall back to directory name
1167
+ }
1152
1168
  }
1153
- } catch {
1154
- // No manifest or invalid — use directory name
1155
1169
  }
1156
1170
 
1157
1171
  injected.push(buildPluginDirRoot(resolved, pluginName));
@@ -20,6 +20,7 @@ import "../capability/ssh";
20
20
  import "../capability/system-prompt";
21
21
  import "../capability/tool";
22
22
  // Import providers (each registers itself on import)
23
+ import "./agent-plugins";
23
24
  import "./agents-md";
24
25
  import "./builtin";
25
26
  import "./builtin-defaults";