@yagni-app/code 1.0.4 → 1.0.6
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 +30 -6
- package/dist/claudePlugins.d.ts +3 -1
- package/dist/claudePlugins.js +3 -1
- package/dist/cli.js +12 -0
- package/dist/doctor.d.ts +28 -3
- package/dist/doctor.js +117 -7
- package/dist/extension/index.d.ts +5 -5
- package/dist/extension/index.js +94 -31
- 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 +131 -0
- package/dist/extension/mcp/config.js +309 -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 +98 -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 +34 -0
- package/dist/extension/mcp/panel.js +258 -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 +150 -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/execPolicy.js +17 -2
- package/dist/extension/permission/gate.d.ts +7 -0
- package/dist/extension/permission/gate.js +12 -5
- package/dist/extension/permission/guardian.d.ts +24 -5
- package/dist/extension/permission/guardian.js +162 -24
- package/dist/extension/pipeline/personas.js +5 -0
- package/dist/mcpCommand.d.ts +113 -0
- package/dist/mcpCommand.js +755 -0
- package/dist/otel.d.ts +36 -7
- package/dist/otel.js +90 -12
- package/dist/upgrade.d.ts +11 -2
- package/dist/upgrade.js +48 -8
- package/package.json +3 -2
- package/dist/extension/mcpTools.d.ts +0 -57
- package/dist/extension/mcpTools.js +0 -132
package/dist/otel.d.ts
CHANGED
|
@@ -43,13 +43,23 @@ export interface OtelLaunchConfig {
|
|
|
43
43
|
extensionPath: string;
|
|
44
44
|
/**
|
|
45
45
|
* Where the enablement signal came from. "workspace" additionally carries
|
|
46
|
-
* protocol/headers/serviceName, delivered as env to the session
|
|
47
|
-
*
|
|
46
|
+
* protocol/headers/serviceName, delivered as env to the session; "env" may
|
|
47
|
+
* carry a protocol when the endpoint arrived via the per-signal
|
|
48
|
+
* `OTEL_EXPORTER_OTLP_TRACES_(ENDPOINT|PROTOCOL)` bridge (pi-otel reads only
|
|
49
|
+
* the generic var, so the child env translates); "project-settings" leaves
|
|
50
|
+
* those to whatever the user/repo already configured.
|
|
48
51
|
*/
|
|
49
52
|
source: "env" | "workspace" | "project-settings";
|
|
53
|
+
/**
|
|
54
|
+
* The env var the endpoint actually came from — the generic
|
|
55
|
+
* `OTEL_EXPORTER_OTLP_ENDPOINT` or the per-signal
|
|
56
|
+
* `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`. Doctor surfaces it so a user who set
|
|
57
|
+
* the per-signal var sees their own var named, not the generic one.
|
|
58
|
+
*/
|
|
59
|
+
envVar?: "OTEL_EXPORTER_OTLP_ENDPOINT" | "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT";
|
|
50
60
|
/** The configured OTLP endpoint (for doctor display; env for workspace source). */
|
|
51
61
|
endpoint: string;
|
|
52
|
-
/**
|
|
62
|
+
/** OTLP protocol: workspace config, or a per-signal env bridge. */
|
|
53
63
|
protocol?: string;
|
|
54
64
|
/** Workspace-configured collector headers (workspace source only; carries secrets). */
|
|
55
65
|
headers?: Record<string, string>;
|
|
@@ -72,6 +82,15 @@ export interface WorkspaceOtelConfig {
|
|
|
72
82
|
* Decide whether this launch exports OTel traces, and with which pi-otel entry.
|
|
73
83
|
* Returns undefined when export stays off: no endpoint configured anywhere,
|
|
74
84
|
* `PI_OTEL_DISABLED` set, or the pi-otel package unresolvable (never fatal).
|
|
85
|
+
*
|
|
86
|
+
* Env sources, in precedence order: the generic `OTEL_EXPORTER_OTLP_ENDPOINT`,
|
|
87
|
+
* then the per-signal `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`. pi-otel only reads
|
|
88
|
+
* the generic var, so the per-signal one (the standard OTel env shape, and
|
|
89
|
+
* what Claude Code setups configure — e.g. Updater's) is bridged: the traces
|
|
90
|
+
* endpoint becomes the generic one, and a per-signal TRACES_PROTOCOL carries
|
|
91
|
+
* over the same way. Logs/metrics per-signal vars are deliberately NOT read:
|
|
92
|
+
* sessions export traces only, and pi-otel's metrics/logs signals are opt-in
|
|
93
|
+
* elsewhere.
|
|
75
94
|
*/
|
|
76
95
|
export declare function resolveOtelLaunch(opts: {
|
|
77
96
|
env: NodeJS.ProcessEnv;
|
|
@@ -80,13 +99,21 @@ export declare function resolveOtelLaunch(opts: {
|
|
|
80
99
|
resolveExtension?: () => string;
|
|
81
100
|
readFile?: typeof readFileSync;
|
|
82
101
|
}): OtelLaunchConfig | undefined;
|
|
83
|
-
/**
|
|
84
|
-
* Absolute path to pi-otel's extension entry (its package main IS the pi
|
|
102
|
+
/** Absolute path to pi-otel's extension entry (its package main IS the pi
|
|
85
103
|
* extension entry, `dist/index.js`). Resolved ESM-native like `paths.ts` does
|
|
86
104
|
* for pi itself. Throws when the package is missing — callers treat that as
|
|
87
105
|
* "export off", not an error.
|
|
88
106
|
*/
|
|
89
107
|
export declare function resolveOtelExtensionPath(): string;
|
|
108
|
+
/**
|
|
109
|
+
* TCP reachability probe for an OTLP endpoint URL, with scheme-default ports:
|
|
110
|
+
* WHATWG `URL` strips `:443`/`:80` as defaults, so an https endpoint with no
|
|
111
|
+
* port probes 443 (the agentless SaaS shape). Returns false on any parse or
|
|
112
|
+
* connect failure — reachability is advisory, never fatal. Mirrors the probe
|
|
113
|
+
* inside (patched) pi-otel but lives here so `doctor` can use it without
|
|
114
|
+
* importing the extension's SDK graph.
|
|
115
|
+
*/
|
|
116
|
+
export declare function probeOtelEndpoint(endpoint: string, timeoutMs?: number): Promise<boolean>;
|
|
90
117
|
/**
|
|
91
118
|
* The env keys an OTel-exporting child must carry. `PI_OTEL_CAPTURE_CONTENT`
|
|
92
119
|
* is pinned unconditionally (policy 2 above); the service name only fills in
|
|
@@ -125,10 +152,12 @@ export declare function fetchWorkspaceOtel(creds: {
|
|
|
125
152
|
token: string;
|
|
126
153
|
}, profileName: string, deps?: WorkspaceOtelFetchDeps): Promise<WorkspaceOtelConfig | null>;
|
|
127
154
|
/**
|
|
128
|
-
* Full launch-time resolution, all
|
|
155
|
+
* Full launch-time resolution, all four sources in precedence order:
|
|
129
156
|
*
|
|
130
157
|
* 1. `PI_OTEL_DISABLED` — personal kill switch, beats everything.
|
|
131
|
-
* 2. env `OTEL_EXPORTER_OTLP_ENDPOINT`
|
|
158
|
+
* 2. env `OTEL_EXPORTER_OTLP_ENDPOINT` / `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`
|
|
159
|
+
* — the user's own setup, untouched (generic wins
|
|
160
|
+
* over per-signal).
|
|
132
161
|
* 3. workspace config — admin-set in the web app, fetched/cached.
|
|
133
162
|
* 4. repo `.pi/settings.json` — committed per-repo config.
|
|
134
163
|
*
|
package/dist/otel.js
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* missing package or unreadable settings file resolves to "disabled".
|
|
30
30
|
*/
|
|
31
31
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
32
|
+
import { createConnection } from "node:net";
|
|
32
33
|
import { join } from "node:path";
|
|
33
34
|
import { fileURLToPath } from "node:url";
|
|
34
35
|
import { credentialsDir } from "./credentials.js";
|
|
@@ -71,16 +72,25 @@ function envDisabled(env) {
|
|
|
71
72
|
* Decide whether this launch exports OTel traces, and with which pi-otel entry.
|
|
72
73
|
* Returns undefined when export stays off: no endpoint configured anywhere,
|
|
73
74
|
* `PI_OTEL_DISABLED` set, or the pi-otel package unresolvable (never fatal).
|
|
75
|
+
*
|
|
76
|
+
* Env sources, in precedence order: the generic `OTEL_EXPORTER_OTLP_ENDPOINT`,
|
|
77
|
+
* then the per-signal `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`. pi-otel only reads
|
|
78
|
+
* the generic var, so the per-signal one (the standard OTel env shape, and
|
|
79
|
+
* what Claude Code setups configure — e.g. Updater's) is bridged: the traces
|
|
80
|
+
* endpoint becomes the generic one, and a per-signal TRACES_PROTOCOL carries
|
|
81
|
+
* over the same way. Logs/metrics per-signal vars are deliberately NOT read:
|
|
82
|
+
* sessions export traces only, and pi-otel's metrics/logs signals are opt-in
|
|
83
|
+
* elsewhere.
|
|
74
84
|
*/
|
|
75
85
|
export function resolveOtelLaunch(opts) {
|
|
76
86
|
const { env, cwd } = opts;
|
|
77
87
|
if (envDisabled(env))
|
|
78
88
|
return undefined;
|
|
79
89
|
const envEndpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!envEndpoint && !settingsEndpoint)
|
|
90
|
+
const tracesEndpoint = env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim();
|
|
91
|
+
const usingPerSignal = !envEndpoint && !!tracesEndpoint;
|
|
92
|
+
const settingsEndpoint = envEndpoint || tracesEndpoint ? undefined : projectOtelEndpoint(cwd, opts.readFile ?? readFileSync);
|
|
93
|
+
if (!envEndpoint && !tracesEndpoint && !settingsEndpoint)
|
|
84
94
|
return undefined;
|
|
85
95
|
let extensionPath;
|
|
86
96
|
try {
|
|
@@ -89,12 +99,28 @@ export function resolveOtelLaunch(opts) {
|
|
|
89
99
|
catch {
|
|
90
100
|
return undefined;
|
|
91
101
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
if (envEndpoint) {
|
|
103
|
+
return {
|
|
104
|
+
extensionPath,
|
|
105
|
+
source: "env",
|
|
106
|
+
envVar: "OTEL_EXPORTER_OTLP_ENDPOINT",
|
|
107
|
+
endpoint: envEndpoint,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (usingPerSignal) {
|
|
111
|
+
return {
|
|
112
|
+
extensionPath,
|
|
113
|
+
source: "env",
|
|
114
|
+
envVar: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
|
|
115
|
+
endpoint: tracesEndpoint,
|
|
116
|
+
...(env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL?.trim()
|
|
117
|
+
? { protocol: env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL.trim() }
|
|
118
|
+
: {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { extensionPath, source: "project-settings", endpoint: settingsEndpoint };
|
|
95
122
|
}
|
|
96
|
-
/**
|
|
97
|
-
* Absolute path to pi-otel's extension entry (its package main IS the pi
|
|
123
|
+
/** Absolute path to pi-otel's extension entry (its package main IS the pi
|
|
98
124
|
* extension entry, `dist/index.js`). Resolved ESM-native like `paths.ts` does
|
|
99
125
|
* for pi itself. Throws when the package is missing — callers treat that as
|
|
100
126
|
* "export off", not an error.
|
|
@@ -106,6 +132,45 @@ export function resolveOtelExtensionPath() {
|
|
|
106
132
|
}
|
|
107
133
|
return path;
|
|
108
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* TCP reachability probe for an OTLP endpoint URL, with scheme-default ports:
|
|
137
|
+
* WHATWG `URL` strips `:443`/`:80` as defaults, so an https endpoint with no
|
|
138
|
+
* port probes 443 (the agentless SaaS shape). Returns false on any parse or
|
|
139
|
+
* connect failure — reachability is advisory, never fatal. Mirrors the probe
|
|
140
|
+
* inside (patched) pi-otel but lives here so `doctor` can use it without
|
|
141
|
+
* importing the extension's SDK graph.
|
|
142
|
+
*/
|
|
143
|
+
export async function probeOtelEndpoint(endpoint, timeoutMs = 1_000) {
|
|
144
|
+
let host;
|
|
145
|
+
let port;
|
|
146
|
+
try {
|
|
147
|
+
const u = new URL(endpoint);
|
|
148
|
+
host = u.hostname;
|
|
149
|
+
port = u.port
|
|
150
|
+
? Number(u.port)
|
|
151
|
+
: u.protocol === "https:"
|
|
152
|
+
? 443
|
|
153
|
+
: u.protocol === "http:"
|
|
154
|
+
? 80
|
|
155
|
+
: null;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
if (!host || !port)
|
|
161
|
+
return false;
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
const sock = createConnection({ host, port });
|
|
164
|
+
const done = (ok) => {
|
|
165
|
+
sock.destroy();
|
|
166
|
+
resolve(ok);
|
|
167
|
+
};
|
|
168
|
+
sock.setTimeout(timeoutMs);
|
|
169
|
+
sock.once("connect", () => done(true));
|
|
170
|
+
sock.once("timeout", () => done(false));
|
|
171
|
+
sock.once("error", () => done(false));
|
|
172
|
+
});
|
|
173
|
+
}
|
|
109
174
|
/**
|
|
110
175
|
* The env keys an OTel-exporting child must carry. `PI_OTEL_CAPTURE_CONTENT`
|
|
111
176
|
* is pinned unconditionally (policy 2 above); the service name only fills in
|
|
@@ -135,6 +200,17 @@ export function otelChildEnv(config, baseEnv) {
|
|
|
135
200
|
workspace.OTEL_SERVICE_NAME = config.serviceName;
|
|
136
201
|
}
|
|
137
202
|
}
|
|
203
|
+
else if (config.source === "env" && config.protocol) {
|
|
204
|
+
// The per-signal bridge: the endpoint came from
|
|
205
|
+
// OTEL_EXPORTER_OTLP_TRACES_ENDPOINT (+ TRACES_PROTOCOL), which pi-otel
|
|
206
|
+
// does not read — deliver them as the generic vars the extension resolves.
|
|
207
|
+
if (!baseEnv.OTEL_EXPORTER_OTLP_ENDPOINT) {
|
|
208
|
+
workspace.OTEL_EXPORTER_OTLP_ENDPOINT = config.endpoint;
|
|
209
|
+
}
|
|
210
|
+
if (!baseEnv.OTEL_EXPORTER_OTLP_PROTOCOL) {
|
|
211
|
+
workspace.OTEL_EXPORTER_OTLP_PROTOCOL = config.protocol;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
138
214
|
return {
|
|
139
215
|
...workspace,
|
|
140
216
|
[OTEL_EXTENSION_PATH_ENV]: config.extensionPath,
|
|
@@ -235,10 +311,12 @@ function readWorkspaceOtelCache(cachePath) {
|
|
|
235
311
|
}
|
|
236
312
|
}
|
|
237
313
|
/**
|
|
238
|
-
* Full launch-time resolution, all
|
|
314
|
+
* Full launch-time resolution, all four sources in precedence order:
|
|
239
315
|
*
|
|
240
316
|
* 1. `PI_OTEL_DISABLED` — personal kill switch, beats everything.
|
|
241
|
-
* 2. env `OTEL_EXPORTER_OTLP_ENDPOINT`
|
|
317
|
+
* 2. env `OTEL_EXPORTER_OTLP_ENDPOINT` / `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`
|
|
318
|
+
* — the user's own setup, untouched (generic wins
|
|
319
|
+
* over per-signal).
|
|
242
320
|
* 3. workspace config — admin-set in the web app, fetched/cached.
|
|
243
321
|
* 4. repo `.pi/settings.json` — committed per-repo config.
|
|
244
322
|
*
|
|
@@ -249,7 +327,7 @@ export async function resolveOtelLaunchWithWorkspace(opts) {
|
|
|
249
327
|
if (envDisabled(env))
|
|
250
328
|
return undefined;
|
|
251
329
|
// Source 2: the user's own env config short-circuits — no fetch needed.
|
|
252
|
-
if (env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim()) {
|
|
330
|
+
if (env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim()) {
|
|
253
331
|
return resolveOtelLaunch({
|
|
254
332
|
env,
|
|
255
333
|
cwd,
|
package/dist/upgrade.d.ts
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export declare const PACKAGE_NAME: string;
|
|
18
18
|
export declare const UPDATE_CHECK_DISABLE_ENV = "YAGNI_DISABLE_UPDATE_CHECK";
|
|
19
|
-
export type InstallMethod = "npm" | "pnpm" | "bun";
|
|
19
|
+
export type InstallMethod = "npm" | "pnpm" | "bun" | "brew";
|
|
20
|
+
/** Fully qualified so `brew upgrade` can never resolve someone else's `yagni`. */
|
|
21
|
+
export declare const BREW_FORMULA = "yagni-app/tap/yagni";
|
|
20
22
|
/**
|
|
21
23
|
* The CLI's own version, read from this package's package.json. Resolved
|
|
22
24
|
* relative to the module (src/ and dist/ both sit one level below the package
|
|
@@ -70,8 +72,15 @@ export declare function maybeNudgeAndRefresh(deps: NudgeDeps): Promise<{
|
|
|
70
72
|
}>;
|
|
71
73
|
/**
|
|
72
74
|
* Which global installer owns the running bin, from its (real) path. The
|
|
73
|
-
* heuristic covers the
|
|
75
|
+
* heuristic covers the four installers the package supports; `--method` is
|
|
74
76
|
* the escape hatch when a layout defeats it.
|
|
77
|
+
*
|
|
78
|
+
* The path is realpath'd first because Homebrew is invisible in argv[1]:
|
|
79
|
+
* `/opt/homebrew/bin/yagni` is a symlink chain into the Cellar, and only the
|
|
80
|
+
* resolved path contains the `/Cellar/` segment (macOS, Intel /usr/local,
|
|
81
|
+
* and Linuxbrew all share it). Detecting brew matters: running `npm install
|
|
82
|
+
* -g` over a brew install creates a shadowing parallel copy — or the exact
|
|
83
|
+
* managed-Mac EACCES the brew channel exists to avoid.
|
|
75
84
|
*/
|
|
76
85
|
export declare function detectInstallMethod(binPath: string): InstallMethod;
|
|
77
86
|
export type ParsedUpgradeArgs = {
|
package/dist/upgrade.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* `yagni upgrade` still works there.
|
|
16
16
|
*/
|
|
17
17
|
import { spawn } from "node:child_process";
|
|
18
|
-
import { readFileSync } from "node:fs";
|
|
18
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
19
19
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
20
20
|
import { dirname, join } from "node:path";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
@@ -26,7 +26,9 @@ export const UPDATE_CHECK_DISABLE_ENV = "YAGNI_DISABLE_UPDATE_CHECK";
|
|
|
26
26
|
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
|
27
27
|
const FETCH_TIMEOUT_MS = 5_000;
|
|
28
28
|
const SEMVER_RE = /^(0|[1-9]\d{0,5})\.(0|[1-9]\d{0,5})\.(0|[1-9]\d{0,5})(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
29
|
-
const INSTALL_METHODS = ["npm", "pnpm", "bun"];
|
|
29
|
+
const INSTALL_METHODS = ["npm", "pnpm", "bun", "brew"];
|
|
30
|
+
/** Fully qualified so `brew upgrade` can never resolve someone else's `yagni`. */
|
|
31
|
+
export const BREW_FORMULA = "yagni-app/tap/yagni";
|
|
30
32
|
/**
|
|
31
33
|
* The CLI's own version, read from this package's package.json. Resolved
|
|
32
34
|
* relative to the module (src/ and dist/ both sit one level below the package
|
|
@@ -188,18 +190,34 @@ export async function maybeNudgeAndRefresh(deps) {
|
|
|
188
190
|
// ── `yagni upgrade` ─────────────────────────────────────────────────────────
|
|
189
191
|
/**
|
|
190
192
|
* Which global installer owns the running bin, from its (real) path. The
|
|
191
|
-
* heuristic covers the
|
|
193
|
+
* heuristic covers the four installers the package supports; `--method` is
|
|
192
194
|
* the escape hatch when a layout defeats it.
|
|
195
|
+
*
|
|
196
|
+
* The path is realpath'd first because Homebrew is invisible in argv[1]:
|
|
197
|
+
* `/opt/homebrew/bin/yagni` is a symlink chain into the Cellar, and only the
|
|
198
|
+
* resolved path contains the `/Cellar/` segment (macOS, Intel /usr/local,
|
|
199
|
+
* and Linuxbrew all share it). Detecting brew matters: running `npm install
|
|
200
|
+
* -g` over a brew install creates a shadowing parallel copy — or the exact
|
|
201
|
+
* managed-Mac EACCES the brew channel exists to avoid.
|
|
193
202
|
*/
|
|
194
203
|
export function detectInstallMethod(binPath) {
|
|
195
|
-
|
|
204
|
+
let resolved = binPath;
|
|
205
|
+
try {
|
|
206
|
+
resolved = realpathSync(binPath);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// Nonexistent or unreadable (tests, exotic layouts): judge the raw path.
|
|
210
|
+
}
|
|
211
|
+
const normalized = resolved.replaceAll("\\", "/").toLowerCase();
|
|
212
|
+
if (normalized.includes("/cellar/"))
|
|
213
|
+
return "brew";
|
|
196
214
|
if (normalized.includes("/.bun/"))
|
|
197
215
|
return "bun";
|
|
198
216
|
if (normalized.includes("pnpm"))
|
|
199
217
|
return "pnpm";
|
|
200
218
|
return "npm";
|
|
201
219
|
}
|
|
202
|
-
const UPGRADE_USAGE = `Usage: ${DISTRIBUTION.commandName} upgrade [version] [--method npm|pnpm|bun]`;
|
|
220
|
+
const UPGRADE_USAGE = `Usage: ${DISTRIBUTION.commandName} upgrade [version] [--method npm|pnpm|bun|brew]`;
|
|
203
221
|
export function parseUpgradeArgs(args) {
|
|
204
222
|
let target;
|
|
205
223
|
let method;
|
|
@@ -232,9 +250,22 @@ export function parseUpgradeArgs(args) {
|
|
|
232
250
|
return { ok: true, target, method };
|
|
233
251
|
}
|
|
234
252
|
function installArgv(method, target) {
|
|
253
|
+
// brew upgrades to whatever the tap formula publishes; it cannot pin a
|
|
254
|
+
// version, so `target` only tells us an upgrade is worthwhile.
|
|
255
|
+
if (method === "brew")
|
|
256
|
+
return ["upgrade", BREW_FORMULA];
|
|
235
257
|
const spec = `${PACKAGE_NAME}@${target}`;
|
|
236
258
|
return method === "npm" ? ["install", "-g", spec] : ["add", "-g", spec];
|
|
237
259
|
}
|
|
260
|
+
function failureHint(method) {
|
|
261
|
+
if (method === "brew") {
|
|
262
|
+
return `Try \`brew update\` and \`brew reinstall ${BREW_FORMULA}\`, or \`brew doctor\` if that fails too.`;
|
|
263
|
+
}
|
|
264
|
+
// Never suggest sudo: on managed machines the fix is a user-writable
|
|
265
|
+
// prefix, which the installer sets up automatically.
|
|
266
|
+
return ("If it was a permissions error, the installer repairs the global prefix without sudo: " +
|
|
267
|
+
"curl -fsSL https://yagni.app/install.sh | sh");
|
|
268
|
+
}
|
|
238
269
|
function defaultRunInstall(command, args) {
|
|
239
270
|
return new Promise((resolve) => {
|
|
240
271
|
// npm/pnpm/bun are .cmd shims on Windows; a shell is required to run them.
|
|
@@ -256,6 +287,11 @@ export async function upgradeCommand(args, deps) {
|
|
|
256
287
|
}
|
|
257
288
|
const env = deps.env ?? process.env;
|
|
258
289
|
const method = parsed.method ?? detectInstallMethod(deps.binPath ?? process.argv[1] ?? "");
|
|
290
|
+
if (method === "brew" && parsed.target !== undefined) {
|
|
291
|
+
logError("Homebrew installs track the tap formula and cannot pin a version. " +
|
|
292
|
+
`Run \`${DISTRIBUTION.commandName} upgrade\` for the newest release.`);
|
|
293
|
+
return 1;
|
|
294
|
+
}
|
|
259
295
|
const fetchLatest = deps.fetchLatest ?? (() => fetchLatestVersion({ env }));
|
|
260
296
|
const target = parsed.target ?? (await fetchLatest());
|
|
261
297
|
if (!target) {
|
|
@@ -272,13 +308,17 @@ export async function upgradeCommand(args, deps) {
|
|
|
272
308
|
const runInstall = deps.runInstall ?? defaultRunInstall;
|
|
273
309
|
const code = await runInstall(method, installArgv(method, target));
|
|
274
310
|
if (code !== 0) {
|
|
275
|
-
logError(`Upgrade failed (${method} exited ${code}). `
|
|
276
|
-
`If it was a permissions error, fix your ${method} global prefix or retry with elevated permissions.`);
|
|
311
|
+
logError(`Upgrade failed (${method} exited ${code}). ${failureHint(method)}`);
|
|
277
312
|
return 1;
|
|
278
313
|
}
|
|
279
314
|
// Keep the nudge honest: the freshly installed version is the known latest.
|
|
280
315
|
await writeUpdateCache(target).catch(() => undefined);
|
|
281
|
-
|
|
316
|
+
// brew lands on whatever the formula publishes, which can trail the
|
|
317
|
+
// registry briefly, so only the package managers that pin get the
|
|
318
|
+
// version-specific claim.
|
|
319
|
+
log(method === "brew"
|
|
320
|
+
? `${DISTRIBUTION.displayName} upgraded via Homebrew. Run \`${DISTRIBUTION.commandName} version\` to confirm.`
|
|
321
|
+
: `${DISTRIBUTION.displayName} ${target} installed. Run \`${DISTRIBUTION.commandName} version\` to confirm.`);
|
|
282
322
|
return 0;
|
|
283
323
|
}
|
|
284
324
|
//# sourceMappingURL=upgrade.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -36,10 +36,11 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
38
38
|
"@earendil-works/pi-tui": "0.84.1",
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
39
40
|
"pi-otel": "0.1.0",
|
|
40
41
|
"smol-toml": "^1.8.0",
|
|
41
42
|
"turndown": "^7.2.4",
|
|
42
43
|
"typebox": "^1.3.15"
|
|
43
44
|
},
|
|
44
|
-
"yagniSourceSha": "
|
|
45
|
+
"yagniSourceSha": "09cd57add6ba5c3028cc4f971459d5c7f9d4da97"
|
|
45
46
|
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workspace MCP servers in YAGNI Code (YAG-446).
|
|
3
|
-
*
|
|
4
|
-
* The workspace's registered MCP servers (Connections → MCP Servers) are
|
|
5
|
-
* the config source: at session start we fetch the list from the backend
|
|
6
|
-
* and register one pi tool per enabled server tool, named with the same
|
|
7
|
-
* `mcp_<slug>__<tool>` wire shape the backend executor parses. Calls
|
|
8
|
-
* execute THROUGH the backend (`POST /api/yagni-code/mcp/call`), so
|
|
9
|
-
* credentials never reach this machine and the backend's egress guard,
|
|
10
|
-
* autonomy gate, per-user credential policy, and audit trail all apply.
|
|
11
|
-
*
|
|
12
|
-
* Everything here is fail-soft against an older backend or an
|
|
13
|
-
* un-rescoped token: a 403/404/network failure on the list fetch means
|
|
14
|
-
* no MCP tools and no startup error.
|
|
15
|
-
*/
|
|
16
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
export interface McpToolInfo {
|
|
18
|
-
name: string;
|
|
19
|
-
description: string | null;
|
|
20
|
-
inputSchema: unknown;
|
|
21
|
-
riskClass: string;
|
|
22
|
-
}
|
|
23
|
-
export interface McpServerInfo {
|
|
24
|
-
slug: string;
|
|
25
|
-
name: string;
|
|
26
|
-
tools: McpToolInfo[];
|
|
27
|
-
viewerCredential: {
|
|
28
|
-
source: "user" | "workspace";
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
export interface McpClientOpts {
|
|
32
|
-
baseUrl: string;
|
|
33
|
-
getToken: () => string | undefined;
|
|
34
|
-
fetchImpl?: typeof fetch;
|
|
35
|
-
}
|
|
36
|
-
export declare function mcpWireToolName(slug: string, tool: string): string;
|
|
37
|
-
/**
|
|
38
|
-
* Fetch the workspace's MCP servers. Fail-soft by design: any non-OK
|
|
39
|
-
* response (older backend without the endpoints, token minted before the
|
|
40
|
-
* `yagni_code:mcp` scope existed) or transport error returns `[]`.
|
|
41
|
-
*/
|
|
42
|
-
export declare function fetchMcpServers(opts: McpClientOpts): Promise<McpServerInfo[]>;
|
|
43
|
-
/**
|
|
44
|
-
* Register every reachable MCP tool. Returns the wire names of MUTATING
|
|
45
|
-
* tools so the caller can extend the permission-gate policy — plan mode
|
|
46
|
-
* holds them, review mode confirms them, exactly like write/edit/bash.
|
|
47
|
-
*/
|
|
48
|
-
export declare function registerMcpTools(pi: ExtensionAPI, servers: McpServerInfo[], opts: McpClientOpts): {
|
|
49
|
-
mutatingToolNames: string[];
|
|
50
|
-
};
|
|
51
|
-
/** Render the /mcp listing (pure, for tests). */
|
|
52
|
-
export declare function renderMcpListing(servers: McpServerInfo[], baseUrl: string): string;
|
|
53
|
-
/** Wire the `/mcp` command: list servers, tools, and credential status. */
|
|
54
|
-
export declare function registerMcpCommand(pi: ExtensionAPI, servers: McpServerInfo[], opts: {
|
|
55
|
-
baseUrl: string;
|
|
56
|
-
}): void;
|
|
57
|
-
//# sourceMappingURL=mcpTools.d.ts.map
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { Type } from "typebox";
|
|
2
|
-
import { friendlyFetchError, METERED_POST_FETCH_POLICY, resilientFetch } from "./resilientFetch.js";
|
|
3
|
-
export function mcpWireToolName(slug, tool) {
|
|
4
|
-
return `mcp_${slug}__${tool}`;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Fetch the workspace's MCP servers. Fail-soft by design: any non-OK
|
|
8
|
-
* response (older backend without the endpoints, token minted before the
|
|
9
|
-
* `yagni_code:mcp` scope existed) or transport error returns `[]`.
|
|
10
|
-
*/
|
|
11
|
-
export async function fetchMcpServers(opts) {
|
|
12
|
-
try {
|
|
13
|
-
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/mcp/servers`, {
|
|
14
|
-
method: "GET",
|
|
15
|
-
headers: { authorization: `Bearer ${opts.getToken() ?? ""}` },
|
|
16
|
-
}, { fetchImpl: opts.fetchImpl });
|
|
17
|
-
if (!res.ok)
|
|
18
|
-
return [];
|
|
19
|
-
const data = (await res.json());
|
|
20
|
-
if (!Array.isArray(data.servers))
|
|
21
|
-
return [];
|
|
22
|
-
return data.servers;
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return [];
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function schemaFor(tool) {
|
|
29
|
-
const schema = tool.inputSchema;
|
|
30
|
-
if (schema !== null &&
|
|
31
|
-
typeof schema === "object" &&
|
|
32
|
-
!Array.isArray(schema) &&
|
|
33
|
-
Object.keys(schema).length > 0) {
|
|
34
|
-
// MCP inputSchemas are plain JSON Schema objects, which is exactly what
|
|
35
|
-
// TypeBox schemas are at runtime — pass the server's schema through so
|
|
36
|
-
// the model sees real parameter shapes.
|
|
37
|
-
return schema;
|
|
38
|
-
}
|
|
39
|
-
return Type.Object({}, { additionalProperties: true });
|
|
40
|
-
}
|
|
41
|
-
function makeMcpTool(server, tool, opts) {
|
|
42
|
-
const wireName = mcpWireToolName(server.slug, tool.name);
|
|
43
|
-
return {
|
|
44
|
-
name: wireName,
|
|
45
|
-
label: `${server.name}: ${tool.name}`,
|
|
46
|
-
description: `${tool.description ?? `MCP tool ${tool.name}`} ` +
|
|
47
|
-
`(MCP server "${server.name}", risk: ${tool.riskClass}; runs through YAGNI)`,
|
|
48
|
-
parameters: schemaFor(tool),
|
|
49
|
-
async execute(_toolCallId, params, signal) {
|
|
50
|
-
const res = await resilientFetch(`${opts.baseUrl}/api/yagni-code/mcp/call`, {
|
|
51
|
-
method: "POST",
|
|
52
|
-
headers: {
|
|
53
|
-
"content-type": "application/json",
|
|
54
|
-
authorization: `Bearer ${opts.getToken() ?? ""}`,
|
|
55
|
-
},
|
|
56
|
-
body: JSON.stringify({
|
|
57
|
-
slug: server.slug,
|
|
58
|
-
tool: tool.name,
|
|
59
|
-
args: params ?? {},
|
|
60
|
-
}),
|
|
61
|
-
}, { fetchImpl: opts.fetchImpl, signal, policy: METERED_POST_FETCH_POLICY });
|
|
62
|
-
if (!res.ok) {
|
|
63
|
-
throw new Error(await friendlyFetchError(wireName, res));
|
|
64
|
-
}
|
|
65
|
-
const result = (await res.json());
|
|
66
|
-
if (!result.success) {
|
|
67
|
-
const suffix = result.errorCode === "user_credential_required"
|
|
68
|
-
? ` Bind your token at ${opts.baseUrl}/services/mcp.`
|
|
69
|
-
: "";
|
|
70
|
-
throw new Error(`${result.error ?? "MCP tool call failed"}${suffix}`);
|
|
71
|
-
}
|
|
72
|
-
const text = typeof result.result === "string"
|
|
73
|
-
? result.result
|
|
74
|
-
: JSON.stringify(result.result ?? null, null, 2);
|
|
75
|
-
return {
|
|
76
|
-
content: [{ type: "text", text }],
|
|
77
|
-
details: { riskClass: tool.riskClass },
|
|
78
|
-
};
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Register every reachable MCP tool. Returns the wire names of MUTATING
|
|
84
|
-
* tools so the caller can extend the permission-gate policy — plan mode
|
|
85
|
-
* holds them, review mode confirms them, exactly like write/edit/bash.
|
|
86
|
-
*/
|
|
87
|
-
export function registerMcpTools(pi, servers, opts) {
|
|
88
|
-
const mutatingToolNames = [];
|
|
89
|
-
for (const server of servers) {
|
|
90
|
-
for (const tool of server.tools) {
|
|
91
|
-
pi.registerTool(makeMcpTool(server, tool, opts));
|
|
92
|
-
if (tool.riskClass !== "read_only") {
|
|
93
|
-
mutatingToolNames.push(mcpWireToolName(server.slug, tool.name));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
return { mutatingToolNames };
|
|
98
|
-
}
|
|
99
|
-
/** Render the /mcp listing (pure, for tests). */
|
|
100
|
-
export function renderMcpListing(servers, baseUrl) {
|
|
101
|
-
if (servers.length === 0) {
|
|
102
|
-
return [
|
|
103
|
-
"No MCP servers connected for this workspace.",
|
|
104
|
-
`An admin can register one under Connections: ${baseUrl}/services/mcp`,
|
|
105
|
-
].join("\n");
|
|
106
|
-
}
|
|
107
|
-
const lines = ["Workspace MCP servers:"];
|
|
108
|
-
for (const server of servers) {
|
|
109
|
-
const mutating = server.tools.filter((t) => t.riskClass !== "read_only").length;
|
|
110
|
-
const credential = server.viewerCredential.source === "user"
|
|
111
|
-
? "connected as you"
|
|
112
|
-
: "workspace credential";
|
|
113
|
-
lines.push(` ${server.name} (${server.slug}) — ${server.tools.length} tool${server.tools.length === 1 ? "" : "s"}` +
|
|
114
|
-
`${mutating > 0 ? ` (${mutating} mutating)` : ""} · ${credential}`);
|
|
115
|
-
for (const tool of server.tools) {
|
|
116
|
-
lines.push(` ${mcpWireToolName(server.slug, tool.name)} [${tool.riskClass}]`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
lines.push(`Bind a personal token (writes carry your authority): ${baseUrl}/services/mcp`);
|
|
120
|
-
return lines.join("\n");
|
|
121
|
-
}
|
|
122
|
-
/** Wire the `/mcp` command: list servers, tools, and credential status. */
|
|
123
|
-
export function registerMcpCommand(pi, servers, opts) {
|
|
124
|
-
pi.registerCommand("mcp", {
|
|
125
|
-
description: "List this workspace's MCP servers, their tools, and your credential status.",
|
|
126
|
-
handler: async (_args, ctx) => {
|
|
127
|
-
if (ctx.hasUI)
|
|
128
|
-
ctx.ui.notify(renderMcpListing(servers, opts.baseUrl), "info");
|
|
129
|
-
},
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
//# sourceMappingURL=mcpTools.js.map
|