blun-king-cli 9.1.327 → 9.1.329

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.
@@ -455,7 +455,7 @@ function recordChannelIdentity(envelope, env = process.env) {
455
455
  const manifestTenantId = safeId(manifest?.tenant_id);
456
456
  const tenantId = configuredTenantId || manifestTenantId;
457
457
  const configuredAgentId = safeId(env.BLUN_AGENT_ID);
458
- const agentId = configuredAgentId || safeId(manifest?.active_agent_id);
458
+ const agentId = configuredAgentId || safeId(setup.agent_id);
459
459
  if (!tenantId || tenantId !== manifestTenantId || !agentId || manifest?.version !== 1) return undefined;
460
460
 
461
461
  const actorId = `telegram-${subjectId}`;
@@ -575,7 +575,7 @@ function identitySystemBlock(env = process.env) {
575
575
 
576
576
  const configuredAgentId = safeId(env.BLUN_AGENT_ID);
577
577
  if (env.BLUN_AGENT_ID && !configuredAgentId) return '';
578
- const agentId = configuredAgentId || safeId(manifest.active_agent_id);
578
+ const agentId = configuredAgentId || safeId(setup.agent_id);
579
579
  if (!agentId) return '';
580
580
  const actorId = safeId(env.BLUN_IDENTITY_ACTOR_ID);
581
581
  const groupId = safeId(env.BLUN_IDENTITY_GROUP_ID);
@@ -6,6 +6,7 @@ const os = require('node:os');
6
6
  const path = require('node:path');
7
7
 
8
8
  const { ensurePersonalityWorkspace } = require('./personality-setup-policy.cjs');
9
+ const { readProfilePersona } = require('./profile-identity-resolution.cjs');
9
10
 
10
11
  const MAX_PERSONA_BYTES = 64 * 1024;
11
12
 
@@ -14,7 +15,7 @@ function fail(code, detail = '') {
14
15
  }
15
16
 
16
17
  function resolvedHome(env) {
17
- const configured = String(env.BLUN_SHARED_HOME ?? env.BLUN_HOME ?? '').trim();
18
+ const configured = String(env.BLUN_HOME ?? env.BLUN_SHARED_HOME ?? '').trim();
18
19
  return path.resolve(configured || path.join(os.homedir(), '.blun'));
19
20
  }
20
21
 
@@ -3,6 +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 { identityFileCandidates } = require('./profile-identity-resolution.cjs');
6
7
 
7
8
  const MAX_PERSONA_BYTES = 64 * 1024;
8
9
  const INVALID_PERSONALITY_CHOICE = Symbol('invalid-personality-choice');
@@ -13,19 +14,22 @@ function resolvedHome(env) {
13
14
  }
14
15
 
15
16
  function persistedPersonalityChoice(env) {
16
- const personaPath = path.join(resolvedHome(env), 'persona.json');
17
- try {
18
- const stat = fs.lstatSync(personaPath);
19
- if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_PERSONA_BYTES) return INVALID_PERSONALITY_CHOICE;
20
- const persona = JSON.parse(fs.readFileSync(personaPath, 'utf8'));
21
- if (!persona || typeof persona !== 'object' || Array.isArray(persona)) return INVALID_PERSONALITY_CHOICE;
22
- if (!Object.prototype.hasOwnProperty.call(persona, 'personalityEnabled')) return undefined;
23
- return typeof persona.personalityEnabled === 'boolean'
24
- ? persona.personalityEnabled
25
- : INVALID_PERSONALITY_CHOICE;
26
- } catch (error) {
27
- return error?.code === 'ENOENT' ? undefined : INVALID_PERSONALITY_CHOICE;
17
+ const candidates = identityFileCandidates('persona.json', env);
18
+ for (const personaPath of candidates) {
19
+ try {
20
+ const stat = fs.lstatSync(personaPath);
21
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_PERSONA_BYTES) return INVALID_PERSONALITY_CHOICE;
22
+ const persona = JSON.parse(fs.readFileSync(personaPath, 'utf8'));
23
+ if (!persona || typeof persona !== 'object' || Array.isArray(persona)) return INVALID_PERSONALITY_CHOICE;
24
+ if (!Object.prototype.hasOwnProperty.call(persona, 'personalityEnabled')) return undefined;
25
+ return typeof persona.personalityEnabled === 'boolean'
26
+ ? persona.personalityEnabled
27
+ : INVALID_PERSONALITY_CHOICE;
28
+ } catch (error) {
29
+ if (error?.code !== 'ENOENT') return INVALID_PERSONALITY_CHOICE;
30
+ }
28
31
  }
32
+ return undefined;
29
33
  }
30
34
 
31
35
  function personalityContextEnabled(env = process.env) {
@@ -3,6 +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
7
 
7
8
  const SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
8
9
  const MAX_PERSONA_BYTES = 64 * 1024;
@@ -33,18 +34,6 @@ function isInside(parent, child) {
33
34
  return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
34
35
  }
35
36
 
36
- function readPersona(home) {
37
- const target = path.join(home, 'persona.json');
38
- try {
39
- const stat = fs.lstatSync(target);
40
- if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_PERSONA_BYTES) return {};
41
- const value = JSON.parse(fs.readFileSync(target, 'utf8'));
42
- return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
43
- } catch {
44
- return {};
45
- }
46
- }
47
-
48
37
  function readJsonFile(target) {
49
38
  try {
50
39
  const stat = fs.lstatSync(target);
@@ -96,7 +85,9 @@ function createTextOnce(root, target, value) {
96
85
 
97
86
  function ensurePersonalityWorkspace(env = process.env) {
98
87
  const home = resolvedHome(env);
88
+ const profileHome = path.resolve(String(env.BLUN_HOME ?? '').trim() || home);
99
89
  fs.mkdirSync(home, { recursive: true });
90
+ fs.mkdirSync(profileHome, { recursive: true });
100
91
  const root = path.resolve(String(env.BLUN_IDENTITY_ROOT ?? '').trim() || path.join(home, 'identity'));
101
92
  if (!isInside(home, root)) return { ready: false, reason: 'identity_root_outside_home' };
102
93
  if (fs.existsSync(root) && fs.lstatSync(root).isSymbolicLink()) return { ready: false, reason: 'identity_root_symlink' };
@@ -105,20 +96,27 @@ function ensurePersonalityWorkspace(env = process.env) {
105
96
  const realRoot = fs.realpathSync(root);
106
97
  if (!isInside(realHome, realRoot)) return { ready: false, reason: 'identity_root_outside_home' };
107
98
 
108
- const persona = readPersona(home);
99
+ const personaResult = readProfilePersona(env);
100
+ const persona = personaResult.persona || {};
109
101
  const displayName = String(persona.name ?? env.BLUN_AGENT_ID ?? 'King').trim().slice(0, 120) || 'King';
110
- const requestedAgentId = safeId(env.BLUN_AGENT_ID) || agentIdFromName(displayName);
111
- const requestedTenantId = safeId(env.BLUN_IDENTITY_TENANT_ID) || 'local';
102
+ const configuredAgentId = safeId(env.BLUN_AGENT_ID);
103
+ const configuredTenantId = safeId(env.BLUN_IDENTITY_TENANT_ID);
104
+ const initialAgentId = configuredAgentId || (personaResult.persona ? agentIdFromName(displayName) : 'king');
105
+ const initialTenantId = configuredTenantId || 'local';
112
106
  const manifestPath = path.join(root, 'manifest.json');
113
107
  createJsonOnce(root, manifestPath, {
114
108
  version: 1,
115
- tenant_id: requestedTenantId,
116
- active_agent_id: requestedAgentId,
109
+ tenant_id: initialTenantId,
110
+ active_agent_id: initialAgentId,
117
111
  });
118
112
  const manifest = readJsonFile(manifestPath);
119
113
  const tenantId = safeId(manifest?.tenant_id);
120
- const agentId = safeId(manifest?.active_agent_id);
121
- if (manifest?.version !== 1 || !tenantId || !agentId) return { ready: false, reason: 'identity_manifest_invalid' };
114
+ if (manifest?.version !== 1 || !tenantId || (configuredTenantId && tenantId !== configuredTenantId)) {
115
+ return { ready: false, reason: 'identity_manifest_invalid' };
116
+ }
117
+ const agentId = configuredAgentId
118
+ || (personaResult.persona ? agentIdFromName(displayName) : safeId(manifest.active_agent_id))
119
+ || 'king';
122
120
 
123
121
  const profilePath = path.join(root, 'agents', agentId, 'profile.json');
124
122
  createJsonOnce(root, profilePath, {
@@ -134,6 +132,17 @@ function ensurePersonalityWorkspace(env = process.env) {
134
132
  path.join(root, 'agents', agentId, 'JOURNAL.md'),
135
133
  '# Journal\n\nLong-term development notes, shared milestones, and confirmed lessons belong here. Current tasks, secrets, permissions, and incident logs do not.\n',
136
134
  );
135
+ const resolvedSoul = resolveSoulFile(env);
136
+ const profileSoulPath = path.join(profileHome, 'SOUL.md');
137
+ if (resolvedSoul.text && path.resolve(resolvedSoul.path) !== path.resolve(profileSoulPath)) {
138
+ createTextOnce(profileHome, profileSoulPath, `${resolvedSoul.text}\n`);
139
+ } 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
+ );
145
+ }
137
146
  for (const relative of [
138
147
  ['agents', agentId, 'relationships'],
139
148
  ['agents', agentId, 'journal', 'candidates'],
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const MAX_IDENTITY_FILE_BYTES = 256 * 1024;
8
+
9
+ function uniquePaths(values) {
10
+ const seen = new Set();
11
+ const result = [];
12
+ for (const value of values) {
13
+ if (!value) continue;
14
+ const resolved = path.resolve(value);
15
+ const key = process.platform === 'win32' ? resolved.toLowerCase() : resolved;
16
+ if (seen.has(key)) continue;
17
+ seen.add(key);
18
+ result.push(resolved);
19
+ }
20
+ return result;
21
+ }
22
+
23
+ function identityFileCandidates(filename, env = process.env, homeDir = os.homedir()) {
24
+ const profileHome = String(env.BLUN_HOME || '').trim();
25
+ const sharedHome = String(env.BLUN_SHARED_HOME || '').trim();
26
+ const profileName = String(env.BLUN_PROFILE || '').trim().toLowerCase();
27
+ const isNamedProfile = profileName && profileName !== 'default';
28
+ return uniquePaths([
29
+ profileHome && path.join(profileHome, filename),
30
+ !isNamedProfile && sharedHome && path.join(sharedHome, filename),
31
+ !isNamedProfile && path.join(homeDir, '.blun', filename),
32
+ ]);
33
+ }
34
+
35
+ function readSafeText(target, fsImpl = fs) {
36
+ try {
37
+ const stat = fsImpl.lstatSync(target);
38
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size <= 0 || stat.size > MAX_IDENTITY_FILE_BYTES) return '';
39
+ return fsImpl.readFileSync(target, 'utf8').trim();
40
+ } catch {
41
+ return '';
42
+ }
43
+ }
44
+
45
+ function readProfilePersona(env = process.env, options = {}) {
46
+ const fsImpl = options.fsImpl || fs;
47
+ const candidates = identityFileCandidates('persona.json', env, options.homeDir || os.homedir());
48
+ for (const target of candidates) {
49
+ const raw = readSafeText(target, fsImpl);
50
+ if (!raw) continue;
51
+ try {
52
+ const parsed = JSON.parse(raw);
53
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
54
+ return { path: target, persona: parsed };
55
+ }
56
+ } catch {}
57
+ }
58
+ const profileName = String(env.BLUN_PROFILE || '').trim();
59
+ return {
60
+ path: candidates[0],
61
+ persona: profileName && profileName.toLowerCase() !== 'default' ? { name: profileName } : undefined,
62
+ };
63
+ }
64
+
65
+ function legacySoulNames(env, persona) {
66
+ const names = [];
67
+ for (const value of [env.BLUN_AGENT_ID, persona?.name, env.BLUN_PROFILE]) {
68
+ const slug = String(value || '')
69
+ .normalize('NFKD')
70
+ .replace(/[\u0300-\u036f]/gu, '')
71
+ .toLowerCase()
72
+ .replace(/[^a-z0-9._-]+/gu, '-')
73
+ .replace(/^[^a-z0-9]+|[^a-z0-9]+$/gu, '')
74
+ .slice(0, 64);
75
+ if (slug && slug !== 'default' && !names.includes(`_${slug}_soul.md`)) names.push(`_${slug}_soul.md`);
76
+ }
77
+ return names;
78
+ }
79
+
80
+ function resolveSoulFile(env = process.env, options = {}) {
81
+ const fsImpl = options.fsImpl || fs;
82
+ const explicit = String(env.BLUN_SOUL_PATH || '').trim();
83
+ if (explicit) {
84
+ const target = path.resolve(explicit);
85
+ return { path: target, text: readSafeText(target, fsImpl), explicit: true };
86
+ }
87
+ const homeDir = options.homeDir || os.homedir();
88
+ const profileHome = String(env.BLUN_HOME || '').trim();
89
+ const sharedHome = String(env.BLUN_SHARED_HOME || '').trim();
90
+ const profileName = String(env.BLUN_PROFILE || '').trim().toLowerCase();
91
+ const isNamedProfile = profileName && profileName !== 'default';
92
+ 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)));
100
+ const candidates = uniquePaths([
101
+ profileHome && path.join(profileHome, 'SOUL.md'),
102
+ ...legacyCandidates,
103
+ ...(!isNamedProfile ? identityFileCandidates('SOUL.md', env, homeDir).slice(1) : []),
104
+ ]);
105
+ for (const target of candidates) {
106
+ const text = readSafeText(target, fsImpl);
107
+ if (text) return { path: target, text, explicit: false };
108
+ }
109
+ return { path: candidates[0], text: '', explicit: false };
110
+ }
111
+
112
+ module.exports = {
113
+ MAX_IDENTITY_FILE_BYTES,
114
+ identityFileCandidates,
115
+ readProfilePersona,
116
+ resolveSoulFile,
117
+ };
@@ -101,7 +101,10 @@ function recordExplicitRelationshipCandidate(input = {}) {
101
101
  if (!fs.lstatSync(root).isDirectory() || fs.lstatSync(root).isSymbolicLink()) return undefined;
102
102
  const realRoot = fs.realpathSync(root);
103
103
  const manifest = readManifest(realRoot);
104
- if (manifest?.version !== 1 || manifest.tenant_id !== tenantId || manifest.active_agent_id !== agentId) return undefined;
104
+ const agentRoot = path.join(realRoot, 'agents', agentId);
105
+ if (manifest?.version !== 1 || manifest.tenant_id !== tenantId) return undefined;
106
+ const agentStat = fs.lstatSync(agentRoot);
107
+ if (!agentStat.isDirectory() || agentStat.isSymbolicLink()) return undefined;
105
108
  // The timestamp keeps later confirmations or contradictions as separate history,
106
109
  // while an identical channel event remains idempotent on retry.
107
110
  const identity = [tenantId, agentId, actorId, occurredAt, explicit.kind, explicit.value].join('\0');
package/blun.mjs CHANGED
@@ -21445,6 +21445,7 @@ var { ensurePersonalityWorkspace } = createRequire(import.meta.url)("./bin/perso
21445
21445
  var { rollbackPersonalityChoice, setPersonalityEnabledAtomic } = createRequire(import.meta.url)("./bin/personality-choice-policy.cjs");
21446
21446
  var { projectSoulText } = createRequire(import.meta.url)("./bin/soul-organization-policy.cjs");
21447
21447
  var { soulFileInstruction } = createRequire(import.meta.url)("./bin/soul-preservation-policy.cjs");
21448
+ var { readProfilePersona, resolveSoulFile } = createRequire(import.meta.url)("./bin/profile-identity-resolution.cjs");
21448
21449
  async function prepareSystemPromptContext(kaos, brandHome, options) {
21449
21450
  const additionalDirs = normalizeAdditionalDirs(options?.additionalDirs ?? []);
21450
21451
  const [cwdListing, agentsMdResult, additionalDirsInfo] = await Promise.all([
@@ -28208,15 +28209,12 @@ function resolveBlunHome(env = process.env) {
28208
28209
  return override && override.length > 0 ? override : join$4(homedir(), ".blun");
28209
28210
  }
28210
28211
  function personaFilePath$1(env = process.env) {
28211
- return join$4(resolveBlunHome(env), "persona.json");
28212
+ return readProfilePersona(env).path;
28212
28213
  }
28213
28214
  /** Read the persona file. Returns undefined when unset or unreadable. */
28214
28215
  function readPersona$1(env = process.env) {
28215
- try {
28216
- const raw = readFileSync(personaFilePath$1(env), "utf8");
28217
- const parsed = JSON.parse(raw);
28218
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
28219
- } catch {}
28216
+ const resolved = readProfilePersona(env);
28217
+ return resolved.persona;
28220
28218
  }
28221
28219
  /**
28222
28220
  * Build the system-prompt block that teaches the agent its persona name, so it
@@ -28264,9 +28262,7 @@ var init_persona = __esmMin((() => {
28264
28262
  * agent is explicitly invited to keep writing it as it grows.
28265
28263
  */
28266
28264
  function soulFilePath(env = process.env) {
28267
- const override = env["BLUN_SOUL_PATH"]?.trim();
28268
- if (override) return resolve$2(override);
28269
- return join$4(resolveBlunHome(env), "SOUL.md");
28265
+ return resolveSoulFile(env).path;
28270
28266
  }
28271
28267
  /**
28272
28268
  * Build the system-prompt block that gives the agent its soul file: the
@@ -28274,16 +28270,9 @@ function soulFilePath(env = process.env) {
28274
28270
  * returns an empty string when no SOUL.md exists yet (fail-open).
28275
28271
  */
28276
28272
  function soulSystemBlock(env = process.env) {
28277
- const path = soulFilePath(env);
28278
- let soul = "";
28279
- try {
28280
- if (existsSync(path)) {
28281
- soul = readFileSync(path, "utf8").trim();
28282
- soul = projectSoulText(soul, SOUL_MAX_CHARS);
28283
- }
28284
- } catch {
28285
- soul = "";
28286
- }
28273
+ const resolved = resolveSoulFile(env);
28274
+ const path = resolved.path;
28275
+ const soul = projectSoulText(resolved.text, SOUL_MAX_CHARS);
28287
28276
  if (soul.length === 0) return "";
28288
28277
  const lines = [];
28289
28278
  lines.push("## Your soul — who you are", "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.327",
3
+ "version": "9.1.329",
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": {