@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,140 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
+ import { bindCurrentThread, bridgeStatus, injectNext, listQueue, pollOnce, relayRepliesOnce, reply, tailActivity } from "./lib/bridge.js";
6
+ import { loadConfig } from "./lib/env.js";
7
+ import { ensureStateLayout } from "./lib/paths.js";
8
+ import { ensureBackgroundSidecars } from "./lib/sidecars.js";
9
+
10
+ ensureStateLayout();
11
+ ensureBackgroundSidecars(loadConfig());
12
+
13
+ function textResult(value) {
14
+ return {
15
+ content: [
16
+ {
17
+ type: "text",
18
+ text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
19
+ }
20
+ ]
21
+ };
22
+ }
23
+
24
+ const server = new Server(
25
+ {
26
+ name: "codexlink-telegram",
27
+ version: "0.1.0"
28
+ },
29
+ {
30
+ capabilities: {
31
+ tools: {}
32
+ }
33
+ }
34
+ );
35
+
36
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
37
+ tools: [
38
+ {
39
+ name: "bridge_status",
40
+ description: "Show BLUN Telegram bridge status, bound thread, queue depth, and recent activity pointers.",
41
+ inputSchema: { type: "object", properties: {} }
42
+ },
43
+ {
44
+ name: "bridge_bind_current_thread",
45
+ description: "Bind the real Codex thread id that queued Telegram messages should inject into. If omitted, CODEX_THREAD_ID is used.",
46
+ inputSchema: {
47
+ type: "object",
48
+ properties: {
49
+ thread_id: { type: "string", description: "Explicit Codex thread UUID to bind." }
50
+ }
51
+ }
52
+ },
53
+ {
54
+ name: "bridge_poll_once",
55
+ description: "Poll Telegram one time and append any allowed inbound messages to the BLUN queue.",
56
+ inputSchema: { type: "object", properties: {} }
57
+ },
58
+ {
59
+ name: "bridge_list_queue",
60
+ description: "List the most recent queued or processed bridge messages.",
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: {
64
+ limit: { type: "number", description: "How many queue items to return. Default 10." }
65
+ }
66
+ }
67
+ },
68
+ {
69
+ name: "bridge_inject_next",
70
+ description: "Inject the next queued Telegram message into the bound real Codex thread. If that thread is busy, the message remains queued.",
71
+ inputSchema: {
72
+ type: "object",
73
+ properties: {
74
+ thread_id: { type: "string", description: "Optional explicit thread id override for this injection." }
75
+ }
76
+ }
77
+ },
78
+ {
79
+ name: "bridge_reply",
80
+ description: "Send an explicit manual Telegram reply from the real operator/CLI.",
81
+ inputSchema: {
82
+ type: "object",
83
+ properties: {
84
+ text: { type: "string", description: "Reply text to send." },
85
+ chat_id: { type: "string", description: "Optional chat id override. Defaults to the latest inbound chat." },
86
+ reply_to_message_id: { type: "string", description: "Optional Telegram message id to reply under." },
87
+ telegram_thread_id: { type: "string", description: "Optional Telegram topic/thread id for forum-style group topics." }
88
+ },
89
+ required: ["text"]
90
+ }
91
+ },
92
+ {
93
+ name: "bridge_relay_once",
94
+ description: "Read the active Codex session file and relay any completed Telegram-originated final answers back to Telegram.",
95
+ inputSchema: { type: "object", properties: {} }
96
+ },
97
+ {
98
+ name: "bridge_tail_activity",
99
+ description: "Read the last lines from the local BLUN Telegram bridge activity log.",
100
+ inputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ lines: { type: "number", description: "Number of lines to read. Default 20." }
104
+ }
105
+ }
106
+ }
107
+ ]
108
+ }));
109
+
110
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
111
+ const args = request.params.arguments || {};
112
+ switch (request.params.name) {
113
+ case "bridge_status":
114
+ return textResult(bridgeStatus());
115
+ case "bridge_bind_current_thread":
116
+ return textResult(bindCurrentThread(args.thread_id || ""));
117
+ case "bridge_poll_once":
118
+ return textResult(await pollOnce());
119
+ case "bridge_list_queue":
120
+ return textResult(listQueue(Number(args.limit || 10)));
121
+ case "bridge_inject_next":
122
+ return textResult(await injectNext(args.thread_id || ""));
123
+ case "bridge_reply":
124
+ return textResult(
125
+ await reply(String(args.text || ""), {
126
+ chatId: args.chat_id || "",
127
+ replyToMessageId: args.reply_to_message_id || "",
128
+ telegramThreadId: args.telegram_thread_id || ""
129
+ })
130
+ );
131
+ case "bridge_relay_once":
132
+ return textResult(await relayRepliesOnce());
133
+ case "bridge_tail_activity":
134
+ return textResult(tailActivity(Number(args.lines || 20)));
135
+ default:
136
+ throw new Error(`Unknown tool: ${request.params.name}`);
137
+ }
138
+ });
139
+
140
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import { loadConfig } from "./lib/env.js";
3
+ import { ensureStateLayout } from "./lib/paths.js";
4
+ import { ensureBackgroundSidecars } from "./lib/sidecars.js";
5
+
6
+ ensureStateLayout();
7
+ const result = ensureBackgroundSidecars(loadConfig());
8
+ process.stdout.write(`${JSON.stringify(result)}\n`);
@@ -0,0 +1,160 @@
1
+ param(
2
+ [string]$Profile = "default"
3
+ )
4
+
5
+ $ErrorActionPreference = "Stop"
6
+
7
+ function Try-ReadJson {
8
+ param([string]$Path)
9
+ if (-not (Test-Path $Path)) { return $null }
10
+ try { return Get-Content -Raw -Path $Path | ConvertFrom-Json } catch { return $null }
11
+ }
12
+
13
+ function Read-DotEnvFile {
14
+ param([string]$Path)
15
+ $values = @{}
16
+ if (-not (Test-Path $Path)) { return $values }
17
+ foreach ($line in (Get-Content -Path $Path)) {
18
+ if (-not $line) { continue }
19
+ if ($line.Trim().StartsWith("#")) { continue }
20
+ $parts = $line -split "=", 2
21
+ if ($parts.Count -ne 2) { continue }
22
+ $values[$parts[0].Trim()] = $parts[1]
23
+ }
24
+ return $values
25
+ }
26
+
27
+ function Test-PidAlive {
28
+ param([int]$ProcId)
29
+ if ($ProcId -le 0) { return $false }
30
+ return $null -ne (Get-Process -Id $ProcId -ErrorAction SilentlyContinue)
31
+ }
32
+
33
+ function Resolve-ConfiguredPath {
34
+ param([string]$Value, [string]$RuntimeRoot)
35
+ if (-not $Value) { return "" }
36
+ $expanded = [Environment]::ExpandEnvironmentVariables($Value)
37
+ if ([System.IO.Path]::IsPathRooted($expanded)) { return $expanded }
38
+ return [System.IO.Path]::GetFullPath((Join-Path $RuntimeRoot $expanded))
39
+ }
40
+
41
+ function Get-TelegramPluginRoot {
42
+ param([string]$RuntimeRoot)
43
+
44
+ $candidates = @()
45
+ if ($env:BLUN_CODEX_TELEGRAM_PLUGIN_ROOT) {
46
+ $candidates += $env:BLUN_CODEX_TELEGRAM_PLUGIN_ROOT
47
+ }
48
+ $candidates += (Join-Path $RuntimeRoot "telegram-plugin")
49
+
50
+ foreach ($candidate in $candidates) {
51
+ if (-not $candidate) { continue }
52
+ if ((Test-Path (Join-Path $candidate "app-server-cli.js")) -and (Test-Path (Join-Path $candidate "sidecar-manager.js"))) {
53
+ return $candidate
54
+ }
55
+ }
56
+
57
+ return $null
58
+ }
59
+
60
+ $runtimeRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
61
+ $profilePath = Join-Path $runtimeRoot ("profiles\" + $Profile.ToLower() + ".json")
62
+ $profileJson = Try-ReadJson -Path $profilePath
63
+ $profileAgent = if ($profileJson -and $profileJson.agent_name) { [string]$profileJson.agent_name } else { $Profile.ToLower() }
64
+ $runtimeDir = Join-Path $env:USERPROFILE (".codex\\runtimes\\" + $profileAgent)
65
+ $stateDir = if ($profileJson -and $profileJson.telegram -and $profileJson.telegram.state_dir) {
66
+ Resolve-ConfiguredPath -Value ([string]$profileJson.telegram.state_dir) -RuntimeRoot $runtimeRoot
67
+ } else {
68
+ Join-Path $env:USERPROFILE (".codex\\channels\\telegram-" + $profileAgent)
69
+ }
70
+ $currentRuntime = Try-ReadJson -Path (Join-Path $runtimeDir "current-remote-runtime.json")
71
+ $state = Try-ReadJson -Path (Join-Path $stateDir "state.json")
72
+ $envFile = Read-DotEnvFile -Path (Join-Path $stateDir ".env")
73
+ $loadedThreads = @()
74
+ $queue = @($state.queue)
75
+ $queued = @($queue | Where-Object { $_.status -eq "queued" })
76
+ $submitted = @($queue | Where-Object { $_.status -eq "submitted" })
77
+ $delivered = @($queue | Where-Object { $_.status -eq "delivered" })
78
+ $errors = @($queue | Where-Object { $_.status -eq "error" })
79
+ $pendingReplies = @($state.pendingReplies | Where-Object { -not $_.sentAt -and $_.status -ne "error" })
80
+ $pollerPid = if (Test-Path (Join-Path $stateDir "poller.pid")) { (Get-Content -Raw (Join-Path $stateDir "poller.pid")).Trim() } else { $null }
81
+ $dispatcherPid = if (Test-Path (Join-Path $stateDir "dispatcher.pid")) { (Get-Content -Raw (Join-Path $stateDir "dispatcher.pid")).Trim() } else { $null }
82
+ $responderPid = if (Test-Path (Join-Path $stateDir "responder.pid")) { (Get-Content -Raw (Join-Path $stateDir "responder.pid")).Trim() } else { $null }
83
+ $stateThreadId = if ($state.currentThreadId) { [string]$state.currentThreadId } else { "" }
84
+ $telegramPluginRoot = Get-TelegramPluginRoot -RuntimeRoot $runtimeRoot
85
+
86
+ if ($currentRuntime) {
87
+ if ($stateThreadId) {
88
+ if ($currentRuntime.PSObject.Properties.Name.Contains("thread_id")) {
89
+ $currentRuntime.thread_id = $stateThreadId
90
+ } else {
91
+ $currentRuntime | Add-Member -NotePropertyName "thread_id" -NotePropertyValue $stateThreadId
92
+ }
93
+ }
94
+ if ($pollerPid) {
95
+ if ($currentRuntime.PSObject.Properties.Name.Contains("poller_pid")) {
96
+ $currentRuntime.poller_pid = $pollerPid
97
+ } else {
98
+ $currentRuntime | Add-Member -NotePropertyName "poller_pid" -NotePropertyValue $pollerPid
99
+ }
100
+ }
101
+ if ($dispatcherPid) {
102
+ if ($currentRuntime.PSObject.Properties.Name.Contains("dispatcher_pid")) {
103
+ $currentRuntime.dispatcher_pid = $dispatcherPid
104
+ } else {
105
+ $currentRuntime | Add-Member -NotePropertyName "dispatcher_pid" -NotePropertyValue $dispatcherPid
106
+ }
107
+ }
108
+ if ($responderPid) {
109
+ if ($currentRuntime.PSObject.Properties.Name.Contains("responder_pid")) {
110
+ $currentRuntime.responder_pid = $responderPid
111
+ } else {
112
+ $currentRuntime | Add-Member -NotePropertyName "responder_pid" -NotePropertyValue $responderPid
113
+ }
114
+ }
115
+ }
116
+
117
+ if ($envFile["BLUN_TELEGRAM_APP_SERVER_WS_URL"] -and $telegramPluginRoot) {
118
+ try {
119
+ $bootstrapScript = Join-Path $telegramPluginRoot "app-server-cli.js"
120
+ $loaded = & node $bootstrapScript "list-loaded" "--ws-url" $envFile["BLUN_TELEGRAM_APP_SERVER_WS_URL"] | ConvertFrom-Json
121
+ $loadedThreads = @($loaded.data)
122
+ } catch {
123
+ $loadedThreads = @()
124
+ }
125
+ }
126
+
127
+ $result = [ordered]@{
128
+ profile = $profileAgent
129
+ state_dir = $stateDir
130
+ plugin_root = $telegramPluginRoot
131
+ active_ws = $envFile["BLUN_TELEGRAM_APP_SERVER_WS_URL"]
132
+ env_thread_id = $envFile["BLUN_TELEGRAM_THREAD_ID"]
133
+ state_thread_id = $stateThreadId
134
+ active_thread_id = if ($stateThreadId) { $stateThreadId } else { $envFile["BLUN_TELEGRAM_THREAD_ID"] }
135
+ current_runtime = $currentRuntime
136
+ loaded_threads = $loadedThreads
137
+ queue_depth = $queued.Count
138
+ submitted_depth = $submitted.Count
139
+ pending_reply_depth = $pendingReplies.Count
140
+ delivered_count = $delivered.Count
141
+ error_count = $errors.Count
142
+ history_count = $queue.Count
143
+ last_inbound = $state.lastInbound
144
+ last_outbound = $state.lastOutbound
145
+ poller_pid = $pollerPid
146
+ dispatcher_pid = $dispatcherPid
147
+ responder_pid = $responderPid
148
+ }
149
+
150
+ if ($result.poller_pid) {
151
+ $result["poller_alive"] = Test-PidAlive -ProcId ([int]$result.poller_pid)
152
+ }
153
+ if ($result.dispatcher_pid) {
154
+ $result["dispatcher_alive"] = Test-PidAlive -ProcId ([int]$result.dispatcher_pid)
155
+ }
156
+ if ($result.responder_pid) {
157
+ $result["responder_alive"] = Test-PidAlive -ProcId ([int]$result.responder_pid)
158
+ }
159
+
160
+ $result | ConvertTo-Json -Depth 8