metame-cli 1.3.0 โ 1.3.1
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 +35 -0
- package/package.json +1 -1
- package/scripts/daemon.js +117 -4
package/README.md
CHANGED
|
@@ -22,7 +22,9 @@ It is not a memory system; it is a **Cognitive Mirror** .
|
|
|
22
22
|
* **๐งฌ Cognitive Evolution Engine:** MetaMe learns how you think through three channels: (1) **Passive** โ silently captures your messages and distills cognitive traits via Haiku on next launch; (2) **Manual** โ `!metame evolve` for explicit teaching; (3) **Confidence gates** โ strong directives ("always"/"ไปฅๅไธๅพ") write immediately, normal observations need 3+ consistent sightings before promotion. Schema-enforced (41 fields, 5 tiers, 800 token budget) to prevent bloat.
|
|
23
23
|
* **๐ค Dynamic Handshake:** The "Canary Test." Claude must address you by your **Codename** in the first sentence. If it doesn't, the link is broken.
|
|
24
24
|
* **๐ก๏ธ Auto-Lock:** Mark any value with `# [LOCKED]` โ treated as a constitution, never auto-modified.
|
|
25
|
+
* **๐ช Metacognition Layer (v1.3):** MetaMe now observes *how* you think, not just *what* you say. Behavioral pattern detection runs inside the existing Haiku distill call (zero extra cost). It tracks decision patterns, cognitive load, comfort zones, and avoidance topics across sessions. When persistent patterns emerge, MetaMe injects a one-line mirror observation โ e.g., *"You tend to avoid testing until forced"* โ with a 14-day cooldown per pattern. Conditional reflection prompts appear only when triggered (every 7th distill or 3x consecutive comfort zone). All injection logic runs in Node.js; Claude receives only pre-decided directives, never rules to self-evaluate.
|
|
25
26
|
* **๐ฑ Remote Claude Code (v1.3):** Full Claude Code from your phone via Telegram or Feishu (Lark). Stateful sessions with `--resume` โ same conversation history, tool use, and file editing as your terminal. Interactive buttons for project/session picking, directory browser, and macOS launchd auto-start.
|
|
27
|
+
* **๐ Workflow Engine (v1.3):** Define multi-step skill chains as heartbeat tasks. Each workflow runs in a single Claude Code session via `--resume`, so step outputs flow as context to the next step. Example: `deep-research` โ `tech-writing` โ `wechat-publisher` โ fully automated content pipeline.
|
|
26
28
|
|
|
27
29
|
## ๐ Prerequisites
|
|
28
30
|
|
|
@@ -107,6 +109,14 @@ metame evolve "I prefer functional programming patterns"
|
|
|
107
109
|
|
|
108
110
|
**Anti-bias safeguards:** single observations โ traits, contradictions are tracked not overwritten, pending traits expire after 30 days, context fields auto-clear on staleness.
|
|
109
111
|
|
|
112
|
+
**Metacognition controls:**
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
metame quiet # Silence mirror observations & reflections for 48h
|
|
116
|
+
metame insights # Show detected behavioral patterns
|
|
117
|
+
metame mirror on|off # Toggle mirror injection
|
|
118
|
+
```
|
|
119
|
+
|
|
110
120
|
### Remote Claude Code โ Telegram & Feishu (v1.3)
|
|
111
121
|
|
|
112
122
|
Full Claude Code from your phone โ stateful sessions with conversation history, tool use, and file editing. Supports both Telegram and Feishu (Lark).
|
|
@@ -168,6 +178,7 @@ Each chat gets a persistent session via `claude -p --resume <session-id>`. This
|
|
|
168
178
|
| `/run <name>` | Run a task immediately |
|
|
169
179
|
| `/budget` | Today's token usage |
|
|
170
180
|
| `/quiet` | Silence mirror/reflections for 48h |
|
|
181
|
+
| `/reload` | Manually reload daemon.yaml (also auto-reloads on file change) |
|
|
171
182
|
|
|
172
183
|
**Heartbeat Tasks:**
|
|
173
184
|
|
|
@@ -188,6 +199,30 @@ heartbeat:
|
|
|
188
199
|
* `type: "script"`: Run a local script directly instead of `claude -p`.
|
|
189
200
|
* `notify: true`: Push results to Telegram/Feishu.
|
|
190
201
|
|
|
202
|
+
**Workflow tasks** (multi-step skill chains):
|
|
203
|
+
|
|
204
|
+
```yaml
|
|
205
|
+
heartbeat:
|
|
206
|
+
tasks:
|
|
207
|
+
- name: "daily-wechat"
|
|
208
|
+
type: "workflow"
|
|
209
|
+
interval: "24h"
|
|
210
|
+
model: "sonnet"
|
|
211
|
+
notify: true
|
|
212
|
+
steps:
|
|
213
|
+
- skill: "deep-research"
|
|
214
|
+
prompt: "Today's top 3 AI news stories"
|
|
215
|
+
- skill: "tech-writing"
|
|
216
|
+
prompt: "Write a WeChat article based on the research above"
|
|
217
|
+
- skill: "wechat-publisher"
|
|
218
|
+
prompt: "Publish the article"
|
|
219
|
+
optional: true
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Each step runs in the same Claude Code session. Step outputs automatically become context for the next step. Set `optional: true` on steps that may fail without aborting the workflow.
|
|
223
|
+
|
|
224
|
+
**Auto-reload:** The daemon watches `daemon.yaml` for changes. When Claude (or you) edits the config file, the daemon automatically reloads โ no restart or `/reload` needed. A notification is pushed to confirm.
|
|
225
|
+
|
|
191
226
|
**Token efficiency:**
|
|
192
227
|
|
|
193
228
|
* Polling, slash commands, directory browsing: **zero tokens**
|
package/package.json
CHANGED
package/scripts/daemon.js
CHANGED
|
@@ -211,6 +211,11 @@ function executeTask(task, config) {
|
|
|
211
211
|
return { success: true, output: '(skipped โ no activity)', skipped: true };
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
// Workflow tasks: multi-step skill chain via --resume session
|
|
215
|
+
if (task.type === 'workflow') {
|
|
216
|
+
return executeWorkflow(task, config);
|
|
217
|
+
}
|
|
218
|
+
|
|
214
219
|
// Script tasks: run a local script directly (e.g. distill.js), no claude -p
|
|
215
220
|
if (task.type === 'script') {
|
|
216
221
|
log('INFO', `Executing script task: ${task.name} โ ${task.command}`);
|
|
@@ -307,6 +312,68 @@ function parseInterval(str) {
|
|
|
307
312
|
}
|
|
308
313
|
}
|
|
309
314
|
|
|
315
|
+
// ---------------------------------------------------------
|
|
316
|
+
// WORKFLOW EXECUTION (multi-step skill chain via --resume)
|
|
317
|
+
// ---------------------------------------------------------
|
|
318
|
+
function executeWorkflow(task, config) {
|
|
319
|
+
const state = loadState();
|
|
320
|
+
if (!checkBudget(config, state)) {
|
|
321
|
+
log('WARN', `Budget exceeded, skipping workflow: ${task.name}`);
|
|
322
|
+
return { success: false, error: 'budget_exceeded', output: '' };
|
|
323
|
+
}
|
|
324
|
+
const precheck = checkPrecondition(task);
|
|
325
|
+
if (!precheck.pass) {
|
|
326
|
+
state.tasks[task.name] = { last_run: new Date().toISOString(), status: 'skipped', output_preview: 'Precondition not met' };
|
|
327
|
+
saveState(state);
|
|
328
|
+
return { success: true, output: '(skipped)', skipped: true };
|
|
329
|
+
}
|
|
330
|
+
const steps = task.steps || [];
|
|
331
|
+
if (steps.length === 0) return { success: false, error: 'No steps defined', output: '' };
|
|
332
|
+
|
|
333
|
+
const model = task.model || 'sonnet';
|
|
334
|
+
const cwd = task.cwd ? task.cwd.replace(/^~/, HOME) : HOME;
|
|
335
|
+
const sessionId = crypto.randomUUID();
|
|
336
|
+
const outputs = [];
|
|
337
|
+
let totalTokens = 0;
|
|
338
|
+
|
|
339
|
+
log('INFO', `Workflow ${task.name}: ${steps.length} steps, session ${sessionId.slice(0, 8)}`);
|
|
340
|
+
|
|
341
|
+
for (let i = 0; i < steps.length; i++) {
|
|
342
|
+
const step = steps[i];
|
|
343
|
+
let prompt = (step.skill ? `/${step.skill} ` : '') + (step.prompt || '');
|
|
344
|
+
if (i === 0 && precheck.context) prompt += `\n\n็ธๅ
ณๆฐๆฎ:\n\`\`\`\n${precheck.context}\n\`\`\``;
|
|
345
|
+
const args = ['-p', '--model', model];
|
|
346
|
+
args.push(i === 0 ? '--session-id' : '--resume', sessionId);
|
|
347
|
+
|
|
348
|
+
log('INFO', `Workflow ${task.name} step ${i + 1}/${steps.length}: ${step.skill || 'prompt'}`);
|
|
349
|
+
try {
|
|
350
|
+
const output = execSync(`claude ${args.join(' ')}`, {
|
|
351
|
+
input: prompt, encoding: 'utf8', timeout: step.timeout || 300000, maxBuffer: 5 * 1024 * 1024, cwd,
|
|
352
|
+
}).trim();
|
|
353
|
+
const tk = Math.ceil((prompt.length + output.length) / 4);
|
|
354
|
+
totalTokens += tk;
|
|
355
|
+
outputs.push({ step: i + 1, skill: step.skill || null, output: output.slice(0, 500), tokens: tk });
|
|
356
|
+
log('INFO', `Workflow ${task.name} step ${i + 1} done (${tk} tokens)`);
|
|
357
|
+
if (!checkBudget(config, loadState())) { log('WARN', 'Budget exceeded mid-workflow'); break; }
|
|
358
|
+
} catch (e) {
|
|
359
|
+
log('ERROR', `Workflow ${task.name} step ${i + 1} failed: ${e.message.slice(0, 200)}`);
|
|
360
|
+
outputs.push({ step: i + 1, skill: step.skill || null, error: e.message.slice(0, 200) });
|
|
361
|
+
if (!step.optional) {
|
|
362
|
+
recordTokens(loadState(), totalTokens);
|
|
363
|
+
state.tasks[task.name] = { last_run: new Date().toISOString(), status: 'error', error: `Step ${i + 1} failed`, steps_completed: i, steps_total: steps.length };
|
|
364
|
+
saveState(state);
|
|
365
|
+
return { success: false, error: `Step ${i + 1} failed`, output: outputs.map(o => `Step ${o.step}: ${o.error ? 'FAILED' : 'OK'}`).join('\n'), tokens: totalTokens };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
recordTokens(loadState(), totalTokens);
|
|
370
|
+
const lastOk = [...outputs].reverse().find(o => !o.error);
|
|
371
|
+
state.tasks[task.name] = { last_run: new Date().toISOString(), status: 'success', output_preview: (lastOk ? lastOk.output : '').slice(0, 200), steps_completed: outputs.filter(o => !o.error).length, steps_total: steps.length };
|
|
372
|
+
saveState(state);
|
|
373
|
+
log('INFO', `Workflow ${task.name} done: ${outputs.filter(o => !o.error).length}/${steps.length} steps (${totalTokens} tokens)`);
|
|
374
|
+
return { success: true, output: outputs.map(o => `Step ${o.step} (${o.skill || 'prompt'}): ${o.error ? 'FAILED' : 'OK'}`).join('\n') + '\n\n' + (lastOk ? lastOk.output : ''), tokens: totalTokens };
|
|
375
|
+
}
|
|
376
|
+
|
|
310
377
|
// ---------------------------------------------------------
|
|
311
378
|
// HEARTBEAT SCHEDULER
|
|
312
379
|
// ---------------------------------------------------------
|
|
@@ -709,6 +776,20 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
709
776
|
return;
|
|
710
777
|
}
|
|
711
778
|
|
|
779
|
+
if (text === '/reload') {
|
|
780
|
+
if (global._metameReload) {
|
|
781
|
+
const r = global._metameReload();
|
|
782
|
+
if (r.success) {
|
|
783
|
+
await bot.sendMessage(chatId, `โ
Config reloaded. ${r.tasks} heartbeat tasks active.`);
|
|
784
|
+
} else {
|
|
785
|
+
await bot.sendMessage(chatId, `โ Reload failed: ${r.error}`);
|
|
786
|
+
}
|
|
787
|
+
} else {
|
|
788
|
+
await bot.sendMessage(chatId, 'โ Reload not available (daemon not fully started).');
|
|
789
|
+
}
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
712
793
|
if (text.startsWith('/')) {
|
|
713
794
|
await bot.sendMessage(chatId, [
|
|
714
795
|
'Commands:',
|
|
@@ -717,7 +798,7 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
|
|
|
717
798
|
'/resume <id> โ resume specific session',
|
|
718
799
|
'/cd <path> โ change workdir',
|
|
719
800
|
'/session โ current session info',
|
|
720
|
-
'/status /tasks /run /budget /quiet',
|
|
801
|
+
'/status /tasks /run /budget /quiet /reload',
|
|
721
802
|
'',
|
|
722
803
|
'Or just type naturally.',
|
|
723
804
|
].join('\n'));
|
|
@@ -974,7 +1055,7 @@ function sleep(ms) {
|
|
|
974
1055
|
// MAIN
|
|
975
1056
|
// ---------------------------------------------------------
|
|
976
1057
|
async function main() {
|
|
977
|
-
|
|
1058
|
+
let config = loadConfig();
|
|
978
1059
|
if (!config || Object.keys(config).length === 0) {
|
|
979
1060
|
console.error('No daemon config found. Run: metame daemon init');
|
|
980
1061
|
process.exit(1);
|
|
@@ -988,7 +1069,7 @@ async function main() {
|
|
|
988
1069
|
|
|
989
1070
|
log('INFO', `MetaMe daemon started (PID: ${process.pid})`);
|
|
990
1071
|
|
|
991
|
-
// Task executor lookup
|
|
1072
|
+
// Task executor lookup (always reads fresh config)
|
|
992
1073
|
function executeTaskByName(name) {
|
|
993
1074
|
const tasks = (config.heartbeat && config.heartbeat.tasks) || [];
|
|
994
1075
|
const task = tasks.find(t => t.name === name);
|
|
@@ -1021,7 +1102,38 @@ async function main() {
|
|
|
1021
1102
|
};
|
|
1022
1103
|
|
|
1023
1104
|
// Start heartbeat scheduler
|
|
1024
|
-
|
|
1105
|
+
let heartbeatTimer = startHeartbeat(config, notifyFn);
|
|
1106
|
+
|
|
1107
|
+
// Hot reload: re-read config and restart heartbeat scheduler
|
|
1108
|
+
function reloadConfig() {
|
|
1109
|
+
const newConfig = loadConfig();
|
|
1110
|
+
if (!newConfig) return { success: false, error: 'Failed to read config' };
|
|
1111
|
+
config = newConfig;
|
|
1112
|
+
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
1113
|
+
heartbeatTimer = startHeartbeat(config, notifyFn);
|
|
1114
|
+
log('INFO', `Config reloaded: ${(config.heartbeat && config.heartbeat.tasks || []).length} tasks`);
|
|
1115
|
+
return { success: true, tasks: (config.heartbeat && config.heartbeat.tasks || []).length };
|
|
1116
|
+
}
|
|
1117
|
+
// Expose reloadConfig to handleCommand via closure
|
|
1118
|
+
global._metameReload = reloadConfig;
|
|
1119
|
+
|
|
1120
|
+
// Auto-reload: watch daemon.yaml for changes (e.g. Claude edits it via askClaude)
|
|
1121
|
+
let _reloadDebounce = null;
|
|
1122
|
+
fs.watchFile(CONFIG_FILE, { interval: 2000 }, (curr, prev) => {
|
|
1123
|
+
if (curr.mtimeMs === prev.mtimeMs) return;
|
|
1124
|
+
// Debounce: wait 1s for file write to finish
|
|
1125
|
+
if (_reloadDebounce) clearTimeout(_reloadDebounce);
|
|
1126
|
+
_reloadDebounce = setTimeout(() => {
|
|
1127
|
+
log('INFO', 'daemon.yaml changed on disk โ auto-reloading config');
|
|
1128
|
+
const r = reloadConfig();
|
|
1129
|
+
if (r.success) {
|
|
1130
|
+
log('INFO', `Auto-reload OK: ${r.tasks} tasks`);
|
|
1131
|
+
notifyFn(`๐ Config auto-reloaded. ${r.tasks} heartbeat tasks active.`).catch(() => {});
|
|
1132
|
+
} else {
|
|
1133
|
+
log('ERROR', `Auto-reload failed: ${r.error}`);
|
|
1134
|
+
}
|
|
1135
|
+
}, 1000);
|
|
1136
|
+
});
|
|
1025
1137
|
|
|
1026
1138
|
// Start bridges (both can run simultaneously)
|
|
1027
1139
|
telegramBridge = await startTelegramBridge(config, executeTaskByName);
|
|
@@ -1030,6 +1142,7 @@ async function main() {
|
|
|
1030
1142
|
// Graceful shutdown
|
|
1031
1143
|
const shutdown = () => {
|
|
1032
1144
|
log('INFO', 'Daemon shutting down...');
|
|
1145
|
+
fs.unwatchFile(CONFIG_FILE);
|
|
1033
1146
|
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
|
1034
1147
|
if (telegramBridge) telegramBridge.stop();
|
|
1035
1148
|
if (feishuBridge) feishuBridge.stop();
|