talon-agent 1.0.0 → 1.2.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 +21 -0
- package/README.md +1 -0
- package/package.json +15 -11
- package/prompts/dream.md +7 -3
- package/prompts/heartbeat.md +30 -0
- package/prompts/identity.md +1 -0
- package/prompts/teams.md +3 -0
- package/prompts/telegram.md +1 -0
- package/src/__tests__/chat-settings.test.ts +108 -2
- package/src/__tests__/cleanup-registry.test.ts +58 -0
- package/src/__tests__/config.test.ts +118 -52
- package/src/__tests__/cron-store-extended.test.ts +661 -0
- package/src/__tests__/cron-store.test.ts +145 -11
- package/src/__tests__/daily-log.test.ts +224 -13
- package/src/__tests__/dispatcher.test.ts +424 -23
- package/src/__tests__/dream.test.ts +1028 -0
- package/src/__tests__/errors-extended.test.ts +428 -0
- package/src/__tests__/errors.test.ts +95 -3
- package/src/__tests__/fuzz.test.ts +87 -15
- package/src/__tests__/gateway-actions.test.ts +1174 -433
- package/src/__tests__/gateway-http.test.ts +210 -19
- package/src/__tests__/gateway-retry.test.ts +359 -0
- package/src/__tests__/gateway-withRetry-extended.test.ts +343 -0
- package/src/__tests__/graph.test.ts +830 -0
- package/src/__tests__/handlers-stream.test.ts +208 -0
- package/src/__tests__/handlers.test.ts +2539 -70
- package/src/__tests__/heartbeat.test.ts +364 -0
- package/src/__tests__/history-extended.test.ts +775 -0
- package/src/__tests__/history-persistence.test.ts +74 -19
- package/src/__tests__/history.test.ts +113 -79
- package/src/__tests__/integration.test.ts +43 -8
- package/src/__tests__/log-init.test.ts +129 -0
- package/src/__tests__/log.test.ts +23 -5
- package/src/__tests__/media-index.test.ts +317 -35
- package/src/__tests__/plugin.test.ts +314 -0
- package/src/__tests__/prompt-builder-extended.test.ts +296 -0
- package/src/__tests__/prompt-builder.test.ts +44 -9
- package/src/__tests__/sessions.test.ts +258 -4
- package/src/__tests__/storage-save-errors.test.ts +342 -0
- package/src/__tests__/teams-frontend.test.ts +526 -31
- package/src/__tests__/telegram-formatting.test.ts +82 -0
- package/src/__tests__/terminal-commands.test.ts +208 -1
- package/src/__tests__/terminal-renderer.test.ts +223 -0
- package/src/__tests__/time.test.ts +107 -0
- package/src/__tests__/workspace-migrate.test.ts +256 -0
- package/src/__tests__/workspace.test.ts +63 -1
- package/src/backend/claude-sdk/tools.ts +64 -18
- package/src/bootstrap.ts +14 -14
- package/src/cli.ts +440 -125
- package/src/core/cron.ts +20 -5
- package/src/core/dispatcher.ts +27 -9
- package/src/core/dream.ts +79 -24
- package/src/core/errors.ts +12 -2
- package/src/core/gateway-actions.ts +182 -46
- package/src/core/gateway.ts +93 -41
- package/src/core/heartbeat.ts +515 -0
- package/src/core/plugin.ts +1 -1
- package/src/core/prompt-builder.ts +1 -4
- package/src/core/pulse.ts +4 -3
- package/src/frontend/teams/actions.ts +3 -1
- package/src/frontend/teams/formatting.ts +47 -8
- package/src/frontend/teams/graph.ts +35 -11
- package/src/frontend/teams/index.ts +155 -57
- package/src/frontend/teams/tools.ts +4 -6
- package/src/frontend/telegram/actions.ts +358 -82
- package/src/frontend/telegram/admin.ts +162 -72
- package/src/frontend/telegram/callbacks.ts +16 -10
- package/src/frontend/telegram/commands.ts +37 -21
- package/src/frontend/telegram/formatting.ts +2 -4
- package/src/frontend/telegram/handlers.ts +262 -66
- package/src/frontend/telegram/index.ts +39 -14
- package/src/frontend/telegram/middleware.ts +14 -4
- package/src/frontend/telegram/userbot.ts +16 -4
- package/src/frontend/terminal/renderer.ts +1 -4
- package/src/index.ts +28 -4
- package/src/storage/chat-settings.ts +32 -9
- package/src/storage/cron-store.ts +53 -11
- package/src/storage/daily-log.ts +72 -19
- package/src/storage/history.ts +39 -21
- package/src/storage/media-index.ts +37 -12
- package/src/storage/sessions.ts +3 -2
- package/src/util/cleanup-registry.ts +34 -0
- package/src/util/config.ts +85 -23
- package/src/util/log.ts +47 -17
- package/src/util/paths.ts +10 -0
- package/src/util/time.ts +29 -6
- package/src/util/watchdog.ts +5 -1
- package/src/util/workspace.ts +51 -10
|
@@ -7,13 +7,13 @@ import { readFileSync } from "node:fs";
|
|
|
7
7
|
import type { TalonConfig } from "../../util/config.js";
|
|
8
8
|
import { files, dirs } from "../../util/paths.js";
|
|
9
9
|
import { escapeHtml } from "./formatting.js";
|
|
10
|
-
import {
|
|
11
|
-
resetSession,
|
|
12
|
-
getAllSessions,
|
|
13
|
-
} from "../../storage/sessions.js";
|
|
10
|
+
import { resetSession, getAllSessions } from "../../storage/sessions.js";
|
|
14
11
|
import { clearHistory } from "../../storage/history.js";
|
|
15
12
|
import { getChatSettings } from "../../storage/chat-settings.js";
|
|
16
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
getAllCronJobs,
|
|
15
|
+
validateCronExpression,
|
|
16
|
+
} from "../../storage/cron-store.js";
|
|
17
17
|
import { getActiveCount } from "../../core/dispatcher.js";
|
|
18
18
|
import { getPulseStatus } from "../../core/pulse.js";
|
|
19
19
|
import { getHealthStatus, getRecentErrors } from "../../util/watchdog.js";
|
|
@@ -24,52 +24,90 @@ export async function handleAdminCommand(
|
|
|
24
24
|
bot: Bot,
|
|
25
25
|
config: TalonConfig,
|
|
26
26
|
): Promise<void> {
|
|
27
|
-
const args = (ctx.match as string ?? "").trim();
|
|
27
|
+
const args = ((ctx.match as string) ?? "").trim();
|
|
28
28
|
const [subcommand, ...rest] = args.split(/\s+/);
|
|
29
29
|
|
|
30
30
|
switch (subcommand) {
|
|
31
31
|
case "chats": {
|
|
32
32
|
const sessions = getAllSessions();
|
|
33
|
-
if (sessions.length === 0) {
|
|
34
|
-
|
|
33
|
+
if (sessions.length === 0) {
|
|
34
|
+
await ctx.reply("No active sessions.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
sessions.sort(
|
|
38
|
+
(a, b) => (b.info.lastActive || 0) - (a.info.lastActive || 0),
|
|
39
|
+
);
|
|
35
40
|
|
|
36
41
|
const titles = new Map<string, string>();
|
|
37
|
-
await Promise.all(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
await Promise.all(
|
|
43
|
+
sessions.map(async (s) => {
|
|
44
|
+
try {
|
|
45
|
+
const id = parseInt(s.chatId, 10);
|
|
46
|
+
if (isNaN(id)) return;
|
|
47
|
+
const chat = await bot.api.getChat(id);
|
|
48
|
+
titles.set(
|
|
49
|
+
s.chatId,
|
|
50
|
+
"title" in chat
|
|
51
|
+
? (chat.title ?? "DM")
|
|
52
|
+
: "first_name" in chat
|
|
53
|
+
? (chat.first_name ?? "DM")
|
|
54
|
+
: "DM",
|
|
55
|
+
);
|
|
56
|
+
} catch {
|
|
57
|
+
/* inaccessible */
|
|
58
|
+
}
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
45
61
|
|
|
46
62
|
const lines = sessions.map((s) => {
|
|
47
|
-
const age = s.info.lastActive
|
|
63
|
+
const age = s.info.lastActive
|
|
64
|
+
? `${Math.round((Date.now() - s.info.lastActive) / 60000)}m ago`
|
|
65
|
+
: "?";
|
|
48
66
|
const title = titles.get(s.chatId) ?? s.chatId;
|
|
49
|
-
const model = (getChatSettings(s.chatId).model ?? config.model).replace(
|
|
67
|
+
const model = (getChatSettings(s.chatId).model ?? config.model).replace(
|
|
68
|
+
"claude-",
|
|
69
|
+
"",
|
|
70
|
+
);
|
|
50
71
|
return `<b>${escapeHtml(title)}</b> <code>${s.chatId}</code>\n ${s.info.turns} turns | ${age} | ${model}`;
|
|
51
72
|
});
|
|
52
|
-
await ctx.reply(
|
|
73
|
+
await ctx.reply(
|
|
74
|
+
`<b>Active chats (${sessions.length})</b>\n\n` + lines.join("\n\n"),
|
|
75
|
+
{ parse_mode: "HTML" },
|
|
76
|
+
);
|
|
53
77
|
return;
|
|
54
78
|
}
|
|
55
79
|
|
|
56
80
|
case "broadcast": {
|
|
57
81
|
const text = rest.join(" ");
|
|
58
|
-
if (!text) {
|
|
82
|
+
if (!text) {
|
|
83
|
+
await ctx.reply("Usage: /admin broadcast <text>");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
59
86
|
const sessions = getAllSessions();
|
|
60
|
-
let sent = 0,
|
|
87
|
+
let sent = 0,
|
|
88
|
+
failed = 0;
|
|
61
89
|
for (const s of sessions) {
|
|
62
90
|
const id = parseInt(s.chatId, 10);
|
|
63
91
|
if (isNaN(id)) continue;
|
|
64
|
-
try {
|
|
92
|
+
try {
|
|
93
|
+
await bot.api.sendMessage(id, text);
|
|
94
|
+
sent++;
|
|
95
|
+
} catch {
|
|
96
|
+
failed++;
|
|
97
|
+
}
|
|
65
98
|
}
|
|
66
|
-
await ctx.reply(
|
|
99
|
+
await ctx.reply(
|
|
100
|
+
`Broadcast: ${sent} sent, ${failed} failed (${sessions.length} total).`,
|
|
101
|
+
);
|
|
67
102
|
return;
|
|
68
103
|
}
|
|
69
104
|
|
|
70
105
|
case "kill": {
|
|
71
106
|
const target = rest[0];
|
|
72
|
-
if (!target) {
|
|
107
|
+
if (!target) {
|
|
108
|
+
await ctx.reply("Usage: /admin kill <chatId>");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
73
111
|
resetSession(target);
|
|
74
112
|
clearHistory(target);
|
|
75
113
|
await ctx.reply(`Session ${target} reset.`);
|
|
@@ -79,16 +117,26 @@ export async function handleAdminCommand(
|
|
|
79
117
|
case "logs": {
|
|
80
118
|
const logPath = files.log;
|
|
81
119
|
try {
|
|
82
|
-
const { statSync, openSync, readSync, closeSync } =
|
|
120
|
+
const { statSync, openSync, readSync, closeSync } =
|
|
121
|
+
await import("node:fs");
|
|
83
122
|
const stat = statSync(logPath);
|
|
84
123
|
const size = Math.min(8192, stat.size);
|
|
85
124
|
const buf = Buffer.alloc(size);
|
|
86
125
|
const fd = openSync(logPath, "r");
|
|
87
126
|
readSync(fd, buf, 0, size, Math.max(0, stat.size - size));
|
|
88
127
|
closeSync(fd);
|
|
89
|
-
const lines = buf
|
|
90
|
-
|
|
91
|
-
|
|
128
|
+
const lines = buf
|
|
129
|
+
.toString("utf-8")
|
|
130
|
+
.trim()
|
|
131
|
+
.split("\n")
|
|
132
|
+
.slice(-20)
|
|
133
|
+
.join("\n");
|
|
134
|
+
await ctx.reply(`<pre>${escapeHtml(lines.slice(0, 3800))}</pre>`, {
|
|
135
|
+
parse_mode: "HTML",
|
|
136
|
+
});
|
|
137
|
+
} catch {
|
|
138
|
+
await ctx.reply(`Could not read ${logPath}`);
|
|
139
|
+
}
|
|
92
140
|
return;
|
|
93
141
|
}
|
|
94
142
|
|
|
@@ -97,56 +145,89 @@ export async function handleAdminCommand(
|
|
|
97
145
|
const sessions = getAllSessions();
|
|
98
146
|
const turns = sessions.reduce((s, x) => s + x.info.turns, 0);
|
|
99
147
|
const mem = process.memoryUsage();
|
|
100
|
-
await ctx.reply(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
148
|
+
await ctx.reply(
|
|
149
|
+
[
|
|
150
|
+
`<b>\uD83E\uDD85 Talon Stats</b>`,
|
|
151
|
+
"",
|
|
152
|
+
`<b>Uptime:</b> ${formatDuration(h.uptimeMs)}`,
|
|
153
|
+
`<b>Messages:</b> ${h.totalMessagesProcessed}`,
|
|
154
|
+
`<b>Sessions:</b> ${sessions.length}`,
|
|
155
|
+
`<b>Turns:</b> ${turns}`,
|
|
156
|
+
`<b>Last active:</b> ${h.msSinceLastMessage < 60000 ? "now" : formatDuration(h.msSinceLastMessage) + " ago"}`,
|
|
157
|
+
"",
|
|
158
|
+
`<b>Memory:</b> ${(mem.heapUsed / 1024 / 1024).toFixed(1)}MB heap / ${(mem.rss / 1024 / 1024).toFixed(1)}MB rss`,
|
|
159
|
+
`<b>Queue:</b> ${getActiveCount()}`,
|
|
160
|
+
`<b>Errors:</b> ${h.recentErrorCount}`,
|
|
161
|
+
].join("\n"),
|
|
162
|
+
{ parse_mode: "HTML" },
|
|
163
|
+
);
|
|
111
164
|
return;
|
|
112
165
|
}
|
|
113
166
|
|
|
114
167
|
case "errors": {
|
|
115
168
|
const errors = getRecentErrors(5);
|
|
116
|
-
if (errors.length === 0) {
|
|
117
|
-
|
|
118
|
-
|
|
169
|
+
if (errors.length === 0) {
|
|
170
|
+
await ctx.reply("No recent errors.");
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const lines = errors.map(
|
|
174
|
+
(e) =>
|
|
175
|
+
`<code>[${new Date(e.timestamp).toISOString().slice(11, 19)}]</code> ${escapeHtml(e.message.slice(0, 200))}`,
|
|
176
|
+
);
|
|
177
|
+
await ctx.reply(
|
|
178
|
+
`<b>Recent Errors (${errors.length})</b>\n\n` + lines.join("\n\n"),
|
|
179
|
+
{ parse_mode: "HTML" },
|
|
180
|
+
);
|
|
119
181
|
return;
|
|
120
182
|
}
|
|
121
183
|
|
|
122
184
|
case "cron": {
|
|
123
185
|
const jobs = getAllCronJobs();
|
|
124
|
-
if (jobs.length === 0) {
|
|
186
|
+
if (jobs.length === 0) {
|
|
187
|
+
await ctx.reply("No cron jobs.");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
125
190
|
const lines = jobs.map((j) => {
|
|
126
191
|
const v = validateCronExpression(j.schedule, j.timezone);
|
|
127
|
-
const last = j.lastRunAt
|
|
128
|
-
|
|
192
|
+
const last = j.lastRunAt
|
|
193
|
+
? new Date(j.lastRunAt).toISOString().slice(0, 16).replace("T", " ")
|
|
194
|
+
: "never";
|
|
195
|
+
const next = v.next
|
|
196
|
+
? new Date(v.next).toISOString().slice(0, 16).replace("T", " ")
|
|
197
|
+
: "?";
|
|
129
198
|
return `${j.enabled ? "\u2713" : "\u2717"} <b>${escapeHtml(j.name)}</b>\n <code>${j.schedule}</code> | ${j.type} | runs: ${j.runCount} | last: ${last} | next: ${next}`;
|
|
130
199
|
});
|
|
131
|
-
await ctx.reply(
|
|
200
|
+
await ctx.reply(
|
|
201
|
+
`<b>Cron Jobs (${jobs.length})</b>\n\n` + lines.join("\n\n"),
|
|
202
|
+
{ parse_mode: "HTML" },
|
|
203
|
+
);
|
|
132
204
|
return;
|
|
133
205
|
}
|
|
134
206
|
|
|
135
207
|
case "pulse": {
|
|
136
208
|
const chats = getPulseStatus();
|
|
137
|
-
if (chats.length === 0) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
209
|
+
if (chats.length === 0) {
|
|
210
|
+
await ctx.reply("No pulse chats.");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const lines = await Promise.all(
|
|
214
|
+
chats.map(async (p) => {
|
|
215
|
+
let title = p.chatId;
|
|
216
|
+
try {
|
|
217
|
+
const id = parseInt(p.chatId, 10);
|
|
218
|
+
if (!isNaN(id)) {
|
|
219
|
+
const chat = await bot.api.getChat(id);
|
|
220
|
+
title = "title" in chat ? (chat.title ?? p.chatId) : p.chatId;
|
|
221
|
+
}
|
|
222
|
+
} catch {
|
|
223
|
+
/* skip */
|
|
145
224
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
await ctx.reply(`<b>Pulse (${chats.length})</b>\n\n` + lines.join("\n"), {
|
|
225
|
+
return `${p.enabled ? "\u2713" : "\u2717"} ${escapeHtml(title)}`;
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
await ctx.reply(`<b>Pulse (${chats.length})</b>\n\n` + lines.join("\n"), {
|
|
229
|
+
parse_mode: "HTML",
|
|
230
|
+
});
|
|
150
231
|
return;
|
|
151
232
|
}
|
|
152
233
|
|
|
@@ -156,23 +237,32 @@ export async function handleAdminCommand(
|
|
|
156
237
|
try {
|
|
157
238
|
const content = readFileSync(logPath, "utf-8");
|
|
158
239
|
const lines = content.trim().split("\n").slice(-30).join("\n");
|
|
159
|
-
await ctx.reply(
|
|
160
|
-
|
|
240
|
+
await ctx.reply(
|
|
241
|
+
`<b>Daily log (${today})</b>\n\n<pre>${escapeHtml(lines.slice(0, 3800))}</pre>`,
|
|
242
|
+
{ parse_mode: "HTML" },
|
|
243
|
+
);
|
|
244
|
+
} catch {
|
|
245
|
+
await ctx.reply(`No daily log for ${today}.`);
|
|
246
|
+
}
|
|
161
247
|
return;
|
|
162
248
|
}
|
|
163
249
|
|
|
164
250
|
default:
|
|
165
|
-
await ctx.reply(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
251
|
+
await ctx.reply(
|
|
252
|
+
[
|
|
253
|
+
"<b>/admin commands</b>",
|
|
254
|
+
"",
|
|
255
|
+
" stats uptime, messages, memory",
|
|
256
|
+
" errors last 5 errors",
|
|
257
|
+
" chats list all active chats",
|
|
258
|
+
" daily today's interaction log",
|
|
259
|
+
" pulse pulse status per chat",
|
|
260
|
+
" cron list all cron jobs",
|
|
261
|
+
" broadcast <text> send to all chats",
|
|
262
|
+
" kill <chatId> reset a chat session",
|
|
263
|
+
" logs last 20 lines of log",
|
|
264
|
+
].join("\n"),
|
|
265
|
+
{ parse_mode: "HTML" },
|
|
266
|
+
);
|
|
177
267
|
}
|
|
178
268
|
}
|
|
@@ -21,10 +21,7 @@ import {
|
|
|
21
21
|
} from "../../core/pulse.js";
|
|
22
22
|
import { handleCallbackQuery } from "./handlers.js";
|
|
23
23
|
import { escapeHtml } from "./formatting.js";
|
|
24
|
-
import {
|
|
25
|
-
renderSettingsText,
|
|
26
|
-
renderSettingsKeyboard,
|
|
27
|
-
} from "./helpers.js";
|
|
24
|
+
import { renderSettingsText, renderSettingsKeyboard } from "./helpers.js";
|
|
28
25
|
|
|
29
26
|
export function registerCallbacks(bot: Bot, config: TalonConfig): void {
|
|
30
27
|
// ── Callback query handler ──────────────────────────────────────────────────
|
|
@@ -130,14 +127,21 @@ export function registerCallbacks(bot: Bot, config: TalonConfig): void {
|
|
|
130
127
|
{
|
|
131
128
|
parse_mode: "HTML",
|
|
132
129
|
reply_markup: {
|
|
133
|
-
inline_keyboard: [
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
130
|
+
inline_keyboard: [
|
|
131
|
+
[
|
|
132
|
+
{ text: enabled ? "✓ On" : "On", callback_data: "pulse:on" },
|
|
133
|
+
{
|
|
134
|
+
text: !enabled ? "✓ Off" : "Off",
|
|
135
|
+
callback_data: "pulse:off",
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
],
|
|
137
139
|
},
|
|
138
140
|
},
|
|
139
141
|
);
|
|
140
|
-
} catch {
|
|
142
|
+
} catch {
|
|
143
|
+
/* unchanged */
|
|
144
|
+
}
|
|
141
145
|
return;
|
|
142
146
|
}
|
|
143
147
|
|
|
@@ -220,7 +224,9 @@ export function registerCallbacks(bot: Bot, config: TalonConfig): void {
|
|
|
220
224
|
inline_keyboard: [
|
|
221
225
|
[
|
|
222
226
|
{
|
|
223
|
-
text: isModel("sonnet")
|
|
227
|
+
text: isModel("sonnet")
|
|
228
|
+
? "\u2713 Sonnet 4.6"
|
|
229
|
+
: "Sonnet 4.6",
|
|
224
230
|
callback_data: "model:sonnet",
|
|
225
231
|
},
|
|
226
232
|
{
|
|
@@ -218,10 +218,9 @@ export function registerCommands(bot: Bot, config: TalonConfig): void {
|
|
|
218
218
|
|
|
219
219
|
const model = resolveModelName(arg);
|
|
220
220
|
setChatModel(cid, model);
|
|
221
|
-
await ctx.reply(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
);
|
|
221
|
+
await ctx.reply(`Model set to <code>${escapeHtml(model)}</code>.`, {
|
|
222
|
+
parse_mode: "HTML",
|
|
223
|
+
});
|
|
225
224
|
});
|
|
226
225
|
|
|
227
226
|
bot.command("effort", async (ctx) => {
|
|
@@ -304,10 +303,15 @@ export function registerCommands(bot: Bot, config: TalonConfig): void {
|
|
|
304
303
|
{
|
|
305
304
|
parse_mode: "HTML",
|
|
306
305
|
reply_markup: {
|
|
307
|
-
inline_keyboard: [
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
306
|
+
inline_keyboard: [
|
|
307
|
+
[
|
|
308
|
+
{ text: enabled ? "✓ On" : "On", callback_data: "pulse:on" },
|
|
309
|
+
{
|
|
310
|
+
text: !enabled ? "✓ Off" : "Off",
|
|
311
|
+
callback_data: "pulse:off",
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
],
|
|
311
315
|
},
|
|
312
316
|
},
|
|
313
317
|
);
|
|
@@ -344,16 +348,16 @@ export function registerCommands(bot: Bot, config: TalonConfig): void {
|
|
|
344
348
|
return;
|
|
345
349
|
}
|
|
346
350
|
|
|
347
|
-
await ctx.reply(
|
|
348
|
-
"Use: /pulse on, /pulse off, /pulse 30m, /pulse 2h",
|
|
349
|
-
);
|
|
351
|
+
await ctx.reply("Use: /pulse on, /pulse off, /pulse 30m, /pulse 2h");
|
|
350
352
|
});
|
|
351
353
|
|
|
352
354
|
bot.command("memory", async (ctx) => {
|
|
353
355
|
try {
|
|
354
356
|
const memoryPath = files.memory;
|
|
355
357
|
if (!existsSync(memoryPath)) {
|
|
356
|
-
await ctx.reply(
|
|
358
|
+
await ctx.reply(
|
|
359
|
+
"No memory file yet. I'll create one as I learn about you.",
|
|
360
|
+
);
|
|
357
361
|
return;
|
|
358
362
|
}
|
|
359
363
|
const content = readFileSync(memoryPath, "utf-8").trim();
|
|
@@ -362,9 +366,10 @@ export function registerCommands(bot: Bot, config: TalonConfig): void {
|
|
|
362
366
|
return;
|
|
363
367
|
}
|
|
364
368
|
// Truncate for Telegram's 4096 char limit
|
|
365
|
-
const display =
|
|
366
|
-
|
|
367
|
-
|
|
369
|
+
const display =
|
|
370
|
+
content.length > 3500
|
|
371
|
+
? content.slice(0, 3500) + "\n\n... (truncated)"
|
|
372
|
+
: content;
|
|
368
373
|
await ctx.reply(display);
|
|
369
374
|
} catch {
|
|
370
375
|
await ctx.reply("Could not read memory file.");
|
|
@@ -427,10 +432,12 @@ export function registerCommands(bot: Bot, config: TalonConfig): void {
|
|
|
427
432
|
: 0;
|
|
428
433
|
const barLen = 20;
|
|
429
434
|
const filled = Math.round((contextPct / 100) * barLen);
|
|
430
|
-
const contextBar =
|
|
435
|
+
const contextBar =
|
|
436
|
+
"\u2588".repeat(filled) + "\u2591".repeat(barLen - filled);
|
|
431
437
|
const contextWarn = contextPct >= 80 ? " \u26A0\uFE0F consider /reset" : "";
|
|
432
438
|
|
|
433
|
-
const totalPrompt =
|
|
439
|
+
const totalPrompt =
|
|
440
|
+
u.totalInputTokens + u.totalCacheRead + u.totalCacheWrite;
|
|
434
441
|
const cacheHitPct =
|
|
435
442
|
totalPrompt > 0 ? Math.round((u.totalCacheRead / totalPrompt) * 100) : 0;
|
|
436
443
|
|
|
@@ -439,7 +446,8 @@ export function registerCommands(bot: Bot, config: TalonConfig): void {
|
|
|
439
446
|
? Math.round(u.totalResponseMs / info.turns)
|
|
440
447
|
: 0;
|
|
441
448
|
const lastResponseMs = u.lastResponseMs || 0;
|
|
442
|
-
const fastestMs =
|
|
449
|
+
const fastestMs =
|
|
450
|
+
u.fastestResponseMs === Infinity ? 0 : u.fastestResponseMs || 0;
|
|
443
451
|
|
|
444
452
|
const diskBytes = getWorkspaceDiskUsage(config.workspace);
|
|
445
453
|
const diskStr = formatBytes(diskBytes);
|
|
@@ -504,14 +512,20 @@ export function registerCommands(bot: Bot, config: TalonConfig): void {
|
|
|
504
512
|
setTimeout(() => {
|
|
505
513
|
// Try `talon restart` (handles daemon stop+start cleanly).
|
|
506
514
|
// Fall back to the local bin if talon isn't on PATH globally.
|
|
507
|
-
const projectRoot = resolve(
|
|
515
|
+
const projectRoot = resolve(
|
|
516
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
517
|
+
"../../../..",
|
|
518
|
+
);
|
|
508
519
|
const localBin = resolve(projectRoot, "bin/talon.js");
|
|
509
520
|
|
|
510
521
|
const trySpawn = (cmd: string, args: string[]): Promise<void> =>
|
|
511
522
|
new Promise((res, rej) => {
|
|
512
523
|
const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
|
|
513
524
|
child.on("error", rej);
|
|
514
|
-
child.on("spawn", () => {
|
|
525
|
+
child.on("spawn", () => {
|
|
526
|
+
child.unref();
|
|
527
|
+
res();
|
|
528
|
+
});
|
|
515
529
|
});
|
|
516
530
|
|
|
517
531
|
// Try global first, then local bin, then just exit (let process manager restart)
|
|
@@ -532,7 +546,9 @@ export function registerCommands(bot: Bot, config: TalonConfig): void {
|
|
|
532
546
|
const ver = p.plugin.version ? ` v${p.plugin.version}` : "";
|
|
533
547
|
const desc = p.plugin.description ? ` — ${p.plugin.description}` : "";
|
|
534
548
|
const mcp = p.plugin.mcpServerPath ? " [MCP]" : "";
|
|
535
|
-
const fe = p.plugin.frontends?.length
|
|
549
|
+
const fe = p.plugin.frontends?.length
|
|
550
|
+
? ` (${p.plugin.frontends.join(", ")})`
|
|
551
|
+
: "";
|
|
536
552
|
return `• <b>${escapeHtml(p.plugin.name)}</b>${ver}${mcp}${fe}${desc}`;
|
|
537
553
|
});
|
|
538
554
|
await ctx.reply(
|
|
@@ -79,10 +79,8 @@ export function markdownToTelegramHtml(text: string): string {
|
|
|
79
79
|
// Italic: _text_ (surrounded by non-word or start/end)
|
|
80
80
|
processed = processed.replace(/(?<!\w)_(.+?)_(?!\w)/g, "<i>$1</i>");
|
|
81
81
|
// Links: [text](url) — only allow safe URL schemes
|
|
82
|
-
processed = processed.replace(
|
|
83
|
-
|
|
84
|
-
(_, text, url) =>
|
|
85
|
-
/^https?:\/\//i.test(url) ? `<a href="${url}">${text}</a>` : text,
|
|
82
|
+
processed = processed.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, text, url) =>
|
|
83
|
+
/^https?:\/\//i.test(url) ? `<a href="${url}">${text}</a>` : text,
|
|
86
84
|
);
|
|
87
85
|
// Strikethrough: ~~text~~
|
|
88
86
|
processed = processed.replace(/~~(.+?)~~/g, "<s>$1</s>");
|