@miphamai/cli 0.24.0 → 0.24.1

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": "@miphamai/cli",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -2,10 +2,65 @@ import { readFileSync, writeFileSync } from 'node:fs'
2
2
  import type { ToolDefinition } from '../../shared/index.ts'
3
3
  import { resolveSafe } from '../../security/path'
4
4
 
5
+ /**
6
+ * Characters considered part of an identifier in source code.
7
+ * A match is only valid if it sits at identifier boundaries —
8
+ * i.e. not embedded inside a larger identifier word.
9
+ */
10
+ function isIdentifierChar(c: string): boolean {
11
+ return /[a-zA-Z0-9_$]/.test(c)
12
+ }
13
+
14
+ /**
15
+ * Find all valid occurrences of `oldStr` in `content`.
16
+ * A match is valid only when it does NOT sit inside a larger identifier:
17
+ * - If oldStr starts with an identifier char, the char before the match
18
+ * must NOT be an identifier char (or the match must be at position 0).
19
+ * - If oldStr ends with an identifier char, the char after the match
20
+ * must NOT be an identifier char (or the match must be at end of file).
21
+ */
22
+ function findValidMatches(content: string, oldStr: string): number[] {
23
+ const indices: number[] = []
24
+ // oldStr is guaranteed non-empty by the required parameter validation
25
+ const oldFirst = oldStr[0]!
26
+ const oldLast = oldStr[oldStr.length - 1]!
27
+ const checkLeft = isIdentifierChar(oldFirst)
28
+ const checkRight = isIdentifierChar(oldLast)
29
+
30
+ let pos = 0
31
+ while (pos < content.length) {
32
+ const idx = content.indexOf(oldStr, pos)
33
+ if (idx === -1) break
34
+
35
+ // Left boundary check: if oldStr starts with identifier char,
36
+ // the character before the match must not be an identifier char.
37
+ if (checkLeft && idx > 0 && isIdentifierChar(content[idx - 1]!)) {
38
+ pos = idx + 1
39
+ continue
40
+ }
41
+
42
+ // Right boundary check: if oldStr ends with identifier char,
43
+ // the character after the match must not be an identifier char.
44
+ const afterIdx = idx + oldStr.length
45
+ if (checkRight && afterIdx < content.length && isIdentifierChar(content[afterIdx]!)) {
46
+ pos = idx + 1
47
+ continue
48
+ }
49
+
50
+ indices.push(idx)
51
+ pos = idx + 1
52
+ }
53
+
54
+ return indices
55
+ }
56
+
5
57
  export const editTool: ToolDefinition = {
6
58
  name: 'Edit',
7
59
  description:
8
- 'Perform exact string replacement in a file. old_string must match exactly and be unique.',
60
+ 'Perform exact string replacement in a file. ' +
61
+ 'old_string must match exactly as a standalone occurrence — it will NOT match inside larger ' +
62
+ 'identifiers or words (e.g. "user" will not match inside "username"). ' +
63
+ 'Must be unique in the file (unless replace_all is used).',
9
64
  category: 'file',
10
65
  permission: 'ask',
11
66
  parameters: {
@@ -43,22 +98,30 @@ export const editTool: ToolDefinition = {
43
98
  }
44
99
  }
45
100
 
46
- if (replaceAll) {
47
- if (!content.includes(oldStr)) {
48
- return { success: false, content: '', error: 'old_string not found in file' }
101
+ const matches = findValidMatches(content, oldStr)
102
+
103
+ if (matches.length === 0) {
104
+ return {
105
+ success: false,
106
+ content: '',
107
+ error:
108
+ 'old_string not found in file. ' +
109
+ 'It may appear only as part of a larger word/identifier — provide more context.',
49
110
  }
50
- const updated = content.replaceAll(oldStr, newStr)
51
- writeFileSync(filePath, updated, 'utf-8')
52
- const count = content.split(oldStr).length - 1
53
- return { success: true, content: `Replaced ${count} occurrences in ${filePath}` }
54
111
  }
55
112
 
56
- const firstIndex = content.indexOf(oldStr)
57
- if (firstIndex === -1) {
58
- return { success: false, content: '', error: 'old_string not found in file' }
113
+ if (replaceAll) {
114
+ // Build result by splicing at each match (in reverse so indices stay valid)
115
+ let result = content
116
+ for (let i = matches.length - 1; i >= 0; i--) {
117
+ const idx = matches[i]!
118
+ result = result.slice(0, idx) + newStr + result.slice(idx + oldStr.length)
119
+ }
120
+ writeFileSync(filePath, result, 'utf-8')
121
+ return { success: true, content: `Replaced ${matches.length} occurrences in ${filePath}` }
59
122
  }
60
- const secondIndex = content.indexOf(oldStr, firstIndex + 1)
61
- if (secondIndex !== -1) {
123
+
124
+ if (matches.length > 1) {
62
125
  return {
63
126
  success: false,
64
127
  content: '',
@@ -66,8 +129,8 @@ export const editTool: ToolDefinition = {
66
129
  }
67
130
  }
68
131
 
69
- const updated =
70
- content.slice(0, firstIndex) + newStr + content.slice(firstIndex + oldStr.length)
132
+ const idx = matches[0]!
133
+ const updated = content.slice(0, idx) + newStr + content.slice(idx + oldStr.length)
71
134
  writeFileSync(filePath, updated, 'utf-8')
72
135
  return { success: true, content: `Replaced 1 occurrence in ${filePath}` }
73
136
  },