appback-remoteagent 0.23.5 → 0.23.6
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 +10 -6
- package/dist/adapters/codex-adapter.js +14 -2
- package/dist/bot.js +30 -17
- package/dist/services/login-service.js +129 -0
- package/dist/services/provider-setup-service.js +4 -4
- package/dist/telegram-command-menu.js +1 -1
- package/docs/LOGIN.md +62 -0
- package/package.json +1 -1
- package/scripts/release-deploy.sh +30 -2
- package/scripts/selftest-codex-stream.mjs +7 -0
- package/scripts/selftest-login.mjs +52 -0
- package/scripts/selftest-telegram-update.mjs +8 -0
package/README.md
CHANGED
|
@@ -122,8 +122,8 @@ Current command surface implemented in `src/bot.ts`:
|
|
|
122
122
|
| `/bot remove <username\|id>` | Removes a configured Telegram bot, restarts the runtime, and confirms the result after restart |
|
|
123
123
|
| `/bot reload` | Restarts the runtime and confirms the result after restart |
|
|
124
124
|
| `/install codex\|claude` | Runs the configured provider install or update command for the bot owner |
|
|
125
|
-
| `/login
|
|
126
|
-
| `/login claude
|
|
125
|
+
| `/login` | Owner-only GitHub / Codex / Claude selection buttons; returns authentication URL and any device code, then reports completion |
|
|
126
|
+
| `/login github\|codex\|claude` | Starts the selected login directly; `git` is an alias for `github` |
|
|
127
127
|
| `/reset` | Clears the current chat binding |
|
|
128
128
|
| `/batch start` | Starts manual batching of multiple text messages |
|
|
129
129
|
| `/batch send` | Sends the collected batch |
|
|
@@ -186,7 +186,7 @@ Current Claude behavior:
|
|
|
186
186
|
- fresh pairing from Telegram
|
|
187
187
|
- attach to existing `session_id`
|
|
188
188
|
- continue the same Claude Code session across turns
|
|
189
|
-
-
|
|
189
|
+
- owner-only installation through `/install claude` and browser authentication through `/login` → Claude
|
|
190
190
|
|
|
191
191
|
### 5. Telegram attachments
|
|
192
192
|
|
|
@@ -302,8 +302,7 @@ Recommended Linux hooks in this repo:
|
|
|
302
302
|
- `CLAUDE_COMMAND`
|
|
303
303
|
- `CLAUDE_PERMISSION_MODE`
|
|
304
304
|
- `CLAUDE_INSTALL_COMMAND`
|
|
305
|
-
- `
|
|
306
|
-
- `CLAUDE_LOGIN_FINISH_COMMAND`
|
|
305
|
+
- `CLAUDE_LOGIN_FINISH_COMMAND` (legacy `/login claude <token>` compatibility only)
|
|
307
306
|
- `REMOTEAGENT_SERVICE_NAME`
|
|
308
307
|
- `BOT_RESTART_HELPER_PATH`
|
|
309
308
|
- `LOCAL_UI_ENABLED`
|
|
@@ -387,15 +386,20 @@ Then open Telegram and start with one of these common flows. `/start` without a
|
|
|
387
386
|
/start claude
|
|
388
387
|
/install codex
|
|
389
388
|
/install claude
|
|
389
|
+
/login
|
|
390
|
+
/login github
|
|
390
391
|
/login codex
|
|
391
392
|
/login claude
|
|
392
|
-
/login claude <token>
|
|
393
393
|
/attach codex <thread_id>
|
|
394
394
|
/attach claude <session_id>
|
|
395
395
|
```
|
|
396
396
|
|
|
397
397
|
Once a chat is bound, ordinary text messages continue the active session. Supported attachments can also be sent directly as normal Telegram messages.
|
|
398
398
|
|
|
399
|
+
`/login` authenticates the server's RemoteAgent OS account without creating a new
|
|
400
|
+
session or changing its model. Already authenticated accounts offer a **Log in
|
|
401
|
+
again** button. See [Login guide](docs/LOGIN.md) for prerequisites and expiry.
|
|
402
|
+
|
|
399
403
|
## Architecture and operations
|
|
400
404
|
|
|
401
405
|
High-level architecture: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
@@ -194,16 +194,28 @@ export class CodexAdapter {
|
|
|
194
194
|
formatProcessError(stdout, stderr, timedOut = false, code) {
|
|
195
195
|
const structured = this.extractStructuredError(stdout, stderr);
|
|
196
196
|
if (structured) {
|
|
197
|
-
return structured;
|
|
197
|
+
return this.addUpgradeGuidance(structured);
|
|
198
198
|
}
|
|
199
199
|
const text = this.extractPlainTextError(stdout, stderr);
|
|
200
200
|
if (text) {
|
|
201
|
-
return text;
|
|
201
|
+
return this.addUpgradeGuidance(text);
|
|
202
202
|
}
|
|
203
203
|
return timedOut
|
|
204
204
|
? this.formatTimeoutError()
|
|
205
205
|
: `Codex process exited with code ${code ?? "unknown"} without stdout/stderr.`;
|
|
206
206
|
}
|
|
207
|
+
addUpgradeGuidance(message) {
|
|
208
|
+
if (!/requires a newer version of Codex/i.test(message)) {
|
|
209
|
+
return message;
|
|
210
|
+
}
|
|
211
|
+
return [
|
|
212
|
+
message,
|
|
213
|
+
"",
|
|
214
|
+
"현재 Codex 버전이 선택한 모델을 지원하지 않습니다.",
|
|
215
|
+
"Telegram에서 /install codex 를 실행하면 최신 버전으로 업데이트됩니다.",
|
|
216
|
+
"업데이트가 완료되면 같은 세션에서 요청을 다시 보내 주세요.",
|
|
217
|
+
].join("\n");
|
|
218
|
+
}
|
|
207
219
|
extractStructuredError(stdout, stderr) {
|
|
208
220
|
const messages = [];
|
|
209
221
|
for (const line of stdout.split(/\r?\n/)) {
|
package/dist/bot.js
CHANGED
|
@@ -9,6 +9,7 @@ import { promisify } from "node:util";
|
|
|
9
9
|
import { Bot, GrammyError, HttpError } from "grammy";
|
|
10
10
|
import { config } from "./config.js";
|
|
11
11
|
import { ProviderSetupService } from "./services/provider-setup-service.js";
|
|
12
|
+
import { LoginService } from "./services/login-service.js";
|
|
12
13
|
import { RemoteShellService } from "./services/remote-shell-service.js";
|
|
13
14
|
import { AgentMemoryService } from "./services/agent-memory-service.js";
|
|
14
15
|
import { WorkspaceCleanupService } from "./services/workspace-cleanup-service.js";
|
|
@@ -47,8 +48,7 @@ const HELP_TEXT = [
|
|
|
47
48
|
"/bot remove <username|id>",
|
|
48
49
|
"/bot reload",
|
|
49
50
|
"/install codex|claude",
|
|
50
|
-
"/login
|
|
51
|
-
"/login claude [token]",
|
|
51
|
+
"/login - choose GitHub, Codex or Claude",
|
|
52
52
|
"/reset",
|
|
53
53
|
"/! <command>",
|
|
54
54
|
"/!cmd <command>",
|
|
@@ -1034,29 +1034,38 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
1034
1034
|
return { chunks: flattenChunks([result.output], 3900) };
|
|
1035
1035
|
});
|
|
1036
1036
|
});
|
|
1037
|
+
const loginService = new LoginService(15 * 60_000, { codex: config.codexBin, claude: config.claudeBin });
|
|
1038
|
+
const startLogin = async (ctx, target, force = false) => {
|
|
1039
|
+
await ensureOwnerControlAccess(ctx);
|
|
1040
|
+
if (!ctx.chat)
|
|
1041
|
+
throw new Error("Telegram chat context is missing.");
|
|
1042
|
+
const result = await loginService.start(target, force, async (text) => { await reply(ctx, text); });
|
|
1043
|
+
await reply(ctx, result.text, result.alreadyLoggedIn ? keyboardOptions([[
|
|
1044
|
+
actionButton(ctx, "Log in again", { kind: "login.start", target, force: true }),
|
|
1045
|
+
]]) : undefined);
|
|
1046
|
+
};
|
|
1037
1047
|
bot.command("login", async (ctx) => {
|
|
1038
1048
|
await ensureOwnerControlAccess(ctx);
|
|
1039
1049
|
const { args, rest } = parseCommand(ctx.message?.text, 1);
|
|
1040
1050
|
const provider = args[0]?.toLowerCase();
|
|
1041
|
-
if (provider
|
|
1042
|
-
await
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1051
|
+
if (!provider) {
|
|
1052
|
+
await reply(ctx, "Choose an account to authenticate on this server. Authentication belongs to the OS account, not an individual session.", keyboardOptions([
|
|
1053
|
+
[actionButton(ctx, "GitHub", { kind: "login.start", target: "github" })],
|
|
1054
|
+
[actionButton(ctx, "Codex", { kind: "login.start", target: "codex" })],
|
|
1055
|
+
[actionButton(ctx, "Claude", { kind: "login.start", target: "claude" })],
|
|
1056
|
+
]));
|
|
1047
1057
|
return;
|
|
1048
1058
|
}
|
|
1049
|
-
if (provider
|
|
1050
|
-
await reply(ctx,
|
|
1059
|
+
if (provider === "claude" && rest?.trim()) {
|
|
1060
|
+
await reply(ctx, await setupService.finishClaudeLogin(rest));
|
|
1051
1061
|
return;
|
|
1052
1062
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
});
|
|
1063
|
+
const target = provider === "git" ? "github" : provider;
|
|
1064
|
+
if (target !== "github" && target !== "codex" && target !== "claude") {
|
|
1065
|
+
await reply(ctx, "Use /login to choose GitHub, Codex or Claude. Direct commands: /login github, /login codex, /login claude. /login git is a GitHub alias.");
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
await startLogin(ctx, target);
|
|
1060
1069
|
});
|
|
1061
1070
|
const setChatSandbox = async (ctx, sandboxMode) => {
|
|
1062
1071
|
if (!ctx.chat) {
|
|
@@ -1137,6 +1146,10 @@ ${bridge.formatStatus(mapping)}`);
|
|
|
1137
1146
|
text: action.kind === "macro.run" ? "Macro selected." : "Applying...",
|
|
1138
1147
|
});
|
|
1139
1148
|
try {
|
|
1149
|
+
if (action.kind === "login.start") {
|
|
1150
|
+
await startLogin(ctx, action.target, action.force);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1140
1153
|
if (action.kind === "session.switch") {
|
|
1141
1154
|
await reply(ctx, await switchChatSession(ctx, action.selector));
|
|
1142
1155
|
return;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { buildProviderEnv } from "../adapters/runtime-env.js";
|
|
4
|
+
const commands = {
|
|
5
|
+
github: { bin: "gh", status: ["auth", "status", "--hostname", "github.com"], login: ["auth", "login", "--hostname", "github.com", "--git-protocol", "https", "--web"] },
|
|
6
|
+
codex: { bin: "codex", status: ["login", "status"], login: ["login", "--device-auth"] },
|
|
7
|
+
claude: { bin: "claude", status: ["auth", "status"], login: ["auth", "login"] },
|
|
8
|
+
};
|
|
9
|
+
// Shared across Telegram bots: authentication belongs to the OS account, not a chat.
|
|
10
|
+
const active = new Set();
|
|
11
|
+
export function loginHints(raw) {
|
|
12
|
+
const text = raw.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
|
|
13
|
+
const urls = [...new Set(text.match(/https?:\/\/[^\s<>"\x1b]+/g) ?? [])];
|
|
14
|
+
if (!urls.length)
|
|
15
|
+
return undefined;
|
|
16
|
+
const code = text.match(/(?:one[- ]time|device) code[^\n]*?\b([A-Z0-9]{4}-[A-Z0-9]{4,5})\b/i)?.[1]
|
|
17
|
+
?? text.match(/\b([A-Z0-9]{4}-[A-Z0-9]{4,5})\b/)?.[1];
|
|
18
|
+
return [...urls, ...(code ? [`One-time code: ${code}`] : [])].join("\n");
|
|
19
|
+
}
|
|
20
|
+
export class LoginService {
|
|
21
|
+
lifetimeMs;
|
|
22
|
+
binaries;
|
|
23
|
+
constructor(lifetimeMs = 15 * 60_000, binaries = {}) {
|
|
24
|
+
this.lifetimeMs = lifetimeMs;
|
|
25
|
+
this.binaries = binaries;
|
|
26
|
+
}
|
|
27
|
+
unavailable(target) {
|
|
28
|
+
const guidance = target === "github" ? "Install GitHub CLI (gh) on this server."
|
|
29
|
+
: `Run /install ${target} first.`;
|
|
30
|
+
return `${commands[target].bin} could not start. ${guidance} If installed, check execution permissions.`;
|
|
31
|
+
}
|
|
32
|
+
async status(target) {
|
|
33
|
+
const command = commands[target];
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const child = spawn(this.binaries[target] ?? command.bin, command.status, { cwd: os.homedir(), env: buildProviderEnv({}) });
|
|
36
|
+
let output = "";
|
|
37
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 10_000);
|
|
38
|
+
child.stdout.on("data", chunk => { output = (output + chunk).slice(-16_384); });
|
|
39
|
+
child.stderr.resume();
|
|
40
|
+
child.stdin.end();
|
|
41
|
+
child.on("error", () => { clearTimeout(timer); reject(new Error(this.unavailable(target))); });
|
|
42
|
+
child.on("close", code => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
if (target === "claude") {
|
|
45
|
+
try {
|
|
46
|
+
resolve(code === 0 && JSON.parse(output).loggedIn === true);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
resolve(false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else
|
|
53
|
+
resolve(code === 0);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async start(target, force, notify) {
|
|
58
|
+
if (active.has(target))
|
|
59
|
+
return { alreadyLoggedIn: false, text: `${target} login is already in progress on this machine.` };
|
|
60
|
+
active.add(target);
|
|
61
|
+
try {
|
|
62
|
+
if (!force && await this.status(target)) {
|
|
63
|
+
active.delete(target);
|
|
64
|
+
return { alreadyLoggedIn: true, text: `${target} is already authenticated for OS account ${os.userInfo().username}.` };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
active.delete(target);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
const command = commands[target];
|
|
72
|
+
return new Promise(resolve => {
|
|
73
|
+
const child = spawn(this.binaries[target] ?? command.bin, command.login, {
|
|
74
|
+
cwd: os.homedir(), env: buildProviderEnv({ BROWSER: "echo", GH_BROWSER: "echo" }),
|
|
75
|
+
});
|
|
76
|
+
const stopOnExit = () => { child.kill("SIGKILL"); };
|
|
77
|
+
process.once("exit", stopOnExit);
|
|
78
|
+
let raw = "";
|
|
79
|
+
let delivered = "";
|
|
80
|
+
let settled = false;
|
|
81
|
+
let expired = false;
|
|
82
|
+
let notifications = Promise.resolve();
|
|
83
|
+
const send = (text) => { notifications = notifications.then(() => notify(text)).catch(() => undefined); };
|
|
84
|
+
const finishStart = (text) => {
|
|
85
|
+
if (settled)
|
|
86
|
+
return;
|
|
87
|
+
settled = true;
|
|
88
|
+
clearTimeout(initialTimer);
|
|
89
|
+
resolve({ alreadyLoggedIn: false, text });
|
|
90
|
+
};
|
|
91
|
+
const initialTimer = setTimeout(() => finishStart(`${target} login is waiting for an authentication URL. Completion or expiry will be reported here.`), 20_000);
|
|
92
|
+
const expiryTimer = setTimeout(() => { expired = true; child.kill("SIGKILL"); }, this.lifetimeMs);
|
|
93
|
+
const consume = (chunk) => {
|
|
94
|
+
raw = (raw + chunk.toString()).slice(-32_768);
|
|
95
|
+
const hints = loginHints(raw);
|
|
96
|
+
if (hints && hints !== delivered) {
|
|
97
|
+
delivered = hints;
|
|
98
|
+
const message = `${target} login (${os.userInfo().username}@${os.hostname()})\n${hints}\nComplete authentication in your browser. Existing sessions remain unchanged.`;
|
|
99
|
+
if (settled)
|
|
100
|
+
send(message);
|
|
101
|
+
else
|
|
102
|
+
finishStart(message);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
child.stdout.on("data", consume);
|
|
106
|
+
child.stderr.on("data", consume);
|
|
107
|
+
child.stdin.on("error", () => undefined);
|
|
108
|
+
child.stdin.end("\n");
|
|
109
|
+
child.on("error", () => finishStart(this.unavailable(target)));
|
|
110
|
+
child.on("close", async (code) => {
|
|
111
|
+
process.removeListener("exit", stopOnExit);
|
|
112
|
+
clearTimeout(expiryTimer);
|
|
113
|
+
try {
|
|
114
|
+
const authenticated = !expired && code === 0 && await this.status(target).catch(() => false);
|
|
115
|
+
const message = authenticated ? `${target} login completed and authentication verified.`
|
|
116
|
+
: expired ? `${target} login expired after ${Math.round(this.lifetimeMs / 60_000)} minutes. Run /login to retry.`
|
|
117
|
+
: `${target} login did not complete (exit=${code}). Authentication was not confirmed. Run /login to retry.`;
|
|
118
|
+
if (settled)
|
|
119
|
+
send(message);
|
|
120
|
+
else
|
|
121
|
+
finishStart(message);
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
active.delete(target);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -141,13 +141,13 @@ export class ProviderSetupService {
|
|
|
141
141
|
if (/not logged in/i.test(statusText)) {
|
|
142
142
|
return [
|
|
143
143
|
"Codex is installed but not logged in yet.",
|
|
144
|
-
"Next step: run
|
|
144
|
+
"Next step: run /login and select Codex.",
|
|
145
145
|
"If you prefer machine-side auth, you can use `codex login --device-auth` and complete the login in your browser.",
|
|
146
146
|
].join("\n");
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
catch {
|
|
150
|
-
return "Codex is installed.
|
|
150
|
+
return "Codex is installed. To authenticate this server account, run /login and select Codex.";
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
if (provider === "claude") {
|
|
@@ -157,12 +157,12 @@ export class ProviderSetupService {
|
|
|
157
157
|
if (/not logged in|loggedIn:\s*false/i.test(statusText)) {
|
|
158
158
|
return [
|
|
159
159
|
"Claude Code is installed but not logged in yet.",
|
|
160
|
-
"Next step: run
|
|
160
|
+
"Next step: run /login and select Claude.",
|
|
161
161
|
].join("\n");
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
catch {
|
|
165
|
-
return "Claude Code is installed.
|
|
165
|
+
return "Claude Code is installed. To authenticate this server account, run /login and select Claude.";
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
return undefined;
|
|
@@ -24,7 +24,7 @@ export const TELEGRAM_COMMAND_MENU = [
|
|
|
24
24
|
{ command: "bots", description: "List configured Telegram bots" },
|
|
25
25
|
{ command: "bot", description: "Manage Telegram bots" },
|
|
26
26
|
{ command: "install", description: "Install or update Codex or Claude" },
|
|
27
|
-
{ command: "login", description: "
|
|
27
|
+
{ command: "login", description: "Choose GitHub, Codex or Claude login" },
|
|
28
28
|
{ command: "reset", description: "Clear this chat binding" },
|
|
29
29
|
{ command: "help", description: "Show command help" },
|
|
30
30
|
];
|
package/docs/LOGIN.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Server Account Login
|
|
2
|
+
|
|
3
|
+
Send `/login` in Telegram and select GitHub, Codex, or Claude.
|
|
4
|
+
`/help` lists this entry once. Telegram's slash-command menu offers `/login`;
|
|
5
|
+
provider names are chosen with buttons, not subcommand autocomplete.
|
|
6
|
+
The login applies to the RemoteAgent OS account on that server. Existing
|
|
7
|
+
Telegram sessions and their selected models remain unchanged.
|
|
8
|
+
|
|
9
|
+
Direct commands:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
/login github
|
|
13
|
+
/login git
|
|
14
|
+
/login codex
|
|
15
|
+
/login claude
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
GitHub uses `gh auth login --hostname github.com --git-protocol https --web`.
|
|
19
|
+
Codex uses `codex login --device-auth`.
|
|
20
|
+
Claude uses `claude auth login`.
|
|
21
|
+
Install the selected CLI on the server first. For providers, use
|
|
22
|
+
`/install codex` or `/install claude`; GitHub requires the GitHub CLI (`gh`).
|
|
23
|
+
|
|
24
|
+
An authenticated account gets a "Log in again" button. A new flow reports
|
|
25
|
+
URLs and one-time device codes emitted by the CLI, then checks authentication
|
|
26
|
+
after a successful process exit. Complete the browser flow from your PC.
|
|
27
|
+
One login per provider can run across all bots in the same RemoteAgent process.
|
|
28
|
+
Flows expire after 15 minutes. Restarting RemoteAgent interrupts the flow;
|
|
29
|
+
start `/login` again after a restart. Telegram delivery failure is not an
|
|
30
|
+
authentication failure: `/login` checks the account again.
|
|
31
|
+
|
|
32
|
+
Raw CLI output is not forwarded. Only authentication URLs, device codes, and
|
|
33
|
+
status messages are delivered. The legacy `/login claude <token>` command
|
|
34
|
+
continues to use its configured finish hook. The button flow uses the native
|
|
35
|
+
Claude CLI, not the old 15-second start hook.
|
|
36
|
+
|
|
37
|
+
Local regression tests:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npm run build
|
|
41
|
+
node scripts/selftest-login.mjs
|
|
42
|
+
npm run selftest:telegram
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
CLI references: https://cli.github.com/manual/gh_auth_login and
|
|
46
|
+
https://code.claude.com/docs/en/cli-reference.
|
|
47
|
+
|
|
48
|
+
## Deploy to Server 50
|
|
49
|
+
|
|
50
|
+
After committing changes, bump the version with `npm run release:version -- patch`,
|
|
51
|
+
commit the version files, and push. Publish and deploy the exact version:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
npm run release:publish
|
|
55
|
+
npm run release:deploy -- 0.23.6 50
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The 50 target uses root SSH to run npm and RemoteAgent as `daone`. It checks
|
|
59
|
+
for active work, stops the runtime, updates the npm package, and restarts it
|
|
60
|
+
from the account home directory. Existing configuration, secrets, and sessions
|
|
61
|
+
remain in place. The existing npm launchers do not require installer regeneration.
|
|
62
|
+
The historical `all` target remains 30/40/26; select 50 explicitly.
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
usage() {
|
|
5
|
-
echo "Usage: npm run release:deploy -- <version> <30|40|26|all>" >&2
|
|
5
|
+
echo "Usage: npm run release:deploy -- <version> <30|40|26|50|all>" >&2
|
|
6
6
|
echo "Example: npm run release:deploy -- 0.15.5 all" >&2
|
|
7
7
|
}
|
|
8
8
|
|
|
@@ -20,7 +20,7 @@ if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
|
20
20
|
fi
|
|
21
21
|
|
|
22
22
|
case "$TARGET" in
|
|
23
|
-
30|40|26|all)
|
|
23
|
+
30|40|26|50|all)
|
|
24
24
|
;;
|
|
25
25
|
*)
|
|
26
26
|
usage
|
|
@@ -192,6 +192,31 @@ fi
|
|
|
192
192
|
REMOTE
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
deploy_50() {
|
|
196
|
+
ssh root@192.168.33.50 "runuser -u daone -- env VERSION=$VERSION PATH=/home/daone/.nvm/versions/node/v22.23.2/bin:/usr/local/bin:/usr/bin:/bin bash -s" <<'REMOTE'
|
|
197
|
+
set -euo pipefail
|
|
198
|
+
cd "$HOME"
|
|
199
|
+
node --input-type=module - <<'NODE'
|
|
200
|
+
import fs from 'node:fs';
|
|
201
|
+
const state = JSON.parse(fs.readFileSync(`${process.env.HOME}/.remoteagent/bot-polling-state.json`, 'utf8'));
|
|
202
|
+
if (Object.values(state.bots || {}).some(bot => bot.runningSessionIds?.length)) {
|
|
203
|
+
throw new Error('Active provider work; deployment aborted.');
|
|
204
|
+
}
|
|
205
|
+
NODE
|
|
206
|
+
remoteagent-stop
|
|
207
|
+
trap 'remoteagent-start' EXIT
|
|
208
|
+
npm install -g "appback-remoteagent@$VERSION"
|
|
209
|
+
test "$(node -p 'require(process.env.HOME + "/.nvm/versions/node/v22.23.2/lib/node_modules/appback-remoteagent/package.json").version')" = "$VERSION"
|
|
210
|
+
# Existing npm installation keeps the same launcher and configuration paths.
|
|
211
|
+
remoteagent-start
|
|
212
|
+
trap - EXIT
|
|
213
|
+
sleep 5
|
|
214
|
+
kill -0 "$(cat "$HOME/.remoteagent/remoteagent.pid")"
|
|
215
|
+
npm list -g appback-remoteagent --depth=0
|
|
216
|
+
tail -n 12 "$HOME/.remoteagent/logs/agent.log"
|
|
217
|
+
REMOTE
|
|
218
|
+
}
|
|
219
|
+
|
|
195
220
|
case "$TARGET" in
|
|
196
221
|
30)
|
|
197
222
|
deploy_30
|
|
@@ -202,6 +227,9 @@ case "$TARGET" in
|
|
|
202
227
|
26)
|
|
203
228
|
deploy_26
|
|
204
229
|
;;
|
|
230
|
+
50)
|
|
231
|
+
deploy_50
|
|
232
|
+
;;
|
|
205
233
|
all)
|
|
206
234
|
deploy_30
|
|
207
235
|
deploy_40
|
|
@@ -34,6 +34,13 @@ await fs.chmod(fakeCodex, 0o755);
|
|
|
34
34
|
|
|
35
35
|
const { CodexAdapter } = await import(path.join(root, "dist", "adapters", "codex-adapter.js"));
|
|
36
36
|
const adapter = new CodexAdapter(fakeCodex, 5000, "read-only");
|
|
37
|
+
const upgradeError = "The 'gpt-6-astra' model requires a newer version of Codex. Please upgrade to the latest app or CLI and try again.";
|
|
38
|
+
for (const raw of [upgradeError, JSON.stringify({type: "error", status: 400, error: {type: "invalid_request_error", message: upgradeError}})]) {
|
|
39
|
+
const formatted = adapter.formatProcessError(raw, "", false, 1);
|
|
40
|
+
if (!formatted.includes(upgradeError) || !formatted.includes("/install codex")) throw new Error("Missing Codex upgrade guidance");
|
|
41
|
+
}
|
|
42
|
+
const unrelatedError = "This request was blocked by our safety systems. Reason: Potentially unintended activity.";
|
|
43
|
+
if (adapter.formatProcessError(unrelatedError, "", false, 1) !== unrelatedError) throw new Error("Unrelated error was changed");
|
|
37
44
|
for (const method of ["buildExecArgs", "buildResumeArgs"]) {
|
|
38
45
|
for (const reasoningEffort of ["low", "medium", "high", "xhigh", "max"]) {
|
|
39
46
|
const args = adapter[method]({model: "gpt-6-astra", reasoningEffort, cwd: tmp, sessionId: "stream-thread"}, path.join(tmp, "output"), "read-only");
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { LoginService, loginHints } from '../dist/services/login-service.js';
|
|
6
|
+
|
|
7
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'ra-login-test-'));
|
|
8
|
+
const bin = path.join(dir, 'fake-cli');
|
|
9
|
+
const state = path.join(dir, 'authenticated');
|
|
10
|
+
const notices = [];
|
|
11
|
+
const waitFor = async predicate => {
|
|
12
|
+
for (let i = 0; i < 100; i++) {
|
|
13
|
+
if (predicate()) return;
|
|
14
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
15
|
+
}
|
|
16
|
+
throw new Error('Timed out waiting for test condition');
|
|
17
|
+
};
|
|
18
|
+
try {
|
|
19
|
+
await fs.writeFile(bin, `#!${process.execPath}
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
if (process.argv.includes('status')) {
|
|
22
|
+
console.log(JSON.stringify({loggedIn:fs.existsSync(${JSON.stringify(state)})}));
|
|
23
|
+
process.exit(fs.existsSync(${JSON.stringify(state)}) ? 0 : 1);
|
|
24
|
+
}
|
|
25
|
+
console.log('First copy your one-time code: ABCD-EFGH');
|
|
26
|
+
console.log('https://github.com/login/device');
|
|
27
|
+
console.log('ACCESS_TOKEN_MUST_NOT_LEAK');
|
|
28
|
+
setTimeout(() => {fs.writeFileSync(${JSON.stringify(state)}, 'ok'); process.exit(0);}, 400);
|
|
29
|
+
`, { mode: 0o700 });
|
|
30
|
+
const binaries = { github: bin, codex: bin, claude: bin };
|
|
31
|
+
const service = new LoginService(4000, binaries);
|
|
32
|
+
const notify = async text => { notices.push(text); };
|
|
33
|
+
const first = service.start('github', false, notify);
|
|
34
|
+
assert.match((await new LoginService(4000, binaries).start('github', false, notify)).text, /already in progress/);
|
|
35
|
+
assert.match((await first).text, /ABCD-EFGH/);
|
|
36
|
+
await waitFor(() => notices.some(text => text.includes('verified')));
|
|
37
|
+
assert.equal((await service.start('github', false, notify)).alreadyLoggedIn, true);
|
|
38
|
+
notices.length = 0;
|
|
39
|
+
assert.match((await service.start('github', true, notify)).text, /login\/device/);
|
|
40
|
+
await waitFor(() => notices.some(text => text.includes('verified')));
|
|
41
|
+
assert.ok(notices.every(text => !text.includes('ACCESS_TOKEN_MUST_NOT_LEAK')));
|
|
42
|
+
await fs.unlink(state);
|
|
43
|
+
notices.length = 0;
|
|
44
|
+
await new LoginService(100, binaries).start('claude', false, notify);
|
|
45
|
+
await waitFor(() => notices.some(text => text.includes('expired')));
|
|
46
|
+
await assert.rejects(new LoginService(100, { codex: path.join(dir, 'missing') }).start('codex', false, notify), /could not start/);
|
|
47
|
+
assert.equal(loginHints('secret token only'), undefined);
|
|
48
|
+
assert.match(loginHints('https://auth.openai.com/codex/device\nABCD-EFGHI'), /ABCD-EFGHI/);
|
|
49
|
+
console.log('PASS login: URL/code, status, cross-bot lock, reauthentication, completion, expiry, missing CLI, output filtering');
|
|
50
|
+
} finally {
|
|
51
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
@@ -527,6 +527,14 @@ if (calls.some((call) => /미완료 TODO|\/task|새 작업으로 접수/.test(ca
|
|
|
527
527
|
throw new Error(`Task gate language leaked to Telegram replies. Calls: ${JSON.stringify(calls, null, 2)}`);
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
+
await send("/login");
|
|
531
|
+
const loginMenu = await waitForTelegramCall(call => call.text.includes("Choose an account to authenticate"));
|
|
532
|
+
for (const label of ["GitHub", "Codex", "Claude"]) {
|
|
533
|
+
if (!findInlineButton(loginMenu, label)?.callback_data?.startsWith("remoteagent:action:")) {
|
|
534
|
+
throw new Error(`Missing login button: ${label}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
530
538
|
await send("/new");
|
|
531
539
|
await send("/list");
|
|
532
540
|
const sessionListCall = await waitForTelegramCall((call) => call.text.includes("Sessions (2/2)"));
|