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.
@@ -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
+ }