grimoire-framework 1.5.0 โ 1.7.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/analyze.js +242 -0
- package/bin/commands/dashboard.js +358 -0
- package/bin/commands/memory.js +21 -1
- package/bin/commands/rag.js +297 -0
- package/bin/commands/search.js +99 -37
- package/bin/commands/setup.js +173 -0
- package/bin/commands/story.js +53 -13
- package/bin/grimoire-cli.js +15 -1
- package/package.json +4 -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-
|
|
10
|
+
version: 1.7.0
|
|
11
|
+
generated_at: "2026-02-22T18:11:23.264Z"
|
|
12
12
|
generator: scripts/generate-install-manifest.js
|
|
13
13
|
file_count: 1011
|
|
14
14
|
files:
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grimoire analyze โ Project Stack Analyzer
|
|
3
|
+
*
|
|
4
|
+
* ๐จ Da Vinci: detecta stack, frameworks, estrutura e sugere atividades
|
|
5
|
+
*
|
|
6
|
+
* grimoire analyze Analisa pasta atual e sugere atividades
|
|
7
|
+
* grimoire analyze --save Salva anรกlise como memรณrias no RAG
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
|
|
15
|
+
function findGrimoireDir() {
|
|
16
|
+
const cwd = process.cwd();
|
|
17
|
+
if (fs.existsSync(path.join(cwd, '.grimoire'))) return path.join(cwd, '.grimoire');
|
|
18
|
+
if (fs.existsSync(path.join(cwd, 'grimoire', '.grimoire'))) return path.join(cwd, 'grimoire', '.grimoire');
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// โโ Detectors โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
23
|
+
function detectFromPackageJson(cwd) {
|
|
24
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
25
|
+
if (!fs.existsSync(pkgPath)) return null;
|
|
26
|
+
|
|
27
|
+
let pkg;
|
|
28
|
+
try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch (_) { return null; }
|
|
29
|
+
|
|
30
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
31
|
+
const result = { name: pkg.name, version: pkg.version, runtime: 'Node.js', frameworks: [], tools: [], suggestions: [] };
|
|
32
|
+
|
|
33
|
+
// Frameworks
|
|
34
|
+
if (deps['next']) result.frameworks.push('Next.js');
|
|
35
|
+
if (deps['nuxt']) result.frameworks.push('Nuxt.js');
|
|
36
|
+
if (deps['react']) result.frameworks.push('React');
|
|
37
|
+
if (deps['vue']) result.frameworks.push('Vue');
|
|
38
|
+
if (deps['@angular/core']) result.frameworks.push('Angular');
|
|
39
|
+
if (deps['svelte']) result.frameworks.push('Svelte');
|
|
40
|
+
if (deps['express']) result.frameworks.push('Express');
|
|
41
|
+
if (deps['fastify']) result.frameworks.push('Fastify');
|
|
42
|
+
if (deps['@nestjs/core']) result.frameworks.push('NestJS');
|
|
43
|
+
if (deps['hono']) result.frameworks.push('Hono');
|
|
44
|
+
|
|
45
|
+
// Databases
|
|
46
|
+
if (deps['prisma'] || deps['@prisma/client']) result.tools.push('Prisma ORM');
|
|
47
|
+
if (deps['mongoose']) result.tools.push('MongoDB/Mongoose');
|
|
48
|
+
if (deps['sequelize']) result.tools.push('Sequelize');
|
|
49
|
+
if (deps['drizzle-orm']) result.tools.push('Drizzle ORM');
|
|
50
|
+
if (deps['pg']) result.tools.push('PostgreSQL');
|
|
51
|
+
if (deps['mysql2']) result.tools.push('MySQL');
|
|
52
|
+
if (deps['sqlite3'] || deps['better-sqlite3']) result.tools.push('SQLite');
|
|
53
|
+
if (deps['redis'] || deps['ioredis']) result.tools.push('Redis');
|
|
54
|
+
|
|
55
|
+
// Auth
|
|
56
|
+
if (deps['jsonwebtoken'] || deps['jose']) result.tools.push('JWT Auth');
|
|
57
|
+
if (deps['passport']) result.tools.push('Passport.js');
|
|
58
|
+
if (deps['next-auth']) result.tools.push('NextAuth');
|
|
59
|
+
if (deps['clerk']) result.tools.push('Clerk');
|
|
60
|
+
|
|
61
|
+
// Testing
|
|
62
|
+
if (deps['jest']) result.tools.push('Jest');
|
|
63
|
+
if (deps['vitest']) result.tools.push('Vitest');
|
|
64
|
+
if (deps['cypress']) result.tools.push('Cypress E2E');
|
|
65
|
+
if (deps['playwright']) result.tools.push('Playwright E2E');
|
|
66
|
+
|
|
67
|
+
// TypeScript
|
|
68
|
+
if (deps['typescript'] || pkg.scripts?.['typecheck']) result.tools.push('TypeScript');
|
|
69
|
+
|
|
70
|
+
// Hosting/Deploy
|
|
71
|
+
if (fs.existsSync(path.join(cwd, 'vercel.json'))) result.tools.push('Vercel');
|
|
72
|
+
if (fs.existsSync(path.join(cwd, 'railway.json'))) result.tools.push('Railway');
|
|
73
|
+
if (fs.existsSync(path.join(cwd, '.fly.toml'))) result.tools.push('Fly.io');
|
|
74
|
+
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function detectFromFiles(cwd) {
|
|
79
|
+
const result = { runtime: null, frameworks: [], tools: [] };
|
|
80
|
+
|
|
81
|
+
if (fs.existsSync(path.join(cwd, 'requirements.txt')) || fs.existsSync(path.join(cwd, 'pyproject.toml'))) {
|
|
82
|
+
result.runtime = 'Python';
|
|
83
|
+
try {
|
|
84
|
+
const req = fs.readFileSync(path.join(cwd, 'requirements.txt'), 'utf8');
|
|
85
|
+
if (req.includes('fastapi')) result.frameworks.push('FastAPI');
|
|
86
|
+
if (req.includes('django')) result.frameworks.push('Django');
|
|
87
|
+
if (req.includes('flask')) result.frameworks.push('Flask');
|
|
88
|
+
} catch (_) { }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) result.runtime = 'Rust';
|
|
92
|
+
if (fs.existsSync(path.join(cwd, 'go.mod'))) result.runtime = 'Go';
|
|
93
|
+
if (fs.existsSync(path.join(cwd, 'pom.xml'))) result.runtime = 'Java/Maven';
|
|
94
|
+
if (fs.existsSync(path.join(cwd, 'build.gradle'))) result.runtime = 'Java/Gradle';
|
|
95
|
+
|
|
96
|
+
if (fs.existsSync(path.join(cwd, 'docker-compose.yml'))) result.tools.push('Docker Compose');
|
|
97
|
+
if (fs.existsSync(path.join(cwd, 'Dockerfile'))) result.tools.push('Docker');
|
|
98
|
+
if (fs.existsSync(path.join(cwd, '.github', 'workflows'))) result.tools.push('GitHub Actions CI/CD');
|
|
99
|
+
if (fs.existsSync(path.join(cwd, 'terraform'))) result.tools.push('Terraform');
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function countFiles(cwd) {
|
|
105
|
+
const counts = {};
|
|
106
|
+
function walk(dir, depth = 0) {
|
|
107
|
+
if (depth > 3) return;
|
|
108
|
+
try {
|
|
109
|
+
for (const f of fs.readdirSync(dir)) {
|
|
110
|
+
if (f.startsWith('.') || f === 'node_modules' || f === '.git') continue;
|
|
111
|
+
const full = path.join(dir, f);
|
|
112
|
+
const stat = fs.statSync(full);
|
|
113
|
+
if (stat.isDirectory()) { walk(full, depth + 1); }
|
|
114
|
+
else {
|
|
115
|
+
const ext = path.extname(f).toLowerCase();
|
|
116
|
+
if (ext) counts[ext] = (counts[ext] || 0) + 1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (_) { }
|
|
120
|
+
}
|
|
121
|
+
walk(cwd);
|
|
122
|
+
return counts;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function generateSuggestions(pkg, files, extra) {
|
|
126
|
+
const suggestions = [];
|
|
127
|
+
|
|
128
|
+
const testFiles = (files['.test.js'] || 0) + (files['.spec.js'] || 0) + (files['.test.ts'] || 0);
|
|
129
|
+
if (testFiles === 0) suggestions.push({ title: 'Configurar cobertura de testes', priority: 'alta' });
|
|
130
|
+
|
|
131
|
+
if (!extra.has_ci) suggestions.push({ title: 'Configurar CI/CD (GitHub Actions)', priority: 'mรฉdia' });
|
|
132
|
+
if (!extra.has_readme) suggestions.push({ title: 'Criar/melhorar README.md', priority: 'mรฉdia' });
|
|
133
|
+
if (!extra.has_env_example) suggestions.push({ title: 'Criar .env.example', priority: 'baixa' });
|
|
134
|
+
if (!extra.has_docker) suggestions.push({ title: 'Containerizar com Docker', priority: 'baixa' });
|
|
135
|
+
|
|
136
|
+
if (pkg?.frameworks?.includes('Next.js') || pkg?.frameworks?.includes('React')) {
|
|
137
|
+
suggestions.push({ title: 'Auditoria de acessibilidade (a11y)', priority: 'mรฉdia' });
|
|
138
|
+
suggestions.push({ title: 'Implementar SEO/meta tags', priority: 'baixa' });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (pkg?.tools?.includes('PostgreSQL') || pkg?.tools?.includes('Prisma ORM')) {
|
|
142
|
+
suggestions.push({ title: 'Implementar connection pooling', priority: 'mรฉdia' });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return suggestions.slice(0, 6);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// โโ run โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
149
|
+
async function run(args) {
|
|
150
|
+
const save = args.includes('--save');
|
|
151
|
+
const cwd = process.cwd();
|
|
152
|
+
const sep = 'โ'.repeat(52);
|
|
153
|
+
|
|
154
|
+
console.log(`\n๐ Grimoire Analyze โ ${path.basename(cwd)}\n${sep}\n`);
|
|
155
|
+
console.log(' Analisando estrutura do projeto...\n');
|
|
156
|
+
|
|
157
|
+
// Detect
|
|
158
|
+
const pkg = detectFromPackageJson(cwd);
|
|
159
|
+
const extra = detectFromFiles(cwd);
|
|
160
|
+
const files = countFiles(cwd);
|
|
161
|
+
|
|
162
|
+
const runtime = pkg?.runtime || extra.runtime || 'Desconhecido';
|
|
163
|
+
const frameworks = [...(pkg?.frameworks || []), ...(extra.frameworks || [])];
|
|
164
|
+
const tools = [...(pkg?.tools || []), ...(extra.tools || [])];
|
|
165
|
+
|
|
166
|
+
const extraFlags = {
|
|
167
|
+
has_ci: fs.existsSync(path.join(cwd, '.github', 'workflows')),
|
|
168
|
+
has_readme: fs.existsSync(path.join(cwd, 'README.md')),
|
|
169
|
+
has_env_example: fs.existsSync(path.join(cwd, '.env.example')),
|
|
170
|
+
has_docker: fs.existsSync(path.join(cwd, 'Dockerfile')) || fs.existsSync(path.join(cwd, 'docker-compose.yml')),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Output
|
|
174
|
+
if (pkg?.name) console.log(` ๐ฆ Projeto: ${pkg.name}${pkg.version ? ` v${pkg.version}` : ''}`);
|
|
175
|
+
console.log(` โ๏ธ Runtime: ${runtime}`);
|
|
176
|
+
if (frameworks.length) console.log(` ๐๏ธ Frameworks: ${frameworks.join(', ')}`);
|
|
177
|
+
if (tools.length) console.log(` ๐ง Tools: ${tools.join(', ')}`);
|
|
178
|
+
|
|
179
|
+
// File counts
|
|
180
|
+
const topExts = Object.entries(files).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
181
|
+
if (topExts.length) {
|
|
182
|
+
console.log(`\n ๐ Arquivos: ${topExts.map(([e, n]) => `${n}ร${e}`).join(' ')}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check flags
|
|
186
|
+
console.log('\n ๐ Checklist:');
|
|
187
|
+
console.log(` ${extraFlags.has_readme ? 'โ
' : 'โ'} README.md`);
|
|
188
|
+
console.log(` ${extraFlags.has_env_example ? 'โ
' : 'โ'} .env.example`);
|
|
189
|
+
console.log(` ${extraFlags.has_ci ? 'โ
' : 'โ'} CI/CD configurado`);
|
|
190
|
+
console.log(` ${extraFlags.has_docker ? 'โ
' : 'โ'} Docker`);
|
|
191
|
+
|
|
192
|
+
// Suggestions
|
|
193
|
+
const suggestions = generateSuggestions(pkg, files, extraFlags);
|
|
194
|
+
if (suggestions.length) {
|
|
195
|
+
console.log('\n ๐ก Atividades sugeridas para o Backlog:\n');
|
|
196
|
+
suggestions.forEach((s, i) =>
|
|
197
|
+
console.log(` ${i + 1}. [${s.priority.toUpperCase()}] ${s.title}`)
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Save to grimoire
|
|
202
|
+
if (save || suggestions.length > 0) {
|
|
203
|
+
const grimoireDir = findGrimoireDir();
|
|
204
|
+
if (grimoireDir) {
|
|
205
|
+
// Save as memory
|
|
206
|
+
if (save) {
|
|
207
|
+
const { run: memRun } = require('./memory');
|
|
208
|
+
const stackSummary = `Stack: ${runtime}${frameworks.length ? ' + ' + frameworks.join(' + ') : ''}${tools.length ? ' ยท ' + tools.slice(0, 3).join(', ') : ''}`;
|
|
209
|
+
await memRun(['save', '--tag', 'stack', stackSummary]);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Create suggested stories
|
|
213
|
+
if (suggestions.length > 0) {
|
|
214
|
+
console.log('\n Criar atividades sugeridas no Backlog? [S/N] ');
|
|
215
|
+
if (process.stdin.isTTY) {
|
|
216
|
+
const readline = require('readline');
|
|
217
|
+
readline.emitKeypressEvents(process.stdin);
|
|
218
|
+
process.stdin.setRawMode?.(true);
|
|
219
|
+
await new Promise(resolve => {
|
|
220
|
+
process.stdin.once('keypress', (str) => {
|
|
221
|
+
process.stdin.setRawMode?.(false);
|
|
222
|
+
if ((str || '').toLowerCase() === 's') {
|
|
223
|
+
console.log('\n Criando...');
|
|
224
|
+
const { run: storyRun } = require('./story');
|
|
225
|
+
suggestions.forEach(s => storyRun(['create', s.title, '--backlog']));
|
|
226
|
+
console.log(` โ
${suggestions.length} atividades criadas no Backlog\n`);
|
|
227
|
+
} else {
|
|
228
|
+
console.log(' Pulado.\n');
|
|
229
|
+
}
|
|
230
|
+
resolve();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log(`\n${sep}`);
|
|
239
|
+
console.log(' grimoire dashboard โ ver kanban\n');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = { run };
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grimoire dashboard โ Kanban TUI
|
|
3
|
+
*
|
|
4
|
+
* ๐ญ Design: Matisse (UX) ยท ๐จ Da Vinci (Dev) ยท ๐๏ธ Gaudรญ (Arquitetura)
|
|
5
|
+
*
|
|
6
|
+
* Exibe kanban com colunas: BACKLOG | EM ANDAMENTO | CONCLUรDO
|
|
7
|
+
* Com wizard se nenhum projeto for detectado.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const readline = require('readline');
|
|
16
|
+
|
|
17
|
+
// โโ Paths โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
18
|
+
function findGrimoireDir() {
|
|
19
|
+
const cwd = process.cwd();
|
|
20
|
+
if (fs.existsSync(path.join(cwd, '.grimoire'))) return path.join(cwd, '.grimoire');
|
|
21
|
+
if (fs.existsSync(path.join(cwd, 'grimoire', '.grimoire'))) return path.join(cwd, 'grimoire', '.grimoire');
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getProjectName(grimoireDir) {
|
|
26
|
+
try {
|
|
27
|
+
const pkg = path.join(path.dirname(grimoireDir), 'package.json');
|
|
28
|
+
if (fs.existsSync(pkg)) return JSON.parse(fs.readFileSync(pkg, 'utf8')).name || 'Projeto';
|
|
29
|
+
} catch (_) { }
|
|
30
|
+
return path.basename(path.dirname(grimoireDir));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadConfig(grimoireDir) {
|
|
34
|
+
const f = path.join(grimoireDir, 'config.yaml');
|
|
35
|
+
if (!fs.existsSync(f)) return {};
|
|
36
|
+
const cfg = {};
|
|
37
|
+
fs.readFileSync(f, 'utf8').split('\n').forEach(l => {
|
|
38
|
+
const m = l.match(/^(\w[\w_]*)\s*:\s*(.+)$/);
|
|
39
|
+
if (m) cfg[m[1].trim()] = m[2].trim();
|
|
40
|
+
});
|
|
41
|
+
return cfg;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function loadStories(grimoireDir) {
|
|
45
|
+
const storiesDir = path.join(grimoireDir, 'stories');
|
|
46
|
+
if (!fs.existsSync(storiesDir)) return [];
|
|
47
|
+
return fs.readdirSync(storiesDir)
|
|
48
|
+
.filter(f => f.endsWith('.json'))
|
|
49
|
+
.map(f => { try { return JSON.parse(fs.readFileSync(path.join(storiesDir, f), 'utf8')); } catch (_) { return null; } })
|
|
50
|
+
.filter(Boolean)
|
|
51
|
+
.sort((a, b) => a.createdAt > b.createdAt ? 1 : -1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// โโ Time helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
55
|
+
function fmtDate(iso) {
|
|
56
|
+
if (!iso) return 'โ';
|
|
57
|
+
return iso.slice(0, 10).split('-').slice(1).join('/'); // MM/DD
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function timeSpent(story) {
|
|
61
|
+
if (!story.startedAt) return null;
|
|
62
|
+
const end = story.completedAt ? new Date(story.completedAt) : new Date();
|
|
63
|
+
const ms = end - new Date(story.startedAt);
|
|
64
|
+
const h = Math.floor(ms / 3600000);
|
|
65
|
+
const m = Math.floor((ms % 3600000) / 60000);
|
|
66
|
+
return `${h}h ${m}min`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// โโ RAG status (quick check) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
70
|
+
function ragStatus(config) {
|
|
71
|
+
const p = config.embedding_provider;
|
|
72
|
+
if (!p || p === 'tfidf') return null;
|
|
73
|
+
return p;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// โโ Today's memory count โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
77
|
+
function todayMemCount(grimoireDir) {
|
|
78
|
+
const today = new Date().toISOString().split('T')[0];
|
|
79
|
+
const f = path.join(grimoireDir, 'memory', 'sessions', `${today}.jsonl`);
|
|
80
|
+
if (!fs.existsSync(f)) return 0;
|
|
81
|
+
return fs.readFileSync(f, 'utf8').split('\n').filter(l => l.trim()).length;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// โโ Terminal width โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
85
|
+
function termWidth() {
|
|
86
|
+
return Math.max(process.stdout.columns || 80, 80);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// โโ Box-drawing helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
90
|
+
const C = {
|
|
91
|
+
tl: 'โ', tr: 'โ', bl: 'โ', br: 'โ',
|
|
92
|
+
h: 'โ', v: 'โ',
|
|
93
|
+
ml: 'โ ', mr: 'โฃ', mt: 'โฆ', mb: 'โฉ', mc: 'โฌ',
|
|
94
|
+
sep: 'โ', lsep: 'โ', rsep: 'โค',
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function pad(str, len, char = ' ') {
|
|
98
|
+
const visible = stripAnsi(str).length;
|
|
99
|
+
const diff = len - visible;
|
|
100
|
+
return diff > 0 ? str + char.repeat(diff) : str;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function stripAnsi(str) {
|
|
104
|
+
return String(str).replace(/\x1b\[[0-9;]*m/g, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function truncate(str, len) {
|
|
108
|
+
const clean = stripAnsi(str);
|
|
109
|
+
if (clean.length <= len) return str;
|
|
110
|
+
return clean.slice(0, len - 1) + 'โฆ';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// โโ Colors โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
114
|
+
const dim = s => `\x1b[2m${s}\x1b[0m`;
|
|
115
|
+
const bold = s => `\x1b[1m${s}\x1b[0m`;
|
|
116
|
+
const cyan = s => `\x1b[36m${s}\x1b[0m`;
|
|
117
|
+
const yellow = s => `\x1b[33m${s}\x1b[0m`;
|
|
118
|
+
const green = s => `\x1b[32m${s}\x1b[0m`;
|
|
119
|
+
const red = s => `\x1b[31m${s}\x1b[0m`;
|
|
120
|
+
const magenta = s => `\x1b[35m${s}\x1b[0m`;
|
|
121
|
+
|
|
122
|
+
// โโ Status icons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
123
|
+
function statusIcon(status) {
|
|
124
|
+
if (status === 'done') return green('โ');
|
|
125
|
+
if (status === 'in_progress') return yellow('โ');
|
|
126
|
+
return dim('โ');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// โโ Render single card โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
130
|
+
function renderCard(story, colW) {
|
|
131
|
+
const inner = colW - 2; // minus borders
|
|
132
|
+
const lines = [];
|
|
133
|
+
|
|
134
|
+
// ID + icon
|
|
135
|
+
const idStr = bold(cyan(truncate(story.id, 8)));
|
|
136
|
+
const icon = statusIcon(story.status);
|
|
137
|
+
lines.push(` ${icon} ${idStr}`);
|
|
138
|
+
|
|
139
|
+
// Title
|
|
140
|
+
lines.push(` ${truncate(story.title, inner - 1)}`);
|
|
141
|
+
|
|
142
|
+
// Dates
|
|
143
|
+
if (story.status === 'backlog') {
|
|
144
|
+
const dead = story.deliveryDate ? `Entrega: ${fmtDate(story.deliveryDate)}` : dim('Sem prazo');
|
|
145
|
+
lines.push(` ${dim(truncate(dead, inner - 1))}`);
|
|
146
|
+
} else if (story.status === 'in_progress') {
|
|
147
|
+
const start = `Inรญcio: ${fmtDate(story.startedAt || story.createdAt)}`;
|
|
148
|
+
const dead = story.deliveryDate ? `Entrega: ${fmtDate(story.deliveryDate)}` : '';
|
|
149
|
+
lines.push(` ${dim(truncate(start, inner - 1))}`);
|
|
150
|
+
if (dead) lines.push(` ${dim(truncate(dead, inner - 1))}`);
|
|
151
|
+
const spent = timeSpent(story);
|
|
152
|
+
if (spent) lines.push(` ${dim('โฑ')} ${dim(spent)}`);
|
|
153
|
+
} else {
|
|
154
|
+
// done
|
|
155
|
+
const done = story.completedAt ? `โ ${fmtDate(story.completedAt)}` : '';
|
|
156
|
+
const spent = timeSpent(story);
|
|
157
|
+
if (done) lines.push(` ${green(truncate(done, inner - 1))}`);
|
|
158
|
+
if (spent) lines.push(` ${dim('โฑ')} ${dim(spent)}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return lines;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// โโ Render full kanban โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
165
|
+
function renderKanban(stories, grimoireDir, config) {
|
|
166
|
+
const W = termWidth();
|
|
167
|
+
const colW = Math.floor((W - 4) / 3); // 3 cols, 4 borders
|
|
168
|
+
|
|
169
|
+
const backlog = stories.filter(s => s.status === 'backlog');
|
|
170
|
+
const inProgress = stories.filter(s => s.status === 'in_progress');
|
|
171
|
+
const done = stories.filter(s => s.status === 'done').slice(-5); // last 5
|
|
172
|
+
|
|
173
|
+
const output = [];
|
|
174
|
+
|
|
175
|
+
// Header
|
|
176
|
+
const projectName = getProjectName(grimoireDir);
|
|
177
|
+
const today = new Date().toISOString().split('T')[0];
|
|
178
|
+
const rag = ragStatus(config);
|
|
179
|
+
const ragBadge = rag ? cyan(` ๐ค ${rag}`) : '';
|
|
180
|
+
const headerText = bold(`๐ฎ Grimoire ยท ${projectName}`) + ` ${dim(today)}${ragBadge}`;
|
|
181
|
+
const headerInner = pad(headerText, W - 2);
|
|
182
|
+
output.push(C.tl + C.h.repeat(W - 2) + C.tr);
|
|
183
|
+
output.push(C.v + headerInner + C.v);
|
|
184
|
+
|
|
185
|
+
// Column headers separator
|
|
186
|
+
output.push(C.ml + C.h.repeat(colW) + C.mt + C.h.repeat(colW) + C.mt + C.h.repeat(W - 2 - colW * 2 - 2) + C.mr);
|
|
187
|
+
|
|
188
|
+
const col1H = bold(' ๐ BACKLOG') + dim(` (${backlog.length})`);
|
|
189
|
+
const col2H = bold(' ๐ EM ANDAMENTO') + dim(` (${inProgress.length})`);
|
|
190
|
+
const col3H = bold(' โ
CONCLUรDO') + dim(` (${done.length})`);
|
|
191
|
+
const colW3 = W - 2 - colW * 2 - 2;
|
|
192
|
+
output.push(C.v + pad(col1H, colW) + C.v + pad(col2H, colW) + C.v + pad(col3H, colW3) + C.v);
|
|
193
|
+
|
|
194
|
+
// Row separator
|
|
195
|
+
output.push(C.ml + C.h.repeat(colW) + C.mc + C.h.repeat(colW) + C.mc + C.h.repeat(colW3) + C.mr);
|
|
196
|
+
|
|
197
|
+
// Cards
|
|
198
|
+
const col1Cards = backlog.map(s => renderCard(s, colW));
|
|
199
|
+
const col2Cards = inProgress.map(s => renderCard(s, colW));
|
|
200
|
+
const col3Cards = done.map(s => renderCard(s, colW3 + 2));
|
|
201
|
+
|
|
202
|
+
const maxRows = Math.max(col1Cards.flat().length + col1Cards.length,
|
|
203
|
+
col2Cards.flat().length + col2Cards.length,
|
|
204
|
+
col3Cards.flat().length + col3Cards.length, 3);
|
|
205
|
+
|
|
206
|
+
let r1 = 0, r2 = 0, r3 = 0;
|
|
207
|
+
let ci1 = 0, ci2 = 0, ci3 = 0;
|
|
208
|
+
|
|
209
|
+
for (let row = 0; row < maxRows; row++) {
|
|
210
|
+
const getCell = (cards, ci, ri) => {
|
|
211
|
+
if (ci >= cards.length) return { line: '', nextCi: ci, nextRi: ri };
|
|
212
|
+
const card = cards[ci];
|
|
213
|
+
if (ri < card.length) return { line: card[ri], nextCi: ci, nextRi: ri + 1 };
|
|
214
|
+
if (ri === card.length) return { line: dim(C.sep.repeat(Math.max(0, colW - 2))), nextCi: ci + 1, nextRi: 0 };
|
|
215
|
+
return { line: '', nextCi: ci + 1, nextRi: 0 };
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const { line: l1, nextCi: nc1, nextRi: nr1 } = getCell(col1Cards, ci1, r1);
|
|
219
|
+
const { line: l2, nextCi: nc2, nextRi: nr2 } = getCell(col2Cards, ci2, r2);
|
|
220
|
+
const { line: l3, nextCi: nc3, nextRi: nr3 } = getCell(col3Cards, ci3, r3);
|
|
221
|
+
|
|
222
|
+
ci1 = nc1; r1 = nr1; ci2 = nc2; r2 = nr2; ci3 = nc3; r3 = nr3;
|
|
223
|
+
|
|
224
|
+
output.push(
|
|
225
|
+
C.v + pad(l1, colW) + C.v + pad(l2, colW) + C.v + pad(l3, colW3) + C.v
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Footer separator
|
|
230
|
+
output.push(C.ml + C.h.repeat(colW) + C.mb + C.h.repeat(colW) + C.mb + C.h.repeat(colW3) + C.mr);
|
|
231
|
+
|
|
232
|
+
// Status bar
|
|
233
|
+
const memCount = todayMemCount(grimoireDir);
|
|
234
|
+
const squad = config.default_squad || '';
|
|
235
|
+
const statusBar = dim(` ๐ง ${memCount} mem hoje`) +
|
|
236
|
+
(squad ? dim(` ยท squad: ${squad}`) : '') +
|
|
237
|
+
` ยท ${dim('[N]')}ova ${dim('[S]')}ession ${dim('[Enter]')} Detalhar ${dim('[Q]')}sair`;
|
|
238
|
+
output.push(C.v + pad(statusBar, W - 2) + C.v);
|
|
239
|
+
output.push(C.bl + C.h.repeat(W - 2) + C.br);
|
|
240
|
+
|
|
241
|
+
console.log('\n' + output.join('\n'));
|
|
242
|
+
|
|
243
|
+
return { backlog, inProgress, done: stories.filter(s => s.status === 'done') };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// โโ Interactive keys โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
247
|
+
function waitForKey(stories, grimoireDir) {
|
|
248
|
+
if (!process.stdin.isTTY) return;
|
|
249
|
+
|
|
250
|
+
readline.emitKeypressEvents(process.stdin);
|
|
251
|
+
process.stdin.setRawMode?.(true);
|
|
252
|
+
process.stdin.resume();
|
|
253
|
+
|
|
254
|
+
process.stdin.once('keypress', (str, key) => {
|
|
255
|
+
process.stdin.setRawMode?.(false);
|
|
256
|
+
process.stdin.pause();
|
|
257
|
+
|
|
258
|
+
const k = (key?.name || str || '').toLowerCase();
|
|
259
|
+
|
|
260
|
+
if (k === 'q' || key?.ctrl && k === 'c') {
|
|
261
|
+
console.log('');
|
|
262
|
+
process.exit(0);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (k === 'n') {
|
|
266
|
+
// New story โ delegate to story create wizard
|
|
267
|
+
console.log('\n๐ Criando nova atividade...');
|
|
268
|
+
console.log(' grimoire story create "Tรญtulo" [--deadline YYYY-MM-DD]\n');
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (k === 's') {
|
|
273
|
+
console.log('\n๐ Iniciando sessรฃo...');
|
|
274
|
+
try { require('./session').run([]); }
|
|
275
|
+
catch (_) { console.log(' grimoire session start\n'); process.exit(0); }
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (k === 'return' || k === 'enter') {
|
|
280
|
+
// Show current in_progress story details
|
|
281
|
+
const active = stories.find(s => s.status === 'in_progress');
|
|
282
|
+
if (active) {
|
|
283
|
+
try { require('./story').run(['show', active.id]); }
|
|
284
|
+
catch (_) { }
|
|
285
|
+
} else {
|
|
286
|
+
console.log('\n Nenhuma atividade em andamento.\n');
|
|
287
|
+
}
|
|
288
|
+
process.exit(0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Any other key โ exit
|
|
292
|
+
console.log('');
|
|
293
|
+
process.exit(0);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// โโ No-project wizard โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
298
|
+
async function showNoProjectWizard() {
|
|
299
|
+
const cwd = process.cwd();
|
|
300
|
+
const W = Math.min(termWidth(), 50);
|
|
301
|
+
|
|
302
|
+
console.log('\n' + 'โ' + 'โ'.repeat(W - 2) + 'โ');
|
|
303
|
+
console.log('โ' + pad(bold(' ๐ฎ Grimoire Framework'), W - 2) + 'โ');
|
|
304
|
+
console.log('โ' + pad(dim(` ${cwd}`), W - 2) + 'โ');
|
|
305
|
+
console.log('โ ' + 'โ'.repeat(W - 2) + 'โฃ');
|
|
306
|
+
console.log('โ' + pad(yellow(' โ ๏ธ Nenhum projeto detectado.'), W - 2) + 'โ');
|
|
307
|
+
console.log('โ' + ' '.repeat(W - 2) + 'โ');
|
|
308
|
+
console.log('โ' + pad(' O que vocรช quer fazer?', W - 2) + 'โ');
|
|
309
|
+
console.log('โ' + ' '.repeat(W - 2) + 'โ');
|
|
310
|
+
console.log('โ' + pad(' โฏ ๐ Novo projeto [N]', W - 2) + 'โ');
|
|
311
|
+
console.log('โ' + pad(' ๐ Analisar pasta atual [A]', W - 2) + 'โ');
|
|
312
|
+
console.log('โ' + pad(' โ Sair [Q]', W - 2) + 'โ');
|
|
313
|
+
console.log('โ' + 'โ'.repeat(W - 2) + 'โ\n');
|
|
314
|
+
|
|
315
|
+
if (!process.stdin.isTTY) return;
|
|
316
|
+
|
|
317
|
+
readline.emitKeypressEvents(process.stdin);
|
|
318
|
+
process.stdin.setRawMode?.(true);
|
|
319
|
+
process.stdin.resume();
|
|
320
|
+
|
|
321
|
+
await new Promise(resolve => {
|
|
322
|
+
process.stdin.once('keypress', (str, key) => {
|
|
323
|
+
process.stdin.setRawMode?.(false);
|
|
324
|
+
process.stdin.pause();
|
|
325
|
+
const k = (key?.name || str || '').toLowerCase();
|
|
326
|
+
|
|
327
|
+
if (k === 'n') {
|
|
328
|
+
console.log('๐ Iniciando novo projeto...');
|
|
329
|
+
console.log(' npx grimoire-framework install\n');
|
|
330
|
+
} else if (k === 'a') {
|
|
331
|
+
console.log('๐ Analisando pasta...');
|
|
332
|
+
try { require('./analyze').run([]); }
|
|
333
|
+
catch (_) { console.log(' grimoire analyze\n'); }
|
|
334
|
+
} else {
|
|
335
|
+
console.log('');
|
|
336
|
+
}
|
|
337
|
+
resolve();
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// โโ run โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
343
|
+
async function run(args) {
|
|
344
|
+
const grimoireDir = findGrimoireDir();
|
|
345
|
+
|
|
346
|
+
if (!grimoireDir) {
|
|
347
|
+
await showNoProjectWizard();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const config = loadConfig(grimoireDir);
|
|
352
|
+
const stories = loadStories(grimoireDir);
|
|
353
|
+
|
|
354
|
+
const { backlog } = renderKanban(stories, grimoireDir, config);
|
|
355
|
+
waitForKey(stories, grimoireDir);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
module.exports = { run, renderKanban };
|
package/bin/commands/memory.js
CHANGED
|
@@ -70,7 +70,7 @@ async function run(args) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// โโ Auto-update context (silent)
|
|
73
|
+
// โโ Auto-update context + RAG index (silent) โโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
74
74
|
function silentContextUpdate(grimoireDir) {
|
|
75
75
|
try {
|
|
76
76
|
const { doUpdate } = require('./context');
|
|
@@ -78,6 +78,24 @@ function silentContextUpdate(grimoireDir) {
|
|
|
78
78
|
} catch (_) { } // never block memory save
|
|
79
79
|
}
|
|
80
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
|
+
|
|
81
99
|
/**
|
|
82
100
|
* Saves memory using JSONL append (O(1) complexity)
|
|
83
101
|
*/
|
|
@@ -167,6 +185,8 @@ async function saveMemory(args, memoryDir, grimoireDir) {
|
|
|
167
185
|
console.log(`โ
Memory saved${tagLabel}${storyLabel}`);
|
|
168
186
|
// Auto-update .grimoire/context/ silently
|
|
169
187
|
silentContextUpdate(grimoireDir);
|
|
188
|
+
// Auto-index for RAG (fire-and-forget)
|
|
189
|
+
silentRagIndex(entry, today, grimoireDir);
|
|
170
190
|
} catch (err) {
|
|
171
191
|
console.error(`โ Failed to acquire lock for memory file: ${err.message}`);
|
|
172
192
|
} finally {
|