forge-openclaw-plugin 0.2.117 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/openclaw/tools.js +2 -2
- package/dist/server/server/src/app.js +4 -2
- package/dist/server/server/src/health.js +2 -0
- package/dist/server/server/src/repositories/wiki-memory.js +39 -4
- package/dist/server/server/src/services/companion-iroh.js +78 -7
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/dist/openclaw/tools.js
CHANGED
|
@@ -613,7 +613,7 @@ export function registerForgePluginTools(api, config) {
|
|
|
613
613
|
});
|
|
614
614
|
registerReadTool(api, config, {
|
|
615
615
|
name: "forge_get_wiki_settings",
|
|
616
|
-
label: "
|
|
616
|
+
label: "KarpaWiki Settings",
|
|
617
617
|
description: "Read the current wiki spaces plus enabled LLM and embedding profiles before search, ingest, or page writes.",
|
|
618
618
|
path: () => "/api/v1/wiki/settings"
|
|
619
619
|
});
|
|
@@ -643,7 +643,7 @@ export function registerForgePluginTools(api, config) {
|
|
|
643
643
|
});
|
|
644
644
|
registerReadTool(api, config, {
|
|
645
645
|
name: "forge_get_wiki_health",
|
|
646
|
-
label: "
|
|
646
|
+
label: "KarpaWiki Health",
|
|
647
647
|
description: "Read unresolved links, orphan pages, missing summaries, raw-source counts, and index-path state for one wiki space.",
|
|
648
648
|
parameters: Type.Object({
|
|
649
649
|
spaceId: optionalString()
|
|
@@ -4766,7 +4766,7 @@ function buildAgentOnboardingPayload(request) {
|
|
|
4766
4766
|
task: "A concrete actionable work item. Task status is board state, not proof of live work.",
|
|
4767
4767
|
taskRun: "A live work session attached to a task. Start, heartbeat, focus, complete, and release runs instead of faking work with status alone.",
|
|
4768
4768
|
note: "A Markdown work note that can link to one or many entities. Use notes for progress evidence, context, and close-out summaries.",
|
|
4769
|
-
wiki: "
|
|
4769
|
+
wiki: "KarpaWiki is the SQLite-backed memory layer: Markdown content in notes rows plus media, backlinks, optional embeddings, explicit spaces, and structured links back to Forge entities.",
|
|
4770
4770
|
sleepSession: "A sleep session is a first-class health record with timing, sleep and bed duration, stage breakdown, recovery metrics, annotations, and Forge links back to planning or Psyche context.",
|
|
4771
4771
|
workoutSession: "A workout session is a first-class sports record imported from HealthKit or generated from a habit. It holds workout type, timing, energy or distance when available, subjective effort, narrative context, and Forge links.",
|
|
4772
4772
|
preferences: "Forge Preferences is the explicit taste-modeling domain. It has workspaces, contexts, concept libraries, direct items, pairwise judgments, direct signals, and inferred scores.",
|
|
@@ -8029,7 +8029,9 @@ export async function buildServer(options = {}) {
|
|
|
8029
8029
|
const pairingTransport = await buildCompanionPairingTransport({
|
|
8030
8030
|
requestedMode: parsed.transportMode,
|
|
8031
8031
|
requestApiBaseUrl,
|
|
8032
|
-
requestUiBaseUrl: buildUiBaseUrlFromApiBaseUrl(requestApiBaseUrl)
|
|
8032
|
+
requestUiBaseUrl: buildUiBaseUrlFromApiBaseUrl(requestApiBaseUrl),
|
|
8033
|
+
fallbackMode: parsed.fallbackMode,
|
|
8034
|
+
publicUrl: parsed.publicUrl
|
|
8033
8035
|
});
|
|
8034
8036
|
reply.code(201);
|
|
8035
8037
|
return createCompanionPairingSession(pairingTransport, parsed);
|
|
@@ -115,6 +115,8 @@ const companionSourceStatesSchema = z.object({
|
|
|
115
115
|
export const createCompanionPairingSessionSchema = z.object({
|
|
116
116
|
label: z.string().trim().default("Forge Companion"),
|
|
117
117
|
userId: z.string().trim().nullable().optional(),
|
|
118
|
+
publicUrl: z.string().trim().optional(),
|
|
119
|
+
fallbackMode: z.enum(["none", "tailscale", "fixed-ip"]).default("none"),
|
|
118
120
|
expiresInMinutes: z.coerce
|
|
119
121
|
.number()
|
|
120
122
|
.int()
|
|
@@ -10,6 +10,7 @@ import { createNoteLinkSchema, crudEntityTypeSchema, noteKindSchema, noteSchema
|
|
|
10
10
|
import { deleteEncryptedSecret, readEncryptedSecret, storeEncryptedSecret } from "./calendar.js";
|
|
11
11
|
import { isEntityDeleted } from "./deleted-entities.js";
|
|
12
12
|
import { recordDiagnosticLog } from "./diagnostic-logs.js";
|
|
13
|
+
import { getUserById } from "./users.js";
|
|
13
14
|
const MAX_WIKI_INGEST_TEXT_CHUNK_CHARS = 220_000;
|
|
14
15
|
const WIKI_INGEST_TEXT_CHUNK_OVERLAP_CHARS = 2_000;
|
|
15
16
|
const wikiSpaceSchema = z.object({
|
|
@@ -34,6 +35,15 @@ const wikiLinkEdgeSchema = z.object({
|
|
|
34
35
|
createdAt: z.string(),
|
|
35
36
|
updatedAt: z.string()
|
|
36
37
|
});
|
|
38
|
+
function normalizeWikiLinkTargetType(value) {
|
|
39
|
+
if (value === "note") {
|
|
40
|
+
return "page";
|
|
41
|
+
}
|
|
42
|
+
if (value === "page" || value === "entity" || value === "unresolved") {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
return "unresolved";
|
|
46
|
+
}
|
|
37
47
|
const wikiMediaAssetSchema = z.object({
|
|
38
48
|
id: z.string(),
|
|
39
49
|
spaceId: z.string(),
|
|
@@ -1228,20 +1238,45 @@ function ensureSharedWikiSpace() {
|
|
|
1228
1238
|
function ensurePersonalWikiSpace(userId) {
|
|
1229
1239
|
const existing = findExistingSpaceByOwner(userId);
|
|
1230
1240
|
if (existing) {
|
|
1241
|
+
repairAutoGeneratedPersonalWikiSpace(existing, userId);
|
|
1231
1242
|
ensureWikiSpaceSeedPages(existing.id);
|
|
1232
|
-
return existing;
|
|
1243
|
+
return getWikiSpaceById(existing.id) ?? existing;
|
|
1233
1244
|
}
|
|
1234
1245
|
const now = nowIso();
|
|
1246
|
+
const presentation = resolvePersonalWikiSpacePresentation(userId);
|
|
1235
1247
|
const id = `wiki_space_user_${slugify(userId)}`;
|
|
1236
|
-
const slug = `user-${slugify(userId)}`;
|
|
1237
1248
|
getDatabase()
|
|
1238
1249
|
.prepare(`INSERT INTO wiki_spaces (id, slug, label, description, owner_user_id, visibility, created_at, updated_at)
|
|
1239
1250
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1240
|
-
.run(id, slug,
|
|
1251
|
+
.run(id, presentation.slug, presentation.label, "Personal Forge wiki space.", userId, "personal", now, now);
|
|
1241
1252
|
const space = getWikiSpaceById(id);
|
|
1242
1253
|
ensureWikiSpaceSeedPages(space.id);
|
|
1243
1254
|
return space;
|
|
1244
1255
|
}
|
|
1256
|
+
function resolvePersonalWikiSpacePresentation(userId) {
|
|
1257
|
+
const user = getUserById(userId);
|
|
1258
|
+
const displayName = user?.displayName?.trim() || user?.handle?.trim() || userId;
|
|
1259
|
+
const slugSource = user?.handle?.trim() || displayName || userId;
|
|
1260
|
+
return {
|
|
1261
|
+
label: `${displayName} Wiki`,
|
|
1262
|
+
slug: slugify(slugSource) || `user-${slugify(userId)}`
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
function repairAutoGeneratedPersonalWikiSpace(space, userId) {
|
|
1266
|
+
const presentation = resolvePersonalWikiSpacePresentation(userId);
|
|
1267
|
+
const oldLabel = `${userId} Wiki`;
|
|
1268
|
+
const oldSlug = `user-${slugify(userId)}`;
|
|
1269
|
+
const nextLabel = space.label === oldLabel ? presentation.label : space.label;
|
|
1270
|
+
const nextSlug = space.slug === oldSlug ? presentation.slug : space.slug;
|
|
1271
|
+
if (nextLabel === space.label && nextSlug === space.slug) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
getDatabase()
|
|
1275
|
+
.prepare(`UPDATE wiki_spaces
|
|
1276
|
+
SET label = ?, slug = ?, updated_at = ?
|
|
1277
|
+
WHERE id = ?`)
|
|
1278
|
+
.run(nextLabel, nextSlug, nowIso(), space.id);
|
|
1279
|
+
}
|
|
1245
1280
|
function buildStarterPageMarkdown(page, space) {
|
|
1246
1281
|
if (page.slug === "index") {
|
|
1247
1282
|
return [
|
|
@@ -1581,7 +1616,7 @@ export function getWikiPageDetail(noteId) {
|
|
|
1581
1616
|
page: note,
|
|
1582
1617
|
backlinks: backlinkRows.map((row) => wikiLinkEdgeSchema.parse({
|
|
1583
1618
|
sourceNoteId: row.source_note_id,
|
|
1584
|
-
targetType: row.target_type,
|
|
1619
|
+
targetType: normalizeWikiLinkTargetType(row.target_type),
|
|
1585
1620
|
targetNoteId: row.target_note_id,
|
|
1586
1621
|
targetEntityType: row.target_entity_type,
|
|
1587
1622
|
targetEntityId: row.target_entity_id,
|
|
@@ -14,8 +14,12 @@ export async function buildCompanionPairingTransport(input) {
|
|
|
14
14
|
const requestApiBaseUrl = normalizeApiBaseUrl(input.requestApiBaseUrl);
|
|
15
15
|
const requestUiBaseUrl = normalizeUiBaseUrl(input.requestUiBaseUrl) ??
|
|
16
16
|
deriveUiBaseUrlFromApiBaseUrl(requestApiBaseUrl);
|
|
17
|
+
const selectedFallback = normalizeSelectedFallback(input.publicUrl) ??
|
|
18
|
+
normalizeRequestFallback(requestApiBaseUrl, requestUiBaseUrl);
|
|
17
19
|
if (input.requestedMode === "manual-http") {
|
|
18
|
-
|
|
20
|
+
const manualApiBaseUrl = selectedFallback?.apiBaseUrl ?? requestApiBaseUrl;
|
|
21
|
+
const manualUiBaseUrl = selectedFallback?.uiBaseUrl ?? requestUiBaseUrl;
|
|
22
|
+
return manualHttpTransport(manualApiBaseUrl, manualUiBaseUrl, [
|
|
19
23
|
"Manual HTTP/TCP pairing was explicitly requested."
|
|
20
24
|
]);
|
|
21
25
|
}
|
|
@@ -30,13 +34,18 @@ export async function buildCompanionPairingTransport(input) {
|
|
|
30
34
|
pairPayload: snapshot.pairPayload,
|
|
31
35
|
alpn: snapshot.alpn ?? COMPANION_IROH_ALPN,
|
|
32
36
|
localBaseUrl: snapshot.localBaseUrl,
|
|
33
|
-
fallbackApiBaseUrl:
|
|
34
|
-
fallbackUiBaseUrl:
|
|
37
|
+
fallbackApiBaseUrl: selectedFallback?.apiBaseUrl ?? null,
|
|
38
|
+
fallbackUiBaseUrl: selectedFallback?.uiBaseUrl ?? null,
|
|
39
|
+
fallbackMode: selectedFallback
|
|
40
|
+
? fallbackModeFor(selectedFallback.apiBaseUrl, input.fallbackMode)
|
|
41
|
+
: "none",
|
|
35
42
|
recreateCommand: snapshot.recreateCommand ?? undefined,
|
|
36
43
|
startedAt: snapshot.startedAt ?? undefined,
|
|
37
44
|
notes: [
|
|
38
45
|
"Default pairing uses Forge's Rust Iroh transport over QUIC first.",
|
|
39
|
-
|
|
46
|
+
selectedFallback
|
|
47
|
+
? "The QR includes the selected direct URL only as an explicit fallback/direct path."
|
|
48
|
+
: "No direct HTTP fallback was selected for this QR.",
|
|
40
49
|
"The QR payload carries the Iroh node id, host token, optional relay, and ALPN forge-companion/1.",
|
|
41
50
|
"Manual HTTP/TCP pairing remains available with --manual-http for advanced local setups."
|
|
42
51
|
]
|
|
@@ -201,21 +210,24 @@ function isIrohPairPayload(value) {
|
|
|
201
210
|
}
|
|
202
211
|
function irohTransport(input) {
|
|
203
212
|
const nodeId = input.pairPayload.node_id;
|
|
213
|
+
const irohApiBaseUrl = companionIrohApiBaseUrlFromNodeId(nodeId);
|
|
214
|
+
const irohUiBaseUrl = companionIrohUiBaseUrlFromNodeId(nodeId);
|
|
204
215
|
return {
|
|
205
216
|
transportMode: "iroh",
|
|
206
|
-
apiBaseUrl: input.fallbackApiBaseUrl,
|
|
207
|
-
uiBaseUrl: input.fallbackUiBaseUrl,
|
|
217
|
+
apiBaseUrl: input.fallbackApiBaseUrl ?? irohApiBaseUrl,
|
|
218
|
+
uiBaseUrl: input.fallbackUiBaseUrl ?? irohUiBaseUrl,
|
|
208
219
|
transport: {
|
|
209
220
|
protocol: "iroh",
|
|
210
221
|
provider: "forge-companion-iroh",
|
|
211
222
|
status: "ready",
|
|
212
|
-
publicBaseUrl: input.fallbackApiBaseUrl,
|
|
223
|
+
publicBaseUrl: input.fallbackApiBaseUrl ?? undefined,
|
|
213
224
|
localBaseUrl: input.localBaseUrl,
|
|
214
225
|
nodeId,
|
|
215
226
|
relay: input.pairPayload.relay,
|
|
216
227
|
alpn: input.alpn,
|
|
217
228
|
agent: FORGE_IROH_AGENT,
|
|
218
229
|
pairPayload: input.pairPayload,
|
|
230
|
+
fallbackMode: input.fallbackMode,
|
|
219
231
|
recreateCommand: input.recreateCommand,
|
|
220
232
|
startedAt: input.startedAt,
|
|
221
233
|
notes: input.notes
|
|
@@ -383,6 +395,65 @@ function normalizeUiBaseUrl(value) {
|
|
|
383
395
|
return null;
|
|
384
396
|
}
|
|
385
397
|
}
|
|
398
|
+
function normalizeSelectedFallback(value) {
|
|
399
|
+
if (!value?.trim()) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
const apiBaseUrl = normalizeFallbackApiBaseUrl(value);
|
|
403
|
+
if (isLoopbackUrl(apiBaseUrl)) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
apiBaseUrl,
|
|
408
|
+
uiBaseUrl: normalizeUiBaseUrl(value) ?? deriveUiBaseUrlFromApiBaseUrl(apiBaseUrl)
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function normalizeRequestFallback(apiBaseUrl, uiBaseUrl) {
|
|
412
|
+
if (isLoopbackUrl(apiBaseUrl)) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
apiBaseUrl,
|
|
417
|
+
uiBaseUrl
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function fallbackModeFor(apiBaseUrl, requestedMode) {
|
|
421
|
+
if (requestedMode === "fixed-ip" || requestedMode === "tailscale") {
|
|
422
|
+
return requestedMode;
|
|
423
|
+
}
|
|
424
|
+
try {
|
|
425
|
+
const hostname = new URL(apiBaseUrl).hostname.toLowerCase();
|
|
426
|
+
return hostname.endsWith(".ts.net") ? "tailscale" : "fixed-ip";
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
return "fixed-ip";
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function normalizeFallbackApiBaseUrl(value) {
|
|
433
|
+
try {
|
|
434
|
+
const url = new URL(value.trim());
|
|
435
|
+
if (url.pathname.includes("/api/v1")) {
|
|
436
|
+
return normalizeApiBaseUrl(url.toString());
|
|
437
|
+
}
|
|
438
|
+
url.pathname = "/api/v1";
|
|
439
|
+
url.search = "";
|
|
440
|
+
url.hash = "";
|
|
441
|
+
return url.toString().replace(/\/$/u, "");
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
return normalizeApiBaseUrl(value);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function isLoopbackUrl(value) {
|
|
448
|
+
try {
|
|
449
|
+
const url = new URL(value);
|
|
450
|
+
const host = url.hostname.toLowerCase();
|
|
451
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
386
457
|
function deriveUiBaseUrlFromApiBaseUrl(apiBaseUrl) {
|
|
387
458
|
try {
|
|
388
459
|
const url = new URL(apiBaseUrl);
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "0.3.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onStartup": true,
|
|
8
8
|
"onCapabilities": [
|
package/package.json
CHANGED