refactor: client

This commit is contained in:
Acbox
2026-01-11 16:57:55 +08:00
parent 2eee14f1ab
commit cc3e85c8b0
33 changed files with 2557 additions and 1152 deletions
+221
View File
@@ -0,0 +1,221 @@
# MemoHome CLI 架构说明
## 项目重构概述
本项目已重构为两个清晰分离的层次:
1. **Core 层** (`src/core/`): 纯粹的功能函数,不依赖任何 CLI UI 库
2. **CLI 层** (`src/cli/`): 命令行交互界面,负责用户输入输出
## 目录结构
```
src/
├── core/ # 核心功能层(可被其他项目使用)
│ ├── index.ts # 统一导出所有核心功能
│ ├── auth.ts # 认证相关功能
│ ├── user.ts # 用户管理功能
│ ├── model.ts # 模型配置功能
│ ├── agent.ts # AI 对话功能
│ ├── memory.ts # 记忆管理功能
│ ├── schedule.ts # 定时任务功能
│ ├── settings.ts # 设置管理功能
│ ├── debug.ts # 调试工具
│ ├── config.ts # 配置管理
│ └── client.ts # API 客户端
├── cli/ # CLI 交互层
│ ├── index.ts # CLI 入口
│ └── commands/ # 命令定义
│ ├── auth.ts
│ ├── user.ts
│ ├── model.ts
│ ├── agent.ts
│ ├── memory.ts
│ ├── schedule.ts
│ ├── config.ts
│ └── debug.ts
├── types/ # 类型定义
│ └── index.ts
├── utils/ # 工具函数
│ └── index.ts
└── index.ts # 主导出文件
```
## Core 层特点
Core 层提供纯粹的功能函数,特点:
-**无 UI 依赖**: 不使用 chalk, ora, inquirer 等 CLI UI 库
-**类型安全**: 提供完整的 TypeScript 类型定义
-**错误处理**: 通过 throw Error 返回错误,调用者可自行处理
-**可复用**: 可被 CLI 或其他项目(如 Web 应用)导入使用
-**单一职责**: 每个模块只负责特定功能域
### Core 层 API 示例
```typescript
// Auth
import { login, logout, getCurrentUser } from '@memohome/cli/core'
await login({ username: 'admin', password: 'password' })
const user = await getCurrentUser()
logout()
// Agent
import { chat, chatStream } from '@memohome/cli/core'
// 非流式对话
const response = await chat({
message: 'Hello',
language: 'Chinese'
})
// 流式对话
await chatStream({ message: 'Hello' }, async (event) => {
if (event.type === 'text-delta') {
console.log(event.text)
}
})
// Model
import { listModels, createModel } from '@memohome/cli/core'
const models = await listModels()
const newModel = await createModel({
name: 'GPT-4',
modelId: 'gpt-4',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'sk-xxx',
clientType: 'openai',
type: 'chat'
})
// Memory
import { searchMemory, addMemory, getMessages } from '@memohome/cli/core'
const memories = await searchMemory({ query: 'test', limit: 10 })
await addMemory({ content: 'Important note' })
const messages = await getMessages({ page: 1, limit: 20 })
```
## CLI 层特点
CLI 层负责用户交互,特点:
-**命令定义**: 使用 commander.js 定义命令
-**美化输出**: 使用 chalk 颜色、ora 加载动画
-**交互输入**: 使用 inquirer 提示用户输入
-**错误显示**: 友好的错误信息展示
-**调用 Core**: 所有业务逻辑调用 Core 层函数
## 使用方式
### 1. 作为 CLI 使用
```bash
# 安装
pnpm install
# 运行命令
memohome auth login
memohome agent chat "Hello"
memohome model list
```
### 2. 作为库使用
在其他项目中导入:
```typescript
// 导入所有功能
import * as memohome from '@memohome/cli'
// 或导入特定模块
import { login, chat, listModels } from '@memohome/cli'
import * as auth from '@memohome/cli/core'
import type { User, Model } from '@memohome/cli/types'
// 使用
await memohome.login({ username: 'admin', password: 'password' })
const response = await memohome.chat({ message: 'Hello' })
const models = await memohome.listModels()
```
## 包导出
`package.json` 中的导出配置:
```json
{
"exports": {
".": "./src/index.ts", // 主入口,导出所有 core 功能
"./core": "./src/core/index.ts", // Core 层
"./types": "./src/types/index.ts", // 类型定义
"./utils": "./src/utils/index.ts" // 工具函数
},
"bin": {
"memohome": "./src/cli/index.ts" // CLI 入口
}
}
```
## 设计原则
1. **关注点分离**: CLI UI 和业务逻辑完全分离
2. **可测试性**: Core 层可以独立测试,无需模拟 CLI 环境
3. **可复用性**: Core 层可在不同环境使用(CLI、Web、Desktop 等)
4. **类型安全**: 完整的 TypeScript 类型定义
5. **错误处理**: 统一的错误处理机制
## 迁移指南
如果你之前使用旧版本的代码,迁移方式:
### 旧代码(CLI 层调用)
```typescript
// 这些只能在 CLI 中使用,有 UI 输出
```
### 新代码(Core 层调用)
```typescript
// 可以在任何地方使用,无 UI 依赖
import { login, chat } from '@memohome/cli'
try {
await login({ username: 'admin', password: 'password' })
const response = await chat({ message: 'Hello' })
console.log(response)
} catch (error) {
console.error('Error:', error.message)
}
```
## 开发指南
### 添加新功能
1.`src/core/` 中添加新的核心功能模块
2.`src/core/index.ts` 中导出
3.`src/cli/commands/` 中添加对应的 CLI 命令
4.`src/cli/index.ts` 中注册命令
### 测试
Core 层可以直接测试:
```typescript
import { login, chat } from '../src/core'
test('login should work', async () => {
const result = await login({ username: 'test', password: 'test' })
expect(result.success).toBe(true)
})
```
## 注意事项
- Core 层函数通过 `throw Error` 返回错误,调用者需要处理
- CLI 层负责美化错误信息和用户反馈
- 配置文件位于 `~/.memohome/config.json`
- API 客户端使用 Eden Treaty
+506
View File
@@ -0,0 +1,506 @@
# MemoHome CLI 使用示例
## 作为 CLI 使用
### 认证
```bash
# 登录
memohome auth login -u admin -p password
# 查看当前用户
memohome auth whoami
# 登出
memohome auth logout
# 配置 API URL
memohome auth config -s http://localhost:7002
```
### AI 对话
```bash
# 单次对话
memohome agent chat "今天天气怎么样?"
# 交互模式
memohome agent interactive
# 指定语言和上下文时间
memohome agent chat "Hello" -l English -t 30
```
### 模型管理
```bash
# 列出所有模型
memohome model list
# 创建模型配置
memohome model create \
-n "GPT-4" \
-m "gpt-4" \
-u "https://api.openai.com/v1" \
-k "sk-xxx" \
-c openai \
-t chat
# 查看默认模型
memohome model defaults
# 获取模型详情
memohome model get <model-id>
# 删除模型
memohome model delete <model-id>
```
### 用户管理
```bash
# 列出所有用户
memohome user list
# 创建用户
memohome user create -u newuser -p password -r user
# 查看用户
memohome user get <user-id>
# 更新密码
memohome user update-password <user-id> -p newpassword
# 删除用户
memohome user delete <user-id>
```
### 记忆管理
```bash
# 搜索记忆
memohome memory search "关键词" -l 10
# 添加记忆
memohome memory add "重要的事情"
# 查看消息历史
memohome memory messages -p 1 -l 20
# 按日期过滤消息
memohome memory filter -s 2024-01-01T00:00:00Z -e 2024-12-31T23:59:59Z
```
### 定时任务
```bash
# 列出所有任务
memohome schedule list
# 创建任务
memohome schedule create -t "每日报告" -c "0 9 * * *" -e
# 查看任务详情
memohome schedule get <schedule-id>
# 更新任务
memohome schedule update <schedule-id> -t "新标题" --enabled true
# 切换任务状态
memohome schedule toggle <schedule-id>
# 删除任务
memohome schedule delete <schedule-id>
```
### 配置管理
```bash
# 查看配置
memohome config get
# 设置配置
memohome config set --language Chinese --max-context-time 60 --chat-model <model-id>
# 交互式配置向导
memohome config setup
```
### 调试工具
```bash
# 测试 API 连接
memohome debug ping
```
## 作为库使用
### 基础使用
```typescript
import * as memohome from '@memohome/cli'
async function main() {
try {
// 登录
const loginResult = await memohome.login({
username: 'admin',
password: 'password'
})
console.log('登录成功:', loginResult.user?.username)
// 获取当前用户
const user = await memohome.getCurrentUser()
console.log('当前用户:', user.username)
// AI 对话
const response = await memohome.chat({
message: 'Hello',
language: 'Chinese'
})
console.log('AI 回复:', response)
// 列出模型
const models = await memohome.listModels()
console.log('模型数量:', models.length)
// 登出
memohome.logout()
} catch (error) {
console.error('错误:', error.message)
}
}
main()
```
### 流式对话
```typescript
import { chatStream } from '@memohome/cli'
async function streamChat() {
await chatStream(
{
message: '讲一个故事',
language: 'Chinese'
},
async (event) => {
if (event.type === 'text-delta') {
process.stdout.write(event.text || '')
} else if (event.type === 'tool-call') {
console.log(`\n[使用工具: ${event.toolName}]`)
} else if (event.type === 'error') {
console.error('\n错误:', event.error)
} else if (event.type === 'done') {
console.log('\n完成')
}
}
)
}
streamChat()
```
### 模型管理
```typescript
import { createModel, listModels, getDefaultModels } from '@memohome/cli'
async function manageModels() {
// 创建 Chat 模型
const chatModel = await createModel({
name: 'GPT-4',
modelId: 'gpt-4',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'sk-xxx',
clientType: 'openai',
type: 'chat'
})
console.log('创建的模型:', chatModel.id)
// 创建 Embedding 模型
const embeddingModel = await createModel({
name: 'Text Embedding',
modelId: 'text-embedding-3-small',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'sk-xxx',
clientType: 'openai',
type: 'embedding',
dimensions: 1536
})
console.log('Embedding 模型:', embeddingModel.id)
// 列出所有模型
const models = await listModels()
models.forEach(item => {
console.log(`- ${item.model.name} (${item.model.type})`)
})
// 获取默认模型
const defaults = await getDefaultModels()
console.log('默认 Chat 模型:', defaults.chat?.name)
console.log('默认 Embedding 模型:', defaults.embedding?.name)
}
manageModels()
```
### 记忆管理
```typescript
import { searchMemory, addMemory, getMessages } from '@memohome/cli'
async function manageMemory() {
// 添加记忆
await addMemory({ content: '今天学习了 TypeScript' })
// 搜索记忆
const memories = await searchMemory({
query: 'TypeScript',
limit: 5
})
memories.forEach(memory => {
console.log(`相似度: ${(memory.similarity! * 100).toFixed(2)}%`)
console.log(`内容: ${memory.content}`)
console.log('---')
})
// 获取消息历史
const result = await getMessages({ page: 1, limit: 20 })
console.log(`总消息数: ${result.pagination.total}`)
result.messages.forEach(msg => {
console.log(`${msg.role}: ${msg.content}`)
})
}
manageMemory()
```
### 用户管理
```typescript
import { createUser, listUsers, updateUserPassword } from '@memohome/cli'
async function manageUsers() {
// 创建用户
const newUser = await createUser({
username: 'testuser',
password: 'password123',
role: 'user'
})
console.log('创建的用户:', newUser.username)
// 列出所有用户
const users = await listUsers()
console.log('用户数量:', users.length)
// 更新密码
await updateUserPassword({
userId: newUser.id,
password: 'newpassword123'
})
console.log('密码已更新')
}
manageUsers()
```
### 定时任务管理
```typescript
import { createSchedule, listSchedules, toggleSchedule } from '@memohome/cli'
async function manageSchedules() {
// 创建定时任务
const schedule = await createSchedule({
title: '每日报告',
description: '生成每日工作报告',
cronExpression: '0 9 * * *',
enabled: true
})
console.log('创建的任务:', schedule.title)
// 列出所有任务
const schedules = await listSchedules()
schedules.forEach(s => {
console.log(`${s.title}: ${s.cronExpression} (${s.enabled ? '启用' : '禁用'})`)
})
// 切换任务状态
const newStatus = await toggleSchedule(schedule.id)
console.log('任务状态:', newStatus ? '启用' : '禁用')
}
manageSchedules()
```
### 配置管理
```typescript
import { getSettings, updateSettings, setConfig } from '@memohome/cli'
async function manageConfig() {
// 设置 API URL
setConfig('http://localhost:7002')
// 获取用户设置
const settings = await getSettings()
console.log('当前语言:', settings.language)
console.log('上下文时间:', settings.maxContextLoadTime)
// 更新设置
await updateSettings({
language: 'English',
maxContextLoadTime: 120,
defaultChatModel: 'model-id-xxx'
})
console.log('设置已更新')
}
manageConfig()
```
### 错误处理
```typescript
import { login, chat } from '@memohome/cli'
async function handleErrors() {
try {
await login({
username: 'wrong',
password: 'wrong'
})
} catch (error) {
if (error instanceof Error) {
console.error('登录失败:', error.message)
}
}
try {
// 未登录时调用需要认证的 API
await chat({ message: 'Hello' })
} catch (error) {
if (error instanceof Error) {
console.error('需要先登录:', error.message)
}
}
}
handleErrors()
```
### 类型安全
```typescript
import type { User, Model, Memory, Schedule } from '@memohome/cli/types'
import { listUsers, listModels } from '@memohome/cli'
async function withTypes() {
// 类型自动推导
const users: User[] = await listUsers()
const models = await listModels()
// 使用类型
users.forEach((user: User) => {
console.log(`${user.username} (${user.role})`)
})
models.forEach((item) => {
const model: Model = item.model
console.log(`${model.name}: ${model.type}`)
})
}
withTypes()
```
## Web 应用集成示例
```typescript
// api/memohome.ts
import * as memohome from '@memohome/cli'
export class MemoHomeService {
async login(username: string, password: string) {
try {
return await memohome.login({ username, password })
} catch (error) {
throw new Error('登录失败')
}
}
async chat(message: string, onChunk: (text: string) => void) {
await memohome.chatStream(
{ message },
async (event) => {
if (event.type === 'text-delta' && event.text) {
onChunk(event.text)
}
}
)
}
async getModels() {
return await memohome.listModels()
}
}
// 在 React 组件中使用
import { useState } from 'react'
import { MemoHomeService } from './api/memohome'
function ChatComponent() {
const [message, setMessage] = useState('')
const [response, setResponse] = useState('')
const service = new MemoHomeService()
const handleChat = async () => {
setResponse('')
await service.chat(message, (chunk) => {
setResponse(prev => prev + chunk)
})
}
return (
<div>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleChat}></button>
<div>{response}</div>
</div>
)
}
```
## 测试示例
```typescript
import { describe, test, expect, beforeAll } from 'bun:test'
import { login, logout, chat, createModel } from '@memohome/cli'
describe('MemoHome Core API', () => {
beforeAll(async () => {
// 测试前登录
await login({ username: 'test', password: 'test' })
})
test('chat should return response', async () => {
const response = await chat({ message: 'Hello' })
expect(response).toBeDefined()
expect(typeof response).toBe('string')
})
test('createModel should work', async () => {
const model = await createModel({
name: 'Test Model',
modelId: 'test-model',
baseUrl: 'https://api.test.com',
apiKey: 'test-key',
clientType: 'openai',
type: 'chat'
})
expect(model.id).toBeDefined()
expect(model.name).toBe('Test Model')
})
test('should throw error when not logged in', async () => {
logout()
await expect(chat({ message: 'Hello' })).rejects.toThrow()
})
})
```
+10 -6
View File
@@ -1,16 +1,20 @@
{
"name": "@memohome/cli",
"name": "@memohome/client",
"version": "1.0.0",
"description": "Command line interface for MemoHome API",
"description": "Command line interface and core API for MemoHome",
"exports": {
".": "./src/index.ts"
".": "./src/index.ts",
"./core": "./src/core/index.ts",
"./types": "./src/types/index.ts",
"./utils": "./src/utils/index.ts",
"./cli": "./src/cli/index.ts"
},
"bin": {
"memohome": "./src/index.ts"
"memohome": "./src/cli/index.ts"
},
"scripts": {
"start": "bun run src/index.ts",
"dev": "bun run --watch src/index.ts"
"start": "bun run src/cli/index.ts",
"dev": "bun run --watch src/cli/index.ts"
},
"dependencies": {
"@elysiajs/eden": "^1.4.6",
+131
View File
@@ -0,0 +1,131 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import * as agentCore from '../../core/agent'
import { requireAuth } from '../../core/client'
export async function startInteractiveMode(options: { maxContextTime?: string; language?: string } = {}) {
try {
requireAuth()
console.log(chalk.green.bold('🤖 MemoHome Agent Interactive Mode'))
console.log(chalk.dim('Type /exit or /quit to exit, type /help for help\n'))
const { createInterface } = await import('readline')
const rl = createInterface({
input: process.stdin,
output: process.stdout,
prompt: chalk.blue('You: '),
})
rl.prompt()
rl.on('line', async (line: string) => {
const input = line.trim()
if (input === '/exit' || input === '/quit') {
console.log(chalk.yellow('Goodbye! 👋'))
rl.close()
process.exit(0)
return
}
if (input === '/help') {
console.log(chalk.green('\nAvailable commands:'))
console.log(chalk.dim(' /exit, /quit - Exit interactive mode'))
console.log(chalk.dim(' /help - Show help information\n'))
rl.prompt()
return
}
if (!input) {
rl.prompt()
return
}
try {
console.log(chalk.green('Agent: '))
await agentCore.chatStream(
{
message: input,
maxContextLoadTime: parseInt(options.maxContextTime || '60'),
language: options.language || 'Chinese',
},
async (event) => {
if (event.type === 'text-delta' && event.text) {
process.stdout.write(event.text)
} else if (event.type === 'tool-call') {
console.log(chalk.dim(`\n[🔧 ${event.toolName}]`))
} else if (event.type === 'error') {
console.error(chalk.red('\n❌'), event.error)
} else if (event.type === 'done') {
console.log('\n')
rl.prompt()
}
}
)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
rl.prompt()
}
})
rl.on('close', () => {
console.log(chalk.yellow('\nGoodbye! 👋'))
process.exit(0)
})
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
}
export function agentCommands(program: Command) {
program
.command('chat <message>')
.description('Chat with AI Agent')
.option('-t, --max-context-time <minutes>', 'Context load time (minutes)', '60')
.option('-l, --language <language>', 'Response language', 'Chinese')
.action(async (message, options) => {
try {
requireAuth()
console.log(chalk.blue('🤖 Agent: '))
await agentCore.chatStream(
{
message,
maxContextLoadTime: parseInt(options.maxContextTime),
language: options.language,
},
async (event) => {
if (event.type === 'text-delta' && event.text) {
process.stdout.write(event.text)
} else if (event.type === 'tool-call') {
console.log(chalk.dim(`\n[🔧 Using tool: ${event.toolName}]`))
} else if (event.type === 'error') {
console.error(chalk.red('\n❌ Error:'), event.error)
} else if (event.type === 'done') {
console.log('\n')
}
}
)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
program
.command('interactive')
.alias('i')
.description('Enter interactive conversation mode')
.option('-t, --max-context-time <minutes>', 'Context load time (minutes)', '60')
.option('-l, --language <language>', 'Response language', 'Chinese')
.action(async (options) => {
await startInteractiveMode(options)
})
}
@@ -2,9 +2,8 @@ import type { Command } from 'commander'
import chalk from 'chalk'
import inquirer from 'inquirer'
import ora from 'ora'
import { createClient } from '../client'
import { setToken, clearToken, getToken, getApiUrl, setApiUrl } from '../config'
import { formatError } from '../utils'
import * as authCore from '../../core/auth'
import { formatError } from '../../utils'
export function authCommands(program: Command) {
program
@@ -38,28 +37,15 @@ export function authCommands(program: Command) {
}
const spinner = ora('Logging in...').start()
const client = createClient()
const response = await client.auth.login.post({
username,
password,
})
if (response.error) {
spinner.fail(chalk.red('Login failed'))
console.error(chalk.red(formatError(response.error.value)))
process.exit(1)
}
const data = response.data as { success?: boolean; data?: { token?: string; user?: { username: string; role: string } } } | null
if (data?.success && data?.data?.token && data?.data?.user) {
setToken(data.data.token)
try {
const result = await authCore.login({ username, password })
spinner.succeed(chalk.green('Login successful!'))
console.log(chalk.blue(`User: ${data.data.user.username}`))
console.log(chalk.blue(`Role: ${data.data.user.role}`))
} else {
console.log(chalk.blue(`User: ${result.user?.username}`))
console.log(chalk.blue(`Role: ${result.user?.role}`))
} catch (error) {
spinner.fail(chalk.red('Login failed'))
console.error(chalk.red('Invalid response format'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
} catch (error) {
@@ -73,13 +59,12 @@ export function authCommands(program: Command) {
.command('logout')
.description('Logout current user')
.action(() => {
const token = getToken()
if (!token) {
if (!authCore.isLoggedIn()) {
console.log(chalk.yellow('Not currently logged in'))
return
}
clearToken()
authCore.logout()
console.log(chalk.green('✓ Logged out'))
})
@@ -88,32 +73,24 @@ export function authCommands(program: Command) {
.description('View current logged in user')
.action(async () => {
try {
const token = getToken()
if (!token) {
if (!authCore.isLoggedIn()) {
console.log(chalk.yellow('Not currently logged in'))
console.log(chalk.dim('Use "memohome auth login" to login'))
return
}
const spinner = ora('Fetching user information...').start()
const client = createClient()
const response = await client.auth.me.get()
if (response.error) {
spinner.fail(chalk.red('Failed to fetch user information'))
console.error(chalk.red(formatError(response.error.value)))
process.exit(1)
}
const data = response.data as { success?: boolean; data?: { username: string; role: string; id: string } } | null
if (data?.success && data?.data) {
try {
const user = await authCore.getCurrentUser()
spinner.succeed(chalk.green('Logged in'))
console.log(chalk.blue(`Username: ${data.data.username}`))
console.log(chalk.blue(`Role: ${data.data.role}`))
console.log(chalk.blue(`User ID: ${data.data.id}`))
} else {
console.log(chalk.blue(`Username: ${user.username}`))
console.log(chalk.blue(`Role: ${user.role}`))
console.log(chalk.blue(`User ID: ${user.id}`))
} catch (error) {
spinner.fail(chalk.red('Failed to fetch user information'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
@@ -129,14 +106,13 @@ export function authCommands(program: Command) {
.action((options) => {
if (options.set) {
const url = options.set
setApiUrl(url)
authCore.setConfig(url)
console.log(chalk.green(`✓ API URL set to: ${url}`))
} else {
const apiUrl = getApiUrl()
const token = getToken()
const config = authCore.getConfig()
console.log(chalk.blue('Current configuration:'))
console.log(chalk.dim(`API URL: ${apiUrl}`))
console.log(chalk.dim(`Logged in: ${token ? 'Yes' : 'No'}`))
console.log(chalk.dim(`API URL: ${config.apiUrl}`))
console.log(chalk.dim(`Logged in: ${config.loggedIn ? 'Yes' : 'No'}`))
}
})
}
@@ -2,7 +2,8 @@ import type { Command } from 'commander'
import chalk from 'chalk'
import inquirer from 'inquirer'
import ora from 'ora'
import { createClient, requireAuth } from '../client'
import * as settingsCore from '../../core/settings'
import { formatError } from '../../utils'
export function configCommands(program: Command) {
program
@@ -10,21 +11,10 @@ export function configCommands(program: Command) {
.description('Get current user settings')
.action(async () => {
try {
requireAuth()
const spinner = ora('Fetching settings...').start()
const client = createClient()
const response = await client.settings.get()
if (response.error) {
spinner.fail(chalk.red('Failed to fetch settings'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
const settings = data.data
try {
const settings = await settingsCore.getSettings()
spinner.succeed(chalk.green('Current Settings'))
console.log()
console.log(chalk.blue('🎯 Agent Configuration:'))
@@ -40,9 +30,14 @@ export function configCommands(program: Command) {
console.log(chalk.dim(` User ID: ${settings.userId}`))
console.log(chalk.dim(` Created At: ${new Date(settings.createdAt).toLocaleString('en-US')}`))
console.log(chalk.dim(` Updated At: ${new Date(settings.updatedAt).toLocaleString('en-US')}`))
} catch (error) {
spinner.fail(chalk.red('Failed to fetch settings'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -57,9 +52,13 @@ export function configCommands(program: Command) {
.option('--embedding-model <id>', 'Default embedding model ID')
.action(async (options) => {
try {
requireAuth()
const updates: any = {}
const updates: {
language?: string
maxContextLoadTime?: number
defaultChatModel?: string
defaultSummaryModel?: string
defaultEmbeddingModel?: string
} = {}
if (options.language) updates.language = options.language
if (options.maxContextTime)
@@ -81,27 +80,23 @@ export function configCommands(program: Command) {
}
const spinner = ora('Updating settings...').start()
const client = createClient()
const response = await client.settings.put(updates)
if (response.error) {
spinner.fail(chalk.red('Failed to update settings'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success) {
try {
await settingsCore.updateSettings(updates)
spinner.succeed(chalk.green('Settings updated'))
console.log()
console.log(chalk.blue('Updated settings:'))
Object.entries(updates).forEach(([key, value]) => {
console.log(chalk.dim(` ${key}: ${value}`))
})
} catch (error) {
spinner.fail(chalk.red('Failed to update settings'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -111,8 +106,6 @@ export function configCommands(program: Command) {
.description('Interactive settings wizard')
.action(async () => {
try {
requireAuth()
console.log(chalk.green.bold('\n🎨 Settings Wizard\n'))
const answers = await inquirer.prompt([
@@ -153,27 +146,32 @@ export function configCommands(program: Command) {
])
// Filter out empty values
const updates: any = {}
const updates: {
language?: string
maxContextLoadTime?: number
defaultChatModel?: string
defaultSummaryModel?: string
defaultEmbeddingModel?: string
} = {}
Object.entries(answers).forEach(([key, value]) => {
if (value) {
updates[key] = value
updates[key as keyof typeof updates] = value as never
}
})
const spinner = ora('Saving settings...').start()
const client = createClient()
const response = await client.settings.put(updates)
if (response.error) {
try {
await settingsCore.updateSettings(updates)
spinner.succeed(chalk.green('Settings saved'))
} catch (error) {
spinner.fail(chalk.red('Failed to save settings'))
console.error(chalk.red(response.error.value))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
spinner.succeed(chalk.green('Settings saved'))
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
+40
View File
@@ -0,0 +1,40 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import ora from 'ora'
import * as debugCore from '../../core/debug'
export function debugCommands(program: Command) {
program
.command('ping')
.description('Test API server connection')
.action(async () => {
const info = debugCore.getConnectionInfo()
console.log(chalk.blue('Connection Info:'))
console.log(chalk.dim(` API URL: ${info.apiUrl}`))
console.log(chalk.dim(` Token: ${info.hasToken ? 'Set' : 'Not set'}`))
console.log()
const spinner = ora('Connecting...').start()
const result = await debugCore.ping()
if (result.success) {
spinner.succeed(chalk.green('Connection successful!'))
if (result.message) {
console.log(chalk.dim('Response:'), result.message)
}
} else {
spinner.fail(chalk.red('Connection failed'))
if (result.error) {
if (result.error.includes('timeout')) {
console.error(chalk.yellow('Connection timeout (5 seconds)'))
console.error(chalk.dim('Please check if the API server is running'))
} else {
console.error(chalk.red('Error:'), result.error)
}
}
}
})
}
@@ -1,8 +1,8 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import ora from 'ora'
import { table } from 'table'
import { createClient, requireAuth } from '../client'
import * as memoryCore from '../../core/memory'
import { formatError } from '../../utils'
export function memoryCommands(program: Command) {
program
@@ -11,41 +11,35 @@ export function memoryCommands(program: Command) {
.option('-l, --limit <limit>', 'Number of results to return', '10')
.action(async (query, options) => {
try {
requireAuth()
const spinner = ora('Searching memories...').start()
const client = createClient()
const response = await client.memory.search.get({
query: {
q: query,
try {
const results = await memoryCore.searchMemory({
query,
limit: parseInt(options.limit),
},
})
})
if (response.error) {
spinner.fail(chalk.red('Search failed'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
spinner.succeed(chalk.green(`Found ${results.length} memories`))
const data = response.data as any
if (data?.success && data?.data) {
spinner.succeed(chalk.green(`Found ${data.data.length} memories`))
if (data.data.length === 0) {
if (results.length === 0) {
console.log(chalk.yellow('No related memories found'))
return
}
data.data.forEach((item: any, index: number) => {
results.forEach((item, index) => {
console.log()
console.log(chalk.blue(`[${index + 1}] Similarity: ${(item.similarity * 100).toFixed(2)}%`))
console.log(chalk.blue(`[${index + 1}] Similarity: ${((item.similarity || 0) * 100).toFixed(2)}%`))
console.log(chalk.dim(`Time: ${new Date(item.timestamp).toLocaleString('en-US')}`))
console.log(chalk.white(item.content))
})
} catch (error) {
spinner.fail(chalk.red('Search failed'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -55,26 +49,19 @@ export function memoryCommands(program: Command) {
.description('Add memory')
.action(async (content) => {
try {
requireAuth()
const spinner = ora('Adding memory...').start()
const client = createClient()
const response = await client.memory.post({
content,
})
if (response.error) {
try {
await memoryCore.addMemory({ content })
spinner.succeed(chalk.green('Memory added'))
} catch (error) {
spinner.fail(chalk.red('Failed to add memory'))
console.error(chalk.red(response.error.value))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
const data = response.data as any
if (data?.success) {
spinner.succeed(chalk.green('Memory added'))
}
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -87,26 +74,15 @@ export function memoryCommands(program: Command) {
.option('-l, --limit <limit>', 'Items per page', '20')
.action(async (options) => {
try {
requireAuth()
const spinner = ora('Fetching message history...').start()
const client = createClient()
const response = await client.memory.message.get({
query: {
try {
const result = await memoryCore.getMessages({
page: parseInt(options.page),
limit: parseInt(options.limit),
},
})
})
if (response.error) {
spinner.fail(chalk.red('Failed to fetch messages'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
const { messages, pagination } = data.data
const { messages, pagination } = result
spinner.succeed(chalk.green(`Message History (Page ${pagination.page}/${pagination.totalPages})`))
if (messages.length === 0) {
@@ -116,7 +92,7 @@ export function memoryCommands(program: Command) {
console.log(chalk.dim(`\nTotal: ${pagination.total} messages\n`))
messages.forEach((msg: any) => {
messages.forEach((msg) => {
const roleColor = msg.role === 'user' ? chalk.blue : chalk.green
const roleIcon = msg.role === 'user' ? '👤' : '🤖'
console.log(roleColor(`${roleIcon} ${msg.role.toUpperCase()}`))
@@ -124,9 +100,14 @@ export function memoryCommands(program: Command) {
console.log(chalk.white(msg.content))
console.log()
})
} catch (error) {
spinner.fail(chalk.red('Failed to fetch messages'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -138,8 +119,6 @@ export function memoryCommands(program: Command) {
.option('-e, --end <date>', 'End date (ISO 8601)')
.action(async (options) => {
try {
requireAuth()
if (!options.start || !options.end) {
console.error(chalk.red('Please provide start and end dates'))
console.log(chalk.dim('Example: memohome memory filter -s 2024-01-01T00:00:00Z -e 2024-12-31T23:59:59Z'))
@@ -147,33 +126,23 @@ export function memoryCommands(program: Command) {
}
const spinner = ora('Filtering messages...').start()
const client = createClient()
const response = await client.memory.message.filter.get({
query: {
try {
const messages = await memoryCore.filterMessages({
startDate: options.start,
endDate: options.end,
},
})
})
if (response.error) {
spinner.fail(chalk.red('Failed to filter messages'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
spinner.succeed(chalk.green(`Found ${messages.length} messages`))
const data = response.data as any
if (data?.success && data?.data) {
spinner.succeed(chalk.green(`Found ${data.data.length} messages`))
if (data.data.length === 0) {
if (messages.length === 0) {
console.log(chalk.yellow('No messages found'))
return
}
console.log()
data.data.forEach((msg: any) => {
messages.forEach((msg) => {
const roleColor = msg.role === 'user' ? chalk.blue : chalk.green
const roleIcon = msg.role === 'user' ? '👤' : '🤖'
console.log(roleColor(`${roleIcon} ${msg.role.toUpperCase()}`))
@@ -181,9 +150,14 @@ export function memoryCommands(program: Command) {
console.log(chalk.white(msg.content))
console.log()
})
} catch (error) {
spinner.fail(chalk.red('Failed to filter messages'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
+270
View File
@@ -0,0 +1,270 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import inquirer from 'inquirer'
import ora from 'ora'
import { table } from 'table'
import * as modelCore from '../../core/model'
import { formatError } from '../../utils'
import { getApiUrl } from '../../core/config'
export function modelCommands(program: Command) {
program
.command('list')
.description('List all model configurations')
.action(async () => {
const spinner = ora('Fetching model list...').start()
try {
const models = await modelCore.listModels()
spinner.succeed(chalk.green('Model List'))
if (models.length === 0) {
console.log(chalk.yellow('No model configurations found'))
return
}
const tableData = [
['ID', 'Name', 'Model ID', 'Type', 'Client'],
...models.map((item) => [
item.id.substring(0, 8) + '...',
item.model.name || '-',
item.model.modelId,
item.model.type === 'embedding' ? chalk.yellow('embedding') : chalk.blue('chat'),
item.model.clientType,
]),
]
console.log(table(tableData))
} catch (error) {
spinner.fail(chalk.red('Operation failed'))
if (error instanceof Error) {
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
console.error(chalk.red('Connection timeout, please check:'))
console.error(chalk.yellow(' 1. Is the API server running?'))
console.error(chalk.yellow(' 2. Is the API URL correct?'))
console.error(chalk.dim(` Current config: ${getApiUrl()}`))
} else {
console.error(chalk.red('Error:'), error.message)
}
} else {
console.error(chalk.red('Error:'), String(error))
}
process.exit(1)
}
})
program
.command('create')
.description('Create model configuration')
.option('-n, --name <name>', 'Model name')
.option('-m, --model-id <modelId>', 'Model ID')
.option('-u, --base-url <baseUrl>', 'API Base URL')
.option('-k, --api-key <apiKey>', 'API Key')
.option('-c, --client-type <clientType>', 'Client type (openai/anthropic/google)')
.option('-t, --type <type>', 'Model type (chat/embedding)', 'chat')
.option('-d, --dimensions <dimensions>', 'Embedding dimensions (required for embedding type)')
.action(async (options) => {
const spinner = ora('Creating model configuration...').start()
try {
let { name, modelId, baseUrl, apiKey, clientType, type, dimensions } = options
if (!name || !modelId || !baseUrl || !apiKey || !clientType) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Model name:',
when: !name,
},
{
type: 'input',
name: 'modelId',
message: 'Model ID (e.g., gpt-4 or text-embedding-3-small):',
when: !modelId,
},
{
type: 'input',
name: 'baseUrl',
message: 'API Base URL:',
default: 'https://api.openai.com/v1',
when: !baseUrl,
},
{
type: 'password',
name: 'apiKey',
message: 'API Key:',
when: !apiKey,
mask: '*',
},
{
type: 'list',
name: 'clientType',
message: 'Client type:',
choices: ['openai', 'anthropic', 'google'],
default: 'openai',
when: !clientType,
},
{
type: 'list',
name: 'type',
message: 'Model type:',
choices: ['chat', 'embedding'],
default: 'chat',
when: !type,
},
])
name = name || answers.name
modelId = modelId || answers.modelId
baseUrl = baseUrl || answers.baseUrl
apiKey = apiKey || answers.apiKey
clientType = clientType || answers.clientType
type = type || answers.type
}
// If embedding type, dimensions is required
if (type === 'embedding' && !dimensions) {
const answer = await inquirer.prompt([
{
type: 'number',
name: 'dimensions',
message: 'Embedding dimensions (e.g., 1536):',
validate: (value: number) => {
if (value > 0) return true
return 'Dimensions must be a positive integer'
},
},
])
dimensions = answer.dimensions
}
spinner.text = 'Creating model configuration...'
const model = await modelCore.createModel({
name,
modelId,
baseUrl,
apiKey,
clientType,
type: type as 'chat' | 'embedding',
dimensions: dimensions ? (typeof dimensions === 'number' ? dimensions : parseInt(dimensions)) : undefined,
})
spinner.succeed(chalk.green('Model configuration created successfully'))
console.log(chalk.blue(`Name: ${model.name}`))
console.log(chalk.blue(`Model ID: ${model.modelId}`))
console.log(chalk.blue(`Type: ${model.type || 'chat'}`))
if (model.type === 'embedding' && model.dimensions) {
console.log(chalk.blue(`Dimensions: ${model.dimensions}`))
}
console.log(chalk.blue(`ID: ${model.id}`))
} catch (error) {
spinner.fail(chalk.red('Operation failed'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
})
program
.command('delete <id>')
.description('Delete model configuration')
.action(async (id) => {
try {
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: chalk.yellow(`Are you sure you want to delete model configuration ${id}?`),
default: false,
},
])
if (!confirm) {
console.log(chalk.yellow('Cancelled'))
return
}
const spinner = ora('Deleting model configuration...').start()
await modelCore.deleteModel(id)
spinner.succeed(chalk.green('Model configuration deleted'))
} catch (error) {
console.error(chalk.red(formatError(error)))
process.exit(1)
}
})
program
.command('get <id>')
.description('Get model configuration details')
.action(async (id) => {
const spinner = ora('Fetching model configuration...').start()
try {
const model = await modelCore.getModel(id)
spinner.succeed(chalk.green('Model Configuration'))
console.log(chalk.blue(`ID: ${model.id}`))
console.log(chalk.blue(`Name: ${model.name}`))
console.log(chalk.blue(`Model ID: ${model.modelId}`))
console.log(chalk.blue(`Type: ${model.type || 'chat'}`))
if (model.type === 'embedding' && model.dimensions) {
console.log(chalk.blue(`Dimensions: ${model.dimensions}`))
}
console.log(chalk.blue(`Base URL: ${model.baseUrl}`))
console.log(chalk.blue(`Client Type: ${model.clientType}`))
console.log(chalk.blue(`Created At: ${new Date(model.createdAt).toLocaleString('en-US')}`))
} catch (error) {
spinner.fail(chalk.red('Operation failed'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
})
program
.command('defaults')
.description('View default model configurations')
.action(async () => {
const spinner = ora('Fetching default model configurations...').start()
try {
const defaults = await modelCore.getDefaultModels()
spinner.stop()
console.log(chalk.green.bold('Default Model Configurations:'))
console.log()
// Chat Model
if (defaults.chat) {
console.log(chalk.blue('💬 Chat Model:'))
console.log(chalk.dim(` Name: ${defaults.chat.name}`))
console.log(chalk.dim(` Model ID: ${defaults.chat.modelId}`))
console.log(chalk.dim(` ID: ${defaults.chat.id}`))
} else {
console.log(chalk.yellow('💬 Chat Model: Not configured'))
}
console.log()
// Summary Model
if (defaults.summary) {
console.log(chalk.blue('📝 Summary Model:'))
console.log(chalk.dim(` Name: ${defaults.summary.name}`))
console.log(chalk.dim(` Model ID: ${defaults.summary.modelId}`))
console.log(chalk.dim(` ID: ${defaults.summary.id}`))
} else {
console.log(chalk.yellow('📝 Summary Model: Not configured'))
}
console.log()
// Embedding Model
if (defaults.embedding) {
console.log(chalk.blue('🔍 Embedding Model:'))
console.log(chalk.dim(` Name: ${defaults.embedding.name}`))
console.log(chalk.dim(` Model ID: ${defaults.embedding.modelId}`))
console.log(chalk.dim(` ID: ${defaults.embedding.id}`))
} else {
console.log(chalk.yellow('🔍 Embedding Model: Not configured'))
}
} catch (error) {
spinner.fail(chalk.red('Operation failed'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
})
}
@@ -3,7 +3,8 @@ import chalk from 'chalk'
import inquirer from 'inquirer'
import ora from 'ora'
import { table } from 'table'
import { createClient, requireAuth } from '../client'
import * as scheduleCore from '../../core/schedule'
import { formatError } from '../../utils'
export function scheduleCommands(program: Command) {
program
@@ -11,23 +12,12 @@ export function scheduleCommands(program: Command) {
.description('List all scheduled tasks')
.action(async () => {
try {
requireAuth()
const spinner = ora('Fetching scheduled tasks list...').start()
const client = createClient()
const response = await client.schedule.get()
if (response.error) {
spinner.fail(chalk.red('Failed to fetch scheduled tasks list'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
try {
const schedules = await scheduleCore.listSchedules()
spinner.succeed(chalk.green('Scheduled Tasks List'))
const schedules = data.data
if (schedules.length === 0) {
console.log(chalk.yellow('No scheduled tasks'))
return
@@ -35,7 +25,7 @@ export function scheduleCommands(program: Command) {
const tableData = [
['ID', 'Title', 'Cron', 'Enabled', 'Created At'],
...schedules.map((schedule: any) => [
...schedules.map((schedule) => [
schedule.id.substring(0, 8) + '...',
schedule.title,
schedule.cronExpression,
@@ -45,9 +35,14 @@ export function scheduleCommands(program: Command) {
]
console.log(table(tableData))
} catch (error) {
spinner.fail(chalk.red('Failed to fetch scheduled tasks list'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -61,8 +56,6 @@ export function scheduleCommands(program: Command) {
.option('-e, --enabled', 'Enable task', false)
.action(async (options) => {
try {
requireAuth()
let { title, description, cron, enabled } = options
if (!title || !cron) {
@@ -101,35 +94,27 @@ export function scheduleCommands(program: Command) {
}
const spinner = ora('Creating scheduled task...').start()
const client = createClient()
const payload: any = {
title,
cronExpression: cron,
enabled,
}
try {
const schedule = await scheduleCore.createSchedule({
title,
description,
cronExpression: cron,
enabled,
})
if (description) {
payload.description = description
}
const response = await client.schedule.post(payload)
if (response.error) {
spinner.succeed(chalk.green('Scheduled task created successfully'))
console.log(chalk.blue(`Title: ${schedule.title}`))
console.log(chalk.blue(`Cron: ${schedule.cronExpression}`))
console.log(chalk.blue(`ID: ${schedule.id}`))
} catch (error) {
spinner.fail(chalk.red('Failed to create scheduled task'))
console.error(chalk.red(response.error.value))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
spinner.succeed(chalk.green('Scheduled task created successfully'))
console.log(chalk.blue(`Title: ${data.data.title}`))
console.log(chalk.blue(`Cron: ${data.data.cronExpression}`))
console.log(chalk.blue(`ID: ${data.data.id}`))
}
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -139,21 +124,10 @@ export function scheduleCommands(program: Command) {
.description('Get scheduled task details')
.action(async (id) => {
try {
requireAuth()
const spinner = ora('Fetching scheduled task details...').start()
const client = createClient()
const response = await client.schedule({ id }).get()
if (response.error) {
spinner.fail(chalk.red('Failed to fetch scheduled task'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
const schedule = data.data
try {
const schedule = await scheduleCore.getSchedule(id)
spinner.succeed(chalk.green('Scheduled Task Details'))
console.log(chalk.blue(`ID: ${schedule.id}`))
console.log(chalk.blue(`Title: ${schedule.title}`))
@@ -170,9 +144,14 @@ export function scheduleCommands(program: Command) {
console.log(
chalk.blue(`Updated At: ${new Date(schedule.updatedAt).toLocaleString('en-US')}`)
)
} catch (error) {
spinner.fail(chalk.red('Failed to fetch scheduled task'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -186,9 +165,12 @@ export function scheduleCommands(program: Command) {
.option('-e, --enabled <boolean>', 'Enable task (true/false)')
.action(async (id, options) => {
try {
requireAuth()
const updates: any = {}
const updates: {
title?: string
description?: string
cronExpression?: string
enabled?: boolean
} = {}
if (options.title) updates.title = options.title
if (options.description) updates.description = options.description
@@ -203,19 +185,18 @@ export function scheduleCommands(program: Command) {
}
const spinner = ora('Updating scheduled task...').start()
const client = createClient()
const response = await client.schedule({ id }).put(updates)
if (response.error) {
try {
await scheduleCore.updateSchedule(id, updates)
spinner.succeed(chalk.green('Scheduled task updated'))
} catch (error) {
spinner.fail(chalk.red('Failed to update scheduled task'))
console.error(chalk.red(response.error.value))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
spinner.succeed(chalk.green('Scheduled task updated'))
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -225,8 +206,6 @@ export function scheduleCommands(program: Command) {
.description('Delete scheduled task')
.action(async (id) => {
try {
requireAuth()
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
@@ -242,19 +221,18 @@ export function scheduleCommands(program: Command) {
}
const spinner = ora('Deleting scheduled task...').start()
const client = createClient()
const response = await client.schedule({ id }).delete()
if (response.error) {
try {
await scheduleCore.deleteSchedule(id)
spinner.succeed(chalk.green('Scheduled task deleted'))
} catch (error) {
spinner.fail(chalk.red('Failed to delete scheduled task'))
console.error(chalk.red(response.error.value))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
spinner.succeed(chalk.green('Scheduled task deleted'))
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -264,40 +242,21 @@ export function scheduleCommands(program: Command) {
.description('Toggle scheduled task enabled status')
.action(async (id) => {
try {
requireAuth()
const spinner = ora('Toggling task status...').start()
const client = createClient()
// First get current status
const getResponse = await client.schedule({ id }).get()
if (getResponse.error) {
spinner.fail(chalk.red('Failed to fetch task'))
console.error(chalk.red(getResponse.error.value))
try {
const newStatus = await scheduleCore.toggleSchedule(id)
spinner.succeed(
chalk.green(`Task ${newStatus ? 'enabled' : 'disabled'}`)
)
} catch (error) {
spinner.fail(chalk.red('Failed to toggle task'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
const getData = getResponse.data as any
if (getData?.success && getData?.data) {
const currentEnabled = getData.data.enabled
// Update status
const updateResponse = await client.schedule({ id }).put({
enabled: !currentEnabled,
})
if (updateResponse.error) {
spinner.fail(chalk.red('Failed to update task'))
console.error(chalk.red(updateResponse.error.value))
process.exit(1)
}
spinner.succeed(
chalk.green(`Task ${!currentEnabled ? 'enabled' : 'disabled'}`)
)
}
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -3,7 +3,8 @@ import chalk from 'chalk'
import inquirer from 'inquirer'
import ora from 'ora'
import { table } from 'table'
import { createClient, requireAuth } from '../client'
import * as userCore from '../../core/user'
import { formatError } from '../../utils'
export function userCommands(program: Command) {
program
@@ -11,23 +12,12 @@ export function userCommands(program: Command) {
.description('List all users (requires admin privileges)')
.action(async () => {
try {
requireAuth()
const spinner = ora('Fetching user list...').start()
const client = createClient()
const response = await client.user.get()
if (response.error) {
spinner.fail(chalk.red('Failed to fetch user list'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
try {
const users = await userCore.listUsers()
spinner.succeed(chalk.green('User List'))
const users = data.data
if (users.length === 0) {
console.log(chalk.yellow('No users'))
return
@@ -35,7 +25,7 @@ export function userCommands(program: Command) {
const tableData = [
['ID', 'Username', 'Role', 'Created At'],
...users.map((user: any) => [
...users.map((user) => [
user.id,
user.username,
user.role === 'admin' ? chalk.red('Admin') : chalk.blue('User'),
@@ -44,9 +34,14 @@ export function userCommands(program: Command) {
]
console.log(table(tableData))
} catch (error) {
spinner.fail(chalk.red('Failed to fetch user list'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -59,8 +54,6 @@ export function userCommands(program: Command) {
.option('-r, --role <role>', 'Role (user/admin)', 'user')
.action(async (options) => {
try {
requireAuth()
let username = options.username
let password = options.password
let role = options.role
@@ -95,29 +88,21 @@ export function userCommands(program: Command) {
}
const spinner = ora('Creating user...').start()
const client = createClient()
const response = await client.user.post({
username,
password,
role,
})
if (response.error) {
try {
const user = await userCore.createUser({ username, password, role })
spinner.succeed(chalk.green('User created successfully'))
console.log(chalk.blue(`Username: ${user.username}`))
console.log(chalk.blue(`Role: ${user.role}`))
console.log(chalk.blue(`ID: ${user.id}`))
} catch (error) {
spinner.fail(chalk.red('Failed to create user'))
console.error(chalk.red(response.error.value))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
spinner.succeed(chalk.green('User created successfully'))
console.log(chalk.blue(`Username: ${data.data.username}`))
console.log(chalk.blue(`Role: ${data.data.role}`))
console.log(chalk.blue(`ID: ${data.data.id}`))
}
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -127,8 +112,6 @@ export function userCommands(program: Command) {
.description('Delete user (requires admin privileges)')
.action(async (id) => {
try {
requireAuth()
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
@@ -144,19 +127,18 @@ export function userCommands(program: Command) {
}
const spinner = ora('Deleting user...').start()
const client = createClient()
const response = await client.user({ id }).delete()
if (response.error) {
try {
await userCore.deleteUser(id)
spinner.succeed(chalk.green('User deleted'))
} catch (error) {
spinner.fail(chalk.red('Failed to delete user'))
console.error(chalk.red(response.error.value))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
spinner.succeed(chalk.green('User deleted'))
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -166,29 +148,23 @@ export function userCommands(program: Command) {
.description('Get user details')
.action(async (id) => {
try {
requireAuth()
const spinner = ora('Fetching user information...').start()
const client = createClient()
const response = await client.user({ id }).get()
if (response.error) {
spinner.fail(chalk.red('Failed to fetch user information'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as any
if (data?.success && data?.data) {
const user = data.data
try {
const user = await userCore.getUser(id)
spinner.succeed(chalk.green('User Information'))
console.log(chalk.blue(`ID: ${user.id}`))
console.log(chalk.blue(`Username: ${user.username}`))
console.log(chalk.blue(`Role: ${user.role}`))
console.log(chalk.blue(`Created At: ${new Date(user.createdAt).toLocaleString('en-US')}`))
} catch (error) {
spinner.fail(chalk.red('Failed to fetch user information'))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
@@ -199,8 +175,6 @@ export function userCommands(program: Command) {
.option('-p, --password <password>', 'New password')
.action(async (id, options) => {
try {
requireAuth()
let password = options.password
if (!password) {
@@ -216,21 +190,18 @@ export function userCommands(program: Command) {
}
const spinner = ora('Updating password...').start()
const client = createClient()
const response = await client.user({ id }).password.patch({
password,
})
if (response.error) {
try {
await userCore.updateUserPassword({ userId: id, password })
spinner.succeed(chalk.green('Password updated'))
} catch (error) {
spinner.fail(chalk.red('Failed to update password'))
console.error(chalk.red(response.error.value))
console.error(chalk.red(formatError(error)))
process.exit(1)
}
spinner.succeed(chalk.green('Password updated'))
} catch (error: any) {
console.error(chalk.red('Error:'), error.message)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bun
import { Command } from 'commander'
import chalk from 'chalk'
import { authCommands } from './commands/auth'
import { userCommands } from './commands/user'
import { modelCommands } from './commands/model'
import { agentCommands, startInteractiveMode } from './commands/agent'
import { memoryCommands } from './commands/memory'
import { configCommands } from './commands/config'
import { scheduleCommands } from './commands/schedule'
import { debugCommands } from './commands/debug'
const program = new Command()
program
.name('memohome')
.description(chalk.bold.blue('🏠 MemoHome Agent'))
.version('1.0.0')
// Authentication commands
const auth = program.command('auth').description('User authentication management')
authCommands(auth)
// User management commands
const user = program.command('user').description('User management (requires admin privileges)')
userCommands(user)
// Model management commands
const model = program.command('model').description('AI model configuration management')
modelCommands(model)
// Agent conversation commands
const agent = program.command('agent').description('Chat with AI Agent')
agentCommands(agent)
// Memory management commands
const memory = program.command('memory').description('Memory management')
memoryCommands(memory)
// Config management commands
const config = program.command('config').description('User configuration management')
configCommands(config)
// Schedule management commands
const schedule = program.command('schedule').description('Schedule management')
scheduleCommands(schedule)
// Debug commands
const debug = program.command('debug').description('Debug tools')
debugCommands(debug)
// If no arguments provided, start interactive mode
if (process.argv.length === 2) {
startInteractiveMode().catch((error) => {
console.error('Failed to start interactive mode:', error)
process.exit(1)
})
} else {
program.parse()
}
-227
View File
@@ -1,227 +0,0 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import { requireAuth, getApiUrl, getToken } from '../client'
export async function startInteractiveMode(options: { maxContextTime?: string; language?: string } = {}) {
try {
requireAuth()
const token = getToken()!
const apiUrl = getApiUrl()
console.log(chalk.green.bold('🤖 MemoHome Agent Interactive Mode'))
console.log(chalk.dim('Type /exit or /quit to exit, type /help for help\n'))
const { createInterface } = await import('readline')
const rl = createInterface({
input: process.stdin,
output: process.stdout,
prompt: chalk.blue('You: '),
})
rl.prompt()
rl.on('line', async (line: string) => {
const input = line.trim()
if (input === '/exit' || input === '/quit') {
console.log(chalk.yellow('Goodbye! 👋'))
rl.close()
process.exit(0)
return
}
if (input === '/help') {
console.log(chalk.green('\nAvailable commands:'))
console.log(chalk.dim(' /exit, /quit - Exit interactive mode'))
console.log(chalk.dim(' /help - Show help information\n'))
rl.prompt()
return
}
if (!input) {
rl.prompt()
return
}
try {
console.log(chalk.green('Agent: '))
const response = await fetch(`${apiUrl}/agent/stream`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: input,
maxContextLoadTime: parseInt(options.maxContextTime || '60'),
language: options.language || 'Chinese',
}),
})
if (!response.ok) {
const errorData = await response.json() as { error?: string }
console.error(chalk.red('Chat failed:'), errorData.error || 'Unknown error')
rl.prompt()
return
}
const reader = response.body?.getReader()
const decoder = new TextDecoder()
if (!reader) {
throw new Error('Unable to read response stream')
}
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
buffer += chunk
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim()
if (data === '[DONE]') {
console.log('\n')
rl.prompt()
return
}
try {
const event = JSON.parse(data)
if (event.type === 'text-delta' && event.text) {
process.stdout.write(event.text)
} else if (event.type === 'tool-call') {
console.log(chalk.dim(`\n[🔧 ${event.toolName}]`))
} else if (event.type === 'error') {
console.error(chalk.red('\n❌'), event.error)
}
} catch {
// Skip unparseable JSON
}
}
}
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
rl.prompt()
}
})
rl.on('close', () => {
console.log(chalk.yellow('\nGoodbye! 👋'))
process.exit(0)
})
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
}
export function agentCommands(program: Command) {
program
.command('chat <message>')
.description('Chat with AI Agent')
.option('-t, --max-context-time <minutes>', 'Context load time (minutes)', '60')
.option('-l, --language <language>', 'Response language', 'Chinese')
.action(async (message, options) => {
try {
requireAuth()
const token = getToken()!
const apiUrl = getApiUrl()
console.log(chalk.blue('🤖 Agent: '))
const response = await fetch(`${apiUrl}/agent/stream`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
message,
maxContextLoadTime: parseInt(options.maxContextTime),
language: options.language,
}),
})
if (!response.ok) {
const errorData = await response.json() as { error?: string }
console.error(chalk.red('Chat failed:'), errorData.error || 'Unknown error')
process.exit(1)
}
const reader = response.body?.getReader()
const decoder = new TextDecoder()
if (!reader) {
throw new Error('Unable to read response stream')
}
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
buffer += chunk
// Process line by line
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim()
if (data === '[DONE]') {
console.log('\n')
return
}
try {
const event = JSON.parse(data)
if (event.type === 'text-delta' && event.text) {
process.stdout.write(event.text)
} else if (event.type === 'tool-call') {
console.log(chalk.dim(`\n[🔧 Using tool: ${event.toolName}]`))
} else if (event.type === 'error') {
console.error(chalk.red('\n❌ Error:'), event.error)
}
} catch {
// Skip unparseable JSON
}
}
}
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
console.error(chalk.red('Error:'), message)
process.exit(1)
}
})
program
.command('interactive')
.alias('i')
.description('Enter interactive conversation mode')
.option('-t, --max-context-time <minutes>', 'Context load time (minutes)', '60')
.option('-l, --language <language>', 'Response language', 'Chinese')
.action(async (options) => {
await startInteractiveMode(options)
})
}
-55
View File
@@ -1,55 +0,0 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import ora from 'ora'
import { getApiUrl, getToken } from '../client'
export function debugCommands(program: Command) {
program
.command('ping')
.description('Test API server connection')
.action(async () => {
const apiUrl = getApiUrl()
const token = getToken()
console.log(chalk.blue('Connection Info:'))
console.log(chalk.dim(` API URL: ${apiUrl}`))
console.log(chalk.dim(` Token: ${token ? 'Set' : 'Not set'}`))
console.log()
const spinner = ora('Connecting...').start()
try {
// Try direct fetch
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000)
const response = await fetch(`${apiUrl}/`, {
signal: controller.signal,
headers: token ? {
'Authorization': `Bearer ${token}`
} : {}
})
clearTimeout(timeoutId)
if (response.ok) {
spinner.succeed(chalk.green('Connection successful!'))
const text = await response.text()
console.log(chalk.dim('Response:'), text.substring(0, 100))
} else {
spinner.fail(chalk.red(`Connection failed: HTTP ${response.status}`))
}
} catch (error) {
spinner.fail(chalk.red('Connection failed'))
if (error instanceof Error) {
if (error.name === 'AbortError') {
console.error(chalk.yellow('Connection timeout (5 seconds)'))
console.error(chalk.dim('Please check if the API server is running'))
} else {
console.error(chalk.red('Error:'), error.message)
}
}
}
})
}
-400
View File
@@ -1,400 +0,0 @@
import type { Command } from 'commander'
import chalk from 'chalk'
import inquirer from 'inquirer'
import ora from 'ora'
import { table } from 'table'
import { createClient, requireAuth } from '../client'
import type { ApiResponse, Model } from '../types'
import { formatError } from '../utils'
export function modelCommands(program: Command) {
program
.command('list')
.description('List all model configurations')
.action(async () => {
const spinner = ora('Fetching model list...').start()
try {
requireAuth()
const client = createClient()
const response = await client.model.get()
if (response.error) {
spinner.fail(chalk.red('Failed to fetch model list'))
console.error(chalk.red(formatError(response.error.value)))
process.exit(1)
}
// API response format: { success, items, pagination }
const data = response.data as { success?: boolean; items?: Model[]; pagination?: unknown } | null
if (data?.success && data?.items) {
spinner.succeed(chalk.green('Model List'))
const models = data.items
if (models.length === 0) {
console.log(chalk.yellow('No model configurations found'))
return
}
const tableData = [
['ID', 'Name', 'Model ID', 'Type', 'Client'],
...models.map((item: unknown) => {
const modelItem = item as { id: string; model: Model }
return [
modelItem.id.substring(0, 8) + '...',
modelItem.model.name || '-',
modelItem.model.modelId,
modelItem.model.type === 'embedding' ? chalk.yellow('embedding') : chalk.blue('chat'),
modelItem.model.clientType,
]
}),
]
console.log(table(tableData))
}
} catch (error) {
spinner.fail(chalk.red('Operation failed'))
if (error instanceof Error) {
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
const { getApiUrl: getUrl } = await import('../config')
console.error(chalk.red('Connection timeout, please check:'))
console.error(chalk.yellow(' 1. Is the API server running?'))
console.error(chalk.yellow(' 2. Is the API URL correct?'))
console.error(chalk.dim(` Current config: ${getUrl()}`))
} else {
console.error(chalk.red('Error:'), error.message)
}
} else {
console.error(chalk.red('Error:'), String(error))
}
process.exit(1)
}
})
program
.command('create')
.description('Create model configuration')
.option('-n, --name <name>', 'Model name')
.option('-m, --model-id <modelId>', 'Model ID')
.option('-u, --base-url <baseUrl>', 'API Base URL')
.option('-k, --api-key <apiKey>', 'API Key')
.option('-c, --client-type <clientType>', 'Client type (openai/anthropic/google)')
.option('-t, --type <type>', 'Model type (chat/embedding)', 'chat')
.option('-d, --dimensions <dimensions>', 'Embedding dimensions (required for embedding type)')
.action(async (options) => {
const spinner = ora('Creating model configuration...').start()
try {
requireAuth()
let { name, modelId, baseUrl, apiKey, clientType, type, dimensions } = options
if (!name || !modelId || !baseUrl || !apiKey || !clientType) {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Model name:',
when: !name,
},
{
type: 'input',
name: 'modelId',
message: 'Model ID (e.g., gpt-4 or text-embedding-3-small):',
when: !modelId,
},
{
type: 'input',
name: 'baseUrl',
message: 'API Base URL:',
default: 'https://api.openai.com/v1',
when: !baseUrl,
},
{
type: 'password',
name: 'apiKey',
message: 'API Key:',
when: !apiKey,
mask: '*',
},
{
type: 'list',
name: 'clientType',
message: 'Client type:',
choices: ['openai', 'anthropic', 'google'],
default: 'openai',
when: !clientType,
},
{
type: 'list',
name: 'type',
message: 'Model type:',
choices: ['chat', 'embedding'],
default: 'chat',
when: !type,
},
])
name = name || answers.name
modelId = modelId || answers.modelId
baseUrl = baseUrl || answers.baseUrl
apiKey = apiKey || answers.apiKey
clientType = clientType || answers.clientType
type = type || answers.type
}
// If embedding type, dimensions is required
if (type === 'embedding' && !dimensions) {
const answer = await inquirer.prompt([
{
type: 'number',
name: 'dimensions',
message: 'Embedding dimensions (e.g., 1536):',
validate: (value: number) => {
if (value > 0) return true
return 'Dimensions must be a positive integer'
},
},
])
dimensions = answer.dimensions
}
spinner.text = 'Creating model configuration...'
const client = createClient()
const payload: Record<string, unknown> = {
name,
modelId,
baseUrl,
apiKey,
clientType,
type,
}
// If embedding type, add dimensions
if (type === 'embedding') {
if (!dimensions) {
console.error(chalk.red('Embedding models require dimensions to be specified'))
process.exit(1)
}
payload.dimensions = typeof dimensions === 'number' ? dimensions : parseInt(dimensions)
}
const response = await client.model.post(payload)
if (response.error) {
spinner.fail(chalk.red('Failed to create model configuration'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as ApiResponse<Model> | null
if (data?.success && data?.data) {
spinner.succeed(chalk.green('Model configuration created successfully'))
console.log(chalk.blue(`Name: ${data.data.name}`))
console.log(chalk.blue(`Model ID: ${data.data.modelId}`))
console.log(chalk.blue(`Type: ${data.data.type || 'chat'}`))
if (data.data.type === 'embedding' && data.data.dimensions) {
console.log(chalk.blue(`Dimensions: ${data.data.dimensions}`))
}
console.log(chalk.blue(`ID: ${data.data.id}`))
}
} catch (error) {
spinner.fail(chalk.red('Operation failed'))
if (error instanceof Error) {
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
const { getApiUrl: getUrl } = await import('../config')
console.error(chalk.red('Connection timeout, please check:'))
console.error(chalk.yellow(' 1. Is the API server running?'))
console.error(chalk.yellow(' 2. Is the API URL correct?'))
console.error(chalk.dim(` Current config: ${getUrl()}`))
} else {
console.error(chalk.red('Error:'), error.message)
}
} else {
console.error(chalk.red('Error:'), String(error))
}
process.exit(1)
}
})
program
.command('delete <id>')
.description('Delete model configuration')
.action(async (id) => {
let spinner: ReturnType<typeof ora> | undefined
try {
requireAuth()
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: chalk.yellow(`Are you sure you want to delete model configuration ${id}?`),
default: false,
},
])
if (!confirm) {
console.log(chalk.yellow('Cancelled'))
return
}
spinner = ora('Deleting model configuration...').start()
const client = createClient()
const response = await client.model({ id }).delete()
if (response.error) {
spinner.fail(chalk.red('Failed to delete model configuration'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
if (spinner) spinner.succeed(chalk.green('Model configuration deleted'))
} catch (error) {
if (spinner) spinner.fail(chalk.red('Operation failed'))
if (error instanceof Error) {
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
const { getApiUrl: getUrl } = await import('../config')
console.error(chalk.red('Connection timeout, please check:'))
console.error(chalk.yellow(' 1. Is the API server running?'))
console.error(chalk.yellow(' 2. Is the API URL correct?'))
console.error(chalk.dim(` Current config: ${getUrl()}`))
} else {
console.error(chalk.red('Error:'), error.message)
}
} else {
console.error(chalk.red('Error:'), String(error))
}
process.exit(1)
}
})
program
.command('get <id>')
.description('Get model configuration details')
.action(async (id) => {
const spinner = ora('Fetching model configuration...').start()
try {
requireAuth()
const client = createClient()
const response = await client.model({ id }).get()
if (response.error) {
spinner.fail(chalk.red('Failed to fetch model configuration'))
console.error(chalk.red(response.error.value))
process.exit(1)
}
const data = response.data as ApiResponse<Model> | null
if (data?.success && data?.data) {
const model = data.data
spinner.succeed(chalk.green('Model Configuration'))
console.log(chalk.blue(`ID: ${model.id}`))
console.log(chalk.blue(`Name: ${model.name}`))
console.log(chalk.blue(`Model ID: ${model.modelId}`))
console.log(chalk.blue(`Type: ${model.type || 'chat'}`))
if (model.type === 'embedding' && model.dimensions) {
console.log(chalk.blue(`Dimensions: ${model.dimensions}`))
}
console.log(chalk.blue(`Base URL: ${model.baseUrl}`))
console.log(chalk.blue(`Client Type: ${model.clientType}`))
console.log(chalk.blue(`Created At: ${new Date(model.createdAt).toLocaleString('en-US')}`))
}
} catch (error) {
spinner.fail(chalk.red('Operation failed'))
if (error instanceof Error) {
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
const { getApiUrl: getUrl } = await import('../config')
console.error(chalk.red('Connection timeout, please check:'))
console.error(chalk.yellow(' 1. Is the API server running?'))
console.error(chalk.yellow(' 2. Is the API URL correct?'))
console.error(chalk.dim(` Current config: ${getUrl()}`))
} else {
console.error(chalk.red('Error:'), error.message)
}
} else {
console.error(chalk.red('Error:'), String(error))
}
process.exit(1)
}
})
program
.command('defaults')
.description('View default model configurations')
.action(async () => {
const spinner = ora('Fetching default model configurations...').start()
try {
requireAuth()
const client = createClient()
const [chatRes, summaryRes, embeddingRes] = await Promise.all([
client.model.chat.default.get(),
client.model.summary.default.get(),
client.model.embedding.default.get(),
])
spinner.stop()
console.log(chalk.green.bold('Default Model Configurations:'))
console.log()
// Chat Model
const chatData = chatRes.data as ApiResponse<Model> | null
if (chatData?.success && chatData.data) {
const model = chatData.data
console.log(chalk.blue('💬 Chat Model:'))
console.log(chalk.dim(` Name: ${model.name}`))
console.log(chalk.dim(` Model ID: ${model.modelId}`))
console.log(chalk.dim(` ID: ${model.id}`))
} else {
console.log(chalk.yellow('💬 Chat Model: Not configured'))
}
console.log()
// Summary Model
const summaryData = summaryRes.data as ApiResponse<Model> | null
if (summaryData?.success && summaryData.data) {
const model = summaryData.data
console.log(chalk.blue('📝 Summary Model:'))
console.log(chalk.dim(` Name: ${model.name}`))
console.log(chalk.dim(` Model ID: ${model.modelId}`))
console.log(chalk.dim(` ID: ${model.id}`))
} else {
console.log(chalk.yellow('📝 Summary Model: Not configured'))
}
console.log()
// Embedding Model
const embeddingData = embeddingRes.data as ApiResponse<Model> | null
if (embeddingData?.success && embeddingData.data) {
const model = embeddingData.data
console.log(chalk.blue('🔍 Embedding Model:'))
console.log(chalk.dim(` Name: ${model.name}`))
console.log(chalk.dim(` Model ID: ${model.modelId}`))
console.log(chalk.dim(` ID: ${model.id}`))
} else {
console.log(chalk.yellow('🔍 Embedding Model: Not configured'))
}
} catch (error) {
spinner.fail(chalk.red('Operation failed'))
if (error instanceof Error) {
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
const { getApiUrl: getUrl } = await import('../config')
console.error(chalk.red('Connection timeout, please check:'))
console.error(chalk.yellow(' 1. Is the API server running?'))
console.error(chalk.yellow(' 2. Is the API URL correct?'))
console.error(chalk.dim(` Current config: ${getUrl()}`))
} else {
console.error(chalk.red('Error:'), error.message)
}
} else {
console.error(chalk.red('Error:'), String(error))
}
process.exit(1)
}
})
}
+109
View File
@@ -0,0 +1,109 @@
import { requireAuth, getToken, getApiUrl } from './client'
export interface ChatParams {
message: string
maxContextLoadTime?: number
language?: string
}
export interface StreamEvent {
type: 'text-delta' | 'tool-call' | 'error' | 'done'
text?: string
toolName?: string
error?: string
}
export type StreamCallback = (event: StreamEvent) => void | Promise<void>
/**
* Chat with AI Agent (streaming)
*/
export async function chatStream(
params: ChatParams,
onEvent: StreamCallback
): Promise<void> {
requireAuth()
const token = getToken()!
const apiUrl = getApiUrl()
const response = await fetch(`${apiUrl}/agent/stream`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: params.message,
maxContextLoadTime: params.maxContextLoadTime || 60,
language: params.language || 'Chinese',
}),
})
if (!response.ok) {
const errorData = await response.json() as { error?: string }
throw new Error(errorData.error || 'Chat failed')
}
const reader = response.body?.getReader()
const decoder = new TextDecoder()
if (!reader) {
throw new Error('Unable to read response stream')
}
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
buffer += chunk
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim()
if (data === '[DONE]') {
await onEvent({ type: 'done' })
return
}
try {
const event = JSON.parse(data)
if (event.type === 'text-delta' && event.text) {
await onEvent({ type: 'text-delta', text: event.text })
} else if (event.type === 'tool-call') {
await onEvent({ type: 'tool-call', toolName: event.toolName })
} else if (event.type === 'error') {
await onEvent({ type: 'error', error: event.error })
}
} catch {
// Skip unparseable JSON
}
}
}
}
}
/**
* Chat with AI Agent (non-streaming, collect full response)
*/
export async function chat(params: ChatParams): Promise<string> {
let fullResponse = ''
await chatStream(params, async (event) => {
if (event.type === 'text-delta' && event.text) {
fullResponse += event.text
} else if (event.type === 'error') {
throw new Error(event.error)
}
})
return fullResponse
}
+117
View File
@@ -0,0 +1,117 @@
import { createClient } from './client'
import { setToken, clearToken, getToken, getApiUrl, setApiUrl } from './config'
export interface LoginParams {
username: string
password: string
}
export interface LoginResult {
success: boolean
token?: string
user?: {
username: string
role: string
id: string
}
}
export interface UserInfo {
username: string
role: string
id: string
}
export interface ConfigInfo {
apiUrl: string
loggedIn: boolean
}
/**
* Login to MemoHome API
*/
export async function login(params: LoginParams): Promise<LoginResult> {
const client = createClient()
const response = await client.auth.login.post({
username: params.username,
password: params.password,
})
if (response.error) {
throw new Error(response.error.value)
}
const data = response.data as { success?: boolean; data?: { token?: string; user?: { username: string; role: string } } } | null
if (data?.success && data?.data?.token && data?.data?.user) {
setToken(data.data.token)
return {
success: true,
token: data.data.token,
user: data.data.user as UserInfo,
}
}
throw new Error('Invalid response format')
}
/**
* Logout current user
*/
export function logout(): void {
clearToken()
}
/**
* Check if user is logged in
*/
export function isLoggedIn(): boolean {
return getToken() !== null
}
/**
* Get current logged in user info
*/
export async function getCurrentUser(): Promise<UserInfo> {
const token = getToken()
if (!token) {
throw new Error('Not logged in')
}
const client = createClient()
const response = await client.auth.me.get()
if (response.error) {
throw new Error(response.error.value)
}
const data = response.data as { success?: boolean; data?: UserInfo } | null
if (data?.success && data?.data) {
return data.data
}
throw new Error('Failed to fetch user information')
}
/**
* Get current API configuration
*/
export function getConfig(): ConfigInfo {
return {
apiUrl: getApiUrl(),
loggedIn: isLoggedIn(),
}
}
/**
* Set API URL
*/
export function setConfig(apiUrl: string): void {
setApiUrl(apiUrl)
}
// Re-export config functions for convenience
export { getToken, getApiUrl, setToken, clearToken, setApiUrl }
@@ -32,7 +32,6 @@ export function loadConfig(): Config {
const data = readFileSync(CONFIG_FILE, 'utf-8')
return { ...DEFAULT_CONFIG, ...JSON.parse(data) }
} catch {
console.error('Failed to load config, using defaults')
return DEFAULT_CONFIG
}
}
+76
View File
@@ -0,0 +1,76 @@
import { getApiUrl, getToken } from './config'
export interface PingResult {
success: boolean
status?: number
message?: string
error?: string
}
/**
* Test API server connection
*/
export async function ping(): Promise<PingResult> {
const apiUrl = getApiUrl()
const token = getToken()
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000)
const response = await fetch(`${apiUrl}/`, {
signal: controller.signal,
headers: token ? {
'Authorization': `Bearer ${token}`
} : {}
})
clearTimeout(timeoutId)
if (response.ok) {
const text = await response.text()
return {
success: true,
status: response.status,
message: text.substring(0, 100),
}
} else {
return {
success: false,
status: response.status,
error: `HTTP ${response.status}`,
}
}
} catch (error) {
if (error instanceof Error) {
if (error.name === 'AbortError') {
return {
success: false,
error: 'Connection timeout (5 seconds)',
}
}
return {
success: false,
error: error.message,
}
}
return {
success: false,
error: 'Unknown error',
}
}
}
/**
* Get connection info
*/
export function getConnectionInfo(): {
apiUrl: string
hasToken: boolean
} {
return {
apiUrl: getApiUrl(),
hasToken: getToken() !== null,
}
}
+109
View File
@@ -0,0 +1,109 @@
/**
* MemoHome Core API
*
* This module provides core functionality that can be used by CLI and other applications.
* All functions are independent of CLI-specific UI concerns (no chalk, ora, inquirer, etc.)
*/
// Auth
export {
login,
logout,
isLoggedIn,
getCurrentUser,
getConfig,
setConfig,
getToken,
getApiUrl,
setToken,
clearToken,
setApiUrl,
type LoginParams,
type LoginResult,
type UserInfo,
type ConfigInfo,
} from './auth'
// User
export {
listUsers,
createUser,
getUser,
deleteUser,
updateUserPassword,
type CreateUserParams,
type UpdatePasswordParams,
} from './user'
// Model
export {
listModels,
createModel,
getModel,
deleteModel,
getDefaultModels,
type CreateModelParams,
type ModelListItem,
} from './model'
// Agent
export {
chat,
chatStream,
type ChatParams,
type StreamEvent,
type StreamCallback,
} from './agent'
// Memory
export {
searchMemory,
addMemory,
getMessages,
filterMessages,
type SearchMemoryParams,
type AddMemoryParams,
type GetMessagesParams,
type FilterMessagesParams,
} from './memory'
// Schedule
export {
listSchedules,
createSchedule,
getSchedule,
updateSchedule,
deleteSchedule,
toggleSchedule,
type CreateScheduleParams,
type UpdateScheduleParams,
} from './schedule'
// Settings
export {
getSettings,
updateSettings,
type UpdateSettingsParams,
} from './settings'
// Debug
export {
ping,
getConnectionInfo,
type PingResult,
} from './debug'
// Config
export {
loadConfig,
saveConfig,
ensureConfigDir,
type Config,
} from './config'
// Client
export {
createClient,
requireAuth,
} from './client'
+125
View File
@@ -0,0 +1,125 @@
import { createClient, requireAuth } from './client'
import type { Memory, Message, MessageListResponse } from '../types'
export interface SearchMemoryParams {
query: string
limit?: number
}
export interface AddMemoryParams {
content: string
}
export interface GetMessagesParams {
page?: number
limit?: number
}
export interface FilterMessagesParams {
startDate: string
endDate: string
}
/**
* Search memories
*/
export async function searchMemory(params: SearchMemoryParams): Promise<Memory[]> {
requireAuth()
const client = createClient()
const response = await client.memory.search.get({
query: {
q: params.query,
limit: params.limit || 10,
},
})
if (response.error) {
throw new Error(response.error.value)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = response.data as any
if (data?.success && data?.data) {
return data.data
}
throw new Error('Search failed')
}
/**
* Add memory
*/
export async function addMemory(params: AddMemoryParams): Promise<void> {
requireAuth()
const client = createClient()
const response = await client.memory.post({
content: params.content,
})
if (response.error) {
throw new Error(response.error.value)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = response.data as any
if (!data?.success) {
throw new Error('Failed to add memory')
}
}
/**
* Get message history
*/
export async function getMessages(params: GetMessagesParams = {}): Promise<MessageListResponse> {
requireAuth()
const client = createClient()
const response = await client.memory.message.get({
query: {
page: params.page || 1,
limit: params.limit || 20,
},
})
if (response.error) {
throw new Error(response.error.value)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = response.data as any
if (data?.success && data?.data) {
return data.data
}
throw new Error('Failed to fetch messages')
}
/**
* Filter messages by date range
*/
export async function filterMessages(params: FilterMessagesParams): Promise<Message[]> {
requireAuth()
const client = createClient()
const response = await client.memory.message.filter.get({
query: {
startDate: params.startDate,
endDate: params.endDate,
},
})
if (response.error) {
throw new Error(response.error.value)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = response.data as any
if (data?.success && data?.data) {
return data.data
}
throw new Error('Failed to filter messages')
}
+149
View File
@@ -0,0 +1,149 @@
import { createClient, requireAuth } from './client'
import type { Model, ApiResponse } from '../types'
export interface CreateModelParams {
name: string
modelId: string
baseUrl: string
apiKey: string
clientType: string
type?: 'chat' | 'embedding'
dimensions?: number
}
export interface ModelListItem {
id: string
model: Model
}
/**
* List all models
*/
export async function listModels(): Promise<ModelListItem[]> {
requireAuth()
const client = createClient()
const response = await client.model.get()
if (response.error) {
throw new Error(response.error.value)
}
const data = response.data as { success?: boolean; items?: ModelListItem[] } | null
if (data?.success && data?.items) {
return data.items
}
throw new Error('Failed to fetch model list')
}
/**
* Create model configuration
*/
export async function createModel(params: CreateModelParams): Promise<Model> {
requireAuth()
const client = createClient()
const payload: Record<string, unknown> = {
name: params.name,
modelId: params.modelId,
baseUrl: params.baseUrl,
apiKey: params.apiKey,
clientType: params.clientType,
type: params.type || 'chat',
}
// If embedding type, add dimensions
if (params.type === 'embedding') {
if (!params.dimensions) {
throw new Error('Embedding models require dimensions to be specified')
}
payload.dimensions = params.dimensions
}
const response = await client.model.post(payload)
if (response.error) {
throw new Error(response.error.value)
}
const data = response.data as ApiResponse<Model> | null
if (data?.success && data?.data) {
return data.data
}
throw new Error('Failed to create model configuration')
}
/**
* Get model by ID
*/
export async function getModel(id: string): Promise<Model> {
requireAuth()
const client = createClient()
const response = await client.model({ id }).get()
if (response.error) {
throw new Error(response.error.value)
}
const data = response.data as ApiResponse<Model> | null
if (data?.success && data?.data) {
return data.data
}
throw new Error('Failed to fetch model configuration')
}
/**
* Delete model
*/
export async function deleteModel(id: string): Promise<void> {
requireAuth()
const client = createClient()
const response = await client.model({ id }).delete()
if (response.error) {
throw new Error(response.error.value)
}
}
/**
* Get default models
*/
export async function getDefaultModels(): Promise<{
chat?: Model
summary?: Model
embedding?: Model
}> {
requireAuth()
const client = createClient()
const [chatRes, summaryRes, embeddingRes] = await Promise.all([
client.model.chat.default.get(),
client.model.summary.default.get(),
client.model.embedding.default.get(),
])
const result: { chat?: Model; summary?: Model; embedding?: Model } = {}
const chatData = chatRes.data as ApiResponse<Model> | null
if (chatData?.success && chatData.data) {
result.chat = chatData.data
}
const summaryData = summaryRes.data as ApiResponse<Model> | null
if (summaryData?.success && summaryData.data) {
result.summary = summaryData.data
}
const embeddingData = embeddingRes.data as ApiResponse<Model> | null
if (embeddingData?.success && embeddingData.data) {
result.embedding = embeddingData.data
}
return result
}
+156
View File
@@ -0,0 +1,156 @@
import { createClient, requireAuth } from './client'
import type { Schedule } from '../types'
export interface CreateScheduleParams {
title: string
description?: string
cronExpression: string
enabled: boolean
}
export interface UpdateScheduleParams {
title?: string
description?: string
cronExpression?: string
enabled?: boolean
}
/**
* List all schedules
*/
export async function listSchedules(): Promise<Schedule[]> {
requireAuth()
const client = createClient()
const response = await client.schedule.get()
if (response.error) {
throw new Error(response.error.value)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = response.data as any
if (data?.success && data?.data) {
return data.data
}
throw new Error('Failed to fetch scheduled tasks list')
}
/**
* Create schedule
*/
export async function createSchedule(params: CreateScheduleParams): Promise<Schedule> {
requireAuth()
const client = createClient()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const payload: any = {
title: params.title,
cronExpression: params.cronExpression,
enabled: params.enabled,
}
if (params.description) {
payload.description = params.description
}
const response = await client.schedule.post(payload)
if (response.error) {
throw new Error(response.error.value)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = response.data as any
if (data?.success && data?.data) {
return data.data
}
throw new Error('Failed to create scheduled task')
}
/**
* Get schedule by ID
*/
export async function getSchedule(id: string): Promise<Schedule> {
requireAuth()
const client = createClient()
const response = await client.schedule({ id }).get()
if (response.error) {
throw new Error(response.error.value)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = response.data as any
if (data?.success && data?.data) {
return data.data
}
throw new Error('Failed to fetch scheduled task')
}
/**
* Update schedule
*/
export async function updateSchedule(id: string, params: UpdateScheduleParams): Promise<void> {
requireAuth()
const client = createClient()
const response = await client.schedule({ id }).put(params)
if (response.error) {
throw new Error(response.error.value)
}
}
/**
* Delete schedule
*/
export async function deleteSchedule(id: string): Promise<void> {
requireAuth()
const client = createClient()
const response = await client.schedule({ id }).delete()
if (response.error) {
throw new Error(response.error.value)
}
}
/**
* Toggle schedule enabled status
*/
export async function toggleSchedule(id: string): Promise<boolean> {
requireAuth()
const client = createClient()
// First get current status
const getResponse = await client.schedule({ id }).get()
if (getResponse.error) {
throw new Error(getResponse.error.value)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getData = getResponse.data as any
if (getData?.success && getData?.data) {
const currentEnabled = getData.data.enabled
// Update status
const updateResponse = await client.schedule({ id }).put({
enabled: !currentEnabled,
})
if (updateResponse.error) {
throw new Error(updateResponse.error.value)
}
return !currentEnabled
}
throw new Error('Failed to toggle task status')
}
+53
View File
@@ -0,0 +1,53 @@
import { createClient, requireAuth } from './client'
import type { Settings } from '../types'
export interface UpdateSettingsParams {
language?: string
maxContextLoadTime?: number
defaultChatModel?: string
defaultSummaryModel?: string
defaultEmbeddingModel?: string
}
/**
* Get current user settings
*/
export async function getSettings(): Promise<Settings> {
requireAuth()
const client = createClient()
const response = await client.settings.get()
if (response.error) {
throw new Error(response.error.value)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = response.data as any
if (data?.success && data?.data) {
return data.data
}
throw new Error('Failed to fetch settings')
}
/**
* Update user settings
*/
export async function updateSettings(params: UpdateSettingsParams): Promise<void> {
requireAuth()
const client = createClient()
const response = await client.settings.put(params)
if (response.error) {
throw new Error(response.error.value)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = response.data as any
if (!data?.success) {
throw new Error('Failed to update settings')
}
}
+114
View File
@@ -0,0 +1,114 @@
import { createClient, requireAuth } from './client'
import type { User } from '../types'
export interface CreateUserParams {
username: string
password: string
role: string
}
export interface UpdatePasswordParams {
userId: string
password: string
}
/**
* List all users
*/
export async function listUsers(): Promise<User[]> {
requireAuth()
const client = createClient()
const response = await client.user.get()
if (response.error) {
throw new Error(response.error.value)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = response.data as any
if (data?.success && data?.data) {
return data.data
}
throw new Error('Failed to fetch user list')
}
/**
* Create new user
*/
export async function createUser(params: CreateUserParams): Promise<User> {
requireAuth()
const client = createClient()
const response = await client.user.post({
username: params.username,
password: params.password,
role: params.role,
})
if (response.error) {
throw new Error(response.error.value)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = response.data as any
if (data?.success && data?.data) {
return data.data
}
throw new Error('Failed to create user')
}
/**
* Get user by ID
*/
export async function getUser(id: string): Promise<User> {
requireAuth()
const client = createClient()
const response = await client.user({ id }).get()
if (response.error) {
throw new Error(response.error.value)
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = response.data as any
if (data?.success && data?.data) {
return data.data
}
throw new Error('Failed to fetch user information')
}
/**
* Delete user
*/
export async function deleteUser(id: string): Promise<void> {
requireAuth()
const client = createClient()
const response = await client.user({ id }).delete()
if (response.error) {
throw new Error(response.error.value)
}
}
/**
* Update user password
*/
export async function updateUserPassword(params: UpdatePasswordParams): Promise<void> {
requireAuth()
const client = createClient()
const response = await client.user({ id: params.userId }).password.patch({
password: params.password,
})
if (response.error) {
throw new Error(response.error.value)
}
}
+33 -59
View File
@@ -1,62 +1,36 @@
#!/usr/bin/env bun
/**
* MemoHome CLI Package
*
* This package provides both:
* 1. A command-line interface (CLI) for interacting with MemoHome API
* 2. Core functionality that can be imported and used in other projects
*
* @example CLI Usage (from terminal)
* ```bash
* memohome auth login
* memohome agent chat "Hello"
* ```
*
* @example Core API Usage (from code)
* ```typescript
* import { login, chat, listModels } from '@memohome/cli'
*
* // Login
* await login({ username: 'admin', password: 'password' })
*
* // Chat with agent
* const response = await chat({ message: 'Hello' })
*
* // List models
* const models = await listModels()
* ```
*/
import { Command } from 'commander'
import chalk from 'chalk'
import { authCommands } from './commands/auth'
import { userCommands } from './commands/user'
import { modelCommands } from './commands/model'
import { agentCommands, startInteractiveMode } from './commands/agent'
import { memoryCommands } from './commands/memory'
import { configCommands } from './commands/config'
import { scheduleCommands } from './commands/schedule'
import { debugCommands } from './commands/debug'
// Export all core functionality
export * from './core'
const program = new Command()
program
.name('memohome')
.description(chalk.bold.blue('🏠 MemoHome Agent'))
.version('1.0.0')
// Authentication commands
const auth = program.command('auth').description('User authentication management')
authCommands(auth)
// User management commands
const user = program.command('user').description('User management (requires admin privileges)')
userCommands(user)
// Model management commands
const model = program.command('model').description('AI model configuration management')
modelCommands(model)
// Agent conversation commands
const agent = program.command('agent').description('Chat with AI Agent')
agentCommands(agent)
// Memory management commands
const memory = program.command('memory').description('Memory management')
memoryCommands(memory)
// Config management commands
const config = program.command('config').description('User configuration management')
configCommands(config)
// Schedule management commands
const schedule = program.command('schedule').description('Schedule management')
scheduleCommands(schedule)
// Debug commands
const debug = program.command('debug').description('Debug tools')
debugCommands(debug)
// If no arguments provided, start interactive mode
if (process.argv.length === 2) {
startInteractiveMode().catch((error) => {
console.error('Failed to start interactive mode:', error)
process.exit(1)
})
} else {
program.parse()
}
// Export types
export * from './types'
// Export utilities
export * from './utils'
@@ -1,5 +1,3 @@
import chalk from 'chalk'
/**
* Format API error information
*/
@@ -40,24 +38,3 @@ export function formatError(error: unknown): string {
return String(error)
}
/**
* Print error and exit
*/
export function exitWithError(message: string, error?: unknown): never {
console.error(chalk.red(message))
if (error) {
console.error(chalk.red(formatError(error)))
}
process.exit(1)
}
/**
* Handle Eden Treaty response errors
*/
export function handleApiError(response: { error?: { value: unknown } }, defaultMessage = 'Operation failed'): never {
if (response.error) {
exitWithError(defaultMessage, response.error.value)
}
exitWithError(defaultMessage, 'Unknown error')
}