grimoire-framework 1.4.1 → 1.5.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.4.1
11
- generated_at: "2026-02-22T17:28:34.901Z"
10
+ version: 1.5.0
11
+ generated_at: "2026-02-22T17:38:38.314Z"
12
12
  generator: scripts/generate-install-manifest.js
13
13
  file_count: 1011
14
14
  files:
@@ -0,0 +1,319 @@
1
+ /**
2
+ * grimoire context — Live Context Manager
3
+ *
4
+ * grimoire context update Regenera CONTEXT.md a partir de todos os .jsonl
5
+ * grimoire context show Exibe CONTEXT.md no terminal
6
+ * grimoire context clean Remove arquivos context/ gerados
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const os = require('os');
14
+
15
+ // ── Paths ─────────────────────────────────────────────────────────────────────
16
+ function findGrimoireDir() {
17
+ const cwd = process.cwd();
18
+ const direct = path.join(cwd, '.grimoire');
19
+ const sub = path.join(cwd, 'grimoire', '.grimoire');
20
+ if (fs.existsSync(direct)) return direct;
21
+ if (fs.existsSync(sub)) return sub;
22
+ return null;
23
+ }
24
+
25
+ function getGlobalMemoryDir() {
26
+ const d = path.join(os.homedir(), '.grimoire', 'memory');
27
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
28
+ return d;
29
+ }
30
+
31
+ function getContextDir(grimoireDir) {
32
+ const d = path.join(grimoireDir, 'context');
33
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
34
+ return d;
35
+ }
36
+
37
+ // ── Load all memory entries ───────────────────────────────────────────────────
38
+ function loadAllEntries(grimoireDir, includeGlobal = true) {
39
+ const entries = [];
40
+
41
+ // Local sessions
42
+ const sessionsDir = path.join(grimoireDir, 'memory', 'sessions');
43
+ if (fs.existsSync(sessionsDir)) {
44
+ for (const f of fs.readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl')).sort()) {
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({ ...JSON.parse(line), date: f.slice(0, 10), source: 'local' }); } catch (_) { }
48
+ }
49
+ }
50
+ }
51
+
52
+ // Pinned (local)
53
+ const pinnedFile = path.join(grimoireDir, 'memory', 'pinned.jsonl');
54
+ if (fs.existsSync(pinnedFile)) {
55
+ const raw = fs.readFileSync(pinnedFile, 'utf8');
56
+ for (const line of raw.split('\n').filter(l => l.trim())) {
57
+ try { entries.push({ ...JSON.parse(line), source: 'pinned' }); } catch (_) { }
58
+ }
59
+ }
60
+
61
+ // Global
62
+ if (includeGlobal) {
63
+ const globalFile = path.join(getGlobalMemoryDir(), 'global.jsonl');
64
+ if (fs.existsSync(globalFile)) {
65
+ const raw = fs.readFileSync(globalFile, 'utf8');
66
+ for (const line of raw.split('\n').filter(l => l.trim())) {
67
+ try { entries.push({ ...JSON.parse(line), source: 'global' }); } catch (_) { }
68
+ }
69
+ }
70
+ }
71
+
72
+ return entries;
73
+ }
74
+
75
+ // ── Build Markdown files ──────────────────────────────────────────────────────
76
+ function buildContextMd(entries) {
77
+ const now = new Date().toISOString().split('T')[0];
78
+ const pinned = entries.filter(e => e.source === 'pinned');
79
+ const recent = entries.filter(e => e.source !== 'pinned').slice(-20).reverse();
80
+ const globals = entries.filter(e => e.source === 'global').slice(-10).reverse();
81
+
82
+ const lines = [];
83
+ lines.push(`# 🧠 Contexto do Projeto\n`);
84
+ lines.push(`> Auto-gerado em ${now} via \`grimoire context update\`\n`);
85
+
86
+ if (pinned.length) {
87
+ lines.push(`## 📌 Fixadas\n`);
88
+ pinned.forEach(e => lines.push(`- ${e.content}`));
89
+ lines.push('');
90
+ }
91
+
92
+ const decisions = recent.filter(e => e.tag === 'decisão' || e.tag === 'decisao' || e.tag === 'decision');
93
+ if (decisions.length) {
94
+ lines.push(`## ✅ Últimas Decisões\n`);
95
+ decisions.slice(0, 8).forEach(e => lines.push(`- **${e.date || ''}** ${e.content}`));
96
+ lines.push('');
97
+ }
98
+
99
+ const patterns = recent.filter(e => e.tag === 'padrão' || e.tag === 'padrao' || e.tag === 'pattern');
100
+ if (patterns.length) {
101
+ lines.push(`## 🔧 Padrões Estabelecidos\n`);
102
+ patterns.slice(0, 8).forEach(e => lines.push(`- **${e.date || ''}** ${e.content}`));
103
+ lines.push('');
104
+ }
105
+
106
+ const other = recent.filter(e => !['decisão', 'decisao', 'decision', 'padrão', 'padrao', 'pattern'].includes(e.tag));
107
+ if (other.length) {
108
+ lines.push(`## 📝 Entradas Recentes\n`);
109
+ other.slice(0, 10).forEach(e => {
110
+ const tag = e.tag ? ` \`#${e.tag}\`` : '';
111
+ const src = e.source === 'global' ? ' 🌐' : '';
112
+ lines.push(`- **${e.date || ''}** ${e.content}${tag}${src}`);
113
+ });
114
+ lines.push('');
115
+ }
116
+
117
+ if (globals.length) {
118
+ lines.push(`## 🌐 Memória Global (cross-projeto)\n`);
119
+ globals.slice(0, 5).forEach(e => {
120
+ const tag = e.tag ? ` \`#${e.tag}\`` : '';
121
+ lines.push(`- ${e.content}${tag}`);
122
+ });
123
+ lines.push('');
124
+ }
125
+
126
+ return lines.join('\n');
127
+ }
128
+
129
+ function buildTagMd(entries, tag, title, emoji) {
130
+ const variants = [tag, tag.normalize('NFD').replace(/[\u0300-\u036f]/g, '')];
131
+ const filtered = entries.filter(e => variants.includes(e.tag)).slice(-30).reverse();
132
+ if (!filtered.length) return null;
133
+
134
+ const lines = [`# ${emoji} ${title}\n`, `> Auto-gerado pelo Grimoire\n`];
135
+ filtered.forEach(e => {
136
+ const src = e.source === 'global' ? ' 🌐' : '';
137
+ lines.push(`- **${e.date || ''}** ${e.content}${src}`);
138
+ });
139
+ return lines.join('\n');
140
+ }
141
+
142
+ // ── Inject into GEMINI.md ─────────────────────────────────────────────────────
143
+ const MARKER_START = '<!-- grimoire-MANAGED-START: memory-context -->';
144
+ const MARKER_END = '<!-- grimoire-MANAGED-END: memory-context -->';
145
+
146
+ function buildGeminiBlock(entries) {
147
+ const now = new Date().toISOString().split('T')[0];
148
+ const pinned = entries.filter(e => e.source === 'pinned');
149
+ const recent = entries.filter(e => e.source !== 'pinned').slice(-15).reverse();
150
+ const decisions = recent.filter(e => ['decisão', 'decisao', 'decision'].includes(e.tag)).slice(0, 5);
151
+ const patterns = recent.filter(e => ['padrão', 'padrao', 'pattern'].includes(e.tag)).slice(0, 5);
152
+
153
+ const block = [];
154
+ block.push(MARKER_START);
155
+ block.push(`## 🧠 Contexto do Projeto (auto-gerado)\n`);
156
+
157
+ if (pinned.length) {
158
+ block.push(`### 📌 Fixadas\n`);
159
+ pinned.forEach(e => block.push(`- ${e.content}`));
160
+ block.push('');
161
+ }
162
+
163
+ if (decisions.length) {
164
+ block.push(`### ✅ Últimas decisões\n`);
165
+ decisions.forEach(e => block.push(`- ${e.date}: ${e.content}`));
166
+ block.push('');
167
+ }
168
+
169
+ if (patterns.length) {
170
+ block.push(`### 🔧 Padrões\n`);
171
+ patterns.forEach(e => block.push(`- ${e.date}: ${e.content}`));
172
+ block.push('');
173
+ }
174
+
175
+ // Any other recent entries without special tag
176
+ const others = recent.filter(e => !['decisão', 'decisao', 'decision', 'padrão', 'padrao', 'pattern'].includes(e.tag)).slice(0, 5);
177
+ if (others.length) {
178
+ block.push(`### 📝 Recentes\n`);
179
+ others.forEach(e => {
180
+ const tag = e.tag ? ` [#${e.tag}]` : '';
181
+ block.push(`- ${e.date}: ${e.content}${tag}`);
182
+ });
183
+ block.push('');
184
+ }
185
+
186
+ block.push(`> _Atualizado em ${now}. Use \`grimoire context update\` para regenerar._`);
187
+ block.push(MARKER_END);
188
+ return block.join('\n');
189
+ }
190
+
191
+ function injectIntoGeminiMd(grimoireDir, entries) {
192
+ // GEMINI.md can be at project root or parent
193
+ const cwd = process.cwd();
194
+ const candidates = [
195
+ path.join(cwd, 'GEMINI.md'),
196
+ path.join(path.dirname(grimoireDir), 'GEMINI.md'),
197
+ ];
198
+ const geminiPath = candidates.find(p => fs.existsSync(p));
199
+ if (!geminiPath) return false;
200
+
201
+ const original = fs.readFileSync(geminiPath, 'utf8');
202
+ const newBlock = buildGeminiBlock(entries);
203
+
204
+ let updated;
205
+ if (original.includes(MARKER_START)) {
206
+ // Replace existing block
207
+ const re = new RegExp(`${regEscape(MARKER_START)}[\\s\\S]*?${regEscape(MARKER_END)}`, 'g');
208
+ updated = original.replace(re, newBlock);
209
+ } else {
210
+ // Append before last ---
211
+ const lastHr = original.lastIndexOf('\n---');
212
+ if (lastHr !== -1) {
213
+ updated = original.slice(0, lastHr) + '\n\n' + newBlock + '\n' + original.slice(lastHr);
214
+ } else {
215
+ updated = original + '\n\n' + newBlock + '\n';
216
+ }
217
+ }
218
+
219
+ if (updated !== original) {
220
+ fs.writeFileSync(geminiPath, updated, 'utf8');
221
+ return true;
222
+ }
223
+ return false;
224
+ }
225
+
226
+ function regEscape(s) {
227
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
228
+ }
229
+
230
+ // ── Core update logic ─────────────────────────────────────────────────────────
231
+ function doUpdate(grimoireDir, silent = false) {
232
+ const contextDir = getContextDir(grimoireDir);
233
+ const entries = loadAllEntries(grimoireDir, true);
234
+
235
+ // CONTEXT.md
236
+ fs.writeFileSync(path.join(contextDir, 'CONTEXT.md'), buildContextMd(entries), 'utf8');
237
+
238
+ // decisions.md
239
+ const dec = buildTagMd(entries, 'decisão', 'Decisões', '✅') ||
240
+ buildTagMd(entries, 'decisao', 'Decisões', '✅') ||
241
+ buildTagMd(entries, 'decision', 'Decisões', '✅');
242
+ if (dec) fs.writeFileSync(path.join(contextDir, 'decisions.md'), dec, 'utf8');
243
+
244
+ // patterns.md
245
+ const pat = buildTagMd(entries, 'padrão', 'Padrões', '🔧') ||
246
+ buildTagMd(entries, 'padrao', 'Padrões', '🔧') ||
247
+ buildTagMd(entries, 'pattern', 'Padrões', '🔧');
248
+ if (pat) fs.writeFileSync(path.join(contextDir, 'patterns.md'), pat, 'utf8');
249
+
250
+ // pinned.md
251
+ const pinned = entries.filter(e => e.source === 'pinned');
252
+ if (pinned.length) {
253
+ const pinnedLines = ['# 📌 Memórias Fixadas\n', '> Estas entradas são sempre incluídas no contexto dos agentes.\n'];
254
+ pinned.forEach(e => pinnedLines.push(`- ${e.content}`));
255
+ fs.writeFileSync(path.join(contextDir, 'pinned.md'), pinnedLines.join('\n'), 'utf8');
256
+ }
257
+
258
+ // Inject GEMINI.md
259
+ const injected = injectIntoGeminiMd(grimoireDir, entries);
260
+
261
+ if (!silent) {
262
+ console.log('\n✅ Contexto atualizado!\n');
263
+ console.log(` 📄 .grimoire/context/CONTEXT.md`);
264
+ if (dec) console.log(` ✅ .grimoire/context/decisions.md`);
265
+ if (pat) console.log(` 🔧 .grimoire/context/patterns.md`);
266
+ if (pinned.length) console.log(` 📌 .grimoire/context/pinned.md`);
267
+ if (injected) console.log(` 🔮 GEMINI.md atualizado com bloco memory-context`);
268
+ console.log(`\n ${entries.length} entradas indexadas`);
269
+ console.log(` grimoire context show ← ver o resultado\n`);
270
+ }
271
+
272
+ return { entries, injected };
273
+ }
274
+
275
+ // ── run ───────────────────────────────────────────────────────────────────────
276
+ function run(args) {
277
+ const sub = args[0] || 'update';
278
+
279
+ const grimoireDir = findGrimoireDir();
280
+ if (!grimoireDir) {
281
+ console.error('❌ .grimoire/ not found. Run: npx grimoire-framework install');
282
+ return;
283
+ }
284
+
285
+ switch (sub) {
286
+ case 'update':
287
+ doUpdate(grimoireDir);
288
+ break;
289
+
290
+ case 'show': {
291
+ const contextFile = path.join(grimoireDir, 'context', 'CONTEXT.md');
292
+ if (!fs.existsSync(contextFile)) {
293
+ console.log('\n⚠️ Contexto ainda não gerado. Rode: grimoire context update\n');
294
+ return;
295
+ }
296
+ console.log('\n' + fs.readFileSync(contextFile, 'utf8'));
297
+ break;
298
+ }
299
+
300
+ case 'clean': {
301
+ const contextDir = path.join(grimoireDir, 'context');
302
+ if (fs.existsSync(contextDir)) {
303
+ fs.readdirSync(contextDir).forEach(f => fs.unlinkSync(path.join(contextDir, f)));
304
+ console.log('\n✅ Arquivos de contexto removidos (.jsonl preservados)\n');
305
+ } else {
306
+ console.log('\nℹ️ Nada para limpar.\n');
307
+ }
308
+ break;
309
+ }
310
+
311
+ default:
312
+ console.log('\nUsage:\n');
313
+ console.log(' grimoire context update Regenera CONTEXT.md + injeta GEMINI.md');
314
+ console.log(' grimoire context show Exibe CONTEXT.md');
315
+ console.log(' grimoire context clean Remove arquivos gerados\n');
316
+ }
317
+ }
318
+
319
+ module.exports = { run, doUpdate, loadAllEntries, getGlobalMemoryDir };
@@ -23,7 +23,8 @@ async function run(args) {
23
23
  baseDir = path.join(baseDir, 'grimoire');
24
24
  }
25
25
 
26
- const memoryDir = path.join(baseDir, '.grimoire', 'memory');
26
+ const grimoireDir = path.join(baseDir, '.grimoire');
27
+ const memoryDir = path.join(grimoireDir, 'memory');
27
28
 
28
29
  if (!existsSync(memoryDir)) {
29
30
  console.error('❌ Memory system not found. Run "grimoire install" first.');
@@ -33,7 +34,10 @@ async function run(args) {
33
34
  try {
34
35
  switch (subCommand) {
35
36
  case 'save':
36
- await saveMemory(args.slice(1), memoryDir);
37
+ await saveMemory(args.slice(1), memoryDir, grimoireDir);
38
+ break;
39
+ case 'pin':
40
+ await pinMemory(args.slice(1), memoryDir, grimoireDir);
37
41
  break;
38
42
  case 'list-tags':
39
43
  await listTags(memoryDir);
@@ -66,10 +70,18 @@ async function run(args) {
66
70
  }
67
71
  }
68
72
 
73
+ // ── Auto-update context (silent) ────────────────────────────────────────────
74
+ function silentContextUpdate(grimoireDir) {
75
+ try {
76
+ const { doUpdate } = require('./context');
77
+ doUpdate(grimoireDir, true); // silent = true
78
+ } catch (_) { } // never block memory save
79
+ }
80
+
69
81
  /**
70
82
  * Saves memory using JSONL append (O(1) complexity)
71
83
  */
72
- async function saveMemory(args, memoryDir) {
84
+ async function saveMemory(args, memoryDir, grimoireDir) {
73
85
  // Extract --tag value if present
74
86
  const tagIdx = args.findIndex(a => a === '--tag' || a.startsWith('--tag='));
75
87
  let tag = null;
@@ -98,6 +110,10 @@ async function saveMemory(args, memoryDir) {
98
110
  }
99
111
  }
100
112
 
113
+ // Extract --global flag
114
+ const isGlobal = filteredArgs.includes('--global');
115
+ filteredArgs = filteredArgs.filter(a => a !== '--global');
116
+
101
117
  const content = filteredArgs.join(' ');
102
118
  if (!content) {
103
119
  console.error('❌ Please provide content to save.');
@@ -105,8 +121,6 @@ async function saveMemory(args, memoryDir) {
105
121
  }
106
122
 
107
123
  const today = new Date().toISOString().split('T')[0];
108
- const sessionFile = path.join(memoryDir, 'sessions', `${today}.jsonl`);
109
-
110
124
  const entry = {
111
125
  timestamp: new Date().toISOString(),
112
126
  content: content,
@@ -114,6 +128,21 @@ async function saveMemory(args, memoryDir) {
114
128
  ...(storyId ? { story: storyId } : {}),
115
129
  };
116
130
 
131
+ if (isGlobal) {
132
+ // Save to global memory
133
+ const { getGlobalMemoryDir } = require('./context');
134
+ const globalDir = getGlobalMemoryDir();
135
+ const globalFile = require('path').join(globalDir, 'global.jsonl');
136
+ const fss = require('fs');
137
+ fss.appendFileSync(globalFile, JSON.stringify({ ...entry, source: 'global' }) + '\n', 'utf8');
138
+ const tagLabel = tag ? ` [#${tag}]` : '';
139
+ console.log(`✅ Memory saved globally 🌐${tagLabel}`);
140
+ silentContextUpdate(grimoireDir);
141
+ return;
142
+ }
143
+
144
+ const sessionFile = require('path').join(memoryDir, 'sessions', `${today}.jsonl`);
145
+
117
146
  // Ensure file exists for lockfile
118
147
  if (!existsSync(sessionFile)) {
119
148
  await fs.writeFile(sessionFile, '', 'utf8');
@@ -134,7 +163,10 @@ async function saveMemory(args, memoryDir) {
134
163
  // Append entry as a new line in JSONL format
135
164
  await fs.appendFile(sessionFile, JSON.stringify(entry) + '\n', 'utf8');
136
165
  const tagLabel = tag ? ` [#${tag}]` : '';
137
- console.log(`✅ Memory appended to session ${today}${tagLabel}`);
166
+ const storyLabel = storyId ? ` [${storyId}]` : '';
167
+ console.log(`✅ Memory saved${tagLabel}${storyLabel}`);
168
+ // Auto-update .grimoire/context/ silently
169
+ silentContextUpdate(grimoireDir);
138
170
  } catch (err) {
139
171
  console.error(`❌ Failed to acquire lock for memory file: ${err.message}`);
140
172
  } finally {
@@ -142,6 +174,22 @@ async function saveMemory(args, memoryDir) {
142
174
  }
143
175
  }
144
176
 
177
+ /**
178
+ * Pins a memory entry permanently in pinned.jsonl
179
+ */
180
+ async function pinMemory(args, memoryDir, grimoireDir) {
181
+ const content = args.filter(a => !a.startsWith('-')).join(' ');
182
+ if (!content) {
183
+ console.log('Usage: grimoire memory pin "texto"\n');
184
+ return;
185
+ }
186
+ const pinnedFile = require('path').join(memoryDir, 'pinned.jsonl');
187
+ const entry = { timestamp: new Date().toISOString(), content, source: 'pinned' };
188
+ require('fs').appendFileSync(pinnedFile, JSON.stringify(entry) + '\n', 'utf8');
189
+ console.log(`📌 Memoria fixada: "${content}"`);
190
+ silentContextUpdate(grimoireDir);
191
+ }
192
+
145
193
  /**
146
194
  * Lists sessions scanning the directory
147
195
  */
@@ -151,6 +151,9 @@ async function main() {
151
151
  case 'export':
152
152
  require('./commands/exportall').run(args.slice(1));
153
153
  break;
154
+ case 'context':
155
+ require('./commands/context').run(args.slice(1));
156
+ break;
154
157
  case 'whoami':
155
158
  handleWhoami();
156
159
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grimoire-framework",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "Grimoire: AI-Orchestrated System for Full Stack Development - Core Framework",
5
5
  "publishConfig": {
6
6
  "access": "public"