grimoire-framework 1.0.20 → 1.0.21
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/.grimoire/install-manifest.yaml +2 -2
- package/bin/commands/metrics.js +213 -55
- package/bin/commands/sync.js +203 -0
- package/bin/grimoire-cli.js +3 -0
- package/package.json +1 -1
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
# - SHA256 hashes for change detection
|
|
8
8
|
# - File types for categorization
|
|
9
9
|
#
|
|
10
|
-
version: 1.0.
|
|
11
|
-
generated_at: "2026-02-22T14:
|
|
10
|
+
version: 1.0.21
|
|
11
|
+
generated_at: "2026-02-22T14:46:40.983Z"
|
|
12
12
|
generator: scripts/generate-install-manifest.js
|
|
13
13
|
file_count: 1011
|
|
14
14
|
files:
|
package/bin/commands/metrics.js
CHANGED
|
@@ -1,107 +1,265 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Grimoire Metrics CLI Command
|
|
2
|
+
* Grimoire Metrics CLI Command — Dashboard + Period Filters + CSV Export
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* grimoire metrics # Resumo do mês atual
|
|
5
|
+
* grimoire metrics --period week # Últimos 7 dias
|
|
6
|
+
* grimoire metrics --period month # Últimos 30 dias
|
|
7
|
+
* grimoire metrics --period all # Todo o histórico
|
|
8
|
+
* grimoire metrics export --csv # Exporta para CSV
|
|
9
|
+
* grimoire metrics track <event> # Registra um evento manualmente
|
|
5
10
|
*/
|
|
6
11
|
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
7
14
|
const path = require('path');
|
|
8
15
|
const fs = require('fs/promises');
|
|
9
|
-
const
|
|
16
|
+
const fsSync = require('fs');
|
|
10
17
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
// ── Agent personas catalogue ───────────────────────────────────────────────────
|
|
19
|
+
const AGENT_ICONS = {
|
|
20
|
+
'dev': '🎨', 'qa': '🖌️', 'architect': '🏛️', 'pm': '📋', 'sm': '🌊',
|
|
21
|
+
'devops': '⚡', 'data-engineer': '📊', 'analyst': '🔍',
|
|
22
|
+
'ux-design-expert': '🎭', 'po': '🎯', 'squad-creator': '🗿',
|
|
23
|
+
'grimoire-master': '👑',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ── Entry point ────────────────────────────────────────────────────────────────
|
|
15
27
|
async function run(args) {
|
|
16
28
|
let baseDir = process.cwd();
|
|
17
|
-
if (!existsSync(path.join(baseDir, '.grimoire')) &&
|
|
29
|
+
if (!fsSync.existsSync(path.join(baseDir, '.grimoire')) &&
|
|
30
|
+
fsSync.existsSync(path.join(baseDir, 'grimoire', '.grimoire'))) {
|
|
18
31
|
baseDir = path.join(baseDir, 'grimoire');
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
const metricsDir = path.join(baseDir, '.grimoire', 'metrics');
|
|
22
35
|
|
|
23
|
-
if
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
// Auto-create metrics dir if .grimoire exists
|
|
37
|
+
if (!fsSync.existsSync(metricsDir)) {
|
|
38
|
+
if (fsSync.existsSync(path.join(baseDir, '.grimoire'))) {
|
|
39
|
+
await fs.mkdir(path.join(metricsDir, 'daily'), { recursive: true });
|
|
40
|
+
console.log('✅ Metrics directory initialized at .grimoire/metrics/');
|
|
41
|
+
} else {
|
|
42
|
+
console.error('❌ Grimoire not installed. Run: npx grimoire-framework install');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
26
45
|
}
|
|
27
46
|
|
|
47
|
+
// Sub-commands
|
|
48
|
+
const sub = args[0];
|
|
49
|
+
if (sub === 'export') { await exportMetrics(metricsDir, args.slice(1)); return; }
|
|
50
|
+
if (sub === 'track') { await trackEvent(metricsDir, args.slice(1)); return; }
|
|
51
|
+
if (sub === 'help') { showHelp(); return; }
|
|
52
|
+
|
|
53
|
+
// Parse period
|
|
54
|
+
const periodIdx = args.indexOf('--period');
|
|
55
|
+
const period = periodIdx !== -1 ? args[periodIdx + 1] : 'month';
|
|
56
|
+
|
|
28
57
|
try {
|
|
29
|
-
const stats = await aggregateMetrics(metricsDir);
|
|
30
|
-
showDashboard(stats);
|
|
31
|
-
} catch (
|
|
32
|
-
console.error(`❌ Error
|
|
58
|
+
const stats = await aggregateMetrics(metricsDir, period);
|
|
59
|
+
showDashboard(stats, period);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error(`❌ Error: ${e.message}`);
|
|
33
62
|
}
|
|
34
63
|
}
|
|
35
64
|
|
|
36
|
-
|
|
65
|
+
// ── Period utilities ───────────────────────────────────────────────────────────
|
|
66
|
+
function cutoffDate(period) {
|
|
67
|
+
const d = new Date();
|
|
68
|
+
if (period === 'week') d.setDate(d.getDate() - 7);
|
|
69
|
+
else if (period === 'month') d.setDate(d.getDate() - 30);
|
|
70
|
+
else if (period === 'all') return new Date(0);
|
|
71
|
+
else d.setDate(d.getDate() - 30); // default month
|
|
72
|
+
return d;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function periodLabel(period) {
|
|
76
|
+
const months = ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho',
|
|
77
|
+
'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
|
|
78
|
+
if (period === 'week') return 'Últimos 7 dias';
|
|
79
|
+
if (period === 'all') return 'Histórico completo';
|
|
80
|
+
return `${months[new Date().getMonth()]} ${new Date().getFullYear()}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Aggregator ─────────────────────────────────────────────────────────────────
|
|
84
|
+
async function aggregateMetrics(metricsDir, period) {
|
|
37
85
|
const dailyDir = path.join(metricsDir, 'daily');
|
|
86
|
+
const cutoff = cutoffDate(period);
|
|
87
|
+
|
|
38
88
|
const stats = {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
daysActive: 0
|
|
89
|
+
sessions: 0, stories: 0, tokens: 0,
|
|
90
|
+
agentUsage: {}, daysActive: 0,
|
|
91
|
+
memoryEntries: 0, commandsRun: 0,
|
|
92
|
+
firstDate: null, lastDate: null,
|
|
44
93
|
};
|
|
45
94
|
|
|
46
|
-
if (!existsSync(dailyDir)) return stats;
|
|
95
|
+
if (!fsSync.existsSync(dailyDir)) return stats;
|
|
96
|
+
|
|
97
|
+
const files = (await fs.readdir(dailyDir))
|
|
98
|
+
.filter(f => f.match(/^\d{4}-\d{2}-\d{2}/) && (f.endsWith('.jsonl') || f.endsWith('.json')))
|
|
99
|
+
.filter(f => new Date(f.substring(0, 10)) >= cutoff)
|
|
100
|
+
.sort();
|
|
47
101
|
|
|
48
|
-
const files = (await fs.readdir(dailyDir)).filter(f => f.endsWith('.jsonl'));
|
|
49
102
|
stats.daysActive = files.length;
|
|
103
|
+
if (files.length > 0) {
|
|
104
|
+
stats.firstDate = files[0].substring(0, 10);
|
|
105
|
+
stats.lastDate = files[files.length - 1].substring(0, 10);
|
|
106
|
+
}
|
|
50
107
|
|
|
51
108
|
for (const file of files) {
|
|
52
109
|
const content = await fs.readFile(path.join(dailyDir, file), 'utf8');
|
|
53
110
|
const lines = content.split('\n').filter(l => l.trim());
|
|
54
|
-
|
|
55
|
-
stats.totalSessions += lines.length;
|
|
111
|
+
stats.sessions += lines.length;
|
|
56
112
|
|
|
57
|
-
|
|
113
|
+
for (const line of lines) {
|
|
58
114
|
try {
|
|
59
115
|
const entry = JSON.parse(line);
|
|
60
|
-
|
|
61
|
-
if (entry.
|
|
62
|
-
|
|
63
|
-
if (entry.type === '
|
|
64
|
-
// Track agent usage
|
|
116
|
+
if (entry.tokens) stats.tokens += entry.tokens;
|
|
117
|
+
if (entry.type === 'story_complete') stats.stories += 1;
|
|
118
|
+
if (entry.type === 'memory_save') stats.memoryEntries += 1;
|
|
119
|
+
if (entry.type === 'command') stats.commandsRun += 1;
|
|
65
120
|
if (entry.agent) {
|
|
66
121
|
stats.agentUsage[entry.agent] = (stats.agentUsage[entry.agent] || 0) + 1;
|
|
67
122
|
}
|
|
68
|
-
} catch (
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
});
|
|
123
|
+
} catch (_) { /* skip bad lines */ }
|
|
124
|
+
}
|
|
72
125
|
}
|
|
73
126
|
|
|
74
127
|
return stats;
|
|
75
128
|
}
|
|
76
129
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
130
|
+
// ── Manual event tracking ──────────────────────────────────────────────────────
|
|
131
|
+
async function trackEvent(metricsDir, args) {
|
|
132
|
+
const type = args[0] || 'custom';
|
|
133
|
+
const content = args.slice(1).join(' ');
|
|
134
|
+
const today = new Date().toISOString().split('T')[0];
|
|
135
|
+
const dailyDir = path.join(metricsDir, 'daily');
|
|
136
|
+
if (!fsSync.existsSync(dailyDir)) await fs.mkdir(dailyDir, { recursive: true });
|
|
137
|
+
|
|
138
|
+
const entry = {
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
type,
|
|
141
|
+
content: content || type,
|
|
142
|
+
};
|
|
143
|
+
const file = path.join(dailyDir, `${today}.jsonl`);
|
|
144
|
+
await fs.appendFile(file, JSON.stringify(entry) + '\n', 'utf8');
|
|
145
|
+
console.log(`✅ Event tracked: [${type}] ${content || ''}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Dashboard renderer ─────────────────────────────────────────────────────────
|
|
149
|
+
function bar(value, max, width = 20) {
|
|
150
|
+
const filled = max > 0 ? Math.round((value / max) * width) : 0;
|
|
151
|
+
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function showDashboard(stats, period) {
|
|
155
|
+
const topAgents = Object.entries(stats.agentUsage).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
156
|
+
const maxUsage = topAgents.length > 0 ? topAgents[0][1] : 0;
|
|
157
|
+
|
|
158
|
+
const sep = '─'.repeat(51);
|
|
159
|
+
|
|
81
160
|
console.log(`
|
|
82
|
-
📊 Grimoire
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Dias
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.sort((a, b) => b[1] - a[1])
|
|
93
|
-
.slice(0, 3);
|
|
161
|
+
📊 Grimoire Metrics — ${periodLabel(period)}
|
|
162
|
+
${sep}
|
|
163
|
+
📅 Período ${stats.firstDate || 'hoje'} → ${stats.lastDate || 'hoje'}
|
|
164
|
+
📆 Dias ativos ${stats.daysActive}
|
|
165
|
+
🔄 Sessões totais ${stats.sessions}
|
|
166
|
+
📖 Stories concluídas ${stats.stories}
|
|
167
|
+
💾 Entradas de memória ${stats.memoryEntries}
|
|
168
|
+
⚡ Tokens estimados ${stats.tokens > 0 ? '~' + (stats.tokens / 1000).toFixed(1) + 'k' : '(sem dados de hooks)'}
|
|
169
|
+
${sep}
|
|
170
|
+
🤖 Agentes mais usados:`);
|
|
94
171
|
|
|
95
172
|
if (topAgents.length > 0) {
|
|
96
173
|
topAgents.forEach(([agent, count]) => {
|
|
97
|
-
|
|
174
|
+
const icon = AGENT_ICONS[agent] || '🎭';
|
|
175
|
+
const b = bar(count, maxUsage, 15);
|
|
176
|
+
console.log(` ${icon} @${agent.padEnd(20)} ${b} ${count}×`);
|
|
98
177
|
});
|
|
99
178
|
} else {
|
|
100
|
-
console.log(' (Nenhum
|
|
179
|
+
console.log(' (Nenhum evento de agente registrado ainda)');
|
|
180
|
+
console.log('\n 💡 Para registrar uso: grimoire metrics track agent dev');
|
|
101
181
|
}
|
|
102
182
|
|
|
103
|
-
console.log(
|
|
104
|
-
💡 Dica:
|
|
183
|
+
console.log(`${sep}
|
|
184
|
+
💡 Dica: Use hooks para coletar dados automaticamente
|
|
185
|
+
grimoire metrics track story_complete "Login implementado"
|
|
186
|
+
grimoire metrics --period week
|
|
187
|
+
grimoire metrics export --csv
|
|
188
|
+
`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── CSV / Markdown export ──────────────────────────────────────────────────────
|
|
192
|
+
async function exportMetrics(metricsDir, args) {
|
|
193
|
+
const format = args.includes('--csv') ? 'csv' : 'markdown';
|
|
194
|
+
const dailyDir = path.join(metricsDir, 'daily');
|
|
195
|
+
|
|
196
|
+
if (!fsSync.existsSync(dailyDir)) {
|
|
197
|
+
console.error('❌ No metrics data found.'); return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const files = (await fs.readdir(dailyDir)).filter(f => f.match(/^\d{4}-\d{2}-\d{2}/)).sort();
|
|
201
|
+
const allEntries = [];
|
|
202
|
+
|
|
203
|
+
for (const file of files) {
|
|
204
|
+
const date = file.substring(0, 10);
|
|
205
|
+
const content = await fs.readFile(path.join(dailyDir, file), 'utf8');
|
|
206
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
try { allEntries.push({ date, ...JSON.parse(line) }); } catch (_) { }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const outPath = path.join(process.cwd(),
|
|
213
|
+
`grimoire-metrics-${new Date().toISOString().split('T')[0]}.${format === 'csv' ? 'csv' : 'md'}`);
|
|
214
|
+
|
|
215
|
+
if (format === 'csv') {
|
|
216
|
+
const headers = ['date', 'timestamp', 'type', 'agent', 'tokens', 'content'];
|
|
217
|
+
const rows = allEntries.map(e =>
|
|
218
|
+
headers.map(h => JSON.stringify(e[h] ?? '')).join(',')
|
|
219
|
+
);
|
|
220
|
+
await fs.writeFile(outPath, [headers.join(','), ...rows].join('\n'), 'utf8');
|
|
221
|
+
} else {
|
|
222
|
+
let md = '# Grimoire Metrics Export\n\n';
|
|
223
|
+
md += `> Exported: ${new Date().toISOString()}\n\n`;
|
|
224
|
+
md += `| Date | Time | Type | Agent | Tokens | Content |\n`;
|
|
225
|
+
md += `|------|------|------|-------|--------|---------|\n`;
|
|
226
|
+
allEntries.forEach(e => {
|
|
227
|
+
const time = e.timestamp ? e.timestamp.split('T')[1]?.split('.')[0] : '';
|
|
228
|
+
md += `| ${e.date} | ${time} | ${e.type || ''} | ${e.agent || ''} | ${e.tokens || ''} | ${e.content || ''} |\n`;
|
|
229
|
+
});
|
|
230
|
+
await fs.writeFile(outPath, md, 'utf8');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log(`✅ Metrics exported (${allEntries.length} events) → ${outPath}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Help ───────────────────────────────────────────────────────────────────────
|
|
237
|
+
function showHelp() {
|
|
238
|
+
console.log(`
|
|
239
|
+
📊 Grimoire Metrics — Dashboard de Produtividade
|
|
240
|
+
|
|
241
|
+
USO:
|
|
242
|
+
grimoire metrics Resumo do mês atual
|
|
243
|
+
grimoire metrics --period week Últimos 7 dias
|
|
244
|
+
grimoire metrics --period month Últimos 30 dias
|
|
245
|
+
grimoire metrics --period all Histórico completo
|
|
246
|
+
grimoire metrics export --csv Exportar para CSV
|
|
247
|
+
grimoire metrics export Exportar para Markdown
|
|
248
|
+
grimoire metrics track <type> [msg] Registrar evento manualmente
|
|
249
|
+
|
|
250
|
+
TIPOS DE EVENTO:
|
|
251
|
+
story_complete "Nome da story" Marcar story como concluída
|
|
252
|
+
agent <name> Registrar uso de agente
|
|
253
|
+
command <cmd> Registrar comando executado
|
|
254
|
+
memory_save Registrar uso de memória
|
|
255
|
+
custom <desc> Evento personalizado
|
|
256
|
+
|
|
257
|
+
EXEMPLOS:
|
|
258
|
+
grimoire metrics --period week
|
|
259
|
+
grimoire metrics track story_complete "Implementar login"
|
|
260
|
+
grimoire metrics track agent dev
|
|
261
|
+
grimoire metrics export --csv
|
|
262
|
+
`);
|
|
105
263
|
}
|
|
106
264
|
|
|
107
265
|
module.exports = { run };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grimoire sync — Global Agent Synchronization
|
|
3
|
+
*
|
|
4
|
+
* Copies agents from the current project to a global ~/.grimoire/agents/
|
|
5
|
+
* and optionally syncs them to other projects on the machine.
|
|
6
|
+
*
|
|
7
|
+
* grimoire sync --global Sync agents to global store
|
|
8
|
+
* grimoire sync --from-global Pull global agents into current project
|
|
9
|
+
* grimoire sync --list List global agents
|
|
10
|
+
* grimoire sync --projects Show projects registered in global store
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
|
|
19
|
+
const GLOBAL_DIR = path.join(os.homedir(), '.grimoire');
|
|
20
|
+
const GLOBAL_AGENTS = path.join(GLOBAL_DIR, 'agents');
|
|
21
|
+
const GLOBAL_REGISTRY = path.join(GLOBAL_DIR, 'projects.json');
|
|
22
|
+
|
|
23
|
+
// ── Utilities ──────────────────────────────────────────────────────────────────
|
|
24
|
+
function ensureGlobalDir() {
|
|
25
|
+
if (!fs.existsSync(GLOBAL_AGENTS)) fs.mkdirSync(GLOBAL_AGENTS, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadRegistry() {
|
|
29
|
+
if (!fs.existsSync(GLOBAL_REGISTRY)) return { projects: [], lastSync: null };
|
|
30
|
+
try { return JSON.parse(fs.readFileSync(GLOBAL_REGISTRY, 'utf8')); }
|
|
31
|
+
catch (_) { return { projects: [], lastSync: null }; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function saveRegistry(data) {
|
|
35
|
+
fs.writeFileSync(GLOBAL_REGISTRY, JSON.stringify(data, null, 2), 'utf8');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getLocalAgentsDir() {
|
|
39
|
+
const cwd = process.cwd();
|
|
40
|
+
const dir = path.join(cwd, '.codex', 'agents');
|
|
41
|
+
if (!fs.existsSync(dir)) return null;
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function copyAgent(src, dest) {
|
|
46
|
+
const content = fs.readFileSync(src, 'utf8');
|
|
47
|
+
fs.writeFileSync(dest, content, 'utf8');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── Commands ───────────────────────────────────────────────────────────────────
|
|
51
|
+
async function run(args) {
|
|
52
|
+
const sub = args.find(a => a.startsWith('--')) || '--global';
|
|
53
|
+
|
|
54
|
+
switch (sub) {
|
|
55
|
+
case '--global': await syncToGlobal(); break;
|
|
56
|
+
case '--from-global': await syncFromGlobal(); break;
|
|
57
|
+
case '--list': listGlobalAgents(); break;
|
|
58
|
+
case '--projects': listProjects(); break;
|
|
59
|
+
case '--help':
|
|
60
|
+
default: showHelp(); break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Sync local → global ────────────────────────────────────────────────────────
|
|
65
|
+
async function syncToGlobal() {
|
|
66
|
+
const localDir = getLocalAgentsDir();
|
|
67
|
+
if (!localDir) {
|
|
68
|
+
console.error('❌ No .codex/agents/ found. Run: npx grimoire-framework install');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ensureGlobalDir();
|
|
73
|
+
|
|
74
|
+
const agents = fs.readdirSync(localDir).filter(f => f.endsWith('.md'));
|
|
75
|
+
let synced = 0, skipped = 0;
|
|
76
|
+
|
|
77
|
+
console.log(`\n🌐 Syncing ${agents.length} agents to global store (~/.grimoire/agents/)...\n`);
|
|
78
|
+
|
|
79
|
+
for (const file of agents) {
|
|
80
|
+
const src = path.join(localDir, file);
|
|
81
|
+
const dest = path.join(GLOBAL_AGENTS, file);
|
|
82
|
+
const exists = fs.existsSync(dest);
|
|
83
|
+
|
|
84
|
+
// Compare content to decide if update is needed
|
|
85
|
+
if (exists) {
|
|
86
|
+
const srcContent = fs.readFileSync(src, 'utf8');
|
|
87
|
+
const destContent = fs.readFileSync(dest, 'utf8');
|
|
88
|
+
if (srcContent === destContent) { skipped++; continue; }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
copyAgent(src, dest);
|
|
92
|
+
console.log(` ✅ ${file.replace('.md', '')}`);
|
|
93
|
+
synced++;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (skipped > 0) console.log(` ⏩ ${skipped} already up-to-date (skipped)`);
|
|
97
|
+
|
|
98
|
+
// Register this project
|
|
99
|
+
const registry = loadRegistry();
|
|
100
|
+
const cwd = process.cwd();
|
|
101
|
+
if (!registry.projects.includes(cwd)) registry.projects.push(cwd);
|
|
102
|
+
registry.lastSync = new Date().toISOString();
|
|
103
|
+
saveRegistry(registry);
|
|
104
|
+
|
|
105
|
+
console.log(`\n✅ ${synced} agent${synced === 1 ? '' : 's'} synced to global store`);
|
|
106
|
+
console.log(` ${GLOBAL_AGENTS}`);
|
|
107
|
+
console.log(`\n💡 Use "grimoire sync --from-global" in other projects to pull these agents`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Sync global → local ────────────────────────────────────────────────────────
|
|
111
|
+
async function syncFromGlobal() {
|
|
112
|
+
if (!fs.existsSync(GLOBAL_AGENTS)) {
|
|
113
|
+
console.error('❌ No global agents found. Run "grimoire sync --global" first.');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const localDir = getLocalAgentsDir();
|
|
118
|
+
if (!localDir) {
|
|
119
|
+
console.error('❌ No .codex/agents/ found. Run: npx grimoire-framework install');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const globalAgents = fs.readdirSync(GLOBAL_AGENTS).filter(f => f.endsWith('.md'));
|
|
124
|
+
const localAgents = new Set(fs.readdirSync(localDir).filter(f => f.endsWith('.md')));
|
|
125
|
+
|
|
126
|
+
// Only pull agents that DON'T exist locally (preserve local customizations)
|
|
127
|
+
const newAgents = globalAgents.filter(f => !localAgents.has(f));
|
|
128
|
+
|
|
129
|
+
console.log(`\n📥 Pulling from global store (~/.grimoire/agents/)...\n`);
|
|
130
|
+
console.log(` Global agents: ${globalAgents.length}`);
|
|
131
|
+
console.log(` Local agents: ${localAgents.size}`);
|
|
132
|
+
console.log(` New to pull: ${newAgents.length}\n`);
|
|
133
|
+
|
|
134
|
+
if (newAgents.length === 0) {
|
|
135
|
+
console.log('✅ Local agents are already up-to-date with global store.');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const file of newAgents) {
|
|
140
|
+
copyAgent(path.join(GLOBAL_AGENTS, file), path.join(localDir, file));
|
|
141
|
+
console.log(` ✅ Added: @${file.replace('.md', '')}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(`\n✅ ${newAgents.length} agent${newAgents.length === 1 ? '' : 's'} pulled from global store`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── List global agents ─────────────────────────────────────────────────────────
|
|
148
|
+
function listGlobalAgents() {
|
|
149
|
+
if (!fs.existsSync(GLOBAL_AGENTS)) {
|
|
150
|
+
console.log('ℹ️ No global agents yet. Run: grimoire sync --global');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const agents = fs.readdirSync(GLOBAL_AGENTS).filter(f => f.endsWith('.md'));
|
|
155
|
+
const registry = loadRegistry();
|
|
156
|
+
|
|
157
|
+
console.log(`\n🌐 Global Grimoire Agents (${agents.length})`);
|
|
158
|
+
console.log(` Store: ${GLOBAL_AGENTS}`);
|
|
159
|
+
if (registry.lastSync) console.log(` Last sync: ${registry.lastSync.split('T')[0]}\n`);
|
|
160
|
+
|
|
161
|
+
agents.forEach(f => console.log(` 🤖 @${f.replace('.md', '')}`));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ── List registered projects ───────────────────────────────────────────────────
|
|
165
|
+
function listProjects() {
|
|
166
|
+
const registry = loadRegistry();
|
|
167
|
+
console.log(`\n📁 Projects using Grimoire global store (${registry.projects.length}):\n`);
|
|
168
|
+
if (registry.projects.length === 0) {
|
|
169
|
+
console.log(' (No projects registered yet — run "grimoire sync --global")');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
registry.projects.forEach(p => {
|
|
173
|
+
const exists = fs.existsSync(p);
|
|
174
|
+
console.log(` ${exists ? '✅' : '❌'} ${p}`);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Help ───────────────────────────────────────────────────────────────────────
|
|
179
|
+
function showHelp() {
|
|
180
|
+
console.log(`
|
|
181
|
+
🌐 Grimoire Sync — Sincronização Global de Agentes
|
|
182
|
+
|
|
183
|
+
USO:
|
|
184
|
+
grimoire sync --global Envia agentes locais para ~/.grimoire/agents/
|
|
185
|
+
grimoire sync --from-global Puxa agentes globais para o projeto atual
|
|
186
|
+
grimoire sync --list Lista agentes no store global
|
|
187
|
+
grimoire sync --projects Lista projetos registrados
|
|
188
|
+
|
|
189
|
+
FLUXO TÍPICO:
|
|
190
|
+
# No projeto A (onde você criou um agente customizado):
|
|
191
|
+
grimoire agent create → grimoire sync --global
|
|
192
|
+
|
|
193
|
+
# No projeto B (onde você quer esse agente):
|
|
194
|
+
grimoire sync --from-global
|
|
195
|
+
|
|
196
|
+
NOTAS:
|
|
197
|
+
- --from-global nunca sobrescreve agentes existentes localmente
|
|
198
|
+
- --global sobrescreve apenas se o conteúdo mudou
|
|
199
|
+
- O store global fica em: ${GLOBAL_DIR}
|
|
200
|
+
`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = { run };
|
package/bin/grimoire-cli.js
CHANGED