@zosmaai/pi-llm-wiki 0.3.0 → 0.5.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.
@@ -1,7 +1,8 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { extname, join } from "node:path";
3
3
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
4
  import { appendEvent } from "./metadata.js";
5
+ import { type ExtractedContent, extractUrlContent, fileExtractorFor } from "./source-extractors.js";
5
6
  import { type VaultPaths, exec, fmtDate, nextSourceId, readText, writeJson } from "./utils.js";
6
7
 
7
8
  /**
@@ -22,295 +23,206 @@ export interface CaptureResult {
22
23
  extracted: string;
23
24
  }
24
25
 
25
- const DEFAULT_MARKITDOWN_TIMEOUT_MS = 180_000;
26
- const DEFAULT_CURL_TIMEOUT_SECONDS = 30;
27
-
28
- function markitdownTimeoutMs(): number {
29
- return positiveIntegerFromEnv("WIKI_MARKITDOWN_TIMEOUT_MS", DEFAULT_MARKITDOWN_TIMEOUT_MS);
30
- }
31
-
32
- function positiveIntegerFromEnv(name: string, fallback: number): number {
33
- const raw = process.env[name];
34
- if (!raw) return fallback;
35
- const parsed = Number.parseInt(raw, 10);
36
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
37
- }
38
-
39
- function isPdfUrl(url: string): boolean {
40
- try {
41
- return new URL(url).pathname.toLowerCase().endsWith(".pdf");
42
- } catch {
43
- return url.toLowerCase().split(/[?#]/, 1)[0].endsWith(".pdf");
44
- }
45
- }
46
-
47
- function looksLikePdf(content: string): boolean {
48
- return content.trimStart().startsWith("%PDF-");
26
+ interface SourcePacket {
27
+ sourceId: string;
28
+ packetPath: string;
49
29
  }
50
30
 
51
- function pdfExtractionFailureMessage(source: string): string {
52
- return `_PDF content could not be converted to markdown from ${source}. Try increasing WIKI_MARKITDOWN_TIMEOUT_MS._\n`;
31
+ interface CaptureSource {
32
+ needsOriginalDir: boolean;
33
+ fallbackText: string;
34
+ preserveOriginal?(packetPath: string): Promise<void>;
35
+ extract(): Promise<ExtractedContent> | ExtractedContent;
36
+ manifest(content: ExtractedContent): Record<string, unknown>;
37
+ event(content: ExtractedContent): Record<string, unknown>;
53
38
  }
54
39
 
55
40
  const URL_ORIGINAL_EXTENSIONS = new Set([".html", ".htm", ".md", ".pdf", ".txt", ".xml", ".json"]);
56
41
 
57
- function originalFileNameForUrl(url: string): string {
58
- try {
59
- const parsed = new URL(url);
60
- const ext = extname(parsed.pathname).toLowerCase();
61
- if (URL_ORIGINAL_EXTENSIONS.has(ext)) return `source${ext}`;
62
- } catch {
63
- const path = url.split(/[?#]/, 1)[0] ?? "";
64
- const ext = extname(path).toLowerCase();
65
- if (URL_ORIGINAL_EXTENSIONS.has(ext)) return `source${ext}`;
66
- }
67
-
68
- return "source.html";
69
- }
70
-
71
- async function preserveUrlOriginal(
42
+ /** Capture a URL into a source packet. */
43
+ export async function captureUrl(
72
44
  pi: ExtensionAPI,
73
- packetPath: string,
45
+ paths: VaultPaths,
74
46
  url: string,
75
47
  signal?: AbortSignal,
76
- ): Promise<void> {
77
- const originalPath = join(packetPath, "original", originalFileNameForUrl(url));
78
- try {
79
- await exec(pi, "curl", ["-sL", "--max-time", "30", "-o", originalPath, url], {
80
- signal,
81
- timeout: 35_000,
82
- });
83
- } catch {
84
- // Preserve best-effort extraction behavior even when the original artifact cannot be saved.
85
- }
48
+ ): Promise<CaptureResult> {
49
+ return captureSource(paths, urlCaptureSource(pi, url, signal));
86
50
  }
87
51
 
88
- /** Capture a URL into a source packet. */
89
- export async function captureUrl(
52
+ /** Capture a local file into a source packet. */
53
+ export async function captureFile(
90
54
  pi: ExtensionAPI,
91
55
  paths: VaultPaths,
92
- url: string,
56
+ filePath: string,
93
57
  signal?: AbortSignal,
94
58
  ): Promise<CaptureResult> {
95
- const sourceId = nextSourceId(paths);
96
- const packetPath = join(paths.rawSources, sourceId);
97
- mkdirSync(packetPath, { recursive: true });
98
- mkdirSync(join(packetPath, "original"), { recursive: true });
99
- mkdirSync(join(packetPath, "attachments"), { recursive: true });
59
+ return captureSource(paths, fileCaptureSource(pi, filePath, signal));
60
+ }
100
61
 
101
- await preserveUrlOriginal(pi, packetPath, url, signal);
102
-
103
- // Try to fetch and extract content
104
- let extracted = "";
105
- let title = url;
106
- const isPdf = isPdfUrl(url);
107
-
108
- // Try MarkItDown first.
109
- const markitdown = await exec(
110
- pi,
111
- "sh",
112
- ["-c", `which uvx >/dev/null 2>&1 && echo "yes" || echo "no"`],
113
- { signal },
114
- );
115
-
116
- if (markitdown.stdout.trim() === "yes") {
117
- try {
118
- const mdResult = await exec(
119
- pi,
120
- "sh",
121
- ["-c", `uvx --from 'markitdown[pdf]' markitdown "${url}" 2>/dev/null || echo ""`],
122
- { signal, timeout: markitdownTimeoutMs() },
123
- );
124
- if (mdResult.stdout.trim()) {
125
- extracted = mdResult.stdout;
126
- // Try to extract title from first h1
127
- const h1Match = extracted.match(/^#\s+(.+)$/m);
128
- if (h1Match) title = h1Match[1].trim();
129
- }
130
- } catch {
131
- // markitdown failed, fall through
132
- }
133
- }
62
+ /** Capture pasted text into a source packet. */
63
+ export function captureText(paths: VaultPaths, text: string, title?: string): CaptureResult {
64
+ return captureSourceSync(paths, textCaptureSource(text, title));
65
+ }
134
66
 
135
- // Fallback: try fetch_content equivalent via curl for text/html sources.
136
- // Do not write binary PDF bytes into extracted.md when PDF conversion fails.
137
- if (!extracted) {
138
- if (isPdf) {
139
- extracted = pdfExtractionFailureMessage(url);
140
- } else {
141
- try {
142
- const curlResult = await exec(
143
- pi,
144
- "curl",
145
- ["-sL", "--max-time", String(DEFAULT_CURL_TIMEOUT_SECONDS), url],
146
- {
147
- signal,
148
- timeout: (DEFAULT_CURL_TIMEOUT_SECONDS + 5) * 1_000,
149
- },
150
- );
151
- if (curlResult.stdout) {
152
- if (looksLikePdf(curlResult.stdout)) {
153
- extracted = pdfExtractionFailureMessage(url);
154
- } else {
155
- extracted = curlResult.stdout;
156
- // Try to extract title from HTML
157
- const titleMatch = extracted.match(/<title>([^<]*)<\/title>/i);
158
- if (titleMatch) title = titleMatch[1].trim();
159
- }
160
- }
161
- } catch {
162
- // curl failed too
163
- }
164
- }
165
- }
67
+ async function captureSource(paths: VaultPaths, source: CaptureSource): Promise<CaptureResult> {
68
+ const packet = createSourcePacket(paths, source.needsOriginalDir);
69
+ await source.preserveOriginal?.(packet.packetPath);
70
+ const content = await source.extract();
71
+ return finalizeCapture(paths, packet, source, content);
72
+ }
166
73
 
167
- // Write extracted text
168
- writeFileSync(
169
- join(packetPath, "extracted.md"),
170
- extracted || `_Content could not be extracted from ${url}_\n`,
171
- "utf-8",
172
- );
74
+ function captureSourceSync(paths: VaultPaths, source: CaptureSource): CaptureResult {
75
+ const packet = createSourcePacket(paths, source.needsOriginalDir);
76
+ const content = source.extract() as ExtractedContent;
77
+ return finalizeCapture(paths, packet, source, content);
78
+ }
173
79
 
174
- // Write manifest
175
- const manifest = {
176
- id: sourceId,
177
- title,
178
- url,
179
- captured: fmtDate(),
180
- format: "web",
181
- packet_version: "1.0",
80
+ function urlCaptureSource(pi: ExtensionAPI, url: string, signal?: AbortSignal): CaptureSource {
81
+ return {
82
+ needsOriginalDir: true,
83
+ fallbackText: contentExtractionFailureMessage(url),
84
+ preserveOriginal: (packetPath) => preserveUrlOriginal(pi, packetPath, url, signal),
85
+ extract: () => extractUrlContent(pi, url, signal),
86
+ manifest: (content) => ({
87
+ title: content.title || url,
88
+ url,
89
+ format: "web",
90
+ }),
91
+ event: () => ({ url, format: "web" }),
182
92
  };
183
- writeJson(join(packetPath, "manifest.json"), manifest);
184
-
185
- // Create skeleton source page in wiki
186
- const sourcePagePath = join(paths.wiki, "sources", `${sourceId}.md`);
187
- const sourcePageContent = buildSourcePageSkeleton(manifest, extracted);
188
- writeFileSync(sourcePagePath, sourcePageContent, "utf-8");
189
-
190
- // Log event
191
- appendEvent(paths, { kind: "capture", source_id: sourceId, url, format: "web" });
192
-
193
- return { sourceId, packetPath, sourcePagePath, extracted };
194
93
  }
195
94
 
196
- /** Capture a local file into a source packet. */
197
- export async function captureFile(
95
+ function fileCaptureSource(
198
96
  pi: ExtensionAPI,
199
- paths: VaultPaths,
200
97
  filePath: string,
201
98
  signal?: AbortSignal,
202
- ): Promise<CaptureResult> {
99
+ ): CaptureSource {
100
+ const fileName = filePath.split("/").pop() || "unknown";
101
+ const extractor = fileExtractorFor(filePath);
102
+ const content = extractor.shouldReadText ? readText(filePath) : "";
103
+
104
+ return {
105
+ needsOriginalDir: true,
106
+ fallbackText: "",
107
+ preserveOriginal: (packetPath) =>
108
+ preserveFileOriginal(pi, packetPath, filePath, fileName, content, signal),
109
+ extract: async () => ({
110
+ extracted: await extractor.extract({ pi, filePath, content, signal }),
111
+ }),
112
+ manifest: () => ({
113
+ title: fileName,
114
+ file_path: filePath,
115
+ format: extractor.format,
116
+ }),
117
+ event: () => ({ file_path: filePath, format: extractor.format }),
118
+ };
119
+ }
120
+
121
+ function textCaptureSource(text: string, title?: string): CaptureSource {
122
+ return {
123
+ needsOriginalDir: false,
124
+ fallbackText: "",
125
+ extract: () => ({ extracted: text }),
126
+ manifest: () => ({
127
+ title: title || `Pasted text — ${fmtDate()}`,
128
+ format: "text",
129
+ }),
130
+ event: () => ({ format: "text" }),
131
+ };
132
+ }
133
+
134
+ function createSourcePacket(paths: VaultPaths, needsOriginalDir: boolean): SourcePacket {
203
135
  const sourceId = nextSourceId(paths);
204
136
  const packetPath = join(paths.rawSources, sourceId);
205
137
  mkdirSync(packetPath, { recursive: true });
206
- mkdirSync(join(packetPath, "original"), { recursive: true });
207
138
  mkdirSync(join(packetPath, "attachments"), { recursive: true });
139
+ if (needsOriginalDir) mkdirSync(join(packetPath, "original"), { recursive: true });
140
+ return { sourceId, packetPath };
141
+ }
208
142
 
209
- const lowerPath = filePath.toLowerCase();
210
- const isPdf = lowerPath.endsWith(".pdf");
211
- const isXml = lowerPath.endsWith(".xml");
212
- const content = isPdf ? "" : readText(filePath);
213
- const fileName = filePath.split("/").pop() || "unknown";
214
-
215
- let extracted = content;
216
-
217
- // Convert XML to markdown
218
- if (isXml && content) {
219
- extracted = xmlToMarkdown(content);
220
- }
221
-
222
- // Try MarkItDown for PDFs.
223
- if (isPdf) {
224
- const markitdown = await exec(
225
- pi,
226
- "sh",
227
- ["-c", `which uvx >/dev/null 2>&1 && echo "yes" || echo "no"`],
228
- { signal },
229
- );
230
-
231
- if (markitdown.stdout.trim() === "yes") {
232
- try {
233
- const mdResult = await exec(
234
- pi,
235
- "sh",
236
- ["-c", `uvx --from 'markitdown[pdf]' markitdown "${filePath}" 2>/dev/null || echo ""`],
237
- { signal, timeout: markitdownTimeoutMs() },
238
- );
239
- if (mdResult.stdout.trim()) extracted = mdResult.stdout;
240
- } catch {
241
- extracted = pdfExtractionFailureMessage(filePath);
242
- }
243
- }
244
- if (!extracted) extracted = pdfExtractionFailureMessage(filePath);
245
- }
246
-
247
- // Copy original to packet
248
- try {
249
- await exec(pi, "cp", [filePath, join(packetPath, "original", fileName)], { signal });
250
- } catch {
251
- // If cp fails, just write the content
252
- writeFileSync(join(packetPath, "original", fileName), content, "utf-8");
253
- }
254
-
255
- // Write extracted text
256
- writeFileSync(join(packetPath, "extracted.md"), extracted, "utf-8");
257
-
258
- // Write manifest
143
+ function finalizeCapture(
144
+ paths: VaultPaths,
145
+ packet: SourcePacket,
146
+ source: CaptureSource,
147
+ content: ExtractedContent,
148
+ ): CaptureResult {
149
+ const extracted = content.extracted || source.fallbackText;
259
150
  const manifest = {
260
- id: sourceId,
261
- title: fileName,
262
- file_path: filePath,
151
+ id: packet.sourceId,
263
152
  captured: fmtDate(),
264
- format: guessFormat(filePath),
265
153
  packet_version: "1.0",
154
+ ...source.manifest({ ...content, extracted }),
266
155
  };
267
- writeJson(join(packetPath, "manifest.json"), manifest);
268
156
 
269
- // Create skeleton source page
270
- const sourcePagePath = join(paths.wiki, "sources", `${sourceId}.md`);
271
- const sourcePageContent = buildSourcePageSkeleton(manifest, extracted);
272
- writeFileSync(sourcePagePath, sourcePageContent, "utf-8");
157
+ writeFileSync(join(packet.packetPath, "extracted.md"), extracted, "utf-8");
158
+ writeJson(join(packet.packetPath, "manifest.json"), manifest);
159
+
160
+ const sourcePagePath = join(paths.wiki, "sources", `${packet.sourceId}.md`);
161
+ writeFileSync(sourcePagePath, buildSourcePageSkeleton(manifest, extracted), "utf-8");
273
162
 
274
- // Log event
275
163
  appendEvent(paths, {
276
164
  kind: "capture",
277
- source_id: sourceId,
278
- file_path: filePath,
279
- format: manifest.format,
165
+ source_id: packet.sourceId,
166
+ ...source.event({ ...content, extracted }),
280
167
  });
281
168
 
282
- return { sourceId, packetPath, sourcePagePath, extracted };
169
+ return {
170
+ sourceId: packet.sourceId,
171
+ packetPath: packet.packetPath,
172
+ sourcePagePath,
173
+ extracted,
174
+ };
283
175
  }
284
176
 
285
- /** Capture pasted text into a source packet. */
286
- export function captureText(paths: VaultPaths, text: string, title?: string): CaptureResult {
287
- const sourceId = nextSourceId(paths);
288
- const packetPath = join(paths.rawSources, sourceId);
289
- mkdirSync(packetPath, { recursive: true });
290
- mkdirSync(join(packetPath, "attachments"), { recursive: true });
291
-
292
- // Write extracted text
293
- writeFileSync(join(packetPath, "extracted.md"), text, "utf-8");
177
+ async function preserveFileOriginal(
178
+ pi: ExtensionAPI,
179
+ packetPath: string,
180
+ filePath: string,
181
+ fileName: string,
182
+ fallbackContent: string,
183
+ signal?: AbortSignal,
184
+ ): Promise<void> {
185
+ try {
186
+ await exec(pi, "cp", [filePath, join(packetPath, "original", fileName)], { signal });
187
+ } catch {
188
+ // If cp fails, preserve whatever text content was available.
189
+ writeFileSync(join(packetPath, "original", fileName), fallbackContent, "utf-8");
190
+ }
191
+ }
294
192
 
295
- // Write manifest
296
- const manifest = {
297
- id: sourceId,
298
- title: title || `Pasted text — ${fmtDate()}`,
299
- captured: fmtDate(),
300
- format: "text",
301
- packet_version: "1.0",
302
- };
303
- writeJson(join(packetPath, "manifest.json"), manifest);
193
+ async function preserveUrlOriginal(
194
+ pi: ExtensionAPI,
195
+ packetPath: string,
196
+ url: string,
197
+ signal?: AbortSignal,
198
+ ): Promise<void> {
199
+ const originalPath = join(packetPath, "original", originalFileNameForUrl(url));
200
+ try {
201
+ await exec(pi, "curl", ["-sL", "--max-time", "30", "-o", originalPath, url], {
202
+ signal,
203
+ timeout: 35_000,
204
+ });
205
+ } catch {
206
+ // Preserve best-effort extraction behavior even when the original artifact cannot be saved.
207
+ }
208
+ }
304
209
 
305
- // Create skeleton source page
306
- const sourcePagePath = join(paths.wiki, "sources", `${sourceId}.md`);
307
- const sourcePageContent = buildSourcePageSkeleton(manifest, text);
308
- writeFileSync(sourcePagePath, sourcePageContent, "utf-8");
210
+ function originalFileNameForUrl(url: string): string {
211
+ try {
212
+ const parsed = new URL(url);
213
+ const ext = extname(parsed.pathname).toLowerCase();
214
+ if (URL_ORIGINAL_EXTENSIONS.has(ext)) return `source${ext}`;
215
+ } catch {
216
+ const path = url.split(/[?#]/, 1)[0] ?? "";
217
+ const ext = extname(path).toLowerCase();
218
+ if (URL_ORIGINAL_EXTENSIONS.has(ext)) return `source${ext}`;
219
+ }
309
220
 
310
- // Log event
311
- appendEvent(paths, { kind: "capture", source_id: sourceId, format: "text" });
221
+ return "source.html";
222
+ }
312
223
 
313
- return { sourceId, packetPath, sourcePagePath, extracted: text };
224
+ function contentExtractionFailureMessage(source: string): string {
225
+ return `_Content could not be extracted from ${source}_\n`;
314
226
  }
315
227
 
316
228
  /** Build a skeleton source page from manifest and extracted text. */
@@ -370,59 +282,3 @@ status: skeleton
370
282
  - **Manifest:** [raw/sources/${id}/manifest.json](../raw/sources/${id}/manifest.json)
371
283
  `;
372
284
  }
373
-
374
- /** Basic XML to markdown conversion: strip tags while preserving text structure. */
375
- function xmlToMarkdown(xml: string): string {
376
- // Extract title from first <title> or root element
377
- let title = "";
378
- const titleMatch = xml.match(/<title[^>]*>([^<]*)<\/title>/i);
379
- if (titleMatch) title = titleMatch[1].trim();
380
-
381
- // Strip XML declaration and doctype
382
- let text = xml.replace(/<\?xml[^>]*\?>\s*/gi, "");
383
- text = text.replace(/<!DOCTYPE[^>]*>\s*/gi, "");
384
-
385
- // Replace block-level tags with newlines
386
- text = text.replace(/<\/(p|div|section|article|li|h\d|tr|blockquote|pre)>/gi, "\n");
387
- text = text.replace(/<br\s*\/?>/gi, "\n");
388
-
389
- // Strip remaining tags — match < followed by tag name characters to >
390
- // Using a loop to handle malformed/broken tags that lack a closing >
391
- let prev = "";
392
- while (prev !== text) {
393
- prev = text;
394
- text = text.replace(/<[a-zA-Z\/!?][^>]*>/g, "");
395
- }
396
- // Remove any stray < that didn't form a complete tag
397
- text = text.replace(/</g, "");
398
-
399
- // Decode XML entities in a single pass to avoid double-unescaping
400
- text = text.replace(/&(?:amp|lt|gt|quot|#\d+);/gi, (entity) => {
401
- const map: Record<string, string> = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"' };
402
- const lower = entity.toLowerCase();
403
- if (map[lower]) return map[lower];
404
- if (lower.startsWith("&#")) return String.fromCodePoint(Number.parseInt(entity.slice(2, -1)));
405
- return entity;
406
- });
407
-
408
- // Clean up excessive blank lines
409
- text = text.replace(/\n{3,}/g, "\n\n").trim();
410
-
411
- if (!text) return xml; // fallback: return raw if stripping produced nothing
412
-
413
- const lines = [];
414
- if (title) lines.push(`# ${title}\n`);
415
- lines.push(text);
416
- return lines.join("\n\n");
417
- }
418
-
419
- function guessFormat(filePath: string): string {
420
- const lower = filePath.toLowerCase();
421
- if (lower.endsWith(".pdf")) return "pdf";
422
- if (lower.endsWith(".md")) return "markdown";
423
- if (lower.endsWith(".txt")) return "text";
424
- if (lower.endsWith(".html") || lower.endsWith(".htm")) return "html";
425
- if (lower.endsWith(".xml")) return "xml";
426
- if (lower.endsWith(".docx")) return "docx";
427
- return "file";
428
- }
package/package.json CHANGED
@@ -1,17 +1,26 @@
1
1
  {
2
2
  "name": "@zosmaai/pi-llm-wiki",
3
- "version": "0.3.0",
4
- "description": "LLM Wiki for Pi — self-maintaining knowledge base following Karpathy's pattern. Obsidian-friendly, auto-updating, personal & company wiki.",
3
+ "version": "0.5.0",
4
+ "description": "Self-maintaining LLM Wiki for Pi — Karpathy-pattern knowledge base with immutable source capture, automated ingestion, search, linting, and Obsidian-compatible vault. auto-updating personal & company wiki.",
5
5
  "keywords": [
6
+ "pi",
6
7
  "pi-package",
7
- "pi-skill",
8
8
  "pi-extension",
9
+ "pi-skill",
9
10
  "llm-wiki",
10
11
  "karpathy",
11
12
  "knowledge-base",
12
13
  "obsidian",
13
14
  "second-brain",
14
15
  "wiki",
16
+ "memory",
17
+ "markdown",
18
+ "pkm",
19
+ "personal-knowledge-management",
20
+ "research",
21
+ "vault",
22
+ "zettelkasten",
23
+ "rag",
15
24
  "zosmaai"
16
25
  ],
17
26
  "license": "MIT",
@@ -24,6 +33,19 @@
24
33
  "url": "https://github.com/zosmaai/pi-llm-wiki/issues"
25
34
  },
26
35
  "homepage": "https://github.com/zosmaai/pi-llm-wiki#readme",
36
+ "files": [
37
+ "extensions",
38
+ "skills",
39
+ "prompts",
40
+ "docs",
41
+ "assets",
42
+ "README.md",
43
+ "CHANGELOG.md",
44
+ "LICENSE"
45
+ ],
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
27
49
  "scripts": {
28
50
  "test": "vitest run",
29
51
  "test:watch": "vitest",
package/.coderabbit.yaml DELETED
@@ -1,43 +0,0 @@
1
- # CodeRabbit AI Code Review Configuration
2
- # Docs: https://docs.coderabbit.ai/guides/configure-coderabbit
3
-
4
- language: en-US
5
- early_access: false
6
- reviews:
7
- profile: assertive
8
- request_changes_workflow: true
9
- high_level_summary: true
10
- poem: false
11
- review_status: true
12
- collapse_walkthrough: false
13
- auto_review:
14
- enabled: true
15
- ignore_title_keywords:
16
- - "chore"
17
- - "WIP"
18
- - "wip"
19
- drafts: false
20
- base_branches:
21
- - main
22
- path_instructions:
23
- - path: "extensions/**/*.ts"
24
- instructions: |
25
- Review TypeScript extension files for pi coding agent. Check that:
26
- 1. Tools use pi.registerTool() with TypeBox schemas
27
- 2. Error handling uses try/catch with proper messages
28
- 3. Unused parameters use underscore prefix or are removed
29
- 4. File exports a default function receiving ExtensionAPI
30
- - path: "skills/**/SKILL.md"
31
- instructions: |
32
- Review SKILL.md for pi. Verify:
33
- 1. Frontmatter follows Agent Skills spec (name, description)
34
- 2. Workflows are clearly documented with steps
35
- 3. Golden rules and conventions are explicit
36
- - path: "prompts/**/*.md"
37
- instructions: |
38
- Review prompt templates. Verify:
39
- 1. Frontmatter has description, args, section, topLevelCli
40
- 2. Instructions are actionable and clear
41
-
42
- chat:
43
- auto_reply: true
@@ -1,12 +0,0 @@
1
- name: "CodeQL Config"
2
-
3
- queries:
4
- - uses: security-and-quality
5
-
6
- # Suppress false positives for temporary file creation.
7
- # This is a single-user CLI tool — TOCTOU race conditions on temp directories
8
- # are not a valid threat model. The alerts only fire because tests pass
9
- # os.tmpdir() paths into production capture functions.
10
- query-filters:
11
- - exclude:
12
- id: js/insecure-temporary-file
@@ -1,45 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches: [main]
6
- pull_request:
7
- branches: [main]
8
-
9
- concurrency:
10
- group: ${{ github.workflow }}-${{ github.ref }}
11
- cancel-in-progress: true
12
-
13
- jobs:
14
- quality:
15
- runs-on: ubuntu-latest
16
- strategy:
17
- matrix:
18
- node-version: [20, 22, 23, 24, 25]
19
-
20
- steps:
21
- - uses: actions/checkout@v4
22
-
23
- - uses: actions/setup-node@v4
24
- with:
25
- node-version: ${{ matrix.node-version }}
26
- cache: npm
27
-
28
- - run: npm ci
29
-
30
- - name: Type Check
31
- run: npm run typecheck
32
-
33
- - name: Lint
34
- run: npm run lint
35
-
36
- - name: Run Tests with Coverage
37
- run: npm run test:coverage
38
-
39
- - name: Upload Coverage to Codecov
40
- if: ${{ matrix.node-version == 22 && github.event_name == 'push' }}
41
- uses: codecov/codecov-action@v5
42
- with:
43
- files: ./coverage/lcov.info
44
- fail_ci_if_error: false
45
- verbose: true