forge-openclaw-plugin 0.2.49 → 0.2.51

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.
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import AdmZip from "adm-zip";
3
- import { existsSync, mkdirSync, readdirSync, unlinkSync, rmSync, writeFileSync } from "node:fs";
4
- import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
3
+ import { existsSync, readdirSync, unlinkSync, rmSync } from "node:fs";
4
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { z } from "zod";
7
7
  import { resolveDataDir, getDatabase } from "../db.js";
@@ -718,7 +718,7 @@ function mapWikiSpace(row) {
718
718
  });
719
719
  }
720
720
  function getWikiRootDir() {
721
- return path.join(resolveDataDir(), "wiki");
721
+ return path.join(resolveDataDir(), "wiki-ingest");
722
722
  }
723
723
  function getSpaceStorageDir(space) {
724
724
  if (space.visibility === "shared") {
@@ -726,16 +726,9 @@ function getSpaceStorageDir(space) {
726
726
  }
727
727
  return path.join(getWikiRootDir(), "users", space.ownerUserId ?? space.slug);
728
728
  }
729
- function getSpaceIndexPath(space) {
730
- return path.join(getSpaceStorageDir(space), "index.md");
731
- }
732
729
  function getSpaceRawDir(space) {
733
730
  return path.join(getSpaceStorageDir(space), "raw");
734
731
  }
735
- function getNoteStoragePath(note, space) {
736
- const directory = note.kind === "wiki" ? "pages" : "evidence";
737
- return path.join(getSpaceStorageDir(space), directory, `${note.slug}.md`);
738
- }
739
732
  function buildNoteFrontmatter(note) {
740
733
  return {
741
734
  ...note.frontmatter,
@@ -761,20 +754,6 @@ function buildNoteFrontmatter(note) {
761
754
  author: note.author
762
755
  };
763
756
  }
764
- function stringifyFrontmatterValue(value) {
765
- if (typeof value === "string") {
766
- return JSON.stringify(value);
767
- }
768
- return JSON.stringify(value);
769
- }
770
- function renderFrontmatter(frontmatter) {
771
- const lines = ["---"];
772
- for (const [key, value] of Object.entries(frontmatter)) {
773
- lines.push(`${key}: ${stringifyFrontmatterValue(value)}`);
774
- }
775
- lines.push("---", "");
776
- return lines.join("\n");
777
- }
778
757
  function hashContent(value) {
779
758
  return createHash("sha256").update(value).digest("hex");
780
759
  }
@@ -1152,7 +1131,7 @@ async function compileImageWithLlm(profile, secrets, input) {
1152
1131
  title: parsed.title?.trim() || input.titleHint || "Imported image",
1153
1132
  summary: parsed.summary?.trim() || "",
1154
1133
  markdown: parsed.markdown?.trim() ||
1155
- `# ${parsed.title?.trim() || input.titleHint || "Imported image"}\n\nImage imported into the wiki vault.\n`,
1134
+ `# ${parsed.title?.trim() || input.titleHint || "Imported image"}\n\nImage imported into Forge wiki memory.\n`,
1156
1135
  tags: normalizeTags(parsed.tags),
1157
1136
  entityProposals: Array.isArray(parsed.entityProposals)
1158
1137
  ? parsed.entityProposals.filter((entry) => entry !== null && typeof entry === "object")
@@ -1239,7 +1218,7 @@ function ensureSharedWikiSpace() {
1239
1218
  getDatabase()
1240
1219
  .prepare(`INSERT INTO wiki_spaces (id, slug, label, description, owner_user_id, visibility, created_at, updated_at)
1241
1220
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
1242
- .run("wiki_space_shared", "shared", "Shared Forge Memory", "Shared wiki space for file-backed Forge knowledge.", null, "shared", now, now);
1221
+ .run("wiki_space_shared", "shared", "Shared Forge Memory", "Shared wiki space for SQLite-backed Forge knowledge.", null, "shared", now, now);
1243
1222
  const space = getWikiSpaceById("wiki_space_shared");
1244
1223
  ensureWikiSpaceSeedPages(space.id);
1245
1224
  return space;
@@ -1346,7 +1325,6 @@ function ensureWikiSpaceSeedPages(spaceId) {
1346
1325
  for (const note of insertedNotes) {
1347
1326
  syncNoteWikiArtifacts(note);
1348
1327
  }
1349
- syncWikiSpaceIndex(spaceId);
1350
1328
  }
1351
1329
  }
1352
1330
  function resolveSpaceId(spaceId, userId) {
@@ -1385,44 +1363,33 @@ export function prepareNoteWikiFields(input) {
1385
1363
  };
1386
1364
  }
1387
1365
  export function syncNoteWikiArtifacts(note) {
1388
- const space = getWikiSpaceById(note.spaceId) ?? ensureSharedWikiSpace();
1389
- const filePath = getNoteStoragePath(note, space);
1390
1366
  const frontmatter = buildNoteFrontmatter(note);
1391
- const payload = `${renderFrontmatter(frontmatter)}${note.contentMarkdown.trim()}\n`;
1392
- const revisionHash = hashContent(payload);
1393
- mkdirSync(path.dirname(filePath), { recursive: true });
1394
- if (note.sourcePath && note.sourcePath !== filePath) {
1395
- if (existsSync(note.sourcePath)) {
1396
- rmSync(note.sourcePath, { force: true });
1397
- }
1398
- }
1399
- writeFileSync(filePath, payload, "utf8");
1367
+ const revisionHash = hashContent(JSON.stringify({
1368
+ frontmatter,
1369
+ contentMarkdown: note.contentMarkdown
1370
+ }));
1400
1371
  const now = nowIso();
1401
1372
  getDatabase()
1402
1373
  .prepare(`UPDATE notes
1403
1374
  SET source_path = ?, frontmatter_json = ?, revision_hash = ?, last_synced_at = ?
1404
1375
  WHERE id = ?`)
1405
- .run(filePath, JSON.stringify(frontmatter), revisionHash, now, note.id);
1376
+ .run("", JSON.stringify(frontmatter), revisionHash, now, note.id);
1406
1377
  upsertWikiSearchRow({
1407
1378
  ...note,
1408
- sourcePath: filePath,
1379
+ sourcePath: "",
1409
1380
  frontmatter,
1410
1381
  revisionHash,
1411
1382
  lastSyncedAt: now
1412
1383
  });
1413
1384
  rebuildWikiLinkEdges({
1414
1385
  ...note,
1415
- sourcePath: filePath,
1386
+ sourcePath: "",
1416
1387
  frontmatter,
1417
1388
  revisionHash,
1418
1389
  lastSyncedAt: now
1419
1390
  });
1420
- syncWikiSpaceIndex(space.id);
1421
1391
  }
1422
1392
  export function deleteNoteWikiArtifacts(note) {
1423
- if (note.sourcePath && existsSync(note.sourcePath)) {
1424
- rmSync(note.sourcePath, { force: true });
1425
- }
1426
1393
  deleteWikiSearchRow(note.id);
1427
1394
  getDatabase()
1428
1395
  .prepare(`DELETE FROM wiki_link_edges WHERE source_note_id = ?`)
@@ -1433,66 +1400,6 @@ export function deleteNoteWikiArtifacts(note) {
1433
1400
  getDatabase()
1434
1401
  .prepare(`DELETE FROM wiki_media_assets WHERE note_id = ? OR transcript_note_id = ?`)
1435
1402
  .run(note.id, note.id);
1436
- syncWikiSpaceIndex(note.spaceId);
1437
- }
1438
- function buildWikiIndexMarkdown(space, pages) {
1439
- const wikiPages = [...pages]
1440
- .filter((page) => page.kind === "wiki")
1441
- .sort((left, right) => left.parentSlug === right.parentSlug
1442
- ? left.indexOrder - right.indexOrder ||
1443
- left.title.localeCompare(right.title)
1444
- : (left.parentSlug ?? "").localeCompare(right.parentSlug ?? ""));
1445
- const evidencePages = [...pages]
1446
- .filter((page) => page.kind === "evidence")
1447
- .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
1448
- const lines = [
1449
- `# ${space.label}`,
1450
- "",
1451
- "Explicit Forge wiki index generated from the local vault.",
1452
- "",
1453
- "## How To Use",
1454
- "",
1455
- "- Start here when an agent needs a crawlable catalog of the space.",
1456
- "- `pages/` contains durable wiki articles.",
1457
- "- `evidence/` contains shorter notes and work traces.",
1458
- "- `raw/` contains imported source material for future recompilation.",
1459
- "",
1460
- `Generated at ${nowIso()}.`,
1461
- "",
1462
- "## Wiki Index",
1463
- ""
1464
- ];
1465
- if (wikiPages.length === 0) {
1466
- lines.push("_No wiki pages yet._", "");
1467
- }
1468
- else {
1469
- for (const page of wikiPages) {
1470
- const depth = page.parentSlug ? 1 : 0;
1471
- const prefix = `${" ".repeat(depth)}- `;
1472
- lines.push(`${prefix}[[${page.slug}]]${page.summary ? ` - ${page.summary}` : ""}`);
1473
- }
1474
- lines.push("");
1475
- }
1476
- lines.push("## Evidence Pages", "");
1477
- if (evidencePages.length === 0) {
1478
- lines.push("_No evidence pages yet._", "");
1479
- }
1480
- else {
1481
- for (const page of evidencePages.slice(0, 200)) {
1482
- lines.push(`- [[${page.slug}]]${page.summary ? ` - ${page.summary}` : ""}`);
1483
- }
1484
- lines.push("");
1485
- }
1486
- return `${lines.join("\n")}\n`;
1487
- }
1488
- function syncWikiSpaceIndex(spaceId) {
1489
- const space = getWikiSpaceById(spaceId) ?? ensureSharedWikiSpace();
1490
- const rootDir = getSpaceStorageDir(space);
1491
- mkdirSync(path.join(rootDir, "pages"), { recursive: true });
1492
- mkdirSync(path.join(rootDir, "evidence"), { recursive: true });
1493
- mkdirSync(path.join(rootDir, "assets"), { recursive: true });
1494
- mkdirSync(path.join(rootDir, "raw"), { recursive: true });
1495
- writeFileSync(getSpaceIndexPath(space), buildWikiIndexMarkdown(space, listWikiPages({ spaceId, limit: 10_000 })), "utf8");
1496
1403
  }
1497
1404
  function upsertWikiSearchRow(note) {
1498
1405
  deleteWikiSearchRow(note.id);
@@ -1574,7 +1481,7 @@ export function listWikiSpaces() {
1574
1481
  const rows = getDatabase()
1575
1482
  .prepare(`SELECT id, slug, label, description, owner_user_id, visibility, created_at, updated_at
1576
1483
  FROM wiki_spaces
1577
- ORDER BY visibility ASC, updated_at DESC`)
1484
+ ORDER BY CASE WHEN visibility = 'shared' THEN 0 ELSE 1 END, updated_at DESC`)
1578
1485
  .all();
1579
1486
  const spaces = rows.map(mapWikiSpace);
1580
1487
  for (const space of spaces) {
@@ -1710,92 +1617,13 @@ export async function syncWikiVaultFromDisk(input) {
1710
1617
  : listWikiSpaces();
1711
1618
  let updated = 0;
1712
1619
  for (const space of spaces) {
1713
- for (const directoryName of ["pages", "evidence"]) {
1714
- const directory = path.join(getSpaceStorageDir(space), directoryName);
1715
- try {
1716
- const entries = await readdir(directory);
1717
- for (const entry of entries) {
1718
- if (!entry.endsWith(".md")) {
1719
- continue;
1720
- }
1721
- const filePath = path.join(directory, entry);
1722
- const content = await readFile(filePath, "utf8");
1723
- const parsedFile = parseFrontmatter(content);
1724
- const noteId = typeof parsedFile.frontmatter.id === "string"
1725
- ? parsedFile.frontmatter.id
1726
- : null;
1727
- if (!noteId) {
1728
- continue;
1729
- }
1730
- const existing = getNoteByIdRaw(noteId);
1731
- if (!existing) {
1732
- continue;
1733
- }
1734
- const markdown = parsedFile.body.trim();
1735
- const contentPlain = buildContentPlain(markdown);
1736
- const title = typeof parsedFile.frontmatter.title === "string"
1737
- ? parsedFile.frontmatter.title
1738
- : inferTitle(markdown, existing.title);
1739
- const aliases = normalizeAliases(Array.isArray(parsedFile.frontmatter.aliases)
1740
- ? parsedFile.frontmatter.aliases.filter((entry) => typeof entry === "string")
1741
- : []);
1742
- const summary = typeof parsedFile.frontmatter.summary === "string"
1743
- ? parsedFile.frontmatter.summary
1744
- : inferSummary(markdown);
1745
- const payload = `${renderFrontmatter(parsedFile.frontmatter)}${markdown}\n`;
1746
- const revisionHash = hashContent(payload);
1747
- const now = nowIso();
1748
- getDatabase()
1749
- .prepare(`UPDATE notes
1750
- SET title = ?, slug = ?, kind = ?, space_id = ?, parent_slug = ?, index_order = ?, show_in_index = ?, aliases_json = ?, summary = ?, content_markdown = ?, content_plain = ?,
1751
- source_path = ?, frontmatter_json = ?, revision_hash = ?, last_synced_at = ?, updated_at = ?
1752
- WHERE id = ?`)
1753
- .run(title, typeof parsedFile.frontmatter.slug === "string"
1754
- ? parsedFile.frontmatter.slug
1755
- : existing.slug, directoryName === "pages" ? "wiki" : "evidence", space.id, typeof parsedFile.frontmatter.parentSlug === "string"
1756
- ? parsedFile.frontmatter.parentSlug
1757
- : existing.parent_slug, typeof parsedFile.frontmatter.indexOrder === "number"
1758
- ? Math.trunc(parsedFile.frontmatter.indexOrder)
1759
- : existing.index_order, parsedFile.frontmatter.showInIndex === false ? 0 : 1, JSON.stringify(aliases), summary, markdown, contentPlain, filePath, JSON.stringify(parsedFile.frontmatter), revisionHash, now, now, noteId);
1760
- const note = mapNoteRow(getNoteByIdRaw(noteId), listLinkRowsForNotes([noteId]));
1761
- upsertWikiSearchRow(note);
1762
- rebuildWikiLinkEdges(note);
1763
- updated += 1;
1764
- }
1765
- }
1766
- catch {
1767
- continue;
1768
- }
1620
+ for (const note of listWikiPages({ spaceId: space.id, limit: 10_000 })) {
1621
+ syncNoteWikiArtifacts(note);
1622
+ updated += 1;
1769
1623
  }
1770
- syncWikiSpaceIndex(space.id);
1771
1624
  }
1772
1625
  return { updated };
1773
1626
  }
1774
- function parseFrontmatter(markdown) {
1775
- const match = markdown.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
1776
- if (!match) {
1777
- return { frontmatter: {}, body: markdown };
1778
- }
1779
- const frontmatter = {};
1780
- for (const line of match[1].split("\n")) {
1781
- const separatorIndex = line.indexOf(":");
1782
- if (separatorIndex <= 0) {
1783
- continue;
1784
- }
1785
- const key = line.slice(0, separatorIndex).trim();
1786
- const rawValue = line.slice(separatorIndex + 1).trim();
1787
- if (!key) {
1788
- continue;
1789
- }
1790
- try {
1791
- frontmatter[key] = JSON.parse(rawValue);
1792
- }
1793
- catch {
1794
- frontmatter[key] = rawValue.replace(/^"(.*)"$/, "$1");
1795
- }
1796
- }
1797
- return { frontmatter, body: match[2] };
1798
- }
1799
1627
  function findMatchingWikiNoteIds(query) {
1800
1628
  const ftsQuery = buildWikiFtsQuery(query);
1801
1629
  if (!ftsQuery) {
@@ -1953,8 +1781,6 @@ export function getWikiHealth(input = {}) {
1953
1781
  const pages = listWikiPages({ spaceId, limit: 10_000 });
1954
1782
  const noteIds = pages.map((page) => page.id);
1955
1783
  const noteIdSet = new Set(noteIds);
1956
- const rootDir = getSpaceStorageDir(space);
1957
- const indexPath = getSpaceIndexPath(space);
1958
1784
  const rawDirectoryPath = getSpaceRawDir(space);
1959
1785
  const edgeRows = getDatabase()
1960
1786
  .prepare(`SELECT e.source_note_id, e.target_type, e.target_note_id, e.raw_target, e.updated_at, n.slug AS source_slug, n.title AS source_title
@@ -1994,7 +1820,7 @@ export function getWikiHealth(input = {}) {
1994
1820
  .get(spaceId).count;
1995
1821
  return wikiHealthPayloadSchema.parse({
1996
1822
  space,
1997
- indexPath,
1823
+ indexPath: "",
1998
1824
  rawDirectoryPath,
1999
1825
  pageCount: pages.length,
2000
1826
  wikiPageCount: pages.filter((page) => page.kind === "wiki").length,
@@ -430,10 +430,6 @@ export async function createDataBackup(input = { note: "" }, options = {}) {
430
430
  current: snapshot
431
431
  }, null, 2), "utf8"));
432
432
  const currentRoot = getEffectiveDataRoot();
433
- const wikiPath = path.join(currentRoot, "wiki");
434
- if (existsSync(wikiPath)) {
435
- zip.addLocalFolder(wikiPath, "wiki");
436
- }
437
433
  const wikiIngestPath = path.join(currentRoot, "wiki-ingest");
438
434
  if (existsSync(wikiIngestPath)) {
439
435
  zip.addLocalFolder(wikiIngestPath, "wiki-ingest");
@@ -455,7 +451,7 @@ export async function createDataBackup(input = { note: "" }, options = {}) {
455
451
  manifestPath,
456
452
  databasePath: snapshot.databasePath,
457
453
  sizeBytes: archiveStat.size,
458
- includesWiki: existsSync(wikiPath),
454
+ includesWiki: false,
459
455
  includesSecretsKey: existsSync(secretsKeyPath),
460
456
  counts: snapshot.counts
461
457
  });
@@ -596,7 +592,6 @@ function runtimeAssetPaths(dataRoot) {
596
592
  return {
597
593
  dataRoot: resolvedRoot,
598
594
  databasePath: resolveDatabasePathForDataRoot(resolvedRoot),
599
- wikiPath: path.join(resolvedRoot, "wiki"),
600
595
  wikiIngestPath: path.join(resolvedRoot, "wiki-ingest"),
601
596
  secretsKeyPath: path.join(resolvedRoot, ".forge-secrets.key")
602
597
  };
@@ -605,11 +600,10 @@ async function copyRuntimeAssets(sourceRoot, targetRoot) {
605
600
  const source = runtimeAssetPaths(sourceRoot);
606
601
  const target = runtimeAssetPaths(targetRoot);
607
602
  await mkdir(target.dataRoot, { recursive: true });
608
- if (existsSync(target.databasePath) || existsSync(target.wikiPath) || existsSync(target.secretsKeyPath)) {
603
+ if (existsSync(target.databasePath) || existsSync(target.secretsKeyPath)) {
609
604
  throw new HttpError(409, "target_data_root_not_empty", `Forge found existing runtime data under ${target.dataRoot}. Pick another folder or adopt the existing runtime instead.`);
610
605
  }
611
606
  await copyIfExists(source.databasePath, target.databasePath);
612
- await copyIfExists(source.wikiPath, target.wikiPath);
613
607
  await copyIfExists(source.wikiIngestPath, target.wikiIngestPath);
614
608
  await copyIfExists(source.secretsKeyPath, target.secretsKeyPath);
615
609
  }
@@ -685,7 +679,6 @@ export async function restoreDataBackup(backupId, input, options = {}) {
685
679
  await removeIfExists(path.join(currentRoot, ".forge-secrets.key"));
686
680
  }
687
681
  await copyIfExists(restoredDatabasePath, path.join(currentRoot, "forge.sqlite"));
688
- await copyIfExists(path.join(tempDir, "wiki"), path.join(currentRoot, "wiki"));
689
682
  await copyIfExists(path.join(tempDir, "wiki-ingest"), path.join(currentRoot, "wiki-ingest"));
690
683
  await copyIfExists(restoredSecretsPath, path.join(currentRoot, ".forge-secrets.key"));
691
684
  await applyRuntimeRootSwitch(currentRoot, options.secretsManager);