@blunking/codexlink 0.1.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.
@@ -0,0 +1,727 @@
1
+ param(
2
+ [Parameter(Mandatory = $true)]
3
+ [string]$Agent,
4
+
5
+ [string]$Prompt = "",
6
+ [string]$Workspace = "",
7
+ [ValidateSet("inherit", "plugin", "off")]
8
+ [string]$TelegramMode = "inherit",
9
+ [switch]$PrintOnly,
10
+ [switch]$RemoteControl,
11
+ [string[]]$ExtraArgs = @()
12
+ )
13
+
14
+ $ErrorActionPreference = "Stop"
15
+
16
+ function Get-JsonFile {
17
+ param([string]$Path)
18
+ if (-not (Test-Path $Path)) {
19
+ throw "Profile not found: $Path"
20
+ }
21
+ return Get-Content -Raw -Path $Path | ConvertFrom-Json
22
+ }
23
+
24
+ function Try-GetJsonFile {
25
+ param([string]$Path)
26
+ if (-not (Test-Path $Path)) {
27
+ return $null
28
+ }
29
+ try {
30
+ return Get-Content -Raw -Path $Path | ConvertFrom-Json
31
+ } catch {
32
+ return $null
33
+ }
34
+ }
35
+
36
+ function Ensure-Dir {
37
+ param([string]$Path)
38
+ if (-not (Test-Path $Path)) {
39
+ New-Item -ItemType Directory -Path $Path -Force | Out-Null
40
+ }
41
+ }
42
+
43
+ function Set-EnvVar {
44
+ param([string]$Name, [string]$Value)
45
+ if ($null -ne $Value -and $Value -ne "") {
46
+ [Environment]::SetEnvironmentVariable($Name, $Value, "Process")
47
+ }
48
+ }
49
+
50
+ function Resolve-ConfiguredPath {
51
+ param([string]$Value)
52
+ if (-not $Value) {
53
+ return ""
54
+ }
55
+ $expanded = [Environment]::ExpandEnvironmentVariables($Value)
56
+ if ([System.IO.Path]::IsPathRooted($expanded)) {
57
+ return $expanded
58
+ }
59
+ return [System.IO.Path]::GetFullPath((Join-Path $runtimeRoot $expanded))
60
+ }
61
+
62
+ function Get-TelegramPluginRoot {
63
+ param([string]$RuntimeRoot)
64
+
65
+ $candidates = @()
66
+ if ($env:BLUN_CODEX_TELEGRAM_PLUGIN_ROOT) {
67
+ $candidates += $env:BLUN_CODEX_TELEGRAM_PLUGIN_ROOT
68
+ }
69
+ $candidates += (Join-Path $RuntimeRoot "telegram-plugin")
70
+
71
+ foreach ($candidate in $candidates) {
72
+ if (-not $candidate) { continue }
73
+ if ((Test-Path (Join-Path $candidate "app-server-cli.js")) -and (Test-Path (Join-Path $candidate "sidecar-manager.js"))) {
74
+ return $candidate
75
+ }
76
+ }
77
+
78
+ throw "Unable to locate the Telegram plugin root. Set BLUN_CODEX_TELEGRAM_PLUGIN_ROOT or include telegram-plugin beside the runtime."
79
+ }
80
+
81
+ function Get-FreeTcpPort {
82
+ $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
83
+ $listener.Start()
84
+ try {
85
+ return $listener.LocalEndpoint.Port
86
+ } finally {
87
+ $listener.Stop()
88
+ }
89
+ }
90
+
91
+ function Stop-ProcessTree {
92
+ param([int[]]$RootIds)
93
+
94
+ $all = @(Get-CimInstance Win32_Process)
95
+ $targets = New-Object System.Collections.Generic.HashSet[int]
96
+ $queue = New-Object System.Collections.Generic.Queue[int]
97
+
98
+ foreach ($rootId in $RootIds) {
99
+ if ($rootId -gt 0 -and $targets.Add($rootId)) {
100
+ $queue.Enqueue($rootId)
101
+ }
102
+ }
103
+
104
+ while ($queue.Count -gt 0) {
105
+ $current = $queue.Dequeue()
106
+ foreach ($proc in $all) {
107
+ if ($proc.ParentProcessId -eq $current) {
108
+ if ($targets.Add([int]$proc.ProcessId)) {
109
+ $queue.Enqueue([int]$proc.ProcessId)
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ $ordered = @($targets) | Sort-Object -Descending
116
+ foreach ($procId in $ordered) {
117
+ try {
118
+ Stop-Process -Id $procId -Force -ErrorAction Stop
119
+ } catch {
120
+ }
121
+ }
122
+ }
123
+
124
+ function Wait-TcpPort {
125
+ param(
126
+ [string]$HostName,
127
+ [int]$Port,
128
+ [int]$TimeoutMs = 15000
129
+ )
130
+
131
+ $deadline = [DateTime]::UtcNow.AddMilliseconds($TimeoutMs)
132
+ while ([DateTime]::UtcNow -lt $deadline) {
133
+ try {
134
+ $client = [System.Net.Sockets.TcpClient]::new()
135
+ $task = $client.ConnectAsync($HostName, $Port)
136
+ $connected = $task.Wait(350)
137
+ if ($connected -and $client.Connected) {
138
+ $client.Dispose()
139
+ return $true
140
+ }
141
+ $client.Dispose()
142
+ } catch {
143
+ }
144
+ Start-Sleep -Milliseconds 200
145
+ }
146
+ return $false
147
+ }
148
+
149
+ function Read-DotEnvFile {
150
+ param([string]$Path)
151
+ $values = @{}
152
+ if (-not (Test-Path $Path)) {
153
+ return $values
154
+ }
155
+ foreach ($line in (Get-Content -Path $Path)) {
156
+ if (-not $line) { continue }
157
+ if ($line.Trim().StartsWith("#")) { continue }
158
+ $parts = $line -split "=", 2
159
+ if ($parts.Count -ne 2) { continue }
160
+ $values[$parts[0].Trim()] = $parts[1]
161
+ }
162
+ return $values
163
+ }
164
+
165
+ function Write-DotEnvFile {
166
+ param(
167
+ [string]$Path,
168
+ [hashtable]$Values
169
+ )
170
+
171
+ $lines = foreach ($key in ($Values.Keys | Sort-Object)) {
172
+ "$key=$($Values[$key])"
173
+ }
174
+ Set-Content -Path $Path -Value $lines -Encoding UTF8
175
+ }
176
+
177
+ function Write-TextFileWithRetry {
178
+ param(
179
+ [string]$Path,
180
+ [string]$Content,
181
+ [int]$Attempts = 6,
182
+ [int]$DelayMs = 120
183
+ )
184
+ for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
185
+ try {
186
+ Set-Content -Path $Path -Value $Content -Encoding UTF8
187
+ return
188
+ } catch {
189
+ if ($attempt -eq $Attempts) { throw }
190
+ Start-Sleep -Milliseconds $DelayMs
191
+ }
192
+ }
193
+ }
194
+
195
+ function Append-TextFileWithRetry {
196
+ param(
197
+ [string]$Path,
198
+ [string]$Content,
199
+ [int]$Attempts = 6,
200
+ [int]$DelayMs = 120
201
+ )
202
+ for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
203
+ try {
204
+ Add-Content -Path $Path -Value $Content -Encoding UTF8
205
+ return
206
+ } catch {
207
+ if ($attempt -eq $Attempts) { throw }
208
+ Start-Sleep -Milliseconds $DelayMs
209
+ }
210
+ }
211
+ }
212
+
213
+ function Read-PidFileValue {
214
+ param([string]$Path)
215
+ if (-not (Test-Path $Path)) {
216
+ return 0
217
+ }
218
+ try {
219
+ return [int]((Get-Content -Raw -Path $Path).Trim())
220
+ } catch {
221
+ return 0
222
+ }
223
+ }
224
+
225
+ function Write-DebugStage {
226
+ param(
227
+ [string]$Path,
228
+ [string]$Message
229
+ )
230
+ $line = ((Get-Date).ToUniversalTime().ToString("o")) + " " + $Message
231
+ Append-TextFileWithRetry -Path $Path -Content ($line + "`n")
232
+ }
233
+
234
+ function Invoke-NodeJsonWithRetry {
235
+ param(
236
+ [string[]]$NodeArgs,
237
+ [int]$Attempts = 12,
238
+ [int]$DelayMs = 500
239
+ )
240
+
241
+ $lastRaw = $null
242
+ for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
243
+ $stdoutPath = Join-Path $env:TEMP ("blun-codex-node-" + [guid]::NewGuid().ToString() + ".stdout.log")
244
+ $stderrPath = Join-Path $env:TEMP ("blun-codex-node-" + [guid]::NewGuid().ToString() + ".stderr.log")
245
+ $argLine = ($NodeArgs | ForEach-Object { '"' + ($_ -replace '"', '\"') + '"' }) -join " "
246
+ $proc = Start-Process -FilePath "node" -ArgumentList $argLine -WindowStyle Hidden -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath -Wait -PassThru
247
+ $stdoutText = if (Test-Path $stdoutPath) { Get-Content -Raw $stdoutPath } else { "" }
248
+ $stderrText = if (Test-Path $stderrPath) { Get-Content -Raw $stderrPath } else { "" }
249
+ Remove-Item $stdoutPath,$stderrPath -Force -ErrorAction SilentlyContinue
250
+ $lastRaw = ($stdoutText + $stderrText).Trim()
251
+ if ($proc.ExitCode -eq 0 -and $stdoutText.Trim()) {
252
+ return ($stdoutText | ConvertFrom-Json)
253
+ }
254
+ if ($attempt -lt $Attempts) {
255
+ Start-Sleep -Milliseconds $DelayMs
256
+ }
257
+ }
258
+
259
+ throw ("Node command failed after retries: " + ($NodeArgs -join " ") + "`n" + ($lastRaw | Out-String))
260
+ }
261
+
262
+ function Quote-TomlLiteral {
263
+ param([string]$Value)
264
+ if ($null -eq $Value) {
265
+ return $null
266
+ }
267
+ return "'" + ($Value -replace "'", "''") + "'"
268
+ }
269
+
270
+ function Quote-PowerShellLiteral {
271
+ param([string]$Value)
272
+ if ($null -eq $Value) {
273
+ return "''"
274
+ }
275
+ return "'" + ($Value -replace "'", "''") + "'"
276
+ }
277
+
278
+ $runtimeRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
279
+ $profilePath = Join-Path $runtimeRoot ("profiles\" + $Agent.ToLower() + ".json")
280
+ $profile = Get-JsonFile -Path $profilePath
281
+
282
+ $resolvedWorkspace = if ($Workspace) { $Workspace } elseif ($profile.workspace) { $profile.workspace } else { (Get-Location).Path }
283
+ $resolvedWorkspace = Resolve-ConfiguredPath -Value $resolvedWorkspace
284
+ if (-not $resolvedWorkspace) {
285
+ throw "No workspace configured for agent $Agent"
286
+ }
287
+
288
+ $agentRuntimeDir = Join-Path $env:USERPROFILE (".codex\runtimes\" + $profile.agent_name)
289
+ Ensure-Dir -Path $agentRuntimeDir
290
+ $debugLogPath = Join-Path $agentRuntimeDir "launch-debug.log"
291
+ Write-DebugStage -Path $debugLogPath -Message ("START agent=" + $profile.agent_name + " telegram_mode=" + $TelegramMode + " print_only=" + [string]$PrintOnly + " remote_control=" + [string]$RemoteControl)
292
+
293
+ $launchAt = (Get-Date).ToUniversalTime().ToString("o")
294
+ $launchManifest = [ordered]@{
295
+ agent_name = $profile.agent_name
296
+ display_name = $profile.display_name
297
+ workspace = $resolvedWorkspace
298
+ model = $profile.model
299
+ reasoning_effort = $profile.reasoning_effort
300
+ sandbox = $profile.sandbox
301
+ approval_policy = $profile.approval_policy
302
+ remote_control = [bool]$RemoteControl
303
+ prompt = $Prompt
304
+ extra_args = @($ExtraArgs)
305
+ launched_at = $launchAt
306
+ profile_path = $profilePath
307
+ }
308
+
309
+ $lastLaunchPath = Join-Path $agentRuntimeDir "last-launch.json"
310
+ $launchHistoryPath = Join-Path $agentRuntimeDir "launch-history.jsonl"
311
+ Write-TextFileWithRetry -Path $lastLaunchPath -Content ($launchManifest | ConvertTo-Json -Depth 8)
312
+ Append-TextFileWithRetry -Path $launchHistoryPath -Content (($launchManifest | ConvertTo-Json -Compress -Depth 8) + "`n")
313
+
314
+ Set-EnvVar "BLUN_CODEX_AGENT" $profile.agent_name
315
+ Set-EnvVar "BLUN_CODEX_DISPLAY_NAME" $profile.display_name
316
+ Set-EnvVar "BLUN_CODEX_PROFILE_PATH" $profilePath
317
+ Set-EnvVar "BLUN_CODEX_RUNTIME_DIR" $agentRuntimeDir
318
+ Set-EnvVar "BLUN_CODEX_LANE" $profile.lane
319
+ Set-EnvVar "BLUN_CODEX_MODEL" $profile.model
320
+ Set-EnvVar "BLUN_CODEX_REASONING_EFFORT" $profile.reasoning_effort
321
+ Set-EnvVar "BLUN_CODEX_PERSONALITY" $profile.personality
322
+
323
+ $mnemoInherit = $false
324
+ $mnemoCwdOverride = $null
325
+ $mnemoEnvOverride = $null
326
+
327
+ if ($null -ne $profile.mnemo -and $profile.mnemo.inherit -eq $true) {
328
+ $mnemoInherit = $true
329
+ }
330
+
331
+ if (-not $mnemoInherit -and $null -ne $profile.mnemo) {
332
+ $effectiveDefaultAgent = if ($profile.mnemo.default_agent) { $profile.mnemo.default_agent } else { $profile.agent_name }
333
+ $effectiveAgent = if ($profile.mnemo.agent) { $profile.mnemo.agent } else { $profile.agent_name }
334
+
335
+ Set-EnvVar "MNEMO_DEFAULT_AGENT" $effectiveDefaultAgent
336
+ Set-EnvVar "MNEMO_AGENT" $effectiveAgent
337
+ Set-EnvVar "MNEMO_DB" $profile.mnemo.db
338
+ Set-EnvVar "MNEMO_TENANT_ROOT" $profile.mnemo.tenant_root
339
+ Set-EnvVar "MNEMO_OWNER_NAME" $profile.mnemo.owner_name
340
+ if ($null -ne $profile.mnemo.timezone_offset_hours) {
341
+ Set-EnvVar "MNEMO_TZ_OFFSET_HOURS" ([string]$profile.mnemo.timezone_offset_hours)
342
+ }
343
+ if ($null -ne $profile.mnemo.local_agents) {
344
+ Set-EnvVar "MNEMO_LOCAL_AGENTS" (($profile.mnemo.local_agents) -join ",")
345
+ }
346
+ Set-EnvVar "MNEMO_DEFAULT_SCOPE" $profile.mnemo.default_scope
347
+ Set-EnvVar "MNEMO_SCOPE" $profile.mnemo.scope
348
+ $mnemoCwdOverride = $profile.mnemo.cwd
349
+
350
+ $mnemoEnvPairs = @()
351
+ $mnemoEnvPairs += "MNEMO_DEFAULT_AGENT = $(Quote-TomlLiteral $effectiveDefaultAgent)"
352
+ $mnemoEnvPairs += "MNEMO_AGENT = $(Quote-TomlLiteral $effectiveAgent)"
353
+ if ($profile.mnemo.db) {
354
+ $mnemoEnvPairs += "MNEMO_DB = $(Quote-TomlLiteral $profile.mnemo.db)"
355
+ }
356
+ if ($profile.mnemo.tenant_root) {
357
+ $mnemoEnvPairs += "MNEMO_TENANT_ROOT = $(Quote-TomlLiteral $profile.mnemo.tenant_root)"
358
+ }
359
+ if ($profile.mnemo.owner_name) {
360
+ $mnemoEnvPairs += "MNEMO_OWNER_NAME = $(Quote-TomlLiteral $profile.mnemo.owner_name)"
361
+ }
362
+ if ($null -ne $profile.mnemo.timezone_offset_hours) {
363
+ $mnemoEnvPairs += "MNEMO_TZ_OFFSET_HOURS = $(Quote-TomlLiteral ([string]$profile.mnemo.timezone_offset_hours))"
364
+ }
365
+ if ($null -ne $profile.mnemo.local_agents -and $profile.mnemo.local_agents.Count -gt 0) {
366
+ $mnemoEnvPairs += "MNEMO_LOCAL_AGENTS = $(Quote-TomlLiteral (($profile.mnemo.local_agents) -join ','))"
367
+ }
368
+ if ($profile.mnemo.default_scope) {
369
+ $mnemoEnvPairs += "MNEMO_DEFAULT_SCOPE = $(Quote-TomlLiteral $profile.mnemo.default_scope)"
370
+ }
371
+ if ($profile.mnemo.scope) {
372
+ $mnemoEnvPairs += "MNEMO_SCOPE = $(Quote-TomlLiteral $profile.mnemo.scope)"
373
+ }
374
+ if ($mnemoEnvPairs.Count -gt 0) {
375
+ $mnemoEnvOverride = "mcp_servers.mnemo.env={" + ($mnemoEnvPairs -join ", ") + "}"
376
+ }
377
+ }
378
+
379
+ $telegramEnvOverride = $null
380
+ $telegramEnabled = $false
381
+ $telegramStateDir = $null
382
+ $telegramAllowedChatId = $null
383
+ $telegramAppServerWsUrl = $null
384
+
385
+ if ($null -ne $profile.telegram) {
386
+ $telegramEnabled = ($profile.telegram.enabled -eq $true)
387
+ $telegramStateDir = $profile.telegram.state_dir
388
+ $telegramAllowedChatId = $profile.telegram.allowed_chat_id
389
+ }
390
+
391
+ if ($TelegramMode -eq "plugin") {
392
+ $telegramEnabled = $true
393
+ } elseif ($TelegramMode -eq "off") {
394
+ $telegramEnabled = $false
395
+ }
396
+
397
+ if (-not $telegramStateDir) {
398
+ $telegramStateDir = Join-Path $env:USERPROFILE (".codex\channels\telegram-" + $profile.agent_name)
399
+ } else {
400
+ $telegramStateDir = Resolve-ConfiguredPath -Value $telegramStateDir
401
+ }
402
+
403
+ if ($telegramEnabled) {
404
+ Set-EnvVar "BLUN_TELEGRAM_AGENT_NAME" $profile.agent_name
405
+ Set-EnvVar "BLUN_TELEGRAM_STATE_DIR" $telegramStateDir
406
+ Set-EnvVar "BLUN_TELEGRAM_ALLOWED_CHAT_ID" $telegramAllowedChatId
407
+ Set-EnvVar "BLUN_TELEGRAM_PLUGIN_MODE" $TelegramMode
408
+ $telegramEnvPairs = @()
409
+ $telegramEnvPairs += "BLUN_TELEGRAM_AGENT_NAME = $(Quote-TomlLiteral $profile.agent_name)"
410
+ $telegramEnvPairs += "BLUN_TELEGRAM_STATE_DIR = $(Quote-TomlLiteral $telegramStateDir)"
411
+ $telegramEnvPairs += "BLUN_TELEGRAM_PLUGIN_MODE = $(Quote-TomlLiteral $TelegramMode)"
412
+ if ($telegramAllowedChatId) {
413
+ $telegramEnvPairs += "BLUN_TELEGRAM_ALLOWED_CHAT_ID = $(Quote-TomlLiteral $telegramAllowedChatId)"
414
+ }
415
+ $telegramEnvOverride = "mcp_servers.codexlink_telegram.env={" + ($telegramEnvPairs -join ", ") + "}"
416
+ }
417
+
418
+ $useRemoteAppServer = ($TelegramMode -eq "plugin")
419
+
420
+ $mnemoOverrides = @()
421
+ if ($mnemoCwdOverride) {
422
+ $mnemoOverrides += "-c"
423
+ $mnemoOverrides += ("mcp_servers.mnemo.cwd=" + (Quote-TomlLiteral $mnemoCwdOverride))
424
+ }
425
+
426
+ if ($mnemoEnvOverride) {
427
+ $mnemoOverrides += "-c"
428
+ $mnemoOverrides += $mnemoEnvOverride
429
+ }
430
+
431
+ $commonOverrides = @() + $mnemoOverrides
432
+ if ($telegramEnvOverride) {
433
+ $commonOverrides += "-c"
434
+ $commonOverrides += $telegramEnvOverride
435
+ }
436
+
437
+ $codexArgs = @()
438
+
439
+ if ($profile.reasoning_effort) {
440
+ $codexArgs += "-c"
441
+ $codexArgs += ("model_reasoning_effort=""" + $profile.reasoning_effort + """")
442
+ }
443
+
444
+ if ($profile.personality) {
445
+ $codexArgs += "-c"
446
+ $codexArgs += ("personality=""" + $profile.personality + """")
447
+ }
448
+
449
+ if ($profile.sandbox) {
450
+ $codexArgs += "--sandbox"
451
+ $codexArgs += $profile.sandbox
452
+ }
453
+
454
+ if ($profile.approval_policy) {
455
+ $codexArgs += "-a"
456
+ $codexArgs += $profile.approval_policy
457
+ }
458
+
459
+ if ($ExtraArgs.Count -gt 0) {
460
+ $codexArgs += $ExtraArgs
461
+ }
462
+
463
+ $remoteSessionInfo = $null
464
+ $backendArgs = @()
465
+ $envFilePath = $null
466
+ $appServerInfoFile = $null
467
+ $bootstrapScript = $null
468
+ $sidecarManager = $null
469
+
470
+ if ($useRemoteAppServer) {
471
+ Write-DebugStage -Path $debugLogPath -Message "REMOTE_MODE enabled"
472
+ $telegramPluginRoot = Get-TelegramPluginRoot -RuntimeRoot $runtimeRoot
473
+ $port = Get-FreeTcpPort
474
+ $telegramAppServerWsUrl = "ws://127.0.0.1:$port"
475
+ $backendArgs = @("app-server", "--listen", $telegramAppServerWsUrl)
476
+ if ($PrintOnly) {
477
+ Write-DebugStage -Path $debugLogPath -Message ("PRINT_ONLY remote ws_url=" + $telegramAppServerWsUrl)
478
+ $remoteSessionInfo = [ordered]@{
479
+ ws_url = $telegramAppServerWsUrl
480
+ thread_id = "{{THREAD_ID}}"
481
+ pid = 0
482
+ started_at = $null
483
+ preview = $true
484
+ }
485
+ $codexArgs += "--remote"
486
+ $codexArgs += $telegramAppServerWsUrl
487
+ } else {
488
+ Ensure-Dir -Path $telegramStateDir
489
+ $envFilePath = Join-Path $telegramStateDir ".env"
490
+ $stateEnv = Read-DotEnvFile -Path $envFilePath
491
+ $stateEnv["BLUN_TELEGRAM_AGENT_NAME"] = $profile.agent_name
492
+ $stateEnv["BLUN_TELEGRAM_STATE_DIR"] = $telegramStateDir
493
+ $stateEnv["BLUN_TELEGRAM_ALLOWED_CHAT_ID"] = $telegramAllowedChatId
494
+ $stateEnv["BLUN_TELEGRAM_PLUGIN_MODE"] = $TelegramMode
495
+ $stateEnv["BLUN_TELEGRAM_APP_SERVER_WS_URL"] = $telegramAppServerWsUrl
496
+ $stateEnv["BLUN_TELEGRAM_THREAD_ID"] = ""
497
+ Write-DotEnvFile -Path $envFilePath -Values $stateEnv
498
+ Write-DebugStage -Path $debugLogPath -Message ("ENV_WRITTEN ws_url=" + $telegramAppServerWsUrl + " env_file=" + $envFilePath)
499
+
500
+ Set-EnvVar "BLUN_TELEGRAM_APP_SERVER_WS_URL" $telegramAppServerWsUrl
501
+
502
+ $appServerPidFile = Join-Path $agentRuntimeDir "app-server.pid"
503
+ $appServerInfoFile = Join-Path $agentRuntimeDir "app-server.json"
504
+ $currentRuntimeFile = Join-Path $agentRuntimeDir "current-remote-runtime.json"
505
+ $appServerStdoutPath = Join-Path $agentRuntimeDir "app-server.stdout.log"
506
+ $appServerStderrPath = Join-Path $agentRuntimeDir "app-server.stderr.log"
507
+ $previousRuntime = Try-GetJsonFile -Path $currentRuntimeFile
508
+ if ($null -ne $previousRuntime) {
509
+ $oldPids = @()
510
+ if ($previousRuntime.frontend_host_pid) { $oldPids += [int]$previousRuntime.frontend_host_pid }
511
+ if ($previousRuntime.app_server_pid) { $oldPids += [int]$previousRuntime.app_server_pid }
512
+ if ($previousRuntime.poller_pid) { $oldPids += [int]$previousRuntime.poller_pid }
513
+ if ($previousRuntime.dispatcher_pid) { $oldPids += [int]$previousRuntime.dispatcher_pid }
514
+ if ($previousRuntime.responder_pid) { $oldPids += [int]$previousRuntime.responder_pid }
515
+ if ($oldPids.Count -gt 0) {
516
+ Stop-ProcessTree -RootIds $oldPids
517
+ Write-DebugStage -Path $debugLogPath -Message ("PREVIOUS_RUNTIME_STOPPED pids=" + (($oldPids | Select-Object -Unique) -join ","))
518
+ }
519
+ Remove-Item $currentRuntimeFile -Force -ErrorAction SilentlyContinue
520
+ }
521
+ $codexScript = (Get-Command codex).Source
522
+ $codexBaseDir = Split-Path -Parent $codexScript
523
+ $codexJs = $null
524
+ $vendorCandidates = Get-ChildItem -Path (Join-Path $codexBaseDir "node_modules\@*\codex\bin\codex.js") -File -ErrorAction SilentlyContinue
525
+ if ($vendorCandidates) {
526
+ $codexJs = $vendorCandidates[0].FullName
527
+ } else {
528
+ $codexJs = Join-Path $codexBaseDir "node_modules\codex\bin\codex.js"
529
+ }
530
+ $nodeExe = (Get-Command node).Source
531
+ Remove-Item $appServerStdoutPath,$appServerStderrPath -Force -ErrorAction SilentlyContinue
532
+ $clearedEnvNames = @(
533
+ "BLUN_TELEGRAM_AGENT_NAME",
534
+ "BLUN_TELEGRAM_STATE_DIR",
535
+ "BLUN_TELEGRAM_ALLOWED_CHAT_ID",
536
+ "BLUN_TELEGRAM_PLUGIN_MODE",
537
+ "BLUN_TELEGRAM_APP_SERVER_WS_URL",
538
+ "BLUN_TELEGRAM_THREAD_ID",
539
+ "BLUN_TELEGRAM_BOT_TOKEN",
540
+ "BLUN_TELEGRAM_CODEX_BIN"
541
+ )
542
+ $savedEnv = @{}
543
+ foreach ($name in $clearedEnvNames) {
544
+ $savedEnv[$name] = [Environment]::GetEnvironmentVariable($name, "Process")
545
+ [Environment]::SetEnvironmentVariable($name, "", "Process")
546
+ }
547
+ try {
548
+ $backendProcess = Start-Process -FilePath $nodeExe -ArgumentList @(
549
+ $codexJs,
550
+ "app-server",
551
+ "--listen",
552
+ $telegramAppServerWsUrl
553
+ ) -WorkingDirectory $resolvedWorkspace -RedirectStandardOutput $appServerStdoutPath -RedirectStandardError $appServerStderrPath -PassThru -WindowStyle Hidden
554
+ } finally {
555
+ foreach ($name in $clearedEnvNames) {
556
+ [Environment]::SetEnvironmentVariable($name, $savedEnv[$name], "Process")
557
+ }
558
+ }
559
+ Set-Content -Path $appServerPidFile -Value "$($backendProcess.Id)`n" -Encoding UTF8
560
+ Write-DebugStage -Path $debugLogPath -Message ("APP_SERVER_SPAWNED pid=" + $backendProcess.Id + " ws_url=" + $telegramAppServerWsUrl)
561
+
562
+ if (-not (Wait-TcpPort -HostName "127.0.0.1" -Port $port -TimeoutMs 20000)) {
563
+ $backendStderrTail = if (Test-Path $appServerStderrPath) { ((Get-Content -LiteralPath $appServerStderrPath -Tail 40) -join " | ") } else { "" }
564
+ $backendStdoutTail = if (Test-Path $appServerStdoutPath) { ((Get-Content -LiteralPath $appServerStdoutPath -Tail 40) -join " | ") } else { "" }
565
+ if ($backendProcess.HasExited) {
566
+ Write-DebugStage -Path $debugLogPath -Message ("APP_SERVER_EXITED exit_code=" + $backendProcess.ExitCode)
567
+ }
568
+ if ($backendStderrTail) {
569
+ Write-DebugStage -Path $debugLogPath -Message ("APP_SERVER_STDERR " + $backendStderrTail)
570
+ }
571
+ if ($backendStdoutTail) {
572
+ Write-DebugStage -Path $debugLogPath -Message ("APP_SERVER_STDOUT " + $backendStdoutTail)
573
+ }
574
+ Write-DebugStage -Path $debugLogPath -Message "WAIT_TCP_TIMEOUT"
575
+ throw "Timed out waiting for app-server to listen on $telegramAppServerWsUrl"
576
+ }
577
+ Write-DebugStage -Path $debugLogPath -Message "TCP_READY"
578
+ $bootstrapScript = Join-Path $telegramPluginRoot "app-server-cli.js"
579
+ $sidecarManager = Join-Path $telegramPluginRoot "sidecar-manager.js"
580
+ $codexArgs += "--remote"
581
+ $codexArgs += $telegramAppServerWsUrl
582
+ }
583
+ } elseif ($RemoteControl) {
584
+ $codexArgs += "remote-control"
585
+ } else {
586
+ $codexArgs += "-C"
587
+ $codexArgs += $resolvedWorkspace
588
+ }
589
+
590
+ if (-not $useRemoteAppServer) {
591
+ $codexArgs = $commonOverrides + $codexArgs
592
+ }
593
+
594
+ if ($Prompt) {
595
+ $codexArgs += $Prompt
596
+ }
597
+
598
+ if ($PrintOnly) {
599
+ [pscustomobject]@{
600
+ runtime_root = $runtimeRoot
601
+ profile_path = $profilePath
602
+ workspace = $resolvedWorkspace
603
+ env = @{
604
+ BLUN_CODEX_AGENT = $env:BLUN_CODEX_AGENT
605
+ BLUN_CODEX_LANE = $env:BLUN_CODEX_LANE
606
+ MNEMO_DEFAULT_AGENT = $env:MNEMO_DEFAULT_AGENT
607
+ MNEMO_AGENT = $env:MNEMO_AGENT
608
+ MNEMO_DB = $env:MNEMO_DB
609
+ MNEMO_TENANT_ROOT = $env:MNEMO_TENANT_ROOT
610
+ BLUN_TELEGRAM_AGENT_NAME = $env:BLUN_TELEGRAM_AGENT_NAME
611
+ BLUN_TELEGRAM_STATE_DIR = $env:BLUN_TELEGRAM_STATE_DIR
612
+ BLUN_TELEGRAM_ALLOWED_CHAT_ID = $env:BLUN_TELEGRAM_ALLOWED_CHAT_ID
613
+ BLUN_TELEGRAM_APP_SERVER_WS_URL = $env:BLUN_TELEGRAM_APP_SERVER_WS_URL
614
+ BLUN_TELEGRAM_THREAD_ID = $env:BLUN_TELEGRAM_THREAD_ID
615
+ }
616
+ mcp_overrides = @{
617
+ mnemo_inherit = $mnemoInherit
618
+ mnemo_cwd = $mnemoCwdOverride
619
+ mnemo_env = $mnemoEnvOverride
620
+ telegram_env = $telegramEnvOverride
621
+ telegram_mode = $TelegramMode
622
+ }
623
+ remote_session = $remoteSessionInfo
624
+ backend_command = if ($backendArgs.Count -gt 0) { @("codex") + $backendArgs } else { @() }
625
+ command = @("codex") + $codexArgs
626
+ } | ConvertTo-Json -Depth 8
627
+ exit 0
628
+ }
629
+
630
+ if ($useRemoteAppServer) {
631
+ $codexScript = (Get-Command codex).Source
632
+ $currentRuntimeFile = Join-Path $agentRuntimeDir "current-remote-runtime.json"
633
+ $windowTitle = "BLUN Codex Telegram [" + $profile.agent_name + "] " + $telegramAppServerWsUrl
634
+ $resumeCommand = "$host.UI.RawUI.WindowTitle = " + (Quote-PowerShellLiteral $windowTitle) + "; & " + (Quote-PowerShellLiteral $codexScript) + " " + (($codexArgs | ForEach-Object { Quote-PowerShellLiteral $_ }) -join " ")
635
+ Write-DebugStage -Path $debugLogPath -Message ("FRONTEND_SPAWN command=" + $resumeCommand)
636
+ $frontendProcess = Start-Process -FilePath "powershell" -WorkingDirectory $resolvedWorkspace -ArgumentList @(
637
+ "-NoExit",
638
+ "-Command",
639
+ $resumeCommand
640
+ ) -PassThru
641
+ $currentRuntime = [ordered]@{
642
+ ws_url = $telegramAppServerWsUrl
643
+ app_server_pid = $backendProcess.Id
644
+ frontend_host_pid = $frontendProcess.Id
645
+ profile = $profile.agent_name
646
+ started_at = (Get-Date).ToUniversalTime().ToString("o")
647
+ }
648
+ Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 4)
649
+ Write-DebugStage -Path $debugLogPath -Message "FRONTEND_SPAWNED"
650
+ if ($envFilePath -and $bootstrapScript) {
651
+ try {
652
+ Write-DebugStage -Path $debugLogPath -Message "LOADED_THREAD_WAIT_START"
653
+ $loadedIds = @()
654
+ for ($attempt = 1; $attempt -le 40; $attempt++) {
655
+ $loaded = Invoke-NodeJsonWithRetry -NodeArgs @($bootstrapScript, "list-loaded", "--ws-url", $telegramAppServerWsUrl) -Attempts 1 -DelayMs 0
656
+ $loadedIds = @($loaded.data)
657
+ if ($loadedIds.Count -gt 0) {
658
+ Write-DebugStage -Path $debugLogPath -Message ("LOADED_THREAD_SEEN attempt=" + $attempt + " count=" + $loadedIds.Count)
659
+ break
660
+ }
661
+ if ($attempt -lt 40) {
662
+ Start-Sleep -Milliseconds 500
663
+ }
664
+ }
665
+ if ($loadedIds.Count -gt 0) {
666
+ $activeThreadId = [string]$loadedIds[$loadedIds.Count - 1]
667
+ Write-DebugStage -Path $debugLogPath -Message ("LOADED_THREAD_PICKED thread_id=" + $activeThreadId + " count=" + $loadedIds.Count)
668
+ $stateEnv = Read-DotEnvFile -Path $envFilePath
669
+ $stateEnv["BLUN_TELEGRAM_THREAD_ID"] = $activeThreadId
670
+ Write-DotEnvFile -Path $envFilePath -Values $stateEnv
671
+ Set-EnvVar "BLUN_TELEGRAM_THREAD_ID" $activeThreadId
672
+ Write-DebugStage -Path $debugLogPath -Message "ENV_THREAD_WRITTEN"
673
+
674
+ $currentRuntime["thread_id"] = $activeThreadId
675
+ Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 6)
676
+ Write-DebugStage -Path $debugLogPath -Message "CURRENT_RUNTIME_THREAD_WRITTEN"
677
+
678
+ try {
679
+ $verify = Invoke-NodeJsonWithRetry -NodeArgs @($bootstrapScript, "read-thread", "--ws-url", $telegramAppServerWsUrl, "--thread-id", $activeThreadId) -Attempts 2 -DelayMs 350
680
+ if ($verify.ok -and $verify.threadId -eq $activeThreadId) {
681
+ Write-DebugStage -Path $debugLogPath -Message "VERIFY_THREAD_OK"
682
+ } else {
683
+ Write-DebugStage -Path $debugLogPath -Message "VERIFY_THREAD_INVALID"
684
+ }
685
+ } catch {
686
+ Write-DebugStage -Path $debugLogPath -Message ("VERIFY_THREAD_WARN " + $_.Exception.Message)
687
+ }
688
+
689
+ & node $sidecarManager | Out-Null
690
+ if ($LASTEXITCODE -ne 0) {
691
+ Write-DebugStage -Path $debugLogPath -Message "SIDECAR_MANAGER_FAILED"
692
+ throw "Failed to ensure Telegram sidecars."
693
+ }
694
+ Write-DebugStage -Path $debugLogPath -Message "SIDECAR_MANAGER_OK"
695
+
696
+ $pollerPid = Read-PidFileValue -Path (Join-Path $telegramStateDir "poller.pid")
697
+ $dispatcherPid = Read-PidFileValue -Path (Join-Path $telegramStateDir "dispatcher.pid")
698
+ $responderPid = Read-PidFileValue -Path (Join-Path $telegramStateDir "responder.pid")
699
+ $remoteSessionInfo = [ordered]@{
700
+ ws_url = $telegramAppServerWsUrl
701
+ thread_id = $activeThreadId
702
+ pid = $backendProcess.Id
703
+ started_at = (Get-Date).ToUniversalTime().ToString("o")
704
+ sidecars = [ordered]@{
705
+ poller = [ordered]@{ pid = $pollerPid }
706
+ dispatcher = [ordered]@{ pid = $dispatcherPid }
707
+ responder = [ordered]@{ pid = $responderPid }
708
+ }
709
+ }
710
+ Write-TextFileWithRetry -Path $appServerInfoFile -Content ($remoteSessionInfo | ConvertTo-Json -Depth 6)
711
+ if ($pollerPid -gt 0) { $currentRuntime["poller_pid"] = $pollerPid }
712
+ if ($dispatcherPid -gt 0) { $currentRuntime["dispatcher_pid"] = $dispatcherPid }
713
+ if ($responderPid -gt 0) { $currentRuntime["responder_pid"] = $responderPid }
714
+ Write-TextFileWithRetry -Path $currentRuntimeFile -Content ($currentRuntime | ConvertTo-Json -Depth 6)
715
+ Write-DebugStage -Path $debugLogPath -Message "APP_SERVER_INFO_WRITTEN"
716
+ } else {
717
+ Write-DebugStage -Path $debugLogPath -Message "LOADED_THREAD_NONE"
718
+ }
719
+ } catch {
720
+ Write-DebugStage -Path $debugLogPath -Message ("LOADED_THREAD_ERROR " + $_.Exception.Message)
721
+ }
722
+ }
723
+ exit 0
724
+ }
725
+
726
+ & codex @codexArgs
727
+ exit $LASTEXITCODE