@miphamai/cli 0.38.0 → 0.39.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.
Files changed (62) hide show
  1. package/bin/mipham.ts +25 -0
  2. package/package.json +1 -1
  3. package/src/agent/sub-agent.ts +4 -3
  4. package/src/config/defaults.ts +19 -1
  5. package/src/config/keys-manager.ts +11 -1
  6. package/src/config/loader.ts +20 -10
  7. package/src/core/context.ts +86 -56
  8. package/src/core/credential-masker/output-scrub.ts +5 -0
  9. package/src/core/crsi-sandbox.ts +19 -7
  10. package/src/core/engine.ts +50 -33
  11. package/src/core/instructions.ts +4 -42
  12. package/src/core/permission.ts +7 -1
  13. package/src/core/red-team.ts +4 -4
  14. package/src/core/self-critique.ts +3 -1
  15. package/src/core/session-log.ts +225 -0
  16. package/src/core/session-store.ts +119 -34
  17. package/src/daemon/auth.ts +9 -6
  18. package/src/daemon/cors.ts +24 -16
  19. package/src/daemon/rate-limiter.ts +4 -0
  20. package/src/daemon/server.ts +24 -3
  21. package/src/index.tsx +54 -28
  22. package/src/mcp/http-transport.ts +2 -0
  23. package/src/mcp/oauth.ts +1 -1
  24. package/src/providers/llm-replay.ts +36 -0
  25. package/src/providers/llm.ts +16 -0
  26. package/src/providers/registry.ts +2 -1
  27. package/src/security/gate.ts +10 -0
  28. package/src/security/url.ts +37 -14
  29. package/src/shared/constants.ts +40 -54
  30. package/src/shared/mipham-models.json +50 -0
  31. package/src/shared/types.ts +53 -49
  32. package/src/skills/fork-executor.ts +3 -1
  33. package/src/skills/loader.ts +2 -1
  34. package/src/skills/seam.ts +18 -0
  35. package/src/tools/agent/agent.ts +1 -0
  36. package/src/tools/agent/skill.ts +1 -0
  37. package/src/tools/exec/bash.ts +140 -128
  38. package/src/tools/exec/git.ts +17 -0
  39. package/src/tools/file/read.ts +73 -66
  40. package/src/tools/file/write.ts +1 -1
  41. package/src/tools/index.ts +54 -132
  42. package/src/tools/network/web-fetch.ts +41 -26
  43. package/src/tools/seam.ts +25 -0
  44. package/src/tools/validation.ts +91 -0
  45. package/src/ui/commands.ts +17 -5
  46. package/src/ui/input.tsx +8 -1
  47. package/src/vajra/compose/assemble.ts +19 -0
  48. package/src/vajra/compose/bundle.ts +39 -0
  49. package/src/vajra/compose/dump.ts +5 -0
  50. package/src/vajra/compose/index.ts +4 -0
  51. package/src/vajra/compose/mount.ts +30 -0
  52. package/src/vajra/context.ts +183 -0
  53. package/src/vajra/events.ts +13 -0
  54. package/src/vajra/index.ts +4 -0
  55. package/src/vajra/leaf/plan-runner.ts +94 -0
  56. package/src/vajra/service.ts +15 -0
  57. package/src/workflow/primitives/agent.ts +3 -1
  58. package/src/workflow/runtime.ts +23 -15
  59. package/src/workflow/sandbox.ts +51 -9
  60. package/src/core/compaction-progress.ts +0 -205
  61. package/src/core/context-compact.ts +0 -124
  62. package/src/core/context-drain.ts +0 -113
@@ -1,10 +1,4 @@
1
1
  import dns from 'node:dns'
2
- const { lookupSync } = dns as unknown as {
3
- lookupSync: (
4
- hostname: string,
5
- options?: { all?: boolean },
6
- ) => Array<{ address: string; family: number }>
7
- }
8
2
 
9
3
  // ── Protocol allow-list ──
10
4
  const ALLOWED_PROTOCOLS = ['http:', 'https:']
@@ -20,7 +14,7 @@ const IPV4_BLOCKED = [
20
14
  ]
21
15
 
22
16
  /** Validate a URL for SSRF safety. Returns null if safe, or an error string. */
23
- export function validateUrl(urlStr: string): string | null {
17
+ export async function validateUrl(urlStr: string): Promise<string | null> {
24
18
  let parsed: URL
25
19
  try {
26
20
  parsed = new URL(urlStr)
@@ -41,18 +35,17 @@ export function validateUrl(urlStr: string): string | null {
41
35
  return `URL hostname "${hostname}" resolves to a blocked IP range (private / loopback / link-local).`
42
36
  }
43
37
 
44
- // 3. Check for raw IPv6 loopback / private
45
- // Strip brackets from IPv6 hostnames (Bun's URL parser keeps them)
38
+ // 3. Check for raw IPv6 loopback / private / IPv4-mapped
46
39
  const ipv6Host = hostname.startsWith('[') ? hostname.slice(1, -1) : hostname
47
40
  if (isBlockedIPv6(ipv6Host)) {
48
41
  return `URL hostname "${hostname}" resolves to a blocked IPv6 address.`
49
42
  }
50
43
 
51
- // 4. DNS rebinding protection: resolve hostname and check resolved IPs
52
- // Use the bracket-less version for DNS lookups
53
- const dnsHostname = hostname.startsWith('[') ? hostname.slice(1, -1) : hostname
44
+ // 4. DNS rebinding protection: resolve hostname and check resolved IPs.
45
+ // Uses async dns.promises.lookup — the old sync `dns.lookupSync` never existed,
46
+ // so this check was previously dead code (always threw → swallowed → "safe").
54
47
  try {
55
- const addresses = lookupSync(dnsHostname, { all: true })
48
+ const addresses = await dns.promises.lookup(ipv6Host, { all: true })
56
49
  for (const addr of addresses) {
57
50
  if (isBlockedIPv4(addr.address)) {
58
51
  return `URL hostname "${hostname}" resolved to ${addr.address} (blocked IP range).`
@@ -62,7 +55,7 @@ export function validateUrl(urlStr: string): string | null {
62
55
  }
63
56
  }
64
57
  } catch {
65
- // DNS resolution failed — that's OK for validation; the fetch will also fail
58
+ // DNS resolution failed — the subsequent fetch will fail too; treat as safe.
66
59
  }
67
60
 
68
61
  return null // safe
@@ -92,5 +85,35 @@ function isBlockedIPv6(ip: string): boolean {
92
85
  if (ip.startsWith('fc') || ip.startsWith('fd')) return true
93
86
  // IPv6 unspecified
94
87
  if (ip === '::' || ip === '0:0:0:0:0:0:0:0') return true
88
+
89
+ // IPv4-mapped (::ffff:a.b.c.d / ::ffff:xxxx:xxxx) and IPv4-compatible (::a.b.c.d / ::xxxx:xxxx)
90
+ // — these can smuggle a private IPv4 past the literal-IPv6 checks above.
91
+ const embedded = extractEmbeddedIPv4(ip)
92
+ if (embedded && isBlockedIPv4(embedded)) return true
93
+
95
94
  return false
96
95
  }
96
+
97
+ /** Extract the IPv4 address embedded in an IPv4-mapped / IPv4-compatible IPv6 address. */
98
+ function extractEmbeddedIPv4(ip: string): string | null {
99
+ let rest: string | null = null
100
+ const mapped = ip.match(/^::ffff:(.+)$/i)
101
+ const compat = ip.match(/^::([0-9a-fA-F]{1,4}):([0-9a-fA-F]{1,4})$/)
102
+ if (mapped) {
103
+ rest = mapped[1]!
104
+ } else if (compat) {
105
+ rest = `${compat[1]}:${compat[2]}`
106
+ } else {
107
+ return null
108
+ }
109
+
110
+ // Dotted-quad form (::ffff:127.0.0.1)
111
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(rest)) return rest
112
+
113
+ // Hex form (::ffff:7f00:1)
114
+ const hex = rest.match(/^([0-9a-fA-F]{1,4}):([0-9a-fA-F]{1,4})$/)
115
+ if (!hex) return null
116
+ const hi = parseInt(hex[1]!, 16)
117
+ const lo = parseInt(hex[2]!, 16)
118
+ return `${hi >> 8}.${hi & 0xff}.${lo >> 8}.${lo & 0xff}`
119
+ }
@@ -1,4 +1,28 @@
1
- import type { ProviderConfig } from './types'
1
+ import type { ProviderConfig, ModelInfo } from './types'
2
+ import miphamModelsSnapshot from './mipham-models.json' with { type: 'json' }
3
+
4
+ // Build MiphamAI provider from synced model definitions.
5
+ // Source of truth: Engine GET /v1/mipham-models → packages/shared/src/mipham-models.json
6
+ // Run `bun run scripts/sync-mipham-models.ts` to update from Engine.
7
+ const MIPHAM_MODELS: ModelInfo[] = miphamModelsSnapshot.models.map((m) => ({
8
+ id: m.id,
9
+ name: m.name,
10
+ providerId: m.providerId,
11
+ contextWindow: m.contextWindow,
12
+ maxOutput: m.maxOutput,
13
+ vision: m.vision,
14
+ status: m.status as 'active' | 'upcoming' | 'deprecated',
15
+ }))
16
+
17
+ const MIPHAM_PROVIDER: ProviderConfig = {
18
+ id: miphamModelsSnapshot.provider.id,
19
+ name: miphamModelsSnapshot.provider.name,
20
+ protocol: miphamModelsSnapshot.provider.protocol as 'openai-compatible',
21
+ baseUrl: miphamModelsSnapshot.provider.baseUrl,
22
+ apiKey: miphamModelsSnapshot.provider.apiKey,
23
+ models: MIPHAM_MODELS,
24
+ status: miphamModelsSnapshot.provider.status as 'active' | 'upcoming',
25
+ }
2
26
 
3
27
  // Providers: alphabetical by id. Models: Lite/Flash → Text → Vision → Code → Pro/Think
4
28
  export const DEFAULT_PROVIDERS: ProviderConfig[] = [
@@ -325,56 +349,11 @@ export const DEFAULT_PROVIDERS: ProviderConfig[] = [
325
349
  },
326
350
  ],
327
351
  },
328
- {
329
- id: 'mipham',
330
- name: 'MiphamAI',
331
- protocol: 'openai-compatible',
332
- baseUrl: 'http://127.0.0.1:8080/v1', // Engine 本地端点(om-V5 生产部署待定)
333
- apiKey: '${MIPHAM_API_KEY}',
334
- models: [
335
- {
336
- id: 'om-v5-flash',
337
- name: 'OM V5 Flash',
338
- providerId: 'mipham',
339
- contextWindow: 1_000_000,
340
- maxOutput: 128_000,
341
- vision: false,
342
- status: 'active',
343
- },
344
- {
345
- id: 'om-v5-pro',
346
- name: 'OM V5 Pro',
347
- providerId: 'mipham',
348
- contextWindow: 1_000_000,
349
- maxOutput: 128_000,
350
- vision: false,
351
- status: 'active',
352
- },
353
- {
354
- id: 'om-v5-visual',
355
- name: 'OM V5 Visual',
356
- providerId: 'mipham',
357
- contextWindow: 200_000,
358
- maxOutput: 32_000,
359
- vision: true,
360
- status: 'active',
361
- },
362
- {
363
- id: 'om-v5-apex',
364
- name: 'OM V5 Apex',
365
- providerId: 'mipham',
366
- contextWindow: 1_000_000,
367
- maxOutput: 128_000,
368
- vision: false,
369
- status: 'active',
370
- },
371
- ],
372
- status: 'active',
373
- },
352
+ MIPHAM_PROVIDER,
374
353
  {
375
354
  id: 'ollama',
376
355
  name: 'Ollama (本地)',
377
- protocol: 'openai-compatible' as const,
356
+ protocol: 'openai-compatible',
378
357
  baseUrl: 'http://localhost:11434/v1',
379
358
  apiKey: 'ollama-local',
380
359
  models: [], // 动态发现,初值为空
@@ -459,7 +438,19 @@ export const PROTOCOL_LABELS: Record<string, string> = {
459
438
  custom: 'Custom Protocol',
460
439
  }
461
440
 
462
- export const TOOL_CATEGORIES = ['file', 'exec', 'agent', 'network', 'system', 'artifact'] as const
441
+ export const TOOL_CATEGORIES = [
442
+ 'file',
443
+ 'exec',
444
+ 'agent',
445
+ 'network',
446
+ 'system',
447
+ 'artifact',
448
+ 'scheduling',
449
+ ] as const
450
+ export const CONFIG_FILE_NAME = 'config.yml'
451
+ export const MIPHAM_DIR = '.mipham'
452
+ export const USER_CONFIG_DIR = '.mipham'
453
+ export const MEMORY_DIR = 'memory'
463
454
 
464
455
  /** 预置 Ollama 模型 — 与 ollama list 结果合并去重后展示 */
465
456
  export const OLLAMA_PRESET_MODELS = [
@@ -473,11 +464,6 @@ export const OLLAMA_PRESET_MODELS = [
473
464
  { id: 'qwen3', source: '热门' },
474
465
  ] as const
475
466
 
476
- export const CONFIG_FILE_NAME = 'config.yml'
477
- export const MIPHAM_DIR = '.mipham'
478
- export const USER_CONFIG_DIR = '.mipham'
479
- export const MEMORY_DIR = 'memory'
480
-
481
467
  // ── Artifact Constants ──
482
468
  export const ARTIFACTS_DIR = 'artifacts'
483
469
  export const ARTIFACT_PORT = 9876
@@ -0,0 +1,50 @@
1
+ {
2
+ "_comment": "Mipham model definitions — synced from Engine GET /v1/mipham-models. Run 'bun run scripts/sync-mipham-models.ts' to update.",
3
+ "provider": {
4
+ "id": "mipham",
5
+ "name": "MiphamAI",
6
+ "protocol": "openai-compatible",
7
+ "baseUrl": "https://api.onemipham.com/v1",
8
+ "apiKey": "${MIPHAM_API_KEY}",
9
+ "status": "active"
10
+ },
11
+ "models": [
12
+ {
13
+ "id": "om-v5-flash",
14
+ "name": "OM V5 Flash",
15
+ "providerId": "mipham",
16
+ "contextWindow": 16384,
17
+ "maxOutput": 4096,
18
+ "vision": false,
19
+ "status": "active"
20
+ },
21
+ {
22
+ "id": "om-v5-pro",
23
+ "name": "OM V5 Pro",
24
+ "providerId": "mipham",
25
+ "contextWindow": 131072,
26
+ "maxOutput": 16384,
27
+ "vision": false,
28
+ "status": "active"
29
+ },
30
+ {
31
+ "id": "om-v5-visual",
32
+ "name": "OM V5 Visual",
33
+ "providerId": "mipham",
34
+ "contextWindow": 65536,
35
+ "maxOutput": 4096,
36
+ "vision": true,
37
+ "status": "active"
38
+ },
39
+ {
40
+ "id": "om-v5-apex",
41
+ "name": "OM V5 Apex",
42
+ "providerId": "mipham",
43
+ "contextWindow": 200000,
44
+ "maxOutput": 32768,
45
+ "vision": false,
46
+ "status": "active"
47
+ }
48
+ ],
49
+ "synced_at": "2026-08-04T00:00:00Z"
50
+ }
@@ -1,3 +1,6 @@
1
+ // ── 类型唯一真源(vendored from packages/shared/src/types.ts)──
2
+ // 为让 npm 包自包含(运行时无需 @mipham/shared workspace 依赖),
3
+ // 此处从 packages/shared/src/types.ts 同步。改动共享类型时两边同步。
1
4
  // ── Provider Types ──
2
5
  export type ProtocolType = 'openai-compatible' | 'anthropic' | 'custom'
3
6
 
@@ -60,23 +63,6 @@ export type ToolPermission = 'auto' | 'ask' | 'bypass'
60
63
  export type ToolCategory =
61
64
  'file' | 'exec' | 'agent' | 'network' | 'system' | 'artifact' | 'scheduling'
62
65
 
63
- export interface ToolContext {
64
- cwd: string
65
- sessionId: string
66
- provider: string
67
- model: string
68
- skillsLoader?: import('../skills/loader').SkillsLoader
69
- registry?: import('../providers/registry').ProviderRegistry
70
- toolRegistry?: Map<string, ToolDefinition>
71
- artifactServer?: import('../artifacts/server').ArtifactServer
72
- agentRegistry?: import('../agent/agent-registry').AgentRegistry
73
- backgroundAgentRegistry?: import('../agent/background-registry').BackgroundAgentRegistry
74
- permissionSystem?: import('../core/permission').PermissionSystem
75
- ruleEngine?: import('../core/rule-engine').ExperienceRuleEngine
76
- /** Files read this session — used by Write tool to check read-before-write */
77
- readFiles?: Set<string>
78
- }
79
-
80
66
  // ── Artifact Types ──
81
67
  export interface ArtifactEntry {
82
68
  name: string
@@ -102,15 +88,6 @@ export interface ToolResult {
102
88
  error?: string
103
89
  }
104
90
 
105
- export interface ToolDefinition {
106
- name: string
107
- description: string
108
- category: ToolCategory
109
- permission: ToolPermission
110
- parameters: Record<string, unknown>
111
- execute: (params: Record<string, unknown>, ctx: ToolContext) => Promise<ToolResult>
112
- }
113
-
114
91
  // ── Task Notification Types ──
115
92
  export interface TaskNotification {
116
93
  taskId: string
@@ -207,29 +184,6 @@ export interface McpServerConfig {
207
184
  }
208
185
  }
209
186
 
210
- // ── Skill Types ──
211
- export interface SkillDefinition {
212
- name: string
213
- description: string
214
- version: string
215
- type: 'standard' | 'mipham'
216
- tools?: ToolDefinition[]
217
- hooks?: HookDefinition[]
218
- prompts?: Record<string, string>
219
- /** Frontmatter: 'fork' means execute in isolated subagent, undefined means inline */
220
- context?: string
221
- /** Model override for fork execution */
222
- model?: string
223
- /** Tool whitelist for fork mode */
224
- allowedTools?: string[]
225
- /** When true, the skill is NOT shown in system-reminder for AI auto-triggering */
226
- disableModelInvocation?: boolean
227
- /** When true, users can invoke this skill directly via /<name> */
228
- userInvocable?: boolean
229
- /** The markdown body content of the skill file (instructions for the AI to follow). */
230
- body?: string
231
- }
232
-
233
187
  // ── Hook Types ──
234
188
  export type HookEvent =
235
189
  | 'PreToolUse'
@@ -482,3 +436,53 @@ export interface BackgroundAgentConfig {
482
436
  auto_worktree: boolean
483
437
  commit_coauthors: boolean
484
438
  }
439
+
440
+ // ── CLI 内部类型(引用活服务,不可共享)──
441
+ export interface ToolContext {
442
+ cwd: string
443
+ sessionId: string
444
+ provider: string
445
+ model: string
446
+ skillsLoader?: import('../skills/seam').Skills
447
+ registry?: import('../providers/registry').ProviderRegistry
448
+ toolRegistry?: Map<string, ToolDefinition>
449
+ artifactServer?: import('../artifacts/server').ArtifactServer
450
+ agentRegistry?: import('../agent/agent-registry').AgentRegistry
451
+ backgroundAgentRegistry?: import('../agent/background-registry').BackgroundAgentRegistry
452
+ permissionSystem?: import('../core/permission').PermissionSystem
453
+ ruleEngine?: import('../core/rule-engine').ExperienceRuleEngine
454
+ llm?: import('../providers/llm').Llm
455
+ /** Files read this session — used by Write tool to check read-before-write */
456
+ readFiles?: Set<string>
457
+ }
458
+
459
+ export interface ToolDefinition {
460
+ name: string
461
+ description: string
462
+ category: ToolCategory
463
+ permission: ToolPermission
464
+ parameters: Record<string, unknown>
465
+ execute: (params: Record<string, unknown>, ctx: ToolContext) => Promise<ToolResult>
466
+ }
467
+
468
+ export interface SkillDefinition {
469
+ name: string
470
+ description: string
471
+ version: string
472
+ type: 'standard' | 'mipham'
473
+ tools?: ToolDefinition[]
474
+ hooks?: HookDefinition[]
475
+ prompts?: Record<string, string>
476
+ /** Frontmatter: 'fork' means execute in isolated subagent, undefined means inline */
477
+ context?: string
478
+ /** Model override for fork execution */
479
+ model?: string
480
+ /** Tool whitelist for fork mode */
481
+ allowedTools?: string[]
482
+ /** When true, the skill is NOT shown in system-reminder for AI auto-triggering */
483
+ disableModelInvocation?: boolean
484
+ /** When true, users can invoke this skill directly via /<name> */
485
+ userInvocable?: boolean
486
+ /** The markdown body content of the skill file (instructions for the AI to follow). */
487
+ body?: string
488
+ }
@@ -1,5 +1,6 @@
1
1
  import { SubAgent } from '../agent/sub-agent'
2
2
  import type { ProviderRegistry } from '../providers/registry'
3
+ import type { Llm } from '../providers/llm'
3
4
  import type { ToolDefinition, SkillDefinition } from '../shared/index.ts'
4
5
  import type { AgentDefinition } from '../agent/types'
5
6
  import type { PermissionSystem } from '../core/permission'
@@ -20,6 +21,7 @@ export async function executeForkedSkill(
20
21
  registry: ProviderRegistry,
21
22
  toolRegistry: Map<string, ToolDefinition>,
22
23
  permissionSystem?: PermissionSystem,
24
+ llm?: Llm,
23
25
  ): Promise<string> {
24
26
  const agentDef: AgentDefinition = {
25
27
  name: `skill:${skill.name}`,
@@ -32,7 +34,7 @@ export async function executeForkedSkill(
32
34
  source: 'builtin',
33
35
  }
34
36
 
35
- const sub = new SubAgent(registry, toolRegistry, permissionSystem)
37
+ const sub = new SubAgent(registry, toolRegistry, permissionSystem, undefined, undefined, llm)
36
38
  const prompt = args
37
39
  ? `Execute the "${skill.name}" skill with arguments: ${args}`
38
40
  : `Execute the "${skill.name}" skill.`
@@ -3,6 +3,7 @@ import { join } from 'node:path'
3
3
  import { homedir } from 'node:os'
4
4
  import { parse as parseYaml } from 'yaml'
5
5
  import type { SkillDefinition } from '../shared/index.ts'
6
+ import type { Skills } from './seam'
6
7
  import { sanitizeSkillDescription, sanitizeSkillBody, checkSkillShadow } from './sanitizer.js'
7
8
 
8
9
  interface FrontmatterResult {
@@ -21,7 +22,7 @@ function parseFrontmatter(raw: string): FrontmatterResult {
21
22
  }
22
23
  }
23
24
 
24
- export class SkillsLoader {
25
+ export class SkillsLoader implements Skills {
25
26
  private skills = new Map<string, SkillDefinition>()
26
27
 
27
28
  loadBuiltin(basePath: string): void {
@@ -0,0 +1,18 @@
1
+ import type { Context, Disposer } from '../vajra'
2
+ import type { SkillDefinition } from '../shared'
3
+
4
+ /** 技能加载缝的 capability —— 读能力。换 provider = 换技能来源,engine 零 fork 跟随。 */
5
+ export interface Skills {
6
+ get(name: string): SkillDefinition | undefined
7
+ list(): SkillDefinition[]
8
+ has(name: string): boolean
9
+ buildSystemReminder(maxTokens?: number): string
10
+ }
11
+
12
+ /** 缝键:ctx.skills。 */
13
+ export const SKILLS_KEY = 'skills'
14
+
15
+ /** 把一个 Skills(如 SkillsLoader 或测试替身)挂载为 ctx.skills。 */
16
+ export function mountSkills(ctx: Context, skills: Skills): Disposer {
17
+ return ctx.provide(SKILLS_KEY, skills)
18
+ }
@@ -82,6 +82,7 @@ export const agentTool: ToolDefinition = {
82
82
  ctx.permissionSystem,
83
83
  undefined,
84
84
  ctx.ruleEngine,
85
+ ctx.llm,
85
86
  )
86
87
  const result = await sub.execute(prompt, description, {
87
88
  type: agentType,
@@ -54,6 +54,7 @@ export const skillTool: ToolDefinition = {
54
54
  registry,
55
55
  ctx.toolRegistry || new Map(),
56
56
  ctx.permissionSystem,
57
+ ctx.llm,
57
58
  )
58
59
  // Return to AI as internal context
59
60
  return { success: true, content: `[Forked skill "${skillName}" result]:\n${result}` }