codemini-cli 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/core/agent-loop.js +196 -0
- package/src/core/chat-runtime.js +18 -3
- package/src/core/default-system-prompt.js +2 -2
- package/src/core/project-index.js +140 -13
- package/src/core/provider/openai-compatible.js +16 -4
- package/src/core/shell-profile.js +4 -0
- package/src/core/tools.js +50 -1
- package/src/tui/chat-app.js +74 -180
- package/src/tui/skill-activity/index.js +20 -0
- package/src/tui/tool-activity/common.js +29 -0
- package/src/tui/tool-activity/index.js +17 -0
- package/src/tui/tool-activity/presenters/command.js +29 -0
- package/src/tui/tool-activity/presenters/files.js +26 -0
- package/src/tui/tool-activity/presenters/misc.js +19 -0
- package/src/tui/tool-activity/presenters/system.js +14 -0
- package/src/tui/tool-narration/common.js +37 -0
- package/src/tui/tool-narration/presenters/change.js +109 -0
- package/src/tui/tool-narration/presenters/edit.js +3 -0
- package/src/tui/tool-narration/presenters/generic.js +10 -0
- package/src/tui/tool-narration/presenters/glob.js +11 -0
- package/src/tui/tool-narration/presenters/grep.js +11 -0
- package/src/tui/tool-narration/presenters/list.js +11 -0
- package/src/tui/tool-narration/presenters/patch.js +3 -0
- package/src/tui/tool-narration/presenters/read.js +11 -0
- package/src/tui/tool-narration/presenters/run.js +29 -0
- package/src/tui/tool-narration/presenters/write.js +3 -0
- package/src/tui/tool-narration.js +67 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { handleConfig } from './commands/config.js';
|
|
|
4
4
|
import { handleDoctor } from './commands/doctor.js';
|
|
5
5
|
import { handleSkill } from './commands/skill.js';
|
|
6
6
|
|
|
7
|
-
const VERSION = '0.
|
|
7
|
+
const VERSION = '0.3.0';
|
|
8
8
|
|
|
9
9
|
function printHelp() {
|
|
10
10
|
console.log(`codemini ${VERSION}
|
package/src/core/agent-loop.js
CHANGED
|
@@ -406,6 +406,161 @@ export function trimInline(value, maxLen = 72) {
|
|
|
406
406
|
return `${s.slice(0, maxLen - 3)}...`;
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
+
function normalizeAssistantText(value) {
|
|
410
|
+
return String(value || '').trim();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function hasTrailingToolContext(messages = []) {
|
|
414
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
415
|
+
const message = messages[index];
|
|
416
|
+
if (!message || typeof message !== 'object') continue;
|
|
417
|
+
if (message.role === 'tool') return true;
|
|
418
|
+
if (message.role === 'assistant' || message.role === 'user') return false;
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function isGenericCompletionText(text) {
|
|
424
|
+
const normalized = normalizeAssistantText(text).toLowerCase();
|
|
425
|
+
if (!normalized) return false;
|
|
426
|
+
const genericPhrases = new Set([
|
|
427
|
+
'done',
|
|
428
|
+
'completed',
|
|
429
|
+
'complete',
|
|
430
|
+
'finished',
|
|
431
|
+
'task completed',
|
|
432
|
+
'all done',
|
|
433
|
+
'ok',
|
|
434
|
+
'okay',
|
|
435
|
+
'已完成',
|
|
436
|
+
'已完成任务',
|
|
437
|
+
'完成',
|
|
438
|
+
'任务完成'
|
|
439
|
+
]);
|
|
440
|
+
return genericPhrases.has(normalized);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function shouldAskForConcreteFinalAnswer(text, messages = []) {
|
|
444
|
+
if (!hasTrailingToolContext(messages)) return false;
|
|
445
|
+
const normalized = normalizeAssistantText(text);
|
|
446
|
+
if (!normalized) return true;
|
|
447
|
+
return isGenericCompletionText(normalized);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function isBroadRepositoryAnalysisTask(text) {
|
|
451
|
+
const normalized = String(text || '').trim().toLowerCase();
|
|
452
|
+
if (!normalized) return false;
|
|
453
|
+
return (
|
|
454
|
+
/optimi|improve|analy[sz]e|audit|review|overview|architecture|codebase|repository|repo/.test(normalized) ||
|
|
455
|
+
/项目.*优化|项目.*问题|可优化|分析这个项目|看看.*项目|代码库|仓库/.test(String(text || ''))
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function parseProjectIndexSummary(text) {
|
|
460
|
+
const sourceRoots = [];
|
|
461
|
+
const entryCandidates = [];
|
|
462
|
+
const candidateFiles = [];
|
|
463
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
464
|
+
const trimmed = line.trim();
|
|
465
|
+
if (trimmed.startsWith('source_roots:')) {
|
|
466
|
+
sourceRoots.push(
|
|
467
|
+
...String(trimmed.slice('source_roots:'.length))
|
|
468
|
+
.split(',')
|
|
469
|
+
.map((value) => value.trim())
|
|
470
|
+
.filter(Boolean)
|
|
471
|
+
);
|
|
472
|
+
} else if (trimmed.startsWith('entry_candidates:')) {
|
|
473
|
+
entryCandidates.push(
|
|
474
|
+
...String(trimmed.slice('entry_candidates:'.length))
|
|
475
|
+
.split(',')
|
|
476
|
+
.map((value) => value.trim())
|
|
477
|
+
.filter(Boolean)
|
|
478
|
+
);
|
|
479
|
+
} else if (trimmed.startsWith('- ')) {
|
|
480
|
+
const match = trimmed.match(/^- ([^ ]+)/);
|
|
481
|
+
if (match?.[1]) candidateFiles.push(match[1].trim());
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return { sourceRoots, entryCandidates, candidateFiles };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function createAnalysisGuardState(userPrompt) {
|
|
488
|
+
return {
|
|
489
|
+
active: isBroadRepositoryAnalysisTask(userPrompt),
|
|
490
|
+
indexQueried: false,
|
|
491
|
+
sourceRoots: new Set(),
|
|
492
|
+
entryCandidates: new Set(),
|
|
493
|
+
candidateFiles: new Set(),
|
|
494
|
+
relevantSourceReads: new Set(),
|
|
495
|
+
blockedExplorations: 0
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function topLevelPath(value) {
|
|
500
|
+
const normalized = String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '').trim();
|
|
501
|
+
return normalized.split('/')[0] || '';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function isRelevantSourcePath(filePath, state) {
|
|
505
|
+
const normalized = String(filePath || '').replace(/\\/g, '/').trim();
|
|
506
|
+
if (!normalized) return false;
|
|
507
|
+
if (state.candidateFiles.has(normalized) || state.entryCandidates.has(normalized)) return true;
|
|
508
|
+
for (const root of state.sourceRoots) {
|
|
509
|
+
if (normalized === root || normalized.startsWith(`${root}/`)) return true;
|
|
510
|
+
}
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function blockedExplorationReason(toolName, args, state) {
|
|
515
|
+
if (!state.active) return '';
|
|
516
|
+
if (!state.indexQueried && toolName !== 'query_project_index') {
|
|
517
|
+
return 'Use query_project_index before broad repository exploration so the next reads stay focused on relevant source files.';
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const target = String(args?.path || args?.pattern || args?.query || '').replace(/\\/g, '/').trim();
|
|
521
|
+
const top = topLevelPath(target);
|
|
522
|
+
if (!top) return '';
|
|
523
|
+
|
|
524
|
+
if (['skills', 'souls', 'templates', '.codemini', '.codemini-project'].includes(top)) {
|
|
525
|
+
return `Skip ${top}/ for broad repository analysis unless the user explicitly asks for it. Inspect relevant source files first.`;
|
|
526
|
+
}
|
|
527
|
+
if (top === 'tests' && state.relevantSourceReads.size < 2) {
|
|
528
|
+
return 'Inspect the next relevant source files before reading tests. Broad analysis should be grounded in production code first.';
|
|
529
|
+
}
|
|
530
|
+
return '';
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function noteAnalysisEvidence(state, toolName, args, toolResult) {
|
|
534
|
+
if (!state.active) return;
|
|
535
|
+
if (toolName === 'query_project_index') {
|
|
536
|
+
state.indexQueried = true;
|
|
537
|
+
const summary = parseProjectIndexSummary(JSON.stringify(toolResult));
|
|
538
|
+
for (const root of summary.sourceRoots) state.sourceRoots.add(root);
|
|
539
|
+
for (const entry of summary.entryCandidates) state.entryCandidates.add(entry);
|
|
540
|
+
for (const file of summary.candidateFiles) state.candidateFiles.add(file);
|
|
541
|
+
const projectMap = toolResult?.project_map || {};
|
|
542
|
+
for (const root of projectMap.source_roots || []) state.sourceRoots.add(String(root));
|
|
543
|
+
for (const entry of projectMap.entry_candidates || []) state.entryCandidates.add(String(entry));
|
|
544
|
+
for (const match of toolResult?.matches || []) {
|
|
545
|
+
if (match?.file) state.candidateFiles.add(String(match.file));
|
|
546
|
+
}
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (toolName === 'read') {
|
|
551
|
+
const filePath = String(toolResult?.path || args?.path || '').split(':')[0];
|
|
552
|
+
if (isRelevantSourcePath(filePath, state)) {
|
|
553
|
+
state.relevantSourceReads.add(filePath);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function needsMoreAnalysisEvidence(state) {
|
|
559
|
+
if (!state.active) return false;
|
|
560
|
+
if (!state.indexQueried) return true;
|
|
561
|
+
return state.relevantSourceReads.size < 2;
|
|
562
|
+
}
|
|
563
|
+
|
|
409
564
|
function normalizeToolCallName(name) {
|
|
410
565
|
return String(name || '').trim();
|
|
411
566
|
}
|
|
@@ -509,6 +664,8 @@ export async function runAgentLoop({
|
|
|
509
664
|
|
|
510
665
|
let finalText = '';
|
|
511
666
|
let lastAssistantText = '';
|
|
667
|
+
let pendingSummaryNudges = 0;
|
|
668
|
+
const analysisGuard = createAnalysisGuardState(userPrompt);
|
|
512
669
|
const alwaysAllowSet = new Set((Array.isArray(alwaysAllowTools) ? alwaysAllowTools : []).map((t) => String(t)));
|
|
513
670
|
|
|
514
671
|
// Mutable tool list — grows as tool_search loads deferred tools
|
|
@@ -522,6 +679,10 @@ export async function runAgentLoop({
|
|
|
522
679
|
tools: activeTools
|
|
523
680
|
});
|
|
524
681
|
|
|
682
|
+
if (completion?.incomplete) {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
|
|
525
686
|
const toolCalls = Array.isArray(completion.toolCalls) ? completion.toolCalls : [];
|
|
526
687
|
const assistantText = completion.text || '';
|
|
527
688
|
lastAssistantText = assistantText || lastAssistantText;
|
|
@@ -546,10 +707,30 @@ export async function runAgentLoop({
|
|
|
546
707
|
}
|
|
547
708
|
|
|
548
709
|
if (toolCalls.length === 0) {
|
|
710
|
+
if (needsMoreAnalysisEvidence(analysisGuard) && pendingSummaryNudges < 2) {
|
|
711
|
+
pendingSummaryNudges += 1;
|
|
712
|
+
messages.push({
|
|
713
|
+
role: 'user',
|
|
714
|
+
content:
|
|
715
|
+
'You have not inspected enough relevant source files yet. Query the project index if needed, then inspect the next relevant source files before concluding. Do not stop after unrelated directories, tests, skills, souls, or templates.'
|
|
716
|
+
});
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
if (shouldAskForConcreteFinalAnswer(assistantText, messages.slice(0, -1)) && pendingSummaryNudges < 2) {
|
|
720
|
+
pendingSummaryNudges += 1;
|
|
721
|
+
messages.push({
|
|
722
|
+
role: 'user',
|
|
723
|
+
content:
|
|
724
|
+
'You have already inspected tool results. Before stopping, check whether the task is actually complete. If it is, provide a concise final answer with specific findings or concrete next steps. If it is not, continue with the next tool call.'
|
|
725
|
+
});
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
549
728
|
finalText = assistantText;
|
|
550
729
|
return { text: finalText, messages, steps: step + 1 };
|
|
551
730
|
}
|
|
552
731
|
|
|
732
|
+
pendingSummaryNudges = 0;
|
|
733
|
+
|
|
553
734
|
if (executionMode === 'plan') {
|
|
554
735
|
const plannedLines = callsToPlanSummary(toolCalls);
|
|
555
736
|
finalText = [
|
|
@@ -615,6 +796,20 @@ export async function runAgentLoop({
|
|
|
615
796
|
throw new Error(`Unknown tool: ${call.name}`);
|
|
616
797
|
}
|
|
617
798
|
|
|
799
|
+
const blockedReason = blockedExplorationReason(toolName, args, analysisGuard);
|
|
800
|
+
if (blockedReason) {
|
|
801
|
+
analysisGuard.blockedExplorations += 1;
|
|
802
|
+
const content = clipToolResult({ error: blockedReason }, toolResultMaxChars);
|
|
803
|
+
if (onEvent) {
|
|
804
|
+
onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments: args, durationMs: 0, summary: trimInline(blockedReason, 120) });
|
|
805
|
+
}
|
|
806
|
+
return {
|
|
807
|
+
callId: call.id,
|
|
808
|
+
content,
|
|
809
|
+
error: true
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
618
813
|
let toolResult;
|
|
619
814
|
try {
|
|
620
815
|
toolResult = await handler(args);
|
|
@@ -638,6 +833,7 @@ export async function runAgentLoop({
|
|
|
638
833
|
|
|
639
834
|
// P1b: Use per-tool formatter if available, else fallback
|
|
640
835
|
let formatted = formatToolResult(toolResult, toolName, args, toolFormatters, toolResultMaxChars);
|
|
836
|
+
noteAnalysisEvidence(analysisGuard, toolName, args, toolResult);
|
|
641
837
|
|
|
642
838
|
// P2: If tool_search loaded deferred tools, inject their schemas into activeTools
|
|
643
839
|
if (toolName === 'tool_search' && toolResult && Array.isArray(toolResult.schemas)) {
|
package/src/core/chat-runtime.js
CHANGED
|
@@ -1518,7 +1518,7 @@ async function askModel({
|
|
|
1518
1518
|
|
|
1519
1519
|
const projectContextSnippet = await buildProjectContextSnippet(process.cwd(), text).catch(() => '');
|
|
1520
1520
|
const effectiveSystemPrompt = projectContextSnippet
|
|
1521
|
-
? `${systemPrompt}\n\n${projectContextSnippet}\n\nUse this project context as lightweight guidance.
|
|
1521
|
+
? `${systemPrompt}\n\n${projectContextSnippet}\n\nUse this project context as lightweight guidance. Query the project index before broad globs or reading many files, then use targeted reads for fresh verification.`
|
|
1522
1522
|
: systemPrompt;
|
|
1523
1523
|
|
|
1524
1524
|
const { definitions, handlers, formatters, deferredDefinitions } = getBuiltinTools({
|
|
@@ -1586,8 +1586,15 @@ async function askModel({
|
|
|
1586
1586
|
toolFormatters: formatters,
|
|
1587
1587
|
deferredDefinitions,
|
|
1588
1588
|
requestCompletion: async ({ messages, tools, model: selectedModel }) => {
|
|
1589
|
-
|
|
1590
|
-
|
|
1589
|
+
let started = false;
|
|
1590
|
+
const startAssistantStream = () => {
|
|
1591
|
+
if (!started && onAgentEvent) {
|
|
1592
|
+
started = true;
|
|
1593
|
+
onAgentEvent({ type: 'assistant:start' });
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
|
|
1597
|
+
const result = await createChatCompletionStream({
|
|
1591
1598
|
baseUrl: config.gateway.base_url,
|
|
1592
1599
|
apiKey: config.gateway.api_key,
|
|
1593
1600
|
model: selectedModel,
|
|
@@ -1596,12 +1603,20 @@ async function askModel({
|
|
|
1596
1603
|
timeoutMs: config.gateway.timeout_ms || 90000,
|
|
1597
1604
|
maxRetries: config.gateway.max_retries ?? 2,
|
|
1598
1605
|
onTextDelta: (delta) => {
|
|
1606
|
+
startAssistantStream();
|
|
1599
1607
|
if (onAgentEvent) onAgentEvent({ type: 'assistant:delta', text: delta });
|
|
1600
1608
|
},
|
|
1601
1609
|
onToolCallDelta: (toolCall) => {
|
|
1610
|
+
startAssistantStream();
|
|
1602
1611
|
if (onAgentEvent) onAgentEvent({ type: 'assistant:tool_call_delta', toolCall });
|
|
1603
1612
|
}
|
|
1604
1613
|
});
|
|
1614
|
+
|
|
1615
|
+
if (!started && !result?.incomplete && (result?.text || result?.toolCalls?.length)) {
|
|
1616
|
+
startAssistantStream();
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
return result;
|
|
1605
1620
|
}
|
|
1606
1621
|
});
|
|
1607
1622
|
|
|
@@ -14,8 +14,8 @@ If the user mentions a project-relative path like src/app.ts, resolve it from ${
|
|
|
14
14
|
|
|
15
15
|
1. File discovery then read
|
|
16
16
|
User: compare the auth flow
|
|
17
|
-
Assistant: first
|
|
18
|
-
Tool:
|
|
17
|
+
Assistant: first narrow the search with the project index
|
|
18
|
+
Tool: query_project_index({"query":"auth flow","path":"src","max_results":3})
|
|
19
19
|
Tool: read({"file_path":"${cwd}/src/auth/service.ts"})
|
|
20
20
|
|
|
21
21
|
2. Targeted search then exact text edit
|
|
@@ -3,7 +3,25 @@ import path from 'node:path';
|
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { getFileIndexPath, getProjectIndexDir, getProjectMapPath, getProjectWorkspaceDir } from './paths.js';
|
|
5
5
|
|
|
6
|
-
const SKIP_DIRS = new Set([
|
|
6
|
+
const SKIP_DIRS = new Set([
|
|
7
|
+
'.git',
|
|
8
|
+
'node_modules',
|
|
9
|
+
'.codemini',
|
|
10
|
+
'.codemini-project',
|
|
11
|
+
'.codemini-global',
|
|
12
|
+
'dist',
|
|
13
|
+
'coverage',
|
|
14
|
+
'sessions',
|
|
15
|
+
'tmp',
|
|
16
|
+
'temp',
|
|
17
|
+
'.cache',
|
|
18
|
+
'.turbo',
|
|
19
|
+
'.next',
|
|
20
|
+
'build',
|
|
21
|
+
'out',
|
|
22
|
+
'logs',
|
|
23
|
+
'artifacts'
|
|
24
|
+
]);
|
|
7
25
|
const PROJECT_MARKER_FILES = new Set([
|
|
8
26
|
'package.json',
|
|
9
27
|
'tsconfig.json',
|
|
@@ -115,9 +133,9 @@ function gitignorePatternToRegex(pattern) {
|
|
|
115
133
|
return new RegExp(`^${regexBody}$`);
|
|
116
134
|
}
|
|
117
135
|
|
|
118
|
-
async function
|
|
136
|
+
async function readIgnoreFileRules(cwd, fileName) {
|
|
119
137
|
try {
|
|
120
|
-
const raw = await fs.readFile(path.join(cwd,
|
|
138
|
+
const raw = await fs.readFile(path.join(cwd, fileName), 'utf8');
|
|
121
139
|
return raw
|
|
122
140
|
.split(/\r?\n/)
|
|
123
141
|
.map((line) => line.trim())
|
|
@@ -143,6 +161,18 @@ async function readGitignoreRules(cwd) {
|
|
|
143
161
|
}
|
|
144
162
|
}
|
|
145
163
|
|
|
164
|
+
async function readProjectIgnoreRules(cwd) {
|
|
165
|
+
const [gitignoreRules, llmignoreRules] = await Promise.all([
|
|
166
|
+
readIgnoreFileRules(cwd, '.gitignore'),
|
|
167
|
+
readIgnoreFileRules(cwd, '.llmignore')
|
|
168
|
+
]);
|
|
169
|
+
return {
|
|
170
|
+
gitignoreRules,
|
|
171
|
+
llmignoreRules,
|
|
172
|
+
combinedRules: [...gitignoreRules, ...llmignoreRules]
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
146
176
|
function matchesGitignoreRule(rule, relativePath, isDirectory) {
|
|
147
177
|
if (!rule || !relativePath) return false;
|
|
148
178
|
if (rule.dirOnly && !isDirectory) return false;
|
|
@@ -212,17 +242,17 @@ async function findNearestIndexedProjectRoot(startDir, workspaceRoot) {
|
|
|
212
242
|
return null;
|
|
213
243
|
}
|
|
214
244
|
|
|
215
|
-
async function walkFiles(cwd, start = cwd, out = [],
|
|
245
|
+
async function walkFiles(cwd, start = cwd, out = [], ignoreRules = []) {
|
|
216
246
|
const entries = await fs.readdir(start, { withFileTypes: true });
|
|
217
247
|
for (const entry of entries) {
|
|
218
248
|
const absolutePath = path.join(start, entry.name);
|
|
219
249
|
const relativePath = rel(cwd, absolutePath);
|
|
220
250
|
if (entry.isDirectory()) {
|
|
221
|
-
if (shouldIgnorePath(relativePath, true,
|
|
222
|
-
await walkFiles(cwd, absolutePath, out,
|
|
251
|
+
if (shouldIgnorePath(relativePath, true, ignoreRules)) continue;
|
|
252
|
+
await walkFiles(cwd, absolutePath, out, ignoreRules);
|
|
223
253
|
continue;
|
|
224
254
|
}
|
|
225
|
-
if (shouldIgnorePath(relativePath, false,
|
|
255
|
+
if (shouldIgnorePath(relativePath, false, ignoreRules)) continue;
|
|
226
256
|
out.push(absolutePath);
|
|
227
257
|
}
|
|
228
258
|
return out;
|
|
@@ -298,12 +328,12 @@ async function scanProject(cwd) {
|
|
|
298
328
|
workspaceKind,
|
|
299
329
|
projectMap: null,
|
|
300
330
|
fileIndex: null,
|
|
301
|
-
|
|
331
|
+
ignoreRules: []
|
|
302
332
|
};
|
|
303
333
|
}
|
|
304
334
|
|
|
305
|
-
const gitignoreRules = await
|
|
306
|
-
const allFiles = await walkFiles(cwd, cwd, [],
|
|
335
|
+
const { gitignoreRules, llmignoreRules, combinedRules } = await readProjectIgnoreRules(cwd);
|
|
336
|
+
const allFiles = await walkFiles(cwd, cwd, [], combinedRules);
|
|
307
337
|
const relativeFiles = allFiles.map((filePath) => rel(cwd, filePath));
|
|
308
338
|
const sourceFiles = allFiles.filter((filePath) => SOURCE_EXTENSIONS.has(path.extname(filePath).toLowerCase()));
|
|
309
339
|
|
|
@@ -362,13 +392,14 @@ async function scanProject(cwd) {
|
|
|
362
392
|
frameworkHints,
|
|
363
393
|
directories,
|
|
364
394
|
gitignoreEnabled: gitignoreRules.length > 0,
|
|
395
|
+
llmignoreEnabled: llmignoreRules.length > 0,
|
|
365
396
|
updatedAt: new Date().toISOString()
|
|
366
397
|
},
|
|
367
398
|
fileIndex: {
|
|
368
399
|
updatedAt: new Date().toISOString(),
|
|
369
400
|
files
|
|
370
401
|
},
|
|
371
|
-
|
|
402
|
+
ignoreRules: combinedRules
|
|
372
403
|
};
|
|
373
404
|
}
|
|
374
405
|
|
|
@@ -417,7 +448,7 @@ export async function refreshIndexedFile(cwd = process.cwd(), relativePath = '')
|
|
|
417
448
|
const projectRoot = await findProjectRootFromFile(cwd, relativePath);
|
|
418
449
|
if (!projectRoot) return null;
|
|
419
450
|
const fileIndexPath = getFileIndexPath(projectRoot);
|
|
420
|
-
const
|
|
451
|
+
const { combinedRules } = await readProjectIgnoreRules(projectRoot);
|
|
421
452
|
const absolutePath = path.join(cwd, relativePath);
|
|
422
453
|
const stat = await safeStat(absolutePath);
|
|
423
454
|
let action = 'updated';
|
|
@@ -426,7 +457,7 @@ export async function refreshIndexedFile(cwd = process.cwd(), relativePath = '')
|
|
|
426
457
|
const files = Array.isArray(current.files) ? [...current.files] : [];
|
|
427
458
|
const index = files.findIndex((entry) => entry.file === projectRelativePath);
|
|
428
459
|
|
|
429
|
-
if (shouldIgnorePath(projectRelativePath, Boolean(stat?.isDirectory?.()),
|
|
460
|
+
if (shouldIgnorePath(projectRelativePath, Boolean(stat?.isDirectory?.()), combinedRules)) {
|
|
430
461
|
if (index >= 0) files.splice(index, 1);
|
|
431
462
|
action = 'removed';
|
|
432
463
|
} else if (!stat || !stat.isFile()) {
|
|
@@ -508,3 +539,99 @@ export async function buildProjectContextSnippet(cwd = process.cwd(), userText =
|
|
|
508
539
|
const snippet = trimMultiline(lines.join('\n'));
|
|
509
540
|
return snippet;
|
|
510
541
|
}
|
|
542
|
+
|
|
543
|
+
export async function queryProjectIndex(cwd = process.cwd(), args = {}) {
|
|
544
|
+
const indexedRoot = await findNearestIndexedProjectRoot(cwd, cwd);
|
|
545
|
+
if (!indexedRoot) {
|
|
546
|
+
return {
|
|
547
|
+
query: String(args?.query || '').trim(),
|
|
548
|
+
project_root: '',
|
|
549
|
+
project_map: null,
|
|
550
|
+
matches: []
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const projectMap = await safeReadJson(getProjectMapPath(indexedRoot), null);
|
|
555
|
+
const fileIndex = await safeReadJson(getFileIndexPath(indexedRoot), null);
|
|
556
|
+
const query = String(args?.query || '').trim();
|
|
557
|
+
const pathPrefix = normalizeRelativePath(args?.path || args?.path_prefix || '');
|
|
558
|
+
const languageFilter = String(args?.language || '').trim().toLowerCase();
|
|
559
|
+
const maxResults = Math.max(1, Math.min(20, Number(args?.max_results || 8) || 8));
|
|
560
|
+
const files = Array.isArray(fileIndex?.files) ? fileIndex.files : [];
|
|
561
|
+
const tokens = tokenizeQuery(query);
|
|
562
|
+
|
|
563
|
+
const matches = [];
|
|
564
|
+
for (const entry of files) {
|
|
565
|
+
const relativePath = String(entry?.file || '');
|
|
566
|
+
if (!relativePath) continue;
|
|
567
|
+
if (pathPrefix && !relativePath.startsWith(pathPrefix)) continue;
|
|
568
|
+
if (languageFilter && String(entry?.language || '').toLowerCase() !== languageFilter) continue;
|
|
569
|
+
|
|
570
|
+
let score = 0;
|
|
571
|
+
const reasons = [];
|
|
572
|
+
const fileText = relativePath.toLowerCase();
|
|
573
|
+
for (const token of tokens) {
|
|
574
|
+
if (!token) continue;
|
|
575
|
+
if (fileText.includes(token)) {
|
|
576
|
+
score += 5;
|
|
577
|
+
reasons.push(`path:${token}`);
|
|
578
|
+
}
|
|
579
|
+
if ((entry.exports || []).some((value) => String(value).toLowerCase() === token)) {
|
|
580
|
+
score += 4;
|
|
581
|
+
reasons.push(`export:${token}`);
|
|
582
|
+
}
|
|
583
|
+
if ((entry.functions || []).some((value) => String(value).toLowerCase().includes(token))) {
|
|
584
|
+
score += 4;
|
|
585
|
+
reasons.push(`function:${token}`);
|
|
586
|
+
}
|
|
587
|
+
if ((entry.classes || []).some((value) => String(value).toLowerCase().includes(token))) {
|
|
588
|
+
score += 4;
|
|
589
|
+
reasons.push(`class:${token}`);
|
|
590
|
+
}
|
|
591
|
+
if ((entry.imports || []).some((value) => String(value).toLowerCase().includes(token))) {
|
|
592
|
+
score += 2;
|
|
593
|
+
reasons.push(`import:${token}`);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (!query) {
|
|
598
|
+
if ((projectMap?.entryCandidates || []).includes(relativePath)) score += 3;
|
|
599
|
+
if ((projectMap?.importantFiles || []).includes(relativePath)) score += 2;
|
|
600
|
+
if (String(relativePath).startsWith('src/')) score += 1;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (score <= 0 && query) continue;
|
|
604
|
+
matches.push({
|
|
605
|
+
file: relativePath,
|
|
606
|
+
language: entry.language || 'text',
|
|
607
|
+
score,
|
|
608
|
+
reasons: clipList(reasons, 8),
|
|
609
|
+
exports: clipList(entry.exports || [], 6),
|
|
610
|
+
functions: clipList(entry.functions || [], 6),
|
|
611
|
+
classes: clipList(entry.classes || [], 6),
|
|
612
|
+
imports: clipList(entry.imports || [], 6)
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
matches.sort((left, right) => right.score - left.score || String(left.file).localeCompare(String(right.file)));
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
query,
|
|
620
|
+
project_root: indexedRoot,
|
|
621
|
+
project_map: projectMap
|
|
622
|
+
? {
|
|
623
|
+
workspace_kind: projectMap.workspaceKind || 'project',
|
|
624
|
+
languages: clipList(projectMap.languages || [], 8),
|
|
625
|
+
package_managers: clipList(projectMap.packageManagers || [], 8),
|
|
626
|
+
important_files: clipList(projectMap.importantFiles || [], 8),
|
|
627
|
+
source_roots: clipList(projectMap.sourceRoots || [], 8),
|
|
628
|
+
test_roots: clipList(projectMap.testRoots || [], 8),
|
|
629
|
+
entry_candidates: clipList(projectMap.entryCandidates || [], 8),
|
|
630
|
+
framework_hints: clipList(projectMap.frameworkHints || [], 8),
|
|
631
|
+
gitignore_enabled: Boolean(projectMap.gitignoreEnabled),
|
|
632
|
+
llmignore_enabled: Boolean(projectMap.llmignoreEnabled)
|
|
633
|
+
}
|
|
634
|
+
: null,
|
|
635
|
+
matches: matches.slice(0, maxResults)
|
|
636
|
+
};
|
|
637
|
+
}
|
|
@@ -145,13 +145,15 @@ function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
|
|
|
145
145
|
arguments: tc.arguments || '{}'
|
|
146
146
|
}))
|
|
147
147
|
.filter((tc) => tc.name);
|
|
148
|
+
const normalizedText = String(text || '').trim();
|
|
148
149
|
|
|
149
|
-
if (!
|
|
150
|
+
if (!normalizedText && toolCalls.length === 0) {
|
|
150
151
|
if (hasTrailingToolContext(messages)) {
|
|
151
152
|
return {
|
|
152
153
|
text: '',
|
|
153
154
|
toolCalls: [],
|
|
154
|
-
usage
|
|
155
|
+
usage,
|
|
156
|
+
incomplete: true
|
|
155
157
|
};
|
|
156
158
|
}
|
|
157
159
|
throw new Error('Gateway stream returned empty assistant response');
|
|
@@ -160,7 +162,8 @@ function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
|
|
|
160
162
|
return {
|
|
161
163
|
text,
|
|
162
164
|
toolCalls,
|
|
163
|
-
usage
|
|
165
|
+
usage,
|
|
166
|
+
incomplete: false
|
|
164
167
|
};
|
|
165
168
|
}
|
|
166
169
|
|
|
@@ -246,8 +249,17 @@ export async function createChatCompletion({
|
|
|
246
249
|
name: tc.function?.name,
|
|
247
250
|
arguments: normalizeIncomingToolCallArguments(tc.function?.arguments)
|
|
248
251
|
}));
|
|
252
|
+
const normalizedText = String(text || '').trim();
|
|
249
253
|
|
|
250
|
-
if (!
|
|
254
|
+
if (!normalizedText && toolCalls.length === 0) {
|
|
255
|
+
if (hasTrailingToolContext(messages)) {
|
|
256
|
+
return {
|
|
257
|
+
text: '',
|
|
258
|
+
toolCalls: [],
|
|
259
|
+
usage: data?.usage || null,
|
|
260
|
+
incomplete: true
|
|
261
|
+
};
|
|
262
|
+
}
|
|
251
263
|
throw new Error('Gateway returned empty assistant response');
|
|
252
264
|
}
|
|
253
265
|
|
|
@@ -123,6 +123,7 @@ export function getShellSystemPrompt(value) {
|
|
|
123
123
|
# Using your tools
|
|
124
124
|
|
|
125
125
|
ALWAYS prefer dedicated tools over raw shell commands:
|
|
126
|
+
- Use query_project_index first for broad repository understanding. It combines project-map metadata with indexed file symbols so you can narrow candidates before reading source files
|
|
126
127
|
- Use read to inspect files — NEVER use cat, head, or tail via run. read returns content directly by default; demo-style shapes like {file_path:"src/app.ts"}, {path:"src/app.ts:10-40"}, or {file_path:"src/app.ts", offset:10, limit:30} are accepted
|
|
127
128
|
- Use grep to search file contents — NEVER use grep or rg via run
|
|
128
129
|
- Use glob to find files by pattern — NEVER use find via run
|
|
@@ -141,6 +142,7 @@ For services: use start_service to launch, list_services/get_service_status/get_
|
|
|
141
142
|
Some tools are loaded on demand. If a needed tool is not listed, call tool_search first to load it.
|
|
142
143
|
|
|
143
144
|
Common tool call patterns:
|
|
145
|
+
- Query the project index first: {query:"login auth flow", path:"src", max_results:5}
|
|
144
146
|
- Read a file: {path:"src/app.ts"} or {file_path:"src/app.ts", offset:20, limit:40}
|
|
145
147
|
- Read a specific range inline: {path:"src/app.ts:20-60"}
|
|
146
148
|
- Search text: {pattern:"loginUser", path:"src"} or {query:"loginUser", directory:"src"}
|
|
@@ -158,6 +160,8 @@ Common tool call patterns:
|
|
|
158
160
|
- Before substantial tool work, send a short progress update to the user about what you are about to inspect or do
|
|
159
161
|
- Do not jump straight into tools without a brief user-facing note when the task is actionable
|
|
160
162
|
- Search or read before editing unless the exact target is already known
|
|
163
|
+
- For broad or ambiguous requests, query_project_index before large globs or reading many files
|
|
164
|
+
- Do not read files one by one after a wide glob when query_project_index can narrow the candidates first
|
|
161
165
|
- If a command or tool is blocked or fails, inspect the error and retry with allowed commands or tools
|
|
162
166
|
- For AST-scoped edits, if edit rejects due to missing or stale ast_target, fix arguments and retry
|
|
163
167
|
- Do not claim filesystem access is impossible unless search/read tools also fail
|