@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.36.0",
3
+ "version": "0.36.1",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -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 [_groupBy, setGroupBy] = useState<'status' | 'directory'>('status')
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'; status: string; label: string; color: string; count: number }
45
+ | { type: 'header'; key: string; label: string; color: string; count: number }
47
46
  | { type: 'session'; session: AgentSession }
48
47
  > = []
49
48
 
50
- const statusOrder: Array<keyof typeof STATUS_HEADERS> = [
51
- 'working',
52
- 'needs-input',
53
- 'completed',
54
- 'failed',
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
- for (const _status of statusOrder) {
58
- const status = _status as SessionStatus
59
- const sessions = groups[status] ?? []
60
- result.push({
61
- type: 'header',
62
- status,
63
- label: STATUS_HEADERS[status]!.label,
64
- color: STATUS_HEADERS[status]!.color,
65
- count: sessions.length,
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
- for (const session of sessions) {
69
- result.push({ type: 'session', session })
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 ${_groupBy === 'status' ? 'directory' : 'status'}`)
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.status}`} marginY={1}>
257
+ <Box key={`h-${item.key}`} marginY={1}>
242
258
  <Text bold color={item.color}>
243
259
  {' '}
244
260
  {item.label} ({item.count})
@@ -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
 
@@ -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 = 16)
6
- * to prevent resource exhaustion from unbounded fan-out.
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
- const MAX_CONCURRENT = 16
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.