@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,158 @@
1
+ import { dirname, resolve } from "node:path";
2
+ import { Type } from "typebox";
3
+ import { scheduleReindex } from "./indexing.js";
4
+ import { createKnowledgeDocument, writeKnowledgeDocumentFile } from "./knowledge-document.js";
5
+ import { appendEvent, rebuildMetadataLight } from "./metadata.js";
6
+ import { fmtDate, resolveVaultPaths } from "./utils.js";
7
+ import { assertWritableVault, inspectWritableVault } from "./vault-format.js";
8
+ /**
9
+ * Save an atomic insight into the wiki as a single markdown file.
10
+ *
11
+ * Unlike wiki_capture_source (which creates a full source packet with
12
+ * manifest.json, extracted.md, and attachments), this is a lightweight
13
+ * path for quick knowledge capture — one file, one call.
14
+ *
15
+ * The 4-layer pipeline (raw → source pages → canonical pages → metadata)
16
+ * is still available via wiki_capture_source → wiki_ingest for deep research.
17
+ */
18
+ function insightPath(paths, slug) {
19
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug) || slug === "index" || slug === "log") {
20
+ throw new Error(`Invalid insight slug: ${slug}`);
21
+ }
22
+ const directory = resolve(paths.wiki, "sources");
23
+ const target = resolve(directory, `${slug}.md`);
24
+ if (dirname(target) !== directory)
25
+ throw new Error(`Invalid insight slug: ${slug}`);
26
+ return target;
27
+ }
28
+ export function saveInsight(paths, slug, title, body, category, opts) {
29
+ assertWritableVault(paths);
30
+ const today = fmtDate();
31
+ const sourcePagePath = insightPath(paths, slug);
32
+ const pageBody = `# ${title}
33
+
34
+ ${body}
35
+
36
+ ${category ? `*Category: ${category}*` : ""}
37
+
38
+ ---
39
+ *Captured: ${today}*
40
+
41
+ ## Related
42
+
43
+ _Add links to related pages._`;
44
+ const doc = createKnowledgeDocument(`sources/${slug}.md`, {
45
+ type: "source",
46
+ title,
47
+ slug,
48
+ status: "insight",
49
+ created: today,
50
+ updated: today,
51
+ ...(category ? { category } : {}),
52
+ }, pageBody);
53
+ writeKnowledgeDocumentFile(sourcePagePath, doc);
54
+ // Log event
55
+ appendEvent(paths, {
56
+ kind: "retro",
57
+ slug,
58
+ title,
59
+ category: category || "uncategorized",
60
+ });
61
+ // Rebuild metadata so the insight is immediately searchable. The wiki_retro
62
+ // tool passes { rebuild: false } and schedules a non-blocking reindex instead.
63
+ if (opts?.rebuild !== false)
64
+ rebuildMetadataLight(paths);
65
+ return { slug, sourcePagePath };
66
+ }
67
+ // ─── Tool Registration ──────────────────────────────────
68
+ /**
69
+ * Register the `wiki_retro` tool.
70
+ * The model calls this to save an atomic insight from a completed task.
71
+ * Inspired by the memex_retro pattern.
72
+ */
73
+ export function registerWikiRetro(pi, runtime) {
74
+ pi.registerTool({
75
+ name: "wiki_retro",
76
+ label: "Wiki Retro",
77
+ description: "Save an atomic insight from a completed task into the wiki. " +
78
+ "Creates a source packet and source page. The insight will be " +
79
+ "surfaced automatically by wiki_recall in future sessions.",
80
+ promptSnippet: "Save atomic insights from completed tasks into the wiki",
81
+ promptGuidelines: [
82
+ "Use wiki_retro at the END of every meaningful task to save what you learned.",
83
+ "Write atomic insights — one insight per call. Use multiple calls for multiple insights.",
84
+ "The insight will be auto-surfaced by wiki_recall in future sessions.",
85
+ ],
86
+ parameters: Type.Object({
87
+ slug: Type.String({
88
+ description: "Unique kebab-case identifier (e.g. 'jwt-revocation-pattern'). Used for lookups.",
89
+ }),
90
+ title: Type.String({
91
+ description: "Short descriptive title (60 chars max). Noun phrase, not a sentence.",
92
+ }),
93
+ body: Type.String({
94
+ description: "Markdown body with [[wikilinks]] to related wiki pages. Explain what was learned.",
95
+ }),
96
+ category: Type.Optional(Type.String({
97
+ description: "Optional category (e.g. frontend, architecture, devops, bugfix, design)",
98
+ })),
99
+ }),
100
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
101
+ const paths = resolveVaultPaths(ctx.cwd ?? process.cwd());
102
+ const vaultCheck = inspectWritableVault(paths);
103
+ if (!vaultCheck.ok) {
104
+ return {
105
+ content: [
106
+ {
107
+ type: "text",
108
+ text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}`,
109
+ },
110
+ ],
111
+ details: {
112
+ error: vaultCheck.diagnostics[0].code,
113
+ diagnostics: vaultCheck.diagnostics,
114
+ },
115
+ isError: true,
116
+ };
117
+ }
118
+ let result;
119
+ try {
120
+ result = saveInsight(paths, params.slug, params.title, params.body, params.category, {
121
+ rebuild: !runtime,
122
+ });
123
+ }
124
+ catch (error) {
125
+ if (error.message.startsWith("Invalid insight slug:")) {
126
+ return {
127
+ content: [{ type: "text", text: error.message }],
128
+ details: { error: "invalid_insight_slug" },
129
+ isError: true,
130
+ };
131
+ }
132
+ throw error;
133
+ }
134
+ if (runtime) {
135
+ scheduleReindex(runtime, { hasUI: ctx.hasUI, ui: ctx.ui }, paths);
136
+ }
137
+ return {
138
+ content: [
139
+ {
140
+ type: "text",
141
+ text: [
142
+ `🧠 **Insight saved**: ${params.title}`,
143
+ "",
144
+ `- Page: \`${result.sourcePagePath}\``,
145
+ "",
146
+ "This insight will be auto-surfaced by wiki_recall in future sessions.",
147
+ ].join("\n"),
148
+ },
149
+ ],
150
+ details: {
151
+ slug: params.slug,
152
+ title: params.title,
153
+ category: params.category || null,
154
+ },
155
+ };
156
+ },
157
+ });
158
+ }
@@ -0,0 +1,191 @@
1
+ import { TASK_DEFAULTS, loadTaskConfig, noticesEnabled } from "./task-config.js";
2
+ export class Runtime {
3
+ config = { ...TASK_DEFAULTS };
4
+ configLoaded = false;
5
+ /**
6
+ * Extension API handle, attached at registration. Used by `report()` to emit
7
+ * visible completion messages for background actions (issue #77). Optional so
8
+ * the Runtime stays unit-testable without a live `pi`.
9
+ */
10
+ pi;
11
+ /** Labels of tasks currently in flight (single-flight guard per label). */
12
+ inFlightLabels = new Set();
13
+ /** All in-flight task promises, keyed for await-at-exit and dedupe. */
14
+ inFlight = new Map();
15
+ /** Whether we've already surfaced a model-resolution failure (avoid spam). */
16
+ resolveFailureNotified = false;
17
+ ensureConfig(cwd) {
18
+ if (this.configLoaded)
19
+ return;
20
+ this.config = loadTaskConfig(cwd);
21
+ this.configLoaded = true;
22
+ }
23
+ /** True if a task with this label is currently running. */
24
+ isInFlight(label) {
25
+ return this.inFlightLabels.has(label);
26
+ }
27
+ /** Number of background tasks currently running. */
28
+ get pendingCount() {
29
+ return this.inFlight.size;
30
+ }
31
+ /**
32
+ * Resolve the model + auth for background work.
33
+ *
34
+ * Precedence (issue #69): per-call `override` → configured `taskModel` →
35
+ * session model. Each configured layer is applied only when the model is
36
+ * found in the registry; a missing layer warns (when UI is available) and
37
+ * falls through to the next. Returns { ok: false } when nothing resolves or
38
+ * no API key exists, so callers can fall back to the synchronous
39
+ * main-agent path.
40
+ */
41
+ async resolveModel(ctx, override) {
42
+ let model = ctx.model;
43
+ // Configured taskModel layer (beats the session model).
44
+ const configured = this.config.taskModel;
45
+ if (configured) {
46
+ const found = ctx.modelRegistry.find(configured.provider, configured.id);
47
+ if (found) {
48
+ model = found;
49
+ }
50
+ else if (ctx.hasUI && ctx.ui) {
51
+ ctx.ui.notify(`LLM Wiki: configured task model ${configured.provider}/${configured.id} not found, using session model`, "warning");
52
+ }
53
+ }
54
+ // Per-call override layer (beats both config and session).
55
+ if (override) {
56
+ const found = ctx.modelRegistry.find(override.provider, override.id);
57
+ if (found) {
58
+ model = found;
59
+ }
60
+ else if (ctx.hasUI && ctx.ui) {
61
+ ctx.ui.notify(`LLM Wiki: model override ${override.provider}/${override.id} not found, using ${configured ? "configured/session" : "session"} model`, "warning");
62
+ }
63
+ }
64
+ if (!model) {
65
+ return {
66
+ ok: false,
67
+ reason: "no model available (session has no model and no taskModel configured)",
68
+ };
69
+ }
70
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
71
+ if (!auth.ok || !auth.apiKey) {
72
+ const provider = model.provider ?? "unknown";
73
+ return { ok: false, reason: `no API key for provider "${provider}"` };
74
+ }
75
+ return { ok: true, model, apiKey: auth.apiKey, headers: auth.headers };
76
+ }
77
+ /**
78
+ * Fire-and-forget a background task.
79
+ *
80
+ * The work runs in a detached promise so the caller (an agent hook/tool)
81
+ * is never blocked. Errors are caught and surfaced via the UI (when
82
+ * available) instead of crashing the agent. Single-flight per label: if a
83
+ * task with the same label is already running, the new request is dropped
84
+ * and the existing promise is returned.
85
+ *
86
+ * The returned promise resolves when the work completes; hold onto it (or
87
+ * call awaitAll) to drain background work before compaction/exit.
88
+ */
89
+ launchTask(ctx, label, work) {
90
+ const existing = this.inFlight.get(label);
91
+ if (existing)
92
+ return existing;
93
+ // Capture ctx properties synchronously — after `await work()` the extension
94
+ // ctx may be stale (e.g. after newSession/fork/switchSession/reload), and
95
+ // accessing ctx.hasUI or ctx.ui on a stale proxy throws.
96
+ const hasUI = ctx.hasUI;
97
+ const ui = ctx.ui;
98
+ this.inFlightLabels.add(label);
99
+ // biome-ignore lint/style/useConst: referenced inside its own initializer (finally block)
100
+ let promise;
101
+ promise = (async () => {
102
+ try {
103
+ await work();
104
+ }
105
+ catch (error) {
106
+ const msg = error instanceof Error ? error.message : String(error);
107
+ if (hasUI && ui)
108
+ ui.notify(`LLM Wiki: ${label} failed: ${msg}`, "warning");
109
+ }
110
+ finally {
111
+ this.inFlightLabels.delete(label);
112
+ if (this.inFlight.get(label) === promise)
113
+ this.inFlight.delete(label);
114
+ }
115
+ })();
116
+ this.inFlight.set(label, promise);
117
+ return promise;
118
+ }
119
+ /**
120
+ * Report a completed background action to the user (issue #77).
121
+ *
122
+ * Every mutating wiki action runs off the agent's critical path; this is how
123
+ * the work becomes visible. Emits a `wiki-action-report` custom message,
124
+ * shown in the UI when notices are enabled (the `notices` config, default
125
+ * on) and otherwise injected silently. Delivered as `nextTurn` so it never
126
+ * interrupts or triggers a turn. Never throws — reporting must not crash the
127
+ * background task that called it.
128
+ */
129
+ report(summary, opts) {
130
+ if (!this.pi || !summary)
131
+ return;
132
+ const display = opts?.display ?? noticesEnabled(this.config);
133
+ try {
134
+ this.pi.sendMessage({ customType: "wiki-action-report", content: summary, display }, { deliverAs: "nextTurn" });
135
+ }
136
+ catch {
137
+ // Reporting is best-effort; a stale/torn-down session must not propagate.
138
+ }
139
+ }
140
+ /**
141
+ * Run a mutating action in the background and report its result (issue #77).
142
+ *
143
+ * Thin wrapper over `launchTask`: `work` performs the off-thread mutation and
144
+ * returns a one-line human summary (or null to stay silent). On success the
145
+ * summary is surfaced via `report()`. Single-flight, error-isolated, and
146
+ * awaited-at-exit exactly like `launchTask`.
147
+ */
148
+ launchReported(ctx, label, work) {
149
+ return this.launchTask(ctx, label, async () => {
150
+ const summary = await work();
151
+ if (summary)
152
+ this.report(summary);
153
+ });
154
+ }
155
+ /**
156
+ * Await all in-flight background tasks. Call at compaction / session exit so
157
+ * background work is not lost. Never rejects — task errors are already
158
+ * isolated inside launchTask.
159
+ */
160
+ async awaitAll() {
161
+ while (this.inFlight.size > 0) {
162
+ await Promise.allSettled([...this.inFlight.values()]);
163
+ }
164
+ }
165
+ }
166
+ /**
167
+ * Register the shared background runtime and wire it into the extension
168
+ * lifecycle: config is loaded lazily per turn, and in-flight tasks are drained
169
+ * before compaction and on shutdown so background work is never lost.
170
+ *
171
+ * Returns the Runtime instance so concrete background workers (issues #65,
172
+ * #66) can launch tasks on it.
173
+ */
174
+ export function registerBackgroundRuntime(pi) {
175
+ const runtime = new Runtime();
176
+ // Attach the API so background tasks can emit visible completion reports
177
+ // (issue #77). Done here (not in the constructor) to keep Runtime testable.
178
+ runtime.pi = pi;
179
+ pi.on("turn_start", (_event, ctx) => {
180
+ runtime.ensureConfig(ctx.cwd);
181
+ });
182
+ // Drain in-flight background work before the session is compacted or shut
183
+ // down, so nothing is lost mid-flight.
184
+ pi.on("session_before_compact", async () => {
185
+ await runtime.awaitAll();
186
+ });
187
+ pi.on("session_shutdown", async () => {
188
+ await runtime.awaitAll();
189
+ });
190
+ return runtime;
191
+ }