blun-king-cli 9.1.335 → 9.1.337

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
  };
@@ -56,6 +56,7 @@ function isExplicitPauseRequest(text) {
56
56
  function createDirectFocusController(options = {}) {
57
57
  const setTimer = options.setTimer ?? setTimeout;
58
58
  const clearTimer = options.clearTimer ?? clearTimeout;
59
+ const now = options.now ?? Date.now;
59
60
  const checkpoint = options.checkpoint ?? (() => {});
60
61
  const resume = options.resume ?? (() => {});
61
62
  const silenceMs = options.silenceMs ?? DEFAULT_SILENCE_MS;
@@ -89,16 +90,23 @@ function createDirectFocusController(options = {}) {
89
90
  finishConversation(chatId, existing);
90
91
  return { resumeGranted: true };
91
92
  }
92
- if (conversations.size === 0) {
93
+ const startsConversation = conversations.size === 0;
94
+ if (startsConversation) {
93
95
  savedCheckpoint = { ...checkpointValue, chatId };
94
- checkpoint(savedCheckpoint);
95
96
  }
96
97
  const conversation = existing ?? { chatId };
97
98
  clearConversationTimer(conversation);
99
+ const wasExplicitPause = conversation.explicitPause === true;
98
100
  conversation.explicitPause = conversation.explicitPause === true || isExplicitPauseRequest(text);
99
101
  conversation.waitingPermission = conversation.explicitPause;
100
102
  conversation.closeAfterReply = isConversationClose(text);
101
103
  conversations.set(chatId, conversation);
104
+ savedCheckpoint = {
105
+ ...savedCheckpoint,
106
+ explicitPause: conversation.explicitPause,
107
+ resumeAfterAt: null,
108
+ };
109
+ if (startsConversation || wasExplicitPause !== conversation.explicitPause) checkpoint(savedCheckpoint);
102
110
  return { resumeGranted: false, explicitPause: conversation.explicitPause };
103
111
  }
104
112
 
@@ -109,10 +117,14 @@ function createDirectFocusController(options = {}) {
109
117
  clearConversationTimer(conversation);
110
118
  if (conversation.explicitPause === true) {
111
119
  conversation.waitingPermission = true;
120
+ checkpoint({ ...savedCheckpoint, explicitPause: true, resumeAfterAt: null });
112
121
  return true;
113
122
  }
114
123
  const delay = conversation.closeAfterReply ? 0 : silenceMs;
115
124
  conversation.closeAfterReply = false;
125
+ const resumeAfterAt = new Date(now() + delay).toISOString();
126
+ savedCheckpoint = { ...savedCheckpoint, explicitPause: false, resumeAfterAt };
127
+ checkpoint(savedCheckpoint);
116
128
  conversation.timer = setTimer(() => {
117
129
  finishConversation(chatId, conversation);
118
130
  }, delay);
@@ -125,6 +137,26 @@ function createDirectFocusController(options = {}) {
125
137
  savedCheckpoint = undefined;
126
138
  }
127
139
 
140
+ const initialCheckpoint = options.initialCheckpoint;
141
+ if (initialCheckpoint?.status === 'paused' && isPrivateTelegramChat(initialCheckpoint.chatId)) {
142
+ const chatId = String(initialCheckpoint.chatId);
143
+ const explicitPause = initialCheckpoint.explicitPause === true;
144
+ const conversation = {
145
+ chatId,
146
+ explicitPause,
147
+ waitingPermission: explicitPause,
148
+ };
149
+ savedCheckpoint = { ...initialCheckpoint, chatId };
150
+ conversations.set(chatId, conversation);
151
+ const resumeAfterMs = Date.parse(String(initialCheckpoint.resumeAfterAt ?? ''));
152
+ if (!explicitPause && Number.isFinite(resumeAfterMs)) {
153
+ conversation.timer = setTimer(
154
+ () => finishConversation(chatId, conversation),
155
+ Math.max(0, resumeAfterMs - now()),
156
+ );
157
+ }
158
+ }
159
+
128
160
  return {
129
161
  dispose,
130
162
  isPaused: () => conversations.size > 0,
@@ -145,7 +177,13 @@ function writeDirectFocusCheckpoint(value, env = process.env) {
145
177
  const record = {
146
178
  version: 1,
147
179
  status: value?.status === 'resumed' ? 'resumed' : 'paused',
148
- pausedAt: new Date().toISOString(),
180
+ pausedAt: Number.isFinite(Date.parse(String(value?.pausedAt ?? '')))
181
+ ? new Date(value.pausedAt).toISOString()
182
+ : new Date().toISOString(),
183
+ explicitPause: value?.explicitPause === true,
184
+ resumeAfterAt: Number.isFinite(Date.parse(String(value?.resumeAfterAt ?? '')))
185
+ ? new Date(value.resumeAfterAt).toISOString()
186
+ : null,
149
187
  chatId: isPrivateTelegramChat(value?.chatId) ? String(value.chatId) : null,
150
188
  sessionId: value?.sessionId === undefined ? null : String(value.sessionId).slice(0, 160),
151
189
  turnId: value?.turnId === undefined ? null : String(value.turnId),
@@ -160,6 +198,18 @@ function writeDirectFocusCheckpoint(value, env = process.env) {
160
198
  return { path: target, record };
161
199
  }
162
200
 
201
+ function readDirectFocusCheckpoint(env = process.env) {
202
+ const target = directFocusCheckpointPath(env);
203
+ try {
204
+ const record = JSON.parse(fs.readFileSync(target, 'utf8'));
205
+ if (record?.version !== 1 || (record.status !== 'paused' && record.status !== 'resumed')) return undefined;
206
+ return record;
207
+ } catch (error) {
208
+ if (error?.code === 'ENOENT') return undefined;
209
+ throw error;
210
+ }
211
+ }
212
+
163
213
  module.exports = {
164
214
  DEFAULT_SILENCE_MS,
165
215
  createDirectFocusController,
@@ -169,6 +219,7 @@ module.exports = {
169
219
  isExplicitPauseRequest,
170
220
  isPrivateTelegramChat,
171
221
  isResumeApproval,
222
+ readDirectFocusCheckpoint,
172
223
  rewriteTelegramDirectEnvelope,
173
224
  telegramDirectMessage,
174
225
  writeDirectFocusCheckpoint,
package/blun.mjs CHANGED
@@ -419036,7 +419036,7 @@ registerUiCatalogFragment({
419036
419036
  */
419037
419037
  var { projectUnaddressedTelegramContext } = createRequire(import.meta.url)("./bin/telegram-context-projection-policy.cjs");
419038
419038
  var { enqueueTelegramUrgent, rewriteTelegramUrgentEnvelope, telegramUrgentMessage } = createRequire(import.meta.url)("./bin/telegram-urgent-policy.cjs");
419039
- var { createDirectFocusController, enqueueTelegramDirect, rewriteTelegramDirectEnvelope, telegramDirectMessage, writeDirectFocusCheckpoint } = createRequire(import.meta.url)("./bin/telegram-direct-focus-policy.cjs");
419039
+ var { createDirectFocusController, enqueueTelegramDirect, readDirectFocusCheckpoint, rewriteTelegramDirectEnvelope, telegramDirectMessage, writeDirectFocusCheckpoint } = createRequire(import.meta.url)("./bin/telegram-direct-focus-policy.cjs");
419040
419040
  var { removeQueuedReloadCommands, removeQueuedSlashCommands } = createRequire(import.meta.url)("./bin/reload-queue-policy.cjs");
419041
419041
  const MAX_BUFFERED_CONTEXT_MESSAGES = 20;
419042
419042
  const MAX_BUFFERED_CONTEXT_CHARS = 24e3;
@@ -509485,6 +509485,9 @@ var SessionReplayRenderer = class {
509485
509485
  cleanupRuntime(context) {
509486
509486
  this.flushAssistant(context);
509487
509487
  this.host.streamingUI.cleanupAfterReplay(context.completedToolCallIds);
509488
+ this.host.setAppState({ streamingPhase: "idle" });
509489
+ this.host.resetLivePane();
509490
+ this.host.requestQueueDrain();
509488
509491
  }
509489
509492
  renderSkillActivation(context, skill) {
509490
509493
  const { sessionEventHandler } = this.host;
@@ -516082,6 +516085,7 @@ var BlunTUI = class {
516082
516085
  deliverOne: () => this.deliverQueuedChannelHead()
516083
516086
  });
516084
516087
  this.directFocusController = createDirectFocusController({
516088
+ initialCheckpoint: readDirectFocusCheckpoint(),
516085
516089
  checkpoint: (checkpoint) => {
516086
516090
  try {
516087
516091
  writeDirectFocusCheckpoint(checkpoint);
@@ -516929,7 +516933,7 @@ var BlunTUI = class {
516929
516933
  return this.restoreQueuedSteer(inFlight, error);
516930
516934
  };
516931
516935
  try {
516932
- if (!(await this.harness.withInteractiveAgent(item.agentId ?? "main", () => session.steerActive(input, expectedTurnId === void 0 ? {} : { expectedTurnId }))).accepted) return restoreHead();
516936
+ if (!(await this.harness.withInteractiveAgent(item.agentId ?? "main", () => session.steerActive(input, expectedTurnId === void 0 ? {} : { expectedTurnId }))).accepted) return this.recoverRejectedActiveSteer(inFlight);
516933
516937
  inFlight.accepted = true;
516934
516938
  if (inFlight.turnEnded) return restoreHead();
516935
516939
  this.commitQueuedSteerIfReady(inFlight);
@@ -516981,6 +516985,15 @@ var BlunTUI = class {
516981
516985
  this.steerQueueFlushPrefix();
516982
516986
  this.scheduleQueueDrain();
516983
516987
  }
516988
+ recoverRejectedActiveSteer(inFlight) {
516989
+ const restored = this.restoreQueuedSteer(inFlight);
516990
+ if (!this.streamingUI.hasActiveTurn()) {
516991
+ this.setAppState({ streamingPhase: "idle" });
516992
+ this.resetLivePane();
516993
+ this.scheduleQueueDrain();
516994
+ }
516995
+ return restored;
516996
+ }
516984
516997
  restoreQueuedSteer(inFlight, error) {
516985
516998
  if (this.queueSteerInFlight !== inFlight) return false;
516986
516999
  this.queueSteerInFlight = void 0;
@@ -517071,7 +517084,7 @@ var BlunTUI = class {
517071
517084
  const expectedTurnId = turnId !== void 0 && /^\d+$/.test(turnId) ? Number(turnId) : void 0;
517072
517085
  session.steerActive(items.map((item) => item.text.trim()).join("\n\n"), expectedTurnId === void 0 ? {} : { expectedTurnId }).then((result) => {
517073
517086
  if (this.queueSteerInFlight !== inFlight) return;
517074
- if (!result.accepted) this.restoreQueuedSteer(inFlight);
517087
+ if (!result.accepted) this.recoverRejectedActiveSteer(inFlight);
517075
517088
  else {
517076
517089
  inFlight.accepted = true;
517077
517090
  if (inFlight.turnEnded) this.restoreQueuedSteer(inFlight);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.335",
3
+ "version": "9.1.337",
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": {