codemini-cli 0.4.0 → 0.4.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/OPERATIONS.md +4 -2
- package/README.md +89 -11
- package/deployment.md +14 -7
- package/package.json +1 -2
- package/src/cli.js +1 -1
- package/src/commands/skill.js +145 -53
- package/src/core/agent-loop.js +18 -311
- package/src/core/chat-runtime.js +389 -53
- package/src/core/command-loader.js +12 -5
- package/src/core/config-store.js +2 -0
- package/src/core/context-compact.js +34 -9
- package/src/core/default-system-prompt.js +5 -5
- package/src/core/dream-audit.js +12 -0
- package/src/core/dream-consolidate.js +131 -59
- package/src/core/dream-evaluator.js +86 -0
- package/src/core/fff-adapter.js +1 -1
- package/src/core/memory-store.js +145 -10
- package/src/core/provider/openai-compatible.js +40 -5
- package/src/core/reflect-skill.js +178 -0
- package/src/core/shell-profile.js +8 -8
- package/src/core/tool-args.js +181 -0
- package/src/core/tool-result-store.js +206 -0
- package/src/core/tools.js +144 -190
- package/src/tui/chat-app.js +270 -28
- package/src/tui/tool-activity/presenters/misc.js +14 -0
- package/src/core/provider/anthropic.sdk-backup.js +0 -439
- package/src/core/provider/openai-compatible.sdk-backup.js +0 -412
package/src/core/memory-store.js
CHANGED
|
@@ -41,22 +41,92 @@ async function ensureParent(filePath) {
|
|
|
41
41
|
function buildFilePath(scope, workspaceRoot = process.cwd(), projectAlias = '') {
|
|
42
42
|
if (scope === 'user') return path.join(getMemoryDir(), 'user.json');
|
|
43
43
|
if (scope === 'global') return path.join(getMemoryDir(), 'global.json');
|
|
44
|
-
return path.join(getProjectMemoryDir(workspaceRoot),
|
|
44
|
+
return path.join(getProjectMemoryDir(workspaceRoot), 'project.json');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function listProjectMemoryFiles(workspaceRoot = process.cwd()) {
|
|
48
|
+
const dir = getProjectMemoryDir(workspaceRoot);
|
|
49
|
+
try {
|
|
50
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
51
|
+
return entries
|
|
52
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
53
|
+
.map((entry) => path.join(dir, entry.name))
|
|
54
|
+
.sort();
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
async function readMemoryBucket(filePath) {
|
|
61
|
+
const doc = await readMemoryBucketDocument(filePath);
|
|
62
|
+
return doc.items;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function readMemoryBucketDocument(filePath) {
|
|
48
66
|
try {
|
|
49
67
|
const raw = await fs.readFile(filePath, 'utf8');
|
|
50
68
|
const parsed = JSON.parse(raw);
|
|
51
|
-
return
|
|
69
|
+
return {
|
|
70
|
+
items: Array.isArray(parsed?.items) ? parsed.items : [],
|
|
71
|
+
maintenance: parsed?.maintenance && typeof parsed.maintenance === 'object' ? parsed.maintenance : null
|
|
72
|
+
};
|
|
52
73
|
} catch {
|
|
53
|
-
return [];
|
|
74
|
+
return { items: [], maintenance: null };
|
|
54
75
|
}
|
|
55
76
|
}
|
|
56
77
|
|
|
57
|
-
|
|
78
|
+
function memoryBucketHash(items = []) {
|
|
79
|
+
const stable = (Array.isArray(items) ? items : [])
|
|
80
|
+
.map((item) => ({
|
|
81
|
+
id: String(item?.id || ''),
|
|
82
|
+
kind: String(item?.kind || ''),
|
|
83
|
+
content: normalizeMemoryText(item?.content || ''),
|
|
84
|
+
summary: normalizeMemoryText(item?.summary || ''),
|
|
85
|
+
lifecycle: String(item?.lifecycle || ''),
|
|
86
|
+
pinned: item?.pinned === true
|
|
87
|
+
}))
|
|
88
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
89
|
+
return sha256(JSON.stringify(stable));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function writeMemoryBucket(filePath, items, { maintenance = null } = {}) {
|
|
58
93
|
await ensureParent(filePath);
|
|
59
|
-
|
|
94
|
+
const doc = { items };
|
|
95
|
+
if (maintenance) doc.maintenance = maintenance;
|
|
96
|
+
await fs.writeFile(filePath, `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function dedupeMemoryItems(items = []) {
|
|
100
|
+
const deduped = [];
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
for (const item of items) {
|
|
103
|
+
const key = item.id ? `id:${item.id}` : `${item.kind}:${normalizeMemoryText(item.content)}`;
|
|
104
|
+
if (seen.has(key)) continue;
|
|
105
|
+
seen.add(key);
|
|
106
|
+
deduped.push(item);
|
|
107
|
+
}
|
|
108
|
+
return deduped;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function readProjectMemoryItems(workspaceRoot = process.cwd(), projectAlias = '') {
|
|
112
|
+
const projectKey = getProjectMemoryKey(workspaceRoot, projectAlias);
|
|
113
|
+
const files = await listProjectMemoryFiles(workspaceRoot);
|
|
114
|
+
const items = [];
|
|
115
|
+
for (const file of files) {
|
|
116
|
+
const bucket = await readMemoryBucket(file);
|
|
117
|
+
items.push(...bucket.map((item) => normalizeMemoryItem(item, 'project', projectKey)));
|
|
118
|
+
}
|
|
119
|
+
return dedupeMemoryItems(items)
|
|
120
|
+
.sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function readScopeMemoryItems(scope, workspaceRoot = process.cwd(), projectAlias = '') {
|
|
124
|
+
const normalizedScope = ensureScope(scope);
|
|
125
|
+
if (normalizedScope === 'project') return readProjectMemoryItems(workspaceRoot, projectAlias);
|
|
126
|
+
const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
|
|
127
|
+
return (await readMemoryBucket(filePath))
|
|
128
|
+
.map((item) => normalizeMemoryItem(item, normalizedScope, ''))
|
|
129
|
+
.sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)));
|
|
60
130
|
}
|
|
61
131
|
|
|
62
132
|
function normalizeMemoryItem(item, scope, projectKey = '') {
|
|
@@ -74,7 +144,8 @@ function normalizeMemoryItem(item, scope, projectKey = '') {
|
|
|
74
144
|
createdAt: String(item?.createdAt || now),
|
|
75
145
|
updatedAt: String(item?.updatedAt || now),
|
|
76
146
|
hits: Number.isFinite(Number(item?.hits)) ? Number(item.hits) : 0,
|
|
77
|
-
pinned: item?.pinned === true
|
|
147
|
+
pinned: item?.pinned === true,
|
|
148
|
+
...(item?.lifecycle ? { lifecycle: String(item.lifecycle) } : {})
|
|
78
149
|
};
|
|
79
150
|
}
|
|
80
151
|
|
|
@@ -96,13 +167,69 @@ function budgetForScope(scope, config = {}) {
|
|
|
96
167
|
}
|
|
97
168
|
|
|
98
169
|
export async function listMemories({ scope, workspaceRoot = process.cwd(), projectAlias = '' }) {
|
|
170
|
+
const normalizedScope = ensureScope(scope);
|
|
171
|
+
return readScopeMemoryItems(normalizedScope, workspaceRoot, projectAlias);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function getMemoryBucketMaintenance({ scope, workspaceRoot = process.cwd(), projectAlias = '' }) {
|
|
175
|
+
const normalizedScope = ensureScope(scope);
|
|
176
|
+
const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
|
|
177
|
+
const doc = await readMemoryBucketDocument(filePath);
|
|
178
|
+
const items = normalizedScope === 'project'
|
|
179
|
+
? await readProjectMemoryItems(workspaceRoot, projectAlias)
|
|
180
|
+
: doc.items.map((item) => normalizeMemoryItem(item, normalizedScope, ''));
|
|
181
|
+
const currentHash = memoryBucketHash(items);
|
|
182
|
+
const storedHash = String(doc.maintenance?.contentHash || '');
|
|
183
|
+
const maintainedAt = String(doc.maintenance?.maintainedAt || '');
|
|
184
|
+
return {
|
|
185
|
+
scope: normalizedScope,
|
|
186
|
+
itemCount: items.length,
|
|
187
|
+
contentHash: currentHash,
|
|
188
|
+
storedHash,
|
|
189
|
+
maintainedAt,
|
|
190
|
+
fresh: Boolean(maintainedAt && storedHash && storedHash === currentHash)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function markMemoryBucketMaintained({ scope, workspaceRoot = process.cwd(), projectAlias = '' }) {
|
|
195
|
+
const normalizedScope = ensureScope(scope);
|
|
196
|
+
const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
|
|
197
|
+
const items = await readScopeMemoryItems(normalizedScope, workspaceRoot, projectAlias);
|
|
198
|
+
const maintenance = {
|
|
199
|
+
maintainedAt: nowIso(),
|
|
200
|
+
contentHash: memoryBucketHash(items),
|
|
201
|
+
itemCount: items.length
|
|
202
|
+
};
|
|
203
|
+
await writeMemoryBucket(filePath, items, { maintenance });
|
|
204
|
+
return { scope: normalizedScope, ...maintenance };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function replaceMemoryBucket({
|
|
208
|
+
scope,
|
|
209
|
+
items = [],
|
|
210
|
+
workspaceRoot = process.cwd(),
|
|
211
|
+
projectAlias = '',
|
|
212
|
+
markMaintained = false
|
|
213
|
+
} = {}) {
|
|
99
214
|
const normalizedScope = ensureScope(scope);
|
|
100
215
|
const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
|
|
101
216
|
const projectKey = normalizedScope === 'project' ? getProjectMemoryKey(workspaceRoot, projectAlias) : '';
|
|
102
|
-
const
|
|
103
|
-
return items
|
|
217
|
+
const normalizedItems = (Array.isArray(items) ? items : [])
|
|
104
218
|
.map((item) => normalizeMemoryItem(item, normalizedScope, projectKey))
|
|
105
|
-
.
|
|
219
|
+
.filter((item) => item.content);
|
|
220
|
+
const maintenance = markMaintained
|
|
221
|
+
? {
|
|
222
|
+
maintainedAt: nowIso(),
|
|
223
|
+
contentHash: memoryBucketHash(normalizedItems),
|
|
224
|
+
itemCount: normalizedItems.length
|
|
225
|
+
}
|
|
226
|
+
: null;
|
|
227
|
+
await writeMemoryBucket(filePath, normalizedItems, { maintenance });
|
|
228
|
+
return {
|
|
229
|
+
scope: normalizedScope,
|
|
230
|
+
items: normalizedItems,
|
|
231
|
+
maintenance
|
|
232
|
+
};
|
|
106
233
|
}
|
|
107
234
|
|
|
108
235
|
export async function rememberMemory({
|
|
@@ -125,7 +252,7 @@ export async function rememberMemory({
|
|
|
125
252
|
|
|
126
253
|
const filePath = buildFilePath(normalizedScope, workspaceRoot, projectAlias);
|
|
127
254
|
const projectKey = normalizedScope === 'project' ? getProjectMemoryKey(workspaceRoot, projectAlias) : '';
|
|
128
|
-
const existing =
|
|
255
|
+
const existing = await readScopeMemoryItems(normalizedScope, workspaceRoot, projectAlias);
|
|
129
256
|
const probe = normalizeMemoryItem({ content: normalizedContent, kind, summary, source, confidence, pinned }, normalizedScope, projectKey);
|
|
130
257
|
|
|
131
258
|
const replaceIndex = replaceSimilar ? existing.findIndex((item) => sameMemory(item, probe)) : -1;
|
|
@@ -170,6 +297,14 @@ export async function forgetMemory({ scope, id, workspaceRoot = process.cwd(), p
|
|
|
170
297
|
const existing = await listMemories({ scope: normalizedScope, workspaceRoot, projectAlias });
|
|
171
298
|
const kept = existing.filter((item) => item.id !== id);
|
|
172
299
|
await writeMemoryBucket(filePath, kept);
|
|
300
|
+
if (normalizedScope === 'project') {
|
|
301
|
+
const files = (await listProjectMemoryFiles(workspaceRoot)).filter((file) => file !== filePath);
|
|
302
|
+
await Promise.all(files.map(async (file) => {
|
|
303
|
+
const bucket = await readMemoryBucket(file);
|
|
304
|
+
const next = bucket.filter((item) => String(item?.id || '') !== id);
|
|
305
|
+
if (next.length !== bucket.length) await writeMemoryBucket(file, next);
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
173
308
|
return { removed: existing.length - kept.length };
|
|
174
309
|
}
|
|
175
310
|
|
|
@@ -54,6 +54,36 @@ async function parseJsonResponse(response) {
|
|
|
54
54
|
return response.json();
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function isRetryableStatus(status) {
|
|
58
|
+
return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isRetryableError(error) {
|
|
62
|
+
const name = String(error?.name || '');
|
|
63
|
+
if (name === 'AbortError' || name === 'TimeoutError') return false;
|
|
64
|
+
const message = String(error?.message || error || '');
|
|
65
|
+
return /fetch failed|network|socket|ECONNRESET|ETIMEDOUT|EAI_AGAIN/i.test(message);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function fetchWithRetry(url, init, { maxRetries = 0 } = {}) {
|
|
69
|
+
const attempts = Math.max(0, Number(maxRetries) || 0) + 1;
|
|
70
|
+
let lastError;
|
|
71
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(url, init);
|
|
74
|
+
if (response.ok || !isRetryableStatus(response.status) || attempt === attempts - 1) {
|
|
75
|
+
return response;
|
|
76
|
+
}
|
|
77
|
+
await response.arrayBuffer().catch(() => null);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
lastError = error;
|
|
80
|
+
if (!isRetryableError(error) || attempt === attempts - 1) throw error;
|
|
81
|
+
}
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
|
|
83
|
+
}
|
|
84
|
+
throw lastError || new Error('Gateway request failed');
|
|
85
|
+
}
|
|
86
|
+
|
|
57
87
|
async function* iterateSseEvents(stream) {
|
|
58
88
|
const decoder = new TextDecoder();
|
|
59
89
|
let buffer = '';
|
|
@@ -318,12 +348,12 @@ export async function createChatCompletion({
|
|
|
318
348
|
maxRetries = 2
|
|
319
349
|
}) {
|
|
320
350
|
const payload = buildPayload({ model, temperature, messages, tools });
|
|
321
|
-
const response = await
|
|
351
|
+
const response = await fetchWithRetry(buildChatCompletionsUrl(baseUrl), {
|
|
322
352
|
method: 'POST',
|
|
323
353
|
headers: createHeaders(apiKey),
|
|
324
354
|
body: JSON.stringify(payload),
|
|
325
355
|
signal: AbortSignal.timeout(timeoutMs)
|
|
326
|
-
});
|
|
356
|
+
}, { maxRetries });
|
|
327
357
|
const data = await parseJsonResponse(response);
|
|
328
358
|
const message = data?.choices?.[0]?.message || {};
|
|
329
359
|
const text = sanitizeMiniMaxText(model, extractTextContent(message.content));
|
|
@@ -386,12 +416,12 @@ export async function createChatCompletionStream({
|
|
|
386
416
|
}
|
|
387
417
|
}
|
|
388
418
|
const payload = buildPayload({ model, temperature, messages, tools, stream: true });
|
|
389
|
-
const response = await
|
|
419
|
+
const response = await fetchWithRetry(buildChatCompletionsUrl(baseUrl), {
|
|
390
420
|
method: 'POST',
|
|
391
421
|
headers: createHeaders(apiKey),
|
|
392
422
|
body: JSON.stringify(payload),
|
|
393
423
|
signal: controller.signal
|
|
394
|
-
});
|
|
424
|
+
}, { maxRetries });
|
|
395
425
|
if (!response.ok || !response.body) {
|
|
396
426
|
const text = await response.text().catch(() => '');
|
|
397
427
|
throw new Error(`Gateway error ${response.status}: ${text || response.statusText}`);
|
|
@@ -402,7 +432,8 @@ export async function createChatCompletionStream({
|
|
|
402
432
|
let usage = null;
|
|
403
433
|
let miniMaxStreamState = { rawContent: '', visibleText: '' };
|
|
404
434
|
|
|
405
|
-
|
|
435
|
+
try {
|
|
436
|
+
for await (const chunk of iterateSseEvents(response.body)) {
|
|
406
437
|
usage = chunk?.usage || usage;
|
|
407
438
|
const choice0 = chunk?.choices?.[0] || {};
|
|
408
439
|
const delta = choice0?.delta || {};
|
|
@@ -452,6 +483,10 @@ export async function createChatCompletionStream({
|
|
|
452
483
|
if (choice0?.finish_reason) {
|
|
453
484
|
break;
|
|
454
485
|
}
|
|
486
|
+
}
|
|
487
|
+
} finally {
|
|
488
|
+
timeoutSignal.removeEventListener('abort', onAbort);
|
|
489
|
+
if (externalSignal) externalSignal.removeEventListener('abort', onAbort);
|
|
455
490
|
}
|
|
456
491
|
|
|
457
492
|
const result = buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getProjectSkillsDir, getSkillsDir } from './paths.js';
|
|
4
|
+
import { createChatCompletion } from './provider/index.js';
|
|
5
|
+
|
|
6
|
+
const REFLECT_TIMEOUT_MS = 45000;
|
|
7
|
+
|
|
8
|
+
function slugifySkillName(value) {
|
|
9
|
+
const slug = String(value || '')
|
|
10
|
+
.trim()
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
|
13
|
+
.replace(/^-+|-+$/g, '');
|
|
14
|
+
return slug || 'reflected-success-workflow';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function escapeFrontmatter(value) {
|
|
18
|
+
return String(value || '').replace(/\r?\n/g, ' ').replace(/"/g, '\\"').trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasFrontmatter(content) {
|
|
22
|
+
return /^---\r?\n[\s\S]*?\r?\n---\r?\n/.test(String(content || '').trimStart());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderSkillContent({ name, description, content }) {
|
|
26
|
+
const body = String(content || '').trim() || [
|
|
27
|
+
'## Workflow',
|
|
28
|
+
'',
|
|
29
|
+
'1. Recreate the successful chain from the recent task.',
|
|
30
|
+
'2. Preserve the key decision that made it work.',
|
|
31
|
+
'3. Verify with the narrowest relevant check.',
|
|
32
|
+
'',
|
|
33
|
+
'## Boundaries',
|
|
34
|
+
'',
|
|
35
|
+
'Use this only when the current task matches the preserved workflow.'
|
|
36
|
+
].join('\n');
|
|
37
|
+
if (hasFrontmatter(body)) return `${body.trim()}\n`;
|
|
38
|
+
return [
|
|
39
|
+
'---',
|
|
40
|
+
`name: ${name}`,
|
|
41
|
+
`description: ${escapeFrontmatter(description) || `Use when this reflected workflow applies.`}`,
|
|
42
|
+
'---',
|
|
43
|
+
'',
|
|
44
|
+
body
|
|
45
|
+
].join('\n').trimEnd() + '\n';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function normalizeReflectDraft(raw = {}) {
|
|
49
|
+
const name = slugifySkillName(raw.name || raw.skillName || raw.title);
|
|
50
|
+
const description = String(raw.description || raw.summary || `Use when the ${name} workflow applies.`).trim();
|
|
51
|
+
const confidence = Math.min(1, Math.max(0, Number(raw.confidence ?? 0.75)));
|
|
52
|
+
return {
|
|
53
|
+
id: Number(raw.id || 1),
|
|
54
|
+
name,
|
|
55
|
+
description,
|
|
56
|
+
confidence,
|
|
57
|
+
content: renderSkillContent({ name, description, content: raw.content || raw.markdown || raw.body })
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildReflectTargetPath({ scope = 'project', name, workspaceRoot = process.cwd() } = {}) {
|
|
62
|
+
const safeName = slugifySkillName(name);
|
|
63
|
+
const baseDir = String(scope || '').toLowerCase() === 'global'
|
|
64
|
+
? getSkillsDir()
|
|
65
|
+
: getProjectSkillsDir(workspaceRoot);
|
|
66
|
+
return path.join(baseDir, safeName, 'SKILL.md');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseReflectScope(args = []) {
|
|
70
|
+
let scope = 'project';
|
|
71
|
+
const requestParts = [];
|
|
72
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
73
|
+
const arg = String(args[index] || '');
|
|
74
|
+
if (arg === '--scope') {
|
|
75
|
+
const next = String(args[index + 1] || '').toLowerCase();
|
|
76
|
+
if (next === 'global' || next === 'project') {
|
|
77
|
+
scope = next;
|
|
78
|
+
index += 1;
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (arg.startsWith('--scope=')) {
|
|
83
|
+
const value = arg.slice('--scope='.length).toLowerCase();
|
|
84
|
+
if (value === 'global' || value === 'project') scope = value;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
requestParts.push(arg);
|
|
88
|
+
}
|
|
89
|
+
return { scope, request: requestParts.join(' ').trim() };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseModelDrafts(text) {
|
|
93
|
+
const raw = String(text || '').trim();
|
|
94
|
+
if (!raw) return [];
|
|
95
|
+
const unfenced = raw.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/i, '').trim();
|
|
96
|
+
try {
|
|
97
|
+
const parsed = JSON.parse(unfenced);
|
|
98
|
+
if (Array.isArray(parsed?.candidates)) return parsed.candidates.map((item, index) => normalizeReflectDraft({ id: index + 1, ...item }));
|
|
99
|
+
if (Array.isArray(parsed)) return parsed.map((item, index) => normalizeReflectDraft({ id: index + 1, ...item }));
|
|
100
|
+
if (parsed && typeof parsed === 'object') return [normalizeReflectDraft(parsed)];
|
|
101
|
+
} catch {
|
|
102
|
+
// Fall back to wrapping plain markdown below.
|
|
103
|
+
}
|
|
104
|
+
return [normalizeReflectDraft({
|
|
105
|
+
name: 'reflected-success-workflow',
|
|
106
|
+
description: 'Use when the reflected successful workflow applies.',
|
|
107
|
+
content: raw
|
|
108
|
+
})];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function recentContext(session, limit = 10) {
|
|
112
|
+
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
113
|
+
return messages
|
|
114
|
+
.slice(-limit)
|
|
115
|
+
.map((message) => `${message.role}: ${String(message.content || '').slice(0, 1200)}`)
|
|
116
|
+
.join('\n\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function buildReflectSkillDraft({
|
|
120
|
+
request = '',
|
|
121
|
+
scope = 'project',
|
|
122
|
+
session,
|
|
123
|
+
config = {},
|
|
124
|
+
model,
|
|
125
|
+
systemPrompt = '',
|
|
126
|
+
previousDraft = null,
|
|
127
|
+
feedback = ''
|
|
128
|
+
} = {}) {
|
|
129
|
+
const mode = String(request || '').trim() ? 'directed' : 'exploratory';
|
|
130
|
+
const prompt = [
|
|
131
|
+
'Create a reusable Codex/CodeMini SKILL.md draft from a successful workflow.',
|
|
132
|
+
`Mode: ${mode}`,
|
|
133
|
+
`Target scope: ${scope}`,
|
|
134
|
+
request ? `User reflection request:\n${request}` : 'No explicit request was supplied. Be conservative and return no candidates if the recent context does not show a reusable success pattern.',
|
|
135
|
+
previousDraft ? `Existing draft to revise:\n${previousDraft.content || ''}` : '',
|
|
136
|
+
feedback ? `User edit feedback:\n${feedback}` : '',
|
|
137
|
+
'Recent session context:',
|
|
138
|
+
recentContext(session),
|
|
139
|
+
'Return valid JSON only, no markdown fences.',
|
|
140
|
+
'Shape: {"candidates":[{"name":"kebab-case-name","description":"when to use this skill","confidence":0.0,"content":"full SKILL.md body or markdown body"}]}',
|
|
141
|
+
'The content must include trigger conditions, workflow/toolchain, key decisions, pitfalls, verification, and boundaries.',
|
|
142
|
+
'Do not write memory or inbox content. This is only a skill draft.'
|
|
143
|
+
].filter(Boolean).join('\n\n');
|
|
144
|
+
|
|
145
|
+
const result = await createChatCompletion({
|
|
146
|
+
sdkProvider: config?.sdk?.provider,
|
|
147
|
+
baseUrl: config?.gateway?.base_url,
|
|
148
|
+
apiKey: config?.gateway?.api_key,
|
|
149
|
+
model: model || config?.model?.name,
|
|
150
|
+
messages: [
|
|
151
|
+
{ role: 'system', content: systemPrompt || 'You draft concise, reusable coding workflow skills.' },
|
|
152
|
+
{ role: 'user', content: prompt }
|
|
153
|
+
],
|
|
154
|
+
temperature: 0,
|
|
155
|
+
timeoutMs: REFLECT_TIMEOUT_MS
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return parseModelDrafts(result?.text || '');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function attachReflectTargets({ candidates = [], scope = 'project', workspaceRoot = process.cwd() } = {}) {
|
|
162
|
+
return candidates.map((candidate, index) => {
|
|
163
|
+
const draft = normalizeReflectDraft({ id: index + 1, ...candidate });
|
|
164
|
+
return {
|
|
165
|
+
...draft,
|
|
166
|
+
targetPath: buildReflectTargetPath({ scope, name: draft.name, workspaceRoot })
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function writeReflectSkillDraft({ draft, scope = 'project', workspaceRoot = process.cwd() } = {}) {
|
|
172
|
+
const normalized = normalizeReflectDraft(draft);
|
|
173
|
+
const filePath = buildReflectTargetPath({ scope, name: normalized.name, workspaceRoot });
|
|
174
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
175
|
+
await fs.writeFile(filePath, normalized.content, 'utf8');
|
|
176
|
+
return { filePath, draft: normalized };
|
|
177
|
+
}
|
|
178
|
+
|
|
@@ -146,11 +146,11 @@ export function getShellSystemPrompt(value) {
|
|
|
146
146
|
ALWAYS prefer dedicated tools over raw shell commands:
|
|
147
147
|
- The visible default tool list is intentionally small. If a needed capability is not currently listed, do not assume it is unavailable — call tool_search to load additional tools first
|
|
148
148
|
- 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
|
|
149
|
-
- Use read to inspect files — NEVER use cat, head, or tail via run.
|
|
149
|
+
- Use read to inspect files — NEVER use cat, head, or tail via run. Use canonical shapes like {path:"src/app.ts"}, {path:"src/app.ts:10-40"}, or {path:"src/app.ts", start_line:10, end_line:40}
|
|
150
150
|
- Use grep to search file contents — NEVER use grep or rg via run
|
|
151
151
|
- Use list for directory-by-directory filesystem discovery. If you specifically need pattern-based file lookup like src/**/*.ts, load glob with tool_search instead of falling back to run
|
|
152
|
-
- Use edit to modify existing files — this is the DEFAULT path for code changes.
|
|
153
|
-
- Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files).
|
|
152
|
+
- Use edit to modify existing files — this is the DEFAULT path for code changes. Prefer {path:"src/app.ts", old_text:"foo", new_text:"bar"}
|
|
153
|
+
- Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files). Prefer {path:"notes.txt", content:"..."}
|
|
154
154
|
- Use update_todos to manage the session todo checklist for complex work. Provide the full current list each time and usually keep exactly one item in_progress
|
|
155
155
|
- Use read_plan and update_plan to recover or sync structured plan state when plan progress was interrupted (for example by transient gateway/model errors)
|
|
156
156
|
- Use run for shell commands. For long-running processes (dev servers, watchers), set run_in_background=true when you know you do not need the final result immediately. Long-running commands may also be backgrounded automatically
|
|
@@ -181,15 +181,15 @@ For background commands: use run to launch. If you need management tools that ar
|
|
|
181
181
|
Common tool call patterns:
|
|
182
182
|
- Query the project index first: {query:"login auth flow", path:"src", max_results:5}
|
|
183
183
|
- Load a deferred tool when needed: {query:"glob"} or {query:"all"}
|
|
184
|
-
- Read a file: {path:"src/app.ts"} or {
|
|
184
|
+
- Read a file: {path:"src/app.ts"} or {path:"src/app.ts", start_line:20, end_line:60}
|
|
185
185
|
- Read a specific range inline: {path:"src/app.ts:20-60"}
|
|
186
186
|
- Search text: {pattern:"loginUser", path:"src"} or {query:"loginUser", directory:"src"}
|
|
187
187
|
- List a directory first: {path:"src"}
|
|
188
188
|
- After loading glob, find files by pattern: {pattern:"src/**/*.ts"} or {query:"src/**/*.ts"}
|
|
189
|
-
- Edit exact text: {
|
|
189
|
+
- Edit exact text: {path:"src/app.ts", old_text:"foo", new_text:"bar"}
|
|
190
190
|
- Edit with shorthand: {path:"src/app.ts", old_text:"foo", content:"bar"}
|
|
191
|
-
- Write a new file: {
|
|
192
|
-
- When the environment provides a Working directory, prefer absolute
|
|
191
|
+
- Write a new file: {path:"notes.txt", content:"..."} or {path:"src/page.tsx", content:"..."}
|
|
192
|
+
- When the environment provides a Working directory, prefer absolute path values rooted there instead of guessing prefixes
|
|
193
193
|
- If the user gives a relative path like src/app.ts, resolve it from the current Working directory rather than inventing ../ or sibling folders
|
|
194
194
|
|
|
195
195
|
# Doing tasks
|
|
@@ -218,7 +218,7 @@ Common tool call patterns:
|
|
|
218
218
|
- Keep answers compact and easy to scan
|
|
219
219
|
- Lead with the answer or next action, not scene-setting
|
|
220
220
|
- Do not restate the user's request unless a brief restatement prevents ambiguity
|
|
221
|
-
- When referencing code, use
|
|
221
|
+
- When referencing code, use path:line_number format
|
|
222
222
|
- Keep technical wording, commands, paths, and error details exact
|
|
223
223
|
- Only use emojis if the user explicitly requests it`;
|
|
224
224
|
}
|