grimoire-framework 1.4.0 → 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.
- package/.grimoire/install-manifest.yaml +2 -2
- package/bin/commands/context.js +319 -0
- package/bin/commands/memory.js +54 -6
- package/bin/commands/pro.js +29 -131
- package/bin/grimoire-cli.js +3 -0
- package/package.json +1 -1
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
# - SHA256 hashes for change detection
|
|
8
8
|
# - File types for categorization
|
|
9
9
|
#
|
|
10
|
-
version: 1.
|
|
11
|
-
generated_at: "2026-02-22T17:
|
|
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 };
|
package/bin/commands/memory.js
CHANGED
|
@@ -23,7 +23,8 @@ async function run(args) {
|
|
|
23
23
|
baseDir = path.join(baseDir, 'grimoire');
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const
|
|
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
|
-
|
|
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
|
*/
|
package/bin/commands/pro.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* grimoire pro —
|
|
2
|
+
* grimoire pro — Feature Catalogue
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Todos os recursos Pro estão disponíveis por padrão.
|
|
5
|
+
* O sistema de licença foi removido — sem chave, sem bloqueio.
|
|
6
6
|
*
|
|
7
|
-
* grimoire pro
|
|
8
|
-
* grimoire pro
|
|
9
|
-
* grimoire pro features Lista features Pro disponíveis
|
|
10
|
-
* grimoire pro deactivate Remove a licença
|
|
7
|
+
* grimoire pro status Mostra status e features
|
|
8
|
+
* grimoire pro features Lista todas as features
|
|
11
9
|
*/
|
|
12
10
|
|
|
13
11
|
'use strict';
|
|
@@ -69,17 +67,19 @@ function isLicenseValid(license) {
|
|
|
69
67
|
return license.checksum === expected;
|
|
70
68
|
}
|
|
71
69
|
|
|
72
|
-
// ── Public API
|
|
70
|
+
// ── Public API (all features unlocked by default) ────────────────────────────
|
|
71
|
+
/**
|
|
72
|
+
* Always returns true — all Pro features are available without a license key.
|
|
73
|
+
*/
|
|
73
74
|
function isPro() {
|
|
74
|
-
|
|
75
|
-
return isLicenseValid(license) && license.type === 'pro';
|
|
75
|
+
return true;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Always returns true — every feature ID is available.
|
|
80
|
+
*/
|
|
78
81
|
function hasFeature(featureId) {
|
|
79
|
-
|
|
80
|
-
if (!isLicenseValid(license)) return false;
|
|
81
|
-
if (license.type === 'pro') return PRO_FEATURES.some(f => f.id === featureId);
|
|
82
|
-
return license.features?.includes(featureId) || false;
|
|
82
|
+
return true;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// ── readline prompt ────────────────────────────────────────────────────────────
|
|
@@ -93,145 +93,43 @@ function prompt(q) {
|
|
|
93
93
|
async function run(args) {
|
|
94
94
|
const sub = args[0];
|
|
95
95
|
switch (sub) {
|
|
96
|
-
case 'activate':
|
|
97
|
-
case 'deactivate':
|
|
96
|
+
case 'activate': showAlreadyOpen(); break;
|
|
97
|
+
case 'deactivate': showAlreadyOpen(); break;
|
|
98
98
|
case 'features': showFeatures(); break;
|
|
99
99
|
case 'status':
|
|
100
100
|
default: showStatus(); break;
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
// ──
|
|
105
|
-
|
|
106
|
-
console.log('\n
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (!validateKeyFormat(key)) {
|
|
111
|
-
console.error('❌ Formato inválido. Use: PRO-XXXX-XXXX-XXXX-XXXX');
|
|
112
|
-
console.log(' Obtenha sua chave em: https://grimoire.dev/pro\n');
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const email = await prompt('Email associado à licença: ');
|
|
117
|
-
if (!email || !email.includes('@')) {
|
|
118
|
-
console.error('❌ Email inválido.');
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Offline HMAC validation
|
|
123
|
-
const checksum = generateKeyChecksum(email, key);
|
|
124
|
-
|
|
125
|
-
// Try online validation (Phase 2 ready but not blocking)
|
|
126
|
-
let onlineValidated = false;
|
|
127
|
-
try {
|
|
128
|
-
const https = require('https');
|
|
129
|
-
const result = await new Promise((resolve, reject) => {
|
|
130
|
-
const data = JSON.stringify({ key, email });
|
|
131
|
-
const req = https.request({
|
|
132
|
-
hostname: 'api.grimoire.dev',
|
|
133
|
-
path: '/v1/license/validate',
|
|
134
|
-
method: 'POST',
|
|
135
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': data.length },
|
|
136
|
-
timeout: 4000,
|
|
137
|
-
}, (res) => {
|
|
138
|
-
let body = '';
|
|
139
|
-
res.on('data', d => body += d);
|
|
140
|
-
res.on('end', () => { try { resolve(JSON.parse(body)); } catch (e) { reject(e); } });
|
|
141
|
-
});
|
|
142
|
-
req.on('error', reject);
|
|
143
|
-
req.on('timeout', () => req.destroy());
|
|
144
|
-
req.write(data);
|
|
145
|
-
req.end();
|
|
146
|
-
});
|
|
147
|
-
onlineValidated = result.valid === true;
|
|
148
|
-
} catch (_) {
|
|
149
|
-
// Offline — use local validation only
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const license = {
|
|
153
|
-
type: 'pro',
|
|
154
|
-
key,
|
|
155
|
-
email,
|
|
156
|
-
checksum,
|
|
157
|
-
activatedAt: new Date().toISOString(),
|
|
158
|
-
expiresAt: null, // null = never (or set by server in Phase 2)
|
|
159
|
-
onlineValidated,
|
|
160
|
-
features: PRO_FEATURES.map(f => f.id),
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
saveLicense(license);
|
|
164
|
-
|
|
165
|
-
console.log(`
|
|
166
|
-
✅ Grimoire Pro ativado!
|
|
167
|
-
|
|
168
|
-
Email: ${email}
|
|
169
|
-
Chave: ${key.substring(0, 7)}...${key.slice(-4)}
|
|
170
|
-
Status: ${onlineValidated ? '✅ Validado online' : '✅ Ativado (validação offline)'}
|
|
171
|
-
|
|
172
|
-
🎯 Features ativas:`);
|
|
173
|
-
PRO_FEATURES.forEach(f => console.log(` ✅ ${f.name} — ${f.desc}`));
|
|
174
|
-
console.log('\n💡 Use "grimoire pro status" para ver os detalhes\n');
|
|
104
|
+
// ── showAlreadyOpen ───────────────────────────────────────────────────────────
|
|
105
|
+
function showAlreadyOpen() {
|
|
106
|
+
console.log('\n🔓 Grimoire Pro está aberto!');
|
|
107
|
+
console.log(' Todos os recursos já estão disponíveis sem preciso de chave.\n');
|
|
108
|
+
showFeatures();
|
|
175
109
|
}
|
|
176
110
|
|
|
177
111
|
// ── Status ─────────────────────────────────────────────────────────────────────
|
|
178
112
|
function showStatus() {
|
|
179
|
-
const license = loadLicense();
|
|
180
113
|
const sep = '─'.repeat(50);
|
|
181
|
-
|
|
182
|
-
console.log(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
console.log(` Status: ⭕ Não ativado (Community)\n`);
|
|
186
|
-
console.log(` ${sep}`);
|
|
187
|
-
console.log(` Para ativar: grimoire pro activate <chave>`);
|
|
188
|
-
console.log(` Obter licença: https://grimoire.dev/pro\n`);
|
|
189
|
-
showFeatureList(false);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const valid = isLicenseValid(license);
|
|
194
|
-
const isPro = license.type === 'pro' && valid;
|
|
195
|
-
const expires = license.expiresAt ? new Date(license.expiresAt).toLocaleDateString('pt-BR') : 'Sem expiração';
|
|
196
|
-
|
|
197
|
-
console.log(` Status: ${valid ? (isPro ? '✅ Pro Ativo' : '✅ Community') : '❌ Licença inválida/expirada'}`);
|
|
198
|
-
if (license.email) console.log(` Email: ${license.email}`);
|
|
199
|
-
if (license.key) console.log(` Chave: ${license.key.substring(0, 7)}...${license.key.slice(-4)}`);
|
|
200
|
-
console.log(` Ativado: ${license.activatedAt ? new Date(license.activatedAt).toLocaleDateString('pt-BR') : 'N/A'}`);
|
|
201
|
-
console.log(` Expira: ${expires}`);
|
|
114
|
+
console.log(`\n🔓 Grimoire Pro — Status\n${sep}`);
|
|
115
|
+
console.log(` Status: ✅ Aberto (todos os recursos disponíveis)`);
|
|
116
|
+
console.log(` Licença: Sem necessidade de chave`);
|
|
117
|
+
console.log(` Features: ${PRO_FEATURES.length} recursos ativos`);
|
|
202
118
|
console.log(` ${sep}`);
|
|
203
|
-
|
|
204
|
-
showFeatureList(isPro);
|
|
119
|
+
showFeatureList(true);
|
|
205
120
|
}
|
|
206
121
|
|
|
207
122
|
// ── Features ───────────────────────────────────────────────────────────────────
|
|
208
123
|
function showFeatures() {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
console.log('\n🎯 Grimoire Pro — Features\n');
|
|
212
|
-
showFeatureList(active);
|
|
124
|
+
console.log('\n🎯 Grimoire Pro — Features (todas ativas)\n');
|
|
125
|
+
showFeatureList(true);
|
|
213
126
|
}
|
|
214
127
|
|
|
215
128
|
function showFeatureList(active) {
|
|
216
129
|
PRO_FEATURES.forEach(f => {
|
|
217
|
-
|
|
218
|
-
console.log(` ${status} ${f.name}`);
|
|
130
|
+
console.log(` ✅ ${f.name}`);
|
|
219
131
|
console.log(` ${f.desc}\n`);
|
|
220
132
|
});
|
|
221
|
-
if (!active) {
|
|
222
|
-
console.log(` 🔗 Ative em: https://grimoire.dev/pro`);
|
|
223
|
-
console.log(` grimoire pro activate\n`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// ── Deactivate ─────────────────────────────────────────────────────────────────
|
|
228
|
-
function deactivate() {
|
|
229
|
-
if (!fs.existsSync(LICENSE_FILE)) {
|
|
230
|
-
console.log('ℹ️ Nenhuma licença ativa encontrada.');
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
fs.unlinkSync(LICENSE_FILE);
|
|
234
|
-
console.log('✅ Licença removida. Grimoire voltou ao modo Community.');
|
|
235
133
|
}
|
|
236
134
|
|
|
237
135
|
module.exports = { run, isPro, hasFeature };
|
package/bin/grimoire-cli.js
CHANGED