mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
refactor: cli
This commit is contained in:
@@ -5,7 +5,7 @@ import ora from 'ora'
|
|||||||
import { table } from 'table'
|
import { table } from 'table'
|
||||||
import * as modelCore from '../../core/model'
|
import * as modelCore from '../../core/model'
|
||||||
import { formatError } from '../../utils'
|
import { formatError } from '../../utils'
|
||||||
import { getApiUrl } from '../../core/config'
|
import { getApiUrl } from '../../core/client'
|
||||||
|
|
||||||
export function modelCommands(program: Command) {
|
export function modelCommands(program: Command) {
|
||||||
program
|
program
|
||||||
|
|||||||
Regular → Executable
@@ -1,4 +1,5 @@
|
|||||||
import { requireAuth, getToken, getApiUrl } from './client'
|
import { requireAuth, getToken, getApiUrl } from './client'
|
||||||
|
import type { MemoHomeContext } from './context'
|
||||||
|
|
||||||
export interface ChatParams {
|
export interface ChatParams {
|
||||||
message: string
|
message: string
|
||||||
@@ -16,16 +17,48 @@ export interface StreamEvent {
|
|||||||
export type StreamCallback = (event: StreamEvent) => void | Promise<void>
|
export type StreamCallback = (event: StreamEvent) => void | Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chat with AI Agent (streaming)
|
* Chat with AI Agent (streaming) - sync version
|
||||||
*/
|
*/
|
||||||
export async function chatStream(
|
export async function chatStream(
|
||||||
|
params: ChatParams,
|
||||||
|
onEvent: StreamCallback,
|
||||||
|
context?: MemoHomeContext
|
||||||
|
): 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?: MemoHomeContext
|
||||||
|
): 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,
|
params: ChatParams,
|
||||||
onEvent: StreamCallback
|
onEvent: StreamCallback
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
requireAuth()
|
|
||||||
const token = getToken()!
|
|
||||||
const apiUrl = getApiUrl()
|
|
||||||
|
|
||||||
const response = await fetch(`${apiUrl}/agent/stream`, {
|
const response = await fetch(`${apiUrl}/agent/stream`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -91,9 +124,9 @@ export async function chatStream(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chat with AI Agent (non-streaming, collect full response)
|
* Chat with AI Agent (non-streaming, collect full response) - sync version
|
||||||
*/
|
*/
|
||||||
export async function chat(params: ChatParams): Promise<string> {
|
export async function chat(params: ChatParams, context?: MemoHomeContext): Promise<string> {
|
||||||
let fullResponse = ''
|
let fullResponse = ''
|
||||||
|
|
||||||
await chatStream(params, async (event) => {
|
await chatStream(params, async (event) => {
|
||||||
@@ -102,8 +135,24 @@ export async function chat(params: ChatParams): Promise<string> {
|
|||||||
} else if (event.type === 'error') {
|
} else if (event.type === 'error') {
|
||||||
throw new Error(event.error)
|
throw new Error(event.error)
|
||||||
}
|
}
|
||||||
})
|
}, context)
|
||||||
|
|
||||||
return fullResponse
|
return fullResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chat with AI Agent (non-streaming, collect full response) - async version
|
||||||
|
*/
|
||||||
|
export async function chatAsync(params: ChatParams, context?: MemoHomeContext): 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createClient } from './client'
|
import { createClient } from './client'
|
||||||
import { setToken, clearToken, getToken, getApiUrl, setApiUrl } from './config'
|
import { getContext, type MemoHomeContext } from './context'
|
||||||
|
|
||||||
export interface LoginParams {
|
export interface LoginParams {
|
||||||
username: string
|
username: string
|
||||||
@@ -28,10 +28,12 @@ export interface ConfigInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login to MemoHome API
|
* Login to MemoHome API (sync version for file storage)
|
||||||
|
* @param params - Login parameters
|
||||||
|
* @param context - Optional context
|
||||||
*/
|
*/
|
||||||
export async function login(params: LoginParams): Promise<LoginResult> {
|
export async function login(params: LoginParams, context?: MemoHomeContext): Promise<LoginResult> {
|
||||||
const client = createClient()
|
const client = createClient(context)
|
||||||
|
|
||||||
const response = await client.auth.login.post({
|
const response = await client.auth.login.post({
|
||||||
username: params.username,
|
username: params.username,
|
||||||
@@ -42,10 +44,18 @@ export async function login(params: LoginParams): Promise<LoginResult> {
|
|||||||
throw new Error(response.error.value)
|
throw new Error(response.error.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = response.data as { success?: boolean; data?: { token?: string; user?: { username: string; role: string } } } | null
|
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) {
|
if (data?.success && data?.data?.token && data?.data?.user) {
|
||||||
setToken(data.data.token)
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
token: data.data.token,
|
token: data.data.token,
|
||||||
@@ -58,28 +68,55 @@ export async function login(params: LoginParams): Promise<LoginResult> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Logout current user
|
* Logout current user
|
||||||
|
* @param context - Optional context
|
||||||
*/
|
*/
|
||||||
export function logout(): void {
|
export function logout(context?: MemoHomeContext): void {
|
||||||
clearToken()
|
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
|
* Check if user is logged in
|
||||||
|
* @param context - Optional context
|
||||||
*/
|
*/
|
||||||
export function isLoggedIn(): boolean {
|
export function isLoggedIn(context?: MemoHomeContext): boolean {
|
||||||
return getToken() !== null
|
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
|
* Get current logged in user info
|
||||||
|
* @param context - Optional context
|
||||||
*/
|
*/
|
||||||
export async function getCurrentUser(): Promise<UserInfo> {
|
export async function getCurrentUser(context?: MemoHomeContext): Promise<UserInfo> {
|
||||||
const token = getToken()
|
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) {
|
if (!token) {
|
||||||
throw new Error('Not logged in')
|
throw new Error('Not logged in')
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = createClient()
|
const client = createClient(context)
|
||||||
const response = await client.auth.me.get()
|
const response = await client.auth.me.get()
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
@@ -97,21 +134,40 @@ export async function getCurrentUser(): Promise<UserInfo> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current API configuration
|
* Get current API configuration
|
||||||
|
* @param context - Optional context
|
||||||
*/
|
*/
|
||||||
export function getConfig(): ConfigInfo {
|
export function getConfig(context?: MemoHomeContext): 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 {
|
return {
|
||||||
apiUrl: getApiUrl(),
|
apiUrl: apiUrl as string,
|
||||||
loggedIn: isLoggedIn(),
|
loggedIn: token !== null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set API URL
|
* Set API URL
|
||||||
|
* @param url - API URL
|
||||||
|
* @param context - Optional context
|
||||||
*/
|
*/
|
||||||
export function setConfig(apiUrl: string): void {
|
export function setConfig(url: string, context?: MemoHomeContext): void {
|
||||||
setApiUrl(apiUrl)
|
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 config functions for convenience
|
// Re-export for backward compatibility
|
||||||
export { getToken, getApiUrl, setToken, clearToken, setApiUrl }
|
export { getToken, getApiUrl } from './client'
|
||||||
|
export { getContext, setContext, createContext } from './context'
|
||||||
|
|||||||
@@ -1,29 +1,97 @@
|
|||||||
import { treaty } from '@elysiajs/eden'
|
import { getContext, type MemoHomeContext } from './context'
|
||||||
import { getApiUrl, getToken } from './config'
|
import { createClient as createClientApi } from '@memohome/api/client'
|
||||||
|
|
||||||
// Use dynamic import to avoid type errors
|
/**
|
||||||
export function createClient() {
|
* Create API client
|
||||||
const apiUrl = getApiUrl()
|
* @param context - Optional context, uses global context if not provided
|
||||||
const token = getToken()
|
*/
|
||||||
|
export function createClient(context?: MemoHomeContext) {
|
||||||
|
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
|
||||||
|
|
||||||
// Eden Treaty configuration
|
// Handle async token retrieval
|
||||||
const client = treaty(apiUrl, {
|
if (token instanceof Promise) {
|
||||||
headers: token ? {
|
throw new Error('createClient does not support async token storage. Use createClientAsync instead.')
|
||||||
'Authorization': `Bearer ${token}`,
|
}
|
||||||
} : undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const client = createClientApi(apiUrl, token ?? undefined)
|
||||||
return client as any
|
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requireAuth(): string {
|
|
||||||
const token = getToken()
|
/**
|
||||||
if (!token) {
|
* Require authentication
|
||||||
throw new Error('Not logged in. Please use "memohome auth login" to login first')
|
* Throws error if not authenticated
|
||||||
|
* @param context - Optional context, uses global context if not provided
|
||||||
|
*/
|
||||||
|
export function requireAuth(context?: MemoHomeContext): 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
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getApiUrl, getToken }
|
/**
|
||||||
|
* Get API URL
|
||||||
|
* @param context - Optional context, uses global context if not provided
|
||||||
|
*/
|
||||||
|
export function getApiUrl(context?: MemoHomeContext): 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?: MemoHomeContext): 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { homedir } from 'os'
|
|
||||||
import { join } from 'path'
|
|
||||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
|
|
||||||
|
|
||||||
const CONFIG_DIR = join(homedir(), '.memohome')
|
|
||||||
const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
|
|
||||||
|
|
||||||
export interface Config {
|
|
||||||
apiUrl: string
|
|
||||||
token?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG: Config = {
|
|
||||||
apiUrl: process.env.API_BASE_URL || 'http://localhost:7002',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ensureConfigDir() {
|
|
||||||
if (!existsSync(CONFIG_DIR)) {
|
|
||||||
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function loadConfig(): Config {
|
|
||||||
ensureConfigDir()
|
|
||||||
|
|
||||||
if (!existsSync(CONFIG_FILE)) {
|
|
||||||
saveConfig(DEFAULT_CONFIG)
|
|
||||||
return DEFAULT_CONFIG
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = readFileSync(CONFIG_FILE, 'utf-8')
|
|
||||||
return { ...DEFAULT_CONFIG, ...JSON.parse(data) }
|
|
||||||
} catch {
|
|
||||||
return DEFAULT_CONFIG
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveConfig(config: Config) {
|
|
||||||
ensureConfigDir()
|
|
||||||
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getToken(): string | null {
|
|
||||||
const config = loadConfig()
|
|
||||||
return config.token || null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setToken(token: string) {
|
|
||||||
const config = loadConfig()
|
|
||||||
config.token = token
|
|
||||||
saveConfig(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearToken() {
|
|
||||||
const config = loadConfig()
|
|
||||||
delete config.token
|
|
||||||
saveConfig(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getApiUrl(): string {
|
|
||||||
const config = loadConfig()
|
|
||||||
return config.apiUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setApiUrl(url: string) {
|
|
||||||
const config = loadConfig()
|
|
||||||
config.apiUrl = url
|
|
||||||
saveConfig(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* MemoHome 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 MemoHomeContext {
|
||||||
|
storage: TokenStorage
|
||||||
|
currentUserId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default context (uses file storage for CLI)
|
||||||
|
*/
|
||||||
|
let defaultContext: MemoHomeContext = {
|
||||||
|
storage: new FileTokenStorage(),
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current context
|
||||||
|
*/
|
||||||
|
export function getContext(): MemoHomeContext {
|
||||||
|
return defaultContext
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the global context
|
||||||
|
* Use this to configure storage backend (e.g., Redis for Telegram bot)
|
||||||
|
*/
|
||||||
|
export function setContext(context: Partial<MemoHomeContext>): 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
|
||||||
|
}): MemoHomeContext {
|
||||||
|
return {
|
||||||
|
storage: options.storage,
|
||||||
|
currentUserId: options.userId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset context to default (file storage)
|
||||||
|
*/
|
||||||
|
export function resetContext(): void {
|
||||||
|
defaultContext = {
|
||||||
|
storage: new FileTokenStorage(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getApiUrl, getToken } from './config'
|
import { getApiUrl, getToken } from './client'
|
||||||
|
import type { MemoHomeContext } from './context'
|
||||||
|
|
||||||
export interface PingResult {
|
export interface PingResult {
|
||||||
success: boolean
|
success: boolean
|
||||||
@@ -9,10 +10,11 @@ export interface PingResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Test API server connection
|
* Test API server connection
|
||||||
|
* @param context - Optional context, uses global context if not provided
|
||||||
*/
|
*/
|
||||||
export async function ping(): Promise<PingResult> {
|
export async function ping(context?: MemoHomeContext): Promise<PingResult> {
|
||||||
const apiUrl = getApiUrl()
|
const apiUrl = getApiUrl(context)
|
||||||
const token = getToken()
|
const token = getToken(context)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
@@ -63,14 +65,15 @@ export async function ping(): Promise<PingResult> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get connection info
|
* Get connection info
|
||||||
|
* @param context - Optional context, uses global context if not provided
|
||||||
*/
|
*/
|
||||||
export function getConnectionInfo(): {
|
export function getConnectionInfo(context?: MemoHomeContext): {
|
||||||
apiUrl: string
|
apiUrl: string
|
||||||
hasToken: boolean
|
hasToken: boolean
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
apiUrl: getApiUrl(),
|
apiUrl: getApiUrl(context),
|
||||||
hasToken: getToken() !== null,
|
hasToken: getToken(context) !== null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,19 @@
|
|||||||
* All functions are independent of CLI-specific UI concerns (no chalk, ora, inquirer, etc.)
|
* All functions are independent of CLI-specific UI concerns (no chalk, ora, inquirer, etc.)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Context
|
||||||
|
export {
|
||||||
|
getContext,
|
||||||
|
setContext,
|
||||||
|
createContext,
|
||||||
|
resetContext,
|
||||||
|
type MemoHomeContext,
|
||||||
|
} from './context'
|
||||||
|
|
||||||
|
// Storage
|
||||||
|
export type { TokenStorage, Config } from './storage'
|
||||||
|
export { FileTokenStorage } from './storage/'
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
export {
|
export {
|
||||||
login,
|
login,
|
||||||
@@ -13,11 +26,6 @@ export {
|
|||||||
getCurrentUser,
|
getCurrentUser,
|
||||||
getConfig,
|
getConfig,
|
||||||
setConfig,
|
setConfig,
|
||||||
getToken,
|
|
||||||
getApiUrl,
|
|
||||||
setToken,
|
|
||||||
clearToken,
|
|
||||||
setApiUrl,
|
|
||||||
type LoginParams,
|
type LoginParams,
|
||||||
type LoginResult,
|
type LoginResult,
|
||||||
type UserInfo,
|
type UserInfo,
|
||||||
@@ -49,7 +57,9 @@ export {
|
|||||||
// Agent
|
// Agent
|
||||||
export {
|
export {
|
||||||
chat,
|
chat,
|
||||||
|
chatAsync,
|
||||||
chatStream,
|
chatStream,
|
||||||
|
chatStreamAsync,
|
||||||
type ChatParams,
|
type ChatParams,
|
||||||
type StreamEvent,
|
type StreamEvent,
|
||||||
type StreamCallback,
|
type StreamCallback,
|
||||||
@@ -93,17 +103,10 @@ export {
|
|||||||
type PingResult,
|
type PingResult,
|
||||||
} from './debug'
|
} from './debug'
|
||||||
|
|
||||||
// Config
|
|
||||||
export {
|
|
||||||
loadConfig,
|
|
||||||
saveConfig,
|
|
||||||
ensureConfigDir,
|
|
||||||
type Config,
|
|
||||||
} from './config'
|
|
||||||
|
|
||||||
// Client
|
// Client
|
||||||
export {
|
export {
|
||||||
createClient,
|
createClient,
|
||||||
requireAuth,
|
requireAuth,
|
||||||
|
getApiUrl,
|
||||||
|
getToken,
|
||||||
} from './client'
|
} from './client'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
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(), '.memohome')
|
||||||
|
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 ~/.memohome/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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { FileTokenStorage } from './file'
|
||||||
|
export type { TokenStorage, Config } from '../storage'
|
||||||
Reference in New Issue
Block a user