ai-growth 1.1.0
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/.env.example +19 -0
- package/AGENTS.md +232 -0
- package/IMPLEMENTATION_PLAN.md +207 -0
- package/LICENSE +21 -0
- package/PRODUCT_SPEC.md +1776 -0
- package/README.md +384 -0
- package/SKILL.md +121 -0
- package/START-HERE.md +156 -0
- package/dist/api/app.js +206 -0
- package/dist/api/server.js +42 -0
- package/dist/cli/main.js +799 -0
- package/dist/core/actions.js +264 -0
- package/dist/core/context.js +7 -0
- package/dist/core/directions.js +26 -0
- package/dist/core/evidence.js +155 -0
- package/dist/core/goals.js +188 -0
- package/dist/core/lifeContext.js +41 -0
- package/dist/core/memory.js +35 -0
- package/dist/core/metrics.js +74 -0
- package/dist/core/profile.js +95 -0
- package/dist/core/progress.js +244 -0
- package/dist/core/prompts/index.js +40 -0
- package/dist/core/replan.js +100 -0
- package/dist/core/rollover.js +68 -0
- package/dist/core/rolloverTypes.js +1 -0
- package/dist/core/stateMachine.js +61 -0
- package/dist/core/status.js +57 -0
- package/dist/core/types.js +9 -0
- package/dist/daemon/main.js +64 -0
- package/dist/db/cli.js +19 -0
- package/dist/db/client.js +36 -0
- package/dist/db/migrate.js +34 -0
- package/dist/db/migrations/V1__init.sql +249 -0
- package/dist/device/card.js +257 -0
- package/dist/device/deck.js +157 -0
- package/dist/device/fontCoverage.js +162 -0
- package/dist/device/http.js +170 -0
- package/dist/device/png.js +111 -0
- package/dist/device/provision.js +185 -0
- package/dist/device/render.js +194 -0
- package/dist/device/renderCard.js +48 -0
- package/dist/device/report.js +146 -0
- package/dist/device/serial.js +201 -0
- package/dist/device/token.js +78 -0
- package/dist/device/types.js +2 -0
- package/dist/mcp/ctx.js +48 -0
- package/dist/mcp/server.js +15 -0
- package/dist/mcp/tools.js +586 -0
- package/dist/passport/builder.js +83 -0
- package/dist/passport/state.js +81 -0
- package/dist/passport/sync.js +61 -0
- package/dist/reminder/notify.js +30 -0
- package/dist/reminder/scheduler.js +87 -0
- package/dist/reminder/service.js +102 -0
- package/dist/shared/config.js +50 -0
- package/dist/shared/result.js +27 -0
- package/dist/shared/time.js +67 -0
- package/dist/shared/util.js +21 -0
- package/dist/ui/app.js +500 -0
- package/dist/ui/index.html +38 -0
- package/dist/ui/style.css +367 -0
- package/docs/images/cards.png +0 -0
- package/docs/images/cover.png +0 -0
- package/launchd/com.lairey.ai-growth.daemon.plist +45 -0
- package/llms.txt +52 -0
- package/package.json +79 -0
- package/scripts/check-device-cards.mjs +177 -0
- package/scripts/doctor.sh +194 -0
- package/scripts/gen-device-assets.mjs +188 -0
- package/scripts/install.sh +291 -0
- package/scripts/preview-cards.mjs +120 -0
- package/scripts/reset-dev-data.sh +92 -0
- package/scripts/smoke-device.mjs +401 -0
- package/scripts/uninstall.sh +63 -0
- package/server.json +56 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { makeCtx, toResult, toResultAsync } from './ctx.js';
|
|
3
|
+
import { ok } from '../shared/result.js';
|
|
4
|
+
import { nowIso, localDateKey } from '../shared/time.js';
|
|
5
|
+
import { growthStatus, getProfile, ensureProfile } from '../core/status.js';
|
|
6
|
+
import { updateProfile } from '../core/profile.js';
|
|
7
|
+
import { getSetting, setSetting, todayIn } from '../core/profile.js';
|
|
8
|
+
import { listSyncJobs } from '../passport/state.js';
|
|
9
|
+
import { getGoal, listGoals, getActiveGoals, createGoalDraft, confirmGoal, activateGoal, pauseGoal, resumeGoal, updateGoal, listStages, createStagePlan, updateStage, getCurrentStage, } from '../core/goals.js';
|
|
10
|
+
import { getTodayPlan, createTodayPlan, confirmTodayPlan, createAction, updateAction, completeAction, skipAction, delayAction, startAction, cancelAction, listActionsForDate, addTaskFeedback, recentFeedbackStats, getAction, } from '../core/actions.js';
|
|
11
|
+
import { runDailyRollover, listPendingReplanActions } from '../core/rollover.js';
|
|
12
|
+
import { resolveReplan, replanHints } from '../core/replan.js';
|
|
13
|
+
import { addEvidence, listEvidence, createAssessment, submitAssessment, createGrowthEvent, listGrowthEvents, listAssessments, } from '../core/evidence.js';
|
|
14
|
+
import { getActiveLifeContext, setLifeContext } from '../core/lifeContext.js';
|
|
15
|
+
import { listMemories, addMemory, confirmMemory } from '../core/memory.js';
|
|
16
|
+
import { listDirections, addDirection, confirmDirection } from '../core/directions.js';
|
|
17
|
+
import { computeMetrics, plannerContext } from '../core/metrics.js';
|
|
18
|
+
import { renderProgress } from '../core/progress.js';
|
|
19
|
+
import { generatePassport } from '../passport/builder.js';
|
|
20
|
+
import { createPassportAdapter, syncPassport, retryPendingSyncs } from '../passport/sync.js';
|
|
21
|
+
import { latestSnapshot, isPassportDirty, flagPassportDirty } from '../passport/state.js';
|
|
22
|
+
import { createReminder, updateReminder, cancelReminder, listRemindersForDate, cancelRemindersByAction, } from '../reminder/service.js';
|
|
23
|
+
/** 注册全部 MCP tools。返回 server。 */
|
|
24
|
+
export function registerTools(server, db, cfg) {
|
|
25
|
+
const ctxOf = () => makeCtx(db, cfg, 'agent');
|
|
26
|
+
const S = z.string();
|
|
27
|
+
const optS = z.string().optional();
|
|
28
|
+
// ---------------- System ----------------
|
|
29
|
+
server.tool('growth_status', 'System status & suggested next operation. Call this FIRST on load.', {}, async () => {
|
|
30
|
+
const r = toResult(() => growthStatus(ctxOf()));
|
|
31
|
+
return json(r);
|
|
32
|
+
});
|
|
33
|
+
server.tool('run_daily_rollover', 'Run daily rollover (idempotent). Expires stale DAILY actions, moves unfinished MAIN_QUEST to PENDING_REPLAN.', {}, async () => {
|
|
34
|
+
const r = toResult(() => runDailyRollover(ctxOf()));
|
|
35
|
+
return json(r);
|
|
36
|
+
});
|
|
37
|
+
// ---------------- Profile ----------------
|
|
38
|
+
server.tool('get_profile', 'Get user profile (timezone, preferences, constraints, onboarding status).', {}, async () => {
|
|
39
|
+
const r = toResult(() => getProfile(ctxOf()) ?? null);
|
|
40
|
+
return json(r);
|
|
41
|
+
});
|
|
42
|
+
server.tool('update_profile_patch', 'Patch profile fields. Completing onboarding requires onboardingStatus=COMPLETED.', {
|
|
43
|
+
displayName: optS,
|
|
44
|
+
timezone: optS,
|
|
45
|
+
onboardingStatus: z.enum(['NOT_STARTED', 'IN_PROGRESS', 'COMPLETED']).optional(),
|
|
46
|
+
timePreferences: z.record(z.unknown()).optional(),
|
|
47
|
+
taskCapacity: z.record(z.unknown()).optional(),
|
|
48
|
+
learningPreferences: z.record(z.unknown()).optional(),
|
|
49
|
+
reminderPreferences: z.record(z.unknown()).optional(),
|
|
50
|
+
constraints: z.record(z.unknown()).optional(),
|
|
51
|
+
}, async (args) => {
|
|
52
|
+
const r = toResult(() => {
|
|
53
|
+
const ctx = ctxOf();
|
|
54
|
+
ensureProfile(ctx, args.timezone);
|
|
55
|
+
return updateProfile(ctx, args);
|
|
56
|
+
});
|
|
57
|
+
return json(r);
|
|
58
|
+
});
|
|
59
|
+
// ---------------- Directions ----------------
|
|
60
|
+
server.tool('list_directions', 'List user-confirmed growth directions / life intentions. Daily Actions MUST trace to one of these.', {
|
|
61
|
+
includeUnconfirmed: z.boolean().optional(),
|
|
62
|
+
}, async (args) => {
|
|
63
|
+
const r = toResult(() => listDirections(ctxOf(), !args?.includeUnconfirmed));
|
|
64
|
+
return json(r);
|
|
65
|
+
});
|
|
66
|
+
server.tool('add_direction', 'Add a growth direction. New long-term directions REQUIRE user confirmation — pass confirmed=true only after user explicitly agrees.', {
|
|
67
|
+
title: S,
|
|
68
|
+
description: optS,
|
|
69
|
+
domain: z.enum(['LEARN', 'BUILD', 'TRAIN', 'HABIT', 'CREATE', 'RELATIONSHIP', 'EXPLORE', 'LIFE', 'OTHER']).optional(),
|
|
70
|
+
userConfirmed: z.boolean().optional(),
|
|
71
|
+
}, async (args) => {
|
|
72
|
+
const r = toResult(() => addDirection(ctxOf(), {
|
|
73
|
+
title: args.title, description: args.description, domain: args.domain,
|
|
74
|
+
confirmed: !!args.userConfirmed,
|
|
75
|
+
}));
|
|
76
|
+
return json(r);
|
|
77
|
+
});
|
|
78
|
+
server.tool('confirm_direction', 'Mark a direction as user-confirmed.', { directionId: S }, async (args) => {
|
|
79
|
+
const r = toResult(() => confirmDirection(ctxOf(), args.directionId));
|
|
80
|
+
return json(r);
|
|
81
|
+
});
|
|
82
|
+
// ---------------- Goals ----------------
|
|
83
|
+
server.tool('create_goal_draft', 'Create a goal DRAFT. Must include why / desiredOutcome / successCriteria (user-confirmed via interview). Stays DRAFT until user confirms.', {
|
|
84
|
+
title: S,
|
|
85
|
+
why: S,
|
|
86
|
+
desiredOutcome: S,
|
|
87
|
+
// 业务规则(至少 1 条成功标准)由 Core 强制,保证错误码统一为结构化 ToolResult
|
|
88
|
+
successCriteria: z.array(S),
|
|
89
|
+
startDate: optS,
|
|
90
|
+
targetDate: optS,
|
|
91
|
+
dailyTimeBudgetMinutes: z.number().optional(),
|
|
92
|
+
motivation: optS,
|
|
93
|
+
}, async (args) => {
|
|
94
|
+
const r = toResult(() => createGoalDraft(ctxOf(), args));
|
|
95
|
+
return json(r);
|
|
96
|
+
});
|
|
97
|
+
server.tool('list_goals', 'List goals, optionally filtered by status.', {
|
|
98
|
+
statuses: z.array(z.enum(['DRAFT', 'CONFIRMED', 'ACTIVE', 'PAUSED', 'COMPLETED', 'ABANDONED'])).optional(),
|
|
99
|
+
}, async (args) => {
|
|
100
|
+
const r = toResult(() => listGoals(ctxOf(), args.statuses));
|
|
101
|
+
return json(r);
|
|
102
|
+
});
|
|
103
|
+
server.tool('get_active_goals', 'List ACTIVE goals with current stage.', {}, async () => {
|
|
104
|
+
const r = toResult(() => {
|
|
105
|
+
const ctx = ctxOf();
|
|
106
|
+
return getActiveGoals(ctx).map((g) => ({ ...g, currentStage: getCurrentStage(ctx, g.id) }));
|
|
107
|
+
});
|
|
108
|
+
return json(r);
|
|
109
|
+
});
|
|
110
|
+
server.tool('get_goal', 'Get one goal with its stages.', { goalId: S }, async (args) => {
|
|
111
|
+
const r = toResult(() => {
|
|
112
|
+
const ctx = ctxOf();
|
|
113
|
+
const g = getGoal(ctx, args.goalId);
|
|
114
|
+
return g ? { ...g, stages: listStages(ctx, g.id) } : null;
|
|
115
|
+
});
|
|
116
|
+
return json(r);
|
|
117
|
+
});
|
|
118
|
+
server.tool('confirm_goal', 'Confirm a goal draft (user has agreed). Transitions DRAFT→CONFIRMED.', { goalId: S }, async (args) => {
|
|
119
|
+
const r = toResult(() => confirmGoal(ctxOf(), args.goalId));
|
|
120
|
+
return json(r);
|
|
121
|
+
});
|
|
122
|
+
server.tool('activate_goal', 'Activate a CONFIRMED goal (after its stage plan is accepted). CONFIRMED→ACTIVE.', { goalId: S }, async (args) => {
|
|
123
|
+
const r = toResult(() => activateGoal(ctxOf(), args.goalId));
|
|
124
|
+
return json(r);
|
|
125
|
+
});
|
|
126
|
+
server.tool('update_goal', 'Update goal fields. why/outcome/criteria/targetDate changes MUST have prior user confirmation.', {
|
|
127
|
+
goalId: S,
|
|
128
|
+
title: optS,
|
|
129
|
+
why: optS,
|
|
130
|
+
desiredOutcome: optS,
|
|
131
|
+
successCriteria: z.array(S).optional(),
|
|
132
|
+
targetDate: optS,
|
|
133
|
+
dailyTimeBudgetMinutes: z.number().nullable().optional(),
|
|
134
|
+
motivation: optS,
|
|
135
|
+
reason: optS,
|
|
136
|
+
}, async (args) => {
|
|
137
|
+
const { goalId, reason, ...patch } = args;
|
|
138
|
+
const r = toResult(() => updateGoal(ctxOf(), goalId, patch, reason ?? 'update goal'));
|
|
139
|
+
return json(r);
|
|
140
|
+
});
|
|
141
|
+
server.tool('pause_goal', 'Pause an active goal.', { goalId: S }, async (args) => {
|
|
142
|
+
const r = toResult(() => pauseGoal(ctxOf(), args.goalId));
|
|
143
|
+
return json(r);
|
|
144
|
+
});
|
|
145
|
+
server.tool('resume_goal', 'Resume a paused goal.', { goalId: S }, async (args) => {
|
|
146
|
+
const r = toResult(() => resumeGoal(ctxOf(), args.goalId));
|
|
147
|
+
return json(r);
|
|
148
|
+
});
|
|
149
|
+
// ---------------- Stage plan ----------------
|
|
150
|
+
server.tool('create_stage_plan', 'Create the stage plan for a goal. Stages only — do NOT pre-generate all future daily actions.', {
|
|
151
|
+
goalId: S,
|
|
152
|
+
stages: z.array(z.object({
|
|
153
|
+
title: S,
|
|
154
|
+
objective: optS,
|
|
155
|
+
order: z.number().optional(),
|
|
156
|
+
plannedStartDate: optS,
|
|
157
|
+
plannedEndDate: optS,
|
|
158
|
+
exitCriteria: z.array(S).optional(),
|
|
159
|
+
})),
|
|
160
|
+
}, async (args) => {
|
|
161
|
+
const r = toResult(() => createStagePlan(ctxOf(), args));
|
|
162
|
+
return json(r);
|
|
163
|
+
});
|
|
164
|
+
server.tool('get_stage_plan', 'Get the stage plan of a goal.', { goalId: S }, async (args) => {
|
|
165
|
+
const r = toResult(() => listStages(ctxOf(), args.goalId));
|
|
166
|
+
return json(r);
|
|
167
|
+
});
|
|
168
|
+
server.tool('update_stage', 'Update one stage (title/window/status/exit criteria).', {
|
|
169
|
+
stageId: S,
|
|
170
|
+
title: optS,
|
|
171
|
+
objective: optS,
|
|
172
|
+
order: z.number().optional(),
|
|
173
|
+
plannedStartDate: optS,
|
|
174
|
+
plannedEndDate: optS,
|
|
175
|
+
status: z.enum(['PLANNED', 'ACTIVE', 'COMPLETED', 'DELAYED', 'SKIPPED']).optional(),
|
|
176
|
+
exitCriteria: z.array(S).optional(),
|
|
177
|
+
reason: optS,
|
|
178
|
+
}, async (args) => {
|
|
179
|
+
const { stageId, reason, ...patch } = args;
|
|
180
|
+
const r = toResult(() => updateStage(ctxOf(), { id: stageId, ...patch }, reason ?? 'update stage'));
|
|
181
|
+
return json(r);
|
|
182
|
+
});
|
|
183
|
+
// ---------------- Today / Actions ----------------
|
|
184
|
+
server.tool('get_today_plan', "Get today's plan and actions.", {}, async () => {
|
|
185
|
+
const r = toResult(() => getTodayPlan(ctxOf()));
|
|
186
|
+
return json(r);
|
|
187
|
+
});
|
|
188
|
+
server.tool('plan_today', "Create today's plan: exactly one mainQuest (with whyToday, completionCriteria, estimatedMinutes) + 0~2 dailyActions. Show it to the user for confirmation — do not self-confirm.", {
|
|
189
|
+
mainQuest: z.object({
|
|
190
|
+
title: S,
|
|
191
|
+
description: optS,
|
|
192
|
+
whyToday: S,
|
|
193
|
+
estimatedMinutes: z.number(),
|
|
194
|
+
completionCriteria: S,
|
|
195
|
+
evidenceRequirements: optS,
|
|
196
|
+
verificationStrategy: optS,
|
|
197
|
+
basis: optS,
|
|
198
|
+
basisGoalId: optS,
|
|
199
|
+
basisStageId: optS,
|
|
200
|
+
basisDirectionId: optS,
|
|
201
|
+
}).optional(),
|
|
202
|
+
dailyActions: z.array(z.object({
|
|
203
|
+
title: S,
|
|
204
|
+
description: optS,
|
|
205
|
+
estimatedMinutes: z.number().optional(),
|
|
206
|
+
basis: optS,
|
|
207
|
+
basisDirectionId: optS,
|
|
208
|
+
basisGoalId: optS,
|
|
209
|
+
domain: z.enum(['LEARN', 'BUILD', 'TRAIN', 'HABIT', 'CREATE', 'RELATIONSHIP', 'EXPLORE', 'LIFE', 'OTHER']).optional(),
|
|
210
|
+
})).optional(),
|
|
211
|
+
rationale: optS,
|
|
212
|
+
}, async (args) => {
|
|
213
|
+
const r = toResult(() => createTodayPlan(ctxOf(), args));
|
|
214
|
+
return json(r);
|
|
215
|
+
});
|
|
216
|
+
server.tool('confirm_today_plan', 'Confirm today plan after user agreement. Moves DRAFT actions to PLANNED.', { planId: optS }, async (args) => {
|
|
217
|
+
const r = toResult(() => confirmTodayPlan(ctxOf(), args.planId));
|
|
218
|
+
return json(r);
|
|
219
|
+
});
|
|
220
|
+
server.tool('create_action', 'Create a standalone execution-level action (agent-autonomous).', {
|
|
221
|
+
title: S,
|
|
222
|
+
kind: z.enum(['MAIN_QUEST', 'DAILY']).optional(),
|
|
223
|
+
domain: z.enum(['LEARN', 'BUILD', 'TRAIN', 'HABIT', 'CREATE', 'RELATIONSHIP', 'EXPLORE', 'LIFE', 'OTHER']).optional(),
|
|
224
|
+
description: optS,
|
|
225
|
+
whyToday: optS,
|
|
226
|
+
estimatedMinutes: z.number().optional(),
|
|
227
|
+
completionCriteria: optS,
|
|
228
|
+
evidenceRequirements: optS,
|
|
229
|
+
verificationStrategy: optS,
|
|
230
|
+
basis: optS,
|
|
231
|
+
basisGoalId: optS,
|
|
232
|
+
basisDirectionId: optS,
|
|
233
|
+
basisStageId: optS,
|
|
234
|
+
dateKey: optS,
|
|
235
|
+
}, async (args) => {
|
|
236
|
+
const r = toResult(() => createAction(ctxOf(), args));
|
|
237
|
+
return json(r);
|
|
238
|
+
});
|
|
239
|
+
server.tool('update_action', 'Update an action (agent-autonomous for execution level).', {
|
|
240
|
+
actionId: S,
|
|
241
|
+
title: optS,
|
|
242
|
+
description: optS,
|
|
243
|
+
whyToday: optS,
|
|
244
|
+
estimatedMinutes: z.number().optional(),
|
|
245
|
+
completionCriteria: optS,
|
|
246
|
+
evidenceRequirements: optS,
|
|
247
|
+
verificationStrategy: optS,
|
|
248
|
+
dateKey: optS,
|
|
249
|
+
reason: optS,
|
|
250
|
+
}, async (args) => {
|
|
251
|
+
const { actionId, reason, ...patch } = args;
|
|
252
|
+
const r = toResult(() => updateAction(ctxOf(), actionId, patch, reason ?? 'agent update action'));
|
|
253
|
+
return json(r);
|
|
254
|
+
});
|
|
255
|
+
server.tool('start_action', 'Mark action in progress.', { actionId: S }, async (args) => {
|
|
256
|
+
const r = toResult(() => startAction(ctxOf(), args.actionId));
|
|
257
|
+
return json(r);
|
|
258
|
+
});
|
|
259
|
+
server.tool('complete_action', 'Complete an action. Low-risk life tasks may be confirmed with one sentence.', {
|
|
260
|
+
actionId: S,
|
|
261
|
+
note: optS,
|
|
262
|
+
}, async (args) => {
|
|
263
|
+
const r = toResult(() => {
|
|
264
|
+
const ctx = ctxOf();
|
|
265
|
+
const a = completeAction(ctx, args.actionId);
|
|
266
|
+
if (args.note) {
|
|
267
|
+
addEvidence(ctx, { actionId: a.id, type: 'TEXT', strength: 'USER_CONFIRMED', content: args.note });
|
|
268
|
+
}
|
|
269
|
+
return a;
|
|
270
|
+
});
|
|
271
|
+
return json(r);
|
|
272
|
+
});
|
|
273
|
+
server.tool('skip_action', 'Skip an action. If the user says the task is unreasonable, prefer add_task_feedback first.', {
|
|
274
|
+
actionId: S,
|
|
275
|
+
reason: optS,
|
|
276
|
+
}, async (args) => {
|
|
277
|
+
const r = toResult(() => skipAction(ctxOf(), args.actionId, args.reason));
|
|
278
|
+
return json(r);
|
|
279
|
+
});
|
|
280
|
+
server.tool('delay_action', 'Delay an action (stays open, does not expire like DAILY).', {
|
|
281
|
+
actionId: S,
|
|
282
|
+
reason: optS,
|
|
283
|
+
}, async (args) => {
|
|
284
|
+
const r = toResult(() => delayAction(ctxOf(), args.actionId, args.reason));
|
|
285
|
+
return json(r);
|
|
286
|
+
});
|
|
287
|
+
server.tool('cancel_action', 'Cancel an action.', { actionId: S, reason: S }, async (args) => {
|
|
288
|
+
const r = toResult(() => cancelAction(ctxOf(), args.actionId, args.reason));
|
|
289
|
+
return json(r);
|
|
290
|
+
});
|
|
291
|
+
server.tool('list_actions', 'List actions for a date (default today).', { dateKey: optS }, async (args) => {
|
|
292
|
+
const r = toResult(() => listActionsForDate(ctxOf(), args.dateKey ?? todayIn(ctxOf())));
|
|
293
|
+
return json(r);
|
|
294
|
+
});
|
|
295
|
+
// ---------------- Feedback ----------------
|
|
296
|
+
server.tool('add_task_feedback', "Record why user rejected/modified a task. Use when user says 'too easy'/'bad timing'/'not interested' etc.", {
|
|
297
|
+
actionId: S,
|
|
298
|
+
reason: z.enum(['NOT_INTERESTED', 'BAD_TIMING', 'TOO_HARD', 'TOO_EASY', 'ALREADY_DONE', 'NOT_RELEVANT', 'NO_TIME', 'OTHER']),
|
|
299
|
+
comment: optS,
|
|
300
|
+
}, async (args) => {
|
|
301
|
+
const r = toResult(() => addTaskFeedback(ctxOf(), args.actionId, args.reason, args.comment));
|
|
302
|
+
return json(r);
|
|
303
|
+
});
|
|
304
|
+
server.tool('recent_feedback_stats', 'Feedback stats (last N days) — use to reduce repeat recommendations.', { days: z.number().optional() }, async (args) => {
|
|
305
|
+
const r = toResult(() => recentFeedbackStats(ctxOf(), args.days ?? 14));
|
|
306
|
+
return json(r);
|
|
307
|
+
});
|
|
308
|
+
// ---------------- Replan ----------------
|
|
309
|
+
server.tool('replan', 'Resolve a pending replan. resolution: CONTINUE | ADJUST | SPLIT | DELAY_STAGE | CANCEL. Delaying stages that clearly moves the final deadline requires prior user confirmation.', {
|
|
310
|
+
actionId: optS,
|
|
311
|
+
resolution: z.enum(['CONTINUE', 'ADJUST', 'SPLIT', 'DELAY_STAGE', 'CANCEL']),
|
|
312
|
+
reason: S,
|
|
313
|
+
adjust: z.object({
|
|
314
|
+
title: optS,
|
|
315
|
+
description: optS,
|
|
316
|
+
estimatedMinutes: z.number().optional(),
|
|
317
|
+
completionCriteria: optS,
|
|
318
|
+
verificationStrategy: optS,
|
|
319
|
+
}).optional(),
|
|
320
|
+
splitInto: z.array(z.object({
|
|
321
|
+
title: S,
|
|
322
|
+
estimatedMinutes: z.number().optional(),
|
|
323
|
+
completionCriteria: optS,
|
|
324
|
+
})).optional(),
|
|
325
|
+
delayDays: z.number().optional(),
|
|
326
|
+
}, async (args) => {
|
|
327
|
+
const r = toResult(() => resolveReplan(ctxOf(), args));
|
|
328
|
+
return json(r);
|
|
329
|
+
});
|
|
330
|
+
server.tool('get_replan_hints', 'Pending replan list + replan priority rules.', {}, async () => {
|
|
331
|
+
const r = toResult(() => replanHints(ctxOf()));
|
|
332
|
+
return json(r);
|
|
333
|
+
});
|
|
334
|
+
// ---------------- Evidence / Assessment ----------------
|
|
335
|
+
server.tool('add_evidence', 'Record evidence. Types: TEXT/FILE/GIT/METRIC/QUIZ/ARTIFACT/REFLECTION/MANUAL. Strength defaults by type (GIT/METRIC/QUIZ/ARTIFACT → AUTO_VERIFIED).', {
|
|
336
|
+
actionId: optS,
|
|
337
|
+
type: z.enum(['TEXT', 'FILE', 'GIT', 'METRIC', 'QUIZ', 'ARTIFACT', 'REFLECTION', 'MANUAL']),
|
|
338
|
+
strength: z.enum(['AUTO_VERIFIED', 'USER_CONFIRMED', 'AGENT_OBSERVED', 'UNVERIFIED']).optional(),
|
|
339
|
+
content: S,
|
|
340
|
+
metadata: z.record(z.unknown()).optional(),
|
|
341
|
+
createGrowthEvent: z.boolean().optional(),
|
|
342
|
+
eventTitle: optS,
|
|
343
|
+
eventDomain: z.enum(['LEARN', 'BUILD', 'TRAIN', 'HABIT', 'CREATE', 'RELATIONSHIP', 'EXPLORE', 'LIFE', 'OTHER']).optional(),
|
|
344
|
+
}, async (args) => {
|
|
345
|
+
const r = toResult(() => {
|
|
346
|
+
const ctx = ctxOf();
|
|
347
|
+
const ev = addEvidence(ctx, {
|
|
348
|
+
actionId: args.actionId,
|
|
349
|
+
type: args.type,
|
|
350
|
+
strength: args.strength,
|
|
351
|
+
content: args.content,
|
|
352
|
+
metadata: args.metadata,
|
|
353
|
+
});
|
|
354
|
+
let event = null;
|
|
355
|
+
if (args.createGrowthEvent) {
|
|
356
|
+
// 需要关联 action 时从该 action 继承 goal/stage,保证 Growth Event 可追溯
|
|
357
|
+
const action = args.actionId ? getAction(ctx, args.actionId) : null;
|
|
358
|
+
event = createGrowthEvent(ctx, {
|
|
359
|
+
title: args.eventTitle ?? ev.content.slice(0, 60),
|
|
360
|
+
domain: args.eventDomain ?? action?.domain,
|
|
361
|
+
evidenceIds: [ev.id],
|
|
362
|
+
goalId: action?.basisGoalId,
|
|
363
|
+
stageId: action?.basisStageId,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return { evidence: ev, event };
|
|
367
|
+
});
|
|
368
|
+
return json(r);
|
|
369
|
+
});
|
|
370
|
+
server.tool('list_evidence', 'List evidence with filters.', {
|
|
371
|
+
actionId: optS,
|
|
372
|
+
sinceDateKey: optS,
|
|
373
|
+
type: z.enum(['TEXT', 'FILE', 'GIT', 'METRIC', 'QUIZ', 'ARTIFACT', 'REFLECTION', 'MANUAL']).optional(),
|
|
374
|
+
limit: z.number().optional(),
|
|
375
|
+
}, async (args) => {
|
|
376
|
+
const r = toResult(() => listEvidence(ctxOf(), args));
|
|
377
|
+
return json(r);
|
|
378
|
+
});
|
|
379
|
+
server.tool('create_assessment', 'Create a learning assessment (quiz/explain/challenge).', {
|
|
380
|
+
topic: S,
|
|
381
|
+
method: z.enum(['explain', 'quiz', 'coding-challenge', 'practical', 'mini-project', 'real-project']).optional(),
|
|
382
|
+
actionId: optS,
|
|
383
|
+
goalId: optS,
|
|
384
|
+
}, async (args) => {
|
|
385
|
+
const r = toResult(() => createAssessment(ctxOf(), args));
|
|
386
|
+
return json(r);
|
|
387
|
+
});
|
|
388
|
+
server.tool('submit_assessment', 'Submit assessment result. Levels: EXPOSURE/RECALL/EXPLAIN/APPLY/REAL_WORLD_EVIDENCE. Failed assessment does NOT upgrade knowledge state.', {
|
|
389
|
+
assessmentId: S,
|
|
390
|
+
passed: z.boolean(),
|
|
391
|
+
level: z.enum(['EXPOSURE', 'RECALL', 'EXPLAIN', 'APPLY', 'REAL_WORLD_EVIDENCE']),
|
|
392
|
+
result: z.record(z.unknown()).optional(),
|
|
393
|
+
}, async (args) => {
|
|
394
|
+
const r = toResult(() => submitAssessment(ctxOf(), { ...args, id: args.assessmentId }));
|
|
395
|
+
return json(r);
|
|
396
|
+
});
|
|
397
|
+
server.tool('list_assessments', 'List assessment history.', { goalId: optS, limit: z.number().optional() }, async (args) => {
|
|
398
|
+
const r = toResult(() => listAssessments(ctxOf(), args.goalId, args.limit));
|
|
399
|
+
return json(r);
|
|
400
|
+
});
|
|
401
|
+
server.tool('get_recent_growth', 'Recent growth events with evidence.', { limit: z.number().optional() }, async (args) => {
|
|
402
|
+
const r = toResult(() => listGrowthEvents(ctxOf(), undefined, args.limit ?? 20));
|
|
403
|
+
return json(r);
|
|
404
|
+
});
|
|
405
|
+
server.tool('create_growth_event', 'Create a growth event after MEANINGFUL real progress only. Requires evidenceIds — no evidence-free judgments.', {
|
|
406
|
+
title: S,
|
|
407
|
+
description: optS,
|
|
408
|
+
domain: z.enum(['LEARN', 'BUILD', 'TRAIN', 'HABIT', 'CREATE', 'RELATIONSHIP', 'EXPLORE', 'LIFE', 'OTHER']).optional(),
|
|
409
|
+
evidenceIds: z.array(S),
|
|
410
|
+
goalId: optS,
|
|
411
|
+
stageId: optS,
|
|
412
|
+
tags: z.array(S).optional(),
|
|
413
|
+
}, async (args) => {
|
|
414
|
+
const r = toResult(() => createGrowthEvent(ctxOf(), args));
|
|
415
|
+
return json(r);
|
|
416
|
+
});
|
|
417
|
+
// ---------------- Life context ----------------
|
|
418
|
+
server.tool('get_life_context', 'Current active life context (NORMAL if none).', {}, async () => {
|
|
419
|
+
const r = toResult(() => getActiveLifeContext(ctxOf()));
|
|
420
|
+
return json(r);
|
|
421
|
+
});
|
|
422
|
+
server.tool('set_life_context', "Set life context (BUSY/RECOVERY/TRAVEL/FOCUS/CUSTOM). E.g. user says 'this week only 30min/day'. Planner must adapt load.", {
|
|
423
|
+
mode: z.enum(['NORMAL', 'BUSY', 'RECOVERY', 'TRAVEL', 'FOCUS', 'CUSTOM']),
|
|
424
|
+
availableMinutesPerDay: z.number().optional(),
|
|
425
|
+
startDate: optS,
|
|
426
|
+
expectedEndDate: optS,
|
|
427
|
+
note: optS,
|
|
428
|
+
userConfirmed: z.boolean().optional(),
|
|
429
|
+
}, async (args) => {
|
|
430
|
+
const r = toResult(() => setLifeContext(ctxOf(), args));
|
|
431
|
+
return json(r);
|
|
432
|
+
});
|
|
433
|
+
// ---------------- Memory ----------------
|
|
434
|
+
server.tool('add_memory', 'Store a memory. Inferences must include confidence and stay unconfirmed until user agrees.', {
|
|
435
|
+
type: z.enum(['FACT', 'PATTERN', 'DECISION', 'CHANGE']),
|
|
436
|
+
content: S,
|
|
437
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
438
|
+
evidenceRefs: z.array(S).optional(),
|
|
439
|
+
userConfirmed: z.boolean().optional(),
|
|
440
|
+
}, async (args) => {
|
|
441
|
+
const r = toResult(() => addMemory(ctxOf(), args));
|
|
442
|
+
return json(r);
|
|
443
|
+
});
|
|
444
|
+
server.tool('list_memories', 'List memories.', { type: z.enum(['FACT', 'PATTERN', 'DECISION', 'CHANGE']).optional(), limit: z.number().optional() }, async (args) => {
|
|
445
|
+
const r = toResult(() => listMemories(ctxOf(), args));
|
|
446
|
+
return json(r);
|
|
447
|
+
});
|
|
448
|
+
server.tool('confirm_memory', 'Mark a memory as user-confirmed (stable fact).', { memoryId: S }, async (args) => {
|
|
449
|
+
const r = toResult(() => confirmMemory(ctxOf(), args.memoryId));
|
|
450
|
+
return json(r);
|
|
451
|
+
});
|
|
452
|
+
// ---------------- Review ----------------
|
|
453
|
+
server.tool('daily_review', 'Summarize today: completion, evidence, feedback. Returns a structured summary the agent can present/extend.', {
|
|
454
|
+
note: optS,
|
|
455
|
+
}, async (args) => {
|
|
456
|
+
const r = toResult(() => {
|
|
457
|
+
const ctx = ctxOf();
|
|
458
|
+
const today = todayIn(ctx);
|
|
459
|
+
const actions = listActionsForDate(ctx, today);
|
|
460
|
+
const summary = {
|
|
461
|
+
date: today,
|
|
462
|
+
mainQuest: actions.find((a) => a.kind === 'MAIN_QUEST') ?? null,
|
|
463
|
+
dailyActions: actions.filter((a) => a.kind === 'DAILY'),
|
|
464
|
+
evidenceToday: listEvidence(ctx, { sinceDateKey: today }),
|
|
465
|
+
pendingReplan: listPendingReplanActions(ctx).map((p) => p.id),
|
|
466
|
+
};
|
|
467
|
+
if (args.note) {
|
|
468
|
+
addEvidence(ctx, { type: 'REFLECTION', strength: 'USER_CONFIRMED', content: args.note });
|
|
469
|
+
}
|
|
470
|
+
flagPassportDirty(ctx, 'daily review');
|
|
471
|
+
return summary;
|
|
472
|
+
});
|
|
473
|
+
return json(r);
|
|
474
|
+
});
|
|
475
|
+
server.tool('weekly_review', 'Weekly summary: goal/stage progress, completion patterns, metrics, replan suggestions.', {}, async () => {
|
|
476
|
+
const r = toResult(() => {
|
|
477
|
+
const ctx = ctxOf();
|
|
478
|
+
const since = localDateKey(new Date(Date.now() - 7 * 86400_000), ctx.tz);
|
|
479
|
+
return {
|
|
480
|
+
since,
|
|
481
|
+
goals: getActiveGoals(ctx).map((g) => ({ id: g.id, title: g.title, stages: listStages(ctx, g.id) })),
|
|
482
|
+
metrics: computeMetrics(ctx, 7),
|
|
483
|
+
growthEvents: listGrowthEvents(ctx, since, 30),
|
|
484
|
+
feedback: recentFeedbackStats(ctx, 7),
|
|
485
|
+
};
|
|
486
|
+
});
|
|
487
|
+
return json(r);
|
|
488
|
+
});
|
|
489
|
+
// ---------------- Planner context ----------------
|
|
490
|
+
server.tool('get_planner_context', 'Everything the planner needs: yesterday actions, recent evidence/feedback, metrics, life context. Use before plan_today.', {}, async () => {
|
|
491
|
+
const r = toResult(() => {
|
|
492
|
+
const ctx = ctxOf();
|
|
493
|
+
return { ...plannerContext(ctx), lifeContext: getActiveLifeContext(ctx) };
|
|
494
|
+
});
|
|
495
|
+
return json(r);
|
|
496
|
+
});
|
|
497
|
+
// ---------------- Reminders ----------------
|
|
498
|
+
server.tool('list_reminders', 'List reminders for a date (default today).', { dateKey: optS }, async (args) => {
|
|
499
|
+
const r = toResult(() => listRemindersForDate(ctxOf(), args.dateKey));
|
|
500
|
+
return json(r);
|
|
501
|
+
});
|
|
502
|
+
server.tool('create_reminder', 'Create a reminder bound to an action. Daily actions are limited to 1 scheduled reminder each. Do NOT guess very specific times — ask once in daily planning if unknown.', {
|
|
503
|
+
actionId: S,
|
|
504
|
+
scheduledAt: S,
|
|
505
|
+
channel: z.enum(['LOCAL_NOTIFICATION', 'WEB', 'OTHER']).optional(),
|
|
506
|
+
allowMultiple: z.boolean().optional(),
|
|
507
|
+
}, async (args) => {
|
|
508
|
+
const r = toResult(() => createReminder(ctxOf(), args));
|
|
509
|
+
return json(r);
|
|
510
|
+
});
|
|
511
|
+
server.tool('update_reminder', 'Reschedule a reminder.', { reminderId: S, scheduledAt: S }, async (args) => {
|
|
512
|
+
const r = toResult(() => updateReminder(ctxOf(), args.reminderId, args.scheduledAt));
|
|
513
|
+
return json(r);
|
|
514
|
+
});
|
|
515
|
+
server.tool('cancel_reminder', 'Cancel a reminder.', { reminderId: S, reason: optS }, async (args) => {
|
|
516
|
+
const r = toResult(() => cancelReminder(ctxOf(), args.reminderId, args.reason ?? 'cancelled'));
|
|
517
|
+
return json(r);
|
|
518
|
+
});
|
|
519
|
+
server.tool('cancel_action_reminders', 'Cancel all scheduled reminders of an action.', { actionId: S }, async (args) => {
|
|
520
|
+
const r = toResult(() => ({ cancelled: cancelRemindersByAction(ctxOf(), args.actionId, 'user cancelled') }));
|
|
521
|
+
return json(r);
|
|
522
|
+
});
|
|
523
|
+
// ---------------- Passport ----------------
|
|
524
|
+
server.tool('get_passport', 'Get latest passport snapshot (or rebuild).', {}, async () => {
|
|
525
|
+
const r = toResult(() => {
|
|
526
|
+
const ctx = ctxOf();
|
|
527
|
+
const snap = latestSnapshot(ctx);
|
|
528
|
+
return snap ?? { snapshot: null, dirty: isPassportDirty(ctx) };
|
|
529
|
+
});
|
|
530
|
+
return json(r);
|
|
531
|
+
});
|
|
532
|
+
server.tool('rebuild_passport', 'Rebuild passport from source of truth (DB + memory).', {}, async () => {
|
|
533
|
+
const r = toResult(() => generatePassport(ctxOf()));
|
|
534
|
+
return json(r);
|
|
535
|
+
});
|
|
536
|
+
server.tool('sync_passport', 'Sync passport via configured adapter (local-json by default). Failures are queued for retry and never break local state.', {}, async () => {
|
|
537
|
+
const r = await toResultAsync(() => syncPassport(ctxOf(), createPassportAdapter(cfg)));
|
|
538
|
+
return json(r);
|
|
539
|
+
});
|
|
540
|
+
server.tool('retry_passport_sync', 'Retry pending/failed passport sync jobs.', {}, async () => {
|
|
541
|
+
const r = await toResultAsync(() => retryPendingSyncs(ctxOf(), createPassportAdapter(cfg)));
|
|
542
|
+
return json(r);
|
|
543
|
+
});
|
|
544
|
+
server.tool('list_sync_jobs', 'List passport sync jobs (status/attempts/lastError).', {}, async () => {
|
|
545
|
+
const r = toResult(() => listSyncJobs(ctxOf()));
|
|
546
|
+
return json(r);
|
|
547
|
+
});
|
|
548
|
+
// ---------------- Settings ----------------
|
|
549
|
+
server.tool('get_setting', 'Read a setting (e.g. timezone).', { key: S }, async (args) => {
|
|
550
|
+
const r = toResult(() => ({ key: args.key, value: getSetting(ctxOf(), args.key) }));
|
|
551
|
+
return json(r);
|
|
552
|
+
});
|
|
553
|
+
server.tool('set_setting', 'Write a setting (e.g. timezone).', { key: S, value: S }, async (args) => {
|
|
554
|
+
const r = toResult(() => {
|
|
555
|
+
setSetting(ctxOf(), args.key, args.value);
|
|
556
|
+
return { key: args.key, value: args.value };
|
|
557
|
+
});
|
|
558
|
+
return json(r);
|
|
559
|
+
});
|
|
560
|
+
server.tool('render_progress', '产出**带样式的进度报告**(Markdown + Mermaid 甘特图 + 进度条 + 事实表),用于把进度以非纯文字形式展示给用户。只想看结构化数据用 growth_status/get_today_plan;要展示给人看用这个。', {
|
|
561
|
+
format: z.enum(['markdown', 'html']).optional(),
|
|
562
|
+
}, async (args) => {
|
|
563
|
+
const r = toResult(() => {
|
|
564
|
+
const ctx = ctxOf();
|
|
565
|
+
const report = renderProgress(ctx);
|
|
566
|
+
return {
|
|
567
|
+
title: report.title,
|
|
568
|
+
facts: report.facts,
|
|
569
|
+
markdown: report.markdown,
|
|
570
|
+
html: args.format === 'html' ? report.html : undefined,
|
|
571
|
+
};
|
|
572
|
+
});
|
|
573
|
+
return json(r);
|
|
574
|
+
});
|
|
575
|
+
server.tool('get_metrics', 'Local planner metrics (completion rate by kind, replan rate, feedback distribution, reminder response). For planner improvement — NOT a self-discipline score.', { windowDays: z.number().optional() }, async (args) => {
|
|
576
|
+
const r = toResult(() => computeMetrics(ctxOf(), args.windowDays ?? 14));
|
|
577
|
+
return json(r);
|
|
578
|
+
});
|
|
579
|
+
server.tool('server_time', 'Current ISO time and local date in configured timezone.', {}, async () => {
|
|
580
|
+
const ctx = ctxOf();
|
|
581
|
+
return json(ok({ iso: nowIso(), localDate: todayIn(ctx), timezone: ctx.tz }));
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
function json(r) {
|
|
585
|
+
return { content: [{ type: 'text', text: JSON.stringify(r, null, 2) }] };
|
|
586
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { getProfile } from '../core/profile.js';
|
|
2
|
+
import { getActiveGoals, getCurrentStage, listStages } from '../core/goals.js';
|
|
3
|
+
import { listGrowthEvents } from '../core/evidence.js';
|
|
4
|
+
import { listMemories } from '../core/memory.js';
|
|
5
|
+
import { getActiveLifeContext } from '../core/lifeContext.js';
|
|
6
|
+
import { saveSnapshot, flagPassportDirty } from './state.js';
|
|
7
|
+
import { nowIso } from '../shared/time.js';
|
|
8
|
+
export const PASSPORT_SCHEMA_VERSION = '1.1.0';
|
|
9
|
+
/** 从 Source of Truth(Growth DB + Memory)重建 Passport */
|
|
10
|
+
export function buildPassport(ctx) {
|
|
11
|
+
const profile = getProfile(ctx);
|
|
12
|
+
const goals = getActiveGoals(ctx);
|
|
13
|
+
const life = getActiveLifeContext(ctx);
|
|
14
|
+
const activeGoals = goals.slice(0, 3).map((g) => {
|
|
15
|
+
const stage = getCurrentStage(ctx, g.id);
|
|
16
|
+
const all = listStages(ctx, g.id);
|
|
17
|
+
return {
|
|
18
|
+
id: g.id,
|
|
19
|
+
title: g.title,
|
|
20
|
+
why: g.why,
|
|
21
|
+
desiredOutcome: g.desiredOutcome,
|
|
22
|
+
targetDate: g.targetDate,
|
|
23
|
+
currentStage: stage ? { title: stage.title, order: stage.order, total: all.length } : undefined,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
const growthEvents = listGrowthEvents(ctx, undefined, 10);
|
|
27
|
+
// capabilities:来自 assessments 的最近知识状态(§22)
|
|
28
|
+
// 注意:同一 topic 可能有多条评估,必须取"最近一次提交"。
|
|
29
|
+
// created_at 在批量写入时会撞同一毫秒,故用 updated_at + rowid 作为稳定排序键。
|
|
30
|
+
const assessmentRows = ctx.db
|
|
31
|
+
.prepare(`SELECT topic, level, passed, updated_at FROM assessments ORDER BY updated_at DESC, rowid DESC LIMIT 50`)
|
|
32
|
+
.all();
|
|
33
|
+
const latestByTopic = new Map();
|
|
34
|
+
for (const a of assessmentRows)
|
|
35
|
+
if (!latestByTopic.has(a.topic))
|
|
36
|
+
latestByTopic.set(a.topic, a);
|
|
37
|
+
const capabilities = [...latestByTopic.values()]
|
|
38
|
+
.filter((a) => a.passed)
|
|
39
|
+
.slice(0, 12)
|
|
40
|
+
.map((a) => ({ topic: a.topic, level: a.level, basis: 'assessment', assessedAt: a.updated_at }));
|
|
41
|
+
// 记忆中的稳定偏好(仅用户确认过的)
|
|
42
|
+
const confirmedStable = listMemories(ctx, { limit: 30 })
|
|
43
|
+
.filter((m) => m.confirmedByUser && m.type === 'FACT')
|
|
44
|
+
.map((m) => m.content)
|
|
45
|
+
.slice(0, 8);
|
|
46
|
+
const planningHints = [];
|
|
47
|
+
if (life?.mode && life.mode !== 'NORMAL') {
|
|
48
|
+
planningHints.push(`LifeContext: ${life.mode}${life.availableMinutesPerDay ? `, ~${life.availableMinutesPerDay} min/day` : ''}`);
|
|
49
|
+
}
|
|
50
|
+
planningHints.push('Daily Actions must trace to user-confirmed directions only');
|
|
51
|
+
planningHints.push('Default load: 1 Main Quest + 0~2 Daily Actions');
|
|
52
|
+
return {
|
|
53
|
+
schemaVersion: PASSPORT_SCHEMA_VERSION,
|
|
54
|
+
generatedAt: nowIso(),
|
|
55
|
+
identity: {
|
|
56
|
+
displayName: profile?.displayName,
|
|
57
|
+
stablePreferences: confirmedStable,
|
|
58
|
+
},
|
|
59
|
+
currentState: {
|
|
60
|
+
currentChapter: activeGoals[0]?.currentStage?.title,
|
|
61
|
+
activeGoals,
|
|
62
|
+
lifeContext: life ? `${life.mode}${life.note ? `: ${life.note}` : ''}` : undefined,
|
|
63
|
+
currentFocus: goals.map((g) => g.title).slice(0, 3),
|
|
64
|
+
},
|
|
65
|
+
capabilities,
|
|
66
|
+
recentGrowth: growthEvents.map((e) => ({
|
|
67
|
+
date: e.dateKey,
|
|
68
|
+
title: e.title,
|
|
69
|
+
domain: e.domain,
|
|
70
|
+
evidenceCount: e.evidenceIds.length,
|
|
71
|
+
})),
|
|
72
|
+
planningHints,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function generatePassport(ctx) {
|
|
76
|
+
const passport = buildPassport(ctx);
|
|
77
|
+
saveSnapshot(ctx, JSON.stringify(passport));
|
|
78
|
+
return passport;
|
|
79
|
+
}
|
|
80
|
+
export { flagPassportDirty, recentEvidenceCount };
|
|
81
|
+
function recentEvidenceCount(ctx) {
|
|
82
|
+
return ctx.db.prepare('SELECT COUNT(*) AS n FROM evidence').get().n;
|
|
83
|
+
}
|