metame-cli 1.5.0 → 1.5.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.
@@ -1,5 +1,15 @@
1
1
  'use strict';
2
2
 
3
+ const {
4
+ createAgentId,
5
+ ensureClaudeMdSoulImport,
6
+ ensureAgentLayer,
7
+ repairAgentLayer,
8
+ refreshMemorySnapshot,
9
+ buildMemorySnapshotContent,
10
+ normalizeEngine: normalizeLayerEngine,
11
+ } = require('./agent-layer');
12
+
3
13
  function createAgentTools(deps) {
4
14
  const {
5
15
  fs,
@@ -31,8 +41,26 @@ function createAgentTools(deps) {
31
41
  return (String(agentName || '').replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase() || String(chatId));
32
42
  }
33
43
 
34
- function normalizeEngine(engine) {
35
- return String(engine || '').trim().toLowerCase() === 'codex' ? 'codex' : 'claude';
44
+ function ensureAgentMetadata({ cfg, projectKey, project, safeName, resolvedDir, engine }) {
45
+ const agentId = String(project && project.agent_id ? project.agent_id : createAgentId({
46
+ projectKey,
47
+ agentName: safeName,
48
+ cwd: resolvedDir,
49
+ }));
50
+ const ensured = ensureAgentLayer({
51
+ agentId,
52
+ projectKey,
53
+ agentName: safeName,
54
+ workspaceDir: resolvedDir,
55
+ engine: normalizeLayerEngine(engine || (project && project.engine)),
56
+ aliases: [safeName],
57
+ homeDir: HOME,
58
+ });
59
+ cfg.projects[projectKey] = {
60
+ ...cfg.projects[projectKey],
61
+ agent_id: ensured.agentId,
62
+ };
63
+ return ensured;
36
64
  }
37
65
 
38
66
  function ensureAdapterConfig(cfg, adapterKey) {
@@ -53,7 +81,7 @@ function createAgentTools(deps) {
53
81
 
54
82
  const projectKey = toProjectKey(safeName, chatId);
55
83
  let resolvedDir = resolveWorkspaceDir(workspaceDir);
56
- const normalizedEngine = engine ? normalizeEngine(engine) : null;
84
+ const normalizedEngine = engine ? normalizeLayerEngine(engine) : null;
57
85
 
58
86
  if (!resolvedDir) {
59
87
  const existing = cfg.projects[projectKey];
@@ -89,6 +117,7 @@ function createAgentTools(deps) {
89
117
  name: safeName,
90
118
  cwd: resolvedDir,
91
119
  nicknames: [safeName],
120
+ agent_id: createAgentId({ projectKey, agentName: safeName, cwd: resolvedDir }),
92
121
  ...(normalizedEngine === 'codex' ? { engine: 'codex' } : {}),
93
122
  };
94
123
  } else {
@@ -101,10 +130,20 @@ function createAgentTools(deps) {
101
130
  name: safeName,
102
131
  cwd: resolvedDir,
103
132
  nicknames,
133
+ agent_id: cfg.projects[projectKey].agent_id || createAgentId({ projectKey, agentName: safeName, cwd: resolvedDir }),
104
134
  ...(normalizedEngine === 'codex' ? { engine: 'codex' } : {}),
105
135
  };
106
136
  }
107
137
 
138
+ const agentLayer = ensureAgentMetadata({
139
+ cfg,
140
+ projectKey,
141
+ project: cfg.projects[projectKey],
142
+ safeName,
143
+ resolvedDir,
144
+ engine: normalizedEngine,
145
+ });
146
+
108
147
  writeConfigSafe(cfg);
109
148
  backupConfig();
110
149
 
@@ -117,6 +156,7 @@ function createAgentTools(deps) {
117
156
  cwd: resolvedDir,
118
157
  isNewProject: !existed,
119
158
  project: cfg.projects[projectKey],
159
+ agent: agentLayer,
120
160
  },
121
161
  };
122
162
  } catch (e) {
@@ -138,6 +178,7 @@ function createAgentTools(deps) {
138
178
  const claudeMdPath = path.join(cwd, 'CLAUDE.md');
139
179
  if (!fs.existsSync(claudeMdPath)) {
140
180
  fs.writeFileSync(claudeMdPath, `## Agent 角色\n\n${safeDelta}\n`, 'utf8');
181
+ try { ensureClaudeMdSoulImport(cwd); } catch { /* non-critical */ }
141
182
  return { ok: true, data: { created: true, merged: false, path: claudeMdPath } };
142
183
  }
143
184
 
@@ -180,6 +221,7 @@ ${safeDelta}
180
221
  cleanOutput = cleanOutput.replace(/^```[a-zA-Z]*\n/, '').replace(/\n```$/, '');
181
222
  }
182
223
  fs.writeFileSync(claudeMdPath, cleanOutput, 'utf8');
224
+ try { ensureClaudeMdSoulImport(cwd); } catch { /* non-critical */ }
183
225
  return { ok: true, data: { created: false, merged: true, path: claudeMdPath } };
184
226
  } catch (e) {
185
227
  return { ok: false, error: e.message };
@@ -188,7 +230,7 @@ ${safeDelta}
188
230
 
189
231
  async function createNewWorkspaceAgent(agentName, workspaceDir, roleDescription, chatId, { skipChatBinding = false, engine = null } = {}) {
190
232
  let bindData;
191
- const normalizedEngine = engine ? normalizeEngine(engine) : null;
233
+ const normalizedEngine = engine ? normalizeLayerEngine(engine) : null;
192
234
 
193
235
  if (skipChatBinding) {
194
236
  // Create the project entry without touching chat_agent_map
@@ -208,21 +250,33 @@ ${safeDelta}
208
250
  name: safeName,
209
251
  cwd: resolvedDir,
210
252
  nicknames: [safeName],
253
+ agent_id: createAgentId({ projectKey, agentName: safeName, cwd: resolvedDir }),
211
254
  ...(normalizedEngine === 'codex' ? { engine: 'codex' } : {}),
212
255
  };
213
- writeConfigSafe(cfg);
214
- backupConfig();
215
- } else if (normalizedEngine === 'codex') {
216
- cfg.projects[projectKey] = { ...cfg.projects[projectKey], engine: 'codex' };
217
- writeConfigSafe(cfg);
218
- backupConfig();
256
+ } else {
257
+ cfg.projects[projectKey] = {
258
+ ...cfg.projects[projectKey],
259
+ ...(normalizedEngine === 'codex' ? { engine: 'codex' } : {}),
260
+ agent_id: cfg.projects[projectKey].agent_id || createAgentId({ projectKey, agentName: safeName, cwd: resolvedDir }),
261
+ };
219
262
  }
263
+ const agentLayer = ensureAgentMetadata({
264
+ cfg,
265
+ projectKey,
266
+ project: cfg.projects[projectKey],
267
+ safeName,
268
+ resolvedDir,
269
+ engine: normalizedEngine,
270
+ });
271
+ writeConfigSafe(cfg);
272
+ backupConfig();
220
273
  bindData = {
221
274
  projectKey,
222
275
  cwd: resolvedDir,
223
276
  isNewProject: !existed,
224
277
  chatId: null, // not bound to any chat
225
278
  project: cfg.projects[projectKey],
279
+ agent: agentLayer,
226
280
  };
227
281
  } else {
228
282
  const bindResult = await bindAgentToChat(chatId, agentName, workspaceDir, { engine: normalizedEngine });
@@ -244,6 +298,10 @@ ${safeDelta}
244
298
  };
245
299
  }
246
300
 
301
+ // editAgentRoleDefinition may have just created CLAUDE.md for the first time.
302
+ // Ensure @SOUL.md import is present so Claude auto-loads soul on every future session.
303
+ try { ensureClaudeMdSoulImport(bindData.cwd); } catch { /* non-critical */ }
304
+
247
305
  return {
248
306
  ok: true,
249
307
  data: { ...bindData, role: roleResult.data },
@@ -302,12 +360,89 @@ ${safeDelta}
302
360
  }
303
361
  }
304
362
 
363
+ /**
364
+ * Lazy-migration repair: given a workspace directory, ensure the agent soul layer
365
+ * (~/.metame/agents/<id>/, SOUL.md, MEMORY.md) exists and is wired up.
366
+ * Persists agent_id back to daemon.yaml if it was missing.
367
+ * Safe to call repeatedly — idempotent.
368
+ */
369
+ async function repairAgentSoul(workspaceDir) {
370
+ try {
371
+ const cwd = resolveWorkspaceDir(workspaceDir);
372
+ if (!cwd) return { ok: false, error: 'workspaceDir is required' };
373
+ if (!fs.existsSync(cwd) || !fs.statSync(cwd).isDirectory()) {
374
+ return { ok: false, error: `workspaceDir not found: ${cwd}` };
375
+ }
376
+
377
+ const cfg = loadConfig();
378
+ let projectKey = null;
379
+ let project = null;
380
+ for (const [key, p] of Object.entries(cfg.projects || {})) {
381
+ if (!p || !p.cwd) continue;
382
+ const pCwd = normalizeCwd ? normalizeCwd(p.cwd) : p.cwd;
383
+ const r1 = path.resolve(pCwd);
384
+ const r2 = path.resolve(cwd);
385
+ const isMatch = process.platform === 'win32'
386
+ ? r1.toLowerCase() === r2.toLowerCase()
387
+ : r1 === r2;
388
+ if (isMatch) {
389
+ projectKey = key;
390
+ project = p;
391
+ break;
392
+ }
393
+ }
394
+ if (!projectKey) {
395
+ return { ok: false, error: `No registered agent found for: ${cwd}. Run /agent bind first.` };
396
+ }
397
+
398
+ const ensured = repairAgentLayer(projectKey, project, HOME);
399
+ if (!ensured) return { ok: false, error: 'repairAgentLayer returned null' };
400
+
401
+ // Persist agent_id back to config if it was missing
402
+ if (!project.agent_id) {
403
+ cfg.projects[projectKey] = { ...cfg.projects[projectKey], agent_id: ensured.agentId };
404
+ writeConfigSafe(cfg);
405
+ backupConfig();
406
+ }
407
+
408
+ return {
409
+ ok: true,
410
+ data: {
411
+ projectKey,
412
+ agentId: ensured.agentId,
413
+ views: ensured.views
414
+ ? Object.fromEntries(Object.entries(ensured.views).map(([k, v]) => [k, v.mode]))
415
+ : null,
416
+ },
417
+ };
418
+ } catch (e) {
419
+ return { ok: false, error: e.message };
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Refresh memory-snapshot.md from fresh session+fact data.
425
+ * Called by the engine after first-message of a session; also callable directly.
426
+ */
427
+ async function updateMemorySnapshot(agentId, sessions = [], facts = []) {
428
+ try {
429
+ if (!agentId) return { ok: false, error: 'agentId is required' };
430
+ const content = buildMemorySnapshotContent(sessions, facts);
431
+ const ok = refreshMemorySnapshot(agentId, content, HOME);
432
+ return { ok, error: ok ? null : 'agent directory not found or not yet created' };
433
+ } catch (e) {
434
+ return { ok: false, error: e.message };
435
+ }
436
+ }
437
+
305
438
  return {
306
439
  bindAgentToChat,
307
440
  createNewWorkspaceAgent,
308
441
  editAgentRoleDefinition,
309
442
  listAllAgents,
310
443
  unbindCurrentAgent,
444
+ repairAgentSoul,
445
+ updateMemorySnapshot,
311
446
  };
312
447
  }
313
448
 
@@ -1,7 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  function createCheckpointUtils(deps) {
4
- const { execSync, path, log } = deps;
4
+ const { execSync, execFile, path, log } = deps;
5
+ const { promisify } = require('util');
6
+ const execFileAsync = execFile ? promisify(execFile) : null;
5
7
 
6
8
  const CHECKPOINT_PREFIX = '[metame-checkpoint]';
7
9
  const MAX_CHECKPOINTS = 20;
@@ -35,19 +37,22 @@ function createCheckpointUtils(deps) {
35
37
  return message.replace(CHECKPOINT_PREFIX, '').trim();
36
38
  }
37
39
 
40
+ // On Windows, git.exe is a console app — windowsHide:true prevents flash
41
+ const WIN_HIDE = process.platform === 'win32' ? { windowsHide: true } : {};
42
+
38
43
  function gitCheckpoint(cwd, label) {
39
44
  try {
40
- execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'ignore' });
41
- execSync('git add -A', { cwd, stdio: 'ignore', timeout: 5000 });
42
- const status = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 5000 }).trim();
45
+ execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'ignore', ...WIN_HIDE });
46
+ execSync('git add -A', { cwd, stdio: 'ignore', timeout: 5000, ...WIN_HIDE });
47
+ const status = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 5000, ...WIN_HIDE }).trim();
43
48
  if (!status) return null;
44
49
  const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
45
50
  const safeLabel = label
46
51
  ? ' Before: ' + label.replace(/["\n\r]/g, ' ').slice(0, 60).trim()
47
52
  : '';
48
53
  const msg = `${CHECKPOINT_PREFIX}${safeLabel} (${ts})`;
49
- execSync(`git commit -m "${msg}" --no-verify`, { cwd, stdio: 'ignore', timeout: 10000 });
50
- const hash = execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', timeout: 3000 }).trim();
54
+ execSync(`git commit -m "${msg}" --no-verify`, { cwd, stdio: 'ignore', timeout: 10000, ...WIN_HIDE });
55
+ const hash = execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).trim();
51
56
  log('INFO', `Git checkpoint: ${hash.slice(0, 8)} in ${path.basename(cwd)}${safeLabel}`);
52
57
  return hash;
53
58
  } catch {
@@ -55,11 +60,34 @@ function createCheckpointUtils(deps) {
55
60
  }
56
61
  }
57
62
 
63
+ // Async version: runs git commands without blocking the event loop.
64
+ // Call fire-and-forget before spawning Claude; completes well before Claude's first file write.
65
+ async function gitCheckpointAsync(cwd, label) {
66
+ if (!execFileAsync) return gitCheckpoint(cwd, label); // fallback
67
+ try {
68
+ await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, timeout: 3000, ...WIN_HIDE });
69
+ await execFileAsync('git', ['add', '-A'], { cwd, timeout: 5000, ...WIN_HIDE });
70
+ const { stdout: status } = await execFileAsync('git', ['status', '--porcelain'], { cwd, encoding: 'utf8', timeout: 5000, ...WIN_HIDE });
71
+ if (!status.trim()) return null;
72
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
73
+ const safeLabel = label
74
+ ? ' Before: ' + label.replace(/["\n\r]/g, ' ').slice(0, 60).trim()
75
+ : '';
76
+ const msg = `${CHECKPOINT_PREFIX}${safeLabel} (${ts})`;
77
+ await execFileAsync('git', ['commit', '-m', msg, '--no-verify'], { cwd, timeout: 10000, ...WIN_HIDE });
78
+ const { stdout: hash } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE });
79
+ log('INFO', `Git checkpoint: ${hash.trim().slice(0, 8)} in ${path.basename(cwd)}${safeLabel}`);
80
+ return hash.trim();
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
85
+
58
86
  function listCheckpoints(cwd, limit = 20) {
59
87
  try {
60
88
  const raw = execSync(
61
89
  `git log --fixed-strings --oneline --all --grep="${CHECKPOINT_PREFIX}" -n ${limit} --format="%H %s"`,
62
- { cwd, encoding: 'utf8', timeout: 5000 }
90
+ { cwd, encoding: 'utf8', timeout: 5000, ...WIN_HIDE }
63
91
  ).trim();
64
92
  if (!raw) return [];
65
93
  return raw.split('\n').map(line => {
@@ -81,6 +109,7 @@ function createCheckpointUtils(deps) {
81
109
  cpExtractTimestamp,
82
110
  cpDisplayLabel,
83
111
  gitCheckpoint,
112
+ gitCheckpointAsync,
84
113
  listCheckpoints,
85
114
  cleanupCheckpoints,
86
115
  };