metame-cli 1.4.31 → 1.4.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.4.31",
3
+ "version": "1.4.32",
4
4
  "description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -13,7 +13,8 @@
13
13
  "scripts": {
14
14
  "test": "node --test scripts/*.test.js",
15
15
  "start": "node index.js",
16
- "sync:plugin": "cp scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/daemon-agent-commands.js scripts/daemon-session-commands.js scripts/daemon-admin-commands.js scripts/daemon-exec-commands.js scripts/daemon-ops-commands.js scripts/daemon-session-store.js scripts/daemon-checkpoints.js scripts/daemon-bridges.js scripts/daemon-file-browser.js scripts/daemon-runtime-lifecycle.js scripts/daemon-notify.js scripts/daemon-claude-engine.js scripts/daemon-command-router.js scripts/daemon-user-acl.js scripts/daemon-agent-tools.js scripts/daemon-task-scheduler.js scripts/daemon-task-envelope.js scripts/task-board.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml scripts/providers.js scripts/utils.js scripts/usage-classifier.js scripts/resolve-yaml.js scripts/memory.js scripts/memory-extract.js scripts/qmd-client.js scripts/session-summarize.js scripts/session-analytics.js scripts/skill-evolution.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo '✅ Plugin scripts synced'",
16
+ "sync:plugin": "cp scripts/platform.js scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/daemon-agent-commands.js scripts/daemon-session-commands.js scripts/daemon-admin-commands.js scripts/daemon-exec-commands.js scripts/daemon-ops-commands.js scripts/daemon-session-store.js scripts/daemon-checkpoints.js scripts/daemon-bridges.js scripts/daemon-file-browser.js scripts/daemon-runtime-lifecycle.js scripts/daemon-notify.js scripts/daemon-claude-engine.js scripts/daemon-command-router.js scripts/daemon-user-acl.js scripts/daemon-agent-tools.js scripts/daemon-task-scheduler.js scripts/daemon-task-envelope.js scripts/task-board.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml scripts/providers.js scripts/utils.js scripts/usage-classifier.js scripts/resolve-yaml.js scripts/memory.js scripts/memory-extract.js scripts/qmd-client.js scripts/session-summarize.js scripts/session-analytics.js scripts/skill-evolution.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo '✅ Plugin scripts synced'",
17
+ "sync:readme": "node scripts/sync-readme.js",
17
18
  "restart:daemon": "node index.js stop 2>/dev/null; sleep 1; node index.js start 2>/dev/null || echo '⚠️ Daemon not running or restart failed'",
18
19
  "precommit": "npm run sync:plugin && npm run restart:daemon"
19
20
  },
@@ -633,7 +633,10 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
633
633
  }, 4000);
634
634
 
635
635
  // Agent nickname routing: "贾维斯" / "小美,帮我..." → switch project session
636
- const agentMatch = routeAgent(prompt, config);
636
+ // Strict chats (chat_agent_map bound groups) must NOT switch agents via nickname
637
+ const _strictAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
638
+ const _isStrictChatSession = !!(_strictAgentMap[String(chatId)] || projectKeyFromVirtualChatId(String(chatId)));
639
+ const agentMatch = _isStrictChatSession ? null : routeAgent(prompt, config);
637
640
  if (agentMatch) {
638
641
  const { key, proj, rest } = agentMatch;
639
642
  const projCwd = normalizeCwd(proj.cwd);
@@ -835,7 +838,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
835
838
  Use these before answering complex questions about MetaMe architecture or past decisions.
836
839
  4. Active memory: After confirming a new insight, bug root cause, or user preference, persist it with:
837
840
  node ~/.metame/memory-write.js "Entity.sub" "relation_type" "value (20-300 chars)"
838
- Valid relations: tech_decision, bug_lesson, arch_convention, config_fact, config_change, user_pref, workflow_rule, project_milestone
841
+ Valid relations: tech_decision, bug_lesson, arch_convention, config_fact, config_change, workflow_rule, project_milestone
839
842
  Only write verified facts. Do not write speculative or process-description entries.
840
843
  When you observe the user is clearly expert or beginner in a domain, note it in your response and suggest: "要不要把你的 {domain} 水平 ({level}) 记录到能力雷达?"
841
844
  5. Task handoff: When suspending a multi-step task or handing off to another agent, write current status to ~/.metame/memory/NOW.md using:
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ const { sleepSync } = require('./platform');
4
+
3
5
  function createPidManager(deps) {
4
- const { fs, execSync, PID_FILE, log } = deps;
6
+ const { fs, PID_FILE, log } = deps;
5
7
 
6
8
  function killExistingDaemon() {
7
9
  if (!fs.existsSync(PID_FILE)) return;
@@ -12,7 +14,7 @@ function createPidManager(deps) {
12
14
  log('INFO', `Killed existing daemon (PID: ${oldPid})`);
13
15
  for (let i = 0; i < 10; i++) {
14
16
  try { process.kill(oldPid, 0); } catch { break; }
15
- execSync('sleep 0.5', { stdio: 'ignore' });
17
+ sleepSync(500);
16
18
  }
17
19
  }
18
20
  } catch {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const crypto = require('crypto');
4
4
  const { classifyTaskUsage } = require('./usage-classifier');
5
+ const { IS_WIN } = require('./platform');
5
6
 
6
7
  const WEEKDAY_INDEX = Object.freeze({
7
8
  sun: 0,
@@ -192,7 +193,30 @@ function createTaskScheduler(deps) {
192
193
  if (!task.precondition) return { pass: true, context: '' };
193
194
 
194
195
  try {
195
- const output = execSync(task.precondition, {
196
+ let cmd = task.precondition;
197
+
198
+ // Cross-platform: expand ~ to HOME and handle `test -s` (Unix-only) via Node.js
199
+ cmd = cmd.replace(/^~|(?<=\s)~/g, HOME);
200
+ if (IS_WIN) {
201
+ // `test -s <file>` checks file exists and is non-empty — do it in JS
202
+ const testMatch = cmd.match(/^test\s+-s\s+(.+)$/);
203
+ if (testMatch) {
204
+ const filePath = testMatch[1].trim().replace(/["']/g, '');
205
+ const fs = require('fs');
206
+ try {
207
+ const stat = fs.statSync(filePath);
208
+ if (stat.size > 0) {
209
+ const content = fs.readFileSync(filePath, 'utf8').trim();
210
+ log('INFO', `Precondition passed for ${task.name} (${content.split('\n').length} lines)`);
211
+ return { pass: true, context: content };
212
+ }
213
+ } catch { /* file doesn't exist */ }
214
+ log('INFO', `Precondition failed for ${task.name}: file empty or missing`);
215
+ return { pass: false, context: '' };
216
+ }
217
+ }
218
+
219
+ const output = execSync(cmd, {
196
220
  encoding: 'utf8',
197
221
  timeout: 15000,
198
222
  maxBuffer: 64 * 1024,
@@ -296,7 +320,8 @@ function createTaskScheduler(deps) {
296
320
 
297
321
  // Script tasks: run a local script directly (e.g. distill.js), no claude -p
298
322
  if (task.type === 'script') {
299
- log('INFO', `Executing script task: ${task.name} → ${task.command}`);
323
+ const scriptCmd = task.command.replace(/^~|(?<=\s)~/g, HOME);
324
+ log('INFO', `Executing script task: ${task.name} → ${scriptCmd}`);
300
325
  try {
301
326
  const scriptEnv = {
302
327
  ...process.env,
@@ -304,7 +329,7 @@ function createTaskScheduler(deps) {
304
329
  METAME_INTERNAL_PROMPT: '1',
305
330
  };
306
331
  delete scriptEnv.CLAUDECODE;
307
- const output = execSync(task.command, {
332
+ const output = execSync(scriptCmd, {
308
333
  encoding: 'utf8',
309
334
  timeout: resolveTimeoutMs(task.timeout, 120),
310
335
  maxBuffer: 1024 * 1024,
package/scripts/daemon.js CHANGED
@@ -16,6 +16,22 @@
16
16
  // Suppress Node.js experimental warnings (e.g. SQLite)
17
17
  process.removeAllListeners('warning');
18
18
 
19
+ // Global error handlers — prevent silent event-loop death
20
+ process.on('unhandledRejection', (reason) => {
21
+ try {
22
+ const msg = reason instanceof Error ? reason.stack || reason.message : String(reason);
23
+ const line = `[${new Date().toISOString()}] [ERROR] [UNHANDLED_REJECTION] ${msg}\n`;
24
+ fs.appendFileSync(path.join(os.homedir(), '.metame', 'daemon.log'), line);
25
+ } catch { /* last resort: don't crash the crash handler */ }
26
+ });
27
+ process.on('uncaughtException', (err) => {
28
+ try {
29
+ const line = `[${new Date().toISOString()}] [FATAL] [UNCAUGHT_EXCEPTION] ${err.stack || err.message}\n`;
30
+ fs.appendFileSync(path.join(os.homedir(), '.metame', 'daemon.log'), line);
31
+ } catch { /* last resort */ }
32
+ // Don't exit — let the daemon survive and self-heal via watchdog
33
+ });
34
+
19
35
  const fs = require('fs');
20
36
  const path = require('path');
21
37
  const os = require('os');
@@ -30,7 +46,8 @@ const LOG_FILE = path.join(METAME_DIR, 'daemon.log');
30
46
  const BRAIN_FILE = path.join(HOME, '.claude_profile.yaml');
31
47
  const DISPATCH_DIR = path.join(METAME_DIR, 'dispatch');
32
48
  const DISPATCH_LOG = path.join(DISPATCH_DIR, 'dispatch-log.jsonl');
33
- const SOCK_PATH = path.join(METAME_DIR, 'daemon.sock');
49
+ const { socketPath, needsSocketCleanup } = require('./platform');
50
+ const SOCK_PATH = socketPath(METAME_DIR);
34
51
 
35
52
  // Resolve claude binary path (daemon may not inherit user's full PATH)
36
53
  const CLAUDE_BIN = (() => {
@@ -975,7 +992,7 @@ function handleDispatchItem(item, config) {
975
992
  */
976
993
  function startDispatchSocket(getConfig) {
977
994
  const net = require('net');
978
- try { fs.unlinkSync(SOCK_PATH); } catch { /* ok */ }
995
+ if (needsSocketCleanup()) { try { fs.unlinkSync(SOCK_PATH); } catch { /* ok */ } }
979
996
  const server = net.createServer((conn) => {
980
997
  let buf = '';
981
998
  conn.on('data', d => { buf += d; });
@@ -1611,7 +1628,6 @@ const { startTelegramBridge, startFeishuBridge } = createBridgeStarter({
1611
1628
 
1612
1629
  const { killExistingDaemon, writePid, cleanPid } = createPidManager({
1613
1630
  fs,
1614
- execSync,
1615
1631
  PID_FILE,
1616
1632
  log,
1617
1633
  });
@@ -1820,6 +1836,27 @@ async function main() {
1820
1836
  process.on('SIGTERM', () => { shutdown().catch(() => process.exit(0)); });
1821
1837
  process.on('SIGINT', () => { shutdown().catch(() => process.exit(0)); });
1822
1838
 
1839
+ // Watchdog: detect heartbeat stall and self-restart
1840
+ const WATCHDOG_INTERVAL = 5 * 60 * 1000; // check every 5 min
1841
+ const HEARTBEAT_STALL_THRESHOLD = 5 * 60 * 1000; // 5 min without heartbeat = stalled
1842
+ setInterval(() => {
1843
+ try {
1844
+ const st = loadState();
1845
+ const lastAlive = st.last_alive ? new Date(st.last_alive).getTime() : 0;
1846
+ const elapsed = Date.now() - lastAlive;
1847
+ if (lastAlive > 0 && elapsed > HEARTBEAT_STALL_THRESHOLD) {
1848
+ log('FATAL', `[WATCHDOG] Heartbeat stalled for ${Math.round(elapsed / 1000)}s — forcing restart`);
1849
+ // Write state before exit so next launch knows why
1850
+ st.watchdog_restart = new Date().toISOString();
1851
+ st.watchdog_stall_seconds = Math.round(elapsed / 1000);
1852
+ saveState(st);
1853
+ process.exit(1); // caffeinate or launchd will restart us
1854
+ }
1855
+ } catch (e) {
1856
+ log('WARN', `[WATCHDOG] Check failed: ${e.message}`);
1857
+ }
1858
+ }, WATCHDOG_INTERVAL).unref();
1859
+
1823
1860
  // Keep alive
1824
1861
  log('INFO', 'Daemon running. Send SIGTERM to stop.');
1825
1862
  }
@@ -401,7 +401,9 @@ function createBot(config) {
401
401
 
402
402
  if (text || fileInfo) {
403
403
  // Fire-and-forget: don't block the event loop (SDK needs fast ack)
404
- Promise.resolve().then(() => onMessage(chatId, text, data, fileInfo, senderId)).catch(() => {});
404
+ Promise.resolve().then(() => onMessage(chatId, text, data, fileInfo, senderId)).catch((err) => {
405
+ try { console.error(`[feishu-adapter] onMessage error: ${err && err.message || err}`); } catch { }
406
+ });
405
407
  }
406
408
  } catch (e) {
407
409
  // Non-fatal
@@ -417,7 +419,9 @@ function createBot(config) {
417
419
  if (action && chatId) {
418
420
  const cmd = action.value && action.value.cmd;
419
421
  if (cmd) {
420
- Promise.resolve().then(() => onMessage(chatId, cmd, data)).catch(() => {});
422
+ Promise.resolve().then(() => onMessage(chatId, cmd, data)).catch((err) => {
423
+ try { console.error(`[feishu-adapter] card action error: ${err && err.message || err}`); } catch { }
424
+ });
421
425
  }
422
426
  }
423
427
  } catch (e) {
@@ -29,8 +29,7 @@ const FACT_EXTRACTION_PROMPT = `你是精准的知识提取引擎。从以下会
29
29
  - arch_convention(架构约定:系统组件的行为边界)
30
30
  - config_fact(配置事实:某个值的真实含义,尤其反直觉的)
31
31
  - config_change(配置变更:用户选择/确认了某个具体配置值,如”字体选了x-large”、”间隔改为2h”)
32
- - user_pref(用户明确表达的偏好/红线)
33
- - workflow_rule(工作流戒律:如”不要在某情况下做某事”的反常识流)
32
+ - workflow_rule(工作流戒律/用户红线:如”不要在某情况下做某事”的反常识流、用户明确表达的项目级红线)
34
33
  - project_milestone(项目里程碑:主要架构重构、版本发布等跨会话级成果)
35
34
 
36
35
  绝对不提取:
@@ -38,6 +37,7 @@ const FACT_EXTRACTION_PROMPT = `你是精准的知识提取引擎。从以下会
38
37
  - 临时状态("当前正在..."、"这次会话...")
39
38
  - 未经验证的猜测("可能是因为..."、"也许...")
40
39
  - 显而易见的常识
40
+ - 泛化偏好(沟通风格、代码风格、语言偏好等)——这些由认知Profile自动采集,memory不重复记录
41
41
 
42
42
  输出 JSON 对象,包含会话名称和提取的事实:
43
43
  {
@@ -11,7 +11,7 @@
11
11
  * 2. search_count < 3
12
12
  * 3. superseded_by IS NULL (already-superseded facts excluded)
13
13
  * 4. conflict_status IS NULL OR conflict_status = 'OK' (skip CONFLICT/ARCHIVED)
14
- * 5. relation NOT IN protected set (user_pref, workflow_rule, arch_convention never archived)
14
+ * 5. relation NOT IN protected set (workflow_rule, arch_convention, config_fact never archived)
15
15
  *
16
16
  * Protected relations are permanently excluded — they are high-value guardrails
17
17
  * that must survive regardless of search frequency.
@@ -32,7 +32,7 @@ const LOCK_FILE = path.join(METAME_DIR, 'memory-gc.lock');
32
32
  const GC_LOG_FILE = path.join(METAME_DIR, 'memory_gc_log.jsonl');
33
33
 
34
34
  // Relations that are permanently protected from archival
35
- const PROTECTED_RELATIONS = ['user_pref', 'workflow_rule', 'arch_convention', 'config_fact'];
35
+ const PROTECTED_RELATIONS = ['workflow_rule', 'arch_convention', 'config_fact'];
36
36
 
37
37
  // GC threshold: facts older than this many days are candidates
38
38
  const STALE_DAYS = 30;
@@ -22,7 +22,6 @@ const VALID_RELATIONS = new Set([
22
22
  'arch_convention',
23
23
  'config_fact',
24
24
  'config_change',
25
- 'user_pref',
26
25
  'workflow_rule',
27
26
  'project_milestone',
28
27
  ]);
package/scripts/memory.js CHANGED
@@ -226,7 +226,7 @@ function saveSession({ sessionId, project, scope = null, summary, keywords = '',
226
226
 
227
227
  // Relations with "current state" semantics: new value replaces old.
228
228
  // Historical relations (tech_decision, bug_lesson, arch_convention, project_milestone) keep all versions.
229
- const STATEFUL_RELATIONS = new Set(['user_pref', 'config_fact', 'config_change', 'workflow_rule']);
229
+ const STATEFUL_RELATIONS = new Set(['config_fact', 'config_change', 'workflow_rule']);
230
230
 
231
231
  /**
232
232
  * Save atomic facts extracted from a session.
@@ -0,0 +1,172 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+
7
+ const IS_WIN = process.platform === 'win32';
8
+ const IS_MAC = process.platform === 'darwin';
9
+ const IS_LINUX = process.platform === 'linux';
10
+
11
+ const HOME = os.homedir();
12
+
13
+ /**
14
+ * IPC socket path: Named Pipe on Windows, Unix Domain Socket elsewhere.
15
+ */
16
+ function socketPath(metameDir) {
17
+ if (IS_WIN) return `\\\\.\\pipe\\metame-daemon-${os.userInfo().username}`;
18
+ return path.join(metameDir, 'daemon.sock');
19
+ }
20
+
21
+ /**
22
+ * Cross-platform synchronous sleep.
23
+ * On Windows, sleep command doesn't exist; use a busy-wait with Atomics for short durations.
24
+ */
25
+ function sleepSync(ms) {
26
+ if (IS_WIN) {
27
+ // Atomics.wait on a SharedArrayBuffer — blocks the thread without shell dependency
28
+ const buf = new SharedArrayBuffer(4);
29
+ const arr = new Int32Array(buf);
30
+ Atomics.wait(arr, 0, 0, ms);
31
+ } else {
32
+ const seconds = ms / 1000;
33
+ execSync(`sleep ${seconds}`, { stdio: 'ignore' });
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Find PIDs matching a command-line pattern.
39
+ * Returns array of PID numbers (excluding current process).
40
+ */
41
+ function findProcessesByPattern(pattern) {
42
+ const pids = [];
43
+ // Sanitize pattern to prevent command injection
44
+ const safe = pattern.replace(/['"%;$`\\]/g, '');
45
+ try {
46
+ let output;
47
+ if (IS_WIN) {
48
+ // wmic is deprecated but widely available; fallback to tasklist
49
+ try {
50
+ output = execSync(
51
+ `wmic process where "CommandLine like '%${safe}%'" get ProcessId /FORMAT:LIST`,
52
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
53
+ );
54
+ const matches = output.match(/ProcessId=(\d+)/g) || [];
55
+ for (const m of matches) {
56
+ const pid = parseInt(m.split('=')[1], 10);
57
+ if (pid && pid !== process.pid) pids.push(pid);
58
+ }
59
+ } catch {
60
+ // wmic not available, try PowerShell
61
+ output = execSync(
62
+ `powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*${safe}*' } | Select-Object -ExpandProperty ProcessId"`,
63
+ { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
64
+ );
65
+ for (const line of output.trim().split('\n')) {
66
+ const pid = parseInt(line.trim(), 10);
67
+ if (pid && pid !== process.pid) pids.push(pid);
68
+ }
69
+ }
70
+ } else {
71
+ output = execSync(`pgrep -f '${safe}' 2>/dev/null || true`, { encoding: 'utf8' });
72
+ for (const line of output.trim().split('\n')) {
73
+ const pid = parseInt(line.trim(), 10);
74
+ if (pid && pid !== process.pid) pids.push(pid);
75
+ }
76
+ }
77
+ } catch { /* ignore errors */ }
78
+ return pids;
79
+ }
80
+
81
+ /**
82
+ * Whether the socket path needs fs.unlinkSync before server.listen().
83
+ * Named Pipes on Windows are kernel-managed — no file to unlink.
84
+ */
85
+ function needsSocketCleanup() {
86
+ return !IS_WIN;
87
+ }
88
+
89
+ /**
90
+ * GBK-safe icon mapping for Windows terminals.
91
+ * Chinese Windows terminals use GBK encoding by default,
92
+ * which cannot render emoji — replace with ASCII equivalents.
93
+ */
94
+ const _icons = IS_WIN ? {
95
+ // status
96
+ ok: '[OK]',
97
+ fail: '[FAIL]',
98
+ warn: '[!]',
99
+ info: '[i]',
100
+ // objects
101
+ pkg: '[PKG]',
102
+ brain: '[*]',
103
+ dna: '[~]',
104
+ new: '[NEW]',
105
+ magic: '[>]',
106
+ bot: '[BOT]',
107
+ green: '[ON]',
108
+ red: '[OFF]',
109
+ hook: '[HOOK]',
110
+ search: '[?]',
111
+ mirror: '[M]',
112
+ plug: '[PLUG]',
113
+ book: '[DOC]',
114
+ pin: '[PIN]',
115
+ arrow: '->',
116
+ down: '[DL]',
117
+ reload: '[R]',
118
+ stop: '[X]',
119
+ chart: '[#]',
120
+ thought: '[.]',
121
+ phone: '[TEL]',
122
+ feishu: '[FS]',
123
+ check: '[v]',
124
+ } : {
125
+ ok: '\u2705', // ✅
126
+ fail: '\u274C', // ❌
127
+ warn: '\u26A0\uFE0F', // ⚠️
128
+ info: '\u2139\uFE0F', // ℹ️
129
+ pkg: '\uD83D\uDCE6', // 📦
130
+ brain: '\uD83E\uDDE0', // 🧠
131
+ dna: '\uD83E\uDDEC', // 🧬
132
+ new: '\uD83C\uDD95', // 🆕
133
+ magic: '\uD83D\uDD2E', // 🔮
134
+ bot: '\uD83E\uDD16', // 🤖
135
+ green: '\uD83D\uDFE2', // 🟢
136
+ red: '\uD83D\uDD34', // 🔴
137
+ hook: '\uD83E\uDE9D', // 🪝
138
+ search: '\uD83D\uDD0D', // 🔍
139
+ mirror: '\uD83E\uDE9E', // 🪞
140
+ plug: '\uD83D\uDD0C', // 🔌
141
+ book: '\uD83D\uDCD6', // 📖
142
+ pin: '\uD83D\uDCCC', // 📌
143
+ arrow: '\u2192', // →
144
+ down: '\u2B07\uFE0F', // ⬇️
145
+ reload: '\uD83D\uDD04', // 🔄
146
+ stop: '\uD83D\uDEAB', // 🚫
147
+ chart: '\uD83D\uDCCA', // 📊
148
+ thought: '\uD83D\uDCAD', // 💭
149
+ phone: '\uD83D\uDCF1', // 📱
150
+ feishu: '\uD83D\uDCD8', // 📘
151
+ check: '\u2714', // ✔
152
+ };
153
+
154
+ /**
155
+ * Get a platform-appropriate icon by name.
156
+ * Usage: icon('ok') → '✅' on macOS, '[OK]' on Windows
157
+ */
158
+ function icon(name) {
159
+ return _icons[name] || name;
160
+ }
161
+
162
+ module.exports = {
163
+ IS_WIN,
164
+ IS_MAC,
165
+ IS_LINUX,
166
+ HOME,
167
+ socketPath,
168
+ sleepSync,
169
+ findProcessesByPattern,
170
+ needsSocketCleanup,
171
+ icon,
172
+ };
@@ -13,10 +13,17 @@ function mkHome(prefix = 'metame-reliability-') {
13
13
  return home;
14
14
  }
15
15
 
16
+ function homeEnv(home) {
17
+ // On Windows, os.homedir() reads USERPROFILE, not HOME
18
+ return process.platform === 'win32'
19
+ ? { HOME: home, USERPROFILE: home }
20
+ : { HOME: home };
21
+ }
22
+
16
23
  function runNode(home, code, extraEnv = {}) {
17
24
  return execFileSync(process.execPath, ['-e', code], {
18
25
  cwd: ROOT,
19
- env: { ...process.env, HOME: home, ...extraEnv },
26
+ env: { ...process.env, ...homeEnv(home), ...extraEnv },
20
27
  encoding: 'utf8',
21
28
  timeout: 30000,
22
29
  });
@@ -25,17 +32,22 @@ function runNode(home, code, extraEnv = {}) {
25
32
  function installFakeClaude(home, body) {
26
33
  const bin = path.join(home, 'bin');
27
34
  fs.mkdirSync(bin, { recursive: true });
35
+ if (process.platform === 'win32') {
36
+ const cli = path.join(bin, 'claude.cmd');
37
+ fs.writeFileSync(cli, `@echo off\n${body}\n`, 'utf8');
38
+ return { ...homeEnv(home), PATH: `${bin};${process.env.PATH}` };
39
+ }
28
40
  const cli = path.join(bin, 'claude');
29
41
  fs.writeFileSync(cli, `#!/bin/sh\n${body}\n`, 'utf8');
30
42
  fs.chmodSync(cli, 0o755);
31
- return { PATH: `${bin}:${process.env.PATH}` };
43
+ return { ...homeEnv(home), PATH: `${bin}:${process.env.PATH}` };
32
44
  }
33
45
 
34
46
  function sendSignal(home, prompt, extraEnv = {}) {
35
47
  return new Promise((resolve, reject) => {
36
48
  const child = spawn(process.execPath, [path.join(ROOT, 'scripts', 'signal-capture.js')], {
37
49
  cwd: ROOT,
38
- env: { ...process.env, HOME: home, ...extraEnv },
50
+ env: { ...process.env, ...homeEnv(home), ...extraEnv },
39
51
  stdio: ['pipe', 'ignore', 'ignore'],
40
52
  });
41
53
  const timer = setTimeout(() => {
package/scripts/schema.js CHANGED
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * Tiers:
11
11
  * T1 — Identity (LOCKED, never auto-modify)
12
- * T2 — Core Values (LOCKED, deep personality)
12
+ * T2 — Soul (LOCKED, 6-dimension personality model)
13
13
  * T3 — Preferences (auto-writable, needs confidence)
14
14
  * T5 — Evolution (system-managed, strict limits)
15
15
  *
@@ -23,14 +23,47 @@ const SCHEMA = {
23
23
  'identity.role': { tier: 'T1', type: 'string', locked: false },
24
24
  'identity.locale': { tier: 'T1', type: 'string', locked: true },
25
25
 
26
- // === T2: Core Values / Traits ===
27
- 'core_values.*': { tier: 'T2', type: 'string', locked: true },
28
- 'core_traits.crisis_reflex': { tier: 'T2', type: 'enum', locked: true, values: ['Action', 'Analysis', 'Delegation', 'Freeze'] },
29
- 'core_traits.flow_trigger': { tier: 'T2', type: 'enum', locked: true, values: ['Ideation', 'Execution', 'Teaching', 'Debugging'] },
30
- 'core_traits.shadow_self': { tier: 'T2', type: 'string', locked: true, maxChars: 80 },
31
- 'core_traits.learning_style': { tier: 'T2', type: 'enum', locked: true, values: ['Hands-on', 'Conceptual', 'Social', 'Reflective'] },
32
- 'core_traits.north_star.aspiration': { tier: 'T2', type: 'string', locked: true, maxChars: 80 },
33
- 'core_traits.north_star.realistic': { tier: 'T2', type: 'string', locked: true, maxChars: 80 },
26
+ // === T2: Soul (6-Dimension Model, LOCKED) ===
27
+
28
+ // Dim 1: Values (Schwartz Value Theory)
29
+ 'soul.values.primary': { tier: 'T2', type: 'string', locked: true, maxChars: 40 },
30
+ 'soul.values.secondary': { tier: 'T2', type: 'string', locked: true, maxChars: 40 },
31
+ 'soul.values.anti_value': { tier: 'T2', type: 'string', locked: true, maxChars: 40 },
32
+
33
+ // Dim 2: Drive (Self-Determination Theory)
34
+ 'soul.drive.primary_need': { tier: 'T2', type: 'enum', locked: true,
35
+ values: ['autonomy', 'mastery', 'connection', 'impact', 'security', 'novelty', 'meaning'] },
36
+ 'soul.drive.flow_trigger': { tier: 'T2', type: 'string', locked: true, maxChars: 60 },
37
+ 'soul.drive.north_star.aspiration': { tier: 'T2', type: 'string', locked: true, maxChars: 80 },
38
+ 'soul.drive.north_star.realistic': { tier: 'T2', type: 'string', locked: true, maxChars: 80 },
39
+
40
+ // Dim 3: Cognition Style (Jung + Kahneman)
41
+ 'soul.cognition_style.thinking_axis': { tier: 'T2', type: 'enum', locked: true,
42
+ values: ['systematic', 'intuitive', 'dialectical'] },
43
+ 'soul.cognition_style.learning_mode': { tier: 'T2', type: 'enum', locked: true,
44
+ values: ['by_doing', 'by_modeling', 'by_abstracting', 'by_debating', 'by_reflecting'] },
45
+ 'soul.cognition_style.complexity_appetite': { tier: 'T2', type: 'enum', locked: true,
46
+ values: ['reductionist', 'comfortable_with_ambiguity', 'complexity_seeker'] },
47
+
48
+ // Dim 4: Stress & Shadow (Jung Shadow + Resilience Theory)
49
+ 'soul.stress.crisis_reflex': { tier: 'T2', type: 'enum', locked: true,
50
+ values: ['fight', 'flight', 'freeze', 'analyze'] },
51
+ 'soul.stress.shadow': { tier: 'T2', type: 'string', locked: true, maxChars: 80 },
52
+ 'soul.stress.recovery_pattern': { tier: 'T2', type: 'enum', locked: true,
53
+ values: ['solitude', 'social_support', 'physical_action', 'intellectual_distraction', 'sleep_reset'] },
54
+
55
+ // Dim 5: Relational (Attachment Theory + FIRO-B)
56
+ 'soul.relational.trust_formation': { tier: 'T2', type: 'enum', locked: true,
57
+ values: ['competence_first', 'character_first', 'shared_experience', 'slow_incremental'] },
58
+ 'soul.relational.conflict_style': { tier: 'T2', type: 'enum', locked: true,
59
+ values: ['direct_confrontation', 'strategic_avoidance', 'diplomatic_mediation', 'withdrawal'] },
60
+ 'soul.relational.authority_stance': { tier: 'T2', type: 'enum', locked: true,
61
+ values: ['challenge_authority', 'respect_hierarchy', 'pragmatic_compliance', 'build_own_authority'] },
62
+
63
+ // Dim 6: Identity Narrative (McAdams Narrative Identity)
64
+ 'soul.identity_narrative.self_in_one_line': { tier: 'T2', type: 'string', locked: true, maxChars: 100 },
65
+ 'soul.identity_narrative.core_contradiction': { tier: 'T2', type: 'string', locked: true, maxChars: 80 },
66
+ 'soul.identity_narrative.feared_self': { tier: 'T2', type: 'string', locked: true, maxChars: 60 },
34
67
 
35
68
  // === T3: Preferences ===
36
69
  'preferences.code_style': { tier: 'T3', type: 'enum', values: ['concise', 'verbose', 'documented'] },
@@ -80,7 +113,7 @@ const SCHEMA = {
80
113
 
81
114
  /**
82
115
  * Check if a dotted key matches the schema.
83
- * Supports wildcard entries like 'core_values.*'
116
+ * Supports wildcard entries (e.g. 'namespace.*') and exact dotted keys.
84
117
  */
85
118
  function hasKey(key) {
86
119
  if (SCHEMA[key]) return true;
@@ -12,10 +12,16 @@ function mkHome() {
12
12
  return fs.mkdtempSync(path.join(os.tmpdir(), 'metame-skill-evo-'));
13
13
  }
14
14
 
15
+ function homeEnv(home) {
16
+ return process.platform === 'win32'
17
+ ? { HOME: home, USERPROFILE: home }
18
+ : { HOME: home };
19
+ }
20
+
15
21
  function runWithHome(home, code) {
16
22
  return execFileSync(process.execPath, ['-e', code], {
17
23
  cwd: ROOT,
18
- env: { ...process.env, HOME: home },
24
+ env: { ...process.env, ...homeEnv(home) },
19
25
  encoding: 'utf8',
20
26
  });
21
27
  }
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * sync-readme.js — Translate README.md (English) → README中文版.md
6
+ *
7
+ * Usage: node scripts/sync-readme.js
8
+ * Or: npm run sync:readme
9
+ *
10
+ * Uses claude CLI to translate. English README is the source of truth.
11
+ */
12
+
13
+ const { execSync } = require('child_process');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const ROOT = path.resolve(__dirname, '..');
18
+ const SRC = path.join(ROOT, 'README.md');
19
+ const DST = path.join(ROOT, 'README中文版.md');
20
+
21
+ const english = fs.readFileSync(SRC, 'utf8');
22
+
23
+ const prompt = `You are a professional translator. Translate the following GitHub README from English to Chinese (简体中文).
24
+
25
+ Rules:
26
+ - Keep ALL markdown formatting, links, code blocks, HTML tags, and badges EXACTLY as-is
27
+ - Keep all technical terms, CLI commands, file paths, and config examples in English
28
+ - Translate prose, descriptions, and comments naturally — not word-by-word
29
+ - The first tagline should be: > **住在你电脑里的数字分身。**
30
+ - Change "Your machine, your data" to "不上云。你的机器,你的数据。"
31
+ - Keep the <p align="center"> header block unchanged
32
+ - Output ONLY the translated markdown, no extra explanation
33
+
34
+ Here is the README to translate:
35
+
36
+ ${english}`;
37
+
38
+ console.log('Translating README.md → README中文版.md ...');
39
+
40
+ try {
41
+ const result = execSync(
42
+ `claude -p --model haiku --output-format text`,
43
+ {
44
+ input: prompt,
45
+ encoding: 'utf8',
46
+ maxBuffer: 1024 * 1024,
47
+ timeout: 120000,
48
+ cwd: ROOT,
49
+ }
50
+ );
51
+
52
+ const translated = result.trim();
53
+
54
+ if (translated.length < 500) {
55
+ console.error('Translation too short, likely failed. Aborting.');
56
+ process.exit(1);
57
+ }
58
+
59
+ fs.writeFileSync(DST, translated + '\n', 'utf8');
60
+ console.log(`✅ README中文版.md updated (${translated.length} chars)`);
61
+ } catch (e) {
62
+ console.error('Translation failed:', e.message);
63
+ process.exit(1);
64
+ }