@blunking/codexlink 0.1.2 → 0.1.10
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.
- package/README.md +67 -49
- package/package.json +40 -38
- package/start-codex-agent.ps1 +179 -71
- package/telegram-doctor.ps1 +34 -20
- package/telegram-plugin/.env.example +1 -0
- package/telegram-plugin/README.md +3 -0
- package/telegram-plugin/dispatcher.js +4 -0
- package/telegram-plugin/lib/bridge.js +1550 -86
- package/telegram-plugin/lib/codex.js +142 -21
- package/telegram-plugin/lib/env.js +29 -1
- package/telegram-plugin/lib/paths.js +7 -1
- package/telegram-plugin/lib/sidecars.js +12 -1
- package/telegram-plugin/lib/singleton.js +66 -0
- package/telegram-plugin/lib/storage.js +66 -25
- package/telegram-plugin/lib/telegram.js +8 -0
- package/telegram-plugin/poller.js +4 -0
- package/telegram-plugin/responder.js +4 -0
- package/telegram-setup.ps1 +217 -58
- package/telegram-status.ps1 +292 -182
- package/telegram-title-embed.ps1 +442 -0
- package/telegram-title-watcher.ps1 +454 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory = $true)]
|
|
3
|
+
[string]$StateFile,
|
|
4
|
+
|
|
5
|
+
[Parameter(Mandatory = $true)]
|
|
6
|
+
[string]$BaseTitle,
|
|
7
|
+
|
|
8
|
+
[string]$LogFile = ""
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
$ErrorActionPreference = "SilentlyContinue"
|
|
12
|
+
|
|
13
|
+
if (-not ("BlunEmbeddedQueueTitleWatcher" -as [type])) {
|
|
14
|
+
Add-Type -ReferencedAssemblies "System.Web.Extensions" -TypeDefinition @"
|
|
15
|
+
using System;
|
|
16
|
+
using System.Collections;
|
|
17
|
+
using System.Collections.Generic;
|
|
18
|
+
using System.IO;
|
|
19
|
+
using System.Threading;
|
|
20
|
+
using System.Web.Script.Serialization;
|
|
21
|
+
|
|
22
|
+
public static class BlunEmbeddedQueueTitleWatcher
|
|
23
|
+
{
|
|
24
|
+
private static Timer _timer;
|
|
25
|
+
private static string _stateFile;
|
|
26
|
+
private static string _baseTitle;
|
|
27
|
+
private static string _lastTitle = "";
|
|
28
|
+
private static long _ambientTtlMs = 600000;
|
|
29
|
+
private static string _lastNotice = "";
|
|
30
|
+
private static string _lastUiNotice = "";
|
|
31
|
+
private static string _lastUiKind = "";
|
|
32
|
+
private static string _logFile = "";
|
|
33
|
+
private static readonly object Gate = new object();
|
|
34
|
+
|
|
35
|
+
public static void Start(string stateFile, string baseTitle, long ambientTtlMs, string logFile)
|
|
36
|
+
{
|
|
37
|
+
lock (Gate)
|
|
38
|
+
{
|
|
39
|
+
_stateFile = stateFile ?? "";
|
|
40
|
+
_baseTitle = baseTitle ?? "";
|
|
41
|
+
_lastTitle = "";
|
|
42
|
+
_ambientTtlMs = ambientTtlMs > 0 ? ambientTtlMs : 600000;
|
|
43
|
+
_logFile = logFile ?? "";
|
|
44
|
+
_lastNotice = "";
|
|
45
|
+
_lastUiNotice = "";
|
|
46
|
+
_lastUiKind = "";
|
|
47
|
+
WriteLog("START");
|
|
48
|
+
TrySetTitle(_baseTitle);
|
|
49
|
+
if (_timer != null)
|
|
50
|
+
{
|
|
51
|
+
_timer.Dispose();
|
|
52
|
+
}
|
|
53
|
+
_timer = new Timer(_ => Tick(), null, 0, 900);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private static void Tick()
|
|
58
|
+
{
|
|
59
|
+
try
|
|
60
|
+
{
|
|
61
|
+
string title;
|
|
62
|
+
string notice;
|
|
63
|
+
string uiNotice;
|
|
64
|
+
string uiKind;
|
|
65
|
+
BuildSnapshot(out title, out notice, out uiNotice, out uiKind);
|
|
66
|
+
TrySetTitle(title);
|
|
67
|
+
TryWriteNotice(notice);
|
|
68
|
+
TryWriteUiNotice(uiKind, uiNotice);
|
|
69
|
+
}
|
|
70
|
+
catch
|
|
71
|
+
{
|
|
72
|
+
WriteLog("TICK_ERROR");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private static void TrySetTitle(string value)
|
|
77
|
+
{
|
|
78
|
+
var normalizedTitle = value ?? "";
|
|
79
|
+
if (string.Equals(normalizedTitle, _lastTitle, StringComparison.Ordinal))
|
|
80
|
+
{
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try
|
|
84
|
+
{
|
|
85
|
+
Console.Title = normalizedTitle;
|
|
86
|
+
_lastTitle = normalizedTitle;
|
|
87
|
+
WriteLog("TITLE " + Normalize(normalizedTitle, 180));
|
|
88
|
+
}
|
|
89
|
+
catch
|
|
90
|
+
{
|
|
91
|
+
WriteLog("TITLE_ERROR");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private static void TryWriteNotice(string notice)
|
|
96
|
+
{
|
|
97
|
+
var normalized = notice ?? "";
|
|
98
|
+
if (string.Equals(normalized, _lastNotice, StringComparison.Ordinal))
|
|
99
|
+
{
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try
|
|
104
|
+
{
|
|
105
|
+
// Do not write background queue notices into the interactive Codex
|
|
106
|
+
// terminal. It corrupts the prompt/input area on Windows terminals.
|
|
107
|
+
}
|
|
108
|
+
catch
|
|
109
|
+
{
|
|
110
|
+
WriteLog("NOTICE_ERROR");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_lastNotice = normalized;
|
|
114
|
+
WriteLog("NOTICE " + Normalize(normalized, 220));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private static void TryWriteUiNotice(string kind, string notice)
|
|
118
|
+
{
|
|
119
|
+
var normalizedKind = string.IsNullOrWhiteSpace(kind) ? "generic" : kind.Trim().ToLowerInvariant();
|
|
120
|
+
var normalized = notice ?? "";
|
|
121
|
+
if (string.IsNullOrWhiteSpace(normalized))
|
|
122
|
+
{
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (string.Equals(normalized, _lastUiNotice, StringComparison.Ordinal)
|
|
127
|
+
&& string.Equals(normalizedKind, _lastUiKind, StringComparison.Ordinal))
|
|
128
|
+
{
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try
|
|
133
|
+
{
|
|
134
|
+
Console.WriteLine("");
|
|
135
|
+
Console.WriteLine("[Telegram] " + normalized);
|
|
136
|
+
}
|
|
137
|
+
catch
|
|
138
|
+
{
|
|
139
|
+
WriteLog("UI_ERROR " + normalizedKind);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_lastUiNotice = normalized;
|
|
143
|
+
_lastUiKind = normalizedKind;
|
|
144
|
+
WriteLog("UI " + normalizedKind + " " + Normalize(normalized, 220));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private static void BuildSnapshot(out string title, out string notice, out string uiNotice, out string uiKind)
|
|
148
|
+
{
|
|
149
|
+
title = _baseTitle;
|
|
150
|
+
notice = "";
|
|
151
|
+
uiNotice = "";
|
|
152
|
+
uiKind = "";
|
|
153
|
+
if (string.IsNullOrWhiteSpace(_stateFile) || !File.Exists(_stateFile))
|
|
154
|
+
{
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
var raw = File.ReadAllText(_stateFile);
|
|
159
|
+
if (string.IsNullOrWhiteSpace(raw))
|
|
160
|
+
{
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
var serializer = new JavaScriptSerializer();
|
|
165
|
+
var root = serializer.DeserializeObject(raw) as Dictionary<string, object>;
|
|
166
|
+
if (root == null || !root.ContainsKey("queue"))
|
|
167
|
+
{
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (root.ContainsKey("lastUiNotice"))
|
|
172
|
+
{
|
|
173
|
+
var ui = root["lastUiNotice"] as Dictionary<string, object>;
|
|
174
|
+
if (ui != null)
|
|
175
|
+
{
|
|
176
|
+
uiNotice = Normalize(GetString(ui, "text"), 220);
|
|
177
|
+
uiKind = Normalize(GetString(ui, "kind"), 32);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
int total = 0;
|
|
182
|
+
int pending = 0;
|
|
183
|
+
int direct = 0;
|
|
184
|
+
int ambient = 0;
|
|
185
|
+
int escalation = 0;
|
|
186
|
+
string preview = "";
|
|
187
|
+
|
|
188
|
+
var pendingReplies = root.ContainsKey("pendingReplies") ? AsObjects(root["pendingReplies"]) : new object[0];
|
|
189
|
+
foreach (var item in pendingReplies)
|
|
190
|
+
{
|
|
191
|
+
var entry = item as Dictionary<string, object>;
|
|
192
|
+
if (entry == null || !IsOpenPendingReply(entry))
|
|
193
|
+
{
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
total += 1;
|
|
198
|
+
pending += 1;
|
|
199
|
+
CountRelevance(entry, ref direct, ref ambient, ref escalation);
|
|
200
|
+
|
|
201
|
+
if (string.IsNullOrWhiteSpace(preview))
|
|
202
|
+
{
|
|
203
|
+
preview = FormatPreview(entry, 44, true);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
var queue = AsObjects(root["queue"]);
|
|
208
|
+
foreach (var item in queue)
|
|
209
|
+
{
|
|
210
|
+
var entry = item as Dictionary<string, object>;
|
|
211
|
+
if (entry == null)
|
|
212
|
+
{
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
var status = GetString(entry, "status");
|
|
217
|
+
if (!string.Equals(status, "queued", StringComparison.OrdinalIgnoreCase))
|
|
218
|
+
{
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
var relevance = GetString(entry, "relevance");
|
|
223
|
+
if (string.Equals(relevance, "ambient", StringComparison.OrdinalIgnoreCase) && IsOlderThan(entry, _ambientTtlMs))
|
|
224
|
+
{
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
total += 1;
|
|
229
|
+
CountRelevance(entry, ref direct, ref ambient, ref escalation);
|
|
230
|
+
|
|
231
|
+
if (string.IsNullOrWhiteSpace(preview))
|
|
232
|
+
{
|
|
233
|
+
preview = FormatPreview(entry, 44, false);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (total == 0)
|
|
238
|
+
{
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
var parts = new List<string> { "Q:" + total.ToString() };
|
|
243
|
+
if (pending > 0) parts.Add("P:" + pending.ToString());
|
|
244
|
+
if (direct > 0) parts.Add("D:" + direct.ToString());
|
|
245
|
+
if (ambient > 0) parts.Add("G:" + ambient.ToString());
|
|
246
|
+
if (escalation > 0) parts.Add("E:" + escalation.ToString());
|
|
247
|
+
|
|
248
|
+
var suffix = string.Join(" ", parts.ToArray());
|
|
249
|
+
title = _baseTitle + " | " + suffix;
|
|
250
|
+
var noticeParts = new List<string> { total.ToString() + " waiting" };
|
|
251
|
+
if (pending > 0) noticeParts.Add("pending " + pending.ToString());
|
|
252
|
+
if (direct > 0) noticeParts.Add("direct " + direct.ToString());
|
|
253
|
+
if (ambient > 0) noticeParts.Add("group " + ambient.ToString());
|
|
254
|
+
if (escalation > 0) noticeParts.Add("escalation " + escalation.ToString());
|
|
255
|
+
notice = string.Join(" | ", noticeParts.ToArray());
|
|
256
|
+
if (string.IsNullOrWhiteSpace(preview))
|
|
257
|
+
{
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
title = title + " | " + preview;
|
|
261
|
+
notice = notice + " | " + preview;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private static object[] AsObjects(object value)
|
|
265
|
+
{
|
|
266
|
+
var arr = value as object[];
|
|
267
|
+
if (arr != null)
|
|
268
|
+
{
|
|
269
|
+
return arr;
|
|
270
|
+
}
|
|
271
|
+
var list = value as ArrayList;
|
|
272
|
+
if (list != null)
|
|
273
|
+
{
|
|
274
|
+
return list.ToArray();
|
|
275
|
+
}
|
|
276
|
+
return new object[0];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private static string GetString(Dictionary<string, object> entry, string key)
|
|
280
|
+
{
|
|
281
|
+
if (!entry.ContainsKey(key) || entry[key] == null)
|
|
282
|
+
{
|
|
283
|
+
return "";
|
|
284
|
+
}
|
|
285
|
+
return Convert.ToString(entry[key]) ?? "";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private static void CountRelevance(Dictionary<string, object> entry, ref int direct, ref int ambient, ref int escalation)
|
|
289
|
+
{
|
|
290
|
+
var relevance = GetString(entry, "relevance");
|
|
291
|
+
if (string.Equals(relevance, "ambient", StringComparison.OrdinalIgnoreCase))
|
|
292
|
+
{
|
|
293
|
+
ambient += 1;
|
|
294
|
+
}
|
|
295
|
+
else if (string.Equals(relevance, "escalation", StringComparison.OrdinalIgnoreCase))
|
|
296
|
+
{
|
|
297
|
+
escalation += 1;
|
|
298
|
+
}
|
|
299
|
+
else
|
|
300
|
+
{
|
|
301
|
+
direct += 1;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private static bool IsOpenPendingReply(Dictionary<string, object> entry)
|
|
306
|
+
{
|
|
307
|
+
var status = GetString(entry, "status").Trim().ToLowerInvariant();
|
|
308
|
+
if (entry.ContainsKey("sentAt") && entry["sentAt"] != null && !string.IsNullOrWhiteSpace(Convert.ToString(entry["sentAt"])))
|
|
309
|
+
{
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
if (status == "sent" || status == "suppressed_ack" || status == "error" || status == "ignored_bot" || status == "superseded" || status == "expired" || status == "stale_thread")
|
|
313
|
+
{
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private static string FormatPreview(Dictionary<string, object> entry, int maxLength, bool pending)
|
|
320
|
+
{
|
|
321
|
+
var user = GetString(entry, "user");
|
|
322
|
+
var group = GetString(entry, "groupTitle");
|
|
323
|
+
var rawText = pending ? GetString(entry, "sourceText") : GetString(entry, "text");
|
|
324
|
+
var text = Normalize(rawText, maxLength);
|
|
325
|
+
if (string.IsNullOrWhiteSpace(text))
|
|
326
|
+
{
|
|
327
|
+
return "";
|
|
328
|
+
}
|
|
329
|
+
var state = pending ? "pending: " : "";
|
|
330
|
+
if (!string.IsNullOrWhiteSpace(group))
|
|
331
|
+
{
|
|
332
|
+
return Normalize(state + user + "@" + group + ": " + text, maxLength);
|
|
333
|
+
}
|
|
334
|
+
if (!string.IsNullOrWhiteSpace(user))
|
|
335
|
+
{
|
|
336
|
+
return Normalize(state + user + ": " + text, maxLength);
|
|
337
|
+
}
|
|
338
|
+
return state + text;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private static string Normalize(string value, int maxLength)
|
|
342
|
+
{
|
|
343
|
+
if (string.IsNullOrWhiteSpace(value))
|
|
344
|
+
{
|
|
345
|
+
return "";
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
var compact = RepairMojibake(value).Replace("\r", " ").Replace("\n", " ").Trim();
|
|
349
|
+
while (compact.Contains(" "))
|
|
350
|
+
{
|
|
351
|
+
compact = compact.Replace(" ", " ");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (compact.Length <= maxLength)
|
|
355
|
+
{
|
|
356
|
+
return compact;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return compact.Substring(0, Math.Max(0, maxLength - 3)).TrimEnd() + "...";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private static string RepairMojibake(string value)
|
|
363
|
+
{
|
|
364
|
+
if (string.IsNullOrWhiteSpace(value))
|
|
365
|
+
{
|
|
366
|
+
return "";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return value
|
|
370
|
+
.Replace("—", "-")
|
|
371
|
+
.Replace("–", "-")
|
|
372
|
+
.Replace("„", "\"")
|
|
373
|
+
.Replace("“", "\"")
|
|
374
|
+
.Replace("â€\u009d", "\"")
|
|
375
|
+
.Replace("’", "'")
|
|
376
|
+
.Replace("‘", "'")
|
|
377
|
+
.Replace("…", "...")
|
|
378
|
+
.Replace("€", "EUR")
|
|
379
|
+
.Replace("Ä", "Ä")
|
|
380
|
+
.Replace("Ö", "Ö")
|
|
381
|
+
.Replace("Ü", "Ü")
|
|
382
|
+
.Replace("ä", "ä")
|
|
383
|
+
.Replace("ö", "ö")
|
|
384
|
+
.Replace("ü", "ü")
|
|
385
|
+
.Replace("ß", "ß");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private static bool IsOlderThan(Dictionary<string, object> entry, long ttlMs)
|
|
389
|
+
{
|
|
390
|
+
if (!entry.ContainsKey("ts") || entry["ts"] == null)
|
|
391
|
+
{
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
try
|
|
395
|
+
{
|
|
396
|
+
var parsed = DateTimeOffset.Parse(Convert.ToString(entry["ts"]) ?? "");
|
|
397
|
+
return (DateTimeOffset.UtcNow - parsed.ToUniversalTime()).TotalMilliseconds >= ttlMs;
|
|
398
|
+
}
|
|
399
|
+
catch
|
|
400
|
+
{
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private static void WriteLog(string message)
|
|
406
|
+
{
|
|
407
|
+
if (string.IsNullOrWhiteSpace(_logFile))
|
|
408
|
+
{
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
try
|
|
412
|
+
{
|
|
413
|
+
File.AppendAllText(_logFile, DateTimeOffset.UtcNow.ToString("o") + " " + message + Environment.NewLine);
|
|
414
|
+
}
|
|
415
|
+
catch
|
|
416
|
+
{
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
"@
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
$ambientTtlMs = 600000
|
|
424
|
+
try {
|
|
425
|
+
$stateDir = Split-Path -Parent $StateFile
|
|
426
|
+
$envPath = Join-Path $stateDir ".env"
|
|
427
|
+
if (Test-Path $envPath) {
|
|
428
|
+
foreach ($line in (Get-Content -Path $envPath)) {
|
|
429
|
+
if (-not $line) { continue }
|
|
430
|
+
if ($line.Trim().StartsWith("#")) { continue }
|
|
431
|
+
$parts = $line -split "=", 2
|
|
432
|
+
if ($parts.Count -ne 2) { continue }
|
|
433
|
+
if ($parts[0].Trim() -eq "BLUN_TELEGRAM_AMBIENT_QUEUE_TTL_MS") {
|
|
434
|
+
$ambientTtlMs = [int64]$parts[1].Trim()
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
$ambientTtlMs = 600000
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
[BlunEmbeddedQueueTitleWatcher]::Start($StateFile, $BaseTitle, $ambientTtlMs, $LogFile)
|