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.
@@ -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. Prefer tools for fresh verification before assuming details.`
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
- if (onAgentEvent) onAgentEvent({ type: 'assistant:start' });
1590
- return createChatCompletionStream({
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: activeBaseSystemPrompt
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: activeBaseSystemPrompt,
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: activeBaseSystemPrompt
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: activeBaseSystemPrompt,
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: activeBaseSystemPrompt,
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: activeBaseSystemPrompt,
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 locate the relevant files
18
- Tool: glob({"pattern":"src/**/*auth*.ts"})
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 initCache = new Map();
33
- const PROJECT_CONTEXT_MAX_FILES = 6;
24
+ const LANGUAGE_BY_EXT = EXTENSION_LANGUAGE_MAP;
34
25
 
35
- function sha1(input) {
36
- return crypto.createHash('sha1').update(String(input || '')).digest('hex');
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 readGitignoreRules(cwd) {
108
+ async function readIgnoreFileRules(cwd, fileName) {
119
109
  try {
120
- const raw = await fs.readFile(path.join(cwd, '.gitignore'), 'utf8');
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 = [], gitignoreRules = []) {
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, gitignoreRules)) continue;
222
- await walkFiles(cwd, absolutePath, out, gitignoreRules);
223
+ if (shouldIgnorePath(relativePath, true, ignoreRules)) continue;
224
+ await walkFiles(cwd, absolutePath, out, ignoreRules);
223
225
  continue;
224
226
  }
225
- if (shouldIgnorePath(relativePath, false, gitignoreRules)) continue;
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
- gitignoreRules: []
303
+ ignoreRules: []
302
304
  };
303
305
  }
304
306
 
305
- const gitignoreRules = await readGitignoreRules(cwd);
306
- const allFiles = await walkFiles(cwd, cwd, [], gitignoreRules);
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
- gitignoreRules
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 gitignoreRules = await readGitignoreRules(projectRoot);
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?.()), gitignoreRules)) {
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 (!text && toolCalls.length === 0) {
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 (!text && toolCalls.length === 0) {
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 `${String(promptWithReplyLanguage || '').trim()}\n\n${guard}\n\n${soulPrompt}`.trim();
57
+ return `${soulPrompt}\n\n${guard}\n\n${String(promptWithReplyLanguage || '').trim()}`.trim();
57
58
  }