@zosmaai/pi-llm-wiki 0.10.7 → 0.11.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 (73) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.de.md +35 -4
  3. package/README.es.md +260 -170
  4. package/README.fr.md +35 -4
  5. package/README.hi.md +35 -4
  6. package/README.ja.md +35 -4
  7. package/README.ko.md +35 -4
  8. package/README.md +38 -3
  9. package/README.pt.md +35 -4
  10. package/README.ru.md +35 -4
  11. package/README.zh.md +260 -170
  12. package/assets/demo.gif +0 -0
  13. package/dist/extensions/llm-wiki/lib/bootstrap.js +71 -0
  14. package/dist/extensions/llm-wiki/lib/embeddings.js +401 -0
  15. package/dist/extensions/llm-wiki/lib/guardrails.js +232 -0
  16. package/dist/extensions/llm-wiki/lib/indexing.js +78 -0
  17. package/dist/extensions/llm-wiki/lib/ingest-worker.js +310 -0
  18. package/dist/extensions/llm-wiki/lib/inject.js +65 -0
  19. package/dist/extensions/llm-wiki/lib/knowledge-document.js +442 -0
  20. package/dist/extensions/llm-wiki/lib/knowledge-links.js +206 -0
  21. package/dist/extensions/llm-wiki/lib/legacy-repair.js +443 -0
  22. package/dist/extensions/llm-wiki/lib/metadata.js +499 -0
  23. package/dist/extensions/llm-wiki/lib/model-command.js +86 -0
  24. package/dist/extensions/llm-wiki/lib/observation.js +283 -0
  25. package/dist/extensions/llm-wiki/lib/recall.js +875 -0
  26. package/dist/extensions/llm-wiki/lib/retro.js +158 -0
  27. package/dist/extensions/llm-wiki/lib/runtime.js +191 -0
  28. package/dist/extensions/llm-wiki/lib/source-extractors.js +426 -0
  29. package/dist/extensions/llm-wiki/lib/source-packet.js +229 -0
  30. package/dist/extensions/llm-wiki/lib/subagent.js +41 -0
  31. package/dist/extensions/llm-wiki/lib/task-config.js +172 -0
  32. package/dist/extensions/llm-wiki/lib/tools.js +1192 -0
  33. package/dist/extensions/llm-wiki/lib/trajectories-command.js +51 -0
  34. package/dist/extensions/llm-wiki/lib/trajectory.js +467 -0
  35. package/dist/extensions/llm-wiki/lib/utils.js +347 -0
  36. package/dist/extensions/llm-wiki/lib/vault-format.js +247 -0
  37. package/dist/extensions/llm-wiki/lib/visible-status.js +31 -0
  38. package/dist/extensions/llm-wiki/lib/wiki-service.js +128 -0
  39. package/dist/mcp/exec.js +121 -0
  40. package/dist/mcp/index.js +229 -0
  41. package/dist/mcp/operations.js +130 -0
  42. package/dist/package.json +1 -0
  43. package/docs/superpowers/plans/2026-08-02-okf-foundation.md +1579 -0
  44. package/docs/superpowers/plans/2026-08-03-okf-foundation-remediation.md +3005 -0
  45. package/docs/superpowers/plans/2026-08-06-okf-foundation-release-remediation.md +1174 -0
  46. package/docs/superpowers/specs/2026-08-02-okf-foundation-design.md +578 -0
  47. package/docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md +538 -0
  48. package/extensions/llm-wiki/index.ts +22 -36
  49. package/extensions/llm-wiki/lib/bootstrap.ts +84 -0
  50. package/extensions/llm-wiki/lib/embeddings.ts +9 -3
  51. package/extensions/llm-wiki/lib/guardrails.ts +174 -29
  52. package/extensions/llm-wiki/lib/indexing.ts +2 -1
  53. package/extensions/llm-wiki/lib/ingest-worker.ts +170 -29
  54. package/extensions/llm-wiki/lib/knowledge-document.ts +661 -0
  55. package/extensions/llm-wiki/lib/knowledge-links.ts +282 -0
  56. package/extensions/llm-wiki/lib/legacy-repair.ts +572 -0
  57. package/extensions/llm-wiki/lib/metadata.ts +531 -116
  58. package/extensions/llm-wiki/lib/observation.ts +37 -43
  59. package/extensions/llm-wiki/lib/recall.ts +61 -33
  60. package/extensions/llm-wiki/lib/retro.ts +65 -41
  61. package/extensions/llm-wiki/lib/source-extractors.ts +12 -17
  62. package/extensions/llm-wiki/lib/source-packet.ts +44 -31
  63. package/extensions/llm-wiki/lib/tools.ts +406 -348
  64. package/extensions/llm-wiki/lib/trajectory.ts +15 -1
  65. package/extensions/llm-wiki/lib/utils.ts +121 -130
  66. package/extensions/llm-wiki/lib/vault-format.ts +363 -0
  67. package/extensions/llm-wiki/lib/wiki-service.ts +183 -0
  68. package/mcp/exec.ts +122 -0
  69. package/mcp/index.ts +60 -250
  70. package/mcp/operations.ts +176 -0
  71. package/package.json +8 -2
  72. package/scripts/migrate-llm-wiki.js +801 -0
  73. package/skills/llm-wiki/SKILL.md +8 -6
@@ -1,11 +1,12 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
1
  import { join } from "node:path";
3
2
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
4
3
  import { Type } from "typebox";
5
4
  import { scheduleReindex } from "./indexing.js";
5
+ import { createKnowledgeDocument, writeKnowledgeDocumentFile } from "./knowledge-document.js";
6
6
  import { appendEvent, rebuildMetadataLight } from "./metadata.js";
7
7
  import type { Runtime } from "./runtime.js";
8
8
  import { type VaultPaths, fmtDate, resolveVaultPaths } from "./utils.js";
9
+ import { assertWritableVault, inspectWritableVault } from "./vault-format.js";
9
10
 
10
11
  // ─── Types ─────────────────────────────────────────────
11
12
 
@@ -51,6 +52,7 @@ export function saveObservation(
51
52
  input: ObservationInput,
52
53
  opts?: { rebuild?: boolean },
53
54
  ): ObservationResult {
55
+ assertWritableVault(paths);
54
56
  const today = fmtDate();
55
57
  const timestamp = new Date().toISOString();
56
58
 
@@ -62,50 +64,38 @@ export function saveObservation(
62
64
  .slice(0, 60);
63
65
  const slug = `obs-${today}-${slugBase}`;
64
66
 
65
- // Write to wiki/sources/{slug}.md
66
- const sourcePageDir = join(paths.wiki, "sources");
67
- mkdirSync(sourcePageDir, { recursive: true });
68
- const pagePath = join(sourcePageDir, `${slug}.md`);
67
+ const pagePath = join(paths.wiki, "sources", `${slug}.md`);
69
68
 
70
69
  const relevanceEmoji = RELEVANCE_EMOJIS[input.relevance] ?? "📝";
71
70
  const tags = input.tags ?? "";
72
71
  const sourceContext = input.source_context ?? "";
73
72
 
74
- const pageContent = [
75
- "---",
76
- "type: source",
77
- `title: "Observation: ${input.title}"`,
78
- `slug: ${slug}`,
79
- "status: observation",
80
- `created: ${today}`,
81
- `updated: ${today}`,
82
- `relevance: ${input.relevance}`,
83
- `observed_at: ${timestamp}`,
84
- tags
85
- ? `tags: [${tags
86
- .split(/\s+/)
87
- .filter(Boolean)
88
- .map((t) => `"${t}"`)
89
- .join(", ")}]`
90
- : "",
91
- sourceContext ? `source_context: "${sourceContext}"` : "",
92
- "---",
93
- "",
94
- `# ${relevanceEmoji} Observation: ${input.title}`,
95
- "",
96
- input.content,
97
- "",
98
- `*Relevance: ${input.relevance}*`,
99
- sourceContext ? `\n*Context: ${sourceContext}*` : "",
100
- tags ? `\n*Tags: ${tags}*` : "",
101
- "",
102
- "---",
103
- `*Observed: ${timestamp}*`,
104
- "",
105
- ]
106
- .filter((l) => l !== "")
107
- .join("\n");
108
- writeFileSync(pagePath, pageContent, "utf-8");
73
+ const body = `# ${relevanceEmoji} Observation: ${input.title}
74
+
75
+ ${input.content}
76
+
77
+ *Relevance: ${input.relevance}*${sourceContext ? `\n*Context: ${sourceContext}*` : ""}${tags ? `\n*Tags: ${tags}*` : ""}
78
+
79
+ ---
80
+ *Observed: ${timestamp}*`;
81
+
82
+ const doc = createKnowledgeDocument(
83
+ `sources/${slug}.md`,
84
+ {
85
+ type: "source",
86
+ title: `Observation: ${input.title}`,
87
+ slug,
88
+ status: "observation",
89
+ created: today,
90
+ updated: today,
91
+ relevance: input.relevance,
92
+ observed_at: timestamp,
93
+ ...(tags ? { tags: tags.split(/\s+/).filter(Boolean) } : {}),
94
+ ...(sourceContext ? { source_context: sourceContext } : {}),
95
+ },
96
+ body,
97
+ );
98
+ writeKnowledgeDocumentFile(pagePath, doc);
109
99
 
110
100
  // Log event
111
101
  appendEvent(paths, {
@@ -213,15 +203,19 @@ export function registerWikiObserve(
213
203
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
214
204
  const paths = resolveVaultPaths(ctx.cwd ?? process.cwd());
215
205
 
216
- if (!existsSync(join(paths.dotWiki, "config.json"))) {
206
+ const vaultCheck = inspectWritableVault(paths);
207
+ if (!vaultCheck.ok) {
217
208
  return {
218
209
  content: [
219
210
  {
220
211
  type: "text",
221
- text: "No wiki vault found at this location. Initialize one with wiki_bootstrap first.",
212
+ text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}`,
222
213
  },
223
214
  ],
224
- details: { error: "no_vault" } as Record<string, unknown>,
215
+ details: {
216
+ error: vaultCheck.diagnostics[0].code,
217
+ diagnostics: vaultCheck.diagnostics,
218
+ } as Record<string, unknown>,
225
219
  isError: true,
226
220
  };
227
221
  }
@@ -10,6 +10,7 @@ import {
10
10
  readEmbeddingStore,
11
11
  resolveEmbedder,
12
12
  } from "./embeddings.js";
13
+ import { parseKnowledgeDocument } from "./knowledge-document.js";
13
14
  import type { Registry } from "./metadata.js";
14
15
  import type { Runtime } from "./runtime.js";
15
16
  import type { TaskConfig } from "./task-config.js";
@@ -17,10 +18,10 @@ import {
17
18
  type VaultPaths,
18
19
  getPersonalWikiPaths,
19
20
  isPersonalVault,
20
- parseFrontmatter,
21
21
  readJson,
22
22
  resolveVaultPaths,
23
23
  } from "./utils.js";
24
+ import { inspectVaultFormat } from "./vault-format.js";
24
25
 
25
26
  // ─── Public API ────────────────────────────────────────
26
27
 
@@ -296,8 +297,19 @@ function chunkPage(body: string): PageChunk[] {
296
297
  return chunks;
297
298
  }
298
299
 
300
+ function parsePage(
301
+ path: string,
302
+ id: string,
303
+ ): { frontmatter: Record<string, unknown>; body: string } | undefined {
304
+ if (!existsSync(path)) return undefined;
305
+ const content = readFileSync(path, "utf-8");
306
+ const result = parseKnowledgeDocument(content, `${id}.md`);
307
+ if (!result.ok) return undefined;
308
+ return { frontmatter: result.document.frontmatter, body: result.document.body };
309
+ }
310
+
299
311
  function pagePreview(content: string): string {
300
- const { body } = parseFrontmatter(content);
312
+ const body = content.replace(/^---\n[\s\S]*?\n---\n?/, "");
301
313
  return body.trim().slice(0, 200).replace(/\n/g, " ");
302
314
  }
303
315
 
@@ -330,7 +342,7 @@ function extractExpansionTerms(
330
342
  const originalNorm = normalizeText(originalQuery);
331
343
  const termFreq = new Map<string, number>();
332
344
 
333
- for (const { pagePath, entry } of topResults) {
345
+ for (const { id, pagePath, entry } of topResults) {
334
346
  // Collect text from registry metadata + file content
335
347
  const metaText = normalizeText(
336
348
  [entry.title, entry.aliases, entry.tags, entry.summary, entry.description]
@@ -345,12 +357,13 @@ function extractExpansionTerms(
345
357
 
346
358
  // Also extract from file body
347
359
  if (existsSync(pagePath)) {
348
- const content = readFileSync(pagePath, "utf-8");
349
- const { body } = parseFrontmatter(content);
350
- const bodyNorm = normalizeText(body);
351
- for (const w of bodyNorm.split(/\s+/)) {
352
- if (w.length >= 4 && !originalNorm.includes(w) && !STOPWORDS.has(w)) {
353
- termFreq.set(w, (termFreq.get(w) || 0) + 1);
360
+ const parsed = parsePage(pagePath, id);
361
+ if (parsed) {
362
+ const bodyNorm = normalizeText(parsed.body);
363
+ for (const w of bodyNorm.split(/\s+/)) {
364
+ if (w.length >= 4 && !originalNorm.includes(w) && !STOPWORDS.has(w)) {
365
+ termFreq.set(w, (termFreq.get(w) || 0) + 1);
366
+ }
354
367
  }
355
368
  }
356
369
  }
@@ -394,8 +407,11 @@ export function searchWiki(
394
407
 
395
408
  for (const [id, entry] of Object.entries(registry.pages)) {
396
409
  const pagePath = join(paths.wiki, `${id}.md`);
397
- const content = existsSync(pagePath) ? readFileSync(pagePath, "utf-8") : "";
398
- const { frontmatter, body } = parseFrontmatter(content);
410
+ const pageExists = existsSync(pagePath);
411
+ const parsed = parsePage(pagePath, id);
412
+ if (pageExists && !parsed) continue;
413
+ const frontmatter = parsed?.frontmatter ?? {};
414
+ const body = parsed?.body ?? "";
399
415
 
400
416
  let score = 0;
401
417
 
@@ -405,8 +421,7 @@ export function searchWiki(
405
421
  score += scoreField(frontmatter.title, terms, 5);
406
422
  score += scoreField(entry.type, terms, 1);
407
423
 
408
- // Recall-oriented metadata. Arrays are supported by parseFrontmatter and
409
- // legacy comma/bracket strings still flatten into searchable text.
424
+ // Recall-oriented metadata.
410
425
  score += scoreField(entry.aliases, terms, 6);
411
426
  score += scoreField(frontmatter.aliases, terms, 6);
412
427
  score += scoreField(entry.recall_triggers, terms, 7);
@@ -489,8 +504,8 @@ export function searchWiki(
489
504
  // Apply expansion scoring to the top 25 results (cheap re-read)
490
505
  const expansionCandidates = scored.slice(0, Math.min(25, scored.length));
491
506
  for (const item of expansionCandidates) {
492
- const content = existsSync(item.pagePath) ? readFileSync(item.pagePath, "utf-8") : "";
493
- const { body } = parseFrontmatter(content);
507
+ const parsed = parsePage(item.pagePath, item.id);
508
+ const body = parsed?.body ?? "";
494
509
  let expChunkScore = 0;
495
510
  if (body.trim()) {
496
511
  const chunks = chunkPage(body);
@@ -960,6 +975,10 @@ export function registerWikiRecall(pi: ExtensionAPI, runtime?: Runtime): void {
960
975
  }
961
976
 
962
977
  const maxResults = Math.min(params.max_results ?? 5, 10);
978
+ const vaultDiagnostics = inspectVaultFormat(paths).diagnostics;
979
+ const diagnosticText = vaultDiagnostics.length
980
+ ? `\n\nDiagnostics: ${vaultDiagnostics.map((diagnostic) => diagnostic.code).join(", ")}`
981
+ : "";
963
982
  // Use layered hybrid search: personal vault + project vault, blending
964
983
  // lexical scoring with precomputed semantic embeddings when available.
965
984
  // No embeddings / no embedder => pure lexical, no network call.
@@ -973,10 +992,14 @@ export function registerWikiRecall(pi: ExtensionAPI, runtime?: Runtime): void {
973
992
  content: [
974
993
  {
975
994
  type: "text",
976
- text: `No wiki pages found matching "${params.query}". The wiki is empty — use wiki_retro to start building knowledge.`,
995
+ text: `No wiki pages found matching "${params.query}". The wiki is empty — use wiki_retro to start building knowledge.${diagnosticText}`,
977
996
  },
978
997
  ],
979
- details: { query: params.query, matches: [] } as Record<string, unknown>,
998
+ details: {
999
+ query: params.query,
1000
+ matches: [],
1001
+ diagnostics: vaultDiagnostics,
1002
+ } as Record<string, unknown>,
980
1003
  };
981
1004
  }
982
1005
 
@@ -997,19 +1020,22 @@ export function registerWikiRecall(pi: ExtensionAPI, runtime?: Runtime): void {
997
1020
  return `${i + 1}. [[${r.id}]] — ${r.title} (${r.type}, score ${r.score.toFixed(1)})${vault}\n Path: ${r.path}${tail}`;
998
1021
  })
999
1022
  .join("\n");
1000
- const text = [
1001
- `Found ${results.length} wiki page(s) matching "${params.query}"${layerTag} (two-stage recall — ranked links, expand on demand):`,
1002
- "",
1003
- linkLines,
1004
- "",
1005
- "Call `read` on the path(s) you need to pull full content.",
1006
- ].join("\n");
1023
+ const text =
1024
+ [
1025
+ `Found ${results.length} wiki page(s) matching "${params.query}"${layerTag} (two-stage recall — ranked links, expand on demand):`,
1026
+ "",
1027
+ linkLines,
1028
+ "",
1029
+ "Call `read` on the path(s) you need to pull full content.",
1030
+ ].join("\n") + diagnosticText;
1007
1031
  return {
1008
1032
  content: [{ type: "text", text }],
1009
- details: { query: params.query, mode: "links", matches: results } as Record<
1010
- string,
1011
- unknown
1012
- >,
1033
+ details: {
1034
+ query: params.query,
1035
+ mode: "links",
1036
+ matches: results,
1037
+ diagnostics: vaultDiagnostics,
1038
+ } as Record<string, unknown>,
1013
1039
  };
1014
1040
  }
1015
1041
 
@@ -1022,13 +1048,15 @@ export function registerWikiRecall(pi: ExtensionAPI, runtime?: Runtime): void {
1022
1048
  const vault = r.vaultLabel ? ` ${r.vaultLabel}` : "";
1023
1049
  return `## [[${r.id}]] — ${r.title}${vault}\nType: ${r.type}\nPath: ${r.path}\n\n${r.preview}`;
1024
1050
  })
1025
- .join("\n\n---\n\n")}`,
1051
+ .join("\n\n---\n\n")}${diagnosticText}`,
1026
1052
  },
1027
1053
  ],
1028
- details: { query: params.query, mode: "preview", matches: results } as Record<
1029
- string,
1030
- unknown
1031
- >,
1054
+ details: {
1055
+ query: params.query,
1056
+ mode: "preview",
1057
+ matches: results,
1058
+ diagnostics: vaultDiagnostics,
1059
+ } as Record<string, unknown>,
1032
1060
  };
1033
1061
  },
1034
1062
  });
@@ -1,11 +1,12 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
1
+ import { dirname, resolve } from "node:path";
3
2
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
3
  import { Type } from "typebox";
5
4
  import { scheduleReindex } from "./indexing.js";
5
+ import { createKnowledgeDocument, writeKnowledgeDocumentFile } from "./knowledge-document.js";
6
6
  import { appendEvent, rebuildMetadataLight } from "./metadata.js";
7
7
  import type { Runtime } from "./runtime.js";
8
8
  import { type VaultPaths, fmtDate, resolveVaultPaths } from "./utils.js";
9
+ import { assertWritableVault, inspectWritableVault } from "./vault-format.js";
9
10
 
10
11
  // ─── Public API ────────────────────────────────────────
11
12
 
@@ -24,6 +25,16 @@ export interface RetroResult {
24
25
  * The 4-layer pipeline (raw → source pages → canonical pages → metadata)
25
26
  * is still available via wiki_capture_source → wiki_ingest for deep research.
26
27
  */
28
+ function insightPath(paths: VaultPaths, slug: string): string {
29
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug) || slug === "index" || slug === "log") {
30
+ throw new Error(`Invalid insight slug: ${slug}`);
31
+ }
32
+ const directory = resolve(paths.wiki, "sources");
33
+ const target = resolve(directory, `${slug}.md`);
34
+ if (dirname(target) !== directory) throw new Error(`Invalid insight slug: ${slug}`);
35
+ return target;
36
+ }
37
+
27
38
  export function saveInsight(
28
39
  paths: VaultPaths,
29
40
  slug: string,
@@ -32,41 +43,38 @@ export function saveInsight(
32
43
  category?: string,
33
44
  opts?: { rebuild?: boolean },
34
45
  ): RetroResult {
46
+ assertWritableVault(paths);
35
47
  const today = fmtDate();
36
48
 
37
- // Write a single markdown file to wiki/sources/{slug}.md
38
- const sourcePageDir = join(paths.wiki, "sources");
39
- mkdirSync(sourcePageDir, { recursive: true });
40
- const sourcePagePath = join(sourcePageDir, `${slug}.md`);
41
-
42
- const pageContent = [
43
- "---",
44
- "type: source",
45
- `title: "${title}"`,
46
- `slug: ${slug}`,
47
- "status: insight",
48
- `created: ${today}`,
49
- `updated: ${today}`,
50
- category ? `category: ${category}` : "",
51
- "---",
52
- "",
53
- `# ${title}`,
54
- "",
55
- body,
56
- "",
57
- category ? `*Category: ${category}*` : "",
58
- "",
59
- "---",
60
- `*Captured: ${today}*`,
61
- "",
62
- "## Related",
63
- "",
64
- "_Add links to related pages._",
65
- "",
66
- ]
67
- .filter((l) => l !== "")
68
- .join("\n");
69
- writeFileSync(sourcePagePath, pageContent, "utf-8");
49
+ const sourcePagePath = insightPath(paths, slug);
50
+
51
+ const pageBody = `# ${title}
52
+
53
+ ${body}
54
+
55
+ ${category ? `*Category: ${category}*` : ""}
56
+
57
+ ---
58
+ *Captured: ${today}*
59
+
60
+ ## Related
61
+
62
+ _Add links to related pages._`;
63
+
64
+ const doc = createKnowledgeDocument(
65
+ `sources/${slug}.md`,
66
+ {
67
+ type: "source",
68
+ title,
69
+ slug,
70
+ status: "insight",
71
+ created: today,
72
+ updated: today,
73
+ ...(category ? { category } : {}),
74
+ },
75
+ pageBody,
76
+ );
77
+ writeKnowledgeDocumentFile(sourcePagePath, doc);
70
78
 
71
79
  // Log event
72
80
  appendEvent(paths, {
@@ -125,22 +133,38 @@ export function registerWikiRetro(pi: ExtensionAPI, runtime?: Runtime): void {
125
133
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
126
134
  const paths = resolveVaultPaths(ctx.cwd ?? process.cwd());
127
135
 
128
- if (!existsSync(join(paths.dotWiki, "config.json"))) {
136
+ const vaultCheck = inspectWritableVault(paths);
137
+ if (!vaultCheck.ok) {
129
138
  return {
130
139
  content: [
131
140
  {
132
141
  type: "text",
133
- text: "No wiki vault found at this location. Initialize one with wiki_bootstrap first.",
142
+ text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}`,
134
143
  },
135
144
  ],
136
- details: { error: "no_vault" } as Record<string, unknown>,
145
+ details: {
146
+ error: vaultCheck.diagnostics[0].code,
147
+ diagnostics: vaultCheck.diagnostics,
148
+ } as Record<string, unknown>,
137
149
  isError: true,
138
150
  };
139
151
  }
140
152
 
141
- const result = saveInsight(paths, params.slug, params.title, params.body, params.category, {
142
- rebuild: !runtime,
143
- });
153
+ let result: RetroResult;
154
+ try {
155
+ result = saveInsight(paths, params.slug, params.title, params.body, params.category, {
156
+ rebuild: !runtime,
157
+ });
158
+ } catch (error: unknown) {
159
+ if ((error as Error).message.startsWith("Invalid insight slug:")) {
160
+ return {
161
+ content: [{ type: "text", text: (error as Error).message }],
162
+ details: { error: "invalid_insight_slug" } as Record<string, unknown>,
163
+ isError: true,
164
+ };
165
+ }
166
+ throw error;
167
+ }
144
168
  if (runtime) {
145
169
  scheduleReindex(runtime, { hasUI: ctx.hasUI, ui: ctx.ui }, paths);
146
170
  }
@@ -1,7 +1,7 @@
1
1
  import { open } from "node:fs/promises";
2
2
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
3
  import { NodeHtmlMarkdown } from "node-html-markdown";
4
- import { exec } from "./utils.js";
4
+ import { type ExecApi, exec } from "./utils.js";
5
5
 
6
6
  export type ExtractionStatus = "success" | "failed" | "unsupported";
7
7
 
@@ -23,7 +23,7 @@ export interface FileExtractor {
23
23
  }
24
24
 
25
25
  interface FileExtractArgs {
26
- pi: ExtensionAPI;
26
+ pi: ExecApi;
27
27
  filePath: string;
28
28
  content: string;
29
29
  signal?: AbortSignal;
@@ -35,7 +35,7 @@ interface UrlExtractor {
35
35
  }
36
36
 
37
37
  interface UrlExtractArgs {
38
- pi: ExtensionAPI;
38
+ pi: ExecApi;
39
39
  url: string;
40
40
  signal?: AbortSignal;
41
41
  }
@@ -161,7 +161,7 @@ export function fileExtractorFor(filePath: string): FileExtractor {
161
161
  }
162
162
 
163
163
  export function extractUrlContent(
164
- pi: ExtensionAPI,
164
+ pi: ExecApi,
165
165
  url: string,
166
166
  signal?: AbortSignal,
167
167
  ): Promise<ExtractedContent> {
@@ -197,7 +197,7 @@ function hasAnyExtension(extensions: string[]): (path: string) => boolean {
197
197
  return (path) => extensions.some((extension) => hasExtension(extension)(path));
198
198
  }
199
199
 
200
- async function extractPdf(pi: ExtensionAPI, source: string, signal?: AbortSignal): Promise<string> {
200
+ async function extractPdf(pi: ExecApi, source: string, signal?: AbortSignal): Promise<string> {
201
201
  const extracted = await extractWithMarkItDown(pi, source, signal);
202
202
  return extracted || pdfExtractionFailureMessage(source);
203
203
  }
@@ -206,17 +206,13 @@ export function docxExtractionFailureMessage(source: string): string {
206
206
  return `_DOCX content could not be converted to markdown from ${source}. Ensure uvx and markitdown are installed._\n`;
207
207
  }
208
208
 
209
- async function extractDocx(
210
- pi: ExtensionAPI,
211
- source: string,
212
- signal?: AbortSignal,
213
- ): Promise<string> {
209
+ async function extractDocx(pi: ExecApi, source: string, signal?: AbortSignal): Promise<string> {
214
210
  const extracted = await extractWithMarkItDown(pi, source, signal);
215
211
  return extracted || docxExtractionFailureMessage(source);
216
212
  }
217
213
 
218
214
  async function extractPdfUrl(
219
- pi: ExtensionAPI,
215
+ pi: ExecApi,
220
216
  url: string,
221
217
  signal?: AbortSignal,
222
218
  ): Promise<ExtractedContent> {
@@ -232,7 +228,7 @@ async function extractPdfUrl(
232
228
  }
233
229
 
234
230
  async function extractTextUrl(
235
- pi: ExtensionAPI,
231
+ pi: ExecApi,
236
232
  url: string,
237
233
  signal?: AbortSignal,
238
234
  ): Promise<ExtractedContent> {
@@ -266,13 +262,12 @@ async function extractTextUrl(
266
262
  }
267
263
 
268
264
  async function extractWithMarkItDown(
269
- pi: ExtensionAPI,
265
+ pi: ExecApi,
270
266
  source: string,
271
267
  signal?: AbortSignal,
272
268
  ): Promise<string> {
273
- if (!(await hasMarkItDown(pi, signal))) return "";
274
-
275
269
  try {
270
+ if (!(await hasMarkItDown(pi, signal))) return "";
276
271
  const mdResult = await exec(
277
272
  pi,
278
273
  "sh",
@@ -285,7 +280,7 @@ async function extractWithMarkItDown(
285
280
  }
286
281
  }
287
282
 
288
- async function hasMarkItDown(pi: ExtensionAPI, signal?: AbortSignal): Promise<boolean> {
283
+ async function hasMarkItDown(pi: ExecApi, signal?: AbortSignal): Promise<boolean> {
289
284
  const markitdown = await exec(
290
285
  pi,
291
286
  "sh",
@@ -295,7 +290,7 @@ async function hasMarkItDown(pi: ExtensionAPI, signal?: AbortSignal): Promise<bo
295
290
  return markitdown.stdout.trim() === "yes";
296
291
  }
297
292
 
298
- async function fetchTextUrl(pi: ExtensionAPI, url: string, signal?: AbortSignal): Promise<string> {
293
+ async function fetchTextUrl(pi: ExecApi, url: string, signal?: AbortSignal): Promise<string> {
299
294
  try {
300
295
  const curlResult = await exec(
301
296
  pi,