forge-openclaw-plugin 0.2.13 → 0.2.18
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 +8 -5
- package/dist/assets/{board-C_m78kvK.js → board-2KevHCI0.js} +2 -2
- package/dist/assets/{board-C_m78kvK.js.map → board-2KevHCI0.js.map} +1 -1
- package/dist/assets/index-CDYW4WDH.js +36 -0
- package/dist/assets/index-CDYW4WDH.js.map +1 -0
- package/dist/assets/index-yroQr6YZ.css +1 -0
- package/dist/assets/{motion-CpZvZumD.js → motion-q19HPmWs.js} +2 -2
- package/dist/assets/{motion-CpZvZumD.js.map → motion-q19HPmWs.js.map} +1 -1
- package/dist/assets/{table-DtyXTw03.js → table-BDMHBY4a.js} +2 -2
- package/dist/assets/{table-DtyXTw03.js.map → table-BDMHBY4a.js.map} +1 -1
- package/dist/assets/{ui-BXbpiKyS.js → ui-CQ_AsFs8.js} +2 -2
- package/dist/assets/{ui-BXbpiKyS.js.map → ui-CQ_AsFs8.js.map} +1 -1
- package/dist/assets/{vendor-QBH6qVEe.js → vendor-5HifrnRK.js} +90 -75
- package/dist/assets/{vendor-QBH6qVEe.js.map → vendor-5HifrnRK.js.map} +1 -1
- package/dist/assets/{viz-w-IMeueL.js → viz-CQzkRnTu.js} +2 -2
- package/dist/assets/{viz-w-IMeueL.js.map → viz-CQzkRnTu.js.map} +1 -1
- package/dist/index.html +8 -8
- package/dist/openclaw/api-client.d.ts +1 -0
- package/dist/openclaw/local-runtime.js +243 -15
- package/dist/openclaw/plugin-entry-shared.js +45 -4
- package/dist/openclaw/tools.js +15 -0
- package/dist/server/app.js +129 -11
- package/dist/server/openapi.js +181 -4
- package/dist/server/repositories/habits.js +358 -0
- package/dist/server/repositories/rewards.js +62 -0
- package/dist/server/services/context.js +16 -6
- package/dist/server/services/dashboard.js +6 -3
- package/dist/server/services/entity-crud.js +23 -1
- package/dist/server/services/gamification.js +66 -18
- package/dist/server/services/insights.js +2 -1
- package/dist/server/services/reviews.js +2 -1
- package/dist/server/types.js +140 -1
- 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/skills/forge-openclaw/SKILL.md +16 -2
- 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/server/app.js
CHANGED
|
@@ -7,6 +7,7 @@ import { listActivityEvents, listActivityEventsForTask, removeActivityEvent } fr
|
|
|
7
7
|
import { approveApprovalRequest, createAgentAction, createInsight, createInsightFeedback, deleteInsight, getInsightById, listAgentActions, listApprovalRequests, listInsights, rejectApprovalRequest, updateInsight } from "./repositories/collaboration.js";
|
|
8
8
|
import { listEventLog } from "./repositories/event-log.js";
|
|
9
9
|
import { createGoal, getGoalById, listGoals, updateGoal } from "./repositories/goals.js";
|
|
10
|
+
import { createHabit, createHabitCheckIn, getHabitById, listHabits, updateHabit } from "./repositories/habits.js";
|
|
10
11
|
import { listDomains } from "./repositories/domains.js";
|
|
11
12
|
import { buildNotesSummaryByEntity, createNote, getNoteById, listNotes, updateNote } from "./repositories/notes.js";
|
|
12
13
|
import { createBehavior, createBehaviorPattern, createBeliefEntry, createEmotionDefinition, createEventType, createModeGuideSession, createModeProfile, createPsycheValue, createTriggerReport, getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getEmotionDefinitionById, getEventTypeById, getModeGuideSessionById, getModeProfileById, getPsycheValueById, getTriggerReportById, listBehaviors, listBehaviorPatterns, listBeliefEntries, listEmotionDefinitions, listEventTypes, listModeGuideSessions, listModeProfiles, listPsycheValues, listSchemaCatalog, listTriggerReports, updateBehavior, updateBehaviorPattern, updateBeliefEntry, updateEmotionDefinition, updateEventType, updateModeGuideSession, updateModeProfile, updatePsycheValue, updateTriggerReport } from "./repositories/psyche.js";
|
|
@@ -27,7 +28,7 @@ import { getWeeklyReviewPayload } from "./services/reviews.js";
|
|
|
27
28
|
import { createTaskRunWatchdog } from "./services/task-run-watchdog.js";
|
|
28
29
|
import { suggestTags } from "./services/tagging.js";
|
|
29
30
|
import { PSYCHE_ENTITY_TYPES, createBehaviorSchema, createBeliefEntrySchema, createBehaviorPatternSchema, createEmotionDefinitionSchema, createEventTypeSchema, createModeGuideSessionSchema, createModeProfileSchema, createPsycheValueSchema, createTriggerReportSchema, updateBehaviorSchema, updateBeliefEntrySchema, updateBehaviorPatternSchema, updateEmotionDefinitionSchema, updateEventTypeSchema, updateModeGuideSessionSchema, updateModeProfileSchema, updatePsycheValueSchema, updateTriggerReportSchema } from "./psyche-types.js";
|
|
30
|
-
import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createSessionEventSchema, createTagSchema, notesListQuerySchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateInsightSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskSchema } from "./types.js";
|
|
31
|
+
import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createHabitCheckInSchema, createHabitSchema, createSessionEventSchema, createTagSchema, notesListQuerySchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskSchema } from "./types.js";
|
|
31
32
|
import { buildOpenApiDocument } from "./openapi.js";
|
|
32
33
|
import { registerWebRoutes } from "./web.js";
|
|
33
34
|
import { createManagerRuntime } from "./managers/runtime.js";
|
|
@@ -186,6 +187,39 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
186
187
|
{ name: "notes", type: "Array<{ contentMarkdown, author?, links? }>", required: false, description: "Optional nested notes that will auto-link to the new task.", defaultValue: [] }
|
|
187
188
|
]
|
|
188
189
|
},
|
|
190
|
+
{
|
|
191
|
+
entityType: "habit",
|
|
192
|
+
purpose: "A recurring commitment or recurring slip with explicit cadence, graph links, and XP consequences.",
|
|
193
|
+
minimumCreateFields: ["title"],
|
|
194
|
+
relationshipRules: [
|
|
195
|
+
"Habits can link directly to goals, projects, tasks, values, patterns, behaviors, beliefs, modes, and trigger reports.",
|
|
196
|
+
"Habits are recurring records, not task variants, and they participate in search, notes, delete/restore, and XP.",
|
|
197
|
+
"linkedBehaviorId remains a compatibility alias; linkedBehaviorIds is the canonical array form."
|
|
198
|
+
],
|
|
199
|
+
searchHints: ["Search by title before creating a duplicate habit.", "Use linkedTo when the habit should already be attached to a goal, project, task, or Psyche entity."],
|
|
200
|
+
examples: ['{"title":"Morning training","frequency":"daily","polarity":"positive","linkedGoalIds":["goal_train_body"],"linkedValueIds":["value_steadiness"],"linkedBehaviorIds":["behavior_regulating_walk"]}'],
|
|
201
|
+
fieldGuide: [
|
|
202
|
+
{ name: "title", type: "string", required: true, description: "Concrete recurring behavior label." },
|
|
203
|
+
{ name: "description", type: "string", required: false, description: "What counts as success or failure for this habit.", defaultValue: "" },
|
|
204
|
+
{ name: "status", type: "active|paused|archived", required: false, description: "Lifecycle state.", enumValues: ["active", "paused", "archived"], defaultValue: "active" },
|
|
205
|
+
{ name: "polarity", type: "positive|negative", required: false, description: "Whether doing the behavior is aligned or misaligned.", enumValues: ["positive", "negative"], defaultValue: "positive" },
|
|
206
|
+
{ name: "frequency", type: "daily|weekly", required: false, description: "Recurrence cadence.", enumValues: ["daily", "weekly"], defaultValue: "daily" },
|
|
207
|
+
{ name: "targetCount", type: "integer", required: false, description: "How many repetitions define the cadence window.", defaultValue: 1 },
|
|
208
|
+
{ name: "weekDays", type: "integer[]", required: false, description: "Weekday numbers for weekly habits where Monday is 1 and Sunday is 0.", defaultValue: [] },
|
|
209
|
+
{ name: "linkedGoalIds", type: "string[]", required: false, description: "Linked goal ids.", defaultValue: [] },
|
|
210
|
+
{ name: "linkedProjectIds", type: "string[]", required: false, description: "Linked project ids.", defaultValue: [] },
|
|
211
|
+
{ name: "linkedTaskIds", type: "string[]", required: false, description: "Linked task ids.", defaultValue: [] },
|
|
212
|
+
{ name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
|
|
213
|
+
{ name: "linkedPatternIds", type: "string[]", required: false, description: "Linked pattern ids.", defaultValue: [] },
|
|
214
|
+
{ name: "linkedBehaviorIds", type: "string[]", required: false, description: "Canonical linked behavior ids.", defaultValue: [] },
|
|
215
|
+
{ name: "linkedBehaviorId", type: "string|null", required: false, description: "Compatibility alias for the first linked behavior id.", defaultValue: null, nullable: true },
|
|
216
|
+
{ name: "linkedBeliefIds", type: "string[]", required: false, description: "Linked belief ids.", defaultValue: [] },
|
|
217
|
+
{ name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] },
|
|
218
|
+
{ name: "linkedReportIds", type: "string[]", required: false, description: "Linked trigger report ids.", defaultValue: [] },
|
|
219
|
+
{ name: "rewardXp", type: "integer", required: false, description: "XP granted on aligned check-ins.", defaultValue: 12 },
|
|
220
|
+
{ name: "penaltyXp", type: "integer", required: false, description: "XP removed on misaligned check-ins.", defaultValue: 8 }
|
|
221
|
+
]
|
|
222
|
+
},
|
|
189
223
|
{
|
|
190
224
|
entityType: "note",
|
|
191
225
|
purpose: "A Markdown note that can link to one or many Forge entities.",
|
|
@@ -594,6 +628,15 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
594
628
|
notes: ["Restore only works for soft-deleted entities."],
|
|
595
629
|
example: '{"operations":[{"entityType":"goal","id":"goal_123","clientRef":"goal-restore-1"}]}'
|
|
596
630
|
},
|
|
631
|
+
{
|
|
632
|
+
toolName: "forge_grant_reward_bonus",
|
|
633
|
+
summary: "Grant an explicit manual XP bonus or penalty with clear provenance.",
|
|
634
|
+
whenToUse: "Use when the user or operator explicitly wants an auditable reward adjustment beyond the automatic task and habit reward paths.",
|
|
635
|
+
inputShape: "{ entityType: RewardableEntityType, entityId: string, deltaXp: integer, reasonTitle: string, reasonSummary?: string, metadata?: object }",
|
|
636
|
+
requiredFields: ["entityType", "entityId", "deltaXp", "reasonTitle"],
|
|
637
|
+
notes: ["Requires rewards.manage and write scopes.", "Use this for explicit operator judgement, not as a substitute for normal task_run or habit check-in rewards."],
|
|
638
|
+
example: '{"entityType":"habit","entityId":"habit_morning_training","deltaXp":18,"reasonTitle":"Operator bonus","reasonSummary":"Stayed with the habit through unusual travel friction.","metadata":{"manual":true,"source":"agent"}}'
|
|
639
|
+
},
|
|
597
640
|
{
|
|
598
641
|
toolName: "forge_post_insight",
|
|
599
642
|
summary: "Store an agent-authored insight.",
|
|
@@ -736,6 +779,7 @@ function buildAgentOnboardingPayload(request) {
|
|
|
736
779
|
"Goals are the top-level strategic layer.",
|
|
737
780
|
"Projects belong to one goal through goalId.",
|
|
738
781
|
"Tasks can belong to a goal, a project, both, or neither.",
|
|
782
|
+
"Habits are recurring records that can connect directly to goals, projects, tasks, and durable Psyche entities.",
|
|
739
783
|
"Task runs represent live work sessions on tasks and are separate from task status.",
|
|
740
784
|
"Notes can link to one or many entities and are the canonical place for Markdown progress context or close-out evidence.",
|
|
741
785
|
"Psyche values can link to goals, projects, and tasks.",
|
|
@@ -771,6 +815,7 @@ function buildAgentOnboardingPayload(request) {
|
|
|
771
815
|
"forge_delete_entities",
|
|
772
816
|
"forge_restore_entities"
|
|
773
817
|
],
|
|
818
|
+
rewardWorkflow: ["forge_grant_reward_bonus"],
|
|
774
819
|
workWorkflow: [
|
|
775
820
|
"forge_log_work",
|
|
776
821
|
"forge_start_task_run",
|
|
@@ -955,7 +1000,17 @@ function buildHealthPayload(taskRunWatchdog, extras = {}) {
|
|
|
955
1000
|
...extras
|
|
956
1001
|
};
|
|
957
1002
|
}
|
|
1003
|
+
function shouldIncludeRuntimeProbe(headers) {
|
|
1004
|
+
const probeHeader = headers["x-forge-runtime-probe"];
|
|
1005
|
+
if (Array.isArray(probeHeader)) {
|
|
1006
|
+
return probeHeader.some((value) => typeof value === "string" && value.trim() === "1");
|
|
1007
|
+
}
|
|
1008
|
+
return typeof probeHeader === "string" && probeHeader.trim() === "1";
|
|
1009
|
+
}
|
|
958
1010
|
function buildV1Context() {
|
|
1011
|
+
const goals = listGoals();
|
|
1012
|
+
const tasks = listTasks();
|
|
1013
|
+
const habits = listHabits();
|
|
959
1014
|
return {
|
|
960
1015
|
meta: {
|
|
961
1016
|
apiVersion: "v1",
|
|
@@ -964,15 +1019,16 @@ function buildV1Context() {
|
|
|
964
1019
|
backend: "forge-node-runtime",
|
|
965
1020
|
mode: "transitional-node"
|
|
966
1021
|
},
|
|
967
|
-
metrics: buildGamificationProfile(
|
|
1022
|
+
metrics: buildGamificationProfile(goals, tasks, habits),
|
|
968
1023
|
dashboard: getDashboard(),
|
|
969
1024
|
overview: getOverviewContext(),
|
|
970
1025
|
today: getTodayContext(),
|
|
971
1026
|
risk: getRiskContext(),
|
|
972
|
-
goals
|
|
1027
|
+
goals,
|
|
973
1028
|
projects: listProjectSummaries(),
|
|
974
1029
|
tags: listTags(),
|
|
975
|
-
tasks
|
|
1030
|
+
tasks,
|
|
1031
|
+
habits,
|
|
976
1032
|
activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
|
|
977
1033
|
activity: listActivityEvents({ limit: 25 })
|
|
978
1034
|
};
|
|
@@ -980,8 +1036,9 @@ function buildV1Context() {
|
|
|
980
1036
|
function buildXpMetricsPayload() {
|
|
981
1037
|
const goals = listGoals();
|
|
982
1038
|
const tasks = listTasks();
|
|
1039
|
+
const habits = listHabits();
|
|
983
1040
|
const rules = listRewardRules();
|
|
984
|
-
const gamificationOverview = buildGamificationOverview(goals, tasks);
|
|
1041
|
+
const gamificationOverview = buildGamificationOverview(goals, tasks, habits);
|
|
985
1042
|
const dailyAmbientCap = rules
|
|
986
1043
|
.filter((rule) => rule.family === "ambient")
|
|
987
1044
|
.reduce((max, rule) => Math.max(max, Number(rule.config.dailyCap ?? 0)), 0) || 12;
|
|
@@ -989,7 +1046,7 @@ function buildXpMetricsPayload() {
|
|
|
989
1046
|
profile: gamificationOverview.profile,
|
|
990
1047
|
achievements: gamificationOverview.achievements,
|
|
991
1048
|
milestoneRewards: gamificationOverview.milestoneRewards,
|
|
992
|
-
momentumPulse: buildXpMomentumPulse(goals, tasks),
|
|
1049
|
+
momentumPulse: buildXpMomentumPulse(goals, tasks, habits),
|
|
993
1050
|
recentLedger: listRewardLedger({ limit: 25 }),
|
|
994
1051
|
rules,
|
|
995
1052
|
dailyAmbientXp: getDailyAmbientXp(new Date().toISOString().slice(0, 10)),
|
|
@@ -998,6 +1055,7 @@ function buildXpMetricsPayload() {
|
|
|
998
1055
|
}
|
|
999
1056
|
function buildOperatorContext() {
|
|
1000
1057
|
const tasks = listTasks();
|
|
1058
|
+
const dueHabits = listHabits({ dueToday: true }).slice(0, 12);
|
|
1001
1059
|
const activeProjects = listProjectSummaries({ status: "active" }).filter((project) => project.activeTaskCount > 0 || project.completedTaskCount > 0);
|
|
1002
1060
|
const focusTasks = tasks.filter((task) => task.status === "focus" || task.status === "in_progress");
|
|
1003
1061
|
const recommendedNextTask = focusTasks[0] ??
|
|
@@ -1008,6 +1066,7 @@ function buildOperatorContext() {
|
|
|
1008
1066
|
generatedAt: new Date().toISOString(),
|
|
1009
1067
|
activeProjects: activeProjects.slice(0, 8),
|
|
1010
1068
|
focusTasks: focusTasks.slice(0, 12),
|
|
1069
|
+
dueHabits,
|
|
1011
1070
|
currentBoard: {
|
|
1012
1071
|
backlog: tasks.filter((task) => task.status === "backlog").slice(0, 20),
|
|
1013
1072
|
focus: tasks.filter((task) => task.status === "focus").slice(0, 20),
|
|
@@ -1231,9 +1290,18 @@ export async function buildServer(options = {}) {
|
|
|
1231
1290
|
return context;
|
|
1232
1291
|
};
|
|
1233
1292
|
app.get("/api/health", async () => buildHealthPayload(taskRunWatchdog));
|
|
1234
|
-
app.get("/api/v1/health", async () => buildHealthPayload(taskRunWatchdog, {
|
|
1293
|
+
app.get("/api/v1/health", async (request) => buildHealthPayload(taskRunWatchdog, {
|
|
1235
1294
|
apiVersion: "v1",
|
|
1236
|
-
backend: "forge-node-runtime"
|
|
1295
|
+
backend: "forge-node-runtime",
|
|
1296
|
+
...(shouldIncludeRuntimeProbe(request.headers)
|
|
1297
|
+
? {
|
|
1298
|
+
runtime: {
|
|
1299
|
+
pid: process.pid,
|
|
1300
|
+
storageRoot: runtimeConfig.dataRoot ?? process.cwd(),
|
|
1301
|
+
basePath: runtimeConfig.basePath
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
: {})
|
|
1237
1305
|
}));
|
|
1238
1306
|
app.get("/api/v1/auth/operator-session", async (request, reply) => ({
|
|
1239
1307
|
session: managers.session.ensureLocalOperatorSession(request.headers, reply)
|
|
@@ -1721,6 +1789,19 @@ export async function buildServer(options = {}) {
|
|
|
1721
1789
|
const query = taskListQuerySchema.parse(request.query ?? {});
|
|
1722
1790
|
return { tasks: listTasks(query) };
|
|
1723
1791
|
});
|
|
1792
|
+
app.get("/api/v1/habits", async (request) => {
|
|
1793
|
+
const query = habitListQuerySchema.parse(request.query ?? {});
|
|
1794
|
+
return { habits: listHabits(query) };
|
|
1795
|
+
});
|
|
1796
|
+
app.get("/api/v1/habits/:id", async (request, reply) => {
|
|
1797
|
+
const { id } = request.params;
|
|
1798
|
+
const habit = getHabitById(id);
|
|
1799
|
+
if (!habit) {
|
|
1800
|
+
reply.code(404);
|
|
1801
|
+
return { error: "Habit not found" };
|
|
1802
|
+
}
|
|
1803
|
+
return { habit };
|
|
1804
|
+
});
|
|
1724
1805
|
app.get("/api/v1/projects/:id", async (request, reply) => {
|
|
1725
1806
|
const { id } = request.params;
|
|
1726
1807
|
const project = listProjectSummaries().find((entry) => entry.id === id);
|
|
@@ -1764,7 +1845,7 @@ export async function buildServer(options = {}) {
|
|
|
1764
1845
|
return { event };
|
|
1765
1846
|
});
|
|
1766
1847
|
app.get("/api/v1/metrics", async () => ({
|
|
1767
|
-
metrics: buildGamificationOverview(listGoals(), listTasks())
|
|
1848
|
+
metrics: buildGamificationOverview(listGoals(), listTasks(), listHabits())
|
|
1768
1849
|
}));
|
|
1769
1850
|
app.get("/api/v1/metrics/xp", async () => ({
|
|
1770
1851
|
metrics: buildXpMetricsPayload()
|
|
@@ -1944,6 +2025,12 @@ export async function buildServer(options = {}) {
|
|
|
1944
2025
|
reply.code(201);
|
|
1945
2026
|
return { project };
|
|
1946
2027
|
});
|
|
2028
|
+
app.post("/api/v1/habits", async (request, reply) => {
|
|
2029
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits" });
|
|
2030
|
+
const habit = createHabit(createHabitSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
2031
|
+
reply.code(201);
|
|
2032
|
+
return { habit };
|
|
2033
|
+
});
|
|
1947
2034
|
app.patch("/api/v1/projects/:id", async (request, reply) => {
|
|
1948
2035
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/projects/:id" });
|
|
1949
2036
|
const { id } = request.params;
|
|
@@ -1964,6 +2051,36 @@ export async function buildServer(options = {}) {
|
|
|
1964
2051
|
}
|
|
1965
2052
|
return { project };
|
|
1966
2053
|
});
|
|
2054
|
+
app.patch("/api/v1/habits/:id", async (request, reply) => {
|
|
2055
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id" });
|
|
2056
|
+
const { id } = request.params;
|
|
2057
|
+
const habit = updateHabit(id, updateHabitSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
2058
|
+
if (!habit) {
|
|
2059
|
+
reply.code(404);
|
|
2060
|
+
return { error: "Habit not found" };
|
|
2061
|
+
}
|
|
2062
|
+
return { habit };
|
|
2063
|
+
});
|
|
2064
|
+
app.delete("/api/v1/habits/:id", async (request, reply) => {
|
|
2065
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id" });
|
|
2066
|
+
const { id } = request.params;
|
|
2067
|
+
const habit = deleteEntity("habit", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
|
|
2068
|
+
if (!habit) {
|
|
2069
|
+
reply.code(404);
|
|
2070
|
+
return { error: "Habit not found" };
|
|
2071
|
+
}
|
|
2072
|
+
return { habit };
|
|
2073
|
+
});
|
|
2074
|
+
app.post("/api/v1/habits/:id/check-ins", async (request, reply) => {
|
|
2075
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id/check-ins" });
|
|
2076
|
+
const { id } = request.params;
|
|
2077
|
+
const habit = createHabitCheckIn(id, createHabitCheckInSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
2078
|
+
if (!habit) {
|
|
2079
|
+
reply.code(404);
|
|
2080
|
+
return { error: "Habit not found" };
|
|
2081
|
+
}
|
|
2082
|
+
return { habit, metrics: buildXpMetricsPayload() };
|
|
2083
|
+
});
|
|
1967
2084
|
app.patch("/api/v1/settings", async (request) => {
|
|
1968
2085
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/settings" });
|
|
1969
2086
|
return {
|
|
@@ -2072,7 +2189,7 @@ export async function buildServer(options = {}) {
|
|
|
2072
2189
|
app.get("/api/metrics", async (_request, reply) => {
|
|
2073
2190
|
markCompatibilityRoute(reply);
|
|
2074
2191
|
return {
|
|
2075
|
-
metrics: buildGamificationProfile(listGoals(), listTasks())
|
|
2192
|
+
metrics: buildGamificationProfile(listGoals(), listTasks(), listHabits())
|
|
2076
2193
|
};
|
|
2077
2194
|
});
|
|
2078
2195
|
app.get("/api/task-runs", async (request, reply) => {
|
|
@@ -2102,7 +2219,7 @@ export async function buildServer(options = {}) {
|
|
|
2102
2219
|
markCompatibilityRoute(reply);
|
|
2103
2220
|
const query = taskListQuerySchema.parse(request.query ?? {});
|
|
2104
2221
|
return {
|
|
2105
|
-
metrics: buildGamificationProfile(listGoals(), listTasks()),
|
|
2222
|
+
metrics: buildGamificationProfile(listGoals(), listTasks(), listHabits()),
|
|
2106
2223
|
dashboard: getDashboard(),
|
|
2107
2224
|
overview: getOverviewContext(),
|
|
2108
2225
|
today: getTodayContext(),
|
|
@@ -2111,6 +2228,7 @@ export async function buildServer(options = {}) {
|
|
|
2111
2228
|
projects: listProjectSummaries(),
|
|
2112
2229
|
tags: listTags(),
|
|
2113
2230
|
tasks: listTasks(query),
|
|
2231
|
+
habits: listHabits(),
|
|
2114
2232
|
activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
|
|
2115
2233
|
activity: listActivityEvents({ limit: 25 })
|
|
2116
2234
|
};
|
package/dist/server/openapi.js
CHANGED
|
@@ -242,6 +242,89 @@ export function buildOpenApiDocument() {
|
|
|
242
242
|
isCurrent: { type: "boolean" }
|
|
243
243
|
}
|
|
244
244
|
};
|
|
245
|
+
const habitCheckIn = {
|
|
246
|
+
type: "object",
|
|
247
|
+
additionalProperties: false,
|
|
248
|
+
required: ["id", "habitId", "dateKey", "status", "note", "deltaXp", "createdAt", "updatedAt"],
|
|
249
|
+
properties: {
|
|
250
|
+
id: { type: "string" },
|
|
251
|
+
habitId: { type: "string" },
|
|
252
|
+
dateKey: { type: "string", format: "date" },
|
|
253
|
+
status: { type: "string", enum: ["done", "missed"] },
|
|
254
|
+
note: { type: "string" },
|
|
255
|
+
deltaXp: { type: "integer" },
|
|
256
|
+
createdAt: { type: "string", format: "date-time" },
|
|
257
|
+
updatedAt: { type: "string", format: "date-time" }
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const habit = {
|
|
261
|
+
type: "object",
|
|
262
|
+
additionalProperties: false,
|
|
263
|
+
required: [
|
|
264
|
+
"id",
|
|
265
|
+
"title",
|
|
266
|
+
"description",
|
|
267
|
+
"status",
|
|
268
|
+
"polarity",
|
|
269
|
+
"frequency",
|
|
270
|
+
"targetCount",
|
|
271
|
+
"weekDays",
|
|
272
|
+
"linkedGoalIds",
|
|
273
|
+
"linkedProjectIds",
|
|
274
|
+
"linkedTaskIds",
|
|
275
|
+
"linkedValueIds",
|
|
276
|
+
"linkedPatternIds",
|
|
277
|
+
"linkedBehaviorIds",
|
|
278
|
+
"linkedBeliefIds",
|
|
279
|
+
"linkedModeIds",
|
|
280
|
+
"linkedReportIds",
|
|
281
|
+
"linkedBehaviorId",
|
|
282
|
+
"linkedBehaviorTitle",
|
|
283
|
+
"linkedBehaviorTitles",
|
|
284
|
+
"rewardXp",
|
|
285
|
+
"penaltyXp",
|
|
286
|
+
"createdAt",
|
|
287
|
+
"updatedAt",
|
|
288
|
+
"lastCheckInAt",
|
|
289
|
+
"lastCheckInStatus",
|
|
290
|
+
"streakCount",
|
|
291
|
+
"completionRate",
|
|
292
|
+
"dueToday",
|
|
293
|
+
"checkIns"
|
|
294
|
+
],
|
|
295
|
+
properties: {
|
|
296
|
+
id: { type: "string" },
|
|
297
|
+
title: { type: "string" },
|
|
298
|
+
description: { type: "string" },
|
|
299
|
+
status: { type: "string", enum: ["active", "paused", "archived"] },
|
|
300
|
+
polarity: { type: "string", enum: ["positive", "negative"] },
|
|
301
|
+
frequency: { type: "string", enum: ["daily", "weekly"] },
|
|
302
|
+
targetCount: { type: "integer" },
|
|
303
|
+
weekDays: arrayOf({ type: "integer" }),
|
|
304
|
+
linkedGoalIds: arrayOf({ type: "string" }),
|
|
305
|
+
linkedProjectIds: arrayOf({ type: "string" }),
|
|
306
|
+
linkedTaskIds: arrayOf({ type: "string" }),
|
|
307
|
+
linkedValueIds: arrayOf({ type: "string" }),
|
|
308
|
+
linkedPatternIds: arrayOf({ type: "string" }),
|
|
309
|
+
linkedBehaviorIds: arrayOf({ type: "string" }),
|
|
310
|
+
linkedBeliefIds: arrayOf({ type: "string" }),
|
|
311
|
+
linkedModeIds: arrayOf({ type: "string" }),
|
|
312
|
+
linkedReportIds: arrayOf({ type: "string" }),
|
|
313
|
+
linkedBehaviorId: nullable({ type: "string" }),
|
|
314
|
+
linkedBehaviorTitle: nullable({ type: "string" }),
|
|
315
|
+
linkedBehaviorTitles: arrayOf({ type: "string" }),
|
|
316
|
+
rewardXp: { type: "integer" },
|
|
317
|
+
penaltyXp: { type: "integer" },
|
|
318
|
+
createdAt: { type: "string", format: "date-time" },
|
|
319
|
+
updatedAt: { type: "string", format: "date-time" },
|
|
320
|
+
lastCheckInAt: nullable({ type: "string", format: "date-time" }),
|
|
321
|
+
lastCheckInStatus: nullable({ type: "string", enum: ["done", "missed"] }),
|
|
322
|
+
streakCount: { type: "integer" },
|
|
323
|
+
completionRate: { type: "number" },
|
|
324
|
+
dueToday: { type: "boolean" },
|
|
325
|
+
checkIns: arrayOf({ $ref: "#/components/schemas/HabitCheckIn" })
|
|
326
|
+
}
|
|
327
|
+
};
|
|
245
328
|
const activityEvent = {
|
|
246
329
|
type: "object",
|
|
247
330
|
additionalProperties: false,
|
|
@@ -252,6 +335,7 @@ export function buildOpenApiDocument() {
|
|
|
252
335
|
type: "string",
|
|
253
336
|
enum: [
|
|
254
337
|
"task",
|
|
338
|
+
"habit",
|
|
255
339
|
"goal",
|
|
256
340
|
"project",
|
|
257
341
|
"domain",
|
|
@@ -378,7 +462,7 @@ export function buildOpenApiDocument() {
|
|
|
378
462
|
const dashboardPayload = {
|
|
379
463
|
type: "object",
|
|
380
464
|
additionalProperties: false,
|
|
381
|
-
required: ["stats", "goals", "projects", "tasks", "tags", "suggestedTags", "owners", "executionBuckets", "gamification", "achievements", "milestoneRewards", "recentActivity", "notesSummaryByEntity"],
|
|
465
|
+
required: ["stats", "goals", "projects", "tasks", "habits", "tags", "suggestedTags", "owners", "executionBuckets", "gamification", "achievements", "milestoneRewards", "recentActivity", "notesSummaryByEntity"],
|
|
382
466
|
properties: {
|
|
383
467
|
stats: {
|
|
384
468
|
type: "object",
|
|
@@ -397,6 +481,7 @@ export function buildOpenApiDocument() {
|
|
|
397
481
|
goals: arrayOf({ $ref: "#/components/schemas/DashboardGoal" }),
|
|
398
482
|
projects: arrayOf({ $ref: "#/components/schemas/ProjectSummary" }),
|
|
399
483
|
tasks: arrayOf({ $ref: "#/components/schemas/Task" }),
|
|
484
|
+
habits: arrayOf({ $ref: "#/components/schemas/Habit" }),
|
|
400
485
|
tags: arrayOf({ $ref: "#/components/schemas/Tag" }),
|
|
401
486
|
suggestedTags: arrayOf({ $ref: "#/components/schemas/Tag" }),
|
|
402
487
|
owners: arrayOf({ type: "string" }),
|
|
@@ -422,7 +507,7 @@ export function buildOpenApiDocument() {
|
|
|
422
507
|
const overviewContext = {
|
|
423
508
|
type: "object",
|
|
424
509
|
additionalProperties: false,
|
|
425
|
-
required: ["generatedAt", "strategicHeader", "projects", "activeGoals", "topTasks", "recentEvidence", "achievements", "domainBalance", "neglectedGoals"],
|
|
510
|
+
required: ["generatedAt", "strategicHeader", "projects", "activeGoals", "topTasks", "dueHabits", "recentEvidence", "achievements", "domainBalance", "neglectedGoals"],
|
|
426
511
|
properties: {
|
|
427
512
|
generatedAt: { type: "string", format: "date-time" },
|
|
428
513
|
strategicHeader: {
|
|
@@ -443,6 +528,7 @@ export function buildOpenApiDocument() {
|
|
|
443
528
|
projects: arrayOf({ $ref: "#/components/schemas/ProjectSummary" }),
|
|
444
529
|
activeGoals: arrayOf({ $ref: "#/components/schemas/DashboardGoal" }),
|
|
445
530
|
topTasks: arrayOf({ $ref: "#/components/schemas/Task" }),
|
|
531
|
+
dueHabits: arrayOf({ $ref: "#/components/schemas/Habit" }),
|
|
446
532
|
recentEvidence: arrayOf({ $ref: "#/components/schemas/ActivityEvent" }),
|
|
447
533
|
achievements: arrayOf({ $ref: "#/components/schemas/AchievementSignal" }),
|
|
448
534
|
domainBalance: arrayOf({
|
|
@@ -475,7 +561,7 @@ export function buildOpenApiDocument() {
|
|
|
475
561
|
const todayContext = {
|
|
476
562
|
type: "object",
|
|
477
563
|
additionalProperties: false,
|
|
478
|
-
required: ["generatedAt", "directive", "timeline", "dailyQuests", "milestoneRewards", "momentum"],
|
|
564
|
+
required: ["generatedAt", "directive", "timeline", "dueHabits", "dailyQuests", "milestoneRewards", "recentHabitRewards", "momentum"],
|
|
479
565
|
properties: {
|
|
480
566
|
generatedAt: { type: "string", format: "date-time" },
|
|
481
567
|
directive: {
|
|
@@ -499,6 +585,7 @@ export function buildOpenApiDocument() {
|
|
|
499
585
|
tasks: arrayOf({ $ref: "#/components/schemas/Task" })
|
|
500
586
|
}
|
|
501
587
|
}),
|
|
588
|
+
dueHabits: arrayOf({ $ref: "#/components/schemas/Habit" }),
|
|
502
589
|
dailyQuests: arrayOf({
|
|
503
590
|
type: "object",
|
|
504
591
|
additionalProperties: false,
|
|
@@ -513,6 +600,7 @@ export function buildOpenApiDocument() {
|
|
|
513
600
|
}
|
|
514
601
|
}),
|
|
515
602
|
milestoneRewards: arrayOf({ $ref: "#/components/schemas/MilestoneReward" }),
|
|
603
|
+
recentHabitRewards: arrayOf({ $ref: "#/components/schemas/RewardLedgerEvent" }),
|
|
516
604
|
momentum: {
|
|
517
605
|
type: "object",
|
|
518
606
|
additionalProperties: false,
|
|
@@ -550,7 +638,7 @@ export function buildOpenApiDocument() {
|
|
|
550
638
|
const forgeSnapshot = {
|
|
551
639
|
type: "object",
|
|
552
640
|
additionalProperties: false,
|
|
553
|
-
required: ["meta", "metrics", "dashboard", "overview", "today", "risk", "goals", "projects", "tags", "tasks", "activeTaskRuns", "activity"],
|
|
641
|
+
required: ["meta", "metrics", "dashboard", "overview", "today", "risk", "goals", "projects", "tags", "tasks", "habits", "activeTaskRuns", "activity"],
|
|
554
642
|
properties: {
|
|
555
643
|
meta: {
|
|
556
644
|
type: "object",
|
|
@@ -573,6 +661,7 @@ export function buildOpenApiDocument() {
|
|
|
573
661
|
projects: arrayOf({ $ref: "#/components/schemas/ProjectSummary" }),
|
|
574
662
|
tags: arrayOf({ $ref: "#/components/schemas/Tag" }),
|
|
575
663
|
tasks: arrayOf({ $ref: "#/components/schemas/Task" }),
|
|
664
|
+
habits: arrayOf({ $ref: "#/components/schemas/Habit" }),
|
|
576
665
|
activeTaskRuns: arrayOf({ $ref: "#/components/schemas/TaskRun" }),
|
|
577
666
|
activity: arrayOf({ $ref: "#/components/schemas/ActivityEvent" })
|
|
578
667
|
}
|
|
@@ -1041,6 +1130,7 @@ export function buildOpenApiDocument() {
|
|
|
1041
1130
|
"generatedAt",
|
|
1042
1131
|
"activeProjects",
|
|
1043
1132
|
"focusTasks",
|
|
1133
|
+
"dueHabits",
|
|
1044
1134
|
"currentBoard",
|
|
1045
1135
|
"recentActivity",
|
|
1046
1136
|
"recentTaskRuns",
|
|
@@ -1051,6 +1141,7 @@ export function buildOpenApiDocument() {
|
|
|
1051
1141
|
generatedAt: { type: "string", format: "date-time" },
|
|
1052
1142
|
activeProjects: arrayOf({ $ref: "#/components/schemas/ProjectSummary" }),
|
|
1053
1143
|
focusTasks: arrayOf({ $ref: "#/components/schemas/Task" }),
|
|
1144
|
+
dueHabits: arrayOf({ $ref: "#/components/schemas/Habit" }),
|
|
1054
1145
|
currentBoard: {
|
|
1055
1146
|
type: "object",
|
|
1056
1147
|
additionalProperties: false,
|
|
@@ -1993,6 +2084,8 @@ export function buildOpenApiDocument() {
|
|
|
1993
2084
|
ProjectSummary: projectSummary,
|
|
1994
2085
|
Task: task,
|
|
1995
2086
|
TaskRun: taskRun,
|
|
2087
|
+
HabitCheckIn: habitCheckIn,
|
|
2088
|
+
Habit: habit,
|
|
1996
2089
|
ActivityEvent: activityEvent,
|
|
1997
2090
|
GamificationProfile: gamificationProfile,
|
|
1998
2091
|
AchievementSignal: achievementSignal,
|
|
@@ -2718,6 +2811,90 @@ export function buildOpenApiDocument() {
|
|
|
2718
2811
|
}
|
|
2719
2812
|
}
|
|
2720
2813
|
},
|
|
2814
|
+
"/api/v1/habits": {
|
|
2815
|
+
get: {
|
|
2816
|
+
summary: "List habits with current streak and due-today state",
|
|
2817
|
+
responses: {
|
|
2818
|
+
"200": jsonResponse({
|
|
2819
|
+
type: "object",
|
|
2820
|
+
required: ["habits"],
|
|
2821
|
+
properties: {
|
|
2822
|
+
habits: arrayOf({ $ref: "#/components/schemas/Habit" })
|
|
2823
|
+
}
|
|
2824
|
+
}, "Habit collection")
|
|
2825
|
+
}
|
|
2826
|
+
},
|
|
2827
|
+
post: {
|
|
2828
|
+
summary: "Create a habit",
|
|
2829
|
+
responses: {
|
|
2830
|
+
"201": jsonResponse({
|
|
2831
|
+
type: "object",
|
|
2832
|
+
required: ["habit"],
|
|
2833
|
+
properties: {
|
|
2834
|
+
habit: { $ref: "#/components/schemas/Habit" }
|
|
2835
|
+
}
|
|
2836
|
+
}, "Created habit"),
|
|
2837
|
+
default: { $ref: "#/components/responses/Error" }
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
},
|
|
2841
|
+
"/api/v1/habits/{id}": {
|
|
2842
|
+
get: {
|
|
2843
|
+
summary: "Get a habit",
|
|
2844
|
+
responses: {
|
|
2845
|
+
"200": jsonResponse({
|
|
2846
|
+
type: "object",
|
|
2847
|
+
required: ["habit"],
|
|
2848
|
+
properties: {
|
|
2849
|
+
habit: { $ref: "#/components/schemas/Habit" }
|
|
2850
|
+
}
|
|
2851
|
+
}, "Habit"),
|
|
2852
|
+
"404": { $ref: "#/components/responses/Error" }
|
|
2853
|
+
}
|
|
2854
|
+
},
|
|
2855
|
+
patch: {
|
|
2856
|
+
summary: "Update a habit",
|
|
2857
|
+
responses: {
|
|
2858
|
+
"200": jsonResponse({
|
|
2859
|
+
type: "object",
|
|
2860
|
+
required: ["habit"],
|
|
2861
|
+
properties: {
|
|
2862
|
+
habit: { $ref: "#/components/schemas/Habit" }
|
|
2863
|
+
}
|
|
2864
|
+
}, "Updated habit"),
|
|
2865
|
+
"404": { $ref: "#/components/responses/Error" }
|
|
2866
|
+
}
|
|
2867
|
+
},
|
|
2868
|
+
delete: {
|
|
2869
|
+
summary: "Delete a habit",
|
|
2870
|
+
responses: {
|
|
2871
|
+
"200": jsonResponse({
|
|
2872
|
+
type: "object",
|
|
2873
|
+
required: ["habit"],
|
|
2874
|
+
properties: {
|
|
2875
|
+
habit: { $ref: "#/components/schemas/Habit" }
|
|
2876
|
+
}
|
|
2877
|
+
}, "Deleted habit"),
|
|
2878
|
+
"404": { $ref: "#/components/responses/Error" }
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
},
|
|
2882
|
+
"/api/v1/habits/{id}/check-ins": {
|
|
2883
|
+
post: {
|
|
2884
|
+
summary: "Record a habit outcome for one day",
|
|
2885
|
+
responses: {
|
|
2886
|
+
"200": jsonResponse({
|
|
2887
|
+
type: "object",
|
|
2888
|
+
required: ["habit", "metrics"],
|
|
2889
|
+
properties: {
|
|
2890
|
+
habit: { $ref: "#/components/schemas/Habit" },
|
|
2891
|
+
metrics: { $ref: "#/components/schemas/XpMetricsPayload" }
|
|
2892
|
+
}
|
|
2893
|
+
}, "Habit check-in result"),
|
|
2894
|
+
"404": { $ref: "#/components/responses/Error" }
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
},
|
|
2721
2898
|
"/api/v1/tags": {
|
|
2722
2899
|
get: {
|
|
2723
2900
|
summary: "List tags",
|