@yagni-app/code-staging 0.1.0-staging.997.1 → 0.2.0-staging.1025.1
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 +58 -9
- package/dist/claudeCompat.d.ts +36 -5
- package/dist/claudeCompat.js +85 -23
- package/dist/claudePlugins.d.ts +109 -0
- package/dist/claudePlugins.js +336 -0
- package/dist/cli.js +14 -4
- package/dist/crashReport.d.ts +135 -0
- package/dist/crashReport.js +291 -0
- package/dist/doctor.d.ts +21 -0
- package/dist/doctor.js +52 -0
- package/dist/extension/askAdvisorTool.js +7 -1
- package/dist/extension/bless.js +16 -3
- package/dist/extension/boostCommand.d.ts +144 -0
- package/dist/extension/boostCommand.js +263 -0
- package/dist/extension/branding.d.ts +31 -0
- package/dist/extension/branding.js +37 -0
- package/dist/extension/chipEditor.js +7 -3
- package/dist/extension/claudeRules.d.ts +54 -0
- package/dist/extension/claudeRules.js +180 -0
- package/dist/extension/config.d.ts +61 -0
- package/dist/extension/config.js +86 -0
- package/dist/extension/costHud.d.ts +128 -15
- package/dist/extension/costHud.js +189 -19
- package/dist/extension/crashReport.d.ts +89 -0
- package/dist/extension/crashReport.js +241 -0
- package/dist/extension/index.d.ts +43 -4
- package/dist/extension/index.js +241 -32
- package/dist/extension/initPass.d.ts +65 -47
- package/dist/extension/initPass.js +145 -145
- package/dist/extension/mcpTools.d.ts +57 -0
- package/dist/extension/mcpTools.js +132 -0
- package/dist/extension/pipeline/eval.d.ts +42 -5
- package/dist/extension/pipeline/eval.js +44 -0
- package/dist/extension/pipeline/goCommand.d.ts +18 -0
- package/dist/extension/pipeline/goCommand.js +139 -26
- package/dist/extension/pipeline/goCompareCommand.d.ts +18 -8
- package/dist/extension/pipeline/goCompareCommand.js +42 -23
- package/dist/extension/pipeline/orchestrator.js +9 -0
- package/dist/extension/pipeline/runCostTable.d.ts +37 -0
- package/dist/extension/pipeline/runCostTable.js +165 -0
- package/dist/extension/pipeline/runState.d.ts +19 -0
- package/dist/extension/pipeline/runState.js +11 -0
- package/dist/extension/pipeline/runner.d.ts +19 -0
- package/dist/extension/pipeline/runner.js +13 -1
- package/dist/extension/pipeline/scrubSecrets.js +2 -2
- package/dist/extension/pipeline/stages.d.ts +3 -1
- package/dist/extension/pipeline/stages.js +3 -1
- package/dist/extension/pipeline/types.d.ts +7 -4
- package/dist/extension/pipeline/verify.js +6 -1
- package/dist/extension/pipeline/worktree.js +3 -1
- package/dist/extension/provider.d.ts +7 -1
- package/dist/extension/provider.js +8 -1
- package/dist/extension/recall.js +5 -2
- package/dist/extension/rerouteNotice.d.ts +42 -0
- package/dist/extension/rerouteNotice.js +67 -0
- package/dist/extension/sessionRuns.d.ts +45 -0
- package/dist/extension/sessionRuns.js +77 -0
- package/dist/extension/subagents.d.ts +17 -7
- package/dist/extension/subagents.js +52 -7
- package/dist/launch.d.ts +17 -3
- package/dist/launch.js +22 -9
- package/dist/login.d.ts +7 -0
- package/dist/login.js +3 -1
- package/package.json +2 -2
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code plugin + marketplace discovery (local content only).
|
|
3
|
+
*
|
|
4
|
+
* Finds the plugin content a Claude Code user already has on disk and exposes
|
|
5
|
+
* it as asset paths the launcher can feed through the existing compat seams:
|
|
6
|
+
* skills → `--skill`, commands → `--prompt-template`, agents → the subagent
|
|
7
|
+
* discovery env var. Two sources:
|
|
8
|
+
*
|
|
9
|
+
* 1. Installed plugins: `~/.claude/plugins/installed_plugins.json` (version-2
|
|
10
|
+
* ledger of `name@marketplace` → cache checkouts), enabled through
|
|
11
|
+
* `enabledPlugins` in the user's `~/.claude/settings.json` (user scope) or
|
|
12
|
+
* the repo's `.claude/settings{,.local}.json` (project scope). When a
|
|
13
|
+
* ledger entry is gone, the marketplace checkout recorded in
|
|
14
|
+
* `known_marketplaces.json` is tried as a fallback.
|
|
15
|
+
* 2. The repo's own `.claude-plugin/marketplace.json` (the internal-
|
|
16
|
+
* marketplace pattern): every plugin it defines with a locally-present
|
|
17
|
+
* relative-path source loads on the project side, unless explicitly
|
|
18
|
+
* disabled via `enabledPlugins`.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately NOT here: network installation of any kind, plugin hooks, MCP
|
|
21
|
+
* servers, LSP servers, themes, output styles. Discovery is read-only.
|
|
22
|
+
*
|
|
23
|
+
* Everything is fail-soft: malformed JSON, missing dirs, or hostile path
|
|
24
|
+
* entries degrade to "that plugin absent" — never a failed launch. Path
|
|
25
|
+
* entries from marketplace/plugin manifests are containment-checked (realpath
|
|
26
|
+
* inside the expected root) so a malicious `source` or component override
|
|
27
|
+
* cannot reach outside its repo/plugin.
|
|
28
|
+
*/
|
|
29
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
30
|
+
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
31
|
+
// ── fail-soft fs/json primitives ────────────────────────────────────────────
|
|
32
|
+
function isDirectory(path) {
|
|
33
|
+
try {
|
|
34
|
+
return statSync(path).isDirectory();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function isFile(path) {
|
|
41
|
+
try {
|
|
42
|
+
return statSync(path).isFile();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function readJsonObject(path) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
51
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
52
|
+
return null;
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function realOrNull(path) {
|
|
60
|
+
try {
|
|
61
|
+
return realpathSync(path);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve `candidate` (a relative path from untrusted JSON) against `root` and
|
|
69
|
+
* return its realpath only when it exists AND stays inside `root` after
|
|
70
|
+
* symlink resolution. Absolute candidates and any `../` escape are rejected.
|
|
71
|
+
*/
|
|
72
|
+
export function containedExistingPath(root, candidate) {
|
|
73
|
+
if (typeof candidate !== "string" || candidate.length === 0)
|
|
74
|
+
return null;
|
|
75
|
+
if (isAbsolute(candidate))
|
|
76
|
+
return null;
|
|
77
|
+
const realRoot = realOrNull(root);
|
|
78
|
+
if (!realRoot)
|
|
79
|
+
return null;
|
|
80
|
+
const real = realOrNull(resolve(root, candidate));
|
|
81
|
+
if (!real)
|
|
82
|
+
return null;
|
|
83
|
+
if (real !== realRoot && !real.startsWith(realRoot + sep))
|
|
84
|
+
return null;
|
|
85
|
+
return real;
|
|
86
|
+
}
|
|
87
|
+
/** `~/.claude/plugins/installed_plugins.json` → `name@marketplace` → entries. */
|
|
88
|
+
export function readInstalledPlugins(homeDir) {
|
|
89
|
+
const out = new Map();
|
|
90
|
+
const parsed = readJsonObject(join(homeDir, ".claude", "plugins", "installed_plugins.json"));
|
|
91
|
+
const plugins = parsed?.plugins;
|
|
92
|
+
if (!plugins || typeof plugins !== "object" || Array.isArray(plugins))
|
|
93
|
+
return out;
|
|
94
|
+
for (const [key, value] of Object.entries(plugins)) {
|
|
95
|
+
if (!Array.isArray(value))
|
|
96
|
+
continue;
|
|
97
|
+
const entries = [];
|
|
98
|
+
for (const item of value) {
|
|
99
|
+
if (!item || typeof item !== "object")
|
|
100
|
+
continue;
|
|
101
|
+
const installPath = item.installPath;
|
|
102
|
+
if (typeof installPath !== "string" || installPath.length === 0)
|
|
103
|
+
continue;
|
|
104
|
+
const projectPath = item.projectPath;
|
|
105
|
+
entries.push({
|
|
106
|
+
installPath,
|
|
107
|
+
...(typeof projectPath === "string" ? { projectPath } : {}),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (entries.length > 0)
|
|
111
|
+
out.set(key, entries);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function collectEnabled(path, scope, into) {
|
|
116
|
+
const parsed = readJsonObject(path);
|
|
117
|
+
const enabled = parsed?.enabledPlugins;
|
|
118
|
+
if (!enabled || typeof enabled !== "object" || Array.isArray(enabled))
|
|
119
|
+
return;
|
|
120
|
+
for (const [key, value] of Object.entries(enabled)) {
|
|
121
|
+
if (value === true || value === false)
|
|
122
|
+
into.set(key, { enabled: value, scope });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* `enabledPlugins` merged across scopes, later (more specific) winning:
|
|
127
|
+
* user `~/.claude/settings.json` < project `.claude/settings.json` <
|
|
128
|
+
* project `.claude/settings.local.json`.
|
|
129
|
+
*/
|
|
130
|
+
export function readEnabledPlugins(cwd, homeDir) {
|
|
131
|
+
const out = new Map();
|
|
132
|
+
collectEnabled(join(homeDir, ".claude", "settings.json"), "user", out);
|
|
133
|
+
collectEnabled(join(cwd, ".claude", "settings.json"), "project", out);
|
|
134
|
+
collectEnabled(join(cwd, ".claude", "settings.local.json"), "project", out);
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
/** `~/.claude/plugins/known_marketplaces.json` → marketplace name → checkout dir. */
|
|
138
|
+
export function readKnownMarketplaces(homeDir) {
|
|
139
|
+
const out = new Map();
|
|
140
|
+
const parsed = readJsonObject(join(homeDir, ".claude", "plugins", "known_marketplaces.json"));
|
|
141
|
+
if (!parsed)
|
|
142
|
+
return out;
|
|
143
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
144
|
+
if (!value || typeof value !== "object")
|
|
145
|
+
continue;
|
|
146
|
+
const loc = value.installLocation;
|
|
147
|
+
if (typeof loc === "string" && loc.length > 0)
|
|
148
|
+
out.set(name, loc);
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
/** Parse `<root>/.claude-plugin/marketplace.json`; null when absent/unusable. */
|
|
153
|
+
export function readMarketplace(root) {
|
|
154
|
+
const parsed = readJsonObject(join(root, ".claude-plugin", "marketplace.json"));
|
|
155
|
+
if (!parsed || typeof parsed.name !== "string" || parsed.name.length === 0)
|
|
156
|
+
return null;
|
|
157
|
+
if (!Array.isArray(parsed.plugins))
|
|
158
|
+
return null;
|
|
159
|
+
const metadata = parsed.metadata && typeof parsed.metadata === "object" && !Array.isArray(parsed.metadata)
|
|
160
|
+
? parsed.metadata
|
|
161
|
+
: {};
|
|
162
|
+
const pluginRoot = typeof metadata.pluginRoot === "string" ? metadata.pluginRoot : null;
|
|
163
|
+
const plugins = [];
|
|
164
|
+
for (const item of parsed.plugins) {
|
|
165
|
+
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
166
|
+
continue;
|
|
167
|
+
const name = item.name;
|
|
168
|
+
if (typeof name !== "string" || name.length === 0)
|
|
169
|
+
continue;
|
|
170
|
+
plugins.push({ name, source: item.source });
|
|
171
|
+
}
|
|
172
|
+
return { name: parsed.name, root, pluginRoot, plugins };
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The local directory a marketplace entry's source points at, when it is a
|
|
176
|
+
* relative-path source that exists inside the marketplace root. Object
|
|
177
|
+
* sources (github/url/npm/...) are network installs → null here.
|
|
178
|
+
*/
|
|
179
|
+
export function resolveLocalPluginRoot(mp, entry) {
|
|
180
|
+
const src = entry.source;
|
|
181
|
+
if (typeof src !== "string" || src.length === 0)
|
|
182
|
+
return null;
|
|
183
|
+
if (isAbsolute(src))
|
|
184
|
+
return null;
|
|
185
|
+
// Docs: `metadata.pluginRoot` lets a bare "formatter" mean "./plugins/formatter".
|
|
186
|
+
// Try the pluginRoot-prefixed form first, then the source as written.
|
|
187
|
+
const candidates = [];
|
|
188
|
+
if (mp.pluginRoot)
|
|
189
|
+
candidates.push(`${mp.pluginRoot}/${src}`);
|
|
190
|
+
candidates.push(src);
|
|
191
|
+
for (const candidate of candidates) {
|
|
192
|
+
const contained = containedExistingPath(mp.root, candidate);
|
|
193
|
+
if (contained && isDirectory(contained))
|
|
194
|
+
return contained;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
// ── plugin component assets ─────────────────────────────────────────────────
|
|
199
|
+
function asStringArray(value) {
|
|
200
|
+
if (typeof value === "string")
|
|
201
|
+
return [value];
|
|
202
|
+
if (Array.isArray(value))
|
|
203
|
+
return value.filter((v) => typeof v === "string");
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The bridgeable assets inside one plugin checkout, per Claude Code's
|
|
208
|
+
* component rules: `skills/` (manifest `skills` ADDS dirs), `commands/`
|
|
209
|
+
* (manifest REPLACES), `agents/` (manifest REPLACES; dirs only — pi's
|
|
210
|
+
* discovery reads whole dirs), root `SKILL.md` fallback. Null when the plugin
|
|
211
|
+
* has nothing we can bridge.
|
|
212
|
+
*/
|
|
213
|
+
export function pluginAssets(root, name) {
|
|
214
|
+
if (!isDirectory(root))
|
|
215
|
+
return null;
|
|
216
|
+
const manifest = readJsonObject(join(root, ".claude-plugin", "plugin.json")) ?? {};
|
|
217
|
+
const skillPaths = [];
|
|
218
|
+
const defaultSkills = join(root, "skills");
|
|
219
|
+
if (isDirectory(defaultSkills))
|
|
220
|
+
skillPaths.push(defaultSkills);
|
|
221
|
+
const manifestSkills = asStringArray(manifest.skills);
|
|
222
|
+
for (const rel of manifestSkills) {
|
|
223
|
+
const contained = containedExistingPath(root, rel);
|
|
224
|
+
if (contained)
|
|
225
|
+
skillPaths.push(contained);
|
|
226
|
+
}
|
|
227
|
+
if (skillPaths.length === 0 && manifestSkills.length === 0) {
|
|
228
|
+
const rootSkill = join(root, "SKILL.md");
|
|
229
|
+
if (isFile(rootSkill))
|
|
230
|
+
skillPaths.push(rootSkill);
|
|
231
|
+
}
|
|
232
|
+
const componentPaths = (manifestValue, defaultDir) => {
|
|
233
|
+
const entries = asStringArray(manifestValue);
|
|
234
|
+
if (entries.length > 0) {
|
|
235
|
+
const out = [];
|
|
236
|
+
for (const rel of entries) {
|
|
237
|
+
const contained = containedExistingPath(root, rel);
|
|
238
|
+
if (contained)
|
|
239
|
+
out.push(contained);
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
const def = join(root, defaultDir);
|
|
244
|
+
return isDirectory(def) ? [def] : [];
|
|
245
|
+
};
|
|
246
|
+
const commandPaths = componentPaths(manifest.commands, "commands");
|
|
247
|
+
const agentDirs = componentPaths(manifest.agents, "agents").filter(isDirectory);
|
|
248
|
+
if (skillPaths.length === 0 && commandPaths.length === 0 && agentDirs.length === 0)
|
|
249
|
+
return null;
|
|
250
|
+
return { name, root, skillPaths, commandPaths, agentDirs };
|
|
251
|
+
}
|
|
252
|
+
function resolveInstalledRoot(key, installed, marketplaces, realCwd) {
|
|
253
|
+
const entries = (installed.get(key) ?? []).filter((e) => isDirectory(e.installPath));
|
|
254
|
+
const projectMatch = entries.find((e) => {
|
|
255
|
+
if (typeof e.projectPath !== "string")
|
|
256
|
+
return false;
|
|
257
|
+
const real = realOrNull(e.projectPath);
|
|
258
|
+
if (!real)
|
|
259
|
+
return false;
|
|
260
|
+
return realCwd === real || realCwd.startsWith(real + sep);
|
|
261
|
+
});
|
|
262
|
+
if (projectMatch)
|
|
263
|
+
return projectMatch.installPath;
|
|
264
|
+
if (entries[0])
|
|
265
|
+
return entries[0].installPath;
|
|
266
|
+
// Cache entry gone: try the marketplace checkout's own copy of the plugin.
|
|
267
|
+
const at = key.lastIndexOf("@");
|
|
268
|
+
if (at <= 0)
|
|
269
|
+
return null;
|
|
270
|
+
const pluginName = key.slice(0, at);
|
|
271
|
+
const marketplaceName = key.slice(at + 1);
|
|
272
|
+
const location = marketplaces.get(marketplaceName);
|
|
273
|
+
if (!location)
|
|
274
|
+
return null;
|
|
275
|
+
const mp = readMarketplace(location);
|
|
276
|
+
if (!mp)
|
|
277
|
+
return null;
|
|
278
|
+
const entry = mp.plugins.find((p) => p.name === pluginName);
|
|
279
|
+
if (!entry)
|
|
280
|
+
return null;
|
|
281
|
+
return resolveLocalPluginRoot(mp, entry);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* All locally-present Claude Code plugin content relevant to `cwd`, split by
|
|
285
|
+
* the trust posture the launcher must apply. Never throws.
|
|
286
|
+
*/
|
|
287
|
+
export function discoverClaudePlugins(deps) {
|
|
288
|
+
try {
|
|
289
|
+
const installed = readInstalledPlugins(deps.homeDir);
|
|
290
|
+
const enabled = readEnabledPlugins(deps.cwd, deps.homeDir);
|
|
291
|
+
const marketplaces = readKnownMarketplaces(deps.homeDir);
|
|
292
|
+
const realCwd = realOrNull(deps.cwd) ?? resolve(deps.cwd);
|
|
293
|
+
const user = [];
|
|
294
|
+
const project = [];
|
|
295
|
+
const seen = new Set();
|
|
296
|
+
for (const [key, state] of enabled) {
|
|
297
|
+
if (!state.enabled)
|
|
298
|
+
continue;
|
|
299
|
+
const root = resolveInstalledRoot(key, installed, marketplaces, realCwd);
|
|
300
|
+
if (!root)
|
|
301
|
+
continue;
|
|
302
|
+
const at = key.lastIndexOf("@");
|
|
303
|
+
const assets = pluginAssets(root, at > 0 ? key.slice(0, at) : key);
|
|
304
|
+
if (!assets)
|
|
305
|
+
continue;
|
|
306
|
+
seen.add(key);
|
|
307
|
+
(state.scope === "user" ? user : project).push(assets);
|
|
308
|
+
}
|
|
309
|
+
// The internal-marketplace pattern: working inside the marketplace repo
|
|
310
|
+
// itself, its locally-present plugins load (trust-gated) with zero config.
|
|
311
|
+
const repoMarketplace = readMarketplace(deps.cwd);
|
|
312
|
+
if (repoMarketplace) {
|
|
313
|
+
for (const entry of repoMarketplace.plugins) {
|
|
314
|
+
const key = `${entry.name}@${repoMarketplace.name}`;
|
|
315
|
+
if (seen.has(key))
|
|
316
|
+
continue;
|
|
317
|
+
if (enabled.get(key)?.enabled === false)
|
|
318
|
+
continue;
|
|
319
|
+
const root = resolveLocalPluginRoot(repoMarketplace, entry) ??
|
|
320
|
+
resolveInstalledRoot(key, installed, marketplaces, realCwd);
|
|
321
|
+
if (!root)
|
|
322
|
+
continue;
|
|
323
|
+
const assets = pluginAssets(root, entry.name);
|
|
324
|
+
if (!assets)
|
|
325
|
+
continue;
|
|
326
|
+
seen.add(key);
|
|
327
|
+
project.push(assets);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return { user, project };
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return { user: [], project: [] };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
//# sourceMappingURL=claudePlugins.js.map
|
package/dist/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ import { login } from "./login.js";
|
|
|
24
24
|
import { logout } from "./logout.js";
|
|
25
25
|
import { buildLaunch } from "./launch.js";
|
|
26
26
|
import { runDoctor } from "./doctor.js";
|
|
27
|
+
import { installProcessCrashHandlers } from "./crashReport.js";
|
|
27
28
|
import { currentCliVersion, maybeNudgeAndRefresh, upgradeCommand } from "./upgrade.js";
|
|
28
29
|
import { maybeRefreshAtLaunch } from "./refresh.js";
|
|
29
30
|
import { ensureShadowPiPackage } from "./piPackage.js";
|
|
@@ -89,16 +90,16 @@ async function runDefault(passthroughArgs) {
|
|
|
89
90
|
// .claude/skills and .claude/commands into the agent, asking the one-time
|
|
90
91
|
// per-folder trust question when needed. Fail-soft: compat can only add
|
|
91
92
|
// flags, never block a launch.
|
|
92
|
-
let
|
|
93
|
+
let compat = { argv: [], env: {} };
|
|
93
94
|
try {
|
|
94
|
-
|
|
95
|
+
compat = await claudeCompatArgs({
|
|
95
96
|
cwd: process.cwd(),
|
|
96
97
|
agentDir: piAgentDir,
|
|
97
98
|
confirm: confirmOnTty,
|
|
98
99
|
});
|
|
99
100
|
}
|
|
100
101
|
catch {
|
|
101
|
-
|
|
102
|
+
compat = { argv: [], env: {} };
|
|
102
103
|
}
|
|
103
104
|
// buildLaunch runs the token-expiry preflight: it throws (with an actionable
|
|
104
105
|
// login prompt) on an already-expired token so we never spawn a session that
|
|
@@ -111,11 +112,13 @@ async function runDefault(passthroughArgs) {
|
|
|
111
112
|
extensionPath: resolveExtensionPath(),
|
|
112
113
|
agentDir: piAgentDir,
|
|
113
114
|
piPackageDir: shadowPiDir,
|
|
114
|
-
extraAgentArgs:
|
|
115
|
+
extraAgentArgs: compat.argv,
|
|
116
|
+
extraEnv: compat.env,
|
|
115
117
|
// Forward the active profile's file path so the extension can persist a
|
|
116
118
|
// mid-session token rotation back to the same profile the launcher read.
|
|
117
119
|
profilePath: profilePath(profile.name),
|
|
118
120
|
stateDir: credentialsDir(),
|
|
121
|
+
cliVersion: cliVersion(),
|
|
119
122
|
baseEnv: process.env,
|
|
120
123
|
});
|
|
121
124
|
}
|
|
@@ -173,6 +176,8 @@ export const HELP_TEXT = [
|
|
|
173
176
|
"Set YAGNI_BASE_URL to override the base URL for a single run.",
|
|
174
177
|
"Set YAGNI_DISABLE_UPDATE_CHECK=1 to silence the new-version notice.",
|
|
175
178
|
"Set YAGNI_DISABLE_CLAUDE_COMPAT=1 to skip loading .claude assets.",
|
|
179
|
+
"Set YAGNI_DISABLE_BRANDING=1 to pass the system prompt through unmodified.",
|
|
180
|
+
"Set YAGNI_DISABLE_CRASH_REPORTS=1 to turn off sanitized crash reports.",
|
|
176
181
|
].join("\n");
|
|
177
182
|
/** Parse `use <name> [--base-url <url>]` argv into its parts. */
|
|
178
183
|
export function parseUseArgs(args) {
|
|
@@ -316,6 +321,11 @@ export function isEntrypoint(argv1, moduleUrl) {
|
|
|
316
321
|
// Only auto-run when invoked as the CLI entry, so tests can import this module
|
|
317
322
|
// (e.g. to exercise wantsHelp) without spawning the agent.
|
|
318
323
|
if (isEntrypoint(process.argv[1], import.meta.url)) {
|
|
324
|
+
// Crash reporting for the LAUNCHER process only (pi runs as a child and the
|
|
325
|
+
// extension covers the session side). Fire-and-forget, sanitized, bounded;
|
|
326
|
+
// YAGNI_DISABLE_CRASH_REPORTS=1 turns it off. Registered before main() so a
|
|
327
|
+
// crash anywhere in the launcher is covered.
|
|
328
|
+
installProcessCrashHandlers({ client: "cli", clientVersion: cliVersion() });
|
|
319
329
|
main(process.argv.slice(2))
|
|
320
330
|
.then((code) => process.exit(code))
|
|
321
331
|
.catch((err) => {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crash reporting for the YAGNI Code launcher (and, via deep import, the
|
|
3
|
+
* desktop driver): a sanitized, fire-and-forget POST to the customer's own
|
|
4
|
+
* YAGNI backend at /api/yagni-code/crash. Deliberately no third-party
|
|
5
|
+
* telemetry SDK — crash reports are the ONLY thing this CLI phones home, they
|
|
6
|
+
* contain no code and no prompts, and `YAGNI_DISABLE_CRASH_REPORTS=1` turns
|
|
7
|
+
* them off entirely (same switch family as YAGNI_DISABLE_UPDATE_CHECK).
|
|
8
|
+
*
|
|
9
|
+
* Everything here is fail-soft: reporting never throws, never retries, never
|
|
10
|
+
* blocks longer than the short timeout, and never changes how the process
|
|
11
|
+
* would otherwise crash.
|
|
12
|
+
*
|
|
13
|
+
* The sanitizer core (scrub patterns + env-value redaction + path collapse +
|
|
14
|
+
* caps) is duplicated in `pi-extension-yagni/src/crashReport.ts` (the
|
|
15
|
+
* extension is bundled standalone and cannot import across packages) and
|
|
16
|
+
* re-run server-side in `backend/src/yagniCode/crashReports.ts`. Keep the
|
|
17
|
+
* three in sync. Spec:
|
|
18
|
+
* docs/superpowers/specs/2026-08-08-crash-reporting-design.md
|
|
19
|
+
*/
|
|
20
|
+
export declare const CRASH_REPORT_DISABLE_ENV = "YAGNI_DISABLE_CRASH_REPORTS";
|
|
21
|
+
export declare const CRASH_REPORT_TIMEOUT_MS = 1500;
|
|
22
|
+
/** Hard ceiling the process-exit path waits for a report before exiting. */
|
|
23
|
+
export declare const CRASH_REPORT_EXIT_RACE_MS = 1600;
|
|
24
|
+
export declare const MAX_CRASH_MESSAGE = 512;
|
|
25
|
+
export declare const MAX_CRASH_ERROR_CLASS = 128;
|
|
26
|
+
export declare const MAX_CRASH_STACK = 8000;
|
|
27
|
+
export declare const MAX_CRASH_STACK_FRAMES = 40;
|
|
28
|
+
/** If the serialized payload would exceed this, the stack is dropped. */
|
|
29
|
+
export declare const MAX_CRASH_PAYLOAD_BYTES = 16384;
|
|
30
|
+
/** Same truthiness rule as updateChecksDisabled: set and not "" / "0". */
|
|
31
|
+
export declare function crashReportsDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
32
|
+
export interface SanitizeCrashOptions {
|
|
33
|
+
/** Environment whose values get redacted (defaults to process.env). */
|
|
34
|
+
env?: NodeJS.ProcessEnv;
|
|
35
|
+
/** Repo root; paths under it become repo-relative instead of basenames. */
|
|
36
|
+
repoRoot?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Sanitize one piece of crash text. Order matters:
|
|
40
|
+
* 1. env-value redaction (non-path values, length >= 8) → `[ENV:NAME]`
|
|
41
|
+
* 2. secret patterns (keys, connection strings, base64 blobs)
|
|
42
|
+
* 3. home-dir collapse to `~`
|
|
43
|
+
* 4. repo-root strip (repo files stay readable as relative paths)
|
|
44
|
+
* 5. every remaining absolute/`~` path → basename (node_modules kept from
|
|
45
|
+
* `node_modules/` on, so dependency frames stay diagnosable)
|
|
46
|
+
* Over-redacts rather than under-redacts; pure; never throws.
|
|
47
|
+
*/
|
|
48
|
+
export declare function sanitizeCrashText(text: string, opts?: SanitizeCrashOptions): string;
|
|
49
|
+
export interface SanitizedCrash {
|
|
50
|
+
errorClass: string;
|
|
51
|
+
message: string;
|
|
52
|
+
stack?: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Reduce an arbitrary thrown value to sanitized error facts. Reads ONLY
|
|
56
|
+
* `name`, `message`, and `stack` — custom fields (response bodies, request
|
|
57
|
+
* payloads…) are never touched.
|
|
58
|
+
*/
|
|
59
|
+
export declare function sanitizeCrashError(err: unknown, opts?: SanitizeCrashOptions): SanitizedCrash;
|
|
60
|
+
export type CrashClient = "cli" | "desktop" | "desktop-driver";
|
|
61
|
+
export interface CrashReportInput {
|
|
62
|
+
client: CrashClient;
|
|
63
|
+
clientVersion: string;
|
|
64
|
+
error: unknown;
|
|
65
|
+
/** Command/phase label, e.g. "uncaught-exception" or "go:implement". */
|
|
66
|
+
context?: string;
|
|
67
|
+
baseUrl: string;
|
|
68
|
+
token: string;
|
|
69
|
+
env?: NodeJS.ProcessEnv;
|
|
70
|
+
repoRoot?: string;
|
|
71
|
+
fetchImpl?: typeof fetch;
|
|
72
|
+
timeoutMs?: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Pre-extracted crash facts (e.g. relayed from the desktop webview or the
|
|
76
|
+
* Rust panic hook, where no live Error object crosses the boundary). Each
|
|
77
|
+
* text field is sanitized and capped before sending.
|
|
78
|
+
*/
|
|
79
|
+
export interface CrashFieldsInput {
|
|
80
|
+
client: CrashClient;
|
|
81
|
+
clientVersion: string;
|
|
82
|
+
errorClass: string;
|
|
83
|
+
message: string;
|
|
84
|
+
stack?: string;
|
|
85
|
+
context?: string;
|
|
86
|
+
baseUrl: string;
|
|
87
|
+
token: string;
|
|
88
|
+
env?: NodeJS.ProcessEnv;
|
|
89
|
+
repoRoot?: string;
|
|
90
|
+
fetchImpl?: typeof fetch;
|
|
91
|
+
timeoutMs?: number;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Sanitize + POST one crash report from pre-extracted fields. Resolves on
|
|
95
|
+
* every outcome — timeout, network error, non-2xx, disabled — and never
|
|
96
|
+
* throws; there is nobody upstream who could act on a failure, and a
|
|
97
|
+
* reporting bug must never re-crash the process.
|
|
98
|
+
*/
|
|
99
|
+
export declare function sendCrashReport(input: CrashFieldsInput): Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* Fire one crash report from a live thrown value. Same never-throws contract
|
|
102
|
+
* as {@link sendCrashReport}, which does the sending.
|
|
103
|
+
*/
|
|
104
|
+
export declare function reportCrash(input: CrashReportInput): Promise<void>;
|
|
105
|
+
export interface InstallCrashHandlersOptions {
|
|
106
|
+
client: CrashClient;
|
|
107
|
+
clientVersion: string;
|
|
108
|
+
/** Credentials source; defaults to the active profile. Fail-soft. */
|
|
109
|
+
resolveTarget?: () => Promise<{
|
|
110
|
+
baseUrl: string;
|
|
111
|
+
token: string;
|
|
112
|
+
} | null>;
|
|
113
|
+
env?: NodeJS.ProcessEnv;
|
|
114
|
+
repoRoot?: string;
|
|
115
|
+
fetchImpl?: typeof fetch;
|
|
116
|
+
/** Ceiling on how long the exit path waits for the report. */
|
|
117
|
+
raceMs?: number;
|
|
118
|
+
/** Test seams. */
|
|
119
|
+
exit?: (code: number) => void;
|
|
120
|
+
writeStderr?: (text: string) => void;
|
|
121
|
+
proc?: Pick<NodeJS.Process, "on">;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* The handler behind uncaughtException/unhandledRejection: report (bounded by
|
|
125
|
+
* `raceMs`), then preserve today's crash behavior — print the error to stderr
|
|
126
|
+
* and exit 1. Exposed for tests; `installProcessCrashHandlers` registers it.
|
|
127
|
+
*/
|
|
128
|
+
export declare function makeProcessCrashHandler(context: "uncaught-exception" | "unhandled-rejection", opts: InstallCrashHandlersOptions): (err: unknown) => void;
|
|
129
|
+
/**
|
|
130
|
+
* Install process-level crash reporting. Call once, before main() runs, from
|
|
131
|
+
* a process YAGNI owns (the launcher, the desktop driver) — never from inside
|
|
132
|
+
* pi's process, where adding these listeners would change crash semantics.
|
|
133
|
+
*/
|
|
134
|
+
export declare function installProcessCrashHandlers(opts: InstallCrashHandlersOptions): void;
|
|
135
|
+
//# sourceMappingURL=crashReport.d.ts.map
|