codemini-cli 0.4.1 → 0.4.3

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.
@@ -294,7 +294,7 @@ export async function createChatCompletion({
294
294
  messages,
295
295
  temperature = 0.2,
296
296
  tools,
297
- timeoutMs = 90000,
297
+ timeoutMs = 1800000,
298
298
  maxTokens = 4096
299
299
  }) {
300
300
  const payload = buildPayload({ model, temperature, messages, tools, maxTokens });
@@ -317,7 +317,7 @@ export async function createChatCompletionStream({
317
317
  tools,
318
318
  onTextDelta,
319
319
  onToolCallDelta,
320
- timeoutMs = 90000,
320
+ timeoutMs = 1800000,
321
321
  maxTokens = 4096,
322
322
  signal: externalSignal
323
323
  }) {
@@ -344,7 +344,7 @@ export async function createChatCompletion({
344
344
  messages,
345
345
  temperature = 0.2,
346
346
  tools,
347
- timeoutMs = 90000,
347
+ timeoutMs = 1800000,
348
348
  maxRetries = 2
349
349
  }) {
350
350
  const payload = buildPayload({ model, temperature, messages, tools });
@@ -399,7 +399,7 @@ export async function createChatCompletionStream({
399
399
  tools,
400
400
  onTextDelta,
401
401
  onToolCallDelta,
402
- timeoutMs = 90000,
402
+ timeoutMs = 1800000,
403
403
  maxRetries = 2,
404
404
  signal: externalSignal
405
405
  }) {
@@ -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
+
package/src/core/shell.js CHANGED
@@ -220,7 +220,7 @@ export function runShellCommand({
220
220
  command,
221
221
  cwd = process.cwd(),
222
222
  shell = 'powershell',
223
- timeoutMs = 120000
223
+ timeoutMs = 1800000
224
224
  }) {
225
225
  const shellSpec = resolveShell(shell);
226
226
  const shellCommand =
@@ -0,0 +1,206 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { BoundedCache } from './bounded-cache.js';
5
+ import { trimInline } from './string-utils.js';
6
+
7
+ const TOOL_RESULT_DISK_THRESHOLD = 6000;
8
+ const PREVIEW_SIZE_BYTES = 2000;
9
+ const TOOL_RESULTS_SUBDIR = 'tool-results';
10
+
11
+ let currentResultDir = null;
12
+ let resultDirReady = false;
13
+
14
+ const storedResults = new BoundedCache({
15
+ maxSize: 64,
16
+ ttlMs: 30 * 60 * 1000,
17
+ onEvict(_key, value) {
18
+ if (value?.filePath) {
19
+ fs.unlink(value.filePath).catch(() => {});
20
+ }
21
+ }
22
+ });
23
+
24
+ const readCache = new BoundedCache({ maxSize: 128, ttlMs: 10 * 60 * 1000 });
25
+
26
+ function generatePreview(content) {
27
+ if (content.length <= PREVIEW_SIZE_BYTES) {
28
+ return { preview: content, hasMore: false };
29
+ }
30
+ const truncated = content.slice(0, PREVIEW_SIZE_BYTES);
31
+ const lastNewline = truncated.lastIndexOf('\n');
32
+ const cutPoint = lastNewline > PREVIEW_SIZE_BYTES * 0.5 ? lastNewline : PREVIEW_SIZE_BYTES;
33
+ return { preview: content.slice(0, cutPoint), hasMore: true };
34
+ }
35
+
36
+ function formatFileSize(chars) {
37
+ if (chars < 1024) return `${chars} B`;
38
+ return `${(chars / 1024).toFixed(1)} KB`;
39
+ }
40
+
41
+ export function setResultDir(dir) {
42
+ currentResultDir = dir ? path.join(dir, TOOL_RESULTS_SUBDIR) : null;
43
+ resultDirReady = false;
44
+ }
45
+
46
+ async function ensureResultDir() {
47
+ if (!currentResultDir) return false;
48
+ if (!resultDirReady) {
49
+ await fs.mkdir(currentResultDir, { recursive: true });
50
+ resultDirReady = true;
51
+ }
52
+ return true;
53
+ }
54
+
55
+ export async function storeResultIfNeeded(callId, formattedContent, rawResult) {
56
+ if (formattedContent.length <= TOOL_RESULT_DISK_THRESHOLD) {
57
+ return formattedContent;
58
+ }
59
+ try {
60
+ const ready = await ensureResultDir();
61
+ const dir = ready ? currentResultDir : path.join(os.tmpdir(), 'codemini-results');
62
+ if (!resultDirReady && dir === currentResultDir) {
63
+ await fs.mkdir(dir, { recursive: true });
64
+ } else if (!resultDirReady) {
65
+ await fs.mkdir(dir, { recursive: true });
66
+ }
67
+ const filePath = path.join(dir, `${callId}.txt`);
68
+ const payload = typeof rawResult === 'string' ? rawResult : JSON.stringify(rawResult, null, 2);
69
+ await fs.writeFile(filePath, payload, 'utf-8');
70
+ const summary = summarizeToolResult(rawResult);
71
+ const { preview, hasMore } = generatePreview(payload);
72
+ storedResults.set(callId, { filePath, summary });
73
+
74
+ return `<persisted-output>
75
+ Output too large (${formatFileSize(payload.length)}). Full output saved to: ${filePath}
76
+
77
+ Preview (first ${formatFileSize(PREVIEW_SIZE_BYTES)}):
78
+ ${preview}${hasMore ? '\n...' : ''}
79
+
80
+ Summary: ${summary}
81
+ </persisted-output>`;
82
+ } catch {
83
+ return formattedContent;
84
+ }
85
+ }
86
+
87
+ export function clearResultStore() {
88
+ const files = [];
89
+ for (const [, val] of storedResults.entries()) {
90
+ files.push(val.filePath);
91
+ }
92
+ storedResults.clear();
93
+ readCache.clear();
94
+ return Promise.allSettled(files.map((filePath) => fs.unlink(filePath).catch(() => {})));
95
+ }
96
+
97
+ export function checkReadDedup(filePath, startLine, endLine, mtimeMs) {
98
+ const key = `${filePath}:${startLine || 0}:${endLine || 0}:${mtimeMs}`;
99
+ if (readCache.has(key)) {
100
+ return true;
101
+ }
102
+ readCache.set(key, true);
103
+ return false;
104
+ }
105
+
106
+ export function summarizeToolResult(result) {
107
+ if (result === null || result === undefined) return 'no output';
108
+ if (typeof result === 'string') {
109
+ const oneLine = result.replace(/\s+/g, ' ').trim();
110
+ return oneLine.length > 90 ? `${oneLine.slice(0, 87)}...` : oneLine || 'empty string';
111
+ }
112
+ if (typeof result === 'object') {
113
+ const obj = result;
114
+ if (Array.isArray(obj)) return `array(${obj.length})`;
115
+ if ('deleted' in obj && 'path' in obj) {
116
+ const kind = trimInline(obj.type || 'item', 16);
117
+ const target = trimInline(obj.path || '', 96);
118
+ if (obj.deleted) return target ? `deleted ${kind} ${target}` : `deleted ${kind}`;
119
+ if (obj.cancelled) return target ? `cancelled delete ${target}` : 'cancelled delete';
120
+ }
121
+ if ('path' in obj && 'action' in obj) {
122
+ const p = String(obj.path || '');
123
+ const action = String(obj.action || 'write');
124
+ const line = Number(obj.changed_line || 1);
125
+ const suffix =
126
+ action === 'delete'
127
+ ? 'deleted'
128
+ : action === 'create'
129
+ ? 'created'
130
+ : action === 'patch'
131
+ ? 'patched'
132
+ : action === 'replace_block' || action === 'replace_text'
133
+ ? 'edited'
134
+ : action === 'append'
135
+ ? 'appended'
136
+ : 'updated';
137
+ return p ? `${suffix} ${p}${line > 0 ? ` @L${line}` : ''}` : suffix;
138
+ }
139
+ if ('path' in obj && 'phase' in obj) {
140
+ const phase = String(obj.phase || '');
141
+ const p = String(obj.path || '');
142
+ const total = Number(obj.total_lines);
143
+ const start =
144
+ Number(obj.suggested_start_line || obj.start_line) > 0
145
+ ? Number(obj.suggested_start_line || obj.start_line)
146
+ : 1;
147
+ const end =
148
+ Number(obj.suggested_end_line || obj.end_line) >= start
149
+ ? Number(obj.suggested_end_line || obj.end_line)
150
+ : start;
151
+ const rangeText = start > 0 && end >= start ? ` lines ${start}-${end}` : '';
152
+ const totalText = total > 0 ? ` of ${total}` : '';
153
+ const enclosingText = obj.enclosing_symbol ? ` in ${obj.enclosing_symbol}` : '';
154
+ const errorText = obj.error ? ` (${trimInline(obj.error, 64)})` : '';
155
+ const truncatedText = obj.truncated ? ' [truncated]' : '';
156
+ return phase === 'metadata'
157
+ ? `metadata for ${p}${rangeText}${totalText}${errorText}`
158
+ : `content from ${p}${rangeText}${totalText}${enclosingText}${truncatedText}`;
159
+ }
160
+ if ('stdout' in obj || 'stderr' in obj || 'code' in obj) {
161
+ const stdout = trimInline(obj.stdout || '', 96);
162
+ const stderr = trimInline(obj.stderr || '', 96);
163
+ const command = trimInline(obj.command || '', 72);
164
+ const lead = command ? `${command} -> ` : '';
165
+ if (stdout) return `${lead}exit ${obj.code ?? 0}\nstdout: ${stdout}`;
166
+ if (stderr) return `${lead}exit ${obj.code ?? 0}\nstderr: ${stderr}`;
167
+ return `${lead}exit ${obj.code ?? 0}`;
168
+ }
169
+ if ('task_id' in obj && 'startup_confirmed' in obj) {
170
+ const status = trimInline(obj.status || 'unknown', 32);
171
+ const taskId = trimInline(obj.task_id || '', 24);
172
+ const source = trimInline(obj.startup_source || '', 24);
173
+ const outputFile = trimInline(obj.output_file || '', 72);
174
+ const output = Array.isArray(obj.recent_output) ? trimInline(obj.recent_output.slice(-1)[0] || '', 96) : '';
175
+ return `${taskId || 'task'} ${status}${source ? ` (${source})` : ''}${outputFile ? ` -> ${outputFile}` : ''}${output ? `\n${output}` : ''}`;
176
+ }
177
+ if ('tasks' in obj && Array.isArray(obj.tasks)) {
178
+ const count = obj.tasks.length;
179
+ const first = obj.tasks[0];
180
+ const lead = first?.task_id ? `${trimInline(first.task_id, 24)} ${trimInline(first.status || 'unknown', 24)}` : '';
181
+ return `tasks(${count})${lead ? `\n${lead}` : ''}`;
182
+ }
183
+ if ('files' in obj && Array.isArray(obj.files)) {
184
+ return `patched ${obj.files.length} file(s)`;
185
+ }
186
+ if ('diff' in obj && 'new_hash' in obj && 'path' in obj) {
187
+ const p = String(obj.path || '');
188
+ return p ? `diff preview for ${p}` : 'diff preview';
189
+ }
190
+ if ('created' in obj && Array.isArray(obj.created)) {
191
+ return `created ${obj.created.length} task(s)`;
192
+ }
193
+ if ('tasks' in obj && Array.isArray(obj.tasks)) {
194
+ return `${obj.tasks.length} task(s)`;
195
+ }
196
+ if ('newTodos' in obj && Array.isArray(obj.newTodos)) {
197
+ return obj.newTodos.length > 0 ? `updated ${obj.newTodos.length} todo item(s)` : 'cleared todo list';
198
+ }
199
+ if ('newPlan' in obj) {
200
+ return obj.newPlan ? `updated plan state (${String(obj.newPlan.status || 'draft')})` : 'cleared plan state';
201
+ }
202
+ const keys = Object.keys(obj);
203
+ return keys.length > 0 ? `keys: ${keys.slice(0, 5).join(',')}` : 'object';
204
+ }
205
+ return String(result);
206
+ }