blun-king-cli 9.1.416 → 9.1.418

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/LIESMICH.txt CHANGED
@@ -67,6 +67,22 @@ Bereinigung und Mikrokompaktierung verwenden dafür dieselbe semantische Grenze.
67
67
  Erst nach der nachweislichen Auswertung darf ein Ergebnis archiviert oder
68
68
  gekürzt werden.
69
69
 
70
+ Ab BLUN King 9.1.417 schneiden automatische Telegram-Rückfallantworten Texte
71
+ nicht mehr bei 4.096 Zeichen ab. Längere Antworten werden in
72
+ aufeinanderfolgenden Nachrichten vollständig zugestellt; dabei bleiben jedes
73
+ Zeichen und jedes Unicode-Surrogatpaar erhalten. Im Ausgangsprotokoll stehen
74
+ sämtliche zurückgegebenen Nachrichten-IDs zusammen mit dem vollständigen Text.
75
+ Schlägt ein Teil fehl, endet die Zustellung an dieser Stelle, statt die Antwort
76
+ fälschlich als vollständig zu melden.
77
+
78
+ Ab BLUN King 9.1.418 übernimmt der Identitätsgraph die von Telegram bestätigte
79
+ Unterscheidung zwischen Menschen und Bots. Bereits vorhandene
80
+ Telegram-Kontakte, die mangels dieses Merkmals als Menschen angelegt wurden,
81
+ werden bei der nächsten eindeutig als Bot bestätigten Nachricht einmalig als
82
+ Agent korrigiert. Alle Rollen, Zuständigkeiten, Beziehungsnotizen und sonstigen
83
+ Felder bleiben unverändert. Ein Agent wird niemals zu einer Person
84
+ zurückgestuft; Namen oder Benutzernamen dienen nicht als Beweis.
85
+
70
86
  Zuverlässiger King-Start
71
87
  -----------------------
72
88
  Bei einer ausdrücklich als wiederholbar gekennzeichneten Serverüberlastung
package/README.md CHANGED
@@ -82,6 +82,22 @@ Bereinigung und Mikrokompaktierung verwenden dafür dieselbe semantische Grenze.
82
82
  Erst nach der nachweislichen Auswertung darf ein Ergebnis archiviert oder
83
83
  gekürzt werden.
84
84
 
85
+ Ab BLUN King 9.1.417 schneiden automatische Telegram-Rückfallantworten Texte
86
+ nicht mehr bei 4.096 Zeichen ab. Längere Antworten werden in
87
+ aufeinanderfolgenden Nachrichten vollständig zugestellt; dabei bleiben jedes
88
+ Zeichen und jedes Unicode-Surrogatpaar erhalten. Im Ausgangsprotokoll stehen
89
+ sämtliche zurückgegebenen Nachrichten-IDs zusammen mit dem vollständigen Text.
90
+ Schlägt ein Teil fehl, endet die Zustellung an dieser Stelle, statt die Antwort
91
+ fälschlich als vollständig zu melden.
92
+
93
+ Ab BLUN King 9.1.418 übernimmt der Identitätsgraph die von Telegram bestätigte
94
+ Unterscheidung zwischen Menschen und Bots. Bereits vorhandene
95
+ Telegram-Kontakte, die mangels dieses Merkmals als Menschen angelegt wurden,
96
+ werden bei der nächsten eindeutig als Bot bestätigten Nachricht einmalig als
97
+ Agent korrigiert. Alle Rollen, Zuständigkeiten, Beziehungsnotizen und sonstigen
98
+ Felder bleiben unverändert. Ein Agent wird niemals zu einer Person
99
+ zurückgestuft; Namen oder Benutzernamen dienen nicht als Beweis.
100
+
85
101
  `Strg+C` und `Esc` brechen einen aktiven Zug zuverlässig ab, ohne den bereits
86
102
  geschriebenen Entwurf zu löschen. Das gilt auch bei Autovervollständigung,
87
103
  Geistervorschlägen, Bash-Eingabe und einer noch offenen Mehrzeileneingabe.
@@ -93,6 +93,51 @@ function createJsonOnce(root, segments, value) {
93
93
  }
94
94
  }
95
95
 
96
+ function updateJsonAtomically(root, segments, update) {
97
+ const target = path.resolve(root, ...segments);
98
+ if (!isInside(root, target)) return false;
99
+ let handle;
100
+ let temp = '';
101
+ try {
102
+ const stat = fs.lstatSync(target);
103
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_FILE_BYTES) return false;
104
+ const original = fs.readFileSync(target, 'utf8');
105
+ const current = JSON.parse(original);
106
+ if (!current || typeof current !== 'object' || Array.isArray(current)) return false;
107
+ const next = update(current);
108
+ if (!next || typeof next !== 'object' || Array.isArray(next)) return false;
109
+ const serialized = `${JSON.stringify(next, null, 2)}\n`;
110
+ if (Buffer.byteLength(serialized, 'utf8') > MAX_FILE_BYTES) return false;
111
+
112
+ const parent = path.dirname(target);
113
+ const realParent = fs.realpathSync(parent);
114
+ if (!isInside(root, realParent)) return false;
115
+ temp = path.join(parent, `.${path.basename(target)}.${process.pid}.${Date.now()}.tmp`);
116
+ handle = fs.openSync(temp, 'wx', 0o600);
117
+ fs.writeFileSync(handle, serialized, 'utf8');
118
+ fs.fsyncSync(handle);
119
+ fs.closeSync(handle);
120
+ handle = undefined;
121
+
122
+ const currentStat = fs.lstatSync(target);
123
+ if (!currentStat.isFile() || currentStat.isSymbolicLink() || fs.readFileSync(target, 'utf8') !== original) return false;
124
+ fs.renameSync(temp, target);
125
+ temp = '';
126
+ return true;
127
+ } catch {
128
+ return false;
129
+ } finally {
130
+ if (handle !== undefined) fs.closeSync(handle);
131
+ if (temp) {
132
+ try { fs.rmSync(temp, { force: true }); } catch {}
133
+ }
134
+ }
135
+ }
136
+
137
+ function isExplicitTelegramBot(meta) {
138
+ return meta?.is_bot === true || String(meta?.is_bot ?? '').toLowerCase() === 'true';
139
+ }
140
+
96
141
  function cleanText(value, maxChars = 240) {
97
142
  if (typeof value !== 'string') return '';
98
143
  return value.replace(/[\u0000-\u001f\u007f]+/gu, ' ').replace(/\s+/gu, ' ').trim().slice(0, maxChars);
@@ -485,19 +530,31 @@ function recordChannelIdentity(envelope, env = process.env) {
485
530
  const firstSeenAt = trustedTimestamp(meta.ts);
486
531
  const receivedAt = trustedTimestamp(meta.received_at) || firstSeenAt;
487
532
  const created = [];
488
- if (createJsonOnce(root, ['actors', `${actorId}.json`], {
533
+ const updated = [];
534
+ const actorSegments = ['actors', `${actorId}.json`];
535
+ const actorCreated = createJsonOnce(root, actorSegments, {
489
536
  version: 1,
490
537
  actor_id: actorId,
491
538
  provider: 'telegram',
492
539
  provider_subject_id: subjectId,
493
540
  display_name: displayName,
494
- kind: meta.is_bot === true || String(meta.is_bot ?? '').toLowerCase() === 'true' ? 'agent' : 'person',
541
+ kind: isExplicitTelegramBot(meta) ? 'agent' : 'person',
495
542
  role: '',
496
543
  responsibilities: [],
497
544
  traits: [],
498
545
  aliases: [],
499
546
  ...(firstSeenAt ? { first_seen_at: firstSeenAt } : {}),
500
- })) created.push('actor');
547
+ });
548
+ if (actorCreated) created.push('actor');
549
+ else if (isExplicitTelegramBot(meta) && updateJsonAtomically(root, actorSegments, (actor) => (
550
+ actor.version === 1
551
+ && actor.actor_id === actorId
552
+ && actor.provider === 'telegram'
553
+ && actor.provider_subject_id === subjectId
554
+ && actor.kind === 'person'
555
+ ? { ...actor, kind: 'agent' }
556
+ : undefined
557
+ ))) updated.push('actor_kind');
501
558
  if (createJsonOnce(root, ['agents', agentId, 'relationships', `${actorId}.json`], {
502
559
  version: 1,
503
560
  actor_id: actorId,
@@ -621,6 +678,7 @@ function recordChannelIdentity(envelope, env = process.env) {
621
678
  actor_id: actorId,
622
679
  ...(groupId ? { group_id: groupId } : {}),
623
680
  created,
681
+ updated,
624
682
  ...(learning ? { learning } : {}),
625
683
  ...(journal ? { journal } : {}),
626
684
  model_context: modelContext,
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const TELEGRAM_TEXT_LIMIT = 4096;
4
+
5
+ function preferredCut(text, start, hardEnd) {
6
+ const minimum = start + Math.floor((hardEnd - start) / 2);
7
+ const boundaries = [
8
+ ['\n\n', 2],
9
+ ['\n', 1],
10
+ [' ', 1],
11
+ ];
12
+ for (const [boundary, width] of boundaries) {
13
+ const index = text.lastIndexOf(boundary, hardEnd - 1);
14
+ if (index >= minimum) return index + width;
15
+ }
16
+ return hardEnd;
17
+ }
18
+
19
+ function preserveSurrogatePair(text, start, end) {
20
+ if (end <= start || end >= text.length) return end;
21
+ const before = text.charCodeAt(end - 1);
22
+ const after = text.charCodeAt(end);
23
+ const splitsPair = before >= 0xD800 && before <= 0xDBFF && after >= 0xDC00 && after <= 0xDFFF;
24
+ return splitsPair && end - 1 > start ? end - 1 : end;
25
+ }
26
+
27
+ function splitTelegramText(value, limit = TELEGRAM_TEXT_LIMIT) {
28
+ const text = typeof value === 'string' ? value : String(value ?? '');
29
+ if (text.length === 0) return [];
30
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
31
+ throw new TypeError('Telegram text chunk limit must be a positive safe integer');
32
+ }
33
+
34
+ const chunks = [];
35
+ let start = 0;
36
+ while (start < text.length) {
37
+ const hardEnd = Math.min(text.length, start + limit);
38
+ let end = hardEnd < text.length ? preferredCut(text, start, hardEnd) : hardEnd;
39
+ end = preserveSurrogatePair(text, start, end);
40
+ if (end <= start) end = hardEnd;
41
+ chunks.push(text.slice(start, end));
42
+ start = end;
43
+ }
44
+ return chunks;
45
+ }
46
+
47
+ async function sendTelegramTextChunks(value, sendChunk, limit = TELEGRAM_TEXT_LIMIT) {
48
+ if (typeof sendChunk !== 'function') {
49
+ throw new TypeError('Telegram chunk sender must be a function');
50
+ }
51
+ const chunks = splitTelegramText(value, limit);
52
+ const results = [];
53
+ for (let index = 0; index < chunks.length; index += 1) {
54
+ results.push(await sendChunk(chunks[index], index, chunks.length));
55
+ }
56
+ return { chunks, results };
57
+ }
58
+
59
+ module.exports = {
60
+ TELEGRAM_TEXT_LIMIT,
61
+ sendTelegramTextChunks,
62
+ splitTelegramText,
63
+ };
package/blun.mjs CHANGED
@@ -514830,6 +514830,7 @@ var createMediaAutoRetrievalController, parseAcceptedMediaJob, validateCompleted
514830
514830
  ({ createMediaAutoRetrievalController, parseAcceptedMediaJob } = createRequire(import.meta.url)("./bin/media-auto-retrieval-policy.cjs"));
514831
514831
  ({ validateCompletedMedia } = createRequire(import.meta.url)("./bin/media-result-policy.cjs"));
514832
514832
  var { findDuplicateReplyWithoutNewInbound, isPrivateInternalStatusReply, sanitizePrivateConversationReply } = createRequire(import.meta.url)("./bin/telegram-private-conversation-policy.cjs");
514833
+ var { sendTelegramTextChunks } = createRequire(import.meta.url)("./bin/telegram-text-chunk-policy.cjs");
514833
514834
  const TELEGRAM_TEXT_LIMIT = 4096;
514834
514835
  const TELEGRAM_ATTACHMENT_LIMIT = 50 * 1024 * 1024;
514835
514836
  function mediaTelegramTarget(filePath) {
@@ -514970,25 +514971,29 @@ async function sendReplyFallback(chatId, text, contextOnly = false) {
514970
514971
  }
514971
514972
  const token = botToken();
514972
514973
  if (token === void 0) return false;
514973
- const body = privateSafeText.length > TELEGRAM_TEXT_LIMIT ? `${privateSafeText.slice(0, TELEGRAM_TEXT_LIMIT - 1)}…` : privateSafeText;
514974
+ const messageIds = [];
514974
514975
  try {
514975
- const payload = await (await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
514976
- method: "POST",
514977
- headers: { "Content-Type": "application/json" },
514978
- body: JSON.stringify({
514979
- chat_id: chatId,
514980
- text: body
514981
- })
514982
- })).json();
514983
- if (payload.ok !== true) return false;
514976
+ await sendTelegramTextChunks(privateSafeText, async (body) => {
514977
+ const payload = await (await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
514978
+ method: "POST",
514979
+ headers: { "Content-Type": "application/json" },
514980
+ body: JSON.stringify({
514981
+ chat_id: chatId,
514982
+ text: body
514983
+ })
514984
+ })).json();
514985
+ if (payload.ok !== true) throw new Error("Telegram fallback chunk delivery failed");
514986
+ if (payload.result?.message_id !== void 0) messageIds.push(payload.result.message_id);
514987
+ return payload;
514988
+ }, TELEGRAM_TEXT_LIMIT);
514984
514989
  try {
514985
514990
  appendFileSync(outboxPath(), `${JSON.stringify({
514986
514991
  ts: (/* @__PURE__ */ new Date()).toISOString(),
514987
514992
  direction: "out",
514988
514993
  kind: "reply-fallback",
514989
514994
  chat_id: String(chatId),
514990
- message_ids: payload.result?.message_id !== void 0 ? [payload.result.message_id] : [],
514991
- text: body
514995
+ message_ids: messageIds,
514996
+ text: privateSafeText
514992
514997
  })}\n`);
514993
514998
  } catch {}
514994
514999
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.416",
3
+ "version": "9.1.418",
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": {
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const TELEGRAM_TEXT_LIMIT = 4096;
4
+
5
+ function preferredCut(text, start, hardEnd) {
6
+ const minimum = start + Math.floor((hardEnd - start) / 2);
7
+ const boundaries = [
8
+ ['\n\n', 2],
9
+ ['\n', 1],
10
+ [' ', 1],
11
+ ];
12
+ for (const [boundary, width] of boundaries) {
13
+ const index = text.lastIndexOf(boundary, hardEnd - 1);
14
+ if (index >= minimum) return index + width;
15
+ }
16
+ return hardEnd;
17
+ }
18
+
19
+ function preserveSurrogatePair(text, start, end) {
20
+ if (end <= start || end >= text.length) return end;
21
+ const before = text.charCodeAt(end - 1);
22
+ const after = text.charCodeAt(end);
23
+ const splitsPair = before >= 0xD800 && before <= 0xDBFF && after >= 0xDC00 && after <= 0xDFFF;
24
+ return splitsPair && end - 1 > start ? end - 1 : end;
25
+ }
26
+
27
+ function splitTelegramText(value, limit = TELEGRAM_TEXT_LIMIT) {
28
+ const text = typeof value === 'string' ? value : String(value ?? '');
29
+ if (text.length === 0) return [];
30
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
31
+ throw new TypeError('Telegram text chunk limit must be a positive safe integer');
32
+ }
33
+
34
+ const chunks = [];
35
+ let start = 0;
36
+ while (start < text.length) {
37
+ const hardEnd = Math.min(text.length, start + limit);
38
+ let end = hardEnd < text.length ? preferredCut(text, start, hardEnd) : hardEnd;
39
+ end = preserveSurrogatePair(text, start, end);
40
+ if (end <= start) end = hardEnd;
41
+ chunks.push(text.slice(start, end));
42
+ start = end;
43
+ }
44
+ return chunks;
45
+ }
46
+
47
+ async function sendTelegramTextChunks(value, sendChunk, limit = TELEGRAM_TEXT_LIMIT) {
48
+ if (typeof sendChunk !== 'function') {
49
+ throw new TypeError('Telegram chunk sender must be a function');
50
+ }
51
+ const chunks = splitTelegramText(value, limit);
52
+ const results = [];
53
+ for (let index = 0; index < chunks.length; index += 1) {
54
+ results.push(await sendChunk(chunks[index], index, chunks.length));
55
+ }
56
+ return { chunks, results };
57
+ }
58
+
59
+ module.exports = {
60
+ TELEGRAM_TEXT_LIMIT,
61
+ sendTelegramTextChunks,
62
+ splitTelegramText,
63
+ };
@@ -10,10 +10,12 @@ import remoteStatusPolicy from "../../bin/telegram-remote-status-policy.cjs";
10
10
  import consoleStatusPolicy from "../../bin/telegram-console-status-policy.cjs";
11
11
  import telegramApprovalRelay from "../../bin/telegram-approval-relay.cjs";
12
12
  import privateConversationPolicy from "../bin/telegram-private-conversation-policy.cjs";
13
+ import telegramTextChunkPolicy from "../bin/telegram-text-chunk-policy.cjs";
13
14
  const { buildTelegramRemoteStatus, resolveTelegramQueueSnapshot, resolveTelegramRemoteVersion } = remoteStatusPolicy;
14
15
  const { parseTelegramConsoleStatus } = consoleStatusPolicy;
15
16
  const { buildTelegramApprovalCard, isAuthorizedTelegramApprovalCallback, listPendingTelegramApprovals, markTelegramApprovalSent, parseTelegramApprovalCallback, resolveTelegramApprovalTarget, wasTelegramApprovalSent, writeTelegramApprovalResponse } = telegramApprovalRelay;
16
17
  const { findDuplicateReplyWithoutNewInbound, isPrivateInternalStatusReply, sanitizePrivateConversationReply } = privateConversationPolicy;
18
+ const { sendTelegramTextChunks } = telegramTextChunkPolicy;
17
19
  //#region ../../node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs
18
20
  const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
19
21
  function normalizeWindowsPath(input = "") {
@@ -4223,25 +4225,29 @@ async function sendReplyFallback(chatId, rawText) {
4223
4225
  return true;
4224
4226
  }
4225
4227
  const text = scrubSecrets(privateSafeText);
4226
- const body = text.length > TELEGRAM_TEXT_LIMIT ? `${text.slice(0, TELEGRAM_TEXT_LIMIT - 1)}…` : text;
4228
+ const messageIds = [];
4227
4229
  try {
4228
- const payload = await (await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
4229
- method: "POST",
4230
- headers: { "Content-Type": "application/json" },
4231
- body: JSON.stringify({
4232
- chat_id: chatId,
4233
- text: body
4234
- })
4235
- })).json();
4236
- if (payload.ok !== true) return false;
4230
+ await sendTelegramTextChunks(text, async (body) => {
4231
+ const payload = await (await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
4232
+ method: "POST",
4233
+ headers: { "Content-Type": "application/json" },
4234
+ body: JSON.stringify({
4235
+ chat_id: chatId,
4236
+ text: body
4237
+ })
4238
+ })).json();
4239
+ if (payload.ok !== true) throw new Error("Telegram fallback chunk delivery failed");
4240
+ if (payload.result?.message_id !== void 0) messageIds.push(payload.result.message_id);
4241
+ return payload;
4242
+ }, TELEGRAM_TEXT_LIMIT);
4237
4243
  try {
4238
4244
  appendFileSync(outboxLog(), `${JSON.stringify({
4239
4245
  ts: (/* @__PURE__ */ new Date()).toISOString(),
4240
4246
  direction: "out",
4241
4247
  kind: "reply-fallback",
4242
4248
  chat_id: String(chatId),
4243
- message_ids: payload.result?.message_id !== void 0 ? [payload.result.message_id] : [],
4244
- text: body
4249
+ message_ids: messageIds,
4250
+ text
4245
4251
  })}\n`);
4246
4252
  } catch {}
4247
4253
  return true;
@@ -4696,6 +4702,7 @@ async function handleInbound(event) {
4696
4702
  ...msgId !== void 0 ? { message_id: String(msgId) } : {},
4697
4703
  addressed: String(addressed),
4698
4704
  user: ctx.from?.username ?? String(ctx.from?.id),
4705
+ is_bot: String(ctx.from?.is_bot === true),
4699
4706
  user_id: String(ctx.from?.id),
4700
4707
  ts: (/* @__PURE__ */ new Date((ctx.date ?? 0) * 1e3)).toISOString(),
4701
4708
  ...imagePath !== void 0 ? { image_path: imagePath } : {},