@miphamai/cli 0.19.5 → 0.20.1
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 +2 -1
- package/src/core/engine.ts +42 -18
- package/src/i18n-context.tsx +33 -0
- package/src/i18n-core/detect.ts +54 -0
- package/src/i18n-core/locales/en-US.json +781 -0
- package/src/i18n-core/locales/zh-CN.json +781 -0
- package/src/i18n-core/t.ts +19 -0
- package/src/i18n-core/types.ts +10 -0
- package/src/index.tsx +30 -11
- package/src/mcp/client.ts +13 -3
- package/src/mcp/oauth.ts +14 -4
- package/src/mcp/registry.ts +16 -2
- package/src/tools/index.ts +22 -8
- package/src/ui/agent-footer.tsx +3 -1
- package/src/ui/app.tsx +73 -22
- package/src/ui/chat.tsx +31 -21
- package/src/ui/command-picker.tsx +13 -6
- package/src/ui/commands.ts +363 -428
- package/src/ui/input.tsx +53 -101
- package/src/ui/picker.tsx +12 -8
package/src/ui/commands.ts
CHANGED
|
@@ -13,6 +13,20 @@ import { McpClient } from '../mcp/client'
|
|
|
13
13
|
import { NPM_UPDATE_COMMAND, PACKAGE_VERSION } from '../shared/index.ts'
|
|
14
14
|
import { getPreference } from '../config/preferences'
|
|
15
15
|
import { stripIndent } from './strip-indent.js'
|
|
16
|
+
import { createT } from '../i18n-core/t'
|
|
17
|
+
import type { TranslationMap } from '../i18n-core/types'
|
|
18
|
+
import enUS from '../i18n-core/locales/en-US.json' with { type: 'json' }
|
|
19
|
+
import zhCN from '../i18n-core/locales/zh-CN.json' with { type: 'json' }
|
|
20
|
+
|
|
21
|
+
// Default t() fallback for plain-function contexts (tests, bootstrap).
|
|
22
|
+
// When called from the React tree, ctx.t is populated from useI18n().
|
|
23
|
+
const defaultT = createT(enUS as unknown as TranslationMap, zhCN as unknown as TranslationMap)
|
|
24
|
+
|
|
25
|
+
/** Resolve the best available t() function: prefer ctx.t (locale-aware from React),
|
|
26
|
+
* fall back to module-level defaultT (en-US). */
|
|
27
|
+
function resolveT(ctx: CommandContext): (key: string, params?: Record<string, string>) => string {
|
|
28
|
+
return ctx.t && typeof ctx.t === 'function' ? ctx.t : defaultT
|
|
29
|
+
}
|
|
16
30
|
import {
|
|
17
31
|
initCmd,
|
|
18
32
|
setupCmd,
|
|
@@ -42,6 +56,9 @@ export interface CommandContext {
|
|
|
42
56
|
setUltracodeMode: (on: boolean) => void
|
|
43
57
|
skillsLoader?: SkillsLoader
|
|
44
58
|
pluginManager?: PluginManager
|
|
59
|
+
/** i18n translate function — populated from React tree via useI18n().
|
|
60
|
+
* Falls back to module-level defaultT (en-US) in tests / bootstrap. */
|
|
61
|
+
t: (key: string, params?: Record<string, string>) => string
|
|
45
62
|
}
|
|
46
63
|
|
|
47
64
|
export interface CommandResult {
|
|
@@ -228,7 +245,8 @@ const versionCmd: CommandHandler = (ctx) => {
|
|
|
228
245
|
|
|
229
246
|
const clearCmd: CommandHandler = (ctx) => {
|
|
230
247
|
ctx.engine.getContext().clear()
|
|
231
|
-
|
|
248
|
+
const t = resolveT(ctx)
|
|
249
|
+
return { content: t('commands.clear.confirmed'), clearMessages: true }
|
|
232
250
|
}
|
|
233
251
|
|
|
234
252
|
const exitCmd: CommandHandler = () => ({ content: '', exit: true })
|
|
@@ -238,63 +256,68 @@ const exitCmd: CommandHandler = () => ({ content: '', exit: true })
|
|
|
238
256
|
// ═══════════════════════════════════════════════════════════════
|
|
239
257
|
|
|
240
258
|
const compactCmd: CommandHandler = async (ctx) => {
|
|
259
|
+
const t = resolveT(ctx)
|
|
241
260
|
const context = ctx.engine.getContext()
|
|
242
261
|
const before = context.getEstimatedTokens()
|
|
243
262
|
await context.compact('user requested compaction')
|
|
244
263
|
const after = context.getEstimatedTokens()
|
|
245
264
|
return {
|
|
246
|
-
content:
|
|
265
|
+
content: `${t('commands.compact.confirmed')}\n${t('commands.compact.tokens', { before: before.toLocaleString(), after: after.toLocaleString(), saved: ((1 - after / before) * 100).toFixed(0) })}`,
|
|
247
266
|
}
|
|
248
267
|
}
|
|
249
268
|
|
|
250
269
|
const contextCmd: CommandHandler = (ctx) => {
|
|
270
|
+
const t = resolveT(ctx)
|
|
251
271
|
const c = ctx.engine.getContext()
|
|
252
272
|
const tokens = c.getEstimatedTokens()
|
|
253
273
|
const msgs = c.getMessages()
|
|
254
274
|
const systemPromptLen = c.getSystemPrompt().length
|
|
255
275
|
return {
|
|
256
276
|
content: stripIndent`
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
277
|
+
${t('commands.context.title')}
|
|
278
|
+
${t('commands.context.messages')} ${msgs.length}
|
|
279
|
+
${t('commands.context.estimated_tokens')} ${tokens.toLocaleString()} / 200,000
|
|
280
|
+
${t('commands.context.usage_pct')} ${((tokens / 200_000) * 100).toFixed(1)}%
|
|
281
|
+
${t('commands.context.system_prompt')} ${systemPromptLen.toLocaleString()} chars (~${Math.ceil(systemPromptLen / 4).toLocaleString()} tokens)
|
|
282
|
+
${t('commands.context.compaction')} at 90% (${(200_000 * 0.9).toLocaleString()} tokens)
|
|
263
283
|
`,
|
|
264
284
|
}
|
|
265
285
|
}
|
|
266
286
|
|
|
267
287
|
const statusCmd: CommandHandler = (ctx) => {
|
|
288
|
+
const t = resolveT(ctx)
|
|
268
289
|
const c = ctx.engine.getContext()
|
|
269
290
|
const tools = ctx.engine.getTools()
|
|
291
|
+
const runtime = typeof Bun !== 'undefined' ? 'Bun' : 'Node.js'
|
|
292
|
+
const runtimeVer = typeof Bun !== 'undefined' ? Bun.version : process.version
|
|
270
293
|
return {
|
|
271
294
|
content: stripIndent`
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
295
|
+
${t('commands.status.session_title')}
|
|
296
|
+
${t('commands.status.provider')} ${ctx.providerId}
|
|
297
|
+
${t('commands.status.model')} ${ctx.modelId}
|
|
298
|
+
${t('commands.status.messages')} ${c.getMessages().length}
|
|
299
|
+
${t('commands.status.tokens')} ~${c.getEstimatedTokens().toLocaleString()} / 200,000
|
|
300
|
+
${t('commands.status.tools')} ${tools.size} ${t('commands.status.loaded')}
|
|
301
|
+
${t('commands.status.permission')} ${ctx.config.permission}
|
|
302
|
+
|
|
303
|
+
${t('commands.status.system_title')}
|
|
304
|
+
${t('commands.status.platform')} ${process.platform} ${process.arch}
|
|
305
|
+
${t('commands.status.runtime')} ${runtime} ${runtimeVer}
|
|
306
|
+
${t('commands.status.cwd')} ${process.cwd()}
|
|
284
307
|
`,
|
|
285
308
|
}
|
|
286
309
|
}
|
|
287
310
|
|
|
288
311
|
const costCmd: CommandHandler = (ctx) => {
|
|
312
|
+
const t = resolveT(ctx)
|
|
289
313
|
const tokens = ctx.engine.getContext().getEstimatedTokens()
|
|
290
314
|
return {
|
|
291
315
|
content: stripIndent`
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
316
|
+
${t('commands.context_tokens.title')}
|
|
317
|
+
${t('commands.context_tokens.context_tokens')} ~${tokens.toLocaleString()} / 200,000
|
|
318
|
+
${t('commands.context_tokens.usage')} ${((tokens / 200_000) * 100).toFixed(1)}%
|
|
295
319
|
|
|
296
|
-
|
|
297
|
-
Actual API usage depends on provider and model.
|
|
320
|
+
${t('commands.context_tokens.footer')}
|
|
298
321
|
`,
|
|
299
322
|
}
|
|
300
323
|
}
|
|
@@ -303,11 +326,13 @@ const costCmd: CommandHandler = (ctx) => {
|
|
|
303
326
|
// Model & Provider
|
|
304
327
|
// ═══════════════════════════════════════════════════════════════
|
|
305
328
|
|
|
306
|
-
const modelCmd: CommandHandler = (ctx) =>
|
|
307
|
-
|
|
308
|
-
})
|
|
329
|
+
const modelCmd: CommandHandler = (ctx) => {
|
|
330
|
+
const t = resolveT(ctx)
|
|
331
|
+
return { content: t('commands.model.current', { model: ctx.modelId, provider: ctx.providerId }) }
|
|
332
|
+
}
|
|
309
333
|
|
|
310
334
|
const modelsCmd: CommandHandler = (ctx) => {
|
|
335
|
+
const t = resolveT(ctx)
|
|
311
336
|
const lines = ctx.config.providers
|
|
312
337
|
.filter((p) => p.status !== 'upcoming')
|
|
313
338
|
.flatMap((p) =>
|
|
@@ -320,40 +345,44 @@ const modelsCmd: CommandHandler = (ctx) => {
|
|
|
320
345
|
)
|
|
321
346
|
|
|
322
347
|
return {
|
|
323
|
-
content:
|
|
348
|
+
content: `${t('commands.models.title', { count: String(lines.length) })}\n\n${t('commands.models.header')}\n${'-'.repeat(80)}\n${lines.join('\n')}\n\n${t('commands.models.hint')}`,
|
|
324
349
|
}
|
|
325
350
|
}
|
|
326
351
|
|
|
327
|
-
const providerCmd: CommandHandler = (ctx) =>
|
|
328
|
-
|
|
329
|
-
|
|
352
|
+
const providerCmd: CommandHandler = (ctx) => {
|
|
353
|
+
const t = resolveT(ctx)
|
|
354
|
+
return {
|
|
355
|
+
content: t('commands.provider.current', { provider: ctx.providerId, model: ctx.modelId }),
|
|
356
|
+
}
|
|
357
|
+
}
|
|
330
358
|
|
|
331
359
|
const providersCmd: CommandHandler = (ctx) => {
|
|
360
|
+
const t = resolveT(ctx)
|
|
332
361
|
const lines = ctx.config.providers.map(
|
|
333
362
|
(p) =>
|
|
334
363
|
` ${p.id.padEnd(14)} ${p.name.padEnd(20)} ${p.protocol.padEnd(18)} ${p.models.length} models ${p.status === 'upcoming' ? '[upcoming]' : '✓'}`,
|
|
335
364
|
)
|
|
336
365
|
return {
|
|
337
|
-
content:
|
|
366
|
+
content: `${t('commands.providers.title')}\n\n${lines.join('\n')}\n\n${t('commands.providers.current', { provider: ctx.providerId, model: ctx.modelId })}`,
|
|
338
367
|
}
|
|
339
368
|
}
|
|
340
369
|
|
|
341
370
|
const switchCmd: CommandHandler = (ctx, args) => {
|
|
371
|
+
const t = resolveT(ctx)
|
|
342
372
|
const [newProvider, newModel] = args
|
|
343
373
|
if (!newProvider || !newModel) {
|
|
344
|
-
return {
|
|
345
|
-
content: 'Usage: /switch <provider> <model>\nExample: /switch deepseek deepseek-v4-pro',
|
|
346
|
-
}
|
|
374
|
+
return { content: t('commands.switch.usage') }
|
|
347
375
|
}
|
|
348
376
|
ctx.engine.switchProvider(newProvider, newModel)
|
|
349
377
|
return {
|
|
350
|
-
content:
|
|
378
|
+
content: t('commands.switch.confirmed', { provider: newProvider, model: newModel }),
|
|
351
379
|
nextProvider: newProvider,
|
|
352
380
|
nextModel: newModel,
|
|
353
381
|
}
|
|
354
382
|
}
|
|
355
383
|
|
|
356
384
|
const configCmd: CommandHandler = (ctx) => {
|
|
385
|
+
const t = resolveT(ctx)
|
|
357
386
|
const c = ctx.config
|
|
358
387
|
const lines = [
|
|
359
388
|
`version: ${c.version}`,
|
|
@@ -363,7 +392,7 @@ const configCmd: CommandHandler = (ctx) => {
|
|
|
363
392
|
`providers: ${c.providers.length} configured`,
|
|
364
393
|
]
|
|
365
394
|
return {
|
|
366
|
-
content:
|
|
395
|
+
content: `${t('commands.config.title')}\n${lines.join('\n')}\n\n${t('commands.config.edit')}`,
|
|
367
396
|
}
|
|
368
397
|
}
|
|
369
398
|
|
|
@@ -372,6 +401,7 @@ const configCmd: CommandHandler = (ctx) => {
|
|
|
372
401
|
// ═══════════════════════════════════════════════════════════════
|
|
373
402
|
|
|
374
403
|
const toolsCmd: CommandHandler = (ctx) => {
|
|
404
|
+
const t = resolveT(ctx)
|
|
375
405
|
const tools = ctx.engine.getTools()
|
|
376
406
|
const categories: Record<string, string[]> = {
|
|
377
407
|
file: [],
|
|
@@ -389,12 +419,15 @@ const toolsCmd: CommandHandler = (ctx) => {
|
|
|
389
419
|
.filter(([, v]) => v!.length > 0)
|
|
390
420
|
.map(([cat, items]) => `── ${cat.toUpperCase()} ──\n${items!.join('\n')}`)
|
|
391
421
|
|
|
392
|
-
return {
|
|
422
|
+
return {
|
|
423
|
+
content: `${t('commands.tools.title', { count: String(tools.size) })}\n\n${sections.join('\n\n')}`,
|
|
424
|
+
}
|
|
393
425
|
}
|
|
394
426
|
|
|
395
427
|
const skillsCmd: CommandHandler = (ctx) => {
|
|
428
|
+
const t = resolveT(ctx)
|
|
396
429
|
if (!ctx.skillsLoader) {
|
|
397
|
-
return { content: '
|
|
430
|
+
return { content: t('commands.skills.unavailable') }
|
|
398
431
|
}
|
|
399
432
|
const counts = ctx.skillsLoader.countByType()
|
|
400
433
|
const standard = ctx.skillsLoader.listByType('standard')
|
|
@@ -407,7 +440,7 @@ const skillsCmd: CommandHandler = (ctx) => {
|
|
|
407
440
|
`── Mipham Exclusive (${counts.mipham}) ──`,
|
|
408
441
|
...mipham.map((s) => ` ${s.name.padEnd(20)} ${s.description}`),
|
|
409
442
|
'',
|
|
410
|
-
|
|
443
|
+
t('commands.skills.loaded', { count: String(counts.total) }),
|
|
411
444
|
]
|
|
412
445
|
return { content: lines.join('\n') }
|
|
413
446
|
}
|
|
@@ -417,10 +450,10 @@ const skillsCmd: CommandHandler = (ctx) => {
|
|
|
417
450
|
// ═══════════════════════════════════════════════════════════════
|
|
418
451
|
|
|
419
452
|
const planCmd: CommandHandler = (_ctx, args) => {
|
|
453
|
+
const t = resolveT(_ctx)
|
|
420
454
|
const description = args.join(' ') || undefined
|
|
421
455
|
return {
|
|
422
|
-
content:
|
|
423
|
-
'── Plan Mode ──\n\nEntering plan mode — read-only analysis and design.\nUse EnterPlanMode to start, ExitPlanMode to submit for approval.',
|
|
456
|
+
content: t('commands.plan.content'),
|
|
424
457
|
forwardToAI: description
|
|
425
458
|
? `Use EnterPlanMode with description: "${description}". Then explore, design, and use ExitPlanMode when ready for approval.`
|
|
426
459
|
: 'Use EnterPlanMode to enter plan mode. Explore the codebase, design an approach, then use ExitPlanMode to submit for approval.',
|
|
@@ -428,18 +461,19 @@ const planCmd: CommandHandler = (_ctx, args) => {
|
|
|
428
461
|
}
|
|
429
462
|
|
|
430
463
|
const tddCmd: CommandHandler = (_ctx, args) => {
|
|
464
|
+
const t = resolveT(_ctx)
|
|
431
465
|
const target = args.join(' ') || 'the current task'
|
|
432
466
|
return {
|
|
433
467
|
content: stripIndent`
|
|
434
|
-
|
|
435
|
-
|
|
468
|
+
${t('commands.tdd.title')}
|
|
469
|
+
${t('commands.tdd.start', { target })}
|
|
436
470
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
471
|
+
${t('commands.tdd.cycle')}
|
|
472
|
+
${t('commands.tdd.red')}
|
|
473
|
+
${t('commands.tdd.green')}
|
|
474
|
+
${t('commands.tdd.refactor')}
|
|
441
475
|
|
|
442
|
-
|
|
476
|
+
${t('commands.tdd.guide')}
|
|
443
477
|
`,
|
|
444
478
|
forwardToAI: `Follow the Test-Driven Development workflow for ${target}:
|
|
445
479
|
1. RED — Write a failing test that defines the expected behavior. Use the project's test framework (Vitest/Jest/pytest). Show me the test code and confirm it fails.
|
|
@@ -450,17 +484,15 @@ Repeat for each behavior. Do NOT write implementation before tests.`,
|
|
|
450
484
|
}
|
|
451
485
|
|
|
452
486
|
const todosCmd: CommandHandler = (_ctx, args) => {
|
|
487
|
+
const t = resolveT(_ctx)
|
|
453
488
|
const sub = args[0]
|
|
454
489
|
if (sub === 'create') {
|
|
455
490
|
const title = args.slice(1).join(' ')
|
|
456
491
|
if (!title.trim()) {
|
|
457
|
-
return {
|
|
458
|
-
content:
|
|
459
|
-
'Usage: /todos create <task-title>\n\nExample: /todos create Add user authentication',
|
|
460
|
-
}
|
|
492
|
+
return { content: t('commands.todos.usage_create') }
|
|
461
493
|
}
|
|
462
494
|
return {
|
|
463
|
-
content:
|
|
495
|
+
content: `${t('commands.todos.create_title')}\n\nCreating task: "${title.trim()}"\n\nPassing to AI for structured task creation with TaskCreate...`,
|
|
464
496
|
forwardToAI: `Create a new task using TaskCreate with subject "${title.trim()}". Set a clear description and activeForm.`,
|
|
465
497
|
}
|
|
466
498
|
}
|
|
@@ -468,12 +500,12 @@ const todosCmd: CommandHandler = (_ctx, args) => {
|
|
|
468
500
|
if (sub === 'list' || !sub) {
|
|
469
501
|
return {
|
|
470
502
|
content: stripIndent`
|
|
471
|
-
|
|
472
|
-
|
|
503
|
+
${t('commands.todos.list_title')}
|
|
504
|
+
${t('commands.todos.list_fetching')}
|
|
473
505
|
|
|
474
506
|
Shortcuts:
|
|
475
|
-
|
|
476
|
-
|
|
507
|
+
${t('commands.todos.item_list')}
|
|
508
|
+
${t('commands.todos.item_create')}
|
|
477
509
|
`,
|
|
478
510
|
forwardToAI:
|
|
479
511
|
'Use TaskList to show all current tasks. Present them in a clear summary grouped by status (pending/in_progress/completed). If there are no tasks, suggest creating one.',
|
|
@@ -482,12 +514,11 @@ const todosCmd: CommandHandler = (_ctx, args) => {
|
|
|
482
514
|
|
|
483
515
|
return {
|
|
484
516
|
content: stripIndent`
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
517
|
+
${t('commands.todos.default_title')}
|
|
518
|
+
${t('commands.todos.item_list')}
|
|
519
|
+
${t('commands.todos.item_create')}
|
|
488
520
|
|
|
489
|
-
|
|
490
|
-
Tasks appear in the /tasks view and persist across the session.
|
|
521
|
+
${t('commands.todos.default_body')}
|
|
491
522
|
`,
|
|
492
523
|
forwardToAI: 'Use TaskList to show all current tasks, then present them clearly.',
|
|
493
524
|
}
|
|
@@ -502,40 +533,37 @@ const todosCmd: CommandHandler = (_ctx, args) => {
|
|
|
502
533
|
// ═══════════════════════════════════════════════════════════════
|
|
503
534
|
|
|
504
535
|
const renameCmd: CommandHandler = (ctx, args) => {
|
|
536
|
+
const t = resolveT(ctx)
|
|
505
537
|
const name = args.join(' ')
|
|
506
538
|
if (!name.trim()) {
|
|
507
|
-
return { content: '
|
|
539
|
+
return { content: t('commands.rename.usage') }
|
|
508
540
|
}
|
|
509
541
|
ctx.setSessionTitle(name.trim())
|
|
510
|
-
return { content:
|
|
542
|
+
return { content: t('commands.rename.confirmed', { name: name.trim() }) }
|
|
511
543
|
}
|
|
512
544
|
|
|
513
545
|
const goalCmd: CommandHandler = (ctx, args) => {
|
|
546
|
+
const t = resolveT(ctx)
|
|
514
547
|
const input = args.join(' ')
|
|
515
548
|
|
|
516
549
|
// ── Show goal status ──
|
|
517
550
|
if (!input.trim()) {
|
|
518
551
|
const state = ctx.engine.getGoalState?.() || { goal: undefined, decompose: false, subtasks: [] }
|
|
519
552
|
if (!state.goal) {
|
|
520
|
-
return {
|
|
521
|
-
content:
|
|
522
|
-
`Usage: /goal <statement> [options]\n\n` +
|
|
523
|
-
`Set a session-level completion condition with optional verification.\n\n` +
|
|
524
|
-
`Options:\n` +
|
|
525
|
-
` --decompose Auto-decompose goal into subtasks\n` +
|
|
526
|
-
` --verify-script <path> Run a shell script to check completion\n` +
|
|
527
|
-
` --verify-skill <name> Use a skill for verification\n\n` +
|
|
528
|
-
`Examples:\n` +
|
|
529
|
-
` /goal Fix all TS errors and make tests pass --decompose\n` +
|
|
530
|
-
` /goal Complete the build --verify-script ./check-build.sh\n` +
|
|
531
|
-
` /goal Security audit passes --verify-skill security-review`,
|
|
532
|
-
}
|
|
553
|
+
return { content: t('commands.goal.usage') }
|
|
533
554
|
}
|
|
534
|
-
const lines = [
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
555
|
+
const lines = [
|
|
556
|
+
t('commands.goal.status_title'),
|
|
557
|
+
'',
|
|
558
|
+
t('commands.goal.status_goal', { goal: state.goal }),
|
|
559
|
+
]
|
|
560
|
+
if (state.verifyScript)
|
|
561
|
+
lines.push(t('commands.goal.status_verify_script', { script: state.verifyScript }))
|
|
562
|
+
if (state.verifySkill)
|
|
563
|
+
lines.push(t('commands.goal.status_verify_skill', { skill: state.verifySkill }))
|
|
564
|
+
if (state.decompose)
|
|
565
|
+
lines.push(t('commands.goal.status_decompose', { count: String(state.subtasks.length) }))
|
|
566
|
+
lines.push('', t('commands.goal.status_clear_hint'))
|
|
539
567
|
return { content: lines.join('\n') }
|
|
540
568
|
}
|
|
541
569
|
|
|
@@ -567,15 +595,15 @@ const goalCmd: CommandHandler = (ctx, args) => {
|
|
|
567
595
|
|
|
568
596
|
goal = goal.trim()
|
|
569
597
|
if (!goal) {
|
|
570
|
-
return { content: '
|
|
598
|
+
return { content: t('commands.goal.empty_set') }
|
|
571
599
|
}
|
|
572
600
|
|
|
573
601
|
ctx.setGoal(goal)
|
|
574
602
|
ctx.engine.setGoal(goal, { verifyScript, verifySkill, decompose })
|
|
575
603
|
|
|
576
|
-
const lines = [
|
|
604
|
+
const lines = [t('commands.goal.set', { goal })]
|
|
577
605
|
if (decompose) {
|
|
578
|
-
lines.push('
|
|
606
|
+
lines.push(t('commands.goal.decompose_enabled'))
|
|
579
607
|
// Decompose by creating initial subtasks
|
|
580
608
|
const decomposeMsg = `Break down this goal into 3-5 subtasks: "${goal}". For each subtask, use TaskCreate with the subject and description. Mark each as blocked by the previous one to create a dependency chain.`
|
|
581
609
|
return {
|
|
@@ -583,27 +611,26 @@ const goalCmd: CommandHandler = (ctx, args) => {
|
|
|
583
611
|
forwardToAI: decomposeMsg,
|
|
584
612
|
}
|
|
585
613
|
}
|
|
586
|
-
if (verifyScript) lines.push(
|
|
587
|
-
if (verifySkill) lines.push(
|
|
614
|
+
if (verifyScript) lines.push(t('commands.goal.status_verify_script', { script: verifyScript }))
|
|
615
|
+
if (verifySkill) lines.push(t('commands.goal.status_verify_skill', { skill: verifySkill }))
|
|
588
616
|
lines.push('')
|
|
589
|
-
lines.push('
|
|
617
|
+
lines.push(t('commands.goal.status_hint'))
|
|
590
618
|
|
|
591
619
|
return { content: lines.join('\n') }
|
|
592
620
|
}
|
|
593
621
|
|
|
594
622
|
const recapCmd: CommandHandler = (ctx) => {
|
|
623
|
+
const t = resolveT(ctx)
|
|
595
624
|
const c = ctx.engine.getContext()
|
|
596
625
|
const msgs = c.getMessages()
|
|
597
626
|
if (msgs.length === 0) {
|
|
598
|
-
return { content: '
|
|
627
|
+
return { content: t('commands.recap.no_conversation') }
|
|
599
628
|
}
|
|
600
|
-
// Show summary of conversation: count messages, roles, estimated tokens
|
|
601
629
|
const userMsgs = msgs.filter((m) => m.role === 'user').length
|
|
602
630
|
const assistantMsgs = msgs.filter((m) => m.role === 'assistant').length
|
|
603
631
|
const tokens = c.getEstimatedTokens()
|
|
604
632
|
const checkpointCount = c.getCheckpoints().length
|
|
605
633
|
|
|
606
|
-
// Extract first few user messages as "topics"
|
|
607
634
|
const topics = msgs
|
|
608
635
|
.filter((m) => m.role === 'user' && typeof m.content === 'string')
|
|
609
636
|
.slice(0, 5)
|
|
@@ -614,20 +641,21 @@ const recapCmd: CommandHandler = (ctx) => {
|
|
|
614
641
|
|
|
615
642
|
return {
|
|
616
643
|
content: stripIndent`
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
644
|
+
${t('commands.recap.title')}
|
|
645
|
+
${t('commands.recap.messages')} ${msgs.length} (${userMsgs} user, ${assistantMsgs} assistant)
|
|
646
|
+
${t('commands.recap.est_tokens')} ~${tokens.toLocaleString()}
|
|
647
|
+
${t('commands.recap.checkpoints')} ${checkpointCount}
|
|
621
648
|
|
|
622
|
-
|
|
649
|
+
${t('commands.recap.recent_topics')}
|
|
623
650
|
${topics.map((t, i) => ` ${i + 1}. ${t}`).join('\n')}
|
|
624
651
|
|
|
625
|
-
|
|
652
|
+
${t('commands.recap.footer')}
|
|
626
653
|
`,
|
|
627
654
|
}
|
|
628
655
|
}
|
|
629
656
|
|
|
630
657
|
const usageCmd: CommandHandler = (ctx) => {
|
|
658
|
+
const t = resolveT(ctx)
|
|
631
659
|
const c = ctx.engine.getContext()
|
|
632
660
|
const estTokens = c.getEstimatedTokens()
|
|
633
661
|
const msgs = c.getMessages()
|
|
@@ -653,46 +681,50 @@ const usageCmd: CommandHandler = (ctx) => {
|
|
|
653
681
|
const apiTotal = summary.apiInputTokens + summary.apiOutputTokens
|
|
654
682
|
const apiLine =
|
|
655
683
|
summary.apiInputTokens > 0 || summary.apiOutputTokens > 0
|
|
656
|
-
?
|
|
657
|
-
|
|
684
|
+
? t('commands.usage.api_tokens', {
|
|
685
|
+
in: summary.apiInputTokens.toLocaleString(),
|
|
686
|
+
out: summary.apiOutputTokens.toLocaleString(),
|
|
687
|
+
total: apiTotal.toLocaleString(),
|
|
688
|
+
})
|
|
689
|
+
: t('commands.usage.no_api')
|
|
658
690
|
|
|
659
|
-
const toolSection =
|
|
691
|
+
const toolSection =
|
|
692
|
+
toolLines.length > 0 ? `\n${t('commands.usage.tool_section')}\n${toolLines.join('\n')}` : ''
|
|
660
693
|
|
|
661
694
|
return {
|
|
662
695
|
content: stripIndent`
|
|
663
|
-
|
|
696
|
+
${t('commands.usage.title')}
|
|
664
697
|
${apiLine}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
698
|
+
${t('commands.usage.context_line', { est: estTokens.toLocaleString(), max: maxTokens.toLocaleString(), pct })}
|
|
699
|
+
${t('commands.usage.messages_line', { count: String(msgs.length) })}
|
|
700
|
+
${t('commands.usage.provider_line', { provider: ctx.providerId })}
|
|
701
|
+
${t('commands.usage.model_line', { model: ctx.modelId })}
|
|
669
702
|
|
|
670
703
|
${'█'.repeat(Math.ceil(Number(pct) / 5))}${'░'.repeat(20 - Math.ceil(Number(pct) / 5))} ${pct}%
|
|
671
704
|
${toolSection}
|
|
672
705
|
|
|
673
|
-
|
|
706
|
+
${t('commands.usage.hint')}
|
|
674
707
|
`,
|
|
675
708
|
}
|
|
676
709
|
}
|
|
677
710
|
|
|
678
711
|
const reloadSkillsCmd: CommandHandler = (ctx) => {
|
|
712
|
+
const t = resolveT(ctx)
|
|
679
713
|
if (!ctx.skillsLoader) {
|
|
680
|
-
return { content: '
|
|
714
|
+
return { content: t('commands.reload_skills.unavailable') }
|
|
681
715
|
}
|
|
682
716
|
try {
|
|
683
717
|
const config = ctx.config
|
|
684
|
-
// Re-load builtin skills from the skills directory
|
|
685
|
-
// The base path is typically relative to the CLI package
|
|
686
718
|
ctx.skillsLoader.loadBuiltin(process.cwd())
|
|
687
719
|
if (config.skills?.paths) {
|
|
688
720
|
ctx.skillsLoader.loadExternal(config.skills.paths)
|
|
689
721
|
}
|
|
690
722
|
const skills = ctx.skillsLoader.list()
|
|
691
723
|
return {
|
|
692
|
-
content:
|
|
724
|
+
content: `${t('commands.reload_skills.confirmed', { count: String(skills.length) })}\n\n${skills.map((s) => ` ${s.name.padEnd(28)} ${s.type.padEnd(10)} ${s.description}`).join('\n')}`,
|
|
693
725
|
}
|
|
694
726
|
} catch (err) {
|
|
695
|
-
return { content:
|
|
727
|
+
return { content: t('commands.reload_skills.failed', { error: String(err) }) }
|
|
696
728
|
}
|
|
697
729
|
}
|
|
698
730
|
|
|
@@ -700,7 +732,8 @@ const reloadSkillsCmd: CommandHandler = (ctx) => {
|
|
|
700
732
|
// Skill Marketplace — Community skill registry + installation
|
|
701
733
|
// ═══════════════════════════════════════════════════════════════
|
|
702
734
|
|
|
703
|
-
const browseSkillsCmd: CommandHandler = async () => {
|
|
735
|
+
const browseSkillsCmd: CommandHandler = async (ctx) => {
|
|
736
|
+
const _t = resolveT(ctx)
|
|
704
737
|
const { getAvailableSkills, listInstalledSkills } = await import('../skills/registry')
|
|
705
738
|
const available = getAvailableSkills()
|
|
706
739
|
const installed = new Set(
|
|
@@ -780,29 +813,19 @@ const removeSkillCmd: CommandHandler = async (_ctx, args) => {
|
|
|
780
813
|
// ═══════════════════════════════════════════════════════════════
|
|
781
814
|
|
|
782
815
|
const pluginsCmd: CommandHandler = (ctx) => {
|
|
816
|
+
const t = resolveT(ctx)
|
|
783
817
|
const manager = ctx.pluginManager
|
|
784
818
|
if (!manager) {
|
|
785
|
-
return { content: '
|
|
819
|
+
return { content: t('commands.plugins.unavailable') }
|
|
786
820
|
}
|
|
787
821
|
|
|
788
822
|
const plugins = manager.list()
|
|
789
823
|
if (plugins.length === 0) {
|
|
790
|
-
return {
|
|
791
|
-
content: [
|
|
792
|
-
'── Installed Plugins ──',
|
|
793
|
-
'',
|
|
794
|
-
'No plugins installed.',
|
|
795
|
-
'',
|
|
796
|
-
'Install plugins:',
|
|
797
|
-
' /install-plugin <npm-package> Install from npm registry',
|
|
798
|
-
' /browse-plugins Browse community plugins',
|
|
799
|
-
' mipham plugin install <path> Install from local directory',
|
|
800
|
-
].join('\n'),
|
|
801
|
-
}
|
|
824
|
+
return { content: t('commands.plugins.none') }
|
|
802
825
|
}
|
|
803
826
|
|
|
804
827
|
const lines: string[] = [
|
|
805
|
-
'
|
|
828
|
+
t('commands.plugins.title'),
|
|
806
829
|
'',
|
|
807
830
|
...plugins.map((p) => {
|
|
808
831
|
const status = p.enabled ? '✅ enabled' : '⛔ disabled'
|
|
@@ -955,29 +978,25 @@ const pluginDisableCmd: CommandHandler = (ctx, args) => {
|
|
|
955
978
|
// ═══════════════════════════════════════════════════════════════
|
|
956
979
|
|
|
957
980
|
const rewindCmd: CommandHandler = (ctx) => {
|
|
981
|
+
const t = resolveT(ctx)
|
|
958
982
|
const c = ctx.engine.getContext()
|
|
959
983
|
const checkpoints = c.getCheckpoints()
|
|
960
984
|
|
|
961
985
|
if (checkpoints.length === 0) {
|
|
962
|
-
return {
|
|
963
|
-
content:
|
|
964
|
-
'No checkpoints available. Checkpoints are automatically saved after each AI response.',
|
|
965
|
-
}
|
|
986
|
+
return { content: t('commands.rewind.no_checkpoints') }
|
|
966
987
|
}
|
|
967
988
|
|
|
968
989
|
const result = c.restoreCheckpoint()
|
|
969
990
|
if (!result.restored) {
|
|
970
|
-
return { content: '
|
|
991
|
+
return { content: t('commands.rewind.no_restore') }
|
|
971
992
|
}
|
|
972
993
|
|
|
973
994
|
return {
|
|
974
|
-
content:
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
Use /rewind again to go back further, or continue chatting.
|
|
980
|
-
`,
|
|
995
|
+
content: t('commands.rewind.confirmed', {
|
|
996
|
+
label: result.label,
|
|
997
|
+
count: String(result.messageCount),
|
|
998
|
+
remaining: String(c.getCheckpoints().length),
|
|
999
|
+
}),
|
|
981
1000
|
clearMessages: true,
|
|
982
1001
|
}
|
|
983
1002
|
}
|
|
@@ -985,12 +1004,13 @@ const rewindCmd: CommandHandler = (ctx) => {
|
|
|
985
1004
|
const undoCmd: CommandHandler = rewindCmd
|
|
986
1005
|
|
|
987
1006
|
const copyCmd: CommandHandler = (ctx, args) => {
|
|
1007
|
+
const t = resolveT(ctx)
|
|
988
1008
|
const c = ctx.engine.getContext()
|
|
989
1009
|
const msgs = c.getMessages()
|
|
990
1010
|
const assistantMsgs = msgs.filter((m) => m.role === 'assistant')
|
|
991
1011
|
|
|
992
1012
|
if (assistantMsgs.length === 0) {
|
|
993
|
-
return { content: '
|
|
1013
|
+
return { content: t('commands.copy.no_responses') }
|
|
994
1014
|
}
|
|
995
1015
|
|
|
996
1016
|
// Determine which response to copy: last N or last 1
|
|
@@ -998,9 +1018,7 @@ const copyCmd: CommandHandler = (ctx, args) => {
|
|
|
998
1018
|
if (args[0]) {
|
|
999
1019
|
n = parseInt(args[0]!, 10)
|
|
1000
1020
|
if (isNaN(n) || n < 1) {
|
|
1001
|
-
return {
|
|
1002
|
-
content: 'Usage: /copy [N]\nN = number of recent assistant responses to copy (default: 1)',
|
|
1003
|
-
}
|
|
1021
|
+
return { content: t('commands.copy.usage') }
|
|
1004
1022
|
}
|
|
1005
1023
|
}
|
|
1006
1024
|
|
|
@@ -1010,17 +1028,18 @@ const copyCmd: CommandHandler = (ctx, args) => {
|
|
|
1010
1028
|
.join('\n\n---\n\n')
|
|
1011
1029
|
|
|
1012
1030
|
return {
|
|
1013
|
-
content:
|
|
1031
|
+
content: t('commands.copy.confirmed', { count: String(toCopy.length) }),
|
|
1014
1032
|
copyContent: text,
|
|
1015
1033
|
}
|
|
1016
1034
|
}
|
|
1017
1035
|
|
|
1018
1036
|
const diffCmd: CommandHandler = async (_ctx) => {
|
|
1037
|
+
const t = resolveT(_ctx)
|
|
1019
1038
|
try {
|
|
1020
1039
|
const { execSync } = await import('node:child_process')
|
|
1021
1040
|
const output = execSync('git diff --stat', { encoding: 'utf-8', timeout: 5000 })
|
|
1022
1041
|
if (!output.trim()) {
|
|
1023
|
-
return { content: '
|
|
1042
|
+
return { content: t('commands.diff.clean') }
|
|
1024
1043
|
}
|
|
1025
1044
|
// Get full diff but limit to reasonable size
|
|
1026
1045
|
const fullDiff = execSync('git diff --no-color', { encoding: 'utf-8', timeout: 5000 })
|
|
@@ -1033,11 +1052,11 @@ const diffCmd: CommandHandler = async (_ctx) => {
|
|
|
1033
1052
|
: fullDiff
|
|
1034
1053
|
|
|
1035
1054
|
return {
|
|
1036
|
-
content:
|
|
1055
|
+
content: `${t('commands.diff.title')}\n\n${truncated}`,
|
|
1037
1056
|
}
|
|
1038
1057
|
} catch {
|
|
1039
1058
|
return {
|
|
1040
|
-
content: '
|
|
1059
|
+
content: t('commands.diff.error'),
|
|
1041
1060
|
}
|
|
1042
1061
|
}
|
|
1043
1062
|
}
|
|
@@ -1047,60 +1066,38 @@ const diffCmd: CommandHandler = async (_ctx) => {
|
|
|
1047
1066
|
// ═══════════════════════════════════════════════════════════════
|
|
1048
1067
|
|
|
1049
1068
|
const fastCmd: CommandHandler = (ctx, args) => {
|
|
1069
|
+
const t = resolveT(ctx)
|
|
1050
1070
|
const arg = args[0]?.toLowerCase()
|
|
1051
1071
|
if (arg === 'on') {
|
|
1052
1072
|
ctx.setFastMode(true)
|
|
1053
|
-
return { content: '
|
|
1073
|
+
return { content: t('commands.fast.on') }
|
|
1054
1074
|
} else if (arg === 'off') {
|
|
1055
1075
|
ctx.setFastMode(false)
|
|
1056
|
-
return { content: '
|
|
1076
|
+
return { content: t('commands.fast.off') }
|
|
1057
1077
|
} else if (arg) {
|
|
1058
|
-
return { content: '
|
|
1078
|
+
return { content: t('commands.fast.usage') }
|
|
1059
1079
|
} else {
|
|
1060
|
-
|
|
1061
|
-
// We can't read current state from context, so we just show usage
|
|
1062
|
-
return {
|
|
1063
|
-
content:
|
|
1064
|
-
'Usage: /fast [on|off]\n\nFast mode prioritizes speed over depth. Currently available as a configuration toggle.\n\nExample:\n /fast on — enable fast mode\n /fast off — disable fast mode',
|
|
1065
|
-
}
|
|
1080
|
+
return { content: t('commands.fast.unknown') }
|
|
1066
1081
|
}
|
|
1067
1082
|
}
|
|
1068
1083
|
|
|
1069
1084
|
const effortCmd: CommandHandler = (ctx, args) => {
|
|
1085
|
+
const t = resolveT(ctx)
|
|
1070
1086
|
const VALID_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max']
|
|
1071
1087
|
const level = args[0]?.toLowerCase()
|
|
1072
1088
|
|
|
1073
1089
|
if (!level || !VALID_LEVELS.includes(level)) {
|
|
1074
|
-
return {
|
|
1075
|
-
content: stripIndent`
|
|
1076
|
-
Usage: /effort <level>
|
|
1077
|
-
|
|
1078
|
-
Set reasoning effort level:
|
|
1079
|
-
low — Fast, simple tasks
|
|
1080
|
-
medium — Balanced quality and speed
|
|
1081
|
-
high — Thorough reasoning (default)
|
|
1082
|
-
xhigh — Maximum depth for coding/agentic use
|
|
1083
|
-
max — Absolute ceiling, very thorough
|
|
1084
|
-
|
|
1085
|
-
Current model: ${ctx.modelId}
|
|
1086
|
-
Effort levels require compatible providers (Anthropic Opus 4.6+, Sonnet 4.6).
|
|
1087
|
-
`,
|
|
1088
|
-
}
|
|
1090
|
+
return { content: t('commands.effort.usage', { model: ctx.modelId }) }
|
|
1089
1091
|
}
|
|
1090
1092
|
|
|
1091
1093
|
ctx.setEffort(level)
|
|
1092
|
-
return { content:
|
|
1094
|
+
return { content: t('commands.effort.confirmed', { level }) }
|
|
1093
1095
|
}
|
|
1094
1096
|
|
|
1095
1097
|
const focusCmd: CommandHandler = (ctx) => {
|
|
1098
|
+
const t = resolveT(ctx)
|
|
1096
1099
|
ctx.setFocusMode(true)
|
|
1097
|
-
return {
|
|
1098
|
-
content: stripIndent`
|
|
1099
|
-
✓ Focus mode ON — showing only the most recent exchange.
|
|
1100
|
-
Previous messages are hidden but preserved.
|
|
1101
|
-
Type /focus again to exit focus mode and show all messages.
|
|
1102
|
-
`,
|
|
1103
|
-
}
|
|
1100
|
+
return { content: t('commands.focus.on') }
|
|
1104
1101
|
}
|
|
1105
1102
|
|
|
1106
1103
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1108,12 +1105,12 @@ const focusCmd: CommandHandler = (ctx) => {
|
|
|
1108
1105
|
// ═══════════════════════════════════════════════════════════════
|
|
1109
1106
|
|
|
1110
1107
|
const ultracodeCmd: CommandHandler = (ctx, args) => {
|
|
1108
|
+
const t = resolveT(ctx)
|
|
1111
1109
|
const arg = args[0]?.toLowerCase()
|
|
1112
1110
|
if (arg === 'on') {
|
|
1113
1111
|
ctx.setUltracodeMode(true)
|
|
1114
1112
|
return {
|
|
1115
|
-
content:
|
|
1116
|
-
'✓ Ultracode mode ON — multi-agent workflow orchestration enabled.\n\nThe AI will use Workflow tool to fan out, verify, and synthesize for all substantive tasks.',
|
|
1113
|
+
content: t('commands.ultracode.on'),
|
|
1117
1114
|
forwardToAI: stripIndent`
|
|
1118
1115
|
Ultracode mode is now ACTIVE. For every substantive task:
|
|
1119
1116
|
- Use the Workflow tool to orchestrate multi-agent execution
|
|
@@ -1129,24 +1126,12 @@ const ultracodeCmd: CommandHandler = (ctx, args) => {
|
|
|
1129
1126
|
} else if (arg === 'off') {
|
|
1130
1127
|
ctx.setUltracodeMode(false)
|
|
1131
1128
|
return {
|
|
1132
|
-
content: '
|
|
1129
|
+
content: t('commands.ultracode.off'),
|
|
1133
1130
|
forwardToAI:
|
|
1134
1131
|
'Ultracode mode is now OFF. Revert to standard single-agent execution. Do NOT use Workflow tool unless explicitly asked.',
|
|
1135
1132
|
}
|
|
1136
1133
|
}
|
|
1137
|
-
return {
|
|
1138
|
-
content: stripIndent`
|
|
1139
|
-
Usage: /ultracode [on|off]
|
|
1140
|
-
|
|
1141
|
-
Ultracode mode enables multi-agent workflow orchestration for every substantive task.
|
|
1142
|
-
When ON, the AI uses the Workflow tool to decompose tasks, fan out parallel agents,
|
|
1143
|
-
verify findings adversarially, and synthesize results with a top-tier model.
|
|
1144
|
-
|
|
1145
|
-
Examples:
|
|
1146
|
-
/ultracode on — enable multi-agent mode
|
|
1147
|
-
/ultracode off — return to standard single-agent mode
|
|
1148
|
-
`,
|
|
1149
|
-
}
|
|
1134
|
+
return { content: t('commands.ultracode.usage') }
|
|
1150
1135
|
}
|
|
1151
1136
|
|
|
1152
1137
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1154,6 +1139,7 @@ const ultracodeCmd: CommandHandler = (ctx, args) => {
|
|
|
1154
1139
|
// ═══════════════════════════════════════════════════════════════
|
|
1155
1140
|
|
|
1156
1141
|
const tasksCmd: CommandHandler = (ctx) => {
|
|
1142
|
+
const t = resolveT(ctx)
|
|
1157
1143
|
const c = ctx.engine.getContext()
|
|
1158
1144
|
const msgs = c.getMessages()
|
|
1159
1145
|
|
|
@@ -1169,15 +1155,11 @@ const tasksCmd: CommandHandler = (ctx) => {
|
|
|
1169
1155
|
|
|
1170
1156
|
return {
|
|
1171
1157
|
content: stripIndent`
|
|
1172
|
-
|
|
1158
|
+
${t('commands.task_list.title')}
|
|
1173
1159
|
|
|
1174
|
-
${
|
|
1175
|
-
toolUses.length > 0
|
|
1176
|
-
? `${toolUses.length} task operations detected in this session.\n\nUse Task tool (TaskCreate / TaskUpdate / TaskList) to manage structured task tracking.`
|
|
1177
|
-
: 'No tasks tracked yet. Use TaskCreate, TaskUpdate, and TaskList tools to manage structured tasks.'
|
|
1178
|
-
}
|
|
1160
|
+
${toolUses.length > 0 ? t('commands.task_list.detected', { count: String(toolUses.length) }) : t('commands.task_list.no_tasks')}
|
|
1179
1161
|
|
|
1180
|
-
|
|
1162
|
+
${t('commands.task_list.reference')}
|
|
1181
1163
|
TaskCreate — create a new task
|
|
1182
1164
|
TaskList — list all tasks
|
|
1183
1165
|
TaskUpdate — update task status
|
|
@@ -1185,35 +1167,28 @@ const tasksCmd: CommandHandler = (ctx) => {
|
|
|
1185
1167
|
TaskOutput — get background task output
|
|
1186
1168
|
TaskStop — stop a running task
|
|
1187
1169
|
|
|
1188
|
-
|
|
1170
|
+
${t('commands.task_list.legacy_hint')}
|
|
1189
1171
|
`,
|
|
1190
1172
|
}
|
|
1191
1173
|
}
|
|
1192
1174
|
|
|
1193
1175
|
const branchCmd: CommandHandler = (ctx, args) => {
|
|
1176
|
+
const t = resolveT(ctx)
|
|
1194
1177
|
const name = args.join(' ') || `branch-${Date.now()}`
|
|
1195
1178
|
const c = ctx.engine.getContext()
|
|
1196
1179
|
const msgs = c.getMessages()
|
|
1197
1180
|
|
|
1198
1181
|
if (msgs.length === 0) {
|
|
1199
|
-
return { content: '
|
|
1182
|
+
return { content: t('commands.branch.no_conversation') }
|
|
1200
1183
|
}
|
|
1201
1184
|
|
|
1202
|
-
// Save current session state as a named checkpoint
|
|
1203
1185
|
const checkpointId = c.saveCheckpoint(name)
|
|
1204
1186
|
return {
|
|
1205
|
-
content:
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
Current conversation continues from this point.
|
|
1212
|
-
To return to this branch point later, use:
|
|
1213
|
-
/rewind
|
|
1214
|
-
|
|
1215
|
-
Note: Full session branching (separate concurrent sessions) requires session persistence, coming in a future release. For now, this saves a named checkpoint you can rewind to.
|
|
1216
|
-
`,
|
|
1187
|
+
content: t('commands.branch.details', {
|
|
1188
|
+
name,
|
|
1189
|
+
checkpoint: String(checkpointId),
|
|
1190
|
+
messages: String(msgs.length),
|
|
1191
|
+
}),
|
|
1217
1192
|
}
|
|
1218
1193
|
}
|
|
1219
1194
|
|
|
@@ -1450,8 +1425,9 @@ const scheduleCmd: CommandHandler = async (_ctx) => {
|
|
|
1450
1425
|
// ═══════════════════════════════════════════════════════════════
|
|
1451
1426
|
|
|
1452
1427
|
const doctorCmd: CommandHandler = async (ctx) => {
|
|
1428
|
+
const t = resolveT(ctx)
|
|
1453
1429
|
const lines: string[] = [
|
|
1454
|
-
'
|
|
1430
|
+
t('commands.doctor.title'),
|
|
1455
1431
|
'',
|
|
1456
1432
|
`Mipham Code v${ctx.version}`,
|
|
1457
1433
|
`Runtime ${typeof Bun !== 'undefined' ? 'Bun ' + Bun.version : 'Node.js ' + process.version}`,
|
|
@@ -1459,12 +1435,12 @@ const doctorCmd: CommandHandler = async (ctx) => {
|
|
|
1459
1435
|
`CWD ${process.cwd()}`,
|
|
1460
1436
|
`PID ${process.pid}`,
|
|
1461
1437
|
'',
|
|
1462
|
-
'
|
|
1438
|
+
t('commands.doctor.config_section'),
|
|
1463
1439
|
`Provider ${ctx.providerId} / ${ctx.modelId}`,
|
|
1464
1440
|
`Permission ${ctx.config.permission}`,
|
|
1465
1441
|
`Providers ${ctx.config.providers.length} configured (${ctx.config.providers.filter((p) => p.status !== 'upcoming').length} active)`,
|
|
1466
1442
|
'',
|
|
1467
|
-
'
|
|
1443
|
+
t('commands.doctor.session_section'),
|
|
1468
1444
|
]
|
|
1469
1445
|
|
|
1470
1446
|
const c = ctx.engine.getContext()
|
|
@@ -1480,7 +1456,7 @@ const doctorCmd: CommandHandler = async (ctx) => {
|
|
|
1480
1456
|
try {
|
|
1481
1457
|
const { execSync } = await import('node:child_process')
|
|
1482
1458
|
lines.push('')
|
|
1483
|
-
lines.push('
|
|
1459
|
+
lines.push(t('commands.doctor.git_section'))
|
|
1484
1460
|
const branch = execSync('git branch --show-current', {
|
|
1485
1461
|
encoding: 'utf-8',
|
|
1486
1462
|
timeout: 3000,
|
|
@@ -1500,21 +1476,21 @@ const doctorCmd: CommandHandler = async (ctx) => {
|
|
|
1500
1476
|
)
|
|
1501
1477
|
} catch {
|
|
1502
1478
|
lines.push('')
|
|
1503
|
-
lines.push('
|
|
1504
|
-
lines.push(
|
|
1479
|
+
lines.push(t('commands.doctor.git_section'))
|
|
1480
|
+
lines.push(t('commands.doctor.no_git'))
|
|
1505
1481
|
}
|
|
1506
1482
|
|
|
1507
1483
|
// Skills info
|
|
1508
1484
|
if (ctx.skillsLoader) {
|
|
1509
1485
|
lines.push('')
|
|
1510
|
-
lines.push('
|
|
1486
|
+
lines.push(t('commands.doctor.skills_section'))
|
|
1511
1487
|
try {
|
|
1512
1488
|
const skills = ctx.skillsLoader.list()
|
|
1513
1489
|
const standard = skills.filter((s: { type: string }) => s.type === 'standard').length
|
|
1514
1490
|
const mipham = skills.filter((s: { type: string }) => s.type === 'mipham').length
|
|
1515
1491
|
lines.push(`Loaded ${skills.length} (${standard} standard + ${mipham} mipham)`)
|
|
1516
1492
|
} catch {
|
|
1517
|
-
lines.push(
|
|
1493
|
+
lines.push(t('commands.doctor.skills_unavailable'))
|
|
1518
1494
|
}
|
|
1519
1495
|
}
|
|
1520
1496
|
|
|
@@ -1527,68 +1503,67 @@ const doctorCmd: CommandHandler = async (ctx) => {
|
|
|
1527
1503
|
|
|
1528
1504
|
function gitDiffBridgeCmd(opts: {
|
|
1529
1505
|
label: string
|
|
1530
|
-
|
|
1531
|
-
|
|
1506
|
+
noChangesKey: string
|
|
1507
|
+
runningKey: string
|
|
1508
|
+
errorKey: string
|
|
1532
1509
|
forwardToAI: string | (() => string)
|
|
1533
1510
|
}): CommandHandler {
|
|
1534
|
-
return async () => {
|
|
1511
|
+
return async (ctx) => {
|
|
1512
|
+
const t = resolveT(ctx)
|
|
1535
1513
|
try {
|
|
1536
1514
|
const { execSync } = await import('node:child_process')
|
|
1537
1515
|
const diff = execSync('git diff --stat', { encoding: 'utf-8', timeout: 5000 }).trim()
|
|
1538
1516
|
if (!diff) {
|
|
1539
|
-
return { content: `─ ${opts.label} ─\n\n${opts.
|
|
1517
|
+
return { content: `─ ${opts.label} ─\n\n${t(opts.noChangesKey)}` }
|
|
1540
1518
|
}
|
|
1541
1519
|
return {
|
|
1542
|
-
content: `─ ${opts.label} ─\n\n${opts.
|
|
1520
|
+
content: `─ ${opts.label} ─\n\n${t(opts.runningKey)}\n\nChanged files:\n${diff}`,
|
|
1543
1521
|
forwardToAI: typeof opts.forwardToAI === 'function' ? opts.forwardToAI() : opts.forwardToAI,
|
|
1544
1522
|
}
|
|
1545
1523
|
} catch {
|
|
1546
|
-
return {
|
|
1547
|
-
content: `─ ${opts.label} ─\n\nCould not detect changes. Are you in a git repository?`,
|
|
1548
|
-
}
|
|
1524
|
+
return { content: `─ ${opts.label} ─\n\n${t(opts.errorKey)}` }
|
|
1549
1525
|
}
|
|
1550
1526
|
}
|
|
1551
1527
|
}
|
|
1552
1528
|
|
|
1553
1529
|
const codeReviewCmd = gitDiffBridgeCmd({
|
|
1554
1530
|
label: 'Code Review',
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
'Reviewing uncommitted changes with the code-review skill (7 dimensions: correctness, security, performance, code quality, architecture, testing, language-specific)...',
|
|
1531
|
+
noChangesKey: 'commands.code_review.no_changes',
|
|
1532
|
+
runningKey: 'commands.code_review.running',
|
|
1533
|
+
errorKey: 'commands.code_review.error',
|
|
1559
1534
|
forwardToAI: () =>
|
|
1560
1535
|
`use the code-review skill to review all uncommitted changes. Check all 7 dimensions: correctness, security, performance, code quality, architecture & design, testing, and language-specific issues. Use effort level: ${getPreference('lastCodeReviewEffort', 'high')}.`,
|
|
1561
1536
|
})
|
|
1562
1537
|
|
|
1563
1538
|
const simplifyCmd = gitDiffBridgeCmd({
|
|
1564
1539
|
label: 'Simplify',
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
'Running cleanup review — 4 passes: reuse, simplification, efficiency, abstraction level...',
|
|
1540
|
+
noChangesKey: 'commands.simplify.no_changes',
|
|
1541
|
+
runningKey: 'commands.simplify.running',
|
|
1542
|
+
errorKey: 'commands.simplify.error',
|
|
1569
1543
|
forwardToAI:
|
|
1570
1544
|
'use the self-review skill to review these uncommitted changes. Focus on 4 cleanup passes: 1) Reuse — find duplicated logic, replace with existing helpers; 2) Simplification — flatten nesting, remove redundant state and dead code; 3) Efficiency — fix repeated object creation, unnecessary I/O, memory issues; 4) Abstraction Level — ensure code sits at the right architectural layer. Apply equivalent transformations only — do NOT change logic or fix bugs.',
|
|
1571
1545
|
})
|
|
1572
1546
|
|
|
1573
1547
|
const verifyCmd = gitDiffBridgeCmd({
|
|
1574
1548
|
label: 'Verify',
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
'Running runtime verification — observing actual execution behavior (not tests, not typecheck)...',
|
|
1549
|
+
noChangesKey: 'commands.verify.no_changes',
|
|
1550
|
+
runningKey: 'commands.verify.running',
|
|
1551
|
+
errorKey: 'commands.verify.error',
|
|
1579
1552
|
forwardToAI:
|
|
1580
1553
|
'verify these uncommitted changes through runtime observation only. For each change: 1) Find the user-facing surface (CLI command, API endpoint, UI interaction); 2) Drive the changed code to execute; 3) Push boundaries — pass null, repeated values, wrong types, interrupt mid-flow (Ctrl-C), resize window; 4) Report verdict per change: PASS (works as expected), FAIL (does not work or breaks something), BLOCKED (cannot reach observable state), SKIP (no runtime surface, e.g. pure documentation). Do NOT run the test suite — observe real execution behavior only.',
|
|
1581
1554
|
})
|
|
1582
1555
|
|
|
1583
1556
|
const designCmd: CommandHandler = (_ctx, args) => {
|
|
1557
|
+
const t = resolveT(_ctx)
|
|
1584
1558
|
const topic = args.join(' ') || 'the current task'
|
|
1585
1559
|
return {
|
|
1586
|
-
content:
|
|
1560
|
+
content: t('commands.design.start', { topic }),
|
|
1587
1561
|
forwardToAI: `help me design the architecture for ${topic}. Explore 2-3 approaches with trade-offs, then present a design covering: component breakdown, data flow, interfaces between components, error handling strategy, and testing approach. Use the plan sub-agent if deeper analysis would help. Prefer simplicity — YAGNI.`,
|
|
1588
1562
|
}
|
|
1589
1563
|
}
|
|
1590
1564
|
|
|
1591
|
-
const lintCmd: CommandHandler = async () => {
|
|
1565
|
+
const lintCmd: CommandHandler = async (ctx) => {
|
|
1566
|
+
const t = resolveT(ctx)
|
|
1592
1567
|
try {
|
|
1593
1568
|
const { execSync } = await import('node:child_process')
|
|
1594
1569
|
const { existsSync } = await import('node:fs')
|
|
@@ -1620,19 +1595,15 @@ const lintCmd: CommandHandler = async () => {
|
|
|
1620
1595
|
|
|
1621
1596
|
return {
|
|
1622
1597
|
content: [
|
|
1623
|
-
'
|
|
1598
|
+
t('commands.lint.title'),
|
|
1624
1599
|
'',
|
|
1625
1600
|
truncated || '(no issues found)',
|
|
1626
1601
|
'',
|
|
1627
|
-
hasPackageJson
|
|
1628
|
-
? 'To fix: type "fix the lint errors"'
|
|
1629
|
-
: 'Set up linting: https://mipham.ai/code/docs/linting',
|
|
1602
|
+
hasPackageJson ? t('commands.lint.fix_hint') : t('commands.lint.setup_hint'),
|
|
1630
1603
|
].join('\n'),
|
|
1631
1604
|
}
|
|
1632
1605
|
} catch {
|
|
1633
|
-
return {
|
|
1634
|
-
content: '── Lint ──\n\nCould not run linter. Ensure ESLint is installed and configured.',
|
|
1635
|
-
}
|
|
1606
|
+
return { content: t('commands.lint.error') }
|
|
1636
1607
|
}
|
|
1637
1608
|
}
|
|
1638
1609
|
|
|
@@ -1640,7 +1611,8 @@ const lintCmd: CommandHandler = async () => {
|
|
|
1640
1611
|
// Session Enhancement Commands (Claude Code parity)
|
|
1641
1612
|
// ═══════════════════════════════════════════════════════════════
|
|
1642
1613
|
|
|
1643
|
-
const filesCmd: CommandHandler = async () => {
|
|
1614
|
+
const filesCmd: CommandHandler = async (ctx) => {
|
|
1615
|
+
const t = resolveT(ctx)
|
|
1644
1616
|
const { readdirSync, statSync } = await import('node:fs')
|
|
1645
1617
|
const { join } = await import('node:path')
|
|
1646
1618
|
const cwd = process.cwd()
|
|
@@ -1663,22 +1635,25 @@ const filesCmd: CommandHandler = async () => {
|
|
|
1663
1635
|
|
|
1664
1636
|
return {
|
|
1665
1637
|
content: [
|
|
1666
|
-
'
|
|
1638
|
+
t('commands.files.title'),
|
|
1667
1639
|
'',
|
|
1668
|
-
|
|
1640
|
+
t('commands.files.cwd', { path: cwd }),
|
|
1669
1641
|
'',
|
|
1670
1642
|
...items,
|
|
1671
|
-
entries.length > 40
|
|
1643
|
+
entries.length > 40
|
|
1644
|
+
? t('commands.files.more_files', { count: String(entries.length - 40) })
|
|
1645
|
+
: '',
|
|
1672
1646
|
'',
|
|
1673
|
-
'
|
|
1647
|
+
t('commands.files.hint'),
|
|
1674
1648
|
].join('\n'),
|
|
1675
1649
|
}
|
|
1676
1650
|
} catch {
|
|
1677
|
-
return { content: '
|
|
1651
|
+
return { content: t('commands.files.error') }
|
|
1678
1652
|
}
|
|
1679
1653
|
}
|
|
1680
1654
|
|
|
1681
1655
|
const statsCmd: CommandHandler = (ctx) => {
|
|
1656
|
+
const t = resolveT(ctx)
|
|
1682
1657
|
const c = ctx.engine.getContext()
|
|
1683
1658
|
const msgs = c.getMessages()
|
|
1684
1659
|
const tokens = c.getEstimatedTokens()
|
|
@@ -1690,26 +1665,32 @@ const statsCmd: CommandHandler = (ctx) => {
|
|
|
1690
1665
|
|
|
1691
1666
|
return {
|
|
1692
1667
|
content: [
|
|
1693
|
-
'
|
|
1668
|
+
t('commands.stats.title'),
|
|
1694
1669
|
'',
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1670
|
+
t('commands.stats.messages', {
|
|
1671
|
+
total: String(msgs.length),
|
|
1672
|
+
user: String(userMsgs),
|
|
1673
|
+
assistant: String(assistantMsgs),
|
|
1674
|
+
system: String(systemMsgs),
|
|
1675
|
+
}),
|
|
1676
|
+
t('commands.stats.tokens', { tokens: tokens.toLocaleString() }),
|
|
1677
|
+
t('commands.stats.tools', { count: String(tools.size) }),
|
|
1678
|
+
t('commands.stats.provider', { provider: ctx.providerId }),
|
|
1679
|
+
t('commands.stats.model', { model: ctx.modelId }),
|
|
1680
|
+
t('commands.stats.permission', { permission: ctx.config.permission }),
|
|
1701
1681
|
'',
|
|
1702
|
-
|
|
1682
|
+
t('commands.stats.usage', { pct: ((tokens / 200_000) * 100).toFixed(1) }),
|
|
1703
1683
|
].join('\n'),
|
|
1704
1684
|
}
|
|
1705
1685
|
}
|
|
1706
1686
|
|
|
1707
1687
|
const summaryCmd: CommandHandler = (ctx) => {
|
|
1688
|
+
const t = resolveT(ctx)
|
|
1708
1689
|
const c = ctx.engine.getContext()
|
|
1709
1690
|
const msgs = c.getMessages()
|
|
1710
1691
|
|
|
1711
1692
|
if (msgs.length === 0) {
|
|
1712
|
-
return { content: '
|
|
1693
|
+
return { content: t('commands.summary.no_conversation') }
|
|
1713
1694
|
}
|
|
1714
1695
|
|
|
1715
1696
|
const userMsgs = msgs
|
|
@@ -1722,32 +1703,24 @@ const summaryCmd: CommandHandler = (ctx) => {
|
|
|
1722
1703
|
|
|
1723
1704
|
return {
|
|
1724
1705
|
content: [
|
|
1725
|
-
'
|
|
1706
|
+
t('commands.summary.title'),
|
|
1726
1707
|
'',
|
|
1727
|
-
|
|
1728
|
-
|
|
1708
|
+
t('commands.summary.total_messages', { count: String(msgs.length) }),
|
|
1709
|
+
t('commands.summary.est_tokens', { tokens: c.getEstimatedTokens().toLocaleString() }),
|
|
1729
1710
|
'',
|
|
1730
|
-
'
|
|
1711
|
+
t('commands.summary.recent_topics'),
|
|
1731
1712
|
...userMsgs.map((t, i) => ` ${i + 1}. ${t}`),
|
|
1732
1713
|
'',
|
|
1733
|
-
'
|
|
1714
|
+
t('commands.summary.footer'),
|
|
1734
1715
|
].join('\n'),
|
|
1735
1716
|
}
|
|
1736
1717
|
}
|
|
1737
1718
|
|
|
1738
1719
|
const cdCmd: CommandHandler = async (ctx, args) => {
|
|
1720
|
+
const t = resolveT(ctx)
|
|
1739
1721
|
const target = args[0]
|
|
1740
1722
|
if (!target) {
|
|
1741
|
-
return {
|
|
1742
|
-
content: [
|
|
1743
|
-
'Usage: /cd <path>',
|
|
1744
|
-
'',
|
|
1745
|
-
'Change the session working directory.',
|
|
1746
|
-
'Example: /cd ~/projects/my-app',
|
|
1747
|
-
'',
|
|
1748
|
-
`Current: ${process.cwd()}`,
|
|
1749
|
-
].join('\n'),
|
|
1750
|
-
}
|
|
1723
|
+
return { content: t('commands.cd.usage', { cwd: process.cwd() }) }
|
|
1751
1724
|
}
|
|
1752
1725
|
|
|
1753
1726
|
const { existsSync } = await import('node:fs')
|
|
@@ -1755,13 +1728,12 @@ const cdCmd: CommandHandler = async (ctx, args) => {
|
|
|
1755
1728
|
const resolved = resolve(target.replace(/^~/, process.env.HOME || '~'))
|
|
1756
1729
|
|
|
1757
1730
|
if (!existsSync(resolved)) {
|
|
1758
|
-
return { content:
|
|
1731
|
+
return { content: t('commands.cd.not_found', { path: resolved }) }
|
|
1759
1732
|
}
|
|
1760
1733
|
|
|
1761
1734
|
try {
|
|
1762
1735
|
process.chdir(resolved)
|
|
1763
1736
|
|
|
1764
|
-
// Persist cwd to active session (best-effort)
|
|
1765
1737
|
try {
|
|
1766
1738
|
const { SessionStore } = await import('../core/session-store')
|
|
1767
1739
|
const saved = SessionStore.load(ctx.sessionId)
|
|
@@ -1776,47 +1748,31 @@ const cdCmd: CommandHandler = async (ctx, args) => {
|
|
|
1776
1748
|
/* session persistence is best-effort */
|
|
1777
1749
|
}
|
|
1778
1750
|
|
|
1779
|
-
return {
|
|
1780
|
-
content: [
|
|
1781
|
-
'── Directory Changed ──',
|
|
1782
|
-
'',
|
|
1783
|
-
`New CWD: ${resolved}`,
|
|
1784
|
-
'',
|
|
1785
|
-
'The AI will now work relative to this directory.',
|
|
1786
|
-
'Note: This changes the filesystem root for tools like Read, Write, Bash.',
|
|
1787
|
-
].join('\n'),
|
|
1788
|
-
}
|
|
1751
|
+
return { content: t('commands.cd.content', { path: resolved }) }
|
|
1789
1752
|
} catch (err) {
|
|
1790
|
-
return { content:
|
|
1753
|
+
return { content: t('commands.cd.failed', { error: (err as Error).message }) }
|
|
1791
1754
|
}
|
|
1792
1755
|
}
|
|
1793
1756
|
|
|
1794
|
-
const hooksCmd: CommandHandler = async () => {
|
|
1757
|
+
const hooksCmd: CommandHandler = async (ctx) => {
|
|
1758
|
+
const t = resolveT(ctx)
|
|
1795
1759
|
const { existsSync, readdirSync } = await import('node:fs')
|
|
1796
1760
|
const { join } = await import('node:path')
|
|
1797
1761
|
const cwd = process.cwd()
|
|
1798
1762
|
const hooksDir = join(cwd, '.mipham', 'hooks')
|
|
1799
1763
|
|
|
1800
1764
|
if (!existsSync(hooksDir)) {
|
|
1801
|
-
return {
|
|
1802
|
-
content: [
|
|
1803
|
-
'── Hooks ──',
|
|
1804
|
-
'',
|
|
1805
|
-
'No hooks configured.',
|
|
1806
|
-
'',
|
|
1807
|
-
'Create hook scripts in .mipham/hooks/:',
|
|
1808
|
-
' pre-tool-use.sh — runs before each tool',
|
|
1809
|
-
' post-tool-use.sh — runs after each tool',
|
|
1810
|
-
' stop.sh — runs when session ends',
|
|
1811
|
-
'',
|
|
1812
|
-
'Use /loop init to scaffold hooks directory.',
|
|
1813
|
-
].join('\n'),
|
|
1814
|
-
}
|
|
1765
|
+
return { content: t('commands.hooks.no_hooks') }
|
|
1815
1766
|
}
|
|
1816
1767
|
|
|
1817
1768
|
try {
|
|
1818
1769
|
const files = readdirSync(hooksDir).filter((f) => f.endsWith('.sh'))
|
|
1819
|
-
const lines: string[] = [
|
|
1770
|
+
const lines: string[] = [
|
|
1771
|
+
t('commands.hooks.title'),
|
|
1772
|
+
'',
|
|
1773
|
+
t('commands.hooks.location', { path: hooksDir }),
|
|
1774
|
+
'',
|
|
1775
|
+
]
|
|
1820
1776
|
|
|
1821
1777
|
for (const f of files) {
|
|
1822
1778
|
try {
|
|
@@ -1831,35 +1787,16 @@ const hooksCmd: CommandHandler = async () => {
|
|
|
1831
1787
|
}
|
|
1832
1788
|
|
|
1833
1789
|
lines.push('')
|
|
1834
|
-
lines.push(
|
|
1790
|
+
lines.push(t('commands.hooks.found', { count: String(files.length) }))
|
|
1835
1791
|
return { content: lines.join('\n') }
|
|
1836
1792
|
} catch {
|
|
1837
|
-
return { content: '
|
|
1793
|
+
return { content: t('commands.hooks.error') }
|
|
1838
1794
|
}
|
|
1839
1795
|
}
|
|
1840
1796
|
|
|
1841
|
-
const batchCmd: CommandHandler = async () => {
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
'── Batch Operations ──',
|
|
1845
|
-
'',
|
|
1846
|
-
'Apply a change across multiple files or directories.',
|
|
1847
|
-
'',
|
|
1848
|
-
'Usage: type "apply this change to all .ts files in src/"',
|
|
1849
|
-
'',
|
|
1850
|
-
'The AI will:',
|
|
1851
|
-
' 1. Understand your change description',
|
|
1852
|
-
' 2. Find all matching files',
|
|
1853
|
-
' 3. Apply the change consistently',
|
|
1854
|
-
' 4. Report what was modified',
|
|
1855
|
-
'',
|
|
1856
|
-
'Use this for:',
|
|
1857
|
-
' • Renaming symbols across codebase',
|
|
1858
|
-
' • Updating import patterns',
|
|
1859
|
-
' • Applying consistent formatting changes',
|
|
1860
|
-
' • Bulk config updates',
|
|
1861
|
-
].join('\n'),
|
|
1862
|
-
}
|
|
1797
|
+
const batchCmd: CommandHandler = async (ctx) => {
|
|
1798
|
+
const t = resolveT(ctx)
|
|
1799
|
+
return { content: t('commands.batch.title') }
|
|
1863
1800
|
}
|
|
1864
1801
|
|
|
1865
1802
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1867,6 +1804,7 @@ const batchCmd: CommandHandler = async () => {
|
|
|
1867
1804
|
// ═══════════════════════════════════════════════════════════════
|
|
1868
1805
|
|
|
1869
1806
|
const exportCmd: CommandHandler = async (ctx) => {
|
|
1807
|
+
const t = resolveT(ctx)
|
|
1870
1808
|
const { writeFileSync } = await import('node:fs')
|
|
1871
1809
|
const { join } = await import('node:path')
|
|
1872
1810
|
|
|
@@ -1897,7 +1835,11 @@ const exportCmd: CommandHandler = async (ctx) => {
|
|
|
1897
1835
|
|
|
1898
1836
|
writeFileSync(filepath, lines.join('\n'), 'utf-8')
|
|
1899
1837
|
return {
|
|
1900
|
-
content:
|
|
1838
|
+
content: t('commands.export.confirmed', {
|
|
1839
|
+
path: filepath,
|
|
1840
|
+
count: String(msgs.length),
|
|
1841
|
+
lines: String(lines.length),
|
|
1842
|
+
}),
|
|
1901
1843
|
copyContent: filepath,
|
|
1902
1844
|
}
|
|
1903
1845
|
}
|
|
@@ -1976,21 +1918,21 @@ const prCommentsCmd: CommandHandler = async () => {
|
|
|
1976
1918
|
// Resume
|
|
1977
1919
|
// ═══════════════════════════════════════════════════════════════
|
|
1978
1920
|
|
|
1979
|
-
const resumeLastCmd: CommandHandler = async () => {
|
|
1921
|
+
const resumeLastCmd: CommandHandler = async (ctx) => {
|
|
1922
|
+
const t = resolveT(ctx)
|
|
1980
1923
|
const { SessionStore } = await import('../core/session-store')
|
|
1981
1924
|
|
|
1982
1925
|
const latest = SessionStore.getLatest()
|
|
1983
1926
|
if (!latest) {
|
|
1984
1927
|
return {
|
|
1985
|
-
content:
|
|
1986
|
-
'─ Resume Session ─\n\nNo saved sessions found.\n\nSessions are auto-saved to ~/.mipham/sessions/ when Mipham Code exits.',
|
|
1928
|
+
content: `${t('commands.resume.restored')}\n\n${t('commands.resume.no_sessions')}\n\n${t('commands.resume.empty_footer')}`,
|
|
1987
1929
|
}
|
|
1988
1930
|
}
|
|
1989
1931
|
|
|
1990
1932
|
const session = SessionStore.load(latest.name)
|
|
1991
1933
|
if (!session) {
|
|
1992
1934
|
return {
|
|
1993
|
-
content:
|
|
1935
|
+
content: `${t('commands.resume.load_failed')}\n\nCould not load session "${latest.name}". The file may have been removed.`,
|
|
1994
1936
|
}
|
|
1995
1937
|
}
|
|
1996
1938
|
|
|
@@ -1999,79 +1941,71 @@ const resumeLastCmd: CommandHandler = async () => {
|
|
|
1999
1941
|
const messages = truncated ? session.messages.slice(-MAX_RESUME) : session.messages
|
|
2000
1942
|
|
|
2001
1943
|
const date = new Date(latest.updatedAt).toLocaleString()
|
|
1944
|
+
const truncatedStr = truncated ? ` (showing last ${MAX_RESUME})` : ''
|
|
1945
|
+
|
|
2002
1946
|
return {
|
|
2003
1947
|
content: [
|
|
2004
|
-
'
|
|
1948
|
+
t('commands.resume.restored'),
|
|
2005
1949
|
'',
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
1950
|
+
t('commands.resume.restored_content', {
|
|
1951
|
+
name: latest.name,
|
|
1952
|
+
total: String(session.messages.length),
|
|
1953
|
+
truncated: truncatedStr,
|
|
1954
|
+
provider: latest.provider,
|
|
1955
|
+
model: latest.model,
|
|
1956
|
+
date,
|
|
1957
|
+
}),
|
|
2010
1958
|
'',
|
|
2011
1959
|
truncated
|
|
2012
|
-
?
|
|
2013
|
-
|
|
1960
|
+
? t('commands.resume.restored_footer', {
|
|
1961
|
+
loaded: String(messages.length),
|
|
1962
|
+
total: String(session.messages.length),
|
|
1963
|
+
})
|
|
1964
|
+
: t('commands.resume.restored_full_footer', { loaded: String(messages.length) }),
|
|
2014
1965
|
].join('\n'),
|
|
2015
1966
|
forwardedMessages: messages,
|
|
2016
1967
|
}
|
|
2017
1968
|
}
|
|
2018
1969
|
|
|
2019
|
-
const resumeDeleteCmd: CommandHandler = async (
|
|
1970
|
+
const resumeDeleteCmd: CommandHandler = async (ctx, args) => {
|
|
1971
|
+
const t = resolveT(ctx)
|
|
2020
1972
|
const name = args.join(' ').trim()
|
|
2021
1973
|
if (!name) {
|
|
2022
|
-
return {
|
|
2023
|
-
content:
|
|
2024
|
-
'Usage: /resume delete <session-name>\n\nDelete a saved session. Use /resume to list all sessions.',
|
|
2025
|
-
}
|
|
1974
|
+
return { content: t('commands.resume.delete_usage') }
|
|
2026
1975
|
}
|
|
2027
1976
|
|
|
2028
1977
|
const { SessionStore } = await import('../core/session-store')
|
|
2029
1978
|
const deleted = SessionStore.delete(name)
|
|
2030
1979
|
|
|
2031
1980
|
return deleted
|
|
2032
|
-
? { content:
|
|
2033
|
-
: { content:
|
|
1981
|
+
? { content: t('commands.resume.delete_confirmed', { name }) }
|
|
1982
|
+
: { content: t('commands.resume.delete_not_found', { name }) }
|
|
2034
1983
|
}
|
|
2035
1984
|
|
|
2036
|
-
const resumeCmd: CommandHandler = async (
|
|
1985
|
+
const resumeCmd: CommandHandler = async (ctx, args) => {
|
|
1986
|
+
const t = resolveT(ctx)
|
|
2037
1987
|
const sub = args[0]?.toLowerCase()
|
|
2038
1988
|
|
|
2039
|
-
// Sub-command: /resume last
|
|
2040
1989
|
if (sub === 'last') {
|
|
2041
|
-
return resumeLastCmd(
|
|
1990
|
+
return resumeLastCmd(ctx, args.slice(1))
|
|
2042
1991
|
}
|
|
2043
1992
|
|
|
2044
|
-
// Sub-command: /resume delete <name>
|
|
2045
1993
|
if (sub === 'delete') {
|
|
2046
|
-
return resumeDeleteCmd(
|
|
1994
|
+
return resumeDeleteCmd(ctx, args.slice(1))
|
|
2047
1995
|
}
|
|
2048
1996
|
|
|
2049
1997
|
const { SessionStore } = await import('../core/session-store')
|
|
2050
1998
|
|
|
2051
|
-
// If a name is provided, show load instructions
|
|
2052
1999
|
const targetName = args.join(' ')
|
|
2053
2000
|
if (targetName) {
|
|
2054
2001
|
const session = SessionStore.load(targetName)
|
|
2055
2002
|
if (session) {
|
|
2056
2003
|
return {
|
|
2057
|
-
content:
|
|
2058
|
-
'─ Session Found ─',
|
|
2059
|
-
'',
|
|
2060
|
-
`Name: ${session.metadata.name}`,
|
|
2061
|
-
`Messages: ${session.metadata.messageCount}`,
|
|
2062
|
-
`Provider: ${session.metadata.provider} / ${session.metadata.model}`,
|
|
2063
|
-
`Updated: ${session.metadata.updatedAt}`,
|
|
2064
|
-
'',
|
|
2065
|
-
'To resume this session:',
|
|
2066
|
-
` /resume last — restore the most recent session`,
|
|
2067
|
-
` mipham --resume "${targetName}"`,
|
|
2068
|
-
'',
|
|
2069
|
-
'Or restart Mipham Code — the most recent session loads automatically.',
|
|
2070
|
-
].join('\n'),
|
|
2004
|
+
content: `${t('commands.resume.found_title')}\n\n${t('commands.resume.found_content', { name: session.metadata.name, messages: String(session.metadata.messageCount), provider: session.metadata.provider, model: session.metadata.model, updated: session.metadata.updatedAt, target: targetName })}`,
|
|
2071
2005
|
}
|
|
2072
2006
|
}
|
|
2073
2007
|
return {
|
|
2074
|
-
content:
|
|
2008
|
+
content: `${t('commands.resume.not_found_title')}\n\n${t('commands.resume.not_found_content', { name: targetName })}`,
|
|
2075
2009
|
}
|
|
2076
2010
|
}
|
|
2077
2011
|
|
|
@@ -2079,29 +2013,28 @@ const resumeCmd: CommandHandler = async (_ctx, args) => {
|
|
|
2079
2013
|
|
|
2080
2014
|
if (sessions.length === 0) {
|
|
2081
2015
|
return {
|
|
2082
|
-
content:
|
|
2083
|
-
'─ Resume Session ─\n\nNo saved sessions found.\n\nSessions are auto-saved to ~/.mipham/sessions/ when Mipham Code exits.\nStart a conversation — it will be saved automatically.',
|
|
2016
|
+
content: `${t('commands.resume.restored')}\n\n${t('commands.resume.no_sessions')}\n\n${t('commands.resume.empty_footer')}`,
|
|
2084
2017
|
}
|
|
2085
2018
|
}
|
|
2086
2019
|
|
|
2087
2020
|
const recent = sessions.slice(0, 10)
|
|
2088
2021
|
|
|
2089
2022
|
const lines: string[] = [
|
|
2090
|
-
'
|
|
2023
|
+
t('commands.resume.saved_title'),
|
|
2091
2024
|
'',
|
|
2092
2025
|
...recent.map(
|
|
2093
2026
|
(s, i) =>
|
|
2094
2027
|
` ${(i + 1).toString().padStart(2)}. ${s.name.padEnd(45)} ${s.messageCount.toString().padStart(4)} msgs ${new Date(s.updatedAt).toLocaleString()}`,
|
|
2095
2028
|
),
|
|
2096
2029
|
'',
|
|
2097
|
-
|
|
2030
|
+
t('commands.resume.total_footer', { count: String(sessions.length) }),
|
|
2098
2031
|
'',
|
|
2099
|
-
'
|
|
2100
|
-
'
|
|
2101
|
-
'
|
|
2102
|
-
'
|
|
2032
|
+
t('commands.resume.resume_hint'),
|
|
2033
|
+
t('commands.resume.resume_last_hint'),
|
|
2034
|
+
t('commands.resume.delete_hint'),
|
|
2035
|
+
t('commands.resume.cli_hint'),
|
|
2103
2036
|
'',
|
|
2104
|
-
'
|
|
2037
|
+
t('commands.resume.auto_save_hint'),
|
|
2105
2038
|
]
|
|
2106
2039
|
|
|
2107
2040
|
return { content: lines.join('\n') }
|
|
@@ -2111,7 +2044,8 @@ const resumeCmd: CommandHandler = async (_ctx, args) => {
|
|
|
2111
2044
|
// Memory Management
|
|
2112
2045
|
// ═══════════════════════════════════════════════════════════════
|
|
2113
2046
|
|
|
2114
|
-
const memoryCmd: CommandHandler = async () => {
|
|
2047
|
+
const memoryCmd: CommandHandler = async (ctx) => {
|
|
2048
|
+
const t = resolveT(ctx)
|
|
2115
2049
|
const { existsSync, readdirSync, readFileSync, statSync } = await import('node:fs')
|
|
2116
2050
|
const { join } = await import('node:path')
|
|
2117
2051
|
|
|
@@ -2119,16 +2053,13 @@ const memoryCmd: CommandHandler = async () => {
|
|
|
2119
2053
|
const memoryDir = join(home, '.mipham', 'memory')
|
|
2120
2054
|
|
|
2121
2055
|
if (!existsSync(memoryDir)) {
|
|
2122
|
-
return {
|
|
2123
|
-
content:
|
|
2124
|
-
'─ Memory ─\n\nNo memories stored yet.\n\nMemory is saved to ~/.mipham/memory/ by the AI when you ask it to remember something.\nTry: "remember that I prefer TypeScript"',
|
|
2125
|
-
}
|
|
2056
|
+
return { content: t('commands.memories.no_memories') }
|
|
2126
2057
|
}
|
|
2127
2058
|
|
|
2128
2059
|
try {
|
|
2129
2060
|
const files = readdirSync(memoryDir).filter((f) => f.endsWith('.md'))
|
|
2130
2061
|
if (files.length === 0) {
|
|
2131
|
-
return { content: '
|
|
2062
|
+
return { content: t('commands.memories.empty') }
|
|
2132
2063
|
}
|
|
2133
2064
|
|
|
2134
2065
|
const memories: Array<{ file: string; size: number; mtime: Date; title: string }> = []
|
|
@@ -2149,23 +2080,25 @@ const memoryCmd: CommandHandler = async () => {
|
|
|
2149
2080
|
memories.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
|
|
2150
2081
|
|
|
2151
2082
|
const lines: string[] = [
|
|
2152
|
-
'
|
|
2083
|
+
t('commands.memories.title'),
|
|
2153
2084
|
'',
|
|
2154
|
-
|
|
2155
|
-
|
|
2085
|
+
t('commands.memories.location', { path: memoryDir }),
|
|
2086
|
+
t('commands.memories.total', {
|
|
2087
|
+
count: String(memories.length),
|
|
2088
|
+
plural: memories.length === 1 ? 'y' : 'ies',
|
|
2089
|
+
}),
|
|
2156
2090
|
'',
|
|
2157
2091
|
...memories.map(
|
|
2158
2092
|
(m, i) =>
|
|
2159
2093
|
` ${(i + 1).toString().padStart(2)}. ${m.file.padEnd(35)} ${(m.size / 1024).toFixed(1)}KB ${m.mtime.toLocaleDateString()} ${m.title}`,
|
|
2160
2094
|
),
|
|
2161
2095
|
'',
|
|
2162
|
-
'
|
|
2163
|
-
'Each file in ~/.mipham/memory/ represents one remembered fact or preference.',
|
|
2096
|
+
t('commands.memories.footer'),
|
|
2164
2097
|
]
|
|
2165
2098
|
|
|
2166
2099
|
return { content: lines.join('\n') }
|
|
2167
2100
|
} catch {
|
|
2168
|
-
return { content: '
|
|
2101
|
+
return { content: t('commands.memories.error') }
|
|
2169
2102
|
}
|
|
2170
2103
|
}
|
|
2171
2104
|
|
|
@@ -2173,7 +2106,8 @@ const memoryCmd: CommandHandler = async () => {
|
|
|
2173
2106
|
// Upgrade
|
|
2174
2107
|
// ═══════════════════════════════════════════════════════════════
|
|
2175
2108
|
|
|
2176
|
-
const upgradeCmd: CommandHandler = async () => {
|
|
2109
|
+
const upgradeCmd: CommandHandler = async (ctx) => {
|
|
2110
|
+
const t = resolveT(ctx)
|
|
2177
2111
|
const { checkForUpdates, backupConfig, performUpdate, restoreConfig, getConfigPath } =
|
|
2178
2112
|
await import('../shared/update')
|
|
2179
2113
|
|
|
@@ -2181,27 +2115,16 @@ const upgradeCmd: CommandHandler = async () => {
|
|
|
2181
2115
|
|
|
2182
2116
|
if (!update.available) {
|
|
2183
2117
|
return {
|
|
2184
|
-
content:
|
|
2185
|
-
|
|
2186
|
-
Current version: v${update.current}
|
|
2187
|
-
Latest: v${update.latest}
|
|
2188
|
-
|
|
2189
|
-
✓ Already up to date.
|
|
2190
|
-
|
|
2191
|
-
To check manually: https://www.npmjs.com/package/@miphamai/cli`,
|
|
2118
|
+
content: t('commands.upgrade.uptodate', { current: update.current, latest: update.latest }),
|
|
2192
2119
|
}
|
|
2193
2120
|
}
|
|
2194
2121
|
|
|
2195
|
-
// Update available — back up config and perform the upgrade
|
|
2196
2122
|
const backupPath = backupConfig(`upgrade-v${update.current}`)
|
|
2197
2123
|
|
|
2198
2124
|
const lines: string[] = [
|
|
2199
|
-
'
|
|
2125
|
+
t('commands.upgrade.title'),
|
|
2200
2126
|
'',
|
|
2201
|
-
|
|
2202
|
-
`Latest: v${update.latest}`,
|
|
2203
|
-
'',
|
|
2204
|
-
`→ New version available! Updating...`,
|
|
2127
|
+
t('commands.upgrade.available', { current: update.current, latest: update.latest }),
|
|
2205
2128
|
'',
|
|
2206
2129
|
]
|
|
2207
2130
|
|
|
@@ -2215,39 +2138,49 @@ To check manually: https://www.npmjs.com/package/@miphamai/cli`,
|
|
|
2215
2138
|
const configPath = getConfigPath()
|
|
2216
2139
|
const { existsSync } = await import('node:fs')
|
|
2217
2140
|
lines.push('')
|
|
2218
|
-
lines.push(
|
|
2141
|
+
lines.push(t('commands.upgrade.updated', { version: update.latest }))
|
|
2219
2142
|
|
|
2220
2143
|
if (existsSync(configPath)) {
|
|
2221
|
-
lines.push(
|
|
2144
|
+
lines.push(t('commands.upgrade.config_preserved', { path: configPath }))
|
|
2222
2145
|
} else if (backupPath) {
|
|
2223
2146
|
if (restoreConfig(backupPath)) {
|
|
2224
|
-
lines.push('
|
|
2147
|
+
lines.push(t('commands.upgrade.config_restored'))
|
|
2225
2148
|
}
|
|
2226
2149
|
}
|
|
2227
2150
|
|
|
2228
2151
|
lines.push('')
|
|
2229
|
-
lines.push('
|
|
2230
|
-
lines.push(' Run `mipham` again to use the new version, or `mipham --version` to verify.')
|
|
2152
|
+
lines.push(t('commands.upgrade.old_version_warning'))
|
|
2231
2153
|
} else {
|
|
2232
2154
|
lines.push('')
|
|
2233
|
-
lines.push(
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
lines.push(` Your config backup is at: ${backupPath}`)
|
|
2237
|
-
}
|
|
2155
|
+
lines.push(
|
|
2156
|
+
t('commands.upgrade.update_failed', { command: NPM_UPDATE_COMMAND, path: backupPath || '' }),
|
|
2157
|
+
)
|
|
2238
2158
|
}
|
|
2239
2159
|
|
|
2240
2160
|
return { content: lines.join('\n') }
|
|
2241
2161
|
}
|
|
2242
2162
|
|
|
2163
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2164
|
+
// Language — i18n locale control
|
|
2165
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2166
|
+
|
|
2167
|
+
const langCmd: CommandHandler = (ctx, args) => {
|
|
2168
|
+
const t = resolveT(ctx)
|
|
2169
|
+
const requested = args[0]
|
|
2170
|
+
if (!requested || !['en-US', 'zh-CN'].includes(requested)) {
|
|
2171
|
+
return { content: t('commands.lang.current', { locale: 'en-US' }) }
|
|
2172
|
+
}
|
|
2173
|
+
return { content: t('commands.lang.set', { locale: requested }) }
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2243
2176
|
// ═══════════════════════════════════════════════════════════════
|
|
2244
2177
|
// No-Plan — exit plan mode
|
|
2245
2178
|
// ═══════════════════════════════════════════════════════════════
|
|
2246
2179
|
|
|
2247
|
-
const noPlanCmd: CommandHandler = () =>
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
}
|
|
2180
|
+
const noPlanCmd: CommandHandler = (ctx) => {
|
|
2181
|
+
const t = resolveT(ctx)
|
|
2182
|
+
return { content: t('commands.no_plan.confirmed') }
|
|
2183
|
+
}
|
|
2251
2184
|
|
|
2252
2185
|
// ═══════════════════════════════════════════════════════════════
|
|
2253
2186
|
// Workflows
|
|
@@ -3310,6 +3243,7 @@ registry.set('/prompt-audit', promptAuditCmd)
|
|
|
3310
3243
|
|
|
3311
3244
|
// Environment
|
|
3312
3245
|
registry.set('/theme', themeCmd)
|
|
3246
|
+
registry.set('/lang', langCmd)
|
|
3313
3247
|
registry.set('/ide', ideCmd)
|
|
3314
3248
|
registry.set('/terminal-setup', terminalSetupCmd)
|
|
3315
3249
|
registry.set('/release-notes', releaseNotesCmd)
|
|
@@ -3407,6 +3341,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
3407
3341
|
'/fast': 'Toggle fast mode',
|
|
3408
3342
|
'/effort': 'Set reasoning effort',
|
|
3409
3343
|
'/theme': 'Set terminal theme',
|
|
3344
|
+
'/lang': 'Set display language (en-US, zh-CN)',
|
|
3410
3345
|
'/tools': 'List available tools',
|
|
3411
3346
|
'/skills': 'List loaded skills',
|
|
3412
3347
|
'/reload-skills': 'Reload all skills',
|