@miphamai/cli 0.83.0 → 0.85.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 +20 -5
- package/package.json +1 -1
- package/skills/standard/mipham-code-setup.SKILL.md +30 -8
- package/src/agent/agent-context.ts +5 -5
- package/src/agent/agent-experience.ts +2 -2
- package/src/agent/agent-registry.ts +4 -4
- package/src/agent/cross-session/discovery.ts +3 -2
- package/src/agent/cross-session/file-inbox.ts +2 -2
- package/src/agent/effectiveness-tracker.ts +2 -2
- package/src/agent/pattern-analyzer.ts +3 -4
- package/src/agent/sub-agent.ts +12 -2
- package/src/agent/types.ts +4 -1
- package/src/commands/autoloop-journal.ts +2 -2
- package/src/commands/environment.ts +2 -1
- package/src/commands/loop-scaffold.ts +2 -1
- package/src/commands/project.ts +86 -33
- package/src/config/keys-manager.ts +2 -2
- package/src/config/loader.ts +11 -10
- package/src/config/preferences.ts +3 -4
- package/src/core/auto-memory.ts +2 -3
- package/src/core/constitution-loader.ts +3 -4
- package/src/core/crsi-producer.ts +3 -3
- package/src/core/crsi-sandbox.ts +3 -2
- package/src/core/dream-engine.ts +2 -2
- package/src/core/engine.ts +38 -6
- package/src/core/error-signature-db.ts +2 -2
- package/src/core/eval-harness.ts +4 -3
- package/src/core/improvement-track.ts +5 -6
- package/src/core/instructions.ts +28 -4
- package/src/core/memory/memory-loader.ts +2 -3
- package/src/core/paths.ts +21 -1
- package/src/core/permission-audit.ts +120 -0
- package/src/core/permission-classifier.ts +449 -0
- package/src/core/permission-config.ts +106 -15
- package/src/core/permission.ts +369 -16
- package/src/core/rule-engine.ts +2 -2
- package/src/core/rules-loader.ts +3 -2
- package/src/core/session-log.ts +2 -3
- package/src/core/session-store.ts +2 -3
- package/src/core/workspace-trust.ts +4 -3
- package/src/daemon/database.ts +2 -2
- package/src/daemon/index.ts +2 -3
- package/src/daemon/launch.ts +3 -3
- package/src/daemon/server.ts +15 -0
- package/src/i18n-core/locales/en-US.json +7 -1
- package/src/i18n-core/locales/zh-CN.json +7 -1
- package/src/index.tsx +30 -6
- package/src/mcp/token-store.ts +2 -2
- package/src/plugin/plugin-manager.ts +2 -2
- package/src/shared/constants.ts +0 -1
- package/src/shared/package-info.ts +1 -1
- package/src/shared/types.ts +46 -6
- package/src/shared/update.ts +255 -20
- package/src/skills/bundled-skills.ts +1 -1
- package/src/skills/loader.ts +2 -3
- package/src/skills/marketplace.ts +2 -3
- package/src/skills/registry.ts +2 -2
- package/src/skills/skill-assets.ts +2 -2
- package/src/skills/usage.ts +2 -2
- package/src/telemetry/consent.ts +2 -3
- package/src/tools/agent/enter-plan.ts +3 -2
- package/src/tools/agent/exit-plan.ts +1 -1
- package/src/tools/agent/list-agents.ts +1 -1
- package/src/tools/agent/memory.ts +3 -3
- package/src/tools/agent/plan.ts +3 -2
- package/src/tools/agent/report-findings.ts +1 -1
- package/src/tools/agent/send-message.ts +1 -1
- package/src/tools/agent/skill.ts +1 -1
- package/src/tools/exec/git.ts +2 -2
- package/src/tools/exec/task.ts +1 -1
- package/src/tools/file/glob.ts +1 -1
- package/src/tools/file/grep.ts +1 -1
- package/src/tools/file/read.ts +1 -1
- package/src/tools/network/web-fetch.ts +1 -1
- package/src/tools/network/web-search.ts +1 -1
- package/src/tools/scheduling/cron.ts +5 -5
- package/src/tools/scheduling/schedule-wakeup.ts +1 -1
- package/src/tools/system/config.ts +2 -2
- package/src/tools/system/tool-search.ts +1 -1
- package/src/ui/app.tsx +51 -9
- package/src/ui/commands.ts +26 -21
- package/src/ui/config-wizard.tsx +2 -2
- package/src/ui/input.tsx +15 -3
- package/src/workflow/journal.ts +2 -2
package/bin/mipham.ts
CHANGED
|
@@ -289,10 +289,18 @@ async function runUpdate(): Promise<boolean> {
|
|
|
289
289
|
console.log()
|
|
290
290
|
console.log(`Updating ${PACKAGE} to v${latestVersion}...`)
|
|
291
291
|
|
|
292
|
-
const
|
|
293
|
-
if (!ok) {
|
|
292
|
+
const result = performUpdate(latestVersion, workingRegistry)
|
|
293
|
+
if (!result.ok) {
|
|
294
294
|
console.log()
|
|
295
|
-
console.log(`✗ Update failed
|
|
295
|
+
console.log(`✗ Update failed: ${result.reason ?? 'unknown error'}`)
|
|
296
|
+
if (result.rolledBack) {
|
|
297
|
+
console.log(
|
|
298
|
+
` Your previous install (v${currentVersion}) has been restored — mipham still works.`,
|
|
299
|
+
)
|
|
300
|
+
} else {
|
|
301
|
+
console.log(' ⚠ The previous install could not be restored.')
|
|
302
|
+
console.log(` Reinstall with: npm install -g ${PACKAGE}@${currentVersion}`)
|
|
303
|
+
}
|
|
296
304
|
if (backupPath && existsSync(backupPath)) {
|
|
297
305
|
console.log(` Your config backup is at: ${backupPath}`)
|
|
298
306
|
}
|
|
@@ -313,8 +321,15 @@ async function runUpdate(): Promise<boolean> {
|
|
|
313
321
|
}
|
|
314
322
|
|
|
315
323
|
console.log()
|
|
316
|
-
|
|
317
|
-
|
|
324
|
+
if (result.verified) {
|
|
325
|
+
// 自证已经跑过(包版本 + launcher 实跑),不再把验证推给用户。
|
|
326
|
+
console.log(`✓ Updated to ${PACKAGE} v${latestVersion} — verified.`)
|
|
327
|
+
} else {
|
|
328
|
+
console.log(`⚠ Installed ${PACKAGE} v${latestVersion}, but could not verify it.`)
|
|
329
|
+
console.log(` ${result.reason ?? ''}`)
|
|
330
|
+
console.log(` Please check: mipham --version`)
|
|
331
|
+
process.exit(1)
|
|
332
|
+
}
|
|
318
333
|
process.exit(0)
|
|
319
334
|
}
|
|
320
335
|
|
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/bypassPermissions)
|
|
80
|
+
Permissions: [mode] (default/acceptEdits/plan/auto/bypassPermissions)
|
|
81
81
|
Trust: [✅/⬜] workspace trusted
|
|
82
82
|
```
|
|
83
83
|
|
|
@@ -331,21 +331,43 @@ Or directly:
|
|
|
331
331
|
|
|
332
332
|
### 6.1 — Permission Modes
|
|
333
333
|
|
|
334
|
-
| Mode | Behavior
|
|
335
|
-
| ------------------- |
|
|
336
|
-
| `default` |
|
|
337
|
-
| `acceptEdits` | Auto-allow edits,
|
|
338
|
-
| `plan` | Plan-only, no tool execution
|
|
339
|
-
| `
|
|
334
|
+
| Mode | Behavior | Use Case |
|
|
335
|
+
| ------------------- | -------------------------------------- | ----------------------------------------- |
|
|
336
|
+
| `default` | Ask-first tools are refused (see note) | Normal development (recommended) |
|
|
337
|
+
| `acceptEdits` | Auto-allow edits, ask for other tools | Active coding sessions |
|
|
338
|
+
| `plan` | Plan-only, no tool execution | Design & architecture work |
|
|
339
|
+
| `auto` | A classifier rules on every call | Long unattended runs you still want gated |
|
|
340
|
+
| `bypassPermissions` | Skip all checks | ⚠️ Only for fully trusted codebases |
|
|
341
|
+
|
|
342
|
+
**Note on `default`**: Mipham Code has no interactive approval prompt, so "ask"
|
|
343
|
+
means the call is **refused** with a message naming the mode — it is not queued
|
|
344
|
+
for your answer. That makes `default` the strictest _usable_ mode for Bash and
|
|
345
|
+
file writes; `auto` is the mode that lets gated calls proceed without a human,
|
|
346
|
+
by having a classifier rule on each one.
|
|
347
|
+
|
|
348
|
+
**What `auto` actually gates.** Reads (Read/Grep/Glob) are never sent to the
|
|
349
|
+
classifier — gating them would make `auto` the only mode in the ladder that cannot
|
|
350
|
+
open a file without a round-trip to another model. Everything else that the static
|
|
351
|
+
chain would refuse goes to the classifier, which can only **lift** a refusal: a
|
|
352
|
+
deny rule or an `ask` rule is never overridden. If the classifier cannot be reached
|
|
353
|
+
or answers unusably, the call is refused (fail-closed) with a message saying the
|
|
354
|
+
refusal is _not_ a policy decision and can be retried.
|
|
340
355
|
|
|
341
356
|
### 6.2 — Configure
|
|
342
357
|
|
|
343
|
-
|
|
358
|
+
Press **Shift+Tab** to change the mode live; it cycles
|
|
359
|
+
`default → acceptEdits → plan → auto` and lasts for the session only.
|
|
360
|
+
`bypassPermissions` is a legal mode but is deliberately **not** on the wheel —
|
|
361
|
+
it is reached by naming it in config, where the user has said what they mean.
|
|
362
|
+
|
|
363
|
+
To persist a mode for a project, in `.mipham/config.yml`:
|
|
344
364
|
|
|
345
365
|
```yaml
|
|
346
366
|
permission: default
|
|
347
367
|
```
|
|
348
368
|
|
|
369
|
+
Any mode name from the table above is accepted, including `bypassPermissions`.
|
|
370
|
+
|
|
349
371
|
Or via slash command:
|
|
350
372
|
|
|
351
373
|
```
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// apps/cli/src/agent/agent-context.ts
|
|
2
2
|
import { readdirSync, readFileSync, existsSync } from 'node:fs'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
|
-
import { homedir } from 'node:os'
|
|
5
4
|
import { ContextManager } from '../core/context'
|
|
6
5
|
import type { ToolDefinition } from '../shared/index.ts'
|
|
7
6
|
import type { AgentDefinition } from './types'
|
|
8
7
|
import type { CrsiConfig } from '../shared/index.ts'
|
|
9
8
|
import { AgentExperience } from './agent-experience'
|
|
10
9
|
import { ExperienceRuleExtractor } from './experience-rules.js'
|
|
10
|
+
import { miphamHome } from '../core/paths.ts'
|
|
11
|
+
import { MIPHAM_DIR } from '../shared/constants.ts'
|
|
11
12
|
|
|
12
13
|
export interface AgentContextResult {
|
|
13
14
|
context: ContextManager
|
|
@@ -26,17 +27,16 @@ function loadAgentMemory(
|
|
|
26
27
|
crsiRuleInjection = true,
|
|
27
28
|
): string {
|
|
28
29
|
let memoryDir: string
|
|
29
|
-
const home = homedir()
|
|
30
30
|
|
|
31
31
|
switch (scope) {
|
|
32
32
|
case 'user':
|
|
33
|
-
memoryDir =
|
|
33
|
+
memoryDir = miphamHome('agent-memory', agentName)
|
|
34
34
|
break
|
|
35
35
|
case 'project':
|
|
36
|
-
memoryDir = join(process.cwd(),
|
|
36
|
+
memoryDir = join(process.cwd(), MIPHAM_DIR, 'agent-memory', agentName)
|
|
37
37
|
break
|
|
38
38
|
case 'local':
|
|
39
|
-
memoryDir = join(process.cwd(),
|
|
39
|
+
memoryDir = join(process.cwd(), MIPHAM_DIR, 'agent-memory-local', agentName)
|
|
40
40
|
break
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import { homedir } from 'node:os'
|
|
4
3
|
import { ExperienceRuleExtractor, type ExperienceRule } from './experience-rules.js'
|
|
4
|
+
import { miphamHome } from '../core/paths.ts'
|
|
5
5
|
|
|
6
6
|
const MAX_EXPERIENCES = 20
|
|
7
7
|
|
|
@@ -11,7 +11,7 @@ export class AgentExperience {
|
|
|
11
11
|
|
|
12
12
|
constructor(
|
|
13
13
|
private readonly agentName: string,
|
|
14
|
-
baseDir: string =
|
|
14
|
+
baseDir: string = miphamHome('agent-memory'),
|
|
15
15
|
) {
|
|
16
16
|
this.expDir = join(baseDir, agentName)
|
|
17
17
|
this.expFile = join(this.expDir, 'experience.md')
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
|
2
|
-
import { homedir } from 'node:os'
|
|
3
2
|
import { join, extname } from 'node:path'
|
|
4
3
|
import { parse as parseYaml } from 'yaml'
|
|
5
4
|
import type { AgentDefinition, SubAgentType } from './types'
|
|
5
|
+
import { miphamHome } from '../core/paths.ts'
|
|
6
|
+
import { MIPHAM_DIR } from '../shared/constants.ts'
|
|
6
7
|
|
|
7
8
|
interface FrontmatterResult {
|
|
8
9
|
data: Record<string, unknown>
|
|
@@ -91,13 +92,12 @@ export class AgentRegistry {
|
|
|
91
92
|
|
|
92
93
|
/** Load project-level agents from .mipham/agents/ */
|
|
93
94
|
loadProjectAgents(cwd: string): void {
|
|
94
|
-
this.loadDirectory(join(cwd,
|
|
95
|
+
this.loadDirectory(join(cwd, MIPHAM_DIR, 'agents'), 'project')
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
/** Load user-level agents from ~/.mipham/agents/ */
|
|
98
99
|
loadUserAgents(): void {
|
|
99
|
-
|
|
100
|
-
this.loadDirectory(join(home, '.mipham', 'agents'), 'user')
|
|
100
|
+
this.loadDirectory(miphamHome('agents'), 'user')
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
/** Get a custom agent by name. Returns undefined for builtins. */
|
|
@@ -8,10 +8,11 @@ import {
|
|
|
8
8
|
statSync,
|
|
9
9
|
} from 'node:fs'
|
|
10
10
|
import { join, basename } from 'node:path'
|
|
11
|
-
import {
|
|
11
|
+
import { hostname } from 'node:os'
|
|
12
12
|
import type { SessionInfo, CrossSessionInbound } from '../../shared/types'
|
|
13
|
+
import { miphamHome } from '../../core/paths.ts'
|
|
13
14
|
|
|
14
|
-
const MIPHAM_HOME =
|
|
15
|
+
const MIPHAM_HOME = miphamHome()
|
|
15
16
|
const ACTIVE_SESSIONS_DIR = join(MIPHAM_HOME, '.active-sessions')
|
|
16
17
|
|
|
17
18
|
/** A session whose heartbeat file hasn't been touched in this long is dead. */
|
|
@@ -9,12 +9,11 @@ import {
|
|
|
9
9
|
lstatSync,
|
|
10
10
|
} from 'node:fs'
|
|
11
11
|
import { join } from 'node:path'
|
|
12
|
-
import { homedir } from 'node:os'
|
|
13
12
|
import type { CrossSessionTransport } from './transport'
|
|
14
13
|
import type { AgentMessage } from '../message-bus'
|
|
15
14
|
import type { SessionInfo } from '../../shared/types'
|
|
16
15
|
|
|
17
|
-
const MIPHAM_HOME =
|
|
16
|
+
const MIPHAM_HOME = miphamHome()
|
|
18
17
|
const INBOX_DIR = join(MIPHAM_HOME, 'inbox')
|
|
19
18
|
|
|
20
19
|
/** True if a path exists and is a symbolic link (symlink-attack guard). */
|
|
@@ -138,6 +137,7 @@ export class FileInboxTransport implements CrossSessionTransport {
|
|
|
138
137
|
|
|
139
138
|
// Import discoverSessions lazily to avoid circular dependency
|
|
140
139
|
import { discoverSessions } from './discovery'
|
|
140
|
+
import { miphamHome } from '../../core/paths.ts'
|
|
141
141
|
|
|
142
142
|
// ── Singleton ──
|
|
143
143
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
2
2
|
import { join, dirname } from 'node:path'
|
|
3
|
-
import { homedir } from 'node:os'
|
|
4
3
|
import type { CrsiProvenanceBridge, CrsiVerdict } from './crsi-provenance-bridge.js'
|
|
5
4
|
import { isRecoverableToolFailure } from './recoverable-failure.js'
|
|
5
|
+
import { miphamHome } from '../core/paths.ts'
|
|
6
6
|
|
|
7
7
|
export interface RuleEffectiveness {
|
|
8
8
|
ruleId: string
|
|
@@ -32,7 +32,7 @@ export class EffectivenessTracker {
|
|
|
32
32
|
private storePath: string
|
|
33
33
|
private provenanceBridge?: CrsiProvenanceBridge
|
|
34
34
|
|
|
35
|
-
constructor(storeDir: string =
|
|
35
|
+
constructor(storeDir: string = miphamHome('rule-engine')) {
|
|
36
36
|
this.storePath = join(storeDir, STORE_FILE)
|
|
37
37
|
this.data = new Map()
|
|
38
38
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { readdirSync, existsSync } from 'node:fs'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { homedir } from 'node:os'
|
|
4
2
|
import { AgentExperience } from './agent-experience.js'
|
|
5
3
|
import { parseFailureEntries, categorize, type ExperienceRule } from './experience-rules.js'
|
|
6
4
|
import type { ToolRule } from '../core/rule-engine.js'
|
|
5
|
+
import { miphamHome } from '../core/paths.ts'
|
|
7
6
|
|
|
8
7
|
export interface Pattern {
|
|
9
8
|
id: string
|
|
@@ -18,7 +17,7 @@ export interface Pattern {
|
|
|
18
17
|
|
|
19
18
|
export class PatternAnalyzer {
|
|
20
19
|
analyzeAgent(agentName: string, baseDir?: string): Pattern[] {
|
|
21
|
-
const dir = baseDir ||
|
|
20
|
+
const dir = baseDir || miphamHome('agent-memory')
|
|
22
21
|
const exp = new AgentExperience(agentName, dir)
|
|
23
22
|
const content = exp.getExperience()
|
|
24
23
|
if (!content) return []
|
|
@@ -55,7 +54,7 @@ export class PatternAnalyzer {
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
analyzeAllAgents(baseDir?: string): Pattern[] {
|
|
58
|
-
const dir = baseDir ||
|
|
57
|
+
const dir = baseDir || miphamHome('agent-memory')
|
|
59
58
|
if (!existsSync(dir)) return []
|
|
60
59
|
|
|
61
60
|
let agents: string[]
|
package/src/agent/sub-agent.ts
CHANGED
|
@@ -498,12 +498,22 @@ export class SubAgent {
|
|
|
498
498
|
// The gate is never absent: when nobody handed in a permission system, `gate` is
|
|
499
499
|
// the CLI's default mode (see above), so this check always runs.
|
|
500
500
|
// P0-3: Uses isolated subPermission (clamped by org restrictions) instead of parent's.
|
|
501
|
-
|
|
501
|
+
//
|
|
502
|
+
// `resolveApproval` replaces `needsApproval` here for the same reason as in
|
|
503
|
+
// `engine.ts`: in `auto` mode the static chain can only say `'ask'`, and the
|
|
504
|
+
// classifier is what turns that into a running call. `gate` only carries a
|
|
505
|
+
// classifier when the sub-agent's resolved mode is `auto` (see
|
|
506
|
+
// `createSubAgentPermission`), so the default sub-agent is unchanged.
|
|
507
|
+
const decision = await gate.resolveApproval(tool, effectiveInput, { signal })
|
|
508
|
+
if (decision.level === 'ask') {
|
|
502
509
|
currentMessages.push({
|
|
503
510
|
role: 'user' as const,
|
|
504
511
|
content:
|
|
505
512
|
`Tool "${tu.name}" requires user approval (permission: ask). ` +
|
|
506
|
-
`Cannot execute in non-interactive sub-agent context
|
|
513
|
+
`Cannot execute in non-interactive sub-agent context.` +
|
|
514
|
+
(decision.source === 'classifier' && decision.classifierReason
|
|
515
|
+
? ` Classifier: ${decision.classifierReason}`
|
|
516
|
+
: ''),
|
|
507
517
|
})
|
|
508
518
|
continue
|
|
509
519
|
}
|
package/src/agent/types.ts
CHANGED
|
@@ -10,7 +10,10 @@ 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
|
-
|
|
13
|
+
// Hand-written union rather than `PermissionMode`: it also admits the legacy
|
|
14
|
+
// `'bypass'` alias that `resolveAgentMode` normalizes. Adding a mode to
|
|
15
|
+
// `PermissionMode` does **not** fail to compile here — keep the two in step.
|
|
16
|
+
permissionMode?: 'default' | 'acceptEdits' | 'bypass' | 'plan' | 'auto' | 'bypassPermissions'
|
|
14
17
|
maxTurns?: number
|
|
15
18
|
skills?: string
|
|
16
19
|
background?: boolean
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync, readdirSync, existsSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import {
|
|
3
|
+
import { miphamHome } from '../core/paths.ts'
|
|
4
4
|
|
|
5
|
-
const AUTOLOOP_DIR =
|
|
5
|
+
const AUTOLOOP_DIR = miphamHome('autoloop')
|
|
6
6
|
|
|
7
7
|
function ensureDir(): void {
|
|
8
8
|
if (!existsSync(AUTOLOOP_DIR)) mkdirSync(AUTOLOOP_DIR, { recursive: true })
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { CommandHandler } from '../ui/commands.js'
|
|
8
8
|
import { NPM_INSTALL_COMMAND } from '../shared/index.ts'
|
|
9
|
+
import { miphamHome } from '../core/paths.ts'
|
|
9
10
|
|
|
10
11
|
export { themeCmd, releaseNotesCmd, ideCmd, terminalSetupCmd }
|
|
11
12
|
|
|
@@ -186,7 +187,7 @@ const terminalSetupCmd: CommandHandler = async () => {
|
|
|
186
187
|
const lines: string[] = ['── Terminal Setup ──', '']
|
|
187
188
|
|
|
188
189
|
// ── 1. Generate standalone shell setup script ──
|
|
189
|
-
const miphamDir =
|
|
190
|
+
const miphamDir = miphamHome()
|
|
190
191
|
mkdirSync(miphamDir, { recursive: true })
|
|
191
192
|
|
|
192
193
|
const shellScript = join(miphamDir, 'shell-setup.sh')
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { mkdirSync, writeFileSync, existsSync, chmodSync } from 'node:fs'
|
|
13
13
|
import { join, resolve } from 'node:path'
|
|
14
14
|
import { homedir } from 'node:os'
|
|
15
|
+
import { MIPHAM_DIR } from '../shared/constants.ts'
|
|
15
16
|
|
|
16
17
|
export interface ScaffoldResult {
|
|
17
18
|
created: string[]
|
|
@@ -89,7 +90,7 @@ export function scaffoldLoopKit(basePath: string): ScaffoldResult {
|
|
|
89
90
|
const skipped: string[] = []
|
|
90
91
|
|
|
91
92
|
const resolved = resolve(basePath.replace(/^~/, homedir()))
|
|
92
|
-
const miphamDir = join(resolved,
|
|
93
|
+
const miphamDir = join(resolved, MIPHAM_DIR)
|
|
93
94
|
|
|
94
95
|
// ── .mipham/ root ──
|
|
95
96
|
ensureDir(miphamDir, created, skipped)
|
package/src/commands/project.ts
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
* /security, /audit, /prompt-audit
|
|
7
7
|
*/
|
|
8
8
|
import type { CommandHandler, CommandContext, CommandResult } from '../ui/commands.js'
|
|
9
|
+
import type { PermissionMode } from '../shared/index.ts'
|
|
9
10
|
import { getWorkspaceTrust } from '../core/workspace-trust'
|
|
10
11
|
import { atomicWriteFileSync } from '../shared/atomic-write'
|
|
11
12
|
import { CLOUD_PROVIDERS } from '../config/wizard-config'
|
|
13
|
+
import { MODE_CYCLE, PERMISSION_MODE_HIERARCHY } from '../core/permission-config'
|
|
12
14
|
import { homedir } from 'node:os'
|
|
15
|
+
import { miphamHome } from '../core/paths.ts'
|
|
16
|
+
import { MIPHAM_DIR } from '../shared/constants.ts'
|
|
13
17
|
|
|
14
18
|
export {
|
|
15
19
|
initCmd,
|
|
@@ -22,6 +26,58 @@ export {
|
|
|
22
26
|
trustCmd,
|
|
23
27
|
}
|
|
24
28
|
|
|
29
|
+
/**
|
|
30
|
+
* One description per mode — exhaustive by type, so adding a member to
|
|
31
|
+
* `PermissionMode` fails to compile until it is described here.
|
|
32
|
+
*
|
|
33
|
+
* `/permissions` and `/setup 5` both render from this single table. Each used to
|
|
34
|
+
* carry its own hand-written list, and they had already drifted: `/setup 5` was
|
|
35
|
+
* still listing the retired 3-level `auto`/`ask`/`bypass` spellings and telling
|
|
36
|
+
* the user to run `/config permission <level>` — a command that has never existed
|
|
37
|
+
* (`/config` takes no arguments). Descriptions state what the mode does **today**.
|
|
38
|
+
*/
|
|
39
|
+
const MODE_DESCRIPTIONS: Record<PermissionMode, string> = {
|
|
40
|
+
plan: 'reads only (Read/Grep/Glob); nothing writes or runs',
|
|
41
|
+
default: 'per-tool defaults (Bash/Write/Edit are refused, not queued)',
|
|
42
|
+
acceptEdits: 'reads + edits free; Bash auto-runs read/check commands',
|
|
43
|
+
auto: 'a classifier rules on each call that would otherwise be refused (reads are never gated)',
|
|
44
|
+
bypassPermissions: 'skip ALL permission checks (⚠️ use with caution)',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The mode table, in `PERMISSION_MODE_HIERARCHY` order — i.e. least → most
|
|
49
|
+
* permissive, which is what the heading above it claims. The order comes from the
|
|
50
|
+
* hierarchy array, never from this file, so it cannot disagree with the ranking
|
|
51
|
+
* `clampMode` actually walks.
|
|
52
|
+
*/
|
|
53
|
+
function renderModeTable(): string {
|
|
54
|
+
return PERMISSION_MODE_HIERARCHY.map((m) => ` ${m.padEnd(19)}— ${MODE_DESCRIPTIONS[m]}`).join(
|
|
55
|
+
'\n',
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* What Shift+Tab reaches, in the order the wheel visits it — derived from
|
|
61
|
+
* `MODE_CYCLE` rather than hand-written.
|
|
62
|
+
*
|
|
63
|
+
* Deliberately its **own line**, not the table above: the table is the
|
|
64
|
+
* permissiveness ladder (every legal mode, in `PERMISSION_MODE_HIERARCHY` order)
|
|
65
|
+
* while this is the cycle, and those two are *allowed* to differ — that is the
|
|
66
|
+
* entire reason `ALL_MODES` and `MODE_CYCLE` are separate arrays. Rendering one
|
|
67
|
+
* list as if it were the other is what the old copy did, and it is how a help
|
|
68
|
+
* screen comes to advertise a mode the wheel cannot reach.
|
|
69
|
+
*
|
|
70
|
+
* `src/ui/app.tsx` used to keep its own copy of this order (`PERMISSION_MODES`).
|
|
71
|
+
* The two happened to agree on the day each was written — which is the state that
|
|
72
|
+
* reads as safe and is not: the copy is what would have kept the footer saying
|
|
73
|
+
* `bypassPermissions` after the wheel moved to `auto`. It now imports `MODE_CYCLE`
|
|
74
|
+
* like this file does, so the help line and the footer are one list by
|
|
75
|
+
* construction.
|
|
76
|
+
*/
|
|
77
|
+
function renderCycleLine(): string {
|
|
78
|
+
return MODE_CYCLE.join(' → ')
|
|
79
|
+
}
|
|
80
|
+
|
|
25
81
|
/**
|
|
26
82
|
* provider id → 环境变量名,例如 `minimax-global` → `MINIMAX_GLOBAL_API_KEY`。
|
|
27
83
|
*
|
|
@@ -36,15 +92,12 @@ function envVarFor(providerId: string): string {
|
|
|
36
92
|
|
|
37
93
|
const initCmd: CommandHandler = async (ctx) => {
|
|
38
94
|
const { existsSync, mkdirSync } = await import('node:fs')
|
|
39
|
-
const { join } = await import('node:path')
|
|
40
|
-
const { homedir } = await import('node:os')
|
|
41
95
|
|
|
42
|
-
const
|
|
43
|
-
const userConfigPath = join(home, '.mipham', 'config.yml')
|
|
96
|
+
const userConfigPath = miphamHome('config.yml')
|
|
44
97
|
|
|
45
98
|
// Generate user-friendly config if it doesn't exist yet
|
|
46
99
|
if (!existsSync(userConfigPath)) {
|
|
47
|
-
mkdirSync(
|
|
100
|
+
mkdirSync(miphamHome(), { recursive: true })
|
|
48
101
|
|
|
49
102
|
// 生成物必须来自随包发布的清单,不能来自 ctx.config —— 后者的 status 表达
|
|
50
103
|
// 的是「用户停用了哪家」这个**运行时**状态,而 12 家里只有 mipham 声明了它
|
|
@@ -89,7 +142,7 @@ const initCmd: CommandHandler = async (ctx) => {
|
|
|
89
142
|
# ── Defaults ──
|
|
90
143
|
defaultProvider: ${ctx.providerId}
|
|
91
144
|
defaultModel: ${ctx.modelId}
|
|
92
|
-
permission:
|
|
145
|
+
permission: default
|
|
93
146
|
|
|
94
147
|
# ── Providers (${presetProviders.length} pre-configured — just add your API keys) ──
|
|
95
148
|
providers:
|
|
@@ -186,11 +239,12 @@ Mode: ${ctx.engine.getPermission().getMode()}
|
|
|
186
239
|
Messages: ${msgs.length} in context
|
|
187
240
|
Tools: ${ctx.engine.getTools().size} available
|
|
188
241
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
242
|
+
Shift+Tab cycles: ${renderCycleLine()} (this session only).
|
|
243
|
+
To persist a mode, set \`permission:\` in .mipham/config.yml — every mode below is
|
|
244
|
+
accepted there.
|
|
245
|
+
|
|
246
|
+
Modes (least → most permissive):
|
|
247
|
+
${renderModeTable()}
|
|
194
248
|
|
|
195
249
|
To let Bash run without asking: press Shift+Tab until the status line shows
|
|
196
250
|
"acceptEdits", then send your message again.
|
|
@@ -338,7 +392,7 @@ const recommendCmd: CommandHandler = async (ctx) => {
|
|
|
338
392
|
lines.push('── Configuration Tips ──')
|
|
339
393
|
lines.push('')
|
|
340
394
|
|
|
341
|
-
const hasProjectMipham = existsSync(join(cwd,
|
|
395
|
+
const hasProjectMipham = existsSync(join(cwd, MIPHAM_DIR))
|
|
342
396
|
if (!hasProjectMipham) {
|
|
343
397
|
lines.push(' /setup 1 Initialize .mipham/ + MIPHAM.md + config.yml')
|
|
344
398
|
}
|
|
@@ -391,12 +445,11 @@ const setupCmd: CommandHandler = async (ctx, args) => {
|
|
|
391
445
|
const { existsSync } = await import('node:fs')
|
|
392
446
|
const { join } = await import('node:path')
|
|
393
447
|
const cwd = process.cwd()
|
|
394
|
-
const home = homedir()
|
|
395
448
|
|
|
396
449
|
const hasProjectMipham = existsSync(join(cwd, 'MIPHAM.md'))
|
|
397
|
-
const hasProjectConfig = existsSync(join(cwd,
|
|
398
|
-
const hasUserConfig = existsSync(
|
|
399
|
-
const hasMiphamDir = existsSync(join(cwd,
|
|
450
|
+
const hasProjectConfig = existsSync(join(cwd, MIPHAM_DIR, 'config.yml'))
|
|
451
|
+
const hasUserConfig = existsSync(miphamHome('config.yml'))
|
|
452
|
+
const hasMiphamDir = existsSync(join(cwd, MIPHAM_DIR))
|
|
400
453
|
|
|
401
454
|
const activeProviders = ctx.config.providers.filter((p) => p.status === 'active').length
|
|
402
455
|
const totalProviders = ctx.config.providers.length
|
|
@@ -444,7 +497,7 @@ async function setupStep1(ctx: CommandContext): Promise<CommandResult> {
|
|
|
444
497
|
const { join } = await import('node:path')
|
|
445
498
|
const cwd = process.cwd()
|
|
446
499
|
|
|
447
|
-
const miphamDir = join(cwd,
|
|
500
|
+
const miphamDir = join(cwd, MIPHAM_DIR)
|
|
448
501
|
const miphamPath = join(cwd, 'MIPHAM.md')
|
|
449
502
|
const configPath = join(miphamDir, 'config.yml')
|
|
450
503
|
|
|
@@ -511,7 +564,7 @@ async function setupStep1(ctx: CommandContext): Promise<CommandResult> {
|
|
|
511
564
|
# ── Defaults ──
|
|
512
565
|
defaultProvider: ${ctx.providerId}
|
|
513
566
|
defaultModel: ${ctx.modelId}
|
|
514
|
-
permission:
|
|
567
|
+
permission: default
|
|
515
568
|
|
|
516
569
|
# ── Providers (${presetProviders.length} pre-configured — just add your API keys) ──
|
|
517
570
|
providers:
|
|
@@ -687,14 +740,19 @@ async function setupStep5(ctx: CommandContext): Promise<CommandResult> {
|
|
|
687
740
|
return {
|
|
688
741
|
content: `── Step 5: Permissions Setup ──
|
|
689
742
|
|
|
690
|
-
Current mode:
|
|
743
|
+
Current mode: ${ctx.engine.getPermission().getMode()}
|
|
744
|
+
config.yml: ${ctx.config.permission}
|
|
691
745
|
|
|
692
|
-
|
|
693
|
-
auto — Run tools automatically (suitable for sandboxed envs)
|
|
694
|
-
ask — Prompt before each tool execution (default, recommended)
|
|
695
|
-
bypass — Skip all checks (⚠ only for trusted codebases)
|
|
746
|
+
Shift+Tab cycles: ${renderCycleLine()} (this session only).
|
|
696
747
|
|
|
697
|
-
|
|
748
|
+
Modes (least → most permissive):
|
|
749
|
+
${renderModeTable()}
|
|
750
|
+
|
|
751
|
+
To persist one for a project, .mipham/config.yml:
|
|
752
|
+
permission: acceptEdits
|
|
753
|
+
permissionRules:
|
|
754
|
+
allow: ["Bash(npm test)"] # or: /permissions allow "Bash(npm test)"
|
|
755
|
+
deny: ["Bash(rm *)"]
|
|
698
756
|
|
|
699
757
|
Available tools (${ctx.engine.getTools().size}):
|
|
700
758
|
File: read, write, edit, glob, grep
|
|
@@ -703,12 +761,6 @@ Available tools (${ctx.engine.getTools().size}):
|
|
|
703
761
|
Net: web-fetch, web-search
|
|
704
762
|
Sys: config, mcp
|
|
705
763
|
|
|
706
|
-
Each tool category can be configured independently in .mipham/config.yml:
|
|
707
|
-
permissions:
|
|
708
|
-
file: ask
|
|
709
|
-
exec: ask
|
|
710
|
-
network: auto
|
|
711
|
-
|
|
712
764
|
Next: /setup 6 for shell integration`,
|
|
713
765
|
}
|
|
714
766
|
}
|
|
@@ -784,7 +836,8 @@ To persist across sessions, add to .mipham/config.yml:
|
|
|
784
836
|
- ${resolved}
|
|
785
837
|
|
|
786
838
|
The AI can now access files in this directory.
|
|
787
|
-
Permission
|
|
839
|
+
Permission mode: Shift+Tab cycles ${renderCycleLine()} (this session);
|
|
840
|
+
\`permission:\` in .mipham/config.yml persists one.`,
|
|
788
841
|
}
|
|
789
842
|
}
|
|
790
843
|
|
|
@@ -872,8 +925,8 @@ const promptAuditCmd: CommandHandler = async () => {
|
|
|
872
925
|
|
|
873
926
|
// Files to scan
|
|
874
927
|
const scanDirs = [
|
|
875
|
-
join(cwd,
|
|
876
|
-
join(cwd,
|
|
928
|
+
join(cwd, MIPHAM_DIR, 'skills'),
|
|
929
|
+
join(cwd, MIPHAM_DIR, 'rules'),
|
|
877
930
|
join(cwd, '.claude'),
|
|
878
931
|
]
|
|
879
932
|
const skillFiles: string[] = []
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs'
|
|
2
2
|
import { join, dirname } from 'node:path'
|
|
3
|
-
import { homedir } from 'node:os'
|
|
4
3
|
import { atomicWriteFileSync } from '../shared/atomic-write'
|
|
5
4
|
import { saveProviderApiKey } from './loader'
|
|
5
|
+
import { miphamHome } from '../core/paths.ts'
|
|
6
6
|
|
|
7
|
-
const MIPHAM_HOME =
|
|
7
|
+
const MIPHAM_HOME = miphamHome()
|
|
8
8
|
const KEYS_FILE = join(MIPHAM_HOME, 'keys.json')
|
|
9
9
|
const EXPIRY_DAYS = 90
|
|
10
10
|
|