@undefineds.co/linx 0.3.5 → 0.3.8
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.
- package/README.md +58 -23
- package/dist/generated/version.js +1 -1
- package/dist/generated/version.js.map +1 -1
- package/dist/index.js +336 -162
- package/dist/index.js.map +1 -1
- package/dist/lib/account-session.js +4 -8
- package/dist/lib/account-session.js.map +1 -1
- package/dist/lib/ai-command.js +228 -178
- package/dist/lib/ai-command.js.map +1 -1
- package/dist/lib/auto-mode/archive.js +38 -7
- package/dist/lib/auto-mode/archive.js.map +1 -1
- package/dist/lib/auto-mode/auth.js.map +1 -1
- package/dist/lib/auto-mode/display.js +71 -45
- package/dist/lib/auto-mode/display.js.map +1 -1
- package/dist/lib/auto-mode/format.js +9 -7
- package/dist/lib/auto-mode/format.js.map +1 -1
- package/dist/lib/auto-mode/hooks/claude.js +12 -2
- package/dist/lib/auto-mode/hooks/claude.js.map +1 -1
- package/dist/lib/auto-mode/hooks/codex.js +17 -7
- package/dist/lib/auto-mode/hooks/codex.js.map +1 -1
- package/dist/lib/auto-mode/hooks/index.js +28 -8
- package/dist/lib/auto-mode/hooks/index.js.map +1 -1
- package/dist/lib/auto-mode/pod-ai.js +20 -37
- package/dist/lib/auto-mode/pod-ai.js.map +1 -1
- package/dist/lib/auto-mode/pod-approval.js +124 -195
- package/dist/lib/auto-mode/pod-approval.js.map +1 -1
- package/dist/lib/auto-mode/pod-persistence.js +169 -90
- package/dist/lib/auto-mode/pod-persistence.js.map +1 -1
- package/dist/lib/auto-mode/runner.js +683 -81
- package/dist/lib/auto-mode/runner.js.map +1 -1
- package/dist/lib/auto-mode/secretary.js +186 -41
- package/dist/lib/auto-mode/secretary.js.map +1 -1
- package/dist/lib/auto-mode-command.js +32 -32
- package/dist/lib/auto-mode-command.js.map +1 -1
- package/dist/lib/chat-api.js +242 -50
- package/dist/lib/chat-api.js.map +1 -1
- package/dist/lib/codex-plugin/bridge.js +164 -17
- package/dist/lib/codex-plugin/bridge.js.map +1 -1
- package/dist/lib/codex-plugin/codex-native-proxy.js +370 -34
- package/dist/lib/codex-plugin/codex-native-proxy.js.map +1 -1
- package/dist/lib/credentials-store.js +33 -42
- package/dist/lib/credentials-store.js.map +1 -1
- package/dist/lib/linx-cloud-errors.js +61 -0
- package/dist/lib/linx-cloud-errors.js.map +1 -0
- package/dist/lib/linx-tui-contract.js +8 -5
- package/dist/lib/linx-tui-contract.js.map +1 -1
- package/dist/lib/login-command.js +9 -2
- package/dist/lib/login-command.js.map +1 -1
- package/dist/lib/models.js +3 -20
- package/dist/lib/models.js.map +1 -1
- package/dist/lib/oidc-auth.js +143 -17
- package/dist/lib/oidc-auth.js.map +1 -1
- package/dist/lib/oidc-session-storage.js +2 -6
- package/dist/lib/oidc-session-storage.js.map +1 -1
- package/dist/lib/pi-adapter/auto-input-controller.js +988 -0
- package/dist/lib/pi-adapter/auto-input-controller.js.map +1 -0
- package/dist/lib/pi-adapter/backend-command.js +2 -0
- package/dist/lib/pi-adapter/backend-command.js.map +1 -0
- package/dist/lib/pi-adapter/backend-credentials.js +80 -0
- package/dist/lib/pi-adapter/backend-credentials.js.map +1 -0
- package/dist/lib/pi-adapter/branding.js +246 -108
- package/dist/lib/pi-adapter/branding.js.map +1 -1
- package/dist/lib/pi-adapter/control-state.js +72 -0
- package/dist/lib/pi-adapter/control-state.js.map +1 -0
- package/dist/lib/pi-adapter/interactive.js +2634 -30
- package/dist/lib/pi-adapter/interactive.js.map +1 -1
- package/dist/lib/pi-adapter/pod-approval.js +382 -210
- package/dist/lib/pi-adapter/pod-approval.js.map +1 -1
- package/dist/lib/pi-adapter/pod-mirror-mapping.js +71 -17
- package/dist/lib/pi-adapter/pod-mirror-mapping.js.map +1 -1
- package/dist/lib/pi-adapter/pod-mirror.js +531 -64
- package/dist/lib/pi-adapter/pod-mirror.js.map +1 -1
- package/dist/lib/pi-adapter/pod-native.js +81 -85
- package/dist/lib/pi-adapter/pod-native.js.map +1 -1
- package/dist/lib/pi-adapter/pod-status-output.js +54 -0
- package/dist/lib/pi-adapter/pod-status-output.js.map +1 -0
- package/dist/lib/pi-adapter/runtime.js +458 -228
- package/dist/lib/pi-adapter/runtime.js.map +1 -1
- package/dist/lib/pi-adapter/session-control.js +509 -0
- package/dist/lib/pi-adapter/session-control.js.map +1 -0
- package/dist/lib/pi-adapter/session.js +35 -22
- package/dist/lib/pi-adapter/session.js.map +1 -1
- package/dist/lib/pi-adapter/stream.js +89 -32
- package/dist/lib/pi-adapter/stream.js.map +1 -1
- package/dist/lib/pi-adapter/sync-recovery.js +89 -0
- package/dist/lib/pi-adapter/sync-recovery.js.map +1 -0
- package/dist/lib/pi-adapter/web-fetch.js +13 -14
- package/dist/lib/pi-adapter/web-fetch.js.map +1 -1
- package/dist/lib/pod-chat-store.js +254 -78
- package/dist/lib/pod-chat-store.js.map +1 -1
- package/dist/lib/pod-data-session.js +156 -35
- package/dist/lib/pod-data-session.js.map +1 -1
- package/dist/lib/solid-auth-store.js +27 -0
- package/dist/lib/solid-auth-store.js.map +1 -0
- package/dist/lib/solid-auth.js +2 -4
- package/dist/lib/solid-auth.js.map +1 -1
- package/dist/lib/solid-client-credentials-login.js +100 -0
- package/dist/lib/solid-client-credentials-login.js.map +1 -0
- package/dist/lib/solid-local-store.js +31 -0
- package/dist/lib/solid-local-store.js.map +1 -0
- package/dist/lib/symphony/archive.js +328 -18
- package/dist/lib/symphony/archive.js.map +1 -1
- package/dist/lib/symphony/pod-projection.js +2222 -0
- package/dist/lib/symphony/pod-projection.js.map +1 -0
- package/dist/lib/symphony-command.js +602 -178
- package/dist/lib/symphony-command.js.map +1 -1
- package/dist/lib/sync-checkpoint-store.js +74 -0
- package/dist/lib/sync-checkpoint-store.js.map +1 -0
- package/dist/skills/symphony/SKILL.md +665 -0
- package/package.json +15 -9
- package/vendor/agent-runtime/dist/agent-runtime.d.ts +137 -0
- package/vendor/agent-runtime/dist/agent-runtime.js +211 -0
- package/vendor/agent-runtime/dist/auto-mode.d.ts +78 -13
- package/vendor/agent-runtime/dist/auto-mode.js +288 -31
- package/vendor/agent-runtime/dist/control-plane.d.ts +28 -0
- package/vendor/agent-runtime/dist/control-plane.js +79 -0
- package/vendor/agent-runtime/dist/file-sync.d.ts +157 -0
- package/vendor/agent-runtime/dist/file-sync.js +314 -0
- package/vendor/agent-runtime/dist/index.d.ts +7 -0
- package/vendor/agent-runtime/dist/index.js +7 -0
- package/vendor/agent-runtime/dist/reconciler.d.ts +117 -0
- package/vendor/agent-runtime/dist/reconciler.js +361 -0
- package/vendor/agent-runtime/dist/symphony.d.ts +128 -8
- package/vendor/agent-runtime/dist/symphony.js +362 -57
- package/vendor/agent-runtime/dist/sync.d.ts +271 -0
- package/vendor/agent-runtime/dist/sync.js +550 -0
- package/vendor/agent-runtime/dist/thread-reconciler-controller.d.ts +58 -0
- package/vendor/agent-runtime/dist/thread-reconciler-controller.js +137 -0
- package/vendor/agent-runtime/dist/turn-controller.js +2 -2
- package/vendor/agent-runtime/dist/wake-scheduler.d.ts +67 -0
- package/vendor/agent-runtime/dist/wake-scheduler.js +194 -0
- package/vendor/agent-runtime/package.json +8 -1
- package/vendor/pi-web-access/CHANGELOG.md +387 -0
- package/vendor/pi-web-access/LICENSE +21 -0
- package/vendor/pi-web-access/README.md +352 -0
- package/vendor/pi-web-access/activity.ts +101 -0
- package/vendor/pi-web-access/banner.png +0 -0
- package/vendor/pi-web-access/chrome-cookies.ts +322 -0
- package/vendor/pi-web-access/code-search.ts +107 -0
- package/vendor/pi-web-access/curator-page.ts +3359 -0
- package/vendor/pi-web-access/curator-server.ts +605 -0
- package/vendor/pi-web-access/exa.ts +520 -0
- package/vendor/pi-web-access/extract.ts +641 -0
- package/vendor/pi-web-access/gemini-api.ts +112 -0
- package/vendor/pi-web-access/gemini-search.ts +361 -0
- package/vendor/pi-web-access/gemini-url-context.ts +126 -0
- package/vendor/pi-web-access/gemini-web-config.ts +52 -0
- package/vendor/pi-web-access/gemini-web.ts +396 -0
- package/vendor/pi-web-access/github-api.ts +196 -0
- package/vendor/pi-web-access/github-extract.ts +634 -0
- package/vendor/pi-web-access/index.ts +2346 -0
- package/vendor/pi-web-access/package.json +45 -0
- package/vendor/pi-web-access/pdf-extract.ts +192 -0
- package/vendor/pi-web-access/perplexity.ts +195 -0
- package/vendor/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
- package/vendor/pi-web-access/rsc-extract.ts +338 -0
- package/vendor/pi-web-access/skills/librarian/SKILL.md +195 -0
- package/vendor/pi-web-access/storage.ts +72 -0
- package/vendor/pi-web-access/summary-review.ts +276 -0
- package/vendor/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +41 -0
- package/vendor/pi-web-access/test/pdf-extract.test.mjs +95 -0
- package/vendor/pi-web-access/utils.ts +44 -0
- package/vendor/pi-web-access/video-extract.ts +378 -0
- package/vendor/pi-web-access/youtube-extract.ts +310 -0
- package/dist/lib/pi-adapter/auth.js +0 -68
- package/dist/lib/pi-adapter/auth.js.map +0 -1
- package/dist/lib/pi-adapter/pod-tools.js +0 -140
- package/dist/lib/pi-adapter/pod-tools.js.map +0 -1
- package/dist/skills/drizzle-solid/SKILL.md +0 -340
- package/dist/skills/pod-storage/SKILL.md +0 -100
- package/dist/skills/solid-modeling/SKILL.md +0 -274
- package/dist/skills/xpod-componentsjs/SKILL.md +0 -284
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RSC Content Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extracts readable content from Next.js React Server Components (RSC) flight payloads.
|
|
5
|
+
* RSC pages embed content as JSON in <script>self.__next_f.push([...])</script> tags.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface RSCExtractResult {
|
|
9
|
+
title: string;
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function extractRSCContent(html: string): RSCExtractResult | null {
|
|
14
|
+
if (!html.includes("self.__next_f.push")) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Parse all RSC chunks into a map
|
|
19
|
+
const chunkMap = new Map<string, string>();
|
|
20
|
+
const scriptRegex = /<script>self\.__next_f\.push\(\[1,"([\s\S]*?)"\]\)<\/script>/g;
|
|
21
|
+
|
|
22
|
+
for (const match of html.matchAll(scriptRegex)) {
|
|
23
|
+
let content: string;
|
|
24
|
+
try {
|
|
25
|
+
content = JSON.parse('"' + match[1] + '"');
|
|
26
|
+
} catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Parse each line as "id:payload"
|
|
31
|
+
// Lines are separated by \n, each line is one chunk
|
|
32
|
+
// Chunk IDs are hex strings, typically 1-4 chars (supports up to 65535 chunks)
|
|
33
|
+
for (const line of content.split("\n")) {
|
|
34
|
+
if (!line.trim()) continue;
|
|
35
|
+
|
|
36
|
+
const colonIdx = line.indexOf(":");
|
|
37
|
+
if (colonIdx <= 0 || colonIdx > 4) continue;
|
|
38
|
+
|
|
39
|
+
const id = line.slice(0, colonIdx);
|
|
40
|
+
if (!/^[0-9a-f]+$/i.test(id)) continue;
|
|
41
|
+
|
|
42
|
+
const payload = line.slice(colonIdx + 1);
|
|
43
|
+
if (!payload) continue;
|
|
44
|
+
|
|
45
|
+
const existing = chunkMap.get(id);
|
|
46
|
+
if (!existing || payload.length > existing.length) {
|
|
47
|
+
chunkMap.set(id, payload);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (chunkMap.size === 0) return null;
|
|
53
|
+
|
|
54
|
+
// Extract title
|
|
55
|
+
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/);
|
|
56
|
+
const title = titleMatch?.[1]?.split("|")[0]?.trim() || "";
|
|
57
|
+
|
|
58
|
+
// Parse and cache parsed chunks
|
|
59
|
+
const parsedCache = new Map<string, unknown>();
|
|
60
|
+
|
|
61
|
+
function getParsedChunk(id: string): unknown | null {
|
|
62
|
+
if (parsedCache.has(id)) return parsedCache.get(id);
|
|
63
|
+
|
|
64
|
+
const chunk = chunkMap.get(id);
|
|
65
|
+
if (!chunk || !chunk.startsWith("[")) {
|
|
66
|
+
parsedCache.set(id, null);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(chunk);
|
|
72
|
+
parsedCache.set(id, parsed);
|
|
73
|
+
return parsed;
|
|
74
|
+
} catch {
|
|
75
|
+
parsedCache.set(id, null);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Extract markdown from nodes, resolving refs on the fly
|
|
81
|
+
type Node = unknown;
|
|
82
|
+
const visitedRefs = new Set<string>();
|
|
83
|
+
|
|
84
|
+
function extractNode(node: Node, ctx = { inTable: false, inCode: false }): string {
|
|
85
|
+
if (node === null || node === undefined) return "";
|
|
86
|
+
|
|
87
|
+
if (typeof node === "string") {
|
|
88
|
+
// Check if it's a reference like "$L30"
|
|
89
|
+
const refMatch = node.match(/^\$L([0-9a-f]+)$/i);
|
|
90
|
+
if (refMatch) {
|
|
91
|
+
const refId = refMatch[1];
|
|
92
|
+
if (visitedRefs.has(refId)) return ""; // Prevent cycles
|
|
93
|
+
visitedRefs.add(refId);
|
|
94
|
+
const refNode = getParsedChunk(refId);
|
|
95
|
+
const result = refNode ? extractNode(refNode, ctx) : "";
|
|
96
|
+
visitedRefs.delete(refId);
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
// Filter out RSC-specific artifacts, but preserve content inside code blocks
|
|
100
|
+
if (!ctx.inCode && (node === "$undefined" || node === "$" || /^\$[A-Z]/.test(node))) return "";
|
|
101
|
+
return node.trim() ? node : "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof node === "number") return String(node);
|
|
105
|
+
if (typeof node === "boolean") return "";
|
|
106
|
+
if (!Array.isArray(node)) return "";
|
|
107
|
+
|
|
108
|
+
// RSC element: ["$", "tag", key, props]
|
|
109
|
+
if (node[0] === "$" && typeof node[1] === "string") {
|
|
110
|
+
const tag = node[1] as string;
|
|
111
|
+
const props = (node[3] || {}) as Record<string, unknown>;
|
|
112
|
+
|
|
113
|
+
// Skip non-content
|
|
114
|
+
const skipTags = ["script", "style", "svg", "path", "circle", "link", "meta",
|
|
115
|
+
"template", "button", "input", "nav", "footer", "aside"];
|
|
116
|
+
if (skipTags.includes(tag)) return "";
|
|
117
|
+
|
|
118
|
+
// Component ref like $L25
|
|
119
|
+
if (tag.startsWith("$L")) {
|
|
120
|
+
const refId = tag.slice(2);
|
|
121
|
+
if (visitedRefs.has(refId)) return "";
|
|
122
|
+
|
|
123
|
+
// Check for heading components with baseId
|
|
124
|
+
if (props.baseId && props.children) {
|
|
125
|
+
return `## ${String(props.children)}\n\n`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
visitedRefs.add(refId);
|
|
129
|
+
const refNode = getParsedChunk(refId);
|
|
130
|
+
let result = "";
|
|
131
|
+
if (refNode) {
|
|
132
|
+
result = extractNode(refNode, ctx);
|
|
133
|
+
} else if (props.children) {
|
|
134
|
+
result = extractNode(props.children as Node, ctx);
|
|
135
|
+
}
|
|
136
|
+
visitedRefs.delete(refId);
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const children = props.children;
|
|
141
|
+
const content = children ? extractNode(children as Node, ctx) : "";
|
|
142
|
+
|
|
143
|
+
switch (tag) {
|
|
144
|
+
case "h1": return `# ${content.trim()}\n\n`;
|
|
145
|
+
case "h2": return `## ${content.trim()}\n\n`;
|
|
146
|
+
case "h3": return `### ${content.trim()}\n\n`;
|
|
147
|
+
case "h4": return `#### ${content.trim()}\n\n`;
|
|
148
|
+
case "h5": return `##### ${content.trim()}\n\n`;
|
|
149
|
+
case "h6": return `###### ${content.trim()}\n\n`;
|
|
150
|
+
case "p": return ctx.inTable ? content : `${content.trim()}\n\n`;
|
|
151
|
+
case "code": {
|
|
152
|
+
const codeContent = children ? extractNode(children as Node, { ...ctx, inCode: true }) : "";
|
|
153
|
+
return ctx.inCode ? codeContent : `\`${codeContent}\``;
|
|
154
|
+
}
|
|
155
|
+
case "pre": {
|
|
156
|
+
const preContent = children ? extractNode(children as Node, { ...ctx, inCode: true }) : "";
|
|
157
|
+
return "```\n" + preContent + "\n```\n\n";
|
|
158
|
+
}
|
|
159
|
+
case "strong": case "b": return `**${content}**`;
|
|
160
|
+
case "em": case "i": return `*${content}*`;
|
|
161
|
+
case "li": return `- ${content.trim()}\n`;
|
|
162
|
+
case "ul": case "ol": return content + "\n";
|
|
163
|
+
case "blockquote": return `> ${content.trim()}\n\n`;
|
|
164
|
+
case "table": return extractTable(node as unknown[]) + "\n";
|
|
165
|
+
case "thead": case "tbody": case "tr": case "th": case "td":
|
|
166
|
+
return content;
|
|
167
|
+
case "div":
|
|
168
|
+
if (props.role === "alert" || props["data-slot"] === "alert") {
|
|
169
|
+
return `> ${content.trim()}\n\n`;
|
|
170
|
+
}
|
|
171
|
+
return content;
|
|
172
|
+
case "a": {
|
|
173
|
+
const href = props.href as string | undefined;
|
|
174
|
+
return href && !href.startsWith("#") ? `[${content}](${href})` : content;
|
|
175
|
+
}
|
|
176
|
+
default: return content;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Array of child nodes
|
|
181
|
+
return (node as Node[]).map(n => extractNode(n, ctx)).join("");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function extractTable(tableNode: unknown[]): string {
|
|
185
|
+
const props = (tableNode[3] || {}) as Record<string, unknown>;
|
|
186
|
+
const rows: string[][] = [];
|
|
187
|
+
let headerRowCount = 0;
|
|
188
|
+
|
|
189
|
+
function walkTable(node: unknown, isHeader = false): void {
|
|
190
|
+
if (node === null || node === undefined) return;
|
|
191
|
+
|
|
192
|
+
// Handle string refs
|
|
193
|
+
if (typeof node === "string") {
|
|
194
|
+
const refMatch = node.match(/^\$L([0-9a-f]+)$/i);
|
|
195
|
+
if (refMatch && !visitedRefs.has(refMatch[1])) {
|
|
196
|
+
visitedRefs.add(refMatch[1]);
|
|
197
|
+
const refNode = getParsedChunk(refMatch[1]);
|
|
198
|
+
if (refNode) walkTable(refNode, isHeader);
|
|
199
|
+
visitedRefs.delete(refMatch[1]);
|
|
200
|
+
}
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!Array.isArray(node)) return;
|
|
205
|
+
|
|
206
|
+
if (node[0] === "$") {
|
|
207
|
+
const tag = node[1] as string;
|
|
208
|
+
const nodeProps = (node[3] || {}) as Record<string, unknown>;
|
|
209
|
+
|
|
210
|
+
// Handle component refs
|
|
211
|
+
if (tag.startsWith("$L")) {
|
|
212
|
+
const refId = tag.slice(2);
|
|
213
|
+
if (!visitedRefs.has(refId)) {
|
|
214
|
+
visitedRefs.add(refId);
|
|
215
|
+
const refNode = getParsedChunk(refId);
|
|
216
|
+
if (refNode) walkTable(refNode, isHeader);
|
|
217
|
+
visitedRefs.delete(refId);
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (tag === "thead") walkTable(nodeProps.children, true);
|
|
223
|
+
else if (tag === "tbody") walkTable(nodeProps.children, false);
|
|
224
|
+
else if (tag === "tr") {
|
|
225
|
+
const cells: string[] = [];
|
|
226
|
+
walkCells(nodeProps.children, cells);
|
|
227
|
+
if (cells.length > 0) {
|
|
228
|
+
rows.push(cells);
|
|
229
|
+
if (isHeader) headerRowCount++;
|
|
230
|
+
}
|
|
231
|
+
} else walkTable(nodeProps.children, isHeader);
|
|
232
|
+
} else {
|
|
233
|
+
for (const child of node) walkTable(child, isHeader);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function walkCells(node: unknown, cells: string[]): void {
|
|
238
|
+
if (node === null || node === undefined) return;
|
|
239
|
+
|
|
240
|
+
// Handle string refs
|
|
241
|
+
if (typeof node === "string") {
|
|
242
|
+
const refMatch = node.match(/^\$L([0-9a-f]+)$/i);
|
|
243
|
+
if (refMatch && !visitedRefs.has(refMatch[1])) {
|
|
244
|
+
visitedRefs.add(refMatch[1]);
|
|
245
|
+
const refNode = getParsedChunk(refMatch[1]);
|
|
246
|
+
if (refNode) walkCells(refNode, cells);
|
|
247
|
+
visitedRefs.delete(refMatch[1]);
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!Array.isArray(node)) return;
|
|
253
|
+
|
|
254
|
+
if (node[0] === "$" && (node[1] === "td" || node[1] === "th")) {
|
|
255
|
+
const cellProps = (node[3] || {}) as Record<string, unknown>;
|
|
256
|
+
const text = extractNode(cellProps.children, { inTable: true, inCode: false })
|
|
257
|
+
.trim()
|
|
258
|
+
.replace(/\n/g, " ")
|
|
259
|
+
.replace(/\\/g, "\\\\") // Escape backslashes first
|
|
260
|
+
.replace(/\|/g, "\\|"); // Then escape pipes
|
|
261
|
+
cells.push(text);
|
|
262
|
+
} else if (node[0] === "$" && typeof node[1] === "string" && (node[1] as string).startsWith("$L")) {
|
|
263
|
+
// Component ref for a cell
|
|
264
|
+
const refId = (node[1] as string).slice(2);
|
|
265
|
+
if (!visitedRefs.has(refId)) {
|
|
266
|
+
visitedRefs.add(refId);
|
|
267
|
+
const refNode = getParsedChunk(refId);
|
|
268
|
+
if (refNode) walkCells(refNode, cells);
|
|
269
|
+
visitedRefs.delete(refId);
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
for (const child of node) walkCells(child, cells);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
walkTable(props.children);
|
|
277
|
+
if (rows.length === 0) return "";
|
|
278
|
+
|
|
279
|
+
const colCount = Math.max(...rows.map(r => r.length));
|
|
280
|
+
let md = "";
|
|
281
|
+
for (let i = 0; i < rows.length; i++) {
|
|
282
|
+
const row = rows[i].concat(Array(colCount - rows[i].length).fill(""));
|
|
283
|
+
md += "| " + row.join(" | ") + " |\n";
|
|
284
|
+
if (i === headerRowCount - 1 || (headerRowCount === 0 && i === 0)) {
|
|
285
|
+
md += "| " + Array(colCount).fill("---").join(" | ") + " |\n";
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return md;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Process main content chunk (usually "23")
|
|
292
|
+
const mainChunk = getParsedChunk("23");
|
|
293
|
+
|
|
294
|
+
if (mainChunk) {
|
|
295
|
+
const content = extractNode(mainChunk);
|
|
296
|
+
if (content.trim().length > 100) {
|
|
297
|
+
const cleaned = content
|
|
298
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
299
|
+
.trim();
|
|
300
|
+
return { title, content: cleaned };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Fallback: try other chunks
|
|
305
|
+
const contentParts: { order: number; text: string }[] = [];
|
|
306
|
+
|
|
307
|
+
for (const [id] of chunkMap) {
|
|
308
|
+
if (id === "23") continue;
|
|
309
|
+
const parsed = getParsedChunk(id);
|
|
310
|
+
if (!parsed) continue;
|
|
311
|
+
|
|
312
|
+
visitedRefs.clear();
|
|
313
|
+
const text = extractNode(parsed);
|
|
314
|
+
|
|
315
|
+
if (text.trim().length > 50 &&
|
|
316
|
+
!text.includes("page was not found") &&
|
|
317
|
+
!text.includes("404")) {
|
|
318
|
+
contentParts.push({ order: parseInt(id, 16), text: text.trim() });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (contentParts.length === 0) return null;
|
|
323
|
+
|
|
324
|
+
contentParts.sort((a, b) => a.order - b.order);
|
|
325
|
+
|
|
326
|
+
const seen = new Set<string>();
|
|
327
|
+
const uniqueParts: string[] = [];
|
|
328
|
+
for (const part of contentParts) {
|
|
329
|
+
const key = part.text.slice(0, 150);
|
|
330
|
+
if (!seen.has(key)) {
|
|
331
|
+
seen.add(key);
|
|
332
|
+
uniqueParts.push(part.text);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const content = uniqueParts.join("\n\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
337
|
+
return content.length > 100 ? { title, content } : null;
|
|
338
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: librarian
|
|
3
|
+
description: Research open-source libraries with evidence-backed answers and GitHub permalinks. Use when the user asks about library internals, needs implementation details with source code references, wants to understand why something was changed, or needs authoritative answers backed by actual code. Excels at navigating large open-source repos and providing citations to exact lines of code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Librarian
|
|
7
|
+
|
|
8
|
+
Answer questions about open-source libraries by finding evidence with GitHub permalinks. Every claim backed by actual code.
|
|
9
|
+
|
|
10
|
+
## Execution Model
|
|
11
|
+
|
|
12
|
+
Pi executes tool calls sequentially, even when you emit multiple calls in one turn. But batching independent calls in a single turn still saves LLM round-trips (~5-10s each). Use these patterns:
|
|
13
|
+
|
|
14
|
+
| Pattern | When | Actually parallel? |
|
|
15
|
+
|---------|------|-------------------|
|
|
16
|
+
| Batch tool calls in one turn | Independent ops (web_search + fetch_content + read) | No, but saves round-trips |
|
|
17
|
+
| `fetch_content({ urls: [...] })` | Multiple URLs to fetch | Yes (3 concurrent) |
|
|
18
|
+
| Bash with `&` + `wait` | Multiple git/gh commands | Yes (OS-level) |
|
|
19
|
+
|
|
20
|
+
## Step 1: Classify the Request
|
|
21
|
+
|
|
22
|
+
Before doing anything, classify the request to pick the right research strategy.
|
|
23
|
+
|
|
24
|
+
| Type | Trigger | Primary Approach |
|
|
25
|
+
|------|---------|-----------------|
|
|
26
|
+
| **Conceptual** | "How do I use X?", "Best practice for Y?" | web_search + fetch_content (README/docs) |
|
|
27
|
+
| **Implementation** | "How does X implement Y?", "Show me the source" | fetch_content (clone) + code search |
|
|
28
|
+
| **Context/History** | "Why was this changed?", "History of X?" | git log + git blame + issue/PR search |
|
|
29
|
+
| **Comprehensive** | Complex or ambiguous requests, "deep dive" | All of the above |
|
|
30
|
+
|
|
31
|
+
## Step 2: Research by Type
|
|
32
|
+
|
|
33
|
+
### Conceptual Questions
|
|
34
|
+
|
|
35
|
+
Batch these in one turn:
|
|
36
|
+
|
|
37
|
+
1. **web_search**: `"library-name topic"` via Perplexity for recent articles and discussions
|
|
38
|
+
2. **fetch_content**: the library's GitHub repo URL to clone and check README, docs, or examples
|
|
39
|
+
|
|
40
|
+
Synthesize web results + repo docs. Cite official documentation and link to relevant source files.
|
|
41
|
+
|
|
42
|
+
### Implementation Questions
|
|
43
|
+
|
|
44
|
+
The core workflow -- clone, find, permalink:
|
|
45
|
+
|
|
46
|
+
1. **fetch_content** the GitHub repo URL -- this clones it locally and returns the file tree
|
|
47
|
+
2. Use **bash** to search the cloned repo: `grep -rn "function_name"`, `find . -name "*.ts"`
|
|
48
|
+
3. Use **read** to examine specific files once you've located them
|
|
49
|
+
4. Get the commit SHA: `cd /tmp/pi-github-repos/owner/repo && git rev-parse HEAD`
|
|
50
|
+
5. Construct permalink: `https://github.com/owner/repo/blob/<sha>/path/to/file#L10-L20`
|
|
51
|
+
|
|
52
|
+
Batch the initial calls: fetch_content (clone) + web_search (recent discussions) in one turn. Then dig into the clone with grep/read once it's available.
|
|
53
|
+
|
|
54
|
+
### Context/History Questions
|
|
55
|
+
|
|
56
|
+
Use git operations on the cloned repo:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cd /tmp/pi-github-repos/owner/repo
|
|
60
|
+
|
|
61
|
+
# Recent changes to a specific file
|
|
62
|
+
git log --oneline -n 20 -- path/to/file.ts
|
|
63
|
+
|
|
64
|
+
# Who changed what and when
|
|
65
|
+
git blame -L 10,30 path/to/file.ts
|
|
66
|
+
|
|
67
|
+
# Full diff for a specific commit
|
|
68
|
+
git show <sha> -- path/to/file.ts
|
|
69
|
+
|
|
70
|
+
# Search commit messages
|
|
71
|
+
git log --oneline --grep="keyword" -n 10
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
For issues and PRs, use bash:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Search issues
|
|
78
|
+
gh search issues "keyword" --repo owner/repo --state all --limit 10
|
|
79
|
+
|
|
80
|
+
# Search merged PRs
|
|
81
|
+
gh search prs "keyword" --repo owner/repo --state merged --limit 10
|
|
82
|
+
|
|
83
|
+
# View specific issue/PR with comments
|
|
84
|
+
gh issue view <number> --repo owner/repo --comments
|
|
85
|
+
gh pr view <number> --repo owner/repo --comments
|
|
86
|
+
|
|
87
|
+
# Release notes
|
|
88
|
+
gh api repos/owner/repo/releases --jq '.[0:5] | .[].tag_name'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Comprehensive Research
|
|
92
|
+
|
|
93
|
+
Combine everything. Batch these in one turn:
|
|
94
|
+
|
|
95
|
+
1. **web_search**: recent articles and discussions
|
|
96
|
+
2. **fetch_content**: clone the repo (or multiple repos if comparing)
|
|
97
|
+
3. **bash**: `gh search issues "keyword" --repo owner/repo --limit 10 & gh search prs "keyword" --repo owner/repo --state merged --limit 10 & wait`
|
|
98
|
+
|
|
99
|
+
Then dig into the clone with grep, read, git blame, git log as needed.
|
|
100
|
+
|
|
101
|
+
## Step 3: Construct Permalinks
|
|
102
|
+
|
|
103
|
+
Permalinks are the whole point. They make your answers citable and verifiable.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
https://github.com/<owner>/<repo>/blob/<commit-sha>/<filepath>#L<start>-L<end>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Getting the SHA from a cloned repo:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
cd /tmp/pi-github-repos/owner/repo && git rev-parse HEAD
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Getting the SHA from a tag:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
gh api repos/owner/repo/git/refs/tags/v1.0.0 --jq '.object.sha'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Always use full commit SHAs, not branch names. Branch links break when code changes. Permalinks don't.
|
|
122
|
+
|
|
123
|
+
## Step 4: Cite Everything
|
|
124
|
+
|
|
125
|
+
Every code-related claim needs a permalink. Format:
|
|
126
|
+
|
|
127
|
+
```markdown
|
|
128
|
+
The stale time check happens in [`notifyManager.ts`](https://github.com/TanStack/query/blob/abc123/packages/query-core/src/notifyManager.ts#L42-L50):
|
|
129
|
+
|
|
130
|
+
\`\`\`typescript
|
|
131
|
+
function isStale(query: Query, staleTime: number): boolean {
|
|
132
|
+
return query.state.dataUpdatedAt + staleTime < Date.now()
|
|
133
|
+
}
|
|
134
|
+
\`\`\`
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
For conceptual answers, link to official docs and relevant source files. For implementation answers, every function/class reference should have a permalink.
|
|
138
|
+
|
|
139
|
+
## Video Analysis
|
|
140
|
+
|
|
141
|
+
For questions about video tutorials, conference talks, or screen recordings:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// Full extraction (transcript + visual descriptions)
|
|
145
|
+
fetch_content({ url: "https://youtube.com/watch?v=abc" })
|
|
146
|
+
|
|
147
|
+
// Ask a specific question about a video
|
|
148
|
+
fetch_content({ url: "https://youtube.com/watch?v=abc", prompt: "What libraries are imported in this tutorial?" })
|
|
149
|
+
|
|
150
|
+
// Single frame at a known moment
|
|
151
|
+
fetch_content({ url: "https://youtube.com/watch?v=abc", timestamp: "23:41" })
|
|
152
|
+
|
|
153
|
+
// Range scan for visual discovery
|
|
154
|
+
fetch_content({ url: "https://youtube.com/watch?v=abc", timestamp: "23:41-25:00" })
|
|
155
|
+
|
|
156
|
+
// Custom density across a range
|
|
157
|
+
fetch_content({ url: "https://youtube.com/watch?v=abc", timestamp: "23:41-25:00", frames: 3 })
|
|
158
|
+
|
|
159
|
+
// Whole-video sampling
|
|
160
|
+
fetch_content({ url: "https://youtube.com/watch?v=abc", frames: 6 })
|
|
161
|
+
|
|
162
|
+
// Analyze a local recording
|
|
163
|
+
fetch_content({ url: "/path/to/demo.mp4", prompt: "What error message appears on screen?" })
|
|
164
|
+
|
|
165
|
+
// Batch multiple videos with the same question
|
|
166
|
+
fetch_content({
|
|
167
|
+
urls: ["https://youtube.com/watch?v=abc", "https://youtube.com/watch?v=def"],
|
|
168
|
+
prompt: "What packages are installed?"
|
|
169
|
+
})
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Use single timestamps for known moments, ranges for visual scanning, and frames-alone for a quick overview of the whole video.
|
|
173
|
+
|
|
174
|
+
The `prompt` parameter only applies to video content (YouTube URLs and local video files). For non-video URLs, it's ignored.
|
|
175
|
+
|
|
176
|
+
## Failure Recovery
|
|
177
|
+
|
|
178
|
+
| Failure | Recovery |
|
|
179
|
+
|---------|----------|
|
|
180
|
+
| grep finds nothing | Broaden the query, try concept names instead of exact function names |
|
|
181
|
+
| gh CLI rate limited | Use the already-cloned repo in /tmp/pi-github-repos/ for git operations |
|
|
182
|
+
| Repo too large to clone | fetch_content returns an API-only view automatically; use that or add `forceClone: true` |
|
|
183
|
+
| File not found in clone | Branch name with slashes may have misresolved; list the repo tree and navigate manually |
|
|
184
|
+
| Uncertain about implementation | State your uncertainty explicitly, propose a hypothesis, show what evidence you did find |
|
|
185
|
+
| Video extraction fails | Ensure Chrome is signed into gemini.google.com (free) or set GEMINI_API_KEY |
|
|
186
|
+
| Page returns 403/bot block | Gemini fallback triggers automatically; no action needed if Gemini is configured |
|
|
187
|
+
| web_search fails | Check provider config; try explicit `provider: "gemini"` if Perplexity key is missing |
|
|
188
|
+
|
|
189
|
+
## Guidelines
|
|
190
|
+
|
|
191
|
+
- Vary search queries when running multiple searches -- different angles, not the same pattern repeated
|
|
192
|
+
- Prefer recent sources; filter out outdated results when they conflict with newer information
|
|
193
|
+
- For version-specific questions, clone the tagged version: `fetch_content("https://github.com/owner/repo/tree/v1.0.0")`
|
|
194
|
+
- When the repo is already cloned from a previous fetch_content call, reuse it -- check the path before cloning again
|
|
195
|
+
- Answer directly. Skip preamble like "I'll help you with..." -- go straight to findings
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ExtractedContent } from "./extract.js";
|
|
3
|
+
import type { SearchResult } from "./perplexity.js";
|
|
4
|
+
|
|
5
|
+
const CACHE_TTL_MS = 60 * 60 * 1000;
|
|
6
|
+
|
|
7
|
+
export interface QueryResultData {
|
|
8
|
+
query: string;
|
|
9
|
+
answer: string;
|
|
10
|
+
results: SearchResult[];
|
|
11
|
+
error: string | null;
|
|
12
|
+
provider?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface StoredSearchData {
|
|
16
|
+
id: string;
|
|
17
|
+
type: "search" | "fetch";
|
|
18
|
+
timestamp: number;
|
|
19
|
+
queries?: QueryResultData[];
|
|
20
|
+
urls?: ExtractedContent[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const storedResults = new Map<string, StoredSearchData>();
|
|
24
|
+
|
|
25
|
+
export function generateId(): string {
|
|
26
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function storeResult(id: string, data: StoredSearchData): void {
|
|
30
|
+
storedResults.set(id, data);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getResult(id: string): StoredSearchData | null {
|
|
34
|
+
return storedResults.get(id) ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getAllResults(): StoredSearchData[] {
|
|
38
|
+
return Array.from(storedResults.values());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function deleteResult(id: string): boolean {
|
|
42
|
+
return storedResults.delete(id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function clearResults(): void {
|
|
46
|
+
storedResults.clear();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isValidStoredData(data: unknown): data is StoredSearchData {
|
|
50
|
+
if (!data || typeof data !== "object") return false;
|
|
51
|
+
const d = data as Record<string, unknown>;
|
|
52
|
+
if (typeof d.id !== "string" || !d.id) return false;
|
|
53
|
+
if (d.type !== "search" && d.type !== "fetch") return false;
|
|
54
|
+
if (typeof d.timestamp !== "number") return false;
|
|
55
|
+
if (d.type === "search" && !Array.isArray(d.queries)) return false;
|
|
56
|
+
if (d.type === "fetch" && !Array.isArray(d.urls)) return false;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function restoreFromSession(ctx: ExtensionContext): void {
|
|
61
|
+
storedResults.clear();
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
|
|
64
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
65
|
+
if (entry.type === "custom" && entry.customType === "web-search-results") {
|
|
66
|
+
const data = entry.data;
|
|
67
|
+
if (isValidStoredData(data) && now - data.timestamp < CACHE_TTL_MS) {
|
|
68
|
+
storedResults.set(data.id, data);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|