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.
- package/.grimoire/install-manifest.yaml +2 -2
- package/README.md +150 -112
- package/bin/commands/backup.js +165 -0
- package/bin/commands/exportall.js +200 -0
- package/bin/commands/memory.js +14 -0
- package/bin/commands/report.js +200 -0
- package/bin/commands/search.js +171 -0
- package/bin/commands/session.js +217 -0
- package/bin/commands/story.js +254 -0
- package/bin/grimoire-cli.js +193 -55
- package/package.json +1 -1
|
@@ -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 };
|
package/bin/commands/memory.js
CHANGED
|
@@ -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 };
|