@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.
- 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 +6 -2
- package/dist/src/chat-surface.d.ts.map +1 -1
- package/dist/src/chat-surface.js +37 -5
- 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 +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -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 +16 -206
- 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 +740 -142
- package/dist/src/slash-commands.js.map +1 -1
- package/dist/src/tab-bar.d.ts +1 -1
- package/dist/src/tab-bar.d.ts.map +1 -1
- package/dist/src/tab-bar.js +1 -0
- package/dist/src/tab-bar.js.map +1 -1
- package/dist/src/wiki-tab.d.ts +15 -0
- package/dist/src/wiki-tab.d.ts.map +1 -0
- package/dist/src/wiki-tab.js +20 -0
- package/dist/src/wiki-tab.js.map +1 -0
- 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 = '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
|
-
|
|
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
|
-
{ name: '
|
|
30
|
-
|
|
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
|
+
{ 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.
|
|
115
|
-
const websearchPerCall = ctx.settings.
|
|
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
|
-
|
|
128
|
-
?
|
|
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 = [
|
|
288
|
+
const lines = [];
|
|
289
|
+
// Group by category
|
|
290
|
+
const groups = {};
|
|
135
291
|
for (const cmd of SLASH_COMMANDS) {
|
|
136
|
-
|
|
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
|
|
178
|
-
const window = settings
|
|
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
|
|
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
|
|
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
|
|
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").` };
|
|
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
|
-
|
|
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 '
|
|
383
|
+
case 'window': {
|
|
202
384
|
if (!rest) {
|
|
203
|
-
const window = settings
|
|
204
|
-
return { type: 'message', message: `Usage: /model
|
|
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>\
|
|
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
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
type: 'message',
|
|
338
|
-
|
|
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
|
-
|
|
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
|
|
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}`);
|
|
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
|
-
|
|
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.` };
|
|
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 =
|
|
391
|
-
return { type: 'message', message: `Cleared ${removed} completed
|
|
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.
|
|
407
|
-
const userId = ctx.settings.
|
|
408
|
-
const chatId = ctx.settings.
|
|
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.
|
|
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
|
-
|
|
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(`
|
|
672
|
+
lines.push(` ✅ ${id} (${name}) — ${transport}: ${detail}`);
|
|
514
673
|
}
|
|
515
674
|
else {
|
|
516
|
-
lines.push(`
|
|
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.
|
|
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.
|
|
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.
|
|
605
|
-
const websearchPerCall = settings.
|
|
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.
|
|
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 (${
|
|
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.
|
|
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:
|
|
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.
|
|
869
|
-
const intraday = ctx.settings.
|
|
870
|
-
const daily = ctx.settings.
|
|
871
|
-
const weekly = ctx.settings.
|
|
872
|
-
const monthly = ctx.settings.
|
|
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:
|
|
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.
|
|
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: '
|
|
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.
|
|
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: '
|
|
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.
|
|
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: '
|
|
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.
|
|
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: '
|
|
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})
|
|
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
|