@songsid/agend 2.0.11-beta.30 β†’ 2.0.11-beta.31

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.
@@ -37,6 +37,7 @@ import { outboundHandlers } from "./outbound-handlers.js";
37
37
  import { handleWebRequest, broadcastSseEvent } from "./web-api.js";
38
38
  import { handleViewRequest, isViewPath } from "./view-api.js";
39
39
  import { handleSettingsRequest } from "./settings-api.js";
40
+ import { setLocale, detectLocale, t } from "./locale.js";
40
41
  import { handleAgentRequest } from "./agent-endpoint.js";
41
42
  import { ClassicChannelManager } from "./classic-channel-manager.js";
42
43
  import { getTmuxSession } from "./config.js";
@@ -208,6 +209,15 @@ export class FleetManager {
208
209
  getInstanceDir(name) {
209
210
  return join(this.dataDir, "instances", name);
210
211
  }
212
+ /** AgEnD package version (for the Settings "current version" / What's New). */
213
+ get currentVersion() {
214
+ try {
215
+ return JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8")).version ?? "unknown";
216
+ }
217
+ catch {
218
+ return "unknown";
219
+ }
220
+ }
211
221
  /**
212
222
  * Resolve a slash-command target in a channel. Classic channels are looked up
213
223
  * per-bot (same-channel multi-bot); a fleet-topic instance is found via the
@@ -388,6 +398,7 @@ export class FleetManager {
388
398
  const { rotateLogIfNeeded } = await import("./logger.js");
389
399
  rotateLogIfNeeded(join(this.dataDir, "fleet.log"));
390
400
  const fleet = this.loadConfig(configPath);
401
+ setLocale(detectLocale(fleet)); // user-facing text language (fleet.yaml defaults.locale / timezone)
391
402
  const topicMode = fleet.channel?.mode === "topic" || !!fleet.channels?.some(ch => ch.mode === "topic");
392
403
  // Set tmux socket isolation for custom AGEND_HOME
393
404
  const { getTmuxSocketName: getSocket } = await import("./paths.js");
@@ -905,12 +916,12 @@ export class FleetManager {
905
916
  else if (data.command === "chat") {
906
917
  const text = data.text ?? "";
907
918
  if (!text) {
908
- await data.respond("Usage: `/chat <message>`");
919
+ await data.respond(t("chat.usage"));
909
920
  return;
910
921
  }
911
922
  const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
912
923
  if (!name) {
913
- await data.respond("No active agent in this channel. Use `/start` first.");
924
+ await data.respond(t("classic.no_agent_start"));
914
925
  return;
915
926
  }
916
927
  const replyMsgId = await data.respond("πŸ‘€");
@@ -932,26 +943,26 @@ export class FleetManager {
932
943
  else if (data.command === "load") {
933
944
  // load is kiro-cli/classic only β€” no claude-code equivalent.
934
945
  if (!this.classicChannels?.isAdmin(data.userId)) {
935
- await data.respond("β›” This command requires admin access.");
946
+ await data.respond(t("admin.required"));
936
947
  return;
937
948
  }
938
949
  const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
939
950
  if (!name) {
940
- await data.respond("No active agent in this channel. Use `/start` first.");
951
+ await data.respond(t("classic.no_agent_start"));
941
952
  return;
942
953
  }
943
954
  const filename = data.options?.filename;
944
955
  if (!SAVE_FILENAME_RE.test(filename ?? "")) {
945
- await data.respond("β›” Invalid filename β€” only letters, numbers, dots, hyphens, underscores allowed.");
956
+ await data.respond(t("filename.invalid"));
946
957
  return;
947
958
  }
948
959
  this.pasteRawToClassicInstance(name, `/chat load ${filename}`);
949
- await data.respond(`βœ… Sent \`/chat load ${filename}\` to ${name}`);
960
+ await data.respond(t("save.sent", `/chat load ${filename}`, name));
950
961
  }
951
962
  else if (data.command === "compact") {
952
963
  const name = this.resolveSlashTarget(data.channelId, adapterId);
953
964
  if (!name) {
954
- await data.respond("No active agent in this channel.");
965
+ await data.respond(t("classic.no_agent"));
955
966
  return;
956
967
  }
957
968
  const result = await this.topicCommands.sendCompact(name);
@@ -960,16 +971,16 @@ export class FleetManager {
960
971
  else if (data.command === "cancel") {
961
972
  const name = this.resolveSlashTarget(data.channelId, adapterId);
962
973
  if (!name) {
963
- await data.respond("No active agent in this channel.");
974
+ await data.respond(t("classic.no_agent"));
964
975
  return;
965
976
  }
966
977
  const ok = this.cancelInstance(name);
967
- await data.respond(ok ? `πŸ›‘ Sent cancel to ${name}.` : `❌ ${name} not running.`);
978
+ await data.respond(ok ? t("cancel.sent", name) : t("cancel.not_running", name));
968
979
  }
969
980
  else if (data.command === "ctx") {
970
981
  const name = this.resolveSlashTarget(data.channelId, adapterId);
971
982
  if (!name) {
972
- await data.respond("No active agent in this channel.");
983
+ await data.respond(t("classic.no_agent"));
973
984
  return;
974
985
  }
975
986
  // Single source of truth (statusline.json + robust tmux pane fallback).
@@ -982,33 +993,33 @@ export class FleetManager {
982
993
  if (collabTarget) {
983
994
  const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
984
995
  if (allowed.length > 0 && !allowed.some(u => String(u) === String(data.userId))) {
985
- await data.respond("β›” Not authorized");
996
+ await data.respond(t("not_authorized"));
986
997
  return;
987
998
  }
988
999
  const isCollab = this.toggleFleetCollab(collabTarget.name);
989
- await data.respond(isCollab ? "🀝 Collaboration mode **ON** β€” bot/webhook messages reach the agent." : "πŸ’¬ Collaboration mode **OFF**");
1000
+ await data.respond(isCollab ? t("collab.on") : t("collab.off"));
990
1001
  return;
991
1002
  }
992
1003
  if (!this.classicChannels?.isAdmin(data.userId)) {
993
- await data.respond("β›” This command requires admin access.");
1004
+ await data.respond(t("admin.required"));
994
1005
  return;
995
1006
  }
996
1007
  if (!this.classicChannels.isClassicChannel(data.channelId, adapterId)) {
997
- await data.respond("No active agent in this channel. Use `/start` first.");
1008
+ await data.respond(t("classic.no_agent_start"));
998
1009
  return;
999
1010
  }
1000
1011
  const newState = this.classicChannels.toggleCollab(data.channelId, adapterId);
1001
1012
  await data.respond(newState
1002
- ? "🀝 Collaboration mode **ON** β€” @mention this bot to trigger the agent. Other bot messages are visible."
1003
- : "πŸ’¬ Collaboration mode **OFF** β€” use `/chat` to talk to the agent.");
1013
+ ? t("collab.on.classic")
1014
+ : t("collab.off.classic"));
1004
1015
  }
1005
1016
  else if (data.command === "update") {
1006
1017
  const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
1007
1018
  if (allowed.length > 0 && !allowed.some(u => String(u) === String(data.userId))) {
1008
- await data.respond("β›” Not authorized");
1019
+ await data.respond(t("not_authorized"));
1009
1020
  return;
1010
1021
  }
1011
- await data.respond("πŸ“¦ Updating AgEnD... Fleet will restart automatically.");
1022
+ await data.respond(t("update.running"));
1012
1023
  const { spawn } = await import("node:child_process");
1013
1024
  const _cv = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8")).version ?? "";
1014
1025
  const _cmd = _cv.includes("beta") ? "agend update --beta" : "agend update";
@@ -1018,7 +1029,7 @@ export class FleetManager {
1018
1029
  else if (data.command === "doctor") {
1019
1030
  const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
1020
1031
  if (allowed.length > 0 && !allowed.some(u => String(u) === String(data.userId))) {
1021
- await data.respond("β›” Not authorized");
1032
+ await data.respond(t("not_authorized"));
1022
1033
  return;
1023
1034
  }
1024
1035
  try {
@@ -1045,11 +1056,11 @@ export class FleetManager {
1045
1056
  // the web-token-bearing URLs are only visible to the caller.
1046
1057
  const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
1047
1058
  if (allowed.length === 0) {
1048
- await data.respond("β›” /dashboard disabled β€” no allowed_users configured");
1059
+ await data.respond(t("dashboard.disabled"));
1049
1060
  return;
1050
1061
  }
1051
1062
  if (!allowed.some(u => String(u) === String(data.userId))) {
1052
- await data.respond("β›” Not authorized");
1063
+ await data.respond(t("not_authorized"));
1053
1064
  return;
1054
1065
  }
1055
1066
  await data.respond(this.topicCommands.getDashboardText());
@@ -1057,16 +1068,16 @@ export class FleetManager {
1057
1068
  else if (data.command === "restart") {
1058
1069
  const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
1059
1070
  if (allowed.length > 0 && !allowed.some(u => String(u) === String(data.userId))) {
1060
- await data.respond("β›” Not authorized");
1071
+ await data.respond(t("not_authorized"));
1061
1072
  return;
1062
1073
  }
1063
- await data.respond("πŸ”„ Graceful restart β€” waiting for instances to idle...");
1074
+ await data.respond(t("restart.graceful"));
1064
1075
  process.kill(process.pid, "SIGUSR2");
1065
1076
  }
1066
1077
  else if (data.command === "compact") {
1067
1078
  const name = this.resolveSlashTarget(data.channelId, adapterId);
1068
1079
  if (!name) {
1069
- await data.respond("No active agent in this channel.");
1080
+ await data.respond(t("classic.no_agent"));
1070
1081
  return;
1071
1082
  }
1072
1083
  const result = await this.topicCommands.sendCompact(name);
@@ -1184,12 +1195,12 @@ export class FleetManager {
1184
1195
  else if (data.command === "chat") {
1185
1196
  const text = data.text ?? "";
1186
1197
  if (!text) {
1187
- await data.respond("Usage: `/chat <message>`");
1198
+ await data.respond(t("chat.usage"));
1188
1199
  return;
1189
1200
  }
1190
1201
  const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
1191
1202
  if (!name) {
1192
- await data.respond("No active agent in this channel. Use `/start` first.");
1203
+ await data.respond(t("classic.no_agent_start"));
1193
1204
  return;
1194
1205
  }
1195
1206
  const replyMsgId = await data.respond("πŸ‘€");
@@ -1211,35 +1222,35 @@ export class FleetManager {
1211
1222
  else if (data.command === "load") {
1212
1223
  // load is kiro-cli/classic only β€” no claude-code equivalent.
1213
1224
  if (!this.classicChannels?.isAdmin(data.userId)) {
1214
- await data.respond("β›” This command requires admin access.");
1225
+ await data.respond(t("admin.required"));
1215
1226
  return;
1216
1227
  }
1217
1228
  const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
1218
1229
  if (!name) {
1219
- await data.respond("No active agent in this channel. Use `/start` first.");
1230
+ await data.respond(t("classic.no_agent_start"));
1220
1231
  return;
1221
1232
  }
1222
1233
  const filename = data.options?.filename;
1223
1234
  if (!SAVE_FILENAME_RE.test(filename ?? "")) {
1224
- await data.respond("β›” Invalid filename β€” only letters, numbers, dots, hyphens, underscores allowed.");
1235
+ await data.respond(t("filename.invalid"));
1225
1236
  return;
1226
1237
  }
1227
1238
  this.pasteRawToClassicInstance(name, `/chat load ${filename}`);
1228
- await data.respond(`βœ… Sent \`/chat load ${filename}\` to ${name}`);
1239
+ await data.respond(t("save.sent", `/chat load ${filename}`, name));
1229
1240
  }
1230
1241
  else if (data.command === "cancel") {
1231
1242
  const name = this.resolveSlashTarget(data.channelId, adapterId);
1232
1243
  if (!name) {
1233
- await data.respond("No active agent in this channel.");
1244
+ await data.respond(t("classic.no_agent"));
1234
1245
  return;
1235
1246
  }
1236
1247
  const ok = this.cancelInstance(name);
1237
- await data.respond(ok ? `πŸ›‘ Sent cancel to ${name}.` : `❌ ${name} not running.`);
1248
+ await data.respond(ok ? t("cancel.sent", name) : t("cancel.not_running", name));
1238
1249
  }
1239
1250
  else if (data.command === "ctx") {
1240
1251
  const name = this.resolveSlashTarget(data.channelId, adapterId);
1241
1252
  if (!name) {
1242
- await data.respond("No active agent in this channel.");
1253
+ await data.respond(t("classic.no_agent"));
1243
1254
  return;
1244
1255
  }
1245
1256
  // Single source of truth (statusline.json + robust tmux pane fallback).
@@ -1252,33 +1263,33 @@ export class FleetManager {
1252
1263
  if (collabTarget2) {
1253
1264
  const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
1254
1265
  if (allowed.length > 0 && !allowed.some(u => String(u) === String(data.userId))) {
1255
- await data.respond("β›” Not authorized");
1266
+ await data.respond(t("not_authorized"));
1256
1267
  return;
1257
1268
  }
1258
1269
  const isCollab = this.toggleFleetCollab(collabTarget2.name);
1259
- await data.respond(isCollab ? "🀝 Collaboration mode **ON** β€” bot/webhook messages reach the agent." : "πŸ’¬ Collaboration mode **OFF**");
1270
+ await data.respond(isCollab ? t("collab.on") : t("collab.off"));
1260
1271
  return;
1261
1272
  }
1262
1273
  if (!this.classicChannels?.isAdmin(data.userId)) {
1263
- await data.respond("β›” This command requires admin access.");
1274
+ await data.respond(t("admin.required"));
1264
1275
  return;
1265
1276
  }
1266
1277
  if (!this.classicChannels.isClassicChannel(data.channelId, adapterId)) {
1267
- await data.respond("No active agent in this channel. Use `/start` first.");
1278
+ await data.respond(t("classic.no_agent_start"));
1268
1279
  return;
1269
1280
  }
1270
1281
  const newState = this.classicChannels.toggleCollab(data.channelId, adapterId);
1271
1282
  await data.respond(newState
1272
- ? "🀝 Collaboration mode **ON** β€” @mention this bot to trigger the agent. Other bot messages are visible."
1273
- : "πŸ’¬ Collaboration mode **OFF** β€” use `/chat` to talk to the agent.");
1283
+ ? t("collab.on.classic")
1284
+ : t("collab.off.classic"));
1274
1285
  }
1275
1286
  else if (data.command === "update") {
1276
1287
  const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
1277
1288
  if (allowed.length > 0 && !allowed.some(u => String(u) === String(data.userId))) {
1278
- await data.respond("β›” Not authorized");
1289
+ await data.respond(t("not_authorized"));
1279
1290
  return;
1280
1291
  }
1281
- await data.respond("πŸ“¦ Updating AgEnD... Fleet will restart automatically.");
1292
+ await data.respond(t("update.running"));
1282
1293
  const { spawn } = await import("node:child_process");
1283
1294
  const _cv = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf-8")).version ?? "";
1284
1295
  const _cmd = _cv.includes("beta") ? "agend update --beta" : "agend update";
@@ -1288,7 +1299,7 @@ export class FleetManager {
1288
1299
  else if (data.command === "doctor") {
1289
1300
  const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
1290
1301
  if (allowed.length > 0 && !allowed.some(u => String(u) === String(data.userId))) {
1291
- await data.respond("β›” Not authorized");
1302
+ await data.respond(t("not_authorized"));
1292
1303
  return;
1293
1304
  }
1294
1305
  try {
@@ -1315,11 +1326,11 @@ export class FleetManager {
1315
1326
  // the web-token-bearing URLs are only visible to the caller.
1316
1327
  const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
1317
1328
  if (allowed.length === 0) {
1318
- await data.respond("β›” /dashboard disabled β€” no allowed_users configured");
1329
+ await data.respond(t("dashboard.disabled"));
1319
1330
  return;
1320
1331
  }
1321
1332
  if (!allowed.some(u => String(u) === String(data.userId))) {
1322
- await data.respond("β›” Not authorized");
1333
+ await data.respond(t("not_authorized"));
1323
1334
  return;
1324
1335
  }
1325
1336
  await data.respond(this.topicCommands.getDashboardText());
@@ -1327,16 +1338,16 @@ export class FleetManager {
1327
1338
  else if (data.command === "restart") {
1328
1339
  const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
1329
1340
  if (allowed.length > 0 && !allowed.some(u => String(u) === String(data.userId))) {
1330
- await data.respond("β›” Not authorized");
1341
+ await data.respond(t("not_authorized"));
1331
1342
  return;
1332
1343
  }
1333
- await data.respond("πŸ”„ Graceful restart β€” waiting for instances to idle...");
1344
+ await data.respond(t("restart.graceful"));
1334
1345
  process.kill(process.pid, "SIGUSR2");
1335
1346
  }
1336
1347
  else if (data.command === "compact") {
1337
1348
  const name = this.resolveSlashTarget(data.channelId, adapterId);
1338
1349
  if (!name) {
1339
- await data.respond("No active agent in this channel.");
1350
+ await data.respond(t("classic.no_agent"));
1340
1351
  return;
1341
1352
  }
1342
1353
  const result = await this.topicCommands.sendCompact(name);
@@ -1677,7 +1688,7 @@ export class FleetManager {
1677
1688
  if (generalId) {
1678
1689
  this.notifyInstanceTopic(generalId, `πŸ†• Unauthorized user tried /start in private chat:\nβ€’ Name: ${msg.username}\nβ€’ ID: ${msg.userId}\nβ€’ Platform: ${msg.source}\n\nTo allow: add \`${msg.userId}\` to classicBot.yaml \`allowed_users\``);
1679
1690
  }
1680
- await msgAdapter?.sendText(chatId, "β›” You are not in the allowed users list.");
1691
+ await msgAdapter?.sendText(chatId, t("classic.not_allowed_user"));
1681
1692
  return;
1682
1693
  }
1683
1694
  }
@@ -1690,11 +1701,11 @@ export class FleetManager {
1690
1701
  if (generalId) {
1691
1702
  this.notifyInstanceTopic(generalId, adminMsg);
1692
1703
  }
1693
- await msgAdapter?.sendText(chatId, "⏳ Access requested. Waiting for admin approval.");
1704
+ await msgAdapter?.sendText(chatId, t("classic.access_requested"));
1694
1705
  return;
1695
1706
  }
1696
1707
  if (!this.classicChannels.isAdmin(msg.userId)) {
1697
- await msgAdapter?.sendText(chatId, "β›” Only admins can start agents. Ask an admin to /start.");
1708
+ await msgAdapter?.sendText(chatId, t("classic.admin_only_start"));
1698
1709
  const generalId = this.findGeneralInstance(msg.adapterId);
1699
1710
  if (generalId) {
1700
1711
  this.notifyInstanceTopic(generalId, `πŸ”‘ User wants to /start but is not admin:\nβ€’ Name: ${msg.username}\nβ€’ ID: ${msg.userId}\nβ€’ Platform: ${msg.source}\nβ€’ Group: ${chatId}\n\nTo approve: add \`${msg.userId}\` to classicBot.yaml \`admin_users\``);
@@ -1711,7 +1722,7 @@ export class FleetManager {
1711
1722
  // Handle /stop command
1712
1723
  if (text === "/stop" || text.startsWith("/stop ")) {
1713
1724
  if (!this.classicChannels.isAdmin(msg.userId)) {
1714
- await msgAdapter?.sendText(chatId, "β›” Only admins can stop agents.");
1725
+ await msgAdapter?.sendText(chatId, t("classic.admin_only_stop"));
1715
1726
  const generalId = this.findGeneralInstance(msg.adapterId);
1716
1727
  if (generalId) {
1717
1728
  this.notifyInstanceTopic(generalId, `πŸ”‘ User wants to /stop but is not admin:\nβ€’ Name: ${msg.username}\nβ€’ ID: ${msg.userId}\nβ€’ Platform: ${msg.source}\nβ€’ Group: ${chatId}\n\nTo approve: add \`${msg.userId}\` to classicBot.yaml \`admin_users\``);
@@ -1725,12 +1736,12 @@ export class FleetManager {
1725
1736
  // Handle /compact command (admin only)
1726
1737
  if (text === "/compact" || text.startsWith("/compact@")) {
1727
1738
  if (!this.classicChannels.isAdmin(msg.userId)) {
1728
- await msgAdapter?.sendText(chatId, "β›” /compact requires admin access.");
1739
+ await msgAdapter?.sendText(chatId, t("cmd.admin_required", "/compact"));
1729
1740
  return;
1730
1741
  }
1731
1742
  const compactName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
1732
1743
  if (!compactName) {
1733
- await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
1744
+ await msgAdapter?.sendText(chatId, t("classic.no_agent_start"));
1734
1745
  return;
1735
1746
  }
1736
1747
  const result = await this.topicCommands.sendCompact(compactName);
@@ -1741,7 +1752,7 @@ export class FleetManager {
1741
1752
  if (text === "/cancel" || text.startsWith("/cancel@")) {
1742
1753
  const cancelName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
1743
1754
  if (!cancelName) {
1744
- await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
1755
+ await msgAdapter?.sendText(chatId, t("classic.no_agent_start"));
1745
1756
  return;
1746
1757
  }
1747
1758
  const ok = this.cancelInstance(cancelName);
@@ -1752,7 +1763,7 @@ export class FleetManager {
1752
1763
  if (text === "/ctx" || text.startsWith("/ctx@")) {
1753
1764
  const ctxName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
1754
1765
  if (!ctxName) {
1755
- await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
1766
+ await msgAdapter?.sendText(chatId, t("classic.no_agent_start"));
1756
1767
  return;
1757
1768
  }
1758
1769
  const reply = await this.topicCommands.getCtxText(ctxName);
@@ -1762,21 +1773,21 @@ export class FleetManager {
1762
1773
  // Handle /save command (admin only)
1763
1774
  if (text === "/save" || text.startsWith("/save ") || text.startsWith("/save@")) {
1764
1775
  if (!this.classicChannels.isAdmin(msg.userId)) {
1765
- await msgAdapter?.sendText(chatId, "β›” /save requires admin access.");
1776
+ await msgAdapter?.sendText(chatId, t("cmd.admin_required", "/save"));
1766
1777
  return;
1767
1778
  }
1768
1779
  const saveName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
1769
1780
  if (!saveName) {
1770
- await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
1781
+ await msgAdapter?.sendText(chatId, t("classic.no_agent_start"));
1771
1782
  return;
1772
1783
  }
1773
1784
  const filename = parseSaveFilename(text);
1774
1785
  if (!filename) {
1775
- await msgAdapter?.sendText(chatId, "Usage: /save <filename>");
1786
+ await msgAdapter?.sendText(chatId, t("save.usage"));
1776
1787
  return;
1777
1788
  }
1778
1789
  if (!SAVE_FILENAME_RE.test(filename)) {
1779
- await msgAdapter?.sendText(chatId, "β›” Invalid filename β€” only letters, numbers, dots, hyphens, underscores allowed.");
1790
+ await msgAdapter?.sendText(chatId, t("filename.invalid"));
1780
1791
  return;
1781
1792
  }
1782
1793
  const backend = this.classicChannels.getBackendByInstance(saveName, this.fleetConfig?.defaults?.backend);
@@ -1786,7 +1797,7 @@ export class FleetManager {
1786
1797
  return;
1787
1798
  }
1788
1799
  this.pasteRawToClassicInstance(saveName, cmd);
1789
- await msgAdapter?.sendText(chatId, `βœ… Sent \`${cmd}\` to ${saveName}`);
1800
+ await msgAdapter?.sendText(chatId, t("save.sent", cmd, saveName));
1790
1801
  return;
1791
1802
  }
1792
1803
  // Route to classic channel if this bot has an agent here (per-bot).
@@ -1804,7 +1815,7 @@ export class FleetManager {
1804
1815
  // Strip @bot from text and forward as /chat
1805
1816
  const cleanText = botUser ? text.replace(new RegExp(`@${botUser}`, "gi"), "").trim() : text;
1806
1817
  if (cleanText.startsWith("/raw") && !this.classicChannels.isAdmin(msg.userId)) {
1807
- await msgAdapter?.sendText(chatId, "β›” /raw requires admin access.");
1818
+ await msgAdapter?.sendText(chatId, t("cmd.admin_required", "/raw"));
1808
1819
  return;
1809
1820
  }
1810
1821
  const syntheticMsg = { ...msg, threadId: chatId, text: `/chat ${cleanText}` };
@@ -1813,7 +1824,7 @@ export class FleetManager {
1813
1824
  }
1814
1825
  // Handle @bot without active agent
1815
1826
  if (isBotMentioned) {
1816
- await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
1827
+ await msgAdapter?.sendText(chatId, t("classic.no_agent_start"));
1817
1828
  return;
1818
1829
  }
1819
1830
  // Unregistered private chat: ignore (don't fall through to General)
@@ -2097,6 +2108,21 @@ export class FleetManager {
2097
2108
  // ===================== Scheduler =====================
2098
2109
  async handleScheduleTrigger(schedule) {
2099
2110
  const { target, reply_chat_id, reply_thread_id, message, label, id, source } = schedule;
2111
+ // Reserved auto-update schedule: run `agend update` instead of delivering a
2112
+ // message to an instance. message === "beta" selects the beta channel.
2113
+ if (target === "__fleet_update__") {
2114
+ const cmd = message === "beta" ? "agend update --beta" : "agend update";
2115
+ this.logger.info({ cmd, scheduleId: id }, "Auto-update schedule fired");
2116
+ try {
2117
+ const { spawn } = await import("node:child_process");
2118
+ spawn("sh", ["-c", `sleep 1 && ${cmd}`], { detached: true, stdio: "ignore" }).unref();
2119
+ this.scheduler?.recordRun(id, "delivered", `ran ${cmd}`);
2120
+ }
2121
+ catch (err) {
2122
+ this.scheduler?.recordRun(id, "retry", `update spawn failed: ${err.message}`);
2123
+ }
2124
+ return;
2125
+ }
2100
2126
  const RATE_LIMIT_DEFER_THRESHOLD = 85;
2101
2127
  const rl = this.statuslineWatcher.getRateLimits(target);
2102
2128
  if (rl && rl.five_hour_pct > RATE_LIMIT_DEFER_THRESHOLD) {
@@ -2762,7 +2788,7 @@ export class FleetManager {
2762
2788
  */
2763
2789
  async handleSlashSave(data, adapterId) {
2764
2790
  if (!this.classicChannels?.isAdmin(data.userId)) {
2765
- await data.respond("β›” This command requires admin access.");
2791
+ await data.respond(t("admin.required"));
2766
2792
  return;
2767
2793
  }
2768
2794
  // Classic resolves per-bot (same-channel multi-bot); otherwise a fleet topic.
@@ -2771,12 +2797,12 @@ export class FleetManager {
2771
2797
  ? { kind: "classic", name: classicName }
2772
2798
  : this.routing.resolve(data.channelId);
2773
2799
  if (!target) {
2774
- await data.respond("No active agent in this channel. Use `/start` first.");
2800
+ await data.respond(t("classic.no_agent_start"));
2775
2801
  return;
2776
2802
  }
2777
2803
  const filename = data.options?.filename ?? "";
2778
2804
  if (!SAVE_FILENAME_RE.test(filename)) {
2779
- await data.respond("β›” Invalid filename β€” only letters, numbers, dots, hyphens, underscores allowed.");
2805
+ await data.respond(t("filename.invalid"));
2780
2806
  return;
2781
2807
  }
2782
2808
  const backend = target.kind === "classic"
@@ -2795,7 +2821,7 @@ export class FleetManager {
2795
2821
  else {
2796
2822
  this.instanceIpcClients.get(target.name)?.send({ type: "raw_paste", content: cmd });
2797
2823
  }
2798
- await data.respond(`βœ… Sent \`${cmd}\` to ${target.name}`);
2824
+ await data.respond(t("save.sent", cmd, target.name));
2799
2825
  }
2800
2826
  /** Whether the instance currently has at least one live cancel button. */
2801
2827
  hasCancelButton(instanceName) {
@@ -2837,7 +2863,7 @@ export class FleetManager {
2837
2863
  type: "cancel",
2838
2864
  instanceName,
2839
2865
  message: "πŸ‘€ 處理中…",
2840
- choices: [{ id: `cancel:${instanceName}`, label: "πŸ›‘ ε–ζΆˆ" }],
2866
+ choices: [{ id: `cancel:${instanceName}`, label: t("cancel.button") }],
2841
2867
  }, threadId ? { threadId } : undefined);
2842
2868
  // A concurrent sendCancelButton for the same instance may have posted its
2843
2869
  // own button while we awaited notifyAlert. Retire any other buttons for
@@ -3619,15 +3645,15 @@ When users create specialized instances, suggest these configurations:
3619
3645
  if (generalId) {
3620
3646
  this.notifyInstanceTopic(generalId, `πŸ†• Unauthorized guild tried /start:\nβ€’ Guild ID: ${guildId}\nβ€’ User: ${userId}\nβ€’ Platform: discord\n\nTo allow: add \`${guildId}\` to classicBot.yaml \`allowed_guilds\``);
3621
3647
  }
3622
- return "β›” This server is not in the allowed guilds list.";
3648
+ return t("classic.not_authorized_guild");
3623
3649
  }
3624
3650
  // Per-bot check: a second bot may /start in the same channel (own agent).
3625
3651
  if (this.classicChannels.isClassicChannel(channelId, adapterId))
3626
- return "This channel already has an active agent. Use /chat to talk.";
3652
+ return t("classic.already_active");
3627
3653
  // Classic no longer lives in the routing engine, so this only guards against
3628
3654
  // a fleet topic-mode instance colliding with the channel.
3629
3655
  if (this.routing.resolve(channelId))
3630
- return "This channel is already bound to a topic-mode instance.";
3656
+ return t("classic.topic_bound");
3631
3657
  const instanceName = this.classicChannels.deriveInstanceName(channelName || channelId, channelId, adapterId);
3632
3658
  this.classicChannels.register(channelId, adapterId, instanceName, channelName || channelId, userId);
3633
3659
  // Bind this classic instance to the bot that started it (authoritative), so
@@ -3642,7 +3668,7 @@ When users create specialized instances, suggest these configurations:
3642
3668
  this.classicChannels.toggleCollab(channelId, adapterId);
3643
3669
  }
3644
3670
  this.logger.info({ channelId, adapterId, instanceName, userId }, "Classic channel started");
3645
- return `βœ… Agent started in this channel. Use \`/chat <message>\` or @mention to talk.`;
3671
+ return t("classic.started");
3646
3672
  }
3647
3673
  /** Handle /stop slash command β€” unregister classic channel */
3648
3674
  async handleClassicStop(channelId, adapterId) {
@@ -3650,12 +3676,12 @@ When users create specialized instances, suggest these configurations:
3650
3676
  return "Classic channel manager not initialized.";
3651
3677
  const ch = this.classicChannels.unregister(channelId, adapterId);
3652
3678
  if (!ch)
3653
- return "No active agent in this channel.";
3679
+ return t("classic.no_agent");
3654
3680
  this.instanceWorldBinding.delete(ch.instanceName);
3655
3681
  await this.stopInstance(ch.instanceName).catch(err => this.logger.warn({ err, instanceName: ch.instanceName }, "Failed to stop classic instance"));
3656
3682
  this.reregisterClassicChannels();
3657
3683
  this.logger.info({ channelId, adapterId, instanceName: ch.instanceName }, "Classic channel stopped");
3658
- return `πŸ›‘ Agent stopped in this channel.`;
3684
+ return t("classic.stopped");
3659
3685
  }
3660
3686
  async stopAll() {
3661
3687
  this.ipcStoppingInstances.add("__fleet_stopping__");
@@ -4066,7 +4092,7 @@ When users create specialized instances, suggest these configurations:
4066
4092
  if (target && this.semverGt(target, currentVersion)) {
4067
4093
  const generalId = this.findGeneralInstance();
4068
4094
  if (generalId) {
4069
- this.notifyInstanceTopic(generalId, `πŸ†• AgEnD v${target} available (current: v${currentVersion}). Use \`/update\` to upgrade.`);
4095
+ this.notifyInstanceTopic(generalId, t("update.available", `v${target}`) + ` (current: v${currentVersion})`);
4070
4096
  }
4071
4097
  }
4072
4098
  }