forge-openclaw-plugin 0.2.19 → 0.2.21
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 +133 -2
- package/dist/assets/board-_C6oMy5w.js +6 -0
- package/dist/assets/{board-8L3uX7_O.js.map → board-_C6oMy5w.js.map} +1 -1
- package/dist/assets/index-B4A6TooJ.js +63 -0
- package/dist/assets/index-B4A6TooJ.js.map +1 -0
- package/dist/assets/index-D6Xs_2mo.css +1 -0
- package/dist/assets/{motion-1GAqqi8M.js → motion-D4sZgCHd.js} +2 -2
- package/dist/assets/{motion-1GAqqi8M.js.map → motion-D4sZgCHd.js.map} +1 -1
- package/dist/assets/{table-DBGlgRjk.js → table-BWzTaky1.js} +2 -2
- package/dist/assets/{table-DBGlgRjk.js.map → table-BWzTaky1.js.map} +1 -1
- package/dist/assets/{ui-iTluWjC4.js → ui-BzK4azQb.js} +7 -7
- package/dist/assets/{ui-iTluWjC4.js.map → ui-BzK4azQb.js.map} +1 -1
- package/dist/assets/vendor-DT3pnAKJ.css +1 -0
- package/dist/assets/vendor-De38P6YR.js +729 -0
- package/dist/assets/vendor-De38P6YR.js.map +1 -0
- package/dist/assets/viz-C6hfyqzu.js +34 -0
- package/dist/assets/viz-C6hfyqzu.js.map +1 -0
- package/dist/index.html +9 -9
- package/dist/openclaw/parity.d.ts +1 -1
- package/dist/openclaw/parity.js +29 -2
- package/dist/openclaw/routes.js +207 -24
- package/dist/openclaw/tools.js +324 -35
- package/dist/server/app.js +2080 -92
- package/dist/server/db.js +3 -0
- package/dist/server/health.js +1284 -0
- package/dist/server/managers/platform/background-job-manager.js +138 -2
- package/dist/server/managers/platform/llm-manager.js +126 -0
- package/dist/server/managers/platform/openai-responses-provider.js +773 -0
- package/dist/server/managers/runtime.js +6 -1
- package/dist/server/openapi.js +718 -0
- package/dist/server/preferences-seeds.js +409 -0
- package/dist/server/preferences-types.js +368 -0
- package/dist/server/psyche-types.js +42 -18
- package/dist/server/repositories/activity-events.js +53 -4
- package/dist/server/repositories/calendar.js +89 -15
- package/dist/server/repositories/collaboration.js +8 -3
- package/dist/server/repositories/diagnostic-logs.js +243 -0
- package/dist/server/repositories/entity-ownership.js +92 -0
- package/dist/server/repositories/goals.js +7 -2
- package/dist/server/repositories/habits.js +122 -16
- package/dist/server/repositories/notes.js +119 -41
- package/dist/server/repositories/preferences.js +1765 -0
- package/dist/server/repositories/projects.js +18 -7
- package/dist/server/repositories/psyche.js +84 -27
- package/dist/server/repositories/rewards.js +112 -4
- package/dist/server/repositories/strategies.js +450 -0
- package/dist/server/repositories/tags.js +11 -6
- package/dist/server/repositories/task-runs.js +10 -2
- package/dist/server/repositories/tasks.js +99 -17
- package/dist/server/repositories/users.js +417 -0
- package/dist/server/repositories/wiki-memory.js +3366 -0
- package/dist/server/services/context.js +20 -18
- package/dist/server/services/dashboard.js +29 -6
- package/dist/server/services/entity-crud.js +21 -3
- package/dist/server/services/insights.js +9 -7
- package/dist/server/services/projects.js +2 -1
- package/dist/server/services/psyche.js +10 -9
- package/dist/server/types.js +594 -30
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/015_multi_user_and_strategies.sql +244 -0
- package/server/migrations/016_health_companion.sql +158 -0
- package/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
- package/server/migrations/017_preferences.sql +131 -0
- package/server/migrations/018_preference_catalogs.sql +31 -0
- package/server/migrations/019_wiki_memory.sql +255 -0
- package/server/migrations/020_wiki_page_hierarchy.sql +11 -0
- package/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
- package/server/migrations/022_wiki_ingest_background.sql +85 -0
- package/server/migrations/023_diagnostic_logs.sql +28 -0
- package/skills/forge-openclaw/SKILL.md +126 -34
- package/skills/forge-openclaw/entity_conversation_playbooks.md +337 -0
- package/skills/forge-openclaw/psyche_entity_playbooks.md +404 -0
- package/dist/assets/board-8L3uX7_O.js +0 -6
- package/dist/assets/index-Cj1IBH_w.js +0 -36
- package/dist/assets/index-Cj1IBH_w.js.map +0 -1
- package/dist/assets/index-DQT6EbuS.css +0 -1
- package/dist/assets/vendor-BvM2F9Dp.js +0 -503
- package/dist/assets/vendor-BvM2F9Dp.js.map +0 -1
- package/dist/assets/vendor-CRS-psbw.css +0 -1
- package/dist/assets/viz-CNeunkfu.js +0 -34
- package/dist/assets/viz-CNeunkfu.js.map +0 -1
|
@@ -3,12 +3,14 @@ import { getDatabase } from "../db.js";
|
|
|
3
3
|
import { runInTransaction } from "../db.js";
|
|
4
4
|
import { HttpError } from "../errors.js";
|
|
5
5
|
import { recordActivityEvent } from "./activity-events.js";
|
|
6
|
+
import { decorateOwnedEntity, inferFirstOwnedUserId, setEntityOwner } from "./entity-ownership.js";
|
|
6
7
|
import { filterDeletedEntities, filterDeletedIds, isEntityDeleted } from "./deleted-entities.js";
|
|
7
8
|
import { getGoalById } from "./goals.js";
|
|
8
9
|
import { createLinkedNotes } from "./notes.js";
|
|
9
10
|
import { ensureDefaultProjectForGoal, getProjectById } from "./projects.js";
|
|
10
11
|
import { pruneLinkedEntityReferences } from "./psyche.js";
|
|
11
12
|
import { awardTaskCompletionReward, reverseLatestTaskCompletionReward } from "./rewards.js";
|
|
13
|
+
import { findUserByLabel, getDefaultUser, getUserById, resolveUserForMutation } from "./users.js";
|
|
12
14
|
import { assertTaskRelations } from "../services/relations.js";
|
|
13
15
|
import { computeWorkTime, emptyTaskTimeSummary } from "../services/work-time.js";
|
|
14
16
|
import { calendarSchedulingRulesSchema, taskSchema } from "../types.js";
|
|
@@ -19,7 +21,7 @@ function readTaskTagIds(taskId) {
|
|
|
19
21
|
return filterDeletedIds("tag", rows.map((row) => row.tag_id));
|
|
20
22
|
}
|
|
21
23
|
function mapTask(row, time = emptyTaskTimeSummary()) {
|
|
22
|
-
return taskSchema.parse({
|
|
24
|
+
return taskSchema.parse(decorateOwnedEntity("task", {
|
|
23
25
|
id: row.id,
|
|
24
26
|
title: row.title,
|
|
25
27
|
description: row.description,
|
|
@@ -42,7 +44,7 @@ function mapTask(row, time = emptyTaskTimeSummary()) {
|
|
|
42
44
|
updatedAt: row.updated_at,
|
|
43
45
|
tagIds: readTaskTagIds(row.id),
|
|
44
46
|
time
|
|
45
|
-
});
|
|
47
|
+
}));
|
|
46
48
|
}
|
|
47
49
|
function replaceTaskTags(taskId, tagIds) {
|
|
48
50
|
const database = getDatabase();
|
|
@@ -64,21 +66,76 @@ function normalizeCompletedAt(status, existingCompletedAt) {
|
|
|
64
66
|
}
|
|
65
67
|
return null;
|
|
66
68
|
}
|
|
69
|
+
function resolveTaskAssignment(input) {
|
|
70
|
+
if (input.userId !== undefined) {
|
|
71
|
+
const user = resolveUserForMutation(input.userId, input.owner ?? undefined);
|
|
72
|
+
return {
|
|
73
|
+
userId: user.id,
|
|
74
|
+
ownerLabel: input.owner?.trim() || user.displayName
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (input.owner && input.owner.trim().length > 0) {
|
|
78
|
+
const matchedUser = findUserByLabel(input.owner);
|
|
79
|
+
return {
|
|
80
|
+
userId: matchedUser?.id ??
|
|
81
|
+
input.currentUserId ??
|
|
82
|
+
input.inheritedUserId ??
|
|
83
|
+
getDefaultUser().id,
|
|
84
|
+
ownerLabel: matchedUser?.displayName ?? input.owner.trim()
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (input.currentUserId) {
|
|
88
|
+
const currentUser = getUserById(input.currentUserId);
|
|
89
|
+
if (currentUser) {
|
|
90
|
+
return {
|
|
91
|
+
userId: currentUser.id,
|
|
92
|
+
ownerLabel: currentUser.displayName
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (input.inheritedUserId) {
|
|
97
|
+
const inheritedUser = getUserById(input.inheritedUserId);
|
|
98
|
+
if (inheritedUser) {
|
|
99
|
+
return {
|
|
100
|
+
userId: inheritedUser.id,
|
|
101
|
+
ownerLabel: inheritedUser.displayName
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const fallbackUser = getDefaultUser();
|
|
106
|
+
return {
|
|
107
|
+
userId: fallbackUser.id,
|
|
108
|
+
ownerLabel: fallbackUser.displayName
|
|
109
|
+
};
|
|
110
|
+
}
|
|
67
111
|
function resolveProjectAndGoalIds(input, current) {
|
|
68
112
|
const currentGoalId = current?.goalId && getGoalById(current.goalId) ? current.goalId : null;
|
|
69
|
-
const currentProject = current?.projectId
|
|
70
|
-
|
|
113
|
+
const currentProject = current?.projectId
|
|
114
|
+
? (getProjectById(current.projectId) ?? null)
|
|
115
|
+
: null;
|
|
116
|
+
const currentProjectGoalId = currentProject?.goalId && getGoalById(currentProject.goalId)
|
|
117
|
+
? currentProject.goalId
|
|
118
|
+
: null;
|
|
71
119
|
const currentProjectId = currentProject?.id ?? null;
|
|
72
120
|
const requestedGoalId = input.goalId === undefined ? currentGoalId : input.goalId;
|
|
73
|
-
const goalChangedWithoutProjectOverride = current !== undefined &&
|
|
74
|
-
|
|
121
|
+
const goalChangedWithoutProjectOverride = current !== undefined &&
|
|
122
|
+
input.goalId !== undefined &&
|
|
123
|
+
input.goalId !== current.goalId &&
|
|
124
|
+
input.projectId === undefined;
|
|
125
|
+
const requestedProjectId = input.projectId === undefined
|
|
126
|
+
? goalChangedWithoutProjectOverride
|
|
127
|
+
? null
|
|
128
|
+
: currentProjectId
|
|
129
|
+
: input.projectId;
|
|
75
130
|
if (requestedProjectId) {
|
|
76
131
|
const project = getProjectById(requestedProjectId);
|
|
77
132
|
if (!project) {
|
|
78
133
|
throw new HttpError(404, "project_not_found", `Project ${requestedProjectId} does not exist`);
|
|
79
134
|
}
|
|
80
135
|
const projectGoalId = getGoalById(project.goalId) ? project.goalId : null;
|
|
81
|
-
if (requestedGoalId &&
|
|
136
|
+
if (requestedGoalId &&
|
|
137
|
+
projectGoalId &&
|
|
138
|
+
project.goalId !== requestedGoalId) {
|
|
82
139
|
throw new HttpError(409, "project_goal_mismatch", `Project ${requestedProjectId} does not belong to goal ${requestedGoalId}`);
|
|
83
140
|
}
|
|
84
141
|
return {
|
|
@@ -117,17 +174,25 @@ function inferReopenStatus(taskId) {
|
|
|
117
174
|
return "focus";
|
|
118
175
|
}
|
|
119
176
|
const metadata = JSON.parse(row.metadata_json);
|
|
120
|
-
return metadata.previousStatus && metadata.previousStatus !== "done"
|
|
177
|
+
return metadata.previousStatus && metadata.previousStatus !== "done"
|
|
178
|
+
? metadata.previousStatus
|
|
179
|
+
: "focus";
|
|
121
180
|
}
|
|
122
181
|
function updateTaskRecord(current, input, activity) {
|
|
123
182
|
const relationState = resolveProjectAndGoalIds(input, current);
|
|
124
183
|
const nextGoalId = relationState.goalId;
|
|
125
184
|
const nextProjectId = relationState.projectId;
|
|
126
185
|
const nextTagIds = input.tagIds ?? current.tagIds;
|
|
186
|
+
const assignment = resolveTaskAssignment({
|
|
187
|
+
userId: input.userId,
|
|
188
|
+
owner: input.owner,
|
|
189
|
+
currentUserId: current.userId
|
|
190
|
+
});
|
|
127
191
|
assertTaskRelations({ goalId: nextGoalId, tagIds: nextTagIds });
|
|
128
192
|
const nextStatus = input.status ?? current.status;
|
|
129
193
|
const movedColumns = nextStatus !== current.status;
|
|
130
|
-
const nextSort = input.sortOrder ??
|
|
194
|
+
const nextSort = input.sortOrder ??
|
|
195
|
+
(movedColumns ? nextSortOrder(nextStatus) : current.sortOrder);
|
|
131
196
|
const completedAt = normalizeCompletedAt(nextStatus, current.completedAt);
|
|
132
197
|
const updatedAt = new Date().toISOString();
|
|
133
198
|
getDatabase()
|
|
@@ -135,7 +200,7 @@ function updateTaskRecord(current, input, activity) {
|
|
|
135
200
|
SET title = ?, description = ?, status = ?, priority = ?, owner = ?, goal_id = ?, due_date = ?, effort = ?,
|
|
136
201
|
energy = ?, points = ?, planned_duration_seconds = ?, scheduling_rules_json = ?, sort_order = ?, completed_at = ?, updated_at = ?, project_id = ?
|
|
137
202
|
WHERE id = ?`)
|
|
138
|
-
.run(input.title ?? current.title, input.description ?? current.description, nextStatus, input.priority ?? current.priority,
|
|
203
|
+
.run(input.title ?? current.title, input.description ?? current.description, nextStatus, input.priority ?? current.priority, assignment.ownerLabel, nextGoalId, input.dueDate === undefined ? current.dueDate : input.dueDate, input.effort ?? current.effort, input.energy ?? current.energy, input.points ?? current.points, input.plannedDurationSeconds === undefined
|
|
139
204
|
? current.plannedDurationSeconds
|
|
140
205
|
: input.plannedDurationSeconds, input.schedulingRules === undefined
|
|
141
206
|
? current.schedulingRules === null
|
|
@@ -145,6 +210,7 @@ function updateTaskRecord(current, input, activity) {
|
|
|
145
210
|
? null
|
|
146
211
|
: JSON.stringify(input.schedulingRules), nextSort, completedAt, updatedAt, nextProjectId, current.id);
|
|
147
212
|
replaceTaskTags(current.id, nextTagIds);
|
|
213
|
+
setEntityOwner("task", current.id, assignment.userId);
|
|
148
214
|
const updated = getTaskById(current.id);
|
|
149
215
|
if (updated && activity) {
|
|
150
216
|
const statusChanged = current.status !== updated.status;
|
|
@@ -236,6 +302,15 @@ function fingerprintTaskCreate(input) {
|
|
|
236
302
|
}
|
|
237
303
|
function insertTaskRecord(input, activity) {
|
|
238
304
|
const relationState = resolveProjectAndGoalIds(input);
|
|
305
|
+
const inheritedUserId = inferFirstOwnedUserId([
|
|
306
|
+
{ entityType: "project", entityId: relationState.projectId },
|
|
307
|
+
{ entityType: "goal", entityId: relationState.goalId }
|
|
308
|
+
]);
|
|
309
|
+
const assignment = resolveTaskAssignment({
|
|
310
|
+
userId: input.userId,
|
|
311
|
+
owner: input.owner,
|
|
312
|
+
inheritedUserId
|
|
313
|
+
});
|
|
239
314
|
assertTaskRelations({ goalId: relationState.goalId, tagIds: input.tagIds });
|
|
240
315
|
if (!relationState.projectId) {
|
|
241
316
|
throw new HttpError(400, "project_required", "Tasks must belong to a project");
|
|
@@ -250,7 +325,10 @@ function insertTaskRecord(input, activity) {
|
|
|
250
325
|
planned_duration_seconds, scheduling_rules_json, sort_order, completed_at, created_at, updated_at
|
|
251
326
|
)
|
|
252
327
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
253
|
-
.run(id, input.title, input.description, input.status, input.priority,
|
|
328
|
+
.run(id, input.title, input.description, input.status, input.priority, assignment.ownerLabel, relationState.goalId, relationState.projectId, input.dueDate, input.effort, input.energy, input.points, input.plannedDurationSeconds, input.schedulingRules === null
|
|
329
|
+
? null
|
|
330
|
+
: JSON.stringify(input.schedulingRules), sortOrder, completedAt, now, now);
|
|
331
|
+
setEntityOwner("task", id, assignment.userId);
|
|
254
332
|
replaceTaskTags(id, input.tagIds);
|
|
255
333
|
const task = getTaskById(id);
|
|
256
334
|
if (activity) {
|
|
@@ -259,7 +337,9 @@ function insertTaskRecord(input, activity) {
|
|
|
259
337
|
entityId: task.id,
|
|
260
338
|
eventType: "task_created",
|
|
261
339
|
title: `Task created: ${task.title}`,
|
|
262
|
-
description: task.goalId
|
|
340
|
+
description: task.goalId
|
|
341
|
+
? `Linked to ${task.goalId} and assigned to ${task.owner}.`
|
|
342
|
+
: `Assigned to ${task.owner}.`,
|
|
263
343
|
actor: activity.actor ?? null,
|
|
264
344
|
source: activity.source,
|
|
265
345
|
metadata: {
|
|
@@ -281,7 +361,9 @@ export function listTasks(filters = {}) {
|
|
|
281
361
|
const whereClauses = [];
|
|
282
362
|
const params = [];
|
|
283
363
|
const todayIso = new Date().toISOString().slice(0, 10);
|
|
284
|
-
const weekIso = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
|
364
|
+
const weekIso = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
|
|
365
|
+
.toISOString()
|
|
366
|
+
.slice(0, 10);
|
|
285
367
|
if (filters.status) {
|
|
286
368
|
whereClauses.push("status = ?");
|
|
287
369
|
params.push(filters.status);
|
|
@@ -352,7 +434,9 @@ export function getTaskById(taskId) {
|
|
|
352
434
|
WHERE id = ?`)
|
|
353
435
|
.get(taskId);
|
|
354
436
|
const workTime = computeWorkTime();
|
|
355
|
-
return row
|
|
437
|
+
return row
|
|
438
|
+
? mapTask(row, workTime.taskSummaries.get(row.id) ?? emptyTaskTimeSummary())
|
|
439
|
+
: undefined;
|
|
356
440
|
}
|
|
357
441
|
export function createTask(input, activity) {
|
|
358
442
|
return runInTransaction(() => insertTaskRecord(input, activity));
|
|
@@ -414,9 +498,7 @@ export function deleteTask(taskId, activity) {
|
|
|
414
498
|
}
|
|
415
499
|
return runInTransaction(() => {
|
|
416
500
|
pruneLinkedEntityReferences("task", taskId);
|
|
417
|
-
getDatabase()
|
|
418
|
-
.prepare(`DELETE FROM tasks WHERE id = ?`)
|
|
419
|
-
.run(taskId);
|
|
501
|
+
getDatabase().prepare(`DELETE FROM tasks WHERE id = ?`).run(taskId);
|
|
420
502
|
if (activity) {
|
|
421
503
|
recordActivityEvent({
|
|
422
504
|
entityType: "task",
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getDatabase } from "../db.js";
|
|
3
|
+
import { createUserSchema, updateUserSchema, userAccessGrantSchema, userAccessRightsSchema, userOwnershipSummarySchema, userXpSummarySchema, updateUserAccessGrantSchema, userSummarySchema } from "../types.js";
|
|
4
|
+
function startOfWeek(date) {
|
|
5
|
+
const clone = new Date(date);
|
|
6
|
+
const day = clone.getDay();
|
|
7
|
+
const delta = day === 0 ? -6 : 1 - day;
|
|
8
|
+
clone.setDate(clone.getDate() + delta);
|
|
9
|
+
clone.setHours(0, 0, 0, 0);
|
|
10
|
+
return clone;
|
|
11
|
+
}
|
|
12
|
+
function normalizeHandle(value) {
|
|
13
|
+
return value
|
|
14
|
+
.trim()
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
17
|
+
.replace(/^_+|_+$/g, "")
|
|
18
|
+
.slice(0, 48);
|
|
19
|
+
}
|
|
20
|
+
function buildDefaultRights(self = false) {
|
|
21
|
+
return userAccessRightsSchema.parse({
|
|
22
|
+
discoverable: true,
|
|
23
|
+
canListUsers: true,
|
|
24
|
+
canReadProfile: true,
|
|
25
|
+
canReadEntities: true,
|
|
26
|
+
canSearchEntities: true,
|
|
27
|
+
canLinkEntities: true,
|
|
28
|
+
canCoordinate: true,
|
|
29
|
+
canAffectEntities: true,
|
|
30
|
+
canManageStrategies: true,
|
|
31
|
+
canCreateOnBehalf: true,
|
|
32
|
+
canViewMetrics: true,
|
|
33
|
+
canViewActivity: true,
|
|
34
|
+
...(self ? { discoverable: true } : {})
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function buildGrantConfig(self = false) {
|
|
38
|
+
return {
|
|
39
|
+
self,
|
|
40
|
+
mutable: self,
|
|
41
|
+
linkedEntities: true,
|
|
42
|
+
rights: buildDefaultRights(self)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function normalizeGrantConfig(value, options = {}) {
|
|
46
|
+
const current = value && typeof value === "object" && !Array.isArray(value)
|
|
47
|
+
? value
|
|
48
|
+
: {};
|
|
49
|
+
const defaultConfig = buildGrantConfig(options.self ?? false);
|
|
50
|
+
return {
|
|
51
|
+
self: typeof current.self === "boolean" ? current.self : defaultConfig.self,
|
|
52
|
+
mutable: typeof current.mutable === "boolean"
|
|
53
|
+
? current.mutable
|
|
54
|
+
: defaultConfig.mutable,
|
|
55
|
+
linkedEntities: typeof current.linkedEntities === "boolean"
|
|
56
|
+
? current.linkedEntities
|
|
57
|
+
: defaultConfig.linkedEntities,
|
|
58
|
+
rights: userAccessRightsSchema.parse({
|
|
59
|
+
...defaultConfig.rights,
|
|
60
|
+
...(current.rights &&
|
|
61
|
+
typeof current.rights === "object" &&
|
|
62
|
+
!Array.isArray(current.rights)
|
|
63
|
+
? current.rights
|
|
64
|
+
: current)
|
|
65
|
+
})
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function deriveAccessLevel(config) {
|
|
69
|
+
return config.self ||
|
|
70
|
+
config.mutable ||
|
|
71
|
+
config.rights.canAffectEntities ||
|
|
72
|
+
config.rights.canCreateOnBehalf ||
|
|
73
|
+
config.rights.canManageStrategies
|
|
74
|
+
? "manage"
|
|
75
|
+
: "view";
|
|
76
|
+
}
|
|
77
|
+
function upsertRelationshipGrant(subjectUserId, targetUserId, now) {
|
|
78
|
+
const database = getDatabase();
|
|
79
|
+
const self = subjectUserId === targetUserId;
|
|
80
|
+
const config = buildGrantConfig(self);
|
|
81
|
+
const accessLevel = deriveAccessLevel(config);
|
|
82
|
+
const existing = database
|
|
83
|
+
.prepare(`SELECT id
|
|
84
|
+
FROM user_access_grants
|
|
85
|
+
WHERE subject_user_id = ?
|
|
86
|
+
AND target_user_id = ?
|
|
87
|
+
ORDER BY CASE access_level WHEN 'manage' THEN 0 ELSE 1 END, created_at ASC
|
|
88
|
+
LIMIT 1`)
|
|
89
|
+
.get(subjectUserId, targetUserId);
|
|
90
|
+
if (existing) {
|
|
91
|
+
database
|
|
92
|
+
.prepare(`UPDATE user_access_grants
|
|
93
|
+
SET access_level = ?, config_json = ?, updated_at = ?
|
|
94
|
+
WHERE id = ?`)
|
|
95
|
+
.run(accessLevel, JSON.stringify(config), now, existing.id);
|
|
96
|
+
database
|
|
97
|
+
.prepare(`DELETE FROM user_access_grants
|
|
98
|
+
WHERE subject_user_id = ?
|
|
99
|
+
AND target_user_id = ?
|
|
100
|
+
AND id != ?`)
|
|
101
|
+
.run(subjectUserId, targetUserId, existing.id);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
database
|
|
105
|
+
.prepare(`INSERT INTO user_access_grants (
|
|
106
|
+
id,
|
|
107
|
+
subject_user_id,
|
|
108
|
+
target_user_id,
|
|
109
|
+
access_level,
|
|
110
|
+
config_json,
|
|
111
|
+
created_at,
|
|
112
|
+
updated_at
|
|
113
|
+
)
|
|
114
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
115
|
+
.run(`grant_${randomUUID().replaceAll("-", "").slice(0, 16)}`, subjectUserId, targetUserId, accessLevel, JSON.stringify(config), now, now);
|
|
116
|
+
}
|
|
117
|
+
export function ensureSystemUsers() {
|
|
118
|
+
const database = getDatabase();
|
|
119
|
+
const settingsRow = database
|
|
120
|
+
.prepare(`SELECT operator_name
|
|
121
|
+
FROM app_settings
|
|
122
|
+
WHERE id = 1`)
|
|
123
|
+
.get();
|
|
124
|
+
const operatorDisplayName = settingsRow?.operator_name?.trim() || "Operator";
|
|
125
|
+
const operatorHandle = normalizeHandle(operatorDisplayName) || "operator";
|
|
126
|
+
const now = new Date().toISOString();
|
|
127
|
+
database
|
|
128
|
+
.prepare(`INSERT OR IGNORE INTO users (id, kind, handle, display_name, description, accent_color, created_at, updated_at)
|
|
129
|
+
VALUES (?, 'human', ?, ?, 'Primary human Forge operator.', '#f4b97a', ?, ?)`)
|
|
130
|
+
.run("user_operator", operatorHandle, operatorDisplayName, now, now);
|
|
131
|
+
database
|
|
132
|
+
.prepare(`UPDATE users
|
|
133
|
+
SET handle = ?, display_name = ?, updated_at = ?
|
|
134
|
+
WHERE id = ?`)
|
|
135
|
+
.run(operatorHandle, operatorDisplayName, now, "user_operator");
|
|
136
|
+
database
|
|
137
|
+
.prepare(`INSERT OR IGNORE INTO users (id, kind, handle, display_name, description, accent_color, created_at, updated_at)
|
|
138
|
+
VALUES (
|
|
139
|
+
'user_forge_bot',
|
|
140
|
+
'bot',
|
|
141
|
+
'forge_bot',
|
|
142
|
+
'Forge Bot',
|
|
143
|
+
'Autonomous or semi-autonomous execution partner inside Forge.',
|
|
144
|
+
'#7dd3fc',
|
|
145
|
+
?,
|
|
146
|
+
?
|
|
147
|
+
)`)
|
|
148
|
+
.run(now, now);
|
|
149
|
+
const users = listUsers();
|
|
150
|
+
for (const subjectUser of users) {
|
|
151
|
+
for (const targetUser of users) {
|
|
152
|
+
upsertRelationshipGrant(subjectUser.id, targetUser.id, now);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function ensurePermissiveGrantsForUser(userId, now) {
|
|
157
|
+
const existingUsers = listUsers();
|
|
158
|
+
for (const otherUser of existingUsers) {
|
|
159
|
+
upsertRelationshipGrant(userId, otherUser.id, now);
|
|
160
|
+
if (otherUser.id !== userId) {
|
|
161
|
+
upsertRelationshipGrant(otherUser.id, userId, now);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function mapUser(row) {
|
|
166
|
+
return userSummarySchema.parse({
|
|
167
|
+
id: row.id,
|
|
168
|
+
kind: row.kind,
|
|
169
|
+
handle: row.handle,
|
|
170
|
+
displayName: row.display_name,
|
|
171
|
+
description: row.description,
|
|
172
|
+
accentColor: row.accent_color,
|
|
173
|
+
createdAt: row.created_at,
|
|
174
|
+
updatedAt: row.updated_at
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function readUserRows(whereSql = "", params = []) {
|
|
178
|
+
return getDatabase()
|
|
179
|
+
.prepare(`SELECT id, kind, handle, display_name, description, accent_color, created_at, updated_at
|
|
180
|
+
FROM users
|
|
181
|
+
${whereSql}
|
|
182
|
+
ORDER BY CASE kind WHEN 'human' THEN 0 ELSE 1 END, display_name ASC`)
|
|
183
|
+
.all(...params);
|
|
184
|
+
}
|
|
185
|
+
export function listUsers(filters = {}) {
|
|
186
|
+
const whereClauses = [];
|
|
187
|
+
const params = [];
|
|
188
|
+
if (filters.kind) {
|
|
189
|
+
whereClauses.push("kind = ?");
|
|
190
|
+
params.push(filters.kind);
|
|
191
|
+
}
|
|
192
|
+
const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
|
|
193
|
+
return readUserRows(whereSql, params).map(mapUser);
|
|
194
|
+
}
|
|
195
|
+
export function listUsersByIds(userIds) {
|
|
196
|
+
if (userIds.length === 0) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
const placeholders = userIds.map(() => "?").join(", ");
|
|
200
|
+
return readUserRows(`WHERE id IN (${placeholders})`, [...userIds]).map(mapUser);
|
|
201
|
+
}
|
|
202
|
+
export function getUserById(userId) {
|
|
203
|
+
const row = getDatabase()
|
|
204
|
+
.prepare(`SELECT id, kind, handle, display_name, description, accent_color, created_at, updated_at
|
|
205
|
+
FROM users
|
|
206
|
+
WHERE id = ?`)
|
|
207
|
+
.get(userId);
|
|
208
|
+
return row ? mapUser(row) : undefined;
|
|
209
|
+
}
|
|
210
|
+
export function listUserAccessGrants(filters = {}) {
|
|
211
|
+
const whereClauses = [];
|
|
212
|
+
const params = [];
|
|
213
|
+
if (filters.subjectUserId) {
|
|
214
|
+
whereClauses.push("subject_user_id = ?");
|
|
215
|
+
params.push(filters.subjectUserId);
|
|
216
|
+
}
|
|
217
|
+
if (filters.targetUserId) {
|
|
218
|
+
whereClauses.push("target_user_id = ?");
|
|
219
|
+
params.push(filters.targetUserId);
|
|
220
|
+
}
|
|
221
|
+
const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
|
|
222
|
+
const rows = getDatabase()
|
|
223
|
+
.prepare(`SELECT id, subject_user_id, target_user_id, access_level, config_json, created_at, updated_at
|
|
224
|
+
FROM user_access_grants
|
|
225
|
+
${whereSql}
|
|
226
|
+
ORDER BY subject_user_id ASC, target_user_id ASC`)
|
|
227
|
+
.all(...params);
|
|
228
|
+
const usersById = new Map(listUsers().map((user) => [user.id, user]));
|
|
229
|
+
return rows.map((row) => userAccessGrantSchema.parse({
|
|
230
|
+
id: row.id,
|
|
231
|
+
subjectUserId: row.subject_user_id,
|
|
232
|
+
targetUserId: row.target_user_id,
|
|
233
|
+
accessLevel: row.access_level,
|
|
234
|
+
config: normalizeGrantConfig(JSON.parse(row.config_json), {
|
|
235
|
+
self: row.subject_user_id === row.target_user_id
|
|
236
|
+
}),
|
|
237
|
+
createdAt: row.created_at,
|
|
238
|
+
updatedAt: row.updated_at,
|
|
239
|
+
subjectUser: usersById.get(row.subject_user_id) ?? null,
|
|
240
|
+
targetUser: usersById.get(row.target_user_id) ?? null
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
export function listUserOwnershipSummaries() {
|
|
244
|
+
const rows = getDatabase()
|
|
245
|
+
.prepare(`SELECT user_id, entity_type, COUNT(*) AS count
|
|
246
|
+
FROM entity_owners
|
|
247
|
+
GROUP BY user_id, entity_type
|
|
248
|
+
ORDER BY user_id ASC, entity_type ASC`)
|
|
249
|
+
.all();
|
|
250
|
+
const countsByUserId = new Map();
|
|
251
|
+
for (const row of rows) {
|
|
252
|
+
const current = countsByUserId.get(row.user_id) ?? {};
|
|
253
|
+
current[row.entity_type] = row.count;
|
|
254
|
+
countsByUserId.set(row.user_id, current);
|
|
255
|
+
}
|
|
256
|
+
return listUsers().map((user) => {
|
|
257
|
+
const entityCounts = countsByUserId.get(user.id) ?? {};
|
|
258
|
+
const totalOwnedEntities = Object.values(entityCounts).reduce((sum, count) => sum + count, 0);
|
|
259
|
+
return userOwnershipSummarySchema.parse({
|
|
260
|
+
userId: user.id,
|
|
261
|
+
totalOwnedEntities,
|
|
262
|
+
entityCounts
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
export function listUserXpSummaries() {
|
|
267
|
+
const users = listUsers();
|
|
268
|
+
const summaries = new Map(users.map((user) => [
|
|
269
|
+
user.id,
|
|
270
|
+
{
|
|
271
|
+
userId: user.id,
|
|
272
|
+
totalXp: 0,
|
|
273
|
+
weeklyXp: 0,
|
|
274
|
+
rewardEventCount: 0,
|
|
275
|
+
lastRewardAt: null
|
|
276
|
+
}
|
|
277
|
+
]));
|
|
278
|
+
const ownerRows = getDatabase()
|
|
279
|
+
.prepare(`SELECT entity_type, entity_id, user_id
|
|
280
|
+
FROM entity_owners`)
|
|
281
|
+
.all();
|
|
282
|
+
const ownerByEntityKey = new Map(ownerRows.map((row) => [`${row.entity_type}:${row.entity_id}`, row.user_id]));
|
|
283
|
+
const usersByLabel = new Map();
|
|
284
|
+
for (const user of users) {
|
|
285
|
+
usersByLabel.set(user.displayName.trim().toLowerCase(), user.id);
|
|
286
|
+
usersByLabel.set(user.handle.trim().toLowerCase(), user.id);
|
|
287
|
+
}
|
|
288
|
+
const weekStartIso = startOfWeek(new Date()).toISOString();
|
|
289
|
+
const rewardRows = getDatabase()
|
|
290
|
+
.prepare(`SELECT entity_type, entity_id, actor, delta_xp, created_at
|
|
291
|
+
FROM reward_ledger
|
|
292
|
+
ORDER BY created_at ASC`)
|
|
293
|
+
.all();
|
|
294
|
+
for (const row of rewardRows) {
|
|
295
|
+
const ownedUserId = row.entity_type === "system"
|
|
296
|
+
? row.actor
|
|
297
|
+
? (usersByLabel.get(row.actor.trim().toLowerCase()) ?? null)
|
|
298
|
+
: null
|
|
299
|
+
: (ownerByEntityKey.get(`${row.entity_type}:${row.entity_id}`) ?? null);
|
|
300
|
+
if (!ownedUserId) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const summary = summaries.get(ownedUserId);
|
|
304
|
+
if (!summary) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
summary.totalXp += row.delta_xp;
|
|
308
|
+
if (row.created_at >= weekStartIso) {
|
|
309
|
+
summary.weeklyXp += row.delta_xp;
|
|
310
|
+
}
|
|
311
|
+
summary.rewardEventCount += 1;
|
|
312
|
+
summary.lastRewardAt = row.created_at;
|
|
313
|
+
}
|
|
314
|
+
return users.map((user) => userXpSummarySchema.parse(summaries.get(user.id) ?? {
|
|
315
|
+
userId: user.id,
|
|
316
|
+
totalXp: 0,
|
|
317
|
+
weeklyXp: 0,
|
|
318
|
+
rewardEventCount: 0,
|
|
319
|
+
lastRewardAt: null
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
export function findUserByLabel(label) {
|
|
323
|
+
const normalizedLabel = label.trim().toLowerCase();
|
|
324
|
+
if (!normalizedLabel) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
const row = getDatabase()
|
|
328
|
+
.prepare(`SELECT id, kind, handle, display_name, description, accent_color, created_at, updated_at
|
|
329
|
+
FROM users
|
|
330
|
+
WHERE lower(display_name) = ?
|
|
331
|
+
OR lower(handle) = ?
|
|
332
|
+
ORDER BY CASE kind WHEN 'human' THEN 0 ELSE 1 END, created_at ASC
|
|
333
|
+
LIMIT 1`)
|
|
334
|
+
.get(normalizedLabel, normalizeHandle(normalizedLabel));
|
|
335
|
+
return row ? mapUser(row) : undefined;
|
|
336
|
+
}
|
|
337
|
+
export function getDefaultUser() {
|
|
338
|
+
return (getUserById("user_operator") ??
|
|
339
|
+
listUsers({ kind: "human" })[0] ??
|
|
340
|
+
listUsers()[0] ??
|
|
341
|
+
(() => {
|
|
342
|
+
throw new Error("Forge has no configured users");
|
|
343
|
+
})());
|
|
344
|
+
}
|
|
345
|
+
export function resolveUserForMutation(userId, fallbackLabel) {
|
|
346
|
+
if (userId) {
|
|
347
|
+
const user = getUserById(userId);
|
|
348
|
+
if (!user) {
|
|
349
|
+
throw new Error(`User ${userId} does not exist`);
|
|
350
|
+
}
|
|
351
|
+
return user;
|
|
352
|
+
}
|
|
353
|
+
if (fallbackLabel) {
|
|
354
|
+
const matched = findUserByLabel(fallbackLabel);
|
|
355
|
+
if (matched) {
|
|
356
|
+
return matched;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return getDefaultUser();
|
|
360
|
+
}
|
|
361
|
+
export function createUser(input) {
|
|
362
|
+
const parsed = createUserSchema.parse({
|
|
363
|
+
...input,
|
|
364
|
+
handle: normalizeHandle(input.handle || input.displayName)
|
|
365
|
+
});
|
|
366
|
+
const now = new Date().toISOString();
|
|
367
|
+
const id = `user_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
368
|
+
getDatabase()
|
|
369
|
+
.prepare(`INSERT INTO users (id, kind, handle, display_name, description, accent_color, created_at, updated_at)
|
|
370
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
371
|
+
.run(id, parsed.kind, parsed.handle, parsed.displayName, parsed.description, parsed.accentColor, now, now);
|
|
372
|
+
ensurePermissiveGrantsForUser(id, now);
|
|
373
|
+
return getUserById(id);
|
|
374
|
+
}
|
|
375
|
+
export function updateUser(userId, patch) {
|
|
376
|
+
const current = getUserById(userId);
|
|
377
|
+
if (!current) {
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
const parsed = updateUserSchema.parse(patch);
|
|
381
|
+
const next = {
|
|
382
|
+
kind: parsed.kind ?? current.kind,
|
|
383
|
+
handle: normalizeHandle(parsed.handle ?? current.handle),
|
|
384
|
+
displayName: parsed.displayName ?? current.displayName,
|
|
385
|
+
description: parsed.description ?? current.description,
|
|
386
|
+
accentColor: parsed.accentColor ?? current.accentColor,
|
|
387
|
+
updatedAt: new Date().toISOString()
|
|
388
|
+
};
|
|
389
|
+
getDatabase()
|
|
390
|
+
.prepare(`UPDATE users
|
|
391
|
+
SET kind = ?, handle = ?, display_name = ?, description = ?, accent_color = ?, updated_at = ?
|
|
392
|
+
WHERE id = ?`)
|
|
393
|
+
.run(next.kind, next.handle, next.displayName, next.description, next.accentColor, next.updatedAt, userId);
|
|
394
|
+
return getUserById(userId);
|
|
395
|
+
}
|
|
396
|
+
export function updateUserAccessGrant(grantId, patch) {
|
|
397
|
+
const current = listUserAccessGrants().find((grant) => grant.id === grantId);
|
|
398
|
+
if (!current) {
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
const parsed = updateUserAccessGrantSchema.parse(patch);
|
|
402
|
+
const nextConfig = normalizeGrantConfig({
|
|
403
|
+
...current.config,
|
|
404
|
+
rights: {
|
|
405
|
+
...current.config.rights,
|
|
406
|
+
...(parsed.rights ?? {})
|
|
407
|
+
}
|
|
408
|
+
}, { self: current.subjectUserId === current.targetUserId });
|
|
409
|
+
const nextAccessLevel = parsed.accessLevel ?? deriveAccessLevel(nextConfig);
|
|
410
|
+
const now = new Date().toISOString();
|
|
411
|
+
getDatabase()
|
|
412
|
+
.prepare(`UPDATE user_access_grants
|
|
413
|
+
SET access_level = ?, config_json = ?, updated_at = ?
|
|
414
|
+
WHERE id = ?`)
|
|
415
|
+
.run(nextAccessLevel, JSON.stringify(nextConfig), now, grantId);
|
|
416
|
+
return listUserAccessGrants().find((grant) => grant.id === grantId);
|
|
417
|
+
}
|