@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,2 @@
1
+ @echo off
2
+ call "%~dp0blun-codex.cmd" %*
@@ -0,0 +1,125 @@
1
+ param(
2
+ [string]$Profile = "default"
3
+ )
4
+
5
+ $ErrorActionPreference = "Stop"
6
+
7
+ function Read-DotEnvFile {
8
+ param([string]$Path)
9
+ $values = @{}
10
+ if (-not (Test-Path $Path)) { return $values }
11
+ foreach ($line in (Get-Content -Path $Path)) {
12
+ if (-not $line) { continue }
13
+ if ($line.Trim().StartsWith("#")) { continue }
14
+ $parts = $line -split "=", 2
15
+ if ($parts.Count -ne 2) { continue }
16
+ $values[$parts[0].Trim()] = $parts[1]
17
+ }
18
+ return $values
19
+ }
20
+
21
+ function Add-Check {
22
+ param(
23
+ [System.Collections.Generic.List[object]]$List,
24
+ [string]$Name,
25
+ [string]$Status,
26
+ [string]$Detail
27
+ )
28
+ $List.Add([pscustomobject]@{
29
+ name = $Name
30
+ status = $Status
31
+ detail = $Detail
32
+ }) | Out-Null
33
+ }
34
+
35
+ $runtimeRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
36
+ $profilePath = Join-Path $runtimeRoot ("profiles\" + $Profile.ToLower() + ".json")
37
+ $checks = New-Object 'System.Collections.Generic.List[object]'
38
+
39
+ if (Test-Path $profilePath) {
40
+ Add-Check -List $checks -Name "profile_file" -Status "ok" -Detail $profilePath
41
+ } else {
42
+ Add-Check -List $checks -Name "profile_file" -Status "fail" -Detail ("Missing profile: " + $profilePath)
43
+ }
44
+
45
+ $statusRaw = & powershell -ExecutionPolicy Bypass -File (Join-Path $runtimeRoot "telegram-status.ps1") -Profile $Profile
46
+ $status = $statusRaw | ConvertFrom-Json
47
+
48
+ $nodeCommand = Get-Command node -ErrorAction SilentlyContinue
49
+ $codexCommand = Get-Command codex -ErrorAction SilentlyContinue
50
+ Add-Check -List $checks -Name "node" -Status $(if ($nodeCommand) { "ok" } else { "fail" }) -Detail $(if ($nodeCommand) { $nodeCommand.Source } else { "node not found in PATH" })
51
+ Add-Check -List $checks -Name "codex" -Status $(if ($codexCommand) { "ok" } else { "fail" }) -Detail $(if ($codexCommand) { $codexCommand.Source } else { "codex not found in PATH" })
52
+
53
+ if ($status.plugin_root) {
54
+ Add-Check -List $checks -Name "telegram_plugin_root" -Status "ok" -Detail $status.plugin_root
55
+ } else {
56
+ Add-Check -List $checks -Name "telegram_plugin_root" -Status "fail" -Detail "Telegram plugin root could not be resolved."
57
+ }
58
+
59
+ if ($status.state_dir -and (Test-Path $status.state_dir)) {
60
+ Add-Check -List $checks -Name "state_dir" -Status "ok" -Detail $status.state_dir
61
+ } else {
62
+ Add-Check -List $checks -Name "state_dir" -Status "fail" -Detail ("Missing state dir: " + $status.state_dir)
63
+ }
64
+
65
+ $activeEnv = Read-DotEnvFile -Path (Join-Path $status.state_dir ".env")
66
+ $legacyEnv = Read-DotEnvFile -Path (Join-Path $env:USERPROFILE ".codex\channels\codexlink-telegram\.env")
67
+ $tokenSource = if ($activeEnv["BLUN_TELEGRAM_BOT_TOKEN"]) {
68
+ "active_state_env"
69
+ } elseif ($legacyEnv["BLUN_TELEGRAM_BOT_TOKEN"]) {
70
+ "legacy_env_fallback"
71
+ } else {
72
+ ""
73
+ }
74
+ Add-Check -List $checks -Name "bot_token" -Status $(if ($tokenSource) { "ok" } else { "fail" }) -Detail $(if ($tokenSource) { $tokenSource } else { "No BLUN_TELEGRAM_BOT_TOKEN found in active or legacy env files." })
75
+
76
+ Add-Check -List $checks -Name "app_server_ws" -Status $(if ($status.active_ws) { "ok" } else { "warn" }) -Detail $(if ($status.active_ws) { $status.active_ws } else { "No active websocket recorded." })
77
+ Add-Check -List $checks -Name "bound_thread" -Status $(if ($status.active_thread_id) { "ok" } else { "warn" }) -Detail $(if ($status.active_thread_id) { $status.active_thread_id } else { "No active thread bound yet." })
78
+ Add-Check -List $checks -Name "poller" -Status $(if ($status.poller_alive) { "ok" } else { "warn" }) -Detail ("pid=" + [string]$status.poller_pid + " alive=" + [string]$status.poller_alive)
79
+ Add-Check -List $checks -Name "dispatcher" -Status $(if ($status.dispatcher_alive) { "ok" } else { "warn" }) -Detail ("pid=" + [string]$status.dispatcher_pid + " alive=" + [string]$status.dispatcher_alive)
80
+ Add-Check -List $checks -Name "responder" -Status $(if ($status.responder_alive) { "ok" } else { "warn" }) -Detail ("pid=" + [string]$status.responder_pid + " alive=" + [string]$status.responder_alive)
81
+
82
+ if ($status.last_inbound) {
83
+ $lastInboundSummary = [string]::Format(
84
+ "chat={0} message={1} type={2} thread={3}",
85
+ $status.last_inbound.chatId,
86
+ $status.last_inbound.messageId,
87
+ $status.last_inbound.chatType,
88
+ $(if ($status.last_inbound.telegramThreadId) { $status.last_inbound.telegramThreadId } else { "-" })
89
+ )
90
+ Add-Check -List $checks -Name "last_inbound" -Status "ok" -Detail $lastInboundSummary
91
+ } else {
92
+ Add-Check -List $checks -Name "last_inbound" -Status "warn" -Detail "No inbound Telegram message recorded yet."
93
+ }
94
+
95
+ if ($status.last_outbound) {
96
+ $lastOutboundSummary = [string]::Format(
97
+ "chat={0} message={1} reply_to={2} thread={3}",
98
+ $status.last_outbound.chatId,
99
+ $status.last_outbound.messageId,
100
+ $(if ($status.last_outbound.replyToMessageId) { $status.last_outbound.replyToMessageId } else { "-" }),
101
+ $(if ($status.last_outbound.telegramThreadId) { $status.last_outbound.telegramThreadId } else { "-" })
102
+ )
103
+ Add-Check -List $checks -Name "last_outbound" -Status "ok" -Detail $lastOutboundSummary
104
+ } else {
105
+ Add-Check -List $checks -Name "last_outbound" -Status "warn" -Detail "No outbound Telegram message recorded yet."
106
+ }
107
+
108
+ Add-Check -List $checks -Name "queue" -Status $(if (([int]$status.queue_depth -eq 0) -and ([int]$status.pending_reply_depth -eq 0)) { "ok" } else { "warn" }) -Detail ("queued=" + $status.queue_depth + " submitted=" + $status.submitted_depth + " pending_replies=" + $status.pending_reply_depth)
109
+
110
+ $overall = "ok"
111
+ if (@($checks | Where-Object { $_.status -eq "fail" }).Count -gt 0) {
112
+ $overall = "fail"
113
+ } elseif (@($checks | Where-Object { $_.status -eq "warn" }).Count -gt 0) {
114
+ $overall = "warn"
115
+ }
116
+
117
+ [ordered]@{
118
+ profile = $status.profile
119
+ overall = $overall
120
+ runtime_root = $runtimeRoot
121
+ state_dir = $status.state_dir
122
+ plugin_root = $status.plugin_root
123
+ checks = $checks
124
+ status = $status
125
+ } | ConvertTo-Json -Depth 8
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "codexlink-telegram",
3
+ "description": "BLUN Telegram bridge for one visible CLI session. Queues inbound messages, binds the live thread, and sends explicit replies without a shadow bot.",
4
+ "version": "0.1.0",
5
+ "keywords": ["blun", "telegram", "codexlink", "bridge", "mcp"]
6
+ }
@@ -0,0 +1,9 @@
1
+ BLUN_TELEGRAM_AGENT_NAME=default
2
+ BLUN_TELEGRAM_BOT_TOKEN=123456789:replace_me
3
+ BLUN_TELEGRAM_ALLOWED_CHAT_ID=1605241602
4
+ BLUN_TELEGRAM_CODEX_BIN=codex
5
+ BLUN_TELEGRAM_STATE_DIR=
6
+ BLUN_TELEGRAM_THREAD_ID=
7
+ BLUN_TELEGRAM_RESUME_TIMEOUT_MS=15000
8
+ BLUN_TELEGRAM_POLL_INTERVAL_MS=5000
9
+ BLUN_TELEGRAM_INJECT_INTERVAL_MS=15000
@@ -0,0 +1,8 @@
1
+ {
2
+ "mcpServers": {
3
+ "codexlink_telegram": {
4
+ "command": "node",
5
+ "args": ["./server.js"]
6
+ }
7
+ }
8
+ }
@@ -0,0 +1,68 @@
1
+ # CodexLink Telegram Plugin
2
+
3
+ This is the bundled Telegram plugin for CodexLink.
4
+
5
+ It is intentionally **not** an autonomous answer bot.
6
+
7
+ ## What it does
8
+
9
+ - polls Telegram updates into a local queue
10
+ - stores inbound and outbound history under a local state directory
11
+ - keeps private chats and group threads separated
12
+ - binds a live thread id
13
+ - injects the next queued Telegram message into that exact live thread
14
+ - sends explicit manual replies from the visible operator session
15
+
16
+ ## What it does not do
17
+
18
+ - no hidden second session
19
+ - no autonomous answer loop
20
+ - no background reply worker pretending to be the operator
21
+
22
+ ## State
23
+
24
+ Default state directory:
25
+
26
+ `%USERPROFILE%\\.codex\\channels\\codexlink-telegram`
27
+
28
+ Files created there:
29
+
30
+ - `.env`
31
+ - `state.json`
32
+ - `inbox.jsonl`
33
+ - `outbox.jsonl`
34
+ - `activity.log`
35
+ - `prompts/`
36
+ - `responses/`
37
+ - `poller.pid`
38
+ - `dispatcher.pid`
39
+ - `responder.pid`
40
+
41
+ ## Env
42
+
43
+ Copy `.env.example` to `.env` in the state directory or export env vars:
44
+
45
+ - `BLUN_TELEGRAM_AGENT_NAME`
46
+ - `BLUN_TELEGRAM_BOT_TOKEN`
47
+ - `BLUN_TELEGRAM_ALLOWED_CHAT_ID`
48
+ - `BLUN_TELEGRAM_CODEX_BIN`
49
+ - `BLUN_TELEGRAM_THREAD_ID`
50
+ - `BLUN_TELEGRAM_RESUME_TIMEOUT_MS`
51
+
52
+ ## Tools
53
+
54
+ - `bridge_status`
55
+ - `bridge_bind_current_thread`
56
+ - `bridge_poll_once`
57
+ - `bridge_list_queue`
58
+ - `bridge_inject_next`
59
+ - `bridge_reply`
60
+ - `bridge_relay_once`
61
+ - `bridge_tail_activity`
62
+
63
+ ## Runtime split
64
+
65
+ - `poller.js` only fetches Telegram updates into the queue
66
+ - `dispatcher.js` only retries queue delivery into the bound live thread
67
+ - `responder.js` only relays finished answers back out
68
+ - none of them are allowed to invent an answer on their own
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ import { listLoadedThreadsOverWs, readThreadOverWs, startTextTurnOverWs, startThreadOverWs } from "./lib/app-server-client.js";
3
+
4
+ function writeLine(stream, text) {
5
+ return new Promise((resolve, reject) => {
6
+ stream.write(text, (error) => {
7
+ if (error) {
8
+ reject(error);
9
+ return;
10
+ }
11
+ resolve();
12
+ });
13
+ });
14
+ }
15
+
16
+ function parseArgs(argv) {
17
+ const [command = "", ...rest] = argv;
18
+ const args = { _: [], command };
19
+ for (let i = 0; i < rest.length; i += 1) {
20
+ const token = rest[i];
21
+ if (!token.startsWith("--")) {
22
+ args._.push(token);
23
+ continue;
24
+ }
25
+ const key = token.slice(2);
26
+ const next = rest[i + 1];
27
+ if (!next || next.startsWith("--")) {
28
+ args[key] = true;
29
+ continue;
30
+ }
31
+ args[key] = next;
32
+ i += 1;
33
+ }
34
+ return args;
35
+ }
36
+
37
+ async function main() {
38
+ const args = parseArgs(process.argv.slice(2));
39
+
40
+ if (args.command === "start-thread") {
41
+ const result = await startThreadOverWs({
42
+ wsUrl: args["ws-url"],
43
+ cwd: args.cwd,
44
+ model: args.model,
45
+ sandbox: args.sandbox,
46
+ approvalPolicy: args["approval-policy"],
47
+ personality: args.personality,
48
+ timeoutMs: args["timeout-ms"]
49
+ });
50
+ await writeLine(process.stdout, `${JSON.stringify(result)}\n`);
51
+ return 0;
52
+ }
53
+
54
+ if (args.command === "start-turn") {
55
+ const result = await startTextTurnOverWs({
56
+ wsUrl: args["ws-url"],
57
+ threadId: args["thread-id"],
58
+ text: args.text || "",
59
+ model: args.model,
60
+ effort: args.effort,
61
+ personality: args.personality,
62
+ timeoutMs: args["timeout-ms"]
63
+ });
64
+ await writeLine(process.stdout, `${JSON.stringify(result)}\n`);
65
+ return 0;
66
+ }
67
+
68
+ if (args.command === "read-thread") {
69
+ const result = await readThreadOverWs({
70
+ wsUrl: args["ws-url"],
71
+ threadId: args["thread-id"],
72
+ timeoutMs: args["timeout-ms"]
73
+ });
74
+ await writeLine(process.stdout, `${JSON.stringify(result)}\n`);
75
+ return 0;
76
+ }
77
+
78
+ if (args.command === "list-loaded") {
79
+ const result = await listLoadedThreadsOverWs({
80
+ wsUrl: args["ws-url"],
81
+ timeoutMs: args["timeout-ms"]
82
+ });
83
+ await writeLine(process.stdout, `${JSON.stringify(result)}\n`);
84
+ return 0;
85
+ }
86
+
87
+ await writeLine(process.stderr, "Usage: node app-server-cli.js <start-thread|start-turn|read-thread|list-loaded> [--key value]\n");
88
+ return 1;
89
+ }
90
+
91
+ try {
92
+ const code = await main();
93
+ process.exit(typeof code === "number" ? code : 0);
94
+ } catch (error) {
95
+ const message = error?.stack || error?.message || String(error);
96
+ await writeLine(process.stderr, `${message}\n`);
97
+ process.exit(1);
98
+ }
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import { injectNext } from "./lib/bridge.js";
3
+
4
+ const intervalMs = Number.parseInt(process.env.BLUN_TELEGRAM_INJECT_INTERVAL_MS || "1500", 10) || 1500;
5
+ let stopping = false;
6
+
7
+ function sleep(ms) {
8
+ return new Promise((resolve) => setTimeout(resolve, ms));
9
+ }
10
+
11
+ process.on("SIGINT", () => {
12
+ stopping = true;
13
+ });
14
+
15
+ process.on("SIGTERM", () => {
16
+ stopping = true;
17
+ });
18
+
19
+ async function main() {
20
+ while (!stopping) {
21
+ try {
22
+ const result = await injectNext("");
23
+ if (result.status !== "empty") {
24
+ console.log(JSON.stringify({ ts: new Date().toISOString(), kind: "inject", result }));
25
+ }
26
+ } catch (error) {
27
+ console.error(JSON.stringify({
28
+ ts: new Date().toISOString(),
29
+ kind: "error",
30
+ error: `${error}`
31
+ }));
32
+ }
33
+ await sleep(intervalMs);
34
+ }
35
+ }
36
+
37
+ await main();
@@ -0,0 +1,290 @@
1
+ function normalizeWsUrl(rawUrl) {
2
+ const value = String(rawUrl || "").trim();
3
+ if (!value) {
4
+ throw new Error("App-server websocket URL is missing.");
5
+ }
6
+ return value;
7
+ }
8
+
9
+ function makeError(message, meta = {}) {
10
+ const error = new Error(message);
11
+ Object.assign(error, meta);
12
+ return error;
13
+ }
14
+
15
+ function parseJson(data) {
16
+ const text = typeof data === "string" ? data : Buffer.from(data).toString("utf8");
17
+ return JSON.parse(text);
18
+ }
19
+
20
+ function buildInitializeRequest(id) {
21
+ return {
22
+ jsonrpc: "2.0",
23
+ id,
24
+ method: "initialize",
25
+ params: {
26
+ clientInfo: {
27
+ name: "codexlink-telegram",
28
+ version: "0.2.0"
29
+ },
30
+ capabilities: {
31
+ experimentalApi: true
32
+ }
33
+ }
34
+ };
35
+ }
36
+
37
+ function buildInitializedNotification() {
38
+ return {
39
+ jsonrpc: "2.0",
40
+ method: "initialized"
41
+ };
42
+ }
43
+
44
+ function extractThreadId(response) {
45
+ return response?.result?.thread?.id
46
+ || response?.result?.threadId
47
+ || response?.result?.id
48
+ || "";
49
+ }
50
+
51
+ function extractTurnId(response) {
52
+ return response?.result?.turn?.id
53
+ || response?.result?.turnId
54
+ || response?.result?.id
55
+ || "";
56
+ }
57
+
58
+ function extractThreadPath(response) {
59
+ return response?.result?.thread?.path
60
+ || response?.result?.path
61
+ || "";
62
+ }
63
+
64
+ export class AppServerClient {
65
+ constructor(wsUrl, options = {}) {
66
+ this.wsUrl = normalizeWsUrl(wsUrl);
67
+ this.timeoutMs = Number.parseInt(String(options.timeoutMs || "15000"), 10) || 15000;
68
+ this.socket = null;
69
+ this.pending = new Map();
70
+ this.nextId = 1;
71
+ this.connected = false;
72
+ }
73
+
74
+ async connect() {
75
+ if (this.connected && this.socket?.readyState === WebSocket.OPEN) {
76
+ return;
77
+ }
78
+
79
+ const socket = new WebSocket(this.wsUrl);
80
+ this.socket = socket;
81
+
82
+ await new Promise((resolve, reject) => {
83
+ const timer = setTimeout(() => {
84
+ reject(makeError(`Timed out connecting to ${this.wsUrl}`));
85
+ }, this.timeoutMs);
86
+
87
+ socket.addEventListener("open", () => {
88
+ clearTimeout(timer);
89
+ resolve();
90
+ }, { once: true });
91
+
92
+ socket.addEventListener("error", (event) => {
93
+ clearTimeout(timer);
94
+ reject(makeError(`WebSocket connection failed for ${this.wsUrl}`, { cause: event?.error || event }));
95
+ }, { once: true });
96
+ });
97
+
98
+ socket.addEventListener("message", (event) => {
99
+ let message;
100
+ try {
101
+ message = parseJson(event.data);
102
+ } catch (error) {
103
+ return;
104
+ }
105
+
106
+ if (message && Object.prototype.hasOwnProperty.call(message, "id")) {
107
+ const key = String(message.id);
108
+ const pending = this.pending.get(key);
109
+ if (!pending) {
110
+ return;
111
+ }
112
+ this.pending.delete(key);
113
+ clearTimeout(pending.timer);
114
+ if (message.error) {
115
+ pending.reject(makeError(message.error.message || "App-server request failed.", {
116
+ code: message.error.code,
117
+ data: message.error.data
118
+ }));
119
+ return;
120
+ }
121
+ pending.resolve(message);
122
+ }
123
+ });
124
+
125
+ socket.addEventListener("close", () => {
126
+ this.connected = false;
127
+ for (const [key, pending] of this.pending.entries()) {
128
+ clearTimeout(pending.timer);
129
+ pending.reject(makeError("App-server websocket closed before request completed."));
130
+ this.pending.delete(key);
131
+ }
132
+ });
133
+
134
+ const initId = String(this.nextId++);
135
+ const initPromise = this._waitForResponse(initId);
136
+ await this._sendRaw(buildInitializeRequest(Number(initId)));
137
+ const initializeResponse = await initPromise;
138
+ if (!initializeResponse?.result) {
139
+ throw makeError("App-server initialize returned no result.");
140
+ }
141
+ await this._sendRaw(buildInitializedNotification());
142
+
143
+ this.connected = true;
144
+ }
145
+
146
+ async request(method, params, options = {}) {
147
+ await this.connect();
148
+ const id = String(this.nextId++);
149
+ const payload = {
150
+ jsonrpc: "2.0",
151
+ id,
152
+ method,
153
+ params
154
+ };
155
+
156
+ const responsePromise = this._waitForResponse(id, options.timeoutMs);
157
+ await this._sendRaw(payload);
158
+ return responsePromise;
159
+ }
160
+
161
+ async close() {
162
+ if (!this.socket) {
163
+ return;
164
+ }
165
+ try {
166
+ this.socket.close();
167
+ } catch {
168
+ // Ignore shutdown errors.
169
+ }
170
+ this.connected = false;
171
+ this.socket = null;
172
+ }
173
+
174
+ async _sendRaw(payload) {
175
+ const text = JSON.stringify(payload);
176
+ this.socket.send(text);
177
+ }
178
+
179
+ _waitForResponse(id, timeoutOverride) {
180
+ const timeoutMs = Number.parseInt(String(timeoutOverride || this.timeoutMs), 10) || this.timeoutMs;
181
+ return new Promise((resolve, reject) => {
182
+ const timer = setTimeout(() => {
183
+ this.pending.delete(id);
184
+ reject(makeError(`App-server request ${id} timed out.`));
185
+ }, timeoutMs);
186
+
187
+ this.pending.set(id, { resolve, reject, timer });
188
+ });
189
+ }
190
+ }
191
+
192
+ export async function startThreadOverWs(options) {
193
+ const client = new AppServerClient(options.wsUrl, { timeoutMs: options.timeoutMs || 20000 });
194
+ try {
195
+ const response = await client.request("thread/start", {
196
+ cwd: options.cwd || null,
197
+ model: options.model || null,
198
+ sandbox: options.sandbox || null,
199
+ approvalPolicy: options.approvalPolicy || null,
200
+ personality: options.personality || null,
201
+ threadSource: "user",
202
+ sessionStartSource: "startup"
203
+ }, { timeoutMs: options.timeoutMs || 20000 });
204
+
205
+ const threadId = extractThreadId(response);
206
+ if (!threadId) {
207
+ throw makeError("App-server thread/start returned no thread id.");
208
+ }
209
+
210
+ return {
211
+ ok: true,
212
+ threadId,
213
+ response
214
+ };
215
+ } finally {
216
+ await client.close();
217
+ }
218
+ }
219
+
220
+ export async function startTextTurnOverWs(options) {
221
+ const client = new AppServerClient(options.wsUrl, { timeoutMs: options.timeoutMs || 20000 });
222
+ try {
223
+ const response = await client.request("turn/start", {
224
+ threadId: options.threadId,
225
+ input: [
226
+ {
227
+ type: "text",
228
+ text: options.text
229
+ }
230
+ ],
231
+ model: options.model || null,
232
+ effort: options.effort || null,
233
+ personality: options.personality || null
234
+ }, { timeoutMs: options.timeoutMs || 20000 });
235
+
236
+ return {
237
+ ok: true,
238
+ busy: false,
239
+ turnId: extractTurnId(response),
240
+ response
241
+ };
242
+ } catch (error) {
243
+ const details = `${error?.message || error}`.toLowerCase();
244
+ const busy = details.includes("active turn")
245
+ || details.includes("cannot accept")
246
+ || details.includes("already running")
247
+ || details.includes("busy");
248
+
249
+ return {
250
+ ok: false,
251
+ busy,
252
+ error
253
+ };
254
+ } finally {
255
+ await client.close();
256
+ }
257
+ }
258
+
259
+ export async function readThreadOverWs(options) {
260
+ const client = new AppServerClient(options.wsUrl, { timeoutMs: options.timeoutMs || 10000 });
261
+ try {
262
+ const response = await client.request("thread/read", {
263
+ threadId: options.threadId
264
+ }, { timeoutMs: options.timeoutMs || 10000 });
265
+ const threadId = extractThreadId(response);
266
+ return {
267
+ ok: Boolean(threadId),
268
+ threadId,
269
+ threadPath: extractThreadPath(response),
270
+ response
271
+ };
272
+ } finally {
273
+ await client.close();
274
+ }
275
+ }
276
+
277
+ export async function listLoadedThreadsOverWs(options) {
278
+ const client = new AppServerClient(options.wsUrl, { timeoutMs: options.timeoutMs || 10000 });
279
+ try {
280
+ const response = await client.request("thread/loaded/list", {}, { timeoutMs: options.timeoutMs || 10000 });
281
+ const data = Array.isArray(response?.result?.data) ? response.result.data : [];
282
+ return {
283
+ ok: true,
284
+ data,
285
+ response
286
+ };
287
+ } finally {
288
+ await client.close();
289
+ }
290
+ }