talon-agent 4.3.2 → 4.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/package.json +1 -1
  2. package/src/backend/claude-sdk/doctor.ts +1 -1
  3. package/src/backend/codex/doctor.ts +1 -1
  4. package/src/backend/openai-agents/doctor.ts +1 -1
  5. package/src/bootstrap.ts +1 -1
  6. package/src/cli/doctor.ts +1 -1
  7. package/src/core/agent-runtime/backend-registry.ts +1 -1
  8. package/src/core/auth/expiry-monitor.ts +1 -1
  9. package/src/core/{doctor.ts → doctor/index.ts} +11 -11
  10. package/src/core/{notify.ts → frontend-runtime/admin-notify.ts} +1 -1
  11. package/src/core/plugin/native-runtimes.ts +1 -1
  12. package/src/frontend/discord/commands/admin.ts +1 -1
  13. package/src/frontend/discord/render.ts +1 -1
  14. package/src/frontend/telegram/actions/index.ts +1 -1
  15. package/src/frontend/telegram/actions/moderation/chat.ts +63 -0
  16. package/src/frontend/telegram/actions/moderation/index.ts +74 -0
  17. package/src/frontend/telegram/actions/moderation/members.ts +77 -0
  18. package/src/frontend/telegram/actions/moderation/permissions.ts +69 -0
  19. package/src/frontend/telegram/actions/moderation/topics.ts +48 -0
  20. package/src/frontend/telegram/actions/moderation/types.ts +16 -0
  21. package/src/frontend/telegram/admin/background.ts +66 -0
  22. package/src/frontend/telegram/admin/chunked-reply.ts +20 -0
  23. package/src/frontend/telegram/admin/health.ts +81 -0
  24. package/src/frontend/telegram/admin/sessions.ts +105 -0
  25. package/src/frontend/telegram/admin.ts +56 -261
  26. package/src/frontend/telegram/callbacks/model/backend.ts +156 -0
  27. package/src/frontend/telegram/callbacks/model/control.ts +29 -0
  28. package/src/frontend/telegram/callbacks/model/nav.ts +54 -0
  29. package/src/frontend/telegram/callbacks/model/select.ts +100 -0
  30. package/src/frontend/telegram/callbacks/model/types.ts +39 -0
  31. package/src/frontend/telegram/callbacks/model/views.ts +116 -0
  32. package/src/frontend/telegram/callbacks/model.ts +61 -331
  33. package/src/frontend/telegram/commands/admin.ts +108 -77
  34. package/src/frontend/telegram/commands/settings.ts +65 -43
  35. package/src/frontend/telegram/commands/whatsapp-pairing.ts +1 -1
  36. package/src/frontend/telegram/helpers/diagnostics.ts +1 -1
  37. package/src/frontend/telegram/middleware.ts +166 -201
  38. package/src/frontend/terminal/builtins/context.ts +184 -0
  39. package/src/frontend/terminal/builtins/help.ts +35 -0
  40. package/src/frontend/terminal/builtins/index.ts +23 -0
  41. package/src/frontend/terminal/builtins/model.ts +198 -0
  42. package/src/frontend/terminal/builtins/session.ts +101 -0
  43. package/src/frontend/terminal/builtins/status.ts +202 -0
  44. package/src/frontend/terminal/command-registry.ts +80 -0
  45. package/src/frontend/terminal/commands.ts +11 -674
  46. package/src/frontend/terminal/input/history.ts +64 -0
  47. package/src/frontend/terminal/input/keys.ts +143 -0
  48. package/src/frontend/terminal/input/parts.ts +33 -0
  49. package/src/frontend/terminal/input/paste.ts +53 -0
  50. package/src/frontend/terminal/input/render.ts +42 -0
  51. package/src/frontend/terminal/input/state.ts +60 -0
  52. package/src/frontend/terminal/input.ts +27 -342
  53. package/src/frontend/whatsapp/connection.ts +1 -1
  54. package/src/frontend/whatsapp/index.ts +1 -1
  55. package/src/frontend/whatsapp/pairing-service.ts +1 -1
  56. package/src/plugins/github/provision.ts +1 -1
  57. package/src/plugins/mempalace/provision.ts +1 -1
  58. package/src/plugins/playwright/provision.ts +1 -1
  59. package/src/frontend/telegram/actions/moderation.ts +0 -255
  60. /package/src/core/{doctor-types.ts → doctor/types.ts} +0 -0
  61. /package/src/core/{pairing-broker.ts → frontend-runtime/pairing-broker.ts} +0 -0
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { Bot } from "grammy";
10
+ import type { TalonConfig } from "../../../util/config.js";
10
11
  import { respawnSelf } from "../../../util/respawn.js";
11
12
  import { isStaleCommand } from "../stale-command.js";
12
13
  import {
@@ -25,16 +26,13 @@ import {
25
26
  renderUsageMessage,
26
27
  } from "../helpers/index.js";
27
28
  import { collectPlanUsage } from "../../shared/plan-usage-report.js";
28
- import { collectDoctorReport } from "../../../core/doctor.js";
29
+ import { collectDoctorReport } from "../../../core/doctor/index.js";
29
30
  import { handleAdminCommand } from "../admin.js";
30
31
  import { getTodayMetrics } from "../../../storage/metrics.js";
31
32
  import { isAuthorizedAdmin, type RegisterDeps } from "./state.js";
32
33
  import { telegramCommandMenu } from "./definitions.js";
33
34
 
34
- export function registerAdminCommands(
35
- bot: Bot,
36
- { config }: RegisterDeps,
37
- ): void {
35
+ function registerAdminCommand(bot: Bot, config: TalonConfig): void {
38
36
  bot.command("admin", async (ctx) => {
39
37
  if (!isAuthorizedAdmin(ctx)) {
40
38
  await ctx.reply("Not authorized.");
@@ -42,7 +40,9 @@ export function registerAdminCommands(
42
40
  }
43
41
  await handleAdminCommand(ctx, bot, config);
44
42
  });
43
+ }
45
44
 
45
+ function registerMetricsCommand(bot: Bot): void {
46
46
  bot.command("metrics", async (ctx) => {
47
47
  if (!isAuthorizedAdmin(ctx)) {
48
48
  await ctx.reply("Not authorized.");
@@ -55,15 +55,19 @@ export function registerAdminCommands(
55
55
  reply_markup: { inline_keyboard: renderMetricsKeyboard("today") },
56
56
  });
57
57
  });
58
+ }
58
59
 
59
- // /usage — plan limits across every exposed backend, not just this
60
- // chat's. Not admin-gated: it says how close the shared account is to a
61
- // wall, which is exactly what a user hitting one needs to know.
60
+ // /usage — plan limits across every exposed backend, not just this
61
+ // chat's. Not admin-gated: it says how close the shared account is to a
62
+ // wall, which is exactly what a user hitting one needs to know.
63
+ function registerUsageCommand(bot: Bot, config: TalonConfig): void {
62
64
  bot.command("usage", async (ctx) => {
63
65
  const entries = await collectPlanUsage(config);
64
66
  await ctx.reply(renderUsageMessage(entries), { parse_mode: "HTML" });
65
67
  });
68
+ }
66
69
 
70
+ function registerDoctorCommand(bot: Bot, config: TalonConfig): void {
67
71
  bot.command("doctor", async (ctx) => {
68
72
  if (!isAuthorizedAdmin(ctx)) {
69
73
  await ctx.reply("Not authorized.");
@@ -90,7 +94,9 @@ export function registerAdminCommands(
90
94
  );
91
95
  }
92
96
  });
97
+ }
93
98
 
99
+ function registerDreamCommand(bot: Bot): void {
94
100
  bot.command("dream", async (ctx) => {
95
101
  if (!isAuthorizedAdmin(ctx)) {
96
102
  await ctx.reply("Not authorized.");
@@ -118,10 +124,12 @@ export function registerAdminCommands(
118
124
  );
119
125
  });
120
126
  });
127
+ }
121
128
 
122
- // /soul — introspect the compiled identity (read-only). `/soul dream`
123
- // (admin) runs the organic maintenance pass. Inert when the soul is
124
- // disabled (TALON_SOUL_ENABLED), so it's safe to ship dormant.
129
+ // /soul — introspect the compiled identity (read-only). `/soul dream`
130
+ // (admin) runs the organic maintenance pass. Inert when the soul is
131
+ // disabled (TALON_SOUL_ENABLED), so it's safe to ship dormant.
132
+ function registerSoulCommand(bot: Bot): void {
125
133
  bot.command("soul", async (ctx) => {
126
134
  const soul = getSoul();
127
135
  if (!soul.enabled) {
@@ -149,7 +157,9 @@ export function registerAdminCommands(
149
157
  }
150
158
  await ctx.reply(soul.introspect());
151
159
  });
160
+ }
152
161
 
162
+ function registerRestartCommand(bot: Bot): void {
153
163
  bot.command("restart", async (ctx) => {
154
164
  if (!isAuthorizedAdmin(ctx)) {
155
165
  await ctx.reply("Not authorized.");
@@ -162,77 +172,82 @@ export function registerAdminCommands(
162
172
  await ctx.reply("♻️ Restarting...");
163
173
  respawnSelf("telegram /restart");
164
174
  });
175
+ }
165
176
 
166
- // /update — pull latest, reinstall, run setup, restart. Only wired
167
- // up for developer builds running from a git checkout; packaged
168
- // binaries have no source tree (getRepoRoot() === null) so the
169
- // command stays absent entirely.
170
- const updateRepoRoot = config.devBuild ? getRepoRoot() : null;
171
- if (updateRepoRoot) {
172
- bot.command("update", async (ctx) => {
173
- if (!isAuthorizedAdmin(ctx)) {
174
- await ctx.reply("Not authorized.");
175
- return;
176
- }
177
- // Same redelivery hazard as /restart — it also ends the process.
178
- if (isStaleCommand(ctx.message?.date, "/update")) return;
179
- const remote = config.update?.remote ?? "origin";
180
- const branch = config.update?.branch ?? "main";
181
- const sent = await ctx.reply(
182
- `⏳ Updating from <code>${escapeHtml(remote)}/${escapeHtml(branch)}</code>…`,
183
- { parse_mode: "HTML" },
184
- );
185
- const edit = (text: string) =>
186
- bot.api
187
- .editMessageText(ctx.chat.id, sent.message_id, text, {
188
- parse_mode: "HTML",
189
- })
190
- .catch(() => {});
177
+ // /update — pull latest, reinstall, run setup, restart. Only wired
178
+ // up for developer builds running from a git checkout; packaged
179
+ // binaries have no source tree (getRepoRoot() === null) so the
180
+ // command stays absent entirely.
181
+ function registerUpdateCommand(
182
+ bot: Bot,
183
+ config: TalonConfig,
184
+ updateRepoRoot: string,
185
+ ): void {
186
+ bot.command("update", async (ctx) => {
187
+ if (!isAuthorizedAdmin(ctx)) {
188
+ await ctx.reply("Not authorized.");
189
+ return;
190
+ }
191
+ // Same redelivery hazard as /restart — it also ends the process.
192
+ if (isStaleCommand(ctx.message?.date, "/update")) return;
193
+ const remote = config.update?.remote ?? "origin";
194
+ const branch = config.update?.branch ?? "main";
195
+ const sent = await ctx.reply(
196
+ `⏳ Updating from <code>${escapeHtml(remote)}/${escapeHtml(branch)}</code>…`,
197
+ { parse_mode: "HTML" },
198
+ );
199
+ const edit = (text: string) =>
200
+ bot.api
201
+ .editMessageText(ctx.chat.id, sent.message_id, text, {
202
+ parse_mode: "HTML",
203
+ })
204
+ .catch(() => {});
191
205
 
192
- // Fire-and-forget so grammY keeps processing other updates.
193
- runSelfUpdate({
194
- remote,
195
- branch,
196
- setup: config.update?.setup,
197
- repoRoot: updateRepoRoot,
198
- })
199
- .then(async (res) => {
200
- if (!res.ok) {
201
- const tail = res.steps[res.steps.length - 1]?.output ?? "";
202
- await edit(
203
- `⚠️ Update failed: ${escapeHtml(res.error ?? "unknown error")}` +
204
- (tail ? `\n\n<pre>${escapeHtml(tail.slice(-1500))}</pre>` : ""),
205
- );
206
- return;
207
- }
208
- if (!res.changed) {
209
- await edit(
210
- `✅ Already up to date at <code>${escapeHtml(res.before ?? "?")}</code> — no restart needed.`,
211
- );
212
- return;
213
- }
206
+ // Fire-and-forget so grammY keeps processing other updates.
207
+ runSelfUpdate({
208
+ remote,
209
+ branch,
210
+ setup: config.update?.setup,
211
+ repoRoot: updateRepoRoot,
212
+ })
213
+ .then(async (res) => {
214
+ if (!res.ok) {
215
+ const tail = res.steps[res.steps.length - 1]?.output ?? "";
214
216
  await edit(
215
- `✅ Updated <code>${escapeHtml(res.before ?? "?")}</code> → <code>${escapeHtml(res.after ?? "?")}</code>. ♻️ Restarting…`,
217
+ `⚠️ Update failed: ${escapeHtml(res.error ?? "unknown error")}` +
218
+ (tail ? `\n\n<pre>${escapeHtml(tail.slice(-1500))}</pre>` : ""),
216
219
  );
217
- // The successor documents any provisioning changes (plugin
218
- // runtime upgrades, migrations) back to this chat once it's up.
219
- const { armProvisionReport } =
220
- await import("../../../core/plugin/provision-journal.js");
221
- armProvisionReport("telegram", String(ctx.chat.id));
222
- respawnSelf("telegram /update");
223
- })
224
- .catch(async (err: unknown) => {
225
- const msg = err instanceof Error ? err.message : String(err);
226
- await edit(`⚠️ Update crashed: ${escapeHtml(msg)}`);
227
- });
228
- });
229
- }
220
+ return;
221
+ }
222
+ if (!res.changed) {
223
+ await edit(
224
+ `✅ Already up to date at <code>${escapeHtml(res.before ?? "?")}</code> — no restart needed.`,
225
+ );
226
+ return;
227
+ }
228
+ await edit(
229
+ `✅ Updated <code>${escapeHtml(res.before ?? "?")}</code> → <code>${escapeHtml(res.after ?? "?")}</code>. ♻️ Restarting…`,
230
+ );
231
+ // The successor documents any provisioning changes (plugin
232
+ // runtime upgrades, migrations) back to this chat once it's up.
233
+ const { armProvisionReport } =
234
+ await import("../../../core/plugin/provision-journal.js");
235
+ armProvisionReport("telegram", String(ctx.chat.id));
236
+ respawnSelf("telegram /update");
237
+ })
238
+ .catch(async (err: unknown) => {
239
+ const msg = err instanceof Error ? err.message : String(err);
240
+ await edit(`⚠️ Update crashed: ${escapeHtml(msg)}`);
241
+ });
242
+ });
243
+ }
230
244
 
231
- // Unknown /command → "did you mean ...?" via the C similarity core
232
- // (native/strsim-wasm). Registered after every real command, so grammY
233
- // only reaches this when nothing above matched. Only bare commands
234
- // are intercepted — a close miss gets a suggestion, anything else
235
- // keeps flowing to the agent as a normal message.
245
+ // Unknown /command → "did you mean ...?" via the C similarity core
246
+ // (native/strsim-wasm). Registered after every real command, so grammY
247
+ // only reaches this when nothing above matched. Only bare commands
248
+ // are intercepted — a close miss gets a suggestion, anything else
249
+ // keeps flowing to the agent as a normal message.
250
+ function registerUnknownCommandSuggester(bot: Bot, config: TalonConfig): void {
236
251
  const commandNames = telegramCommandMenu(config).map((c) => c.command);
237
252
  bot.on("message::bot_command", async (ctx, next) => {
238
253
  const typed = /^\/([a-zA-Z0-9_]+)(?:@(\w+))?\s*$/.exec(ctx.msg.text ?? "");
@@ -250,3 +265,19 @@ export function registerAdminCommands(
250
265
  );
251
266
  });
252
267
  }
268
+
269
+ export function registerAdminCommands(
270
+ bot: Bot,
271
+ { config }: RegisterDeps,
272
+ ): void {
273
+ registerAdminCommand(bot, config);
274
+ registerMetricsCommand(bot);
275
+ registerUsageCommand(bot, config);
276
+ registerDoctorCommand(bot, config);
277
+ registerDreamCommand(bot);
278
+ registerSoulCommand(bot);
279
+ registerRestartCommand(bot);
280
+ const updateRepoRoot = config.devBuild ? getRepoRoot() : null;
281
+ if (updateRepoRoot) registerUpdateCommand(bot, config, updateRepoRoot);
282
+ registerUnknownCommandSuggester(bot, config);
283
+ }
@@ -2,7 +2,7 @@
2
2
  * Settings commands — /model, /effort, /pulse, /settings.
3
3
  */
4
4
 
5
- import type { Bot } from "grammy";
5
+ import type { Bot, Context } from "grammy";
6
6
  import {
7
7
  getChatSettings,
8
8
  setChatModelForBackend,
@@ -43,10 +43,56 @@ import {
43
43
  } from "../../shared/reasoning-levels.js";
44
44
  import type { RegisterDeps } from "./state.js";
45
45
 
46
- export function registerSettingsCommands(
47
- bot: Bot,
48
- { config, gateway }: RegisterDeps,
49
- ): void {
46
+ /** `/model <query>` — resolve the query against the per-chat backend and
47
+ * persist the pick. `be` + `beId` are the caller's (override-aware). */
48
+ async function replyModelSet(
49
+ ctx: Context,
50
+ arg: string,
51
+ cid: string,
52
+ be: ReturnType<typeof resolveBackendForChat>,
53
+ beId: string,
54
+ ): Promise<void> {
55
+ if (be?.models?.resolveModelInfo) {
56
+ const resolution = await be.models?.resolveModelInfo(arg);
57
+ if (resolution.kind !== "exact") {
58
+ // Every backend's formatModelError returns plain text and
59
+ // interpolates the raw query, so escape at this boundary — the
60
+ // same fix as the model menu's status lines. `/model <name>`
61
+ // (which the OpenCode/Kilo hint literally tells you to type)
62
+ // otherwise reaches Telegram as an unsupported `<name>` tag and
63
+ // the whole reply 400s, leaving the command looking dead.
64
+ const msg =
65
+ be.models?.formatModelError?.(arg, resolution) ??
66
+ `No model matched "${arg}".`;
67
+ await ctx.reply(escapeHtml(msg), { parse_mode: "HTML" });
68
+ return;
69
+ }
70
+ if (!resolution.model.selectable) {
71
+ const msg =
72
+ resolution.model.unavailableReason ??
73
+ `${resolution.model.providerName} is not connected.`;
74
+ await ctx.reply(escapeHtml(msg), { parse_mode: "HTML" });
75
+ return;
76
+ }
77
+ setChatModelForBackend(cid, beId, resolution.storedValue);
78
+ setChatBackend(cid, beId);
79
+ await ctx.reply(
80
+ `Model set to <code>${escapeHtml(resolution.storedValue)}</code> (${escapeHtml(resolution.model.providerName)}${resolution.model.free ? " · free" : ""}).`,
81
+ { parse_mode: "HTML" },
82
+ );
83
+ } else {
84
+ // Fallback for backends without model resolution
85
+ const model = resolveModelName(arg);
86
+ setChatModelForBackend(cid, beId, model);
87
+ setChatBackend(cid, beId);
88
+ await ctx.reply(
89
+ `Model set to <code>${escapeHtml(formatModelLabel(model))}</code>.`,
90
+ { parse_mode: "HTML" },
91
+ );
92
+ }
93
+ }
94
+
95
+ function registerModelCommand(bot: Bot, { config, gateway }: RegisterDeps) {
50
96
  bot.command("model", async (ctx) => {
51
97
  const cid = String(ctx.chat.id);
52
98
  const arg = ctx.match?.trim();
@@ -109,46 +155,11 @@ export function registerSettingsCommands(
109
155
 
110
156
  // `be` + `beId` already resolved above for the activeModel lookup.
111
157
  // Reuse them — they point at the per-chat backend (override-aware).
112
- if (be?.models?.resolveModelInfo) {
113
- const resolution = await be.models?.resolveModelInfo(arg);
114
- if (resolution.kind !== "exact") {
115
- // Every backend's formatModelError returns plain text and
116
- // interpolates the raw query, so escape at this boundary — the
117
- // same fix as the model menu's status lines. `/model <name>`
118
- // (which the OpenCode/Kilo hint literally tells you to type)
119
- // otherwise reaches Telegram as an unsupported `<name>` tag and
120
- // the whole reply 400s, leaving the command looking dead.
121
- const msg =
122
- be.models?.formatModelError?.(arg, resolution) ??
123
- `No model matched "${arg}".`;
124
- await ctx.reply(escapeHtml(msg), { parse_mode: "HTML" });
125
- return;
126
- }
127
- if (!resolution.model.selectable) {
128
- const msg =
129
- resolution.model.unavailableReason ??
130
- `${resolution.model.providerName} is not connected.`;
131
- await ctx.reply(escapeHtml(msg), { parse_mode: "HTML" });
132
- return;
133
- }
134
- setChatModelForBackend(cid, beId, resolution.storedValue);
135
- setChatBackend(cid, beId);
136
- await ctx.reply(
137
- `Model set to <code>${escapeHtml(resolution.storedValue)}</code> (${escapeHtml(resolution.model.providerName)}${resolution.model.free ? " · free" : ""}).`,
138
- { parse_mode: "HTML" },
139
- );
140
- } else {
141
- // Fallback for backends without model resolution
142
- const model = resolveModelName(arg);
143
- setChatModelForBackend(cid, beId, model);
144
- setChatBackend(cid, beId);
145
- await ctx.reply(
146
- `Model set to <code>${escapeHtml(formatModelLabel(model))}</code>.`,
147
- { parse_mode: "HTML" },
148
- );
149
- }
158
+ await replyModelSet(ctx, arg, cid, be, beId);
150
159
  });
160
+ }
151
161
 
162
+ function registerEffortCommand(bot: Bot, { config, gateway }: RegisterDeps) {
152
163
  bot.command("effort", async (ctx) => {
153
164
  const cid = String(ctx.chat.id);
154
165
  const arg = ctx.match?.trim().toLowerCase();
@@ -207,7 +218,9 @@ export function registerSettingsCommands(
207
218
  `Unknown level for this model. Valid: ${reasoning.levels.join(", ")}, or adaptive.`,
208
219
  );
209
220
  });
221
+ }
210
222
 
223
+ function registerPulseCommand(bot: Bot) {
211
224
  bot.command("pulse", async (ctx) => {
212
225
  const cid = String(ctx.chat.id);
213
226
  const arg = ctx.match?.trim().toLowerCase();
@@ -270,7 +283,9 @@ export function registerSettingsCommands(
270
283
 
271
284
  await ctx.reply("Use: /pulse on, /pulse off, /pulse 30m, /pulse 2h");
272
285
  });
286
+ }
273
287
 
288
+ function registerSettingsCommand(bot: Bot, { config, gateway }: RegisterDeps) {
274
289
  bot.command("settings", async (ctx) => {
275
290
  const cid = String(ctx.chat.id);
276
291
  const chatSets = getChatSettings(cid);
@@ -349,3 +364,10 @@ export function registerSettingsCommands(
349
364
  );
350
365
  });
351
366
  }
367
+
368
+ export function registerSettingsCommands(bot: Bot, deps: RegisterDeps): void {
369
+ registerModelCommand(bot, deps);
370
+ registerEffortCommand(bot, deps);
371
+ registerPulseCommand(bot);
372
+ registerSettingsCommand(bot, deps);
373
+ }
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { InputFile, type Bot, type Context } from "grammy";
14
- import { getPairingProvider } from "../../../core/pairing-broker.js";
14
+ import { getPairingProvider } from "../../../core/frontend-runtime/pairing-broker.js";
15
15
  import { log } from "../../../util/log.js";
16
16
  import { isAuthorizedAdmin } from "./state.js";
17
17
 
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { escapeHtml } from "../formatting.js";
6
- import type { DoctorReport } from "../../../core/doctor.js";
6
+ import type { DoctorReport } from "../../../core/doctor/index.js";
7
7
  import type { MeshPingResult } from "../../../core/mesh/service.js";
8
8
  import type { BackendUsageEntry } from "../../shared/plan-usage-report.js";
9
9
  import type { SettingsButton } from "./menu.js";