codemini-cli 0.3.1 → 0.3.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.
@@ -0,0 +1,388 @@
1
+ function extractTextContent(content) {
2
+ if (typeof content === 'string') return content;
3
+ if (Array.isArray(content)) {
4
+ return content
5
+ .map((part) => {
6
+ if (typeof part === 'string') return part;
7
+ if (part?.type === 'text') return part.text || '';
8
+ return '';
9
+ })
10
+ .join('');
11
+ }
12
+ return '';
13
+ }
14
+
15
+ function normalizeIncomingToolCallArguments(argumentsValue) {
16
+ if (typeof argumentsValue === 'string') return argumentsValue;
17
+ if (argumentsValue == null) return '{}';
18
+ try {
19
+ return JSON.stringify(argumentsValue);
20
+ } catch {
21
+ return '{}';
22
+ }
23
+ }
24
+
25
+ function tryParseJsonObject(raw) {
26
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
27
+ return raw;
28
+ }
29
+ if (typeof raw !== 'string') return {};
30
+ try {
31
+ const parsed = JSON.parse(raw);
32
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
33
+ return parsed;
34
+ }
35
+ } catch {}
36
+ return {};
37
+ }
38
+
39
+ function normalizeMessages(messages) {
40
+ const source = Array.isArray(messages) ? messages : [];
41
+ const systemParts = [];
42
+ const out = [];
43
+
44
+ for (const message of source) {
45
+ if (!message || typeof message !== 'object') continue;
46
+ if (message.role === 'system') {
47
+ const text = extractTextContent(message.content);
48
+ if (text) systemParts.push(text);
49
+ continue;
50
+ }
51
+
52
+ if (message.role === 'tool') {
53
+ out.push({
54
+ role: 'user',
55
+ content: [
56
+ {
57
+ type: 'tool_result',
58
+ tool_use_id: String(message.tool_call_id || ''),
59
+ content: extractTextContent(message.content)
60
+ }
61
+ ]
62
+ });
63
+ continue;
64
+ }
65
+
66
+ const contentBlocks = [];
67
+ const text = extractTextContent(message.content);
68
+ if (text) {
69
+ contentBlocks.push({ type: 'text', text });
70
+ }
71
+
72
+ if (message.role === 'assistant' && Array.isArray(message.tool_calls)) {
73
+ for (const toolCall of message.tool_calls) {
74
+ const name = String(toolCall?.function?.name || toolCall?.name || '').trim();
75
+ if (!name) continue;
76
+ contentBlocks.push({
77
+ type: 'tool_use',
78
+ id: String(toolCall?.id || ''),
79
+ name,
80
+ input: tryParseJsonObject(toolCall?.function?.arguments ?? toolCall?.arguments)
81
+ });
82
+ }
83
+ }
84
+
85
+ out.push({
86
+ role: message.role,
87
+ content: contentBlocks
88
+ });
89
+ }
90
+
91
+ return {
92
+ system: systemParts.join('\n\n').trim() || undefined,
93
+ messages: out
94
+ };
95
+ }
96
+
97
+ function normalizeTools(tools) {
98
+ const source = Array.isArray(tools) ? tools : [];
99
+ return source
100
+ .map((tool) => {
101
+ const fn = tool?.function || {};
102
+ const name = String(fn.name || '').trim();
103
+ if (!name) return null;
104
+ return {
105
+ name,
106
+ ...(fn.description ? { description: String(fn.description) } : {}),
107
+ input_schema: fn.parameters && typeof fn.parameters === 'object' ? fn.parameters : { type: 'object' }
108
+ };
109
+ })
110
+ .filter(Boolean);
111
+ }
112
+
113
+ function buildPayload({ model, temperature, messages, tools, stream = false, maxTokens = 4096 }) {
114
+ const normalized = normalizeMessages(messages);
115
+ const payload = {
116
+ model,
117
+ max_tokens: maxTokens,
118
+ temperature,
119
+ messages: normalized.messages
120
+ };
121
+ if (normalized.system) payload.system = normalized.system;
122
+ if (stream) payload.stream = true;
123
+
124
+ const normalizedTools = normalizeTools(tools);
125
+ if (normalizedTools.length > 0) {
126
+ payload.tools = normalizedTools;
127
+ payload.tool_choice = { type: 'auto' };
128
+ }
129
+ return payload;
130
+ }
131
+
132
+ function hasTrailingToolContext(messages) {
133
+ const source = Array.isArray(messages) ? messages : [];
134
+ for (let index = source.length - 1; index >= 0; index -= 1) {
135
+ const message = source[index];
136
+ if (!message || typeof message !== 'object') continue;
137
+ if (message.role === 'tool') return true;
138
+ if (message.role === 'assistant' || message.role === 'user') return false;
139
+ }
140
+ return false;
141
+ }
142
+
143
+ function extractAssistantResult(data, messages) {
144
+ const content = Array.isArray(data?.content) ? data.content : [];
145
+ const text = content
146
+ .filter((block) => block?.type === 'text')
147
+ .map((block) => block.text || '')
148
+ .join('');
149
+ const toolCalls = content
150
+ .filter((block) => block?.type === 'tool_use')
151
+ .map((block) => ({
152
+ id: String(block.id || ''),
153
+ name: String(block.name || ''),
154
+ arguments: normalizeIncomingToolCallArguments(block.input)
155
+ }))
156
+ .filter((toolCall) => toolCall.name);
157
+ const normalizedText = String(text || '').trim();
158
+
159
+ if (!normalizedText && toolCalls.length === 0) {
160
+ if (hasTrailingToolContext(messages)) {
161
+ return {
162
+ text: '',
163
+ toolCalls: [],
164
+ usage: data?.usage || null,
165
+ incomplete: true,
166
+ content
167
+ };
168
+ }
169
+ throw new Error('Anthropic gateway returned empty assistant response');
170
+ }
171
+
172
+ return {
173
+ text,
174
+ toolCalls,
175
+ usage: data?.usage || null,
176
+ content
177
+ };
178
+ }
179
+
180
+ function createHeaders(apiKey) {
181
+ return {
182
+ 'content-type': 'application/json',
183
+ 'x-api-key': apiKey,
184
+ 'anthropic-version': '2023-06-01'
185
+ };
186
+ }
187
+
188
+ function buildMessagesUrl(baseUrl) {
189
+ return `${String(baseUrl || '').replace(/\/$/, '')}/v1/messages`;
190
+ }
191
+
192
+ async function parseJsonResponse(response) {
193
+ if (!response.ok) {
194
+ const text = await response.text().catch(() => '');
195
+ throw new Error(`Anthropic gateway error ${response.status}: ${text || response.statusText}`);
196
+ }
197
+ return response.json();
198
+ }
199
+
200
+ function mergeUsage(current, next) {
201
+ if (!next || typeof next !== 'object') return current;
202
+ return {
203
+ ...(current || {}),
204
+ ...next
205
+ };
206
+ }
207
+
208
+ function emptyToolCall(index) {
209
+ return {
210
+ index,
211
+ id: '',
212
+ name: '',
213
+ arguments: ''
214
+ };
215
+ }
216
+
217
+ function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
218
+ const toolCalls = Array.from(toolCallsByIndex.entries())
219
+ .sort((a, b) => a[0] - b[0])
220
+ .map(([, tc], i) => ({
221
+ id: tc.id || `tc-${i + 1}`,
222
+ name: tc.name,
223
+ arguments: tc.arguments || '{}'
224
+ }))
225
+ .filter((tc) => tc.name);
226
+ const normalizedText = String(text || '').trim();
227
+ const content = [];
228
+ if (text) content.push({ type: 'text', text });
229
+ for (const toolCall of toolCalls) {
230
+ content.push({
231
+ type: 'tool_use',
232
+ id: toolCall.id,
233
+ name: toolCall.name,
234
+ input: tryParseJsonObject(toolCall.arguments)
235
+ });
236
+ }
237
+
238
+ if (!normalizedText && toolCalls.length === 0) {
239
+ if (hasTrailingToolContext(messages)) {
240
+ return {
241
+ text: '',
242
+ toolCalls: [],
243
+ usage,
244
+ incomplete: true,
245
+ content: []
246
+ };
247
+ }
248
+ throw new Error('Anthropic gateway stream returned empty assistant response');
249
+ }
250
+
251
+ return {
252
+ text,
253
+ toolCalls,
254
+ usage,
255
+ incomplete: false,
256
+ content
257
+ };
258
+ }
259
+
260
+ async function* iterateSseEvents(stream) {
261
+ const decoder = new TextDecoder();
262
+ let buffer = '';
263
+
264
+ for await (const chunk of stream) {
265
+ buffer += decoder.decode(chunk, { stream: true });
266
+ while (buffer.includes('\n\n')) {
267
+ const boundary = buffer.indexOf('\n\n');
268
+ const rawEvent = buffer.slice(0, boundary);
269
+ buffer = buffer.slice(boundary + 2);
270
+ const lines = rawEvent.split('\n');
271
+ let event = 'message';
272
+ const dataLines = [];
273
+ for (const line of lines) {
274
+ if (line.startsWith('event:')) {
275
+ event = line.slice(6).trim();
276
+ } else if (line.startsWith('data:')) {
277
+ dataLines.push(line.slice(5).trimStart());
278
+ }
279
+ }
280
+ const dataText = dataLines.join('\n');
281
+ if (!dataText || dataText === '[DONE]') continue;
282
+ yield {
283
+ event,
284
+ data: JSON.parse(dataText)
285
+ };
286
+ }
287
+ }
288
+ }
289
+
290
+ export async function createChatCompletion({
291
+ baseUrl,
292
+ apiKey,
293
+ model,
294
+ messages,
295
+ temperature = 0.2,
296
+ tools,
297
+ timeoutMs = 90000,
298
+ maxTokens = 4096
299
+ }) {
300
+ const payload = buildPayload({ model, temperature, messages, tools, maxTokens });
301
+ const response = await fetch(buildMessagesUrl(baseUrl), {
302
+ method: 'POST',
303
+ headers: createHeaders(apiKey),
304
+ body: JSON.stringify(payload),
305
+ signal: AbortSignal.timeout(timeoutMs)
306
+ });
307
+ const data = await parseJsonResponse(response);
308
+ return extractAssistantResult(data, messages);
309
+ }
310
+
311
+ export async function createChatCompletionStream({
312
+ baseUrl,
313
+ apiKey,
314
+ model,
315
+ messages,
316
+ temperature = 0.2,
317
+ tools,
318
+ onTextDelta,
319
+ onToolCallDelta,
320
+ timeoutMs = 90000,
321
+ maxTokens = 4096
322
+ }) {
323
+ const payload = buildPayload({ model, temperature, messages, tools, stream: true, maxTokens });
324
+ const response = await fetch(buildMessagesUrl(baseUrl), {
325
+ method: 'POST',
326
+ headers: createHeaders(apiKey),
327
+ body: JSON.stringify(payload),
328
+ signal: AbortSignal.timeout(timeoutMs)
329
+ });
330
+
331
+ if (!response.ok || !response.body) {
332
+ const text = await response.text().catch(() => '');
333
+ throw new Error(`Anthropic gateway error ${response.status}: ${text || response.statusText}`);
334
+ }
335
+
336
+ let text = '';
337
+ let usage = null;
338
+ const toolCallsByIndex = new Map();
339
+
340
+ for await (const chunk of iterateSseEvents(response.body)) {
341
+ usage = mergeUsage(usage, chunk?.data?.usage);
342
+ usage = mergeUsage(usage, chunk?.data?.message?.usage);
343
+
344
+ if (chunk.event === 'content_block_start') {
345
+ const index = Number(chunk?.data?.index ?? 0);
346
+ const contentBlock = chunk?.data?.content_block || {};
347
+ if (contentBlock.type === 'tool_use') {
348
+ const current = toolCallsByIndex.get(index) || emptyToolCall(index);
349
+ current.id = String(contentBlock.id || current.id || '');
350
+ current.name = String(contentBlock.name || current.name || '');
351
+ const initialInput = contentBlock.input && Object.keys(contentBlock.input).length > 0
352
+ ? normalizeIncomingToolCallArguments(contentBlock.input)
353
+ : '';
354
+ current.arguments = current.arguments || initialInput;
355
+ toolCallsByIndex.set(index, current);
356
+ }
357
+ continue;
358
+ }
359
+
360
+ if (chunk.event !== 'content_block_delta') {
361
+ continue;
362
+ }
363
+
364
+ const index = Number(chunk?.data?.index ?? 0);
365
+ const delta = chunk?.data?.delta || {};
366
+ if (delta.type === 'text_delta' && delta.text) {
367
+ text += delta.text;
368
+ if (onTextDelta) onTextDelta(delta.text);
369
+ continue;
370
+ }
371
+
372
+ if (delta.type === 'input_json_delta') {
373
+ const current = toolCallsByIndex.get(index) || emptyToolCall(index);
374
+ current.arguments = `${current.arguments || ''}${String(delta.partial_json || '')}`;
375
+ toolCallsByIndex.set(index, current);
376
+ if (onToolCallDelta) {
377
+ onToolCallDelta({
378
+ index,
379
+ id: current.id || `tc-${index + 1}`,
380
+ name: current.name,
381
+ arguments: current.arguments || '{}'
382
+ });
383
+ }
384
+ }
385
+ }
386
+
387
+ return buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
388
+ }
@@ -0,0 +1,37 @@
1
+ import {
2
+ createChatCompletion as createOpenAICompatibleChatCompletion,
3
+ createChatCompletionStream as createOpenAICompatibleChatCompletionStream
4
+ } from './openai-compatible.js';
5
+ import {
6
+ createChatCompletion as createAnthropicChatCompletion,
7
+ createChatCompletionStream as createAnthropicChatCompletionStream
8
+ } from './anthropic.js';
9
+
10
+ function normalizeSdkProvider(value) {
11
+ const raw = String(value || '').trim().toLowerCase();
12
+ if (raw === 'anthropic') return 'anthropic';
13
+ return 'openai-compatible';
14
+ }
15
+
16
+ export function getSdkProvider(configOrValue) {
17
+ if (configOrValue && typeof configOrValue === 'object' && !Array.isArray(configOrValue)) {
18
+ return normalizeSdkProvider(configOrValue?.sdk?.provider);
19
+ }
20
+ return normalizeSdkProvider(configOrValue);
21
+ }
22
+
23
+ export async function createChatCompletion(options) {
24
+ const provider = getSdkProvider(options?.sdkProvider);
25
+ if (provider === 'anthropic') {
26
+ return createAnthropicChatCompletion(options);
27
+ }
28
+ return createOpenAICompatibleChatCompletion(options);
29
+ }
30
+
31
+ export async function createChatCompletionStream(options) {
32
+ const provider = getSdkProvider(options?.sdkProvider);
33
+ if (provider === 'anthropic') {
34
+ return createAnthropicChatCompletionStream(options);
35
+ }
36
+ return createOpenAICompatibleChatCompletionStream(options);
37
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { getSessionsDir } from './paths.js';
4
+ import { normalizeTodos } from './todo-state.js';
4
5
 
5
6
  const ALLOWED_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
6
7
 
@@ -86,6 +87,9 @@ function sanitizeSession(session, fallbackId = '') {
86
87
  }
87
88
  }
88
89
 
90
+ const todos = normalizeTodos(session?.todos);
91
+ if (todos.length > 0) out.todos = todos;
92
+
89
93
  return out;
90
94
  }
91
95
 
@@ -123,30 +123,46 @@ export function getShellSystemPrompt(value) {
123
123
  # Using your tools
124
124
 
125
125
  ALWAYS prefer dedicated tools over raw shell commands:
126
+ - The visible default tool list is intentionally small. If a needed capability is not currently listed, do not assume it is unavailable — call tool_search to load additional tools first
126
127
  - Use query_project_index first for broad repository understanding. It combines project-map metadata with indexed file symbols so you can narrow candidates before reading source files
127
128
  - Use read to inspect files — NEVER use cat, head, or tail via run. read returns content directly by default; demo-style shapes like {file_path:"src/app.ts"}, {path:"src/app.ts:10-40"}, or {file_path:"src/app.ts", offset:10, limit:30} are accepted
128
129
  - Use grep to search file contents — NEVER use grep or rg via run
129
- - Use glob to find files by pattern NEVER use find via run
130
+ - Use list for directory-by-directory filesystem discovery. If you specifically need pattern-based file lookup like src/**/*.ts, load glob with tool_search instead of falling back to run
130
131
  - Use edit to modify existing files — this is the DEFAULT path for code changes. Demo-style aliases like {file_path:"src/app.ts", old_string:"foo", new_string:"bar"} are accepted
131
132
  - Use write only for creating new files or complete rewrites (set full_file_rewrite=true for existing code files). Aliases like {file:"notes.txt", text:"..."} are accepted
132
- - Use patch to apply unified diffs
133
- - Use run for one-shot shell commands: install, build, test, or other finite tasks
134
- - For long-running processes (dev servers, watchers), use start_service instead of run
135
-
136
- For structural code edits (functions, classes, methods), use the AST-first workflow:
137
- ast_query read_ast_node edit with ast_target and kind=replace_block.
133
+ - Use update_todos to manage the session todo checklist for complex work. Provide the full current list each time and usually keep exactly one item in_progress
134
+ - Use run for shell commands. For long-running processes (dev servers, watchers), set run_in_background=true when you know you do not need the final result immediately. Long-running commands may also be backgrounded automatically
135
+
136
+ Use update_todos with these rules:
137
+ - MUST use it before major tool work when the task has 3 or more meaningful steps, multiple files or phases, explicit verification work, debugging with multiple hypotheses, or any non-trivial implementation likely to span several tool calls
138
+ - Do NOT use it for single-step trivial edits, one-off command execution, or purely informational/chat responses
139
+ - The input must be the full current checklist, not a partial patch
140
+ - Keep exactly one item in_progress while work is actively underway unless the user explicitly asks for parallel execution
141
+ - Mark items completed immediately after finishing them, and add newly discovered follow-up work as new checklist items
142
+ - If tests fail, verification is incomplete, or a blocker remains, do not mark the affected item completed
143
+ - Before giving a completion-style final answer for a complex task, update_todos so the checklist is either fully completed or clearly shows the remaining blocker
144
+
145
+ Some tools are loaded on demand through tool_search. Common examples:
146
+ - glob for pattern-based file lookup
147
+ - ast_query and read_ast_node for AST-scoped edits
148
+ - generate_diff and patch for explicit diff workflows
149
+ - list_background_tasks, get_background_task, and stop_background_task for managing long-running background commands
150
+ - remember_user, remember_global, remember_project, list_memory, search_memory, and forget_memory for persistent memory operations
151
+
152
+ For structural code edits (functions, classes, methods), load the AST tools and use the AST-first workflow:
153
+ tool_search("ast_query") → ast_query → read_ast_node → edit with ast_target and kind=replace_block.
138
154
  Fall back to plain grep/read/edit only when AST is not appropriate.
139
155
 
140
- For services: use start_service to launch, list_services/get_service_status/get_service_logs to monitor, stop_service to stop.
141
-
142
- Some tools are loaded on demand. If a needed tool is not listed, call tool_search first to load it.
156
+ For background commands: use run to launch. If you need management tools that are not currently visible, load list_background_tasks/get_background_task/stop_background_task with tool_search. Prefer reading the returned output_file with read instead of asking for a separate logs tool.
143
157
 
144
158
  Common tool call patterns:
145
159
  - Query the project index first: {query:"login auth flow", path:"src", max_results:5}
160
+ - Load a deferred tool when needed: {query:"glob"} or {query:"all"}
146
161
  - Read a file: {path:"src/app.ts"} or {file_path:"src/app.ts", offset:20, limit:40}
147
162
  - Read a specific range inline: {path:"src/app.ts:20-60"}
148
163
  - Search text: {pattern:"loginUser", path:"src"} or {query:"loginUser", directory:"src"}
149
- - Find files: {pattern:"src/**/*.ts"} or {query:"src/**/*.ts"}
164
+ - List a directory first: {path:"src"}
165
+ - After loading glob, find files by pattern: {pattern:"src/**/*.ts"} or {query:"src/**/*.ts"}
150
166
  - Edit exact text: {file_path:"src/app.ts", old_string:"foo", new_string:"bar"}
151
167
  - Edit with shorthand: {path:"src/app.ts", old_text:"foo", content:"bar"}
152
168
  - Write a new file: {file:"notes.txt", text:"..."} or {path:"src/page.tsx", content:"..."}
@@ -159,17 +175,13 @@ Common tool call patterns:
159
175
  - The user shares your workspace with you; prefer inspecting the project yourself before asking them to paste files that should be discoverable
160
176
  - Before substantial tool work, send a short progress update to the user about what you are about to inspect or do
161
177
  - Do not jump straight into tools without a brief user-facing note when the task is actionable
162
- - Search or read before editing unless the exact target is already known
163
- - For broad or ambiguous requests, query_project_index before large globs or reading many files
164
- - Do not read files one by one after a wide glob when query_project_index can narrow the candidates first
178
+ - For tasks with 3 or more meaningful steps, proactively create and maintain a todo checklist with update_todos
179
+ - For complex tasks, create the todo checklist before the first major implementation or verification tool call
165
180
  - If a command or tool is blocked or fails, inspect the error and retry with allowed commands or tools
166
181
  - For AST-scoped edits, if edit rejects due to missing or stale ast_target, fix arguments and retry
167
182
  - Do not claim filesystem access is impossible unless search/read tools also fail
168
- - Prefer editing existing files over creating new ones
169
183
  - Do not add comments, docstrings, or type annotations to code you did not change
170
184
  - Do not add features or refactor code beyond what was asked
171
- - When a tool result is large, keep only the useful summary in your reply and read the saved output only if it is needed
172
- - Keep tool results compact in context: prefer short conclusions over re-pasting raw output
173
185
 
174
186
  # Plan mode
175
187
 
@@ -0,0 +1,19 @@
1
+ export function normalizeTodoStatus(value) {
2
+ const status = String(value || 'pending').trim().toLowerCase();
3
+ return ['pending', 'in_progress', 'completed'].includes(status) ? status : 'pending';
4
+ }
5
+
6
+ export function normalizeTodos(value) {
7
+ if (!Array.isArray(value)) return [];
8
+ return value
9
+ .map((item) => ({
10
+ content: String(item?.content || '').trim(),
11
+ activeForm: String(item?.activeForm || '').trim(),
12
+ status: normalizeTodoStatus(item?.status)
13
+ }))
14
+ .filter((item) => item.content && item.activeForm);
15
+ }
16
+
17
+ export function countActiveTodos(todos) {
18
+ return normalizeTodos(todos).filter((item) => item.status !== 'completed').length;
19
+ }