forge-openclaw-plugin 0.2.48 → 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.
Files changed (36) hide show
  1. package/README.md +3 -3
  2. package/dist/assets/index-2_tuemtU.css +1 -0
  3. package/dist/assets/index-C9_gJvi6.js +91 -0
  4. package/dist/assets/index-C9_gJvi6.js.map +1 -0
  5. package/dist/index.html +2 -2
  6. package/dist/openclaw/parity.js +14 -0
  7. package/dist/openclaw/routes.js +42 -0
  8. package/dist/openclaw/session-registry.js +17 -0
  9. package/dist/openclaw/tools.js +3 -3
  10. package/dist/server/server/migrations/019_wiki_memory.sql +1 -1
  11. package/dist/server/server/migrations/052_agent_identity_tightening.sql +307 -0
  12. package/dist/server/server/migrations/053_agent_runtime_session_canonical_labels.sql +9 -0
  13. package/dist/server/server/migrations/054_sqlite_backed_wiki_memory.sql +8 -0
  14. package/dist/server/server/src/app.js +46 -14
  15. package/dist/server/server/src/db.js +0 -2
  16. package/dist/server/server/src/openapi.js +58 -3
  17. package/dist/server/server/src/repositories/agent-runtime-sessions.js +122 -16
  18. package/dist/server/server/src/repositories/model-settings.js +5 -0
  19. package/dist/server/server/src/repositories/notes.js +5 -2
  20. package/dist/server/server/src/repositories/settings.js +101 -13
  21. package/dist/server/server/src/repositories/users.js +23 -0
  22. package/dist/server/server/src/repositories/wiki-memory.js +16 -190
  23. package/dist/server/server/src/services/data-management.js +2 -9
  24. package/dist/server/server/src/types.js +13 -0
  25. package/openclaw.plugin.json +1 -1
  26. package/package.json +5 -2
  27. package/server/migrations/019_wiki_memory.sql +1 -1
  28. package/server/migrations/052_agent_identity_tightening.sql +307 -0
  29. package/server/migrations/053_agent_runtime_session_canonical_labels.sql +9 -0
  30. package/server/migrations/054_sqlite_backed_wiki_memory.sql +8 -0
  31. package/skills/forge-openclaw/SKILL.md +6 -6
  32. package/skills/forge-openclaw/entity_conversation_playbooks.md +49 -0
  33. package/skills/forge-openclaw/psyche_entity_playbooks.md +32 -0
  34. package/dist/assets/index-Bv9FWWsZ.js +0 -91
  35. package/dist/assets/index-Bv9FWWsZ.js.map +0 -1
  36. package/dist/assets/index-DtEvFzXp.css +0 -1
@@ -7,6 +7,7 @@ import { recordActivityEvent } from "./activity-events.js";
7
7
  import { recordEventLog } from "./event-log.js";
8
8
  import { resolveGoogleCalendarOauthPublicConfig } from "../services/google-calendar-oauth-config.js";
9
9
  import { buildConnectionAgentIdentity, FORGE_DEFAULT_AGENT_ID, listAiModelConnections, syncForgeManagedWikiProfile } from "./model-settings.js";
10
+ import { listUsersByIds } from "./users.js";
10
11
  import { agentBootstrapPolicySchema, agentScopePolicySchema, createAgentTokenSchema, legacyAgentBootstrapPolicy, defaultAgentScopePolicy, agentIdentitySchema, customThemeSchema, settingsPayloadSchema, updateSettingsSchema } from "../types.js";
11
12
  const settingsFileSchema = settingsPayloadSchema.deepPartial();
12
13
  let settingsFileSyncDepth = 0;
@@ -303,11 +304,40 @@ function pickComparableOverrideSubset(source, template) {
303
304
  }
304
305
  return picked;
305
306
  }
306
- function mapAgent(row) {
307
+ function listAgentIdentityUserLinks(agentIds) {
308
+ if (agentIds.length === 0) {
309
+ return new Map();
310
+ }
311
+ const placeholders = agentIds.map(() => "?").join(",");
312
+ const rows = getDatabase()
313
+ .prepare(`SELECT agent_id, user_id, role
314
+ FROM agent_identity_users
315
+ WHERE agent_id IN (${placeholders})
316
+ ORDER BY role = 'primary' DESC, created_at ASC`)
317
+ .all(...agentIds);
318
+ const usersById = new Map(listUsersByIds(rows.map((row) => row.user_id)).map((user) => [user.id, user]));
319
+ const linksByAgentId = new Map();
320
+ for (const row of rows) {
321
+ const current = linksByAgentId.get(row.agent_id) ?? [];
322
+ current.push({
323
+ userId: row.user_id,
324
+ role: row.role,
325
+ user: usersById.get(row.user_id) ?? null
326
+ });
327
+ linksByAgentId.set(row.agent_id, current);
328
+ }
329
+ return linksByAgentId;
330
+ }
331
+ function mapAgent(row, linkedUsers = []) {
307
332
  return agentIdentitySchema.parse({
308
333
  id: row.id,
309
334
  label: row.label,
310
335
  agentType: row.agent_type,
336
+ identityKey: row.identity_key,
337
+ provider: row.provider,
338
+ machineKey: row.machine_key,
339
+ personaKey: row.persona_key,
340
+ linkedUsers,
311
341
  trustLevel: row.trust_level,
312
342
  autonomyMode: row.autonomy_mode,
313
343
  approvalMode: row.approval_mode,
@@ -351,6 +381,10 @@ function findAgentIdentity(agentId) {
351
381
  agent_identities.id,
352
382
  agent_identities.label,
353
383
  agent_identities.agent_type,
384
+ agent_identities.identity_key,
385
+ agent_identities.provider,
386
+ agent_identities.machine_key,
387
+ agent_identities.persona_key,
354
388
  agent_identities.trust_level,
355
389
  agent_identities.autonomy_mode,
356
390
  agent_identities.approval_mode,
@@ -358,36 +392,76 @@ function findAgentIdentity(agentId) {
358
392
  agent_identities.created_at,
359
393
  agent_identities.updated_at,
360
394
  COUNT(agent_tokens.id) AS token_count,
361
- COALESCE(SUM(CASE WHEN agent_tokens.revoked_at IS NULL THEN 1 ELSE 0 END), 0) AS active_token_count
395
+ COALESCE(SUM(CASE WHEN agent_tokens.id IS NOT NULL AND agent_tokens.revoked_at IS NULL THEN 1 ELSE 0 END), 0) AS active_token_count
362
396
  FROM agent_identities
363
397
  LEFT JOIN agent_tokens ON agent_tokens.agent_id = agent_identities.id
364
398
  WHERE agent_identities.id = ?
365
399
  GROUP BY agent_identities.id`)
366
400
  .get(agentId);
367
- return row ? mapAgent(row) : undefined;
401
+ const links = row ? listAgentIdentityUserLinks([row.id]) : new Map();
402
+ return row ? mapAgent(row, links.get(row.id) ?? []) : undefined;
403
+ }
404
+ function normalizeAgentIdentityPart(value) {
405
+ return value
406
+ ?.trim()
407
+ .toLowerCase()
408
+ .replace(/[^a-z0-9._:]+/g, "_")
409
+ .replace(/^_+|_+$/g, "") || "";
410
+ }
411
+ function runtimeProviderFromAgentType(agentType) {
412
+ const normalized = normalizeAgentIdentityPart(agentType);
413
+ if (normalized === "openclaw" ||
414
+ normalized === "hermes" ||
415
+ normalized === "codex") {
416
+ return normalized;
417
+ }
418
+ return null;
419
+ }
420
+ function deriveTokenAgentIdentityFields(input) {
421
+ const provider = runtimeProviderFromAgentType(input.agentType);
422
+ if (!provider) {
423
+ return {
424
+ identityKey: null,
425
+ provider: null,
426
+ machineKey: null,
427
+ personaKey: null
428
+ };
429
+ }
430
+ return {
431
+ identityKey: `runtime:${provider}:token:default`,
432
+ provider,
433
+ machineKey: "token",
434
+ personaKey: "default"
435
+ };
368
436
  }
369
437
  function upsertAgentIdentity(input) {
370
438
  const now = new Date().toISOString();
439
+ const identityFields = deriveTokenAgentIdentityFields(input);
371
440
  const existing = getDatabase()
372
441
  .prepare(`SELECT id
373
442
  FROM agent_identities
374
- WHERE lower(label) = lower(?)
443
+ WHERE (? IS NOT NULL AND identity_key = ?)
444
+ OR lower(label) = lower(?)
375
445
  LIMIT 1`)
376
- .get(input.agentLabel);
446
+ .get(identityFields.identityKey, identityFields.identityKey, input.agentLabel);
377
447
  if (existing) {
378
448
  getDatabase()
379
449
  .prepare(`UPDATE agent_identities
380
- SET agent_type = ?, trust_level = ?, autonomy_mode = ?, approval_mode = ?, description = ?, updated_at = ?
450
+ SET agent_type = ?, identity_key = COALESCE(identity_key, ?),
451
+ provider = COALESCE(provider, ?), machine_key = COALESCE(machine_key, ?),
452
+ persona_key = COALESCE(persona_key, ?), trust_level = ?,
453
+ autonomy_mode = ?, approval_mode = ?, description = ?, updated_at = ?
381
454
  WHERE id = ?`)
382
- .run(input.agentType, input.trustLevel, input.autonomyMode, input.approvalMode, input.description, now, existing.id);
455
+ .run(input.agentType, identityFields.identityKey, identityFields.provider, identityFields.machineKey, identityFields.personaKey, input.trustLevel, input.autonomyMode, input.approvalMode, input.description, now, existing.id);
383
456
  return findAgentIdentity(existing.id);
384
457
  }
385
458
  const agentId = `agt_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
386
459
  getDatabase()
387
460
  .prepare(`INSERT INTO agent_identities (
388
- id, label, agent_type, trust_level, autonomy_mode, approval_mode, description, created_at, updated_at
389
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
390
- .run(agentId, input.agentLabel, input.agentType, input.trustLevel, input.autonomyMode, input.approvalMode, input.description, now, now);
461
+ id, label, agent_type, identity_key, provider, machine_key, persona_key,
462
+ trust_level, autonomy_mode, approval_mode, description, created_at, updated_at
463
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
464
+ .run(agentId, input.agentLabel, input.agentType, identityFields.identityKey, identityFields.provider, identityFields.machineKey, identityFields.personaKey, input.trustLevel, input.autonomyMode, input.approvalMode, input.description, now, now);
391
465
  return findAgentIdentity(agentId);
392
466
  }
393
467
  function ensureSettingsRow(now = new Date().toISOString()) {
@@ -441,6 +515,10 @@ export function listAgentIdentities() {
441
515
  agent_identities.id,
442
516
  agent_identities.label,
443
517
  agent_identities.agent_type,
518
+ agent_identities.identity_key,
519
+ agent_identities.provider,
520
+ agent_identities.machine_key,
521
+ agent_identities.persona_key,
444
522
  agent_identities.trust_level,
445
523
  agent_identities.autonomy_mode,
446
524
  agent_identities.approval_mode,
@@ -448,19 +526,25 @@ export function listAgentIdentities() {
448
526
  agent_identities.created_at,
449
527
  agent_identities.updated_at,
450
528
  COUNT(agent_tokens.id) AS token_count,
451
- COALESCE(SUM(CASE WHEN agent_tokens.revoked_at IS NULL THEN 1 ELSE 0 END), 0) AS active_token_count
529
+ COALESCE(SUM(CASE WHEN agent_tokens.id IS NOT NULL AND agent_tokens.revoked_at IS NULL THEN 1 ELSE 0 END), 0) AS active_token_count
452
530
  FROM agent_identities
453
531
  LEFT JOIN agent_tokens ON agent_tokens.agent_id = agent_identities.id
454
532
  GROUP BY agent_identities.id
455
533
  ORDER BY agent_identities.created_at DESC`)
456
534
  .all();
457
- const manualAgents = rows.map(mapAgent);
535
+ const links = listAgentIdentityUserLinks(rows.map((row) => row.id));
536
+ const manualAgents = rows.map((row) => mapAgent(row, links.get(row.id) ?? []));
458
537
  const modelAgents = listAiModelConnections().map(buildConnectionAgentIdentity);
459
538
  const settings = readSettingsRow();
460
539
  const forgeAgent = agentIdentitySchema.parse({
461
540
  id: FORGE_DEFAULT_AGENT_ID,
462
541
  label: "Forge Agent",
463
542
  agentType: "forge_default",
543
+ identityKey: "forge:default",
544
+ provider: null,
545
+ machineKey: null,
546
+ personaKey: "default",
547
+ linkedUsers: [],
464
548
  trustLevel: "trusted",
465
549
  autonomyMode: "approval_required",
466
550
  approvalMode: "approval_by_default",
@@ -470,7 +554,11 @@ export function listAgentIdentities() {
470
554
  createdAt: settings.created_at,
471
555
  updatedAt: settings.updated_at
472
556
  });
473
- return [forgeAgent, ...modelAgents, ...manualAgents];
557
+ const deduped = new Map();
558
+ for (const agent of [forgeAgent, ...modelAgents, ...manualAgents]) {
559
+ deduped.set(agent.identityKey ?? agent.id, agent);
560
+ }
561
+ return Array.from(deduped.values());
474
562
  }
475
563
  export function isPsycheAuthRequired() {
476
564
  ensureSettingsRow();
@@ -153,6 +153,29 @@ export function ensureSystemUsers() {
153
153
  }
154
154
  }
155
155
  }
156
+ export function ensureBotUser(input) {
157
+ const parsed = createUserSchema.parse({
158
+ kind: "bot",
159
+ handle: normalizeHandle(input.handle),
160
+ displayName: input.displayName,
161
+ description: input.description,
162
+ accentColor: input.accentColor
163
+ });
164
+ const now = new Date().toISOString();
165
+ getDatabase()
166
+ .prepare(`INSERT INTO users (id, kind, handle, display_name, description, accent_color, created_at, updated_at)
167
+ VALUES (?, 'bot', ?, ?, ?, ?, ?, ?)
168
+ ON CONFLICT(id) DO UPDATE SET
169
+ kind = 'bot',
170
+ handle = excluded.handle,
171
+ display_name = excluded.display_name,
172
+ description = excluded.description,
173
+ accent_color = excluded.accent_color,
174
+ updated_at = excluded.updated_at`)
175
+ .run(input.id, parsed.handle, parsed.displayName, parsed.description, parsed.accentColor, now, now);
176
+ ensurePermissiveGrantsForUser(input.id, now);
177
+ return getUserById(input.id);
178
+ }
156
179
  function ensurePermissiveGrantsForUser(userId, now) {
157
180
  const existingUsers = listUsers();
158
181
  for (const otherUser of existingUsers) {
@@ -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);
@@ -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);
@@ -2104,6 +2104,15 @@ export const agentIdentitySchema = z.object({
2104
2104
  id: z.string(),
2105
2105
  label: z.string(),
2106
2106
  agentType: z.string(),
2107
+ identityKey: z.string().nullable().default(null),
2108
+ provider: agentRuntimeProviderSchema.nullable().default(null),
2109
+ machineKey: z.string().nullable().default(null),
2110
+ personaKey: z.string().nullable().default(null),
2111
+ linkedUsers: z.array(z.object({
2112
+ userId: z.string(),
2113
+ role: z.string(),
2114
+ user: userSummarySchema.nullable().default(null)
2115
+ })).default([]),
2107
2116
  trustLevel: agentTrustLevelSchema,
2108
2117
  autonomyMode: autonomyModeSchema,
2109
2118
  approvalMode: approvalModeSchema,
@@ -2188,6 +2197,10 @@ export const createAgentRuntimeSessionSchema = z.object({
2188
2197
  provider: agentRuntimeProviderSchema,
2189
2198
  agentLabel: nonEmptyTrimmedString,
2190
2199
  agentType: trimmedString.default("assistant"),
2200
+ agentIdentityKey: trimmedString.optional(),
2201
+ machineKey: trimmedString.optional(),
2202
+ personaKey: trimmedString.optional(),
2203
+ linkedUserIds: uniqueStringArraySchema.default([]),
2191
2204
  actorLabel: nonEmptyTrimmedString,
2192
2205
  sessionKey: nonEmptyTrimmedString,
2193
2206
  sessionLabel: trimmedString.default(""),
@@ -2,7 +2,7 @@
2
2
  "id": "forge-openclaw-plugin",
3
3
  "name": "Forge",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
- "version": "0.2.48",
5
+ "version": "0.2.50",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-openclaw-plugin",
3
- "version": "0.2.48",
3
+ "version": "0.2.50",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -95,10 +95,13 @@
95
95
  "zustand": "^5.0.5"
96
96
  },
97
97
  "overrides": {
98
+ "@aws-sdk/xml-builder": "^3.972.19",
98
99
  "basic-ftp": "^5.3.0",
99
100
  "axios": "^1.15.0",
101
+ "fast-xml-parser": "^5.7.1",
100
102
  "follow-redirects": "^1.16.0",
101
- "hono": "4.12.14"
103
+ "hono": "4.12.14",
104
+ "uuid": "^14.0.0"
102
105
  },
103
106
  "scripts": {
104
107
  "build": "node ./scripts/build.mjs"
@@ -57,7 +57,7 @@ VALUES (
57
57
  'wiki_space_shared',
58
58
  'shared',
59
59
  'Shared Forge Memory',
60
- 'Shared wiki space for file-backed Forge knowledge.',
60
+ 'Shared wiki space for SQLite-backed Forge knowledge.',
61
61
  NULL,
62
62
  'shared',
63
63
  CURRENT_TIMESTAMP,