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.
- package/OPERATIONS.md +4 -2
- package/README.md +87 -7
- package/deployment.md +14 -7
- package/package.json +1 -3
- package/skills/grill-me/SKILL.md +30 -0
- package/skills/project-requirements/SKILL.md +245 -0
- package/skills/superpowers-lite/SKILL.md +5 -1
- package/src/cli.js +1 -1
- package/src/commands/run.js +5 -4
- package/src/commands/skill.js +145 -53
- package/src/core/agent-loop.js +9 -214
- package/src/core/chat-runtime.js +520 -78
- package/src/core/command-loader.js +12 -5
- package/src/core/config-store.js +6 -3
- package/src/core/context-compact.js +2 -1
- 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/anthropic.js +2 -2
- package/src/core/provider/openai-compatible.js +2 -2
- package/src/core/reflect-skill.js +178 -0
- package/src/core/shell.js +1 -1
- package/src/core/tool-result-store.js +206 -0
- package/src/core/tools.js +242 -69
- package/src/tui/chat-app.js +298 -48
- package/src/tui/tool-activity/presenters/system.js +6 -0
- package/src/core/provider/anthropic.sdk-backup.js +0 -439
- package/src/core/provider/openai-compatible.sdk-backup.js +0 -412
|
@@ -294,7 +294,7 @@ export async function createChatCompletion({
|
|
|
294
294
|
messages,
|
|
295
295
|
temperature = 0.2,
|
|
296
296
|
tools,
|
|
297
|
-
timeoutMs =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
@@ -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
|
+
}
|