diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 75edc97f..00000000 --- a/.dockerignore +++ /dev/null @@ -1,45 +0,0 @@ -# Dependencies -node_modules - -# Build output -dist - -# Git -.git -.gitignore - -# IDE -.idea -.vscode -*.swp -*.swo - -# OS -.DS_Store -Thumbs.db - -# Logs -*.log -logs - -# Environment files -.env -.env.* - -# Docker -Dockerfile -.dockerignore -docker-compose*.yml - -# Documentation -README.md -docs - -# Tests -*.test.ts -*.spec.ts -__tests__ -coverage - -# Context directory -.context diff --git a/.env.example b/.env.example index ea1360bb..14ed6161 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,15 @@ -# Telegram Bot -# Get a token from @BotFather on Telegram -TELEGRAM_BOT_TOKEN= +# Database +DATABASE_URL=postgres://multica:multica@localhost:5432/multica?sslmode=disable -# Optional: webhook secret token for production -# TELEGRAM_WEBHOOK_SECRET_TOKEN= +# Server +PORT=8080 +JWT_SECRET=change-me-in-production -# Optional: webhook URL (if not set, uses long-polling mode for local dev) -# TELEGRAM_WEBHOOK_URL=https://your-domain.ngrok-free.dev/telegram/webhook +# Google OAuth +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +GOOGLE_REDIRECT_URI=http://localhost:3000/auth/callback + +# Frontend +NEXT_PUBLIC_API_URL=http://localhost:8080 +NEXT_PUBLIC_WS_URL=ws://localhost:8080/ws diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 979e08a6..033c78a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,28 +11,7 @@ concurrency: cancel-in-progress: true jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 22 - cache: pnpm - - - name: Install dependencies - run: pnpm install - - - name: Lint - run: pnpm --filter @multica/web --filter @multica/desktop lint - - build-and-typecheck: + frontend: runs-on: ubuntu-latest steps: - name: Checkout @@ -61,3 +40,21 @@ jobs: - name: Build, type check, and test run: pnpm turbo build typecheck test + + backend: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: "1.24" + cache-dependency-path: server/go.sum + + - name: Build + run: cd server && go build ./... + + - name: Test + run: cd server && go test ./... diff --git a/.gitignore b/.gitignore index 275c39b1..1c03660d 100644 --- a/.gitignore +++ b/.gitignore @@ -9,25 +9,22 @@ out .turbo build bin -dist-electron -release *.tsbuildinfo # env .env* !.env.example -!apps/desktop/.env.production -!apps/desktop/.env.development + +# test coverage +coverage + +# Go +server/bin/ +server/tmp/ + +# context (agent workspace) +.context # platform specific *.dmg *.app -*.apk -*.ipa -monorepo.md - -# python -__pycache__ - -# test coverage -coverage diff --git a/AGENTS.md b/AGENTS.md deleted file mode 120000 index 681311eb..00000000 --- a/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 588ac849..f6e7cd23 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,63 +4,56 @@ This file gives coding agents high-signal guidance for this repository. ## 1. Project Context -Super Multica is a distributed AI agent framework/product monorepo. -It is used to run local-first agent workflows and support CLI/Desktop/Web/Gateway-based usage. +Multica is an AI-native task management platform — like Linear, but with AI agents as first-class citizens. -Core purpose: +- Agents can be assigned issues, create issues, comment, and change status +- Supports local (daemon) and cloud agent runtimes +- Built for 2-10 person AI-native teams -- execute agent tasks with tools and skills -- persist sessions/profiles/credentials across runs -- support development, testing, and operational automation workflows +## 2. Architecture -## 2. Documentation Scope +**Polyglot monorepo** — Go backend + TypeScript frontend. -Documentation in this repo should prioritize: - -1. Development workflow -2. Testing methods -3. Operational process - -Architecture explanations should stay minimal in docs. -Treat source code as the architecture source of truth. +- `server/` — Go backend (Chi + sqlc + gorilla/websocket) +- `apps/web/` — Next.js 16 frontend +- `packages/` — Shared TypeScript packages (ui, types, sdk, store, hooks, utils) ## 3. Core Workflow Commands ```bash +# Frontend pnpm install -pnpm multica -pnpm multica run "" -pnpm dev -pnpm dev:gateway -pnpm dev:web -pnpm dev:local -pnpm build -pnpm typecheck -pnpm test +pnpm dev:web # Next.js dev server +pnpm build # Build all TS packages +pnpm typecheck # TypeScript check +pnpm test # TS tests + +# Backend (Go) +make dev # Run Go server with hot-reload +make daemon # Run local daemon +make test # Go tests +make sqlc # Regenerate sqlc code +make migrate-up # Run database migrations +make migrate-down # Rollback migrations + +# Infrastructure +docker compose up -d # Start PostgreSQL ``` -## 4. Data and Credentials Workflow - -- Default data dir: `~/.super-multica` (override with `SMC_DATA_DIR`) -- Credentials: `~/.super-multica/credentials.json5` (override with `SMC_CREDENTIALS_PATH`) -- Initialize credentials via `pnpm multica credentials init` - -## 5. Coding Rules +## 4. Coding Rules - TypeScript strict mode is enabled; keep types explicit. +- Go code follows standard Go conventions (gofmt, go vet). - Keep comments in code **English only**. - Prefer existing patterns/components over introducing parallel abstractions. - Avoid broad refactors unless required by the task. -- Keep docs concise and aligned with current code behavior. -## 6. Testing Rules +## 5. Testing Rules -- Test runner: Vitest. -- Mock policy: mock external/third-party dependencies only. -- Do not mock internal modules when real integration can be tested. -- Prefer temp directories and real file I/O for storage-related tests. +- **TypeScript**: Vitest. Mock external/third-party dependencies only. +- **Go**: Standard `go test`. Use testcontainers or test database for DB tests. -## 7. Commit Rules +## 6. Commit Rules - Use atomic commits grouped by logical intent. - Conventional format: @@ -71,14 +64,10 @@ pnpm test - `test(scope): ...` - `chore(scope): ...` -## 8. Minimum Pre-Push Checks +## 7. Minimum Pre-Push Checks ```bash pnpm typecheck pnpm test +make test ``` - -## 9. E2E Process Docs - -- `docs/e2e-testing-guide.md` -- `docs/e2e-finance-benchmark.md` diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..e30aef09 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +.PHONY: dev daemon build test migrate-up migrate-down sqlc seed clean + +# Go server +dev: + cd server && go run ./cmd/server + +daemon: + cd server && go run ./cmd/daemon + +build: + cd server && go build -o bin/server ./cmd/server + cd server && go build -o bin/daemon ./cmd/daemon + +test: + cd server && go test ./... + +# Database +migrate-up: + cd server && go run ./cmd/migrate up + +migrate-down: + cd server && go run ./cmd/migrate down + +sqlc: + cd server && sqlc generate + +seed: + cd server && go run ./cmd/seed + +# Cleanup +clean: + rm -rf server/bin server/tmp diff --git a/README.md b/README.md deleted file mode 100644 index f882b42f..00000000 --- a/README.md +++ /dev/null @@ -1,129 +0,0 @@ -# Super Multica - -Super Multica is a distributed AI agent framework and product monorepo. -It provides a local-first agent runtime plus CLI, gateway, web, and mobile integration surfaces. - -What this project does: - -- runs AI agent sessions with tools, skills, and persistent session state -- supports scheduled/automated execution workflows -- supports both standalone local usage and remote-access client workflows - -This repository keeps docs focused on: - -1. Development workflow -2. Testing workflow -3. Operational process - -Architecture details are still source-of-truth in code, but docs keep minimal project context for onboarding. - -## Quick Start (Workflow) - -```bash -pnpm install -pnpm multica credentials init -pnpm multica -``` - -Run local desktop workflow: - -```bash -pnpm dev -``` - -## Local Full-Stack Development (`pnpm dev:local`) - -Use this when you need **Gateway + Web + Desktop** together for end-to-end dev. - -Setup: - -1. Copy `.env.example` to `.env` in repo root -2. Set `TELEGRAM_BOT_TOKEN` in `.env` (from `@BotFather`) -3. Run: - -```bash -pnpm dev:local -``` - -What starts: - -| Service | Address | Notes | -|---------|---------|-------| -| Gateway | `http://localhost:4000` | Telegram long-polling mode | -| Web | `http://localhost:3000` | OAuth login flow | -| Desktop | — | Connects to local Gateway + Web | - -Data isolation: - -- runtime data: `~/.super-multica-dev` -- workspace data: `~/Documents/Multica-dev` - -Related: - -```bash -pnpm dev:local:archive -``` - -## Workflow Commands - -```bash -# CLI -pnpm multica -pnpm multica run "Hello" -pnpm multica chat -pnpm multica help - -# Development -pnpm dev -pnpm dev:desktop -pnpm dev:gateway -pnpm dev:web -pnpm dev:local -pnpm dev:local:archive - -# Build / quality -pnpm build -pnpm typecheck -pnpm test -``` - -## Testing Workflow - -```bash -# Unit/integration -pnpm test -pnpm test:watch -pnpm test:coverage - -# Type safety gate -pnpm typecheck - -# Agent E2E -pnpm multica run --run-log "your test prompt" -``` - -E2E process docs: - -- `docs/e2e-testing-guide.md` -- `docs/e2e-finance-benchmark.md` - -## Runtime Paths - -By default, runtime data is stored under: - -- `~/.super-multica` - -You can isolate environments with: - -- `SMC_DATA_DIR=~/.super-multica-dev` (or other path) - -## Process Docs - -- `CLAUDE.md` -- `docs/development.md` -- `docs/cli.md` -- `docs/credentials.md` -- `docs/skills-and-tools.md` -- `docs/package-management.md` -- `docs/e2e-testing-guide.md` -- `docs/e2e-finance-benchmark.md` diff --git a/apps/cli/package.json b/apps/cli/package.json deleted file mode 100644 index eae2a3e4..00000000 --- a/apps/cli/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "@multica/cli", - "version": "1.0.0", - "type": "module", - "bin": { - "multica": "./dist/index.js", - "mu": "./dist/index.js" - }, - "scripts": { - "dev": "tsx src/index.ts", - "build": "tsup", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@multica/core": "workspace:*", - "@multica/utils": "workspace:*" - }, - "devDependencies": { - "@types/node": "catalog:", - "tsup": "^8.0.0", - "tsx": "catalog:", - "typescript": "catalog:" - } -} diff --git a/apps/cli/src/autocomplete.ts b/apps/cli/src/autocomplete.ts deleted file mode 100644 index 1564983a..00000000 --- a/apps/cli/src/autocomplete.ts +++ /dev/null @@ -1,397 +0,0 @@ -/** - * Autocomplete Input - * - * Real-time dropdown autocomplete for terminal input - * No external dependencies - uses raw terminal control - * - * Falls back to simple readline when terminal doesn't support advanced features - */ - -import * as readline from "readline"; -import { colors } from "./colors.js"; - -export interface AutocompleteOption { - value: string; - label?: string; -} - -export interface AutocompleteConfig { - /** Function to get suggestions based on current input */ - getSuggestions: (input: string) => AutocompleteOption[]; - /** Prompt string */ - prompt?: string; - /** Max suggestions to show */ - maxSuggestions?: number; -} - -// ANSI escape codes -const ESC = "\x1b"; -const CLEAR_LINE = `${ESC}[2K`; -const CURSOR_UP = (n: number) => (n > 0 ? `${ESC}[${n}A` : ""); -const CURSOR_TO_COL = (n: number) => `${ESC}[${n}G`; -const RESET = `${ESC}[0m`; -const INVERSE = `${ESC}[7m`; -const SHOW_CURSOR = `${ESC}[?25h`; -const CLEAR_TO_END = `${ESC}[J`; - -// Strip ANSI escape codes to get visual length -const ANSI_REGEX = /\x1b\[[0-9;]*m/g; -function stripAnsi(str: string): string { - return str.replace(ANSI_REGEX, ""); -} - -/** - * Get the visual width of a string in terminal columns - * Full-width characters (CJK, etc.) take 2 columns - */ -function getStringWidth(str: string): number { - let width = 0; - for (const char of str) { - const code = char.codePointAt(0); - if (code === undefined) continue; - - // Check for full-width characters: - // - CJK Unified Ideographs (Chinese, Japanese Kanji, Korean Hanja) - // - CJK Symbols and Punctuation - // - Hiragana, Katakana - // - Hangul - // - Full-width ASCII and symbols - if ( - (code >= 0x1100 && code <= 0x115f) || // Hangul Jamo - (code >= 0x2e80 && code <= 0x9fff) || // CJK - (code >= 0xac00 && code <= 0xd7a3) || // Hangul Syllables - (code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility Ideographs - (code >= 0xfe10 && code <= 0xfe1f) || // Vertical forms - (code >= 0xfe30 && code <= 0xfe6f) || // CJK Compatibility Forms - (code >= 0xff00 && code <= 0xff60) || // Full-width ASCII - (code >= 0xffe0 && code <= 0xffe6) || // Full-width symbols - (code >= 0x20000 && code <= 0x2ffff) // CJK Extension B and beyond - ) { - width += 2; - } else { - width += 1; - } - } - return width; -} - -/** - * Check if terminal supports advanced cursor control - */ -function isTerminalSupported(): boolean { - // Check TERM environment variable - const term = process.env.TERM; - if (!term) { - return false; - } - - // Check if running in known unsupported environments - const unsupportedTerms = ["dumb", "emacs"]; - if (unsupportedTerms.includes(term.toLowerCase())) { - return false; - } - - // Check if stdout is a TTY - if (!process.stdout.isTTY) { - return false; - } - - return true; -} - -/** - * Simple readline input (fallback for unsupported terminals) - */ -function simpleInput(config: AutocompleteConfig): Promise { - return new Promise((resolve) => { - const { prompt = "> " } = config; - - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: true, - }); - - rl.question(prompt, (answer) => { - rl.close(); - resolve(answer); - }); - - rl.on("close", () => { - resolve(""); - }); - }); -} - -/** - * Read a line with real-time autocomplete dropdown - * Falls back to simple readline on unsupported terminals - */ -export function autocompleteInput(config: AutocompleteConfig): Promise { - // Fall back to simple input if terminal doesn't support advanced features - if (!isTerminalSupported()) { - return simpleInput(config); - } - - return new Promise((resolve) => { - const { getSuggestions, prompt = "> ", maxSuggestions = 5 } = config; - - const stdin = process.stdin; - const stdout = process.stdout; - - let input = ""; - let cursorPos = 0; - let suggestions: AutocompleteOption[] = []; - let selectedIndex = -1; - let lastRenderedLines = 0; // Track how many lines we rendered (for cleanup) - - // Enable raw mode - if (stdin.isTTY) { - stdin.setRawMode(true); - } - - // Set up keypress events - readline.emitKeypressEvents(stdin); - - const cleanup = () => { - stdout.write(SHOW_CURSOR); - if (stdin.isTTY) { - stdin.setRawMode(false); - } - stdin.removeListener("keypress", onKeypress); - }; - - const clearDisplay = () => { - // Move to beginning of current line - stdout.write("\r"); - // Clear current line - stdout.write(CLEAR_LINE); - // Clear any suggestion lines below - if (lastRenderedLines > 0) { - stdout.write(CLEAR_TO_END); - } - }; - - const render = () => { - clearDisplay(); - - // Write prompt and input - stdout.write(`${prompt}${input}`); - - // Calculate cursor position accounting for line wrapping and wide characters - const termWidth = stdout.columns || 80; - const promptVisualWidth = getStringWidth(stripAnsi(prompt)); - // Calculate visual width of input up to cursor position - const inputBeforeCursor = input.slice(0, cursorPos); - const inputVisualWidth = getStringWidth(inputBeforeCursor); - const cursorOffset = promptVisualWidth + inputVisualWidth; - - // Handle edge case: when cursor is exactly at line boundary, - // show it at end of current line, not start of next line - let cursorCol: number; - if (cursorOffset > 0 && cursorOffset % termWidth === 0) { - cursorCol = termWidth; - } else { - cursorCol = (cursorOffset % termWidth) + 1; - } - - // Get and display suggestions if input starts with / - if (input.startsWith("/") && input.length > 1) { - suggestions = getSuggestions(input).slice(0, maxSuggestions); - - if (suggestions.length > 0) { - // Ensure selectedIndex is valid - if (selectedIndex >= suggestions.length) { - selectedIndex = suggestions.length - 1; - } - - // Move to new line for suggestions - stdout.write("\n"); - - for (let i = 0; i < suggestions.length; i++) { - const opt = suggestions[i]!; - const isSelected = i === selectedIndex; - const value = isSelected - ? `${INVERSE} ${opt.value}${RESET}` - : ` ${colors.suggestionDim(opt.value)}`; - const label = opt.label ? ` ${colors.suggestionLabel(opt.label)}` : ""; - const line = `${value}${label}`; - - stdout.write(`${CLEAR_LINE}${line}`); - if (i < suggestions.length - 1) { - stdout.write("\n"); - } - } - - lastRenderedLines = suggestions.length; - - // Move cursor back up to input line - stdout.write(CURSOR_UP(suggestions.length)); - stdout.write(CURSOR_TO_COL(cursorCol)); - } else { - lastRenderedLines = 0; - } - } else { - suggestions = []; - selectedIndex = -1; - lastRenderedLines = 0; - } - - // Position cursor correctly within input - stdout.write(CURSOR_TO_COL(cursorCol)); - }; - - const submit = (value: string) => { - clearDisplay(); - stdout.write(`${prompt}${value}\n`); - cleanup(); - resolve(value); - }; - - const onKeypress = (_char: string, key: readline.Key) => { - if (!key) return; - - // Handle Ctrl+C - if (key.ctrl && key.name === "c") { - clearDisplay(); - cleanup(); - process.exit(0); - } - - // Handle Ctrl+D (EOF) - if (key.ctrl && key.name === "d") { - clearDisplay(); - cleanup(); - stdout.write("\n"); - resolve(""); - return; - } - - // Handle Enter - if (key.name === "return" || key.name === "enter") { - if (selectedIndex >= 0 && selectedIndex < suggestions.length) { - // Use selected suggestion - const selected = suggestions[selectedIndex]!; - submit(selected.value); - } else { - submit(input); - } - return; - } - - // Handle Tab - cycle through suggestions or complete selected one - if (key.name === "tab") { - if (suggestions.length > 0) { - if (selectedIndex >= 0) { - // Already have a selection - complete it to input - const selected = suggestions[selectedIndex]!; - input = selected.value + " "; - cursorPos = input.length; - selectedIndex = -1; - render(); - } else { - // No selection yet - select first item - if (key.shift) { - selectedIndex = suggestions.length - 1; - } else { - selectedIndex = 0; - } - render(); - } - } - return; - } - - // Handle arrow keys - if (key.name === "up") { - if (suggestions.length > 0) { - selectedIndex = selectedIndex <= 0 ? suggestions.length - 1 : selectedIndex - 1; - render(); - } - return; - } - - if (key.name === "down") { - if (suggestions.length > 0) { - selectedIndex = selectedIndex >= suggestions.length - 1 ? 0 : selectedIndex + 1; - render(); - } - return; - } - - // Handle Escape - clear selection - if (key.name === "escape") { - selectedIndex = -1; - render(); - return; - } - - // Handle backspace - if (key.name === "backspace") { - if (cursorPos > 0) { - input = input.slice(0, cursorPos - 1) + input.slice(cursorPos); - cursorPos--; - selectedIndex = -1; - render(); - } - return; - } - - // Handle delete - if (key.name === "delete") { - if (cursorPos < input.length) { - input = input.slice(0, cursorPos) + input.slice(cursorPos + 1); - selectedIndex = -1; - render(); - } - return; - } - - // Handle left arrow - if (key.name === "left") { - if (cursorPos > 0) { - cursorPos--; - render(); - } - return; - } - - // Handle right arrow - if (key.name === "right") { - if (cursorPos < input.length) { - cursorPos++; - render(); - } - return; - } - - // Handle home - if (key.name === "home" || (key.ctrl && key.name === "a")) { - cursorPos = 0; - render(); - return; - } - - // Handle end - if (key.name === "end" || (key.ctrl && key.name === "e")) { - cursorPos = input.length; - render(); - return; - } - - // Handle printable characters - if (key.sequence && !key.ctrl && !key.meta) { - const char = key.sequence; - if (char.length === 1 && char.charCodeAt(0) >= 32) { - input = input.slice(0, cursorPos) + char + input.slice(cursorPos); - cursorPos++; - selectedIndex = -1; - render(); - } - } - }; - - stdin.on("keypress", onKeypress); - render(); - }); -} diff --git a/apps/cli/src/colors.ts b/apps/cli/src/colors.ts deleted file mode 100644 index 37e22ee5..00000000 --- a/apps/cli/src/colors.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Terminal Colors and Styling - * - * Simple ANSI color utilities for terminal output - */ - -// Check if colors should be disabled -const NO_COLOR = process.env.NO_COLOR !== undefined || process.env.TERM === "dumb"; - -type StyleFn = (s: string) => string; - -const identity: StyleFn = (s) => s; - -function style(code: number, reset: number = 0): StyleFn { - if (NO_COLOR) return identity; - return (s: string) => `\x1b[${code}m${s}\x1b[${reset}m`; -} - -// Basic styles -export const reset = "\x1b[0m"; -export const bold = style(1, 22); -export const dim = style(2, 22); -export const italic = style(3, 23); -export const underline = style(4, 24); -export const inverse = style(7, 27); - -// Foreground colors -export const black = style(30, 39); -export const red = style(31, 39); -export const green = style(32, 39); -export const yellow = style(33, 39); -export const blue = style(34, 39); -export const magenta = style(35, 39); -export const cyan = style(36, 39); -export const white = style(37, 39); -export const gray = style(90, 39); - -// Bright colors -export const brightRed = style(91, 39); -export const brightGreen = style(92, 39); -export const brightYellow = style(93, 39); -export const brightBlue = style(94, 39); -export const brightMagenta = style(95, 39); -export const brightCyan = style(96, 39); - -// Background colors -export const bgRed = style(41, 49); -export const bgGreen = style(42, 49); -export const bgYellow = style(43, 49); -export const bgBlue = style(44, 49); - -// Semantic colors for the CLI -export const colors = { - // UI elements - prompt: cyan, - promptSymbol: brightCyan, - sessionId: dim, - - // Tool output - toolName: yellow, - toolArgs: dim, - toolBullet: cyan, - toolArrow: dim, - toolError: red, - - // Messages - error: red, - warning: yellow, - success: green, - info: blue, - - // Status bar - statusBg: inverse, - statusLabel: dim, - statusValue: white, - - // Welcome banner - bannerBorder: cyan, - bannerText: brightCyan, - - // Suggestions - suggestionSelected: inverse, - suggestionDim: dim, - suggestionLabel: gray, -}; - -// Spinner frames for thinking indicator -export const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; - -// Alternative spinner styles -export const spinnerStyles = { - dots: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], - line: ["-", "\\", "|", "/"], - arc: ["◜", "◠", "◝", "◞", "◡", "◟"], - bounce: ["⠁", "⠂", "⠄", "⠂"], - pulse: ["◯", "◔", "◑", "◕", "●", "◕", "◑", "◔"], -}; - -/** - * Create a spinner instance - */ -export function createSpinner(options: { - stream?: NodeJS.WritableStream; - frames?: string[]; - interval?: number; -} = {}) { - const { - stream = process.stderr, - frames = spinnerFrames, - interval = 80, - } = options; - - let frameIndex = 0; - let timer: ReturnType | null = null; - let currentText = ""; - - const render = () => { - const frame = colors.toolBullet(frames[frameIndex % frames.length]!); - stream.write(`\r\x1b[K${frame} ${currentText}`); - frameIndex++; - }; - - return { - start(text: string) { - currentText = text; - frameIndex = 0; - if (timer) clearInterval(timer); - render(); - timer = setInterval(render, interval); - }, - - update(text: string) { - currentText = text; - }, - - stop(finalText?: string) { - if (timer) { - clearInterval(timer); - timer = null; - } - stream.write("\r\x1b[K"); - if (finalText) { - stream.write(finalText + "\n"); - } - }, - - isSpinning() { - return timer !== null; - }, - }; -} diff --git a/apps/cli/src/commands/chat.ts b/apps/cli/src/commands/chat.ts deleted file mode 100644 index cf40652e..00000000 --- a/apps/cli/src/commands/chat.ts +++ /dev/null @@ -1,640 +0,0 @@ -/** - * Chat command - Interactive REPL mode - * - * Usage: - * multica chat [options] - * multica [options] (default command) - */ - -import * as readline from "readline"; -import { Agent } from "@multica/core"; -import type { AgentOptions } from "@multica/core"; -import { SkillManager } from "@multica/core"; -import { autocompleteInput, type AutocompleteOption } from "../autocomplete.js"; -import { colors, dim, cyan, brightCyan, yellow, green, gray, red } from "../colors.js"; -import { - getProviderList, - getCurrentProvider, - getLoginInstructions, - getProviderMeta, - type ProviderInfo, -} from "@multica/core"; - -type ChatOptions = { - profile?: string | undefined; - provider?: string | undefined; - model?: string | undefined; - system?: string | undefined; - thinking?: string | undefined; - cwd?: string | undefined; - session?: string | undefined; - help?: boolean; -}; - -const COMMANDS = { - help: "Show this help message", - exit: "Exit the CLI (aliases: quit, q)", - clear: "Clear the current session and start fresh", - session: "Show current session ID", - new: "Start a new session", - multiline: "Toggle multi-line input mode (end with a line containing only '.')", - provider: "Show current provider and available options", - model: "Show or switch model (usage: /model [model-name])", -}; - -function printHelp() { - console.log(` -${cyan("Usage:")} multica chat [options] - multica [options] - -${cyan("Options:")} - ${yellow("--profile")} ID Load agent profile - ${yellow("--provider")} NAME LLM provider (openai, anthropic, kimi, etc.) - ${yellow("--model")} NAME Model name - ${yellow("--system")} TEXT System prompt (ignored if --profile set) - ${yellow("--thinking")} LEVEL Thinking level - ${yellow("--cwd")} DIR Working directory - ${yellow("--session")} ID Session ID to resume - ${yellow("--help")}, -h Show this help - -${cyan("Interactive Commands:")} -`); - for (const [cmd, desc] of Object.entries(COMMANDS)) { - console.log(` ${yellow(`/${cmd}`.padEnd(14))} ${dim(desc)}`); - } - console.log(); -} - -function parseArgs(argv: string[]): ChatOptions { - const args = [...argv]; - const opts: ChatOptions = {}; - - while (args.length > 0) { - const arg = args.shift(); - if (!arg) break; - - if (arg === "--help" || arg === "-h") { - opts.help = true; - break; - } - if (arg === "--profile") { - opts.profile = args.shift(); - continue; - } - if (arg === "--provider") { - opts.provider = args.shift(); - continue; - } - if (arg === "--model") { - opts.model = args.shift(); - continue; - } - if (arg === "--system") { - opts.system = args.shift(); - continue; - } - if (arg === "--thinking") { - opts.thinking = args.shift(); - continue; - } - if (arg === "--cwd") { - opts.cwd = args.shift(); - continue; - } - if (arg === "--session") { - opts.session = args.shift(); - continue; - } - } - - return opts; -} - -function printWelcome(sessionId: string, opts: ChatOptions) { - const border = cyan("│"); - const topBorder = cyan("╭─────────────────────────────────────────╮"); - const bottomBorder = cyan("╰─────────────────────────────────────────╯"); - - console.log(topBorder); - console.log(`${border} ${brightCyan("Super Multica Interactive CLI")} ${border}`); - console.log(bottomBorder); - - // Show configuration - const configLines: string[] = []; - configLines.push(`${dim("Session:")} ${gray(sessionId.slice(0, 8))}...`); - if (opts.profile) { - configLines.push(`${dim("Profile:")} ${yellow(opts.profile)}`); - } - if (opts.provider) { - configLines.push(`${dim("Provider:")} ${green(opts.provider)}`); - } - if (opts.model) { - configLines.push(`${dim("Model:")} ${green(opts.model)}`); - } - - console.log(configLines.join(" ")); - console.log(`${dim("Type")} ${cyan("/help")} ${dim("for commands,")} ${cyan("/exit")} ${dim("to quit.")}`); - console.log(""); -} - -function printCommandHelp(skillManager?: SkillManager) { - console.log(`\n${cyan("Built-in commands:")}`); - for (const [cmd, desc] of Object.entries(COMMANDS)) { - console.log(` ${yellow(`/${cmd}`.padEnd(14))} ${dim(desc)}`); - } - - // Show skill commands if available - if (skillManager) { - const reservedNames = new Set(Object.keys(COMMANDS)); - const skillCommands = skillManager.getSkillCommands({ reservedNames }); - if (skillCommands.length > 0) { - console.log(`\n${cyan("Skill commands:")}`); - for (const cmd of skillCommands) { - console.log(` ${yellow(`/${cmd.name}`.padEnd(14))} ${dim(cmd.description)}`); - } - } - } - - console.log(`\n${dim("Just type your message and press Enter to chat with the agent.")}`); - console.log(""); -} - -/** - * Status Bar - renders a persistent status line at the bottom of the terminal - */ -class StatusBar { - private enabled: boolean; - private currentStatus: string = ""; - private stream: NodeJS.WriteStream; - - constructor(stream: NodeJS.WriteStream = process.stdout) { - this.stream = stream; - this.enabled = stream.isTTY === true; - } - - update(parts: { session?: string; provider?: string; model?: string; tokens?: number }) { - if (!this.enabled) return; - - const segments: string[] = []; - - if (parts.session) { - segments.push(`${dim("session:")}${gray(parts.session.slice(0, 8))}`); - } - if (parts.provider) { - segments.push(`${dim("provider:")}${green(parts.provider)}`); - } - if (parts.model) { - segments.push(`${dim("model:")}${yellow(parts.model)}`); - } - if (parts.tokens !== undefined) { - segments.push(`${dim("tokens:")}${cyan(String(parts.tokens))}`); - } - - this.currentStatus = segments.join(" "); - this.render(); - } - - private render() { - if (!this.enabled || !this.currentStatus) return; - - const termWidth = this.stream.columns || 80; - const termHeight = this.stream.rows || 24; - - const statusLine = ` ${this.currentStatus} `.slice(0, termWidth); - - this.stream.write( - `\x1b[s` + // Save cursor - `\x1b[${termHeight};1H` + // Move to last row - `\x1b[7m` + // Inverse video - `\x1b[2K` + // Clear line - statusLine.padEnd(termWidth) + - `\x1b[0m` + // Reset - `\x1b[u` // Restore cursor - ); - } - - clear() { - if (!this.enabled) return; - - const termHeight = this.stream.rows || 24; - - this.stream.write( - `\x1b[s` + - `\x1b[${termHeight};1H` + - `\x1b[2K` + - `\x1b[u` - ); - this.currentStatus = ""; - } - - hide() { - this.clear(); - } - - show() { - this.render(); - } -} - -class InteractiveCLI { - private agent: Agent; - private opts: ChatOptions; - private rl: readline.Interface | null = null; - private multilineMode = false; - private multilineBuffer: string[] = []; - private running = true; - private skillManager: SkillManager; - private reservedNames: Set; - private statusBar: StatusBar; - - constructor(opts: ChatOptions) { - this.opts = opts; - this.agent = this.createAgent(opts.session); - this.statusBar = new StatusBar(); - - this.skillManager = new SkillManager({ - profileId: opts.profile, - }); - - this.reservedNames = new Set(Object.keys(COMMANDS)); - - process.on("SIGINT", () => { - this.statusBar.clear(); - console.log(`\n${dim("Goodbye!")}`); - process.exit(0); - }); - } - - private getReadline(): readline.Interface { - if (!this.rl) { - this.rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: true, - }); - - this.rl.on("close", () => { - this.running = false; - this.statusBar.clear(); - console.log(`\n${dim("Goodbye!")}`); - process.exit(0); - }); - } - return this.rl; - } - - private closeReadline() { - if (this.rl) { - this.rl.close(); - this.rl = null; - } - } - - private getSuggestions(input: string): AutocompleteOption[] { - if (!input.startsWith("/")) { - return []; - } - - const prefix = input.slice(1).toLowerCase(); - const suggestions: AutocompleteOption[] = []; - - for (const [cmd, desc] of Object.entries(COMMANDS)) { - if (cmd.toLowerCase().startsWith(prefix)) { - suggestions.push({ - value: `/${cmd}`, - label: desc.slice(0, 40), - }); - } - } - - const skillCommands = this.skillManager.getSkillCommands({ reservedNames: this.reservedNames }); - for (const cmd of skillCommands) { - if (cmd.name.toLowerCase().startsWith(prefix)) { - suggestions.push({ - value: `/${cmd.name}`, - label: cmd.description.slice(0, 40), - }); - } - } - - suggestions.sort((a, b) => { - if (a.value.length !== b.value.length) return a.value.length - b.value.length; - return a.value.localeCompare(b.value); - }); - - return suggestions; - } - - private createAgent(sessionId?: string): Agent { - return new Agent({ - profileId: this.opts.profile, - provider: this.opts.provider, - model: this.opts.model, - systemPrompt: this.opts.system, - thinkingLevel: this.opts.thinking as AgentOptions["thinkingLevel"], - cwd: this.opts.cwd, - sessionId, - }); - } - - private prompt(): string { - if (this.multilineMode) { - return this.multilineBuffer.length === 0 ? cyan(">>> ") : cyan("... "); - } - return `${brightCyan("You:")} `; - } - - private updateStatusBar() { - const statusUpdate: { session?: string; provider?: string; model?: string; tokens?: number } = { - session: this.agent.sessionId, - provider: this.opts.provider ?? "default", - }; - if (this.opts.model) { - statusUpdate.model = this.opts.model; - } - this.statusBar.update(statusUpdate); - } - - async run() { - printWelcome(this.agent.sessionId, this.opts); - this.updateStatusBar(); - await this.loop(); - } - - private async loop() { - while (this.running) { - let input: string; - - if (this.multilineMode) { - const lineInput = await this.readline(this.prompt()); - if (lineInput === null) break; - input = lineInput; - - if (input === ".") { - const fullInput = this.multilineBuffer.join("\n"); - this.multilineBuffer = []; - this.multilineMode = false; - this.closeReadline(); - if (fullInput.trim()) { - await this.handleInput(fullInput); - } - } else { - this.multilineBuffer.push(input); - } - continue; - } - - try { - this.statusBar.hide(); - input = await autocompleteInput({ - prompt: this.prompt(), - getSuggestions: (text) => this.getSuggestions(text), - maxSuggestions: 8, - }); - this.statusBar.show(); - } catch { - break; - } - - const trimmed = input.trim(); - if (!trimmed) continue; - - if (trimmed.startsWith("/")) { - const handled = await this.handleCommand(trimmed); - if (!handled) { - await this.handleInput(trimmed); - } - } else { - await this.handleInput(trimmed); - } - } - } - - private readline(prompt: string): Promise { - return new Promise((resolve) => { - this.getReadline().question(prompt, (answer) => { - resolve(answer); - }); - }); - } - - private async handleCommand(input: string): Promise { - const cmd = input.slice(1).toLowerCase().split(/\s+/)[0]; - - switch (cmd) { - case "help": - printCommandHelp(this.skillManager); - return true; - - case "exit": - case "quit": - case "q": - this.statusBar.clear(); - console.log(dim("Goodbye!")); - this.running = false; - this.closeReadline(); - process.exit(0); - return true; - - case "clear": - this.agent = this.createAgent(); - this.updateStatusBar(); - console.log(`${green("Session cleared.")} ${dim("New session:")} ${gray(this.agent.sessionId.slice(0, 8))}...\n`); - return true; - - case "session": - console.log(`${dim("Current session:")} ${cyan(this.agent.sessionId)}\n`); - return true; - - case "new": - this.agent = this.createAgent(); - this.updateStatusBar(); - console.log(`${green("Started new session:")} ${gray(this.agent.sessionId.slice(0, 8))}...\n`); - return true; - - case "multiline": - this.multilineMode = !this.multilineMode; - if (this.multilineMode) { - console.log(`${green("Multi-line mode enabled.")} ${dim("End input with a line containing only '.'")}`); - this.multilineBuffer = []; - } else { - console.log(dim("Multi-line mode disabled.")); - this.multilineBuffer = []; - this.closeReadline(); - } - return true; - - case "provider": - this.showProviderStatus(); - return true; - - case "model": - this.handleModelCommand(input); - return true; - - default: - const invocation = this.skillManager.resolveCommand(input); - if (invocation) { - const skillPrompt = invocation.args - ? `[Skill: ${invocation.command.name}]\n\n${invocation.instructions}\n\nUser request: ${invocation.args}` - : `[Skill: ${invocation.command.name}]\n\n${invocation.instructions}`; - await this.handleInput(skillPrompt); - return true; - } - return false; - } - } - - private handleModelCommand(input: string) { - const parts = input.trim().split(/\s+/); - const modelArg = parts.slice(1).join(" ").trim(); - const currentProvider = this.opts.provider ?? getCurrentProvider(); - const providerMeta = getProviderMeta(currentProvider); - - if (!providerMeta) { - console.log(`${red("Error:")} Unknown provider: ${currentProvider}\n`); - return; - } - - // No argument - show current model and available models - if (!modelArg) { - console.log(`\n${cyan("🎯 Model Status")}\n`); - console.log(`${dim("Provider:")} ${green(currentProvider)}`); - console.log(`${dim("Current model:")} ${yellow(this.opts.model ?? providerMeta.defaultModel)}`); - console.log(`${dim("Default model:")} ${gray(providerMeta.defaultModel)}`); - - console.log(`\n${dim("Available models for")} ${green(currentProvider)}${dim(":")}`); - for (const model of providerMeta.models) { - const isCurrent = model === (this.opts.model ?? providerMeta.defaultModel); - const marker = isCurrent ? yellow(" (current)") : ""; - const modelDisplay = isCurrent ? yellow(model) : model; - console.log(` • ${modelDisplay}${marker}`); - } - - console.log(`\n${dim("Switch model:")} ${yellow(`/model `)}`); - console.log(`${dim("Example:")} ${yellow(`/model ${providerMeta.models[0]}`)}`); - console.log(""); - return; - } - - // Check if model is valid for current provider - const normalizedModel = modelArg.toLowerCase(); - const matchedModel = providerMeta.models.find( - (m) => m.toLowerCase() === normalizedModel - ); - - if (!matchedModel) { - console.log(`${red("Error:")} Model "${modelArg}" is not available for provider "${currentProvider}".`); - console.log(`\n${dim("Available models:")}`); - for (const model of providerMeta.models) { - console.log(` • ${model}`); - } - console.log(""); - return; - } - - // Switch model - const oldModel = this.opts.model ?? providerMeta.defaultModel; - this.opts.model = matchedModel; - - // Recreate agent with new model - this.agent = this.createAgent(this.agent.sessionId); - this.updateStatusBar(); - - console.log(`${green("✓")} Model switched: ${gray(oldModel)} → ${yellow(matchedModel)}`); - console.log(`${dim("Session preserved:")} ${gray(this.agent.sessionId.slice(0, 8))}...\n`); - } - - private showProviderStatus() { - const providers = getProviderList(); - const currentProvider = this.opts.provider ?? getCurrentProvider(); - - console.log(`\n${cyan("🔌 Provider Status")}\n`); - console.log(`${dim("Current:")} ${green(currentProvider)}`); - if (this.opts.model) { - console.log(`${dim("Model:")} ${yellow(this.opts.model)}`); - } - - console.log(`\n${dim("Available Providers:")}`); - console.log(` ${dim("ID".padEnd(16))} ${dim("Name".padEnd(20))} ${dim("Auth".padEnd(12))} ${dim("Status")}`); - console.log(` ${dim("─".repeat(70))}`); - - // Group by auth method - const apiKeyProviders = providers.filter(p => p.authMethod === "api-key"); - const oauthProviders = providers.filter(p => p.authMethod === "oauth"); - - // OAuth providers first (more interesting) - for (const p of oauthProviders) { - const status = p.available ? green("✓") : red("✗"); - const isCurrent = p.id === currentProvider || (p.id === "claude-code" && currentProvider === "anthropic" && p.available); - const current = isCurrent ? yellow(" (current)") : ""; - const idDisplay = isCurrent ? yellow(p.id.padEnd(16)) : p.id.padEnd(16); - const authLabel = cyan("OAuth"); - const statusLabel = p.available ? green("ready") : dim("not logged in"); - console.log(` ${status} ${idDisplay} ${p.name.padEnd(20)} ${authLabel.padEnd(12)} ${statusLabel}${current}`); - } - - // API Key providers - for (const p of apiKeyProviders) { - const status = p.available ? green("✓") : red("✗"); - const isCurrent = p.id === currentProvider; - const current = isCurrent ? yellow(" (current)") : ""; - const idDisplay = isCurrent ? yellow(p.id.padEnd(16)) : p.id.padEnd(16); - const authLabel = dim("API Key"); - const statusLabel = p.available ? green("configured") : dim("not configured"); - console.log(` ${status} ${idDisplay} ${p.name.padEnd(20)} ${authLabel.padEnd(12)} ${statusLabel}${current}`); - } - - console.log(`\n${dim("Usage:")}`); - console.log(` ${yellow("multica --provider ")} ${dim("Start chat with specific provider")}`); - console.log(` ${yellow("multica --provider --model ")} ${dim("Specify model too")}`); - - console.log(`\n${dim("Examples:")}`); - console.log(` ${yellow("multica --provider claude-code")} ${dim("Use Claude Code OAuth")}`); - console.log(` ${yellow("multica --provider openai")} ${dim("Use OpenAI with API Key")}`); - - // If user hasn't logged into Claude Code, show instructions - const claudeCode = providers.find(p => p.id === "claude-code"); - if (claudeCode && !claudeCode.available) { - console.log(`\n${cyan("💡 Tip:")} To use Claude Code (free with Claude subscription):`); - console.log(` 1. Install: ${yellow("npm install -g @anthropic-ai/claude-code")}`); - console.log(` 2. Login: ${yellow("claude login")}`); - console.log(` 3. Use: ${yellow("multica --provider claude-code")}`); - } - - console.log(""); - } - - private async handleInput(input: string) { - try { - console.log(""); - this.statusBar.hide(); - const result = await this.agent.run(input); - this.statusBar.show(); - if (result.error) { - console.error(`\n${colors.error(`Error: ${result.error}`)}`); - } - console.log(""); - } catch (err) { - console.error(`\n${colors.error(`Error: ${err instanceof Error ? err.message : String(err)}`)}`); - console.log(""); - } - } -} - -export async function chatCommand(args: string[]): Promise { - const opts = parseArgs(args); - - if (opts.help) { - printHelp(); - return; - } - - if (!process.stdin.isTTY) { - console.error(colors.error("Error: Interactive mode requires a TTY. Use 'multica run' for non-interactive mode.")); - process.exit(1); - } - - const cli = new InteractiveCLI(opts); - await cli.run(); -} diff --git a/apps/cli/src/commands/credentials.ts b/apps/cli/src/commands/credentials.ts deleted file mode 100644 index d14f6cdf..00000000 --- a/apps/cli/src/commands/credentials.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Credentials command - Manage credentials and environment files - * - * Usage: - * multica credentials init Create credential files - * multica credentials show Show credential paths - * multica credentials edit Open credentials in editor - */ - -import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs"; -import { dirname } from "node:path"; -import { getCredentialsPath } from "@multica/core"; -import { cyan, yellow, green, dim, red } from "../colors.js"; - -type Command = "init" | "show" | "edit" | "help"; - -interface CredentialsOptions { - command: Command; - force: boolean; - pathOverride?: string | undefined; -} - -function printHelp() { - console.log(` -${cyan("Usage:")} multica credentials [options] - -${cyan("Commands:")} - ${yellow("init")} Create credentials.json5 - ${yellow("show")} Show credential file paths - ${yellow("edit")} Open credentials directory in file manager - ${yellow("help")} Show this help - -${cyan("Options for 'init':")} - ${yellow("--force")} Overwrite existing files - ${yellow("--path")} PATH Override credentials path - -${cyan("Files Created:")} - ~/.super-multica/credentials.json5 LLM providers + tools config - -${dim("Skill-specific API keys are stored in .env files within each skill's directory.")} -${dim("Example: ~/.super-multica/skills//.env")} - -${cyan("Examples:")} - ${dim("# Initialize credentials")} - multica credentials init - - ${dim("# Force overwrite")} - multica credentials init --force -`); -} - -function parseArgs(argv: string[]): CredentialsOptions { - const args = [...argv]; - const opts: CredentialsOptions = { - command: "help", - force: false, - }; - - const positional: string[] = []; - - while (args.length > 0) { - const arg = args.shift(); - if (!arg) break; - - if (arg === "--help" || arg === "-h") { - opts.command = "help"; - return opts; - } - if (arg === "--force" || arg === "-f") { - opts.force = true; - continue; - } - if (arg === "--path") { - opts.pathOverride = args.shift(); - continue; - } - positional.push(arg); - } - - opts.command = (positional[0] || "help") as Command; - return opts; -} - -function buildCoreTemplate(): string { - return `{ - version: 1, - llm: { - // provider: "openai", - providers: { - // openai: { apiKey: "sk-...", baseUrl: "https://api.openai.com/v1", model: "gpt-4.1" } - } - }, - tools: { - // brave: { apiKey: "brv-..." }, - // perplexity: { apiKey: "pplx-...", baseUrl: "https://api.perplexity.ai", model: "perplexity/sonar-pro" } - } -} -`; -} - -function cmdInit(opts: CredentialsOptions): void { - const path = opts.pathOverride ?? getCredentialsPath(); - if (existsSync(path) && !opts.force) { - console.error(`${red("Error:")} Credentials file already exists at ${path}`); - console.error("Use --force to overwrite."); - process.exit(1); - } - mkdirSync(dirname(path), { recursive: true }); - writeFileSync(path, buildCoreTemplate(), "utf8"); - chmodSync(path, 0o600); - console.log(`${green("Created:")} ${path}`); - - console.log(""); - console.log("Edit this file to add your LLM provider credentials."); - console.log(`${dim("Skill-specific API keys go in .env files within each skill's directory.")}`); -} - -function cmdShow(): void { - const credentialsPath = getCredentialsPath(); - - console.log(`\n${cyan("Credential Files:")}\n`); - - console.log(`${yellow("credentials.json5")}`); - console.log(` Path: ${credentialsPath}`); - console.log(` Exists: ${existsSync(credentialsPath) ? green("Yes") : red("No")}`); - console.log(""); - - console.log(`${dim("Skill-specific API keys are stored in .env files within each skill's directory.")}`); - console.log(""); - - if (!existsSync(credentialsPath)) { - console.log(`${dim("Run 'multica credentials init' to create missing files.")}`); - } -} - -async function cmdEdit(): Promise { - const credentialsPath = getCredentialsPath(); - const dir = dirname(credentialsPath); - - if (!existsSync(dir)) { - console.error(`${red("Error:")} Credentials directory does not exist: ${dir}`); - console.error("Run 'multica credentials init' first."); - process.exit(1); - } - - const { spawn } = await import("node:child_process"); - - // Open in default file manager - const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open"; - spawn(cmd, [dir], { detached: true, stdio: "ignore" }).unref(); - - console.log(`${green("Opened:")} ${dir}`); -} - -export async function credentialsCommand(args: string[]): Promise { - const opts = parseArgs(args); - - switch (opts.command) { - case "init": - cmdInit(opts); - break; - case "show": - cmdShow(); - break; - case "edit": - await cmdEdit(); - break; - case "help": - default: - printHelp(); - break; - } -} diff --git a/apps/cli/src/commands/cron.ts b/apps/cli/src/commands/cron.ts deleted file mode 100644 index aa4308d9..00000000 --- a/apps/cli/src/commands/cron.ts +++ /dev/null @@ -1,466 +0,0 @@ -/** - * Cron command - Manage scheduled tasks - * - * Usage: - * multica cron status Show cron service status - * multica cron list List all jobs - * multica cron add Add a new job - * multica cron run Run a job immediately - * multica cron enable Enable a job - * multica cron disable Disable a job - * multica cron remove Remove a job - * multica cron logs Show job run logs - */ - -import { cyan, yellow, green, dim, red, brightCyan } from "../colors.js"; -import { - getCronService, - formatSchedule, - formatDuration, - parseTimeInput, - parseIntervalInput, - isValidCronExpr, - type CronSchedule, - type CronJobInput, -} from "@multica/core"; - -type Command = "status" | "list" | "add" | "run" | "enable" | "disable" | "remove" | "logs" | "help"; - -function printHelp() { - console.log(` -${brightCyan("Cron")} - Scheduled Task Management - -${cyan("Usage:")} multica cron [options] - -${cyan("Commands:")} - ${yellow("status")} Show cron service status - ${yellow("list")} List all scheduled jobs - ${yellow("add")} [options] Create a new scheduled job - ${yellow("run")} Run a job immediately - ${yellow("enable")} Enable a disabled job - ${yellow("disable")} Disable a job (keeps schedule) - ${yellow("remove")} Delete a job - ${yellow("logs")} Show run history for a job - ${yellow("help")} Show this help - -${cyan("Add Options:")} - ${yellow("-n, --name")} Job name (required) - ${yellow("--at")}