@curie-agent/tui 0.2.3 → 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.
- package/LICENSE +179 -0
- package/dist/src/agents-tab.d.ts +6 -1
- package/dist/src/agents-tab.d.ts.map +1 -1
- package/dist/src/agents-tab.js +14 -2
- package/dist/src/agents-tab.js.map +1 -1
- package/dist/src/approval-picker.d.ts.map +1 -1
- package/dist/src/approval-picker.js +19 -1
- package/dist/src/approval-picker.js.map +1 -1
- package/dist/src/chat-surface.d.ts +3 -2
- package/dist/src/chat-surface.d.ts.map +1 -1
- package/dist/src/chat-surface.js +35 -4
- package/dist/src/chat-surface.js.map +1 -1
- package/dist/src/footer.d.ts +6 -1
- package/dist/src/footer.d.ts.map +1 -1
- package/dist/src/footer.js +10 -2
- package/dist/src/footer.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/init-wizard.d.ts +6 -18
- package/dist/src/init-wizard.d.ts.map +1 -1
- package/dist/src/init-wizard.js +11 -205
- package/dist/src/init-wizard.js.map +1 -1
- package/dist/src/slash-commands.d.ts +19 -3
- package/dist/src/slash-commands.d.ts.map +1 -1
- package/dist/src/slash-commands.js +739 -142
- package/dist/src/slash-commands.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -14
|
@@ -1,4 +1,37 @@
|
|
|
1
|
-
import { pickNextSchedule
|
|
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
|
-
|
|
11
|
-
{ name: '
|
|
12
|
-
{ name: '
|
|
13
|
-
{ name: '
|
|
14
|
-
{ name: '
|
|
15
|
-
|
|
16
|
-
{ name: '
|
|
17
|
-
{ name: '
|
|
18
|
-
{ name: '
|
|
19
|
-
{ name: '
|
|
20
|
-
|
|
21
|
-
{ name: '
|
|
22
|
-
{ name: '
|
|
23
|
-
{ name: '
|
|
24
|
-
|
|
25
|
-
{ name: '
|
|
26
|
-
{ name: '
|
|
27
|
-
{ name: '
|
|
28
|
-
{ name: '
|
|
29
|
-
|
|
30
|
-
{ name: '
|
|
31
|
-
{ name: '
|
|
32
|
-
{ name: '
|
|
33
|
-
{ name: '
|
|
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.
|
|
115
|
-
const websearchPerCall = ctx.settings.
|
|
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
|
-
|
|
128
|
-
?
|
|
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 = [
|
|
287
|
+
const lines = [];
|
|
288
|
+
// Group by category
|
|
289
|
+
const groups = {};
|
|
135
290
|
for (const cmd of SLASH_COMMANDS) {
|
|
136
|
-
|
|
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
|
|
178
|
-
const window = settings
|
|
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
|
|
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
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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 '
|
|
382
|
+
case 'window': {
|
|
202
383
|
if (!rest) {
|
|
203
|
-
const window = settings
|
|
204
|
-
return { type: 'message', message: `Usage: /model
|
|
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>\
|
|
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
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
type: 'message',
|
|
338
|
-
|
|
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
|
-
|
|
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
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
380
|
-
|
|
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 =
|
|
391
|
-
return { type: 'message', message: `Cleared ${removed} completed
|
|
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.
|
|
407
|
-
const userId = ctx.settings.
|
|
408
|
-
const chatId = ctx.settings.
|
|
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.
|
|
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
|
-
|
|
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(`
|
|
671
|
+
lines.push(` ✅ ${id} (${name}) — ${transport}: ${detail}`);
|
|
514
672
|
}
|
|
515
673
|
else {
|
|
516
|
-
lines.push(`
|
|
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.
|
|
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.
|
|
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.
|
|
605
|
-
const websearchPerCall = settings.
|
|
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.
|
|
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 (${
|
|
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.
|
|
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:
|
|
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.
|
|
869
|
-
const intraday = ctx.settings.
|
|
870
|
-
const daily = ctx.settings.
|
|
871
|
-
const weekly = ctx.settings.
|
|
872
|
-
const monthly = ctx.settings.
|
|
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:
|
|
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.
|
|
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: '
|
|
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.
|
|
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: '
|
|
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.
|
|
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: '
|
|
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.
|
|
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: '
|
|
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})
|
|
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
|