@cad0p/pi-usage 0.0.0-stage → 0.11.2
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/LICENSE +21 -0
- package/README.md +509 -2
- package/bin/piquota.js +199 -0
- package/extensions/quota-panel.ts +963 -0
- package/package.json +59 -4
- package/src/auth/claude-code-auth.js +490 -0
- package/src/auth/jwt.js +84 -0
- package/src/auth/pi-auth.js +424 -0
- package/src/browser/cookies.js +557 -0
- package/src/browser/history.js +173 -0
- package/src/cache.js +224 -0
- package/src/cli/args.js +154 -0
- package/src/cli/commands/auth.js +216 -0
- package/src/cli/commands/gentle-pi.js +44 -0
- package/src/cli/commands/omarchy.js +75 -0
- package/src/cli/explain.js +99 -0
- package/src/cli/output.js +34 -0
- package/src/engine.js +404 -0
- package/src/exec.js +31 -0
- package/src/gentle-pi/rail-patch.js +335 -0
- package/src/http.js +182 -0
- package/src/model.js +332 -0
- package/src/omarchy/assets/antigravity.svg +3 -0
- package/src/omarchy/assets/claude.svg +1 -0
- package/src/omarchy/assets/codex.svg +1 -0
- package/src/omarchy/assets/github-actions.svg +3 -0
- package/src/omarchy/assets/opencode-go.svg +3 -0
- package/src/omarchy/publish.js +152 -0
- package/src/omarchy/record.js +462 -0
- package/src/opencode/dashboard.js +337 -0
- package/src/opencode/session.js +485 -0
- package/src/providers/antigravity-oauth.js +173 -0
- package/src/providers/antigravity.js +225 -0
- package/src/providers/backoff.js +146 -0
- package/src/providers/claude.js +194 -0
- package/src/providers/codex.js +151 -0
- package/src/providers/error-kind.js +128 -0
- package/src/providers/github-actions.js +379 -0
- package/src/providers/opencode-go.js +274 -0
- package/src/refresh.js +292 -0
- package/src/render/panel.js +411 -0
- package/src/render/theme.js +102 -0
- package/src/sticky.js +319 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth subcommands: OpenCode session capture and credential status inspection.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
import { describeStore, resolveAuthPaths } from "../../auth/pi-auth.js";
|
|
9
|
+
import { discoverCookieStores, findCookie } from "../../browser/cookies.js";
|
|
10
|
+
import { humanReset } from "../../model.js";
|
|
11
|
+
import {
|
|
12
|
+
OPENCODE_COOKIE_NAME,
|
|
13
|
+
OPENCODE_COOKIE_NAMES,
|
|
14
|
+
configPaths,
|
|
15
|
+
discoverWorkspaceId,
|
|
16
|
+
readGoPlan,
|
|
17
|
+
resolveCookie,
|
|
18
|
+
writeSecretFile,
|
|
19
|
+
} from "../../opencode/session.js";
|
|
20
|
+
import { err, out, sleep } from "../output.js";
|
|
21
|
+
|
|
22
|
+
const OPENCODE_LOGIN_URL = "https://opencode.ai/auth";
|
|
23
|
+
const WINDOWS_FIREFOX = [
|
|
24
|
+
"/mnt/c/Program Files/Mozilla Firefox/firefox.exe",
|
|
25
|
+
"/mnt/c/Program Files (x86)/Mozilla Firefox/firefox.exe",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Launch Firefox at the login URL. Returns how it was launched.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} url
|
|
32
|
+
* @returns {{ launched: boolean, via: string, error?: string }}
|
|
33
|
+
*/
|
|
34
|
+
export function openLogin(url) {
|
|
35
|
+
for (const candidate of WINDOWS_FIREFOX) {
|
|
36
|
+
if (!existsSync(candidate)) continue;
|
|
37
|
+
const result = spawnSync(candidate, [url], { stdio: "ignore", detached: true });
|
|
38
|
+
if (!result.error) return { launched: true, via: `Windows Firefox (${candidate})` };
|
|
39
|
+
}
|
|
40
|
+
if (process.platform === "darwin" || existsSync("/Applications/Firefox.app")) {
|
|
41
|
+
const launched = spawnSync("open", ["-a", "Firefox", url], { stdio: "ignore", detached: true });
|
|
42
|
+
if (!launched.error && launched.status === 0) return { launched: true, via: "macOS Firefox (open -a Firefox)" };
|
|
43
|
+
}
|
|
44
|
+
for (const command of ["firefox", "firefox-esr", "firefox-bin"]) {
|
|
45
|
+
const result = spawnSync("which", [command], { encoding: "utf-8" });
|
|
46
|
+
if (result.status !== 0) continue;
|
|
47
|
+
const launched = spawnSync(command, [url], { stdio: "ignore", detached: true });
|
|
48
|
+
if (!launched.error) return { launched: true, via: `Firefox (${command})` };
|
|
49
|
+
}
|
|
50
|
+
return { launched: false, via: "none", error: "no Firefox binary found" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {{ stores?: import("../../browser/cookies.js").CookieStore[] }} [options]
|
|
55
|
+
* @returns {Promise<number>}
|
|
56
|
+
*/
|
|
57
|
+
export async function verifyGoPlan(options = {}) {
|
|
58
|
+
const cookie = resolveCookie({ stores: options.stores, allowBrowser: true });
|
|
59
|
+
if (!cookie.found) {
|
|
60
|
+
err(`no opencode.ai session found (${cookie.detail})`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const workspace = await discoverWorkspaceId({ cookie: cookie.value, stores: options.stores });
|
|
65
|
+
if (!workspace.workspaceId) {
|
|
66
|
+
err(`session found but no workspace id (${workspace.error})`);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
out(` workspace: ${workspace.workspaceId}${workspace.origin ? ` (from ${workspace.origin})` : ""}`);
|
|
70
|
+
|
|
71
|
+
const plan = await readGoPlan({ cookie: cookie.value, workspaceId: workspace.workspaceId });
|
|
72
|
+
if (!plan.ok) {
|
|
73
|
+
err(` dashboard read failed: ${plan.error}`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
out(" Go plan windows:");
|
|
77
|
+
for (const window of plan.windows) {
|
|
78
|
+
const remaining = window.remainingPercent === null ? "n/a" : `${Math.round(window.remainingPercent)}% left`;
|
|
79
|
+
const reset = window.resetsInSec === null ? "" : ` · ${humanReset(window.resetsInSec)}`;
|
|
80
|
+
out(` ${window.label.padEnd(16)} ${remaining}${reset}`);
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {string[]} argv
|
|
87
|
+
* @returns {Promise<number>}
|
|
88
|
+
*/
|
|
89
|
+
export async function authOpenCode(argv) {
|
|
90
|
+
const paste = argv.includes("--paste");
|
|
91
|
+
const noBrowser = argv.includes("--no-browser");
|
|
92
|
+
const waitIndex = argv.indexOf("--wait");
|
|
93
|
+
const waitSeconds = waitIndex >= 0 ? Number(argv[waitIndex + 1]) || 180 : 180;
|
|
94
|
+
|
|
95
|
+
const paths = configPaths({});
|
|
96
|
+
out("OpenCode Go session");
|
|
97
|
+
out(` config file: ${paths.path}`);
|
|
98
|
+
|
|
99
|
+
if (paste) {
|
|
100
|
+
const chunks = [];
|
|
101
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
102
|
+
const value = Buffer.concat(chunks).toString("utf-8").trim();
|
|
103
|
+
if (value === "") {
|
|
104
|
+
err("no cookie received on stdin");
|
|
105
|
+
return 2;
|
|
106
|
+
}
|
|
107
|
+
const written = writeSecretFile(paths.path, value);
|
|
108
|
+
if (!written.ok) {
|
|
109
|
+
err(`could not write ${paths.path}: ${written.error}`);
|
|
110
|
+
return 2;
|
|
111
|
+
}
|
|
112
|
+
out(` stored (0600): ${paths.path}`);
|
|
113
|
+
return verifyGoPlan({});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const stores = discoverCookieStores({});
|
|
117
|
+
out(" browser stores found:");
|
|
118
|
+
if (stores.length === 0) {
|
|
119
|
+
out(" (none)");
|
|
120
|
+
}
|
|
121
|
+
for (const store of stores) {
|
|
122
|
+
const readable = store.readability === "plaintext";
|
|
123
|
+
out(` ${readable ? "readable " : "ENCRYPTED"} ${store.browser} ${store.profile}`);
|
|
124
|
+
if (!readable && store.note) out(` ${store.note}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const before = resolveCookie({ allowBrowser: false });
|
|
128
|
+
if (before.found) {
|
|
129
|
+
out(` a session is already available via ${before.origin} (${before.detail})`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const readableFirefox = stores.filter((store) => store.browser === "firefox" && store.readability === "plaintext");
|
|
133
|
+
if (readableFirefox.length === 0) {
|
|
134
|
+
err("");
|
|
135
|
+
err("No readable Firefox cookie store found. Log in with Firefox, or paste the cookie:");
|
|
136
|
+
err(" piquota auth opencode --paste (get `auth` for opencode.ai from your browser devtools)");
|
|
137
|
+
return 2;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
out("");
|
|
141
|
+
if (!noBrowser) {
|
|
142
|
+
const opened = openLogin(OPENCODE_LOGIN_URL);
|
|
143
|
+
out(opened.launched ? ` opened ${OPENCODE_LOGIN_URL} in ${opened.via}` : ` could not launch a browser (${opened.error})`);
|
|
144
|
+
if (!opened.launched) out(` open manually: ${OPENCODE_LOGIN_URL}`);
|
|
145
|
+
} else {
|
|
146
|
+
out(` open this URL in Firefox: ${OPENCODE_LOGIN_URL}`);
|
|
147
|
+
}
|
|
148
|
+
out(" log in with GitHub, Google or Apple, then click Authorize on the consent screen.");
|
|
149
|
+
out(` Waiting up to ${waitSeconds}s for the session...`);
|
|
150
|
+
|
|
151
|
+
const deadline = Date.now() + waitSeconds * 1000;
|
|
152
|
+
let found = null;
|
|
153
|
+
while (Date.now() < deadline) {
|
|
154
|
+
await sleep(3);
|
|
155
|
+
for (const name of OPENCODE_COOKIE_NAMES) {
|
|
156
|
+
const hit = findCookie({ host: "opencode.ai", name, stores: readableFirefox });
|
|
157
|
+
if (hit.found && hit.value) {
|
|
158
|
+
found = hit;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (found) break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (!found) {
|
|
166
|
+
err("");
|
|
167
|
+
err(`No "${OPENCODE_COOKIE_NAME}" cookie for opencode.ai appeared within ${waitSeconds}s.`);
|
|
168
|
+
err("Check that you completed the login in Firefox, then re-run this command.");
|
|
169
|
+
return 1;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
out(` session captured from ${found.store?.browser} ${found.store?.profile}`);
|
|
173
|
+
out(" the cookie is read live from the browser store; it is not copied anywhere");
|
|
174
|
+
const code = await verifyGoPlan({ stores: readableFirefox });
|
|
175
|
+
if (code !== 0) {
|
|
176
|
+
out("");
|
|
177
|
+
out("If the workspace id could not be found, open https://opencode.ai/ once in Firefox");
|
|
178
|
+
out("(the app lands on /workspace/<id>), then run: piquota auth opencode");
|
|
179
|
+
}
|
|
180
|
+
return code;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @returns {Promise<number>}
|
|
185
|
+
*/
|
|
186
|
+
export async function authStatus() {
|
|
187
|
+
out("Credential sources");
|
|
188
|
+
const paths = resolveAuthPaths({});
|
|
189
|
+
for (const path of paths) {
|
|
190
|
+
const store = describeStore(path);
|
|
191
|
+
out(` pi store ${path} (${store.sizeBytes ?? "?"} bytes)`);
|
|
192
|
+
}
|
|
193
|
+
const cookie = resolveCookie({});
|
|
194
|
+
out(` opencode ${cookie.found ? `session via ${cookie.origin} — ${cookie.detail}` : `no session — ${cookie.detail}`}`);
|
|
195
|
+
const workspace = configPaths({}).workspacePath;
|
|
196
|
+
out(` workspace ${existsSync(workspace) ? "cached" : "not cached"}`);
|
|
197
|
+
|
|
198
|
+
const stores = discoverCookieStores({});
|
|
199
|
+
out(" browser stores:");
|
|
200
|
+
for (const store of stores) {
|
|
201
|
+
out(` ${store.readability === "plaintext" ? "readable " : "ENCRYPTED"} ${store.browser} ${store.profile}`);
|
|
202
|
+
}
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @param {string} sub
|
|
208
|
+
* @param {string[]} argv
|
|
209
|
+
* @returns {Promise<number>}
|
|
210
|
+
*/
|
|
211
|
+
export async function runAuthCommand(sub, argv) {
|
|
212
|
+
if (sub === "opencode") return authOpenCode(argv);
|
|
213
|
+
if (sub === "status") return authStatus();
|
|
214
|
+
err(`unknown auth action: ${sub}`);
|
|
215
|
+
return 2;
|
|
216
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gentle-pi subcommands: inspect, apply, or revert rail patch.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
applyRailPatch,
|
|
7
|
+
inspectRailPatch,
|
|
8
|
+
resolveGentlePiLayout,
|
|
9
|
+
revertRailPatch,
|
|
10
|
+
} from "../../gentle-pi/rail-patch.js";
|
|
11
|
+
import { err, out } from "../output.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} sub
|
|
15
|
+
* @returns {number}
|
|
16
|
+
*/
|
|
17
|
+
export function runGentlePiCommand(sub) {
|
|
18
|
+
if (sub !== "status" && sub !== "apply" && sub !== "revert") {
|
|
19
|
+
err(`unknown gentle-pi action: ${sub}`);
|
|
20
|
+
err("expected one of: status, apply, revert");
|
|
21
|
+
return 2;
|
|
22
|
+
}
|
|
23
|
+
const layout = resolveGentlePiLayout();
|
|
24
|
+
if (!layout.present) {
|
|
25
|
+
out("gentle-pi: not installed");
|
|
26
|
+
out("nothing to do: piQuota renders its own widget without the rail");
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
out(`gentle-pi: ${layout.layoutPath}`);
|
|
30
|
+
if (sub === "status") {
|
|
31
|
+
const inspected = inspectRailPatch({ layoutPath: layout.layoutPath });
|
|
32
|
+
out(`rail patch: ${inspected.state} (${inspected.detail})`);
|
|
33
|
+
if (inspected.state !== "patched") {
|
|
34
|
+
out("run `piquota gentle-pi apply`, or let the extension repair it at session start");
|
|
35
|
+
}
|
|
36
|
+
return inspected.state === "unknown" ? 1 : 0;
|
|
37
|
+
}
|
|
38
|
+
const result = sub === "revert"
|
|
39
|
+
? revertRailPatch({ layoutPath: layout.layoutPath })
|
|
40
|
+
: applyRailPatch({ layoutPath: layout.layoutPath });
|
|
41
|
+
out(`rail patch: ${result.state} — ${result.detail}`);
|
|
42
|
+
if (result.backupPath) out(`backup: ${result.backupPath}`);
|
|
43
|
+
return result.ok ? 0 : 1;
|
|
44
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `piquota omarchy`: publish the quota as Omarchy agent usage records.
|
|
3
|
+
*
|
|
4
|
+
* One shot, opt-in, never throws: a provider that fails becomes a record that
|
|
5
|
+
* says so, and a write failure is reported with a non-zero exit code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { redact } from "../../http.js";
|
|
12
|
+
import { publishRecords, resolveOmarchyDir } from "../../omarchy/publish.js";
|
|
13
|
+
import { buildRecords } from "../../omarchy/record.js";
|
|
14
|
+
import { err, out } from "../output.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Families to leave out, e.g. PI_QUOTA_OMARCHY_SKIP=claude when Omarchy's own
|
|
18
|
+
* collector already covers that provider. Their stale pi-*.json is removed.
|
|
19
|
+
*
|
|
20
|
+
* Defaults to skipping `claude` when Omarchy's native `claude.json` exists in
|
|
21
|
+
* the destination directory, avoiding duplicate Claude cards in the bar panel.
|
|
22
|
+
*
|
|
23
|
+
* @param {Record<string, string | undefined>} env
|
|
24
|
+
* @param {string} [dir]
|
|
25
|
+
* @returns {Set<string>}
|
|
26
|
+
*/
|
|
27
|
+
export function skippedFamilies(env, dir) {
|
|
28
|
+
if (env.PI_QUOTA_OMARCHY_SKIP !== undefined) {
|
|
29
|
+
return new Set(
|
|
30
|
+
String(env.PI_QUOTA_OMARCHY_SKIP)
|
|
31
|
+
.split(",")
|
|
32
|
+
.map((value) => value.trim().toLowerCase())
|
|
33
|
+
.filter(Boolean),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const skip = new Set();
|
|
37
|
+
const targetDir = dir ?? resolveOmarchyDir({ env });
|
|
38
|
+
try {
|
|
39
|
+
if (existsSync(join(targetDir, "claude.json"))) {
|
|
40
|
+
skip.add("claude");
|
|
41
|
+
}
|
|
42
|
+
} catch {}
|
|
43
|
+
return skip;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {{ load: () => Promise<import("../../engine.js").PiQuotaReport>, dir?: string, env?: Record<string, string | undefined>, quiet?: boolean }} options
|
|
48
|
+
* @returns {Promise<number>}
|
|
49
|
+
*/
|
|
50
|
+
export async function runOmarchyCommand(options) {
|
|
51
|
+
let report;
|
|
52
|
+
try {
|
|
53
|
+
report = await options.load();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
err(`omarchy: could not collect quota: ${redact(/** @type {{ message?: string }} */ (error)?.message ?? error)}`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const skip = skippedFamilies(options.env ?? process.env, options.dir);
|
|
60
|
+
const records = buildRecords(skip.size > 0 ? { ...report, providers: (report.providers ?? []).filter((p) => !skip.has(p?.family)) } : report);
|
|
61
|
+
let result;
|
|
62
|
+
try {
|
|
63
|
+
result = publishRecords(records, { dir: options.dir, env: options.env });
|
|
64
|
+
} catch (error) {
|
|
65
|
+
err(`omarchy: could not write records: ${redact(/** @type {{ message?: string }} */ (error)?.message ?? error)}`);
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!options.quiet) {
|
|
70
|
+
out(`omarchy: wrote ${result.written.length} record(s) to ${result.dir}`);
|
|
71
|
+
for (const path of result.written) out(` ${path}`);
|
|
72
|
+
for (const path of result.removed) out(` removed stale ${path}`);
|
|
73
|
+
}
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Family aliases, family resolution and diagnostic explain formatting.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describeClaudeCodeSource } from "../auth/claude-code-auth.js";
|
|
6
|
+
import { describeStore, loadPiCredentials, resolveAuthPaths } from "../auth/pi-auth.js";
|
|
7
|
+
import { describeCache, resolveCachePath } from "../cache.js";
|
|
8
|
+
import { FAMILIES } from "../engine.js";
|
|
9
|
+
import { redact } from "../http.js";
|
|
10
|
+
import { resolveCookie } from "../opencode/session.js";
|
|
11
|
+
|
|
12
|
+
export const FAMILY_ALIASES = {
|
|
13
|
+
claude: "claude",
|
|
14
|
+
anthropic: "claude",
|
|
15
|
+
codex: "codex",
|
|
16
|
+
"openai-codex": "codex",
|
|
17
|
+
chatgpt: "codex",
|
|
18
|
+
antigravity: "antigravity",
|
|
19
|
+
google: "antigravity",
|
|
20
|
+
agy: "antigravity",
|
|
21
|
+
"opencode-go": "opencode-go",
|
|
22
|
+
opencode: "opencode-go",
|
|
23
|
+
go: "opencode-go",
|
|
24
|
+
zen: "opencode-go",
|
|
25
|
+
"github-actions": "github-actions",
|
|
26
|
+
gh: "github-actions",
|
|
27
|
+
actions: "github-actions",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string[]} positionals
|
|
32
|
+
* @returns {{ families: string[], unknown: string[], explicit: boolean }}
|
|
33
|
+
*/
|
|
34
|
+
export function resolveFamilies(positionals) {
|
|
35
|
+
/** @type {string[]} */
|
|
36
|
+
const families = [];
|
|
37
|
+
/** @type {string[]} */
|
|
38
|
+
const unknown = [];
|
|
39
|
+
for (const positional of positionals) {
|
|
40
|
+
if (positional === "all") continue;
|
|
41
|
+
const family = FAMILY_ALIASES[positional.toLowerCase()];
|
|
42
|
+
if (!family) {
|
|
43
|
+
unknown.push(positional);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!families.includes(family)) families.push(family);
|
|
47
|
+
}
|
|
48
|
+
return { families, unknown, explicit: families.length > 0 };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string[]} families
|
|
53
|
+
* @returns {string[]}
|
|
54
|
+
*/
|
|
55
|
+
export function explainLines(families) {
|
|
56
|
+
const paths = resolveAuthPaths({});
|
|
57
|
+
const lines = ["", "Credential resolution:"];
|
|
58
|
+
if (paths.length === 0) {
|
|
59
|
+
lines.push(" no Pi auth store found (looked for $PI_AUTH_PATH, ~/.pi/agent/auth.json, /mnt/c/Users/*/.pi/agent/auth.json)");
|
|
60
|
+
}
|
|
61
|
+
for (const path of paths) {
|
|
62
|
+
const store = describeStore(path);
|
|
63
|
+
lines.push(` store ${path} exists=${store.exists} file=${store.isFile} bytes=${store.sizeBytes ?? "?"}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const loaded = loadPiCredentials({});
|
|
67
|
+
const counts = new Map();
|
|
68
|
+
for (const credential of loaded.credentials) counts.set(credential.family, (counts.get(credential.family) ?? 0) + 1);
|
|
69
|
+
lines.push(" credentials: " + FAMILIES.map((family) => `${family}=${counts.get(family) ?? 0}`).join(" "));
|
|
70
|
+
lines.push(" fields read from auth.json: type, access, refresh, expires, accountId, projectId, email, key");
|
|
71
|
+
lines.push(" derived from the Codex JWT (payload only): chatgpt_account_id, chatgpt_plan_type, email");
|
|
72
|
+
|
|
73
|
+
const cookie = resolveCookie({});
|
|
74
|
+
lines.push(` opencode.ai cookie: ${cookie.found ? `found via ${cookie.origin} (${cookie.detail})` : `not found (${cookie.detail})`}`);
|
|
75
|
+
// Claude has two sources, and which one is in use is the single most useful
|
|
76
|
+
// diagnostic for a provider that answers 400 or "not configured".
|
|
77
|
+
const claudeCode = describeClaudeCodeSource({});
|
|
78
|
+
lines.push("");
|
|
79
|
+
lines.push("Claude source resolution:");
|
|
80
|
+
lines.push(` preference: ${process.env.PI_QUOTA_CLAUDE_SOURCE ?? "auto"} (auto prefers the Claude Code CLI)`);
|
|
81
|
+
lines.push(` Claude Code store: ${claudeCode.path ?? "none found"}`);
|
|
82
|
+
if (claudeCode.path) {
|
|
83
|
+
lines.push(
|
|
84
|
+
` access token: ${claudeCode.hasAccessToken ? "yes" : "no"}` +
|
|
85
|
+
` · refresh token present: ${claudeCode.hasRefreshToken ? "yes (never read, never used)" : "no"}` +
|
|
86
|
+
` · expires in: ${claudeCode.expiresInMin === null ? "unknown" : `${claudeCode.expiresInMin}m`}` +
|
|
87
|
+
` · plan: ${claudeCode.plan ?? "unknown"}`,
|
|
88
|
+
);
|
|
89
|
+
} else if (claudeCode.error) {
|
|
90
|
+
lines.push(` ${redact(claudeCode.error)}`);
|
|
91
|
+
}
|
|
92
|
+
lines.push(" fields read from that store: claudeAiOauth.accessToken, expiresAt, subscriptionType, rateLimitTier");
|
|
93
|
+
lines.push(" never read from that store: the refresh token value, and nothing is ever written back");
|
|
94
|
+
lines.push(` cache: ${resolveCachePath({})} (${describeCache({}).exists ? "present" : "absent"})`);
|
|
95
|
+
lines.push(` families requested: ${families.join(", ")}`);
|
|
96
|
+
for (const warning of loaded.warnings) lines.push(` warn: ${redact(warning)}`);
|
|
97
|
+
lines.push("");
|
|
98
|
+
return lines;
|
|
99
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI output and timing helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {string} text
|
|
7
|
+
*/
|
|
8
|
+
export function out(text) {
|
|
9
|
+
process.stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} text
|
|
14
|
+
*/
|
|
15
|
+
export function err(text) {
|
|
16
|
+
process.stderr.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A non-fatal problem worth showing without hiding the rest of the output.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} text
|
|
23
|
+
*/
|
|
24
|
+
export function warn(text) {
|
|
25
|
+
process.stderr.write(`warning: ${text.endsWith("\n") ? text : `${text}\n`}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {number} seconds
|
|
30
|
+
* @returns {Promise<void>}
|
|
31
|
+
*/
|
|
32
|
+
export function sleep(seconds) {
|
|
33
|
+
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
|
34
|
+
}
|