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.
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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();