@yagni-app/code 1.0.5 → 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 +89 -27
- package/dist/claudeCompat.d.ts +59 -0
- package/dist/claudeCompat.js +109 -2
- package/dist/claudePlugins.d.ts +45 -3
- package/dist/claudePlugins.js +129 -19
- package/dist/cli.js +28 -4
- package/dist/doctor.d.ts +42 -4
- package/dist/doctor.js +150 -15
- package/dist/extension/condensedTools.d.ts +12 -1
- package/dist/extension/condensedTools.js +17 -9
- package/dist/extension/index.d.ts +18 -5
- package/dist/extension/index.js +319 -72
- package/dist/extension/mcp/approval.d.ts +45 -0
- package/dist/extension/mcp/approval.js +164 -0
- package/dist/extension/mcp/auth.d.ts +124 -0
- package/dist/extension/mcp/auth.js +560 -0
- package/dist/extension/mcp/authStore.d.ts +61 -0
- package/dist/extension/mcp/authStore.js +105 -0
- package/dist/extension/mcp/callbackPage.d.ts +31 -0
- package/dist/extension/mcp/callbackPage.js +222 -0
- package/dist/extension/mcp/cliConfig.d.ts +12 -0
- package/dist/extension/mcp/cliConfig.js +12 -0
- package/dist/extension/mcp/config.d.ts +153 -0
- package/dist/extension/mcp/config.js +381 -0
- package/dist/extension/mcp/log.d.ts +28 -0
- package/dist/extension/mcp/log.js +82 -0
- package/dist/extension/mcp/manager.d.ts +100 -0
- package/dist/extension/mcp/manager.js +273 -0
- package/dist/extension/mcp/names.d.ts +25 -0
- package/dist/extension/mcp/names.js +40 -0
- package/dist/extension/mcp/panel.d.ts +33 -0
- package/dist/extension/mcp/panel.js +268 -0
- package/dist/extension/mcp/prompts.d.ts +23 -0
- package/dist/extension/mcp/prompts.js +93 -0
- package/dist/extension/mcp/startup.d.ts +55 -0
- package/dist/extension/mcp/startup.js +152 -0
- package/dist/extension/mcp/tools.d.ts +31 -0
- package/dist/extension/mcp/tools.js +117 -0
- package/dist/extension/mcp/transports.d.ts +17 -0
- package/dist/extension/mcp/transports.js +44 -0
- package/dist/extension/permission/gate.d.ts +104 -2
- package/dist/extension/permission/gate.js +387 -31
- package/dist/extension/permission/guardian.d.ts +24 -5
- package/dist/extension/permission/guardian.js +162 -24
- 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/personas.js +5 -0
- 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 +122 -0
- package/dist/mcpCommand.js +787 -0
- package/dist/otel.d.ts +69 -63
- package/dist/otel.js +154 -119
- 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.d.ts +11 -2
- package/dist/upgrade.js +48 -8
- package/package.json +20 -3
- package/dist/extension/mcpTools.d.ts +0 -57
- package/dist/extension/mcpTools.js +0 -132
|
@@ -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
|
|
@@ -0,0 +1,293 @@
|
|
|
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 { applyEnv, cliPath, defaultCliRunner, discoverPaths, fetchInventory, planReload, storedInventory, } from "./inventory.js";
|
|
19
|
+
const USAGE = [
|
|
20
|
+
"/plugin Browse installed plugins and marketplaces.",
|
|
21
|
+
"/plugin list Show what loads here.",
|
|
22
|
+
"/plugin install <plugin>[@marketplace] [-s user|project]",
|
|
23
|
+
"/plugin uninstall <plugin> (also: enable, disable, update [plugin])",
|
|
24
|
+
"/plugin marketplace add <owner/repo | git-url | path> (also: list, update, remove)",
|
|
25
|
+
"/reload-plugins Apply plugin changes without restarting.",
|
|
26
|
+
].join("\n");
|
|
27
|
+
const MUTATING = new Set(["install", "add", "uninstall", "remove", "enable", "disable", "update"]);
|
|
28
|
+
export function describePlugin(p) {
|
|
29
|
+
const parts = [];
|
|
30
|
+
if (p.skillPaths.length)
|
|
31
|
+
parts.push(`${p.skillPaths.length} skill dir${p.skillPaths.length === 1 ? "" : "s"}`);
|
|
32
|
+
if (p.commandPaths.length)
|
|
33
|
+
parts.push(`${p.commandPaths.length} command dir${p.commandPaths.length === 1 ? "" : "s"}`);
|
|
34
|
+
if (p.agentDirs.length)
|
|
35
|
+
parts.push(`${p.agentDirs.length} agent dir${p.agentDirs.length === 1 ? "" : "s"}`);
|
|
36
|
+
if (p.mcpServers.length)
|
|
37
|
+
parts.push(`MCP: ${p.mcpServers.join(", ")}`);
|
|
38
|
+
const origin = p.origin === "yagni" ? "installed via yagni" : p.origin === "claude-code" ? "mirrored from Claude Code" : "from this repo's marketplace";
|
|
39
|
+
const lines = [`${p.key}${p.version ? ` v${p.version}` : ""} ${origin}`, ` ${parts.length ? parts.join(" · ") : "nothing loadable"}`];
|
|
40
|
+
if (p.unsupported.length)
|
|
41
|
+
lines.push(` not bridged: ${p.unsupported.join(", ")}`);
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
export function renderInventory(inventory, env) {
|
|
45
|
+
const lines = [];
|
|
46
|
+
if (inventory.plugins.length === 0) {
|
|
47
|
+
lines.push("No plugins load here.");
|
|
48
|
+
lines.push("Add a marketplace with `/plugin marketplace add <owner/repo | git-url | path>`, then `/plugin install <plugin>`.");
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
lines.push(`Plugins (${inventory.plugins.length}):`);
|
|
52
|
+
for (const p of inventory.plugins)
|
|
53
|
+
lines.push(describePlugin(p));
|
|
54
|
+
}
|
|
55
|
+
if (inventory.pendingTrust.length > 0) {
|
|
56
|
+
lines.push(`Held back until this folder is trusted: ${inventory.pendingTrust.join(", ")}`);
|
|
57
|
+
}
|
|
58
|
+
const stale = staleSince(inventory, env);
|
|
59
|
+
if (stale.added.length > 0 || stale.removed.length > 0) {
|
|
60
|
+
lines.push(`Not yet applied to this session: ${[...stale.added.map((k) => `+${k}`), ...stale.removed.map((k) => `-${k}`)].join(", ")} — run /reload-plugins.`);
|
|
61
|
+
}
|
|
62
|
+
return lines.join("\n");
|
|
63
|
+
}
|
|
64
|
+
/** Plugins the inventory has that the session does not (and vice versa). */
|
|
65
|
+
function staleSince(inventory, env) {
|
|
66
|
+
const plan = planReload(inventory, env);
|
|
67
|
+
return { added: plan.added.map((p) => p.key), removed: plan.removed };
|
|
68
|
+
}
|
|
69
|
+
export function renderReloadSummary(added, removed) {
|
|
70
|
+
const lines = [];
|
|
71
|
+
if (added.length === 0 && removed.length === 0) {
|
|
72
|
+
lines.push("Plugins reloaded — nothing changed since launch.");
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
|
75
|
+
if (added.length > 0) {
|
|
76
|
+
lines.push(`Loaded: ${added.map((p) => p.key).join(", ")}`);
|
|
77
|
+
const mcp = added.flatMap((p) => p.mcpServers);
|
|
78
|
+
if (mcp.length > 0)
|
|
79
|
+
lines.push(` MCP servers connecting: ${mcp.join(", ")} (see /mcp)`);
|
|
80
|
+
const commands = added.filter((p) => p.commandPaths.length > 0).map((p) => p.key);
|
|
81
|
+
if (commands.length > 0)
|
|
82
|
+
lines.push(` Commands and skills from ${commands.join(", ")} are available now.`);
|
|
83
|
+
}
|
|
84
|
+
if (removed.length > 0) {
|
|
85
|
+
lines.push(`Removed: ${removed.join(", ")}`);
|
|
86
|
+
lines.push(" Their MCP servers and agents are gone now; skills and commands stay until you restart (`yagni -c` keeps this conversation).");
|
|
87
|
+
}
|
|
88
|
+
return lines.join("\n");
|
|
89
|
+
}
|
|
90
|
+
/** Wire /plugin, /reload-plugins, and the resources_discover contribution. */
|
|
91
|
+
export function registerPluginPanel(pi, deps = {}) {
|
|
92
|
+
const env = deps.env ?? process.env;
|
|
93
|
+
const runCli = deps.runCli ?? defaultCliRunner;
|
|
94
|
+
// Skills/prompts of plugins that arrived via /reload-plugins: pi re-runs
|
|
95
|
+
// this on every reload and session start, so they persist for the process.
|
|
96
|
+
pi.on("resources_discover", async () => discoverPaths(env));
|
|
97
|
+
async function runPlugin(args, cwd, ui) {
|
|
98
|
+
const path = cliPath(env);
|
|
99
|
+
if (!path) {
|
|
100
|
+
ui.notify("Plugin management needs the yagni launcher (YAGNI_CLI_PATH is unset in this session). Run `yagni plugin …` in a terminal.", "warning");
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const result = await runCli(["plugin", ...args], { cliPath: path, cwd, env });
|
|
104
|
+
const out = result.stdout.trim();
|
|
105
|
+
const err = result.stderr.trim();
|
|
106
|
+
if (result.code === 0) {
|
|
107
|
+
if (out)
|
|
108
|
+
ui.notify(out, "info");
|
|
109
|
+
if (err)
|
|
110
|
+
ui.notify(err, "warning");
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
ui.notify(err || out || `yagni plugin ${args.join(" ")} failed (exit ${result.code})`, "error");
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
async function reload(ctx) {
|
|
117
|
+
let inventory;
|
|
118
|
+
try {
|
|
119
|
+
inventory = await fetchInventory({ cwd: ctx.cwd, env, runCli });
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
ctx.ui.notify(`Could not read the plugin inventory — ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const plan = planReload(inventory, env);
|
|
126
|
+
applyEnv(env, plan.env);
|
|
127
|
+
ctx.ui.notify(renderReloadSummary(plan.added, plan.removed), "info");
|
|
128
|
+
// Treat reload as terminal for this handler (pi docs): the runtime that
|
|
129
|
+
// registered us is torn down and re-created from the rewritten env.
|
|
130
|
+
await ctx.reload();
|
|
131
|
+
}
|
|
132
|
+
async function listing(ctx) {
|
|
133
|
+
try {
|
|
134
|
+
const inventory = await fetchInventory({ cwd: ctx.cwd, env, runCli });
|
|
135
|
+
return inventory;
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
ctx.ui.notify(err instanceof Error ? err.message : String(err), "error");
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
pi.registerCommand("reload-plugins", {
|
|
143
|
+
description: "Apply plugin installs/removals to this session without restarting.",
|
|
144
|
+
handler: async (_args, ctx) => {
|
|
145
|
+
await reload(ctx);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
pi.registerCommand("plugin", {
|
|
149
|
+
description: "Manage Claude Code plugins: browse marketplaces, install, uninstall, enable/disable.",
|
|
150
|
+
handler: async (args, rawCtx) => {
|
|
151
|
+
const ctx = rawCtx;
|
|
152
|
+
const sub = args.trim().split(/\s+/).filter(Boolean);
|
|
153
|
+
const [verb] = sub;
|
|
154
|
+
if (verb === "help" || verb === "--help" || verb === "-h") {
|
|
155
|
+
ctx.ui.notify(USAGE, "info");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (verb === "list" || (!verb && !ctx.hasUI)) {
|
|
159
|
+
const inventory = await listing(ctx);
|
|
160
|
+
if (inventory)
|
|
161
|
+
ctx.ui.notify(renderInventory(inventory, env), "info");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!verb) {
|
|
165
|
+
await interactive(ctx);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (verb === "reload") {
|
|
169
|
+
await reload(ctx);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Everything else is the CLI's grammar verbatim (install, uninstall,
|
|
173
|
+
// enable, disable, update, marketplace …); mutations reload afterwards.
|
|
174
|
+
const ok = await runPlugin(sub, ctx.cwd, ctx.ui);
|
|
175
|
+
const mutates = MUTATING.has(verb) || (verb === "marketplace" && sub[1] !== "list");
|
|
176
|
+
if (ok && mutates)
|
|
177
|
+
await reload(ctx);
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
async function interactive(ctx) {
|
|
181
|
+
const inventory = await listing(ctx);
|
|
182
|
+
if (!inventory)
|
|
183
|
+
return;
|
|
184
|
+
const choice = await ctx.ui.select("Plugins", [`Installed plugins (${inventory.plugins.length})`, "Browse marketplaces", "Update all", "Reload plugins", "Exit"], ctx.signal ? { signal: ctx.signal } : undefined);
|
|
185
|
+
if (!choice || choice === "Exit")
|
|
186
|
+
return;
|
|
187
|
+
if (choice.startsWith("Installed")) {
|
|
188
|
+
await installedMenu(ctx, inventory);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (choice === "Browse marketplaces") {
|
|
192
|
+
await browseMenu(ctx, inventory);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (choice === "Update all") {
|
|
196
|
+
if (await runPlugin(["update"], ctx.cwd, ctx.ui))
|
|
197
|
+
await reload(ctx);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (choice === "Reload plugins") {
|
|
201
|
+
await reload(ctx);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function installedMenu(ctx, inventory) {
|
|
206
|
+
if (inventory.plugins.length === 0) {
|
|
207
|
+
ctx.ui.notify(renderInventory(inventory, env), "info");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const options = inventory.plugins.map((p) => `${p.key}${p.version ? ` · v${p.version}` : ""}`);
|
|
211
|
+
const choice = await ctx.ui.select("Installed plugins", [...options, "Back"], ctx.signal ? { signal: ctx.signal } : undefined);
|
|
212
|
+
if (!choice || choice === "Back")
|
|
213
|
+
return;
|
|
214
|
+
const plugin = inventory.plugins[options.indexOf(choice)];
|
|
215
|
+
if (!plugin)
|
|
216
|
+
return;
|
|
217
|
+
const actions = ["View details"];
|
|
218
|
+
if (plugin.origin === "yagni")
|
|
219
|
+
actions.push("Uninstall", "Disable");
|
|
220
|
+
actions.push("Back");
|
|
221
|
+
const action = await ctx.ui.select(describePlugin(plugin).split("\n")[0], actions, ctx.signal ? { signal: ctx.signal } : undefined);
|
|
222
|
+
if (!action || action === "Back")
|
|
223
|
+
return;
|
|
224
|
+
if (action === "View details") {
|
|
225
|
+
ctx.ui.notify(describePlugin(plugin), "info");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (action === "Uninstall") {
|
|
229
|
+
if (await runPlugin(["uninstall", plugin.key], ctx.cwd, ctx.ui))
|
|
230
|
+
await reload(ctx);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (action === "Disable") {
|
|
234
|
+
if (await runPlugin(["disable", plugin.key], ctx.cwd, ctx.ui))
|
|
235
|
+
await reload(ctx);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function browseMenu(ctx, inventory) {
|
|
239
|
+
const path = cliPath(env);
|
|
240
|
+
if (!path) {
|
|
241
|
+
ctx.ui.notify("Plugin management needs the yagni launcher (YAGNI_CLI_PATH is unset in this session).", "warning");
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const result = await runCli(["plugin", "list", "--available"], { cliPath: path, cwd: ctx.cwd, env });
|
|
245
|
+
const catalog = parseCatalog(result.stdout);
|
|
246
|
+
if (catalog.length === 0) {
|
|
247
|
+
ctx.ui.notify(result.stdout.trim() || "No marketplaces added. Use `/plugin marketplace add <owner/repo | git-url | path>`.", "info");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const installed = new Set(inventory.plugins.map((p) => p.key));
|
|
251
|
+
const entries = catalog.flatMap((mp) => mp.plugins.map((p) => ({ key: `${p.name}@${mp.name}`, label: `${p.name}@${mp.name}${p.version ? ` · v${p.version}` : ""}${installed.has(`${p.name}@${mp.name}`) ? " (installed)" : ""}${p.installable ? "" : " (remote source, not installable)"}` })));
|
|
252
|
+
const choice = await ctx.ui.select("Marketplace plugins", [...entries.map((e) => e.label), "Back"], ctx.signal ? { signal: ctx.signal } : undefined);
|
|
253
|
+
if (!choice || choice === "Back")
|
|
254
|
+
return;
|
|
255
|
+
const entry = entries[entries.map((e) => e.label).indexOf(choice)];
|
|
256
|
+
if (!entry)
|
|
257
|
+
return;
|
|
258
|
+
const scope = await ctx.ui.select(`Install ${entry.key}`, ["For all my projects (user)", "Only this repo (project)", "Back"], ctx.signal ? { signal: ctx.signal } : undefined);
|
|
259
|
+
if (!scope || scope === "Back")
|
|
260
|
+
return;
|
|
261
|
+
const args = ["install", entry.key, ...(scope.includes("project") ? ["-s", "project"] : [])];
|
|
262
|
+
if (await runPlugin(args, ctx.cwd, ctx.ui))
|
|
263
|
+
await reload(ctx);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Parse `yagni plugin list --available` text (the CLI's stable, tested
|
|
268
|
+
* shape: `<marketplace>:` headers, then ` <name> v<ver> [flags]` lines).
|
|
269
|
+
*/
|
|
270
|
+
export function parseCatalog(text) {
|
|
271
|
+
const out = [];
|
|
272
|
+
let current = null;
|
|
273
|
+
for (const raw of text.split("\n")) {
|
|
274
|
+
const header = /^(\S.*):$/.exec(raw);
|
|
275
|
+
if (header) {
|
|
276
|
+
current = { name: header[1], plugins: [] };
|
|
277
|
+
out.push(current);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const entry = /^ (\S+)(?:\s+v(\S+))?(?:\s+\[([^\]]*)\])?\s*$/.exec(raw);
|
|
281
|
+
if (entry && current) {
|
|
282
|
+
current.plugins.push({
|
|
283
|
+
name: entry[1],
|
|
284
|
+
version: entry[2] ?? null,
|
|
285
|
+
installable: !(entry[3] ?? "").includes("not installable"),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return out;
|
|
290
|
+
}
|
|
291
|
+
/** Exported for tests: the current stored inventory, if a reload happened. */
|
|
292
|
+
export { storedInventory };
|
|
293
|
+
//# sourceMappingURL=panel.js.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash-tool sandbox wiring: decides whether a bash command runs
|
|
3
|
+
* sandboxed, and provides the spawnHook that wraps the command with the OS
|
|
4
|
+
* sandbox (srt) while pi's local exec backend keeps owning process
|
|
5
|
+
* lifecycle, timeout, and abort handling.
|
|
6
|
+
*
|
|
7
|
+
* The wrap uses pi's BashToolOptions.spawnHook (command/cwd/env rewrite
|
|
8
|
+
* before spawn) rather than replacing BashOperations wholesale — pi's
|
|
9
|
+
* battle-tested exec backend stays in charge; only the command string and
|
|
10
|
+
* proxy env change. user_bash (! commands) routes through the same wrap.
|
|
11
|
+
*
|
|
12
|
+
`dangerouslyDisableSandbox` mirrors Claude Code's bash tool parameter: the
|
|
13
|
+
* model may set it when it has evidence a sandbox restriction caused a
|
|
14
|
+
* failure; the parameter NEVER skips the permission gate (gate.ts routes the
|
|
15
|
+
* retry through the normal execPolicy/Guardian flow) — it only skips the OS
|
|
16
|
+
* wrap, and only when sandbox.allowUnsandboxedCommands is true.
|
|
17
|
+
*
|
|
18
|
+
* Child-process stdio-grace handling derived from pi-sandbox (MIT,
|
|
19
|
+
* carderne/pi-sandbox src/sandbox-runtime.ts) — not needed here because pi's
|
|
20
|
+
* local backend owns the child; the reference is credited for the operational
|
|
21
|
+
* lessons (process-group kill, stdio release).
|
|
22
|
+
*/
|
|
23
|
+
import type { BashSpawnHook, BashToolInput } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import type { YagniSandboxManager } from "./manager.js";
|
|
25
|
+
import type { SandboxSettings } from "./config.js";
|
|
26
|
+
export interface SandboxBashInput extends BashToolInput {
|
|
27
|
+
/** Claude Code parity: model-requested unsandboxed retry. */
|
|
28
|
+
dangerouslyDisableSandbox?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Does an excludedCommands pattern match any segment of the command?
|
|
32
|
+
* (Compound-aware: "echo hi && docker ps" must not ride an "echo:*" exclude.)
|
|
33
|
+
*/
|
|
34
|
+
export declare function commandHasExcludedSegment(command: string, excludedPatterns: readonly string[]): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Should this bash tool call run inside the OS sandbox?
|
|
37
|
+
* Pure — reads the decision inputs, no I/O.
|
|
38
|
+
*/
|
|
39
|
+
export declare function shouldUseSandbox(input: Pick<SandboxBashInput, "command" | "dangerouslyDisableSandbox">, manager: Pick<YagniSandboxManager, "initialized">, settings: Pick<SandboxSettings, "allowUnsandboxedCommands" | "excludedCommands">): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Build the spawnHook that wraps the command with the sandbox. The hook is
|
|
42
|
+
* async-ignoring by design: spawnHook is sync in pi's interface, so the
|
|
43
|
+
* command wrap must already be resolved — use preWrappedCommand() below to
|
|
44
|
+
* pre-wrap and stash, or build the hook from a pre-wrapped string.
|
|
45
|
+
*
|
|
46
|
+
* In practice we pre-wrap in tool_call (async, where we can await the wrap)
|
|
47
|
+
* by mutating event.input.command to the wrapped string, and the spawnHook
|
|
48
|
+
* only injects proxy env. That keeps a single code path for tool calls.
|
|
49
|
+
*/
|
|
50
|
+
/**
|
|
51
|
+
* Env additions every sandboxed process needs (srt injects proxy vars into
|
|
52
|
+
* the WRAPPED command's env; the spawnHook passes them through).
|
|
53
|
+
*/
|
|
54
|
+
export declare function sandboxSpawnEnv(manager: YagniSandboxManager): Record<string, string>;
|
|
55
|
+
/**
|
|
56
|
+
* spawnHook for the sandboxed bash tool: injects proxy-support env vars.
|
|
57
|
+
* The command itself was already wrapped (pre-wrap in tool_call, async).
|
|
58
|
+
*/
|
|
59
|
+
export declare function makeSandboxSpawnHook(manager: YagniSandboxManager): BashSpawnHook;
|
|
60
|
+
/**
|
|
61
|
+
* Wrap a raw command for OS-sandboxed execution. Returns the original string
|
|
62
|
+
* when the manager is not initialized (fail-open to plain execution — the
|
|
63
|
+
* permission gate still ran; the sandbox simply is not active).
|
|
64
|
+
*/
|
|
65
|
+
export declare function preWrappedCommand(manager: YagniSandboxManager, command: string, binShell?: string): Promise<string>;
|
|
66
|
+
/**
|
|
67
|
+
* Detect "Operation not permitted" style sandbox denials in bash output so
|
|
68
|
+
* callers (tool_result) can annotate and the model can react. Returns the
|
|
69
|
+
* offending path when found (pi-sandbox's regex, credited MIT).
|
|
70
|
+
*/
|
|
71
|
+
export declare function extractBlockedWritePath(output: string): string | null;
|
|
72
|
+
/**
|
|
73
|
+
* Wrap decision for user_bash (! commands): same rules as the tool, minus
|
|
74
|
+
* the dangerouslyDisableSandbox parameter (users running ! commands are
|
|
75
|
+
* already the principal, not the model).
|
|
76
|
+
*/
|
|
77
|
+
export declare function shouldUseSandboxForUserCommand(command: string, manager: Pick<YagniSandboxManager, "initialized">, settings: Pick<SandboxSettings, "excludedCommands">): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Post-execution violation annotation: annotate a command's combined output
|
|
80
|
+
* with sandbox violation details when the sandbox is active. Returns the
|
|
81
|
+
* annotated output (or the original when nothing to annotate).
|
|
82
|
+
*/
|
|
83
|
+
export declare function annotateCommandOutput(manager: YagniSandboxManager, command: string, output: string): string;
|
|
84
|
+
/** cwd must exist before spawn (guard borrowed from pi's local ops). */
|
|
85
|
+
export declare function assertSpawnableCwd(cwd: string): void;
|
|
86
|
+
/**
|
|
87
|
+
* The gate's sandbox auto-allow decision — the production predicate index.ts
|
|
88
|
+
* wires into registerPermissionGate's sandboxAutoAllow dep. Exported so the
|
|
89
|
+
* precedence table is pinned by real tests, not mirrored tautologies.
|
|
90
|
+
*
|
|
91
|
+
* Declines (returns false): non-bash tools, autoAllow disabled by setting,
|
|
92
|
+
* manager not initialized, empty command, and — critically —
|
|
93
|
+
* dangerouslyDisableSandbox retries that will run UNSANDBOXED (they must pay
|
|
94
|
+
* the normal permission price; only strict-mode-forced sandboxing rides the
|
|
95
|
+
* auto-allow). Otherwise: shouldUseSandbox's decision (excludedCommands
|
|
96
|
+
* included — an excluded command runs unsandboxed through the ordinary flow).
|
|
97
|
+
*/
|
|
98
|
+
export declare function sandboxAutoAllowDecision(toolName: string, params: Record<string, unknown>, manager: Pick<YagniSandboxManager, "initialized">, settings: Pick<SandboxSettings, "autoAllowBashIfSandboxed" | "allowUnsandboxedCommands" | "excludedCommands">): boolean;
|
|
99
|
+
//# sourceMappingURL=bash.d.ts.map
|