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,506 @@
1
+ # Session Directory Plan
2
+
3
+ ## Decision
4
+
5
+ RemoteAgent should move to a hybrid model:
6
+
7
+ - session-owned storage as the source of truth
8
+ - bot-scoped bindings as the entry point
9
+
10
+ In practice:
11
+
12
+ - directories are owned by RemoteAgent sessions
13
+ - each Telegram bot keeps its own chat bindings and defaults
14
+ - a bot/chat points to a current session
15
+
16
+ This matches the core product goal better than bot-owned directories because the main UX target is continuing the same work across:
17
+
18
+ - work PC UI
19
+ - Telegram bot A
20
+ - Telegram bot B
21
+
22
+ without fragmenting one task into separate storage roots.
23
+
24
+ ## Why not bot-owned directories?
25
+
26
+ Bot-owned directories make sense for policy isolation, but they are a weaker fit for the primary workflow:
27
+
28
+ 1. the same work should survive switching between PC and Telegram
29
+ 2. the same work may later move between different Telegram bots
30
+ 3. future PC UI should not be forced to pretend to be a bot
31
+
32
+ If storage is bot-owned, the system tends to duplicate:
33
+
34
+ - event history
35
+ - session metadata
36
+ - attachments
37
+ - provider bindings
38
+
39
+ and cross-bot continuation becomes an explicit migration instead of a normal action.
40
+
41
+ ## Recommended model
42
+
43
+ ### Source of truth
44
+
45
+ The source of truth should be the RemoteAgent session.
46
+
47
+ Each session owns:
48
+
49
+ - workspace metadata
50
+ - provider bindings
51
+ - event history
52
+ - attachments and exports
53
+ - derived session state
54
+
55
+ ### Bot responsibility
56
+
57
+ Each Telegram bot should own:
58
+
59
+ - bot identity and policy
60
+ - bot-level defaults
61
+ - mappings from `(platform, bot, chat)` to `sessionId`
62
+
63
+ That gives the desired UX:
64
+
65
+ - users feel each bot is separately managed
66
+ - the underlying work remains session-centric
67
+
68
+ ## Target data model
69
+
70
+ ### 1. BotProfile
71
+
72
+ Represents one configured Telegram bot.
73
+
74
+ Suggested fields:
75
+
76
+ ```ts
77
+ type BotProfile = {
78
+ botId: string; // internal id, usually Telegram bot user id or username
79
+ platform: "telegram";
80
+ username?: string; // e.g. codex_remoteagent_bot
81
+ tokenLabel?: string; // optional human-friendly label, never the secret token
82
+ ownerUserId?: string;
83
+ enabled: boolean;
84
+ defaults: {
85
+ mode: BridgeMode;
86
+ workspace?: string;
87
+ provider?: Provider;
88
+ codexSandboxMode?: CodexSandboxMode;
89
+ };
90
+ policy: {
91
+ allowRemoteShell: boolean;
92
+ privateOnly: boolean;
93
+ };
94
+ createdAt: string;
95
+ updatedAt: string;
96
+ };
97
+ ```
98
+
99
+ ### 2. ChannelBinding
100
+
101
+ Represents one external client channel bound to a RemoteAgent session.
102
+
103
+ Suggested fields:
104
+
105
+ ```ts
106
+ type ChannelBinding = {
107
+ bindingId: string;
108
+ platform: "telegram" | "pc-ui";
109
+ botId?: string; // required for telegram, omitted for local UI
110
+ chatId: string; // Telegram chat id or PC UI client id
111
+ sessionId: string;
112
+ title?: string;
113
+ state: "active" | "archived";
114
+ boundAt: string;
115
+ updatedAt: string;
116
+ };
117
+ ```
118
+
119
+ Key change from current model:
120
+
121
+ - today the top-level mapping is effectively `chatId -> sessionId`
122
+ - target model is `(platform, botId, chatId) -> sessionId`
123
+
124
+ This is the critical change needed for multi-bot correctness.
125
+
126
+ ### 3. SessionRecord
127
+
128
+ Represents the canonical RemoteAgent session.
129
+
130
+ Suggested fields:
131
+
132
+ ```ts
133
+ type SessionRecord = {
134
+ sessionId: string;
135
+ title?: string;
136
+ mode: BridgeMode;
137
+ workspace: string;
138
+ status: "active" | "archived";
139
+ activeChannel?: {
140
+ platform: "telegram" | "pc-ui";
141
+ botId?: string;
142
+ chatId: string;
143
+ };
144
+ codex?: ProviderSession;
145
+ claude?: ProviderSession;
146
+ createdAt: string;
147
+ updatedAt: string;
148
+ archivedAt?: string;
149
+ };
150
+ ```
151
+
152
+ ### 4. ProviderSession
153
+
154
+ This stays similar to the current model, but should become session-owned only.
155
+
156
+ ```ts
157
+ type ProviderSession = {
158
+ provider: Provider;
159
+ cwd: string;
160
+ pairedAt: string;
161
+ sessionId?: string; // provider's own id
162
+ model?: string;
163
+ lastUsedAt?: string;
164
+ sandboxMode?: CodexSandboxMode;
165
+ };
166
+ ```
167
+
168
+ ### 5. SessionEvent
169
+
170
+ This replaces the current flat JSONL log shape conceptually, even if JSONL stays as the storage format.
171
+
172
+ ```ts
173
+ type SessionEvent = {
174
+ eventId: string;
175
+ sessionId: string;
176
+ timestamp: string;
177
+ source: "telegram" | "pc-ui" | "codex" | "claude" | "system";
178
+ direction: "in" | "out" | "system";
179
+ actor?: {
180
+ botId?: string;
181
+ chatId?: string;
182
+ providerSessionId?: string;
183
+ };
184
+ text: string;
185
+ metadata?: Record<string, string | number | boolean | null>;
186
+ };
187
+ ```
188
+
189
+ ## Target on-disk layout
190
+
191
+ Today the installed runtime stores most state in:
192
+
193
+ - `state.json`
194
+ - `logs/<sessionId>.jsonl`
195
+
196
+ Target layout should become:
197
+
198
+ ```text
199
+ ~/.remoteagent/
200
+ config/
201
+ bots.json
202
+ channels/
203
+ telegram/
204
+ <bot-id>/
205
+ <chat-id>.json
206
+ pc-ui/
207
+ <client-id>.json
208
+ sessions/
209
+ <session-id>/
210
+ session.json
211
+ events.jsonl
212
+ attachments/
213
+ exports/
214
+ migrations/
215
+ applied.json
216
+ ```
217
+
218
+ ### Directory ownership
219
+
220
+ #### `config/bots.json`
221
+
222
+ Stores configured bot profiles and bot-level defaults.
223
+
224
+ #### `channels/telegram/<bot-id>/<chat-id>.json`
225
+
226
+ Stores one Telegram chat binding per bot.
227
+
228
+ This is the main answer to the user's requirement:
229
+
230
+ - Telegram bot A and Telegram bot B are managed separately
231
+ - but they can still point to the same `sessionId`
232
+
233
+ #### `sessions/<session-id>/session.json`
234
+
235
+ Stores canonical session metadata.
236
+
237
+ #### `sessions/<session-id>/events.jsonl`
238
+
239
+ Stores append-only history for that one session.
240
+
241
+ #### `sessions/<session-id>/attachments/`
242
+
243
+ Stores future uploaded files, screenshots, exports, and attachment metadata.
244
+
245
+ ## Example
246
+
247
+ One user may have:
248
+
249
+ - `@codex_remoteagent_bot`
250
+ - `@sqream_bot`
251
+
252
+ Both can point at the same session:
253
+
254
+ ```text
255
+ channels/telegram/codex_remoteagent_bot/8202993989.json -> sessionId=S1
256
+ channels/telegram/sqream_bot/8202993989.json -> sessionId=S1
257
+ sessions/S1/session.json
258
+ sessions/S1/events.jsonl
259
+ ```
260
+
261
+ This preserves:
262
+
263
+ - separate bot routing
264
+ - shared work history
265
+ - one workspace and one session identity
266
+
267
+ ## Required code changes
268
+
269
+ ### A. Types
270
+
271
+ Current relevant file:
272
+
273
+ - `src/types.ts`
274
+
275
+ Required changes:
276
+
277
+ 1. add `BotProfile`
278
+ 2. replace `ChatBinding` with `ChannelBinding`
279
+ 3. add `platform` and `botId`
280
+ 4. split session event type from provider response type
281
+ 5. add `status` and `activeChannel` to `SessionRecord`
282
+
283
+ ### B. Store layer
284
+
285
+ Current relevant file:
286
+
287
+ - `src/store/file-store.ts`
288
+
289
+ Required changes:
290
+
291
+ 1. stop using one `state.json` as the only state source
292
+ 2. add directory-backed reads and writes for:
293
+ - bot profiles
294
+ - channel bindings
295
+ - session records
296
+ - session event logs
297
+ 3. add lookup by:
298
+ - `sessionId`
299
+ - `(platform, botId, chatId)`
300
+ 4. add session listing independent of Telegram chats
301
+ 5. add migration from legacy `state.json`
302
+
303
+ ### C. Bridge service
304
+
305
+ Current relevant file:
306
+
307
+ - `src/services/bridge-service.ts`
308
+
309
+ Required changes:
310
+
311
+ 1. route messages through `(platform, botId, chatId)`
312
+ 2. treat chat binding as a client pointer, not as session ownership
313
+ 3. add APIs for:
314
+ - create session
315
+ - bind channel to existing session
316
+ - switch a bot/chat to another session
317
+ - list sessions for UI and Telegram commands
318
+ 4. write all incoming and outgoing traffic as session events
319
+
320
+ ### D. Bot runtime
321
+
322
+ Current relevant files:
323
+
324
+ - `src/index.ts`
325
+ - `src/bot.ts`
326
+ - `src/config.ts`
327
+
328
+ Required changes:
329
+
330
+ 1. keep multiple Telegram bots running at once
331
+ 2. assign each bot a stable `botId`
332
+ 3. include that `botId` when calling the bridge
333
+ 4. expose future bot-scoped commands like:
334
+ - `/session`
335
+ - `/new`
336
+ - `/switch`
337
+ - `/archive`
338
+
339
+ ### E. Local UI
340
+
341
+ Current relevant file:
342
+
343
+ - `src/services/local-ui-service.ts`
344
+
345
+ Required changes:
346
+
347
+ 1. list canonical sessions, not chats
348
+ 2. show bound channels under a session
349
+ 3. open one session and read `events.jsonl`
350
+ 4. allow binding or rebinding Telegram chats later
351
+
352
+ ## Migration plan
353
+
354
+ ### Phase 1. Add the new directory model alongside legacy state
355
+
356
+ Goal:
357
+
358
+ - introduce new store layout without breaking the current bot flow
359
+
360
+ Work:
361
+
362
+ 1. create `config/`, `channels/`, and `sessions/`
363
+ 2. on startup, read legacy `state.json` if present
364
+ 3. materialize equivalent:
365
+ - session directories
366
+ - Telegram channel binding files
367
+ 4. keep writing both formats temporarily
368
+
369
+ ### Phase 2. Switch reads to the new store
370
+
371
+ Goal:
372
+
373
+ - make directory-backed storage the primary source
374
+
375
+ Work:
376
+
377
+ 1. read sessions from `sessions/<id>/session.json`
378
+ 2. read Telegram bindings from `channels/telegram/<bot-id>/<chat-id>.json`
379
+ 3. read events from per-session `events.jsonl`
380
+ 4. stop depending on `state.json` for live behavior
381
+
382
+ ### Phase 3. Remove dual-write and archive legacy state
383
+
384
+ Goal:
385
+
386
+ - simplify the runtime after migration is stable
387
+
388
+ Work:
389
+
390
+ 1. write migration marker in `migrations/applied.json`
391
+ 2. move legacy `state.json` to `state.legacy.json`
392
+ 3. keep a recovery path, but no longer write the old format
393
+
394
+ ## Telegram command implications
395
+
396
+ Current commands can stay, but semantics should be clarified.
397
+
398
+ ### `/startpair codex [path]`
399
+
400
+ Recommended target behavior:
401
+
402
+ - create a new RemoteAgent session if the current bot/chat has no binding
403
+ - otherwise update the current session's Codex provider binding
404
+
405
+ ### `/attach codex <thread_id> [path]`
406
+
407
+ Recommended target behavior:
408
+
409
+ - attach the provider binding inside the current session
410
+ - optionally support `--session <session-id>` later
411
+
412
+ ### New commands worth adding
413
+
414
+ 1. `/session`
415
+ - show the current bound session id and title
416
+ 2. `/new [path]`
417
+ - create a new RemoteAgent session and bind this bot/chat to it
418
+ 3. `/switch <session-id>`
419
+ - rebind this bot/chat to another existing session
420
+ 4. `/sessions`
421
+ - list recent sessions available to this owner
422
+ 5. `/archive [session-id]`
423
+ - archive a session without deleting history
424
+
425
+ ## Recommendation for implementation order
426
+
427
+ ### Step 1
428
+
429
+ Add `botId` into the live routing path first.
430
+
431
+ This fixes the biggest correctness gap introduced by multi-bot support.
432
+
433
+ ### Step 2
434
+
435
+ Move from flat `state.json` to:
436
+
437
+ - `channels/telegram/<bot-id>/<chat-id>.json`
438
+ - `sessions/<session-id>/session.json`
439
+
440
+ ### Step 3
441
+
442
+ Move logs from `logs/<sessionId>.jsonl` to:
443
+
444
+ - `sessions/<session-id>/events.jsonl`
445
+
446
+ ### Step 4
447
+
448
+ Add session listing and session switching commands.
449
+
450
+ ### Step 5
451
+
452
+ Teach the local UI to read sessions directly.
453
+
454
+ ## Concrete MVP TODO
455
+
456
+ ### Data model
457
+
458
+ - [ ] add `BotProfile`
459
+ - [ ] add `ChannelBinding`
460
+ - [ ] extend `SessionRecord` with `status`, `title`, and `activeChannel`
461
+ - [ ] rename `LogEntry` to a session-owned event model
462
+
463
+ ### Storage
464
+
465
+ - [ ] add `config/bots.json`
466
+ - [ ] add `channels/telegram/<bot-id>/<chat-id>.json`
467
+ - [ ] add `sessions/<session-id>/session.json`
468
+ - [ ] add `sessions/<session-id>/events.jsonl`
469
+ - [ ] keep legacy import from `state.json`
470
+
471
+ ### Runtime
472
+
473
+ - [ ] resolve stable `botId` from each initialized Telegram bot
474
+ - [ ] pass `botId` into every bridge operation
475
+ - [ ] update remote shell guardrails to be bot-aware
476
+ - [ ] update status formatting to show bot-scoped bindings
477
+
478
+ ### Commands
479
+
480
+ - [ ] add `/session`
481
+ - [ ] add `/sessions`
482
+ - [ ] add `/new`
483
+ - [ ] add `/switch`
484
+ - [ ] add `/archive`
485
+
486
+ ### UI
487
+
488
+ - [ ] list canonical sessions in the local UI
489
+ - [ ] show bound Telegram channels under each session
490
+ - [ ] open and read shared event history
491
+
492
+ ### Migration
493
+
494
+ - [ ] auto-migrate legacy `state.json`
495
+ - [ ] dual-write temporarily
496
+ - [ ] cut over to directory-backed reads
497
+ - [ ] remove legacy write path
498
+
499
+ ## Final recommendation
500
+
501
+ For RemoteAgent, the right answer is:
502
+
503
+ - manage Telegram bots separately at the binding layer
504
+ - manage work separately at the session layer
505
+
506
+ So the product should feel "bot-specific" from the user's point of view, while the storage model remains session-centric underneath.
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "appback-remoteagent",
3
+ "version": "0.13.0",
4
+ "description": "Personal installable session server for continuing local AI work across PC and Telegram",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "remoteagent": "bin/remoteagent.js",
9
+ "remoteagent-install": "scripts/install.sh",
10
+ "remoteagent-start": "scripts/start.sh",
11
+ "remoteagent-stop": "scripts/stop.sh"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.json",
18
+ "dev": "tsx watch src/index.ts",
19
+ "start": "node dist/index.js",
20
+ "check": "tsc --noEmit -p tsconfig.json",
21
+ "selftest:telegram": "npm run build && node scripts/selftest-telegram-update.mjs",
22
+ "prepare": "npm run build",
23
+ "version:patch": "npm version --no-git-tag-version patch",
24
+ "version:minor": "npm version --no-git-tag-version minor",
25
+ "version:major": "npm version --no-git-tag-version major"
26
+ },
27
+ "dependencies": {
28
+ "dotenv": "^16.6.1",
29
+ "grammy": "^1.38.3"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^24.6.1",
33
+ "tsx": "^4.20.6",
34
+ "typescript": "^5.9.3"
35
+ },
36
+ "files": [
37
+ "bin",
38
+ "dist",
39
+ "scripts",
40
+ "docs",
41
+ "README.md",
42
+ "LICENSE",
43
+ ".env.example",
44
+ "package.json",
45
+ "tsconfig.json"
46
+ ]
47
+ }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ if [ $# -ne 1 ]; then
5
+ echo "Usage: $0 <patch|minor|major>" >&2
6
+ exit 1
7
+ fi
8
+
9
+ case "$1" in
10
+ patch|minor|major)
11
+ ;;
12
+ *)
13
+ echo "Release type must be one of: patch, minor, major" >&2
14
+ exit 1
15
+ ;;
16
+ esac
17
+
18
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
19
+ export PATH="$HOME/.local/bin:$PATH"
20
+ cd "$ROOT_DIR"
21
+
22
+ npm version --no-git-tag-version "$1"
23
+ node -p "'RemoteAgent version is now ' + require('./package.json').version"
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ source "$HOME/.profile" >/dev/null 2>&1 || true
4
+
5
+ token="${REMOTEAGENT_AUTH_TOKEN:-${CLAUDE_AUTH_TOKEN:-}}"
6
+ if [ -z "$token" ]; then
7
+ echo "Missing token. Usage: /login claude <token>"
8
+ exit 1
9
+ fi
10
+
11
+ verify_output="$(timeout 45s env ANTHROPIC_API_KEY="$token" CLAUDE_CODE_SIMPLE=1 claude --bare --print --output-format text "Reply exactly: claude token ok" 2>&1 || true)"
12
+ if ! printf '%s' "$verify_output" | grep -Fq 'claude token ok'; then
13
+ printf '%s
14
+ ' "$verify_output"
15
+ echo
16
+ echo "Claude token verification failed."
17
+ exit 1
18
+ fi
19
+
20
+ env_file="$HOME/.remoteagent/.env"
21
+ mkdir -p "$(dirname "$env_file")"
22
+ touch "$env_file"
23
+ chmod 600 "$env_file"
24
+ python3 - "$env_file" "$token" <<'PY2'
25
+ from pathlib import Path
26
+ import sys
27
+ path = Path(sys.argv[1])
28
+ token = sys.argv[2]
29
+ lines = path.read_text(encoding='utf8').splitlines() if path.exists() else []
30
+ updated = []
31
+ seen = False
32
+ for line in lines:
33
+ if line.startswith('ANTHROPIC_API_KEY='):
34
+ if not seen:
35
+ updated.append(f'ANTHROPIC_API_KEY={token}')
36
+ seen = True
37
+ continue
38
+ updated.append(line)
39
+ if not seen:
40
+ updated.append(f'ANTHROPIC_API_KEY={token}')
41
+ path.write_text('\n'.join(updated) + '\n', encoding='utf8')
42
+ PY2
43
+
44
+ sudo systemctl restart remoteagent
45
+ printf '%s
46
+
47
+ ' "$verify_output"
48
+ echo "Claude token saved to ~/.remoteagent/.env as ANTHROPIC_API_KEY and remoteagent was restarted."
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ source "$HOME/.profile" >/dev/null 2>&1 || true
4
+ npm install -g @anthropic-ai/claude-code
5
+ claude install
6
+ claude --version
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ source "$HOME/.profile" >/dev/null 2>&1 || true
4
+ mkdir -p "$HOME/.local/bin"
5
+ npm install -g @openai/codex
6
+ prefix="$(npm prefix -g)"
7
+ ln -sf "$prefix/bin/codex" "$HOME/.local/bin/codex"
8
+ codex --version
@@ -0,0 +1,51 @@
1
+ $ErrorActionPreference = "Stop"
2
+
3
+ $rootDir = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
4
+ $dataDir = if ($env:DATA_DIR) { $env:DATA_DIR } else { Join-Path $env:USERPROFILE ".remoteagent" }
5
+ $envFile = Join-Path $dataDir ".env"
6
+
7
+ New-Item -ItemType Directory -Force -Path $dataDir | Out-Null
8
+ New-Item -ItemType Directory -Force -Path (Join-Path $dataDir "logs") | Out-Null
9
+
10
+ if (-not (Test-Path $envFile)) {
11
+ Copy-Item (Join-Path $rootDir ".env.example") $envFile
12
+ Write-Host "Created $envFile"
13
+ }
14
+
15
+ function Set-EnvValue {
16
+ param(
17
+ [string]$Path,
18
+ [string]$Key,
19
+ [string]$Value
20
+ )
21
+
22
+ $content = if (Test-Path $Path) { Get-Content $Path } else { @() }
23
+ $updated = $false
24
+ $result = foreach ($line in $content) {
25
+ if ($line -like "$Key=*") {
26
+ $updated = $true
27
+ "$Key=$Value"
28
+ } else {
29
+ $line
30
+ }
31
+ }
32
+ if (-not $updated) {
33
+ $result += "$Key=$Value"
34
+ }
35
+ Set-Content -Path $Path -Value $result
36
+ }
37
+
38
+ Set-EnvValue -Path $envFile -Key "SETUP_COMMAND_TIMEOUT_MS" -Value "600000"
39
+ Set-EnvValue -Path $envFile -Key "CODEX_INSTALL_COMMAND" -Value "$rootDir/scripts/install-codex.sh"
40
+ Set-EnvValue -Path $envFile -Key "CLAUDE_INSTALL_COMMAND" -Value "$rootDir/scripts/install-claude.sh"
41
+ Set-EnvValue -Path $envFile -Key "CLAUDE_LOGIN_START_COMMAND" -Value "$rootDir/scripts/start-claude-login.sh"
42
+ Set-EnvValue -Path $envFile -Key "CLAUDE_LOGIN_FINISH_COMMAND" -Value "$rootDir/scripts/finish-claude-login.sh"
43
+
44
+ npm --prefix $rootDir install
45
+ npm --prefix $rootDir run build
46
+
47
+ Write-Host ""
48
+ Write-Host "RemoteAgent is installed."
49
+ Write-Host "Provider install/login hooks were configured in $envFile"
50
+ Write-Host "Set TELEGRAM_BOT_TOKEN or TELEGRAM_BOT_TOKENS in $envFile"
51
+ Write-Host "Start with: $rootDir/scripts/start.ps1"