forge-openclaw-plugin 0.2.19 → 0.2.21

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 (82) hide show
  1. package/README.md +133 -2
  2. package/dist/assets/board-_C6oMy5w.js +6 -0
  3. package/dist/assets/{board-8L3uX7_O.js.map → board-_C6oMy5w.js.map} +1 -1
  4. package/dist/assets/index-B4A6TooJ.js +63 -0
  5. package/dist/assets/index-B4A6TooJ.js.map +1 -0
  6. package/dist/assets/index-D6Xs_2mo.css +1 -0
  7. package/dist/assets/{motion-1GAqqi8M.js → motion-D4sZgCHd.js} +2 -2
  8. package/dist/assets/{motion-1GAqqi8M.js.map → motion-D4sZgCHd.js.map} +1 -1
  9. package/dist/assets/{table-DBGlgRjk.js → table-BWzTaky1.js} +2 -2
  10. package/dist/assets/{table-DBGlgRjk.js.map → table-BWzTaky1.js.map} +1 -1
  11. package/dist/assets/{ui-iTluWjC4.js → ui-BzK4azQb.js} +7 -7
  12. package/dist/assets/{ui-iTluWjC4.js.map → ui-BzK4azQb.js.map} +1 -1
  13. package/dist/assets/vendor-DT3pnAKJ.css +1 -0
  14. package/dist/assets/vendor-De38P6YR.js +729 -0
  15. package/dist/assets/vendor-De38P6YR.js.map +1 -0
  16. package/dist/assets/viz-C6hfyqzu.js +34 -0
  17. package/dist/assets/viz-C6hfyqzu.js.map +1 -0
  18. package/dist/index.html +9 -9
  19. package/dist/openclaw/parity.d.ts +1 -1
  20. package/dist/openclaw/parity.js +29 -2
  21. package/dist/openclaw/routes.js +207 -24
  22. package/dist/openclaw/tools.js +324 -35
  23. package/dist/server/app.js +2080 -92
  24. package/dist/server/db.js +3 -0
  25. package/dist/server/health.js +1284 -0
  26. package/dist/server/managers/platform/background-job-manager.js +138 -2
  27. package/dist/server/managers/platform/llm-manager.js +126 -0
  28. package/dist/server/managers/platform/openai-responses-provider.js +773 -0
  29. package/dist/server/managers/runtime.js +6 -1
  30. package/dist/server/openapi.js +718 -0
  31. package/dist/server/preferences-seeds.js +409 -0
  32. package/dist/server/preferences-types.js +368 -0
  33. package/dist/server/psyche-types.js +42 -18
  34. package/dist/server/repositories/activity-events.js +53 -4
  35. package/dist/server/repositories/calendar.js +89 -15
  36. package/dist/server/repositories/collaboration.js +8 -3
  37. package/dist/server/repositories/diagnostic-logs.js +243 -0
  38. package/dist/server/repositories/entity-ownership.js +92 -0
  39. package/dist/server/repositories/goals.js +7 -2
  40. package/dist/server/repositories/habits.js +122 -16
  41. package/dist/server/repositories/notes.js +119 -41
  42. package/dist/server/repositories/preferences.js +1765 -0
  43. package/dist/server/repositories/projects.js +18 -7
  44. package/dist/server/repositories/psyche.js +84 -27
  45. package/dist/server/repositories/rewards.js +112 -4
  46. package/dist/server/repositories/strategies.js +450 -0
  47. package/dist/server/repositories/tags.js +11 -6
  48. package/dist/server/repositories/task-runs.js +10 -2
  49. package/dist/server/repositories/tasks.js +99 -17
  50. package/dist/server/repositories/users.js +417 -0
  51. package/dist/server/repositories/wiki-memory.js +3366 -0
  52. package/dist/server/services/context.js +20 -18
  53. package/dist/server/services/dashboard.js +29 -6
  54. package/dist/server/services/entity-crud.js +21 -3
  55. package/dist/server/services/insights.js +9 -7
  56. package/dist/server/services/projects.js +2 -1
  57. package/dist/server/services/psyche.js +10 -9
  58. package/dist/server/types.js +594 -30
  59. package/openclaw.plugin.json +1 -1
  60. package/package.json +1 -1
  61. package/server/migrations/015_multi_user_and_strategies.sql +244 -0
  62. package/server/migrations/016_health_companion.sql +158 -0
  63. package/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
  64. package/server/migrations/017_preferences.sql +131 -0
  65. package/server/migrations/018_preference_catalogs.sql +31 -0
  66. package/server/migrations/019_wiki_memory.sql +255 -0
  67. package/server/migrations/020_wiki_page_hierarchy.sql +11 -0
  68. package/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
  69. package/server/migrations/022_wiki_ingest_background.sql +85 -0
  70. package/server/migrations/023_diagnostic_logs.sql +28 -0
  71. package/skills/forge-openclaw/SKILL.md +126 -34
  72. package/skills/forge-openclaw/entity_conversation_playbooks.md +337 -0
  73. package/skills/forge-openclaw/psyche_entity_playbooks.md +404 -0
  74. package/dist/assets/board-8L3uX7_O.js +0 -6
  75. package/dist/assets/index-Cj1IBH_w.js +0 -36
  76. package/dist/assets/index-Cj1IBH_w.js.map +0 -1
  77. package/dist/assets/index-DQT6EbuS.css +0 -1
  78. package/dist/assets/vendor-BvM2F9Dp.js +0 -503
  79. package/dist/assets/vendor-BvM2F9Dp.js.map +0 -1
  80. package/dist/assets/vendor-CRS-psbw.css +0 -1
  81. package/dist/assets/viz-CNeunkfu.js +0 -34
  82. package/dist/assets/viz-CNeunkfu.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getDatabase, runInTransaction } from "../db.js";
3
3
  import { recordActivityEvent } from "./activity-events.js";
4
+ import { decorateOwnedEntity, setEntityOwner } from "./entity-ownership.js";
4
5
  import { filterDeletedEntities, filterDeletedIds, isEntityDeleted } from "./deleted-entities.js";
5
6
  import { createLinkedNotes } from "./notes.js";
6
7
  import { assertGoalRelations } from "../services/relations.js";
@@ -13,7 +14,7 @@ function readGoalTagIds(goalId) {
13
14
  return filterDeletedIds("tag", rows.map((row) => row.tag_id));
14
15
  }
15
16
  function mapGoal(row) {
16
- return goalSchema.parse({
17
+ return goalSchema.parse(decorateOwnedEntity("goal", {
17
18
  id: row.id,
18
19
  title: row.title,
19
20
  description: row.description,
@@ -24,7 +25,7 @@ function mapGoal(row) {
24
25
  createdAt: row.created_at,
25
26
  updatedAt: row.updated_at,
26
27
  tagIds: readGoalTagIds(row.id)
27
- });
28
+ }));
28
29
  }
29
30
  function replaceGoalTags(goalId, tagIds) {
30
31
  const database = getDatabase();
@@ -51,6 +52,7 @@ export function createGoal(input, activity) {
51
52
  .prepare(`INSERT INTO goals (id, title, description, horizon, status, target_points, theme_color, created_at, updated_at)
52
53
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
53
54
  .run(id, input.title, input.description, input.horizon, input.status, input.targetPoints, input.themeColor, now, now);
55
+ setEntityOwner("goal", id, input.userId);
54
56
  replaceGoalTags(id, input.tagIds);
55
57
  const goal = getGoalById(id);
56
58
  createLinkedNotes(input.notes, { entityType: "goal", entityId: goal.id, anchorKey: null }, activity ?? { source: "ui", actor: null });
@@ -92,6 +94,9 @@ export function updateGoal(goalId, input, activity) {
92
94
  WHERE id = ?`)
93
95
  .run(next.title, next.description, next.horizon, next.status, next.targetPoints, next.themeColor, next.updatedAt, goalId);
94
96
  replaceGoalTags(goalId, next.tagIds);
97
+ if (input.userId !== undefined) {
98
+ setEntityOwner("goal", goalId, input.userId);
99
+ }
95
100
  const goal = getGoalById(goalId);
96
101
  if (goal && activity) {
97
102
  const statusChanged = current.status !== goal.status;
@@ -1,29 +1,39 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getDatabase, runInTransaction } from "../db.js";
3
3
  import { HttpError } from "../errors.js";
4
+ import { createGeneratedWorkoutFromHabit, parseGeneratedHealthEventTemplate } from "../health.js";
5
+ import { decorateOwnedEntity, inferFirstOwnedUserId, setEntityOwner } from "./entity-ownership.js";
4
6
  import { getGoalById } from "./goals.js";
5
7
  import { getProjectById } from "./projects.js";
6
8
  import { getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getModeProfileById, getPsycheValueById, getTriggerReportById } from "./psyche.js";
7
9
  import { getTaskById } from "./tasks.js";
8
10
  import { recordActivityEvent } from "./activity-events.js";
9
- import { recordHabitCheckInReward } from "./rewards.js";
11
+ import { recordHabitCheckInReward, reverseLatestHabitCheckInReward } from "./rewards.js";
10
12
  import { createHabitCheckInSchema, createHabitSchema, habitCheckInSchema, habitSchema, updateHabitSchema } from "../types.js";
11
13
  function todayKey(now = new Date()) {
12
14
  return now.toISOString().slice(0, 10);
13
15
  }
14
16
  function parseWeekDays(raw) {
15
17
  const parsed = JSON.parse(raw);
16
- return Array.isArray(parsed) ? parsed.filter((value) => Number.isInteger(value) && value >= 0 && value <= 6) : [];
18
+ return Array.isArray(parsed)
19
+ ? parsed.filter((value) => Number.isInteger(value) && value >= 0 && value <= 6)
20
+ : [];
17
21
  }
18
22
  function parseIdList(raw) {
19
23
  const parsed = JSON.parse(raw);
20
- return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
24
+ return Array.isArray(parsed)
25
+ ? parsed.filter((value) => typeof value === "string" && value.trim().length > 0)
26
+ : [];
21
27
  }
22
28
  function uniqueIds(values) {
23
- return [...new Set(values.filter((value) => typeof value === "string" && value.trim().length > 0))];
29
+ return [
30
+ ...new Set(values.filter((value) => typeof value === "string" && value.trim().length > 0))
31
+ ];
24
32
  }
25
33
  function normalizeLinkedBehaviorIds(input) {
26
- const fromArray = Array.isArray(input.linkedBehaviorIds) ? input.linkedBehaviorIds : [];
34
+ const fromArray = Array.isArray(input.linkedBehaviorIds)
35
+ ? input.linkedBehaviorIds
36
+ : [];
27
37
  return uniqueIds([...fromArray, input.linkedBehaviorId ?? null]);
28
38
  }
29
39
  function validateExistingIds(ids, getById, code, label) {
@@ -56,7 +66,8 @@ function listCheckInsForHabit(habitId, limit = 14) {
56
66
  return rows.map(mapCheckIn);
57
67
  }
58
68
  function isAligned(habit, checkIn) {
59
- return (habit.polarity === "positive" && checkIn.status === "done") || (habit.polarity === "negative" && checkIn.status === "missed");
69
+ return ((habit.polarity === "positive" && checkIn.status === "done") ||
70
+ (habit.polarity === "negative" && checkIn.status === "missed"));
60
71
  }
61
72
  function calculateCompletionRate(habit, checkIns) {
62
73
  if (checkIns.length === 0) {
@@ -120,6 +131,7 @@ function mapHabit(row, checkIns = listCheckInsForHabit(row.id)) {
120
131
  linkedBehaviorTitles: linkedBehaviors.map((behavior) => behavior.title),
121
132
  rewardXp: row.reward_xp,
122
133
  penaltyXp: row.penalty_xp,
134
+ generatedHealthEventTemplate: parseGeneratedHealthEventTemplate(row.generated_health_event_template_json),
123
135
  createdAt: row.created_at,
124
136
  updatedAt: row.updated_at,
125
137
  lastCheckInAt: latestCheckIn?.createdAt ?? null,
@@ -129,8 +141,12 @@ function mapHabit(row, checkIns = listCheckInsForHabit(row.id)) {
129
141
  dueToday: false,
130
142
  checkIns
131
143
  };
132
- draft.dueToday = isHabitDueToday({ status: draft.status, frequency: draft.frequency, weekDays: draft.weekDays }, latestCheckIn);
133
- return habitSchema.parse(draft);
144
+ draft.dueToday = isHabitDueToday({
145
+ status: draft.status,
146
+ frequency: draft.frequency,
147
+ weekDays: draft.weekDays
148
+ }, latestCheckIn);
149
+ return habitSchema.parse(decorateOwnedEntity("habit", draft));
134
150
  }
135
151
  function getHabitRow(habitId) {
136
152
  return getDatabase()
@@ -139,7 +155,7 @@ function getHabitRow(habitId) {
139
155
  linked_goal_ids_json, linked_project_ids_json, linked_task_ids_json,
140
156
  linked_value_ids_json, linked_pattern_ids_json, linked_behavior_ids_json,
141
157
  linked_belief_ids_json, linked_mode_ids_json, linked_report_ids_json,
142
- linked_behavior_id, reward_xp, penalty_xp, created_at, updated_at
158
+ linked_behavior_id, reward_xp, penalty_xp, generated_health_event_template_json, created_at, updated_at
143
159
  FROM habits
144
160
  WHERE id = ?`)
145
161
  .get(habitId);
@@ -167,7 +183,7 @@ export function listHabits(filters = {}) {
167
183
  linked_goal_ids_json, linked_project_ids_json, linked_task_ids_json,
168
184
  linked_value_ids_json, linked_pattern_ids_json, linked_behavior_ids_json,
169
185
  linked_belief_ids_json, linked_mode_ids_json, linked_report_ids_json,
170
- linked_behavior_id, reward_xp, penalty_xp, created_at, updated_at
186
+ linked_behavior_id, reward_xp, penalty_xp, generated_health_event_template_json, created_at, updated_at
171
187
  FROM habits
172
188
  ${whereSql}
173
189
  ORDER BY
@@ -203,9 +219,24 @@ export function createHabit(input, activity) {
203
219
  linked_goal_ids_json, linked_project_ids_json, linked_task_ids_json,
204
220
  linked_value_ids_json, linked_pattern_ids_json, linked_behavior_ids_json,
205
221
  linked_belief_ids_json, linked_mode_ids_json, linked_report_ids_json,
206
- linked_behavior_id, reward_xp, penalty_xp, created_at, updated_at
207
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
208
- .run(id, parsed.title, parsed.description, parsed.status, parsed.polarity, parsed.frequency, parsed.targetCount, JSON.stringify(parsed.weekDays), JSON.stringify(parsed.linkedGoalIds), JSON.stringify(parsed.linkedProjectIds), JSON.stringify(parsed.linkedTaskIds), JSON.stringify(parsed.linkedValueIds), JSON.stringify(parsed.linkedPatternIds), JSON.stringify(linkedBehaviorIds), JSON.stringify(parsed.linkedBeliefIds), JSON.stringify(parsed.linkedModeIds), JSON.stringify(parsed.linkedReportIds), linkedBehaviorIds[0] ?? null, parsed.rewardXp, parsed.penaltyXp, now, now);
222
+ linked_behavior_id, reward_xp, penalty_xp, generated_health_event_template_json, created_at, updated_at
223
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
224
+ .run(id, parsed.title, parsed.description, parsed.status, parsed.polarity, parsed.frequency, parsed.targetCount, JSON.stringify(parsed.weekDays), JSON.stringify(parsed.linkedGoalIds), JSON.stringify(parsed.linkedProjectIds), JSON.stringify(parsed.linkedTaskIds), JSON.stringify(parsed.linkedValueIds), JSON.stringify(parsed.linkedPatternIds), JSON.stringify(linkedBehaviorIds), JSON.stringify(parsed.linkedBeliefIds), JSON.stringify(parsed.linkedModeIds), JSON.stringify(parsed.linkedReportIds), linkedBehaviorIds[0] ?? null, parsed.rewardXp, parsed.penaltyXp, JSON.stringify(parsed.generatedHealthEventTemplate), now, now);
225
+ setEntityOwner("habit", id, parsed.userId ??
226
+ inferFirstOwnedUserId([
227
+ ...parsed.linkedProjectIds.map((entityId) => ({
228
+ entityType: "project",
229
+ entityId
230
+ })),
231
+ ...parsed.linkedGoalIds.map((entityId) => ({
232
+ entityType: "goal",
233
+ entityId
234
+ })),
235
+ ...parsed.linkedTaskIds.map((entityId) => ({
236
+ entityType: "task",
237
+ entityId
238
+ }))
239
+ ]));
209
240
  const habit = getHabitById(id);
210
241
  if (activity) {
211
242
  recordActivityEvent({
@@ -232,7 +263,8 @@ export function updateHabit(habitId, input, activity) {
232
263
  return undefined;
233
264
  }
234
265
  const parsed = updateHabitSchema.parse(input);
235
- const nextLinkedBehaviorIds = parsed.linkedBehaviorIds !== undefined || parsed.linkedBehaviorId !== undefined
266
+ const nextLinkedBehaviorIds = parsed.linkedBehaviorIds !== undefined ||
267
+ parsed.linkedBehaviorId !== undefined
236
268
  ? normalizeLinkedBehaviorIds({
237
269
  linkedBehaviorIds: parsed.linkedBehaviorIds ?? current.linkedBehaviorIds,
238
270
  linkedBehaviorId: parsed.linkedBehaviorId === undefined
@@ -257,9 +289,13 @@ export function updateHabit(habitId, input, activity) {
257
289
  week_days_json = ?, linked_goal_ids_json = ?, linked_project_ids_json = ?, linked_task_ids_json = ?,
258
290
  linked_value_ids_json = ?, linked_pattern_ids_json = ?, linked_behavior_ids_json = ?,
259
291
  linked_belief_ids_json = ?, linked_mode_ids_json = ?, linked_report_ids_json = ?,
260
- linked_behavior_id = ?, reward_xp = ?, penalty_xp = ?, updated_at = ?
292
+ linked_behavior_id = ?, reward_xp = ?, penalty_xp = ?, generated_health_event_template_json = ?, updated_at = ?
261
293
  WHERE id = ?`)
262
- .run(parsed.title ?? current.title, parsed.description ?? current.description, parsed.status ?? current.status, parsed.polarity ?? current.polarity, parsed.frequency ?? current.frequency, parsed.targetCount ?? current.targetCount, JSON.stringify(parsed.weekDays ?? current.weekDays), JSON.stringify(parsed.linkedGoalIds ?? current.linkedGoalIds), JSON.stringify(parsed.linkedProjectIds ?? current.linkedProjectIds), JSON.stringify(parsed.linkedTaskIds ?? current.linkedTaskIds), JSON.stringify(parsed.linkedValueIds ?? current.linkedValueIds), JSON.stringify(parsed.linkedPatternIds ?? current.linkedPatternIds), JSON.stringify(nextLinkedBehaviorIds), JSON.stringify(parsed.linkedBeliefIds ?? current.linkedBeliefIds), JSON.stringify(parsed.linkedModeIds ?? current.linkedModeIds), JSON.stringify(parsed.linkedReportIds ?? current.linkedReportIds), nextLinkedBehaviorIds[0] ?? null, parsed.rewardXp ?? current.rewardXp, parsed.penaltyXp ?? current.penaltyXp, updatedAt, habitId);
294
+ .run(parsed.title ?? current.title, parsed.description ?? current.description, parsed.status ?? current.status, parsed.polarity ?? current.polarity, parsed.frequency ?? current.frequency, parsed.targetCount ?? current.targetCount, JSON.stringify(parsed.weekDays ?? current.weekDays), JSON.stringify(parsed.linkedGoalIds ?? current.linkedGoalIds), JSON.stringify(parsed.linkedProjectIds ?? current.linkedProjectIds), JSON.stringify(parsed.linkedTaskIds ?? current.linkedTaskIds), JSON.stringify(parsed.linkedValueIds ?? current.linkedValueIds), JSON.stringify(parsed.linkedPatternIds ?? current.linkedPatternIds), JSON.stringify(nextLinkedBehaviorIds), JSON.stringify(parsed.linkedBeliefIds ?? current.linkedBeliefIds), JSON.stringify(parsed.linkedModeIds ?? current.linkedModeIds), JSON.stringify(parsed.linkedReportIds ?? current.linkedReportIds), nextLinkedBehaviorIds[0] ?? null, parsed.rewardXp ?? current.rewardXp, parsed.penaltyXp ?? current.penaltyXp, JSON.stringify(parsed.generatedHealthEventTemplate ??
295
+ current.generatedHealthEventTemplate), updatedAt, habitId);
296
+ if (parsed.userId !== undefined) {
297
+ setEntityOwner("habit", habitId, parsed.userId);
298
+ }
263
299
  const habit = getHabitById(habitId);
264
300
  if (activity) {
265
301
  recordActivityEvent({
@@ -353,6 +389,76 @@ export function createHabitCheckIn(habitId, input, activity) {
353
389
  deltaXp: reward.deltaXp
354
390
  }
355
391
  });
392
+ if (parsed.status === "done") {
393
+ const checkInId = existing?.id
394
+ ? existing.id
395
+ : getDatabase()
396
+ .prepare(`SELECT id FROM habit_check_ins WHERE habit_id = ? AND date_key = ?`)
397
+ .get(habitId, parsed.dateKey)?.id;
398
+ if (checkInId) {
399
+ createGeneratedWorkoutFromHabit({
400
+ habitId: habit.id,
401
+ checkInId,
402
+ habitTitle: habit.title,
403
+ userId: habit.userId ?? "user_operator",
404
+ dateKey: parsed.dateKey,
405
+ template: habit.generatedHealthEventTemplate,
406
+ linkedEntities: [
407
+ ...habit.linkedGoalIds.map((entityId) => ({
408
+ entityType: "goal",
409
+ entityId,
410
+ relationshipType: "habit_context"
411
+ })),
412
+ ...habit.linkedProjectIds.map((entityId) => ({
413
+ entityType: "project",
414
+ entityId,
415
+ relationshipType: "habit_context"
416
+ })),
417
+ ...habit.linkedTaskIds.map((entityId) => ({
418
+ entityType: "task",
419
+ entityId,
420
+ relationshipType: "habit_context"
421
+ }))
422
+ ]
423
+ });
424
+ }
425
+ }
426
+ return getHabitById(habitId);
427
+ });
428
+ }
429
+ export function deleteHabitCheckIn(habitId, dateKey, activity) {
430
+ const habit = getHabitById(habitId);
431
+ if (!habit) {
432
+ return undefined;
433
+ }
434
+ return runInTransaction(() => {
435
+ const existing = getDatabase()
436
+ .prepare(`SELECT id, habit_id, date_key, status, note, delta_xp, created_at, updated_at
437
+ FROM habit_check_ins
438
+ WHERE habit_id = ? AND date_key = ?`)
439
+ .get(habitId, dateKey);
440
+ if (!existing) {
441
+ return getHabitById(habitId);
442
+ }
443
+ getDatabase()
444
+ .prepare(`DELETE FROM habit_check_ins WHERE id = ?`)
445
+ .run(existing.id);
446
+ reverseLatestHabitCheckInReward(habit, dateKey, activity ?? { source: "ui", actor: null });
447
+ recordActivityEvent({
448
+ entityType: "habit",
449
+ entityId: habit.id,
450
+ eventType: "habit_check_in_deleted",
451
+ title: `Habit entry removed: ${habit.title}`,
452
+ description: "Habit check-in removed from the timeline.",
453
+ actor: activity?.actor ?? null,
454
+ source: activity?.source ?? "ui",
455
+ metadata: {
456
+ dateKey,
457
+ status: existing.status,
458
+ polarity: habit.polarity,
459
+ deltaXp: existing.delta_xp
460
+ }
461
+ });
356
462
  return getHabitById(habitId);
357
463
  });
358
464
  }
@@ -1,9 +1,11 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getDatabase } from "../db.js";
3
3
  import { recordActivityEvent } from "./activity-events.js";
4
- import { filterDeletedEntities, getDeletedEntityRecord, clearDeletedEntityRecord, isEntityDeleted, upsertDeletedEntityRecord } from "./deleted-entities.js";
4
+ import { decorateOwnedEntity, setEntityOwner } from "./entity-ownership.js";
5
+ import { filterDeletedEntities, getDeletedEntityRecord, clearDeletedEntityRecord, isEntityDeleted } from "./deleted-entities.js";
5
6
  import { recordEventLog } from "./event-log.js";
6
7
  import { noteSchema, notesListQuerySchema, createNoteSchema, updateNoteSchema } from "../types.js";
8
+ import { deleteNoteWikiArtifacts, prepareNoteWikiFields, syncNoteWikiArtifacts } from "./wiki-memory.js";
7
9
  function normalizeAnchorKey(anchorKey) {
8
10
  return anchorKey.trim().length > 0 ? anchorKey : null;
9
11
  }
@@ -46,6 +48,31 @@ function parseTagsJson(raw) {
46
48
  return [];
47
49
  }
48
50
  }
51
+ function parseAliasesJson(raw) {
52
+ try {
53
+ const parsed = JSON.parse(raw);
54
+ return Array.isArray(parsed)
55
+ ? Array.from(new Set(parsed
56
+ .filter((value) => typeof value === "string")
57
+ .map((value) => value.trim())
58
+ .filter(Boolean)))
59
+ : [];
60
+ }
61
+ catch {
62
+ return [];
63
+ }
64
+ }
65
+ function parseFrontmatterJson(raw) {
66
+ try {
67
+ const parsed = JSON.parse(raw);
68
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
69
+ ? parsed
70
+ : {};
71
+ }
72
+ catch {
73
+ return {};
74
+ }
75
+ }
49
76
  function noteMatchesTextTerm(note, term) {
50
77
  const normalized = term.trim().toLowerCase();
51
78
  if (!normalized) {
@@ -102,7 +129,8 @@ function buildFtsQuery(query) {
102
129
  }
103
130
  function getNoteRow(noteId) {
104
131
  return getDatabase()
105
- .prepare(`SELECT id, content_markdown, content_plain, author, source, tags_json, destroy_at, created_at, updated_at
132
+ .prepare(`SELECT id, kind, title, slug, space_id, aliases_json, summary, content_markdown, content_plain, author, source, tags_json, destroy_at,
133
+ source_path, frontmatter_json, revision_hash, last_synced_at, parent_slug, index_order, show_in_index, created_at, updated_at
106
134
  FROM notes
107
135
  WHERE id = ?`)
108
136
  .get(noteId);
@@ -127,18 +155,31 @@ function mapLinks(rows) {
127
155
  }));
128
156
  }
129
157
  function mapNote(row, linkRows) {
130
- return noteSchema.parse({
158
+ return noteSchema.parse(decorateOwnedEntity("note", {
131
159
  id: row.id,
160
+ kind: row.kind,
161
+ title: row.title,
162
+ slug: row.slug,
163
+ spaceId: row.space_id,
164
+ parentSlug: row.parent_slug,
165
+ indexOrder: row.index_order,
166
+ showInIndex: row.show_in_index === 1,
167
+ aliases: parseAliasesJson(row.aliases_json),
168
+ summary: row.summary,
132
169
  contentMarkdown: row.content_markdown,
133
170
  contentPlain: row.content_plain,
134
171
  author: row.author,
135
172
  source: row.source,
173
+ sourcePath: row.source_path,
174
+ frontmatter: parseFrontmatterJson(row.frontmatter_json),
175
+ revisionHash: row.revision_hash,
176
+ lastSyncedAt: row.last_synced_at,
136
177
  tags: parseTagsJson(row.tags_json),
137
178
  destroyAt: row.destroy_at,
138
179
  createdAt: row.created_at,
139
180
  updatedAt: row.updated_at,
140
181
  links: mapLinks(linkRows)
141
- });
182
+ }));
142
183
  }
143
184
  function upsertSearchRow(noteId, contentPlain, author) {
144
185
  getDatabase().prepare(`DELETE FROM notes_fts WHERE note_id = ?`).run(noteId);
@@ -151,7 +192,8 @@ function deleteSearchRow(noteId) {
151
192
  }
152
193
  function listAllNoteRows() {
153
194
  return getDatabase()
154
- .prepare(`SELECT id, content_markdown, content_plain, author, source, tags_json, destroy_at, created_at, updated_at
195
+ .prepare(`SELECT id, kind, title, slug, space_id, aliases_json, summary, content_markdown, content_plain, author, source, tags_json, destroy_at,
196
+ source_path, frontmatter_json, revision_hash, last_synced_at, parent_slug, index_order, show_in_index, created_at, updated_at
155
197
  FROM notes
156
198
  ORDER BY created_at DESC`)
157
199
  .all();
@@ -279,6 +321,9 @@ export function listNotes(query = {}) {
279
321
  linksByNoteId.set(link.note_id, current);
280
322
  }
281
323
  return filterDeletedEntities("note", rows
324
+ .filter((row) => parsed.kind ? row.kind === parsed.kind : true)
325
+ .filter((row) => parsed.spaceId ? row.space_id === parsed.spaceId : true)
326
+ .filter((row) => parsed.slug ? row.slug.toLowerCase() === parsed.slug.toLowerCase() : true)
282
327
  .filter((row) => parsed.author
283
328
  ? (row.author ?? "")
284
329
  .toLowerCase()
@@ -327,28 +372,59 @@ export function createNote(input, context) {
327
372
  });
328
373
  const now = new Date().toISOString();
329
374
  const id = `note_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
375
+ const wikiFields = prepareNoteWikiFields({
376
+ id,
377
+ contentMarkdown: parsed.contentMarkdown,
378
+ kind: parsed.kind,
379
+ title: parsed.title,
380
+ slug: parsed.slug,
381
+ spaceId: parsed.spaceId,
382
+ parentSlug: parsed.parentSlug,
383
+ indexOrder: parsed.indexOrder,
384
+ showInIndex: parsed.showInIndex,
385
+ aliases: parsed.aliases,
386
+ summary: parsed.summary,
387
+ userId: parsed.userId ?? null
388
+ });
330
389
  const contentPlain = stripMarkdown(parsed.contentMarkdown);
331
390
  getDatabase()
332
- .prepare(`INSERT INTO notes (id, content_markdown, content_plain, author, source, tags_json, destroy_at, created_at, updated_at)
333
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
334
- .run(id, parsed.contentMarkdown, contentPlain, parsed.author ?? context.actor ?? null, context.source, JSON.stringify(parsed.tags), parsed.destroyAt, now, now);
391
+ .prepare(`INSERT INTO notes (
392
+ id, kind, title, slug, space_id, parent_slug, index_order, show_in_index, aliases_json, summary, content_markdown, content_plain, author, source, tags_json, destroy_at,
393
+ source_path, frontmatter_json, revision_hash, last_synced_at, created_at, updated_at
394
+ )
395
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
396
+ .run(id, wikiFields.kind, wikiFields.title, wikiFields.slug, wikiFields.spaceId, wikiFields.parentSlug, wikiFields.indexOrder, wikiFields.showInIndex ? 1 : 0, JSON.stringify(wikiFields.aliases), wikiFields.summary, parsed.contentMarkdown, contentPlain, parsed.author ?? context.actor ?? null, context.source, JSON.stringify(parsed.tags), parsed.destroyAt, parsed.sourcePath, JSON.stringify(parsed.frontmatter), parsed.revisionHash, parsed.lastSyncedAt ?? null, now, now);
335
397
  insertLinks(id, parsed.links, now);
398
+ setEntityOwner("note", id, parsed.userId, parsed.author ?? context.actor ?? null);
336
399
  clearDeletedEntityRecord("note", id);
337
400
  upsertSearchRow(id, contentPlain, parsed.author ?? context.actor ?? null);
338
401
  const note = getNoteById(id, { skipCleanup: true });
402
+ syncNoteWikiArtifacts(note);
339
403
  recordNoteActivity(note, "note.created", "Note added", context);
340
- return note;
404
+ return getNoteById(id, { skipCleanup: true });
341
405
  }
342
406
  export function createLinkedNotes(notes, entityLink, context) {
343
407
  if (!notes || notes.length === 0) {
344
408
  return [];
345
409
  }
346
410
  return notes.map((note) => createNote({
411
+ kind: "evidence",
412
+ title: "",
413
+ slug: "",
414
+ spaceId: "",
415
+ parentSlug: null,
416
+ indexOrder: 0,
417
+ showInIndex: false,
418
+ aliases: [],
419
+ summary: "",
347
420
  contentMarkdown: note.contentMarkdown,
348
421
  author: note.author,
349
422
  tags: note.tags,
350
423
  destroyAt: note.destroyAt,
351
- links: [entityLink, ...note.links]
424
+ links: [entityLink, ...note.links],
425
+ sourcePath: "",
426
+ frontmatter: {},
427
+ revisionHash: ""
352
428
  }, context));
353
429
  }
354
430
  export function updateNote(noteId, input, context) {
@@ -367,36 +443,50 @@ export function updateNote(noteId, input, context) {
367
443
  const nextAuthor = patch.author === undefined ? existing.author : patch.author;
368
444
  const nextTags = patch.tags ?? existing.tags;
369
445
  const nextDestroyAt = patch.destroyAt === undefined ? existing.destroyAt : patch.destroyAt;
446
+ const wikiFields = prepareNoteWikiFields({
447
+ id: noteId,
448
+ contentMarkdown: nextMarkdown,
449
+ kind: patch.kind ?? existing.kind,
450
+ title: patch.title,
451
+ slug: patch.slug,
452
+ spaceId: patch.spaceId,
453
+ parentSlug: patch.parentSlug,
454
+ indexOrder: patch.indexOrder,
455
+ showInIndex: patch.showInIndex,
456
+ aliases: patch.aliases,
457
+ summary: patch.summary,
458
+ userId: patch.userId ?? existing.userId ?? null,
459
+ existing
460
+ });
461
+ const nextFrontmatter = patch.frontmatter === undefined ? existing.frontmatter : patch.frontmatter;
462
+ const nextSourcePath = patch.sourcePath === undefined ? existing.sourcePath : patch.sourcePath;
463
+ const nextRevisionHash = patch.revisionHash === undefined
464
+ ? existing.revisionHash
465
+ : patch.revisionHash;
466
+ const nextLastSyncedAt = patch.lastSyncedAt === undefined
467
+ ? existing.lastSyncedAt
468
+ : patch.lastSyncedAt;
370
469
  const updatedAt = new Date().toISOString();
371
470
  getDatabase()
372
471
  .prepare(`UPDATE notes
373
- SET content_markdown = ?, content_plain = ?, author = ?, tags_json = ?, destroy_at = ?, updated_at = ?
472
+ SET kind = ?, title = ?, slug = ?, space_id = ?, parent_slug = ?, index_order = ?, show_in_index = ?, aliases_json = ?, summary = ?, content_markdown = ?, content_plain = ?, author = ?,
473
+ tags_json = ?, destroy_at = ?, source_path = ?, frontmatter_json = ?, revision_hash = ?, last_synced_at = ?, updated_at = ?
374
474
  WHERE id = ?`)
375
- .run(nextMarkdown, nextPlain, nextAuthor, JSON.stringify(nextTags), nextDestroyAt, updatedAt, noteId);
475
+ .run(wikiFields.kind, wikiFields.title, wikiFields.slug, wikiFields.spaceId, wikiFields.parentSlug, wikiFields.indexOrder, wikiFields.showInIndex ? 1 : 0, JSON.stringify(wikiFields.aliases), wikiFields.summary, nextMarkdown, nextPlain, nextAuthor, JSON.stringify(nextTags), nextDestroyAt, nextSourcePath, JSON.stringify(nextFrontmatter), nextRevisionHash, nextLastSyncedAt, updatedAt, noteId);
376
476
  if (patch.links) {
377
477
  replaceLinks(noteId, patch.links, updatedAt);
378
478
  }
379
- const note = getNoteByIdIncludingDeleted(noteId, { skipCleanup: true });
380
- if (note.links.length > 0) {
381
- clearDeletedEntityRecord("note", noteId);
382
- }
383
- else {
384
- const details = describeNote(note);
385
- upsertDeletedEntityRecord({
386
- entityType: "note",
387
- entityId: note.id,
388
- title: details.title,
389
- subtitle: details.subtitle,
390
- snapshot: note,
391
- deleteReason: "Note no longer has any linked entities.",
392
- context
393
- });
479
+ if (patch.userId !== undefined) {
480
+ setEntityOwner("note", noteId, patch.userId, nextAuthor ?? context.actor ?? null);
394
481
  }
482
+ const note = getNoteByIdIncludingDeleted(noteId, { skipCleanup: true });
483
+ clearDeletedEntityRecord("note", noteId);
395
484
  upsertSearchRow(noteId, nextPlain, nextAuthor);
396
485
  if (nextDestroyAt && Date.parse(nextDestroyAt) <= Date.now()) {
397
486
  deleteNoteInternal(noteId, { source: "system", actor: null }, "Ephemeral note expired");
398
487
  return undefined;
399
488
  }
489
+ syncNoteWikiArtifacts(note);
400
490
  recordNoteActivity(note, "note.updated", "Note updated", context);
401
491
  return getNoteById(noteId);
402
492
  }
@@ -411,6 +501,7 @@ function deleteNoteInternal(noteId, context, title) {
411
501
  getDatabase().prepare(`DELETE FROM note_links WHERE note_id = ?`).run(noteId);
412
502
  getDatabase().prepare(`DELETE FROM notes WHERE id = ?`).run(noteId);
413
503
  deleteSearchRow(noteId);
504
+ deleteNoteWikiArtifacts(existing);
414
505
  clearDeletedEntityRecord("note", noteId);
415
506
  recordNoteActivity(existing, "note.deleted", title, context);
416
507
  return existing;
@@ -474,19 +565,6 @@ export function unlinkNotesForEntity(entityType, entityId, context) {
474
565
  clearDeletedEntityRecord("note", row.note_id);
475
566
  continue;
476
567
  }
477
- const note = getNoteByIdIncludingDeleted(row.note_id);
478
- if (!note) {
479
- continue;
480
- }
481
- const details = describeNote(note);
482
- upsertDeletedEntityRecord({
483
- entityType: "note",
484
- entityId: note.id,
485
- title: details.title,
486
- subtitle: details.subtitle,
487
- snapshot: { ...note, links: [] },
488
- deleteReason: `All links were removed when ${entityType} ${entityId} was deleted.`,
489
- context
490
- });
568
+ clearDeletedEntityRecord("note", row.note_id);
491
569
  }
492
570
  }