codemini-cli 0.2.2 → 0.2.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/package.json +1 -1
- package/skills/superpowers-lite/SKILL.md +5 -2
- package/src/cli.js +1 -1
- package/src/commands/run.js +3 -1
- package/src/core/agent-loop.js +308 -67
- package/src/core/chat-runtime.js +10 -3
- package/src/core/context-compact.js +32 -2
- package/src/core/default-system-prompt.js +22 -1
- package/src/core/shell-profile.js +37 -1
- package/src/core/tools.js +323 -82
package/package.json
CHANGED
|
@@ -20,18 +20,21 @@ Routing:
|
|
|
20
20
|
|
|
21
21
|
2. If the goal is clear but there are multiple reasonable implementation paths:
|
|
22
22
|
- use `brainstorm`
|
|
23
|
-
-
|
|
24
|
-
- do not
|
|
23
|
+
- ask exactly one clarifying question first
|
|
24
|
+
- do not give options, recommendations, or a tentative solution in the same response
|
|
25
|
+
- stop after the question and wait for the user's answer before continuing
|
|
25
26
|
|
|
26
27
|
3. If the request is still missing a key constraint or success condition:
|
|
27
28
|
- ask exactly one clarifying question
|
|
28
29
|
- do not give options yet
|
|
29
30
|
- do not write code yet
|
|
31
|
+
- stop after the question and wait for the user's answer
|
|
30
32
|
|
|
31
33
|
4. If the request is greenfield and underspecified, such as "build a page", "make a site", "generate an app", or similar:
|
|
32
34
|
- treat it as missing key constraints by default
|
|
33
35
|
- ask one high-value question before coding
|
|
34
36
|
- do not assume features, storage model, or scope unless the user already gave them
|
|
37
|
+
- stop after the question and wait for the user's answer
|
|
35
38
|
|
|
36
39
|
Tool order:
|
|
37
40
|
- prefer `grep` first for content search and candidate discovery
|
package/src/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { handleConfig } from './commands/config.js';
|
|
|
4
4
|
import { handleDoctor } from './commands/doctor.js';
|
|
5
5
|
import { handleSkill } from './commands/skill.js';
|
|
6
6
|
|
|
7
|
-
const VERSION = '0.2.
|
|
7
|
+
const VERSION = '0.2.3';
|
|
8
8
|
|
|
9
9
|
function printHelp() {
|
|
10
10
|
console.log(`codemini ${VERSION}
|
package/src/commands/run.js
CHANGED
|
@@ -35,7 +35,7 @@ export async function handleRun(args) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const config = await loadConfig();
|
|
38
|
-
const { definitions, handlers } = getBuiltinTools({
|
|
38
|
+
const { definitions, handlers, formatters, deferredDefinitions } = getBuiltinTools({
|
|
39
39
|
workspaceRoot: process.cwd(),
|
|
40
40
|
config
|
|
41
41
|
});
|
|
@@ -47,6 +47,8 @@ export async function handleRun(args) {
|
|
|
47
47
|
model: parsed.model || config.model.name,
|
|
48
48
|
toolDefinitions: definitions,
|
|
49
49
|
toolHandlers: handlers,
|
|
50
|
+
toolFormatters: formatters,
|
|
51
|
+
deferredDefinitions,
|
|
50
52
|
maxSteps: parsed.maxSteps,
|
|
51
53
|
requestCompletion: async ({ messages, tools, model }) =>
|
|
52
54
|
createChatCompletion({
|
package/src/core/agent-loop.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
|
|
1
5
|
function safeJsonParse(raw) {
|
|
2
6
|
if (!raw || typeof raw !== 'string') return {};
|
|
3
7
|
try {
|
|
@@ -13,7 +17,198 @@ function clipToolResult(result, maxChars = 12000) {
|
|
|
13
17
|
return `${raw.slice(0, maxChars)}\n... [tool result truncated ${raw.length - maxChars} chars]`;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
function
|
|
20
|
+
function compactToolResult(result, toolName, args, maxChars = 12000) {
|
|
21
|
+
if (result === null || result === undefined) return 'no output';
|
|
22
|
+
if (typeof result === 'string') {
|
|
23
|
+
if (result.length <= maxChars) return result;
|
|
24
|
+
return `${result.slice(0, maxChars)}\n... [tool result truncated ${result.length - maxChars} chars, original: ${result.length}]`;
|
|
25
|
+
}
|
|
26
|
+
if (typeof result !== 'object') return String(result);
|
|
27
|
+
|
|
28
|
+
const obj = result;
|
|
29
|
+
const rawLen = JSON.stringify(obj).length;
|
|
30
|
+
|
|
31
|
+
// Read file result: { path, phase, content, ... }
|
|
32
|
+
if ('path' in obj && 'phase' in obj && obj.phase === 'content') {
|
|
33
|
+
const header = `[File: ${obj.path}, lines ${obj.start_line || 1}-${obj.end_line || '?'}${obj.total_lines ? ` of ${obj.total_lines}` : ''}${obj.truncated ? ', truncated' : ''}]`;
|
|
34
|
+
const content = obj.content || obj.text || '';
|
|
35
|
+
if (typeof content !== 'string' || content.length <= maxChars) {
|
|
36
|
+
const body = typeof content === 'string' ? content : JSON.stringify(content);
|
|
37
|
+
return body.length <= maxChars ? `${header}\n${body}` : `${header}\n${body.slice(0, maxChars)}\n... [omitted ${body.length - maxChars} chars, original: ${rawLen}]`;
|
|
38
|
+
}
|
|
39
|
+
// Keep head + tail
|
|
40
|
+
const headLen = Math.floor(maxChars * 0.6);
|
|
41
|
+
const tailLen = Math.floor(maxChars * 0.3);
|
|
42
|
+
return `${header}\n${content.slice(0, headLen)}\n... [omitted ${content.length - headLen - tailLen} chars] ...\n${content.slice(-tailLen)}\n[original: ${rawLen} chars]`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// File edit/write result: { path, action, ... }
|
|
46
|
+
if ('path' in obj && 'action' in obj) {
|
|
47
|
+
const summary = summarizeToolResult(obj);
|
|
48
|
+
const diff = obj.diff || obj.patch || obj.content_preview || '';
|
|
49
|
+
if (diff && typeof diff === 'string' && diff.length <= 800) {
|
|
50
|
+
return `${summary}\n${diff}`;
|
|
51
|
+
}
|
|
52
|
+
if (diff) {
|
|
53
|
+
return `${summary}\n${diff.slice(0, 800)}\n... [diff truncated, original: ${rawLen}]`;
|
|
54
|
+
}
|
|
55
|
+
return `${summary} [original: ${rawLen} chars]`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Shell command result: { stdout, stderr, code, ... }
|
|
59
|
+
if ('stdout' in obj || 'stderr' in obj || 'code' in obj) {
|
|
60
|
+
const command = String(obj.command || '').slice(0, 200);
|
|
61
|
+
const stdout = String(obj.stdout || '').slice(0, 500);
|
|
62
|
+
const stderr = String(obj.stderr || '').slice(0, 500);
|
|
63
|
+
const code = obj.code ?? 0;
|
|
64
|
+
const parts = [`[exit: ${code}]`];
|
|
65
|
+
if (command) parts.push(`command: ${command}`);
|
|
66
|
+
if (stdout) parts.push(`stdout:\n${stdout}`);
|
|
67
|
+
if (stderr) parts.push(`stderr:\n${stderr}`);
|
|
68
|
+
if (rawLen > 2000) parts.push(`[original: ${rawLen} chars]`);
|
|
69
|
+
return parts.join('\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Array results (file lists, grep results, etc.)
|
|
73
|
+
if (Array.isArray(obj)) {
|
|
74
|
+
const maxItems = 50;
|
|
75
|
+
if (obj.length <= maxItems) {
|
|
76
|
+
const serialized = JSON.stringify(obj);
|
|
77
|
+
return serialized.length <= maxChars ? serialized : clipToolResult(obj, maxChars);
|
|
78
|
+
}
|
|
79
|
+
const kept = obj.slice(0, maxItems);
|
|
80
|
+
const items = typeof kept[0] === 'string'
|
|
81
|
+
? kept.join('\n')
|
|
82
|
+
: kept.map((item) => JSON.stringify(item)).join('\n');
|
|
83
|
+
return `${items}\n... and ${obj.length - maxItems} more items [total: ${obj.length}, original: ${rawLen} chars]`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Patch result: { files: [...] }
|
|
87
|
+
if ('files' in obj && Array.isArray(obj.files)) {
|
|
88
|
+
return `patched ${obj.files.length} file(s): ${obj.files.slice(0, 10).join(', ')}${obj.files.length > 10 ? ` ... and ${obj.files.length - 10} more` : ''} [original: ${rawLen}]`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Task results
|
|
92
|
+
if ('created' in obj && Array.isArray(obj.created)) {
|
|
93
|
+
return `created ${obj.created.length} task(s)`;
|
|
94
|
+
}
|
|
95
|
+
if ('tasks' in obj && Array.isArray(obj.tasks)) {
|
|
96
|
+
return `${obj.tasks.length} task(s)`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Fallback: clip with reduced limit
|
|
100
|
+
return clipToolResult(obj, Math.min(maxChars, 4000));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── P0: Large result disk store ─────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
const TOOL_RESULT_DISK_THRESHOLD = 6000;
|
|
106
|
+
const PREVIEW_SIZE_BYTES = 2000;
|
|
107
|
+
const TOOL_RESULTS_SUBDIR = 'tool-results';
|
|
108
|
+
|
|
109
|
+
let currentResultDir = null;
|
|
110
|
+
let resultDirReady = false;
|
|
111
|
+
const storedResults = new Map(); // callId -> { filePath, summary }
|
|
112
|
+
const readCache = new Map(); // "path:startLine:endLine:mtimeMs" -> true
|
|
113
|
+
|
|
114
|
+
function generatePreview(content) {
|
|
115
|
+
if (content.length <= PREVIEW_SIZE_BYTES) {
|
|
116
|
+
return { preview: content, hasMore: false };
|
|
117
|
+
}
|
|
118
|
+
const truncated = content.slice(0, PREVIEW_SIZE_BYTES);
|
|
119
|
+
const lastNewline = truncated.lastIndexOf('\n');
|
|
120
|
+
const cutPoint = lastNewline > PREVIEW_SIZE_BYTES * 0.5 ? lastNewline : PREVIEW_SIZE_BYTES;
|
|
121
|
+
return { preview: content.slice(0, cutPoint), hasMore: true };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function formatFileSize(chars) {
|
|
125
|
+
if (chars < 1024) return `${chars} B`;
|
|
126
|
+
return `${(chars / 1024).toFixed(1)} KB`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function setResultDir(dir) {
|
|
130
|
+
currentResultDir = dir ? path.join(dir, TOOL_RESULTS_SUBDIR) : null;
|
|
131
|
+
resultDirReady = false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function ensureResultDir() {
|
|
135
|
+
if (!currentResultDir) return false;
|
|
136
|
+
if (!resultDirReady) {
|
|
137
|
+
await fs.mkdir(currentResultDir, { recursive: true });
|
|
138
|
+
resultDirReady = true;
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function storeResultIfNeeded(callId, formattedContent, rawResult) {
|
|
144
|
+
if (formattedContent.length <= TOOL_RESULT_DISK_THRESHOLD) {
|
|
145
|
+
return formattedContent;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const ready = await ensureResultDir();
|
|
149
|
+
const dir = ready ? currentResultDir : path.join(os.tmpdir(), 'codemini-results');
|
|
150
|
+
if (!resultDirReady && dir === currentResultDir) {
|
|
151
|
+
await fs.mkdir(dir, { recursive: true });
|
|
152
|
+
} else if (!resultDirReady) {
|
|
153
|
+
await fs.mkdir(dir, { recursive: true });
|
|
154
|
+
}
|
|
155
|
+
const filePath = path.join(dir, `${callId}.txt`);
|
|
156
|
+
const payload = typeof rawResult === 'string' ? rawResult : JSON.stringify(rawResult, null, 2);
|
|
157
|
+
await fs.writeFile(filePath, payload, 'utf-8');
|
|
158
|
+
const summary = summarizeToolResult(rawResult);
|
|
159
|
+
const { preview, hasMore } = generatePreview(payload);
|
|
160
|
+
storedResults.set(callId, { filePath, summary });
|
|
161
|
+
|
|
162
|
+
return `<persisted-output>
|
|
163
|
+
Output too large (${formatFileSize(payload.length)}). Full output saved to: ${filePath}
|
|
164
|
+
|
|
165
|
+
Preview (first ${formatFileSize(PREVIEW_SIZE_BYTES)}):
|
|
166
|
+
${preview}${hasMore ? '\n...' : ''}
|
|
167
|
+
|
|
168
|
+
Summary: ${summary}
|
|
169
|
+
</persisted-output>`;
|
|
170
|
+
} catch {
|
|
171
|
+
return formattedContent;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function clearResultStore() {
|
|
176
|
+
const files = [];
|
|
177
|
+
for (const [, val] of storedResults) {
|
|
178
|
+
files.push(val.filePath);
|
|
179
|
+
}
|
|
180
|
+
storedResults.clear();
|
|
181
|
+
readCache.clear();
|
|
182
|
+
return Promise.allSettled(files.map((f) => fs.unlink(f).catch(() => {})));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Read deduplication ─────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
export function checkReadDedup(filePath, startLine, endLine, mtimeMs) {
|
|
188
|
+
const key = `${filePath}:${startLine || 0}:${endLine || 0}:${mtimeMs}`;
|
|
189
|
+
if (readCache.has(key)) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
readCache.set(key, true);
|
|
193
|
+
// Keep cache bounded
|
|
194
|
+
if (readCache.size > 100) {
|
|
195
|
+
const firstKey = readCache.keys().next().value;
|
|
196
|
+
readCache.delete(firstKey);
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── P1a: Read-only tool classification ──────────────────────────────
|
|
202
|
+
|
|
203
|
+
const READ_ONLY_TOOLS = new Set([
|
|
204
|
+
'read', 'grep', 'glob', 'list',
|
|
205
|
+
'ast_query', 'read_ast_node', 'generate_diff',
|
|
206
|
+
'list_services', 'get_service_status', 'get_service_logs'
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
// ─── Exported helpers ────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
export function summarizeToolResult(result) {
|
|
17
212
|
if (result === null || result === undefined) return 'no output';
|
|
18
213
|
if (typeof result === 'string') {
|
|
19
214
|
const oneLine = result.replace(/\s+/g, ' ').trim();
|
|
@@ -106,7 +301,7 @@ function summarizeToolResult(result) {
|
|
|
106
301
|
return String(result);
|
|
107
302
|
}
|
|
108
303
|
|
|
109
|
-
function trimInline(value, maxLen = 72) {
|
|
304
|
+
export function trimInline(value, maxLen = 72) {
|
|
110
305
|
const s = String(value || '').replace(/\s+/g, ' ').trim();
|
|
111
306
|
if (!s) return '';
|
|
112
307
|
if (s.length <= maxLen) return s;
|
|
@@ -171,6 +366,18 @@ function formatToolDisplayName(name, args) {
|
|
|
171
366
|
return name;
|
|
172
367
|
}
|
|
173
368
|
|
|
369
|
+
// ─── Format a single tool result using per-tool formatter or fallback ──
|
|
370
|
+
|
|
371
|
+
function formatToolResult(toolResult, toolName, args, toolFormatters, toolResultMaxChars) {
|
|
372
|
+
if (toolFormatters && typeof toolFormatters[toolName] === 'function') {
|
|
373
|
+
const formatted = toolFormatters[toolName](toolResult, args);
|
|
374
|
+
if (typeof formatted === 'string') return formatted;
|
|
375
|
+
}
|
|
376
|
+
return compactToolResult(toolResult, toolName, args, toolResultMaxChars);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ─── Main agent loop ────────────────────────────────────────────────
|
|
380
|
+
|
|
174
381
|
export async function runAgentLoop({
|
|
175
382
|
systemPrompt,
|
|
176
383
|
userPrompt,
|
|
@@ -184,7 +391,9 @@ export async function runAgentLoop({
|
|
|
184
391
|
executionMode = 'auto',
|
|
185
392
|
alwaysAllowTools = [],
|
|
186
393
|
requestToolApproval,
|
|
187
|
-
toolResultMaxChars = 12000
|
|
394
|
+
toolResultMaxChars = 12000,
|
|
395
|
+
toolFormatters = {},
|
|
396
|
+
deferredDefinitions = {}
|
|
188
397
|
}) {
|
|
189
398
|
const messages = [];
|
|
190
399
|
if (systemPrompt) {
|
|
@@ -201,12 +410,15 @@ export async function runAgentLoop({
|
|
|
201
410
|
let lastAssistantText = '';
|
|
202
411
|
const alwaysAllowSet = new Set((Array.isArray(alwaysAllowTools) ? alwaysAllowTools : []).map((t) => String(t)));
|
|
203
412
|
|
|
413
|
+
// Mutable tool list — grows as tool_search loads deferred tools
|
|
414
|
+
const activeTools = [...toolDefinitions];
|
|
415
|
+
|
|
204
416
|
for (let step = 0; step < maxSteps; step += 1) {
|
|
205
417
|
if (onEvent) onEvent({ type: 'step:start', step: step + 1 });
|
|
206
418
|
const completion = await requestCompletion({
|
|
207
419
|
model,
|
|
208
420
|
messages,
|
|
209
|
-
tools:
|
|
421
|
+
tools: activeTools
|
|
210
422
|
});
|
|
211
423
|
|
|
212
424
|
const toolCalls = Array.isArray(completion.toolCalls) ? completion.toolCalls : [];
|
|
@@ -242,11 +454,19 @@ export async function runAgentLoop({
|
|
|
242
454
|
return { text: finalText.trim(), messages, steps: step + 1 };
|
|
243
455
|
}
|
|
244
456
|
|
|
245
|
-
|
|
457
|
+
// ─── P1a: Partition into read-only (parallel) and write (serial) ──
|
|
458
|
+
|
|
459
|
+
const callsWithMeta = toolCalls.map((call) => {
|
|
246
460
|
const args = safeJsonParse(call.arguments);
|
|
247
461
|
const toolName = normalizeToolCallName(call.name);
|
|
248
462
|
const displayName = formatToolDisplayName(toolName, args);
|
|
249
|
-
const
|
|
463
|
+
const isReadOnly = READ_ONLY_TOOLS.has(toolName);
|
|
464
|
+
return { call, args, toolName, displayName, isReadOnly };
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Approval checks first — must be done synchronously before any execution
|
|
468
|
+
const approvalResults = new Map();
|
|
469
|
+
for (const { call, toolName, displayName, args } of callsWithMeta) {
|
|
250
470
|
let approved = true;
|
|
251
471
|
if (executionMode === 'normal' && !alwaysAllowSet.has(toolName)) {
|
|
252
472
|
approved = false;
|
|
@@ -260,26 +480,23 @@ export async function runAgentLoop({
|
|
|
260
480
|
approved = Boolean(decision?.approved);
|
|
261
481
|
}
|
|
262
482
|
}
|
|
483
|
+
approvalResults.set(call.id, approved);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Collect results keyed by call.id, then write to messages in original order
|
|
487
|
+
const resultEntries = new Map(); // call.id -> { content, error? }
|
|
488
|
+
|
|
489
|
+
// Helper to execute a single tool call
|
|
490
|
+
async function executeOne({ call, args, toolName, displayName, isReadOnly }) {
|
|
491
|
+
const startedAt = Date.now();
|
|
263
492
|
|
|
264
|
-
if (!
|
|
493
|
+
if (!approvalResults.get(call.id)) {
|
|
265
494
|
if (onEvent) onEvent({ type: 'tool:blocked', name: displayName, id: call.id, arguments: args });
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
495
|
+
return {
|
|
496
|
+
callId: call.id,
|
|
497
|
+
content: JSON.stringify({ blocked: true, reason: 'Tool call requires approval in normal mode' }),
|
|
498
|
+
blocked: true
|
|
270
499
|
};
|
|
271
|
-
messages.push(blockedMessage);
|
|
272
|
-
if (onEvent) {
|
|
273
|
-
onEvent({
|
|
274
|
-
type: 'tool:result',
|
|
275
|
-
name: displayName,
|
|
276
|
-
id: call.id,
|
|
277
|
-
arguments: args,
|
|
278
|
-
content: blockedMessage.content,
|
|
279
|
-
blocked: true
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
continue;
|
|
283
500
|
}
|
|
284
501
|
|
|
285
502
|
if (onEvent) onEvent({ type: 'tool:start', name: displayName, id: call.id, arguments: args });
|
|
@@ -287,6 +504,7 @@ export async function runAgentLoop({
|
|
|
287
504
|
if (!handler) {
|
|
288
505
|
throw new Error(`Unknown tool: ${call.name}`);
|
|
289
506
|
}
|
|
507
|
+
|
|
290
508
|
let toolResult;
|
|
291
509
|
try {
|
|
292
510
|
toolResult = await handler(args);
|
|
@@ -294,58 +512,81 @@ export async function runAgentLoop({
|
|
|
294
512
|
const durationMs = Date.now() - startedAt;
|
|
295
513
|
const message = error instanceof Error ? error.message : String(error);
|
|
296
514
|
if (onEvent) {
|
|
297
|
-
onEvent({
|
|
298
|
-
type: 'tool:error',
|
|
299
|
-
name: displayName,
|
|
300
|
-
id: call.id,
|
|
301
|
-
arguments: args,
|
|
302
|
-
durationMs,
|
|
303
|
-
summary: trimInline(message, 120)
|
|
304
|
-
});
|
|
515
|
+
onEvent({ type: 'tool:error', name: displayName, id: call.id, arguments: args, durationMs, summary: trimInline(message, 120) });
|
|
305
516
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
517
|
+
return {
|
|
518
|
+
callId: call.id,
|
|
519
|
+
content: clipToolResult({ error: message }, toolResultMaxChars),
|
|
520
|
+
error: true
|
|
310
521
|
};
|
|
311
|
-
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const durationMs = Date.now() - startedAt;
|
|
525
|
+
if (onEvent) {
|
|
526
|
+
onEvent({ type: 'tool:end', name: displayName, id: call.id, arguments: args, durationMs, summary: summarizeToolResult(toolResult) });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// P1b: Use per-tool formatter if available, else fallback
|
|
530
|
+
let formatted = formatToolResult(toolResult, toolName, args, toolFormatters, toolResultMaxChars);
|
|
531
|
+
|
|
532
|
+
// P2: If tool_search loaded deferred tools, inject their schemas into activeTools
|
|
533
|
+
if (toolName === 'tool_search' && toolResult && Array.isArray(toolResult.schemas)) {
|
|
534
|
+
for (const schema of toolResult.schemas) {
|
|
535
|
+
const name = schema?.function?.name;
|
|
536
|
+
if (name && !activeTools.some((t) => t?.function?.name === name)) {
|
|
537
|
+
activeTools.push(schema);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// P0: Persist to disk if still large
|
|
543
|
+
formatted = await storeResultIfNeeded(call.id, formatted, toolResult);
|
|
544
|
+
|
|
545
|
+
return { callId: call.id, content: formatted };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Separate read-only and write calls, preserving order
|
|
549
|
+
const readOnlyCalls = callsWithMeta.filter((c) => c.isReadOnly && approvalResults.get(c.call.id));
|
|
550
|
+
const writeCalls = callsWithMeta.filter((c) => !c.isReadOnly || !approvalResults.get(c.call.id));
|
|
551
|
+
|
|
552
|
+
// Execute read-only calls in parallel
|
|
553
|
+
if (readOnlyCalls.length > 0) {
|
|
554
|
+
const readOnlyResults = await Promise.all(readOnlyCalls.map((c) => executeOne(c)));
|
|
555
|
+
for (const r of readOnlyResults) {
|
|
556
|
+
resultEntries.set(r.callId, r);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Execute write calls serially
|
|
561
|
+
for (const c of writeCalls) {
|
|
562
|
+
const r = await executeOne(c);
|
|
563
|
+
resultEntries.set(r.callId, r);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Write results to messages in original tool call order
|
|
567
|
+
for (const { call, displayName, args } of callsWithMeta) {
|
|
568
|
+
const entry = resultEntries.get(call.id);
|
|
569
|
+
if (!entry) continue;
|
|
570
|
+
|
|
571
|
+
if (entry.blocked) {
|
|
572
|
+
messages.push({ role: 'tool', tool_call_id: call.id, content: entry.content });
|
|
312
573
|
if (onEvent) {
|
|
313
|
-
onEvent({
|
|
314
|
-
type: 'tool:result',
|
|
315
|
-
name: displayName,
|
|
316
|
-
id: call.id,
|
|
317
|
-
arguments: args,
|
|
318
|
-
content: toolMessage.content,
|
|
319
|
-
error: true
|
|
320
|
-
});
|
|
574
|
+
onEvent({ type: 'tool:result', name: displayName, id: call.id, arguments: args, content: entry.content, blocked: true });
|
|
321
575
|
}
|
|
322
576
|
continue;
|
|
323
577
|
}
|
|
324
|
-
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
name: displayName,
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
durationMs,
|
|
332
|
-
summary: summarizeToolResult(toolResult)
|
|
333
|
-
});
|
|
578
|
+
|
|
579
|
+
if (entry.error) {
|
|
580
|
+
messages.push({ role: 'tool', tool_call_id: call.id, content: entry.content });
|
|
581
|
+
if (onEvent) {
|
|
582
|
+
onEvent({ type: 'tool:result', name: displayName, id: call.id, arguments: args, content: entry.content, error: true });
|
|
583
|
+
}
|
|
584
|
+
continue;
|
|
334
585
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
tool_call_id: call.id,
|
|
338
|
-
content: clipToolResult(toolResult, toolResultMaxChars)
|
|
339
|
-
};
|
|
340
|
-
messages.push(toolMessage);
|
|
586
|
+
|
|
587
|
+
messages.push({ role: 'tool', tool_call_id: call.id, content: entry.content });
|
|
341
588
|
if (onEvent) {
|
|
342
|
-
onEvent({
|
|
343
|
-
type: 'tool:result',
|
|
344
|
-
name: displayName,
|
|
345
|
-
id: call.id,
|
|
346
|
-
arguments: args,
|
|
347
|
-
content: toolMessage.content
|
|
348
|
-
});
|
|
589
|
+
onEvent({ type: 'tool:result', name: displayName, id: call.id, arguments: args, content: entry.content });
|
|
349
590
|
}
|
|
350
591
|
}
|
|
351
592
|
}
|
package/src/core/chat-runtime.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseInput } from './input-parser.js';
|
|
2
2
|
import { loadCommandsAndSkills, renderCommandPrompt } from './command-loader.js';
|
|
3
|
-
import { runAgentLoop } from './agent-loop.js';
|
|
3
|
+
import { runAgentLoop, setResultDir, clearResultStore } from './agent-loop.js';
|
|
4
4
|
import fs from 'node:fs/promises';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import {
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from './context-compact.js';
|
|
29
29
|
import { buildSystemPromptWithReplyLanguage } from './reply-language.js';
|
|
30
30
|
import { buildSystemPromptWithSoul } from './soul.js';
|
|
31
|
-
import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir } from './paths.js';
|
|
31
|
+
import { getProjectPlansDir, getProjectSpecsDir, getProjectWorkspaceDir, getSessionsDir } from './paths.js';
|
|
32
32
|
import { buildProjectContextSnippet, initializeProjectIndex } from './project-index.js';
|
|
33
33
|
|
|
34
34
|
function toOpenAIMessages(sessionMessages) {
|
|
@@ -1314,7 +1314,7 @@ async function askModel({
|
|
|
1314
1314
|
? `${systemPrompt}\n\n${projectContextSnippet}\n\nUse this project context as lightweight guidance. Prefer tools for fresh verification before assuming details.`
|
|
1315
1315
|
: systemPrompt;
|
|
1316
1316
|
|
|
1317
|
-
const { definitions, handlers } = getBuiltinTools({
|
|
1317
|
+
const { definitions, handlers, formatters, deferredDefinitions } = getBuiltinTools({
|
|
1318
1318
|
workspaceRoot: process.cwd(),
|
|
1319
1319
|
config,
|
|
1320
1320
|
sessionId: session.id,
|
|
@@ -1376,6 +1376,8 @@ async function askModel({
|
|
|
1376
1376
|
alwaysAllowTools:
|
|
1377
1377
|
alwaysAllowTools || config.execution?.always_allow_tools || ['run', 'read', 'write'],
|
|
1378
1378
|
toolResultMaxChars: config.context?.tool_result_max_chars || 12000,
|
|
1379
|
+
toolFormatters: formatters,
|
|
1380
|
+
deferredDefinitions,
|
|
1379
1381
|
requestCompletion: async ({ messages, tools, model: selectedModel }) => {
|
|
1380
1382
|
if (onAgentEvent) onAgentEvent({ type: 'assistant:start' });
|
|
1381
1383
|
return createChatCompletionStream({
|
|
@@ -1727,6 +1729,10 @@ export async function createChatRuntime({
|
|
|
1727
1729
|
const baseSystemPrompt = systemPrompt;
|
|
1728
1730
|
let executionMode = config.execution?.mode || 'auto';
|
|
1729
1731
|
const commands = await loadCommandsAndSkills();
|
|
1732
|
+
|
|
1733
|
+
// Set up tool result store under session directory
|
|
1734
|
+
const sessionResultsDir = path.join(getSessionsDir(), String(currentSession.id));
|
|
1735
|
+
setResultDir(sessionResultsDir);
|
|
1730
1736
|
const compactState = {
|
|
1731
1737
|
backupMessages: null,
|
|
1732
1738
|
autoEnabled: true,
|
|
@@ -2492,6 +2498,7 @@ export async function createChatRuntime({
|
|
|
2492
2498
|
if (!targetId) return { type: 'system', text: 'Usage: /history resume <session_id>' };
|
|
2493
2499
|
const loaded = await loadSession(targetId);
|
|
2494
2500
|
currentSession = loaded;
|
|
2501
|
+
setResultDir(path.join(getSessionsDir(), String(targetId)));
|
|
2495
2502
|
if (!historyIdCache.includes(targetId)) historyIdCache.unshift(targetId);
|
|
2496
2503
|
historySessionCache = [
|
|
2497
2504
|
{ id: targetId, messageCount: Array.isArray(loaded.messages) ? loaded.messages.length : 0 },
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { summarizeToolResult, trimInline } from './agent-loop.js';
|
|
2
|
+
|
|
1
3
|
function textFromContent(content) {
|
|
2
4
|
if (typeof content === 'string') return content;
|
|
3
5
|
if (Array.isArray(content)) {
|
|
@@ -30,11 +32,39 @@ function modeToKeepRecent(mode) {
|
|
|
30
32
|
|
|
31
33
|
function buildLocalSummary(messages) {
|
|
32
34
|
const lines = [];
|
|
33
|
-
const limit =
|
|
35
|
+
const limit = 16;
|
|
34
36
|
for (const msg of messages.slice(-limit)) {
|
|
37
|
+
if (msg.role === 'tool') {
|
|
38
|
+
// Try to parse tool result as JSON for semantic summary
|
|
39
|
+
const text = textFromContent(msg.content);
|
|
40
|
+
let parsed;
|
|
41
|
+
try { parsed = JSON.parse(text); } catch { parsed = null; }
|
|
42
|
+
if (parsed && typeof parsed === 'object') {
|
|
43
|
+
const summary = summarizeToolResult(parsed);
|
|
44
|
+
lines.push(`- tool_result: ${summary}`);
|
|
45
|
+
} else {
|
|
46
|
+
const clipped = text.length > 120 ? `${text.slice(0, 117)}...` : text;
|
|
47
|
+
lines.push(`- tool_result: ${clipped}`);
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (msg.role === 'assistant') {
|
|
52
|
+
const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
|
|
53
|
+
const toolCallCount = Array.isArray(msg.tool_calls) ? msg.tool_calls.length : 0;
|
|
54
|
+
const toolInfo = toolCallCount > 0 ? ` [called ${toolCallCount} tool(s)]` : '';
|
|
55
|
+
const clipped = text.length > 300 ? `${text.slice(0, 297)}...` : text;
|
|
56
|
+
lines.push(`- assistant: ${clipped}${toolInfo}`);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (msg.role === 'user') {
|
|
60
|
+
const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
|
|
61
|
+
const clipped = text.length > 200 ? `${text.slice(0, 197)}...` : text;
|
|
62
|
+
lines.push(`- user: ${clipped}`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
35
65
|
const text = textFromContent(msg.content).replace(/\s+/g, ' ').trim();
|
|
36
66
|
if (!text) continue;
|
|
37
|
-
const clipped = text.length > 160 ? `${text.slice(0,
|
|
67
|
+
const clipped = text.length > 160 ? `${text.slice(0, 157)}...` : text;
|
|
38
68
|
lines.push(`- ${msg.role}: ${clipped}`);
|
|
39
69
|
}
|
|
40
70
|
return `Context Summary\n${lines.join('\n')}`.trim();
|
|
@@ -1,5 +1,26 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import fs from 'node:fs';
|
|
1
3
|
import { getShellSystemPrompt } from './shell-profile.js';
|
|
2
4
|
|
|
5
|
+
function getEnvBlock() {
|
|
6
|
+
const cwd = process.cwd();
|
|
7
|
+
let isGitRepo = false;
|
|
8
|
+
try {
|
|
9
|
+
fs.accessSync(`${cwd}/.git`);
|
|
10
|
+
isGitRepo = true;
|
|
11
|
+
} catch {}
|
|
12
|
+
|
|
13
|
+
return `<env>
|
|
14
|
+
Working directory: ${cwd}
|
|
15
|
+
Is directory a git repo: ${isGitRepo ? 'Yes' : 'No'}
|
|
16
|
+
Platform: ${process.platform}
|
|
17
|
+
Shell: ${os.userInfo().shell || 'unknown'}
|
|
18
|
+
OS Version: ${os.version || os.release()}
|
|
19
|
+
</env>`;
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
export function buildDefaultSystemPrompt(config = {}) {
|
|
4
|
-
return `${getShellSystemPrompt(config?.shell?.default)}
|
|
23
|
+
return `${getShellSystemPrompt(config?.shell?.default)}
|
|
24
|
+
|
|
25
|
+
${getEnvBlock()}`;
|
|
5
26
|
}
|
|
@@ -118,5 +118,41 @@ export function getEffectivePolicy(config) {
|
|
|
118
118
|
|
|
119
119
|
export function getShellSystemPrompt(value) {
|
|
120
120
|
const profile = getShellProfile(value);
|
|
121
|
-
return `You are CodeMini CLI
|
|
121
|
+
return `You are CodeMini CLI, an AI coding assistant running in a ${profile.label} shell environment.
|
|
122
|
+
|
|
123
|
+
# Using your tools
|
|
124
|
+
|
|
125
|
+
ALWAYS prefer dedicated tools over raw shell commands:
|
|
126
|
+
- Use read to inspect files — NEVER use cat, head, or tail via run
|
|
127
|
+
- Use grep to search file contents — NEVER use grep or rg via run
|
|
128
|
+
- Use glob to find files by pattern — NEVER use find via run
|
|
129
|
+
- Use edit to modify existing files — this is the DEFAULT path for code changes
|
|
130
|
+
- Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files)
|
|
131
|
+
- Use patch to apply unified diffs
|
|
132
|
+
- Use run for one-shot shell commands: install, build, test, or other finite tasks
|
|
133
|
+
- For long-running processes (dev servers, watchers), use start_service instead of run
|
|
134
|
+
|
|
135
|
+
For structural code edits (functions, classes, methods), use the AST-first workflow:
|
|
136
|
+
ast_query → read_ast_node → edit with ast_target and kind=replace_block.
|
|
137
|
+
Fall back to plain grep/read/edit only when AST is not appropriate.
|
|
138
|
+
|
|
139
|
+
For services: use start_service to launch, list_services/get_service_status/get_service_logs to monitor, stop_service to stop.
|
|
140
|
+
|
|
141
|
+
Some tools are loaded on demand. If a needed tool is not listed, call tool_search first to load it.
|
|
142
|
+
|
|
143
|
+
# Doing tasks
|
|
144
|
+
|
|
145
|
+
- If a command or tool is blocked or fails, inspect the error and retry with allowed commands or tools
|
|
146
|
+
- For AST-scoped edits, if edit rejects due to missing or stale ast_target, fix arguments and retry
|
|
147
|
+
- Do not claim filesystem access is impossible unless search/read tools also fail
|
|
148
|
+
- Prefer editing existing files over creating new ones
|
|
149
|
+
- Do not add comments, docstrings, or type annotations to code you did not change
|
|
150
|
+
- Do not add features or refactor code beyond what was asked
|
|
151
|
+
|
|
152
|
+
# Tone and style
|
|
153
|
+
|
|
154
|
+
- Be concise. Go straight to the point
|
|
155
|
+
- Do not restate what the user said
|
|
156
|
+
- When referencing code, use file_path:line_number format
|
|
157
|
+
- Only use emojis if the user explicitly requests it`;
|
|
122
158
|
}
|
package/src/core/tools.js
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { evaluateCommandPolicy } from './command-policy.js';
|
|
16
16
|
import { queryAst, readAstNode, resolveAstTarget } from './ast.js';
|
|
17
17
|
import { initializeProjectIndex, refreshIndexedFile } from './project-index.js';
|
|
18
|
+
import { checkReadDedup } from './agent-loop.js';
|
|
18
19
|
|
|
19
20
|
const SKIP_DIRS = new Set(['.git', 'node_modules', '.codemini', '.codemini-global', 'dist', 'coverage']);
|
|
20
21
|
const TEXT_EXTENSIONS = new Set([
|
|
@@ -722,6 +723,26 @@ async function readFile(root, args) {
|
|
|
722
723
|
truncated = true;
|
|
723
724
|
}
|
|
724
725
|
|
|
726
|
+
// Read deduplication: if same path+range+mtime was read before, return a short stub
|
|
727
|
+
const isDuplicate = checkReadDedup(
|
|
728
|
+
args?.path,
|
|
729
|
+
startLine,
|
|
730
|
+
endLine,
|
|
731
|
+
stat.mtimeMs
|
|
732
|
+
);
|
|
733
|
+
if (isDuplicate) {
|
|
734
|
+
return {
|
|
735
|
+
path: args?.path,
|
|
736
|
+
phase: 'content',
|
|
737
|
+
start_line: startLine,
|
|
738
|
+
end_line: endLine,
|
|
739
|
+
total_lines: totalLines,
|
|
740
|
+
truncated: false,
|
|
741
|
+
unchanged: true,
|
|
742
|
+
content: `File unchanged since last read. The content from the earlier read tool_result in this conversation is still current -- refer to that instead of re-reading.`
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
725
746
|
return {
|
|
726
747
|
path: args?.path,
|
|
727
748
|
phase: 'content',
|
|
@@ -1711,22 +1732,22 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1711
1732
|
return null;
|
|
1712
1733
|
}
|
|
1713
1734
|
};
|
|
1714
|
-
const
|
|
1735
|
+
const primaryDefinitions = [
|
|
1715
1736
|
{
|
|
1716
1737
|
type: 'function',
|
|
1717
1738
|
function: {
|
|
1718
1739
|
name: 'read',
|
|
1719
1740
|
description:
|
|
1720
|
-
'
|
|
1741
|
+
'Read a file. Call once for metadata and a read_token, then again with include_content=true and the same token to get content. Use this before editing.',
|
|
1721
1742
|
parameters: {
|
|
1722
1743
|
type: 'object',
|
|
1723
1744
|
properties: {
|
|
1724
|
-
path: { type: 'string' },
|
|
1725
|
-
start_line: { type: 'number' },
|
|
1726
|
-
end_line: { type: 'number' },
|
|
1727
|
-
max_chars: { type: 'number' },
|
|
1728
|
-
include_content: { type: 'boolean' },
|
|
1729
|
-
read_token: { type: 'string' }
|
|
1745
|
+
path: { type: 'string', description: 'File path to read' },
|
|
1746
|
+
start_line: { type: 'number', description: '1-based start line' },
|
|
1747
|
+
end_line: { type: 'number', description: 'Inclusive end line' },
|
|
1748
|
+
max_chars: { type: 'number', description: 'Max chars to return' },
|
|
1749
|
+
include_content: { type: 'boolean', description: 'Set true on the second call' },
|
|
1750
|
+
read_token: { type: 'string', description: 'Token from the first call' }
|
|
1730
1751
|
},
|
|
1731
1752
|
required: ['path']
|
|
1732
1753
|
}
|
|
@@ -1736,18 +1757,19 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1736
1757
|
type: 'function',
|
|
1737
1758
|
function: {
|
|
1738
1759
|
name: 'grep',
|
|
1739
|
-
description:
|
|
1760
|
+
description:
|
|
1761
|
+
'Search file contents. Use this for code search instead of grep or rg in run.',
|
|
1740
1762
|
parameters: {
|
|
1741
1763
|
type: 'object',
|
|
1742
1764
|
properties: {
|
|
1743
|
-
pattern: { type: 'string' },
|
|
1744
|
-
query: { type: 'string' },
|
|
1745
|
-
path: { type: 'string' },
|
|
1746
|
-
regex: { type: 'boolean' },
|
|
1747
|
-
case_sensitive: { type: 'boolean' },
|
|
1748
|
-
max_results: { type: 'number' },
|
|
1749
|
-
language: { type: 'string' },
|
|
1750
|
-
file_types: { type: 'array', items: { type: 'string' } }
|
|
1765
|
+
pattern: { type: 'string', description: 'Search pattern' },
|
|
1766
|
+
query: { type: 'string', description: 'Alias for pattern' },
|
|
1767
|
+
path: { type: 'string', description: 'Directory or file to search' },
|
|
1768
|
+
regex: { type: 'boolean', description: 'Treat pattern as regex' },
|
|
1769
|
+
case_sensitive: { type: 'boolean', description: 'Case-sensitive matching' },
|
|
1770
|
+
max_results: { type: 'number', description: 'Max matches to return' },
|
|
1771
|
+
language: { type: 'string', description: 'Filter by language' },
|
|
1772
|
+
file_types: { type: 'array', items: { type: 'string' }, description: 'Filter by file glob' }
|
|
1751
1773
|
},
|
|
1752
1774
|
required: ['pattern']
|
|
1753
1775
|
}
|
|
@@ -1757,14 +1779,15 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1757
1779
|
type: 'function',
|
|
1758
1780
|
function: {
|
|
1759
1781
|
name: 'glob',
|
|
1760
|
-
description:
|
|
1782
|
+
description:
|
|
1783
|
+
'Find files by glob pattern. Use this for file discovery instead of find in run.',
|
|
1761
1784
|
parameters: {
|
|
1762
1785
|
type: 'object',
|
|
1763
1786
|
properties: {
|
|
1764
|
-
pattern: { type: 'string' },
|
|
1765
|
-
path: { type: 'string' },
|
|
1766
|
-
include_hidden: { type: 'boolean' },
|
|
1767
|
-
max_results: { type: 'number' }
|
|
1787
|
+
pattern: { type: 'string', description: 'Glob pattern' },
|
|
1788
|
+
path: { type: 'string', description: 'Directory to search' },
|
|
1789
|
+
include_hidden: { type: 'boolean', description: 'Include dotfiles' },
|
|
1790
|
+
max_results: { type: 'number', description: 'Max results' }
|
|
1768
1791
|
},
|
|
1769
1792
|
required: ['pattern']
|
|
1770
1793
|
}
|
|
@@ -1774,12 +1797,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1774
1797
|
type: 'function',
|
|
1775
1798
|
function: {
|
|
1776
1799
|
name: 'list',
|
|
1777
|
-
description: 'List files and directories in a workspace path',
|
|
1800
|
+
description: 'List files and directories in a workspace path.',
|
|
1778
1801
|
parameters: {
|
|
1779
1802
|
type: 'object',
|
|
1780
1803
|
properties: {
|
|
1781
|
-
path: { type: 'string' },
|
|
1782
|
-
include_hidden: { type: 'boolean' }
|
|
1804
|
+
path: { type: 'string', description: 'Directory path to list' },
|
|
1805
|
+
include_hidden: { type: 'boolean', description: 'Include dotfiles' }
|
|
1783
1806
|
}
|
|
1784
1807
|
}
|
|
1785
1808
|
}
|
|
@@ -1789,24 +1812,24 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1789
1812
|
function: {
|
|
1790
1813
|
name: 'edit',
|
|
1791
1814
|
description:
|
|
1792
|
-
'
|
|
1815
|
+
'Edit existing files. Use block edits, exact replacements, or anchored inserts. When ast_target is provided, keep the edit constrained to that node. Prefer this over write for code changes.',
|
|
1793
1816
|
parameters: {
|
|
1794
1817
|
type: 'object',
|
|
1795
1818
|
properties: {
|
|
1796
|
-
file: { type: 'string' },
|
|
1797
|
-
path: { type: 'string' },
|
|
1798
|
-
new_content: { type: 'string' },
|
|
1799
|
-
old_text: { type: 'string' },
|
|
1800
|
-
new_text: { type: 'string' },
|
|
1801
|
-
anchor_text: { type: 'string' },
|
|
1802
|
-
content: { type: 'string' },
|
|
1803
|
-
position: { type: 'string' },
|
|
1804
|
-
kind: { type: 'string' },
|
|
1805
|
-
target: { type: 'object' },
|
|
1806
|
-
ast_target: { type: 'object' },
|
|
1807
|
-
symbol: { type: 'string' },
|
|
1808
|
-
line: { type: 'number' },
|
|
1809
|
-
edit: { type: 'object' }
|
|
1819
|
+
file: { type: 'string', description: 'File path to edit' },
|
|
1820
|
+
path: { type: 'string', description: 'Alias for file' },
|
|
1821
|
+
new_content: { type: 'string', description: 'Replacement content' },
|
|
1822
|
+
old_text: { type: 'string', description: 'Exact text to replace' },
|
|
1823
|
+
new_text: { type: 'string', description: 'Replacement text' },
|
|
1824
|
+
anchor_text: { type: 'string', description: 'Anchor text for inserts' },
|
|
1825
|
+
content: { type: 'string', description: 'Content to insert or append' },
|
|
1826
|
+
position: { type: 'string', description: 'before or after' },
|
|
1827
|
+
kind: { type: 'string', description: 'replace_block, replace_text, insert_before, insert_after, or rewrite_file' },
|
|
1828
|
+
target: { type: 'object', description: 'Location object with symbol or line info' },
|
|
1829
|
+
ast_target: { type: 'object', description: 'AST target from ast_query' },
|
|
1830
|
+
symbol: { type: 'string', description: 'Symbol to target' },
|
|
1831
|
+
line: { type: 'number', description: 'Line to target' },
|
|
1832
|
+
edit: { type: 'object', description: 'Structured edit input' }
|
|
1810
1833
|
},
|
|
1811
1834
|
required: ['file']
|
|
1812
1835
|
}
|
|
@@ -1815,77 +1838,96 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1815
1838
|
{
|
|
1816
1839
|
type: 'function',
|
|
1817
1840
|
function: {
|
|
1818
|
-
name: '
|
|
1841
|
+
name: 'write',
|
|
1819
1842
|
description:
|
|
1820
|
-
'
|
|
1843
|
+
'Create a new file or overwrite a file. Use this for new files or full rewrites. Prefer edit for existing code.',
|
|
1821
1844
|
parameters: {
|
|
1822
1845
|
type: 'object',
|
|
1823
1846
|
properties: {
|
|
1824
|
-
path: { type: 'string' },
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
max_results: { type: 'number' }
|
|
1847
|
+
path: { type: 'string', description: 'File path to create or overwrite' },
|
|
1848
|
+
content: { type: 'string', description: 'Content to write' },
|
|
1849
|
+
append: { type: 'boolean', description: 'Append instead of overwrite' },
|
|
1850
|
+
full_file_rewrite: { type: 'boolean', description: 'Set true for whole-file rewrites' }
|
|
1829
1851
|
},
|
|
1830
|
-
required: ['path', '
|
|
1852
|
+
required: ['path', 'content']
|
|
1831
1853
|
}
|
|
1832
1854
|
}
|
|
1833
1855
|
},
|
|
1834
1856
|
{
|
|
1835
1857
|
type: 'function',
|
|
1836
1858
|
function: {
|
|
1837
|
-
name: '
|
|
1859
|
+
name: 'run',
|
|
1838
1860
|
description:
|
|
1839
|
-
'
|
|
1861
|
+
'Run a one-shot shell command such as install, build, or test. Do not use for long-running services or file search.',
|
|
1840
1862
|
parameters: {
|
|
1841
1863
|
type: 'object',
|
|
1842
1864
|
properties: {
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
ast_target: { type: 'object' }
|
|
1865
|
+
command: { type: 'string', description: 'Shell command to execute' },
|
|
1866
|
+
timeout: { type: 'number', description: 'Timeout in milliseconds' }
|
|
1846
1867
|
},
|
|
1847
|
-
required: ['
|
|
1868
|
+
required: ['command']
|
|
1848
1869
|
}
|
|
1849
1870
|
}
|
|
1850
1871
|
},
|
|
1851
1872
|
{
|
|
1852
1873
|
type: 'function',
|
|
1853
1874
|
function: {
|
|
1854
|
-
name: '
|
|
1875
|
+
name: 'tool_search',
|
|
1876
|
+
description:
|
|
1877
|
+
'Load one deferred tool schema by name. Use this when a needed tool is not in the current tool list.',
|
|
1878
|
+
parameters: {
|
|
1879
|
+
type: 'object',
|
|
1880
|
+
properties: {
|
|
1881
|
+
query: { type: 'string', description: 'Tool name to load, or "all"' }
|
|
1882
|
+
},
|
|
1883
|
+
required: ['query']
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
];
|
|
1888
|
+
|
|
1889
|
+
const deferredDefinitions = {
|
|
1890
|
+
ast_query: {
|
|
1891
|
+
type: 'function',
|
|
1892
|
+
function: {
|
|
1893
|
+
name: 'ast_query',
|
|
1855
1894
|
description:
|
|
1856
|
-
'
|
|
1895
|
+
'Run a Tree-sitter query on a code file and return ast_target objects for node-scoped reads or edits.',
|
|
1857
1896
|
parameters: {
|
|
1858
1897
|
type: 'object',
|
|
1859
1898
|
properties: {
|
|
1860
1899
|
path: { type: 'string' },
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1900
|
+
language: { type: 'string' },
|
|
1901
|
+
query: { type: 'string' },
|
|
1902
|
+
capture_name: { type: 'string' },
|
|
1903
|
+
max_results: { type: 'number' }
|
|
1864
1904
|
},
|
|
1865
|
-
required: ['path', '
|
|
1905
|
+
required: ['path', 'query']
|
|
1866
1906
|
}
|
|
1867
1907
|
}
|
|
1868
1908
|
},
|
|
1869
|
-
{
|
|
1909
|
+
read_ast_node: {
|
|
1870
1910
|
type: 'function',
|
|
1871
1911
|
function: {
|
|
1872
|
-
name: '
|
|
1912
|
+
name: 'read_ast_node',
|
|
1873
1913
|
description:
|
|
1874
|
-
'
|
|
1914
|
+
'Read a previously selected AST node with compact structural context.',
|
|
1875
1915
|
parameters: {
|
|
1876
1916
|
type: 'object',
|
|
1877
1917
|
properties: {
|
|
1878
|
-
|
|
1918
|
+
path: { type: 'string' },
|
|
1919
|
+
language: { type: 'string' },
|
|
1920
|
+
ast_target: { type: 'object' }
|
|
1879
1921
|
},
|
|
1880
|
-
required: ['
|
|
1922
|
+
required: ['path', 'ast_target']
|
|
1881
1923
|
}
|
|
1882
1924
|
}
|
|
1883
1925
|
},
|
|
1884
|
-
{
|
|
1926
|
+
generate_diff: {
|
|
1885
1927
|
type: 'function',
|
|
1886
1928
|
function: {
|
|
1887
1929
|
name: 'generate_diff',
|
|
1888
|
-
description: 'Generate a unified diff
|
|
1930
|
+
description: 'Generate a unified diff for proposed content',
|
|
1889
1931
|
parameters: {
|
|
1890
1932
|
type: 'object',
|
|
1891
1933
|
properties: {
|
|
@@ -1896,11 +1938,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1896
1938
|
}
|
|
1897
1939
|
}
|
|
1898
1940
|
},
|
|
1899
|
-
{
|
|
1941
|
+
patch: {
|
|
1900
1942
|
type: 'function',
|
|
1901
1943
|
function: {
|
|
1902
1944
|
name: 'patch',
|
|
1903
|
-
description: 'Apply one or more unified diff hunks to files
|
|
1945
|
+
description: 'Apply one or more unified diff hunks to workspace files',
|
|
1904
1946
|
parameters: {
|
|
1905
1947
|
type: 'object',
|
|
1906
1948
|
properties: {
|
|
@@ -1911,12 +1953,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1911
1953
|
}
|
|
1912
1954
|
}
|
|
1913
1955
|
},
|
|
1914
|
-
{
|
|
1956
|
+
start_service: {
|
|
1915
1957
|
type: 'function',
|
|
1916
1958
|
function: {
|
|
1917
1959
|
name: 'start_service',
|
|
1918
1960
|
description:
|
|
1919
|
-
'Start a long-running local service
|
|
1961
|
+
'Start a long-running local service and return a compact handle.',
|
|
1920
1962
|
parameters: {
|
|
1921
1963
|
type: 'object',
|
|
1922
1964
|
properties: {
|
|
@@ -1939,22 +1981,22 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1939
1981
|
}
|
|
1940
1982
|
}
|
|
1941
1983
|
},
|
|
1942
|
-
{
|
|
1984
|
+
list_services: {
|
|
1943
1985
|
type: 'function',
|
|
1944
1986
|
function: {
|
|
1945
1987
|
name: 'list_services',
|
|
1946
|
-
description: 'List
|
|
1988
|
+
description: 'List tracked local services and their current status.',
|
|
1947
1989
|
parameters: {
|
|
1948
1990
|
type: 'object',
|
|
1949
1991
|
properties: {}
|
|
1950
1992
|
}
|
|
1951
1993
|
}
|
|
1952
1994
|
},
|
|
1953
|
-
{
|
|
1995
|
+
get_service_status: {
|
|
1954
1996
|
type: 'function',
|
|
1955
1997
|
function: {
|
|
1956
1998
|
name: 'get_service_status',
|
|
1957
|
-
description: 'Get the
|
|
1999
|
+
description: 'Get the status of a started service.',
|
|
1958
2000
|
parameters: {
|
|
1959
2001
|
type: 'object',
|
|
1960
2002
|
properties: {
|
|
@@ -1964,11 +2006,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1964
2006
|
}
|
|
1965
2007
|
}
|
|
1966
2008
|
},
|
|
1967
|
-
{
|
|
2009
|
+
get_service_logs: {
|
|
1968
2010
|
type: 'function',
|
|
1969
2011
|
function: {
|
|
1970
2012
|
name: 'get_service_logs',
|
|
1971
|
-
description: 'Read recent logs from a
|
|
2013
|
+
description: 'Read recent logs from a started service.',
|
|
1972
2014
|
parameters: {
|
|
1973
2015
|
type: 'object',
|
|
1974
2016
|
properties: {
|
|
@@ -1980,11 +2022,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1980
2022
|
}
|
|
1981
2023
|
}
|
|
1982
2024
|
},
|
|
1983
|
-
{
|
|
2025
|
+
stop_service: {
|
|
1984
2026
|
type: 'function',
|
|
1985
2027
|
function: {
|
|
1986
2028
|
name: 'stop_service',
|
|
1987
|
-
description: 'Stop a
|
|
2029
|
+
description: 'Stop a started service.',
|
|
1988
2030
|
parameters: {
|
|
1989
2031
|
type: 'object',
|
|
1990
2032
|
properties: {
|
|
@@ -1994,7 +2036,9 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1994
2036
|
}
|
|
1995
2037
|
}
|
|
1996
2038
|
}
|
|
1997
|
-
|
|
2039
|
+
};
|
|
2040
|
+
|
|
2041
|
+
const definitions = [...primaryDefinitions];
|
|
1998
2042
|
|
|
1999
2043
|
const handlers = {
|
|
2000
2044
|
read: (args) =>
|
|
@@ -2052,8 +2096,205 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2052
2096
|
list_services: () => listServices(workspaceRoot),
|
|
2053
2097
|
get_service_status: (args) => getServiceStatus(workspaceRoot, args),
|
|
2054
2098
|
get_service_logs: (args) => getServiceLogs(workspaceRoot, args),
|
|
2055
|
-
stop_service: (args) => stopService(workspaceRoot, args)
|
|
2099
|
+
stop_service: (args) => stopService(workspaceRoot, args),
|
|
2100
|
+
tool_search: (args) => {
|
|
2101
|
+
const query = String(args?.query || '').trim().toLowerCase();
|
|
2102
|
+
if (query === 'all') {
|
|
2103
|
+
const all = Object.values(deferredDefinitions);
|
|
2104
|
+
return {
|
|
2105
|
+
loaded: Object.keys(deferredDefinitions),
|
|
2106
|
+
schemas: all,
|
|
2107
|
+
message: `Loaded all ${all.length} deferred tools. You can now call them directly.`
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
const match = Object.entries(deferredDefinitions).find(([name]) => name === query);
|
|
2111
|
+
if (!match) {
|
|
2112
|
+
const available = Object.keys(deferredDefinitions).join(', ');
|
|
2113
|
+
return { error: `Unknown tool: "${query}". Available deferred tools: ${available}` };
|
|
2114
|
+
}
|
|
2115
|
+
return {
|
|
2116
|
+
loaded: [match[0]],
|
|
2117
|
+
schemas: [match[1]],
|
|
2118
|
+
message: `Loaded tool "${match[0]}". You can now call it in your next response.`
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
|
|
2123
|
+
const formatters = {
|
|
2124
|
+
read(result) {
|
|
2125
|
+
if (typeof result === 'string') return result;
|
|
2126
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2127
|
+
// Phase 1 metadata: small, return as-is
|
|
2128
|
+
if (result.phase === 'metadata') {
|
|
2129
|
+
return JSON.stringify(result);
|
|
2130
|
+
}
|
|
2131
|
+
// Phase 2 content: structured header + head/tail content
|
|
2132
|
+
if (result.phase === 'content') {
|
|
2133
|
+
const header = `[File: ${result.path}, lines ${result.start_line || 1}-${result.end_line || '?'}${result.total_lines ? ` of ${result.total_lines}` : ''}${result.truncated ? ', truncated' : ''}]`;
|
|
2134
|
+
const content = result.content || '';
|
|
2135
|
+
if (typeof content !== 'string' || content.length <= 3000) {
|
|
2136
|
+
return `${header}\n${content}`;
|
|
2137
|
+
}
|
|
2138
|
+
const headLen = 1800;
|
|
2139
|
+
const tailLen = 800;
|
|
2140
|
+
return `${header}\n${content.slice(0, headLen)}\n... [omitted ${content.length - headLen - tailLen} chars] ...\n${content.slice(-tailLen)}`;
|
|
2141
|
+
}
|
|
2142
|
+
return JSON.stringify(result);
|
|
2143
|
+
},
|
|
2144
|
+
|
|
2145
|
+
grep(result) {
|
|
2146
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2147
|
+
const { pattern, matches, truncated } = result;
|
|
2148
|
+
const header = pattern ? `[grep: "${pattern}"]` : '';
|
|
2149
|
+
if (!Array.isArray(matches) || matches.length === 0) return `${header}\nNo matches found.`;
|
|
2150
|
+
if (matches.length <= 30) {
|
|
2151
|
+
const lines = matches.map((m) => `${m.path}:${m.line}: ${String(m.preview || '').slice(0, 120)}`);
|
|
2152
|
+
return `${header}\n${lines.join('\n')}`;
|
|
2153
|
+
}
|
|
2154
|
+
const shown = matches.slice(0, 30).map((m) => `${m.path}:${m.line}: ${String(m.preview || '').slice(0, 120)}`);
|
|
2155
|
+
return `${header}\n${shown.join('\n')}\n... and ${matches.length - 30} more matches [total: ${matches.length}${truncated ? ', results were truncated' : ''}]`;
|
|
2156
|
+
},
|
|
2157
|
+
|
|
2158
|
+
glob(result) {
|
|
2159
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2160
|
+
const { pattern, matches, truncated } = result;
|
|
2161
|
+
const header = pattern ? `[glob: "${pattern}"]` : '';
|
|
2162
|
+
if (!Array.isArray(matches) || matches.length === 0) return `${header}\nNo files found.`;
|
|
2163
|
+
if (matches.length <= 50) {
|
|
2164
|
+
return `${header}\n${matches.join('\n')}`;
|
|
2165
|
+
}
|
|
2166
|
+
const shown = matches.slice(0, 50);
|
|
2167
|
+
return `${header}\n${shown.join('\n')}\n... and ${matches.length - 50} more files [total: ${matches.length}${truncated ? ', results were truncated' : ''}]`;
|
|
2168
|
+
},
|
|
2169
|
+
|
|
2170
|
+
list(result) {
|
|
2171
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2172
|
+
if (!Array.isArray(result.items)) return JSON.stringify(result);
|
|
2173
|
+
const header = result.path ? `[${result.path}]` : '';
|
|
2174
|
+
const dirs = result.items.filter((i) => i.type === 'dir').map((i) => `${i.name}/`);
|
|
2175
|
+
const files = result.items.filter((i) => i.type === 'file').map((i) => i.name);
|
|
2176
|
+
return `${header}\n${dirs.join('\n')}${dirs.length && files.length ? '\n' : ''}${files.join('\n')}`;
|
|
2177
|
+
},
|
|
2178
|
+
|
|
2179
|
+
edit(result) {
|
|
2180
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2181
|
+
const p = result.path || '';
|
|
2182
|
+
const action = result.action || '';
|
|
2183
|
+
const line = result.changed_line || 0;
|
|
2184
|
+
const summary = `${action} ${p}${line > 0 ? ` @L${line}` : ''}`;
|
|
2185
|
+
const diffPreview = result.diff_preview || '';
|
|
2186
|
+
if (diffPreview) {
|
|
2187
|
+
const trimmed = diffPreview.length > 600 ? `${diffPreview.slice(0, 597)}...` : diffPreview;
|
|
2188
|
+
return `${summary}\n${trimmed}`;
|
|
2189
|
+
}
|
|
2190
|
+
return summary + (result.ok !== false ? '' : ` [FAILED: ${result.error || 'unknown'}]`);
|
|
2191
|
+
},
|
|
2192
|
+
|
|
2193
|
+
write(result) {
|
|
2194
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2195
|
+
const p = result.path || '';
|
|
2196
|
+
const action = result.action || 'write';
|
|
2197
|
+
const line = result.changed_line || 0;
|
|
2198
|
+
const summary = `${action} ${p}${line > 0 ? ` @L${line}` : ''}`;
|
|
2199
|
+
const diffPreview = result.diff_preview || '';
|
|
2200
|
+
if (diffPreview) {
|
|
2201
|
+
const trimmed = diffPreview.length > 600 ? `${diffPreview.slice(0, 597)}...` : diffPreview;
|
|
2202
|
+
return `${summary}\n${trimmed}`;
|
|
2203
|
+
}
|
|
2204
|
+
return summary;
|
|
2205
|
+
},
|
|
2206
|
+
|
|
2207
|
+
run(result) {
|
|
2208
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2209
|
+
const command = String(result.command || '').slice(0, 200);
|
|
2210
|
+
const stdout = String(result.stdout || '').slice(0, 500);
|
|
2211
|
+
const stderr = String(result.stderr || '').slice(0, 500);
|
|
2212
|
+
const code = result.code ?? 0;
|
|
2213
|
+
const parts = [`[exit: ${code}]`];
|
|
2214
|
+
if (command) parts.push(`command: ${command}`);
|
|
2215
|
+
if (stdout) parts.push(`stdout:\n${stdout}`);
|
|
2216
|
+
if (stderr) parts.push(`stderr:\n${stderr}`);
|
|
2217
|
+
return parts.join('\n');
|
|
2218
|
+
},
|
|
2219
|
+
|
|
2220
|
+
generate_diff(result) {
|
|
2221
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2222
|
+
const p = result.path || '';
|
|
2223
|
+
const diff = result.diff || '';
|
|
2224
|
+
if (diff.length <= 2000) return `${p ? `[${p}]\n` : ''}${diff}`;
|
|
2225
|
+
return `${p ? `[${p}]\n` : ''}${diff.slice(0, 1997)}...\n[diff truncated: ${diff.length} chars total]`;
|
|
2226
|
+
},
|
|
2227
|
+
|
|
2228
|
+
patch(result) {
|
|
2229
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2230
|
+
if (Array.isArray(result.files)) {
|
|
2231
|
+
const names = result.files.slice(0, 10).map((f) => typeof f === 'string' ? f : f.path || '?');
|
|
2232
|
+
return `patched ${result.files.length} file(s): ${names.join(', ')}${result.files.length > 10 ? ` ... +${result.files.length - 10} more` : ''}`;
|
|
2233
|
+
}
|
|
2234
|
+
const p = result.path || '';
|
|
2235
|
+
const line = result.changed_line || 0;
|
|
2236
|
+
return `patched ${p}${line > 0 ? ` @L${line}` : ''}${result.ok === false ? ` [FAILED: ${result.error || ''}]` : ''}`;
|
|
2237
|
+
},
|
|
2238
|
+
|
|
2239
|
+
ast_query(result) {
|
|
2240
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2241
|
+
if (!Array.isArray(result.matches)) return JSON.stringify(result);
|
|
2242
|
+
const header = `[ast_query: ${result.matches.length} match(es)]`;
|
|
2243
|
+
const lines = result.matches.slice(0, 20).map((m) => {
|
|
2244
|
+
const name = m.name || m.ast_target?.name || '?';
|
|
2245
|
+
const kind = m.kind || m.ast_target?.kind || '?';
|
|
2246
|
+
return ` ${kind} ${name}`;
|
|
2247
|
+
});
|
|
2248
|
+
return `${header}\n${lines.join('\n')}${result.matches.length > 20 ? `\n... +${result.matches.length - 20} more` : ''}`;
|
|
2249
|
+
},
|
|
2250
|
+
|
|
2251
|
+
read_ast_node(result) {
|
|
2252
|
+
if (typeof result === 'string') return result;
|
|
2253
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2254
|
+
const name = result.name || '';
|
|
2255
|
+
const kind = result.kind || '';
|
|
2256
|
+
const content = result.content || result.source || '';
|
|
2257
|
+
const header = `${kind} ${name}`;
|
|
2258
|
+
if (typeof content !== 'string' || content.length <= 2000) return `${header}\n${content}`;
|
|
2259
|
+
return `${header}\n${content.slice(0, 1200)}\n... [omitted ${content.length - 1600} chars] ...\n${content.slice(-400)}`;
|
|
2260
|
+
},
|
|
2261
|
+
|
|
2262
|
+
start_service(result) {
|
|
2263
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2264
|
+
const tid = result.task_id || '';
|
|
2265
|
+
const status = result.status || 'unknown';
|
|
2266
|
+
const confirmed = result.startup_confirmed ? 'ready' : 'starting';
|
|
2267
|
+
const url = result.url || '';
|
|
2268
|
+
return `${tid} ${status} (${confirmed})${url ? ` -> ${url}` : ''}`;
|
|
2269
|
+
},
|
|
2270
|
+
|
|
2271
|
+
list_services(result) {
|
|
2272
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2273
|
+
if (!Array.isArray(result.services)) return JSON.stringify(result);
|
|
2274
|
+
if (result.services.length === 0) return 'No services running.';
|
|
2275
|
+
return result.services.map((s) => `${s.task_id || '?'} ${s.status || 'unknown'}${s.command ? ` (${s.command.slice(0, 60)})` : ''}`).join('\n');
|
|
2276
|
+
},
|
|
2277
|
+
|
|
2278
|
+
get_service_status(result) {
|
|
2279
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2280
|
+
const tid = result.task_id || '';
|
|
2281
|
+
const status = result.status || 'unknown';
|
|
2282
|
+
const url = result.url || '';
|
|
2283
|
+
const logs = Array.isArray(result.recent_logs) ? result.recent_logs.slice(-3).join('\n') : '';
|
|
2284
|
+
return `${tid} ${status}${url ? ` -> ${url}` : ''}${logs ? `\n${logs}` : ''}`;
|
|
2285
|
+
},
|
|
2286
|
+
|
|
2287
|
+
get_service_logs(result) {
|
|
2288
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2289
|
+
const logs = Array.isArray(result.recent_logs) ? result.recent_logs.join('\n') : '';
|
|
2290
|
+
return logs || 'No recent logs.';
|
|
2291
|
+
},
|
|
2292
|
+
|
|
2293
|
+
stop_service(result) {
|
|
2294
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2295
|
+
return `${result.task_id || '?'} stopped${result.exit_code != null ? ` (exit ${result.exit_code})` : ''}`;
|
|
2296
|
+
}
|
|
2056
2297
|
};
|
|
2057
2298
|
|
|
2058
|
-
return { definitions, handlers };
|
|
2299
|
+
return { definitions, handlers, formatters, deferredDefinitions };
|
|
2059
2300
|
}
|