forge-openclaw-plugin 0.2.49 → 0.2.50
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/README.md +3 -3
- package/dist/assets/{index-BAmEvOXb.js → index-C9_gJvi6.js} +29 -29
- package/dist/assets/index-C9_gJvi6.js.map +1 -0
- package/dist/index.html +1 -1
- package/dist/openclaw/parity.js +14 -0
- package/dist/openclaw/routes.js +42 -0
- package/dist/openclaw/tools.js +3 -3
- package/dist/server/server/migrations/019_wiki_memory.sql +1 -1
- package/dist/server/server/migrations/054_sqlite_backed_wiki_memory.sql +8 -0
- package/dist/server/server/src/app.js +24 -13
- package/dist/server/server/src/db.js +0 -2
- package/dist/server/server/src/openapi.js +39 -3
- package/dist/server/server/src/repositories/notes.js +5 -2
- package/dist/server/server/src/repositories/wiki-memory.js +16 -190
- package/dist/server/server/src/services/data-management.js +2 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/019_wiki_memory.sql +1 -1
- package/server/migrations/054_sqlite_backed_wiki_memory.sql +8 -0
- package/skills/forge-openclaw/SKILL.md +6 -6
- package/skills/forge-openclaw/entity_conversation_playbooks.md +30 -0
- package/skills/forge-openclaw/psyche_entity_playbooks.md +21 -0
- package/dist/assets/index-BAmEvOXb.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import AdmZip from "adm-zip";
|
|
3
|
-
import { existsSync,
|
|
4
|
-
import { mkdir, readFile,
|
|
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
|
|
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
|
|
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
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
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(
|
|
1376
|
+
.run("", JSON.stringify(frontmatter), revisionHash, now, note.id);
|
|
1406
1377
|
upsertWikiSearchRow({
|
|
1407
1378
|
...note,
|
|
1408
|
-
sourcePath:
|
|
1379
|
+
sourcePath: "",
|
|
1409
1380
|
frontmatter,
|
|
1410
1381
|
revisionHash,
|
|
1411
1382
|
lastSyncedAt: now
|
|
1412
1383
|
});
|
|
1413
1384
|
rebuildWikiLinkEdges({
|
|
1414
1385
|
...note,
|
|
1415
|
-
sourcePath:
|
|
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);
|
|
@@ -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
|
|
1714
|
-
|
|
1715
|
-
|
|
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:
|
|
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.
|
|
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);
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
UPDATE wiki_spaces
|
|
2
|
+
SET description = 'Shared wiki space for SQLite-backed Forge knowledge.'
|
|
3
|
+
WHERE id = 'wiki_space_shared'
|
|
4
|
+
AND description != 'Shared wiki space for SQLite-backed Forge knowledge.';
|
|
5
|
+
|
|
6
|
+
UPDATE notes
|
|
7
|
+
SET source_path = ''
|
|
8
|
+
WHERE source_path != '';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: forge-openclaw
|
|
2
|
+
name: forge-openclaw-plugin
|
|
3
3
|
description: use when the user wants to save, search, update, review, start, stop, reward, explain, compare, or run Forge records, or when the conversation is clearly about a Forge entity such as a goal, project, strategy, task, habit, note, calendar_event, work_block_template, task_timebox, task_run, insight, preference item, preference context, preference catalog, questionnaire instrument, questionnaire run, self observation, psyche_value, behavior_pattern, behavior, belief_entry, mode_profile, mode_guide_session, trigger_report, event_type, emotion_definition, sleep_session, or workout_session. identify the exact Forge object, keep the main conversation natural, guide psyche intake with active listening before storing it, and for psyche issues that need understanding first usually begin with one exploratory question before any formulation or save suggestion.
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -59,7 +59,7 @@ PM surface rule:
|
|
|
59
59
|
human/bot ownership filters.
|
|
60
60
|
- Guided modal flows handle create, edit, move, link, and closeout actions.
|
|
61
61
|
|
|
62
|
-
Forge has four major surfaces. The planning side covers goals, projects, strategies, tasks, habits, notes, calendar events, recurring work blocks, task timeboxes, live work sessions, and agent-authored insights. The Health side covers sleep sessions, sports and workout sessions, companion pairing, and habit-generated workout records that should still stay linked to the broader Forge graph. The Preferences side covers contextual taste modeling, pairwise comparisons, direct signals, editable concept libraries, and preference items that can come from Forge entities or seeded concept domains such as food, activities, places, countries, fashion, people, media, and tools. The Psyche side covers values, patterns, behaviors, beliefs, modes, guided mode sessions, trigger reports, event types, and reusable emotion definitions. Forge also has a
|
|
62
|
+
Forge has four major surfaces. The planning side covers goals, projects, strategies, tasks, habits, notes, calendar events, recurring work blocks, task timeboxes, live work sessions, and agent-authored insights. The Health side covers sleep sessions, sports and workout sessions, companion pairing, and habit-generated workout records that should still stay linked to the broader Forge graph. The Preferences side covers contextual taste modeling, pairwise comparisons, direct signals, editable concept libraries, and preference items that can come from Forge entities or seeded concept domains such as food, activities, places, countries, fashion, people, media, and tools. The Psyche side covers values, patterns, behaviors, beliefs, modes, guided mode sessions, trigger reports, event types, and reusable emotion definitions. Forge also has a SQLite-backed Wiki memory layer with explicit spaces, Markdown content in database rows, backlinks, optional embeddings, and structured links back to Forge entities. Forge is also multi-user: every entity can belong to a typed `human` or `bot` user through `userId`, and read routes can scope to one or many users with `userId` or repeated `userIds`. The current access posture is configurable through a directional user graph, but the live default is still permissive: Forge can list users directly, every relationship edge starts open, and a user can read or affect another user's linked records when the route explicitly asks for them. Use `forge_get_user_directory` when owner identity or cross-user access matters. Strategies can also be locked into a contract with `isLocked`; once locked, do not mutate the graph or target structure unless the user explicitly wants the strategy unlocked first. The model should use the real entity names, not vague substitutes. Say `project`, not “initiative”. Say `behavior_pattern`, not “theme”. Say `trigger_report`, not “incident note”.
|
|
63
63
|
Habits are a first-class recurring entity in the planning side.
|
|
64
64
|
NEGATIVE HABIT CHECK-IN RULE: for a `negative` habit, the correct aligned/resisted outcome is `missed`. `missed` means the bad habit was resisted, the user stayed aligned, and the habit should award its XP bonus.
|
|
65
65
|
|
|
@@ -74,9 +74,9 @@ Preferences rule:
|
|
|
74
74
|
Wiki rule:
|
|
75
75
|
|
|
76
76
|
- Treat the Wiki as the canonical long-form memory surface, not as a loose note dump.
|
|
77
|
-
- Use the wiki tools when the user wants
|
|
77
|
+
- Use the wiki tools when the user wants SQLite-backed reference pages, backlink-aware recall, ingest from a URL or local file, or wiki maintenance work such as unresolved-link cleanup.
|
|
78
78
|
- `forge_ingest_wiki_source` now queues the ingest as background work; use the Forge UI handoff when the user wants to review or keep only selected wiki/entity candidates after the ingest finishes.
|
|
79
|
-
- Keep evidence notes and wiki pages conceptually distinct: evidence notes are linked operating records, while wiki pages are
|
|
79
|
+
- Keep evidence notes and wiki pages conceptually distinct: evidence notes are linked operating records, while wiki pages are curated long-form memory.
|
|
80
80
|
|
|
81
81
|
Wiki navigation and search rule:
|
|
82
82
|
|
|
@@ -90,7 +90,7 @@ Wiki navigation and search rule:
|
|
|
90
90
|
- Use `forge_search_wiki` as the default wiki recall tool. It is the main route for people, conversations, concepts, and exact page lookup.
|
|
91
91
|
- Use `forge_list_wiki_pages` when the user wants to browse structure, inspect a branch, or understand how pages are organized rather than search by phrase.
|
|
92
92
|
- Use `forge_get_wiki_page` after search yields a likely hit, or when the user already identified the page to open.
|
|
93
|
-
- Use `forge_get_wiki_settings` or `forge_get_wiki_health` when the task is about wiki maintenance, indexing, unresolved links, ingest setup, or
|
|
93
|
+
- Use `forge_get_wiki_settings` or `forge_get_wiki_health` when the task is about wiki maintenance, indexing, unresolved links, ingest setup, or memory integrity rather than content recall.
|
|
94
94
|
|
|
95
95
|
Health rule:
|
|
96
96
|
|
|
@@ -353,7 +353,7 @@ Use the batch entity tools for stored records:
|
|
|
353
353
|
These tools operate on:
|
|
354
354
|
`goal`, `project`, `strategy`, `task`, `habit`, `tag`, `note`, `insight`, `calendar_event`, `work_block_template`, `task_timebox`, `psyche_value`, `behavior_pattern`, `behavior`, `belief_entry`, `mode_profile`, `mode_guide_session`, `trigger_report`, `event_type`, `emotion_definition`, `preference_catalog`, `preference_catalog_item`, `preference_context`, `preference_item`, `questionnaire_instrument`, `sleep_session`, `workout_session`
|
|
355
355
|
|
|
356
|
-
Use the wiki tools for
|
|
356
|
+
Use the wiki tools for SQLite-backed memory work:
|
|
357
357
|
`forge_get_wiki_settings`, `forge_list_wiki_pages`, `forge_get_wiki_page`, `forge_search_wiki`, `forge_upsert_wiki_page`, `forge_get_wiki_health`, `forge_sync_wiki_vault`, `forge_reindex_wiki_embeddings`, `forge_ingest_wiki_source`
|
|
358
358
|
|
|
359
359
|
Use the health tools for review and reflective enrichment, not as the default CRUD architecture:
|
|
@@ -63,6 +63,11 @@ Forge correctly, and gather only the structure that still matters.
|
|
|
63
63
|
and then act.
|
|
64
64
|
- Once the route family is clear, say it plainly enough that another agent could follow
|
|
65
65
|
the same path without guessing.
|
|
66
|
+
- For updates, start with the smallest thing that now feels wrong, newly true, or
|
|
67
|
+
newly visible. Do not make the user retell the whole record unless the change is
|
|
68
|
+
genuinely structural.
|
|
69
|
+
- For review requests, ask what practical question they want the read to answer before
|
|
70
|
+
you ask for more scope.
|
|
66
71
|
- For meaning-bearing updates, especially in Psyche-adjacent work, briefly say what
|
|
67
72
|
feels newly true before you ask for the one structural detail that still changes the
|
|
68
73
|
save.
|
|
@@ -222,6 +227,31 @@ When you are about to save:
|
|
|
222
227
|
enough or needs one correction
|
|
223
228
|
- if the user confirms it, stop asking and save
|
|
224
229
|
|
|
230
|
+
## Update And Review Shortcuts
|
|
231
|
+
|
|
232
|
+
Use these when the user is correcting, reviewing, or tightening something that already
|
|
233
|
+
exists.
|
|
234
|
+
|
|
235
|
+
- When the user already gave the correction in usable language, reflect what still
|
|
236
|
+
seems true, then ask only for the one thing that no longer fits.
|
|
237
|
+
- A good narrow update line is:
|
|
238
|
+
"I can stay narrow here. What is the one thing that no longer fits?"
|
|
239
|
+
- When the user is revising placement, timing, or ownership rather than meaning, do
|
|
240
|
+
not reopen the whole story. Confirm only the parent, interval, owner, or route scope
|
|
241
|
+
that changes the write.
|
|
242
|
+
- When the record is abstract or reusable and the user wants an update, ask what
|
|
243
|
+
future decision, comparison, or retrieval moment got muddy with the old wording.
|
|
244
|
+
- When the user wants review rather than mutation, ask what answer they need from the
|
|
245
|
+
read:
|
|
246
|
+
what this would help them decide later is often the clearest scope signal.
|
|
247
|
+
- For specialized surfaces, ask what exact saved object, span, weekday, flow, run, or
|
|
248
|
+
node the user wants to check before you ask why it matters.
|
|
249
|
+
- If the next answer would not change the route, wording, timing, links, or useful
|
|
250
|
+
interpretation, stop asking and act.
|
|
251
|
+
- Close cleanly:
|
|
252
|
+
once the user says the wording or route lands, summarize once and move to the read
|
|
253
|
+
or write.
|
|
254
|
+
|
|
225
255
|
When an adjacent record becomes visible:
|
|
226
256
|
|
|
227
257
|
- name it gently and ask whether it should be linked now, saved separately later, or
|
|
@@ -72,6 +72,10 @@ Forge without turning the conversation into a worksheet.
|
|
|
72
72
|
container first and hold the others lightly until the user wants to map them.
|
|
73
73
|
- When the user has said enough for an accurate working formulation, stop deepening and
|
|
74
74
|
help them name it cleanly.
|
|
75
|
+
- For Psyche updates, begin with the smallest part of the old wording that no longer
|
|
76
|
+
fits instead of reopening the whole formulation immediately.
|
|
77
|
+
- Do not reopen the full origin story when the update is really about one changed
|
|
78
|
+
meaning, one newly visible protection, or one new sentence the user can already say.
|
|
75
79
|
|
|
76
80
|
## First reflection menu
|
|
77
81
|
|
|
@@ -193,6 +197,23 @@ opening:
|
|
|
193
197
|
- one missing-detail question
|
|
194
198
|
- then move toward the write instead of forcing exploration
|
|
195
199
|
|
|
200
|
+
## Update micro-openers
|
|
201
|
+
|
|
202
|
+
Use these when the user is revising an existing Psyche record and the tone should stay
|
|
203
|
+
therapist-like without becoming expansive again.
|
|
204
|
+
|
|
205
|
+
- "Something about the old wording no longer holds the whole experience. What felt
|
|
206
|
+
different in the moment that made that visible?"
|
|
207
|
+
- "The old name was trying to protect something real. What part of the recent episode
|
|
208
|
+
made it feel too small or off?"
|
|
209
|
+
- "It sounds like the same pain, but not quite the same meaning. What changed in what
|
|
210
|
+
it started to say?"
|
|
211
|
+
- If the user already gave the new sentence in usable language, reflect it once, ask
|
|
212
|
+
what part of the old wording it replaces, and then save.
|
|
213
|
+
- If the user wants review rather than storage, ask whether they need clearer
|
|
214
|
+
language, better understanding, or next-step help before you reopen the whole
|
|
215
|
+
formulation.
|
|
216
|
+
|
|
196
217
|
## Therapeutic turn shapes
|
|
197
218
|
|
|
198
219
|
Keep the pacing human and intentional.
|