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.
@@ -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: "Forge Wiki Settings",
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: "Forge Wiki Health",
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: "Forge Wiki 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.",
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, `${userId} Wiki`, "Personal Forge wiki space.", userId, "personal", now, now);
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
- return manualHttpTransport(requestApiBaseUrl, requestUiBaseUrl, [
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: requestApiBaseUrl,
34
- fallbackUiBaseUrl: requestUiBaseUrl,
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
- "The QR keeps the request API/UI URL as a direct fallback when Iroh cannot complete a request.",
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);
@@ -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.117",
5
+ "version": "0.3.0",
6
6
  "activation": {
7
7
  "onStartup": true,
8
8
  "onCapabilities": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-openclaw-plugin",
3
- "version": "0.2.117",
3
+ "version": "0.3.0",
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": "Apache-2.0",