@yagni-app/code-staging 1.0.6-staging.1255.1 → 1.0.6-staging.1258.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.
- package/dist/claudeCompat.d.ts +59 -0
- package/dist/claudeCompat.js +109 -2
- package/dist/claudePlugins.d.ts +45 -5
- package/dist/claudePlugins.js +129 -21
- package/dist/cli.js +12 -0
- package/dist/doctor.js +1 -1
- package/dist/extension/index.d.ts +3 -0
- package/dist/extension/index.js +4 -0
- package/dist/extension/mcp/cliConfig.d.ts +1 -1
- package/dist/extension/mcp/cliConfig.js +1 -1
- package/dist/extension/mcp/config.d.ts +24 -2
- package/dist/extension/mcp/config.js +75 -3
- package/dist/extension/mcp/manager.d.ts +3 -1
- package/dist/extension/mcp/manager.js +2 -2
- package/dist/extension/mcp/panel.d.ts +0 -1
- package/dist/extension/mcp/panel.js +13 -3
- package/dist/extension/mcp/startup.js +8 -6
- package/dist/extension/plugins/inventory.d.ts +88 -0
- package/dist/extension/plugins/inventory.js +144 -0
- package/dist/extension/plugins/panel.d.ts +45 -0
- package/dist/extension/plugins/panel.js +293 -0
- package/dist/launch.js +4 -0
- package/dist/mcpCommand.d.ts +10 -1
- package/dist/mcpCommand.js +42 -10
- package/dist/paths.d.ts +6 -0
- package/dist/paths.js +8 -0
- package/dist/pluginCommand.d.ts +43 -0
- package/dist/pluginCommand.js +499 -0
- package/dist/pluginStore.d.ts +170 -0
- package/dist/pluginStore.js +554 -0
- package/package.json +2 -2
package/dist/claudeCompat.d.ts
CHANGED
|
@@ -34,6 +34,30 @@ export declare const CLAUDE_COMPAT_DISABLE_ENV = "YAGNI_DISABLE_CLAUDE_COMPAT";
|
|
|
34
34
|
export declare const CLAUDE_AGENT_DIRS_ENV = "YAGNI_CLAUDE_AGENT_DIRS";
|
|
35
35
|
/** `.claude/rules` dirs for the extension's rules injection (delimiter-joined). */
|
|
36
36
|
export declare const CLAUDE_RULES_DIRS_ENV = "YAGNI_CLAUDE_RULES_DIRS";
|
|
37
|
+
/**
|
|
38
|
+
* Plugin MCP servers for the extension's MCP client: a JSON array of
|
|
39
|
+
* `{ plugin, sourcePath, gated, servers }`. `${CLAUDE_PLUGIN_ROOT}` is already
|
|
40
|
+
* expanded to the plugin's install dir; other `${VAR}` references are left for
|
|
41
|
+
* the extension's normal env expansion. `gated` marks servers that came from
|
|
42
|
+
* repo config (repo marketplace, project-scope `enabledPlugins`) and must pass
|
|
43
|
+
* the same approval the repo's `.mcp.json` would.
|
|
44
|
+
*/
|
|
45
|
+
export declare const CLAUDE_PLUGIN_MCP_ENV = "YAGNI_CLAUDE_PLUGIN_MCP";
|
|
46
|
+
/**
|
|
47
|
+
* `name@marketplace` keys of the plugins wired at launch (delimiter-joined).
|
|
48
|
+
* `/reload-plugins` diffs a fresh inventory against this baseline: pi already
|
|
49
|
+
* holds the launch-time plugins through argv, so only newer ones need the
|
|
50
|
+
* extension's `resources_discover` contribution.
|
|
51
|
+
*/
|
|
52
|
+
export declare const CLAUDE_PLUGIN_KEYS_ENV = "YAGNI_CLAUDE_PLUGIN_KEYS";
|
|
53
|
+
export interface PluginMcpEnvEntry {
|
|
54
|
+
plugin: string;
|
|
55
|
+
sourcePath: string;
|
|
56
|
+
gated: boolean;
|
|
57
|
+
servers: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
/** The MCP payload entries for a set of plugins (those without servers are skipped). */
|
|
60
|
+
export declare function pluginMcpEntries(plugins: PluginAssets[], gated: boolean): PluginMcpEnvEntry[];
|
|
37
61
|
export interface ClaudeAssetDirs {
|
|
38
62
|
skills: string | null;
|
|
39
63
|
commands: string | null;
|
|
@@ -61,6 +85,9 @@ export interface ClaudeCompatDecision {
|
|
|
61
85
|
/** `.claude/rules` dirs, user first so project rules win in the prompt. */
|
|
62
86
|
userRulesDirs: string[];
|
|
63
87
|
projectRulesDirs: string[];
|
|
88
|
+
/** Plugin MCP servers, split by the same trust posture (project ones are approval-gated too). */
|
|
89
|
+
userMcp: PluginMcpEnvEntry[];
|
|
90
|
+
projectMcp: PluginMcpEnvEntry[];
|
|
64
91
|
/** Ask the user for a trust decision before wiring any project content. */
|
|
65
92
|
needsPrompt: boolean;
|
|
66
93
|
}
|
|
@@ -104,6 +131,38 @@ export interface ClaudeCompatLaunch {
|
|
|
104
131
|
* compat can only add flags/env, never break a launch.
|
|
105
132
|
*/
|
|
106
133
|
export declare function claudeCompatArgs(deps: ClaudeCompatDeps): Promise<ClaudeCompatLaunch>;
|
|
134
|
+
/** One plugin as the in-session `/plugin` panel sees it. */
|
|
135
|
+
export interface InventoryPlugin {
|
|
136
|
+
key: string;
|
|
137
|
+
name: string;
|
|
138
|
+
origin: PluginAssets["origin"];
|
|
139
|
+
version: string | null;
|
|
140
|
+
root: string;
|
|
141
|
+
/** Repo-sourced: its MCP servers ride the project approval gate. */
|
|
142
|
+
gated: boolean;
|
|
143
|
+
skillPaths: string[];
|
|
144
|
+
commandPaths: string[];
|
|
145
|
+
agentDirs: string[];
|
|
146
|
+
mcpServers: string[];
|
|
147
|
+
unsupported: string[];
|
|
148
|
+
}
|
|
149
|
+
export interface PluginInventory {
|
|
150
|
+
/** Plugins that load for `cwd` right now (trust respected, never prompting). */
|
|
151
|
+
plugins: InventoryPlugin[];
|
|
152
|
+
/** The MCP payload those plugins contribute (same shape as YAGNI_CLAUDE_PLUGIN_MCP). */
|
|
153
|
+
mcp: PluginMcpEnvEntry[];
|
|
154
|
+
/** Repo-sourced plugins held back because the folder is not trusted yet. */
|
|
155
|
+
pendingTrust: string[];
|
|
156
|
+
/** Plugin agent dirs, user first (same order as YAGNI_CLAUDE_AGENT_DIRS). */
|
|
157
|
+
agentDirs: string[];
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The plugin inventory a launch from `cwd` would wire, computed without
|
|
161
|
+
* prompting: the user's own plugins always, repo-sourced ones only once the
|
|
162
|
+
* folder is trusted. Backs `yagni plugin inventory --json`, which the
|
|
163
|
+
* in-session `/plugin` panel and `/reload-plugins` call.
|
|
164
|
+
*/
|
|
165
|
+
export declare function pluginInventory(deps: Omit<ClaudeCompatDeps, "confirm" | "interactive">): PluginInventory;
|
|
107
166
|
/** Whether pi's trust file exists yet (used only for messaging). */
|
|
108
167
|
export declare function trustFileExists(agentDir: string): boolean;
|
|
109
168
|
//# sourceMappingURL=claudeCompat.d.ts.map
|
package/dist/claudeCompat.js
CHANGED
|
@@ -32,11 +32,57 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmdirSyn
|
|
|
32
32
|
import { homedir } from "node:os";
|
|
33
33
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
34
34
|
import { discoverClaudePlugins } from "./claudePlugins.js";
|
|
35
|
+
import { pluginsHome, readLedger } from "./pluginStore.js";
|
|
35
36
|
export const CLAUDE_COMPAT_DISABLE_ENV = "YAGNI_DISABLE_CLAUDE_COMPAT";
|
|
36
37
|
/** Plugin `agents/` dirs for the extension's subagent discovery (delimiter-joined). */
|
|
37
38
|
export const CLAUDE_AGENT_DIRS_ENV = "YAGNI_CLAUDE_AGENT_DIRS";
|
|
38
39
|
/** `.claude/rules` dirs for the extension's rules injection (delimiter-joined). */
|
|
39
40
|
export const CLAUDE_RULES_DIRS_ENV = "YAGNI_CLAUDE_RULES_DIRS";
|
|
41
|
+
/**
|
|
42
|
+
* Plugin MCP servers for the extension's MCP client: a JSON array of
|
|
43
|
+
* `{ plugin, sourcePath, gated, servers }`. `${CLAUDE_PLUGIN_ROOT}` is already
|
|
44
|
+
* expanded to the plugin's install dir; other `${VAR}` references are left for
|
|
45
|
+
* the extension's normal env expansion. `gated` marks servers that came from
|
|
46
|
+
* repo config (repo marketplace, project-scope `enabledPlugins`) and must pass
|
|
47
|
+
* the same approval the repo's `.mcp.json` would.
|
|
48
|
+
*/
|
|
49
|
+
export const CLAUDE_PLUGIN_MCP_ENV = "YAGNI_CLAUDE_PLUGIN_MCP";
|
|
50
|
+
/**
|
|
51
|
+
* `name@marketplace` keys of the plugins wired at launch (delimiter-joined).
|
|
52
|
+
* `/reload-plugins` diffs a fresh inventory against this baseline: pi already
|
|
53
|
+
* holds the launch-time plugins through argv, so only newer ones need the
|
|
54
|
+
* extension's `resources_discover` contribution.
|
|
55
|
+
*/
|
|
56
|
+
export const CLAUDE_PLUGIN_KEYS_ENV = "YAGNI_CLAUDE_PLUGIN_KEYS";
|
|
57
|
+
const PLUGIN_ROOT_VAR = "${CLAUDE_PLUGIN_ROOT}";
|
|
58
|
+
function expandPluginRoot(value, root) {
|
|
59
|
+
if (typeof value === "string")
|
|
60
|
+
return value.split(PLUGIN_ROOT_VAR).join(root);
|
|
61
|
+
if (Array.isArray(value))
|
|
62
|
+
return value.map((v) => expandPluginRoot(v, root));
|
|
63
|
+
if (value && typeof value === "object") {
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const [k, v] of Object.entries(value))
|
|
66
|
+
out[k] = expandPluginRoot(v, root);
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
/** The MCP payload entries for a set of plugins (those without servers are skipped). */
|
|
72
|
+
export function pluginMcpEntries(plugins, gated) {
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const plugin of plugins) {
|
|
75
|
+
if (!plugin.mcp)
|
|
76
|
+
continue;
|
|
77
|
+
out.push({
|
|
78
|
+
plugin: plugin.key,
|
|
79
|
+
sourcePath: plugin.mcp.sourcePath,
|
|
80
|
+
gated,
|
|
81
|
+
servers: expandPluginRoot(plugin.mcp.servers, plugin.root),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
40
86
|
function dirsToArgv(dirs) {
|
|
41
87
|
const argv = [];
|
|
42
88
|
if (dirs.skills)
|
|
@@ -60,7 +106,8 @@ export function decideClaudeCompat(probe) {
|
|
|
60
106
|
const projectArgv = [...dirsToArgv(probe.project), ...pluginsToArgv(probe.projectPlugins)];
|
|
61
107
|
const projectAgentDirs = probe.projectPlugins.flatMap((p) => p.agentDirs);
|
|
62
108
|
const projectRulesDirs = probe.project.rules ? [probe.project.rules] : [];
|
|
63
|
-
const
|
|
109
|
+
const projectMcp = pluginMcpEntries(probe.projectPlugins, true);
|
|
110
|
+
const hasProjectContent = projectArgv.length > 0 || projectAgentDirs.length > 0 || projectRulesDirs.length > 0 || projectMcp.length > 0;
|
|
64
111
|
return {
|
|
65
112
|
userArgv: [...dirsToArgv(probe.user), ...pluginsToArgv(probe.userPlugins)],
|
|
66
113
|
projectArgv,
|
|
@@ -68,6 +115,8 @@ export function decideClaudeCompat(probe) {
|
|
|
68
115
|
projectAgentDirs,
|
|
69
116
|
userRulesDirs: probe.user.rules ? [probe.user.rules] : [],
|
|
70
117
|
projectRulesDirs,
|
|
118
|
+
userMcp: pluginMcpEntries(probe.userPlugins, false),
|
|
119
|
+
projectMcp,
|
|
71
120
|
needsPrompt: probe.interactive && hasProjectContent && probe.projectTrust === null,
|
|
72
121
|
};
|
|
73
122
|
}
|
|
@@ -210,7 +259,11 @@ export async function claudeCompatArgs(deps) {
|
|
|
210
259
|
const projectTrust = readTrustDecision(deps.agentDir, deps.cwd);
|
|
211
260
|
let plugins;
|
|
212
261
|
try {
|
|
213
|
-
plugins = discoverClaudePlugins({
|
|
262
|
+
plugins = discoverClaudePlugins({
|
|
263
|
+
cwd: deps.cwd,
|
|
264
|
+
homeDir: home,
|
|
265
|
+
yagniLedger: readLedger(pluginsHome(env, home)),
|
|
266
|
+
});
|
|
214
267
|
}
|
|
215
268
|
catch {
|
|
216
269
|
plugins = { user: [], project: [] };
|
|
@@ -246,13 +299,67 @@ export async function claudeCompatArgs(deps) {
|
|
|
246
299
|
...decision.userRulesDirs,
|
|
247
300
|
...(trusted === true ? decision.projectRulesDirs : []),
|
|
248
301
|
];
|
|
302
|
+
const mcpEntries = [
|
|
303
|
+
...decision.userMcp,
|
|
304
|
+
...(trusted === true ? decision.projectMcp : []),
|
|
305
|
+
];
|
|
306
|
+
const loadedKeys = [
|
|
307
|
+
...plugins.user.map((p) => p.key),
|
|
308
|
+
...(trusted === true ? plugins.project.map((p) => p.key) : []),
|
|
309
|
+
];
|
|
249
310
|
const extraEnv = {};
|
|
250
311
|
if (agentDirs.length > 0)
|
|
251
312
|
extraEnv[CLAUDE_AGENT_DIRS_ENV] = agentDirs.join(delimiter);
|
|
252
313
|
if (rulesDirs.length > 0)
|
|
253
314
|
extraEnv[CLAUDE_RULES_DIRS_ENV] = rulesDirs.join(delimiter);
|
|
315
|
+
if (mcpEntries.length > 0)
|
|
316
|
+
extraEnv[CLAUDE_PLUGIN_MCP_ENV] = JSON.stringify(mcpEntries);
|
|
317
|
+
if (loadedKeys.length > 0)
|
|
318
|
+
extraEnv[CLAUDE_PLUGIN_KEYS_ENV] = loadedKeys.join(delimiter);
|
|
254
319
|
return { argv, env: extraEnv };
|
|
255
320
|
}
|
|
321
|
+
function toInventory(plugin, gated) {
|
|
322
|
+
return {
|
|
323
|
+
key: plugin.key,
|
|
324
|
+
name: plugin.name,
|
|
325
|
+
origin: plugin.origin,
|
|
326
|
+
version: plugin.version,
|
|
327
|
+
root: plugin.root,
|
|
328
|
+
gated,
|
|
329
|
+
skillPaths: plugin.skillPaths,
|
|
330
|
+
commandPaths: plugin.commandPaths,
|
|
331
|
+
agentDirs: plugin.agentDirs,
|
|
332
|
+
mcpServers: plugin.mcp ? Object.keys(plugin.mcp.servers) : [],
|
|
333
|
+
unsupported: plugin.unsupported,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* The plugin inventory a launch from `cwd` would wire, computed without
|
|
338
|
+
* prompting: the user's own plugins always, repo-sourced ones only once the
|
|
339
|
+
* folder is trusted. Backs `yagni plugin inventory --json`, which the
|
|
340
|
+
* in-session `/plugin` panel and `/reload-plugins` call.
|
|
341
|
+
*/
|
|
342
|
+
export function pluginInventory(deps) {
|
|
343
|
+
const env = deps.env ?? process.env;
|
|
344
|
+
if (compatDisabled(env))
|
|
345
|
+
return { plugins: [], mcp: [], pendingTrust: [], agentDirs: [] };
|
|
346
|
+
const home = deps.homeDir ?? homedir();
|
|
347
|
+
const trusted = readTrustDecision(deps.agentDir, deps.cwd);
|
|
348
|
+
let plugins;
|
|
349
|
+
try {
|
|
350
|
+
plugins = discoverClaudePlugins({ cwd: deps.cwd, homeDir: home, yagniLedger: readLedger(pluginsHome(env, home)) });
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
plugins = { user: [], project: [] };
|
|
354
|
+
}
|
|
355
|
+
const project = trusted === true ? plugins.project : [];
|
|
356
|
+
return {
|
|
357
|
+
plugins: [...plugins.user.map((p) => toInventory(p, false)), ...project.map((p) => toInventory(p, true))],
|
|
358
|
+
mcp: [...pluginMcpEntries(plugins.user, false), ...pluginMcpEntries(project, true)],
|
|
359
|
+
pendingTrust: trusted === true ? [] : plugins.project.map((p) => p.key),
|
|
360
|
+
agentDirs: [...plugins.user, ...project].flatMap((p) => p.agentDirs),
|
|
361
|
+
};
|
|
362
|
+
}
|
|
256
363
|
/** Whether pi's trust file exists yet (used only for messaging). */
|
|
257
364
|
export function trustFileExists(agentDir) {
|
|
258
365
|
return existsSync(trustPath(agentDir));
|
package/dist/claudePlugins.d.ts
CHANGED
|
@@ -17,10 +17,19 @@
|
|
|
17
17
|
* relative-path source loads on the project side, unless explicitly
|
|
18
18
|
* disabled via `enabledPlugins`.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* Three sources, first wins on the same `name@marketplace`:
|
|
21
|
+
*
|
|
22
|
+
* 0. YAGNI's own ledger (`~/.yagni-code/plugins`, written by `yagni plugin`;
|
|
23
|
+
* see pluginStore.ts) — handed in by the caller as `yagniLedger`.
|
|
24
|
+
* 1. Claude Code's ledger + `enabledPlugins` (mirrored read-only).
|
|
25
|
+
* 2. The repo's own `.claude-plugin/marketplace.json`.
|
|
26
|
+
*
|
|
27
|
+
* Plugin MCP servers (`.mcp.json` at the plugin root, or the manifest's
|
|
28
|
+
* `mcpServers`) are collected here and bridged by the launcher into the
|
|
29
|
+
* extension's MCP client (see claudeCompat.ts). Deliberately NOT here:
|
|
30
|
+
* network installation (that is pluginStore.ts), plugin hooks, LSP servers,
|
|
31
|
+
* themes, output styles — those are reported as `unsupported` so `yagni
|
|
32
|
+
* plugin list` can say so. Discovery is read-only.
|
|
24
33
|
*
|
|
25
34
|
* Everything is fail-soft: malformed JSON, missing dirs, or hostile path
|
|
26
35
|
* entries degrade to "that plugin absent" — never a failed launch. Path
|
|
@@ -28,9 +37,21 @@
|
|
|
28
37
|
* inside the expected root) so a malicious `source` or component override
|
|
29
38
|
* cannot reach outside its repo/plugin.
|
|
30
39
|
*/
|
|
40
|
+
import type { PluginLedger } from "./pluginStore.js";
|
|
41
|
+
export type PluginOrigin = "yagni" | "claude-code" | "repo-marketplace";
|
|
42
|
+
export interface PluginMcp {
|
|
43
|
+
/** File the servers came from (plugin `.mcp.json` or `plugin.json`). */
|
|
44
|
+
sourcePath: string;
|
|
45
|
+
/** Raw `mcpServers` map; validated by the extension, `${CLAUDE_PLUGIN_ROOT}` expanded by the launcher. */
|
|
46
|
+
servers: Record<string, unknown>;
|
|
47
|
+
}
|
|
31
48
|
export interface PluginAssets {
|
|
32
49
|
/** The plugin's name (marketplace-entry name — the `enabledPlugins` key half). */
|
|
33
50
|
name: string;
|
|
51
|
+
/** `name@marketplace` when the marketplace is known; else the bare name. */
|
|
52
|
+
key: string;
|
|
53
|
+
origin: PluginOrigin;
|
|
54
|
+
version: string | null;
|
|
34
55
|
/** Absolute plugin root directory. */
|
|
35
56
|
root: string;
|
|
36
57
|
/** Dirs (or a lone root SKILL.md file) for pi `--skill`. */
|
|
@@ -39,6 +60,14 @@ export interface PluginAssets {
|
|
|
39
60
|
commandPaths: string[];
|
|
40
61
|
/** Dirs of Claude Code-format agent markdown for subagent discovery. */
|
|
41
62
|
agentDirs: string[];
|
|
63
|
+
mcp: PluginMcp | null;
|
|
64
|
+
/** Component kinds present in the plugin that YAGNI Code does not bridge. */
|
|
65
|
+
unsupported: string[];
|
|
66
|
+
}
|
|
67
|
+
export interface PluginMeta {
|
|
68
|
+
key?: string;
|
|
69
|
+
origin?: PluginOrigin;
|
|
70
|
+
version?: string | null;
|
|
42
71
|
}
|
|
43
72
|
export interface DiscoveredClaudePlugins {
|
|
44
73
|
/** Enabled via the user's own `~/.claude` config; loads without ceremony. */
|
|
@@ -71,9 +100,18 @@ export interface EnableState {
|
|
|
71
100
|
export declare function readEnabledPlugins(cwd: string, homeDir: string): Map<string, EnableState>;
|
|
72
101
|
/** `~/.claude/plugins/known_marketplaces.json` → marketplace name → checkout dir. */
|
|
73
102
|
export declare function readKnownMarketplaces(homeDir: string): Map<string, string>;
|
|
103
|
+
/**
|
|
104
|
+
* Names and versions from marketplace/plugin JSON become path segments under
|
|
105
|
+
* the plugin home (`cache/<mp>/<plugin>/<version>`), so they must be plain
|
|
106
|
+
* single segments: no separators, no `.`/`..`, nothing starting with `-`.
|
|
107
|
+
*/
|
|
108
|
+
export declare const SAFE_SEGMENT: RegExp;
|
|
109
|
+
export declare function isSafeSegment(value: unknown): value is string;
|
|
74
110
|
export interface MarketplaceEntry {
|
|
75
111
|
name: string;
|
|
76
112
|
source: unknown;
|
|
113
|
+
version?: string;
|
|
114
|
+
description?: string;
|
|
77
115
|
}
|
|
78
116
|
export interface Marketplace {
|
|
79
117
|
name: string;
|
|
@@ -98,10 +136,12 @@ export declare function resolveLocalPluginRoot(mp: Marketplace, entry: Marketpla
|
|
|
98
136
|
* discovery reads whole dirs), root `SKILL.md` fallback. Null when the plugin
|
|
99
137
|
* has nothing we can bridge.
|
|
100
138
|
*/
|
|
101
|
-
export declare function pluginAssets(root: string, name: string): PluginAssets | null;
|
|
139
|
+
export declare function pluginAssets(root: string, name: string, meta?: PluginMeta): PluginAssets | null;
|
|
102
140
|
export interface DiscoverPluginsDeps {
|
|
103
141
|
cwd: string;
|
|
104
142
|
homeDir: string;
|
|
143
|
+
/** YAGNI's own ledger (`readLedger` from pluginStore.ts); wins over the mirrored Claude Code state. */
|
|
144
|
+
yagniLedger?: PluginLedger;
|
|
105
145
|
}
|
|
106
146
|
/**
|
|
107
147
|
* All locally-present Claude Code plugin content relevant to `cwd`, split by
|
package/dist/claudePlugins.js
CHANGED
|
@@ -17,10 +17,19 @@
|
|
|
17
17
|
* relative-path source loads on the project side, unless explicitly
|
|
18
18
|
* disabled via `enabledPlugins`.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* Three sources, first wins on the same `name@marketplace`:
|
|
21
|
+
*
|
|
22
|
+
* 0. YAGNI's own ledger (`~/.yagni-code/plugins`, written by `yagni plugin`;
|
|
23
|
+
* see pluginStore.ts) — handed in by the caller as `yagniLedger`.
|
|
24
|
+
* 1. Claude Code's ledger + `enabledPlugins` (mirrored read-only).
|
|
25
|
+
* 2. The repo's own `.claude-plugin/marketplace.json`.
|
|
26
|
+
*
|
|
27
|
+
* Plugin MCP servers (`.mcp.json` at the plugin root, or the manifest's
|
|
28
|
+
* `mcpServers`) are collected here and bridged by the launcher into the
|
|
29
|
+
* extension's MCP client (see claudeCompat.ts). Deliberately NOT here:
|
|
30
|
+
* network installation (that is pluginStore.ts), plugin hooks, LSP servers,
|
|
31
|
+
* themes, output styles — those are reported as `unsupported` so `yagni
|
|
32
|
+
* plugin list` can say so. Discovery is read-only.
|
|
24
33
|
*
|
|
25
34
|
* Everything is fail-soft: malformed JSON, missing dirs, or hostile path
|
|
26
35
|
* entries degrade to "that plugin absent" — never a failed launch. Path
|
|
@@ -151,10 +160,20 @@ export function readKnownMarketplaces(homeDir) {
|
|
|
151
160
|
}
|
|
152
161
|
return out;
|
|
153
162
|
}
|
|
163
|
+
// ── marketplace file ────────────────────────────────────────────────────────
|
|
164
|
+
/**
|
|
165
|
+
* Names and versions from marketplace/plugin JSON become path segments under
|
|
166
|
+
* the plugin home (`cache/<mp>/<plugin>/<version>`), so they must be plain
|
|
167
|
+
* single segments: no separators, no `.`/`..`, nothing starting with `-`.
|
|
168
|
+
*/
|
|
169
|
+
export const SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
170
|
+
export function isSafeSegment(value) {
|
|
171
|
+
return typeof value === "string" && SAFE_SEGMENT.test(value);
|
|
172
|
+
}
|
|
154
173
|
/** Parse `<root>/.claude-plugin/marketplace.json`; null when absent/unusable. */
|
|
155
174
|
export function readMarketplace(root) {
|
|
156
175
|
const parsed = readJsonObject(join(root, ".claude-plugin", "marketplace.json"));
|
|
157
|
-
if (!parsed ||
|
|
176
|
+
if (!parsed || !isSafeSegment(parsed.name))
|
|
158
177
|
return null;
|
|
159
178
|
if (!Array.isArray(parsed.plugins))
|
|
160
179
|
return null;
|
|
@@ -166,10 +185,16 @@ export function readMarketplace(root) {
|
|
|
166
185
|
for (const item of parsed.plugins) {
|
|
167
186
|
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
168
187
|
continue;
|
|
169
|
-
const
|
|
170
|
-
|
|
188
|
+
const entry = item;
|
|
189
|
+
const name = entry.name;
|
|
190
|
+
if (!isSafeSegment(name))
|
|
171
191
|
continue;
|
|
172
|
-
plugins.push({
|
|
192
|
+
plugins.push({
|
|
193
|
+
name,
|
|
194
|
+
source: entry.source,
|
|
195
|
+
...(isSafeSegment(entry.version) ? { version: entry.version } : {}),
|
|
196
|
+
...(typeof entry.description === "string" ? { description: entry.description } : {}),
|
|
197
|
+
});
|
|
173
198
|
}
|
|
174
199
|
return { name: parsed.name, root, pluginRoot, plugins };
|
|
175
200
|
}
|
|
@@ -212,13 +237,17 @@ function asStringArray(value) {
|
|
|
212
237
|
* discovery reads whole dirs), root `SKILL.md` fallback. Null when the plugin
|
|
213
238
|
* has nothing we can bridge.
|
|
214
239
|
*/
|
|
215
|
-
export function pluginAssets(root, name) {
|
|
240
|
+
export function pluginAssets(root, name, meta = {}) {
|
|
216
241
|
if (!isDirectory(root))
|
|
217
242
|
return null;
|
|
218
|
-
const
|
|
243
|
+
const manifestPath = join(root, ".claude-plugin", "plugin.json");
|
|
244
|
+
const manifest = readJsonObject(manifestPath) ?? {};
|
|
245
|
+
// Default component locations go through the same containment check as
|
|
246
|
+
// manifest-declared ones: a `skills` symlink pointing outside the plugin
|
|
247
|
+
// must not hand that directory to pi.
|
|
219
248
|
const skillPaths = [];
|
|
220
|
-
const defaultSkills =
|
|
221
|
-
if (isDirectory(defaultSkills))
|
|
249
|
+
const defaultSkills = containedExistingPath(root, "skills");
|
|
250
|
+
if (defaultSkills && isDirectory(defaultSkills))
|
|
222
251
|
skillPaths.push(defaultSkills);
|
|
223
252
|
const manifestSkills = asStringArray(manifest.skills);
|
|
224
253
|
for (const rel of manifestSkills) {
|
|
@@ -227,8 +256,8 @@ export function pluginAssets(root, name) {
|
|
|
227
256
|
skillPaths.push(contained);
|
|
228
257
|
}
|
|
229
258
|
if (skillPaths.length === 0 && manifestSkills.length === 0) {
|
|
230
|
-
const rootSkill =
|
|
231
|
-
if (isFile(rootSkill))
|
|
259
|
+
const rootSkill = containedExistingPath(root, "SKILL.md");
|
|
260
|
+
if (rootSkill && isFile(rootSkill))
|
|
232
261
|
skillPaths.push(rootSkill);
|
|
233
262
|
}
|
|
234
263
|
const componentPaths = (manifestValue, defaultDir) => {
|
|
@@ -242,14 +271,62 @@ export function pluginAssets(root, name) {
|
|
|
242
271
|
}
|
|
243
272
|
return out;
|
|
244
273
|
}
|
|
245
|
-
const def =
|
|
246
|
-
return isDirectory(def) ? [def] : [];
|
|
274
|
+
const def = containedExistingPath(root, defaultDir);
|
|
275
|
+
return def && isDirectory(def) ? [def] : [];
|
|
247
276
|
};
|
|
248
277
|
const commandPaths = componentPaths(manifest.commands, "commands");
|
|
249
278
|
const agentDirs = componentPaths(manifest.agents, "agents").filter(isDirectory);
|
|
250
|
-
|
|
279
|
+
const mcp = pluginMcp(root, manifest, manifestPath);
|
|
280
|
+
const unsupported = [];
|
|
281
|
+
if (manifest.hooks !== undefined || isDirectory(join(root, "hooks")) || isFile(join(root, "hooks.json")))
|
|
282
|
+
unsupported.push("hooks");
|
|
283
|
+
if (manifest.lspServers !== undefined || isFile(join(root, ".lsp.json")))
|
|
284
|
+
unsupported.push("lsp");
|
|
285
|
+
if (manifest.outputStyles !== undefined)
|
|
286
|
+
unsupported.push("output styles");
|
|
287
|
+
if (skillPaths.length === 0 && commandPaths.length === 0 && agentDirs.length === 0 && !mcp)
|
|
251
288
|
return null;
|
|
252
|
-
|
|
289
|
+
const version = meta.version ?? (typeof manifest.version === "string" ? manifest.version : null);
|
|
290
|
+
return {
|
|
291
|
+
name,
|
|
292
|
+
key: meta.key ?? name,
|
|
293
|
+
origin: meta.origin ?? "claude-code",
|
|
294
|
+
version,
|
|
295
|
+
root,
|
|
296
|
+
skillPaths,
|
|
297
|
+
commandPaths,
|
|
298
|
+
agentDirs,
|
|
299
|
+
mcp,
|
|
300
|
+
unsupported,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* A plugin's MCP servers, per Claude Code's rules: the manifest `mcpServers`
|
|
305
|
+
* field (an inline map, or a contained path to a JSON file with one), else
|
|
306
|
+
* `.mcp.json` at the plugin root. Malformed → null (fail-soft).
|
|
307
|
+
*/
|
|
308
|
+
function pluginMcp(root, manifest, manifestPath) {
|
|
309
|
+
const field = manifest.mcpServers;
|
|
310
|
+
if (field && typeof field === "object" && !Array.isArray(field)) {
|
|
311
|
+
return { sourcePath: manifestPath, servers: field };
|
|
312
|
+
}
|
|
313
|
+
const candidates = [];
|
|
314
|
+
if (typeof field === "string") {
|
|
315
|
+
const contained = containedExistingPath(root, field);
|
|
316
|
+
if (contained && isFile(contained))
|
|
317
|
+
candidates.push(contained);
|
|
318
|
+
}
|
|
319
|
+
const rootFile = containedExistingPath(root, ".mcp.json");
|
|
320
|
+
if (rootFile && isFile(rootFile))
|
|
321
|
+
candidates.push(rootFile);
|
|
322
|
+
for (const path of candidates) {
|
|
323
|
+
const parsed = readJsonObject(path);
|
|
324
|
+
const servers = parsed?.mcpServers;
|
|
325
|
+
if (servers && typeof servers === "object" && !Array.isArray(servers)) {
|
|
326
|
+
return { sourcePath: path, servers: servers };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
253
330
|
}
|
|
254
331
|
function resolveInstalledRoot(key, installed, marketplaces, realCwd) {
|
|
255
332
|
const entries = (installed.get(key) ?? []).filter((e) => isDirectory(e.installPath));
|
|
@@ -295,14 +372,40 @@ export function discoverClaudePlugins(deps) {
|
|
|
295
372
|
const user = [];
|
|
296
373
|
const project = [];
|
|
297
374
|
const seen = new Set();
|
|
375
|
+
// YAGNI-installed plugins are the user's own explicit action (`yagni
|
|
376
|
+
// plugin install`), so both scopes load without ceremony; a project-scope
|
|
377
|
+
// install applies only inside its repo.
|
|
378
|
+
for (const [key, entries] of Object.entries(deps.yagniLedger?.plugins ?? {})) {
|
|
379
|
+
const applicable = entries.filter((e) => {
|
|
380
|
+
if (!e.enabled || !isDirectory(e.installPath))
|
|
381
|
+
return false;
|
|
382
|
+
if (e.scope === "user")
|
|
383
|
+
return true;
|
|
384
|
+
const real = typeof e.projectPath === "string" ? realOrNull(e.projectPath) : null;
|
|
385
|
+
return !!real && (realCwd === real || realCwd.startsWith(real + sep));
|
|
386
|
+
});
|
|
387
|
+
const chosen = applicable.find((e) => e.scope === "project") ?? applicable[0];
|
|
388
|
+
if (!chosen)
|
|
389
|
+
continue;
|
|
390
|
+
const at = key.lastIndexOf("@");
|
|
391
|
+
const assets = pluginAssets(chosen.installPath, at > 0 ? key.slice(0, at) : key, {
|
|
392
|
+
key,
|
|
393
|
+
origin: "yagni",
|
|
394
|
+
version: chosen.version,
|
|
395
|
+
});
|
|
396
|
+
if (!assets)
|
|
397
|
+
continue;
|
|
398
|
+
seen.add(key);
|
|
399
|
+
user.push(assets);
|
|
400
|
+
}
|
|
298
401
|
for (const [key, state] of enabled) {
|
|
299
|
-
if (!state.enabled)
|
|
402
|
+
if (!state.enabled || seen.has(key))
|
|
300
403
|
continue;
|
|
301
404
|
const root = resolveInstalledRoot(key, installed, marketplaces, realCwd);
|
|
302
405
|
if (!root)
|
|
303
406
|
continue;
|
|
304
407
|
const at = key.lastIndexOf("@");
|
|
305
|
-
const assets = pluginAssets(root, at > 0 ? key.slice(0, at) : key);
|
|
408
|
+
const assets = pluginAssets(root, at > 0 ? key.slice(0, at) : key, { key, origin: "claude-code" });
|
|
306
409
|
if (!assets)
|
|
307
410
|
continue;
|
|
308
411
|
seen.add(key);
|
|
@@ -322,7 +425,12 @@ export function discoverClaudePlugins(deps) {
|
|
|
322
425
|
resolveInstalledRoot(key, installed, marketplaces, realCwd);
|
|
323
426
|
if (!root)
|
|
324
427
|
continue;
|
|
325
|
-
const
|
|
428
|
+
const declared = entry.version;
|
|
429
|
+
const assets = pluginAssets(root, entry.name, {
|
|
430
|
+
key,
|
|
431
|
+
origin: "repo-marketplace",
|
|
432
|
+
version: typeof declared === "string" ? declared : null,
|
|
433
|
+
});
|
|
326
434
|
if (!assets)
|
|
327
435
|
continue;
|
|
328
436
|
seen.add(key);
|
package/dist/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ import { DISTRIBUTION } from "./distribution.js";
|
|
|
24
24
|
import { connectCommand } from "./connectClaudeCode.js";
|
|
25
25
|
import { goCommand } from "./goHeadless.js";
|
|
26
26
|
import { mcpCommand } from "./mcpCommand.js";
|
|
27
|
+
import { pluginCommand } from "./pluginCommand.js";
|
|
27
28
|
import { login } from "./login.js";
|
|
28
29
|
import { logout } from "./logout.js";
|
|
29
30
|
import { tokenCommand } from "./token.js";
|
|
@@ -520,6 +521,10 @@ export const HELP_TEXT = [
|
|
|
520
521
|
" add-from-claude, and more — run `yagni mcp` for",
|
|
521
522
|
" details. OAuth servers authenticate via the",
|
|
522
523
|
" /mcp panel in a session.",
|
|
524
|
+
" yagni plugin <subcommand> Claude Code plugins from a marketplace: marketplace",
|
|
525
|
+
" add, install, uninstall, update, list — run",
|
|
526
|
+
" `yagni plugin` for details. Skills, commands,",
|
|
527
|
+
" agents, and bundled MCP servers load on launch.",
|
|
523
528
|
" yagni token Output the active environment's API token (for helpers).",
|
|
524
529
|
" yagni use <name> Switch the active environment (sticky).",
|
|
525
530
|
" Presets: prod, local. Others need --base-url <url>.",
|
|
@@ -542,6 +547,8 @@ export const HELP_TEXT = [
|
|
|
542
547
|
" /todos Show the agent's live task list.",
|
|
543
548
|
" /cost Session usage and credit headroom.",
|
|
544
549
|
" /mcp Manage MCP servers; authenticate OAuth servers.",
|
|
550
|
+
" /plugin Browse, install, and remove Claude Code plugins.",
|
|
551
|
+
" /reload-plugins Apply plugin changes without restarting.",
|
|
545
552
|
"",
|
|
546
553
|
"The active environment is sticky; `use` switches it (prod is the default).",
|
|
547
554
|
"Set YAGNI_BASE_URL to override the base URL for a single run.",
|
|
@@ -665,6 +672,11 @@ export async function main(argv) {
|
|
|
665
672
|
if (command === "mcp") {
|
|
666
673
|
return mcpCommand(rest);
|
|
667
674
|
}
|
|
675
|
+
// Claude Code plugin marketplaces: install/remove/update from YAGNI itself
|
|
676
|
+
// (state under ~/.yagni-code/plugins; Claude Code's own installs still mirror).
|
|
677
|
+
if (command === "plugin") {
|
|
678
|
+
return pluginCommand(rest);
|
|
679
|
+
}
|
|
668
680
|
// The headless pipeline entry. `go` is a real subcommand, not passthrough:
|
|
669
681
|
// the interactive run stays `/go` inside a session, and a `yagni go` without
|
|
670
682
|
// --headless is refused with usage rather than launched as an agent prompt.
|
package/dist/doctor.js
CHANGED
|
@@ -422,7 +422,7 @@ async function defaultProbeMcp(env = process.env) {
|
|
|
422
422
|
const repoRoot = mod.resolveProjectRoot(process.cwd());
|
|
423
423
|
const { state } = mod.readProjectApproval(repoRoot);
|
|
424
424
|
const undecided = loaded.servers
|
|
425
|
-
.filter((s) => s.scope === "project" && mod.decisionFor(state, s.name) === "undecided")
|
|
425
|
+
.filter((s) => (s.scope === "project" || s.gated === true) && mod.decisionFor(state, s.name) === "undecided")
|
|
426
426
|
.map((s) => s.name);
|
|
427
427
|
return {
|
|
428
428
|
disabled: false,
|
|
@@ -3,6 +3,7 @@ import { type SpendResponse } from "./costHud.js";
|
|
|
3
3
|
import { runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
4
4
|
import { startMcp as defaultStartMcp } from "./mcp/startup.js";
|
|
5
5
|
import { type GuardianGateEvent } from "./permission/gate.js";
|
|
6
|
+
import { type PluginCliRunner } from "./plugins/panel.js";
|
|
6
7
|
import { type FlushOutcome, type SpoolClientOpts } from "./spool.js";
|
|
7
8
|
import { type TokenProvider } from "./tokenProvider.js";
|
|
8
9
|
import { type CatalogResult, type ContextBrief } from "./config.js";
|
|
@@ -59,6 +60,8 @@ export interface RegisterYagniDeps {
|
|
|
59
60
|
* homes or spawning servers. Default runs the real startup.
|
|
60
61
|
*/
|
|
61
62
|
startMcp?: typeof defaultStartMcp;
|
|
63
|
+
/** Test seam for the /plugin panel: runs `yagni plugin …` (defaults to spawning YAGNI_CLI_PATH). */
|
|
64
|
+
runPluginCli?: PluginCliRunner;
|
|
62
65
|
/**
|
|
63
66
|
* The CLI init pass (Onramp Door B). Injectable so tests assert it fires on a
|
|
64
67
|
* fresh-workspace first-run and is skipped otherwise, without a network or disk.
|
package/dist/extension/index.js
CHANGED
|
@@ -41,6 +41,7 @@ import { sandboxAutoAllowDecision } from "./sandbox/bash.js";
|
|
|
41
41
|
import { registerTelemetry } from "./telemetry/register.js";
|
|
42
42
|
import { loadHooksConfig, makeHookRunner, registerHooks } from "./hooks.js";
|
|
43
43
|
import { loadPermissionRules } from "./permissionRules/loadConfig.js";
|
|
44
|
+
import { registerPluginPanel } from "./plugins/panel.js";
|
|
44
45
|
import { registerSubagents } from "./subagents.js";
|
|
45
46
|
import { createUltraHolder, registerUltraCommand } from "./ultra.js";
|
|
46
47
|
import { registerTodos } from "./todos.js";
|
|
@@ -278,6 +279,9 @@ export async function registerYagni(pi, deps = {}) {
|
|
|
278
279
|
// before_agent_start handler below) and widens the tool's fan-out ceiling.
|
|
279
280
|
const ultraHolder = createUltraHolder();
|
|
280
281
|
registerSubagents(pi, { isUltra: () => ultraHolder.get(), workingLine, childUsage });
|
|
282
|
+
// /plugin + /reload-plugins: manage Claude Code marketplace plugins from the
|
|
283
|
+
// session, driving the launcher's `yagni plugin …` (YAGNI_CLI_PATH).
|
|
284
|
+
registerPluginPanel(pi, { env: deps.env, runCli: deps.runPluginCli });
|
|
281
285
|
registerUltraCommand(pi, ultraHolder);
|
|
282
286
|
// Shared fetch timeout for the small, interactive display-path reads below
|
|
283
287
|
// (/cost's spend + headroom, and Task 8's per-run spend for /go's summary):
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* from its bundled copy). Pure config I/O — no pi imports, no TUI, no network —
|
|
5
5
|
* so importing it from the launcher is side-effect-free.
|
|
6
6
|
*/
|
|
7
|
-
export { McpServerConfig, McpScope, PROJECT_CONFIG_FILENAME, ScopedMcpServerConfig, expandEnvVarsInString, expandServerEnv, loadMcpServers, mcpConfigPath, readProjectMcpConfig, readUserMcpConfig, resolveProjectRoot, validateServerConfig, writeUserMcpConfig, } from "./config.js";
|
|
7
|
+
export { McpServerConfig, McpScope, PLUGIN_MCP_ENV, PROJECT_CONFIG_FILENAME, ScopedMcpServerConfig, expandEnvVarsInString, expandServerEnv, loadMcpServers, mcpConfigPath, readPluginMcpServers, readProjectMcpConfig, readUserMcpConfig, resolveProjectRoot, validateServerConfig, writeUserMcpConfig, } from "./config.js";
|
|
8
8
|
export { ProjectApprovalState, decisionFor, readProjectApproval, recordProjectDecision, resetProjectChoices, undecidedProjectServers, } from "./approval.js";
|
|
9
9
|
export { McpAuthFile, StoredOAuthEntry, deleteStoredOAuthEntry, getServerKey, getStoredOAuthEntry, mcpAuthPath, readMcpAuth, updateStoredOAuthEntry, writeMcpAuth, } from "./authStore.js";
|
|
10
10
|
export { revokeTokensOnRemove } from "./auth.js";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* from its bundled copy). Pure config I/O — no pi imports, no TUI, no network —
|
|
5
5
|
* so importing it from the launcher is side-effect-free.
|
|
6
6
|
*/
|
|
7
|
-
export { PROJECT_CONFIG_FILENAME, expandEnvVarsInString, expandServerEnv, loadMcpServers, mcpConfigPath, readProjectMcpConfig, readUserMcpConfig, resolveProjectRoot, validateServerConfig, writeUserMcpConfig, } from "./config.js";
|
|
7
|
+
export { PLUGIN_MCP_ENV, PROJECT_CONFIG_FILENAME, expandEnvVarsInString, expandServerEnv, loadMcpServers, mcpConfigPath, readPluginMcpServers, readProjectMcpConfig, readUserMcpConfig, resolveProjectRoot, validateServerConfig, writeUserMcpConfig, } from "./config.js";
|
|
8
8
|
export { decisionFor, readProjectApproval, recordProjectDecision, resetProjectChoices, undecidedProjectServers, } from "./approval.js";
|
|
9
9
|
export { deleteStoredOAuthEntry, getServerKey, getStoredOAuthEntry, mcpAuthPath, readMcpAuth, updateStoredOAuthEntry, writeMcpAuth, } from "./authStore.js";
|
|
10
10
|
export { revokeTokensOnRemove } from "./auth.js";
|