grimoire-framework 1.2.0 → 1.4.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.
@@ -0,0 +1,200 @@
1
+ /**
2
+ * grimoire export — Full Export
3
+ *
4
+ * grimoire export --all Markdown bundle de tudo
5
+ * grimoire export --all --json JSON bundle de tudo
6
+ * grimoire export --all --out <dir> Diretório de saída customizado
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+
14
+ function findGrimoireDir() {
15
+ const cwd = process.cwd();
16
+ const direct = path.join(cwd, '.grimoire');
17
+ const sub = path.join(cwd, 'grimoire', '.grimoire');
18
+ if (fs.existsSync(direct)) return direct;
19
+ if (fs.existsSync(sub)) return sub;
20
+ return null;
21
+ }
22
+
23
+ // ── Loaders ───────────────────────────────────────────────────────────────────
24
+ function loadAllStories(grimoireDir) {
25
+ const dir = path.join(grimoireDir, 'stories');
26
+ if (!fs.existsSync(dir)) return [];
27
+ return fs.readdirSync(dir)
28
+ .filter(f => f.endsWith('.json'))
29
+ .map(f => { try { return JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')); } catch (_) { return null; } })
30
+ .filter(Boolean)
31
+ .sort((a, b) => a.createdAt > b.createdAt ? 1 : -1);
32
+ }
33
+
34
+ function loadAllMemory(grimoireDir) {
35
+ const sessionsDir = path.join(grimoireDir, 'memory', 'sessions');
36
+ if (!fs.existsSync(sessionsDir)) return [];
37
+ const entries = [];
38
+ const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl')).sort();
39
+ for (const f of files) {
40
+ const raw = fs.readFileSync(path.join(sessionsDir, f), 'utf8');
41
+ for (const line of raw.split('\n').filter(l => l.trim())) {
42
+ try { entries.push({ date: f.slice(0, 10), ...JSON.parse(line) }); } catch (_) { }
43
+ }
44
+ }
45
+ return entries;
46
+ }
47
+
48
+ function loadMetrics(grimoireDir) {
49
+ const dir = path.join(grimoireDir, 'metrics');
50
+ if (!fs.existsSync(dir)) return { sessions: 0, commits: 0, storiesDone: 0 };
51
+ let sessions = 0, commits = 0, storiesDone = 0;
52
+ for (const f of fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'))) {
53
+ const raw = fs.readFileSync(path.join(dir, f), 'utf8');
54
+ for (const line of raw.split('\n').filter(l => l.trim())) {
55
+ try {
56
+ const e = JSON.parse(line);
57
+ if (e.type === 'session_start' || e.type === 'agent_session') sessions++;
58
+ if (e.type === 'commit') commits++;
59
+ if (e.type === 'story_complete') storiesDone++;
60
+ } catch (_) { }
61
+ }
62
+ }
63
+ return { sessions, commits, storiesDone };
64
+ }
65
+
66
+ // ── Markdown export ───────────────────────────────────────────────────────────
67
+ function buildMarkdown(stories, memory, metrics, projectName) {
68
+ const now = new Date().toISOString().split('T')[0];
69
+ const lines = [];
70
+
71
+ lines.push(`# Grimoire Export — ${projectName}`);
72
+ lines.push(`\n> Exportado em: ${now}\n`);
73
+
74
+ // Stats
75
+ lines.push(`## 📈 Resumo\n`);
76
+ lines.push(`| Métrica | Valor |`);
77
+ lines.push(`|---|---|`);
78
+ lines.push(`| Stories totais | ${stories.length} |`);
79
+ lines.push(`| Sessões | ${metrics.sessions} |`);
80
+ lines.push(`| Commits | ${metrics.commits} |`);
81
+ lines.push(`| Stories concluídas | ${metrics.storiesDone} |`);
82
+ lines.push(`| Entradas de memória | ${memory.length} |`);
83
+ lines.push('');
84
+
85
+ // Stories
86
+ lines.push(`## 📋 Stories\n`);
87
+ const openStories = stories.filter(s => s.status !== 'done');
88
+ const doneStories = stories.filter(s => s.status === 'done');
89
+
90
+ if (openStories.length) {
91
+ lines.push(`### Em andamento\n`);
92
+ openStories.forEach(s => {
93
+ lines.push(`#### 🔄 [${s.id}] ${s.title}`);
94
+ lines.push(`- **Criada:** ${s.createdAt.slice(0, 10)}`);
95
+ if (s.notes && s.notes.length) {
96
+ lines.push(`- **Notas:**`);
97
+ s.notes.forEach(n => lines.push(` - ${n}`));
98
+ }
99
+ lines.push('');
100
+ });
101
+ }
102
+ if (doneStories.length) {
103
+ lines.push(`### Concluídas\n`);
104
+ doneStories.forEach(s => {
105
+ lines.push(`- ✅ **[${s.id}]** ${s.title}${s.doneAt ? ` — ${s.doneAt.slice(0, 10)}` : ''}`);
106
+ });
107
+ lines.push('');
108
+ }
109
+
110
+ // Memory by tag
111
+ lines.push(`## 🧠 Memória\n`);
112
+ const byTag = {};
113
+ const noTag = [];
114
+ for (const e of memory) {
115
+ if (e.tag) { byTag[e.tag] = byTag[e.tag] || []; byTag[e.tag].push(e); }
116
+ else noTag.push(e);
117
+ }
118
+
119
+ for (const [tag, entries] of Object.entries(byTag)) {
120
+ lines.push(`### #${tag}\n`);
121
+ entries.forEach(e => {
122
+ const story = e.story ? ` \`[${e.story}]\`` : '';
123
+ lines.push(`- **${e.date}** ${e.content}${story}`);
124
+ });
125
+ lines.push('');
126
+ }
127
+
128
+ if (noTag.length) {
129
+ lines.push(`### Sem tag\n`);
130
+ noTag.slice(0, 20).forEach(e => lines.push(`- **${e.date}** ${e.content}`));
131
+ if (noTag.length > 20) lines.push(`- *(e mais ${noTag.length - 20} entradas...)*`);
132
+ lines.push('');
133
+ }
134
+
135
+ return lines.join('\n');
136
+ }
137
+
138
+ // ── JSON export ───────────────────────────────────────────────────────────────
139
+ function buildJSON(stories, memory, metrics, projectName) {
140
+ return JSON.stringify({
141
+ exportedAt: new Date().toISOString(),
142
+ project: projectName,
143
+ metrics,
144
+ stories,
145
+ memory,
146
+ }, null, 2);
147
+ }
148
+
149
+ // ── run ───────────────────────────────────────────────────────────────────────
150
+ function run(args) {
151
+ if (!args.includes('--all')) {
152
+ console.log('\nUsage:\n');
153
+ console.log(' grimoire export --all Markdown bundle de tudo');
154
+ console.log(' grimoire export --all --json JSON bundle de tudo\n');
155
+ return;
156
+ }
157
+
158
+ const grimoireDir = findGrimoireDir();
159
+ if (!grimoireDir) {
160
+ console.error('❌ .grimoire/ not found. Run: npx grimoire-framework install');
161
+ return;
162
+ }
163
+
164
+ // Project name
165
+ let projectName = path.basename(process.cwd());
166
+ try {
167
+ const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
168
+ projectName = pkg.name || projectName;
169
+ } catch (_) { }
170
+
171
+ // Output dir
172
+ const outIdx = args.indexOf('--out');
173
+ const outDir = outIdx !== -1 && args[outIdx + 1]
174
+ ? args[outIdx + 1]
175
+ : path.join(grimoireDir, 'exports');
176
+ if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
177
+
178
+ const now = new Date().toISOString().split('T')[0];
179
+ const isJson = args.includes('--json');
180
+ const ext = isJson ? 'json' : 'md';
181
+ const outFile = path.join(outDir, `${now}-export.${ext}`);
182
+
183
+ console.log('\n⏳ Exportando dados do .grimoire/ ...\n');
184
+
185
+ const stories = loadAllStories(grimoireDir);
186
+ const memory = loadAllMemory(grimoireDir);
187
+ const metrics = loadMetrics(grimoireDir);
188
+
189
+ const content = isJson
190
+ ? buildJSON(stories, memory, metrics, projectName)
191
+ : buildMarkdown(stories, memory, metrics, projectName);
192
+
193
+ fs.writeFileSync(outFile, content, 'utf8');
194
+
195
+ const rel = path.relative(process.cwd(), outFile);
196
+ console.log(`✅ Export gerado: ${rel}`);
197
+ console.log(` Stories: ${stories.length} · Memória: ${memory.length} · Formato: ${isJson ? 'JSON' : 'Markdown'}\n`);
198
+ }
199
+
200
+ module.exports = { run };
@@ -85,6 +85,19 @@ async function saveMemory(args, memoryDir) {
85
85
  else { filteredArgs = filteredArgs.filter(a => a !== args[tagIdx] && a !== tag); }
86
86
  }
87
87
 
88
+ // Extract --story value if present
89
+ const storyIdx = filteredArgs.findIndex(a => a === '--story' || a.startsWith('--story='));
90
+ let storyId = null;
91
+ if (storyIdx !== -1) {
92
+ if (filteredArgs[storyIdx].includes('=')) {
93
+ storyId = filteredArgs[storyIdx].split('=')[1];
94
+ filteredArgs.splice(storyIdx, 1);
95
+ } else {
96
+ storyId = filteredArgs[storyIdx + 1];
97
+ filteredArgs.splice(storyIdx, 2);
98
+ }
99
+ }
100
+
88
101
  const content = filteredArgs.join(' ');
89
102
  if (!content) {
90
103
  console.error('❌ Please provide content to save.');
@@ -98,6 +111,7 @@ async function saveMemory(args, memoryDir) {
98
111
  timestamp: new Date().toISOString(),
99
112
  content: content,
100
113
  ...(tag ? { tag } : {}),
114
+ ...(storyId ? { story: storyId } : {}),
101
115
  };
102
116
 
103
117
  // Ensure file exists for lockfile
@@ -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 };
@@ -0,0 +1,171 @@
1
+ /**
2
+ * grimoire search — Global Search
3
+ *
4
+ * grimoire search "termo" Busca em memory + stories + agents
5
+ * grimoire search "JWT" --memory Só memória
6
+ * grimoire search "JWT" --stories Só stories
7
+ * grimoire search "JWT" --agents Só agentes
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
+ function findAgentsDir() {
25
+ const cwd = process.cwd();
26
+ const dirs = [
27
+ path.join(cwd, '.codex', 'agents'),
28
+ path.join(cwd, 'node_modules', 'grimoire-framework', '.codex', 'agents'),
29
+ ];
30
+ return dirs.find(d => fs.existsSync(d)) || null;
31
+ }
32
+
33
+ function hl(text, query) {
34
+ if (!query) return text;
35
+ const re = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
36
+ return text.replace(re, m => `\x1b[33m${m}\x1b[0m`);
37
+ }
38
+
39
+ // ── Search memory sessions ────────────────────────────────────────────────────
40
+ function searchMemory(grimoireDir, query) {
41
+ const sessionsDir = path.join(grimoireDir, 'memory', 'sessions');
42
+ if (!fs.existsSync(sessionsDir)) return [];
43
+ const lq = query.toLowerCase();
44
+ const results = [];
45
+ const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl')).sort().reverse();
46
+ for (const f of files) {
47
+ const raw = fs.readFileSync(path.join(sessionsDir, f), 'utf8');
48
+ for (const line of raw.split('\n').filter(l => l.trim())) {
49
+ try {
50
+ const e = JSON.parse(line);
51
+ if ((e.content || '').toLowerCase().includes(lq)) {
52
+ results.push({ date: f.slice(0, 10), content: e.content, tag: e.tag || null, story: e.story || null });
53
+ }
54
+ } catch (_) { }
55
+ }
56
+ if (results.length >= 20) break;
57
+ }
58
+ return results;
59
+ }
60
+
61
+ // ── Search stories ────────────────────────────────────────────────────────────
62
+ function searchStories(grimoireDir, query) {
63
+ const storiesDir = path.join(grimoireDir, 'stories');
64
+ if (!fs.existsSync(storiesDir)) return [];
65
+ const lq = query.toLowerCase();
66
+ return fs.readdirSync(storiesDir)
67
+ .filter(f => f.endsWith('.json'))
68
+ .map(f => { try { return JSON.parse(fs.readFileSync(path.join(storiesDir, f), 'utf8')); } catch (_) { return null; } })
69
+ .filter(Boolean)
70
+ .filter(s => (s.title || '').toLowerCase().includes(lq) ||
71
+ (s.notes || []).some(n => n.toLowerCase().includes(lq)));
72
+ }
73
+
74
+ // ── Search agents ─────────────────────────────────────────────────────────────
75
+ function searchAgents(agentsDir, query) {
76
+ if (!agentsDir) return [];
77
+ const lq = query.toLowerCase();
78
+ const results = [];
79
+ for (const f of fs.readdirSync(agentsDir).filter(f => f.endsWith('.md'))) {
80
+ const raw = fs.readFileSync(path.join(agentsDir, f), 'utf8').toLowerCase();
81
+ if (raw.includes(lq)) {
82
+ const id = f.replace('.md', '');
83
+ // Extract name from yaml block
84
+ let name = id;
85
+ const nameMatch = raw.match(/name:\s*([^\n]+)/);
86
+ if (nameMatch) name = nameMatch[1].trim();
87
+ const titleMatch = raw.match(/title:\s*([^\n]+)/);
88
+ const title = titleMatch ? titleMatch[1].trim() : '';
89
+ results.push({ id, name, title });
90
+ }
91
+ }
92
+ return results;
93
+ }
94
+
95
+ // ── run ───────────────────────────────────────────────────────────────────────
96
+ function run(args) {
97
+ const query = args.filter(a => !a.startsWith('-')).join(' ');
98
+ if (!query) {
99
+ console.log('Usage: grimoire search "termo"\n');
100
+ console.log('Busca em: memória · stories · agentes\n');
101
+ return;
102
+ }
103
+
104
+ const onlyMemory = args.includes('--memory');
105
+ const onlyStories = args.includes('--stories');
106
+ const onlyAgents = args.includes('--agents');
107
+ const all = !onlyMemory && !onlyStories && !onlyAgents;
108
+
109
+ const grimoireDir = findGrimoireDir();
110
+ const agentsDir = findAgentsDir();
111
+
112
+ let totalResults = 0;
113
+
114
+ console.log(`\n🔍 Grimoire Search — "${query}"\n${'─'.repeat(44)}`);
115
+
116
+ // Memory
117
+ if (all || onlyMemory) {
118
+ if (grimoireDir) {
119
+ const memResults = searchMemory(grimoireDir, query);
120
+ if (memResults.length > 0) {
121
+ console.log('\n🧠 Memória:');
122
+ for (const r of memResults) {
123
+ const tag = r.tag ? ` [#${r.tag}]` : '';
124
+ const story = r.story ? ` [${r.story}]` : '';
125
+ console.log(` ${r.date} ${hl(r.content, query)}${tag}${story}`);
126
+ }
127
+ totalResults += memResults.length;
128
+ }
129
+ }
130
+ }
131
+
132
+ // Stories
133
+ if (all || onlyStories) {
134
+ if (grimoireDir) {
135
+ const storyResults = searchStories(grimoireDir, query);
136
+ if (storyResults.length > 0) {
137
+ console.log('\n📋 Stories:');
138
+ for (const s of storyResults) {
139
+ const icon = s.status === 'done' ? '✅' : '🔄';
140
+ console.log(` ${icon} [${s.id}] ${hl(s.title, query)}`);
141
+ for (const n of (s.notes || []).filter(n => n.toLowerCase().includes(query.toLowerCase()))) {
142
+ console.log(` 📝 ${hl(n, query)}`);
143
+ }
144
+ }
145
+ totalResults += storyResults.length;
146
+ }
147
+ }
148
+ }
149
+
150
+ // Agents
151
+ if (all || onlyAgents) {
152
+ const agentResults = searchAgents(agentsDir, query);
153
+ if (agentResults.length > 0) {
154
+ console.log('\n🤖 Agentes:');
155
+ for (const a of agentResults) {
156
+ console.log(` @${hl(a.id, query)} — ${a.name}${a.title ? ' ' + a.title : ''}`);
157
+ }
158
+ totalResults += agentResults.length;
159
+ }
160
+ }
161
+
162
+ if (totalResults === 0) {
163
+ console.log('\n (nenhum resultado encontrado)');
164
+ console.log(` Dica: tente termos mais curtos ou use --memory / --stories / --agents\n`);
165
+ } else {
166
+ console.log(`\n${'─'.repeat(44)}`);
167
+ console.log(` ${totalResults} resultado(s)\n`);
168
+ }
169
+ }
170
+
171
+ module.exports = { run };