codemini-cli 0.3.1 → 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.
- package/package.json +2 -1
- package/src/commands/chat.js +1 -0
- package/src/commands/run.js +6 -2
- package/src/core/agent-loop.js +1 -1
- package/src/core/chat-runtime.js +546 -136
- package/src/core/config-store.js +30 -0
- package/src/core/memory-policy.js +27 -0
- package/src/core/memory-prompt.js +45 -0
- package/src/core/memory-store.js +181 -0
- package/src/core/paths.js +8 -0
- package/src/core/provider/anthropic.js +388 -0
- package/src/core/provider/index.js +37 -0
- package/src/core/tools.js +173 -0
- package/src/tui/chat-app.js +224 -24
package/src/core/config-store.js
CHANGED
|
@@ -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,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
|
+
}
|