@miphamai/cli 0.2.3

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 (67) hide show
  1. package/README.md +76 -0
  2. package/assets/icon.icns +0 -0
  3. package/assets/icon.jpg +0 -0
  4. package/bin/mipham +51 -0
  5. package/bin/mipham.ts +8 -0
  6. package/package.json +59 -0
  7. package/skills/mipham/om-model-optimize.mipham-skill.md +20 -0
  8. package/skills/mipham/om-security.mipham-skill.md +21 -0
  9. package/skills/standard/code-review.SKILL.md +116 -0
  10. package/skills/standard/compassionate-communication.SKILL.md +80 -0
  11. package/skills/standard/doc-generator.SKILL.md +21 -0
  12. package/skills/standard/github-ops.SKILL.md +22 -0
  13. package/skills/standard/memory.SKILL.md +22 -0
  14. package/skills/standard/mipham-code-setup.SKILL.md +113 -0
  15. package/skills/standard/security-review.SKILL.md +122 -0
  16. package/skills/standard/self-review.SKILL.md +20 -0
  17. package/skills/standard/superpower.SKILL.md +19 -0
  18. package/skills/standard/tdd.SKILL.md +20 -0
  19. package/skills/standard/web-search.SKILL.md +23 -0
  20. package/src/agent/sub-agent.ts +122 -0
  21. package/src/config/defaults.ts +10 -0
  22. package/src/config/loader.ts +30 -0
  23. package/src/core/context.ts +135 -0
  24. package/src/core/engine.ts +193 -0
  25. package/src/core/hooks.ts +116 -0
  26. package/src/core/instructions.ts +126 -0
  27. package/src/core/permission.ts +54 -0
  28. package/src/core/session-store.ts +136 -0
  29. package/src/index.tsx +93 -0
  30. package/src/mcp/client.ts +170 -0
  31. package/src/mcp/protocol.ts +104 -0
  32. package/src/mcp/transport.ts +169 -0
  33. package/src/mcp/types.ts +112 -0
  34. package/src/providers/anthropic.ts +286 -0
  35. package/src/providers/bootstrap.ts +28 -0
  36. package/src/providers/openai-compat.ts +158 -0
  37. package/src/providers/registry.ts +70 -0
  38. package/src/security/path.ts +90 -0
  39. package/src/security/url.ts +96 -0
  40. package/src/shared/constants.ts +368 -0
  41. package/src/shared/index.ts +2 -0
  42. package/src/shared/types.ts +161 -0
  43. package/src/skills/loader.ts +109 -0
  44. package/src/skills/mipham/runtime.ts +63 -0
  45. package/src/skills/standard/runtime.ts +59 -0
  46. package/src/tools/agent/agent.ts +51 -0
  47. package/src/tools/agent/memory.ts +51 -0
  48. package/src/tools/agent/plan.ts +87 -0
  49. package/src/tools/agent/skill.ts +58 -0
  50. package/src/tools/exec/bash.ts +111 -0
  51. package/src/tools/exec/git.ts +63 -0
  52. package/src/tools/exec/task.ts +69 -0
  53. package/src/tools/file/edit.ts +57 -0
  54. package/src/tools/file/glob.ts +29 -0
  55. package/src/tools/file/grep.ts +52 -0
  56. package/src/tools/file/read.ts +38 -0
  57. package/src/tools/file/write.ts +27 -0
  58. package/src/tools/index.ts +126 -0
  59. package/src/tools/network/web-fetch.ts +61 -0
  60. package/src/tools/network/web-search.ts +32 -0
  61. package/src/tools/system/config.ts +68 -0
  62. package/src/tools/system/mcp.ts +75 -0
  63. package/src/ui/app.tsx +317 -0
  64. package/src/ui/chat.tsx +76 -0
  65. package/src/ui/commands.ts +2232 -0
  66. package/src/ui/input.tsx +83 -0
  67. package/src/ui/picker.tsx +209 -0
@@ -0,0 +1,38 @@
1
+ import { readFileSync, existsSync, statSync } from 'node:fs'
2
+ import type { ToolDefinition } from '../../shared/index.ts'
3
+ import { resolveSafe } from '../../security/path'
4
+
5
+ export const readTool: ToolDefinition = {
6
+ name: 'Read',
7
+ description: 'Read a file from the local filesystem. Supports offset and limit for large files.',
8
+ category: 'file',
9
+ permission: 'auto',
10
+ parameters: {
11
+ type: 'object',
12
+ properties: {
13
+ file_path: { type: 'string', description: 'Absolute path to the file to read' },
14
+ offset: { type: 'integer', description: 'Line number to start reading from' },
15
+ limit: { type: 'integer', description: 'Number of lines to read' },
16
+ },
17
+ required: ['file_path'],
18
+ },
19
+ async execute(params, ctx) {
20
+ const filePath = resolveSafe(ctx.cwd, params.file_path as string)
21
+ if (!existsSync(filePath)) {
22
+ return { success: false, content: '', error: `File not found: ${filePath}` }
23
+ }
24
+ const stat = statSync(filePath)
25
+ if (stat.isDirectory()) {
26
+ return { success: false, content: '', error: `Path is a directory: ${filePath}` }
27
+ }
28
+ const offset = (params.offset as number) || 0
29
+ const limit = (params.limit as number) || 2000
30
+ const content = readFileSync(filePath, 'utf-8')
31
+ const lines = content.split('\n')
32
+ const slice = lines.slice(offset, offset + limit)
33
+ const result = slice
34
+ .map((l, i) => `${String(offset + i + 1).padStart(6, ' ')}\t${l}`)
35
+ .join('\n')
36
+ return { success: true, content: result }
37
+ },
38
+ }
@@ -0,0 +1,27 @@
1
+ import { writeFileSync, mkdirSync } from 'node:fs'
2
+ import { dirname } from 'node:path'
3
+ import type { ToolDefinition } from '../../shared/index.ts'
4
+ import { resolveSafe } from '../../security/path'
5
+
6
+ export const writeTool: ToolDefinition = {
7
+ name: 'Write',
8
+ description: 'Write a file to the local filesystem. Creates parent directories if needed.',
9
+ category: 'file',
10
+ permission: 'ask',
11
+ parameters: {
12
+ type: 'object',
13
+ properties: {
14
+ file_path: { type: 'string', description: 'Absolute path to write to' },
15
+ content: { type: 'string', description: 'Content to write' },
16
+ },
17
+ required: ['file_path', 'content'],
18
+ },
19
+ async execute(params, ctx) {
20
+ const filePath = resolveSafe(ctx.cwd, params.file_path as string)
21
+ const content = params.content as string
22
+ mkdirSync(dirname(filePath), { recursive: true })
23
+ writeFileSync(filePath, content, 'utf-8')
24
+ const lines = content.split('\n').length
25
+ return { success: true, content: `Wrote ${lines} lines to ${filePath}` }
26
+ },
27
+ }
@@ -0,0 +1,126 @@
1
+ import type { ToolDefinition, ToolResult } from '../shared/index.ts'
2
+ import { readTool } from './file/read'
3
+ import { writeTool } from './file/write'
4
+ import { editTool } from './file/edit'
5
+ import { globTool } from './file/glob'
6
+ import { grepTool } from './file/grep'
7
+ import { bashTool } from './exec/bash'
8
+ import { gitTool } from './exec/git'
9
+ import { taskTool } from './exec/task'
10
+ import { agentTool } from './agent/agent'
11
+ import { skillTool } from './agent/skill'
12
+ import { planTool } from './agent/plan'
13
+ import { memoryTool } from './agent/memory'
14
+ import { webFetchTool } from './network/web-fetch'
15
+ import { webSearchTool } from './network/web-search'
16
+ import { configTool } from './system/config'
17
+ import { mcpTool } from './system/mcp'
18
+
19
+ /**
20
+ * Validate tool parameters against the tool's JSON Schema definition.
21
+ * Returns an array of error messages (empty = valid).
22
+ */
23
+ function validateParams(
24
+ schema: Record<string, unknown>,
25
+ params: Record<string, unknown>,
26
+ ): string[] {
27
+ const errors: string[] = []
28
+
29
+ // Check required fields
30
+ const required = schema.required as string[] | undefined
31
+ if (required) {
32
+ for (const field of required) {
33
+ if (params[field] === undefined || params[field] === null) {
34
+ errors.push(`Missing required parameter: "${field}"`)
35
+ }
36
+ }
37
+ }
38
+
39
+ // Check types for provided fields
40
+ const properties = schema.properties as
41
+ | Record<string, { type: string; enum?: string[] }>
42
+ | undefined
43
+ if (properties) {
44
+ for (const [key, def] of Object.entries(properties)) {
45
+ const value = params[key]
46
+ if (value === undefined || value === null) continue // already caught by required check
47
+
48
+ switch (def.type) {
49
+ case 'string':
50
+ if (typeof value !== 'string') errors.push(`"${key}" must be a string`)
51
+ else if (def.enum && !def.enum.includes(value)) {
52
+ errors.push(`"${key}" must be one of: ${def.enum.join(', ')}`)
53
+ }
54
+ break
55
+ case 'integer':
56
+ case 'number':
57
+ if (typeof value !== 'number') errors.push(`"${key}" must be a number`)
58
+ break
59
+ case 'boolean':
60
+ if (typeof value !== 'boolean') errors.push(`"${key}" must be a boolean`)
61
+ break
62
+ case 'object':
63
+ if (typeof value !== 'object' || Array.isArray(value)) {
64
+ errors.push(`"${key}" must be an object`)
65
+ }
66
+ break
67
+ case 'array':
68
+ if (!Array.isArray(value)) errors.push(`"${key}" must be an array`)
69
+ break
70
+ }
71
+ }
72
+ }
73
+
74
+ return errors
75
+ }
76
+
77
+ /**
78
+ * Wrap a tool's execute with parameter validation.
79
+ */
80
+ function withValidation(tool: ToolDefinition): ToolDefinition {
81
+ const schema = tool.parameters as Record<string, unknown>
82
+ if (!schema || !schema.properties) return tool // no schema to validate against
83
+
84
+ return {
85
+ ...tool,
86
+ async execute(params, ctx): Promise<ToolResult> {
87
+ const errors = validateParams(schema, params)
88
+ if (errors.length > 0) {
89
+ return { success: false, content: '', error: `Invalid parameters: ${errors.join('; ')}` }
90
+ }
91
+ return tool.execute(params, ctx)
92
+ },
93
+ }
94
+ }
95
+
96
+ export function createToolRegistry(): Map<string, ToolDefinition> {
97
+ const tools: ToolDefinition[] = [
98
+ // File tools
99
+ withValidation(readTool),
100
+ withValidation(writeTool),
101
+ withValidation(editTool),
102
+ withValidation(globTool),
103
+ withValidation(grepTool),
104
+ // Exec tools
105
+ withValidation(bashTool),
106
+ withValidation(gitTool),
107
+ withValidation(taskTool),
108
+ // Agent tools
109
+ withValidation(agentTool),
110
+ withValidation(skillTool),
111
+ withValidation(planTool),
112
+ withValidation(memoryTool),
113
+ // Network tools
114
+ withValidation(webFetchTool),
115
+ withValidation(webSearchTool),
116
+ // System tools
117
+ withValidation(configTool),
118
+ withValidation(mcpTool),
119
+ ]
120
+
121
+ const map = new Map<string, ToolDefinition>()
122
+ for (const tool of tools) {
123
+ map.set(tool.name, tool)
124
+ }
125
+ return map
126
+ }
@@ -0,0 +1,61 @@
1
+ import type { ToolDefinition } from '../../shared/index.ts'
2
+ import { validateUrl } from '../../security/url'
3
+
4
+ export const webFetchTool: ToolDefinition = {
5
+ name: 'WebFetch',
6
+ description: 'Fetch content from a URL and process into markdown.',
7
+ category: 'network',
8
+ permission: 'auto',
9
+ parameters: {
10
+ type: 'object',
11
+ properties: {
12
+ url: { type: 'string', format: 'uri', description: 'URL to fetch' },
13
+ prompt: { type: 'string', description: 'Prompt to run on fetched content' },
14
+ },
15
+ required: ['url'],
16
+ },
17
+ async execute(params, _ctx) {
18
+ const url = params.url as string
19
+
20
+ // SSRF protection: validate URL before fetching
21
+ const validationError = validateUrl(url)
22
+ if (validationError) {
23
+ return { success: false, content: '', error: validationError }
24
+ }
25
+
26
+ try {
27
+ const controller = new AbortController()
28
+ const timer = setTimeout(() => controller.abort(), 30_000) // 30s timeout
29
+
30
+ const response = await fetch(url, {
31
+ headers: { 'User-Agent': 'Mipham-Code/0.1.0' },
32
+ redirect: 'follow',
33
+ signal: controller.signal,
34
+ })
35
+
36
+ clearTimeout(timer)
37
+
38
+ if (!response.ok) {
39
+ return {
40
+ success: false,
41
+ content: '',
42
+ error: `HTTP ${response.status}: ${response.statusText}`,
43
+ }
44
+ }
45
+ const html = await response.text()
46
+ // Simple HTML-to-text extraction
47
+ const text = html
48
+ .replace(/<[^>]*>/g, ' ')
49
+ .replace(/\s+/g, ' ')
50
+ .trim()
51
+ .slice(0, 50_000)
52
+ return { success: true, content: text }
53
+ } catch (err) {
54
+ const message =
55
+ err instanceof Error && err.name === 'AbortError'
56
+ ? 'Request timed out (30s)'
57
+ : `Fetch failed: ${String(err)}`
58
+ return { success: false, content: '', error: message }
59
+ }
60
+ },
61
+ }
@@ -0,0 +1,32 @@
1
+ import type { ToolDefinition } from '../../shared/index.ts'
2
+
3
+ export const webSearchTool: ToolDefinition = {
4
+ name: 'WebSearch',
5
+ description: 'Search the web. Returns result titles and URLs.',
6
+ category: 'network',
7
+ permission: 'auto',
8
+ parameters: {
9
+ type: 'object',
10
+ properties: {
11
+ query: { type: 'string', minLength: 2, description: 'Search query' },
12
+ allowed_domains: {
13
+ type: 'array',
14
+ items: { type: 'string' },
15
+ description: 'Only include results from these domains',
16
+ },
17
+ blocked_domains: {
18
+ type: 'array',
19
+ items: { type: 'string' },
20
+ description: 'Never include results from these domains',
21
+ },
22
+ },
23
+ required: ['query'],
24
+ },
25
+ async execute(params, _ctx) {
26
+ const query = params.query as string
27
+ return {
28
+ success: true,
29
+ content: `WebSearch query: "${query}". Results would appear here when search API is configured.`,
30
+ }
31
+ },
32
+ }
@@ -0,0 +1,68 @@
1
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { parse as parseYaml, stringify } from 'yaml'
4
+ import type { ToolDefinition } from '../../shared/index.ts'
5
+
6
+ const MIPHAM_DIR = join(process.env.HOME || '~', '.mipham')
7
+ const USER_CONFIG = join(MIPHAM_DIR, 'config.yml')
8
+
9
+ export const configTool: ToolDefinition = {
10
+ name: 'Config',
11
+ description: 'Read or update Mipham Code configuration.',
12
+ category: 'system',
13
+ permission: 'ask',
14
+ parameters: {
15
+ type: 'object',
16
+ properties: {
17
+ action: {
18
+ type: 'string',
19
+ enum: ['get', 'set', 'list'],
20
+ description: 'Action to perform',
21
+ },
22
+ key: { type: 'string', description: 'Config key (dot notation)' },
23
+ value: { type: 'string', description: 'Value to set' },
24
+ },
25
+ required: ['action'],
26
+ },
27
+ async execute(params, _ctx) {
28
+ mkdirSync(MIPHAM_DIR, { recursive: true })
29
+ const action = params.action as string
30
+
31
+ let config: Record<string, unknown> = {}
32
+ if (existsSync(USER_CONFIG)) {
33
+ config = parseYaml(readFileSync(USER_CONFIG, 'utf-8')) as Record<string, unknown>
34
+ }
35
+
36
+ if (action === 'list') {
37
+ return { success: true, content: stringify(config) || '(empty config)' }
38
+ }
39
+
40
+ const key = params.key as string
41
+ if (!key) return { success: false, content: '', error: 'key is required for get/set' }
42
+
43
+ if (action === 'get') {
44
+ const value = key.split('.').reduce((obj: unknown, k) => {
45
+ if (obj && typeof obj === 'object') {
46
+ return (obj as Record<string, unknown>)[k]
47
+ }
48
+ return undefined
49
+ }, config)
50
+ return { success: true, content: JSON.stringify(value) }
51
+ }
52
+
53
+ if (action === 'set') {
54
+ const keys = key.split('.')
55
+ let obj: Record<string, unknown> = config
56
+ for (let i = 0; i < keys.length - 1; i++) {
57
+ const k = keys[i]!
58
+ if (!obj[k]) obj[k] = {}
59
+ obj = obj[k] as Record<string, unknown>
60
+ }
61
+ obj[keys[keys.length - 1]!] = params.value
62
+ writeFileSync(USER_CONFIG, stringify(config), 'utf-8')
63
+ return { success: true, content: `Set ${key} = ${params.value}` }
64
+ }
65
+
66
+ return { success: false, content: '', error: `Unknown action: ${action}` }
67
+ },
68
+ }
@@ -0,0 +1,75 @@
1
+ import type { ToolDefinition } from '../../shared/index.ts'
2
+ import { McpClient } from '../../mcp/client'
3
+
4
+ export const mcpTool: ToolDefinition = {
5
+ name: 'MCP',
6
+ description:
7
+ 'Interact with MCP (Model Context Protocol) servers. Call tools on connected MCP servers via JSON-RPC 2.0 over stdio.',
8
+ category: 'system',
9
+ permission: 'ask',
10
+ parameters: {
11
+ type: 'object',
12
+ properties: {
13
+ server: {
14
+ type: 'string',
15
+ description: 'MCP server name as configured in .mipham/config.yml',
16
+ },
17
+ tool: { type: 'string', description: 'Tool name on the MCP server' },
18
+ params: { type: 'object', description: 'Parameters to pass to the tool' },
19
+ },
20
+ required: ['server', 'tool'],
21
+ },
22
+ async execute(params, _ctx) {
23
+ const server = params.server as string
24
+ const toolName = params.tool as string
25
+ const toolParams = (params.params as Record<string, unknown>) || {}
26
+
27
+ const client = McpClient.getInstance()
28
+ const connection = client.getConnection(server)
29
+
30
+ if (!connection) {
31
+ return {
32
+ success: false,
33
+ content: '',
34
+ error: `MCP server "${server}" is not configured.\n\nAdd it to .mipham/config.yml under skills.mcpServers, or check available servers with /mcp.`,
35
+ }
36
+ }
37
+
38
+ if (connection.status !== 'connected') {
39
+ return {
40
+ success: false,
41
+ content: '',
42
+ error: `MCP server "${server}" is not connected (status: ${connection.status}).\n${connection.error ? 'Error: ' + connection.error : 'Use /mcp to check status.'}`,
43
+ }
44
+ }
45
+
46
+ // Check if the requested tool exists on this server
47
+ const toolExists = connection.tools.some((t) => t.name === toolName)
48
+ if (!toolExists) {
49
+ const availableTools = connection.tools.map((t) => ` • ${t.name}`).join('\n')
50
+ return {
51
+ success: false,
52
+ content: '',
53
+ error: `Tool "${toolName}" not found on MCP server "${server}".\n\nAvailable tools:\n${availableTools || ' (none discovered)'}`,
54
+ }
55
+ }
56
+
57
+ // Execute the tool via the MCP client
58
+ const result = await client.callTool(server, toolName, toolParams)
59
+
60
+ // Format the result content
61
+ const text = result.content
62
+ .map((c) => {
63
+ if (c.type === 'text' && c.text) return c.text
64
+ if (c.type === 'image') return `[Image: ${c.mimeType || 'unknown'}]`
65
+ return JSON.stringify(c)
66
+ })
67
+ .join('\n')
68
+
69
+ return {
70
+ success: !result.isError,
71
+ content: text || '(empty result)',
72
+ error: result.isError ? text : undefined,
73
+ }
74
+ },
75
+ }