@miphamai/cli 0.39.0 → 0.40.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.
@@ -1,8 +1,443 @@
1
- // ── CLI 内部类型(引用活服务,不可共享)──
2
- // 纯数据类型(Message/StreamChunk/ToolResult/配置/常量等)见 @mipham/shared —— 唯一真源。
3
- export * from '@mipham/shared/types'
4
- import type { HookDefinition, ToolCategory, ToolPermission, ToolResult } from '@mipham/shared'
1
+ // ── 类型唯一真源(vendored from packages/shared/src/types.ts)──
2
+ // 为让 npm 包自包含(运行时无需 @mipham/shared workspace 依赖),
3
+ // 此处从 packages/shared/src/types.ts 同步。改动共享类型时两边同步。
4
+ // ── Provider Types ──
5
+ export type ProtocolType = 'openai-compatible' | 'anthropic' | 'custom'
6
+
7
+ export interface ModelInfo {
8
+ id: string
9
+ name: string
10
+ providerId: string
11
+ contextWindow: number
12
+ maxOutput: number
13
+ vision: boolean
14
+ status: 'active' | 'upcoming' | 'deprecated'
15
+ }
16
+
17
+ export interface ProviderConfig {
18
+ id: string
19
+ name: string
20
+ protocol: ProtocolType
21
+ baseUrl?: string
22
+ apiKey: string
23
+ models: ModelInfo[]
24
+ status?: 'active' | 'upcoming'
25
+ }
26
+
27
+ // ── Message Types ──
28
+ export interface TextContent {
29
+ type: 'text'
30
+ text: string
31
+ }
32
+ export interface ImageContent {
33
+ type: 'image_url'
34
+ image_url: { url: string }
35
+ }
36
+ export interface ToolUseContent {
37
+ type: 'tool_use'
38
+ id: string
39
+ name: string
40
+ input: Record<string, unknown>
41
+ }
42
+ export interface ToolResultContent {
43
+ type: 'tool_result'
44
+ tool_use_id: string
45
+ content: string
46
+ }
47
+ export interface ThinkingContent {
48
+ type: 'thinking'
49
+ thinking: string
50
+ }
51
+ export type ContentBlock =
52
+ TextContent | ImageContent | ToolUseContent | ToolResultContent | ThinkingContent
53
+
54
+ export interface Message {
55
+ role: 'system' | 'user' | 'assistant'
56
+ content: string | ContentBlock[]
57
+ /** DeepSeek reasoning tokens — must be passed back to the API in multi-turn conversations. */
58
+ reasoning_content?: string
59
+ }
60
+
61
+ // ── Tool Types ──
62
+ export type ToolPermission = 'auto' | 'ask' | 'bypass'
63
+ export type ToolCategory =
64
+ 'file' | 'exec' | 'agent' | 'network' | 'system' | 'artifact' | 'scheduling'
65
+
66
+ // ── Artifact Types ──
67
+ export interface ArtifactEntry {
68
+ name: string
69
+ path: string
70
+ url: string
71
+ size: number
72
+ type: 'html' | 'svg'
73
+ createdAt: string
74
+ sessionId: string
75
+ versions?: string[] // version tags e.g. ['v1', 'v2']
76
+ versionCount?: number
77
+ }
78
+
79
+ export interface ArtifactManifest {
80
+ version: 1
81
+ artifacts: ArtifactEntry[]
82
+ port?: number
83
+ }
84
+
85
+ export interface ToolResult {
86
+ success: boolean
87
+ content: string
88
+ error?: string
89
+ }
90
+
91
+ // ── Task Notification Types ──
92
+ export interface TaskNotification {
93
+ taskId: string
94
+ status: 'started' | 'completed' | 'failed'
95
+ description: string
96
+ content?: string
97
+ error?: string
98
+ }
99
+
100
+ // ── Stream Types ──
101
+ export interface StreamChunk {
102
+ type:
103
+ | 'text'
104
+ | 'tool_use'
105
+ | 'tool_result'
106
+ | 'thinking'
107
+ | 'stop'
108
+ | 'error'
109
+ | 'warning'
110
+ | 'task_notification'
111
+ | 'usage'
112
+ content?: string
113
+ toolUse?: ToolUseContent
114
+ tool_use_id?: string
115
+ error?: string
116
+ /** DeepSeek reasoning tokens accumulated during this stream. */
117
+ reasoning_content?: string
118
+ /** Anthropic thinking block content (DeepSeek Anthropic endpoint). */
119
+ thinking?: string
120
+ /** Background task notification payload (type: 'task_notification'). */
121
+ taskNotification?: TaskNotification
122
+ /** API-reported input token count (type: 'usage'). */
123
+ inputTokens?: number
124
+ /** API-reported output token count (type: 'usage'). */
125
+ outputTokens?: number
126
+ }
127
+
128
+ // ── Config Types ──
129
+ export interface MiphamConfig {
130
+ version: string
131
+ defaultProvider: string
132
+ defaultModel: string
133
+ permission: ToolPermission
134
+ /** Org-level permission restrictions (forbiddenModes, maxAllowedMode). */
135
+ permissionRestrictions?: PermissionRestrictions
136
+ providers: ProviderConfig[]
137
+ skills?: { paths: string[]; mcpServers: McpServerConfig[] }
138
+ marketplace?: {
139
+ /** If set, only allow installs from matching repos (e.g. ["One-Mipham/*"]) */
140
+ strictKnownMarketplaces?: string[]
141
+ /** Block installs from matching repos (e.g. ["malicious-org/*"]) */
142
+ blockedMarketplaces?: string[]
143
+ }
144
+ /** Phase 9 feature flags. All default to true. */
145
+ features?: Partial<FeatureFlags>
146
+ /** Phase 10 CRSI feature flags. All default to true. */
147
+ crsi?: Partial<CrsiConfig>
148
+ }
149
+
150
+ export interface FeatureFlags {
151
+ mcp: { oauthEnabled: boolean }
152
+ context: { useRealTokenizer: boolean; adaptiveThresholds: boolean }
153
+ }
154
+
155
+ export interface CrsiConfig {
156
+ /** Extract and inject experience rules into agent system prompts. */
157
+ ruleInjection: boolean
158
+ /** Intercept tool calls and auto-fix known failure patterns before execution. */
159
+ preToolHook: boolean
160
+ /** Analyze agent outcomes for recurring failure patterns. */
161
+ autoPatternAnalysis: boolean
162
+ /** Auto-degrade/disable low-effectiveness rules based on success-rate tracking. */
163
+ autoRuleManagement: boolean
164
+ }
165
+
166
+ export interface McpServerConfig {
167
+ name: string
168
+ /** stdio: executable to spawn (mutually exclusive with `url`). */
169
+ command?: string
170
+ /** stdio: args passed to `command`. */
171
+ args?: string[]
172
+ /** HTTP: Streamable HTTP endpoint (mutually exclusive with `command`). */
173
+ url?: string
174
+ /** HTTP: extra request headers (e.g. Authorization). */
175
+ headers?: Record<string, string>
176
+ env?: Record<string, string>
177
+ auth?: {
178
+ type: 'oauth'
179
+ authorizationUrl: string
180
+ tokenUrl: string
181
+ clientId: string
182
+ scopes?: string[]
183
+ redirectPort?: number
184
+ }
185
+ }
186
+
187
+ // ── Hook Types ──
188
+ export type HookEvent =
189
+ | 'PreToolUse'
190
+ | 'PostToolUse'
191
+ | 'PostToolUseFailure'
192
+ | 'SessionStart'
193
+ | 'SessionEnd'
194
+ | 'Notification'
195
+ | 'Stop'
196
+ | 'UserPromptSubmit'
197
+ | 'PreCompact'
198
+ | 'PostCompact'
199
+ | 'ConfigChange'
200
+ | 'SubagentStart'
201
+ | 'SubagentStop'
202
+ | 'PreInference'
203
+
204
+ export type HookType = 'command' | 'http' | 'code' | 'mcp_tool'
205
+
206
+ export interface HookConfig {
207
+ type: HookType
208
+ command?: string
209
+ args?: string[]
210
+ url?: string
211
+ method?: 'GET' | 'POST'
212
+ headers?: Record<string, string>
213
+ mcpServer?: string
214
+ mcpTool?: string
215
+ continueOnBlock?: boolean
216
+ }
217
+
218
+ export interface HookDefinition {
219
+ event: HookEvent
220
+ toolName?: string
221
+ handler: (context: HookContext) => Promise<HookResult>
222
+ }
223
+
224
+ export interface HookContext {
225
+ event: HookEvent
226
+ toolName?: string
227
+ toolInput?: Record<string, unknown>
228
+ toolResult?: ToolResult
229
+ sessionId: string
230
+ userPrompt?: string
231
+ configKey?: string
232
+ configValue?: unknown
233
+ /** PreInference: full conversation messages for DLP inspection. */
234
+ messages?: Array<{ role: string; content: string }>
235
+ /** PreInference: recent tool calls and their results. */
236
+ toolCalls?: Array<{
237
+ name: string
238
+ input: Record<string, unknown>
239
+ resultPreview: string
240
+ }>
241
+ /** PreInference: current provider ID. */
242
+ provider?: string
243
+ /** PreInference: current model ID. */
244
+ model?: string
245
+ }
246
+
247
+ export interface HookResult {
248
+ allowed: boolean
249
+ reason?: string
250
+ modifiedInput?: Record<string, unknown>
251
+ decision?: 'allow' | 'block'
252
+ permissionDecision?: 'allow' | 'deny' | 'ask' | 'defer'
253
+ additionalContext?: string
254
+ updatedOutput?: string
255
+ }
256
+
257
+ // ── Instruction Types ──
258
+ export interface InstructionFile {
259
+ path: string
260
+ level: 'group' | 'company' | 'project' | 'directory' | 'user'
261
+ privacy: 'public' | 'project' | 'private'
262
+ language: string
263
+ content: string
264
+ frontmatter: Record<string, unknown>
265
+ }
266
+
267
+ // ── Permission Types ──
268
+ /** Six explicit permission modes matching Claude Code's permission architecture */
269
+ export type PermissionMode =
270
+ 'default' | 'acceptEdits' | 'plan' | 'auto' | 'dontAsk' | 'bypassPermissions'
5
271
 
272
+ /** Backward-compatible alias: PermissionMode plus legacy 'ask' and 'bypass' */
273
+ export type PermissionLevel = PermissionMode | 'ask' | 'bypass'
274
+
275
+ /** Org-level restrictions that cap or forbid specific permission modes. */
276
+ export interface PermissionRestrictions {
277
+ /** Modes that may not be entered (cycle skips them). */
278
+ forbiddenModes?: PermissionMode[]
279
+ /** Ceiling — modes ranked higher (more permissive) than this are treated as forbidden. */
280
+ maxAllowedMode?: PermissionMode
281
+ }
282
+
283
+ export interface PermissionConfig {
284
+ mode: PermissionMode
285
+ allow: string[]
286
+ deny: string[]
287
+ /** Optional org-level restrictions enforced on every mode transition. */
288
+ restrictions?: PermissionRestrictions
289
+ }
290
+
291
+ export interface PermissionRuleEntry {
292
+ pattern: string // e.g., "Bash(git:*)"
293
+ level: 'allow' | 'deny' | 'ask'
294
+ compiled: RegExp
295
+ }
296
+
297
+ export interface PermissionRule {
298
+ toolName: string
299
+ level: PermissionLevel
300
+ pattern?: string
301
+ }
302
+
303
+ // ── Inference Hook (DLP) Types ──
304
+
305
+ /** Configuration for the PreInference DLP hook, loaded from config.yml. */
306
+ export interface InferenceHookConfig {
307
+ /** DLP server endpoint (HTTPS). Empty = feature disabled. */
308
+ endpoint: string
309
+ /** HMAC signing secret (format: mis_<random>). */
310
+ signing_secret: string
311
+ /** Request timeout in milliseconds. Default 5000. */
312
+ timeout: number
313
+ /** Failure posture: 'fail-closed' blocks on error, 'fail-open' allows. */
314
+ on_failure: 'fail-closed' | 'fail-open'
315
+ /** Organization identifier (optional, sent in payload). */
316
+ organization_id: string
317
+ /** Additional custom headers to send with each request. */
318
+ headers: Record<string, string>
319
+ }
320
+
321
+ /** Outgoing request to the DLP server. */
322
+ export interface InferenceCheckRequest {
323
+ type: 'inference_check'
324
+ id: string
325
+ created_at: string
326
+ data: {
327
+ type: 'pre_inference'
328
+ session_id: string
329
+ organization_id?: string
330
+ provider: string
331
+ model: string
332
+ messages: Array<{ role: string; content: string }>
333
+ tool_calls: Array<{
334
+ name: string
335
+ input: Record<string, unknown>
336
+ result_preview: string
337
+ }>
338
+ }
339
+ }
340
+
341
+ /** Response from the DLP server. */
342
+ export interface InferenceCheckResponse {
343
+ verdict: 'allow' | 'deny'
344
+ reason?: string
345
+ }
346
+
347
+ // ── Credential Masking Types ──
348
+
349
+ /** Full-file masking rule: entire file content replaced with sentinel. */
350
+ export interface CredentialFullMaskRule {
351
+ path: string
352
+ mode: 'full'
353
+ }
354
+
355
+ /** Per-extract-pattern configuration with optional field-based extraction. */
356
+ export interface CredentialExtractPattern {
357
+ /** Regex pattern to match. When `field` is set, applied to field value only. */
358
+ pattern: string
359
+ /** Optional replacement string. Defaults to CREDENTIAL_SENTINEL. */
360
+ replacement?: string
361
+ /** 🆕 JSON key to extract before applying pattern. If unset, pattern runs against full content. */
362
+ field?: string
363
+ }
364
+
365
+ /** Extract-based masking rule: only regex-matched tokens are replaced. */
366
+ export interface CredentialExtractRule {
367
+ path: string
368
+ mode: 'extract'
369
+ extract: CredentialExtractPattern[]
370
+ /** 🆕 Behavior when no extract pattern matches: 'mask' (replace all) or 'passthrough' (keep). Default: 'mask'. */
371
+ onExtractNoMatch?: 'mask' | 'passthrough'
372
+ }
373
+
374
+ /** 🆕 JWT-aware masking rule: decode payload and mask specified claims. */
375
+ export interface JwtMaskingRule {
376
+ path: string
377
+ type: 'jwt'
378
+ decode: 'jwt'
379
+ /** Claim names to mask in the JWT payload (e.g. ["sub", "email"]). */
380
+ maskClaims: string[]
381
+ }
382
+
383
+ /** 🆕 AWS credential pair masking rule: detect key pairs and optionally re-sign. */
384
+ export interface AwsMaskingRule {
385
+ path: string
386
+ type: 'aws'
387
+ awsPairs: boolean
388
+ sigv4: boolean
389
+ }
390
+
391
+ export type CredentialFileRule =
392
+ CredentialFullMaskRule | CredentialExtractRule | JwtMaskingRule | AwsMaskingRule
393
+
394
+ /** Configuration for credential masking, loaded from config.yml. */
395
+ export interface CredentialMaskingConfig {
396
+ enabled: boolean
397
+ files: CredentialFileRule[]
398
+ output_scrubbing: {
399
+ enabled: boolean
400
+ patterns: string[]
401
+ }
402
+ env_filter: {
403
+ enabled: boolean
404
+ patterns: string[]
405
+ }
406
+ }
407
+
408
+ // ── Cross-Session Messaging Types ──
409
+
410
+ /** Controls how inbound cross-session messages are handled. */
411
+ export type CrossSessionInbound = 'allow' | 'ask' | 'deny'
412
+
413
+ /** 🆕 Configuration for cross-session messaging. */
414
+ export interface CrossSessionConfig {
415
+ crossSessionInbound: CrossSessionInbound
416
+ dialogExpiry: number // seconds
417
+ }
418
+
419
+ /** Session information exposed via ListAgents. */
420
+ export interface SessionInfo {
421
+ id: string
422
+ name: string
423
+ machine: string
424
+ pid: number
425
+ startedAt: string
426
+ cwd?: string
427
+ provider?: string
428
+ model?: string
429
+ }
430
+
431
+ // ── Background Agent Types ──
432
+
433
+ export interface BackgroundAgentConfig {
434
+ auto_commit: boolean
435
+ auto_push: boolean
436
+ auto_worktree: boolean
437
+ commit_coauthors: boolean
438
+ }
439
+
440
+ // ── CLI 内部类型(引用活服务,不可共享)──
6
441
  export interface ToolContext {
7
442
  cwd: string
8
443
  sessionId: string
@@ -668,6 +668,13 @@ const crsiRestoreCmd: CommandHandler = (ctx, args) => {
668
668
  return { content: `Rule \`${ruleId}\` has been re-enabled.` }
669
669
  }
670
670
 
671
+ /** Render a tiny ASCII sparkline from a series of 0-1 values (success rates). */
672
+ function crsiSparkline(values: number[]): string {
673
+ if (values.length === 0) return '—'
674
+ const chars = '▁▂▃▄▅▆▇█'
675
+ return values.map((v) => chars[Math.min(Math.max(Math.round(v * 7), 0), 7)] ?? '▁').join('')
676
+ }
677
+
671
678
  const crsiStatsCmd: CommandHandler = async (ctx) => {
672
679
  const engine = ctx.engine.getRuleEngine()
673
680
  const tracker = ctx.engine.getEffectivenessTracker()
@@ -682,21 +689,43 @@ const crsiStatsCmd: CommandHandler = async (ctx) => {
682
689
  lines.push(`Auto-generated: ${rules.filter((r) => r.source === 'pattern-analyzer').length}`)
683
690
  lines.push(`Manual: ${rules.filter((r) => r.source === 'manual').length}`)
684
691
 
685
- if (tracker) {
692
+ const effs = tracker?.allRules ?? []
693
+ if (effs.length > 0) {
686
694
  let totalInterceptions = 0
687
695
  let totalSuccesses = 0
688
- for (const r of rules) {
689
- const eff = tracker.getEffectiveness(r.id)
690
- if (eff) {
691
- totalInterceptions += eff.appliedCount
692
- totalSuccesses += eff.successAfterCount
693
- }
696
+ for (const eff of effs) {
697
+ totalInterceptions += eff.appliedCount
698
+ totalSuccesses += eff.successAfterCount
694
699
  }
695
700
  lines.push('')
696
701
  lines.push(`Total interceptions: ${totalInterceptions}`)
697
702
  lines.push(
698
703
  `Success rate after rules: ${totalInterceptions > 0 ? Math.round((totalSuccesses / totalInterceptions) * 100) : 0}%`,
699
704
  )
705
+
706
+ lines.push('')
707
+ lines.push('### Rule effectiveness')
708
+ lines.push('| Rule | Status | Applied | Success | Trend |')
709
+ lines.push('|------|--------|---------|---------|-------|')
710
+ const statusIcon: Record<string, string> = { active: '🟢', degrading: '🟡', disabled: '⚫' }
711
+ for (const eff of effs) {
712
+ const icon = statusIcon[eff.status] ?? '⚪'
713
+ const rate =
714
+ eff.appliedCount > 0 ? Math.round((eff.successAfterCount / eff.appliedCount) * 100) : 0
715
+ const trend = crsiSparkline(eff.evaluationHistory.map((h) => 1 - h.failureRate))
716
+ lines.push(
717
+ `| ${eff.ruleId} | ${icon} ${eff.status} | ${eff.appliedCount} | ${rate}% | ${trend} |`,
718
+ )
719
+ }
720
+
721
+ const disabled = effs.filter((e) => e.status === 'disabled')
722
+ if (disabled.length > 0) {
723
+ lines.push('')
724
+ lines.push('### Disabled rules (retired)')
725
+ for (const d of disabled) {
726
+ lines.push(`- ${d.ruleId} (failure rate ${Math.round(d.postRuleFailureRate * 100)}%)`)
727
+ }
728
+ }
700
729
  }
701
730
 
702
731
  return { content: lines.join('\n') }
@@ -907,6 +936,21 @@ const sisStatsCmd: CommandHandler = (ctx) => {
907
936
  )
908
937
  lines.push(`平均成功率: ${Math.round(stats.avgSuccessRate * 100)}%`)
909
938
  lines.push(`总拦截次数: ${stats.totalInterceptions}`)
939
+
940
+ const active = db.getActive()
941
+ if (active.length > 0) {
942
+ lines.push('')
943
+ lines.push('### 签名明细(按拦截次数排序)')
944
+ lines.push('| 签名 | 状态 | 拦截 | 成功率 |')
945
+ lines.push('|------|------|------|--------|')
946
+ for (const s of active) {
947
+ const icon = s.status === 'active' ? '🟢' : '🟡'
948
+ lines.push(
949
+ `| ${s.id} | ${icon} ${s.status} | ${s.occurrences} | ${Math.round(s.successRate * 100)}% |`,
950
+ )
951
+ }
952
+ }
953
+
910
954
  return { content: lines.join('\n') }
911
955
  }
912
956
 
package/src/ui/input.tsx CHANGED
@@ -231,7 +231,14 @@ export function InputBar({
231
231
  if (submittedHistory.length === 0) return
232
232
  // Save current draft the first time we enter history browsing
233
233
  if (historyIndexRef.current === -1) {
234
- savedDraftRef.current = value
234
+ // Flush any pending throttle so its deferred setValue doesn't overwrite
235
+ // history navigation (paste → immediate ↑ race). Save the latest value
236
+ // from the ref — state `value` is stale inside the throttle window.
237
+ if (onChangeTimerRef.current) {
238
+ clearTimeout(onChangeTimerRef.current)
239
+ onChangeTimerRef.current = null
240
+ }
241
+ savedDraftRef.current = valueRef.current
235
242
  }
236
243
  const newIndex = Math.min(historyIndexRef.current + 1, submittedHistory.length - 1)
237
244
  historyIndexRef.current = newIndex