tau-coding-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/extensions/answer.ts +601 -0
  4. package/extensions/branch-term.ts +405 -0
  5. package/extensions/btw.ts +444 -0
  6. package/extensions/ghostty.ts +301 -0
  7. package/extensions/git-diff-stats.ts +277 -0
  8. package/extensions/git-pr-status.ts +286 -0
  9. package/extensions/insights.ts +2367 -0
  10. package/extensions/interlude.ts +144 -0
  11. package/extensions/loop.ts +529 -0
  12. package/extensions/memory.ts +1889 -0
  13. package/extensions/notify.ts +161 -0
  14. package/extensions/openai-fast.ts +227 -0
  15. package/extensions/openai-verbosity.ts +223 -0
  16. package/extensions/review.ts +4347 -0
  17. package/extensions/sandbox/index.ts +2578 -0
  18. package/extensions/usage/anthropic.ts +198 -0
  19. package/extensions/usage/github-copilot.ts +204 -0
  20. package/extensions/usage/google-gemini-cli.ts +232 -0
  21. package/extensions/usage/index.ts +2310 -0
  22. package/extensions/usage/minimax.ts +208 -0
  23. package/extensions/usage/openai-codex.ts +180 -0
  24. package/extensions/usage/openrouter.ts +168 -0
  25. package/extensions/usage/providers.ts +26 -0
  26. package/extensions/usage/shared.ts +156 -0
  27. package/extensions/usage/types.ts +53 -0
  28. package/extensions/usage/zai.ts +186 -0
  29. package/extensions/websearch/README.md +68 -0
  30. package/extensions/websearch/browser/chromium.ts +330 -0
  31. package/extensions/websearch/browser/discovery.ts +75 -0
  32. package/extensions/websearch/browser/firefox.ts +150 -0
  33. package/extensions/websearch/browser/sqlite.ts +38 -0
  34. package/extensions/websearch/config.ts +76 -0
  35. package/extensions/websearch/index.ts +312 -0
  36. package/extensions/websearch/normalize.ts +155 -0
  37. package/extensions/websearch/providers/anthropic.pi.ts +139 -0
  38. package/extensions/websearch/providers/gemini.browser.ts +185 -0
  39. package/extensions/websearch/providers/gemini.pi.ts +108 -0
  40. package/extensions/websearch/providers/openai-codex.browser.ts +77 -0
  41. package/extensions/websearch/providers/openai-codex.pi.ts +49 -0
  42. package/extensions/websearch/providers/openai-codex.shared.ts +123 -0
  43. package/extensions/websearch/providers/pi-model.shared.ts +86 -0
  44. package/extensions/websearch/providers/search-prompt.shared.ts +17 -0
  45. package/extensions/websearch/providers/shared.ts +76 -0
  46. package/extensions/websearch/types.ts +52 -0
  47. package/extensions/worktree.ts +2223 -0
  48. package/package.json +72 -0
  49. package/skills/browser-tools/SKILL.md +253 -0
  50. package/skills/browser-tools/scripts/browser-content.js +100 -0
  51. package/skills/browser-tools/scripts/browser-cookies.js +33 -0
  52. package/skills/browser-tools/scripts/browser-dismiss-cookies.js +455 -0
  53. package/skills/browser-tools/scripts/browser-eval.js +75 -0
  54. package/skills/browser-tools/scripts/browser-logs-tail.js +89 -0
  55. package/skills/browser-tools/scripts/browser-nav.js +28 -0
  56. package/skills/browser-tools/scripts/browser-net-summary.js +115 -0
  57. package/skills/browser-tools/scripts/browser-pick.js +133 -0
  58. package/skills/browser-tools/scripts/browser-screenshot.js +18 -0
  59. package/skills/browser-tools/scripts/browser-start.js +281 -0
  60. package/skills/browser-tools/scripts/browser-watch.js +321 -0
  61. package/skills/browser-tools/scripts/utils.js +60 -0
  62. package/skills/git-clean-history/SKILL.md +45 -0
  63. package/skills/git-commit/SKILL.md +55 -0
  64. package/skills/oracle/SKILL.md +64 -0
  65. package/skills/oracle/scripts/oracle +249 -0
  66. package/skills/oracle/scripts/oracle-bundle +227 -0
  67. package/skills/sentry/SKILL.md +198 -0
  68. package/skills/sentry/lib/auth.js +99 -0
  69. package/skills/sentry/scripts/fetch-event.js +325 -0
  70. package/skills/sentry/scripts/fetch-issue.js +368 -0
  71. package/skills/sentry/scripts/list-issues.js +245 -0
  72. package/skills/sentry/scripts/search-events.js +291 -0
  73. package/skills/sentry/scripts/search-logs.js +234 -0
  74. package/skills/update-changelog/SKILL.md +135 -0
  75. package/skills/web-design/SKILL.md +117 -0
@@ -0,0 +1,330 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createDecipheriv, createHash, pbkdf2Sync } from "node:crypto";
3
+ import { existsSync, readdirSync } from "node:fs";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+
8
+ import type { BrowserCookie, BrowserProfile } from "../types.js";
9
+ import { withSqliteSnapshot } from "./sqlite.js";
10
+
11
+ const MACOS_BROWSER_CONFIGS: ChromiumBrowserConfig[] = [
12
+ {
13
+ name: "Chromium",
14
+ baseDir: path.join(os.homedir(), "Library", "Application Support", "Chromium"),
15
+ keychainService: "Chromium Safe Storage",
16
+ keychainAccount: "Chromium",
17
+ },
18
+ {
19
+ name: "Chrome",
20
+ baseDir: path.join(os.homedir(), "Library", "Application Support", "Google", "Chrome"),
21
+ keychainService: "Chrome Safe Storage",
22
+ keychainAccount: "Chrome",
23
+ },
24
+ {
25
+ name: "Brave",
26
+ baseDir: path.join(
27
+ os.homedir(),
28
+ "Library",
29
+ "Application Support",
30
+ "BraveSoftware",
31
+ "Brave-Browser",
32
+ ),
33
+ keychainService: "Brave Safe Storage",
34
+ keychainAccount: "Brave",
35
+ },
36
+ {
37
+ name: "Edge",
38
+ baseDir: path.join(os.homedir(), "Library", "Application Support", "Microsoft Edge"),
39
+ keychainService: "Microsoft Edge Safe Storage",
40
+ keychainAccount: "Microsoft Edge",
41
+ },
42
+ ];
43
+
44
+ const LINUX_BROWSER_CONFIGS: ChromiumBrowserConfig[] = [
45
+ {
46
+ name: "Chromium",
47
+ baseDir: path.join(os.homedir(), ".config", "chromium"),
48
+ secretToolApp: "chromium",
49
+ },
50
+ {
51
+ name: "Chrome",
52
+ baseDir: path.join(os.homedir(), ".config", "google-chrome"),
53
+ secretToolApp: "chrome",
54
+ },
55
+ {
56
+ name: "Brave",
57
+ baseDir: path.join(os.homedir(), ".config", "BraveSoftware", "Brave-Browser"),
58
+ secretToolApp: "brave",
59
+ },
60
+ {
61
+ name: "Edge",
62
+ baseDir: path.join(os.homedir(), ".config", "microsoft-edge"),
63
+ secretToolApp: "microsoft-edge",
64
+ },
65
+ ];
66
+
67
+ interface ChromiumBrowserConfig {
68
+ name: string;
69
+ baseDir: string;
70
+ keychainService?: string;
71
+ keychainAccount?: string;
72
+ secretToolApp?: string;
73
+ }
74
+
75
+ const passwordCache = new Map<string, string | null>();
76
+
77
+ function browserConfigs(): ChromiumBrowserConfig[] {
78
+ if (process.platform === "darwin") return MACOS_BROWSER_CONFIGS;
79
+ if (process.platform === "linux") return LINUX_BROWSER_CONFIGS;
80
+ return [];
81
+ }
82
+
83
+ export function discoverChromiumProfiles(preferredProfileName?: string): BrowserProfile[] {
84
+ const profiles: BrowserProfile[] = [];
85
+
86
+ for (const config of browserConfigs()) {
87
+ if (!existsSync(config.baseDir)) continue;
88
+
89
+ const candidates = ["Default", ...discoverProfileDirectories(config.baseDir)];
90
+ const seen = new Set<string>();
91
+ for (const directoryName of candidates) {
92
+ if (!directoryName || seen.has(directoryName)) continue;
93
+ seen.add(directoryName);
94
+
95
+ const profilePath = path.join(config.baseDir, directoryName);
96
+ const cookiesPath = path.join(profilePath, "Cookies");
97
+ if (!existsSync(cookiesPath)) continue;
98
+
99
+ profiles.push({
100
+ family: "chromium",
101
+ browserName: config.name,
102
+ profileName: directoryName,
103
+ profilePath,
104
+ });
105
+ }
106
+ }
107
+
108
+ const sorted = profiles.sort((left, right) => {
109
+ const leftPreferred = isPreferredChromiumProfile(left, preferredProfileName);
110
+ const rightPreferred = isPreferredChromiumProfile(right, preferredProfileName);
111
+ if (leftPreferred !== rightPreferred) return leftPreferred ? -1 : 1;
112
+
113
+ if (left.browserName !== right.browserName) {
114
+ return browserPriority(left.browserName) - browserPriority(right.browserName);
115
+ }
116
+
117
+ return left.profileName.localeCompare(right.profileName);
118
+ });
119
+
120
+ return preferredProfileName
121
+ ? sorted.filter((profile) => isPreferredChromiumProfile(profile, preferredProfileName))
122
+ : sorted;
123
+ }
124
+
125
+ function browserPriority(browserName: string): number {
126
+ const order = ["Chromium", "Chrome", "Brave", "Edge"];
127
+ const index = order.indexOf(browserName);
128
+ return index === -1 ? order.length : index;
129
+ }
130
+
131
+ function isPreferredChromiumProfile(
132
+ profile: BrowserProfile,
133
+ preferredProfileName?: string,
134
+ ): boolean {
135
+ if (!preferredProfileName) return false;
136
+ return profile.profileName === preferredProfileName;
137
+ }
138
+
139
+ function discoverProfileDirectories(baseDir: string): string[] {
140
+ try {
141
+ return readdirSync(baseDir, { withFileTypes: true })
142
+ .filter((entry) => entry.isDirectory())
143
+ .map((entry) => entry.name)
144
+ .filter((name) => name === "Default" || /^Profile\s+\d+$/.test(name));
145
+ } catch {
146
+ return [];
147
+ }
148
+ }
149
+
150
+ export function loadChromiumCookies(profile: BrowserProfile): BrowserCookie[] {
151
+ const browserConfig = browserConfigs().find((config) =>
152
+ profile.profilePath.startsWith(config.baseDir),
153
+ );
154
+ if (!browserConfig) return [];
155
+
156
+ const password = readBrowserPassword(browserConfig);
157
+ if (!password) return [];
158
+
159
+ const keyMaterial = pbkdf2Sync(
160
+ password,
161
+ "saltysalt",
162
+ process.platform === "darwin" ? 1003 : 1,
163
+ 16,
164
+ "sha1",
165
+ );
166
+ const cookiesPath = path.join(profile.profilePath, "Cookies");
167
+ if (!existsSync(cookiesPath)) return [];
168
+
169
+ try {
170
+ return withSqliteSnapshot(cookiesPath, "websearch-chromium", (tempDbPath) => {
171
+ const db = new DatabaseSync(tempDbPath, { readOnly: true });
172
+ try {
173
+ const rows = db
174
+ .prepare(
175
+ "SELECT host_key, name, value, encrypted_value, path, is_secure FROM cookies ORDER BY host_key ASC, path DESC, expires_utc DESC",
176
+ )
177
+ .all() as Array<Record<string, unknown>>;
178
+
179
+ const cookies: BrowserCookie[] = [];
180
+ const seen = new Set<string>();
181
+ let sawEncryptedCookie = false;
182
+ let failedDecrypt = false;
183
+ for (const row of rows) {
184
+ const name = typeof row.name === "string" ? row.name : null;
185
+ const domain = typeof row.host_key === "string" ? row.host_key : null;
186
+ const cookiePath = typeof row.path === "string" ? row.path : "/";
187
+ if (!name || !domain) continue;
188
+
189
+ const key = `${name}\u0000${domain}\u0000${cookiePath}`;
190
+ if (seen.has(key)) continue;
191
+
192
+ const plainValue =
193
+ typeof row.value === "string" && row.value.length > 0 ? row.value : null;
194
+ if (plainValue !== null) {
195
+ seen.add(key);
196
+ cookies.push({
197
+ name,
198
+ value: plainValue,
199
+ domain,
200
+ path: cookiePath,
201
+ secure: Boolean(row.is_secure),
202
+ });
203
+ continue;
204
+ }
205
+
206
+ if (!(row.encrypted_value instanceof Uint8Array)) continue;
207
+ sawEncryptedCookie = true;
208
+ const decrypted = decryptCookieValue(row.encrypted_value, keyMaterial, domain);
209
+ if (decrypted !== null) {
210
+ seen.add(key);
211
+ cookies.push({
212
+ name,
213
+ value: decrypted,
214
+ domain,
215
+ path: cookiePath,
216
+ secure: Boolean(row.is_secure),
217
+ });
218
+ } else {
219
+ failedDecrypt = true;
220
+ }
221
+ }
222
+
223
+ if (cookies.length === 0 && sawEncryptedCookie && failedDecrypt) {
224
+ throw new Error(`Could not decrypt ${profile.browserName} cookies.`);
225
+ }
226
+
227
+ return cookies;
228
+ } finally {
229
+ db.close();
230
+ }
231
+ });
232
+ } catch (error) {
233
+ if (error instanceof Error) throw error;
234
+ throw new Error(`Could not read ${profile.browserName} cookies.`);
235
+ }
236
+ }
237
+
238
+ function decryptCookieValue(
239
+ encrypted: Uint8Array,
240
+ key: Buffer,
241
+ hostKey: string | null,
242
+ ): string | null {
243
+ const buffer = Buffer.from(encrypted);
244
+ if (buffer.length < 3) return null;
245
+
246
+ const prefix = buffer.subarray(0, 3).toString("utf8");
247
+ if (prefix !== "v10") return null;
248
+
249
+ try {
250
+ const iv = Buffer.alloc(16, 0x20);
251
+ const decipher = createDecipheriv("aes-128-cbc", key, iv);
252
+ decipher.setAutoPadding(false);
253
+ const decrypted = stripPkcs7Padding(
254
+ Buffer.concat([decipher.update(buffer.subarray(3)), decipher.final()]),
255
+ );
256
+ const normalized = stripHostKeyDigest(decrypted, hostKey);
257
+ const text = normalized.toString("utf8");
258
+ let start = 0;
259
+ while (text.charCodeAt(start) === 0) start += 1;
260
+ return text.slice(start);
261
+ } catch {
262
+ return null;
263
+ }
264
+ }
265
+
266
+ function stripPkcs7Padding(buffer: Buffer): Buffer {
267
+ if (buffer.length === 0) return buffer;
268
+ const padding = buffer[buffer.length - 1];
269
+ if (padding <= 0 || padding > 16) return buffer;
270
+ return buffer.subarray(0, buffer.length - padding);
271
+ }
272
+
273
+ function stripHostKeyDigest(buffer: Buffer, hostKey: string | null): Buffer {
274
+ if (!hostKey || buffer.length <= 32) return buffer;
275
+
276
+ const digest = createHash("sha256").update(hostKey).digest();
277
+ return buffer.subarray(0, 32).equals(digest) ? buffer.subarray(32) : buffer;
278
+ }
279
+
280
+ function readBrowserPassword(config: ChromiumBrowserConfig): string | null {
281
+ const cacheKey = `${config.name}:${config.baseDir}`;
282
+ if (passwordCache.has(cacheKey)) {
283
+ return passwordCache.get(cacheKey) ?? null;
284
+ }
285
+
286
+ let password: string | null = null;
287
+ if (process.platform === "darwin") {
288
+ if (!config.keychainAccount || !config.keychainService) return null;
289
+ password = readMacKeychainPassword(config.name, config.keychainAccount, config.keychainService);
290
+ } else if (process.platform === "linux") {
291
+ password = readLinuxPassword(config.secretToolApp);
292
+ }
293
+
294
+ if (password) {
295
+ passwordCache.set(cacheKey, password);
296
+ }
297
+ return password;
298
+ }
299
+
300
+ function readMacKeychainPassword(
301
+ browserName: string,
302
+ account: string,
303
+ service: string,
304
+ ): string | null {
305
+ 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
311
+ );
312
+ } catch {
313
+ throw new Error(`Could not read ${browserName} cookie decryption password from Keychain.`);
314
+ }
315
+ }
316
+
317
+ function readLinuxPassword(secretToolApp?: string): string | null {
318
+ if (!secretToolApp) return "peanuts";
319
+
320
+ try {
321
+ return (
322
+ execFileSync("secret-tool", ["lookup", "application", secretToolApp], {
323
+ encoding: "utf8",
324
+ stdio: ["ignore", "pipe", "ignore"],
325
+ }).trim() || "peanuts"
326
+ );
327
+ } catch {
328
+ return "peanuts";
329
+ }
330
+ }
@@ -0,0 +1,75 @@
1
+ import { discoverChromiumProfiles, loadChromiumCookies } from "./chromium.js";
2
+ import { discoverFirefoxProfiles, loadFirefoxCookies } from "./firefox.js";
3
+ import type {
4
+ BrowserCookie,
5
+ BrowserProfile,
6
+ BrowserSession,
7
+ WebsearchBrowserFamily,
8
+ WebsearchConfig,
9
+ } from "../types.js";
10
+
11
+ const COOKIE_CACHE_TTL_MS = 30_000;
12
+ const cookieCache = new Map<string, { cookies: BrowserCookie[]; expiresAt: number }>();
13
+
14
+ export function discoverProfiles(
15
+ browserFamilies: WebsearchBrowserFamily[],
16
+ preferredProfiles: WebsearchConfig["profiles"],
17
+ ): BrowserProfile[] {
18
+ const profiles: BrowserProfile[] = [];
19
+
20
+ for (const family of browserFamilies) {
21
+ if (family === "firefox") {
22
+ profiles.push(...discoverFirefoxProfiles(preferredProfiles.firefox));
23
+ continue;
24
+ }
25
+
26
+ profiles.push(...discoverChromiumProfiles(preferredProfiles.chromium));
27
+ }
28
+
29
+ return profiles;
30
+ }
31
+
32
+ export function loadCookies(profile: BrowserProfile): BrowserCookie[] {
33
+ const cacheKey = `${profile.family}:${profile.profilePath}`;
34
+ const cached = cookieCache.get(cacheKey);
35
+ if (cached && cached.expiresAt > Date.now()) return cached.cookies;
36
+ if (cached) cookieCache.delete(cacheKey);
37
+
38
+ const cookies =
39
+ profile.family === "firefox" ? loadFirefoxCookies(profile) : loadChromiumCookies(profile);
40
+
41
+ if (cookies.length > 0) {
42
+ cookieCache.set(cacheKey, {
43
+ cookies,
44
+ expiresAt: Date.now() + COOKIE_CACHE_TTL_MS,
45
+ });
46
+ }
47
+
48
+ return cookies;
49
+ }
50
+
51
+ export function createBrowserSession(
52
+ profile: BrowserProfile,
53
+ domains: string[],
54
+ ): BrowserSession | null {
55
+ const cookies = loadCookies(profile).filter((cookie) => {
56
+ return domains.some((domain) => matchesBrowserDomain(domain, cookie.domain));
57
+ });
58
+ if (cookies.length === 0) return null;
59
+
60
+ return {
61
+ profile,
62
+ cookies,
63
+ };
64
+ }
65
+
66
+ function matchesBrowserDomain(requestedDomain: string, cookieDomain: string): boolean {
67
+ const normalizedRequested = requestedDomain.toLowerCase().replace(/^\./, "");
68
+ const normalizedCookie = cookieDomain.toLowerCase().replace(/^\./, "");
69
+
70
+ return (
71
+ normalizedCookie === normalizedRequested ||
72
+ normalizedCookie.endsWith(`.${normalizedRequested}`) ||
73
+ normalizedRequested.endsWith(`.${normalizedCookie}`)
74
+ );
75
+ }
@@ -0,0 +1,150 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { DatabaseSync } from "node:sqlite";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import type { BrowserCookie, BrowserProfile } from "../types.js";
7
+ import { withSqliteSnapshot } from "./sqlite.js";
8
+
9
+ function firefoxBaseDir(): string | null {
10
+ if (process.platform === "darwin") {
11
+ return path.join(os.homedir(), "Library", "Application Support", "Firefox");
12
+ }
13
+
14
+ if (process.platform === "linux") {
15
+ return path.join(os.homedir(), ".mozilla", "firefox");
16
+ }
17
+
18
+ return null;
19
+ }
20
+
21
+ function parseIni(text: string): Array<Record<string, string>> {
22
+ const sections: Array<Record<string, string>> = [];
23
+ let current: Record<string, string> | null = null;
24
+
25
+ for (const rawLine of text.split(/\r?\n/)) {
26
+ const line = rawLine.trim();
27
+ if (!line || line.startsWith(";") || line.startsWith("#")) continue;
28
+
29
+ const sectionMatch = line.match(/^\[(.+)\]$/);
30
+ if (sectionMatch) {
31
+ current = { __section: sectionMatch[1] ?? "" };
32
+ sections.push(current);
33
+ continue;
34
+ }
35
+
36
+ if (!current) continue;
37
+
38
+ const separatorIndex = line.indexOf("=");
39
+ if (separatorIndex === -1) continue;
40
+ const key = line.slice(0, separatorIndex).trim();
41
+ const value = line.slice(separatorIndex + 1).trim();
42
+ current[key] = value;
43
+ }
44
+
45
+ return sections;
46
+ }
47
+
48
+ export function discoverFirefoxProfiles(preferredProfileName?: string): BrowserProfile[] {
49
+ const baseDir = firefoxBaseDir();
50
+ if (!baseDir) return [];
51
+
52
+ const profilesIniPath = path.join(baseDir, "profiles.ini");
53
+
54
+ let sections: Array<Record<string, string>> = [];
55
+ try {
56
+ sections = parseIni(readFileSync(profilesIniPath, "utf8"));
57
+ } catch {
58
+ return [];
59
+ }
60
+
61
+ const profiles: BrowserProfile[] = [];
62
+ for (const section of sections) {
63
+ if (!section.__section?.startsWith("Profile")) continue;
64
+ const profilePathValue = section.Path;
65
+ if (!profilePathValue) continue;
66
+
67
+ const profilePath =
68
+ section.IsRelative === "1" ? path.join(baseDir, profilePathValue) : profilePathValue;
69
+ const cookiesPath = path.join(profilePath, "cookies.sqlite");
70
+ if (!existsSync(cookiesPath)) continue;
71
+
72
+ profiles.push({
73
+ family: "firefox",
74
+ browserName: "Firefox",
75
+ profileName: section.Name?.trim() || path.basename(profilePath),
76
+ profilePath,
77
+ });
78
+ }
79
+
80
+ const sorted = profiles.sort((left, right) => {
81
+ const leftPreferred = isPreferredFirefoxProfile(left, preferredProfileName);
82
+ const rightPreferred = isPreferredFirefoxProfile(right, preferredProfileName);
83
+ if (leftPreferred !== rightPreferred) return leftPreferred ? -1 : 1;
84
+ return left.profileName.localeCompare(right.profileName);
85
+ });
86
+
87
+ return preferredProfileName
88
+ ? sorted.filter((profile) => isPreferredFirefoxProfile(profile, preferredProfileName))
89
+ : sorted;
90
+ }
91
+
92
+ function isPreferredFirefoxProfile(
93
+ profile: BrowserProfile,
94
+ preferredProfileName?: string,
95
+ ): boolean {
96
+ if (!preferredProfileName) return false;
97
+ return (
98
+ profile.profileName === preferredProfileName ||
99
+ path.basename(profile.profilePath) === preferredProfileName
100
+ );
101
+ }
102
+
103
+ export function loadFirefoxCookies(profile: BrowserProfile): BrowserCookie[] {
104
+ const cookiesPath = path.join(profile.profilePath, "cookies.sqlite");
105
+ if (!existsSync(cookiesPath)) return [];
106
+
107
+ try {
108
+ return withSqliteSnapshot(cookiesPath, "websearch-firefox", (tempDbPath) => {
109
+ const db = new DatabaseSync(tempDbPath, { readOnly: true });
110
+ try {
111
+ const rows = db
112
+ .prepare(
113
+ "SELECT name, value, host, path, isSecure FROM moz_cookies ORDER BY host ASC, path DESC, expiry DESC",
114
+ )
115
+ .all() as Array<Record<string, unknown>>;
116
+
117
+ const cookies: BrowserCookie[] = [];
118
+ const seen = new Set<string>();
119
+ for (const row of rows) {
120
+ const name = typeof row.name === "string" ? row.name : null;
121
+ const value = typeof row.value === "string" ? row.value : null;
122
+ const domain = typeof row.host === "string" ? row.host : null;
123
+ const cookiePath = typeof row.path === "string" ? row.path : "/";
124
+ if (!name || value === null || !domain) continue;
125
+
126
+ const key = `${name}\u0000${domain}\u0000${cookiePath}`;
127
+ if (seen.has(key)) continue;
128
+ seen.add(key);
129
+ cookies.push({
130
+ name,
131
+ value,
132
+ domain,
133
+ path: cookiePath,
134
+ secure: Boolean(row.isSecure),
135
+ });
136
+ }
137
+
138
+ return cookies;
139
+ } catch (error) {
140
+ if (error instanceof Error) throw error;
141
+ throw new Error("Could not read Firefox cookies.");
142
+ } finally {
143
+ db.close();
144
+ }
145
+ });
146
+ } catch (error) {
147
+ if (error instanceof Error) throw error;
148
+ throw new Error("Could not snapshot Firefox cookies.");
149
+ }
150
+ }
@@ -0,0 +1,38 @@
1
+ import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ export function withSqliteSnapshot<T>(
6
+ sourceDbPath: string,
7
+ tempPrefix: string,
8
+ operation: (snapshotPath: string) => T,
9
+ ): T {
10
+ const tempDir = path.join(os.tmpdir(), `${tempPrefix}-${process.pid}-${Date.now()}`);
11
+ const snapshotPath = path.join(tempDir, path.basename(sourceDbPath));
12
+
13
+ try {
14
+ mkdirSync(tempDir, { recursive: true });
15
+ copyFileSync(sourceDbPath, snapshotPath);
16
+ copySidecar(sourceDbPath, snapshotPath, "-wal");
17
+ copySidecar(sourceDbPath, snapshotPath, "-shm");
18
+ copySidecar(sourceDbPath, snapshotPath, "-journal");
19
+ return operation(snapshotPath);
20
+ } finally {
21
+ rmSync(tempDir, { recursive: true, force: true });
22
+ }
23
+ }
24
+
25
+ export function copySidecar(sourceDbPath: string, targetDbPath: string, suffix: string): void {
26
+ const source = `${sourceDbPath}${suffix}`;
27
+ if (!existsSync(source)) return;
28
+
29
+ try {
30
+ copyFileSync(source, `${targetDbPath}${suffix}`);
31
+ } catch (error) {
32
+ const code =
33
+ error && typeof error === "object" && "code" in error
34
+ ? (error as { code?: string }).code
35
+ : undefined;
36
+ if (code !== "ENOENT") throw error;
37
+ }
38
+ }
@@ -0,0 +1,76 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+
5
+ import type { WebsearchConfig, WebsearchRouteId } from "./types.js";
6
+
7
+ const WEBSEARCH_CONFIG_PATH = path.join(homedir(), ".pi", "websearch.json");
8
+
9
+ const DEFAULT_ROUTES: WebsearchRouteId[] = [
10
+ "pi:openai-codex",
11
+ "pi:anthropic",
12
+ "pi:gemini",
13
+ "firefox:gemini",
14
+ "firefox:openai-codex",
15
+ "chromium:gemini",
16
+ "chromium:openai-codex",
17
+ ];
18
+
19
+ function sanitizeRoutes(value: unknown): WebsearchRouteId[] {
20
+ if (value === undefined) return [...DEFAULT_ROUTES];
21
+ if (!Array.isArray(value)) {
22
+ throw new Error("websearch config routes must be an array.");
23
+ }
24
+
25
+ const valid = new Set<WebsearchRouteId>(DEFAULT_ROUTES);
26
+ const result: WebsearchRouteId[] = [];
27
+
28
+ for (const item of value) {
29
+ if (typeof item !== "string" || !valid.has(item as WebsearchRouteId)) {
30
+ throw new Error(`Invalid websearch route: ${String(item)}`);
31
+ }
32
+ if (!result.includes(item as WebsearchRouteId)) {
33
+ result.push(item as WebsearchRouteId);
34
+ }
35
+ }
36
+
37
+ return result;
38
+ }
39
+
40
+ function sanitizeProfileName(value: unknown): string | undefined {
41
+ if (typeof value !== "string") return undefined;
42
+ const trimmed = value.trim();
43
+ return trimmed.length > 0 ? trimmed : undefined;
44
+ }
45
+
46
+ export function loadConfig(): WebsearchConfig {
47
+ let raw: Record<string, unknown> = {};
48
+
49
+ try {
50
+ raw = JSON.parse(readFileSync(WEBSEARCH_CONFIG_PATH, "utf8")) as Record<string, unknown>;
51
+ } catch (error) {
52
+ const code =
53
+ error && typeof error === "object" && "code" in error
54
+ ? (error as { code?: string }).code
55
+ : undefined;
56
+ if (code === "ENOENT") {
57
+ raw = {};
58
+ } else {
59
+ const message = error instanceof Error ? error.message : String(error);
60
+ throw new Error(`Invalid websearch config at ${WEBSEARCH_CONFIG_PATH}: ${message}`);
61
+ }
62
+ }
63
+
64
+ const profiles =
65
+ raw.profiles && typeof raw.profiles === "object"
66
+ ? (raw.profiles as Record<string, unknown>)
67
+ : {};
68
+
69
+ return {
70
+ routes: sanitizeRoutes(raw.routes),
71
+ profiles: {
72
+ firefox: sanitizeProfileName(profiles.firefox),
73
+ chromium: sanitizeProfileName(profiles.chromium),
74
+ },
75
+ };
76
+ }