@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.
- package/README.md +91 -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/askUserQuestionTool.js +7 -2
- package/dist/extension/condensedTools.d.ts +12 -1
- package/dist/extension/condensedTools.js +17 -9
- package/dist/extension/config.d.ts +6 -0
- package/dist/extension/hooks.d.ts +3 -3
- package/dist/extension/hooks.js +30 -5
- package/dist/extension/index.d.ts +19 -0
- package/dist/extension/index.js +248 -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 +101 -2
- package/dist/extension/permission/gate.js +485 -42
- 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 +64 -0
- package/dist/extension/permissionRules/loadConfig.js +105 -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 +60 -0
- package/dist/extension/permissionRules/pathRules.js +122 -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 +115 -0
- package/dist/extension/sandbox/config.js +388 -0
- package/dist/extension/sandbox/manager.d.ts +108 -0
- package/dist/extension/sandbox/manager.js +243 -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 +829 -0
- package/dist/extension/settingsFiles.d.ts +50 -0
- package/dist/extension/settingsFiles.js +206 -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 +103 -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 +47 -0
- package/dist/extension/telemetry/register.js +207 -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/dist/upgrade.js +10 -1
- package/package.json +19 -3
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash command-rule matching — ported from Claude Code's
|
|
3
|
+
* shellRuleMatching.ts + the matching half of BashTool/bashPermissions.ts.
|
|
4
|
+
*
|
|
5
|
+
* A bash rule's content is one of:
|
|
6
|
+
* - exact: `npm run build` — matches that exact command string
|
|
7
|
+
* - prefix: `npm install:*` — `:*` suffix = trailing wildcard
|
|
8
|
+
* - wildcard: `git log *` — `*` anywhere; `*` matches any text
|
|
9
|
+
* including spaces; `\*` literal
|
|
10
|
+
*
|
|
11
|
+
* Security invariants (Claude Code parity, enforced here):
|
|
12
|
+
* - compound commands are split on ;, &&, ||, |, and newlines, and EVERY
|
|
13
|
+
* subcommand must pass the rule check independently. Wildcards and
|
|
14
|
+
* prefixes never run against the raw compound string (`cd /x && rm -rf`
|
|
15
|
+
* must not ride a `cd:*` allow).
|
|
16
|
+
* - prefix matching requires a word boundary: `ls:*` matches `ls` and
|
|
17
|
+
* `ls -la` but never `lsof`.
|
|
18
|
+
* - a trailing ` *` wildcard that is the rule's ONLY wildcard also matches
|
|
19
|
+
* the bare command (`git log *` matches `git log`).
|
|
20
|
+
* - safe wrappers (timeout/time/nice/nohup) are stripped before matching,
|
|
21
|
+
* so `Bash(npm install:*)` matches `timeout 10 npm install foo`.
|
|
22
|
+
* - for DENY and ASK rules, ALL leading env-var assignments are stripped
|
|
23
|
+
* (fixed-point, mirroring Claude's stripAllLeadingEnvVars) so
|
|
24
|
+
* `FOO=bar denied_cmd` stays denied. For ALLOW rules only the safe-list
|
|
25
|
+
* env prefixes are stripped — an env var must never widen an allow
|
|
26
|
+
* (Claude's asymmetry, HackerOne #3543050).
|
|
27
|
+
*/
|
|
28
|
+
import { shellParse } from "../permission/execPolicy.js";
|
|
29
|
+
/** `npm install:*` → prefix `npm install`; else null. */
|
|
30
|
+
function extractPrefix(ruleContent) {
|
|
31
|
+
const m = ruleContent.match(/^(.+):\*$/);
|
|
32
|
+
return m?.[1] ?? null;
|
|
33
|
+
}
|
|
34
|
+
/** Any unescaped `*` that is not the `:*` trailing legacy form? */
|
|
35
|
+
function hasWildcards(pattern) {
|
|
36
|
+
if (pattern.endsWith(":*"))
|
|
37
|
+
return false;
|
|
38
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
39
|
+
if (pattern[i] !== "*")
|
|
40
|
+
continue;
|
|
41
|
+
let backslashes = 0;
|
|
42
|
+
let j = i - 1;
|
|
43
|
+
while (j >= 0 && pattern[j] === "\\") {
|
|
44
|
+
backslashes++;
|
|
45
|
+
j--;
|
|
46
|
+
}
|
|
47
|
+
if (backslashes % 2 === 0)
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
export function parseShellRule(ruleContent) {
|
|
53
|
+
const prefix = extractPrefix(ruleContent);
|
|
54
|
+
if (prefix !== null)
|
|
55
|
+
return { type: "prefix", prefix };
|
|
56
|
+
if (hasWildcards(ruleContent))
|
|
57
|
+
return { type: "wildcard", pattern: ruleContent };
|
|
58
|
+
return { type: "exact", command: ruleContent };
|
|
59
|
+
}
|
|
60
|
+
// --- Wildcard → regex (Claude Code shellRuleMatching.ts) ---
|
|
61
|
+
const ESCAPED_STAR = "\x00ESCAPED_STAR\x00";
|
|
62
|
+
const ESCAPED_BACKSLASH = "\x00ESCAPED_BACKSLASH\x00";
|
|
63
|
+
/** Match a command string against a `*`-wildcard pattern. */
|
|
64
|
+
export function matchWildcardPattern(pattern, command) {
|
|
65
|
+
const trimmed = pattern.trim();
|
|
66
|
+
let processed = "";
|
|
67
|
+
let i = 0;
|
|
68
|
+
while (i < trimmed.length) {
|
|
69
|
+
const ch = trimmed[i];
|
|
70
|
+
if (ch === "\\" && i + 1 < trimmed.length) {
|
|
71
|
+
const next = trimmed[i + 1];
|
|
72
|
+
if (next === "*") {
|
|
73
|
+
processed += ESCAPED_STAR;
|
|
74
|
+
i += 2;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (next === "\\") {
|
|
78
|
+
processed += ESCAPED_BACKSLASH;
|
|
79
|
+
i += 2;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
processed += ch;
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
const escaped = processed.replace(/[.+?^${}()|[\]\\'"]/g, "\\$&");
|
|
87
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
88
|
+
let regexPattern = withWildcards
|
|
89
|
+
.replace(new RegExp(ESCAPED_STAR, "g"), "\\*")
|
|
90
|
+
.replace(new RegExp(ESCAPED_BACKSLASH, "g"), "\\\\");
|
|
91
|
+
// A trailing ` *` wildcard that is the ONLY wildcard also matches the bare
|
|
92
|
+
// command — aligns `git log *` with `git log:*` prefix semantics.
|
|
93
|
+
const starCount = (processed.match(/\*/g) ?? []).length;
|
|
94
|
+
if (regexPattern.endsWith(" .*") && starCount === 1) {
|
|
95
|
+
regexPattern = regexPattern.slice(0, -3) + "( .*)?";
|
|
96
|
+
}
|
|
97
|
+
// `s` (dotAll): `*` spans embedded newlines within a single subcommand.
|
|
98
|
+
return new RegExp(`^${regexPattern}$`, "s").test(command);
|
|
99
|
+
}
|
|
100
|
+
// --- Command splitting / stripping ---
|
|
101
|
+
/** Split a command into its subcommand strings (operators are boundaries). */
|
|
102
|
+
export function splitSubcommands(command) {
|
|
103
|
+
const tokens = shellParse(command);
|
|
104
|
+
const segments = [];
|
|
105
|
+
let current = [];
|
|
106
|
+
const flush = () => {
|
|
107
|
+
if (current.length > 0) {
|
|
108
|
+
segments.push(current.join(" "));
|
|
109
|
+
current = [];
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
for (const t of tokens) {
|
|
113
|
+
if (typeof t === "object" && ("op" in t) && (t.op === "pipe" || t.op === "and" || t.op === "or" || t.op === "semi" || t.op === "substitution")) {
|
|
114
|
+
flush();
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
// Redirects/background are constructs, not boundaries — a subcommand
|
|
118
|
+
// carrying them is matched WITH the construct (the exec policy's
|
|
119
|
+
// construct floor handles the safety side; for rules we keep the segment).
|
|
120
|
+
current.push(typeof t === "string" ? t : renderOp(t));
|
|
121
|
+
}
|
|
122
|
+
flush();
|
|
123
|
+
return segments;
|
|
124
|
+
}
|
|
125
|
+
function renderOp(t) {
|
|
126
|
+
if (t.op === "redirect" && "target" in t)
|
|
127
|
+
return `>${t.target}`;
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Safe wrappers stripped before rule matching (Claude Code parity):
|
|
132
|
+
* timeout <n>, time, nice, nohup. Allow rules ALSO strip safe env prefixes
|
|
133
|
+
* (NODE_ENV=test go build). Deny/ask use stripAllEnvVars instead.
|
|
134
|
+
*/
|
|
135
|
+
const SAFE_WRAPPER_RE = /^(?:timeout\s+\d+(?:\.\d+)?[a-z]*)?/;
|
|
136
|
+
function stripSafeWrappers(cmd) {
|
|
137
|
+
let s = cmd.trim();
|
|
138
|
+
for (;;) {
|
|
139
|
+
const next = s
|
|
140
|
+
.replace(/^timeout\s+\S+\s+/, "")
|
|
141
|
+
.replace(/^(?:time|nice|nohup)\s+/, "")
|
|
142
|
+
.replace(SAFE_WRAPPER_RE, "");
|
|
143
|
+
if (next === s)
|
|
144
|
+
return s;
|
|
145
|
+
s = next.trim();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const SAFE_ENV_VARS = new Set([
|
|
149
|
+
"NODE_ENV", "CI", "FORCE_COLOR", "NO_COLOR", "DEBUG", "VERBOSE",
|
|
150
|
+
"GOOS", "GOARCH", "CGO_ENABLED", "PWD", "LANG", "LC_ALL", "TERM",
|
|
151
|
+
]);
|
|
152
|
+
function stripSafeEnvPrefixes(cmd) {
|
|
153
|
+
let s = cmd.trim();
|
|
154
|
+
for (;;) {
|
|
155
|
+
const m = s.match(/^([A-Za-z_][A-Za-z0-9_]*)=(\S*)\s+/);
|
|
156
|
+
if (!m || !SAFE_ENV_VARS.has(m[1]))
|
|
157
|
+
return s;
|
|
158
|
+
s = s.slice(m[0].length).trim();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Broader env-assignment pattern for deny/ask stripping (Claude parity). */
|
|
162
|
+
const ENV_VAR_PATTERN = /^([A-Za-z_][A-Za-z0-9_]*(?:\[[^\]]*\])?)\+?=(?:'[^'\n\r]*'|"(?:\\.|[^"$`\\\n\r])*"|\\.|[^ \t\n\r$`;|&()<>\\'"])*[ \t]+/;
|
|
163
|
+
/** Strip ALL leading env assignments — deny/ask rules only. Fixed-point. */
|
|
164
|
+
export function stripAllEnvVars(command) {
|
|
165
|
+
let stripped = command.trim();
|
|
166
|
+
for (;;) {
|
|
167
|
+
const m = stripped.match(ENV_VAR_PATTERN);
|
|
168
|
+
if (!m)
|
|
169
|
+
return stripped;
|
|
170
|
+
stripped = stripped.slice(m[0].length).trim();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Prepare a subcommand for matching: candidate strings to try. */
|
|
174
|
+
function candidatesFor(segment, behavior) {
|
|
175
|
+
const stripped = stripSafeWrappers(stripSafeEnvPrefixes(segment));
|
|
176
|
+
const base = [segment, stripped];
|
|
177
|
+
if (behavior === "deny" || behavior === "ask") {
|
|
178
|
+
base.push(stripAllEnvVars(stripped));
|
|
179
|
+
// fixed-point: env prefix may reveal another wrapper (`nohup FOO=bar …`)
|
|
180
|
+
base.push(stripSafeWrappers(stripAllEnvVars(stripped)));
|
|
181
|
+
}
|
|
182
|
+
return [...new Set(base.filter((c) => c.length > 0))];
|
|
183
|
+
}
|
|
184
|
+
/** Match one prepared candidate against one parsed rule. */
|
|
185
|
+
function matchOne(rule, candidate) {
|
|
186
|
+
switch (rule.type) {
|
|
187
|
+
case "exact":
|
|
188
|
+
return rule.command === candidate;
|
|
189
|
+
case "prefix":
|
|
190
|
+
if (candidate === rule.prefix)
|
|
191
|
+
return true;
|
|
192
|
+
return candidate.startsWith(rule.prefix + " ");
|
|
193
|
+
case "wildcard":
|
|
194
|
+
return matchWildcardPattern(rule.pattern, candidate);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Does this bash rule match the full command? Mirrors Claude Code's
|
|
199
|
+
* matchingRulesForInput: compound commands split, every subcommand must
|
|
200
|
+
* pass, candidates include stripped variants, and deny/ask additionally
|
|
201
|
+
* try all-env-stripped variants.
|
|
202
|
+
*/
|
|
203
|
+
export function shellRuleMatches(ruleContent, command, behavior) {
|
|
204
|
+
const rule = parseShellRule(ruleContent);
|
|
205
|
+
const trimmed = command.trim();
|
|
206
|
+
if (!trimmed)
|
|
207
|
+
return false;
|
|
208
|
+
const segments = splitSubcommands(trimmed);
|
|
209
|
+
if (segments.length === 0)
|
|
210
|
+
return false;
|
|
211
|
+
// Compound: EVERY segment must match (a deny rule needs only ONE matching
|
|
212
|
+
// segment to deny the whole; an allow rule must clear every segment).
|
|
213
|
+
if (segments.length > 1) {
|
|
214
|
+
if (behavior === "allow") {
|
|
215
|
+
return segments.every((seg) => candidatesFor(seg, behavior).some((c) => matchOne(rule, c)));
|
|
216
|
+
}
|
|
217
|
+
return segments.some((seg) => candidatesFor(seg, behavior).some((c) => matchOne(rule, c)));
|
|
218
|
+
}
|
|
219
|
+
return candidatesFor(segments[0], behavior).some((c) => matchOne(rule, c));
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=shellRules.js.map
|
|
@@ -66,15 +66,12 @@ export declare function buildStageInvocation(stage: PipelineStage, ctx: {
|
|
|
66
66
|
* `userChoseProvider` guard so a passthrough that already chose a provider is
|
|
67
67
|
* left untouched.
|
|
68
68
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* /go run's cost
|
|
72
|
-
* arrives via `YAGNI_OTEL_EXTENSION_PATH` from the launcher's gate (see the
|
|
73
|
-
* CLI's otel.ts); the capture-mode and endpoint env rides the inherited env.
|
|
69
|
+
* OTel export rides the inherited env (the launcher's `YAGNI_OTEL_EXPORT`
|
|
70
|
+
* gate plus the OTLP variables): our extension exports child LLM spend too,
|
|
71
|
+
* so stage children — the bulk of a /go run's cost — are never undercounted.
|
|
74
72
|
*/
|
|
75
73
|
export declare function groundedChildArgv(stageArgv: string[], opts: {
|
|
76
74
|
piCli: string;
|
|
77
75
|
extensionPath: string;
|
|
78
|
-
otelExtensionPath?: string;
|
|
79
76
|
}): string[];
|
|
80
77
|
//# sourceMappingURL=invocation.d.ts.map
|
|
@@ -85,11 +85,9 @@ export function buildStageInvocation(stage, ctx) {
|
|
|
85
85
|
* `userChoseProvider` guard so a passthrough that already chose a provider is
|
|
86
86
|
* left untouched.
|
|
87
87
|
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* /go run's cost
|
|
91
|
-
* arrives via `YAGNI_OTEL_EXTENSION_PATH` from the launcher's gate (see the
|
|
92
|
-
* CLI's otel.ts); the capture-mode and endpoint env rides the inherited env.
|
|
88
|
+
* OTel export rides the inherited env (the launcher's `YAGNI_OTEL_EXPORT`
|
|
89
|
+
* gate plus the OTLP variables): our extension exports child LLM spend too,
|
|
90
|
+
* so stage children — the bulk of a /go run's cost — are never undercounted.
|
|
93
91
|
*/
|
|
94
92
|
export function groundedChildArgv(stageArgv, opts) {
|
|
95
93
|
const userChoseProvider = stageArgv.includes("--provider");
|
|
@@ -97,7 +95,6 @@ export function groundedChildArgv(stageArgv, opts) {
|
|
|
97
95
|
opts.piCli,
|
|
98
96
|
"-e",
|
|
99
97
|
opts.extensionPath,
|
|
100
|
-
...(opts.otelExtensionPath ? ["-e", opts.otelExtensionPath] : []),
|
|
101
98
|
...(userChoseProvider ? [] : ["--provider", "yagni"]),
|
|
102
99
|
...stageArgv,
|
|
103
100
|
];
|
|
@@ -62,18 +62,14 @@ async function defaultWritePrompt(body) {
|
|
|
62
62
|
* Default child resolution: the parent process IS pi (so `process.argv[1]` is
|
|
63
63
|
* pi's cli), and our compiled extension entry sits one dir up from this module.
|
|
64
64
|
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* existsSync so a stale env value degrades to an untraced child, never a
|
|
69
|
-
* child that fails to boot.
|
|
65
|
+
* OTel export needs no extra child argv: the extension itself exports when
|
|
66
|
+
* the inherited env carries the launcher's `YAGNI_OTEL_EXPORT` gate (see the
|
|
67
|
+
* CLI's otel.ts), so stage children's LLM spend is traced automatically.
|
|
70
68
|
*/
|
|
71
69
|
function defaultResolveChild() {
|
|
72
70
|
const piCli = process.argv[1] ?? "pi";
|
|
73
71
|
const extensionPath = fileURLToPath(new URL("../index.js", import.meta.url));
|
|
74
|
-
|
|
75
|
-
const otelExtensionPath = otelPath && fs.existsSync(otelPath) ? otelPath : undefined;
|
|
76
|
-
return { piCli, extensionPath, ...(otelExtensionPath ? { otelExtensionPath } : {}) };
|
|
72
|
+
return { piCli, extensionPath };
|
|
77
73
|
}
|
|
78
74
|
const EMPTY_USAGE = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
|
|
79
75
|
/** Truncate to PER_TASK_OUTPUT_CAP bytes, never exceeding the cap. */
|
|
@@ -124,12 +120,8 @@ export async function runStage(stage, ctx, deps) {
|
|
|
124
120
|
lens: ctx.lens,
|
|
125
121
|
...(tierCap ? { tierCap } : {}),
|
|
126
122
|
});
|
|
127
|
-
const { piCli, extensionPath
|
|
128
|
-
const argv = groundedChildArgv(passthrough, {
|
|
129
|
-
piCli,
|
|
130
|
-
extensionPath,
|
|
131
|
-
...(otelExtensionPath ? { otelExtensionPath } : {}),
|
|
132
|
-
});
|
|
123
|
+
const { piCli, extensionPath } = resolveChild();
|
|
124
|
+
const argv = groundedChildArgv(passthrough, { piCli, extensionPath });
|
|
133
125
|
// Fold events into a fixed-size accumulator as they stream — never retain
|
|
134
126
|
// the full array (that OOMs on a verbose stage; YAG-317 follow-up).
|
|
135
127
|
const acc = newEventAccumulator();
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session's view of Claude Code plugins, and how a mid-session change
|
|
3
|
+
* reaches pi without a restart.
|
|
4
|
+
*
|
|
5
|
+
* The launcher (yagni-code-cli) owns discovery, trust, and the ledger. This
|
|
6
|
+
* module only talks to it: `yagni plugin inventory --json` (via
|
|
7
|
+
* `YAGNI_CLI_PATH`) returns what would load for the cwd right now. Reload is
|
|
8
|
+
* pi's own `/reload` flow: before calling it we rewrite this process's env so
|
|
9
|
+
* the re-created extension runtime sees the fresh MCP payload and agent dirs,
|
|
10
|
+
* and `resources_discover` hands pi the skill/prompt paths of plugins that
|
|
11
|
+
* were not in the launch argv (see panel.ts). Everything is fail-soft.
|
|
12
|
+
*/
|
|
13
|
+
import { type PluginMcpEnvEntry } from "../mcp/config.js";
|
|
14
|
+
/** Launcher → extension: the yagni CLI entry to run `plugin …` with. */
|
|
15
|
+
export declare const CLI_PATH_ENV = "YAGNI_CLI_PATH";
|
|
16
|
+
/** Launcher → extension: `name@marketplace` keys wired through argv at launch. */
|
|
17
|
+
export declare const PLUGIN_KEYS_ENV = "YAGNI_CLAUDE_PLUGIN_KEYS";
|
|
18
|
+
/** Set by /reload-plugins before pi reloads: the fresh inventory (JSON). */
|
|
19
|
+
export declare const INVENTORY_ENV = "YAGNI_CLAUDE_PLUGIN_INVENTORY";
|
|
20
|
+
export interface InventoryPlugin {
|
|
21
|
+
key: string;
|
|
22
|
+
name: string;
|
|
23
|
+
origin: "yagni" | "claude-code" | "repo-marketplace";
|
|
24
|
+
version: string | null;
|
|
25
|
+
root: string;
|
|
26
|
+
gated: boolean;
|
|
27
|
+
skillPaths: string[];
|
|
28
|
+
commandPaths: string[];
|
|
29
|
+
agentDirs: string[];
|
|
30
|
+
mcpServers: string[];
|
|
31
|
+
unsupported: string[];
|
|
32
|
+
}
|
|
33
|
+
export interface PluginInventory {
|
|
34
|
+
plugins: InventoryPlugin[];
|
|
35
|
+
mcp: PluginMcpEnvEntry[];
|
|
36
|
+
pendingTrust: string[];
|
|
37
|
+
agentDirs: string[];
|
|
38
|
+
}
|
|
39
|
+
export interface CliResult {
|
|
40
|
+
code: number;
|
|
41
|
+
stdout: string;
|
|
42
|
+
stderr: string;
|
|
43
|
+
}
|
|
44
|
+
export type CliRunner = (args: string[], opts: {
|
|
45
|
+
cliPath: string;
|
|
46
|
+
cwd: string;
|
|
47
|
+
env: NodeJS.ProcessEnv;
|
|
48
|
+
}) => Promise<CliResult>;
|
|
49
|
+
export declare const defaultCliRunner: CliRunner;
|
|
50
|
+
export declare function cliPath(env: NodeJS.ProcessEnv): string | null;
|
|
51
|
+
export declare function launchKeys(env: NodeJS.ProcessEnv): Set<string>;
|
|
52
|
+
/** The inventory recorded by the last /reload-plugins in this process, if any. */
|
|
53
|
+
export declare function storedInventory(env: NodeJS.ProcessEnv): PluginInventory | null;
|
|
54
|
+
export declare function parseInventory(value: unknown): PluginInventory;
|
|
55
|
+
export declare function fetchInventory(opts: {
|
|
56
|
+
cwd: string;
|
|
57
|
+
env: NodeJS.ProcessEnv;
|
|
58
|
+
runCli?: CliRunner;
|
|
59
|
+
}): Promise<PluginInventory>;
|
|
60
|
+
/**
|
|
61
|
+
* The plugins the session knows about right now: launch-time keys plus
|
|
62
|
+
* anything a previous /reload-plugins in this process brought in.
|
|
63
|
+
*/
|
|
64
|
+
export declare function knownKeys(env: NodeJS.ProcessEnv): Set<string>;
|
|
65
|
+
export interface ReloadPlan {
|
|
66
|
+
added: InventoryPlugin[];
|
|
67
|
+
removed: string[];
|
|
68
|
+
/** Env values to set on this process before pi reloads. */
|
|
69
|
+
env: Record<string, string | undefined>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Diff a fresh inventory against what the session already has, and the env
|
|
73
|
+
* rewrite that makes the reloaded runtime pick the fresh state up: the MCP
|
|
74
|
+
* payload and agent dirs are re-read by the extension on startup, and the
|
|
75
|
+
* inventory itself feeds `resources_discover` for skills/prompts.
|
|
76
|
+
*/
|
|
77
|
+
export declare function planReload(inventory: PluginInventory, env: NodeJS.ProcessEnv): ReloadPlan;
|
|
78
|
+
export declare function applyEnv(target: NodeJS.ProcessEnv, values: Record<string, string | undefined>): void;
|
|
79
|
+
/**
|
|
80
|
+
* Skill/prompt paths pi does not already hold through the launch argv: the
|
|
81
|
+
* plugins that arrived via /reload-plugins. Returned from `resources_discover`
|
|
82
|
+
* on every reason so a `/new` after a reload keeps them too.
|
|
83
|
+
*/
|
|
84
|
+
export declare function discoverPaths(env: NodeJS.ProcessEnv): {
|
|
85
|
+
skillPaths: string[];
|
|
86
|
+
promptPaths: string[];
|
|
87
|
+
};
|
|
88
|
+
//# sourceMappingURL=inventory.d.ts.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The session's view of Claude Code plugins, and how a mid-session change
|
|
3
|
+
* reaches pi without a restart.
|
|
4
|
+
*
|
|
5
|
+
* The launcher (yagni-code-cli) owns discovery, trust, and the ledger. This
|
|
6
|
+
* module only talks to it: `yagni plugin inventory --json` (via
|
|
7
|
+
* `YAGNI_CLI_PATH`) returns what would load for the cwd right now. Reload is
|
|
8
|
+
* pi's own `/reload` flow: before calling it we rewrite this process's env so
|
|
9
|
+
* the re-created extension runtime sees the fresh MCP payload and agent dirs,
|
|
10
|
+
* and `resources_discover` hands pi the skill/prompt paths of plugins that
|
|
11
|
+
* were not in the launch argv (see panel.ts). Everything is fail-soft.
|
|
12
|
+
*/
|
|
13
|
+
import { execFile } from "node:child_process";
|
|
14
|
+
import { delimiter } from "node:path";
|
|
15
|
+
import { PLUGIN_MCP_ENV } from "../mcp/config.js";
|
|
16
|
+
import { CLAUDE_AGENT_DIRS_ENV } from "../subagents.js";
|
|
17
|
+
/** Launcher → extension: the yagni CLI entry to run `plugin …` with. */
|
|
18
|
+
export const CLI_PATH_ENV = "YAGNI_CLI_PATH";
|
|
19
|
+
/** Launcher → extension: `name@marketplace` keys wired through argv at launch. */
|
|
20
|
+
export const PLUGIN_KEYS_ENV = "YAGNI_CLAUDE_PLUGIN_KEYS";
|
|
21
|
+
/** Set by /reload-plugins before pi reloads: the fresh inventory (JSON). */
|
|
22
|
+
export const INVENTORY_ENV = "YAGNI_CLAUDE_PLUGIN_INVENTORY";
|
|
23
|
+
export const defaultCliRunner = (args, opts) => new Promise((resolve) => {
|
|
24
|
+
execFile(process.execPath, [opts.cliPath, ...args], { cwd: opts.cwd, env: { ...opts.env, YAGNI_DISABLE_UPDATE_CHECK: "1" }, timeout: 120_000, maxBuffer: 8 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
25
|
+
const code = err && typeof err.code === "number" ? (err.code) : err ? 1 : 0;
|
|
26
|
+
resolve({ code, stdout: String(stdout ?? ""), stderr: String(stderr ?? (err?.message ?? "")) });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
export function cliPath(env) {
|
|
30
|
+
const value = env[CLI_PATH_ENV];
|
|
31
|
+
return value && value.length > 0 ? value : null;
|
|
32
|
+
}
|
|
33
|
+
export function launchKeys(env) {
|
|
34
|
+
const raw = env[PLUGIN_KEYS_ENV];
|
|
35
|
+
return new Set(raw ? raw.split(delimiter).filter(Boolean) : []);
|
|
36
|
+
}
|
|
37
|
+
/** The inventory recorded by the last /reload-plugins in this process, if any. */
|
|
38
|
+
export function storedInventory(env) {
|
|
39
|
+
const raw = env[INVENTORY_ENV];
|
|
40
|
+
if (!raw)
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
return parseInventory(JSON.parse(raw));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function parseInventory(value) {
|
|
50
|
+
if (!value || typeof value !== "object")
|
|
51
|
+
throw new Error("inventory is not an object");
|
|
52
|
+
const v = value;
|
|
53
|
+
const plugins = Array.isArray(v.plugins) ? v.plugins.filter((p) => !!p && typeof p === "object" && typeof p.key === "string") : [];
|
|
54
|
+
return {
|
|
55
|
+
plugins: plugins.map((p) => ({
|
|
56
|
+
key: p.key,
|
|
57
|
+
name: typeof p.name === "string" ? p.name : p.key,
|
|
58
|
+
origin: p.origin === "yagni" || p.origin === "repo-marketplace" ? p.origin : "claude-code",
|
|
59
|
+
version: typeof p.version === "string" ? p.version : null,
|
|
60
|
+
root: typeof p.root === "string" ? p.root : "",
|
|
61
|
+
gated: p.gated === true,
|
|
62
|
+
skillPaths: stringList(p.skillPaths),
|
|
63
|
+
commandPaths: stringList(p.commandPaths),
|
|
64
|
+
agentDirs: stringList(p.agentDirs),
|
|
65
|
+
mcpServers: stringList(p.mcpServers),
|
|
66
|
+
unsupported: stringList(p.unsupported),
|
|
67
|
+
})),
|
|
68
|
+
mcp: Array.isArray(v.mcp) ? v.mcp : [],
|
|
69
|
+
pendingTrust: stringList(v.pendingTrust),
|
|
70
|
+
agentDirs: stringList(v.agentDirs),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function stringList(value) {
|
|
74
|
+
return Array.isArray(value) ? value.filter((s) => typeof s === "string") : [];
|
|
75
|
+
}
|
|
76
|
+
export async function fetchInventory(opts) {
|
|
77
|
+
const path = cliPath(opts.env);
|
|
78
|
+
if (!path)
|
|
79
|
+
throw new Error("This session was not started by the yagni launcher (YAGNI_CLI_PATH is unset); run `yagni plugin …` in a terminal instead.");
|
|
80
|
+
const result = await (opts.runCli ?? defaultCliRunner)(["plugin", "inventory", "--json"], { cliPath: path, cwd: opts.cwd, env: opts.env });
|
|
81
|
+
if (result.code !== 0)
|
|
82
|
+
throw new Error(result.stderr.trim() || `yagni plugin inventory exited ${result.code}`);
|
|
83
|
+
return parseInventory(JSON.parse(result.stdout));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The plugins the session knows about right now: launch-time keys plus
|
|
87
|
+
* anything a previous /reload-plugins in this process brought in.
|
|
88
|
+
*/
|
|
89
|
+
export function knownKeys(env) {
|
|
90
|
+
const keys = launchKeys(env);
|
|
91
|
+
for (const p of storedInventory(env)?.plugins ?? [])
|
|
92
|
+
keys.add(p.key);
|
|
93
|
+
return keys;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Diff a fresh inventory against what the session already has, and the env
|
|
97
|
+
* rewrite that makes the reloaded runtime pick the fresh state up: the MCP
|
|
98
|
+
* payload and agent dirs are re-read by the extension on startup, and the
|
|
99
|
+
* inventory itself feeds `resources_discover` for skills/prompts.
|
|
100
|
+
*/
|
|
101
|
+
export function planReload(inventory, env) {
|
|
102
|
+
const before = knownKeys(env);
|
|
103
|
+
const now = new Set(inventory.plugins.map((p) => p.key));
|
|
104
|
+
const added = inventory.plugins.filter((p) => !before.has(p.key));
|
|
105
|
+
const removed = [...before].filter((key) => !now.has(key));
|
|
106
|
+
return {
|
|
107
|
+
added,
|
|
108
|
+
removed,
|
|
109
|
+
env: {
|
|
110
|
+
[PLUGIN_MCP_ENV]: inventory.mcp.length > 0 ? JSON.stringify(inventory.mcp) : undefined,
|
|
111
|
+
[CLAUDE_AGENT_DIRS_ENV]: inventory.agentDirs.length > 0 ? inventory.agentDirs.join(delimiter) : undefined,
|
|
112
|
+
[INVENTORY_ENV]: JSON.stringify(inventory),
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function applyEnv(target, values) {
|
|
117
|
+
for (const [key, value] of Object.entries(values)) {
|
|
118
|
+
if (value === undefined)
|
|
119
|
+
delete target[key];
|
|
120
|
+
else
|
|
121
|
+
target[key] = value;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Skill/prompt paths pi does not already hold through the launch argv: the
|
|
126
|
+
* plugins that arrived via /reload-plugins. Returned from `resources_discover`
|
|
127
|
+
* on every reason so a `/new` after a reload keeps them too.
|
|
128
|
+
*/
|
|
129
|
+
export function discoverPaths(env) {
|
|
130
|
+
const inventory = storedInventory(env);
|
|
131
|
+
if (!inventory)
|
|
132
|
+
return { skillPaths: [], promptPaths: [] };
|
|
133
|
+
const launched = launchKeys(env);
|
|
134
|
+
const skillPaths = [];
|
|
135
|
+
const promptPaths = [];
|
|
136
|
+
for (const plugin of inventory.plugins) {
|
|
137
|
+
if (launched.has(plugin.key))
|
|
138
|
+
continue;
|
|
139
|
+
skillPaths.push(...plugin.skillPaths);
|
|
140
|
+
promptPaths.push(...plugin.commandPaths);
|
|
141
|
+
}
|
|
142
|
+
return { skillPaths, promptPaths };
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=inventory.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/plugin` and `/reload-plugins` — Claude Code marketplace plugins managed
|
|
3
|
+
* from inside a session.
|
|
4
|
+
*
|
|
5
|
+
* `/plugin` mirrors Claude Code's panel: the installed inventory (origin,
|
|
6
|
+
* version, what loads, what is not bridged), the marketplace catalog with
|
|
7
|
+
* one-keystroke install, and per-plugin uninstall/enable/disable. Every
|
|
8
|
+
* mutation runs the launcher's `yagni plugin …` (the single owner of the
|
|
9
|
+
* ledger, trust, and discovery) and then reloads.
|
|
10
|
+
*
|
|
11
|
+
* `/reload-plugins` is pi's own `/reload` with the plugin state refreshed
|
|
12
|
+
* first: MCP servers and agents re-read their env on the new runtime, and
|
|
13
|
+
* skills/commands of plugins that were not in the launch argv reach pi
|
|
14
|
+
* through `resources_discover`. Removed plugins stop contributing MCP servers
|
|
15
|
+
* and agents at once; their skills/commands stay until a restart, and the
|
|
16
|
+
* summary says so.
|
|
17
|
+
*/
|
|
18
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { type CliRunner, type InventoryPlugin, type PluginInventory, storedInventory } from "./inventory.js";
|
|
20
|
+
export type PluginCliRunner = CliRunner;
|
|
21
|
+
export interface PluginPanelDeps {
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
runCli?: CliRunner;
|
|
24
|
+
}
|
|
25
|
+
export declare function describePlugin(p: InventoryPlugin): string;
|
|
26
|
+
export declare function renderInventory(inventory: PluginInventory, env: NodeJS.ProcessEnv): string;
|
|
27
|
+
export declare function renderReloadSummary(added: InventoryPlugin[], removed: string[]): string;
|
|
28
|
+
/** Wire /plugin, /reload-plugins, and the resources_discover contribution. */
|
|
29
|
+
export declare function registerPluginPanel(pi: ExtensionAPI, deps?: PluginPanelDeps): void;
|
|
30
|
+
export interface CatalogMarketplace {
|
|
31
|
+
name: string;
|
|
32
|
+
plugins: {
|
|
33
|
+
name: string;
|
|
34
|
+
version: string | null;
|
|
35
|
+
installable: boolean;
|
|
36
|
+
}[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse `yagni plugin list --available` text (the CLI's stable, tested
|
|
40
|
+
* shape: `<marketplace>:` headers, then ` <name> v<ver> [flags]` lines).
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseCatalog(text: string): CatalogMarketplace[];
|
|
43
|
+
/** Exported for tests: the current stored inventory, if a reload happened. */
|
|
44
|
+
export { storedInventory };
|
|
45
|
+
//# sourceMappingURL=panel.d.ts.map
|