codemini-cli 0.2.7 → 0.2.9

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemini-cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
5
  "keywords": [
6
6
  "cli",
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';
7
+ const VERSION = '0.2.9';
8
8
 
9
9
  function printHelp() {
10
10
  console.log(`codemini ${VERSION}
@@ -14,6 +14,101 @@ function safeJsonParse(raw) {
14
14
  }
15
15
  }
16
16
 
17
+ function parseInlineRangePath(value) {
18
+ const text = String(value || '').trim();
19
+ if (!text) return null;
20
+ const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
21
+ if (!match) return null;
22
+ const [, maybePath, startRaw, endRaw] = match;
23
+ if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
24
+ const start = Number(startRaw);
25
+ const end = Number(endRaw || startRaw);
26
+ if (!Number.isFinite(start) || start <= 0) return null;
27
+ if (!Number.isFinite(end) || end < start) return null;
28
+ return { path: maybePath, start_line: start, end_line: end };
29
+ }
30
+
31
+ function normalizeToolArguments(toolName, args, rawArguments) {
32
+ const rawText = typeof rawArguments === 'string' ? rawArguments.trim() : '';
33
+ const primitive =
34
+ args == null || Array.isArray(args) || typeof args !== 'object'
35
+ ? args
36
+ : null;
37
+ const source =
38
+ args && typeof args === 'object' && !Array.isArray(args)
39
+ ? { ...args }
40
+ : {};
41
+
42
+ if (primitive != null && typeof primitive !== 'object') {
43
+ source._raw = rawText || String(primitive);
44
+ } else if (!source._raw && rawText && source._invalid_json) {
45
+ source._raw = rawText;
46
+ }
47
+
48
+ const stringValue =
49
+ typeof primitive === 'string'
50
+ ? primitive.trim()
51
+ : String(source._raw || '').trim();
52
+
53
+ if (toolName === 'read') {
54
+ const value = String(source.path || source.file_path || source.file || stringValue || '').trim();
55
+ if (value) source.path = value;
56
+ if (source.offset != null && source.start_line == null) source.start_line = source.offset;
57
+ if (source.limit != null && source.end_line == null && Number(source.start_line) > 0) {
58
+ source.end_line = Number(source.start_line) + Number(source.limit) - 1;
59
+ }
60
+ const range = parseInlineRangePath(source.path);
61
+ if (range) {
62
+ source.path = range.path;
63
+ if (source.start_line == null) source.start_line = range.start_line;
64
+ if (source.end_line == null) source.end_line = range.end_line;
65
+ }
66
+ return source;
67
+ }
68
+
69
+ if (toolName === 'list') {
70
+ const value = String(source.path || source.dir || source.directory || stringValue || '.').trim();
71
+ return { ...source, path: value || '.' };
72
+ }
73
+
74
+ if (toolName === 'glob') {
75
+ const pattern = String(source.pattern || source.glob || source.query || stringValue || '').trim();
76
+ if (pattern) source.pattern = pattern;
77
+ if (!source.path && source.directory) source.path = source.directory;
78
+ return source;
79
+ }
80
+
81
+ if (toolName === 'grep') {
82
+ const pattern = String(source.pattern || source.query || source.symbol || source.q || stringValue || '').trim();
83
+ if (pattern) source.pattern = pattern;
84
+ if (!source.path && (source.directory || source.dir || source.cwd)) {
85
+ source.path = source.directory || source.dir || source.cwd;
86
+ }
87
+ return source;
88
+ }
89
+
90
+ if (toolName === 'write') {
91
+ const value = String(source.path || source.file_path || source.file || stringValue || '').trim();
92
+ if (value) source.path = value;
93
+ if (source.content == null && source.text != null) source.content = source.text;
94
+ if (source.content == null && source.new_content != null) source.content = source.new_content;
95
+ return source;
96
+ }
97
+
98
+ if (toolName === 'edit') {
99
+ const value = String(source.path || source.file || source.file_path || '').trim();
100
+ if (value && !source.path) source.path = value;
101
+ return source;
102
+ }
103
+
104
+ return source;
105
+ }
106
+
107
+ function emptyToolResultMarker(toolName) {
108
+ const name = String(toolName || 'tool').trim() || 'tool';
109
+ return `(${name} completed with no output)`;
110
+ }
111
+
17
112
  function clipToolResult(result, maxChars = 12000) {
18
113
  const raw = typeof result === 'string' ? result : JSON.stringify(result);
19
114
  if (!maxChars || raw.length <= maxChars) return raw;
@@ -374,9 +469,12 @@ function formatToolDisplayName(name, args) {
374
469
  function formatToolResult(toolResult, toolName, args, toolFormatters, toolResultMaxChars) {
375
470
  if (toolFormatters && typeof toolFormatters[toolName] === 'function') {
376
471
  const formatted = toolFormatters[toolName](toolResult, args);
377
- if (typeof formatted === 'string') return formatted;
472
+ if (typeof formatted === 'string') {
473
+ return formatted.trim() ? formatted : emptyToolResultMarker(toolName);
474
+ }
378
475
  }
379
- return compactToolResult(toolResult, toolName, args, toolResultMaxChars);
476
+ const fallback = compactToolResult(toolResult, toolName, args, toolResultMaxChars);
477
+ return String(fallback || '').trim() ? fallback : emptyToolResultMarker(toolName);
380
478
  }
381
479
 
382
480
  // ─── Main agent loop ────────────────────────────────────────────────
@@ -469,8 +567,8 @@ export async function runAgentLoop({
469
567
  // ─── P1a: Partition into read-only (parallel) and write (serial) ──
470
568
 
471
569
  const callsWithMeta = toolCalls.map((call) => {
472
- const args = safeJsonParse(call.arguments);
473
570
  const toolName = normalizeToolCallName(call.name);
571
+ const args = normalizeToolArguments(toolName, safeJsonParse(call.arguments), call.arguments);
474
572
  const displayName = formatToolDisplayName(toolName, args);
475
573
  const isReadOnly = READ_ONLY_TOOLS.has(toolName);
476
574
  return { call, args, toolName, displayName, isReadOnly };
@@ -2,6 +2,42 @@ import os from 'node:os';
2
2
  import fs from 'node:fs';
3
3
  import { getShellSystemPrompt } from './shell-profile.js';
4
4
 
5
+ function getToolFewShotBlock() {
6
+ const cwd = process.cwd();
7
+ return `# Tool Examples
8
+
9
+ Use these as style examples for tool calls:
10
+
11
+ Current working directory: ${cwd}
12
+ When a tool takes file_path, build it from the current working directory and prefer absolute paths.
13
+ If the user mentions a project-relative path like src/app.ts, resolve it from ${cwd} instead of guessing parent directories.
14
+
15
+ 1. File discovery then read
16
+ User: compare the auth flow
17
+ Assistant: first locate the relevant files
18
+ Tool: glob({"pattern":"src/**/*auth*.ts"})
19
+ Tool: read({"file_path":"${cwd}/src/auth/service.ts"})
20
+
21
+ 2. Targeted search then exact text edit
22
+ User: rename loginUser to signInUser
23
+ Assistant: first find the exact occurrences
24
+ Tool: grep({"pattern":"loginUser","path":"src"})
25
+ Tool: edit({"file_path":"${cwd}/src/auth/service.ts","old_string":"loginUser","new_string":"signInUser"})
26
+
27
+ 3. Read a specific range
28
+ User: inspect the reducer around line 120
29
+ Assistant: read only the needed range
30
+ Tool: read({"path":"${cwd}/src/store/reducer.ts:110-150"})
31
+
32
+ 4. Create a new file
33
+ User: add a notes file
34
+ Assistant: create the file directly
35
+ Tool: write({"file":"${cwd}/notes.txt","text":"todo\\n"})
36
+
37
+ Prefer these direct tool shapes over multi-step metadata reads or shell fallbacks.
38
+ Prefer explicit absolute file_path values when the current working directory is known.`;
39
+ }
40
+
5
41
  function getEnvBlock() {
6
42
  const cwd = process.cwd();
7
43
  let isGitRepo = false;
@@ -22,5 +58,7 @@ OS Version: ${os.version || os.release()}
22
58
  export function buildDefaultSystemPrompt(config = {}) {
23
59
  return `${getShellSystemPrompt(config?.shell?.default)}
24
60
 
61
+ ${getToolFewShotBlock()}
62
+
25
63
  ${getEnvBlock()}`;
26
64
  }
@@ -47,6 +47,16 @@ function normalizeToolCallArguments(argumentsText) {
47
47
  return '{}';
48
48
  }
49
49
 
50
+ function normalizeIncomingToolCallArguments(argumentsValue) {
51
+ if (typeof argumentsValue === 'string') return argumentsValue;
52
+ if (argumentsValue == null) return '{}';
53
+ try {
54
+ return JSON.stringify(argumentsValue);
55
+ } catch {
56
+ return '{}';
57
+ }
58
+ }
59
+
50
60
  function sanitizeGatewayMessages(messages) {
51
61
  const source = Array.isArray(messages) ? messages : [];
52
62
  return source
@@ -115,7 +125,18 @@ function buildPayload({ model, temperature, messages, tools, stream = false }) {
115
125
  return payload;
116
126
  }
117
127
 
118
- function buildFinalStreamResult(text, toolCallsByIndex, usage) {
128
+ function hasTrailingToolContext(messages) {
129
+ const source = Array.isArray(messages) ? messages : [];
130
+ for (let index = source.length - 1; index >= 0; index -= 1) {
131
+ const message = source[index];
132
+ if (!message || typeof message !== 'object') continue;
133
+ if (message.role === 'tool') return true;
134
+ if (message.role === 'assistant' || message.role === 'user') return false;
135
+ }
136
+ return false;
137
+ }
138
+
139
+ function buildFinalStreamResult(text, toolCallsByIndex, usage, messages) {
119
140
  const toolCalls = Array.from(toolCallsByIndex.entries())
120
141
  .sort((a, b) => a[0] - b[0])
121
142
  .map(([, tc], i) => ({
@@ -126,6 +147,13 @@ function buildFinalStreamResult(text, toolCallsByIndex, usage) {
126
147
  .filter((tc) => tc.name);
127
148
 
128
149
  if (!text && toolCalls.length === 0) {
150
+ if (hasTrailingToolContext(messages)) {
151
+ return {
152
+ text: '',
153
+ toolCalls: [],
154
+ usage
155
+ };
156
+ }
129
157
  throw new Error('Gateway stream returned empty assistant response');
130
158
  }
131
159
 
@@ -216,7 +244,7 @@ export async function createChatCompletion({
216
244
  const toolCalls = (message.tool_calls || []).map((tc) => ({
217
245
  id: tc.id,
218
246
  name: tc.function?.name,
219
- arguments: tc.function?.arguments || '{}'
247
+ arguments: normalizeIncomingToolCallArguments(tc.function?.arguments)
220
248
  }));
221
249
 
222
250
  if (!text && toolCalls.length === 0) {
@@ -280,7 +308,9 @@ export async function createChatCompletionStream({
280
308
  const current = toolCallsByIndex.get(idx) || emptyToolCall(idx);
281
309
  if (td.id) current.id = td.id;
282
310
  if (td.function?.name) current.name = `${current.name}${td.function.name}`;
283
- if (td.function?.arguments) current.arguments = `${current.arguments}${td.function.arguments}`;
311
+ if (td.function?.arguments !== undefined) {
312
+ current.arguments = `${current.arguments}${normalizeIncomingToolCallArguments(td.function.arguments)}`;
313
+ }
284
314
  toolCallsByIndex.set(idx, current);
285
315
  if (onToolCallDelta) {
286
316
  onToolCallDelta({
@@ -293,5 +323,5 @@ export async function createChatCompletionStream({
293
323
  }
294
324
  }
295
325
 
296
- return buildFinalStreamResult(text, toolCallsByIndex, usage);
326
+ return buildFinalStreamResult(text, toolCallsByIndex, usage, messages);
297
327
  }
@@ -123,11 +123,11 @@ export function getShellSystemPrompt(value) {
123
123
  # Using your tools
124
124
 
125
125
  ALWAYS prefer dedicated tools over raw shell commands:
126
- - Use read to inspect files — NEVER use cat, head, or tail via run
126
+ - 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
127
127
  - Use grep to search file contents — NEVER use grep or rg via run
128
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)
129
+ - 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
130
+ - 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
131
131
  - Use patch to apply unified diffs
132
132
  - Use run for one-shot shell commands: install, build, test, or other finite tasks
133
133
  - For long-running processes (dev servers, watchers), use start_service instead of run
@@ -140,6 +140,17 @@ For services: use start_service to launch, list_services/get_service_status/get_
140
140
 
141
141
  Some tools are loaded on demand. If a needed tool is not listed, call tool_search first to load it.
142
142
 
143
+ Common tool call patterns:
144
+ - Read a file: {path:"src/app.ts"} or {file_path:"src/app.ts", offset:20, limit:40}
145
+ - Read a specific range inline: {path:"src/app.ts:20-60"}
146
+ - Search text: {pattern:"loginUser", path:"src"} or {query:"loginUser", directory:"src"}
147
+ - Find files: {pattern:"src/**/*.ts"} or {query:"src/**/*.ts"}
148
+ - Edit exact text: {file_path:"src/app.ts", old_string:"foo", new_string:"bar"}
149
+ - Edit with shorthand: {path:"src/app.ts", old_text:"foo", content:"bar"}
150
+ - Write a new file: {file:"notes.txt", text:"..."} or {path:"src/page.tsx", content:"..."}
151
+ - When the environment provides a Working directory, prefer absolute file_path values rooted there instead of guessing prefixes
152
+ - If the user gives a relative path like src/app.ts, resolve it from the current Working directory rather than inventing ../ or sibling folders
153
+
143
154
  # Doing tasks
144
155
 
145
156
  - You are a terminal-first CLI coding agent, not a generic chat assistant