tau-coding-agent 0.1.4 → 0.1.6

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 (51) hide show
  1. package/README.md +1 -1
  2. package/extensions/answer.ts +59 -47
  3. package/extensions/branch-term.ts +8 -13
  4. package/extensions/btw.ts +77 -6
  5. package/extensions/ghostty.ts +64 -11
  6. package/extensions/insights.ts +42 -4
  7. package/extensions/loop.ts +51 -12
  8. package/extensions/memory.ts +35 -17
  9. package/extensions/notify.ts +24 -5
  10. package/extensions/openai-fast.ts +6 -4
  11. package/extensions/openai-verbosity.ts +6 -4
  12. package/extensions/review/fix.ts +239 -0
  13. package/extensions/review/git.ts +517 -0
  14. package/extensions/review/index.ts +232 -0
  15. package/extensions/review/message-queue.ts +249 -0
  16. package/extensions/review/models.ts +589 -0
  17. package/extensions/review/prompts.ts +337 -0
  18. package/extensions/review/renderers/inline.ts +126 -0
  19. package/extensions/review/request.ts +504 -0
  20. package/extensions/review/review.ts +1305 -0
  21. package/extensions/review/runner.ts +522 -0
  22. package/extensions/review/runtime.ts +275 -0
  23. package/extensions/review/schema.ts +160 -0
  24. package/extensions/review/submit-review-tool.ts +62 -0
  25. package/extensions/review/submit-triage-tool.ts +60 -0
  26. package/extensions/review/triage.ts +746 -0
  27. package/extensions/sandbox/index.ts +976 -185
  28. package/extensions/sandbox/macos-sandbox-shell.mjs +202 -0
  29. package/extensions/{interlude.ts → stash.ts} +13 -11
  30. package/extensions/tool-display-mode.ts +2 -3
  31. package/extensions/usage/anthropic.ts +5 -6
  32. package/extensions/usage/github-copilot.ts +30 -9
  33. package/extensions/usage/index.ts +9 -5
  34. package/extensions/usage/openai-codex.ts +2 -2
  35. package/extensions/websearch/README.md +1 -1
  36. package/extensions/websearch/browser/chromium.ts +42 -31
  37. package/extensions/websearch/browser/discovery.ts +11 -9
  38. package/extensions/websearch/browser/firefox.ts +10 -7
  39. package/extensions/websearch/browser/fs.ts +10 -0
  40. package/extensions/websearch/browser/sqlite.ts +22 -15
  41. package/extensions/websearch/config.ts +2 -2
  42. package/extensions/websearch/index.ts +6 -6
  43. package/extensions/websearch/providers/pi-model.shared.ts +19 -1
  44. package/extensions/worktree.ts +33 -33
  45. package/package.json +4 -4
  46. package/skills/git-clean-history/SKILL.md +1 -0
  47. package/skills/git-commit/SKILL.md +2 -2
  48. package/skills/oracle/SKILL.md +3 -3
  49. package/skills/oracle/scripts/oracle +82 -61
  50. package/themes/tau-dark.json +2 -1
  51. package/extensions/review.ts +0 -4420
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Compatibility workaround for sandbox-runtime on macOS.
5
+ *
6
+ * The runtime routes Git-over-SSH through BSD nc, which cannot authenticate to
7
+ * its SOCKS5 proxy. This shell preserves normal bash execution while replacing
8
+ * that transport with an authenticated HTTP CONNECT tunnel.
9
+ *
10
+ * Remove once the upstream fix ships:
11
+ * https://github.com/anthropic-experimental/sandbox-runtime/pull/385
12
+ */
13
+
14
+ import { spawn } from "node:child_process";
15
+ import { connect } from "node:net";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ const PROXY_CONNECT_MODE = "--proxy-connect";
19
+ const CONNECT_TIMEOUT_MS = 15_000;
20
+ const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
21
+
22
+ if (process.argv[2] === PROXY_CONNECT_MODE) {
23
+ connectThroughSandboxProxy(process.argv[3], process.argv[4]);
24
+ } else {
25
+ runBash(process.argv.slice(2));
26
+ }
27
+
28
+ function runBash(args) {
29
+ if (hasAuthenticatedSandboxProxy()) {
30
+ const scriptPath = fileURLToPath(import.meta.url);
31
+ const proxyCommand = [
32
+ shellQuote(escapeSshPercent(process.execPath)),
33
+ shellQuote(escapeSshPercent(scriptPath)),
34
+ PROXY_CONNECT_MODE,
35
+ "%h",
36
+ "%p",
37
+ ].join(" ");
38
+ process.env.GIT_SSH_COMMAND = [
39
+ "ssh",
40
+ "-o ControlMaster=no",
41
+ "-o ControlPath=none",
42
+ `-o ProxyCommand=${shellQuote(proxyCommand)}`,
43
+ ].join(" ");
44
+ }
45
+
46
+ const child = spawn("bash", args, {
47
+ env: process.env,
48
+ stdio: "inherit",
49
+ });
50
+ child.once("error", (error) => {
51
+ console.error(error.message);
52
+ process.exitCode = 1;
53
+ });
54
+ child.once("exit", (code, signal) => {
55
+ if (signal) {
56
+ process.kill(process.pid, signal);
57
+ return;
58
+ }
59
+ process.exitCode = code ?? 1;
60
+ });
61
+ }
62
+
63
+ function hasAuthenticatedSandboxProxy() {
64
+ try {
65
+ const proxy = getSandboxProxyUrl();
66
+ return proxy.protocol === "http:" && Boolean(proxy.username || proxy.password);
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ function connectThroughSandboxProxy(targetHost, targetPortText) {
73
+ const targetPort = Number(targetPortText);
74
+ if (
75
+ !targetHost ||
76
+ /[\r\n]/u.test(targetHost) ||
77
+ !Number.isInteger(targetPort) ||
78
+ targetPort < 1 ||
79
+ targetPort > 65_535
80
+ ) {
81
+ fail("Invalid SSH proxy target.");
82
+ }
83
+
84
+ let proxy;
85
+ try {
86
+ proxy = getSandboxProxyUrl();
87
+ } catch (error) {
88
+ fail(error);
89
+ }
90
+ if (proxy.protocol !== "http:") {
91
+ fail(`Unsupported sandbox proxy protocol: ${proxy.protocol}`);
92
+ }
93
+
94
+ const proxyPort = Number(proxy.port || 80);
95
+ const authority = targetHost.includes(":")
96
+ ? `[${targetHost}]:${targetPort}`
97
+ : `${targetHost}:${targetPort}`;
98
+ let username;
99
+ let password;
100
+ try {
101
+ username = decodeURIComponent(proxy.username);
102
+ password = decodeURIComponent(proxy.password);
103
+ } catch {
104
+ fail("Sandbox HTTP proxy credentials are invalid.");
105
+ }
106
+ const authorization =
107
+ username || password
108
+ ? `Proxy-Authorization: Basic ${Buffer.from(`${username}:${password}`).toString("base64")}\r\n`
109
+ : "";
110
+
111
+ const socket = connect({ host: proxy.hostname, port: proxyPort });
112
+ const connectTimeout = setTimeout(() => {
113
+ socket.destroy(new Error("Sandbox proxy connection timed out."));
114
+ }, CONNECT_TIMEOUT_MS);
115
+ let response = Buffer.alloc(0);
116
+ let tunnelEstablished = false;
117
+ let connectionFailed = false;
118
+
119
+ socket.once("connect", () => {
120
+ clearTimeout(connectTimeout);
121
+ socket.write(
122
+ `CONNECT ${authority} HTTP/1.1\r\nHost: ${authority}\r\n${authorization}Proxy-Connection: Keep-Alive\r\n\r\n`,
123
+ );
124
+ });
125
+
126
+ const failConnection = (error) => {
127
+ if (connectionFailed) return;
128
+ connectionFailed = true;
129
+ clearTimeout(connectTimeout);
130
+ console.error(error instanceof Error ? error.message : String(error));
131
+ process.exitCode = 1;
132
+ socket.destroy();
133
+ };
134
+
135
+ const handleResponse = (chunk) => {
136
+ response = Buffer.concat([response, chunk]);
137
+ if (response.length > MAX_RESPONSE_HEADER_BYTES) {
138
+ failConnection(new Error("Sandbox proxy returned oversized headers."));
139
+ return;
140
+ }
141
+
142
+ const headerEnd = response.indexOf("\r\n\r\n");
143
+ if (headerEnd === -1) return;
144
+
145
+ const header = response.subarray(0, headerEnd).toString("latin1");
146
+ const status = header.match(/^HTTP\/\d(?:\.\d)?\s+(\d{3})/u)?.[1];
147
+ if (status !== "200") {
148
+ failConnection(new Error(`Sandbox proxy CONNECT failed${status ? ` (${status})` : ""}.`));
149
+ return;
150
+ }
151
+
152
+ tunnelEstablished = true;
153
+ socket.off("data", handleResponse);
154
+ const remainder = response.subarray(headerEnd + 4);
155
+ if (remainder.length > 0) process.stdout.write(remainder);
156
+ process.stdin.pipe(socket);
157
+ socket.pipe(process.stdout);
158
+ };
159
+
160
+ socket.on("data", handleResponse);
161
+ socket.once("error", failConnection);
162
+ socket.once("close", () => {
163
+ clearTimeout(connectTimeout);
164
+ process.stdin.unpipe(socket);
165
+ process.stdin.pause();
166
+ if (!tunnelEstablished && !connectionFailed) {
167
+ failConnection(new Error("Sandbox proxy closed before establishing the SSH tunnel."));
168
+ }
169
+ });
170
+ process.stdin.on("error", failConnection);
171
+ process.stdout.on("error", (error) => {
172
+ if (error?.code === "EPIPE") {
173
+ socket.destroy();
174
+ return;
175
+ }
176
+ failConnection(error);
177
+ });
178
+ }
179
+
180
+ function getSandboxProxyUrl() {
181
+ const value = process.env.HTTPS_PROXY ?? process.env.HTTP_PROXY;
182
+ if (!value) throw new Error("Sandbox HTTP proxy is unavailable.");
183
+
184
+ try {
185
+ return new URL(value);
186
+ } catch {
187
+ throw new Error("Sandbox HTTP proxy URL is invalid.");
188
+ }
189
+ }
190
+
191
+ function escapeSshPercent(value) {
192
+ return value.replaceAll("%", "%%");
193
+ }
194
+
195
+ function shellQuote(value) {
196
+ return `'${value.replaceAll("'", `'\\''`)}'`;
197
+ }
198
+
199
+ function fail(error) {
200
+ console.error(error instanceof Error ? error.message : String(error));
201
+ process.exit(1);
202
+ }
@@ -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 "@earendil-works/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,7 @@
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
5
  type AppKeybinding,
6
6
  CustomEditor,
7
7
  createBashToolDefinition,
@@ -77,8 +77,7 @@ function emptyConfig(): ToolDisplayModeConfig {
77
77
  }
78
78
 
79
79
  function getConfigPath(): string {
80
- const agentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
81
- return path.join(agentDir, CONFIG_FILE);
80
+ return path.join(getAgentDir(), CONFIG_FILE);
82
81
  }
83
82
 
84
83
  function isObject(value: unknown): value is JsonObject {
@@ -1,4 +1,4 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ import { readStoredCredential, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  clampPercent,
4
4
  formatCurrency,
@@ -111,7 +111,7 @@ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<
111
111
  const token = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
112
112
  if (!token) throw new LiveUsageUnavailableError("no auth configured");
113
113
 
114
- const credential = ctx.modelRegistry.authStorage.get(PROVIDER_ID);
114
+ const credential = readStoredCredential(PROVIDER_ID);
115
115
  if (credential?.type === "api_key") {
116
116
  throw new LiveUsageUnavailableError("API-key auth does not expose Claude subscription quota");
117
117
  }
@@ -170,12 +170,11 @@ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<
170
170
  };
171
171
  }
172
172
 
173
- async function getLiveUsageAvailability(ctx: ExtensionContext) {
174
- if (!ctx.modelRegistry.authStorage.hasAuth(PROVIDER_ID)) {
173
+ async function getLiveUsageAvailability(_ctx: ExtensionContext) {
174
+ const credential = readStoredCredential(PROVIDER_ID);
175
+ if (!credential) {
175
176
  return { available: false as const, reason: "no auth configured" };
176
177
  }
177
-
178
- const credential = ctx.modelRegistry.authStorage.get(PROVIDER_ID);
179
178
  if (credential?.type === "oauth") {
180
179
  return { available: true as const };
181
180
  }
@@ -1,4 +1,5 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ import type { OAuthCredential } from "@earendil-works/pi-ai";
2
+ import { readStoredCredential, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
3
  import {
3
4
  clampPercent,
4
5
  formatCount,
@@ -8,7 +9,12 @@ import {
8
9
  readNumber,
9
10
  readString,
10
11
  } from "./shared.js";
11
- import type { LiveUsageItem, LiveUsageSnapshot, UsageProviderDefinition } from "./types.js";
12
+ import type {
13
+ LiveUsageAvailability,
14
+ LiveUsageItem,
15
+ LiveUsageSnapshot,
16
+ UsageProviderDefinition,
17
+ } from "./types.js";
12
18
 
13
19
  const PROVIDER_ID = "github-copilot";
14
20
 
@@ -136,13 +142,18 @@ function buildUsageUrl(enterpriseUrl?: string): string {
136
142
  return `https://${host}/copilot_internal/user`;
137
143
  }
138
144
 
139
- async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<LiveUsageSnapshot> {
140
- const credential = ctx.modelRegistry.authStorage.get(PROVIDER_ID);
141
- if (
142
- credential?.type !== "oauth" ||
143
- typeof credential.refresh !== "string" ||
144
- !credential.refresh.trim()
145
- ) {
145
+ async function getLiveUsageAvailability(_ctx: ExtensionContext): Promise<LiveUsageAvailability> {
146
+ return getStoredCopilotCredential()
147
+ ? { available: true }
148
+ : { available: false, reason: "not logged in" };
149
+ }
150
+
151
+ async function fetchUsage(
152
+ _ctx: ExtensionContext,
153
+ signal?: AbortSignal,
154
+ ): Promise<LiveUsageSnapshot> {
155
+ const credential = getStoredCopilotCredential();
156
+ if (!credential) {
146
157
  throw new Error("Not logged in to GitHub Copilot.");
147
158
  }
148
159
 
@@ -193,11 +204,21 @@ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<
193
204
  };
194
205
  }
195
206
 
207
+ function getStoredCopilotCredential(): OAuthCredential | undefined {
208
+ const credential = readStoredCredential(PROVIDER_ID);
209
+ return credential?.type === "oauth" &&
210
+ typeof credential.refresh === "string" &&
211
+ credential.refresh.trim()
212
+ ? credential
213
+ : undefined;
214
+ }
215
+
196
216
  const githubCopilotProvider: UsageProviderDefinition = {
197
217
  id: PROVIDER_ID,
198
218
  label: "GitHub Copilot",
199
219
  shortLabel: "copilot",
200
220
  color: { r: 168, g: 85, b: 247 },
221
+ getLiveUsageAvailability,
201
222
  fetchLiveUsage: fetchUsage,
202
223
  };
203
224
 
@@ -1,5 +1,9 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { BorderedLoader } from "@earendil-works/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,
@@ -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
 
@@ -1429,7 +1433,7 @@ async function getLiveUsageAvailability(
1429
1433
  return provider.getLiveUsageAvailability(ctx);
1430
1434
  }
1431
1435
 
1432
- return ctx.modelRegistry.authStorage.hasAuth(provider.id)
1436
+ return ctx.modelRegistry.getProviderAuthStatus(provider.id).configured
1433
1437
  ? { available: true }
1434
1438
  : { available: false, reason: "no auth configured" };
1435
1439
  }
@@ -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 "@earendil-works/pi-coding-agent";
1
+ import { readStoredCredential, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
3
  clampPercent,
4
4
  formatCurrency,
@@ -112,7 +112,7 @@ async function fetchUsage(ctx: ExtensionContext, signal?: AbortSignal): Promise<
112
112
  const apiKey = await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_ID);
113
113
  if (!apiKey) throw new Error("Not logged in to OpenAI Codex.");
114
114
 
115
- const credential = ctx.modelRegistry.authStorage.get(PROVIDER_ID);
115
+ const credential = readStoredCredential(PROVIDER_ID);
116
116
  const accountId =
117
117
  credential?.type === "oauth" && typeof credential.accountId === "string"
118
118
  ? credential.accountId
@@ -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;