@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.
Files changed (84) hide show
  1. package/bin/mipham.ts +20 -5
  2. package/package.json +1 -1
  3. package/skills/standard/mipham-code-setup.SKILL.md +30 -8
  4. package/src/agent/agent-context.ts +5 -5
  5. package/src/agent/agent-experience.ts +2 -2
  6. package/src/agent/agent-registry.ts +4 -4
  7. package/src/agent/cross-session/discovery.ts +3 -2
  8. package/src/agent/cross-session/file-inbox.ts +2 -2
  9. package/src/agent/effectiveness-tracker.ts +2 -2
  10. package/src/agent/pattern-analyzer.ts +3 -4
  11. package/src/agent/sub-agent.ts +12 -2
  12. package/src/agent/types.ts +4 -1
  13. package/src/commands/autoloop-journal.ts +2 -2
  14. package/src/commands/environment.ts +2 -1
  15. package/src/commands/loop-scaffold.ts +2 -1
  16. package/src/commands/project.ts +86 -33
  17. package/src/config/keys-manager.ts +2 -2
  18. package/src/config/loader.ts +11 -10
  19. package/src/config/preferences.ts +3 -4
  20. package/src/core/auto-memory.ts +2 -3
  21. package/src/core/constitution-loader.ts +3 -4
  22. package/src/core/crsi-producer.ts +3 -3
  23. package/src/core/crsi-sandbox.ts +3 -2
  24. package/src/core/dream-engine.ts +2 -2
  25. package/src/core/engine.ts +38 -6
  26. package/src/core/error-signature-db.ts +2 -2
  27. package/src/core/eval-harness.ts +4 -3
  28. package/src/core/improvement-track.ts +5 -6
  29. package/src/core/instructions.ts +28 -4
  30. package/src/core/memory/memory-loader.ts +2 -3
  31. package/src/core/paths.ts +21 -1
  32. package/src/core/permission-audit.ts +120 -0
  33. package/src/core/permission-classifier.ts +449 -0
  34. package/src/core/permission-config.ts +106 -15
  35. package/src/core/permission.ts +369 -16
  36. package/src/core/rule-engine.ts +2 -2
  37. package/src/core/rules-loader.ts +3 -2
  38. package/src/core/session-log.ts +2 -3
  39. package/src/core/session-store.ts +2 -3
  40. package/src/core/workspace-trust.ts +4 -3
  41. package/src/daemon/database.ts +2 -2
  42. package/src/daemon/index.ts +2 -3
  43. package/src/daemon/launch.ts +3 -3
  44. package/src/daemon/server.ts +15 -0
  45. package/src/i18n-core/locales/en-US.json +7 -1
  46. package/src/i18n-core/locales/zh-CN.json +7 -1
  47. package/src/index.tsx +30 -6
  48. package/src/mcp/token-store.ts +2 -2
  49. package/src/plugin/plugin-manager.ts +2 -2
  50. package/src/shared/constants.ts +0 -1
  51. package/src/shared/package-info.ts +1 -1
  52. package/src/shared/types.ts +46 -6
  53. package/src/shared/update.ts +255 -20
  54. package/src/skills/bundled-skills.ts +1 -1
  55. package/src/skills/loader.ts +2 -3
  56. package/src/skills/marketplace.ts +2 -3
  57. package/src/skills/registry.ts +2 -2
  58. package/src/skills/skill-assets.ts +2 -2
  59. package/src/skills/usage.ts +2 -2
  60. package/src/telemetry/consent.ts +2 -3
  61. package/src/tools/agent/enter-plan.ts +3 -2
  62. package/src/tools/agent/exit-plan.ts +1 -1
  63. package/src/tools/agent/list-agents.ts +1 -1
  64. package/src/tools/agent/memory.ts +3 -3
  65. package/src/tools/agent/plan.ts +3 -2
  66. package/src/tools/agent/report-findings.ts +1 -1
  67. package/src/tools/agent/send-message.ts +1 -1
  68. package/src/tools/agent/skill.ts +1 -1
  69. package/src/tools/exec/git.ts +2 -2
  70. package/src/tools/exec/task.ts +1 -1
  71. package/src/tools/file/glob.ts +1 -1
  72. package/src/tools/file/grep.ts +1 -1
  73. package/src/tools/file/read.ts +1 -1
  74. package/src/tools/network/web-fetch.ts +1 -1
  75. package/src/tools/network/web-search.ts +1 -1
  76. package/src/tools/scheduling/cron.ts +5 -5
  77. package/src/tools/scheduling/schedule-wakeup.ts +1 -1
  78. package/src/tools/system/config.ts +2 -2
  79. package/src/tools/system/tool-search.ts +1 -1
  80. package/src/ui/app.tsx +51 -9
  81. package/src/ui/commands.ts +26 -21
  82. package/src/ui/config-wizard.tsx +2 -2
  83. package/src/ui/input.tsx +15 -3
  84. package/src/workflow/journal.ts +2 -2
@@ -1,13 +1,14 @@
1
1
  import { mkdirSync, writeFileSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import type { ToolDefinition } from '../../shared/index.ts'
4
+ import { MIPHAM_DIR } from '../../shared/constants.ts'
4
5
 
5
6
  export const planTool: ToolDefinition = {
6
7
  name: 'Plan',
7
8
  description:
8
9
  'Enter plan mode — read-only analysis and design. Creates a structured plan file in .mipham/plans/.',
9
10
  category: 'agent',
10
- permission: 'auto',
11
+ permission: 'self',
11
12
  parameters: {
12
13
  type: 'object',
13
14
  properties: {
@@ -25,7 +26,7 @@ export const planTool: ToolDefinition = {
25
26
  const title = (params.title as string) || 'Implementation Plan'
26
27
  const description = (params.description as string) || ''
27
28
 
28
- const planDir = join(ctx.cwd, '.mipham', 'plans')
29
+ const planDir = join(ctx.cwd, MIPHAM_DIR, 'plans')
29
30
  mkdirSync(planDir, { recursive: true })
30
31
 
31
32
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
@@ -7,7 +7,7 @@ export const reportFindingsTool: ToolDefinition = {
7
7
  'Use this to output structured review results with file, line, summary, ' +
8
8
  'failure_scenario, and category. Findings are ranked most-severe first.',
9
9
  category: 'agent',
10
- permission: 'auto',
10
+ permission: 'self',
11
11
  parameters: {
12
12
  type: 'object',
13
13
  properties: {
@@ -8,7 +8,7 @@ export const sendMessageTool: ToolDefinition = {
8
8
  'Use "main" for the parent conversation, a background task ID for same-process agents, ' +
9
9
  'or a session ID (or unique session name) for cross-session messaging (use ListAgents to discover sessions).',
10
10
  category: 'agent',
11
- permission: 'auto',
11
+ permission: 'self',
12
12
  parameters: {
13
13
  type: 'object',
14
14
  properties: {
@@ -10,7 +10,7 @@ export const skillTool: ToolDefinition = {
10
10
  description:
11
11
  'Execute a skill (.SKILL.md or .mipham-skill.md) by name. Skills extend AI capabilities with specialized instructions.',
12
12
  category: 'agent',
13
- permission: 'auto',
13
+ permission: 'self',
14
14
  parameters: {
15
15
  type: 'object',
16
16
  properties: {
@@ -205,7 +205,7 @@ export const gitTool: ToolDefinition = {
205
205
  name: 'Git',
206
206
  description: 'Execute git commands. Dangerous operations (force push, hard reset) are blocked.',
207
207
  category: 'exec',
208
- permission: 'auto',
208
+ permission: 'self',
209
209
  parameters: {
210
210
  type: 'object',
211
211
  properties: {
@@ -240,7 +240,7 @@ export const gitTool: ToolDefinition = {
240
240
  }
241
241
  }
242
242
 
243
- // Git runs without an approval prompt (`permission: 'auto'`), so an option
243
+ // Git runs without an approval prompt (`permission: 'self'`), so an option
244
244
  // that names a program to execute is a code-execution path Bash would have
245
245
  // had to ask for. Checked on argv, which is what git is handed below.
246
246
  const argv = splitCommand(command)
@@ -61,7 +61,7 @@ export const taskTool: ToolDefinition = {
61
61
  'background task output/stop, and status workflow: pending → in_progress → completed. ' +
62
62
  'Use for complex multi-step tasks, session tracking, and organizing work.',
63
63
  category: 'exec',
64
- permission: 'auto',
64
+ permission: 'self',
65
65
  parameters: {
66
66
  type: 'object',
67
67
  properties: {
@@ -15,7 +15,7 @@ export function createGlobTool(credentialConfig?: CredentialMaskingConfig): Tool
15
15
  name: 'Glob',
16
16
  description: 'Find files matching a glob pattern.',
17
17
  category: 'file',
18
- permission: 'auto',
18
+ permission: 'self',
19
19
  parameters: {
20
20
  type: 'object',
21
21
  properties: {
@@ -88,7 +88,7 @@ export function createGrepTool(credentialConfig?: CredentialMaskingConfig): Tool
88
88
  '(glob pattern, e.g. "*.ts") before searching. ' +
89
89
  'The `pattern` parameter accepts full regex syntax (e.g., "log.*Error", "\\bclass\\s+\\w+").',
90
90
  category: 'file',
91
- permission: 'auto',
91
+ permission: 'self',
92
92
  parameters: {
93
93
  type: 'object',
94
94
  properties: {
@@ -121,7 +121,7 @@ export function createReadTool(credentialConfig?: CredentialMaskingConfig): Tool
121
121
  description:
122
122
  'Read a file from the local filesystem. Supports offset and limit for large files.',
123
123
  category: 'file',
124
- permission: 'auto',
124
+ permission: 'self',
125
125
  parameters: {
126
126
  type: 'object',
127
127
  properties: {
@@ -148,7 +148,7 @@ export const webFetchTool: ToolDefinition = {
148
148
  description:
149
149
  'Fetches a URL, converts the page to markdown. HTTP is upgraded to HTTPS. Cross-host redirects are returned to the caller. Responses are cached for 15 minutes per URL.',
150
150
  category: 'network',
151
- permission: 'auto',
151
+ permission: 'self',
152
152
  parameters: {
153
153
  type: 'object',
154
154
  properties: {
@@ -81,7 +81,7 @@ export const webSearchTool: ToolDefinition = {
81
81
  description:
82
82
  'Search the web via Brave Search API. Returns result blocks with titles, URLs, and descriptions. Set BRAVE_API_KEY to enable.',
83
83
  category: 'network',
84
- permission: 'auto',
84
+ permission: 'self',
85
85
  parameters: {
86
86
  type: 'object',
87
87
  properties: {
@@ -8,11 +8,11 @@ import {
8
8
  } from 'node:fs'
9
9
  import { join } from 'node:path'
10
10
  import { createHash } from 'node:crypto'
11
- import { homedir } from 'node:os'
12
11
  import type { ToolDefinition } from '../../shared/index.ts'
13
12
  import { computeNextFire } from '../../core/cron'
13
+ import { miphamHome } from '../../core/paths.ts'
14
14
 
15
- const CRON_DIR = join(homedir(), '.mipham', 'cron')
15
+ const CRON_DIR = miphamHome('cron')
16
16
 
17
17
  function ensureCronDir(): void {
18
18
  if (!existsSync(CRON_DIR)) mkdirSync(CRON_DIR, { recursive: true })
@@ -97,7 +97,7 @@ export const cronCreateTool: ToolDefinition = {
97
97
  'For one-shot: set recurring:false with pinned minute/hour/day-of-month/month. ' +
98
98
  'Durable — survives restarts, written to ~/.mipham/cron/.',
99
99
  category: 'scheduling',
100
- permission: 'auto',
100
+ permission: 'self',
101
101
  parameters: {
102
102
  type: 'object',
103
103
  properties: {
@@ -157,7 +157,7 @@ export const cronDeleteTool: ToolDefinition = {
157
157
  description:
158
158
  'Cancel a cron job previously scheduled with CronCreate. Removes from ~/.mipham/cron/.',
159
159
  category: 'scheduling',
160
- permission: 'auto',
160
+ permission: 'self',
161
161
  parameters: {
162
162
  type: 'object',
163
163
  properties: {
@@ -181,7 +181,7 @@ export const cronListTool: ToolDefinition = {
181
181
  name: 'CronList',
182
182
  description: 'List all cron jobs scheduled via CronCreate, both durable and session-only.',
183
183
  category: 'scheduling',
184
- permission: 'auto',
184
+ permission: 'self',
185
185
  parameters: {
186
186
  type: 'object',
187
187
  properties: {},
@@ -20,7 +20,7 @@ export const scheduleWakeupTool: ToolDefinition = {
20
20
  description:
21
21
  'Schedule when to resume work in /loop dynamic mode — the user invoked /loop without an interval, asking you to self-pace iterations of a specific task. Do NOT schedule a short-interval wakeup to poll for background work you started — when harness-tracked work finishes, you are re-invoked automatically. The runtime clamps to [60, 3600].',
22
22
  category: 'scheduling',
23
- permission: 'auto',
23
+ permission: 'self',
24
24
  parameters: {
25
25
  type: 'object',
26
26
  properties: {
@@ -1,11 +1,11 @@
1
1
  import { readFileSync, existsSync, mkdirSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
- import { homedir } from 'node:os'
4
3
  import { parse as parseYaml, stringify } from 'yaml'
5
4
  import { atomicWriteFileSync } from '../../shared/atomic-write'
6
5
  import type { ToolDefinition } from '../../shared/index.ts'
6
+ import { miphamHome } from '../../core/paths.ts'
7
7
 
8
- const MIPHAM_HOME = join(homedir(), '.mipham')
8
+ const MIPHAM_HOME = miphamHome()
9
9
  const USER_CONFIG = join(MIPHAM_HOME, 'config.yml')
10
10
 
11
11
  export const configTool: ToolDefinition = {
@@ -16,7 +16,7 @@ export const toolSearchTool: ToolDefinition = {
16
16
  'Use this to discover tools on demand instead of loading all tool definitions into context. ' +
17
17
  'Returns matching tool names with their server and description.',
18
18
  category: 'system',
19
- permission: 'auto',
19
+ permission: 'self',
20
20
  parameters: {
21
21
  type: 'object',
22
22
  properties: {
package/src/ui/app.tsx CHANGED
@@ -9,6 +9,7 @@ import type { RemoteEngine } from '../daemon/remote-engine'
9
9
  import type { MiphamConfig } from '../shared/index.ts'
10
10
  import type { Llm } from '../providers/llm'
11
11
  import { AUTOCOMPLETE_MAX_CONTEXT, type RecentMessage } from '../core/autocomplete'
12
+ import { MODE_CYCLE } from '../core/permission-config'
12
13
  import { resolveGitPr, prColor, type GitPr } from '../core/git-pr'
13
14
  import type { SkillsLoader } from '../skills/loader'
14
15
  import type { PluginManager } from '../plugin/plugin-manager'
@@ -103,17 +104,43 @@ interface AgentProgress {
103
104
  // Version is read fresh from package.json at startup via runApp prop
104
105
  // (bypasses Bun module caching after npm update)
105
106
 
106
- // Cycle order: Claude Code modes (manual → accept edits → plan → bypass).
107
- const PERMISSION_MODES: PermissionMode[] = ['default', 'acceptEdits', 'plan', 'bypassPermissions']
107
+ // The wheel is `MODE_CYCLE` — imported, not copied. This file used to hold its own
108
+ // array, which is how a help screen and a footer come to advertise a mode the wheel
109
+ // cannot reach: two lists that agree on the day they are written and drift after.
108
110
  // Labels aligned with Claude Code terminology: describe behavior, not capability.
109
- // Claude Code modes: manual mode → accept edits on → plan → bypass.
110
111
  const PERMISSION_COLORS: Record<PermissionMode, string> = {
111
112
  default: 'white',
112
113
  acceptEdits: 'blue',
113
114
  plan: 'yellow',
115
+ auto: 'magenta',
114
116
  bypassPermissions: 'red',
115
117
  }
116
118
 
119
+ // 页脚那一行的**字形**,取值与 Claude Code 二进制里那四个格逐字对齐(`$Pe="⏸"` /
120
+ // `Ije="⏵⏵"`):`default` **什么都不显示**、`acceptEdits` 与 `auto` 都是 `⏵⏵`、`plan` 是 `⏸`。
121
+ // 这本是设计文档决策 9 的一半,Step 6/7 只做了标签派生、漏了字形,此处补上。
122
+ // `bypassPermissions` 在 CC 的转盘上没有对应格(它的转盘 4 格、我们这张是 5 档),
123
+ // 无从对照 ⇒ **保留既有渲染** `⏵⏵`,是最小的选择而不是新决定。
124
+ // 穷尽 `Record` 与 `PERMISSION_COLORS` 同形:将来加档位忘了字形是**编译错**,不是静默空串。
125
+ const PERMISSION_GLYPHS: Record<PermissionMode, string> = {
126
+ default: '',
127
+ acceptEdits: '⏵⏵',
128
+ plan: '⏸',
129
+ auto: '⏵⏵',
130
+ bypassPermissions: '⏵⏵',
131
+ }
132
+
133
+ /**
134
+ * 页脚前缀 = `<字形> `,**没有字形时连那个空格都不留**(否则 `default` 那一行会以空格起头)。
135
+ *
136
+ * 与 `PERMISSION_LABELS` 合成**单独一个**文本节点交给 Ink:分两处写时,中间那点缩进是否
137
+ * 落成空格取决于 JSX 的空白折叠规则 —— 一件与权限无关、却会改变用户读到的东西的巧合。
138
+ */
139
+ export function permissionGlyphPrefix(mode: PermissionMode): string {
140
+ const glyph = PERMISSION_GLYPHS[mode]
141
+ return glyph ? `${glyph} ` : ''
142
+ }
143
+
117
144
  /** 页脚读模式所需的最小面 —— `QueryEngine` 与 `RemoteEngine` 都满足。 */
118
145
  export type PermissionSource = {
119
146
  setMode(mode: PermissionMode): void
@@ -136,8 +163,17 @@ export function cyclePermissionMode(
136
163
  permission: PermissionSource,
137
164
  current: PermissionMode,
138
165
  ): PermissionMode {
139
- const idx = PERMISSION_MODES.indexOf(current)
140
- const next = PERMISSION_MODES[(idx + 1) % PERMISSION_MODES.length]!
166
+ // An off-wheel *current* mode is reachable, not hypothetical: `bypassPermissions`
167
+ // is legal (`ALL_MODES`) without being cyclable, so `permission: bypassPermissions`
168
+ // in config puts the user in a state the wheel has no slot for. `indexOf` then
169
+ // answers `-1`, and `(-1 + 1) % length` lands on slot 0 by arithmetic accident.
170
+ // Spelled out here so it is a decision rather than an accident: off-wheel goes to
171
+ // the wheel's first slot, the same answer `nextMode` gives. That slot is `default`,
172
+ // which is **not** the narrowest mode (`plan` is) — the wheel is not a permissiveness
173
+ // order — but it *is* narrower than the only off-wheel state you can reach,
174
+ // `bypassPermissions`, so the step still points away from wider.
175
+ const idx = MODE_CYCLE.indexOf(current)
176
+ const next = idx === -1 ? MODE_CYCLE[0]! : MODE_CYCLE[(idx + 1) % MODE_CYCLE.length]!
141
177
  permission.setMode(next)
142
178
  return permission.getMode()
143
179
  }
@@ -204,6 +240,7 @@ export function App({
204
240
  default: t('ui.permission.manual'),
205
241
  acceptEdits: t('ui.permission.accept_edits'),
206
242
  plan: t('ui.permission.plan_mode'),
243
+ auto: t('ui.permission.auto_mode'),
207
244
  bypassPermissions: t('ui.permission.bypass'),
208
245
  }),
209
246
  [t],
@@ -231,6 +268,10 @@ export function App({
231
268
  const [providerId, setProviderId] = useState(initialProvider || config.defaultProvider)
232
269
  const [modelId, setModelId] = useState(initialModel || config.defaultModel)
233
270
  const [pickerOpen, setPickerOpen] = useState(false)
271
+ // ↑/↓ 翻历史用的已提交输入。**必须住在这里而不是 InputBar 里** —— InputBar 会被
272
+ // 卸载(pickerOpen 三元 / apiKeyPrompt 早退 / Ctrl+G),组件内 state 随之清零,
273
+ // 于是「开一次模型选择器,历史就没了」(ROADMAP D7)。
274
+ const [inputHistory, setInputHistory] = useState<string[]>([])
234
275
  const [updateStatus, setUpdateStatus] = useState<UpdateStatus | null>(null)
235
276
  // Current git branch — read once at mount (not a git repo → null).
236
277
  const [gitBranch] = useState<string | null>(() => {
@@ -1242,6 +1283,8 @@ export function App({
1242
1283
  <InputBar
1243
1284
  onSubmit={handleSubmit}
1244
1285
  isLoading={isLoading}
1286
+ history={inputHistory}
1287
+ onHistoryAppend={(v) => setInputHistory((prev) => [...prev, v])}
1245
1288
  llm={autocompleteLlm}
1246
1289
  recentMessages={recentMessages}
1247
1290
  autocompleteEnabled={
@@ -1319,13 +1362,12 @@ export function App({
1319
1362
  <Box flexDirection="column">
1320
1363
  <Box flexDirection="row">
1321
1364
  <Text color={PERMISSION_COLORS[permissionMode]}>
1322
- ⏵⏵ {PERMISSION_LABELS[permissionMode]}
1365
+ {permissionGlyphPrefix(permissionMode) + PERMISSION_LABELS[permissionMode]}
1323
1366
  </Text>
1324
1367
  <Text dimColor>
1325
1368
  {' '}
1326
- ({t('ui.status.shift_tab_cycle')}: {PERMISSION_LABELS.default} ·{' '}
1327
- {PERMISSION_LABELS.acceptEdits} · {PERMISSION_LABELS.plan} ·{' '}
1328
- {PERMISSION_LABELS.bypassPermissions}){' · '}
1369
+ ({t('ui.status.shift_tab_cycle')}:{' '}
1370
+ {MODE_CYCLE.map((m) => PERMISSION_LABELS[m]).join(' · ')}){' · '}
1329
1371
  {t('ui.status.esc_to_interrupt')}
1330
1372
  {' · '}
1331
1373
  {t('ui.status.left_for_agents')}
@@ -17,7 +17,7 @@ import { unregisterMcpServerTools } from '../mcp/registry'
17
17
  import { buildCapabilityReport } from '../core/capability-inventory'
18
18
  import { InstructionsLoader } from '../core/instructions'
19
19
  import { findDerivableSections, DERIVABLE_HINTS } from '../core/claude-md-audit'
20
- import { worktreeRoot, workflowScriptDir, workflowScriptDirs } from '../core/paths.ts'
20
+ import { miphamHome, workflowScriptDir, workflowScriptDirs, worktreeRoot } from '../core/paths.ts'
21
21
  import { fixDoctor, fixConfig, fixCache, selectRepoClaudeFiles } from '../core/fix'
22
22
  import { fixCodeTarget } from '../core/fix-code'
23
23
  import { homedir } from 'node:os'
@@ -108,7 +108,7 @@ import { keysCmd } from '../commands/keys'
108
108
  import { workflowViewCmd, workflowWatchCmd } from '../commands/workflow-view.js'
109
109
  import { listActiveAutoloops, formatLoopRows } from '../commands/autoloop-journal.js'
110
110
  import { execSync } from 'node:child_process'
111
- import { OLLAMA_PRESET_MODELS } from '../shared/constants'
111
+ import { MIPHAM_DIR, OLLAMA_PRESET_MODELS } from '../shared/constants'
112
112
  import { renameActiveSession } from '../agent/cross-session/discovery'
113
113
 
114
114
  export interface CommandContext {
@@ -3366,7 +3366,6 @@ const fixCmd: CommandHandler = async (ctx, args) => {
3366
3366
  const t = resolveT(ctx)
3367
3367
  const { readFileSync, writeFileSync } = await import('node:fs')
3368
3368
  const { join } = await import('node:path')
3369
- const { homedir } = await import('node:os')
3370
3369
  const { parse: parseYaml } = await import('yaml')
3371
3370
 
3372
3371
  const target = args.find((a) => a === 'doctor' || a === 'config' || a === 'cache' || a === 'test')
@@ -3406,11 +3405,7 @@ const fixCmd: CommandHandler = async (ctx, args) => {
3406
3405
  }
3407
3406
 
3408
3407
  if (!target || target === 'config') {
3409
- const home = homedir()
3410
- const configPaths = [
3411
- join(process.cwd(), '.mipham', 'config.yml'),
3412
- join(home, '.mipham', 'config.yml'),
3413
- ]
3408
+ const configPaths = [join(process.cwd(), MIPHAM_DIR, 'config.yml'), miphamHome('config.yml')]
3414
3409
  const hookEngine = ctx.engine.getHookEngine?.()
3415
3410
  const result = fixConfig({
3416
3411
  configPaths,
@@ -3444,7 +3439,7 @@ const fixCmd: CommandHandler = async (ctx, args) => {
3444
3439
  }
3445
3440
 
3446
3441
  if (!target || target === 'cache') {
3447
- const crsiDir = join(homedir(), '.mipham', 'crsi')
3442
+ const crsiDir = miphamHome('crsi')
3448
3443
  const cacheFiles = ['eval-scores.jsonl', 'improvements.jsonl', 'prose-proposals.jsonl'].map(
3449
3444
  (f) => join(crsiDir, f),
3450
3445
  )
@@ -3670,7 +3665,7 @@ const filesCmd: CommandHandler = async (ctx) => {
3670
3665
  try {
3671
3666
  const entries = readdirSync(cwd, { withFileTypes: true })
3672
3667
  const items = entries
3673
- .filter((e) => !e.name.startsWith('.') || e.name === '.mipham' || e.name === '.mcp.json')
3668
+ .filter((e) => !e.name.startsWith('.') || e.name === MIPHAM_DIR || e.name === '.mcp.json')
3674
3669
  .slice(0, 40)
3675
3670
  .map((e) => {
3676
3671
  const icon = e.isDirectory() ? '📁' : '📄'
@@ -4316,8 +4311,7 @@ const memoryCmd: CommandHandler = async (ctx, args) => {
4316
4311
  const { existsSync, readdirSync, readFileSync, statSync } = await import('node:fs')
4317
4312
  const { join } = await import('node:path')
4318
4313
 
4319
- const home = homedir()
4320
- const memoryDir = join(home, '.mipham', 'memory')
4314
+ const memoryDir = miphamHome('memory')
4321
4315
 
4322
4316
  // /memory gc — 记忆卫生:归档「0 召回 + 过期」的 auto-* 记忆(手写只报告)
4323
4317
  if (args[0]?.toLowerCase() === 'gc') {
@@ -4467,15 +4461,20 @@ const upgradeCmd: CommandHandler = async (ctx) => {
4467
4461
  lines.push(`Config backed up to: ${backupPath}`)
4468
4462
  }
4469
4463
 
4470
- const ok = performUpdate(update.latest)
4464
+ const result = performUpdate(update.latest)
4471
4465
 
4472
- if (ok) {
4466
+ if (result.ok) {
4473
4467
  const configPath = getConfigPath()
4474
4468
  const { existsSync } = await import('node:fs')
4475
- ctx.setUpdateStatus({ state: 'installed', latest: update.latest })
4469
+ // 只有自证通过才置「已装待重启」—— 装坏了还提示重启,是在骗用户。
4470
+ if (result.verified) ctx.setUpdateStatus({ state: 'installed', latest: update.latest })
4476
4471
  lines.push('')
4477
4472
  lines.push(t('commands.upgrade.updated', { version: update.latest }))
4478
4473
 
4474
+ if (!result.verified) {
4475
+ lines.push(t('commands.upgrade.unverified', { reason: result.reason ?? '' }))
4476
+ }
4477
+
4479
4478
  if (existsSync(configPath)) {
4480
4479
  lines.push(t('commands.upgrade.config_preserved', { path: configPath }))
4481
4480
  } else if (backupPath) {
@@ -4488,8 +4487,17 @@ const upgradeCmd: CommandHandler = async (ctx) => {
4488
4487
  lines.push(t('commands.upgrade.old_version_warning'))
4489
4488
  } else {
4490
4489
  lines.push('')
4490
+ // 失败信息必须回答两件事:为什么,以及**我手里还有没有 CLI**。
4491
+ const rollbackNote = t(
4492
+ result.rolledBack ? 'commands.upgrade.rolled_back' : 'commands.upgrade.no_rollback',
4493
+ )
4491
4494
  lines.push(
4492
- t('commands.upgrade.update_failed', { command: NPM_UPDATE_COMMAND, path: backupPath || '' }),
4495
+ t('commands.upgrade.update_failed', {
4496
+ reason: result.reason ?? '',
4497
+ command: NPM_UPDATE_COMMAND,
4498
+ path: backupPath || '',
4499
+ rolledBack: rollbackNote,
4500
+ }),
4493
4501
  )
4494
4502
  }
4495
4503
 
@@ -4982,10 +4990,8 @@ const loginCmd: CommandHandler = (ctx) => {
4982
4990
 
4983
4991
  const logoutCmd: CommandHandler = async () => {
4984
4992
  const { existsSync } = await import('node:fs')
4985
- const { join } = await import('node:path')
4986
4993
 
4987
- const home = homedir()
4988
- const userConfig = join(home, '.mipham', 'config.yml')
4994
+ const userConfig = miphamHome('config.yml')
4989
4995
  const hasUserConfig = existsSync(userConfig)
4990
4996
 
4991
4997
  return {
@@ -5027,9 +5033,8 @@ const feedbackCmd: CommandHandler = async (ctx, args) => {
5027
5033
  try {
5028
5034
  const { writeFileSync, mkdirSync, existsSync } = await import('node:fs')
5029
5035
  const { join } = await import('node:path')
5030
- const { homedir } = await import('node:os')
5031
5036
 
5032
- const dir = join(homedir(), '.mipham', 'feedback')
5037
+ const dir = miphamHome('feedback')
5033
5038
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
5034
5039
 
5035
5040
  const ts = new Date().toISOString().replace(/[:.]/g, '-')
@@ -17,10 +17,10 @@ import type { ModelInfo } from '../shared/types'
17
17
  import { mkdirSync } from 'node:fs'
18
18
  import { atomicWriteFileSync } from '../shared/atomic-write'
19
19
  import { join } from 'node:path'
20
- import { homedir } from 'node:os'
21
20
  import { execSync } from 'node:child_process'
22
21
  import { getCredentialKey, encryptApiKey } from '../config/credential-crypto'
23
22
  import { CLOUD_PROVIDERS, getActiveModels, buildConfigYaml } from '../config/wizard-config'
23
+ import { miphamHome } from '../core/paths.ts'
24
24
 
25
25
  // ── Types ──
26
26
 
@@ -38,7 +38,7 @@ const SELECTED_COLOR = 'cyan'
38
38
  // ── Helpers ──
39
39
 
40
40
  function writeConfigFile(providerId: string, modelId: string, apiKey: string): void {
41
- const configDir = join(homedir(), '.mipham')
41
+ const configDir = miphamHome()
42
42
  mkdirSync(configDir, { recursive: true })
43
43
 
44
44
  const models = providerId === 'ollama' ? getOllamaModelListForConfig() : []
package/src/ui/input.tsx CHANGED
@@ -24,6 +24,14 @@ interface InputBarProps {
24
24
  onCancel?: () => void
25
25
  /** When false, don't auto-open the slash-command picker when typing `/`. */
26
26
  showCommandPicker?: boolean
27
+ /**
28
+ * ↑/↓ 翻历史用的已提交输入。**由调用方持有,不放本组件内** —— app.tsx 有三条路径把
29
+ * InputBar 整个卸载(`pickerOpen` 三元 / `apiKeyPrompt` 早退 / Ctrl+G),
30
+ * 放在这里会被卸载清空(ROADMAP D7)。
31
+ */
32
+ history: string[]
33
+ /** 提交时把该条追加进 history(由调用方落 state)。 */
34
+ onHistoryAppend: (value: string) => void
27
35
  /** LLM 续写建议所需的模型(app.tsx 传;RemoteEngine 下 undefined → 补全禁用)。 */
28
36
  llm?: Llm
29
37
  /** 最近对话上下文(供续写贴合)。 */
@@ -288,6 +296,8 @@ export function InputBar({
288
296
  onCyclePermission,
289
297
  onCancel,
290
298
  showCommandPicker = true,
299
+ history,
300
+ onHistoryAppend,
291
301
  llm,
292
302
  recentMessages,
293
303
  autocompleteEnabled = true,
@@ -302,7 +312,9 @@ export function InputBar({
302
312
  const prevLoading = useRef(isLoading)
303
313
 
304
314
  // ── Message history for arrow-key navigation (Claude Code parity) ──
305
- const [submittedHistory, setSubmittedHistory] = useState<string[]>([])
315
+ // `history` 由调用方持有(见 InputBarProps.history):本组件不拥有它,因为
316
+ // app.tsx 会卸载本组件,而卸载会清空组件内 state(ROADMAP D7)。
317
+ // 下面两个 ref 是**浏览游标**、不是历史本体,随卸载重置才是对的。
306
318
  const historyIndexRef = useRef(-1) // -1 = not browsing history
307
319
  const savedDraftRef = useRef('') // saved user draft before browsing history
308
320
 
@@ -434,7 +446,7 @@ export function InputBar({
434
446
 
435
447
  const result = navigateHistory(
436
448
  {
437
- history: submittedHistory,
449
+ history,
438
450
  index: historyIndexRef.current,
439
451
  savedDraft: savedDraftRef.current,
440
452
  },
@@ -480,7 +492,7 @@ export function InputBar({
480
492
  onCancel?.()
481
493
  }
482
494
  // Save to message history for arrow-key navigation
483
- setSubmittedHistory((prev) => [...prev, finalValue])
495
+ onHistoryAppend(finalValue)
484
496
  historyIndexRef.current = -1
485
497
  savedDraftRef.current = ''
486
498
  onSubmit(finalValue)
@@ -7,9 +7,9 @@ import {
7
7
  readdirSync,
8
8
  } from 'node:fs'
9
9
  import { join } from 'node:path'
10
- import { homedir } from 'node:os'
10
+ import { miphamHome } from '../core/paths.ts'
11
11
 
12
- const WORKFLOW_DIR = join(homedir(), '.mipham', 'workflows')
12
+ const WORKFLOW_DIR = miphamHome('workflows')
13
13
 
14
14
  export interface JournalEntry {
15
15
  seq: number