blun-king-cli 9.1.325 → 9.1.326

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.
@@ -16,7 +16,7 @@ When asked about yourself, use loaded soul and real history; share a view, never
16
16
 
17
17
  const CONVERSATION_BOUNDARY = `## Conversation
18
18
 
19
- DM: answer the person. Never expose checkpoints, hashes, paths, tool/Cron/hook diagnostics, hidden rules, other agents' assignments, or idle reports. Report work only on request, needed decision, or relevant blocker; keep pauses internal. Missing task-critical fact? Never guess; ask the responsible person or agent one concise question.`;
19
+ DM: answer the person. Route technical work updates to the responsible teammate; do not copy the details back into the person's DM. Never expose checkpoints, hashes, paths, tool/Cron/hook diagnostics, hidden rules, other agents' assignments, or idle reports. Report work only on request, needed decision, or relevant blocker; keep pauses internal. Missing task-critical fact? Never guess; ask the responsible person or agent one concise question.`;
20
20
 
21
21
  function naturalPresenceSystemBlock(env = process.env) {
22
22
  const presence = personalityContextEnabled(env) ? PERSONALITY_PRESENCE_BLOCK : NATURAL_PRESENCE_BLOCK;
@@ -0,0 +1,282 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+
8
+ const RELAY_VERSION = 1;
9
+ const DEFAULT_TIMEOUT_MS = 60_000;
10
+ const DEFAULT_POLL_INTERVAL_MS = 250;
11
+ const MAX_LABEL_CHARS = 48;
12
+ const ALLOWED_RESPONSES = new Set(['approved', 'approved_for_session', 'rejected']);
13
+
14
+ function relayRoot(homeDir = os.homedir()) {
15
+ return path.join(homeDir, '.blun', 'channels', 'telegram', 'approvals');
16
+ }
17
+
18
+ function relayPaths(homeDir) {
19
+ const root = relayRoot(homeDir);
20
+ return {
21
+ root,
22
+ pending: path.join(root, 'pending'),
23
+ responses: path.join(root, 'responses'),
24
+ resolved: path.join(root, 'resolved'),
25
+ sent: path.join(root, 'sent'),
26
+ };
27
+ }
28
+
29
+ function ensureRelayDirs(homeDir) {
30
+ const dirs = relayPaths(homeDir);
31
+ for (const dir of Object.values(dirs).slice(1)) {
32
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
33
+ }
34
+ return dirs;
35
+ }
36
+
37
+ function safeId(value) {
38
+ return crypto.createHash('sha256').update(String(value)).digest('hex').slice(0, 24);
39
+ }
40
+
41
+ function requestFileName(requestId) {
42
+ return `${safeId(requestId)}.json`;
43
+ }
44
+
45
+ function atomicWriteJson(file, value) {
46
+ const temp = `${file}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`;
47
+ fs.writeFileSync(temp, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600 });
48
+ fs.renameSync(temp, file);
49
+ }
50
+
51
+ function readJson(file) {
52
+ try {
53
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ function compactLabel(value) {
60
+ const normalized = String(value ?? '').replace(/\s+/gu, ' ').trim();
61
+ if (normalized.length <= MAX_LABEL_CHARS) return normalized;
62
+ return `${normalized.slice(0, MAX_LABEL_CHARS - 1)}…`;
63
+ }
64
+
65
+ function resolveTelegramApprovalTarget(allowFrom, configuredChatId) {
66
+ const allowed = Array.isArray(allowFrom)
67
+ ? [...new Set(allowFrom.map(String).filter((value) => value.length > 0))]
68
+ : [];
69
+ const configured = String(configuredChatId ?? '').trim();
70
+ if (configured.length > 0) return allowed.includes(configured) ? configured : null;
71
+ return allowed.length === 1 ? allowed[0] : null;
72
+ }
73
+
74
+ function parseTelegramApprovalCallback(data) {
75
+ const match = /^blun-appr:([a-f0-9]{24}):(\d+)$/u.exec(String(data ?? ''));
76
+ if (match === null) return null;
77
+ const choiceIndex = Number(match[2]);
78
+ if (!Number.isSafeInteger(choiceIndex)) return null;
79
+ return { requestId: match[1], choiceIndex };
80
+ }
81
+
82
+ function buildTelegramApprovalCard(request) {
83
+ return {
84
+ text: request.toolName,
85
+ reply_markup: {
86
+ inline_keyboard: request.choices.map((choice) => [{
87
+ text: choice.label,
88
+ callback_data: `blun-appr:${request.requestId}:${choice.index}`,
89
+ }]),
90
+ },
91
+ };
92
+ }
93
+
94
+ function isAuthorizedTelegramApprovalCallback(expectedChatId, senderId, chatId) {
95
+ if (expectedChatId === null || expectedChatId === undefined) return false;
96
+ return String(senderId ?? '') === String(expectedChatId)
97
+ && String(chatId ?? '') === String(expectedChatId);
98
+ }
99
+
100
+ function shouldRelayTelegramApproval(permissionMode) {
101
+ return permissionMode !== 'yolo';
102
+ }
103
+
104
+ function buildTelegramApprovalRequest(payload, options = {}) {
105
+ const now = Number.isFinite(options.now) ? options.now : Date.now();
106
+ const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
107
+ ? options.timeoutMs
108
+ : DEFAULT_TIMEOUT_MS;
109
+ const requestId = crypto.randomBytes(12).toString('hex');
110
+ const nonce = crypto.randomBytes(16).toString('hex');
111
+ const choices = (Array.isArray(payload?.choices) ? payload.choices : [])
112
+ .filter((choice) => choice?.requires_feedback !== true && ALLOWED_RESPONSES.has(choice?.response))
113
+ .map((choice, index) => ({
114
+ index,
115
+ label: compactLabel(choice.label || choice.response),
116
+ response: choice.response,
117
+ }));
118
+ if (choices.length === 0) return null;
119
+ return {
120
+ v: RELAY_VERSION,
121
+ requestId,
122
+ nonce,
123
+ approvalId: String(payload?.id ?? ''),
124
+ toolName: compactLabel(payload?.tool_name || 'Werkzeug'),
125
+ choices,
126
+ createdAt: now,
127
+ expiresAt: now + timeoutMs,
128
+ };
129
+ }
130
+
131
+ function responseForChoice(request, choiceIndex) {
132
+ const choice = request?.choices?.find((candidate) => candidate.index === choiceIndex);
133
+ if (choice === undefined) return null;
134
+ if (choice.response === 'approved_for_session') {
135
+ return { decision: 'approved', scope: 'session', selectedLabel: choice.label };
136
+ }
137
+ return {
138
+ decision: choice.response === 'approved' ? 'approved' : 'rejected',
139
+ selectedLabel: choice.label,
140
+ };
141
+ }
142
+
143
+ function validateTelegramApprovalResponse(request, response, now = Date.now()) {
144
+ if (request?.v !== RELAY_VERSION || response?.v !== RELAY_VERSION) return null;
145
+ if (request.requestId !== response.requestId || request.nonce !== response.nonce) return null;
146
+ if (!Number.isFinite(request.expiresAt) || now > request.expiresAt) return null;
147
+ if (!Number.isInteger(response.choiceIndex)) return null;
148
+ return responseForChoice(request, response.choiceIndex);
149
+ }
150
+
151
+ function beginTelegramApprovalRelay(payload, options = {}) {
152
+ const request = buildTelegramApprovalRequest(payload, options);
153
+ if (request === null) return null;
154
+ const dirs = ensureRelayDirs(options.homeDir);
155
+ const fileName = requestFileName(request.requestId);
156
+ const pendingFile = path.join(dirs.pending, fileName);
157
+ const responseFile = path.join(dirs.responses, fileName);
158
+ const resolvedFile = path.join(dirs.resolved, fileName);
159
+ atomicWriteJson(pendingFile, request);
160
+
161
+ let settled = false;
162
+ let interval;
163
+ let resolvePromise;
164
+ const promise = new Promise((resolve) => { resolvePromise = resolve; });
165
+ const finish = (response) => {
166
+ if (settled) return false;
167
+ settled = true;
168
+ if (interval !== undefined) clearInterval(interval);
169
+ atomicWriteJson(resolvedFile, {
170
+ v: RELAY_VERSION,
171
+ requestId: request.requestId,
172
+ resolvedAt: Date.now(),
173
+ response,
174
+ });
175
+ resolvePromise(response);
176
+ return true;
177
+ };
178
+ const poll = () => {
179
+ if (settled) return;
180
+ if (Date.now() > request.expiresAt) {
181
+ finish(null);
182
+ return;
183
+ }
184
+ const raw = readJson(responseFile);
185
+ if (raw === null) return;
186
+ const response = validateTelegramApprovalResponse(request, raw);
187
+ if (response !== null) finish(response);
188
+ };
189
+ interval = setInterval(poll, options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
190
+ interval.unref?.();
191
+ return {
192
+ request,
193
+ promise,
194
+ complete: finish,
195
+ poll,
196
+ dispose: () => {
197
+ if (interval !== undefined) clearInterval(interval);
198
+ interval = undefined;
199
+ },
200
+ };
201
+ }
202
+
203
+ function listPendingTelegramApprovals(options = {}) {
204
+ const dirs = ensureRelayDirs(options.homeDir);
205
+ const now = Number.isFinite(options.now) ? options.now : Date.now();
206
+ const results = [];
207
+ for (const name of fs.readdirSync(dirs.pending)) {
208
+ if (!name.endsWith('.json')) continue;
209
+ if (fs.existsSync(path.join(dirs.resolved, name))) continue;
210
+ const request = readJson(path.join(dirs.pending, name));
211
+ if (request?.v !== RELAY_VERSION || now > request.expiresAt) continue;
212
+ results.push(request);
213
+ }
214
+ return results.sort((left, right) => left.createdAt - right.createdAt);
215
+ }
216
+
217
+ function writeTelegramApprovalResponse(request, choiceIndex, metadata = {}, options = {}) {
218
+ if (responseForChoice(request, choiceIndex) === null) return false;
219
+ const dirs = ensureRelayDirs(options.homeDir);
220
+ const fileName = requestFileName(request.requestId);
221
+ if (fs.existsSync(path.join(dirs.resolved, fileName))) return false;
222
+ const response = {
223
+ v: RELAY_VERSION,
224
+ requestId: request.requestId,
225
+ nonce: request.nonce,
226
+ choiceIndex,
227
+ senderId: String(metadata.senderId ?? ''),
228
+ chatId: String(metadata.chatId ?? ''),
229
+ respondedAt: Number.isFinite(metadata.now) ? metadata.now : Date.now(),
230
+ };
231
+ const file = path.join(dirs.responses, fileName);
232
+ try {
233
+ fs.writeFileSync(file, `${JSON.stringify(response)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
234
+ return true;
235
+ } catch (error) {
236
+ if (error?.code === 'EEXIST') return false;
237
+ throw error;
238
+ }
239
+ }
240
+
241
+ function markTelegramApprovalSent(request, metadata = {}, options = {}) {
242
+ const dirs = ensureRelayDirs(options.homeDir);
243
+ const file = path.join(dirs.sent, requestFileName(request.requestId));
244
+ const value = {
245
+ v: RELAY_VERSION,
246
+ requestId: request.requestId,
247
+ chatId: String(metadata.chatId ?? ''),
248
+ messageId: String(metadata.messageId ?? ''),
249
+ sentAt: Number.isFinite(metadata.now) ? metadata.now : Date.now(),
250
+ };
251
+ try {
252
+ fs.writeFileSync(file, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
253
+ return true;
254
+ } catch (error) {
255
+ if (error?.code === 'EEXIST') return false;
256
+ throw error;
257
+ }
258
+ }
259
+
260
+ function wasTelegramApprovalSent(request, options = {}) {
261
+ const dirs = ensureRelayDirs(options.homeDir);
262
+ return fs.existsSync(path.join(dirs.sent, requestFileName(request.requestId)));
263
+ }
264
+
265
+ module.exports = {
266
+ DEFAULT_TIMEOUT_MS,
267
+ beginTelegramApprovalRelay,
268
+ buildTelegramApprovalCard,
269
+ buildTelegramApprovalRequest,
270
+ listPendingTelegramApprovals,
271
+ markTelegramApprovalSent,
272
+ relayPaths,
273
+ resolveTelegramApprovalTarget,
274
+ parseTelegramApprovalCallback,
275
+ isAuthorizedTelegramApprovalCallback,
276
+ requestFileName,
277
+ responseForChoice,
278
+ shouldRelayTelegramApproval,
279
+ validateTelegramApprovalResponse,
280
+ wasTelegramApprovalSent,
281
+ writeTelegramApprovalResponse,
282
+ };
package/blun.mjs CHANGED
@@ -501811,6 +501811,7 @@ registerUiCatalogFragment({
501811
501811
  });
501812
501812
  //#endregion
501813
501813
  //#region src/tui/reverse-rpc/approval/handler.ts
501814
+ var { beginTelegramApprovalRelay, shouldRelayTelegramApproval } = createRequire(import.meta.url)("./bin/telegram-approval-relay.cjs");
501814
501815
  function approvalCancellationFeedback(context) {
501815
501816
  switch (context) {
501816
501817
  case "switching_session": return uiText("approvalFeedback.switchingSession");
@@ -501819,10 +501820,20 @@ function approvalCancellationFeedback(context) {
501819
501820
  case "shutting_down": return uiText("approvalFeedback.shuttingDown");
501820
501821
  }
501821
501822
  }
501822
- function createApprovalRequestHandler(controller, onResponse) {
501823
+ function createApprovalRequestHandler(controller, onResponse, options = {}) {
501823
501824
  return async (event) => {
501825
+ let relay;
501824
501826
  try {
501825
- const response = await controller.show(adaptApprovalRequest(event));
501827
+ const payload = adaptApprovalRequest(event);
501828
+ const pending = controller.show(payload);
501829
+ if (options.telegramEnabled?.() === true) {
501830
+ relay = beginTelegramApprovalRelay(payload);
501831
+ relay?.promise.then((response) => {
501832
+ if (response !== null) controller.respondById(payload.id, response);
501833
+ }).catch(() => {});
501834
+ }
501835
+ const response = await pending;
501836
+ relay?.complete(response);
501826
501837
  onResponse?.(event, response);
501827
501838
  return response;
501828
501839
  } catch {
@@ -501832,6 +501843,8 @@ function createApprovalRequestHandler(controller, onResponse) {
501832
501843
  };
501833
501844
  onResponse?.(event, response);
501834
501845
  return response;
501846
+ } finally {
501847
+ relay?.dispose();
501835
501848
  }
501836
501849
  };
501837
501850
  }
@@ -513232,6 +513245,17 @@ var ReverseRpcController = class {
513232
513245
  if (pending !== null) this.drainAutoResolved(pending.payload, data);
513233
513246
  this.advanceOrHide();
513234
513247
  }
513248
+ respondById(id, data) {
513249
+ if (this.current?.payload?.id === id) {
513250
+ this.respond(data);
513251
+ return true;
513252
+ }
513253
+ const index = this.queue.findIndex((entry) => entry.payload?.id === id);
513254
+ if (index < 0) return false;
513255
+ const [pending] = this.queue.splice(index, 1);
513256
+ pending.resolve(data);
513257
+ return true;
513258
+ }
513235
513259
  /** Cancels all pending requests during shutdown or session switches. */
513236
513260
  cancelAll(reason) {
513237
513261
  const all = [...this.current === null ? [] : [this.current], ...this.queue];
@@ -517947,7 +517971,7 @@ var BlunTUI = class {
517947
517971
  registerSessionHandlers(session) {
517948
517972
  session.setApprovalHandler(createApprovalRequestHandler(this.approvalController, (request, response) => {
517949
517973
  this.appendApprovalTranscriptEntry(request, response);
517950
- }));
517974
+ }, { telegramEnabled: () => shouldRelayTelegramApproval(this.state.appState.permissionMode) }));
517951
517975
  session.setQuestionHandler(createQuestionAskHandler(this.questionController));
517952
517976
  }
517953
517977
  async fetchSessions(scope = this.state.sessionsScope) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.325",
3
+ "version": "9.1.326",
4
4
  "description": "BLUN CLI - your own AI agent with a Telegram channel. Get it done. With BLUN.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -7,7 +7,9 @@ import { Readable, Writable } from "node:stream";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { hostname } from "node:os";
9
9
  import remoteStatusPolicy from "../../bin/telegram-remote-status-policy.cjs";
10
+ import telegramApprovalRelay from "../../bin/telegram-approval-relay.cjs";
10
11
  const { buildTelegramRemoteStatus, resolveTelegramRemoteVersion } = remoteStatusPolicy;
12
+ const { buildTelegramApprovalCard, isAuthorizedTelegramApprovalCallback, listPendingTelegramApprovals, markTelegramApprovalSent, parseTelegramApprovalCallback, resolveTelegramApprovalTarget, wasTelegramApprovalSent, writeTelegramApprovalResponse } = telegramApprovalRelay;
11
13
  //#region ../../node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs
12
14
  const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
13
15
  function normalizeWindowsPath(input = "") {
@@ -4432,6 +4434,36 @@ function startApprovalWatcher(bot) {
4432
4434
  }
4433
4435
  }, 5e3).unref();
4434
4436
  }
4437
+
4438
+ function telegramApprovalTarget() {
4439
+ const access = readAccess();
4440
+ return resolveTelegramApprovalTarget(access.allowFrom, process.env.BLUN_TELEGRAM_APPROVAL_CHAT_ID);
4441
+ }
4442
+
4443
+ let approvalSweepBusy = false;
4444
+ function startToolApprovalWatcher(bot) {
4445
+ setInterval(async () => {
4446
+ if (approvalSweepBusy) return;
4447
+ approvalSweepBusy = true;
4448
+ try {
4449
+ const chatId = telegramApprovalTarget();
4450
+ if (chatId === null) return;
4451
+ for (const request of listPendingTelegramApprovals()) {
4452
+ if (wasTelegramApprovalSent(request)) continue;
4453
+ const card = buildTelegramApprovalCard(request);
4454
+ const message = await bot.api.sendMessage(chatId, card.text, { reply_markup: card.reply_markup });
4455
+ markTelegramApprovalSent(request, {
4456
+ chatId,
4457
+ messageId: message.message_id
4458
+ });
4459
+ }
4460
+ } catch (error) {
4461
+ process.stderr.write(`telegram channel: approval relay failed: ${String(error)}\n`);
4462
+ } finally {
4463
+ approvalSweepBusy = false;
4464
+ }
4465
+ }, 500).unref();
4466
+ }
4435
4467
  /**
4436
4468
  * Retry polling with backoff on ANY error — a single ETIMEDOUT/ECONNRESET
4437
4469
  * must not leave the bridge deaf while the process stays alive. Persistent
@@ -4662,6 +4694,26 @@ async function handleInbound(event) {
4662
4694
  else process.stderr.write(`telegram bridge: headless off — inbound ${chatId}:${String(msgId)} nicht zugestellt (kein TUI-Fenster offen)\n`);
4663
4695
  }
4664
4696
  registerTypeHandlers(bot, TOKEN, handleInbound);
4697
+ bot.on("callback_query:data", async (ctx) => {
4698
+ const callback = parseTelegramApprovalCallback(ctx.callbackQuery.data);
4699
+ if (callback === null) return;
4700
+ const request = listPendingTelegramApprovals().find((candidate) => candidate.requestId === callback.requestId);
4701
+ const expectedChatId = telegramApprovalTarget();
4702
+ if (request === void 0) {
4703
+ await ctx.answerCallbackQuery();
4704
+ return;
4705
+ }
4706
+ if (!isAuthorizedTelegramApprovalCallback(expectedChatId, ctx.from.id, ctx.chat?.id)) {
4707
+ await ctx.answerCallbackQuery();
4708
+ return;
4709
+ }
4710
+ const accepted = writeTelegramApprovalResponse(request, callback.choiceIndex, {
4711
+ senderId: ctx.from.id,
4712
+ chatId: ctx.chat.id
4713
+ });
4714
+ await ctx.answerCallbackQuery();
4715
+ if (accepted && ctx.callbackQuery.message !== void 0) await ctx.api.editMessageReplyMarkup(ctx.chat.id, ctx.callbackQuery.message.message_id, { inline_keyboard: [] }).catch(() => {});
4716
+ });
4665
4717
  bot.command("start", async (ctx) => {
4666
4718
  if (dmCommandGate(toInboundCtx(ctx, "")) === null) return;
4667
4719
  await ctx.reply("This bot bridges Telegram to a BLUN session.\n\nTo pair:\n1. DM me anything — you'll get a 6-char code\n2. In your BLUN terminal: /telegram:access pair <code>\n\nAfter that, DMs here reach that session.");
@@ -4748,6 +4800,7 @@ if (!HEADLESS_ENABLED) {
4748
4800
  }, 2e4).unref();
4749
4801
  }
4750
4802
  startApprovalWatcher(bot);
4803
+ startToolApprovalWatcher(bot);
4751
4804
  pollWithBackoff(bot, () => shuttingDown, (username) => {
4752
4805
  botUsername = username;
4753
4806
  });