@miphamai/cli 0.69.0 → 0.70.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.
@@ -14,6 +14,8 @@ import { McpClient } from '../mcp/client'
14
14
  import { buildCapabilityReport } from '../core/capability-inventory'
15
15
  import { InstructionsLoader } from '../core/instructions'
16
16
  import { findDerivableSections, DERIVABLE_HINTS } from '../core/claude-md-audit'
17
+ import { fixDoctor, fixConfig, fixCache, selectRepoClaudeFiles } from '../core/fix'
18
+ import { fixCodeTarget } from '../core/fix-code'
17
19
  import { runCrsiModification, approvePending, rejectPending, hasPending } from '../core/crsi-modify'
18
20
  import {
19
21
  produceCrsiProposal,
@@ -32,7 +34,11 @@ import {
32
34
  import { prefilterProposal } from '../core/proposal-guard'
33
35
  import { runEval, appendEvalScore } from '../core/eval-harness'
34
36
  import { listRewardFns } from '../core/reward-fn'
35
- import { runTaskPerformance, measureSkillDeltaRepeated } from '../core/task-performance'
37
+ import {
38
+ runTaskPerformance,
39
+ measureSkillDeltaRepeated,
40
+ stripCodeFences,
41
+ } from '../core/task-performance'
36
42
  import { randomUUID } from 'node:crypto'
37
43
  import {
38
44
  buildImprovementReport,
@@ -45,7 +51,7 @@ import {
45
51
  } from '../core/improvement-track'
46
52
  import { NPM_UPDATE_COMMAND, PACKAGE_VERSION, COAUTHOR_TRAILER } from '../shared/index.ts'
47
53
  import { getPreference } from '../config/preferences'
48
- import { loadCrossSessionConfig } from '../config/loader'
54
+ import { loadCrossSessionConfig, tryRestoreFromBackup } from '../config/loader'
49
55
  import { getMemoryManager } from '../core/memory/memory-loader'
50
56
  import { stripIndent } from './strip-indent.js'
51
57
  import { createT } from '../i18n-core/t'
@@ -2359,9 +2365,9 @@ const installSkillCmd: CommandHandler = async (ctx, args) => {
2359
2365
  let result: { success: boolean; name: string; message: string }
2360
2366
 
2361
2367
  if (target.startsWith('http://') || target.startsWith('https://')) {
2362
- result = installSkillFromUrl(target, marketplaceConfig)
2368
+ result = await installSkillFromUrl(target, marketplaceConfig)
2363
2369
  } else {
2364
- result = installSkill(target, marketplaceConfig)
2370
+ result = await installSkill(target, marketplaceConfig)
2365
2371
  }
2366
2372
 
2367
2373
  return {
@@ -2383,6 +2389,83 @@ const removeSkillCmd: CommandHandler = async (_ctx, args) => {
2383
2389
  }
2384
2390
  }
2385
2391
 
2392
+ const marketplaceCmd: CommandHandler = async (_ctx, args) => {
2393
+ const {
2394
+ readMarketplaces,
2395
+ addMarketplace,
2396
+ removeMarketplace,
2397
+ isValidMarketplaceRef,
2398
+ MARKETPLACES_PATH,
2399
+ } = await import('../skills/marketplace')
2400
+ const { readFileSync, writeFileSync, existsSync, mkdirSync } = await import('node:fs')
2401
+ const { dirname } = await import('node:path')
2402
+
2403
+ const readSources = () =>
2404
+ readMarketplaces((p) => (existsSync(p) ? readFileSync(p, 'utf-8') : null))
2405
+
2406
+ const sub = args[0]
2407
+ const ref = args[1]
2408
+
2409
+ if (!sub || sub === 'list') {
2410
+ const sources = readSources()
2411
+ const lines = ['── Marketplace Sources ──', '']
2412
+ for (const s of sources) lines.push(` ${s.owner}/${s.repo}`)
2413
+ lines.push(
2414
+ '',
2415
+ 'Add: /marketplace add <owner>/<repo>',
2416
+ 'Remove: /marketplace remove <owner>/<repo>',
2417
+ )
2418
+ return { content: lines.join('\n') }
2419
+ }
2420
+
2421
+ if (sub === 'add' || sub === 'remove') {
2422
+ if (!ref) return { content: `Usage: /marketplace ${sub} <owner>/<repo>` }
2423
+ const [owner, repo] = ref.split('/')
2424
+ if (!owner || !repo || !isValidMarketplaceRef(owner, repo)) {
2425
+ return { content: `❌ Invalid marketplace ref "${ref}". Expected <owner>/<repo>.` }
2426
+ }
2427
+ const current = readSources()
2428
+ const result =
2429
+ sub === 'add' ? addMarketplace(current, owner, repo) : removeMarketplace(current, owner, repo)
2430
+ const changed = 'added' in result ? result.added : result.removed
2431
+ try {
2432
+ mkdirSync(dirname(MARKETPLACES_PATH), { recursive: true })
2433
+ writeFileSync(MARKETPLACES_PATH, JSON.stringify(result.sources, null, 2), 'utf-8')
2434
+ } catch (err) {
2435
+ return { content: `❌ Failed to save marketplaces: ${String(err)}` }
2436
+ }
2437
+ if (sub === 'add') {
2438
+ return {
2439
+ content: changed ? `✅ Added ${owner}/${repo}` : `ℹ ${owner}/${repo} already exists`,
2440
+ }
2441
+ }
2442
+ return { content: changed ? `✅ Removed ${owner}/${repo}` : `ℹ ${owner}/${repo} not found` }
2443
+ }
2444
+
2445
+ return { content: 'Usage: /marketplace add|list|remove [owner/repo]' }
2446
+ }
2447
+
2448
+ const browseMarketplaceCmd: CommandHandler = async (_ctx) => {
2449
+ const { readMarketplaces, discoverSkills, downloadFile } = await import('../skills/marketplace')
2450
+ const { readFileSync, existsSync } = await import('node:fs')
2451
+
2452
+ const sources = readMarketplaces((p) => (existsSync(p) ? readFileSync(p, 'utf-8') : null))
2453
+ const lines = ['── Marketplace Skills ──', '']
2454
+ for (const source of sources) {
2455
+ lines.push(` ${source.owner}/${source.repo}:`)
2456
+ try {
2457
+ const skills = await discoverSkills(source, globalThis.fetch, downloadFile)
2458
+ if (skills.length === 0) lines.push(' (no skills found)')
2459
+ for (const s of skills) lines.push(` ${s.name.padEnd(24)} ${s.description}`)
2460
+ } catch {
2461
+ lines.push(' (failed to discover — check network / rate limit)')
2462
+ }
2463
+ lines.push('')
2464
+ }
2465
+ lines.push('Install: /install-skill <name>')
2466
+ return { content: lines.join('\n') }
2467
+ }
2468
+
2386
2469
  // ═══════════════════════════════════════════════════════════════
2387
2470
  // Plugins — Community plugin marketplace + local management
2388
2471
  // ═══════════════════════════════════════════════════════════════
@@ -3105,6 +3188,191 @@ const doctorCmd: CommandHandler = async (ctx) => {
3105
3188
  return { content: lines.join('\n') }
3106
3189
  }
3107
3190
 
3191
+ const fixCmd: CommandHandler = async (ctx, args) => {
3192
+ const t = resolveT(ctx)
3193
+ const { readFileSync, writeFileSync } = await import('node:fs')
3194
+ const { join } = await import('node:path')
3195
+ const { homedir } = await import('node:os')
3196
+ const { parse: parseYaml } = await import('yaml')
3197
+
3198
+ const target = args.find((a) => a === 'doctor' || a === 'config' || a === 'cache' || a === 'test')
3199
+ const dryRun = args.includes('--dry-run')
3200
+ const apply = args.includes('--apply')
3201
+
3202
+ const lines: string[] = [t('commands.fix.title')]
3203
+ if (dryRun) lines.push(t('commands.fix.dryrun_banner'))
3204
+ lines.push('')
3205
+
3206
+ const readSafe = (p: string): string | null => {
3207
+ try {
3208
+ return readFileSync(p, 'utf-8')
3209
+ } catch {
3210
+ return null
3211
+ }
3212
+ }
3213
+
3214
+ if (!target || target === 'doctor') {
3215
+ const loader = new InstructionsLoader()
3216
+ loader.loadAll(process.cwd())
3217
+ const files = selectRepoClaudeFiles(loader.list())
3218
+ const result = fixDoctor(files, {
3219
+ read: readSafe,
3220
+ write: (p, c) => {
3221
+ if (!dryRun) writeFileSync(p, c, 'utf-8')
3222
+ },
3223
+ })
3224
+ if (result.fixed.length === 0) {
3225
+ lines.push(t('commands.fix.doctor_clean'))
3226
+ } else {
3227
+ for (const f of result.fixed) {
3228
+ lines.push(t('commands.fix.doctor_fixed', { path: f.path, added: f.added.join(', ') }))
3229
+ }
3230
+ }
3231
+ lines.push('')
3232
+ }
3233
+
3234
+ if (!target || target === 'config') {
3235
+ const home = homedir()
3236
+ const configPaths = [
3237
+ join(process.cwd(), '.mipham', 'config.yml'),
3238
+ join(home, '.mipham', 'config.yml'),
3239
+ ]
3240
+ const hookEngine = ctx.engine.getHookEngine?.()
3241
+ const result = fixConfig({
3242
+ configPaths,
3243
+ read: readSafe,
3244
+ parseYaml,
3245
+ restore: (p) => tryRestoreFromBackup(p),
3246
+ hookHealth: () =>
3247
+ hookEngine
3248
+ ? hookEngine.getHookHealth().map((h) => ({ key: h.key, disabled: h.health.disabled }))
3249
+ : [],
3250
+ reEnableHook: (k) => (hookEngine ? hookEngine.reEnableHook(k) : false),
3251
+ dryRun,
3252
+ })
3253
+ if (result.corruptConfigs.length === 0 && result.disabledHooks.length === 0) {
3254
+ lines.push(t('commands.fix.config_clean'))
3255
+ } else {
3256
+ for (const p of result.restoredConfigs) {
3257
+ lines.push(t('commands.fix.config_restored', { path: p }))
3258
+ }
3259
+ for (const p of result.corruptConfigs.filter((p) => !result.restoredConfigs.includes(p))) {
3260
+ lines.push(t('commands.fix.config_corrupt', { path: p }))
3261
+ }
3262
+ for (const k of result.reenabledHooks) {
3263
+ lines.push(t('commands.fix.hook_reenabled', { key: k }))
3264
+ }
3265
+ for (const k of result.disabledHooks.filter((k) => !result.reenabledHooks.includes(k))) {
3266
+ lines.push(t('commands.fix.hook_disabled', { key: k }))
3267
+ }
3268
+ }
3269
+ lines.push('')
3270
+ }
3271
+
3272
+ if (!target || target === 'cache') {
3273
+ const crsiDir = join(homedir(), '.mipham', 'crsi')
3274
+ const cacheFiles = ['eval-scores.jsonl', 'improvements.jsonl', 'prose-proposals.jsonl'].map(
3275
+ (f) => join(crsiDir, f),
3276
+ )
3277
+ const result = fixCache(
3278
+ cacheFiles,
3279
+ {
3280
+ read: readSafe,
3281
+ write: (p, c) => writeFileSync(p, c, 'utf-8'),
3282
+ },
3283
+ apply && !dryRun,
3284
+ )
3285
+ if (result.files.length === 0) {
3286
+ lines.push(t('commands.fix.cache_clean'))
3287
+ } else {
3288
+ for (const f of result.files) {
3289
+ lines.push(
3290
+ t('commands.fix.cache_found', { path: f.path, count: String(f.corruptLines.length) }),
3291
+ )
3292
+ }
3293
+ if (apply && !dryRun) {
3294
+ for (const c of result.cleaned) {
3295
+ lines.push(t('commands.fix.cache_cleaned', { path: c.path, count: String(c.removed) }))
3296
+ }
3297
+ } else {
3298
+ lines.push(t('commands.fix.cache_hint'))
3299
+ }
3300
+ }
3301
+ lines.push('')
3302
+ }
3303
+
3304
+ if (!target || target === 'test') {
3305
+ const testFile = args.find(
3306
+ (a) =>
3307
+ !a.startsWith('--') && a !== 'test' && a !== 'doctor' && a !== 'config' && a !== 'cache',
3308
+ )
3309
+ if (!testFile) {
3310
+ lines.push(t('commands.fix.test_usage'))
3311
+ } else {
3312
+ const llm = ctx.engine.getLlm() ?? ctx.engine.getRegistry()
3313
+ if (!llm) {
3314
+ lines.push(t('commands.fix.test_no_llm'))
3315
+ } else {
3316
+ const { execSync } = await import('node:child_process')
3317
+ const { dirname, resolve } = await import('node:path')
3318
+ const result = await fixCodeTarget(
3319
+ {
3320
+ runVitest: (file) => {
3321
+ try {
3322
+ const output = execSync(`pnpm vitest run '${file}'`, {
3323
+ cwd: process.cwd(),
3324
+ encoding: 'utf-8',
3325
+ stdio: ['ignore', 'pipe', 'pipe'],
3326
+ })
3327
+ return { exitCode: 0, output }
3328
+ } catch (e) {
3329
+ const err = e as { stdout?: string; stderr?: string }
3330
+ return { exitCode: 1, output: `${err.stdout ?? ''}${err.stderr ?? ''}` }
3331
+ }
3332
+ },
3333
+ readFile: readSafe,
3334
+ writeFile: (p, c) => writeFileSync(p, c, 'utf-8'),
3335
+ generateFix: async (prompt) => {
3336
+ let text = ''
3337
+ for await (const chunk of llm.chat({
3338
+ model: '',
3339
+ messages: [{ role: 'user' as const, content: prompt }],
3340
+ temperature: 0,
3341
+ })) {
3342
+ if (chunk.type === 'text' && chunk.content) text += chunk.content
3343
+ }
3344
+ return stripCodeFences(text)
3345
+ },
3346
+ resolveSourceFile: (tf, specifier) => {
3347
+ const base = resolve(dirname(tf), specifier)
3348
+ return /\.[cm]?[jt]sx?$/.test(base) ? base : `${base}.ts`
3349
+ },
3350
+ },
3351
+ testFile,
3352
+ { apply: apply && !dryRun, maxRetries: 3 },
3353
+ )
3354
+ if (result.fixed) {
3355
+ lines.push(
3356
+ t('commands.fix.test_fixed', {
3357
+ test: result.testFile,
3358
+ source: result.sourceFile ?? '-',
3359
+ attempts: String(result.attempts),
3360
+ }),
3361
+ )
3362
+ if (!apply) lines.push(t('commands.fix.test_hint'))
3363
+ } else {
3364
+ lines.push(
3365
+ t('commands.fix.test_failed', { test: result.testFile, detail: result.detail ?? '' }),
3366
+ )
3367
+ }
3368
+ }
3369
+ }
3370
+ lines.push('')
3371
+ }
3372
+
3373
+ return { content: lines.join('\n').trimEnd() }
3374
+ }
3375
+
3108
3376
  // ═══════════════════════════════════════════════════════════════
3109
3377
  // GitHub & Git Workflow Commands (Claude Code parity)
3110
3378
  // ═══════════════════════════════════════════════════════════════
@@ -3422,42 +3690,44 @@ const cdCmd: CommandHandler = async (ctx, args) => {
3422
3690
 
3423
3691
  const hooksCmd: CommandHandler = async (ctx) => {
3424
3692
  const t = resolveT(ctx)
3425
- const { existsSync, readdirSync } = await import('node:fs')
3426
- const { join } = await import('node:path')
3427
- const cwd = process.cwd()
3428
- const hooksDir = join(cwd, '.mipham', 'hooks')
3693
+ const { loadSettingsJson } = await import('../config/loader')
3694
+ const settingsJson = loadSettingsJson(process.cwd())
3429
3695
 
3430
- if (!existsSync(hooksDir)) {
3696
+ const hooks = settingsJson.hooks as Record<
3697
+ string,
3698
+ Array<{ matcher?: string; hooks: Array<{ type: string; command?: string }> }>
3699
+ >
3700
+
3701
+ const configured = Object.entries(hooks).filter(
3702
+ ([, entries]) => Array.isArray(entries) && entries.length > 0,
3703
+ )
3704
+
3705
+ if (configured.length === 0) {
3431
3706
  return { content: t('commands.hooks.no_hooks') }
3432
3707
  }
3433
3708
 
3434
- try {
3435
- const files = readdirSync(hooksDir).filter((f) => f.endsWith('.sh'))
3436
- const lines: string[] = [
3437
- t('commands.hooks.title'),
3438
- '',
3439
- t('commands.hooks.location', { path: hooksDir }),
3440
- '',
3441
- ]
3709
+ const lines: string[] = [
3710
+ t('commands.hooks.title'),
3711
+ '',
3712
+ t('commands.hooks.location', { path: '.mipham/settings.json' }),
3713
+ '',
3714
+ ]
3442
3715
 
3443
- for (const f of files) {
3444
- try {
3445
- const stat = (await import('node:fs')).statSync(join(hooksDir, f))
3446
- const isExec = (stat.mode & 0o111) !== 0
3447
- lines.push(
3448
- ` ${isExec ? '✅' : '⚠️'} ${f}${isExec ? '' : ' (not executable — run chmod +x)'}`,
3449
- )
3450
- } catch {
3451
- lines.push(` 📄 ${f}`)
3716
+ let count = 0
3717
+ for (const [eventName, entries] of configured) {
3718
+ for (const entry of entries) {
3719
+ for (const hook of entry.hooks) {
3720
+ count++
3721
+ const matcher = entry.matcher || '*'
3722
+ const cmd = hook.type === 'command' && hook.command ? hook.command : hook.type
3723
+ lines.push(` ${eventName} [${matcher}] → ${cmd}`)
3452
3724
  }
3453
3725
  }
3454
-
3455
- lines.push('')
3456
- lines.push(t('commands.hooks.found', { count: String(files.length) }))
3457
- return { content: lines.join('\n') }
3458
- } catch {
3459
- return { content: t('commands.hooks.error') }
3460
3726
  }
3727
+
3728
+ lines.push('')
3729
+ lines.push(t('commands.hooks.found', { count: String(count) }))
3730
+ return { content: lines.join('\n') }
3461
3731
  }
3462
3732
 
3463
3733
  const hooksHealthCmd: CommandHandler = (ctx) => {
@@ -4868,6 +5138,8 @@ const commandsListCmd: CommandHandler = () => {
4868
5138
  '/reload-skills': 'Tools & Skills',
4869
5139
  '/browse-skills': 'Tools & Skills',
4870
5140
  '/install-skill': 'Tools & Skills',
5141
+ '/marketplace': 'Tools & Skills',
5142
+ '/browse-marketplace': 'Tools & Skills',
4871
5143
  '/remove-skill': 'Tools & Skills',
4872
5144
  '/mcp': 'Tools & Skills',
4873
5145
  '/plugins': 'Plugins',
@@ -5028,6 +5300,8 @@ registry.set('/skills', skillsCmd)
5028
5300
  registry.set('/reload-skills', reloadSkillsCmd)
5029
5301
  registry.set('/browse-skills', browseSkillsCmd)
5030
5302
  registry.set('/install-skill', installSkillCmd)
5303
+ registry.set('/marketplace', marketplaceCmd)
5304
+ registry.set('/browse-marketplace', browseMarketplaceCmd)
5031
5305
  registry.set('/remove-skill', removeSkillCmd)
5032
5306
  registry.set('/plugins', pluginsCmd)
5033
5307
  registry.set('/browse-plugins', browsePluginsCmd)
@@ -5079,6 +5353,7 @@ registry.set('/pr-comments', prCommentsCmd)
5079
5353
 
5080
5354
  // Session Management
5081
5355
  registry.set('/doctor', doctorCmd)
5356
+ registry.set('/fix', fixCmd)
5082
5357
  registry.set('/export', exportCmd)
5083
5358
  registry.set('/resume', resumeCmd)
5084
5359
  registry.set('/resume last', resumeLastCmd)
@@ -5183,6 +5458,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
5183
5458
  '/save': 'Save conversation to Obsidian wiki (skill: save-to-wiki)',
5184
5459
  '/export': 'Export conversation to file',
5185
5460
  '/doctor': 'System diagnostics',
5461
+ '/fix': 'Deterministic self-repair: doctor/config/cache',
5186
5462
  '/dream': 'Background memory consolidation',
5187
5463
  '/constitution': 'View or reload constitutional principles',
5188
5464
  '/bug-report': 'Generate diagnostic report for GitHub Issues',
@@ -5212,6 +5488,8 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
5212
5488
  '/reload-skills': 'Reload all skills',
5213
5489
  '/browse-skills': 'Browse community skill marketplace',
5214
5490
  '/install-skill': 'Install a skill by name or URL',
5491
+ '/marketplace': 'Manage marketplace sources (add/list/remove)',
5492
+ '/browse-marketplace': 'Browse skills across all marketplace sources',
5215
5493
  '/remove-skill': 'Remove an installed skill',
5216
5494
  '/mcp': 'MCP server status',
5217
5495
  '/plugins': 'List installed plugins',
@@ -5294,7 +5572,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
5294
5572
  '/stats': 'Show session usage statistics',
5295
5573
  '/summary': 'Generate session summary',
5296
5574
  '/cd': 'Change session working directory',
5297
- '/hooks': 'Manage lifecycle hook scripts',
5575
+ '/hooks': 'List configured hooks from settings.json',
5298
5576
  '/hooks health': 'Check hook health — see failures, disabled hooks, recovery status',
5299
5577
  '/hooks enable': 'Manually re-enable a hook that was auto-disabled after repeated failures',
5300
5578
  '/batch': 'Apply changes across multiple files',