@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.
- package/package.json +1 -2
- package/skills/mipham/save-to-wiki.mipham-skill.md +92 -0
- package/src/commands/keys.ts +26 -0
- package/src/config/credential-crypto.ts +114 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/loader.ts +65 -14
- package/src/core/instructions.ts +36 -12
- package/src/daemon/channel-message.ts +59 -0
- package/src/daemon/feishu/adapter.ts +16 -27
- package/src/daemon/index.ts +17 -0
- package/src/daemon/server.ts +18 -0
- package/src/daemon/telegram/adapter.ts +16 -27
- package/src/daemon/wecom/adapter.ts +61 -0
- package/src/daemon/wecom/api.ts +58 -0
- package/src/daemon/wecom/env.ts +16 -0
- package/src/daemon/wecom/types.ts +12 -0
- package/src/daemon/wecom/ws-client.ts +77 -0
- package/src/i18n-core/locales/en-US.json +3 -0
- package/src/i18n-core/locales/zh-CN.json +3 -0
- package/src/index.tsx +4 -8
- package/src/mcp/token-store.ts +2 -37
- package/src/shared/package-info.ts +1 -1
- package/src/shared/types.ts +3 -3
- package/src/skills/bin-check.ts +40 -0
- package/src/skills/bundled-skills.ts +1 -0
- package/src/skills/loader.ts +35 -10
- package/src/skills/sanitizer.ts +1 -2
- package/src/skills/seam.ts +1 -1
- package/src/tools/agent/skill.ts +14 -0
- package/src/ui/commands.ts +22 -0
- package/src/ui/config-wizard.tsx +4 -1
- package/src/ui/input.tsx +4 -2
- package/src/ui/vim-motions.ts +17 -0
- package/src/core/tokenizer.ts +0 -80
package/src/ui/commands.ts
CHANGED
|
@@ -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',
|
package/src/ui/config-wizard.tsx
CHANGED
|
@@ -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: ${
|
|
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
|
-
|
|
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
|
package/src/ui/vim-motions.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/core/tokenizer.ts
DELETED
|
@@ -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
|
-
}
|