metame-cli 1.4.31 → 1.4.33
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 +42 -19
- package/index.js +237 -149
- package/package.json +3 -2
- package/scripts/bin/dispatch_to +92 -0
- package/scripts/daemon-claude-engine.js +5 -2
- package/scripts/daemon-runtime-lifecycle.js +4 -2
- package/scripts/daemon-task-scheduler.js +28 -3
- package/scripts/daemon.js +40 -3
- package/scripts/docs/agent-guide.md +50 -0
- package/scripts/docs/file-transfer.md +32 -0
- package/scripts/feishu-adapter.js +6 -2
- package/scripts/memory-extract.js +2 -2
- package/scripts/memory-gc.js +2 -2
- package/scripts/memory-write.js +0 -1
- package/scripts/memory.js +1 -1
- package/scripts/platform.js +172 -0
- package/scripts/reliability-core.test.js +15 -3
- package/scripts/schema.js +43 -10
- package/scripts/skill-evolution.test.js +7 -1
- package/scripts/sync-readme.js +64 -0
- package/scripts/templates/default-global-claude.md +19 -0
- package/scripts/utils.test.js +12 -5
- package/scripts/migrate-v2.js +0 -112
- package/scripts/self-reflect.js +0 -285
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metame-cli",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.33",
|
|
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
|
},
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dispatch_to [--new] <project_key> "<prompt>"
|
|
4
|
+
* Tries Unix socket / Named Pipe first (low-latency), falls back to pending.jsonl.
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const net = require('net');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const { socketPath } = require('../platform');
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const newSession = args[0] === '--new' ? (args.shift(), true) : false;
|
|
16
|
+
const [target, ...rest] = args;
|
|
17
|
+
const prompt = rest.join(' ').replace(/^["']|["']$/g, '');
|
|
18
|
+
if (!target || !prompt) {
|
|
19
|
+
console.error('Usage: dispatch_to [--new] <project_key> "<prompt>"');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const METAME_DIR = path.join(os.homedir(), '.metame');
|
|
24
|
+
const DISPATCH_DIR = path.join(METAME_DIR, 'dispatch');
|
|
25
|
+
const PENDING = path.join(DISPATCH_DIR, 'pending.jsonl');
|
|
26
|
+
const DISPATCH_SECRET_FILE = path.join(METAME_DIR, '.dispatch_secret');
|
|
27
|
+
const SOCK_PATH = socketPath(METAME_DIR);
|
|
28
|
+
|
|
29
|
+
function getDispatchSecret() {
|
|
30
|
+
try {
|
|
31
|
+
if (fs.existsSync(DISPATCH_SECRET_FILE)) {
|
|
32
|
+
return fs.readFileSync(DISPATCH_SECRET_FILE, 'utf8').trim();
|
|
33
|
+
}
|
|
34
|
+
} catch { /* fall through to generate */ }
|
|
35
|
+
const secret = crypto.randomBytes(32).toString('hex');
|
|
36
|
+
try {
|
|
37
|
+
fs.writeFileSync(DISPATCH_SECRET_FILE, secret, { mode: 0o600 });
|
|
38
|
+
} catch { /* ignore write errors */ }
|
|
39
|
+
return secret;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const ts = new Date().toISOString();
|
|
43
|
+
const secret = getDispatchSecret();
|
|
44
|
+
const sigPayload = JSON.stringify({ target, prompt, ts });
|
|
45
|
+
const sig = crypto.createHmac('sha256', secret).update(sigPayload).digest('hex');
|
|
46
|
+
|
|
47
|
+
const msg = { target, prompt, from: '_claude_session', new_session: newSession, created_at: ts, ts, sig };
|
|
48
|
+
|
|
49
|
+
function fallbackToFile() {
|
|
50
|
+
fs.mkdirSync(DISPATCH_DIR, { recursive: true });
|
|
51
|
+
fs.appendFileSync(PENDING, JSON.stringify(msg) + '\n');
|
|
52
|
+
console.log(`DISPATCH_OK(file): ${target} → ${prompt.slice(0, 60)}${newSession ? ' [new session]' : ''}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const sock = net.createConnection({ path: SOCK_PATH });
|
|
56
|
+
let done = false;
|
|
57
|
+
|
|
58
|
+
const timer = setTimeout(() => {
|
|
59
|
+
if (done) return;
|
|
60
|
+
done = true;
|
|
61
|
+
sock.destroy();
|
|
62
|
+
fallbackToFile();
|
|
63
|
+
}, 2000);
|
|
64
|
+
|
|
65
|
+
sock.on('connect', () => {
|
|
66
|
+
sock.write(JSON.stringify(msg));
|
|
67
|
+
sock.end();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
sock.on('data', (data) => {
|
|
71
|
+
if (done) return;
|
|
72
|
+
done = true;
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
try {
|
|
75
|
+
const res = JSON.parse(data.toString().trim());
|
|
76
|
+
if (res.ok) {
|
|
77
|
+
console.log(`DISPATCH_OK(socket): ${target} → ${prompt.slice(0, 60)}${newSession ? ' [new session]' : ''}`);
|
|
78
|
+
} else {
|
|
79
|
+
fallbackToFile();
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
fallbackToFile();
|
|
83
|
+
}
|
|
84
|
+
sock.destroy();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
sock.on('error', () => {
|
|
88
|
+
if (done) return;
|
|
89
|
+
done = true;
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
fallbackToFile();
|
|
92
|
+
});
|
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Agent 创建与管理指南
|
|
2
|
+
|
|
3
|
+
## 创建 Agent(完整流程)
|
|
4
|
+
|
|
5
|
+
用户说"创建agent"、"新建agent"、"帮我建个agent"时,按此流程引导:
|
|
6
|
+
|
|
7
|
+
### Step 1: 收集信息
|
|
8
|
+
需要两个必要信息:
|
|
9
|
+
- **工作目录**:Agent 的代码/项目目录(如 `~/projects/my-bot`)
|
|
10
|
+
- **角色描述**(可选):Agent 的职责定义
|
|
11
|
+
|
|
12
|
+
如果用户没给目录,提示:
|
|
13
|
+
> 请告诉我 Agent 的工作目录,例如 `~/projects/my-bot`
|
|
14
|
+
|
|
15
|
+
### Step 2: 执行创建
|
|
16
|
+
在手机端(飞书/Telegram),直接说即可,daemon 会自动处理:
|
|
17
|
+
> 创建一个 Agent,目录是 ~/projects/my-bot
|
|
18
|
+
|
|
19
|
+
在桌面 Claude Code 终端,需要手动操作:
|
|
20
|
+
1. 创建项目目录和 CLAUDE.md(角色定义)
|
|
21
|
+
2. 编辑 `~/.metame/daemon.yaml`,在 `projects` 下新增:
|
|
22
|
+
```yaml
|
|
23
|
+
projects:
|
|
24
|
+
my_bot:
|
|
25
|
+
name: "我的机器人"
|
|
26
|
+
cwd: "~/projects/my-bot"
|
|
27
|
+
icon: "🤖"
|
|
28
|
+
```
|
|
29
|
+
3. 运行 `touch ~/.metame/daemon.js` 触发热重载
|
|
30
|
+
|
|
31
|
+
### Step 3: 绑定群聊
|
|
32
|
+
告知用户:
|
|
33
|
+
> 请在飞书/Telegram 新建群组,把 bot 加进去,发送 `/activate` 完成绑定。
|
|
34
|
+
|
|
35
|
+
`/activate` 会自动将群与最近创建的 Agent 绑定(30分钟内有效)。
|
|
36
|
+
|
|
37
|
+
## 常用命令速查
|
|
38
|
+
|
|
39
|
+
| 操作 | 手机端命令 |
|
|
40
|
+
|------|-----------|
|
|
41
|
+
| 新建 Agent | `/agent new` 或自然语言"创建agent" |
|
|
42
|
+
| 绑定群 | `/activate` 或 `/agent bind <名称> [目录]` |
|
|
43
|
+
| 查看列表 | `/agent list` |
|
|
44
|
+
| 编辑角色 | `/agent edit` |
|
|
45
|
+
| 解绑群 | `/agent unbind` |
|
|
46
|
+
| 切换 Agent | 直接@昵称(仅非专属群) |
|
|
47
|
+
|
|
48
|
+
## 注意事项
|
|
49
|
+
- 专属群(chat_agent_map 中的群)永远绑定同一个 Agent,不能通过昵称切换
|
|
50
|
+
- 新群必须发 `/activate` 才能使用,未授权群会提示"此群未授权"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# 文件传输协议
|
|
2
|
+
|
|
3
|
+
## 用户要文件(发送到手机)
|
|
4
|
+
|
|
5
|
+
当用户说"把xx发给我"、"文件发过来"、"发个截图"、"导出给我"等**任何要求获取文件**的场景:
|
|
6
|
+
|
|
7
|
+
1. 找到文件路径(用 Glob/ls 搜索)
|
|
8
|
+
2. **不要读取文件内容**(浪费 token)
|
|
9
|
+
3. 在回复末尾加标记:`[[FILE:/absolute/path/to/file]]`
|
|
10
|
+
4. 多个文件用多个标记
|
|
11
|
+
|
|
12
|
+
示例回复:
|
|
13
|
+
```
|
|
14
|
+
请查收~! [[FILE:/Users/xxx/project/output.pdf]]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
多文件:
|
|
18
|
+
```
|
|
19
|
+
这是你要的文件:
|
|
20
|
+
[[FILE:/path/to/report.pdf]]
|
|
21
|
+
[[FILE:/path/to/data.xlsx]]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 用户发文件(从手机上传)
|
|
25
|
+
|
|
26
|
+
用户从手机发送的图片/文件会自动保存到当前项目的 `upload/` 目录。
|
|
27
|
+
直接用 Read 工具读取 `upload/` 下的文件即可。
|
|
28
|
+
|
|
29
|
+
## 关键规则
|
|
30
|
+
- **永远不要读取再复述文件内容**,直接用 `[[FILE:...]]` 标记发送
|
|
31
|
+
- 路径必须是绝对路径
|
|
32
|
+
- daemon 会自动解析标记并通过 bot 发送给用户
|
|
@@ -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
|
-
-
|
|
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
|
{
|
package/scripts/memory-gc.js
CHANGED
|
@@ -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 (
|
|
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 = ['
|
|
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;
|
package/scripts/memory-write.js
CHANGED
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(['
|
|
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,
|
|
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,
|
|
50
|
+
env: { ...process.env, ...homeEnv(home), ...extraEnv },
|
|
39
51
|
stdio: ['pipe', 'ignore', 'ignore'],
|
|
40
52
|
});
|
|
41
53
|
const timer = setTimeout(() => {
|