@miphamai/cli 0.56.0 → 0.58.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.
- package/bin/mipham.ts +35 -0
- package/package.json +1 -1
- package/skills/standard/mipham-code-setup.SKILL.md +5 -9
- package/src/agent/sub-agent.ts +0 -3
- package/src/agent/types.ts +1 -2
- package/src/commands/project.ts +8 -7
- package/src/config/defaults.ts +1 -1
- package/src/core/crsi-producer.ts +41 -0
- package/src/core/engine.ts +19 -1
- package/src/core/instructions.ts +35 -3
- package/src/core/permission-config.ts +0 -2
- package/src/core/permission.ts +5 -60
- package/src/core/project-scaffold.ts +158 -0
- package/src/daemon/server.ts +0 -2
- package/src/i18n-core/locales/en-US.json +3 -5
- package/src/i18n-core/locales/zh-CN.json +3 -5
- package/src/index.tsx +8 -0
- package/src/security/gate.ts +3 -2
- package/src/shared/graft-stats.ts +18 -1
- package/src/shared/package-info.ts +1 -1
- package/src/shared/types.ts +5 -6
- package/src/skills/bundled-skills.ts +1 -1
- package/src/tools/exec/bash.ts +4 -4
- package/src/ui/app.tsx +53 -34
- package/src/ui/graft-status.tsx +27 -23
- package/src/ui/input.tsx +73 -163
- package/src/ui/vim-motions.ts +0 -113
package/bin/mipham.ts
CHANGED
|
@@ -1057,6 +1057,36 @@ async function runTokenCLI(): Promise<boolean> {
|
|
|
1057
1057
|
process.exit(1)
|
|
1058
1058
|
}
|
|
1059
1059
|
|
|
1060
|
+
// ── mipham init [dir] — 生成规范项目文档 (CLAUDE.md/MIPHAM.md/README.md) ──
|
|
1061
|
+
async function runInitCLI(): Promise<boolean> {
|
|
1062
|
+
const args = process.argv.slice(2)
|
|
1063
|
+
if (args[0] !== 'init') return false
|
|
1064
|
+
|
|
1065
|
+
const { resolve } = await import('node:path')
|
|
1066
|
+
const { homedir } = await import('node:os')
|
|
1067
|
+
const { scaffoldProject } = await import('../src/core/project-scaffold')
|
|
1068
|
+
|
|
1069
|
+
const rawTarget = args.slice(1).find((a) => !a.startsWith('--'))
|
|
1070
|
+
const targetDir = rawTarget ? resolve(rawTarget.replace(/^~/, homedir())) : process.cwd()
|
|
1071
|
+
|
|
1072
|
+
const gitInit = !args.includes('--no-git')
|
|
1073
|
+
const result = scaffoldProject(targetDir, { gitInit })
|
|
1074
|
+
|
|
1075
|
+
const lines: string[] = ['', `✅ Mipham Code 项目已初始化:${targetDir}`, '']
|
|
1076
|
+
if (result.created.length > 0) {
|
|
1077
|
+
lines.push('已创建:')
|
|
1078
|
+
for (const c of result.created) lines.push(` ✅ ${c}`)
|
|
1079
|
+
}
|
|
1080
|
+
if (result.skipped.length > 0) {
|
|
1081
|
+
lines.push('已存在(跳过):')
|
|
1082
|
+
for (const s of result.skipped) lines.push(` ⏭ ${s}`)
|
|
1083
|
+
}
|
|
1084
|
+
if (result.gitInitialized) lines.push(' ✅ git init')
|
|
1085
|
+
lines.push('', '下一步:', ' 1. 填写各文档中的 [占位符]', ' 2. 运行 mipham 开始协作')
|
|
1086
|
+
console.log(lines.join('\n'))
|
|
1087
|
+
process.exit(0)
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1060
1090
|
async function main() {
|
|
1061
1091
|
// ── Deleted-cwd guard ──────────────────────────────────────────────────
|
|
1062
1092
|
// `process.cwd()` throws ENOENT when the directory the process was launched
|
|
@@ -1129,6 +1159,7 @@ async function main() {
|
|
|
1129
1159
|
|
|
1130
1160
|
Usage:
|
|
1131
1161
|
mipham Launch interactive CLI
|
|
1162
|
+
mipham init [dir] Scaffold a project (CLAUDE.md/MIPHAM.md/README.md)
|
|
1132
1163
|
mipham update Update to the latest version
|
|
1133
1164
|
mipham upgrade Same as 'mipham update'
|
|
1134
1165
|
mipham daemon <cmd> Daemon lifecycle (start, stop, status, restart)
|
|
@@ -1183,6 +1214,10 @@ npm: https://www.npmjs.com/package/@miphamai/cli`)
|
|
|
1183
1214
|
const handledUpdate = await runUpdate()
|
|
1184
1215
|
if (handledUpdate) return
|
|
1185
1216
|
|
|
1217
|
+
// ── Init command ─────────────────────────────────────────────────────────
|
|
1218
|
+
const handledInit = await runInitCLI()
|
|
1219
|
+
if (handledInit) return
|
|
1220
|
+
|
|
1186
1221
|
// ── Daemon commands ───────────────────────────────────────────────────────
|
|
1187
1222
|
const handledDaemon = await runDaemonCLI()
|
|
1188
1223
|
if (handledDaemon) return
|
package/package.json
CHANGED
|
@@ -77,7 +77,7 @@ Project: [✅/⬜] .mipham/ [✅/⬜] config.yml [✅/⬜] MIPHAM.md
|
|
|
77
77
|
User Config: [✅/⬜] ~/.mipham/config.yml
|
|
78
78
|
API Keys: [N] set (list names or "none")
|
|
79
79
|
Skills: [N] installed
|
|
80
|
-
Permissions: [mode] (default/acceptEdits/plan/
|
|
80
|
+
Permissions: [mode] (default/acceptEdits/plan/bypassPermissions)
|
|
81
81
|
Trust: [✅/⬜] workspace trusted
|
|
82
82
|
```
|
|
83
83
|
|
|
@@ -336,8 +336,6 @@ Or directly:
|
|
|
336
336
|
| `default` | Prompt for each tool | Normal development (recommended) |
|
|
337
337
|
| `acceptEdits` | Auto-allow edits, prompt others | Active coding sessions |
|
|
338
338
|
| `plan` | Plan-only, no tool execution | Design & architecture work |
|
|
339
|
-
| `auto` | Auto-allow all | Trusted, frequent use |
|
|
340
|
-
| `dontAsk` | Never auto-allow | CI/CD safety |
|
|
341
339
|
| `bypassPermissions` | Skip all checks | ⚠️ Only for fully trusted codebases |
|
|
342
340
|
|
|
343
341
|
### 6.2 — Configure
|
|
@@ -345,7 +343,7 @@ Or directly:
|
|
|
345
343
|
In `.mipham/config.yml`:
|
|
346
344
|
|
|
347
345
|
```yaml
|
|
348
|
-
permission:
|
|
346
|
+
permission: default
|
|
349
347
|
```
|
|
350
348
|
|
|
351
349
|
Or via slash command:
|
|
@@ -357,11 +355,9 @@ Or via slash command:
|
|
|
357
355
|
|
|
358
356
|
### 6.3 — CI/CD Safety
|
|
359
357
|
|
|
360
|
-
For CI/CD environments, use `
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
permission: dontAsk
|
|
364
|
-
```
|
|
358
|
+
For CI/CD environments, use the `default` mode (the daemon default): headless
|
|
359
|
+
sessions never prompt, so `ask`-level tools (Bash/Write/Edit) are blocked rather
|
|
360
|
+
than auto-approved.
|
|
365
361
|
|
|
366
362
|
### ✅ Verification
|
|
367
363
|
|
package/src/agent/sub-agent.ts
CHANGED
|
@@ -210,9 +210,6 @@ export class SubAgent {
|
|
|
210
210
|
? agentDef.permissionMode
|
|
211
211
|
: 'inherit'
|
|
212
212
|
subPermission = this.permission.createSubAgentPermission(agentPermMode)
|
|
213
|
-
// P0-5: Enable sub-agent safety mode — auto mode returns 'ask' for
|
|
214
|
-
// Bash/Write/Edit so hooks remain the safety gate in background agents.
|
|
215
|
-
subPermission.setSubAgentMode(true)
|
|
216
213
|
}
|
|
217
214
|
|
|
218
215
|
// Resolve execution directory: worktree isolation or process cwd
|
package/src/agent/types.ts
CHANGED
|
@@ -10,8 +10,7 @@ export interface AgentFrontmatter {
|
|
|
10
10
|
tools?: string // comma-separated allowlist
|
|
11
11
|
disallowedTools?: string
|
|
12
12
|
model?: string // 'sonnet' | 'opus' | 'haiku' | 'inherit' | full model ID
|
|
13
|
-
permissionMode?:
|
|
14
|
-
'default' | 'acceptEdits' | 'auto' | 'bypass' | 'plan' | 'bypassPermissions' | 'dontAsk'
|
|
13
|
+
permissionMode?: 'default' | 'acceptEdits' | 'bypass' | 'plan' | 'bypassPermissions'
|
|
15
14
|
maxTurns?: number
|
|
16
15
|
skills?: string
|
|
17
16
|
background?: boolean
|
package/src/commands/project.ts
CHANGED
|
@@ -111,21 +111,22 @@ const permissionsCmd: CommandHandler = (ctx) => {
|
|
|
111
111
|
return {
|
|
112
112
|
content: `─ Permission Settings ─
|
|
113
113
|
|
|
114
|
-
Mode: ${ctx.
|
|
114
|
+
Mode: ${ctx.engine.getPermission().getMode()}
|
|
115
115
|
Messages: ${msgs.length} in context
|
|
116
116
|
Tools: ${ctx.engine.getTools().size} available
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
Switch mode with Shift+Tab. Modes (least → most permissive):
|
|
119
|
+
default — per-tool defaults (Bash/Write/Edit ask first)
|
|
120
|
+
acceptEdits — reads + edits free; Bash auto-runs read/check commands
|
|
121
|
+
plan — reads only (Read/Grep/Glob); nothing writes or runs
|
|
122
|
+
bypassPermissions — skip ALL permission checks (⚠️ use with caution)
|
|
122
123
|
|
|
123
|
-
|
|
124
|
+
To let Bash run without asking: press Shift+Tab until the status line shows
|
|
125
|
+
"acceptEdits", then send your message again.
|
|
124
126
|
|
|
125
127
|
Current directory permissions:
|
|
126
128
|
CWD: ${process.cwd()}
|
|
127
129
|
|
|
128
|
-
To add a directory: use /add-dir (coming soon).
|
|
129
130
|
Tool execution is sandboxed to the project directory by default.`,
|
|
130
131
|
}
|
|
131
132
|
}
|
package/src/config/defaults.ts
CHANGED
|
@@ -12,7 +12,7 @@ export const DEFAULT_CONFIG: MiphamConfig = {
|
|
|
12
12
|
version: PACKAGE_VERSION,
|
|
13
13
|
defaultProvider: 'deepseek',
|
|
14
14
|
defaultModel: 'deepseek-v4-pro',
|
|
15
|
-
permission: '
|
|
15
|
+
permission: 'default',
|
|
16
16
|
// Org 级权限限制(可选):forbiddenModes 禁指定模式 / maxAllowedMode 封顶层级;
|
|
17
17
|
// 请求被禁模式时 fail-closed 降级(如 forbiddenModes:['bypassPermissions'])。
|
|
18
18
|
// permissionRestrictions: { forbiddenModes: ['bypassPermissions'] },
|
|
@@ -80,6 +80,47 @@ export function buildLessonContent(signal: CrsiSignal, timestamp: string): strin
|
|
|
80
80
|
return lines.join('\n')
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** 教训精华(标题 + 建议),用于运行时召回注入系统提示。 */
|
|
84
|
+
export interface CrsiLessonSummary {
|
|
85
|
+
title: string
|
|
86
|
+
suggestion: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 从 crsi-lessons.md 提取每条教训的「精华」(标题 + 建议),跳过证据段落。
|
|
91
|
+
* 这是「只写不读」缺口 → 「写后召回」的读取侧。
|
|
92
|
+
*/
|
|
93
|
+
export function extractCrsiLessonSummaries(content: string): CrsiLessonSummary[] {
|
|
94
|
+
const out: CrsiLessonSummary[] = []
|
|
95
|
+
let title = ''
|
|
96
|
+
for (const line of content.split('\n')) {
|
|
97
|
+
const h = line.match(/^##\s+(.+?)\s*$/)
|
|
98
|
+
if (h) {
|
|
99
|
+
title = h[1]!.trim()
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
const s = line.match(/^-\s*建议[::]\s*(.+)$/)
|
|
103
|
+
if (s && title) {
|
|
104
|
+
out.push({ title, suggestion: s[1]!.trim() })
|
|
105
|
+
title = ''
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 把教训精华渲染为系统提示召回块。无教训时返回空串。 */
|
|
112
|
+
export function buildCrsiLessonsBlock(summaries: CrsiLessonSummary[]): string {
|
|
113
|
+
if (summaries.length === 0) return ''
|
|
114
|
+
const items = summaries.map((s, i) => `${i + 1}. **${s.title}**\n ${s.suggestion}`).join('\n\n')
|
|
115
|
+
return `## CRSI Lessons (Self-Improvement Recall)
|
|
116
|
+
|
|
117
|
+
These are hard-won lessons consolidated by the CRSI self-improvement
|
|
118
|
+
loop from past sessions. Apply them proactively — do not repeat these
|
|
119
|
+
mistakes:
|
|
120
|
+
|
|
121
|
+
${items}`
|
|
122
|
+
}
|
|
123
|
+
|
|
83
124
|
/** 产出教训文件变更候选。无合格信号时返回 null。 */
|
|
84
125
|
export function produceCrsiProposal(
|
|
85
126
|
insights: CrsiInsight[],
|
package/src/core/engine.ts
CHANGED
|
@@ -764,10 +764,26 @@ export class QueryEngine {
|
|
|
764
764
|
}
|
|
765
765
|
|
|
766
766
|
private async *continueWithTools(signal?: AbortSignal): AsyncGenerator<StreamChunk> {
|
|
767
|
-
|
|
767
|
+
// Tool-calling round cap. 20 was too low for real multi-step tasks — the model
|
|
768
|
+
// hit "max turns" and dropped pending tools mid-task. 100 stays bounded.
|
|
769
|
+
const MAX_TURNS = 100
|
|
770
|
+
// Task-level stall guard: a turn that produces no text or tool result for
|
|
771
|
+
// this long is considered stalled and stopped (prevents ~40-min idle spins).
|
|
772
|
+
const TURN_TIMEOUT_MS = 15 * 60 * 1000
|
|
773
|
+
let lastActivity = Date.now()
|
|
768
774
|
const toolDefs = this.getToolDefinitions()
|
|
769
775
|
|
|
770
776
|
for (let turn = 0; turn < MAX_TURNS; turn++) {
|
|
777
|
+
if (Date.now() - lastActivity > TURN_TIMEOUT_MS) {
|
|
778
|
+
yield {
|
|
779
|
+
type: 'warning',
|
|
780
|
+
content: t('errors.turn_timeout', {
|
|
781
|
+
minutes: String(Math.round(TURN_TIMEOUT_MS / 60000)),
|
|
782
|
+
}),
|
|
783
|
+
}
|
|
784
|
+
yield { type: 'stop' }
|
|
785
|
+
return
|
|
786
|
+
}
|
|
771
787
|
const systemPrompt = this.context.getSystemPrompt()
|
|
772
788
|
const messages = this.context.getMessages()
|
|
773
789
|
|
|
@@ -812,6 +828,7 @@ export class QueryEngine {
|
|
|
812
828
|
if (chunk.type === 'text' && chunk.content) {
|
|
813
829
|
assistantContent += chunk.content
|
|
814
830
|
this.context.recordChunk(chunk.content)
|
|
831
|
+
lastActivity = Date.now()
|
|
815
832
|
}
|
|
816
833
|
|
|
817
834
|
if (chunk.reasoning_content) {
|
|
@@ -911,6 +928,7 @@ export class QueryEngine {
|
|
|
911
928
|
// Execute tools and feed results back to the model for the next turn
|
|
912
929
|
for (const toolUse of toolUses) {
|
|
913
930
|
const result = await this.executeTool(toolUse.name, toolUse.input)
|
|
931
|
+
lastActivity = Date.now()
|
|
914
932
|
yield {
|
|
915
933
|
type: 'tool_result',
|
|
916
934
|
tool_use_id: toolUse.id,
|
package/src/core/instructions.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
+
import { execSync } from 'node:child_process'
|
|
3
4
|
import { parse as parseYaml } from 'yaml'
|
|
4
5
|
import type { InstructionFile } from '../shared/index.ts'
|
|
5
6
|
import { COAUTHOR_TRAILER } from '../shared/index.ts'
|
|
7
|
+
import {
|
|
8
|
+
LESSONS_FILE,
|
|
9
|
+
extractCrsiLessonSummaries,
|
|
10
|
+
buildCrsiLessonsBlock,
|
|
11
|
+
type CrsiLessonSummary,
|
|
12
|
+
} from './crsi-producer'
|
|
6
13
|
|
|
7
14
|
interface FrontmatterResult {
|
|
8
15
|
data: Record<string, unknown>
|
|
@@ -54,6 +61,7 @@ export function parsePromptExclude(value: unknown): string[] {
|
|
|
54
61
|
|
|
55
62
|
export class InstructionsLoader {
|
|
56
63
|
private instructions: InstructionFile[] = []
|
|
64
|
+
private crsiLessonSummaries: CrsiLessonSummary[] = []
|
|
57
65
|
|
|
58
66
|
loadAll(cwd: string): void {
|
|
59
67
|
this.instructions = []
|
|
@@ -74,6 +82,9 @@ export class InstructionsLoader {
|
|
|
74
82
|
// Tier 3: User-level ~/.mipham/USER.md
|
|
75
83
|
const home = process.env.HOME || '~'
|
|
76
84
|
this.tryLoad(join(home, '.mipham', 'USER.md'), 'user')
|
|
85
|
+
|
|
86
|
+
// CRSI 教训召回:读 crsi-lessons.md 提取精华,注入系统提示(只写不读 → 写后召回)
|
|
87
|
+
this.crsiLessonSummaries = this.loadCrsiLessons(cwd)
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
buildSystemPrompt(permissionMode?: string): string {
|
|
@@ -206,6 +217,10 @@ its live CRSI / SIS / constitution state. Report the numbers you read
|
|
|
206
217
|
from it as live counts; if it shows a subsystem as 未初始化 (uninitialized),
|
|
207
218
|
say so explicitly instead of claiming it exists.`)
|
|
208
219
|
|
|
220
|
+
// CRSI 教训召回 — 把 crsi-lessons.md 的教训精华注入,让模型「写后召回」而非只写不读
|
|
221
|
+
const lessonsBlock = buildCrsiLessonsBlock(this.crsiLessonSummaries)
|
|
222
|
+
if (lessonsBlock) parts.push(lessonsBlock)
|
|
223
|
+
|
|
209
224
|
// AI 署名披露:提交时附带 Co-Authored-By 署名(与 Undercover 式隐瞒相反)
|
|
210
225
|
parts.push(`## Commit Attribution
|
|
211
226
|
|
|
@@ -219,6 +234,26 @@ Never omit it or present the work as purely human-authored.`)
|
|
|
219
234
|
return parts.join('\n\n---\n\n')
|
|
220
235
|
}
|
|
221
236
|
|
|
237
|
+
/** 读 crsi-lessons.md(按仓库根定位)提取教训精华。读不到则返回空。 */
|
|
238
|
+
private loadCrsiLessons(cwd: string): CrsiLessonSummary[] {
|
|
239
|
+
let root = cwd
|
|
240
|
+
try {
|
|
241
|
+
root = execSync('git rev-parse --show-toplevel', {
|
|
242
|
+
cwd,
|
|
243
|
+
timeout: 5000,
|
|
244
|
+
encoding: 'utf-8',
|
|
245
|
+
}).trim()
|
|
246
|
+
} catch {
|
|
247
|
+
// 非 git 目录 → 回退 cwd
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const content = readFileSync(join(root, LESSONS_FILE), 'utf-8')
|
|
251
|
+
return extractCrsiLessonSummaries(content)
|
|
252
|
+
} catch {
|
|
253
|
+
return []
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
222
257
|
/**
|
|
223
258
|
* P2-2: Build a permission-mode context block for the system prompt.
|
|
224
259
|
* Tells the model its current permission level and what to expect.
|
|
@@ -230,9 +265,6 @@ Never omit it or present the work as purely human-authored.`)
|
|
|
230
265
|
acceptEdits:
|
|
231
266
|
'You are in **acceptEdits** mode. File reads and edits are allowed; Bash requires approval.',
|
|
232
267
|
plan: 'You are in **plan** mode. Only Read/Grep/Glob are allowed — no file modifications or command execution.',
|
|
233
|
-
auto: 'You are in **auto** mode. Most tools run without approval. If a tool is blocked by security policy or hooks, try a different approach instead of retrying.',
|
|
234
|
-
dontAsk:
|
|
235
|
-
'You are in **dontAsk** mode. All tools blocked unless explicitly allowlisted. Check your allow rules before acting.',
|
|
236
268
|
bypassPermissions:
|
|
237
269
|
'You are in **bypassPermissions** mode. All tools are allowed. Use this power responsibly.',
|
|
238
270
|
}
|
package/src/core/permission.ts
CHANGED
|
@@ -9,20 +9,6 @@ import type { PermissionRuleEntry } from '../shared/index.ts'
|
|
|
9
9
|
import { matchBashRule, compileRule } from './permission-rules'
|
|
10
10
|
import { loadPermissionConfig, nextMode, clampMode, MODE_CYCLE } from './permission-config'
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* Check if a Bash command is a git/gh operation using a destructive flag
|
|
14
|
-
* (--force, --amend, --no-verify, --hard) that must never be auto-approved.
|
|
15
|
-
* v2.1.229 alignment: these rewrite history / force-push, so they fall back
|
|
16
|
-
* to explicit user confirmation even in auto mode.
|
|
17
|
-
*/
|
|
18
|
-
function hasDangerousGitFlags(input: Record<string, unknown>): boolean {
|
|
19
|
-
const cmd = (input.command as string) || ''
|
|
20
|
-
// Detect a git/gh invocation anywhere in the command (not just `^git`), so
|
|
21
|
-
// `echo ok && git push --force` is still caught.
|
|
22
|
-
if (!/(?:^|[\s;&|])(?:sudo\s+)?(?:git|gh)\s+/.test(cmd)) return false
|
|
23
|
-
return /--force(?!-with-lease)\b|--amend\b|--no-verify\b|--hard\b/.test(cmd)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
12
|
/**
|
|
27
13
|
* Check if a Bash command is a "verification-only" command that should be
|
|
28
14
|
* auto-approved in acceptEdits mode. These are non-destructive read/check
|
|
@@ -94,9 +80,6 @@ export class PermissionSystem {
|
|
|
94
80
|
// ── Org-level restrictions (P0 security) ──
|
|
95
81
|
private restrictions: PermissionRestrictions | undefined = undefined
|
|
96
82
|
|
|
97
|
-
// ── P0-5: Sub-agent mode (auto mode hardening for background tasks) ──
|
|
98
|
-
private isSubAgent = false
|
|
99
|
-
|
|
100
83
|
// ── P1-4: Consecutive block counter (prevents infinite retry loops) ──
|
|
101
84
|
private consecutiveBlockCount = 0
|
|
102
85
|
private static readonly MAX_CONSECUTIVE_BLOCKS = 3
|
|
@@ -149,16 +132,6 @@ export class PermissionSystem {
|
|
|
149
132
|
return this.restrictions
|
|
150
133
|
}
|
|
151
134
|
|
|
152
|
-
/**
|
|
153
|
-
* P0-5 (v2.1.222 alignment): Enable sub-agent safety mode.
|
|
154
|
-
* When true, auto mode returns 'ask' for Bash/Write/Edit tools instead of
|
|
155
|
-
* 'bypass', ensuring PreToolUse hooks remain the safety gate in background agents.
|
|
156
|
-
*/
|
|
157
|
-
setSubAgentMode(enabled: boolean): void {
|
|
158
|
-
this.isSubAgent = enabled
|
|
159
|
-
this.invalidateCache()
|
|
160
|
-
}
|
|
161
|
-
|
|
162
135
|
/**
|
|
163
136
|
* P1-4 (v2.1.225 alignment): Increment the consecutive block counter
|
|
164
137
|
* when a tool is denied. Safety-filter refusals should NOT count.
|
|
@@ -216,8 +189,6 @@ export class PermissionSystem {
|
|
|
216
189
|
|
|
217
190
|
const modeMap: Record<string, PermissionMode> = {
|
|
218
191
|
bypassPermissions: 'bypassPermissions',
|
|
219
|
-
dontAsk: 'dontAsk',
|
|
220
|
-
auto: 'auto',
|
|
221
192
|
plan: 'plan',
|
|
222
193
|
acceptEdits: 'acceptEdits',
|
|
223
194
|
default: 'default',
|
|
@@ -418,30 +389,6 @@ export class PermissionSystem {
|
|
|
418
389
|
? 'bypass'
|
|
419
390
|
: 'ask'
|
|
420
391
|
|
|
421
|
-
case 'auto':
|
|
422
|
-
// Safety checks handled by hook layer (PreToolUse hooks).
|
|
423
|
-
// Bypass the static permission system so hooks are the sole gate.
|
|
424
|
-
// Exception: SendMessage always goes through the permission classifier
|
|
425
|
-
// so deny/allow rules are honored for cross-session messages.
|
|
426
|
-
if (tool.name === 'SendMessage') {
|
|
427
|
-
return 'mode-baseline'
|
|
428
|
-
}
|
|
429
|
-
// P0-5: In sub-agent context, enforce 'ask' for destructive tools
|
|
430
|
-
// so hooks remain the sole safety gate. Without hooks, these tools
|
|
431
|
-
// would otherwise run completely un-gated in auto mode.
|
|
432
|
-
if (this.isSubAgent && ['Bash', 'Write', 'Edit'].includes(tool.name)) {
|
|
433
|
-
return 'ask'
|
|
434
|
-
}
|
|
435
|
-
// v2.1.229 alignment: destructive git/gh flags are never auto-approved.
|
|
436
|
-
if (tool.name === 'Bash' && input && hasDangerousGitFlags(input)) {
|
|
437
|
-
return 'ask'
|
|
438
|
-
}
|
|
439
|
-
return 'bypass'
|
|
440
|
-
|
|
441
|
-
case 'dontAsk':
|
|
442
|
-
// Only allowlisted tools free (already handled above); everything else requires approval
|
|
443
|
-
return 'ask'
|
|
444
|
-
|
|
445
392
|
case 'bypassPermissions':
|
|
446
393
|
return 'bypass'
|
|
447
394
|
|
|
@@ -453,11 +400,10 @@ export class PermissionSystem {
|
|
|
453
400
|
// ── Legacy compatibility ──
|
|
454
401
|
|
|
455
402
|
setDefaultLevel(level: PermissionLevel): void {
|
|
456
|
-
// Map legacy 3-level to new mode
|
|
457
|
-
let
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
else newMode = 'default'
|
|
403
|
+
// Map legacy 3-level (auto/ask/bypass) to new 4-level mode.
|
|
404
|
+
// Legacy 'auto'/'ask' = "let each tool self-decide" → 'default'.
|
|
405
|
+
// Legacy 'bypass' → 'bypassPermissions'.
|
|
406
|
+
const newMode: PermissionMode = level === 'bypass' ? 'bypassPermissions' : 'default'
|
|
461
407
|
this.mode = clampMode(newMode, this.restrictions)
|
|
462
408
|
this.invalidateCache()
|
|
463
409
|
}
|
|
@@ -465,8 +411,7 @@ export class PermissionSystem {
|
|
|
465
411
|
getDefaultLevel(): PermissionLevel {
|
|
466
412
|
// Legacy constructor fallback takes priority
|
|
467
413
|
if (this.legacyDefaultFallback) return this.legacyDefaultFallback
|
|
468
|
-
if (this.mode === '
|
|
469
|
-
return 'bypass'
|
|
414
|
+
if (this.mode === 'bypassPermissions') return 'bypass'
|
|
470
415
|
if (this.mode === 'plan') return 'ask'
|
|
471
416
|
return 'auto'
|
|
472
417
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mipham Code — Project Scaffold
|
|
3
|
+
*
|
|
4
|
+
* `mipham init` 生成规范的项目文档(CLAUDE.md + MIPHAM.md + README.md),
|
|
5
|
+
* 避免用户「冷建立文件」。模板遵循 One Mipham Corporation 规范。
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs'
|
|
8
|
+
import { join, basename } from 'node:path'
|
|
9
|
+
import { execSync } from 'node:child_process'
|
|
10
|
+
|
|
11
|
+
export interface ScaffoldResult {
|
|
12
|
+
created: string[]
|
|
13
|
+
skipped: string[]
|
|
14
|
+
gitInitialized: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function renderMiphamMd(projectName: string): string {
|
|
18
|
+
return `---
|
|
19
|
+
model: mipham-code
|
|
20
|
+
version: 1.0.0
|
|
21
|
+
privacy: project
|
|
22
|
+
language: zh-CN
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# MIPHAM.md — ${projectName}
|
|
26
|
+
|
|
27
|
+
> 本文件定义 ${projectName} 项目中 AI 助手的交互人格和项目规范。
|
|
28
|
+
> 继承自 One Mipham Corporation 集团 MIPHAM.md。
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 项目概述
|
|
33
|
+
|
|
34
|
+
[简要描述项目目的和定位]
|
|
35
|
+
|
|
36
|
+
## 技术栈
|
|
37
|
+
|
|
38
|
+
[列出主要技术栈]
|
|
39
|
+
|
|
40
|
+
## 项目规范
|
|
41
|
+
|
|
42
|
+
- [添加项目特有的编码规则]
|
|
43
|
+
- [添加团队约定]
|
|
44
|
+
|
|
45
|
+
## AI 交互偏好
|
|
46
|
+
|
|
47
|
+
- 回复语言:[中文/英文]
|
|
48
|
+
- 代码风格:[偏好]
|
|
49
|
+
- 注释语言:[中文/英文]
|
|
50
|
+
`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function renderClaudeMd(projectName: string): string {
|
|
54
|
+
return `# CLAUDE.md
|
|
55
|
+
|
|
56
|
+
> 项目:${projectName}
|
|
57
|
+
> 版本:0.1.0
|
|
58
|
+
> 初始化:\`mipham init\`
|
|
59
|
+
|
|
60
|
+
## 项目概述
|
|
61
|
+
|
|
62
|
+
[简要描述项目目的和定位]
|
|
63
|
+
|
|
64
|
+
## 技术栈
|
|
65
|
+
|
|
66
|
+
[列出主要技术栈]
|
|
67
|
+
|
|
68
|
+
## 开发命令
|
|
69
|
+
|
|
70
|
+
- 构建:[\`填入 build 命令\`]
|
|
71
|
+
- 测试:[\`填入 test 命令\`]
|
|
72
|
+
- Lint:[\`填入 lint 命令\`]
|
|
73
|
+
|
|
74
|
+
## 项目规范
|
|
75
|
+
|
|
76
|
+
- [添加项目特有的编码规则]
|
|
77
|
+
- [添加团队约定]
|
|
78
|
+
|
|
79
|
+
## 关键约束
|
|
80
|
+
|
|
81
|
+
- 遵循上级 One Mipham Corporation CLAUDE.md 技术规范
|
|
82
|
+
- 提交信息遵循 Conventional Commits
|
|
83
|
+
- 禁止在代码、日志、配置或提交历史中硬编码凭据
|
|
84
|
+
`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function renderReadmeMd(projectName: string): string {
|
|
88
|
+
return `# ${projectName}
|
|
89
|
+
|
|
90
|
+
[一句话描述项目]
|
|
91
|
+
|
|
92
|
+
## 快速开始
|
|
93
|
+
|
|
94
|
+
\`\`\`bash
|
|
95
|
+
# 安装依赖 / 运行项目
|
|
96
|
+
\`\`\`
|
|
97
|
+
|
|
98
|
+
## 技术栈
|
|
99
|
+
|
|
100
|
+
[列出技术栈]
|
|
101
|
+
|
|
102
|
+
## 文档
|
|
103
|
+
|
|
104
|
+
- \`CLAUDE.md\` — AI 协作规范
|
|
105
|
+
- \`MIPHAM.md\` — AI 交互人格
|
|
106
|
+
- \`README.md\` — 项目说明(本文件)
|
|
107
|
+
|
|
108
|
+
## 许可
|
|
109
|
+
|
|
110
|
+
[填入许可证]
|
|
111
|
+
`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 检测目录是否为「愣建文件夹」——无任何条目(忽略 .DS_Store)。目录不存在则视为非空。 */
|
|
115
|
+
export function isEmptyProject(dir: string): boolean {
|
|
116
|
+
try {
|
|
117
|
+
const entries = readdirSync(dir).filter((e) => e !== '.DS_Store')
|
|
118
|
+
return entries.length === 0
|
|
119
|
+
} catch {
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function scaffoldProject(targetDir: string, opts?: { gitInit?: boolean }): ScaffoldResult {
|
|
125
|
+
mkdirSync(targetDir, { recursive: true })
|
|
126
|
+
|
|
127
|
+
const name = basename(targetDir) || 'my-project'
|
|
128
|
+
const files = [
|
|
129
|
+
{ path: 'CLAUDE.md', content: renderClaudeMd(name) },
|
|
130
|
+
{ path: 'MIPHAM.md', content: renderMiphamMd(name) },
|
|
131
|
+
{ path: 'README.md', content: renderReadmeMd(name) },
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
const created: string[] = []
|
|
135
|
+
const skipped: string[] = []
|
|
136
|
+
|
|
137
|
+
for (const f of files) {
|
|
138
|
+
const full = join(targetDir, f.path)
|
|
139
|
+
if (existsSync(full)) {
|
|
140
|
+
skipped.push(f.path)
|
|
141
|
+
} else {
|
|
142
|
+
writeFileSync(full, f.content, 'utf-8')
|
|
143
|
+
created.push(f.path)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let gitInitialized = false
|
|
148
|
+
if (opts?.gitInit && !existsSync(join(targetDir, '.git'))) {
|
|
149
|
+
try {
|
|
150
|
+
execSync('git init', { cwd: targetDir, stdio: 'ignore' })
|
|
151
|
+
gitInitialized = true
|
|
152
|
+
} catch {
|
|
153
|
+
// git 不可用或失败 — 不阻断脚手架
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { created, skipped, gitInitialized }
|
|
158
|
+
}
|
package/src/daemon/server.ts
CHANGED
|
@@ -757,8 +757,6 @@
|
|
|
757
757
|
"manual": "manual mode",
|
|
758
758
|
"accept_edits": "accept edits on",
|
|
759
759
|
"plan_mode": "plan mode / read-only",
|
|
760
|
-
"auto": "auto mode",
|
|
761
|
-
"dont_ask": "don't ask",
|
|
762
760
|
"bypass": "bypass"
|
|
763
761
|
},
|
|
764
762
|
"loading": {
|
|
@@ -804,8 +802,7 @@
|
|
|
804
802
|
"assistant": { "role_label": "Mipham Code" },
|
|
805
803
|
"user": { "role_label": "User" },
|
|
806
804
|
"input": {
|
|
807
|
-
"placeholder": "Type a message (Esc to
|
|
808
|
-
"vim_help": "[NORMAL] h/j/k/l w/b 0/$ dd yy p u / (Esc to insert)"
|
|
805
|
+
"placeholder": "Type a message (Esc to clear)..."
|
|
809
806
|
},
|
|
810
807
|
"slash_hints": {
|
|
811
808
|
"label": "Commands:",
|
|
@@ -942,7 +939,7 @@
|
|
|
942
939
|
"mcp": { "name": "MCP", "description": "Manage MCP server connections" }
|
|
943
940
|
},
|
|
944
941
|
"errors": {
|
|
945
|
-
"tool_not_allowed": "Tool \"{name}\" requires user approval (permission: ask). The tool was not executed.",
|
|
942
|
+
"tool_not_allowed": "Tool \"{name}\" requires user approval (permission: ask). The tool was not executed. Press Shift+Tab to switch permission mode, or run /permissions.",
|
|
946
943
|
"tool_blocked": "Tool \"{name}\" blocked by hook",
|
|
947
944
|
"user_input_blocked": "User input blocked by hook.",
|
|
948
945
|
"dlp_blocked": "Request blocked by DLP policy.",
|
|
@@ -963,6 +960,7 @@
|
|
|
963
960
|
"mcp_register_failed": "[mcp] Failed to register tool \"{tool}\" from server \"{server}\": {error}",
|
|
964
961
|
"max_tool_turns": "Max tool-calling turns ({max}) reached. Some tool calls were not executed.",
|
|
965
962
|
"max_tool_turns_warning": "You've reached the maximum of {max} tool-calling rounds. {pending} tool call(s) were not executed. Please summarize what you found so far and any next steps the user should take.",
|
|
963
|
+
"turn_timeout": "Task produced no substantial output for {minutes} minutes and was stopped. Break the task into smaller steps or clarify the next step, then retry.",
|
|
966
964
|
"mcp_not_connected": "Error: MCP server \"{server}\" not connected",
|
|
967
965
|
"mcp_tool_error": "MCP tool error: {error}",
|
|
968
966
|
"mcp_no_connection": "No connection for \"{name}\""
|