@yagni-app/code 1.0.6 → 1.0.7
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/README.md +84 -46
- 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 +16 -4
- package/dist/doctor.d.ts +21 -8
- package/dist/doctor.js +53 -28
- package/dist/extension/condensedTools.d.ts +12 -1
- package/dist/extension/condensedTools.js +17 -9
- package/dist/extension/index.d.ts +13 -0
- package/dist/extension/index.js +230 -43
- 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/permission/gate.d.ts +97 -2
- package/dist/extension/permission/gate.js +375 -26
- package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
- package/dist/extension/permissionRules/bashFileArgs.js +236 -0
- package/dist/extension/permissionRules/engine.d.ts +50 -0
- package/dist/extension/permissionRules/engine.js +238 -0
- package/dist/extension/permissionRules/loadConfig.d.ts +53 -0
- package/dist/extension/permissionRules/loadConfig.js +90 -0
- package/dist/extension/permissionRules/parser.d.ts +38 -0
- package/dist/extension/permissionRules/parser.js +136 -0
- package/dist/extension/permissionRules/pathRules.d.ts +58 -0
- package/dist/extension/permissionRules/pathRules.js +120 -0
- package/dist/extension/permissionRules/shellRules.d.ts +52 -0
- package/dist/extension/permissionRules/shellRules.js +221 -0
- package/dist/extension/pipeline/invocation.d.ts +3 -6
- package/dist/extension/pipeline/invocation.js +3 -6
- package/dist/extension/pipeline/runner.d.ts +0 -1
- package/dist/extension/pipeline/runner.js +6 -14
- 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/extension/sandbox/bash.d.ts +99 -0
- package/dist/extension/sandbox/bash.js +190 -0
- package/dist/extension/sandbox/config.d.ts +114 -0
- package/dist/extension/sandbox/config.js +366 -0
- package/dist/extension/sandbox/manager.d.ts +98 -0
- package/dist/extension/sandbox/manager.js +216 -0
- package/dist/extension/sandbox/panel.d.ts +111 -0
- package/dist/extension/sandbox/panel.js +342 -0
- package/dist/extension/sandbox/session.d.ts +85 -0
- package/dist/extension/sandbox/session.js +775 -0
- package/dist/extension/telemetry/attrs.d.ts +96 -0
- package/dist/extension/telemetry/attrs.js +149 -0
- package/dist/extension/telemetry/config.d.ts +99 -0
- package/dist/extension/telemetry/config.js +193 -0
- package/dist/extension/telemetry/index.d.ts +7 -0
- package/dist/extension/telemetry/index.js +7 -0
- package/dist/extension/telemetry/probe.d.ts +29 -0
- package/dist/extension/telemetry/probe.js +122 -0
- package/dist/extension/telemetry/register.d.ts +40 -0
- package/dist/extension/telemetry/register.js +192 -0
- package/dist/extension/telemetry/sdk.d.ts +63 -0
- package/dist/extension/telemetry/sdk.js +207 -0
- package/dist/extension/telemetry/tracker.d.ts +131 -0
- package/dist/extension/telemetry/tracker.js +551 -0
- package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
- package/dist/extension/vendor/ignore.d.ts +86 -0
- package/dist/extension/vendor/ignore.js +788 -0
- package/dist/goHeadless.d.ts +1 -1
- package/dist/goHeadless.js +2 -2
- package/dist/launch.d.ts +4 -3
- package/dist/launch.js +7 -4
- package/dist/mcpCommand.d.ts +10 -1
- package/dist/mcpCommand.js +42 -10
- package/dist/otel.d.ts +67 -90
- package/dist/otel.js +152 -195
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +18 -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 +19 -3
package/README.md
CHANGED
|
@@ -173,62 +173,100 @@ 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
|
|
177
|
-
Alloy,
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
`
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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`, `terminal.type`, and a per-prompt `prompt.id`.
|
|
207
|
+
|
|
208
|
+
Nothing is exported unless you configure an endpoint. Enable it one of three
|
|
209
|
+
ways (first match wins):
|
|
210
|
+
|
|
211
|
+
- the standard OTel env in your shell — the generic
|
|
212
|
+
`OTEL_EXPORTER_OTLP_ENDPOINT` (a base URL; `/v1/<signal>` is appended for
|
|
213
|
+
http transports) or the per-signal
|
|
214
|
+
`OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_ENDPOINT` a Claude Code setup
|
|
215
|
+
already carries — so a machine configured for Claude Code lights up with
|
|
216
|
+
no extra config (with per-signal endpoints only, exactly those signals
|
|
217
|
+
export; nothing falls back to a localhost collector), or
|
|
192
218
|
- **workspace settings** (the zero-setup path): a workspace admin sets the
|
|
193
219
|
endpoint, protocol, and any collector headers (e.g. a Datadog API key) once
|
|
194
|
-
in the web app under Settings → YAGNI Code →
|
|
195
|
-
the workspace picks it up at launch — nothing to install or
|
|
196
|
-
developer machines. Header values are encrypted at rest
|
|
197
|
-
launch-time copy is cached at mode `0600`, the same
|
|
198
|
-
token. Or,
|
|
199
|
-
- commit `{ "otel": { "endpoint": "http://<collector>:4317" } }`
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
220
|
+
in the web app under Settings → YAGNI Code → Telemetry export. Every
|
|
221
|
+
session in the workspace picks it up at launch — nothing to install or
|
|
222
|
+
configure on developer machines. Header values are encrypted at rest
|
|
223
|
+
server-side and the launch-time copy is cached at mode `0600`, the same
|
|
224
|
+
posture as your device token. Or,
|
|
225
|
+
- commit `{ "otel": { "endpoint": "http://<collector>:4317" } }` (optionally
|
|
226
|
+
with `protocol`, `headers`, `serviceName`) to the repo's `.pi/settings.json`
|
|
227
|
+
so one repo's sessions export without per-machine setup. Header values
|
|
228
|
+
must not contain commas (they travel as a comma-separated list); one that
|
|
229
|
+
does is dropped.
|
|
230
|
+
|
|
231
|
+
The rest of the standard OTel env is honored the way Claude Code honors it:
|
|
232
|
+
`OTEL_EXPORTER_OTLP_PROTOCOL` (and per-signal), `OTEL_EXPORTER_OTLP_HEADERS`
|
|
233
|
+
(and per-signal), `OTEL_SERVICE_NAME` (defaults to `yagni-code`),
|
|
234
|
+
`OTEL_RESOURCE_ATTRIBUTES`, `OTEL_{METRICS,LOGS,TRACES}_EXPORTER=none` to
|
|
235
|
+
switch one signal off, `OTEL_METRIC_EXPORT_INTERVAL` /
|
|
236
|
+
`OTEL_LOGS_EXPORT_INTERVAL`, `OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE`
|
|
237
|
+
(default `delta`), and the `OTEL_METRICS_INCLUDE_*` cardinality flags.
|
|
238
|
+
`YAGNI_OTEL_DISABLED=1` is the kill switch. `yagni doctor` shows the current
|
|
239
|
+
export state and sends one real record per signal, printing the collector's
|
|
240
|
+
own verdict (a 403 is a bad key, a 404 a bad path or site).
|
|
241
|
+
|
|
242
|
+
Three recipes for common collectors:
|
|
243
|
+
|
|
244
|
+
- **Datadog, direct SaaS (no agent):** endpoint `https://otlp.datadoghq.com`
|
|
245
|
+
(US1; other sites have their own host — `otlp.us3.datadoghq.com`,
|
|
246
|
+
`otlp.us5.datadoghq.com`, `otlp.datadoghq.eu`, `otlp.ap1.datadoghq.com`),
|
|
247
|
+
protocol `http/protobuf` (Datadog's direct intake does not accept gRPC),
|
|
248
|
+
header `dd-api-key=<your Datadog API key>`. Add `dd-otlp-source=llmobs`
|
|
249
|
+
(the workspace preset does) to have the traces routed into LLM
|
|
250
|
+
Observability as well as APM; the launcher scopes that header to the
|
|
251
|
+
traces request. Traces show in APM under service `yagni-code`, metrics
|
|
252
|
+
under `yagni_code.*`, events in Log Explorer as `service:yagni-code`.
|
|
217
253
|
- **Datadog Agent:** endpoint `http://<agent-host>:4317`, protocol `grpc`, no
|
|
218
254
|
key (the agent forwards with its own). OTLP ingest ships in the agent but
|
|
219
255
|
is off by default — enable it with
|
|
220
256
|
`otlp_config.receiver.protocols.grpc.endpoint: 0.0.0.0:4317` in
|
|
221
257
|
`datadog.yaml`.
|
|
222
|
-
- **Grafana Cloud:** endpoint `https://otlp-gateway-prod
|
|
223
|
-
|
|
224
|
-
|
|
258
|
+
- **Grafana Cloud:** endpoint `https://otlp-gateway-prod-<region>.grafana.net/otlp`
|
|
259
|
+
(the base URL from your stack's OTLP page — signals go to `/otlp/v1/traces`
|
|
260
|
+
and friends), protocol `http/protobuf`, header `authorization=<the raw
|
|
261
|
+
MTgx… value>` (the `Basic ` scheme is added for you).
|
|
225
262
|
|
|
226
263
|
Two things are enforced and not configurable:
|
|
227
264
|
|
|
228
|
-
- **Metadata only.** Token counts, cost, tier,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
265
|
+
- **Metadata only.** Token counts, cost, tier, durations, tool names, and
|
|
266
|
+
payload sizes export; prompt text, model output, tool arguments, and tool
|
|
267
|
+
results never do — `OTEL_LOG_USER_PROMPTS` and `OTEL_LOG_TOOL_DETAILS` are
|
|
268
|
+
deliberately not honored.
|
|
269
|
+
- **Cost is your contracted rate.** `cost_usd` is computed from your
|
|
232
270
|
workspace's tier rate card, and the exported model name is the opaque tier
|
|
233
271
|
id (`advanced`, `peak`, …), so traces never fingerprint the backing model.
|
|
234
272
|
|
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
|