one-code-extension 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -202
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- 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
|
-
*
|
|
5
|
-
* plugin's resources so the other extensions can use them:
|
|
4
|
+
* Two jobs:
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
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 {
|
|
24
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
25
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
94
|
+
text = (stdout || stderr || "").trim();
|
|
47
95
|
} catch (error) {
|
|
48
96
|
const detail = error as { stdout?: string; stderr?: string; message?: string };
|
|
49
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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: "
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 } =
|
|
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 =
|
|
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
|
},
|