codemini-cli 0.2.9 → 0.3.1
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/souls/anime.md +13 -2
- package/souls/caveman.md +12 -2
- package/souls/ceo.md +14 -3
- package/souls/default.md +10 -2
- package/souls/pirate.md +13 -3
- package/souls/playful.md +13 -3
- package/souls/professional.md +13 -3
- package/src/cli.js +2 -1
- package/src/commands/chat.js +2 -0
- package/src/core/agent-loop.js +208 -8
- package/src/core/ast.js +2 -55
- package/src/core/bounded-cache.js +121 -0
- package/src/core/chat-runtime.js +24 -10
- package/src/core/constants.js +171 -0
- package/src/core/crypto-utils.js +18 -0
- package/src/core/default-system-prompt.js +2 -2
- package/src/core/project-index.js +127 -28
- package/src/core/provider/openai-compatible.js +16 -4
- package/src/core/shell-profile.js +4 -0
- package/src/core/soul.js +3 -2
- package/src/core/tools.js +52 -72
- package/src/tui/chat-app.js +67 -14
- package/src/tui/tool-activity/presenters/command.js +14 -1
- package/src/tui/tool-activity/presenters/files.js +23 -1
- package/src/tui/tool-activity/presenters/system.js +1 -1
- package/src/tui/tool-narration/presenters/glob.js +2 -2
- package/src/tui/tool-narration/presenters/grep.js +2 -2
- package/src/tui/tool-narration/presenters/list.js +2 -2
- package/src/tui/tool-narration/presenters/run.js +2 -2
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
|
|
|
@@ -2297,7 +2312,6 @@ export async function createChatRuntime({
|
|
|
2297
2312
|
};
|
|
2298
2313
|
|
|
2299
2314
|
const submit = async (line, onAgentEvent) => {
|
|
2300
|
-
const activeBaseSystemPrompt = buildSystemPromptWithReplyLanguage(baseSystemPrompt, config);
|
|
2301
2315
|
const activeReplySystemPrompt = await buildSystemPromptWithSoul(baseSystemPrompt, config);
|
|
2302
2316
|
try {
|
|
2303
2317
|
await appendInputHistory(line);
|
|
@@ -2471,7 +2485,7 @@ export async function createChatRuntime({
|
|
|
2471
2485
|
topic,
|
|
2472
2486
|
config,
|
|
2473
2487
|
model,
|
|
2474
|
-
systemPrompt:
|
|
2488
|
+
systemPrompt: activeReplySystemPrompt
|
|
2475
2489
|
});
|
|
2476
2490
|
} catch (err) {
|
|
2477
2491
|
content = buildSpecTemplate(topic);
|
|
@@ -2498,7 +2512,7 @@ export async function createChatRuntime({
|
|
|
2498
2512
|
session: currentSession,
|
|
2499
2513
|
config,
|
|
2500
2514
|
model,
|
|
2501
|
-
systemPrompt:
|
|
2515
|
+
systemPrompt: activeReplySystemPrompt,
|
|
2502
2516
|
onAgentEvent,
|
|
2503
2517
|
sessionId: currentSession.id
|
|
2504
2518
|
});
|
|
@@ -2562,7 +2576,7 @@ export async function createChatRuntime({
|
|
|
2562
2576
|
specPath,
|
|
2563
2577
|
config,
|
|
2564
2578
|
model,
|
|
2565
|
-
systemPrompt:
|
|
2579
|
+
systemPrompt: activeReplySystemPrompt
|
|
2566
2580
|
});
|
|
2567
2581
|
} catch (err) {
|
|
2568
2582
|
planContent = buildPlanTemplate(specTitle);
|
|
@@ -2615,7 +2629,7 @@ export async function createChatRuntime({
|
|
|
2615
2629
|
parentSession: currentSession,
|
|
2616
2630
|
config,
|
|
2617
2631
|
model,
|
|
2618
|
-
systemPrompt:
|
|
2632
|
+
systemPrompt: activeReplySystemPrompt,
|
|
2619
2633
|
onAgentEvent
|
|
2620
2634
|
});
|
|
2621
2635
|
const text = `[sub-agent:${role}]\n${output.text || output}`;
|
|
@@ -2827,7 +2841,7 @@ export async function createChatRuntime({
|
|
|
2827
2841
|
session: currentSession,
|
|
2828
2842
|
config,
|
|
2829
2843
|
model,
|
|
2830
|
-
systemPrompt:
|
|
2844
|
+
systemPrompt: activeReplySystemPrompt,
|
|
2831
2845
|
onAgentEvent,
|
|
2832
2846
|
executionMode
|
|
2833
2847
|
});
|
|
@@ -2909,7 +2923,7 @@ export async function createChatRuntime({
|
|
|
2909
2923
|
session: currentSession,
|
|
2910
2924
|
config,
|
|
2911
2925
|
model,
|
|
2912
|
-
systemPrompt:
|
|
2926
|
+
systemPrompt: activeReplySystemPrompt,
|
|
2913
2927
|
onAgentEvent,
|
|
2914
2928
|
sessionId: currentSession.id
|
|
2915
2929
|
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 项目级共享常量。
|
|
3
|
+
* 所有需要在多个模块间复用的目录集、扩展名集、语言映射等统一在此定义。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ─── 工具遍历跳过的目录(glob / list / grep 等)─────────────────────
|
|
7
|
+
export const TOOL_SKIP_DIRS = new Set([
|
|
8
|
+
'.git',
|
|
9
|
+
'node_modules',
|
|
10
|
+
'.codemini',
|
|
11
|
+
'.codemini-global',
|
|
12
|
+
'dist',
|
|
13
|
+
'coverage'
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
// ─── 项目索引跳过的目录(更宽,包含构建产物和临时目录)──────────────
|
|
17
|
+
export const INDEX_SKIP_DIRS = new Set([
|
|
18
|
+
'.git',
|
|
19
|
+
'node_modules',
|
|
20
|
+
'.codemini',
|
|
21
|
+
'.codemini-project',
|
|
22
|
+
'.codemini-global',
|
|
23
|
+
'dist',
|
|
24
|
+
'coverage',
|
|
25
|
+
'sessions',
|
|
26
|
+
'tmp',
|
|
27
|
+
'temp',
|
|
28
|
+
'.cache',
|
|
29
|
+
'.turbo',
|
|
30
|
+
'.next',
|
|
31
|
+
'build',
|
|
32
|
+
'out',
|
|
33
|
+
'logs',
|
|
34
|
+
'artifacts'
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// ─── 文本扩展名 ──────────────────────────────────────────────────────
|
|
38
|
+
export const TEXT_EXTENSIONS = new Set([
|
|
39
|
+
'.js',
|
|
40
|
+
'.jsx',
|
|
41
|
+
'.ts',
|
|
42
|
+
'.tsx',
|
|
43
|
+
'.json',
|
|
44
|
+
'.md',
|
|
45
|
+
'.mjs',
|
|
46
|
+
'.cjs',
|
|
47
|
+
'.py',
|
|
48
|
+
'.rb',
|
|
49
|
+
'.go',
|
|
50
|
+
'.rs',
|
|
51
|
+
'.java',
|
|
52
|
+
'.cs',
|
|
53
|
+
'.css',
|
|
54
|
+
'.scss',
|
|
55
|
+
'.html',
|
|
56
|
+
'.yml',
|
|
57
|
+
'.yaml',
|
|
58
|
+
'.sh',
|
|
59
|
+
'.ps1',
|
|
60
|
+
'.c',
|
|
61
|
+
'.h',
|
|
62
|
+
'.cpp',
|
|
63
|
+
'.cc',
|
|
64
|
+
'.cxx',
|
|
65
|
+
'.hpp',
|
|
66
|
+
'.hh',
|
|
67
|
+
'.bash',
|
|
68
|
+
'.php'
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
// ─── 源码扩展名(用于项目索引)──────────────────────────────────────
|
|
72
|
+
export const SOURCE_EXTENSIONS = new Set([
|
|
73
|
+
'.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.py', '.go',
|
|
74
|
+
'.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hh',
|
|
75
|
+
'.sh', '.bash', '.java', '.rs', '.cs', '.php', '.rb'
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
// ─── 需要写入守卫的代码扩展名 ───────────────────────────────────────
|
|
79
|
+
export const CODE_WRITE_GUARD_EXTENSIONS = new Set([
|
|
80
|
+
'.js',
|
|
81
|
+
'.jsx',
|
|
82
|
+
'.ts',
|
|
83
|
+
'.tsx',
|
|
84
|
+
'.mjs',
|
|
85
|
+
'.cjs',
|
|
86
|
+
'.py',
|
|
87
|
+
'.rb',
|
|
88
|
+
'.go',
|
|
89
|
+
'.rs',
|
|
90
|
+
'.java',
|
|
91
|
+
'.cs',
|
|
92
|
+
'.css',
|
|
93
|
+
'.scss',
|
|
94
|
+
'.html',
|
|
95
|
+
'.sh',
|
|
96
|
+
'.ps1'
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
// ─── 扩展名 -> 语言(标准化) ────────────────────────────────────────
|
|
100
|
+
export const EXTENSION_LANGUAGE_MAP = {
|
|
101
|
+
'.js': 'js',
|
|
102
|
+
'.jsx': 'js',
|
|
103
|
+
'.mjs': 'js',
|
|
104
|
+
'.cjs': 'js',
|
|
105
|
+
'.ts': 'ts',
|
|
106
|
+
'.tsx': 'tsx',
|
|
107
|
+
'.py': 'python',
|
|
108
|
+
'.go': 'go',
|
|
109
|
+
'.c': 'c',
|
|
110
|
+
'.h': 'c',
|
|
111
|
+
'.cpp': 'cpp',
|
|
112
|
+
'.cc': 'cpp',
|
|
113
|
+
'.cxx': 'cpp',
|
|
114
|
+
'.hpp': 'cpp',
|
|
115
|
+
'.hh': 'cpp',
|
|
116
|
+
'.java': 'java',
|
|
117
|
+
'.rs': 'rust',
|
|
118
|
+
'.cs': 'csharp',
|
|
119
|
+
'.php': 'php',
|
|
120
|
+
'.rb': 'ruby',
|
|
121
|
+
'.sh': 'bash',
|
|
122
|
+
'.bash': 'bash'
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// ─── 语言别名 -> 标准语言名 ──────────────────────────────────────────
|
|
126
|
+
export const LANGUAGE_ALIASES = {
|
|
127
|
+
javascript: 'js',
|
|
128
|
+
js: 'js',
|
|
129
|
+
jsx: 'js',
|
|
130
|
+
typescript: 'ts',
|
|
131
|
+
ts: 'ts',
|
|
132
|
+
tsx: 'tsx',
|
|
133
|
+
python: 'python',
|
|
134
|
+
py: 'python',
|
|
135
|
+
go: 'go',
|
|
136
|
+
c: 'c',
|
|
137
|
+
cpp: 'cpp',
|
|
138
|
+
'c++': 'cpp',
|
|
139
|
+
bash: 'bash',
|
|
140
|
+
sh: 'bash',
|
|
141
|
+
shell: 'bash',
|
|
142
|
+
java: 'java',
|
|
143
|
+
rust: 'rust',
|
|
144
|
+
rs: 'rust',
|
|
145
|
+
csharp: 'csharp',
|
|
146
|
+
'c#': 'csharp',
|
|
147
|
+
cs: 'csharp',
|
|
148
|
+
php: 'php',
|
|
149
|
+
ruby: 'ruby',
|
|
150
|
+
rb: 'ruby'
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// ─── 工具 schema 用的语言 -> 文件类型列表 ─────────────────────────────
|
|
154
|
+
export const LANGUAGE_FILE_TYPES = {
|
|
155
|
+
js: ['js', 'jsx', 'mjs', 'cjs'],
|
|
156
|
+
ts: ['ts', 'tsx'],
|
|
157
|
+
py: ['py'],
|
|
158
|
+
python: ['py'],
|
|
159
|
+
md: ['md'],
|
|
160
|
+
json: ['json'],
|
|
161
|
+
css: ['css', 'scss'],
|
|
162
|
+
html: ['html'],
|
|
163
|
+
java: ['java'],
|
|
164
|
+
csharp: ['cs'],
|
|
165
|
+
cs: ['cs'],
|
|
166
|
+
go: ['go'],
|
|
167
|
+
rust: ['rs'],
|
|
168
|
+
ruby: ['rb'],
|
|
169
|
+
shell: ['sh', 'ps1'],
|
|
170
|
+
yaml: ['yml', 'yaml']
|
|
171
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 共享加密工具函数。
|
|
3
|
+
* 统一 sha1 / sha256 的实现,避免在各模块中重复定义。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
|
|
8
|
+
export function sha1(input) {
|
|
9
|
+
return crypto.createHash('sha1').update(String(input || '')).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function sha256(input) {
|
|
13
|
+
return crypto.createHash('sha256').update(String(input || '')).digest('hex');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function sha256Prefixed(input) {
|
|
17
|
+
return `sha256:${sha256(input)}`;
|
|
18
|
+
}
|
|
@@ -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
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import crypto from 'node:crypto';
|
|
4
3
|
import { getFileIndexPath, getProjectIndexDir, getProjectMapPath, getProjectWorkspaceDir } from './paths.js';
|
|
4
|
+
import { INDEX_SKIP_DIRS as SKIP_DIRS, SOURCE_EXTENSIONS, EXTENSION_LANGUAGE_MAP } from './constants.js';
|
|
5
|
+
import { sha1 } from './crypto-utils.js';
|
|
6
|
+
import { BoundedCache } from './bounded-cache.js';
|
|
5
7
|
|
|
6
|
-
const SKIP_DIRS = new Set(['.git', 'node_modules', '.codemini', '.codemini-project', '.codemini-global', 'dist', 'coverage']);
|
|
7
8
|
const PROJECT_MARKER_FILES = new Set([
|
|
8
9
|
'package.json',
|
|
9
10
|
'tsconfig.json',
|
|
@@ -19,22 +20,11 @@ const PROJECT_MARKER_FILES = new Set([
|
|
|
19
20
|
'Makefile',
|
|
20
21
|
'.gitignore'
|
|
21
22
|
]);
|
|
22
|
-
const SOURCE_EXTENSIONS = new Set([
|
|
23
|
-
'.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.py', '.go', '.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hh',
|
|
24
|
-
'.sh', '.bash', '.java', '.rs', '.cs', '.php', '.rb'
|
|
25
|
-
]);
|
|
26
|
-
const LANGUAGE_BY_EXT = {
|
|
27
|
-
'.js': 'js', '.jsx': 'jsx', '.mjs': 'js', '.cjs': 'js', '.ts': 'ts', '.tsx': 'tsx', '.py': 'python',
|
|
28
|
-
'.go': 'go', '.c': 'c', '.h': 'c', '.cpp': 'cpp', '.cc': 'cpp', '.cxx': 'cpp', '.hpp': 'cpp', '.hh': 'cpp',
|
|
29
|
-
'.sh': 'bash', '.bash': 'bash', '.java': 'java', '.rs': 'rust', '.cs': 'csharp', '.php': 'php', '.rb': 'ruby'
|
|
30
|
-
};
|
|
31
23
|
|
|
32
|
-
const
|
|
33
|
-
const PROJECT_CONTEXT_MAX_FILES = 6;
|
|
24
|
+
const LANGUAGE_BY_EXT = EXTENSION_LANGUAGE_MAP;
|
|
34
25
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
26
|
+
const initCache = new BoundedCache({ maxSize: 32, ttlMs: 10 * 60 * 1000 });
|
|
27
|
+
const PROJECT_CONTEXT_MAX_FILES = 6;
|
|
38
28
|
|
|
39
29
|
function clipList(values, max = 32) {
|
|
40
30
|
return [...new Set((Array.isArray(values) ? values : []).filter(Boolean))].slice(0, max);
|
|
@@ -115,9 +105,9 @@ function gitignorePatternToRegex(pattern) {
|
|
|
115
105
|
return new RegExp(`^${regexBody}$`);
|
|
116
106
|
}
|
|
117
107
|
|
|
118
|
-
async function
|
|
108
|
+
async function readIgnoreFileRules(cwd, fileName) {
|
|
119
109
|
try {
|
|
120
|
-
const raw = await fs.readFile(path.join(cwd,
|
|
110
|
+
const raw = await fs.readFile(path.join(cwd, fileName), 'utf8');
|
|
121
111
|
return raw
|
|
122
112
|
.split(/\r?\n/)
|
|
123
113
|
.map((line) => line.trim())
|
|
@@ -143,6 +133,18 @@ async function readGitignoreRules(cwd) {
|
|
|
143
133
|
}
|
|
144
134
|
}
|
|
145
135
|
|
|
136
|
+
async function readProjectIgnoreRules(cwd) {
|
|
137
|
+
const [gitignoreRules, llmignoreRules] = await Promise.all([
|
|
138
|
+
readIgnoreFileRules(cwd, '.gitignore'),
|
|
139
|
+
readIgnoreFileRules(cwd, '.llmignore')
|
|
140
|
+
]);
|
|
141
|
+
return {
|
|
142
|
+
gitignoreRules,
|
|
143
|
+
llmignoreRules,
|
|
144
|
+
combinedRules: [...gitignoreRules, ...llmignoreRules]
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
146
148
|
function matchesGitignoreRule(rule, relativePath, isDirectory) {
|
|
147
149
|
if (!rule || !relativePath) return false;
|
|
148
150
|
if (rule.dirOnly && !isDirectory) return false;
|
|
@@ -212,17 +214,17 @@ async function findNearestIndexedProjectRoot(startDir, workspaceRoot) {
|
|
|
212
214
|
return null;
|
|
213
215
|
}
|
|
214
216
|
|
|
215
|
-
async function walkFiles(cwd, start = cwd, out = [],
|
|
217
|
+
async function walkFiles(cwd, start = cwd, out = [], ignoreRules = []) {
|
|
216
218
|
const entries = await fs.readdir(start, { withFileTypes: true });
|
|
217
219
|
for (const entry of entries) {
|
|
218
220
|
const absolutePath = path.join(start, entry.name);
|
|
219
221
|
const relativePath = rel(cwd, absolutePath);
|
|
220
222
|
if (entry.isDirectory()) {
|
|
221
|
-
if (shouldIgnorePath(relativePath, true,
|
|
222
|
-
await walkFiles(cwd, absolutePath, out,
|
|
223
|
+
if (shouldIgnorePath(relativePath, true, ignoreRules)) continue;
|
|
224
|
+
await walkFiles(cwd, absolutePath, out, ignoreRules);
|
|
223
225
|
continue;
|
|
224
226
|
}
|
|
225
|
-
if (shouldIgnorePath(relativePath, false,
|
|
227
|
+
if (shouldIgnorePath(relativePath, false, ignoreRules)) continue;
|
|
226
228
|
out.push(absolutePath);
|
|
227
229
|
}
|
|
228
230
|
return out;
|
|
@@ -298,12 +300,12 @@ async function scanProject(cwd) {
|
|
|
298
300
|
workspaceKind,
|
|
299
301
|
projectMap: null,
|
|
300
302
|
fileIndex: null,
|
|
301
|
-
|
|
303
|
+
ignoreRules: []
|
|
302
304
|
};
|
|
303
305
|
}
|
|
304
306
|
|
|
305
|
-
const gitignoreRules = await
|
|
306
|
-
const allFiles = await walkFiles(cwd, cwd, [],
|
|
307
|
+
const { gitignoreRules, llmignoreRules, combinedRules } = await readProjectIgnoreRules(cwd);
|
|
308
|
+
const allFiles = await walkFiles(cwd, cwd, [], combinedRules);
|
|
307
309
|
const relativeFiles = allFiles.map((filePath) => rel(cwd, filePath));
|
|
308
310
|
const sourceFiles = allFiles.filter((filePath) => SOURCE_EXTENSIONS.has(path.extname(filePath).toLowerCase()));
|
|
309
311
|
|
|
@@ -362,13 +364,14 @@ async function scanProject(cwd) {
|
|
|
362
364
|
frameworkHints,
|
|
363
365
|
directories,
|
|
364
366
|
gitignoreEnabled: gitignoreRules.length > 0,
|
|
367
|
+
llmignoreEnabled: llmignoreRules.length > 0,
|
|
365
368
|
updatedAt: new Date().toISOString()
|
|
366
369
|
},
|
|
367
370
|
fileIndex: {
|
|
368
371
|
updatedAt: new Date().toISOString(),
|
|
369
372
|
files
|
|
370
373
|
},
|
|
371
|
-
|
|
374
|
+
ignoreRules: combinedRules
|
|
372
375
|
};
|
|
373
376
|
}
|
|
374
377
|
|
|
@@ -417,7 +420,7 @@ export async function refreshIndexedFile(cwd = process.cwd(), relativePath = '')
|
|
|
417
420
|
const projectRoot = await findProjectRootFromFile(cwd, relativePath);
|
|
418
421
|
if (!projectRoot) return null;
|
|
419
422
|
const fileIndexPath = getFileIndexPath(projectRoot);
|
|
420
|
-
const
|
|
423
|
+
const { combinedRules } = await readProjectIgnoreRules(projectRoot);
|
|
421
424
|
const absolutePath = path.join(cwd, relativePath);
|
|
422
425
|
const stat = await safeStat(absolutePath);
|
|
423
426
|
let action = 'updated';
|
|
@@ -426,7 +429,7 @@ export async function refreshIndexedFile(cwd = process.cwd(), relativePath = '')
|
|
|
426
429
|
const files = Array.isArray(current.files) ? [...current.files] : [];
|
|
427
430
|
const index = files.findIndex((entry) => entry.file === projectRelativePath);
|
|
428
431
|
|
|
429
|
-
if (shouldIgnorePath(projectRelativePath, Boolean(stat?.isDirectory?.()),
|
|
432
|
+
if (shouldIgnorePath(projectRelativePath, Boolean(stat?.isDirectory?.()), combinedRules)) {
|
|
430
433
|
if (index >= 0) files.splice(index, 1);
|
|
431
434
|
action = 'removed';
|
|
432
435
|
} else if (!stat || !stat.isFile()) {
|
|
@@ -508,3 +511,99 @@ export async function buildProjectContextSnippet(cwd = process.cwd(), userText =
|
|
|
508
511
|
const snippet = trimMultiline(lines.join('\n'));
|
|
509
512
|
return snippet;
|
|
510
513
|
}
|
|
514
|
+
|
|
515
|
+
export async function queryProjectIndex(cwd = process.cwd(), args = {}) {
|
|
516
|
+
const indexedRoot = await findNearestIndexedProjectRoot(cwd, cwd);
|
|
517
|
+
if (!indexedRoot) {
|
|
518
|
+
return {
|
|
519
|
+
query: String(args?.query || '').trim(),
|
|
520
|
+
project_root: '',
|
|
521
|
+
project_map: null,
|
|
522
|
+
matches: []
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const projectMap = await safeReadJson(getProjectMapPath(indexedRoot), null);
|
|
527
|
+
const fileIndex = await safeReadJson(getFileIndexPath(indexedRoot), null);
|
|
528
|
+
const query = String(args?.query || '').trim();
|
|
529
|
+
const pathPrefix = normalizeRelativePath(args?.path || args?.path_prefix || '');
|
|
530
|
+
const languageFilter = String(args?.language || '').trim().toLowerCase();
|
|
531
|
+
const maxResults = Math.max(1, Math.min(20, Number(args?.max_results || 8) || 8));
|
|
532
|
+
const files = Array.isArray(fileIndex?.files) ? fileIndex.files : [];
|
|
533
|
+
const tokens = tokenizeQuery(query);
|
|
534
|
+
|
|
535
|
+
const matches = [];
|
|
536
|
+
for (const entry of files) {
|
|
537
|
+
const relativePath = String(entry?.file || '');
|
|
538
|
+
if (!relativePath) continue;
|
|
539
|
+
if (pathPrefix && !relativePath.startsWith(pathPrefix)) continue;
|
|
540
|
+
if (languageFilter && String(entry?.language || '').toLowerCase() !== languageFilter) continue;
|
|
541
|
+
|
|
542
|
+
let score = 0;
|
|
543
|
+
const reasons = [];
|
|
544
|
+
const fileText = relativePath.toLowerCase();
|
|
545
|
+
for (const token of tokens) {
|
|
546
|
+
if (!token) continue;
|
|
547
|
+
if (fileText.includes(token)) {
|
|
548
|
+
score += 5;
|
|
549
|
+
reasons.push(`path:${token}`);
|
|
550
|
+
}
|
|
551
|
+
if ((entry.exports || []).some((value) => String(value).toLowerCase() === token)) {
|
|
552
|
+
score += 4;
|
|
553
|
+
reasons.push(`export:${token}`);
|
|
554
|
+
}
|
|
555
|
+
if ((entry.functions || []).some((value) => String(value).toLowerCase().includes(token))) {
|
|
556
|
+
score += 4;
|
|
557
|
+
reasons.push(`function:${token}`);
|
|
558
|
+
}
|
|
559
|
+
if ((entry.classes || []).some((value) => String(value).toLowerCase().includes(token))) {
|
|
560
|
+
score += 4;
|
|
561
|
+
reasons.push(`class:${token}`);
|
|
562
|
+
}
|
|
563
|
+
if ((entry.imports || []).some((value) => String(value).toLowerCase().includes(token))) {
|
|
564
|
+
score += 2;
|
|
565
|
+
reasons.push(`import:${token}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (!query) {
|
|
570
|
+
if ((projectMap?.entryCandidates || []).includes(relativePath)) score += 3;
|
|
571
|
+
if ((projectMap?.importantFiles || []).includes(relativePath)) score += 2;
|
|
572
|
+
if (String(relativePath).startsWith('src/')) score += 1;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (score <= 0 && query) continue;
|
|
576
|
+
matches.push({
|
|
577
|
+
file: relativePath,
|
|
578
|
+
language: entry.language || 'text',
|
|
579
|
+
score,
|
|
580
|
+
reasons: clipList(reasons, 8),
|
|
581
|
+
exports: clipList(entry.exports || [], 6),
|
|
582
|
+
functions: clipList(entry.functions || [], 6),
|
|
583
|
+
classes: clipList(entry.classes || [], 6),
|
|
584
|
+
imports: clipList(entry.imports || [], 6)
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
matches.sort((left, right) => right.score - left.score || String(left.file).localeCompare(String(right.file)));
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
query,
|
|
592
|
+
project_root: indexedRoot,
|
|
593
|
+
project_map: projectMap
|
|
594
|
+
? {
|
|
595
|
+
workspace_kind: projectMap.workspaceKind || 'project',
|
|
596
|
+
languages: clipList(projectMap.languages || [], 8),
|
|
597
|
+
package_managers: clipList(projectMap.packageManagers || [], 8),
|
|
598
|
+
important_files: clipList(projectMap.importantFiles || [], 8),
|
|
599
|
+
source_roots: clipList(projectMap.sourceRoots || [], 8),
|
|
600
|
+
test_roots: clipList(projectMap.testRoots || [], 8),
|
|
601
|
+
entry_candidates: clipList(projectMap.entryCandidates || [], 8),
|
|
602
|
+
framework_hints: clipList(projectMap.frameworkHints || [], 8),
|
|
603
|
+
gitignore_enabled: Boolean(projectMap.gitignoreEnabled),
|
|
604
|
+
llmignore_enabled: Boolean(projectMap.llmignoreEnabled)
|
|
605
|
+
}
|
|
606
|
+
: null,
|
|
607
|
+
matches: matches.slice(0, maxResults)
|
|
608
|
+
};
|
|
609
|
+
}
|
|
@@ -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
|
package/src/core/soul.js
CHANGED
|
@@ -51,7 +51,8 @@ export async function buildSystemPromptWithSoul(baseSystemPrompt, config = {}) {
|
|
|
51
51
|
const guard = [
|
|
52
52
|
'[Soul guard]',
|
|
53
53
|
'Apply this soul to response tone only.',
|
|
54
|
-
'Response tone only: do not change plans, code, tests, file formats, or technical decisions.'
|
|
54
|
+
'Response tone only: do not change plans, code, tests, file formats, or technical decisions.',
|
|
55
|
+
'This tone directive has HIGH priority. Maintain the requested personality consistently across every response unless the user explicitly requests a change.'
|
|
55
56
|
].join('\n');
|
|
56
|
-
return `${
|
|
57
|
+
return `${soulPrompt}\n\n${guard}\n\n${String(promptWithReplyLanguage || '').trim()}`.trim();
|
|
57
58
|
}
|