@miphamai/cli 0.81.0 → 0.81.2
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/package.json +3 -2
- package/src/agent/agent-experience.ts +2 -1
- package/src/agent/effectiveness-tracker.ts +2 -1
- package/src/commands/loop-scaffold.ts +2 -1
- package/src/commands/project.ts +3 -2
- package/src/config/wizard-config.ts +72 -0
- package/src/core/claude-md-audit.ts +32 -8
- package/src/core/instructions.ts +2 -1
- package/src/core/rule-engine.ts +2 -1
- package/src/core/session-log.ts +2 -1
- package/src/core/session-store.ts +2 -1
- package/src/daemon/database.ts +2 -1
- package/src/i18n-core/locales/en-US.json +1 -0
- package/src/i18n-core/locales/zh-CN.json +1 -0
- package/src/shared/package-info.ts +1 -1
- package/src/tools/system/config.ts +2 -1
- package/src/ui/chat.tsx +2 -1
- package/src/ui/commands.ts +8 -7
- package/src/ui/config-wizard.tsx +9 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miphamai/cli",
|
|
3
|
-
"version": "0.81.
|
|
3
|
+
"version": "0.81.2",
|
|
4
4
|
"description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"dev": "bun run bin/mipham.ts",
|
|
39
39
|
"build": "bun run scripts/generate-bundled-skills.ts && bun build --compile --minify ./bin/mipham.ts --outfile dist/mipham",
|
|
40
40
|
"typecheck": "tsc --noEmit",
|
|
41
|
-
"test": "vitest run"
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"knip": "knip --production --no-progress --no-exit-code"
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
44
45
|
"@larksuiteoapi/node-sdk": "^1.73.0",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
3
4
|
import { ExperienceRuleExtractor, type ExperienceRule } from './experience-rules.js'
|
|
4
5
|
|
|
5
6
|
const MAX_EXPERIENCES = 20
|
|
@@ -10,7 +11,7 @@ export class AgentExperience {
|
|
|
10
11
|
|
|
11
12
|
constructor(
|
|
12
13
|
private readonly agentName: string,
|
|
13
|
-
baseDir: string = join(
|
|
14
|
+
baseDir: string = join(homedir(), '.mipham', 'agent-memory'),
|
|
14
15
|
) {
|
|
15
16
|
this.expDir = join(baseDir, agentName)
|
|
16
17
|
this.expFile = join(this.expDir, 'experience.md')
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs'
|
|
2
2
|
import { join, dirname } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
3
4
|
import type { CrsiProvenanceBridge, CrsiVerdict } from './crsi-provenance-bridge.js'
|
|
4
5
|
import { isRecoverableToolFailure } from './recoverable-failure.js'
|
|
5
6
|
|
|
@@ -31,7 +32,7 @@ export class EffectivenessTracker {
|
|
|
31
32
|
private storePath: string
|
|
32
33
|
private provenanceBridge?: CrsiProvenanceBridge
|
|
33
34
|
|
|
34
|
-
constructor(storeDir: string = join(
|
|
35
|
+
constructor(storeDir: string = join(homedir(), '.mipham', 'rule-engine')) {
|
|
35
36
|
this.storePath = join(storeDir, STORE_FILE)
|
|
36
37
|
this.data = new Map()
|
|
37
38
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { mkdirSync, writeFileSync, existsSync, chmodSync } from 'node:fs'
|
|
13
13
|
import { join, resolve } from 'node:path'
|
|
14
|
+
import { homedir } from 'node:os'
|
|
14
15
|
|
|
15
16
|
export interface ScaffoldResult {
|
|
16
17
|
created: string[]
|
|
@@ -87,7 +88,7 @@ export function scaffoldLoopKit(basePath: string): ScaffoldResult {
|
|
|
87
88
|
const created: string[] = []
|
|
88
89
|
const skipped: string[] = []
|
|
89
90
|
|
|
90
|
-
const resolved = resolve(basePath.replace(/^~/,
|
|
91
|
+
const resolved = resolve(basePath.replace(/^~/, homedir()))
|
|
91
92
|
const miphamDir = join(resolved, '.mipham')
|
|
92
93
|
|
|
93
94
|
// ── .mipham/ root ──
|
package/src/commands/project.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { CommandHandler, CommandContext, CommandResult } from '../ui/commands.js'
|
|
9
9
|
import { getWorkspaceTrust } from '../core/workspace-trust'
|
|
10
10
|
import { atomicWriteFileSync } from '../shared/atomic-write'
|
|
11
|
+
import { homedir } from 'node:os'
|
|
11
12
|
|
|
12
13
|
export {
|
|
13
14
|
initCmd,
|
|
@@ -311,7 +312,7 @@ const setupCmd: CommandHandler = async (ctx, args) => {
|
|
|
311
312
|
const { existsSync } = await import('node:fs')
|
|
312
313
|
const { join } = await import('node:path')
|
|
313
314
|
const cwd = process.cwd()
|
|
314
|
-
const home =
|
|
315
|
+
const home = homedir()
|
|
315
316
|
|
|
316
317
|
const hasProjectMipham = existsSync(join(cwd, 'MIPHAM.md'))
|
|
317
318
|
const hasProjectConfig = existsSync(join(cwd, '.mipham', 'config.yml'))
|
|
@@ -688,7 +689,7 @@ permission boundaries. Adding directories here extends read/write access.`,
|
|
|
688
689
|
|
|
689
690
|
const { existsSync } = await import('node:fs')
|
|
690
691
|
const { resolve } = await import('node:path')
|
|
691
|
-
const resolved = resolve(dir.replace(/^~/,
|
|
692
|
+
const resolved = resolve(dir.replace(/^~/, homedir()))
|
|
692
693
|
|
|
693
694
|
if (!existsSync(resolved)) {
|
|
694
695
|
return { content: `✗ Directory not found: ${resolved}\n\nCheck the path and try again.` }
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data layer for the first-run Config Wizard — no Ink, no React, so it can be
|
|
3
|
+
* tested directly (the UI component imports it, tests import it too).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DEFAULT_PROVIDERS } from '../shared/constants'
|
|
7
|
+
import type { ModelInfo } from '../shared/types'
|
|
8
|
+
|
|
9
|
+
/** Providers offered in the wizard's cloud path (Ollama has its own step). */
|
|
10
|
+
export const CLOUD_PROVIDERS = DEFAULT_PROVIDERS.filter(
|
|
11
|
+
(p) => p.id !== 'ollama' && p.id !== 'mipham',
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export function getActiveModels(providerId: string): ModelInfo[] {
|
|
15
|
+
const provider = DEFAULT_PROVIDERS.find((p) => p.id === providerId)
|
|
16
|
+
return provider?.models.filter((m) => m.status === 'active') || []
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build the config.yml body written by the wizard.
|
|
21
|
+
*
|
|
22
|
+
* Built-in providers get **no** `models:` block on purpose: `mergeProviders`
|
|
23
|
+
* (config/loader.ts) treats a supplied `models:` list as a wholesale
|
|
24
|
+
* replacement of the built-in list, so writing bare `- id:` lines there would
|
|
25
|
+
* strip `status` (→ the model picker shows "no active models"),
|
|
26
|
+
* `contextWindow`, `maxOutput` and `vision` from every built-in model.
|
|
27
|
+
* `models:` is written only for providers whose built-in list is empty
|
|
28
|
+
* (Ollama), where it must carry every model field in full.
|
|
29
|
+
*/
|
|
30
|
+
export function buildConfigYaml(
|
|
31
|
+
providerId: string,
|
|
32
|
+
modelId: string,
|
|
33
|
+
storedKey: string,
|
|
34
|
+
ollamaModels: ModelInfo[] = [],
|
|
35
|
+
): string {
|
|
36
|
+
const provider = DEFAULT_PROVIDERS.find((p) => p.id === providerId)
|
|
37
|
+
const hasBuiltInModels = (provider?.models.length ?? 0) > 0
|
|
38
|
+
|
|
39
|
+
const lines = [
|
|
40
|
+
'# Mipham Code Configuration',
|
|
41
|
+
`# Generated by Config Wizard — ${new Date().toISOString()}`,
|
|
42
|
+
'',
|
|
43
|
+
'version: 1',
|
|
44
|
+
`defaultProvider: ${providerId}`,
|
|
45
|
+
`defaultModel: ${modelId}`,
|
|
46
|
+
'permission: default',
|
|
47
|
+
'',
|
|
48
|
+
'providers:',
|
|
49
|
+
` - id: ${providerId}`,
|
|
50
|
+
` name: ${provider?.name || providerId}`,
|
|
51
|
+
` protocol: ${provider?.protocol || 'openai-compatible'}`,
|
|
52
|
+
...(provider?.baseUrl ? [` baseUrl: ${provider.baseUrl}`] : []),
|
|
53
|
+
` apiKey: ${storedKey}`,
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
if (!hasBuiltInModels) {
|
|
57
|
+
lines.push(' models:')
|
|
58
|
+
for (const m of ollamaModels) {
|
|
59
|
+
lines.push(
|
|
60
|
+
` - id: ${m.id}`,
|
|
61
|
+
` name: ${m.name}`,
|
|
62
|
+
` providerId: ${m.providerId}`,
|
|
63
|
+
` contextWindow: ${m.contextWindow}`,
|
|
64
|
+
` maxOutput: ${m.maxOutput}`,
|
|
65
|
+
` vision: ${m.vision}`,
|
|
66
|
+
` status: ${m.status}`,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return lines.join('\n')
|
|
72
|
+
}
|
|
@@ -44,21 +44,45 @@ const PATTERNS: Array<{ pattern: RegExp; reason: DerivableReason }> = [
|
|
|
44
44
|
]
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
47
|
+
* A section that already points at another doc has been disclosed — there is
|
|
48
|
+
* nothing left to exclude, so the audit stays silent about it.
|
|
49
|
+
*/
|
|
50
|
+
const DOC_POINTER = /\]\([^)\s]*\.md(?:[#?][^)\s]*)?\)/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Return `##`/`###` headings whose title matches a derivable-content pattern and
|
|
54
|
+
* whose body is not already disclosed behind a `.md` pointer, in document order.
|
|
55
|
+
* Headings without a match are ignored.
|
|
49
56
|
*/
|
|
50
57
|
export function findDerivableSections(content: string): DerivableSection[] {
|
|
51
58
|
const found: DerivableSection[] = []
|
|
59
|
+
let heading: string | null = null
|
|
60
|
+
let reason: DerivableReason | null = null
|
|
61
|
+
let body: string[] = []
|
|
62
|
+
|
|
63
|
+
const flush = () => {
|
|
64
|
+
if (heading !== null && reason !== null && !DOC_POINTER.test(body.join('\n'))) {
|
|
65
|
+
found.push({ heading, reason })
|
|
66
|
+
}
|
|
67
|
+
heading = null
|
|
68
|
+
reason = null
|
|
69
|
+
body = []
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
for (const line of content.split('\n')) {
|
|
53
73
|
const m = line.match(/^(#{2,3})\s+(.+?)\s*$/)
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
if (m) {
|
|
75
|
+
flush()
|
|
76
|
+
const title = m[2]!.trim()
|
|
77
|
+
const hit = PATTERNS.find(({ pattern }) => pattern.test(title))
|
|
78
|
+
if (hit) {
|
|
79
|
+
heading = title
|
|
80
|
+
reason = hit.reason
|
|
60
81
|
}
|
|
82
|
+
continue
|
|
61
83
|
}
|
|
84
|
+
if (heading !== null) body.push(line)
|
|
62
85
|
}
|
|
86
|
+
flush()
|
|
63
87
|
return found
|
|
64
88
|
}
|
package/src/core/instructions.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'node:fs'
|
|
2
2
|
import { join, resolve, relative, sep, isAbsolute } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
3
4
|
import { execSync } from 'node:child_process'
|
|
4
5
|
import { parse as parseYaml } from 'yaml'
|
|
5
6
|
import type { InstructionFile } from '../shared/index.ts'
|
|
@@ -126,7 +127,7 @@ export class InstructionsLoader {
|
|
|
126
127
|
})
|
|
127
128
|
|
|
128
129
|
// Tier 3: 用户层 ~/.mipham/USER.md
|
|
129
|
-
const home =
|
|
130
|
+
const home = homedir()
|
|
130
131
|
this.tryLoad(join(home, '.mipham', 'USER.md'), 'user')
|
|
131
132
|
|
|
132
133
|
// CRSI 教训召回:读 crsi-lessons.md 提取精华,注入系统提示(只写不读 → 写后召回)
|
package/src/core/rule-engine.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExperienceRule } from '../agent/experience-rules.js'
|
|
2
2
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs'
|
|
3
3
|
import { join, dirname } from 'node:path'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
4
5
|
import { MANAGED_RULES } from './crsi-managed-rules'
|
|
5
6
|
|
|
6
7
|
export interface ToolRule {
|
|
@@ -61,7 +62,7 @@ export class ExperienceRuleEngine {
|
|
|
61
62
|
private rules: ToolRule[]
|
|
62
63
|
private storePath: string
|
|
63
64
|
|
|
64
|
-
constructor(storeDir: string = join(
|
|
65
|
+
constructor(storeDir: string = join(homedir(), '.mipham', 'rule-engine')) {
|
|
65
66
|
this.rules = [...BUILTIN_RULES, ...MANAGED_RULES].map((r) => ({ ...r }))
|
|
66
67
|
this.storePath = join(storeDir, 'rules.json')
|
|
67
68
|
this.load()
|
package/src/core/session-log.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { appendFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
2
3
|
import { join } from 'node:path'
|
|
3
4
|
import { createHash } from 'node:crypto'
|
|
4
5
|
import type { Message, ToolUseContent, ToolResultContent, ToolResult } from '../shared/types'
|
|
@@ -101,7 +102,7 @@ export function deriveMessages(events: SessionEvent[]): Message[] {
|
|
|
101
102
|
return out
|
|
102
103
|
}
|
|
103
104
|
|
|
104
|
-
const HOME =
|
|
105
|
+
const HOME = homedir()
|
|
105
106
|
const LOG_DIR = join(HOME, '.mipham', 'sessions')
|
|
106
107
|
|
|
107
108
|
/** 将会话名消毒为安全文件名(与 SessionStore 共用;防路径穿越)。 */
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
existsSync,
|
|
9
9
|
statSync,
|
|
10
10
|
} from 'node:fs'
|
|
11
|
+
import { homedir } from 'node:os'
|
|
11
12
|
import { join } from 'node:path'
|
|
12
13
|
import { createHash } from 'node:crypto'
|
|
13
14
|
import type { Message } from '../shared/types'
|
|
@@ -28,7 +29,7 @@ export interface StoredSession {
|
|
|
28
29
|
messages: Message[]
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const HOME =
|
|
32
|
+
const HOME = homedir()
|
|
32
33
|
const SESSIONS_DIR = join(HOME, '.mipham', 'sessions')
|
|
33
34
|
const INDEX_FILE = join(SESSIONS_DIR, '.index.json')
|
|
34
35
|
const SUMMARIES_DIR = join(SESSIONS_DIR, '.summaries')
|
package/src/daemon/database.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// apps/cli/src/daemon/database.ts
|
|
2
2
|
import { Database } from 'bun:sqlite'
|
|
3
3
|
import { readdirSync, readFileSync, existsSync } from 'node:fs'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
4
5
|
import { join } from 'node:path'
|
|
5
6
|
import type {
|
|
6
7
|
DaemonSession,
|
|
@@ -339,7 +340,7 @@ export class DaemonDatabase {
|
|
|
339
340
|
// ── Migration ──────────────────────────────────────────────
|
|
340
341
|
|
|
341
342
|
migrateFromJsonl(): number {
|
|
342
|
-
const sessionsDir = join(
|
|
343
|
+
const sessionsDir = join(homedir(), '.mipham', 'sessions')
|
|
343
344
|
|
|
344
345
|
if (!existsSync(sessionsDir)) return 0
|
|
345
346
|
|
|
@@ -901,6 +901,7 @@
|
|
|
901
901
|
"mode_title": "Choose connection method:",
|
|
902
902
|
"mode_cloud": "🌐 Cloud AI models (recommended)",
|
|
903
903
|
"mode_local": "🖥️ Local models (Ollama)",
|
|
904
|
+
"mode_cloud_hint": " {count} cloud providers built in",
|
|
904
905
|
"mode_local_hint": " Requires Ollama installed with models downloaded",
|
|
905
906
|
"nav_hint": "↑↓ select · Enter confirm · Esc back",
|
|
906
907
|
"provider_title": "Choose AI model provider:",
|
|
@@ -901,6 +901,7 @@
|
|
|
901
901
|
"mode_title": "选择模型连接方式:",
|
|
902
902
|
"mode_cloud": "🌐 云端 AI 模型(推荐)",
|
|
903
903
|
"mode_local": "🖥️ 本地模型(Ollama)",
|
|
904
|
+
"mode_cloud_hint": " 内置 {count} 家云端供应商",
|
|
904
905
|
"mode_local_hint": " 需要提前安装 Ollama 并下载模型",
|
|
905
906
|
"nav_hint": "↑↓ 选择 · Enter 确认 · Esc 返回",
|
|
906
907
|
"provider_title": "选择 AI 模型提供商:",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export const PACKAGE_NAME = '@miphamai/cli' as const
|
|
10
10
|
|
|
11
11
|
/** 当前发布版本 */
|
|
12
|
-
export const PACKAGE_VERSION = '0.81.
|
|
12
|
+
export const PACKAGE_VERSION = '0.81.2' as const
|
|
13
13
|
|
|
14
14
|
/** npm install 全局安装命令 */
|
|
15
15
|
export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
3
4
|
import { parse as parseYaml, stringify } from 'yaml'
|
|
4
5
|
import type { ToolDefinition } from '../../shared/index.ts'
|
|
5
6
|
|
|
6
|
-
const MIPHAM_DIR = join(
|
|
7
|
+
const MIPHAM_DIR = join(homedir(), '.mipham')
|
|
7
8
|
const USER_CONFIG = join(MIPHAM_DIR, 'config.yml')
|
|
8
9
|
|
|
9
10
|
export const configTool: ToolDefinition = {
|
package/src/ui/chat.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import React, { useMemo } from 'react'
|
|
|
2
2
|
import { Box, Text } from 'ink'
|
|
3
3
|
import { useI18n } from '../i18n-context'
|
|
4
4
|
import type { ChatMessage } from './app'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
5
6
|
|
|
6
7
|
interface ChatPanelProps {
|
|
7
8
|
messages: ChatMessage[]
|
|
@@ -11,7 +12,7 @@ interface ChatPanelProps {
|
|
|
11
12
|
/** Format cwd for display: replace HOME with ~, truncate if too long */
|
|
12
13
|
function displayCwd(): string {
|
|
13
14
|
const cwd = process.cwd()
|
|
14
|
-
const home =
|
|
15
|
+
const home = homedir()
|
|
15
16
|
if (home && cwd.startsWith(home)) {
|
|
16
17
|
return '~' + cwd.slice(home.length)
|
|
17
18
|
}
|
package/src/ui/commands.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { InstructionsLoader } from '../core/instructions'
|
|
|
18
18
|
import { findDerivableSections, DERIVABLE_HINTS } from '../core/claude-md-audit'
|
|
19
19
|
import { fixDoctor, fixConfig, fixCache, selectRepoClaudeFiles } from '../core/fix'
|
|
20
20
|
import { fixCodeTarget } from '../core/fix-code'
|
|
21
|
+
import { homedir } from 'node:os'
|
|
21
22
|
import { runCrsiModification, approvePending, rejectPending, hasPending } from '../core/crsi-modify'
|
|
22
23
|
import {
|
|
23
24
|
produceCrsiProposal,
|
|
@@ -2573,7 +2574,7 @@ const installPluginCmd: CommandHandler = async (ctx, args) => {
|
|
|
2573
2574
|
|
|
2574
2575
|
if (isLocalPath) {
|
|
2575
2576
|
const { resolve } = await import('node:path')
|
|
2576
|
-
const resolved = resolve(target.replace(/^~/,
|
|
2577
|
+
const resolved = resolve(target.replace(/^~/, homedir()))
|
|
2577
2578
|
result = manager.install(resolved)
|
|
2578
2579
|
} else {
|
|
2579
2580
|
// Treat as npm package name
|
|
@@ -2882,7 +2883,7 @@ const loopCmd: CommandHandler = async (ctx, args) => {
|
|
|
2882
2883
|
const { scaffoldLoopKit } = await import('../commands/loop-scaffold')
|
|
2883
2884
|
const { created, skipped } = scaffoldLoopKit(targetPath)
|
|
2884
2885
|
const { resolve } = await import('node:path')
|
|
2885
|
-
const resolved = resolve(targetPath.replace(/^~/,
|
|
2886
|
+
const resolved = resolve(targetPath.replace(/^~/, homedir()))
|
|
2886
2887
|
|
|
2887
2888
|
const lines: string[] = ['── LoopKit Vault Created ──', '', `Location: ${resolved}`, '']
|
|
2888
2889
|
|
|
@@ -3721,7 +3722,7 @@ const cdCmd: CommandHandler = async (ctx, args) => {
|
|
|
3721
3722
|
|
|
3722
3723
|
const { existsSync } = await import('node:fs')
|
|
3723
3724
|
const { resolve } = await import('node:path')
|
|
3724
|
-
const resolved = resolve(target.replace(/^~/,
|
|
3725
|
+
const resolved = resolve(target.replace(/^~/, homedir()))
|
|
3725
3726
|
|
|
3726
3727
|
if (!existsSync(resolved)) {
|
|
3727
3728
|
const suggestions = suggestDirectories(resolved)
|
|
@@ -4118,7 +4119,7 @@ const memoryCmd: CommandHandler = async (ctx, args) => {
|
|
|
4118
4119
|
const { existsSync, readdirSync, readFileSync, statSync } = await import('node:fs')
|
|
4119
4120
|
const { join } = await import('node:path')
|
|
4120
4121
|
|
|
4121
|
-
const home =
|
|
4122
|
+
const home = homedir()
|
|
4122
4123
|
const memoryDir = join(home, '.mipham', 'memory')
|
|
4123
4124
|
|
|
4124
4125
|
// /memory gc — 记忆卫生:归档「0 召回 + 过期」的 auto-* 记忆(手写只报告)
|
|
@@ -4320,7 +4321,7 @@ const workflowsCmd: CommandHandler = async () => {
|
|
|
4320
4321
|
|
|
4321
4322
|
const locations = [
|
|
4322
4323
|
join(process.cwd(), '.claude', 'workflows'),
|
|
4323
|
-
join(
|
|
4324
|
+
join(homedir(), '.claude', 'workflows'),
|
|
4324
4325
|
]
|
|
4325
4326
|
|
|
4326
4327
|
const lines: string[] = ['─ Workflows ─', '']
|
|
@@ -4418,7 +4419,7 @@ const workflowRunCmd = async (name: string): Promise<CommandResult> => {
|
|
|
4418
4419
|
|
|
4419
4420
|
const locations = [
|
|
4420
4421
|
join(process.cwd(), '.claude', 'workflows'),
|
|
4421
|
-
join(
|
|
4422
|
+
join(homedir(), '.claude', 'workflows'),
|
|
4422
4423
|
]
|
|
4423
4424
|
|
|
4424
4425
|
for (const loc of locations) {
|
|
@@ -4739,7 +4740,7 @@ const logoutCmd: CommandHandler = async () => {
|
|
|
4739
4740
|
const { existsSync } = await import('node:fs')
|
|
4740
4741
|
const { join } = await import('node:path')
|
|
4741
4742
|
|
|
4742
|
-
const home =
|
|
4743
|
+
const home = homedir()
|
|
4743
4744
|
const userConfig = join(home, '.mipham', 'config.yml')
|
|
4744
4745
|
const hasUserConfig = existsSync(userConfig)
|
|
4745
4746
|
|
package/src/ui/config-wizard.tsx
CHANGED
|
@@ -20,6 +20,7 @@ import { join } from 'node:path'
|
|
|
20
20
|
import { homedir } from 'node:os'
|
|
21
21
|
import { execSync } from 'node:child_process'
|
|
22
22
|
import { getCredentialKey, encryptApiKey } from '../config/credential-crypto'
|
|
23
|
+
import { CLOUD_PROVIDERS, getActiveModels, buildConfigYaml } from '../config/wizard-config'
|
|
23
24
|
|
|
24
25
|
// ── Types ──
|
|
25
26
|
|
|
@@ -32,24 +33,15 @@ interface Props {
|
|
|
32
33
|
|
|
33
34
|
// ── Constants ──
|
|
34
35
|
|
|
35
|
-
const CLOUD_PROVIDERS = DEFAULT_PROVIDERS.filter((p) => p.id !== 'ollama' && p.id !== 'mipham')
|
|
36
|
-
|
|
37
36
|
const SELECTED_COLOR = 'cyan'
|
|
38
37
|
|
|
39
38
|
// ── Helpers ──
|
|
40
39
|
|
|
41
|
-
function getActiveModels(providerId: string): ModelInfo[] {
|
|
42
|
-
const provider = DEFAULT_PROVIDERS.find((p) => p.id === providerId)
|
|
43
|
-
return provider?.models.filter((m) => m.status === 'active') || []
|
|
44
|
-
}
|
|
45
|
-
|
|
46
40
|
function writeConfigFile(providerId: string, modelId: string, apiKey: string): void {
|
|
47
41
|
const configDir = join(homedir(), '.mipham')
|
|
48
42
|
mkdirSync(configDir, { recursive: true })
|
|
49
43
|
|
|
50
|
-
const
|
|
51
|
-
const models =
|
|
52
|
-
providerId === 'ollama' ? getOllamaModelListForConfig() : getActiveModels(providerId)
|
|
44
|
+
const models = providerId === 'ollama' ? getOllamaModelListForConfig() : []
|
|
53
45
|
|
|
54
46
|
function getOllamaModelListForConfig(): ModelInfo[] {
|
|
55
47
|
// 与 getOllamaModelList 逻辑一致,但用于配置写入
|
|
@@ -97,26 +89,10 @@ function writeConfigFile(providerId: string, modelId: string, apiKey: string): v
|
|
|
97
89
|
}
|
|
98
90
|
const storedKey = encryptApiKey(apiKey, getCredentialKey(configDir))
|
|
99
91
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
'version: 1',
|
|
105
|
-
`defaultProvider: ${providerId}`,
|
|
106
|
-
`defaultModel: ${modelId}`,
|
|
107
|
-
'permission: default',
|
|
108
|
-
'',
|
|
109
|
-
'providers:',
|
|
110
|
-
` - id: ${providerId}`,
|
|
111
|
-
` name: ${provider?.name || providerId}`,
|
|
112
|
-
` protocol: ${provider?.protocol || 'openai-compatible'}`,
|
|
113
|
-
...(provider?.baseUrl ? [` baseUrl: ${provider.baseUrl}`] : []),
|
|
114
|
-
` apiKey: ${storedKey}`,
|
|
115
|
-
` models:`,
|
|
116
|
-
...models.map((m) => ` - id: ${m.id}`),
|
|
117
|
-
]
|
|
118
|
-
|
|
119
|
-
atomicWriteFileSync(join(configDir, 'config.yml'), lines.join('\n'))
|
|
92
|
+
atomicWriteFileSync(
|
|
93
|
+
join(configDir, 'config.yml'),
|
|
94
|
+
buildConfigYaml(providerId, modelId, storedKey, models),
|
|
95
|
+
)
|
|
120
96
|
}
|
|
121
97
|
|
|
122
98
|
function checkOllama(): { installed: boolean; running: boolean; models: string[] } {
|
|
@@ -409,7 +385,9 @@ export function ConfigWizard({ onComplete, onSkip }: Props) {
|
|
|
409
385
|
{cursor === 0 ? '▶' : ' '} {t('ui.wizard.mode_cloud')}
|
|
410
386
|
</Text>
|
|
411
387
|
</Box>
|
|
412
|
-
<Text dimColor>
|
|
388
|
+
<Text dimColor>
|
|
389
|
+
{t('ui.wizard.mode_cloud_hint', { count: String(CLOUD_PROVIDERS.length) })}
|
|
390
|
+
</Text>
|
|
413
391
|
|
|
414
392
|
<Box marginTop={2} marginBottom={1}>
|
|
415
393
|
<Text color={cursor === 1 ? SELECTED_COLOR : undefined}>
|