@yagni-app/code 0.1.0

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.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Credential storage for the yagni-code CLI: `~/.yagni-code/credentials.json`,
3
+ * written 0600 (owner read/write only) since it holds a live API token.
4
+ */
5
+ export interface Credentials {
6
+ token: string;
7
+ baseUrl: string;
8
+ workspaceId: string;
9
+ expiresAt?: string;
10
+ }
11
+ export declare function _setYagniCodeHomeForTest(dir: string | null): void;
12
+ export declare function credentialsDir(): string;
13
+ export declare function credentialsPath(): string;
14
+ /**
15
+ * Isolated pi config/state dir for YAGNI Code: `~/.yagni-code/agent/<profile>`.
16
+ *
17
+ * The launcher pins pi's `PI_CODING_AGENT_DIR` here so YAGNI Code never reads
18
+ * the user's machine-wide `~/.pi/agent` — keeping its own providers (only
19
+ * `yagni`), stored auth, model scope (the backend catalog), and theme out of
20
+ * the agent. Everything routes through the YAGNI proxy; the user's local
21
+ * Together/OpenRouter keys and saved default model cannot bleed in or bypass it.
22
+ *
23
+ * Scoping the dir per profile keeps each environment's pi state (session
24
+ * history, theme, settings) separate, so pointing at staging never bleeds prod
25
+ * state. Called with no profile it returns the legacy un-scoped dir.
26
+ */
27
+ export declare function agentDir(profileName?: string): string;
28
+ /**
29
+ * Hermetic shadow pi package dir: `~/.yagni-code/pi-package`.
30
+ *
31
+ * Holds a generated `package.json` (pi's, with `piConfig.name` set so the
32
+ * terminal title/process name read "YAGNI Code" instead of "pi"/"π") plus
33
+ * symlinks back to the real pi package's assets. The launcher pins pi's
34
+ * `PI_PACKAGE_DIR` here. Kept under the YAGNI-owned dir so it never collides
35
+ * with the user's machine-wide pi install.
36
+ */
37
+ export declare function piPackageDir(): string;
38
+ export declare function writeCredentials(creds: Credentials, path?: string): Promise<void>;
39
+ export declare function readCredentials(path?: string): Promise<Credentials | null>;
40
+ export declare function deleteCredentials(path?: string): Promise<void>;
41
+ //# sourceMappingURL=credentials.d.ts.map
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Credential storage for the yagni-code CLI: `~/.yagni-code/credentials.json`,
3
+ * written 0600 (owner read/write only) since it holds a live API token.
4
+ */
5
+ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
6
+ import { homedir } from "node:os";
7
+ import { dirname, join } from "node:path";
8
+ /**
9
+ * Test seam: when set, all path resolution roots here instead of
10
+ * `~/.yagni-code`, so tests can exercise profile/credential storage against a
11
+ * tmpdir without touching the real home dir. Mirrors the `_setTestPool` style
12
+ * used elsewhere in the repo (no public env var). Pass `null` to restore.
13
+ */
14
+ let homeOverride = null;
15
+ export function _setYagniCodeHomeForTest(dir) {
16
+ homeOverride = dir;
17
+ }
18
+ export function credentialsDir() {
19
+ return homeOverride ?? join(homedir(), ".yagni-code");
20
+ }
21
+ export function credentialsPath() {
22
+ return join(credentialsDir(), "credentials.json");
23
+ }
24
+ /**
25
+ * Isolated pi config/state dir for YAGNI Code: `~/.yagni-code/agent/<profile>`.
26
+ *
27
+ * The launcher pins pi's `PI_CODING_AGENT_DIR` here so YAGNI Code never reads
28
+ * the user's machine-wide `~/.pi/agent` — keeping its own providers (only
29
+ * `yagni`), stored auth, model scope (the backend catalog), and theme out of
30
+ * the agent. Everything routes through the YAGNI proxy; the user's local
31
+ * Together/OpenRouter keys and saved default model cannot bleed in or bypass it.
32
+ *
33
+ * Scoping the dir per profile keeps each environment's pi state (session
34
+ * history, theme, settings) separate, so pointing at staging never bleeds prod
35
+ * state. Called with no profile it returns the legacy un-scoped dir.
36
+ */
37
+ export function agentDir(profileName) {
38
+ const base = join(credentialsDir(), "agent");
39
+ return profileName ? join(base, profileName) : base;
40
+ }
41
+ /**
42
+ * Hermetic shadow pi package dir: `~/.yagni-code/pi-package`.
43
+ *
44
+ * Holds a generated `package.json` (pi's, with `piConfig.name` set so the
45
+ * terminal title/process name read "YAGNI Code" instead of "pi"/"π") plus
46
+ * symlinks back to the real pi package's assets. The launcher pins pi's
47
+ * `PI_PACKAGE_DIR` here. Kept under the YAGNI-owned dir so it never collides
48
+ * with the user's machine-wide pi install.
49
+ */
50
+ export function piPackageDir() {
51
+ return join(credentialsDir(), "pi-package");
52
+ }
53
+ export async function writeCredentials(creds, path = credentialsPath()) {
54
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
55
+ await writeFile(path, `${JSON.stringify(creds, null, 2)}\n`, { mode: 0o600 });
56
+ // writeFile honors mode only on create; chmod guarantees 0600 on overwrite too.
57
+ await chmod(path, 0o600);
58
+ }
59
+ export async function readCredentials(path = credentialsPath()) {
60
+ try {
61
+ const raw = await readFile(path, "utf8");
62
+ const parsed = JSON.parse(raw);
63
+ if (!parsed || typeof parsed.token !== "string")
64
+ return null;
65
+ return parsed;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ export async function deleteCredentials(path = credentialsPath()) {
72
+ await rm(path, { force: true });
73
+ }
74
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1,88 @@
1
+ /**
2
+ * `yagni doctor` — a green/red readiness checklist.
3
+ *
4
+ * Diagnoses whether a `yagni` launch will actually work: the pi engine is built,
5
+ * the extension is built, the active profile has a live (unexpired) token, the
6
+ * backend is reachable with YAGNI Code enabled, the state dir is locked down, and
7
+ * `gh` is available for `/go --pr`. Every failing/soft check carries a one-line
8
+ * actionable fix.
9
+ *
10
+ * The check LOGIC is pure (given probe inputs) so it is fully unit-testable; the
11
+ * impure probes (fs, fetch, profile read) are injectable seams. `runDoctor`
12
+ * returns the process exit code: 0 when all REQUIRED checks pass, 1 otherwise.
13
+ * Advisory checks (loose perms, missing `gh`) never flip the exit code.
14
+ */
15
+ import { type TokenExpiryStatus } from "./launch.js";
16
+ import { type Profile } from "./profiles.js";
17
+ export type CheckStatus = "ok" | "warn" | "fail";
18
+ export interface CheckResult {
19
+ /** Short column label, e.g. "pi engine". */
20
+ name: string;
21
+ status: CheckStatus;
22
+ /** One-line current state. */
23
+ detail: string;
24
+ /** Actionable fix, shown when the check is not `ok`. */
25
+ hint?: string;
26
+ /** Only `required` failures flip the exit code to 1. */
27
+ required: boolean;
28
+ }
29
+ export interface PiEngineProbe {
30
+ binPath?: string;
31
+ binExists: boolean;
32
+ version?: string;
33
+ }
34
+ export interface ExtensionProbe {
35
+ path?: string;
36
+ exists: boolean;
37
+ }
38
+ export interface StateDirProbe {
39
+ path: string;
40
+ exists: boolean;
41
+ /** The dir's permission bits (mode & 0o777), or null when it doesn't exist. */
42
+ mode: number | null;
43
+ }
44
+ export type BackendProbe = {
45
+ kind: "skipped";
46
+ } | {
47
+ kind: "status";
48
+ status: number;
49
+ } | {
50
+ kind: "network";
51
+ };
52
+ export declare function checkPiEngine(probe: PiEngineProbe): CheckResult;
53
+ export declare function checkExtension(probe: ExtensionProbe): CheckResult;
54
+ export declare function checkProfileToken(profile: Pick<Profile, "name" | "token">): CheckResult;
55
+ export declare function checkTokenExpiry(status: TokenExpiryStatus): CheckResult;
56
+ export declare function checkBackend(probe: BackendProbe): CheckResult;
57
+ export declare function checkStateDir(probe: StateDirProbe): CheckResult;
58
+ export declare function checkGh(onPath: boolean): CheckResult;
59
+ export interface DoctorReport {
60
+ checks: CheckResult[];
61
+ exitCode: number;
62
+ }
63
+ /** Exit 1 iff any REQUIRED check failed; advisory warns never flip it. */
64
+ export declare function buildDoctorReport(checks: CheckResult[]): DoctorReport;
65
+ export declare function formatDoctorReport(report: DoctorReport): string;
66
+ export interface DoctorDeps {
67
+ now?: () => number;
68
+ probePiEngine?: () => PiEngineProbe;
69
+ probeExtension?: () => ExtensionProbe;
70
+ readActiveProfile?: () => Promise<Profile>;
71
+ probeBackend?: (baseUrl: string, token: string) => Promise<BackendProbe>;
72
+ probeStateDir?: () => StateDirProbe;
73
+ ghOnPath?: () => boolean;
74
+ log?: (msg: string) => void;
75
+ }
76
+ /** Whether a `gh` executable is resolvable on PATH (no subprocess spawn). */
77
+ export declare function ghOnPathDefault(env?: NodeJS.ProcessEnv): boolean;
78
+ /**
79
+ * Gather every check result against the (injectable) probes. Pure ordering; each
80
+ * individual check is a pure function of its probe.
81
+ */
82
+ export declare function gatherChecks(deps?: DoctorDeps): Promise<CheckResult[]>;
83
+ /**
84
+ * Run the full doctor flow: gather → assemble → print → return the exit code.
85
+ * Never throws (a probe failure degrades to a failing/soft check, not a crash).
86
+ */
87
+ export declare function runDoctor(deps?: DoctorDeps): Promise<number>;
88
+ //# sourceMappingURL=doctor.d.ts.map
package/dist/doctor.js ADDED
@@ -0,0 +1,317 @@
1
+ /**
2
+ * `yagni doctor` — a green/red readiness checklist.
3
+ *
4
+ * Diagnoses whether a `yagni` launch will actually work: the pi engine is built,
5
+ * the extension is built, the active profile has a live (unexpired) token, the
6
+ * backend is reachable with YAGNI Code enabled, the state dir is locked down, and
7
+ * `gh` is available for `/go --pr`. Every failing/soft check carries a one-line
8
+ * actionable fix.
9
+ *
10
+ * The check LOGIC is pure (given probe inputs) so it is fully unit-testable; the
11
+ * impure probes (fs, fetch, profile read) are injectable seams. `runDoctor`
12
+ * returns the process exit code: 0 when all REQUIRED checks pass, 1 otherwise.
13
+ * Advisory checks (loose perms, missing `gh`) never flip the exit code.
14
+ */
15
+ import { existsSync, readFileSync, statSync } from "node:fs";
16
+ import { delimiter, join } from "node:path";
17
+ import { credentialsDir } from "./credentials.js";
18
+ import { classifyTokenExpiry } from "./launch.js";
19
+ import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
20
+ import { readActiveProfile } from "./profiles.js";
21
+ // ── Pure check builders ─────────────────────────────────────────────────────
22
+ export function checkPiEngine(probe) {
23
+ if (!probe.binPath || !probe.binExists) {
24
+ return {
25
+ name: "pi engine",
26
+ status: "fail",
27
+ detail: "not found or not built",
28
+ hint: "reinstall it: npm i -g @yagni-app/code (monorepo: npm i -g ./packages/yagni-code-cli)",
29
+ required: true,
30
+ };
31
+ }
32
+ return {
33
+ name: "pi engine",
34
+ status: "ok",
35
+ detail: probe.version ? `installed and built (v${probe.version})` : "installed and built",
36
+ required: true,
37
+ };
38
+ }
39
+ export function checkExtension(probe) {
40
+ if (!probe.path || !probe.exists) {
41
+ return {
42
+ name: "extension",
43
+ status: "fail",
44
+ detail: "pi-extension-yagni not built (dist/index.js missing)",
45
+ hint: "reinstall it: npm i -g @yagni-app/code (monorepo: pnpm --filter pi-extension-yagni build)",
46
+ required: true,
47
+ };
48
+ }
49
+ return { name: "extension", status: "ok", detail: "pi-extension-yagni built", required: true };
50
+ }
51
+ export function checkProfileToken(profile) {
52
+ if (!profile.token) {
53
+ return {
54
+ name: "profile",
55
+ status: "fail",
56
+ detail: `not logged in to "${profile.name}"`,
57
+ hint: "run `yagni login`",
58
+ required: true,
59
+ };
60
+ }
61
+ return {
62
+ name: "profile",
63
+ status: "ok",
64
+ detail: `logged in to "${profile.name}"`,
65
+ required: true,
66
+ };
67
+ }
68
+ export function checkTokenExpiry(status) {
69
+ switch (status.kind) {
70
+ case "expired":
71
+ return {
72
+ name: "token",
73
+ status: "fail",
74
+ detail: "expired",
75
+ hint: "run `yagni login` to re-authenticate",
76
+ required: true,
77
+ };
78
+ case "approaching":
79
+ return {
80
+ name: "token",
81
+ status: "warn",
82
+ detail: `expires in about ${status.minutesRemaining} minute${status.minutesRemaining === 1 ? "" : "s"}`,
83
+ hint: "run `yagni login` (launch also auto-refreshes within 7 days of expiry)",
84
+ required: false,
85
+ };
86
+ case "valid":
87
+ return { name: "token", status: "ok", detail: "valid", required: false };
88
+ case "unknown":
89
+ default:
90
+ return { name: "token", status: "ok", detail: "no expiry recorded (older login)", required: false };
91
+ }
92
+ }
93
+ export function checkBackend(probe) {
94
+ if (probe.kind === "skipped") {
95
+ return {
96
+ name: "backend",
97
+ status: "warn",
98
+ detail: "skipped (not logged in)",
99
+ hint: "run `yagni login`",
100
+ required: false,
101
+ };
102
+ }
103
+ if (probe.kind === "network") {
104
+ return {
105
+ name: "backend",
106
+ status: "warn",
107
+ detail: "could not reach the backend (network)",
108
+ hint: "check your connection, or `yagni use <env>` to point at the right host",
109
+ required: false,
110
+ };
111
+ }
112
+ const { status } = probe;
113
+ if (status === 200) {
114
+ return { name: "backend", status: "ok", detail: "reachable, YAGNI Code enabled", required: true };
115
+ }
116
+ if (status === 401 || status === 403) {
117
+ return {
118
+ name: "backend",
119
+ status: "fail",
120
+ detail: `token rejected (HTTP ${status})`,
121
+ hint: "run `yagni login` to re-authenticate",
122
+ required: true,
123
+ };
124
+ }
125
+ if (status === 404) {
126
+ return {
127
+ name: "backend",
128
+ status: "fail",
129
+ detail: "YAGNI Code is not enabled for this workspace (HTTP 404)",
130
+ hint: "ask an admin to enable the yagni_code.enabled feature flag",
131
+ required: true,
132
+ };
133
+ }
134
+ return {
135
+ name: "backend",
136
+ status: "warn",
137
+ detail: `unexpected response (HTTP ${status})`,
138
+ hint: "retry in a moment; if it persists, contact support",
139
+ required: false,
140
+ };
141
+ }
142
+ export function checkStateDir(probe) {
143
+ if (!probe.exists || probe.mode === null) {
144
+ return {
145
+ name: "state dir",
146
+ status: "ok",
147
+ detail: `${probe.path} not created yet`,
148
+ required: false,
149
+ };
150
+ }
151
+ // Any group/other bit set on a dir that holds live tokens is a leak.
152
+ if ((probe.mode & 0o077) !== 0) {
153
+ return {
154
+ name: "state dir",
155
+ status: "warn",
156
+ detail: `${probe.path} is ${toOctal(probe.mode)} (should be 0700)`,
157
+ hint: `run: chmod 700 ${probe.path}`,
158
+ required: false,
159
+ };
160
+ }
161
+ return { name: "state dir", status: "ok", detail: `${probe.path} is 0700`, required: false };
162
+ }
163
+ export function checkGh(onPath) {
164
+ if (onPath) {
165
+ return {
166
+ name: "gh (optional)",
167
+ status: "ok",
168
+ detail: "on PATH (for `yagni /go --pr`)",
169
+ required: false,
170
+ };
171
+ }
172
+ return {
173
+ name: "gh (optional)",
174
+ status: "warn",
175
+ detail: "not found (only needed for `yagni /go --pr`)",
176
+ hint: "install the GitHub CLI: https://cli.github.com",
177
+ required: false,
178
+ };
179
+ }
180
+ function toOctal(mode) {
181
+ return `0${(mode & 0o777).toString(8).padStart(3, "0")}`;
182
+ }
183
+ /** Exit 1 iff any REQUIRED check failed; advisory warns never flip it. */
184
+ export function buildDoctorReport(checks) {
185
+ const failedRequired = checks.some((c) => c.required && c.status === "fail");
186
+ return { checks, exitCode: failedRequired ? 1 : 0 };
187
+ }
188
+ const SYMBOL = { ok: "✓", warn: "!", fail: "✗" };
189
+ export function formatDoctorReport(report) {
190
+ const lines = ["YAGNI Code doctor", ""];
191
+ const width = Math.max(...report.checks.map((c) => c.name.length), 0);
192
+ for (const c of report.checks) {
193
+ lines.push(` ${SYMBOL[c.status]} ${c.name.padEnd(width)} ${c.detail}`);
194
+ if (c.status !== "ok" && c.hint) {
195
+ lines.push(` ${" ".repeat(width + 3)}→ ${c.hint}`);
196
+ }
197
+ }
198
+ lines.push("");
199
+ const problems = report.checks.filter((c) => c.required && c.status === "fail").length;
200
+ const advisories = report.checks.filter((c) => c.status === "warn").length;
201
+ if (problems > 0) {
202
+ lines.push(`${problems} problem${problems === 1 ? "" : "s"} found — see the hints above.`);
203
+ }
204
+ else if (advisories > 0) {
205
+ lines.push(`All required checks passed (${advisories} advisory note${advisories === 1 ? "" : "s"}).`);
206
+ }
207
+ else {
208
+ lines.push("All checks passed. You're ready to run `yagni`.");
209
+ }
210
+ return lines.join("\n");
211
+ }
212
+ function defaultProbePiEngine() {
213
+ try {
214
+ const binPath = resolvePiCliPath();
215
+ const binExists = existsSync(binPath);
216
+ let version;
217
+ try {
218
+ const pkg = JSON.parse(readFileSync(join(resolvePiPackageDir(), "package.json"), "utf8"));
219
+ version = typeof pkg.version === "string" ? pkg.version : undefined;
220
+ }
221
+ catch {
222
+ /* version is nice-to-have */
223
+ }
224
+ return { binPath, binExists, version };
225
+ }
226
+ catch {
227
+ return { binExists: false };
228
+ }
229
+ }
230
+ function defaultProbeExtension() {
231
+ try {
232
+ const path = resolveExtensionPath();
233
+ return { path, exists: existsSync(path) };
234
+ }
235
+ catch {
236
+ return { exists: false };
237
+ }
238
+ }
239
+ function defaultProbeStateDir() {
240
+ const path = credentialsDir();
241
+ try {
242
+ const st = statSync(path);
243
+ return { path, exists: true, mode: st.mode & 0o777 };
244
+ }
245
+ catch {
246
+ return { path, exists: false, mode: null };
247
+ }
248
+ }
249
+ async function defaultProbeBackend(baseUrl, token) {
250
+ if (!token)
251
+ return { kind: "skipped" };
252
+ try {
253
+ const res = await fetch(`${baseUrl}/api/yagni-code/models`, {
254
+ method: "GET",
255
+ headers: { authorization: `Bearer ${token}` },
256
+ signal: AbortSignal.timeout(10_000),
257
+ });
258
+ return { kind: "status", status: res.status };
259
+ }
260
+ catch {
261
+ return { kind: "network" };
262
+ }
263
+ }
264
+ /** Whether a `gh` executable is resolvable on PATH (no subprocess spawn). */
265
+ export function ghOnPathDefault(env = process.env) {
266
+ const raw = env.PATH ?? "";
267
+ const names = process.platform === "win32" ? ["gh.exe", "gh.cmd", "gh"] : ["gh"];
268
+ for (const dir of raw.split(delimiter)) {
269
+ if (!dir)
270
+ continue;
271
+ for (const name of names) {
272
+ if (existsSync(join(dir, name)))
273
+ return true;
274
+ }
275
+ }
276
+ return false;
277
+ }
278
+ /**
279
+ * Gather every check result against the (injectable) probes. Pure ordering; each
280
+ * individual check is a pure function of its probe.
281
+ */
282
+ export async function gatherChecks(deps = {}) {
283
+ const now = deps.now ?? Date.now;
284
+ const probePiEngine = deps.probePiEngine ?? defaultProbePiEngine;
285
+ const probeExtension = deps.probeExtension ?? defaultProbeExtension;
286
+ const readProfile = deps.readActiveProfile ?? readActiveProfile;
287
+ const probeBackend = deps.probeBackend ?? defaultProbeBackend;
288
+ const probeStateDir = deps.probeStateDir ?? defaultProbeStateDir;
289
+ const ghOnPath = deps.ghOnPath ?? (() => ghOnPathDefault());
290
+ const checks = [];
291
+ checks.push(checkPiEngine(probePiEngine()));
292
+ checks.push(checkExtension(probeExtension()));
293
+ const profile = await readProfile();
294
+ checks.push(checkProfileToken(profile));
295
+ if (profile.token) {
296
+ checks.push(checkTokenExpiry(classifyTokenExpiry(profile.expiresAt, now())));
297
+ }
298
+ const backend = profile.token
299
+ ? await probeBackend(profile.baseUrl, profile.token)
300
+ : { kind: "skipped" };
301
+ checks.push(checkBackend(backend));
302
+ checks.push(checkStateDir(probeStateDir()));
303
+ checks.push(checkGh(ghOnPath()));
304
+ return checks;
305
+ }
306
+ /**
307
+ * Run the full doctor flow: gather → assemble → print → return the exit code.
308
+ * Never throws (a probe failure degrades to a failing/soft check, not a crash).
309
+ */
310
+ export async function runDoctor(deps = {}) {
311
+ const log = deps.log ?? ((m) => process.stdout.write(`${m}\n`));
312
+ const checks = await gatherChecks(deps);
313
+ const report = buildDoctorReport(checks);
314
+ log(formatDoctorReport(report));
315
+ return report.exitCode;
316
+ }
317
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Build the configured pi launch. `buildLaunch` is pure (no spawn, no fs) so it
3
+ * is fully unit-testable: it returns the child environment and the pi argv that
4
+ * registers pi-extension-yagni and defaults the provider to `yagni`.
5
+ */
6
+ import type { Credentials } from "./credentials.js";
7
+ export interface LaunchPlan {
8
+ env: NodeJS.ProcessEnv;
9
+ argv: string[];
10
+ /**
11
+ * Non-fatal launch-time notices for the caller to surface (e.g. an
12
+ * expiry-approaching warning). Empty when there is nothing to say.
13
+ */
14
+ warnings: string[];
15
+ }
16
+ /**
17
+ * How close to expiry the token can be before launch warns. A coding session
18
+ * easily outlives a token, so warn early enough that re-logging in before a long
19
+ * task is worthwhile, without nagging on every launch.
20
+ */
21
+ export declare const EXPIRY_WARNING_THRESHOLD_MS: number;
22
+ export type TokenExpiryStatus = {
23
+ kind: "unknown";
24
+ } | {
25
+ kind: "valid";
26
+ } | {
27
+ kind: "approaching";
28
+ minutesRemaining: number;
29
+ } | {
30
+ kind: "expired";
31
+ };
32
+ /**
33
+ * Pure classifier for a token's ISO `expiresAt` against the current time. A
34
+ * missing or unparseable expiry is "unknown" (older profiles predate the field,
35
+ * so we proceed rather than block). Never throws.
36
+ */
37
+ export declare function classifyTokenExpiry(expiresAt: string | undefined, nowMs: number, thresholdMs?: number): TokenExpiryStatus;
38
+ export interface BuildLaunchOptions {
39
+ /** Absolute path to pi-extension-yagni's entry (resolved by the caller). */
40
+ extensionPath: string;
41
+ /**
42
+ * Isolated pi config dir. Pinning this keeps YAGNI Code hermetic: it never
43
+ * reads the user's machine-wide `~/.pi/agent`, so local providers
44
+ * (Together/OpenRouter), stored keys, a saved default model, and theme cannot
45
+ * bleed in or route around the YAGNI proxy.
46
+ */
47
+ agentDir: string;
48
+ /**
49
+ * Shadow pi package dir to expose as `PI_PACKAGE_DIR` so pi rebrands its
50
+ * chrome to "YAGNI Code". Optional: if the shadow package could not be
51
+ * generated, we omit it and pi runs un-rebranded but still hermetic.
52
+ */
53
+ piPackageDir?: string;
54
+ /**
55
+ * Absolute path of the active profile's JSON file. Forwarded to the child as
56
+ * `YAGNI_PROFILE_PATH` so the extension can persist a mid-session token
57
+ * rotation back to the same profile the launcher read. Optional: omitted when
58
+ * the caller cannot resolve it (the extension then keeps its rotation
59
+ * in-process only, still fail-soft).
60
+ */
61
+ profilePath?: string;
62
+ /** Base environment to extend (defaults to process.env at call sites). */
63
+ baseEnv?: NodeJS.ProcessEnv;
64
+ /** Clock seam for the token-expiry preflight (defaults to Date.now). */
65
+ now?: () => number;
66
+ }
67
+ export declare function buildLaunch(creds: Credentials | null, passthroughArgs: string[], opts: BuildLaunchOptions): LaunchPlan;
68
+ //# sourceMappingURL=launch.d.ts.map