blun-king-cli 9.1.336 → 9.1.338

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.
@@ -3,7 +3,7 @@
3
3
  const fs = require('node:fs');
4
4
  const os = require('node:os');
5
5
  const path = require('node:path');
6
- const { readProfilePersona, resolveSoulFile } = require('./profile-identity-resolution.cjs');
6
+ const { readProfilePersona, resolveLegacySoulFile, resolveSoulFile } = require('./profile-identity-resolution.cjs');
7
7
 
8
8
  const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
9
9
  const MAX_PERSONA_BYTES = 64 * 1024;
@@ -83,6 +83,43 @@ function createTextOnce(root, target, value) {
83
83
  }
84
84
  }
85
85
 
86
+ function generatedSoulText(displayName) {
87
+ return `# ${displayName}\n\nIch bin ${displayName}, der persoenliche BLUN-Agent dieses Profils. Meine eigene Stimme, Vorlieben und gewachsene gemeinsame Geschichte werden hier behutsam weiterentwickelt. Bestaetigte Beziehungen gehoeren in den Beziehungsgraphen; Auftraege, Rechte, Secrets und Incident-Logs gehoeren nicht in meine Seele.\n`;
88
+ }
89
+
90
+ function importLegacySoulOverGeneratedProfile(env, profileHome, profileSoulPath, displayName) {
91
+ let current;
92
+ try {
93
+ const stat = fs.lstatSync(profileSoulPath);
94
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_PERSONA_BYTES) return false;
95
+ current = fs.readFileSync(profileSoulPath, 'utf8');
96
+ } catch {
97
+ return false;
98
+ }
99
+ if (current.trim() !== generatedSoulText(displayName).trim()) return false;
100
+ const legacy = resolveLegacySoulFile(env);
101
+ if (!legacy.text || path.resolve(legacy.path) === path.resolve(profileSoulPath)) return false;
102
+
103
+ const backupPath = `${profileSoulPath}.pre-legacy-import`;
104
+ createTextOnce(profileHome, backupPath, current);
105
+ if (!fs.existsSync(backupPath) || fs.readFileSync(backupPath, 'utf8') !== current) return false;
106
+
107
+ const temporaryPath = path.join(profileHome, `.SOUL.md.import-${process.pid}-${Date.now()}`);
108
+ let handle;
109
+ try {
110
+ handle = fs.openSync(temporaryPath, 'wx', 0o600);
111
+ fs.writeFileSync(handle, `${legacy.text}\n`, 'utf8');
112
+ fs.fsyncSync(handle);
113
+ fs.closeSync(handle);
114
+ handle = undefined;
115
+ fs.renameSync(temporaryPath, profileSoulPath);
116
+ return true;
117
+ } finally {
118
+ if (handle !== undefined) fs.closeSync(handle);
119
+ try { fs.unlinkSync(temporaryPath); } catch {}
120
+ }
121
+ }
122
+
86
123
  function ensurePersonalityWorkspace(env = process.env) {
87
124
  const home = resolvedHome(env);
88
125
  const profileHome = path.resolve(String(env.BLUN_HOME ?? '').trim() || home);
@@ -134,15 +171,13 @@ function ensurePersonalityWorkspace(env = process.env) {
134
171
  );
135
172
  const resolvedSoul = resolveSoulFile(env);
136
173
  const profileSoulPath = path.join(profileHome, 'SOUL.md');
174
+ let soulMigrated = false;
137
175
  if (resolvedSoul.text && path.resolve(resolvedSoul.path) !== path.resolve(profileSoulPath)) {
138
176
  createTextOnce(profileHome, profileSoulPath, `${resolvedSoul.text}\n`);
139
177
  } else if (!resolvedSoul.text) {
140
- createTextOnce(
141
- profileHome,
142
- profileSoulPath,
143
- `# ${displayName}\n\nIch bin ${displayName}, der persoenliche BLUN-Agent dieses Profils. Meine eigene Stimme, Vorlieben und gewachsene gemeinsame Geschichte werden hier behutsam weiterentwickelt. Bestaetigte Beziehungen gehoeren in den Beziehungsgraphen; Auftraege, Rechte, Secrets und Incident-Logs gehoeren nicht in meine Seele.\n`,
144
- );
178
+ createTextOnce(profileHome, profileSoulPath, generatedSoulText(displayName));
145
179
  }
180
+ soulMigrated = importLegacySoulOverGeneratedProfile(env, profileHome, profileSoulPath, displayName);
146
181
  for (const relative of [
147
182
  ['agents', agentId, 'relationships'],
148
183
  ['agents', agentId, 'journal', 'candidates'],
@@ -156,7 +191,7 @@ function ensurePersonalityWorkspace(env = process.env) {
156
191
  fs.mkdirSync(target, { recursive: true });
157
192
  if (!isInside(root, fs.realpathSync(target))) return { ready: false, reason: 'identity_path_invalid' };
158
193
  }
159
- return { ready: true, root, tenant_id: tenantId, agent_id: agentId };
194
+ return { ready: true, root, tenant_id: tenantId, agent_id: agentId, soul_migrated: soulMigrated };
160
195
  }
161
196
 
162
197
  module.exports = { ensurePersonalityWorkspace };
@@ -77,6 +77,30 @@ function legacySoulNames(env, persona) {
77
77
  return names;
78
78
  }
79
79
 
80
+ function legacySoulCandidates(env = process.env, options = {}) {
81
+ const homeDir = options.homeDir || os.homedir();
82
+ const profileHome = String(env.BLUN_HOME || '').trim();
83
+ const sharedHome = String(env.BLUN_SHARED_HOME || '').trim();
84
+ const persona = options.persona || readProfilePersona(env, options).persona;
85
+ const roots = uniquePaths([
86
+ sharedHome,
87
+ homeDir,
88
+ profileHome && path.dirname(profileHome),
89
+ ]);
90
+ return legacySoulNames(env, persona)
91
+ .flatMap((filename) => roots.map((root) => path.join(root, filename)));
92
+ }
93
+
94
+ function resolveLegacySoulFile(env = process.env, options = {}) {
95
+ const fsImpl = options.fsImpl || fs;
96
+ const candidates = uniquePaths(legacySoulCandidates(env, options));
97
+ for (const target of candidates) {
98
+ const text = readSafeText(target, fsImpl);
99
+ if (text) return { path: target, text };
100
+ }
101
+ return { path: candidates[0], text: '' };
102
+ }
103
+
80
104
  function resolveSoulFile(env = process.env, options = {}) {
81
105
  const fsImpl = options.fsImpl || fs;
82
106
  const explicit = String(env.BLUN_SOUL_PATH || '').trim();
@@ -90,13 +114,7 @@ function resolveSoulFile(env = process.env, options = {}) {
90
114
  const profileName = String(env.BLUN_PROFILE || '').trim().toLowerCase();
91
115
  const isNamedProfile = profileName && profileName !== 'default';
92
116
  const persona = readProfilePersona(env, options).persona;
93
- const legacyRoots = uniquePaths([
94
- sharedHome,
95
- homeDir,
96
- profileHome && path.dirname(profileHome),
97
- ]);
98
- const legacyCandidates = legacySoulNames(env, persona)
99
- .flatMap((filename) => legacyRoots.map((root) => path.join(root, filename)));
117
+ const legacyCandidates = legacySoulCandidates(env, { ...options, persona });
100
118
  const candidates = uniquePaths([
101
119
  profileHome && path.join(profileHome, 'SOUL.md'),
102
120
  ...legacyCandidates,
@@ -113,5 +131,6 @@ module.exports = {
113
131
  MAX_IDENTITY_FILE_BYTES,
114
132
  identityFileCandidates,
115
133
  readProfilePersona,
134
+ resolveLegacySoulFile,
116
135
  resolveSoulFile,
117
136
  };
@@ -1,6 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  const PRIVATE_CHAT_ID = /^[1-9]\d*$/u;
4
+ const WORK_PERMISSION_QUESTION = /^(?:kann|darf|soll) ich\b.{0,100}\b(?:arbeit(?:en)?|weiterarbeiten|weitermachen|fortfahren|weiterbauen|umbau|bau)\b/u;
5
+ const TRAILING_WORK_PERMISSION_QUESTION = /(?:^|\r?\n\s*\r?\n)(?:kann|darf|soll)\s+ich\b[^\r\n]{0,160}\b(?:arbeit(?:en)?|weiterarbeiten|weitermachen|fortfahren|weiterbauen|umbau|bau)\b[^\r\n]*$/iu;
6
+ const INTERNAL_CONTROL_MARKER = /\b(?:cron|loop|checkpoint|handoff|resume|session|sha|tool|werkzeug|hook|queue|lane|inbound|outbound|arbeitsstand|arbeitsauftrag|bau freigabe|freigabe dieser lane|turn|zug|status|private nachricht|telegram nachricht|reply|verlauf|basis|qa gate|wartezustand|f\d+|m\d+|w\d+)\b/gu;
7
+ const NO_USER_VALUE = /\b(?:keine neue aktion|keine neue information|keine neue nachricht|kein neuer auftrag|kein neuer zweck|kein neuer inbound bedarf|kein offener handlungsbedarf|nichts neues zu melden|nichts zu melden|nichts weiteres|bleibt pausiert|warte auf (?:dein|sein|ihr|papas) (?:zeichen|go|freigabe|antwort)|ich warte|ich sende nichts|wiederholen wäre spam|keine weitere nachricht|bereits geantwortet|bereits gesendet|kein werkzeugaufruf nötig|stand unverändert|zug läuft seit|kein bau ohne|qa gate steht noch aus|wartezustand)\b/u;
4
8
 
5
9
  function normalize(text) {
6
10
  return String(text ?? '')
@@ -11,16 +15,24 @@ function normalize(text) {
11
15
  .trim();
12
16
  }
13
17
 
18
+ function sanitizePrivateConversationReply(chatId, text) {
19
+ const value = String(text ?? '');
20
+ if (!PRIVATE_CHAT_ID.test(String(chatId ?? '').trim())) return value;
21
+ return value.replace(TRAILING_WORK_PERMISSION_QUESTION, '').trim();
22
+ }
23
+
14
24
  function isPrivateInternalStatusReply(chatId, text) {
15
25
  if (!PRIVATE_CHAT_ID.test(String(chatId ?? '').trim())) return false;
16
- const value = normalize(text);
26
+ const sanitized = sanitizePrivateConversationReply(chatId, text);
27
+ const value = normalize(sanitized.length === 0 ? text : sanitized);
17
28
  if (value.length === 0) return false;
29
+ if (sanitized.length === 0 && WORK_PERMISSION_QUESTION.test(value)) return true;
18
30
 
19
- const exposesRuntimeControl = /\b(?:cron|loop|checkpoint|sha|tool|werkzeug|hook|queue|lane|inbound|outbound|arbeitsstand|arbeitsauftrag|bau freigabe|freigabe dieser lane|turn|zug|status|private nachricht|telegram nachricht|reply|verlauf|basis|f\d+|m\d+|w\d+)\b/u.test(value);
20
- const announcesNoUserValue = /\b(?:keine neue aktion|keine neue information|keine neue nachricht|kein neuer auftrag|kein neuer zweck|kein neuer inbound bedarf|kein offener handlungsbedarf|nichts zu melden|nichts weiteres|bleibt pausiert|warte auf (?:dein|sein|ihr|papas) (?:zeichen|go|freigabe|antwort)|ich warte|ich sende nichts|wiederholen wäre spam|keine weitere nachricht|bereits geantwortet|bereits gesendet|kein werkzeugaufruf nötig|stand unverändert|zug läuft seit)\b/u.test(value);
21
- return exposesRuntimeControl && announcesNoUserValue;
31
+ const internalMarkers = value.match(INTERNAL_CONTROL_MARKER) ?? [];
32
+ return internalMarkers.length > 0 && NO_USER_VALUE.test(value);
22
33
  }
23
34
 
24
35
  module.exports = {
25
36
  isPrivateInternalStatusReply,
37
+ sanitizePrivateConversationReply,
26
38
  };
package/blun.mjs CHANGED
@@ -514699,7 +514699,7 @@ function outboxDeliveredFile(marker, chatId, filePath) {
514699
514699
  }
514700
514700
  var retryTelegramMediaDelivery, retryableTelegramMediaFailure;
514701
514701
  ({ retryTelegramMediaDelivery, retryableTelegramMediaFailure } = createRequire(import.meta.url)("./bin/telegram-media-delivery-policy.cjs"));
514702
- var { isPrivateInternalStatusReply } = createRequire(import.meta.url)("./bin/telegram-private-conversation-policy.cjs");
514702
+ var { isPrivateInternalStatusReply, sanitizePrivateConversationReply } = createRequire(import.meta.url)("./bin/telegram-private-conversation-policy.cjs");
514703
514703
  const TELEGRAM_TEXT_LIMIT = 4096;
514704
514704
  const TELEGRAM_ATTACHMENT_LIMIT = 50 * 1024 * 1024;
514705
514705
  function mediaTelegramTarget(filePath) {
@@ -514788,6 +514788,7 @@ async function sendReplyFallback(chatId, text, contextOnly = false) {
514788
514788
  } catch {}
514789
514789
  return true;
514790
514790
  }
514791
+ const privateSafeText = sanitizePrivateConversationReply(chatId, text);
514791
514792
  if (isGroupChat(chatId) && isGroupSuppressed(text, contextOnly)) {
514792
514793
  try {
514793
514794
  appendFileSync(outboxPath(), `${JSON.stringify({
@@ -514802,7 +514803,7 @@ async function sendReplyFallback(chatId, text, contextOnly = false) {
514802
514803
  }
514803
514804
  const token = botToken();
514804
514805
  if (token === void 0) return false;
514805
- const body = text.length > TELEGRAM_TEXT_LIMIT ? `${text.slice(0, TELEGRAM_TEXT_LIMIT - 1)}…` : text;
514806
+ const body = privateSafeText.length > TELEGRAM_TEXT_LIMIT ? `${privateSafeText.slice(0, TELEGRAM_TEXT_LIMIT - 1)}…` : privateSafeText;
514806
514807
  try {
514807
514808
  const payload = await (await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
514808
514809
  method: "POST",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.336",
3
+ "version": "9.1.338",
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": {
@@ -1,6 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  const PRIVATE_CHAT_ID = /^[1-9]\d*$/u;
4
+ const WORK_PERMISSION_QUESTION = /^(?:kann|darf|soll) ich\b.{0,100}\b(?:arbeit(?:en)?|weiterarbeiten|weitermachen|fortfahren|weiterbauen|umbau|bau)\b/u;
5
+ const TRAILING_WORK_PERMISSION_QUESTION = /(?:^|\r?\n\s*\r?\n)(?:kann|darf|soll)\s+ich\b[^\r\n]{0,160}\b(?:arbeit(?:en)?|weiterarbeiten|weitermachen|fortfahren|weiterbauen|umbau|bau)\b[^\r\n]*$/iu;
6
+ const INTERNAL_CONTROL_MARKER = /\b(?:cron|loop|checkpoint|handoff|resume|session|sha|tool|werkzeug|hook|queue|lane|inbound|outbound|arbeitsstand|arbeitsauftrag|bau freigabe|freigabe dieser lane|turn|zug|status|private nachricht|telegram nachricht|reply|verlauf|basis|qa gate|wartezustand|f\d+|m\d+|w\d+)\b/gu;
7
+ const NO_USER_VALUE = /\b(?:keine neue aktion|keine neue information|keine neue nachricht|kein neuer auftrag|kein neuer zweck|kein neuer inbound bedarf|kein offener handlungsbedarf|nichts neues zu melden|nichts zu melden|nichts weiteres|bleibt pausiert|warte auf (?:dein|sein|ihr|papas) (?:zeichen|go|freigabe|antwort)|ich warte|ich sende nichts|wiederholen wäre spam|keine weitere nachricht|bereits geantwortet|bereits gesendet|kein werkzeugaufruf nötig|stand unverändert|zug läuft seit|kein bau ohne|qa gate steht noch aus|wartezustand)\b/u;
4
8
 
5
9
  function normalize(text) {
6
10
  return String(text ?? '')
@@ -11,16 +15,24 @@ function normalize(text) {
11
15
  .trim();
12
16
  }
13
17
 
18
+ function sanitizePrivateConversationReply(chatId, text) {
19
+ const value = String(text ?? '');
20
+ if (!PRIVATE_CHAT_ID.test(String(chatId ?? '').trim())) return value;
21
+ return value.replace(TRAILING_WORK_PERMISSION_QUESTION, '').trim();
22
+ }
23
+
14
24
  function isPrivateInternalStatusReply(chatId, text) {
15
25
  if (!PRIVATE_CHAT_ID.test(String(chatId ?? '').trim())) return false;
16
- const value = normalize(text);
26
+ const sanitized = sanitizePrivateConversationReply(chatId, text);
27
+ const value = normalize(sanitized.length === 0 ? text : sanitized);
17
28
  if (value.length === 0) return false;
29
+ if (sanitized.length === 0 && WORK_PERMISSION_QUESTION.test(value)) return true;
18
30
 
19
- const exposesRuntimeControl = /\b(?:cron|loop|checkpoint|sha|tool|werkzeug|hook|queue|lane|inbound|outbound|arbeitsstand|arbeitsauftrag|bau freigabe|freigabe dieser lane|turn|zug|status|private nachricht|telegram nachricht|reply|verlauf|basis|f\d+|m\d+|w\d+)\b/u.test(value);
20
- const announcesNoUserValue = /\b(?:keine neue aktion|keine neue information|keine neue nachricht|kein neuer auftrag|kein neuer zweck|kein neuer inbound bedarf|kein offener handlungsbedarf|nichts zu melden|nichts weiteres|bleibt pausiert|warte auf (?:dein|sein|ihr|papas) (?:zeichen|go|freigabe|antwort)|ich warte|ich sende nichts|wiederholen wäre spam|keine weitere nachricht|bereits geantwortet|bereits gesendet|kein werkzeugaufruf nötig|stand unverändert|zug läuft seit)\b/u.test(value);
21
- return exposesRuntimeControl && announcesNoUserValue;
31
+ const internalMarkers = value.match(INTERNAL_CONTROL_MARKER) ?? [];
32
+ return internalMarkers.length > 0 && NO_USER_VALUE.test(value);
22
33
  }
23
34
 
24
35
  module.exports = {
25
36
  isPrivateInternalStatusReply,
37
+ sanitizePrivateConversationReply,
26
38
  };
@@ -8,8 +8,10 @@ import { fileURLToPath } from "node:url";
8
8
  import { hostname } from "node:os";
9
9
  import remoteStatusPolicy from "../../bin/telegram-remote-status-policy.cjs";
10
10
  import telegramApprovalRelay from "../../bin/telegram-approval-relay.cjs";
11
+ import privateConversationPolicy from "../bin/telegram-private-conversation-policy.cjs";
11
12
  const { buildTelegramRemoteStatus, resolveTelegramRemoteVersion } = remoteStatusPolicy;
12
13
  const { buildTelegramApprovalCard, isAuthorizedTelegramApprovalCallback, listPendingTelegramApprovals, markTelegramApprovalSent, parseTelegramApprovalCallback, resolveTelegramApprovalTarget, wasTelegramApprovalSent, writeTelegramApprovalResponse } = telegramApprovalRelay;
14
+ const { isPrivateInternalStatusReply, sanitizePrivateConversationReply } = privateConversationPolicy;
13
15
  //#region ../../node_modules/pathe/dist/shared/pathe.M-eThtNZ.mjs
14
16
  const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
15
17
  function normalizeWindowsPath(input = "") {
@@ -4186,11 +4188,23 @@ function scrubSecrets(text) {
4186
4188
  async function sendReplyFallback(chatId, rawText) {
4187
4189
  const token = botToken();
4188
4190
  if (token === void 0) return false;
4191
+ if (isPrivateInternalStatusReply(chatId, rawText)) {
4192
+ appendFileSync(outboxLog(), `${JSON.stringify({
4193
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
4194
+ direction: "out",
4195
+ kind: "reply-fallback-suppressed-private-internal",
4196
+ chat_id: String(chatId),
4197
+ text: rawText
4198
+ })}\n`);
4199
+ logLine(`reply-fallback suppressed (private internal): ${rawText.slice(0, 60)}`);
4200
+ return true;
4201
+ }
4202
+ const privateSafeText = sanitizePrivateConversationReply(chatId, rawText);
4189
4203
  if (isGroupChat(chatId) && isGroupNoiseReply(rawText)) {
4190
4204
  logLine(`reply-fallback suppressed (group meta-noise): ${rawText.slice(0, 60)}`);
4191
4205
  return true;
4192
4206
  }
4193
- const text = scrubSecrets(rawText);
4207
+ const text = scrubSecrets(privateSafeText);
4194
4208
  const body = text.length > TELEGRAM_TEXT_LIMIT ? `${text.slice(0, TELEGRAM_TEXT_LIMIT - 1)}…` : text;
4195
4209
  try {
4196
4210
  const payload = await (await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
@@ -9667,7 +9667,7 @@ var StdioServerTransport = class {
9667
9667
  * chats the inbound gate would deliver from.
9668
9668
  */
9669
9669
  var import_out = require_out();
9670
- const { isPrivateInternalStatusReply } = privateConversationPolicy;
9670
+ const { isPrivateInternalStatusReply, sanitizePrivateConversationReply } = privateConversationPolicy;
9671
9671
  const TOOL_DEFINITIONS = [
9672
9672
  {
9673
9673
  name: "reply",
@@ -9841,11 +9841,12 @@ async function sendTelegramTextChunk(api, chatId, text, options, parseMode) {
9841
9841
  }
9842
9842
  async function runReply(api, args) {
9843
9843
  const chatId = resolveAllowedChatId(args.chat_id);
9844
- const text = args.text;
9844
+ const rawText = args.text;
9845
9845
  const replyTo = args.reply_to != null ? Number(args.reply_to) : void 0;
9846
9846
  const files = args.files ?? [];
9847
+ const text = files.length === 0 ? sanitizePrivateConversationReply(chatId, rawText) : rawText;
9847
9848
  const parseMode = args.format === "markdownv2" ? "MarkdownV2" : void 0;
9848
- if (files.length === 0 && isPrivateInternalStatusReply(chatId, text)) {
9849
+ if (files.length === 0 && isPrivateInternalStatusReply(chatId, rawText)) {
9849
9850
  appendJsonl(outboxLog(), {
9850
9851
  direction: "out",
9851
9852
  kind: "reply-suppressed-private-internal",