tau-coding-agent 0.1.3 → 0.1.5

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 (60) hide show
  1. package/README.md +1 -1
  2. package/extensions/answer.ts +49 -43
  3. package/extensions/branch-term.ts +11 -16
  4. package/extensions/btw.ts +8 -8
  5. package/extensions/ghostty.ts +65 -12
  6. package/extensions/git-diff-stats.ts +1 -1
  7. package/extensions/git-pr-status.ts +1 -1
  8. package/extensions/insights.ts +6 -5
  9. package/extensions/loop.ts +39 -11
  10. package/extensions/memory.ts +6 -5
  11. package/extensions/notify.ts +25 -6
  12. package/extensions/openai-fast.ts +6 -4
  13. package/extensions/openai-verbosity.ts +6 -4
  14. package/extensions/review/fix.ts +239 -0
  15. package/extensions/review/git.ts +517 -0
  16. package/extensions/review/index.ts +232 -0
  17. package/extensions/review/message-queue.ts +249 -0
  18. package/extensions/review/models.ts +589 -0
  19. package/extensions/review/prompts.ts +337 -0
  20. package/extensions/review/renderers/inline.ts +126 -0
  21. package/extensions/review/request.ts +504 -0
  22. package/extensions/review/review.ts +1305 -0
  23. package/extensions/review/runner.ts +522 -0
  24. package/extensions/review/runtime.ts +275 -0
  25. package/extensions/review/schema.ts +160 -0
  26. package/extensions/review/submit-review-tool.ts +66 -0
  27. package/extensions/review/submit-triage-tool.ts +59 -0
  28. package/extensions/review/triage.ts +746 -0
  29. package/extensions/sandbox/index.ts +428 -103
  30. package/extensions/{interlude.ts → stash.ts} +13 -11
  31. package/extensions/tool-display-mode.ts +173 -17
  32. package/extensions/usage/anthropic.ts +1 -1
  33. package/extensions/usage/github-copilot.ts +1 -1
  34. package/extensions/usage/index.ts +9 -5
  35. package/extensions/usage/minimax.ts +1 -1
  36. package/extensions/usage/openai-codex.ts +1 -1
  37. package/extensions/usage/openrouter.ts +1 -1
  38. package/extensions/usage/providers.ts +0 -2
  39. package/extensions/usage/types.ts +1 -2
  40. package/extensions/usage/zai.ts +1 -1
  41. package/extensions/websearch/README.md +1 -1
  42. package/extensions/websearch/browser/chromium.ts +42 -31
  43. package/extensions/websearch/browser/discovery.ts +11 -9
  44. package/extensions/websearch/browser/firefox.ts +10 -7
  45. package/extensions/websearch/browser/fs.ts +10 -0
  46. package/extensions/websearch/browser/sqlite.ts +22 -15
  47. package/extensions/websearch/config.ts +2 -2
  48. package/extensions/websearch/index.ts +9 -9
  49. package/extensions/websearch/providers/anthropic.pi.ts +1 -1
  50. package/extensions/websearch/providers/gemini.pi.ts +1 -1
  51. package/extensions/websearch/providers/openai-codex.pi.ts +1 -1
  52. package/extensions/websearch/providers/pi-model.shared.ts +2 -2
  53. package/extensions/worktree.ts +36 -36
  54. package/package.json +7 -7
  55. package/skills/git-clean-history/SKILL.md +1 -0
  56. package/skills/git-commit/SKILL.md +2 -2
  57. package/skills/oracle/scripts/oracle +28 -12
  58. package/themes/tau-dark.json +2 -1
  59. package/extensions/review.ts +0 -4347
  60. package/extensions/usage/google-gemini-cli.ts +0 -232
@@ -1,16 +1,19 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
3
+ import {
4
+ getAgentDir,
5
+ type ExtensionAPI,
6
+ type ExtensionContext,
7
+ } from "@earendil-works/pi-coding-agent";
5
8
 
6
9
  const DEFAULT_SHORTCUTS = ["alt+x"] as const;
7
- const STATUS_KEY = "interlude";
8
- const KEYBINDINGS_PATH = path.join(os.homedir(), ".pi", "agent", "keybindings.json");
10
+ const STATUS_KEY = "stash";
11
+ const KEYBINDINGS_PATH = path.join(getAgentDir(), "keybindings.json");
9
12
 
10
13
  type ShortcutConfig = string | string[] | undefined;
11
14
 
12
15
  type KeybindingsConfig = {
13
- interlude?: ShortcutConfig;
16
+ stash?: ShortcutConfig;
14
17
  [key: string]: unknown;
15
18
  };
16
19
 
@@ -37,9 +40,9 @@ function normalizeShortcuts(value: ShortcutConfig): string[] {
37
40
  return normalized.length > 0 ? [...new Set(normalized)] : [...DEFAULT_SHORTCUTS];
38
41
  }
39
42
 
40
- export default function interludeExtension(pi: ExtensionAPI): void {
43
+ export default function stashExtension(pi: ExtensionAPI): void {
41
44
  const keybindings = readKeybindings(KEYBINDINGS_PATH);
42
- const shortcuts = normalizeShortcuts(keybindings.interlude);
45
+ const shortcuts = normalizeShortcuts(keybindings.stash);
43
46
 
44
47
  let stashedDraft: string | null = null;
45
48
  let armed = false;
@@ -52,7 +55,7 @@ export default function interludeExtension(pi: ExtensionAPI): void {
52
55
  if (!ctx.hasUI) return;
53
56
 
54
57
  if (armed && stashedDraft !== null) {
55
- ctx.ui.setStatus(STATUS_KEY, `interlude (${shortcutLabel()})`);
58
+ ctx.ui.setStatus(STATUS_KEY, `stash (${shortcutLabel()})`);
56
59
  return;
57
60
  }
58
61
 
@@ -80,7 +83,7 @@ export default function interludeExtension(pi: ExtensionAPI): void {
80
83
 
81
84
  if (armed && stashedDraft !== null) {
82
85
  restoreDraft(ctx);
83
- ctx.ui.notify("Interlude draft restored", "info");
86
+ ctx.ui.notify("Stashed draft restored", "info");
84
87
  return;
85
88
  }
86
89
 
@@ -101,8 +104,7 @@ export default function interludeExtension(pi: ExtensionAPI): void {
101
104
 
102
105
  for (const shortcut of shortcuts) {
103
106
  pi.registerShortcut(shortcut as never, {
104
- description:
105
- "Stash the current message draft, send one interlude message, then restore the draft",
107
+ description: "Stash the current message draft, send one message, then restore it",
106
108
  handler: async (ctx) => {
107
109
  stashOrRestore(ctx);
108
110
  },
@@ -1,7 +1,8 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
3
  import {
4
+ getAgentDir,
5
+ type AppKeybinding,
5
6
  CustomEditor,
6
7
  createBashToolDefinition,
7
8
  createFindToolDefinition,
@@ -16,8 +17,15 @@ import {
16
17
  type ToolDefinition,
17
18
  type ToolInfo,
18
19
  type ToolRenderResultOptions,
19
- } from "@mariozechner/pi-coding-agent";
20
- import { Container, Text, type Component, type EditorTheme, type TUI } from "@mariozechner/pi-tui";
20
+ } from "@earendil-works/pi-coding-agent";
21
+ import {
22
+ Container,
23
+ Text,
24
+ type AutocompleteProvider,
25
+ type Component,
26
+ type EditorComponent,
27
+ type Focusable,
28
+ } from "@earendil-works/pi-tui";
21
29
 
22
30
  // --- Constants ---
23
31
 
@@ -46,11 +54,22 @@ type ToolName = keyof typeof TOOL_FACTORIES;
46
54
  type AnyToolDefinition = ToolDefinition<any, any, any>;
47
55
  type AnyToolRenderContext = Parameters<NonNullable<AnyToolDefinition["renderResult"]>>[3];
48
56
  type JsonObject = Record<string, unknown>;
57
+ type EditorFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorComponent"]>>;
49
58
 
50
59
  type ToolDisplayModeConfig = {
51
60
  mode: Mode;
52
61
  };
53
62
 
63
+ type CustomEditorLike = EditorComponent &
64
+ Partial<Focusable> & {
65
+ actionHandlers?: Map<AppKeybinding, () => void>;
66
+ onEscape?: () => void;
67
+ onCtrlD?: () => void;
68
+ onPasteImage?: () => void;
69
+ onExtensionShortcut?: (data: string) => boolean;
70
+ isShowingAutocomplete?: () => boolean;
71
+ };
72
+
54
73
  // --- Config ---
55
74
 
56
75
  function emptyConfig(): ToolDisplayModeConfig {
@@ -58,8 +77,7 @@ function emptyConfig(): ToolDisplayModeConfig {
58
77
  }
59
78
 
60
79
  function getConfigPath(): string {
61
- const agentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
62
- return path.join(agentDir, CONFIG_FILE);
80
+ return path.join(getAgentDir(), CONFIG_FILE);
63
81
  }
64
82
 
65
83
  function isObject(value: unknown): value is JsonObject {
@@ -242,11 +260,11 @@ function renderMinimalResult(
242
260
  break;
243
261
  }
244
262
 
245
- return renderMinimalText(theme, "muted", `→ ${summary}`);
263
+ return renderMinimalText(theme, "muted", `↳ ${summary}`);
246
264
  }
247
265
 
248
266
  function renderMinimalText(theme: Theme, color: "error" | "muted", text: string): Text {
249
- return new Text(theme.fg(color, `\n${text}`), 0, 0);
267
+ return new Text(theme.fg(color, text), 0, 0);
250
268
  }
251
269
 
252
270
  function bashSummary(result: AgentToolResult<any>): string {
@@ -328,29 +346,152 @@ function emptyComponent(): Container {
328
346
 
329
347
  // --- Editor ---
330
348
 
331
- class ToolDisplayEditor extends CustomEditor {
349
+ class ToolDisplayEditor implements EditorComponent, Focusable {
350
+ readonly actionHandlers: Map<AppKeybinding, () => void>;
351
+ private readonly customBase: CustomEditorLike;
352
+ private fallbackFocused = false;
353
+ private fallbackOnEscape?: () => void;
354
+ private fallbackOnCtrlD?: () => void;
355
+ private fallbackOnPasteImage?: () => void;
356
+ private fallbackOnExtensionShortcut?: (data: string) => boolean;
357
+
332
358
  constructor(
333
- tui: TUI,
334
- theme: EditorTheme,
359
+ private readonly base: EditorComponent,
335
360
  private readonly appKeybindings: KeybindingsManager,
336
361
  private readonly cycleMode: () => void,
337
362
  ) {
338
- super(tui, theme, appKeybindings);
363
+ this.customBase = base as CustomEditorLike;
364
+ this.actionHandlers = this.customBase.actionHandlers ?? new Map();
365
+ }
366
+
367
+ get focused(): boolean {
368
+ return this.customBase.focused ?? this.fallbackFocused;
369
+ }
370
+
371
+ set focused(value: boolean) {
372
+ this.fallbackFocused = value;
373
+ if ("focused" in this.customBase) this.customBase.focused = value;
374
+ }
375
+
376
+ get onSubmit(): ((text: string) => void) | undefined {
377
+ return this.base.onSubmit;
378
+ }
379
+
380
+ set onSubmit(handler: ((text: string) => void) | undefined) {
381
+ this.base.onSubmit = handler;
382
+ }
383
+
384
+ get onChange(): ((text: string) => void) | undefined {
385
+ return this.base.onChange;
386
+ }
387
+
388
+ set onChange(handler: ((text: string) => void) | undefined) {
389
+ this.base.onChange = handler;
390
+ }
391
+
392
+ get wantsKeyRelease(): boolean | undefined {
393
+ return this.base.wantsKeyRelease;
394
+ }
395
+
396
+ get borderColor(): ((str: string) => string) | undefined {
397
+ return this.base.borderColor;
398
+ }
399
+
400
+ set borderColor(handler: ((str: string) => string) | undefined) {
401
+ this.base.borderColor = handler;
402
+ }
403
+
404
+ get onEscape(): (() => void) | undefined {
405
+ return this.customBase.onEscape ?? this.fallbackOnEscape;
406
+ }
407
+
408
+ set onEscape(handler: (() => void) | undefined) {
409
+ this.fallbackOnEscape = handler;
410
+ if ("onEscape" in this.customBase) this.customBase.onEscape = handler;
411
+ }
412
+
413
+ get onCtrlD(): (() => void) | undefined {
414
+ return this.customBase.onCtrlD ?? this.fallbackOnCtrlD;
415
+ }
416
+
417
+ set onCtrlD(handler: (() => void) | undefined) {
418
+ this.fallbackOnCtrlD = handler;
419
+ if ("onCtrlD" in this.customBase) this.customBase.onCtrlD = handler;
420
+ }
421
+
422
+ get onPasteImage(): (() => void) | undefined {
423
+ return this.customBase.onPasteImage ?? this.fallbackOnPasteImage;
339
424
  }
340
425
 
341
- override handleInput(data: string): void {
426
+ set onPasteImage(handler: (() => void) | undefined) {
427
+ this.fallbackOnPasteImage = handler;
428
+ if ("onPasteImage" in this.customBase) this.customBase.onPasteImage = handler;
429
+ }
430
+
431
+ get onExtensionShortcut(): ((data: string) => boolean) | undefined {
432
+ return this.customBase.onExtensionShortcut ?? this.fallbackOnExtensionShortcut;
433
+ }
434
+
435
+ set onExtensionShortcut(handler: ((data: string) => boolean) | undefined) {
436
+ this.fallbackOnExtensionShortcut = handler;
437
+ if ("onExtensionShortcut" in this.customBase) this.customBase.onExtensionShortcut = handler;
438
+ }
439
+
440
+ getText(): string {
441
+ return this.base.getText();
442
+ }
443
+
444
+ setText(text: string): void {
445
+ this.base.setText(text);
446
+ }
447
+
448
+ addToHistory(text: string): void {
449
+ this.base.addToHistory?.(text);
450
+ }
451
+
452
+ insertTextAtCursor(text: string): void {
453
+ this.base.insertTextAtCursor?.(text);
454
+ }
455
+
456
+ getExpandedText(): string {
457
+ return this.base.getExpandedText?.() ?? this.base.getText();
458
+ }
459
+
460
+ setAutocompleteProvider(provider: AutocompleteProvider): void {
461
+ this.base.setAutocompleteProvider?.(provider);
462
+ }
463
+
464
+ setPaddingX(padding: number): void {
465
+ this.base.setPaddingX?.(padding);
466
+ }
467
+
468
+ setAutocompleteMaxVisible(maxVisible: number): void {
469
+ this.base.setAutocompleteMaxVisible?.(maxVisible);
470
+ }
471
+
472
+ render(width: number): string[] {
473
+ return this.base.render(width);
474
+ }
475
+
476
+ invalidate(): void {
477
+ this.base.invalidate();
478
+ }
479
+
480
+ handleInput(data: string): void {
342
481
  if (this.appKeybindings.matches(data, "app.tools.expand")) {
343
482
  this.cycleMode();
344
483
  return;
345
484
  }
346
485
 
347
- super.handleInput(data);
486
+ this.base.handleInput(data);
348
487
  }
349
488
  }
350
489
 
351
490
  export default function toolDisplayModeExtension(pi: ExtensionAPI): void {
352
491
  let mode: Mode = DEFAULT_MODE;
353
492
  let registeredToolRenderers = false;
493
+ let installedEditorFactory: EditorFactory | undefined;
494
+ let previousEditorFactory: EditorFactory | undefined;
354
495
  const toolCache = new Map<string, Partial<Record<ToolName, AnyToolDefinition>>>();
355
496
 
356
497
  const setMode = (ctx: ExtensionContext, next: Mode): void => {
@@ -379,9 +520,24 @@ export default function toolDisplayModeExtension(pi: ExtensionAPI): void {
379
520
  applyMode(ctx, mode);
380
521
  if (!ctx.hasUI) return;
381
522
 
382
- ctx.ui.setEditorComponent(
383
- (tui, theme, keybindings) =>
384
- new ToolDisplayEditor(tui, theme, keybindings, () => setMode(ctx, nextMode(mode))),
385
- );
523
+ if (ctx.ui.getEditorComponent() === installedEditorFactory) return;
524
+
525
+ previousEditorFactory = ctx.ui.getEditorComponent();
526
+ installedEditorFactory = (tui, theme, keybindings) => {
527
+ const baseEditor =
528
+ previousEditorFactory?.(tui, theme, keybindings) ??
529
+ new CustomEditor(tui, theme, keybindings);
530
+ return new ToolDisplayEditor(baseEditor, keybindings, () => setMode(ctx, nextMode(mode)));
531
+ };
532
+ ctx.ui.setEditorComponent(installedEditorFactory);
533
+ });
534
+
535
+ pi.on("session_shutdown", async (_event, ctx) => {
536
+ if (ctx.hasUI && ctx.ui.getEditorComponent() === installedEditorFactory) {
537
+ ctx.ui.setEditorComponent(previousEditorFactory);
538
+ }
539
+
540
+ installedEditorFactory = undefined;
541
+ previousEditorFactory = undefined;
386
542
  });
387
543
  }
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  clampPercent,
4
4
  formatCurrency,
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  clampPercent,
4
4
  formatCount,
@@ -1,5 +1,9 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import { BorderedLoader } from "@mariozechner/pi-coding-agent";
1
+ import {
2
+ BorderedLoader,
3
+ getAgentDir,
4
+ type ExtensionAPI,
5
+ type ExtensionContext,
6
+ } from "@earendil-works/pi-coding-agent";
3
7
  import {
4
8
  Key,
5
9
  matchesKey,
@@ -7,7 +11,7 @@ import {
7
11
  type TUI,
8
12
  truncateToWidth,
9
13
  visibleWidth,
10
- } from "@mariozechner/pi-tui";
14
+ } from "@earendil-works/pi-tui";
11
15
  import { createReadStream, type Dirent } from "node:fs";
12
16
  import fs from "node:fs/promises";
13
17
  import os from "node:os";
@@ -31,7 +35,7 @@ import type {
31
35
  } from "./types.js";
32
36
 
33
37
  const ALL_TAB_ID = "all";
34
- const SESSION_ROOT = path.join(os.homedir(), ".pi", "agent", "sessions");
38
+ const SESSION_ROOT = path.join(getAgentDir(), "sessions");
35
39
  const RANGE_DAYS = [7, 30, 90] as const;
36
40
  const FILE_PARSE_CONCURRENCY = 16;
37
41
 
@@ -2205,7 +2209,7 @@ export default function usageBreakdownExtension(pi: ExtensionAPI) {
2205
2209
  handler: async (_args, ctx: ExtensionContext) => {
2206
2210
  const liveUsageAvailability = await getLiveUsageAvailabilityMap(ctx);
2207
2211
 
2208
- if (!ctx.hasUI) {
2212
+ if (ctx.mode !== "tui") {
2209
2213
  const [data, liveUsage] = await Promise.all([
2210
2214
  computeBreakdown(liveUsageAvailability, undefined),
2211
2215
  fetchLiveUsage(SUPPORTED_PROVIDERS, liveUsageAvailability, ctx, undefined),
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  clampPercent,
4
4
  formatCount,
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  clampPercent,
4
4
  formatCurrency,
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  formatCurrency,
4
4
  HttpError,
@@ -1,6 +1,5 @@
1
1
  import anthropicProvider from "./anthropic.js";
2
2
  import githubCopilotProvider from "./github-copilot.js";
3
- import googleGeminiCliProvider from "./google-gemini-cli.js";
4
3
  import minimaxProvider from "./minimax.js";
5
4
  import openAICodexProvider from "./openai-codex.js";
6
5
  import openRouterProvider from "./openrouter.js";
@@ -11,7 +10,6 @@ export const SUPPORTED_PROVIDERS: readonly UsageProviderDefinition[] = [
11
10
  openAICodexProvider,
12
11
  anthropicProvider,
13
12
  githubCopilotProvider,
14
- googleGeminiCliProvider,
15
13
  openRouterProvider,
16
14
  zaiProvider,
17
15
  minimaxProvider,
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  export interface RGB {
4
4
  r: number;
@@ -10,7 +10,6 @@ export type SupportedProviderId =
10
10
  | "openai-codex"
11
11
  | "anthropic"
12
12
  | "github-copilot"
13
- | "google-gemini-cli"
14
13
  | "openrouter"
15
14
  | "zai"
16
15
  | "minimax";
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  clampPercent,
4
4
  formatCount,
@@ -16,7 +16,7 @@ Current routes:
16
16
 
17
17
  Optional config file:
18
18
 
19
- - `~/.pi/websearch.json`
19
+ - `~/.pi/agent/websearch.json`
20
20
 
21
21
  Example:
22
22
 
@@ -1,11 +1,13 @@
1
- import { execFileSync } from "node:child_process";
2
- import { createDecipheriv, createHash, pbkdf2Sync } from "node:crypto";
3
- import { existsSync, readdirSync } from "node:fs";
1
+ import { execFile } from "node:child_process";
2
+ import { createDecipheriv, createHash, pbkdf2 } from "node:crypto";
3
+ import { readdir } from "node:fs/promises";
4
4
  import { DatabaseSync } from "node:sqlite";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
+ import { promisify } from "node:util";
7
8
 
8
9
  import type { BrowserCookie, BrowserProfile } from "../types.js";
10
+ import { pathExists } from "./fs.js";
9
11
  import { withSqliteSnapshot } from "./sqlite.js";
10
12
 
11
13
  const MACOS_BROWSER_CONFIGS: ChromiumBrowserConfig[] = [
@@ -72,6 +74,8 @@ interface ChromiumBrowserConfig {
72
74
  secretToolApp?: string;
73
75
  }
74
76
 
77
+ const execFileAsync = promisify(execFile);
78
+ const pbkdf2Async = promisify(pbkdf2);
75
79
  const passwordCache = new Map<string, string | null>();
76
80
 
77
81
  function browserConfigs(): ChromiumBrowserConfig[] {
@@ -80,13 +84,15 @@ function browserConfigs(): ChromiumBrowserConfig[] {
80
84
  return [];
81
85
  }
82
86
 
83
- export function discoverChromiumProfiles(preferredProfileName?: string): BrowserProfile[] {
87
+ export async function discoverChromiumProfiles(
88
+ preferredProfileName?: string,
89
+ ): Promise<BrowserProfile[]> {
84
90
  const profiles: BrowserProfile[] = [];
85
91
 
86
92
  for (const config of browserConfigs()) {
87
- if (!existsSync(config.baseDir)) continue;
93
+ if (!(await pathExists(config.baseDir))) continue;
88
94
 
89
- const candidates = ["Default", ...discoverProfileDirectories(config.baseDir)];
95
+ const candidates = ["Default", ...(await discoverProfileDirectories(config.baseDir))];
90
96
  const seen = new Set<string>();
91
97
  for (const directoryName of candidates) {
92
98
  if (!directoryName || seen.has(directoryName)) continue;
@@ -94,7 +100,7 @@ export function discoverChromiumProfiles(preferredProfileName?: string): Browser
94
100
 
95
101
  const profilePath = path.join(config.baseDir, directoryName);
96
102
  const cookiesPath = path.join(profilePath, "Cookies");
97
- if (!existsSync(cookiesPath)) continue;
103
+ if (!(await pathExists(cookiesPath))) continue;
98
104
 
99
105
  profiles.push({
100
106
  family: "chromium",
@@ -136,9 +142,9 @@ function isPreferredChromiumProfile(
136
142
  return profile.profileName === preferredProfileName;
137
143
  }
138
144
 
139
- function discoverProfileDirectories(baseDir: string): string[] {
145
+ async function discoverProfileDirectories(baseDir: string): Promise<string[]> {
140
146
  try {
141
- return readdirSync(baseDir, { withFileTypes: true })
147
+ return (await readdir(baseDir, { withFileTypes: true }))
142
148
  .filter((entry) => entry.isDirectory())
143
149
  .map((entry) => entry.name)
144
150
  .filter((name) => name === "Default" || /^Profile\s+\d+$/.test(name));
@@ -147,27 +153,28 @@ function discoverProfileDirectories(baseDir: string): string[] {
147
153
  }
148
154
  }
149
155
 
150
- export function loadChromiumCookies(profile: BrowserProfile): BrowserCookie[] {
156
+ export async function loadChromiumCookies(profile: BrowserProfile): Promise<BrowserCookie[]> {
151
157
  const browserConfig = browserConfigs().find((config) =>
152
158
  profile.profilePath.startsWith(config.baseDir),
153
159
  );
154
160
  if (!browserConfig) return [];
155
161
 
156
- const password = readBrowserPassword(browserConfig);
162
+ const cookiesPath = path.join(profile.profilePath, "Cookies");
163
+ if (!(await pathExists(cookiesPath))) return [];
164
+
165
+ const password = await readBrowserPassword(browserConfig);
157
166
  if (!password) return [];
158
167
 
159
- const keyMaterial = pbkdf2Sync(
168
+ const keyMaterial = await pbkdf2Async(
160
169
  password,
161
170
  "saltysalt",
162
171
  process.platform === "darwin" ? 1003 : 1,
163
172
  16,
164
173
  "sha1",
165
174
  );
166
- const cookiesPath = path.join(profile.profilePath, "Cookies");
167
- if (!existsSync(cookiesPath)) return [];
168
175
 
169
176
  try {
170
- return withSqliteSnapshot(cookiesPath, "websearch-chromium", (tempDbPath) => {
177
+ return await withSqliteSnapshot(cookiesPath, "websearch-chromium", (tempDbPath) => {
171
178
  const db = new DatabaseSync(tempDbPath, { readOnly: true });
172
179
  try {
173
180
  const rows = db
@@ -277,7 +284,7 @@ function stripHostKeyDigest(buffer: Buffer, hostKey: string | null): Buffer {
277
284
  return buffer.subarray(0, 32).equals(digest) ? buffer.subarray(32) : buffer;
278
285
  }
279
286
 
280
- function readBrowserPassword(config: ChromiumBrowserConfig): string | null {
287
+ async function readBrowserPassword(config: ChromiumBrowserConfig): Promise<string | null> {
281
288
  const cacheKey = `${config.name}:${config.baseDir}`;
282
289
  if (passwordCache.has(cacheKey)) {
283
290
  return passwordCache.get(cacheKey) ?? null;
@@ -286,9 +293,13 @@ function readBrowserPassword(config: ChromiumBrowserConfig): string | null {
286
293
  let password: string | null = null;
287
294
  if (process.platform === "darwin") {
288
295
  if (!config.keychainAccount || !config.keychainService) return null;
289
- password = readMacKeychainPassword(config.name, config.keychainAccount, config.keychainService);
296
+ password = await readMacKeychainPassword(
297
+ config.name,
298
+ config.keychainAccount,
299
+ config.keychainService,
300
+ );
290
301
  } else if (process.platform === "linux") {
291
- password = readLinuxPassword(config.secretToolApp);
302
+ password = await readLinuxPassword(config.secretToolApp);
292
303
  }
293
304
 
294
305
  if (password) {
@@ -297,33 +308,33 @@ function readBrowserPassword(config: ChromiumBrowserConfig): string | null {
297
308
  return password;
298
309
  }
299
310
 
300
- function readMacKeychainPassword(
311
+ async function readMacKeychainPassword(
301
312
  browserName: string,
302
313
  account: string,
303
314
  service: string,
304
- ): string | null {
315
+ ): Promise<string | null> {
305
316
  try {
306
- return (
307
- execFileSync("security", ["find-generic-password", "-w", "-a", account, "-s", service], {
308
- encoding: "utf8",
309
- stdio: ["ignore", "pipe", "ignore"],
310
- }).trim() || null
317
+ const { stdout } = await execFileAsync(
318
+ "security",
319
+ ["find-generic-password", "-w", "-a", account, "-s", service],
320
+ { encoding: "utf8" },
311
321
  );
322
+ return stdout.trim() || null;
312
323
  } catch {
313
324
  throw new Error(`Could not read ${browserName} cookie decryption password from Keychain.`);
314
325
  }
315
326
  }
316
327
 
317
- function readLinuxPassword(secretToolApp?: string): string | null {
328
+ async function readLinuxPassword(secretToolApp?: string): Promise<string | null> {
318
329
  if (!secretToolApp) return "peanuts";
319
330
 
320
331
  try {
321
- return (
322
- execFileSync("secret-tool", ["lookup", "application", secretToolApp], {
323
- encoding: "utf8",
324
- stdio: ["ignore", "pipe", "ignore"],
325
- }).trim() || "peanuts"
332
+ const { stdout } = await execFileAsync(
333
+ "secret-tool",
334
+ ["lookup", "application", secretToolApp],
335
+ { encoding: "utf8" },
326
336
  );
337
+ return stdout.trim() || "peanuts";
327
338
  } catch {
328
339
  return "peanuts";
329
340
  }
@@ -11,32 +11,34 @@ import type {
11
11
  const COOKIE_CACHE_TTL_MS = 30_000;
12
12
  const cookieCache = new Map<string, { cookies: BrowserCookie[]; expiresAt: number }>();
13
13
 
14
- export function discoverProfiles(
14
+ export async function discoverProfiles(
15
15
  browserFamilies: WebsearchBrowserFamily[],
16
16
  preferredProfiles: WebsearchConfig["profiles"],
17
- ): BrowserProfile[] {
17
+ ): Promise<BrowserProfile[]> {
18
18
  const profiles: BrowserProfile[] = [];
19
19
 
20
20
  for (const family of browserFamilies) {
21
21
  if (family === "firefox") {
22
- profiles.push(...discoverFirefoxProfiles(preferredProfiles.firefox));
22
+ profiles.push(...(await discoverFirefoxProfiles(preferredProfiles.firefox)));
23
23
  continue;
24
24
  }
25
25
 
26
- profiles.push(...discoverChromiumProfiles(preferredProfiles.chromium));
26
+ profiles.push(...(await discoverChromiumProfiles(preferredProfiles.chromium)));
27
27
  }
28
28
 
29
29
  return profiles;
30
30
  }
31
31
 
32
- export function loadCookies(profile: BrowserProfile): BrowserCookie[] {
32
+ export async function loadCookies(profile: BrowserProfile): Promise<BrowserCookie[]> {
33
33
  const cacheKey = `${profile.family}:${profile.profilePath}`;
34
34
  const cached = cookieCache.get(cacheKey);
35
35
  if (cached && cached.expiresAt > Date.now()) return cached.cookies;
36
36
  if (cached) cookieCache.delete(cacheKey);
37
37
 
38
38
  const cookies =
39
- profile.family === "firefox" ? loadFirefoxCookies(profile) : loadChromiumCookies(profile);
39
+ profile.family === "firefox"
40
+ ? await loadFirefoxCookies(profile)
41
+ : await loadChromiumCookies(profile);
40
42
 
41
43
  if (cookies.length > 0) {
42
44
  cookieCache.set(cacheKey, {
@@ -48,11 +50,11 @@ export function loadCookies(profile: BrowserProfile): BrowserCookie[] {
48
50
  return cookies;
49
51
  }
50
52
 
51
- export function createBrowserSession(
53
+ export async function createBrowserSession(
52
54
  profile: BrowserProfile,
53
55
  domains: string[],
54
- ): BrowserSession | null {
55
- const cookies = loadCookies(profile).filter((cookie) => {
56
+ ): Promise<BrowserSession | null> {
57
+ const cookies = (await loadCookies(profile)).filter((cookie) => {
56
58
  return domains.some((domain) => matchesBrowserDomain(domain, cookie.domain));
57
59
  });
58
60
  if (cookies.length === 0) return null;