@zosmaai/pi-llm-wiki 0.10.9 → 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 +31 -10
- 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 +26 -18
- 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 +404 -342
- package/extensions/llm-wiki/lib/trajectory.ts +15 -1
- package/extensions/llm-wiki/lib/utils.ts +120 -131
- 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
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
resolveVaultPaths,
|
|
13
13
|
writeJson,
|
|
14
14
|
} from "./utils.js";
|
|
15
|
+
import { assertWritableVault, inspectWritableVault } from "./vault-format.js";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Agent trajectory memory — the working-memory half of the wiki.
|
|
@@ -230,6 +231,7 @@ export function captureTrajectory(
|
|
|
230
231
|
paths: VaultPaths,
|
|
231
232
|
input: CaptureTrajectoryInput,
|
|
232
233
|
): CaptureTrajectoryResult {
|
|
234
|
+
assertWritableVault(paths);
|
|
233
235
|
const trajectoryId = nextTrajectoryId(paths);
|
|
234
236
|
const packetPath = join(paths.rawTrajectories, trajectoryId);
|
|
235
237
|
mkdirSync(packetPath, { recursive: true });
|
|
@@ -356,7 +358,19 @@ export function registerWikiCaptureTrajectory(pi: ExtensionAPI): void {
|
|
|
356
358
|
}),
|
|
357
359
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx: ExtensionContext) {
|
|
358
360
|
const paths = resolveVaultPaths(ctx.cwd ?? process.cwd());
|
|
359
|
-
|
|
361
|
+
const vaultCheck = inspectWritableVault(paths);
|
|
362
|
+
if (!vaultCheck.ok) {
|
|
363
|
+
return {
|
|
364
|
+
content: [
|
|
365
|
+
{ type: "text", text: `Wiki vault error: ${vaultCheck.diagnostics[0].message}` },
|
|
366
|
+
],
|
|
367
|
+
details: {
|
|
368
|
+
error: vaultCheck.diagnostics[0].code,
|
|
369
|
+
diagnostics: vaultCheck.diagnostics,
|
|
370
|
+
} as Record<string, unknown>,
|
|
371
|
+
isError: true,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
360
374
|
|
|
361
375
|
let steps = params.steps as TrajectoryStep[] | undefined;
|
|
362
376
|
let model = params.model;
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
existsSync,
|
|
3
|
+
lstatSync,
|
|
3
4
|
mkdirSync,
|
|
4
5
|
readFileSync,
|
|
5
6
|
readdirSync,
|
|
7
|
+
readlinkSync,
|
|
8
|
+
realpathSync,
|
|
6
9
|
renameSync,
|
|
7
10
|
rmdirSync,
|
|
8
|
-
statSync,
|
|
9
11
|
writeFileSync,
|
|
10
12
|
} from "node:fs";
|
|
11
13
|
import { homedir } from "node:os";
|
|
12
|
-
import { dirname, join,
|
|
14
|
+
import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
13
15
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -33,13 +35,12 @@ export type VaultFormat = "new" | "legacy" | "none";
|
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* Detect the vault format at a given directory.
|
|
36
|
-
* Returns "new" if .llm-wiki
|
|
37
|
-
* "
|
|
38
|
-
* "none" otherwise.
|
|
38
|
+
* Returns "new" if .llm-wiki exists, "legacy" if .wiki exists,
|
|
39
|
+
* and "none" otherwise. A missing config is still a detected, damaged vault.
|
|
39
40
|
*/
|
|
40
41
|
export function detectVaultFormat(dir: string): VaultFormat {
|
|
41
|
-
if (existsSync(join(dir, ".llm-wiki"
|
|
42
|
-
if (existsSync(join(dir, ".wiki"
|
|
42
|
+
if (existsSync(join(dir, ".llm-wiki"))) return "new";
|
|
43
|
+
if (existsSync(join(dir, ".wiki"))) return "legacy";
|
|
43
44
|
return "none";
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -134,9 +135,13 @@ export function isPersonalVault(paths: VaultPaths): boolean {
|
|
|
134
135
|
* 4. Fallback: ~/.llm-wiki/ (create personal wiki)
|
|
135
136
|
*/
|
|
136
137
|
export function resolveVaultRoot(cwd: string): string {
|
|
137
|
-
//
|
|
138
|
+
// A vault rooted at cwd is always the project-local choice.
|
|
138
139
|
if (detectVaultFormat(cwd) !== "none") return cwd;
|
|
139
140
|
|
|
141
|
+
// An explicit WIKI_HOME is a testable/user-selected fallback and must win
|
|
142
|
+
// over an unrelated personal vault found while walking parent directories.
|
|
143
|
+
if (process.env.WIKI_HOME) return process.env.WIKI_HOME;
|
|
144
|
+
|
|
140
145
|
// Walk up looking for a vault sentinel (new or legacy)
|
|
141
146
|
let dir = cwd;
|
|
142
147
|
while (dir !== dirname(dir)) {
|
|
@@ -271,113 +276,37 @@ function nextSequentialId(dir: string, kind: string): string {
|
|
|
271
276
|
return `${prefix}-${String(num + 1).padStart(3, "0")}`;
|
|
272
277
|
}
|
|
273
278
|
|
|
274
|
-
/**
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return unquote ? unquoted(trimmed) : trimmed;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** Extract frontmatter from markdown. */
|
|
291
|
-
export function parseFrontmatter(content: string): {
|
|
292
|
-
frontmatter: Record<string, unknown>;
|
|
293
|
-
body: string;
|
|
294
|
-
} {
|
|
295
|
-
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
296
|
-
if (!match) return { frontmatter: {}, body: content };
|
|
297
|
-
|
|
298
|
-
const frontmatter: Record<string, unknown> = {};
|
|
299
|
-
const lines = match[1].split("\n");
|
|
300
|
-
let currentListKey: string | null = null;
|
|
301
|
-
|
|
302
|
-
for (const line of lines) {
|
|
303
|
-
const listMatch = line.match(/^\s*-\s+(.*)$/);
|
|
304
|
-
if (listMatch && currentListKey) {
|
|
305
|
-
const current = frontmatter[currentListKey];
|
|
306
|
-
const list = Array.isArray(current) ? current : [];
|
|
307
|
-
list.push(parseFrontmatterValue(listMatch[1], true));
|
|
308
|
-
frontmatter[currentListKey] = list;
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const idx = line.indexOf(":");
|
|
313
|
-
if (idx <= 0) {
|
|
314
|
-
currentListKey = null;
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const key = line.slice(0, idx).trim();
|
|
319
|
-
const val = line.slice(idx + 1).trim();
|
|
320
|
-
|
|
321
|
-
if (!val) {
|
|
322
|
-
frontmatter[key] = [];
|
|
323
|
-
currentListKey = key;
|
|
324
|
-
} else {
|
|
325
|
-
frontmatter[key] = parseFrontmatterValue(val);
|
|
326
|
-
currentListKey = null;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
return { frontmatter, body: match[2] };
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/** Find all wiki pages recursively. */
|
|
333
|
-
export function findWikiPages(
|
|
334
|
-
wikiDir: string,
|
|
335
|
-
): Array<{ path: string; relative: string; content: string }> {
|
|
336
|
-
const results: Array<{ path: string; relative: string; content: string }> = [];
|
|
337
|
-
|
|
338
|
-
function walk(dir: string, rel: string) {
|
|
339
|
-
if (!existsSync(dir)) return;
|
|
340
|
-
for (const entry of readdirSync(dir)) {
|
|
341
|
-
const full = join(dir, entry);
|
|
342
|
-
const stat = statSync(full);
|
|
343
|
-
if (stat.isDirectory()) {
|
|
344
|
-
walk(full, rel ? `${rel}/${entry}` : entry);
|
|
345
|
-
} else if (entry.endsWith(".md")) {
|
|
346
|
-
results.push({
|
|
347
|
-
path: full,
|
|
348
|
-
relative: rel ? `${rel}/${entry.slice(0, -3)}` : entry.slice(0, -3),
|
|
349
|
-
content: readFileSync(full, "utf-8"),
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
walk(wikiDir, "");
|
|
356
|
-
return results;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** Extract all [[wikilinks]] from content. */
|
|
360
|
-
export function extractWikilinks(content: string): string[] {
|
|
361
|
-
const links: string[] = [];
|
|
362
|
-
const regex = /\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g;
|
|
363
|
-
let m: RegExpExecArray | null = regex.exec(content);
|
|
364
|
-
while (m !== null) {
|
|
365
|
-
links.push(m[1]);
|
|
366
|
-
m = regex.exec(content);
|
|
367
|
-
}
|
|
368
|
-
return links;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/** Slugify a title. */
|
|
279
|
+
/**
|
|
280
|
+
* Slugify a title to a kebab-case page slug.
|
|
281
|
+
*
|
|
282
|
+
* - Folds full-width ASCII forms and ideographic spaces without changing
|
|
283
|
+
* unrelated Unicode compatibility characters.
|
|
284
|
+
* - Collapses whitespace AND existing hyphens into a single `-`.
|
|
285
|
+
* - Trims leading/trailing hyphens.
|
|
286
|
+
* - Prefixes Windows reserved device names (CON, PRN, AUX, NUL, COM1-9,
|
|
287
|
+
* LPT1-9, including superscript 1-3 forms) with `_`.
|
|
288
|
+
* - Appends `-page` to `index`/`log` slugs to avoid colliding with the
|
|
289
|
+
* special INDEX/LOG wiki pages.
|
|
290
|
+
*/
|
|
372
291
|
export function slugify(title: string): string {
|
|
373
|
-
|
|
292
|
+
const slug =
|
|
374
293
|
title
|
|
375
|
-
.
|
|
294
|
+
.replace(/[\uFF01-\uFF5E]/g, (char) => String.fromCharCode(char.charCodeAt(0) - 0xfee0))
|
|
295
|
+
.replace(/\u3000/g, " ")
|
|
296
|
+
.toLowerCase()
|
|
297
|
+
.normalize("NFC")
|
|
376
298
|
.replace(/[^\p{L}\p{N}\s-]/gu, "")
|
|
377
299
|
.trim()
|
|
378
|
-
.replace(
|
|
379
|
-
.
|
|
380
|
-
|
|
300
|
+
.replace(/[\s-]+/g, "-")
|
|
301
|
+
.replace(/^-+|-+$/g, "")
|
|
302
|
+
.slice(0, 80)
|
|
303
|
+
.replace(/[\uD800-\uDBFF]$/, "")
|
|
304
|
+
.replace(/^-+|-+$/g, "") || "untitled";
|
|
305
|
+
|
|
306
|
+
if (/^(?:con|prn|aux|nul|com(?:[1-9¹²³])|lpt(?:[1-9¹²³]))$/.test(slug)) {
|
|
307
|
+
return `_${slug}`;
|
|
308
|
+
}
|
|
309
|
+
return slug === "index" || slug === "log" ? `${slug}-page` : slug;
|
|
381
310
|
}
|
|
382
311
|
|
|
383
312
|
/** Format date as YYYY-MM-DD. */
|
|
@@ -385,38 +314,98 @@ export function fmtDate(d = new Date()): string {
|
|
|
385
314
|
return d.toISOString().split("T")[0];
|
|
386
315
|
}
|
|
387
316
|
|
|
388
|
-
/**
|
|
317
|
+
/** Narrow exec-only interface shared by Pi and MCP. */
|
|
318
|
+
export type ExecApi = Pick<ExtensionAPI, "exec">;
|
|
319
|
+
|
|
320
|
+
/** Run a shell command via pi.exec and reject failed or cancelled commands. */
|
|
389
321
|
export async function exec(
|
|
390
|
-
pi:
|
|
322
|
+
pi: ExecApi,
|
|
391
323
|
command: string,
|
|
392
324
|
args: string[],
|
|
393
325
|
options?: { signal?: AbortSignal; timeout?: number; cwd?: string },
|
|
394
|
-
): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
326
|
+
): Promise<{ stdout: string; stderr: string; code: number; killed: boolean }> {
|
|
395
327
|
const result = await pi.exec(command, args, options ?? {});
|
|
328
|
+
if (result.killed) throw new Error(`Command timed out or was aborted: ${command}`);
|
|
329
|
+
if (result.code !== 0) {
|
|
330
|
+
const detail = result.stderr.trim();
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Command failed (${command} exited ${result.code})${detail ? `: ${detail}` : ""}`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
396
335
|
return result;
|
|
397
336
|
}
|
|
398
337
|
|
|
338
|
+
function appendPath(base: string, ...parts: string[]): string {
|
|
339
|
+
if (parts.length === 0) return base;
|
|
340
|
+
return `${base}${base.endsWith(sep) ? "" : sep}${parts.join(sep)}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function realpathWithMissingTail(path: string, seen = new Set<string>()): string {
|
|
344
|
+
let current = path;
|
|
345
|
+
const tail: string[] = [];
|
|
346
|
+
|
|
347
|
+
while (true) {
|
|
348
|
+
try {
|
|
349
|
+
const stat = lstatSync(current);
|
|
350
|
+
if (stat.isSymbolicLink()) {
|
|
351
|
+
const symlinkPath = appendPath(realpathSync.native(dirname(current)), basename(current));
|
|
352
|
+
if (seen.has(symlinkPath)) throw new Error(`Cannot resolve symlink cycle: ${path}`);
|
|
353
|
+
seen.add(symlinkPath);
|
|
354
|
+
const target = readlinkSync(current).toString();
|
|
355
|
+
const resolvedTarget = isAbsolute(target) ? target : appendPath(dirname(current), target);
|
|
356
|
+
return realpathWithMissingTail(appendPath(resolvedTarget, ...tail.reverse()), seen);
|
|
357
|
+
}
|
|
358
|
+
} catch (error: unknown) {
|
|
359
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
return appendPath(realpathSync.native(current), ...tail.reverse());
|
|
364
|
+
} catch (error: unknown) {
|
|
365
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
366
|
+
const parent = dirname(current);
|
|
367
|
+
if (parent === current) throw error;
|
|
368
|
+
tail.push(basename(current));
|
|
369
|
+
current = parent;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** Return the candidate path relative to the physical root. */
|
|
375
|
+
export function relativePhysicalPath(rootPath: string, candidatePath: string): string {
|
|
376
|
+
return relative(realpathWithMissingTail(rootPath), realpathWithMissingTail(candidatePath));
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Check physical containment, resolving existing symlink ancestors. */
|
|
380
|
+
export function isPathWithin(rootPath: string, candidatePath: string): boolean {
|
|
381
|
+
const relation = relativePhysicalPath(rootPath, candidatePath);
|
|
382
|
+
return (
|
|
383
|
+
relation === "" ||
|
|
384
|
+
(!isAbsolute(relation) && relation !== ".." && !relation.startsWith(`..${sep}`))
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
399
388
|
/** Check if a path is inside a protected directory. */
|
|
400
389
|
export function isProtectedPath(
|
|
401
390
|
absPath: string,
|
|
402
391
|
paths: VaultPaths,
|
|
403
392
|
): { protected: boolean; reason?: string } {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
reason: "Metadata is auto-generated. Use wiki_rebuild_meta or wiki_log_event instead.",
|
|
418
|
-
};
|
|
419
|
-
}
|
|
393
|
+
try {
|
|
394
|
+
if (isPathWithin(paths.raw, absPath)) {
|
|
395
|
+
return {
|
|
396
|
+
protected: true,
|
|
397
|
+
reason: "Raw sources are immutable. Use wiki_capture_source to add sources.",
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
if (isPathWithin(paths.meta, absPath)) {
|
|
401
|
+
return {
|
|
402
|
+
protected: true,
|
|
403
|
+
reason: "Metadata is auto-generated. Use wiki_rebuild_meta or wiki_log_event instead.",
|
|
404
|
+
};
|
|
405
|
+
}
|
|
420
406
|
|
|
421
|
-
|
|
407
|
+
return { protected: false };
|
|
408
|
+
} catch {
|
|
409
|
+
return { protected: true, reason: "Cannot safely resolve mutation path." };
|
|
410
|
+
}
|
|
422
411
|
}
|