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.
Files changed (53) hide show
  1. package/README.md +73 -1
  2. package/dist/assets/{board-CzgvdLO8.js → board-C_m78kvK.js} +2 -2
  3. package/dist/assets/{board-CzgvdLO8.js.map → board-C_m78kvK.js.map} +1 -1
  4. package/dist/assets/index-BWtLtXwb.js +36 -0
  5. package/dist/assets/index-BWtLtXwb.js.map +1 -0
  6. package/dist/assets/index-Dp5GXY_z.css +1 -0
  7. package/dist/assets/{motion-STUd1O46.js → motion-CpZvZumD.js} +2 -2
  8. package/dist/assets/{motion-STUd1O46.js.map → motion-CpZvZumD.js.map} +1 -1
  9. package/dist/assets/{table-CtNlETLc.js → table-DtyXTw03.js} +2 -2
  10. package/dist/assets/{table-CtNlETLc.js.map → table-DtyXTw03.js.map} +1 -1
  11. package/dist/assets/{ui-ThzkR_oW.js → ui-BXbpiKyS.js} +2 -2
  12. package/dist/assets/{ui-ThzkR_oW.js.map → ui-BXbpiKyS.js.map} +1 -1
  13. package/dist/assets/{vendor-DyHAI6nk.js → vendor-QBH6qVEe.js} +84 -74
  14. package/dist/assets/vendor-QBH6qVEe.js.map +1 -0
  15. package/dist/assets/{viz-BJuBCz_G.js → viz-w-IMeueL.js} +2 -2
  16. package/dist/assets/{viz-BJuBCz_G.js.map → viz-w-IMeueL.js.map} +1 -1
  17. package/dist/index.html +8 -8
  18. package/dist/openclaw/api-client.d.ts +1 -0
  19. package/dist/openclaw/local-runtime.js +2 -1
  20. package/dist/openclaw/plugin-entry-shared.js +12 -0
  21. package/dist/server/app.js +104 -67
  22. package/dist/server/demo-data.js +49 -0
  23. package/dist/server/openapi.js +84 -43
  24. package/dist/server/psyche-types.js +1 -30
  25. package/dist/server/repositories/deleted-entities.js +60 -26
  26. package/dist/server/repositories/goals.js +2 -5
  27. package/dist/server/repositories/notes.js +359 -0
  28. package/dist/server/repositories/projects.js +2 -5
  29. package/dist/server/repositories/psyche.js +11 -14
  30. package/dist/server/repositories/task-runs.js +2 -0
  31. package/dist/server/repositories/tasks.js +21 -10
  32. package/dist/server/seed-demo.js +11 -0
  33. package/dist/server/services/dashboard.js +4 -1
  34. package/dist/server/services/entity-crud.js +27 -30
  35. package/dist/server/services/insights.js +5 -5
  36. package/dist/server/services/projects.js +3 -1
  37. package/dist/server/services/psyche.js +4 -4
  38. package/dist/server/types.js +70 -11
  39. package/openclaw.plugin.json +12 -1
  40. package/package.json +1 -1
  41. package/server/migrations/001_core.sql +78 -0
  42. package/server/migrations/002_psyche.sql +164 -13
  43. package/skills/forge-openclaw/SKILL.md +17 -5
  44. package/dist/assets/index-8d_oM8fL.js +0 -27
  45. package/dist/assets/index-8d_oM8fL.js.map +0 -1
  46. package/dist/assets/index-D4A_bq8m.css +0 -1
  47. package/dist/assets/vendor-DyHAI6nk.js.map +0 -1
  48. package/dist/server/repositories/comments.js +0 -176
  49. package/server/migrations/003_timer_execution.sql +0 -18
  50. package/server/migrations/004_psyche_linked_entities.sql +0 -5
  51. package/server/migrations/005_adaptive_schemas.sql +0 -157
  52. package/server/migrations/006_psyche_auth_setting.sql +0 -4
  53. 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 deleteEntityComments(entityType, entityId) {
316
- getDatabase()
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
- deleteEntityComments("event_type", eventTypeId);
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
- deleteEntityComments("emotion_definition", emotionId);
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
- deleteEntityComments("psyche_value", valueId);
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
- deleteEntityComments("behavior_pattern", patternId);
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
- deleteEntityComments("behavior", behaviorId);
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
- deleteEntityComments("belief_entry", beliefId);
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
- deleteEntityComments("mode_profile", modeId);
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
- deleteEntityComments("trigger_report", reportId);
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 requestedGoalId = input.goalId === undefined ? current?.goalId ?? null : input.goalId;
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 : current?.projectId ?? null) : input.projectId;
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
- if (requestedGoalId && project.goalId !== requestedGoalId) {
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: project.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
  }