grimoire-framework 1.4.1 → 1.6.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.6.0
11
+ generated_at: "2026-02-22T17:57:17.499Z"
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,36 @@ async function run(args) {
66
70
  }
67
71
  }
68
72
 
73
+ // ── Auto-update context + RAG index (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
+
81
+ async function silentRagIndex(entry, date, grimoireDir) {
82
+ try {
83
+ const fs2 = require('fs');
84
+ const cfgF = require('path').join(grimoireDir, 'config.yaml');
85
+ const cfg = {};
86
+ if (fs2.existsSync(cfgF)) {
87
+ fs2.readFileSync(cfgF, 'utf8').split('\n').forEach(l => {
88
+ const m = l.match(/^(\w[\w_]*)\s*:\s*(.+)$/);
89
+ if (m) cfg[m[1].trim()] = m[2].trim();
90
+ });
91
+ }
92
+ // Only index if a real embedding provider is configured
93
+ if (!cfg.embedding_provider || cfg.embedding_provider === 'tfidf') return;
94
+ const { indexEntry } = require('./rag');
95
+ await indexEntry(entry, date, grimoireDir, cfg);
96
+ } catch (_) { }
97
+ }
98
+
69
99
  /**
70
100
  * Saves memory using JSONL append (O(1) complexity)
71
101
  */
72
- async function saveMemory(args, memoryDir) {
102
+ async function saveMemory(args, memoryDir, grimoireDir) {
73
103
  // Extract --tag value if present
74
104
  const tagIdx = args.findIndex(a => a === '--tag' || a.startsWith('--tag='));
75
105
  let tag = null;
@@ -98,6 +128,10 @@ async function saveMemory(args, memoryDir) {
98
128
  }
99
129
  }
100
130
 
131
+ // Extract --global flag
132
+ const isGlobal = filteredArgs.includes('--global');
133
+ filteredArgs = filteredArgs.filter(a => a !== '--global');
134
+
101
135
  const content = filteredArgs.join(' ');
102
136
  if (!content) {
103
137
  console.error('❌ Please provide content to save.');
@@ -105,8 +139,6 @@ async function saveMemory(args, memoryDir) {
105
139
  }
106
140
 
107
141
  const today = new Date().toISOString().split('T')[0];
108
- const sessionFile = path.join(memoryDir, 'sessions', `${today}.jsonl`);
109
-
110
142
  const entry = {
111
143
  timestamp: new Date().toISOString(),
112
144
  content: content,
@@ -114,6 +146,21 @@ async function saveMemory(args, memoryDir) {
114
146
  ...(storyId ? { story: storyId } : {}),
115
147
  };
116
148
 
149
+ if (isGlobal) {
150
+ // Save to global memory
151
+ const { getGlobalMemoryDir } = require('./context');
152
+ const globalDir = getGlobalMemoryDir();
153
+ const globalFile = require('path').join(globalDir, 'global.jsonl');
154
+ const fss = require('fs');
155
+ fss.appendFileSync(globalFile, JSON.stringify({ ...entry, source: 'global' }) + '\n', 'utf8');
156
+ const tagLabel = tag ? ` [#${tag}]` : '';
157
+ console.log(`✅ Memory saved globally 🌐${tagLabel}`);
158
+ silentContextUpdate(grimoireDir);
159
+ return;
160
+ }
161
+
162
+ const sessionFile = require('path').join(memoryDir, 'sessions', `${today}.jsonl`);
163
+
117
164
  // Ensure file exists for lockfile
118
165
  if (!existsSync(sessionFile)) {
119
166
  await fs.writeFile(sessionFile, '', 'utf8');
@@ -134,7 +181,12 @@ async function saveMemory(args, memoryDir) {
134
181
  // Append entry as a new line in JSONL format
135
182
  await fs.appendFile(sessionFile, JSON.stringify(entry) + '\n', 'utf8');
136
183
  const tagLabel = tag ? ` [#${tag}]` : '';
137
- console.log(`✅ Memory appended to session ${today}${tagLabel}`);
184
+ const storyLabel = storyId ? ` [${storyId}]` : '';
185
+ console.log(`✅ Memory saved${tagLabel}${storyLabel}`);
186
+ // Auto-update .grimoire/context/ silently
187
+ silentContextUpdate(grimoireDir);
188
+ // Auto-index for RAG (fire-and-forget)
189
+ silentRagIndex(entry, today, grimoireDir);
138
190
  } catch (err) {
139
191
  console.error(`❌ Failed to acquire lock for memory file: ${err.message}`);
140
192
  } finally {
@@ -142,6 +194,22 @@ async function saveMemory(args, memoryDir) {
142
194
  }
143
195
  }
144
196
 
197
+ /**
198
+ * Pins a memory entry permanently in pinned.jsonl
199
+ */
200
+ async function pinMemory(args, memoryDir, grimoireDir) {
201
+ const content = args.filter(a => !a.startsWith('-')).join(' ');
202
+ if (!content) {
203
+ console.log('Usage: grimoire memory pin "texto"\n');
204
+ return;
205
+ }
206
+ const pinnedFile = require('path').join(memoryDir, 'pinned.jsonl');
207
+ const entry = { timestamp: new Date().toISOString(), content, source: 'pinned' };
208
+ require('fs').appendFileSync(pinnedFile, JSON.stringify(entry) + '\n', 'utf8');
209
+ console.log(`📌 Memoria fixada: "${content}"`);
210
+ silentContextUpdate(grimoireDir);
211
+ }
212
+
145
213
  /**
146
214
  * Lists sessions scanning the directory
147
215
  */
@@ -0,0 +1,297 @@
1
+ /**
2
+ * grimoire rag.js — Embedding Engine (Layered Strategy)
3
+ *
4
+ * Layer 1: @xenova/transformers (local, no server needed)
5
+ * Layer 2: Ollama (http://localhost:11434) — if running
6
+ * Layer 3: TF-IDF (always available, zero deps)
7
+ *
8
+ * Vector store: .grimoire/memory/vectors/YYYY-MM-DD.json (plain JSON arrays)
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+ const os = require('os');
16
+
17
+ // ── Config ────────────────────────────────────────────────────────────────────
18
+ const DEFAULT_XENOVA_MODEL = 'Xenova/all-MiniLM-L6-v2'; // 23MB, fast
19
+ const OLLAMA_URL = 'http://localhost:11434';
20
+ const OLLAMA_MODEL = 'nomic-embed-text';
21
+ const VECTORS_DIR_NAME = 'vectors';
22
+
23
+ // ── Cosine similarity ─────────────────────────────────────────────────────────
24
+ function cosineSimilarity(a, b) {
25
+ if (!a || !b || a.length !== b.length) return 0;
26
+ let dot = 0, normA = 0, normB = 0;
27
+ for (let i = 0; i < a.length; i++) {
28
+ dot += a[i] * b[i];
29
+ normA += a[i] * a[i];
30
+ normB += b[i] * b[i];
31
+ }
32
+ const denom = Math.sqrt(normA) * Math.sqrt(normB);
33
+ return denom === 0 ? 0 : dot / denom;
34
+ }
35
+
36
+ // ── TF-IDF fallback ───────────────────────────────────────────────────────────
37
+ function tokenize(text) {
38
+ return text.toLowerCase().replace(/[^a-z0-9\u00c0-\u024f\s]/g, '').split(/\s+/).filter(Boolean);
39
+ }
40
+
41
+ const STOP_WORDS = new Set(['de', 'do', 'da', 'em', 'um', 'uma', 'e', 'a', 'o', 'os', 'as', 'para', 'com', 'que', 'não', 'por', 'se', 'mais', 'mas', 'ou', 'no', 'na', 'ao', 'isso', 'este', 'esta', 'é', 'foi', 'são', 'tem', 'como', 'sua', 'seu', 'usar', 'usar', 'ter', 'ser', 'was', 'the', 'and', 'for', 'are', 'with', 'this', 'that', 'from', 'not', 'has', 'have', 'will', 'can', 'also', 'use', 'used', 'via']);
42
+
43
+ function buildTFIDF(allEntries, query) {
44
+ // Build corpus
45
+ const docs = allEntries.map(e => tokenize(e.content));
46
+ const queryTokens = tokenize(query);
47
+ const N = docs.length;
48
+
49
+ // Build vocabulary (union of query + docs)
50
+ const vocab = new Set([...queryTokens]);
51
+ docs.forEach(d => d.forEach(t => { if (!STOP_WORDS.has(t)) vocab.add(t); }));
52
+ const vocabArr = [...vocab];
53
+
54
+ // IDF
55
+ const idf = {};
56
+ vocabArr.forEach(term => {
57
+ const df = docs.filter(d => d.includes(term)).length;
58
+ idf[term] = df === 0 ? 0 : Math.log((N + 1) / (df + 1)) + 1;
59
+ });
60
+
61
+ // TF-IDF vector for a token list
62
+ function toVec(tokens) {
63
+ const tf = {};
64
+ tokens.forEach(t => { tf[t] = (tf[t] || 0) + 1; });
65
+ const maxTF = Math.max(...Object.values(tf), 1);
66
+ return vocabArr.map(term => ((tf[term] || 0) / maxTF) * (idf[term] || 0));
67
+ }
68
+
69
+ const queryVec = toVec(queryTokens);
70
+ return allEntries.map((entry, i) => ({
71
+ entry,
72
+ score: cosineSimilarity(queryVec, toVec(docs[i])),
73
+ }));
74
+ }
75
+
76
+ // ── Xenova embedding ──────────────────────────────────────────────────────────
77
+ let _xenovaExtractor = null;
78
+
79
+ async function getXenovaExtractor() {
80
+ if (_xenovaExtractor) return _xenovaExtractor;
81
+ try {
82
+ // Dynamic import for ESM compat
83
+ const { pipeline } = await import('@xenova/transformers');
84
+ _xenovaExtractor = await pipeline('feature-extraction', DEFAULT_XENOVA_MODEL, {
85
+ progress_callback: null, // silent
86
+ });
87
+ return _xenovaExtractor;
88
+ } catch (_) {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ async function embedXenova(text) {
94
+ const extractor = await getXenovaExtractor();
95
+ if (!extractor) return null;
96
+ const output = await extractor(text, { pooling: 'mean', normalize: true });
97
+ return Array.from(output.data);
98
+ }
99
+
100
+ // ── Ollama embedding ──────────────────────────────────────────────────────────
101
+ async function embedOllama(text) {
102
+ try {
103
+ const res = await fetch(`${OLLAMA_URL}/api/embeddings`, {
104
+ method: 'POST',
105
+ headers: { 'Content-Type': 'application/json' },
106
+ body: JSON.stringify({ model: OLLAMA_MODEL, prompt: text }),
107
+ signal: AbortSignal.timeout(5000),
108
+ });
109
+ const { embedding } = await res.json();
110
+ return Array.isArray(embedding) ? embedding : null;
111
+ } catch (_) {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ // ── Auto-detect provider ──────────────────────────────────────────────────────
117
+ async function detectProvider(config) {
118
+ const preferred = config?.embedding_provider || 'auto';
119
+
120
+ if (preferred === 'tfidf') return 'tfidf';
121
+ if (preferred === 'ollama') return 'ollama';
122
+
123
+ if (preferred === 'xenova' || preferred === 'auto') {
124
+ try {
125
+ await import('@xenova/transformers');
126
+ return 'xenova';
127
+ } catch (_) { }
128
+ }
129
+
130
+ if (preferred === 'auto') {
131
+ // Try Ollama ping
132
+ try {
133
+ const r = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(1000) });
134
+ if (r.ok) return 'ollama';
135
+ } catch (_) { }
136
+ }
137
+
138
+ return 'tfidf';
139
+ }
140
+
141
+ // ── Public: embed a single text ───────────────────────────────────────────────
142
+ async function embed(text, config) {
143
+ const provider = await detectProvider(config);
144
+
145
+ if (provider === 'xenova') {
146
+ const v = await embedXenova(text);
147
+ if (v) return { vector: v, provider: 'xenova' };
148
+ }
149
+
150
+ if (provider === 'ollama') {
151
+ const v = await embedOllama(text);
152
+ if (v) return { vector: v, provider: 'ollama' };
153
+ }
154
+
155
+ return { vector: null, provider: 'tfidf' }; // TF-IDF is computed at search time
156
+ }
157
+
158
+ // ── Vector store ──────────────────────────────────────────────────────────────
159
+ function getVectorsDir(grimoireDir) {
160
+ const d = path.join(grimoireDir, 'memory', VECTORS_DIR_NAME);
161
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
162
+ return d;
163
+ }
164
+
165
+ function loadVectorFile(vectorsDir, date) {
166
+ const f = path.join(vectorsDir, `${date}.json`);
167
+ if (!fs.existsSync(f)) return [];
168
+ try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch (_) { return []; }
169
+ }
170
+
171
+ function saveVectorFile(vectorsDir, date, records) {
172
+ fs.writeFileSync(path.join(vectorsDir, `${date}.json`), JSON.stringify(records), 'utf8');
173
+ }
174
+
175
+ // Index a single entry (called after memory save)
176
+ async function indexEntry(entry, date, grimoireDir, config) {
177
+ try {
178
+ const { vector, provider } = await embed(entry.content, config);
179
+ if (!vector) return; // TF-IDF: skip vector storage, computed at query time
180
+
181
+ const vectorsDir = getVectorsDir(grimoireDir);
182
+ const records = loadVectorFile(vectorsDir, date);
183
+
184
+ // Remove existing record with same timestamp to avoid duplicates
185
+ const filtered = records.filter(r => r.timestamp !== entry.timestamp);
186
+ filtered.push({
187
+ timestamp: entry.timestamp,
188
+ content: entry.content,
189
+ tag: entry.tag || null,
190
+ story: entry.story || null,
191
+ source: entry.source || 'local',
192
+ vector,
193
+ provider,
194
+ });
195
+ saveVectorFile(vectorsDir, date, filtered);
196
+ } catch (_) { } // never block memory save
197
+ }
198
+
199
+ // ── Public: semantic search ───────────────────────────────────────────────────
200
+ async function search(query, grimoireDir, config, topK = 5) {
201
+ const provider = await detectProvider(config);
202
+
203
+ if (provider === 'xenova' || provider === 'ollama') {
204
+ return await vectorSearch(query, grimoireDir, config, topK, provider);
205
+ }
206
+
207
+ // TF-IDF fallback — load all entries from JSONL
208
+ return tfidfSearch(query, grimoireDir, topK);
209
+ }
210
+
211
+ async function vectorSearch(query, grimoireDir, config, topK, provider) {
212
+ const { vector: queryVec } = await embed(query, config);
213
+ if (!queryVec) return tfidfSearch(query, grimoireDir, topK);
214
+
215
+ const vectorsDir = getVectorsDir(grimoireDir);
216
+ if (!fs.existsSync(vectorsDir)) return [];
217
+
218
+ const results = [];
219
+ for (const f of fs.readdirSync(vectorsDir).filter(f => f.endsWith('.json'))) {
220
+ const records = loadVectorFile(vectorsDir, f.replace('.json', ''));
221
+ for (const rec of records) {
222
+ if (!rec.vector) continue;
223
+ const score = cosineSimilarity(queryVec, rec.vector);
224
+ results.push({ ...rec, score, searchMode: 'semantic' });
225
+ }
226
+ }
227
+
228
+ return results.sort((a, b) => b.score - a.score).slice(0, topK);
229
+ }
230
+
231
+ function tfidfSearch(query, grimoireDir, topK) {
232
+ // Load all JSONL entries
233
+ const allEntries = [];
234
+ const sessionsDir = path.join(grimoireDir, 'memory', 'sessions');
235
+ if (!fs.existsSync(sessionsDir)) return [];
236
+
237
+ for (const f of fs.readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl')).sort()) {
238
+ const raw = fs.readFileSync(path.join(sessionsDir, f), 'utf8');
239
+ for (const line of raw.split('\n').filter(l => l.trim())) {
240
+ try { allEntries.push({ date: f.slice(0, 10), ...JSON.parse(line) }); } catch (_) { }
241
+ }
242
+ }
243
+
244
+ return buildTFIDF(allEntries, query)
245
+ .filter(r => r.score > 0)
246
+ .sort((a, b) => b.score - a.score)
247
+ .slice(0, topK)
248
+ .map(r => ({ ...r.entry, score: r.score, searchMode: 'tfidf' }));
249
+ }
250
+
251
+ // ── Re-index all existing entries ─────────────────────────────────────────────
252
+ async function reindexAll(grimoireDir, config, onProgress) {
253
+ const sessionsDir = path.join(grimoireDir, 'memory', 'sessions');
254
+ if (!fs.existsSync(sessionsDir)) return 0;
255
+
256
+ let count = 0;
257
+ const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith('.jsonl')).sort();
258
+
259
+ for (const f of files) {
260
+ const date = f.slice(0, 10);
261
+ const raw = fs.readFileSync(path.join(sessionsDir, f), 'utf8');
262
+ const lines = raw.split('\n').filter(l => l.trim());
263
+
264
+ for (const line of lines) {
265
+ try {
266
+ const entry = JSON.parse(line);
267
+ await indexEntry(entry, date, grimoireDir, config);
268
+ count++;
269
+ if (onProgress) onProgress(count);
270
+ } catch (_) { }
271
+ }
272
+ }
273
+ return count;
274
+ }
275
+
276
+ // ── Status check ──────────────────────────────────────────────────────────────
277
+ async function ragStatus(config) {
278
+ const provider = await detectProvider(config);
279
+ const xenovaAvail = await (async () => {
280
+ try { await import('@xenova/transformers'); return true; } catch (_) { return false; }
281
+ })();
282
+ const ollamaAvail = await (async () => {
283
+ try {
284
+ const r = await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(1000) });
285
+ return r.ok;
286
+ } catch (_) { return false; }
287
+ })();
288
+
289
+ return {
290
+ activeProvider: provider,
291
+ xenova: { available: xenovaAvail, model: DEFAULT_XENOVA_MODEL },
292
+ ollama: { available: ollamaAvail, url: OLLAMA_URL, model: OLLAMA_MODEL },
293
+ tfidf: { available: true },
294
+ };
295
+ }
296
+
297
+ module.exports = { embed, indexEntry, search, reindexAll, ragStatus, cosineSimilarity };
@@ -1,10 +1,12 @@
1
1
  /**
2
- * grimoire search — Global Search
2
+ * grimoire search — Global Search (RAG-enhanced)
3
3
  *
4
- * grimoire search "termo" Busca em memory + stories + agents
5
- * grimoire search "JWT" --memory Só memória
4
+ * grimoire search "termo" Busca semântica em memory + stories + agents
5
+ * grimoire search "JWT" --memory Só memória (semântica se RAG ativo)
6
6
  * grimoire search "JWT" --stories Só stories
7
7
  * grimoire search "JWT" --agents Só agentes
8
+ * grimoire search "JWT" --semantic Força busca semântica
9
+ * grimoire search "JWT" --exact Força busca por substring
8
10
  */
9
11
 
10
12
  'use strict';
@@ -36,8 +38,38 @@ function hl(text, query) {
36
38
  return text.replace(re, m => `\x1b[33m${m}\x1b[0m`);
37
39
  }
38
40
 
39
- // ── Search memory sessions ────────────────────────────────────────────────────
40
- function searchMemory(grimoireDir, query) {
41
+ function loadConfig(grimoireDir) {
42
+ const f = path.join(grimoireDir, 'config.yaml');
43
+ if (!fs.existsSync(f)) return {};
44
+ const cfg = {};
45
+ fs.readFileSync(f, 'utf8').split('\n').forEach(line => {
46
+ const m = line.match(/^(\w[\w_]*)\s*:\s*(.+)$/);
47
+ if (m) cfg[m[1].trim()] = m[2].trim();
48
+ });
49
+ return cfg;
50
+ }
51
+
52
+ // ── RAG memory search ─────────────────────────────────────────────────────────
53
+ async function searchMemoryRAG(grimoireDir, query, topK = 15) {
54
+ try {
55
+ const { search } = require('./rag');
56
+ const config = loadConfig(grimoireDir);
57
+ const results = await search(query, grimoireDir, config, topK);
58
+ return results.map(r => ({
59
+ date: r.date || r.timestamp?.slice(0, 10) || '',
60
+ content: r.content,
61
+ tag: r.tag || null,
62
+ story: r.story || null,
63
+ score: r.score,
64
+ searchMode: r.searchMode || 'unknown',
65
+ }));
66
+ } catch (_) {
67
+ return searchMemoryExact(grimoireDir, query);
68
+ }
69
+ }
70
+
71
+ // ── Exact substring memory search (fallback) ─────────────────────────────────
72
+ function searchMemoryExact(grimoireDir, query) {
41
73
  const sessionsDir = path.join(grimoireDir, 'memory', 'sessions');
42
74
  if (!fs.existsSync(sessionsDir)) return [];
43
75
  const lq = query.toLowerCase();
@@ -49,7 +81,7 @@ function searchMemory(grimoireDir, query) {
49
81
  try {
50
82
  const e = JSON.parse(line);
51
83
  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 });
84
+ results.push({ date: f.slice(0, 10), content: e.content, tag: e.tag || null, story: e.story || null, score: 1, searchMode: 'exact' });
53
85
  }
54
86
  } catch (_) { }
55
87
  }
@@ -80,7 +112,6 @@ function searchAgents(agentsDir, query) {
80
112
  const raw = fs.readFileSync(path.join(agentsDir, f), 'utf8').toLowerCase();
81
113
  if (raw.includes(lq)) {
82
114
  const id = f.replace('.md', '');
83
- // Extract name from yaml block
84
115
  let name = id;
85
116
  const nameMatch = raw.match(/name:\s*([^\n]+)/);
86
117
  if (nameMatch) name = nameMatch[1].trim();
@@ -92,18 +123,33 @@ function searchAgents(agentsDir, query) {
92
123
  return results;
93
124
  }
94
125
 
126
+ // ── Render score bar ──────────────────────────────────────────────────────────
127
+ function scoreBar(score) {
128
+ if (score === null || score === undefined) return '';
129
+ const pct = Math.round(score * 10);
130
+ const bar = '█'.repeat(pct) + '░'.repeat(10 - pct);
131
+ const pctStr = `${Math.round(score * 100)}%`.padStart(4);
132
+ return ` \x1b[2m${bar} ${pctStr}\x1b[0m`;
133
+ }
134
+
95
135
  // ── run ───────────────────────────────────────────────────────────────────────
96
- function run(args) {
136
+ async function run(args) {
97
137
  const query = args.filter(a => !a.startsWith('-')).join(' ');
98
138
  if (!query) {
99
139
  console.log('Usage: grimoire search "termo"\n');
100
- console.log('Busca em: memória · stories · agentes\n');
140
+ console.log('Busca semântica em: memória · stories · agentes\n');
141
+ console.log('Flags:');
142
+ console.log(' --memory Só memória');
143
+ console.log(' --stories Só stories');
144
+ console.log(' --agents Só agentes');
145
+ console.log(' --exact Força busca por substring (ignora RAG)\n');
101
146
  return;
102
147
  }
103
148
 
104
149
  const onlyMemory = args.includes('--memory');
105
150
  const onlyStories = args.includes('--stories');
106
151
  const onlyAgents = args.includes('--agents');
152
+ const forceExact = args.includes('--exact');
107
153
  const all = !onlyMemory && !onlyStories && !onlyAgents;
108
154
 
109
155
  const grimoireDir = findGrimoireDir();
@@ -111,43 +157,59 @@ function run(args) {
111
157
 
112
158
  let totalResults = 0;
113
159
 
114
- console.log(`\n🔍 Grimoire Search "${query}"\n${'─'.repeat(44)}`);
160
+ // Detect search mode label
161
+ let searchModeLabel = '(substring)';
162
+ if (!forceExact && grimoireDir) {
163
+ const config = loadConfig(grimoireDir);
164
+ const provider = config.embedding_provider || 'auto';
165
+ if (provider !== 'tfidf') {
166
+ searchModeLabel = provider === 'xenova' ? '🤖 semântica (xenova)' :
167
+ provider === 'ollama' ? '🦙 semântica (ollama)' :
168
+ '📊 TF-IDF';
169
+ } else {
170
+ searchModeLabel = '📊 TF-IDF';
171
+ }
172
+ }
173
+
174
+ console.log(`\n🔍 Grimoire Search — "${query}" ${searchModeLabel}\n${'─'.repeat(52)}`);
115
175
 
116
176
  // 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;
177
+ if ((all || onlyMemory) && grimoireDir) {
178
+ const memResults = forceExact
179
+ ? searchMemoryExact(grimoireDir, query)
180
+ : await searchMemoryRAG(grimoireDir, query, 10);
181
+
182
+ if (memResults.length > 0) {
183
+ const modeIcon = memResults[0]?.searchMode === 'semantic' ? ' 🤖' :
184
+ memResults[0]?.searchMode === 'tfidf' ? ' 📊' : '';
185
+ console.log(`\n🧠 Memória${modeIcon}:`);
186
+ for (const r of memResults) {
187
+ const tag = r.tag ? ` \x1b[36m[#${r.tag}]\x1b[0m` : '';
188
+ const story = r.story ? ` \x1b[35m[${r.story}]\x1b[0m` : '';
189
+ const bar = r.searchMode !== 'exact' ? scoreBar(r.score) : '';
190
+ console.log(` ${r.date} ${hl(r.content, query)}${tag}${story}${bar}`);
128
191
  }
192
+ totalResults += memResults.length;
129
193
  }
130
194
  }
131
195
 
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
- }
196
+ // Stories (always exact — small dataset)
197
+ if ((all || onlyStories) && grimoireDir) {
198
+ const storyResults = searchStories(grimoireDir, query);
199
+ if (storyResults.length > 0) {
200
+ console.log('\n📋 Stories:');
201
+ for (const s of storyResults) {
202
+ const icon = s.status === 'done' ? '✅' : '🔄';
203
+ console.log(` ${icon} [${s.id}] ${hl(s.title, query)}`);
204
+ for (const n of (s.notes || []).filter(n => n.toLowerCase().includes(query.toLowerCase()))) {
205
+ console.log(` 📝 ${hl(n, query)}`);
144
206
  }
145
- totalResults += storyResults.length;
146
207
  }
208
+ totalResults += storyResults.length;
147
209
  }
148
210
  }
149
211
 
150
- // Agents
212
+ // Agents (always exact)
151
213
  if (all || onlyAgents) {
152
214
  const agentResults = searchAgents(agentsDir, query);
153
215
  if (agentResults.length > 0) {
@@ -161,9 +223,9 @@ function run(args) {
161
223
 
162
224
  if (totalResults === 0) {
163
225
  console.log('\n (nenhum resultado encontrado)');
164
- console.log(` Dica: tente termos mais curtos ou use --memory / --stories / --agents\n`);
226
+ console.log(` Dica: tente termos mais amplos ou "grimoire setup rag" para busca semântica\n`);
165
227
  } else {
166
- console.log(`\n${'─'.repeat(44)}`);
228
+ console.log(`\n${'─'.repeat(52)}`);
167
229
  console.log(` ${totalResults} resultado(s)\n`);
168
230
  }
169
231
  }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * grimoire setup — Environment & RAG Setup
3
+ *
4
+ * grimoire setup rag Configura RAG (detecta provider + baixa modelo)
5
+ * grimoire setup rag --reindex Reindexar todas as entradas existentes
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const path = require('path');
11
+ const fs = require('fs');
12
+ const os = require('os');
13
+ const { execSync, spawn } = require('child_process');
14
+
15
+ function findGrimoireDir() {
16
+ const cwd = process.cwd();
17
+ const d1 = path.join(cwd, '.grimoire');
18
+ const d2 = path.join(cwd, 'grimoire', '.grimoire');
19
+ if (fs.existsSync(d1)) return d1;
20
+ if (fs.existsSync(d2)) return d2;
21
+ return null;
22
+ }
23
+
24
+ function loadConfig(grimoireDir) {
25
+ const f = path.join(grimoireDir, 'config.yaml');
26
+ if (!fs.existsSync(f)) return {};
27
+ // Simple YAML key: value parser
28
+ const cfg = {};
29
+ fs.readFileSync(f, 'utf8').split('\n').forEach(line => {
30
+ const m = line.match(/^(\w[\w_]*)\s*:\s*(.+)$/);
31
+ if (m) cfg[m[1].trim()] = m[2].trim();
32
+ });
33
+ return cfg;
34
+ }
35
+
36
+ function saveConfig(grimoireDir, updates) {
37
+ const f = path.join(grimoireDir, 'config.yaml');
38
+ let content = fs.existsSync(f) ? fs.readFileSync(f, 'utf8') : '';
39
+
40
+ Object.entries(updates).forEach(([key, value]) => {
41
+ const re = new RegExp(`^${key}\\s*:.*$`, 'm');
42
+ if (re.test(content)) {
43
+ content = content.replace(re, `${key}: ${value}`);
44
+ } else {
45
+ content += `\n${key}: ${value}`;
46
+ }
47
+ });
48
+
49
+ fs.writeFileSync(f, content.trim() + '\n', 'utf8');
50
+ }
51
+
52
+ // ── Check if @xenova/transformers is installed ─────────────────────────────────
53
+ function isXenovaInstalled() {
54
+ try { require.resolve('@xenova/transformers'); return true; } catch (_) { return false; }
55
+ }
56
+
57
+ // ── Check if Ollama is running ─────────────────────────────────────────────────
58
+ async function isOllamaRunning() {
59
+ try {
60
+ const r = await fetch('http://localhost:11434/api/tags', { signal: AbortSignal.timeout(1500) });
61
+ return r.ok;
62
+ } catch (_) { return false; }
63
+ }
64
+
65
+ // ── Setup RAG ─────────────────────────────────────────────────────────────────
66
+ async function setupRag(args) {
67
+ const reindex = args.includes('--reindex');
68
+ const sep = '─'.repeat(52);
69
+
70
+ console.log(`\n🔮 Grimoire RAG Setup\n${sep}\n`);
71
+
72
+ const { ragStatus } = require('./rag');
73
+ const grimoireDir = findGrimoireDir();
74
+
75
+ if (!grimoireDir) {
76
+ console.error('❌ .grimoire/ not found. Run: npx grimoire-framework install\n');
77
+ return;
78
+ }
79
+
80
+ const config = loadConfig(grimoireDir);
81
+
82
+ console.log('🔍 Detectando providers disponíveis...\n');
83
+
84
+ // ── Layer 1: @xenova/transformers ──────────────────────────────────────────
85
+ console.log(' [1/3] @xenova/transformers (local, offline)');
86
+
87
+ if (isXenovaInstalled()) {
88
+ console.log(' ✅ Já instalado');
89
+ } else {
90
+ console.log(' 📦 Instalando @xenova/transformers...');
91
+ try {
92
+ execSync('npm install @xenova/transformers --save-optional --no-fund --no-audit', {
93
+ stdio: 'inherit',
94
+ cwd: path.dirname(require.main?.filename || process.cwd()),
95
+ });
96
+ console.log(' ✅ Instalado com sucesso');
97
+ } catch (e) {
98
+ console.log(' ⚠️ Falha ao instalar — tentando layer 2 (Ollama)');
99
+ }
100
+ }
101
+
102
+ // Download model on first use (xenova downloads automatically)
103
+ if (isXenovaInstalled()) {
104
+ console.log(' 🤖 Carregando modelo Xenova/all-MiniLM-L6-v2...');
105
+ console.log(' (primeiro carregamento pode levar 30-60s ~23MB)');
106
+ try {
107
+ const { pipeline } = await import('@xenova/transformers');
108
+ await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', { progress_callback: null });
109
+ console.log(' ✅ Modelo pronto!\n');
110
+ saveConfig(grimoireDir, { embedding_provider: 'xenova', embedding_model: 'Xenova/all-MiniLM-L6-v2' });
111
+ } catch (e) {
112
+ console.log(` ❌ Erro ao carregar modelo: ${e.message}\n`);
113
+ }
114
+ }
115
+
116
+ // ── Layer 2: Ollama ────────────────────────────────────────────────────────
117
+ console.log(' [2/3] Ollama (local server, melhor qualidade)');
118
+ const ollamaRunning = await isOllamaRunning();
119
+ if (ollamaRunning) {
120
+ console.log(' ✅ Ollama detectado em localhost:11434');
121
+ if (!isXenovaInstalled()) {
122
+ saveConfig(grimoireDir, { embedding_provider: 'ollama', embedding_model: 'nomic-embed-text' });
123
+ }
124
+ } else {
125
+ console.log(' ℹ️ Ollama não detectado (opcional)');
126
+ console.log(' Instale em: https://ollama.com');
127
+ console.log(' Depois: ollama pull nomic-embed-text\n');
128
+ }
129
+
130
+ // ── Layer 3: TF-IDF ────────────────────────────────────────────────────────
131
+ console.log(' [3/3] TF-IDF (fallback — sempre disponível)');
132
+ console.log(' ✅ Ativo\n');
133
+
134
+ // ── Final status ───────────────────────────────────────────────────────────
135
+ const status = await ragStatus(loadConfig(grimoireDir));
136
+ const activeIcon = { xenova: '🤖', ollama: '🦙', tfidf: '📊' };
137
+
138
+ console.log(`${sep}`);
139
+ console.log(` Provider ativo: ${activeIcon[status.activeProvider] || '📊'} ${status.activeProvider}`);
140
+ console.log(` Xenova: ${status.xenova.available ? '✅' : '❌'} ${status.xenova.model}`);
141
+ console.log(` Ollama: ${status.ollama.available ? '✅' : '❌'} ${status.ollama.url}`);
142
+ console.log(` TF-IDF: ✅ sempre disponível`);
143
+ console.log(`${sep}\n`);
144
+
145
+ // ── Reindex ────────────────────────────────────────────────────────────────
146
+ if (reindex || status.activeProvider !== 'tfidf') {
147
+ const { reindexAll } = require('./rag');
148
+ process.stdout.write(' 🔄 Indexando entradas de memória existentes...');
149
+ let n = 0;
150
+ const count = await reindexAll(grimoireDir, loadConfig(grimoireDir), (i) => {
151
+ n = i;
152
+ if (i % 10 === 0) process.stdout.write('.');
153
+ });
154
+ console.log(`\n ✅ ${count} entradas indexadas\n`);
155
+ }
156
+
157
+ console.log('🎯 RAG configurado! Agora o grimoire memory search é semântico.');
158
+ console.log(' grimoire memory search "autenticação" ← busca por significado\n');
159
+ }
160
+
161
+ // ── run ────────────────────────────────────────────────────────────────────────
162
+ async function run(args) {
163
+ const sub = args[0];
164
+ switch (sub) {
165
+ case 'rag': await setupRag(args.slice(1)); break;
166
+ default:
167
+ console.log('\nUsage:\n');
168
+ console.log(' grimoire setup rag Configura RAG (xenova + modelos)');
169
+ console.log(' grimoire setup rag --reindex Reindexar todas as entradas\n');
170
+ }
171
+ }
172
+
173
+ module.exports = { run };
@@ -151,6 +151,12 @@ 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;
157
+ case 'setup':
158
+ require('./commands/setup').run(args.slice(1));
159
+ break;
154
160
  case 'whoami':
155
161
  handleWhoami();
156
162
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grimoire-framework",
3
- "version": "1.4.1",
3
+ "version": "1.6.0",
4
4
  "description": "Grimoire: AI-Orchestrated System for Full Stack Development - Core Framework",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -82,6 +82,9 @@
82
82
  "prepublishOnly": "node scripts/pre-publish-check.js && npm run generate:manifest && npm run validate:manifest",
83
83
  "prepare": "husky"
84
84
  },
85
+ "optionalDependencies": {
86
+ "@xenova/transformers": "^2.17.2"
87
+ },
85
88
  "dependencies": {
86
89
  "@clack/prompts": "^0.11.0",
87
90
  "@kayvan/markdown-tree-parser": "^1.5.0",