@miphamai/cli 0.37.0 → 0.39.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.
Files changed (69) hide show
  1. package/bin/mipham.ts +25 -0
  2. package/package.json +1 -1
  3. package/src/agent/cross-session/discovery.ts +45 -1
  4. package/src/agent/cross-session/file-inbox.ts +15 -0
  5. package/src/agent/message-router.ts +22 -2
  6. package/src/agent/sub-agent.ts +4 -3
  7. package/src/config/defaults.ts +19 -1
  8. package/src/config/keys-manager.ts +11 -1
  9. package/src/config/loader.ts +20 -10
  10. package/src/core/context.ts +86 -56
  11. package/src/core/credential-masker/output-scrub.ts +5 -0
  12. package/src/core/crsi-sandbox.ts +19 -7
  13. package/src/core/engine.ts +69 -35
  14. package/src/core/instructions.ts +4 -42
  15. package/src/core/permission.ts +7 -1
  16. package/src/core/red-team.ts +20 -5
  17. package/src/core/self-critique.ts +3 -1
  18. package/src/core/session-log.ts +225 -0
  19. package/src/core/session-store.ts +119 -34
  20. package/src/core/workspace-trust.ts +25 -2
  21. package/src/daemon/auth.ts +9 -6
  22. package/src/daemon/cors.ts +24 -16
  23. package/src/daemon/rate-limiter.ts +4 -0
  24. package/src/daemon/server.ts +24 -3
  25. package/src/i18n-core/locales/en-US.json +4 -0
  26. package/src/i18n-core/locales/zh-CN.json +4 -0
  27. package/src/index.tsx +62 -30
  28. package/src/mcp/client.ts +56 -17
  29. package/src/mcp/http-transport.ts +2 -0
  30. package/src/mcp/oauth.ts +1 -1
  31. package/src/providers/llm-replay.ts +36 -0
  32. package/src/providers/llm.ts +16 -0
  33. package/src/providers/registry.ts +2 -1
  34. package/src/security/gate.ts +10 -0
  35. package/src/security/url.ts +37 -14
  36. package/src/shared/constants.ts +4 -486
  37. package/src/shared/types.ts +6 -437
  38. package/src/skills/fork-executor.ts +3 -1
  39. package/src/skills/loader.ts +2 -1
  40. package/src/skills/seam.ts +18 -0
  41. package/src/tools/agent/agent.ts +1 -0
  42. package/src/tools/agent/skill.ts +1 -0
  43. package/src/tools/exec/bash.ts +141 -129
  44. package/src/tools/exec/git.ts +17 -0
  45. package/src/tools/file/read.ts +73 -66
  46. package/src/tools/file/write.ts +1 -1
  47. package/src/tools/index.ts +54 -132
  48. package/src/tools/network/web-fetch.ts +41 -26
  49. package/src/tools/seam.ts +25 -0
  50. package/src/tools/validation.ts +91 -0
  51. package/src/ui/app.tsx +36 -0
  52. package/src/ui/commands.ts +27 -6
  53. package/src/ui/input.tsx +21 -0
  54. package/src/vajra/compose/assemble.ts +19 -0
  55. package/src/vajra/compose/bundle.ts +39 -0
  56. package/src/vajra/compose/dump.ts +5 -0
  57. package/src/vajra/compose/index.ts +4 -0
  58. package/src/vajra/compose/mount.ts +30 -0
  59. package/src/vajra/context.ts +183 -0
  60. package/src/vajra/events.ts +13 -0
  61. package/src/vajra/index.ts +4 -0
  62. package/src/vajra/leaf/plan-runner.ts +94 -0
  63. package/src/vajra/service.ts +15 -0
  64. package/src/workflow/primitives/agent.ts +3 -1
  65. package/src/workflow/runtime.ts +23 -15
  66. package/src/workflow/sandbox.ts +51 -9
  67. package/src/core/compaction-progress.ts +0 -205
  68. package/src/core/context-compact.ts +0 -124
  69. package/src/core/context-drain.ts +0 -113
@@ -1,205 +0,0 @@
1
- /**
2
- * P1.1: Compaction Progress Tracker
3
- *
4
- * Provides user-visible progress feedback during context compaction:
5
- * - Phase indicator (which compaction layer is active)
6
- * - Message count (how many messages being processed)
7
- * - Retry countdown for API-based summarization
8
- * - Stuck detection hints
9
- *
10
- * Designed as an event emitter that the UI layer subscribes to.
11
- * Falls back to stderr output when no UI subscriber is registered.
12
- */
13
-
14
- // ── Types ──
15
-
16
- export type CompactionPhase =
17
- 'microcompact' | 'snip' | 'reactive-compact' | 'emergency-drain' | 'complete'
18
-
19
- export interface CompactionStatus {
20
- phase: CompactionPhase
21
- /** Human-readable description of current action */
22
- message: string
23
- /** 0-100 percentage, or -1 if indeterminate */
24
- percent: number
25
- /** Total messages before compaction */
26
- messagesBefore: number
27
- /** Messages remaining after compaction */
28
- messagesAfter: number
29
- /** Tokens before compaction */
30
- tokensBefore: number
31
- /** Tokens after compaction */
32
- tokensAfter: number
33
- /** Elapsed ms since compaction started */
34
- elapsedMs: number
35
- /** Retry count for summarization API calls */
36
- retryCount: number
37
- /** Whether a retry is in progress */
38
- retrying: boolean
39
- /** Whether the compaction appears stuck (no progress for >30s) */
40
- stuck: boolean
41
- }
42
-
43
- export type ProgressCallback = (status: CompactionStatus) => void
44
-
45
- // ── Constants ──
46
-
47
- /** Timeout in ms after which we consider compaction "stuck" */
48
- const STUCK_TIMEOUT_MS = 30_000
49
-
50
- /** Max retries for summarizer API calls */
51
- const MAX_SUMMARIZER_RETRIES = 3
52
-
53
- /** Retry delay in ms between summarizer attempts */
54
- const RETRY_DELAY_MS = 2_000
55
-
56
- // ── Tracker ──
57
-
58
- export class CompactionProgressTracker {
59
- private callback: ProgressCallback | null = null
60
- private status: CompactionStatus
61
- private startTime = 0
62
- private lastProgressTime = 0
63
-
64
- constructor(messagesBefore: number, tokensBefore: number) {
65
- this.status = {
66
- phase: 'microcompact',
67
- message: 'Starting compaction...',
68
- percent: 0,
69
- messagesBefore,
70
- messagesAfter: messagesBefore,
71
- tokensBefore,
72
- tokensAfter: tokensBefore,
73
- elapsedMs: 0,
74
- retryCount: 0,
75
- retrying: false,
76
- stuck: false,
77
- }
78
- this.startTime = Date.now()
79
- this.lastProgressTime = Date.now()
80
- }
81
-
82
- /** Register a UI callback for progress updates. */
83
- onProgress(cb: ProgressCallback): void {
84
- this.callback = cb
85
- }
86
-
87
- /** Update phase with new values. */
88
- update(partial: {
89
- phase?: CompactionPhase
90
- message?: string
91
- percent?: number
92
- messagesAfter?: number
93
- tokensAfter?: number
94
- retrying?: boolean
95
- }): void {
96
- if (partial.phase !== undefined) this.status.phase = partial.phase
97
- if (partial.message !== undefined) this.status.message = partial.message
98
- if (partial.percent !== undefined) this.status.percent = partial.percent
99
- if (partial.messagesAfter !== undefined) this.status.messagesAfter = partial.messagesAfter
100
- if (partial.tokensAfter !== undefined) this.status.tokensAfter = partial.tokensAfter
101
- if (partial.retrying !== undefined) this.status.retrying = partial.retrying
102
-
103
- this.status.elapsedMs = Date.now() - this.startTime
104
-
105
- // Check for stuck detection
106
- if (this.status.phase !== 'complete') {
107
- const timeSinceProgress = Date.now() - this.lastProgressTime
108
- this.status.stuck = timeSinceProgress > STUCK_TIMEOUT_MS
109
- }
110
-
111
- this.emit()
112
- }
113
-
114
- /** Mark current retry attempt. */
115
- onRetry(attempt: number): void {
116
- this.status.retryCount = attempt
117
- this.status.retrying = true
118
- this.emit()
119
- }
120
-
121
- /** Mark retry as complete (success or giving up). */
122
- onRetryComplete(): void {
123
- this.status.retrying = false
124
- this.lastProgressTime = Date.now()
125
- this.status.stuck = false
126
- this.emit()
127
- }
128
-
129
- /** Mark compaction as complete. */
130
- complete(messagesAfter: number, tokensAfter: number): void {
131
- this.status.phase = 'complete'
132
- this.status.percent = 100
133
- this.status.messagesAfter = messagesAfter
134
- this.status.tokensAfter = tokensAfter
135
- this.status.message = `Compaction complete: ${this.status.messagesBefore} → ${messagesAfter} messages`
136
- this.status.elapsedMs = Date.now() - this.startTime
137
- this.emit()
138
- }
139
-
140
- /** Get current status. */
141
- getStatus(): CompactionStatus {
142
- return { ...this.status }
143
- }
144
-
145
- /** Generate a compact summary string for CLI display. */
146
- getSummary(): string {
147
- const s = this.status
148
- const elapsed = (s.elapsedMs / 1000).toFixed(1)
149
- const stuckHint = s.stuck ? ' (stalled — press Esc to cancel)' : ''
150
- const retryHint = s.retrying ? ` [retry ${s.retryCount}/${MAX_SUMMARIZER_RETRIES}...]` : ''
151
- const pct = s.percent >= 0 ? ` ${s.percent}%` : ''
152
- return `🔄 [${s.phase}]${pct} ${s.message}${retryHint}${stuckHint} (${elapsed}s)`
153
- }
154
-
155
- private emit(): void {
156
- if (this.callback) {
157
- try {
158
- this.callback(this.getStatus())
159
- } catch {
160
- // UI callback failure shouldn't block compaction
161
- }
162
- }
163
- }
164
-
165
- // ── Static helpers ──
166
-
167
- /** Retry a summarizer call with backoff and progress tracking. */
168
- static async retrySummarizer(
169
- summarizer: () => Promise<string>,
170
- tracker: CompactionProgressTracker,
171
- messagesToSummarize: number,
172
- ): Promise<string> {
173
- let lastError: Error | undefined
174
-
175
- for (let attempt = 1; attempt <= MAX_SUMMARIZER_RETRIES; attempt++) {
176
- try {
177
- tracker.update({
178
- message: `Summarizing ${messagesToSummarize} messages${attempt > 1 ? ` (attempt ${attempt}/${MAX_SUMMARIZER_RETRIES})` : ''}...`,
179
- percent: 30 + attempt * 20,
180
- retrying: attempt > 1,
181
- })
182
-
183
- const result = await summarizer()
184
- tracker.onRetryComplete()
185
- return result
186
- } catch (err) {
187
- lastError = err instanceof Error ? err : new Error(String(err))
188
-
189
- if (attempt < MAX_SUMMARIZER_RETRIES) {
190
- tracker.onRetry(attempt)
191
- // Wait before retry
192
- await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS))
193
- tracker.onRetryComplete()
194
- }
195
- }
196
- }
197
-
198
- tracker.update({
199
- message: `Summarization failed after ${MAX_SUMMARIZER_RETRIES} attempts — falling back to truncation`,
200
- percent: 60,
201
- })
202
-
203
- throw lastError ?? new Error('Summarization failed')
204
- }
205
- }
@@ -1,124 +0,0 @@
1
- import type { ContextManager, Summarizer } from './context'
2
- import { snipMessages } from './context-snip'
3
- import { CompactionProgressTracker } from './compaction-progress'
4
-
5
- /**
6
- * Layer 3: Reactive Compact — API-based summarization.
7
- *
8
- * When the context exceeds the compaction threshold, uses an LLM summarizer
9
- * to condense the conversation history, keeping recent messages intact.
10
- *
11
- * After compaction, the context contains:
12
- * 1. A summary message of the truncated history
13
- * 2. The most recent N messages (kept intact)
14
- * 3. The system prompt (unchanged)
15
- */
16
- /**
17
- * Adaptive keep-recent count: scales with context window size.
18
- * 128K → 20, 200K → 20, 500K → 40, 1M → 80
19
- */
20
- function getKeepRecent(contextWindow: number): number {
21
- return Math.max(20, Math.floor(contextWindow / 12500))
22
- }
23
-
24
- export interface CompactResult {
25
- /** Messages before compaction */
26
- beforeCount: number
27
- /** Messages after compaction */
28
- afterCount: number
29
- /** Whether compaction was actually performed */
30
- compacted: boolean
31
- /** Progress summary for display */
32
- summary: string
33
- }
34
-
35
- export async function reactiveCompact(
36
- context: ContextManager,
37
- summarizer: Summarizer,
38
- heading: string,
39
- keepRecent?: number,
40
- ): Promise<CompactResult> {
41
- // Use explicit override, or adaptive default based on context window
42
- const effectiveKeepRecent = keepRecent ?? getKeepRecent(context.getMaxTokens())
43
- const messages = context.getMessages()
44
- const tokensBefore = context.getEstimatedTokens()
45
-
46
- if (messages.length <= effectiveKeepRecent + 4) {
47
- return {
48
- beforeCount: messages.length,
49
- afterCount: messages.length,
50
- compacted: false,
51
- summary: `Context has ${messages.length} messages — no compaction needed (threshold: ${effectiveKeepRecent + 4}).`,
52
- }
53
- }
54
-
55
- // ── Progress tracker ──
56
- const tracker = new CompactionProgressTracker(messages.length, tokensBefore)
57
- tracker.update({ phase: 'snip', message: 'Snipping empty pairs...', percent: 5 })
58
-
59
- // First, run snip to remove empty pairs
60
- const { messages: snipped } = snipMessages(messages)
61
- tracker.update({
62
- phase: 'snip',
63
- message: `Snipped ${messages.length - snipped.length} empty pairs`,
64
- percent: 15,
65
- messagesAfter: snipped.length,
66
- })
67
-
68
- if (snipped.length <= effectiveKeepRecent + 4) {
69
- const afterCount = snipped.length
70
- tracker.complete(afterCount, tokensBefore)
71
- return {
72
- beforeCount: messages.length,
73
- afterCount,
74
- compacted: true,
75
- summary: `Snip-only: ${messages.length} → ${afterCount} messages (removed ${messages.length - afterCount} empty pairs).`,
76
- }
77
- }
78
-
79
- // Split: old messages to summarize, recent messages to keep
80
- const toSummarize = snipped.slice(0, -effectiveKeepRecent)
81
- const toKeep = snipped.slice(-effectiveKeepRecent)
82
-
83
- tracker.update({
84
- phase: 'reactive-compact',
85
- message: `Summarizing ${toSummarize.length} messages...`,
86
- percent: 20,
87
- })
88
-
89
- // Generate summary with retry logic
90
- let summary: string
91
- try {
92
- summary = await CompactionProgressTracker.retrySummarizer(
93
- () => summarizer(toSummarize, heading),
94
- tracker,
95
- toSummarize.length,
96
- )
97
- } catch {
98
- // On summarizer failure, do a simple truncation
99
- tracker.update({
100
- message: `Summarizer unavailable — truncating to last ${effectiveKeepRecent} messages.`,
101
- percent: 80,
102
- })
103
- summary = `Earlier conversation (${toSummarize.length} messages) omitted due to context limits.`
104
- }
105
-
106
- // Rebuild messages: summary + recent
107
- const summaryMsg = {
108
- role: 'user' as const,
109
- content: `[Earlier conversation summary — ${heading}]: ${summary.slice(0, 8000)}`,
110
- }
111
-
112
- const newMessages = [summaryMsg, ...toKeep]
113
- context.replaceMessages(newMessages)
114
-
115
- const tokensAfter = context.getEstimatedTokens()
116
- tracker.complete(newMessages.length, tokensAfter)
117
-
118
- return {
119
- beforeCount: messages.length,
120
- afterCount: newMessages.length,
121
- compacted: true,
122
- summary: tracker.getSummary(),
123
- }
124
- }
@@ -1,113 +0,0 @@
1
- import type { ContextManager } from './context'
2
- import type { CompactionProgressTracker } from './compaction-progress'
3
-
4
- const MINIMAL_KEEP = 5
5
- let drainLevel = 0
6
-
7
- /**
8
- * Layer 4: Emergency Drain — 413 error recovery.
9
- *
10
- * Called when a 413 (context too large) error is received.
11
- * Progressively strips messages until the context fits:
12
- *
13
- * Level 1: Drop earliest 50% of messages
14
- * Level 2+: Keep only system prompt + last 5 messages
15
- *
16
- * Returns drain result with progress summary.
17
- */
18
- export interface DrainResult {
19
- /** Whether recovery was possible */
20
- recovered: boolean
21
- /** Messages before drain */
22
- beforeCount: number
23
- /** Messages after drain */
24
- afterCount: number
25
- /** Current drain level */
26
- level: number
27
- /** Human-readable summary */
28
- summary: string
29
- }
30
-
31
- export async function emergencyDrain(
32
- context: ContextManager,
33
- progress?: CompactionProgressTracker,
34
- ): Promise<DrainResult> {
35
- const messages = context.getMessages()
36
-
37
- if (messages.length <= MINIMAL_KEEP) {
38
- return {
39
- recovered: false,
40
- beforeCount: messages.length,
41
- afterCount: messages.length,
42
- level: drainLevel,
43
- summary: `Cannot drain further: already at minimal ${messages.length} messages.`,
44
- }
45
- }
46
-
47
- progress?.update({
48
- phase: 'emergency-drain',
49
- message: `Emergency drain level ${drainLevel + 1}: recovering from 413 error...`,
50
- percent: 50,
51
- })
52
-
53
- if (drainLevel === 0) {
54
- // First attempt: drop earliest 50%
55
- const keepCount = Math.max(MINIMAL_KEEP, Math.floor(messages.length / 2))
56
- const kept = messages.slice(-keepCount)
57
- context.replaceMessages(kept)
58
- drainLevel++
59
- progress?.update({
60
- phase: 'emergency-drain',
61
- message: `Drain level 1: dropped ${messages.length - keepCount} messages (${messages.length} → ${keepCount})`,
62
- percent: 80,
63
- messagesAfter: keepCount,
64
- })
65
- return {
66
- recovered: true,
67
- beforeCount: messages.length,
68
- afterCount: keepCount,
69
- level: drainLevel,
70
- summary: `Emergency drain level 1: ${messages.length} → ${keepCount} messages (dropped earliest 50%).`,
71
- }
72
- }
73
-
74
- // Subsequent attempts: keep only system + last MINIMAL_KEEP messages
75
- const kept = messages.slice(-MINIMAL_KEEP)
76
-
77
- // Add a summary placeholder if we're dropping a lot
78
- if (messages.length > MINIMAL_KEEP * 2) {
79
- const summaryMsg = {
80
- role: 'user' as const,
81
- content: `[Emergency context drain level ${drainLevel + 1}: ${messages.length - MINIMAL_KEEP} earlier messages discarded due to token limit.]`,
82
- }
83
- context.replaceMessages([summaryMsg, ...kept])
84
- } else {
85
- context.replaceMessages(kept)
86
- }
87
-
88
- drainLevel++
89
- progress?.update({
90
- phase: 'emergency-drain',
91
- message: `Drain level ${drainLevel}: ${messages.length} → ${kept.length} messages`,
92
- percent: 90,
93
- messagesAfter: kept.length,
94
- })
95
-
96
- return {
97
- recovered: true,
98
- beforeCount: messages.length,
99
- afterCount: kept.length,
100
- level: drainLevel,
101
- summary: `Emergency drain level ${drainLevel}: ${messages.length} → ${kept.length} messages.`,
102
- }
103
- }
104
-
105
- /** Reset drain level (call at session start). */
106
- export function resetDrainLevel(): void {
107
- drainLevel = 0
108
- }
109
-
110
- /** Get current drain level for display. */
111
- export function getDrainLevel(): number {
112
- return drainLevel
113
- }