one-code-extension 0.1.4 → 0.3.1

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 (300) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +222 -177
  3. package/agents/explore.md +9 -6
  4. package/agents/general-purpose.md +1 -1
  5. package/agents/plan.md +6 -4
  6. package/docs/guide/README.md +52 -0
  7. package/docs/guide/bring-your-claude-code-setup.md +128 -0
  8. package/docs/guide/configuration.md +173 -0
  9. package/docs/guide/differences-from-claude-code.md +152 -0
  10. package/docs/guide/doctor.md +88 -0
  11. package/docs/guide/hooks.md +111 -0
  12. package/docs/guide/installation.md +126 -0
  13. package/docs/guide/permissions-modes-and-auto-mode.md +452 -0
  14. package/docs/guide/providers-and-models.md +196 -0
  15. package/docs/guide/reference.md +226 -0
  16. package/docs/guide/sessions-and-context.md +192 -0
  17. package/docs/guide/skills-plugins-and-mcp.md +178 -0
  18. package/docs/guide/subagents-and-workflows.md +195 -0
  19. package/docs/guide/tasks-and-background-work.md +113 -0
  20. package/docs/guide/terminal-interface.md +157 -0
  21. package/docs/guide/tools.md +177 -0
  22. package/docs/guide/troubleshooting.md +177 -0
  23. package/extensions/ask-user/index.ts +65 -71
  24. package/extensions/ask-user/questions.ts +15 -28
  25. package/extensions/ask-user/widget.ts +533 -0
  26. package/extensions/auto-mode/actions.ts +9 -26
  27. package/extensions/auto-mode/classifier-prompt.ts +127 -0
  28. package/extensions/auto-mode/classifier.ts +305 -227
  29. package/extensions/auto-mode/config.ts +279 -77
  30. package/extensions/auto-mode/defaults.ts +17 -60
  31. package/extensions/auto-mode/instructions.ts +2 -1
  32. package/extensions/auto-mode/model-picker.ts +53 -31
  33. package/extensions/auto-mode/model-select.ts +153 -82
  34. package/extensions/auto-mode/paths.ts +13 -1
  35. package/extensions/auto-mode/pause.ts +8 -0
  36. package/extensions/auto-mode/prompt.ts +205 -238
  37. package/extensions/auto-mode/recoverability.ts +229 -0
  38. package/extensions/auto-mode/rules.ts +92 -75
  39. package/extensions/auto-mode/safety-floor.ts +43 -12
  40. package/extensions/auto-mode/sensitive.ts +11 -0
  41. package/extensions/auto-mode/setup-run.ts +245 -0
  42. package/extensions/auto-mode/setup.ts +235 -0
  43. package/extensions/auto-mode/shell-analysis.ts +332 -26
  44. package/extensions/auto-mode/transcript.ts +102 -0
  45. package/extensions/background/index.ts +256 -43
  46. package/extensions/background/monitor-batch.ts +58 -0
  47. package/extensions/background/registry.ts +9 -0
  48. package/extensions/background/wakeup.ts +45 -0
  49. package/extensions/bash/background.ts +47 -22
  50. package/extensions/bash/guards.ts +229 -0
  51. package/extensions/bash/index.ts +81 -19
  52. package/extensions/branding/index.ts +44 -18
  53. package/extensions/branding/startup.ts +23 -8
  54. package/extensions/claude-compat/index.ts +17 -2
  55. package/extensions/claude-context/index.ts +41 -12
  56. package/extensions/clear/index.ts +27 -0
  57. package/extensions/compaction/fit.ts +121 -0
  58. package/extensions/compaction/index.ts +176 -71
  59. package/extensions/compaction/prompt.ts +21 -1
  60. package/extensions/context-budget/budget.ts +61 -0
  61. package/extensions/context-budget/index.ts +66 -0
  62. package/extensions/context-management/index.ts +40 -16
  63. package/extensions/doctor/build.ts +226 -0
  64. package/extensions/doctor/cli.ts +99 -0
  65. package/extensions/doctor/compat.ts +532 -0
  66. package/extensions/doctor/dependencies.ts +186 -0
  67. package/extensions/doctor/fix-prompt.ts +125 -0
  68. package/extensions/doctor/index.ts +308 -0
  69. package/extensions/doctor/models.ts +231 -0
  70. package/extensions/doctor/presets.ts +208 -0
  71. package/extensions/doctor/report.ts +189 -0
  72. package/extensions/doctor/update-lookup.ts +46 -0
  73. package/extensions/doctor/viewer.ts +124 -0
  74. package/extensions/effort/index.ts +41 -4
  75. package/extensions/effort/slider.ts +33 -2
  76. package/extensions/file-tracker/index.ts +143 -20
  77. package/extensions/file-tracker/replay.ts +46 -0
  78. package/extensions/file-tracker/tracker.ts +51 -4
  79. package/extensions/footer/footer-line.ts +207 -0
  80. package/extensions/footer/index.ts +152 -0
  81. package/extensions/footer/pr.ts +53 -0
  82. package/extensions/hooks/executor.ts +57 -20
  83. package/extensions/hooks/formatter-notice.ts +76 -0
  84. package/extensions/hooks/index.ts +314 -73
  85. package/extensions/hooks/matcher.ts +35 -1
  86. package/extensions/hooks/plugin-hooks.ts +3 -3
  87. package/extensions/hooks/protocol.ts +58 -0
  88. package/extensions/hooks/subagent-bridge.ts +77 -0
  89. package/extensions/hooks/trust.ts +15 -4
  90. package/extensions/init/index.ts +26 -0
  91. package/extensions/init/prompt.ts +40 -0
  92. package/extensions/interrupted/index.ts +35 -0
  93. package/extensions/interrupted/line.ts +9 -0
  94. package/extensions/lib/abort.ts +15 -0
  95. package/extensions/lib/agent-loader.ts +183 -0
  96. package/extensions/lib/anthropic-payload.ts +15 -0
  97. package/extensions/lib/atomic-write.ts +56 -0
  98. package/extensions/lib/bridge-watch.ts +20 -0
  99. package/extensions/lib/cache-retention.ts +60 -0
  100. package/extensions/lib/capability-index.ts +466 -0
  101. package/extensions/lib/claude-context.ts +318 -27
  102. package/extensions/lib/claude-settings.ts +64 -0
  103. package/extensions/lib/consent-preview.ts +30 -0
  104. package/extensions/lib/deferred.ts +144 -13
  105. package/extensions/lib/favorites.ts +43 -0
  106. package/extensions/lib/fetch-timeout.ts +15 -0
  107. package/extensions/lib/frontmatter.ts +31 -0
  108. package/extensions/lib/git.ts +44 -2
  109. package/extensions/lib/hook-gate.ts +90 -0
  110. package/extensions/lib/interrupt.ts +94 -0
  111. package/extensions/lib/mcp-overrides.ts +61 -0
  112. package/extensions/lib/mcp-share.ts +25 -0
  113. package/extensions/lib/mcp-status.ts +34 -0
  114. package/extensions/lib/memory.ts +41 -6
  115. package/extensions/lib/model-facts.generated.json +2480 -0
  116. package/extensions/lib/model-facts.ts +162 -0
  117. package/extensions/lib/model-policy.ts +145 -189
  118. package/extensions/lib/model-tier.ts +480 -64
  119. package/extensions/lib/model-unusable.ts +37 -0
  120. package/extensions/lib/notifications.ts +320 -1
  121. package/extensions/lib/one-code-settings.ts +63 -0
  122. package/extensions/lib/original-command.ts +71 -0
  123. package/extensions/lib/package-version.ts +31 -0
  124. package/extensions/lib/paths.ts +39 -4
  125. package/extensions/lib/permission-gate.ts +192 -0
  126. package/extensions/lib/persisted-output.ts +18 -0
  127. package/extensions/lib/pi-ai-estimate.ts +160 -0
  128. package/extensions/lib/pi-version.ts +1 -1
  129. package/extensions/lib/plan-mode-channels.ts +11 -0
  130. package/extensions/lib/plugin-overrides.ts +24 -0
  131. package/extensions/lib/plugin-root.ts +44 -0
  132. package/extensions/lib/plugins.ts +233 -54
  133. package/extensions/lib/prefix-warm-gate.ts +157 -0
  134. package/extensions/lib/process-tree.ts +56 -0
  135. package/extensions/lib/reminders.ts +321 -68
  136. package/extensions/lib/scratchpad.ts +11 -8
  137. package/extensions/lib/session-lifecycle.ts +28 -0
  138. package/extensions/lib/settings-channels.ts +14 -0
  139. package/extensions/lib/shell-tasks.ts +54 -0
  140. package/extensions/lib/skill-overrides.ts +86 -0
  141. package/extensions/lib/skill-scan.ts +110 -0
  142. package/extensions/lib/text-width.ts +267 -0
  143. package/extensions/lib/tui-render.ts +281 -54
  144. package/extensions/lib/usage-bus.ts +75 -0
  145. package/extensions/lib/usage-tracker.ts +67 -0
  146. package/extensions/lib/worktree-isolation.ts +170 -0
  147. package/extensions/lsp/client.ts +69 -12
  148. package/extensions/lsp/format.ts +12 -2
  149. package/extensions/lsp/index.ts +273 -84
  150. package/extensions/lsp/install-hints.ts +43 -0
  151. package/extensions/lsp/keep-alive.ts +19 -0
  152. package/extensions/lsp/plugin-servers.ts +331 -0
  153. package/extensions/lsp/watcher.ts +157 -0
  154. package/extensions/mcp/announce.ts +99 -0
  155. package/extensions/mcp/client.ts +181 -22
  156. package/extensions/mcp/config.ts +55 -24
  157. package/extensions/mcp/index.ts +586 -86
  158. package/extensions/mcp/oauth/browser.ts +26 -0
  159. package/extensions/mcp/oauth/callback.ts +104 -0
  160. package/extensions/mcp/oauth/flow.ts +68 -0
  161. package/extensions/mcp/oauth/provider.ts +147 -0
  162. package/extensions/mcp/oauth/store.ts +86 -0
  163. package/extensions/mcp/panel/keys.ts +34 -0
  164. package/extensions/mcp/panel/model.ts +62 -0
  165. package/extensions/mcp/panel/render.ts +141 -0
  166. package/extensions/mcp/panel/state.ts +108 -0
  167. package/extensions/mcp/schema.ts +83 -6
  168. package/extensions/mcp/trust.ts +340 -0
  169. package/extensions/memory/entries.ts +163 -0
  170. package/extensions/memory/index.ts +119 -2
  171. package/extensions/memory/open-external.ts +97 -0
  172. package/extensions/memory/panel.ts +124 -0
  173. package/extensions/model-default/index.ts +74 -0
  174. package/extensions/notebook/index.ts +36 -18
  175. package/extensions/notebook/notebook.ts +110 -17
  176. package/extensions/permissions/index.ts +1012 -247
  177. package/extensions/permissions/matcher.ts +488 -57
  178. package/extensions/permissions/modes.ts +55 -7
  179. package/extensions/permissions/project-trust.ts +81 -0
  180. package/extensions/permissions/protected-paths.ts +16 -3
  181. package/extensions/permissions/rule-prose.ts +79 -0
  182. package/extensions/permissions/session-grant.ts +91 -0
  183. package/extensions/permissions/settings.ts +112 -33
  184. package/extensions/permissions/subagent-gate.ts +57 -0
  185. package/extensions/plan-mode/index.ts +143 -33
  186. package/extensions/plan-mode/reminder.ts +16 -19
  187. package/extensions/plan-mode/viewer.ts +40 -31
  188. package/extensions/plugins/counts.ts +68 -0
  189. package/extensions/plugins/index.ts +496 -32
  190. package/extensions/plugins/install/install.ts +167 -0
  191. package/extensions/plugins/install/paths.ts +23 -0
  192. package/extensions/plugins/install/registry.ts +72 -0
  193. package/extensions/plugins/marketplace/git.ts +103 -0
  194. package/extensions/plugins/marketplace/official.ts +36 -0
  195. package/extensions/plugins/marketplace/parse.ts +94 -0
  196. package/extensions/plugins/marketplace/registry.ts +60 -0
  197. package/extensions/plugins/marketplace/sync.ts +146 -0
  198. package/extensions/plugins/marketplace/types.ts +128 -0
  199. package/extensions/plugins/panel/keys.ts +52 -0
  200. package/extensions/plugins/panel/render.ts +315 -0
  201. package/extensions/plugins/panel/rows.ts +230 -0
  202. package/extensions/plugins/panel/state.ts +333 -0
  203. package/extensions/recap/index.ts +215 -0
  204. package/extensions/recap/prompt.ts +44 -0
  205. package/extensions/recap/scheduler.ts +87 -0
  206. package/extensions/search-tools/index.ts +0 -0
  207. package/extensions/search-tools/tools.ts +7 -5
  208. package/extensions/skill/index.ts +360 -34
  209. package/extensions/skill/invoke.ts +185 -0
  210. package/extensions/skill/listing.ts +21 -0
  211. package/extensions/skill/panel/keys.ts +41 -0
  212. package/extensions/skill/panel/render.ts +91 -0
  213. package/extensions/skill/panel/state.ts +115 -0
  214. package/extensions/spinner/index.ts +110 -0
  215. package/extensions/spinner/line.ts +51 -0
  216. package/extensions/spinner/verbs.ts +201 -0
  217. package/extensions/subagents/agents.ts +88 -44
  218. package/extensions/subagents/default-model.ts +62 -32
  219. package/extensions/subagents/delegation-steer.ts +23 -0
  220. package/extensions/subagents/hand-back-review.ts +67 -0
  221. package/extensions/subagents/index.ts +1545 -346
  222. package/extensions/subagents/live-runs.ts +293 -0
  223. package/extensions/subagents/model-select.ts +252 -82
  224. package/extensions/subagents/outcome.ts +90 -0
  225. package/extensions/subagents/panel-keys.ts +41 -0
  226. package/extensions/subagents/panel-render.ts +357 -0
  227. package/extensions/subagents/panel-widget.ts +330 -0
  228. package/extensions/subagents/pending-claim.ts +45 -0
  229. package/extensions/subagents/prose.ts +105 -0
  230. package/extensions/subagents/runner.ts +674 -0
  231. package/extensions/subagents/runs.ts +68 -1
  232. package/extensions/subagents/send-to-main-tool.ts +55 -0
  233. package/extensions/subagents/session-turns.ts +138 -0
  234. package/extensions/subagents/shell-panel.ts +198 -0
  235. package/extensions/subagents/usage.ts +16 -4
  236. package/extensions/subagents/worktree.ts +13 -0
  237. package/extensions/system-prompt/environment.ts +5 -3
  238. package/extensions/system-prompt/git-status.ts +93 -0
  239. package/extensions/system-prompt/index.ts +23 -1
  240. package/extensions/system-prompt/template.ts +34 -4
  241. package/extensions/system-prompt/tiers/common.ts +31 -6
  242. package/extensions/system-prompt/tiers/low.ts +13 -3
  243. package/extensions/system-prompt/tiers/mid.ts +21 -8
  244. package/extensions/system-reminder/index.ts +53 -9
  245. package/extensions/tasks/index.ts +65 -16
  246. package/extensions/tasks/store.ts +76 -3
  247. package/extensions/tool-search/announce.ts +42 -0
  248. package/extensions/tool-search/index.ts +136 -13
  249. package/extensions/turn-duration/index.ts +60 -0
  250. package/extensions/turn-duration/line.ts +22 -0
  251. package/extensions/turn-duration/span.ts +41 -0
  252. package/extensions/turn-duration/verbs.ts +24 -0
  253. package/extensions/web/backends.ts +421 -0
  254. package/extensions/web/index.ts +178 -21
  255. package/extensions/web-fetch/index.ts +74 -18
  256. package/extensions/web-fetch/summarize.ts +1 -1
  257. package/extensions/workflow/agent-session.ts +182 -112
  258. package/extensions/workflow/globals.ts +129 -106
  259. package/extensions/workflow/index.ts +343 -28
  260. package/extensions/workflow/records.ts +118 -0
  261. package/extensions/workflow/run-manager.ts +80 -31
  262. package/extensions/workflow/saved-workflows.ts +2 -1
  263. package/extensions/workflow/script-worker.mjs +110 -0
  264. package/extensions/workflow/types.ts +74 -8
  265. package/extensions/workflow/viewer.ts +562 -0
  266. package/extensions/workflow/vm-runtime.ts +118 -22
  267. package/extensions/workflow/widget.ts +126 -18
  268. package/extensions/worktree/guards.ts +240 -0
  269. package/extensions/worktree/index.ts +30 -5
  270. package/extensions/worktree/rewrite.ts +32 -16
  271. package/package.json +20 -10
  272. package/skills/code-review/SKILL.md +165 -0
  273. package/skills/fewer-permission-prompts/SKILL.md +61 -0
  274. package/skills/security-review/SKILL.md +178 -0
  275. package/skills/simplify/SKILL.md +70 -0
  276. package/themes/{one-code-light.json → onecode-light.json} +6 -1
  277. package/themes/{one-code.json → onecode.json} +6 -1
  278. package/types/app.d.ts +8 -1
  279. package/types/community-packages.d.ts +45 -2
  280. package/docs/decisions/auto-mode.md +0 -526
  281. package/docs/decisions/branding.md +0 -127
  282. package/docs/decisions/code-review-remediation.md +0 -106
  283. package/docs/decisions/compaction.md +0 -140
  284. package/docs/decisions/distribution.md +0 -168
  285. package/docs/decisions/hooks.md +0 -64
  286. package/docs/decisions/lsp.md +0 -58
  287. package/docs/decisions/mcp.md +0 -104
  288. package/docs/decisions/memory-state.md +0 -170
  289. package/docs/decisions/model-policy.md +0 -126
  290. package/docs/decisions/modes.md +0 -178
  291. package/docs/decisions/skills-plugins.md +0 -56
  292. package/docs/decisions/subagents-workflows.md +0 -225
  293. package/docs/decisions/system-prompt.md +0 -98
  294. package/docs/decisions/tools.md +0 -407
  295. package/docs/decisions/tui.md +0 -217
  296. package/docs/decisions.md +0 -148
  297. package/extensions/subagents/child.ts +0 -414
  298. package/extensions/subagents/rpc-turns.ts +0 -123
  299. package/extensions/todo/index.ts +0 -133
  300. package/extensions/workflow/permission-gate.ts +0 -71
@@ -1,41 +1,89 @@
1
1
  /**
2
2
  * plugins extension — Claude Code plugin support.
3
3
  *
4
- * Reads `~/.claude/plugins/installed_plugins.json`, then publishes each
5
- * plugin's resources so the other extensions can use them:
4
+ * Two jobs:
6
5
  *
7
- * agents/ → the subagent tool, as `<plugin>:<agent>`
8
- * skills/ → the skill tool, as `<plugin>:<skill>`
9
- * commands/ → slash commands, as `/<plugin>:<command>`
10
- * .mcp.json → the MCP client
6
+ * 1. Wire installed plugins' slash commands in (agents/skills/MCP/LSP/hooks
7
+ * are consumed by their own extensions via discoverPlugins — module state
8
+ * is not shared between extension files, so each re-derives).
9
+ * 2. The /plugins panel: a bounded tabbed manager (Discover / Installed /
10
+ * Marketplaces / Errors) that docks below the transcript like Claude Code's
11
+ * (see PANEL_MAX_HEIGHT), over the marketplace backend in ./marketplace and
12
+ * ./install. All layout/key logic is pure in ./panel; this file owns
13
+ * mutable state, async marketplace/install work, and repaints.
11
14
  *
12
- * Everything is namespaced the way Claude Code namespaces it, so two plugins can
13
- * both ship a `commit` command. This extension only owns the slash commands and
14
- * the `/plugins` listing; the other consumers call `discoverPlugins()` for
15
- * themselves, because module state is not shared between extension files.
15
+ * Every write goes to the One Code plugin root (lib/plugin-root.ts) —
16
+ * ~/.claude is read-only here, always.
16
17
  */
17
18
 
18
19
  import { execFile } from "node:child_process";
19
- import { readFileSync } from "node:fs";
20
- import os from "node:os";
20
+ import { readFileSync, rmSync } from "node:fs";
21
21
  import { join } from "node:path";
22
22
  import { promisify } from "node:util";
23
- import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
24
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
25
- import { discoverPlugins, type Plugin } from "../lib/plugins.ts";
23
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
24
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
25
+ import { readFavorites, toggleFavorite } from "../lib/favorites.ts";
26
+ import { MCP_STATUS_CHANNEL, MCP_STATUS_REQUEST_CHANNEL, type McpStatusEvent } from "../lib/mcp-status.ts";
27
+ import { persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
28
+ import { setOverride } from "../lib/plugin-overrides.ts";
29
+ import { pathWithinBase, pluginRoot } from "../lib/plugin-root.ts";
30
+ import {
31
+ defaultDiscoverRoots,
32
+ type DiscoveredPlugins,
33
+ discoverPlugins,
34
+ findPluginSkills,
35
+ invalidatePluginsCache,
36
+ type Plugin,
37
+ pluginResources,
38
+ } from "../lib/plugins.ts";
39
+ import { estimateSkillTokens } from "../lib/skill-scan.ts";
40
+ import { readSkillStates, setSkillOverride } from "../lib/skill-overrides.ts";
41
+ import { boundedDockHeight, safeThemeBold, safeThemeInverse, safeThemePaint, truncateLine } from "../lib/tui-render.ts";
42
+ import { readUsage, recordUsage } from "../lib/usage-tracker.ts";
43
+ import { fetchInstallCounts, readCachedCounts } from "./counts.ts";
44
+ import { installPlugin, uninstallPlugin } from "./install/install.ts";
45
+ import { setInstalledEnabled } from "./install/registry.ts";
46
+ import {
47
+ ensureOfficialRegistered,
48
+ isStale,
49
+ OFFICIAL_MARKETPLACE_NAME,
50
+ OFFICIAL_MARKETPLACE_SOURCE,
51
+ } from "./marketplace/official.ts";
52
+ import { parseMarketplaceInput } from "./marketplace/parse.ts";
53
+ import {
54
+ availableMarketplaceName,
55
+ readKnownMarketplaces,
56
+ removeKnownMarketplace,
57
+ writeKnownMarketplace,
58
+ } from "./marketplace/registry.ts";
59
+ import { installLocationFor, marketplaceCacheDir, type MarketplaceSnapshot, syncMarketplace } from "./marketplace/sync.ts";
60
+ import type { KnownMarketplace } from "./marketplace/types.ts";
61
+ import { decodePanelKey } from "./panel/keys.ts";
62
+ import { type DiscoverDetail, renderPanel, type PanelPaint } from "./panel/render.ts";
63
+ import { buildDiscoverRows, buildInstalledRows, buildMarketplaceRows, type DiscoverRow } from "./panel/rows.ts";
64
+ import { applyPanelKey, initialPanelState, type PanelEffect, type PanelView } from "./panel/state.ts";
26
65
  import { findShellPlaceholders, replaceShellPlaceholders, substituteArguments } from "./template.ts";
66
+ import { parseFrontmatterLoosely } from "../lib/frontmatter.ts";
67
+ import { sessionAlive } from "../lib/session-lifecycle.ts";
27
68
 
28
69
  const run = promisify(execFile);
29
70
  const SHELL_TIMEOUT_MS = 30_000;
30
71
 
31
- async function expandTemplate(body: string, args: string, cwd: string): Promise<string> {
72
+ // The panel is a bounded dock like Claude Code's /plugins, NOT a full-screen
73
+ // takeover: it swaps the editor container and leaves the transcript visible
74
+ // above it. Cap the height so the conversation stays on screen; smaller
75
+ // terminals still shrink to fit. ~6 discover rows show at this cap.
76
+ const PANEL_MAX_HEIGHT = 22;
77
+
78
+ async function expandTemplate(body: string, args: string, cwd: string, persist?: { dir: string; name: string }): Promise<string> {
32
79
  const withArgs = substituteArguments(body, args);
33
80
  const commands = [...new Set(findShellPlaceholders(withArgs))];
34
81
  if (commands.length === 0) return withArgs;
35
82
 
36
83
  const outputs = new Map<string, string>();
37
84
  await Promise.all(
38
- commands.map(async (command) => {
85
+ commands.map(async (command, index) => {
86
+ let text: string;
39
87
  try {
40
88
  const { stdout, stderr } = await run(command, {
41
89
  cwd,
@@ -43,40 +91,455 @@ async function expandTemplate(body: string, args: string, cwd: string): Promise<
43
91
  timeout: SHELL_TIMEOUT_MS,
44
92
  maxBuffer: 2 * 1024 * 1024,
45
93
  });
46
- outputs.set(command, (stdout || stderr || "").trim());
94
+ text = (stdout || stderr || "").trim();
47
95
  } catch (error) {
48
96
  const detail = error as { stdout?: string; stderr?: string; message?: string };
49
- outputs.set(command, (detail.stdout || detail.stderr || detail.message || "command failed").trim());
97
+ text = (detail.stdout || detail.stderr || detail.message || "command failed").trim();
50
98
  }
99
+ // A `!`command's output (a `git diff` of a large change) lands in the
100
+ // user turn bounded only by maxBuffer — persist it past the cap so the
101
+ // model gets a preview plus the path, never a wall of text (review L3).
102
+ outputs.set(command, persist ? persistIfLarge(text, { dir: persist.dir, id: `plugin-cmd-${persist.name}-${index}` }) : text);
51
103
  }),
52
104
  );
53
105
  return replaceShellPlaceholders(withArgs, outputs);
54
106
  }
55
107
 
56
- export default function pluginsExtension(pi: ExtensionAPI) {
57
- const claudeDir = join(os.homedir(), ".claude");
58
- const discovered = discoverPlugins(claudeDir);
108
+ /** An empty discovery result — the fallback when a scan throws. */
109
+ function emptyDiscovered(): DiscoveredPlugins {
110
+ return {
111
+ plugins: [],
112
+ enabledPlugins: [],
113
+ agentDirs: [],
114
+ skills: [],
115
+ commands: [],
116
+ mcpConfigs: [],
117
+ mcpConfigPlugins: new Map(),
118
+ byPlugin: new Map(),
119
+ };
120
+ }
59
121
 
60
- for (const command of discovered.commands) {
61
- const plugin = discovered.plugins.find((p) => p.name === command.plugin);
62
- if (plugin) registerPluginCommand(pi, plugin, command.name, command.path);
122
+ /**
123
+ * discoverPlugins can throw on a malformed installed plugin (a bad manifest, an
124
+ * unreadable file). This extension is the one that runs discovery at LOAD time,
125
+ * where a throw becomes "Failed to load extension" and aborts pi entirely — so
126
+ * it degrades to "no plugins" instead of taking the whole session down (review H2).
127
+ */
128
+ function safeDiscover(roots: ReturnType<typeof defaultDiscoverRoots>): DiscoveredPlugins {
129
+ try {
130
+ return discoverPlugins(roots);
131
+ } catch (error) {
132
+ console.error(`One Code: plugin discovery failed, continuing without plugins: ${(error as Error).message}`);
133
+ return emptyDiscovered();
63
134
  }
135
+ }
136
+
137
+ export default function pluginsExtension(pi: ExtensionAPI) {
138
+ // False once the session is replaced: a panel action still awaiting then
139
+ // throws on its first pi.* call, which is expected and not an error to show.
140
+ const alive = sessionAlive(pi);
141
+ const registeredCommands = new Set<string>();
142
+ // Load-time discovery has no ctx, so it runs on process.cwd(); session_start
143
+ // below re-discovers on the session's real cwd (they differ under `pi -C`
144
+ // and in RPC children) so project-scoped plugin settings are the right ones.
145
+ let discovered = safeDiscover(defaultDiscoverRoots(getAgentDir()));
146
+
147
+ const registerCommands = (plugins: DiscoveredPlugins) => {
148
+ for (const command of plugins.commands) {
149
+ if (registeredCommands.has(command.name)) continue;
150
+ // Two marketplaces may ship a plugin of the same name (they are keyed by
151
+ // `<name>@<marketplace>`); the command belongs to the ENABLED one.
152
+ const plugin =
153
+ plugins.plugins.find((p) => p.name === command.plugin && p.enabled) ??
154
+ plugins.plugins.find((p) => p.name === command.plugin);
155
+ if (!plugin) continue;
156
+ registeredCommands.add(command.name);
157
+ registerPluginCommand(pi, plugin, command.name, command.path);
158
+ }
159
+ };
160
+
161
+ pi.on("session_start", (_event, ctx) => {
162
+ if (ctx.cwd === process.cwd()) return;
163
+ invalidatePluginsCache();
164
+ discovered = safeDiscover(defaultDiscoverRoots(getAgentDir(), ctx.cwd));
165
+ registerCommands(discovered);
166
+ });
167
+ registerCommands(discovered);
168
+
169
+ // Latest MCP snapshot; the panel triggers a request on open (the bus does
170
+ // not replay, and this extension can't read the mcp extension's state).
171
+ let mcpStatus: McpStatusEvent | undefined;
172
+ let repaintPanel: (() => void) | undefined;
173
+ pi.events.on(MCP_STATUS_CHANNEL, (event) => {
174
+ mcpStatus = event as McpStatusEvent;
175
+ repaintPanel?.();
176
+ });
177
+
178
+ let panelOpen = false;
179
+
180
+ const openPanel = async (ctx: ExtensionContext): Promise<void> => {
181
+ if (panelOpen) return;
182
+ panelOpen = true;
183
+ let restartNotice = false;
184
+ try {
185
+ await ctx.ui.custom<null>((tui, theme, _keybindings, done) => {
186
+ const paint: PanelPaint = {
187
+ fg: safeThemePaint(theme),
188
+ bold: safeThemeBold(theme),
189
+ inverse: safeThemeInverse(theme),
190
+ };
191
+ const roots = defaultDiscoverRoots(getAgentDir(), ctx.cwd);
192
+ const oneCodeRoot = roots.oneCodeRoot;
193
+
194
+ const state = initialPanelState();
195
+ const snapshots = new Map<string, MarketplaceSnapshot>();
196
+ const loading = new Set<string>();
197
+ const actionErrors: string[] = [];
198
+ const busy = new Set<string>();
199
+ let counts = readCachedCounts(oneCodeRoot);
200
+ let known: Record<string, KnownMarketplace> = {};
201
+
202
+ // Two caches: rendered lines (per width, cleared on ANY repaint) and
203
+ // the view model (fs scans — cleared only when data actually changed,
204
+ // so a cursor move or ticker tick never re-reads the disk).
205
+ let cache: { width: number; lines: string[] } | undefined;
206
+ let viewCache: PanelView | undefined;
207
+ const repaint = () => {
208
+ cache = undefined;
209
+ tui.requestRender();
210
+ };
211
+ /** Data changed (effect ran, async work landed, search edited). */
212
+ const refresh = () => {
213
+ viewCache = undefined;
214
+ repaint();
215
+ };
216
+ repaintPanel = refresh;
217
+ // The ticker only animates in-flight async work; every real state
218
+ // change repaints explicitly.
219
+ const ticker = setInterval(() => {
220
+ if (loading.size > 0 || busy.size > 0) repaint();
221
+ }, 500);
222
+ ticker.unref?.();
223
+
224
+ const rediscover = () => {
225
+ invalidatePluginsCache();
226
+ discovered = discoverPlugins(roots);
227
+ registerCommands(discovered);
228
+ viewCache = undefined;
229
+ };
230
+
231
+ const track = async <T>(label: string, work: Promise<T>): Promise<T | undefined> => {
232
+ loading.add(label);
233
+ repaint();
234
+ try {
235
+ return await work;
236
+ } catch (error) {
237
+ actionErrors.push(`${label} failed: ${(error as Error).message}`);
238
+ return undefined;
239
+ } finally {
240
+ loading.delete(label);
241
+ refresh();
242
+ }
243
+ };
244
+
245
+ const syncOne = async (name: string, entry: KnownMarketplace, refreshContent: boolean) => {
246
+ const snapshot = await track(
247
+ `${refreshContent ? "updating" : "loading"} ${name}…`,
248
+ syncMarketplace(oneCodeRoot, name, entry, { refresh: refreshContent }),
249
+ );
250
+ if (snapshot) {
251
+ snapshots.set(name, snapshot);
252
+ known = { ...known, [name]: snapshot.known };
253
+ }
254
+ };
255
+
256
+ // Lazy startup work — on open only, never at session start: register
257
+ // the official marketplace, sync every known one (refresh official
258
+ // when stale), fetch install counts, ask mcp for a status snapshot.
259
+ const openTasks = async () => {
260
+ ensureOfficialRegistered(
261
+ oneCodeRoot,
262
+ installLocationFor(oneCodeRoot, OFFICIAL_MARKETPLACE_NAME, OFFICIAL_MARKETPLACE_SOURCE),
263
+ );
264
+ known = readKnownMarketplaces(oneCodeRoot);
265
+ // The counts fetch is independent of the marketplace syncs.
266
+ await Promise.all([
267
+ ...Object.entries(known).map(([name, entry]) =>
268
+ syncOne(name, entry, name === OFFICIAL_MARKETPLACE_NAME && isStale(entry)),
269
+ ),
270
+ track("fetching install counts…", fetchInstallCounts(oneCodeRoot)).then((fetched) => {
271
+ if (fetched) counts = fetched;
272
+ }),
273
+ ]);
274
+ };
275
+ // void'ed panel work: a late throw (a pi.* call on a replaced session)
276
+ // must never surface as an unhandled rejection (LIFECYCLE-REVIEW L2);
277
+ // any other failure is shown in the panel's error list.
278
+ const guarded = (work: Promise<unknown>) =>
279
+ work.catch((error) => {
280
+ if (!alive()) return;
281
+ actionErrors.push((error as Error).message);
282
+ state.notice = (error as Error).message;
283
+ refresh();
284
+ });
285
+ void guarded(openTasks());
286
+ pi.events.emit(MCP_STATUS_REQUEST_CHANNEL, {});
287
+
288
+ const buildView = (): PanelView => {
289
+ const installedIds = new Set(discovered.plugins.map((p) => p.id));
290
+ const usage = readUsage(oneCodeRoot);
291
+ const favorites = readFavorites(oneCodeRoot);
292
+ const skillOverrides = readSkillStates(oneCodeRoot);
293
+ // /plugins manages plugin skills only; project/user skills (with their
294
+ // on/name-only/user-only/off nuance) belong to the /skills panel. Build
295
+ // the rows straight from the discovered plugin skills — no project/user
296
+ // dir scan whose results would only be filtered back out.
297
+ const skills = discovered.enabledPlugins
298
+ .flatMap((plugin) => {
299
+ const dir = pluginResources(plugin).skillsDir;
300
+ return dir ? findPluginSkills(plugin, dir) : [];
301
+ })
302
+ .map((skill) => ({
303
+ name: skill.name,
304
+ path: skill.path,
305
+ scope: "plugin" as const,
306
+ tokens: estimateSkillTokens(skill.path),
307
+ }));
308
+ return {
309
+ discover: buildDiscoverRows({
310
+ snapshots: [...snapshots.values()],
311
+ installedIds,
312
+ counts,
313
+ busy,
314
+ search: state.search.discover,
315
+ }),
316
+ installed: buildInstalledRows({
317
+ plugins: discovered.plugins,
318
+ mcpServers: mcpStatus?.servers,
319
+ skills,
320
+ usage,
321
+ skillOverrides,
322
+ favorites,
323
+ busy,
324
+ search: state.search.installed,
325
+ }),
326
+ marketplaces: buildMarketplaceRows({ known, snapshots, installedIds }),
327
+ errors: [...[...snapshots.values()].flatMap((s) => s.errors), ...actionErrors],
328
+ };
329
+ };
330
+
331
+ const currentView = (): PanelView => (viewCache ??= buildView());
332
+
333
+ const discoverDetails = (): Map<string, DiscoverDetail> => {
334
+ const details = new Map<string, DiscoverDetail>();
335
+ for (const snapshot of snapshots.values()) {
336
+ if (!snapshot.manifest) continue;
337
+ for (const entry of snapshot.manifest.plugins) {
338
+ const contributions: string[] = [];
339
+ const provides: Array<[string, string]> = [
340
+ ["commands", "commands"],
341
+ ["agents", "agents"],
342
+ ["skills", "skills"],
343
+ ["mcpServers", "MCP servers"],
344
+ ["lspServers", "LSP servers"],
345
+ ["hooks", "hooks"],
346
+ ];
347
+ for (const [key, label] of provides) if (entry.raw[key] !== undefined) contributions.push(label);
348
+ if (typeof entry.source === "string") contributions.push(`files: ${entry.source}`);
349
+ details.set(`${entry.name}@${snapshot.name}`, {
350
+ version: entry.version,
351
+ author: entry.author?.name,
352
+ category: entry.category,
353
+ contributions,
354
+ });
355
+ }
356
+ }
357
+ return details;
358
+ };
359
+
360
+ const close = () => {
361
+ clearInterval(ticker);
362
+ repaintPanel = undefined;
363
+ restartNotice = state.restartNeeded;
364
+ done(null);
365
+ };
366
+
367
+ const installToggle = async (row: DiscoverRow) => {
368
+ const plugin = discovered.plugins.find((p) => p.id === row.id);
369
+ if (row.installed && plugin?.originRoot === "claude") {
370
+ state.notice = `${row.name} was installed by Claude Code — uninstall it there (files are read-only here).`;
371
+ return;
372
+ }
373
+ busy.add(row.id);
374
+ refresh();
375
+ try {
376
+ if (row.installed) {
377
+ uninstallPlugin(oneCodeRoot, row.id);
378
+ } else {
379
+ const snapshot = snapshots.get(row.marketplace);
380
+ const entry = snapshot?.manifest?.plugins.find((p) => p.name === row.name);
381
+ if (!snapshot || !entry) throw new Error(`${row.marketplace} has no entry named ${row.name}`);
382
+ await installPlugin(oneCodeRoot, row.marketplace, entry, snapshot.contentRoot);
383
+ }
384
+ state.restartNeeded = true;
385
+ rediscover();
386
+ } catch (error) {
387
+ actionErrors.push((error as Error).message);
388
+ state.notice = (error as Error).message;
389
+ } finally {
390
+ busy.delete(row.id);
391
+ refresh();
392
+ }
393
+ };
394
+
395
+ const runEffect = (effect: PanelEffect) => {
396
+ switch (effect.kind) {
397
+ case "close":
398
+ return close();
399
+ case "installToggle":
400
+ void guarded(installToggle(effect.row));
401
+ return;
402
+ case "setPluginEnabled": {
403
+ if (effect.origin === "one-code") setInstalledEnabled(oneCodeRoot, effect.id, effect.enabled);
404
+ else setOverride(oneCodeRoot, effect.id, effect.enabled);
405
+ state.restartNeeded = true;
406
+ rediscover();
407
+ return;
408
+ }
409
+ case "uninstall": {
410
+ uninstallPlugin(oneCodeRoot, effect.id);
411
+ state.detail = undefined;
412
+ state.restartNeeded = true;
413
+ rediscover();
414
+ return;
415
+ }
416
+ case "setSkillEnabled":
417
+ setSkillOverride(oneCodeRoot, effect.overrideKey, effect.enabled);
418
+ rediscover();
419
+ return;
420
+ case "toggleFavorite":
421
+ toggleFavorite(oneCodeRoot, effect.target === "plugin" ? "plugin" : "skill", effect.key);
422
+ return;
423
+ case "addMarketplace": {
424
+ const parsed = parseMarketplaceInput(effect.input, ctx.cwd);
425
+ if ("error" in parsed) {
426
+ state.notice = parsed.error;
427
+ return;
428
+ }
429
+ const name = availableMarketplaceName(oneCodeRoot, parsed.suggestedName);
430
+ const entry: KnownMarketplace = {
431
+ source: parsed.source,
432
+ installLocation: installLocationFor(oneCodeRoot, name, parsed.source),
433
+ lastUpdated: new Date(0).toISOString(),
434
+ };
435
+ writeKnownMarketplace(oneCodeRoot, name, entry);
436
+ known = { ...known, [name]: entry };
437
+ void guarded(syncOne(name, entry, false));
438
+ state.tab = "marketplaces";
439
+ return;
440
+ }
441
+ case "removeMarketplace": {
442
+ if (effect.name === OFFICIAL_MARKETPLACE_NAME) {
443
+ state.notice = "The official marketplace re-registers on next open; remove individual plugins instead.";
444
+ }
445
+ removeKnownMarketplace(oneCodeRoot, effect.name);
446
+ snapshots.delete(effect.name);
447
+ // Cached content lives under OUR marketplaces dir only — a
448
+ // file/directory source's user-owned path is never touched, and
449
+ // a name that would escape the cache dir deletes nothing.
450
+ const cacheBase = marketplaceCacheDir(oneCodeRoot);
451
+ if (pathWithinBase(cacheBase, effect.name)) {
452
+ rmSync(join(cacheBase, effect.name), { recursive: true, force: true });
453
+ rmSync(join(cacheBase, `${effect.name}.json`), { force: true });
454
+ }
455
+ const { [effect.name]: _removed, ...rest } = known;
456
+ known = rest;
457
+ return;
458
+ }
459
+ case "refreshMarketplace": {
460
+ const entry = known[effect.name];
461
+ if (entry) void guarded(syncOne(effect.name, entry, true));
462
+ return;
463
+ }
464
+ }
465
+ };
466
+
467
+ return {
468
+ render: (width: number) => {
469
+ if (cache?.width === width) return cache.lines;
470
+ const rows = (tui as { terminal: { rows: number } }).terminal.rows;
471
+ const height = boundedDockHeight(rows, PANEL_MAX_HEIGHT);
472
+ const lines = renderPanel(
473
+ {
474
+ state,
475
+ view: currentView(),
476
+ width,
477
+ height,
478
+ loading: [...loading],
479
+ discoverDetails: discoverDetails(),
480
+ restartNeeded: state.restartNeeded,
481
+ },
482
+ paint,
483
+ ).map((line) => truncateLine(line, width));
484
+ cache = { width, lines };
485
+ return lines;
486
+ },
487
+ handleInput: (data: string) => {
488
+ const key = decodePanelKey(data);
489
+ if (!key) return;
490
+ const effect = applyPanelKey(state, key, currentView());
491
+ // Search edits change which rows exist; effects change the data
492
+ // underneath. Pure navigation keeps the cached view.
493
+ if (effect || key.kind === "text" || key.kind === "backspace" || key.kind === "back") {
494
+ viewCache = undefined;
495
+ }
496
+ if (effect) runEffect(effect);
497
+ repaint();
498
+ },
499
+ invalidate: () => {
500
+ cache = undefined;
501
+ },
502
+ // Covers dismissal paths that aren't esc (session end, another
503
+ // overlay) — without this the ticker outlives the component.
504
+ dispose: () => {
505
+ clearInterval(ticker);
506
+ repaintPanel = undefined;
507
+ },
508
+ };
509
+ });
510
+ } finally {
511
+ panelOpen = false;
512
+ }
513
+ if (restartNotice && ctx.hasUI) {
514
+ ctx.ui.notify(
515
+ "Plugin changes saved. Commands and the /plugins list update immediately; MCP servers, agents, and hooks apply on the next session restart.",
516
+ "info",
517
+ );
518
+ }
519
+ };
64
520
 
65
521
  pi.registerCommand("plugins", {
66
- description: "List installed Claude Code plugins and what they contribute",
522
+ description: "Browse, install, and manage Claude Code-compatible plugins and marketplaces",
67
523
  handler: async (_args, ctx) => {
524
+ if (ctx.hasUI) {
525
+ await openPanel(ctx);
526
+ return;
527
+ }
528
+ // Non-interactive fallback: the old text listing.
68
529
  if (discovered.plugins.length === 0) {
69
- ctx.ui.notify("No Claude Code plugins installed (~/.claude/plugins/installed_plugins.json).", "info");
530
+ ctx.ui.notify("No plugins installed.", "info");
70
531
  return;
71
532
  }
72
533
  const lines = discovered.plugins.map((plugin) => {
73
- const summary = discovered.byPlugin.get(plugin.name);
534
+ const summary = discovered.byPlugin.get(plugin.id);
74
535
  const parts: string[] = [];
75
536
  if (summary?.agents) parts.push("agents");
76
537
  if (summary?.skills) parts.push(`${summary.skills} skill${summary.skills === 1 ? "" : "s"}`);
77
538
  if (summary?.commands) parts.push(`${summary.commands} command${summary.commands === 1 ? "" : "s"}`);
78
539
  if (summary?.mcp) parts.push("mcp");
79
- return `${plugin.name}${plugin.version && plugin.version !== "unknown" ? ` ${plugin.version}` : ""} — ${parts.join(", ") || "nothing usable"}`;
540
+ if (summary?.lsp) parts.push("lsp");
541
+ const state = plugin.enabled ? "" : " (disabled)";
542
+ return `${plugin.name}${plugin.version && plugin.version !== "unknown" ? ` ${plugin.version}` : ""}${state} — ${parts.join(", ") || "nothing usable"}`;
80
543
  });
81
544
  ctx.ui.notify(lines.join("\n"), "info");
82
545
  },
@@ -87,7 +550,7 @@ function registerPluginCommand(pi: ExtensionAPI, plugin: Plugin, name: string, p
87
550
  let description = `Command from the ${plugin.name} plugin`;
88
551
  let argumentHint: string | undefined;
89
552
  try {
90
- const { frontmatter } = parseFrontmatter(readFileSync(path, "utf-8")) as {
553
+ const { frontmatter } = parseFrontmatterLoosely(readFileSync(path, "utf-8")) as {
91
554
  frontmatter?: Record<string, unknown>;
92
555
  };
93
556
  if (typeof frontmatter?.description === "string") description = frontmatter.description;
@@ -101,13 +564,14 @@ function registerPluginCommand(pi: ExtensionAPI, plugin: Plugin, name: string, p
101
564
  handler: async (args, ctx) => {
102
565
  let body: string;
103
566
  try {
104
- const parsed = parseFrontmatter(readFileSync(path, "utf-8")) as { body: string };
567
+ const parsed = parseFrontmatterLoosely(readFileSync(path, "utf-8")) as { body: string };
105
568
  body = parsed.body;
106
569
  } catch (error) {
107
570
  ctx.ui.notify(`Could not read ${path}: ${(error as Error).message}`, "error");
108
571
  return;
109
572
  }
110
- const expanded = await expandTemplate(body, args, ctx.cwd);
573
+ const expanded = await expandTemplate(body, args, ctx.cwd, { dir: sessionResultsDir(ctx), name });
574
+ recordUsage(pluginRoot(getAgentDir()), "command", name);
111
575
  // Deliver as a user turn, which is how Claude Code runs a command template.
112
576
  pi.sendUserMessage(expanded);
113
577
  },