forge-openclaw-plugin 0.2.7 → 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.
- package/README.md +73 -1
- package/dist/assets/{board-CzgvdLO8.js → board-C_m78kvK.js} +2 -2
- package/dist/assets/{board-CzgvdLO8.js.map → board-C_m78kvK.js.map} +1 -1
- package/dist/assets/index-BWtLtXwb.js +36 -0
- package/dist/assets/index-BWtLtXwb.js.map +1 -0
- package/dist/assets/index-Dp5GXY_z.css +1 -0
- package/dist/assets/{motion-STUd1O46.js → motion-CpZvZumD.js} +2 -2
- package/dist/assets/{motion-STUd1O46.js.map → motion-CpZvZumD.js.map} +1 -1
- package/dist/assets/{table-CtNlETLc.js → table-DtyXTw03.js} +2 -2
- package/dist/assets/{table-CtNlETLc.js.map → table-DtyXTw03.js.map} +1 -1
- package/dist/assets/{ui-ThzkR_oW.js → ui-BXbpiKyS.js} +2 -2
- package/dist/assets/{ui-ThzkR_oW.js.map → ui-BXbpiKyS.js.map} +1 -1
- package/dist/assets/{vendor-DyHAI6nk.js → vendor-QBH6qVEe.js} +84 -74
- package/dist/assets/vendor-QBH6qVEe.js.map +1 -0
- package/dist/assets/{viz-BJuBCz_G.js → viz-w-IMeueL.js} +2 -2
- package/dist/assets/{viz-BJuBCz_G.js.map → viz-w-IMeueL.js.map} +1 -1
- package/dist/index.html +8 -8
- package/dist/openclaw/api-client.d.ts +1 -0
- package/dist/openclaw/local-runtime.js +2 -1
- package/dist/openclaw/plugin-entry-shared.js +12 -0
- package/dist/server/app.js +104 -67
- package/dist/server/demo-data.js +49 -0
- package/dist/server/openapi.js +84 -43
- package/dist/server/psyche-types.js +1 -30
- package/dist/server/repositories/deleted-entities.js +60 -26
- package/dist/server/repositories/goals.js +2 -5
- package/dist/server/repositories/notes.js +359 -0
- package/dist/server/repositories/projects.js +2 -5
- package/dist/server/repositories/psyche.js +11 -14
- package/dist/server/repositories/task-runs.js +2 -0
- package/dist/server/repositories/tasks.js +21 -10
- package/dist/server/seed-demo.js +11 -0
- package/dist/server/services/dashboard.js +4 -1
- package/dist/server/services/entity-crud.js +27 -30
- package/dist/server/services/insights.js +5 -5
- package/dist/server/services/projects.js +3 -1
- package/dist/server/services/psyche.js +4 -4
- package/dist/server/types.js +70 -11
- package/openclaw.plugin.json +12 -1
- package/package.json +1 -1
- package/server/migrations/001_core.sql +78 -0
- package/server/migrations/002_psyche.sql +164 -13
- package/skills/forge-openclaw/SKILL.md +17 -5
- package/dist/assets/index-8d_oM8fL.js +0 -27
- package/dist/assets/index-8d_oM8fL.js.map +0 -1
- package/dist/assets/index-D4A_bq8m.css +0 -1
- package/dist/assets/vendor-DyHAI6nk.js.map +0 -1
- package/dist/server/repositories/comments.js +0 -176
- package/server/migrations/003_timer_execution.sql +0 -18
- package/server/migrations/004_psyche_linked_entities.sql +0 -5
- package/server/migrations/005_adaptive_schemas.sql +0 -157
- package/server/migrations/006_psyche_auth_setting.sql +0 -4
- package/server/migrations/007_deleted_entities.sql +0 -16
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { getDatabase } from "../db.js";
|
|
3
|
+
import { recordActivityEvent } from "./activity-events.js";
|
|
4
|
+
import { filterDeletedEntities, getDeletedEntityRecord, clearDeletedEntityRecord, isEntityDeleted, upsertDeletedEntityRecord } from "./deleted-entities.js";
|
|
5
|
+
import { recordEventLog } from "./event-log.js";
|
|
6
|
+
import { noteSchema, notesListQuerySchema, createNoteSchema, updateNoteSchema } from "../types.js";
|
|
7
|
+
function normalizeAnchorKey(anchorKey) {
|
|
8
|
+
return anchorKey.trim().length > 0 ? anchorKey : null;
|
|
9
|
+
}
|
|
10
|
+
function normalizeLinks(links) {
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
return links.filter((link) => {
|
|
13
|
+
const key = `${link.entityType}:${link.entityId}:${link.anchorKey ?? ""}`;
|
|
14
|
+
if (seen.has(key)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
seen.add(key);
|
|
18
|
+
return true;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function stripMarkdown(markdown) {
|
|
22
|
+
return markdown
|
|
23
|
+
.replace(/```[\s\S]*?```/g, (block) => block.replace(/```/g, "").trim())
|
|
24
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
25
|
+
.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
|
|
26
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
27
|
+
.replace(/^>\s?/gm, "")
|
|
28
|
+
.replace(/^#{1,6}\s+/gm, "")
|
|
29
|
+
.replace(/^[-*+]\s+/gm, "")
|
|
30
|
+
.replace(/^\d+\.\s+/gm, "")
|
|
31
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
|
32
|
+
.replace(/\*([^*]+)\*/g, "$1")
|
|
33
|
+
.replace(/_([^_]+)_/g, "$1")
|
|
34
|
+
.replace(/~~([^~]+)~~/g, "$1")
|
|
35
|
+
.replace(/\r/g, "")
|
|
36
|
+
.trim();
|
|
37
|
+
}
|
|
38
|
+
function describeNote(note) {
|
|
39
|
+
const plain = note.contentPlain.trim() || stripMarkdown(note.contentMarkdown);
|
|
40
|
+
const compact = plain.replace(/\s+/g, " ").trim();
|
|
41
|
+
const title = compact.slice(0, 72) || "Note";
|
|
42
|
+
const subtitle = compact.length > 72 ? compact.slice(72, 168).trim() : "";
|
|
43
|
+
return { title, subtitle };
|
|
44
|
+
}
|
|
45
|
+
function buildFtsQuery(query) {
|
|
46
|
+
const tokens = query
|
|
47
|
+
.trim()
|
|
48
|
+
.split(/\s+/)
|
|
49
|
+
.map((token) => token.replace(/["*']/g, "").trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
if (tokens.length === 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return tokens.map((token) => `${token}*`).join(" AND ");
|
|
55
|
+
}
|
|
56
|
+
function getNoteRow(noteId) {
|
|
57
|
+
return getDatabase()
|
|
58
|
+
.prepare(`SELECT id, content_markdown, content_plain, author, source, created_at, updated_at
|
|
59
|
+
FROM notes
|
|
60
|
+
WHERE id = ?`)
|
|
61
|
+
.get(noteId);
|
|
62
|
+
}
|
|
63
|
+
function listLinkRowsForNotes(noteIds) {
|
|
64
|
+
if (noteIds.length === 0) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const placeholders = noteIds.map(() => "?").join(", ");
|
|
68
|
+
return getDatabase()
|
|
69
|
+
.prepare(`SELECT note_id, entity_type, entity_id, anchor_key, created_at
|
|
70
|
+
FROM note_links
|
|
71
|
+
WHERE note_id IN (${placeholders})
|
|
72
|
+
ORDER BY created_at ASC`)
|
|
73
|
+
.all(...noteIds);
|
|
74
|
+
}
|
|
75
|
+
function mapLinks(rows) {
|
|
76
|
+
return rows.map((row) => ({
|
|
77
|
+
entityType: row.entity_type,
|
|
78
|
+
entityId: row.entity_id,
|
|
79
|
+
anchorKey: normalizeAnchorKey(row.anchor_key)
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
function mapNote(row, linkRows) {
|
|
83
|
+
return noteSchema.parse({
|
|
84
|
+
id: row.id,
|
|
85
|
+
contentMarkdown: row.content_markdown,
|
|
86
|
+
contentPlain: row.content_plain,
|
|
87
|
+
author: row.author,
|
|
88
|
+
source: row.source,
|
|
89
|
+
createdAt: row.created_at,
|
|
90
|
+
updatedAt: row.updated_at,
|
|
91
|
+
links: mapLinks(linkRows)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function upsertSearchRow(noteId, contentPlain, author) {
|
|
95
|
+
getDatabase().prepare(`DELETE FROM notes_fts WHERE note_id = ?`).run(noteId);
|
|
96
|
+
getDatabase()
|
|
97
|
+
.prepare(`INSERT INTO notes_fts (note_id, content_plain, author) VALUES (?, ?, ?)`)
|
|
98
|
+
.run(noteId, contentPlain, author ?? "");
|
|
99
|
+
}
|
|
100
|
+
function deleteSearchRow(noteId) {
|
|
101
|
+
getDatabase().prepare(`DELETE FROM notes_fts WHERE note_id = ?`).run(noteId);
|
|
102
|
+
}
|
|
103
|
+
function listAllNoteRows() {
|
|
104
|
+
return getDatabase()
|
|
105
|
+
.prepare(`SELECT id, content_markdown, content_plain, author, source, created_at, updated_at
|
|
106
|
+
FROM notes
|
|
107
|
+
ORDER BY created_at DESC`)
|
|
108
|
+
.all();
|
|
109
|
+
}
|
|
110
|
+
function findMatchingNoteIds(query) {
|
|
111
|
+
const ftsQuery = buildFtsQuery(query);
|
|
112
|
+
if (!ftsQuery) {
|
|
113
|
+
return new Set();
|
|
114
|
+
}
|
|
115
|
+
const rows = getDatabase()
|
|
116
|
+
.prepare(`SELECT note_id FROM notes_fts WHERE notes_fts MATCH ?`)
|
|
117
|
+
.all(ftsQuery);
|
|
118
|
+
return new Set(rows.map((row) => row.note_id));
|
|
119
|
+
}
|
|
120
|
+
function insertLinks(noteId, links, createdAt) {
|
|
121
|
+
const statement = getDatabase().prepare(`INSERT OR IGNORE INTO note_links (note_id, entity_type, entity_id, anchor_key, created_at)
|
|
122
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
123
|
+
for (const link of links) {
|
|
124
|
+
statement.run(noteId, link.entityType, link.entityId, link.anchorKey ?? "", createdAt);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function replaceLinks(noteId, links, createdAt) {
|
|
128
|
+
getDatabase().prepare(`DELETE FROM note_links WHERE note_id = ?`).run(noteId);
|
|
129
|
+
insertLinks(noteId, links, createdAt);
|
|
130
|
+
}
|
|
131
|
+
function listNoteLinks(noteId) {
|
|
132
|
+
return getDatabase()
|
|
133
|
+
.prepare(`SELECT note_id, entity_type, entity_id, anchor_key, created_at
|
|
134
|
+
FROM note_links
|
|
135
|
+
WHERE note_id = ?
|
|
136
|
+
ORDER BY created_at ASC`)
|
|
137
|
+
.all(noteId);
|
|
138
|
+
}
|
|
139
|
+
function recordNoteActivity(note, eventType, title, context) {
|
|
140
|
+
for (const link of note.links) {
|
|
141
|
+
recordActivityEvent({
|
|
142
|
+
entityType: link.entityType,
|
|
143
|
+
entityId: link.entityId,
|
|
144
|
+
eventType,
|
|
145
|
+
title,
|
|
146
|
+
description: note.contentPlain,
|
|
147
|
+
actor: note.author ?? context.actor ?? null,
|
|
148
|
+
source: context.source,
|
|
149
|
+
metadata: {
|
|
150
|
+
noteId: note.id,
|
|
151
|
+
anchorKey: link.anchorKey ?? ""
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
recordEventLog({
|
|
155
|
+
eventKind: eventType,
|
|
156
|
+
entityType: link.entityType,
|
|
157
|
+
entityId: link.entityId,
|
|
158
|
+
actor: note.author ?? context.actor ?? null,
|
|
159
|
+
source: context.source,
|
|
160
|
+
metadata: {
|
|
161
|
+
noteId: note.id,
|
|
162
|
+
anchorKey: link.anchorKey ?? ""
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export function getNoteById(noteId) {
|
|
168
|
+
if (isEntityDeleted("note", noteId)) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const row = getNoteRow(noteId);
|
|
172
|
+
if (!row) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
return mapNote(row, listNoteLinks(noteId));
|
|
176
|
+
}
|
|
177
|
+
export function getNoteByIdIncludingDeleted(noteId) {
|
|
178
|
+
const row = getNoteRow(noteId);
|
|
179
|
+
if (!row) {
|
|
180
|
+
const deleted = getDeletedEntityRecord("note", noteId);
|
|
181
|
+
return deleted?.snapshot;
|
|
182
|
+
}
|
|
183
|
+
return mapNote(row, listNoteLinks(noteId));
|
|
184
|
+
}
|
|
185
|
+
export function listNotes(query = {}) {
|
|
186
|
+
const parsed = notesListQuerySchema.parse(query);
|
|
187
|
+
if (parsed.linkedEntityType &&
|
|
188
|
+
parsed.linkedEntityId &&
|
|
189
|
+
isEntityDeleted(parsed.linkedEntityType, parsed.linkedEntityId)) {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
const matchingIds = parsed.query ? findMatchingNoteIds(parsed.query) : null;
|
|
193
|
+
const rows = listAllNoteRows();
|
|
194
|
+
const linksByNoteId = new Map();
|
|
195
|
+
for (const link of listLinkRowsForNotes(rows.map((row) => row.id))) {
|
|
196
|
+
const current = linksByNoteId.get(link.note_id) ?? [];
|
|
197
|
+
current.push(link);
|
|
198
|
+
linksByNoteId.set(link.note_id, current);
|
|
199
|
+
}
|
|
200
|
+
return filterDeletedEntities("note", rows
|
|
201
|
+
.filter((row) => (matchingIds ? matchingIds.has(row.id) : true))
|
|
202
|
+
.filter((row) => (parsed.author ? (row.author ?? "").toLowerCase().includes(parsed.author.toLowerCase()) : true))
|
|
203
|
+
.map((row) => mapNote(row, linksByNoteId.get(row.id) ?? []))
|
|
204
|
+
.filter((note) => parsed.linkedEntityType && parsed.linkedEntityId
|
|
205
|
+
? note.links.some((link) => link.entityType === parsed.linkedEntityType &&
|
|
206
|
+
link.entityId === parsed.linkedEntityId &&
|
|
207
|
+
(parsed.anchorKey === undefined ? true : (link.anchorKey ?? null) === parsed.anchorKey))
|
|
208
|
+
: true)
|
|
209
|
+
.slice(0, parsed.limit ?? 100));
|
|
210
|
+
}
|
|
211
|
+
export function createNote(input, context) {
|
|
212
|
+
const parsed = createNoteSchema.parse({
|
|
213
|
+
...input,
|
|
214
|
+
links: normalizeLinks(input.links)
|
|
215
|
+
});
|
|
216
|
+
const now = new Date().toISOString();
|
|
217
|
+
const id = `note_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
218
|
+
const contentPlain = stripMarkdown(parsed.contentMarkdown);
|
|
219
|
+
getDatabase()
|
|
220
|
+
.prepare(`INSERT INTO notes (id, content_markdown, content_plain, author, source, created_at, updated_at)
|
|
221
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
222
|
+
.run(id, parsed.contentMarkdown, contentPlain, parsed.author ?? context.actor ?? null, context.source, now, now);
|
|
223
|
+
insertLinks(id, parsed.links, now);
|
|
224
|
+
clearDeletedEntityRecord("note", id);
|
|
225
|
+
upsertSearchRow(id, contentPlain, parsed.author ?? context.actor ?? null);
|
|
226
|
+
const note = getNoteById(id);
|
|
227
|
+
recordNoteActivity(note, "note.created", "Note added", context);
|
|
228
|
+
return note;
|
|
229
|
+
}
|
|
230
|
+
export function createLinkedNotes(notes, entityLink, context) {
|
|
231
|
+
if (!notes || notes.length === 0) {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
return notes.map((note) => createNote({
|
|
235
|
+
contentMarkdown: note.contentMarkdown,
|
|
236
|
+
author: note.author,
|
|
237
|
+
links: [entityLink, ...note.links]
|
|
238
|
+
}, context));
|
|
239
|
+
}
|
|
240
|
+
export function updateNote(noteId, input, context) {
|
|
241
|
+
const existing = getNoteByIdIncludingDeleted(noteId);
|
|
242
|
+
if (!existing) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
const patch = updateNoteSchema.parse({
|
|
246
|
+
...input,
|
|
247
|
+
links: input.links ? normalizeLinks(input.links) : undefined
|
|
248
|
+
});
|
|
249
|
+
const nextMarkdown = patch.contentMarkdown ?? existing.contentMarkdown;
|
|
250
|
+
const nextPlain = stripMarkdown(nextMarkdown);
|
|
251
|
+
const nextAuthor = patch.author === undefined ? existing.author : patch.author;
|
|
252
|
+
const updatedAt = new Date().toISOString();
|
|
253
|
+
getDatabase()
|
|
254
|
+
.prepare(`UPDATE notes
|
|
255
|
+
SET content_markdown = ?, content_plain = ?, author = ?, updated_at = ?
|
|
256
|
+
WHERE id = ?`)
|
|
257
|
+
.run(nextMarkdown, nextPlain, nextAuthor, updatedAt, noteId);
|
|
258
|
+
if (patch.links) {
|
|
259
|
+
replaceLinks(noteId, patch.links, updatedAt);
|
|
260
|
+
}
|
|
261
|
+
const note = getNoteByIdIncludingDeleted(noteId);
|
|
262
|
+
if (note.links.length > 0) {
|
|
263
|
+
clearDeletedEntityRecord("note", noteId);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
const details = describeNote(note);
|
|
267
|
+
upsertDeletedEntityRecord({
|
|
268
|
+
entityType: "note",
|
|
269
|
+
entityId: note.id,
|
|
270
|
+
title: details.title,
|
|
271
|
+
subtitle: details.subtitle,
|
|
272
|
+
snapshot: note,
|
|
273
|
+
deleteReason: "Note no longer has any linked entities.",
|
|
274
|
+
context
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
upsertSearchRow(noteId, nextPlain, nextAuthor);
|
|
278
|
+
recordNoteActivity(note, "note.updated", "Note updated", context);
|
|
279
|
+
return getNoteById(noteId);
|
|
280
|
+
}
|
|
281
|
+
export function deleteNote(noteId, context) {
|
|
282
|
+
const existing = getNoteByIdIncludingDeleted(noteId);
|
|
283
|
+
if (!existing) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
getDatabase().prepare(`DELETE FROM note_links WHERE note_id = ?`).run(noteId);
|
|
287
|
+
getDatabase().prepare(`DELETE FROM notes WHERE id = ?`).run(noteId);
|
|
288
|
+
deleteSearchRow(noteId);
|
|
289
|
+
recordNoteActivity(existing, "note.deleted", "Note deleted", context);
|
|
290
|
+
return existing;
|
|
291
|
+
}
|
|
292
|
+
export function buildNotesSummaryByEntity() {
|
|
293
|
+
const rows = getDatabase()
|
|
294
|
+
.prepare(`SELECT
|
|
295
|
+
note_links.entity_type AS entity_type,
|
|
296
|
+
note_links.entity_id AS entity_id,
|
|
297
|
+
notes.id AS note_id,
|
|
298
|
+
notes.created_at AS created_at
|
|
299
|
+
FROM note_links
|
|
300
|
+
INNER JOIN notes ON notes.id = note_links.note_id
|
|
301
|
+
LEFT JOIN deleted_entities
|
|
302
|
+
ON deleted_entities.entity_type = 'note'
|
|
303
|
+
AND deleted_entities.entity_id = notes.id
|
|
304
|
+
WHERE deleted_entities.entity_id IS NULL
|
|
305
|
+
ORDER BY notes.created_at DESC`)
|
|
306
|
+
.all();
|
|
307
|
+
return rows.reduce((acc, row) => {
|
|
308
|
+
const key = `${row.entity_type}:${row.entity_id}`;
|
|
309
|
+
const current = acc[key];
|
|
310
|
+
if (!current) {
|
|
311
|
+
acc[key] = {
|
|
312
|
+
count: 1,
|
|
313
|
+
latestNoteId: row.note_id,
|
|
314
|
+
latestCreatedAt: row.created_at
|
|
315
|
+
};
|
|
316
|
+
return acc;
|
|
317
|
+
}
|
|
318
|
+
current.count += 1;
|
|
319
|
+
if (!current.latestCreatedAt || row.created_at > current.latestCreatedAt) {
|
|
320
|
+
current.latestCreatedAt = row.created_at;
|
|
321
|
+
current.latestNoteId = row.note_id;
|
|
322
|
+
}
|
|
323
|
+
return acc;
|
|
324
|
+
}, {});
|
|
325
|
+
}
|
|
326
|
+
export function unlinkNotesForEntity(entityType, entityId, context) {
|
|
327
|
+
const noteIds = getDatabase()
|
|
328
|
+
.prepare(`SELECT DISTINCT note_id FROM note_links WHERE entity_type = ? AND entity_id = ?`)
|
|
329
|
+
.all(entityType, entityId);
|
|
330
|
+
if (noteIds.length === 0) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
getDatabase()
|
|
334
|
+
.prepare(`DELETE FROM note_links WHERE entity_type = ? AND entity_id = ?`)
|
|
335
|
+
.run(entityType, entityId);
|
|
336
|
+
for (const row of noteIds) {
|
|
337
|
+
const remaining = getDatabase()
|
|
338
|
+
.prepare(`SELECT COUNT(*) AS count FROM note_links WHERE note_id = ?`)
|
|
339
|
+
.get(row.note_id);
|
|
340
|
+
if (remaining.count > 0) {
|
|
341
|
+
clearDeletedEntityRecord("note", row.note_id);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const note = getNoteByIdIncludingDeleted(row.note_id);
|
|
345
|
+
if (!note) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
const details = describeNote(note);
|
|
349
|
+
upsertDeletedEntityRecord({
|
|
350
|
+
entityType: "note",
|
|
351
|
+
entityId: note.id,
|
|
352
|
+
title: details.title,
|
|
353
|
+
subtitle: details.subtitle,
|
|
354
|
+
snapshot: { ...note, links: [] },
|
|
355
|
+
deleteReason: `All links were removed when ${entityType} ${entityId} was deleted.`,
|
|
356
|
+
context
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { getDatabase, runInTransaction } from "../db.js";
|
|
3
3
|
import { recordActivityEvent } from "./activity-events.js";
|
|
4
4
|
import { filterDeletedEntities, isEntityDeleted } from "./deleted-entities.js";
|
|
5
|
+
import { createLinkedNotes } from "./notes.js";
|
|
5
6
|
import { assertGoalExists } from "../services/relations.js";
|
|
6
7
|
import { getGoalById } from "./goals.js";
|
|
7
8
|
import { pruneLinkedEntityReferences } from "./psyche.js";
|
|
@@ -90,6 +91,7 @@ export function createProject(input, activity) {
|
|
|
90
91
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
91
92
|
.run(id, parsed.goalId, parsed.title, parsed.description, parsed.status, parsed.themeColor, parsed.targetPoints, now, now);
|
|
92
93
|
const project = getProjectById(id);
|
|
94
|
+
createLinkedNotes(parsed.notes, { entityType: "project", entityId: project.id, anchorKey: null }, activity ?? { source: "ui", actor: null });
|
|
93
95
|
if (activity) {
|
|
94
96
|
recordActivityEvent({
|
|
95
97
|
entityType: "project",
|
|
@@ -185,11 +187,6 @@ export function deleteProject(projectId, activity) {
|
|
|
185
187
|
}
|
|
186
188
|
return runInTransaction(() => {
|
|
187
189
|
pruneLinkedEntityReferences("project", projectId);
|
|
188
|
-
getDatabase()
|
|
189
|
-
.prepare(`DELETE FROM entity_comments
|
|
190
|
-
WHERE entity_type = 'project'
|
|
191
|
-
AND entity_id = ?`)
|
|
192
|
-
.run(projectId);
|
|
193
190
|
getDatabase()
|
|
194
191
|
.prepare(`DELETE FROM projects WHERE id = ?`)
|
|
195
192
|
.run(projectId);
|
|
@@ -3,6 +3,7 @@ import { getDatabase, runInTransaction } from "../db.js";
|
|
|
3
3
|
import { recordActivityEvent } from "./activity-events.js";
|
|
4
4
|
import { filterDeletedEntities, filterDeletedIds, isEntityDeleted } from "./deleted-entities.js";
|
|
5
5
|
import { recordEventLog } from "./event-log.js";
|
|
6
|
+
import { unlinkNotesForEntity } from "./notes.js";
|
|
6
7
|
import { recordPsycheClarityReward, recordPsycheReflectionReward } from "./rewards.js";
|
|
7
8
|
import { behaviorPatternSchema, behaviorSchema, beliefEntrySchema, createBehaviorPatternSchema, createBehaviorSchema, createBeliefEntrySchema, createEmotionDefinitionSchema, createEventTypeSchema, createModeGuideSessionSchema, createModeProfileSchema, createPsycheValueSchema, createTriggerReportSchema, domainSchema, emotionDefinitionSchema, eventTypeSchema, modeFamilySchema, modeGuideResultSchema, modeGuideSessionSchema, modeProfileSchema, modeTimelineEntrySchema, psycheValueSchema, schemaCatalogEntrySchema, triggerReportSchema, updateBehaviorPatternSchema, updateBehaviorSchema, updateBeliefEntrySchema, updateEmotionDefinitionSchema, updateEventTypeSchema, updateModeGuideSessionSchema, updateModeProfileSchema, updatePsycheValueSchema, updateTriggerReportSchema } from "../psyche-types.js";
|
|
8
9
|
const PSYCHE_DOMAIN_ID = "domain_psyche";
|
|
@@ -312,12 +313,8 @@ function mapCreateUpdateContext(input) {
|
|
|
312
313
|
function getRow(sql, id) {
|
|
313
314
|
return getDatabase().prepare(sql).get(id);
|
|
314
315
|
}
|
|
315
|
-
function
|
|
316
|
-
|
|
317
|
-
.prepare(`DELETE FROM entity_comments
|
|
318
|
-
WHERE entity_type = ?
|
|
319
|
-
AND entity_id = ?`)
|
|
320
|
-
.run(entityType, entityId);
|
|
316
|
+
function unlinkEntityNotes(entityType, entityId) {
|
|
317
|
+
unlinkNotesForEntity(entityType, entityId, { source: "system", actor: null });
|
|
321
318
|
}
|
|
322
319
|
function rewriteJsonColumn(table, column, transform) {
|
|
323
320
|
const rows = getDatabase()
|
|
@@ -453,7 +450,7 @@ export function deleteEventType(eventTypeId, context) {
|
|
|
453
450
|
return undefined;
|
|
454
451
|
}
|
|
455
452
|
return runInTransaction(() => {
|
|
456
|
-
|
|
453
|
+
unlinkEntityNotes("event_type", eventTypeId);
|
|
457
454
|
getDatabase()
|
|
458
455
|
.prepare(`DELETE FROM event_types WHERE id = ?`)
|
|
459
456
|
.run(eventTypeId);
|
|
@@ -549,7 +546,7 @@ export function deleteEmotionDefinition(emotionId, context) {
|
|
|
549
546
|
}
|
|
550
547
|
return runInTransaction(() => {
|
|
551
548
|
nullifyTriggerEmotionReferences(emotionId);
|
|
552
|
-
|
|
549
|
+
unlinkEntityNotes("emotion_definition", emotionId);
|
|
553
550
|
getDatabase()
|
|
554
551
|
.prepare(`DELETE FROM emotion_definitions WHERE id = ?`)
|
|
555
552
|
.run(emotionId);
|
|
@@ -662,7 +659,7 @@ export function deletePsycheValue(valueId, context) {
|
|
|
662
659
|
removeIdFromStringArrayColumn("belief_entries", "linked_value_ids_json", valueId);
|
|
663
660
|
removeIdFromStringArrayColumn("mode_profiles", "linked_value_ids_json", valueId);
|
|
664
661
|
removeIdFromStringArrayColumn("trigger_reports", "linked_value_ids_json", valueId);
|
|
665
|
-
|
|
662
|
+
unlinkEntityNotes("psyche_value", valueId);
|
|
666
663
|
getDatabase()
|
|
667
664
|
.prepare(`DELETE FROM psyche_values WHERE id = ?`)
|
|
668
665
|
.run(valueId);
|
|
@@ -780,7 +777,7 @@ export function deleteBehaviorPattern(patternId, context) {
|
|
|
780
777
|
removeIdFromStringArrayColumn("psyche_behaviors", "linked_pattern_ids_json", patternId);
|
|
781
778
|
removeIdFromStringArrayColumn("mode_profiles", "linked_pattern_ids_json", patternId);
|
|
782
779
|
removeIdFromStringArrayColumn("trigger_reports", "linked_pattern_ids_json", patternId);
|
|
783
|
-
|
|
780
|
+
unlinkEntityNotes("behavior_pattern", patternId);
|
|
784
781
|
getDatabase()
|
|
785
782
|
.prepare(`DELETE FROM behavior_patterns WHERE id = ?`)
|
|
786
783
|
.run(patternId);
|
|
@@ -888,7 +885,7 @@ export function deleteBehavior(behaviorId, context) {
|
|
|
888
885
|
removeIdFromStringArrayColumn("mode_profiles", "linked_behavior_ids_json", behaviorId);
|
|
889
886
|
removeIdFromStringArrayColumn("trigger_reports", "linked_behavior_ids_json", behaviorId);
|
|
890
887
|
nullifyTriggerBehaviorReferences(behaviorId);
|
|
891
|
-
|
|
888
|
+
unlinkEntityNotes("behavior", behaviorId);
|
|
892
889
|
getDatabase()
|
|
893
890
|
.prepare(`DELETE FROM psyche_behaviors WHERE id = ?`)
|
|
894
891
|
.run(behaviorId);
|
|
@@ -995,7 +992,7 @@ export function deleteBeliefEntry(beliefId, context) {
|
|
|
995
992
|
removeIdFromStringArrayColumn("behavior_patterns", "linked_belief_ids_json", beliefId);
|
|
996
993
|
removeIdFromStringArrayColumn("trigger_reports", "linked_belief_ids_json", beliefId);
|
|
997
994
|
nullifyTriggerThoughtBeliefReferences(beliefId);
|
|
998
|
-
|
|
995
|
+
unlinkEntityNotes("belief_entry", beliefId);
|
|
999
996
|
getDatabase()
|
|
1000
997
|
.prepare(`DELETE FROM belief_entries WHERE id = ?`)
|
|
1001
998
|
.run(beliefId);
|
|
@@ -1104,7 +1101,7 @@ export function deleteModeProfile(modeId, context) {
|
|
|
1104
1101
|
removeIdFromStringArrayColumn("belief_entries", "linked_mode_ids_json", modeId);
|
|
1105
1102
|
removeIdFromStringArrayColumn("trigger_reports", "linked_mode_ids_json", modeId);
|
|
1106
1103
|
nullifyTriggerTimelineModeReferences(modeId);
|
|
1107
|
-
|
|
1104
|
+
unlinkEntityNotes("mode_profile", modeId);
|
|
1108
1105
|
getDatabase()
|
|
1109
1106
|
.prepare(`DELETE FROM mode_profiles WHERE id = ?`)
|
|
1110
1107
|
.run(modeId);
|
|
@@ -1338,7 +1335,7 @@ export function deleteTriggerReport(reportId, context) {
|
|
|
1338
1335
|
}
|
|
1339
1336
|
return runInTransaction(() => {
|
|
1340
1337
|
removeIdFromStringArrayColumn("belief_entries", "linked_report_ids_json", reportId);
|
|
1341
|
-
|
|
1338
|
+
unlinkEntityNotes("trigger_report", reportId);
|
|
1342
1339
|
getDatabase()
|
|
1343
1340
|
.prepare(`DELETE FROM trigger_reports WHERE id = ?`)
|
|
1344
1341
|
.run(reportId);
|
|
@@ -3,6 +3,7 @@ import { getDatabase, runInTransaction } from "../db.js";
|
|
|
3
3
|
import { HttpError } from "../errors.js";
|
|
4
4
|
import { computeWorkTime } from "../services/work-time.js";
|
|
5
5
|
import { recordActivityEvent } from "./activity-events.js";
|
|
6
|
+
import { createLinkedNotes } from "./notes.js";
|
|
6
7
|
import { recordTaskRunCompletionReward, recordTaskRunProgressRewards, recordTaskRunStartReward } from "./rewards.js";
|
|
7
8
|
import { getTaskById, updateTaskInTransaction } from "./tasks.js";
|
|
8
9
|
import { taskRunClaimSchema, taskRunSchema } from "../types.js";
|
|
@@ -477,6 +478,7 @@ function finishTaskRun(taskRunId, nextStatus, timestampColumn, input, now, activ
|
|
|
477
478
|
});
|
|
478
479
|
}
|
|
479
480
|
}
|
|
481
|
+
createLinkedNotes(input.closeoutNote ? [input.closeoutNote] : [], { entityType: "task", entityId: run.taskId, anchorKey: null }, { source: activity.source, actor: input.actor ?? run.actor });
|
|
480
482
|
return run;
|
|
481
483
|
});
|
|
482
484
|
}
|
|
@@ -4,6 +4,8 @@ import { runInTransaction } from "../db.js";
|
|
|
4
4
|
import { HttpError } from "../errors.js";
|
|
5
5
|
import { recordActivityEvent } from "./activity-events.js";
|
|
6
6
|
import { filterDeletedEntities, filterDeletedIds, isEntityDeleted } from "./deleted-entities.js";
|
|
7
|
+
import { getGoalById } from "./goals.js";
|
|
8
|
+
import { createLinkedNotes } from "./notes.js";
|
|
7
9
|
import { ensureDefaultProjectForGoal, getProjectById } from "./projects.js";
|
|
8
10
|
import { pruneLinkedEntityReferences } from "./psyche.js";
|
|
9
11
|
import { awardTaskCompletionReward, reverseLatestTaskCompletionReward } from "./rewards.js";
|
|
@@ -59,19 +61,24 @@ function normalizeCompletedAt(status, existingCompletedAt) {
|
|
|
59
61
|
return null;
|
|
60
62
|
}
|
|
61
63
|
function resolveProjectAndGoalIds(input, current) {
|
|
62
|
-
const
|
|
64
|
+
const currentGoalId = current?.goalId && getGoalById(current.goalId) ? current.goalId : null;
|
|
65
|
+
const currentProject = current?.projectId ? getProjectById(current.projectId) ?? null : null;
|
|
66
|
+
const currentProjectGoalId = currentProject?.goalId && getGoalById(currentProject.goalId) ? currentProject.goalId : null;
|
|
67
|
+
const currentProjectId = currentProject?.id ?? null;
|
|
68
|
+
const requestedGoalId = input.goalId === undefined ? currentGoalId : input.goalId;
|
|
63
69
|
const goalChangedWithoutProjectOverride = current !== undefined && input.goalId !== undefined && input.goalId !== current.goalId && input.projectId === undefined;
|
|
64
|
-
const requestedProjectId = input.projectId === undefined ? (goalChangedWithoutProjectOverride ? null :
|
|
70
|
+
const requestedProjectId = input.projectId === undefined ? (goalChangedWithoutProjectOverride ? null : currentProjectId) : input.projectId;
|
|
65
71
|
if (requestedProjectId) {
|
|
66
72
|
const project = getProjectById(requestedProjectId);
|
|
67
73
|
if (!project) {
|
|
68
74
|
throw new HttpError(404, "project_not_found", `Project ${requestedProjectId} does not exist`);
|
|
69
75
|
}
|
|
70
|
-
|
|
76
|
+
const projectGoalId = getGoalById(project.goalId) ? project.goalId : null;
|
|
77
|
+
if (requestedGoalId && projectGoalId && project.goalId !== requestedGoalId) {
|
|
71
78
|
throw new HttpError(409, "project_goal_mismatch", `Project ${requestedProjectId} does not belong to goal ${requestedGoalId}`);
|
|
72
79
|
}
|
|
73
80
|
return {
|
|
74
|
-
goalId:
|
|
81
|
+
goalId: projectGoalId,
|
|
75
82
|
projectId: project.id
|
|
76
83
|
};
|
|
77
84
|
}
|
|
@@ -184,6 +191,9 @@ function updateTaskRecord(current, input, activity) {
|
|
|
184
191
|
reverseLatestTaskCompletionReward(updated, activity);
|
|
185
192
|
}
|
|
186
193
|
}
|
|
194
|
+
if (updated) {
|
|
195
|
+
createLinkedNotes(input.notes, { entityType: "task", entityId: updated.id, anchorKey: null }, activity ?? { source: "ui", actor: null });
|
|
196
|
+
}
|
|
187
197
|
return updated;
|
|
188
198
|
}
|
|
189
199
|
function fingerprintTaskCreate(input) {
|
|
@@ -201,7 +211,12 @@ function fingerprintTaskCreate(input) {
|
|
|
201
211
|
energy: input.energy,
|
|
202
212
|
points: input.points,
|
|
203
213
|
sortOrder: input.sortOrder ?? null,
|
|
204
|
-
tagIds: input.tagIds
|
|
214
|
+
tagIds: input.tagIds,
|
|
215
|
+
notes: input.notes.map((note) => ({
|
|
216
|
+
contentMarkdown: note.contentMarkdown,
|
|
217
|
+
author: note.author,
|
|
218
|
+
links: note.links
|
|
219
|
+
}))
|
|
205
220
|
}))
|
|
206
221
|
.digest("hex");
|
|
207
222
|
}
|
|
@@ -245,6 +260,7 @@ function insertTaskRecord(input, activity) {
|
|
|
245
260
|
awardTaskCompletionReward(task, activity);
|
|
246
261
|
}
|
|
247
262
|
}
|
|
263
|
+
createLinkedNotes(input.notes, { entityType: "task", entityId: task.id, anchorKey: null }, activity ?? { source: "ui", actor: null });
|
|
248
264
|
return task;
|
|
249
265
|
}
|
|
250
266
|
export function listTasks(filters = {}) {
|
|
@@ -382,11 +398,6 @@ export function deleteTask(taskId, activity) {
|
|
|
382
398
|
}
|
|
383
399
|
return runInTransaction(() => {
|
|
384
400
|
pruneLinkedEntityReferences("task", taskId);
|
|
385
|
-
getDatabase()
|
|
386
|
-
.prepare(`DELETE FROM entity_comments
|
|
387
|
-
WHERE entity_type = 'task'
|
|
388
|
-
AND entity_id = ?`)
|
|
389
|
-
.run(taskId);
|
|
390
401
|
getDatabase()
|
|
391
402
|
.prepare(`DELETE FROM tasks WHERE id = ?`)
|
|
392
403
|
.run(taskId);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { seedDemoDataIntoRuntime } from "./demo-data.js";
|
|
2
|
+
const explicitDataRoot = process.argv[2];
|
|
3
|
+
try {
|
|
4
|
+
const summary = await seedDemoDataIntoRuntime(explicitDataRoot);
|
|
5
|
+
console.log(`Seeded Forge demo data into ${summary.databasePath}`);
|
|
6
|
+
console.log(`Counts: goals=${summary.counts.goals}, projects=${summary.counts.projects}, tasks=${summary.counts.tasks}, task_runs=${summary.counts.task_runs}`);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
10
|
+
process.exitCode = 1;
|
|
11
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listGoals } from "../repositories/goals.js";
|
|
2
2
|
import { listActivityEvents } from "../repositories/activity-events.js";
|
|
3
|
+
import { buildNotesSummaryByEntity } from "../repositories/notes.js";
|
|
3
4
|
import { listTagsByIds, listTags } from "../repositories/tags.js";
|
|
4
5
|
import { listTasks } from "../repositories/tasks.js";
|
|
5
6
|
import { buildAchievementSignals, buildGamificationProfile, buildMilestoneRewards } from "./gamification.js";
|
|
@@ -153,6 +154,7 @@ export function getDashboard() {
|
|
|
153
154
|
const achievements = buildAchievementSignals(goals, tasks, now);
|
|
154
155
|
const milestoneRewards = buildMilestoneRewards(goals, tasks, now);
|
|
155
156
|
const recentActivity = listActivityEvents({ limit: 12 });
|
|
157
|
+
const notesSummaryByEntity = buildNotesSummaryByEntity();
|
|
156
158
|
return dashboardPayloadSchema.parse({
|
|
157
159
|
stats,
|
|
158
160
|
goals: goalCards,
|
|
@@ -165,6 +167,7 @@ export function getDashboard() {
|
|
|
165
167
|
gamification,
|
|
166
168
|
achievements,
|
|
167
169
|
milestoneRewards,
|
|
168
|
-
recentActivity
|
|
170
|
+
recentActivity,
|
|
171
|
+
notesSummaryByEntity
|
|
169
172
|
});
|
|
170
173
|
}
|