@miphamai/cli 0.24.0 → 0.24.2
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/src/core/engine.ts +38 -1
- package/src/index.tsx +3 -2
- package/src/tools/file/edit.ts +78 -15
package/package.json
CHANGED
package/src/core/engine.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
ToolResult,
|
|
5
5
|
InferenceHookConfig,
|
|
6
6
|
CrsiConfig,
|
|
7
|
+
CrossSessionConfig,
|
|
7
8
|
} from '../shared/index.ts'
|
|
8
9
|
import { ProviderRegistry } from '../providers/registry'
|
|
9
10
|
import { ContextManager } from './context'
|
|
@@ -69,6 +70,12 @@ export class QueryEngine {
|
|
|
69
70
|
/** Session identifier for cross-session messaging. Set by the app startup via setSessionId(). */
|
|
70
71
|
private sessionId: string = 'session-1'
|
|
71
72
|
|
|
73
|
+
/** Cross-session inbound policy. Default: 'ask' (messages require user review). */
|
|
74
|
+
private crossSessionConfig: CrossSessionConfig = {
|
|
75
|
+
crossSessionInbound: 'ask',
|
|
76
|
+
dialogExpiry: 300,
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
/** Set the session identifier (called from index.tsx at startup with the actual session name). */
|
|
73
80
|
setSessionId(id: string): void {
|
|
74
81
|
this.sessionId = id
|
|
@@ -79,15 +86,45 @@ export class QueryEngine {
|
|
|
79
86
|
return this.sessionId
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
/** Set the cross-session messaging policy (called from index.tsx at startup). */
|
|
90
|
+
setCrossSessionConfig(config: CrossSessionConfig): void {
|
|
91
|
+
this.crossSessionConfig = config
|
|
92
|
+
}
|
|
93
|
+
|
|
82
94
|
/** Poll the cross-session file inbox for messages addressed to this session. */
|
|
83
95
|
async pollCrossSessionInbox(): Promise<void> {
|
|
96
|
+
const policy = this.crossSessionConfig.crossSessionInbound
|
|
97
|
+
|
|
98
|
+
// 'deny' — silently discard all inbound messages
|
|
99
|
+
if (policy === 'deny') {
|
|
100
|
+
try {
|
|
101
|
+
const transport = getFileInboxTransport()
|
|
102
|
+
await transport.poll(this.sessionId) // read + delete, don't forward
|
|
103
|
+
} catch {
|
|
104
|
+
// Best-effort discard
|
|
105
|
+
}
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
84
109
|
try {
|
|
85
110
|
const transport = getFileInboxTransport()
|
|
86
111
|
const messages = await transport.poll(this.sessionId)
|
|
87
112
|
if (messages.length > 0) {
|
|
88
113
|
const bus = getMessageBus()
|
|
89
114
|
for (const msg of messages) {
|
|
90
|
-
|
|
115
|
+
if (policy === 'ask') {
|
|
116
|
+
// Mark as awaiting approval — the model should verify with the user before acting
|
|
117
|
+
bus.post(
|
|
118
|
+
msg.from,
|
|
119
|
+
msg.to,
|
|
120
|
+
`[Awaiting Approval] ${msg.summary}`,
|
|
121
|
+
`[Cross-session message from ${msg.from} — verify with user before acting]\n\n${msg.message}`,
|
|
122
|
+
'warning',
|
|
123
|
+
)
|
|
124
|
+
} else {
|
|
125
|
+
// 'allow' — forward directly
|
|
126
|
+
bus.post(msg.from, msg.to, msg.summary, msg.message, msg.type)
|
|
127
|
+
}
|
|
91
128
|
}
|
|
92
129
|
}
|
|
93
130
|
} catch (err) {
|
package/src/index.tsx
CHANGED
|
@@ -318,8 +318,9 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
318
318
|
heartbeatInterval.unref()
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
// Load cross-session config
|
|
322
|
-
loadCrossSessionConfig()
|
|
321
|
+
// Load cross-session config and wire into the engine's inbound policy
|
|
322
|
+
const crossSessionConfig = loadCrossSessionConfig(process.cwd())
|
|
323
|
+
engine.setCrossSessionConfig(crossSessionConfig)
|
|
323
324
|
|
|
324
325
|
// Auto-save session on exit
|
|
325
326
|
let saved = false
|
package/src/tools/file/edit.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
if (
|
|
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
|
|
70
|
-
|
|
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
|
},
|