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,217 @@
1
+ /**
2
+ * grimoire session — Session Bootstrap Command
3
+ *
4
+ * grimoire session start Gera prompt de início de sessão
5
+ * grimoire session start --squad <squad> Com squad específico
6
+ * grimoire session start --copy (legacy alias)
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
+ function findSquadsDir() {
24
+ const cwd = process.cwd();
25
+ const home = path.join(cwd, '.grimoire', 'squads');
26
+ const local = path.join(cwd, 'squads');
27
+ const pkg = path.join(cwd, 'node_modules', 'grimoire-framework', 'squads');
28
+ if (fs.existsSync(home)) return home;
29
+ if (fs.existsSync(local)) return local;
30
+ if (fs.existsSync(pkg)) return pkg;
31
+ return null;
32
+ }
33
+
34
+ function readSquadYaml(squadDir) {
35
+ const yamlFile = path.join(squadDir, 'squad.yaml');
36
+ if (!fs.existsSync(yamlFile)) return null;
37
+ const raw = fs.readFileSync(yamlFile, 'utf8');
38
+ const squad = { agents: [] };
39
+ for (const line of raw.split('\n')) {
40
+ const s = line.trim();
41
+ if (s.startsWith('name:')) squad.name = s.slice(5).trim().replace(/^"|"$/g, '');
42
+ if (s.startsWith('description:')) squad.description = s.slice(12).trim().replace(/^"|"$/g, '');
43
+ if (s.startsWith('- ') && squad._inAgents) squad.agents.push(s.slice(2).trim());
44
+ if (s === 'agents:') squad._inAgents = true;
45
+ else if (s.endsWith(':')) squad._inAgents = false;
46
+ }
47
+ delete squad._inAgents;
48
+ return squad;
49
+ }
50
+
51
+ function getLastMemoryEntries(grimoireDir, tag, n = 3) {
52
+ const sessionsDir = path.join(grimoireDir, 'memory', 'sessions');
53
+ if (!fs.existsSync(sessionsDir)) return [];
54
+ const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl')).sort().reverse();
55
+ const results = [];
56
+ for (const f of files) {
57
+ if (results.length >= n) break;
58
+ const raw = fs.readFileSync(path.join(sessionsDir, f), 'utf8');
59
+ const lines = raw.split('\n').filter(l => l.trim()).reverse();
60
+ for (const line of lines) {
61
+ if (results.length >= n) break;
62
+ try {
63
+ const e = JSON.parse(line);
64
+ if (!tag || e.tag === tag) results.push(e);
65
+ } catch (_) { }
66
+ }
67
+ }
68
+ return results;
69
+ }
70
+
71
+ function loadConfig(grimoireDir) {
72
+ const file = path.join(grimoireDir, 'config.yaml');
73
+ if (!fs.existsSync(file)) return {};
74
+ const result = {};
75
+ for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
76
+ const s = line.trim();
77
+ if (!s || s.startsWith('#')) continue;
78
+ const i = s.indexOf(':');
79
+ if (i === -1) continue;
80
+ let val = s.slice(i + 1).trim();
81
+ if (val === 'true') val = true;
82
+ else if (val === 'false') val = false;
83
+ result[s.slice(0, i).trim()] = val;
84
+ }
85
+ return result;
86
+ }
87
+
88
+ // ── run ───────────────────────────────────────────────────────────────────────
89
+ function run(args) {
90
+ const sub = args[0];
91
+ if (sub === 'start' || !sub) {
92
+ sessionStart(args.slice(sub === 'start' ? 1 : 0));
93
+ } else {
94
+ console.log('Usage: grimoire session start [--squad <squad>]\n');
95
+ }
96
+ }
97
+
98
+ function sessionStart(args) {
99
+ const grimoireDir = findGrimoireDir();
100
+ if (!grimoireDir) {
101
+ console.error('❌ .grimoire/ not found. Run: npx grimoire-framework install');
102
+ return;
103
+ }
104
+
105
+ // Detect project name
106
+ let projectName = '';
107
+ try {
108
+ const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
109
+ projectName = pkg.name || '';
110
+ } catch (_) { }
111
+
112
+ // Squad
113
+ const squadIdx = args.findIndex(a => a === '--squad' || a.startsWith('--squad='));
114
+ let squadName = null;
115
+ if (squadIdx !== -1) {
116
+ squadName = args[squadIdx].includes('=') ? args[squadIdx].split('=')[1] : args[squadIdx + 1];
117
+ }
118
+ // Fallback to config
119
+ const config = loadConfig(grimoireDir);
120
+ if (!squadName && config.default_squad) squadName = config.default_squad;
121
+
122
+ let squadInfo = null;
123
+ if (squadName) {
124
+ const squadsDir = findSquadsDir();
125
+ if (squadsDir && fs.existsSync(path.join(squadsDir, squadName))) {
126
+ squadInfo = readSquadYaml(path.join(squadsDir, squadName));
127
+ if (squadInfo) squadInfo.id = squadName;
128
+ }
129
+ }
130
+
131
+ // Active stories
132
+ let activeStories = [];
133
+ try {
134
+ const { getActiveStories } = require('./story');
135
+ activeStories = getActiveStories(grimoireDir).slice(0, 5);
136
+ } catch (_) { }
137
+
138
+ // Recent memory by tag
139
+ const decisions = getLastMemoryEntries(grimoireDir, 'decisão', 3);
140
+ const patterns = getLastMemoryEntries(grimoireDir, 'padrão', 2);
141
+
142
+ // Default agent
143
+ const defaultAgent = config.default_agent || 'dev';
144
+
145
+ // Build prompt
146
+ const now = new Date();
147
+ const dateStr = now.toLocaleDateString('pt-BR');
148
+ const timeStr = now.toTimeString().slice(0, 5);
149
+ const sep = '═'.repeat(52);
150
+
151
+ const lines = [];
152
+ lines.push(`${sep}`);
153
+ lines.push(`🚀 INÍCIO DE SESSÃO — ${projectName || 'projeto'}`);
154
+ lines.push(`Data: ${dateStr} ${timeStr}${squadInfo ? ` | Squad: ${squadInfo.id}` : ''}`);
155
+ lines.push('');
156
+
157
+ if (squadInfo) {
158
+ const agentList = squadInfo.agents.map(a => `@${a}`).join(' · ');
159
+ lines.push(`Squad ativo: ${agentList}`);
160
+ } else {
161
+ lines.push(`Agente padrão: @${defaultAgent}`);
162
+ }
163
+
164
+ if (config.metrics_enabled !== false) lines.push('Métricas: ✅ ativas');
165
+ if (config.hooks_enabled) lines.push('Hooks: ✅ instalado');
166
+ lines.push('');
167
+
168
+ if (activeStories.length > 0) {
169
+ lines.push('Stories abertas:');
170
+ activeStories.forEach(s => lines.push(` [IN PROGRESS] [${s.id}] — ${s.title}`));
171
+ lines.push('');
172
+ }
173
+
174
+ if (decisions.length > 0) {
175
+ lines.push('Últimas decisões (#decisão):');
176
+ decisions.forEach(d => lines.push(` - ${d.content}`));
177
+ lines.push('');
178
+ }
179
+
180
+ if (patterns.length > 0) {
181
+ lines.push('Padrões ativos (#padrão):');
182
+ patterns.forEach(p => lines.push(` - ${p.content}`));
183
+ lines.push('');
184
+ }
185
+
186
+ const firstStory = activeStories[0];
187
+ const firstAgent = squadInfo?.agents[0] || defaultAgent;
188
+ if (firstStory) {
189
+ lines.push(`Contexto: Continue o desenvolvimento com base nas stories abertas.`);
190
+ lines.push(`Para iniciar: @${firstAgent} apresente o estado atual de [${firstStory.id}] ${firstStory.title}.`);
191
+ } else {
192
+ lines.push(`Para iniciar: @${firstAgent} apresente-se e sugira o próximo passo.`);
193
+ }
194
+
195
+ lines.push(sep);
196
+
197
+ const prompt = lines.join('\n');
198
+
199
+ console.log('\n📋 Prompt de sessão (copie e cole no chat da IDE):\n');
200
+ console.log(prompt);
201
+ console.log('\n💡 Atalhos:');
202
+ console.log(' grimoire story create "título" ← criar nova story');
203
+ console.log(' grimoire memory save "observação" ← salvar decisão');
204
+ console.log(' grimoire report --period week ← relatório da semana\n');
205
+
206
+ // Track session start in metrics
207
+ try {
208
+ const metricsDir = path.join(grimoireDir, 'metrics');
209
+ if (fs.existsSync(metricsDir)) {
210
+ const today = now.toISOString().split('T')[0];
211
+ const mFile = path.join(metricsDir, `${today}.jsonl`);
212
+ fs.appendFileSync(mFile, JSON.stringify({ timestamp: now.toISOString(), type: 'session_start', squad: squadName || null }) + '\n');
213
+ }
214
+ } catch (_) { }
215
+ }
216
+
217
+ module.exports = { run };
@@ -0,0 +1,254 @@
1
+ /**
2
+ * grimoire story — Story Tracking CLI
3
+ *
4
+ * grimoire story create "Título da story" Cria nova story
5
+ * grimoire story list Lista stories ativas
6
+ * grimoire story list --all Lista todas (inclui concluídas)
7
+ * grimoire story done <id> Marca story como concluída
8
+ * grimoire story show <id> Detalhes da story
9
+ * grimoire story delete <id> Remove story
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const path = require('path');
15
+ const fs = require('fs');
16
+
17
+ function findGrimoireDir() {
18
+ const cwd = process.cwd();
19
+ const direct = path.join(cwd, '.grimoire');
20
+ const sub = path.join(cwd, 'grimoire', '.grimoire');
21
+ if (fs.existsSync(direct)) return direct;
22
+ if (fs.existsSync(sub)) return sub;
23
+ return null;
24
+ }
25
+
26
+ function getStoriesDir(grimoireDir) {
27
+ const d = path.join(grimoireDir, 'stories');
28
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
29
+ return d;
30
+ }
31
+
32
+ function loadStories(storiesDir) {
33
+ return fs.readdirSync(storiesDir)
34
+ .filter(f => f.endsWith('.json'))
35
+ .map(f => {
36
+ try { return JSON.parse(fs.readFileSync(path.join(storiesDir, f), 'utf8')); }
37
+ catch (_) { return null; }
38
+ })
39
+ .filter(Boolean)
40
+ .sort((a, b) => a.createdAt > b.createdAt ? 1 : -1);
41
+ }
42
+
43
+ function nextId(stories) {
44
+ const nums = stories.map(s => parseInt(s.id.replace(/\D/g, '')) || 0);
45
+ const max = nums.length ? Math.max(...nums) : 0;
46
+ return `US-${String(max + 1).padStart(3, '0')}`;
47
+ }
48
+
49
+ // ── run ───────────────────────────────────────────────────────────────────────
50
+ function run(args) {
51
+ const sub = args[0];
52
+ const rest = args.slice(1);
53
+
54
+ const grimoireDir = findGrimoireDir();
55
+ if (!grimoireDir) {
56
+ console.error('❌ .grimoire/ not found. Run: npx grimoire-framework install');
57
+ return;
58
+ }
59
+ const storiesDir = getStoriesDir(grimoireDir);
60
+
61
+ switch (sub) {
62
+ case 'create': storyCreate(rest, storiesDir, grimoireDir); break;
63
+ case 'done': storyDone(rest[0], storiesDir, grimoireDir); break;
64
+ case 'note': storyNote(rest[0], rest.slice(1), storiesDir); break;
65
+ case 'delete':
66
+ case 'remove': storyDelete(rest[0], storiesDir); break;
67
+ case 'show': storyShow(rest[0], storiesDir, grimoireDir); break;
68
+ case 'list':
69
+ default: storyList(rest, storiesDir); break;
70
+ }
71
+ }
72
+
73
+ // ── create ────────────────────────────────────────────────────────────────────
74
+ function storyCreate(args, storiesDir, grimoireDir) {
75
+ const title = args.filter(a => !a.startsWith('--')).join(' ');
76
+ if (!title) {
77
+ console.log('Usage: grimoire story create "Título da story"\n');
78
+ return;
79
+ }
80
+ const stories = loadStories(storiesDir);
81
+ const id = nextId(stories);
82
+ const story = {
83
+ id,
84
+ title,
85
+ status: 'in_progress',
86
+ createdAt: new Date().toISOString(),
87
+ doneAt: null,
88
+ tags: [],
89
+ notes: [],
90
+ };
91
+ const filename = path.join(storiesDir, `${id}.json`);
92
+ fs.writeFileSync(filename, JSON.stringify(story, null, 2), 'utf8');
93
+
94
+ // Track in metrics
95
+ try {
96
+ const metricsDir = path.join(grimoireDir, 'metrics');
97
+ if (fs.existsSync(metricsDir)) {
98
+ const today = new Date().toISOString().split('T')[0];
99
+ const mFile = path.join(metricsDir, `${today}.jsonl`);
100
+ fs.appendFileSync(mFile, JSON.stringify({ timestamp: new Date().toISOString(), type: 'story_create', story: id, title }) + '\n');
101
+ }
102
+ } catch (_) { }
103
+
104
+ console.log(`\n✅ Story criada: [${id}] ${title}\n`);
105
+ console.log(` Status: 🔄 em andamento`);
106
+ console.log(` Para concluir: grimoire story done ${id}`);
107
+ console.log(` Para ver: grimoire story show ${id}\n`);
108
+ }
109
+
110
+ // ── list ──────────────────────────────────────────────────────────────────────
111
+ function storyList(args, storiesDir) {
112
+ const all = args.includes('--all');
113
+ const stories = loadStories(storiesDir);
114
+ const filtered = all ? stories : stories.filter(s => s.status !== 'done');
115
+
116
+ console.log(`\n📋 Stories${all ? '' : ' ativas'} (${filtered.length}):\n`);
117
+
118
+ if (filtered.length === 0) {
119
+ if (!all) {
120
+ console.log(' (nenhuma story ativa)');
121
+ console.log(' grimoire story create "Título" para criar uma nova\n');
122
+ } else {
123
+ console.log(' (nenhuma story)\n');
124
+ }
125
+ return;
126
+ }
127
+
128
+ const done = filtered.filter(s => s.status === 'done');
129
+ const open = filtered.filter(s => s.status !== 'done');
130
+
131
+ open.forEach(s => console.log(` 🔄 [${s.id}] ${s.title}`));
132
+ done.forEach(s => {
133
+ const d = s.doneAt ? ` — concluída ${s.doneAt.slice(0, 10)}` : '';
134
+ console.log(` ✅ [${s.id}] ${s.title}${d}`);
135
+ });
136
+ console.log(`\n grimoire story done <id> ← marcar como concluída`);
137
+ console.log(` grimoire story show <id> ← ver detalhes\n`);
138
+ }
139
+
140
+ // ── done ──────────────────────────────────────────────────────────────────────
141
+ function storyDone(id, storiesDir, grimoireDir) {
142
+ if (!id) { console.log('Usage: grimoire story done <id>\nEx: grimoire story done US-001\n'); return; }
143
+ const storyFile = path.join(storiesDir, id.endsWith('.json') ? id : `${id}.json`);
144
+ if (!fs.existsSync(storyFile)) {
145
+ console.log(`❌ Story "${id}" not found. Use: grimoire story list\n`); return;
146
+ }
147
+ const story = JSON.parse(fs.readFileSync(storyFile, 'utf8'));
148
+ story.status = 'done';
149
+ story.doneAt = new Date().toISOString();
150
+ fs.writeFileSync(storyFile, JSON.stringify(story, null, 2), 'utf8');
151
+
152
+ // Track in metrics
153
+ try {
154
+ const metricsDir = path.join(grimoireDir, 'metrics');
155
+ if (fs.existsSync(metricsDir)) {
156
+ const today = new Date().toISOString().split('T')[0];
157
+ const mFile = path.join(metricsDir, `${today}.jsonl`);
158
+ fs.appendFileSync(mFile, JSON.stringify({ timestamp: new Date().toISOString(), type: 'story_complete', story: id, title: story.title }) + '\n');
159
+ }
160
+ } catch (_) { }
161
+
162
+ console.log(`\n✅ [${story.id}] ${story.title} — marcada como concluída! 🎉\n`);
163
+ }
164
+
165
+ // ── note ─────────────────────────────────────────────────────────────────────────
166
+ function storyNote(id, args, storiesDir) {
167
+ if (!id) { console.log('Usage: grimoire story note <id> "nota"\n'); return; }
168
+ const note = args.filter(a => !a.startsWith('-')).join(' ');
169
+ if (!note) { console.log('Usage: grimoire story note <id> "nota"\n'); return; }
170
+ const storyFile = path.join(storiesDir, id.endsWith('.json') ? id : `${id}.json`);
171
+ if (!fs.existsSync(storyFile)) { console.log(`❌ Story "${id}" not found.\n`); return; }
172
+ const story = JSON.parse(fs.readFileSync(storyFile, 'utf8'));
173
+ story.notes = story.notes || [];
174
+ story.notes.push(note);
175
+ fs.writeFileSync(storyFile, JSON.stringify(story, null, 2), 'utf8');
176
+ console.log(`\n📝 Nota adicionada a [${id}]:\n ${note}\n`);
177
+ }
178
+
179
+ // ── show ─────────────────────────────────────────────────────────────────────────
180
+ function storyShow(id, storiesDir, grimoireDir) {
181
+ if (!id) { storyList([], storiesDir); return; }
182
+ const storyFile = path.join(storiesDir, id.endsWith('.json') ? id : `${id}.json`);
183
+ if (!fs.existsSync(storyFile)) {
184
+ console.log(`❌ Story "${id}" not found.\n`); return;
185
+ }
186
+ const s = JSON.parse(fs.readFileSync(storyFile, 'utf8'));
187
+ const statusIcon = s.status === 'done' ? '✅' : '🔄';
188
+ console.log(`\n${statusIcon} [${s.id}] ${s.title}`);
189
+ console.log(`${'\u2500'.repeat(50)}`);
190
+ console.log(` Status: ${s.status}`);
191
+ console.log(` Criada: ${s.createdAt.slice(0, 10)}`);
192
+ if (s.doneAt) console.log(` Concluída: ${s.doneAt.slice(0, 10)}`);
193
+ if (s.notes && s.notes.length) {
194
+ console.log('\n 📝 Notas:');
195
+ s.notes.forEach(n => console.log(` - ${n}`));
196
+ }
197
+ // Linked memory entries
198
+ if (grimoireDir) {
199
+ const sessionsDir = path.join(grimoireDir, 'memory', 'sessions');
200
+ if (fs.existsSync(sessionsDir)) {
201
+ const linked = [];
202
+ for (const f of fs.readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl'))) {
203
+ const raw = fs.readFileSync(path.join(sessionsDir, f), 'utf8');
204
+ for (const line of raw.split('\n').filter(l => l.trim())) {
205
+ try {
206
+ const e = JSON.parse(line);
207
+ if (e.story === s.id) linked.push({ date: f.slice(0, 10), ...e });
208
+ } catch (_) { }
209
+ }
210
+ }
211
+ if (linked.length > 0) {
212
+ console.log('\n 🧠 Memórias linkadas:');
213
+ linked.forEach(e => {
214
+ const tag = e.tag ? ` [#${e.tag}]` : '';
215
+ console.log(` ${e.date} ${e.content}${tag}`);
216
+ });
217
+ }
218
+ }
219
+ }
220
+ console.log(`\n grimoire story note ${s.id} "obs" ← adicionar nota`);
221
+ console.log(` grimoire memory save --story ${s.id} "texto" ← linkar memória\n`);
222
+ }
223
+
224
+ // ── delete ────────────────────────────────────────────────────────────────────
225
+ function storyDelete(id, storiesDir) {
226
+ if (!id) { console.log('Usage: grimoire story delete <id>\n'); return; }
227
+ const storyFile = path.join(storiesDir, id.endsWith('.json') ? id : `${id}.json`);
228
+ if (!fs.existsSync(storyFile)) { console.log(`❌ Story "${id}" not found.\n`); return; }
229
+ fs.unlinkSync(storyFile);
230
+ console.log(`✅ Story "${id}" removida.\n`);
231
+ }
232
+
233
+ // ── Public API for use by other modules ───────────────────────────────────────
234
+ function getActiveStories(grimoireDir) {
235
+ const dir = path.join(grimoireDir, 'stories');
236
+ if (!fs.existsSync(dir)) return [];
237
+ return fs.readdirSync(dir)
238
+ .filter(f => f.endsWith('.json'))
239
+ .map(f => { try { return JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')); } catch (_) { return null; } })
240
+ .filter(Boolean)
241
+ .filter(s => s.status !== 'done')
242
+ .sort((a, b) => a.createdAt > b.createdAt ? -1 : 1);
243
+ }
244
+
245
+ function getAllStories(grimoireDir) {
246
+ const dir = path.join(grimoireDir, 'stories');
247
+ if (!fs.existsSync(dir)) return [];
248
+ return fs.readdirSync(dir)
249
+ .filter(f => f.endsWith('.json'))
250
+ .map(f => { try { return JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')); } catch (_) { return null; } })
251
+ .filter(Boolean);
252
+ }
253
+
254
+ module.exports = { run, getActiveStories, getAllStories };