obol-ai 0.3.32 → 0.3.34

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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## 0.3.34
2
+ - generic PostgREST filters for memory, self-memory, and events
3
+ - drop select= filters — return all columns from memory and events
4
+
5
+ ## 0.3.33
6
+ - schedule wizard — use config timezone, drop tz selector
7
+
1
8
  ## 0.3.32
2
9
  - tz confirm button shows toast feedback
3
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "obol-ai",
3
- "version": "0.3.32",
3
+ "version": "0.3.34",
4
4
  "description": "Self-evolving AI assistant that learns, remembers, and acts on its own. Persistent vector memory, self-rewriting personality, proactive heartbeats.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -40,7 +40,7 @@ const definitions = [
40
40
  },
41
41
  {
42
42
  name: 'memory_query',
43
- description: 'Filter memories by tag, date, category, source, or importance. Use for "what did we do today", "anything tagged X", "all decisions this week".',
43
+ description: 'Filter memories by tag, date, category, source, importance, or any column. Use for "what did we do today", "anything tagged X", "all decisions this week".',
44
44
  input_schema: {
45
45
  type: 'object',
46
46
  properties: {
@@ -50,6 +50,7 @@ const definitions = [
50
50
  source: { type: 'string', description: 'Filter by source (e.g. "turn-extraction", "evolution-3")' },
51
51
  minImportance: { type: 'number', description: 'Minimum importance threshold (0-1)' },
52
52
  limit: { type: 'number', description: 'Max results (default 20)' },
53
+ filters: { type: 'object', description: 'PostgREST column filters. Key = column name, value = operator.value. E.g. {"importance":"gte.0.8","source":"eq.evolution-3","content":"like.*bitcoin*","access_count":"gte.5"}', additionalProperties: { type: 'string' } },
53
54
  },
54
55
  },
55
56
  },
@@ -98,6 +99,7 @@ const handlers = {
98
99
  source: input.source,
99
100
  minImportance: input.minImportance,
100
101
  limit: input.limit,
102
+ filters: input.filters,
101
103
  });
102
104
  return JSON.stringify(results.map(formatMemory));
103
105
  },
@@ -34,6 +34,7 @@ Always search memory first for the user's timezone/location.`,
34
34
  type: 'object',
35
35
  properties: {
36
36
  status: { type: 'string', enum: ['pending', 'sent', 'cancelled', 'completed'], description: 'Filter by status (default: pending)' },
37
+ filters: { type: 'object', description: 'PostgREST column filters. Key = column name, value = operator.value. E.g. {"title":"like.*briefing*","cron_expr":"not.is.null","run_count":"gte.5"}', additionalProperties: { type: 'string' } },
37
38
  },
38
39
  },
39
40
  },
@@ -107,7 +108,7 @@ const handlers = {
107
108
 
108
109
  async list_events(input, memory, context) {
109
110
  if (!context.scheduler) return 'Scheduler not available (Supabase not configured).';
110
- const events = await context.scheduler.list({ status: input.status });
111
+ const events = await context.scheduler.list({ status: input.status, filters: input.filters });
111
112
  if (events.length === 0) return `No ${input.status || 'pending'} events.`;
112
113
  return JSON.stringify(events.map(e => {
113
114
  const entry = {
@@ -80,7 +80,7 @@ async function createMemory(supabaseConfig, userId = 0) {
80
80
  const { start, end } = parseDateRange(dateStr);
81
81
  const limit = opts.limit || 50;
82
82
 
83
- let fetchUrl = `${url}/rest/v1/obol_memory?select=id,content,category,tags,importance,source,created_at&created_at=gte.${start.toISOString()}&created_at=lt.${end.toISOString()}&order=created_at.asc&limit=${limit}&user_id=eq.${userId}`;
83
+ let fetchUrl = `${url}/rest/v1/obol_memory?created_at=gte.${start.toISOString()}&created_at=lt.${end.toISOString()}&order=created_at.asc&limit=${limit}&user_id=eq.${userId}`;
84
84
  if (opts.category) fetchUrl += `&category=eq.${opts.category}`;
85
85
 
86
86
  const res = await fetch(fetchUrl, { headers });
@@ -91,7 +91,7 @@ async function createMemory(supabaseConfig, userId = 0) {
91
91
 
92
92
  async function recent(opts = {}) {
93
93
  const limit = opts.limit || 10;
94
- let fetchUrl = `${url}/rest/v1/obol_memory?select=id,content,category,tags,importance,source,created_at&order=created_at.desc&limit=${limit}&user_id=eq.${userId}`;
94
+ let fetchUrl = `${url}/rest/v1/obol_memory?order=created_at.desc&limit=${limit}&user_id=eq.${userId}`;
95
95
  if (opts.category) fetchUrl += `&category=eq.${opts.category}`;
96
96
 
97
97
  const res = await fetch(fetchUrl, { headers });
@@ -129,7 +129,7 @@ async function createMemory(supabaseConfig, userId = 0) {
129
129
 
130
130
  async function stats() {
131
131
  const countHeaders = { ...headers, 'Prefer': 'count=exact' };
132
- const res = await fetch(`${url}/rest/v1/obol_memory?select=category&user_id=eq.${userId}`, { headers: countHeaders });
132
+ const res = await fetch(`${url}/rest/v1/obol_memory?user_id=eq.${userId}`, { headers: countHeaders });
133
133
  if (!res.ok) throw new Error(`Stats failed: HTTP ${res.status}`);
134
134
  const contentRange = res.headers?.get?.('content-range');
135
135
  const data = await res.json();
@@ -159,9 +159,14 @@ async function createMemory(supabaseConfig, userId = 0) {
159
159
  parts.push(`created_at=gte.${start.toISOString()}`);
160
160
  parts.push(`created_at=lt.${end.toISOString()}`);
161
161
  }
162
+ if (opts.filters) {
163
+ for (const [col, op] of Object.entries(opts.filters)) {
164
+ if (/^[a-z_]+$/.test(col)) parts.push(`${col}=${op}`);
165
+ }
166
+ }
162
167
  const filter = parts.join('&');
163
168
  const res = await fetch(
164
- `${url}/rest/v1/obol_memory?select=id,content,category,tags,importance,source,created_at&${filter}&order=created_at.desc&limit=${limit}`,
169
+ `${url}/rest/v1/obol_memory?${filter}&order=created_at.desc&limit=${limit}`,
165
170
  { headers }
166
171
  );
167
172
  const data = await res.json();
@@ -64,7 +64,7 @@ async function createSelfMemory(supabaseConfig, userId) {
64
64
 
65
65
  async function recent(opts = {}) {
66
66
  const limit = opts.limit || 10;
67
- let fetchUrl = `${url}/rest/v1/obol_self_memory?select=id,content,category,tags,importance,source,created_at&order=created_at.desc&limit=${limit}&user_id=eq.${userId}`;
67
+ let fetchUrl = `${url}/rest/v1/obol_self_memory?order=created_at.desc&limit=${limit}&user_id=eq.${userId}`;
68
68
  if (opts.category) fetchUrl += `&category=eq.${opts.category}`;
69
69
 
70
70
  const res = await fetch(fetchUrl, { headers });
@@ -79,9 +79,14 @@ async function createSelfMemory(supabaseConfig, userId) {
79
79
  if (opts.source) parts.push(`source=eq.${opts.source}`);
80
80
  if (opts.minImportance) parts.push(`importance=gte.${opts.minImportance}`);
81
81
  if (opts.tags?.length) parts.push(`tags=ov.{${opts.tags.join(',')}}`);
82
+ if (opts.filters) {
83
+ for (const [col, op] of Object.entries(opts.filters)) {
84
+ if (/^[a-z_]+$/.test(col)) parts.push(`${col}=${op}`);
85
+ }
86
+ }
82
87
 
83
88
  const res = await fetch(
84
- `${url}/rest/v1/obol_self_memory?select=id,content,category,tags,importance,source,created_at&${parts.join('&')}&order=created_at.desc&limit=${limit}`,
89
+ `${url}/rest/v1/obol_self_memory?${parts.join('&')}&order=created_at.desc&limit=${limit}`,
85
90
  { headers }
86
91
  );
87
92
  const data = await res.json();
@@ -37,7 +37,12 @@ function createScheduler(supabaseConfig, userId = 0) {
37
37
  async function list(opts = {}) {
38
38
  const status = opts.status || 'pending';
39
39
  const limit = opts.limit || 20;
40
- const fetchUrl = `${url}/rest/v1/obol_events?user_id=eq.${userId}&status=eq.${status}&order=due_at.asc&limit=${limit}&select=id,title,description,instructions,due_at,timezone,status,created_at,cron_expr,last_run_at,run_count,max_runs,ends_at`;
40
+ let fetchUrl = `${url}/rest/v1/obol_events?user_id=eq.${userId}&status=eq.${status}&order=due_at.asc&limit=${limit}`;
41
+ if (opts.filters) {
42
+ for (const [col, op] of Object.entries(opts.filters)) {
43
+ if (/^[a-z_]+$/.test(col)) fetchUrl += `&${col}=${op}`;
44
+ }
45
+ }
41
46
  const res = await fetch(fetchUrl, { headers });
42
47
  const data = await res.json();
43
48
  if (!res.ok) throw new Error(JSON.stringify(data));
@@ -57,7 +62,7 @@ function createScheduler(supabaseConfig, userId = 0) {
57
62
 
58
63
  async function getDue() {
59
64
  const now = new Date().toISOString();
60
- const fetchUrl = `${url}/rest/v1/obol_events?status=eq.pending&due_at=lte.${now}&select=id,user_id,chat_id,title,description,due_at,timezone,cron_expr,run_count,max_runs,ends_at,instructions`;
65
+ const fetchUrl = `${url}/rest/v1/obol_events?status=eq.pending&due_at=lte.${now}`;
61
66
  const res = await fetch(fetchUrl, { headers });
62
67
  const data = await res.json();
63
68
  if (!res.ok) throw new Error(JSON.stringify(data));
@@ -78,36 +78,20 @@ async function stepTitle(ctx, userId) {
78
78
  trackMsg(userId, msg.chat.id, msg.message_id);
79
79
  }
80
80
 
81
- async function stepTime(ctx, userId) {
81
+ async function stepTime(ctx, userId, config) {
82
82
  const draft = schedDrafts.get(userId);
83
83
  if (!draft) return;
84
84
  draft.step = 'time';
85
85
 
86
- const { getTenant } = require('../tenant');
87
- let tz = 'UTC';
88
- try {
89
- const tenant = await getTenant(userId, ctx._config || {});
90
- if (tenant.memory) {
91
- const hits = await tenant.memory.search('timezone', { limit: 1, threshold: 0.3 });
92
- for (const h of hits) {
93
- const match = h.content.match(/(?:timezone|time zone)[:\s]+([A-Za-z_/]+)/i);
94
- if (match) { tz = match[1]; break; }
95
- }
96
- }
97
- } catch {}
98
-
86
+ const { getUserTimezone } = require('../config');
87
+ const tz = getUserTimezone(config, userId);
99
88
  draft.timezone = tz;
100
89
  setPendingInput(userId, 'time', TIME_TTL_MS);
101
90
 
102
- const kb = new InlineKeyboard()
103
- .text(`Use ${tz}`, 'sched:tz:confirm')
104
- .text('Change timezone', 'sched:tz:change');
105
-
106
91
  const msg = await sendHtml(ctx,
107
92
  `When should it ${draft.isRecurring ? 'first fire' : 'fire'}?\n\n` +
108
93
  `Examples: \`2026-03-10 14:00\`, \`tomorrow at 9am\`\n` +
109
- `Timezone: ${tz}`,
110
- { reply_markup: kb }
94
+ `Timezone: ${tz}`
111
95
  );
112
96
  trackMsg(userId, msg.chat.id, msg.message_id);
113
97
  }
@@ -331,31 +315,10 @@ async function handleSchedCallback(ctx, data, answer, { getTenant, config, bot }
331
315
  draft.isRecurring = value === 'recurring' || value === 'agentic-rec';
332
316
  draft.isAgentic = value === 'agentic' || value === 'agentic-rec';
333
317
  await answer();
334
- ctx._config = config;
335
318
  await stepTitle(ctx, userId);
336
319
  return;
337
320
  }
338
321
 
339
- if (action === 'tz') {
340
- const draft = schedDrafts.get(userId);
341
- if (!draft) return answer({ text: 'Session expired' });
342
-
343
- if (value === 'confirm') {
344
- await answer({ text: `Using ${draft.timezone} — now type the date/time` });
345
- return;
346
- }
347
- if (value === 'change') {
348
- await answer();
349
- cancelPending(userId);
350
- setPendingInput(userId, 'timezone', TIME_TTL_MS);
351
- const msg = await ctx.api.sendMessage(ctx.chat?.id || userId,
352
- 'Enter your timezone (e.g. Europe/Brussels, America/New_York, Asia/Tokyo):');
353
- trackMsg(userId, msg.chat.id, msg.message_id);
354
- return;
355
- }
356
- return answer();
357
- }
358
-
359
322
  if (action === 'cron') {
360
323
  const draft = schedDrafts.get(userId);
361
324
  if (!draft) return answer({ text: 'Session expired' });
@@ -432,7 +395,7 @@ async function handleSchedCallback(ctx, data, answer, { getTenant, config, bot }
432
395
 
433
396
  switch (value) {
434
397
  case 'title': await stepTitle(ctx, userId); break;
435
- case 'time': ctx._config = config; await stepTime(ctx, userId); break;
398
+ case 'time': await stepTime(ctx, userId, config); break;
436
399
  case 'cron': await stepCron(ctx, userId); break;
437
400
  case 'desc': await stepDescription(ctx, userId); break;
438
401
  case 'instr': await stepInstructions(ctx, userId); break;
@@ -458,23 +421,7 @@ async function handleSchedText(ctx, text, { getTenant, config, bot }) {
458
421
 
459
422
  if (field === 'title') {
460
423
  draft.title = text.substring(0, 200);
461
- ctx._config = config;
462
- await stepTime(ctx, userId);
463
- return;
464
- }
465
-
466
- if (field === 'timezone') {
467
- try {
468
- Intl.DateTimeFormat(undefined, { timeZone: text.trim() });
469
- draft.timezone = text.trim();
470
- const msg = await ctx.reply(`Timezone set to ${draft.timezone}. Now enter the date/time.`);
471
- trackMsg(userId, msg.chat.id, msg.message_id);
472
- setPendingInput(userId, 'time', TIME_TTL_MS);
473
- } catch {
474
- const msg = await ctx.reply('Invalid timezone. Try again (e.g. Europe/Brussels, America/New_York):');
475
- trackMsg(userId, msg.chat.id, msg.message_id);
476
- setPendingInput(userId, 'timezone', TIME_TTL_MS);
477
- }
424
+ await stepTime(ctx, userId, config);
478
425
  return;
479
426
  }
480
427