@miphamai/cli 0.69.0 → 0.71.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/package.json +1 -1
- package/src/commands/loop-scaffold.ts +15 -44
- package/src/config/loader.ts +56 -1
- package/src/core/claude-md-fix.ts +28 -0
- package/src/core/crsi-producer.ts +15 -0
- package/src/core/fix-code.ts +167 -0
- package/src/core/fix.ts +176 -0
- package/src/core/hooks-config.ts +10 -5
- package/src/core/hooks-executor.ts +84 -5
- package/src/core/improvement-track.ts +34 -6
- package/src/core/instructions.ts +2 -2
- package/src/core/memory/memory-manager.ts +21 -1
- package/src/core/memory/tfidf.ts +30 -0
- package/src/i18n-core/locales/en-US.json +26 -2
- package/src/i18n-core/locales/zh-CN.json +26 -2
- package/src/index.tsx +12 -0
- package/src/providers/openai-compat.ts +3 -1
- package/src/shared/package-info.ts +1 -1
- package/src/shared/types.ts +2 -0
- package/src/skills/community-registry.json +16 -0
- package/src/skills/marketplace.ts +233 -0
- package/src/skills/registry.ts +77 -63
- package/src/tools/exec/bash.ts +4 -1
- package/src/ui/commands.ts +345 -33
package/src/ui/commands.ts
CHANGED
|
@@ -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 {
|
|
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'
|
|
@@ -394,11 +400,20 @@ const statusCmd: CommandHandler = async (ctx) => {
|
|
|
394
400
|
const costCmd: CommandHandler = (ctx) => {
|
|
395
401
|
const t = resolveT(ctx)
|
|
396
402
|
const tokens = ctx.engine.getContext().getEstimatedTokens()
|
|
403
|
+
const cacheStatus = ctx.engine.getContext().getCacheStatus()
|
|
404
|
+
const cachedTokens = cacheStatus.cachedTokens
|
|
405
|
+
const hitRatio = tokens > 0 ? Math.min(1, cachedTokens / tokens) : 0
|
|
406
|
+
const uncachedTokens = Math.max(0, tokens - cachedTokens)
|
|
397
407
|
return {
|
|
398
408
|
content: stripIndent`
|
|
399
409
|
${t('commands.context_tokens.title')}
|
|
400
410
|
${t('commands.context_tokens.context_tokens')} ~${tokens.toLocaleString()} / 200,000
|
|
401
411
|
${t('commands.context_tokens.usage')} ${((tokens / 200_000) * 100).toFixed(1)}%
|
|
412
|
+
${t('commands.context_tokens.prompt_cache', {
|
|
413
|
+
cached: cachedTokens.toLocaleString(),
|
|
414
|
+
ratio: (hitRatio * 100).toFixed(1),
|
|
415
|
+
uncached: uncachedTokens.toLocaleString(),
|
|
416
|
+
})}
|
|
402
417
|
|
|
403
418
|
${t('commands.context_tokens.footer')}
|
|
404
419
|
`,
|
|
@@ -2359,9 +2374,9 @@ const installSkillCmd: CommandHandler = async (ctx, args) => {
|
|
|
2359
2374
|
let result: { success: boolean; name: string; message: string }
|
|
2360
2375
|
|
|
2361
2376
|
if (target.startsWith('http://') || target.startsWith('https://')) {
|
|
2362
|
-
result = installSkillFromUrl(target, marketplaceConfig)
|
|
2377
|
+
result = await installSkillFromUrl(target, marketplaceConfig)
|
|
2363
2378
|
} else {
|
|
2364
|
-
result = installSkill(target, marketplaceConfig)
|
|
2379
|
+
result = await installSkill(target, marketplaceConfig)
|
|
2365
2380
|
}
|
|
2366
2381
|
|
|
2367
2382
|
return {
|
|
@@ -2383,6 +2398,83 @@ const removeSkillCmd: CommandHandler = async (_ctx, args) => {
|
|
|
2383
2398
|
}
|
|
2384
2399
|
}
|
|
2385
2400
|
|
|
2401
|
+
const marketplaceCmd: CommandHandler = async (_ctx, args) => {
|
|
2402
|
+
const {
|
|
2403
|
+
readMarketplaces,
|
|
2404
|
+
addMarketplace,
|
|
2405
|
+
removeMarketplace,
|
|
2406
|
+
isValidMarketplaceRef,
|
|
2407
|
+
MARKETPLACES_PATH,
|
|
2408
|
+
} = await import('../skills/marketplace')
|
|
2409
|
+
const { readFileSync, writeFileSync, existsSync, mkdirSync } = await import('node:fs')
|
|
2410
|
+
const { dirname } = await import('node:path')
|
|
2411
|
+
|
|
2412
|
+
const readSources = () =>
|
|
2413
|
+
readMarketplaces((p) => (existsSync(p) ? readFileSync(p, 'utf-8') : null))
|
|
2414
|
+
|
|
2415
|
+
const sub = args[0]
|
|
2416
|
+
const ref = args[1]
|
|
2417
|
+
|
|
2418
|
+
if (!sub || sub === 'list') {
|
|
2419
|
+
const sources = readSources()
|
|
2420
|
+
const lines = ['── Marketplace Sources ──', '']
|
|
2421
|
+
for (const s of sources) lines.push(` ${s.owner}/${s.repo}`)
|
|
2422
|
+
lines.push(
|
|
2423
|
+
'',
|
|
2424
|
+
'Add: /marketplace add <owner>/<repo>',
|
|
2425
|
+
'Remove: /marketplace remove <owner>/<repo>',
|
|
2426
|
+
)
|
|
2427
|
+
return { content: lines.join('\n') }
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
if (sub === 'add' || sub === 'remove') {
|
|
2431
|
+
if (!ref) return { content: `Usage: /marketplace ${sub} <owner>/<repo>` }
|
|
2432
|
+
const [owner, repo] = ref.split('/')
|
|
2433
|
+
if (!owner || !repo || !isValidMarketplaceRef(owner, repo)) {
|
|
2434
|
+
return { content: `❌ Invalid marketplace ref "${ref}". Expected <owner>/<repo>.` }
|
|
2435
|
+
}
|
|
2436
|
+
const current = readSources()
|
|
2437
|
+
const result =
|
|
2438
|
+
sub === 'add' ? addMarketplace(current, owner, repo) : removeMarketplace(current, owner, repo)
|
|
2439
|
+
const changed = 'added' in result ? result.added : result.removed
|
|
2440
|
+
try {
|
|
2441
|
+
mkdirSync(dirname(MARKETPLACES_PATH), { recursive: true })
|
|
2442
|
+
writeFileSync(MARKETPLACES_PATH, JSON.stringify(result.sources, null, 2), 'utf-8')
|
|
2443
|
+
} catch (err) {
|
|
2444
|
+
return { content: `❌ Failed to save marketplaces: ${String(err)}` }
|
|
2445
|
+
}
|
|
2446
|
+
if (sub === 'add') {
|
|
2447
|
+
return {
|
|
2448
|
+
content: changed ? `✅ Added ${owner}/${repo}` : `ℹ ${owner}/${repo} already exists`,
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
return { content: changed ? `✅ Removed ${owner}/${repo}` : `ℹ ${owner}/${repo} not found` }
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
return { content: 'Usage: /marketplace add|list|remove [owner/repo]' }
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
const browseMarketplaceCmd: CommandHandler = async (_ctx) => {
|
|
2458
|
+
const { readMarketplaces, discoverSkills, downloadFile } = await import('../skills/marketplace')
|
|
2459
|
+
const { readFileSync, existsSync } = await import('node:fs')
|
|
2460
|
+
|
|
2461
|
+
const sources = readMarketplaces((p) => (existsSync(p) ? readFileSync(p, 'utf-8') : null))
|
|
2462
|
+
const lines = ['── Marketplace Skills ──', '']
|
|
2463
|
+
for (const source of sources) {
|
|
2464
|
+
lines.push(` ${source.owner}/${source.repo}:`)
|
|
2465
|
+
try {
|
|
2466
|
+
const skills = await discoverSkills(source, globalThis.fetch, downloadFile)
|
|
2467
|
+
if (skills.length === 0) lines.push(' (no skills found)')
|
|
2468
|
+
for (const s of skills) lines.push(` ${s.name.padEnd(24)} ${s.description}`)
|
|
2469
|
+
} catch {
|
|
2470
|
+
lines.push(' (failed to discover — check network / rate limit)')
|
|
2471
|
+
}
|
|
2472
|
+
lines.push('')
|
|
2473
|
+
}
|
|
2474
|
+
lines.push('Install: /install-skill <name>')
|
|
2475
|
+
return { content: lines.join('\n') }
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2386
2478
|
// ═══════════════════════════════════════════════════════════════
|
|
2387
2479
|
// Plugins — Community plugin marketplace + local management
|
|
2388
2480
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -3105,6 +3197,191 @@ const doctorCmd: CommandHandler = async (ctx) => {
|
|
|
3105
3197
|
return { content: lines.join('\n') }
|
|
3106
3198
|
}
|
|
3107
3199
|
|
|
3200
|
+
const fixCmd: CommandHandler = async (ctx, args) => {
|
|
3201
|
+
const t = resolveT(ctx)
|
|
3202
|
+
const { readFileSync, writeFileSync } = await import('node:fs')
|
|
3203
|
+
const { join } = await import('node:path')
|
|
3204
|
+
const { homedir } = await import('node:os')
|
|
3205
|
+
const { parse: parseYaml } = await import('yaml')
|
|
3206
|
+
|
|
3207
|
+
const target = args.find((a) => a === 'doctor' || a === 'config' || a === 'cache' || a === 'test')
|
|
3208
|
+
const dryRun = args.includes('--dry-run')
|
|
3209
|
+
const apply = args.includes('--apply')
|
|
3210
|
+
|
|
3211
|
+
const lines: string[] = [t('commands.fix.title')]
|
|
3212
|
+
if (dryRun) lines.push(t('commands.fix.dryrun_banner'))
|
|
3213
|
+
lines.push('')
|
|
3214
|
+
|
|
3215
|
+
const readSafe = (p: string): string | null => {
|
|
3216
|
+
try {
|
|
3217
|
+
return readFileSync(p, 'utf-8')
|
|
3218
|
+
} catch {
|
|
3219
|
+
return null
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
if (!target || target === 'doctor') {
|
|
3224
|
+
const loader = new InstructionsLoader()
|
|
3225
|
+
loader.loadAll(process.cwd())
|
|
3226
|
+
const files = selectRepoClaudeFiles(loader.list())
|
|
3227
|
+
const result = fixDoctor(files, {
|
|
3228
|
+
read: readSafe,
|
|
3229
|
+
write: (p, c) => {
|
|
3230
|
+
if (!dryRun) writeFileSync(p, c, 'utf-8')
|
|
3231
|
+
},
|
|
3232
|
+
})
|
|
3233
|
+
if (result.fixed.length === 0) {
|
|
3234
|
+
lines.push(t('commands.fix.doctor_clean'))
|
|
3235
|
+
} else {
|
|
3236
|
+
for (const f of result.fixed) {
|
|
3237
|
+
lines.push(t('commands.fix.doctor_fixed', { path: f.path, added: f.added.join(', ') }))
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
lines.push('')
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
if (!target || target === 'config') {
|
|
3244
|
+
const home = homedir()
|
|
3245
|
+
const configPaths = [
|
|
3246
|
+
join(process.cwd(), '.mipham', 'config.yml'),
|
|
3247
|
+
join(home, '.mipham', 'config.yml'),
|
|
3248
|
+
]
|
|
3249
|
+
const hookEngine = ctx.engine.getHookEngine?.()
|
|
3250
|
+
const result = fixConfig({
|
|
3251
|
+
configPaths,
|
|
3252
|
+
read: readSafe,
|
|
3253
|
+
parseYaml,
|
|
3254
|
+
restore: (p) => tryRestoreFromBackup(p),
|
|
3255
|
+
hookHealth: () =>
|
|
3256
|
+
hookEngine
|
|
3257
|
+
? hookEngine.getHookHealth().map((h) => ({ key: h.key, disabled: h.health.disabled }))
|
|
3258
|
+
: [],
|
|
3259
|
+
reEnableHook: (k) => (hookEngine ? hookEngine.reEnableHook(k) : false),
|
|
3260
|
+
dryRun,
|
|
3261
|
+
})
|
|
3262
|
+
if (result.corruptConfigs.length === 0 && result.disabledHooks.length === 0) {
|
|
3263
|
+
lines.push(t('commands.fix.config_clean'))
|
|
3264
|
+
} else {
|
|
3265
|
+
for (const p of result.restoredConfigs) {
|
|
3266
|
+
lines.push(t('commands.fix.config_restored', { path: p }))
|
|
3267
|
+
}
|
|
3268
|
+
for (const p of result.corruptConfigs.filter((p) => !result.restoredConfigs.includes(p))) {
|
|
3269
|
+
lines.push(t('commands.fix.config_corrupt', { path: p }))
|
|
3270
|
+
}
|
|
3271
|
+
for (const k of result.reenabledHooks) {
|
|
3272
|
+
lines.push(t('commands.fix.hook_reenabled', { key: k }))
|
|
3273
|
+
}
|
|
3274
|
+
for (const k of result.disabledHooks.filter((k) => !result.reenabledHooks.includes(k))) {
|
|
3275
|
+
lines.push(t('commands.fix.hook_disabled', { key: k }))
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
lines.push('')
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
if (!target || target === 'cache') {
|
|
3282
|
+
const crsiDir = join(homedir(), '.mipham', 'crsi')
|
|
3283
|
+
const cacheFiles = ['eval-scores.jsonl', 'improvements.jsonl', 'prose-proposals.jsonl'].map(
|
|
3284
|
+
(f) => join(crsiDir, f),
|
|
3285
|
+
)
|
|
3286
|
+
const result = fixCache(
|
|
3287
|
+
cacheFiles,
|
|
3288
|
+
{
|
|
3289
|
+
read: readSafe,
|
|
3290
|
+
write: (p, c) => writeFileSync(p, c, 'utf-8'),
|
|
3291
|
+
},
|
|
3292
|
+
apply && !dryRun,
|
|
3293
|
+
)
|
|
3294
|
+
if (result.files.length === 0) {
|
|
3295
|
+
lines.push(t('commands.fix.cache_clean'))
|
|
3296
|
+
} else {
|
|
3297
|
+
for (const f of result.files) {
|
|
3298
|
+
lines.push(
|
|
3299
|
+
t('commands.fix.cache_found', { path: f.path, count: String(f.corruptLines.length) }),
|
|
3300
|
+
)
|
|
3301
|
+
}
|
|
3302
|
+
if (apply && !dryRun) {
|
|
3303
|
+
for (const c of result.cleaned) {
|
|
3304
|
+
lines.push(t('commands.fix.cache_cleaned', { path: c.path, count: String(c.removed) }))
|
|
3305
|
+
}
|
|
3306
|
+
} else {
|
|
3307
|
+
lines.push(t('commands.fix.cache_hint'))
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
lines.push('')
|
|
3311
|
+
}
|
|
3312
|
+
|
|
3313
|
+
if (!target || target === 'test') {
|
|
3314
|
+
const testFile = args.find(
|
|
3315
|
+
(a) =>
|
|
3316
|
+
!a.startsWith('--') && a !== 'test' && a !== 'doctor' && a !== 'config' && a !== 'cache',
|
|
3317
|
+
)
|
|
3318
|
+
if (!testFile) {
|
|
3319
|
+
lines.push(t('commands.fix.test_usage'))
|
|
3320
|
+
} else {
|
|
3321
|
+
const llm = ctx.engine.getLlm() ?? ctx.engine.getRegistry()
|
|
3322
|
+
if (!llm) {
|
|
3323
|
+
lines.push(t('commands.fix.test_no_llm'))
|
|
3324
|
+
} else {
|
|
3325
|
+
const { execSync } = await import('node:child_process')
|
|
3326
|
+
const { dirname, resolve } = await import('node:path')
|
|
3327
|
+
const result = await fixCodeTarget(
|
|
3328
|
+
{
|
|
3329
|
+
runVitest: (file) => {
|
|
3330
|
+
try {
|
|
3331
|
+
const output = execSync(`pnpm vitest run '${file}'`, {
|
|
3332
|
+
cwd: process.cwd(),
|
|
3333
|
+
encoding: 'utf-8',
|
|
3334
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
3335
|
+
})
|
|
3336
|
+
return { exitCode: 0, output }
|
|
3337
|
+
} catch (e) {
|
|
3338
|
+
const err = e as { stdout?: string; stderr?: string }
|
|
3339
|
+
return { exitCode: 1, output: `${err.stdout ?? ''}${err.stderr ?? ''}` }
|
|
3340
|
+
}
|
|
3341
|
+
},
|
|
3342
|
+
readFile: readSafe,
|
|
3343
|
+
writeFile: (p, c) => writeFileSync(p, c, 'utf-8'),
|
|
3344
|
+
generateFix: async (prompt) => {
|
|
3345
|
+
let text = ''
|
|
3346
|
+
for await (const chunk of llm.chat({
|
|
3347
|
+
model: '',
|
|
3348
|
+
messages: [{ role: 'user' as const, content: prompt }],
|
|
3349
|
+
temperature: 0,
|
|
3350
|
+
})) {
|
|
3351
|
+
if (chunk.type === 'text' && chunk.content) text += chunk.content
|
|
3352
|
+
}
|
|
3353
|
+
return stripCodeFences(text)
|
|
3354
|
+
},
|
|
3355
|
+
resolveSourceFile: (tf, specifier) => {
|
|
3356
|
+
const base = resolve(dirname(tf), specifier)
|
|
3357
|
+
return /\.[cm]?[jt]sx?$/.test(base) ? base : `${base}.ts`
|
|
3358
|
+
},
|
|
3359
|
+
},
|
|
3360
|
+
testFile,
|
|
3361
|
+
{ apply: apply && !dryRun, maxRetries: 3 },
|
|
3362
|
+
)
|
|
3363
|
+
if (result.fixed) {
|
|
3364
|
+
lines.push(
|
|
3365
|
+
t('commands.fix.test_fixed', {
|
|
3366
|
+
test: result.testFile,
|
|
3367
|
+
source: result.sourceFile ?? '-',
|
|
3368
|
+
attempts: String(result.attempts),
|
|
3369
|
+
}),
|
|
3370
|
+
)
|
|
3371
|
+
if (!apply) lines.push(t('commands.fix.test_hint'))
|
|
3372
|
+
} else {
|
|
3373
|
+
lines.push(
|
|
3374
|
+
t('commands.fix.test_failed', { test: result.testFile, detail: result.detail ?? '' }),
|
|
3375
|
+
)
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
lines.push('')
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
return { content: lines.join('\n').trimEnd() }
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3108
3385
|
// ═══════════════════════════════════════════════════════════════
|
|
3109
3386
|
// GitHub & Git Workflow Commands (Claude Code parity)
|
|
3110
3387
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -3422,42 +3699,44 @@ const cdCmd: CommandHandler = async (ctx, args) => {
|
|
|
3422
3699
|
|
|
3423
3700
|
const hooksCmd: CommandHandler = async (ctx) => {
|
|
3424
3701
|
const t = resolveT(ctx)
|
|
3425
|
-
const {
|
|
3426
|
-
const
|
|
3427
|
-
|
|
3428
|
-
const
|
|
3702
|
+
const { loadSettingsJson } = await import('../config/loader')
|
|
3703
|
+
const settingsJson = loadSettingsJson(process.cwd())
|
|
3704
|
+
|
|
3705
|
+
const hooks = settingsJson.hooks as Record<
|
|
3706
|
+
string,
|
|
3707
|
+
Array<{ matcher?: string; hooks: Array<{ type: string; command?: string }> }>
|
|
3708
|
+
>
|
|
3709
|
+
|
|
3710
|
+
const configured = Object.entries(hooks).filter(
|
|
3711
|
+
([, entries]) => Array.isArray(entries) && entries.length > 0,
|
|
3712
|
+
)
|
|
3429
3713
|
|
|
3430
|
-
if (
|
|
3714
|
+
if (configured.length === 0) {
|
|
3431
3715
|
return { content: t('commands.hooks.no_hooks') }
|
|
3432
3716
|
}
|
|
3433
3717
|
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
'',
|
|
3441
|
-
]
|
|
3718
|
+
const lines: string[] = [
|
|
3719
|
+
t('commands.hooks.title'),
|
|
3720
|
+
'',
|
|
3721
|
+
t('commands.hooks.location', { path: '.mipham/settings.json' }),
|
|
3722
|
+
'',
|
|
3723
|
+
]
|
|
3442
3724
|
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
lines.push(` 📄 ${f}`)
|
|
3725
|
+
let count = 0
|
|
3726
|
+
for (const [eventName, entries] of configured) {
|
|
3727
|
+
for (const entry of entries) {
|
|
3728
|
+
for (const hook of entry.hooks) {
|
|
3729
|
+
count++
|
|
3730
|
+
const matcher = entry.matcher || '*'
|
|
3731
|
+
const cmd = hook.type === 'command' && hook.command ? hook.command : hook.type
|
|
3732
|
+
lines.push(` ${eventName} [${matcher}] → ${cmd}`)
|
|
3452
3733
|
}
|
|
3453
3734
|
}
|
|
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
3735
|
}
|
|
3736
|
+
|
|
3737
|
+
lines.push('')
|
|
3738
|
+
lines.push(t('commands.hooks.found', { count: String(count) }))
|
|
3739
|
+
return { content: lines.join('\n') }
|
|
3461
3740
|
}
|
|
3462
3741
|
|
|
3463
3742
|
const hooksHealthCmd: CommandHandler = (ctx) => {
|
|
@@ -3816,6 +4095,23 @@ const memoryCmd: CommandHandler = async (ctx, args) => {
|
|
|
3816
4095
|
return { content: lines.join('\n') }
|
|
3817
4096
|
}
|
|
3818
4097
|
|
|
4098
|
+
// /memory dedup — 只读近重复报告:列出 TF-IDF 余弦 > 阈值的近重复对,用户决定是否合并
|
|
4099
|
+
if (args[0]?.toLowerCase() === 'dedup') {
|
|
4100
|
+
const mm = getMemoryManager()
|
|
4101
|
+
const pairs = mm.listNearDuplicates()
|
|
4102
|
+
const lines: string[] = [t('commands.memories.dedup_title'), '']
|
|
4103
|
+
if (pairs.length === 0) {
|
|
4104
|
+
lines.push(t('commands.memories.dedup_none'))
|
|
4105
|
+
} else {
|
|
4106
|
+
for (const { a, b, similarity } of pairs) {
|
|
4107
|
+
lines.push(` ${a} ↔ ${b} (${(similarity * 100).toFixed(0)}%)`)
|
|
4108
|
+
}
|
|
4109
|
+
lines.push('')
|
|
4110
|
+
lines.push(t('commands.memories.dedup_footer'))
|
|
4111
|
+
}
|
|
4112
|
+
return { content: lines.join('\n') }
|
|
4113
|
+
}
|
|
4114
|
+
|
|
3819
4115
|
if (!existsSync(memoryDir)) {
|
|
3820
4116
|
return { content: t('commands.memories.no_memories') }
|
|
3821
4117
|
}
|
|
@@ -4225,6 +4521,14 @@ const mcpCmd: CommandHandler = async (ctx, args) => {
|
|
|
4225
4521
|
}
|
|
4226
4522
|
}
|
|
4227
4523
|
const via = config.url ? 'HTTP' : 'stdio'
|
|
4524
|
+
if (config.url) {
|
|
4525
|
+
const headerKeys = config.headers ? Object.keys(config.headers) : []
|
|
4526
|
+
const headerLine = headerKeys.length > 0 ? `\nHeaders to send: ${headerKeys.join(', ')}` : ''
|
|
4527
|
+
return {
|
|
4528
|
+
content: `── MCP Connect: ${name} ──\n\nConnecting via HTTP\nURL: ${config.url}${headerLine}\n\n⚠️ Verify the URL — any configured headers (e.g. Authorization) will be sent to this server.`,
|
|
4529
|
+
forwardToAI: `Connect to MCP server "${name}" using McpClient.getInstance().connect(config), then register its tools. Report the result.`,
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4228
4532
|
return {
|
|
4229
4533
|
content: `── MCP Connect: ${name} ──\n\nConnecting via ${via}...`,
|
|
4230
4534
|
forwardToAI: `Connect to MCP server "${name}" using McpClient.getInstance().connect(config), then register its tools. Report the result.`,
|
|
@@ -4868,6 +5172,8 @@ const commandsListCmd: CommandHandler = () => {
|
|
|
4868
5172
|
'/reload-skills': 'Tools & Skills',
|
|
4869
5173
|
'/browse-skills': 'Tools & Skills',
|
|
4870
5174
|
'/install-skill': 'Tools & Skills',
|
|
5175
|
+
'/marketplace': 'Tools & Skills',
|
|
5176
|
+
'/browse-marketplace': 'Tools & Skills',
|
|
4871
5177
|
'/remove-skill': 'Tools & Skills',
|
|
4872
5178
|
'/mcp': 'Tools & Skills',
|
|
4873
5179
|
'/plugins': 'Plugins',
|
|
@@ -5028,6 +5334,8 @@ registry.set('/skills', skillsCmd)
|
|
|
5028
5334
|
registry.set('/reload-skills', reloadSkillsCmd)
|
|
5029
5335
|
registry.set('/browse-skills', browseSkillsCmd)
|
|
5030
5336
|
registry.set('/install-skill', installSkillCmd)
|
|
5337
|
+
registry.set('/marketplace', marketplaceCmd)
|
|
5338
|
+
registry.set('/browse-marketplace', browseMarketplaceCmd)
|
|
5031
5339
|
registry.set('/remove-skill', removeSkillCmd)
|
|
5032
5340
|
registry.set('/plugins', pluginsCmd)
|
|
5033
5341
|
registry.set('/browse-plugins', browsePluginsCmd)
|
|
@@ -5079,6 +5387,7 @@ registry.set('/pr-comments', prCommentsCmd)
|
|
|
5079
5387
|
|
|
5080
5388
|
// Session Management
|
|
5081
5389
|
registry.set('/doctor', doctorCmd)
|
|
5390
|
+
registry.set('/fix', fixCmd)
|
|
5082
5391
|
registry.set('/export', exportCmd)
|
|
5083
5392
|
registry.set('/resume', resumeCmd)
|
|
5084
5393
|
registry.set('/resume last', resumeLastCmd)
|
|
@@ -5183,6 +5492,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
5183
5492
|
'/save': 'Save conversation to Obsidian wiki (skill: save-to-wiki)',
|
|
5184
5493
|
'/export': 'Export conversation to file',
|
|
5185
5494
|
'/doctor': 'System diagnostics',
|
|
5495
|
+
'/fix': 'Deterministic self-repair: doctor/config/cache',
|
|
5186
5496
|
'/dream': 'Background memory consolidation',
|
|
5187
5497
|
'/constitution': 'View or reload constitutional principles',
|
|
5188
5498
|
'/bug-report': 'Generate diagnostic report for GitHub Issues',
|
|
@@ -5212,6 +5522,8 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
5212
5522
|
'/reload-skills': 'Reload all skills',
|
|
5213
5523
|
'/browse-skills': 'Browse community skill marketplace',
|
|
5214
5524
|
'/install-skill': 'Install a skill by name or URL',
|
|
5525
|
+
'/marketplace': 'Manage marketplace sources (add/list/remove)',
|
|
5526
|
+
'/browse-marketplace': 'Browse skills across all marketplace sources',
|
|
5215
5527
|
'/remove-skill': 'Remove an installed skill',
|
|
5216
5528
|
'/mcp': 'MCP server status',
|
|
5217
5529
|
'/plugins': 'List installed plugins',
|
|
@@ -5294,7 +5606,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
5294
5606
|
'/stats': 'Show session usage statistics',
|
|
5295
5607
|
'/summary': 'Generate session summary',
|
|
5296
5608
|
'/cd': 'Change session working directory',
|
|
5297
|
-
'/hooks': '
|
|
5609
|
+
'/hooks': 'List configured hooks from settings.json',
|
|
5298
5610
|
'/hooks health': 'Check hook health — see failures, disabled hooks, recovery status',
|
|
5299
5611
|
'/hooks enable': 'Manually re-enable a hook that was auto-disabled after repeated failures',
|
|
5300
5612
|
'/batch': 'Apply changes across multiple files',
|