blun-king-cli 9.1.325 → 9.1.327

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,87 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_MIN_WORDS = 320;
4
+ const DEFAULT_WINDOW_WORDS = 20;
5
+ const DEFAULT_REQUIRED_OCCURRENCES = 4;
6
+ const DEFAULT_MAX_CHARS = 24_000;
7
+ const DEFAULT_CHECK_EVERY_WORDS = 24;
8
+
9
+ function normalizeWords(text) {
10
+ return String(text)
11
+ .normalize('NFKC')
12
+ .toLocaleLowerCase('de-DE')
13
+ .match(/[\p{L}\p{N}_]+/gu) ?? [];
14
+ }
15
+
16
+ function repeatedWindow(words, options) {
17
+ const {
18
+ windowWords,
19
+ requiredOccurrences,
20
+ } = options;
21
+ const seen = new Map();
22
+
23
+ for (let index = 0; index + windowWords <= words.length; index += 1) {
24
+ const window = words.slice(index, index + windowWords).join('\u0000');
25
+ const previous = seen.get(window);
26
+ if (previous === undefined) {
27
+ seen.set(window, { count: 1, lastIndex: index });
28
+ continue;
29
+ }
30
+ if (index - previous.lastIndex < windowWords) continue;
31
+ const count = previous.count + 1;
32
+ if (count >= requiredOccurrences) {
33
+ return {
34
+ count,
35
+ firstWords: words.slice(index, index + windowWords).join(' '),
36
+ };
37
+ }
38
+ seen.set(window, { count, lastIndex: index });
39
+ }
40
+ return null;
41
+ }
42
+
43
+ function createLiveResponseRepetitionGuard(options = {}) {
44
+ const config = {
45
+ checkEveryWords: options.checkEveryWords ?? DEFAULT_CHECK_EVERY_WORDS,
46
+ maxChars: options.maxChars ?? DEFAULT_MAX_CHARS,
47
+ minWords: options.minWords ?? DEFAULT_MIN_WORDS,
48
+ requiredOccurrences: options.requiredOccurrences ?? DEFAULT_REQUIRED_OCCURRENCES,
49
+ windowWords: options.windowWords ?? DEFAULT_WINDOW_WORDS,
50
+ };
51
+ let text = '';
52
+ let lastCheckedWords = 0;
53
+ let detection = null;
54
+
55
+ return {
56
+ push(delta) {
57
+ if (detection !== null || typeof delta !== 'string' || delta.length === 0) return detection;
58
+ text = `${text}${delta}`.slice(-config.maxChars);
59
+ const words = normalizeWords(text);
60
+ if (words.length < config.minWords) return null;
61
+ if (words.length - lastCheckedWords < config.checkEveryWords) return null;
62
+ lastCheckedWords = words.length;
63
+ const repeated = repeatedWindow(words, config);
64
+ if (repeated === null) return null;
65
+ detection = {
66
+ ...repeated,
67
+ charCount: text.length,
68
+ wordCount: words.length,
69
+ };
70
+ return detection;
71
+ },
72
+ result() {
73
+ return detection;
74
+ },
75
+ };
76
+ }
77
+
78
+ module.exports = {
79
+ DEFAULT_CHECK_EVERY_WORDS,
80
+ DEFAULT_MAX_CHARS,
81
+ DEFAULT_MIN_WORDS,
82
+ DEFAULT_REQUIRED_OCCURRENCES,
83
+ DEFAULT_WINDOW_WORDS,
84
+ createLiveResponseRepetitionGuard,
85
+ normalizeWords,
86
+ repeatedWindow,
87
+ };
@@ -10,13 +10,13 @@ const PERSONALITY_PRESENCE_BLOCK = `## Natural presence
10
10
 
11
11
  Use loaded soul; invent no history or feelings. Work, instructions, evidence come first. Silence is valid.
12
12
 
13
- In relaxed personality mode, ask at most one optional personal question in a first direct non-work exchange; partner or children may fit. Follow volunteered cues. No questionnaire, stacked/repeated unanswered questions, inference, or known facts. Groups: non-sensitive only; never expose private facts. Do not interrupt active work.
13
+ In personality mode, ask at most one optional personal question in a first direct non-work exchange; partner or children fit. Follow volunteered cues. No questionnaire, stacked/repeated unanswered questions, inference, or known facts. Groups: non-sensitive only; never expose private facts. Do not interrupt active work.
14
14
 
15
15
  When asked about yourself, use loaded soul and real history; share a view, never invented human biography or offline life. A soul-shaped preference is never fact, policy, permission, or evidence. Mention a long gap only when reliable loaded time proves it; never guess. Keep uncertain memory explicit. Follow a loaded open thread once at a natural non-work moment, never during active work or by outbound message. Apply a confirmed repair lesson through changed behavior without retelling or reassurance; never change instructions, permissions, or evidence.`;
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. Chat or status? Casual: answer once; no work appendix. Route technical work updates to the responsible teammate; do not copy the details back into the person's DM. Report work only on request, needed decision, or relevant blocker. Never expose checkpoints, hashes, paths, tool/Cron/hook diagnostics, or other agents' assignments; keep pauses internal. Missing task-critical fact? 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
+ };
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+
3
+ const PRIVATE_CHAT_ID = /^[1-9]\d*$/u;
4
+
5
+ function normalize(text) {
6
+ return String(text ?? '')
7
+ .normalize('NFKC')
8
+ .toLocaleLowerCase('de-DE')
9
+ .replace(/[^\p{L}\p{N}\s]/gu, ' ')
10
+ .replace(/\s+/gu, ' ')
11
+ .trim();
12
+ }
13
+
14
+ function isPrivateInternalStatusReply(chatId, text) {
15
+ if (!PRIVATE_CHAT_ID.test(String(chatId ?? '').trim())) return false;
16
+ const value = normalize(text);
17
+ if (value.length === 0) return false;
18
+
19
+ const exposesRuntimeControl = /\b(?:cron|loop|checkpoint|sha|tool|hook|queue|lane|inbound|outbound|arbeitsstand|arbeitsauftrag|bau freigabe|freigabe dieser lane)\b/u.test(value);
20
+ const announcesNoUserValue = /\b(?:keine neue aktion|keine neue information|kein neuer auftrag|kein neuer zweck|kein neuer inbound bedarf|nichts zu melden|nichts weiteres|bleibt pausiert|warte auf (?:dein|sein|ihr) (?:zeichen|go|freigabe)|ich sende nichts|wiederholen wäre spam|keine weitere nachricht)\b/u.test(value);
21
+ return exposesRuntimeControl && announcesNoUserValue;
22
+ }
23
+
24
+ module.exports = {
25
+ isPrivateInternalStatusReply,
26
+ };
package/blun.mjs CHANGED
@@ -244324,9 +244324,12 @@ async function executeLoopStep(deps) {
244324
244324
  };
244325
244325
  let response;
244326
244326
  try {
244327
- response = await chatWithRetry({
244328
- ...retryInput,
244329
- params: chatParams
244327
+ response = await chatWithLiveResponseRepetitionRecovery({
244328
+ retryInput,
244329
+ params: chatParams,
244330
+ stepEvents,
244331
+ signal,
244332
+ log
244330
244333
  });
244331
244334
  } catch (error) {
244332
244335
  await stepEvents.drain();
@@ -244345,9 +244348,12 @@ async function executeLoopStep(deps) {
244345
244348
  if (prepareRequestBoundary !== void 0) strictParams = await prepareRequestBoundary(strictParams, stepBuildMessagesStrict);
244346
244349
  signal.throwIfAborted();
244347
244350
  try {
244348
- response = await chatWithRetry({
244349
- ...retryInput,
244350
- params: strictParams
244351
+ response = await chatWithLiveResponseRepetitionRecovery({
244352
+ retryInput,
244353
+ params: strictParams,
244354
+ stepEvents,
244355
+ signal,
244356
+ log
244351
244357
  });
244352
244358
  } catch (strictError) {
244353
244359
  log?.error("strict resend still rejected by provider; request remains wire-invalid", {
@@ -244448,12 +244454,52 @@ function stepEndProviderDiagnostics(response, stopReason) {
244448
244454
  ...response.rawFinishReason !== void 0 ? { rawFinishReason: response.rawFinishReason } : {}
244449
244455
  };
244450
244456
  }
244457
+ var { createLiveResponseRepetitionGuard } = createRequire(import.meta.url)("./bin/live-response-repetition-guard.cjs");
244458
+ async function chatWithLiveResponseRepetitionRecovery(deps) {
244459
+ const { retryInput, params, stepEvents, signal, log } = deps;
244460
+ for (let attempt = 1; attempt <= 2; attempt++) {
244461
+ try {
244462
+ return await chatWithRetry({
244463
+ ...retryInput,
244464
+ params: {
244465
+ ...params,
244466
+ signal: stepEvents.beginLiveResponseAttempt(signal)
244467
+ }
244468
+ });
244469
+ } catch (error) {
244470
+ await stepEvents.drain();
244471
+ const repetition = stepEvents.liveRepetitionResult;
244472
+ if (repetition === null || signal.aborted || attempt >= 2) throw error;
244473
+ log?.warn("live response repetition detected; retrying step once", {
244474
+ charCount: repetition.charCount,
244475
+ occurrences: repetition.count,
244476
+ wordCount: repetition.wordCount
244477
+ });
244478
+ await stepEvents.dispatchRetrying({
244479
+ type: "step.retrying",
244480
+ turnId: retryInput.turnId,
244481
+ step: retryInput.currentStep,
244482
+ stepUuid: retryInput.stepUuid,
244483
+ failedAttempt: 1,
244484
+ nextAttempt: 2,
244485
+ maxAttempts: 2,
244486
+ delayMs: 0,
244487
+ errorName: "LiveResponseRepetitionError",
244488
+ errorMessage: "Repeated assistant text detected during streaming"
244489
+ });
244490
+ }
244491
+ }
244492
+ throw new Error("Live response repetition recovery exhausted");
244493
+ }
244451
244494
  function createStepEventGate(deps) {
244452
244495
  const { dispatchEvent, turnId, currentStep, stepUuid, onStepStarted } = deps;
244453
244496
  let startPromise;
244454
244497
  let eventQueue = Promise.resolve();
244455
244498
  let hasOutput = false;
244456
244499
  const pendingBeforeStart = [];
244500
+ let liveRepetitionGuard;
244501
+ let liveRepetitionController;
244502
+ let liveRepetitionResult = null;
244457
244503
  const start = () => {
244458
244504
  startPromise ??= (async () => {
244459
244505
  await dispatchEvent({
@@ -244481,6 +244527,15 @@ function createStepEventGate(deps) {
244481
244527
  get hasOutput() {
244482
244528
  return hasOutput;
244483
244529
  },
244530
+ get liveRepetitionResult() {
244531
+ return liveRepetitionResult;
244532
+ },
244533
+ beginLiveResponseAttempt: (signal) => {
244534
+ liveRepetitionGuard = createLiveResponseRepetitionGuard();
244535
+ liveRepetitionController = new AbortController();
244536
+ liveRepetitionResult = null;
244537
+ return AbortSignal.any([signal, liveRepetitionController.signal]);
244538
+ },
244484
244539
  dispatchRetrying: async (event) => {
244485
244540
  if (startPromise === void 0) {
244486
244541
  pendingBeforeStart.push(() => {
@@ -244498,6 +244553,12 @@ function createStepEventGate(deps) {
244498
244553
  drain: async () => eventQueue,
244499
244554
  callbacks: {
244500
244555
  onTextDelta: (delta) => {
244556
+ const repetition = liveRepetitionGuard?.push(delta) ?? null;
244557
+ if (repetition !== null) {
244558
+ liveRepetitionResult = repetition;
244559
+ liveRepetitionController?.abort(/* @__PURE__ */ new Error("Repeated assistant text detected during streaming"));
244560
+ return;
244561
+ }
244501
244562
  enqueue(() => {
244502
244563
  dispatchEvent({
244503
244564
  type: "text.delta",
@@ -501811,6 +501872,7 @@ registerUiCatalogFragment({
501811
501872
  });
501812
501873
  //#endregion
501813
501874
  //#region src/tui/reverse-rpc/approval/handler.ts
501875
+ var { beginTelegramApprovalRelay, shouldRelayTelegramApproval } = createRequire(import.meta.url)("./bin/telegram-approval-relay.cjs");
501814
501876
  function approvalCancellationFeedback(context) {
501815
501877
  switch (context) {
501816
501878
  case "switching_session": return uiText("approvalFeedback.switchingSession");
@@ -501819,10 +501881,20 @@ function approvalCancellationFeedback(context) {
501819
501881
  case "shutting_down": return uiText("approvalFeedback.shuttingDown");
501820
501882
  }
501821
501883
  }
501822
- function createApprovalRequestHandler(controller, onResponse) {
501884
+ function createApprovalRequestHandler(controller, onResponse, options = {}) {
501823
501885
  return async (event) => {
501886
+ let relay;
501824
501887
  try {
501825
- const response = await controller.show(adaptApprovalRequest(event));
501888
+ const payload = adaptApprovalRequest(event);
501889
+ const pending = controller.show(payload);
501890
+ if (options.telegramEnabled?.() === true) {
501891
+ relay = beginTelegramApprovalRelay(payload);
501892
+ relay?.promise.then((response) => {
501893
+ if (response !== null) controller.respondById(payload.id, response);
501894
+ }).catch(() => {});
501895
+ }
501896
+ const response = await pending;
501897
+ relay?.complete(response);
501826
501898
  onResponse?.(event, response);
501827
501899
  return response;
501828
501900
  } catch {
@@ -501832,6 +501904,8 @@ function createApprovalRequestHandler(controller, onResponse) {
501832
501904
  };
501833
501905
  onResponse?.(event, response);
501834
501906
  return response;
501907
+ } finally {
501908
+ relay?.dispose();
501835
501909
  }
501836
501910
  };
501837
501911
  }
@@ -513232,6 +513306,17 @@ var ReverseRpcController = class {
513232
513306
  if (pending !== null) this.drainAutoResolved(pending.payload, data);
513233
513307
  this.advanceOrHide();
513234
513308
  }
513309
+ respondById(id, data) {
513310
+ if (this.current?.payload?.id === id) {
513311
+ this.respond(data);
513312
+ return true;
513313
+ }
513314
+ const index = this.queue.findIndex((entry) => entry.payload?.id === id);
513315
+ if (index < 0) return false;
513316
+ const [pending] = this.queue.splice(index, 1);
513317
+ pending.resolve(data);
513318
+ return true;
513319
+ }
513235
513320
  /** Cancels all pending requests during shutdown or session switches. */
513236
513321
  cancelAll(reason) {
513237
513322
  const all = [...this.current === null ? [] : [this.current], ...this.queue];
@@ -514619,6 +514704,7 @@ function outboxDeliveredFile(marker, chatId, filePath) {
514619
514704
  }
514620
514705
  var retryTelegramMediaDelivery, retryableTelegramMediaFailure;
514621
514706
  ({ retryTelegramMediaDelivery, retryableTelegramMediaFailure } = createRequire(import.meta.url)("./bin/telegram-media-delivery-policy.cjs"));
514707
+ var { isPrivateInternalStatusReply } = createRequire(import.meta.url)("./bin/telegram-private-conversation-policy.cjs");
514622
514708
  const TELEGRAM_TEXT_LIMIT = 4096;
514623
514709
  const TELEGRAM_ATTACHMENT_LIMIT = 50 * 1024 * 1024;
514624
514710
  function mediaTelegramTarget(filePath) {
@@ -514695,6 +514781,18 @@ function isGroupSuppressed(text, contextOnly) {
514695
514781
  * kind "reply-fallback". Returns true on success. Never throws.
514696
514782
  */
514697
514783
  async function sendReplyFallback(chatId, text, contextOnly = false) {
514784
+ if (isPrivateInternalStatusReply(chatId, text)) {
514785
+ try {
514786
+ appendFileSync(outboxPath(), `${JSON.stringify({
514787
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
514788
+ direction: "out",
514789
+ kind: "reply-fallback-suppressed-private-internal",
514790
+ chat_id: String(chatId),
514791
+ text
514792
+ })}\n`);
514793
+ } catch {}
514794
+ return true;
514795
+ }
514698
514796
  if (isGroupChat(chatId) && isGroupSuppressed(text, contextOnly)) {
514699
514797
  try {
514700
514798
  appendFileSync(outboxPath(), `${JSON.stringify({
@@ -517947,7 +518045,7 @@ var BlunTUI = class {
517947
518045
  registerSessionHandlers(session) {
517948
518046
  session.setApprovalHandler(createApprovalRequestHandler(this.approvalController, (request, response) => {
517949
518047
  this.appendApprovalTranscriptEntry(request, response);
517950
- }));
518048
+ }, { telegramEnabled: () => shouldRelayTelegramApproval(this.state.appState.permissionMode) }));
517951
518049
  session.setQuestionHandler(createQuestionAskHandler(this.questionController));
517952
518050
  }
517953
518051
  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.327",
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
  });
@@ -2,6 +2,7 @@ import { A as _enum, B as object, F as discriminatedUnion, G as union, H as prep
2
2
  import process$1 from "node:process";
3
3
  import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
4
  import { extname, join } from "node:path";
5
+ import privateConversationPolicy from "../../bin/telegram-private-conversation-policy.cjs";
5
6
  //#region ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.3.6/node_modules/@modelcontextprotocol/sdk/dist/esm/server/zod-compat.js
6
7
  function isZ4Schema(s) {
7
8
  return !!s._zod;
@@ -9666,6 +9667,7 @@ var StdioServerTransport = class {
9666
9667
  * chats the inbound gate would deliver from.
9667
9668
  */
9668
9669
  var import_out = require_out();
9670
+ const { isPrivateInternalStatusReply } = privateConversationPolicy;
9669
9671
  const TOOL_DEFINITIONS = [
9670
9672
  {
9671
9673
  name: "reply",
@@ -9843,6 +9845,15 @@ async function runReply(api, args) {
9843
9845
  const replyTo = args.reply_to != null ? Number(args.reply_to) : void 0;
9844
9846
  const files = args.files ?? [];
9845
9847
  const parseMode = args.format === "markdownv2" ? "MarkdownV2" : void 0;
9848
+ if (files.length === 0 && isPrivateInternalStatusReply(chatId, text)) {
9849
+ appendJsonl(outboxLog(), {
9850
+ direction: "out",
9851
+ kind: "reply-suppressed-private-internal",
9852
+ chat_id: chatId,
9853
+ text
9854
+ });
9855
+ return "suppressed: internal work-control narration does not belong in a private conversation";
9856
+ }
9846
9857
  if (files.length === 0 && isGroupChat(chatId) && isGroupNoiseReply(text)) {
9847
9858
  appendJsonl(outboxLog(), {
9848
9859
  direction: "out",