@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,801 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* migrate-llm-wiki.js
|
|
5
|
+
*
|
|
6
|
+
* One-time migration script for wiki vault layouts.
|
|
7
|
+
*
|
|
8
|
+
* Two migrations are supported:
|
|
9
|
+
*
|
|
10
|
+
* 1. LEGACY → NEW (default): moves an old-style vault (`.wiki/` sentinel)
|
|
11
|
+
* to the new `.llm-wiki/` layout.
|
|
12
|
+
*
|
|
13
|
+
* 2. DOUBLED → FLATTENED (`--fix-doubled`): flattens a vault that was
|
|
14
|
+
* accidentally created at `<root>/.llm-wiki/.llm-wiki/…` due to a bug in
|
|
15
|
+
* `getPersonalWikiRoot()` (fixed in 0.6.4). The extension auto-runs this
|
|
16
|
+
* migration on `session_start`, but the script is provided for manual
|
|
17
|
+
* recovery on arbitrary roots.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* node scripts/migrate-llm-wiki.js # Legacy migration in cwd
|
|
21
|
+
* node scripts/migrate-llm-wiki.js ~/my-wiki # Legacy migration at path
|
|
22
|
+
* node scripts/migrate-llm-wiki.js --dry-run # Preview without changes
|
|
23
|
+
* node scripts/migrate-llm-wiki.js --force # Skip confirmation prompt
|
|
24
|
+
* node scripts/migrate-llm-wiki.js --fix-doubled # Flatten doubled .llm-wiki/.llm-wiki
|
|
25
|
+
* node scripts/migrate-llm-wiki.js --fix-doubled ~/ # …at a specific root
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const { createHash, randomUUID } = require("node:crypto");
|
|
29
|
+
const {
|
|
30
|
+
closeSync,
|
|
31
|
+
constants,
|
|
32
|
+
existsSync,
|
|
33
|
+
fstatSync,
|
|
34
|
+
fsyncSync,
|
|
35
|
+
linkSync,
|
|
36
|
+
lstatSync,
|
|
37
|
+
mkdirSync,
|
|
38
|
+
openSync,
|
|
39
|
+
readFileSync,
|
|
40
|
+
readdirSync,
|
|
41
|
+
readlinkSync,
|
|
42
|
+
renameSync,
|
|
43
|
+
rmdirSync,
|
|
44
|
+
statSync,
|
|
45
|
+
symlinkSync,
|
|
46
|
+
unlinkSync,
|
|
47
|
+
writeFileSync,
|
|
48
|
+
} = require("node:fs");
|
|
49
|
+
const { homedir } = require("node:os");
|
|
50
|
+
const { dirname, join, resolve } = require("node:path");
|
|
51
|
+
|
|
52
|
+
// ─── Helpers ────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const DRY_RUN = process.argv.includes("--dry-run");
|
|
55
|
+
const FORCE = process.argv.includes("--force");
|
|
56
|
+
const FIX_DOUBLED = process.argv.includes("--fix-doubled");
|
|
57
|
+
|
|
58
|
+
function resolvePositionalRoot(defaultRoot) {
|
|
59
|
+
const positional = process.argv.slice(2).find((argument) => !argument.startsWith("--"));
|
|
60
|
+
return positional ? resolve(process.cwd(), positional) : defaultRoot;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function log(action, ...args) {
|
|
64
|
+
const prefix = DRY_RUN ? "[DRY-RUN]" : "[MIGRATE]";
|
|
65
|
+
console.log(`${prefix} ${action}`, ...args);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const JOURNAL_NAME = "MIGRATION_TO_LLM_WIKI.json";
|
|
69
|
+
const JOURNAL_VERSION = 1;
|
|
70
|
+
|
|
71
|
+
function readRegularFile(path, encoding) {
|
|
72
|
+
const flags =
|
|
73
|
+
process.platform === "win32" ? "r" : constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
|
|
74
|
+
const handle = openSync(path, flags);
|
|
75
|
+
try {
|
|
76
|
+
if (!fstatSync(handle).isFile()) throw new Error(`Expected regular file: ${path}`);
|
|
77
|
+
return readFileSync(handle, encoding);
|
|
78
|
+
} finally {
|
|
79
|
+
closeSync(handle);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readEntry(path) {
|
|
84
|
+
const flags =
|
|
85
|
+
process.platform === "win32" ? "r" : constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0);
|
|
86
|
+
let handle;
|
|
87
|
+
try {
|
|
88
|
+
handle = openSync(path, flags);
|
|
89
|
+
const stat = fstatSync(handle);
|
|
90
|
+
if (stat.isFile()) return { kind: "file", content: readFileSync(handle) };
|
|
91
|
+
if (stat.isDirectory()) return { kind: "directory" };
|
|
92
|
+
throw new Error(`Unsupported filesystem entry: ${path}`);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if ((error.code === "ELOOP" || error.code === "EPERM") && lstatSync(path).isSymbolicLink()) {
|
|
95
|
+
return { kind: "symlink", target: readlinkSync(path) };
|
|
96
|
+
}
|
|
97
|
+
if (error.code === "EISDIR" && lstatSync(path).isDirectory()) return { kind: "directory" };
|
|
98
|
+
throw error;
|
|
99
|
+
} finally {
|
|
100
|
+
if (handle !== undefined) closeSync(handle);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hashPath(path) {
|
|
105
|
+
const hash = createHash("sha256");
|
|
106
|
+
function part(value) {
|
|
107
|
+
const bytes = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
108
|
+
const length = Buffer.allocUnsafe(8);
|
|
109
|
+
length.writeBigUInt64BE(BigInt(bytes.length));
|
|
110
|
+
hash.update(length);
|
|
111
|
+
hash.update(bytes);
|
|
112
|
+
}
|
|
113
|
+
function visit(current, rel) {
|
|
114
|
+
const entry = readEntry(current);
|
|
115
|
+
if (entry.kind === "directory") {
|
|
116
|
+
part("dir");
|
|
117
|
+
part(rel);
|
|
118
|
+
for (const child of readdirSync(current).sort())
|
|
119
|
+
visit(join(current, child), join(rel, child));
|
|
120
|
+
} else if (entry.kind === "file") {
|
|
121
|
+
part("file");
|
|
122
|
+
part(rel);
|
|
123
|
+
part(entry.content);
|
|
124
|
+
} else {
|
|
125
|
+
part("symlink");
|
|
126
|
+
part(rel);
|
|
127
|
+
part(entry.target);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
visit(path, ".");
|
|
131
|
+
return hash.digest("hex");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function assertNoSymlinkAncestors(path, boundary) {
|
|
135
|
+
let current = resolve(path);
|
|
136
|
+
const root = resolve(boundary);
|
|
137
|
+
while (true) {
|
|
138
|
+
const stat = lstatSync(current);
|
|
139
|
+
if (stat.isSymbolicLink()) {
|
|
140
|
+
throw new Error(`Symlinked migration path requires manual migration: ${current}`);
|
|
141
|
+
}
|
|
142
|
+
if (current === root) return;
|
|
143
|
+
const parent = dirname(current);
|
|
144
|
+
if (parent === current) throw new Error(`Migration path escapes root: ${path}`);
|
|
145
|
+
current = parent;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function assertSameDevice(path, device) {
|
|
150
|
+
const stat = lstatSync(path);
|
|
151
|
+
if (stat.dev !== device) throw new Error(`Cross-device migration entry is unsupported: ${path}`);
|
|
152
|
+
if (stat.isSymbolicLink()) {
|
|
153
|
+
throw new Error(`Symlinked migration entries require manual migration: ${path}`);
|
|
154
|
+
}
|
|
155
|
+
if (stat.isDirectory()) {
|
|
156
|
+
for (const entry of readdirSync(path)) assertSameDevice(join(path, entry), device);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function syncDirectory(path) {
|
|
161
|
+
let handle;
|
|
162
|
+
try {
|
|
163
|
+
handle = openSync(path, "r");
|
|
164
|
+
fsyncSync(handle);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (process.platform !== "win32") throw error;
|
|
167
|
+
} finally {
|
|
168
|
+
if (handle !== undefined) closeSync(handle);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function writeDurableFile(path, content, exclusive = false) {
|
|
173
|
+
const handle = openSync(path, exclusive ? "wx" : "w");
|
|
174
|
+
try {
|
|
175
|
+
writeFileSync(handle, content, "utf8");
|
|
176
|
+
fsyncSync(handle);
|
|
177
|
+
} finally {
|
|
178
|
+
closeSync(handle);
|
|
179
|
+
}
|
|
180
|
+
if (exclusive) syncDirectory(dirname(path));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function writeJournal(path, journal, exclusive = false) {
|
|
184
|
+
const content = `${JSON.stringify(journal, null, 2)}\n`;
|
|
185
|
+
const temporary = `${path}.${process.pid}-${randomUUID()}.tmp`;
|
|
186
|
+
writeDurableFile(temporary, content, true);
|
|
187
|
+
if (exclusive) {
|
|
188
|
+
try {
|
|
189
|
+
linkSync(temporary, path);
|
|
190
|
+
syncDirectory(dirname(path));
|
|
191
|
+
} finally {
|
|
192
|
+
if (existsSync(temporary)) unlinkSync(temporary);
|
|
193
|
+
}
|
|
194
|
+
syncDirectory(dirname(path));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
renameSync(temporary, path);
|
|
198
|
+
syncDirectory(dirname(path));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function readJournal(path, root) {
|
|
202
|
+
const journal = JSON.parse(readFileSync(path, "utf8"));
|
|
203
|
+
if (
|
|
204
|
+
journal.version !== JOURNAL_VERSION ||
|
|
205
|
+
journal.root !== root ||
|
|
206
|
+
!Array.isArray(journal.items) ||
|
|
207
|
+
typeof journal.started !== "string" ||
|
|
208
|
+
typeof journal.operation_id !== "string"
|
|
209
|
+
) {
|
|
210
|
+
throw new Error(`Invalid migration journal: ${path}`);
|
|
211
|
+
}
|
|
212
|
+
return journal;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function moveNoClobber(item) {
|
|
216
|
+
mkdirSync(dirname(item.dest), { recursive: true });
|
|
217
|
+
if (item.type === "file") {
|
|
218
|
+
// Hard-link creation is atomic and fails with EEXIST instead of replacing a raced file.
|
|
219
|
+
linkSync(item.src, item.dest);
|
|
220
|
+
syncDirectory(dirname(item.dest));
|
|
221
|
+
unlinkSync(item.src);
|
|
222
|
+
syncDirectory(dirname(item.src));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (item.type === "symlink") {
|
|
226
|
+
symlinkSync(readlinkSync(item.src), item.dest);
|
|
227
|
+
syncDirectory(dirname(item.dest));
|
|
228
|
+
unlinkSync(item.src);
|
|
229
|
+
syncDirectory(dirname(item.src));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (existsSync(item.dest)) throw new Error(`Destination appeared: ${item.dest}`);
|
|
233
|
+
renameSync(item.src, item.dest);
|
|
234
|
+
syncDirectory(dirname(item.dest));
|
|
235
|
+
syncDirectory(dirname(item.src));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function moveTreeNoClobber(src, dest) {
|
|
239
|
+
const stat = lstatSync(src);
|
|
240
|
+
if (stat.isSymbolicLink()) {
|
|
241
|
+
throw new Error(`Symlinked doubled-layout entries require manual migration: ${src}`);
|
|
242
|
+
}
|
|
243
|
+
if (stat.isFile()) {
|
|
244
|
+
linkSync(src, dest);
|
|
245
|
+
syncDirectory(dirname(dest));
|
|
246
|
+
unlinkSync(src);
|
|
247
|
+
syncDirectory(dirname(src));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (!stat.isDirectory()) throw new Error(`Unsupported filesystem entry: ${src}`);
|
|
251
|
+
|
|
252
|
+
mkdirSync(dest);
|
|
253
|
+
syncDirectory(dirname(dest));
|
|
254
|
+
const completed = [];
|
|
255
|
+
try {
|
|
256
|
+
for (const entry of readdirSync(src).sort()) {
|
|
257
|
+
const child = { src: join(src, entry), dest: join(dest, entry) };
|
|
258
|
+
moveTreeNoClobber(child.src, child.dest);
|
|
259
|
+
completed.push(child);
|
|
260
|
+
}
|
|
261
|
+
rmdirSync(src);
|
|
262
|
+
syncDirectory(dirname(src));
|
|
263
|
+
} catch (error) {
|
|
264
|
+
const rollbackErrors = [];
|
|
265
|
+
for (const child of completed.reverse()) {
|
|
266
|
+
try {
|
|
267
|
+
moveTreeNoClobber(child.dest, child.src);
|
|
268
|
+
} catch (rollbackError) {
|
|
269
|
+
rollbackErrors.push(rollbackError.message);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
rmdirSync(dest);
|
|
274
|
+
syncDirectory(dirname(dest));
|
|
275
|
+
} catch (rollbackError) {
|
|
276
|
+
rollbackErrors.push(rollbackError.message);
|
|
277
|
+
}
|
|
278
|
+
const rollback = rollbackErrors.length ? ` Rollback errors: ${rollbackErrors.join("; ")}` : "";
|
|
279
|
+
throw new Error(`${error.message}.${rollback}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function restoreMove(item) {
|
|
284
|
+
if (hashPath(item.dest) !== item.digest) {
|
|
285
|
+
throw new Error(`Rollback destination changed: ${item.dest}`);
|
|
286
|
+
}
|
|
287
|
+
mkdirSync(dirname(item.src), { recursive: true });
|
|
288
|
+
if (item.type === "file") {
|
|
289
|
+
linkSync(item.dest, item.src);
|
|
290
|
+
syncDirectory(dirname(item.src));
|
|
291
|
+
unlinkSync(item.dest);
|
|
292
|
+
syncDirectory(dirname(item.dest));
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (item.type === "symlink") {
|
|
296
|
+
symlinkSync(readlinkSync(item.dest), item.src);
|
|
297
|
+
syncDirectory(dirname(item.src));
|
|
298
|
+
unlinkSync(item.dest);
|
|
299
|
+
syncDirectory(dirname(item.dest));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (existsSync(item.src)) throw new Error(`Rollback source exists: ${item.src}`);
|
|
303
|
+
renameSync(item.dest, item.src);
|
|
304
|
+
syncDirectory(dirname(item.src));
|
|
305
|
+
syncDirectory(dirname(item.dest));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function completeOrResumeMove(item) {
|
|
309
|
+
const sourceExists = existsSync(item.src);
|
|
310
|
+
const destinationExists = existsSync(item.dest);
|
|
311
|
+
|
|
312
|
+
if (sourceExists && destinationExists) {
|
|
313
|
+
if (item.type === "file") {
|
|
314
|
+
const source = statSync(item.src);
|
|
315
|
+
const destination = statSync(item.dest);
|
|
316
|
+
if (source.dev === destination.dev && source.ino === destination.ino) {
|
|
317
|
+
if (hashPath(item.dest) !== item.digest) {
|
|
318
|
+
throw new Error(`Moved destination changed: ${item.dest}`);
|
|
319
|
+
}
|
|
320
|
+
unlinkSync(item.src);
|
|
321
|
+
syncDirectory(dirname(item.src));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (item.type === "symlink" && readlinkSync(item.src) === readlinkSync(item.dest)) {
|
|
326
|
+
unlinkSync(item.src);
|
|
327
|
+
syncDirectory(dirname(item.src));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
throw new Error(`Both source and destination exist: ${item.src} → ${item.dest}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (sourceExists) {
|
|
334
|
+
if (hashPath(item.src) !== item.digest) throw new Error(`Source changed: ${item.src}`);
|
|
335
|
+
moveNoClobber(item);
|
|
336
|
+
} else if (!destinationExists) {
|
|
337
|
+
throw new Error(`Both source and destination are missing: ${item.name}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (hashPath(item.dest) !== item.digest)
|
|
341
|
+
throw new Error(`Destination verification failed: ${item.dest}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function executeMovePlan(
|
|
345
|
+
plan,
|
|
346
|
+
newRoot,
|
|
347
|
+
forwardingMarker,
|
|
348
|
+
markerContent,
|
|
349
|
+
journalPath,
|
|
350
|
+
journal,
|
|
351
|
+
ownerPath,
|
|
352
|
+
resuming,
|
|
353
|
+
) {
|
|
354
|
+
const moved = [];
|
|
355
|
+
let ownsMarker = false;
|
|
356
|
+
let ownsRoot = false;
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
if (resuming) {
|
|
360
|
+
try {
|
|
361
|
+
mkdirSync(newRoot);
|
|
362
|
+
ownsRoot = true;
|
|
363
|
+
syncDirectory(dirname(newRoot));
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (error.code !== "EEXIST") throw error;
|
|
366
|
+
}
|
|
367
|
+
let ownerContent;
|
|
368
|
+
try {
|
|
369
|
+
if (process.platform === "win32" && lstatSync(ownerPath).isSymbolicLink()) {
|
|
370
|
+
throw new Error(`Migration owner marker is symlinked: ${ownerPath}`);
|
|
371
|
+
}
|
|
372
|
+
ownerContent = readRegularFile(ownerPath, "utf8");
|
|
373
|
+
} catch (error) {
|
|
374
|
+
if (error.code !== "ENOENT") throw error;
|
|
375
|
+
}
|
|
376
|
+
if (ownerContent !== undefined) {
|
|
377
|
+
if (ownerContent !== `${journal.operation_id}\n`) {
|
|
378
|
+
throw new Error(`Migration destination is owned by another operation: ${newRoot}`);
|
|
379
|
+
}
|
|
380
|
+
ownsRoot = true;
|
|
381
|
+
} else if (!journal.finalizing) {
|
|
382
|
+
if (readdirSync(newRoot).length > 0) {
|
|
383
|
+
throw new Error(`Migration owner marker is missing: ${ownerPath}`);
|
|
384
|
+
}
|
|
385
|
+
writeDurableFile(ownerPath, `${journal.operation_id}\n`, true);
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
// Reserve the whole destination namespace atomically after preflight.
|
|
389
|
+
mkdirSync(newRoot);
|
|
390
|
+
ownsRoot = true;
|
|
391
|
+
syncDirectory(dirname(newRoot));
|
|
392
|
+
writeDurableFile(ownerPath, `${journal.operation_id}\n`, true);
|
|
393
|
+
}
|
|
394
|
+
for (const item of plan) {
|
|
395
|
+
log(`MOVE ${item.name}: ${item.src} → ${item.dest}`);
|
|
396
|
+
const sourceExisted = existsSync(item.src);
|
|
397
|
+
moved.push(item);
|
|
398
|
+
completeOrResumeMove(item);
|
|
399
|
+
if (!journal.completed.includes(item.name)) journal.completed.push(item.name);
|
|
400
|
+
writeJournal(journalPath, journal);
|
|
401
|
+
if (sourceExisted && process.env.LLM_WIKI_MIGRATION_PAUSE_AFTER === item.name) {
|
|
402
|
+
process.kill(process.pid, "SIGSTOP");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (existsSync(forwardingMarker)) {
|
|
407
|
+
if (readFileSync(forwardingMarker, "utf8") !== markerContent) {
|
|
408
|
+
throw new Error(`Destination appeared: ${forwardingMarker}`);
|
|
409
|
+
}
|
|
410
|
+
} else {
|
|
411
|
+
writeDurableFile(forwardingMarker, markerContent, true);
|
|
412
|
+
ownsMarker = true;
|
|
413
|
+
}
|
|
414
|
+
log("CREATE forwarding marker: .wiki/MIGRATED_TO_LLM_WIKI.md");
|
|
415
|
+
journal.finalizing = true;
|
|
416
|
+
writeJournal(journalPath, journal);
|
|
417
|
+
if (existsSync(ownerPath)) {
|
|
418
|
+
unlinkSync(ownerPath);
|
|
419
|
+
syncDirectory(dirname(ownerPath));
|
|
420
|
+
}
|
|
421
|
+
unlinkSync(journalPath);
|
|
422
|
+
syncDirectory(dirname(journalPath));
|
|
423
|
+
} catch (error) {
|
|
424
|
+
const rollbackErrors = [];
|
|
425
|
+
if (ownsMarker && existsSync(forwardingMarker)) {
|
|
426
|
+
try {
|
|
427
|
+
unlinkSync(forwardingMarker);
|
|
428
|
+
} catch (unlinkError) {
|
|
429
|
+
rollbackErrors.push(`marker cleanup: ${unlinkError.message}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
for (const item of moved.reverse()) {
|
|
433
|
+
try {
|
|
434
|
+
if (existsSync(item.dest) && !existsSync(item.src)) restoreMove(item);
|
|
435
|
+
} catch (rollbackError) {
|
|
436
|
+
rollbackErrors.push(`${item.name}: ${rollbackError.message}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
const rolledBack = plan.every((item) => existsSync(item.src) && !existsSync(item.dest));
|
|
440
|
+
if (rollbackErrors.length === 0 && rolledBack) {
|
|
441
|
+
try {
|
|
442
|
+
if (existsSync(ownerPath)) {
|
|
443
|
+
const owner = readFileSync(ownerPath, "utf8");
|
|
444
|
+
if (owner === `${journal.operation_id}\n`) unlinkSync(ownerPath);
|
|
445
|
+
}
|
|
446
|
+
if (existsSync(journalPath)) unlinkSync(journalPath);
|
|
447
|
+
syncDirectory(dirname(journalPath));
|
|
448
|
+
if (ownsRoot) {
|
|
449
|
+
try {
|
|
450
|
+
rmdirSync(newRoot);
|
|
451
|
+
} catch {
|
|
452
|
+
// A non-empty directory is evidence preserved for manual recovery.
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
} catch (journalError) {
|
|
456
|
+
rollbackErrors.push(`journal cleanup: ${journalError.message}`);
|
|
457
|
+
}
|
|
458
|
+
} else if (!rolledBack) {
|
|
459
|
+
rollbackErrors.push("rollback incomplete; migration journal retained");
|
|
460
|
+
}
|
|
461
|
+
const rollback = rollbackErrors.length
|
|
462
|
+
? ` Rollback errors: ${rollbackErrors.join("; ")}`
|
|
463
|
+
: " All completed moves were rolled back.";
|
|
464
|
+
throw new Error(`Migration move failed: ${error.message}.${rollback}`);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ─── Main ───────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
async function fixDoubled() {
|
|
471
|
+
// This is the directory that CONTAINS the outer .llm-wiki/.
|
|
472
|
+
const parentRoot = resolvePositionalRoot(homedir());
|
|
473
|
+
|
|
474
|
+
const outer = join(parentRoot, ".llm-wiki");
|
|
475
|
+
const inner = join(outer, ".llm-wiki");
|
|
476
|
+
const innerSentinel = join(inner, "config.json");
|
|
477
|
+
|
|
478
|
+
if (existsSync(outer) && lstatSync(outer).isSymbolicLink()) {
|
|
479
|
+
throw new Error(`Symlinked doubled-layout root requires manual migration: ${outer}`);
|
|
480
|
+
}
|
|
481
|
+
if (existsSync(inner) && lstatSync(inner).isSymbolicLink()) {
|
|
482
|
+
throw new Error(`Symlinked doubled-layout root requires manual migration: ${inner}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
console.log(`\n🔍 Scanning for doubled vault at: ${inner}\n`);
|
|
486
|
+
|
|
487
|
+
if (!existsSync(innerSentinel)) {
|
|
488
|
+
console.log("✅ No doubled vault detected (no inner .llm-wiki/config.json).");
|
|
489
|
+
console.log(` Outer: ${outer}`);
|
|
490
|
+
console.log(` Inner: ${inner}`);
|
|
491
|
+
console.log(" Nothing to do.");
|
|
492
|
+
process.exit(0);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const entries = readdirSync(inner);
|
|
496
|
+
const plan = entries.map((entry) => {
|
|
497
|
+
const src = join(inner, entry);
|
|
498
|
+
const dest = join(outer, entry);
|
|
499
|
+
return { src, dest, entry, collision: existsSync(dest) };
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
console.log("📋 Flatten plan:");
|
|
503
|
+
console.log(` ${inner}/ → ${outer}/`);
|
|
504
|
+
console.log(" ─────────────────────────────");
|
|
505
|
+
for (const p of plan) {
|
|
506
|
+
const status = p.collision ? "⚠️ COLLISION (skip)" : "✓ move";
|
|
507
|
+
console.log(` ${status} ${p.entry}`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (plan.every((p) => p.collision)) {
|
|
511
|
+
console.log("\n❌ Every inner entry collides with the outer vault. Resolve manually.");
|
|
512
|
+
process.exit(1);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (!FORCE && !DRY_RUN) {
|
|
516
|
+
console.log("\n❓ Proceed with flatten? (y/N)");
|
|
517
|
+
process.stdin.setRawMode?.(false);
|
|
518
|
+
const answer = await new Promise((resolve) => {
|
|
519
|
+
process.stdin.once("data", (data) => resolve(data.toString().trim().toLowerCase()));
|
|
520
|
+
});
|
|
521
|
+
if (answer !== "y" && answer !== "yes") {
|
|
522
|
+
console.log("Migration cancelled.");
|
|
523
|
+
process.exit(0);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
console.log("");
|
|
528
|
+
let moved = 0;
|
|
529
|
+
let skipped = 0;
|
|
530
|
+
for (const p of plan) {
|
|
531
|
+
if (p.collision || existsSync(p.dest)) {
|
|
532
|
+
log(`SKIP ${p.entry} — destination already exists`);
|
|
533
|
+
skipped++;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
log(`MOVE ${p.entry}: ${p.src} → ${p.dest}`);
|
|
537
|
+
if (!DRY_RUN && process.env.LLM_WIKI_MIGRATION_PAUSE_BEFORE_DOUBLED === p.entry) {
|
|
538
|
+
await new Promise((resolve) => process.stdin.once("data", resolve));
|
|
539
|
+
}
|
|
540
|
+
if (!DRY_RUN) moveTreeNoClobber(p.src, p.dest);
|
|
541
|
+
moved++;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (skipped === 0 && !DRY_RUN) {
|
|
545
|
+
try {
|
|
546
|
+
rmdirSync(inner);
|
|
547
|
+
log(`RMDIR ${inner}`);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
log(`RMDIR ${inner} failed: ${err.message} (left in place)`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
console.log("");
|
|
554
|
+
if (DRY_RUN) {
|
|
555
|
+
console.log(`✅ Dry-run complete. Would move ${moved}, skip ${skipped}.`);
|
|
556
|
+
} else {
|
|
557
|
+
console.log(`✅ Flatten complete. Moved ${moved}, skipped ${skipped}.`);
|
|
558
|
+
if (skipped > 0) {
|
|
559
|
+
console.log(` ${skipped} entry(ies) left in ${inner} due to collisions. Resolve manually.`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function main() {
|
|
565
|
+
if (FIX_DOUBLED) {
|
|
566
|
+
await fixDoubled();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// Determine root directory
|
|
571
|
+
const root = resolvePositionalRoot(process.cwd());
|
|
572
|
+
|
|
573
|
+
console.log(`\n🔍 Scanning for legacy wiki at: ${root}\n`);
|
|
574
|
+
|
|
575
|
+
const oldSentinel = join(root, ".wiki", "config.json");
|
|
576
|
+
const newRoot = join(root, ".llm-wiki");
|
|
577
|
+
const newSentinel = join(newRoot, "config.json");
|
|
578
|
+
const forwardingMarker = join(root, ".wiki", "MIGRATED_TO_LLM_WIKI.md");
|
|
579
|
+
const journalPath = join(root, ".wiki", JOURNAL_NAME);
|
|
580
|
+
const ownerPath = join(newRoot, ".migration-owner");
|
|
581
|
+
const resuming = existsSync(journalPath);
|
|
582
|
+
if (existsSync(join(root, ".wiki"))) {
|
|
583
|
+
assertNoSymlinkAncestors(join(root, ".wiki"), root);
|
|
584
|
+
}
|
|
585
|
+
if (existsSync(newRoot)) {
|
|
586
|
+
assertNoSymlinkAncestors(newRoot, root);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Full plan is deterministic so a journal never controls arbitrary paths.
|
|
590
|
+
const moves = [
|
|
591
|
+
{
|
|
592
|
+
src: join(root, ".wiki", "config.json"),
|
|
593
|
+
dest: join(root, ".llm-wiki", "config.json"),
|
|
594
|
+
type: "file",
|
|
595
|
+
name: "config",
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
src: join(root, ".wiki", "templates"),
|
|
599
|
+
dest: join(root, ".llm-wiki", "templates"),
|
|
600
|
+
type: "dir",
|
|
601
|
+
name: "templates",
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
src: join(root, "raw"),
|
|
605
|
+
dest: join(root, ".llm-wiki", "raw"),
|
|
606
|
+
type: "dir",
|
|
607
|
+
name: "raw sources",
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
src: join(root, "wiki"),
|
|
611
|
+
dest: join(root, ".llm-wiki", "wiki"),
|
|
612
|
+
type: "dir",
|
|
613
|
+
name: "wiki pages",
|
|
614
|
+
},
|
|
615
|
+
{
|
|
616
|
+
src: join(root, "meta"),
|
|
617
|
+
dest: join(root, ".llm-wiki", "meta"),
|
|
618
|
+
type: "dir",
|
|
619
|
+
name: "metadata",
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
src: join(root, "outputs"),
|
|
623
|
+
dest: join(root, ".llm-wiki", "outputs"),
|
|
624
|
+
type: "dir",
|
|
625
|
+
name: "outputs",
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
src: join(root, ".discoveries"),
|
|
629
|
+
dest: join(root, ".llm-wiki", ".discoveries"),
|
|
630
|
+
type: "dir",
|
|
631
|
+
name: "discovery tracking",
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
src: join(root, "WIKI_SCHEMA.md"),
|
|
635
|
+
dest: join(newRoot, "WIKI_SCHEMA.md"),
|
|
636
|
+
type: "file",
|
|
637
|
+
name: "WIKI_SCHEMA",
|
|
638
|
+
},
|
|
639
|
+
];
|
|
640
|
+
|
|
641
|
+
let journal;
|
|
642
|
+
let movePlan;
|
|
643
|
+
if (resuming) {
|
|
644
|
+
journal = readJournal(journalPath, root);
|
|
645
|
+
const journalItems = new Map(
|
|
646
|
+
journal.items.map((item) => {
|
|
647
|
+
if (!item || typeof item.name !== "string" || typeof item.digest !== "string") {
|
|
648
|
+
throw new Error(`Invalid migration journal item: ${journalPath}`);
|
|
649
|
+
}
|
|
650
|
+
return [item.name, item.digest];
|
|
651
|
+
}),
|
|
652
|
+
);
|
|
653
|
+
movePlan = moves
|
|
654
|
+
.filter((item) => journalItems.has(item.name))
|
|
655
|
+
.map((item) => ({ ...item, digest: journalItems.get(item.name) }));
|
|
656
|
+
if (movePlan.length !== journalItems.size || !Array.isArray(journal.completed)) {
|
|
657
|
+
throw new Error(`Invalid migration journal plan: ${journalPath}`);
|
|
658
|
+
}
|
|
659
|
+
console.log(`↻ Resuming interrupted migration from ${journal.started}`);
|
|
660
|
+
} else {
|
|
661
|
+
if (!existsSync(oldSentinel)) {
|
|
662
|
+
console.log("❌ No legacy wiki found (no .wiki/config.json). Nothing to migrate.");
|
|
663
|
+
if (existsSync(newSentinel)) {
|
|
664
|
+
console.log(" ✓ New-format wiki already exists at .llm-wiki/");
|
|
665
|
+
} else {
|
|
666
|
+
console.log(" No wiki vault found. Use wiki_bootstrap to create one.");
|
|
667
|
+
}
|
|
668
|
+
process.exit(0);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const rootDevice = statSync(root).dev;
|
|
672
|
+
movePlan = moves
|
|
673
|
+
.filter((item) => existsSync(item.src))
|
|
674
|
+
.map((item) => {
|
|
675
|
+
assertSameDevice(item.src, rootDevice);
|
|
676
|
+
return { ...item, digest: hashPath(item.src) };
|
|
677
|
+
});
|
|
678
|
+
const conflicts = [
|
|
679
|
+
...movePlan.filter((item) => existsSync(item.dest)),
|
|
680
|
+
...(existsSync(newRoot) && !movePlan.some((item) => existsSync(item.dest))
|
|
681
|
+
? [{ dest: newRoot, name: "destination root" }]
|
|
682
|
+
: []),
|
|
683
|
+
...(existsSync(forwardingMarker)
|
|
684
|
+
? [{ dest: forwardingMarker, name: "forwarding marker" }]
|
|
685
|
+
: []),
|
|
686
|
+
];
|
|
687
|
+
|
|
688
|
+
if (conflicts.length > 0) {
|
|
689
|
+
console.log("\n❌ Migration blocked by destination conflicts:");
|
|
690
|
+
for (const conflict of conflicts) console.log(` ${conflict.dest}`);
|
|
691
|
+
console.log(" No files were moved. Resolve these paths and rerun the migration.");
|
|
692
|
+
process.exit(1);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const started = new Date().toISOString();
|
|
696
|
+
journal = {
|
|
697
|
+
version: JOURNAL_VERSION,
|
|
698
|
+
operation_id: randomUUID(),
|
|
699
|
+
root,
|
|
700
|
+
started,
|
|
701
|
+
items: movePlan.map(({ name, digest }) => ({ name, digest })),
|
|
702
|
+
completed: [],
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Print plan
|
|
707
|
+
console.log("📋 Migration plan:");
|
|
708
|
+
console.log(" Legacy format → New format");
|
|
709
|
+
console.log(" ─────────────────────────────");
|
|
710
|
+
for (const item of moves) {
|
|
711
|
+
const included = movePlan.some((candidate) => candidate.name === item.name);
|
|
712
|
+
console.log(` ${included ? "✓" : "○"} ${item.name}: ${item.src} → ${item.dest}`);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Remaining .wiki/ dir contents (after config + templates moved)
|
|
716
|
+
const dotWikiContents = readdirSync(join(root, ".wiki")).filter(
|
|
717
|
+
(entry) => entry !== "config.json" && entry !== "templates" && entry !== JOURNAL_NAME,
|
|
718
|
+
);
|
|
719
|
+
if (dotWikiContents.length > 0) {
|
|
720
|
+
console.log(
|
|
721
|
+
`\n ⚠️ Additional .wiki/ contents (${dotWikiContents.length} items) will be left in place.`,
|
|
722
|
+
);
|
|
723
|
+
for (const c of dotWikiContents) {
|
|
724
|
+
console.log(` .wiki/${c}`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Confirmation
|
|
729
|
+
if (!FORCE && !DRY_RUN) {
|
|
730
|
+
console.log("\n❓ Proceed with migration? (y/N)");
|
|
731
|
+
// Read from stdin
|
|
732
|
+
process.stdin.setRawMode?.(false);
|
|
733
|
+
const answer = await new Promise((resolve) => {
|
|
734
|
+
process.stdin.once("data", (data) => {
|
|
735
|
+
resolve(data.toString().trim().toLowerCase());
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
if (answer !== "y" && answer !== "yes") {
|
|
739
|
+
console.log("Migration cancelled.");
|
|
740
|
+
process.exit(0);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const markerContent = [
|
|
745
|
+
"# Migration Complete",
|
|
746
|
+
"",
|
|
747
|
+
`This vault was migrated to the new layout at \`.llm-wiki/\` on ${journal.started.split("T")[0]}.`,
|
|
748
|
+
"",
|
|
749
|
+
"The old `.wiki/` directory is kept as a forwarding marker.",
|
|
750
|
+
"Remove it once you've verified everything works.",
|
|
751
|
+
"",
|
|
752
|
+
`New location: \`${newRoot}\``,
|
|
753
|
+
"",
|
|
754
|
+
].join("\n");
|
|
755
|
+
|
|
756
|
+
// Execute
|
|
757
|
+
console.log("");
|
|
758
|
+
if (!DRY_RUN) {
|
|
759
|
+
if (!resuming) writeJournal(journalPath, journal, true);
|
|
760
|
+
executeMovePlan(
|
|
761
|
+
movePlan,
|
|
762
|
+
newRoot,
|
|
763
|
+
forwardingMarker,
|
|
764
|
+
markerContent,
|
|
765
|
+
journalPath,
|
|
766
|
+
journal,
|
|
767
|
+
ownerPath,
|
|
768
|
+
resuming,
|
|
769
|
+
);
|
|
770
|
+
} else {
|
|
771
|
+
for (const item of movePlan) log(`MOVE ${item.name}: ${item.src} → ${item.dest}`);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
console.log("");
|
|
775
|
+
if (DRY_RUN) {
|
|
776
|
+
console.log("✅ Dry-run complete. No changes made.");
|
|
777
|
+
console.log(" Run without --dry-run to perform the migration.");
|
|
778
|
+
} else {
|
|
779
|
+
console.log("✅ Migration complete!");
|
|
780
|
+
console.log("");
|
|
781
|
+
console.log(" What changed:");
|
|
782
|
+
console.log(" • All wiki content moved under .llm-wiki/");
|
|
783
|
+
console.log(" • Raw sources: .llm-wiki/raw/");
|
|
784
|
+
console.log(" • Wiki pages: .llm-wiki/wiki/");
|
|
785
|
+
console.log(" • Metadata: .llm-wiki/meta/");
|
|
786
|
+
console.log(" • Config/templates: .llm-wiki/ (config.json, templates/)");
|
|
787
|
+
console.log(" • Outputs: .llm-wiki/outputs/");
|
|
788
|
+
console.log(" • Forwarding marker: .wiki/MIGRATED_TO_LLM_WIKI.md");
|
|
789
|
+
console.log("");
|
|
790
|
+
console.log(" The old .wiki/ dir is kept as a marker. You can remove it once verified.");
|
|
791
|
+
console.log("");
|
|
792
|
+
console.log(" Update your gitignore:");
|
|
793
|
+
console.log(" echo '.llm-wiki/' >> .gitignore");
|
|
794
|
+
console.log("");
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
main().catch((err) => {
|
|
799
|
+
console.error("Migration error:", err);
|
|
800
|
+
process.exit(1);
|
|
801
|
+
});
|