@miphamai/cli 0.19.4 → 0.20.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/bin/mipham.ts +1 -1
- package/package.json +2 -1
- package/src/agent/sub-agent.ts +1 -1
- package/src/agent-view/dashboard.tsx +2 -2
- package/src/commands/environment.ts +2 -3
- package/src/commands/git.ts +0 -1
- package/src/commands/project.ts +1 -6
- package/src/config/loader.ts +1 -1
- package/src/core/credential-masker.ts +0 -1
- package/src/core/engine.ts +42 -18
- package/src/core/hooks.ts +0 -1
- package/src/core/rules-loader.ts +2 -2
- package/src/i18n-context.tsx +37 -0
- package/src/index.tsx +27 -11
- package/src/mcp/client.ts +13 -3
- package/src/mcp/oauth.ts +14 -4
- package/src/mcp/registry.ts +12 -2
- package/src/skills/registry.ts +1 -1
- package/src/tools/agent/skill.ts +1 -1
- package/src/tools/exec/enter-worktree.ts +1 -1
- package/src/tools/index.ts +18 -8
- package/src/ui/agent-footer.tsx +3 -1
- package/src/ui/app.tsx +22 -16
- package/src/ui/chat.tsx +19 -15
- package/src/ui/command-picker.tsx +13 -8
- package/src/ui/commands.ts +310 -434
- package/src/ui/input.tsx +55 -103
- package/src/ui/picker.tsx +10 -8
package/src/ui/commands.ts
CHANGED
|
@@ -10,9 +10,23 @@ import type { SkillsLoader } from '../skills/loader'
|
|
|
10
10
|
import type { PluginManager } from '../plugin/plugin-manager'
|
|
11
11
|
import type { Message } from '../shared/types.js'
|
|
12
12
|
import { McpClient } from '../mcp/client'
|
|
13
|
-
import {
|
|
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 '@mipham/shared/i18n/t'
|
|
17
|
+
import type { TranslationMap } from '@mipham/shared/i18n/types'
|
|
18
|
+
import enUS from '@mipham/shared/i18n/locales/en-US.json' with { type: 'json' }
|
|
19
|
+
import zhCN from '@mipham/shared/i18n/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,42 @@ 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 { content: t('commands.provider.current', { provider: ctx.providerId, model: ctx.modelId }) }
|
|
355
|
+
}
|
|
330
356
|
|
|
331
357
|
const providersCmd: CommandHandler = (ctx) => {
|
|
358
|
+
const t = resolveT(ctx)
|
|
332
359
|
const lines = ctx.config.providers.map(
|
|
333
360
|
(p) =>
|
|
334
361
|
` ${p.id.padEnd(14)} ${p.name.padEnd(20)} ${p.protocol.padEnd(18)} ${p.models.length} models ${p.status === 'upcoming' ? '[upcoming]' : '✓'}`,
|
|
335
362
|
)
|
|
336
363
|
return {
|
|
337
|
-
content:
|
|
364
|
+
content: `${t('commands.providers.title')}\n\n${lines.join('\n')}\n\n${t('commands.providers.current', { provider: ctx.providerId, model: ctx.modelId })}`,
|
|
338
365
|
}
|
|
339
366
|
}
|
|
340
367
|
|
|
341
368
|
const switchCmd: CommandHandler = (ctx, args) => {
|
|
369
|
+
const t = resolveT(ctx)
|
|
342
370
|
const [newProvider, newModel] = args
|
|
343
371
|
if (!newProvider || !newModel) {
|
|
344
|
-
return {
|
|
345
|
-
content: 'Usage: /switch <provider> <model>\nExample: /switch deepseek deepseek-v4-pro',
|
|
346
|
-
}
|
|
372
|
+
return { content: t('commands.switch.usage') }
|
|
347
373
|
}
|
|
348
374
|
ctx.engine.switchProvider(newProvider, newModel)
|
|
349
375
|
return {
|
|
350
|
-
content:
|
|
376
|
+
content: t('commands.switch.confirmed', { provider: newProvider, model: newModel }),
|
|
351
377
|
nextProvider: newProvider,
|
|
352
378
|
nextModel: newModel,
|
|
353
379
|
}
|
|
354
380
|
}
|
|
355
381
|
|
|
356
382
|
const configCmd: CommandHandler = (ctx) => {
|
|
383
|
+
const t = resolveT(ctx)
|
|
357
384
|
const c = ctx.config
|
|
358
385
|
const lines = [
|
|
359
386
|
`version: ${c.version}`,
|
|
@@ -363,7 +390,7 @@ const configCmd: CommandHandler = (ctx) => {
|
|
|
363
390
|
`providers: ${c.providers.length} configured`,
|
|
364
391
|
]
|
|
365
392
|
return {
|
|
366
|
-
content:
|
|
393
|
+
content: `${t('commands.config.title')}\n${lines.join('\n')}\n\n${t('commands.config.edit')}`,
|
|
367
394
|
}
|
|
368
395
|
}
|
|
369
396
|
|
|
@@ -372,6 +399,7 @@ const configCmd: CommandHandler = (ctx) => {
|
|
|
372
399
|
// ═══════════════════════════════════════════════════════════════
|
|
373
400
|
|
|
374
401
|
const toolsCmd: CommandHandler = (ctx) => {
|
|
402
|
+
const t = resolveT(ctx)
|
|
375
403
|
const tools = ctx.engine.getTools()
|
|
376
404
|
const categories: Record<string, string[]> = {
|
|
377
405
|
file: [],
|
|
@@ -389,12 +417,13 @@ const toolsCmd: CommandHandler = (ctx) => {
|
|
|
389
417
|
.filter(([, v]) => v!.length > 0)
|
|
390
418
|
.map(([cat, items]) => `── ${cat.toUpperCase()} ──\n${items!.join('\n')}`)
|
|
391
419
|
|
|
392
|
-
return { content:
|
|
420
|
+
return { content: `${t('commands.tools.title', { count: String(tools.size) })}\n\n${sections.join('\n\n')}` }
|
|
393
421
|
}
|
|
394
422
|
|
|
395
423
|
const skillsCmd: CommandHandler = (ctx) => {
|
|
424
|
+
const t = resolveT(ctx)
|
|
396
425
|
if (!ctx.skillsLoader) {
|
|
397
|
-
return { content: '
|
|
426
|
+
return { content: t('commands.skills.unavailable') }
|
|
398
427
|
}
|
|
399
428
|
const counts = ctx.skillsLoader.countByType()
|
|
400
429
|
const standard = ctx.skillsLoader.listByType('standard')
|
|
@@ -407,7 +436,7 @@ const skillsCmd: CommandHandler = (ctx) => {
|
|
|
407
436
|
`── Mipham Exclusive (${counts.mipham}) ──`,
|
|
408
437
|
...mipham.map((s) => ` ${s.name.padEnd(20)} ${s.description}`),
|
|
409
438
|
'',
|
|
410
|
-
|
|
439
|
+
t('commands.skills.loaded', { count: String(counts.total) }),
|
|
411
440
|
]
|
|
412
441
|
return { content: lines.join('\n') }
|
|
413
442
|
}
|
|
@@ -417,10 +446,10 @@ const skillsCmd: CommandHandler = (ctx) => {
|
|
|
417
446
|
// ═══════════════════════════════════════════════════════════════
|
|
418
447
|
|
|
419
448
|
const planCmd: CommandHandler = (_ctx, args) => {
|
|
449
|
+
const t = resolveT(_ctx)
|
|
420
450
|
const description = args.join(' ') || undefined
|
|
421
451
|
return {
|
|
422
|
-
content:
|
|
423
|
-
'── Plan Mode ──\n\nEntering plan mode — read-only analysis and design.\nUse EnterPlanMode to start, ExitPlanMode to submit for approval.',
|
|
452
|
+
content: t('commands.plan.content'),
|
|
424
453
|
forwardToAI: description
|
|
425
454
|
? `Use EnterPlanMode with description: "${description}". Then explore, design, and use ExitPlanMode when ready for approval.`
|
|
426
455
|
: 'Use EnterPlanMode to enter plan mode. Explore the codebase, design an approach, then use ExitPlanMode to submit for approval.',
|
|
@@ -428,18 +457,19 @@ const planCmd: CommandHandler = (_ctx, args) => {
|
|
|
428
457
|
}
|
|
429
458
|
|
|
430
459
|
const tddCmd: CommandHandler = (_ctx, args) => {
|
|
460
|
+
const t = resolveT(_ctx)
|
|
431
461
|
const target = args.join(' ') || 'the current task'
|
|
432
462
|
return {
|
|
433
463
|
content: stripIndent`
|
|
434
|
-
|
|
435
|
-
|
|
464
|
+
${t('commands.tdd.title')}
|
|
465
|
+
${t('commands.tdd.start', { target })}
|
|
436
466
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
467
|
+
${t('commands.tdd.cycle')}
|
|
468
|
+
${t('commands.tdd.red')}
|
|
469
|
+
${t('commands.tdd.green')}
|
|
470
|
+
${t('commands.tdd.refactor')}
|
|
441
471
|
|
|
442
|
-
|
|
472
|
+
${t('commands.tdd.guide')}
|
|
443
473
|
`,
|
|
444
474
|
forwardToAI: `Follow the Test-Driven Development workflow for ${target}:
|
|
445
475
|
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 +480,15 @@ Repeat for each behavior. Do NOT write implementation before tests.`,
|
|
|
450
480
|
}
|
|
451
481
|
|
|
452
482
|
const todosCmd: CommandHandler = (_ctx, args) => {
|
|
483
|
+
const t = resolveT(_ctx)
|
|
453
484
|
const sub = args[0]
|
|
454
485
|
if (sub === 'create') {
|
|
455
486
|
const title = args.slice(1).join(' ')
|
|
456
487
|
if (!title.trim()) {
|
|
457
|
-
return {
|
|
458
|
-
content:
|
|
459
|
-
'Usage: /todos create <task-title>\n\nExample: /todos create Add user authentication',
|
|
460
|
-
}
|
|
488
|
+
return { content: t('commands.todos.usage_create') }
|
|
461
489
|
}
|
|
462
490
|
return {
|
|
463
|
-
content:
|
|
491
|
+
content: `${t('commands.todos.create_title')}\n\nCreating task: "${title.trim()}"\n\nPassing to AI for structured task creation with TaskCreate...`,
|
|
464
492
|
forwardToAI: `Create a new task using TaskCreate with subject "${title.trim()}". Set a clear description and activeForm.`,
|
|
465
493
|
}
|
|
466
494
|
}
|
|
@@ -468,12 +496,12 @@ const todosCmd: CommandHandler = (_ctx, args) => {
|
|
|
468
496
|
if (sub === 'list' || !sub) {
|
|
469
497
|
return {
|
|
470
498
|
content: stripIndent`
|
|
471
|
-
|
|
472
|
-
|
|
499
|
+
${t('commands.todos.list_title')}
|
|
500
|
+
${t('commands.todos.list_fetching')}
|
|
473
501
|
|
|
474
502
|
Shortcuts:
|
|
475
|
-
|
|
476
|
-
|
|
503
|
+
${t('commands.todos.item_list')}
|
|
504
|
+
${t('commands.todos.item_create')}
|
|
477
505
|
`,
|
|
478
506
|
forwardToAI:
|
|
479
507
|
'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 +510,11 @@ const todosCmd: CommandHandler = (_ctx, args) => {
|
|
|
482
510
|
|
|
483
511
|
return {
|
|
484
512
|
content: stripIndent`
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
513
|
+
${t('commands.todos.default_title')}
|
|
514
|
+
${t('commands.todos.item_list')}
|
|
515
|
+
${t('commands.todos.item_create')}
|
|
488
516
|
|
|
489
|
-
|
|
490
|
-
Tasks appear in the /tasks view and persist across the session.
|
|
517
|
+
${t('commands.todos.default_body')}
|
|
491
518
|
`,
|
|
492
519
|
forwardToAI: 'Use TaskList to show all current tasks, then present them clearly.',
|
|
493
520
|
}
|
|
@@ -502,40 +529,30 @@ const todosCmd: CommandHandler = (_ctx, args) => {
|
|
|
502
529
|
// ═══════════════════════════════════════════════════════════════
|
|
503
530
|
|
|
504
531
|
const renameCmd: CommandHandler = (ctx, args) => {
|
|
532
|
+
const t = resolveT(ctx)
|
|
505
533
|
const name = args.join(' ')
|
|
506
534
|
if (!name.trim()) {
|
|
507
|
-
return { content: '
|
|
535
|
+
return { content: t('commands.rename.usage') }
|
|
508
536
|
}
|
|
509
537
|
ctx.setSessionTitle(name.trim())
|
|
510
|
-
return { content:
|
|
538
|
+
return { content: t('commands.rename.confirmed', { name: name.trim() }) }
|
|
511
539
|
}
|
|
512
540
|
|
|
513
541
|
const goalCmd: CommandHandler = (ctx, args) => {
|
|
542
|
+
const t = resolveT(ctx)
|
|
514
543
|
const input = args.join(' ')
|
|
515
544
|
|
|
516
545
|
// ── Show goal status ──
|
|
517
546
|
if (!input.trim()) {
|
|
518
547
|
const state = ctx.engine.getGoalState?.() || { goal: undefined, decompose: false, subtasks: [] }
|
|
519
548
|
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
|
-
}
|
|
549
|
+
return { content: t('commands.goal.usage') }
|
|
533
550
|
}
|
|
534
|
-
const lines = [
|
|
535
|
-
if (state.verifyScript) lines.push(
|
|
536
|
-
if (state.verifySkill) lines.push(
|
|
537
|
-
if (state.decompose) lines.push(
|
|
538
|
-
lines.push('', '
|
|
551
|
+
const lines = [t('commands.goal.status_title'), '', t('commands.goal.status_goal', { goal: state.goal })]
|
|
552
|
+
if (state.verifyScript) lines.push(t('commands.goal.status_verify_script', { script: state.verifyScript }))
|
|
553
|
+
if (state.verifySkill) lines.push(t('commands.goal.status_verify_skill', { skill: state.verifySkill }))
|
|
554
|
+
if (state.decompose) lines.push(t('commands.goal.status_decompose', { count: String(state.subtasks.length) }))
|
|
555
|
+
lines.push('', t('commands.goal.status_clear_hint'))
|
|
539
556
|
return { content: lines.join('\n') }
|
|
540
557
|
}
|
|
541
558
|
|
|
@@ -567,15 +584,15 @@ const goalCmd: CommandHandler = (ctx, args) => {
|
|
|
567
584
|
|
|
568
585
|
goal = goal.trim()
|
|
569
586
|
if (!goal) {
|
|
570
|
-
return { content: '
|
|
587
|
+
return { content: t('commands.goal.empty_set') }
|
|
571
588
|
}
|
|
572
589
|
|
|
573
590
|
ctx.setGoal(goal)
|
|
574
591
|
ctx.engine.setGoal(goal, { verifyScript, verifySkill, decompose })
|
|
575
592
|
|
|
576
|
-
const lines = [
|
|
593
|
+
const lines = [t('commands.goal.set', { goal })]
|
|
577
594
|
if (decompose) {
|
|
578
|
-
lines.push('
|
|
595
|
+
lines.push(t('commands.goal.decompose_enabled'))
|
|
579
596
|
// Decompose by creating initial subtasks
|
|
580
597
|
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
598
|
return {
|
|
@@ -583,27 +600,26 @@ const goalCmd: CommandHandler = (ctx, args) => {
|
|
|
583
600
|
forwardToAI: decomposeMsg,
|
|
584
601
|
}
|
|
585
602
|
}
|
|
586
|
-
if (verifyScript) lines.push(
|
|
587
|
-
if (verifySkill) lines.push(
|
|
603
|
+
if (verifyScript) lines.push(t('commands.goal.status_verify_script', { script: verifyScript }))
|
|
604
|
+
if (verifySkill) lines.push(t('commands.goal.status_verify_skill', { skill: verifySkill }))
|
|
588
605
|
lines.push('')
|
|
589
|
-
lines.push('
|
|
606
|
+
lines.push(t('commands.goal.status_hint'))
|
|
590
607
|
|
|
591
608
|
return { content: lines.join('\n') }
|
|
592
609
|
}
|
|
593
610
|
|
|
594
611
|
const recapCmd: CommandHandler = (ctx) => {
|
|
612
|
+
const t = resolveT(ctx)
|
|
595
613
|
const c = ctx.engine.getContext()
|
|
596
614
|
const msgs = c.getMessages()
|
|
597
615
|
if (msgs.length === 0) {
|
|
598
|
-
return { content: '
|
|
616
|
+
return { content: t('commands.recap.no_conversation') }
|
|
599
617
|
}
|
|
600
|
-
// Show summary of conversation: count messages, roles, estimated tokens
|
|
601
618
|
const userMsgs = msgs.filter((m) => m.role === 'user').length
|
|
602
619
|
const assistantMsgs = msgs.filter((m) => m.role === 'assistant').length
|
|
603
620
|
const tokens = c.getEstimatedTokens()
|
|
604
621
|
const checkpointCount = c.getCheckpoints().length
|
|
605
622
|
|
|
606
|
-
// Extract first few user messages as "topics"
|
|
607
623
|
const topics = msgs
|
|
608
624
|
.filter((m) => m.role === 'user' && typeof m.content === 'string')
|
|
609
625
|
.slice(0, 5)
|
|
@@ -614,20 +630,21 @@ const recapCmd: CommandHandler = (ctx) => {
|
|
|
614
630
|
|
|
615
631
|
return {
|
|
616
632
|
content: stripIndent`
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
633
|
+
${t('commands.recap.title')}
|
|
634
|
+
${t('commands.recap.messages')} ${msgs.length} (${userMsgs} user, ${assistantMsgs} assistant)
|
|
635
|
+
${t('commands.recap.est_tokens')} ~${tokens.toLocaleString()}
|
|
636
|
+
${t('commands.recap.checkpoints')} ${checkpointCount}
|
|
621
637
|
|
|
622
|
-
|
|
638
|
+
${t('commands.recap.recent_topics')}
|
|
623
639
|
${topics.map((t, i) => ` ${i + 1}. ${t}`).join('\n')}
|
|
624
640
|
|
|
625
|
-
|
|
641
|
+
${t('commands.recap.footer')}
|
|
626
642
|
`,
|
|
627
643
|
}
|
|
628
644
|
}
|
|
629
645
|
|
|
630
646
|
const usageCmd: CommandHandler = (ctx) => {
|
|
647
|
+
const t = resolveT(ctx)
|
|
631
648
|
const c = ctx.engine.getContext()
|
|
632
649
|
const estTokens = c.getEstimatedTokens()
|
|
633
650
|
const msgs = c.getMessages()
|
|
@@ -653,46 +670,45 @@ const usageCmd: CommandHandler = (ctx) => {
|
|
|
653
670
|
const apiTotal = summary.apiInputTokens + summary.apiOutputTokens
|
|
654
671
|
const apiLine =
|
|
655
672
|
summary.apiInputTokens > 0 || summary.apiOutputTokens > 0
|
|
656
|
-
?
|
|
657
|
-
: '
|
|
673
|
+
? t('commands.usage.api_tokens', { in: summary.apiInputTokens.toLocaleString(), out: summary.apiOutputTokens.toLocaleString(), total: apiTotal.toLocaleString() })
|
|
674
|
+
: t('commands.usage.no_api')
|
|
658
675
|
|
|
659
|
-
const toolSection = toolLines.length > 0 ? `\n
|
|
676
|
+
const toolSection = toolLines.length > 0 ? `\n${t('commands.usage.tool_section')}\n${toolLines.join('\n')}` : ''
|
|
660
677
|
|
|
661
678
|
return {
|
|
662
679
|
content: stripIndent`
|
|
663
|
-
|
|
680
|
+
${t('commands.usage.title')}
|
|
664
681
|
${apiLine}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
682
|
+
${t('commands.usage.context_line', { est: estTokens.toLocaleString(), max: maxTokens.toLocaleString(), pct })}
|
|
683
|
+
${t('commands.usage.messages_line', { count: String(msgs.length) })}
|
|
684
|
+
${t('commands.usage.provider_line', { provider: ctx.providerId })}
|
|
685
|
+
${t('commands.usage.model_line', { model: ctx.modelId })}
|
|
669
686
|
|
|
670
687
|
${'█'.repeat(Math.ceil(Number(pct) / 5))}${'░'.repeat(20 - Math.ceil(Number(pct) / 5))} ${pct}%
|
|
671
688
|
${toolSection}
|
|
672
689
|
|
|
673
|
-
|
|
690
|
+
${t('commands.usage.hint')}
|
|
674
691
|
`,
|
|
675
692
|
}
|
|
676
693
|
}
|
|
677
694
|
|
|
678
695
|
const reloadSkillsCmd: CommandHandler = (ctx) => {
|
|
696
|
+
const t = resolveT(ctx)
|
|
679
697
|
if (!ctx.skillsLoader) {
|
|
680
|
-
return { content: '
|
|
698
|
+
return { content: t('commands.reload_skills.unavailable') }
|
|
681
699
|
}
|
|
682
700
|
try {
|
|
683
701
|
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
702
|
ctx.skillsLoader.loadBuiltin(process.cwd())
|
|
687
703
|
if (config.skills?.paths) {
|
|
688
704
|
ctx.skillsLoader.loadExternal(config.skills.paths)
|
|
689
705
|
}
|
|
690
706
|
const skills = ctx.skillsLoader.list()
|
|
691
707
|
return {
|
|
692
|
-
content:
|
|
708
|
+
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
709
|
}
|
|
694
710
|
} catch (err) {
|
|
695
|
-
return { content:
|
|
711
|
+
return { content: t('commands.reload_skills.failed', { error: String(err) }) }
|
|
696
712
|
}
|
|
697
713
|
}
|
|
698
714
|
|
|
@@ -700,7 +716,8 @@ const reloadSkillsCmd: CommandHandler = (ctx) => {
|
|
|
700
716
|
// Skill Marketplace — Community skill registry + installation
|
|
701
717
|
// ═══════════════════════════════════════════════════════════════
|
|
702
718
|
|
|
703
|
-
const browseSkillsCmd: CommandHandler = async () => {
|
|
719
|
+
const browseSkillsCmd: CommandHandler = async (ctx) => {
|
|
720
|
+
const _t = resolveT(ctx)
|
|
704
721
|
const { getAvailableSkills, listInstalledSkills } = await import('../skills/registry')
|
|
705
722
|
const available = getAvailableSkills()
|
|
706
723
|
const installed = new Set(
|
|
@@ -780,29 +797,19 @@ const removeSkillCmd: CommandHandler = async (_ctx, args) => {
|
|
|
780
797
|
// ═══════════════════════════════════════════════════════════════
|
|
781
798
|
|
|
782
799
|
const pluginsCmd: CommandHandler = (ctx) => {
|
|
800
|
+
const t = resolveT(ctx)
|
|
783
801
|
const manager = ctx.pluginManager
|
|
784
802
|
if (!manager) {
|
|
785
|
-
return { content: '
|
|
803
|
+
return { content: t('commands.plugins.unavailable') }
|
|
786
804
|
}
|
|
787
805
|
|
|
788
806
|
const plugins = manager.list()
|
|
789
807
|
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
|
-
}
|
|
808
|
+
return { content: t('commands.plugins.none') }
|
|
802
809
|
}
|
|
803
810
|
|
|
804
811
|
const lines: string[] = [
|
|
805
|
-
'
|
|
812
|
+
t('commands.plugins.title'),
|
|
806
813
|
'',
|
|
807
814
|
...plugins.map((p) => {
|
|
808
815
|
const status = p.enabled ? '✅ enabled' : '⛔ disabled'
|
|
@@ -955,29 +962,21 @@ const pluginDisableCmd: CommandHandler = (ctx, args) => {
|
|
|
955
962
|
// ═══════════════════════════════════════════════════════════════
|
|
956
963
|
|
|
957
964
|
const rewindCmd: CommandHandler = (ctx) => {
|
|
965
|
+
const t = resolveT(ctx)
|
|
958
966
|
const c = ctx.engine.getContext()
|
|
959
967
|
const checkpoints = c.getCheckpoints()
|
|
960
968
|
|
|
961
969
|
if (checkpoints.length === 0) {
|
|
962
|
-
return {
|
|
963
|
-
content:
|
|
964
|
-
'No checkpoints available. Checkpoints are automatically saved after each AI response.',
|
|
965
|
-
}
|
|
970
|
+
return { content: t('commands.rewind.no_checkpoints') }
|
|
966
971
|
}
|
|
967
972
|
|
|
968
973
|
const result = c.restoreCheckpoint()
|
|
969
974
|
if (!result.restored) {
|
|
970
|
-
return { content: '
|
|
975
|
+
return { content: t('commands.rewind.no_restore') }
|
|
971
976
|
}
|
|
972
977
|
|
|
973
978
|
return {
|
|
974
|
-
content:
|
|
975
|
-
✓ Rewound to checkpoint "${result.label}"
|
|
976
|
-
Restored ${result.messageCount} messages.
|
|
977
|
-
|
|
978
|
-
Remaining checkpoints: ${c.getCheckpoints().length}
|
|
979
|
-
Use /rewind again to go back further, or continue chatting.
|
|
980
|
-
`,
|
|
979
|
+
content: t('commands.rewind.confirmed', { label: result.label, count: String(result.messageCount), remaining: String(c.getCheckpoints().length) }),
|
|
981
980
|
clearMessages: true,
|
|
982
981
|
}
|
|
983
982
|
}
|
|
@@ -985,12 +984,13 @@ const rewindCmd: CommandHandler = (ctx) => {
|
|
|
985
984
|
const undoCmd: CommandHandler = rewindCmd
|
|
986
985
|
|
|
987
986
|
const copyCmd: CommandHandler = (ctx, args) => {
|
|
987
|
+
const t = resolveT(ctx)
|
|
988
988
|
const c = ctx.engine.getContext()
|
|
989
989
|
const msgs = c.getMessages()
|
|
990
990
|
const assistantMsgs = msgs.filter((m) => m.role === 'assistant')
|
|
991
991
|
|
|
992
992
|
if (assistantMsgs.length === 0) {
|
|
993
|
-
return { content: '
|
|
993
|
+
return { content: t('commands.copy.no_responses') }
|
|
994
994
|
}
|
|
995
995
|
|
|
996
996
|
// Determine which response to copy: last N or last 1
|
|
@@ -998,9 +998,7 @@ const copyCmd: CommandHandler = (ctx, args) => {
|
|
|
998
998
|
if (args[0]) {
|
|
999
999
|
n = parseInt(args[0]!, 10)
|
|
1000
1000
|
if (isNaN(n) || n < 1) {
|
|
1001
|
-
return {
|
|
1002
|
-
content: 'Usage: /copy [N]\nN = number of recent assistant responses to copy (default: 1)',
|
|
1003
|
-
}
|
|
1001
|
+
return { content: t('commands.copy.usage') }
|
|
1004
1002
|
}
|
|
1005
1003
|
}
|
|
1006
1004
|
|
|
@@ -1010,17 +1008,18 @@ const copyCmd: CommandHandler = (ctx, args) => {
|
|
|
1010
1008
|
.join('\n\n---\n\n')
|
|
1011
1009
|
|
|
1012
1010
|
return {
|
|
1013
|
-
content:
|
|
1011
|
+
content: t('commands.copy.confirmed', { count: String(toCopy.length) }),
|
|
1014
1012
|
copyContent: text,
|
|
1015
1013
|
}
|
|
1016
1014
|
}
|
|
1017
1015
|
|
|
1018
1016
|
const diffCmd: CommandHandler = async (_ctx) => {
|
|
1017
|
+
const t = resolveT(_ctx)
|
|
1019
1018
|
try {
|
|
1020
1019
|
const { execSync } = await import('node:child_process')
|
|
1021
1020
|
const output = execSync('git diff --stat', { encoding: 'utf-8', timeout: 5000 })
|
|
1022
1021
|
if (!output.trim()) {
|
|
1023
|
-
return { content: '
|
|
1022
|
+
return { content: t('commands.diff.clean') }
|
|
1024
1023
|
}
|
|
1025
1024
|
// Get full diff but limit to reasonable size
|
|
1026
1025
|
const fullDiff = execSync('git diff --no-color', { encoding: 'utf-8', timeout: 5000 })
|
|
@@ -1033,11 +1032,11 @@ const diffCmd: CommandHandler = async (_ctx) => {
|
|
|
1033
1032
|
: fullDiff
|
|
1034
1033
|
|
|
1035
1034
|
return {
|
|
1036
|
-
content:
|
|
1035
|
+
content: `${t('commands.diff.title')}\n\n${truncated}`,
|
|
1037
1036
|
}
|
|
1038
1037
|
} catch {
|
|
1039
1038
|
return {
|
|
1040
|
-
content: '
|
|
1039
|
+
content: t('commands.diff.error'),
|
|
1041
1040
|
}
|
|
1042
1041
|
}
|
|
1043
1042
|
}
|
|
@@ -1047,60 +1046,38 @@ const diffCmd: CommandHandler = async (_ctx) => {
|
|
|
1047
1046
|
// ═══════════════════════════════════════════════════════════════
|
|
1048
1047
|
|
|
1049
1048
|
const fastCmd: CommandHandler = (ctx, args) => {
|
|
1049
|
+
const t = resolveT(ctx)
|
|
1050
1050
|
const arg = args[0]?.toLowerCase()
|
|
1051
1051
|
if (arg === 'on') {
|
|
1052
1052
|
ctx.setFastMode(true)
|
|
1053
|
-
return { content: '
|
|
1053
|
+
return { content: t('commands.fast.on') }
|
|
1054
1054
|
} else if (arg === 'off') {
|
|
1055
1055
|
ctx.setFastMode(false)
|
|
1056
|
-
return { content: '
|
|
1056
|
+
return { content: t('commands.fast.off') }
|
|
1057
1057
|
} else if (arg) {
|
|
1058
|
-
return { content: '
|
|
1058
|
+
return { content: t('commands.fast.usage') }
|
|
1059
1059
|
} 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
|
-
}
|
|
1060
|
+
return { content: t('commands.fast.unknown') }
|
|
1066
1061
|
}
|
|
1067
1062
|
}
|
|
1068
1063
|
|
|
1069
1064
|
const effortCmd: CommandHandler = (ctx, args) => {
|
|
1065
|
+
const t = resolveT(ctx)
|
|
1070
1066
|
const VALID_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max']
|
|
1071
1067
|
const level = args[0]?.toLowerCase()
|
|
1072
1068
|
|
|
1073
1069
|
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
|
-
}
|
|
1070
|
+
return { content: t('commands.effort.usage', { model: ctx.modelId }) }
|
|
1089
1071
|
}
|
|
1090
1072
|
|
|
1091
1073
|
ctx.setEffort(level)
|
|
1092
|
-
return { content:
|
|
1074
|
+
return { content: t('commands.effort.confirmed', { level }) }
|
|
1093
1075
|
}
|
|
1094
1076
|
|
|
1095
1077
|
const focusCmd: CommandHandler = (ctx) => {
|
|
1078
|
+
const t = resolveT(ctx)
|
|
1096
1079
|
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
|
-
}
|
|
1080
|
+
return { content: t('commands.focus.on') }
|
|
1104
1081
|
}
|
|
1105
1082
|
|
|
1106
1083
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1108,12 +1085,12 @@ const focusCmd: CommandHandler = (ctx) => {
|
|
|
1108
1085
|
// ═══════════════════════════════════════════════════════════════
|
|
1109
1086
|
|
|
1110
1087
|
const ultracodeCmd: CommandHandler = (ctx, args) => {
|
|
1088
|
+
const t = resolveT(ctx)
|
|
1111
1089
|
const arg = args[0]?.toLowerCase()
|
|
1112
1090
|
if (arg === 'on') {
|
|
1113
1091
|
ctx.setUltracodeMode(true)
|
|
1114
1092
|
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.',
|
|
1093
|
+
content: t('commands.ultracode.on'),
|
|
1117
1094
|
forwardToAI: stripIndent`
|
|
1118
1095
|
Ultracode mode is now ACTIVE. For every substantive task:
|
|
1119
1096
|
- Use the Workflow tool to orchestrate multi-agent execution
|
|
@@ -1129,24 +1106,12 @@ const ultracodeCmd: CommandHandler = (ctx, args) => {
|
|
|
1129
1106
|
} else if (arg === 'off') {
|
|
1130
1107
|
ctx.setUltracodeMode(false)
|
|
1131
1108
|
return {
|
|
1132
|
-
content: '
|
|
1109
|
+
content: t('commands.ultracode.off'),
|
|
1133
1110
|
forwardToAI:
|
|
1134
1111
|
'Ultracode mode is now OFF. Revert to standard single-agent execution. Do NOT use Workflow tool unless explicitly asked.',
|
|
1135
1112
|
}
|
|
1136
1113
|
}
|
|
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
|
-
}
|
|
1114
|
+
return { content: t('commands.ultracode.usage') }
|
|
1150
1115
|
}
|
|
1151
1116
|
|
|
1152
1117
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1154,6 +1119,7 @@ const ultracodeCmd: CommandHandler = (ctx, args) => {
|
|
|
1154
1119
|
// ═══════════════════════════════════════════════════════════════
|
|
1155
1120
|
|
|
1156
1121
|
const tasksCmd: CommandHandler = (ctx) => {
|
|
1122
|
+
const t = resolveT(ctx)
|
|
1157
1123
|
const c = ctx.engine.getContext()
|
|
1158
1124
|
const msgs = c.getMessages()
|
|
1159
1125
|
|
|
@@ -1169,15 +1135,11 @@ const tasksCmd: CommandHandler = (ctx) => {
|
|
|
1169
1135
|
|
|
1170
1136
|
return {
|
|
1171
1137
|
content: stripIndent`
|
|
1172
|
-
|
|
1138
|
+
${t('commands.task_list.title')}
|
|
1173
1139
|
|
|
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
|
-
}
|
|
1140
|
+
${toolUses.length > 0 ? t('commands.task_list.detected', { count: String(toolUses.length) }) : t('commands.task_list.no_tasks')}
|
|
1179
1141
|
|
|
1180
|
-
|
|
1142
|
+
${t('commands.task_list.reference')}
|
|
1181
1143
|
TaskCreate — create a new task
|
|
1182
1144
|
TaskList — list all tasks
|
|
1183
1145
|
TaskUpdate — update task status
|
|
@@ -1185,35 +1147,24 @@ const tasksCmd: CommandHandler = (ctx) => {
|
|
|
1185
1147
|
TaskOutput — get background task output
|
|
1186
1148
|
TaskStop — stop a running task
|
|
1187
1149
|
|
|
1188
|
-
|
|
1150
|
+
${t('commands.task_list.legacy_hint')}
|
|
1189
1151
|
`,
|
|
1190
1152
|
}
|
|
1191
1153
|
}
|
|
1192
1154
|
|
|
1193
1155
|
const branchCmd: CommandHandler = (ctx, args) => {
|
|
1156
|
+
const t = resolveT(ctx)
|
|
1194
1157
|
const name = args.join(' ') || `branch-${Date.now()}`
|
|
1195
1158
|
const c = ctx.engine.getContext()
|
|
1196
1159
|
const msgs = c.getMessages()
|
|
1197
1160
|
|
|
1198
1161
|
if (msgs.length === 0) {
|
|
1199
|
-
return { content: '
|
|
1162
|
+
return { content: t('commands.branch.no_conversation') }
|
|
1200
1163
|
}
|
|
1201
1164
|
|
|
1202
|
-
// Save current session state as a named checkpoint
|
|
1203
1165
|
const checkpointId = c.saveCheckpoint(name)
|
|
1204
1166
|
return {
|
|
1205
|
-
content:
|
|
1206
|
-
── Branch Created ──
|
|
1207
|
-
Name: "${name}"
|
|
1208
|
-
Checkpoint: #${checkpointId}
|
|
1209
|
-
Messages: ${msgs.length} saved
|
|
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
|
-
`,
|
|
1167
|
+
content: t('commands.branch.details', { name, checkpoint: String(checkpointId), messages: String(msgs.length) }),
|
|
1217
1168
|
}
|
|
1218
1169
|
}
|
|
1219
1170
|
|
|
@@ -1450,8 +1401,9 @@ const scheduleCmd: CommandHandler = async (_ctx) => {
|
|
|
1450
1401
|
// ═══════════════════════════════════════════════════════════════
|
|
1451
1402
|
|
|
1452
1403
|
const doctorCmd: CommandHandler = async (ctx) => {
|
|
1404
|
+
const t = resolveT(ctx)
|
|
1453
1405
|
const lines: string[] = [
|
|
1454
|
-
'
|
|
1406
|
+
t('commands.doctor.title'),
|
|
1455
1407
|
'',
|
|
1456
1408
|
`Mipham Code v${ctx.version}`,
|
|
1457
1409
|
`Runtime ${typeof Bun !== 'undefined' ? 'Bun ' + Bun.version : 'Node.js ' + process.version}`,
|
|
@@ -1459,12 +1411,12 @@ const doctorCmd: CommandHandler = async (ctx) => {
|
|
|
1459
1411
|
`CWD ${process.cwd()}`,
|
|
1460
1412
|
`PID ${process.pid}`,
|
|
1461
1413
|
'',
|
|
1462
|
-
'
|
|
1414
|
+
t('commands.doctor.config_section'),
|
|
1463
1415
|
`Provider ${ctx.providerId} / ${ctx.modelId}`,
|
|
1464
1416
|
`Permission ${ctx.config.permission}`,
|
|
1465
1417
|
`Providers ${ctx.config.providers.length} configured (${ctx.config.providers.filter((p) => p.status !== 'upcoming').length} active)`,
|
|
1466
1418
|
'',
|
|
1467
|
-
'
|
|
1419
|
+
t('commands.doctor.session_section'),
|
|
1468
1420
|
]
|
|
1469
1421
|
|
|
1470
1422
|
const c = ctx.engine.getContext()
|
|
@@ -1480,7 +1432,7 @@ const doctorCmd: CommandHandler = async (ctx) => {
|
|
|
1480
1432
|
try {
|
|
1481
1433
|
const { execSync } = await import('node:child_process')
|
|
1482
1434
|
lines.push('')
|
|
1483
|
-
lines.push('
|
|
1435
|
+
lines.push(t('commands.doctor.git_section'))
|
|
1484
1436
|
const branch = execSync('git branch --show-current', {
|
|
1485
1437
|
encoding: 'utf-8',
|
|
1486
1438
|
timeout: 3000,
|
|
@@ -1500,21 +1452,21 @@ const doctorCmd: CommandHandler = async (ctx) => {
|
|
|
1500
1452
|
)
|
|
1501
1453
|
} catch {
|
|
1502
1454
|
lines.push('')
|
|
1503
|
-
lines.push('
|
|
1504
|
-
lines.push(
|
|
1455
|
+
lines.push(t('commands.doctor.git_section'))
|
|
1456
|
+
lines.push(t('commands.doctor.no_git'))
|
|
1505
1457
|
}
|
|
1506
1458
|
|
|
1507
1459
|
// Skills info
|
|
1508
1460
|
if (ctx.skillsLoader) {
|
|
1509
1461
|
lines.push('')
|
|
1510
|
-
lines.push('
|
|
1462
|
+
lines.push(t('commands.doctor.skills_section'))
|
|
1511
1463
|
try {
|
|
1512
1464
|
const skills = ctx.skillsLoader.list()
|
|
1513
1465
|
const standard = skills.filter((s: { type: string }) => s.type === 'standard').length
|
|
1514
1466
|
const mipham = skills.filter((s: { type: string }) => s.type === 'mipham').length
|
|
1515
1467
|
lines.push(`Loaded ${skills.length} (${standard} standard + ${mipham} mipham)`)
|
|
1516
1468
|
} catch {
|
|
1517
|
-
lines.push(
|
|
1469
|
+
lines.push(t('commands.doctor.skills_unavailable'))
|
|
1518
1470
|
}
|
|
1519
1471
|
}
|
|
1520
1472
|
|
|
@@ -1527,68 +1479,67 @@ const doctorCmd: CommandHandler = async (ctx) => {
|
|
|
1527
1479
|
|
|
1528
1480
|
function gitDiffBridgeCmd(opts: {
|
|
1529
1481
|
label: string
|
|
1530
|
-
|
|
1531
|
-
|
|
1482
|
+
noChangesKey: string
|
|
1483
|
+
runningKey: string
|
|
1484
|
+
errorKey: string
|
|
1532
1485
|
forwardToAI: string | (() => string)
|
|
1533
1486
|
}): CommandHandler {
|
|
1534
|
-
return async () => {
|
|
1487
|
+
return async (ctx) => {
|
|
1488
|
+
const t = resolveT(ctx)
|
|
1535
1489
|
try {
|
|
1536
1490
|
const { execSync } = await import('node:child_process')
|
|
1537
1491
|
const diff = execSync('git diff --stat', { encoding: 'utf-8', timeout: 5000 }).trim()
|
|
1538
1492
|
if (!diff) {
|
|
1539
|
-
return { content: `─ ${opts.label} ─\n\n${opts.
|
|
1493
|
+
return { content: `─ ${opts.label} ─\n\n${t(opts.noChangesKey)}` }
|
|
1540
1494
|
}
|
|
1541
1495
|
return {
|
|
1542
|
-
content: `─ ${opts.label} ─\n\n${opts.
|
|
1496
|
+
content: `─ ${opts.label} ─\n\n${t(opts.runningKey)}\n\nChanged files:\n${diff}`,
|
|
1543
1497
|
forwardToAI: typeof opts.forwardToAI === 'function' ? opts.forwardToAI() : opts.forwardToAI,
|
|
1544
1498
|
}
|
|
1545
1499
|
} catch {
|
|
1546
|
-
return {
|
|
1547
|
-
content: `─ ${opts.label} ─\n\nCould not detect changes. Are you in a git repository?`,
|
|
1548
|
-
}
|
|
1500
|
+
return { content: `─ ${opts.label} ─\n\n${t(opts.errorKey)}` }
|
|
1549
1501
|
}
|
|
1550
1502
|
}
|
|
1551
1503
|
}
|
|
1552
1504
|
|
|
1553
1505
|
const codeReviewCmd = gitDiffBridgeCmd({
|
|
1554
1506
|
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)...',
|
|
1507
|
+
noChangesKey: 'commands.code_review.no_changes',
|
|
1508
|
+
runningKey: 'commands.code_review.running',
|
|
1509
|
+
errorKey: 'commands.code_review.error',
|
|
1559
1510
|
forwardToAI: () =>
|
|
1560
1511
|
`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
1512
|
})
|
|
1562
1513
|
|
|
1563
1514
|
const simplifyCmd = gitDiffBridgeCmd({
|
|
1564
1515
|
label: 'Simplify',
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
'Running cleanup review — 4 passes: reuse, simplification, efficiency, abstraction level...',
|
|
1516
|
+
noChangesKey: 'commands.simplify.no_changes',
|
|
1517
|
+
runningKey: 'commands.simplify.running',
|
|
1518
|
+
errorKey: 'commands.simplify.error',
|
|
1569
1519
|
forwardToAI:
|
|
1570
1520
|
'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
1521
|
})
|
|
1572
1522
|
|
|
1573
1523
|
const verifyCmd = gitDiffBridgeCmd({
|
|
1574
1524
|
label: 'Verify',
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
'Running runtime verification — observing actual execution behavior (not tests, not typecheck)...',
|
|
1525
|
+
noChangesKey: 'commands.verify.no_changes',
|
|
1526
|
+
runningKey: 'commands.verify.running',
|
|
1527
|
+
errorKey: 'commands.verify.error',
|
|
1579
1528
|
forwardToAI:
|
|
1580
1529
|
'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
1530
|
})
|
|
1582
1531
|
|
|
1583
1532
|
const designCmd: CommandHandler = (_ctx, args) => {
|
|
1533
|
+
const t = resolveT(_ctx)
|
|
1584
1534
|
const topic = args.join(' ') || 'the current task'
|
|
1585
1535
|
return {
|
|
1586
|
-
content:
|
|
1536
|
+
content: t('commands.design.start', { topic }),
|
|
1587
1537
|
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
1538
|
}
|
|
1589
1539
|
}
|
|
1590
1540
|
|
|
1591
|
-
const lintCmd: CommandHandler = async () => {
|
|
1541
|
+
const lintCmd: CommandHandler = async (ctx) => {
|
|
1542
|
+
const t = resolveT(ctx)
|
|
1592
1543
|
try {
|
|
1593
1544
|
const { execSync } = await import('node:child_process')
|
|
1594
1545
|
const { existsSync } = await import('node:fs')
|
|
@@ -1620,19 +1571,17 @@ const lintCmd: CommandHandler = async () => {
|
|
|
1620
1571
|
|
|
1621
1572
|
return {
|
|
1622
1573
|
content: [
|
|
1623
|
-
'
|
|
1574
|
+
t('commands.lint.title'),
|
|
1624
1575
|
'',
|
|
1625
1576
|
truncated || '(no issues found)',
|
|
1626
1577
|
'',
|
|
1627
1578
|
hasPackageJson
|
|
1628
|
-
? '
|
|
1629
|
-
: '
|
|
1579
|
+
? t('commands.lint.fix_hint')
|
|
1580
|
+
: t('commands.lint.setup_hint'),
|
|
1630
1581
|
].join('\n'),
|
|
1631
1582
|
}
|
|
1632
1583
|
} catch {
|
|
1633
|
-
return {
|
|
1634
|
-
content: '── Lint ──\n\nCould not run linter. Ensure ESLint is installed and configured.',
|
|
1635
|
-
}
|
|
1584
|
+
return { content: t('commands.lint.error') }
|
|
1636
1585
|
}
|
|
1637
1586
|
}
|
|
1638
1587
|
|
|
@@ -1640,7 +1589,8 @@ const lintCmd: CommandHandler = async () => {
|
|
|
1640
1589
|
// Session Enhancement Commands (Claude Code parity)
|
|
1641
1590
|
// ═══════════════════════════════════════════════════════════════
|
|
1642
1591
|
|
|
1643
|
-
const filesCmd: CommandHandler = async () => {
|
|
1592
|
+
const filesCmd: CommandHandler = async (ctx) => {
|
|
1593
|
+
const t = resolveT(ctx)
|
|
1644
1594
|
const { readdirSync, statSync } = await import('node:fs')
|
|
1645
1595
|
const { join } = await import('node:path')
|
|
1646
1596
|
const cwd = process.cwd()
|
|
@@ -1663,22 +1613,23 @@ const filesCmd: CommandHandler = async () => {
|
|
|
1663
1613
|
|
|
1664
1614
|
return {
|
|
1665
1615
|
content: [
|
|
1666
|
-
'
|
|
1616
|
+
t('commands.files.title'),
|
|
1667
1617
|
'',
|
|
1668
|
-
|
|
1618
|
+
t('commands.files.cwd', { path: cwd }),
|
|
1669
1619
|
'',
|
|
1670
1620
|
...items,
|
|
1671
|
-
entries.length > 40 ?
|
|
1621
|
+
entries.length > 40 ? t('commands.files.more_files', { count: String(entries.length - 40) }) : '',
|
|
1672
1622
|
'',
|
|
1673
|
-
'
|
|
1623
|
+
t('commands.files.hint'),
|
|
1674
1624
|
].join('\n'),
|
|
1675
1625
|
}
|
|
1676
1626
|
} catch {
|
|
1677
|
-
return { content: '
|
|
1627
|
+
return { content: t('commands.files.error') }
|
|
1678
1628
|
}
|
|
1679
1629
|
}
|
|
1680
1630
|
|
|
1681
1631
|
const statsCmd: CommandHandler = (ctx) => {
|
|
1632
|
+
const t = resolveT(ctx)
|
|
1682
1633
|
const c = ctx.engine.getContext()
|
|
1683
1634
|
const msgs = c.getMessages()
|
|
1684
1635
|
const tokens = c.getEstimatedTokens()
|
|
@@ -1690,26 +1641,27 @@ const statsCmd: CommandHandler = (ctx) => {
|
|
|
1690
1641
|
|
|
1691
1642
|
return {
|
|
1692
1643
|
content: [
|
|
1693
|
-
'
|
|
1644
|
+
t('commands.stats.title'),
|
|
1694
1645
|
'',
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1646
|
+
t('commands.stats.messages', { total: String(msgs.length), user: String(userMsgs), assistant: String(assistantMsgs), system: String(systemMsgs) }),
|
|
1647
|
+
t('commands.stats.tokens', { tokens: tokens.toLocaleString() }),
|
|
1648
|
+
t('commands.stats.tools', { count: String(tools.size) }),
|
|
1649
|
+
t('commands.stats.provider', { provider: ctx.providerId }),
|
|
1650
|
+
t('commands.stats.model', { model: ctx.modelId }),
|
|
1651
|
+
t('commands.stats.permission', { permission: ctx.config.permission }),
|
|
1701
1652
|
'',
|
|
1702
|
-
|
|
1653
|
+
t('commands.stats.usage', { pct: ((tokens / 200_000) * 100).toFixed(1) }),
|
|
1703
1654
|
].join('\n'),
|
|
1704
1655
|
}
|
|
1705
1656
|
}
|
|
1706
1657
|
|
|
1707
1658
|
const summaryCmd: CommandHandler = (ctx) => {
|
|
1659
|
+
const t = resolveT(ctx)
|
|
1708
1660
|
const c = ctx.engine.getContext()
|
|
1709
1661
|
const msgs = c.getMessages()
|
|
1710
1662
|
|
|
1711
1663
|
if (msgs.length === 0) {
|
|
1712
|
-
return { content: '
|
|
1664
|
+
return { content: t('commands.summary.no_conversation') }
|
|
1713
1665
|
}
|
|
1714
1666
|
|
|
1715
1667
|
const userMsgs = msgs
|
|
@@ -1722,32 +1674,24 @@ const summaryCmd: CommandHandler = (ctx) => {
|
|
|
1722
1674
|
|
|
1723
1675
|
return {
|
|
1724
1676
|
content: [
|
|
1725
|
-
'
|
|
1677
|
+
t('commands.summary.title'),
|
|
1726
1678
|
'',
|
|
1727
|
-
|
|
1728
|
-
|
|
1679
|
+
t('commands.summary.total_messages', { count: String(msgs.length) }),
|
|
1680
|
+
t('commands.summary.est_tokens', { tokens: c.getEstimatedTokens().toLocaleString() }),
|
|
1729
1681
|
'',
|
|
1730
|
-
'
|
|
1682
|
+
t('commands.summary.recent_topics'),
|
|
1731
1683
|
...userMsgs.map((t, i) => ` ${i + 1}. ${t}`),
|
|
1732
1684
|
'',
|
|
1733
|
-
'
|
|
1685
|
+
t('commands.summary.footer'),
|
|
1734
1686
|
].join('\n'),
|
|
1735
1687
|
}
|
|
1736
1688
|
}
|
|
1737
1689
|
|
|
1738
1690
|
const cdCmd: CommandHandler = async (ctx, args) => {
|
|
1691
|
+
const t = resolveT(ctx)
|
|
1739
1692
|
const target = args[0]
|
|
1740
1693
|
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
|
-
}
|
|
1694
|
+
return { content: t('commands.cd.usage', { cwd: process.cwd() }) }
|
|
1751
1695
|
}
|
|
1752
1696
|
|
|
1753
1697
|
const { existsSync } = await import('node:fs')
|
|
@@ -1755,13 +1699,12 @@ const cdCmd: CommandHandler = async (ctx, args) => {
|
|
|
1755
1699
|
const resolved = resolve(target.replace(/^~/, process.env.HOME || '~'))
|
|
1756
1700
|
|
|
1757
1701
|
if (!existsSync(resolved)) {
|
|
1758
|
-
return { content:
|
|
1702
|
+
return { content: t('commands.cd.not_found', { path: resolved }) }
|
|
1759
1703
|
}
|
|
1760
1704
|
|
|
1761
1705
|
try {
|
|
1762
1706
|
process.chdir(resolved)
|
|
1763
1707
|
|
|
1764
|
-
// Persist cwd to active session (best-effort)
|
|
1765
1708
|
try {
|
|
1766
1709
|
const { SessionStore } = await import('../core/session-store')
|
|
1767
1710
|
const saved = SessionStore.load(ctx.sessionId)
|
|
@@ -1776,47 +1719,26 @@ const cdCmd: CommandHandler = async (ctx, args) => {
|
|
|
1776
1719
|
/* session persistence is best-effort */
|
|
1777
1720
|
}
|
|
1778
1721
|
|
|
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
|
-
}
|
|
1722
|
+
return { content: t('commands.cd.content', { path: resolved }) }
|
|
1789
1723
|
} catch (err) {
|
|
1790
|
-
return { content:
|
|
1724
|
+
return { content: t('commands.cd.failed', { error: (err as Error).message }) }
|
|
1791
1725
|
}
|
|
1792
1726
|
}
|
|
1793
1727
|
|
|
1794
|
-
const hooksCmd: CommandHandler = async () => {
|
|
1728
|
+
const hooksCmd: CommandHandler = async (ctx) => {
|
|
1729
|
+
const t = resolveT(ctx)
|
|
1795
1730
|
const { existsSync, readdirSync } = await import('node:fs')
|
|
1796
1731
|
const { join } = await import('node:path')
|
|
1797
1732
|
const cwd = process.cwd()
|
|
1798
1733
|
const hooksDir = join(cwd, '.mipham', 'hooks')
|
|
1799
1734
|
|
|
1800
1735
|
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
|
-
}
|
|
1736
|
+
return { content: t('commands.hooks.no_hooks') }
|
|
1815
1737
|
}
|
|
1816
1738
|
|
|
1817
1739
|
try {
|
|
1818
1740
|
const files = readdirSync(hooksDir).filter((f) => f.endsWith('.sh'))
|
|
1819
|
-
const lines: string[] = ['
|
|
1741
|
+
const lines: string[] = [t('commands.hooks.title'), '', t('commands.hooks.location', { path: hooksDir }), '']
|
|
1820
1742
|
|
|
1821
1743
|
for (const f of files) {
|
|
1822
1744
|
try {
|
|
@@ -1831,35 +1753,16 @@ const hooksCmd: CommandHandler = async () => {
|
|
|
1831
1753
|
}
|
|
1832
1754
|
|
|
1833
1755
|
lines.push('')
|
|
1834
|
-
lines.push(
|
|
1756
|
+
lines.push(t('commands.hooks.found', { count: String(files.length) }))
|
|
1835
1757
|
return { content: lines.join('\n') }
|
|
1836
1758
|
} catch {
|
|
1837
|
-
return { content: '
|
|
1759
|
+
return { content: t('commands.hooks.error') }
|
|
1838
1760
|
}
|
|
1839
1761
|
}
|
|
1840
1762
|
|
|
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
|
-
}
|
|
1763
|
+
const batchCmd: CommandHandler = async (ctx) => {
|
|
1764
|
+
const t = resolveT(ctx)
|
|
1765
|
+
return { content: t('commands.batch.title') }
|
|
1863
1766
|
}
|
|
1864
1767
|
|
|
1865
1768
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1867,6 +1770,7 @@ const batchCmd: CommandHandler = async () => {
|
|
|
1867
1770
|
// ═══════════════════════════════════════════════════════════════
|
|
1868
1771
|
|
|
1869
1772
|
const exportCmd: CommandHandler = async (ctx) => {
|
|
1773
|
+
const t = resolveT(ctx)
|
|
1870
1774
|
const { writeFileSync } = await import('node:fs')
|
|
1871
1775
|
const { join } = await import('node:path')
|
|
1872
1776
|
|
|
@@ -1897,7 +1801,7 @@ const exportCmd: CommandHandler = async (ctx) => {
|
|
|
1897
1801
|
|
|
1898
1802
|
writeFileSync(filepath, lines.join('\n'), 'utf-8')
|
|
1899
1803
|
return {
|
|
1900
|
-
content:
|
|
1804
|
+
content: t('commands.export.confirmed', { path: filepath, count: String(msgs.length), lines: String(lines.length) }),
|
|
1901
1805
|
copyContent: filepath,
|
|
1902
1806
|
}
|
|
1903
1807
|
}
|
|
@@ -1976,21 +1880,19 @@ const prCommentsCmd: CommandHandler = async () => {
|
|
|
1976
1880
|
// Resume
|
|
1977
1881
|
// ═══════════════════════════════════════════════════════════════
|
|
1978
1882
|
|
|
1979
|
-
const resumeLastCmd: CommandHandler = async () => {
|
|
1883
|
+
const resumeLastCmd: CommandHandler = async (ctx) => {
|
|
1884
|
+
const t = resolveT(ctx)
|
|
1980
1885
|
const { SessionStore } = await import('../core/session-store')
|
|
1981
1886
|
|
|
1982
1887
|
const latest = SessionStore.getLatest()
|
|
1983
1888
|
if (!latest) {
|
|
1984
|
-
return {
|
|
1985
|
-
content:
|
|
1986
|
-
'─ Resume Session ─\n\nNo saved sessions found.\n\nSessions are auto-saved to ~/.mipham/sessions/ when Mipham Code exits.',
|
|
1987
|
-
}
|
|
1889
|
+
return { content: `${t('commands.resume.restored')}\n\n${t('commands.resume.no_sessions')}\n\n${t('commands.resume.empty_footer')}` }
|
|
1988
1890
|
}
|
|
1989
1891
|
|
|
1990
1892
|
const session = SessionStore.load(latest.name)
|
|
1991
1893
|
if (!session) {
|
|
1992
1894
|
return {
|
|
1993
|
-
content:
|
|
1895
|
+
content: `${t('commands.resume.load_failed')}\n\nCould not load session "${latest.name}". The file may have been removed.`,
|
|
1994
1896
|
}
|
|
1995
1897
|
}
|
|
1996
1898
|
|
|
@@ -1999,109 +1901,86 @@ const resumeLastCmd: CommandHandler = async () => {
|
|
|
1999
1901
|
const messages = truncated ? session.messages.slice(-MAX_RESUME) : session.messages
|
|
2000
1902
|
|
|
2001
1903
|
const date = new Date(latest.updatedAt).toLocaleString()
|
|
1904
|
+
const truncatedStr = truncated ? ` (showing last ${MAX_RESUME})` : ''
|
|
1905
|
+
|
|
2002
1906
|
return {
|
|
2003
1907
|
content: [
|
|
2004
|
-
'
|
|
1908
|
+
t('commands.resume.restored'),
|
|
2005
1909
|
'',
|
|
2006
|
-
|
|
2007
|
-
`Messages: ${session.messages.length} total${truncated ? ` (showing last ${MAX_RESUME})` : ''}`,
|
|
2008
|
-
`Provider: ${latest.provider} / ${latest.model}`,
|
|
2009
|
-
`Updated: ${date}`,
|
|
1910
|
+
t('commands.resume.restored_content', { name: latest.name, total: String(session.messages.length), truncated: truncatedStr, provider: latest.provider, model: latest.model, date }),
|
|
2010
1911
|
'',
|
|
2011
1912
|
truncated
|
|
2012
|
-
?
|
|
2013
|
-
:
|
|
1913
|
+
? t('commands.resume.restored_footer', { loaded: String(messages.length), total: String(session.messages.length) })
|
|
1914
|
+
: t('commands.resume.restored_full_footer', { loaded: String(messages.length) }),
|
|
2014
1915
|
].join('\n'),
|
|
2015
1916
|
forwardedMessages: messages,
|
|
2016
1917
|
}
|
|
2017
1918
|
}
|
|
2018
1919
|
|
|
2019
|
-
const resumeDeleteCmd: CommandHandler = async (
|
|
1920
|
+
const resumeDeleteCmd: CommandHandler = async (ctx, args) => {
|
|
1921
|
+
const t = resolveT(ctx)
|
|
2020
1922
|
const name = args.join(' ').trim()
|
|
2021
1923
|
if (!name) {
|
|
2022
|
-
return {
|
|
2023
|
-
content:
|
|
2024
|
-
'Usage: /resume delete <session-name>\n\nDelete a saved session. Use /resume to list all sessions.',
|
|
2025
|
-
}
|
|
1924
|
+
return { content: t('commands.resume.delete_usage') }
|
|
2026
1925
|
}
|
|
2027
1926
|
|
|
2028
1927
|
const { SessionStore } = await import('../core/session-store')
|
|
2029
1928
|
const deleted = SessionStore.delete(name)
|
|
2030
1929
|
|
|
2031
1930
|
return deleted
|
|
2032
|
-
? { content:
|
|
2033
|
-
: { content:
|
|
1931
|
+
? { content: t('commands.resume.delete_confirmed', { name }) }
|
|
1932
|
+
: { content: t('commands.resume.delete_not_found', { name }) }
|
|
2034
1933
|
}
|
|
2035
1934
|
|
|
2036
|
-
const resumeCmd: CommandHandler = async (
|
|
1935
|
+
const resumeCmd: CommandHandler = async (ctx, args) => {
|
|
1936
|
+
const t = resolveT(ctx)
|
|
2037
1937
|
const sub = args[0]?.toLowerCase()
|
|
2038
1938
|
|
|
2039
|
-
// Sub-command: /resume last
|
|
2040
1939
|
if (sub === 'last') {
|
|
2041
|
-
return resumeLastCmd(
|
|
1940
|
+
return resumeLastCmd(ctx, args.slice(1))
|
|
2042
1941
|
}
|
|
2043
1942
|
|
|
2044
|
-
// Sub-command: /resume delete <name>
|
|
2045
1943
|
if (sub === 'delete') {
|
|
2046
|
-
return resumeDeleteCmd(
|
|
1944
|
+
return resumeDeleteCmd(ctx, args.slice(1))
|
|
2047
1945
|
}
|
|
2048
1946
|
|
|
2049
1947
|
const { SessionStore } = await import('../core/session-store')
|
|
2050
1948
|
|
|
2051
|
-
// If a name is provided, show load instructions
|
|
2052
1949
|
const targetName = args.join(' ')
|
|
2053
1950
|
if (targetName) {
|
|
2054
1951
|
const session = SessionStore.load(targetName)
|
|
2055
1952
|
if (session) {
|
|
2056
1953
|
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'),
|
|
1954
|
+
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
1955
|
}
|
|
2072
1956
|
}
|
|
2073
|
-
return {
|
|
2074
|
-
content: `─ Session Not Found ─\n\nNo session named "${targetName}".\n\nUse /resume without arguments to list all saved sessions.`,
|
|
2075
|
-
}
|
|
1957
|
+
return { content: `${t('commands.resume.not_found_title')}\n\n${t('commands.resume.not_found_content', { name: targetName })}` }
|
|
2076
1958
|
}
|
|
2077
1959
|
|
|
2078
1960
|
const sessions = SessionStore.list()
|
|
2079
1961
|
|
|
2080
1962
|
if (sessions.length === 0) {
|
|
2081
|
-
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.',
|
|
2084
|
-
}
|
|
1963
|
+
return { content: `${t('commands.resume.restored')}\n\n${t('commands.resume.no_sessions')}\n\n${t('commands.resume.empty_footer')}` }
|
|
2085
1964
|
}
|
|
2086
1965
|
|
|
2087
1966
|
const recent = sessions.slice(0, 10)
|
|
2088
1967
|
|
|
2089
1968
|
const lines: string[] = [
|
|
2090
|
-
'
|
|
1969
|
+
t('commands.resume.saved_title'),
|
|
2091
1970
|
'',
|
|
2092
1971
|
...recent.map(
|
|
2093
1972
|
(s, i) =>
|
|
2094
1973
|
` ${(i + 1).toString().padStart(2)}. ${s.name.padEnd(45)} ${s.messageCount.toString().padStart(4)} msgs ${new Date(s.updatedAt).toLocaleString()}`,
|
|
2095
1974
|
),
|
|
2096
1975
|
'',
|
|
2097
|
-
|
|
1976
|
+
t('commands.resume.total_footer', { count: String(sessions.length) }),
|
|
2098
1977
|
'',
|
|
2099
|
-
'
|
|
2100
|
-
'
|
|
2101
|
-
'
|
|
2102
|
-
'
|
|
1978
|
+
t('commands.resume.resume_hint'),
|
|
1979
|
+
t('commands.resume.resume_last_hint'),
|
|
1980
|
+
t('commands.resume.delete_hint'),
|
|
1981
|
+
t('commands.resume.cli_hint'),
|
|
2103
1982
|
'',
|
|
2104
|
-
'
|
|
1983
|
+
t('commands.resume.auto_save_hint'),
|
|
2105
1984
|
]
|
|
2106
1985
|
|
|
2107
1986
|
return { content: lines.join('\n') }
|
|
@@ -2111,7 +1990,8 @@ const resumeCmd: CommandHandler = async (_ctx, args) => {
|
|
|
2111
1990
|
// Memory Management
|
|
2112
1991
|
// ═══════════════════════════════════════════════════════════════
|
|
2113
1992
|
|
|
2114
|
-
const memoryCmd: CommandHandler = async () => {
|
|
1993
|
+
const memoryCmd: CommandHandler = async (ctx) => {
|
|
1994
|
+
const t = resolveT(ctx)
|
|
2115
1995
|
const { existsSync, readdirSync, readFileSync, statSync } = await import('node:fs')
|
|
2116
1996
|
const { join } = await import('node:path')
|
|
2117
1997
|
|
|
@@ -2119,16 +1999,13 @@ const memoryCmd: CommandHandler = async () => {
|
|
|
2119
1999
|
const memoryDir = join(home, '.mipham', 'memory')
|
|
2120
2000
|
|
|
2121
2001
|
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
|
-
}
|
|
2002
|
+
return { content: t('commands.memories.no_memories') }
|
|
2126
2003
|
}
|
|
2127
2004
|
|
|
2128
2005
|
try {
|
|
2129
2006
|
const files = readdirSync(memoryDir).filter((f) => f.endsWith('.md'))
|
|
2130
2007
|
if (files.length === 0) {
|
|
2131
|
-
return { content: '
|
|
2008
|
+
return { content: t('commands.memories.empty') }
|
|
2132
2009
|
}
|
|
2133
2010
|
|
|
2134
2011
|
const memories: Array<{ file: string; size: number; mtime: Date; title: string }> = []
|
|
@@ -2149,23 +2026,22 @@ const memoryCmd: CommandHandler = async () => {
|
|
|
2149
2026
|
memories.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
|
|
2150
2027
|
|
|
2151
2028
|
const lines: string[] = [
|
|
2152
|
-
'
|
|
2029
|
+
t('commands.memories.title'),
|
|
2153
2030
|
'',
|
|
2154
|
-
|
|
2155
|
-
|
|
2031
|
+
t('commands.memories.location', { path: memoryDir }),
|
|
2032
|
+
t('commands.memories.total', { count: String(memories.length), plural: memories.length === 1 ? 'y' : 'ies' }),
|
|
2156
2033
|
'',
|
|
2157
2034
|
...memories.map(
|
|
2158
2035
|
(m, i) =>
|
|
2159
2036
|
` ${(i + 1).toString().padStart(2)}. ${m.file.padEnd(35)} ${(m.size / 1024).toFixed(1)}KB ${m.mtime.toLocaleDateString()} ${m.title}`,
|
|
2160
2037
|
),
|
|
2161
2038
|
'',
|
|
2162
|
-
'
|
|
2163
|
-
'Each file in ~/.mipham/memory/ represents one remembered fact or preference.',
|
|
2039
|
+
t('commands.memories.footer'),
|
|
2164
2040
|
]
|
|
2165
2041
|
|
|
2166
2042
|
return { content: lines.join('\n') }
|
|
2167
2043
|
} catch {
|
|
2168
|
-
return { content: '
|
|
2044
|
+
return { content: t('commands.memories.error') }
|
|
2169
2045
|
}
|
|
2170
2046
|
}
|
|
2171
2047
|
|
|
@@ -2173,7 +2049,8 @@ const memoryCmd: CommandHandler = async () => {
|
|
|
2173
2049
|
// Upgrade
|
|
2174
2050
|
// ═══════════════════════════════════════════════════════════════
|
|
2175
2051
|
|
|
2176
|
-
const upgradeCmd: CommandHandler = async () => {
|
|
2052
|
+
const upgradeCmd: CommandHandler = async (ctx) => {
|
|
2053
|
+
const t = resolveT(ctx)
|
|
2177
2054
|
const { checkForUpdates, backupConfig, performUpdate, restoreConfig, getConfigPath } =
|
|
2178
2055
|
await import('../shared/update')
|
|
2179
2056
|
|
|
@@ -2181,27 +2058,16 @@ const upgradeCmd: CommandHandler = async () => {
|
|
|
2181
2058
|
|
|
2182
2059
|
if (!update.available) {
|
|
2183
2060
|
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`,
|
|
2061
|
+
content: t('commands.upgrade.uptodate', { current: update.current, latest: update.latest }),
|
|
2192
2062
|
}
|
|
2193
2063
|
}
|
|
2194
2064
|
|
|
2195
|
-
// Update available — back up config and perform the upgrade
|
|
2196
2065
|
const backupPath = backupConfig(`upgrade-v${update.current}`)
|
|
2197
2066
|
|
|
2198
2067
|
const lines: string[] = [
|
|
2199
|
-
'
|
|
2068
|
+
t('commands.upgrade.title'),
|
|
2200
2069
|
'',
|
|
2201
|
-
|
|
2202
|
-
`Latest: v${update.latest}`,
|
|
2203
|
-
'',
|
|
2204
|
-
`→ New version available! Updating...`,
|
|
2070
|
+
t('commands.upgrade.available', { current: update.current, latest: update.latest }),
|
|
2205
2071
|
'',
|
|
2206
2072
|
]
|
|
2207
2073
|
|
|
@@ -2215,39 +2081,47 @@ To check manually: https://www.npmjs.com/package/@miphamai/cli`,
|
|
|
2215
2081
|
const configPath = getConfigPath()
|
|
2216
2082
|
const { existsSync } = await import('node:fs')
|
|
2217
2083
|
lines.push('')
|
|
2218
|
-
lines.push(
|
|
2084
|
+
lines.push(t('commands.upgrade.updated', { version: update.latest }))
|
|
2219
2085
|
|
|
2220
2086
|
if (existsSync(configPath)) {
|
|
2221
|
-
lines.push(
|
|
2087
|
+
lines.push(t('commands.upgrade.config_preserved', { path: configPath }))
|
|
2222
2088
|
} else if (backupPath) {
|
|
2223
2089
|
if (restoreConfig(backupPath)) {
|
|
2224
|
-
lines.push('
|
|
2090
|
+
lines.push(t('commands.upgrade.config_restored'))
|
|
2225
2091
|
}
|
|
2226
2092
|
}
|
|
2227
2093
|
|
|
2228
2094
|
lines.push('')
|
|
2229
|
-
lines.push('
|
|
2230
|
-
lines.push(' Run `mipham` again to use the new version, or `mipham --version` to verify.')
|
|
2095
|
+
lines.push(t('commands.upgrade.old_version_warning'))
|
|
2231
2096
|
} else {
|
|
2232
2097
|
lines.push('')
|
|
2233
|
-
lines.push('
|
|
2234
|
-
lines.push(` Try manually: ${NPM_UPDATE_COMMAND}`)
|
|
2235
|
-
if (backupPath) {
|
|
2236
|
-
lines.push(` Your config backup is at: ${backupPath}`)
|
|
2237
|
-
}
|
|
2098
|
+
lines.push(t('commands.upgrade.update_failed', { command: NPM_UPDATE_COMMAND, path: backupPath || '' }))
|
|
2238
2099
|
}
|
|
2239
2100
|
|
|
2240
2101
|
return { content: lines.join('\n') }
|
|
2241
2102
|
}
|
|
2242
2103
|
|
|
2104
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2105
|
+
// Language — i18n locale control
|
|
2106
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2107
|
+
|
|
2108
|
+
const langCmd: CommandHandler = (ctx, args) => {
|
|
2109
|
+
const t = resolveT(ctx)
|
|
2110
|
+
const requested = args[0]
|
|
2111
|
+
if (!requested || !['en-US', 'zh-CN'].includes(requested)) {
|
|
2112
|
+
return { content: t('commands.lang.current', { locale: 'en-US' }) }
|
|
2113
|
+
}
|
|
2114
|
+
return { content: t('commands.lang.set', { locale: requested }) }
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2243
2117
|
// ═══════════════════════════════════════════════════════════════
|
|
2244
2118
|
// No-Plan — exit plan mode
|
|
2245
2119
|
// ═══════════════════════════════════════════════════════════════
|
|
2246
2120
|
|
|
2247
|
-
const noPlanCmd: CommandHandler = () =>
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
}
|
|
2121
|
+
const noPlanCmd: CommandHandler = (ctx) => {
|
|
2122
|
+
const t = resolveT(ctx)
|
|
2123
|
+
return { content: t('commands.no_plan.confirmed') }
|
|
2124
|
+
}
|
|
2251
2125
|
|
|
2252
2126
|
// ═══════════════════════════════════════════════════════════════
|
|
2253
2127
|
// Workflows
|
|
@@ -3310,6 +3184,7 @@ registry.set('/prompt-audit', promptAuditCmd)
|
|
|
3310
3184
|
|
|
3311
3185
|
// Environment
|
|
3312
3186
|
registry.set('/theme', themeCmd)
|
|
3187
|
+
registry.set('/lang', langCmd)
|
|
3313
3188
|
registry.set('/ide', ideCmd)
|
|
3314
3189
|
registry.set('/terminal-setup', terminalSetupCmd)
|
|
3315
3190
|
registry.set('/release-notes', releaseNotesCmd)
|
|
@@ -3407,6 +3282,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
3407
3282
|
'/fast': 'Toggle fast mode',
|
|
3408
3283
|
'/effort': 'Set reasoning effort',
|
|
3409
3284
|
'/theme': 'Set terminal theme',
|
|
3285
|
+
'/lang': 'Set display language (en-US, zh-CN)',
|
|
3410
3286
|
'/tools': 'List available tools',
|
|
3411
3287
|
'/skills': 'List loaded skills',
|
|
3412
3288
|
'/reload-skills': 'Reload all skills',
|