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,101 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ DATA_DIR="${DATA_DIR:-$HOME/.remoteagent}"
5
+ ENV_FILE="$DATA_DIR/.env"
6
+ PACKAGE_NAME="${REMOTEAGENT_PACKAGE_NAME:-appback-remoteagent}"
7
+ PACKAGE_VERSION="${REMOTEAGENT_VERSION:-latest}"
8
+
9
+ script_dir() {
10
+ local source="$1"
11
+ while [ -L "$source" ]; do
12
+ local dir
13
+ dir="$(cd -P "$(dirname "$source")" && pwd)"
14
+ source="$(readlink "$source")"
15
+ case "$source" in
16
+ /*) ;;
17
+ *) source="$dir/$source" ;;
18
+ esac
19
+ done
20
+ cd -P "$(dirname "$source")" && pwd
21
+ }
22
+
23
+ find_global_package_root() {
24
+ local global_root
25
+ global_root="$(npm root -g)"
26
+ if [ -d "$global_root/$PACKAGE_NAME" ]; then
27
+ printf '%s\n' "$global_root/$PACKAGE_NAME"
28
+ return 0
29
+ fi
30
+ return 1
31
+ }
32
+
33
+ SCRIPT_SOURCE="${BASH_SOURCE[0]:-}"
34
+ if [ -n "$SCRIPT_SOURCE" ] && [ -f "$SCRIPT_SOURCE" ]; then
35
+ ROOT_DIR="$(cd "$(script_dir "$SCRIPT_SOURCE")/.." && pwd)"
36
+ else
37
+ if ! command -v npm >/dev/null 2>&1; then
38
+ echo "npm is required to install RemoteAgent." >&2
39
+ exit 1
40
+ fi
41
+ echo "Installing $PACKAGE_NAME@$PACKAGE_VERSION from npm..."
42
+ npm install -g "$PACKAGE_NAME@$PACKAGE_VERSION"
43
+ ROOT_DIR="$(find_global_package_root)"
44
+ fi
45
+
46
+ mkdir -p "$DATA_DIR" "$DATA_DIR/logs"
47
+
48
+ if [ ! -f "$ENV_FILE" ]; then
49
+ cp "$ROOT_DIR/.env.example" "$ENV_FILE"
50
+ echo "Created $ENV_FILE"
51
+ fi
52
+
53
+ upsert_env() {
54
+ local key="$1"
55
+ local value="$2"
56
+ python3 - "$ENV_FILE" "$key" "$value" <<'PY'
57
+ from pathlib import Path
58
+ import sys
59
+ path = Path(sys.argv[1])
60
+ key = sys.argv[2]
61
+ value = sys.argv[3]
62
+ text = path.read_text() if path.exists() else ""
63
+ lines = text.splitlines()
64
+ out = []
65
+ updated = False
66
+ for line in lines:
67
+ if line.startswith(f"{key}="):
68
+ out.append(f"{key}={value}")
69
+ updated = True
70
+ else:
71
+ out.append(line)
72
+ if not updated:
73
+ out.append(f"{key}={value}")
74
+ path.write_text("\n".join(out) + "\n")
75
+ PY
76
+ }
77
+
78
+ upsert_env "SETUP_COMMAND_TIMEOUT_MS" "600000"
79
+ upsert_env "CODEX_INSTALL_COMMAND" "$ROOT_DIR/scripts/install-codex.sh"
80
+ upsert_env "CLAUDE_INSTALL_COMMAND" "$ROOT_DIR/scripts/install-claude.sh"
81
+ upsert_env "CLAUDE_LOGIN_START_COMMAND" "$ROOT_DIR/scripts/start-claude-login.sh"
82
+ upsert_env "CLAUDE_LOGIN_FINISH_COMMAND" "$ROOT_DIR/scripts/finish-claude-login.sh"
83
+ upsert_env "BOT_RESTART_HELPER_PATH" "$ROOT_DIR/scripts/restart-after-bot-op.sh"
84
+
85
+ cat > "$DATA_DIR/start-remoteagent.sh" <<EOF
86
+ #!/usr/bin/env bash
87
+ DATA_DIR="$DATA_DIR" "$ROOT_DIR/scripts/start.sh"
88
+ EOF
89
+ chmod +x "$DATA_DIR/start-remoteagent.sh"
90
+
91
+ cat > "$DATA_DIR/stop-remoteagent.sh" <<EOF
92
+ #!/usr/bin/env bash
93
+ DATA_DIR="$DATA_DIR" "$ROOT_DIR/scripts/stop.sh"
94
+ EOF
95
+ chmod +x "$DATA_DIR/stop-remoteagent.sh"
96
+
97
+ echo
98
+ echo "RemoteAgent is installed."
99
+ echo "Provider install/login hooks were configured in $ENV_FILE"
100
+ echo "Set TELEGRAM_BOT_TOKEN or TELEGRAM_BOT_TOKENS in $ENV_FILE"
101
+ echo "Start with: remoteagent-start"
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ printf '[mock:%s][session:%s] %s\n' \
5
+ "${BRIDGE_PROVIDER}" \
6
+ "${BRIDGE_SESSION_ID}" \
7
+ "${BRIDGE_MESSAGE}"
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env bash
2
+ set -u
3
+
4
+ SERVICE_NAME="${1:-remoteagent}"
5
+ DATA_DIR="${2:?data dir is required}"
6
+ PENDING_FILE="$DATA_DIR/pending-bot-operation.json"
7
+ ENV_FILE="$DATA_DIR/.env"
8
+ START_SCRIPT="$DATA_DIR/start-remoteagent.sh"
9
+ STOP_SCRIPT="$DATA_DIR/stop-remoteagent.sh"
10
+
11
+ log() {
12
+ printf '[bot-op] %s\n' "$1" >&2
13
+ }
14
+
15
+ update_pending_status() {
16
+ local status="$1"
17
+ local reason="$2"
18
+ python3 - "$PENDING_FILE" "$status" "$reason" <<'PY'
19
+ import json
20
+ import sys
21
+ from pathlib import Path
22
+ pending_path = Path(sys.argv[1])
23
+ status = sys.argv[2]
24
+ reason = sys.argv[3]
25
+ if not pending_path.exists():
26
+ raise SystemExit(0)
27
+ obj = json.loads(pending_path.read_text())
28
+ obj['status'] = status
29
+ obj['reason'] = reason
30
+ pending_path.write_text(json.dumps(obj, indent=2) + '\n')
31
+ PY
32
+ }
33
+
34
+ restore_backup() {
35
+ python3 - "$PENDING_FILE" "$ENV_FILE" <<'PY'
36
+ import json
37
+ import shutil
38
+ import sys
39
+ from pathlib import Path
40
+ pending_path = Path(sys.argv[1])
41
+ env_path = Path(sys.argv[2])
42
+ if not pending_path.exists():
43
+ raise SystemExit(0)
44
+ obj = json.loads(pending_path.read_text())
45
+ backup = obj.get('backupEnvPath')
46
+ if backup and Path(backup).exists():
47
+ shutil.copyfile(backup, env_path)
48
+ PY
49
+ }
50
+
51
+ wait_until_active() {
52
+ local attempts=10
53
+ local i=0
54
+ while [ "$i" -lt "$attempts" ]; do
55
+ if systemctl is-active --quiet "$SERVICE_NAME"; then
56
+ return 0
57
+ fi
58
+ sleep 1
59
+ i=$((i + 1))
60
+ done
61
+ return 1
62
+ }
63
+
64
+ wait_until_pid_running() {
65
+ local pid_file="$1"
66
+ local attempts=20
67
+ local i=0
68
+
69
+ while [ "$i" -lt "$attempts" ]; do
70
+ if [ -f "$pid_file" ]; then
71
+ local pid
72
+ pid="$(cat "$pid_file" 2>/dev/null || true)"
73
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
74
+ return 0
75
+ fi
76
+ fi
77
+ sleep 0.5
78
+ i=$((i + 1))
79
+ done
80
+
81
+ return 1
82
+ }
83
+
84
+ restart_user_runtime() {
85
+ if [ ! -x "$STOP_SCRIPT" ] || [ ! -x "$START_SCRIPT" ]; then
86
+ return 1
87
+ fi
88
+
89
+ DATA_DIR="$DATA_DIR" "$STOP_SCRIPT" || true
90
+ DATA_DIR="$DATA_DIR" "$START_SCRIPT" || return 1
91
+ wait_until_pid_running "$DATA_DIR/remoteagent.pid"
92
+ }
93
+
94
+ sleep 1
95
+
96
+ if command -v systemctl >/dev/null 2>&1 && systemctl cat "$SERVICE_NAME" >/dev/null 2>&1; then
97
+ if systemctl restart "$SERVICE_NAME" && wait_until_active; then
98
+ exit 0
99
+ fi
100
+ else
101
+ if restart_user_runtime; then
102
+ exit 0
103
+ fi
104
+ fi
105
+
106
+ reason="service did not become active after restart"
107
+ log "$reason"
108
+ update_pending_status "rolled_back" "$reason"
109
+ restore_backup
110
+
111
+ if command -v systemctl >/dev/null 2>&1 && systemctl cat "$SERVICE_NAME" >/dev/null 2>&1; then
112
+ systemctl restart "$SERVICE_NAME" || true
113
+ wait_until_active || true
114
+ else
115
+ restart_user_runtime || true
116
+ fi
117
+
118
+ exit 0
@@ -0,0 +1,359 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import process from "node:process";
6
+
7
+ const root = path.resolve(new URL("..", import.meta.url).pathname);
8
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "remoteagent-telegram-inject-"));
9
+ const dataDir = path.join(tmp, "data");
10
+ const workspace = path.join(tmp, "workspace");
11
+ const workspaceRoot = path.join(tmp, "workspaces");
12
+ const binDir = path.join(tmp, "bin");
13
+ const telegramCalls = path.join(tmp, "telegram-calls.jsonl");
14
+
15
+ await fs.mkdir(workspace, { recursive: true });
16
+ await fs.mkdir(workspaceRoot, { recursive: true });
17
+ await fs.mkdir(binDir, { recursive: true });
18
+
19
+ await fs.writeFile(path.join(binDir, "curl"), `#!/usr/bin/env bash
20
+ set -euo pipefail
21
+ method="unknown"
22
+ text=""
23
+ chat_id=""
24
+ for arg in "$@"; do
25
+ case "$arg" in
26
+ https://api.telegram.org/bot*/sendMessage) method="sendMessage" ;;
27
+ https://api.telegram.org/bot*/editMessageText) method="editMessageText" ;;
28
+ https://api.telegram.org/bot*/deleteMessage) method="deleteMessage" ;;
29
+ https://api.telegram.org/bot*/sendDocument) method="sendDocument" ;;
30
+ chat_id=*) chat_id="\${arg#chat_id=}" ;;
31
+ text=*) text="\${arg#text=}" ;;
32
+ esac
33
+ done
34
+ text_b64="$(printf '%s' "$text" | base64 -w 0)"
35
+ printf '%s\\t%s\\t%s\\n' "$method" "$chat_id" "$text_b64" >> ${JSON.stringify(telegramCalls)}
36
+ case "$method" in
37
+ sendMessage|editMessageText)
38
+ printf '{"ok":true,"result":{"message_id":1001}}'
39
+ ;;
40
+ deleteMessage)
41
+ printf '{"ok":true,"result":true}'
42
+ ;;
43
+ sendDocument)
44
+ printf '{"ok":true,"result":{"message_id":1002,"document":{"file_id":"fake"}}}'
45
+ ;;
46
+ *)
47
+ printf '{"ok":true,"result":true}'
48
+ ;;
49
+ esac
50
+ `, "utf8");
51
+ await fs.chmod(path.join(binDir, "curl"), 0o755);
52
+
53
+ process.env.PATH = `${binDir}:${process.env.PATH ?? ""}`;
54
+ process.env.DATA_DIR = dataDir;
55
+ process.env.DEFAULT_WORKSPACE = workspace;
56
+ process.env.WORKSPACE_ROOT = workspaceRoot;
57
+ process.env.TELEGRAM_BOT_TOKEN = "000000:test-token";
58
+ process.env.TELEGRAM_OWNER_ID = "111";
59
+ process.env.TELEGRAM_MESSAGE_BATCH_MS = "600000";
60
+ process.env.TELEGRAM_AUTO_PROGRESS_MAX_TURNS = "1";
61
+ process.env.TELEGRAM_EMPTY_RESPONSE_RETRIES = "0";
62
+ process.env.TELEGRAM_RETRYABLE_ERROR_RETRIES = "0";
63
+ process.env.LOCAL_UI_ENABLED = "false";
64
+
65
+ const [{ createBot }, { BridgeService }, { BotManagementService }, { FileStore }, { AgentMemoryService }] = await Promise.all([
66
+ import(path.join(root, "dist", "bot.js")),
67
+ import(path.join(root, "dist", "services", "bridge-service.js")),
68
+ import(path.join(root, "dist", "services", "bot-management-service.js")),
69
+ import(path.join(root, "dist", "store", "file-store.js")),
70
+ import(path.join(root, "dist", "services", "agent-memory-service.js")),
71
+ ]);
72
+
73
+ const providerCalls = [];
74
+ let providerMode = "success";
75
+ let untaggedIntentCalls = 0;
76
+ let missingEvidenceCalls = 0;
77
+ const provider = {
78
+ async send(request) {
79
+ providerCalls.push(request);
80
+ if (providerMode === "timeout") {
81
+ throw new Error("Codex timed out after 600s without returning a final reply.");
82
+ }
83
+ if (providerMode === "untagged-intent") {
84
+ untaggedIntentCalls += 1;
85
+ return {
86
+ provider: "codex",
87
+ sessionId: request.sessionId || "mock-thread",
88
+ publicSessionId: request.publicSessionId,
89
+ cwd: request.cwd,
90
+ output: untaggedIntentCalls === 1
91
+ ? "계속 진행해서 확인하겠습니다."
92
+ : "REPORT:result\nuntagged intent recovered",
93
+ };
94
+ }
95
+ if (providerMode === "missing-evidence") {
96
+ missingEvidenceCalls += 1;
97
+ return {
98
+ provider: "codex",
99
+ sessionId: request.sessionId || "mock-thread",
100
+ publicSessionId: request.publicSessionId,
101
+ cwd: request.cwd,
102
+ output: missingEvidenceCalls === 1
103
+ ? "REPORT:result\n수정 완료했습니다."
104
+ : "REPORT:result\n수정 완료했습니다.\n\n근거:\n- 변경 파일: `src/example.ts`\n- 검증: `npm run check` 통과",
105
+ };
106
+ }
107
+ return {
108
+ provider: "codex",
109
+ sessionId: request.sessionId || "mock-thread",
110
+ publicSessionId: request.publicSessionId,
111
+ cwd: request.cwd,
112
+ output: "REPORT:result\\nmock provider completed",
113
+ };
114
+ },
115
+ };
116
+
117
+ const store = new FileStore(dataDir, "codex");
118
+ await store.init();
119
+ const bridge = new BridgeService(
120
+ store,
121
+ { codex: provider },
122
+ workspace,
123
+ workspaceRoot,
124
+ (name) => name === "codex",
125
+ "codex",
126
+ "workspace-write",
127
+ );
128
+ const botManagement = new BotManagementService(dataDir, undefined, undefined);
129
+ const bot = createBot("000000:test-token", bridge, botManagement, {
130
+ id: 999001,
131
+ is_bot: true,
132
+ first_name: "RemoteAgent Test",
133
+ username: "remoteagent_test_bot",
134
+ });
135
+
136
+ const injectedBot = bot;
137
+ let updateId = 1000;
138
+ let messageId = 2000;
139
+ const now = () => Math.floor(Date.now() / 1000);
140
+
141
+ function commandEntity(text) {
142
+ const first = text.split(/\s+/, 1)[0] ?? text;
143
+ return [{ type: "bot_command", offset: 0, length: first.length }];
144
+ }
145
+
146
+ function update(text) {
147
+ return {
148
+ update_id: updateId++,
149
+ message: {
150
+ message_id: messageId++,
151
+ date: now(),
152
+ chat: { id: 111222333, type: "private", first_name: "Tester", username: "tester" },
153
+ from: { id: 111, is_bot: false, first_name: "Tester", username: "tester" },
154
+ text,
155
+ entities: text.startsWith("/") ? commandEntity(text) : undefined,
156
+ },
157
+ };
158
+ }
159
+
160
+ async function send(text) {
161
+ await injectedBot.handleUpdates([update(text)]);
162
+ }
163
+
164
+ await send("/start codex");
165
+ await send("/option retry 6");
166
+ await send("/option timeout 600");
167
+ await send("/option intent 4");
168
+ await send("같은 값을 봐야하는데 로직문제네? 확인해줘\\n이미 수정되어 있을 수 있어.\\n나한테 수정했다고 보고했었거든");
169
+ await send("/state");
170
+
171
+ const state = JSON.parse(await fs.readFile(path.join(dataDir, "state.json"), "utf8"));
172
+ const sessions = Object.values(state.sessions);
173
+ if (sessions.length !== 1) {
174
+ throw new Error(`Expected one session, got ${sessions.length}`);
175
+ }
176
+ const session = sessions[0];
177
+ if (providerCalls.length !== 0) {
178
+ throw new Error(`Provider should not run before batch flush, got ${providerCalls.length} calls`);
179
+ }
180
+ const envText = await fs.readFile(path.join(dataDir, ".env"), "utf8");
181
+ if (!/^TELEGRAM_AUTO_PROGRESS_MAX_TURNS=6$/m.test(envText)) {
182
+ throw new Error(`Option command did not persist retry limit to .env: ${envText}`);
183
+ }
184
+ if (!/^COMMAND_TIMEOUT_MS=600000$/m.test(envText)) {
185
+ throw new Error(`Option command did not persist command timeout to .env: ${envText}`);
186
+ }
187
+ if (!/^TELEGRAM_UNTAGGED_INTENT_RETRIES=4$/m.test(envText)) {
188
+ throw new Error(`Option command did not persist untagged intent retry limit to .env: ${envText}`);
189
+ }
190
+
191
+ const memory = new AgentMemoryService(dataDir);
192
+ const developmentSession = {
193
+ ...session,
194
+ sessionId: "selftest-development-session",
195
+ publicId: "SDEV",
196
+ workspace: path.join(tmp, "dev-workspace"),
197
+ };
198
+ await memory.recordInstruction(developmentSession, "그럼 기프티쇼 개발 진행해");
199
+ const developmentCurrent = await fs.readFile(path.join(dataDir, "managed", "sessions", "SDEV", "current.md"), "utf8");
200
+ if (!/기프티쇼 개발 진행해/.test(developmentCurrent) || /Manage work by the TODO list/.test(developmentCurrent)) {
201
+ throw new Error(`Development instruction was not stored as session state: ${developmentCurrent}`);
202
+ }
203
+ const developmentContext = await memory.formatProviderContext(developmentSession);
204
+ if (/Task TODO: none|context only|Manage work by the TODO list/.test(developmentContext)) {
205
+ throw new Error(`Provider context still contains TODO gate language: ${developmentContext}`);
206
+ }
207
+
208
+ const legacySession = {
209
+ ...session,
210
+ sessionId: "selftest-legacy-session",
211
+ publicId: "SLEG",
212
+ workspace: path.join(tmp, "legacy-workspace"),
213
+ };
214
+ const legacyDir = path.join(dataDir, "managed", "sessions", "SLEG");
215
+ await fs.mkdir(legacyDir, { recursive: true });
216
+ await fs.writeFile(path.join(legacyDir, "current.md"), [
217
+ "# Session State",
218
+ "",
219
+ "session: SLEG",
220
+ "updatedAt: 2026-06-09T00:00:00.000Z",
221
+ "",
222
+ "## Latest User Instruction",
223
+ "그럼 기프티쇼 개발 진행해",
224
+ "",
225
+ "## Harness Rule",
226
+ "RemoteAgent records this as session state.",
227
+ "",
228
+ ].join("\n"), "utf8");
229
+ await fs.writeFile(path.join(legacyDir, "todo.json"), JSON.stringify({ createdAt: "", updatedAt: "", items: [] }, null, 2), "utf8");
230
+ await memory.recordInstruction(legacySession, "진행해");
231
+ const recoveredTodo = JSON.parse(await fs.readFile(path.join(legacyDir, "todo.json"), "utf8"));
232
+ const recoveredActive = recoveredTodo.items.filter((item) => item.status === "in_progress" || item.status === "pending");
233
+ if (recoveredActive.length !== 0) {
234
+ throw new Error(`Continuation unexpectedly created TODO gate items: ${JSON.stringify(recoveredTodo, null, 2)}`);
235
+ }
236
+
237
+ const calls = (await fs.readFile(telegramCalls, "utf8"))
238
+ .trim()
239
+ .split("\n")
240
+ .filter(Boolean)
241
+ .map((line) => {
242
+ const [method, chatId, textB64 = ""] = line.split("\t");
243
+ return {
244
+ method,
245
+ chat_id: chatId,
246
+ text: Buffer.from(textB64, "base64").toString("utf8"),
247
+ };
248
+ });
249
+ if (!calls.some((call) => call.method === "sendMessage" && /Session state for S001/.test(call.text))) {
250
+ throw new Error(`Did not see state status reply. Calls: ${JSON.stringify(calls, null, 2)}`);
251
+ }
252
+ if (!calls.some((call) => call.method === "sendMessage" && /Set automatic continuation retry limit to 6/.test(call.text))) {
253
+ throw new Error(`Did not see option retry acknowledgement. Calls: ${JSON.stringify(calls, null, 2)}`);
254
+ }
255
+ if (!calls.some((call) => call.method === "sendMessage" && /Set provider execution timeout to 600s/.test(call.text))) {
256
+ throw new Error(`Did not see option timeout acknowledgement. Calls: ${JSON.stringify(calls, null, 2)}`);
257
+ }
258
+ if (!calls.some((call) => call.method === "sendMessage" && /Set untagged intent retry limit to 4/.test(call.text))) {
259
+ throw new Error(`Did not see option intent acknowledgement. Calls: ${JSON.stringify(calls, null, 2)}`);
260
+ }
261
+ if (calls.some((call) => /미완료 TODO|\/task|새 작업으로 접수/.test(call.text))) {
262
+ throw new Error(`Task gate language leaked to Telegram replies. Calls: ${JSON.stringify(calls, null, 2)}`);
263
+ }
264
+
265
+ providerMode = "timeout";
266
+ await send("/batch start");
267
+ await send("timeout regression test");
268
+ await send("/batch send");
269
+
270
+ const timeoutCalls = (await fs.readFile(telegramCalls, "utf8"))
271
+ .trim()
272
+ .split("\n")
273
+ .filter(Boolean)
274
+ .map((line) => {
275
+ const [method, chatId, textB64 = ""] = line.split("\t");
276
+ return {
277
+ method,
278
+ chat_id: chatId,
279
+ text: Buffer.from(textB64, "base64").toString("utf8"),
280
+ };
281
+ });
282
+ if (!timeoutCalls.some((call) => /Codex 실행이 600초 안에 최종 응답을 반환하지 않아 중단했습니다/.test(call.text))) {
283
+ throw new Error(`Did not see provider timeout final message. Calls: ${JSON.stringify(timeoutCalls, null, 2)}`);
284
+ }
285
+ if (timeoutCalls.some((call) => /응답이 지연되어 .*다시 시도합니다/.test(call.text))) {
286
+ throw new Error(`Provider timeout should not be automatically retried. Calls: ${JSON.stringify(timeoutCalls, null, 2)}`);
287
+ }
288
+
289
+ providerMode = "untagged-intent";
290
+ await send("/batch start");
291
+ await send("untagged intent regression test");
292
+ await send("/batch send");
293
+
294
+ const untaggedCalls = (await fs.readFile(telegramCalls, "utf8"))
295
+ .trim()
296
+ .split("\n")
297
+ .filter(Boolean)
298
+ .map((line) => {
299
+ const [method, chatId, textB64 = ""] = line.split("\t");
300
+ return {
301
+ method,
302
+ chat_id: chatId,
303
+ text: Buffer.from(textB64, "base64").toString("utf8"),
304
+ };
305
+ });
306
+ if (untaggedIntentCalls !== 2) {
307
+ throw new Error(`Expected untagged intent response to be retried once, got ${untaggedIntentCalls}`);
308
+ }
309
+ if (!untaggedCalls.some((call) => /untagged intent recovered/.test(call.text))) {
310
+ throw new Error(`Did not see recovered result after untagged intent retry. Calls: ${JSON.stringify(untaggedCalls, null, 2)}`);
311
+ }
312
+ if (untaggedCalls.some((call) => call.method === "sendMessage" && /^계속 진행해서 확인하겠습니다\.$/.test(call.text.trim()))) {
313
+ throw new Error(`Untagged intent-only response leaked as final Telegram message. Calls: ${JSON.stringify(untaggedCalls, null, 2)}`);
314
+ }
315
+
316
+ providerMode = "missing-evidence";
317
+ await send("/batch start");
318
+ await send("missing evidence regression test");
319
+ await send("/batch send");
320
+
321
+ const evidenceCalls = (await fs.readFile(telegramCalls, "utf8"))
322
+ .trim()
323
+ .split("\n")
324
+ .filter(Boolean)
325
+ .map((line) => {
326
+ const [method, chatId, textB64 = ""] = line.split("\t");
327
+ return {
328
+ method,
329
+ chat_id: chatId,
330
+ text: Buffer.from(textB64, "base64").toString("utf8"),
331
+ };
332
+ });
333
+ if (missingEvidenceCalls !== 2) {
334
+ throw new Error(`Expected missing evidence result to be retried once, got ${missingEvidenceCalls}`);
335
+ }
336
+ if (!evidenceCalls.some((call) => /변경 파일: `src\/example\.ts`/.test(call.text) && /`npm run check` 통과/.test(call.text))) {
337
+ throw new Error(`Did not see recovered result with concrete evidence. Calls: ${JSON.stringify(evidenceCalls, null, 2)}`);
338
+ }
339
+ if (evidenceCalls.some((call) => call.method === "sendMessage" && /^수정 완료했습니다\.$/.test(call.text.trim()))) {
340
+ throw new Error(`Evidence-free completion leaked as final Telegram message. Calls: ${JSON.stringify(evidenceCalls, null, 2)}`);
341
+ }
342
+
343
+ console.log(JSON.stringify({
344
+ ok: true,
345
+ dataDir,
346
+ session: session.publicId,
347
+ developmentState: /기프티쇼 개발 진행해/.test(developmentCurrent),
348
+ recoveredTodoItems: recoveredActive.length,
349
+ retryOption: 6,
350
+ timeoutOptionMs: 600000,
351
+ intentRetryOption: 4,
352
+ providerCalls: providerCalls.length,
353
+ untaggedIntentCalls,
354
+ missingEvidenceCalls,
355
+ timeoutFinalMessage: true,
356
+ telegramSendMessages: evidenceCalls.filter((call) => call.method === "sendMessage").length,
357
+ }, null, 2));
358
+
359
+ process.exit(0);
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ source "$HOME/.profile" >/dev/null 2>&1 || true
4
+ timeout 15s claude auth login || true
@@ -0,0 +1,39 @@
1
+ $ErrorActionPreference = "Stop"
2
+
3
+ $root = Split-Path -Parent $PSScriptRoot
4
+ if ($root -like "\\wsl.localhost\*") {
5
+ throw "Windows start expects the repository on a local Windows path. Clone RemoteAgent under C:\ or another Windows drive, or use scripts/start.sh inside Linux."
6
+ }
7
+
8
+ $dataDir = if ($env:DATA_DIR) { $env:DATA_DIR } else { Join-Path $HOME ".remoteagent" }
9
+ $logsDir = Join-Path $dataDir "logs"
10
+ $pidFile = Join-Path $dataDir "remoteagent.pid"
11
+ $stdoutLog = Join-Path $logsDir "agent.stdout.log"
12
+ $stderrLog = Join-Path $logsDir "agent.stderr.log"
13
+
14
+ New-Item -ItemType Directory -Force -Path $dataDir, $logsDir | Out-Null
15
+
16
+ if (Test-Path $pidFile) {
17
+ $existingPid = Get-Content $pidFile -ErrorAction SilentlyContinue
18
+ if ($existingPid -and (Get-Process -Id $existingPid -ErrorAction SilentlyContinue)) {
19
+ Write-Host "RemoteAgent is already running with PID $existingPid"
20
+ exit 0
21
+ }
22
+ }
23
+
24
+ $escapedDataDir = $dataDir.Replace('"', '\"')
25
+ $escapedRoot = $root.Replace('"', '\"')
26
+ $command = "pushd `"$escapedRoot`" && set DATA_DIR=$escapedDataDir && npm run start && popd"
27
+
28
+ $proc = Start-Process -FilePath "cmd.exe" `
29
+ -ArgumentList @("/d", "/c", $command) `
30
+ -WorkingDirectory $root `
31
+ -RedirectStandardOutput $stdoutLog `
32
+ -RedirectStandardError $stderrLog `
33
+ -WindowStyle Hidden `
34
+ -PassThru
35
+
36
+ Set-Content -Path $pidFile -Value $proc.Id
37
+
38
+ Write-Host "RemoteAgent started with PID $($proc.Id)"
39
+ Write-Host "Logs: $stdoutLog"
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ script_dir() {
5
+ local source="$1"
6
+ while [ -L "$source" ]; do
7
+ local dir
8
+ dir="$(cd -P "$(dirname "$source")" && pwd)"
9
+ source="$(readlink "$source")"
10
+ case "$source" in
11
+ /*) ;;
12
+ *) source="$dir/$source" ;;
13
+ esac
14
+ done
15
+ cd -P "$(dirname "$source")" && pwd
16
+ }
17
+
18
+ ROOT_DIR="$(cd "$(script_dir "${BASH_SOURCE[0]}")/.." && pwd)"
19
+ DATA_DIR="${DATA_DIR:-$HOME/.remoteagent}"
20
+ PID_FILE="$DATA_DIR/remoteagent.pid"
21
+ LOG_FILE="$DATA_DIR/logs/agent.log"
22
+ NODE_BIN="${NODE_BIN:-node}"
23
+ ENV_FILE="$DATA_DIR/.env"
24
+
25
+ mkdir -p "$DATA_DIR" "$(dirname "$LOG_FILE")"
26
+
27
+ if command -v systemctl >/dev/null 2>&1 && systemctl cat remoteagent >/dev/null 2>&1; then
28
+ if sudo -n true >/dev/null 2>&1; then
29
+ sudo systemctl start remoteagent
30
+ sudo systemctl status remoteagent --no-pager -l --lines=5 || true
31
+ exit 0
32
+ fi
33
+
34
+ echo "remoteagent.service is installed. Use sudo systemctl start remoteagent." >&2
35
+ exit 1
36
+ fi
37
+
38
+ if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
39
+ echo "RemoteAgent is already running with PID $(cat "$PID_FILE")"
40
+ exit 0
41
+ fi
42
+
43
+ if [ -f "$ENV_FILE" ]; then
44
+ set -a
45
+ # shellcheck disable=SC1090
46
+ . "$ENV_FILE"
47
+ set +a
48
+ fi
49
+
50
+ nohup env DATA_DIR="$DATA_DIR" "$NODE_BIN" "$ROOT_DIR/dist/index.js" >>"$LOG_FILE" 2>&1 &
51
+ echo $! > "$PID_FILE"
52
+
53
+ echo "RemoteAgent started with PID $(cat "$PID_FILE")"
54
+ echo "Logs: $LOG_FILE"