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.
- package/README.md +114 -6
- package/dist/assets/board-CzgvdLO8.js +6 -0
- package/dist/assets/board-CzgvdLO8.js.map +1 -0
- package/dist/assets/favicon-BCHm9dUV.ico +0 -0
- package/dist/assets/index-8d_oM8fL.js +27 -0
- package/dist/assets/index-8d_oM8fL.js.map +1 -0
- package/dist/assets/index-D4A_bq8m.css +1 -0
- package/dist/assets/motion-STUd1O46.js +10 -0
- package/dist/assets/motion-STUd1O46.js.map +1 -0
- package/dist/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
- package/dist/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
- package/dist/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
- package/dist/assets/sora-latin-ext-wght-normal-CawQDOvP.woff2 +0 -0
- package/dist/assets/sora-latin-wght-normal-DdqRvwsR.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
- package/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
- package/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
- package/dist/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
- package/dist/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
- package/dist/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
- package/dist/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
- package/dist/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
- package/dist/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
- package/dist/assets/table-CtNlETLc.js +23 -0
- package/dist/assets/table-CtNlETLc.js.map +1 -0
- package/dist/assets/ui-ThzkR_oW.js +46 -0
- package/dist/assets/ui-ThzkR_oW.js.map +1 -0
- package/dist/assets/vendor-CRS-psbw.css +1 -0
- package/dist/assets/vendor-DyHAI6nk.js +423 -0
- package/dist/assets/vendor-DyHAI6nk.js.map +1 -0
- package/dist/assets/viz-BJuBCz_G.js +34 -0
- package/dist/assets/viz-BJuBCz_G.js.map +1 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.png +0 -0
- package/dist/index.html +29 -0
- package/dist/openclaw/api-client.d.ts +8 -0
- package/dist/openclaw/api-client.js +31 -4
- package/dist/openclaw/local-runtime.d.ts +3 -0
- package/dist/openclaw/local-runtime.js +135 -0
- package/dist/openclaw/parity.d.ts +4 -4
- package/dist/openclaw/parity.js +23 -33
- package/dist/openclaw/plugin-entry-shared.d.ts +5 -3
- package/dist/openclaw/plugin-entry-shared.js +52 -10
- package/dist/openclaw/routes.d.ts +12 -3
- package/dist/openclaw/routes.js +156 -924
- package/dist/openclaw/tools.js +242 -1100
- package/dist/server/app.js +2450 -0
- package/dist/server/db.js +313 -0
- package/dist/server/e2e-server.js +20 -0
- package/dist/server/errors.js +15 -0
- package/dist/server/index.js +16 -0
- package/dist/server/managers/base.js +17 -0
- package/dist/server/managers/contracts.js +47 -0
- package/dist/server/managers/platform/api-gateway-manager.js +11 -0
- package/dist/server/managers/platform/audit-manager.js +15 -0
- package/dist/server/managers/platform/authentication-manager.js +56 -0
- package/dist/server/managers/platform/authorization-manager.js +56 -0
- package/dist/server/managers/platform/background-job-manager.js +10 -0
- package/dist/server/managers/platform/configuration-manager.js +33 -0
- package/dist/server/managers/platform/database-manager.js +14 -0
- package/dist/server/managers/platform/event-bus-manager.js +7 -0
- package/dist/server/managers/platform/external-service-manager.js +11 -0
- package/dist/server/managers/platform/health-manager.js +7 -0
- package/dist/server/managers/platform/migration-manager.js +8 -0
- package/dist/server/managers/platform/search-index-manager.js +4 -0
- package/dist/server/managers/platform/secrets-manager.js +19 -0
- package/dist/server/managers/platform/session-manager.js +121 -0
- package/dist/server/managers/platform/storage-manager.js +16 -0
- package/dist/server/managers/platform/token-manager.js +37 -0
- package/dist/server/managers/platform/transaction-manager.js +8 -0
- package/dist/server/managers/platform/trusted-network.js +39 -0
- package/dist/server/managers/runtime.js +56 -0
- package/dist/server/managers/type-guards.js +4 -0
- package/dist/server/openapi.js +3512 -0
- package/dist/server/psyche-types.js +395 -0
- package/dist/server/repositories/activity-events.js +157 -0
- package/dist/server/repositories/collaboration.js +497 -0
- package/dist/server/repositories/comments.js +176 -0
- package/dist/server/repositories/deleted-entities.js +192 -0
- package/dist/server/repositories/domains.js +30 -0
- package/dist/server/repositories/event-log.js +64 -0
- package/dist/server/repositories/goals.js +159 -0
- package/dist/server/repositories/projects.js +214 -0
- package/dist/server/repositories/psyche.js +1356 -0
- package/dist/server/repositories/rewards.js +675 -0
- package/dist/server/repositories/settings.js +399 -0
- package/dist/server/repositories/tags.js +160 -0
- package/dist/server/repositories/task-runs.js +488 -0
- package/dist/server/repositories/tasks.js +413 -0
- package/dist/server/services/context.js +214 -0
- package/dist/server/services/dashboard.js +170 -0
- package/dist/server/services/entity-crud.js +576 -0
- package/dist/server/services/gamification.js +215 -0
- package/dist/server/services/insights.js +91 -0
- package/dist/server/services/projects.js +75 -0
- package/dist/server/services/psyche.js +63 -0
- package/dist/server/services/relations.js +28 -0
- package/dist/server/services/reviews.js +88 -0
- package/dist/server/services/run-recovery.js +13 -0
- package/dist/server/services/tagging.js +49 -0
- package/dist/server/services/task-run-watchdog.js +92 -0
- package/dist/server/services/work-time.js +176 -0
- package/dist/server/types.js +999 -0
- package/dist/server/web.js +91 -0
- package/openclaw.plugin.json +22 -10
- package/package.json +17 -4
- package/server/migrations/001_core.sql +333 -0
- package/server/migrations/002_psyche.sql +241 -0
- package/server/migrations/003_timer_execution.sql +18 -0
- package/server/migrations/004_psyche_linked_entities.sql +5 -0
- package/server/migrations/005_adaptive_schemas.sql +157 -0
- package/server/migrations/006_psyche_auth_setting.sql +4 -0
- package/server/migrations/007_deleted_entities.sql +16 -0
- 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
|
+
}
|