@yagni-app/code 0.1.0 → 0.2.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 +65 -11
- package/dist/claudeCompat.d.ts +109 -0
- package/dist/claudeCompat.js +260 -0
- package/dist/claudePlugins.d.ts +109 -0
- package/dist/claudePlugins.js +336 -0
- package/dist/cli.d.ts +1 -5
- package/dist/cli.js +79 -20
- package/dist/config.d.ts +1 -1
- package/dist/config.js +4 -2
- package/dist/crashReport.d.ts +135 -0
- package/dist/crashReport.js +291 -0
- package/dist/credentials.js +2 -1
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.js +45 -0
- package/dist/doctor.d.ts +27 -0
- package/dist/doctor.js +81 -3
- package/dist/extension/advisor.d.ts +84 -0
- package/dist/extension/advisor.js +101 -0
- package/dist/extension/askAdvisorTool.d.ts +83 -0
- package/dist/extension/askAdvisorTool.js +194 -0
- package/dist/extension/askYagniTool.d.ts +39 -0
- package/dist/extension/askYagniTool.js +63 -0
- package/dist/extension/bless.d.ts +45 -0
- package/dist/extension/bless.js +87 -0
- package/dist/extension/boostCommand.d.ts +144 -0
- package/dist/extension/boostCommand.js +263 -0
- package/dist/extension/branding.d.ts +79 -0
- package/dist/extension/branding.js +156 -0
- package/dist/extension/chipEditor.d.ts +117 -0
- package/dist/extension/chipEditor.js +373 -0
- package/dist/extension/claudeRules.d.ts +54 -0
- package/dist/extension/claudeRules.js +180 -0
- package/dist/extension/config.d.ts +173 -0
- package/dist/extension/config.js +194 -0
- package/dist/extension/costHud.d.ts +186 -0
- package/dist/extension/costHud.js +293 -0
- package/dist/extension/crashReport.d.ts +89 -0
- package/dist/extension/crashReport.js +241 -0
- package/dist/extension/decisionCapture.d.ts +52 -0
- package/dist/extension/decisionCapture.js +66 -0
- package/dist/extension/decisions.d.ts +83 -0
- package/dist/extension/decisions.js +200 -0
- package/dist/extension/diagnostics.d.ts +41 -0
- package/dist/extension/diagnostics.js +110 -0
- package/dist/extension/index.d.ts +155 -0
- package/dist/extension/index.js +597 -0
- package/dist/extension/initDone.d.ts +28 -0
- package/dist/extension/initDone.js +66 -0
- package/dist/extension/initPass.d.ts +170 -0
- package/dist/extension/initPass.js +394 -0
- package/dist/extension/mcpTools.d.ts +57 -0
- package/dist/extension/mcpTools.js +132 -0
- package/dist/extension/nextWorkTool.d.ts +51 -0
- package/dist/extension/nextWorkTool.js +80 -0
- package/dist/extension/permission.d.ts +91 -0
- package/dist/extension/permission.js +236 -0
- package/dist/extension/pipeline/activity.d.ts +37 -0
- package/dist/extension/pipeline/activity.js +151 -0
- package/dist/extension/pipeline/activityFeed.d.ts +59 -0
- package/dist/extension/pipeline/activityFeed.js +175 -0
- package/dist/extension/pipeline/budget.d.ts +48 -0
- package/dist/extension/pipeline/budget.js +68 -0
- package/dist/extension/pipeline/checkpoint.d.ts +31 -0
- package/dist/extension/pipeline/checkpoint.js +176 -0
- package/dist/extension/pipeline/eval.d.ts +205 -0
- package/dist/extension/pipeline/eval.js +226 -0
- package/dist/extension/pipeline/events.d.ts +56 -0
- package/dist/extension/pipeline/events.js +147 -0
- package/dist/extension/pipeline/findings.d.ts +42 -0
- package/dist/extension/pipeline/findings.js +144 -0
- package/dist/extension/pipeline/finish.d.ts +128 -0
- package/dist/extension/pipeline/finish.js +307 -0
- package/dist/extension/pipeline/goCommand.d.ts +146 -0
- package/dist/extension/pipeline/goCommand.js +1085 -0
- package/dist/extension/pipeline/goCompareCommand.d.ts +58 -0
- package/dist/extension/pipeline/goCompareCommand.js +203 -0
- package/dist/extension/pipeline/goFlags.d.ts +40 -0
- package/dist/extension/pipeline/goFlags.js +46 -0
- package/dist/extension/pipeline/goStatusCommands.d.ts +59 -0
- package/dist/extension/pipeline/goStatusCommands.js +188 -0
- package/dist/extension/pipeline/invocation.d.ts +45 -0
- package/dist/extension/pipeline/invocation.js +64 -0
- package/dist/extension/pipeline/orchestrator.d.ts +131 -0
- package/dist/extension/pipeline/orchestrator.js +645 -0
- package/dist/extension/pipeline/personas.d.ts +44 -0
- package/dist/extension/pipeline/personas.js +248 -0
- package/dist/extension/pipeline/resilience.d.ts +85 -0
- package/dist/extension/pipeline/resilience.js +166 -0
- package/dist/extension/pipeline/resume.d.ts +18 -0
- package/dist/extension/pipeline/resume.js +106 -0
- package/dist/extension/pipeline/runCostTable.d.ts +37 -0
- package/dist/extension/pipeline/runCostTable.js +165 -0
- package/dist/extension/pipeline/runRegistry.d.ts +112 -0
- package/dist/extension/pipeline/runRegistry.js +202 -0
- package/dist/extension/pipeline/runSession.d.ts +152 -0
- package/dist/extension/pipeline/runSession.js +167 -0
- package/dist/extension/pipeline/runState.d.ts +177 -0
- package/dist/extension/pipeline/runState.js +275 -0
- package/dist/extension/pipeline/runner.d.ts +98 -0
- package/dist/extension/pipeline/runner.js +310 -0
- package/dist/extension/pipeline/scrubSecrets.d.ts +15 -0
- package/dist/extension/pipeline/scrubSecrets.js +32 -0
- package/dist/extension/pipeline/stages.d.ts +53 -0
- package/dist/extension/pipeline/stages.js +115 -0
- package/dist/extension/pipeline/ticketResolution.d.ts +34 -0
- package/dist/extension/pipeline/ticketResolution.js +75 -0
- package/dist/extension/pipeline/types.d.ts +439 -0
- package/dist/extension/pipeline/types.js +48 -0
- package/dist/extension/pipeline/verify.d.ts +277 -0
- package/dist/extension/pipeline/verify.js +763 -0
- package/dist/extension/pipeline/workspace.d.ts +32 -0
- package/dist/extension/pipeline/workspace.js +73 -0
- package/dist/extension/pipeline/worktree.d.ts +107 -0
- package/dist/extension/pipeline/worktree.js +202 -0
- package/dist/extension/provider.d.ts +17 -0
- package/dist/extension/provider.js +33 -0
- package/dist/extension/recall.d.ts +93 -0
- package/dist/extension/recall.js +190 -0
- package/dist/extension/recordContextTool.d.ts +38 -0
- package/dist/extension/recordContextTool.js +85 -0
- package/dist/extension/recordDecisionTool.d.ts +52 -0
- package/dist/extension/recordDecisionTool.js +102 -0
- package/dist/extension/repoDocs.d.ts +81 -0
- package/dist/extension/repoDocs.js +260 -0
- package/dist/extension/rerouteNotice.d.ts +34 -0
- package/dist/extension/rerouteNotice.js +79 -0
- package/dist/extension/resilientFetch.d.ts +60 -0
- package/dist/extension/resilientFetch.js +133 -0
- package/dist/extension/reviewTool.d.ts +34 -0
- package/dist/extension/reviewTool.js +81 -0
- package/dist/extension/sessionRuns.d.ts +45 -0
- package/dist/extension/sessionRuns.js +77 -0
- package/dist/extension/spool.d.ts +92 -0
- package/dist/extension/spool.js +266 -0
- package/dist/extension/stateHome.d.ts +2 -0
- package/dist/extension/stateHome.js +6 -0
- package/dist/extension/subagents.d.ts +145 -0
- package/dist/extension/subagents.js +326 -0
- package/dist/extension/surface.d.ts +10 -0
- package/dist/extension/surface.js +12 -0
- package/dist/extension/todos.d.ts +110 -0
- package/dist/extension/todos.js +217 -0
- package/dist/extension/tokenProvider.d.ts +93 -0
- package/dist/extension/tokenProvider.js +234 -0
- package/dist/launch.d.ts +25 -3
- package/dist/launch.js +27 -9
- package/dist/login.d.ts +7 -0
- package/dist/login.js +3 -1
- package/dist/paths.d.ts +13 -4
- package/dist/paths.js +17 -5
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +5 -2
- package/dist/upgrade.d.ts +97 -0
- package/dist/upgrade.js +284 -0
- package/package.json +11 -15
|
@@ -0,0 +1,291 @@
|
|
|
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
|
+
import { credentialsFromProfile, readActiveProfile } from "./profiles.js";
|
|
21
|
+
export const CRASH_REPORT_DISABLE_ENV = "YAGNI_DISABLE_CRASH_REPORTS";
|
|
22
|
+
export const CRASH_REPORT_TIMEOUT_MS = 1_500;
|
|
23
|
+
/** Hard ceiling the process-exit path waits for a report before exiting. */
|
|
24
|
+
export const CRASH_REPORT_EXIT_RACE_MS = 1_600;
|
|
25
|
+
export const MAX_CRASH_MESSAGE = 512;
|
|
26
|
+
export const MAX_CRASH_ERROR_CLASS = 128;
|
|
27
|
+
export const MAX_CRASH_STACK = 8_000;
|
|
28
|
+
export const MAX_CRASH_STACK_FRAMES = 40;
|
|
29
|
+
/** If the serialized payload would exceed this, the stack is dropped. */
|
|
30
|
+
export const MAX_CRASH_PAYLOAD_BYTES = 16_384;
|
|
31
|
+
/** Same truthiness rule as updateChecksDisabled: set and not "" / "0". */
|
|
32
|
+
export function crashReportsDisabled(env = process.env) {
|
|
33
|
+
const value = env[CRASH_REPORT_DISABLE_ENV];
|
|
34
|
+
return value !== undefined && value !== "" && value !== "0";
|
|
35
|
+
}
|
|
36
|
+
// Mirrors scrubSecrets (backend yagniCode/scrubSecrets.ts and
|
|
37
|
+
// pi-extension-yagni pipeline/scrubSecrets.ts) — keep in sync.
|
|
38
|
+
const SECRET_PATTERNS = [
|
|
39
|
+
[/\b([a-z][a-z0-9+.\-]*:\/\/[^\s:@/]+):[^\s:@/]+@/gi, "$1:[REDACTED]@"],
|
|
40
|
+
[/\b(sk-[A-Za-z0-9]{16,}|sk_(?:live|test)_[A-Za-z0-9]{16,}|rk_(?:live|test)_[A-Za-z0-9]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_\-]{20,})\b/g, "[REDACTED]"],
|
|
41
|
+
[/\b([A-Za-z0-9_]*(?:secret|password|passwd|api[_-]?key|token|private[_-]?key|access[_-]?key)[A-Za-z0-9_]*)\b(\s*[:=]\s*)("[^"]+"|'[^']+'|`[^`]+`|[^\s"']+)/gi, "$1$2[REDACTED]"],
|
|
42
|
+
[/\b[A-Za-z0-9+/]{40,}={0,2}\b/g, "[REDACTED]"],
|
|
43
|
+
];
|
|
44
|
+
const HOME_DIR_RE = /(?:\/(?:Users|home)\/|[A-Za-z]:\\Users\\)[^\s/\\]+/g;
|
|
45
|
+
/**
|
|
46
|
+
* Absolute or ~-rooted path tokens (colon excluded so `file.ts:10:5` keeps its
|
|
47
|
+
* location). The lookbehind stops a mid-path slash from matching, so an
|
|
48
|
+
* already-relative path (`src/pipeline/run.ts` after the repo-root strip)
|
|
49
|
+
* survives intact instead of being re-collapsed at its inner slashes. Spaces
|
|
50
|
+
* are deliberately ALLOWED inside the token: real directories contain spaces
|
|
51
|
+
* ("~/Client Confidential Project/src/x.ts"), and a space-stopping class
|
|
52
|
+
* would leak every segment after the first space verbatim. The cost is that
|
|
53
|
+
* prose following a path can be folded into the kept basename — over-redacts
|
|
54
|
+
* rather than under-redacts.
|
|
55
|
+
*/
|
|
56
|
+
const PATH_TOKEN_RE = /(?<![\w.~/\\])(?:~\/|\/|[A-Za-z]:\\)[^\n():'",]+/g;
|
|
57
|
+
/** Values too short to redact safely ("1", "true", "utf-8" would shred text). */
|
|
58
|
+
const MIN_ENV_VALUE_LENGTH = 8;
|
|
59
|
+
function isPathLikeValue(value) {
|
|
60
|
+
// Filesystem-path values (HOME, PWD, PATH, TMPDIR…) are handled — and more
|
|
61
|
+
// readably — by the path rules; replacing them here would break ~-collapse.
|
|
62
|
+
return value.startsWith("/") || value.startsWith("~") || /^[A-Za-z]:\\/.test(value);
|
|
63
|
+
}
|
|
64
|
+
function collapsePathToken(token) {
|
|
65
|
+
const nm = token.lastIndexOf("node_modules/");
|
|
66
|
+
if (nm >= 0)
|
|
67
|
+
return token.slice(nm);
|
|
68
|
+
const cut = Math.max(token.lastIndexOf("/"), token.lastIndexOf("\\"));
|
|
69
|
+
const base = cut >= 0 ? token.slice(cut + 1) : token;
|
|
70
|
+
return base.length > 0 ? base : token;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Sanitize one piece of crash text. Order matters:
|
|
74
|
+
* 1. env-value redaction (non-path values, length >= 8) → `[ENV:NAME]`
|
|
75
|
+
* 2. secret patterns (keys, connection strings, base64 blobs)
|
|
76
|
+
* 3. home-dir collapse to `~`
|
|
77
|
+
* 4. repo-root strip (repo files stay readable as relative paths)
|
|
78
|
+
* 5. every remaining absolute/`~` path → basename (node_modules kept from
|
|
79
|
+
* `node_modules/` on, so dependency frames stay diagnosable)
|
|
80
|
+
* Over-redacts rather than under-redacts; pure; never throws.
|
|
81
|
+
*/
|
|
82
|
+
export function sanitizeCrashText(text, opts = {}) {
|
|
83
|
+
let out = text;
|
|
84
|
+
const env = opts.env ?? process.env;
|
|
85
|
+
const entries = Object.entries(env)
|
|
86
|
+
.filter((entry) => typeof entry[1] === "string" &&
|
|
87
|
+
entry[1].length >= MIN_ENV_VALUE_LENGTH &&
|
|
88
|
+
!isPathLikeValue(entry[1]))
|
|
89
|
+
// Longest first so a value containing another value cannot leave a tail.
|
|
90
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
91
|
+
for (const [name, value] of entries) {
|
|
92
|
+
if (out.includes(value))
|
|
93
|
+
out = out.split(value).join(`[ENV:${name}]`);
|
|
94
|
+
}
|
|
95
|
+
for (const [re, repl] of SECRET_PATTERNS)
|
|
96
|
+
out = out.replace(re, repl);
|
|
97
|
+
out = out.replace(HOME_DIR_RE, "~");
|
|
98
|
+
if (opts.repoRoot) {
|
|
99
|
+
const root = opts.repoRoot.replace(HOME_DIR_RE, "~").replace(/\/+$/, "");
|
|
100
|
+
if (root.length > 1)
|
|
101
|
+
out = out.split(`${root}/`).join("");
|
|
102
|
+
}
|
|
103
|
+
out = out.replace(PATH_TOKEN_RE, collapsePathToken);
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Reduce an arbitrary thrown value to sanitized error facts. Reads ONLY
|
|
108
|
+
* `name`, `message`, and `stack` — custom fields (response bodies, request
|
|
109
|
+
* payloads…) are never touched.
|
|
110
|
+
*/
|
|
111
|
+
export function sanitizeCrashError(err, opts = {}) {
|
|
112
|
+
let errorClass = "Error";
|
|
113
|
+
let message;
|
|
114
|
+
let stack;
|
|
115
|
+
if (err instanceof Error) {
|
|
116
|
+
errorClass = err.name || "Error";
|
|
117
|
+
message = err.message;
|
|
118
|
+
stack = typeof err.stack === "string" ? err.stack : undefined;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
try {
|
|
122
|
+
message = String(err);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
message = "unstringifiable thrown value";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const cappedStack = stack !== undefined
|
|
129
|
+
? sanitizeCrashText(stack, opts)
|
|
130
|
+
.split("\n")
|
|
131
|
+
.slice(0, MAX_CRASH_STACK_FRAMES)
|
|
132
|
+
.join("\n")
|
|
133
|
+
.slice(0, MAX_CRASH_STACK)
|
|
134
|
+
: undefined;
|
|
135
|
+
return {
|
|
136
|
+
errorClass: sanitizeCrashText(errorClass, opts).slice(0, MAX_CRASH_ERROR_CLASS),
|
|
137
|
+
message: sanitizeCrashText(message, opts).slice(0, MAX_CRASH_MESSAGE),
|
|
138
|
+
...(cappedStack !== undefined ? { stack: cappedStack } : {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Sanitize + POST one crash report from pre-extracted fields. Resolves on
|
|
143
|
+
* every outcome — timeout, network error, non-2xx, disabled — and never
|
|
144
|
+
* throws; there is nobody upstream who could act on a failure, and a
|
|
145
|
+
* reporting bug must never re-crash the process.
|
|
146
|
+
*/
|
|
147
|
+
export async function sendCrashReport(input) {
|
|
148
|
+
try {
|
|
149
|
+
const env = input.env ?? process.env;
|
|
150
|
+
if (crashReportsDisabled(env))
|
|
151
|
+
return;
|
|
152
|
+
const opts = { env, repoRoot: input.repoRoot };
|
|
153
|
+
const stack = input.stack !== undefined
|
|
154
|
+
? sanitizeCrashText(input.stack, opts)
|
|
155
|
+
.split("\n")
|
|
156
|
+
.slice(0, MAX_CRASH_STACK_FRAMES)
|
|
157
|
+
.join("\n")
|
|
158
|
+
.slice(0, MAX_CRASH_STACK)
|
|
159
|
+
: undefined;
|
|
160
|
+
const payload = {
|
|
161
|
+
client: input.client,
|
|
162
|
+
clientVersion: input.clientVersion,
|
|
163
|
+
platform: `${process.platform} ${process.arch}`,
|
|
164
|
+
errorClass: sanitizeCrashText(input.errorClass, opts).slice(0, MAX_CRASH_ERROR_CLASS),
|
|
165
|
+
message: sanitizeCrashText(input.message, opts).slice(0, MAX_CRASH_MESSAGE),
|
|
166
|
+
...(stack !== undefined ? { stack } : {}),
|
|
167
|
+
...(input.context !== undefined ? { context: input.context } : {}),
|
|
168
|
+
timestamp: new Date().toISOString(),
|
|
169
|
+
};
|
|
170
|
+
if (JSON.stringify(payload).length > MAX_CRASH_PAYLOAD_BYTES) {
|
|
171
|
+
delete payload.stack;
|
|
172
|
+
}
|
|
173
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
const timer = setTimeout(() => controller.abort(), input.timeoutMs ?? CRASH_REPORT_TIMEOUT_MS);
|
|
176
|
+
timer.unref?.();
|
|
177
|
+
try {
|
|
178
|
+
await fetchImpl(`${input.baseUrl.replace(/\/$/, "")}/api/yagni-code/crash`, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: {
|
|
181
|
+
"content-type": "application/json",
|
|
182
|
+
authorization: `Bearer ${input.token}`,
|
|
183
|
+
},
|
|
184
|
+
body: JSON.stringify(payload),
|
|
185
|
+
signal: controller.signal,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// fail-soft by contract
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Fire one crash report from a live thrown value. Same never-throws contract
|
|
198
|
+
* as {@link sendCrashReport}, which does the sending.
|
|
199
|
+
*/
|
|
200
|
+
export async function reportCrash(input) {
|
|
201
|
+
try {
|
|
202
|
+
const env = input.env ?? process.env;
|
|
203
|
+
const sanitized = sanitizeCrashError(input.error, { env, repoRoot: input.repoRoot });
|
|
204
|
+
await sendCrashReport({
|
|
205
|
+
client: input.client,
|
|
206
|
+
clientVersion: input.clientVersion,
|
|
207
|
+
errorClass: sanitized.errorClass,
|
|
208
|
+
message: sanitized.message,
|
|
209
|
+
stack: sanitized.stack,
|
|
210
|
+
context: input.context,
|
|
211
|
+
baseUrl: input.baseUrl,
|
|
212
|
+
token: input.token,
|
|
213
|
+
env,
|
|
214
|
+
repoRoot: input.repoRoot,
|
|
215
|
+
fetchImpl: input.fetchImpl,
|
|
216
|
+
timeoutMs: input.timeoutMs,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// fail-soft by contract
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function resolveProfileTarget() {
|
|
224
|
+
try {
|
|
225
|
+
const profile = await readActiveProfile();
|
|
226
|
+
const creds = credentialsFromProfile(profile);
|
|
227
|
+
if (!creds?.token)
|
|
228
|
+
return null;
|
|
229
|
+
return { baseUrl: creds.baseUrl, token: creds.token };
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The handler behind uncaughtException/unhandledRejection: report (bounded by
|
|
237
|
+
* `raceMs`), then preserve today's crash behavior — print the error to stderr
|
|
238
|
+
* and exit 1. Exposed for tests; `installProcessCrashHandlers` registers it.
|
|
239
|
+
*/
|
|
240
|
+
export function makeProcessCrashHandler(context, opts) {
|
|
241
|
+
const exit = opts.exit ?? ((code) => process.exit(code));
|
|
242
|
+
const writeStderr = opts.writeStderr ?? ((text) => process.stderr.write(text));
|
|
243
|
+
return (err) => {
|
|
244
|
+
void (async () => {
|
|
245
|
+
try {
|
|
246
|
+
const resolveTarget = opts.resolveTarget ?? resolveProfileTarget;
|
|
247
|
+
const target = await resolveTarget();
|
|
248
|
+
if (target) {
|
|
249
|
+
// The race timer is deliberately ref'd: it guarantees the exit path
|
|
250
|
+
// even if the fetch never settles.
|
|
251
|
+
await Promise.race([
|
|
252
|
+
reportCrash({
|
|
253
|
+
client: opts.client,
|
|
254
|
+
clientVersion: opts.clientVersion,
|
|
255
|
+
error: err,
|
|
256
|
+
context,
|
|
257
|
+
baseUrl: target.baseUrl,
|
|
258
|
+
token: target.token,
|
|
259
|
+
env: opts.env,
|
|
260
|
+
repoRoot: opts.repoRoot,
|
|
261
|
+
fetchImpl: opts.fetchImpl,
|
|
262
|
+
}),
|
|
263
|
+
new Promise((resolve) => setTimeout(resolve, opts.raceMs ?? CRASH_REPORT_EXIT_RACE_MS)),
|
|
264
|
+
]);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// reporting must never re-crash the crash path
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
const text = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
272
|
+
writeStderr(`${text}\n`);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
// even stderr can fail (EPIPE); still exit
|
|
276
|
+
}
|
|
277
|
+
exit(1);
|
|
278
|
+
})();
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Install process-level crash reporting. Call once, before main() runs, from
|
|
283
|
+
* a process YAGNI owns (the launcher, the desktop driver) — never from inside
|
|
284
|
+
* pi's process, where adding these listeners would change crash semantics.
|
|
285
|
+
*/
|
|
286
|
+
export function installProcessCrashHandlers(opts) {
|
|
287
|
+
const proc = opts.proc ?? process;
|
|
288
|
+
proc.on("uncaughtException", makeProcessCrashHandler("uncaught-exception", opts));
|
|
289
|
+
proc.on("unhandledRejection", makeProcessCrashHandler("unhandled-rejection", opts));
|
|
290
|
+
}
|
|
291
|
+
//# sourceMappingURL=crashReport.js.map
|
package/dist/credentials.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
|
+
import { DISTRIBUTION } from "./distribution.js";
|
|
8
9
|
/**
|
|
9
10
|
* Test seam: when set, all path resolution roots here instead of
|
|
10
11
|
* `~/.yagni-code`, so tests can exercise profile/credential storage against a
|
|
@@ -16,7 +17,7 @@ export function _setYagniCodeHomeForTest(dir) {
|
|
|
16
17
|
homeOverride = dir;
|
|
17
18
|
}
|
|
18
19
|
export function credentialsDir() {
|
|
19
|
-
return homeOverride ?? join(homedir(),
|
|
20
|
+
return homeOverride ?? join(homedir(), DISTRIBUTION.stateDirName);
|
|
20
21
|
}
|
|
21
22
|
export function credentialsPath() {
|
|
22
23
|
return join(credentialsDir(), "credentials.json");
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type DistributionChannel = "production" | "staging";
|
|
2
|
+
export interface CodeDistribution {
|
|
3
|
+
channel: DistributionChannel;
|
|
4
|
+
packageName: string;
|
|
5
|
+
commandName: string;
|
|
6
|
+
displayName: string;
|
|
7
|
+
defaultBaseUrl: string;
|
|
8
|
+
defaultProfile: string;
|
|
9
|
+
stateDirName: string;
|
|
10
|
+
updateTag: "latest" | "staging";
|
|
11
|
+
}
|
|
12
|
+
export declare function distributionForPackageName(packageName: string): CodeDistribution;
|
|
13
|
+
export declare function resolveDistribution(env?: NodeJS.ProcessEnv, packageName?: string): CodeDistribution;
|
|
14
|
+
export declare const DISTRIBUTION: CodeDistribution;
|
|
15
|
+
//# sourceMappingURL=distribution.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const PRODUCTION = {
|
|
5
|
+
channel: "production",
|
|
6
|
+
packageName: "@yagni-app/code",
|
|
7
|
+
commandName: "yagni",
|
|
8
|
+
displayName: "YAGNI Code",
|
|
9
|
+
defaultBaseUrl: "https://yagni.app",
|
|
10
|
+
defaultProfile: "prod",
|
|
11
|
+
stateDirName: ".yagni-code",
|
|
12
|
+
updateTag: "latest",
|
|
13
|
+
};
|
|
14
|
+
const STAGING = {
|
|
15
|
+
channel: "staging",
|
|
16
|
+
packageName: "@yagni-app/code-staging",
|
|
17
|
+
commandName: "yagni-staging",
|
|
18
|
+
displayName: "YAGNI Code Staging",
|
|
19
|
+
defaultBaseUrl: "https://yagni-staging.app",
|
|
20
|
+
defaultProfile: "staging",
|
|
21
|
+
stateDirName: ".yagni-code-staging",
|
|
22
|
+
updateTag: "staging",
|
|
23
|
+
};
|
|
24
|
+
export function distributionForPackageName(packageName) {
|
|
25
|
+
return packageName === STAGING.packageName ? { ...STAGING } : { ...PRODUCTION };
|
|
26
|
+
}
|
|
27
|
+
function installedPackageName() {
|
|
28
|
+
try {
|
|
29
|
+
const path = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
30
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
31
|
+
return parsed.name ?? PRODUCTION.packageName;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return PRODUCTION.packageName;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function resolveDistribution(env = process.env, packageName = installedPackageName()) {
|
|
38
|
+
if (env.YAGNI_CODE_DISTRIBUTION === "staging")
|
|
39
|
+
return { ...STAGING };
|
|
40
|
+
if (env.YAGNI_CODE_DISTRIBUTION === "production")
|
|
41
|
+
return { ...PRODUCTION };
|
|
42
|
+
return distributionForPackageName(packageName);
|
|
43
|
+
}
|
|
44
|
+
export const DISTRIBUTION = resolveDistribution();
|
|
45
|
+
//# sourceMappingURL=distribution.js.map
|
package/dist/doctor.d.ts
CHANGED
|
@@ -55,7 +55,18 @@ export declare function checkProfileToken(profile: Pick<Profile, "name" | "token
|
|
|
55
55
|
export declare function checkTokenExpiry(status: TokenExpiryStatus): CheckResult;
|
|
56
56
|
export declare function checkBackend(probe: BackendProbe): CheckResult;
|
|
57
57
|
export declare function checkStateDir(probe: StateDirProbe): CheckResult;
|
|
58
|
+
export declare function checkCliUpdate(probe: {
|
|
59
|
+
current: string;
|
|
60
|
+
latest: string | null;
|
|
61
|
+
}): CheckResult;
|
|
58
62
|
export declare function checkGh(onPath: boolean): CheckResult;
|
|
63
|
+
/** What the Windows bash probe found (pi needs a bash — Git Bash — on win32). */
|
|
64
|
+
export interface BashProbe {
|
|
65
|
+
found: boolean;
|
|
66
|
+
/** The resolved bash path, when found. */
|
|
67
|
+
where?: string;
|
|
68
|
+
}
|
|
69
|
+
export declare function checkBash(probe: BashProbe): CheckResult;
|
|
59
70
|
export interface DoctorReport {
|
|
60
71
|
checks: CheckResult[];
|
|
61
72
|
exitCode: number;
|
|
@@ -71,10 +82,26 @@ export interface DoctorDeps {
|
|
|
71
82
|
probeBackend?: (baseUrl: string, token: string) => Promise<BackendProbe>;
|
|
72
83
|
probeStateDir?: () => StateDirProbe;
|
|
73
84
|
ghOnPath?: () => boolean;
|
|
85
|
+
/** Platform seam for the win32-only bash check (defaults to process.platform). */
|
|
86
|
+
platform?: NodeJS.Platform;
|
|
87
|
+
/** Windows bash probe; only ever called when the platform is win32. */
|
|
88
|
+
probeBash?: () => BashProbe;
|
|
89
|
+
currentVersion?: string;
|
|
90
|
+
probeLatestVersion?: () => Promise<string | null>;
|
|
74
91
|
log?: (msg: string) => void;
|
|
75
92
|
}
|
|
76
93
|
/** Whether a `gh` executable is resolvable on PATH (no subprocess spawn). */
|
|
77
94
|
export declare function ghOnPathDefault(env?: NodeJS.ProcessEnv): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Locate the bash pi will actually use on Windows. The order and locations
|
|
97
|
+
* MIRROR pi 0.83's own shell resolution (dist/utils/shell.js) exactly:
|
|
98
|
+
* `%ProgramFiles%\Git\bin\bash.exe`, then `%ProgramFiles(x86)%\Git\bin\bash.exe`,
|
|
99
|
+
* then `bash.exe` on PATH (`where bash.exe`). Deliberately NOTHING wider — a
|
|
100
|
+
* per-user Git install in `%LOCALAPPDATA%` that is not on PATH is invisible
|
|
101
|
+
* to pi, and a doctor that reported it green would bless a machine where the
|
|
102
|
+
* first bash tool call throws. Pure function of env, like the gh probe.
|
|
103
|
+
*/
|
|
104
|
+
export declare function bashOnWindowsDefault(env?: NodeJS.ProcessEnv): BashProbe;
|
|
78
105
|
/**
|
|
79
106
|
* Gather every check result against the (injectable) probes. Pure ordering; each
|
|
80
107
|
* individual check is a pure function of its probe.
|
package/dist/doctor.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
16
16
|
import { delimiter, join } from "node:path";
|
|
17
17
|
import { credentialsDir } from "./credentials.js";
|
|
18
|
+
import { currentCliVersion, fetchLatestVersion, isNewerVersion } from "./upgrade.js";
|
|
18
19
|
import { classifyTokenExpiry } from "./launch.js";
|
|
19
20
|
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
|
|
20
21
|
import { readActiveProfile } from "./profiles.js";
|
|
@@ -41,12 +42,12 @@ export function checkExtension(probe) {
|
|
|
41
42
|
return {
|
|
42
43
|
name: "extension",
|
|
43
44
|
status: "fail",
|
|
44
|
-
detail: "
|
|
45
|
-
hint: "reinstall it: npm i -g @yagni-app/code (monorepo: pnpm --filter
|
|
45
|
+
detail: "YAGNI extension not found (dist/extension/index.js missing)",
|
|
46
|
+
hint: "reinstall it: npm i -g @yagni-app/code (monorepo: pnpm --filter @yagni-app/code build)",
|
|
46
47
|
required: true,
|
|
47
48
|
};
|
|
48
49
|
}
|
|
49
|
-
return { name: "extension", status: "ok", detail: "
|
|
50
|
+
return { name: "extension", status: "ok", detail: "YAGNI extension loaded", required: true };
|
|
50
51
|
}
|
|
51
52
|
export function checkProfileToken(profile) {
|
|
52
53
|
if (!profile.token) {
|
|
@@ -160,6 +161,26 @@ export function checkStateDir(probe) {
|
|
|
160
161
|
}
|
|
161
162
|
return { name: "state dir", status: "ok", detail: `${probe.path} is 0700`, required: false };
|
|
162
163
|
}
|
|
164
|
+
export function checkCliUpdate(probe) {
|
|
165
|
+
if (probe.latest === null) {
|
|
166
|
+
return {
|
|
167
|
+
name: "cli version",
|
|
168
|
+
status: "ok",
|
|
169
|
+
detail: `v${probe.current} (update check unavailable)`,
|
|
170
|
+
required: false,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
if (isNewerVersion(probe.current, probe.latest)) {
|
|
174
|
+
return {
|
|
175
|
+
name: "cli version",
|
|
176
|
+
status: "warn",
|
|
177
|
+
detail: `v${probe.current} — v${probe.latest} is available`,
|
|
178
|
+
hint: "run `yagni upgrade`",
|
|
179
|
+
required: false,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
return { name: "cli version", status: "ok", detail: `v${probe.current} (latest)`, required: false };
|
|
183
|
+
}
|
|
163
184
|
export function checkGh(onPath) {
|
|
164
185
|
if (onPath) {
|
|
165
186
|
return {
|
|
@@ -177,6 +198,24 @@ export function checkGh(onPath) {
|
|
|
177
198
|
required: false,
|
|
178
199
|
};
|
|
179
200
|
}
|
|
201
|
+
export function checkBash(probe) {
|
|
202
|
+
if (!probe.found) {
|
|
203
|
+
return {
|
|
204
|
+
name: "bash",
|
|
205
|
+
status: "fail",
|
|
206
|
+
detail: "no bash found (pi runs its shell commands through bash)",
|
|
207
|
+
hint: "Install Git for Windows — pi needs its bash: https://gitforwindows.org "
|
|
208
|
+
+ "(a per-user install must also put bash.exe on PATH)",
|
|
209
|
+
required: true,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
name: "bash",
|
|
214
|
+
status: "ok",
|
|
215
|
+
detail: probe.where ? `found (${probe.where})` : "found",
|
|
216
|
+
required: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
180
219
|
function toOctal(mode) {
|
|
181
220
|
return `0${(mode & 0o777).toString(8).padStart(3, "0")}`;
|
|
182
221
|
}
|
|
@@ -275,6 +314,32 @@ export function ghOnPathDefault(env = process.env) {
|
|
|
275
314
|
}
|
|
276
315
|
return false;
|
|
277
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Locate the bash pi will actually use on Windows. The order and locations
|
|
319
|
+
* MIRROR pi 0.83's own shell resolution (dist/utils/shell.js) exactly:
|
|
320
|
+
* `%ProgramFiles%\Git\bin\bash.exe`, then `%ProgramFiles(x86)%\Git\bin\bash.exe`,
|
|
321
|
+
* then `bash.exe` on PATH (`where bash.exe`). Deliberately NOTHING wider — a
|
|
322
|
+
* per-user Git install in `%LOCALAPPDATA%` that is not on PATH is invisible
|
|
323
|
+
* to pi, and a doctor that reported it green would bless a machine where the
|
|
324
|
+
* first bash tool call throws. Pure function of env, like the gh probe.
|
|
325
|
+
*/
|
|
326
|
+
export function bashOnWindowsDefault(env = process.env) {
|
|
327
|
+
for (const root of [env.ProgramFiles, env["ProgramFiles(x86)"]]) {
|
|
328
|
+
if (!root)
|
|
329
|
+
continue;
|
|
330
|
+
const candidate = join(root, "Git", "bin", "bash.exe");
|
|
331
|
+
if (existsSync(candidate))
|
|
332
|
+
return { found: true, where: candidate };
|
|
333
|
+
}
|
|
334
|
+
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
335
|
+
if (!dir)
|
|
336
|
+
continue;
|
|
337
|
+
const candidate = join(dir, "bash.exe");
|
|
338
|
+
if (existsSync(candidate))
|
|
339
|
+
return { found: true, where: candidate };
|
|
340
|
+
}
|
|
341
|
+
return { found: false };
|
|
342
|
+
}
|
|
278
343
|
/**
|
|
279
344
|
* Gather every check result against the (injectable) probes. Pure ordering; each
|
|
280
345
|
* individual check is a pure function of its probe.
|
|
@@ -287,9 +352,22 @@ export async function gatherChecks(deps = {}) {
|
|
|
287
352
|
const probeBackend = deps.probeBackend ?? defaultProbeBackend;
|
|
288
353
|
const probeStateDir = deps.probeStateDir ?? defaultProbeStateDir;
|
|
289
354
|
const ghOnPath = deps.ghOnPath ?? (() => ghOnPathDefault());
|
|
355
|
+
const platform = deps.platform ?? process.platform;
|
|
356
|
+
const probeBash = deps.probeBash ?? (() => bashOnWindowsDefault());
|
|
357
|
+
const probeLatestVersion = deps.probeLatestVersion ?? (() => fetchLatestVersion());
|
|
290
358
|
const checks = [];
|
|
291
359
|
checks.push(checkPiEngine(probePiEngine()));
|
|
292
360
|
checks.push(checkExtension(probeExtension()));
|
|
361
|
+
// win32 only, and skipped means NOT SHOWN: on macOS/Linux there is nothing
|
|
362
|
+
// to say. pi shells out through bash, so a Windows machine without Git Bash
|
|
363
|
+
// cannot launch at all — a required red, like a missing engine.
|
|
364
|
+
if (platform === "win32") {
|
|
365
|
+
checks.push(checkBash(probeBash()));
|
|
366
|
+
}
|
|
367
|
+
checks.push(checkCliUpdate({
|
|
368
|
+
current: deps.currentVersion ?? currentCliVersion(),
|
|
369
|
+
latest: await probeLatestVersion(),
|
|
370
|
+
}));
|
|
293
371
|
const profile = await readProfile();
|
|
294
372
|
checks.push(checkProfileToken(profile));
|
|
295
373
|
if (profile.token) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The PURE half of the ask_advisor escalation (YAG-380).
|
|
3
|
+
*
|
|
4
|
+
* Balanced sessions drive on `advanced` and escalate hard judgment calls to
|
|
5
|
+
* `peak`. This module owns every decision about whether a consult may happen and
|
|
6
|
+
* what it cost; `askAdvisorTool.ts` owns the spawn. Same split as `permission.ts`
|
|
7
|
+
* (`decideGate` pure, wiring separate), for the same reason: the rules are what
|
|
8
|
+
* need exhaustive tests, and they must not require a child process to exercise.
|
|
9
|
+
*
|
|
10
|
+
* Two bounds, both necessarily POST-HOC. pi exposes no pre-emptive turn or token
|
|
11
|
+
* ceiling (see `pipeline/budget.ts`), so a consult's cost is only knowable once
|
|
12
|
+
* the child has exited:
|
|
13
|
+
*
|
|
14
|
+
* - `maxConsults` is the real product bound. It makes escalation predictable
|
|
15
|
+
* and priceable, which is the point of the cap.
|
|
16
|
+
* - `maxSpend` is a RUNAWAY CATCH, not a budget — deliberately generous, in
|
|
17
|
+
* the same spirit as `DEFAULT_RUN_BUDGET`. A thorough consult on a large
|
|
18
|
+
* repo runs $0.40-$1.00, so a full 5-consult session lands near $2-$5 and
|
|
19
|
+
* never approaches this. It exists only so an advisor stuck in a loop has
|
|
20
|
+
* something to stop it.
|
|
21
|
+
*
|
|
22
|
+
* State is held in a closure via {@link makeAdvisorState}; no module-level
|
|
23
|
+
* mutable state, so two sessions in one process cannot bleed into each other.
|
|
24
|
+
*/
|
|
25
|
+
/** The session tier that may escalate. Balanced only, by design. */
|
|
26
|
+
export declare const ADVISOR_TIER = "balanced";
|
|
27
|
+
/** The tier a consult itself runs on. */
|
|
28
|
+
export declare const ADVISOR_MODEL_TIER = "peak";
|
|
29
|
+
/** Bounds on escalation within a single session. */
|
|
30
|
+
export interface AdvisorLimits {
|
|
31
|
+
/** Hard cap on consults per session. */
|
|
32
|
+
maxConsults: number;
|
|
33
|
+
/** Runaway-catch ceiling on aggregate advisor spend, in the same unit as StageUsage.cost. */
|
|
34
|
+
maxSpend: number;
|
|
35
|
+
}
|
|
36
|
+
export declare const DEFAULT_ADVISOR_LIMITS: AdvisorLimits;
|
|
37
|
+
/** Mutable per-session advisor accounting. */
|
|
38
|
+
export interface AdvisorState {
|
|
39
|
+
consults: number;
|
|
40
|
+
spend: number;
|
|
41
|
+
}
|
|
42
|
+
/** A live session's advisor accounting, held in a closure. */
|
|
43
|
+
export interface AdvisorStateHandle {
|
|
44
|
+
read(): AdvisorState;
|
|
45
|
+
/** Record a completed consult. Returns the state after recording. */
|
|
46
|
+
record(cost: number): AdvisorState;
|
|
47
|
+
}
|
|
48
|
+
export declare function makeAdvisorState(): AdvisorStateHandle;
|
|
49
|
+
/** The outcome of the pre-consult gate. */
|
|
50
|
+
export interface ConsultDecision {
|
|
51
|
+
allow: boolean;
|
|
52
|
+
/** Plain-text explanation when disallowed, surfaced to the model verbatim. */
|
|
53
|
+
reason?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface ConsultGateInput {
|
|
56
|
+
/** The LIVE session model id (pi's picker can change this mid-session). */
|
|
57
|
+
model: string | undefined;
|
|
58
|
+
state: AdvisorState;
|
|
59
|
+
limits: AdvisorLimits;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Decide whether one consult may proceed. PURE.
|
|
63
|
+
*
|
|
64
|
+
* The tier check reads the model at CALL time rather than at registration time:
|
|
65
|
+
* tools register once at activation but pi's picker can switch the session model
|
|
66
|
+
* afterwards, so a registration-time check would both leave the tool live after
|
|
67
|
+
* switching away from Balanced and hide it forever from a session that switched
|
|
68
|
+
* to Balanced.
|
|
69
|
+
*/
|
|
70
|
+
export declare function decideConsult(input: ConsultGateInput): ConsultDecision;
|
|
71
|
+
/**
|
|
72
|
+
* The line appended to a consult's advice so spend is visible at the moment it
|
|
73
|
+
* happens. `/cost` cannot see child processes at all (it accumulates off the
|
|
74
|
+
* parent's `turn_end`), so without this an advisor consult is invisible — see
|
|
75
|
+
* YAG-383 for the server-authoritative fix.
|
|
76
|
+
*/
|
|
77
|
+
export declare function formatConsultCost(input: {
|
|
78
|
+
cost: number;
|
|
79
|
+
consults: number;
|
|
80
|
+
limits: AdvisorLimits;
|
|
81
|
+
}): string;
|
|
82
|
+
/** The advisor subtotal line `/cost` appends when a session has escalated. */
|
|
83
|
+
export declare function formatAdvisorSubtotal(state: AdvisorState, limits: AdvisorLimits): string;
|
|
84
|
+
//# sourceMappingURL=advisor.d.ts.map
|