@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
package/dist/claudePlugins.js
CHANGED
|
@@ -17,8 +17,19 @@
|
|
|
17
17
|
* relative-path source loads on the project side, unless explicitly
|
|
18
18
|
* disabled via `enabledPlugins`.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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.
|
|
22
33
|
*
|
|
23
34
|
* Everything is fail-soft: malformed JSON, missing dirs, or hostile path
|
|
24
35
|
* entries degrade to "that plugin absent" — never a failed launch. Path
|
|
@@ -149,10 +160,20 @@ export function readKnownMarketplaces(homeDir) {
|
|
|
149
160
|
}
|
|
150
161
|
return out;
|
|
151
162
|
}
|
|
163
|
+
// ── marketplace file ────────────────────────────────────────────────────────
|
|
164
|
+
/**
|
|
165
|
+
* Names and versions from marketplace/plugin JSON become path segments under
|
|
166
|
+
* the plugin home (`cache/<mp>/<plugin>/<version>`), so they must be plain
|
|
167
|
+
* single segments: no separators, no `.`/`..`, nothing starting with `-`.
|
|
168
|
+
*/
|
|
169
|
+
export const SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
170
|
+
export function isSafeSegment(value) {
|
|
171
|
+
return typeof value === "string" && SAFE_SEGMENT.test(value);
|
|
172
|
+
}
|
|
152
173
|
/** Parse `<root>/.claude-plugin/marketplace.json`; null when absent/unusable. */
|
|
153
174
|
export function readMarketplace(root) {
|
|
154
175
|
const parsed = readJsonObject(join(root, ".claude-plugin", "marketplace.json"));
|
|
155
|
-
if (!parsed ||
|
|
176
|
+
if (!parsed || !isSafeSegment(parsed.name))
|
|
156
177
|
return null;
|
|
157
178
|
if (!Array.isArray(parsed.plugins))
|
|
158
179
|
return null;
|
|
@@ -164,10 +185,16 @@ export function readMarketplace(root) {
|
|
|
164
185
|
for (const item of parsed.plugins) {
|
|
165
186
|
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
166
187
|
continue;
|
|
167
|
-
const
|
|
168
|
-
|
|
188
|
+
const entry = item;
|
|
189
|
+
const name = entry.name;
|
|
190
|
+
if (!isSafeSegment(name))
|
|
169
191
|
continue;
|
|
170
|
-
plugins.push({
|
|
192
|
+
plugins.push({
|
|
193
|
+
name,
|
|
194
|
+
source: entry.source,
|
|
195
|
+
...(isSafeSegment(entry.version) ? { version: entry.version } : {}),
|
|
196
|
+
...(typeof entry.description === "string" ? { description: entry.description } : {}),
|
|
197
|
+
});
|
|
171
198
|
}
|
|
172
199
|
return { name: parsed.name, root, pluginRoot, plugins };
|
|
173
200
|
}
|
|
@@ -210,13 +237,17 @@ function asStringArray(value) {
|
|
|
210
237
|
* discovery reads whole dirs), root `SKILL.md` fallback. Null when the plugin
|
|
211
238
|
* has nothing we can bridge.
|
|
212
239
|
*/
|
|
213
|
-
export function pluginAssets(root, name) {
|
|
240
|
+
export function pluginAssets(root, name, meta = {}) {
|
|
214
241
|
if (!isDirectory(root))
|
|
215
242
|
return null;
|
|
216
|
-
const
|
|
243
|
+
const manifestPath = join(root, ".claude-plugin", "plugin.json");
|
|
244
|
+
const manifest = readJsonObject(manifestPath) ?? {};
|
|
245
|
+
// Default component locations go through the same containment check as
|
|
246
|
+
// manifest-declared ones: a `skills` symlink pointing outside the plugin
|
|
247
|
+
// must not hand that directory to pi.
|
|
217
248
|
const skillPaths = [];
|
|
218
|
-
const defaultSkills =
|
|
219
|
-
if (isDirectory(defaultSkills))
|
|
249
|
+
const defaultSkills = containedExistingPath(root, "skills");
|
|
250
|
+
if (defaultSkills && isDirectory(defaultSkills))
|
|
220
251
|
skillPaths.push(defaultSkills);
|
|
221
252
|
const manifestSkills = asStringArray(manifest.skills);
|
|
222
253
|
for (const rel of manifestSkills) {
|
|
@@ -225,8 +256,8 @@ export function pluginAssets(root, name) {
|
|
|
225
256
|
skillPaths.push(contained);
|
|
226
257
|
}
|
|
227
258
|
if (skillPaths.length === 0 && manifestSkills.length === 0) {
|
|
228
|
-
const rootSkill =
|
|
229
|
-
if (isFile(rootSkill))
|
|
259
|
+
const rootSkill = containedExistingPath(root, "SKILL.md");
|
|
260
|
+
if (rootSkill && isFile(rootSkill))
|
|
230
261
|
skillPaths.push(rootSkill);
|
|
231
262
|
}
|
|
232
263
|
const componentPaths = (manifestValue, defaultDir) => {
|
|
@@ -240,14 +271,62 @@ export function pluginAssets(root, name) {
|
|
|
240
271
|
}
|
|
241
272
|
return out;
|
|
242
273
|
}
|
|
243
|
-
const def =
|
|
244
|
-
return isDirectory(def) ? [def] : [];
|
|
274
|
+
const def = containedExistingPath(root, defaultDir);
|
|
275
|
+
return def && isDirectory(def) ? [def] : [];
|
|
245
276
|
};
|
|
246
277
|
const commandPaths = componentPaths(manifest.commands, "commands");
|
|
247
278
|
const agentDirs = componentPaths(manifest.agents, "agents").filter(isDirectory);
|
|
248
|
-
|
|
279
|
+
const mcp = pluginMcp(root, manifest, manifestPath);
|
|
280
|
+
const unsupported = [];
|
|
281
|
+
if (manifest.hooks !== undefined || isDirectory(join(root, "hooks")) || isFile(join(root, "hooks.json")))
|
|
282
|
+
unsupported.push("hooks");
|
|
283
|
+
if (manifest.lspServers !== undefined || isFile(join(root, ".lsp.json")))
|
|
284
|
+
unsupported.push("lsp");
|
|
285
|
+
if (manifest.outputStyles !== undefined)
|
|
286
|
+
unsupported.push("output styles");
|
|
287
|
+
if (skillPaths.length === 0 && commandPaths.length === 0 && agentDirs.length === 0 && !mcp)
|
|
249
288
|
return null;
|
|
250
|
-
|
|
289
|
+
const version = meta.version ?? (typeof manifest.version === "string" ? manifest.version : null);
|
|
290
|
+
return {
|
|
291
|
+
name,
|
|
292
|
+
key: meta.key ?? name,
|
|
293
|
+
origin: meta.origin ?? "claude-code",
|
|
294
|
+
version,
|
|
295
|
+
root,
|
|
296
|
+
skillPaths,
|
|
297
|
+
commandPaths,
|
|
298
|
+
agentDirs,
|
|
299
|
+
mcp,
|
|
300
|
+
unsupported,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* A plugin's MCP servers, per Claude Code's rules: the manifest `mcpServers`
|
|
305
|
+
* field (an inline map, or a contained path to a JSON file with one), else
|
|
306
|
+
* `.mcp.json` at the plugin root. Malformed → null (fail-soft).
|
|
307
|
+
*/
|
|
308
|
+
function pluginMcp(root, manifest, manifestPath) {
|
|
309
|
+
const field = manifest.mcpServers;
|
|
310
|
+
if (field && typeof field === "object" && !Array.isArray(field)) {
|
|
311
|
+
return { sourcePath: manifestPath, servers: field };
|
|
312
|
+
}
|
|
313
|
+
const candidates = [];
|
|
314
|
+
if (typeof field === "string") {
|
|
315
|
+
const contained = containedExistingPath(root, field);
|
|
316
|
+
if (contained && isFile(contained))
|
|
317
|
+
candidates.push(contained);
|
|
318
|
+
}
|
|
319
|
+
const rootFile = containedExistingPath(root, ".mcp.json");
|
|
320
|
+
if (rootFile && isFile(rootFile))
|
|
321
|
+
candidates.push(rootFile);
|
|
322
|
+
for (const path of candidates) {
|
|
323
|
+
const parsed = readJsonObject(path);
|
|
324
|
+
const servers = parsed?.mcpServers;
|
|
325
|
+
if (servers && typeof servers === "object" && !Array.isArray(servers)) {
|
|
326
|
+
return { sourcePath: path, servers: servers };
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
251
330
|
}
|
|
252
331
|
function resolveInstalledRoot(key, installed, marketplaces, realCwd) {
|
|
253
332
|
const entries = (installed.get(key) ?? []).filter((e) => isDirectory(e.installPath));
|
|
@@ -293,14 +372,40 @@ export function discoverClaudePlugins(deps) {
|
|
|
293
372
|
const user = [];
|
|
294
373
|
const project = [];
|
|
295
374
|
const seen = new Set();
|
|
375
|
+
// YAGNI-installed plugins are the user's own explicit action (`yagni
|
|
376
|
+
// plugin install`), so both scopes load without ceremony; a project-scope
|
|
377
|
+
// install applies only inside its repo.
|
|
378
|
+
for (const [key, entries] of Object.entries(deps.yagniLedger?.plugins ?? {})) {
|
|
379
|
+
const applicable = entries.filter((e) => {
|
|
380
|
+
if (!e.enabled || !isDirectory(e.installPath))
|
|
381
|
+
return false;
|
|
382
|
+
if (e.scope === "user")
|
|
383
|
+
return true;
|
|
384
|
+
const real = typeof e.projectPath === "string" ? realOrNull(e.projectPath) : null;
|
|
385
|
+
return !!real && (realCwd === real || realCwd.startsWith(real + sep));
|
|
386
|
+
});
|
|
387
|
+
const chosen = applicable.find((e) => e.scope === "project") ?? applicable[0];
|
|
388
|
+
if (!chosen)
|
|
389
|
+
continue;
|
|
390
|
+
const at = key.lastIndexOf("@");
|
|
391
|
+
const assets = pluginAssets(chosen.installPath, at > 0 ? key.slice(0, at) : key, {
|
|
392
|
+
key,
|
|
393
|
+
origin: "yagni",
|
|
394
|
+
version: chosen.version,
|
|
395
|
+
});
|
|
396
|
+
if (!assets)
|
|
397
|
+
continue;
|
|
398
|
+
seen.add(key);
|
|
399
|
+
user.push(assets);
|
|
400
|
+
}
|
|
296
401
|
for (const [key, state] of enabled) {
|
|
297
|
-
if (!state.enabled)
|
|
402
|
+
if (!state.enabled || seen.has(key))
|
|
298
403
|
continue;
|
|
299
404
|
const root = resolveInstalledRoot(key, installed, marketplaces, realCwd);
|
|
300
405
|
if (!root)
|
|
301
406
|
continue;
|
|
302
407
|
const at = key.lastIndexOf("@");
|
|
303
|
-
const assets = pluginAssets(root, at > 0 ? key.slice(0, at) : key);
|
|
408
|
+
const assets = pluginAssets(root, at > 0 ? key.slice(0, at) : key, { key, origin: "claude-code" });
|
|
304
409
|
if (!assets)
|
|
305
410
|
continue;
|
|
306
411
|
seen.add(key);
|
|
@@ -320,7 +425,12 @@ export function discoverClaudePlugins(deps) {
|
|
|
320
425
|
resolveInstalledRoot(key, installed, marketplaces, realCwd);
|
|
321
426
|
if (!root)
|
|
322
427
|
continue;
|
|
323
|
-
const
|
|
428
|
+
const declared = entry.version;
|
|
429
|
+
const assets = pluginAssets(root, entry.name, {
|
|
430
|
+
key,
|
|
431
|
+
origin: "repo-marketplace",
|
|
432
|
+
version: typeof declared === "string" ? declared : null,
|
|
433
|
+
});
|
|
324
434
|
if (!assets)
|
|
325
435
|
continue;
|
|
326
436
|
seen.add(key);
|
package/dist/cli.js
CHANGED
|
@@ -23,6 +23,8 @@ import { agentDir, credentialsDir, piPackageDir } from "./credentials.js";
|
|
|
23
23
|
import { DISTRIBUTION } from "./distribution.js";
|
|
24
24
|
import { connectCommand } from "./connectClaudeCode.js";
|
|
25
25
|
import { goCommand } from "./goHeadless.js";
|
|
26
|
+
import { mcpCommand } from "./mcpCommand.js";
|
|
27
|
+
import { pluginCommand } from "./pluginCommand.js";
|
|
26
28
|
import { login } from "./login.js";
|
|
27
29
|
import { logout } from "./logout.js";
|
|
28
30
|
import { tokenCommand } from "./token.js";
|
|
@@ -246,10 +248,10 @@ async function runDefault(passthroughArgs) {
|
|
|
246
248
|
catch {
|
|
247
249
|
compat = { argv: [], env: {} };
|
|
248
250
|
}
|
|
249
|
-
// OTel export:
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
251
|
+
// OTel export: on only when an OTLP endpoint is configured — the user's
|
|
252
|
+
// env, the workspace's admin-set config (fetched fail-soft, cached), or the
|
|
253
|
+
// repo's .pi/settings.json, in that order. undefined keeps the launch
|
|
254
|
+
// untouched. See otel.ts for the policy.
|
|
253
255
|
const otel = await resolveOtelLaunchWithWorkspace({
|
|
254
256
|
env: process.env,
|
|
255
257
|
cwd: process.cwd(),
|
|
@@ -515,6 +517,14 @@ export const HELP_TEXT = [
|
|
|
515
517
|
" yagni connect claude-code Route Claude Code through the YAGNI model proxy",
|
|
516
518
|
" (--project scopes to this repo; --off disconnects).",
|
|
517
519
|
" yagni connect codex Route Codex CLI through the YAGNI model proxy.",
|
|
520
|
+
" yagni mcp <subcommand> Manage MCP servers: add, remove, list, get,",
|
|
521
|
+
" add-from-claude, and more — run `yagni mcp` for",
|
|
522
|
+
" details. OAuth servers authenticate via the",
|
|
523
|
+
" /mcp panel in a session.",
|
|
524
|
+
" yagni plugin <subcommand> Claude Code plugins from a marketplace: marketplace",
|
|
525
|
+
" add, install, uninstall, update, list — run",
|
|
526
|
+
" `yagni plugin` for details. Skills, commands,",
|
|
527
|
+
" agents, and bundled MCP servers load on launch.",
|
|
518
528
|
" yagni token Output the active environment's API token (for helpers).",
|
|
519
529
|
" yagni use <name> Switch the active environment (sticky).",
|
|
520
530
|
" Presets: prod, local. Others need --base-url <url>.",
|
|
@@ -536,6 +546,9 @@ export const HELP_TEXT = [
|
|
|
536
546
|
" /mode plan|review|auto Hold writes for planning, or confirm each change.",
|
|
537
547
|
" /todos Show the agent's live task list.",
|
|
538
548
|
" /cost Session usage and credit headroom.",
|
|
549
|
+
" /mcp Manage MCP servers; authenticate OAuth servers.",
|
|
550
|
+
" /plugin Browse, install, and remove Claude Code plugins.",
|
|
551
|
+
" /reload-plugins Apply plugin changes without restarting.",
|
|
539
552
|
"",
|
|
540
553
|
"The active environment is sticky; `use` switches it (prod is the default).",
|
|
541
554
|
"Set YAGNI_BASE_URL to override the base URL for a single run.",
|
|
@@ -653,6 +666,17 @@ export async function main(argv) {
|
|
|
653
666
|
if (command === "connect") {
|
|
654
667
|
return connectCommand(rest);
|
|
655
668
|
}
|
|
669
|
+
// MCP server configuration: same three-scope model and file homes the
|
|
670
|
+
// in-session /mcp panel uses, shared code via the bundled extension's
|
|
671
|
+
// config entry. `list` runs real health-check connections.
|
|
672
|
+
if (command === "mcp") {
|
|
673
|
+
return mcpCommand(rest);
|
|
674
|
+
}
|
|
675
|
+
// Claude Code plugin marketplaces: install/remove/update from YAGNI itself
|
|
676
|
+
// (state under ~/.yagni-code/plugins; Claude Code's own installs still mirror).
|
|
677
|
+
if (command === "plugin") {
|
|
678
|
+
return pluginCommand(rest);
|
|
679
|
+
}
|
|
656
680
|
// The headless pipeline entry. `go` is a real subcommand, not passthrough:
|
|
657
681
|
// the interactive run stays `/go` inside a session, and a `yagni go` without
|
|
658
682
|
// --headless is refused with usage rather than launched as an agent prompt.
|
package/dist/doctor.d.ts
CHANGED
|
@@ -61,12 +61,44 @@ export declare function checkCliUpdate(probe: {
|
|
|
61
61
|
latest: string | null;
|
|
62
62
|
}): CheckResult;
|
|
63
63
|
export declare function checkGh(onPath: boolean): CheckResult;
|
|
64
|
+
/** One signal's verdict from the extension's probe (see telemetry/probe.ts). */
|
|
65
|
+
export interface OtelProbeSignal {
|
|
66
|
+
signal: "traces" | "metrics" | "logs";
|
|
67
|
+
url: string;
|
|
68
|
+
protocol: string;
|
|
69
|
+
ok: boolean;
|
|
70
|
+
error?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Advisory OTel-export line: says whether sessions will stream telemetry to
|
|
74
|
+
* an OTLP collector, from which config source, and — when a collector is
|
|
75
|
+
* configured — whether it ACCEPTED a real record on each signal (a wrong API
|
|
76
|
+
* key, a wrong path, or a protocol mismatch shows up as the collector's own
|
|
77
|
+
* error). Never flips the exit code — most machines have no collector, and
|
|
78
|
+
* that is the healthy default.
|
|
79
|
+
*/
|
|
80
|
+
export declare function checkOtelExport(config: OtelLaunchConfig | undefined, deps?: {
|
|
81
|
+
probe?: (env: NodeJS.ProcessEnv) => Promise<OtelProbeSignal[]>;
|
|
82
|
+
baseEnv?: NodeJS.ProcessEnv;
|
|
83
|
+
}): Promise<CheckResult>;
|
|
84
|
+
/** Config-only MCP snapshot (no connections — health checks live in `mcp list`/`get`). */
|
|
85
|
+
export interface McpProbe {
|
|
86
|
+
/** YAGNI_CODE_MCP_DISABLED kill-switch is active. */
|
|
87
|
+
disabled: boolean;
|
|
88
|
+
/** Count of configured servers (post-merge, before the approval gate). */
|
|
89
|
+
serverCount: number;
|
|
90
|
+
/** Project-scope servers that are neither enabled nor disabled. */
|
|
91
|
+
undecidedProjectServers: string[];
|
|
92
|
+
/** Config parse/validation errors (already human-readable). */
|
|
93
|
+
errors: string[];
|
|
94
|
+
}
|
|
64
95
|
/**
|
|
65
|
-
* Advisory
|
|
66
|
-
*
|
|
67
|
-
*
|
|
96
|
+
* Advisory (never required) MCP line: reports config health without connecting
|
|
97
|
+
* to anything — `yagni mcp list`/`get` already run the real health checks, and
|
|
98
|
+
* doctor must never spawn an unapproved server process. The kill-switch and
|
|
99
|
+
* pending-approval cases read as `warn`, config errors as `fail` (non-required).
|
|
68
100
|
*/
|
|
69
|
-
export declare function
|
|
101
|
+
export declare function checkMcpConfig(probe: McpProbe): CheckResult;
|
|
70
102
|
/** What the Windows bash probe found (pi needs a bash — Git Bash — on win32). */
|
|
71
103
|
export interface BashProbe {
|
|
72
104
|
found: boolean;
|
|
@@ -95,6 +127,12 @@ export interface DoctorDeps {
|
|
|
95
127
|
probeBash?: () => BashProbe;
|
|
96
128
|
currentVersion?: string;
|
|
97
129
|
probeLatestVersion?: () => Promise<string | null>;
|
|
130
|
+
/** MCP config snapshot (config-only, no connections). */
|
|
131
|
+
probeMcp?: () => Promise<McpProbe>;
|
|
132
|
+
/** OTel gate resolution (env / workspace / repo settings); tests stub it. */
|
|
133
|
+
resolveOtel?: (profile: Profile) => Promise<OtelLaunchConfig | undefined>;
|
|
134
|
+
/** OTel export probe: one real record per signal against the session env. */
|
|
135
|
+
probeOtel?: (env: NodeJS.ProcessEnv) => Promise<OtelProbeSignal[]>;
|
|
98
136
|
log?: (msg: string) => void;
|
|
99
137
|
}
|
|
100
138
|
/** Whether a `gh` executable is resolvable on PATH (no subprocess spawn). */
|
package/dist/doctor.js
CHANGED
|
@@ -17,9 +17,10 @@ import { delimiter, join } from "node:path";
|
|
|
17
17
|
import { credentialsDir } from "./credentials.js";
|
|
18
18
|
import { currentCliVersion, fetchLatestVersion, isNewerVersion } from "./upgrade.js";
|
|
19
19
|
import { classifyTokenExpiry } from "./launch.js";
|
|
20
|
-
import { resolveOtelLaunchWithWorkspace } from "./otel.js";
|
|
21
|
-
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
|
|
20
|
+
import { otelChildEnv, resolveOtelLaunchWithWorkspace } from "./otel.js";
|
|
21
|
+
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir, resolveTelemetryProbePath } from "./paths.js";
|
|
22
22
|
import { readActiveProfile } from "./profiles.js";
|
|
23
|
+
import { resolveMcpConfigPath } from "./mcpCommand.js";
|
|
23
24
|
// ── Pure check builders ─────────────────────────────────────────────────────
|
|
24
25
|
export function checkPiEngine(probe) {
|
|
25
26
|
if (!probe.binPath || !probe.binExists) {
|
|
@@ -200,11 +201,24 @@ export function checkGh(onPath) {
|
|
|
200
201
|
};
|
|
201
202
|
}
|
|
202
203
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
204
|
+
* Run the extension's telemetry probe against the exact env a session would
|
|
205
|
+
* get: one real span, one metric point, one log record, each reported with
|
|
206
|
+
* the collector's actual verdict. Dynamic import so `doctor` on a machine
|
|
207
|
+
* with no collector never loads the OTel SDK graph.
|
|
206
208
|
*/
|
|
207
|
-
|
|
209
|
+
async function defaultProbeOtel(env) {
|
|
210
|
+
const mod = (await import(resolveTelemetryProbePath()));
|
|
211
|
+
return (await mod.probeTelemetryExport(env)).results;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Advisory OTel-export line: says whether sessions will stream telemetry to
|
|
215
|
+
* an OTLP collector, from which config source, and — when a collector is
|
|
216
|
+
* configured — whether it ACCEPTED a real record on each signal (a wrong API
|
|
217
|
+
* key, a wrong path, or a protocol mismatch shows up as the collector's own
|
|
218
|
+
* error). Never flips the exit code — most machines have no collector, and
|
|
219
|
+
* that is the healthy default.
|
|
220
|
+
*/
|
|
221
|
+
export async function checkOtelExport(config, deps = {}) {
|
|
208
222
|
if (!config) {
|
|
209
223
|
return {
|
|
210
224
|
name: "otel export (optional)",
|
|
@@ -214,14 +228,97 @@ export function checkOtelExport(config) {
|
|
|
214
228
|
};
|
|
215
229
|
}
|
|
216
230
|
const source = config.source === "env"
|
|
217
|
-
? "OTEL_EXPORTER_OTLP_ENDPOINT"
|
|
231
|
+
? (config.envVar ?? "OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
218
232
|
: config.source === "workspace"
|
|
219
233
|
? "workspace settings"
|
|
220
234
|
: ".pi/settings.json";
|
|
235
|
+
const base = `on → ${config.endpoint} (${source}, metadata-only)`;
|
|
236
|
+
const baseEnv = deps.baseEnv ?? process.env;
|
|
237
|
+
const env = { ...baseEnv, ...otelChildEnv(config, baseEnv) };
|
|
238
|
+
const doProbe = deps.probe ?? defaultProbeOtel;
|
|
239
|
+
try {
|
|
240
|
+
const results = await doProbe(env);
|
|
241
|
+
const accepted = results.filter((r) => r.ok).map((r) => r.signal);
|
|
242
|
+
const failed = results.filter((r) => !r.ok);
|
|
243
|
+
const summary = results.map((r) => `${r.signal} ${r.ok ? "accepted" : "FAILED"}`).join(", ");
|
|
244
|
+
if (failed.length === 0) {
|
|
245
|
+
return {
|
|
246
|
+
name: "otel export (optional)",
|
|
247
|
+
status: "ok",
|
|
248
|
+
detail: `${base}; ${summary}`,
|
|
249
|
+
required: false,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const reasons = failed.map((r) => `${r.signal} → ${r.url} (${r.protocol}): ${r.error ?? "rejected"}`).join("; ");
|
|
253
|
+
return {
|
|
254
|
+
name: "otel export (optional)",
|
|
255
|
+
status: "warn",
|
|
256
|
+
detail: `${base}; ${summary}`,
|
|
257
|
+
hint: `${reasons}. A 401/403 means the collector key is wrong, a 404 the path or site is wrong, ` +
|
|
258
|
+
`a connection error the collector is unreachable from this machine` +
|
|
259
|
+
(accepted.length > 0 ? `; ${accepted.join(" and ")} still export` : ""),
|
|
260
|
+
required: false,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// The probe itself errored (not "endpoint down" — the check could not
|
|
265
|
+
// run). Distinct from a verified-healthy line so nobody reads an
|
|
266
|
+
// unverified config as checked-and-good.
|
|
267
|
+
return {
|
|
268
|
+
name: "otel export (optional)",
|
|
269
|
+
status: "warn",
|
|
270
|
+
detail: `${base}, export probe failed`,
|
|
271
|
+
hint: "the export probe could not run (the bundled extension is missing or the OTel SDK failed to load) — export is configured but unverified; sessions may still export normally",
|
|
272
|
+
required: false,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Advisory (never required) MCP line: reports config health without connecting
|
|
278
|
+
* to anything — `yagni mcp list`/`get` already run the real health checks, and
|
|
279
|
+
* doctor must never spawn an unapproved server process. The kill-switch and
|
|
280
|
+
* pending-approval cases read as `warn`, config errors as `fail` (non-required).
|
|
281
|
+
*/
|
|
282
|
+
export function checkMcpConfig(probe) {
|
|
283
|
+
if (probe.disabled) {
|
|
284
|
+
return {
|
|
285
|
+
name: "mcp servers",
|
|
286
|
+
status: "warn",
|
|
287
|
+
detail: "disabled (YAGNI_CODE_MCP_DISABLED=1)",
|
|
288
|
+
hint: "unset YAGNI_CODE_MCP_DISABLED to re-enable MCP servers",
|
|
289
|
+
required: false,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (probe.errors.length > 0) {
|
|
293
|
+
return {
|
|
294
|
+
name: "mcp servers",
|
|
295
|
+
status: "fail",
|
|
296
|
+
detail: `${probe.serverCount} configured, ${probe.errors.length} config error${probe.errors.length === 1 ? "" : "s"}`,
|
|
297
|
+
hint: `run \`yagni mcp list\` (first error: ${probe.errors[0]})`,
|
|
298
|
+
required: false,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (probe.serverCount === 0) {
|
|
302
|
+
return {
|
|
303
|
+
name: "mcp servers",
|
|
304
|
+
status: "ok",
|
|
305
|
+
detail: "none configured",
|
|
306
|
+
required: false,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (probe.undecidedProjectServers.length > 0) {
|
|
310
|
+
return {
|
|
311
|
+
name: "mcp servers",
|
|
312
|
+
status: "warn",
|
|
313
|
+
detail: `${probe.serverCount} configured, ${probe.undecidedProjectServers.length} awaiting approval`,
|
|
314
|
+
hint: "run /mcp to approve the project servers",
|
|
315
|
+
required: false,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
221
318
|
return {
|
|
222
|
-
name: "
|
|
319
|
+
name: "mcp servers",
|
|
223
320
|
status: "ok",
|
|
224
|
-
detail:
|
|
321
|
+
detail: `${probe.serverCount} configured`,
|
|
225
322
|
required: false,
|
|
226
323
|
};
|
|
227
324
|
}
|
|
@@ -312,6 +409,37 @@ function defaultProbeStateDir() {
|
|
|
312
409
|
return { path, exists: false, mode: null };
|
|
313
410
|
}
|
|
314
411
|
}
|
|
412
|
+
async function defaultProbeMcp(env = process.env) {
|
|
413
|
+
const disabled = env.YAGNI_CODE_MCP_DISABLED === "1";
|
|
414
|
+
if (disabled)
|
|
415
|
+
return { disabled: true, serverCount: 0, undecidedProjectServers: [], errors: [] };
|
|
416
|
+
try {
|
|
417
|
+
const mod = (await import(resolveMcpConfigPath()));
|
|
418
|
+
if (typeof mod?.loadMcpServers !== "function") {
|
|
419
|
+
return { disabled: false, serverCount: 0, undecidedProjectServers: [], errors: ["extension MCP config entry missing"] };
|
|
420
|
+
}
|
|
421
|
+
const loaded = mod.loadMcpServers(process.cwd(), env);
|
|
422
|
+
const repoRoot = mod.resolveProjectRoot(process.cwd());
|
|
423
|
+
const { state } = mod.readProjectApproval(repoRoot);
|
|
424
|
+
const undecided = loaded.servers
|
|
425
|
+
.filter((s) => (s.scope === "project" || s.gated === true) && mod.decisionFor(state, s.name) === "undecided")
|
|
426
|
+
.map((s) => s.name);
|
|
427
|
+
return {
|
|
428
|
+
disabled: false,
|
|
429
|
+
serverCount: loaded.servers.length,
|
|
430
|
+
undecidedProjectServers: undecided,
|
|
431
|
+
errors: loaded.errors.map((e) => (e.serverName ? `${e.serverName}: ${e.message}` : e.message)),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
return {
|
|
436
|
+
disabled: false,
|
|
437
|
+
serverCount: 0,
|
|
438
|
+
undecidedProjectServers: [],
|
|
439
|
+
errors: [err instanceof Error ? err.message : String(err)],
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
315
443
|
async function defaultProbeBackend(baseUrl, token) {
|
|
316
444
|
if (!token)
|
|
317
445
|
return { kind: "skipped" };
|
|
@@ -378,6 +506,7 @@ export async function gatherChecks(deps = {}) {
|
|
|
378
506
|
const readProfile = deps.readActiveProfile ?? readActiveProfile;
|
|
379
507
|
const probeBackend = deps.probeBackend ?? defaultProbeBackend;
|
|
380
508
|
const probeStateDir = deps.probeStateDir ?? defaultProbeStateDir;
|
|
509
|
+
const probeMcp = deps.probeMcp ?? (() => defaultProbeMcp());
|
|
381
510
|
const ghOnPath = deps.ghOnPath ?? (() => ghOnPathDefault());
|
|
382
511
|
const platform = deps.platform ?? process.platform;
|
|
383
512
|
const probeBash = deps.probeBash ?? (() => bashOnWindowsDefault());
|
|
@@ -406,12 +535,18 @@ export async function gatherChecks(deps = {}) {
|
|
|
406
535
|
checks.push(checkBackend(backend));
|
|
407
536
|
checks.push(checkStateDir(probeStateDir()));
|
|
408
537
|
checks.push(checkGh(ghOnPath()));
|
|
409
|
-
checks.push(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
538
|
+
checks.push(checkMcpConfig(await probeMcp()));
|
|
539
|
+
const resolveOtel = deps.resolveOtel ??
|
|
540
|
+
((p) => resolveOtelLaunchWithWorkspace({
|
|
541
|
+
env: process.env,
|
|
542
|
+
cwd: process.cwd(),
|
|
543
|
+
creds: p.token ? { baseUrl: p.baseUrl, token: p.token } : null,
|
|
544
|
+
profileName: p.name,
|
|
545
|
+
}));
|
|
546
|
+
checks.push(await checkOtelExport(await resolveOtel(profile), {
|
|
547
|
+
...(deps.probeOtel ? { probe: deps.probeOtel } : {}),
|
|
548
|
+
baseEnv: process.env,
|
|
549
|
+
}));
|
|
415
550
|
return checks;
|
|
416
551
|
}
|
|
417
552
|
/**
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* pattern) so tests run against plain text. Renderer exceptions are swallowed
|
|
25
25
|
* by pi (degrading to the built-in fallback), so everything stays total.
|
|
26
26
|
*/
|
|
27
|
-
import { SettingsManager, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import { SettingsManager, type ExtensionAPI, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
28
28
|
import type { RenderTheme } from "./subagentRender.js";
|
|
29
29
|
import { ToolRunTracker } from "./toolRuns.js";
|
|
30
30
|
/** Lines of write content shown collapsed (mirrors Claude Code's preview). */
|
|
@@ -87,6 +87,17 @@ export interface RegisterCondensedToolsDeps {
|
|
|
87
87
|
* image autoResize. Injectable so tests never touch the config dir.
|
|
88
88
|
*/
|
|
89
89
|
loadSettings?: (cwd: string) => SettingsManager;
|
|
90
|
+
/**
|
|
91
|
+
* sandbox composition: when present, the bash definition is passed
|
|
92
|
+
* through this wrap before registration — the sandbox adds its schema field
|
|
93
|
+
* (dangerouslyDisableSandbox), wrap-on-execute, and violation annotation
|
|
94
|
+
* while condensed keeps its rendering. The compose fn is built by
|
|
95
|
+
* sandbox/session.ts (makeSandboxBashComposition) and is a no-op when the
|
|
96
|
+
* sandbox is disabled. Condensed registers LAST among the bash-defining
|
|
97
|
+
* sites, so composing HERE is what makes the sandbox actually reach the
|
|
98
|
+
* model's tool calls.
|
|
99
|
+
*/
|
|
100
|
+
wrapBash?: (def: ToolDefinition) => ToolDefinition;
|
|
90
101
|
}
|
|
91
102
|
/**
|
|
92
103
|
* Re-register the seven built-ins with condensed renderers. Returns the
|
|
@@ -291,15 +291,23 @@ export function registerCondensedTools(pi, deps = {}) {
|
|
|
291
291
|
catch {
|
|
292
292
|
toolOptions = { bash: {}, read: {} };
|
|
293
293
|
}
|
|
294
|
-
const buildDefinitions = (cwd) =>
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
294
|
+
const buildDefinitions = (cwd) => {
|
|
295
|
+
const defs = {
|
|
296
|
+
read: createReadToolDefinition(cwd, toolOptions.read),
|
|
297
|
+
bash: createBashToolDefinition(cwd, toolOptions.bash),
|
|
298
|
+
edit: createEditToolDefinition(cwd),
|
|
299
|
+
write: createWriteToolDefinition(cwd),
|
|
300
|
+
grep: createGrepToolDefinition(cwd),
|
|
301
|
+
find: createFindToolDefinition(cwd),
|
|
302
|
+
ls: createLsToolDefinition(cwd),
|
|
303
|
+
};
|
|
304
|
+
// the sandbox wraps the bash DEFINITION (not the registration
|
|
305
|
+
// wrapper) so the cwd-delegating execute below still lands on the
|
|
306
|
+
// sandbox-aware tool for every cwd the session visits.
|
|
307
|
+
if (defs.bash && deps.wrapBash)
|
|
308
|
+
defs.bash = deps.wrapBash(defs.bash);
|
|
309
|
+
return defs;
|
|
310
|
+
};
|
|
303
311
|
const definitionsByCwd = new Map();
|
|
304
312
|
const definitionsFor = (cwd) => {
|
|
305
313
|
let defs = definitionsByCwd.get(cwd);
|