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.
- package/README.md +3 -3
- package/dist/assets/index-2_tuemtU.css +1 -0
- package/dist/assets/index-C9_gJvi6.js +91 -0
- package/dist/assets/index-C9_gJvi6.js.map +1 -0
- package/dist/index.html +2 -2
- package/dist/openclaw/parity.js +14 -0
- package/dist/openclaw/routes.js +42 -0
- package/dist/openclaw/session-registry.js +17 -0
- package/dist/openclaw/tools.js +3 -3
- package/dist/server/server/migrations/019_wiki_memory.sql +1 -1
- package/dist/server/server/migrations/052_agent_identity_tightening.sql +307 -0
- package/dist/server/server/migrations/053_agent_runtime_session_canonical_labels.sql +9 -0
- package/dist/server/server/migrations/054_sqlite_backed_wiki_memory.sql +8 -0
- package/dist/server/server/src/app.js +46 -14
- package/dist/server/server/src/db.js +0 -2
- package/dist/server/server/src/openapi.js +58 -3
- package/dist/server/server/src/repositories/agent-runtime-sessions.js +122 -16
- package/dist/server/server/src/repositories/model-settings.js +5 -0
- package/dist/server/server/src/repositories/notes.js +5 -2
- package/dist/server/server/src/repositories/settings.js +101 -13
- package/dist/server/server/src/repositories/users.js +23 -0
- package/dist/server/server/src/repositories/wiki-memory.js +16 -190
- package/dist/server/server/src/services/data-management.js +2 -9
- package/dist/server/server/src/types.js +13 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -2
- package/server/migrations/019_wiki_memory.sql +1 -1
- package/server/migrations/052_agent_identity_tightening.sql +307 -0
- package/server/migrations/053_agent_runtime_session_canonical_labels.sql +9 -0
- 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 +49 -0
- package/skills/forge-openclaw/psyche_entity_playbooks.md +32 -0
- package/dist/assets/index-Bv9FWWsZ.js +0 -91
- package/dist/assets/index-Bv9FWWsZ.js.map +0 -1
- 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
|
|
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
|
-
|
|
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
|
|
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 = ?,
|
|
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,
|
|
389
|
-
|
|
390
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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);
|
|
@@ -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(""),
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-openclaw-plugin",
|
|
3
|
-
"version": "0.2.
|
|
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"
|