@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.
Files changed (64) hide show
  1. package/README.md +58 -9
  2. package/dist/claudeCompat.d.ts +36 -5
  3. package/dist/claudeCompat.js +85 -23
  4. package/dist/claudePlugins.d.ts +109 -0
  5. package/dist/claudePlugins.js +336 -0
  6. package/dist/cli.js +14 -4
  7. package/dist/crashReport.d.ts +135 -0
  8. package/dist/crashReport.js +291 -0
  9. package/dist/doctor.d.ts +21 -0
  10. package/dist/doctor.js +52 -0
  11. package/dist/extension/askAdvisorTool.js +7 -1
  12. package/dist/extension/bless.js +16 -3
  13. package/dist/extension/boostCommand.d.ts +144 -0
  14. package/dist/extension/boostCommand.js +263 -0
  15. package/dist/extension/branding.d.ts +31 -0
  16. package/dist/extension/branding.js +37 -0
  17. package/dist/extension/chipEditor.js +7 -3
  18. package/dist/extension/claudeRules.d.ts +54 -0
  19. package/dist/extension/claudeRules.js +180 -0
  20. package/dist/extension/config.d.ts +61 -0
  21. package/dist/extension/config.js +86 -0
  22. package/dist/extension/costHud.d.ts +128 -15
  23. package/dist/extension/costHud.js +189 -19
  24. package/dist/extension/crashReport.d.ts +89 -0
  25. package/dist/extension/crashReport.js +241 -0
  26. package/dist/extension/index.d.ts +43 -4
  27. package/dist/extension/index.js +241 -32
  28. package/dist/extension/initPass.d.ts +65 -47
  29. package/dist/extension/initPass.js +145 -145
  30. package/dist/extension/mcpTools.d.ts +57 -0
  31. package/dist/extension/mcpTools.js +132 -0
  32. package/dist/extension/pipeline/eval.d.ts +42 -5
  33. package/dist/extension/pipeline/eval.js +44 -0
  34. package/dist/extension/pipeline/goCommand.d.ts +18 -0
  35. package/dist/extension/pipeline/goCommand.js +139 -26
  36. package/dist/extension/pipeline/goCompareCommand.d.ts +18 -8
  37. package/dist/extension/pipeline/goCompareCommand.js +42 -23
  38. package/dist/extension/pipeline/orchestrator.js +9 -0
  39. package/dist/extension/pipeline/runCostTable.d.ts +37 -0
  40. package/dist/extension/pipeline/runCostTable.js +165 -0
  41. package/dist/extension/pipeline/runState.d.ts +19 -0
  42. package/dist/extension/pipeline/runState.js +11 -0
  43. package/dist/extension/pipeline/runner.d.ts +19 -0
  44. package/dist/extension/pipeline/runner.js +13 -1
  45. package/dist/extension/pipeline/scrubSecrets.js +2 -2
  46. package/dist/extension/pipeline/stages.d.ts +3 -1
  47. package/dist/extension/pipeline/stages.js +3 -1
  48. package/dist/extension/pipeline/types.d.ts +7 -4
  49. package/dist/extension/pipeline/verify.js +6 -1
  50. package/dist/extension/pipeline/worktree.js +3 -1
  51. package/dist/extension/provider.d.ts +7 -1
  52. package/dist/extension/provider.js +8 -1
  53. package/dist/extension/recall.js +5 -2
  54. package/dist/extension/rerouteNotice.d.ts +42 -0
  55. package/dist/extension/rerouteNotice.js +67 -0
  56. package/dist/extension/sessionRuns.d.ts +45 -0
  57. package/dist/extension/sessionRuns.js +77 -0
  58. package/dist/extension/subagents.d.ts +17 -7
  59. package/dist/extension/subagents.js +52 -7
  60. package/dist/launch.d.ts +17 -3
  61. package/dist/launch.js +22 -9
  62. package/dist/login.d.ts +7 -0
  63. package/dist/login.js +3 -1
  64. 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 compatArgs = [];
93
+ let compat = { argv: [], env: {} };
93
94
  try {
94
- compatArgs = await claudeCompatArgs({
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
- compatArgs = [];
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: compatArgs,
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