@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
@@ -0,0 +1,128 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { readJson } from "./utils.js";
4
+ import { compareCodePoint, discoverKnowledgeDocuments, inspectVaultFormat, } from "./vault-format.js";
5
+ /**
6
+ * Search the registry for matching concepts.
7
+ *
8
+ * Matches ID, semantic title, type, category, domain, tags, aliases, and recall triggers.
9
+ * Preserves unknown types as strings.
10
+ */
11
+ export function searchRegistry(paths, query, typeFilter) {
12
+ const diagnostics = [];
13
+ const vaultState = inspectVaultFormat(paths);
14
+ diagnostics.push(...vaultState.diagnostics);
15
+ const registryPath = join(paths.meta, "registry.json");
16
+ if (!existsSync(registryPath)) {
17
+ return { matches: [], diagnostics };
18
+ }
19
+ const registry = readJson(registryPath, {
20
+ version: "1.0",
21
+ last_updated: "",
22
+ pages: {},
23
+ });
24
+ const normalizedQuery = query.toLowerCase();
25
+ const matches = [];
26
+ for (const [id, entry] of Object.entries(registry.pages)) {
27
+ // Apply type filter
28
+ if (typeFilter && String(entry.type).toLowerCase() !== typeFilter.toLowerCase()) {
29
+ continue;
30
+ }
31
+ // Check if query matches any searchable field
32
+ if (matchesField(id, entry, normalizedQuery)) {
33
+ matches.push({
34
+ id,
35
+ title: String(entry.title || id),
36
+ type: String(entry.type || "unknown"),
37
+ });
38
+ }
39
+ }
40
+ // Sort by code point order for determinism
41
+ matches.sort((a, b) => compareCodePoint(a.id, b.id));
42
+ return { matches, diagnostics };
43
+ }
44
+ function matchesField(id, entry, query) {
45
+ // Match ID
46
+ if (id.toLowerCase().includes(query))
47
+ return true;
48
+ // Match title
49
+ if (String(entry.title || "")
50
+ .toLowerCase()
51
+ .includes(query))
52
+ return true;
53
+ // Match type
54
+ if (String(entry.type || "")
55
+ .toLowerCase()
56
+ .includes(query))
57
+ return true;
58
+ // Match category/domain
59
+ if (String(entry.category || "")
60
+ .toLowerCase()
61
+ .includes(query))
62
+ return true;
63
+ if (String(entry.domain || "")
64
+ .toLowerCase()
65
+ .includes(query))
66
+ return true;
67
+ // Match tags (array or string)
68
+ const tags = entry.tags;
69
+ if (Array.isArray(tags)) {
70
+ if (tags.some((t) => String(t).toLowerCase().includes(query)))
71
+ return true;
72
+ }
73
+ else if (typeof tags === "string" && tags.toLowerCase().includes(query)) {
74
+ return true;
75
+ }
76
+ // Match aliases (array or string)
77
+ const aliases = entry.aliases;
78
+ if (Array.isArray(aliases)) {
79
+ if (aliases.some((a) => String(a).toLowerCase().includes(query)))
80
+ return true;
81
+ }
82
+ else if (typeof aliases === "string" && aliases.toLowerCase().includes(query)) {
83
+ return true;
84
+ }
85
+ // Match recall_triggers (array or string)
86
+ const triggers = entry.recall_triggers;
87
+ if (Array.isArray(triggers)) {
88
+ if (triggers.some((t) => String(t).toLowerCase().includes(query)))
89
+ return true;
90
+ }
91
+ else if (typeof triggers === "string" && triggers.toLowerCase().includes(query)) {
92
+ return true;
93
+ }
94
+ return false;
95
+ }
96
+ /**
97
+ * Get a status snapshot of the wiki.
98
+ *
99
+ * Reports resolved knowledge_format, page counts, and blocking diagnostics.
100
+ */
101
+ export function getWikiStatus(paths) {
102
+ const vaultState = inspectVaultFormat(paths);
103
+ const diagnostics = [...vaultState.diagnostics];
104
+ // Also check discovery for current concept health
105
+ const discovery = discoverKnowledgeDocuments(paths);
106
+ diagnostics.push(...discovery.diagnostics);
107
+ const registryPath = join(paths.meta, "registry.json");
108
+ let registry;
109
+ if (existsSync(registryPath)) {
110
+ registry = readJson(registryPath, { version: "1.0", last_updated: "", pages: {} });
111
+ }
112
+ const byType = {};
113
+ let totalPages = 0;
114
+ if (registry) {
115
+ for (const entry of Object.values(registry.pages)) {
116
+ totalPages++;
117
+ const type = String(entry.type || "unknown");
118
+ byType[type] = (byType[type] || 0) + 1;
119
+ }
120
+ }
121
+ return {
122
+ knowledgeFormat: vaultState.knowledgeFormat,
123
+ totalPages,
124
+ byType,
125
+ blockingDiagnostics: diagnostics.filter((d) => d.severity === "error"),
126
+ lastUpdated: registry?.last_updated || "",
127
+ };
128
+ }
@@ -0,0 +1,121 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
3
+ export function createExecApi() {
4
+ return {
5
+ exec(command, args, options = {}) {
6
+ return new Promise((resolve) => {
7
+ let killed = false;
8
+ let settled = false;
9
+ let stdoutLimited = false;
10
+ let stderrLimited = false;
11
+ let forceTimer;
12
+ let stdout = "";
13
+ let stderr = "";
14
+ const child = spawn(command, args, {
15
+ cwd: options.cwd,
16
+ stdio: ["ignore", "pipe", "pipe"],
17
+ detached: process.platform !== "win32",
18
+ });
19
+ const sendSignal = (signal) => {
20
+ if (process.platform === "win32" && child.pid) {
21
+ execFile("taskkill", ["/pid", String(child.pid), "/T", "/F"], () => { });
22
+ return;
23
+ }
24
+ if (child.pid) {
25
+ try {
26
+ process.kill(-child.pid, signal);
27
+ return;
28
+ }
29
+ catch {
30
+ // Fall back to the direct child when process-group signalling fails.
31
+ }
32
+ }
33
+ child.kill(signal);
34
+ };
35
+ const forceStop = () => {
36
+ if (forceTimer)
37
+ clearTimeout(forceTimer);
38
+ forceTimer = undefined;
39
+ sendSignal("SIGKILL");
40
+ };
41
+ const stop = () => {
42
+ if (killed)
43
+ return;
44
+ killed = true;
45
+ sendSignal("SIGTERM");
46
+ if (process.platform !== "win32")
47
+ forceTimer = setTimeout(forceStop, 100);
48
+ };
49
+ const appendOutput = (current, chunk, limited) => {
50
+ if (limited)
51
+ return [current, true];
52
+ const remaining = MAX_OUTPUT_BYTES - Buffer.byteLength(current);
53
+ if (remaining <= 0) {
54
+ stop();
55
+ return [current, true];
56
+ }
57
+ const bytes = Buffer.from(chunk);
58
+ if (bytes.byteLength <= remaining)
59
+ return [current + chunk, false];
60
+ stop();
61
+ let prefix = "";
62
+ let prefixBytes = 0;
63
+ for (const char of chunk) {
64
+ const charBytes = Buffer.byteLength(char);
65
+ if (prefixBytes + charBytes > remaining)
66
+ break;
67
+ prefix += char;
68
+ prefixBytes += charBytes;
69
+ }
70
+ return [current + prefix, true];
71
+ };
72
+ child.stdout?.setEncoding("utf8");
73
+ child.stderr?.setEncoding("utf8");
74
+ child.stdout?.on("data", (chunk) => {
75
+ [stdout, stdoutLimited] = appendOutput(stdout, chunk, stdoutLimited);
76
+ });
77
+ child.stderr?.on("data", (chunk) => {
78
+ [stderr, stderrLimited] = appendOutput(stderr, chunk, stderrLimited);
79
+ });
80
+ const timer = options.timeout ? setTimeout(stop, options.timeout) : undefined;
81
+ const abort = () => stop();
82
+ options.signal?.addEventListener("abort", abort, { once: true });
83
+ const finish = (code) => {
84
+ if (settled)
85
+ return;
86
+ if (forceTimer) {
87
+ clearTimeout(forceTimer);
88
+ forceTimer = undefined;
89
+ if (child.pid) {
90
+ try {
91
+ process.kill(-child.pid, "SIGKILL");
92
+ }
93
+ catch {
94
+ // The process group already exited; do not signal the closed child's stale PID.
95
+ }
96
+ }
97
+ }
98
+ cleanup();
99
+ resolve({
100
+ stdout,
101
+ stderr,
102
+ code: stdoutLimited || stderrLimited ? 1 : killed && code === 0 ? 1 : code,
103
+ killed,
104
+ });
105
+ };
106
+ child.once("error", () => finish(1));
107
+ child.once("close", (code) => finish(typeof code === "number" ? code : 1));
108
+ function cleanup() {
109
+ settled = true;
110
+ if (timer)
111
+ clearTimeout(timer);
112
+ if (forceTimer)
113
+ clearTimeout(forceTimer);
114
+ options.signal?.removeEventListener("abort", abort);
115
+ }
116
+ if (options.signal?.aborted)
117
+ stop();
118
+ });
119
+ },
120
+ };
121
+ }
@@ -0,0 +1,229 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * LLM Wiki MCP Server
4
+ *
5
+ * Exposes wiki tools over the Model Context Protocol (MCP).
6
+ * Run: node dist/mcp/index.js
7
+ *
8
+ * Environment:
9
+ * WIKI_ROOT — path to wiki vault (default: auto-detect from cwd)
10
+ */
11
+ import { existsSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { McpServer, StdioServerTransport } from "@modelcontextprotocol/server";
14
+ import * as z from "zod/v4";
15
+ import { resolveVaultPaths } from "../extensions/llm-wiki/lib/utils.js";
16
+ import { createExecApi } from "./exec.js";
17
+ import { captureSourceOperation, recallOperation, retroOperation, searchOperation, statusOperation, } from "./operations.js";
18
+ const execApi = createExecApi();
19
+ // ─── Vault Detection ────────────────────────────────────
20
+ /** Resolve vault paths, same as Pi extension. */
21
+ function getPaths() {
22
+ const root = process.env.WIKI_ROOT || process.cwd();
23
+ return resolveVaultPaths(root);
24
+ }
25
+ function hasVault() {
26
+ const paths = getPaths();
27
+ return existsSync(join(paths.dotWiki, "config.json"));
28
+ }
29
+ // ─── MCP Server ─────────────────────────────────────────
30
+ const server = new McpServer({
31
+ name: "llm-wiki",
32
+ version: "1.0.0",
33
+ });
34
+ // ---- wiki_recall ----
35
+ server.registerTool("wiki_recall", {
36
+ description: "Search the wiki for pages relevant to a query. Returns matching page IDs, titles, types, and content previews.",
37
+ inputSchema: z.object({
38
+ query: z.string().describe("Search query — use the user's full request or key terms"),
39
+ max_results: z.number().optional().default(5).describe("Max results (default: 5, max: 10)"),
40
+ }),
41
+ }, async ({ query, max_results }) => {
42
+ if (!hasVault()) {
43
+ return {
44
+ content: [
45
+ {
46
+ type: "text",
47
+ text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
48
+ },
49
+ ],
50
+ isError: true,
51
+ };
52
+ }
53
+ const paths = getPaths();
54
+ const result = await recallOperation(paths, query, Math.min(max_results ?? 5, 10));
55
+ const detailLines = result.diagnostics.map((d) => `⚠️ ${d.code}: ${d.message}`);
56
+ const detailText = detailLines.length > 0 ? `\n\n${detailLines.join("\n")}` : "";
57
+ return {
58
+ content: [
59
+ {
60
+ type: "text",
61
+ text: JSON.stringify(result.results, null, 2) + detailText,
62
+ },
63
+ ],
64
+ };
65
+ });
66
+ // ---- wiki_search ----
67
+ server.registerTool("wiki_search", {
68
+ description: "Search the wiki registry for pages matching a query.",
69
+ inputSchema: z.object({
70
+ query: z.string().describe("Search term"),
71
+ type: z
72
+ .string()
73
+ .optional()
74
+ .describe("Filter by page type (source, entity, concept, synthesis, analysis)"),
75
+ }),
76
+ }, async ({ query, type }) => {
77
+ if (!hasVault()) {
78
+ return {
79
+ content: [
80
+ {
81
+ type: "text",
82
+ text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
83
+ },
84
+ ],
85
+ isError: true,
86
+ };
87
+ }
88
+ const paths = getPaths();
89
+ const result = await searchOperation(paths, query, type);
90
+ if (result.matches.length === 0) {
91
+ return {
92
+ content: [{ type: "text", text: `No pages found for "${query}"` }],
93
+ };
94
+ }
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: JSON.stringify(result.matches, null, 2),
100
+ },
101
+ ],
102
+ };
103
+ });
104
+ // ---- wiki_status ----
105
+ server.registerTool("wiki_status", {
106
+ description: "Show wiki health and stats: page counts, orphans, recent activity.",
107
+ inputSchema: z.object({}),
108
+ }, async () => {
109
+ if (!hasVault()) {
110
+ return {
111
+ content: [
112
+ {
113
+ type: "text",
114
+ text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
115
+ },
116
+ ],
117
+ isError: true,
118
+ };
119
+ }
120
+ const paths = getPaths();
121
+ const status = await statusOperation(paths);
122
+ return {
123
+ content: [
124
+ {
125
+ type: "text",
126
+ text: JSON.stringify(status, null, 2),
127
+ },
128
+ ],
129
+ };
130
+ });
131
+ // ---- wiki_retro ----
132
+ server.registerTool("wiki_retro", {
133
+ description: "Save an atomic insight from a completed task into the wiki. Creates a source page.",
134
+ inputSchema: z.object({
135
+ slug: z.string().describe("Unique kebab-case identifier (e.g. 'jwt-revocation-pattern')"),
136
+ title: z.string().describe("Short descriptive title (60 chars max)"),
137
+ body: z.string().describe("Markdown body explaining what was learned."),
138
+ category: z
139
+ .string()
140
+ .optional()
141
+ .describe("Category (e.g. frontend, architecture, devops, bugfix)"),
142
+ }),
143
+ }, async ({ slug, title, body, category }) => {
144
+ if (!hasVault()) {
145
+ return {
146
+ content: [
147
+ {
148
+ type: "text",
149
+ text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
150
+ },
151
+ ],
152
+ isError: true,
153
+ };
154
+ }
155
+ const paths = getPaths();
156
+ const result = await retroOperation(paths, slug, title, body, category);
157
+ if (!result.ok) {
158
+ return {
159
+ content: [
160
+ {
161
+ type: "text",
162
+ text: `Vault error: ${result.diagnostics[0].message}`,
163
+ },
164
+ ],
165
+ isError: true,
166
+ };
167
+ }
168
+ return {
169
+ content: [
170
+ {
171
+ type: "text",
172
+ text: `Insight saved: ${result.slug} — ${title}`,
173
+ },
174
+ ],
175
+ };
176
+ });
177
+ // ---- wiki_capture_source ----
178
+ server.registerTool("wiki_capture_source", {
179
+ description: "Capture a URL, local file, or pasted text into an immutable source packet.",
180
+ inputSchema: z.object({
181
+ text: z.string().optional().describe("Text content to capture"),
182
+ url: z.string().optional().describe("URL to capture"),
183
+ file_path: z.string().optional().describe("Local file path to capture"),
184
+ title: z.string().optional().describe("Title for the captured source"),
185
+ }),
186
+ }, async ({ text, url, file_path, title }) => {
187
+ if (!hasVault()) {
188
+ return {
189
+ content: [
190
+ {
191
+ type: "text",
192
+ text: "No wiki vault found. Set WIKI_ROOT or run wiki_bootstrap first.",
193
+ },
194
+ ],
195
+ isError: true,
196
+ };
197
+ }
198
+ const paths = getPaths();
199
+ const result = await captureSourceOperation(paths, { text, url, filePath: file_path, title }, execApi);
200
+ if (!result.ok) {
201
+ return {
202
+ content: [
203
+ {
204
+ type: "text",
205
+ text: result.diagnostics[0].message,
206
+ },
207
+ ],
208
+ isError: true,
209
+ };
210
+ }
211
+ return {
212
+ content: [
213
+ {
214
+ type: "text",
215
+ text: `Source captured: ${result.sourceId}`,
216
+ },
217
+ ],
218
+ };
219
+ });
220
+ // ─── Main ───────────────────────────────────────────────
221
+ async function main() {
222
+ const transport = new StdioServerTransport();
223
+ await server.connect(transport);
224
+ console.error("🧠 LLM Wiki MCP Server running on stdio");
225
+ }
226
+ main().catch((err) => {
227
+ console.error("MCP Server error:", err);
228
+ process.exit(1);
229
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * MCP operation adapters over shared wiki services.
3
+ *
4
+ * Each operation is a thin, testable wrapper around the same services
5
+ * used by Pi tools. No operation parses YAML, scans files, scores
6
+ * registry entries, or builds page strings itself.
7
+ */
8
+ import { rebuildMetadata } from "../extensions/llm-wiki/lib/metadata.js";
9
+ import { searchWiki } from "../extensions/llm-wiki/lib/recall.js";
10
+ import { saveInsight } from "../extensions/llm-wiki/lib/retro.js";
11
+ import { captureFile, captureText, captureUrl } from "../extensions/llm-wiki/lib/source-packet.js";
12
+ import { VaultWriteError, inspectVaultFormat, inspectWritableVault, } from "../extensions/llm-wiki/lib/vault-format.js";
13
+ import { getWikiStatus, searchRegistry } from "../extensions/llm-wiki/lib/wiki-service.js";
14
+ function projectionOutcome(projection) {
15
+ return projection.ok
16
+ ? { ok: true }
17
+ : {
18
+ ok: false,
19
+ diagnostics: projection.diagnostics.map(({ code, message }) => ({ code, message })),
20
+ };
21
+ }
22
+ /** Shared recall operation: calls searchWiki and appends vault diagnostics. */
23
+ export async function recallOperation(paths, query, maxResults = 5) {
24
+ const results = searchWiki(paths, query, maxResults);
25
+ const vaultState = inspectVaultFormat(paths);
26
+ return {
27
+ results,
28
+ diagnostics: vaultState.diagnostics.map((d) => ({ code: d.code, message: d.message })),
29
+ };
30
+ }
31
+ /** Shared search operation: delegates directly to wiki-service. */
32
+ export async function searchOperation(paths, query, type) {
33
+ const result = searchRegistry(paths, query, type);
34
+ return {
35
+ matches: result.matches,
36
+ diagnostics: result.diagnostics.map((d) => ({ code: d.code, message: d.message })),
37
+ };
38
+ }
39
+ /** Shared status operation: delegates directly to wiki-service. */
40
+ export async function statusOperation(paths) {
41
+ const status = getWikiStatus(paths);
42
+ return {
43
+ knowledgeFormat: status.knowledgeFormat,
44
+ totalPages: status.totalPages,
45
+ byType: status.byType,
46
+ blockingDiagnostics: status.blockingDiagnostics.map((d) => ({
47
+ code: d.code,
48
+ message: d.message,
49
+ })),
50
+ lastUpdated: status.lastUpdated,
51
+ };
52
+ }
53
+ /** Shared retro operation: validates vault then delegates to saveInsight. */
54
+ export async function retroOperation(paths, slug, title, body, category) {
55
+ const vaultCheck = inspectWritableVault(paths);
56
+ if (!vaultCheck.ok) {
57
+ return {
58
+ ok: false,
59
+ diagnostics: vaultCheck.diagnostics.map((d) => ({ code: d.code, message: d.message })),
60
+ };
61
+ }
62
+ try {
63
+ const result = saveInsight(paths, slug, title, body, category, { rebuild: false });
64
+ const projection = projectionOutcome(rebuildMetadata(paths));
65
+ if (!projection.ok)
66
+ return projection;
67
+ return { ok: true, slug: result.slug, sourcePagePath: result.sourcePagePath };
68
+ }
69
+ catch (error) {
70
+ if (error instanceof VaultWriteError) {
71
+ return {
72
+ ok: false,
73
+ diagnostics: error.diagnostics.map((d) => ({ code: d.code, message: d.message })),
74
+ };
75
+ }
76
+ if (error.message.startsWith("Invalid insight slug:")) {
77
+ return {
78
+ ok: false,
79
+ diagnostics: [{ code: "invalid_insight_slug", message: error.message }],
80
+ };
81
+ }
82
+ throw error;
83
+ }
84
+ }
85
+ /** Shared capture operation: validates vault then delegates to capture functions. */
86
+ export async function captureSourceOperation(paths, input, execApi) {
87
+ const vaultCheck = inspectWritableVault(paths);
88
+ if (!vaultCheck.ok) {
89
+ return {
90
+ ok: false,
91
+ diagnostics: vaultCheck.diagnostics.map((d) => ({ code: d.code, message: d.message })),
92
+ };
93
+ }
94
+ try {
95
+ let sourceId;
96
+ if (input.url) {
97
+ sourceId = (await captureUrl(execApi, paths, input.url)).sourceId;
98
+ }
99
+ else if (input.filePath) {
100
+ sourceId = (await captureFile(execApi, paths, input.filePath)).sourceId;
101
+ }
102
+ else if (input.text) {
103
+ sourceId = captureText(paths, input.text, input.title).sourceId;
104
+ }
105
+ else {
106
+ return {
107
+ ok: false,
108
+ diagnostics: [
109
+ {
110
+ code: "event_missing_kind",
111
+ message: "Provide one of: text, url, or filePath",
112
+ },
113
+ ],
114
+ };
115
+ }
116
+ const projection = projectionOutcome(rebuildMetadata(paths));
117
+ if (!projection.ok)
118
+ return projection;
119
+ return { ok: true, sourceId };
120
+ }
121
+ catch (error) {
122
+ if (error instanceof VaultWriteError) {
123
+ return {
124
+ ok: false,
125
+ diagnostics: error.diagnostics.map((d) => ({ code: d.code, message: d.message })),
126
+ };
127
+ }
128
+ throw error;
129
+ }
130
+ }
@@ -0,0 +1 @@
1
+ {"type":"module"}