@undefineds.co/linx 0.3.4 → 0.3.7

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 (172) hide show
  1. package/README.md +58 -23
  2. package/dist/generated/version.js +1 -1
  3. package/dist/generated/version.js.map +1 -1
  4. package/dist/index.js +334 -162
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/account-session.js +4 -8
  7. package/dist/lib/account-session.js.map +1 -1
  8. package/dist/lib/ai-command.js +228 -178
  9. package/dist/lib/ai-command.js.map +1 -1
  10. package/dist/lib/auto-mode/archive.js +38 -7
  11. package/dist/lib/auto-mode/archive.js.map +1 -1
  12. package/dist/lib/auto-mode/auth.js.map +1 -1
  13. package/dist/lib/auto-mode/display.js +71 -45
  14. package/dist/lib/auto-mode/display.js.map +1 -1
  15. package/dist/lib/auto-mode/format.js +9 -7
  16. package/dist/lib/auto-mode/format.js.map +1 -1
  17. package/dist/lib/auto-mode/hooks/claude.js +12 -2
  18. package/dist/lib/auto-mode/hooks/claude.js.map +1 -1
  19. package/dist/lib/auto-mode/hooks/codex.js +17 -7
  20. package/dist/lib/auto-mode/hooks/codex.js.map +1 -1
  21. package/dist/lib/auto-mode/hooks/index.js +28 -8
  22. package/dist/lib/auto-mode/hooks/index.js.map +1 -1
  23. package/dist/lib/auto-mode/pod-ai.js +20 -37
  24. package/dist/lib/auto-mode/pod-ai.js.map +1 -1
  25. package/dist/lib/auto-mode/pod-approval.js +124 -195
  26. package/dist/lib/auto-mode/pod-approval.js.map +1 -1
  27. package/dist/lib/auto-mode/pod-persistence.js +169 -90
  28. package/dist/lib/auto-mode/pod-persistence.js.map +1 -1
  29. package/dist/lib/auto-mode/runner.js +683 -81
  30. package/dist/lib/auto-mode/runner.js.map +1 -1
  31. package/dist/lib/auto-mode/secretary.js +186 -41
  32. package/dist/lib/auto-mode/secretary.js.map +1 -1
  33. package/dist/lib/auto-mode-command.js +32 -32
  34. package/dist/lib/auto-mode-command.js.map +1 -1
  35. package/dist/lib/chat-api.js +242 -50
  36. package/dist/lib/chat-api.js.map +1 -1
  37. package/dist/lib/codex-plugin/bridge.js +164 -17
  38. package/dist/lib/codex-plugin/bridge.js.map +1 -1
  39. package/dist/lib/codex-plugin/codex-native-proxy.js +370 -34
  40. package/dist/lib/codex-plugin/codex-native-proxy.js.map +1 -1
  41. package/dist/lib/credentials-store.js +33 -42
  42. package/dist/lib/credentials-store.js.map +1 -1
  43. package/dist/lib/linx-cloud-errors.js +61 -0
  44. package/dist/lib/linx-cloud-errors.js.map +1 -0
  45. package/dist/lib/linx-tui-contract.js +8 -5
  46. package/dist/lib/linx-tui-contract.js.map +1 -1
  47. package/dist/lib/login-command.js +9 -2
  48. package/dist/lib/login-command.js.map +1 -1
  49. package/dist/lib/models.js +3 -20
  50. package/dist/lib/models.js.map +1 -1
  51. package/dist/lib/oidc-auth.js +143 -17
  52. package/dist/lib/oidc-auth.js.map +1 -1
  53. package/dist/lib/oidc-session-storage.js +2 -6
  54. package/dist/lib/oidc-session-storage.js.map +1 -1
  55. package/dist/lib/pi-adapter/auto-input-controller.js +988 -0
  56. package/dist/lib/pi-adapter/auto-input-controller.js.map +1 -0
  57. package/dist/lib/pi-adapter/backend-command.js +2 -0
  58. package/dist/lib/pi-adapter/backend-command.js.map +1 -0
  59. package/dist/lib/pi-adapter/backend-credentials.js +80 -0
  60. package/dist/lib/pi-adapter/backend-credentials.js.map +1 -0
  61. package/dist/lib/pi-adapter/branding.js +246 -108
  62. package/dist/lib/pi-adapter/branding.js.map +1 -1
  63. package/dist/lib/pi-adapter/control-state.js +72 -0
  64. package/dist/lib/pi-adapter/control-state.js.map +1 -0
  65. package/dist/lib/pi-adapter/interactive.js +2634 -30
  66. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  67. package/dist/lib/pi-adapter/pod-approval.js +382 -210
  68. package/dist/lib/pi-adapter/pod-approval.js.map +1 -1
  69. package/dist/lib/pi-adapter/pod-mirror-mapping.js +71 -17
  70. package/dist/lib/pi-adapter/pod-mirror-mapping.js.map +1 -1
  71. package/dist/lib/pi-adapter/pod-mirror.js +531 -64
  72. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -1
  73. package/dist/lib/pi-adapter/pod-native.js +81 -85
  74. package/dist/lib/pi-adapter/pod-native.js.map +1 -1
  75. package/dist/lib/pi-adapter/pod-status-output.js +54 -0
  76. package/dist/lib/pi-adapter/pod-status-output.js.map +1 -0
  77. package/dist/lib/pi-adapter/runtime.js +458 -228
  78. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  79. package/dist/lib/pi-adapter/session-control.js +509 -0
  80. package/dist/lib/pi-adapter/session-control.js.map +1 -0
  81. package/dist/lib/pi-adapter/session.js +35 -22
  82. package/dist/lib/pi-adapter/session.js.map +1 -1
  83. package/dist/lib/pi-adapter/stream.js +89 -32
  84. package/dist/lib/pi-adapter/stream.js.map +1 -1
  85. package/dist/lib/pi-adapter/sync-recovery.js +89 -0
  86. package/dist/lib/pi-adapter/sync-recovery.js.map +1 -0
  87. package/dist/lib/pi-adapter/web-fetch.js +13 -14
  88. package/dist/lib/pi-adapter/web-fetch.js.map +1 -1
  89. package/dist/lib/pod-chat-store.js +254 -78
  90. package/dist/lib/pod-chat-store.js.map +1 -1
  91. package/dist/lib/pod-data-session.js +156 -35
  92. package/dist/lib/pod-data-session.js.map +1 -1
  93. package/dist/lib/solid-auth-store.js +27 -0
  94. package/dist/lib/solid-auth-store.js.map +1 -0
  95. package/dist/lib/solid-auth.js +2 -4
  96. package/dist/lib/solid-auth.js.map +1 -1
  97. package/dist/lib/solid-client-credentials-login.js +100 -0
  98. package/dist/lib/solid-client-credentials-login.js.map +1 -0
  99. package/dist/lib/solid-local-store.js +31 -0
  100. package/dist/lib/solid-local-store.js.map +1 -0
  101. package/dist/lib/symphony/archive.js +328 -18
  102. package/dist/lib/symphony/archive.js.map +1 -1
  103. package/dist/lib/symphony/pod-projection.js +2222 -0
  104. package/dist/lib/symphony/pod-projection.js.map +1 -0
  105. package/dist/lib/symphony-command.js +602 -178
  106. package/dist/lib/symphony-command.js.map +1 -1
  107. package/dist/lib/sync-checkpoint-store.js +74 -0
  108. package/dist/lib/sync-checkpoint-store.js.map +1 -0
  109. package/dist/skills/symphony/SKILL.md +665 -0
  110. package/package.json +15 -9
  111. package/vendor/agent-runtime/dist/agent-runtime.d.ts +137 -0
  112. package/vendor/agent-runtime/dist/agent-runtime.js +211 -0
  113. package/vendor/agent-runtime/dist/auto-mode.d.ts +78 -13
  114. package/vendor/agent-runtime/dist/auto-mode.js +288 -31
  115. package/vendor/agent-runtime/dist/control-plane.d.ts +28 -0
  116. package/vendor/agent-runtime/dist/control-plane.js +79 -0
  117. package/vendor/agent-runtime/dist/file-sync.d.ts +157 -0
  118. package/vendor/agent-runtime/dist/file-sync.js +314 -0
  119. package/vendor/agent-runtime/dist/index.d.ts +7 -0
  120. package/vendor/agent-runtime/dist/index.js +7 -0
  121. package/vendor/agent-runtime/dist/reconciler.d.ts +117 -0
  122. package/vendor/agent-runtime/dist/reconciler.js +361 -0
  123. package/vendor/agent-runtime/dist/symphony.d.ts +128 -8
  124. package/vendor/agent-runtime/dist/symphony.js +362 -57
  125. package/vendor/agent-runtime/dist/sync.d.ts +271 -0
  126. package/vendor/agent-runtime/dist/sync.js +550 -0
  127. package/vendor/agent-runtime/dist/thread-reconciler-controller.d.ts +58 -0
  128. package/vendor/agent-runtime/dist/thread-reconciler-controller.js +137 -0
  129. package/vendor/agent-runtime/dist/turn-controller.js +2 -2
  130. package/vendor/agent-runtime/dist/wake-scheduler.d.ts +67 -0
  131. package/vendor/agent-runtime/dist/wake-scheduler.js +194 -0
  132. package/vendor/agent-runtime/package.json +8 -1
  133. package/vendor/pi-web-access/CHANGELOG.md +387 -0
  134. package/vendor/pi-web-access/LICENSE +21 -0
  135. package/vendor/pi-web-access/README.md +352 -0
  136. package/vendor/pi-web-access/activity.ts +101 -0
  137. package/vendor/pi-web-access/banner.png +0 -0
  138. package/vendor/pi-web-access/chrome-cookies.ts +322 -0
  139. package/vendor/pi-web-access/code-search.ts +107 -0
  140. package/vendor/pi-web-access/curator-page.ts +3359 -0
  141. package/vendor/pi-web-access/curator-server.ts +605 -0
  142. package/vendor/pi-web-access/exa.ts +520 -0
  143. package/vendor/pi-web-access/extract.ts +641 -0
  144. package/vendor/pi-web-access/gemini-api.ts +112 -0
  145. package/vendor/pi-web-access/gemini-search.ts +361 -0
  146. package/vendor/pi-web-access/gemini-url-context.ts +126 -0
  147. package/vendor/pi-web-access/gemini-web-config.ts +52 -0
  148. package/vendor/pi-web-access/gemini-web.ts +396 -0
  149. package/vendor/pi-web-access/github-api.ts +196 -0
  150. package/vendor/pi-web-access/github-extract.ts +634 -0
  151. package/vendor/pi-web-access/index.ts +2346 -0
  152. package/vendor/pi-web-access/package.json +45 -0
  153. package/vendor/pi-web-access/pdf-extract.ts +192 -0
  154. package/vendor/pi-web-access/perplexity.ts +195 -0
  155. package/vendor/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
  156. package/vendor/pi-web-access/rsc-extract.ts +338 -0
  157. package/vendor/pi-web-access/skills/librarian/SKILL.md +195 -0
  158. package/vendor/pi-web-access/storage.ts +72 -0
  159. package/vendor/pi-web-access/summary-review.ts +276 -0
  160. package/vendor/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +41 -0
  161. package/vendor/pi-web-access/test/pdf-extract.test.mjs +95 -0
  162. package/vendor/pi-web-access/utils.ts +44 -0
  163. package/vendor/pi-web-access/video-extract.ts +378 -0
  164. package/vendor/pi-web-access/youtube-extract.ts +310 -0
  165. package/dist/lib/pi-adapter/auth.js +0 -68
  166. package/dist/lib/pi-adapter/auth.js.map +0 -1
  167. package/dist/lib/pi-adapter/pod-tools.js +0 -140
  168. package/dist/lib/pi-adapter/pod-tools.js.map +0 -1
  169. package/dist/skills/drizzle-solid/SKILL.md +0 -340
  170. package/dist/skills/pod-storage/SKILL.md +0 -100
  171. package/dist/skills/solid-modeling/SKILL.md +0 -274
  172. package/dist/skills/xpod-componentsjs/SKILL.md +0 -284
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "pi-web-access",
3
+ "version": "0.10.7",
4
+ "description": "Web search, URL fetching, GitHub repo cloning, PDF extraction, YouTube video understanding, and local video analysis for Pi coding agent",
5
+ "type": "module",
6
+ "scripts": {
7
+ "test": "node --test"
8
+ },
9
+ "keywords": [
10
+ "pi-package",
11
+ "pi",
12
+ "pi-coding-agent",
13
+ "extension",
14
+ "web-search",
15
+ "perplexity",
16
+ "fetch",
17
+ "scraping"
18
+ ],
19
+ "author": "Nico Bailon",
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/nicobailon/pi-web-access.git"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/nicobailon/pi-web-access/issues"
27
+ },
28
+ "homepage": "https://github.com/nicobailon/pi-web-access#readme",
29
+ "dependencies": {
30
+ "@mozilla/readability": "^0.5.0",
31
+ "linkedom": "^0.16.0",
32
+ "p-limit": "^6.1.0",
33
+ "turndown": "^7.2.0",
34
+ "unpdf": "^1.6.2"
35
+ },
36
+ "pi": {
37
+ "extensions": [
38
+ "./index.ts"
39
+ ],
40
+ "skills": [
41
+ "./skills"
42
+ ],
43
+ "video": "https://github.com/nicobailon/pi-web-access/raw/refs/heads/main/pi-web-fetch-demo.mp4"
44
+ }
45
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * PDF Content Extractor
3
+ *
4
+ * Extracts text from PDF files and saves to markdown.
5
+ * Uses unpdf (pdfjs-dist wrapper) for text extraction.
6
+ */
7
+
8
+ import { getDocumentProxy } from "unpdf";
9
+ import { writeFile, mkdir } from "node:fs/promises";
10
+ import { join, basename } from "node:path";
11
+ import { homedir } from "node:os";
12
+
13
+ export interface PDFExtractResult {
14
+ title: string;
15
+ pages: number;
16
+ chars: number;
17
+ outputPath: string;
18
+ }
19
+
20
+ export interface PDFExtractOptions {
21
+ maxPages?: number;
22
+ outputDir?: string;
23
+ filename?: string;
24
+ }
25
+
26
+ const DEFAULT_MAX_PAGES = 100;
27
+ const DEFAULT_OUTPUT_DIR = join(homedir(), "Downloads");
28
+
29
+ /**
30
+ * Extract text from a PDF buffer and save to markdown file
31
+ */
32
+ export async function extractPDFToMarkdown(
33
+ buffer: ArrayBuffer,
34
+ url: string,
35
+ options: PDFExtractOptions = {}
36
+ ): Promise<PDFExtractResult> {
37
+ const {
38
+ maxPages = DEFAULT_MAX_PAGES,
39
+ outputDir = DEFAULT_OUTPUT_DIR,
40
+ filename,
41
+ } = options;
42
+
43
+ const safeMaxPages = Number.isFinite(maxPages)
44
+ ? Math.max(1, Math.floor(maxPages))
45
+ : DEFAULT_MAX_PAGES;
46
+
47
+ const pdf = await getDocumentProxy(new Uint8Array(buffer));
48
+ const metadata = await pdf.getMetadata();
49
+ const metadataInfo = metadata.info && typeof metadata.info === "object"
50
+ ? metadata.info as Record<string, unknown>
51
+ : null;
52
+
53
+ // Extract title from metadata or URL
54
+ const metaTitle = typeof metadataInfo?.Title === "string" ? metadataInfo.Title : undefined;
55
+ const metaAuthor = typeof metadataInfo?.Author === "string" ? metadataInfo.Author : undefined;
56
+ const urlTitle = extractTitleFromURL(url);
57
+ const title = metaTitle?.trim() || urlTitle;
58
+
59
+ // Determine pages to extract
60
+ const pagesToExtract = Math.min(pdf.numPages, safeMaxPages);
61
+ const truncated = pdf.numPages > safeMaxPages;
62
+
63
+ // Extract text page by page for better structure
64
+ const pages: { pageNum: number; text: string }[] = [];
65
+ for (let i = 1; i <= pagesToExtract; i++) {
66
+ const page = await pdf.getPage(i);
67
+ const textContent = await page.getTextContent();
68
+ const pageText = textContent.items
69
+ .map((item: unknown) => {
70
+ const textItem = item as { str?: string };
71
+ return textItem.str || "";
72
+ })
73
+ .join(" ")
74
+ .replace(/\s+/g, " ")
75
+ .trim();
76
+
77
+ if (pageText) {
78
+ pages.push({ pageNum: i, text: pageText });
79
+ }
80
+ }
81
+
82
+ // Build markdown content
83
+ const lines: string[] = [];
84
+
85
+ // Header with metadata
86
+ lines.push(`# ${title}`);
87
+ lines.push("");
88
+ lines.push(`> Source: ${url}`);
89
+ lines.push(`> Pages: ${pdf.numPages}${truncated ? ` (extracted first ${pagesToExtract})` : ""}`);
90
+ if (metaAuthor) {
91
+ lines.push(`> Author: ${metaAuthor}`);
92
+ }
93
+ lines.push("");
94
+ lines.push("---");
95
+ lines.push("");
96
+
97
+ // Content with page markers
98
+ for (let i = 0; i < pages.length; i++) {
99
+ if (i > 0) {
100
+ lines.push("");
101
+ lines.push(`<!-- Page ${pages[i].pageNum} -->`);
102
+ lines.push("");
103
+ }
104
+ lines.push(pages[i].text);
105
+ }
106
+
107
+ if (truncated) {
108
+ lines.push("");
109
+ lines.push("---");
110
+ lines.push("");
111
+ lines.push(`*[Truncated: Only first ${pagesToExtract} of ${pdf.numPages} pages extracted]*`);
112
+ }
113
+
114
+ const content = lines.join("\n");
115
+
116
+ // Generate output filename
117
+ const outputFilename = filename || sanitizeFilename(title) + ".md";
118
+ const outputPath = join(outputDir, outputFilename);
119
+
120
+ // Ensure output directory exists
121
+ await mkdir(outputDir, { recursive: true });
122
+
123
+ // Write file
124
+ await writeFile(outputPath, content, "utf-8");
125
+
126
+ return {
127
+ title,
128
+ pages: pdf.numPages,
129
+ chars: content.length,
130
+ outputPath,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Extract a reasonable title from URL
136
+ */
137
+ function extractTitleFromURL(url: string): string {
138
+ try {
139
+ const urlObj = new URL(url);
140
+ const pathname = urlObj.pathname;
141
+
142
+ // Get filename without extension
143
+ let filename = basename(pathname, ".pdf");
144
+
145
+ // Handle arxiv URLs: /pdf/1706.03762 → "arxiv-1706.03762"
146
+ if (urlObj.hostname.includes("arxiv.org")) {
147
+ const match = pathname.match(/\/(?:pdf|abs)\/(\d+\.\d+)/);
148
+ if (match) {
149
+ filename = `arxiv-${match[1]}`;
150
+ }
151
+ }
152
+
153
+ // Clean up filename
154
+ filename = filename
155
+ .replace(/[_-]+/g, " ")
156
+ .replace(/\s+/g, " ")
157
+ .trim();
158
+
159
+ return filename || "document";
160
+ } catch {
161
+ return "document";
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Sanitize string for use as filename
167
+ */
168
+ function sanitizeFilename(name: string): string {
169
+ return name
170
+ .toLowerCase()
171
+ .replace(/[^a-z0-9\s-]/g, "")
172
+ .replace(/\s+/g, "-")
173
+ .replace(/-+/g, "-")
174
+ .slice(0, 100)
175
+ .replace(/^-|-$/g, "")
176
+ || "document";
177
+ }
178
+
179
+ /**
180
+ * Check if URL or content-type indicates a PDF
181
+ */
182
+ export function isPDF(url: string, contentType?: string): boolean {
183
+ if (contentType?.includes("application/pdf")) {
184
+ return true;
185
+ }
186
+ try {
187
+ const urlObj = new URL(url);
188
+ return urlObj.pathname.toLowerCase().endsWith(".pdf");
189
+ } catch {
190
+ return false;
191
+ }
192
+ }
@@ -0,0 +1,195 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { activityMonitor } from "./activity.js";
5
+ import type { ExtractedContent } from "./extract.js";
6
+
7
+ const PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
8
+ const CONFIG_PATH = join(process.env.LINX_HOME?.trim() || join(process.env.SOLID_HOME?.trim() || join(homedir(), ".solid"), "apps", "linx"), "pi-web-access.json");
9
+
10
+ const RATE_LIMIT = {
11
+ maxRequests: 10,
12
+ windowMs: 60 * 1000,
13
+ };
14
+
15
+ const requestTimestamps: number[] = [];
16
+
17
+ export interface SearchResult {
18
+ title: string;
19
+ url: string;
20
+ snippet: string;
21
+ }
22
+
23
+ export interface SearchResponse {
24
+ answer: string;
25
+ results: SearchResult[];
26
+ inlineContent?: ExtractedContent[];
27
+ }
28
+
29
+ export interface SearchOptions {
30
+ numResults?: number;
31
+ recencyFilter?: "day" | "week" | "month" | "year";
32
+ domainFilter?: string[];
33
+ signal?: AbortSignal;
34
+ }
35
+
36
+ interface WebSearchConfig {
37
+ perplexityApiKey?: unknown;
38
+ }
39
+
40
+ let cachedConfig: WebSearchConfig | null = null;
41
+
42
+ function loadConfig(): WebSearchConfig {
43
+ if (cachedConfig) return cachedConfig;
44
+ if (!existsSync(CONFIG_PATH)) {
45
+ cachedConfig = {};
46
+ return cachedConfig;
47
+ }
48
+
49
+ const content = readFileSync(CONFIG_PATH, "utf-8");
50
+ try {
51
+ cachedConfig = JSON.parse(content) as WebSearchConfig;
52
+ return cachedConfig;
53
+ } catch (err) {
54
+ const message = err instanceof Error ? err.message : String(err);
55
+ throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
56
+ }
57
+ }
58
+
59
+ function normalizeApiKey(value: unknown): string | null {
60
+ if (typeof value !== "string") return null;
61
+ const normalized = value.trim();
62
+ return normalized.length > 0 ? normalized : null;
63
+ }
64
+
65
+ function getApiKey(): string {
66
+ const config = loadConfig();
67
+ const key = normalizeApiKey(process.env.PERPLEXITY_API_KEY) ?? normalizeApiKey(config.perplexityApiKey);
68
+ if (!key) {
69
+ throw new Error(
70
+ "Perplexity API key not found. Either:\n" +
71
+ ` 1. Create ${CONFIG_PATH} with { "perplexityApiKey": "your-key" }\n` +
72
+ " 2. Set PERPLEXITY_API_KEY environment variable\n" +
73
+ "Get a key at https://perplexity.ai/settings/api"
74
+ );
75
+ }
76
+ return key;
77
+ }
78
+
79
+ function checkRateLimit(): void {
80
+ const now = Date.now();
81
+ const windowStart = now - RATE_LIMIT.windowMs;
82
+
83
+ while (requestTimestamps.length > 0 && requestTimestamps[0] < windowStart) {
84
+ requestTimestamps.shift();
85
+ }
86
+
87
+ if (requestTimestamps.length >= RATE_LIMIT.maxRequests) {
88
+ const waitMs = requestTimestamps[0] + RATE_LIMIT.windowMs - now;
89
+ throw new Error(`Rate limited. Try again in ${Math.ceil(waitMs / 1000)}s`);
90
+ }
91
+
92
+ requestTimestamps.push(now);
93
+ }
94
+
95
+ function validateDomainFilter(domains: string[]): string[] {
96
+ return domains.filter((d) => {
97
+ const domain = d.startsWith("-") ? d.slice(1) : d;
98
+ return /^[a-zA-Z0-9][a-zA-Z0-9-_.]*\.[a-zA-Z]{2,}$/.test(domain);
99
+ });
100
+ }
101
+
102
+ export function isPerplexityAvailable(): boolean {
103
+ const config = loadConfig();
104
+ return !!(normalizeApiKey(process.env.PERPLEXITY_API_KEY) ?? normalizeApiKey(config.perplexityApiKey));
105
+ }
106
+
107
+ export async function searchWithPerplexity(query: string, options: SearchOptions = {}): Promise<SearchResponse> {
108
+ checkRateLimit();
109
+
110
+ const activityId = activityMonitor.logStart({ type: "api", query });
111
+
112
+ activityMonitor.updateRateLimit({
113
+ used: requestTimestamps.length,
114
+ max: RATE_LIMIT.maxRequests,
115
+ oldestTimestamp: requestTimestamps[0] ?? null,
116
+ windowMs: RATE_LIMIT.windowMs,
117
+ });
118
+
119
+ const apiKey = getApiKey();
120
+ const numResults = Math.min(options.numResults ?? 5, 20);
121
+
122
+ const requestBody: Record<string, unknown> = {
123
+ model: "sonar",
124
+ messages: [{ role: "user", content: query }],
125
+ max_tokens: 1024,
126
+ return_related_questions: false,
127
+ };
128
+
129
+ if (options.recencyFilter) {
130
+ requestBody.search_recency_filter = options.recencyFilter;
131
+ }
132
+
133
+ if (options.domainFilter && options.domainFilter.length > 0) {
134
+ const validated = validateDomainFilter(options.domainFilter);
135
+ if (validated.length > 0) {
136
+ requestBody.search_domain_filter = validated;
137
+ }
138
+ }
139
+
140
+ let response: Response;
141
+ try {
142
+ response = await fetch(PERPLEXITY_API_URL, {
143
+ method: "POST",
144
+ headers: {
145
+ Authorization: `Bearer ${apiKey}`,
146
+ "Content-Type": "application/json",
147
+ },
148
+ body: JSON.stringify(requestBody),
149
+ signal: options.signal,
150
+ });
151
+ } catch (err) {
152
+ const message = err instanceof Error ? err.message : String(err);
153
+ if (message.toLowerCase().includes("abort")) {
154
+ activityMonitor.logComplete(activityId, 0);
155
+ } else {
156
+ activityMonitor.logError(activityId, message);
157
+ }
158
+ throw err;
159
+ }
160
+
161
+ if (!response.ok) {
162
+ activityMonitor.logComplete(activityId, response.status);
163
+ const errorText = await response.text();
164
+ throw new Error(`Perplexity API error ${response.status}: ${errorText}`);
165
+ }
166
+
167
+ let data: Record<string, unknown>;
168
+ try {
169
+ data = await response.json();
170
+ } catch (err) {
171
+ activityMonitor.logComplete(activityId, response.status);
172
+ const message = err instanceof Error ? err.message : String(err);
173
+ throw new Error(`Perplexity API returned invalid JSON: ${message}`);
174
+ }
175
+
176
+ const answer = (data.choices as Array<{ message?: { content?: string } }>)?.[0]?.message?.content || "";
177
+ const citations = Array.isArray(data.citations) ? data.citations : [];
178
+
179
+ const results: SearchResult[] = [];
180
+ for (let i = 0; i < Math.min(citations.length, numResults); i++) {
181
+ const citation = citations[i];
182
+ if (typeof citation === "string") {
183
+ results.push({ title: `Source ${i + 1}`, url: citation, snippet: "" });
184
+ } else if (citation && typeof citation === "object" && typeof citation.url === "string") {
185
+ results.push({
186
+ title: citation.title || `Source ${i + 1}`,
187
+ url: citation.url,
188
+ snippet: "",
189
+ });
190
+ }
191
+ }
192
+
193
+ activityMonitor.logComplete(activityId, response.status);
194
+ return { answer, results };
195
+ }