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.
- package/README.md +39 -4
- package/dist/assets/{board-C_m78kvK.js → board-8L3uX7_O.js} +2 -2
- package/dist/assets/{board-C_m78kvK.js.map → board-8L3uX7_O.js.map} +1 -1
- package/dist/assets/index-Cj1IBH_w.js +36 -0
- package/dist/assets/index-Cj1IBH_w.js.map +1 -0
- package/dist/assets/index-DQT6EbuS.css +1 -0
- package/dist/assets/{motion-CpZvZumD.js → motion-1GAqqi8M.js} +2 -2
- package/dist/assets/{motion-CpZvZumD.js.map → motion-1GAqqi8M.js.map} +1 -1
- package/dist/assets/{table-DtyXTw03.js → table-DBGlgRjk.js} +2 -2
- package/dist/assets/{table-DtyXTw03.js.map → table-DBGlgRjk.js.map} +1 -1
- package/dist/assets/{ui-BXbpiKyS.js → ui-iTluWjC4.js} +2 -2
- package/dist/assets/{ui-BXbpiKyS.js.map → ui-iTluWjC4.js.map} +1 -1
- package/dist/assets/{vendor-QBH6qVEe.js → vendor-BvM2F9Dp.js} +151 -81
- package/dist/assets/vendor-BvM2F9Dp.js.map +1 -0
- package/dist/assets/{viz-w-IMeueL.js → viz-CNeunkfu.js} +2 -2
- package/dist/assets/{viz-w-IMeueL.js.map → viz-CNeunkfu.js.map} +1 -1
- package/dist/index.html +8 -8
- package/dist/openclaw/local-runtime.js +142 -9
- package/dist/openclaw/parity.js +1 -0
- package/dist/openclaw/plugin-entry-shared.js +7 -1
- package/dist/openclaw/routes.js +7 -0
- package/dist/openclaw/tools.js +198 -16
- package/dist/server/app.js +2615 -251
- package/dist/server/managers/platform/secrets-manager.js +44 -1
- package/dist/server/managers/runtime.js +3 -1
- package/dist/server/openapi.js +2212 -170
- package/dist/server/repositories/calendar.js +1101 -0
- package/dist/server/repositories/deleted-entities.js +10 -2
- package/dist/server/repositories/habits.js +358 -0
- package/dist/server/repositories/notes.js +161 -28
- package/dist/server/repositories/projects.js +45 -13
- package/dist/server/repositories/rewards.js +176 -6
- package/dist/server/repositories/settings.js +47 -5
- package/dist/server/repositories/task-runs.js +46 -10
- package/dist/server/repositories/tasks.js +25 -9
- package/dist/server/repositories/weekly-reviews.js +109 -0
- package/dist/server/repositories/work-adjustments.js +105 -0
- package/dist/server/services/calendar-runtime.js +1301 -0
- package/dist/server/services/context.js +16 -6
- package/dist/server/services/dashboard.js +6 -3
- package/dist/server/services/entity-crud.js +116 -3
- package/dist/server/services/gamification.js +66 -18
- package/dist/server/services/insights.js +2 -1
- package/dist/server/services/projects.js +32 -8
- package/dist/server/services/reviews.js +17 -2
- package/dist/server/services/work-time.js +27 -0
- package/dist/server/types.js +1069 -45
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/003_habits.sql +30 -0
- package/server/migrations/004_habit_links.sql +8 -0
- package/server/migrations/005_habit_psyche_links.sql +24 -0
- package/server/migrations/006_work_adjustments.sql +14 -0
- package/server/migrations/007_weekly_review_closures.sql +17 -0
- package/server/migrations/008_calendar_execution.sql +147 -0
- package/server/migrations/009_true_calendar_events.sql +195 -0
- package/server/migrations/010_calendar_selection_state.sql +6 -0
- package/server/migrations/011_calendar_timezone_backfill.sql +11 -0
- package/server/migrations/012_work_block_ranges.sql +7 -0
- package/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
- package/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
- package/skills/forge-openclaw/SKILL.md +130 -10
- package/skills/forge-openclaw/cron_jobs.md +395 -0
- package/dist/assets/index-BWtLtXwb.js +0 -36
- package/dist/assets/index-BWtLtXwb.js.map +0 -1
- package/dist/assets/index-Dp5GXY_z.css +0 -1
- 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)
|
|
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
|
|
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
|
+
}
|