soulbridge 0.0.2 → 0.0.4
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/README.md +23 -3
- package/dist/packages/cli/src/cli.js +136 -36
- package/dist/packages/cli/src/llm-analyzer.js +82 -0
- package/dist/packages/core/src/affect/analyze.js +210 -0
- package/dist/packages/core/src/affect/directives.js +29 -0
- package/dist/packages/core/src/affect/index.js +4 -0
- package/dist/packages/core/src/affect/lexicon.js +62 -0
- package/dist/packages/core/src/affect/port.js +1 -0
- package/dist/packages/core/src/affect/rules.js +12 -0
- package/dist/packages/core/src/affect/seed-lexicon.js +123 -0
- package/dist/packages/core/src/context.js +24 -1
- package/dist/packages/core/src/index.js +55 -7
- package/dist/packages/core/src/sqlite.js +176 -37
- package/dist/packages/core/src/storage/config.js +83 -0
- package/dist/packages/core/src/storage/merge.js +21 -0
- package/dist/packages/core/src/style/measure.js +28 -0
- package/dist/packages/core/src/types.js +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity and location (ADR 0020).
|
|
3
|
+
*
|
|
4
|
+
* Cross-tool only works if every host resolves the same person: the database
|
|
5
|
+
* lives in the user's home (not the cwd), and the soul/user/device identity
|
|
6
|
+
* lives in a config file that all hosts read.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { randomBytes } from 'node:crypto';
|
|
12
|
+
export const DEFAULT_SOUL_ID = 'xiaoling';
|
|
13
|
+
export const DEFAULT_USER_ID = 'local';
|
|
14
|
+
export const DEFAULT_NAME = '小灵';
|
|
15
|
+
export function soulbridgeHome() {
|
|
16
|
+
return process.env.SOULBRIDGE_HOME ?? join(homedir(), '.soulbridge');
|
|
17
|
+
}
|
|
18
|
+
export function configPath() {
|
|
19
|
+
return join(soulbridgeHome(), 'config.json');
|
|
20
|
+
}
|
|
21
|
+
export function defaultDbPath() {
|
|
22
|
+
return join(soulbridgeHome(), 'soul.db');
|
|
23
|
+
}
|
|
24
|
+
export function newDeviceId() {
|
|
25
|
+
return `dev_${randomBytes(4).toString('hex')}`;
|
|
26
|
+
}
|
|
27
|
+
/** DB location: explicit flag > env > home. Never the cwd. */
|
|
28
|
+
export function resolveDbPath(explicit) {
|
|
29
|
+
return explicit ?? process.env.SOULBRIDGE_DB ?? defaultDbPath();
|
|
30
|
+
}
|
|
31
|
+
export function loadConfig() {
|
|
32
|
+
const file = configPath();
|
|
33
|
+
if (!existsSync(file))
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
37
|
+
if (!parsed.soulId || !parsed.userId)
|
|
38
|
+
return null;
|
|
39
|
+
return {
|
|
40
|
+
soulId: parsed.soulId,
|
|
41
|
+
userId: parsed.userId,
|
|
42
|
+
name: parsed.name ?? DEFAULT_NAME,
|
|
43
|
+
deviceId: parsed.deviceId ?? newDeviceId(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function saveConfig(config) {
|
|
51
|
+
const home = soulbridgeHome();
|
|
52
|
+
if (!existsSync(home))
|
|
53
|
+
mkdirSync(home, { recursive: true });
|
|
54
|
+
writeFileSync(configPath(), `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read the config, creating or completing it when needed.
|
|
58
|
+
* Env overrides win, but are not persisted (an override is per-process).
|
|
59
|
+
*/
|
|
60
|
+
export function ensureConfig(overrides = {}) {
|
|
61
|
+
const loaded = loadConfig();
|
|
62
|
+
const env = {};
|
|
63
|
+
if (process.env.SOULBRIDGE_SOUL)
|
|
64
|
+
env.soulId = process.env.SOULBRIDGE_SOUL;
|
|
65
|
+
if (process.env.SOULBRIDGE_USER)
|
|
66
|
+
env.userId = process.env.SOULBRIDGE_USER;
|
|
67
|
+
if (process.env.SOULBRIDGE_NAME)
|
|
68
|
+
env.name = process.env.SOULBRIDGE_NAME;
|
|
69
|
+
const merged = {
|
|
70
|
+
soulId: overrides.soulId ?? env.soulId ?? loaded?.soulId ?? DEFAULT_SOUL_ID,
|
|
71
|
+
userId: overrides.userId ?? env.userId ?? loaded?.userId ?? DEFAULT_USER_ID,
|
|
72
|
+
name: overrides.name ?? env.name ?? loaded?.name ?? DEFAULT_NAME,
|
|
73
|
+
deviceId: overrides.deviceId ?? loaded?.deviceId ?? newDeviceId(),
|
|
74
|
+
};
|
|
75
|
+
const changed = !loaded ||
|
|
76
|
+
loaded.soulId !== merged.soulId ||
|
|
77
|
+
loaded.userId !== merged.userId ||
|
|
78
|
+
loaded.name !== merged.name ||
|
|
79
|
+
loaded.deviceId !== merged.deviceId;
|
|
80
|
+
if (changed)
|
|
81
|
+
saveConfig(merged);
|
|
82
|
+
return merged;
|
|
83
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function pickLatest(a, b) {
|
|
2
|
+
if (a.updatedAt !== b.updatedAt)
|
|
3
|
+
return a.updatedAt > b.updatedAt ? a : b;
|
|
4
|
+
return a.device >= b.device ? a : b;
|
|
5
|
+
}
|
|
6
|
+
/** Union by id, keeping tombstones (deletion must survive a merge). */
|
|
7
|
+
export function mergeRecords(records) {
|
|
8
|
+
const byId = new Map();
|
|
9
|
+
for (const record of records) {
|
|
10
|
+
const existing = byId.get(record.id);
|
|
11
|
+
byId.set(record.id, existing ? pickLatest(existing, record) : record);
|
|
12
|
+
}
|
|
13
|
+
return [...byId.values()];
|
|
14
|
+
}
|
|
15
|
+
/** A record is visible only if the winning version is not a tombstone. */
|
|
16
|
+
export function isVisible(record) {
|
|
17
|
+
return record.deleted !== true;
|
|
18
|
+
}
|
|
19
|
+
export function liveRecords(records) {
|
|
20
|
+
return mergeRecords(records).filter(isVisible);
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output-side style measurement.
|
|
3
|
+
*
|
|
4
|
+
* This is the instrument for the emotion→behaviour experiment (ADR 0017): we
|
|
5
|
+
* measure what the agent actually wrote, never what it says about itself.
|
|
6
|
+
*/
|
|
7
|
+
import { SEED_LEXICON } from '../affect/seed-lexicon.js';
|
|
8
|
+
import { normalizeForAffect, tokenizeForAffect } from '../affect/analyze.js';
|
|
9
|
+
const CONFIRM_OPENERS = ['好的', '好', '我先', '让我先', '我理解', '先确认', '确认一下', '明白'];
|
|
10
|
+
export function measureStyle(text, lexicon = SEED_LEXICON) {
|
|
11
|
+
if (typeof text !== 'string')
|
|
12
|
+
throw new TypeError('text must be a string');
|
|
13
|
+
const normalized = normalizeForAffect(text);
|
|
14
|
+
let hedges = 0;
|
|
15
|
+
if (lexicon.hedges.length > 0) {
|
|
16
|
+
const dict = new Set(lexicon.hedges);
|
|
17
|
+
const maxLen = lexicon.hedges.reduce((n, w) => Math.max(n, w.length), 1);
|
|
18
|
+
hedges = tokenizeForAffect(normalized, dict, maxLen).filter((t) => dict.has(t)).length;
|
|
19
|
+
}
|
|
20
|
+
const questions = (normalized.match(/\?/g) ?? []).length;
|
|
21
|
+
const head = normalized.trim().slice(0, 24);
|
|
22
|
+
return {
|
|
23
|
+
length: text.length,
|
|
24
|
+
hedgeRate: normalized.length > 0 ? Math.round(((hedges * 100) / normalized.length) * 100) / 100 : 0,
|
|
25
|
+
questions,
|
|
26
|
+
confirmFirst: CONFIRM_OPENERS.some((opener) => head.startsWith(opener)),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -12,3 +12,16 @@ export const DEFAULT_EMOTION = {
|
|
|
12
12
|
curiosity: 0.3,
|
|
13
13
|
contentment: 0.3,
|
|
14
14
|
};
|
|
15
|
+
export const EMPTY_AFFECT = {
|
|
16
|
+
analyzer: 'none',
|
|
17
|
+
hits: [],
|
|
18
|
+
dominant: null,
|
|
19
|
+
intensity: 0,
|
|
20
|
+
valence: 0,
|
|
21
|
+
arousal: 0,
|
|
22
|
+
features: { fillers: 0, hedges: 0, vague: 0, tokens: 0, density: 1 },
|
|
23
|
+
confidence: 0,
|
|
24
|
+
abstain: false,
|
|
25
|
+
};
|
|
26
|
+
/** Wire version of the four verbs. Additive changes bump MINOR (ADR 0005). */
|
|
27
|
+
export const PROTOCOL_VERSION = '0.2';
|