From af9023c87bd31241eaa181a63e9b149b3845fd42 Mon Sep 17 00:00:00 2001 From: Acbox Date: Thu, 29 Jan 2026 01:37:47 +0800 Subject: [PATCH] refactor: cli --- agent/src/modules/chat.ts | 2 +- internal/chat/resolver.go | 92 +++- internal/handlers/models.go | 16 + packages/cli/src/cli/commands/agent.ts | 129 ----- packages/cli/src/cli/commands/auth.ts | 119 ----- packages/cli/src/cli/commands/config.ts | 179 ------- packages/cli/src/cli/commands/debug.ts | 40 -- packages/cli/src/cli/commands/mcp.ts | 328 ------------ packages/cli/src/cli/commands/memory.ts | 165 ------ packages/cli/src/cli/commands/model.ts | 270 ---------- packages/cli/src/cli/commands/platform.ts | 325 ------------ packages/cli/src/cli/commands/schedule.ts | 264 ---------- packages/cli/src/cli/commands/user.ts | 209 -------- packages/cli/src/cli/index.ts | 616 +++++++++++++++++++--- packages/cli/src/core/agent.ts | 163 ------ packages/cli/src/core/api.ts | 43 ++ packages/cli/src/core/auth.ts | 173 ------ packages/cli/src/core/client.ts | 102 ---- packages/cli/src/core/context.ts | 62 --- packages/cli/src/core/debug.ts | 79 --- packages/cli/src/core/index.ts | 138 +---- packages/cli/src/core/mcp.ts | 212 -------- packages/cli/src/core/memory.ts | 125 ----- packages/cli/src/core/model.ts | 150 ------ packages/cli/src/core/platform.ts | 185 ------- packages/cli/src/core/schedule.ts | 156 ------ packages/cli/src/core/settings.ts | 53 -- packages/cli/src/core/storage.ts | 52 -- packages/cli/src/core/storage/file.ts | 73 --- packages/cli/src/core/storage/index.ts | 2 - packages/cli/src/core/user.ts | 114 ---- packages/cli/src/index.ts | 38 +- packages/cli/src/types/index.ts | 1 + packages/cli/src/utils/index.ts | 41 +- packages/cli/src/utils/store.ts | 98 ++++ 35 files changed, 790 insertions(+), 4024 deletions(-) delete mode 100644 packages/cli/src/cli/commands/agent.ts delete mode 100644 packages/cli/src/cli/commands/auth.ts delete mode 100644 packages/cli/src/cli/commands/config.ts delete mode 100644 packages/cli/src/cli/commands/debug.ts delete mode 100644 packages/cli/src/cli/commands/mcp.ts delete mode 100644 packages/cli/src/cli/commands/memory.ts delete mode 100644 packages/cli/src/cli/commands/model.ts delete mode 100644 packages/cli/src/cli/commands/platform.ts delete mode 100644 packages/cli/src/cli/commands/schedule.ts delete mode 100644 packages/cli/src/cli/commands/user.ts mode change 100755 => 100644 packages/cli/src/cli/index.ts delete mode 100644 packages/cli/src/core/agent.ts create mode 100644 packages/cli/src/core/api.ts delete mode 100644 packages/cli/src/core/auth.ts delete mode 100644 packages/cli/src/core/client.ts delete mode 100644 packages/cli/src/core/context.ts delete mode 100644 packages/cli/src/core/debug.ts delete mode 100644 packages/cli/src/core/mcp.ts delete mode 100644 packages/cli/src/core/memory.ts delete mode 100644 packages/cli/src/core/model.ts delete mode 100644 packages/cli/src/core/platform.ts delete mode 100644 packages/cli/src/core/schedule.ts delete mode 100644 packages/cli/src/core/settings.ts delete mode 100644 packages/cli/src/core/storage.ts delete mode 100644 packages/cli/src/core/storage/file.ts delete mode 100644 packages/cli/src/core/storage/index.ts delete mode 100644 packages/cli/src/core/user.ts create mode 100644 packages/cli/src/utils/store.ts diff --git a/agent/src/modules/chat.ts b/agent/src/modules/chat.ts index 144b4fa8..838eac3b 100644 --- a/agent/src/modules/chat.ts +++ b/agent/src/modules/chat.ts @@ -20,7 +20,7 @@ const ChatBody = z.object({ platforms: z.array(z.string()).optional(), currentPlatform: z.string().optional(), - messages: z.array(z.object()), + messages: z.array(z.any()), query: z.string().min(1, 'Query is required'), }) diff --git a/internal/chat/resolver.go b/internal/chat/resolver.go index 70306ed3..4619157f 100644 --- a/internal/chat/resolver.go +++ b/internal/chat/resolver.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "log" "net/http" "strings" "time" @@ -275,9 +276,20 @@ func (r *Resolver) streamChat(ctx context.Context, payload agentGatewayRequest, scanner := bufio.NewScanner(resp.Body) scanner.Buffer(make([]byte, 0, 64*1024), 2*1024*1024) + currentEventType := "" + stored := false + log.Printf("chat stream started user_id=%s", userID) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) - if line == "" || !strings.HasPrefix(line, "data:") { + if line == "" { + continue + } + if strings.HasPrefix(line, "event:") { + currentEventType = strings.TrimSpace(strings.TrimPrefix(line, "event:")) + log.Printf("chat stream event=%s", currentEventType) + continue + } + if !strings.HasPrefix(line, "data:") { continue } data := strings.TrimSpace(strings.TrimPrefix(line, "data:")) @@ -286,25 +298,14 @@ func (r *Resolver) streamChat(ctx context.Context, payload agentGatewayRequest, } chunkChan <- StreamChunk([]byte(data)) - var envelope struct { - Type string `json:"type"` - Data json.RawMessage `json:"data"` - } - if err := json.Unmarshal([]byte(data), &envelope); err != nil { + if stored { continue } - if envelope.Type != "done" || len(envelope.Data) == 0 { - continue - } - var parsed agentGatewayResponse - if err := json.Unmarshal(envelope.Data, &parsed); err != nil { - continue - } - if err := r.storeHistory(ctx, userID, query, parsed.Messages); err != nil { - return err - } - if err := r.storeMemory(ctx, userID, query, parsed.Messages); err != nil { + + if handled, err := r.tryStoreFromStreamPayload(ctx, userID, query, currentEventType, data); err != nil { return err + } else if handled { + stored = true } } @@ -378,6 +379,9 @@ func (r *Resolver) storeHistory(ctx context.Context, userID, query string, respo }, User: pgUserID, }) + if err == nil { + log.Printf("history saved user_id=%s messages=%d", userID, len(messages)) + } return err } @@ -419,6 +423,60 @@ func (r *Resolver) storeMemory(ctx context.Context, userID, query string, respon return err } +func (r *Resolver) tryStoreFromStreamPayload(ctx context.Context, userID, query, eventType, data string) (bool, error) { + // Case 1: event: done + data: {messages: [...]} + if eventType == "done" { + if parsed, ok := parseGatewayResponse([]byte(data)); ok { + log.Printf("chat stream done payload messages=%d", len(parsed.Messages)) + return r.storeRound(ctx, userID, query, parsed.Messages) + } + } + + // Case 2: data: {"type":"done","data":{messages:[...]}} + var envelope struct { + Type string `json:"type"` + Data json.RawMessage `json:"data"` + } + if err := json.Unmarshal([]byte(data), &envelope); err == nil { + if envelope.Type == "done" && len(envelope.Data) > 0 { + if parsed, ok := parseGatewayResponse(envelope.Data); ok { + log.Printf("chat stream done envelope messages=%d", len(parsed.Messages)) + return r.storeRound(ctx, userID, query, parsed.Messages) + } + } + } + + // Case 3: data: {messages:[...]} without event + if parsed, ok := parseGatewayResponse([]byte(data)); ok { + log.Printf("chat stream done implicit messages=%d", len(parsed.Messages)) + return r.storeRound(ctx, userID, query, parsed.Messages) + } + return false, nil +} + +func parseGatewayResponse(payload []byte) (agentGatewayResponse, bool) { + var parsed agentGatewayResponse + if err := json.Unmarshal(payload, &parsed); err != nil { + return agentGatewayResponse{}, false + } + if len(parsed.Messages) == 0 { + return agentGatewayResponse{}, false + } + return parsed, true +} + +func (r *Resolver) storeRound(ctx context.Context, userID, query string, messages []GatewayMessage) (bool, error) { + if err := r.storeHistory(ctx, userID, query, messages); err != nil { + log.Printf("chat stream storeHistory error=%v", err) + return true, err + } + if err := r.storeMemory(ctx, userID, query, messages); err != nil { + log.Printf("chat stream storeMemory error=%v", err) + return true, err + } + return true, nil +} + func gatewayMessageToMemory(msg GatewayMessage) (string, string) { role := "assistant" if raw, ok := msg["role"].(string); ok && strings.TrimSpace(raw) != "" { diff --git a/internal/handlers/models.go b/internal/handlers/models.go index 6c654cad..cd00055f 100644 --- a/internal/handlers/models.go +++ b/internal/handlers/models.go @@ -2,6 +2,7 @@ package handlers import ( "net/http" + "net/url" "github.com/labstack/echo/v4" @@ -121,6 +122,11 @@ func (h *ModelsHandler) GetByModelID(c echo.Context) error { if modelID == "" { return echo.NewHTTPError(http.StatusBadRequest, "modelId is required") } + if decoded, err := url.PathUnescape(modelID); err == nil { + modelID = decoded + } else { + return echo.NewHTTPError(http.StatusBadRequest, "invalid modelId") + } resp, err := h.service.GetByModelID(c.Request().Context(), modelID) if err != nil { @@ -174,6 +180,11 @@ func (h *ModelsHandler) UpdateByModelID(c echo.Context) error { if modelID == "" { return echo.NewHTTPError(http.StatusBadRequest, "modelId is required") } + if decoded, err := url.PathUnescape(modelID); err == nil { + modelID = decoded + } else { + return echo.NewHTTPError(http.StatusBadRequest, "invalid modelId") + } var req models.UpdateRequest if err := c.Bind(&req); err != nil { @@ -224,6 +235,11 @@ func (h *ModelsHandler) DeleteByModelID(c echo.Context) error { if modelID == "" { return echo.NewHTTPError(http.StatusBadRequest, "modelId is required") } + if decoded, err := url.PathUnescape(modelID); err == nil { + modelID = decoded + } else { + return echo.NewHTTPError(http.StatusBadRequest, "invalid modelId") + } if err := h.service.DeleteByModelID(c.Request().Context(), modelID); err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) diff --git a/packages/cli/src/cli/commands/agent.ts b/packages/cli/src/cli/commands/agent.ts deleted file mode 100644 index 32eb12df..00000000 --- a/packages/cli/src/cli/commands/agent.ts +++ /dev/null @@ -1,129 +0,0 @@ -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('šŸ¤– Memoh 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, - 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 ') - .description('Chat with AI Agent') - .option('-t, --max-context-time ', 'Context load time (minutes)', '60') - .option('-l, --language ', 'Response language', 'Chinese') - .action(async (message, options) => { - try { - requireAuth() - console.log(chalk.blue('šŸ¤– Agent: ')) - - await agentCore.chatStream( - { - message, - 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 ', 'Context load time (minutes)', '60') - .option('-l, --language ', 'Response language', 'Chinese') - .action(async (options) => { - await startInteractiveMode(options) - }) -} - diff --git a/packages/cli/src/cli/commands/auth.ts b/packages/cli/src/cli/commands/auth.ts deleted file mode 100644 index c91b9ea7..00000000 --- a/packages/cli/src/cli/commands/auth.ts +++ /dev/null @@ -1,119 +0,0 @@ -import type { Command } from 'commander' -import chalk from 'chalk' -import inquirer from 'inquirer' -import ora from 'ora' -import * as authCore from '../../core/auth' -import { formatError } from '../../utils' - -export function authCommands(program: Command) { - program - .command('login') - .description('Login to Memoh') - .option('-u, --username ', 'Username') - .option('-p, --password ', 'Password') - .action(async (options) => { - try { - let username = options.username - let password = options.password - - if (!username || !password) { - const answers = await inquirer.prompt([ - { - type: 'input', - name: 'username', - message: 'Please enter username:', - when: !username, - }, - { - type: 'password', - name: 'password', - message: 'Please enter password:', - when: !password, - mask: '*', - }, - ]) - username = username || answers.username - password = password || answers.password - } - - const spinner = ora('Logging in...').start() - - try { - const result = await authCore.login({ username, password }) - spinner.succeed(chalk.green('Login successful!')) - 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(formatError(error))) - process.exit(1) - } - } catch (error) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Login error:'), message) - process.exit(1) - } - }) - - program - .command('logout') - .description('Logout current user') - .action(() => { - if (!authCore.isLoggedIn()) { - console.log(chalk.yellow('Not currently logged in')) - return - } - - authCore.logout() - console.log(chalk.green('āœ“ Logged out')) - }) - - program - .command('whoami') - .description('View current logged in user') - .action(async () => { - try { - if (!authCore.isLoggedIn()) { - console.log(chalk.yellow('Not currently logged in')) - console.log(chalk.dim('Use "memoh auth login" to login')) - return - } - - const spinner = ora('Fetching user information...').start() - - try { - const user = await authCore.getCurrentUser() - spinner.succeed(chalk.green('Logged in')) - 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) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('config') - .description('View or set API configuration') - .option('-s, --set ', 'Set API URL') - .action((options) => { - if (options.set) { - const url = options.set - authCore.setConfig(url) - console.log(chalk.green(`āœ“ API URL set to: ${url}`)) - } else { - const config = authCore.getConfig() - console.log(chalk.blue('Current configuration:')) - console.log(chalk.dim(`API URL: ${config.apiUrl}`)) - console.log(chalk.dim(`Logged in: ${config.loggedIn ? 'Yes' : 'No'}`)) - } - }) -} - diff --git a/packages/cli/src/cli/commands/config.ts b/packages/cli/src/cli/commands/config.ts deleted file mode 100644 index 355f1186..00000000 --- a/packages/cli/src/cli/commands/config.ts +++ /dev/null @@ -1,179 +0,0 @@ -import type { Command } from 'commander' -import chalk from 'chalk' -import inquirer from 'inquirer' -import ora from 'ora' -import * as settingsCore from '../../core/settings' -import { formatError } from '../../utils' - -export function configCommands(program: Command) { - program - .command('get') - .description('Get current user settings') - .action(async () => { - try { - const spinner = ora('Fetching settings...').start() - - try { - const settings = await settingsCore.getSettings() - spinner.succeed(chalk.green('Current Settings')) - console.log() - console.log(chalk.blue('šŸŽÆ Agent Configuration:')) - console.log(chalk.dim(` Language: ${settings.language || 'Not set'}`)) - console.log(chalk.dim(` Context Load Time: ${settings.maxContextLoadTime || 'Not set'} minutes`)) - console.log() - console.log(chalk.blue('šŸ¤– Default Models:')) - console.log(chalk.dim(` Chat Model ID: ${settings.defaultChatModel || 'Not set'}`)) - console.log(chalk.dim(` Summary Model ID: ${settings.defaultSummaryModel || 'Not set'}`)) - console.log(chalk.dim(` Embedding Model ID: ${settings.defaultEmbeddingModel || 'Not set'}`)) - console.log() - console.log(chalk.blue('šŸ“Š Other:')) - 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: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('set') - .description('Update user settings') - .option('--language ', 'Preferred language') - .option('--max-context-time ', 'Context load time (minutes)') - .option('--chat-model ', 'Default chat model ID') - .option('--summary-model ', 'Default summary model ID') - .option('--embedding-model ', 'Default embedding model ID') - .action(async (options) => { - try { - const updates: { - language?: string - maxContextLoadTime?: number - defaultChatModel?: string - defaultSummaryModel?: string - defaultEmbeddingModel?: string - } = {} - - if (options.language) updates.language = options.language - if (options.maxContextTime) - updates.maxContextLoadTime = parseInt(options.maxContextTime) - if (options.chatModel) updates.defaultChatModel = options.chatModel - if (options.summaryModel) updates.defaultSummaryModel = options.summaryModel - if (options.embeddingModel) - updates.defaultEmbeddingModel = options.embeddingModel - - if (Object.keys(updates).length === 0) { - console.log(chalk.yellow('No update parameters provided')) - console.log(chalk.dim('\nAvailable options:')) - console.log(chalk.dim(' --language ')) - console.log(chalk.dim(' --max-context-time ')) - console.log(chalk.dim(' --chat-model ')) - console.log(chalk.dim(' --summary-model ')) - console.log(chalk.dim(' --embedding-model ')) - return - } - - const spinner = ora('Updating settings...').start() - - 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: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('setup') - .description('Interactive settings wizard') - .action(async () => { - try { - console.log(chalk.green.bold('\nšŸŽØ Settings Wizard\n')) - - const answers = await inquirer.prompt([ - { - type: 'input', - name: 'language', - message: 'Preferred language:', - default: 'Chinese', - }, - { - type: 'number', - name: 'maxContextLoadTime', - message: 'Context load time (minutes):', - default: 60, - validate: (value) => { - const num = parseInt(value) - if (num < 1 || num > 1440) { - return 'Please enter a number between 1-1440' - } - return true - }, - }, - { - type: 'input', - name: 'defaultChatModel', - message: 'Default chat model ID (leave empty to skip):', - }, - { - type: 'input', - name: 'defaultSummaryModel', - message: 'Default summary model ID (leave empty to skip):', - }, - { - type: 'input', - name: 'defaultEmbeddingModel', - message: 'Default embedding model ID (leave empty to skip):', - }, - ]) - - // Filter out empty values - const updates: { - language?: string - maxContextLoadTime?: number - defaultChatModel?: string - defaultSummaryModel?: string - defaultEmbeddingModel?: string - } = {} - Object.entries(answers).forEach(([key, value]) => { - if (value) { - updates[key as keyof typeof updates] = value as never - } - }) - - const spinner = ora('Saving settings...').start() - - 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(formatError(error))) - process.exit(1) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) -} - diff --git a/packages/cli/src/cli/commands/debug.ts b/packages/cli/src/cli/commands/debug.ts deleted file mode 100644 index 9887058f..00000000 --- a/packages/cli/src/cli/commands/debug.ts +++ /dev/null @@ -1,40 +0,0 @@ -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) - } - } - } - }) -} - diff --git a/packages/cli/src/cli/commands/mcp.ts b/packages/cli/src/cli/commands/mcp.ts deleted file mode 100644 index 96e7c034..00000000 --- a/packages/cli/src/cli/commands/mcp.ts +++ /dev/null @@ -1,328 +0,0 @@ -import type { Command } from 'commander' -import chalk from 'chalk' -import inquirer from 'inquirer' -import ora from 'ora' -import { table } from 'table' -import * as mcpCore from '../../core/mcp' -import { formatError } from '../../utils' -import type { MCPConnectionConfig } from '../../types' - -export function mcpCommands(program: Command) { - program - .command('list') - .description('List all MCP connections') - .action(async () => { - try { - const spinner = ora('Fetching MCP connections list...').start() - - try { - const connections = await mcpCore.listMCPConnections() - spinner.succeed(chalk.green('MCP Connections List')) - - if (connections.length === 0) { - console.log(chalk.yellow('No MCP connections')) - return - } - - const tableData = [ - ['ID', 'Name', 'Type', 'Active'], - ...connections.map((conn) => [ - conn.id.substring(0, 8) + '...', - conn.name, - conn.type, - conn.active ? chalk.green('Yes') : chalk.red('No'), - ]), - ] - - console.log(table(tableData)) - } catch (error) { - spinner.fail(chalk.red('Failed to fetch MCP connections list')) - console.error(chalk.red(formatError(error))) - process.exit(1) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('create') - .description('Create MCP connection') - .option('-n, --name ', 'Connection name') - .option('-t, --type ', 'Connection type (stdio/http/sse)') - .action(async (options) => { - try { - let { name, type } = options - - // Get basic info - if (!name || !type) { - const basicAnswers = await inquirer.prompt([ - { - type: 'input', - name: 'name', - message: 'Connection name:', - when: !name, - }, - { - type: 'list', - name: 'type', - message: 'Connection type:', - choices: ['stdio', 'http', 'sse'], - when: !type, - }, - ]) - - name = name || basicAnswers.name - type = type || basicAnswers.type - } - - let config: MCPConnectionConfig - - // Get type-specific config - if (type === 'stdio') { - const stdioAnswers = await inquirer.prompt([ - { - type: 'input', - name: 'command', - message: 'Command:', - }, - { - type: 'input', - name: 'args', - message: 'Arguments (comma-separated, optional):', - default: '', - }, - { - type: 'input', - name: 'cwd', - message: 'Working directory:', - default: process.cwd(), - }, - { - type: 'input', - name: 'env', - message: 'Environment variables (key=value, comma-separated, optional):', - default: '', - }, - ]) - - const args = stdioAnswers.args ? stdioAnswers.args.split(',').map((s: string) => s.trim()) : [] - const env: Record = {} - if (stdioAnswers.env) { - stdioAnswers.env.split(',').forEach((pair: string) => { - const [key, value] = pair.split('=').map((s: string) => s.trim()) - if (key && value) { - env[key] = value - } - }) - } - - config = { - type: 'stdio', - command: stdioAnswers.command, - args, - env, - cwd: stdioAnswers.cwd, - } - } else if (type === 'http' || type === 'sse') { - const httpAnswers = await inquirer.prompt([ - { - type: 'input', - name: 'url', - message: 'URL:', - }, - { - type: 'input', - name: 'headers', - message: 'Headers (key=value, comma-separated, optional):', - default: '', - }, - ]) - - const headers: Record = {} - if (httpAnswers.headers) { - httpAnswers.headers.split(',').forEach((pair: string) => { - const [key, value] = pair.split('=').map((s: string) => s.trim()) - if (key && value) { - headers[key] = value - } - }) - } - - config = { - type: type as 'http' | 'sse', - url: httpAnswers.url, - headers, - } - } else { - console.error(chalk.red('Invalid connection type')) - process.exit(1) - } - - const { active } = await inquirer.prompt([ - { - type: 'confirm', - name: 'active', - message: 'Activate connection?', - default: true, - }, - ]) - - const spinner = ora('Creating MCP connection...').start() - - try { - const connection = await mcpCore.createMCPConnection({ - name, - config, - active, - }) - - spinner.succeed(chalk.green('MCP connection created successfully')) - console.log(chalk.blue(`Name: ${connection.name}`)) - console.log(chalk.blue(`Type: ${connection.type}`)) - console.log(chalk.blue(`ID: ${connection.id}`)) - } catch (error) { - spinner.fail(chalk.red('Failed to create MCP connection')) - console.error(chalk.red(formatError(error))) - process.exit(1) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('get ') - .description('Get MCP connection details') - .action(async (id) => { - try { - const spinner = ora('Fetching MCP connection details...').start() - - try { - const connection = await mcpCore.getMCPConnection(id) - spinner.succeed(chalk.green('MCP Connection Details')) - console.log(chalk.blue(`ID: ${connection.id}`)) - console.log(chalk.blue(`Name: ${connection.name}`)) - console.log(chalk.blue(`Type: ${connection.type}`)) - console.log( - chalk.blue(`Active: ${connection.active ? chalk.green('Yes') : chalk.red('No')}`) - ) - console.log(chalk.blue(`Config:`)) - console.log(JSON.stringify(connection.config, null, 2)) - } catch (error) { - spinner.fail(chalk.red('Failed to fetch MCP connection')) - console.error(chalk.red(formatError(error))) - process.exit(1) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('update ') - .description('Update MCP connection') - .option('-n, --name ', 'Connection name') - .option('-a, --active ', 'Active status (true/false)') - .action(async (id, options) => { - try { - const updates: { - name?: string - active?: boolean - } = {} - - if (options.name) updates.name = options.name - if (options.active !== undefined) { - updates.active = options.active === 'true' || options.active === true - } - - if (Object.keys(updates).length === 0) { - console.log(chalk.yellow('No update parameters provided')) - console.log(chalk.yellow('Note: Config updates are not yet supported via CLI')) - return - } - - const spinner = ora('Updating MCP connection...').start() - - try { - await mcpCore.updateMCPConnection(id, updates) - spinner.succeed(chalk.green('MCP connection updated')) - } catch (error) { - spinner.fail(chalk.red('Failed to update MCP connection')) - console.error(chalk.red(formatError(error))) - process.exit(1) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('delete ') - .description('Delete MCP connection') - .action(async (id) => { - try { - const { confirm } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirm', - message: chalk.yellow(`Are you sure you want to delete MCP connection ${id}?`), - default: false, - }, - ]) - - if (!confirm) { - console.log(chalk.yellow('Cancelled')) - return - } - - const spinner = ora('Deleting MCP connection...').start() - - try { - await mcpCore.deleteMCPConnection(id) - spinner.succeed(chalk.green('MCP connection deleted')) - } catch (error) { - spinner.fail(chalk.red('Failed to delete MCP connection')) - console.error(chalk.red(formatError(error))) - process.exit(1) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('toggle ') - .description('Toggle MCP connection active status') - .action(async (id) => { - try { - const spinner = ora('Toggling connection status...').start() - - try { - const newStatus = await mcpCore.toggleMCPConnection(id) - spinner.succeed( - chalk.green(`Connection ${newStatus ? 'activated' : 'deactivated'}`) - ) - } catch (error) { - spinner.fail(chalk.red('Failed to toggle connection')) - console.error(chalk.red(formatError(error))) - process.exit(1) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) -} - diff --git a/packages/cli/src/cli/commands/memory.ts b/packages/cli/src/cli/commands/memory.ts deleted file mode 100644 index 95c58f56..00000000 --- a/packages/cli/src/cli/commands/memory.ts +++ /dev/null @@ -1,165 +0,0 @@ -import type { Command } from 'commander' -import chalk from 'chalk' -import ora from 'ora' -import * as memoryCore from '../../core/memory' -import { formatError } from '../../utils' - -export function memoryCommands(program: Command) { - program - .command('search ') - .description('Search memories') - .option('-l, --limit ', 'Number of results to return', '10') - .action(async (query, options) => { - try { - const spinner = ora('Searching memories...').start() - - try { - const results = await memoryCore.searchMemory({ - query, - limit: parseInt(options.limit), - }) - - spinner.succeed(chalk.green(`Found ${results.length} memories`)) - - if (results.length === 0) { - console.log(chalk.yellow('No related memories found')) - return - } - - results.forEach((item, index) => { - console.log() - 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: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('add ') - .description('Add memory') - .action(async (content) => { - try { - const spinner = ora('Adding memory...').start() - - 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(formatError(error))) - process.exit(1) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('messages') - .alias('msg') - .description('Get message history') - .option('-p, --page ', 'Page number', '1') - .option('-l, --limit ', 'Items per page', '20') - .action(async (options) => { - try { - const spinner = ora('Fetching message history...').start() - - try { - const result = await memoryCore.getMessages({ - page: parseInt(options.page), - limit: parseInt(options.limit), - }) - - const { messages, pagination } = result - spinner.succeed(chalk.green(`Message History (Page ${pagination.page}/${pagination.totalPages})`)) - - if (messages.length === 0) { - console.log(chalk.yellow('No messages')) - return - } - - console.log(chalk.dim(`\nTotal: ${pagination.total} messages\n`)) - - messages.forEach((msg) => { - const roleColor = msg.role === 'user' ? chalk.blue : chalk.green - const roleIcon = msg.role === 'user' ? 'šŸ‘¤' : 'šŸ¤–' - console.log(roleColor(`${roleIcon} ${msg.role.toUpperCase()}`)) - console.log(chalk.dim(new Date(msg.timestamp).toLocaleString('en-US'))) - 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: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('filter') - .description('Filter messages by date range') - .option('-s, --start ', 'Start date (ISO 8601)') - .option('-e, --end ', 'End date (ISO 8601)') - .action(async (options) => { - try { - if (!options.start || !options.end) { - console.error(chalk.red('Please provide start and end dates')) - console.log(chalk.dim('Example: memoh memory filter -s 2024-01-01T00:00:00Z -e 2024-12-31T23:59:59Z')) - process.exit(1) - } - - const spinner = ora('Filtering messages...').start() - - try { - const messages = await memoryCore.filterMessages({ - startDate: options.start, - endDate: options.end, - }) - - spinner.succeed(chalk.green(`Found ${messages.length} messages`)) - - if (messages.length === 0) { - console.log(chalk.yellow('No messages found')) - return - } - - console.log() - - messages.forEach((msg) => { - const roleColor = msg.role === 'user' ? chalk.blue : chalk.green - const roleIcon = msg.role === 'user' ? 'šŸ‘¤' : 'šŸ¤–' - console.log(roleColor(`${roleIcon} ${msg.role.toUpperCase()}`)) - console.log(chalk.dim(new Date(msg.timestamp).toLocaleString('en-US'))) - 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: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) -} - diff --git a/packages/cli/src/cli/commands/model.ts b/packages/cli/src/cli/commands/model.ts deleted file mode 100644 index 80f3c71e..00000000 --- a/packages/cli/src/cli/commands/model.ts +++ /dev/null @@ -1,270 +0,0 @@ -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/client' - -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 ', 'Model name') - .option('-m, --model-id ', 'Model ID') - .option('-u, --base-url ', 'API Base URL') - .option('-k, --api-key ', 'API Key') - .option('-c, --client-type ', 'Client type (openai/anthropic/google)') - .option('-t, --type ', 'Model type (chat/embedding)', 'chat') - .option('-d, --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 ') - .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 ') - .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) - } - }) -} - diff --git a/packages/cli/src/cli/commands/platform.ts b/packages/cli/src/cli/commands/platform.ts deleted file mode 100644 index abd5c5c8..00000000 --- a/packages/cli/src/cli/commands/platform.ts +++ /dev/null @@ -1,325 +0,0 @@ -import type { Command } from 'commander' -import chalk from 'chalk' -import inquirer from 'inquirer' -import ora from 'ora' -import { table } from 'table' -import * as platformCore from '../../core/platform' -import { formatError } from '../../utils' -import { getApiUrl } from '../../core/client' -import { PLATFORM_DEFINITIONS, type PlatformDefinition } from '../../types' - -export function platformCommands(program: Command) { - program - .command('list') - .description('List all platform configurations') - .action(async () => { - const spinner = ora('Fetching platform list...').start() - try { - const platforms = await platformCore.listPlatforms() - spinner.succeed(chalk.green('Platform List')) - - if (platforms.length === 0) { - console.log(chalk.yellow('No platform configurations found')) - return - } - - const tableData = [ - ['ID', 'Name', 'Active', 'Created'], - ...platforms.map((item) => [ - item.id.substring(0, 8) + '...', - item.name, - item.active ? chalk.green('āœ“ Active') : chalk.dim('āœ— Inactive'), - new Date(item.createdAt).toLocaleDateString(), - ]), - ] - - 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 platform configuration') - .action(async () => { - try { - // Step 1: Select platform type - const { platformType } = await inquirer.prompt([ - { - type: 'list', - name: 'platformType', - message: 'Select platform type:', - choices: PLATFORM_DEFINITIONS.map((def) => ({ - name: `${def.displayName} - ${def.description}`, - value: def.name, - })), - }, - ]) - - const platformDef = PLATFORM_DEFINITIONS.find((def) => def.name === platformType) - if (!platformDef) { - console.error(chalk.red('Invalid platform type')) - process.exit(1) - } - - // Step 2: Collect platform-specific config - console.log(chalk.cyan(`\nConfiguring ${platformDef.displayName}...\n`)) - - const configAnswers: Record = {} - for (const field of platformDef.configFields) { - const answer = await inquirer.prompt([ - { - type: field.type || 'input', - name: field.name, - message: field.message, - default: field.default, - validate: field.validate || ((value: string) => { - if (field.required && !value?.toString().trim()) { - return `${field.name} is required` - } - return true - }), - }, - ]) - configAnswers[field.name] = answer[field.name] - } - - // Step 3: Confirm active status - const { active } = await inquirer.prompt([ - { - type: 'confirm', - name: 'active', - message: 'Set as active?', - default: true, - }, - ]) - - const spinner = ora('Creating platform configuration...').start() - - const platform = await platformCore.createPlatform({ - name: platformType, - config: configAnswers, - active, - }) - - spinner.succeed(chalk.green('Platform configuration created successfully')) - console.log(chalk.blue(`\nPlatform: ${platformDef.displayName}`)) - console.log(chalk.blue(`Type: ${platform.name}`)) - console.log(chalk.blue(`Active: ${platform.active ? 'Yes' : 'No'}`)) - console.log(chalk.blue(`ID: ${platform.id}`)) - console.log(chalk.dim(`\nConfig: ${JSON.stringify(platform.config, null, 2)}`)) - } catch (error) { - console.error(chalk.red(formatError(error))) - process.exit(1) - } - }) - - program - .command('get ') - .description('Get platform configuration details') - .action(async (id) => { - const spinner = ora('Fetching platform configuration...').start() - try { - const platform = await platformCore.getPlatform(id) - const platformDef = PLATFORM_DEFINITIONS.find((def) => def.name === platform.name) - spinner.succeed(chalk.green('Platform Configuration')) - console.log(chalk.blue(`ID: ${platform.id}`)) - console.log(chalk.blue(`Platform: ${platformDef?.displayName || platform.name}`)) - console.log(chalk.blue(`Type: ${platform.name}`)) - console.log(chalk.blue(`Active: ${platform.active ? 'Yes' : 'No'}`)) - console.log(chalk.blue(`Config: ${JSON.stringify(platform.config, null, 2)}`)) - console.log(chalk.blue(`Created At: ${new Date(platform.createdAt).toLocaleString('en-US')}`)) - console.log(chalk.blue(`Updated At: ${new Date(platform.updatedAt).toLocaleString('en-US')}`)) - } catch (error) { - spinner.fail(chalk.red('Operation failed')) - console.error(chalk.red(formatError(error))) - process.exit(1) - } - }) - - program - .command('update ') - .description('Update platform configuration') - .option('-n, --name ', 'Platform type (e.g., telegram)') - .option('-c, --config ', 'Platform config (JSON string)') - .option('-a, --active ', 'Set active status (true/false)') - .action(async (id, options) => { - try { - const updates: Record = {} - - if (options.name) updates.name = options.name - if (options.config) { - try { - updates.config = JSON.parse(options.config) - } catch { - console.error(chalk.red('Invalid JSON config')) - process.exit(1) - } - } - if (options.active !== undefined) { - updates.active = options.active === 'true' - } - - if (Object.keys(updates).length === 0) { - console.log(chalk.yellow('No updates specified')) - return - } - - const spinner = ora('Updating platform configuration...').start() - const platform = await platformCore.updatePlatform(id, updates as any) - spinner.succeed(chalk.green('Platform configuration updated successfully')) - const platformDef = PLATFORM_DEFINITIONS.find((def) => def.name === platform.name) - console.log(chalk.blue(`Platform: ${platformDef?.displayName || platform.name}`)) - console.log(chalk.blue(`Type: ${platform.name}`)) - console.log(chalk.blue(`Active: ${platform.active ? 'Yes' : 'No'}`)) - } catch (error) { - console.error(chalk.red(formatError(error))) - process.exit(1) - } - }) - - program - .command('update-config ') - .description('Update platform config interactively or via JSON') - .option('-c, --config ', 'Platform config (JSON string)') - .action(async (id, options) => { - try { - let configObj: Record - - if (options.config) { - // Use provided JSON config - try { - configObj = JSON.parse(options.config) - } catch { - console.error(chalk.red('Invalid JSON config')) - process.exit(1) - } - } else { - // Interactive mode - get current platform first - const spinner = ora('Fetching platform...').start() - const platform = await platformCore.getPlatform(id) - spinner.stop() - - const platformDef = PLATFORM_DEFINITIONS.find((def) => def.name === platform.name) - if (!platformDef) { - console.error(chalk.red(`Unknown platform type: ${platform.name}`)) - process.exit(1) - } - - console.log(chalk.cyan(`\nUpdating config for ${platformDef.displayName}...\n`)) - - configObj = {} - for (const field of platformDef.configFields) { - const currentValue = (platform.config as Record)[field.name] - const answer = await inquirer.prompt([ - { - type: field.type || 'input', - name: field.name, - message: field.message, - default: currentValue || field.default, - validate: field.validate || ((value: string) => { - if (field.required && !value?.toString().trim()) { - return `${field.name} is required` - } - return true - }), - }, - ]) - configObj[field.name] = answer[field.name] - } - } - - const spinner = ora('Updating platform config...').start() - const platform = await platformCore.updatePlatformConfig(id, configObj) - spinner.succeed(chalk.green('Platform config updated successfully')) - const platformDef = PLATFORM_DEFINITIONS.find((def) => def.name === platform.name) - console.log(chalk.blue(`\nPlatform: ${platformDef?.displayName || platform.name}`)) - console.log(chalk.blue(`Type: ${platform.name}`)) - console.log(chalk.dim(`Config: ${JSON.stringify(platform.config, null, 2)}`)) - } catch (error) { - console.error(chalk.red(formatError(error))) - process.exit(1) - } - }) - - program - .command('delete ') - .description('Delete platform configuration') - .action(async (id) => { - try { - const { confirm } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirm', - message: chalk.yellow(`Are you sure you want to delete platform configuration ${id}?`), - default: false, - }, - ]) - - if (!confirm) { - console.log(chalk.yellow('Cancelled')) - return - } - - const spinner = ora('Deleting platform configuration...').start() - await platformCore.deletePlatform(id) - spinner.succeed(chalk.green('Platform configuration deleted')) - } catch (error) { - console.error(chalk.red(formatError(error))) - process.exit(1) - } - }) - - program - .command('activate ') - .description('Activate platform (admin only)') - .action(async (id) => { - const spinner = ora('Activating platform...').start() - try { - const platform = await platformCore.activatePlatform(id) - spinner.succeed(chalk.green('Platform activated successfully')) - const platformDef = PLATFORM_DEFINITIONS.find((def) => def.name === platform.name) - console.log(chalk.blue(`Platform: ${platformDef?.displayName || platform.name}`)) - console.log(chalk.blue(`Type: ${platform.name}`)) - console.log(chalk.blue(`Active: ${platform.active ? chalk.green('Yes') : 'No'}`)) - } catch (error) { - spinner.fail(chalk.red('Operation failed')) - console.error(chalk.red(formatError(error))) - process.exit(1) - } - }) - - program - .command('deactivate ') - .description('Deactivate platform (admin only)') - .action(async (id) => { - const spinner = ora('Deactivating platform...').start() - try { - const platform = await platformCore.inactivatePlatform(id) - spinner.succeed(chalk.green('Platform deactivated successfully')) - const platformDef = PLATFORM_DEFINITIONS.find((def) => def.name === platform.name) - console.log(chalk.blue(`Platform: ${platformDef?.displayName || platform.name}`)) - console.log(chalk.blue(`Type: ${platform.name}`)) - console.log(chalk.blue(`Active: ${platform.active ? 'Yes' : chalk.dim('No')}`)) - } catch (error) { - spinner.fail(chalk.red('Operation failed')) - console.error(chalk.red(formatError(error))) - process.exit(1) - } - }) -} - diff --git a/packages/cli/src/cli/commands/schedule.ts b/packages/cli/src/cli/commands/schedule.ts deleted file mode 100644 index 64b9d046..00000000 --- a/packages/cli/src/cli/commands/schedule.ts +++ /dev/null @@ -1,264 +0,0 @@ -import type { Command } from 'commander' -import chalk from 'chalk' -import inquirer from 'inquirer' -import ora from 'ora' -import { table } from 'table' -import * as scheduleCore from '../../core/schedule' -import { formatError } from '../../utils' - -export function scheduleCommands(program: Command) { - program - .command('list') - .description('List all scheduled tasks') - .action(async () => { - try { - const spinner = ora('Fetching scheduled tasks list...').start() - - try { - const schedules = await scheduleCore.listSchedules() - spinner.succeed(chalk.green('Scheduled Tasks List')) - - if (schedules.length === 0) { - console.log(chalk.yellow('No scheduled tasks')) - return - } - - const tableData = [ - ['ID', 'Title', 'Cron', 'Enabled', 'Created At'], - ...schedules.map((schedule) => [ - schedule.id.substring(0, 8) + '...', - schedule.title, - schedule.cronExpression, - schedule.enabled ? chalk.green('Yes') : chalk.red('No'), - new Date(schedule.createdAt).toLocaleString('en-US'), - ]), - ] - - 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: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('create') - .description('Create scheduled task') - .option('-t, --title ', 'Task title') - .option('-d, --description <description>', 'Task description') - .option('-c, --cron <expression>', 'Cron expression') - .option('-e, --enabled', 'Enable task', false) - .action(async (options) => { - try { - let { title, description, cron, enabled } = options - - if (!title || !cron) { - const answers = await inquirer.prompt([ - { - type: 'input', - name: 'title', - message: 'Task title:', - when: !title, - }, - { - type: 'input', - name: 'description', - message: 'Task description (optional):', - when: !description, - }, - { - type: 'input', - name: 'cron', - message: 'Cron expression (e.g., 0 9 * * *):', - when: !cron, - }, - { - type: 'confirm', - name: 'enabled', - message: 'Enable task?', - default: false, - when: enabled === undefined, - }, - ]) - - title = title || answers.title - description = description || answers.description - cron = cron || answers.cron - enabled = enabled !== undefined ? enabled : answers.enabled - } - - const spinner = ora('Creating scheduled task...').start() - - try { - const schedule = await scheduleCore.createSchedule({ - title, - description, - cronExpression: cron, - enabled, - }) - - 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(formatError(error))) - process.exit(1) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('get <id>') - .description('Get scheduled task details') - .action(async (id) => { - try { - const spinner = ora('Fetching scheduled task details...').start() - - 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}`)) - if (schedule.description) { - console.log(chalk.blue(`Description: ${schedule.description}`)) - } - console.log(chalk.blue(`Cron: ${schedule.cronExpression}`)) - console.log( - chalk.blue(`Enabled: ${schedule.enabled ? chalk.green('Yes') : chalk.red('No')}`) - ) - console.log( - chalk.blue(`Created At: ${new Date(schedule.createdAt).toLocaleString('en-US')}`) - ) - 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: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('update <id>') - .description('Update scheduled task') - .option('-t, --title <title>', 'Task title') - .option('-d, --description <description>', 'Task description') - .option('-c, --cron <expression>', 'Cron expression') - .option('-e, --enabled <boolean>', 'Enable task (true/false)') - .action(async (id, options) => { - try { - const updates: { - title?: string - description?: string - cronExpression?: string - enabled?: boolean - } = {} - - if (options.title) updates.title = options.title - if (options.description) updates.description = options.description - if (options.cron) updates.cronExpression = options.cron - if (options.enabled !== undefined) { - updates.enabled = options.enabled === 'true' || options.enabled === true - } - - if (Object.keys(updates).length === 0) { - console.log(chalk.yellow('No update parameters provided')) - return - } - - const spinner = ora('Updating scheduled task...').start() - - 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(formatError(error))) - process.exit(1) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('delete <id>') - .description('Delete scheduled task') - .action(async (id) => { - try { - const { confirm } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirm', - message: chalk.yellow(`Are you sure you want to delete scheduled task ${id}?`), - default: false, - }, - ]) - - if (!confirm) { - console.log(chalk.yellow('Cancelled')) - return - } - - const spinner = ora('Deleting scheduled task...').start() - - 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(formatError(error))) - process.exit(1) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('toggle <id>') - .description('Toggle scheduled task enabled status') - .action(async (id) => { - try { - const spinner = ora('Toggling task status...').start() - - 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) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) -} - diff --git a/packages/cli/src/cli/commands/user.ts b/packages/cli/src/cli/commands/user.ts deleted file mode 100644 index 0947ba8c..00000000 --- a/packages/cli/src/cli/commands/user.ts +++ /dev/null @@ -1,209 +0,0 @@ -import type { Command } from 'commander' -import chalk from 'chalk' -import inquirer from 'inquirer' -import ora from 'ora' -import { table } from 'table' -import * as userCore from '../../core/user' -import { formatError } from '../../utils' - -export function userCommands(program: Command) { - program - .command('list') - .description('List all users (requires admin privileges)') - .action(async () => { - try { - const spinner = ora('Fetching user list...').start() - - try { - const users = await userCore.listUsers() - spinner.succeed(chalk.green('User List')) - - if (users.length === 0) { - console.log(chalk.yellow('No users')) - return - } - - const tableData = [ - ['ID', 'Username', 'Role', 'Created At'], - ...users.map((user) => [ - user.id, - user.username, - user.role === 'admin' ? chalk.red('Admin') : chalk.blue('User'), - new Date(user.createdAt).toLocaleString('en-US'), - ]), - ] - - 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: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('create') - .description('Create new user (requires admin privileges)') - .option('-u, --username <username>', 'Username') - .option('-p, --password <password>', 'Password') - .option('-r, --role <role>', 'Role (user/admin)', 'user') - .action(async (options) => { - try { - let username = options.username - let password = options.password - let role = options.role - - if (!username || !password) { - const answers = await inquirer.prompt([ - { - type: 'input', - name: 'username', - message: 'Username:', - when: !username, - }, - { - type: 'password', - name: 'password', - message: 'Password:', - when: !password, - mask: '*', - }, - { - type: 'list', - name: 'role', - message: 'Role:', - choices: ['user', 'admin'], - default: 'user', - when: !role, - }, - ]) - username = username || answers.username - password = password || answers.password - role = role || answers.role - } - - const spinner = ora('Creating user...').start() - - 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(formatError(error))) - process.exit(1) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('delete <id>') - .description('Delete user (requires admin privileges)') - .action(async (id) => { - try { - const { confirm } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirm', - message: chalk.yellow(`Are you sure you want to delete user ${id}?`), - default: false, - }, - ]) - - if (!confirm) { - console.log(chalk.yellow('Cancelled')) - return - } - - const spinner = ora('Deleting user...').start() - - 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(formatError(error))) - process.exit(1) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('get <id>') - .description('Get user details') - .action(async (id) => { - try { - const spinner = ora('Fetching user information...').start() - - 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: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) - - program - .command('update-password <id>') - .description('Update user password (requires admin privileges)') - .option('-p, --password <password>', 'New password') - .action(async (id, options) => { - try { - let password = options.password - - if (!password) { - const answers = await inquirer.prompt([ - { - type: 'password', - name: 'password', - message: 'New password:', - mask: '*', - }, - ]) - password = answers.password - } - - const spinner = ora('Updating password...').start() - - 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(formatError(error))) - process.exit(1) - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - console.error(chalk.red('Error:'), message) - process.exit(1) - } - }) -} - diff --git a/packages/cli/src/cli/index.ts b/packages/cli/src/cli/index.ts old mode 100755 new mode 100644 index 48683198..a8f5f88f --- a/packages/cli/src/cli/index.ts +++ b/packages/cli/src/cli/index.ts @@ -1,72 +1,558 @@ #!/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 { platformCommands } from './commands/platform' -import { agentCommands, startInteractiveMode } from './commands/agent' -import { memoryCommands } from './commands/memory' -import { configCommands } from './commands/config' -import { scheduleCommands } from './commands/schedule' -import { mcpCommands } from './commands/mcp' -import { debugCommands } from './commands/debug' +import inquirer from 'inquirer' +import ora from 'ora' +import { table } from 'table' +import readline from 'node:readline/promises' +import { stdin as input, stdout as output } from 'node:process' -const program = new Command() +import { apiRequest } from '../core/api' +import { + readConfig, + writeConfig, + readToken, + writeToken, + clearToken, + TokenInfo, + getBaseURL, +} from '../utils/store' -program - .name('memoh') - .description(chalk.bold.blue('šŸ  Memoh 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) - -// Platform management commands -const platform = program.command('platform').description('Platform configuration management') -platformCommands(platform) - -// 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) - -// MCP management commands -const mcp = program.command('mcp').description('MCP connection management') -mcpCommands(mcp) - -// 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() +type Provider = { + id: string + name: string + client_type: string + base_url: string + api_key?: string +} + +type Model = { + model_id: string + name?: string + llm_provider_id: string + is_multimodal: boolean + type: 'chat' | 'embedding' + dimensions?: number + enable_as?: 'chat' | 'memory' | 'embedding' +} + +type ModelResponse = Partial<Model> & { + model_id?: string + model?: Model +} + +const program = new Command() +program + .name('memoh') + .description('Memoh CLI') + .version('0.1.0') + +const ensureAuth = () => { + const token = readToken() + if (!token?.access_token) { + console.log(chalk.red('Not logged in. Run `memoh login` first.')) + process.exit(1) + } + return token +} + +const ensureModelsReady = async () => { + const token = ensureAuth() + const [chatModels, embeddingModels] = await Promise.all([ + apiRequest<ModelResponse[]>('/models?type=chat', {}, token), + apiRequest<ModelResponse[]>('/models?type=embedding', {}, token), + ]) + if (chatModels.length === 0 || embeddingModels.length === 0) { + console.log(chalk.red('Model configuration incomplete.')) + console.log(chalk.yellow('At least one chat model and one embedding model are required.')) + process.exit(1) + } +} + +const getErrorMessage = (err: unknown) => { + if (err && typeof err === 'object' && 'message' in err) { + const value = (err as { message?: unknown }).message + if (typeof value === 'string') return value + } + return 'Unknown error' +} + +const getModelId = (item: ModelResponse) => item.model?.model_id ?? item.model_id ?? '' +const getProviderId = (item: ModelResponse) => item.model?.llm_provider_id ?? item.llm_provider_id ?? '' +const getModelType = (item: ModelResponse) => item.model?.type ?? item.type ?? 'chat' +const getModelMultimodal = (item: ModelResponse) => item.model?.is_multimodal ?? item.is_multimodal ?? false +const getModelEnableAs = (item: ModelResponse) => item.model?.enable_as ?? item.enable_as + +const renderProvidersTable = (providers: Provider[], models: ModelResponse[]) => { + const rows: string[][] = [['Provider', 'Type', 'Base URL', 'Models']] + for (const provider of providers) { + const providerModels = models + .filter(m => getProviderId(m) === provider.id) + .map(m => `${getModelId(m)} (${getModelType(m)})`) + rows.push([ + provider.name, + provider.client_type, + provider.base_url, + providerModels.join(', ') || '-', + ]) + } + return table(rows) +} + +const renderModelsTable = (models: ModelResponse[], providers: Provider[]) => { + const providerMap = new Map(providers.map(p => [p.id, p.name])) + const rows: string[][] = [['Model ID', 'Type', 'Provider', 'Multimodal', 'Enable As']] + for (const item of models) { + rows.push([ + getModelId(item), + getModelType(item), + providerMap.get(getProviderId(item)) ?? getProviderId(item), + getModelMultimodal(item) ? 'yes' : 'no', + getModelEnableAs(item) ?? '-', + ]) + } + return table(rows) +} + +program + .command('login') + .description('Login') + .action(async () => { + const answers = await inquirer.prompt([ + { type: 'input', name: 'username', message: 'Username:' }, + { type: 'password', name: 'password', message: 'Password:' }, + ]) + const spinner = ora('Logging in...').start() + try { + const resp = await apiRequest<TokenInfo>('/auth/login', { + method: 'POST', + body: JSON.stringify({ + username: answers.username, + password: answers.password, + }), + }, null) + writeToken(resp) + spinner.succeed('Logged in') + } catch (err: unknown) { + spinner.fail(getErrorMessage(err) || 'Login failed') + process.exit(1) + } + }) + +program + .command('logout') + .description('Logout') + .action(() => { + clearToken() + console.log(chalk.green('Logged out')) + }) + +program + .command('whoami') + .description('Show current user') + .action(() => { + const token = readToken() + if (!token?.access_token) { + console.log(chalk.red('Not logged in.')) + process.exit(1) + } + if (token.username) { + console.log(`username: ${token.username}`) + } + if (token.user_id) { + console.log(`user_id: ${token.user_id}`) + return + } + const payload = token.access_token.split('.')[1] + if (!payload) { + console.log(chalk.yellow('Token found but payload missing.')) + return + } + const decoded = Buffer.from(payload, 'base64').toString('utf-8') + try { + const data = JSON.parse(decoded) + console.log(`user_id: ${data.user_id ?? data.sub ?? 'unknown'}`) + } catch { + console.log(chalk.yellow('Unable to parse token payload.')) + } + }) + +const configCmd = program + .command('config') + .description('Show or update current config') + +configCmd.action(() => { + const config = readConfig() + console.log(`host = "${config.host}"`) + console.log(`port = ${config.port}`) +}) + +configCmd + .command('set') + .description('Update config') + .option('--host <host>') + .option('--port <port>') + .action(async (opts) => { + const current = readConfig() + let host = opts.host + let port = opts.port ? Number.parseInt(opts.port, 10) : undefined + + if (!host && !port) { + const answers = await inquirer.prompt([ + { type: 'input', name: 'host', message: 'Host:', default: current.host }, + { type: 'input', name: 'port', message: 'Port:', default: current.port }, + ]) + host = answers.host + port = Number.parseInt(answers.port, 10) + } + + if (host) current.host = host + if (port && !Number.isNaN(port)) current.port = port + + writeConfig(current) + console.log(chalk.green('Config updated')) + }) + +const provider = program.command('provider').description('Provider management') + +provider + .command('list') + .description('List providers') + .option('--provider <name>', 'Filter by provider name') + .action(async (opts) => { + const token = ensureAuth() + const providers = opts.provider + ? [await apiRequest<Provider>(`/providers/name/${encodeURIComponent(opts.provider)}`, {}, token)] + : await apiRequest<Provider[]>('/providers', {}, token) + const models = await apiRequest<ModelResponse[]>('/models', {}, token) + console.log(renderProvidersTable(providers, models)) + }) + +provider + .command('create') + .description('Create provider') + .option('--name <name>') + .option('--type <type>') + .option('--base_url <url>') + .option('--api_key <key>') + .action(async (opts) => { + const token = ensureAuth() + const questions = [] + if (!opts.name) questions.push({ type: 'input', name: 'name', message: 'Provider name:' }) + if (!opts.type) { + questions.push({ + type: 'list', + name: 'client_type', + message: 'Client type:', + choices: ['openai', 'anthropic', 'google', 'ollama'], + }) + } + if (!opts.base_url) questions.push({ type: 'input', name: 'base_url', message: 'Base URL:' }) + if (!opts.api_key) questions.push({ type: 'password', name: 'api_key', message: 'API key:' }) + const answers = questions.length ? await inquirer.prompt(questions) : {} + const payload = { + name: opts.name ?? answers.name, + client_type: opts.type ?? answers.client_type, + base_url: opts.base_url ?? answers.base_url, + api_key: opts.api_key ?? answers.api_key, + } + const spinner = ora('Creating provider...').start() + try { + await apiRequest('/providers', { method: 'POST', body: JSON.stringify(payload) }, token) + spinner.succeed('Provider created') + } catch (err: unknown) { + spinner.fail(getErrorMessage(err) || 'Failed to create provider') + process.exit(1) + } + }) + +provider + .command('delete') + .description('Delete provider') + .option('--provider <name>', 'Provider name') + .action(async (opts) => { + const token = ensureAuth() + if (!opts.provider) { + console.log(chalk.red('Provider name is required.')) + process.exit(1) + } + const providerInfo = await apiRequest<Provider>(`/providers/name/${encodeURIComponent(opts.provider)}`, {}, token) + const spinner = ora('Deleting provider...').start() + try { + await apiRequest(`/providers/${providerInfo.id}`, { method: 'DELETE' }, token) + spinner.succeed('Provider deleted') + } catch (err: unknown) { + spinner.fail(getErrorMessage(err) || 'Failed to delete provider') + process.exit(1) + } + }) + +const model = program.command('model').description('Model management') + +model + .command('list') + .description('List models') + .action(async () => { + const token = ensureAuth() + const [models, providers] = await Promise.all([ + apiRequest<ModelResponse[]>('/models', {}, token), + apiRequest<Provider[]>('/providers', {}, token), + ]) + console.log(renderModelsTable(models, providers)) + }) + +model + .command('create') + .description('Create model') + .option('--model_id <model_id>') + .option('--name <name>') + .option('--provider <provider>') + .option('--type <type>') + .option('--dimensions <dimensions>') + .option('--multimodal', 'Is multimodal') + .option('--enable_as <enable_as>') + .action(async (opts) => { + const token = ensureAuth() + const providers = await apiRequest<Provider[]>('/providers', {}, token) + let provider = providers.find(p => p.name === opts.provider) + if (!provider) { + const answer = await inquirer.prompt([{ + type: 'list', + name: 'provider', + message: 'Select provider:', + choices: providers.map(p => p.name), + }]) + provider = providers.find(p => p.name === answer.provider) + } + if (!provider) { + console.log(chalk.red('Provider not found.')) + process.exit(1) + } + const questions = [] + if (!opts.model_id) questions.push({ type: 'input', name: 'model_id', message: 'Model ID (e.g. gpt-4):' }) + if (!opts.type) questions.push({ type: 'list', name: 'type', message: 'Model type:', choices: ['chat', 'embedding'] }) + const answers = questions.length ? await inquirer.prompt(questions) : {} + const modelId = opts.model_id ?? answers.model_id + const modelType = opts.type ?? answers.type + let dimensions = opts.dimensions ? Number.parseInt(opts.dimensions, 10) : undefined + if (modelType === 'embedding' && (!dimensions || Number.isNaN(dimensions))) { + const dimAnswer = await inquirer.prompt([{ + type: 'input', + name: 'dimensions', + message: 'Embedding dimensions (e.g. 1536):', + }]) + dimensions = Number.parseInt(dimAnswer.dimensions, 10) + } + if (modelType === 'embedding' && (!dimensions || Number.isNaN(dimensions) || dimensions <= 0)) { + console.log(chalk.red('Embedding models require a valid dimensions value.')) + process.exit(1) + } + const isMultimodal = Boolean(opts.multimodal) + const payload = { + model_id: modelId, + name: opts.name ?? modelId, + llm_provider_id: provider.id, + is_multimodal: isMultimodal, + type: modelType, + dimensions, + enable_as: opts.enable_as, + } + const spinner = ora('Creating model...').start() + try { + await apiRequest('/models', { method: 'POST', body: JSON.stringify(payload) }, token) + spinner.succeed('Model created') + } catch (err: unknown) { + spinner.fail(getErrorMessage(err) || 'Failed to create model') + process.exit(1) + } + }) + +model + .command('delete') + .description('Delete model') + .option('--model <model>') + .action(async (opts) => { + const token = ensureAuth() + if (!opts.model) { + console.log(chalk.red('Model name is required.')) + process.exit(1) + } + const spinner = ora('Deleting model...').start() + try { + await apiRequest(`/models/model/${encodeURIComponent(opts.model)}`, { method: 'DELETE' }, token) + spinner.succeed('Model deleted') + } catch (err: unknown) { + spinner.fail(getErrorMessage(err) || 'Failed to delete model') + process.exit(1) + } + }) + +model + .command('enable') + .description('Enable model') + .option('--as <enable_as>') + .option('--model <model>') + .option('--provider <provider>') + .action(async (opts) => { + const token = ensureAuth() + let enableAs = opts.as + if (!enableAs) { + const answer = await inquirer.prompt([{ + type: 'list', + name: 'enable_as', + message: 'Enable as:', + choices: ['chat', 'memory', 'embedding'], + }]) + enableAs = answer.enable_as + } + const [providers, models] = await Promise.all([ + apiRequest<Provider[]>('/providers', {}, token), + apiRequest<ModelResponse[]>('/models', {}, token), + ]) + let providerName = opts.provider + if (!providerName) { + const answer = await inquirer.prompt([{ + type: 'list', + name: 'provider', + message: 'Select provider:', + choices: providers.map(p => p.name), + }]) + providerName = answer.provider + } + const provider = providers.find(p => p.name === providerName) + if (!provider) { + console.log(chalk.red('Provider not found.')) + process.exit(1) + } + let modelName = opts.model + if (!modelName) { + const providerModels = models + .filter(m => getProviderId(m) === provider.id) + .map(m => getModelId(m)) + if (providerModels.length === 0) { + console.log(chalk.red('No models found for selected provider.')) + process.exit(1) + } + const answer = await inquirer.prompt([{ + type: 'list', + name: 'model', + message: 'Select model:', + choices: providerModels, + }]) + modelName = answer.model + } + const current = models.find(m => getModelId(m) === modelName && getProviderId(m) === provider.id) + ?? await apiRequest<ModelResponse>(`/models/model/${encodeURIComponent(modelName)}`, {}, token) + const modelPayload = current.model + ? { ...current.model, model_id: current.model.model_id } + : { ...current, model_id: current.model_id ?? modelName } + const payload = { + ...modelPayload, + enable_as: enableAs, + } + const spinner = ora('Updating model...').start() + try { + await apiRequest(`/models/model/${encodeURIComponent(modelName)}`, { + method: 'PUT', + body: JSON.stringify(payload), + }, token) + spinner.succeed('Model enabled') + } catch (err: unknown) { + spinner.fail(getErrorMessage(err) || 'Failed to enable model') + process.exit(1) + } + }) + +program + .action(async () => { + await ensureModelsReady() + const token = ensureAuth() + const rl = readline.createInterface({ input, output }) + console.log(chalk.green('Memoh chat. Type `exit` to quit.')) + while (true) { + const line = (await rl.question(chalk.cyan('> '))).trim() + if (!line || line.toLowerCase() === 'exit') { + break + } + try { + const streamed = await streamChat(line, token) + if (!streamed) { + const resp = await apiRequest<{ messages: Array<{ role?: string; content?: unknown }> }>('/chat', { + method: 'POST', + body: JSON.stringify({ query: line }), + }, token) + const assistant = [...resp.messages].reverse().find(m => m.role === 'assistant') ?? resp.messages.at(-1) + const content = assistant?.content + if (typeof content === 'string') { + console.log(chalk.white(content)) + } else { + console.log(chalk.white(JSON.stringify(content, null, 2))) + } + } + } catch (err: unknown) { + console.log(chalk.red(getErrorMessage(err) || 'Chat failed')) + } + } + rl.close() + }) + +program.parseAsync(process.argv) + +const streamChat = async (query: string, token: TokenInfo) => { + const config = readConfig() + const baseURL = getBaseURL(config) + const resp = await fetch(`${baseURL}/chat/stream`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token.access_token}`, + }, + body: JSON.stringify({ query }), + }).catch(() => null) + if (!resp || !resp.ok || !resp.body) return false + + const stream = resp.body + const reader = stream.getReader() + const decoder = new TextDecoder() + let buffer = '' + let printed = false + while (true) { + const { value, done } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + let idx + while ((idx = buffer.indexOf('\n')) >= 0) { + const line = buffer.slice(0, idx).trim() + buffer = buffer.slice(idx + 1) + if (!line.startsWith('data:')) continue + const payload = line.slice(5).trim() + if (!payload || payload === '[DONE]') continue + const text = extractTextFromEvent(payload) + if (text) { + process.stdout.write(text) + printed = true + } + } + } + if (printed) { + process.stdout.write('\n') + } + return true +} + +const extractTextFromEvent = (payload: string) => { + try { + const event = JSON.parse(payload) + if (typeof event === 'string') return event + if (typeof event?.text === 'string') return event.text + if (typeof event?.delta?.content === 'string') return event.delta.content + if (typeof event?.content === 'string') return event.content + if (typeof event?.data === 'string') return event.data + if (typeof event?.data?.text === 'string') return event.data.text + if (typeof event?.data?.delta?.content === 'string') return event.data.delta.content + return null + } catch { + return payload + } } diff --git a/packages/cli/src/core/agent.ts b/packages/cli/src/core/agent.ts deleted file mode 100644 index 960feede..00000000 --- a/packages/cli/src/core/agent.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { requireAuth, getToken, getApiUrl } from './client' -import type { MemohContext } from './context' - -export interface ChatParams { - message: string - 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) - sync version - */ -export async function chatStream( - params: ChatParams, - onEvent: StreamCallback, - context?: MemohContext -): Promise<void> { - requireAuth(context) - const token = getToken(context)! - const apiUrl = getApiUrl(context) - - await performStreamChat(apiUrl, token, params, onEvent) -} - -/** - * Chat with AI Agent (streaming) - async version for Redis storage - */ -export async function chatStreamAsync( - params: ChatParams, - onEvent: StreamCallback, - context?: MemohContext -): Promise<void> { - requireAuth(context) - const token = getToken(context)! - const apiUrl = getApiUrl(context) - - if (!token) { - throw new Error('Not authenticated') - } - - await performStreamChat(apiUrl, token, params, onEvent) -} - -/** - * Internal function to perform streaming chat - */ -async function performStreamChat( - apiUrl: string, - token: string, - params: ChatParams, - onEvent: StreamCallback -): Promise<void> { - const response = await fetch(`${apiUrl}/agent/stream`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - message: params.message, - 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 = '' - let receivedDone = false - - 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]') { - receivedDone = true - 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 - } - } - } - } - - // If stream ended without [DONE], it's an error - if (!receivedDone) { - throw new Error('Connection closed unexpectedly - stream ended without completion signal') - } -} - -/** - * Chat with AI Agent (non-streaming, collect full response) - sync version - */ -export async function chat(params: ChatParams, context?: MemohContext): 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) - } - }, context) - - return fullResponse -} - -/** - * Chat with AI Agent (non-streaming, collect full response) - async version - */ -export async function chatAsync(params: ChatParams, context?: MemohContext): Promise<string> { - let fullResponse = '' - - await chatStreamAsync(params, async (event) => { - if (event.type === 'text-delta' && event.text) { - fullResponse += event.text - } else if (event.type === 'error') { - throw new Error(event.error) - } - }, context) - - return fullResponse -} diff --git a/packages/cli/src/core/api.ts b/packages/cli/src/core/api.ts new file mode 100644 index 00000000..3f80a147 --- /dev/null +++ b/packages/cli/src/core/api.ts @@ -0,0 +1,43 @@ +import { readConfig, readToken, getBaseURL, TokenInfo } from '../utils/store' + +export type ApiError = { + status: number + message: string +} + +export const apiRequest = async <T>( + path: string, + options: RequestInit = {}, + tokenOverride?: TokenInfo | null +): Promise<T> => { + const config = readConfig() + const baseURL = getBaseURL(config) + const token = tokenOverride ?? readToken() + + const headers = new Headers(options.headers || {}) + headers.set('Content-Type', 'application/json') + if (token?.access_token) { + headers.set('Authorization', `Bearer ${token.access_token}`) + } + + const resp = await fetch(`${baseURL}${path}`, { + ...options, + headers, + }) + if (!resp.ok) { + let message = resp.statusText + try { + const data = await resp.json() + if (data?.message) message = data.message + } catch { + // ignore + } + const err: ApiError = { status: resp.status, message } + throw err + } + if (resp.status === 204) { + return null as T + } + return (await resp.json()) as T +} + diff --git a/packages/cli/src/core/auth.ts b/packages/cli/src/core/auth.ts deleted file mode 100644 index 69a44d0c..00000000 --- a/packages/cli/src/core/auth.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { createClient } from './client' -import { getContext, type MemohContext } from './context' - -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 Memoh API (sync version for file storage) - * @param params - Login parameters - * @param context - Optional context - */ -export async function login(params: LoginParams, context?: MemohContext): Promise<LoginResult> { - const client = createClient(context) - - 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; id: string } } } | null - - if (data?.success && data?.data?.token && data?.data?.user) { - const ctx = context || getContext() - const storage = ctx.storage - - // Set token (handle both sync and async) - const setResult = storage.setToken(data.data.token, ctx.currentUserId) - if (setResult instanceof Promise) { - await setResult - } - - return { - success: true, - token: data.data.token, - user: data.data.user as UserInfo, - } - } - - throw new Error('Invalid response format') -} - -/** - * Logout current user - * @param context - Optional context - */ -export function logout(context?: MemohContext): void { - const ctx = context || getContext() - const storage = ctx.storage - - const result = storage.clearToken(ctx.currentUserId) - if (result instanceof Promise) { - throw new Error('logout does not support async storage. Use logoutAsync instead.') - } -} - - -/** - * Check if user is logged in - * @param context - Optional context - */ -export function isLoggedIn(context?: MemohContext): boolean { - const ctx = context || getContext() - const storage = ctx.storage - - const token = storage.getToken(ctx.currentUserId) - - if (token instanceof Promise) { - throw new Error('isLoggedIn does not support async storage. Use isLoggedInAsync instead.') - } - - return token !== null -} - -/** - * Get current logged in user info - * @param context - Optional context - */ -export async function getCurrentUser(context?: MemohContext): Promise<UserInfo> { - const ctx = context || getContext() - const storage = ctx.storage - - const token = storage.getToken(ctx.currentUserId) - - if (token instanceof Promise) { - throw new Error('getCurrentUser does not support async storage. Use getCurrentUserAsync instead.') - } - - if (!token) { - throw new Error('Not logged in') - } - - const client = createClient(context) - 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 - * @param context - Optional context - */ -export function getConfig(context?: MemohContext): ConfigInfo { - const ctx = context || getContext() - const storage = ctx.storage - - const apiUrl = storage.getApiUrl() - const token = storage.getToken(ctx.currentUserId) - - if (apiUrl instanceof Promise || token instanceof Promise) { - throw new Error('getConfig does not support async storage. Use getConfigAsync instead.') - } - - return { - apiUrl: apiUrl as string, - loggedIn: token !== null, - } -} - -/** - * Set API URL - * @param url - API URL - * @param context - Optional context - */ -export function setConfig(url: string, context?: MemohContext): void { - const ctx = context || getContext() - const storage = ctx.storage - - const result = storage.setApiUrl(url) - if (result instanceof Promise) { - throw new Error('setConfig does not support async storage. Use setConfigAsync instead.') - } -} - -// Re-export for backward compatibility -export { getToken, getApiUrl } from './client' -export { getContext, setContext, createContext } from './context' diff --git a/packages/cli/src/core/client.ts b/packages/cli/src/core/client.ts deleted file mode 100644 index 47ca47a8..00000000 --- a/packages/cli/src/core/client.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { getContext, type MemohContext } from './context' -import { createClient as createClientApi } from '@memoh/api/client' - -/** - * Create API client - * @param context - Optional context, uses global context if not provided - */ -export function createClient(context?: MemohContext) { - const ctx = context || getContext() - const storage = ctx.storage - - - const apiUrlResult = typeof storage.getApiUrl === 'function' - ? storage.getApiUrl() - : (storage as unknown as Record<string, string>).apiUrl - - - if (apiUrlResult instanceof Promise) { - throw new Error('createClient does not support async storage. Use createClientAsync instead.') - } - - const apiUrl = apiUrlResult as string - - const token = typeof storage.getToken === 'function' - ? storage.getToken(ctx.currentUserId) - : null - - - // Handle async token retrieval - if (token instanceof Promise) { - throw new Error('createClient does not support async token storage. Use createClientAsync instead.') - } - - - const client = createClientApi(apiUrl, token ?? undefined) - - - return client -} - - -/** - * Require authentication - * Throws error if not authenticated - * @param context - Optional context, uses global context if not provided - */ -export function requireAuth(context?: MemohContext): string { - const ctx = context || getContext() - const storage = ctx.storage - - const token = typeof storage.getToken === 'function' - ? storage.getToken(ctx.currentUserId) - : null - - if (token instanceof Promise) { - throw new Error('requireAuth does not support async token storage. Use requireAuthAsync instead.') - } - - if (!token) { - throw new Error('Not logged in. Please login first') - } - - return token -} - -/** - * Get API URL - * @param context - Optional context, uses global context if not provided - */ -export function getApiUrl(context?: MemohContext): string { - const ctx = context || getContext() - const storage = ctx.storage - - const urlResult = typeof storage.getApiUrl === 'function' - ? storage.getApiUrl() - : (storage as unknown as Record<string, string>).apiUrl - - if (urlResult instanceof Promise) { - throw new Error('getApiUrl does not support async storage. Use getApiUrlAsync instead.') - } - - return urlResult as string -} - -/** - * Get token - * @param context - Optional context, uses global context if not provided - */ -export function getToken(context?: MemohContext): string | null { - const ctx = context || getContext() - const storage = ctx.storage - - const token = typeof storage.getToken === 'function' - ? storage.getToken(ctx.currentUserId) - : null - - if (token instanceof Promise) { - throw new Error('getToken does not support async storage. Use getTokenAsync instead.') - } - - return token -} diff --git a/packages/cli/src/core/context.ts b/packages/cli/src/core/context.ts deleted file mode 100644 index fd11bbf2..00000000 --- a/packages/cli/src/core/context.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Memoh Core Context - * - * Provides a configurable context for core functions to use different storage backends - */ - -import type { TokenStorage } from './storage' -import { FileTokenStorage } from './storage/file' - -/** - * Global context for core functions - */ -export interface MemohContext { - storage: TokenStorage - currentUserId?: string -} - -/** - * Default context (uses file storage for CLI) - */ -let defaultContext: MemohContext = { - storage: new FileTokenStorage(), -} - -/** - * Get the current context - */ -export function getContext(): MemohContext { - return defaultContext -} - -/** - * Set the global context - * Use this to configure storage backend (e.g., Redis for Telegram bot) - */ -export function setContext(context: Partial<MemohContext>): void { - defaultContext = { ...defaultContext, ...context } -} - -/** - * Create a new context without modifying the global one - * Useful for multi-user scenarios - */ -export function createContext(options: { - storage: TokenStorage - userId?: string -}): MemohContext { - return { - storage: options.storage, - currentUserId: options.userId, - } -} - -/** - * Reset context to default (file storage) - */ -export function resetContext(): void { - defaultContext = { - storage: new FileTokenStorage(), - } -} - diff --git a/packages/cli/src/core/debug.ts b/packages/cli/src/core/debug.ts deleted file mode 100644 index 4b0f6673..00000000 --- a/packages/cli/src/core/debug.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { getApiUrl, getToken } from './client' -import type { MemohContext } from './context' - -export interface PingResult { - success: boolean - status?: number - message?: string - error?: string -} - -/** - * Test API server connection - * @param context - Optional context, uses global context if not provided - */ -export async function ping(context?: MemohContext): Promise<PingResult> { - const apiUrl = getApiUrl(context) - const token = getToken(context) - - 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 - * @param context - Optional context, uses global context if not provided - */ -export function getConnectionInfo(context?: MemohContext): { - apiUrl: string - hasToken: boolean -} { - return { - apiUrl: getApiUrl(context), - hasToken: getToken(context) !== null, - } -} - diff --git a/packages/cli/src/core/index.ts b/packages/cli/src/core/index.ts index 0b0a16bb..0bf83f78 100644 --- a/packages/cli/src/core/index.ts +++ b/packages/cli/src/core/index.ts @@ -1,138 +1,2 @@ -/** - * Memoh 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.) - */ +export * from './api' -// Context -export { - getContext, - setContext, - createContext, - resetContext, - type MemohContext, -} from './context' - -// Storage -export type { TokenStorage, Config } from './storage' -export { FileTokenStorage } from './storage/' - -// Auth -export { - login, - logout, - isLoggedIn, - getCurrentUser, - getConfig, - setConfig, - 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' - -// Platform -export { - listPlatforms, - createPlatform, - getPlatform, - updatePlatform, - updatePlatformConfig, - deletePlatform, - activatePlatform, - inactivatePlatform, - type CreatePlatformParams, - type PlatformListItem, -} from './platform' - -// Agent -export { - chat, - chatAsync, - chatStream, - chatStreamAsync, - 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' - -// MCP -export { - listMCPConnections, - createMCPConnection, - getMCPConnection, - updateMCPConnection, - deleteMCPConnection, - toggleMCPConnection, - type CreateMCPConnectionParams, - type UpdateMCPConnectionParams, -} from './mcp' - -// Settings -export { - getSettings, - updateSettings, - type UpdateSettingsParams, -} from './settings' - -// Debug -export { - ping, - getConnectionInfo, - type PingResult, -} from './debug' - -// Client -export { - createClient, - requireAuth, - getApiUrl, - getToken, -} from './client' diff --git a/packages/cli/src/core/mcp.ts b/packages/cli/src/core/mcp.ts deleted file mode 100644 index 6b123796..00000000 --- a/packages/cli/src/core/mcp.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { createClient, requireAuth } from './client' -import type { MCPConnection, MCPConnectionConfig } from '../types' - -export interface CreateMCPConnectionParams { - name: string - config: MCPConnectionConfig - active?: boolean -} - -export interface UpdateMCPConnectionParams { - name?: string - config?: MCPConnectionConfig - active?: boolean -} - -/** - * List all MCP connections - */ -export async function listMCPConnections(): Promise<MCPConnection[]> { - requireAuth() - const client = createClient() - - const response = await client.mcp.get() - - if (response.error) { - const errorValue = response.error.value - if (typeof errorValue === 'string') { - throw new Error(errorValue) - } else if (typeof errorValue === 'object' && errorValue !== null) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const errorObj = errorValue as any - const errorMsg = errorObj.error || errorObj.message || JSON.stringify(errorValue) - throw new Error(errorMsg) - } - throw new Error('Failed to fetch MCP connections list') - } - - // 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 MCP connections list') -} - -/** - * Create MCP connection - */ -export async function createMCPConnection(params: CreateMCPConnectionParams): Promise<MCPConnection> { - requireAuth() - const client = createClient() - - const payload = { - name: params.name, - config: params.config, - active: params.active ?? true, - } - - const response = await client.mcp.post(payload) - - if (response.error) { - const errorValue = response.error.value - if (typeof errorValue === 'string') { - throw new Error(errorValue) - } else if (typeof errorValue === 'object' && errorValue !== null) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const errorObj = errorValue as any - const errorMsg = errorObj.error || errorObj.message || JSON.stringify(errorValue) - throw new Error(errorMsg) - } - throw new Error('Failed to create MCP connection') - } - - // 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 MCP connection') -} - -/** - * Get MCP connection by ID - */ -export async function getMCPConnection(id: string): Promise<MCPConnection> { - requireAuth() - const client = createClient() - - const response = await client.mcp({ id }).get() - - if (response.error) { - const errorValue = response.error.value - if (typeof errorValue === 'string') { - throw new Error(errorValue) - } else if (typeof errorValue === 'object' && errorValue !== null) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const errorObj = errorValue as any - const errorMsg = errorObj.error || errorObj.message || JSON.stringify(errorValue) - throw new Error(errorMsg) - } - throw new Error('Failed to fetch MCP connection') - } - - // 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 MCP connection') -} - -/** - * Update MCP connection - */ -export async function updateMCPConnection(id: string, params: UpdateMCPConnectionParams): Promise<void> { - requireAuth() - const client = createClient() - - const response = await client.mcp({ id }).put(params) - - if (response.error) { - const errorValue = response.error.value - if (typeof errorValue === 'string') { - throw new Error(errorValue) - } else if (typeof errorValue === 'object' && errorValue !== null) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const errorObj = errorValue as any - const errorMsg = errorObj.error || errorObj.message || JSON.stringify(errorValue) - throw new Error(errorMsg) - } - throw new Error('Failed to update MCP connection') - } -} - -/** - * Delete MCP connection - */ -export async function deleteMCPConnection(id: string): Promise<void> { - requireAuth() - const client = createClient() - - const response = await client.mcp({ id }).delete() - - if (response.error) { - const errorValue = response.error.value - if (typeof errorValue === 'string') { - throw new Error(errorValue) - } else if (typeof errorValue === 'object' && errorValue !== null) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const errorObj = errorValue as any - const errorMsg = errorObj.error || errorObj.message || JSON.stringify(errorValue) - throw new Error(errorMsg) - } - throw new Error('Failed to delete MCP connection') - } -} - -/** - * Toggle MCP connection active status - */ -export async function toggleMCPConnection(id: string): Promise<boolean> { - requireAuth() - const client = createClient() - - // First get current status - const getResponse = await client.mcp({ id }).get() - - if (getResponse.error) { - const errorValue = getResponse.error.value - if (typeof errorValue === 'string') { - throw new Error(errorValue) - } else if (typeof errorValue === 'object' && errorValue !== null) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const errorObj = errorValue as any - const errorMsg = errorObj.error || errorObj.message || JSON.stringify(errorValue) - throw new Error(errorMsg) - } - throw new Error('Failed to get MCP connection') - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const getData = getResponse.data as any - if (getData?.success && getData?.data) { - const currentActive = getData.data.active - - // Update status - const updateResponse = await client.mcp({ id }).put({ - active: !currentActive, - }) - - if (updateResponse.error) { - const errorValue = updateResponse.error.value - if (typeof errorValue === 'string') { - throw new Error(errorValue) - } else if (typeof errorValue === 'object' && errorValue !== null) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const errorObj = errorValue as any - const errorMsg = errorObj.error || errorObj.message || JSON.stringify(errorValue) - throw new Error(errorMsg) - } - throw new Error('Failed to update MCP connection') - } - - return !currentActive - } - - throw new Error('Failed to toggle MCP connection status') -} - diff --git a/packages/cli/src/core/memory.ts b/packages/cli/src/core/memory.ts deleted file mode 100644 index 56aeb721..00000000 --- a/packages/cli/src/core/memory.ts +++ /dev/null @@ -1,125 +0,0 @@ -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') -} - diff --git a/packages/cli/src/core/model.ts b/packages/cli/src/core/model.ts deleted file mode 100644 index 8cc735ce..00000000 --- a/packages/cli/src/core/model.ts +++ /dev/null @@ -1,150 +0,0 @@ -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 -} - diff --git a/packages/cli/src/core/platform.ts b/packages/cli/src/core/platform.ts deleted file mode 100644 index de1a74de..00000000 --- a/packages/cli/src/core/platform.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { createClient, requireAuth } from './client' -import type { Platform, ApiResponse } from '../types' - -export interface CreatePlatformParams { - name: string - config: Record<string, unknown> - active?: boolean -} - -export interface PlatformListItem { - id: string - name: string - config: Record<string, unknown> - active: boolean - createdAt: string - updatedAt: string -} - -/** - * List all platforms - */ -export async function listPlatforms(): Promise<PlatformListItem[]> { - requireAuth() - const client = createClient() - - const response = await client.platform.get() - - if (response.error) { - throw new Error(response.error.value) - } - - const data = response.data as { success?: boolean; items?: PlatformListItem[] } | null - if (data?.success && data?.items) { - return data.items - } - - throw new Error('Failed to fetch platform list') -} - -/** - * Create platform configuration - */ -export async function createPlatform(params: CreatePlatformParams): Promise<Platform> { - requireAuth() - const client = createClient() - - const payload: Record<string, unknown> = { - name: params.name, - config: params.config, - active: params.active ?? true, - } - - const response = await client.platform.post(payload) - - if (response.error) { - throw new Error(response.error.value) - } - - const data = response.data as ApiResponse<Platform> | null - if (data?.success && data?.data) { - return data.data - } - - throw new Error('Failed to create platform configuration') -} - -/** - * Get platform by ID - */ -export async function getPlatform(id: string): Promise<Platform> { - requireAuth() - const client = createClient() - - const response = await client.platform({ id }).get() - - if (response.error) { - throw new Error(response.error.value) - } - - const data = response.data as ApiResponse<Platform> | null - if (data?.success && data?.data) { - return data.data - } - - throw new Error('Failed to fetch platform configuration') -} - -/** - * Update platform - */ -export async function updatePlatform(id: string, params: Partial<CreatePlatformParams>): Promise<Platform> { - requireAuth() - const client = createClient() - - const response = await client.platform({ id }).put(params) - - if (response.error) { - throw new Error(response.error.value) - } - - const data = response.data as ApiResponse<Platform> | null - if (data?.success && data?.data) { - return data.data - } - - throw new Error('Failed to update platform configuration') -} - -/** - * Update platform config - */ -export async function updatePlatformConfig(id: string, config: Record<string, unknown>): Promise<Platform> { - requireAuth() - const client = createClient() - - const response = await client.platform({ id }).config.put({ config }) - - if (response.error) { - throw new Error(response.error.value) - } - - const data = response.data as ApiResponse<Platform> | null - if (data?.success && data?.data) { - return data.data - } - - throw new Error('Failed to update platform config') -} - -/** - * Delete platform - */ -export async function deletePlatform(id: string): Promise<void> { - requireAuth() - const client = createClient() - - const response = await client.platform({ id }).delete() - - if (response.error) { - throw new Error(response.error.value) - } -} - -/** - * Activate platform - */ -export async function activatePlatform(id: string): Promise<Platform> { - requireAuth() - const client = createClient() - - const response = await client.platform({ id }).active.post() - - if (response.error) { - throw new Error(response.error.value) - } - - const data = response.data as ApiResponse<Platform> | null - if (data?.success && data?.data) { - return data.data - } - - throw new Error('Failed to activate platform') -} - -/** - * Inactivate platform - */ -export async function inactivatePlatform(id: string): Promise<Platform> { - requireAuth() - const client = createClient() - - const response = await client.platform({ id }).inactive.post() - - if (response.error) { - throw new Error(response.error.value) - } - - const data = response.data as ApiResponse<Platform> | null - if (data?.success && data?.data) { - return data.data - } - - throw new Error('Failed to inactivate platform') -} - diff --git a/packages/cli/src/core/schedule.ts b/packages/cli/src/core/schedule.ts deleted file mode 100644 index 979fd200..00000000 --- a/packages/cli/src/core/schedule.ts +++ /dev/null @@ -1,156 +0,0 @@ -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') -} - diff --git a/packages/cli/src/core/settings.ts b/packages/cli/src/core/settings.ts deleted file mode 100644 index 0abb8c1d..00000000 --- a/packages/cli/src/core/settings.ts +++ /dev/null @@ -1,53 +0,0 @@ -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') - } -} - diff --git a/packages/cli/src/core/storage.ts b/packages/cli/src/core/storage.ts deleted file mode 100644 index b1ed80bb..00000000 --- a/packages/cli/src/core/storage.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Token Storage Interface - * - * Abstraction for storing authentication tokens in different backends - */ - -export interface Config { - apiUrl: string - token?: string -} - -export interface TokenStorage { - /** - * Get the API URL - */ - getApiUrl(): Promise<string> | string - - /** - * Set the API URL - */ - setApiUrl(url: string): Promise<void> | void - - /** - * Get the authentication token for a user - * @param userId - User identifier (optional for single-user storage) - */ - getToken(userId?: string): Promise<string | null> | string | null - - /** - * Set the authentication token for a user - * @param token - The authentication token - * @param userId - User identifier (optional for single-user storage) - */ - setToken(token: string, userId?: string): Promise<void> | void - - /** - * Clear the authentication token for a user - * @param userId - User identifier (optional for single-user storage) - */ - clearToken(userId?: string): Promise<void> | void - - /** - * Load full configuration (if applicable) - */ - loadConfig?(): Promise<Config> | Config - - /** - * Save full configuration (if applicable) - */ - saveConfig?(config: Config): Promise<void> | void -} - diff --git a/packages/cli/src/core/storage/file.ts b/packages/cli/src/core/storage/file.ts deleted file mode 100644 index 43c7c9d7..00000000 --- a/packages/cli/src/core/storage/file.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { homedir } from 'os' -import { join } from 'path' -import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs' -import type { TokenStorage, Config } from '../storage' - -const CONFIG_DIR = join(homedir(), '.memoh') -const CONFIG_FILE = join(CONFIG_DIR, 'config.json') - -const DEFAULT_CONFIG: Config = { - apiUrl: process.env.API_BASE_URL || 'http://localhost:7002', -} - -/** - * File-based token storage for CLI - * Stores config in ~/.memoh/config.json - */ -export class FileTokenStorage implements TokenStorage { - private ensureConfigDir() { - if (!existsSync(CONFIG_DIR)) { - mkdirSync(CONFIG_DIR, { recursive: true }) - } - } - - loadConfig(): Config { - this.ensureConfigDir() - - if (!existsSync(CONFIG_FILE)) { - this.saveConfig(DEFAULT_CONFIG) - return DEFAULT_CONFIG - } - - try { - const data = readFileSync(CONFIG_FILE, 'utf-8') - return { ...DEFAULT_CONFIG, ...JSON.parse(data) } - } catch { - return DEFAULT_CONFIG - } - } - - saveConfig(config: Config): void { - this.ensureConfigDir() - writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)) - } - - getApiUrl(): string { - const config = this.loadConfig() - return config.apiUrl - } - - setApiUrl(url: string): void { - const config = this.loadConfig() - config.apiUrl = url - this.saveConfig(config) - } - - getToken(): string | null { - const config = this.loadConfig() - return config.token || null - } - - setToken(token: string): void { - const config = this.loadConfig() - config.token = token - this.saveConfig(config) - } - - clearToken(): void { - const config = this.loadConfig() - delete config.token - this.saveConfig(config) - } -} - diff --git a/packages/cli/src/core/storage/index.ts b/packages/cli/src/core/storage/index.ts deleted file mode 100644 index b9dcc88d..00000000 --- a/packages/cli/src/core/storage/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { FileTokenStorage } from './file' -export type { TokenStorage, Config } from '../storage' diff --git a/packages/cli/src/core/user.ts b/packages/cli/src/core/user.ts deleted file mode 100644 index 5f5b908b..00000000 --- a/packages/cli/src/core/user.ts +++ /dev/null @@ -1,114 +0,0 @@ -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) - } -} - diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 537cb586..d5500857 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,36 +1,4 @@ -/** - * Memoh CLI Package - * - * This package provides both: - * 1. A command-line interface (CLI) for interacting with Memoh API - * 2. Core functionality that can be imported and used in other projects - * - * @example CLI Usage (from terminal) - * ```bash - * memoh auth login - * memoh agent chat "Hello" - * ``` - * - * @example Core API Usage (from code) - * ```typescript - * import { login, chat, listModels } from '@memoh/cli' - * - * // Login - * await login({ username: 'admin', password: 'password' }) - * - * // Chat with agent - * const response = await chat({ message: 'Hello' }) - * - * // List models - * const models = await listModels() - * ``` - */ +export * from './core/index' +export * from './types/index' +export * from './utils/index' -// Export all core functionality -export * from './core' - -// Export types -export * from './types' - -// Export utilities -export * from './utils' diff --git a/packages/cli/src/types/index.ts b/packages/cli/src/types/index.ts index 9fb8985b..34394946 100644 --- a/packages/cli/src/types/index.ts +++ b/packages/cli/src/types/index.ts @@ -1,3 +1,4 @@ +export type { CliConfig, TokenInfo } from '../utils/store' // API response type definitions export interface ApiResponse<T = unknown> { diff --git a/packages/cli/src/utils/index.ts b/packages/cli/src/utils/index.ts index f3d0bab3..16c86332 100644 --- a/packages/cli/src/utils/index.ts +++ b/packages/cli/src/utils/index.ts @@ -1,40 +1 @@ -/** - * Format API error information - */ -export function formatError(error: unknown): string { - if (error === null || error === undefined) { - return 'Unknown error' - } - - if (typeof error === 'string') { - return error - } - - if (typeof error === 'object') { - // Try to extract common error fields - const errorObj = error as Record<string, unknown> - - if ('message' in errorObj && typeof errorObj.message === 'string') { - return errorObj.message - } - - if ('error' in errorObj && typeof errorObj.error === 'string') { - return errorObj.error - } - - // If status and statusText exist - if ('status' in errorObj && 'statusText' in errorObj) { - return `${errorObj.status} ${errorObj.statusText}` - } - - // Otherwise return formatted JSON - try { - return JSON.stringify(error, null, 2) - } catch { - return String(error) - } - } - - return String(error) -} - +export * from './store' diff --git a/packages/cli/src/utils/store.ts b/packages/cli/src/utils/store.ts new file mode 100644 index 00000000..7bf277b6 --- /dev/null +++ b/packages/cli/src/utils/store.ts @@ -0,0 +1,98 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { homedir } from 'node:os' +import { join } from 'node:path' + +export type CliConfig = { + host: string + port: number +} + +export type TokenInfo = { + access_token: string + token_type: string + expires_at: string + user_id: string + username?: string +} + +const defaultConfig: CliConfig = { + host: '127.0.0.1', + port: 8080, +} + +const memohDir = () => join(homedir(), '.memoh') +const configPath = () => join(memohDir(), 'config.toml') +const tokenPath = () => join(memohDir(), 'token.json') + +export const ensureStore = () => { + const dir = memohDir() + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }) + } +} + +const parseTomlConfig = (raw: string): CliConfig => { + const result: CliConfig = { ...defaultConfig } + const lines = raw.split(/\r?\n/) + for (const line of lines) { + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) continue + const match = trimmed.match(/^(\w+)\s*=\s*"?([^"]+)"?$/) + if (!match) continue + const key = match[1] + const value = match[2] + if (key === 'host') { + result.host = value + } else if (key === 'port') { + const parsed = Number.parseInt(value, 10) + if (!Number.isNaN(parsed)) result.port = parsed + } + } + return result +} + +const serializeTomlConfig = (config: CliConfig) => { + return `host = "${config.host}"\nport = ${config.port}\n` +} + +export const readConfig = (): CliConfig => { + ensureStore() + const path = configPath() + if (!existsSync(path)) { + writeFileSync(path, serializeTomlConfig(defaultConfig), 'utf-8') + return { ...defaultConfig } + } + const raw = readFileSync(path, 'utf-8') + return parseTomlConfig(raw) +} + +export const writeConfig = (config: CliConfig) => { + ensureStore() + writeFileSync(configPath(), serializeTomlConfig(config), 'utf-8') +} + +export const readToken = (): TokenInfo | null => { + ensureStore() + if (!existsSync(tokenPath())) return null + try { + const raw = readFileSync(tokenPath(), 'utf-8') + return JSON.parse(raw) as TokenInfo + } catch { + return null + } +} + +export const writeToken = (token: TokenInfo) => { + ensureStore() + writeFileSync(tokenPath(), JSON.stringify(token, null, 2), 'utf-8') +} + +export const clearToken = () => { + ensureStore() + writeFileSync(tokenPath(), '', 'utf-8') +} + +export const getBaseURL = (config: CliConfig) => { + return `http://${config.host}:${config.port}` +} +