@neurcode-ai/cli 0.9.9 → 0.9.11
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/commands/ask.d.ts +10 -0
- package/dist/commands/ask.d.ts.map +1 -0
- package/dist/commands/ask.js +631 -0
- package/dist/commands/ask.js.map +1 -0
- package/dist/commands/brain.d.ts.map +1 -1
- package/dist/commands/brain.js +33 -0
- package/dist/commands/brain.js.map +1 -1
- package/dist/commands/plan.d.ts +2 -0
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +209 -60
- package/dist/commands/plan.js.map +1 -1
- package/dist/index.js +33 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/ask-cache.d.ts +78 -0
- package/dist/utils/ask-cache.d.ts.map +1 -0
- package/dist/utils/ask-cache.js +424 -0
- package/dist/utils/ask-cache.js.map +1 -0
- package/dist/utils/brain-context.d.ts +20 -1
- package/dist/utils/brain-context.d.ts.map +1 -1
- package/dist/utils/brain-context.js +56 -1
- package/dist/utils/brain-context.js.map +1 -1
- package/dist/utils/plan-cache.d.ts +35 -0
- package/dist/utils/plan-cache.d.ts.map +1 -1
- package/dist/utils/plan-cache.js +191 -0
- package/dist/utils/plan-cache.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface AskOptions {
|
|
2
|
+
projectId?: string;
|
|
3
|
+
json?: boolean;
|
|
4
|
+
cache?: boolean;
|
|
5
|
+
maxCitations?: number;
|
|
6
|
+
fromPlan?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function askCommand(question: string, options?: AskOptions): Promise<void>;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=ask.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ask.d.ts","sourceRoot":"","sources":["../../src/commands/ask.ts"],"names":[],"mappings":"AA+CA,UAAU,UAAU;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAkVD,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAoV1F"}
|
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.askCommand = askCommand;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const config_1 = require("../config");
|
|
7
|
+
const project_root_1 = require("../utils/project-root");
|
|
8
|
+
const state_1 = require("../utils/state");
|
|
9
|
+
const neurcode_context_1 = require("../utils/neurcode-context");
|
|
10
|
+
const plan_cache_1 = require("../utils/plan-cache");
|
|
11
|
+
const brain_context_1 = require("../utils/brain-context");
|
|
12
|
+
const ask_cache_1 = require("../utils/ask-cache");
|
|
13
|
+
let chalk;
|
|
14
|
+
try {
|
|
15
|
+
chalk = require('chalk');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
chalk = {
|
|
19
|
+
green: (str) => str,
|
|
20
|
+
yellow: (str) => str,
|
|
21
|
+
red: (str) => str,
|
|
22
|
+
bold: (str) => str,
|
|
23
|
+
dim: (str) => str,
|
|
24
|
+
cyan: (str) => str,
|
|
25
|
+
white: (str) => str,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const MAX_SCAN_FILES = 320;
|
|
29
|
+
const MAX_FILE_BYTES = 512 * 1024;
|
|
30
|
+
const MAX_RAW_CITATIONS = 120;
|
|
31
|
+
const STOP_WORDS = new Set([
|
|
32
|
+
'the', 'and', 'for', 'with', 'that', 'this', 'what', 'where', 'when', 'which', 'into',
|
|
33
|
+
'from', 'your', 'about', 'there', 'their', 'them', 'have', 'does', 'is', 'are', 'was',
|
|
34
|
+
'were', 'any', 'all', 'read', 'tell', 'me', 'its', 'it', 'instead', 'than', 'then',
|
|
35
|
+
'workflow', 'codebase', 'repo', 'repository',
|
|
36
|
+
]);
|
|
37
|
+
function scanFiles(dir, maxFiles = MAX_SCAN_FILES) {
|
|
38
|
+
const files = [];
|
|
39
|
+
const ignoreDirs = new Set(['node_modules', '.git', '.next', 'dist', 'build', '.turbo', '.cache', 'coverage']);
|
|
40
|
+
const ignoreExts = new Set(['map', 'log', 'lock', 'png', 'jpg', 'jpeg', 'gif', 'ico', 'svg', 'woff', 'woff2', 'ttf', 'eot', 'pdf']);
|
|
41
|
+
const walk = (current) => {
|
|
42
|
+
if (files.length >= maxFiles)
|
|
43
|
+
return;
|
|
44
|
+
let entries = [];
|
|
45
|
+
try {
|
|
46
|
+
entries = (0, fs_1.readdirSync)(current);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (files.length >= maxFiles)
|
|
53
|
+
break;
|
|
54
|
+
if (entry.startsWith('.') && !entry.startsWith('.env')) {
|
|
55
|
+
if (ignoreDirs.has(entry))
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const fullPath = (0, path_1.join)(current, entry);
|
|
59
|
+
let st;
|
|
60
|
+
try {
|
|
61
|
+
st = (0, fs_1.statSync)(fullPath);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (st.isDirectory()) {
|
|
67
|
+
if (ignoreDirs.has(entry))
|
|
68
|
+
continue;
|
|
69
|
+
walk(fullPath);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!st.isFile())
|
|
73
|
+
continue;
|
|
74
|
+
if (st.size > MAX_FILE_BYTES)
|
|
75
|
+
continue;
|
|
76
|
+
const ext = entry.includes('.') ? entry.split('.').pop()?.toLowerCase() || '' : '';
|
|
77
|
+
if (ext && ignoreExts.has(ext))
|
|
78
|
+
continue;
|
|
79
|
+
const rel = fullPath.slice(dir.length + 1).replace(/\\/g, '/');
|
|
80
|
+
files.push(rel);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
walk(dir);
|
|
84
|
+
return files.slice(0, maxFiles);
|
|
85
|
+
}
|
|
86
|
+
function tokenizeQuestion(question) {
|
|
87
|
+
return (0, plan_cache_1.normalizeIntent)(question)
|
|
88
|
+
.split(/\s+/)
|
|
89
|
+
.filter((token) => token.length >= 3 && !STOP_WORDS.has(token));
|
|
90
|
+
}
|
|
91
|
+
function escapeRegExp(input) {
|
|
92
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
93
|
+
}
|
|
94
|
+
function normalizeTerm(raw) {
|
|
95
|
+
return raw
|
|
96
|
+
.toLowerCase()
|
|
97
|
+
.replace(/['"`]/g, '')
|
|
98
|
+
.replace(/\s+/g, ' ')
|
|
99
|
+
.trim();
|
|
100
|
+
}
|
|
101
|
+
function extractQuotedTerms(question) {
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
const terms = [];
|
|
104
|
+
const re = /["'`](.{2,80}?)["'`]/g;
|
|
105
|
+
for (const match of question.matchAll(re)) {
|
|
106
|
+
const term = normalizeTerm(match[1] || '');
|
|
107
|
+
if (!term || seen.has(term))
|
|
108
|
+
continue;
|
|
109
|
+
seen.add(term);
|
|
110
|
+
terms.push(term);
|
|
111
|
+
}
|
|
112
|
+
return terms;
|
|
113
|
+
}
|
|
114
|
+
function extractIdentityTerms(question) {
|
|
115
|
+
const normalized = (0, plan_cache_1.normalizeIntent)(question);
|
|
116
|
+
const matches = normalized.match(/(?:organization[\s_-]*id|org[\s_-]*id|orgid|organizationid|user[\s_-]*id|userid|userid)/g);
|
|
117
|
+
if (!matches)
|
|
118
|
+
return [];
|
|
119
|
+
const seen = new Set();
|
|
120
|
+
const ordered = [];
|
|
121
|
+
for (const raw of matches) {
|
|
122
|
+
const term = normalizeTerm(raw);
|
|
123
|
+
if (seen.has(term))
|
|
124
|
+
continue;
|
|
125
|
+
seen.add(term);
|
|
126
|
+
ordered.push(term);
|
|
127
|
+
}
|
|
128
|
+
return ordered;
|
|
129
|
+
}
|
|
130
|
+
function extractComparisonTerms(question) {
|
|
131
|
+
const quoted = extractQuotedTerms(question);
|
|
132
|
+
if (quoted.length >= 2)
|
|
133
|
+
return quoted.slice(0, 2);
|
|
134
|
+
const normalized = (0, plan_cache_1.normalizeIntent)(question);
|
|
135
|
+
const hasComparisonJoiner = normalized.includes('instead of') ||
|
|
136
|
+
normalized.includes(' versus ') ||
|
|
137
|
+
normalized.includes(' vs ') ||
|
|
138
|
+
normalized.includes(' compared to ');
|
|
139
|
+
if (!hasComparisonJoiner)
|
|
140
|
+
return [];
|
|
141
|
+
const identities = extractIdentityTerms(question);
|
|
142
|
+
if (identities.length >= 2)
|
|
143
|
+
return identities.slice(0, 2);
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
function buildTermMatchers(term, weight) {
|
|
147
|
+
const normalized = normalizeTerm(term);
|
|
148
|
+
if (!normalized)
|
|
149
|
+
return [];
|
|
150
|
+
const tokens = normalized.split(/\s+/).filter(Boolean);
|
|
151
|
+
const out = [];
|
|
152
|
+
const push = (label, regexSource) => {
|
|
153
|
+
out.push({
|
|
154
|
+
label,
|
|
155
|
+
regex: new RegExp(regexSource, 'i'),
|
|
156
|
+
weight,
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
if (tokens.length === 1) {
|
|
160
|
+
const token = escapeRegExp(tokens[0]);
|
|
161
|
+
push(normalized, `\\b${token}\\b`);
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
const tokenChain = tokens.map((t) => escapeRegExp(t)).join('[\\s_-]*');
|
|
165
|
+
push(normalized, `\\b${tokenChain}\\b`);
|
|
166
|
+
push(normalized, `\\b${escapeRegExp(tokens.join(''))}\\b`);
|
|
167
|
+
push(normalized, `\\b${escapeRegExp(tokens.join('_'))}\\b`);
|
|
168
|
+
push(normalized, `\\b${escapeRegExp(tokens.join('-'))}\\b`);
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
function buildMatchers(question) {
|
|
172
|
+
const comparisonTerms = extractComparisonTerms(question);
|
|
173
|
+
if (comparisonTerms.length >= 2) {
|
|
174
|
+
const matchers = [
|
|
175
|
+
...buildTermMatchers(comparisonTerms[0], 1.0),
|
|
176
|
+
...buildTermMatchers(comparisonTerms[1], 0.9),
|
|
177
|
+
];
|
|
178
|
+
return {
|
|
179
|
+
mode: 'comparison',
|
|
180
|
+
terms: comparisonTerms.slice(0, 2),
|
|
181
|
+
matchers,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const quoted = extractQuotedTerms(question);
|
|
185
|
+
const keywords = tokenizeQuestion(question).slice(0, 8);
|
|
186
|
+
const terms = [...new Set([...quoted, ...keywords].map(normalizeTerm).filter(Boolean))];
|
|
187
|
+
const matchers = terms.flatMap((term) => buildTermMatchers(term, quoted.includes(term) ? 0.9 : 0.55));
|
|
188
|
+
return {
|
|
189
|
+
mode: 'search',
|
|
190
|
+
terms,
|
|
191
|
+
matchers,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function derivePathHints(question) {
|
|
195
|
+
const normalized = (0, plan_cache_1.normalizeIntent)(question);
|
|
196
|
+
const hints = [];
|
|
197
|
+
if (/\bcli|command|terminal\b/.test(normalized)) {
|
|
198
|
+
hints.push('packages/cli/src/commands/');
|
|
199
|
+
hints.push('packages/cli/src/');
|
|
200
|
+
hints.push('packages/cli/');
|
|
201
|
+
}
|
|
202
|
+
if (/\bapi|backend|server|route|middleware\b/.test(normalized)) {
|
|
203
|
+
hints.push('services/api/');
|
|
204
|
+
}
|
|
205
|
+
if (/\bdashboard|landing|docs|frontend|ui|pricing\b/.test(normalized)) {
|
|
206
|
+
hints.push('web/dashboard/');
|
|
207
|
+
}
|
|
208
|
+
if (/(github action|\bci\b)/.test(normalized)) {
|
|
209
|
+
hints.push('packages/action/');
|
|
210
|
+
hints.push('actions/');
|
|
211
|
+
}
|
|
212
|
+
return [...new Set(hints)];
|
|
213
|
+
}
|
|
214
|
+
function normalizeSnippet(line) {
|
|
215
|
+
return line
|
|
216
|
+
.replace(/\t/g, ' ')
|
|
217
|
+
.replace(/\s+/g, ' ')
|
|
218
|
+
.trim()
|
|
219
|
+
.slice(0, 220);
|
|
220
|
+
}
|
|
221
|
+
function scoreConfidence(citationCount, matchedFiles) {
|
|
222
|
+
if (citationCount === 0)
|
|
223
|
+
return 'low';
|
|
224
|
+
if (citationCount >= 8 && matchedFiles >= 3)
|
|
225
|
+
return 'high';
|
|
226
|
+
return 'medium';
|
|
227
|
+
}
|
|
228
|
+
function buildAnswer(mode, question, terms, citations, stats, termCounts, perFileCounts) {
|
|
229
|
+
const confidence = scoreConfidence(citations.length, stats.matchedFiles);
|
|
230
|
+
const normalizedQuestion = (0, plan_cache_1.normalizeIntent)(question);
|
|
231
|
+
const findings = [];
|
|
232
|
+
let answer = '';
|
|
233
|
+
if (mode === 'comparison' && terms.length >= 2) {
|
|
234
|
+
const left = terms[0];
|
|
235
|
+
const right = terms[1];
|
|
236
|
+
const leftCount = termCounts.get(left) || 0;
|
|
237
|
+
const rightCount = termCounts.get(right) || 0;
|
|
238
|
+
if (leftCount > 0 && rightCount > 0) {
|
|
239
|
+
answer = `Found both "${left}" (${leftCount}) and "${right}" (${rightCount}) references in the scanned scope.`;
|
|
240
|
+
}
|
|
241
|
+
else if (leftCount > 0) {
|
|
242
|
+
answer = `Found "${left}" references (${leftCount}) and no direct "${right}" references in the scanned scope.`;
|
|
243
|
+
}
|
|
244
|
+
else if (rightCount > 0) {
|
|
245
|
+
answer = `No direct "${left}" references found; "${right}" appears ${rightCount} time(s) in the scanned scope.`;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
answer = `No direct references to "${left}" or "${right}" were found in the scanned scope.`;
|
|
249
|
+
}
|
|
250
|
+
findings.push(`Compared terms: "${left}" vs "${right}"`);
|
|
251
|
+
findings.push(`Matches by term: ${left}=${leftCount}, ${right}=${rightCount}`);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
if (citations.length > 0) {
|
|
255
|
+
answer = `Found ${citations.length} relevant evidence line(s) across ${stats.matchedFiles} file(s) for your question.`;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
answer = 'No direct evidence lines were found for this question in the scanned files.';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const topFiles = [...perFileCounts.entries()]
|
|
262
|
+
.sort((a, b) => b[1] - a[1])
|
|
263
|
+
.slice(0, 3)
|
|
264
|
+
.map(([file, count]) => `${file} (${count})`);
|
|
265
|
+
if (topFiles.length > 0) {
|
|
266
|
+
findings.push(`Most relevant files: ${topFiles.join(', ')}`);
|
|
267
|
+
}
|
|
268
|
+
findings.push(`Scanned ${stats.scannedFiles} file(s); matched ${stats.matchedFiles} file(s).`);
|
|
269
|
+
return {
|
|
270
|
+
question,
|
|
271
|
+
questionNormalized: normalizedQuestion,
|
|
272
|
+
mode,
|
|
273
|
+
answer,
|
|
274
|
+
findings,
|
|
275
|
+
confidence,
|
|
276
|
+
citations,
|
|
277
|
+
generatedAt: new Date().toISOString(),
|
|
278
|
+
stats,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function emitAskResult(result, options) {
|
|
282
|
+
if (options.json) {
|
|
283
|
+
console.log(JSON.stringify({
|
|
284
|
+
...result,
|
|
285
|
+
citations: result.citations.slice(0, options.maxCitations),
|
|
286
|
+
cache: options.cacheLabel || 'miss',
|
|
287
|
+
}, null, 2));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (options.cacheLabel) {
|
|
291
|
+
console.log(chalk.dim(`⚡ ${options.cacheLabel}\n`));
|
|
292
|
+
}
|
|
293
|
+
if (!options.fromPlan) {
|
|
294
|
+
console.log(chalk.bold.cyan('🧠 Neurcode Ask\n'));
|
|
295
|
+
}
|
|
296
|
+
console.log(chalk.bold.white('Question:'));
|
|
297
|
+
console.log(chalk.dim(result.question));
|
|
298
|
+
console.log('');
|
|
299
|
+
console.log(chalk.bold.white('Answer:'));
|
|
300
|
+
console.log(chalk.green(result.answer));
|
|
301
|
+
if (result.findings.length > 0) {
|
|
302
|
+
console.log(chalk.bold.white('\nFindings:'));
|
|
303
|
+
for (const finding of result.findings) {
|
|
304
|
+
console.log(chalk.cyan(` • ${finding}`));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const citations = result.citations.slice(0, options.maxCitations);
|
|
308
|
+
if (citations.length > 0) {
|
|
309
|
+
console.log(chalk.bold.white('\nEvidence:'));
|
|
310
|
+
citations.forEach((citation, idx) => {
|
|
311
|
+
const prefix = citation.term ? `${citation.term} ` : '';
|
|
312
|
+
console.log(chalk.dim(` ${idx + 1}. ${citation.path}:${citation.line} ${prefix}${citation.snippet}`));
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
console.log(chalk.yellow('\nEvidence: no direct matching lines found.'));
|
|
317
|
+
}
|
|
318
|
+
console.log(chalk.dim(`\nConfidence: ${result.confidence.toUpperCase()} | scanned=${result.stats.scannedFiles} matched=${result.stats.matchedFiles}`));
|
|
319
|
+
}
|
|
320
|
+
async function askCommand(question, options = {}) {
|
|
321
|
+
try {
|
|
322
|
+
if (!question || !question.trim()) {
|
|
323
|
+
console.error(chalk.red('❌ Error: Question cannot be empty.'));
|
|
324
|
+
console.log(chalk.dim('Usage: neurcode ask "<question>"'));
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
328
|
+
const config = (0, config_1.loadConfig)();
|
|
329
|
+
const orgId = (0, state_1.getOrgId)();
|
|
330
|
+
const stateProjectId = (0, state_1.getProjectId)();
|
|
331
|
+
const projectId = options.projectId || stateProjectId || config.projectId || null;
|
|
332
|
+
const scope = { orgId: orgId || null, projectId: projectId || null };
|
|
333
|
+
const maxCitations = Math.max(3, Math.min(options.maxCitations || 12, 30));
|
|
334
|
+
const shouldUseCache = options.cache !== false && process.env.NEURCODE_ASK_NO_CACHE !== '1';
|
|
335
|
+
const normalizedQuestion = (0, plan_cache_1.normalizeIntent)(question);
|
|
336
|
+
if (process.stdout.isTTY && !process.env.CI) {
|
|
337
|
+
(0, neurcode_context_1.ensureDefaultLocalContextFile)(cwd);
|
|
338
|
+
}
|
|
339
|
+
const fileTree = scanFiles(cwd, MAX_SCAN_FILES);
|
|
340
|
+
if (fileTree.length === 0) {
|
|
341
|
+
console.error(chalk.red('❌ No files found in the current project.'));
|
|
342
|
+
process.exit(1);
|
|
343
|
+
}
|
|
344
|
+
const staticContext = (0, neurcode_context_1.loadStaticNeurcodeContext)(cwd, orgId && projectId ? { orgId, projectId } : undefined);
|
|
345
|
+
const policyVersionHash = (0, plan_cache_1.computePolicyVersionHash)(cwd);
|
|
346
|
+
const neurcodeVersion = (0, plan_cache_1.getNeurcodeVersion)();
|
|
347
|
+
const gitFingerprint = (0, plan_cache_1.getGitRepoFingerprint)(cwd);
|
|
348
|
+
const repoFingerprint = gitFingerprint || (0, plan_cache_1.getFilesystemFingerprintFromTree)(fileTree, cwd);
|
|
349
|
+
if (orgId && projectId) {
|
|
350
|
+
try {
|
|
351
|
+
(0, brain_context_1.refreshBrainContextFromWorkspace)(cwd, scope, {
|
|
352
|
+
workingTreeHash: gitFingerprint?.kind === 'git' ? gitFingerprint.workingTreeHash : undefined,
|
|
353
|
+
maxFiles: 90,
|
|
354
|
+
recordEvent: false,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
// non-blocking
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (shouldUseCache && orgId && projectId) {
|
|
362
|
+
const questionHash = (0, ask_cache_1.computeAskQuestionHash)({
|
|
363
|
+
question: normalizedQuestion,
|
|
364
|
+
contextHash: staticContext.hash,
|
|
365
|
+
});
|
|
366
|
+
const exactKey = (0, ask_cache_1.computeAskCacheKey)({
|
|
367
|
+
schemaVersion: 2,
|
|
368
|
+
orgId,
|
|
369
|
+
projectId,
|
|
370
|
+
repo: repoFingerprint,
|
|
371
|
+
questionHash,
|
|
372
|
+
policyVersionHash,
|
|
373
|
+
neurcodeVersion,
|
|
374
|
+
});
|
|
375
|
+
const exact = (0, ask_cache_1.readCachedAsk)(cwd, exactKey);
|
|
376
|
+
if (exact) {
|
|
377
|
+
const exactOutput = {
|
|
378
|
+
...exact.output,
|
|
379
|
+
question,
|
|
380
|
+
questionNormalized: normalizedQuestion,
|
|
381
|
+
};
|
|
382
|
+
emitAskResult(exactOutput, {
|
|
383
|
+
json: options.json,
|
|
384
|
+
maxCitations,
|
|
385
|
+
cacheLabel: `Using cached answer (created: ${new Date(exact.createdAt).toLocaleString()})`,
|
|
386
|
+
fromPlan: options.fromPlan,
|
|
387
|
+
});
|
|
388
|
+
(0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
|
|
389
|
+
type: 'ask',
|
|
390
|
+
note: 'cache_hit=exact',
|
|
391
|
+
});
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
const near = (0, ask_cache_1.findNearCachedAsk)(cwd, {
|
|
395
|
+
orgId,
|
|
396
|
+
projectId,
|
|
397
|
+
repo: repoFingerprint,
|
|
398
|
+
question: normalizedQuestion,
|
|
399
|
+
policyVersionHash,
|
|
400
|
+
neurcodeVersion,
|
|
401
|
+
contextHash: staticContext.hash,
|
|
402
|
+
changedPaths: (0, ask_cache_1.getChangedWorkingTreePaths)(cwd),
|
|
403
|
+
minSimilarity: 0.68,
|
|
404
|
+
});
|
|
405
|
+
if (near) {
|
|
406
|
+
const nearOutput = {
|
|
407
|
+
...near.entry.output,
|
|
408
|
+
question,
|
|
409
|
+
questionNormalized: normalizedQuestion,
|
|
410
|
+
};
|
|
411
|
+
const reasonText = near.reason === 'safe_repo_drift_similar_question'
|
|
412
|
+
? 'Using near-cached answer (safe repo drift)'
|
|
413
|
+
: 'Using near-cached answer';
|
|
414
|
+
emitAskResult(nearOutput, {
|
|
415
|
+
json: options.json,
|
|
416
|
+
maxCitations,
|
|
417
|
+
cacheLabel: `${reasonText}, similarity ${near.similarity.toFixed(2)}, created: ${new Date(near.entry.createdAt).toLocaleString()}`,
|
|
418
|
+
fromPlan: options.fromPlan,
|
|
419
|
+
});
|
|
420
|
+
(0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
|
|
421
|
+
type: 'ask',
|
|
422
|
+
note: `cache_hit=near;similarity=${near.similarity.toFixed(2)};reason=${near.reason}`,
|
|
423
|
+
});
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (!options.json) {
|
|
428
|
+
console.log(chalk.dim(`🧠 Asking repo context in ${cwd}...`));
|
|
429
|
+
}
|
|
430
|
+
const brainResults = orgId && projectId
|
|
431
|
+
? (0, brain_context_1.searchBrainContextEntries)(cwd, scope, normalizedQuestion, { limit: 48 })
|
|
432
|
+
: { entries: [], totalIndexedFiles: 0 };
|
|
433
|
+
if (!options.json && brainResults.entries.length > 0) {
|
|
434
|
+
const top = brainResults.entries.filter((entry) => entry.score > 0).length;
|
|
435
|
+
console.log(chalk.dim(`🧠 Brain retrieval: ${top} relevant file summaries from ${brainResults.totalIndexedFiles} indexed files`));
|
|
436
|
+
}
|
|
437
|
+
const { mode, terms, matchers } = buildMatchers(question);
|
|
438
|
+
const pathHints = derivePathHints(question);
|
|
439
|
+
const mentionsAskCommand = /\bask\b/.test(normalizedQuestion);
|
|
440
|
+
if (matchers.length === 0) {
|
|
441
|
+
console.error(chalk.red('❌ Could not derive useful search terms from the question.'));
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
const candidateSet = new Set();
|
|
445
|
+
const pathPriority = new Map();
|
|
446
|
+
for (const entry of brainResults.entries) {
|
|
447
|
+
candidateSet.add(entry.path);
|
|
448
|
+
pathPriority.set(entry.path, (pathPriority.get(entry.path) || 0) + entry.score);
|
|
449
|
+
}
|
|
450
|
+
const tokenHints = tokenizeQuestion(question);
|
|
451
|
+
if (candidateSet.size < 80) {
|
|
452
|
+
for (const filePath of fileTree) {
|
|
453
|
+
const normalizedPath = filePath.toLowerCase();
|
|
454
|
+
let score = pathPriority.get(filePath) || 0;
|
|
455
|
+
for (const token of tokenHints) {
|
|
456
|
+
if (normalizedPath.includes(token))
|
|
457
|
+
score += 0.2;
|
|
458
|
+
}
|
|
459
|
+
if (!mentionsAskCommand && (filePath.endsWith('/commands/ask.ts') || filePath.endsWith('/utils/ask-cache.ts'))) {
|
|
460
|
+
score -= 0.45;
|
|
461
|
+
}
|
|
462
|
+
if (pathHints.some((hint) => normalizedPath.startsWith(hint))) {
|
|
463
|
+
score += 0.45;
|
|
464
|
+
}
|
|
465
|
+
else if (pathHints.length > 0) {
|
|
466
|
+
score -= 0.08;
|
|
467
|
+
}
|
|
468
|
+
if (score > 0) {
|
|
469
|
+
candidateSet.add(filePath);
|
|
470
|
+
pathPriority.set(filePath, score);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (candidateSet.size < 20) {
|
|
475
|
+
for (const filePath of fileTree.slice(0, 160)) {
|
|
476
|
+
candidateSet.add(filePath);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const selfReferenceFiles = new Set([
|
|
480
|
+
'packages/cli/src/commands/ask.ts',
|
|
481
|
+
'packages/cli/src/utils/ask-cache.ts',
|
|
482
|
+
]);
|
|
483
|
+
const prioritized = [...candidateSet]
|
|
484
|
+
.filter((filePath) => (0, fs_1.existsSync)((0, path_1.join)(cwd, filePath)))
|
|
485
|
+
.filter((filePath) => mentionsAskCommand || !selfReferenceFiles.has(filePath))
|
|
486
|
+
.sort((a, b) => (pathPriority.get(b) || 0) - (pathPriority.get(a) || 0));
|
|
487
|
+
const hinted = pathHints.length > 0
|
|
488
|
+
? prioritized.filter((filePath) => pathHints.some((hint) => filePath.startsWith(hint)))
|
|
489
|
+
: [];
|
|
490
|
+
const nonHinted = pathHints.length > 0
|
|
491
|
+
? prioritized.filter((filePath) => !pathHints.some((hint) => filePath.startsWith(hint)))
|
|
492
|
+
: prioritized;
|
|
493
|
+
const candidates = [...hinted, ...nonHinted].slice(0, MAX_SCAN_FILES);
|
|
494
|
+
const citations = [];
|
|
495
|
+
const perFileCounts = new Map();
|
|
496
|
+
const termCounts = new Map();
|
|
497
|
+
let scannedFiles = 0;
|
|
498
|
+
for (const filePath of candidates) {
|
|
499
|
+
if (citations.length >= MAX_RAW_CITATIONS)
|
|
500
|
+
break;
|
|
501
|
+
const fullPath = (0, path_1.join)(cwd, filePath);
|
|
502
|
+
let content = '';
|
|
503
|
+
try {
|
|
504
|
+
const st = (0, fs_1.statSync)(fullPath);
|
|
505
|
+
if (st.size > MAX_FILE_BYTES)
|
|
506
|
+
continue;
|
|
507
|
+
content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
|
|
508
|
+
}
|
|
509
|
+
catch {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
scannedFiles++;
|
|
513
|
+
const lines = content.split(/\r?\n/);
|
|
514
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
515
|
+
if (citations.length >= MAX_RAW_CITATIONS)
|
|
516
|
+
break;
|
|
517
|
+
const line = lines[idx];
|
|
518
|
+
if (!line || line.trim().length === 0)
|
|
519
|
+
continue;
|
|
520
|
+
for (const matcher of matchers) {
|
|
521
|
+
if (!matcher.regex.test(line))
|
|
522
|
+
continue;
|
|
523
|
+
const snippet = normalizeSnippet(line);
|
|
524
|
+
if (!snippet)
|
|
525
|
+
continue;
|
|
526
|
+
const evidence = {
|
|
527
|
+
path: filePath,
|
|
528
|
+
line: idx + 1,
|
|
529
|
+
snippet,
|
|
530
|
+
term: matcher.label,
|
|
531
|
+
weight: matcher.weight +
|
|
532
|
+
(pathPriority.get(filePath) || 0) +
|
|
533
|
+
(pathHints.some((hint) => filePath.startsWith(hint)) ? 0.25 : 0),
|
|
534
|
+
};
|
|
535
|
+
const dedupeKey = `${evidence.path}:${evidence.line}:${evidence.term || ''}`;
|
|
536
|
+
if (citations.some((item) => `${item.path}:${item.line}:${item.term || ''}` === dedupeKey)) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
citations.push(evidence);
|
|
540
|
+
perFileCounts.set(filePath, (perFileCounts.get(filePath) || 0) + 1);
|
|
541
|
+
termCounts.set(matcher.label, (termCounts.get(matcher.label) || 0) + 1);
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
citations.sort((a, b) => b.weight - a.weight);
|
|
547
|
+
const selectedForOutput = [];
|
|
548
|
+
if (mode === 'comparison' && terms.length >= 2) {
|
|
549
|
+
for (const term of terms.slice(0, 2)) {
|
|
550
|
+
const firstForTerm = citations.find((citation) => citation.term === term);
|
|
551
|
+
if (firstForTerm) {
|
|
552
|
+
selectedForOutput.push(firstForTerm);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
for (const citation of citations) {
|
|
557
|
+
if (selectedForOutput.length >= maxCitations)
|
|
558
|
+
break;
|
|
559
|
+
if (selectedForOutput.some((existing) => existing.path === citation.path && existing.line === citation.line && existing.term === citation.term)) {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
selectedForOutput.push(citation);
|
|
563
|
+
}
|
|
564
|
+
const finalCitations = selectedForOutput.map(({ path, line, snippet, term }) => ({
|
|
565
|
+
path,
|
|
566
|
+
line,
|
|
567
|
+
snippet,
|
|
568
|
+
term,
|
|
569
|
+
}));
|
|
570
|
+
const stats = {
|
|
571
|
+
scannedFiles,
|
|
572
|
+
matchedFiles: perFileCounts.size,
|
|
573
|
+
matchedLines: citations.length,
|
|
574
|
+
brainCandidates: brainResults.entries.length,
|
|
575
|
+
};
|
|
576
|
+
const answer = buildAnswer(mode, question, terms, finalCitations, stats, termCounts, perFileCounts);
|
|
577
|
+
emitAskResult(answer, {
|
|
578
|
+
json: options.json,
|
|
579
|
+
maxCitations,
|
|
580
|
+
fromPlan: options.fromPlan,
|
|
581
|
+
});
|
|
582
|
+
if (orgId && projectId) {
|
|
583
|
+
(0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
|
|
584
|
+
type: 'ask',
|
|
585
|
+
note: `mode=${mode};matched_files=${stats.matchedFiles};matched_lines=${stats.matchedLines}`,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
if (shouldUseCache && orgId && projectId) {
|
|
589
|
+
const questionHash = (0, ask_cache_1.computeAskQuestionHash)({
|
|
590
|
+
question: normalizedQuestion,
|
|
591
|
+
contextHash: staticContext.hash,
|
|
592
|
+
});
|
|
593
|
+
const key = (0, ask_cache_1.computeAskCacheKey)({
|
|
594
|
+
schemaVersion: 2,
|
|
595
|
+
orgId,
|
|
596
|
+
projectId,
|
|
597
|
+
repo: repoFingerprint,
|
|
598
|
+
questionHash,
|
|
599
|
+
policyVersionHash,
|
|
600
|
+
neurcodeVersion,
|
|
601
|
+
});
|
|
602
|
+
(0, ask_cache_1.writeCachedAsk)(cwd, {
|
|
603
|
+
key,
|
|
604
|
+
input: {
|
|
605
|
+
schemaVersion: 2,
|
|
606
|
+
orgId,
|
|
607
|
+
projectId,
|
|
608
|
+
repo: repoFingerprint,
|
|
609
|
+
questionHash,
|
|
610
|
+
policyVersionHash,
|
|
611
|
+
neurcodeVersion,
|
|
612
|
+
question: normalizedQuestion,
|
|
613
|
+
contextHash: staticContext.hash,
|
|
614
|
+
},
|
|
615
|
+
output: answer,
|
|
616
|
+
evidencePaths: finalCitations.map((citation) => citation.path),
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
console.error(chalk.red('\n❌ Error answering question:'));
|
|
622
|
+
if (error instanceof Error) {
|
|
623
|
+
console.error(chalk.red(error.message));
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
console.error(error);
|
|
627
|
+
}
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
//# sourceMappingURL=ask.js.map
|