@miphamai/cli 0.51.0 → 0.53.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.
@@ -3545,6 +3545,22 @@ const resumeCmd: CommandHandler = async (ctx, args) => {
3545
3545
  return { content: lines.join('\n') }
3546
3546
  }
3547
3547
 
3548
+ // ═══════════════════════════════════════════════════════════════
3549
+ // Wiki Save
3550
+ // ═══════════════════════════════════════════════════════════════
3551
+
3552
+ const saveCmd: CommandHandler = (ctx, args) => {
3553
+ const t = resolveT(ctx)
3554
+ const target = args.join(' ').trim()
3555
+ const override = target
3556
+ ? `\nUser override: "${target}". If it names a note type (synthesis/concept/source/decision/session), file under that type; otherwise treat it as the note title.`
3557
+ : ''
3558
+ return {
3559
+ content: t('commands.save.content'),
3560
+ forwardToAI: `Invoke the save-to-wiki skill (use the Skill tool) to save this conversation into the Obsidian wiki.${override}`,
3561
+ }
3562
+ }
3563
+
3548
3564
  // ═══════════════════════════════════════════════════════════════
3549
3565
  // Memory Management
3550
3566
  // ═══════════════════════════════════════════════════════════════
@@ -4575,6 +4591,7 @@ const commandsListCmd: CommandHandler = () => {
4575
4591
  '/rename': 'Session & Identity',
4576
4592
  '/goal': 'Session & Identity',
4577
4593
  '/recap': 'Session & Identity',
4594
+ '/save': 'Session & Identity',
4578
4595
  '/export': 'Session & Identity',
4579
4596
  '/doctor': 'Session & Identity',
4580
4597
  '/dream': 'Session & Identity',
@@ -4665,6 +4682,7 @@ const commandsListCmd: CommandHandler = () => {
4665
4682
  '/keys': 'Account',
4666
4683
  '/keys rotate': 'Account',
4667
4684
  '/keys audit': 'Account',
4685
+ '/keys view': 'Account',
4668
4686
  '/feedback': 'Account',
4669
4687
  '/agents': 'Agents',
4670
4688
  '/bg': 'Agents',
@@ -4820,6 +4838,7 @@ registry.set('/resume', resumeCmd)
4820
4838
  registry.set('/resume last', resumeLastCmd)
4821
4839
  registry.set('/resume delete', resumeDeleteCmd)
4822
4840
  registry.set('/memory', memoryCmd)
4841
+ registry.set('/save', saveCmd)
4823
4842
  registry.set('/upgrade', upgradeCmd)
4824
4843
 
4825
4844
  // Project
@@ -4847,6 +4866,7 @@ registry.set('/logout', logoutCmd)
4847
4866
  registry.set('/keys', keysCmd)
4848
4867
  registry.set('/keys rotate', keysCmd)
4849
4868
  registry.set('/keys audit', keysCmd)
4869
+ registry.set('/keys view', keysCmd)
4850
4870
  registry.set('/feedback', feedbackCmd)
4851
4871
  registry.set('/agents', agentsCmd)
4852
4872
  registry.set('/bg', bgCmd)
@@ -4914,6 +4934,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
4914
4934
  '/rename': 'Rename current session',
4915
4935
  '/goal': 'Set session goal',
4916
4936
  '/recap': 'Summarize session so far',
4937
+ '/save': 'Save conversation to Obsidian wiki (skill: save-to-wiki)',
4917
4938
  '/export': 'Export conversation to file',
4918
4939
  '/doctor': 'System diagnostics',
4919
4940
  '/dream': 'Background memory consolidation',
@@ -5006,6 +5027,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
5006
5027
  '/keys': 'List API key rotation status',
5007
5028
  '/keys rotate': 'Rotate an API key',
5008
5029
  '/keys audit': 'Check for expired keys',
5030
+ '/keys view': 'View a provider plaintext API key',
5009
5031
  '/feedback': 'Send feedback',
5010
5032
  '/agents': 'Agent view dashboard',
5011
5033
  '/bg': 'Run a background agent task',
@@ -18,6 +18,7 @@ import { mkdirSync, writeFileSync } from 'node:fs'
18
18
  import { join } from 'node:path'
19
19
  import { homedir } from 'node:os'
20
20
  import { execSync } from 'node:child_process'
21
+ import { getCredentialKey, encryptApiKey } from '../config/credential-crypto'
21
22
 
22
23
  // ── Types ──
23
24
 
@@ -93,6 +94,8 @@ function writeConfigFile(providerId: string, modelId: string, apiKey: string): v
93
94
  }
94
95
  return result
95
96
  }
97
+ const storedKey = encryptApiKey(apiKey, getCredentialKey(configDir))
98
+
96
99
  const lines = [
97
100
  '# Mipham Code Configuration',
98
101
  `# Generated by Config Wizard — ${new Date().toISOString()}`,
@@ -107,7 +110,7 @@ function writeConfigFile(providerId: string, modelId: string, apiKey: string): v
107
110
  ` name: ${provider?.name || providerId}`,
108
111
  ` protocol: ${provider?.protocol || 'openai-compatible'}`,
109
112
  ...(provider?.baseUrl ? [` baseUrl: ${provider.baseUrl}`] : []),
110
- ` apiKey: ${apiKey}`,
113
+ ` apiKey: ${storedKey}`,
111
114
  ` models:`,
112
115
  ...models.map((m) => ` - id: ${m.id}`),
113
116
  ]
package/src/ui/input.tsx CHANGED
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useEffect, useRef, useMemo } from 'react'
2
2
  import { Box, Text, useInput } from 'ink'
3
3
  import TextInput from 'ink-text-input'
4
- import { VimMotionEngine, type VimMode } from './vim-motions.js'
4
+ import { VimMotionEngine, handleSearchBackspace, type VimMode } from './vim-motions.js'
5
5
  import { getCommandList } from './commands.js'
6
6
  import { CommandPicker } from './command-picker.js'
7
7
  import { useI18n } from '../i18n-context'
@@ -274,7 +274,9 @@ export function InputBar({
274
274
  return
275
275
  }
276
276
  if (key.backspace || key.delete) {
277
- setSearchQuery((q) => q.slice(0, -1))
277
+ const next = handleSearchBackspace(searchQuery)
278
+ setSearchQuery(next.query)
279
+ if (next.exit) setSearchMode(false)
278
280
  return
279
281
  }
280
282
  // Accumulate printable characters
@@ -94,3 +94,20 @@ export interface VimAction {
94
94
  cursor?: number
95
95
  pending?: string
96
96
  }
97
+
98
+ export interface SearchBackspaceResult {
99
+ query: string
100
+ /** true → the `/` prefix should be removed by exiting search mode */
101
+ exit: boolean
102
+ }
103
+
104
+ /**
105
+ * Backspace while in `/` search mode.
106
+ * Trims the query; when the query is already empty, signals that search mode
107
+ * should exit so the `/` placeholder disappears (the user can "delete" the
108
+ * slash instead of being trapped).
109
+ */
110
+ export function handleSearchBackspace(query: string): SearchBackspaceResult {
111
+ if (query.length === 0) return { query: '', exit: true }
112
+ return { query: query.slice(0, -1), exit: false }
113
+ }
@@ -1,80 +0,0 @@
1
- import type { Tiktoken } from 'js-tiktoken'
2
- import type { Message } from '../shared/types'
3
-
4
- let encoder: Tiktoken | null = null
5
- let initPromise: Promise<void> | null = null
6
-
7
- async function getEncoder(): Promise<Tiktoken> {
8
- if (encoder) return encoder
9
- if (!initPromise) {
10
- initPromise = (async () => {
11
- const { getEncoding } = await import('js-tiktoken')
12
- encoder = getEncoding('cl100k_base')
13
- })()
14
- }
15
- await initPromise
16
- return encoder!
17
- }
18
-
19
- export class TokenCounter {
20
- private cache = new Map<string, number>()
21
- private initialized = false
22
-
23
- private async ensureInit(): Promise<void> {
24
- if (!this.initialized) {
25
- await getEncoder()
26
- this.initialized = true
27
- }
28
- }
29
-
30
- async count(text: string): Promise<number> {
31
- if (!text) return 0
32
- const cached = this.cache.get(text)
33
- if (cached !== undefined) return cached
34
- await this.ensureInit()
35
- const tokens = encoder!.encode(text).length
36
- this.cache.set(text, tokens)
37
- return tokens
38
- }
39
-
40
- /** Synchronous fallback — chars/4 heuristic when WASM not loaded. */
41
- countSync(text: string): number {
42
- if (!text) return 0
43
- const cached = this.cache.get(text)
44
- if (cached !== undefined) return cached
45
- return Math.ceil(text.length / 4)
46
- }
47
-
48
- async countMessages(messages: Message[]): Promise<number> {
49
- let total = 0
50
- for (const msg of messages) {
51
- if (typeof msg.content === 'string') {
52
- total += await this.count(msg.content)
53
- } else if (Array.isArray(msg.content)) {
54
- for (const block of msg.content) {
55
- if (block.type === 'text' && block.text) {
56
- total += await this.count(block.text)
57
- }
58
- }
59
- }
60
- total += 4 // per-message format overhead
61
- }
62
- return total
63
- }
64
-
65
- async truncateToTokens(text: string, maxTokens: number): Promise<string> {
66
- await this.ensureInit()
67
- const tokens = encoder!.encode(text)
68
- if (tokens.length <= maxTokens) return text
69
- return text.slice(0, Math.floor(text.length * (maxTokens / tokens.length)))
70
- }
71
-
72
- invalidateCache(): void {
73
- this.cache.clear()
74
- }
75
-
76
- static reset(): void {
77
- encoder = null
78
- initPromise = null
79
- }
80
- }