forge-openclaw-plugin 0.2.15 → 0.2.19

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 (67) hide show
  1. package/README.md +39 -4
  2. package/dist/assets/{board-C_m78kvK.js → board-8L3uX7_O.js} +2 -2
  3. package/dist/assets/{board-C_m78kvK.js.map → board-8L3uX7_O.js.map} +1 -1
  4. package/dist/assets/index-Cj1IBH_w.js +36 -0
  5. package/dist/assets/index-Cj1IBH_w.js.map +1 -0
  6. package/dist/assets/index-DQT6EbuS.css +1 -0
  7. package/dist/assets/{motion-CpZvZumD.js → motion-1GAqqi8M.js} +2 -2
  8. package/dist/assets/{motion-CpZvZumD.js.map → motion-1GAqqi8M.js.map} +1 -1
  9. package/dist/assets/{table-DtyXTw03.js → table-DBGlgRjk.js} +2 -2
  10. package/dist/assets/{table-DtyXTw03.js.map → table-DBGlgRjk.js.map} +1 -1
  11. package/dist/assets/{ui-BXbpiKyS.js → ui-iTluWjC4.js} +2 -2
  12. package/dist/assets/{ui-BXbpiKyS.js.map → ui-iTluWjC4.js.map} +1 -1
  13. package/dist/assets/{vendor-QBH6qVEe.js → vendor-BvM2F9Dp.js} +151 -81
  14. package/dist/assets/vendor-BvM2F9Dp.js.map +1 -0
  15. package/dist/assets/{viz-w-IMeueL.js → viz-CNeunkfu.js} +2 -2
  16. package/dist/assets/{viz-w-IMeueL.js.map → viz-CNeunkfu.js.map} +1 -1
  17. package/dist/index.html +8 -8
  18. package/dist/openclaw/local-runtime.js +142 -9
  19. package/dist/openclaw/parity.js +1 -0
  20. package/dist/openclaw/plugin-entry-shared.js +7 -1
  21. package/dist/openclaw/routes.js +7 -0
  22. package/dist/openclaw/tools.js +198 -16
  23. package/dist/server/app.js +2615 -251
  24. package/dist/server/managers/platform/secrets-manager.js +44 -1
  25. package/dist/server/managers/runtime.js +3 -1
  26. package/dist/server/openapi.js +2212 -170
  27. package/dist/server/repositories/calendar.js +1101 -0
  28. package/dist/server/repositories/deleted-entities.js +10 -2
  29. package/dist/server/repositories/habits.js +358 -0
  30. package/dist/server/repositories/notes.js +161 -28
  31. package/dist/server/repositories/projects.js +45 -13
  32. package/dist/server/repositories/rewards.js +176 -6
  33. package/dist/server/repositories/settings.js +47 -5
  34. package/dist/server/repositories/task-runs.js +46 -10
  35. package/dist/server/repositories/tasks.js +25 -9
  36. package/dist/server/repositories/weekly-reviews.js +109 -0
  37. package/dist/server/repositories/work-adjustments.js +105 -0
  38. package/dist/server/services/calendar-runtime.js +1301 -0
  39. package/dist/server/services/context.js +16 -6
  40. package/dist/server/services/dashboard.js +6 -3
  41. package/dist/server/services/entity-crud.js +116 -3
  42. package/dist/server/services/gamification.js +66 -18
  43. package/dist/server/services/insights.js +2 -1
  44. package/dist/server/services/projects.js +32 -8
  45. package/dist/server/services/reviews.js +17 -2
  46. package/dist/server/services/work-time.js +27 -0
  47. package/dist/server/types.js +1069 -45
  48. package/openclaw.plugin.json +1 -1
  49. package/package.json +1 -1
  50. package/server/migrations/003_habits.sql +30 -0
  51. package/server/migrations/004_habit_links.sql +8 -0
  52. package/server/migrations/005_habit_psyche_links.sql +24 -0
  53. package/server/migrations/006_work_adjustments.sql +14 -0
  54. package/server/migrations/007_weekly_review_closures.sql +17 -0
  55. package/server/migrations/008_calendar_execution.sql +147 -0
  56. package/server/migrations/009_true_calendar_events.sql +195 -0
  57. package/server/migrations/010_calendar_selection_state.sql +6 -0
  58. package/server/migrations/011_calendar_timezone_backfill.sql +11 -0
  59. package/server/migrations/012_work_block_ranges.sql +7 -0
  60. package/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
  61. package/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
  62. package/skills/forge-openclaw/SKILL.md +130 -10
  63. package/skills/forge-openclaw/cron_jobs.md +395 -0
  64. package/dist/assets/index-BWtLtXwb.js +0 -36
  65. package/dist/assets/index-BWtLtXwb.js.map +0 -1
  66. package/dist/assets/index-Dp5GXY_z.css +0 -1
  67. package/dist/assets/vendor-QBH6qVEe.js.map +0 -1
@@ -114,6 +114,8 @@ export function cascadeSoftDeleteAnchoredCollaboration(parentEntityType, parentE
114
114
  notes.content_plain AS content_plain,
115
115
  notes.author AS author,
116
116
  notes.source AS source,
117
+ notes.tags_json AS tags_json,
118
+ notes.destroy_at AS destroy_at,
117
119
  notes.created_at AS created_at,
118
120
  notes.updated_at AS updated_at
119
121
  FROM notes
@@ -145,18 +147,24 @@ export function cascadeSoftDeleteAnchoredCollaboration(parentEntityType, parentE
145
147
  linksByNoteId.set(link.note_id, current);
146
148
  }
147
149
  for (const row of noteRows) {
148
- const compact = (row.content_plain || row.content_markdown).replace(/\s+/g, " ").trim();
150
+ const compact = (row.content_plain || row.content_markdown)
151
+ .replace(/\s+/g, " ")
152
+ .trim();
149
153
  upsertDeletedEntityRecord({
150
154
  entityType: "note",
151
155
  entityId: row.id,
152
156
  title: compact.slice(0, 72) || "Note",
153
- subtitle: compact.length > 72 ? compact.slice(72, 168).trim() : `Linked to ${parentEntityType.replaceAll("_", " ")}`,
157
+ subtitle: compact.length > 72
158
+ ? compact.slice(72, 168).trim()
159
+ : `Linked to ${parentEntityType.replaceAll("_", " ")}`,
154
160
  snapshot: {
155
161
  id: row.id,
156
162
  contentMarkdown: row.content_markdown,
157
163
  contentPlain: row.content_plain,
158
164
  author: row.author,
159
165
  source: row.source,
166
+ tags: JSON.parse(row.tags_json),
167
+ destroyAt: row.destroy_at,
160
168
  createdAt: row.created_at,
161
169
  updatedAt: row.updated_at,
162
170
  links: linksByNoteId.get(row.id) ?? []
@@ -0,0 +1,358 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDatabase, runInTransaction } from "../db.js";
3
+ import { HttpError } from "../errors.js";
4
+ import { getGoalById } from "./goals.js";
5
+ import { getProjectById } from "./projects.js";
6
+ import { getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getModeProfileById, getPsycheValueById, getTriggerReportById } from "./psyche.js";
7
+ import { getTaskById } from "./tasks.js";
8
+ import { recordActivityEvent } from "./activity-events.js";
9
+ import { recordHabitCheckInReward } from "./rewards.js";
10
+ import { createHabitCheckInSchema, createHabitSchema, habitCheckInSchema, habitSchema, updateHabitSchema } from "../types.js";
11
+ function todayKey(now = new Date()) {
12
+ return now.toISOString().slice(0, 10);
13
+ }
14
+ function parseWeekDays(raw) {
15
+ const parsed = JSON.parse(raw);
16
+ return Array.isArray(parsed) ? parsed.filter((value) => Number.isInteger(value) && value >= 0 && value <= 6) : [];
17
+ }
18
+ function parseIdList(raw) {
19
+ const parsed = JSON.parse(raw);
20
+ return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
21
+ }
22
+ function uniqueIds(values) {
23
+ return [...new Set(values.filter((value) => typeof value === "string" && value.trim().length > 0))];
24
+ }
25
+ function normalizeLinkedBehaviorIds(input) {
26
+ const fromArray = Array.isArray(input.linkedBehaviorIds) ? input.linkedBehaviorIds : [];
27
+ return uniqueIds([...fromArray, input.linkedBehaviorId ?? null]);
28
+ }
29
+ function validateExistingIds(ids, getById, code, label) {
30
+ for (const id of ids) {
31
+ if (!getById(id)) {
32
+ throw new HttpError(404, code, `${label} ${id} does not exist`);
33
+ }
34
+ }
35
+ }
36
+ function mapCheckIn(row) {
37
+ return habitCheckInSchema.parse({
38
+ id: row.id,
39
+ habitId: row.habit_id,
40
+ dateKey: row.date_key,
41
+ status: row.status,
42
+ note: row.note,
43
+ deltaXp: row.delta_xp,
44
+ createdAt: row.created_at,
45
+ updatedAt: row.updated_at
46
+ });
47
+ }
48
+ function listCheckInsForHabit(habitId, limit = 14) {
49
+ const rows = getDatabase()
50
+ .prepare(`SELECT id, habit_id, date_key, status, note, delta_xp, created_at, updated_at
51
+ FROM habit_check_ins
52
+ WHERE habit_id = ?
53
+ ORDER BY date_key DESC, created_at DESC
54
+ LIMIT ?`)
55
+ .all(habitId, limit);
56
+ return rows.map(mapCheckIn);
57
+ }
58
+ function isAligned(habit, checkIn) {
59
+ return (habit.polarity === "positive" && checkIn.status === "done") || (habit.polarity === "negative" && checkIn.status === "missed");
60
+ }
61
+ function calculateCompletionRate(habit, checkIns) {
62
+ if (checkIns.length === 0) {
63
+ return 0;
64
+ }
65
+ const aligned = checkIns.filter((checkIn) => isAligned(habit, checkIn)).length;
66
+ return Math.round((aligned / checkIns.length) * 100);
67
+ }
68
+ function calculateStreak(habit, checkIns) {
69
+ let streak = 0;
70
+ for (const checkIn of checkIns) {
71
+ if (!isAligned(habit, checkIn)) {
72
+ break;
73
+ }
74
+ streak += 1;
75
+ }
76
+ return streak;
77
+ }
78
+ function isHabitDueToday(habit, latestCheckIn, now = new Date()) {
79
+ if (habit.status !== "active") {
80
+ return false;
81
+ }
82
+ const key = todayKey(now);
83
+ if (latestCheckIn?.dateKey === key) {
84
+ return false;
85
+ }
86
+ if (habit.frequency === "daily") {
87
+ return true;
88
+ }
89
+ return habit.weekDays.includes(now.getUTCDay());
90
+ }
91
+ function mapHabit(row, checkIns = listCheckInsForHabit(row.id)) {
92
+ const latestCheckIn = checkIns[0] ?? null;
93
+ const linkedBehaviorIds = normalizeLinkedBehaviorIds({
94
+ linkedBehaviorIds: parseIdList(row.linked_behavior_ids_json),
95
+ linkedBehaviorId: row.linked_behavior_id
96
+ });
97
+ const linkedBehaviors = linkedBehaviorIds
98
+ .map((behaviorId) => getBehaviorById(behaviorId))
99
+ .filter((behavior) => behavior !== undefined);
100
+ const draft = {
101
+ id: row.id,
102
+ title: row.title,
103
+ description: row.description,
104
+ status: row.status,
105
+ polarity: row.polarity,
106
+ frequency: row.frequency,
107
+ targetCount: row.target_count,
108
+ weekDays: parseWeekDays(row.week_days_json),
109
+ linkedGoalIds: parseIdList(row.linked_goal_ids_json),
110
+ linkedProjectIds: parseIdList(row.linked_project_ids_json),
111
+ linkedTaskIds: parseIdList(row.linked_task_ids_json),
112
+ linkedValueIds: parseIdList(row.linked_value_ids_json),
113
+ linkedPatternIds: parseIdList(row.linked_pattern_ids_json),
114
+ linkedBehaviorIds,
115
+ linkedBeliefIds: parseIdList(row.linked_belief_ids_json),
116
+ linkedModeIds: parseIdList(row.linked_mode_ids_json),
117
+ linkedReportIds: parseIdList(row.linked_report_ids_json),
118
+ linkedBehaviorId: linkedBehaviorIds[0] ?? null,
119
+ linkedBehaviorTitle: linkedBehaviors[0]?.title ?? null,
120
+ linkedBehaviorTitles: linkedBehaviors.map((behavior) => behavior.title),
121
+ rewardXp: row.reward_xp,
122
+ penaltyXp: row.penalty_xp,
123
+ createdAt: row.created_at,
124
+ updatedAt: row.updated_at,
125
+ lastCheckInAt: latestCheckIn?.createdAt ?? null,
126
+ lastCheckInStatus: latestCheckIn?.status ?? null,
127
+ streakCount: calculateStreak({ polarity: row.polarity }, checkIns),
128
+ completionRate: calculateCompletionRate({ polarity: row.polarity }, checkIns),
129
+ dueToday: false,
130
+ checkIns
131
+ };
132
+ draft.dueToday = isHabitDueToday({ status: draft.status, frequency: draft.frequency, weekDays: draft.weekDays }, latestCheckIn);
133
+ return habitSchema.parse(draft);
134
+ }
135
+ function getHabitRow(habitId) {
136
+ return getDatabase()
137
+ .prepare(`SELECT
138
+ id, title, description, status, polarity, frequency, target_count, week_days_json,
139
+ linked_goal_ids_json, linked_project_ids_json, linked_task_ids_json,
140
+ linked_value_ids_json, linked_pattern_ids_json, linked_behavior_ids_json,
141
+ linked_belief_ids_json, linked_mode_ids_json, linked_report_ids_json,
142
+ linked_behavior_id, reward_xp, penalty_xp, created_at, updated_at
143
+ FROM habits
144
+ WHERE id = ?`)
145
+ .get(habitId);
146
+ }
147
+ export function listHabits(filters = {}) {
148
+ const parsed = filters;
149
+ const whereClauses = [];
150
+ const params = [];
151
+ if (parsed.status) {
152
+ whereClauses.push("status = ?");
153
+ params.push(parsed.status);
154
+ }
155
+ if (parsed.polarity) {
156
+ whereClauses.push("polarity = ?");
157
+ params.push(parsed.polarity);
158
+ }
159
+ const limitSql = parsed.limit ? "LIMIT ?" : "";
160
+ if (parsed.limit) {
161
+ params.push(parsed.limit);
162
+ }
163
+ const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
164
+ const rows = getDatabase()
165
+ .prepare(`SELECT
166
+ id, title, description, status, polarity, frequency, target_count, week_days_json,
167
+ linked_goal_ids_json, linked_project_ids_json, linked_task_ids_json,
168
+ linked_value_ids_json, linked_pattern_ids_json, linked_behavior_ids_json,
169
+ linked_belief_ids_json, linked_mode_ids_json, linked_report_ids_json,
170
+ linked_behavior_id, reward_xp, penalty_xp, created_at, updated_at
171
+ FROM habits
172
+ ${whereSql}
173
+ ORDER BY
174
+ CASE status WHEN 'active' THEN 0 WHEN 'paused' THEN 1 ELSE 2 END,
175
+ updated_at DESC
176
+ ${limitSql}`)
177
+ .all(...params);
178
+ const habits = rows.map((row) => mapHabit(row));
179
+ return parsed.dueToday ? habits.filter((habit) => habit.dueToday) : habits;
180
+ }
181
+ export function getHabitById(habitId) {
182
+ const row = getHabitRow(habitId);
183
+ return row ? mapHabit(row) : undefined;
184
+ }
185
+ export function createHabit(input, activity) {
186
+ const parsed = createHabitSchema.parse(input);
187
+ const linkedBehaviorIds = normalizeLinkedBehaviorIds(parsed);
188
+ validateExistingIds(parsed.linkedGoalIds, getGoalById, "goal_not_found", "Goal");
189
+ validateExistingIds(parsed.linkedProjectIds, getProjectById, "project_not_found", "Project");
190
+ validateExistingIds(parsed.linkedTaskIds, getTaskById, "task_not_found", "Task");
191
+ validateExistingIds(parsed.linkedValueIds, getPsycheValueById, "value_not_found", "Value");
192
+ validateExistingIds(parsed.linkedPatternIds, getBehaviorPatternById, "pattern_not_found", "Pattern");
193
+ validateExistingIds(linkedBehaviorIds, getBehaviorById, "behavior_not_found", "Behavior");
194
+ validateExistingIds(parsed.linkedBeliefIds, getBeliefEntryById, "belief_not_found", "Belief");
195
+ validateExistingIds(parsed.linkedModeIds, getModeProfileById, "mode_not_found", "Mode");
196
+ validateExistingIds(parsed.linkedReportIds, getTriggerReportById, "report_not_found", "Report");
197
+ return runInTransaction(() => {
198
+ const now = new Date().toISOString();
199
+ const id = `habit_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
200
+ getDatabase()
201
+ .prepare(`INSERT INTO habits (
202
+ id, title, description, status, polarity, frequency, target_count, week_days_json,
203
+ linked_goal_ids_json, linked_project_ids_json, linked_task_ids_json,
204
+ linked_value_ids_json, linked_pattern_ids_json, linked_behavior_ids_json,
205
+ 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);
209
+ const habit = getHabitById(id);
210
+ if (activity) {
211
+ recordActivityEvent({
212
+ entityType: "habit",
213
+ entityId: habit.id,
214
+ eventType: "habit_created",
215
+ title: `Habit created: ${habit.title}`,
216
+ description: `${habit.frequency === "daily" ? "Daily" : "Weekly"} ${habit.polarity} habit added to Forge.`,
217
+ actor: activity.actor ?? null,
218
+ source: activity.source,
219
+ metadata: {
220
+ polarity: habit.polarity,
221
+ frequency: habit.frequency,
222
+ targetCount: habit.targetCount
223
+ }
224
+ });
225
+ }
226
+ return habit;
227
+ });
228
+ }
229
+ export function updateHabit(habitId, input, activity) {
230
+ const current = getHabitById(habitId);
231
+ if (!current) {
232
+ return undefined;
233
+ }
234
+ const parsed = updateHabitSchema.parse(input);
235
+ const nextLinkedBehaviorIds = parsed.linkedBehaviorIds !== undefined || parsed.linkedBehaviorId !== undefined
236
+ ? normalizeLinkedBehaviorIds({
237
+ linkedBehaviorIds: parsed.linkedBehaviorIds ?? current.linkedBehaviorIds,
238
+ linkedBehaviorId: parsed.linkedBehaviorId === undefined
239
+ ? current.linkedBehaviorId
240
+ : parsed.linkedBehaviorId
241
+ })
242
+ : current.linkedBehaviorIds;
243
+ validateExistingIds(parsed.linkedGoalIds ?? current.linkedGoalIds, getGoalById, "goal_not_found", "Goal");
244
+ validateExistingIds(parsed.linkedProjectIds ?? current.linkedProjectIds, getProjectById, "project_not_found", "Project");
245
+ validateExistingIds(parsed.linkedTaskIds ?? current.linkedTaskIds, getTaskById, "task_not_found", "Task");
246
+ validateExistingIds(parsed.linkedValueIds ?? current.linkedValueIds, getPsycheValueById, "value_not_found", "Value");
247
+ validateExistingIds(parsed.linkedPatternIds ?? current.linkedPatternIds, getBehaviorPatternById, "pattern_not_found", "Pattern");
248
+ validateExistingIds(nextLinkedBehaviorIds, getBehaviorById, "behavior_not_found", "Behavior");
249
+ validateExistingIds(parsed.linkedBeliefIds ?? current.linkedBeliefIds, getBeliefEntryById, "belief_not_found", "Belief");
250
+ validateExistingIds(parsed.linkedModeIds ?? current.linkedModeIds, getModeProfileById, "mode_not_found", "Mode");
251
+ validateExistingIds(parsed.linkedReportIds ?? current.linkedReportIds, getTriggerReportById, "report_not_found", "Report");
252
+ return runInTransaction(() => {
253
+ const updatedAt = new Date().toISOString();
254
+ getDatabase()
255
+ .prepare(`UPDATE habits
256
+ SET title = ?, description = ?, status = ?, polarity = ?, frequency = ?, target_count = ?,
257
+ week_days_json = ?, linked_goal_ids_json = ?, linked_project_ids_json = ?, linked_task_ids_json = ?,
258
+ linked_value_ids_json = ?, linked_pattern_ids_json = ?, linked_behavior_ids_json = ?,
259
+ linked_belief_ids_json = ?, linked_mode_ids_json = ?, linked_report_ids_json = ?,
260
+ linked_behavior_id = ?, reward_xp = ?, penalty_xp = ?, updated_at = ?
261
+ 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);
263
+ const habit = getHabitById(habitId);
264
+ if (activity) {
265
+ recordActivityEvent({
266
+ entityType: "habit",
267
+ entityId: habit.id,
268
+ eventType: "habit_updated",
269
+ title: `Habit updated: ${habit.title}`,
270
+ description: "Habit settings and recurrence were updated.",
271
+ actor: activity.actor ?? null,
272
+ source: activity.source,
273
+ metadata: {
274
+ polarity: habit.polarity,
275
+ frequency: habit.frequency,
276
+ targetCount: habit.targetCount
277
+ }
278
+ });
279
+ }
280
+ return habit;
281
+ });
282
+ }
283
+ export function deleteHabit(habitId, activity) {
284
+ const current = getHabitById(habitId);
285
+ if (!current) {
286
+ return undefined;
287
+ }
288
+ return runInTransaction(() => {
289
+ getDatabase().prepare(`DELETE FROM habits WHERE id = ?`).run(habitId);
290
+ if (activity) {
291
+ recordActivityEvent({
292
+ entityType: "habit",
293
+ entityId: current.id,
294
+ eventType: "habit_deleted",
295
+ title: `Habit deleted: ${current.title}`,
296
+ description: "Habit removed from Forge.",
297
+ actor: activity.actor ?? null,
298
+ source: activity.source,
299
+ metadata: {
300
+ polarity: current.polarity,
301
+ frequency: current.frequency
302
+ }
303
+ });
304
+ }
305
+ return current;
306
+ });
307
+ }
308
+ export function createHabitCheckIn(habitId, input, activity) {
309
+ const habit = getHabitById(habitId);
310
+ if (!habit) {
311
+ return undefined;
312
+ }
313
+ const parsed = createHabitCheckInSchema.parse(input);
314
+ return runInTransaction(() => {
315
+ const existing = getDatabase()
316
+ .prepare(`SELECT id, habit_id, date_key, status, note, delta_xp, created_at, updated_at
317
+ FROM habit_check_ins
318
+ WHERE habit_id = ? AND date_key = ?`)
319
+ .get(habitId, parsed.dateKey);
320
+ const reward = recordHabitCheckInReward(habit, parsed.status, parsed.dateKey, activity ?? { source: "ui", actor: null });
321
+ const now = new Date().toISOString();
322
+ if (existing) {
323
+ getDatabase()
324
+ .prepare(`UPDATE habit_check_ins
325
+ SET status = ?, note = ?, delta_xp = ?, updated_at = ?
326
+ WHERE id = ?`)
327
+ .run(parsed.status, parsed.note, reward.deltaXp, now, existing.id);
328
+ }
329
+ else {
330
+ getDatabase()
331
+ .prepare(`INSERT INTO habit_check_ins (id, habit_id, date_key, status, note, delta_xp, created_at, updated_at)
332
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
333
+ .run(`hci_${randomUUID().replaceAll("-", "").slice(0, 10)}`, habitId, parsed.dateKey, parsed.status, parsed.note, reward.deltaXp, now, now);
334
+ }
335
+ recordActivityEvent({
336
+ entityType: "habit",
337
+ entityId: habit.id,
338
+ eventType: parsed.status === "done" ? "habit_done" : "habit_missed",
339
+ title: `${parsed.status === "done" ? "Habit completed" : "Habit missed"}: ${habit.title}`,
340
+ description: habit.polarity === "positive"
341
+ ? parsed.status === "done"
342
+ ? "Positive habit logged as completed."
343
+ : "Positive habit logged as missed."
344
+ : parsed.status === "done"
345
+ ? "Negative habit logged as performed."
346
+ : "Negative habit logged as resisted.",
347
+ actor: activity?.actor ?? null,
348
+ source: activity?.source ?? "ui",
349
+ metadata: {
350
+ dateKey: parsed.dateKey,
351
+ status: parsed.status,
352
+ polarity: habit.polarity,
353
+ deltaXp: reward.deltaXp
354
+ }
355
+ });
356
+ return getHabitById(habitId);
357
+ });
358
+ }