@miphamai/cli 0.16.0 → 0.16.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.
@@ -277,6 +277,7 @@ export class QueryEngine {
277
277
 
278
278
  // ── PreInference DLP checkpoint ──
279
279
  if (isInferenceHookEnabled(this.inferenceHookConfig)) {
280
+ const hookConfig = this.inferenceHookConfig!
280
281
  const provider = this.registry.getActive().config.id
281
282
  const model = this.registry.getActiveModel()
282
283
  const request = buildRequest(
@@ -284,9 +285,9 @@ export class QueryEngine {
284
285
  'session-1',
285
286
  provider,
286
287
  model,
287
- this.inferenceHookConfig!.organization_id,
288
+ hookConfig.organization_id,
288
289
  )
289
- const verdict = await sendInferenceCheck(this.inferenceHookConfig!, request)
290
+ const verdict = await sendInferenceCheck(hookConfig, request)
290
291
  if (!verdict.allowed) {
291
292
  yield {
292
293
  type: 'error',
@@ -556,6 +557,7 @@ export class QueryEngine {
556
557
 
557
558
  // ── PreInference DLP checkpoint (every tool-calling turn) ──
558
559
  if (isInferenceHookEnabled(this.inferenceHookConfig)) {
560
+ const hookConfig = this.inferenceHookConfig!
559
561
  const provider = this.registry.getActive().config.id
560
562
  const model = this.registry.getActiveModel()
561
563
  const request = buildRequest(
@@ -563,9 +565,9 @@ export class QueryEngine {
563
565
  'session-1',
564
566
  provider,
565
567
  model,
566
- this.inferenceHookConfig!.organization_id,
568
+ hookConfig.organization_id,
567
569
  )
568
- const verdict = await sendInferenceCheck(this.inferenceHookConfig!, request)
570
+ const verdict = await sendInferenceCheck(hookConfig, request)
569
571
  if (!verdict.allowed) {
570
572
  yield {
571
573
  type: 'error',
@@ -826,9 +828,8 @@ export class QueryEngine {
826
828
  const model = this.registry.findModel(modelId)
827
829
  if (model) {
828
830
  const DISABLE_1M = process.env.MIPHAM_DISABLE_1M_CONTEXT === '1'
829
- const maxTokens = (DISABLE_1M && model.contextWindow > 200_000)
830
- ? 200_000
831
- : model.contextWindow
831
+ const maxTokens =
832
+ DISABLE_1M && model.contextWindow > 200_000 ? 200_000 : model.contextWindow
832
833
  this.context.updateMaxTokens(maxTokens)
833
834
  }
834
835
  }
@@ -139,6 +139,11 @@ export class PermissionSystem {
139
139
  * 8. System default → 'ask'
140
140
  */
141
141
  check(tool: ToolDefinition, input: Record<string, unknown>): PermissionLevel {
142
+ // ── Guard: reject undefined/null tool (defense-in-depth) ──
143
+ if (!tool) {
144
+ return 'ask' // absent tool → safest default
145
+ }
146
+
142
147
  // ── Cache lookup (P2): reuse decision for same tool+mode+input ──
143
148
  const cacheKey = tool.name + '|' + JSON.stringify(input, Object.keys(input).sort())
144
149
  if (this.cacheMode === this.mode) {
@@ -134,7 +134,10 @@ export class SessionStore {
134
134
  /**
135
135
  * Auto-save with timestamp-based name.
136
136
  */
137
- static autoSave(messages: Message[], metadata?: { provider?: string; model?: string; cwd?: string }): string {
137
+ static autoSave(
138
+ messages: Message[],
139
+ metadata?: { provider?: string; model?: string; cwd?: string },
140
+ ): string {
138
141
  const name = `session-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`
139
142
  SessionStore.save(name, messages, metadata)
140
143
  return name
package/src/index.tsx CHANGED
@@ -87,7 +87,8 @@ export async function runApp(options: RunOptions): Promise<void> {
87
87
  const pluginManager = new PluginManager()
88
88
 
89
89
  // Generate session name for tracking (used by /cd to persist cwd)
90
- const sessionName = options.resume || `session-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`
90
+ const sessionName =
91
+ options.resume || `session-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`
91
92
 
92
93
  // Initialize context — restore saved session if available
93
94
  // Read the active model's context window for dynamic max-token sizing.
@@ -111,12 +112,12 @@ export async function runApp(options: RunOptions): Promise<void> {
111
112
  }
112
113
 
113
114
  const DISABLE_1M = process.env.MIPHAM_DISABLE_1M_CONTEXT === '1'
114
- const contextMaxTokens = (DISABLE_1M && modelContextWindow > 200_000)
115
- ? 200_000
116
- : modelContextWindow
115
+ const contextMaxTokens = DISABLE_1M && modelContextWindow > 200_000 ? 200_000 : modelContextWindow
117
116
 
118
117
  if (DISABLE_1M && modelContextWindow <= 200_000) {
119
- console.error('[mipham] ⚠ MIPHAM_DISABLE_1M_CONTEXT is set but auto-compaction is not holding the session to 200K — model context window is already ≤ 200K')
118
+ console.error(
119
+ '[mipham] ⚠ MIPHAM_DISABLE_1M_CONTEXT is set but auto-compaction is not holding the session to 200K — model context window is already ≤ 200K',
120
+ )
120
121
  }
121
122
 
122
123
  const context = new ContextManager({ maxTokens: contextMaxTokens, compactionThreshold: 0.9 })
@@ -0,0 +1,72 @@
1
+ [
2
+ {
3
+ "name": "code-review",
4
+ "description": "Automated code review with multiple dimensions — complexity, security, performance, quality",
5
+ "url": "https://github.com/One-Mipham/skill-code-review",
6
+ "file": "code-review.SKILL.md",
7
+ "category": "Development",
8
+ "author": "MiphamAI",
9
+ "builtin": true
10
+ },
11
+ {
12
+ "name": "systematic-debugging",
13
+ "description": "Systematic debugging workflow — find root cause before fixing",
14
+ "url": "https://github.com/anthropics/skills",
15
+ "file": "systematic-debugging.SKILL.md",
16
+ "category": "Development",
17
+ "author": "Anthropic"
18
+ },
19
+ {
20
+ "name": "test-driven-development",
21
+ "description": "TDD workflow — write failing test first, then implement",
22
+ "url": "https://github.com/anthropics/skills",
23
+ "file": "test-driven-development.SKILL.md",
24
+ "category": "Development",
25
+ "author": "Anthropic",
26
+ "builtin": true
27
+ },
28
+ {
29
+ "name": "web-access",
30
+ "description": "Web search, scraping, and browser automation — search engines, page fetching, authenticated sites",
31
+ "url": "https://github.com/One-Mipham/skill-web-access",
32
+ "file": "web-access.SKILL.md",
33
+ "category": "Network",
34
+ "author": "MiphamAI",
35
+ "builtin": true
36
+ },
37
+ {
38
+ "name": "doc-generator",
39
+ "description": "Generate API docs, READMEs, and changelogs from code analysis",
40
+ "url": "https://github.com/One-Mipham/skill-doc-generator",
41
+ "file": "doc-generator.SKILL.md",
42
+ "category": "Documentation",
43
+ "author": "MiphamAI",
44
+ "builtin": true
45
+ },
46
+ {
47
+ "name": "github-ops",
48
+ "description": "GitHub PR/issue management and automation — labels, milestones, reviews",
49
+ "url": "https://github.com/One-Mipham/skill-github-ops",
50
+ "file": "github-ops.SKILL.md",
51
+ "category": "DevOps",
52
+ "author": "MiphamAI",
53
+ "builtin": true
54
+ },
55
+ {
56
+ "name": "security-review",
57
+ "description": "Security audit and vulnerability scanning for code changes — OWASP, secrets, supply chain",
58
+ "url": "https://github.com/One-Mipham/skill-security-review",
59
+ "file": "security-review.SKILL.md",
60
+ "category": "Security",
61
+ "author": "MiphamAI",
62
+ "builtin": true
63
+ },
64
+ {
65
+ "name": "frontend-design",
66
+ "description": "Distinctive visual design guidance for UI components — typography, color, layout",
67
+ "url": "https://github.com/anthropics/skills",
68
+ "file": "frontend-design.SKILL.md",
69
+ "category": "Design",
70
+ "author": "Anthropic"
71
+ }
72
+ ]
@@ -11,6 +11,7 @@ import { homedir } from 'node:os'
11
11
  import { spawnSync } from 'node:child_process'
12
12
  import { URL } from 'node:url'
13
13
  import type { MiphamConfig } from '../shared/types.js'
14
+ import communitySkills from './community-registry.json'
14
15
 
15
16
  const SKILLS_DIR = join(homedir(), '.mipham', 'skills')
16
17
 
@@ -39,81 +40,7 @@ export interface SkillEntry {
39
40
  * Embedded community skill registry.
40
41
  * In the future, this can be fetched from a remote URL.
41
42
  */
42
- const COMMUNITY_SKILLS: SkillEntry[] = [
43
- {
44
- name: 'code-review',
45
- description:
46
- 'Automated code review with multiple dimensions — complexity, security, performance, quality',
47
- url: 'https://github.com/One-Mipham/skill-code-review',
48
- file: 'code-review.SKILL.md',
49
- category: 'Development',
50
- author: 'MiphamAI',
51
- builtin: true,
52
- },
53
- {
54
- name: 'systematic-debugging',
55
- description: 'Systematic debugging workflow — find root cause before fixing',
56
- url: 'https://github.com/anthropics/skills',
57
- file: 'systematic-debugging.SKILL.md',
58
- category: 'Development',
59
- author: 'Anthropic',
60
- },
61
- {
62
- name: 'test-driven-development',
63
- description: 'TDD workflow — write failing test first, then implement',
64
- url: 'https://github.com/anthropics/skills',
65
- file: 'test-driven-development.SKILL.md',
66
- category: 'Development',
67
- author: 'Anthropic',
68
- builtin: true,
69
- },
70
- {
71
- name: 'web-access',
72
- description:
73
- 'Web search, scraping, and browser automation — search engines, page fetching, authenticated sites',
74
- url: 'https://github.com/One-Mipham/skill-web-access',
75
- file: 'web-access.SKILL.md',
76
- category: 'Network',
77
- author: 'MiphamAI',
78
- builtin: true,
79
- },
80
- {
81
- name: 'doc-generator',
82
- description: 'Generate API docs, READMEs, and changelogs from code analysis',
83
- url: 'https://github.com/One-Mipham/skill-doc-generator',
84
- file: 'doc-generator.SKILL.md',
85
- category: 'Documentation',
86
- author: 'MiphamAI',
87
- builtin: true,
88
- },
89
- {
90
- name: 'github-ops',
91
- description: 'GitHub PR/issue management and automation — labels, milestones, reviews',
92
- url: 'https://github.com/One-Mipham/skill-github-ops',
93
- file: 'github-ops.SKILL.md',
94
- category: 'DevOps',
95
- author: 'MiphamAI',
96
- builtin: true,
97
- },
98
- {
99
- name: 'security-review',
100
- description:
101
- 'Security audit and vulnerability scanning for code changes — OWASP, secrets, supply chain',
102
- url: 'https://github.com/One-Mipham/skill-security-review',
103
- file: 'security-review.SKILL.md',
104
- category: 'Security',
105
- author: 'MiphamAI',
106
- builtin: true,
107
- },
108
- {
109
- name: 'frontend-design',
110
- description: 'Distinctive visual design guidance for UI components — typography, color, layout',
111
- url: 'https://github.com/anthropics/skills',
112
- file: 'frontend-design.SKILL.md',
113
- category: 'Design',
114
- author: 'Anthropic',
115
- },
116
- ]
43
+ const COMMUNITY_SKILLS: SkillEntry[] = communitySkills as SkillEntry[]
117
44
 
118
45
  // ═══════════════════════════════════════════════════════════════
119
46
  // Skill Installer
@@ -28,9 +28,9 @@ const BLOCKED_PATTERNS = [
28
28
  /\$\(/,
29
29
  // Backtick command substitution
30
30
  /`[^`]+`/,
31
- // Interpreter code execution (bypass vector)
32
- /\bpython3?\s+-c\b/,
33
- /\bpython3?\s+-m\b/,
31
+ // Interpreter code execution (bypass vector) — covers python, python2, python3
32
+ /\bpython[23]?\s+-c\b/,
33
+ /\bpython[23]?\s+-m\b/,
34
34
  /\bperl\s+-[ep]\b/,
35
35
  /\bruby\s+-e\b/,
36
36
  /\bnode\s+-e\b/,
@@ -45,9 +45,31 @@ export const enterWorktreeTool: ToolDefinition = {
45
45
  'Worktree name may only contain letters, digits, dots, underscores, dashes, and slashes.',
46
46
  }
47
47
  }
48
+ // Prevent path traversal via .. segments
49
+ if (name.includes('..')) {
50
+ return {
51
+ success: false,
52
+ content: '',
53
+ error: 'Worktree name may not contain ".." path segments.',
54
+ }
55
+ }
48
56
 
49
57
  const cwd = ctx.cwd
50
- const worktreePath = `${cwd}/.claude/worktrees/${name}`
58
+ const { resolve } = await import('node:path')
59
+ const worktreePath = resolve(`${cwd}/.claude/worktrees/${name}`)
60
+ const allowedPrefix = resolve(`${cwd}/.claude/worktrees/`)
61
+
62
+ // Defense-in-depth: verify resolved path is within .claude/worktrees/
63
+ if (
64
+ !worktreePath.startsWith(allowedPrefix + '/') &&
65
+ worktreePath !== allowedPrefix.slice(0, -1)
66
+ ) {
67
+ return {
68
+ success: false,
69
+ content: '',
70
+ error: 'Worktree path must be within .claude/worktrees/.',
71
+ }
72
+ }
51
73
 
52
74
  try {
53
75
  // Validate we're in a git repo
@@ -36,6 +36,19 @@ export const exitWorktreeTool: ToolDefinition = {
36
36
 
37
37
  // Validate the path is under .claude/worktrees
38
38
  const cwd = ctx.cwd
39
+ const { resolve } = await import('node:path')
40
+ const resolvedPath = resolve(worktreePath)
41
+ const allowedPrefix = resolve(`${cwd}/.claude/worktrees/`)
42
+
43
+ if (!resolvedPath.startsWith(allowedPrefix)) {
44
+ return {
45
+ success: false,
46
+ content: '',
47
+ error:
48
+ `Path "${worktreePath}" is not under .claude/worktrees/. ` +
49
+ `Only worktrees created by EnterWorktree can be managed here.`,
50
+ }
51
+ }
39
52
 
40
53
  try {
41
54
  // Verify the worktree exists in git's worktree list
@@ -0,0 +1,89 @@
1
+ import React from 'react'
2
+ import { Box, Text } from 'ink'
3
+
4
+ export interface AgentEntry {
5
+ id: string
6
+ name: string
7
+ description: string
8
+ startTime: number
9
+ tokensUsed: number
10
+ status: 'running' | 'completed'
11
+ }
12
+
13
+ interface AgentFooterProps {
14
+ agents: AgentEntry[]
15
+ gitBranch: string
16
+ /** Tick counter for live elapsed-time re-renders. */
17
+ tick: number
18
+ }
19
+
20
+ /** Format elapsed seconds into human-readable string (e.g. "2m 51s", "38s"). */
21
+ function formatElapsed(seconds: number): string {
22
+ if (seconds < 60) return `${seconds}s`
23
+ const m = Math.floor(seconds / 60)
24
+ const s = seconds % 60
25
+ return `${m}m ${s}s`
26
+ }
27
+
28
+ /** Format token count: 42400 → "42.4k", 1500000 → "1.5M". */
29
+ function formatTokens(n: number): string {
30
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
31
+ if (n >= 1000) return `${(n / 1000).toFixed(1)}k`
32
+ return String(n)
33
+ }
34
+
35
+ export function AgentFooter({ agents, gitBranch, tick: _tick }: AgentFooterProps) {
36
+ const hasAgents = agents.length > 0
37
+ const hasBranch = gitBranch.length > 0
38
+
39
+ if (!hasAgents && !hasBranch) return null
40
+
41
+ return (
42
+ <Box flexDirection="column" marginTop={1}>
43
+ {/* Git branch indicator */}
44
+ {hasBranch && (
45
+ <Box>
46
+ <Text dimColor>
47
+ {' '}⏺ {gitBranch}
48
+ </Text>
49
+ </Box>
50
+ )}
51
+
52
+ {/* Agent status lines */}
53
+ {agents.map((agent) => {
54
+ const elapsed = Math.floor((Date.now() - agent.startTime) / 1000)
55
+ const isRunning = agent.status === 'running'
56
+ const symbol = isRunning ? '◯' : '◼'
57
+ const symbolColor = isRunning ? 'cyan' : undefined
58
+
59
+ // Truncate description to keep lines readable
60
+ const desc =
61
+ agent.description.length > 80 ? agent.description.slice(0, 80) + '...' : agent.description
62
+
63
+ return (
64
+ <Box key={agent.id}>
65
+ <Text dimColor={!isRunning} color={symbolColor}>
66
+ {' '}
67
+ {symbol}{' '}
68
+ </Text>
69
+ <Text bold color={isRunning ? 'cyan' : undefined} dimColor={!isRunning}>
70
+ {agent.name}
71
+ </Text>
72
+ <Text dimColor> {desc}</Text>
73
+ {/* Spacer pushes stats to the right — Ink handles this naturally */}
74
+ <Text dimColor>
75
+ {' '}
76
+ {isRunning ? formatElapsed(elapsed) : 'finished'}
77
+ </Text>
78
+ {isRunning && agent.tokensUsed > 0 && (
79
+ <Text dimColor>
80
+ {' · ↓ '}
81
+ {formatTokens(agent.tokensUsed)} tokens
82
+ </Text>
83
+ )}
84
+ </Box>
85
+ )
86
+ })}
87
+ </Box>
88
+ )
89
+ }