@miphamai/cli 0.64.0 → 0.66.0

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.64.0",
3
+ "version": "0.66.0",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -20,6 +20,8 @@ export interface BackgroundTask {
20
20
  status: BackgroundTaskStatus
21
21
  kind: BackgroundTaskKind
22
22
  startedAt: Date
23
+ /** Cumulative token usage (input + output) reported by the running task. */
24
+ tokensUsed: number
23
25
  completedAt?: Date
24
26
  result?: string
25
27
  error?: string
@@ -99,6 +101,7 @@ export class BackgroundAgentRegistry {
99
101
  status: 'running',
100
102
  kind,
101
103
  startedAt: now,
104
+ tokensUsed: 0,
102
105
  abortController: new AbortController(),
103
106
  }
104
107
 
@@ -177,6 +180,15 @@ export class BackgroundAgentRegistry {
177
180
  return this.list().filter((t) => t.status === 'running')
178
181
  }
179
182
 
183
+ /**
184
+ * Update the cumulative token usage reported by a running task.
185
+ * No-op if the task no longer exists.
186
+ */
187
+ updateTokenUsage(id: string, tokensUsed: number): void {
188
+ const task = this.tasks.get(id)
189
+ if (task) task.tokensUsed = tokensUsed
190
+ }
191
+
180
192
  /**
181
193
  * Stop a running task by aborting its controller.
182
194
  * Returns true if the task was running and was stopped.
@@ -73,8 +73,13 @@ export class SubAgent {
73
73
  const bgRegistry = getBackgroundAgentRegistry()
74
74
 
75
75
  const taskId = bgRegistry.spawn(description, agentType, async (signal) => {
76
- // Run the synchronous execution inside the background executor
77
- return this.runExecution(prompt, options, signal)
76
+ // Run the synchronous execution inside the background executor, reporting
77
+ // cumulative token usage back to the registry for live footer display.
78
+ const opts: SubAgentOptions = {
79
+ ...options,
80
+ onTokenUsage: (total) => bgRegistry.updateTokenUsage(taskId, total),
81
+ }
82
+ return this.runExecution(prompt, opts, signal)
78
83
  })
79
84
 
80
85
  // Register completion callback for hook firing
@@ -323,6 +328,7 @@ export class SubAgent {
323
328
 
324
329
  let currentMessages = messages
325
330
  let currentSystemPrompt = systemPrompt
331
+ let totalTokens = 0
326
332
 
327
333
  try {
328
334
  for (let turn = 0; turn < MAX_TOOL_TURNS; turn++) {
@@ -366,6 +372,10 @@ export class SubAgent {
366
372
  if (chunk.type === 'stop') {
367
373
  break
368
374
  }
375
+ if (chunk.type === 'usage') {
376
+ totalTokens += (chunk.inputTokens ?? 0) + (chunk.outputTokens ?? 0)
377
+ if (options.onTokenUsage) options.onTokenUsage(totalTokens)
378
+ }
369
379
  }
370
380
 
371
381
  chunks.push(turnText)
@@ -46,6 +46,8 @@ export interface SubAgentOptions {
46
46
  runInBackground?: boolean
47
47
  /** Optional callback for streaming progress chunks during background execution. */
48
48
  onProgress?: (chunk: string) => void
49
+ /** Optional callback reporting cumulative token usage (input + output) from the API stream. */
50
+ onTokenUsage?: (totalTokens: number) => void
49
51
  /** When set, tool executions use this path as cwd (git worktree isolation). */
50
52
  worktreePath?: string
51
53
  /** Seed the sub-agent with a parent conversation prefix (e.g., fork inheritance). */
@@ -263,6 +263,18 @@ its live CRSI / SIS / constitution state. Report the numbers you read
263
263
  from it as live counts; if it shows a subsystem as 未初始化 (uninitialized),
264
264
  say so explicitly instead of claiming it exists.`)
265
265
 
266
+ // CRSI 先读代码铁律 — 回答代码问题前必须先读实际代码,勿凭记忆/命名/静态清单下结论
267
+ parts.push(`## Read-Code-First Rule
268
+
269
+ Before answering ANY question about this codebase — whether a file,
270
+ function, feature, or capability exists, how it works, or whether
271
+ something is missing — you MUST first read the actual code with the
272
+ Read, Grep, Glob, or graft tools. Do not infer or assert from memory,
273
+ naming conventions, or static tool lists. If you have not read the code,
274
+ say so and read it first, rather than answering hastily and retracting
275
+ afterwards. This applies to every code question, not only research or
276
+ borrow-analysis tasks.`)
277
+
266
278
  // CRSI 教训召回 — 把 crsi-lessons.md 的教训精华注入,让模型「写后召回」而非只写不读
267
279
  const lessonsBlock = buildCrsiLessonsBlock(this.crsiLessonSummaries)
268
280
  if (lessonsBlock) parts.push(lessonsBlock)
@@ -9,7 +9,7 @@
9
9
  export const PACKAGE_NAME = '@miphamai/cli' as const
10
10
 
11
11
  /** 当前发布版本 */
12
- export const PACKAGE_VERSION = '0.64.0' as const
12
+ export const PACKAGE_VERSION = '0.66.0' as const
13
13
 
14
14
  /** npm install 全局安装命令 */
15
15
  export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
@@ -75,6 +75,60 @@ function isOutsideWorktree(command: string, cwd: string): string | null {
75
75
  return null
76
76
  }
77
77
 
78
+ /**
79
+ * Split a git command string into argv tokens, honoring shell-style quoting
80
+ * (single quotes, double quotes, and backslash escapes). Unlike a naive
81
+ * `split(/\s+/)`, this keeps `-m "multi word message"` as a single argument
82
+ * instead of splitting it into separate tokens.
83
+ */
84
+ export function splitCommand(command: string): string[] {
85
+ const tokens: string[] = []
86
+ let current = ''
87
+ let inSingle = false
88
+ let inDouble = false
89
+
90
+ for (let i = 0; i < command.length; i++) {
91
+ const ch = command[i]!
92
+
93
+ if (inSingle) {
94
+ if (ch === "'") inSingle = false
95
+ else current += ch
96
+ continue
97
+ }
98
+
99
+ if (inDouble) {
100
+ if (ch === '"') inDouble = false
101
+ else if (ch === '\\' && i + 1 < command.length && '$`"\\'.includes(command[i + 1]!)) {
102
+ current += command[++i]!
103
+ } else {
104
+ current += ch
105
+ }
106
+ continue
107
+ }
108
+
109
+ if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
110
+ if (current !== '') {
111
+ tokens.push(current)
112
+ current = ''
113
+ }
114
+ continue
115
+ }
116
+
117
+ if (ch === "'") {
118
+ inSingle = true
119
+ } else if (ch === '"') {
120
+ inDouble = true
121
+ } else if (ch === '\\' && i + 1 < command.length) {
122
+ current += command[++i]!
123
+ } else {
124
+ current += ch
125
+ }
126
+ }
127
+
128
+ if (current !== '') tokens.push(current)
129
+ return tokens
130
+ }
131
+
78
132
  export const gitTool: ToolDefinition = {
79
133
  name: 'Git',
80
134
  description: 'Execute git commands. Dangerous operations (force push, hard reset) are blocked.',
@@ -115,7 +169,7 @@ export const gitTool: ToolDefinition = {
115
169
  }
116
170
 
117
171
  try {
118
- const proc = Bun.spawn(['git', ...command.split(/\s+/)], {
172
+ const proc = Bun.spawn(['git', ...splitCommand(command)], {
119
173
  cwd: ctx.cwd,
120
174
  stdout: 'pipe',
121
175
  stderr: 'pipe',
@@ -101,6 +101,9 @@ function rotatingGerund(tick: number): string {
101
101
  return GERUNDS[Math.floor(tick / 2) % GERUNDS.length]!
102
102
  }
103
103
 
104
+ /** Max agent lines to render before collapsing the rest into "↓ N more". */
105
+ const MAX_AGENT_LINES = 5
106
+
104
107
  export function AgentFooter({ agents, tick, activeTool, agentProgress }: AgentFooterProps) {
105
108
  const { t } = useI18n()
106
109
  const hasAgents = agents.length > 0
@@ -118,6 +121,7 @@ export function AgentFooter({ agents, tick, activeTool, agentProgress }: AgentFo
118
121
  {' '}[{activeTool!.name} {activeTool!.detail.slice(0, 60)}
119
122
  {animatedDots(tick)}]
120
123
  </Text>
124
+ <Box flexGrow={1} />
121
125
  <Text dimColor>
122
126
  {' '}
123
127
  {formatElapsed(Math.floor((Date.now() - activeTool!.startTime) / 1000))}
@@ -131,6 +135,7 @@ export function AgentFooter({ agents, tick, activeTool, agentProgress }: AgentFo
131
135
  <Text color="#FFA500">
132
136
  {' '}✻ {rotatingGerund(tick)}…
133
137
  </Text>
138
+ <Box flexGrow={1} />
134
139
  <Text dimColor>
135
140
  {' ('}
136
141
  {formatElapsed(Math.floor((Date.now() - agentProgress!.startTime) / 1000))}
@@ -142,8 +147,8 @@ export function AgentFooter({ agents, tick, activeTool, agentProgress }: AgentFo
142
147
  </Box>
143
148
  )}
144
149
 
145
- {/* Agent status lines */}
146
- {agents.map((agent) => {
150
+ {/* Agent status lines — collapse to MAX_AGENT_LINES + "↓ N more" */}
151
+ {agents.slice(0, MAX_AGENT_LINES).map((agent) => {
147
152
  const elapsed = Math.floor((Date.now() - agent.startTime) / 1000)
148
153
  const isRunning = agent.status === 'running'
149
154
  const symbol = isRunning ? '◯' : '◼'
@@ -163,7 +168,8 @@ export function AgentFooter({ agents, tick, activeTool, agentProgress }: AgentFo
163
168
  {agent.name}
164
169
  </Text>
165
170
  <Text dimColor> {desc}</Text>
166
- {/* Spacer pushes stats to the right — Ink handles this naturally */}
171
+ {/* Spacer pushes elapsed + tokens to the right edge */}
172
+ <Box flexGrow={1} />
167
173
  <Text dimColor>
168
174
  {' '}
169
175
  {isRunning ? formatElapsed(elapsed) : t('ui.agent.finished')}
@@ -177,6 +183,11 @@ export function AgentFooter({ agents, tick, activeTool, agentProgress }: AgentFo
177
183
  </Box>
178
184
  )
179
185
  })}
186
+ {agents.length > MAX_AGENT_LINES && (
187
+ <Text dimColor>
188
+ {' '}↓ {agents.length - MAX_AGENT_LINES} more
189
+ </Text>
190
+ )}
180
191
  </Box>
181
192
  )
182
193
  }
package/src/ui/app.tsx CHANGED
@@ -270,7 +270,23 @@ export function App({
270
270
  agentProgressRef.current !== null ||
271
271
  activeToolRef.current !== null
272
272
  if (!hasRunning) return
273
- const i = setInterval(() => setAgentTick((t) => t + 1), 1000)
273
+ const i = setInterval(() => {
274
+ setAgentTick((t) => t + 1)
275
+ // Refresh live token counts for running background agents.
276
+ setRunningAgents((prev) => {
277
+ const bg = getBackgroundAgentRegistry()
278
+ let changed = false
279
+ const next = { ...prev }
280
+ for (const task of bg.list()) {
281
+ const cur = next[task.id]
282
+ if (cur && cur.status === 'running' && cur.tokensUsed !== task.tokensUsed) {
283
+ next[task.id] = { ...cur, tokensUsed: task.tokensUsed }
284
+ changed = true
285
+ }
286
+ }
287
+ return changed ? next : prev
288
+ })
289
+ }, 1000)
274
290
  return () => clearInterval(i)
275
291
  }, [runningAgents, agentProgress, activeTool])
276
292
 
@@ -282,14 +298,14 @@ export function App({
282
298
  setRunningAgents((prev) => {
283
299
  const next: Record<string, AgentEntry> = {}
284
300
  for (const task of all) {
285
- // Preserve existing entry if it has more data (e.g. tokens accumulated during streaming)
301
+ // Preserve existing entry for metadata; tokens come from the registry task.
286
302
  const existing = prev[task.id]
287
303
  next[task.id] = {
288
304
  id: task.id,
289
305
  name: existing?.name || task.agentType.charAt(0).toUpperCase() + task.agentType.slice(1),
290
306
  description: existing?.description || task.description,
291
307
  startTime: existing?.startTime || task.startedAt.getTime(),
292
- tokensUsed: existing?.tokensUsed || 0,
308
+ tokensUsed: task.tokensUsed ?? 0,
293
309
  status: task.status === 'running' ? 'running' : 'completed',
294
310
  }
295
311
  }