forge-openclaw-plugin 0.2.3 → 0.2.7

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 (117) hide show
  1. package/README.md +114 -6
  2. package/dist/assets/board-CzgvdLO8.js +6 -0
  3. package/dist/assets/board-CzgvdLO8.js.map +1 -0
  4. package/dist/assets/favicon-BCHm9dUV.ico +0 -0
  5. package/dist/assets/index-8d_oM8fL.js +27 -0
  6. package/dist/assets/index-8d_oM8fL.js.map +1 -0
  7. package/dist/assets/index-D4A_bq8m.css +1 -0
  8. package/dist/assets/motion-STUd1O46.js +10 -0
  9. package/dist/assets/motion-STUd1O46.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-CtNlETLc.js +23 -0
  28. package/dist/assets/table-CtNlETLc.js.map +1 -0
  29. package/dist/assets/ui-ThzkR_oW.js +46 -0
  30. package/dist/assets/ui-ThzkR_oW.js.map +1 -0
  31. package/dist/assets/vendor-CRS-psbw.css +1 -0
  32. package/dist/assets/vendor-DyHAI6nk.js +423 -0
  33. package/dist/assets/vendor-DyHAI6nk.js.map +1 -0
  34. package/dist/assets/viz-BJuBCz_G.js +34 -0
  35. package/dist/assets/viz-BJuBCz_G.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 +8 -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 +135 -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 +5 -3
  46. package/dist/openclaw/plugin-entry-shared.js +52 -10
  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 +2450 -0
  51. package/dist/server/db.js +313 -0
  52. package/dist/server/e2e-server.js +20 -0
  53. package/dist/server/errors.js +15 -0
  54. package/dist/server/index.js +16 -0
  55. package/dist/server/managers/base.js +17 -0
  56. package/dist/server/managers/contracts.js +47 -0
  57. package/dist/server/managers/platform/api-gateway-manager.js +11 -0
  58. package/dist/server/managers/platform/audit-manager.js +15 -0
  59. package/dist/server/managers/platform/authentication-manager.js +56 -0
  60. package/dist/server/managers/platform/authorization-manager.js +56 -0
  61. package/dist/server/managers/platform/background-job-manager.js +10 -0
  62. package/dist/server/managers/platform/configuration-manager.js +33 -0
  63. package/dist/server/managers/platform/database-manager.js +14 -0
  64. package/dist/server/managers/platform/event-bus-manager.js +7 -0
  65. package/dist/server/managers/platform/external-service-manager.js +11 -0
  66. package/dist/server/managers/platform/health-manager.js +7 -0
  67. package/dist/server/managers/platform/migration-manager.js +8 -0
  68. package/dist/server/managers/platform/search-index-manager.js +4 -0
  69. package/dist/server/managers/platform/secrets-manager.js +19 -0
  70. package/dist/server/managers/platform/session-manager.js +121 -0
  71. package/dist/server/managers/platform/storage-manager.js +16 -0
  72. package/dist/server/managers/platform/token-manager.js +37 -0
  73. package/dist/server/managers/platform/transaction-manager.js +8 -0
  74. package/dist/server/managers/platform/trusted-network.js +39 -0
  75. package/dist/server/managers/runtime.js +56 -0
  76. package/dist/server/managers/type-guards.js +4 -0
  77. package/dist/server/openapi.js +3512 -0
  78. package/dist/server/psyche-types.js +395 -0
  79. package/dist/server/repositories/activity-events.js +157 -0
  80. package/dist/server/repositories/collaboration.js +497 -0
  81. package/dist/server/repositories/comments.js +176 -0
  82. package/dist/server/repositories/deleted-entities.js +192 -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 +159 -0
  86. package/dist/server/repositories/projects.js +214 -0
  87. package/dist/server/repositories/psyche.js +1356 -0
  88. package/dist/server/repositories/rewards.js +675 -0
  89. package/dist/server/repositories/settings.js +399 -0
  90. package/dist/server/repositories/tags.js +160 -0
  91. package/dist/server/repositories/task-runs.js +488 -0
  92. package/dist/server/repositories/tasks.js +413 -0
  93. package/dist/server/services/context.js +214 -0
  94. package/dist/server/services/dashboard.js +170 -0
  95. package/dist/server/services/entity-crud.js +576 -0
  96. package/dist/server/services/gamification.js +215 -0
  97. package/dist/server/services/insights.js +91 -0
  98. package/dist/server/services/projects.js +75 -0
  99. package/dist/server/services/psyche.js +63 -0
  100. package/dist/server/services/relations.js +28 -0
  101. package/dist/server/services/reviews.js +88 -0
  102. package/dist/server/services/run-recovery.js +13 -0
  103. package/dist/server/services/tagging.js +49 -0
  104. package/dist/server/services/task-run-watchdog.js +92 -0
  105. package/dist/server/services/work-time.js +176 -0
  106. package/dist/server/types.js +999 -0
  107. package/dist/server/web.js +91 -0
  108. package/openclaw.plugin.json +22 -10
  109. package/package.json +17 -4
  110. package/server/migrations/001_core.sql +333 -0
  111. package/server/migrations/002_psyche.sql +241 -0
  112. package/server/migrations/003_timer_execution.sql +18 -0
  113. package/server/migrations/004_psyche_linked_entities.sql +5 -0
  114. package/server/migrations/005_adaptive_schemas.sql +157 -0
  115. package/server/migrations/006_psyche_auth_setting.sql +4 -0
  116. package/server/migrations/007_deleted_entities.sql +16 -0
  117. package/skills/forge-openclaw/SKILL.md +189 -275
@@ -0,0 +1,192 @@
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 commentRows = getDatabase()
111
+ .prepare(`SELECT id, body, author, source, created_at, updated_at
112
+ FROM entity_comments
113
+ WHERE entity_type = ? AND entity_id = ?`)
114
+ .all(parentEntityType, parentEntityId);
115
+ for (const row of commentRows) {
116
+ upsertDeletedEntityRecord({
117
+ entityType: "comment",
118
+ entityId: row.id,
119
+ title: row.body.slice(0, 72) || "Comment",
120
+ subtitle: `Comment on ${parentEntityType.replaceAll("_", " ")}`,
121
+ snapshot: {
122
+ id: row.id,
123
+ entityType: parentEntityType,
124
+ entityId: parentEntityId,
125
+ body: row.body,
126
+ author: row.author,
127
+ source: row.source,
128
+ createdAt: row.created_at,
129
+ updatedAt: row.updated_at
130
+ },
131
+ deleteReason,
132
+ context
133
+ });
134
+ }
135
+ const insightRows = getDatabase()
136
+ .prepare(`SELECT id, title, summary, created_at, updated_at
137
+ FROM insights
138
+ WHERE entity_type = ? AND entity_id = ?`)
139
+ .all(parentEntityType, parentEntityId);
140
+ for (const row of insightRows) {
141
+ upsertDeletedEntityRecord({
142
+ entityType: "insight",
143
+ entityId: row.id,
144
+ title: row.title,
145
+ subtitle: row.summary,
146
+ snapshot: {
147
+ id: row.id,
148
+ entityType: parentEntityType,
149
+ entityId: parentEntityId,
150
+ title: row.title,
151
+ summary: row.summary,
152
+ createdAt: row.created_at,
153
+ updatedAt: row.updated_at
154
+ },
155
+ deleteReason,
156
+ context
157
+ });
158
+ }
159
+ }
160
+ export function restoreAnchoredCollaboration(parentEntityType, parentEntityId) {
161
+ getDatabase()
162
+ .prepare(`DELETE FROM deleted_entities
163
+ WHERE entity_type = 'comment'
164
+ AND entity_id IN (
165
+ SELECT id
166
+ FROM entity_comments
167
+ WHERE entity_type = ? AND entity_id = ?
168
+ )`)
169
+ .run(parentEntityType, parentEntityId);
170
+ getDatabase()
171
+ .prepare(`DELETE FROM deleted_entities
172
+ WHERE entity_type = 'insight'
173
+ AND entity_id IN (
174
+ SELECT id
175
+ FROM insights
176
+ WHERE entity_type = ? AND entity_id = ?
177
+ )`)
178
+ .run(parentEntityType, parentEntityId);
179
+ }
180
+ export function buildSettingsBinPayload() {
181
+ const items = listDeletedEntities();
182
+ const counts = items.reduce((acc, item) => {
183
+ acc[item.entityType] = (acc[item.entityType] ?? 0) + 1;
184
+ return acc;
185
+ }, {});
186
+ return settingsBinPayloadSchema.parse({
187
+ generatedAt: new Date().toISOString(),
188
+ totalCount: items.length,
189
+ countsByEntityType: counts,
190
+ records: items
191
+ });
192
+ }
@@ -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,159 @@
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 { assertGoalRelations } from "../services/relations.js";
6
+ import { pruneLinkedEntityReferences } from "./psyche.js";
7
+ import { goalSchema } from "../types.js";
8
+ function readGoalTagIds(goalId) {
9
+ const rows = getDatabase()
10
+ .prepare(`SELECT tag_id FROM goal_tags WHERE goal_id = ? ORDER BY tag_id`)
11
+ .all(goalId);
12
+ return filterDeletedIds("tag", rows.map((row) => row.tag_id));
13
+ }
14
+ function mapGoal(row) {
15
+ return goalSchema.parse({
16
+ id: row.id,
17
+ title: row.title,
18
+ description: row.description,
19
+ horizon: row.horizon,
20
+ status: row.status,
21
+ targetPoints: row.target_points,
22
+ themeColor: row.theme_color,
23
+ createdAt: row.created_at,
24
+ updatedAt: row.updated_at,
25
+ tagIds: readGoalTagIds(row.id)
26
+ });
27
+ }
28
+ function replaceGoalTags(goalId, tagIds) {
29
+ const database = getDatabase();
30
+ database.prepare(`DELETE FROM goal_tags WHERE goal_id = ?`).run(goalId);
31
+ const insert = database.prepare(`INSERT INTO goal_tags (goal_id, tag_id) VALUES (?, ?)`);
32
+ for (const tagId of tagIds) {
33
+ insert.run(goalId, tagId);
34
+ }
35
+ }
36
+ export function listGoals() {
37
+ const rows = getDatabase()
38
+ .prepare(`SELECT id, title, description, horizon, status, target_points, theme_color, created_at, updated_at
39
+ FROM goals
40
+ ORDER BY created_at`)
41
+ .all();
42
+ return filterDeletedEntities("goal", rows.map(mapGoal));
43
+ }
44
+ export function createGoal(input, activity) {
45
+ return runInTransaction(() => {
46
+ assertGoalRelations(input);
47
+ const now = new Date().toISOString();
48
+ const id = `goal_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
49
+ getDatabase()
50
+ .prepare(`INSERT INTO goals (id, title, description, horizon, status, target_points, theme_color, created_at, updated_at)
51
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
52
+ .run(id, input.title, input.description, input.horizon, input.status, input.targetPoints, input.themeColor, now, now);
53
+ replaceGoalTags(id, input.tagIds);
54
+ const goal = getGoalById(id);
55
+ if (activity) {
56
+ recordActivityEvent({
57
+ entityType: "goal",
58
+ entityId: goal.id,
59
+ eventType: "goal_created",
60
+ title: `Goal created: ${goal.title}`,
61
+ description: `Target set to ${goal.targetPoints} points.`,
62
+ actor: activity.actor ?? null,
63
+ source: activity.source,
64
+ metadata: {
65
+ horizon: goal.horizon,
66
+ status: goal.status,
67
+ targetPoints: goal.targetPoints
68
+ }
69
+ });
70
+ }
71
+ return goal;
72
+ });
73
+ }
74
+ export function updateGoal(goalId, input, activity) {
75
+ const current = getGoalById(goalId);
76
+ if (!current) {
77
+ return undefined;
78
+ }
79
+ return runInTransaction(() => {
80
+ const next = {
81
+ ...current,
82
+ ...input,
83
+ updatedAt: new Date().toISOString(),
84
+ tagIds: input.tagIds ?? current.tagIds
85
+ };
86
+ assertGoalRelations(next);
87
+ getDatabase()
88
+ .prepare(`UPDATE goals
89
+ SET title = ?, description = ?, horizon = ?, status = ?, target_points = ?, theme_color = ?, updated_at = ?
90
+ WHERE id = ?`)
91
+ .run(next.title, next.description, next.horizon, next.status, next.targetPoints, next.themeColor, next.updatedAt, goalId);
92
+ replaceGoalTags(goalId, next.tagIds);
93
+ const goal = getGoalById(goalId);
94
+ if (goal && activity) {
95
+ const statusChanged = current.status !== goal.status;
96
+ recordActivityEvent({
97
+ entityType: "goal",
98
+ entityId: goal.id,
99
+ eventType: statusChanged ? "goal_status_changed" : "goal_updated",
100
+ title: statusChanged ? `Goal ${goal.status}: ${goal.title}` : `Goal updated: ${goal.title}`,
101
+ description: statusChanged ? `Goal status moved from ${current.status} to ${goal.status}.` : "Goal details were edited.",
102
+ actor: activity.actor ?? null,
103
+ source: activity.source,
104
+ metadata: {
105
+ previousStatus: current.status,
106
+ status: goal.status,
107
+ targetPoints: goal.targetPoints,
108
+ previousTargetPoints: current.targetPoints
109
+ }
110
+ });
111
+ }
112
+ return goal;
113
+ });
114
+ }
115
+ export function getGoalById(goalId) {
116
+ if (isEntityDeleted("goal", goalId)) {
117
+ return undefined;
118
+ }
119
+ const row = getDatabase()
120
+ .prepare(`SELECT id, title, description, horizon, status, target_points, theme_color, created_at, updated_at
121
+ FROM goals
122
+ WHERE id = ?`)
123
+ .get(goalId);
124
+ return row ? mapGoal(row) : undefined;
125
+ }
126
+ export function deleteGoal(goalId, activity) {
127
+ const current = getGoalById(goalId);
128
+ if (!current) {
129
+ return undefined;
130
+ }
131
+ return runInTransaction(() => {
132
+ pruneLinkedEntityReferences("goal", goalId);
133
+ getDatabase()
134
+ .prepare(`DELETE FROM entity_comments
135
+ WHERE entity_type = 'goal'
136
+ AND entity_id = ?`)
137
+ .run(goalId);
138
+ getDatabase()
139
+ .prepare(`DELETE FROM goals WHERE id = ?`)
140
+ .run(goalId);
141
+ if (activity) {
142
+ recordActivityEvent({
143
+ entityType: "goal",
144
+ entityId: current.id,
145
+ eventType: "goal_deleted",
146
+ title: `Goal deleted: ${current.title}`,
147
+ description: "Goal removed from the system.",
148
+ actor: activity.actor ?? null,
149
+ source: activity.source,
150
+ metadata: {
151
+ horizon: current.horizon,
152
+ status: current.status,
153
+ targetPoints: current.targetPoints
154
+ }
155
+ });
156
+ }
157
+ return current;
158
+ });
159
+ }
@@ -0,0 +1,214 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDatabase, runInTransaction } from "../db.js";
3
+ import { recordActivityEvent } from "./activity-events.js";
4
+ import { filterDeletedEntities, isEntityDeleted } from "./deleted-entities.js";
5
+ import { assertGoalExists } from "../services/relations.js";
6
+ import { getGoalById } from "./goals.js";
7
+ import { pruneLinkedEntityReferences } from "./psyche.js";
8
+ import { createProjectSchema, projectSchema, updateProjectSchema } from "../types.js";
9
+ function getDefaultProjectTemplate(goal) {
10
+ switch (goal.title) {
11
+ case "Build a durable body and calm energy":
12
+ return {
13
+ title: "Energy Foundation Sprint",
14
+ description: "Build the routines, scheduling, and recovery rhythm that make consistent physical energy possible."
15
+ };
16
+ case "Ship meaningful creative work every week":
17
+ return {
18
+ title: "Weekly Creative Shipping System",
19
+ description: "Create a repeatable system for deep work, reviews, and visible weekly output."
20
+ };
21
+ case "Strengthen shared life systems":
22
+ return {
23
+ title: "Shared Life Admin Reset",
24
+ description: "Reduce friction in logistics, planning, and recurring obligations that support shared life."
25
+ };
26
+ default:
27
+ return {
28
+ title: `${goal.title}: Active Project`,
29
+ description: "Concrete workstream under this life goal so tasks, evidence, and progress have a clear home."
30
+ };
31
+ }
32
+ }
33
+ function mapProject(row) {
34
+ return projectSchema.parse({
35
+ id: row.id,
36
+ goalId: row.goal_id,
37
+ title: row.title,
38
+ description: row.description,
39
+ status: row.status,
40
+ themeColor: row.theme_color,
41
+ targetPoints: row.target_points,
42
+ createdAt: row.created_at,
43
+ updatedAt: row.updated_at
44
+ });
45
+ }
46
+ export function listProjects(filters = {}) {
47
+ const whereClauses = [];
48
+ const params = [];
49
+ if (filters.goalId) {
50
+ whereClauses.push("goal_id = ?");
51
+ params.push(filters.goalId);
52
+ }
53
+ if (filters.status) {
54
+ whereClauses.push("status = ?");
55
+ params.push(filters.status);
56
+ }
57
+ const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
58
+ const limitSql = filters.limit ? "LIMIT ?" : "";
59
+ if (filters.limit) {
60
+ params.push(filters.limit);
61
+ }
62
+ const rows = getDatabase()
63
+ .prepare(`SELECT id, goal_id, title, description, status, theme_color, target_points, created_at, updated_at
64
+ FROM projects
65
+ ${whereSql}
66
+ ORDER BY created_at ASC
67
+ ${limitSql}`)
68
+ .all(...params);
69
+ return filterDeletedEntities("project", rows.map(mapProject));
70
+ }
71
+ export function getProjectById(projectId) {
72
+ if (isEntityDeleted("project", projectId)) {
73
+ return undefined;
74
+ }
75
+ const row = getDatabase()
76
+ .prepare(`SELECT id, goal_id, title, description, status, theme_color, target_points, created_at, updated_at
77
+ FROM projects
78
+ WHERE id = ?`)
79
+ .get(projectId);
80
+ return row ? mapProject(row) : undefined;
81
+ }
82
+ export function createProject(input, activity) {
83
+ return runInTransaction(() => {
84
+ const parsed = createProjectSchema.parse(input);
85
+ assertGoalExists(parsed.goalId);
86
+ const now = new Date().toISOString();
87
+ const id = `project_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
88
+ getDatabase()
89
+ .prepare(`INSERT INTO projects (id, goal_id, title, description, status, theme_color, target_points, created_at, updated_at)
90
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
91
+ .run(id, parsed.goalId, parsed.title, parsed.description, parsed.status, parsed.themeColor, parsed.targetPoints, now, now);
92
+ const project = getProjectById(id);
93
+ if (activity) {
94
+ recordActivityEvent({
95
+ entityType: "project",
96
+ entityId: project.id,
97
+ eventType: "project_created",
98
+ title: `Project created: ${project.title}`,
99
+ description: "A new path was added under a life goal.",
100
+ actor: activity.actor ?? null,
101
+ source: activity.source,
102
+ metadata: {
103
+ goalId: project.goalId,
104
+ status: project.status,
105
+ targetPoints: project.targetPoints
106
+ }
107
+ });
108
+ }
109
+ return project;
110
+ });
111
+ }
112
+ export function updateProject(projectId, input, activity) {
113
+ const current = getProjectById(projectId);
114
+ if (!current) {
115
+ return undefined;
116
+ }
117
+ return runInTransaction(() => {
118
+ const parsed = updateProjectSchema.parse(input);
119
+ const nextGoalId = parsed.goalId ?? current.goalId;
120
+ assertGoalExists(nextGoalId);
121
+ const next = {
122
+ goalId: nextGoalId,
123
+ title: parsed.title ?? current.title,
124
+ description: parsed.description ?? current.description,
125
+ status: parsed.status ?? current.status,
126
+ themeColor: parsed.themeColor ?? current.themeColor,
127
+ targetPoints: parsed.targetPoints ?? current.targetPoints,
128
+ updatedAt: new Date().toISOString()
129
+ };
130
+ getDatabase()
131
+ .prepare(`UPDATE projects
132
+ SET goal_id = ?, title = ?, description = ?, status = ?, theme_color = ?, target_points = ?, updated_at = ?
133
+ WHERE id = ?`)
134
+ .run(next.goalId, next.title, next.description, next.status, next.themeColor, next.targetPoints, next.updatedAt, projectId);
135
+ // Keep legacy task.goal_id aligned with the project's parent goal.
136
+ getDatabase()
137
+ .prepare(`UPDATE tasks SET goal_id = ?, updated_at = ? WHERE project_id = ?`)
138
+ .run(next.goalId, next.updatedAt, projectId);
139
+ const project = getProjectById(projectId);
140
+ if (project && activity) {
141
+ recordActivityEvent({
142
+ entityType: "project",
143
+ entityId: project.id,
144
+ eventType: current.status !== project.status ? "project_status_changed" : "project_updated",
145
+ title: current.status !== project.status ? `Project ${project.status}: ${project.title}` : `Project updated: ${project.title}`,
146
+ description: "Project details were updated.",
147
+ actor: activity.actor ?? null,
148
+ source: activity.source,
149
+ metadata: {
150
+ goalId: project.goalId,
151
+ previousGoalId: current.goalId,
152
+ status: project.status,
153
+ previousStatus: current.status
154
+ }
155
+ });
156
+ }
157
+ return project;
158
+ });
159
+ }
160
+ export function ensureDefaultProjectForGoal(goalId) {
161
+ assertGoalExists(goalId);
162
+ const existing = listProjects({ goalId, limit: 1 })[0];
163
+ if (existing) {
164
+ return existing;
165
+ }
166
+ return runInTransaction(() => {
167
+ const goal = getGoalById(goalId);
168
+ if (!goal) {
169
+ throw new Error(`Goal ${goalId} is missing`);
170
+ }
171
+ const template = getDefaultProjectTemplate(goal);
172
+ const now = new Date().toISOString();
173
+ const id = `project_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
174
+ getDatabase()
175
+ .prepare(`INSERT INTO projects (id, goal_id, title, description, status, theme_color, target_points, created_at, updated_at)
176
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
177
+ .run(id, goalId, template.title, template.description, goal.status === "completed" ? "completed" : goal.status === "paused" ? "paused" : "active", goal.themeColor, Math.max(100, Math.round(goal.targetPoints / 2)), now, now);
178
+ return getProjectById(id);
179
+ });
180
+ }
181
+ export function deleteProject(projectId, activity) {
182
+ const current = getProjectById(projectId);
183
+ if (!current) {
184
+ return undefined;
185
+ }
186
+ return runInTransaction(() => {
187
+ pruneLinkedEntityReferences("project", projectId);
188
+ getDatabase()
189
+ .prepare(`DELETE FROM entity_comments
190
+ WHERE entity_type = 'project'
191
+ AND entity_id = ?`)
192
+ .run(projectId);
193
+ getDatabase()
194
+ .prepare(`DELETE FROM projects WHERE id = ?`)
195
+ .run(projectId);
196
+ if (activity) {
197
+ recordActivityEvent({
198
+ entityType: "project",
199
+ entityId: current.id,
200
+ eventType: "project_deleted",
201
+ title: `Project deleted: ${current.title}`,
202
+ description: "Project removed from the system.",
203
+ actor: activity.actor ?? null,
204
+ source: activity.source,
205
+ metadata: {
206
+ goalId: current.goalId,
207
+ status: current.status,
208
+ targetPoints: current.targetPoints
209
+ }
210
+ });
211
+ }
212
+ return current;
213
+ });
214
+ }