@curie-agent/tui 0.2.4 → 0.2.5

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.
@@ -1,4 +1,37 @@
1
- import { pickNextSchedule, scheduleLabel } from '@curie-agent/core';
1
+ import { pickNextSchedule } from '@curie-agent/core';
2
+ import { readFileSync, existsSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ /** Resolve the unified task file path for a scope. */
6
+ function resolveTaskPath(scope, cwd) {
7
+ if (scope === 'personal')
8
+ return join(homedir(), '.curie-agent', 'tasks.json');
9
+ return join(cwd, 'tasks.json');
10
+ }
11
+ /** Read tasks file; falls back to legacy todo.json. Returns null if neither exists. */
12
+ function readTaskJson(path) {
13
+ try {
14
+ if (!existsSync(path))
15
+ return null;
16
+ const raw = readFileSync(path, 'utf-8');
17
+ const data = JSON.parse(raw);
18
+ if (Array.isArray(data.tasks))
19
+ return data;
20
+ // Legacy format — check for plain array at root? No, legacy is {tasks: []} with no version/schema.
21
+ return null;
22
+ }
23
+ catch {
24
+ return null;
25
+ }
26
+ }
27
+ /** Normalize a legacy task record (missing mode/scope) to UnifiedTask fields. */
28
+ function normalizeTaskRecord(t) {
29
+ if (!t.mode)
30
+ t.mode = 'manual';
31
+ if (!t.scope)
32
+ t.scope = 'personal';
33
+ return t;
34
+ }
2
35
  const THINKING_BUDGET_MAP = {
3
36
  low: 2_000,
4
37
  medium: 6_000,
@@ -7,30 +40,41 @@ const THINKING_BUDGET_MAP = {
7
40
  auto: 0,
8
41
  };
9
42
  export const SLASH_COMMANDS = [
10
- { name: 'status', description: 'Show version, model, and account info', usage: '/status' },
11
- { name: 'help', description: 'Show all available commands', usage: '/help' },
12
- { name: 'debug', description: 'Toggle debug logging', usage: '/debug [on|off]' },
13
- { name: 'statusline', description: 'Toggle status line display', usage: '/statusline [on|off]' },
14
- { name: 'theme', description: 'Change color theme', usage: '/theme <name>' },
15
- { name: 'memory', description: 'View memory file sizes or capture a memory', usage: '/memory [status|add]' },
16
- { name: 'stats', description: 'Daily usage, sessions, streaks', usage: '/stats' },
17
- { name: 'context', description: 'Visual grid showing context window usage', usage: '/context [messages|compact [detailed|brief]]' },
18
- { name: 'model', description: 'Switch AI model, set pricing or context window', usage: '/model <model|pricing in;out|windows tokens>' },
19
- { name: 'effort', description: 'Set reasoning effort level', usage: '/effort <low|medium|high|max|auto>' },
20
- { name: 'mode', description: 'Set approval mode', usage: '/mode <manual|plan|auto-edit|full-auto|yolo>' },
21
- { name: 'agent', description: 'Launch external AI agent', usage: '/agent <prompt>' },
22
- { name: 'remind', description: 'Create a reminder', usage: '/remind <message at time>' },
23
- { name: 'cron', description: 'Manage reminders', usage: '/cron <list|delete|clear>' },
24
- { name: 'channels', description: 'Manage Telegram channel config', usage: '/channels <list|set-bot-token|set-user-id|set-chat-id|disconnect>' },
25
- { name: 'tools', description: 'View/set tool call limits per turn', usage: '/tools [tools_per_call [websearch_per_call]]' },
26
- { name: 'websearch', description: 'View/set web search+fetch limit per turn', usage: '/websearch [count]' },
27
- { name: 'mcp', description: 'Manage MCP server connections', usage: '/mcp <list|add|remove|reload>' },
28
- { name: 'exit', description: 'Exit curie-agent', usage: '/exit' },
29
- { name: 'provider', description: 'Switch AI provider', usage: '/provider <anthropic|openai|google|local|ollama|openrouter>' },
30
- { name: 'heartbeat', description: 'Manage heartbeat cycle', usage: '/heartbeat <status|enable|disable|intraday|daily|weekly|monthly|now>' },
31
- { name: 'init', description: 'Run the setup wizard', usage: '/init' },
32
- { name: 'snapshots', description: 'List recent git snapshots for recovery', usage: '/snapshots' },
33
- { name: 'revert', description: 'Revert to a git snapshot (index, default: most recent)', usage: '/revert [index]' },
43
+ // General
44
+ { name: 'status', description: 'Show version, model, and account info', usage: '/status', category: 'General' },
45
+ { name: 'help', description: 'Show all available commands', usage: '/help', category: 'General' },
46
+ { name: 'init', description: 'Run the setup wizard', usage: '/init', category: 'General' },
47
+ { name: 'exit', description: 'Exit curie-agent', usage: '/exit', category: 'General' },
48
+ // Model & Provider
49
+ { name: 'provider', description: 'Switch AI provider', usage: '/provider <anthropic|openai|google|local|ollama|openrouter>', category: 'Model & Provider' },
50
+ { name: 'model', description: 'Switch AI model, set pricing or context window', usage: '/model <model|pricing in;out|window tokens>', category: 'Model & Provider' },
51
+ { name: 'effort', description: 'Set reasoning effort level', usage: '/effort <low|medium|high|max|auto>', category: 'Model & Provider' },
52
+ { name: 'mode', description: 'Set approval mode', usage: '/mode <plan|edit|auto|yolo>', category: 'Model & Provider' },
53
+ // Display
54
+ { name: 'theme', description: 'Change color theme', usage: '/theme <name>', category: 'Display' },
55
+ { name: 'debug', description: 'Toggle debug logging', usage: '/debug [on|off]', category: 'Display' },
56
+ { name: 'statusline', description: 'Toggle status line display', usage: '/statusline [on|off]', category: 'Display' },
57
+ // Knowledge
58
+ { name: 'memory', description: 'View memory file sizes or capture a memory', usage: '/memory [status|add]', category: 'Knowledge' },
59
+ { name: 'todo', description: 'Manage tasks in todo.json', usage: '/todo <list|add|complete|remove>', category: 'Knowledge' },
60
+ { name: 'stats', description: 'Daily usage, sessions, streaks', usage: '/stats', category: 'Knowledge' },
61
+ { name: 'context', description: 'Visual grid showing context window usage, compaction, autocompaction', usage: '/context [auto|messages|compact [detailed|brief]]', category: 'Knowledge' },
62
+ // Automation
63
+ { name: 'remind', description: 'Create a reminder', usage: '/remind <message at time>', category: 'Automation' },
64
+ { name: 'cron', description: 'Manage reminders', usage: '/cron <list|delete|clear>', category: 'Automation' },
65
+ { name: 'task', description: 'Schedule an agent task', usage: '/task <create|list|delete>', category: 'Automation' },
66
+ { name: 'heartbeat', description: 'Manage heartbeat cycle', usage: '/heartbeat <status|enable|disable|intraday|daily|weekly|monthly|dreaming|now>', category: 'Automation' },
67
+ // Tools
68
+ { name: 'agent', description: 'Launch external AI agent', usage: '/agent <prompt>', category: 'Tools' },
69
+ { name: 'tools', description: 'View/set tool call limits per turn', usage: '/tools [tools_per_call [websearch_per_call]]', category: 'Tools' },
70
+ { name: 'websearch', description: 'View/set web search+fetch limit per turn', usage: '/websearch [count]', category: 'Tools' },
71
+ { name: 'mcp', description: 'Manage MCP server connections', usage: '/mcp <list|add|remove|reload>', category: 'Tools' },
72
+ { name: 'skill', description: 'List or show available skills', usage: '/skill [name]', category: 'Tools' },
73
+ // Communication
74
+ { name: 'channels', description: 'Manage Telegram channel config', usage: '/channels <list|set-bot-token|set-user-id|set-chat-id|disconnect>', category: 'Communication' },
75
+ // Safety
76
+ { name: 'snapshots', description: 'List recent git snapshots for recovery', usage: '/snapshots', category: 'Safety' },
77
+ { name: 'revert', description: 'Revert to a git snapshot (index, default: most recent)', usage: '/revert [index]', category: 'Safety' },
34
78
  ];
35
79
  export function parseSlashCommand(input) {
36
80
  const trimmed = input.trim();
@@ -59,6 +103,8 @@ export async function handleSlashCommand(cmd, args, ctx) {
59
103
  return handleTheme(args);
60
104
  case 'memory':
61
105
  return handleMemory(args, ctx);
106
+ case 'todo':
107
+ return handleTodo(args, ctx);
62
108
  case 'stats':
63
109
  return { type: 'switch_tab', tab: 'stats', message: 'Switched to Stats tab' };
64
110
  case 'context':
@@ -78,6 +124,8 @@ export async function handleSlashCommand(cmd, args, ctx) {
78
124
  return handleRemind(args, ctx);
79
125
  case 'cron':
80
126
  return handleCron(args, ctx);
127
+ case 'task':
128
+ return handleTask(args, ctx);
81
129
  case 'channels':
82
130
  return handleChannels(args, ctx);
83
131
  case 'mcp':
@@ -103,6 +151,8 @@ export async function handleSlashCommand(cmd, args, ctx) {
103
151
  return handleSnapshots(ctx);
104
152
  case 'revert':
105
153
  return handleRevert(args, ctx);
154
+ case 'skill':
155
+ return handleSkill(args, ctx);
106
156
  default:
107
157
  return {
108
158
  type: 'message',
@@ -110,9 +160,112 @@ export async function handleSlashCommand(cmd, args, ctx) {
110
160
  };
111
161
  }
112
162
  }
163
+ function handleSkill(args, ctx) {
164
+ const listSkillsFn = ctx.listSkills;
165
+ if (!listSkillsFn) {
166
+ return { type: 'message', message: 'Skills discovery is not available in this context.' };
167
+ }
168
+ const skills = listSkillsFn(ctx.cwd);
169
+ if (!args.trim()) {
170
+ if (skills.length === 0) {
171
+ return {
172
+ type: 'message',
173
+ message: [
174
+ 'No skills found.',
175
+ '',
176
+ 'Skills are SKILL.md files in:',
177
+ ' ~/.curie-agent/skills/ (global)',
178
+ ' <cwd>/.curie-agent/skills/ (project)',
179
+ '',
180
+ 'Directory format: skill-name/SKILL.md',
181
+ 'Flat format: skill-name-SKILL.md',
182
+ ].join('\n'),
183
+ };
184
+ }
185
+ const lines = [`Available Skills (${skills.length}):`];
186
+ for (const s of skills) {
187
+ const source = s.source === 'project' ? '[project]' : '[global]';
188
+ const desc = s.description.length > 80 ? s.description.slice(0, 77) + '...' : s.description;
189
+ lines.push(` ${s.name} ${source}`);
190
+ lines.push(` ${desc}`);
191
+ }
192
+ lines.push('');
193
+ lines.push('Use /skill <name> to see full details.');
194
+ return { type: 'message', message: lines.join('\n') };
195
+ }
196
+ const name = args.trim().toLowerCase();
197
+ const skill = skills.find(s => s.name.toLowerCase() === name);
198
+ if (!skill) {
199
+ const available = skills.map(s => s.name).join(', ');
200
+ return { type: 'message', message: `Skill "${name}" not found. Available: ${available}` };
201
+ }
202
+ try {
203
+ const content = readFileSync(skill.filePath, 'utf-8');
204
+ const bodyStart = content.indexOf('---', 3);
205
+ const body = bodyStart > 0 ? content.slice(bodyStart + 3).trim() : content;
206
+ return {
207
+ type: 'message',
208
+ message: [
209
+ `## ${skill.name} (${skill.source})`,
210
+ `Description: ${skill.description}`,
211
+ `Source: ${skill.filePath}`,
212
+ '',
213
+ '---',
214
+ body,
215
+ ].join('\n'),
216
+ };
217
+ }
218
+ catch {
219
+ return { type: 'message', message: `Could not read skill file: ${skill.filePath}` };
220
+ }
221
+ }
222
+ function validatePricingString(cost) {
223
+ if (!cost)
224
+ return false;
225
+ if (!cost.includes('|')) {
226
+ const [inStr = '', outStr = ''] = cost.split(';');
227
+ const inC = parseFloat(inStr);
228
+ const outC = parseFloat(outStr);
229
+ return !isNaN(inC) && !isNaN(outC) && inC >= 0 && outC >= 0;
230
+ }
231
+ const tiers = cost.split('|').map(s => s.trim());
232
+ const firstPair = (tiers[0] ?? '').split(';');
233
+ const inStr = firstPair[0] ?? '';
234
+ const outStr = firstPair[1] ?? '';
235
+ if (isNaN(parseFloat(inStr)) || isNaN(parseFloat(outStr)))
236
+ return false;
237
+ for (let i = 1; i < tiers.length; i++) {
238
+ const tier = tiers[i];
239
+ const idx = tier.indexOf('<');
240
+ if (idx === -1)
241
+ return false;
242
+ const threshold = parseInt(tier.substring(0, idx).trim(), 10);
243
+ const rest = tier.substring(idx + 1).trim();
244
+ const [inStr2 = '', outStr2 = ''] = rest.split(';');
245
+ const tierIn = parseFloat(inStr2);
246
+ const tierOut = parseFloat(outStr2);
247
+ if (isNaN(threshold) || isNaN(tierIn) || isNaN(tierOut) || threshold < 0 || tierIn < 0 || tierOut < 0)
248
+ return false;
249
+ }
250
+ return true;
251
+ }
252
+ function formatPricingDisplay(cost) {
253
+ if (!cost)
254
+ return '';
255
+ if (!cost.includes('|')) {
256
+ const [inStr = '', outStr = ''] = cost.split(';');
257
+ return `Pricing: $${inStr} in / $${outStr} out per 1M`;
258
+ }
259
+ const tiers = cost.split('|').map(s => s.trim());
260
+ const firstPair = (tiers[0] ?? '').split(';');
261
+ const inStr = firstPair[0] ?? '?';
262
+ const outStr = firstPair[1] ?? '?';
263
+ return `Pricing: $${inStr} in / $${outStr} out per 1M (${tiers.length} tier${tiers.length > 1 ? 's' : ''})`;
264
+ }
113
265
  function handleStatus(ctx) {
114
- const toolsPerCall = ctx.settings.TOOLS_PER_CALL ?? 10;
115
- const websearchPerCall = ctx.settings.WEBSEARCH_PER_CALL ?? 5;
266
+ const toolsPerCall = ctx.settings.tools_per_call ?? 10;
267
+ const websearchPerCall = ctx.settings.websearch_per_call ?? 5;
268
+ const modelCost = ctx.settings.providers?.[ctx.settings.current_provider]?.model_cost;
116
269
  const lines = [
117
270
  `curie-agent v${ctx.version}`,
118
271
  `Model: ${ctx.model}`,
@@ -124,16 +277,34 @@ function handleStatus(ctx) {
124
277
  : null,
125
278
  `Tools per turn: ${toolsPerCall}`,
126
279
  `WebSearch per turn: ${websearchPerCall}`,
127
- ctx.settings.MODEL_COST
128
- ? `Pricing: $${ctx.settings.MODEL_COST.split(';')[0]} in / $${ctx.settings.MODEL_COST.split(';')[1]} out per 1M`
280
+ modelCost
281
+ ? formatPricingDisplay(modelCost)
129
282
  : null,
130
283
  ].filter(Boolean);
131
284
  return { type: 'message', message: lines.join('\n') };
132
285
  }
133
286
  function handleHelp() {
134
- const lines = ['Slash commands:'];
287
+ const lines = [];
288
+ // Group by category
289
+ const groups = {};
135
290
  for (const cmd of SLASH_COMMANDS) {
136
- lines.push(` ${cmd.usage.padEnd(24)} ${cmd.description}`);
291
+ if (!groups[cmd.category])
292
+ groups[cmd.category] = [];
293
+ groups[cmd.category].push(cmd);
294
+ }
295
+ // Calculate column width per group
296
+ const colWidth = {};
297
+ for (const [cat, cmds] of Object.entries(groups)) {
298
+ colWidth[cat] = Math.min(Math.max(...cmds.map((c) => c.usage.length)) + 4, 40);
299
+ }
300
+ for (const [cat, cmds] of Object.entries(groups)) {
301
+ if (lines.length > 0)
302
+ lines.push('');
303
+ lines.push(cat);
304
+ const w = colWidth[cat];
305
+ for (const cmd of cmds) {
306
+ lines.push(` ${cmd.usage.padEnd(w)} ${cmd.description}`);
307
+ }
137
308
  }
138
309
  return { type: 'message', message: lines.join('\n') };
139
310
  }
@@ -171,14 +342,15 @@ function handleTheme(args) {
171
342
  return { type: 'update_theme', theme, message: `Theme changed to: ${theme}` };
172
343
  }
173
344
  function handleModel(args, settings) {
174
- const MODEL_COST_DEFAULT = '(not set)';
175
345
  const WINDOW_DEFAULT = 200_000;
346
+ const getModelCost = (s) => s?.providers?.[s.current_provider]?.model_cost;
347
+ const getWindow = (s) => s?.providers?.[s.current_provider]?.model_context_window;
176
348
  if (!args) {
177
- const cost = settings?.MODEL_COST ?? MODEL_COST_DEFAULT;
178
- const window = settings?.MODEL_CONTEXT_WINDOW ?? WINDOW_DEFAULT;
349
+ const cost = getModelCost(settings) ?? '(not set)';
350
+ const window = getWindow(settings) ?? WINDOW_DEFAULT;
179
351
  return {
180
352
  type: 'message',
181
- message: `Usage: /model <model>\n /model pricing [in;out] — set custom per-million pricing (e.g. "0.5;2.0")\n /model windows <tokens> — set max context window\n\nCurrent: pricing=${cost} window=${window} tokens`,
353
+ message: `Usage: /model <model>\n /model pricing [in;out] — set custom per-million pricing (e.g. "0.5;2.0" or "0.5;2.0|200000<1.0;4.0")\n /model window <tokens> — set max context window\n\nCurrent: pricing=${cost} window=${window} tokens`,
182
354
  };
183
355
  }
184
356
  const parts = args.trim().split(/\s+/);
@@ -187,21 +359,30 @@ function handleModel(args, settings) {
187
359
  switch (sub) {
188
360
  case 'pricing': {
189
361
  if (!rest) {
190
- const cost = settings?.MODEL_COST ?? MODEL_COST_DEFAULT;
191
- return { type: 'message', message: `Usage: /model pricing <in;out>\nExample: /model pricing 0.5;2.0\nCurrent: ${cost}` };
362
+ const cost = getModelCost(settings) ?? '(not set)';
363
+ return { type: 'message', message: `Usage: /model pricing <in;out>\n /model pricing <in;out|threshold<input;out>...\nExample: /model pricing 0.5;2.0\n /model pricing 0.5;2.0|200000<1.0;4.0\nCurrent: ${cost}` };
192
364
  }
193
- const [inStr, outStr] = rest.split(';');
194
- const inCost = parseFloat(inStr ?? '');
195
- const outCost = parseFloat(outStr ?? '');
196
- if (isNaN(inCost) || isNaN(outCost) || inCost < 0 || outCost < 0) {
197
- return { type: 'message', message: `Invalid pricing: "${rest}". Use format: <in;out> (per million tokens, e.g. "0.5;2.0").` };
365
+ const isValidPricing = validatePricingString(rest);
366
+ if (!isValidPricing) {
367
+ return { type: 'message', message: `Invalid pricing: "${rest}". Use format: <in;out> or <in;out|threshold<input;out> (per million tokens, e.g. "0.5;2.0" or "0.5;2.0|200000<1.0;4.0").` };
198
368
  }
199
- return { type: 'update_model_cost', modelCost: rest, message: `Model pricing set to: $${inCost} in / $${outCost} out per 1M tokens` };
369
+ const [inStr = ''] = rest.split(';');
370
+ const inCost = parseFloat(inStr);
371
+ const firstTier = rest.split('|')[0]?.split(';');
372
+ const outCost = firstTier ? parseFloat(firstTier[1] ?? '') : NaN;
373
+ const tierCount = rest.includes('|') ? rest.split('|').length : 1;
374
+ return {
375
+ type: 'update_model_cost',
376
+ modelCost: rest,
377
+ message: tierCount === 1
378
+ ? `Model pricing set to: $${inCost} in / $${outCost} out per 1M tokens`
379
+ : `Model pricing set to: ${tierCount} tiers, base $${inCost} in / $${outCost} out per 1M tokens`,
380
+ };
200
381
  }
201
- case 'windows': {
382
+ case 'window': {
202
383
  if (!rest) {
203
- const window = settings?.MODEL_CONTEXT_WINDOW ?? WINDOW_DEFAULT;
204
- return { type: 'message', message: `Usage: /model windows <tokens>\nExample: /model windows 1000000\nCurrent: ${window}` };
384
+ const window = getWindow(settings) ?? WINDOW_DEFAULT;
385
+ return { type: 'message', message: `Usage: /model window <tokens>\nExample: /model window 1000000\nCurrent: ${window}` };
205
386
  }
206
387
  const windowSize = parseInt(rest, 10);
207
388
  if (isNaN(windowSize) || windowSize < 1024) {
@@ -313,88 +494,63 @@ function handleRemind(args, ctx) {
313
494
  if (!args) {
314
495
  return {
315
496
  type: 'message',
316
- message: 'Usage: /remind <message at time>\nExamples: /remind "tomorrow at 7am make breakfast", /remind "in 30 minutes call mom"',
497
+ message: 'Usage: /remind <message at time>\nExample: /remind "tomorrow at 7am make breakfast"\nOr use /todo notify add "..." for the unified format.',
317
498
  };
318
499
  }
319
- const cronManager = ctx.cronManager;
320
- if (!cronManager) {
321
- return {
322
- type: 'message',
323
- message: 'Reminder service not available. Please restart the application.',
324
- };
500
+ const { parseReminderTime } = {};
501
+ try {
502
+ Object.assign(require('../../core/src/reminder-parser.js'), { parseReminderTime });
503
+ }
504
+ catch { /* module not available */ }
505
+ if (!parseReminderTime) {
506
+ return { type: 'message', message: `Could not parse time from: "${args}".\nUse /todo notify add "tomorrow at 7am make breakfast"` };
325
507
  }
326
- const { parseReminderTime } = require('../../core/src/reminder-parser.js');
327
508
  const parsed = parseReminderTime(args);
328
509
  if (!parsed) {
329
- return {
330
- type: 'message',
331
- message: `Could not parse time from: "${args}".\nTry: /remind "tomorrow at 7am make breakfast"`,
332
- };
510
+ return { type: 'message', message: `Could not parse time from: "${args}".\nUse /todo notify add "tomorrow at 7am make breakfast"` };
333
511
  }
334
- const task = cronManager.createReminder(parsed.message, parsed.scheduledAt);
335
- const timeStr = new Date(task.scheduledAt).toLocaleString();
336
- return {
337
- type: 'message',
338
- message: `Curie reminder:\nDate: ${timeStr}\n${parsed.message}\nID: ${task.id}`,
339
- };
512
+ if (ctx.taskManager) {
513
+ ctx.taskManager.load();
514
+ const task = ctx.taskManager.create({ title: parsed.message, mode: 'notify', scope: 'personal', scheduled_at: parsed.scheduledAt });
515
+ return { type: 'message', message: `Reminder set:\nTime: ${new Date(task.scheduled_at).toLocaleString()}\nMessage: ${task.title}\nID: ${task.id}` };
516
+ }
517
+ return { type: 'message', message: 'Reminder service not available. Please restart the application.' };
340
518
  }
341
519
  function handleCron(args, ctx) {
342
- const cronManager = ctx.cronManager;
343
- if (!cronManager) {
344
- return {
345
- type: 'message',
346
- message: 'Reminder service not available. Please restart the application.',
347
- };
348
- }
349
- // Reload from disk before each command so tool-created reminders
350
- // are visible even if the tool used a separate CronManager instance.
351
- cronManager.load();
520
+ // /cron is an alias for viewing notify-mode tasks in the unified store.
352
521
  const parts = args.trim().split(/\s+/);
353
522
  const action = parts[0]?.toLowerCase();
354
523
  const rest = parts.slice(1).join(' ').trim();
524
+ if (!ctx.taskManager) {
525
+ return { type: 'message', message: 'Task service not available. Please restart the application.' };
526
+ }
527
+ ctx.taskManager.load();
355
528
  switch (action) {
356
529
  case 'list': {
357
- const statusFilter = ['pending', 'fired', 'cancelled'].includes(rest) ? rest : undefined;
358
- const tasks = cronManager.listReminders(statusFilter);
359
- if (tasks.length === 0) {
360
- return {
361
- type: 'message',
362
- message: statusFilter
363
- ? `No ${statusFilter} reminders.`
364
- : 'No reminders yet.\nUse /remind to create one.',
365
- };
366
- }
367
- const lines = [`Reminders (${tasks.length}${statusFilter ? ` — ${statusFilter}` : ''}):`];
368
- for (const t of tasks) {
369
- const timeStr = new Date(t.scheduledAt).toLocaleString();
370
- const statusEmoji = t.status === 'pending' ? '⏳' : t.status === 'fired' ? '✅' : '❌';
371
- const label = t.schedule ? `[${scheduleLabel(t.schedule.type)}] ` : '';
372
- lines.push(` ${statusEmoji} ${label}${t.message}\n Date: ${timeStr}\n ID: ${t.id}`);
530
+ const allTasks = ctx.taskManager.list({ mode: 'notify' });
531
+ if (allTasks.length === 0)
532
+ return { type: 'message', message: 'No scheduled reminders.\nUse /todo notify add "..." to create one.' };
533
+ const lines = [`Reminders (${allTasks.length}):`];
534
+ for (const t of allTasks.sort((a, b) => Number(a.scheduled_at ?? 0) - Number(b.scheduled_at ?? 0))) {
535
+ const timeStr = t.scheduled_at ? new Date(t.scheduled_at).toLocaleString() : '—';
536
+ lines.push(` ${t.status} ${t.title}\n Time: ${timeStr}\n ID: ${t.id.slice(0, 8)}`);
373
537
  }
374
538
  return { type: 'message', message: lines.join('\n') };
375
539
  }
376
540
  case 'delete': {
377
- if (!rest) {
378
- return {
379
- type: 'message',
380
- message: 'Usage: /cron delete <id>\nExample: /cron delete abc-123',
381
- };
382
- }
383
- const result = cronManager.cancelReminder(rest);
384
- if (!result) {
385
- return { type: 'message', message: `No reminder found with ID: ${rest}` };
386
- }
387
- return { type: 'message', message: `Reminder cancelled.` };
541
+ if (!rest)
542
+ return { type: 'message', message: 'Usage: /cron delete <id>\nExample: /cron delete abc-123' };
543
+ const result = ctx.taskManager.cancelTask(rest);
544
+ if (result)
545
+ return { type: 'message', message: 'Reminder cancelled.' };
546
+ return { type: 'message', message: `No reminder found with ID: ${rest}` };
388
547
  }
389
548
  case 'clear': {
390
- const removed = cronManager.clearCompleted();
391
- return { type: 'message', message: `Cleared ${removed} completed reminder(s).` };
549
+ const removed = ctx.taskManager.clearCompleted();
550
+ return { type: 'message', message: `Cleared ${removed} completed task(s).` };
392
551
  }
393
552
  default:
394
- return {
395
- type: 'message',
396
- message: `Unknown cron action: "${action}". Use: list, delete, clear`,
397
- };
553
+ return handleTodo('list personal', ctx);
398
554
  }
399
555
  }
400
556
  function handleChannels(args, ctx) {
@@ -403,9 +559,9 @@ function handleChannels(args, ctx) {
403
559
  const rest = parts.slice(1).join(' ').trim();
404
560
  switch (sub) {
405
561
  case 'list': {
406
- const token = ctx.settings.TELEGRAM_BOT_TOKEN;
407
- const userId = ctx.settings.TELEGRAM_USER_ID;
408
- const chatId = ctx.settings.TELEGRAM_CHAT_ID;
562
+ const token = ctx.settings.channels?.bot_token;
563
+ const userId = ctx.settings.channels?.user_id;
564
+ const chatId = ctx.settings.channels?.chat_id;
409
565
  const tokenMask = token && token.length > 8
410
566
  ? token.slice(0, 8) + '...'
411
567
  : token || '(not set)';
@@ -473,7 +629,7 @@ function handleMcp(args, ctx) {
473
629
  // Parse current MCP servers from settings
474
630
  let configs = {};
475
631
  try {
476
- const raw = ctx.settings.MCP_SERVERS;
632
+ const raw = ctx.settings.mcp_servers;
477
633
  if (typeof raw === 'string' && raw.trim().length > 0) {
478
634
  configs = JSON.parse(raw);
479
635
  }
@@ -506,14 +662,16 @@ function handleMcp(args, ctx) {
506
662
  detail = `url: ${c.url || '?'}`;
507
663
  }
508
664
  const client = ctx.mcpClients?.find((cl) => cl.serverId === id);
665
+ const wasFailed = ctx.mcpFailed?.includes(id);
509
666
  if (!client) {
510
- lines.push(` · ${id} (${name}) ${transport}: ${detail} [not loaded]`);
667
+ const status = wasFailed ? 'connection failed' : 'not running';
668
+ lines.push(` ⚠️ ${id} (${name}) — ${transport}: ${detail} [${status}]`);
511
669
  }
512
670
  else if (client.isConnected) {
513
- lines.push(` ${id} (${name}) — ${transport}: ${detail} [connected]`);
671
+ lines.push(` ${id} (${name}) — ${transport}: ${detail}`);
514
672
  }
515
673
  else {
516
- lines.push(` ${id} (${name}) — ${transport}: ${detail} [disconnected]`);
674
+ lines.push(` ⚠️ ${id} (${name}) — ${transport}: ${detail} [disconnected]`);
517
675
  }
518
676
  }
519
677
  return { type: 'message', message: lines.join('\n') };
@@ -573,7 +731,7 @@ function handleMcp(args, ctx) {
573
731
  if (Object.keys(env).length > 0)
574
732
  cfg.env = env;
575
733
  configs[id] = cfg;
576
- ctx.settings.MCP_SERVERS = configs;
734
+ ctx.settings.mcp_servers = configs;
577
735
  return { type: 'update_mcp', mcpServerId: id, mcpServers: JSON.stringify(configs, null, 2), message: `Added MCP server "${id}":\n\n${JSON.stringify(configs, null, 2)}\n\nRun /mcp reload to connect.` };
578
736
  }
579
737
  case 'remove': {
@@ -587,7 +745,7 @@ function handleMcp(args, ctx) {
587
745
  return { type: 'message', message: `No MCP server found with ID: ${rest}` };
588
746
  }
589
747
  delete configs[rest];
590
- ctx.settings.MCP_SERVERS = configs;
748
+ ctx.settings.mcp_servers = configs;
591
749
  return { type: 'update_mcp', mcpServerId: rest, message: `Removed MCP server "${rest}". Run /mcp reload to apply.` };
592
750
  }
593
751
  case 'reload': {
@@ -601,8 +759,8 @@ function handleMcp(args, ctx) {
601
759
  }
602
760
  }
603
761
  function handleTools(args, settings) {
604
- const toolsPerCall = settings.TOOLS_PER_CALL ?? 10;
605
- const websearchPerCall = settings.WEBSEARCH_PER_CALL ?? 5;
762
+ const toolsPerCall = settings.tools_per_call ?? 10;
763
+ const websearchPerCall = settings.websearch_per_call ?? 5;
606
764
  if (!args.trim()) {
607
765
  return {
608
766
  type: 'message',
@@ -629,7 +787,7 @@ function handleTools(args, settings) {
629
787
  return result;
630
788
  }
631
789
  function handleWebsearch(args, settings) {
632
- const websearchPerCall = settings.WEBSEARCH_PER_CALL ?? 5;
790
+ const websearchPerCall = settings.websearch_per_call ?? 5;
633
791
  if (!args.trim()) {
634
792
  return {
635
793
  type: 'message',
@@ -679,6 +837,298 @@ function handleMemory(args, ctx) {
679
837
  }
680
838
  }
681
839
  }
840
+ /**
841
+ * Parse /todo args: "/todo [scope:]add|list|complete|cancel|start|remove [title|id]"
842
+ * Optionally with mode keywords: "auto/add", "notify/add"
843
+ */
844
+ function parseTodoArgs(args) {
845
+ const parts = args.trim().split(/\s+/);
846
+ if (!parts.length)
847
+ return { scope: 'project', action: '', titleOrId: '' };
848
+ let scope = 'project';
849
+ let action = '';
850
+ let idx = 0;
851
+ // First token: scope, mode keyword, or action
852
+ const first = parts[0].toLowerCase();
853
+ if (first === 'personal' || first === 'project') {
854
+ scope = first;
855
+ idx = 1;
856
+ }
857
+ else if (first === 'auto' || first === 'notify') {
858
+ // mode keyword: /todo auto add ..., /todo notify list ...
859
+ idx = 1;
860
+ }
861
+ // Second token: action or scope fallback
862
+ const second = parts[idx]?.toLowerCase() ?? '';
863
+ const actions = ['list', 'add', 'complete', 'cancel', 'start', 'remove'];
864
+ if (actions.includes(second)) {
865
+ action = second;
866
+ idx++;
867
+ }
868
+ else if (first === 'personal' || first === 'project') {
869
+ // no scope given — second token is the action, third is the content
870
+ if (actions.includes(second))
871
+ action = second;
872
+ idx += actions.includes(second) ? 1 : 0;
873
+ }
874
+ const titleOrId = parts.slice(idx).join(' ').trim();
875
+ return { scope, action, titleOrId };
876
+ }
877
+ /** Helper to read a tasks file (unified format), falls back to legacy todo.json. */
878
+ function readTasksAtPath(path) {
879
+ if (!existsSync(path))
880
+ return null;
881
+ try {
882
+ const raw = readFileSync(path, 'utf-8');
883
+ const parsed = JSON.parse(raw);
884
+ if (Array.isArray(parsed.tasks))
885
+ return { tasks: parsed.tasks };
886
+ return null;
887
+ }
888
+ catch {
889
+ return null;
890
+ }
891
+ }
892
+ /** Write a task record to file (unified format). */
893
+ function writeTaskToFile(filePath, taskRecord) {
894
+ let data = readTasksAtPath(filePath);
895
+ if (!data) {
896
+ data = { tasks: [] };
897
+ }
898
+ // Update/insert the task
899
+ const idx = data.tasks.findIndex((t) => t.id === taskRecord.id);
900
+ if (idx >= 0) {
901
+ data.tasks[idx] = taskRecord;
902
+ }
903
+ else {
904
+ data.tasks.push(taskRecord);
905
+ }
906
+ writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
907
+ }
908
+ /** Remove a task from file by ID. */
909
+ function removeTaskFromFile(filePath, id) {
910
+ if (!existsSync(filePath))
911
+ return false;
912
+ try {
913
+ const raw = readFileSync(filePath, 'utf-8');
914
+ const parsed = JSON.parse(raw);
915
+ const tasks = parsed.tasks;
916
+ if (!Array.isArray(tasks))
917
+ return false;
918
+ for (const t of tasks)
919
+ normalizeTaskRecord(t);
920
+ const idx = tasks.findIndex((t) => t.id === id);
921
+ if (idx === -1)
922
+ return false;
923
+ tasks.splice(idx, 1);
924
+ tasks.forEach((t, i) => { t.order = i; });
925
+ parsed.tasks = tasks;
926
+ writeFileSync(filePath, JSON.stringify(parsed, null, 2), 'utf-8');
927
+ return true;
928
+ }
929
+ catch {
930
+ return false;
931
+ }
932
+ }
933
+ /** Normalize and return the active+done task counts from a TasksData. */
934
+ function getTaskCounts(tasks) {
935
+ const active = tasks.filter((t) => !['done', 'canceled'].includes(String(t.status ?? '')));
936
+ const doneCount = tasks.filter((t) => t.status === 'done').length;
937
+ return { active: active.length, done: doneCount };
938
+ }
939
+ function handleTodo(args, ctx) {
940
+ const parsed = parseTodoArgs(args);
941
+ const { scope, action, titleOrId } = parsed;
942
+ const path = resolveTaskPath(scope, ctx.cwd);
943
+ // Support both unified tasks.json and legacy todo.json
944
+ const fullPath = existsSync(path) ? path : (scope === 'personal'
945
+ ? join(homedir(), '.curie-agent', 'todo.json')
946
+ : join(ctx.cwd, 'todo.json'));
947
+ switch (action) {
948
+ case 'list': {
949
+ const data = readTasksAtPath(fullPath);
950
+ if (!data || !data.tasks.length) {
951
+ return { type: 'message', message: `No tasks in ${scope}.` };
952
+ }
953
+ // Normalize and filter
954
+ const normalizedTasks = data.tasks.map((t) => normalizeTaskRecord(t));
955
+ let tasks = normalizedTasks;
956
+ const active = tasks.filter((t) => !['done', 'canceled'].includes(String(t.status ?? '')));
957
+ const done = tasks.filter((t) => t.status === 'done');
958
+ // Auto-detect mode from title (for user convenience)
959
+ const lower = args.toLowerCase();
960
+ const hasModeKeyword = /auto/.test(lower) || /notify/.test(lower);
961
+ const lines = [`Tasks (${scope}) — ${active.length} active, ${done.length} done:`];
962
+ for (const t of active.sort((a, b) => Number(a.order ?? 0) - Number(b.order ?? 0))) {
963
+ const icon = String(t.status) === 'in_progress' ? '[*]' : '-';
964
+ const prio = (t.priority !== 'medium' && t.priority) ? ` [${t.priority}]` : '';
965
+ const modeCol = t.mode ? `[${String(t.mode).toUpperCase()}]` : '[MANUAL]';
966
+ const scheduledAt = typeof t.scheduled_at === 'number' ? t.scheduled_at : undefined;
967
+ const timeStr = scheduledAt ? ` (at ${new Date(scheduledAt).toLocaleString()})` : '';
968
+ lines.push(` ${icon} ${String(t.id).slice(0, 8)} ${modeCol}${prio} ${t.title}${timeStr}`);
969
+ }
970
+ return { type: 'message', message: lines.join('\n') };
971
+ }
972
+ case 'add': {
973
+ if (!titleOrId) {
974
+ return {
975
+ type: 'message',
976
+ message: `Usage: /todo add <title>\nExample: /todo add "Finish the report"\n /todo auto add "build at 3pm" — agent executes it\n /todo notify add "remind about X" — notification only`,
977
+ };
978
+ }
979
+ // Detect mode from keyword or natural language time parsing
980
+ let mode = 'manual';
981
+ const lower = titleOrId.toLowerCase();
982
+ if (lower.startsWith('at ') || /\bat\b/.test(lower)) {
983
+ // Has time reference — try to parse with TaskManager
984
+ const taskMgr = ctx.taskManager;
985
+ let instruction;
986
+ // Check if "auto" was explicitly requested
987
+ if (/^auto\s+add\s*/i.test(args) || /^auto\b/.test(args)) {
988
+ mode = 'auto';
989
+ }
990
+ else if (/^notify\s+add\s*/i.test(args) || /notify\s+/i.test(args)) {
991
+ mode = 'notify';
992
+ }
993
+ const timeKeywordPattern = /^(auto|notify)\s*add\s*/i;
994
+ instruction = titleOrId.replace(timeKeywordPattern, '');
995
+ if (!instruction) {
996
+ return { type: 'message', message: 'Usage: /todo add <title>\n /todo auto add "build at 3pm"\n /todo notify add "remind about X"' };
997
+ }
998
+ // Try natural language time parsing
999
+ let { parseReminderTime } = {};
1000
+ try {
1001
+ parseReminderTime = require('../../core/src/reminder-parser.js').parseReminderTime;
1002
+ }
1003
+ catch { /* module not available */ }
1004
+ if (mode === 'manual' && parseReminderTime) {
1005
+ const parsed = parseReminderTime(instruction);
1006
+ if (parsed) {
1007
+ instruction = parsed.message;
1008
+ if (taskMgr) {
1009
+ taskMgr.load();
1010
+ taskMgr.create({ title: instruction, mode: 'auto', scope: 'personal', scheduled_at: parsed.scheduledAt });
1011
+ const timeStr = new Date(parsed.scheduledAt).toLocaleString();
1012
+ return { type: 'message', message: `Task scheduled:\nTime: ${timeStr}\nInstruction: ${instruction}` };
1013
+ }
1014
+ }
1015
+ }
1016
+ if (mode === 'notify' && parseReminderTime) {
1017
+ const parsed = parseReminderTime(instruction);
1018
+ if (parsed && taskMgr) {
1019
+ taskMgr.load();
1020
+ const task = taskMgr.create({ title: parsed.message, mode: 'notify', scope: 'personal', scheduled_at: parsed.scheduledAt });
1021
+ const timeStr = new Date(task.scheduled_at).toLocaleString();
1022
+ return { type: 'message', message: `Reminder scheduled:\nTime: ${timeStr}\nMessage: ${parsed.message}` };
1023
+ }
1024
+ }
1025
+ if (mode === 'auto' && parseReminderTime) {
1026
+ const parsed = parseReminderTime(instruction);
1027
+ if (parsed && taskMgr) {
1028
+ taskMgr.load();
1029
+ const task = taskMgr.create({ title: parsed.message, mode: 'auto', scope: 'personal', scheduled_at: parsed.scheduledAt });
1030
+ const timeStr = new Date(task.scheduled_at).toLocaleString();
1031
+ return { type: 'message', message: `Scheduled task:\nTime: ${timeStr}\nInstruction: ${parsed.message}` };
1032
+ }
1033
+ }
1034
+ // No time could be parsed — fall through to manual mode
1035
+ }
1036
+ else if (/^auto\s+add\s*/i.test(args) || /^auto\b/.test(args)) {
1037
+ mode = 'auto';
1038
+ }
1039
+ else if (/^notify\s+add\s*/i.test(args) || /notify\s+/i.test(args)) {
1040
+ mode = 'notify';
1041
+ }
1042
+ const title = (mode === 'manual' ? titleOrId : titleOrId.replace(/^(auto|notify)\s+add\s*/i, '').trim()) || titleOrId;
1043
+ if (!title)
1044
+ return { type: 'message', message: `Usage: /todo add <title>\nExample: /todo add "Finish the report"` };
1045
+ let data = readTasksAtPath(fullPath);
1046
+ if (!data) {
1047
+ data = { $schema: 'tasks.schema.json', version: 1, tasks: [] };
1048
+ }
1049
+ data.tasks = data.tasks.map((t) => normalizeTaskRecord(t));
1050
+ const id = crypto.randomUUID();
1051
+ const task = {
1052
+ id,
1053
+ title,
1054
+ description: '',
1055
+ status: 'todo',
1056
+ priority: 'medium',
1057
+ tags: [],
1058
+ order: data.tasks.length,
1059
+ created_at: new Date().toISOString(),
1060
+ completed_at: null,
1061
+ };
1062
+ // Add mode and scope fields
1063
+ task.mode = mode;
1064
+ task.scope = scope;
1065
+ data.tasks.push(task);
1066
+ writeFileSync(fullPath, JSON.stringify(data, null, 2), 'utf-8');
1067
+ const modePrefix = mode === 'manual' ? '' : `[${mode}] `;
1068
+ return { type: 'message', message: `${modePrefix}Added task: "${title}" (ID: ${id.slice(0, 8)})` };
1069
+ }
1070
+ case 'complete': {
1071
+ if (!titleOrId)
1072
+ return { type: 'message', message: 'Usage: /todo complete <id>' };
1073
+ let data = readTasksAtPath(fullPath);
1074
+ if (!data)
1075
+ return { type: 'message', message: 'No tasks found.' };
1076
+ data.tasks = data.tasks.map((t) => normalizeTaskRecord(t));
1077
+ const idx = data.tasks.findIndex((t) => String(t.id) === titleOrId || String(t.id).startsWith(titleOrId));
1078
+ if (idx === -1)
1079
+ return { type: 'message', message: `Task not found: ${titleOrId}` };
1080
+ data.tasks[idx].status = 'done';
1081
+ data.tasks[idx].completed_at = new Date().toISOString();
1082
+ writeFileSync(fullPath, JSON.stringify(data, null, 2), 'utf-8');
1083
+ return { type: 'message', message: `Completed: "${data.tasks[idx].title}"` };
1084
+ }
1085
+ case 'cancel': {
1086
+ if (!titleOrId)
1087
+ return { type: 'message', message: 'Usage: /todo cancel <id>' };
1088
+ let data = readTasksAtPath(fullPath);
1089
+ if (!data)
1090
+ return { type: 'message', message: 'No tasks found.' };
1091
+ data.tasks = data.tasks.map((t) => normalizeTaskRecord(t));
1092
+ const idx = data.tasks.findIndex((t) => String(t.id) === titleOrId || String(t.id).startsWith(titleOrId));
1093
+ if (idx === -1)
1094
+ return { type: 'message', message: `Task not found: ${titleOrId}` };
1095
+ data.tasks[idx].status = 'canceled';
1096
+ writeFileSync(fullPath, JSON.stringify(data, null, 2), 'utf-8');
1097
+ return { type: 'message', message: `Canceled: "${data.tasks[idx].title}"` };
1098
+ }
1099
+ case 'start': {
1100
+ if (!titleOrId)
1101
+ return { type: 'message', message: 'Usage: /todo start <id>' };
1102
+ let data = readTasksAtPath(fullPath);
1103
+ if (!data)
1104
+ return { type: 'message', message: 'No tasks found.' };
1105
+ data.tasks = data.tasks.map((t) => normalizeTaskRecord(t));
1106
+ const idx = data.tasks.findIndex((t) => String(t.id) === titleOrId || String(t.id).startsWith(titleOrId));
1107
+ if (idx === -1)
1108
+ return { type: 'message', message: `Task not found: ${titleOrId}` };
1109
+ data.tasks[idx].status = 'in_progress';
1110
+ writeFileSync(fullPath, JSON.stringify(data, null, 2), 'utf-8');
1111
+ return { type: 'message', message: `Started: "${data.tasks[idx].title}"` };
1112
+ }
1113
+ case 'remove': {
1114
+ if (!titleOrId)
1115
+ return { type: 'message', message: 'Usage: /todo remove <id>' };
1116
+ if (removeTaskFromFile(fullPath, titleOrId)) {
1117
+ return { type: 'message', message: `Removed task: ${titleOrId.slice(0, 8)}` };
1118
+ }
1119
+ return { type: 'message', message: `Task not found: ${titleOrId}` };
1120
+ }
1121
+ default: {
1122
+ if (!action) {
1123
+ return {
1124
+ type: 'message',
1125
+ message: 'Task commands:\n /todo list [personal|project] — List tasks\n /todo add <title> — Add a manual task\n /todo auto add "X at Y" — Agent executes X at Y\n /todo notify add "remind about X" — Notification only\n /todo complete <id> — Mark done\n /todo cancel <id> — Cancel a task\n /todo start <id> — Start working on it\n /todo remove <id> — Delete permanently',
1126
+ };
1127
+ }
1128
+ return { type: 'message', message: `Unknown todo action: "${action}". Use: list, add, complete, cancel, start, remove` };
1129
+ }
1130
+ }
1131
+ }
682
1132
  function formatChannelMessages(messages) {
683
1133
  const lines = [`Conversation Messages (${messages.length}):`];
684
1134
  const maxOutput = 8000;
@@ -692,7 +1142,9 @@ function formatChannelMessages(messages) {
692
1142
  : msg.role === 'system' ? 'System'
693
1143
  : msg.role === 'decision' ? 'Decision'
694
1144
  : msg.role === 'heartbeat' ? 'Heartbeat'
695
- : msg.role;
1145
+ : msg.role === 'task' ? 'Task'
1146
+ : msg.role === 'debug' ? 'Debug'
1147
+ : msg.role;
696
1148
  const titlePrefix = msg.title ? `[${msg.title}] ` : '';
697
1149
  const truncated = msg.content.length > 300
698
1150
  ? msg.content.slice(0, 300) + '...'
@@ -811,14 +1263,76 @@ function handleContext(ctx, args) {
811
1263
  messages: loopMsgs,
812
1264
  depth,
813
1265
  },
814
- message: `Compacting conversation (${depth} summary)...\nThe agent will process the summary and continue with a condensed context.`,
1266
+ message: `Compacting conversation (${loopMsgs.length} messages)... This will use one AI turn to summarize your session. The agent will continue automatically.`,
815
1267
  };
816
1268
  }
1269
+ // /context auto — autocompaction settings and controls
1270
+ if (sub && (sub === 'auto' || sub.startsWith('auto '))) {
1271
+ const parts = sub.split(/\s+/);
1272
+ const action = parts[0]?.toLowerCase() ?? 'auto';
1273
+ const arg1 = parts[1]?.toLowerCase();
1274
+ const arg2 = parts[2];
1275
+ const s = ctx.settings;
1276
+ if (action === 'auto' && !arg1) {
1277
+ // Show current settings
1278
+ const lines = [
1279
+ 'Autocompaction Settings:',
1280
+ ` Enabled: ${s.auto_compact?.enabled ?? 'on'}`,
1281
+ ` Context fill threshold: ${s.auto_compact?.threshold ?? 75}%`,
1282
+ ` Warning threshold: ${s.auto_compact?.warn_threshold ?? 60}%`,
1283
+ ` Forced compaction threshold: ${s.auto_compact?.forced_threshold ?? 85}%`,
1284
+ ` Pricing tier warning: ${s.pricing_tier_warn ?? 'on'}`,
1285
+ '',
1286
+ 'Usage:',
1287
+ ' /context auto on/off — enable or disable autocompaction',
1288
+ ' /context auto threshold <N> — set compaction threshold (%)',
1289
+ ' /context auto warn <N> — set warning threshold (%)',
1290
+ ' /context auto pricing on/off — enable/disable pricing tier warnings',
1291
+ ];
1292
+ return { type: 'message', message: lines.join('\n') };
1293
+ }
1294
+ if (arg1 === 'on') {
1295
+ ctx.settingsMgr?.update({ auto_compact: { ...s.auto_compact, enabled: 'on' } });
1296
+ return { type: 'message', message: 'Autocompaction enabled.' };
1297
+ }
1298
+ if (arg1 === 'off') {
1299
+ ctx.settingsMgr?.update({ auto_compact: { ...s.auto_compact, enabled: 'off' } });
1300
+ return { type: 'message', message: 'Autocompaction disabled.' };
1301
+ }
1302
+ if (arg1 === 'threshold' && arg2) {
1303
+ const pct = parseInt(arg2, 10);
1304
+ if (isNaN(pct) || pct < 10 || pct > 99) {
1305
+ return { type: 'message', message: 'Invalid threshold. Use a value between 10 and 99.' };
1306
+ }
1307
+ ctx.settingsMgr?.update({ auto_compact: { ...s.auto_compact, threshold: pct } });
1308
+ return { type: 'message', message: `Compaction threshold set to ${pct}%.` };
1309
+ }
1310
+ if (arg1 === 'warn' && arg2) {
1311
+ const pct = parseInt(arg2, 10);
1312
+ if (isNaN(pct) || pct < 5 || pct > 95) {
1313
+ return { type: 'message', message: 'Invalid warning threshold. Use a value between 5 and 95.' };
1314
+ }
1315
+ ctx.settingsMgr?.update({ auto_compact: { ...s.auto_compact, warn_threshold: pct } });
1316
+ return { type: 'message', message: `Warning threshold set to ${pct}%.` };
1317
+ }
1318
+ if (arg1 === 'pricing') {
1319
+ if (arg2 === 'on') {
1320
+ ctx.settingsMgr?.update({ pricing_tier_warn: 'on' });
1321
+ return { type: 'message', message: 'Pricing tier warnings enabled.' };
1322
+ }
1323
+ if (arg2 === 'off') {
1324
+ ctx.settingsMgr?.update({ pricing_tier_warn: 'off' });
1325
+ return { type: 'message', message: 'Pricing tier warnings disabled.' };
1326
+ }
1327
+ return { type: 'message', message: 'Usage: /context auto pricing on/off' };
1328
+ }
1329
+ return { type: 'message', message: 'Usage: /context auto [on|off|threshold N|warn N|pricing on/off]\nRun "/context auto" without arguments to see current settings.' };
1330
+ }
817
1331
  // Default: context window visual (unchanged behavior)
818
- const input = ctx.inputTokens ?? 0;
819
- const output = ctx.outputTokens ?? 0;
1332
+ const input = ctx.contextWindowInputTokens ?? ctx.inputTokens ?? 0;
1333
+ const output = ctx.contextWindowOutputTokens ?? ctx.outputTokens ?? 0;
820
1334
  const model = ctx.model || 'unknown';
821
- const windowSize = ctx.settings.MODEL_CONTEXT_WINDOW ?? ctx.contextWindowSize ?? 200_000;
1335
+ const windowSize = ctx.settings.providers?.[ctx.settings.current_provider]?.model_context_window ?? ctx.contextWindowSize ?? 200_000;
822
1336
  const pct = input > 0 ? Math.min(100, Math.round((input / windowSize) * 100)) : 0;
823
1337
  const filled = Math.round((pct / 100) * 24);
824
1338
  const bar = '█'.repeat(filled) + '░'.repeat(24 - filled);
@@ -846,7 +1360,7 @@ function handleProvider(args) {
846
1360
  if (!args) {
847
1361
  return {
848
1362
  type: 'message',
849
- message: `Usage: /provider <name>\nProviders: ${validProviders.join(', ')}\n\nCurrent settings:\n anthropic: MODEL_API_KEY, MODEL_URL\n openai: OPENAI_API_KEY, OPENAI_URL\n google: GOOGLE_API_KEY, GOOGLE_URL\n local: MODEL_URL, MODEL_API_KEY\n ollama: MODEL_URL, MODEL_API_KEY\n openrouter: OPENROUTER_URL, OPENROUTER_API_KEY`,
1363
+ message: `Usage: /provider <name>\nProviders: ${validProviders.join(', ')}\n\nCurrent settings:\n anthropic: providers.anthropic.api_key, providers.anthropic.url\n openai: providers.openai.api_key, providers.openai.url\n google: providers.google.api_key, providers.google.url\n local: providers.local.url, providers.local.api_key\n ollama: providers.ollama.url, providers.ollama.api_key\n openrouter: providers.openrouter.api_key, providers.openrouter.url`,
850
1364
  };
851
1365
  }
852
1366
  const provider = args.toLowerCase();
@@ -865,17 +1379,18 @@ function handleHeartbeat(args, ctx) {
865
1379
  switch (sub) {
866
1380
  case 'status':
867
1381
  case '': {
868
- const active = ctx.settings.HEARTBEAT === 'on';
869
- const intraday = ctx.settings.HEARTBEAT_INTRADAY ?? '';
870
- const daily = ctx.settings.HEARTBEAT_DAILY ?? '6:00';
871
- const weekly = ctx.settings.HEARTBEAT_WEEKLY ?? 'monday@6:00';
872
- const monthly = ctx.settings.HEARTBEAT_MONTHLY ?? '1@6:00';
1382
+ const active = ctx.settings.heartbeat?.schedule === 'on';
1383
+ const intraday = ctx.settings.heartbeat?.intraday ?? '';
1384
+ const daily = ctx.settings.heartbeat?.daily ?? '6:00';
1385
+ const weekly = ctx.settings.heartbeat?.weekly ?? 'monday@6:00';
1386
+ const monthly = ctx.settings.heartbeat?.monthly ?? '1@6:00';
1387
+ const dreaming = ctx.settings.heartbeat?.dreaming ?? '2:00';
873
1388
  const intradayDisplay = intraday ? intraday.split(',').map((s) => s.trim()).join(', ') : '(not set)';
874
- const picked = pickNextSchedule({ HEARTBEAT_INTRADAY: intraday, HEARTBEAT_DAILY: daily, HEARTBEAT_WEEKLY: weekly, HEARTBEAT_MONTHLY: monthly });
1389
+ const picked = pickNextSchedule({ HEARTBEAT_INTRADAY: intraday, HEARTBEAT_DAILY: daily, HEARTBEAT_WEEKLY: weekly, HEARTBEAT_MONTHLY: monthly, HEARTBEAT_DREAMING: dreaming });
875
1390
  const activeSchedule = picked ? `${picked.type} (${picked.value})` : '(none configured)';
876
1391
  return {
877
1392
  type: 'message',
878
- message: `Heartbeat cycle:\n Enabled: ${active ? 'yes' : 'no'}\n Active schedule: ${activeSchedule}\n\n Intraday: ${intradayDisplay}\n Daily: ${daily}\n Weekly: ${weekly}\n Monthly: ${monthly}\n\nUsage:\n /heartbeat — show status\n /heartbeat enable — turn on\n /heartbeat disable — turn off\n /heartbeat intraday <H:MM,...> — set intra-day times (e.g. 8:10,10:10,14:20)\n /heartbeat daily <H:MM> — set daily time (24h)\n /heartbeat weekly <day@H:MM>\n /heartbeat monthly <D@H:MM>\n /heartbeat now — run immediately`,
1393
+ message: `Heartbeat cycle:\n Enabled : ${active ? 'yes' : 'no'}\n Active schedule: ${activeSchedule}\n\n Intraday: ${intradayDisplay}\n Daily : ${daily}\n Weekly : ${weekly}\n Monthly : ${monthly}\n Dreaming: ${dreaming}\n\nUsage:\n /heartbeat — show status\n /heartbeat enable — turn on\n /heartbeat disable — turn off\n /heartbeat intraday <H:MM,...> — set intra-day times (e.g. 8:10,10:10,14:20)\n /heartbeat daily <H:MM> — set daily time (24h)\n /heartbeat weekly <day@H:MM>\n /heartbeat monthly <D@H:MM>\n /heartbeat dreaming <H:MM> — set dreaming time (24h)\n /heartbeat now — run immediately`,
879
1394
  };
880
1395
  }
881
1396
  case 'enable': {
@@ -886,7 +1401,7 @@ function handleHeartbeat(args, ctx) {
886
1401
  }
887
1402
  case 'intraday': {
888
1403
  if (!rest) {
889
- const current = ctx.settings.HEARTBEAT_INTRADAY ?? '';
1404
+ const current = ctx.settings.heartbeat?.intraday ?? '';
890
1405
  return {
891
1406
  type: 'message',
892
1407
  message: `Usage: /heartbeat intraday <H:MM,...>\nExample: /heartbeat intraday 8:10,10:10,14:20,16:20\nCurrent: ${current || '(not set)'}`,
@@ -907,14 +1422,14 @@ function handleHeartbeat(args, ctx) {
907
1422
  const value = tokens.join(',');
908
1423
  return {
909
1424
  type: 'notification',
910
- notification: { type: 'heartbeat-set', key: 'HEARTBEAT_INTRADAY', value },
1425
+ notification: { type: 'heartbeat-set', key: 'heartbeat.intraday', value },
911
1426
  };
912
1427
  }
913
1428
  case 'daily': {
914
1429
  if (!rest) {
915
1430
  return {
916
1431
  type: 'message',
917
- message: `Usage: /heartbeat daily <H:MM>\nExample: /heartbeat daily 6:00\nCurrent: ${ctx.settings.HEARTBEAT_DAILY ?? '6:00'}`,
1432
+ message: `Usage: /heartbeat daily <H:MM>\nExample: /heartbeat daily 6:00\nCurrent: ${ctx.settings.heartbeat?.daily ?? '6:00'}`,
918
1433
  };
919
1434
  }
920
1435
  if (!/^\d{1,2}:\d{2}$/.test(rest)) {
@@ -928,14 +1443,14 @@ function handleHeartbeat(args, ctx) {
928
1443
  }
929
1444
  return {
930
1445
  type: 'notification',
931
- notification: { type: 'heartbeat-set', key: 'HEARTBEAT_DAILY', value: rest },
1446
+ notification: { type: 'heartbeat-set', key: 'heartbeat.daily', value: rest },
932
1447
  };
933
1448
  }
934
1449
  case 'weekly': {
935
1450
  if (!rest) {
936
1451
  return {
937
1452
  type: 'message',
938
- message: `Usage: /heartbeat weekly <day@H:MM>\nExample: /heartbeat weekly monday@6:00\nCurrent: ${ctx.settings.HEARTBEAT_WEEKLY ?? 'monday@6:00'}`,
1453
+ message: `Usage: /heartbeat weekly <day@H:MM>\nExample: /heartbeat weekly monday@6:00\nCurrent: ${ctx.settings.heartbeat?.weekly ?? 'monday@6:00'}`,
939
1454
  };
940
1455
  }
941
1456
  const atIdx = rest.indexOf('@');
@@ -952,14 +1467,14 @@ function handleHeartbeat(args, ctx) {
952
1467
  }
953
1468
  return {
954
1469
  type: 'notification',
955
- notification: { type: 'heartbeat-set', key: 'HEARTBEAT_WEEKLY', value: rest },
1470
+ notification: { type: 'heartbeat-set', key: 'heartbeat.weekly', value: rest },
956
1471
  };
957
1472
  }
958
1473
  case 'monthly': {
959
1474
  if (!rest) {
960
1475
  return {
961
1476
  type: 'message',
962
- message: `Usage: /heartbeat monthly <D@H:MM>\nExample: /heartbeat monthly 1@6:00\nCurrent: ${ctx.settings.HEARTBEAT_MONTHLY ?? '1@6:00'}`,
1477
+ message: `Usage: /heartbeat monthly <D@H:MM>\nExample: /heartbeat monthly 1@6:00\nCurrent: ${ctx.settings.heartbeat?.monthly ?? '1@6:00'}`,
963
1478
  };
964
1479
  }
965
1480
  const atIdx = rest.indexOf('@');
@@ -975,7 +1490,28 @@ function handleHeartbeat(args, ctx) {
975
1490
  }
976
1491
  return {
977
1492
  type: 'notification',
978
- notification: { type: 'heartbeat-set', key: 'HEARTBEAT_MONTHLY', value: rest },
1493
+ notification: { type: 'heartbeat-set', key: 'heartbeat.monthly', value: rest },
1494
+ };
1495
+ }
1496
+ case 'dreaming': {
1497
+ if (!rest) {
1498
+ return {
1499
+ type: 'message',
1500
+ message: `Usage: /heartbeat dreaming <H:MM>\nExample: /heartbeat dreaming 2:00\nCurrent: ${ctx.settings.heartbeat?.dreaming ?? '2:00'}`,
1501
+ };
1502
+ }
1503
+ if (!/^\d{1,2}:\d{2}$/.test(rest)) {
1504
+ return { type: 'message', message: `Invalid time: "${rest}". Use H:MM (e.g., 2:00, 14:30).` };
1505
+ }
1506
+ const [hStr, mStr] = rest.split(':');
1507
+ const h = parseInt(hStr ?? '0', 10);
1508
+ const m = parseInt(mStr ?? '0', 10);
1509
+ if (isNaN(h) || isNaN(m) || h < 0 || h > 23 || m < 0 || m > 59) {
1510
+ return { type: 'message', message: `Invalid time: "${rest}". Hour 0-23, minute 0-59.` };
1511
+ }
1512
+ return {
1513
+ type: 'notification',
1514
+ notification: { type: 'heartbeat-set', key: 'heartbeat.dreaming', value: rest },
979
1515
  };
980
1516
  }
981
1517
  case 'now': {
@@ -984,7 +1520,7 @@ function handleHeartbeat(args, ctx) {
984
1520
  default:
985
1521
  return {
986
1522
  type: 'message',
987
- message: `Unknown heartbeat action: "${sub}". Use: status, enable, disable, intraday, daily, weekly, monthly, now`,
1523
+ message: `Unknown heartbeat action: "${sub}". Use: status, enable, disable, intraday, daily, weekly, monthly, dreaming, now`,
988
1524
  };
989
1525
  }
990
1526
  }
@@ -997,7 +1533,7 @@ function handleSnapshots(ctx) {
997
1533
  list.forEach((s, i) => {
998
1534
  const dt = new Date(s.timestamp);
999
1535
  const timeStr = dt.toLocaleString();
1000
- lines.push(` ${i}) ${timeStr} — ${s.sha.slice(0, 7)} (${s.label}, ${s.changedFiles} file${s.changedFiles === 1 ? '' : 's'})`);
1536
+ lines.push(` ${i})-${timeStr} — ${s.sha.slice(0, 7)} (${s.label}, ${s.changedFiles} file${s.changedFiles === 1 ? '' : 's'})`);
1001
1537
  });
1002
1538
  lines.push('\nUse /revert <index> to restore a snapshot.');
1003
1539
  return { type: 'message', message: lines.join('\n') };
@@ -1029,4 +1565,65 @@ async function handleRevert(args, ctx) {
1029
1565
  }
1030
1566
  return { type: 'message', message: result.error ?? 'Revert failed.' };
1031
1567
  }
1568
+ function handleTask(args, ctx) {
1569
+ // /task creates auto-mode scheduled tasks (LLM executes at given time).
1570
+ // Uses unified TaskManager.
1571
+ const parts = args.trim().split(/\s+/);
1572
+ const action = parts[0]?.toLowerCase();
1573
+ const rest = parts.slice(1).join(' ').trim();
1574
+ if (!ctx.taskManager) {
1575
+ return { type: 'message', message: 'Task service not available. Please restart the application.' };
1576
+ }
1577
+ switch (action) {
1578
+ case 'create': {
1579
+ if (!rest) {
1580
+ return {
1581
+ type: 'message',
1582
+ message: 'Usage: /task create <instruction at time>\nExample:\n /task create "at 7:55 make a report about AI models"\nOr use /todo auto add "..." for the new unified format.',
1583
+ };
1584
+ }
1585
+ let { parseReminderTime } = {};
1586
+ try {
1587
+ Object.assign(require('../../core/src/reminder-parser.js'), { parseReminderTime });
1588
+ }
1589
+ catch { /* not available */ }
1590
+ if (!parseReminderTime) {
1591
+ return { type: 'message', message: `Could not parse time from: "${rest}".\nTry: /todo auto add "at 7:55 do something"` };
1592
+ }
1593
+ const parsed = parseReminderTime(rest);
1594
+ if (!parsed) {
1595
+ return { type: 'message', message: `Could not parse time from: "${rest}".\nUse /todo auto add "instruction at time"` };
1596
+ }
1597
+ ctx.taskManager.load();
1598
+ const task = ctx.taskManager.create({ title: parsed.message, mode: 'auto', scope: 'personal', scheduled_at: parsed.scheduledAt });
1599
+ return { type: 'message', message: `Task scheduled:\nTime: ${new Date(task.scheduled_at).toLocaleString()}\nInstruction: ${task.title}\nID: ${task.id}` };
1600
+ }
1601
+ case 'list': {
1602
+ ctx.taskManager.load();
1603
+ const tasks = ctx.taskManager.list({ mode: 'auto' });
1604
+ if (!tasks.length)
1605
+ return { type: 'message', message: 'No scheduled tasks.\nUse /todo auto add "..." to create one.' };
1606
+ const lines = [`Tasks (${tasks.length}):`];
1607
+ for (const t of tasks) {
1608
+ const timeStr = t.scheduled_at ? new Date(t.scheduled_at).toLocaleString() : '—';
1609
+ const statusLabel = t.status === 'pending' ? 'PENDING' : t.status === 'executing' ? 'RUNNING' : t.status.toUpperCase();
1610
+ lines.push(` [${statusLabel}] ${t.title}\n Time: ${timeStr}\n ID: ${t.id.slice(0, 8)}`);
1611
+ }
1612
+ return { type: 'message', message: lines.join('\n') };
1613
+ }
1614
+ case 'delete': {
1615
+ if (!rest)
1616
+ return { type: 'message', message: 'Usage: /task delete <id>' };
1617
+ const result = ctx.taskManager.cancelTask(rest);
1618
+ if (result)
1619
+ return { type: 'message', message: 'Task cancelled.' };
1620
+ return { type: 'message', message: `No task found with ID: ${rest}` };
1621
+ }
1622
+ default:
1623
+ return {
1624
+ type: 'message',
1625
+ message: 'Usage: /task <create|list|delete>\n create <instruction at time> — Schedule a task\n list — List scheduled tasks\n delete <id> — Cancel a task\nOr use /todo auto add "..." for the new unified format.',
1626
+ };
1627
+ }
1628
+ }
1032
1629
  //# sourceMappingURL=slash-commands.js.map