@blunking/codexlink 0.1.9 → 0.1.12

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.
@@ -2,7 +2,7 @@
2
2
  import { pollOnce } from "./lib/bridge.js";
3
3
  import { isCurrentSidecarPid } from "./lib/singleton.js";
4
4
 
5
- const intervalMs = Number.parseInt(process.env.BLUN_TELEGRAM_POLL_INTERVAL_MS || "1500", 10) || 1500;
5
+ const intervalMs = Number.parseInt(process.env.BLUN_TELEGRAM_POLL_INTERVAL_MS || "700", 10) || 700;
6
6
  let stopping = false;
7
7
 
8
8
  function sleep(ms) {
@@ -1,96 +1,230 @@
1
- param(
2
- [string]$Profile = "default",
3
- [switch]$EnsureConfigured,
4
- [switch]$Json
5
- )
6
-
7
- $ErrorActionPreference = "Stop"
8
-
9
- function Try-ReadJson {
10
- param([string]$Path)
11
- if (-not (Test-Path $Path)) { return $null }
12
- try { return Get-Content -Raw -Path $Path | ConvertFrom-Json } catch { return $null }
13
- }
14
-
15
- function Ensure-Dir {
16
- param([string]$Path)
17
- if (-not (Test-Path $Path)) {
18
- New-Item -ItemType Directory -Path $Path -Force | Out-Null
19
- }
20
- }
21
-
22
- function Resolve-ConfiguredPath {
23
- param([string]$Value, [string]$RuntimeRoot)
24
- if (-not $Value) { return "" }
25
- $expanded = [Environment]::ExpandEnvironmentVariables($Value)
26
- if ([System.IO.Path]::IsPathRooted($expanded)) { return $expanded }
27
- return [System.IO.Path]::GetFullPath((Join-Path $RuntimeRoot $expanded))
28
- }
29
-
30
- function Read-DotEnvFile {
31
- param([string]$Path)
32
- $values = @{}
33
- if (-not (Test-Path $Path)) { return $values }
34
- foreach ($line in (Get-Content -Path $Path)) {
35
- if (-not $line) { continue }
36
- if ($line.Trim().StartsWith("#")) { continue }
37
- $parts = $line -split "=", 2
38
- if ($parts.Count -ne 2) { continue }
39
- $values[$parts[0].Trim()] = $parts[1]
40
- }
41
- return $values
42
- }
43
-
44
- function Write-DotEnvFile {
45
- param(
46
- [string]$Path,
47
- [hashtable]$Values
48
- )
49
-
50
- $lines = foreach ($key in ($Values.Keys | Sort-Object)) {
51
- "$key=$($Values[$key])"
52
- }
53
- Set-Content -Path $Path -Value $lines -Encoding UTF8
54
- }
55
-
56
- function Test-TelegramTokenFormat {
57
- param([string]$Value)
58
- if (-not $Value) { return $false }
59
- return $Value -match '^\d{6,}:[A-Za-z0-9_-]{20,}$'
60
- }
61
-
62
- function Test-AllowedChatIdsFormat {
63
- param([string]$Value)
64
- if (-not $Value) { return $false }
65
- $parts = @($Value -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ })
66
- if ($parts.Count -eq 0) { return $false }
67
- foreach ($part in $parts) {
68
- if ($part -notmatch '^-?\d+$') {
69
- return $false
70
- }
71
- }
72
- return $true
73
- }
74
-
1
+ param(
2
+ [string]$Profile = "default",
3
+ [switch]$EnsureConfigured,
4
+ [switch]$Json
5
+ )
6
+
7
+ $ErrorActionPreference = "Stop"
8
+
9
+ function Try-ReadJson {
10
+ param([string]$Path)
11
+ if (-not (Test-Path $Path)) { return $null }
12
+ try { return Get-Content -Raw -Path $Path | ConvertFrom-Json } catch { return $null }
13
+ }
14
+
15
+ function Ensure-Dir {
16
+ param([string]$Path)
17
+ if (-not (Test-Path $Path)) {
18
+ New-Item -ItemType Directory -Path $Path -Force | Out-Null
19
+ }
20
+ }
21
+
22
+ function Resolve-ConfiguredPath {
23
+ param([string]$Value, [string]$RuntimeRoot)
24
+ if (-not $Value) { return "" }
25
+ $expanded = [Environment]::ExpandEnvironmentVariables($Value)
26
+ if ([System.IO.Path]::IsPathRooted($expanded)) { return $expanded }
27
+ return [System.IO.Path]::GetFullPath((Join-Path $RuntimeRoot $expanded))
28
+ }
29
+
30
+ function Read-DotEnvFile {
31
+ param([string]$Path)
32
+ $values = @{}
33
+ if (-not (Test-Path $Path)) { return $values }
34
+ foreach ($line in (Get-Content -Path $Path)) {
35
+ if (-not $line) { continue }
36
+ if ($line.Trim().StartsWith("#")) { continue }
37
+ $parts = $line -split "=", 2
38
+ if ($parts.Count -ne 2) { continue }
39
+ $values[$parts[0].Trim()] = $parts[1]
40
+ }
41
+ return $values
42
+ }
43
+
44
+ function Write-DotEnvFile {
45
+ param(
46
+ [string]$Path,
47
+ [hashtable]$Values
48
+ )
49
+
50
+ $lines = foreach ($key in ($Values.Keys | Sort-Object)) {
51
+ "$key=$($Values[$key])"
52
+ }
53
+ Set-Content -Path $Path -Value $lines -Encoding UTF8
54
+ }
55
+
56
+ function Test-TelegramTokenFormat {
57
+ param([string]$Value)
58
+ if (-not $Value) { return $false }
59
+ return $Value -match '^\d{6,}:[A-Za-z0-9_-]{20,}$'
60
+ }
61
+
62
+ function Test-AllowedChatIdsFormat {
63
+ param([string]$Value)
64
+ if (-not $Value) { return $false }
65
+ $parts = @($Value -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ })
66
+ if ($parts.Count -eq 0) { return $false }
67
+ foreach ($part in $parts) {
68
+ if ($part -notmatch '^-?\d+$') {
69
+ return $false
70
+ }
71
+ }
72
+ return $true
73
+ }
74
+
75
+ function Invoke-TelegramSetupRequest {
76
+ param(
77
+ [string]$Token,
78
+ [string]$Method,
79
+ [hashtable]$Body = @{}
80
+ )
81
+
82
+ $uri = "https://api.telegram.org/bot$Token/$Method"
83
+ try {
84
+ return Invoke-RestMethod -Method Post -Uri $uri -ContentType "application/json" -Body ($Body | ConvertTo-Json -Depth 10) -TimeoutSec 35
85
+ } catch {
86
+ $message = $_.Exception.Message
87
+ if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
88
+ $message = $_.ErrorDetails.Message
89
+ }
90
+ throw "Telegram $Method fehlgeschlagen: $message"
91
+ }
92
+ }
93
+
94
+ function Get-TelegramBotInfo {
95
+ param([string]$Token)
96
+ $response = Invoke-TelegramSetupRequest -Token $Token -Method "getMe"
97
+ if (-not $response.ok) {
98
+ throw "Telegram Bot Token wurde von Telegram abgelehnt."
99
+ }
100
+ return $response.result
101
+ }
102
+
103
+ function Get-TelegramUpdatesForSetup {
104
+ param(
105
+ [string]$Token,
106
+ [long]$Offset = 0,
107
+ [int]$TimeoutSeconds = 0
108
+ )
109
+
110
+ $body = @{
111
+ timeout = $TimeoutSeconds
112
+ limit = 20
113
+ allowed_updates = @("message", "edited_message", "channel_post")
114
+ }
115
+ if ($Offset -gt 0) {
116
+ $body["offset"] = $Offset
117
+ }
118
+ $response = Invoke-TelegramSetupRequest -Token $Token -Method "getUpdates" -Body $body
119
+ if (-not $response.ok) {
120
+ return @()
121
+ }
122
+ return @($response.result)
123
+ }
124
+
125
+ function Get-TelegramUpdateId {
126
+ param($Update)
127
+ try {
128
+ return [long]$Update.update_id
129
+ } catch {
130
+ return 0
131
+ }
132
+ }
133
+
134
+ function Get-TelegramChatFromUpdate {
135
+ param($Update)
136
+
137
+ $message = $null
138
+ if ($Update.message) {
139
+ $message = $Update.message
140
+ } elseif ($Update.edited_message) {
141
+ $message = $Update.edited_message
142
+ } elseif ($Update.channel_post) {
143
+ $message = $Update.channel_post
144
+ }
145
+
146
+ if (-not $message -or -not $message.chat -or -not $message.chat.id) {
147
+ return $null
148
+ }
149
+
150
+ $chat = $message.chat
151
+ $title = ""
152
+ if ($chat.title) {
153
+ $title = [string]$chat.title
154
+ } elseif ($chat.username) {
155
+ $title = "@" + [string]$chat.username
156
+ } elseif ($chat.first_name -or $chat.last_name) {
157
+ $title = ((@($chat.first_name, $chat.last_name) | Where-Object { $_ }) -join " ")
158
+ }
159
+
160
+ return [ordered]@{
161
+ chat_id = [string]$chat.id
162
+ chat_type = [string]$chat.type
163
+ title = $title
164
+ update_id = Get-TelegramUpdateId -Update $Update
165
+ }
166
+ }
167
+
168
+ function Wait-TelegramPairingChat {
169
+ param(
170
+ [string]$Token,
171
+ $BotInfo,
172
+ [int]$TimeoutSeconds = 90
173
+ )
174
+
175
+ $baseline = Get-TelegramUpdatesForSetup -Token $Token -TimeoutSeconds 0
176
+ $offset = 0
177
+ foreach ($update in $baseline) {
178
+ $offset = [Math]::Max($offset, (Get-TelegramUpdateId -Update $update) + 1)
179
+ }
180
+
181
+ $botName = if ($BotInfo.username) { "@" + [string]$BotInfo.username } else { "deinen Bot" }
182
+ Write-Host ""
183
+ Write-Host "Telegram Pairing" -ForegroundColor Cyan
184
+ Write-Host "Oeffne Telegram und sende jetzt eine neue Nachricht an $botName." -ForegroundColor White
185
+ Write-Host "Fuer Gruppen: Bot in die Gruppe einladen und dort kurz '$botName connect' schreiben." -ForegroundColor DarkGray
186
+ Write-Host "Ich erkenne die Chat-ID automatisch. Du musst keine ID suchen." -ForegroundColor DarkGray
187
+ Write-Host ""
188
+
189
+ $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
190
+ while ((Get-Date) -lt $deadline) {
191
+ $remaining = [Math]::Max(1, [int]([Math]::Ceiling(($deadline - (Get-Date)).TotalSeconds)))
192
+ $pollTimeout = [Math]::Min(10, $remaining)
193
+ $updates = Get-TelegramUpdatesForSetup -Token $Token -Offset $offset -TimeoutSeconds $pollTimeout
194
+ foreach ($update in $updates) {
195
+ $updateId = Get-TelegramUpdateId -Update $update
196
+ if ($updateId -gt 0) {
197
+ $offset = [Math]::Max($offset, $updateId + 1)
198
+ }
199
+ $chat = Get-TelegramChatFromUpdate -Update $update
200
+ if ($chat -and $chat.chat_id) {
201
+ return $chat
202
+ }
203
+ }
204
+ }
205
+
206
+ return $null
207
+ }
208
+
75
209
  function Prompt-RequiredValue {
76
- param(
77
- [string]$Prompt,
78
- [string]$CurrentValue,
79
- [scriptblock]$Validator,
80
- [string]$ErrorMessage
81
- )
82
-
83
- while ($true) {
84
- $fullPrompt = if ($CurrentValue) { "$Prompt [$CurrentValue]" } else { $Prompt }
85
- $inputValue = Read-Host -Prompt $fullPrompt
86
- if (-not $inputValue -and $CurrentValue) {
87
- $inputValue = $CurrentValue
88
- }
89
- $inputValue = [string]$inputValue
90
- if (& $Validator $inputValue) {
91
- return $inputValue
92
- }
93
- Write-Host $ErrorMessage -ForegroundColor Yellow
210
+ param(
211
+ [string]$Prompt,
212
+ [string]$CurrentValue,
213
+ [scriptblock]$Validator,
214
+ [string]$ErrorMessage
215
+ )
216
+
217
+ while ($true) {
218
+ $fullPrompt = if ($CurrentValue) { "$Prompt [$CurrentValue]" } else { $Prompt }
219
+ $inputValue = Read-Host -Prompt $fullPrompt
220
+ if (-not $inputValue -and $CurrentValue) {
221
+ $inputValue = $CurrentValue
222
+ }
223
+ $inputValue = [string]$inputValue
224
+ if (& $Validator $inputValue) {
225
+ return $inputValue
226
+ }
227
+ Write-Host $ErrorMessage -ForegroundColor Yellow
94
228
  }
95
229
  }
96
230
 
@@ -122,19 +256,19 @@ function Get-ProfilePath {
122
256
  $runtimeRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
123
257
  $profilePath = Get-ProfilePath -RuntimeRoot $runtimeRoot -ProfileName $Profile
124
258
  $profileJson = Try-ReadJson -Path $profilePath
125
-
126
- if (-not $profileJson) {
127
- throw "Profile not found or invalid: $profilePath"
128
- }
129
-
130
- $profileAgent = if ($profileJson.agent_name) { [string]$profileJson.agent_name } else { $Profile.ToLower() }
131
- $stateDir = if ($profileJson.telegram -and $profileJson.telegram.state_dir) {
132
- Resolve-ConfiguredPath -Value ([string]$profileJson.telegram.state_dir) -RuntimeRoot $runtimeRoot
133
- } else {
134
- Join-Path $env:USERPROFILE (".codex\channels\telegram-" + $profileAgent)
135
- }
136
-
137
- Ensure-Dir -Path $stateDir
259
+
260
+ if (-not $profileJson) {
261
+ throw "Profile not found or invalid: $profilePath"
262
+ }
263
+
264
+ $profileAgent = if ($profileJson.agent_name) { [string]$profileJson.agent_name } else { $Profile.ToLower() }
265
+ $stateDir = if ($profileJson.telegram -and $profileJson.telegram.state_dir) {
266
+ Resolve-ConfiguredPath -Value ([string]$profileJson.telegram.state_dir) -RuntimeRoot $runtimeRoot
267
+ } else {
268
+ Join-Path $env:USERPROFILE (".codex\channels\telegram-" + $profileAgent)
269
+ }
270
+
271
+ Ensure-Dir -Path $stateDir
138
272
  $envPath = Join-Path $stateDir ".env"
139
273
  $envValues = Read-DotEnvFile -Path $envPath
140
274
 
@@ -147,12 +281,13 @@ $currentAllowedChatIds = [string]$envValues["BLUN_TELEGRAM_ALLOWED_CHAT_ID"]
147
281
  if (-not (Test-AllowedChatIdsFormat -Value $currentAllowedChatIds)) {
148
282
  $currentAllowedChatIds = [string]$envValues["TELEGRAM_ALLOWED_CHAT_ID"]
149
283
  }
150
-
284
+
151
285
  $needsToken = -not (Test-TelegramTokenFormat -Value $currentToken)
152
286
  $needsChatIds = -not (Test-AllowedChatIdsFormat -Value $currentAllowedChatIds)
153
287
  $changed = $false
288
+ $tokenWasPrompted = $false
154
289
 
155
- if ($EnsureConfigured -and -not $needsToken) {
290
+ if ($EnsureConfigured -and -not $needsToken -and -not $needsChatIds) {
156
291
  $result = [ordered]@{
157
292
  ok = $true
158
293
  changed = $false
@@ -160,18 +295,13 @@ if ($EnsureConfigured -and -not $needsToken) {
160
295
  state_dir = $stateDir
161
296
  env_path = $envPath
162
297
  missing = @()
163
- optional = @(
164
- "allowed_chat_ids"
165
- )
298
+ optional = @()
166
299
  }
167
300
  if ($Json) {
168
301
  $result | ConvertTo-Json -Depth 6
169
302
  } else {
170
303
  Write-Host "Telegram ist bereits eingerichtet fuer Profil '$profileAgent'." -ForegroundColor Green
171
304
  Write-Host "State-Ordner: $stateDir"
172
- if ($needsChatIds) {
173
- Write-Host "Hinweis: keine Chat-Allowlist gesetzt. Der Bot akzeptiert aktuell alle Chats, die er sehen kann." -ForegroundColor Yellow
174
- }
175
305
  }
176
306
  exit 0
177
307
  }
@@ -192,63 +322,83 @@ if ($EnsureConfigured -and $Json -and $needsToken) {
192
322
  } | ConvertTo-Json -Depth 6
193
323
  exit 2
194
324
  }
195
-
196
- if (-not $Json) {
197
- Write-Host ""
198
- Write-Host "CodexLink Telegram Setup" -ForegroundColor Cyan
199
- Write-Host "Profil: $profileAgent"
200
- Write-Host "Lokaler State-Ordner: $stateDir"
201
- Write-Host ""
202
- Write-Host "Ich speichere die Telegram-Werte automatisch an die richtige lokale Stelle." -ForegroundColor DarkGray
325
+
326
+ if (-not $Json) {
327
+ Write-Host ""
328
+ Write-Host "CodexLink Telegram Setup" -ForegroundColor Cyan
329
+ Write-Host "Profil: $profileAgent"
330
+ Write-Host "Lokaler State-Ordner: $stateDir"
331
+ Write-Host ""
332
+ Write-Host "Ich speichere die Telegram-Werte automatisch an die richtige lokale Stelle." -ForegroundColor DarkGray
203
333
  Write-Host "Du musst keine .env-Datei selbst suchen." -ForegroundColor DarkGray
204
- Write-Host "Chat-ID-Allowlist ist optional und blockiert den Start nicht mehr." -ForegroundColor DarkGray
334
+ Write-Host "Die Chat-ID wird automatisch erkannt. Du musst sie nicht wissen." -ForegroundColor DarkGray
205
335
  Write-Host ""
206
336
  }
207
-
208
- if ($needsToken) {
209
- $currentToken = Prompt-RequiredValue `
210
- -Prompt "Telegram Bot Token" `
211
- -CurrentValue $currentToken `
212
- -Validator { param($v) Test-TelegramTokenFormat -Value $v } `
213
- -ErrorMessage "Bitte einen gueltigen Telegram Bot Token eingeben. Beispiel: 123456789:ABC..."
214
- $envValues["BLUN_TELEGRAM_BOT_TOKEN"] = $currentToken
215
- $changed = $true
216
- }
217
-
218
- if (-not $EnsureConfigured -and $needsChatIds) {
219
- $currentAllowedChatIds = Prompt-RequiredValue `
220
- -Prompt "Erlaubte Chat ID(s), komma-getrennt" `
221
- -CurrentValue $currentAllowedChatIds `
222
- -Validator { param($v) Test-AllowedChatIdsFormat -Value $v } `
223
- -ErrorMessage "Bitte mindestens eine numerische Chat-ID eingeben. Mehrere IDs mit Komma trennen."
224
- $envValues["BLUN_TELEGRAM_ALLOWED_CHAT_ID"] = (($currentAllowedChatIds -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ }) -join ",")
337
+
338
+ if ($needsToken) {
339
+ $currentToken = Prompt-RequiredValue `
340
+ -Prompt "Telegram Bot Token" `
341
+ -CurrentValue $currentToken `
342
+ -Validator { param($v) Test-TelegramTokenFormat -Value $v } `
343
+ -ErrorMessage "Bitte einen gueltigen Telegram Bot Token eingeben. Beispiel: 123456789:ABC..."
344
+ $envValues["BLUN_TELEGRAM_BOT_TOKEN"] = $currentToken
225
345
  $changed = $true
346
+ $tokenWasPrompted = $true
226
347
  }
227
-
228
- $envValues["BLUN_TELEGRAM_AGENT_NAME"] = $profileAgent
229
- $envValues["BLUN_TELEGRAM_STATE_DIR"] = $stateDir
230
- Write-DotEnvFile -Path $envPath -Values $envValues
231
-
232
- $result = [ordered]@{
233
- ok = $true
234
- changed = $changed
235
- profile = $profileAgent
236
- state_dir = $stateDir
237
- env_path = $envPath
238
- missing = @()
239
- }
240
-
241
- if ($Json) {
242
- $result | ConvertTo-Json -Depth 6
243
- exit 0
244
- }
245
-
246
- Write-Host ""
247
- Write-Host "Telegram ist jetzt eingerichtet." -ForegroundColor Green
248
- Write-Host "Gespeichert unter: $envPath"
249
- Write-Host ""
250
- Write-Host "Naechster Schritt:" -ForegroundColor Cyan
251
- Write-Host " blun-codex --profile $profileAgent telegram-plugin"
252
- Write-Host ""
253
- Write-Host "Pruefen kannst du spaeter mit:" -ForegroundColor Cyan
254
- Write-Host " blun-codex --profile $profileAgent telegram-doctor"
348
+
349
+ if ($needsChatIds -and -not $Json) {
350
+ try {
351
+ $botInfo = Get-TelegramBotInfo -Token $currentToken
352
+ $shouldPair = $tokenWasPrompted -or (-not $EnsureConfigured)
353
+ if ($shouldPair) {
354
+ $pairedChat = Wait-TelegramPairingChat -Token $currentToken -BotInfo $botInfo -TimeoutSeconds 90
355
+ if ($pairedChat -and $pairedChat.chat_id) {
356
+ $currentAllowedChatIds = [string]$pairedChat.chat_id
357
+ $envValues["BLUN_TELEGRAM_ALLOWED_CHAT_ID"] = $currentAllowedChatIds
358
+ $envValues["BLUN_TELEGRAM_PAIRING_DONE"] = "1"
359
+ $changed = $true
360
+ $label = if ($pairedChat.title) { "$($pairedChat.title) ($($pairedChat.chat_type))" } else { $pairedChat.chat_type }
361
+ Write-Host "Gekoppelt: $label -> $currentAllowedChatIds" -ForegroundColor Green
362
+ } else {
363
+ $envValues["BLUN_TELEGRAM_PAIRING_DONE"] = "1"
364
+ Write-Host "Keine Telegram-Nachricht erkannt. Ich starte ohne Allowlist; du kannst spaeter erneut `blun-codex telegram-setup` ausfuehren." -ForegroundColor Yellow
365
+ }
366
+ } else {
367
+ Write-Host "Hinweis: keine Chat-Allowlist gesetzt. Der Bot akzeptiert aktuell alle Chats, die er sehen kann." -ForegroundColor Yellow
368
+ }
369
+ } catch {
370
+ if ($tokenWasPrompted) {
371
+ throw
372
+ }
373
+ Write-Host $_.Exception.Message -ForegroundColor Yellow
374
+ Write-Host "Ich starte ohne Allowlist; du kannst spaeter erneut `blun-codex telegram-setup` ausfuehren." -ForegroundColor Yellow
375
+ }
376
+ }
377
+
378
+ $envValues["BLUN_TELEGRAM_AGENT_NAME"] = $profileAgent
379
+ $envValues["BLUN_TELEGRAM_STATE_DIR"] = $stateDir
380
+ Write-DotEnvFile -Path $envPath -Values $envValues
381
+
382
+ $result = [ordered]@{
383
+ ok = $true
384
+ changed = $changed
385
+ profile = $profileAgent
386
+ state_dir = $stateDir
387
+ env_path = $envPath
388
+ missing = @()
389
+ }
390
+
391
+ if ($Json) {
392
+ $result | ConvertTo-Json -Depth 6
393
+ exit 0
394
+ }
395
+
396
+ Write-Host ""
397
+ Write-Host "Telegram ist jetzt eingerichtet." -ForegroundColor Green
398
+ Write-Host "Gespeichert unter: $envPath"
399
+ Write-Host ""
400
+ Write-Host "Naechster Schritt:" -ForegroundColor Cyan
401
+ Write-Host " blun-codex --profile $profileAgent telegram-plugin"
402
+ Write-Host ""
403
+ Write-Host "Pruefen kannst du spaeter mit:" -ForegroundColor Cyan
404
+ Write-Host " blun-codex --profile $profileAgent telegram-doctor"