appback-remoteagent 0.13.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.
Files changed (46) hide show
  1. package/.env.example +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +371 -0
  4. package/bin/remoteagent.js +2 -0
  5. package/dist/adapters/claude-adapter.js +78 -0
  6. package/dist/adapters/codex-adapter.js +241 -0
  7. package/dist/adapters/provider-adapter.js +1 -0
  8. package/dist/adapters/shell-adapter.js +44 -0
  9. package/dist/adapters/windows-shell.js +111 -0
  10. package/dist/bot.js +2135 -0
  11. package/dist/config.js +170 -0
  12. package/dist/index.js +534 -0
  13. package/dist/secret-helper.js +24 -0
  14. package/dist/services/agent-memory-service.js +737 -0
  15. package/dist/services/bot-management-service.js +626 -0
  16. package/dist/services/bridge-service.js +807 -0
  17. package/dist/services/local-ui-service.js +533 -0
  18. package/dist/services/provider-setup-service.js +284 -0
  19. package/dist/services/remote-shell-service.js +97 -0
  20. package/dist/store/file-store.js +690 -0
  21. package/dist/telegram-fetch.js +85 -0
  22. package/dist/types.js +1 -0
  23. package/docs/ARCHITECTURE.md +170 -0
  24. package/docs/COKACDIR_NOTES.md +79 -0
  25. package/docs/ERROR_NORMALIZATION.md +46 -0
  26. package/docs/MINI_APP.md +112 -0
  27. package/docs/MVP.md +108 -0
  28. package/docs/OPERATIONS.md +181 -0
  29. package/docs/RELEASING.md +87 -0
  30. package/docs/SESSION_DIRECTORY_PLAN.md +506 -0
  31. package/package.json +47 -0
  32. package/scripts/bump-version.sh +23 -0
  33. package/scripts/finish-claude-login.sh +48 -0
  34. package/scripts/install-claude.sh +6 -0
  35. package/scripts/install-codex.sh +8 -0
  36. package/scripts/install.ps1 +51 -0
  37. package/scripts/install.sh +101 -0
  38. package/scripts/mock-adapter.sh +7 -0
  39. package/scripts/restart-after-bot-op.sh +118 -0
  40. package/scripts/selftest-telegram-update.mjs +359 -0
  41. package/scripts/start-claude-login.sh +4 -0
  42. package/scripts/start.ps1 +39 -0
  43. package/scripts/start.sh +54 -0
  44. package/scripts/stop.ps1 +40 -0
  45. package/scripts/stop.sh +39 -0
  46. package/tsconfig.json +20 -0
@@ -0,0 +1,241 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { spawnWithPlatformShell } from "./windows-shell.js";
6
+ export class CodexAdapter {
7
+ codexBin;
8
+ timeoutMs;
9
+ sandboxMode;
10
+ constructor(codexBin, timeoutMs, sandboxMode) {
11
+ this.codexBin = codexBin;
12
+ this.timeoutMs = timeoutMs;
13
+ this.sandboxMode = sandboxMode;
14
+ }
15
+ async send(request) {
16
+ const outputPath = await this.createOutputPath();
17
+ const sandboxMode = request.sandboxMode ?? this.sandboxMode;
18
+ const args = request.sessionId
19
+ ? this.buildResumeArgs(request, outputPath, sandboxMode)
20
+ : this.buildExecArgs(request, outputPath, sandboxMode);
21
+ const { stdout, stderr, code, timedOut } = await this.runCodex(args, request.cwd, request.remoteSessionId, request.publicSessionId, request.message);
22
+ try {
23
+ const sessionId = this.extractThreadId(stdout) ?? request.sessionId;
24
+ const output = await this.readOutput(outputPath) || this.extractAgentMessage(stdout);
25
+ if (sessionId && output) {
26
+ return {
27
+ provider: "codex",
28
+ sessionId,
29
+ cwd: request.cwd,
30
+ output,
31
+ };
32
+ }
33
+ if (code !== 0) {
34
+ throw new Error(this.formatProcessError(stdout, stderr, timedOut));
35
+ }
36
+ if (timedOut) {
37
+ throw new Error(this.formatTimeoutError());
38
+ }
39
+ if (!sessionId) {
40
+ throw new Error("Codex response did not include a session id.");
41
+ }
42
+ if (!output) {
43
+ throw new Error("Codex returned an empty response.");
44
+ }
45
+ return {
46
+ provider: "codex",
47
+ sessionId,
48
+ cwd: request.cwd,
49
+ output,
50
+ };
51
+ }
52
+ finally {
53
+ await fs.rm(outputPath, { force: true }).catch(() => undefined);
54
+ await fs.rm(path.dirname(outputPath), { recursive: true, force: true }).catch(() => undefined);
55
+ }
56
+ }
57
+ async createOutputPath() {
58
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "remoteagent-codex-"));
59
+ return path.join(dir, `${randomUUID()}.txt`);
60
+ }
61
+ buildExecArgs(request, outputPath, sandboxMode) {
62
+ const args = [
63
+ "exec",
64
+ "--json",
65
+ "--skip-git-repo-check",
66
+ ];
67
+ if (request.model) {
68
+ args.push("-m", request.model);
69
+ }
70
+ this.appendSandboxArgs(args, sandboxMode);
71
+ args.push("-o", outputPath, "-C", request.cwd);
72
+ this.appendPromptStdinArg(args);
73
+ return args;
74
+ }
75
+ buildResumeArgs(request, outputPath, sandboxMode) {
76
+ const args = [
77
+ "exec",
78
+ "resume",
79
+ "--json",
80
+ "--skip-git-repo-check",
81
+ ];
82
+ if (request.model) {
83
+ args.push("-m", request.model);
84
+ }
85
+ this.appendSandboxArgs(args, sandboxMode);
86
+ args.push("-o", outputPath, request.sessionId);
87
+ this.appendPromptStdinArg(args);
88
+ return args;
89
+ }
90
+ appendPromptStdinArg(args) {
91
+ args.push("--", "-");
92
+ }
93
+ appendSandboxArgs(args, sandboxMode) {
94
+ if (!sandboxMode || sandboxMode === "read-only") {
95
+ return;
96
+ }
97
+ if (sandboxMode === "workspace-write") {
98
+ args.push("--full-auto");
99
+ return;
100
+ }
101
+ if (sandboxMode === "danger-full-access") {
102
+ args.push("--dangerously-bypass-approvals-and-sandbox");
103
+ }
104
+ }
105
+ runCodex(args, cwd, remoteSessionId, publicSessionId, input) {
106
+ return spawnWithPlatformShell(this.codexBin, args, cwd, this.currentTimeoutMs(), input, remoteSessionId, {
107
+ REMOTEAGENT_SESSION_ID: remoteSessionId,
108
+ REMOTEAGENT_PUBLIC_SESSION_ID: publicSessionId ?? "",
109
+ REMOTEAGENT_WORKSPACE: cwd,
110
+ });
111
+ }
112
+ extractThreadId(stdout) {
113
+ for (const line of stdout.split(/\r?\n/)) {
114
+ if (!line.startsWith("{")) {
115
+ continue;
116
+ }
117
+ try {
118
+ const event = JSON.parse(line);
119
+ if (event.thread_id) {
120
+ return event.thread_id;
121
+ }
122
+ }
123
+ catch {
124
+ continue;
125
+ }
126
+ }
127
+ return undefined;
128
+ }
129
+ extractAgentMessage(stdout) {
130
+ let latest = "";
131
+ for (const line of stdout.split(/\r?\n/)) {
132
+ if (!line.startsWith("{")) {
133
+ continue;
134
+ }
135
+ try {
136
+ const event = JSON.parse(line);
137
+ if (event.type === "item.completed" && event.item?.type === "agent_message" && typeof event.item.text === "string") {
138
+ latest = event.item.text.trim();
139
+ }
140
+ }
141
+ catch {
142
+ continue;
143
+ }
144
+ }
145
+ return latest;
146
+ }
147
+ async readOutput(outputPath) {
148
+ return (await fs.readFile(outputPath, "utf8").catch(() => "")).trim();
149
+ }
150
+ formatProcessError(stdout, stderr, timedOut = false) {
151
+ const structured = this.extractStructuredError(stdout, stderr);
152
+ if (structured) {
153
+ return structured;
154
+ }
155
+ const text = this.extractPlainTextError(stdout, stderr);
156
+ if (text) {
157
+ return text;
158
+ }
159
+ return timedOut
160
+ ? this.formatTimeoutError()
161
+ : "Codex execution failed without any output.";
162
+ }
163
+ extractStructuredError(stdout, stderr) {
164
+ const messages = [];
165
+ for (const line of stdout.split(/\r?\n/)) {
166
+ if (!line.startsWith("{")) {
167
+ continue;
168
+ }
169
+ try {
170
+ const event = JSON.parse(line);
171
+ if (event.type === "error" && typeof event.message === "string" && event.message.trim()) {
172
+ messages.push(event.message.trim());
173
+ }
174
+ if (event.is_error) {
175
+ if (Array.isArray(event.errors)) {
176
+ for (const entry of event.errors) {
177
+ if (typeof entry === "string" && entry.trim()) {
178
+ messages.push(entry.trim());
179
+ }
180
+ }
181
+ }
182
+ if (typeof event.error === "string" && event.error.trim()) {
183
+ messages.push(event.error.trim());
184
+ }
185
+ else if (event.error && typeof event.error === "object" && typeof event.error.message === "string" && event.error.message.trim()) {
186
+ messages.push(event.error.message.trim());
187
+ }
188
+ if (typeof event.message === "string" && event.message.trim()) {
189
+ messages.push(event.message.trim());
190
+ }
191
+ }
192
+ }
193
+ catch {
194
+ continue;
195
+ }
196
+ }
197
+ for (const chunk of [stderr, stdout]) {
198
+ const trimmed = chunk.trim();
199
+ if (!trimmed) {
200
+ continue;
201
+ }
202
+ try {
203
+ const parsed = JSON.parse(trimmed);
204
+ if (typeof parsed.message === "string" && parsed.message.trim()) {
205
+ messages.push(parsed.message.trim());
206
+ }
207
+ if (parsed.error && typeof parsed.error.message === "string" && parsed.error.message.trim()) {
208
+ messages.push(parsed.error.message.trim());
209
+ }
210
+ if (Array.isArray(parsed.errors)) {
211
+ for (const entry of parsed.errors) {
212
+ if (typeof entry === "string" && entry.trim()) {
213
+ messages.push(entry.trim());
214
+ }
215
+ }
216
+ }
217
+ }
218
+ catch {
219
+ // ignore non-JSON blocks
220
+ }
221
+ }
222
+ return messages.at(-1);
223
+ }
224
+ extractPlainTextError(stdout, stderr) {
225
+ const sanitize = (value) => value
226
+ .split(/\r?\n/)
227
+ .filter((line) => !line.trim().startsWith("{"))
228
+ .join("\n")
229
+ .trim();
230
+ return [sanitize(stderr), sanitize(stdout)]
231
+ .filter(Boolean)
232
+ .join("\n")
233
+ .trim();
234
+ }
235
+ formatTimeoutError() {
236
+ return `Codex timed out after ${Math.round(this.currentTimeoutMs() / 1000)}s without returning a final reply.`;
237
+ }
238
+ currentTimeoutMs() {
239
+ return typeof this.timeoutMs === "function" ? this.timeoutMs() : this.timeoutMs;
240
+ }
241
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ import process from "node:process";
2
+ import { promisify } from "node:util";
3
+ import { exec as execCallback } from "node:child_process";
4
+ const exec = promisify(execCallback);
5
+ export class ShellAdapter {
6
+ provider;
7
+ command;
8
+ timeoutMs;
9
+ constructor(provider, command, timeoutMs) {
10
+ this.provider = provider;
11
+ this.command = command;
12
+ this.timeoutMs = timeoutMs;
13
+ }
14
+ async send(request) {
15
+ const env = {
16
+ ...process.env,
17
+ BRIDGE_PROVIDER: this.provider,
18
+ BRIDGE_BOT_ID: request.botId ?? "",
19
+ BRIDGE_CHAT_ID: request.chatId,
20
+ BRIDGE_SESSION_ID: request.sessionId ?? "",
21
+ BRIDGE_PUBLIC_SESSION_ID: request.publicSessionId ?? "",
22
+ BRIDGE_CWD: request.cwd,
23
+ BRIDGE_MESSAGE: request.message,
24
+ };
25
+ const { stdout, stderr } = await exec(this.command, {
26
+ env,
27
+ timeout: this.currentTimeoutMs(),
28
+ maxBuffer: 1024 * 1024,
29
+ });
30
+ const output = stdout.trim() || stderr.trim();
31
+ if (!output) {
32
+ throw new Error(`${this.provider} command completed without output`);
33
+ }
34
+ return {
35
+ provider: this.provider,
36
+ sessionId: request.sessionId ?? "",
37
+ cwd: request.cwd,
38
+ output,
39
+ };
40
+ }
41
+ currentTimeoutMs() {
42
+ return typeof this.timeoutMs === "function" ? this.timeoutMs() : this.timeoutMs;
43
+ }
44
+ }
@@ -0,0 +1,111 @@
1
+ import process from "node:process";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { execFile, spawn } from "node:child_process";
5
+ const activeCommands = new Map();
6
+ const CHILD_ENV_BLOCKED_PREFIXES = ["TELEGRAM_"];
7
+ function buildChildEnv(extraEnv) {
8
+ const env = { ...process.env };
9
+ for (const key of Object.keys(env)) {
10
+ if (CHILD_ENV_BLOCKED_PREFIXES.some((prefix) => key.startsWith(prefix))) {
11
+ delete env[key];
12
+ }
13
+ }
14
+ const dataDir = process.env.DATA_DIR?.trim() || path.join(os.homedir(), ".remoteagent");
15
+ env.REMOTEAGENT_DATA_DIR = dataDir;
16
+ env.REMOTEAGENT_SECRET_BIN = path.resolve(process.cwd(), "dist", "secret-helper.js");
17
+ if (extraEnv) {
18
+ Object.assign(env, extraEnv);
19
+ }
20
+ return env;
21
+ }
22
+ export function spawnWithPlatformShell(bin, args, cwd, timeoutMs, input, executionKey, extraEnv) {
23
+ return new Promise((resolve, reject) => {
24
+ const command = process.platform === "win32"
25
+ ? spawn("cmd.exe", ["/d", "/c", "call", bin, ...args], {
26
+ cwd,
27
+ env: buildChildEnv(extraEnv),
28
+ })
29
+ : spawn(bin, args, {
30
+ cwd,
31
+ env: buildChildEnv(extraEnv),
32
+ detached: true,
33
+ });
34
+ if (executionKey) {
35
+ activeCommands.set(executionKey, command);
36
+ }
37
+ let stdout = "";
38
+ let stderr = "";
39
+ let timedOut = false;
40
+ const timer = setTimeout(() => {
41
+ timedOut = true;
42
+ terminateProcessTree(command);
43
+ }, timeoutMs);
44
+ command.stdout.on("data", (chunk) => {
45
+ stdout += chunk.toString();
46
+ });
47
+ command.stderr.on("data", (chunk) => {
48
+ stderr += chunk.toString();
49
+ });
50
+ command.on("error", (error) => {
51
+ clearTimeout(timer);
52
+ if (executionKey && activeCommands.get(executionKey) === command) {
53
+ activeCommands.delete(executionKey);
54
+ }
55
+ reject(error);
56
+ });
57
+ if (input !== undefined) {
58
+ command.stdin.write(input);
59
+ }
60
+ command.stdin.end();
61
+ command.on("close", (code) => {
62
+ clearTimeout(timer);
63
+ if (executionKey && activeCommands.get(executionKey) === command) {
64
+ activeCommands.delete(executionKey);
65
+ }
66
+ resolve({ stdout, stderr, code, timedOut });
67
+ });
68
+ });
69
+ }
70
+ export function stopSpawnedExecution(executionKey) {
71
+ const command = activeCommands.get(executionKey);
72
+ if (!command) {
73
+ return false;
74
+ }
75
+ return terminateProcessTree(command);
76
+ }
77
+ export function terminateAllSpawnedExecutions() {
78
+ let stopped = 0;
79
+ for (const [key, command] of activeCommands.entries()) {
80
+ if (terminateProcessTree(command)) {
81
+ stopped += 1;
82
+ }
83
+ activeCommands.delete(key);
84
+ }
85
+ return stopped;
86
+ }
87
+ function terminateProcessTree(command) {
88
+ if (!command.pid) {
89
+ return false;
90
+ }
91
+ try {
92
+ if (process.platform === "win32") {
93
+ execFile("taskkill", ["/pid", String(command.pid), "/t", "/f"], () => undefined);
94
+ }
95
+ else {
96
+ process.kill(-command.pid, "SIGTERM");
97
+ setTimeout(() => {
98
+ try {
99
+ process.kill(-command.pid, "SIGKILL");
100
+ }
101
+ catch {
102
+ // Process group already exited.
103
+ }
104
+ }, 3000).unref();
105
+ }
106
+ return true;
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }