codemini-cli 0.3.0 → 0.3.2

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.
@@ -13,6 +13,9 @@ function normalizeUiLanguage(value) {
13
13
  }
14
14
 
15
15
  const DEFAULT_CONFIG = {
16
+ sdk: {
17
+ provider: 'openai-compatible'
18
+ },
16
19
  gateway: {
17
20
  base_url: 'http://127.0.0.1:8000/v1',
18
21
  api_key: '',
@@ -63,6 +66,17 @@ const DEFAULT_CONFIG = {
63
66
  language: 'zh',
64
67
  reply_language: 'zh'
65
68
  },
69
+ memory: {
70
+ enabled: true,
71
+ auto_write: true,
72
+ inject_on_session_start: true,
73
+ max_items_per_scope: 12,
74
+ max_prompt_chars: 4000,
75
+ max_user_chars: 1375,
76
+ max_global_chars: 2200,
77
+ max_project_chars: 2200,
78
+ project_binding: 'path-or-alias'
79
+ },
66
80
  soul: {
67
81
  preset: 'default',
68
82
  custom_path: ''
@@ -117,6 +131,10 @@ function uniqueStrings(items = []) {
117
131
 
118
132
  function normalizePolicyLists(config) {
119
133
  const next = structuredClone(config);
134
+ next.sdk = next.sdk || {};
135
+ next.sdk.provider = ['openai-compatible', 'anthropic'].includes(String(next.sdk.provider || '').toLowerCase())
136
+ ? String(next.sdk.provider).toLowerCase()
137
+ : 'openai-compatible';
120
138
  next.shell = next.shell || {};
121
139
  next.shell.default = normalizeShellName(next.shell.default);
122
140
  next.execution = next.execution || {};
@@ -147,6 +165,18 @@ function normalizePolicyLists(config) {
147
165
  next.ui = next.ui || {};
148
166
  next.ui.language = normalizeUiLanguage(next.ui.language);
149
167
  next.ui.reply_language = normalizeReplyLanguage(next.ui.reply_language);
168
+ next.memory = next.memory || {};
169
+ next.memory.enabled = next.memory.enabled !== false;
170
+ next.memory.auto_write = next.memory.auto_write !== false;
171
+ next.memory.inject_on_session_start = next.memory.inject_on_session_start !== false;
172
+ next.memory.max_items_per_scope = Math.max(1, Number(next.memory.max_items_per_scope || 12));
173
+ next.memory.max_prompt_chars = Math.max(200, Number(next.memory.max_prompt_chars || 4000));
174
+ next.memory.max_user_chars = Math.max(80, Number(next.memory.max_user_chars || 1375));
175
+ next.memory.max_global_chars = Math.max(80, Number(next.memory.max_global_chars || 2200));
176
+ next.memory.max_project_chars = Math.max(80, Number(next.memory.max_project_chars || 2200));
177
+ next.memory.project_binding = ['path', 'alias', 'path-or-alias'].includes(String(next.memory.project_binding || ''))
178
+ ? String(next.memory.project_binding)
179
+ : 'path-or-alias';
150
180
  next.policy = next.policy || {};
151
181
  next.policy.command_allowlist = uniqueStrings(
152
182
  Array.isArray(next.policy.command_allowlist) ? next.policy.command_allowlist : []
@@ -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
+ }
@@ -0,0 +1,27 @@
1
+ const SECRET_PATTERNS = [
2
+ /\b(api[_-]?key|token|secret|password|passwd|bearer)\b/i,
3
+ /\bsk-[a-z0-9]{8,}\b/i,
4
+ /-----BEGIN [A-Z ]+PRIVATE KEY-----/i
5
+ ];
6
+
7
+ export function normalizeMemoryText(value) {
8
+ return String(value || '').replace(/\s+/g, ' ').trim();
9
+ }
10
+
11
+ export function isSensitiveMemoryContent(value) {
12
+ const text = normalizeMemoryText(value);
13
+ if (!text) return false;
14
+ return SECRET_PATTERNS.some((pattern) => pattern.test(text));
15
+ }
16
+
17
+ export function assertSafeMemoryContent(value) {
18
+ if (isSensitiveMemoryContent(value)) {
19
+ throw new Error('Refusing to store sensitive or secret-like memory content');
20
+ }
21
+ }
22
+
23
+ export function summarizeMemoryContent(value, maxChars = 72) {
24
+ const text = normalizeMemoryText(value);
25
+ if (text.length <= maxChars) return text;
26
+ return `${text.slice(0, Math.max(0, maxChars - 3))}...`;
27
+ }
@@ -0,0 +1,45 @@
1
+ import { listMemories } from './memory-store.js';
2
+
3
+ function renderScope(title, items = []) {
4
+ if (!Array.isArray(items) || items.length === 0) return '';
5
+ const lines = items.map((item) =>
6
+ [
7
+ `- [${item.kind}] summary=${JSON.stringify(String(item.summary || item.content || ''))}`,
8
+ ` exact_text=${JSON.stringify(String(item.content || ''))}`
9
+ ].join('\n')
10
+ );
11
+ return `${title}\n${lines.join('\n')}`;
12
+ }
13
+
14
+ export async function buildMemorySnapshot({
15
+ config = {},
16
+ workspaceRoot = process.cwd()
17
+ }) {
18
+ if (config?.memory?.enabled === false || config?.memory?.inject_on_session_start === false) return '';
19
+
20
+ const [user, globalItems, project] = await Promise.all([
21
+ listMemories({ scope: 'user', workspaceRoot }),
22
+ listMemories({ scope: 'global', workspaceRoot }),
23
+ listMemories({ scope: 'project', workspaceRoot })
24
+ ]);
25
+
26
+ const maxItems = Math.max(1, Number(config?.memory?.max_items_per_scope || 12));
27
+ const sections = [
28
+ renderScope('User Memory:', user.slice(0, maxItems)),
29
+ renderScope('Global Memory:', globalItems.slice(0, maxItems)),
30
+ renderScope('Project Memory:', project.slice(0, maxItems))
31
+ ].filter(Boolean);
32
+
33
+ if (sections.length === 0) return '';
34
+
35
+ const snapshot = [
36
+ 'Persistent Memory:',
37
+ 'Use these durable notes only as stable guidance. Prefer fresh reads when code or files can verify the answer.',
38
+ 'When recalling memory, preserve command names, file paths, identifiers, and punctuation exactly. Do not rewrite exact_text values.',
39
+ ...sections
40
+ ].join('\n\n');
41
+
42
+ const maxChars = Math.max(200, Number(config?.memory?.max_prompt_chars || 4000));
43
+ if (snapshot.length <= maxChars) return snapshot;
44
+ return `${snapshot.slice(0, maxChars - 3)}...`;
45
+ }
@@ -0,0 +1,181 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { sha1 } from './crypto-utils.js';
4
+ import { getMemoryDir, getProjectMemoryDir } from './paths.js';
5
+ import { assertSafeMemoryContent, normalizeMemoryText, summarizeMemoryContent } from './memory-policy.js';
6
+
7
+ const ALLOWED_SCOPES = new Set(['user', 'global', 'project']);
8
+
9
+ function nowIso() {
10
+ return new Date().toISOString();
11
+ }
12
+
13
+ function slugify(value) {
14
+ const text = String(value || '')
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
17
+ .replace(/^-+|-+$/g, '');
18
+ return text || 'project';
19
+ }
20
+
21
+ export function getProjectMemoryKey(workspaceRoot = process.cwd(), projectAlias = '') {
22
+ const alias = normalizeMemoryText(projectAlias);
23
+ if (alias) return slugify(alias);
24
+ const root = path.resolve(workspaceRoot || process.cwd());
25
+ const base = path.basename(root);
26
+ return `${slugify(base)}-${sha1(root).slice(0, 10)}`;
27
+ }
28
+
29
+ function ensureScope(scope) {
30
+ const value = String(scope || '').trim().toLowerCase();
31
+ if (!ALLOWED_SCOPES.has(value)) {
32
+ throw new Error(`Unsupported memory scope: ${scope}`);
33
+ }
34
+ return value;
35
+ }
36
+
37
+ async function ensureParent(filePath) {
38
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
39
+ }
40
+
41
+ function buildFilePath(scope, workspaceRoot = process.cwd(), projectAlias = '') {
42
+ if (scope === 'user') return path.join(getMemoryDir(), 'user.json');
43
+ if (scope === 'global') return path.join(getMemoryDir(), 'global.json');
44
+ return path.join(getProjectMemoryDir(workspaceRoot), `${getProjectMemoryKey(workspaceRoot, projectAlias)}.json`);
45
+ }
46
+
47
+ async function readMemoryBucket(filePath) {
48
+ try {
49
+ const raw = await fs.readFile(filePath, 'utf8');
50
+ const parsed = JSON.parse(raw);
51
+ return Array.isArray(parsed?.items) ? parsed.items : [];
52
+ } catch {
53
+ return [];
54
+ }
55
+ }
56
+
57
+ async function writeMemoryBucket(filePath, items) {
58
+ await ensureParent(filePath);
59
+ await fs.writeFile(filePath, `${JSON.stringify({ items }, null, 2)}\n`, 'utf8');
60
+ }
61
+
62
+ function normalizeMemoryItem(item, scope, projectKey = '') {
63
+ const now = nowIso();
64
+ const content = normalizeMemoryText(item?.content || '');
65
+ return {
66
+ id: String(item?.id || `mem_${sha1(`${scope}:${projectKey}:${content}:${now}:${Math.random()}`).slice(0, 12)}`),
67
+ scope,
68
+ projectKey: projectKey || undefined,
69
+ kind: String(item?.kind || 'note').trim() || 'note',
70
+ content,
71
+ summary: normalizeMemoryText(item?.summary || summarizeMemoryContent(content)),
72
+ source: String(item?.source || 'tool').trim() || 'tool',
73
+ confidence: Number.isFinite(Number(item?.confidence)) ? Number(item.confidence) : 0.9,
74
+ createdAt: String(item?.createdAt || now),
75
+ updatedAt: String(item?.updatedAt || now),
76
+ hits: Number.isFinite(Number(item?.hits)) ? Number(item.hits) : 0,
77
+ pinned: item?.pinned === true
78
+ };
79
+ }
80
+
81
+ function sameMemory(left, right) {
82
+ const a = normalizeMemoryText(left?.content);
83
+ const b = normalizeMemoryText(right?.content);
84
+ if (!a || !b) return false;
85
+ return a === b;
86
+ }
87
+
88
+ function measureMemoryChars(item) {
89
+ return normalizeMemoryText(item?.content).length + normalizeMemoryText(item?.summary).length;
90
+ }
91
+
92
+ function budgetForScope(scope, config = {}) {
93
+ if (scope === 'user') return Math.max(80, Number(config?.memory?.max_user_chars || 1375));
94
+ if (scope === 'global') return Math.max(80, Number(config?.memory?.max_global_chars || 2200));
95
+ return Math.max(80, Number(config?.memory?.max_project_chars || 2200));
96
+ }
97
+
98
+ export async function listMemories({ scope, workspaceRoot = process.cwd(), projectAlias = '' }) {
99
+ const normalizedScope = ensureScope(scope);
100
+ const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
101
+ const projectKey = normalizedScope === 'project' ? getProjectMemoryKey(workspaceRoot, projectAlias) : '';
102
+ const items = await readMemoryBucket(filePath);
103
+ return items
104
+ .map((item) => normalizeMemoryItem(item, normalizedScope, projectKey))
105
+ .sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)));
106
+ }
107
+
108
+ export async function rememberMemory({
109
+ scope,
110
+ content,
111
+ kind = 'note',
112
+ summary = '',
113
+ source = 'tool',
114
+ confidence = 0.9,
115
+ replaceSimilar = true,
116
+ pinned = false,
117
+ workspaceRoot = process.cwd(),
118
+ projectAlias = '',
119
+ config = {}
120
+ }) {
121
+ const normalizedScope = ensureScope(scope);
122
+ const normalizedContent = normalizeMemoryText(content);
123
+ if (!normalizedContent) throw new Error('Memory content is required');
124
+ assertSafeMemoryContent(normalizedContent);
125
+
126
+ const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
127
+ const projectKey = normalizedScope === 'project' ? getProjectMemoryKey(workspaceRoot, projectAlias) : '';
128
+ const existing = (await readMemoryBucket(filePath)).map((item) => normalizeMemoryItem(item, normalizedScope, projectKey));
129
+ const probe = normalizeMemoryItem({ content: normalizedContent, kind, summary, source, confidence, pinned }, normalizedScope, projectKey);
130
+
131
+ const replaceIndex = replaceSimilar ? existing.findIndex((item) => sameMemory(item, probe)) : -1;
132
+ let saved;
133
+ if (replaceIndex >= 0) {
134
+ saved = {
135
+ ...existing[replaceIndex],
136
+ ...probe,
137
+ id: existing[replaceIndex].id,
138
+ createdAt: existing[replaceIndex].createdAt,
139
+ updatedAt: nowIso()
140
+ };
141
+ existing.splice(replaceIndex, 1, saved);
142
+ } else {
143
+ saved = probe;
144
+ existing.unshift(saved);
145
+ }
146
+
147
+ const maxItems = Math.max(1, Number(config?.memory?.max_items_per_scope || 12));
148
+ const maxChars = budgetForScope(normalizedScope, config);
149
+ const deduped = [];
150
+ const seen = new Set();
151
+ for (const item of existing) {
152
+ const key = `${item.kind}:${normalizeMemoryText(item.content)}`;
153
+ if (seen.has(key)) continue;
154
+ seen.add(key);
155
+ deduped.push(item);
156
+ if (deduped.length >= maxItems) break;
157
+ }
158
+ let totalChars = deduped.reduce((sum, item) => sum + measureMemoryChars(item), 0);
159
+ while (deduped.length > 1 && totalChars > maxChars) {
160
+ const removed = deduped.pop();
161
+ totalChars -= measureMemoryChars(removed);
162
+ }
163
+ await writeMemoryBucket(filePath, deduped);
164
+ return saved;
165
+ }
166
+
167
+ export async function forgetMemory({ scope, id, workspaceRoot = process.cwd(), projectAlias = '' }) {
168
+ const normalizedScope = ensureScope(scope);
169
+ const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
170
+ const existing = await listMemories({ scope: normalizedScope, workspaceRoot, projectAlias });
171
+ const kept = existing.filter((item) => item.id !== id);
172
+ await writeMemoryBucket(filePath, kept);
173
+ return { removed: existing.length - kept.length };
174
+ }
175
+
176
+ export async function searchMemories({ scope, query, workspaceRoot = process.cwd(), projectAlias = '' }) {
177
+ const items = await listMemories({ scope, workspaceRoot, projectAlias });
178
+ const needle = normalizeMemoryText(query).toLowerCase();
179
+ if (!needle) return items;
180
+ return items.filter((item) => item.content.toLowerCase().includes(needle) || item.summary.toLowerCase().includes(needle));
181
+ }
package/src/core/paths.js CHANGED
@@ -50,6 +50,10 @@ export function getCommandsDir() {
50
50
  return path.join(getBaseConfigDir(), 'commands');
51
51
  }
52
52
 
53
+ export function getMemoryDir() {
54
+ return path.join(getBaseConfigDir(), 'memory');
55
+ }
56
+
53
57
  export function getInputHistoryFilePath() {
54
58
  return path.join(getBaseConfigDir(), 'input-history.json');
55
59
  }
@@ -101,3 +105,7 @@ export function getFileIndexPath(cwd = process.cwd()) {
101
105
  export function getProjectIndexDir(cwd = process.cwd()) {
102
106
  return path.join(cwd, PROJECT_INDEX_DIR);
103
107
  }
108
+
109
+ export function getProjectMemoryDir(cwd = process.cwd()) {
110
+ return path.join(getProjectIndexDir(cwd), 'memory');
111
+ }
@@ -1,27 +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([
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
- ]);
25
8
  const PROJECT_MARKER_FILES = new Set([
26
9
  'package.json',
27
10
  'tsconfig.json',
@@ -37,22 +20,11 @@ const PROJECT_MARKER_FILES = new Set([
37
20
  'Makefile',
38
21
  '.gitignore'
39
22
  ]);
40
- const SOURCE_EXTENSIONS = new Set([
41
- '.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.py', '.go', '.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hh',
42
- '.sh', '.bash', '.java', '.rs', '.cs', '.php', '.rb'
43
- ]);
44
- const LANGUAGE_BY_EXT = {
45
- '.js': 'js', '.jsx': 'jsx', '.mjs': 'js', '.cjs': 'js', '.ts': 'ts', '.tsx': 'tsx', '.py': 'python',
46
- '.go': 'go', '.c': 'c', '.h': 'c', '.cpp': 'cpp', '.cc': 'cpp', '.cxx': 'cpp', '.hpp': 'cpp', '.hh': 'cpp',
47
- '.sh': 'bash', '.bash': 'bash', '.java': 'java', '.rs': 'rust', '.cs': 'csharp', '.php': 'php', '.rb': 'ruby'
48
- };
49
23
 
50
- const initCache = new Map();
51
- const PROJECT_CONTEXT_MAX_FILES = 6;
24
+ const LANGUAGE_BY_EXT = EXTENSION_LANGUAGE_MAP;
52
25
 
53
- function sha1(input) {
54
- return crypto.createHash('sha1').update(String(input || '')).digest('hex');
55
- }
26
+ const initCache = new BoundedCache({ maxSize: 32, ttlMs: 10 * 60 * 1000 });
27
+ const PROJECT_CONTEXT_MAX_FILES = 6;
56
28
 
57
29
  function clipList(values, max = 32) {
58
30
  return [...new Set((Array.isArray(values) ? values : []).filter(Boolean))].slice(0, max);