forge-openclaw-plugin 0.2.59 → 0.2.61

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 (62) hide show
  1. package/README.md +93 -46
  2. package/dist/assets/{board-BAxNp060.js → board-DThHV1D8.js} +1 -2
  3. package/dist/assets/index-7gvVCqnV.css +1 -0
  4. package/dist/assets/index-_Cn6Prym.js +90 -0
  5. package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js +0 -1
  6. package/dist/assets/{motion-B9BeeSmV.js → motion-BtTJtHCw.js} +1 -2
  7. package/dist/assets/{table-kY1tUKX5.js → table-Bnw6pcwN.js} +1 -2
  8. package/dist/assets/{ui-FaWfAb5Q.js → ui-CnVxFkj0.js} +1 -2
  9. package/dist/assets/{vendor-CUxVKN94.js → vendor-BgZ3YrRd.js} +212 -208
  10. package/dist/gamification-previews/dark-fantasy-item-trophy-tasks-anvil-marathon.webp +0 -0
  11. package/dist/gamification-previews/dark-fantasy-item-trophy-xp-levels-the-first-heat.webp +0 -0
  12. package/dist/gamification-previews/dark-fantasy-item-unlock-streaks-molten-crown-fire.webp +0 -0
  13. package/dist/gamification-previews/dark-fantasy-mascot.webp +0 -0
  14. package/dist/gamification-previews/dramatic-smithie-item-trophy-tasks-anvil-marathon.webp +0 -0
  15. package/dist/gamification-previews/dramatic-smithie-item-trophy-xp-levels-the-first-heat.webp +0 -0
  16. package/dist/gamification-previews/dramatic-smithie-item-unlock-streaks-molten-crown-fire.webp +0 -0
  17. package/dist/gamification-previews/dramatic-smithie-mascot.webp +0 -0
  18. package/dist/gamification-previews/mind-locksmith-item-trophy-tasks-anvil-marathon.webp +0 -0
  19. package/dist/gamification-previews/mind-locksmith-item-trophy-xp-levels-the-first-heat.webp +0 -0
  20. package/dist/gamification-previews/mind-locksmith-item-unlock-streaks-molten-crown-fire.webp +0 -0
  21. package/dist/gamification-previews/mind-locksmith-mascot.webp +0 -0
  22. package/dist/index.html +7 -7
  23. package/dist/openclaw/parity.js +27 -0
  24. package/dist/openclaw/plugin-entry-shared.js +2 -2
  25. package/dist/openclaw/plugin-sdk-types.d.ts +2 -1
  26. package/dist/openclaw/routes.d.ts +4 -0
  27. package/dist/openclaw/routes.js +112 -3
  28. package/dist/openclaw/tools.js +32 -4
  29. package/dist/server/server/migrations/058_gamification_theme_preference.sql +1 -1
  30. package/dist/server/server/migrations/059_data_backup_retention.sql +2 -0
  31. package/dist/server/server/src/app.js +152 -43
  32. package/dist/server/server/src/data-management-types.js +2 -0
  33. package/dist/server/server/src/health.js +40 -0
  34. package/dist/server/server/src/openapi.js +398 -7
  35. package/dist/server/server/src/repositories/rewards.js +60 -0
  36. package/dist/server/server/src/repositories/settings.js +1 -1
  37. package/dist/server/server/src/services/data-management.js +32 -2
  38. package/dist/server/server/src/services/doctor.js +762 -0
  39. package/dist/server/server/src/services/gamification-assets.js +231 -0
  40. package/dist/server/server/src/services/gamification.js +75 -3
  41. package/dist/server/server/src/types.js +1 -1
  42. package/dist/server/server/src/web.js +7 -104
  43. package/dist/server/src/lib/api.js +18 -0
  44. package/dist/server/src/lib/gamification-catalog.js +1 -1
  45. package/dist/server/src/lib/schemas.js +1 -1
  46. package/openclaw.plugin.json +85 -3
  47. package/package.json +8 -4
  48. package/server/migrations/058_gamification_theme_preference.sql +1 -1
  49. package/server/migrations/059_data_backup_retention.sql +2 -0
  50. package/skills/forge-openclaw/SKILL.md +38 -19
  51. package/skills/forge-openclaw/entity_conversation_playbooks.md +66 -8
  52. package/skills/forge-openclaw/psyche_entity_playbooks.md +23 -0
  53. package/dist/assets/board-BAxNp060.js.map +0 -1
  54. package/dist/assets/index-B5Yt4i07.css +0 -1
  55. package/dist/assets/index-NZwTuYPs.js +0 -91
  56. package/dist/assets/index-NZwTuYPs.js.map +0 -1
  57. package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js.map +0 -1
  58. package/dist/assets/motion-B9BeeSmV.js.map +0 -1
  59. package/dist/assets/table-kY1tUKX5.js.map +0 -1
  60. package/dist/assets/ui-FaWfAb5Q.js.map +0 -1
  61. package/dist/assets/vendor-CUxVKN94.js.map +0 -1
  62. package/dist/gamification/sprites.zip +0 -0
@@ -0,0 +1,231 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync } from "node:fs";
4
+ import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import AdmZip from "adm-zip";
7
+ import { getEffectiveDataRoot } from "../db.js";
8
+ import { GAMIFICATION_CATALOG, GAMIFICATION_MASCOT_KEYS } from "../../../src/lib/gamification-catalog.js";
9
+ const assetVersion = "0.2.59";
10
+ const defaultReleaseBaseUrl = "https://github.com/albertbuchard/forge/releases/download/forge-gamification-assets-v0.2.59";
11
+ const styleDefinitions = [
12
+ {
13
+ id: "dramatic-smithie",
14
+ label: "Fantasy",
15
+ description: "Warm, lighthearted 3D forge art with expressive mascot reactions and playful trophies.",
16
+ previewUrl: "/gamification-previews/dramatic-smithie-mascot.webp",
17
+ fileName: `forge-gamification-dramatic-smithie-${assetVersion}.zip`,
18
+ sha256: "407c98a89626d723f9f92e79411df7c999458459c96e0e09e73020b3d3ce14c0"
19
+ },
20
+ {
21
+ id: "dark-fantasy",
22
+ label: "Dark Fantasy",
23
+ description: "Obsidian iron, ember gold, high-pressure streak energy, and mythic trophy silhouettes.",
24
+ previewUrl: "/gamification-previews/dark-fantasy-mascot.webp",
25
+ fileName: `forge-gamification-dark-fantasy-${assetVersion}.zip`,
26
+ sha256: "9545900906784a23d15f4536eb8c32683ffff0ef42006d06c70cea101c1db570"
27
+ },
28
+ {
29
+ id: "mind-locksmith",
30
+ label: "Mind Locksmith",
31
+ description: "Modern locksmith-of-the-mind art for planning, memory, Psyche, health, and agent work.",
32
+ previewUrl: "/gamification-previews/mind-locksmith-mascot.webp",
33
+ fileName: `forge-gamification-mind-locksmith-${assetVersion}.zip`,
34
+ sha256: "cfdfd4259145e589e6e0fba8e1deb69d30931cfabbe6d626c0053e4f4cfe5f10"
35
+ }
36
+ ];
37
+ export const defaultGamificationAssetStyle = "dramatic-smithie";
38
+ function getCustomReleaseBaseUrl() {
39
+ return process.env.FORGE_GAMIFICATION_ASSET_BASE_URL?.trim().replace(/\/+$/, "");
40
+ }
41
+ function getDownloadUrl(style) {
42
+ const customReleaseBaseUrl = getCustomReleaseBaseUrl();
43
+ return `${customReleaseBaseUrl ?? defaultReleaseBaseUrl}/${style.fileName}`;
44
+ }
45
+ function getStyleDefinition(styleId) {
46
+ const style = styleDefinitions.find((candidate) => candidate.id === styleId);
47
+ if (!style) {
48
+ throw new Error(`Unknown gamification asset style: ${styleId}`);
49
+ }
50
+ return style;
51
+ }
52
+ function getExpectedSpritePaths(styleId) {
53
+ const itemAssetKeys = [
54
+ ...new Set(GAMIFICATION_CATALOG.map((item) => item.assetKey))
55
+ ];
56
+ const expectedPaths = new Set();
57
+ for (const key of itemAssetKeys) {
58
+ expectedPaths.add(`themes/${styleId}/items/${key}-256.webp`);
59
+ expectedPaths.add(`themes/${styleId}/items/${key}-512.webp`);
60
+ }
61
+ for (const key of GAMIFICATION_MASCOT_KEYS) {
62
+ expectedPaths.add(`themes/${styleId}/mascots/${key}-256.webp`);
63
+ expectedPaths.add(`themes/${styleId}/mascots/${key}-512.webp`);
64
+ }
65
+ return expectedPaths;
66
+ }
67
+ function assertSafeRelativePath(relativePath) {
68
+ if (relativePath.startsWith("/") ||
69
+ relativePath.includes("\\") ||
70
+ relativePath.split("/").some((segment) => segment === "..")) {
71
+ throw new Error(`Unsafe gamification asset path: ${relativePath}`);
72
+ }
73
+ }
74
+ function getStyleRoot(styleId) {
75
+ const style = getStyleDefinition(styleId);
76
+ return path.join(getEffectiveDataRoot(), "runtime-assets", "gamification", "styles", `${style.id}-${assetVersion}-${style.sha256.slice(0, 16)}`);
77
+ }
78
+ function getMarkerPath(styleId) {
79
+ return path.join(getStyleRoot(styleId), ".forge-gamification-style-ready.json");
80
+ }
81
+ async function countReadableFiles(root, relativePaths) {
82
+ let count = 0;
83
+ for (const relativePath of relativePaths) {
84
+ try {
85
+ await access(path.join(root, relativePath));
86
+ count += 1;
87
+ }
88
+ catch {
89
+ // Missing files are reported through the count.
90
+ }
91
+ }
92
+ return count;
93
+ }
94
+ async function readInstalledAt(styleId) {
95
+ try {
96
+ const marker = JSON.parse(await readFile(getMarkerPath(styleId), "utf8"));
97
+ return typeof marker.installedAt === "string" ? marker.installedAt : null;
98
+ }
99
+ catch {
100
+ return null;
101
+ }
102
+ }
103
+ async function getStyleStatus(styleId) {
104
+ const style = getStyleDefinition(styleId);
105
+ const expectedPaths = getExpectedSpritePaths(style.id);
106
+ const root = getStyleRoot(style.id);
107
+ const spriteCount = existsSync(getMarkerPath(style.id))
108
+ ? await countReadableFiles(root, expectedPaths)
109
+ : 0;
110
+ const installed = spriteCount === expectedPaths.size;
111
+ return {
112
+ ...style,
113
+ downloadUrl: getDownloadUrl(style),
114
+ installed,
115
+ spriteCount,
116
+ expectedSpriteCount: expectedPaths.size,
117
+ installedAt: installed ? await readInstalledAt(style.id) : null
118
+ };
119
+ }
120
+ export async function getGamificationAssetStatus() {
121
+ const styles = await Promise.all(styleDefinitions.map((style) => getStyleStatus(style.id)));
122
+ return {
123
+ version: assetVersion,
124
+ defaultStyle: defaultGamificationAssetStyle,
125
+ styles
126
+ };
127
+ }
128
+ function buildDownloadHeaders(url) {
129
+ const headers = {
130
+ Accept: "application/octet-stream"
131
+ };
132
+ const token = resolveGithubTokenForDownload(url);
133
+ if (token && /github\.com/i.test(url)) {
134
+ headers.Authorization = `Bearer ${token}`;
135
+ }
136
+ return headers;
137
+ }
138
+ function resolveGithubTokenForDownload(url) {
139
+ if (!/github\.com/i.test(url)) {
140
+ return undefined;
141
+ }
142
+ const envToken = process.env.FORGE_GAMIFICATION_GITHUB_TOKEN ?? process.env.GITHUB_TOKEN;
143
+ if (envToken?.trim()) {
144
+ return envToken.trim();
145
+ }
146
+ const result = spawnSync("gh", ["auth", "token"], {
147
+ encoding: "utf8",
148
+ stdio: ["ignore", "pipe", "ignore"]
149
+ });
150
+ const cliToken = result.status === 0 ? result.stdout.trim() : "";
151
+ return cliToken || undefined;
152
+ }
153
+ function validateArchive(styleId, archive) {
154
+ const expectedPaths = getExpectedSpritePaths(styleId);
155
+ const entriesByName = new Map(archive
156
+ .getEntries()
157
+ .filter((entry) => !entry.isDirectory)
158
+ .map((entry) => {
159
+ assertSafeRelativePath(entry.entryName);
160
+ return [entry.entryName, entry];
161
+ }));
162
+ const missing = [...expectedPaths].filter((entryName) => !entriesByName.has(entryName));
163
+ const unexpected = [...entriesByName.keys()].filter((entryName) => !expectedPaths.has(entryName));
164
+ if (missing.length > 0 || unexpected.length > 0) {
165
+ throw new Error(`Invalid gamification style archive for ${styleId}. Missing ${missing.length}, unexpected ${unexpected.length}.`);
166
+ }
167
+ return { expectedPaths, entriesByName };
168
+ }
169
+ export async function installGamificationAssetStyle(styleId, fetchImpl = fetch) {
170
+ const style = getStyleDefinition(styleId);
171
+ const downloadUrl = getDownloadUrl(style);
172
+ const response = await fetchImpl(downloadUrl, {
173
+ headers: buildDownloadHeaders(downloadUrl)
174
+ });
175
+ if (!response.ok) {
176
+ throw new Error(`Could not download gamification assets (${response.status} ${response.statusText}).`);
177
+ }
178
+ const archivePayload = Buffer.from(await response.arrayBuffer());
179
+ const actualSha256 = createHash("sha256").update(archivePayload).digest("hex");
180
+ if (actualSha256 !== style.sha256) {
181
+ throw new Error(`Gamification asset checksum mismatch for ${style.id}. Expected ${style.sha256}, got ${actualSha256}.`);
182
+ }
183
+ const archive = new AdmZip(archivePayload);
184
+ const { expectedPaths, entriesByName } = validateArchive(style.id, archive);
185
+ const targetRoot = getStyleRoot(style.id);
186
+ await rm(targetRoot, { recursive: true, force: true });
187
+ for (const relativePath of expectedPaths) {
188
+ const entry = entriesByName.get(relativePath);
189
+ if (!entry) {
190
+ throw new Error(`Missing gamification archive entry: ${relativePath}`);
191
+ }
192
+ const targetPath = path.join(targetRoot, relativePath);
193
+ await mkdir(path.dirname(targetPath), { recursive: true });
194
+ await writeFile(targetPath, entry.getData());
195
+ }
196
+ await writeFile(getMarkerPath(style.id), `${JSON.stringify({
197
+ style: style.id,
198
+ version: assetVersion,
199
+ sha256: style.sha256,
200
+ spriteCount: expectedPaths.size,
201
+ installedAt: new Date().toISOString(),
202
+ source: downloadUrl
203
+ }, null, 2)}\n`, "utf8");
204
+ return getStyleStatus(style.id);
205
+ }
206
+ export async function resolveGamificationSpriteAssetPath(relativeSpritePath) {
207
+ const safePath = relativeSpritePath.replace(/^\/+/, "");
208
+ try {
209
+ assertSafeRelativePath(safePath);
210
+ }
211
+ catch {
212
+ return path.join(getEffectiveDataRoot(), "runtime-assets", "missing-gamification-asset");
213
+ }
214
+ const match = /^themes\/([^/]+)\//.exec(safePath);
215
+ if (!match) {
216
+ return path.join(getEffectiveDataRoot(), "runtime-assets", "missing-gamification-asset");
217
+ }
218
+ const styleId = match[1];
219
+ const style = styleDefinitions.find((candidate) => candidate.id === styleId);
220
+ if (!style) {
221
+ return path.join(getEffectiveDataRoot(), "runtime-assets", "missing-gamification-asset");
222
+ }
223
+ if (!getExpectedSpritePaths(style.id).has(safePath)) {
224
+ return path.join(getEffectiveDataRoot(), "runtime-assets", "missing-gamification-asset");
225
+ }
226
+ const status = await getStyleStatus(style.id);
227
+ if (!status.installed) {
228
+ return path.join(getEffectiveDataRoot(), "runtime-assets", "missing-gamification-asset");
229
+ }
230
+ return path.join(getStyleRoot(style.id), safePath);
231
+ }
@@ -1,10 +1,41 @@
1
1
  import { getDatabase } from "../db.js";
2
2
  import { enqueueGamificationCelebration, getGamificationEquipment, insertGamificationUnlock, listGamificationDailyActivity, listGamificationUnlocks, listUnseenGamificationCelebrations, replaceGamificationDailyActivity, upsertGamificationEquipment } from "../repositories/gamification.js";
3
- import { getDailyAmbientXp, listRewardRules } from "../repositories/rewards.js";
3
+ import { getDailyAmbientXp, listRewardRules, recordEntityCreationReward } from "../repositories/rewards.js";
4
4
  import { getDefaultUser, listUsers, listUsersByIds } from "../repositories/users.js";
5
5
  import { GAMIFICATION_CATALOG, GAMIFICATION_STREAK_AWAY_DAY_KEYS, GAMIFICATION_STREAK_POWER_DAY_KEYS } from "../../../src/lib/gamification-catalog.js";
6
6
  import { achievementSignalSchema, gamificationCatalogPayloadSchema, gamificationProfileSchema, milestoneRewardSchema, rewardLedgerEventSchema } from "../types.js";
7
7
  const XP_CURVE_VERSION = "smith-forge";
8
+ const ENTITY_CREATION_REWARD_SOURCES = [
9
+ { entityType: "goal", tableName: "goals", titleColumn: "title" },
10
+ { entityType: "project", tableName: "projects", titleColumn: "title" },
11
+ { entityType: "strategy", tableName: "strategies", titleColumn: "title" },
12
+ { entityType: "task", tableName: "tasks", titleColumn: "title" },
13
+ { entityType: "habit", tableName: "habits", titleColumn: "title" },
14
+ { entityType: "note", tableName: "notes", titleColumn: "content_plain" },
15
+ { entityType: "tag", tableName: "tags", titleColumn: "name" },
16
+ { entityType: "calendar_event", tableName: "calendar_events", titleColumn: "title" },
17
+ {
18
+ entityType: "work_block_template",
19
+ tableName: "work_block_templates",
20
+ titleColumn: "title"
21
+ },
22
+ { entityType: "task_timebox", tableName: "task_timeboxes", titleColumn: "title" },
23
+ {
24
+ entityType: "questionnaire_instrument",
25
+ tableName: "questionnaire_instruments",
26
+ titleColumn: "title"
27
+ },
28
+ { entityType: "psyche_value", tableName: "psyche_values", titleColumn: "title" },
29
+ {
30
+ entityType: "behavior_pattern",
31
+ tableName: "behavior_patterns",
32
+ titleColumn: "title"
33
+ },
34
+ { entityType: "behavior", tableName: "psyche_behaviors", titleColumn: "title" },
35
+ { entityType: "belief_entry", tableName: "belief_entries", titleColumn: "statement" },
36
+ { entityType: "mode_profile", tableName: "mode_profiles", titleColumn: "title" },
37
+ { entityType: "trigger_report", tableName: "trigger_reports", titleColumn: "title" }
38
+ ];
8
39
  function startOfWeek(date) {
9
40
  const clone = new Date(date);
10
41
  const day = clone.getDay();
@@ -209,6 +240,42 @@ function loadScopedRewardEvents(scope) {
209
240
  ? true
210
241
  : event.ownerUserId !== null && scopeUserIds.has(event.ownerUserId));
211
242
  }
243
+ function syncEntityCreationRewards(scope) {
244
+ const database = getDatabase();
245
+ const scopeUserIds = [...new Set(scope.userIds)];
246
+ const scopePlaceholders = scopeUserIds.map(() => "?").join(", ");
247
+ for (const source of ENTITY_CREATION_REWARD_SOURCES) {
248
+ const scopedWhere = scopeUserIds.length > 0
249
+ ? `WHERE (
250
+ entity_owners.user_id IN (${scopePlaceholders})
251
+ OR (entity_owners.user_id IS NULL AND ? IS NOT NULL)
252
+ )`
253
+ : "";
254
+ const params = scopeUserIds.length > 0
255
+ ? [source.entityType, ...scopeUserIds, scopeUserIds[0] ?? null]
256
+ : [source.entityType];
257
+ const rows = database
258
+ .prepare(`SELECT
259
+ ${source.tableName}.id AS id,
260
+ ${source.tableName}.${source.titleColumn} AS title,
261
+ ${source.tableName}.created_at AS created_at
262
+ FROM ${source.tableName}
263
+ LEFT JOIN entity_owners
264
+ ON entity_owners.entity_type = ?
265
+ AND entity_owners.entity_id = ${source.tableName}.id
266
+ ${scopedWhere}`)
267
+ .all(...params);
268
+ for (const row of rows) {
269
+ recordEntityCreationReward({
270
+ entityType: source.entityType,
271
+ entityId: row.id,
272
+ title: row.title,
273
+ source: "system",
274
+ createdAt: row.created_at
275
+ });
276
+ }
277
+ }
278
+ }
212
279
  function isQualifyingStreakReward(event) {
213
280
  return (event.deltaXp > 0 &&
214
281
  event.reversedByRewardId === null &&
@@ -245,8 +312,9 @@ function syncDailyActivity(userId, scopedRewards, timezone) {
245
312
  }
246
313
  function calculateStreakFromActivity(activeDateKeys, now, timezone) {
247
314
  const today = dateKeyInTimezone(now, timezone);
315
+ const yesterday = subtractDaysFromDateKey(today, 1);
248
316
  let streak = 0;
249
- let cursor = today;
317
+ let cursor = activeDateKeys.has(today) ? today : yesterday;
250
318
  while (activeDateKeys.has(cursor)) {
251
319
  streak += 1;
252
320
  cursor = subtractDaysFromDateKey(cursor, 1);
@@ -297,8 +365,11 @@ function calculateMissedDays(activeDateKeys, now, timezone) {
297
365
  if (!latest || latest === today) {
298
366
  return { missedDays: 0, lastActiveDateKey: latest };
299
367
  }
368
+ if (latest === subtractDaysFromDateKey(today, 1)) {
369
+ return { missedDays: 0, lastActiveDateKey: latest };
370
+ }
300
371
  return {
301
- missedDays: Math.max(0, daysBetweenDateKeys(latest, today)),
372
+ missedDays: Math.max(0, daysBetweenDateKeys(latest, today) - 1),
302
373
  lastActiveDateKey: latest
303
374
  };
304
375
  }
@@ -746,6 +817,7 @@ function syncCatalog(input) {
746
817
  function buildGamificationState(goals, tasks, habits, options = {}) {
747
818
  const now = options.now ?? new Date();
748
819
  const scope = resolveGamificationScope(options.userIds);
820
+ syncEntityCreationRewards(scope);
749
821
  const scopedRewards = loadScopedRewardEvents(scope);
750
822
  const timezone = resolveTimezone();
751
823
  const primaryUserId = scope.userIds[0] ?? "aggregate";
@@ -2690,7 +2690,7 @@ export const settingsPayloadSchema = z.object({
2690
2690
  notifications: notificationPreferencesSchema,
2691
2691
  execution: executionSettingsSchema,
2692
2692
  themePreference: themePreferenceSchema,
2693
- gamificationTheme: gamificationThemeSchema.default("dark-fantasy"),
2693
+ gamificationTheme: gamificationThemeSchema.default("dramatic-smithie"),
2694
2694
  customTheme: customThemeSchema.nullable(),
2695
2695
  localePreference: appLocaleSchema,
2696
2696
  security: z.object({
@@ -1,14 +1,11 @@
1
1
  import { request as httpRequest } from "node:http";
2
2
  import { request as httpsRequest } from "node:https";
3
3
  import { spawn } from "node:child_process";
4
- import { createHash } from "node:crypto";
5
4
  import { existsSync } from "node:fs";
6
- import { access, mkdir, readFile, rm, writeFile } from "node:fs/promises";
5
+ import { access, readFile } from "node:fs/promises";
7
6
  import path from "node:path";
8
7
  import { setTimeout as delay } from "node:timers/promises";
9
- import AdmZip from "adm-zip";
10
- import { getEffectiveDataRoot } from "./db.js";
11
- import { GAMIFICATION_CATALOG, GAMIFICATION_MASCOT_KEYS } from "../../src/lib/gamification-catalog.js";
8
+ import { resolveGamificationSpriteAssetPath } from "./services/gamification-assets.js";
12
9
  const distDir = path.join(process.cwd(), "dist");
13
10
  const packagedRuntimeDistDir = path.join(process.cwd(), "plugins", "forge-codex", "runtime", "dist");
14
11
  const contentTypes = {
@@ -23,14 +20,6 @@ const contentTypes = {
23
20
  ".woff2": "font/woff2"
24
21
  };
25
22
  const gamificationSpriteRoutePrefix = "/gamification/sprites/";
26
- const gamificationSpriteArchivePath = path.join("gamification", "sprites.zip");
27
- const gamificationSpriteThemes = [
28
- "dark-fantasy",
29
- "dramatic-smithie",
30
- "mind-locksmith"
31
- ];
32
- const gamificationSpriteSizes = [256, 512];
33
- let gamificationAssetExtractionPromise = null;
34
23
  function normalizeBasePath(value) {
35
24
  if (!value || value === "/") {
36
25
  return "/";
@@ -124,99 +113,10 @@ function resolveAsset(clientDir, requestPath) {
124
113
  const safePath = requestPath.replace(/^\/+/, "");
125
114
  return path.join(clientDir, safePath);
126
115
  }
127
- function buildExpectedGamificationSpritePaths() {
128
- const itemAssetKeys = [
129
- ...new Set(GAMIFICATION_CATALOG.map((item) => item.assetKey))
130
- ];
131
- const expectedPaths = new Set();
132
- for (const theme of gamificationSpriteThemes) {
133
- for (const size of gamificationSpriteSizes) {
134
- for (const key of itemAssetKeys) {
135
- expectedPaths.add(`themes/${theme}/items/${key}-${size}.webp`);
136
- }
137
- for (const key of GAMIFICATION_MASCOT_KEYS) {
138
- expectedPaths.add(`themes/${theme}/mascots/${key}-${size}.webp`);
139
- }
140
- }
141
- }
142
- return expectedPaths;
143
- }
144
- function assertSafeZipEntryName(entryName) {
145
- if (entryName.startsWith("/") ||
146
- entryName.includes("\\") ||
147
- entryName.split("/").some((segment) => segment === "..")) {
148
- throw new Error(`Unsafe gamification asset archive entry: ${entryName}`);
149
- }
150
- }
151
- async function countReadableFiles(root, relativePaths) {
152
- let count = 0;
153
- for (const relativePath of relativePaths) {
154
- try {
155
- await access(path.join(root, relativePath));
156
- count += 1;
157
- }
158
- catch {
159
- // Missing files are counted by the caller.
160
- }
161
- }
162
- return count;
163
- }
164
- async function materializeGamificationAssets(clientDir) {
165
- const archivePath = path.join(clientDir, gamificationSpriteArchivePath);
166
- const archivePayload = await readFile(archivePath);
167
- const archiveHash = createHash("sha256")
168
- .update(archivePayload)
169
- .digest("hex")
170
- .slice(0, 16);
171
- const targetRoot = path.join(getEffectiveDataRoot(), "runtime-assets", "gamification", `sprites-${archiveHash}`);
172
- const markerPath = path.join(targetRoot, ".forge-gamification-sprites-ready.json");
173
- const expectedPaths = buildExpectedGamificationSpritePaths();
174
- if (existsSync(markerPath) &&
175
- (await countReadableFiles(targetRoot, expectedPaths)) === expectedPaths.size) {
176
- return targetRoot;
177
- }
178
- const archive = new AdmZip(archivePayload);
179
- const entriesByName = new Map(archive
180
- .getEntries()
181
- .filter((entry) => !entry.isDirectory)
182
- .map((entry) => {
183
- assertSafeZipEntryName(entry.entryName);
184
- return [entry.entryName, entry];
185
- }));
186
- const missing = [...expectedPaths].filter((entryName) => !entriesByName.has(entryName));
187
- const unexpected = [...entriesByName.keys()].filter((entryName) => !expectedPaths.has(entryName));
188
- if (missing.length > 0 || unexpected.length > 0) {
189
- throw new Error(`Invalid gamification sprite archive. Missing ${missing.length}, unexpected ${unexpected.length}.`);
190
- }
191
- await rm(targetRoot, { recursive: true, force: true });
192
- for (const relativePath of expectedPaths) {
193
- const entry = entriesByName.get(relativePath);
194
- if (!entry) {
195
- throw new Error(`Missing gamification sprite archive entry: ${relativePath}`);
196
- }
197
- const targetPath = path.join(targetRoot, relativePath);
198
- await mkdir(path.dirname(targetPath), { recursive: true });
199
- await writeFile(targetPath, entry.getData());
200
- }
201
- await writeFile(markerPath, `${JSON.stringify({
202
- archiveHash,
203
- spriteCount: expectedPaths.size,
204
- materializedAt: new Date().toISOString()
205
- }, null, 2)}\n`, "utf8");
206
- return targetRoot;
207
- }
208
116
  async function resolveBuiltAsset(clientDir, requestPath) {
209
117
  if (requestPath.startsWith(gamificationSpriteRoutePrefix)) {
210
118
  const relativeSpritePath = requestPath.slice(gamificationSpriteRoutePrefix.length);
211
- const safePath = relativeSpritePath.replace(/^\/+/, "");
212
- if (!gamificationAssetExtractionPromise) {
213
- gamificationAssetExtractionPromise = materializeGamificationAssets(clientDir).catch((error) => {
214
- gamificationAssetExtractionPromise = null;
215
- throw error;
216
- });
217
- }
218
- const spriteRoot = await gamificationAssetExtractionPromise;
219
- return path.join(spriteRoot, safePath);
119
+ return resolveGamificationSpriteAssetPath(relativeSpritePath);
220
120
  }
221
121
  return resolveAsset(clientDir, requestPath);
222
122
  }
@@ -438,7 +338,10 @@ async function serveAsset(requestPath, reply, options) {
438
338
  return { error: "Not found" };
439
339
  }
440
340
  const normalizedRequestPath = stripBasePath(requestTarget.pathname, getDefaultBasePath());
441
- const devWebOrigin = await options.devWebRuntime.ensureReady();
341
+ const handlesLocalGamificationSprite = normalizedRequestPath.startsWith(gamificationSpriteRoutePrefix);
342
+ const devWebOrigin = handlesLocalGamificationSprite
343
+ ? null
344
+ : await options.devWebRuntime.ensureReady();
442
345
  if (devWebOrigin) {
443
346
  try {
444
347
  return await proxyDevAsset({
@@ -1515,6 +1515,15 @@ export function getOperatorOverview() {
1515
1515
  export function getSettings() {
1516
1516
  return request("/api/v1/settings");
1517
1517
  }
1518
+ export function getForgeDoctor() {
1519
+ return request("/api/v1/doctor");
1520
+ }
1521
+ export function applyForgeDoctorFixes(input) {
1522
+ return request("/api/v1/doctor/fixes", {
1523
+ method: "POST",
1524
+ body: JSON.stringify(input)
1525
+ });
1526
+ }
1518
1527
  export function saveAiModelConnection(input) {
1519
1528
  return request("/api/v1/settings/models/connections", {
1520
1529
  method: "POST",
@@ -2063,6 +2072,15 @@ export function getGamificationCatalog(userIds) {
2063
2072
  const suffix = search.toString() ? `?${search}` : "";
2064
2073
  return request(`/api/v1/gamification/catalog${suffix}`);
2065
2074
  }
2075
+ export function getGamificationAssetStatus() {
2076
+ return request("/api/v1/gamification/assets");
2077
+ }
2078
+ export function installGamificationAssetStyle(style) {
2079
+ return request("/api/v1/gamification/assets/install", {
2080
+ method: "POST",
2081
+ body: JSON.stringify({ style })
2082
+ });
2083
+ }
2066
2084
  export function getGamificationEquipment(userIds) {
2067
2085
  const search = new URLSearchParams();
2068
2086
  appendUserIds(search, coerceUserIds(userIds));
@@ -222,7 +222,7 @@ const PSYCHE_TROPHIES = [
222
222
  trophy("psyche", "Value Blade", allOf(metric("psycheValueCount", 10), metric("goalLinkedTaskCompletionCount", 100)), "Create 10 values and complete 100 goal-linked tasks.", "Values and work started cutting in the same direction."),
223
223
  trophy("psyche", "Shadow Temper", allOf(metric("modeProfileCount", 12), metric("triggerReportRichCount", 50)), "Create 12 modes and 50 rich trigger reports.", "Shadow material became usable steel."),
224
224
  trophy("psyche", "Inner Forge", allOf(metric("psycheValueCount", 12), metric("behaviorPatternCount", 25), metric("beliefFlexibleAlternativeCount", 30)), "Create 12 values, 25 patterns, and 30 flexible beliefs.", "A full inner forge takes shape."),
225
- trophy("psyche", "Schema Bell", metric("questionnaireRunCount", 10), "Complete 10 questionnaire runs.", "Structured self-observation rang the bell repeatedly."),
225
+ trophy("psyche", "Schema Bell", metric("questionnaireRunCount", 40), "Complete 40 questionnaire runs.", "Structured self-observation rang the bell repeatedly."),
226
226
  trophy("psyche", "Mode Guide", metric("modeGuideSessionCount", 5), "Complete 5 mode guide sessions.", "Guided mode work became an actual practice."),
227
227
  trophy("psyche", "Repair Script", metric("behaviorCount", 10), "Create 10 Psyche behaviors.", "Behaviors now carry repair plans, not just names."),
228
228
  trophy("psyche", "Flexible Self", metric("beliefFlexibleAlternativeCount", 50), "Create 50 beliefs with flexible alternatives.", "A trophy for not letting old beliefs remain iron cages."),
@@ -56,7 +56,7 @@ export const settingsMutationSchema = z.object({
56
56
  timeAccountingMode: z.enum(["split", "parallel", "primary_only"])
57
57
  }),
58
58
  themePreference: forgeThemePreferenceSchema,
59
- gamificationTheme: gamificationThemeSchema.default("dark-fantasy"),
59
+ gamificationTheme: gamificationThemeSchema.default("dramatic-smithie"),
60
60
  customTheme: forgeCustomThemeSchema.nullable().optional(),
61
61
  localePreference: appLocaleSchema,
62
62
  calendarProviders: z
@@ -2,10 +2,92 @@
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.59",
5
+ "version": "0.2.61",
6
+ "activation": {
7
+ "onStartup": true,
8
+ "onCapabilities": [
9
+ "tool"
10
+ ],
11
+ "onCommands": [
12
+ "forge"
13
+ ]
14
+ },
6
15
  "skills": [
7
16
  "./skills"
8
17
  ],
18
+ "commandAliases": [
19
+ {
20
+ "name": "forge"
21
+ }
22
+ ],
23
+ "contracts": {
24
+ "tools": [
25
+ "forge_adjust_work_minutes",
26
+ "forge_apply_doctor_fix",
27
+ "forge_call_life_force_route",
28
+ "forge_call_movement_route",
29
+ "forge_call_workbench_route",
30
+ "forge_clone_questionnaire",
31
+ "forge_complete_questionnaire_run",
32
+ "forge_complete_task_run",
33
+ "forge_connect_calendar_provider",
34
+ "forge_create_entities",
35
+ "forge_create_task_timebox",
36
+ "forge_create_work_block_template",
37
+ "forge_delete_entities",
38
+ "forge_enqueue_preferences_item_from_entity",
39
+ "forge_ensure_questionnaire_draft",
40
+ "forge_focus_task_run",
41
+ "forge_get_agent_onboarding",
42
+ "forge_get_calendar_overview",
43
+ "forge_get_current_work",
44
+ "forge_get_doctor",
45
+ "forge_get_operator_context",
46
+ "forge_get_operator_overview",
47
+ "forge_get_preferences_workspace",
48
+ "forge_get_psyche_overview",
49
+ "forge_get_questionnaire",
50
+ "forge_get_questionnaire_run",
51
+ "forge_get_self_observation_calendar",
52
+ "forge_get_sleep_overview",
53
+ "forge_get_sports_overview",
54
+ "forge_get_ui_entrypoint",
55
+ "forge_get_user_directory",
56
+ "forge_get_weekly_review",
57
+ "forge_get_wiki_health",
58
+ "forge_get_wiki_page",
59
+ "forge_get_wiki_settings",
60
+ "forge_get_xp_metrics",
61
+ "forge_grant_reward_bonus",
62
+ "forge_heartbeat_task_run",
63
+ "forge_ingest_wiki_source",
64
+ "forge_list_questionnaires",
65
+ "forge_list_wiki_pages",
66
+ "forge_log_work",
67
+ "forge_merge_preferences_contexts",
68
+ "forge_post_insight",
69
+ "forge_publish_questionnaire_draft",
70
+ "forge_recommend_task_timeboxes",
71
+ "forge_reindex_wiki_embeddings",
72
+ "forge_release_task_run",
73
+ "forge_restore_entities",
74
+ "forge_search_entities",
75
+ "forge_search_wiki",
76
+ "forge_start_preferences_game",
77
+ "forge_start_questionnaire_run",
78
+ "forge_start_task_run",
79
+ "forge_submit_preferences_judgment",
80
+ "forge_submit_preferences_signal",
81
+ "forge_sync_calendar_connection",
82
+ "forge_sync_wiki_vault",
83
+ "forge_update_entities",
84
+ "forge_update_preferences_score",
85
+ "forge_update_questionnaire_run",
86
+ "forge_update_sleep_session",
87
+ "forge_update_workout_session",
88
+ "forge_upsert_wiki_page"
89
+ ]
90
+ },
9
91
  "uiHints": {
10
92
  "origin": {
11
93
  "label": "Forge Origin",
@@ -19,7 +101,7 @@
19
101
  },
20
102
  "dataRoot": {
21
103
  "label": "Forge Data Root",
22
- "help": "Absolute folder path for the shared Forge home. Local installs default to ~/.forge so OpenClaw, Hermes, and Codex automatically meet in one runtime.",
104
+ "help": "Optional absolute folder path for Forge data. Defaults to ~/.forge. When set, Forge stores forge.sqlite directly in this folder; use the same value across adapters that should share one database.",
23
105
  "placeholder": "~/.forge",
24
106
  "advanced": true
25
107
  },
@@ -63,7 +145,7 @@
63
145
  "dataRoot": {
64
146
  "type": "string",
65
147
  "default": "~/.forge",
66
- "description": "Absolute path for the shared Forge data root. Defaults to ~/.forge so local OpenClaw, Hermes, and Codex installs converge on one runtime."
148
+ "description": "Absolute path for the shared Forge data root. Defaults to ~/.forge. Explicit values override that default; use the same value across local adapters that should share one database."
67
149
  },
68
150
  "apiToken": {
69
151
  "type": "string",