kernelbot 1.0.28 → 1.0.32
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/.env.example +4 -0
- package/bin/kernel.js +68 -7
- package/config.example.yaml +45 -1
- package/package.json +1 -1
- package/src/agent.js +613 -28
- package/src/bot.js +643 -7
- package/src/claude-auth.js +93 -0
- package/src/coder.js +48 -6
- package/src/life/codebase.js +388 -0
- package/src/life/engine.js +1317 -0
- package/src/life/evolution.js +244 -0
- package/src/life/improvements.js +81 -0
- package/src/life/journal.js +109 -0
- package/src/life/memory.js +283 -0
- package/src/life/share-queue.js +136 -0
- package/src/prompts/orchestrator.js +71 -5
- package/src/prompts/workers.js +65 -5
- package/src/providers/models.js +8 -1
- package/src/self.js +122 -0
- package/src/services/stt.js +139 -0
- package/src/services/tts.js +124 -0
- package/src/swarm/job-manager.js +54 -7
- package/src/swarm/job.js +19 -1
- package/src/swarm/worker-registry.js +5 -0
- package/src/tools/coding.js +6 -1
- package/src/tools/orchestrator-tools.js +93 -21
- package/src/tools/os.js +14 -1
- package/src/utils/config.js +105 -2
- package/src/worker.js +98 -5
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { getLogger } from './utils/logger.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run `claude auth status` and return parsed output.
|
|
6
|
+
*/
|
|
7
|
+
export function getClaudeAuthStatus() {
|
|
8
|
+
const logger = getLogger();
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const child = spawn('claude', ['auth', 'status'], {
|
|
11
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
let stdout = '';
|
|
15
|
+
let stderr = '';
|
|
16
|
+
|
|
17
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
18
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
19
|
+
|
|
20
|
+
child.on('close', (code) => {
|
|
21
|
+
const output = (stdout || stderr).trim();
|
|
22
|
+
logger.debug(`claude auth status (code ${code}): ${output.slice(0, 300)}`);
|
|
23
|
+
resolve({ code, output });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
child.on('error', (err) => {
|
|
27
|
+
if (err.code === 'ENOENT') {
|
|
28
|
+
resolve({ code: -1, output: 'Claude Code CLI not found. Install with: npm i -g @anthropic-ai/claude-code' });
|
|
29
|
+
} else {
|
|
30
|
+
resolve({ code: -1, output: err.message });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Timeout after 10s
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
child.kill('SIGTERM');
|
|
37
|
+
resolve({ code: -1, output: 'Timed out checking auth status' });
|
|
38
|
+
}, 10_000);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run `claude auth logout`.
|
|
44
|
+
*/
|
|
45
|
+
export function claudeLogout() {
|
|
46
|
+
const logger = getLogger();
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const child = spawn('claude', ['auth', 'logout'], {
|
|
49
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
let stdout = '';
|
|
53
|
+
let stderr = '';
|
|
54
|
+
|
|
55
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
56
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
57
|
+
|
|
58
|
+
child.on('close', (code) => {
|
|
59
|
+
const output = (stdout || stderr).trim();
|
|
60
|
+
logger.info(`claude auth logout (code ${code}): ${output.slice(0, 300)}`);
|
|
61
|
+
resolve({ code, output });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
child.on('error', (err) => {
|
|
65
|
+
resolve({ code: -1, output: err.message });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
setTimeout(() => {
|
|
69
|
+
child.kill('SIGTERM');
|
|
70
|
+
resolve({ code: -1, output: 'Timed out during logout' });
|
|
71
|
+
}, 10_000);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Return the current Claude Code auth mode from config.
|
|
77
|
+
*/
|
|
78
|
+
export function getClaudeCodeAuthMode(config) {
|
|
79
|
+
const mode = config.claude_code?.auth_mode || 'system';
|
|
80
|
+
const info = { mode };
|
|
81
|
+
|
|
82
|
+
if (mode === 'api_key') {
|
|
83
|
+
const key = config.claude_code?.api_key || process.env.CLAUDE_CODE_API_KEY || '';
|
|
84
|
+
info.credential = key ? `${key.slice(0, 8)}...${key.slice(-4)}` : '(not set)';
|
|
85
|
+
} else if (mode === 'oauth_token') {
|
|
86
|
+
const token = config.claude_code?.oauth_token || process.env.CLAUDE_CODE_OAUTH_TOKEN || '';
|
|
87
|
+
info.credential = token ? `${token.slice(0, 8)}...${token.slice(-4)}` : '(not set)';
|
|
88
|
+
} else {
|
|
89
|
+
info.credential = 'Using host system login';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return info;
|
|
93
|
+
}
|
package/src/coder.js
CHANGED
|
@@ -135,17 +135,43 @@ function processEvent(line, onOutput, logger) {
|
|
|
135
135
|
|
|
136
136
|
export class ClaudeCodeSpawner {
|
|
137
137
|
constructor(config) {
|
|
138
|
+
this.config = config;
|
|
138
139
|
this.maxTurns = config.claude_code?.max_turns || 50;
|
|
139
|
-
this.timeout = (config.claude_code?.timeout_seconds ||
|
|
140
|
-
this.model = config.claude_code?.model || null;
|
|
140
|
+
this.timeout = (config.claude_code?.timeout_seconds || 86400) * 1000;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
_buildSpawnEnv() {
|
|
144
|
+
const authMode = this.config.claude_code?.auth_mode || 'system';
|
|
145
|
+
const env = { ...process.env, IS_SANDBOX: '1' };
|
|
146
|
+
|
|
147
|
+
if (authMode === 'api_key') {
|
|
148
|
+
const key = this.config.claude_code?.api_key || process.env.CLAUDE_CODE_API_KEY;
|
|
149
|
+
if (key) {
|
|
150
|
+
env.ANTHROPIC_API_KEY = key;
|
|
151
|
+
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
152
|
+
}
|
|
153
|
+
} else if (authMode === 'oauth_token') {
|
|
154
|
+
const token = this.config.claude_code?.oauth_token || process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
155
|
+
if (token) {
|
|
156
|
+
env.CLAUDE_CODE_OAUTH_TOKEN = token;
|
|
157
|
+
// Remove ANTHROPIC_API_KEY so it doesn't override subscription auth
|
|
158
|
+
delete env.ANTHROPIC_API_KEY;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// authMode === 'system' — pass env as-is
|
|
162
|
+
|
|
163
|
+
return env;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async run({ workingDirectory, prompt, maxTurns, onOutput, signal }) {
|
|
144
167
|
const logger = getLogger();
|
|
145
168
|
const turns = maxTurns || this.maxTurns;
|
|
146
169
|
|
|
147
170
|
ensureClaudeCodeSetup();
|
|
148
171
|
|
|
172
|
+
// Read model dynamically from config (supports hot-reload via switchClaudeCodeModel)
|
|
173
|
+
const model = this.config.claude_code?.model || null;
|
|
174
|
+
|
|
149
175
|
const args = [
|
|
150
176
|
'-p', prompt,
|
|
151
177
|
'--max-turns', String(turns),
|
|
@@ -153,8 +179,8 @@ export class ClaudeCodeSpawner {
|
|
|
153
179
|
'--verbose',
|
|
154
180
|
'--dangerously-skip-permissions',
|
|
155
181
|
];
|
|
156
|
-
if (
|
|
157
|
-
args.push('--model',
|
|
182
|
+
if (model) {
|
|
183
|
+
args.push('--model', model);
|
|
158
184
|
}
|
|
159
185
|
|
|
160
186
|
const cmd = `claude ${args.map((a) => a.includes(' ') ? `"${a}"` : a).join(' ')}`;
|
|
@@ -219,10 +245,24 @@ export class ClaudeCodeSpawner {
|
|
|
219
245
|
return new Promise((resolve, reject) => {
|
|
220
246
|
const child = spawn('claude', args, {
|
|
221
247
|
cwd: workingDirectory,
|
|
222
|
-
env:
|
|
248
|
+
env: this._buildSpawnEnv(),
|
|
223
249
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
224
250
|
});
|
|
225
251
|
|
|
252
|
+
// Wire abort signal to kill the child process
|
|
253
|
+
let abortHandler = null;
|
|
254
|
+
if (signal) {
|
|
255
|
+
if (signal.aborted) {
|
|
256
|
+
child.kill('SIGTERM');
|
|
257
|
+
} else {
|
|
258
|
+
abortHandler = () => {
|
|
259
|
+
logger.info('Claude Code: abort signal received — killing child process');
|
|
260
|
+
child.kill('SIGTERM');
|
|
261
|
+
};
|
|
262
|
+
signal.addEventListener('abort', abortHandler, { once: true });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
226
266
|
let fullOutput = '';
|
|
227
267
|
let stderr = '';
|
|
228
268
|
let buffer = '';
|
|
@@ -268,6 +308,7 @@ export class ClaudeCodeSpawner {
|
|
|
268
308
|
|
|
269
309
|
child.on('close', async (code) => {
|
|
270
310
|
clearTimeout(timer);
|
|
311
|
+
if (abortHandler && signal) signal.removeEventListener('abort', abortHandler);
|
|
271
312
|
|
|
272
313
|
if (buffer.trim()) {
|
|
273
314
|
fullOutput += buffer.trim();
|
|
@@ -312,6 +353,7 @@ export class ClaudeCodeSpawner {
|
|
|
312
353
|
|
|
313
354
|
child.on('error', (err) => {
|
|
314
355
|
clearTimeout(timer);
|
|
356
|
+
if (abortHandler && signal) signal.removeEventListener('abort', abortHandler);
|
|
315
357
|
if (err.code === 'ENOENT') {
|
|
316
358
|
reject(new Error('Claude Code CLI not found. Install with: npm i -g @anthropic-ai/claude-code'));
|
|
317
359
|
} else {
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
import { getLogger } from '../utils/logger.js';
|
|
7
|
+
|
|
8
|
+
const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
9
|
+
const CODEBASE_DIR = join(LIFE_DIR, 'codebase');
|
|
10
|
+
const SUMMARIES_FILE = join(CODEBASE_DIR, 'file-summaries.json');
|
|
11
|
+
const ARCHITECTURE_FILE = join(CODEBASE_DIR, 'architecture.md');
|
|
12
|
+
|
|
13
|
+
// Files to always skip during scanning
|
|
14
|
+
const SKIP_PATTERNS = [
|
|
15
|
+
'node_modules', '.git', 'package-lock.json', 'yarn.lock',
|
|
16
|
+
'.env', '.DS_Store', 'dist/', 'build/', 'coverage/',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export class CodebaseKnowledge {
|
|
20
|
+
constructor({ config } = {}) {
|
|
21
|
+
this.config = config || {};
|
|
22
|
+
this._projectRoot = null;
|
|
23
|
+
this._summaries = {};
|
|
24
|
+
this._agent = null;
|
|
25
|
+
|
|
26
|
+
mkdirSync(CODEBASE_DIR, { recursive: true });
|
|
27
|
+
this._summaries = this._loadSummaries();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Set the agent reference (called after agent is created). */
|
|
31
|
+
setAgent(agent) {
|
|
32
|
+
this._agent = agent;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Set/detect project root. */
|
|
36
|
+
setProjectRoot(root) {
|
|
37
|
+
this._projectRoot = root;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getProjectRoot() {
|
|
41
|
+
if (this._projectRoot) return this._projectRoot;
|
|
42
|
+
// Try to detect from git
|
|
43
|
+
try {
|
|
44
|
+
this._projectRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
|
45
|
+
} catch {
|
|
46
|
+
this._projectRoot = process.cwd();
|
|
47
|
+
}
|
|
48
|
+
return this._projectRoot;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Persistence ───────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
_loadSummaries() {
|
|
54
|
+
if (existsSync(SUMMARIES_FILE)) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(readFileSync(SUMMARIES_FILE, 'utf-8'));
|
|
57
|
+
} catch {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_saveSummaries() {
|
|
65
|
+
writeFileSync(SUMMARIES_FILE, JSON.stringify(this._summaries, null, 2), 'utf-8');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_saveArchitecture(content) {
|
|
69
|
+
writeFileSync(ARCHITECTURE_FILE, content, 'utf-8');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── File Hashing ──────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
_hashFile(filePath) {
|
|
75
|
+
try {
|
|
76
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
77
|
+
return createHash('md5').update(content).digest('hex').slice(0, 12);
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_lineCount(filePath) {
|
|
84
|
+
try {
|
|
85
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
86
|
+
return content.split('\n').length;
|
|
87
|
+
} catch {
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Scanning ──────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Scan a single file using the LLM to generate a summary.
|
|
96
|
+
* Requires this._agent to be set.
|
|
97
|
+
*/
|
|
98
|
+
async scanFile(filePath) {
|
|
99
|
+
const logger = getLogger();
|
|
100
|
+
const root = this.getProjectRoot();
|
|
101
|
+
const fullPath = filePath.startsWith('/') ? filePath : join(root, filePath);
|
|
102
|
+
const relativePath = filePath.startsWith('/') ? filePath.replace(root + '/', '') : filePath;
|
|
103
|
+
|
|
104
|
+
// Check if file should be skipped
|
|
105
|
+
if (SKIP_PATTERNS.some(p => relativePath.includes(p))) return null;
|
|
106
|
+
|
|
107
|
+
const hash = this._hashFile(fullPath);
|
|
108
|
+
if (!hash) return null;
|
|
109
|
+
|
|
110
|
+
// Skip if already scanned and unchanged
|
|
111
|
+
const existing = this._summaries[relativePath];
|
|
112
|
+
if (existing && existing.lastHash === hash) return existing;
|
|
113
|
+
|
|
114
|
+
// Read file content
|
|
115
|
+
let content;
|
|
116
|
+
try {
|
|
117
|
+
content = readFileSync(fullPath, 'utf-8');
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Truncate very large files
|
|
123
|
+
const maxChars = 8000;
|
|
124
|
+
const truncated = content.length > maxChars
|
|
125
|
+
? content.slice(0, maxChars) + '\n... (truncated)'
|
|
126
|
+
: content;
|
|
127
|
+
|
|
128
|
+
// Use LLM to summarize if agent is available
|
|
129
|
+
let summary;
|
|
130
|
+
if (this._agent) {
|
|
131
|
+
try {
|
|
132
|
+
const prompt = `Analyze this source file and respond with ONLY a JSON object (no markdown, no code blocks):
|
|
133
|
+
{
|
|
134
|
+
"summary": "one-paragraph description of what this file does",
|
|
135
|
+
"exports": ["list", "of", "exported", "names"],
|
|
136
|
+
"dependencies": ["list", "of", "local", "imports"]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
File: ${relativePath}
|
|
140
|
+
\`\`\`
|
|
141
|
+
${truncated}
|
|
142
|
+
\`\`\``;
|
|
143
|
+
|
|
144
|
+
const response = await this._agent.orchestratorProvider.chat({
|
|
145
|
+
system: 'You are a code analysis assistant. Respond with only valid JSON, no markdown formatting.',
|
|
146
|
+
messages: [{ role: 'user', content: prompt }],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const text = (response.text || '').trim();
|
|
150
|
+
// Try to parse JSON from the response
|
|
151
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
152
|
+
if (jsonMatch) {
|
|
153
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
154
|
+
summary = {
|
|
155
|
+
summary: parsed.summary || 'No summary generated',
|
|
156
|
+
exports: parsed.exports || [],
|
|
157
|
+
dependencies: parsed.dependencies || [],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
logger.debug(`[Codebase] LLM scan failed for ${relativePath}: ${err.message}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Fallback: basic static analysis
|
|
166
|
+
if (!summary) {
|
|
167
|
+
summary = this._staticAnalysis(content, relativePath);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const entry = {
|
|
171
|
+
...summary,
|
|
172
|
+
lineCount: this._lineCount(fullPath),
|
|
173
|
+
lastHash: hash,
|
|
174
|
+
lastScanned: Date.now(),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
this._summaries[relativePath] = entry;
|
|
178
|
+
this._saveSummaries();
|
|
179
|
+
logger.debug(`[Codebase] Scanned: ${relativePath}`);
|
|
180
|
+
return entry;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Scan only files that have changed since last scan (git-based).
|
|
185
|
+
*/
|
|
186
|
+
async scanChanged() {
|
|
187
|
+
const logger = getLogger();
|
|
188
|
+
const root = this.getProjectRoot();
|
|
189
|
+
|
|
190
|
+
let changedFiles = [];
|
|
191
|
+
try {
|
|
192
|
+
// Get all tracked files that differ from what we've scanned
|
|
193
|
+
const allFiles = execSync('git ls-files --full-name', {
|
|
194
|
+
cwd: root,
|
|
195
|
+
encoding: 'utf-8',
|
|
196
|
+
}).trim().split('\n').filter(Boolean);
|
|
197
|
+
|
|
198
|
+
// Filter to source files
|
|
199
|
+
changedFiles = allFiles.filter(f =>
|
|
200
|
+
(f.endsWith('.js') || f.endsWith('.mjs') || f.endsWith('.json') || f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.md')) &&
|
|
201
|
+
!SKIP_PATTERNS.some(p => f.includes(p))
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Only scan files whose hash has changed
|
|
205
|
+
changedFiles = changedFiles.filter(f => {
|
|
206
|
+
const fullPath = join(root, f);
|
|
207
|
+
const hash = this._hashFile(fullPath);
|
|
208
|
+
const existing = this._summaries[f];
|
|
209
|
+
return !existing || existing.lastHash !== hash;
|
|
210
|
+
});
|
|
211
|
+
} catch (err) {
|
|
212
|
+
logger.warn(`[Codebase] Git scan failed: ${err.message}`);
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
logger.info(`[Codebase] Scanning ${changedFiles.length} changed files...`);
|
|
217
|
+
let scanned = 0;
|
|
218
|
+
|
|
219
|
+
for (const file of changedFiles) {
|
|
220
|
+
try {
|
|
221
|
+
await this.scanFile(file);
|
|
222
|
+
scanned++;
|
|
223
|
+
} catch (err) {
|
|
224
|
+
logger.debug(`[Codebase] Failed to scan ${file}: ${err.message}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
logger.info(`[Codebase] Scan complete: ${scanned} files updated`);
|
|
229
|
+
return scanned;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Full scan of all source files. Heavy operation — use sparingly.
|
|
234
|
+
*/
|
|
235
|
+
async scanAll() {
|
|
236
|
+
const logger = getLogger();
|
|
237
|
+
const root = this.getProjectRoot();
|
|
238
|
+
|
|
239
|
+
let allFiles = [];
|
|
240
|
+
try {
|
|
241
|
+
allFiles = execSync('git ls-files --full-name', {
|
|
242
|
+
cwd: root,
|
|
243
|
+
encoding: 'utf-8',
|
|
244
|
+
}).trim().split('\n').filter(Boolean);
|
|
245
|
+
|
|
246
|
+
allFiles = allFiles.filter(f =>
|
|
247
|
+
(f.endsWith('.js') || f.endsWith('.mjs') || f.endsWith('.json') || f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.md')) &&
|
|
248
|
+
!SKIP_PATTERNS.some(p => f.includes(p))
|
|
249
|
+
);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
logger.warn(`[Codebase] Git ls-files failed: ${err.message}`);
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
logger.info(`[Codebase] Full scan: ${allFiles.length} files...`);
|
|
256
|
+
let scanned = 0;
|
|
257
|
+
|
|
258
|
+
for (const file of allFiles) {
|
|
259
|
+
try {
|
|
260
|
+
await this.scanFile(file);
|
|
261
|
+
scanned++;
|
|
262
|
+
} catch (err) {
|
|
263
|
+
logger.debug(`[Codebase] Failed to scan ${file}: ${err.message}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
logger.info(`[Codebase] Full scan complete: ${scanned} files`);
|
|
268
|
+
return scanned;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Queries ───────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
getFileSummary(path) {
|
|
274
|
+
return this._summaries[path] || null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getAllSummaries() {
|
|
278
|
+
return { ...this._summaries };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
getArchitecture() {
|
|
282
|
+
if (existsSync(ARCHITECTURE_FILE)) {
|
|
283
|
+
try {
|
|
284
|
+
return readFileSync(ARCHITECTURE_FILE, 'utf-8');
|
|
285
|
+
} catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Find files relevant to a proposed change description.
|
|
294
|
+
* Returns file paths sorted by relevance.
|
|
295
|
+
*/
|
|
296
|
+
getRelevantFiles(description) {
|
|
297
|
+
const descLower = description.toLowerCase();
|
|
298
|
+
const keywords = descLower.split(/\W+/).filter(w => w.length > 2);
|
|
299
|
+
|
|
300
|
+
const scored = Object.entries(this._summaries).map(([path, info]) => {
|
|
301
|
+
let score = 0;
|
|
302
|
+
const text = `${path} ${info.summary || ''}`.toLowerCase();
|
|
303
|
+
|
|
304
|
+
for (const keyword of keywords) {
|
|
305
|
+
if (text.includes(keyword)) score++;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return { path, score, summary: info.summary };
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return scored
|
|
312
|
+
.filter(s => s.score > 0)
|
|
313
|
+
.sort((a, b) => b.score - a.score)
|
|
314
|
+
.slice(0, 15);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Regenerate the architecture overview from all summaries.
|
|
319
|
+
*/
|
|
320
|
+
async updateArchitecture() {
|
|
321
|
+
const logger = getLogger();
|
|
322
|
+
const entries = Object.entries(this._summaries);
|
|
323
|
+
if (entries.length === 0) return;
|
|
324
|
+
|
|
325
|
+
// Group by directory
|
|
326
|
+
const byDir = {};
|
|
327
|
+
for (const [path, info] of entries) {
|
|
328
|
+
const dir = path.includes('/') ? path.split('/').slice(0, -1).join('/') : '.';
|
|
329
|
+
if (!byDir[dir]) byDir[dir] = [];
|
|
330
|
+
byDir[dir].push({ path, ...info });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Build a compact summary for LLM
|
|
334
|
+
const summaryText = Object.entries(byDir)
|
|
335
|
+
.map(([dir, files]) => {
|
|
336
|
+
const fileLines = files
|
|
337
|
+
.map(f => ` - ${f.path}: ${(f.summary || 'no summary').slice(0, 120)}`)
|
|
338
|
+
.join('\n');
|
|
339
|
+
return `### ${dir}/\n${fileLines}`;
|
|
340
|
+
})
|
|
341
|
+
.join('\n\n');
|
|
342
|
+
|
|
343
|
+
if (this._agent) {
|
|
344
|
+
try {
|
|
345
|
+
const prompt = `Based on these file summaries, write a concise architecture overview document in Markdown. Include: project structure, key components, data flow, and patterns used.
|
|
346
|
+
|
|
347
|
+
${summaryText}`;
|
|
348
|
+
|
|
349
|
+
const response = await this._agent.orchestratorProvider.chat({
|
|
350
|
+
system: 'You are a software architect. Write clear, concise architecture documentation.',
|
|
351
|
+
messages: [{ role: 'user', content: prompt }],
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (response.text) {
|
|
355
|
+
this._saveArchitecture(response.text);
|
|
356
|
+
logger.info(`[Codebase] Architecture doc updated (${response.text.length} chars)`);
|
|
357
|
+
}
|
|
358
|
+
} catch (err) {
|
|
359
|
+
logger.warn(`[Codebase] Architecture update failed: ${err.message}`);
|
|
360
|
+
}
|
|
361
|
+
} else {
|
|
362
|
+
// Fallback: just dump the summaries
|
|
363
|
+
const doc = `# KERNEL Architecture\n\n_Auto-generated on ${new Date().toISOString()}_\n\n${summaryText}`;
|
|
364
|
+
this._saveArchitecture(doc);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── Static Analysis Fallback ──────────────────────────────────
|
|
369
|
+
|
|
370
|
+
_staticAnalysis(content, filePath) {
|
|
371
|
+
const exports = [];
|
|
372
|
+
const dependencies = [];
|
|
373
|
+
|
|
374
|
+
// Extract exports
|
|
375
|
+
const exportMatches = content.matchAll(/export\s+(?:default\s+)?(?:class|function|const|let|var)\s+(\w+)/g);
|
|
376
|
+
for (const m of exportMatches) exports.push(m[1]);
|
|
377
|
+
|
|
378
|
+
// Extract local imports
|
|
379
|
+
const importMatches = content.matchAll(/from\s+['"](\.[^'"]+)['"]/g);
|
|
380
|
+
for (const m of importMatches) dependencies.push(m[1]);
|
|
381
|
+
|
|
382
|
+
// Simple summary based on file path
|
|
383
|
+
let summary = `Source file at ${filePath}`;
|
|
384
|
+
if (exports.length > 0) summary += ` — exports: ${exports.join(', ')}`;
|
|
385
|
+
|
|
386
|
+
return { summary, exports, dependencies };
|
|
387
|
+
}
|
|
388
|
+
}
|