@hopper-agent/tui 0.1.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 +15 -0
- package/dist/src/agents-tab.d.ts.map +1 -0
- package/dist/src/agents-tab.js +32 -0
- package/dist/src/agents-tab.js.map +1 -0
- package/dist/src/approval-picker.d.ts +11 -0
- package/dist/src/approval-picker.d.ts.map +1 -0
- package/dist/src/approval-picker.js +14 -0
- package/dist/src/approval-picker.js.map +1 -0
- package/dist/src/channels-tab.d.ts +19 -0
- package/dist/src/channels-tab.d.ts.map +1 -0
- package/dist/src/channels-tab.js +37 -0
- package/dist/src/channels-tab.js.map +1 -0
- package/dist/src/chat-surface.d.ts +58 -0
- package/dist/src/chat-surface.d.ts.map +1 -0
- package/dist/src/chat-surface.js +493 -0
- package/dist/src/chat-surface.js.map +1 -0
- package/dist/src/effort-picker.d.ts +10 -0
- package/dist/src/effort-picker.d.ts.map +1 -0
- package/dist/src/effort-picker.js +30 -0
- package/dist/src/effort-picker.js.map +1 -0
- package/dist/src/footer.d.ts +11 -0
- package/dist/src/footer.d.ts.map +1 -0
- package/dist/src/footer.js +11 -0
- package/dist/src/footer.js.map +1 -0
- package/dist/src/header.d.ts +11 -0
- package/dist/src/header.d.ts.map +1 -0
- package/dist/src/header.js +15 -0
- package/dist/src/header.js.map +1 -0
- package/dist/src/index.d.ts +25 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +17 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/init-wizard.d.ts +37 -0
- package/dist/src/init-wizard.d.ts.map +1 -0
- package/dist/src/init-wizard.js +500 -0
- package/dist/src/init-wizard.js.map +1 -0
- package/dist/src/mascot.d.ts +2 -0
- package/dist/src/mascot.d.ts.map +1 -0
- package/dist/src/mascot.js +6 -0
- package/dist/src/mascot.js.map +1 -0
- package/dist/src/mode-picker.d.ts +10 -0
- package/dist/src/mode-picker.d.ts.map +1 -0
- package/dist/src/mode-picker.js +29 -0
- package/dist/src/mode-picker.js.map +1 -0
- package/dist/src/projects-tab.d.ts +16 -0
- package/dist/src/projects-tab.d.ts.map +1 -0
- package/dist/src/projects-tab.js +76 -0
- package/dist/src/projects-tab.js.map +1 -0
- package/dist/src/slash-commands.d.ts +87 -0
- package/dist/src/slash-commands.d.ts.map +1 -0
- package/dist/src/slash-commands.js +814 -0
- package/dist/src/slash-commands.js.map +1 -0
- package/dist/src/stats-data.d.ts +15 -0
- package/dist/src/stats-data.d.ts.map +1 -0
- package/dist/src/stats-data.js +76 -0
- package/dist/src/stats-data.js.map +1 -0
- package/dist/src/stats-tab.d.ts +16 -0
- package/dist/src/stats-tab.d.ts.map +1 -0
- package/dist/src/stats-tab.js +103 -0
- package/dist/src/stats-tab.js.map +1 -0
- package/dist/src/status-line.d.ts +13 -0
- package/dist/src/status-line.d.ts.map +1 -0
- package/dist/src/status-line.js +31 -0
- package/dist/src/status-line.js.map +1 -0
- package/dist/src/tab-bar.d.ts +14 -0
- package/dist/src/tab-bar.d.ts.map +1 -0
- package/dist/src/tab-bar.js +22 -0
- package/dist/src/tab-bar.js.map +1 -0
- package/dist/src/waiting-dot.d.ts +8 -0
- package/dist/src/waiting-dot.d.ts.map +1 -0
- package/dist/src/waiting-dot.js +19 -0
- package/dist/src/waiting-dot.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
import { pickNextSchedule, scheduleLabel } from '@hopper-agent/core';
|
|
2
|
+
const CONTEXT_WINDOW_SIZE = 200_000;
|
|
3
|
+
const THINKING_BUDGET_MAP = {
|
|
4
|
+
low: 2_000,
|
|
5
|
+
medium: 6_000,
|
|
6
|
+
high: 16_000,
|
|
7
|
+
max: 32_000,
|
|
8
|
+
auto: 0,
|
|
9
|
+
};
|
|
10
|
+
export const SLASH_COMMANDS = [
|
|
11
|
+
{ name: 'status', description: 'Show version, model, and account info', usage: '/status' },
|
|
12
|
+
{ name: 'help', description: 'Show all available commands', usage: '/help' },
|
|
13
|
+
{ name: 'debug', description: 'Toggle debug logging', usage: '/debug [on|off]' },
|
|
14
|
+
{ name: 'statusline', description: 'Toggle status line display', usage: '/statusline [on|off]' },
|
|
15
|
+
{ name: 'theme', description: 'Change color theme', usage: '/theme <name>' },
|
|
16
|
+
{ name: 'memory', description: 'View memory file sizes or capture a memory', usage: '/memory [status|add]' },
|
|
17
|
+
{ name: 'stats', description: 'Daily usage, sessions, streaks', usage: '/stats' },
|
|
18
|
+
{ name: 'context', description: 'Visual grid showing context window usage', usage: '/context' },
|
|
19
|
+
{ name: 'model', description: 'Switch AI model', usage: '/model <model>' },
|
|
20
|
+
{ name: 'effort', description: 'Set reasoning effort level', usage: '/effort <low|medium|high|max|auto>' },
|
|
21
|
+
{ name: 'mode', description: 'Set approval mode', usage: '/mode <manual|plan|auto-edit|full-auto|yolo>' },
|
|
22
|
+
{ name: 'agent', description: 'Launch external AI agent', usage: '/agent <prompt>' },
|
|
23
|
+
{ name: 'remind', description: 'Create a reminder', usage: '/remind <message at time>' },
|
|
24
|
+
{ name: 'cron', description: 'Manage reminders', usage: '/cron <list|delete|clear>' },
|
|
25
|
+
{ name: 'channels', description: 'Manage Telegram channel config', usage: '/channels <list|set-bot-token|set-user-id|set-chat-id|disconnect>' },
|
|
26
|
+
{ name: 'tools', description: 'View/set tool call limits per turn', usage: '/tools [tools_per_call [websearch_per_call]]' },
|
|
27
|
+
{ name: 'websearch', description: 'View/set web search+fetch limit per turn', usage: '/websearch [count]' },
|
|
28
|
+
{ name: 'mcp', description: 'Manage MCP server connections', usage: '/mcp <list|add|remove|reload>' },
|
|
29
|
+
{ name: 'exit', description: 'Exit hopper-agent', usage: '/exit' },
|
|
30
|
+
{ name: 'provider', description: 'Switch AI provider', usage: '/provider <anthropic|openai|local|openrouter>' },
|
|
31
|
+
{ name: 'heartbeat', description: 'Manage heartbeat cycle', usage: '/heartbeat <status|enable|disable|times|daily|weekly|monthly|now>' },
|
|
32
|
+
{ name: 'init', description: 'Run the setup wizard', usage: '/init' },
|
|
33
|
+
];
|
|
34
|
+
export function parseSlashCommand(input) {
|
|
35
|
+
const trimmed = input.trim();
|
|
36
|
+
if (!trimmed.startsWith('/'))
|
|
37
|
+
return null;
|
|
38
|
+
const spaceIdx = trimmed.indexOf(' ');
|
|
39
|
+
if (spaceIdx === -1) {
|
|
40
|
+
return { command: trimmed.slice(1).toLowerCase(), args: '' };
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
command: trimmed.slice(1, spaceIdx).toLowerCase(),
|
|
44
|
+
args: trimmed.slice(spaceIdx + 1).trim(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function handleSlashCommand(cmd, args, ctx) {
|
|
48
|
+
switch (cmd) {
|
|
49
|
+
case 'status':
|
|
50
|
+
return handleStatus(ctx);
|
|
51
|
+
case 'help':
|
|
52
|
+
return handleHelp();
|
|
53
|
+
case 'debug':
|
|
54
|
+
return handleDebug(args, ctx.settings.debug);
|
|
55
|
+
case 'statusline':
|
|
56
|
+
return handleStatusline(args, ctx.settings.statusline);
|
|
57
|
+
case 'theme':
|
|
58
|
+
return handleTheme(args);
|
|
59
|
+
case 'memory':
|
|
60
|
+
return handleMemory(args, ctx);
|
|
61
|
+
case 'stats':
|
|
62
|
+
return { type: 'switch_tab', tab: 'stats', message: 'Switched to Stats tab' };
|
|
63
|
+
case 'context':
|
|
64
|
+
return handleContext(ctx);
|
|
65
|
+
case 'model':
|
|
66
|
+
return handleModel(args);
|
|
67
|
+
case 'effort':
|
|
68
|
+
return handleEffort(args);
|
|
69
|
+
case 'mode':
|
|
70
|
+
return handleMode(args);
|
|
71
|
+
case 'exit':
|
|
72
|
+
case 'quit':
|
|
73
|
+
return { type: 'exit', message: 'Exiting hopper-agent.' };
|
|
74
|
+
case 'agent':
|
|
75
|
+
return handleAgent(args);
|
|
76
|
+
case 'remind':
|
|
77
|
+
return handleRemind(args, ctx);
|
|
78
|
+
case 'cron':
|
|
79
|
+
return handleCron(args, ctx);
|
|
80
|
+
case 'channels':
|
|
81
|
+
return handleChannels(args, ctx);
|
|
82
|
+
case 'mcp':
|
|
83
|
+
return handleMcp(args, ctx);
|
|
84
|
+
case 'tools':
|
|
85
|
+
return handleTools(args, ctx.settings);
|
|
86
|
+
case 'websearch':
|
|
87
|
+
return handleWebsearch(args, ctx.settings);
|
|
88
|
+
case 'provider':
|
|
89
|
+
return handleProvider(args);
|
|
90
|
+
case 'heartbeat':
|
|
91
|
+
return handleHeartbeat(args, ctx);
|
|
92
|
+
case 'init': {
|
|
93
|
+
// Wizard is interactive — provider, key, URL, model are handled via onSubmit.
|
|
94
|
+
// /init triggers the initial prompt; subsequent user input flows through onSubmit.
|
|
95
|
+
if (!args) {
|
|
96
|
+
return { type: 'message', message: 'init_wizard' };
|
|
97
|
+
}
|
|
98
|
+
// Treat as direct API key (legacy behavior)
|
|
99
|
+
return { type: 'update_init', message: `API key configured: ${args.trim()}`, apiKey: args.trim() };
|
|
100
|
+
}
|
|
101
|
+
default:
|
|
102
|
+
return {
|
|
103
|
+
type: 'message',
|
|
104
|
+
message: `Unknown command: /${cmd}. Type /help for available commands.`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function handleStatus(ctx) {
|
|
109
|
+
const toolsPerCall = ctx.settings.TOOLS_PER_CALL ?? 10;
|
|
110
|
+
const websearchPerCall = ctx.settings.WEBSEARCH_PER_CALL ?? 5;
|
|
111
|
+
const lines = [
|
|
112
|
+
`hopper-agent v${ctx.version}`,
|
|
113
|
+
`Model: ${ctx.model}`,
|
|
114
|
+
`Provider: ${ctx.provider}`,
|
|
115
|
+
`Mode: ${ctx.approvalMode}`,
|
|
116
|
+
`CWD: ${ctx.cwd}`,
|
|
117
|
+
ctx.inputTokens !== undefined
|
|
118
|
+
? `Tokens: ${ctx.inputTokens} in / ${ctx.outputTokens} out`
|
|
119
|
+
: null,
|
|
120
|
+
`Tools per turn: ${toolsPerCall}`,
|
|
121
|
+
`WebSearch per turn: ${websearchPerCall}`,
|
|
122
|
+
].filter(Boolean);
|
|
123
|
+
return { type: 'message', message: lines.join('\n') };
|
|
124
|
+
}
|
|
125
|
+
function handleHelp() {
|
|
126
|
+
const lines = ['Slash commands:'];
|
|
127
|
+
for (const cmd of SLASH_COMMANDS) {
|
|
128
|
+
lines.push(` ${cmd.usage.padEnd(24)} ${cmd.description}`);
|
|
129
|
+
}
|
|
130
|
+
return { type: 'message', message: lines.join('\n') };
|
|
131
|
+
}
|
|
132
|
+
function handleDebug(args, current) {
|
|
133
|
+
const next = args === 'on' ? true : args === 'off' ? false : !current;
|
|
134
|
+
return {
|
|
135
|
+
type: 'update_debug',
|
|
136
|
+
debug: next,
|
|
137
|
+
message: `Debug logging: ${next ? 'enabled' : 'disabled'}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function handleStatusline(args, current) {
|
|
141
|
+
const next = args === 'on' ? true : args === 'off' ? false : !current;
|
|
142
|
+
return {
|
|
143
|
+
type: 'update_statusline',
|
|
144
|
+
statusline: next,
|
|
145
|
+
message: `Status line: ${next ? 'visible' : 'hidden'}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function handleTheme(args) {
|
|
149
|
+
const validThemes = ['tokyo-night', 'nord', 'dracula', 'solarized', 'gruvbox', 'black', 'white', 'grey'];
|
|
150
|
+
const theme = args.toLowerCase();
|
|
151
|
+
if (!theme) {
|
|
152
|
+
return {
|
|
153
|
+
type: 'message',
|
|
154
|
+
message: `Available themes: ${validThemes.join(', ')}\nUsage: /theme <name>`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (!validThemes.includes(theme)) {
|
|
158
|
+
return {
|
|
159
|
+
type: 'message',
|
|
160
|
+
message: `Unknown theme: "${theme}". Available: ${validThemes.join(', ')}`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return { type: 'update_theme', theme, message: `Theme changed to: ${theme}` };
|
|
164
|
+
}
|
|
165
|
+
function handleModel(args) {
|
|
166
|
+
if (!args) {
|
|
167
|
+
return {
|
|
168
|
+
type: 'message',
|
|
169
|
+
message: 'Usage: /model <model>\nExamples: /model claude-opus-4-7, /model sonnet, /model gpt4o, /model o1',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// Allow aliases
|
|
173
|
+
const aliasMap = {
|
|
174
|
+
opus: 'claude-opus-4-7',
|
|
175
|
+
sonnet: 'claude-sonnet-4-6',
|
|
176
|
+
haiku: 'claude-haiku-4-5-20251001',
|
|
177
|
+
gpt4o: 'gpt-4o',
|
|
178
|
+
gpt4turbo: 'gpt-4-turbo',
|
|
179
|
+
o1: 'o1',
|
|
180
|
+
'o3-mini': 'o3-mini',
|
|
181
|
+
};
|
|
182
|
+
const resolved = aliasMap[args.toLowerCase()] || args;
|
|
183
|
+
return { type: 'update_model', model: resolved, message: `Model changed to: ${resolved}` };
|
|
184
|
+
}
|
|
185
|
+
function handleEffort(args) {
|
|
186
|
+
const valid = ['low', 'medium', 'high', 'max', 'auto'];
|
|
187
|
+
if (!args) {
|
|
188
|
+
return {
|
|
189
|
+
type: 'message',
|
|
190
|
+
message: `Usage: /effort <level>\nLevels: ${valid.join(', ')}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const level = args.toLowerCase();
|
|
194
|
+
if (!valid.includes(level)) {
|
|
195
|
+
return {
|
|
196
|
+
type: 'message',
|
|
197
|
+
message: `Invalid effort level: "${args}". Valid: ${valid.join(', ')}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return { type: 'update_effort', effort: level, message: `Effort set to: ${level}` };
|
|
201
|
+
}
|
|
202
|
+
function handleMode(args) {
|
|
203
|
+
const valid = ['plan', 'edit', 'auto', 'yolo'];
|
|
204
|
+
if (!args) {
|
|
205
|
+
return {
|
|
206
|
+
type: 'message',
|
|
207
|
+
message: `Usage: /mode <mode>\nModes: ${valid.join(', ')}`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const mode = args.toLowerCase();
|
|
211
|
+
if (!valid.includes(mode)) {
|
|
212
|
+
return {
|
|
213
|
+
type: 'message',
|
|
214
|
+
message: `Invalid mode: "${args}". Valid: ${valid.join(', ')}`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return { type: 'update_mode', mode: mode, message: `Mode changed to: ${mode}` };
|
|
218
|
+
}
|
|
219
|
+
function handleAgent(args) {
|
|
220
|
+
if (!args) {
|
|
221
|
+
return {
|
|
222
|
+
type: 'message',
|
|
223
|
+
message: 'Usage: /agent [--mode plan|edit|auto|yolo] [--effort low|medium|high|max|auto] <prompt>\nExample: /agent --mode auto --effort medium check codebase',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
let agentMode;
|
|
227
|
+
let agentEffort;
|
|
228
|
+
let remaining = args;
|
|
229
|
+
const validModes = ['plan', 'edit', 'auto', 'yolo'];
|
|
230
|
+
const validEfforts = ['low', 'medium', 'high', 'max', 'auto'];
|
|
231
|
+
// Match --mode <word> or --effort <word> with optional trailing text
|
|
232
|
+
const flagRegex = /^--(mode|effort)\s+(\S+?)(?:\s+(.+))?$/;
|
|
233
|
+
let found = true;
|
|
234
|
+
while (found) {
|
|
235
|
+
found = false;
|
|
236
|
+
const match = remaining.match(flagRegex);
|
|
237
|
+
if (match) {
|
|
238
|
+
const flag = match[1];
|
|
239
|
+
const value = (match[2] ?? '').trim();
|
|
240
|
+
if (flag === 'mode' && validModes.includes(value)) {
|
|
241
|
+
agentMode = value;
|
|
242
|
+
}
|
|
243
|
+
else if (flag === 'effort' && validEfforts.includes(value)) {
|
|
244
|
+
agentEffort = value;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
break; // unknown flag or invalid value → stop parsing
|
|
248
|
+
}
|
|
249
|
+
// match[3] is the remaining text after the flag value
|
|
250
|
+
remaining = (match[3] ?? '').trim();
|
|
251
|
+
found = true;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const prompt = remaining.trim();
|
|
255
|
+
if (!prompt) {
|
|
256
|
+
return {
|
|
257
|
+
type: 'message',
|
|
258
|
+
message: 'Usage: /agent [--mode plan|edit|auto|yolo] [--effort low|medium|high|max|auto] <prompt>\nExample: /agent --mode auto --effort medium check codebase',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const agentId = crypto.randomUUID();
|
|
262
|
+
return {
|
|
263
|
+
type: 'start_agent',
|
|
264
|
+
agentId,
|
|
265
|
+
message: `Agent started: "${prompt}"`,
|
|
266
|
+
agentMode,
|
|
267
|
+
agentEffort,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function handleRemind(args, ctx) {
|
|
271
|
+
if (!args) {
|
|
272
|
+
return {
|
|
273
|
+
type: 'message',
|
|
274
|
+
message: 'Usage: /remind <message at time>\nExamples: /remind "tomorrow at 7am make breakfast", /remind "in 30 minutes call mom"',
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const cronManager = ctx.cronManager;
|
|
278
|
+
if (!cronManager) {
|
|
279
|
+
return {
|
|
280
|
+
type: 'message',
|
|
281
|
+
message: 'Reminder service not available. Please restart the application.',
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
const { parseReminderTime } = require('../../core/src/reminder-parser.js');
|
|
285
|
+
const parsed = parseReminderTime(args);
|
|
286
|
+
if (!parsed) {
|
|
287
|
+
return {
|
|
288
|
+
type: 'message',
|
|
289
|
+
message: `Could not parse time from: "${args}".\nTry: /remind "tomorrow at 7am make breakfast"`,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
const task = cronManager.createReminder(parsed.message, parsed.scheduledAt);
|
|
293
|
+
const timeStr = new Date(task.scheduledAt).toLocaleString();
|
|
294
|
+
return {
|
|
295
|
+
type: 'message',
|
|
296
|
+
message: `Hopper reminder:\nDate: ${timeStr}\n${parsed.message}\nID: ${task.id}`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function handleCron(args, ctx) {
|
|
300
|
+
const cronManager = ctx.cronManager;
|
|
301
|
+
if (!cronManager) {
|
|
302
|
+
return {
|
|
303
|
+
type: 'message',
|
|
304
|
+
message: 'Reminder service not available. Please restart the application.',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
// Reload from disk before each command so tool-created reminders
|
|
308
|
+
// are visible even if the tool used a separate CronManager instance.
|
|
309
|
+
cronManager.load();
|
|
310
|
+
const parts = args.trim().split(/\s+/);
|
|
311
|
+
const action = parts[0]?.toLowerCase();
|
|
312
|
+
const rest = parts.slice(1).join(' ').trim();
|
|
313
|
+
switch (action) {
|
|
314
|
+
case 'list': {
|
|
315
|
+
const statusFilter = ['pending', 'fired', 'cancelled'].includes(rest) ? rest : undefined;
|
|
316
|
+
const tasks = cronManager.listReminders(statusFilter);
|
|
317
|
+
if (tasks.length === 0) {
|
|
318
|
+
return {
|
|
319
|
+
type: 'message',
|
|
320
|
+
message: statusFilter
|
|
321
|
+
? `No ${statusFilter} reminders.`
|
|
322
|
+
: 'No reminders yet.\nUse /remind to create one.',
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const lines = [`Reminders (${tasks.length}${statusFilter ? ` — ${statusFilter}` : ''}):`];
|
|
326
|
+
for (const t of tasks) {
|
|
327
|
+
const timeStr = new Date(t.scheduledAt).toLocaleString();
|
|
328
|
+
const statusEmoji = t.status === 'pending' ? '⏳' : t.status === 'fired' ? '✅' : '❌';
|
|
329
|
+
const label = t.schedule ? `[${scheduleLabel(t.schedule.type)}] ` : '';
|
|
330
|
+
lines.push(` ${statusEmoji} ${label}${t.message}\n Date: ${timeStr}\n ID: ${t.id}`);
|
|
331
|
+
}
|
|
332
|
+
return { type: 'message', message: lines.join('\n') };
|
|
333
|
+
}
|
|
334
|
+
case 'delete': {
|
|
335
|
+
if (!rest) {
|
|
336
|
+
return {
|
|
337
|
+
type: 'message',
|
|
338
|
+
message: 'Usage: /cron delete <id>\nExample: /cron delete abc-123',
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const result = cronManager.cancelReminder(rest);
|
|
342
|
+
if (!result) {
|
|
343
|
+
return { type: 'message', message: `No reminder found with ID: ${rest}` };
|
|
344
|
+
}
|
|
345
|
+
return { type: 'message', message: `Reminder cancelled.` };
|
|
346
|
+
}
|
|
347
|
+
case 'clear': {
|
|
348
|
+
const removed = cronManager.clearCompleted();
|
|
349
|
+
return { type: 'message', message: `Cleared ${removed} completed reminder(s).` };
|
|
350
|
+
}
|
|
351
|
+
default:
|
|
352
|
+
return {
|
|
353
|
+
type: 'message',
|
|
354
|
+
message: `Unknown cron action: "${action}". Use: list, delete, clear`,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function handleChannels(args, ctx) {
|
|
359
|
+
const parts = args.trim().split(/\s+/);
|
|
360
|
+
const sub = parts[0]?.toLowerCase();
|
|
361
|
+
const rest = parts.slice(1).join(' ').trim();
|
|
362
|
+
switch (sub) {
|
|
363
|
+
case 'list': {
|
|
364
|
+
const token = ctx.settings.TELEGRAM_BOT_TOKEN;
|
|
365
|
+
const userId = ctx.settings.TELEGRAM_USER_ID;
|
|
366
|
+
const chatId = ctx.settings.TELEGRAM_CHAT_ID;
|
|
367
|
+
const tokenMask = token && token.length > 8
|
|
368
|
+
? token.slice(0, 8) + '...'
|
|
369
|
+
: token || '(not set)';
|
|
370
|
+
return {
|
|
371
|
+
type: 'message',
|
|
372
|
+
message: `Telegram Configuration:\n Bot Token: ${tokenMask}\n Allowed User ID: ${userId || '(not set)'}\n Chat ID: ${chatId || '(not set)'}`,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
case 'set-bot-token': {
|
|
376
|
+
if (!rest) {
|
|
377
|
+
return {
|
|
378
|
+
type: 'message',
|
|
379
|
+
message: 'Usage: /channels set-bot-token <token>\nExample: /channels set-bot-token 123456:ABC-DEF...',
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return { type: 'external', external: 'channels.set-bot-token', message: rest };
|
|
383
|
+
}
|
|
384
|
+
case 'set-user-id': {
|
|
385
|
+
if (!rest) {
|
|
386
|
+
return {
|
|
387
|
+
type: 'message',
|
|
388
|
+
message: 'Usage: /channels set-user-id <id>\nExample: /channels set-user-id 123456789',
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
return { type: 'external', external: 'channels.set-user-id', message: rest };
|
|
392
|
+
}
|
|
393
|
+
case 'set-chat-id': {
|
|
394
|
+
if (!rest) {
|
|
395
|
+
return {
|
|
396
|
+
type: 'message',
|
|
397
|
+
message: 'Usage: /channels set-chat-id <chatId>\nExample: /channels set-chat-id -1001234567890',
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return { type: 'external', external: 'channels.set-chat-id', message: rest };
|
|
401
|
+
}
|
|
402
|
+
case 'disconnect': {
|
|
403
|
+
return { type: 'external', external: 'channels.disconnect' };
|
|
404
|
+
}
|
|
405
|
+
case 'switch': {
|
|
406
|
+
if (!rest) {
|
|
407
|
+
return {
|
|
408
|
+
type: 'message',
|
|
409
|
+
message: 'Usage: /channels switch <channelId>\nExamples: /channels switch main, /channels switch telegram:12345',
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
return { type: 'external', external: 'channels.switch', message: rest };
|
|
413
|
+
}
|
|
414
|
+
default:
|
|
415
|
+
return {
|
|
416
|
+
type: 'message',
|
|
417
|
+
message: `Unknown channel action: "${sub}". Use: list, switch, set-bot-token, set-user-id, set-chat-id, disconnect`,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function handleMcp(args, ctx) {
|
|
422
|
+
const parts = args.trim().split(/\s+/);
|
|
423
|
+
const sub = parts[0]?.toLowerCase();
|
|
424
|
+
const rest = parts.slice(1).join(' ').trim();
|
|
425
|
+
if (!sub) {
|
|
426
|
+
return {
|
|
427
|
+
type: 'message',
|
|
428
|
+
message: 'Usage: /mcp <list|add|remove|reload>\n list — Show configured MCP servers\n add <id> <transport> ... — Add an MCP server\n remove <id> — Remove an MCP server by ID\n reload — Reconnect all MCP servers',
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
// Parse current MCP servers from settings
|
|
432
|
+
let configs = {};
|
|
433
|
+
try {
|
|
434
|
+
const raw = ctx.settings.MCP_SERVERS;
|
|
435
|
+
if (typeof raw === 'string' && raw.trim().length > 0) {
|
|
436
|
+
configs = JSON.parse(raw);
|
|
437
|
+
}
|
|
438
|
+
else if (raw && typeof raw === 'object') {
|
|
439
|
+
configs = raw;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
/* ignore */
|
|
444
|
+
}
|
|
445
|
+
switch (sub) {
|
|
446
|
+
case 'list': {
|
|
447
|
+
const entries = Object.entries(configs);
|
|
448
|
+
if (entries.length === 0) {
|
|
449
|
+
return {
|
|
450
|
+
type: 'message',
|
|
451
|
+
message: 'No MCP servers configured.\nUse /mcp add to add one.\n\nExample:\n /mcp add filesystem stdio npx -y @modelcontextprotocol/server-filesystem /workspace',
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const lines = [`MCP Servers (${entries.length}):`];
|
|
455
|
+
for (const [id, cfg] of entries) {
|
|
456
|
+
const c = cfg;
|
|
457
|
+
const transport = c.transport || 'unknown';
|
|
458
|
+
const name = c.name || id;
|
|
459
|
+
let detail = '';
|
|
460
|
+
if (transport === 'stdio') {
|
|
461
|
+
detail = `${c.command || '?'} ${c.args?.join(' ') || ''}`;
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
detail = `url: ${c.url || '?'}`;
|
|
465
|
+
}
|
|
466
|
+
const client = ctx.mcpClients?.find((cl) => cl.serverId === id);
|
|
467
|
+
if (!client) {
|
|
468
|
+
lines.push(` · ${id} (${name}) — ${transport}: ${detail} [not loaded]`);
|
|
469
|
+
}
|
|
470
|
+
else if (client.isConnected) {
|
|
471
|
+
lines.push(` ● ${id} (${name}) — ${transport}: ${detail} [connected]`);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
lines.push(` ○ ${id} (${name}) — ${transport}: ${detail} [disconnected]`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
return { type: 'message', message: lines.join('\n') };
|
|
478
|
+
}
|
|
479
|
+
case 'add': {
|
|
480
|
+
// /mcp add <id> <transport> [--env key=value ...] [command] [arg ...]
|
|
481
|
+
if (!rest) {
|
|
482
|
+
return {
|
|
483
|
+
type: 'message',
|
|
484
|
+
message: 'Usage: /mcp add <id> <transport> [--env key=value ...] [command] [arg ...]\n\nExamples:\n /mcp add filesystem stdio npx -y @modelcontextprotocol/server-filesystem /workspace\n /mcp add github stdio npx -y @modelcontextprotocol/server-github --env GITHUB_TOKEN=ghp_xxx\n /mcp add my-api sse https://api.example.com/mcp',
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const addParts = rest.split(/\s+/);
|
|
488
|
+
const id = addParts[0];
|
|
489
|
+
const transport = addParts[1]?.toLowerCase();
|
|
490
|
+
if (!id || !transport) {
|
|
491
|
+
return {
|
|
492
|
+
type: 'message',
|
|
493
|
+
message: 'Usage: /mcp add <id> <stdio|sse|streamable-http> [flags] [command] [args...]\nExample: /mcp add filesystem stdio npx -y @modelcontextprotocol/server-filesystem /workspace',
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
if (!['stdio', 'sse', 'streamable-http'].includes(transport)) {
|
|
497
|
+
return { type: 'message', message: `Invalid transport: "${transport}". Use: stdio, sse, streamable-http` };
|
|
498
|
+
}
|
|
499
|
+
if (configs[id]) {
|
|
500
|
+
return { type: 'message', message: `Server "${id}" already exists. Use /mcp reload to reconnect.` };
|
|
501
|
+
}
|
|
502
|
+
const cfg = { id, name: id, transport };
|
|
503
|
+
let i = 2;
|
|
504
|
+
// Parse --env key=value flags
|
|
505
|
+
const env = {};
|
|
506
|
+
while (i < addParts.length && addParts[i]?.startsWith('--')) {
|
|
507
|
+
if (addParts[i] === '--env' && i + 1 < addParts.length) {
|
|
508
|
+
i++;
|
|
509
|
+
const kv = addParts[i];
|
|
510
|
+
if (!kv)
|
|
511
|
+
break;
|
|
512
|
+
const eqIdx = kv.indexOf('=');
|
|
513
|
+
if (eqIdx > 0) {
|
|
514
|
+
env[kv.slice(0, eqIdx)] = kv.slice(eqIdx + 1);
|
|
515
|
+
}
|
|
516
|
+
i++;
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (transport === 'stdio' && i < addParts.length) {
|
|
523
|
+
cfg.command = addParts[i];
|
|
524
|
+
const cmdArgs = addParts.slice(i + 1);
|
|
525
|
+
if (cmdArgs.length > 0)
|
|
526
|
+
cfg.args = cmdArgs;
|
|
527
|
+
}
|
|
528
|
+
else if (transport === 'sse' || transport === 'streamable-http') {
|
|
529
|
+
cfg.url = addParts[i] || undefined;
|
|
530
|
+
}
|
|
531
|
+
if (Object.keys(env).length > 0)
|
|
532
|
+
cfg.env = env;
|
|
533
|
+
configs[id] = cfg;
|
|
534
|
+
ctx.settings.MCP_SERVERS = configs;
|
|
535
|
+
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.` };
|
|
536
|
+
}
|
|
537
|
+
case 'remove': {
|
|
538
|
+
if (!rest) {
|
|
539
|
+
return {
|
|
540
|
+
type: 'message',
|
|
541
|
+
message: 'Usage: /mcp remove <id>\nExample: /mcp remove filesystem',
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
if (!configs[rest]) {
|
|
545
|
+
return { type: 'message', message: `No MCP server found with ID: ${rest}` };
|
|
546
|
+
}
|
|
547
|
+
delete configs[rest];
|
|
548
|
+
ctx.settings.MCP_SERVERS = configs;
|
|
549
|
+
return { type: 'update_mcp', mcpServerId: rest, message: `Removed MCP server "${rest}". Run /mcp reload to apply.` };
|
|
550
|
+
}
|
|
551
|
+
case 'reload': {
|
|
552
|
+
return { type: 'update_mcp', message: 'MCP servers reloaded. Reconnecting...' };
|
|
553
|
+
}
|
|
554
|
+
default:
|
|
555
|
+
return {
|
|
556
|
+
type: 'message',
|
|
557
|
+
message: `Unknown MCP action: "${sub}". Use: list, add, remove, reload`,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function handleTools(args, settings) {
|
|
562
|
+
const toolsPerCall = settings.TOOLS_PER_CALL ?? 10;
|
|
563
|
+
const websearchPerCall = settings.WEBSEARCH_PER_CALL ?? 5;
|
|
564
|
+
if (!args.trim()) {
|
|
565
|
+
return {
|
|
566
|
+
type: 'message',
|
|
567
|
+
message: `Tool call limits (per turn):\n Tools: ${toolsPerCall}\n WebSearch+WebFetch: ${websearchPerCall}\n\nUsage:\n /tools 15 — set tools per call\n /tools 15 8 — set both limits`,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const parts = args.trim().split(/\s+/);
|
|
571
|
+
const val = parseInt(parts[0], 10);
|
|
572
|
+
if (isNaN(val) || val < 1) {
|
|
573
|
+
return { type: 'message', message: `Invalid value: "${parts[0]}". Must be a positive integer.` };
|
|
574
|
+
}
|
|
575
|
+
const result = {
|
|
576
|
+
type: 'update_tools_per_call',
|
|
577
|
+
toolsPerCall: val,
|
|
578
|
+
message: `Tools per call set to: ${val}`,
|
|
579
|
+
};
|
|
580
|
+
if (parts[1]) {
|
|
581
|
+
const wsVal = parseInt(parts[1], 10);
|
|
582
|
+
if (isNaN(wsVal) || wsVal < 1) {
|
|
583
|
+
return { type: 'message', message: `Invalid websearch value: "${parts[1]}". Must be a positive integer.` };
|
|
584
|
+
}
|
|
585
|
+
result.websearchPerCall = wsVal;
|
|
586
|
+
}
|
|
587
|
+
return result;
|
|
588
|
+
}
|
|
589
|
+
function handleWebsearch(args, settings) {
|
|
590
|
+
const websearchPerCall = settings.WEBSEARCH_PER_CALL ?? 5;
|
|
591
|
+
if (!args.trim()) {
|
|
592
|
+
return {
|
|
593
|
+
type: 'message',
|
|
594
|
+
message: `WebSearch/WebFetch limit per turn: ${websearchPerCall}\n\nUsage:\n /websearch 3 — set websearch+fetch limit`,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const val = parseInt(args.trim(), 10);
|
|
598
|
+
if (isNaN(val) || val < 1) {
|
|
599
|
+
return { type: 'message', message: `Invalid value: "${args}". Must be a positive integer.` };
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
type: 'update_websearch_per_call',
|
|
603
|
+
websearchPerCall: val,
|
|
604
|
+
message: `WebSearch+WebFetch per turn set to: ${val}`,
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
function handleMemory(args, ctx) {
|
|
608
|
+
const parts = args.trim().split(/\s+/);
|
|
609
|
+
const sub = parts[0]?.toLowerCase();
|
|
610
|
+
const rest = parts.slice(1).join(' ').trim();
|
|
611
|
+
switch (sub) {
|
|
612
|
+
case 'status':
|
|
613
|
+
return {
|
|
614
|
+
type: 'update_memory',
|
|
615
|
+
message: 'Retrieving memory file sizes...',
|
|
616
|
+
memory: { content: '', operation: 'status' },
|
|
617
|
+
};
|
|
618
|
+
case 'add': {
|
|
619
|
+
if (!rest) {
|
|
620
|
+
return {
|
|
621
|
+
type: 'message',
|
|
622
|
+
message: 'Usage: /memory add <text>\nExample: /memory add user prefers TypeScript over JavaScript',
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
const entry = `- [${new Date().toISOString()}] ${rest}`;
|
|
626
|
+
return {
|
|
627
|
+
type: 'update_memory',
|
|
628
|
+
message: `Memory captured: "${rest}"\nThe agent will organize it into memory files on the next turn.`,
|
|
629
|
+
memory: { content: entry, operation: 'add' },
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
default: {
|
|
633
|
+
return {
|
|
634
|
+
type: 'message',
|
|
635
|
+
message: 'Memory commands:\n /memory status — Show memory file sizes\n /memory add <text> — Capture a memory for the agent to organize',
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function handleContext(ctx) {
|
|
641
|
+
const input = ctx.inputTokens ?? 0;
|
|
642
|
+
const output = ctx.outputTokens ?? 0;
|
|
643
|
+
const model = ctx.model || 'unknown';
|
|
644
|
+
const windowSize = ctx.contextWindowSize ?? CONTEXT_WINDOW_SIZE;
|
|
645
|
+
const pct = input > 0 ? Math.min(100, Math.round((input / windowSize) * 100)) : 0;
|
|
646
|
+
const filled = Math.round((pct / 100) * 24);
|
|
647
|
+
const bar = '█'.repeat(filled) + '░'.repeat(24 - filled);
|
|
648
|
+
const fmt = (n) => (n >= 1_000 ? `${Math.round(n / 1_000)}k` : String(n));
|
|
649
|
+
if (input === 0 && output === 0) {
|
|
650
|
+
return { type: 'message', message: 'No token data yet. Start a conversation to see context window usage.' };
|
|
651
|
+
}
|
|
652
|
+
const lines = [`Context Window (${model}): ${bar} ${pct}% (${fmt(input)}/${fmt(windowSize)})`];
|
|
653
|
+
if (input > 0 && output > 0) {
|
|
654
|
+
lines.push(` └─ ${fmt(input)} in / ${fmt(output)} out`);
|
|
655
|
+
}
|
|
656
|
+
return { type: 'message', message: lines.join('\n') };
|
|
657
|
+
}
|
|
658
|
+
const PROVIDER_MODEL_ALIASES = {
|
|
659
|
+
opus: 'claude-opus-4-7',
|
|
660
|
+
sonnet: 'claude-sonnet-4-6',
|
|
661
|
+
haiku: 'claude-haiku-4-5-20251001',
|
|
662
|
+
gpt4o: 'gpt-4o',
|
|
663
|
+
gpt4turbo: 'gpt-4-turbo',
|
|
664
|
+
o1: 'o1',
|
|
665
|
+
'o3-mini': 'o3-mini',
|
|
666
|
+
};
|
|
667
|
+
function handleProvider(args) {
|
|
668
|
+
const validProviders = ['anthropic', 'openai', 'local', 'openrouter'];
|
|
669
|
+
if (!args) {
|
|
670
|
+
return {
|
|
671
|
+
type: 'message',
|
|
672
|
+
message: `Usage: /provider <name>\nProviders: ${validProviders.join(', ')}\n\nCurrent settings:\n anthropic: MODEL_API_KEY, MODEL_URL\n openai: OPENAI_API_KEY, OPENAI_BASE_URL\n local: MODEL_URL, MODEL_API_KEY\n openrouter: OPENROUTER_URL, OPENROUTER_API_KEY`,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
const provider = args.toLowerCase();
|
|
676
|
+
if (!validProviders.includes(provider)) {
|
|
677
|
+
return {
|
|
678
|
+
type: 'message',
|
|
679
|
+
message: `Unknown provider: "${args}". Valid: ${validProviders.join(', ')}`,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
return { type: 'update_provider', provider, message: `Provider switched to: ${provider}` };
|
|
683
|
+
}
|
|
684
|
+
function handleHeartbeat(args, ctx) {
|
|
685
|
+
const parts = args.trim().split(/\s+/);
|
|
686
|
+
const sub = parts[0]?.toLowerCase();
|
|
687
|
+
const rest = parts.slice(1).join(' ').trim();
|
|
688
|
+
switch (sub) {
|
|
689
|
+
case 'status':
|
|
690
|
+
case '': {
|
|
691
|
+
const active = ctx.settings.HEARTBEAT === 'on';
|
|
692
|
+
const times = ctx.settings.HEARTBEAT_TIMES ?? '';
|
|
693
|
+
const daily = ctx.settings.HEARTBEAT_DAILY ?? '6:00';
|
|
694
|
+
const weekly = ctx.settings.HEARTBEAT_WEEKLY ?? 'monday@6:00';
|
|
695
|
+
const monthly = ctx.settings.HEARTBEAT_MONTHLY ?? '1@6:00';
|
|
696
|
+
const timesDisplay = times ? times.split(',').map((s) => s.trim()).join(', ') : '(not set)';
|
|
697
|
+
const picked = pickNextSchedule({ HEARTBEAT_TIMES: times, HEARTBEAT_DAILY: daily, HEARTBEAT_WEEKLY: weekly, HEARTBEAT_MONTHLY: monthly });
|
|
698
|
+
const activeSchedule = picked ? `${picked.type} (${picked.value})` : '(none configured)';
|
|
699
|
+
return {
|
|
700
|
+
type: 'message',
|
|
701
|
+
message: `Heartbeat cycle:\n Enabled: ${active ? 'yes' : 'no'}\n Active schedule: ${activeSchedule}\n\n Times: ${timesDisplay}\n Daily: ${daily}\n Weekly: ${weekly}\n Monthly: ${monthly}\n\nUsage:\n /heartbeat — show status\n /heartbeat enable — turn on\n /heartbeat disable — turn off\n /heartbeat times <H:MM,...> — set intra-day times (e.g. 8:10,10:10,14:20)\n /heartbeat daily <H:MM> — set daily time (24h)\n /heartbeat weekly <day@H:MM>\n /heartbeat monthly <D@H:MM>\n /heartbeat now — run immediately`,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
case 'enable': {
|
|
705
|
+
return { type: 'notification', notification: { type: 'heartbeat', enabled: true } };
|
|
706
|
+
}
|
|
707
|
+
case 'disable': {
|
|
708
|
+
return { type: 'notification', notification: { type: 'heartbeat', enabled: false } };
|
|
709
|
+
}
|
|
710
|
+
case 'times': {
|
|
711
|
+
if (!rest) {
|
|
712
|
+
const current = ctx.settings.HEARTBEAT_TIMES ?? '';
|
|
713
|
+
return {
|
|
714
|
+
type: 'message',
|
|
715
|
+
message: `Usage: /heartbeat times <H:MM,...>\nExample: /heartbeat times 8:10,10:10,14:20,16:20\nCurrent: ${current || '(not set)'}`,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
const tokens = rest.split(',').map((s) => s.trim()).filter(Boolean);
|
|
719
|
+
const invalid = tokens.filter((t) => {
|
|
720
|
+
if (!/^\d{1,2}:\d{2}$/.test(t))
|
|
721
|
+
return true;
|
|
722
|
+
const colonIdx = t.indexOf(':');
|
|
723
|
+
const h = parseInt(t.slice(0, colonIdx), 10);
|
|
724
|
+
const m = parseInt(t.slice(colonIdx + 1), 10);
|
|
725
|
+
return h < 0 || h > 23 || m < 0 || m > 59;
|
|
726
|
+
});
|
|
727
|
+
if (invalid.length > 0) {
|
|
728
|
+
return { type: 'message', message: `Invalid time(s): ${invalid.join(', ')}. Use H:MM in 24h format (e.g., 8:10,14:20).` };
|
|
729
|
+
}
|
|
730
|
+
const value = tokens.join(',');
|
|
731
|
+
return {
|
|
732
|
+
type: 'notification',
|
|
733
|
+
notification: { type: 'heartbeat-set', key: 'HEARTBEAT_TIMES', value },
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
case 'daily': {
|
|
737
|
+
if (!rest) {
|
|
738
|
+
return {
|
|
739
|
+
type: 'message',
|
|
740
|
+
message: `Usage: /heartbeat daily <H:MM>\nExample: /heartbeat daily 6:00\nCurrent: ${ctx.settings.HEARTBEAT_DAILY ?? '6:00'}`,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
if (!/^\d{1,2}:\d{2}$/.test(rest)) {
|
|
744
|
+
return { type: 'message', message: `Invalid time: "${rest}". Use H:MM (e.g., 6:00, 14:30).` };
|
|
745
|
+
}
|
|
746
|
+
const [hStr, mStr] = rest.split(':');
|
|
747
|
+
const h = parseInt(hStr ?? '0', 10);
|
|
748
|
+
const m = parseInt(mStr ?? '0', 10);
|
|
749
|
+
if (isNaN(h) || isNaN(m) || h < 0 || h > 23 || m < 0 || m > 59) {
|
|
750
|
+
return { type: 'message', message: `Invalid time: "${rest}". Hour 0-23, minute 0-59.` };
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
type: 'notification',
|
|
754
|
+
notification: { type: 'heartbeat-set', key: 'HEARTBEAT_DAILY', value: rest },
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
case 'weekly': {
|
|
758
|
+
if (!rest) {
|
|
759
|
+
return {
|
|
760
|
+
type: 'message',
|
|
761
|
+
message: `Usage: /heartbeat weekly <day@H:MM>\nExample: /heartbeat weekly monday@6:00\nCurrent: ${ctx.settings.HEARTBEAT_WEEKLY ?? 'monday@6:00'}`,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
const atIdx = rest.indexOf('@');
|
|
765
|
+
if (atIdx < 0) {
|
|
766
|
+
return { type: 'message', message: `Invalid weekly schedule: "${rest}". Use day@H:MM (e.g., monday@6:00).` };
|
|
767
|
+
}
|
|
768
|
+
const day = rest.slice(0, atIdx).toLowerCase();
|
|
769
|
+
const validDays = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
|
|
770
|
+
if (!validDays.includes(day)) {
|
|
771
|
+
return { type: 'message', message: `Invalid day: "${day}". Use: ${validDays.join(', ')}.` };
|
|
772
|
+
}
|
|
773
|
+
if (!/^\d{1,2}:\d{2}$/.test(rest.slice(atIdx + 1))) {
|
|
774
|
+
return { type: 'message', message: `Invalid time: "${rest}". Use day@H:MM (e.g., monday@6:00).` };
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
type: 'notification',
|
|
778
|
+
notification: { type: 'heartbeat-set', key: 'HEARTBEAT_WEEKLY', value: rest },
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
case 'monthly': {
|
|
782
|
+
if (!rest) {
|
|
783
|
+
return {
|
|
784
|
+
type: 'message',
|
|
785
|
+
message: `Usage: /heartbeat monthly <D@H:MM>\nExample: /heartbeat monthly 1@6:00\nCurrent: ${ctx.settings.HEARTBEAT_MONTHLY ?? '1@6:00'}`,
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
const atIdx = rest.indexOf('@');
|
|
789
|
+
if (atIdx < 0) {
|
|
790
|
+
return { type: 'message', message: `Invalid monthly schedule: "${rest}". Use D@H:MM (e.g., 1@6:00).` };
|
|
791
|
+
}
|
|
792
|
+
const dayOfMonth = parseInt(rest.slice(0, atIdx), 10);
|
|
793
|
+
if (isNaN(dayOfMonth) || dayOfMonth < 1 || dayOfMonth > 31) {
|
|
794
|
+
return { type: 'message', message: `Invalid day: "${rest}". Day of month 1-31.` };
|
|
795
|
+
}
|
|
796
|
+
if (!/^\d{1,2}:\d{2}$/.test(rest.slice(atIdx + 1))) {
|
|
797
|
+
return { type: 'message', message: `Invalid time: "${rest}". Use D@H:MM (e.g., 1@6:00).` };
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
type: 'notification',
|
|
801
|
+
notification: { type: 'heartbeat-set', key: 'HEARTBEAT_MONTHLY', value: rest },
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
case 'now': {
|
|
805
|
+
return { type: 'notification', notification: { type: 'heartbeat-now' } };
|
|
806
|
+
}
|
|
807
|
+
default:
|
|
808
|
+
return {
|
|
809
|
+
type: 'message',
|
|
810
|
+
message: `Unknown heartbeat action: "${sub}". Use: status, enable, disable, times, daily, weekly, monthly, now`,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
//# sourceMappingURL=slash-commands.js.map
|