@yagni-app/code 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -46
- package/dist/claudeCompat.d.ts +59 -0
- package/dist/claudeCompat.js +109 -2
- package/dist/claudePlugins.d.ts +45 -5
- package/dist/claudePlugins.js +129 -21
- package/dist/cli.js +16 -4
- package/dist/doctor.d.ts +21 -8
- package/dist/doctor.js +53 -28
- package/dist/extension/condensedTools.d.ts +12 -1
- package/dist/extension/condensedTools.js +17 -9
- package/dist/extension/index.d.ts +13 -0
- package/dist/extension/index.js +230 -43
- package/dist/extension/mcp/cliConfig.d.ts +1 -1
- package/dist/extension/mcp/cliConfig.js +1 -1
- package/dist/extension/mcp/config.d.ts +24 -2
- package/dist/extension/mcp/config.js +75 -3
- package/dist/extension/mcp/manager.d.ts +3 -1
- package/dist/extension/mcp/manager.js +2 -2
- package/dist/extension/mcp/panel.d.ts +0 -1
- package/dist/extension/mcp/panel.js +13 -3
- package/dist/extension/mcp/startup.js +8 -6
- package/dist/extension/permission/gate.d.ts +97 -2
- package/dist/extension/permission/gate.js +375 -26
- package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
- package/dist/extension/permissionRules/bashFileArgs.js +236 -0
- package/dist/extension/permissionRules/engine.d.ts +50 -0
- package/dist/extension/permissionRules/engine.js +238 -0
- package/dist/extension/permissionRules/loadConfig.d.ts +53 -0
- package/dist/extension/permissionRules/loadConfig.js +90 -0
- package/dist/extension/permissionRules/parser.d.ts +38 -0
- package/dist/extension/permissionRules/parser.js +136 -0
- package/dist/extension/permissionRules/pathRules.d.ts +58 -0
- package/dist/extension/permissionRules/pathRules.js +120 -0
- package/dist/extension/permissionRules/shellRules.d.ts +52 -0
- package/dist/extension/permissionRules/shellRules.js +221 -0
- package/dist/extension/pipeline/invocation.d.ts +3 -6
- package/dist/extension/pipeline/invocation.js +3 -6
- package/dist/extension/pipeline/runner.d.ts +0 -1
- package/dist/extension/pipeline/runner.js +6 -14
- package/dist/extension/plugins/inventory.d.ts +88 -0
- package/dist/extension/plugins/inventory.js +144 -0
- package/dist/extension/plugins/panel.d.ts +45 -0
- package/dist/extension/plugins/panel.js +293 -0
- package/dist/extension/sandbox/bash.d.ts +99 -0
- package/dist/extension/sandbox/bash.js +190 -0
- package/dist/extension/sandbox/config.d.ts +114 -0
- package/dist/extension/sandbox/config.js +366 -0
- package/dist/extension/sandbox/manager.d.ts +98 -0
- package/dist/extension/sandbox/manager.js +216 -0
- package/dist/extension/sandbox/panel.d.ts +111 -0
- package/dist/extension/sandbox/panel.js +342 -0
- package/dist/extension/sandbox/session.d.ts +85 -0
- package/dist/extension/sandbox/session.js +775 -0
- package/dist/extension/telemetry/attrs.d.ts +96 -0
- package/dist/extension/telemetry/attrs.js +149 -0
- package/dist/extension/telemetry/config.d.ts +99 -0
- package/dist/extension/telemetry/config.js +193 -0
- package/dist/extension/telemetry/index.d.ts +7 -0
- package/dist/extension/telemetry/index.js +7 -0
- package/dist/extension/telemetry/probe.d.ts +29 -0
- package/dist/extension/telemetry/probe.js +122 -0
- package/dist/extension/telemetry/register.d.ts +40 -0
- package/dist/extension/telemetry/register.js +192 -0
- package/dist/extension/telemetry/sdk.d.ts +63 -0
- package/dist/extension/telemetry/sdk.js +207 -0
- package/dist/extension/telemetry/tracker.d.ts +131 -0
- package/dist/extension/telemetry/tracker.js +551 -0
- package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
- package/dist/extension/vendor/ignore.d.ts +86 -0
- package/dist/extension/vendor/ignore.js +788 -0
- package/dist/goHeadless.d.ts +1 -1
- package/dist/goHeadless.js +2 -2
- package/dist/launch.d.ts +4 -3
- package/dist/launch.js +7 -4
- package/dist/mcpCommand.d.ts +10 -1
- package/dist/mcpCommand.js +42 -10
- package/dist/otel.d.ts +67 -90
- package/dist/otel.js +152 -195
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +18 -0
- package/dist/pluginCommand.d.ts +43 -0
- package/dist/pluginCommand.js +499 -0
- package/dist/pluginStore.d.ts +170 -0
- package/dist/pluginStore.js +554 -0
- package/package.json +19 -3
package/dist/claudePlugins.js
CHANGED
|
@@ -17,10 +17,19 @@
|
|
|
17
17
|
* relative-path source loads on the project side, unless explicitly
|
|
18
18
|
* disabled via `enabledPlugins`.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* Three sources, first wins on the same `name@marketplace`:
|
|
21
|
+
*
|
|
22
|
+
* 0. YAGNI's own ledger (`~/.yagni-code/plugins`, written by `yagni plugin`;
|
|
23
|
+
* see pluginStore.ts) — handed in by the caller as `yagniLedger`.
|
|
24
|
+
* 1. Claude Code's ledger + `enabledPlugins` (mirrored read-only).
|
|
25
|
+
* 2. The repo's own `.claude-plugin/marketplace.json`.
|
|
26
|
+
*
|
|
27
|
+
* Plugin MCP servers (`.mcp.json` at the plugin root, or the manifest's
|
|
28
|
+
* `mcpServers`) are collected here and bridged by the launcher into the
|
|
29
|
+
* extension's MCP client (see claudeCompat.ts). Deliberately NOT here:
|
|
30
|
+
* network installation (that is pluginStore.ts), plugin hooks, LSP servers,
|
|
31
|
+
* themes, output styles — those are reported as `unsupported` so `yagni
|
|
32
|
+
* plugin list` can say so. Discovery is read-only.
|
|
24
33
|
*
|
|
25
34
|
* Everything is fail-soft: malformed JSON, missing dirs, or hostile path
|
|
26
35
|
* entries degrade to "that plugin absent" — never a failed launch. Path
|
|
@@ -151,10 +160,20 @@ export function readKnownMarketplaces(homeDir) {
|
|
|
151
160
|
}
|
|
152
161
|
return out;
|
|
153
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
|
+
}
|
|
154
173
|
/** Parse `<root>/.claude-plugin/marketplace.json`; null when absent/unusable. */
|
|
155
174
|
export function readMarketplace(root) {
|
|
156
175
|
const parsed = readJsonObject(join(root, ".claude-plugin", "marketplace.json"));
|
|
157
|
-
if (!parsed ||
|
|
176
|
+
if (!parsed || !isSafeSegment(parsed.name))
|
|
158
177
|
return null;
|
|
159
178
|
if (!Array.isArray(parsed.plugins))
|
|
160
179
|
return null;
|
|
@@ -166,10 +185,16 @@ export function readMarketplace(root) {
|
|
|
166
185
|
for (const item of parsed.plugins) {
|
|
167
186
|
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
168
187
|
continue;
|
|
169
|
-
const
|
|
170
|
-
|
|
188
|
+
const entry = item;
|
|
189
|
+
const name = entry.name;
|
|
190
|
+
if (!isSafeSegment(name))
|
|
171
191
|
continue;
|
|
172
|
-
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
|
+
});
|
|
173
198
|
}
|
|
174
199
|
return { name: parsed.name, root, pluginRoot, plugins };
|
|
175
200
|
}
|
|
@@ -212,13 +237,17 @@ function asStringArray(value) {
|
|
|
212
237
|
* discovery reads whole dirs), root `SKILL.md` fallback. Null when the plugin
|
|
213
238
|
* has nothing we can bridge.
|
|
214
239
|
*/
|
|
215
|
-
export function pluginAssets(root, name) {
|
|
240
|
+
export function pluginAssets(root, name, meta = {}) {
|
|
216
241
|
if (!isDirectory(root))
|
|
217
242
|
return null;
|
|
218
|
-
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.
|
|
219
248
|
const skillPaths = [];
|
|
220
|
-
const defaultSkills =
|
|
221
|
-
if (isDirectory(defaultSkills))
|
|
249
|
+
const defaultSkills = containedExistingPath(root, "skills");
|
|
250
|
+
if (defaultSkills && isDirectory(defaultSkills))
|
|
222
251
|
skillPaths.push(defaultSkills);
|
|
223
252
|
const manifestSkills = asStringArray(manifest.skills);
|
|
224
253
|
for (const rel of manifestSkills) {
|
|
@@ -227,8 +256,8 @@ export function pluginAssets(root, name) {
|
|
|
227
256
|
skillPaths.push(contained);
|
|
228
257
|
}
|
|
229
258
|
if (skillPaths.length === 0 && manifestSkills.length === 0) {
|
|
230
|
-
const rootSkill =
|
|
231
|
-
if (isFile(rootSkill))
|
|
259
|
+
const rootSkill = containedExistingPath(root, "SKILL.md");
|
|
260
|
+
if (rootSkill && isFile(rootSkill))
|
|
232
261
|
skillPaths.push(rootSkill);
|
|
233
262
|
}
|
|
234
263
|
const componentPaths = (manifestValue, defaultDir) => {
|
|
@@ -242,14 +271,62 @@ export function pluginAssets(root, name) {
|
|
|
242
271
|
}
|
|
243
272
|
return out;
|
|
244
273
|
}
|
|
245
|
-
const def =
|
|
246
|
-
return isDirectory(def) ? [def] : [];
|
|
274
|
+
const def = containedExistingPath(root, defaultDir);
|
|
275
|
+
return def && isDirectory(def) ? [def] : [];
|
|
247
276
|
};
|
|
248
277
|
const commandPaths = componentPaths(manifest.commands, "commands");
|
|
249
278
|
const agentDirs = componentPaths(manifest.agents, "agents").filter(isDirectory);
|
|
250
|
-
|
|
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)
|
|
251
288
|
return null;
|
|
252
|
-
|
|
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;
|
|
253
330
|
}
|
|
254
331
|
function resolveInstalledRoot(key, installed, marketplaces, realCwd) {
|
|
255
332
|
const entries = (installed.get(key) ?? []).filter((e) => isDirectory(e.installPath));
|
|
@@ -295,14 +372,40 @@ export function discoverClaudePlugins(deps) {
|
|
|
295
372
|
const user = [];
|
|
296
373
|
const project = [];
|
|
297
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
|
+
}
|
|
298
401
|
for (const [key, state] of enabled) {
|
|
299
|
-
if (!state.enabled)
|
|
402
|
+
if (!state.enabled || seen.has(key))
|
|
300
403
|
continue;
|
|
301
404
|
const root = resolveInstalledRoot(key, installed, marketplaces, realCwd);
|
|
302
405
|
if (!root)
|
|
303
406
|
continue;
|
|
304
407
|
const at = key.lastIndexOf("@");
|
|
305
|
-
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" });
|
|
306
409
|
if (!assets)
|
|
307
410
|
continue;
|
|
308
411
|
seen.add(key);
|
|
@@ -322,7 +425,12 @@ export function discoverClaudePlugins(deps) {
|
|
|
322
425
|
resolveInstalledRoot(key, installed, marketplaces, realCwd);
|
|
323
426
|
if (!root)
|
|
324
427
|
continue;
|
|
325
|
-
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
|
+
});
|
|
326
434
|
if (!assets)
|
|
327
435
|
continue;
|
|
328
436
|
seen.add(key);
|
package/dist/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ import { DISTRIBUTION } from "./distribution.js";
|
|
|
24
24
|
import { connectCommand } from "./connectClaudeCode.js";
|
|
25
25
|
import { goCommand } from "./goHeadless.js";
|
|
26
26
|
import { mcpCommand } from "./mcpCommand.js";
|
|
27
|
+
import { pluginCommand } from "./pluginCommand.js";
|
|
27
28
|
import { login } from "./login.js";
|
|
28
29
|
import { logout } from "./logout.js";
|
|
29
30
|
import { tokenCommand } from "./token.js";
|
|
@@ -247,10 +248,10 @@ async function runDefault(passthroughArgs) {
|
|
|
247
248
|
catch {
|
|
248
249
|
compat = { argv: [], env: {} };
|
|
249
250
|
}
|
|
250
|
-
// OTel export:
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
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.
|
|
254
255
|
const otel = await resolveOtelLaunchWithWorkspace({
|
|
255
256
|
env: process.env,
|
|
256
257
|
cwd: process.cwd(),
|
|
@@ -520,6 +521,10 @@ export const HELP_TEXT = [
|
|
|
520
521
|
" add-from-claude, and more — run `yagni mcp` for",
|
|
521
522
|
" details. OAuth servers authenticate via the",
|
|
522
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.",
|
|
523
528
|
" yagni token Output the active environment's API token (for helpers).",
|
|
524
529
|
" yagni use <name> Switch the active environment (sticky).",
|
|
525
530
|
" Presets: prod, local. Others need --base-url <url>.",
|
|
@@ -542,6 +547,8 @@ export const HELP_TEXT = [
|
|
|
542
547
|
" /todos Show the agent's live task list.",
|
|
543
548
|
" /cost Session usage and credit headroom.",
|
|
544
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.",
|
|
545
552
|
"",
|
|
546
553
|
"The active environment is sticky; `use` switches it (prod is the default).",
|
|
547
554
|
"Set YAGNI_BASE_URL to override the base URL for a single run.",
|
|
@@ -665,6 +672,11 @@ export async function main(argv) {
|
|
|
665
672
|
if (command === "mcp") {
|
|
666
673
|
return mcpCommand(rest);
|
|
667
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
|
+
}
|
|
668
680
|
// The headless pipeline entry. `go` is a real subcommand, not passthrough:
|
|
669
681
|
// the interactive run stays `/go` inside a session, and a `yagni go` without
|
|
670
682
|
// --headless is refused with usage rather than launched as an agent prompt.
|
package/dist/doctor.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Advisory checks (loose perms, missing `gh`) never flip the exit code.
|
|
14
14
|
*/
|
|
15
15
|
import { type TokenExpiryStatus } from "./launch.js";
|
|
16
|
-
import {
|
|
16
|
+
import { type OtelLaunchConfig } from "./otel.js";
|
|
17
17
|
import { type Profile } from "./profiles.js";
|
|
18
18
|
export type CheckStatus = "ok" | "warn" | "fail";
|
|
19
19
|
export interface CheckResult {
|
|
@@ -61,16 +61,25 @@ 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
|
+
}
|
|
64
72
|
/**
|
|
65
|
-
* Advisory OTel-export line: says whether sessions will stream
|
|
66
|
-
* OTLP collector, from which config source, and — when a collector is
|
|
67
|
-
* configured — whether
|
|
68
|
-
*
|
|
69
|
-
* most machines have no collector, and
|
|
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.
|
|
70
79
|
*/
|
|
71
80
|
export declare function checkOtelExport(config: OtelLaunchConfig | undefined, deps?: {
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
probe?: (env: NodeJS.ProcessEnv) => Promise<OtelProbeSignal[]>;
|
|
82
|
+
baseEnv?: NodeJS.ProcessEnv;
|
|
74
83
|
}): Promise<CheckResult>;
|
|
75
84
|
/** Config-only MCP snapshot (no connections — health checks live in `mcp list`/`get`). */
|
|
76
85
|
export interface McpProbe {
|
|
@@ -120,6 +129,10 @@ export interface DoctorDeps {
|
|
|
120
129
|
probeLatestVersion?: () => Promise<string | null>;
|
|
121
130
|
/** MCP config snapshot (config-only, no connections). */
|
|
122
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[]>;
|
|
123
136
|
log?: (msg: string) => void;
|
|
124
137
|
}
|
|
125
138
|
/** Whether a `gh` executable is resolvable on PATH (no subprocess spawn). */
|
package/dist/doctor.js
CHANGED
|
@@ -17,8 +17,8 @@ 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 {
|
|
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
23
|
import { resolveMcpConfigPath } from "./mcpCommand.js";
|
|
24
24
|
// ── Pure check builders ─────────────────────────────────────────────────────
|
|
@@ -201,11 +201,22 @@ export function checkGh(onPath) {
|
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
203
|
/**
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
|
|
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.
|
|
208
|
+
*/
|
|
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.
|
|
209
220
|
*/
|
|
210
221
|
export async function checkOtelExport(config, deps = {}) {
|
|
211
222
|
if (!config) {
|
|
@@ -222,23 +233,32 @@ export async function checkOtelExport(config, deps = {}) {
|
|
|
222
233
|
? "workspace settings"
|
|
223
234
|
: ".pi/settings.json";
|
|
224
235
|
const base = `on → ${config.endpoint} (${source}, metadata-only)`;
|
|
225
|
-
const
|
|
236
|
+
const baseEnv = deps.baseEnv ?? process.env;
|
|
237
|
+
const env = { ...baseEnv, ...otelChildEnv(config, baseEnv) };
|
|
238
|
+
const doProbe = deps.probe ?? defaultProbeOtel;
|
|
226
239
|
try {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
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 {
|
|
230
246
|
name: "otel export (optional)",
|
|
231
247
|
status: "ok",
|
|
232
|
-
detail: `${base}
|
|
233
|
-
required: false,
|
|
234
|
-
}
|
|
235
|
-
: {
|
|
236
|
-
name: "otel export (optional)",
|
|
237
|
-
status: "warn",
|
|
238
|
-
detail: `${base}, endpoint not reachable`,
|
|
239
|
-
hint: "the collector is down, unreachable from this machine, or the URL is wrong — traces will not export until it answers",
|
|
248
|
+
detail: `${base}; ${summary}`,
|
|
240
249
|
required: false,
|
|
241
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
|
+
};
|
|
242
262
|
}
|
|
243
263
|
catch {
|
|
244
264
|
// The probe itself errored (not "endpoint down" — the check could not
|
|
@@ -247,8 +267,8 @@ export async function checkOtelExport(config, deps = {}) {
|
|
|
247
267
|
return {
|
|
248
268
|
name: "otel export (optional)",
|
|
249
269
|
status: "warn",
|
|
250
|
-
detail: `${base},
|
|
251
|
-
hint: "the
|
|
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",
|
|
252
272
|
required: false,
|
|
253
273
|
};
|
|
254
274
|
}
|
|
@@ -402,7 +422,7 @@ async function defaultProbeMcp(env = process.env) {
|
|
|
402
422
|
const repoRoot = mod.resolveProjectRoot(process.cwd());
|
|
403
423
|
const { state } = mod.readProjectApproval(repoRoot);
|
|
404
424
|
const undecided = loaded.servers
|
|
405
|
-
.filter((s) => s.scope === "project" && mod.decisionFor(state, s.name) === "undecided")
|
|
425
|
+
.filter((s) => (s.scope === "project" || s.gated === true) && mod.decisionFor(state, s.name) === "undecided")
|
|
406
426
|
.map((s) => s.name);
|
|
407
427
|
return {
|
|
408
428
|
disabled: false,
|
|
@@ -516,12 +536,17 @@ export async function gatherChecks(deps = {}) {
|
|
|
516
536
|
checks.push(checkStateDir(probeStateDir()));
|
|
517
537
|
checks.push(checkGh(ghOnPath()));
|
|
518
538
|
checks.push(checkMcpConfig(await probeMcp()));
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
+
}));
|
|
525
550
|
return checks;
|
|
526
551
|
}
|
|
527
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);
|
|
@@ -2,6 +2,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { type SpendResponse } from "./costHud.js";
|
|
3
3
|
import { runInitPass as defaultRunInitPass } from "./initPass.js";
|
|
4
4
|
import { startMcp as defaultStartMcp } from "./mcp/startup.js";
|
|
5
|
+
import { type GuardianGateEvent } from "./permission/gate.js";
|
|
6
|
+
import { type PluginCliRunner } from "./plugins/panel.js";
|
|
5
7
|
import { type FlushOutcome, type SpoolClientOpts } from "./spool.js";
|
|
6
8
|
import { type TokenProvider } from "./tokenProvider.js";
|
|
7
9
|
import { type CatalogResult, type ContextBrief } from "./config.js";
|
|
@@ -58,6 +60,8 @@ export interface RegisterYagniDeps {
|
|
|
58
60
|
* homes or spawning servers. Default runs the real startup.
|
|
59
61
|
*/
|
|
60
62
|
startMcp?: typeof defaultStartMcp;
|
|
63
|
+
/** Test seam for the /plugin panel: runs `yagni plugin …` (defaults to spawning YAGNI_CLI_PATH). */
|
|
64
|
+
runPluginCli?: PluginCliRunner;
|
|
61
65
|
/**
|
|
62
66
|
* The CLI init pass (Onramp Door B). Injectable so tests assert it fires on a
|
|
63
67
|
* fresh-workspace first-run and is skipped otherwise, without a network or disk.
|
|
@@ -115,6 +119,15 @@ export declare function buildRunSpendUrl(baseUrl: string, runId: string): string
|
|
|
115
119
|
* Returns null on anything that doesn't look like the real shape.
|
|
116
120
|
*/
|
|
117
121
|
export declare function parseSpendResponse(data: unknown): SpendResponse | null;
|
|
122
|
+
/**
|
|
123
|
+
* Local trail for EVERY terminal gate outcome (sandbox auto-allow included),
|
|
124
|
+
* always on so e2e and support can see what the gate did even when the
|
|
125
|
+
* storage tier is off. Decision K: the always-on line carries outcome
|
|
126
|
+
* metadata ONLY — no command content. The command prefix and the exec
|
|
127
|
+
* justification are command/model content and ride the debug tier (never
|
|
128
|
+
* uploaded by /feedback) plus the opt-in storage stream.
|
|
129
|
+
*/
|
|
130
|
+
export declare function logGateOutcomeTrail(ev: GuardianGateEvent, env: NodeJS.ProcessEnv): void;
|
|
118
131
|
export declare function registerYagni(pi: ExtensionAPI, deps?: RegisterYagniDeps): Promise<void>;
|
|
119
132
|
export default function (pi: ExtensionAPI): Promise<void>;
|
|
120
133
|
export { makeAskYagniTool } from "./askYagniTool.js";
|