agentrem 1.0.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/dist/core.js ADDED
@@ -0,0 +1,816 @@
1
+ // ── Core Business Logic ───────────────────────────────────────────────────
2
+ // Pure functions that return data. No stdout, no process.exit.
3
+ import { execFileSync } from 'node:child_process';
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
+ import * as os from 'node:os';
7
+ import { SCHEMA_VERSION, VALID_TRIGGERS, AgentremError, } from './types.js';
8
+ import { parseDate, dtToIso, truncate, parseRecur, nextRecurrence } from './date-parser.js';
9
+ import { findReminder, recordHistory } from './db.js';
10
+ export function coreAdd(db, opts) {
11
+ const content = opts.content;
12
+ const trigger = opts.trigger || 'time';
13
+ const priority = opts.priority || 3;
14
+ if (priority < 1 || priority > 5) {
15
+ throw new AgentremError('Priority must be 1-5');
16
+ }
17
+ if (!VALID_TRIGGERS.has(trigger)) {
18
+ throw new AgentremError(`Invalid trigger type: '${trigger}'. Must be one of: ${[...VALID_TRIGGERS].sort().join(', ')}`);
19
+ }
20
+ // Parse due date
21
+ let triggerAt = null;
22
+ if (opts.due) {
23
+ triggerAt = dtToIso(parseDate(opts.due));
24
+ }
25
+ // Validation
26
+ if (trigger === 'time' && !triggerAt) {
27
+ throw new AgentremError('Time trigger requires --due / -d flag');
28
+ }
29
+ if (trigger === 'keyword' && !opts.keywords) {
30
+ throw new AgentremError('Keyword trigger requires --keywords / -k flag');
31
+ }
32
+ if (trigger === 'condition' && (!opts.check || !opts.expect)) {
33
+ throw new AgentremError('Condition trigger requires both --check and --expect flags');
34
+ }
35
+ // Build trigger_config
36
+ let triggerConfig = null;
37
+ if (trigger === 'keyword') {
38
+ triggerConfig = JSON.stringify({
39
+ keywords: opts.keywords.split(',').map((k) => k.trim()),
40
+ match: opts.match || 'any',
41
+ });
42
+ }
43
+ else if (trigger === 'condition') {
44
+ triggerConfig = JSON.stringify({
45
+ check: opts.check,
46
+ expect: opts.expect,
47
+ });
48
+ }
49
+ // Parse decay
50
+ let decayAt = null;
51
+ if (opts.decay) {
52
+ decayAt = dtToIso(parseDate(opts.decay));
53
+ }
54
+ // Parse recurrence
55
+ let recurRule = null;
56
+ if (opts.recur) {
57
+ recurRule = JSON.stringify(parseRecur(opts.recur));
58
+ }
59
+ // Validate depends_on
60
+ if (opts.dependsOn) {
61
+ const dep = findReminder(db, opts.dependsOn);
62
+ if (!dep) {
63
+ throw new AgentremError(`Reminder not found: ${opts.dependsOn}`);
64
+ }
65
+ }
66
+ const source = opts.source || 'agent';
67
+ const agent = opts.agent || 'main';
68
+ if (opts.dryRun) {
69
+ // Return a fake reminder for dry run display
70
+ return {
71
+ id: 'dry-run',
72
+ content,
73
+ context: opts.context || null,
74
+ trigger_type: trigger,
75
+ trigger_at: triggerAt,
76
+ trigger_config: triggerConfig,
77
+ priority,
78
+ tags: opts.tags || null,
79
+ category: opts.category || null,
80
+ status: 'active',
81
+ snoozed_until: null,
82
+ decay_at: decayAt,
83
+ escalation: null,
84
+ fire_count: 0,
85
+ last_fired: null,
86
+ max_fires: opts.maxFires ?? null,
87
+ recur_rule: recurRule,
88
+ recur_parent_id: null,
89
+ depends_on: opts.dependsOn || null,
90
+ related_ids: null,
91
+ source,
92
+ agent,
93
+ created_at: dtToIso(new Date()),
94
+ updated_at: dtToIso(new Date()),
95
+ completed_at: null,
96
+ notes: null,
97
+ };
98
+ }
99
+ // Insert
100
+ const info = db
101
+ .prepare(`INSERT INTO reminders(content, context, trigger_type, trigger_at, trigger_config,
102
+ priority, tags, category, decay_at, max_fires, recur_rule, depends_on,
103
+ source, agent)
104
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
105
+ .run(content, opts.context || null, trigger, triggerAt, triggerConfig, priority, opts.tags || null, opts.category || null, decayAt, opts.maxFires ?? null, recurRule, opts.dependsOn || null, source, agent);
106
+ const rem = db
107
+ .prepare('SELECT * FROM reminders WHERE rowid = ?')
108
+ .get(info.lastInsertRowid);
109
+ recordHistory(db, rem.id, 'created', null, rem, source);
110
+ return rem;
111
+ }
112
+ export function coreCheck(db, opts) {
113
+ const now = new Date();
114
+ const nowIso = dtToIso(now);
115
+ const agent = opts.agent || 'main';
116
+ const budget = (opts.budget || 800) * 4; // tokens -> chars
117
+ const typesFilter = opts.type ? new Set(opts.type.split(',')) : null;
118
+ // 1. Reactivate snoozed reminders whose snooze has expired
119
+ db.prepare("UPDATE reminders SET status='active', snoozed_until=NULL, updated_at=? " +
120
+ "WHERE status='snoozed' AND snoozed_until <= ? AND agent=?").run(nowIso, nowIso, agent);
121
+ // 2. Expire decayed reminders
122
+ const expiredRows = db
123
+ .prepare("SELECT id FROM reminders WHERE decay_at <= ? AND status='active' AND agent=?")
124
+ .all(nowIso, agent);
125
+ for (const row of expiredRows) {
126
+ const rem = db.prepare('SELECT * FROM reminders WHERE id=?').get(row.id);
127
+ db.prepare("UPDATE reminders SET status='expired', updated_at=? WHERE id=?").run(nowIso, row.id);
128
+ recordHistory(db, row.id, 'expired', rem, null, 'system');
129
+ }
130
+ // 3. Escalation
131
+ if (opts.escalate) {
132
+ const cutoff48h = dtToIso(new Date(now.getTime() - 48 * 3600 * 1000));
133
+ db.prepare("UPDATE reminders SET priority=2, updated_at=? " +
134
+ "WHERE priority=3 AND trigger_type='time' AND trigger_at <= ? AND status='active' AND agent=?").run(nowIso, cutoff48h, agent);
135
+ const cutoff24h = dtToIso(new Date(now.getTime() - 24 * 3600 * 1000));
136
+ db.prepare("UPDATE reminders SET priority=1, updated_at=? " +
137
+ "WHERE priority=2 AND trigger_type='time' AND trigger_at <= ? AND status='active' AND agent=?").run(nowIso, cutoff24h, agent);
138
+ }
139
+ // 4. Gather triggered reminders
140
+ const triggered = [];
141
+ // Get all completed IDs for dependency checking
142
+ const completedIds = new Set(db
143
+ .prepare("SELECT id FROM reminders WHERE status='completed'")
144
+ .all().map((r) => r.id));
145
+ function checkDependency(rem) {
146
+ if (!rem.depends_on)
147
+ return true;
148
+ return completedIds.has(rem.depends_on);
149
+ }
150
+ // Time triggers
151
+ if (!typesFilter || typesFilter.has('time')) {
152
+ const rows = db
153
+ .prepare("SELECT * FROM reminders WHERE trigger_type='time' AND trigger_at <= ? " +
154
+ "AND status='active' AND agent=?")
155
+ .all(nowIso, agent);
156
+ for (const rem of rows) {
157
+ if (checkDependency(rem)) {
158
+ triggered.push(rem);
159
+ }
160
+ }
161
+ }
162
+ // Keyword triggers
163
+ if ((!typesFilter || typesFilter.has('keyword')) && opts.text) {
164
+ const rows = db
165
+ .prepare("SELECT * FROM reminders WHERE trigger_type='keyword' AND status='active' AND agent=?")
166
+ .all(agent);
167
+ const textLower = opts.text.toLowerCase();
168
+ for (const rem of rows) {
169
+ if (!checkDependency(rem))
170
+ continue;
171
+ const config = JSON.parse(rem.trigger_config || '{}');
172
+ const keywords = config.keywords || [];
173
+ const matchMode = config.match || 'any';
174
+ if (matchMode === 'any') {
175
+ if (keywords.some((kw) => textLower.includes(kw.toLowerCase()))) {
176
+ triggered.push(rem);
177
+ }
178
+ }
179
+ else if (matchMode === 'all') {
180
+ if (keywords.every((kw) => textLower.includes(kw.toLowerCase()))) {
181
+ triggered.push(rem);
182
+ }
183
+ }
184
+ else if (matchMode === 'regex') {
185
+ for (const kw of keywords) {
186
+ try {
187
+ if (new RegExp(kw, 'i').test(opts.text)) {
188
+ triggered.push(rem);
189
+ break;
190
+ }
191
+ }
192
+ catch {
193
+ // Invalid regex, skip
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ // Condition triggers
200
+ if (!typesFilter || typesFilter.has('condition')) {
201
+ const rows = db
202
+ .prepare("SELECT * FROM reminders WHERE trigger_type='condition' AND status='active' AND agent=?")
203
+ .all(agent);
204
+ for (const rem of rows) {
205
+ if (!checkDependency(rem))
206
+ continue;
207
+ const config = JSON.parse(rem.trigger_config || '{}');
208
+ try {
209
+ // Use execFileSync with shell for condition checks (user-defined commands)
210
+ const result = execFileSync('/bin/sh', ['-c', config.check], {
211
+ timeout: 10000,
212
+ encoding: 'utf8',
213
+ stdio: ['pipe', 'pipe', 'pipe'],
214
+ }).trim();
215
+ if (result === config.expect) {
216
+ triggered.push(rem);
217
+ }
218
+ }
219
+ catch {
220
+ // Command failed or timed out
221
+ }
222
+ }
223
+ }
224
+ // Session triggers
225
+ if (!typesFilter || typesFilter.has('session')) {
226
+ const rows = db
227
+ .prepare("SELECT * FROM reminders WHERE trigger_type='session' AND status='active' AND agent=?")
228
+ .all(agent);
229
+ for (const rem of rows) {
230
+ if (checkDependency(rem)) {
231
+ triggered.push(rem);
232
+ }
233
+ }
234
+ }
235
+ // Heartbeat triggers
236
+ if (!typesFilter || typesFilter.has('heartbeat')) {
237
+ const rows = db
238
+ .prepare("SELECT * FROM reminders WHERE trigger_type='heartbeat' AND status='active' AND agent=?")
239
+ .all(agent);
240
+ for (const rem of rows) {
241
+ if (checkDependency(rem)) {
242
+ triggered.push(rem);
243
+ }
244
+ }
245
+ }
246
+ // Manual triggers are never auto-injected
247
+ if (triggered.length === 0) {
248
+ return { included: [], overflowCounts: { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }, totalTriggered: 0 };
249
+ }
250
+ // Sort by priority
251
+ triggered.sort((a, b) => a.priority - b.priority);
252
+ // Deduplicate by ID
253
+ const seen = new Set();
254
+ const deduped = [];
255
+ for (const rem of triggered) {
256
+ if (!seen.has(rem.id)) {
257
+ seen.add(rem.id);
258
+ deduped.push(rem);
259
+ }
260
+ }
261
+ // Budget system
262
+ const charLimits = { 1: 200, 2: 100, 3: 60, 4: 0, 5: 0 };
263
+ let used = 0;
264
+ const included = [];
265
+ const overflowCounts = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
266
+ for (const rem of deduped) {
267
+ const p = rem.priority;
268
+ if (p === 5)
269
+ continue;
270
+ if (p === 4) {
271
+ overflowCounts[4]++;
272
+ continue;
273
+ }
274
+ const limit = charLimits[p];
275
+ const contentText = truncate(rem.content, limit);
276
+ const entrySize = contentText.length + 30;
277
+ if (p === 1) {
278
+ included.push(rem);
279
+ used += entrySize;
280
+ }
281
+ else if (p === 2) {
282
+ if (used + entrySize <= budget * 0.6) {
283
+ included.push(rem);
284
+ used += entrySize;
285
+ }
286
+ else {
287
+ overflowCounts[2]++;
288
+ }
289
+ }
290
+ else if (p === 3) {
291
+ if (used + entrySize <= budget * 0.85) {
292
+ included.push(rem);
293
+ used += entrySize;
294
+ }
295
+ else {
296
+ overflowCounts[3]++;
297
+ }
298
+ }
299
+ }
300
+ // Update fire counts (unless dry run)
301
+ if (!opts.dryRun) {
302
+ for (const rem of included) {
303
+ const newFire = (rem.fire_count || 0) + 1;
304
+ db.prepare('UPDATE reminders SET fire_count=?, last_fired=?, updated_at=? WHERE id=?').run(newFire, nowIso, nowIso, rem.id);
305
+ if (rem.max_fires && newFire >= rem.max_fires) {
306
+ const old = { ...rem };
307
+ db.prepare("UPDATE reminders SET status='completed', completed_at=?, updated_at=? WHERE id=?").run(nowIso, nowIso, rem.id);
308
+ const remAfter = db
309
+ .prepare('SELECT * FROM reminders WHERE id=?')
310
+ .get(rem.id);
311
+ recordHistory(db, rem.id, 'completed', old, remAfter, 'system');
312
+ }
313
+ }
314
+ }
315
+ return { included, overflowCounts, totalTriggered: deduped.length };
316
+ }
317
+ export function coreList(db, opts) {
318
+ const agent = opts.agent || 'main';
319
+ const limit = opts.limit || 20;
320
+ const conditions = [];
321
+ const params = [];
322
+ if (opts.all) {
323
+ // no status filter
324
+ }
325
+ else if (opts.status) {
326
+ const statuses = opts.status.split(',').map((s) => s.trim());
327
+ conditions.push(`status IN (${statuses.map(() => '?').join(',')})`);
328
+ params.push(...statuses);
329
+ }
330
+ else {
331
+ conditions.push("status = 'active'");
332
+ }
333
+ conditions.push('agent = ?');
334
+ params.push(agent);
335
+ if (opts.priority) {
336
+ const prios = opts.priority.split(',').map((p) => parseInt(p.trim(), 10));
337
+ conditions.push(`priority IN (${prios.map(() => '?').join(',')})`);
338
+ params.push(...prios);
339
+ }
340
+ if (opts.tag) {
341
+ conditions.push('tags LIKE ?');
342
+ params.push(`%${opts.tag}%`);
343
+ }
344
+ if (opts.trigger) {
345
+ conditions.push('trigger_type = ?');
346
+ params.push(opts.trigger);
347
+ }
348
+ if (opts.category) {
349
+ conditions.push('category = ?');
350
+ params.push(opts.category);
351
+ }
352
+ if (opts.due) {
353
+ const now = new Date();
354
+ const d = opts.due.toLowerCase();
355
+ if (d === 'today') {
356
+ const eod = new Date(now);
357
+ eod.setHours(23, 59, 59, 0);
358
+ conditions.push("trigger_at <= ? AND trigger_type='time'");
359
+ params.push(dtToIso(eod));
360
+ }
361
+ else if (d === 'tomorrow') {
362
+ const tmrw = new Date(now);
363
+ tmrw.setDate(tmrw.getDate() + 1);
364
+ tmrw.setHours(23, 59, 59, 0);
365
+ conditions.push("trigger_at <= ? AND trigger_type='time'");
366
+ params.push(dtToIso(tmrw));
367
+ }
368
+ else if (d === 'overdue') {
369
+ conditions.push("trigger_at <= ? AND trigger_type='time'");
370
+ params.push(dtToIso(now));
371
+ }
372
+ else if (d === 'week') {
373
+ const eow = new Date(now);
374
+ eow.setDate(eow.getDate() + 7);
375
+ eow.setHours(23, 59, 59, 0);
376
+ conditions.push("trigger_at <= ? AND trigger_type='time'");
377
+ params.push(dtToIso(eow));
378
+ }
379
+ else {
380
+ const dt = parseDate(d);
381
+ conditions.push('DATE(trigger_at) = DATE(?)');
382
+ params.push(dtToIso(dt));
383
+ }
384
+ }
385
+ const where = conditions.length > 0 ? conditions.join(' AND ') : '1=1';
386
+ const query = `SELECT * FROM reminders WHERE ${where} ORDER BY priority, trigger_at LIMIT ?`;
387
+ params.push(limit);
388
+ return db.prepare(query).all(...params);
389
+ }
390
+ export function coreSearch(db, opts) {
391
+ const limit = opts.limit || 10;
392
+ const statuses = opts.status ? opts.status.split(',').map((s) => s.trim()) : ['active'];
393
+ const placeholders = statuses.map(() => '?').join(',');
394
+ return db
395
+ .prepare(`SELECT r.* FROM reminders_fts f
396
+ JOIN reminders r ON r.rowid = f.rowid
397
+ WHERE reminders_fts MATCH ? AND r.status IN (${placeholders})
398
+ ORDER BY rank LIMIT ?`)
399
+ .all(opts.query, ...statuses, limit);
400
+ }
401
+ export function coreComplete(db, id, notes) {
402
+ const rem = findReminder(db, id);
403
+ if (!rem) {
404
+ throw new AgentremError(`Reminder not found: ${id}`);
405
+ }
406
+ const nowIso = dtToIso(new Date());
407
+ const oldData = { ...rem };
408
+ let nextRem = null;
409
+ // Check for recurrence
410
+ if (rem.recur_rule) {
411
+ const rule = JSON.parse(rem.recur_rule);
412
+ const nextDt = nextRecurrence(rem.trigger_at, rule);
413
+ const info = db
414
+ .prepare(`INSERT INTO reminders(content, context, trigger_type, trigger_at, trigger_config,
415
+ priority, tags, category, decay_at, max_fires, recur_rule, recur_parent_id,
416
+ depends_on, source, agent)
417
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
418
+ .run(rem.content, rem.context, rem.trigger_type, dtToIso(nextDt), rem.trigger_config, rem.priority, rem.tags, rem.category, rem.decay_at, rem.max_fires, rem.recur_rule, rem.recur_parent_id || rem.id, rem.depends_on, rem.source, rem.agent);
419
+ nextRem = db
420
+ .prepare('SELECT * FROM reminders WHERE rowid = ?')
421
+ .get(info.lastInsertRowid);
422
+ recordHistory(db, nextRem.id, 'created', null, nextRem, 'system');
423
+ }
424
+ // Complete the current one
425
+ let finalNotes = notes || null;
426
+ if (notes && rem.notes) {
427
+ finalNotes = rem.notes + '\n' + notes;
428
+ }
429
+ else if (!notes) {
430
+ finalNotes = rem.notes;
431
+ }
432
+ db.prepare("UPDATE reminders SET status='completed', completed_at=?, updated_at=?, notes=? WHERE id=?").run(nowIso, nowIso, finalNotes, rem.id);
433
+ const newData = db.prepare('SELECT * FROM reminders WHERE id=?').get(rem.id);
434
+ recordHistory(db, rem.id, 'completed', oldData, newData, 'agent');
435
+ return { completed: newData, nextRecurrence: nextRem };
436
+ }
437
+ // ── Snooze ────────────────────────────────────────────────────────────────
438
+ export function coreSnooze(db, id, until, forDuration) {
439
+ const rem = findReminder(db, id);
440
+ if (!rem) {
441
+ throw new AgentremError(`Reminder not found: ${id}`);
442
+ }
443
+ if (!until && !forDuration) {
444
+ throw new AgentremError('Snooze requires --until or --for');
445
+ }
446
+ let snoozeDt;
447
+ if (until) {
448
+ snoozeDt = parseDate(until);
449
+ }
450
+ else {
451
+ // Parse duration like "1h", "2h", "1d", "3d", "1w"
452
+ try {
453
+ snoozeDt = parseDate(`+${forDuration}`);
454
+ }
455
+ catch {
456
+ const m = /^(\d+)([mhdw])$/i.exec(forDuration);
457
+ if (!m) {
458
+ throw new AgentremError(`Cannot parse duration: '${forDuration}'`);
459
+ }
460
+ const n = parseInt(m[1], 10);
461
+ const u = m[2].toLowerCase();
462
+ snoozeDt = new Date();
463
+ if (u === 'm')
464
+ snoozeDt.setMinutes(snoozeDt.getMinutes() + n);
465
+ else if (u === 'h')
466
+ snoozeDt.setHours(snoozeDt.getHours() + n);
467
+ else if (u === 'd')
468
+ snoozeDt.setDate(snoozeDt.getDate() + n);
469
+ else if (u === 'w')
470
+ snoozeDt.setDate(snoozeDt.getDate() + n * 7);
471
+ }
472
+ }
473
+ const nowIso = dtToIso(new Date());
474
+ const oldData = { ...rem };
475
+ db.prepare("UPDATE reminders SET status='snoozed', snoozed_until=?, updated_at=? WHERE id=?").run(dtToIso(snoozeDt), nowIso, rem.id);
476
+ const newData = db.prepare('SELECT * FROM reminders WHERE id=?').get(rem.id);
477
+ recordHistory(db, rem.id, 'snoozed', oldData, newData, 'agent');
478
+ return newData;
479
+ }
480
+ export function coreEdit(db, id, opts) {
481
+ const rem = findReminder(db, id);
482
+ if (!rem) {
483
+ throw new AgentremError(`Reminder not found: ${id}`);
484
+ }
485
+ const oldData = { ...rem };
486
+ const nowIso = dtToIso(new Date());
487
+ const updates = {};
488
+ if (opts.content !== undefined)
489
+ updates['content'] = opts.content;
490
+ if (opts.context !== undefined)
491
+ updates['context'] = opts.context;
492
+ if (opts.priority !== undefined) {
493
+ if (opts.priority < 1 || opts.priority > 5) {
494
+ throw new AgentremError('Priority must be 1-5');
495
+ }
496
+ updates['priority'] = opts.priority;
497
+ }
498
+ if (opts.due !== undefined) {
499
+ updates['trigger_at'] = dtToIso(parseDate(opts.due));
500
+ }
501
+ if (opts.tags !== undefined) {
502
+ updates['tags'] = opts.tags;
503
+ }
504
+ if (opts.addTags) {
505
+ const existing = new Set((rem.tags || '')
506
+ .split(',')
507
+ .map((t) => t.trim())
508
+ .filter(Boolean));
509
+ const newTags = opts.addTags
510
+ .split(',')
511
+ .map((t) => t.trim())
512
+ .filter(Boolean);
513
+ for (const t of newTags)
514
+ existing.add(t);
515
+ updates['tags'] = [...existing].sort().join(',');
516
+ }
517
+ if (opts.removeTags) {
518
+ const existing = new Set((rem.tags || '')
519
+ .split(',')
520
+ .map((t) => t.trim())
521
+ .filter(Boolean));
522
+ const rmTags = opts.removeTags
523
+ .split(',')
524
+ .map((t) => t.trim())
525
+ .filter(Boolean);
526
+ for (const t of rmTags)
527
+ existing.delete(t);
528
+ updates['tags'] = [...existing].sort().join(',');
529
+ }
530
+ if (opts.category !== undefined)
531
+ updates['category'] = opts.category;
532
+ if (opts.decay !== undefined) {
533
+ updates['decay_at'] = dtToIso(parseDate(opts.decay));
534
+ }
535
+ if (opts.maxFires !== undefined)
536
+ updates['max_fires'] = opts.maxFires;
537
+ if (opts.keywords !== undefined) {
538
+ const config = JSON.parse(rem.trigger_config || '{}');
539
+ config.keywords = opts.keywords.split(',').map((k) => k.trim());
540
+ updates['trigger_config'] = JSON.stringify(config);
541
+ }
542
+ if (opts.agent !== undefined)
543
+ updates['agent'] = opts.agent;
544
+ if (Object.keys(updates).length === 0) {
545
+ throw new AgentremError('No changes specified. Use --content, --priority, --due, --tags, etc.');
546
+ }
547
+ updates['updated_at'] = nowIso;
548
+ const setClause = Object.keys(updates)
549
+ .map((k) => `${k}=?`)
550
+ .join(', ');
551
+ const values = [...Object.values(updates), rem.id];
552
+ db.prepare(`UPDATE reminders SET ${setClause} WHERE id=?`).run(...values);
553
+ const newData = db.prepare('SELECT * FROM reminders WHERE id=?').get(rem.id);
554
+ recordHistory(db, rem.id, 'updated', oldData, newData, 'agent');
555
+ return newData;
556
+ }
557
+ export function coreDelete(db, opts) {
558
+ const nowIso = dtToIso(new Date());
559
+ // Bulk delete by status
560
+ if (opts.status) {
561
+ const conditions = ['status = ?'];
562
+ const params = [opts.status];
563
+ if (opts.olderThan) {
564
+ const cutoff = new Date();
565
+ cutoff.setDate(cutoff.getDate() - parseInt(opts.olderThan, 10));
566
+ conditions.push('updated_at <= ?');
567
+ params.push(dtToIso(cutoff));
568
+ }
569
+ const where = conditions.join(' AND ');
570
+ const countRow = db
571
+ .prepare(`SELECT COUNT(*) as c FROM reminders WHERE ${where}`)
572
+ .get(...params);
573
+ const count = countRow.c;
574
+ if (opts.permanent) {
575
+ db.prepare(`DELETE FROM reminders WHERE ${where}`).run(...params);
576
+ }
577
+ else {
578
+ db.prepare(`UPDATE reminders SET status='deleted', updated_at=? WHERE ${where}`).run(nowIso, ...params);
579
+ }
580
+ return { count, permanent: !!opts.permanent };
581
+ }
582
+ if (!opts.id) {
583
+ throw new AgentremError('Reminder ID required (or use --status for bulk delete)');
584
+ }
585
+ const rem = findReminder(db, opts.id);
586
+ if (!rem) {
587
+ throw new AgentremError(`Reminder not found: ${opts.id}`);
588
+ }
589
+ const oldData = { ...rem };
590
+ if (opts.permanent) {
591
+ db.prepare('DELETE FROM reminders WHERE id=?').run(rem.id);
592
+ recordHistory(db, rem.id, 'deleted', oldData, null, 'agent');
593
+ }
594
+ else {
595
+ db.prepare("UPDATE reminders SET status='deleted', updated_at=? WHERE id=?").run(nowIso, rem.id);
596
+ const newData = db.prepare('SELECT * FROM reminders WHERE id=?').get(rem.id);
597
+ recordHistory(db, rem.id, 'deleted', oldData, newData, 'agent');
598
+ }
599
+ return { count: 1, permanent: !!opts.permanent };
600
+ }
601
+ export function coreStats(db) {
602
+ const nowIso = dtToIso(new Date());
603
+ const active = db
604
+ .prepare("SELECT priority, COUNT(*) as cnt FROM reminders WHERE status='active' GROUP BY priority ORDER BY priority")
605
+ .all();
606
+ const totalActive = active.reduce((sum, r) => sum + r.cnt, 0);
607
+ const byPriority = active.map((r) => ({
608
+ priority: r.priority,
609
+ count: r.cnt,
610
+ label: { 1: 'critical', 2: 'high', 3: 'normal', 4: 'low', 5: 'someday' }[r.priority] || `p${r.priority}`,
611
+ }));
612
+ const overdueRow = db
613
+ .prepare("SELECT COUNT(*) as c FROM reminders WHERE trigger_type='time' AND trigger_at <= ? AND status='active'")
614
+ .get(nowIso);
615
+ const snoozedRow = db
616
+ .prepare("SELECT COUNT(*) as c FROM reminders WHERE status='snoozed'")
617
+ .get();
618
+ const weekAgo = new Date();
619
+ weekAgo.setDate(weekAgo.getDate() - 7);
620
+ const completedWeekRow = db
621
+ .prepare("SELECT COUNT(*) as c FROM reminders WHERE status='completed' AND completed_at >= ?")
622
+ .get(dtToIso(weekAgo));
623
+ const expiredRow = db
624
+ .prepare("SELECT COUNT(*) as c FROM reminders WHERE status='expired'")
625
+ .get();
626
+ const triggers = db
627
+ .prepare("SELECT trigger_type, COUNT(*) as cnt FROM reminders WHERE status='active' GROUP BY trigger_type ORDER BY cnt DESC")
628
+ .all();
629
+ const byTrigger = triggers.map((r) => ({ type: r.trigger_type, count: r.cnt }));
630
+ const nextDueRow = db
631
+ .prepare("SELECT content, trigger_at FROM reminders WHERE trigger_type='time' AND trigger_at > ? AND status='active' ORDER BY trigger_at LIMIT 1")
632
+ .get(nowIso);
633
+ const lastRow = db
634
+ .prepare('SELECT created_at FROM reminders ORDER BY created_at DESC LIMIT 1')
635
+ .get();
636
+ // DB size
637
+ let dbSizeBytes = 0;
638
+ try {
639
+ const dbPath = process.env['AGENTREM_DB'] ||
640
+ path.join(process.env['AGENTREM_DIR'] || path.join(os.homedir(), '.agentrem'), 'reminders.db');
641
+ if (fs.existsSync(dbPath)) {
642
+ dbSizeBytes = fs.statSync(dbPath).size;
643
+ }
644
+ }
645
+ catch {
646
+ // ignore
647
+ }
648
+ return {
649
+ totalActive,
650
+ byPriority,
651
+ overdue: overdueRow.c,
652
+ snoozed: snoozedRow.c,
653
+ completedWeek: completedWeekRow.c,
654
+ expired: expiredRow.c,
655
+ byTrigger,
656
+ nextDue: nextDueRow
657
+ ? { content: nextDueRow.content, triggerAt: nextDueRow.trigger_at }
658
+ : null,
659
+ lastCreated: lastRow?.created_at || null,
660
+ dbSizeBytes,
661
+ };
662
+ }
663
+ export function coreGc(db, olderThan = 30, dryRun = false) {
664
+ const cutoff = new Date();
665
+ cutoff.setDate(cutoff.getDate() - olderThan);
666
+ const cutoffIso = dtToIso(cutoff);
667
+ const rows = db
668
+ .prepare("SELECT id, status, content FROM reminders WHERE status IN ('completed', 'expired', 'deleted') AND updated_at <= ?")
669
+ .all(cutoffIso);
670
+ if (rows.length === 0 || dryRun) {
671
+ return { count: rows.length, reminders: rows };
672
+ }
673
+ const ids = rows.map((r) => r.id);
674
+ const placeholders = ids.map(() => '?').join(',');
675
+ db.prepare(`DELETE FROM reminders WHERE id IN (${placeholders})`).run(...ids);
676
+ db.prepare(`DELETE FROM history WHERE reminder_id IN (${placeholders})`).run(...ids);
677
+ db.exec('VACUUM');
678
+ return { count: rows.length, reminders: rows };
679
+ }
680
+ // ── History ───────────────────────────────────────────────────────────────
681
+ export function coreHistory(db, id, limit = 20) {
682
+ if (id) {
683
+ // Try to resolve the reminder ID first
684
+ const rem = findReminder(db, id);
685
+ const rid = rem ? rem.id : id;
686
+ return db
687
+ .prepare('SELECT * FROM history WHERE reminder_id = ? OR reminder_id LIKE ? ORDER BY timestamp DESC LIMIT ?')
688
+ .all(rid, rid + '%', limit);
689
+ }
690
+ return db
691
+ .prepare('SELECT * FROM history ORDER BY timestamp DESC LIMIT ?')
692
+ .all(limit);
693
+ }
694
+ // ── Undo ──────────────────────────────────────────────────────────────────
695
+ export function coreUndo(db, historyId) {
696
+ const hist = db
697
+ .prepare('SELECT * FROM history WHERE id = ?')
698
+ .get(historyId);
699
+ if (!hist) {
700
+ throw new AgentremError(`History entry not found: ${historyId}`);
701
+ }
702
+ if (hist.action === 'created') {
703
+ throw new AgentremError('Cannot undo creation — use `agentrem delete` instead');
704
+ }
705
+ if (!hist.old_data) {
706
+ throw new AgentremError('No old data to restore');
707
+ }
708
+ const old = JSON.parse(hist.old_data);
709
+ const rem = findReminder(db, hist.reminder_id);
710
+ if (!rem) {
711
+ // Reminder might have been permanently deleted — recreate
712
+ const cols = Object.keys(old);
713
+ const placeholders = cols.map(() => '?').join(',');
714
+ const colNames = cols.join(',');
715
+ db.prepare(`INSERT INTO reminders(${colNames}) VALUES (${placeholders})`).run(...cols.map((c) => old[c]));
716
+ }
717
+ else {
718
+ // Update to old state
719
+ const nowIso = dtToIso(new Date());
720
+ old['updated_at'] = nowIso;
721
+ const setClause = Object.keys(old)
722
+ .filter((k) => k !== 'id')
723
+ .map((k) => `${k}=?`)
724
+ .join(', ');
725
+ const values = Object.keys(old)
726
+ .filter((k) => k !== 'id')
727
+ .map((k) => old[k]);
728
+ values.push(hist.reminder_id);
729
+ db.prepare(`UPDATE reminders SET ${setClause} WHERE id=?`).run(...values);
730
+ }
731
+ recordHistory(db, hist.reminder_id, 'reverted', rem, old, 'agent');
732
+ }
733
+ export function coreExport(db, status) {
734
+ const conditions = [];
735
+ const params = [];
736
+ if (status) {
737
+ const statuses = status.split(',').map((s) => s.trim());
738
+ conditions.push(`status IN (${statuses.map(() => '?').join(',')})`);
739
+ params.push(...statuses);
740
+ }
741
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
742
+ const reminders = db
743
+ .prepare(`SELECT * FROM reminders ${where}`)
744
+ .all(...params);
745
+ const allHistory = [];
746
+ for (const rem of reminders) {
747
+ const h = db
748
+ .prepare('SELECT * FROM history WHERE reminder_id = ?')
749
+ .all(rem['id']);
750
+ allHistory.push(...h);
751
+ }
752
+ return {
753
+ exported_at: dtToIso(new Date()),
754
+ schema_version: SCHEMA_VERSION,
755
+ reminder_count: reminders.length,
756
+ reminders,
757
+ history: allHistory,
758
+ };
759
+ }
760
+ export function coreImport(db, data, merge = false, replace = false, dryRun = false) {
761
+ const reminders = data.reminders || [];
762
+ const history = data.history || [];
763
+ if (dryRun) {
764
+ return {
765
+ imported: reminders.length,
766
+ skipped: 0,
767
+ historyImported: history.length,
768
+ };
769
+ }
770
+ if (replace) {
771
+ db.prepare('DELETE FROM reminders').run();
772
+ db.prepare('DELETE FROM history').run();
773
+ }
774
+ let imported = 0;
775
+ let skipped = 0;
776
+ for (const rem of reminders) {
777
+ if (merge) {
778
+ const existing = db
779
+ .prepare('SELECT id FROM reminders WHERE id = ?')
780
+ .get(rem['id']);
781
+ if (existing) {
782
+ skipped++;
783
+ continue;
784
+ }
785
+ }
786
+ const cols = Object.keys(rem);
787
+ const placeholders = cols.map(() => '?').join(',');
788
+ const colNames = cols.join(',');
789
+ try {
790
+ db.prepare(`INSERT INTO reminders(${colNames}) VALUES (${placeholders})`).run(...cols.map((c) => rem[c]));
791
+ imported++;
792
+ }
793
+ catch {
794
+ skipped++;
795
+ }
796
+ }
797
+ let historyImported = 0;
798
+ for (const h of history) {
799
+ try {
800
+ db.prepare('INSERT INTO history(reminder_id, action, old_data, new_data, timestamp, source) VALUES (?, ?, ?, ?, ?, ?)').run(h['reminder_id'], h['action'], h['old_data'] || null, h['new_data'] || null, h['timestamp'], h['source'] || null);
801
+ historyImported++;
802
+ }
803
+ catch {
804
+ // skip
805
+ }
806
+ }
807
+ return { imported, skipped, historyImported };
808
+ }
809
+ // ── Schema ────────────────────────────────────────────────────────────────
810
+ export function coreSchema(db) {
811
+ const rows = db
812
+ .prepare('SELECT sql FROM sqlite_master WHERE sql IS NOT NULL ORDER BY type, name')
813
+ .all();
814
+ return rows.map((r) => r.sql);
815
+ }
816
+ //# sourceMappingURL=core.js.map