forge-openclaw-plugin 0.2.4 → 0.2.10

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 (114) hide show
  1. package/README.md +186 -6
  2. package/dist/assets/board-C_m78kvK.js +6 -0
  3. package/dist/assets/board-C_m78kvK.js.map +1 -0
  4. package/dist/assets/favicon-BCHm9dUV.ico +0 -0
  5. package/dist/assets/index-BWtLtXwb.js +36 -0
  6. package/dist/assets/index-BWtLtXwb.js.map +1 -0
  7. package/dist/assets/index-Dp5GXY_z.css +1 -0
  8. package/dist/assets/motion-CpZvZumD.js +10 -0
  9. package/dist/assets/motion-CpZvZumD.js.map +1 -0
  10. package/dist/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
  11. package/dist/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
  12. package/dist/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
  13. package/dist/assets/sora-latin-ext-wght-normal-CawQDOvP.woff2 +0 -0
  14. package/dist/assets/sora-latin-wght-normal-DdqRvwsR.woff2 +0 -0
  15. package/dist/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
  16. package/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
  17. package/dist/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
  18. package/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
  19. package/dist/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
  20. package/dist/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
  21. package/dist/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
  22. package/dist/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
  23. package/dist/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
  24. package/dist/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
  25. package/dist/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
  26. package/dist/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
  27. package/dist/assets/table-DtyXTw03.js +23 -0
  28. package/dist/assets/table-DtyXTw03.js.map +1 -0
  29. package/dist/assets/ui-BXbpiKyS.js +46 -0
  30. package/dist/assets/ui-BXbpiKyS.js.map +1 -0
  31. package/dist/assets/vendor-CRS-psbw.css +1 -0
  32. package/dist/assets/vendor-QBH6qVEe.js +433 -0
  33. package/dist/assets/vendor-QBH6qVEe.js.map +1 -0
  34. package/dist/assets/viz-w-IMeueL.js +34 -0
  35. package/dist/assets/viz-w-IMeueL.js.map +1 -0
  36. package/dist/favicon.ico +0 -0
  37. package/dist/favicon.png +0 -0
  38. package/dist/index.html +29 -0
  39. package/dist/openclaw/api-client.d.ts +9 -0
  40. package/dist/openclaw/api-client.js +31 -4
  41. package/dist/openclaw/local-runtime.d.ts +3 -0
  42. package/dist/openclaw/local-runtime.js +136 -0
  43. package/dist/openclaw/parity.d.ts +4 -4
  44. package/dist/openclaw/parity.js +23 -33
  45. package/dist/openclaw/plugin-entry-shared.d.ts +4 -2
  46. package/dist/openclaw/plugin-entry-shared.js +63 -9
  47. package/dist/openclaw/routes.d.ts +12 -3
  48. package/dist/openclaw/routes.js +156 -924
  49. package/dist/openclaw/tools.js +242 -1100
  50. package/dist/server/app.js +2487 -0
  51. package/dist/server/db.js +313 -0
  52. package/dist/server/demo-data.js +49 -0
  53. package/dist/server/e2e-server.js +20 -0
  54. package/dist/server/errors.js +15 -0
  55. package/dist/server/index.js +16 -0
  56. package/dist/server/managers/base.js +17 -0
  57. package/dist/server/managers/contracts.js +47 -0
  58. package/dist/server/managers/platform/api-gateway-manager.js +11 -0
  59. package/dist/server/managers/platform/audit-manager.js +15 -0
  60. package/dist/server/managers/platform/authentication-manager.js +56 -0
  61. package/dist/server/managers/platform/authorization-manager.js +56 -0
  62. package/dist/server/managers/platform/background-job-manager.js +10 -0
  63. package/dist/server/managers/platform/configuration-manager.js +33 -0
  64. package/dist/server/managers/platform/database-manager.js +14 -0
  65. package/dist/server/managers/platform/event-bus-manager.js +7 -0
  66. package/dist/server/managers/platform/external-service-manager.js +11 -0
  67. package/dist/server/managers/platform/health-manager.js +7 -0
  68. package/dist/server/managers/platform/migration-manager.js +8 -0
  69. package/dist/server/managers/platform/search-index-manager.js +4 -0
  70. package/dist/server/managers/platform/secrets-manager.js +19 -0
  71. package/dist/server/managers/platform/session-manager.js +121 -0
  72. package/dist/server/managers/platform/storage-manager.js +16 -0
  73. package/dist/server/managers/platform/token-manager.js +37 -0
  74. package/dist/server/managers/platform/transaction-manager.js +8 -0
  75. package/dist/server/managers/platform/trusted-network.js +39 -0
  76. package/dist/server/managers/runtime.js +56 -0
  77. package/dist/server/managers/type-guards.js +4 -0
  78. package/dist/server/openapi.js +3553 -0
  79. package/dist/server/psyche-types.js +366 -0
  80. package/dist/server/repositories/activity-events.js +157 -0
  81. package/dist/server/repositories/collaboration.js +497 -0
  82. package/dist/server/repositories/deleted-entities.js +226 -0
  83. package/dist/server/repositories/domains.js +30 -0
  84. package/dist/server/repositories/event-log.js +64 -0
  85. package/dist/server/repositories/goals.js +156 -0
  86. package/dist/server/repositories/notes.js +359 -0
  87. package/dist/server/repositories/projects.js +211 -0
  88. package/dist/server/repositories/psyche.js +1353 -0
  89. package/dist/server/repositories/rewards.js +675 -0
  90. package/dist/server/repositories/settings.js +399 -0
  91. package/dist/server/repositories/tags.js +160 -0
  92. package/dist/server/repositories/task-runs.js +490 -0
  93. package/dist/server/repositories/tasks.js +424 -0
  94. package/dist/server/seed-demo.js +11 -0
  95. package/dist/server/services/context.js +214 -0
  96. package/dist/server/services/dashboard.js +173 -0
  97. package/dist/server/services/entity-crud.js +573 -0
  98. package/dist/server/services/gamification.js +215 -0
  99. package/dist/server/services/insights.js +91 -0
  100. package/dist/server/services/projects.js +77 -0
  101. package/dist/server/services/psyche.js +63 -0
  102. package/dist/server/services/relations.js +28 -0
  103. package/dist/server/services/reviews.js +88 -0
  104. package/dist/server/services/run-recovery.js +13 -0
  105. package/dist/server/services/tagging.js +49 -0
  106. package/dist/server/services/task-run-watchdog.js +92 -0
  107. package/dist/server/services/work-time.js +176 -0
  108. package/dist/server/types.js +1058 -0
  109. package/dist/server/web.js +91 -0
  110. package/openclaw.plugin.json +32 -9
  111. package/package.json +17 -4
  112. package/server/migrations/001_core.sql +411 -0
  113. package/server/migrations/002_psyche.sql +392 -0
  114. package/skills/forge-openclaw/SKILL.md +197 -271
@@ -0,0 +1,226 @@
1
+ import { getDatabase } from "../db.js";
2
+ import { activitySourceSchema, crudEntityTypeSchema, deletedEntityRecordSchema, settingsBinPayloadSchema } from "../types.js";
3
+ function mapDeletedEntity(row) {
4
+ return deletedEntityRecordSchema.parse({
5
+ entityType: row.entity_type,
6
+ entityId: row.entity_id,
7
+ title: row.title,
8
+ subtitle: row.subtitle,
9
+ deletedAt: row.deleted_at,
10
+ deletedByActor: row.deleted_by_actor,
11
+ deletedSource: row.deleted_source,
12
+ deleteReason: row.delete_reason,
13
+ snapshot: JSON.parse(row.snapshot_json)
14
+ });
15
+ }
16
+ function listDeletedEntityRows(entityType) {
17
+ if (entityType) {
18
+ return getDatabase()
19
+ .prepare(`SELECT entity_type, entity_id, title, subtitle, deleted_at, deleted_by_actor, deleted_source, delete_reason, snapshot_json
20
+ FROM deleted_entities
21
+ WHERE entity_type = ?
22
+ ORDER BY deleted_at DESC`)
23
+ .all(entityType);
24
+ }
25
+ return getDatabase()
26
+ .prepare(`SELECT entity_type, entity_id, title, subtitle, deleted_at, deleted_by_actor, deleted_source, delete_reason, snapshot_json
27
+ FROM deleted_entities
28
+ ORDER BY deleted_at DESC`)
29
+ .all();
30
+ }
31
+ export function getDeletedEntityRecord(entityType, entityId) {
32
+ const row = getDatabase()
33
+ .prepare(`SELECT entity_type, entity_id, title, subtitle, deleted_at, deleted_by_actor, deleted_source, delete_reason, snapshot_json
34
+ FROM deleted_entities
35
+ WHERE entity_type = ? AND entity_id = ?`)
36
+ .get(entityType, entityId);
37
+ return row ? mapDeletedEntity(row) : undefined;
38
+ }
39
+ export function listDeletedEntities() {
40
+ return listDeletedEntityRows().map(mapDeletedEntity);
41
+ }
42
+ export function getDeletedEntityIdSet(entityType) {
43
+ const rows = getDatabase()
44
+ .prepare(`SELECT entity_id FROM deleted_entities WHERE entity_type = ?`)
45
+ .all(entityType);
46
+ return new Set(rows.map((row) => row.entity_id));
47
+ }
48
+ export function isEntityDeleted(entityType, entityId) {
49
+ const row = getDatabase()
50
+ .prepare(`SELECT 1
51
+ FROM deleted_entities
52
+ WHERE entity_type = ? AND entity_id = ?
53
+ LIMIT 1`)
54
+ .get(entityType, entityId);
55
+ return Boolean(row);
56
+ }
57
+ export function filterDeletedEntities(entityType, items) {
58
+ if (items.length === 0) {
59
+ return items;
60
+ }
61
+ const deletedIds = getDeletedEntityIdSet(entityType);
62
+ if (deletedIds.size === 0) {
63
+ return items;
64
+ }
65
+ return items.filter((item) => !deletedIds.has(item.id));
66
+ }
67
+ export function filterDeletedIds(entityType, ids) {
68
+ if (ids.length === 0) {
69
+ return ids;
70
+ }
71
+ const deletedIds = getDeletedEntityIdSet(entityType);
72
+ if (deletedIds.size === 0) {
73
+ return ids;
74
+ }
75
+ return ids.filter((id) => !deletedIds.has(id));
76
+ }
77
+ export function upsertDeletedEntityRecord(input) {
78
+ const entityType = crudEntityTypeSchema.parse(input.entityType);
79
+ const deletedAt = new Date().toISOString();
80
+ getDatabase()
81
+ .prepare(`INSERT INTO deleted_entities (
82
+ entity_type, entity_id, title, subtitle, deleted_at, deleted_by_actor, deleted_source, delete_reason, snapshot_json
83
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
84
+ ON CONFLICT(entity_type, entity_id) DO UPDATE SET
85
+ title = excluded.title,
86
+ subtitle = excluded.subtitle,
87
+ deleted_at = excluded.deleted_at,
88
+ deleted_by_actor = excluded.deleted_by_actor,
89
+ deleted_source = excluded.deleted_source,
90
+ delete_reason = excluded.delete_reason,
91
+ snapshot_json = excluded.snapshot_json`)
92
+ .run(entityType, input.entityId, input.title, input.subtitle ?? "", deletedAt, input.context.actor ?? null, activitySourceSchema.parse(input.context.source), input.deleteReason ?? "", JSON.stringify(input.snapshot));
93
+ }
94
+ export function restoreDeletedEntityRecord(entityType, entityId) {
95
+ const existing = getDeletedEntityRecord(entityType, entityId);
96
+ if (!existing) {
97
+ return undefined;
98
+ }
99
+ getDatabase()
100
+ .prepare(`DELETE FROM deleted_entities WHERE entity_type = ? AND entity_id = ?`)
101
+ .run(entityType, entityId);
102
+ return existing;
103
+ }
104
+ export function clearDeletedEntityRecord(entityType, entityId) {
105
+ getDatabase()
106
+ .prepare(`DELETE FROM deleted_entities WHERE entity_type = ? AND entity_id = ?`)
107
+ .run(entityType, entityId);
108
+ }
109
+ export function cascadeSoftDeleteAnchoredCollaboration(parentEntityType, parentEntityId, context, deleteReason = "") {
110
+ const noteRows = getDatabase()
111
+ .prepare(`SELECT DISTINCT
112
+ notes.id AS id,
113
+ notes.content_markdown AS content_markdown,
114
+ notes.content_plain AS content_plain,
115
+ notes.author AS author,
116
+ notes.source AS source,
117
+ notes.created_at AS created_at,
118
+ notes.updated_at AS updated_at
119
+ FROM notes
120
+ INNER JOIN note_links
121
+ ON note_links.note_id = notes.id
122
+ LEFT JOIN deleted_entities
123
+ ON deleted_entities.entity_type = 'note'
124
+ AND deleted_entities.entity_id = notes.id
125
+ WHERE note_links.entity_type = ?
126
+ AND note_links.entity_id = ?
127
+ AND deleted_entities.entity_id IS NULL`)
128
+ .all(parentEntityType, parentEntityId);
129
+ if (noteRows.length > 0) {
130
+ const placeholders = noteRows.map(() => "?").join(", ");
131
+ const linkRows = getDatabase()
132
+ .prepare(`SELECT note_id, entity_type, entity_id, anchor_key
133
+ FROM note_links
134
+ WHERE note_id IN (${placeholders})
135
+ ORDER BY created_at ASC`)
136
+ .all(...noteRows.map((row) => row.id));
137
+ const linksByNoteId = new Map();
138
+ for (const link of linkRows) {
139
+ const current = linksByNoteId.get(link.note_id) ?? [];
140
+ current.push({
141
+ entityType: link.entity_type,
142
+ entityId: link.entity_id,
143
+ anchorKey: link.anchor_key.trim().length > 0 ? link.anchor_key : null
144
+ });
145
+ linksByNoteId.set(link.note_id, current);
146
+ }
147
+ for (const row of noteRows) {
148
+ const compact = (row.content_plain || row.content_markdown).replace(/\s+/g, " ").trim();
149
+ upsertDeletedEntityRecord({
150
+ entityType: "note",
151
+ entityId: row.id,
152
+ title: compact.slice(0, 72) || "Note",
153
+ subtitle: compact.length > 72 ? compact.slice(72, 168).trim() : `Linked to ${parentEntityType.replaceAll("_", " ")}`,
154
+ snapshot: {
155
+ id: row.id,
156
+ contentMarkdown: row.content_markdown,
157
+ contentPlain: row.content_plain,
158
+ author: row.author,
159
+ source: row.source,
160
+ createdAt: row.created_at,
161
+ updatedAt: row.updated_at,
162
+ links: linksByNoteId.get(row.id) ?? []
163
+ },
164
+ deleteReason,
165
+ context
166
+ });
167
+ }
168
+ }
169
+ const insightRows = getDatabase()
170
+ .prepare(`SELECT id, title, summary, created_at, updated_at
171
+ FROM insights
172
+ WHERE entity_type = ? AND entity_id = ?`)
173
+ .all(parentEntityType, parentEntityId);
174
+ for (const row of insightRows) {
175
+ upsertDeletedEntityRecord({
176
+ entityType: "insight",
177
+ entityId: row.id,
178
+ title: row.title,
179
+ subtitle: row.summary,
180
+ snapshot: {
181
+ id: row.id,
182
+ entityType: parentEntityType,
183
+ entityId: parentEntityId,
184
+ title: row.title,
185
+ summary: row.summary,
186
+ createdAt: row.created_at,
187
+ updatedAt: row.updated_at
188
+ },
189
+ deleteReason,
190
+ context
191
+ });
192
+ }
193
+ }
194
+ export function restoreAnchoredCollaboration(parentEntityType, parentEntityId) {
195
+ getDatabase()
196
+ .prepare(`DELETE FROM deleted_entities
197
+ WHERE entity_type = 'note'
198
+ AND entity_id IN (
199
+ SELECT DISTINCT note_id
200
+ FROM note_links
201
+ WHERE entity_type = ? AND entity_id = ?
202
+ )`)
203
+ .run(parentEntityType, parentEntityId);
204
+ getDatabase()
205
+ .prepare(`DELETE FROM deleted_entities
206
+ WHERE entity_type = 'insight'
207
+ AND entity_id IN (
208
+ SELECT id
209
+ FROM insights
210
+ WHERE entity_type = ? AND entity_id = ?
211
+ )`)
212
+ .run(parentEntityType, parentEntityId);
213
+ }
214
+ export function buildSettingsBinPayload() {
215
+ const items = listDeletedEntities();
216
+ const counts = items.reduce((acc, item) => {
217
+ acc[item.entityType] = (acc[item.entityType] ?? 0) + 1;
218
+ return acc;
219
+ }, {});
220
+ return settingsBinPayloadSchema.parse({
221
+ generatedAt: new Date().toISOString(),
222
+ totalCount: items.length,
223
+ countsByEntityType: counts,
224
+ records: items
225
+ });
226
+ }
@@ -0,0 +1,30 @@
1
+ import { getDatabase } from "../db.js";
2
+ import { domainSchema } from "../psyche-types.js";
3
+ function mapDomain(row) {
4
+ return domainSchema.parse({
5
+ id: row.id,
6
+ slug: row.slug,
7
+ title: row.title,
8
+ description: row.description,
9
+ themeColor: row.theme_color,
10
+ sensitive: row.sensitive === 1,
11
+ createdAt: row.created_at,
12
+ updatedAt: row.updated_at
13
+ });
14
+ }
15
+ export function listDomains() {
16
+ const rows = getDatabase()
17
+ .prepare(`SELECT id, slug, title, description, theme_color, sensitive, created_at, updated_at
18
+ FROM domains
19
+ ORDER BY sensitive DESC, title`)
20
+ .all();
21
+ return rows.map(mapDomain);
22
+ }
23
+ export function getDomainBySlug(slug) {
24
+ const row = getDatabase()
25
+ .prepare(`SELECT id, slug, title, description, theme_color, sensitive, created_at, updated_at
26
+ FROM domains
27
+ WHERE slug = ?`)
28
+ .get(slug);
29
+ return row ? mapDomain(row) : undefined;
30
+ }
@@ -0,0 +1,64 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDatabase } from "../db.js";
3
+ import { eventLogEntrySchema } from "../types.js";
4
+ function mapEvent(row) {
5
+ return eventLogEntrySchema.parse({
6
+ id: row.id,
7
+ eventKind: row.event_kind,
8
+ entityType: row.entity_type,
9
+ entityId: row.entity_id,
10
+ actor: row.actor,
11
+ source: row.source,
12
+ causedByEventId: row.caused_by_event_id,
13
+ metadata: JSON.parse(row.metadata_json),
14
+ createdAt: row.created_at
15
+ });
16
+ }
17
+ export function recordEventLog(input, now = new Date()) {
18
+ const event = eventLogEntrySchema.parse({
19
+ id: `log_${randomUUID().replaceAll("-", "").slice(0, 10)}`,
20
+ eventKind: input.eventKind,
21
+ entityType: input.entityType,
22
+ entityId: input.entityId,
23
+ actor: input.actor ?? null,
24
+ source: input.source,
25
+ causedByEventId: input.causedByEventId ?? null,
26
+ metadata: input.metadata ?? {},
27
+ createdAt: now.toISOString()
28
+ });
29
+ getDatabase()
30
+ .prepare(`INSERT INTO event_log (
31
+ id, event_kind, entity_type, entity_id, actor, source, caused_by_event_id, metadata_json, created_at
32
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
33
+ .run(event.id, event.eventKind, event.entityType, event.entityId, event.actor, event.source, event.causedByEventId, JSON.stringify(event.metadata), event.createdAt);
34
+ return event;
35
+ }
36
+ export function listEventLog(filters = {}) {
37
+ const whereClauses = [];
38
+ const params = [];
39
+ if (filters.entityType) {
40
+ whereClauses.push("entity_type = ?");
41
+ params.push(filters.entityType);
42
+ }
43
+ if (filters.entityId) {
44
+ whereClauses.push("entity_id = ?");
45
+ params.push(filters.entityId);
46
+ }
47
+ if (filters.eventKind) {
48
+ whereClauses.push("event_kind = ?");
49
+ params.push(filters.eventKind);
50
+ }
51
+ const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
52
+ const limitSql = filters.limit ? "LIMIT ?" : "";
53
+ if (filters.limit) {
54
+ params.push(filters.limit);
55
+ }
56
+ const rows = getDatabase()
57
+ .prepare(`SELECT id, event_kind, entity_type, entity_id, actor, source, caused_by_event_id, metadata_json, created_at
58
+ FROM event_log
59
+ ${whereSql}
60
+ ORDER BY created_at DESC
61
+ ${limitSql}`)
62
+ .all(...params);
63
+ return rows.map(mapEvent);
64
+ }
@@ -0,0 +1,156 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDatabase, runInTransaction } from "../db.js";
3
+ import { recordActivityEvent } from "./activity-events.js";
4
+ import { filterDeletedEntities, filterDeletedIds, isEntityDeleted } from "./deleted-entities.js";
5
+ import { createLinkedNotes } from "./notes.js";
6
+ import { assertGoalRelations } from "../services/relations.js";
7
+ import { pruneLinkedEntityReferences } from "./psyche.js";
8
+ import { goalSchema } from "../types.js";
9
+ function readGoalTagIds(goalId) {
10
+ const rows = getDatabase()
11
+ .prepare(`SELECT tag_id FROM goal_tags WHERE goal_id = ? ORDER BY tag_id`)
12
+ .all(goalId);
13
+ return filterDeletedIds("tag", rows.map((row) => row.tag_id));
14
+ }
15
+ function mapGoal(row) {
16
+ return goalSchema.parse({
17
+ id: row.id,
18
+ title: row.title,
19
+ description: row.description,
20
+ horizon: row.horizon,
21
+ status: row.status,
22
+ targetPoints: row.target_points,
23
+ themeColor: row.theme_color,
24
+ createdAt: row.created_at,
25
+ updatedAt: row.updated_at,
26
+ tagIds: readGoalTagIds(row.id)
27
+ });
28
+ }
29
+ function replaceGoalTags(goalId, tagIds) {
30
+ const database = getDatabase();
31
+ database.prepare(`DELETE FROM goal_tags WHERE goal_id = ?`).run(goalId);
32
+ const insert = database.prepare(`INSERT INTO goal_tags (goal_id, tag_id) VALUES (?, ?)`);
33
+ for (const tagId of tagIds) {
34
+ insert.run(goalId, tagId);
35
+ }
36
+ }
37
+ export function listGoals() {
38
+ const rows = getDatabase()
39
+ .prepare(`SELECT id, title, description, horizon, status, target_points, theme_color, created_at, updated_at
40
+ FROM goals
41
+ ORDER BY created_at`)
42
+ .all();
43
+ return filterDeletedEntities("goal", rows.map(mapGoal));
44
+ }
45
+ export function createGoal(input, activity) {
46
+ return runInTransaction(() => {
47
+ assertGoalRelations(input);
48
+ const now = new Date().toISOString();
49
+ const id = `goal_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
50
+ getDatabase()
51
+ .prepare(`INSERT INTO goals (id, title, description, horizon, status, target_points, theme_color, created_at, updated_at)
52
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
53
+ .run(id, input.title, input.description, input.horizon, input.status, input.targetPoints, input.themeColor, now, now);
54
+ replaceGoalTags(id, input.tagIds);
55
+ const goal = getGoalById(id);
56
+ createLinkedNotes(input.notes, { entityType: "goal", entityId: goal.id, anchorKey: null }, activity ?? { source: "ui", actor: null });
57
+ if (activity) {
58
+ recordActivityEvent({
59
+ entityType: "goal",
60
+ entityId: goal.id,
61
+ eventType: "goal_created",
62
+ title: `Goal created: ${goal.title}`,
63
+ description: `Target set to ${goal.targetPoints} points.`,
64
+ actor: activity.actor ?? null,
65
+ source: activity.source,
66
+ metadata: {
67
+ horizon: goal.horizon,
68
+ status: goal.status,
69
+ targetPoints: goal.targetPoints
70
+ }
71
+ });
72
+ }
73
+ return goal;
74
+ });
75
+ }
76
+ export function updateGoal(goalId, input, activity) {
77
+ const current = getGoalById(goalId);
78
+ if (!current) {
79
+ return undefined;
80
+ }
81
+ return runInTransaction(() => {
82
+ const next = {
83
+ ...current,
84
+ ...input,
85
+ updatedAt: new Date().toISOString(),
86
+ tagIds: input.tagIds ?? current.tagIds
87
+ };
88
+ assertGoalRelations(next);
89
+ getDatabase()
90
+ .prepare(`UPDATE goals
91
+ SET title = ?, description = ?, horizon = ?, status = ?, target_points = ?, theme_color = ?, updated_at = ?
92
+ WHERE id = ?`)
93
+ .run(next.title, next.description, next.horizon, next.status, next.targetPoints, next.themeColor, next.updatedAt, goalId);
94
+ replaceGoalTags(goalId, next.tagIds);
95
+ const goal = getGoalById(goalId);
96
+ if (goal && activity) {
97
+ const statusChanged = current.status !== goal.status;
98
+ recordActivityEvent({
99
+ entityType: "goal",
100
+ entityId: goal.id,
101
+ eventType: statusChanged ? "goal_status_changed" : "goal_updated",
102
+ title: statusChanged ? `Goal ${goal.status}: ${goal.title}` : `Goal updated: ${goal.title}`,
103
+ description: statusChanged ? `Goal status moved from ${current.status} to ${goal.status}.` : "Goal details were edited.",
104
+ actor: activity.actor ?? null,
105
+ source: activity.source,
106
+ metadata: {
107
+ previousStatus: current.status,
108
+ status: goal.status,
109
+ targetPoints: goal.targetPoints,
110
+ previousTargetPoints: current.targetPoints
111
+ }
112
+ });
113
+ }
114
+ return goal;
115
+ });
116
+ }
117
+ export function getGoalById(goalId) {
118
+ if (isEntityDeleted("goal", goalId)) {
119
+ return undefined;
120
+ }
121
+ const row = getDatabase()
122
+ .prepare(`SELECT id, title, description, horizon, status, target_points, theme_color, created_at, updated_at
123
+ FROM goals
124
+ WHERE id = ?`)
125
+ .get(goalId);
126
+ return row ? mapGoal(row) : undefined;
127
+ }
128
+ export function deleteGoal(goalId, activity) {
129
+ const current = getGoalById(goalId);
130
+ if (!current) {
131
+ return undefined;
132
+ }
133
+ return runInTransaction(() => {
134
+ pruneLinkedEntityReferences("goal", goalId);
135
+ getDatabase()
136
+ .prepare(`DELETE FROM goals WHERE id = ?`)
137
+ .run(goalId);
138
+ if (activity) {
139
+ recordActivityEvent({
140
+ entityType: "goal",
141
+ entityId: current.id,
142
+ eventType: "goal_deleted",
143
+ title: `Goal deleted: ${current.title}`,
144
+ description: "Goal removed from the system.",
145
+ actor: activity.actor ?? null,
146
+ source: activity.source,
147
+ metadata: {
148
+ horizon: current.horizon,
149
+ status: current.status,
150
+ targetPoints: current.targetPoints
151
+ }
152
+ });
153
+ }
154
+ return current;
155
+ });
156
+ }