@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.
- package/CHANGELOG.md +4 -0
- package/README.de.md +35 -4
- package/README.es.md +260 -170
- package/README.fr.md +35 -4
- package/README.hi.md +35 -4
- package/README.ja.md +35 -4
- package/README.ko.md +35 -4
- package/README.md +38 -3
- package/README.pt.md +35 -4
- package/README.ru.md +35 -4
- package/README.zh.md +260 -170
- package/assets/demo.gif +0 -0
- package/dist/extensions/llm-wiki/lib/bootstrap.js +71 -0
- package/dist/extensions/llm-wiki/lib/embeddings.js +401 -0
- package/dist/extensions/llm-wiki/lib/guardrails.js +232 -0
- package/dist/extensions/llm-wiki/lib/indexing.js +78 -0
- package/dist/extensions/llm-wiki/lib/ingest-worker.js +310 -0
- package/dist/extensions/llm-wiki/lib/inject.js +65 -0
- package/dist/extensions/llm-wiki/lib/knowledge-document.js +442 -0
- package/dist/extensions/llm-wiki/lib/knowledge-links.js +206 -0
- package/dist/extensions/llm-wiki/lib/legacy-repair.js +443 -0
- package/dist/extensions/llm-wiki/lib/metadata.js +499 -0
- package/dist/extensions/llm-wiki/lib/model-command.js +86 -0
- package/dist/extensions/llm-wiki/lib/observation.js +283 -0
- package/dist/extensions/llm-wiki/lib/recall.js +875 -0
- package/dist/extensions/llm-wiki/lib/retro.js +158 -0
- package/dist/extensions/llm-wiki/lib/runtime.js +191 -0
- package/dist/extensions/llm-wiki/lib/source-extractors.js +426 -0
- package/dist/extensions/llm-wiki/lib/source-packet.js +229 -0
- package/dist/extensions/llm-wiki/lib/subagent.js +41 -0
- package/dist/extensions/llm-wiki/lib/task-config.js +172 -0
- package/dist/extensions/llm-wiki/lib/tools.js +1192 -0
- package/dist/extensions/llm-wiki/lib/trajectories-command.js +51 -0
- package/dist/extensions/llm-wiki/lib/trajectory.js +467 -0
- package/dist/extensions/llm-wiki/lib/utils.js +347 -0
- package/dist/extensions/llm-wiki/lib/vault-format.js +247 -0
- package/dist/extensions/llm-wiki/lib/visible-status.js +31 -0
- package/dist/extensions/llm-wiki/lib/wiki-service.js +128 -0
- package/dist/mcp/exec.js +121 -0
- package/dist/mcp/index.js +229 -0
- package/dist/mcp/operations.js +130 -0
- package/dist/package.json +1 -0
- package/docs/superpowers/plans/2026-08-02-okf-foundation.md +1579 -0
- package/docs/superpowers/plans/2026-08-03-okf-foundation-remediation.md +3005 -0
- package/docs/superpowers/plans/2026-08-06-okf-foundation-release-remediation.md +1174 -0
- package/docs/superpowers/specs/2026-08-02-okf-foundation-design.md +578 -0
- package/docs/superpowers/specs/2026-08-02-okf-v0.2-interoperability-design.md +538 -0
- package/extensions/llm-wiki/index.ts +22 -36
- package/extensions/llm-wiki/lib/bootstrap.ts +84 -0
- package/extensions/llm-wiki/lib/embeddings.ts +9 -3
- package/extensions/llm-wiki/lib/guardrails.ts +174 -29
- package/extensions/llm-wiki/lib/indexing.ts +2 -1
- package/extensions/llm-wiki/lib/ingest-worker.ts +170 -29
- package/extensions/llm-wiki/lib/knowledge-document.ts +661 -0
- package/extensions/llm-wiki/lib/knowledge-links.ts +282 -0
- package/extensions/llm-wiki/lib/legacy-repair.ts +572 -0
- package/extensions/llm-wiki/lib/metadata.ts +531 -116
- package/extensions/llm-wiki/lib/observation.ts +37 -43
- package/extensions/llm-wiki/lib/recall.ts +61 -33
- package/extensions/llm-wiki/lib/retro.ts +65 -41
- package/extensions/llm-wiki/lib/source-extractors.ts +12 -17
- package/extensions/llm-wiki/lib/source-packet.ts +44 -31
- package/extensions/llm-wiki/lib/tools.ts +406 -348
- package/extensions/llm-wiki/lib/trajectory.ts +15 -1
- package/extensions/llm-wiki/lib/utils.ts +121 -130
- package/extensions/llm-wiki/lib/vault-format.ts +363 -0
- package/extensions/llm-wiki/lib/wiki-service.ts +183 -0
- package/mcp/exec.ts +122 -0
- package/mcp/index.ts +60 -250
- package/mcp/operations.ts +176 -0
- package/package.json +8 -2
- package/scripts/migrate-llm-wiki.js +801 -0
- 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
|
+
}
|