@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
|
@@ -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,127 +276,113 @@ 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
|
-
|
|
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
|
+
*/
|
|
291
|
+
export function slugify(title: string): string {
|
|
292
|
+
const slug =
|
|
293
|
+
title
|
|
294
|
+
.replace(/[\uFF01-\uFF5E]/g, (char) => String.fromCharCode(char.charCodeAt(0) - 0xfee0))
|
|
295
|
+
.replace(/\u3000/g, " ")
|
|
296
|
+
.toLowerCase()
|
|
297
|
+
.normalize("NFC")
|
|
298
|
+
.replace(/[^\p{L}\p{N}\s-]/gu, "")
|
|
299
|
+
.trim()
|
|
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}`;
|
|
285
308
|
}
|
|
286
|
-
|
|
287
|
-
return unquote ? unquoted(trimmed) : trimmed;
|
|
309
|
+
return slug === "index" || slug === "log" ? `${slug}-page` : slug;
|
|
288
310
|
}
|
|
289
311
|
|
|
290
|
-
/**
|
|
291
|
-
export function
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
}
|
|
312
|
+
/** Format date as YYYY-MM-DD. */
|
|
313
|
+
export function fmtDate(d = new Date()): string {
|
|
314
|
+
return d.toISOString().split("T")[0];
|
|
315
|
+
}
|
|
317
316
|
|
|
318
|
-
|
|
319
|
-
|
|
317
|
+
/** Narrow exec-only interface shared by Pi and MCP. */
|
|
318
|
+
export type ExecApi = Pick<ExtensionAPI, "exec">;
|
|
320
319
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
320
|
+
/** Run a shell command via pi.exec and reject failed or cancelled commands. */
|
|
321
|
+
export async function exec(
|
|
322
|
+
pi: ExecApi,
|
|
323
|
+
command: string,
|
|
324
|
+
args: string[],
|
|
325
|
+
options?: { signal?: AbortSignal; timeout?: number; cwd?: string },
|
|
326
|
+
): Promise<{ stdout: string; stderr: string; code: number; killed: boolean }> {
|
|
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
|
+
);
|
|
328
334
|
}
|
|
329
|
-
return
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
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)}`;
|
|
330
341
|
}
|
|
331
342
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
});
|
|
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);
|
|
351
357
|
}
|
|
358
|
+
} catch (error: unknown) {
|
|
359
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
352
360
|
}
|
|
353
|
-
}
|
|
354
361
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
while (m !== null) {
|
|
365
|
-
links.push(m[1]);
|
|
366
|
-
m = regex.exec(content);
|
|
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
|
+
}
|
|
367
371
|
}
|
|
368
|
-
return links;
|
|
369
372
|
}
|
|
370
373
|
|
|
371
|
-
/**
|
|
372
|
-
export function
|
|
373
|
-
return
|
|
374
|
-
.toLowerCase()
|
|
375
|
-
.replace(/[^a-z0-9\s-]/g, "")
|
|
376
|
-
.trim()
|
|
377
|
-
.replace(/\s+/g, "-")
|
|
378
|
-
.slice(0, 80);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/** Format date as YYYY-MM-DD. */
|
|
382
|
-
export function fmtDate(d = new Date()): string {
|
|
383
|
-
return d.toISOString().split("T")[0];
|
|
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));
|
|
384
377
|
}
|
|
385
378
|
|
|
386
|
-
/**
|
|
387
|
-
export
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
)
|
|
393
|
-
const result = await pi.exec(command, args, options ?? {});
|
|
394
|
-
return result;
|
|
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
|
+
);
|
|
395
386
|
}
|
|
396
387
|
|
|
397
388
|
/** Check if a path is inside a protected directory. */
|
|
@@ -399,22 +390,22 @@ export function isProtectedPath(
|
|
|
399
390
|
absPath: string,
|
|
400
391
|
paths: VaultPaths,
|
|
401
392
|
): { protected: boolean; reason?: string } {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
reason: "Metadata is auto-generated. Use wiki_rebuild_meta or wiki_log_event instead.",
|
|
416
|
-
};
|
|
417
|
-
}
|
|
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
|
+
}
|
|
418
406
|
|
|
419
|
-
|
|
407
|
+
return { protected: false };
|
|
408
|
+
} catch {
|
|
409
|
+
return { protected: true, reason: "Cannot safely resolve mutation path." };
|
|
410
|
+
}
|
|
420
411
|
}
|