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 +7 -0
- package/dist/cli.js +25 -1
- package/dist/config.js +4 -5
- package/dist/services/bot-management-service.js +3 -1
- package/dist/services/cli-config-service.js +49 -1
- package/docs/CLI_BOOTSTRAP_AND_SECRET_MIGRATION.md +16 -0
- package/package.json +1 -1
- package/scripts/selftest-cli.mjs +51 -0
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
|
|
37
|
-
if (
|
|
36
|
+
const raw = process.env.TELEGRAM_BOT_USERNAMES;
|
|
37
|
+
if (raw === undefined || raw === "") {
|
|
38
38
|
return [];
|
|
39
39
|
}
|
|
40
40
|
return raw
|
|
41
|
-
.split(
|
|
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
|
|
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 =
|
|
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
package/scripts/selftest-cli.mjs
CHANGED
|
@@ -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",
|