@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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +509 -2
  3. package/bin/piquota.js +199 -0
  4. package/extensions/quota-panel.ts +963 -0
  5. package/package.json +59 -4
  6. package/src/auth/claude-code-auth.js +490 -0
  7. package/src/auth/jwt.js +84 -0
  8. package/src/auth/pi-auth.js +424 -0
  9. package/src/browser/cookies.js +557 -0
  10. package/src/browser/history.js +173 -0
  11. package/src/cache.js +224 -0
  12. package/src/cli/args.js +154 -0
  13. package/src/cli/commands/auth.js +216 -0
  14. package/src/cli/commands/gentle-pi.js +44 -0
  15. package/src/cli/commands/omarchy.js +75 -0
  16. package/src/cli/explain.js +99 -0
  17. package/src/cli/output.js +34 -0
  18. package/src/engine.js +404 -0
  19. package/src/exec.js +31 -0
  20. package/src/gentle-pi/rail-patch.js +335 -0
  21. package/src/http.js +182 -0
  22. package/src/model.js +332 -0
  23. package/src/omarchy/assets/antigravity.svg +3 -0
  24. package/src/omarchy/assets/claude.svg +1 -0
  25. package/src/omarchy/assets/codex.svg +1 -0
  26. package/src/omarchy/assets/github-actions.svg +3 -0
  27. package/src/omarchy/assets/opencode-go.svg +3 -0
  28. package/src/omarchy/publish.js +152 -0
  29. package/src/omarchy/record.js +462 -0
  30. package/src/opencode/dashboard.js +337 -0
  31. package/src/opencode/session.js +485 -0
  32. package/src/providers/antigravity-oauth.js +173 -0
  33. package/src/providers/antigravity.js +225 -0
  34. package/src/providers/backoff.js +146 -0
  35. package/src/providers/claude.js +194 -0
  36. package/src/providers/codex.js +151 -0
  37. package/src/providers/error-kind.js +128 -0
  38. package/src/providers/github-actions.js +379 -0
  39. package/src/providers/opencode-go.js +274 -0
  40. package/src/refresh.js +292 -0
  41. package/src/render/panel.js +411 -0
  42. package/src/render/theme.js +102 -0
  43. package/src/sticky.js +319 -0
@@ -0,0 +1,557 @@
1
+ /**
2
+ * Read-only browser cookie access.
3
+ *
4
+ * Only used to obtain the opencode.ai session cookie that Pi does not store.
5
+ * Guarantees:
6
+ * - the browser database is never opened in place: it is copied to a private
7
+ * temporary directory (including `-wal`/`-shm`) and opened read-only there;
8
+ * - cookie values are returned to the caller but never logged, printed or
9
+ * written to the cache;
10
+ * - Chromium cookie databases are copied before read-only access; Linux OSCrypt
11
+ * values are decrypted in memory, while unsupported platform encryption is
12
+ * reported instead of guessed.
13
+ */
14
+
15
+ import { execFileSync } from "node:child_process";
16
+ import { createDecipheriv, pbkdf2Sync } from "node:crypto";
17
+ import { DatabaseSync } from "node:sqlite";
18
+ import { copyFileSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
19
+ import { homedir, tmpdir } from "node:os";
20
+ import { join } from "node:path";
21
+
22
+ const WINDOWS_USERS_ROOT = "/mnt/c/Users";
23
+
24
+ /**
25
+ * @typedef {Object} CookieStore
26
+ * @property {"firefox" | "chromium"} browser
27
+ * @property {string} path Path to the cookie database.
28
+ * @property {string} profile Human profile label.
29
+ * @property {"plaintext" | "encrypted" | "unknown"} readability
30
+ * @property {string} [note]
31
+ * @property {string} [app]
32
+ */
33
+
34
+ /**
35
+ * Firefox roots: ~/.mozilla/firefox, plus native Windows %APPDATA% profiles,
36
+ * plus every Windows profile reachable from WSL.
37
+ *
38
+ * @param {{ home?: string, appData?: string, usersRoot?: string, platform?: string }} [options]
39
+ * @returns {string[]}
40
+ */
41
+ export function firefoxRoots(options = {}) {
42
+ const home = options.home ?? homedir();
43
+ const platform = options.platform ?? process.platform;
44
+ const env = options.env ?? process.env;
45
+ /** @type {string[]} */
46
+ const roots = [];
47
+
48
+ if (platform === "win32") {
49
+ // Windows keeps profiles under %APPDATA%\Mozilla\Firefox, not ~/.mozilla/firefox.
50
+ const appData = options.appData ?? env.APPDATA ?? join(home, "AppData", "Roaming");
51
+ roots.push(join(appData, "Mozilla", "Firefox"));
52
+ } else if (platform === "darwin") {
53
+ roots.push(join(home, "Library", "Application Support", "Firefox"));
54
+ } else {
55
+ roots.push(
56
+ join(home, ".mozilla", "firefox"),
57
+ join(home, ".config", "mozilla", "firefox"),
58
+ join(home, "Library", "Application Support", "Firefox"),
59
+ );
60
+ if (env.XDG_CONFIG_HOME) {
61
+ roots.push(join(env.XDG_CONFIG_HOME, "mozilla", "firefox"));
62
+ }
63
+ if (existsSync(WINDOWS_USERS_ROOT)) {
64
+ let entries = [];
65
+ try {
66
+ entries = readdirSync(options.usersRoot ?? WINDOWS_USERS_ROOT, { withFileTypes: true });
67
+ } catch {
68
+ entries = [];
69
+ }
70
+ for (const entry of entries) {
71
+ if (!entry.isDirectory()) continue;
72
+ if (["Default", "Default User", "All Users", "Public"].includes(entry.name)) continue;
73
+ roots.push(join(options.usersRoot ?? WINDOWS_USERS_ROOT, entry.name, "AppData", "Roaming", "Mozilla", "Firefox"));
74
+ }
75
+ }
76
+ }
77
+ return roots.filter((root) => existsSync(join(root, "profiles.ini")));
78
+ }
79
+
80
+ /**
81
+ * Parse `profiles.ini` for profile directories.
82
+ *
83
+ * @param {string} root
84
+ * @returns {Array<{ label: string, dir: string, isDefault: boolean }>}
85
+ */
86
+ export function parseFirefoxProfiles(root) {
87
+ let text;
88
+ try {
89
+ text = readFileSync(join(root, "profiles.ini"), "utf-8");
90
+ } catch {
91
+ return [];
92
+ }
93
+
94
+ /** @type {Array<{ label: string, dir: string, isDefault: boolean, relative: boolean }>} */
95
+ const profiles = [];
96
+ let current = null;
97
+ for (const rawLine of text.split("\n")) {
98
+ const line = rawLine.trim();
99
+ if (line.startsWith("[")) {
100
+ if (current) profiles.push(current);
101
+ current = { label: line.replace(/[[\]]/g, ""), dir: "", isDefault: false, relative: true };
102
+ continue;
103
+ }
104
+ if (!current) continue;
105
+ const equals = line.indexOf("=");
106
+ if (equals <= 0) continue;
107
+ const key = line.slice(0, equals).trim();
108
+ const value = line.slice(equals + 1).trim();
109
+ if (key === "Path") current.dir = value;
110
+ if (key === "IsRelative") current.relative = value !== "0";
111
+ if (key === "Default") current.isDefault = value === "1";
112
+ }
113
+ if (current) profiles.push(current);
114
+
115
+ return profiles
116
+ .filter((profile) => profile.dir)
117
+ .map((profile) => ({
118
+ label: profile.label,
119
+ isDefault: profile.isDefault,
120
+ dir: profile.relative ? join(root, profile.dir) : profile.dir,
121
+ }))
122
+ .filter((profile) => existsSync(join(profile.dir, "cookies.sqlite")));
123
+ }
124
+
125
+ /**
126
+ * Every readable cookie store, most readable first.
127
+ *
128
+ * @param {{ home?: string, usersRoot?: string, platform?: string, env?: Record<string, string | undefined>, secretTool?: (args: string[]) => string | null }} [options]
129
+ * @returns {CookieStore[]}
130
+ */
131
+ export function discoverCookieStores(options = {}) {
132
+ /** @type {CookieStore[]} */
133
+ const stores = [];
134
+ for (const root of firefoxRoots(options)) {
135
+ for (const profile of parseFirefoxProfiles(root)) {
136
+ let platformPrefix = "linux";
137
+ if (root.includes("Application Support")) platformPrefix = "mac";
138
+ else if (root.endsWith("Firefox")) platformPrefix = "windows";
139
+ stores.push({
140
+ browser: "firefox",
141
+ path: join(profile.dir, "cookies.sqlite"),
142
+ profile: `${platformPrefix}:${profile.label}${profile.isDefault ? " (default)" : ""}`,
143
+ readability: "plaintext",
144
+ });
145
+ }
146
+ }
147
+
148
+ const platform = options.platform ?? process.platform;
149
+ if (platform !== "win32" && platform !== "darwin") {
150
+ for (const store of linuxChromiumStores(options)) stores.push(store);
151
+ }
152
+
153
+ // Chromium on Windows encrypts values with DPAPI; surface it so the user gets
154
+ // an explanation instead of a silent failure.
155
+ for (const userRoot of windowsChromiumRoots(options)) {
156
+ for (const browserName of ["Google/Chrome", "Microsoft/Edge", "BraveSoftware/Brave-Browser"]) {
157
+ const base = join(userRoot, browserName, "User Data");
158
+ if (!existsSync(base)) continue;
159
+ for (const profile of ["Default", "Profile 1", "Profile 2"]) {
160
+ const db = join(base, profile, "Network", "Cookies");
161
+ if (!existsSync(db)) continue;
162
+ stores.push({
163
+ browser: "chromium",
164
+ path: db,
165
+ profile: `windows:${browserName.split("/").pop()}/${profile}`,
166
+ readability: "encrypted",
167
+ note: "Chromium on Windows encrypts cookies with DPAPI, which is not available from WSL. Use Firefox for opencode.ai.",
168
+ });
169
+ }
170
+ }
171
+ }
172
+
173
+ return stores;
174
+ }
175
+
176
+ const LINUX_CHROMIUM_BROWSERS = [
177
+ { directory: "google-chrome", label: "Google Chrome", app: "chrome" },
178
+ { directory: "google-chrome-beta", label: "Google Chrome Beta", app: "chrome" },
179
+ { directory: "google-chrome-unstable", label: "Google Chrome Unstable", app: "chrome" },
180
+ { directory: "chromium", label: "Chromium", app: "chromium" },
181
+ { directory: "microsoft-edge", label: "Microsoft Edge", app: "microsoft-edge" },
182
+ { directory: "microsoft-edge-dev", label: "Microsoft Edge Dev", app: "microsoft-edge" },
183
+ { directory: "BraveSoftware/Brave-Browser", label: "Brave", app: "brave" },
184
+ ];
185
+
186
+ /**
187
+ * @param {{ home?: string, env?: Record<string, string | undefined>, secretTool?: (args: string[]) => string | null }} [options]
188
+ * @returns {CookieStore[]}
189
+ */
190
+ function linuxChromiumStores(options = {}) {
191
+ const home = options.home ?? homedir();
192
+ const env = options.env ?? process.env;
193
+ const configRoot = env.XDG_CONFIG_HOME || join(home, ".config");
194
+ /** @type {CookieStore[]} */
195
+ const stores = [];
196
+ const seen = new Set();
197
+
198
+ for (const browser of LINUX_CHROMIUM_BROWSERS) {
199
+ const browserDir = join(configRoot, browser.directory);
200
+ if (!existsSync(browserDir)) continue;
201
+ const profiles = ["Default"];
202
+ try {
203
+ profiles.push(...readdirSync(browserDir, { withFileTypes: true })
204
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith("Profile "))
205
+ .map((entry) => entry.name));
206
+ } catch {
207
+ // A browser directory may disappear while discovery is running.
208
+ }
209
+
210
+ for (const profile of profiles) {
211
+ for (const relative of [join(profile, "Cookies"), join(profile, "Network", "Cookies")]) {
212
+ const dbPath = join(browserDir, relative);
213
+ if (seen.has(dbPath) || !existsSync(dbPath)) continue;
214
+ seen.add(dbPath);
215
+ const status = chromiumStoreReadability(dbPath, browser.app, options);
216
+ stores.push({
217
+ browser: "chromium",
218
+ path: dbPath,
219
+ profile: `linux:${browser.label}/${profile}`,
220
+ app: browser.app,
221
+ ...status,
222
+ });
223
+ }
224
+ }
225
+ }
226
+ return stores;
227
+ }
228
+
229
+ /**
230
+ * @param {{ usersRoot?: string, platform?: string }} [options]
231
+ * @returns {string[]}
232
+ */
233
+ function windowsChromiumRoots(options = {}) {
234
+ if ((options.platform ?? process.platform) === "win32") return [];
235
+ const usersRoot = options.usersRoot ?? WINDOWS_USERS_ROOT;
236
+ if (!existsSync(usersRoot)) return [];
237
+ /** @type {string[]} */
238
+ const roots = [];
239
+ try {
240
+ for (const entry of readdirSync(usersRoot, { withFileTypes: true })) {
241
+ if (!entry.isDirectory()) continue;
242
+ if (["Default", "Default User", "All Users", "Public"].includes(entry.name)) continue;
243
+ roots.push(join(usersRoot, entry.name, "AppData", "Local"));
244
+ }
245
+ } catch {
246
+ return [];
247
+ }
248
+ return roots;
249
+ }
250
+
251
+ const secretStoragePasswords = new Map();
252
+
253
+ /**
254
+ * Look up the Linux Chromium Safe Storage password without exposing it outside
255
+ * this process. Both common Secret Service attributes are attempted.
256
+ *
257
+ * @param {string} app
258
+ * @param {{ secretTool?: (args: string[]) => string | null }} [options]
259
+ * @returns {string | null}
260
+ */
261
+ function linuxChromiumPassword(app, options = {}) {
262
+ const candidates = [app];
263
+ if (app !== "chromium") candidates.push("chromium");
264
+ if (app !== "chrome") candidates.push("chrome");
265
+
266
+ if (typeof options.secretTool === "function") {
267
+ for (const candidate of candidates) {
268
+ for (const args of [
269
+ ["lookup", "application", candidate],
270
+ ["lookup", "service", `${candidate} Safe Storage`],
271
+ ]) {
272
+ try {
273
+ const value = options.secretTool(args)?.replace(/[\r\n]+$/, "");
274
+ if (value) return value;
275
+ } catch {
276
+ // Try the alternate Secret Service attribute.
277
+ }
278
+ }
279
+ }
280
+ return null;
281
+ }
282
+ if (secretStoragePasswords.has(app)) return secretStoragePasswords.get(app);
283
+
284
+ let password = null;
285
+ for (const candidate of candidates) {
286
+ for (const args of [
287
+ ["lookup", "application", candidate],
288
+ ["lookup", "service", `${candidate} Safe Storage`],
289
+ ]) {
290
+ try {
291
+ const value = execFileSync("secret-tool", args, {
292
+ encoding: "utf8",
293
+ stdio: ["ignore", "pipe", "ignore"],
294
+ timeout: 2000,
295
+ }).replace(/[\r\n]+$/, "");
296
+ if (value.length > 0) {
297
+ password = value;
298
+ break;
299
+ }
300
+ } catch {
301
+ // Missing secret-tool, locked keyrings and absent entries are expected.
302
+ }
303
+ }
304
+ if (password) break;
305
+ }
306
+ secretStoragePasswords.set(app, password);
307
+ return password;
308
+ }
309
+
310
+ /**
311
+ * Decrypt a Linux Chromium OSCrypt cookie value. The `password` option allows
312
+ * callers/tests to supply an already-resolved Safe Storage password.
313
+ *
314
+ * @param {Buffer | Uint8Array} encryptedValue
315
+ * @param {{ app?: string, password?: string | null, secretTool?: (args: string[]) => string | null }} [options]
316
+ * @returns {string | null}
317
+ */
318
+ export function decryptLinuxChromiumCookie(encryptedValue, options = {}) {
319
+ const encrypted = Buffer.from(encryptedValue);
320
+ const version = encrypted.subarray(0, 3).toString("ascii");
321
+ if ((version !== "v10" && version !== "v11") || encrypted.length <= 3) return null;
322
+
323
+ let password;
324
+ if (version === "v10") password = "peanuts";
325
+ else if (Object.hasOwn(options, "password")) password = options.password;
326
+ else password = linuxChromiumPassword(options.app ?? "chromium", options);
327
+ if (typeof password !== "string" || password.length === 0) return null;
328
+
329
+ try {
330
+ const key = pbkdf2Sync(password, "saltysalt", 1, 16, "sha1");
331
+ const decipher = createDecipheriv("aes-128-cbc", key, Buffer.alloc(16, 0x20));
332
+ const decrypted = Buffer.concat([decipher.update(encrypted.subarray(3)), decipher.final()]);
333
+ const payload = version === "v11" ? decrypted.subarray(32) : decrypted;
334
+ if (payload.length === 0) return null;
335
+ if (version === "v11") {
336
+ const knownPrefix = payload.subarray(0, 6).toString("ascii").startsWith("Fe26.2")
337
+ || payload.subarray(0, 3).toString("ascii") === "st_";
338
+ if (!knownPrefix && !isPrintableAscii(payload)) return null;
339
+ }
340
+ return payload.toString("utf8");
341
+ } catch {
342
+ return null;
343
+ }
344
+ }
345
+
346
+ /** @param {Buffer} bytes */
347
+ function isPrintableAscii(bytes) {
348
+ return bytes.length > 0 && bytes.every((byte) => byte >= 0x20 && byte <= 0x7e);
349
+ }
350
+
351
+ /**
352
+ * Decide whether the encrypted values in a Linux Chromium database are
353
+ * supported by this process. v10 uses the documented legacy password; v11
354
+ * requires a password from the browser's Secret Service entry.
355
+ *
356
+ * @param {string} dbPath
357
+ * @param {string} app
358
+ * @param {{ secretTool?: (args: string[]) => string | null }} [options]
359
+ * @returns {{ readability: "plaintext" | "encrypted", note?: string }}
360
+ */
361
+ function chromiumStoreReadability(dbPath, app, options = {}) {
362
+ const copy = withCopy(dbPath);
363
+ let database;
364
+ try {
365
+ database = new DatabaseSync(copy.db, { readOnly: true });
366
+ const rows = database.prepare("select encrypted_value from cookies where encrypted_value is not null and length(encrypted_value) > 0").all();
367
+ for (const row of rows) {
368
+ const encrypted = Buffer.from(row.encrypted_value);
369
+ const version = encrypted.subarray(0, 3).toString("ascii");
370
+ if (version === "v10") continue;
371
+ if (version === "v11" && linuxChromiumPassword(app, options)) continue;
372
+ return {
373
+ readability: "encrypted",
374
+ note: "Chromium OSCrypt cookies need a supported v10 value or an unlocked Secret Service Safe Storage entry for this browser.",
375
+ };
376
+ }
377
+ return { readability: "plaintext" };
378
+ } catch {
379
+ return {
380
+ readability: "encrypted",
381
+ note: "Chromium cookie database could not be inspected; its encrypted values may require the browser's Secret Service Safe Storage entry.",
382
+ };
383
+ } finally {
384
+ try { database?.close(); } catch {}
385
+ copy.cleanup();
386
+ }
387
+ }
388
+
389
+ const activeCookieCleanups = new Set();
390
+ if (typeof process !== "undefined" && typeof process.on === "function") {
391
+ const runCleanups = () => {
392
+ for (const fn of activeCookieCleanups) {
393
+ try { fn(); } catch {}
394
+ }
395
+ activeCookieCleanups.clear();
396
+ };
397
+ process.once("exit", runCleanups);
398
+ process.once("SIGINT", () => {
399
+ runCleanups();
400
+ process.exit(130);
401
+ });
402
+ process.once("SIGTERM", () => {
403
+ runCleanups();
404
+ process.exit(143);
405
+ });
406
+ }
407
+
408
+ /**
409
+ * Copy a SQLite database (with its journal) to a private temp dir so the
410
+ * browser's own file is never opened or locked by us.
411
+ *
412
+ * @param {string} dbPath
413
+ * @returns {{ dir: string, db: string, cleanup: () => void }}
414
+ */
415
+ function withCopy(dbPath) {
416
+ const dir = mkdtempSync(join(tmpdir(), "pi-quota-cookies-"));
417
+ const target = join(dir, "cookies.sqlite");
418
+ for (const suffix of ["", "-wal", "-shm"]) {
419
+ const source = `${dbPath}${suffix}`;
420
+ if (existsSync(source)) {
421
+ try {
422
+ copyFileSync(source, `${target}${suffix}`);
423
+ } catch {
424
+ // A locked sidecar is not fatal: the main database copy is usually enough.
425
+ }
426
+ }
427
+ }
428
+ const cleanupFn = () => {
429
+ activeCookieCleanups.delete(cleanupFn);
430
+ try {
431
+ rmSync(dir, { recursive: true, force: true });
432
+ } catch {
433
+ // Best effort.
434
+ }
435
+ };
436
+ activeCookieCleanups.add(cleanupFn);
437
+ return {
438
+ dir,
439
+ db: target,
440
+ cleanup: cleanupFn,
441
+ };
442
+ }
443
+
444
+ /**
445
+ * Query Firefox's `moz_cookies`.
446
+ *
447
+ * @param {string} dbPath
448
+ * @param {{ hostLike: string, name?: string }} query
449
+ * @returns {Array<{ host: string, name: string, value: string, path: string }>}
450
+ */
451
+ export function readFirefoxCookies(dbPath, query) {
452
+ const copy = withCopy(dbPath);
453
+ let database;
454
+ try {
455
+ try {
456
+ database = new DatabaseSync(copy.db, { readOnly: true });
457
+ } catch {
458
+ database = new DatabaseSync(copy.db);
459
+ }
460
+ const sql = query.name
461
+ ? "select host, name, value, path from moz_cookies where host like ? and name = ?"
462
+ : "select host, name, value, path from moz_cookies where host like ?";
463
+ const statement = database.prepare(sql);
464
+ const rows = query.name
465
+ ? statement.all(query.hostLike, query.name)
466
+ : statement.all(query.hostLike);
467
+ return /** @type {Array<{ host: string, name: string, value: string, path: string }>} */ (rows);
468
+ } catch {
469
+ return [];
470
+ } finally {
471
+ try {
472
+ database?.close();
473
+ } catch {
474
+ // Ignore.
475
+ }
476
+ copy.cleanup();
477
+ }
478
+ }
479
+
480
+ /**
481
+ * Query Chromium's `cookies` table from a private database copy.
482
+ *
483
+ * @param {string} dbPath
484
+ * @param {{ hostLike: string, name?: string }} query
485
+ * @returns {Array<{ host: string, name: string, value: string, encrypted_value: Buffer, path: string }>}
486
+ */
487
+ function readChromiumCookies(dbPath, query) {
488
+ const copy = withCopy(dbPath);
489
+ let database;
490
+ try {
491
+ database = new DatabaseSync(copy.db, { readOnly: true });
492
+ const sql = query.name
493
+ ? "select host_key as host, name, value, encrypted_value, path from cookies where host_key like ? and name = ?"
494
+ : "select host_key as host, name, value, encrypted_value, path from cookies where host_key like ?";
495
+ const rows = query.name
496
+ ? database.prepare(sql).all(query.hostLike, query.name)
497
+ : database.prepare(sql).all(query.hostLike);
498
+ return /** @type {Array<{ host: string, name: string, value: string, encrypted_value: Buffer, path: string }>} */ (rows);
499
+ } catch {
500
+ return [];
501
+ } finally {
502
+ try { database?.close(); } catch {}
503
+ copy.cleanup();
504
+ }
505
+ }
506
+
507
+ /**
508
+ * Find one cookie across every readable store.
509
+ *
510
+ * @param {{
511
+ * host: string,
512
+ * name: string,
513
+ * stores?: CookieStore[],
514
+ * home?: string,
515
+ * usersRoot?: string,
516
+ * platform?: string,
517
+ * secretTool?: (args: string[]) => string | null,
518
+ * }} options
519
+ * @returns {{
520
+ * found: boolean,
521
+ * value?: string,
522
+ * store?: CookieStore,
523
+ * candidates: number,
524
+ * encryptedOnly: boolean,
525
+ * }}
526
+ */
527
+ export function findCookie(options) {
528
+ const stores = options.stores ?? discoverCookieStores(options);
529
+ const readable = stores.filter((store) => store.readability === "plaintext");
530
+ let scanned = 0;
531
+
532
+ for (const store of readable) {
533
+ scanned += 1;
534
+ const query = { hostLike: `%${options.host}%`, name: options.name };
535
+ const rows = store.browser === "firefox"
536
+ ? readFirefoxCookies(store.path, query)
537
+ : readChromiumCookies(store.path, query);
538
+ for (const row of rows) {
539
+ if (row.value && row.value.length > 0) {
540
+ return { found: true, value: row.value, store, candidates: scanned, encryptedOnly: false };
541
+ }
542
+ if (store.browser === "chromium" && row.encrypted_value?.length > 0) {
543
+ const value = decryptLinuxChromiumCookie(row.encrypted_value, {
544
+ app: store.app ?? "chromium",
545
+ secretTool: options.secretTool,
546
+ });
547
+ if (value) return { found: true, value, store, candidates: scanned, encryptedOnly: false };
548
+ }
549
+ }
550
+ }
551
+
552
+ return {
553
+ found: false,
554
+ candidates: scanned,
555
+ encryptedOnly: scanned === 0 && stores.length > 0,
556
+ };
557
+ }