zaileys 3.1.0 → 3.2.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/dist/index.js +2 -2
- package/dist/index.mjs +2 -2
- package/package.json +5 -2
- package/.changeset/README.md +0 -8
- package/.changeset/config.json +0 -11
- package/.kelar/kelar-tools.cjs +0 -829
- package/.kelar/memory/INDEX.md +0 -21
- package/.kelar/memory/technical/issue-34-sendpresenceupdate-undefined.md +0 -4
- package/.kelar/memory/technical/lmdb-native-binary-fallback.md +0 -4
- package/.kelar/memory/technical/nedbadapter-ignoring-encoder-causes-buffer-typing-crash-err-invalid-arg-type.md +0 -4
- package/.kelar/memory/technical/signal-media-constructor-input-contract.md +0 -4
- package/.kelar/memory/technical/sticker-example-source-format.md +0 -4
- package/.kelar/memory/technical/ts2307-missing-seald-io-nedb.md +0 -4
- package/.kelar/memory/technical/ts2307-missing-zaileys-store-adapters.md +0 -4
- package/.kelar/plans/README.md +0 -3
- package/.kelar/research/README.md +0 -3
- package/.kelar/research/mysql-adapter-research.md +0 -32
- package/.kelar/state/ASSUMPTIONS.md +0 -14
- package/.kelar/state/DEBT.md +0 -13
- package/.kelar/state/DIARY.md +0 -15
- package/.kelar/state/HANDOFF.md +0 -54
- package/.kelar/state/PATTERNS.md +0 -43
- package/.kelar/state/STATE.md +0 -37
- package/.kelar/state/TASKS.md +0 -146
- package/AGENTS.md +0 -108
- package/CHANGELOG.md +0 -12
- package/GEMINI.md +0 -58
- package/packages/media-process/CHANGELOG.md +0 -7
- package/packages/media-process/LICENSE +0 -21
- package/packages/media-process/dist/index.d.mts +0 -142
- package/packages/media-process/dist/index.d.ts +0 -142
- package/packages/media-process/dist/index.mjs +0 -3
- package/packages/media-process/package.json +0 -39
- package/packages/media-process/tsconfig.json +0 -15
- package/packages/mysql-adapter/dist/index.d.mts +0 -36
- package/packages/mysql-adapter/dist/index.d.ts +0 -36
- package/packages/mysql-adapter/dist/index.js.map +0 -1
- package/packages/mysql-adapter/dist/index.mjs +0 -101
- package/packages/mysql-adapter/dist/index.mjs.map +0 -1
- package/packages/mysql-adapter/package.json +0 -36
- package/pnpm-workspace.yaml +0 -3
- package/tsconfig.json +0 -19
package/.kelar/kelar-tools.cjs
DELETED
|
@@ -1,829 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* ╔═══════════════════════════════════════════════════════════════╗
|
|
6
|
-
* ║ KELAR TOOLS v2 — Ultra Magic Edition ║
|
|
7
|
-
* ║ ║
|
|
8
|
-
* ║ The brain behind KELAR's multi-agent system. ║
|
|
9
|
-
* ║ Not just a CLI — a context intelligence layer. ║
|
|
10
|
-
* ╚═══════════════════════════════════════════════════════════════╝
|
|
11
|
-
*
|
|
12
|
-
* STANDARD (v1):
|
|
13
|
-
* state get|patch|snapshot
|
|
14
|
-
* tasks log|active|complete|pause
|
|
15
|
-
* memory search|save|index
|
|
16
|
-
* patterns get|set|list
|
|
17
|
-
* handoff write|read
|
|
18
|
-
* plan validate|tasks|wave
|
|
19
|
-
* git status|changed|commit|checkpoint
|
|
20
|
-
* debt add|list
|
|
21
|
-
* session start|end
|
|
22
|
-
* health · version
|
|
23
|
-
*
|
|
24
|
-
* ULTRA MAGIC (v2):
|
|
25
|
-
* context build [task] Token-aware context assembler for agents
|
|
26
|
-
* context inject [agent] Generate complete Task() spawn prompt, pre-loaded
|
|
27
|
-
* scan risk [path] Static analysis: secrets, N+1, empty catch, any types
|
|
28
|
-
* scan imports [file] Full import dependency graph for a file
|
|
29
|
-
* impact score [file] 0-100 importance score based on dependents
|
|
30
|
-
* diff smart [from] [to] Semantic diff: breaking changes, new exports
|
|
31
|
-
* tokens estimate [path] Token count per file/dir, context window fit
|
|
32
|
-
* plan generate [desc] Auto-generate XML plan skeleton from description
|
|
33
|
-
* knowledge extract Analyze git commits, suggest knowledge entries
|
|
34
|
-
* timeline [N] Project timeline from TASKS.md + git log
|
|
35
|
-
* debt score Technical debt score (0-100) with trend tracking
|
|
36
|
-
* similar [file] Find 5 most structurally similar files
|
|
37
|
-
* watch [path] File watcher: auto-log changes to TASKS.md
|
|
38
|
-
* agent brief [name] Complete spawn prompt for any KELAR agent
|
|
39
|
-
*/
|
|
40
|
-
|
|
41
|
-
const fs = require('fs');
|
|
42
|
-
const path = require('path');
|
|
43
|
-
const { execSync } = require('child_process');
|
|
44
|
-
|
|
45
|
-
// ─── Config ────────────────────────────────────────────────────────────────────
|
|
46
|
-
const VERSION = '2.0.0';
|
|
47
|
-
const KELAR_DIR = findKelarDir();
|
|
48
|
-
|
|
49
|
-
function p(rel) { return path.join(KELAR_DIR, rel); }
|
|
50
|
-
const STATE_F = p('state/STATE.md');
|
|
51
|
-
const TASKS_F = p('state/TASKS.md');
|
|
52
|
-
const PATTERNS_F = p('state/PATTERNS.md');
|
|
53
|
-
const DEBT_F = p('state/DEBT.md');
|
|
54
|
-
const DIARY_F = p('state/DIARY.md');
|
|
55
|
-
const HANDOFF_F = p('state/HANDOFF.md');
|
|
56
|
-
const MEMORY_DIR = p('memory');
|
|
57
|
-
const MEMORY_IDX = p('memory/INDEX.md');
|
|
58
|
-
const AGENTS_DIR = p('agents');
|
|
59
|
-
|
|
60
|
-
function findKelarDir() {
|
|
61
|
-
let dir = process.cwd();
|
|
62
|
-
for (let i = 0; i < 6; i++) {
|
|
63
|
-
const c = path.join(dir, '.kelar');
|
|
64
|
-
if (fs.existsSync(c)) return c;
|
|
65
|
-
dir = path.dirname(dir);
|
|
66
|
-
}
|
|
67
|
-
return path.join(process.cwd(), '.kelar');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ─── Utils ─────────────────────────────────────────────────────────────────────
|
|
71
|
-
const now = () => new Date().toISOString().replace('T', ' ').split('.')[0];
|
|
72
|
-
const nowDate = () => new Date().toISOString().split('T')[0];
|
|
73
|
-
const readF = f => fs.existsSync(f) ? fs.readFileSync(f, 'utf8') : '';
|
|
74
|
-
const appendF = (f, s) => { ensureF(f); fs.appendFileSync(f, '\n' + s, 'utf8'); };
|
|
75
|
-
const ensureF = (f, d = '') => {
|
|
76
|
-
if (!fs.existsSync(f)) {
|
|
77
|
-
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
78
|
-
fs.writeFileSync(f, d);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
function out(data) {
|
|
83
|
-
process.stdout.write((typeof data === 'object' ? JSON.stringify(data, null, 2) : String(data)) + '\n');
|
|
84
|
-
}
|
|
85
|
-
function die(msg) { process.stderr.write('ERROR: ' + msg + '\n'); process.exit(1); }
|
|
86
|
-
function bash(cmd) {
|
|
87
|
-
try { return execSync(cmd, { encoding: 'utf8' }).trim(); }
|
|
88
|
-
catch { return ''; }
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function walkFiles(dir, exts) {
|
|
92
|
-
const defaultExts = ['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs'];
|
|
93
|
-
const useExts = exts !== undefined ? exts : defaultExts;
|
|
94
|
-
const result = [];
|
|
95
|
-
const ignored = new Set(['node_modules', '.git', '.kelar', 'dist', 'build', '.next', 'coverage', '__pycache__']);
|
|
96
|
-
if (!fs.existsSync(dir)) return result;
|
|
97
|
-
function recurse(d) {
|
|
98
|
-
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
99
|
-
const full = path.join(d, e.name);
|
|
100
|
-
if (e.isDirectory() && !ignored.has(e.name)) recurse(full);
|
|
101
|
-
else if (e.isFile() && (useExts.length === 0 || useExts.some(x => e.name.endsWith(x)))) result.push(full);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
recurse(dir);
|
|
105
|
-
return result;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function estimateTokens(text) { return Math.ceil(text.length / 4); }
|
|
109
|
-
|
|
110
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
111
|
-
// V1 — STANDARD COMMANDS
|
|
112
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
113
|
-
|
|
114
|
-
function stateGet(field) {
|
|
115
|
-
const c = readF(STATE_F);
|
|
116
|
-
const m = c.match(new RegExp(`^${field}\\s*:\\s*(.+)$`, 'm'));
|
|
117
|
-
if (m) { out(m[1].trim()); return; }
|
|
118
|
-
const s = c.match(new RegExp(`^## ${field}\\s*\\n([\\s\\S]*?)(?=^## |$)`, 'm'));
|
|
119
|
-
out(s ? s[1].trim() : '');
|
|
120
|
-
}
|
|
121
|
-
function statePatch(field, value) {
|
|
122
|
-
let c = readF(STATE_F);
|
|
123
|
-
const re = new RegExp(`^(${field}\\s*:\\s*)(.+)$`, 'm');
|
|
124
|
-
c = re.test(c) ? c.replace(re, `$1${value}`) : c + `\n${field}: ${value}`;
|
|
125
|
-
fs.writeFileSync(STATE_F, c, 'utf8');
|
|
126
|
-
out({ ok: true, field, value });
|
|
127
|
-
}
|
|
128
|
-
function stateSnapshot() {
|
|
129
|
-
const c = readF(STATE_F);
|
|
130
|
-
const snap = {};
|
|
131
|
-
for (const f of ['Type', 'Stack', 'Working on', 'Progress']) {
|
|
132
|
-
const m = c.match(new RegExp(`^${f}\\s*:\\s*(.+)$`, 'm'));
|
|
133
|
-
if (m) snap[f.toLowerCase().replace(/ /g, '_')] = m[1].trim();
|
|
134
|
-
}
|
|
135
|
-
const fm = c.match(/## Current Feature\s*\n([\s\S]*?)(?=^## |$)/m);
|
|
136
|
-
if (fm) snap.current_feature = fm[1].trim().split('\n')[0];
|
|
137
|
-
out(snap);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function tasksLog(type, message) {
|
|
141
|
-
const ts = now();
|
|
142
|
-
const map = {
|
|
143
|
-
start: `\n## [${ts}] TASK STARTED\n${message}`,
|
|
144
|
-
done: `[${ts}] ✅ ${message}`,
|
|
145
|
-
pause: `\n## [${ts}] PAUSED ⏸\n${message}`,
|
|
146
|
-
note: `[${ts}] 📝 ${message}`,
|
|
147
|
-
notice: `[${ts}] 🔍 NOTICED: ${message}`,
|
|
148
|
-
knowledge: `[${ts}] 📚 KNOWLEDGE: ${message}`,
|
|
149
|
-
error: `[${ts}] ❌ ERROR: ${message}`,
|
|
150
|
-
wave: `\n### [${ts}] WAVE COMPLETE\n${message}`,
|
|
151
|
-
feature_start: `\n## [${ts}] FEATURE STARTED: ${message}`,
|
|
152
|
-
feature_done: `\n## [${ts}] FEATURE COMPLETE ✅: ${message}`,
|
|
153
|
-
fix_start: `\n## [${ts}] FIX STARTED: ${message}`,
|
|
154
|
-
fix_done: `\n## [${ts}] FIX COMPLETE ✅: ${message}`,
|
|
155
|
-
};
|
|
156
|
-
appendF(TASKS_F, map[type] || `[${ts}] ${type.toUpperCase()}: ${message}`);
|
|
157
|
-
out({ ok: true, type, timestamp: ts });
|
|
158
|
-
}
|
|
159
|
-
function tasksActive() {
|
|
160
|
-
const content = readF(TASKS_F);
|
|
161
|
-
const blocks = content.split(/(?=^## \[)/m).filter(Boolean);
|
|
162
|
-
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
163
|
-
const block = blocks[i];
|
|
164
|
-
if (block.includes('TASK STARTED') || block.includes('FEATURE STARTED')) {
|
|
165
|
-
const name = block.match(/STARTED[^:]*:\s*(.+)/)?.[1]?.trim() || 'unknown';
|
|
166
|
-
const completedLater = blocks.slice(i + 1).some(b =>
|
|
167
|
-
b.includes('COMPLETE') && b.includes(name.split(' ').slice(0, 3).join(' '))
|
|
168
|
-
);
|
|
169
|
-
if (!completedLater) {
|
|
170
|
-
out({ name, status: block.includes('PAUSED') ? 'paused' : 'active', next_step: block.match(/Next step\s*:\s*(.+)/)?.[1]?.trim() || null });
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
out({ name: null, status: 'idle', next_step: null });
|
|
176
|
-
}
|
|
177
|
-
function tasksPause(id, nextStep) {
|
|
178
|
-
appendF(TASKS_F, `\n## [${now()}] PAUSED ⏸\nTask : ${id}\nNext step : ${nextStep}\nResume with: /kelar:resume`);
|
|
179
|
-
out({ ok: true, next_step: nextStep });
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function memorySearch(query) {
|
|
183
|
-
if (!fs.existsSync(MEMORY_DIR)) { out([]); return; }
|
|
184
|
-
const results = [];
|
|
185
|
-
const words = query.toLowerCase().split(/\s+/).filter(w => w.length > 2);
|
|
186
|
-
function searchDir(dir) {
|
|
187
|
-
if (!fs.existsSync(dir)) return;
|
|
188
|
-
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
189
|
-
const full = path.join(dir, e.name);
|
|
190
|
-
if (e.isDirectory()) { searchDir(full); continue; }
|
|
191
|
-
if (!e.name.endsWith('.md')) continue;
|
|
192
|
-
const raw = readF(full);
|
|
193
|
-
const lower = raw.toLowerCase();
|
|
194
|
-
let score = 0;
|
|
195
|
-
for (const w of words) score += (lower.match(new RegExp(w, 'g')) || []).length;
|
|
196
|
-
if (score > 0) {
|
|
197
|
-
results.push({
|
|
198
|
-
file: full.replace(KELAR_DIR, '.kelar'),
|
|
199
|
-
title: raw.match(/^##\s+(.+)/m)?.[1] || e.name,
|
|
200
|
-
score,
|
|
201
|
-
snippet: raw.split('\n').find(l => words.some(w => l.toLowerCase().includes(w)))?.trim().substring(0, 120) || '',
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
searchDir(MEMORY_DIR);
|
|
207
|
-
out(results.sort((a, b) => b.score - a.score).slice(0, 5));
|
|
208
|
-
}
|
|
209
|
-
function memorySave(category, title, content) {
|
|
210
|
-
const valid = ['domain', 'technical', 'solutions', 'environment'];
|
|
211
|
-
if (!valid.includes(category)) die(`Category: ${valid.join(', ')}`);
|
|
212
|
-
const fileName = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') + '.md';
|
|
213
|
-
const filePath = path.join(MEMORY_DIR, category, fileName);
|
|
214
|
-
const entry = `## ${title}\nAdded: ${nowDate()}\n\n${content}\n`;
|
|
215
|
-
if (fs.existsSync(filePath)) fs.appendFileSync(filePath, '\n---\n\n' + entry, 'utf8');
|
|
216
|
-
else { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, entry, 'utf8'); }
|
|
217
|
-
memoryRebuildIndex();
|
|
218
|
-
out({ ok: true, file: filePath.replace(KELAR_DIR, '.kelar'), category, title });
|
|
219
|
-
}
|
|
220
|
-
function memoryRebuildIndex() {
|
|
221
|
-
if (!fs.existsSync(MEMORY_DIR)) return;
|
|
222
|
-
const cats = { domain: [], technical: [], solutions: [], environment: [] };
|
|
223
|
-
for (const cat of Object.keys(cats)) {
|
|
224
|
-
const d = path.join(MEMORY_DIR, cat);
|
|
225
|
-
if (!fs.existsSync(d)) continue;
|
|
226
|
-
for (const f of fs.readdirSync(d)) {
|
|
227
|
-
if (!f.endsWith('.md')) continue;
|
|
228
|
-
const raw = readF(path.join(d, f));
|
|
229
|
-
cats[cat].push({ title: raw.match(/^##\s+(.+)/m)?.[1] || f.replace('.md', ''), summary: (raw.split('\n').find(l => l.length > 20 && !l.startsWith('#') && !l.startsWith('Added:')) || '').trim().substring(0, 80) });
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
const lines = [`# KELAR Knowledge Index\nLast updated: ${nowDate()}\n`,
|
|
233
|
-
...Object.entries(cats).map(([cat, entries]) => `## ${cat.charAt(0).toUpperCase() + cat.slice(1)}\n` + (entries.length ? entries.map(e => `- **${e.title}** — ${e.summary}`).join('\n') : '*(none yet)*'))
|
|
234
|
-
];
|
|
235
|
-
fs.writeFileSync(MEMORY_IDX, lines.join('\n\n'), 'utf8');
|
|
236
|
-
out({ ok: true, total: Object.values(cats).flat().length });
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function patternsGet(cat) {
|
|
240
|
-
const c = readF(PATTERNS_F);
|
|
241
|
-
const m = c.match(new RegExp(`## ${cat}[^\n]*\n([\\s\\S]*?)(?=^## |$)`, 'm'));
|
|
242
|
-
out(m ? { category: cat, pattern: m[1].trim() } : { category: cat, pattern: null });
|
|
243
|
-
}
|
|
244
|
-
function patternsSet(cat, pattern) {
|
|
245
|
-
let c = readF(PATTERNS_F);
|
|
246
|
-
const re = new RegExp(`## ${cat}[^\n]*\n[\\s\\S]*?(?=^## |$)`, 'm');
|
|
247
|
-
const entry = `## ${cat} — ${nowDate()}\n${pattern}\n\n`;
|
|
248
|
-
fs.writeFileSync(PATTERNS_F, re.test(c) ? c.replace(re, entry) : c + '\n' + entry, 'utf8');
|
|
249
|
-
out({ ok: true, category: cat });
|
|
250
|
-
}
|
|
251
|
-
function patternsList() {
|
|
252
|
-
out([...readF(PATTERNS_F).matchAll(/^## (.+?) —/gm)].map(m => m[1].trim()));
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function handoffWrite() {
|
|
256
|
-
const tasks = readF(TASKS_F);
|
|
257
|
-
const recent = tasks.split('\n').slice(-40).join('\n');
|
|
258
|
-
const last = (tasks.match(/## \[(.+?)\] (?:TASK STARTED|PAUSED)[^\n]*\n([^#]+)/g) || []).at(-1) || '';
|
|
259
|
-
const nextStep = last.match(/Next step\s*:\s*(.+)/)?.[1] || 'Check TASKS.md';
|
|
260
|
-
const feature = readF(STATE_F).match(/Working on\s*:\s*(.+)/)?.[1]?.trim() || 'Unknown';
|
|
261
|
-
fs.writeFileSync(HANDOFF_F, `# KELAR HANDOFF\nGenerated: ${now()}\n\n## Status\nFeature : ${feature}\nNext step : ${nextStep}\n\n## Recent Activity\n\`\`\`\n${recent}\n\`\`\`\n\n## Resume\n1. Run /kelar:resume\n2. Confirm next step\n`, 'utf8');
|
|
262
|
-
out({ ok: true, next_step: nextStep, feature });
|
|
263
|
-
}
|
|
264
|
-
function handoffRead() {
|
|
265
|
-
const c = readF(HANDOFF_F);
|
|
266
|
-
if (!c) { out({ exists: false }); return; }
|
|
267
|
-
out({ exists: true, feature: c.match(/Feature\s*:\s*(.+)/)?.[1]?.trim() || null, next_step: c.match(/Next step\s*:\s*(.+)/)?.[1]?.trim() || null, generated: c.match(/Generated:\s*(.+)/)?.[1]?.trim() || null, raw: c });
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function planValidate(file) {
|
|
271
|
-
if (!fs.existsSync(file)) die(`Not found: ${file}`);
|
|
272
|
-
const c = readF(file);
|
|
273
|
-
const errors = [], warnings = [];
|
|
274
|
-
if (!c.includes('<kelar_plan>')) errors.push('Missing <kelar_plan>');
|
|
275
|
-
if (!c.includes('<meta>')) errors.push('Missing <meta>');
|
|
276
|
-
if (!c.includes('<goal>')) errors.push('Missing <goal>');
|
|
277
|
-
if (!c.includes('<wave')) errors.push('No waves defined');
|
|
278
|
-
for (const t of [...c.matchAll(/<task id="([^"]+)">/g)]) {
|
|
279
|
-
const id = t[1];
|
|
280
|
-
const body = c.slice(c.indexOf(`<task id="${id}">`), c.indexOf('</task>', c.indexOf(`<task id="${id}">`)));
|
|
281
|
-
if (!body.includes('<action>')) warnings.push(`Task ${id}: missing <action>`);
|
|
282
|
-
if (!body.includes('<done>')) warnings.push(`Task ${id}: missing <done>`);
|
|
283
|
-
if (!body.includes('<file>')) errors.push(`Task ${id}: missing <file>`);
|
|
284
|
-
}
|
|
285
|
-
out({ valid: errors.length === 0, task_count: [...c.matchAll(/<task id="/g)].length, errors, warnings });
|
|
286
|
-
}
|
|
287
|
-
function planTasks(file) {
|
|
288
|
-
if (!fs.existsSync(file)) die(`Not found: ${file}`);
|
|
289
|
-
const c = readF(file);
|
|
290
|
-
const ex = (body, tag) => { const m = body.match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`)); return m ? m[1].trim() : null; };
|
|
291
|
-
out([...c.matchAll(/<task id="([^"]+)">([\s\S]*?)<\/task>/g)].map(m => {
|
|
292
|
-
const dep = ex(m[2], 'depends_on');
|
|
293
|
-
return { id: m[1], title: ex(m[2], 'title'), file: ex(m[2], 'file'), action: ex(m[2], 'action'), verify: ex(m[2], 'verify'), done: ex(m[2], 'done'), depends_on: dep ? dep.split(',').map(s => s.trim()).filter(Boolean) : [] };
|
|
294
|
-
}));
|
|
295
|
-
}
|
|
296
|
-
function planWave(file, num) {
|
|
297
|
-
if (!fs.existsSync(file)) die(`Not found: ${file}`);
|
|
298
|
-
const c = readF(file);
|
|
299
|
-
const wm = c.match(new RegExp(`<wave number="${num}"([^>]*)>([\\s\\S]*?)<\\/wave>`));
|
|
300
|
-
if (!wm) { out({ wave: null, tasks: [] }); return; }
|
|
301
|
-
const ex = (body, tag) => { const m = body.match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`)); return m ? m[1].trim() : null; };
|
|
302
|
-
out({ wave: parseInt(num), title: wm[1].match(/title="([^"]+)"/)?.[1] || '', parallel: wm[1].includes('parallel="true"'), tasks: [...wm[2].matchAll(/<task id="([^"]+)">([\s\S]*?)<\/task>/g)].map(m => ({ id: m[1], title: ex(m[2], 'title'), file: ex(m[2], 'file'), action: ex(m[2], 'action') })) });
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function gitStatus() {
|
|
306
|
-
const status = bash('git status --porcelain');
|
|
307
|
-
const branch = bash('git branch --show-current');
|
|
308
|
-
const changed = status.split('\n').filter(Boolean).map(l => ({ status: l.substring(0, 2).trim(), file: l.substring(3).trim() }));
|
|
309
|
-
out({ branch, changed_count: changed.length, changed });
|
|
310
|
-
}
|
|
311
|
-
function gitChanged() { out(bash('git diff --name-only HEAD 2>/dev/null').split('\n').filter(Boolean)); }
|
|
312
|
-
function gitCommit(msg) {
|
|
313
|
-
try { bash('git add -A'); bash(`git commit -m ${JSON.stringify(msg)}`); out({ ok: true, hash: bash('git rev-parse --short HEAD'), message: msg }); }
|
|
314
|
-
catch(e) { out({ ok: false, error: e.message }); }
|
|
315
|
-
}
|
|
316
|
-
function gitCheckpoint() {
|
|
317
|
-
const label = `kelar-checkpoint-${nowDate().replace(/-/g, '')}-${Date.now()}`;
|
|
318
|
-
try { bash(`git stash push -m "${label}"`); out({ ok: true, label, rollback: 'git stash pop' }); }
|
|
319
|
-
catch(e) { out({ ok: false, error: e.message }); }
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function debtAdd(file, issue, priority) {
|
|
323
|
-
const pr = (priority || 'MEDIUM').toUpperCase();
|
|
324
|
-
if (!['HIGH', 'MEDIUM', 'LOW'].includes(pr)) die('Priority: HIGH, MEDIUM, or LOW');
|
|
325
|
-
const emoji = { HIGH: '🔴', MEDIUM: '🟡', LOW: '🟢' }[pr];
|
|
326
|
-
let c = readF(DEBT_F);
|
|
327
|
-
const entry = `| ${nowDate()} | ${file} | ${issue} | ${emoji} ${pr} | ? |`;
|
|
328
|
-
const ip = c.indexOf('## Resolved');
|
|
329
|
-
fs.writeFileSync(DEBT_F, ip > -1 ? c.slice(0, ip) + entry + '\n' + c.slice(ip) : c + '\n' + entry, 'utf8');
|
|
330
|
-
out({ ok: true, file, issue, priority: pr });
|
|
331
|
-
}
|
|
332
|
-
function debtList() {
|
|
333
|
-
out([...readF(DEBT_F).matchAll(/^\| (\d{4}-\d{2}-\d{2}) \| (.+?) \| (.+?) \| (.+?) \| (.+?) \|$/gm)]
|
|
334
|
-
.map(r => ({ date: r[1], file: r[2].trim(), issue: r[3].trim(), priority: r[4].trim(), est: r[5].trim() })));
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function health() {
|
|
338
|
-
const required = ['state/STATE.md', 'state/TASKS.md', 'state/PATTERNS.md', 'memory/INDEX.md'].map(r => p(r));
|
|
339
|
-
const checks = required.map(f => ({ file: f.replace(KELAR_DIR, '.kelar'), exists: fs.existsSync(f) }));
|
|
340
|
-
out({ healthy: checks.every(c => c.exists), kelar_dir: KELAR_DIR, checks });
|
|
341
|
-
}
|
|
342
|
-
function sessionStart(task) { tasksLog('start', `Task: ${task}`); out({ ok: true, started: now(), task }); }
|
|
343
|
-
function sessionEnd() {
|
|
344
|
-
const recent = readF(TASKS_F).split('\n').slice(-20).filter(l => l.includes('✅')).slice(-5).map(l => ` - ${l.trim()}`).join('\n');
|
|
345
|
-
const feature = readF(STATE_F).match(/Working on\s*:\s*(.+)/)?.[1]?.trim() || 'unknown';
|
|
346
|
-
appendF(DIARY_F, `\n## ${nowDate()} ${now().split(' ')[1]}\nWorked on : ${feature}\nActivity :\n${recent}\nNext : [see HANDOFF.md]\n`);
|
|
347
|
-
handoffWrite();
|
|
348
|
-
out({ ok: true });
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
352
|
-
// V2 — ULTRA MAGIC
|
|
353
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
354
|
-
|
|
355
|
-
function contextBuild(taskDesc, tokenBudget) {
|
|
356
|
-
const budget = parseInt(tokenBudget) || 8000;
|
|
357
|
-
const result = { task: taskDesc, token_budget: budget, sections: [], files_included: [], files_excluded: [], total_tokens: 0 };
|
|
358
|
-
let remaining = budget;
|
|
359
|
-
|
|
360
|
-
function addSection(name, content, extra) {
|
|
361
|
-
const tokens = estimateTokens(content);
|
|
362
|
-
if (tokens > remaining) { result.files_excluded.push({ name, tokens, reason: 'over budget' }); return false; }
|
|
363
|
-
result.sections.push({ name, content, tokens, ...extra });
|
|
364
|
-
remaining -= tokens;
|
|
365
|
-
return true;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
addSection('project_state', readF(STATE_F));
|
|
369
|
-
addSection('patterns', readF(PATTERNS_F).split('\n').slice(0, 40).join('\n'));
|
|
370
|
-
addSection('recent_activity', readF(TASKS_F).split('\n').slice(-25).join('\n'));
|
|
371
|
-
|
|
372
|
-
if (taskDesc) {
|
|
373
|
-
// Memory search
|
|
374
|
-
const memResults = JSON.parse(bash(`node "${__filename}" memory search ${JSON.stringify(taskDesc)}`) || '[]');
|
|
375
|
-
for (const entry of memResults.slice(0, 3)) {
|
|
376
|
-
const full = path.join(process.cwd(), entry.file);
|
|
377
|
-
if (fs.existsSync(full)) addSection(`memory:${entry.title}`, readF(full), { relevance: entry.score });
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Relevant source files by keyword density
|
|
381
|
-
const keywords = taskDesc.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').split(/\s+/)
|
|
382
|
-
.filter(w => w.length > 3 && !['this', 'that', 'with', 'from', 'have', 'will', 'need', 'make'].includes(w));
|
|
383
|
-
|
|
384
|
-
const srcDir = path.join(process.cwd(), 'src');
|
|
385
|
-
const files = walkFiles(fs.existsSync(srcDir) ? srcDir : process.cwd());
|
|
386
|
-
const scored = files.map(f => {
|
|
387
|
-
const c = readF(f); const lower = c.toLowerCase(); const name = path.basename(f).toLowerCase();
|
|
388
|
-
let score = 0;
|
|
389
|
-
for (const kw of keywords) { score += (lower.match(new RegExp(kw, 'g')) || []).length; if (name.includes(kw)) score += 10; }
|
|
390
|
-
return { file: f, content: c, score, tokens: estimateTokens(c) };
|
|
391
|
-
}).filter(f => f.score > 0).sort((a, b) => b.score - a.score);
|
|
392
|
-
|
|
393
|
-
for (const f of scored) {
|
|
394
|
-
const truncated = f.tokens > remaining * 0.4 ? f.content.substring(0, remaining * 0.4 * 4) + '\n...[truncated]' : f.content;
|
|
395
|
-
if (!addSection(`source:${path.relative(process.cwd(), f.file)}`, truncated, { relevance: f.score })) break;
|
|
396
|
-
result.files_included.push(path.relative(process.cwd(), f.file));
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
result.total_tokens = budget - remaining;
|
|
401
|
-
result.remaining_tokens = remaining;
|
|
402
|
-
out(result);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function contextInject(agentName) {
|
|
406
|
-
if (!agentName) die('Usage: context inject <agent-name>');
|
|
407
|
-
const agentFile = path.join(AGENTS_DIR, `${agentName}.md`);
|
|
408
|
-
if (!fs.existsSync(agentFile)) {
|
|
409
|
-
const avail = fs.existsSync(AGENTS_DIR) ? fs.readdirSync(AGENTS_DIR).filter(f => f.endsWith('.md')).map(f => f.replace('.md', '')) : [];
|
|
410
|
-
die(`Agent '${agentName}' not found. Available: ${avail.join(', ')}`);
|
|
411
|
-
}
|
|
412
|
-
const agentDef = readF(agentFile);
|
|
413
|
-
const snap = JSON.parse(bash(`node "${__filename}" state snapshot`) || '{}');
|
|
414
|
-
const active = JSON.parse(bash(`node "${__filename}" tasks active`) || '{}');
|
|
415
|
-
const recentActivity = readF(TASKS_F).split('\n').slice(-15).join('\n');
|
|
416
|
-
const patterns = readF(PATTERNS_F).split('\n').slice(0, 30).join('\n');
|
|
417
|
-
|
|
418
|
-
const prompt = `You are ${agentName}. Read your role definition and begin immediately.
|
|
419
|
-
|
|
420
|
-
<agent_definition>
|
|
421
|
-
${agentDef}
|
|
422
|
-
</agent_definition>
|
|
423
|
-
|
|
424
|
-
<project_context>
|
|
425
|
-
Stack: ${snap.stack || 'see STATE.md'} | Type: ${snap.type || 'unknown'}
|
|
426
|
-
Working on: ${snap.working_on || active.name || 'awaiting task'}
|
|
427
|
-
Status: ${active.status || 'idle'}${active.next_step ? ` | Next: ${active.next_step}` : ''}
|
|
428
|
-
</project_context>
|
|
429
|
-
|
|
430
|
-
<recent_activity>
|
|
431
|
-
${recentActivity}
|
|
432
|
-
</recent_activity>
|
|
433
|
-
|
|
434
|
-
<approved_patterns>
|
|
435
|
-
${patterns}
|
|
436
|
-
</approved_patterns>
|
|
437
|
-
|
|
438
|
-
<files_to_read>
|
|
439
|
-
AGENTS.md
|
|
440
|
-
.kelar/state/STATE.md
|
|
441
|
-
.kelar/state/PATTERNS.md
|
|
442
|
-
.kelar/memory/INDEX.md
|
|
443
|
-
</files_to_read>
|
|
444
|
-
|
|
445
|
-
<kelar_tools_reference>
|
|
446
|
-
node .kelar/kelar-tools.cjs tasks log start "Task: [name]"
|
|
447
|
-
node .kelar/kelar-tools.cjs tasks log done "Task [id]: [result]"
|
|
448
|
-
node .kelar/kelar-tools.cjs memory save technical "[title]" "[content]"
|
|
449
|
-
node .kelar/kelar-tools.cjs memory search "[query]"
|
|
450
|
-
node .kelar/kelar-tools.cjs git checkpoint
|
|
451
|
-
node .kelar/kelar-tools.cjs git commit "feat(kelar): [message]"
|
|
452
|
-
node .kelar/kelar-tools.cjs debt add "[file]" "[issue]" "MEDIUM"
|
|
453
|
-
</kelar_tools_reference>`;
|
|
454
|
-
|
|
455
|
-
out({ agent: agentName, prompt, prompt_tokens: estimateTokens(prompt) });
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function scanRisk(scanPath) {
|
|
459
|
-
const targetDir = path.resolve(process.cwd(), scanPath || 'src');
|
|
460
|
-
const files = walkFiles(targetDir);
|
|
461
|
-
const issues = [];
|
|
462
|
-
|
|
463
|
-
const patterns = [
|
|
464
|
-
{ re: /(?:password|secret|api_key|apikey|token)\s*[:=]\s*['"][^'"]{6,}/gi, severity: 'HIGH', category: 'secret', msg: 'Potential hardcoded secret' },
|
|
465
|
-
{ re: /https?:\/\/(?:localhost|127\.0\.0\.1|192\.168)/gi, severity: 'HIGH', category: 'hardcoded_url', msg: 'Hardcoded local/internal URL' },
|
|
466
|
-
{ re: /:\s*any\b/g, severity: 'MEDIUM', category: 'any_type', msg: 'TypeScript any type' },
|
|
467
|
-
{ re: /as\s+any\b/g, severity: 'HIGH', category: 'any_cast', msg: 'Unsafe any cast' },
|
|
468
|
-
{ re: /\/\/ @ts-ignore/g, severity: 'MEDIUM', category: 'ts_ignore', msg: 'TypeScript error suppressed' },
|
|
469
|
-
{ re: /\/\/ @ts-nocheck/g, severity: 'HIGH', category: 'ts_nocheck', msg: 'TypeScript disabled for file' },
|
|
470
|
-
{ re: /catch\s*\([^)]*\)\s*\{\s*\}/g, severity: 'HIGH', category: 'empty_catch', msg: 'Empty catch — swallowed error' },
|
|
471
|
-
{ re: /\.catch\(\s*\(\s*\)\s*=>\s*\{\s*\}\)/g, severity: 'HIGH', category: 'empty_catch', msg: 'Empty catch callback' },
|
|
472
|
-
{ re: /for\s*\([^)]+\)[^{]*\{[^}]*await\s+\w+\(/gs, severity: 'HIGH', category: 'n_plus_1', msg: 'Possible N+1: await inside loop' },
|
|
473
|
-
{ re: /\.forEach\([^)]+async/g, severity: 'MEDIUM', category: 'async_foreach', msg: 'async in forEach — errors silently swallowed' },
|
|
474
|
-
{ re: /console\.(log|warn|error|info|debug)\s*\(/g, severity: 'LOW', category: 'console_log', msg: 'console.log found' },
|
|
475
|
-
{ re: /\/\/\s*(TODO|FIXME|HACK|XXX|TEMP|BUG):/gi, severity: 'LOW', category: 'todo', msg: 'TODO/FIXME comment' },
|
|
476
|
-
{ re: /debugger;/g, severity: 'HIGH', category: 'debugger', msg: 'debugger statement' },
|
|
477
|
-
];
|
|
478
|
-
|
|
479
|
-
for (const file of files) {
|
|
480
|
-
const content = readF(file);
|
|
481
|
-
const rel = path.relative(process.cwd(), file);
|
|
482
|
-
|
|
483
|
-
// Function length check
|
|
484
|
-
for (const m of [...content.matchAll(/(?:function\s+\w+|(?:const|let)\s+\w+\s*=\s*(?:async\s+)?\([^)]*\)\s*=>)\s*\{/g)]) {
|
|
485
|
-
const line = content.substring(0, m.index).split('\n').length;
|
|
486
|
-
let depth = 0, end = m.index;
|
|
487
|
-
for (let i = m.index; i < Math.min(content.length, m.index + 4000); i++) {
|
|
488
|
-
if (content[i] === '{') depth++;
|
|
489
|
-
else if (content[i] === '}') { depth--; if (depth === 0) { end = i; break; } }
|
|
490
|
-
}
|
|
491
|
-
const len = content.substring(m.index, end).split('\n').length;
|
|
492
|
-
if (len > 25) issues.push({ file: rel, line, severity: 'MEDIUM', category: 'long_function', message: `Function is ${len} lines (max 20)`, snippet: m[0].substring(0, 60) });
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
for (const { re, severity, category, msg } of patterns) {
|
|
496
|
-
const g = new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g');
|
|
497
|
-
let m;
|
|
498
|
-
while ((m = g.exec(content)) !== null) {
|
|
499
|
-
issues.push({ file: rel, line: content.substring(0, m.index).split('\n').length, severity, category, message: msg, snippet: m[0].trim().substring(0, 80) });
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
const order = { HIGH: 0, MEDIUM: 1, LOW: 2 };
|
|
505
|
-
const sorted = issues.sort((a, b) => order[a.severity] - order[b.severity] || a.file.localeCompare(b.file));
|
|
506
|
-
out({ summary: { total: sorted.length, HIGH: sorted.filter(i => i.severity === 'HIGH').length, MEDIUM: sorted.filter(i => i.severity === 'MEDIUM').length, LOW: sorted.filter(i => i.severity === 'LOW').length, files_scanned: files.length }, issues: sorted });
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function scanImports(filePath) {
|
|
510
|
-
if (!filePath) die('Usage: scan imports <file>');
|
|
511
|
-
const abs = path.resolve(process.cwd(), filePath);
|
|
512
|
-
if (!fs.existsSync(abs)) die(`Not found: ${filePath}`);
|
|
513
|
-
const content = readF(abs);
|
|
514
|
-
const targetBase = path.basename(abs, path.extname(abs));
|
|
515
|
-
const allFiles = walkFiles(path.join(process.cwd(), 'src'));
|
|
516
|
-
const direct = [...content.matchAll(/(?:import|require)\s*(?:\{[^}]*\}|\w+)?\s*(?:from\s*)?['"]([^'"]+)['"]/g)]
|
|
517
|
-
.map(m => m[1]).filter(i => i.startsWith('.') || i.startsWith('@/'));
|
|
518
|
-
const dependents = allFiles.filter(f => f !== abs && readF(f).includes(targetBase)).map(f => path.relative(process.cwd(), f));
|
|
519
|
-
out({
|
|
520
|
-
file: path.relative(process.cwd(), abs), direct_imports: direct, direct_import_count: direct.length,
|
|
521
|
-
depended_on_by: dependents, dependent_count: dependents.length,
|
|
522
|
-
blast_radius: dependents.length === 0 ? 'LOW' : dependents.length < 4 ? 'MEDIUM' : 'HIGH',
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
function impactScore(filePath) {
|
|
527
|
-
if (!filePath) die('Usage: impact score <file>');
|
|
528
|
-
const abs = path.resolve(process.cwd(), filePath);
|
|
529
|
-
if (!fs.existsSync(abs)) die(`Not found: ${filePath}`);
|
|
530
|
-
const content = readF(abs);
|
|
531
|
-
const base = path.basename(abs, path.extname(abs));
|
|
532
|
-
const allFiles = walkFiles(path.join(process.cwd(), 'src'));
|
|
533
|
-
let deps = 0, refs = 0;
|
|
534
|
-
for (const f of allFiles) {
|
|
535
|
-
if (f === abs) continue;
|
|
536
|
-
const fc = readF(f);
|
|
537
|
-
if (fc.includes(base)) { deps++; refs += (fc.match(new RegExp(base, 'g')) || []).length; }
|
|
538
|
-
}
|
|
539
|
-
const exports = (content.match(/^export /gm) || []).length;
|
|
540
|
-
const size = fs.statSync(abs).size;
|
|
541
|
-
const score = Math.min(100, Math.round(Math.min(40, deps * 5) + Math.min(20, refs * 2) + Math.min(20, exports * 3) + Math.min(20, size / 500)));
|
|
542
|
-
const level = score >= 70 ? 'CRITICAL' : score >= 40 ? 'HIGH' : score >= 20 ? 'MEDIUM' : 'LOW';
|
|
543
|
-
out({ file: path.relative(process.cwd(), abs), score, level, stats: { files_that_import_it: deps, total_references: refs, exports, size_bytes: size }, recommendation: level === 'CRITICAL' || level === 'HIGH' ? '⚠️ Run: node .kelar/kelar-tools.cjs git checkpoint first' : 'Standard procedures apply.' });
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function diffSmart(from, to) {
|
|
547
|
-
const a = from || 'HEAD~1', b = to || 'HEAD';
|
|
548
|
-
const raw = bash(`git diff ${a} ${b} 2>/dev/null`);
|
|
549
|
-
if (!raw) { out({ changes: [], summary: 'No changes' }); return; }
|
|
550
|
-
const changes = [];
|
|
551
|
-
for (const fileDiff of raw.split(/^diff --git /m).filter(Boolean)) {
|
|
552
|
-
const fm = fileDiff.match(/a\/(.+?) b\//);
|
|
553
|
-
if (!fm || !fm[1].match(/\.(ts|tsx|js|jsx|py|go)$/)) continue;
|
|
554
|
-
const added = fileDiff.split('\n').filter(l => l.startsWith('+') && !l.startsWith('+++')).map(l => l.slice(1));
|
|
555
|
-
const removed = fileDiff.split('\n').filter(l => l.startsWith('-') && !l.startsWith('---')).map(l => l.slice(1));
|
|
556
|
-
const newFns = added.filter(l => l.match(/^(?:export\s+)?(?:async\s+)?function\s+\w+|^(?:export\s+)?const\s+\w+\s*=\s*(?:async\s+)?\(/));
|
|
557
|
-
const rmFns = removed.filter(l => l.match(/^(?:export\s+)?(?:async\s+)?function\s+\w+|^(?:export\s+)?const\s+\w+\s*=\s*(?:async\s+)?\(/));
|
|
558
|
-
const breaking = rmFns.filter(rm => {
|
|
559
|
-
const name = rm.match(/function\s+(\w+)/)?.[1] || rm.match(/const\s+(\w+)/)?.[1];
|
|
560
|
-
return name && !newFns.some(a => a.includes(name));
|
|
561
|
-
}).map(rm => `Deleted: ${(rm.match(/function\s+(\w+)/)?.[1] || rm.match(/const\s+(\w+)/)?.[1]) || '?'}`);
|
|
562
|
-
changes.push({ file: fm[1], new_functions: newFns.slice(0, 5).map(f => f.trim().substring(0, 80)), removed_functions: rmFns.slice(0, 5).map(f => f.trim().substring(0, 80)), breaking_changes: breaking, risk: breaking.length > 0 ? 'HIGH' : rmFns.length > 0 ? 'MEDIUM' : 'LOW', lines_added: added.length, lines_removed: removed.length });
|
|
563
|
-
}
|
|
564
|
-
const order = { HIGH: 0, MEDIUM: 1, LOW: 2 };
|
|
565
|
-
out({ from: a, to: b, files_changed: changes.length, breaking_total: changes.reduce((s, c) => s + c.breaking_changes.length, 0), changes: changes.sort((a, b) => order[a.risk] - order[b.risk]) });
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function tokensEstimate(targetPath) {
|
|
569
|
-
const abs = path.resolve(process.cwd(), targetPath || 'src');
|
|
570
|
-
if (!fs.existsSync(abs)) die(`Not found: ${targetPath}`);
|
|
571
|
-
if (fs.statSync(abs).isFile()) {
|
|
572
|
-
const tokens = estimateTokens(readF(abs));
|
|
573
|
-
out({ file: targetPath, tokens, fits_in: { '8k': tokens < 8000, '32k': tokens < 32000, '100k': tokens < 100000, '200k': tokens < 200000 } });
|
|
574
|
-
return;
|
|
575
|
-
}
|
|
576
|
-
const results = walkFiles(abs, []).map(f => ({ file: path.relative(process.cwd(), f), tokens: estimateTokens(readF(f)), size_bytes: fs.statSync(f).size })).sort((a, b) => b.tokens - a.tokens);
|
|
577
|
-
const total = results.reduce((s, r) => s + r.tokens, 0);
|
|
578
|
-
out({ path: targetPath, total_tokens: total, file_count: results.length, fits_200k: total < 200000, biggest_files: results.slice(0, 10) });
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function planGenerate(desc) {
|
|
582
|
-
if (!desc) die('Usage: plan generate "description"');
|
|
583
|
-
const lower = desc.toLowerCase();
|
|
584
|
-
const has = f => f.test(lower);
|
|
585
|
-
const hasUI = has(/component|page|form|button|modal|ui|interface|layout|screen|view/);
|
|
586
|
-
const hasAuth = has(/auth|login|logout|session|token|jwt|oauth|permission|role/);
|
|
587
|
-
const hasDB = has(/database|schema|migration|model|table|query|prisma|orm|sql/);
|
|
588
|
-
const hasAPI = has(/api|endpoint|route|controller|rest|graphql|webhook/);
|
|
589
|
-
const hasService = has(/service|logic|business|process|calculate|compute/);
|
|
590
|
-
const slug = desc.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '-').substring(0, 40).replace(/-$/, '');
|
|
591
|
-
|
|
592
|
-
let wn = 0;
|
|
593
|
-
const waves = [];
|
|
594
|
-
const task = (id, title, file, action, verify, done) => ` <task id="${id}">\n <title>${title}</title>\n <file>${file}</file>\n <action>${action}</action>\n <verify>${verify}</verify>\n <done>${done}</done>\n <depends_on></depends_on>\n </task>`;
|
|
595
|
-
|
|
596
|
-
// Wave 1: Foundation
|
|
597
|
-
const w1 = [];
|
|
598
|
-
if (hasDB) w1.push(task('1.1', 'Define schema / migration', 'prisma/schema.prisma', `Schema for: ${desc}`, 'Migration valid', 'Schema reflects requirements'));
|
|
599
|
-
w1.push(task(`1.${w1.length+1}`, 'Define TypeScript types', `src/types/${slug}.ts`, `Types and interfaces for: ${desc}`, 'tsc --noEmit passes', 'All types exported'));
|
|
600
|
-
waves.push(` <wave number="${++wn}" title="Foundation" parallel="true">\n${w1.join('\n')}\n </wave>`);
|
|
601
|
-
|
|
602
|
-
// Wave 2: Core
|
|
603
|
-
if (hasService || hasAPI || hasDB) {
|
|
604
|
-
const w2 = [];
|
|
605
|
-
if (hasService) w2.push(task('2.1', 'Implement service logic', `src/services/${slug}Service.ts`, `Main logic for: ${desc}`, 'Service compiles', 'Core logic implemented'));
|
|
606
|
-
if (hasDB) w2.push(task(`2.${w2.length+1}`, 'Implement repository', `src/repositories/${slug}Repository.ts`, 'DB queries following existing pattern', 'Queries type-safe', 'Data layer working'));
|
|
607
|
-
waves.push(` <wave number="${++wn}" title="Core Logic" parallel="false">\n${w2.join('\n')}\n </wave>`);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Wave 3: API
|
|
611
|
-
if (hasAPI) {
|
|
612
|
-
waves.push(` <wave number="${++wn}" title="API Layer" parallel="false">\n${task(`${wn}.1`, 'Create API endpoints', 'src/routes/[feature].ts', `Endpoints for: ${desc}`, 'Route responds', 'Endpoint validates input')}\n </wave>`);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Wave 4: UI
|
|
616
|
-
if (hasUI) {
|
|
617
|
-
const w = [];
|
|
618
|
-
w.push(task(`${wn+1}.1`, 'Build UI component', `src/components/${slug}/${slug}.tsx`, `UI for: ${desc}. All 8 states required.`, 'Renders without errors', 'All states: default, hover, focus, active, disabled, loading, error, empty'));
|
|
619
|
-
w.push(task(`${wn+1}.2`, 'Wire component to data', `src/components/${slug}/${slug}.tsx`, 'Connect to API/service', 'Shows real data', 'Data flows through component'));
|
|
620
|
-
waves.push(` <wave number="${++wn}" title="UI" parallel="true">\n${w.join('\n')}\n </wave>`);
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
624
|
-
<kelar_plan>
|
|
625
|
-
<meta>
|
|
626
|
-
<feature>${slug}</feature>
|
|
627
|
-
<goal>[TODO: one sentence user-facing goal]</goal>
|
|
628
|
-
<wave_count>${waves.length}</wave_count>
|
|
629
|
-
<created>${new Date().toISOString()}</created>
|
|
630
|
-
<generated_by>kelar-tools plan generate</generated_by>
|
|
631
|
-
</meta>
|
|
632
|
-
|
|
633
|
-
${waves.join('\n\n')}
|
|
634
|
-
|
|
635
|
-
<out_of_scope>[TODO: what is NOT included]</out_of_scope>
|
|
636
|
-
<risks><risk level="LOW">[TODO: potential issues]</risk></risks>
|
|
637
|
-
</kelar_plan>`;
|
|
638
|
-
|
|
639
|
-
const planDir = p('plans');
|
|
640
|
-
fs.mkdirSync(planDir, { recursive: true });
|
|
641
|
-
const planFile = path.join(planDir, `${slug}-plan.xml`);
|
|
642
|
-
fs.writeFileSync(planFile, xml, 'utf8');
|
|
643
|
-
out({ ok: true, plan_file: planFile.replace(process.cwd(), '.'), feature_slug: slug, waves: waves.length, detected: { hasUI, hasAuth, hasDB, hasAPI, hasService } });
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function knowledgeExtract() {
|
|
647
|
-
const commits = bash('git log --oneline -20 2>/dev/null');
|
|
648
|
-
if (!commits) { out({ suggestions: [], reason: 'No git history' }); return; }
|
|
649
|
-
const suggestions = [];
|
|
650
|
-
for (const line of commits.split('\n').filter(Boolean)) {
|
|
651
|
-
const hash = line.split(' ')[0];
|
|
652
|
-
const message = line.substring(hash.length + 1);
|
|
653
|
-
const isFix = /fix|bug|broken|error|crash|null|undefined|wrong/i.test(message);
|
|
654
|
-
const isGotcha = /workaround|hack|trick|weird|quirk|actually|turns out/i.test(message);
|
|
655
|
-
const isPattern = /pattern|prefer|instead|refactor|switch|improve/i.test(message);
|
|
656
|
-
const isConfig = /config|env|setup|install|dependency|version|upgrade/i.test(message);
|
|
657
|
-
if (isFix || isGotcha || isPattern || isConfig) {
|
|
658
|
-
const category = isFix ? 'technical' : isConfig ? 'environment' : isPattern ? 'solutions' : 'technical';
|
|
659
|
-
suggestions.push({ commit: hash, message, category, reason: isFix ? 'Bug fix worth documenting' : isGotcha ? 'Non-obvious workaround' : isPattern ? 'Pattern change — capture reasoning' : 'Config change', save_command: `node .kelar/kelar-tools.cjs memory save ${category} "${message.substring(0, 50)}" "[your notes]"` });
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
out({ commits_analyzed: commits.split('\n').filter(Boolean).length, suggestions_found: suggestions.length, suggestions });
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function timeline(maxItems) {
|
|
666
|
-
const n = parseInt(maxItems) || 20;
|
|
667
|
-
const tasksContent = readF(TASKS_F);
|
|
668
|
-
const gitLog = bash(`git log --pretty=format:"%h|%ai|%s" -${n} 2>/dev/null`);
|
|
669
|
-
const taskEntries = [...tasksContent.matchAll(/## \[(.+?)\] (TASK|FEATURE|FIX) (STARTED|COMPLETE)[^\n]*(.*)/g)]
|
|
670
|
-
.map(m => ({ timestamp: m[1], type: `${m[2]}_${m[3]}`, description: m[4].trim(), source: 'tasks' }));
|
|
671
|
-
const gitEntries = gitLog.split('\n').filter(Boolean).map(line => {
|
|
672
|
-
const parts = line.split('|');
|
|
673
|
-
return parts.length >= 3 ? { hash: parts[0], timestamp: parts[1], message: parts[2], source: 'git' } : null;
|
|
674
|
-
}).filter(Boolean);
|
|
675
|
-
const all = [...taskEntries, ...gitEntries].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)).slice(0, n);
|
|
676
|
-
out({ generated_at: now(), total_events: all.length, task_events: taskEntries.length, git_commits: gitEntries.length, timeline: all });
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function debtScore() {
|
|
680
|
-
const srcDir = path.join(process.cwd(), 'src');
|
|
681
|
-
if (!fs.existsSync(srcDir)) { out({ score: 0, reason: 'No src/ directory' }); return; }
|
|
682
|
-
const files = walkFiles(srcDir);
|
|
683
|
-
let anyTypes = 0, todos = 0, emptyCatch = 0;
|
|
684
|
-
for (const f of files) {
|
|
685
|
-
const c = readF(f);
|
|
686
|
-
anyTypes += (c.match(/:\s*any\b|as\s+any\b/g) || []).length;
|
|
687
|
-
todos += (c.match(/\/\/\s*TODO:|\/\/\s*FIXME:/gi) || []).length;
|
|
688
|
-
emptyCatch += (c.match(/catch\s*\([^)]*\)\s*\{\s*\}/g) || []).length;
|
|
689
|
-
}
|
|
690
|
-
const debtItems = readF(DEBT_F).split('\n').filter(l => l.startsWith('|') && !l.includes('Date') && !l.includes('---'));
|
|
691
|
-
const high = debtItems.filter(l => l.includes('🔴')).length;
|
|
692
|
-
const med = debtItems.filter(l => l.includes('🟡')).length;
|
|
693
|
-
const low = debtItems.filter(l => l.includes('🟢')).length;
|
|
694
|
-
const score = Math.min(100, high * 10 + med * 5 + low * 2 + anyTypes * 3 + todos + emptyCatch * 8);
|
|
695
|
-
const level = score >= 60 ? 'CRITICAL' : score >= 30 ? 'HIGH' : score >= 15 ? 'MEDIUM' : 'LOW';
|
|
696
|
-
const scoreFile = p('state/DEBT_SCORE.json');
|
|
697
|
-
const history = fs.existsSync(scoreFile) ? JSON.parse(readF(scoreFile)) : [];
|
|
698
|
-
const prev = history.at(-1);
|
|
699
|
-
history.push({ date: nowDate(), score });
|
|
700
|
-
if (history.length > 30) history.shift();
|
|
701
|
-
fs.writeFileSync(scoreFile, JSON.stringify(history, null, 2), 'utf8');
|
|
702
|
-
out({ score, level, trend: prev ? (score > prev.score ? '↗ WORSENING' : score < prev.score ? '↘ IMPROVING' : '→ STABLE') : 'first scan', previous_score: prev?.score ?? null, breakdown: { debt_md: { high, med, low }, any_types: anyTypes, todos, empty_catch: emptyCatch }, files_scanned: files.length, recommendation: level === 'CRITICAL' ? '🔴 Stop features. Fix HIGH items first.' : level === 'HIGH' ? '🟡 Schedule debt reduction.' : '🟢 Debt under control.' });
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function similar(filePath) {
|
|
706
|
-
if (!filePath) die('Usage: similar <file>');
|
|
707
|
-
const abs = path.resolve(process.cwd(), filePath);
|
|
708
|
-
if (!fs.existsSync(abs)) die(`Not found: ${filePath}`);
|
|
709
|
-
const content = readF(abs);
|
|
710
|
-
const imports = [...content.matchAll(/from\s+['"]([^'"]+)['"]/g)].map(m => m[1]);
|
|
711
|
-
const ext = path.extname(abs); const dir = path.dirname(abs);
|
|
712
|
-
const suffix = path.basename(abs).replace(/\.[^.]+$/, '').match(/[A-Z][a-z]+$/)?.[0];
|
|
713
|
-
const scored = walkFiles(path.join(process.cwd(), 'src')).filter(f => f !== abs && path.extname(f) === ext).map(f => {
|
|
714
|
-
const fc = readF(f);
|
|
715
|
-
const fi = [...fc.matchAll(/from\s+['"]([^'"]+)['"]/g)].map(m => m[1]);
|
|
716
|
-
const shared = imports.filter(i => fi.includes(i));
|
|
717
|
-
let score = shared.length * 3;
|
|
718
|
-
if (path.dirname(f) === dir) score += 5;
|
|
719
|
-
if (suffix && f.includes(suffix)) score += 4;
|
|
720
|
-
if (Math.abs(content.length - fc.length) / Math.max(content.length, fc.length) < 0.3) score += 2;
|
|
721
|
-
return { file: path.relative(process.cwd(), f), score, shared_imports: shared };
|
|
722
|
-
}).filter(f => f.score > 0).sort((a, b) => b.score - a.score).slice(0, 5);
|
|
723
|
-
out({ target: path.relative(process.cwd(), abs), similar_files: scored, usage: scored.length > 0 ? `Best reference: ${scored[0].file}` : 'No similar files found.' });
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
function watch(watchPath) {
|
|
727
|
-
const abs = path.resolve(process.cwd(), watchPath || 'src');
|
|
728
|
-
if (!fs.existsSync(abs)) die(`Not found: ${watchPath}`);
|
|
729
|
-
const active = JSON.parse(bash(`node "${__filename}" tasks active`) || '{}');
|
|
730
|
-
process.stderr.write(`\n🔍 KELAR WATCH ACTIVE — ${watchPath || 'src'} — ${active.name || 'no active task'} — Ctrl+C to stop\n\n`);
|
|
731
|
-
const seen = new Set(); let changes = 0;
|
|
732
|
-
function check() {
|
|
733
|
-
for (const f of walkFiles(abs, [])) {
|
|
734
|
-
try {
|
|
735
|
-
const mtime = fs.statSync(f).mtimeMs; const key = `${f}:${mtime}`;
|
|
736
|
-
if (!seen.has(key)) {
|
|
737
|
-
if (seen.size > 0) {
|
|
738
|
-
const rel = path.relative(process.cwd(), f);
|
|
739
|
-
const isNew = ![...seen].some(k => k.startsWith(f + ':'));
|
|
740
|
-
const action = isNew ? 'CREATED' : 'MODIFIED'; changes++;
|
|
741
|
-
appendF(TASKS_F, `[${now()}] 📝 WATCH: ${action}: ${rel}`);
|
|
742
|
-
process.stderr.write(`[${now()}] ${action}: ${rel}\n`);
|
|
743
|
-
}
|
|
744
|
-
seen.add(key);
|
|
745
|
-
}
|
|
746
|
-
} catch { /* deleted */ }
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
check();
|
|
750
|
-
const iv = setInterval(check, 1500);
|
|
751
|
-
process.on('SIGINT', () => { clearInterval(iv); process.stderr.write(`\n🛑 STOPPED — ${changes} changes logged to TASKS.md\n\n`); process.exit(0); });
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function agentBrief(name) { contextInject(name); }
|
|
755
|
-
|
|
756
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
757
|
-
// ROUTER
|
|
758
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
759
|
-
const [,, cmd, sub, ...rest] = process.argv;
|
|
760
|
-
const args = sub ? [sub, ...rest] : rest;
|
|
761
|
-
|
|
762
|
-
switch (cmd) {
|
|
763
|
-
case 'state':
|
|
764
|
-
if (sub==='get') stateGet(args[1]); else if (sub==='patch') statePatch(args[1], rest.join(' ')); else if (sub==='snapshot') stateSnapshot(); else die('state: get|patch|snapshot'); break;
|
|
765
|
-
case 'tasks':
|
|
766
|
-
if (sub==='log') tasksLog(args[1], rest.join(' ')); else if (sub==='active') tasksActive(); else if (sub==='complete') tasksLog('done', rest.join(' ')); else if (sub==='pause') tasksPause(args[1], rest.join(' ')); else die('tasks: log|active|complete|pause'); break;
|
|
767
|
-
case 'memory':
|
|
768
|
-
if (sub==='search') memorySearch(rest.join(' ')); else if (sub==='save') memorySave(args[1], args[2], rest.slice(1).join(' ')); else if (sub==='index') memoryRebuildIndex(); else die('memory: search|save|index'); break;
|
|
769
|
-
case 'patterns':
|
|
770
|
-
if (sub==='get') patternsGet(rest.join(' ')); else if (sub==='set') patternsSet(args[1], rest.join(' ')); else if (sub==='list') patternsList(); else die('patterns: get|set|list'); break;
|
|
771
|
-
case 'handoff':
|
|
772
|
-
if (sub==='write') handoffWrite(); else if (sub==='read') handoffRead(); else die('handoff: write|read'); break;
|
|
773
|
-
case 'plan':
|
|
774
|
-
if (sub==='validate') planValidate(args[1]); else if (sub==='tasks') planTasks(args[1]); else if (sub==='wave') planWave(args[1], args[2]); else if (sub==='generate') planGenerate(rest.join(' ')); else die('plan: validate|tasks|wave|generate'); break;
|
|
775
|
-
case 'git':
|
|
776
|
-
if (sub==='status') gitStatus(); else if (sub==='changed') gitChanged(); else if (sub==='commit') gitCommit(rest.join(' ')); else if (sub==='checkpoint') gitCheckpoint(); else die('git: status|changed|commit|checkpoint'); break;
|
|
777
|
-
case 'debt':
|
|
778
|
-
if (sub==='add') debtAdd(args[1], args[2], args[3]); else if (sub==='list') debtList(); else if (sub==='score') debtScore(); else die('debt: add|list|score'); break;
|
|
779
|
-
case 'session':
|
|
780
|
-
if (sub==='start') sessionStart(rest.join(' ')); else if (sub==='end') sessionEnd(); else die('session: start|end'); break;
|
|
781
|
-
// V2
|
|
782
|
-
case 'context':
|
|
783
|
-
if (sub==='build') contextBuild(rest.join(' ')); else if (sub==='inject') contextInject(args[1]); else die('context: build|inject'); break;
|
|
784
|
-
case 'scan':
|
|
785
|
-
if (sub==='risk') scanRisk(args[1]); else if (sub==='imports') scanImports(args[1]); else die('scan: risk|imports'); break;
|
|
786
|
-
case 'impact':
|
|
787
|
-
if (sub==='score') impactScore(args[1]); else die('impact: score'); break;
|
|
788
|
-
case 'diff':
|
|
789
|
-
if (sub==='smart') diffSmart(args[1], args[2]); else die('diff: smart'); break;
|
|
790
|
-
case 'tokens':
|
|
791
|
-
if (sub==='estimate') tokensEstimate(args[1]); else die('tokens: estimate'); break;
|
|
792
|
-
case 'plan':
|
|
793
|
-
if (sub==='generate') planGenerate(rest.join(' ')); break;
|
|
794
|
-
case 'timeline': timeline(sub); break;
|
|
795
|
-
case 'knowledge':
|
|
796
|
-
if (sub==='extract') knowledgeExtract(); else die('knowledge: extract'); break;
|
|
797
|
-
case 'similar': similar(sub); break;
|
|
798
|
-
case 'watch': watch(sub); break;
|
|
799
|
-
case 'agent':
|
|
800
|
-
if (sub==='brief') agentBrief(args[1]); else die('agent: brief'); break;
|
|
801
|
-
case 'health': health(); break;
|
|
802
|
-
case 'version': out(VERSION); break;
|
|
803
|
-
default:
|
|
804
|
-
process.stderr.write(`
|
|
805
|
-
KELAR Tools v${VERSION}
|
|
806
|
-
|
|
807
|
-
Standard: state · tasks · memory · patterns · handoff · plan · git · debt · session · health
|
|
808
|
-
Ultra: context build/inject · scan risk/imports · impact score
|
|
809
|
-
diff smart · tokens estimate · plan generate · knowledge extract
|
|
810
|
-
timeline · debt score · similar · watch · agent brief
|
|
811
|
-
|
|
812
|
-
Examples:
|
|
813
|
-
node .kelar/kelar-tools.cjs context build "add stripe payment"
|
|
814
|
-
node .kelar/kelar-tools.cjs context inject kelar-executor
|
|
815
|
-
node .kelar/kelar-tools.cjs scan risk src/
|
|
816
|
-
node .kelar/kelar-tools.cjs scan imports src/services/Auth.ts
|
|
817
|
-
node .kelar/kelar-tools.cjs impact score src/lib/db.ts
|
|
818
|
-
node .kelar/kelar-tools.cjs diff smart HEAD~5 HEAD
|
|
819
|
-
node .kelar/kelar-tools.cjs tokens estimate src/
|
|
820
|
-
node .kelar/kelar-tools.cjs plan generate "add stripe payment checkout"
|
|
821
|
-
node .kelar/kelar-tools.cjs knowledge extract
|
|
822
|
-
node .kelar/kelar-tools.cjs similar src/services/UserService.ts
|
|
823
|
-
node .kelar/kelar-tools.cjs debt score
|
|
824
|
-
node .kelar/kelar-tools.cjs timeline 20
|
|
825
|
-
node .kelar/kelar-tools.cjs watch src/
|
|
826
|
-
node .kelar/kelar-tools.cjs agent brief kelar-planner
|
|
827
|
-
\n`);
|
|
828
|
-
process.exit(1);
|
|
829
|
-
}
|