grimoire-framework 1.1.1 β†’ 1.3.0

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.
@@ -7,8 +7,8 @@
7
7
  # - SHA256 hashes for change detection
8
8
  # - File types for categorization
9
9
  #
10
- version: 1.1.1
11
- generated_at: "2026-02-22T15:03:35.007Z"
10
+ version: 1.3.0
11
+ generated_at: "2026-02-22T16:50:35.905Z"
12
12
  generator: scripts/generate-install-manifest.js
13
13
  file_count: 1011
14
14
  files:
@@ -100,6 +100,7 @@ async function run(args) {
100
100
  switch (sub) {
101
101
  case 'create': await createWizard(); break;
102
102
  case 'edit': await editAgent(args[1]); break;
103
+ case 'validate': validateAgent(args[1]); break;
103
104
  case 'remove':
104
105
  case 'delete': await removeAgent(args[1]); break;
105
106
  case 'list':
@@ -191,6 +192,43 @@ Para sincronizar com outras IDEs:
191
192
  `);
192
193
  }
193
194
 
195
+ function validateAgent(id) {
196
+ if (!id) { console.log('Usage: grimoire agent validate <name>'); return; }
197
+ const file = path.join(process.cwd(), '.codex', 'agents', `${id}.md`);
198
+ if (!fs.existsSync(file)) { console.log(`❌ Agent "${id}" not found in .codex/agents/`); return; }
199
+
200
+ const content = fs.readFileSync(file, 'utf8');
201
+ const checks = [
202
+ { name: 'YAML block (`\`\`\`yaml`)', pass: content.includes('```yaml') },
203
+ { name: 'activation-instructions', pass: content.includes('activation-instructions') },
204
+ { name: 'STEP 1 / STEP 2 / STEP 3', pass: /STEP 1/.test(content) && /STEP 2/.test(content) },
205
+ { name: 'agent.name defined', pass: /\bname:\s*\S+/.test(content) },
206
+ { name: 'agent.id defined', pass: /\bid:\s*\S+/.test(content) },
207
+ { name: 'agent.icon defined', pass: /\bicon:\s*\S+/.test(content) },
208
+ { name: 'greeting_levels present', pass: content.includes('greeting_levels') },
209
+ { name: 'signature_closing present', pass: content.includes('signature_closing') },
210
+ { name: 'ACTIVATION-NOTICE present', pass: content.includes('ACTIVATION-NOTICE') },
211
+ { name: '*exit command referenced', pass: content.includes('*exit') },
212
+ ];
213
+
214
+ const passed = checks.filter(c => c.pass).length;
215
+ const score = Math.round((passed / checks.length) * 100);
216
+ const emoji = score === 100 ? '🟒' : score >= 70 ? '🟑' : 'πŸ”΄';
217
+
218
+ console.log(`\nπŸ” Agent Validation: @${id}\n${'─'.repeat(40)}`);
219
+ checks.forEach(c => console.log(` ${c.pass ? 'βœ…' : '❌'} ${c.name}`));
220
+ console.log(`${'─'.repeat(40)}`);
221
+ console.log(` ${emoji} Score: ${score}/100 (${passed}/${checks.length} checks)\n`);
222
+
223
+ if (score < 100) {
224
+ console.log(' Fixes sugeridos:');
225
+ checks.filter(c => !c.pass).forEach(c => console.log(` β†’ Adicionar: ${c.name}`));
226
+ console.log();
227
+ } else {
228
+ console.log(' βœ… Agente vΓ‘lido e pronto para uso!\n');
229
+ }
230
+ }
231
+
194
232
  async function editAgent(id) {
195
233
  if (!id) { console.log('Usage: grimoire agent edit <name>'); return; }
196
234
  const file = path.join(process.cwd(), '.codex', 'agents', `${id}.md`);
@@ -0,0 +1,181 @@
1
+ /**
2
+ * grimoire config β€” Project-level configuration
3
+ *
4
+ * grimoire config get <key> LΓͺ valor de uma chave
5
+ * grimoire config set <key> <value> Define valor de uma chave
6
+ * grimoire config list Lista todas as configuraΓ§Γ΅es
7
+ * grimoire config reset Reseta para os defaults
8
+ *
9
+ * Config file: .grimoire/config.yaml
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+
17
+ // ── Defaults ───────────────────────────────────────────────────────────────────
18
+ const DEFAULTS = {
19
+ default_agent: 'grimoire-master',
20
+ memory_auto_save: false,
21
+ update_check_interval: 24, // hours
22
+ metrics_enabled: true,
23
+ hooks_enabled: false,
24
+ marketplace_cache_ttl: 60, // minutes
25
+ language: 'pt-BR',
26
+ };
27
+
28
+ const DESCRIPTIONS = {
29
+ default_agent: 'Agente padrΓ£o ao iniciar o IDE',
30
+ memory_auto_save: 'Salvar memΓ³ria automaticamente ao encerrar sessΓ£o',
31
+ update_check_interval: 'Intervalo de horas entre verificaΓ§Γ΅es de atualizaΓ§Γ£o',
32
+ metrics_enabled: 'Habilitar coleta de mΓ©tricas de produtividade',
33
+ hooks_enabled: 'Habilitar git hooks para auto-tracking de commits',
34
+ marketplace_cache_ttl: 'Tempo de cache do marketplace em minutos',
35
+ language: 'Idioma preferido para prompts e saΓ­das',
36
+ };
37
+
38
+ // ── File location ──────────────────────────────────────────────────────────────
39
+ function findConfigFile() {
40
+ const cwd = process.cwd();
41
+ const direct = path.join(cwd, '.grimoire', 'config.yaml');
42
+ const subdir = path.join(cwd, 'grimoire', '.grimoire', 'config.yaml');
43
+ if (fs.existsSync(direct)) return direct;
44
+ if (fs.existsSync(subdir)) return subdir;
45
+ // Return direct path for writing even if it doesn't exist
46
+ const grimoireDir = path.join(cwd, '.grimoire');
47
+ if (fs.existsSync(grimoireDir)) return direct;
48
+ return null;
49
+ }
50
+
51
+ // ── Minimal YAML parser/writer ─────────────────────────────────────────────────
52
+ function parseYaml(content) {
53
+ const result = {};
54
+ for (const line of content.split('\n')) {
55
+ const stripped = line.trim();
56
+ if (!stripped || stripped.startsWith('#')) continue;
57
+ const colonIdx = stripped.indexOf(':');
58
+ if (colonIdx === -1) continue;
59
+ const key = stripped.slice(0, colonIdx).trim();
60
+ let val = stripped.slice(colonIdx + 1).trim();
61
+ // Parse types
62
+ if (val === 'true') val = true;
63
+ else if (val === 'false') val = false;
64
+ else if (!isNaN(val) && val !== '') val = Number(val);
65
+ result[key] = val;
66
+ }
67
+ return result;
68
+ }
69
+
70
+ function toYaml(obj) {
71
+ const lines = ['# Grimoire Framework β€” Project Configuration', '# grimoire config set <key> <value> to edit', ''];
72
+ for (const [k, v] of Object.entries(obj)) {
73
+ const desc = DESCRIPTIONS[k];
74
+ if (desc) lines.push(`# ${desc}`);
75
+ lines.push(`${k}: ${v}`);
76
+ lines.push('');
77
+ }
78
+ return lines.join('\n');
79
+ }
80
+
81
+ function loadConfig() {
82
+ const file = findConfigFile();
83
+ if (!file || !fs.existsSync(file)) return { ...DEFAULTS };
84
+ try {
85
+ return { ...DEFAULTS, ...parseYaml(fs.readFileSync(file, 'utf8')) };
86
+ } catch (_) { return { ...DEFAULTS }; }
87
+ }
88
+
89
+ function saveConfig(obj) {
90
+ const cwd = process.cwd();
91
+ let grimoireDir = path.join(cwd, '.grimoire');
92
+ if (!fs.existsSync(grimoireDir)) {
93
+ const sub = path.join(cwd, 'grimoire', '.grimoire');
94
+ if (fs.existsSync(sub)) grimoireDir = sub;
95
+ else { console.error('❌ .grimoire/ not found. Run: npx grimoire-framework install'); return false; }
96
+ }
97
+ const file = path.join(grimoireDir, 'config.yaml');
98
+ fs.writeFileSync(file, toYaml(obj), 'utf8');
99
+ return true;
100
+ }
101
+
102
+ // ── Public API ─────────────────────────────────────────────────────────────────
103
+ function getConfigValue(key) {
104
+ return loadConfig()[key];
105
+ }
106
+
107
+ // ── Commands ───────────────────────────────────────────────────────────────────
108
+ function run(args) {
109
+ const sub = args[0];
110
+ const key = args[1];
111
+ const value = args[2];
112
+
113
+ switch (sub) {
114
+ case 'set': configSet(key, value); break;
115
+ case 'reset': configReset(); break;
116
+ case 'get':
117
+ if (!key) { configList(); break; }
118
+ configGet(key); break;
119
+ case 'list':
120
+ default: configList(); break;
121
+ }
122
+ }
123
+
124
+ function configGet(key) {
125
+ if (!key) { configList(); return; }
126
+ const config = loadConfig();
127
+ if (!(key in config)) {
128
+ console.log(`❌ Chave desconhecida: "${key}"`);
129
+ console.log(` Chaves vΓ‘lidas: ${Object.keys(DEFAULTS).join(', ')}\n`);
130
+ return;
131
+ }
132
+ console.log(`${key} = ${config[key]}`);
133
+ }
134
+
135
+ function configSet(key, value) {
136
+ if (!key || value === undefined) {
137
+ console.log('Usage: grimoire config set <key> <value>');
138
+ console.log('Ex: grimoire config set default_agent dev\n');
139
+ return;
140
+ }
141
+ if (!(key in DEFAULTS)) {
142
+ console.log(`❌ Chave desconhecida: "${key}"`);
143
+ console.log(` Chaves vΓ‘lidas: ${Object.keys(DEFAULTS).join(', ')}\n`);
144
+ return;
145
+ }
146
+ const config = loadConfig();
147
+ // Type coerce
148
+ let coerced = value;
149
+ if (value === 'true') coerced = true;
150
+ else if (value === 'false') coerced = false;
151
+ else if (!isNaN(value)) coerced = Number(value);
152
+
153
+ config[key] = coerced;
154
+ if (saveConfig(config)) {
155
+ console.log(`βœ… ${key} = ${coerced}`);
156
+ }
157
+ }
158
+
159
+ function configList() {
160
+ const config = loadConfig();
161
+ const file = findConfigFile();
162
+ console.log(`\nβš™οΈ Grimoire Config${file && fs.existsSync(file) ? ` (${path.relative(process.cwd(), file)})` : ' (defaults)'}\n`);
163
+ console.log(' ' + '─'.repeat(48));
164
+ for (const [k, v] of Object.entries(DEFAULTS)) {
165
+ const current = config[k];
166
+ const isDef = current === v;
167
+ const marker = isDef ? ' ' : '✏️ ';
168
+ console.log(` ${marker} ${k.padEnd(28)} ${String(current)}`);
169
+ }
170
+ console.log(' ' + '─'.repeat(48));
171
+ console.log(` πŸ’‘ grimoire config set <key> <value> para alterar`);
172
+ console.log(` πŸ’‘ grimoire config reset para restaurar defaults\n`);
173
+ }
174
+
175
+ function configReset() {
176
+ if (saveConfig({ ...DEFAULTS })) {
177
+ console.log('βœ… ConfiguraΓ§Γ£o restaurada para os defaults.\n');
178
+ }
179
+ }
180
+
181
+ module.exports = { run, getConfigValue, loadConfig };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * grimoire hooks β€” Git Hook Integration for Auto-Metrics Tracking
3
+ *
4
+ * grimoire hooks install Instala post-commit hook no repositΓ³rio atual
5
+ * grimoire hooks uninstall Remove o hook
6
+ * grimoire hooks status Verifica se o hook estΓ‘ instalado
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+
14
+ const HOOK_MARKER = '# grimoire-hook-v1';
15
+ const HOOK_CONTENT = `#!/bin/sh
16
+ ${HOOK_MARKER}
17
+ # Auto-installed by Grimoire Framework
18
+ # Remove with: grimoire hooks uninstall
19
+
20
+ # Get commit message (first line)
21
+ COMMIT_MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1)
22
+
23
+ # Track the commit in grimoire metrics (non-blocking)
24
+ grimoire metrics track commit "$COMMIT_MSG" 2>/dev/null &
25
+
26
+ exit 0
27
+ `;
28
+
29
+ function findGitDir() {
30
+ let dir = process.cwd();
31
+ for (let i = 0; i < 8; i++) {
32
+ if (fs.existsSync(path.join(dir, '.git'))) return dir;
33
+ const parent = path.dirname(dir);
34
+ if (parent === dir) break;
35
+ dir = parent;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function run(args) {
41
+ const sub = args[0] || 'status';
42
+ switch (sub) {
43
+ case 'install': install(); break;
44
+ case 'uninstall': uninstall(); break;
45
+ case 'status':
46
+ default: status(); break;
47
+ }
48
+ }
49
+
50
+ function install() {
51
+ const gitRoot = findGitDir();
52
+ if (!gitRoot) {
53
+ console.error('❌ Repositório git não encontrado. Execute dentro de um projeto git.');
54
+ return;
55
+ }
56
+
57
+ const hooksDir = path.join(gitRoot, '.git', 'hooks');
58
+ const hookFile = path.join(hooksDir, 'post-commit');
59
+
60
+ if (!fs.existsSync(hooksDir)) fs.mkdirSync(hooksDir, { recursive: true });
61
+
62
+ // Check if hook already exists and isn't ours
63
+ if (fs.existsSync(hookFile)) {
64
+ const existing = fs.readFileSync(hookFile, 'utf8');
65
+ if (existing.includes(HOOK_MARKER)) {
66
+ console.log('ℹ️ Hook jΓ‘ instalado. Use "grimoire hooks status" para verificar.');
67
+ return;
68
+ }
69
+ // Append to existing hook
70
+ const updated = existing.trimEnd() + '\n\n' + HOOK_CONTENT;
71
+ fs.writeFileSync(hookFile, updated, 'utf8');
72
+ } else {
73
+ fs.writeFileSync(hookFile, HOOK_CONTENT, 'utf8');
74
+ }
75
+
76
+ // Make executable (Unix/Mac β€” no-op on Windows but harmless)
77
+ try { fs.chmodSync(hookFile, '755'); } catch (_) { }
78
+
79
+ console.log(`
80
+ βœ… grimoire hooks instalado!
81
+
82
+ Hook: .git/hooks/post-commit
83
+ Efeito: commits sΓ£o automaticamente registrados em grimoire metrics
84
+
85
+ PrΓ³ximo passo: faΓ§a um git commit e execute:
86
+ grimoire metrics --period week
87
+
88
+ Para remover: grimoire hooks uninstall
89
+ `);
90
+ }
91
+
92
+ function uninstall() {
93
+ const gitRoot = findGitDir();
94
+ if (!gitRoot) { console.error('❌ Repositório git não encontrado.'); return; }
95
+
96
+ const hookFile = path.join(gitRoot, '.git', 'hooks', 'post-commit');
97
+ if (!fs.existsSync(hookFile)) {
98
+ console.log('ℹ️ Nenhum hook encontrado.');
99
+ return;
100
+ }
101
+
102
+ const content = fs.readFileSync(hookFile, 'utf8');
103
+ if (!content.includes(HOOK_MARKER)) {
104
+ console.log('ℹ️ Hook nΓ£o foi instalado pelo Grimoire. Nada removido.');
105
+ return;
106
+ }
107
+
108
+ // If our hook is the only content, delete the file
109
+ const lines = content.split('\n');
110
+ const otherLines = lines.filter(l => !l.includes(HOOK_MARKER) && !l.includes('grimoire metrics track'));
111
+ const cleaned = otherLines.join('\n').trim();
112
+
113
+ if (!cleaned || cleaned === '#!/bin/sh') {
114
+ fs.unlinkSync(hookFile);
115
+ console.log('βœ… Hook removido (arquivo deletado).');
116
+ } else {
117
+ fs.writeFileSync(hookFile, cleaned + '\n', 'utf8');
118
+ console.log('βœ… Hook grimoire removido (conteΓΊdo do hook preservado).');
119
+ }
120
+ }
121
+
122
+ function status() {
123
+ const gitRoot = findGitDir();
124
+ if (!gitRoot) { console.log('⚠️ Repositório git não encontrado.\n'); return; }
125
+
126
+ const hookFile = path.join(gitRoot, '.git', 'hooks', 'post-commit');
127
+ const exists = fs.existsSync(hookFile);
128
+ const isOurs = exists && fs.readFileSync(hookFile, 'utf8').includes(HOOK_MARKER);
129
+
130
+ console.log('\nπŸͺ Grimoire Hooks\n' + '─'.repeat(40));
131
+ console.log(` post-commit: ${isOurs ? 'βœ… instalado' : exists ? '⚠️ arquivo existe (nΓ£o Γ© nosso)' : 'β­• nΓ£o instalado'}`);
132
+ if (!isOurs) console.log(`\n Para instalar: grimoire hooks install`);
133
+ else console.log(`\n Para remover: grimoire hooks uninstall`);
134
+ console.log();
135
+ }
136
+
137
+ module.exports = { run };
@@ -35,6 +35,9 @@ async function run(args) {
35
35
  case 'save':
36
36
  await saveMemory(args.slice(1), memoryDir);
37
37
  break;
38
+ case 'list-tags':
39
+ await listTags(memoryDir);
40
+ break;
38
41
  case 'list':
39
42
  await listSessions(memoryDir);
40
43
  break;
@@ -67,7 +70,22 @@ async function run(args) {
67
70
  * Saves memory using JSONL append (O(1) complexity)
68
71
  */
69
72
  async function saveMemory(args, memoryDir) {
70
- const content = args.join(' ');
73
+ // Extract --tag value if present
74
+ const tagIdx = args.findIndex(a => a === '--tag' || a.startsWith('--tag='));
75
+ let tag = null;
76
+ let filteredArgs = [...args];
77
+ if (tagIdx !== -1) {
78
+ if (args[tagIdx].includes('=')) {
79
+ tag = args[tagIdx].split('=')[1];
80
+ } else {
81
+ tag = args[tagIdx + 1];
82
+ filteredArgs.splice(tagIdx, 2);
83
+ }
84
+ if (!tag) { filteredArgs.splice(tagIdx, 1); }
85
+ else { filteredArgs = filteredArgs.filter(a => a !== args[tagIdx] && a !== tag); }
86
+ }
87
+
88
+ const content = filteredArgs.join(' ');
71
89
  if (!content) {
72
90
  console.error('❌ Please provide content to save.');
73
91
  return;
@@ -78,7 +96,8 @@ async function saveMemory(args, memoryDir) {
78
96
 
79
97
  const entry = {
80
98
  timestamp: new Date().toISOString(),
81
- content: content
99
+ content: content,
100
+ ...(tag ? { tag } : {}),
82
101
  };
83
102
 
84
103
  // Ensure file exists for lockfile
@@ -100,7 +119,8 @@ async function saveMemory(args, memoryDir) {
100
119
 
101
120
  // Append entry as a new line in JSONL format
102
121
  await fs.appendFile(sessionFile, JSON.stringify(entry) + '\n', 'utf8');
103
- console.log(`βœ… Memory appended to session ${today} (JSONL)`);
122
+ const tagLabel = tag ? ` [#${tag}]` : '';
123
+ console.log(`βœ… Memory appended to session ${today}${tagLabel}`);
104
124
  } catch (err) {
105
125
  console.error(`❌ Failed to acquire lock for memory file: ${err.message}`);
106
126
  } finally {
@@ -120,6 +140,37 @@ async function listSessions(memoryDir) {
120
140
  files.forEach(f => console.log(` - ${f.replace('.jsonl', '').replace('.json', '')}`));
121
141
  }
122
142
 
143
+ /**
144
+ * Lists all unique tags across all sessions
145
+ */
146
+ async function listTags(memoryDir) {
147
+ const sessionsDir = path.join(memoryDir, 'sessions');
148
+ let files;
149
+ try { files = (await fs.readdir(sessionsDir)).filter(f => f.endsWith('.jsonl')); }
150
+ catch (_) { files = []; }
151
+
152
+ const tagCount = {};
153
+ for (const file of files) {
154
+ const raw = await fs.readFile(path.join(sessionsDir, file), 'utf8').catch(() => '');
155
+ for (const line of raw.split('\n').filter(l => l.trim())) {
156
+ try {
157
+ const entry = JSON.parse(line);
158
+ if (entry.tag) tagCount[entry.tag] = (tagCount[entry.tag] || 0) + 1;
159
+ } catch (_) { }
160
+ }
161
+ }
162
+
163
+ const tags = Object.entries(tagCount).sort((a, b) => b[1] - a[1]);
164
+ console.log('\n🏷️ Grimoire Memory Tags:\n');
165
+ if (tags.length === 0) {
166
+ console.log(' (nenhuma tag registrada)');
167
+ console.log(' Use: grimoire memory save --tag <nome> "texto"\n');
168
+ return;
169
+ }
170
+ tags.forEach(([tag, count]) => console.log(` #${tag.padEnd(20)} ${count}Γ—`));
171
+ console.log(`\n Para filtrar: grimoire memory search --tag <nome>\n`);
172
+ }
173
+
123
174
  /**
124
175
  * Shows session parsing JSONL or legacy JSON
125
176
  */
@@ -0,0 +1,200 @@
1
+ /**
2
+ * grimoire report β€” Daily / Weekly Consolidated Report
3
+ *
4
+ * grimoire report RelatΓ³rio de hoje
5
+ * grimoire report --period week Última semana (7 dias)
6
+ * grimoire report --period all HistΓ³rico completo
7
+ * grimoire report --md Exporta para .grimoire/reports/YYYY-MM-DD.md
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+
15
+ function findGrimoireDir() {
16
+ const cwd = process.cwd();
17
+ const direct = path.join(cwd, '.grimoire');
18
+ const sub = path.join(cwd, 'grimoire', '.grimoire');
19
+ if (fs.existsSync(direct)) return direct;
20
+ if (fs.existsSync(sub)) return sub;
21
+ return null;
22
+ }
23
+
24
+ // ── Date helpers ───────────────────────────────────────────────────────────────
25
+ function today() { return new Date().toISOString().split('T')[0]; }
26
+ function daysAgo(n) {
27
+ const d = new Date();
28
+ d.setDate(d.getDate() - n);
29
+ return d.toISOString().split('T')[0];
30
+ }
31
+ function dateRange(period) {
32
+ if (period === 'week') return { from: daysAgo(6), to: today(), label: 'semana' };
33
+ if (period === 'month') return { from: daysAgo(29), to: today(), label: 'mΓͺs' };
34
+ return { from: '2000-01-01', to: today(), label: 'histΓ³rico' };
35
+ }
36
+
37
+ // ── Helpers: read JSONL sessions ───────────────────────────────────────────────
38
+ function readSessionEntries(grimoireDir, from, to) {
39
+ const sessionsDir = path.join(grimoireDir, 'memory', 'sessions');
40
+ if (!fs.existsSync(sessionsDir)) return [];
41
+ const files = fs.readdirSync(sessionsDir)
42
+ .filter(f => f.endsWith('.jsonl') && f.slice(0, 10) >= from && f.slice(0, 10) <= to);
43
+ const entries = [];
44
+ for (const f of files) {
45
+ const raw = fs.readFileSync(path.join(sessionsDir, f), 'utf8');
46
+ for (const line of raw.split('\n').filter(l => l.trim())) {
47
+ try { entries.push({ date: f.slice(0, 10), ...JSON.parse(line) }); } catch (_) { }
48
+ }
49
+ }
50
+ return entries;
51
+ }
52
+
53
+ // ── Helpers: read metrics JSONL ────────────────────────────────────────────────
54
+ function readMetricsEntries(grimoireDir, from, to) {
55
+ const metricsDir = path.join(grimoireDir, 'metrics');
56
+ if (!fs.existsSync(metricsDir)) return [];
57
+ const files = fs.readdirSync(metricsDir)
58
+ .filter(f => f.endsWith('.jsonl') && f.slice(0, 10) >= from && f.slice(0, 10) <= to);
59
+ const entries = [];
60
+ for (const f of files) {
61
+ const raw = fs.readFileSync(path.join(metricsDir, f), 'utf8');
62
+ for (const line of raw.split('\n').filter(l => l.trim())) {
63
+ try { entries.push({ date: f.slice(0, 10), ...JSON.parse(line) }); } catch (_) { }
64
+ }
65
+ }
66
+ return entries;
67
+ }
68
+
69
+ // ── Read stories ───────────────────────────────────────────────────────────────
70
+ function readStories(grimoireDir, from, to) {
71
+ const storiesDir = path.join(grimoireDir, 'stories');
72
+ if (!fs.existsSync(storiesDir)) return [];
73
+ return fs.readdirSync(storiesDir)
74
+ .filter(f => f.endsWith('.json'))
75
+ .map(f => { try { return JSON.parse(fs.readFileSync(path.join(storiesDir, f), 'utf8')); } catch (_) { return null; } })
76
+ .filter(Boolean)
77
+ .filter(s => s.createdAt && s.createdAt.slice(0, 10) >= from);
78
+ }
79
+
80
+ // ── Build report object ────────────────────────────────────────────────────────
81
+ function buildReport(grimoireDir, period) {
82
+ const { from, to, label } = dateRange(period);
83
+ const memEntries = readSessionEntries(grimoireDir, from, to);
84
+ const metricEntries = readMetricsEntries(grimoireDir, from, to);
85
+ const stories = readStories(grimoireDir, from, to);
86
+
87
+ // Tag buckets
88
+ const byTag = {};
89
+ for (const e of memEntries) {
90
+ if (e.tag) {
91
+ byTag[e.tag] = byTag[e.tag] || [];
92
+ byTag[e.tag].push(e);
93
+ }
94
+ }
95
+
96
+ // Agent usage from metrics
97
+ const agentUse = {};
98
+ for (const e of metricEntries) {
99
+ if (e.agent) agentUse[e.agent] = (agentUse[e.agent] || 0) + 1;
100
+ if (e.type === 'agent_session' && e.agent) agentUse[e.agent] = (agentUse[e.agent] || 0) + 1;
101
+ }
102
+ const topAgents = Object.entries(agentUse).sort((a, b) => b[1] - a[1]).slice(0, 5);
103
+
104
+ // Metrics summary
105
+ const sessions = metricEntries.filter(e => e.type === 'session_start' || e.type === 'session' || e.type === 'agent_session').length;
106
+ const commits = metricEntries.filter(e => e.type === 'commit').length;
107
+ const done = stories.filter(s => s.status === 'done').length;
108
+ const open = stories.filter(s => s.status !== 'done').length;
109
+
110
+ return { from, to, label, memEntries, byTag, topAgents, sessions, commits, stories, done, open };
111
+ }
112
+
113
+ // ── Render text report ─────────────────────────────────────────────────────────
114
+ function renderReport(r) {
115
+ const lines = [];
116
+ const sep = '═'.repeat(50);
117
+ lines.push(`\nπŸ“Š Grimoire Report β€” ${r.to} (${r.label}: ${r.from} β†’ ${r.to})`);
118
+ lines.push(sep);
119
+
120
+ // Metrics
121
+ lines.push('\nπŸ“ˆ MΓ©tricas:');
122
+ lines.push(` SessΓ΅es: ${r.sessions} Commits: ${r.commits} Stories: ${r.done} concluΓ­das / ${r.open} abertas`);
123
+ if (r.topAgents.length) {
124
+ lines.push(` Agentes: ${r.topAgents.map(([a, n]) => `@${a} (${n}Γ—)`).join(' Β· ')}`);
125
+ }
126
+
127
+ // Memory by tag
128
+ const tagKeys = Object.keys(r.byTag);
129
+ if (tagKeys.length > 0) {
130
+ lines.push('\n🧠 Memória por tag:');
131
+ for (const tag of tagKeys) {
132
+ lines.push(`\n #${tag}:`);
133
+ for (const e of r.byTag[tag].slice(0, 5)) {
134
+ lines.push(` ${e.date} ${e.content}`);
135
+ }
136
+ }
137
+ }
138
+
139
+ // Untagged entries summary
140
+ const untagged = r.memEntries.filter(e => !e.tag);
141
+ if (untagged.length) {
142
+ lines.push(`\nπŸ—’οΈ MemΓ³rias sem tag: ${untagged.length} entradas`);
143
+ untagged.slice(0, 3).forEach(e => lines.push(` ${e.date} ${e.content}`));
144
+ if (untagged.length > 3) lines.push(` ... e mais ${untagged.length - 3}`);
145
+ }
146
+
147
+ // Stories
148
+ if (r.stories.length > 0) {
149
+ lines.push('\nπŸ“‹ Stories:');
150
+ r.stories.filter(s => s.status === 'done').forEach(s =>
151
+ lines.push(` βœ… [${s.id}] ${s.title} β€” concluΓ­da`)
152
+ );
153
+ r.stories.filter(s => s.status !== 'done').forEach(s =>
154
+ lines.push(` πŸ”„ [${s.id}] ${s.title} β€” em andamento`)
155
+ );
156
+ }
157
+
158
+ if (r.memEntries.length === 0 && r.sessions === 0 && r.stories.length === 0) {
159
+ lines.push('\n (nenhuma atividade registrada no perΓ­odo)');
160
+ lines.push(' Dica: grimoire memory save "texto" Β· grimoire metrics track session');
161
+ }
162
+
163
+ lines.push('\n' + sep + '\n');
164
+ return lines.join('\n');
165
+ }
166
+
167
+ // ── Main run ───────────────────────────────────────────────────────────────────
168
+ function run(args) {
169
+ const grimoireDir = findGrimoireDir();
170
+ if (!grimoireDir) {
171
+ console.error('❌ .grimoire/ not found. Run: npx grimoire-framework install');
172
+ return;
173
+ }
174
+
175
+ const periodIdx = args.findIndex(a => a === '--period' || a.startsWith('--period='));
176
+ let period = 'day';
177
+ if (periodIdx !== -1) {
178
+ period = args[periodIdx].includes('=')
179
+ ? args[periodIdx].split('=')[1]
180
+ : (args[periodIdx + 1] || 'day');
181
+ } else if (args.includes('week')) {
182
+ period = 'week';
183
+ }
184
+
185
+ const exportMd = args.includes('--md');
186
+ const report = buildReport(grimoireDir, period);
187
+ const text = renderReport(report);
188
+
189
+ console.log(text);
190
+
191
+ if (exportMd) {
192
+ const reportsDir = path.join(grimoireDir, 'reports');
193
+ if (!fs.existsSync(reportsDir)) fs.mkdirSync(reportsDir, { recursive: true });
194
+ const filename = path.join(reportsDir, `${report.to}-${period}.md`);
195
+ fs.writeFileSync(filename, text.replace(/\n/g, '\n'), 'utf8');
196
+ console.log(`βœ… RelatΓ³rio exportado: ${path.relative(process.cwd(), filename)}\n`);
197
+ }
198
+ }
199
+
200
+ module.exports = { run };