appback-remoteagent 0.22.3 → 0.23.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/README.md CHANGED
@@ -329,6 +329,13 @@ remoteagent-start
329
329
 
330
330
  The command validates the BotFather token and shows a one-time Telegram `/start` link. Open that link from the account that will own the installation; RemoteAgent detects the sender's numeric user ID and stores it as the owner automatically. The token is entered through a hidden prompt, and the owner ID does not need to be looked up or typed manually.
331
331
 
332
+ Remove a configured bot by username or numeric Bot ID, then restart the runtime:
333
+
334
+ ```bash
335
+ remoteagent bot remove @example_bot
336
+ sudo systemctl restart remoteagent
337
+ ```
338
+
332
339
  To move installation-wide `/secret` values to another PC, export and import a password-encrypted bundle:
333
340
 
334
341
  ```bash
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import { randomBytes } from "node:crypto";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import process from "node:process";
6
- import { fetchTelegramBotIdentity, registerTelegramBot, readConfiguredOwnerId, waitForTelegramOwner, } from "./services/cli-config-service.js";
6
+ import { fetchTelegramBotIdentity, registerTelegramBot, removeTelegramBot, readConfiguredOwnerId, waitForTelegramOwner, } from "./services/cli-config-service.js";
7
7
  import { exportSecrets, importSecrets } from "./services/secret-transfer-service.js";
8
8
  async function main() {
9
9
  const args = process.argv.slice(2);
@@ -21,6 +21,10 @@ async function main() {
21
21
  await addBot(dataDir, args.slice(2));
22
22
  return;
23
23
  }
24
+ if (group === "bot" && action === "remove") {
25
+ await removeBot(dataDir, args.slice(2));
26
+ return;
27
+ }
24
28
  if (group === "secret" && action === "export") {
25
29
  await exportSecretCommand(dataDir, args.slice(2));
26
30
  return;
@@ -76,6 +80,25 @@ async function addBot(dataDir, args) {
76
80
  " # systemd runtime: sudo systemctl restart remoteagent",
77
81
  ].join("\n"));
78
82
  }
83
+ async function removeBot(dataDir, args) {
84
+ if (args.some((arg) => arg.startsWith("--"))) {
85
+ throw new Error(`Unknown bot remove option: ${args.find((arg) => arg.startsWith("--"))}`);
86
+ }
87
+ const selector = args.shift();
88
+ if (!selector || args.length > 0) {
89
+ throw new Error("Usage: remoteagent bot remove <username|id>");
90
+ }
91
+ const result = await removeTelegramBot({ dataDir, selector });
92
+ const label = result.username ? `@${result.username}` : `bot ${result.id}`;
93
+ console.log([
94
+ `Removed ${label} (${result.id}).`,
95
+ `Configured bots: ${result.botCount}`,
96
+ `Configuration: ${result.envPath}`,
97
+ "Start or restart RemoteAgent to apply it:",
98
+ " remoteagent-start",
99
+ " # systemd runtime: sudo systemctl restart remoteagent",
100
+ ].join("\n"));
101
+ }
79
102
  async function exportSecretCommand(dataDir, args) {
80
103
  const passphraseFile = takeOption(args, "--passphrase-file");
81
104
  if (args.some((arg) => arg.startsWith("--"))) {
@@ -197,6 +220,7 @@ Usage:
197
220
  remoteagent Start the foreground runtime
198
221
  remoteagent bot add [token] [--owner <telegram-user-id>]
199
222
  remoteagent bot add --token-file <file> --owner <telegram-user-id>
223
+ remoteagent bot remove <username|id>
200
224
  remoteagent secret export [file] [--passphrase-file <file>]
201
225
  remoteagent secret import <file> [--replace] [--passphrase-file <file>]
202
226
 
package/dist/config.js CHANGED
@@ -33,14 +33,13 @@ function readTelegramBotTokens() {
33
33
  return [readRequired("TELEGRAM_BOT_TOKEN")];
34
34
  }
35
35
  function readTelegramBotUsernames() {
36
- const raw = process.env.TELEGRAM_BOT_USERNAMES?.trim();
37
- if (!raw) {
36
+ const raw = process.env.TELEGRAM_BOT_USERNAMES;
37
+ if (raw === undefined || raw === "") {
38
38
  return [];
39
39
  }
40
40
  return raw
41
- .split(/[\r\n,]+/)
42
- .map((value) => value.trim())
43
- .filter(Boolean);
41
+ .split(",")
42
+ .map((value) => value.trim());
44
43
  }
45
44
  function readOptional(name) {
46
45
  const value = process.env[name]?.trim();
@@ -437,7 +437,9 @@ export class BotManagementService {
437
437
  : singleTokenLine
438
438
  ? [singleTokenLine.slice("TELEGRAM_BOT_TOKEN=".length).trim()].filter(Boolean)
439
439
  : [];
440
- const configuredUsernames = usernameLine ? this.parseCsv(usernameLine.slice("TELEGRAM_BOT_USERNAMES=".length)) : [];
440
+ const configuredUsernames = usernameLine
441
+ ? usernameLine.slice("TELEGRAM_BOT_USERNAMES=".length).split(",").map((value) => value.trim())
442
+ : [];
441
443
  const usernames = await this.normalizeUsernamesFromTelegram(tokens, configuredUsernames);
442
444
  return {
443
445
  lines,
@@ -18,7 +18,7 @@ export async function registerTelegramBot(options) {
18
18
  });
19
19
  const values = parseEnv(original);
20
20
  const configuredTokens = parseCsv(values.get("TELEGRAM_BOT_TOKENS") || values.get("TELEGRAM_BOT_TOKEN") || "");
21
- const configuredUsernames = parseCsv(values.get("TELEGRAM_BOT_USERNAMES") || "");
21
+ const configuredUsernames = parseCsvSlots(values.get("TELEGRAM_BOT_USERNAMES") || "");
22
22
  const validIndexes = configuredTokens
23
23
  .map((configuredToken, index) => isBotToken(configuredToken) ? index : -1)
24
24
  .filter((index) => index >= 0);
@@ -54,6 +54,51 @@ export async function registerTelegramBot(options) {
54
54
  botCount: tokens.length,
55
55
  };
56
56
  }
57
+ export async function removeTelegramBot(options) {
58
+ const selector = options.selector.trim().replace(/^@/, "").toLowerCase();
59
+ if (!selector) {
60
+ throw new Error("Usage: remoteagent bot remove <username|id>");
61
+ }
62
+ const envPath = path.join(options.dataDir, ".env");
63
+ const original = await fs.readFile(envPath, "utf8").catch((error) => {
64
+ if (error.code === "ENOENT") {
65
+ throw new Error(`RemoteAgent configuration was not found: ${envPath}`);
66
+ }
67
+ throw error;
68
+ });
69
+ const values = parseEnv(original);
70
+ const configuredTokens = parseCsv(values.get("TELEGRAM_BOT_TOKENS") || values.get("TELEGRAM_BOT_TOKEN") || "");
71
+ const configuredUsernames = parseCsvSlots(values.get("TELEGRAM_BOT_USERNAMES") || "");
72
+ const validIndexes = configuredTokens
73
+ .map((token, index) => isBotToken(token) ? index : -1)
74
+ .filter((index) => index >= 0);
75
+ const tokens = validIndexes.map((index) => configuredTokens[index]);
76
+ const usernames = validIndexes.map((index) => configuredUsernames[index] || "");
77
+ const targetIndex = tokens.findIndex((token, index) => token.slice(0, token.indexOf(":")) === selector
78
+ || usernames[index]?.toLowerCase() === selector);
79
+ if (targetIndex < 0) {
80
+ throw new Error(`Telegram bot was not found: ${options.selector.trim()}`);
81
+ }
82
+ if (tokens.length <= 1) {
83
+ throw new Error("Cannot remove the last configured bot.");
84
+ }
85
+ const removedToken = tokens[targetIndex];
86
+ const removedUsername = usernames[targetIndex] || undefined;
87
+ const remainingTokens = tokens.filter((_, index) => index !== targetIndex);
88
+ const remainingUsernames = usernames.filter((_, index) => index !== targetIndex);
89
+ const next = upsertEnv(original, {
90
+ TELEGRAM_BOT_TOKEN: remainingTokens[0],
91
+ TELEGRAM_BOT_TOKENS: remainingTokens.join(","),
92
+ TELEGRAM_BOT_USERNAMES: remainingUsernames.join(","),
93
+ });
94
+ await atomicWrite(envPath, next, 0o600);
95
+ return {
96
+ id: Number(removedToken.slice(0, removedToken.indexOf(":"))),
97
+ username: removedUsername,
98
+ envPath,
99
+ botCount: remainingTokens.length,
100
+ };
101
+ }
57
102
  export async function readConfiguredOwnerId(dataDir) {
58
103
  const envPath = path.join(dataDir, ".env");
59
104
  const text = await fs.readFile(envPath, "utf8").catch((error) => {
@@ -200,6 +245,9 @@ function parseCsv(value) {
200
245
  .map((item) => item.trim())
201
246
  .filter(Boolean);
202
247
  }
248
+ function parseCsvSlots(value) {
249
+ return value.split(",").map((item) => item.trim());
250
+ }
203
251
  function parseEnv(text) {
204
252
  const result = new Map();
205
253
  for (const line of text.split(/\r?\n/)) {
@@ -22,6 +22,22 @@ chmod 600 /secure/path/telegram-token
22
22
  remoteagent bot add --token-file /secure/path/telegram-token --owner 123456789
23
23
  ```
24
24
 
25
+ Remove a configured bot by username or numeric Bot ID, then restart the runtime:
26
+
27
+ ```bash
28
+ remoteagent bot remove @example_bot
29
+ sudo systemctl restart remoteagent
30
+ ```
31
+
32
+ ```bash
33
+ remoteagent bot remove 123456789
34
+ sudo systemctl restart remoteagent
35
+ ```
36
+
37
+ The command updates `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_TOKENS`, and
38
+ `TELEGRAM_BOT_USERNAMES` together. It does not print bot tokens and refuses to
39
+ remove the final configured bot.
40
+
25
41
  After adding or updating a bot, apply the configuration with the runtime command appropriate to the installation:
26
42
 
27
43
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appback-remoteagent",
3
- "version": "0.22.3",
3
+ "version": "0.23.0",
4
4
  "description": "Personal installable session server for continuing local AI work across PC and Telegram",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import {
8
8
  fetchTelegramBotIdentity,
9
9
  registerTelegramBot,
10
+ removeTelegramBot,
10
11
  waitForTelegramOwner,
11
12
  } from "../dist/services/cli-config-service.js";
12
13
  import { buildProviderEnv, buildRuntimePath } from "../dist/adapters/runtime-env.js";
@@ -14,6 +15,7 @@ import { exportSecrets, importSecrets } from "../dist/services/secret-transfer-s
14
15
 
15
16
  const root = await fs.mkdtemp(path.join(os.tmpdir(), "remoteagent-cli-selftest-"));
16
17
  const sourceDataDir = path.join(root, "source");
18
+ const sparseUsernameDataDir = path.join(root, "sparse-usernames");
17
19
  const targetDataDir = path.join(root, "target");
18
20
  const bundlePath = path.join(root, "transfer.ra-secrets");
19
21
  const selectedBundlePath = path.join(root, "selected-transfer.ra-secrets");
@@ -112,6 +114,55 @@ fi
112
114
  assert.doesNotMatch(envText, /your-telegram-bot-token/);
113
115
  assert.match(envText, /TELEGRAM_BOT_USERNAMES=first_remoteagent_bot,second_remoteagent_bot/);
114
116
 
117
+ const removedByUsername = await removeTelegramBot({
118
+ dataDir: sourceDataDir,
119
+ selector: "@second_remoteagent_bot",
120
+ });
121
+ assert.equal(removedByUsername.id, 100002);
122
+ assert.equal(removedByUsername.username, "second_remoteagent_bot");
123
+ assert.equal(removedByUsername.botCount, 1);
124
+ const envAfterUsernameRemoval = await fs.readFile(path.join(sourceDataDir, ".env"), "utf8");
125
+ assert.match(envAfterUsernameRemoval, /TELEGRAM_BOT_TOKEN=100001:abcdefghijklmnopqrstuvwxyz_123456/);
126
+ assert.match(envAfterUsernameRemoval, /TELEGRAM_BOT_TOKENS=100001:abcdefghijklmnopqrstuvwxyz_123456/);
127
+ assert.match(envAfterUsernameRemoval, /TELEGRAM_BOT_USERNAMES=first_remoteagent_bot/);
128
+ assert.doesNotMatch(envAfterUsernameRemoval, /second_remoteagent_bot/);
129
+
130
+ await assert.rejects(
131
+ removeTelegramBot({ dataDir: sourceDataDir, selector: "100001" }),
132
+ /Cannot remove the last configured bot/,
133
+ );
134
+ await assert.rejects(
135
+ removeTelegramBot({ dataDir: sourceDataDir, selector: "missing_bot" }),
136
+ /Telegram bot was not found/,
137
+ );
138
+
139
+ await registerTelegramBot({
140
+ dataDir: sourceDataDir,
141
+ token: "100002:abcdefghijklmnopqrstuvwxyz_654321",
142
+ ownerId: "8202993989",
143
+ identity: { id: 100002, username: "second_remoteagent_bot" },
144
+ });
145
+ const removedById = await removeTelegramBot({ dataDir: sourceDataDir, selector: "100001" });
146
+ assert.equal(removedById.username, "first_remoteagent_bot");
147
+ assert.equal(removedById.botCount, 1);
148
+
149
+ await fs.mkdir(sparseUsernameDataDir, { recursive: true });
150
+ await fs.writeFile(path.join(sparseUsernameDataDir, ".env"), [
151
+ "TELEGRAM_BOT_TOKEN=100001:abcdefghijklmnopqrstuvwxyz_123456",
152
+ "TELEGRAM_BOT_TOKENS=100001:abcdefghijklmnopqrstuvwxyz_123456,100002:abcdefghijklmnopqrstuvwxyz_654321",
153
+ "TELEGRAM_BOT_USERNAMES=,second_remoteagent_bot",
154
+ "TELEGRAM_OWNER_ID=8202993989",
155
+ "",
156
+ ].join("\n"), { mode: 0o600 });
157
+ const removedSparseUsername = await removeTelegramBot({
158
+ dataDir: sparseUsernameDataDir,
159
+ selector: "second_remoteagent_bot",
160
+ });
161
+ assert.equal(removedSparseUsername.id, 100002);
162
+ const sparseEnv = await fs.readFile(path.join(sparseUsernameDataDir, ".env"), "utf8");
163
+ assert.match(sparseEnv, /TELEGRAM_BOT_TOKENS=100001:abcdefghijklmnopqrstuvwxyz_123456/);
164
+ assert.doesNotMatch(sparseEnv, /100002:abcdefghijklmnopqrstuvwxyz_654321/);
165
+
115
166
  const sourceSecrets = {
116
167
  API_TOKEN: {
117
168
  key: "API_TOKEN",