metame-cli 1.5.8 → 1.5.9

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.5.8",
3
+ "version": "1.5.9",
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": {
@@ -20,11 +20,13 @@ function createBridgeStarter(deps) {
20
20
  getSession,
21
21
  restoreSessionFromReply,
22
22
  handleCommand,
23
+ pipeline, // message pipeline for per-chatId serial execution
23
24
  pendingActivations, // optional — used to show smart activation hint
24
- activeProcesses, // optionalused for auto-dispatch to clones
25
- messageQueue, // optionalused for /stop to clear queued messages
25
+ activeProcesses: _activeProcesses, // legacynow handled by pipeline
26
+ messageQueue: _messageQueue, // legacynow handled by pipeline
26
27
  sendRemoteDispatch, // optional — send packet to remote peer via relay chat
27
28
  handleRemoteDispatchMessage, // optional — intercept relay chat messages
29
+ getOrCreateWorktree, // optional — isolated worktree per actor
28
30
  } = deps;
29
31
 
30
32
  async function sendAclReply(bot, chatId, text) {
@@ -278,19 +280,26 @@ function createBridgeStarter(deps) {
278
280
  const virtualChatId = `_agent_${member.key}`;
279
281
  const parentCwd = member.cwd || boundProj.cwd;
280
282
  const resolvedParentCwd = parentCwd.replace(/^~/, require('os').homedir());
281
- const memberCwd = _getMemberCwd(resolvedParentCwd, member.key);
283
+ const memberCwd = typeof getOrCreateWorktree === 'function'
284
+ ? getOrCreateWorktree(resolvedParentCwd, member.key)
285
+ : _getMemberCwd(resolvedParentCwd, member.key);
282
286
  if (!memberCwd) {
283
287
  log('ERROR', `Team [${member.key}] cannot start: directory unavailable`);
284
288
  bot.sendMessage(realChatId, `❌ ${member.icon || '🤖'} ${member.name} 启动失败:工作目录创建失败`).catch(() => {});
285
289
  return;
286
290
  }
287
291
  log('INFO', `Team [${member.key}] using cwd: ${memberCwd}`);
292
+ // Spawn cwd MUST be the actual work directory (worktree/member dir) so that:
293
+ // 1. Claude CLI operates in the correct directory (git, file edits)
294
+ // 2. /undo, /redo, /reset target the right repo
295
+ // Session visibility on desktop is handled by findSessionFile scanning all project dirs,
296
+ // and by session naming (auto-name with agent label prefix).
288
297
  const teamCfg = {
289
298
  ...cfg,
290
299
  projects: {
291
300
  ...(cfg.projects || {}),
292
301
  [member.key]: {
293
- cwd: memberCwd,
302
+ cwd: memberCwd, // actual work directory
294
303
  name: member.name,
295
304
  icon: member.icon || '🤖',
296
305
  color: member.color || 'blue',
@@ -299,7 +308,7 @@ function createBridgeStarter(deps) {
299
308
  },
300
309
  };
301
310
  const proxyBot = _createTeamProxyBot(bot, realChatId);
302
- handleCommand(proxyBot, virtualChatId, text, teamCfg, executeTaskByName, acl.senderId, acl.readOnly)
311
+ pipeline.processMessage(virtualChatId, text, { bot: proxyBot, config: teamCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly })
303
312
  .catch(e => log('ERROR', `Team [${member.key}] error: ${e.message}`));
304
313
  }
305
314
  // ────────────────────────────────────────────────────────────────────────
@@ -359,7 +368,7 @@ function createBridgeStarter(deps) {
359
368
  bypassAcl: !allowedIds.includes(chatId) && !!isBindCmd,
360
369
  });
361
370
  if (acl.blocked) continue;
362
- handleCommand(bot, chatId, cb.data, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
371
+ pipeline.processMessage(chatId, cb.data, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly }).catch(e => {
363
372
  log('ERROR', `Telegram callback handler error: ${e.message}`);
364
373
  });
365
374
  }
@@ -434,7 +443,7 @@ function createBridgeStarter(deps) {
434
443
  continue;
435
444
  }
436
445
  }
437
- handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
446
+ pipeline.processMessage(chatId, prompt, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly }).catch(e => {
438
447
  log('ERROR', `Telegram file handler error: ${e.message}`);
439
448
  });
440
449
  } catch (err) {
@@ -515,16 +524,9 @@ function createBridgeStarter(deps) {
515
524
  const vid = `_agent_${_targetKey}`;
516
525
  const member = _boundProj.team.find(t => t.key === _targetKey);
517
526
  const label = member ? `${member.icon || '🤖'} ${member.name}` : _targetKey;
518
- if (messageQueue.has(vid)) {
519
- const vq = messageQueue.get(vid);
520
- if (vq && vq.timer) clearTimeout(vq.timer);
521
- messageQueue.delete(vid);
522
- }
523
- const vproc = activeProcesses && activeProcesses.get(vid);
524
- if (vproc && vproc.child) {
525
- vproc.aborted = true;
526
- const sig = vproc.killSignal || 'SIGTERM';
527
- try { process.kill(-vproc.child.pid, sig); } catch { try { vproc.child.kill(sig); } catch { /* */ } }
527
+ pipeline.clearQueue(vid);
528
+ const stopped = pipeline.interruptActive(vid);
529
+ if (stopped) {
528
530
  await bot.sendMessage(chatId, `⏹ Stopping ${label}...`);
529
531
  } else {
530
532
  await bot.sendMessage(chatId, `${label} 当前没有活跃任务`);
@@ -577,7 +579,7 @@ function createBridgeStarter(deps) {
577
579
  continue;
578
580
  }
579
581
  try {
580
- await handleCommand(bot, chatId, rest, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
582
+ await pipeline.processMessage(chatId, rest, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly });
581
583
  } catch (e) {
582
584
  log('ERROR', `Team main-route handleCommand failed: ${e.message}`);
583
585
  bot.sendMessage(chatId, `❌ 执行失败: ${e.message}`).catch(() => {});
@@ -597,7 +599,7 @@ function createBridgeStarter(deps) {
597
599
  }
598
600
 
599
601
  // Default: route to main project
600
- handleCommand(bot, chatId, text, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
602
+ pipeline.processMessage(chatId, text, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly }).catch(e => {
601
603
  log('ERROR', `Telegram handler error: ${e.message}`);
602
604
  });
603
605
  }
@@ -727,7 +729,7 @@ function createBridgeStarter(deps) {
727
729
  return;
728
730
  }
729
731
  }
730
- await handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
732
+ await pipeline.processMessage(chatId, prompt, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly });
731
733
  } catch (err) {
732
734
  log('ERROR', `Feishu file download failed: ${err.message}`);
733
735
  await bot.sendMessage(chatId, `❌ Download failed: ${err.message}`);
@@ -816,17 +818,9 @@ function createBridgeStarter(deps) {
816
818
  const vid = `_agent_${_targetKey}`;
817
819
  const member = _boundProj.team.find(t => t.key === _targetKey);
818
820
  const label = member ? `${member.icon || '🤖'} ${member.name}` : _targetKey;
819
- // Clear message queue for this virtual agent
820
- if (messageQueue.has(vid)) {
821
- const vq = messageQueue.get(vid);
822
- if (vq && vq.timer) clearTimeout(vq.timer);
823
- messageQueue.delete(vid);
824
- }
825
- const vproc = activeProcesses && activeProcesses.get(vid);
826
- if (vproc && vproc.child) {
827
- vproc.aborted = true;
828
- const sig = vproc.killSignal || 'SIGTERM';
829
- try { process.kill(-vproc.child.pid, sig); } catch { try { vproc.child.kill(sig); } catch { /* */ } }
821
+ pipeline.clearQueue(vid);
822
+ const stopped = pipeline.interruptActive(vid);
823
+ if (stopped) {
830
824
  await bot.sendMessage(chatId, `⏹ Stopping ${label}...`);
831
825
  } else {
832
826
  await bot.sendMessage(chatId, `${label} 当前没有活跃任务`);
@@ -902,7 +896,7 @@ function createBridgeStarter(deps) {
902
896
  return;
903
897
  }
904
898
  try {
905
- await handleCommand(bot, chatId, rest, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
899
+ await pipeline.processMessage(chatId, rest, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly });
906
900
  } catch (e) {
907
901
  log('ERROR', `Team main-route handleCommand failed: ${e.message}`);
908
902
  bot.sendMessage(chatId, `❌ 执行失败: ${e.message}`).catch(() => {});
@@ -922,7 +916,12 @@ function createBridgeStarter(deps) {
922
916
 
923
917
  }
924
918
 
925
- await handleCommand(bot, chatId, text, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
919
+ try {
920
+ await pipeline.processMessage(chatId, text, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly });
921
+ } catch (e) {
922
+ log('ERROR', `Feishu handleCommand failed for ${chatId}: ${e.message}`);
923
+ bot.sendMessage(chatId, `❌ 命令执行失败: ${e.message}`).catch(() => {});
924
+ }
926
925
  }
927
926
  }, { log: (lvl, msg) => log(lvl, msg) });
928
927
 
@@ -1071,16 +1070,9 @@ function createBridgeStarter(deps) {
1071
1070
  const vid = `_agent_${_targetKey}`;
1072
1071
  const member = _boundProj.team.find(t => t.key === _targetKey);
1073
1072
  const label = member ? `${member.icon || '🤖'} ${member.name}` : _targetKey;
1074
- if (messageQueue.has(vid)) {
1075
- const vq = messageQueue.get(vid);
1076
- if (vq && vq.timer) clearTimeout(vq.timer);
1077
- messageQueue.delete(vid);
1078
- }
1079
- const vproc = activeProcesses && activeProcesses.get(vid);
1080
- if (vproc && vproc.child) {
1081
- vproc.aborted = true;
1082
- const sig = vproc.killSignal || 'SIGTERM';
1083
- try { process.kill(-vproc.child.pid, sig); } catch { try { vproc.child.kill(sig); } catch { /* */ } }
1073
+ pipeline.clearQueue(vid);
1074
+ const stopped = pipeline.interruptActive(vid);
1075
+ if (stopped) {
1084
1076
  await bot.sendMessage(chatId, `Stopping ${label}...`);
1085
1077
  } else {
1086
1078
  await bot.sendMessage(chatId, `${label} 当前没有活跃任务`);
@@ -1132,7 +1124,7 @@ function createBridgeStarter(deps) {
1132
1124
  }
1133
1125
  }
1134
1126
 
1135
- handleCommand(bot, chatId, commandText, liveCfg, executeTaskByName, sender, false)
1127
+ pipeline.processMessage(chatId, commandText, { bot, config: liveCfg, executeTaskByName, senderId: sender, readOnly: false })
1136
1128
  .catch(e => log('ERROR', `[IMESSAGE] handleCommand error: ${e.message}`));
1137
1129
  }
1138
1130
  } catch (e) {
@@ -1,12 +1,23 @@
1
1
  'use strict';
2
2
 
3
3
  function createCheckpointUtils(deps) {
4
- const { execSync, execFile, path, log } = deps;
4
+ const { execSync: _execSync, execFile, path, log } = deps;
5
5
  const { promisify } = require('util');
6
6
  const execFileAsync = execFile ? promisify(execFile) : null;
7
7
 
8
8
  const CHECKPOINT_PREFIX = '[metame-checkpoint]';
9
9
  const CHECKPOINT_REF_PREFIX = 'refs/metame/checkpoints/';
10
+
11
+ // Build the ref path for a checkpoint.
12
+ // When agentKey is provided: refs/metame/checkpoints/<agentKey>/<ts>
13
+ // Otherwise: refs/metame/checkpoints/<ts> (backward compat)
14
+ function _checkpointRef(ts, agentKey) {
15
+ if (agentKey && String(agentKey).trim()) {
16
+ const safe = String(agentKey).replace(/[^a-zA-Z0-9_\-]/g, '_').slice(0, 60);
17
+ return `${CHECKPOINT_REF_PREFIX}${safe}/${ts}`;
18
+ }
19
+ return `${CHECKPOINT_REF_PREFIX}${ts}`;
20
+ }
10
21
  const MAX_CHECKPOINTS = 20;
11
22
 
12
23
  function cpExtractTimestamp(message) {
@@ -50,41 +61,41 @@ function createCheckpointUtils(deps) {
50
61
  return { ts, safeLabel, msg: `${CHECKPOINT_PREFIX}${safeLabel} (${ts})` };
51
62
  }
52
63
 
53
- // Build a checkpoint commit stored under refs/metame/checkpoints/{ts} — never pushed by git push.
64
+ // Build a checkpoint commit stored under refs/metame/checkpoints/{agentKey}/{ts} — never pushed by git push.
54
65
  // Returns the commit SHA, or null if nothing changed.
55
- function gitCheckpoint(cwd, label) {
66
+ function gitCheckpoint(cwd, label, agentKey) {
67
+ const { execFileSync } = require('child_process');
56
68
  try {
57
- execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'ignore', ...WIN_HIDE });
69
+ execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, stdio: 'ignore', ...WIN_HIDE });
58
70
 
59
71
  // Snapshot current index so we can restore it after staging
60
- const originalTree = execSync('git write-tree', { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).trim();
72
+ const originalTree = execFileSync('git', ['write-tree'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).toString().trim();
61
73
 
62
74
  // Stage everything to get a full snapshot tree
63
- execSync('git add -A', { cwd, stdio: 'ignore', timeout: 5000, ...WIN_HIDE });
64
- const cpTree = execSync('git write-tree', { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).trim();
75
+ execFileSync('git', ['add', '-A'], { cwd, stdio: 'ignore', timeout: 5000, ...WIN_HIDE });
76
+ const cpTree = execFileSync('git', ['write-tree'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).toString().trim();
65
77
 
66
78
  // Restore index immediately — leave the user's staged state intact
67
- execSync(`git read-tree ${originalTree}`, { cwd, stdio: 'ignore', timeout: 3000, ...WIN_HIDE });
79
+ execFileSync('git', ['read-tree', originalTree], { cwd, stdio: 'ignore', timeout: 3000, ...WIN_HIDE });
68
80
 
69
81
  // Compare against HEAD tree — skip if nothing changed
70
82
  let headTree = '';
71
- try { headTree = execSync('git rev-parse HEAD^{tree}', { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).trim(); } catch { /* no commits yet */ }
83
+ try { headTree = execFileSync('git', ['rev-parse', 'HEAD^{tree}'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).toString().trim(); } catch { /* no commits yet */ }
72
84
  if (cpTree === headTree) return null;
73
85
 
74
86
  const { ts, safeLabel, msg } = buildCheckpointMsg(label);
75
87
 
76
- // Build parent arg (-p HEAD, or nothing for initial commit)
77
- let parentFlag = '';
78
- try { parentFlag = `-p ${execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).trim()}`; } catch { /* no HEAD */ }
88
+ // Build parent args (-p HEAD, or empty for initial commit)
89
+ let parentArgs = [];
90
+ try { parentArgs = ['-p', execFileSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).toString().trim()]; } catch { /* no HEAD */ }
79
91
 
80
92
  // Create an orphaned commit object — NOT on any branch
81
- const cpSha = execSync(
82
- `git commit-tree ${cpTree} ${parentFlag} -m "${msg}"`,
93
+ const cpSha = execFileSync('git', ['commit-tree', cpTree, ...parentArgs, '-m', msg],
83
94
  { cwd, encoding: 'utf8', timeout: 10000, ...WIN_HIDE }
84
- ).trim();
95
+ ).toString().trim();
85
96
 
86
97
  // Point a local-only ref at it — git push never transfers refs/metame/*
87
- execSync(`git update-ref ${CHECKPOINT_REF_PREFIX}${ts} ${cpSha}`, { cwd, stdio: 'ignore', timeout: 3000, ...WIN_HIDE });
98
+ execFileSync('git', ['update-ref', _checkpointRef(ts, agentKey), cpSha], { cwd, stdio: 'ignore', timeout: 3000, ...WIN_HIDE });
88
99
 
89
100
  log('INFO', `Git checkpoint: ${cpSha.slice(0, 8)} in ${path.basename(cwd)}${safeLabel}`);
90
101
  return cpSha;
@@ -94,8 +105,8 @@ function createCheckpointUtils(deps) {
94
105
  }
95
106
 
96
107
  // Async version: same logic but non-blocking.
97
- async function gitCheckpointAsync(cwd, label) {
98
- if (!execFileAsync) return gitCheckpoint(cwd, label);
108
+ async function gitCheckpointAsync(cwd, label, agentKey) {
109
+ if (!execFileAsync) return gitCheckpoint(cwd, label, agentKey);
99
110
  try {
100
111
  await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, timeout: 3000, ...WIN_HIDE });
101
112
 
@@ -121,7 +132,7 @@ function createCheckpointUtils(deps) {
121
132
  const { stdout: cpShaOut } = await execFileAsync('git', ['commit-tree', cpTree, ...parentArgs, '-m', msg], { cwd, encoding: 'utf8', timeout: 10000, ...WIN_HIDE });
122
133
  const cpSha = cpShaOut.trim();
123
134
 
124
- await execFileAsync('git', ['update-ref', `${CHECKPOINT_REF_PREFIX}${ts}`, cpSha], { cwd, timeout: 3000, ...WIN_HIDE });
135
+ await execFileAsync('git', ['update-ref', _checkpointRef(ts, agentKey), cpSha], { cwd, timeout: 3000, ...WIN_HIDE });
125
136
 
126
137
  log('INFO', `Git checkpoint: ${cpSha.slice(0, 8)} in ${path.basename(cwd)}${safeLabel}`);
127
138
  return cpSha;
@@ -133,11 +144,13 @@ function createCheckpointUtils(deps) {
133
144
  // List checkpoints, newest first. Returns [{hash, message, ref, parentHash}].
134
145
  // Uses %(parent) in for-each-ref format — no extra subprocess per checkpoint.
135
146
  function listCheckpoints(cwd, limit = 20) {
147
+ const { execFileSync } = require('child_process');
136
148
  try {
137
- const raw = execSync(
138
- `git for-each-ref --sort=-committerdate --format="%(objectname)|%(refname)|%(parent)|%(contents:subject)" --count=${limit} ${CHECKPOINT_REF_PREFIX}`,
139
- { cwd, encoding: 'utf8', timeout: 5000, ...WIN_HIDE }
140
- ).trim();
149
+ const raw = execFileSync('git', [
150
+ 'for-each-ref', '--sort=-committerdate',
151
+ `--format=%(objectname)|%(refname)|%(parent)|%(contents:subject)`,
152
+ `--count=${limit}`, CHECKPOINT_REF_PREFIX,
153
+ ], { cwd, encoding: 'utf8', timeout: 5000, ...WIN_HIDE }).toString().trim();
141
154
  if (!raw) return [];
142
155
  return raw.split('\n').filter(Boolean).map(line => {
143
156
  const [hash, ref, parent, ...rest] = line.split('|');
@@ -148,12 +161,13 @@ function createCheckpointUtils(deps) {
148
161
 
149
162
  // Delete checkpoints beyond MAX_CHECKPOINTS (oldest first).
150
163
  function cleanupCheckpoints(cwd) {
164
+ const { execFileSync } = require('child_process');
151
165
  try {
152
166
  const all = listCheckpoints(cwd, 100);
153
167
  if (all.length <= MAX_CHECKPOINTS) return;
154
168
  const toDelete = all.slice(MAX_CHECKPOINTS); // oldest (for-each-ref sorted newest-first)
155
169
  for (const cp of toDelete) {
156
- try { execSync(`git update-ref -d ${cp.ref}`, { cwd, stdio: 'ignore', timeout: 3000, ...WIN_HIDE }); } catch { /* ignore */ }
170
+ try { execFileSync('git', ['update-ref', '-d', cp.ref], { cwd, stdio: 'ignore', timeout: 3000, ...WIN_HIDE }); } catch { /* ignore */ }
157
171
  }
158
172
  log('INFO', `Cleaned up ${toDelete.length} old checkpoints in ${path.basename(cwd)}`);
159
173
  } catch { /* ignore */ }