@yagni-app/code 1.0.6 → 1.0.8

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 (92) hide show
  1. package/README.md +91 -46
  2. package/dist/claudeCompat.d.ts +59 -0
  3. package/dist/claudeCompat.js +109 -2
  4. package/dist/claudePlugins.d.ts +45 -5
  5. package/dist/claudePlugins.js +129 -21
  6. package/dist/cli.js +16 -4
  7. package/dist/doctor.d.ts +21 -8
  8. package/dist/doctor.js +53 -28
  9. package/dist/extension/askUserQuestionTool.js +7 -2
  10. package/dist/extension/condensedTools.d.ts +12 -1
  11. package/dist/extension/condensedTools.js +17 -9
  12. package/dist/extension/config.d.ts +6 -0
  13. package/dist/extension/hooks.d.ts +3 -3
  14. package/dist/extension/hooks.js +30 -5
  15. package/dist/extension/index.d.ts +19 -0
  16. package/dist/extension/index.js +248 -43
  17. package/dist/extension/mcp/cliConfig.d.ts +1 -1
  18. package/dist/extension/mcp/cliConfig.js +1 -1
  19. package/dist/extension/mcp/config.d.ts +24 -2
  20. package/dist/extension/mcp/config.js +75 -3
  21. package/dist/extension/mcp/manager.d.ts +3 -1
  22. package/dist/extension/mcp/manager.js +2 -2
  23. package/dist/extension/mcp/panel.d.ts +0 -1
  24. package/dist/extension/mcp/panel.js +13 -3
  25. package/dist/extension/mcp/startup.js +8 -6
  26. package/dist/extension/permission/gate.d.ts +101 -2
  27. package/dist/extension/permission/gate.js +485 -42
  28. package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
  29. package/dist/extension/permissionRules/bashFileArgs.js +236 -0
  30. package/dist/extension/permissionRules/engine.d.ts +50 -0
  31. package/dist/extension/permissionRules/engine.js +238 -0
  32. package/dist/extension/permissionRules/loadConfig.d.ts +64 -0
  33. package/dist/extension/permissionRules/loadConfig.js +105 -0
  34. package/dist/extension/permissionRules/parser.d.ts +38 -0
  35. package/dist/extension/permissionRules/parser.js +136 -0
  36. package/dist/extension/permissionRules/pathRules.d.ts +60 -0
  37. package/dist/extension/permissionRules/pathRules.js +122 -0
  38. package/dist/extension/permissionRules/shellRules.d.ts +52 -0
  39. package/dist/extension/permissionRules/shellRules.js +221 -0
  40. package/dist/extension/pipeline/invocation.d.ts +3 -6
  41. package/dist/extension/pipeline/invocation.js +3 -6
  42. package/dist/extension/pipeline/runner.d.ts +0 -1
  43. package/dist/extension/pipeline/runner.js +6 -14
  44. package/dist/extension/plugins/inventory.d.ts +88 -0
  45. package/dist/extension/plugins/inventory.js +144 -0
  46. package/dist/extension/plugins/panel.d.ts +45 -0
  47. package/dist/extension/plugins/panel.js +293 -0
  48. package/dist/extension/sandbox/bash.d.ts +99 -0
  49. package/dist/extension/sandbox/bash.js +190 -0
  50. package/dist/extension/sandbox/config.d.ts +115 -0
  51. package/dist/extension/sandbox/config.js +388 -0
  52. package/dist/extension/sandbox/manager.d.ts +108 -0
  53. package/dist/extension/sandbox/manager.js +243 -0
  54. package/dist/extension/sandbox/panel.d.ts +111 -0
  55. package/dist/extension/sandbox/panel.js +342 -0
  56. package/dist/extension/sandbox/session.d.ts +85 -0
  57. package/dist/extension/sandbox/session.js +829 -0
  58. package/dist/extension/settingsFiles.d.ts +50 -0
  59. package/dist/extension/settingsFiles.js +206 -0
  60. package/dist/extension/telemetry/attrs.d.ts +96 -0
  61. package/dist/extension/telemetry/attrs.js +149 -0
  62. package/dist/extension/telemetry/config.d.ts +103 -0
  63. package/dist/extension/telemetry/config.js +193 -0
  64. package/dist/extension/telemetry/index.d.ts +7 -0
  65. package/dist/extension/telemetry/index.js +7 -0
  66. package/dist/extension/telemetry/probe.d.ts +29 -0
  67. package/dist/extension/telemetry/probe.js +122 -0
  68. package/dist/extension/telemetry/register.d.ts +47 -0
  69. package/dist/extension/telemetry/register.js +207 -0
  70. package/dist/extension/telemetry/sdk.d.ts +63 -0
  71. package/dist/extension/telemetry/sdk.js +207 -0
  72. package/dist/extension/telemetry/tracker.d.ts +131 -0
  73. package/dist/extension/telemetry/tracker.js +551 -0
  74. package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
  75. package/dist/extension/vendor/ignore.d.ts +86 -0
  76. package/dist/extension/vendor/ignore.js +788 -0
  77. package/dist/goHeadless.d.ts +1 -1
  78. package/dist/goHeadless.js +2 -2
  79. package/dist/launch.d.ts +4 -3
  80. package/dist/launch.js +7 -4
  81. package/dist/mcpCommand.d.ts +10 -1
  82. package/dist/mcpCommand.js +42 -10
  83. package/dist/otel.d.ts +67 -90
  84. package/dist/otel.js +152 -195
  85. package/dist/paths.d.ts +13 -0
  86. package/dist/paths.js +18 -0
  87. package/dist/pluginCommand.d.ts +43 -0
  88. package/dist/pluginCommand.js +499 -0
  89. package/dist/pluginStore.d.ts +170 -0
  90. package/dist/pluginStore.js +554 -0
  91. package/dist/upgrade.js +10 -1
  92. package/package.json +19 -3
package/README.md CHANGED
@@ -173,62 +173,107 @@ environment is recorded in `~/.yagni-code/config.json`. A pre-profiles
173
173
 
174
174
  ### OTel export (opt-in)
175
175
 
176
- Point sessions at **your own** OpenTelemetry collector (Datadog Agent, Grafana
177
- Alloy, an OTLP-native backend) and every session — including `/go` stage
178
- children and subagents emits a per-prompt span tree: interaction → LLM
179
- request tool calls, following the OTel GenAI semantic conventions. Nothing is
180
- exported unless you configure an endpoint.
181
-
182
- Enable it one of four ways (first match wins):
183
-
184
- - `OTEL_EXPORTER_OTLP_ENDPOINT=…` in the environment (a personal override
185
- handy for pointing one session at a scratch collector), or
186
- - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=…` in the environment the per-signal
187
- form Claude Code setups use (e.g.
188
- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://otlp.datadoghq.com/v1/traces`
189
- with `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf`); the launcher
190
- bridges it to the generic var pi-otel reads, so a Claude Code OTel env
191
- lights up YAGNI Code traces with no extra config, or
176
+ Point sessions at **your own** OpenTelemetry collector (Datadog, Grafana
177
+ Cloud, a Datadog Agent or Grafana Alloy, any OTLP-native backend) and every
178
+ session including `/go` stage children and subagents exports three
179
+ signals in the shape Claude Code's telemetry uses, under a `yagni_code`
180
+ prefix instead of `claude_code`:
181
+
182
+ - **Traces:** a per-prompt span tree, `yagni_code.interaction`
183
+ `yagni_code.llm_request` and `yagni_code.tool`, carrying both the flat
184
+ Claude Code attributes (`model`, `input_tokens`, `cost_usd`, `tool_name`)
185
+ and the OTel GenAI conventions (`gen_ai.*`) that LLM-observability
186
+ products read. `gen_ai.operation.name` classifies each span the way those
187
+ products expect: `invoke_agent` on the interaction root, `chat` on the LLM
188
+ request, `execute_tool` on tool spans (the turn span has no semconv
189
+ operation and is left unset). The two count tokens differently on purpose: the flat
190
+ `input_tokens` / `cache_read_tokens` / `cache_creation_tokens` are disjoint,
191
+ exactly as Claude Code reports them, while `gen_ai.usage.input_tokens`
192
+ follows the semconv and includes the cached tokens, with
193
+ `gen_ai.usage.cache_read.input_tokens` and
194
+ `gen_ai.usage.cache_write.input_tokens` as subsets of it.
195
+ `gen_ai.cost.estimated_total` carries the same sell-rate cost as `cost_usd`
196
+ so vendor cost views work for the opaque tier id. It is an integer in
197
+ nanodollars (`cost_usd * 1e9`): Datadog reads that attribute unscaled into
198
+ its nanodollar cost metric, so a dollar value shows up a billion times too
199
+ small.
200
+ - **Metrics:** `yagni_code.session.count`, `token.usage` (by `type` and
201
+ `model`), `cost.usage`, `lines_of_code.count`, `commit.count`,
202
+ `pull_request.count`, `code_edit_tool.decision`, `active_time.total`.
203
+ - **Log events:** `user_prompt`, `assistant_response`, `api_request`,
204
+ `api_error`, `tool_result`, `tool_decision`, `permission_mode_changed`
205
+ (as `event.name`, body `yagni_code.<name>`), with `session.id`,
206
+ `organization.id`, `user.email`, `terminal.type`, and a per-prompt
207
+ `prompt.id`.
208
+
209
+ Every signal carries `organization.id` (the workspace) and `user.email` (the
210
+ signed-in developer's account email, learned from the backend at session
211
+ start) so a collector can attribute sessions and spend per person.
212
+ `OTEL_METRICS_INCLUDE_ACCOUNT_UUID=false` drops the email from all three
213
+ signals. Prompt, response, and tool content never export.
214
+
215
+ Nothing is exported unless you configure an endpoint. Enable it one of three
216
+ ways (first match wins):
217
+
218
+ - the standard OTel env in your shell — the generic
219
+ `OTEL_EXPORTER_OTLP_ENDPOINT` (a base URL; `/v1/<signal>` is appended for
220
+ http transports) or the per-signal
221
+ `OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_ENDPOINT` a Claude Code setup
222
+ already carries — so a machine configured for Claude Code lights up with
223
+ no extra config (with per-signal endpoints only, exactly those signals
224
+ export; nothing falls back to a localhost collector), or
192
225
  - **workspace settings** (the zero-setup path): a workspace admin sets the
193
226
  endpoint, protocol, and any collector headers (e.g. a Datadog API key) once
194
- in the web app under Settings → YAGNI Code → Trace export. Every session in
195
- the workspace picks it up at launch — nothing to install or configure on
196
- developer machines. Header values are encrypted at rest server-side and the
197
- launch-time copy is cached at mode `0600`, the same posture as your device
198
- token. Or,
199
- - commit `{ "otel": { "endpoint": "http://<collector>:4317" } }` to the repo's
200
- `.pi/settings.json` so one repo's sessions export without per-machine setup.
201
-
202
- Standard OTel env vars are honored (`OTEL_EXPORTER_OTLP_PROTOCOL`,
203
- `OTEL_EXPORTER_OTLP_HEADERS`, `OTEL_SERVICE_NAME` — defaults to `yagni-code`),
204
- and `PI_OTEL_DISABLED=1` is the kill switch. `yagni doctor` shows the current
205
- export state and probes the endpoint live (unreachable a warning with a
206
- hint, never an exit-code flip).
207
-
208
- Two recipes for common collectors:
209
-
210
- - **Datadog, direct SaaS (no agent):** endpoint
211
- `https://otlp.datadoghq.com/v1/traces` (US1; other sites have their own
212
- host `otlp.us3.datadoghq.com`, `otlp.us5.datadoghq.com`,
213
- `otlp.datadoghq.eu`, `otlp.ap1.datadoghq.com`), protocol `http/protobuf`
214
- (Datadog's direct trace intake does not accept gRPC), header
215
- `dd-api-key=<your Datadog API key>`. Use the full path, not `/v1/` — the
216
- endpoint is passed through verbatim, no path is appended for you.
227
+ in the web app under Settings → YAGNI Code → Telemetry export. Every
228
+ session in the workspace picks it up at launch — nothing to install or
229
+ configure on developer machines. Header values are encrypted at rest
230
+ server-side and the launch-time copy is cached at mode `0600`, the same
231
+ posture as your device token. Or,
232
+ - commit `{ "otel": { "endpoint": "http://<collector>:4317" } }` (optionally
233
+ with `protocol`, `headers`, `serviceName`) to the repo's `.pi/settings.json`
234
+ so one repo's sessions export without per-machine setup. Header values
235
+ must not contain commas (they travel as a comma-separated list); one that
236
+ does is dropped.
237
+
238
+ The rest of the standard OTel env is honored the way Claude Code honors it:
239
+ `OTEL_EXPORTER_OTLP_PROTOCOL` (and per-signal), `OTEL_EXPORTER_OTLP_HEADERS`
240
+ (and per-signal), `OTEL_SERVICE_NAME` (defaults to `yagni-code`),
241
+ `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_{METRICS,LOGS,TRACES}_EXPORTER=none` to
242
+ switch one signal off, `OTEL_METRIC_EXPORT_INTERVAL` /
243
+ `OTEL_LOGS_EXPORT_INTERVAL`, `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE`
244
+ (default `delta`), and the `OTEL_METRICS_INCLUDE_*` cardinality flags.
245
+ `YAGNI_OTEL_DISABLED=1` is the kill switch. `yagni doctor` shows the current
246
+ export state and sends one real record per signal, printing the collector's
247
+ own verdict (a 403 is a bad key, a 404 a bad path or site).
248
+
249
+ Three recipes for common collectors:
250
+
251
+ - **Datadog, direct SaaS (no agent):** endpoint `https://otlp.datadoghq.com`
252
+ (US1; other sites have their own host — `otlp.us3.datadoghq.com`,
253
+ `otlp.us5.datadoghq.com`, `otlp.datadoghq.eu`, `otlp.ap1.datadoghq.com`),
254
+ protocol `http/protobuf` (Datadog's direct intake does not accept gRPC),
255
+ header `dd-api-key=<your Datadog API key>`. Add `dd-otlp-source=llmobs`
256
+ (the workspace preset does) to have the traces routed into LLM
257
+ Observability as well as APM; the launcher scopes that header to the
258
+ traces request. Traces show in APM under service `yagni-code`, metrics
259
+ under `yagni_code.*`, events in Log Explorer as `service:yagni-code`.
217
260
  - **Datadog Agent:** endpoint `http://<agent-host>:4317`, protocol `grpc`, no
218
261
  key (the agent forwards with its own). OTLP ingest ships in the agent but
219
262
  is off by default — enable it with
220
263
  `otlp_config.receiver.protocols.grpc.endpoint: 0.0.0.0:4317` in
221
264
  `datadog.yaml`.
222
- - **Grafana Cloud:** endpoint `https://otlp-gateway-prod-…-0.grafana.net:443/otlp`,
223
- protocol `http/protobuf`, header `authorization=<the raw MTgx… value>` (the
224
- `Basic ` scheme is added for you).
265
+ - **Grafana Cloud:** endpoint `https://otlp-gateway-prod-<region>.grafana.net/otlp`
266
+ (the base URL from your stack's OTLP page — signals go to `/otlp/v1/traces`
267
+ and friends), protocol `http/protobuf`, header `authorization=<the raw
268
+ MTgx… value>` (the `Basic ` scheme is added for you).
225
269
 
226
270
  Two things are enforced and not configurable:
227
271
 
228
- - **Metadata only.** Token counts, cost, tier, finish reasons, and tool-call
229
- ids export; prompt and response text never do a settings file or env var
230
- asking for content capture is overridden.
231
- - **Cost is your contracted rate.** `pi.cost.usd` is computed from your
272
+ - **Metadata only.** Token counts, cost, tier, durations, tool names, and
273
+ payload sizes export; prompt text, model output, tool arguments, and tool
274
+ results never do `OTEL_LOG_USER_PROMPTS` and `OTEL_LOG_TOOL_DETAILS` are
275
+ deliberately not honored.
276
+ - **Cost is your contracted rate.** `cost_usd` is computed from your
232
277
  workspace's tier rate card, and the exported model name is the opaque tier
233
278
  id (`advanced`, `peak`, …), so traces never fingerprint the backing model.
234
279
 
@@ -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
@@ -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 hasProjectContent = projectArgv.length > 0 || projectAgentDirs.length > 0 || projectRulesDirs.length > 0;
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({ cwd: deps.cwd, homeDir: home });
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));
@@ -17,10 +17,19 @@
17
17
  * relative-path source loads on the project side, unless explicitly
18
18
  * disabled via `enabledPlugins`.
19
19
  *
20
- * Deliberately NOT here: network installation of any kind, plugin hooks, MCP
21
- * servers (standard MCP support lives in `src/mcpCommand.ts` + the extension's
22
- * `mcp/` module see `yagni mcp --help`), LSP servers, themes, output styles.
23
- * Discovery is read-only.
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