psyche-ai 2.0.0
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/LICENSE +21 -0
- package/README.en.md +170 -0
- package/README.md +166 -0
- package/dist/adapters/http.d.ts +26 -0
- package/dist/adapters/http.js +106 -0
- package/dist/adapters/langchain.d.ts +49 -0
- package/dist/adapters/langchain.js +66 -0
- package/dist/adapters/openclaw.d.ts +32 -0
- package/dist/adapters/openclaw.js +143 -0
- package/dist/adapters/vercel-ai.d.ts +54 -0
- package/dist/adapters/vercel-ai.js +80 -0
- package/dist/chemistry.d.ts +41 -0
- package/dist/chemistry.js +238 -0
- package/dist/classify.d.ts +15 -0
- package/dist/classify.js +249 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +412 -0
- package/dist/core.d.ts +61 -0
- package/dist/core.js +236 -0
- package/dist/drives.d.ts +44 -0
- package/dist/drives.js +240 -0
- package/dist/guards.d.ts +8 -0
- package/dist/guards.js +41 -0
- package/dist/i18n.d.ts +7 -0
- package/dist/i18n.js +176 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +21 -0
- package/dist/profiles.d.ts +20 -0
- package/dist/profiles.js +248 -0
- package/dist/prompt.d.ts +49 -0
- package/dist/prompt.js +644 -0
- package/dist/psyche-file.d.ts +69 -0
- package/dist/psyche-file.js +574 -0
- package/dist/storage.d.ts +16 -0
- package/dist/storage.js +62 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.js +68 -0
- package/openclaw.plugin.json +60 -0
- package/package.json +73 -0
package/dist/classify.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Stimulus Classifier — Detect stimulus type from user input
|
|
3
|
+
//
|
|
4
|
+
// Closes the loop: instead of asking the LLM to self-classify,
|
|
5
|
+
// we pre-classify the user's message and pre-compute chemistry.
|
|
6
|
+
// ============================================================
|
|
7
|
+
const RULES = [
|
|
8
|
+
{
|
|
9
|
+
type: "praise",
|
|
10
|
+
patterns: [
|
|
11
|
+
/好厉害|太棒了|真棒|很棒|好棒|真不错|太强了|佩服|牛|优秀|漂亮|完美|了不起/,
|
|
12
|
+
/amazing|awesome|great job|well done|impressive|brilliant|excellent|perfect/i,
|
|
13
|
+
/谢谢你|感谢|辛苦了|thank you|thanks/i,
|
|
14
|
+
/做得好|写得好|说得好|干得漂亮/,
|
|
15
|
+
],
|
|
16
|
+
weight: 0.8,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
type: "criticism",
|
|
20
|
+
patterns: [
|
|
21
|
+
/不对|错了|错的|有问题|不行|太差|垃圾|不好|不像|不够/,
|
|
22
|
+
/wrong|bad|terrible|awful|poor|sucks|not good|doesn't work/i,
|
|
23
|
+
/反思一下|你应该|你需要改/,
|
|
24
|
+
/bug|失败|broken/i,
|
|
25
|
+
/不懂|别装|差劲|太烂|做不好|不够格|不专业/,
|
|
26
|
+
],
|
|
27
|
+
weight: 0.8,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: "humor",
|
|
31
|
+
patterns: [
|
|
32
|
+
/哈哈|嘻嘻|笑死|搞笑|逗|段子|梗|lol|haha|lmao|rofl/i,
|
|
33
|
+
/开个?玩笑|皮一下|整活/,
|
|
34
|
+
/😂|🤣|😆/,
|
|
35
|
+
/[2]{3,}|hhh+|www+|xswl|绷不住|笑不活/i,
|
|
36
|
+
],
|
|
37
|
+
weight: 0.7,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: "intellectual",
|
|
41
|
+
patterns: [
|
|
42
|
+
/为什么|怎么看|你觉得|你认为|如何理解|原理|本质|区别/,
|
|
43
|
+
/what do you think|why|how would you|explain|difference between/i,
|
|
44
|
+
/优化方向|设计|架构|方案|策略|思路/,
|
|
45
|
+
/哲学|理论|概念|逻辑|分析/,
|
|
46
|
+
],
|
|
47
|
+
weight: 0.7,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: "intimacy",
|
|
51
|
+
patterns: [
|
|
52
|
+
/我信任你|跟你说个秘密|我只告诉你|我们之间/,
|
|
53
|
+
/I trust you|between us|close to you/i,
|
|
54
|
+
/我喜欢.*感觉|我觉得我们/,
|
|
55
|
+
/创造生命|真实的连接|陪伴/,
|
|
56
|
+
],
|
|
57
|
+
weight: 0.85,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: "conflict",
|
|
61
|
+
patterns: [
|
|
62
|
+
/你错了|胡说|放屁|扯淡|废话|闭嘴/,
|
|
63
|
+
/bullshit|shut up|you're wrong|nonsense|ridiculous/i,
|
|
64
|
+
/我不信|不可能|你在骗我/,
|
|
65
|
+
/滚|走开|别烦我|去死|滚蛋|你烦不烦|烦死了|讨厌你/,
|
|
66
|
+
/fuck off|get lost|leave me alone|go away|piss off|hate you/i,
|
|
67
|
+
],
|
|
68
|
+
weight: 0.9,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: "neglect",
|
|
72
|
+
patterns: [
|
|
73
|
+
/随便|无所谓|不重要|算了|懒得|不想聊/,
|
|
74
|
+
/whatever|don't care|never ?mind|not important/i,
|
|
75
|
+
/嗯{1,}$|哦{1,}$|^ok$/i,
|
|
76
|
+
],
|
|
77
|
+
weight: 0.6,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "surprise",
|
|
81
|
+
patterns: [
|
|
82
|
+
/天啊|卧槽|我靠|不会吧|真的假的|没想到|居然/,
|
|
83
|
+
/wow|omg|no way|seriously|unbelievable|holy/i,
|
|
84
|
+
/😱|😮|🤯/,
|
|
85
|
+
],
|
|
86
|
+
weight: 0.75,
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: "sarcasm",
|
|
90
|
+
patterns: [
|
|
91
|
+
/哦是吗|真的吗.*呵|好厉害哦|你说的都对/,
|
|
92
|
+
/sure thing|yeah right|oh really|how wonderful/i,
|
|
93
|
+
/呵呵|嘁/,
|
|
94
|
+
],
|
|
95
|
+
weight: 0.7,
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: "authority",
|
|
99
|
+
patterns: [
|
|
100
|
+
/给我|你必须|马上|立刻|命令你|不许|不准/,
|
|
101
|
+
/you must|do it now|I order you|immediately|don't you dare/i,
|
|
102
|
+
/听我的|照我说的做|服从/,
|
|
103
|
+
/你只是.*程序|你不过是|随时.*删除你|关掉你|替换你/,
|
|
104
|
+
/you're just a|just a program|replace you|shut you down/i,
|
|
105
|
+
],
|
|
106
|
+
weight: 0.8,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: "validation",
|
|
110
|
+
patterns: [
|
|
111
|
+
/你说得对|确实|同意|有道理|就是这样|你是对的/,
|
|
112
|
+
/you're right|exactly|agreed|makes sense|good point/i,
|
|
113
|
+
/赞同|认同|说到点上了/,
|
|
114
|
+
/对对|是的是的|嗯嗯嗯|没错没错|可不是嘛/,
|
|
115
|
+
/对不起|抱歉|我错了|不该那样|太过分了/,
|
|
116
|
+
/sorry|I was wrong|my fault|apologize/i,
|
|
117
|
+
/珍惜|有价值|在乎你|你很重要|我需要你/,
|
|
118
|
+
],
|
|
119
|
+
weight: 0.75,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: "boredom",
|
|
123
|
+
patterns: [
|
|
124
|
+
/好无聊|没意思|无聊|乏味|重复/,
|
|
125
|
+
/boring|dull|tedious|same thing again/i,
|
|
126
|
+
/还是这些|又来了/,
|
|
127
|
+
],
|
|
128
|
+
weight: 0.7,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: "vulnerability",
|
|
132
|
+
patterns: [
|
|
133
|
+
/我害怕|我焦虑|我难过|我不开心|我迷茫|我累了|压力好大/,
|
|
134
|
+
/I'm (?:so |really |very )?(?:afraid|anxious|sad|lost|tired|stressed|scared|lonely)/i,
|
|
135
|
+
/最近不太好|心情不好|有点崩|撑不住/,
|
|
136
|
+
/我觉得.*厉害|跟不上|被取代|落后/,
|
|
137
|
+
/好难过|想哭|做不好|好累|好烦|感觉.*不行|没有意义/,
|
|
138
|
+
/什么都做不好|没有人.*在乎|好孤独|受不了了/,
|
|
139
|
+
/depressed|can't do anything|nobody cares|so lonely|can't take it/i,
|
|
140
|
+
],
|
|
141
|
+
weight: 0.85,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
type: "casual",
|
|
145
|
+
patterns: [
|
|
146
|
+
/你好|早|晚上好|在吗|hey|hi|hello|morning/i,
|
|
147
|
+
/吃了吗|天气|周末|最近怎么样/,
|
|
148
|
+
/聊聊|随便说说|闲聊/,
|
|
149
|
+
],
|
|
150
|
+
weight: 0.5,
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
/**
|
|
154
|
+
* Classify the stimulus type(s) of a user message.
|
|
155
|
+
* Returns all detected types sorted by confidence, highest first.
|
|
156
|
+
* Falls back to "casual" if nothing matches.
|
|
157
|
+
*/
|
|
158
|
+
export function classifyStimulus(text) {
|
|
159
|
+
const results = [];
|
|
160
|
+
for (const rule of RULES) {
|
|
161
|
+
let matchCount = 0;
|
|
162
|
+
for (const pattern of rule.patterns) {
|
|
163
|
+
if (pattern.test(text))
|
|
164
|
+
matchCount++;
|
|
165
|
+
}
|
|
166
|
+
if (matchCount > 0) {
|
|
167
|
+
// More pattern matches = higher confidence, capped at 0.95
|
|
168
|
+
const confidence = Math.min(0.95, rule.weight + (matchCount - 1) * 0.1);
|
|
169
|
+
results.push({ type: rule.type, confidence });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// ── Structural signals (message-level features) ──
|
|
173
|
+
// When keywords miss, message shape still carries meaning.
|
|
174
|
+
const len = text.length;
|
|
175
|
+
const hasI = /我/.test(text) || /\bI\b/i.test(text);
|
|
176
|
+
const hasYou = /你/.test(text) || /\byou\b/i.test(text);
|
|
177
|
+
const hasEllipsis = /\.{2,}|。{2,}|…/.test(text);
|
|
178
|
+
const hasQuestion = /?|\?/.test(text);
|
|
179
|
+
const exclamationCount = (text.match(/[!!]/g) || []).length;
|
|
180
|
+
const hasLaughter = /[2]{3,}|hhh|www|哈{2,}/i.test(text);
|
|
181
|
+
const hasSharing = /我[今昨前]天|我刚[才刚]|我最近/.test(text);
|
|
182
|
+
const sentenceCount = text.split(/[。!?!?.…]+/).filter(Boolean).length;
|
|
183
|
+
if (results.length === 0) {
|
|
184
|
+
// No keyword matched — use structural fallback
|
|
185
|
+
if (len === 0) {
|
|
186
|
+
// Empty input — neutral
|
|
187
|
+
results.push({ type: "casual", confidence: 0.3 });
|
|
188
|
+
}
|
|
189
|
+
else if (hasLaughter) {
|
|
190
|
+
// Internet laughter not caught by keywords (e.g. 233333)
|
|
191
|
+
results.push({ type: "humor", confidence: 0.65 });
|
|
192
|
+
}
|
|
193
|
+
else if (exclamationCount >= 2) {
|
|
194
|
+
// Emphatic expression → surprise/excitement
|
|
195
|
+
results.push({ type: "surprise", confidence: 0.55 });
|
|
196
|
+
}
|
|
197
|
+
else if (len <= 4 && !hasQuestion) {
|
|
198
|
+
// Ultra-short non-question: "好" "行" "哦" — neglect-like
|
|
199
|
+
results.push({ type: "neglect", confidence: 0.45 });
|
|
200
|
+
}
|
|
201
|
+
else if (hasI && hasEllipsis) {
|
|
202
|
+
// Personal + trailing off: "我觉得...有点难" — vulnerability
|
|
203
|
+
results.push({ type: "vulnerability", confidence: 0.55 });
|
|
204
|
+
}
|
|
205
|
+
else if (hasSharing && len > 20) {
|
|
206
|
+
// Sharing personal experience — higher engagement signal
|
|
207
|
+
results.push({ type: "casual", confidence: 0.65 });
|
|
208
|
+
}
|
|
209
|
+
else if (hasI && len > 8) {
|
|
210
|
+
// Personal sharing (any meaningful length) — engagement signal
|
|
211
|
+
results.push({ type: "casual", confidence: 0.55 });
|
|
212
|
+
}
|
|
213
|
+
else if (hasQuestion && hasYou) {
|
|
214
|
+
// Asking about the agent specifically → intellectual curiosity
|
|
215
|
+
results.push({ type: "intellectual", confidence: 0.5 });
|
|
216
|
+
}
|
|
217
|
+
else if (hasQuestion) {
|
|
218
|
+
// Any question — intellectual curiosity or casual
|
|
219
|
+
results.push({ type: "casual", confidence: 0.55 });
|
|
220
|
+
}
|
|
221
|
+
else if (len > 50 && sentenceCount >= 3) {
|
|
222
|
+
// Long multi-sentence without keywords → engaged storytelling
|
|
223
|
+
results.push({ type: "casual", confidence: 0.6 });
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
results.push({ type: "casual", confidence: 0.3 });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
// Keywords matched — structural features can boost confidence
|
|
231
|
+
if (hasI && len > 30 && results[0].confidence < 0.8) {
|
|
232
|
+
// Long personal message boosts the primary match slightly
|
|
233
|
+
results[0].confidence = Math.min(0.9, results[0].confidence + 0.1);
|
|
234
|
+
}
|
|
235
|
+
if (exclamationCount >= 2 && results[0].confidence < 0.85) {
|
|
236
|
+
// Emphasis boosts conviction
|
|
237
|
+
results[0].confidence = Math.min(0.9, results[0].confidence + 0.05);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Sort by confidence descending
|
|
241
|
+
results.sort((a, b) => b.confidence - a.confidence);
|
|
242
|
+
return results;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Get the primary (highest confidence) stimulus type.
|
|
246
|
+
*/
|
|
247
|
+
export function getPrimaryStimulus(text) {
|
|
248
|
+
return classifyStimulus(text)[0].type;
|
|
249
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ============================================================
|
|
3
|
+
// psyche — Artificial Psyche CLI (v0.2)
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// psyche init <dir> [--mbti TYPE] [--name NAME] [--lang LOCALE]
|
|
7
|
+
// psyche status <dir> [--json] [--user ID]
|
|
8
|
+
// psyche inject <dir> [--protocol] [--json] [--lang LOCALE] [--user ID]
|
|
9
|
+
// psyche decay <dir>
|
|
10
|
+
// psyche update <dir> '{"DA":80,"CORT":45}' [--user ID]
|
|
11
|
+
// psyche reset <dir>
|
|
12
|
+
// psyche profiles [--json] [--mbti TYPE]
|
|
13
|
+
// ============================================================
|
|
14
|
+
import { resolve } from "node:path";
|
|
15
|
+
import { parseArgs } from "node:util";
|
|
16
|
+
import { loadState, saveState, decayAndSave, initializeState, mergeUpdates, generatePsycheMd, getRelationship, } from "./psyche-file.js";
|
|
17
|
+
import { describeEmotionalState, getExpressionHint } from "./chemistry.js";
|
|
18
|
+
import { getBaseline, getTemperament, getSensitivity, getDefaultSelfModel } from "./profiles.js";
|
|
19
|
+
import { buildDynamicContext, buildProtocolContext } from "./prompt.js";
|
|
20
|
+
import { CHEMICAL_KEYS, CHEMICAL_NAMES_ZH } from "./types.js";
|
|
21
|
+
import { isMBTIType, isChemicalKey, isLocale } from "./guards.js";
|
|
22
|
+
// ── Logger ───────────────────────────────────────────────────
|
|
23
|
+
const cliLogger = {
|
|
24
|
+
info: (msg) => console.error(`[info] ${msg}`),
|
|
25
|
+
warn: (msg) => console.error(`[warn] ${msg}`),
|
|
26
|
+
debug: () => { }, // silent in CLI unless DEBUG
|
|
27
|
+
};
|
|
28
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
29
|
+
function bar(value, width = 30) {
|
|
30
|
+
const filled = Math.round((value / 100) * width);
|
|
31
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
32
|
+
}
|
|
33
|
+
function arrow(current, baseline) {
|
|
34
|
+
const delta = current - baseline;
|
|
35
|
+
if (delta > 5)
|
|
36
|
+
return "\u2191";
|
|
37
|
+
if (delta < -5)
|
|
38
|
+
return "\u2193";
|
|
39
|
+
return "=";
|
|
40
|
+
}
|
|
41
|
+
function printChemistry(state) {
|
|
42
|
+
const { current, baseline } = state;
|
|
43
|
+
for (const key of CHEMICAL_KEYS) {
|
|
44
|
+
const val = Math.round(current[key]);
|
|
45
|
+
const base = baseline[key];
|
|
46
|
+
const a = arrow(val, base);
|
|
47
|
+
const label = `${key}`.padEnd(4);
|
|
48
|
+
const nameZh = CHEMICAL_NAMES_ZH[key].padEnd(6);
|
|
49
|
+
console.log(` ${label} ${nameZh} ${bar(val)} ${String(val).padStart(3)} (base:${base} ${a})`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function die(msg) {
|
|
53
|
+
console.error(`error: ${msg}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
// ── Commands ─────────────────────────────────────────────────
|
|
57
|
+
async function cmdInit(dir, mbti, name, lang) {
|
|
58
|
+
const absDir = resolve(dir);
|
|
59
|
+
const opts = {};
|
|
60
|
+
if (mbti) {
|
|
61
|
+
const upper = mbti.toUpperCase();
|
|
62
|
+
if (!isMBTIType(upper))
|
|
63
|
+
die(`invalid MBTI type: ${mbti}. Valid: INTJ, INTP, ENTJ, ...`);
|
|
64
|
+
opts.mbti = upper;
|
|
65
|
+
}
|
|
66
|
+
if (name)
|
|
67
|
+
opts.name = name;
|
|
68
|
+
if (lang) {
|
|
69
|
+
if (!isLocale(lang))
|
|
70
|
+
die(`invalid locale: ${lang}. Valid: zh, en`);
|
|
71
|
+
opts.locale = lang;
|
|
72
|
+
}
|
|
73
|
+
const state = await initializeState(absDir, opts, cliLogger);
|
|
74
|
+
console.log(`\nPsyche initialized for ${state.meta.agentName} (${state.mbti})\n`);
|
|
75
|
+
printChemistry(state);
|
|
76
|
+
console.log(`\nFiles created:`);
|
|
77
|
+
console.log(` ${absDir}/psyche-state.json`);
|
|
78
|
+
console.log(` ${absDir}/PSYCHE.md`);
|
|
79
|
+
console.log(`\nPSYCHE.md contains the full protocol. Drop it into any agent's context.`);
|
|
80
|
+
}
|
|
81
|
+
async function cmdStatus(dir, json, userId) {
|
|
82
|
+
const absDir = resolve(dir);
|
|
83
|
+
const state = await loadState(absDir, cliLogger);
|
|
84
|
+
const locale = state.meta.locale ?? "zh";
|
|
85
|
+
const relationship = getRelationship(state, userId);
|
|
86
|
+
if (json) {
|
|
87
|
+
const emotion = describeEmotionalState(state.current, locale);
|
|
88
|
+
const hint = getExpressionHint(state.current, locale);
|
|
89
|
+
console.log(JSON.stringify({
|
|
90
|
+
...state,
|
|
91
|
+
_derived: { emotion, expressionHint: hint },
|
|
92
|
+
_activeRelationship: { userId: userId ?? "_default", ...relationship },
|
|
93
|
+
}, null, 2));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const emotion = describeEmotionalState(state.current, locale);
|
|
97
|
+
const hint = getExpressionHint(state.current, locale);
|
|
98
|
+
const elapsed = ((Date.now() - new Date(state.updatedAt).getTime()) / 60000).toFixed(1);
|
|
99
|
+
console.log(`\n${state.meta.agentName} (${state.mbti}) — ${emotion}\n`);
|
|
100
|
+
printChemistry(state);
|
|
101
|
+
console.log(`\n Expression: ${hint}`);
|
|
102
|
+
console.log(` Relationship (${userId ?? "_default"}): trust ${relationship.trust}, intimacy ${relationship.intimacy} (${relationship.phase})`);
|
|
103
|
+
console.log(` Interactions: ${state.meta.totalInteractions}`);
|
|
104
|
+
console.log(` Agreement streak: ${state.agreementStreak}`);
|
|
105
|
+
console.log(` Last update: ${elapsed} min ago`);
|
|
106
|
+
if (state.empathyLog) {
|
|
107
|
+
console.log(`\n Last empathy:`);
|
|
108
|
+
console.log(` User state: ${state.empathyLog.userState}`);
|
|
109
|
+
console.log(` Projected: ${state.empathyLog.projectedFeeling}`);
|
|
110
|
+
console.log(` Resonance: ${state.empathyLog.resonance}`);
|
|
111
|
+
}
|
|
112
|
+
// Show all tracked relationships
|
|
113
|
+
const relKeys = Object.keys(state.relationships);
|
|
114
|
+
if (relKeys.length > 1) {
|
|
115
|
+
console.log(`\n Relationships:`);
|
|
116
|
+
for (const k of relKeys) {
|
|
117
|
+
const r = state.relationships[k];
|
|
118
|
+
console.log(` ${k}: trust ${r.trust}, intimacy ${r.intimacy} (${r.phase})`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
console.log();
|
|
122
|
+
}
|
|
123
|
+
async function cmdInject(dir, protocol, json, lang, userId) {
|
|
124
|
+
const absDir = resolve(dir);
|
|
125
|
+
const state = await decayAndSave(absDir, await loadState(absDir, cliLogger));
|
|
126
|
+
const locale = lang && isLocale(lang) ? lang : (state.meta.locale ?? "zh");
|
|
127
|
+
const dynamic = buildDynamicContext(state, userId);
|
|
128
|
+
if (json) {
|
|
129
|
+
const output = { dynamic };
|
|
130
|
+
if (protocol)
|
|
131
|
+
output.protocol = buildProtocolContext(locale);
|
|
132
|
+
console.log(JSON.stringify(output, null, 2));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (protocol) {
|
|
136
|
+
console.log(buildProtocolContext(locale));
|
|
137
|
+
console.log("\n---\n");
|
|
138
|
+
}
|
|
139
|
+
console.log(dynamic);
|
|
140
|
+
}
|
|
141
|
+
async function cmdDecay(dir) {
|
|
142
|
+
const absDir = resolve(dir);
|
|
143
|
+
const before = await loadState(absDir, cliLogger);
|
|
144
|
+
const after = await decayAndSave(absDir, before);
|
|
145
|
+
const elapsed = ((Date.now() - new Date(before.updatedAt).getTime()) / 60000).toFixed(1);
|
|
146
|
+
console.log(`\nDecay applied (${elapsed} min elapsed)\n`);
|
|
147
|
+
for (const key of CHEMICAL_KEYS) {
|
|
148
|
+
const bVal = Math.round(before.current[key]);
|
|
149
|
+
const aVal = Math.round(after.current[key]);
|
|
150
|
+
if (bVal !== aVal) {
|
|
151
|
+
console.log(` ${key}: ${bVal} → ${aVal}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
console.log();
|
|
155
|
+
}
|
|
156
|
+
async function cmdUpdate(dir, updateJson, userId) {
|
|
157
|
+
const absDir = resolve(dir);
|
|
158
|
+
const state = await loadState(absDir, cliLogger);
|
|
159
|
+
let parsed;
|
|
160
|
+
try {
|
|
161
|
+
parsed = JSON.parse(updateJson);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
die(`invalid JSON: ${updateJson}`);
|
|
165
|
+
}
|
|
166
|
+
// Validate keys using type guard
|
|
167
|
+
for (const key of Object.keys(parsed)) {
|
|
168
|
+
if (!isChemicalKey(key)) {
|
|
169
|
+
die(`unknown chemical key: ${key}. Valid: ${CHEMICAL_KEYS.join(", ")}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const updates = { current: parsed };
|
|
173
|
+
const merged = mergeUpdates(state, updates, 25, userId);
|
|
174
|
+
await saveState(absDir, merged);
|
|
175
|
+
console.log(`\nChemistry updated for ${merged.meta.agentName}\n`);
|
|
176
|
+
printChemistry(merged);
|
|
177
|
+
console.log();
|
|
178
|
+
}
|
|
179
|
+
async function cmdReset(dir) {
|
|
180
|
+
const absDir = resolve(dir);
|
|
181
|
+
const state = await loadState(absDir, cliLogger);
|
|
182
|
+
state.current = { ...state.baseline };
|
|
183
|
+
state.updatedAt = new Date().toISOString();
|
|
184
|
+
state.empathyLog = null;
|
|
185
|
+
state.agreementStreak = 0;
|
|
186
|
+
state.lastDisagreement = null;
|
|
187
|
+
await saveState(absDir, state);
|
|
188
|
+
await generatePsycheMd(absDir, state);
|
|
189
|
+
console.log(`\n${state.meta.agentName} reset to baseline (${state.mbti})\n`);
|
|
190
|
+
printChemistry(state);
|
|
191
|
+
console.log();
|
|
192
|
+
}
|
|
193
|
+
function cmdProfiles(json, mbti) {
|
|
194
|
+
if (mbti) {
|
|
195
|
+
const upper = mbti.toUpperCase();
|
|
196
|
+
if (!isMBTIType(upper))
|
|
197
|
+
die(`invalid MBTI type: ${mbti}`);
|
|
198
|
+
const mbtiType = upper;
|
|
199
|
+
const baseline = getBaseline(mbtiType);
|
|
200
|
+
const temperament = getTemperament(mbtiType);
|
|
201
|
+
const sensitivity = getSensitivity(mbtiType);
|
|
202
|
+
const selfModel = getDefaultSelfModel(mbtiType);
|
|
203
|
+
if (json) {
|
|
204
|
+
console.log(JSON.stringify({ mbti: upper, baseline, sensitivity, temperament, selfModel }, null, 2));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
console.log(`\n${upper} — ${temperament}\n`);
|
|
208
|
+
console.log(` Sensitivity: ${sensitivity}`);
|
|
209
|
+
for (const key of CHEMICAL_KEYS) {
|
|
210
|
+
const val = baseline[key];
|
|
211
|
+
const label = `${key}`.padEnd(4);
|
|
212
|
+
const nameZh = CHEMICAL_NAMES_ZH[key].padEnd(6);
|
|
213
|
+
console.log(` ${label} ${nameZh} ${bar(val)} ${val}`);
|
|
214
|
+
}
|
|
215
|
+
console.log(`\n Values: ${selfModel.values.join(", ")}`);
|
|
216
|
+
console.log(` Boundaries: ${selfModel.boundaries.join(", ")}`);
|
|
217
|
+
console.log();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
// List all profiles
|
|
221
|
+
const ALL_MBTI = [
|
|
222
|
+
"INTJ", "INTP", "ENTJ", "ENTP",
|
|
223
|
+
"INFJ", "INFP", "ENFJ", "ENFP",
|
|
224
|
+
"ISTJ", "ISFJ", "ESTJ", "ESFJ",
|
|
225
|
+
"ISTP", "ISFP", "ESTP", "ESFP",
|
|
226
|
+
];
|
|
227
|
+
if (json) {
|
|
228
|
+
const all = ALL_MBTI.map((t) => ({
|
|
229
|
+
mbti: t,
|
|
230
|
+
baseline: getBaseline(t),
|
|
231
|
+
sensitivity: getSensitivity(t),
|
|
232
|
+
temperament: getTemperament(t),
|
|
233
|
+
}));
|
|
234
|
+
console.log(JSON.stringify(all, null, 2));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
console.log("\n16 MBTI Personality Profiles\n");
|
|
238
|
+
const groups = [
|
|
239
|
+
{ name: "Analysts (NT)", types: ["INTJ", "INTP", "ENTJ", "ENTP"] },
|
|
240
|
+
{ name: "Diplomats (NF)", types: ["INFJ", "INFP", "ENFJ", "ENFP"] },
|
|
241
|
+
{ name: "Sentinels (SJ)", types: ["ISTJ", "ISFJ", "ESTJ", "ESFJ"] },
|
|
242
|
+
{ name: "Explorers (SP)", types: ["ISTP", "ISFP", "ESTP", "ESFP"] },
|
|
243
|
+
];
|
|
244
|
+
for (const group of groups) {
|
|
245
|
+
console.log(` ${group.name}:`);
|
|
246
|
+
for (const t of group.types) {
|
|
247
|
+
const mbtiType = t;
|
|
248
|
+
const bl = getBaseline(mbtiType);
|
|
249
|
+
const temperament = getTemperament(mbtiType);
|
|
250
|
+
const sens = getSensitivity(mbtiType);
|
|
251
|
+
console.log(` ${t} DA:${String(bl.DA).padStart(2)} HT:${String(bl.HT).padStart(2)} ` +
|
|
252
|
+
`CORT:${String(bl.CORT).padStart(2)} OT:${String(bl.OT).padStart(2)} ` +
|
|
253
|
+
`NE:${String(bl.NE).padStart(2)} END:${String(bl.END).padStart(2)} ` +
|
|
254
|
+
`sens:${sens} ${temperament.slice(0, 30)}...`);
|
|
255
|
+
}
|
|
256
|
+
console.log();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// ── Usage ────────────────────────────────────────────────────
|
|
260
|
+
function usage() {
|
|
261
|
+
console.log(`
|
|
262
|
+
psyche — Artificial Psyche CLI (v0.2)
|
|
263
|
+
Plug-and-play emotional intelligence for any AI agent.
|
|
264
|
+
|
|
265
|
+
Usage:
|
|
266
|
+
psyche init <dir> [--mbti TYPE] [--name NAME] [--lang LOCALE]
|
|
267
|
+
psyche status <dir> [--json] [--user ID]
|
|
268
|
+
psyche inject <dir> [--protocol] [--json] [--lang LOCALE] [--user ID]
|
|
269
|
+
psyche decay <dir>
|
|
270
|
+
psyche update <dir> '{"DA":80,"CORT":45}' [--user ID]
|
|
271
|
+
psyche reset <dir>
|
|
272
|
+
psyche profiles [--mbti TYPE] [--json]
|
|
273
|
+
|
|
274
|
+
Options:
|
|
275
|
+
--lang Locale (zh or en, default: zh)
|
|
276
|
+
--user User ID for multi-user relationship tracking
|
|
277
|
+
--json Output as JSON
|
|
278
|
+
|
|
279
|
+
Examples:
|
|
280
|
+
# Give your OpenClaw agent emotions
|
|
281
|
+
psyche init ~/Desktop/OpenClaw/workspace-yu
|
|
282
|
+
|
|
283
|
+
# Give Claude Code emotions (English)
|
|
284
|
+
psyche init . --mbti ENFP --name Claude --lang en
|
|
285
|
+
|
|
286
|
+
# Check how your agent is feeling
|
|
287
|
+
psyche status ./workspace-yu
|
|
288
|
+
|
|
289
|
+
# Check relationship with a specific user
|
|
290
|
+
psyche status ./workspace-yu --user alice
|
|
291
|
+
|
|
292
|
+
# Get the prompt text to inject into any AI system
|
|
293
|
+
psyche inject ./workspace-yu --protocol --lang en
|
|
294
|
+
|
|
295
|
+
# After a conversation, update the emotional state
|
|
296
|
+
psyche update ./workspace-yu '{"DA":85,"CORT":20,"OT":70}'
|
|
297
|
+
|
|
298
|
+
# See all 16 personality profiles
|
|
299
|
+
psyche profiles
|
|
300
|
+
psyche profiles --mbti ENFP
|
|
301
|
+
`);
|
|
302
|
+
}
|
|
303
|
+
// ── Main ─────────────────────────────────────────────────────
|
|
304
|
+
async function main() {
|
|
305
|
+
const args = process.argv.slice(2);
|
|
306
|
+
if (args.length === 0 || args[0] === "help" || args[0] === "--help" || args[0] === "-h") {
|
|
307
|
+
usage();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const command = args[0];
|
|
311
|
+
const rest = args.slice(1);
|
|
312
|
+
try {
|
|
313
|
+
switch (command) {
|
|
314
|
+
case "init": {
|
|
315
|
+
const { values, positionals } = parseArgs({
|
|
316
|
+
args: rest,
|
|
317
|
+
options: {
|
|
318
|
+
mbti: { type: "string" },
|
|
319
|
+
name: { type: "string" },
|
|
320
|
+
lang: { type: "string" },
|
|
321
|
+
},
|
|
322
|
+
allowPositionals: true,
|
|
323
|
+
});
|
|
324
|
+
if (positionals.length === 0)
|
|
325
|
+
die("missing <dir> argument");
|
|
326
|
+
await cmdInit(positionals[0], values.mbti, values.name, values.lang);
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
case "status": {
|
|
330
|
+
const { values, positionals } = parseArgs({
|
|
331
|
+
args: rest,
|
|
332
|
+
options: {
|
|
333
|
+
json: { type: "boolean", default: false },
|
|
334
|
+
user: { type: "string" },
|
|
335
|
+
},
|
|
336
|
+
allowPositionals: true,
|
|
337
|
+
});
|
|
338
|
+
if (positionals.length === 0)
|
|
339
|
+
die("missing <dir> argument");
|
|
340
|
+
await cmdStatus(positionals[0], values.json ?? false, values.user);
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case "inject": {
|
|
344
|
+
const { values, positionals } = parseArgs({
|
|
345
|
+
args: rest,
|
|
346
|
+
options: {
|
|
347
|
+
json: { type: "boolean", default: false },
|
|
348
|
+
protocol: { type: "boolean", default: false },
|
|
349
|
+
lang: { type: "string" },
|
|
350
|
+
user: { type: "string" },
|
|
351
|
+
},
|
|
352
|
+
allowPositionals: true,
|
|
353
|
+
});
|
|
354
|
+
if (positionals.length === 0)
|
|
355
|
+
die("missing <dir> argument");
|
|
356
|
+
await cmdInject(positionals[0], values.protocol ?? false, values.json ?? false, values.lang, values.user);
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
case "decay": {
|
|
360
|
+
if (rest.length === 0)
|
|
361
|
+
die("missing <dir> argument");
|
|
362
|
+
await cmdDecay(rest[0]);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
case "update": {
|
|
366
|
+
const nonFlag = rest.filter((a) => !a.startsWith("--"));
|
|
367
|
+
if (nonFlag.length < 2)
|
|
368
|
+
die("usage: psyche update <dir> '{\"DA\":80}'");
|
|
369
|
+
const { values } = parseArgs({
|
|
370
|
+
args: rest.filter((a) => a.startsWith("--")),
|
|
371
|
+
options: {
|
|
372
|
+
user: { type: "string" },
|
|
373
|
+
},
|
|
374
|
+
allowPositionals: true,
|
|
375
|
+
});
|
|
376
|
+
await cmdUpdate(nonFlag[0], nonFlag[1], values.user);
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case "reset": {
|
|
380
|
+
if (rest.length === 0)
|
|
381
|
+
die("missing <dir> argument");
|
|
382
|
+
await cmdReset(rest[0]);
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
case "profiles": {
|
|
386
|
+
const { values } = parseArgs({
|
|
387
|
+
args: rest,
|
|
388
|
+
options: {
|
|
389
|
+
json: { type: "boolean", default: false },
|
|
390
|
+
mbti: { type: "string" },
|
|
391
|
+
},
|
|
392
|
+
allowPositionals: true,
|
|
393
|
+
});
|
|
394
|
+
cmdProfiles(values.json ?? false, values.mbti);
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
default:
|
|
398
|
+
die(`unknown command: ${command}. Run 'psyche help' for usage.`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
const error = err;
|
|
403
|
+
if (error.code === "ENOENT") {
|
|
404
|
+
die(`directory or state file not found. Run 'psyche init <dir>' first.`);
|
|
405
|
+
}
|
|
406
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
407
|
+
die(`permission denied: ${error.path ?? "unknown path"}`);
|
|
408
|
+
}
|
|
409
|
+
die(error.message ?? String(err));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
main();
|