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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
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": {
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
- const config = loadConfig();
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
- const heartbeatTimer = startHeartbeat(config, notifyFn);
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();