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.
Files changed (42) hide show
  1. package/dist/index.js +2 -2
  2. package/dist/index.mjs +2 -2
  3. package/package.json +5 -2
  4. package/.changeset/README.md +0 -8
  5. package/.changeset/config.json +0 -11
  6. package/.kelar/kelar-tools.cjs +0 -829
  7. package/.kelar/memory/INDEX.md +0 -21
  8. package/.kelar/memory/technical/issue-34-sendpresenceupdate-undefined.md +0 -4
  9. package/.kelar/memory/technical/lmdb-native-binary-fallback.md +0 -4
  10. package/.kelar/memory/technical/nedbadapter-ignoring-encoder-causes-buffer-typing-crash-err-invalid-arg-type.md +0 -4
  11. package/.kelar/memory/technical/signal-media-constructor-input-contract.md +0 -4
  12. package/.kelar/memory/technical/sticker-example-source-format.md +0 -4
  13. package/.kelar/memory/technical/ts2307-missing-seald-io-nedb.md +0 -4
  14. package/.kelar/memory/technical/ts2307-missing-zaileys-store-adapters.md +0 -4
  15. package/.kelar/plans/README.md +0 -3
  16. package/.kelar/research/README.md +0 -3
  17. package/.kelar/research/mysql-adapter-research.md +0 -32
  18. package/.kelar/state/ASSUMPTIONS.md +0 -14
  19. package/.kelar/state/DEBT.md +0 -13
  20. package/.kelar/state/DIARY.md +0 -15
  21. package/.kelar/state/HANDOFF.md +0 -54
  22. package/.kelar/state/PATTERNS.md +0 -43
  23. package/.kelar/state/STATE.md +0 -37
  24. package/.kelar/state/TASKS.md +0 -146
  25. package/AGENTS.md +0 -108
  26. package/CHANGELOG.md +0 -12
  27. package/GEMINI.md +0 -58
  28. package/packages/media-process/CHANGELOG.md +0 -7
  29. package/packages/media-process/LICENSE +0 -21
  30. package/packages/media-process/dist/index.d.mts +0 -142
  31. package/packages/media-process/dist/index.d.ts +0 -142
  32. package/packages/media-process/dist/index.mjs +0 -3
  33. package/packages/media-process/package.json +0 -39
  34. package/packages/media-process/tsconfig.json +0 -15
  35. package/packages/mysql-adapter/dist/index.d.mts +0 -36
  36. package/packages/mysql-adapter/dist/index.d.ts +0 -36
  37. package/packages/mysql-adapter/dist/index.js.map +0 -1
  38. package/packages/mysql-adapter/dist/index.mjs +0 -101
  39. package/packages/mysql-adapter/dist/index.mjs.map +0 -1
  40. package/packages/mysql-adapter/package.json +0 -36
  41. package/pnpm-workspace.yaml +0 -3
  42. package/tsconfig.json +0 -19
@@ -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
- }