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