@miphamai/cli 0.36.0 → 0.36.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
|
@@ -34,11 +34,14 @@ export interface AgentSession {
|
|
|
34
34
|
branch?: string
|
|
35
35
|
/** Created PR URL (if requested) */
|
|
36
36
|
prUrl?: string
|
|
37
|
+
/** Working directory the session was spawned from (used for directory grouping). */
|
|
38
|
+
directory: string
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export interface CreateSessionOptions {
|
|
40
42
|
provider?: string
|
|
41
43
|
model?: string
|
|
44
|
+
directory?: string
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
export interface SessionPeek {
|
|
@@ -48,6 +51,11 @@ export interface SessionPeek {
|
|
|
48
51
|
|
|
49
52
|
export type StatusGroups = Record<SessionStatus, AgentSession[]>
|
|
50
53
|
|
|
54
|
+
export interface DirectoryGroup {
|
|
55
|
+
directory: string
|
|
56
|
+
sessions: AgentSession[]
|
|
57
|
+
}
|
|
58
|
+
|
|
51
59
|
export class AgentViewManager {
|
|
52
60
|
private sessions: Map<string, AgentSession> = new Map()
|
|
53
61
|
private sessionOrder: string[] = []
|
|
@@ -66,6 +74,7 @@ export class AgentViewManager {
|
|
|
66
74
|
provider: options.provider ?? 'unknown',
|
|
67
75
|
model: options.model ?? 'unknown',
|
|
68
76
|
task,
|
|
77
|
+
directory: options.directory ?? process.cwd(),
|
|
69
78
|
createdAt: new Date(),
|
|
70
79
|
elapsedMs: 0,
|
|
71
80
|
messages: [],
|
|
@@ -109,6 +118,27 @@ export class AgentViewManager {
|
|
|
109
118
|
return groups
|
|
110
119
|
}
|
|
111
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Group all sessions by their working directory (v2.1.229 session-grouping alignment).
|
|
123
|
+
* Returns groups sorted alphabetically by directory path.
|
|
124
|
+
*/
|
|
125
|
+
groupByDirectory(): DirectoryGroup[] {
|
|
126
|
+
const groups = new Map<string, AgentSession[]>()
|
|
127
|
+
|
|
128
|
+
for (const id of this.sessionOrder) {
|
|
129
|
+
const session = this.sessions.get(id)
|
|
130
|
+
if (!session) continue
|
|
131
|
+
const dir = session.directory || '(unknown)'
|
|
132
|
+
const existing = groups.get(dir)
|
|
133
|
+
if (existing) existing.push(session)
|
|
134
|
+
else groups.set(dir, [session])
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return Array.from(groups.entries())
|
|
138
|
+
.map(([directory, sessions]) => ({ directory, sessions }))
|
|
139
|
+
.sort((a, b) => a.directory.localeCompare(b.directory))
|
|
140
|
+
}
|
|
141
|
+
|
|
112
142
|
/**
|
|
113
143
|
* Get a session by ID.
|
|
114
144
|
*/
|
|
@@ -30,7 +30,7 @@ const STATUS_HEADERS: Record<string, { label: string; color: string }> = {
|
|
|
30
30
|
export function AgentViewDashboard({ manager, onAttach, onExit }: DashboardProps) {
|
|
31
31
|
const [selectedIndex, setSelectedIndex] = useState(0)
|
|
32
32
|
const [peekingSessionId, setPeekingSessionId] = useState<string | null>(null)
|
|
33
|
-
const [
|
|
33
|
+
const [groupBy, setGroupBy] = useState<'status' | 'directory'>('status')
|
|
34
34
|
const [feedback, setFeedback] = useState<string | null>(null)
|
|
35
35
|
|
|
36
36
|
// Flash a brief feedback message that auto-clears
|
|
@@ -41,37 +41,53 @@ export function AgentViewDashboard({ manager, onAttach, onExit }: DashboardProps
|
|
|
41
41
|
|
|
42
42
|
// Build a flat list of sessions in group order, with group headers
|
|
43
43
|
const flatList = useMemo(() => {
|
|
44
|
-
const groups = manager.groupByStatus()
|
|
45
44
|
const result: Array<
|
|
46
|
-
| { type: 'header';
|
|
45
|
+
| { type: 'header'; key: string; label: string; color: string; count: number }
|
|
47
46
|
| { type: 'session'; session: AgentSession }
|
|
48
47
|
> = []
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
if (groupBy === 'directory') {
|
|
50
|
+
for (const group of manager.groupByDirectory()) {
|
|
51
|
+
result.push({
|
|
52
|
+
type: 'header',
|
|
53
|
+
key: `dir-${group.directory}`,
|
|
54
|
+
label: group.directory,
|
|
55
|
+
color: 'blue',
|
|
56
|
+
count: group.sessions.length,
|
|
57
|
+
})
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
for (const session of group.sessions) {
|
|
60
|
+
result.push({ type: 'session', session })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
const groups = manager.groupByStatus()
|
|
65
|
+
const statusOrder: Array<keyof typeof STATUS_HEADERS> = [
|
|
66
|
+
'working',
|
|
67
|
+
'needs-input',
|
|
68
|
+
'completed',
|
|
69
|
+
'failed',
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
for (const _status of statusOrder) {
|
|
73
|
+
const status = _status as SessionStatus
|
|
74
|
+
const sessions = groups[status] ?? []
|
|
75
|
+
result.push({
|
|
76
|
+
type: 'header',
|
|
77
|
+
key: `status-${status}`,
|
|
78
|
+
label: STATUS_HEADERS[status]!.label,
|
|
79
|
+
color: STATUS_HEADERS[status]!.color,
|
|
80
|
+
count: sessions.length,
|
|
81
|
+
})
|
|
67
82
|
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
for (const session of sessions) {
|
|
84
|
+
result.push({ type: 'session', session })
|
|
85
|
+
}
|
|
70
86
|
}
|
|
71
87
|
}
|
|
72
88
|
|
|
73
89
|
return result
|
|
74
|
-
}, [manager])
|
|
90
|
+
}, [manager, groupBy])
|
|
75
91
|
|
|
76
92
|
// Flatten sessions only for navigation (skip headers)
|
|
77
93
|
const sessionsOnly = useMemo(
|
|
@@ -106,7 +122,7 @@ export function AgentViewDashboard({ manager, onAttach, onExit }: DashboardProps
|
|
|
106
122
|
// Ctrl+T — toggle group by (status ↔ directory)
|
|
107
123
|
if (key.ctrl && input === 't') {
|
|
108
124
|
setGroupBy((prev) => (prev === 'status' ? 'directory' : 'status'))
|
|
109
|
-
showFeedback(`Grouped by ${
|
|
125
|
+
showFeedback(`Grouped by ${groupBy === 'status' ? 'directory' : 'status'}`)
|
|
110
126
|
return
|
|
111
127
|
}
|
|
112
128
|
|
|
@@ -238,7 +254,7 @@ export function AgentViewDashboard({ manager, onAttach, onExit }: DashboardProps
|
|
|
238
254
|
{flatList.map((item, _flatIdx) => {
|
|
239
255
|
if (item.type === 'header') {
|
|
240
256
|
return (
|
|
241
|
-
<Box key={`h-${item.
|
|
257
|
+
<Box key={`h-${item.key}`} marginY={1}>
|
|
242
258
|
<Text bold color={item.color}>
|
|
243
259
|
{' '}
|
|
244
260
|
{item.label} ({item.count})
|
package/src/core/engine.ts
CHANGED
|
@@ -377,6 +377,10 @@ export class QueryEngine {
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
async *process(userInput: string, signal?: AbortSignal): AsyncGenerator<StreamChunk> {
|
|
380
|
+
// v2.1.229 alignment: reject whitespace-only messages before they reach the
|
|
381
|
+
// provider API (which would otherwise 400 on an empty/blank message).
|
|
382
|
+
if (!userInput.trim()) return
|
|
383
|
+
|
|
380
384
|
// Poll for cross-session messages before each turn
|
|
381
385
|
await this.pollCrossSessionInbox()
|
|
382
386
|
|
package/src/core/permission.ts
CHANGED
|
@@ -9,6 +9,18 @@ import type { PermissionRuleEntry } from '../shared/index.ts'
|
|
|
9
9
|
import { matchBashRule, compileRule } from './permission-rules'
|
|
10
10
|
import { loadPermissionConfig, nextMode, clampMode, MODE_CYCLE } from './permission-config'
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Check if a Bash command is a git/gh operation using a destructive flag
|
|
14
|
+
* (--force, --amend, --no-verify, --hard) that must never be auto-approved.
|
|
15
|
+
* v2.1.229 alignment: these rewrite history / force-push, so they fall back
|
|
16
|
+
* to explicit user confirmation even in auto mode.
|
|
17
|
+
*/
|
|
18
|
+
function hasDangerousGitFlags(input: Record<string, unknown>): boolean {
|
|
19
|
+
const cmd = (input.command as string) || ''
|
|
20
|
+
if (!/^(git|gh)\s+/.test(cmd.trim())) return false
|
|
21
|
+
return /--force(?!-with-lease)\b|--amend\b|--no-verify\b|--hard\b/.test(cmd)
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
/**
|
|
13
25
|
* Check if a Bash command is a "verification-only" command that should be
|
|
14
26
|
* auto-approved in acceptEdits mode. These are non-destructive read/check
|
|
@@ -414,6 +426,10 @@ export class PermissionSystem {
|
|
|
414
426
|
if (this.isSubAgent && ['Bash', 'Write', 'Edit'].includes(tool.name)) {
|
|
415
427
|
return 'ask'
|
|
416
428
|
}
|
|
429
|
+
// v2.1.229 alignment: destructive git/gh flags are never auto-approved.
|
|
430
|
+
if (tool.name === 'Bash' && input && hasDangerousGitFlags(input)) {
|
|
431
|
+
return 'ask'
|
|
432
|
+
}
|
|
417
433
|
return 'bypass'
|
|
418
434
|
|
|
419
435
|
case 'dontAsk':
|
|
@@ -1,11 +1,29 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* parallel() — barrier: executes all thunks concurrently, waits for all.
|
|
3
5
|
* Failed thunks resolve to null. Never throws.
|
|
4
6
|
*
|
|
5
|
-
* P0-2 (v2.1.223 alignment): Added concurrency cap (MAX_CONCURRENT
|
|
6
|
-
*
|
|
7
|
+
* P0-2 (v2.1.223 alignment): Added concurrency cap (MAX_CONCURRENT) to prevent
|
|
8
|
+
* resource exhaustion from unbounded fan-out.
|
|
9
|
+
*
|
|
10
|
+
* v2.1.229 alignment: detect parallelism via os.availableParallelism() so
|
|
11
|
+
* CPU-limited containers (cgroup quota) don't fan out to the host core count.
|
|
12
|
+
* Still capped at 16 to bound resource usage, with a floor of 1.
|
|
7
13
|
*/
|
|
8
|
-
|
|
14
|
+
function detectParallelism(): number {
|
|
15
|
+
try {
|
|
16
|
+
if (typeof os.availableParallelism === 'function') {
|
|
17
|
+
const p = os.availableParallelism()
|
|
18
|
+
if (typeof p === 'number' && p > 0) return p
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
/* fall through to cpus() */
|
|
22
|
+
}
|
|
23
|
+
return os.cpus().length || 1
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MAX_CONCURRENT = Math.max(1, Math.min(16, detectParallelism()))
|
|
9
27
|
|
|
10
28
|
/**
|
|
11
29
|
* Simple async semaphore for concurrency limiting.
|