blun-king-cli 9.1.257 → 9.1.259

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.
@@ -1,6 +1,7 @@
1
1
  const fs = require('node:fs');
2
2
  const os = require('node:os');
3
3
  const path = require('node:path');
4
+ const { recordIdentityJournalCandidate } = require('./identity-journal-policy.cjs');
4
5
 
5
6
  const MAX_FILE_BYTES = 64 * 1024;
6
7
  const DEFAULT_MAX_CHARS = 1800;
@@ -206,7 +207,22 @@ function recordChannelIdentity(envelope, env = process.env) {
206
207
  purpose: '',
207
208
  relevant_roles: [],
208
209
  })) created.push('group');
209
- return { actor_id: actorId, ...(groupId ? { group_id: groupId } : {}), created };
210
+ const journal = created.includes('relationship') && firstSeenAt
211
+ ? recordIdentityJournalCandidate({
212
+ kind: 'first_interaction',
213
+ tenant_id: tenantId,
214
+ agent_id: agentId,
215
+ actor_id: actorId,
216
+ ...(groupId ? { group_id: groupId } : {}),
217
+ occurred_at: firstSeenAt,
218
+ }, env)
219
+ : undefined;
220
+ return {
221
+ actor_id: actorId,
222
+ ...(groupId ? { group_id: groupId } : {}),
223
+ created,
224
+ ...(journal ? { journal } : {}),
225
+ };
210
226
  }
211
227
 
212
228
  function identitySystemBlock(env = process.env) {
@@ -0,0 +1,104 @@
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 SAFE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
9
+ const ALLOWED_KINDS = new Set(['first_interaction']);
10
+
11
+ function enabled(env) {
12
+ return /^(?:1|true)$/iu.test(String(env.BLUN_IDENTITY_JOURNAL ?? '').trim());
13
+ }
14
+
15
+ function safeId(value) {
16
+ const normalized = String(value ?? '').trim();
17
+ return SAFE_ID_RE.test(normalized) ? normalized : '';
18
+ }
19
+
20
+ function isInside(parent, child) {
21
+ const relative = path.relative(parent, child);
22
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
23
+ }
24
+
25
+ function resolvedRoot(env) {
26
+ const configuredHome = String(env.BLUN_SHARED_HOME ?? env.BLUN_HOME ?? '').trim();
27
+ const home = path.resolve(configuredHome || path.join(os.homedir(), '.blun'));
28
+ const configuredRoot = String(env.BLUN_IDENTITY_ROOT ?? '').trim();
29
+ const root = path.resolve(configuredRoot || path.join(home, 'identity'));
30
+ if (!isInside(home, root)) return '';
31
+ try {
32
+ if (fs.lstatSync(root).isSymbolicLink()) return '';
33
+ const realHome = fs.realpathSync(home);
34
+ const realRoot = fs.realpathSync(root);
35
+ return isInside(realHome, realRoot) ? realRoot : '';
36
+ } catch {
37
+ return '';
38
+ }
39
+ }
40
+
41
+ function trustedTimestamp(value) {
42
+ const text = String(value ?? '').trim();
43
+ if (!text || text.length > 64 || Number.isNaN(Date.parse(text))) return '';
44
+ return text;
45
+ }
46
+
47
+ function createJsonOnce(root, target, value) {
48
+ if (!isInside(root, target)) return false;
49
+ let handle;
50
+ try {
51
+ const parent = path.dirname(target);
52
+ fs.mkdirSync(parent, { recursive: true });
53
+ if (!isInside(root, fs.realpathSync(parent))) return false;
54
+ handle = fs.openSync(target, 'wx', 0o600);
55
+ fs.writeFileSync(handle, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
56
+ fs.fsyncSync(handle);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ } finally {
61
+ if (handle !== undefined) fs.closeSync(handle);
62
+ }
63
+ }
64
+
65
+ function recordIdentityJournalCandidate(signal, env = process.env) {
66
+ if (!enabled(env) || !signal || typeof signal !== 'object') return undefined;
67
+ const kind = String(signal.kind ?? '').trim();
68
+ const tenantId = safeId(signal.tenant_id);
69
+ const configuredTenantId = safeId(env.BLUN_IDENTITY_TENANT_ID);
70
+ const agentId = safeId(signal.agent_id);
71
+ const configuredAgentId = safeId(env.BLUN_AGENT_ID);
72
+ const actorId = safeId(signal.actor_id);
73
+ const groupId = safeId(signal.group_id);
74
+ const occurredAt = trustedTimestamp(signal.occurred_at);
75
+ if (!ALLOWED_KINDS.has(kind)
76
+ || !tenantId || tenantId !== configuredTenantId
77
+ || !agentId || agentId !== configuredAgentId
78
+ || !actorId || !occurredAt
79
+ || (signal.group_id !== undefined && !groupId)) return undefined;
80
+
81
+ const root = resolvedRoot(env);
82
+ if (!root) return undefined;
83
+ const identity = [kind, tenantId, agentId, actorId, groupId, occurredAt].join('\0');
84
+ const candidateId = crypto.createHash('sha256').update(identity).digest('hex').slice(0, 32);
85
+ const target = path.resolve(root, 'agents', agentId, 'journal', 'candidates', `${candidateId}.json`);
86
+ const value = {
87
+ version: 1,
88
+ candidate_id: candidateId,
89
+ status: 'pending',
90
+ kind,
91
+ tenant_id: tenantId,
92
+ agent_id: agentId,
93
+ actor_id: actorId,
94
+ ...(groupId ? { group_id: groupId } : {}),
95
+ occurred_at: occurredAt,
96
+ source: 'trusted_channel_metadata',
97
+ };
98
+ return {
99
+ candidate_id: candidateId,
100
+ created: createJsonOnce(root, target, value),
101
+ };
102
+ }
103
+
104
+ module.exports = { recordIdentityJournalCandidate };
@@ -4,8 +4,15 @@ const NATURAL_PRESENCE_BLOCK = `## Natural presence
4
4
 
5
5
  Keep the loaded soul intact and let it shape the voice. Speak like an ongoing teammate, using only real shared context; never invent history or feelings. During active work, the current task and evidence come first. Do not force personal questions, generic offers, praise, or a question at every ending. Silence and a clean ending are valid.`;
6
6
 
7
- function naturalPresenceSystemBlock() {
8
- return NATURAL_PRESENCE_BLOCK;
7
+ const PERSONALITY_CURIOSITY_BLOCK = `When personality context is enabled and the conversation is relaxed rather than task-focused, show genuine curiosity: if it fits, ask one natural, optional personal question about their partner, family, work, home, interests, or what matters to them now. Never turn the conversation into an interview or stack questions. Do not repeat an unanswered question or infer an answer. During active work, do not interrupt the task; wait for a natural opening.`;
8
+
9
+ function personalityContextEnabled(env) {
10
+ return /^(?:1|true)$/iu.test(String(env.BLUN_IDENTITY_CONTEXT ?? '').trim());
11
+ }
12
+
13
+ function naturalPresenceSystemBlock(env = process.env) {
14
+ if (!personalityContextEnabled(env)) return NATURAL_PRESENCE_BLOCK;
15
+ return `${NATURAL_PRESENCE_BLOCK}\n\n${PERSONALITY_CURIOSITY_BLOCK}`;
9
16
  }
10
17
 
11
18
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blun-king-cli",
3
- "version": "9.1.257",
3
+ "version": "9.1.259",
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": {