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.
Files changed (82) hide show
  1. package/README.md +133 -2
  2. package/dist/assets/board-_C6oMy5w.js +6 -0
  3. package/dist/assets/{board-8L3uX7_O.js.map → board-_C6oMy5w.js.map} +1 -1
  4. package/dist/assets/index-B4A6TooJ.js +63 -0
  5. package/dist/assets/index-B4A6TooJ.js.map +1 -0
  6. package/dist/assets/index-D6Xs_2mo.css +1 -0
  7. package/dist/assets/{motion-1GAqqi8M.js → motion-D4sZgCHd.js} +2 -2
  8. package/dist/assets/{motion-1GAqqi8M.js.map → motion-D4sZgCHd.js.map} +1 -1
  9. package/dist/assets/{table-DBGlgRjk.js → table-BWzTaky1.js} +2 -2
  10. package/dist/assets/{table-DBGlgRjk.js.map → table-BWzTaky1.js.map} +1 -1
  11. package/dist/assets/{ui-iTluWjC4.js → ui-BzK4azQb.js} +7 -7
  12. package/dist/assets/{ui-iTluWjC4.js.map → ui-BzK4azQb.js.map} +1 -1
  13. package/dist/assets/vendor-DT3pnAKJ.css +1 -0
  14. package/dist/assets/vendor-De38P6YR.js +729 -0
  15. package/dist/assets/vendor-De38P6YR.js.map +1 -0
  16. package/dist/assets/viz-C6hfyqzu.js +34 -0
  17. package/dist/assets/viz-C6hfyqzu.js.map +1 -0
  18. package/dist/index.html +9 -9
  19. package/dist/openclaw/parity.d.ts +1 -1
  20. package/dist/openclaw/parity.js +29 -2
  21. package/dist/openclaw/routes.js +207 -24
  22. package/dist/openclaw/tools.js +324 -35
  23. package/dist/server/app.js +2080 -92
  24. package/dist/server/db.js +3 -0
  25. package/dist/server/health.js +1284 -0
  26. package/dist/server/managers/platform/background-job-manager.js +138 -2
  27. package/dist/server/managers/platform/llm-manager.js +126 -0
  28. package/dist/server/managers/platform/openai-responses-provider.js +773 -0
  29. package/dist/server/managers/runtime.js +6 -1
  30. package/dist/server/openapi.js +718 -0
  31. package/dist/server/preferences-seeds.js +409 -0
  32. package/dist/server/preferences-types.js +368 -0
  33. package/dist/server/psyche-types.js +42 -18
  34. package/dist/server/repositories/activity-events.js +53 -4
  35. package/dist/server/repositories/calendar.js +89 -15
  36. package/dist/server/repositories/collaboration.js +8 -3
  37. package/dist/server/repositories/diagnostic-logs.js +243 -0
  38. package/dist/server/repositories/entity-ownership.js +92 -0
  39. package/dist/server/repositories/goals.js +7 -2
  40. package/dist/server/repositories/habits.js +122 -16
  41. package/dist/server/repositories/notes.js +119 -41
  42. package/dist/server/repositories/preferences.js +1765 -0
  43. package/dist/server/repositories/projects.js +18 -7
  44. package/dist/server/repositories/psyche.js +84 -27
  45. package/dist/server/repositories/rewards.js +112 -4
  46. package/dist/server/repositories/strategies.js +450 -0
  47. package/dist/server/repositories/tags.js +11 -6
  48. package/dist/server/repositories/task-runs.js +10 -2
  49. package/dist/server/repositories/tasks.js +99 -17
  50. package/dist/server/repositories/users.js +417 -0
  51. package/dist/server/repositories/wiki-memory.js +3366 -0
  52. package/dist/server/services/context.js +20 -18
  53. package/dist/server/services/dashboard.js +29 -6
  54. package/dist/server/services/entity-crud.js +21 -3
  55. package/dist/server/services/insights.js +9 -7
  56. package/dist/server/services/projects.js +2 -1
  57. package/dist/server/services/psyche.js +10 -9
  58. package/dist/server/types.js +594 -30
  59. package/openclaw.plugin.json +1 -1
  60. package/package.json +1 -1
  61. package/server/migrations/015_multi_user_and_strategies.sql +244 -0
  62. package/server/migrations/016_health_companion.sql +158 -0
  63. package/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
  64. package/server/migrations/017_preferences.sql +131 -0
  65. package/server/migrations/018_preference_catalogs.sql +31 -0
  66. package/server/migrations/019_wiki_memory.sql +255 -0
  67. package/server/migrations/020_wiki_page_hierarchy.sql +11 -0
  68. package/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
  69. package/server/migrations/022_wiki_ingest_background.sql +85 -0
  70. package/server/migrations/023_diagnostic_logs.sql +28 -0
  71. package/skills/forge-openclaw/SKILL.md +126 -34
  72. package/skills/forge-openclaw/entity_conversation_playbooks.md +337 -0
  73. package/skills/forge-openclaw/psyche_entity_playbooks.md +404 -0
  74. package/dist/assets/board-8L3uX7_O.js +0 -6
  75. package/dist/assets/index-Cj1IBH_w.js +0 -36
  76. package/dist/assets/index-Cj1IBH_w.js.map +0 -1
  77. package/dist/assets/index-DQT6EbuS.css +0 -1
  78. package/dist/assets/vendor-BvM2F9Dp.js +0 -503
  79. package/dist/assets/vendor-BvM2F9Dp.js.map +0 -1
  80. package/dist/assets/vendor-CRS-psbw.css +0 -1
  81. package/dist/assets/viz-CNeunkfu.js +0 -34
  82. 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 ? getProjectById(current.projectId) ?? null : null;
70
- const currentProjectGoalId = currentProject?.goalId && getGoalById(currentProject.goalId) ? currentProject.goalId : null;
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 && input.goalId !== undefined && input.goalId !== current.goalId && input.projectId === undefined;
74
- const requestedProjectId = input.projectId === undefined ? (goalChangedWithoutProjectOverride ? null : currentProjectId) : input.projectId;
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 && projectGoalId && project.goalId !== 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" ? metadata.previousStatus : "focus";
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 ?? (movedColumns ? nextSortOrder(nextStatus) : current.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, input.owner ?? current.owner, nextGoalId, input.dueDate === undefined ? current.dueDate : input.dueDate, input.effort ?? current.effort, input.energy ?? current.energy, input.points ?? current.points, input.plannedDurationSeconds === undefined
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, input.owner, relationState.goalId, relationState.projectId, input.dueDate, input.effort, input.energy, input.points, input.plannedDurationSeconds, input.schedulingRules === null ? null : JSON.stringify(input.schedulingRules), sortOrder, completedAt, now, now);
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 ? `Linked to ${task.goalId} and assigned to ${task.owner}.` : `Assigned to ${task.owner}.`,
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).toISOString().slice(0, 10);
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 ? mapTask(row, workTime.taskSummaries.get(row.id) ?? emptyTaskTimeSummary()) : undefined;
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
+ }