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.
- package/README.md +93 -46
- package/dist/assets/{board-BAxNp060.js → board-DThHV1D8.js} +1 -2
- package/dist/assets/index-7gvVCqnV.css +1 -0
- package/dist/assets/index-_Cn6Prym.js +90 -0
- package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js +0 -1
- package/dist/assets/{motion-B9BeeSmV.js → motion-BtTJtHCw.js} +1 -2
- package/dist/assets/{table-kY1tUKX5.js → table-Bnw6pcwN.js} +1 -2
- package/dist/assets/{ui-FaWfAb5Q.js → ui-CnVxFkj0.js} +1 -2
- package/dist/assets/{vendor-CUxVKN94.js → vendor-BgZ3YrRd.js} +212 -208
- package/dist/gamification-previews/dark-fantasy-item-trophy-tasks-anvil-marathon.webp +0 -0
- package/dist/gamification-previews/dark-fantasy-item-trophy-xp-levels-the-first-heat.webp +0 -0
- package/dist/gamification-previews/dark-fantasy-item-unlock-streaks-molten-crown-fire.webp +0 -0
- package/dist/gamification-previews/dark-fantasy-mascot.webp +0 -0
- package/dist/gamification-previews/dramatic-smithie-item-trophy-tasks-anvil-marathon.webp +0 -0
- package/dist/gamification-previews/dramatic-smithie-item-trophy-xp-levels-the-first-heat.webp +0 -0
- package/dist/gamification-previews/dramatic-smithie-item-unlock-streaks-molten-crown-fire.webp +0 -0
- package/dist/gamification-previews/dramatic-smithie-mascot.webp +0 -0
- package/dist/gamification-previews/mind-locksmith-item-trophy-tasks-anvil-marathon.webp +0 -0
- package/dist/gamification-previews/mind-locksmith-item-trophy-xp-levels-the-first-heat.webp +0 -0
- package/dist/gamification-previews/mind-locksmith-item-unlock-streaks-molten-crown-fire.webp +0 -0
- package/dist/gamification-previews/mind-locksmith-mascot.webp +0 -0
- package/dist/index.html +7 -7
- package/dist/openclaw/parity.js +27 -0
- package/dist/openclaw/plugin-entry-shared.js +2 -2
- package/dist/openclaw/plugin-sdk-types.d.ts +2 -1
- package/dist/openclaw/routes.d.ts +4 -0
- package/dist/openclaw/routes.js +112 -3
- package/dist/openclaw/tools.js +32 -4
- package/dist/server/server/migrations/058_gamification_theme_preference.sql +1 -1
- package/dist/server/server/migrations/059_data_backup_retention.sql +2 -0
- package/dist/server/server/src/app.js +152 -43
- package/dist/server/server/src/data-management-types.js +2 -0
- package/dist/server/server/src/health.js +40 -0
- package/dist/server/server/src/openapi.js +398 -7
- package/dist/server/server/src/repositories/rewards.js +60 -0
- package/dist/server/server/src/repositories/settings.js +1 -1
- package/dist/server/server/src/services/data-management.js +32 -2
- package/dist/server/server/src/services/doctor.js +762 -0
- package/dist/server/server/src/services/gamification-assets.js +231 -0
- package/dist/server/server/src/services/gamification.js +75 -3
- package/dist/server/server/src/types.js +1 -1
- package/dist/server/server/src/web.js +7 -104
- package/dist/server/src/lib/api.js +18 -0
- package/dist/server/src/lib/gamification-catalog.js +1 -1
- package/dist/server/src/lib/schemas.js +1 -1
- package/openclaw.plugin.json +85 -3
- package/package.json +8 -4
- package/server/migrations/058_gamification_theme_preference.sql +1 -1
- package/server/migrations/059_data_backup_retention.sql +2 -0
- package/skills/forge-openclaw/SKILL.md +38 -19
- package/skills/forge-openclaw/entity_conversation_playbooks.md +66 -8
- package/skills/forge-openclaw/psyche_entity_playbooks.md +23 -0
- package/dist/assets/board-BAxNp060.js.map +0 -1
- package/dist/assets/index-B5Yt4i07.css +0 -1
- package/dist/assets/index-NZwTuYPs.js +0 -91
- package/dist/assets/index-NZwTuYPs.js.map +0 -1
- package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js.map +0 -1
- package/dist/assets/motion-B9BeeSmV.js.map +0 -1
- package/dist/assets/table-kY1tUKX5.js.map +0 -1
- package/dist/assets/ui-FaWfAb5Q.js.map +0 -1
- package/dist/assets/vendor-CUxVKN94.js.map +0 -1
- 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("
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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",
|
|
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("
|
|
59
|
+
gamificationTheme: gamificationThemeSchema.default("dramatic-smithie"),
|
|
60
60
|
customTheme: forgeCustomThemeSchema.nullable().optional(),
|
|
61
61
|
localePreference: appLocaleSchema,
|
|
62
62
|
calendarProviders: z
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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": "
|
|
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
|
|
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",
|