Merge pull request #49 from multica-ai/jiayuan/better-cli-commands
refactor(cli): unify CLI with multica command and subcommands
This commit is contained in:
commit
9f48dca6af
13 changed files with 2677 additions and 94 deletions
32
CLAUDE.md
32
CLAUDE.md
|
|
@ -22,18 +22,24 @@ Super Multica is a distributed AI agent framework with a monorepo architecture.
|
|||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Development (all services concurrently: gateway:3000, console, web:3001)
|
||||
pnpm dev
|
||||
# Multica CLI (unified entry point)
|
||||
multica # Interactive mode (default)
|
||||
multica run "<prompt>" # Run a single prompt
|
||||
multica chat # Interactive REPL mode
|
||||
multica session list # List sessions
|
||||
multica profile list # List profiles
|
||||
multica skills list # List skills
|
||||
multica tools list # List tools
|
||||
multica credentials init # Initialize credentials
|
||||
multica dev # Start all dev services
|
||||
multica help # Show help
|
||||
|
||||
# Individual services
|
||||
pnpm dev:gateway # WebSocket gateway only
|
||||
pnpm dev:console # NestJS console with agent
|
||||
pnpm dev:web # Next.js web app
|
||||
pnpm dev:desktop # Electron desktop app
|
||||
|
||||
# Agent CLI
|
||||
pnpm agent:cli # Non-interactive agent
|
||||
pnpm agent:interactive # Interactive REPL mode
|
||||
# Development servers
|
||||
multica dev # All services (gateway:3000, console:4000, web:3001)
|
||||
multica dev gateway # WebSocket gateway only
|
||||
multica dev console # NestJS console with agent
|
||||
multica dev web # Next.js web app
|
||||
multica dev desktop # Electron desktop app
|
||||
|
||||
# Build (turbo-orchestrated)
|
||||
pnpm build
|
||||
|
|
@ -57,7 +63,7 @@ Frontend (web:3001 / desktop)
|
|||
→ Agent Engine (LLM runner, sessions, skills, tools)
|
||||
```
|
||||
|
||||
**Agent Engine** (`src/agent/`): Orchestrates LLM interactions with multi-provider support (OpenAI, Anthropic, DeepSeek, Kimi, Groq, Mistral, Google, Together). Features session management (JSONL-based, UUIDv7 IDs), profile system (`~/.super-multica/agent-profiles/`), modular skills with hot-reload, and token-aware context window guards (compaction modes: tokens, count, summary). CLI tools are organized in `src/agent/cli/` (interactive, non-interactive, profile, skills, tools).
|
||||
**Agent Engine** (`src/agent/`): Orchestrates LLM interactions with multi-provider support (OpenAI, Anthropic, DeepSeek, Kimi, Groq, Mistral, Google, Together). Features session management (JSONL-based, UUIDv7 IDs), profile system (`~/.super-multica/agent-profiles/`), modular skills with hot-reload, and token-aware context window guards (compaction modes: tokens, count, summary). Unified CLI in `src/agent/cli/index.ts` with subcommands in `src/agent/cli/commands/`.
|
||||
|
||||
**Gateway** (`src/gateway/`): NestJS WebSocket server with Socket.io for real-time message passing, RPC request/response, and streaming.
|
||||
|
||||
|
|
@ -82,7 +88,7 @@ Frontend (web:3001 / desktop)
|
|||
Use JSON5 credential files instead of `.env`:
|
||||
|
||||
```bash
|
||||
pnpm credentials:cli init
|
||||
multica credentials init
|
||||
```
|
||||
|
||||
This creates:
|
||||
|
|
|
|||
99
README.md
99
README.md
|
|
@ -42,7 +42,7 @@ The Agent reads credentials from JSON5 files (no `.env` required).
|
|||
Create empty templates:
|
||||
|
||||
```bash
|
||||
pnpm credentials:cli init
|
||||
multica credentials init
|
||||
```
|
||||
|
||||
This creates:
|
||||
|
|
@ -85,9 +85,9 @@ Example `skills.env.json5` (dynamic keys):
|
|||
Start services directly (no `source .env`):
|
||||
|
||||
```bash
|
||||
pnpm dev:console
|
||||
pnpm agent:cli "hello"
|
||||
pnpm dev:gateway
|
||||
multica dev console
|
||||
multica run "hello"
|
||||
multica dev gateway
|
||||
```
|
||||
|
||||
Optional overrides:
|
||||
|
|
@ -104,32 +104,52 @@ Each setting is resolved in order (first match wins):
|
|||
3. **Session metadata** — restored from previous session
|
||||
4. **Default** — `kimi-coding` provider with `kimi-k2-thinking` model
|
||||
|
||||
## Agent CLI
|
||||
## Multica CLI
|
||||
|
||||
Use the agent module directly from the CLI for isolated testing.
|
||||
The unified CLI provides access to all agent features through a single command.
|
||||
|
||||
```bash
|
||||
# New sessions get a UUIDv7 ID (shown on start)
|
||||
pnpm agent:cli "hello"
|
||||
# [session: 019c0b0a-b111-765c-8bbd-f4149beac9c4]
|
||||
# Interactive mode (default)
|
||||
multica
|
||||
multica chat
|
||||
multica chat --profile my-agent
|
||||
|
||||
# Run a single prompt
|
||||
multica run "hello"
|
||||
multica run --session demo "remember my name is Alice"
|
||||
|
||||
# Session management
|
||||
multica session list
|
||||
multica session show abc12345
|
||||
multica session delete abc12345
|
||||
|
||||
# Continue a session
|
||||
pnpm agent:cli --session 019c0b0a-b111-765c-8bbd-f4149beac9c4 "what did I say?"
|
||||
|
||||
# Or use a custom session name
|
||||
pnpm agent:cli --session demo "remember my name is Alice"
|
||||
pnpm agent:cli --session demo "what's my name?"
|
||||
multica --session abc12345
|
||||
multica run --session abc12345 "what did I say?"
|
||||
|
||||
# Override provider/model
|
||||
pnpm agent:cli --provider openai --model gpt-4o-mini "hi"
|
||||
multica run --provider openai --model gpt-4o-mini "hi"
|
||||
|
||||
# Use an agent profile
|
||||
pnpm agent:cli --profile my-agent "hello"
|
||||
multica chat --profile my-agent
|
||||
|
||||
# Set thinking level
|
||||
pnpm agent:cli --thinking high "solve this complex problem"
|
||||
multica run --thinking high "solve this complex problem"
|
||||
|
||||
# Development servers
|
||||
multica dev # Start all services
|
||||
multica dev gateway # Gateway only (:3000)
|
||||
multica dev console # Console only (:4000)
|
||||
multica dev web # Web app only (:3001)
|
||||
|
||||
# Help
|
||||
multica help
|
||||
multica run --help
|
||||
multica session --help
|
||||
```
|
||||
|
||||
Short alias: `mu` (same as `multica`)
|
||||
|
||||
## Sessions
|
||||
|
||||
Sessions persist conversation history to `~/.super-multica/sessions/<id>/`. Each session includes:
|
||||
|
|
@ -156,16 +176,19 @@ Agent profiles define identity, personality, tools, and memory for an agent. Pro
|
|||
|
||||
```bash
|
||||
# Create a new profile with default templates
|
||||
pnpm agent:profile new my-agent
|
||||
multica profile new my-agent
|
||||
|
||||
# List all profiles
|
||||
pnpm agent:profile list
|
||||
multica profile list
|
||||
|
||||
# Show profile contents
|
||||
pnpm agent:profile show my-agent
|
||||
multica profile show my-agent
|
||||
|
||||
# Open profile directory in file manager
|
||||
pnpm agent:profile edit my-agent
|
||||
multica profile edit my-agent
|
||||
|
||||
# Delete a profile
|
||||
multica profile delete my-agent
|
||||
```
|
||||
|
||||
### Profile Structure
|
||||
|
|
@ -194,17 +217,20 @@ Skills are modular capabilities that extend agent functionality through `SKILL.m
|
|||
|
||||
```bash
|
||||
# List all skills
|
||||
pnpm skills:cli list
|
||||
multica skills list
|
||||
|
||||
# Install skills from GitHub
|
||||
pnpm skills:cli add anthropics/skills
|
||||
multica skills add anthropics/skills
|
||||
|
||||
# Check skill status with diagnostics
|
||||
pnpm skills:cli status
|
||||
pnpm skills:cli status pdf -v
|
||||
multica skills status
|
||||
multica skills status pdf -v
|
||||
|
||||
# Install skill dependencies
|
||||
multica skills install nano-pdf
|
||||
|
||||
# Remove installed skills
|
||||
pnpm skills:cli remove skills
|
||||
multica skills remove skills
|
||||
```
|
||||
|
||||
### Built-in Skills
|
||||
|
|
@ -320,22 +346,31 @@ The Hub manages multiple agents and gateway connections:
|
|||
|
||||
## Scripts
|
||||
|
||||
### Agent Commands
|
||||
### Multica CLI Commands
|
||||
|
||||
- `pnpm agent:cli` - Run the agent CLI for module-level testing
|
||||
- `pnpm agent:interactive` - Interactive REPL mode
|
||||
- `pnpm agent:profile` - Manage agent profiles
|
||||
- `multica` / `mu` - Unified CLI entry point
|
||||
- `multica run <prompt>` - Run a single prompt
|
||||
- `multica chat` - Interactive REPL mode
|
||||
- `multica session <cmd>` - Session management
|
||||
- `multica profile <cmd>` - Profile management
|
||||
- `multica skills <cmd>` - Skills management
|
||||
- `multica tools <cmd>` - Tool policy inspection
|
||||
- `multica credentials <cmd>` - Credentials management
|
||||
- `multica dev [service]` - Development servers
|
||||
- `multica help` - Show help
|
||||
|
||||
### Development
|
||||
### Development (shortcuts)
|
||||
|
||||
- `pnpm dev` - Run full stack in development mode
|
||||
- `pnpm dev` - Run full stack (gateway + console + web)
|
||||
- `pnpm dev:gateway` - Run gateway only
|
||||
- `pnpm dev:console` - Run console only
|
||||
- `pnpm dev:web` - Run web app only
|
||||
- `pnpm dev:desktop` - Run desktop app
|
||||
|
||||
### Build & Test
|
||||
|
||||
- `pnpm build` - Build for production
|
||||
- `pnpm build:sdk` - Build SDK package
|
||||
- `pnpm build:cli` - Build CLI binary
|
||||
- `pnpm start` - Run production build
|
||||
- `pnpm typecheck` - Type check without emitting
|
||||
|
|
|
|||
25
package.json
25
package.json
|
|
@ -5,24 +5,17 @@
|
|||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"multica": "./bin/multica-interactive.mjs",
|
||||
"multica-interactive": "./bin/multica-interactive.mjs",
|
||||
"multica-cli": "./bin/multica-cli.mjs",
|
||||
"multica-profile": "./bin/multica-profile.mjs",
|
||||
"multica-credentials": "./bin/multica-credentials.mjs"
|
||||
"multica": "./bin/multica.mjs",
|
||||
"mu": "./bin/multica.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "concurrently -n gateway,console,web -c blue,yellow,green \"pnpm dev:gateway\" \"pnpm dev:console\" \"pnpm dev:web\"",
|
||||
"agent:cli": "tsx src/agent/cli/non-interactive.ts",
|
||||
"agent:interactive": "tsx src/agent/cli/interactive.ts",
|
||||
"agent:profile": "tsx src/agent/cli/profile.ts",
|
||||
"credentials:cli": "tsx src/agent/credentials-cli.ts",
|
||||
"skills:cli": "tsx src/agent/cli/skills.ts",
|
||||
"tools:cli": "tsx src/agent/cli/tools.ts",
|
||||
"dev:gateway": "tsx --watch src/gateway/main.ts",
|
||||
"dev:console": "tsx --watch src/console/main.ts",
|
||||
"dev:web": "pnpm --filter @multica/web dev",
|
||||
"dev:desktop": "pnpm --filter @multica/desktop dev",
|
||||
"multica": "tsx src/agent/cli/index.ts",
|
||||
"mu": "tsx src/agent/cli/index.ts",
|
||||
"dev": "tsx src/agent/cli/index.ts dev",
|
||||
"dev:gateway": "tsx src/agent/cli/index.ts dev gateway",
|
||||
"dev:console": "tsx src/agent/cli/index.ts dev console",
|
||||
"dev:web": "tsx src/agent/cli/index.ts dev web",
|
||||
"dev:desktop": "tsx src/agent/cli/index.ts dev desktop",
|
||||
"build": "turbo build",
|
||||
"build:sdk": "pnpm --filter @multica/sdk build",
|
||||
"build:cli": "node scripts/build-cli.js",
|
||||
|
|
|
|||
|
|
@ -28,44 +28,48 @@ const stripShebangPlugin = {
|
|||
};
|
||||
|
||||
async function build() {
|
||||
const entryPoints = [
|
||||
{ entry: "src/agent/cli/interactive.ts", outfile: "bin/multica-interactive.mjs" },
|
||||
{ entry: "src/agent/cli/non-interactive.ts", outfile: "bin/multica-cli.mjs" },
|
||||
{ entry: "src/agent/cli/profile.ts", outfile: "bin/multica-profile.mjs" },
|
||||
{ entry: "src/agent/credentials-cli.ts", outfile: "bin/multica-credentials.mjs" },
|
||||
];
|
||||
// Unified CLI entry point
|
||||
const entryPoint = {
|
||||
entry: "src/agent/cli/index.ts",
|
||||
outfile: "bin/multica.mjs",
|
||||
};
|
||||
|
||||
for (const { entry, outfile } of entryPoints) {
|
||||
console.log(`Building ${entry} -> ${outfile}...`);
|
||||
console.log(`Building ${entryPoint.entry} -> ${entryPoint.outfile}...`);
|
||||
|
||||
await esbuild.build({
|
||||
entryPoints: [resolve(rootDir, entry)],
|
||||
outfile: resolve(rootDir, outfile),
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
target: "node20",
|
||||
format: "esm",
|
||||
banner: {
|
||||
js: "#!/usr/bin/env node",
|
||||
},
|
||||
plugins: [stripShebangPlugin],
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
// Externalize all dependencies - they will be loaded from node_modules at runtime
|
||||
external: allDeps,
|
||||
});
|
||||
await esbuild.build({
|
||||
entryPoints: [resolve(rootDir, entryPoint.entry)],
|
||||
outfile: resolve(rootDir, entryPoint.outfile),
|
||||
bundle: true,
|
||||
platform: "node",
|
||||
target: "node20",
|
||||
format: "esm",
|
||||
banner: {
|
||||
js: "#!/usr/bin/env node",
|
||||
},
|
||||
plugins: [stripShebangPlugin],
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
// Externalize all dependencies - they will be loaded from node_modules at runtime
|
||||
external: allDeps,
|
||||
});
|
||||
|
||||
// Make executable
|
||||
chmodSync(resolve(rootDir, outfile), 0o755);
|
||||
console.log(` ✓ ${outfile}`);
|
||||
}
|
||||
// Make executable
|
||||
chmodSync(resolve(rootDir, entryPoint.outfile), 0o755);
|
||||
console.log(` ✓ ${entryPoint.outfile}`);
|
||||
|
||||
console.log("\nBuild complete! Binaries are in ./bin/");
|
||||
console.log("\nBuild complete! Binary is in ./bin/");
|
||||
console.log("\nUsage:");
|
||||
console.log(" node bin/multica-interactive.mjs # Interactive CLI");
|
||||
console.log(" node bin/multica-cli.mjs # Non-interactive CLI");
|
||||
console.log(" node bin/multica-profile.mjs # Profile management");
|
||||
console.log("\nNote: The built binaries require node_modules to be present.");
|
||||
console.log(" multica # Interactive mode (default)");
|
||||
console.log(" multica run <prompt> # Run a single prompt");
|
||||
console.log(" multica chat # Interactive mode");
|
||||
console.log(" multica session list # List sessions");
|
||||
console.log(" multica profile list # List profiles");
|
||||
console.log(" multica skills list # List skills");
|
||||
console.log(" multica tools list # List tools");
|
||||
console.log(" multica credentials init # Initialize credentials");
|
||||
console.log(" multica dev # Start dev servers");
|
||||
console.log(" multica help # Show help");
|
||||
console.log("\nNote: The built binary requires node_modules to be present.");
|
||||
console.log("Run 'pnpm install --prod' to install only production dependencies.");
|
||||
}
|
||||
|
||||
|
|
|
|||
503
src/agent/cli/commands/chat.ts
Normal file
503
src/agent/cli/commands/chat.ts
Normal file
|
|
@ -0,0 +1,503 @@
|
|||
/**
|
||||
* Chat command - Interactive REPL mode
|
||||
*
|
||||
* Usage:
|
||||
* multica chat [options]
|
||||
* multica [options] (default command)
|
||||
*/
|
||||
|
||||
import * as readline from "readline";
|
||||
import { Agent } from "../../runner.js";
|
||||
import type { AgentOptions } from "../../types.js";
|
||||
import { SkillManager } from "../../skills/index.js";
|
||||
import { autocompleteInput, type AutocompleteOption } from "../autocomplete.js";
|
||||
import { colors, dim, cyan, brightCyan, yellow, green, gray } from "../colors.js";
|
||||
|
||||
type ChatOptions = {
|
||||
profile?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
system?: string;
|
||||
thinking?: string;
|
||||
cwd?: string;
|
||||
session?: string;
|
||||
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 '.')",
|
||||
};
|
||||
|
||||
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<string>;
|
||||
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<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
this.getReadline().question(prompt, (answer) => {
|
||||
resolve(answer);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async handleCommand(input: string): Promise<boolean> {
|
||||
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;
|
||||
|
||||
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 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<void> {
|
||||
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();
|
||||
}
|
||||
229
src/agent/cli/commands/credentials.ts
Normal file
229
src/agent/cli/commands/credentials.ts
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
/**
|
||||
* 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, getSkillsEnvPath } from "../../credentials.js";
|
||||
import { cyan, yellow, green, dim, red } from "../colors.js";
|
||||
|
||||
type Command = "init" | "show" | "edit" | "help";
|
||||
|
||||
interface CredentialsOptions {
|
||||
command: Command;
|
||||
force: boolean;
|
||||
coreOnly: boolean;
|
||||
skillsOnly: boolean;
|
||||
pathOverride?: string;
|
||||
skillsPathOverride?: string;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
${cyan("Usage:")} multica credentials <command> [options]
|
||||
|
||||
${cyan("Commands:")}
|
||||
${yellow("init")} Create credentials.json5 and skills.env.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("--core-only")} Only create credentials.json5
|
||||
${yellow("--skills-only")} Only create skills.env.json5
|
||||
${yellow("--path")} PATH Override credentials path
|
||||
${yellow("--skills-path")} PATH Override skills env path
|
||||
|
||||
${cyan("Files Created:")}
|
||||
~/.super-multica/credentials.json5 LLM providers + tools config
|
||||
~/.super-multica/skills.env.json5 Skills/plugins/integrations env vars
|
||||
|
||||
${cyan("Examples:")}
|
||||
${dim("# Initialize credentials")}
|
||||
multica credentials init
|
||||
|
||||
${dim("# Force overwrite")}
|
||||
multica credentials init --force
|
||||
|
||||
${dim("# Only create core credentials")}
|
||||
multica credentials init --core-only
|
||||
`);
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): CredentialsOptions {
|
||||
const args = [...argv];
|
||||
const opts: CredentialsOptions = {
|
||||
command: "help",
|
||||
force: false,
|
||||
coreOnly: false,
|
||||
skillsOnly: 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 === "--core-only") {
|
||||
opts.coreOnly = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--skills-only") {
|
||||
opts.skillsOnly = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--path") {
|
||||
opts.pathOverride = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--skills-path") {
|
||||
opts.skillsPathOverride = 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 buildSkillsTemplate(): string {
|
||||
return `{
|
||||
env: {
|
||||
// Dynamic keys (skills, plugins, integrations)
|
||||
// LINEAR_API_KEY: "lin-..."
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function cmdInit(opts: CredentialsOptions): void {
|
||||
const createCore = !opts.skillsOnly;
|
||||
const createSkills = !opts.coreOnly;
|
||||
|
||||
if (!createCore && !createSkills) {
|
||||
console.error(`${red("Error:")} Both --core-only and --skills-only were provided.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (createCore) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
if (createSkills) {
|
||||
const skillsPath = opts.skillsPathOverride ?? getSkillsEnvPath();
|
||||
if (existsSync(skillsPath) && !opts.force) {
|
||||
console.error(`${red("Error:")} Skills env file already exists at ${skillsPath}`);
|
||||
console.error("Use --force to overwrite.");
|
||||
process.exit(1);
|
||||
}
|
||||
mkdirSync(dirname(skillsPath), { recursive: true });
|
||||
writeFileSync(skillsPath, buildSkillsTemplate(), "utf8");
|
||||
chmodSync(skillsPath, 0o600);
|
||||
console.log(`${green("Created:")} ${skillsPath}`);
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log("Edit these files to add your credentials.");
|
||||
}
|
||||
|
||||
function cmdShow(): void {
|
||||
const credentialsPath = getCredentialsPath();
|
||||
const skillsEnvPath = getSkillsEnvPath();
|
||||
|
||||
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(`${yellow("skills.env.json5")}`);
|
||||
console.log(` Path: ${skillsEnvPath}`);
|
||||
console.log(` Exists: ${existsSync(skillsEnvPath) ? green("Yes") : red("No")}`);
|
||||
console.log("");
|
||||
|
||||
if (!existsSync(credentialsPath) || !existsSync(skillsEnvPath)) {
|
||||
console.log(`${dim("Run 'multica credentials init' to create missing files.")}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdEdit(): Promise<void> {
|
||||
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<void> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
195
src/agent/cli/commands/dev.ts
Normal file
195
src/agent/cli/commands/dev.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
/**
|
||||
* Dev command - Start development servers
|
||||
*
|
||||
* Usage:
|
||||
* multica dev Start all services (gateway + console + web)
|
||||
* multica dev gateway Start gateway only (:3000)
|
||||
* multica dev console Start console only (:4000)
|
||||
* multica dev web Start web app only (:3001)
|
||||
* multica dev desktop Start desktop app
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { cyan, yellow, green, dim, red } from "../colors.js";
|
||||
|
||||
type Service = "all" | "gateway" | "console" | "web" | "desktop" | "help";
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
${cyan("Usage:")} multica dev [service]
|
||||
|
||||
${cyan("Services:")}
|
||||
${yellow("(default)")} Start all services (gateway + console + web)
|
||||
${yellow("gateway")} Start Gateway server (:3000)
|
||||
${yellow("console")} Start Console server (:4000)
|
||||
${yellow("web")} Start Web app (:3001)
|
||||
${yellow("desktop")} Start Desktop app
|
||||
${yellow("help")} Show this help
|
||||
|
||||
${cyan("Architecture:")}
|
||||
Frontend (web:3001 / desktop)
|
||||
→ Gateway (WebSocket, :3000)
|
||||
→ Console Hub (multi-agent coordination, :4000)
|
||||
→ Agent Engine
|
||||
|
||||
${cyan("Examples:")}
|
||||
${dim("# Start all services")}
|
||||
multica dev
|
||||
|
||||
${dim("# Start only the gateway")}
|
||||
multica dev gateway
|
||||
|
||||
${dim("# Start web and gateway separately")}
|
||||
multica dev gateway &
|
||||
multica dev web
|
||||
`);
|
||||
}
|
||||
|
||||
interface DevOptions {
|
||||
service: Service;
|
||||
watch: boolean;
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): DevOptions {
|
||||
const args = [...argv];
|
||||
let service: Service = "all";
|
||||
let watch = true;
|
||||
|
||||
while (args.length > 0) {
|
||||
const arg = args.shift();
|
||||
if (!arg) break;
|
||||
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
return { service: "help", watch };
|
||||
}
|
||||
if (arg === "--no-watch") {
|
||||
watch = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Service name
|
||||
if (["gateway", "console", "web", "desktop", "all", "help"].includes(arg)) {
|
||||
service = arg as Service;
|
||||
}
|
||||
}
|
||||
|
||||
return { service, watch };
|
||||
}
|
||||
|
||||
function runCommand(command: string, args: string[], options: { name: string; color: string }) {
|
||||
console.log(`${options.color}[${options.name}]${"\x1b[0m"} Starting...`);
|
||||
|
||||
const child = spawn(command, args, {
|
||||
stdio: "inherit",
|
||||
shell: true,
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
console.error(`${red(`[${options.name}]`)} Error: ${err.message}`);
|
||||
});
|
||||
|
||||
child.on("exit", (code) => {
|
||||
if (code !== 0 && code !== null) {
|
||||
console.error(`${red(`[${options.name}]`)} Exited with code ${code}`);
|
||||
}
|
||||
});
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
async function startGateway(watch: boolean) {
|
||||
const watchFlag = watch ? "--watch" : "";
|
||||
return runCommand("tsx", [watchFlag, "src/gateway/main.ts"].filter(Boolean), {
|
||||
name: "gateway",
|
||||
color: "\x1b[34m", // blue
|
||||
});
|
||||
}
|
||||
|
||||
async function startConsole(watch: boolean) {
|
||||
const watchFlag = watch ? "--watch" : "";
|
||||
return runCommand("tsx", [watchFlag, "src/console/main.ts"].filter(Boolean), {
|
||||
name: "console",
|
||||
color: "\x1b[33m", // yellow
|
||||
});
|
||||
}
|
||||
|
||||
async function startWeb() {
|
||||
return runCommand("pnpm", ["--filter", "@multica/web", "dev"], {
|
||||
name: "web",
|
||||
color: "\x1b[32m", // green
|
||||
});
|
||||
}
|
||||
|
||||
async function startDesktop() {
|
||||
return runCommand("pnpm", ["--filter", "@multica/desktop", "dev"], {
|
||||
name: "desktop",
|
||||
color: "\x1b[35m", // magenta
|
||||
});
|
||||
}
|
||||
|
||||
async function startAll(watch: boolean) {
|
||||
console.log(`\n${cyan("Starting all services...")}\n`);
|
||||
console.log(` ${"\x1b[34m"}Gateway${"\x1b[0m"} → http://localhost:3000`);
|
||||
console.log(` ${"\x1b[33m"}Console${"\x1b[0m"} → http://localhost:4000`);
|
||||
console.log(` ${"\x1b[32m"}Web${"\x1b[0m"} → http://localhost:3001`);
|
||||
console.log("");
|
||||
|
||||
// Start all services
|
||||
const gateway = await startGateway(watch);
|
||||
const console_ = await startConsole(watch);
|
||||
const web = await startWeb();
|
||||
|
||||
// Handle Ctrl+C
|
||||
const cleanup = () => {
|
||||
console.log(`\n${dim("Stopping all services...")}`);
|
||||
gateway.kill();
|
||||
console_.kill();
|
||||
web.kill();
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on("SIGINT", cleanup);
|
||||
process.on("SIGTERM", cleanup);
|
||||
|
||||
// Wait for all to exit
|
||||
await Promise.all([
|
||||
new Promise((resolve) => gateway.on("exit", resolve)),
|
||||
new Promise((resolve) => console_.on("exit", resolve)),
|
||||
new Promise((resolve) => web.on("exit", resolve)),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function devCommand(args: string[]): Promise<void> {
|
||||
const opts = parseArgs(args);
|
||||
|
||||
switch (opts.service) {
|
||||
case "gateway":
|
||||
console.log(`\n${cyan("Starting Gateway...")} → http://localhost:3000\n`);
|
||||
await startGateway(opts.watch);
|
||||
break;
|
||||
|
||||
case "console":
|
||||
console.log(`\n${cyan("Starting Console...")} → http://localhost:4000\n`);
|
||||
await startConsole(opts.watch);
|
||||
break;
|
||||
|
||||
case "web":
|
||||
console.log(`\n${cyan("Starting Web App...")} → http://localhost:3001\n`);
|
||||
await startWeb();
|
||||
break;
|
||||
|
||||
case "desktop":
|
||||
console.log(`\n${cyan("Starting Desktop App...")}\n`);
|
||||
await startDesktop();
|
||||
break;
|
||||
|
||||
case "all":
|
||||
await startAll(opts.watch);
|
||||
break;
|
||||
|
||||
case "help":
|
||||
default:
|
||||
printHelp();
|
||||
break;
|
||||
}
|
||||
}
|
||||
246
src/agent/cli/commands/profile.ts
Normal file
246
src/agent/cli/commands/profile.ts
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
/**
|
||||
* Profile command - Manage agent profiles
|
||||
*
|
||||
* Usage:
|
||||
* multica profile list List all profiles
|
||||
* multica profile new <id> Create a new profile
|
||||
* multica profile show <id> Show profile contents
|
||||
* multica profile edit <id> Open profile in file manager
|
||||
* multica profile delete <id> Delete a profile
|
||||
*/
|
||||
|
||||
import { existsSync, readdirSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
createAgentProfile,
|
||||
loadAgentProfile,
|
||||
getProfileDir,
|
||||
profileExists,
|
||||
} from "../../profile/index.js";
|
||||
import { DATA_DIR } from "../../../shared/index.js";
|
||||
import { cyan, yellow, green, dim, red } from "../colors.js";
|
||||
|
||||
const PROFILES_DIR = join(DATA_DIR, "agent-profiles");
|
||||
|
||||
type Command = "new" | "list" | "show" | "edit" | "delete" | "help";
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
${cyan("Usage:")} multica profile <command> [options]
|
||||
|
||||
${cyan("Commands:")}
|
||||
${yellow("list")} List all profiles
|
||||
${yellow("new")} <id> Create a new profile
|
||||
${yellow("show")} <id> Show profile contents
|
||||
${yellow("edit")} <id> Open profile directory in file manager
|
||||
${yellow("delete")} <id> Delete a profile
|
||||
${yellow("help")} Show this help
|
||||
|
||||
${cyan("Profile Structure:")}
|
||||
Each profile is a directory containing:
|
||||
- soul.md Personality and constraints
|
||||
- identity.md Name and role
|
||||
- tools.md Tool usage instructions
|
||||
- memory.md Persistent knowledge
|
||||
- bootstrap.md Initial context
|
||||
|
||||
${cyan("Examples:")}
|
||||
${dim("# Create a new profile")}
|
||||
multica profile new my-agent
|
||||
|
||||
${dim("# List all profiles")}
|
||||
multica profile list
|
||||
|
||||
${dim("# Use a profile")}
|
||||
multica chat --profile my-agent
|
||||
`);
|
||||
}
|
||||
|
||||
function cmdNew(profileId: string | undefined) {
|
||||
if (!profileId) {
|
||||
console.error("Error: Profile ID is required");
|
||||
console.error("Usage: multica profile new <id>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Validate profile ID
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(profileId)) {
|
||||
console.error("Error: Profile ID can only contain letters, numbers, hyphens, and underscores");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (profileExists(profileId)) {
|
||||
console.error(`Error: Profile "${profileId}" already exists`);
|
||||
console.error(`Location: ${getProfileDir(profileId)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const profile = createAgentProfile(profileId);
|
||||
const dir = getProfileDir(profileId);
|
||||
|
||||
console.log(`${green("Created profile:")} ${yellow(profile.id)}`);
|
||||
console.log(`${dim("Location:")} ${dir}`);
|
||||
console.log("");
|
||||
console.log("Files created:");
|
||||
console.log(" - soul.md (personality and constraints)");
|
||||
console.log(" - identity.md (name and role)");
|
||||
console.log(" - tools.md (tool usage instructions)");
|
||||
console.log(" - memory.md (persistent knowledge)");
|
||||
console.log(" - bootstrap.md (initial context)");
|
||||
console.log("");
|
||||
console.log("Edit these files to customize your agent, then run:");
|
||||
console.log(` multica chat --profile ${profileId}`);
|
||||
}
|
||||
|
||||
function cmdList() {
|
||||
if (!existsSync(PROFILES_DIR)) {
|
||||
console.log("No profiles found.");
|
||||
console.log(`Create one with: multica profile new <id>`);
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = readdirSync(PROFILES_DIR, { withFileTypes: true });
|
||||
const profiles = entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
||||
|
||||
if (profiles.length === 0) {
|
||||
console.log("No profiles found.");
|
||||
console.log(`Create one with: multica profile new <id>`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n${cyan("Available profiles:")}\n`);
|
||||
for (const id of profiles) {
|
||||
const dir = getProfileDir(id);
|
||||
console.log(` ${yellow(id)}`);
|
||||
console.log(` ${dim(dir)}`);
|
||||
}
|
||||
console.log("");
|
||||
console.log(`${dim(`Total: ${profiles.length} profile(s)`)}`);
|
||||
}
|
||||
|
||||
function cmdShow(profileId: string | undefined) {
|
||||
if (!profileId) {
|
||||
console.error("Error: Profile ID is required");
|
||||
console.error("Usage: multica profile show <id>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const profile = loadAgentProfile(profileId);
|
||||
if (!profile) {
|
||||
console.error(`Error: Profile "${profileId}" not found`);
|
||||
console.error(`Create it with: multica profile new ${profileId}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n${cyan("Profile:")} ${yellow(profile.id)}`);
|
||||
console.log(`${dim("Location:")} ${getProfileDir(profileId)}`);
|
||||
console.log("");
|
||||
|
||||
if (profile.identity) {
|
||||
console.log(`${green("=== identity.md ===")}`);
|
||||
console.log(profile.identity.trim());
|
||||
console.log("");
|
||||
}
|
||||
|
||||
if (profile.soul) {
|
||||
console.log(`${green("=== soul.md ===")}`);
|
||||
console.log(profile.soul.trim());
|
||||
console.log("");
|
||||
}
|
||||
|
||||
if (profile.tools) {
|
||||
console.log(`${green("=== tools.md ===")}`);
|
||||
console.log(profile.tools.trim());
|
||||
console.log("");
|
||||
}
|
||||
|
||||
if (profile.memory) {
|
||||
console.log(`${green("=== memory.md ===")}`);
|
||||
console.log(profile.memory.trim());
|
||||
console.log("");
|
||||
}
|
||||
|
||||
if (profile.bootstrap) {
|
||||
console.log(`${green("=== bootstrap.md ===")}`);
|
||||
console.log(profile.bootstrap.trim());
|
||||
console.log("");
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdEdit(profileId: string | undefined) {
|
||||
if (!profileId) {
|
||||
console.error("Error: Profile ID is required");
|
||||
console.error("Usage: multica profile edit <id>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!profileExists(profileId)) {
|
||||
console.error(`Error: Profile "${profileId}" not found`);
|
||||
console.error(`Create it with: multica profile new ${profileId}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const dir = getProfileDir(profileId);
|
||||
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}`);
|
||||
}
|
||||
|
||||
function cmdDelete(profileId: string | undefined) {
|
||||
if (!profileId) {
|
||||
console.error("Error: Profile ID is required");
|
||||
console.error("Usage: multica profile delete <id>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!profileExists(profileId)) {
|
||||
console.error(`Error: Profile "${profileId}" not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const dir = getProfileDir(profileId);
|
||||
|
||||
try {
|
||||
rmSync(dir, { recursive: true });
|
||||
console.log(`${green("Deleted:")} ${profileId}`);
|
||||
} catch (err) {
|
||||
console.error(`${red("Error:")} Failed to delete profile: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export async function profileCommand(args: string[]): Promise<void> {
|
||||
const command = (args[0] || "help") as Command;
|
||||
const arg1 = args[1];
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case "new":
|
||||
cmdNew(arg1);
|
||||
break;
|
||||
case "list":
|
||||
cmdList();
|
||||
break;
|
||||
case "show":
|
||||
cmdShow(arg1);
|
||||
break;
|
||||
case "edit":
|
||||
await cmdEdit(arg1);
|
||||
break;
|
||||
case "delete":
|
||||
cmdDelete(arg1);
|
||||
break;
|
||||
case "help":
|
||||
default:
|
||||
printHelp();
|
||||
break;
|
||||
}
|
||||
}
|
||||
211
src/agent/cli/commands/run.ts
Normal file
211
src/agent/cli/commands/run.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
/**
|
||||
* Run command - Execute a single prompt non-interactively
|
||||
*
|
||||
* Usage:
|
||||
* multica run [options] <prompt>
|
||||
* echo "prompt" | multica run
|
||||
*/
|
||||
|
||||
import { Agent } from "../../runner.js";
|
||||
import type { ToolsConfig } from "../../tools/policy.js";
|
||||
import { cyan, yellow, dim } from "../colors.js";
|
||||
|
||||
type RunOptions = {
|
||||
profile?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
apiKey?: string;
|
||||
baseUrl?: string;
|
||||
system?: string;
|
||||
thinking?: string;
|
||||
cwd?: string;
|
||||
session?: string;
|
||||
debug?: boolean;
|
||||
toolsProfile?: string;
|
||||
toolsAllow?: string[];
|
||||
toolsDeny?: string[];
|
||||
help?: boolean;
|
||||
};
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
${cyan("Usage:")} multica run [options] <prompt>
|
||||
echo "prompt" | multica run
|
||||
|
||||
${cyan("Options:")}
|
||||
${yellow("--profile")} ID Load agent profile
|
||||
${yellow("--provider")} NAME LLM provider (openai, anthropic, kimi, etc.)
|
||||
${yellow("--model")} NAME Model name
|
||||
${yellow("--api-key")} KEY API key (overrides environment)
|
||||
${yellow("--base-url")} URL Custom base URL for provider
|
||||
${yellow("--system")} TEXT System prompt (ignored if --profile set)
|
||||
${yellow("--thinking")} LEVEL Thinking level
|
||||
${yellow("--cwd")} DIR Working directory
|
||||
${yellow("--session")} ID Session ID for persistence
|
||||
${yellow("--debug")} Enable debug logging
|
||||
${yellow("--help")}, -h Show this help
|
||||
|
||||
${cyan("Tools Configuration:")}
|
||||
${yellow("--tools-profile")} P Tool profile (minimal, coding, web, full)
|
||||
${yellow("--tools-allow")} T Allow specific tools (comma-separated)
|
||||
${yellow("--tools-deny")} T Deny specific tools (comma-separated)
|
||||
|
||||
${cyan("Examples:")}
|
||||
${dim("# Run with default settings")}
|
||||
multica run "What is 2+2?"
|
||||
|
||||
${dim("# Use a specific profile")}
|
||||
multica run --profile coder "List files in this directory"
|
||||
|
||||
${dim("# Pipe input")}
|
||||
echo "Explain this code" | multica run
|
||||
|
||||
${dim("# Resume a session")}
|
||||
multica run --session abc123 "Continue from where we left off"
|
||||
`);
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): { opts: RunOptions; prompt: string } {
|
||||
const args = [...argv];
|
||||
const opts: RunOptions = {};
|
||||
const promptParts: string[] = [];
|
||||
|
||||
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 === "--api-key") {
|
||||
opts.apiKey = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--base-url") {
|
||||
opts.baseUrl = 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;
|
||||
}
|
||||
if (arg === "--debug") {
|
||||
opts.debug = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--tools-profile") {
|
||||
opts.toolsProfile = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--tools-allow") {
|
||||
const value = args.shift();
|
||||
opts.toolsAllow = value?.split(",").map((s) => s.trim()) ?? [];
|
||||
continue;
|
||||
}
|
||||
if (arg === "--tools-deny") {
|
||||
const value = args.shift();
|
||||
opts.toolsDeny = value?.split(",").map((s) => s.trim()) ?? [];
|
||||
continue;
|
||||
}
|
||||
if (arg === "--") {
|
||||
promptParts.push(...args);
|
||||
break;
|
||||
}
|
||||
promptParts.push(arg);
|
||||
}
|
||||
|
||||
return { opts, prompt: promptParts.join(" ") };
|
||||
}
|
||||
|
||||
async function readStdin(): Promise<string> {
|
||||
if (process.stdin.isTTY) return "";
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => (data += chunk));
|
||||
process.stdin.on("end", () => resolve(data.trim()));
|
||||
process.stdin.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
export async function runCommand(args: string[]): Promise<void> {
|
||||
const { opts, prompt } = parseArgs(args);
|
||||
|
||||
if (opts.help) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
const stdinPrompt = await readStdin();
|
||||
const finalPrompt = prompt || stdinPrompt;
|
||||
|
||||
if (!finalPrompt) {
|
||||
printHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build tools config if any tools options are set
|
||||
let toolsConfig: ToolsConfig | undefined;
|
||||
if (opts.toolsProfile || opts.toolsAllow || opts.toolsDeny) {
|
||||
toolsConfig = {};
|
||||
if (opts.toolsProfile) {
|
||||
toolsConfig.profile = opts.toolsProfile as ToolsConfig["profile"];
|
||||
}
|
||||
if (opts.toolsAllow) {
|
||||
toolsConfig.allow = opts.toolsAllow;
|
||||
}
|
||||
if (opts.toolsDeny) {
|
||||
toolsConfig.deny = opts.toolsDeny;
|
||||
}
|
||||
}
|
||||
|
||||
const agent = new Agent({
|
||||
profileId: opts.profile,
|
||||
provider: opts.provider,
|
||||
model: opts.model,
|
||||
apiKey: opts.apiKey,
|
||||
baseUrl: opts.baseUrl,
|
||||
systemPrompt: opts.system,
|
||||
thinkingLevel: opts.thinking as any,
|
||||
cwd: opts.cwd,
|
||||
sessionId: opts.session,
|
||||
debug: opts.debug,
|
||||
tools: toolsConfig,
|
||||
});
|
||||
|
||||
// If it's a newly created session, notify user of sessionId
|
||||
if (!opts.session) {
|
||||
console.error(`[session: ${agent.sessionId}]`);
|
||||
}
|
||||
|
||||
const result = await agent.run(finalPrompt);
|
||||
if (result.error) {
|
||||
console.error(`Error: ${result.error}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
262
src/agent/cli/commands/session.ts
Normal file
262
src/agent/cli/commands/session.ts
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
/**
|
||||
* Session command - Manage conversation sessions
|
||||
*
|
||||
* Usage:
|
||||
* multica session list List all sessions
|
||||
* multica session show <id> Show session details
|
||||
* multica session delete <id> Delete a session
|
||||
*/
|
||||
|
||||
import { existsSync, readdirSync, readFileSync, unlinkSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { DATA_DIR } from "../../../shared/index.js";
|
||||
import { cyan, yellow, green, dim, red } from "../colors.js";
|
||||
|
||||
const SESSIONS_DIR = join(DATA_DIR, "sessions");
|
||||
|
||||
type Command = "list" | "show" | "delete" | "help";
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
${cyan("Usage:")} multica session <command> [options]
|
||||
|
||||
${cyan("Commands:")}
|
||||
${yellow("list")} List all sessions
|
||||
${yellow("show")} <id> Show session details
|
||||
${yellow("delete")} <id> Delete a session
|
||||
${yellow("help")} Show this help
|
||||
|
||||
${cyan("Examples:")}
|
||||
${dim("# List all sessions")}
|
||||
multica session list
|
||||
|
||||
${dim("# Show session details")}
|
||||
multica session show abc12345
|
||||
|
||||
${dim("# Delete a session")}
|
||||
multica session delete abc12345
|
||||
|
||||
${dim("# Resume a session")}
|
||||
multica --session abc12345
|
||||
multica chat --session abc12345
|
||||
`);
|
||||
}
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
interface SessionInfo {
|
||||
id: string;
|
||||
path: string;
|
||||
size: number;
|
||||
mtime: Date;
|
||||
messageCount: number;
|
||||
}
|
||||
|
||||
function getSessionInfo(sessionId: string): SessionInfo | null {
|
||||
const sessionPath = join(SESSIONS_DIR, `${sessionId}.jsonl`);
|
||||
if (!existsSync(sessionPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stat = statSync(sessionPath);
|
||||
const content = readFileSync(sessionPath, "utf8");
|
||||
const lines = content.trim().split("\n").filter(Boolean);
|
||||
|
||||
return {
|
||||
id: sessionId,
|
||||
path: sessionPath,
|
||||
size: stat.size,
|
||||
mtime: stat.mtime,
|
||||
messageCount: lines.length,
|
||||
};
|
||||
}
|
||||
|
||||
function listSessions(): SessionInfo[] {
|
||||
if (!existsSync(SESSIONS_DIR)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const files = readdirSync(SESSIONS_DIR);
|
||||
const sessions: SessionInfo[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.endsWith(".jsonl")) continue;
|
||||
const sessionId = file.replace(".jsonl", "");
|
||||
const info = getSessionInfo(sessionId);
|
||||
if (info) {
|
||||
sessions.push(info);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by modification time, newest first
|
||||
sessions.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
|
||||
return sessions;
|
||||
}
|
||||
|
||||
function cmdList() {
|
||||
const sessions = listSessions();
|
||||
|
||||
if (sessions.length === 0) {
|
||||
console.log("No sessions found.");
|
||||
console.log(`${dim("Sessions are stored in:")} ${SESSIONS_DIR}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n${cyan("Sessions:")}\n`);
|
||||
|
||||
for (const session of sessions) {
|
||||
const shortId = session.id.slice(0, 8);
|
||||
console.log(` ${yellow(shortId)} ${dim(formatDate(session.mtime))} ${dim(`${session.messageCount} msgs`)} ${dim(formatSize(session.size))}`);
|
||||
}
|
||||
|
||||
console.log(`\n${dim(`Total: ${sessions.length} session(s)`)}`);
|
||||
console.log(`${dim("Resume with:")} multica --session <id>`);
|
||||
}
|
||||
|
||||
function cmdShow(sessionId: string | undefined) {
|
||||
if (!sessionId) {
|
||||
console.error("Error: Session ID is required");
|
||||
console.error("Usage: multica session show <id>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Support partial ID matching
|
||||
const sessions = listSessions();
|
||||
const matches = sessions.filter((s) => s.id.startsWith(sessionId));
|
||||
|
||||
if (matches.length === 0) {
|
||||
console.error(`Error: Session "${sessionId}" not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (matches.length > 1) {
|
||||
console.error(`Error: Multiple sessions match "${sessionId}":`);
|
||||
for (const s of matches) {
|
||||
console.error(` ${s.id.slice(0, 8)}`);
|
||||
}
|
||||
console.error("Please provide a more specific ID.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const session = matches[0];
|
||||
const content = readFileSync(session.path, "utf8");
|
||||
const lines = content.trim().split("\n").filter(Boolean);
|
||||
|
||||
console.log(`\n${cyan("Session:")} ${yellow(session.id)}`);
|
||||
console.log(`${dim("Path:")} ${session.path}`);
|
||||
console.log(`${dim("Size:")} ${formatSize(session.size)}`);
|
||||
console.log(`${dim("Modified:")} ${formatDate(session.mtime)}`);
|
||||
console.log(`${dim("Messages:")} ${session.messageCount}`);
|
||||
console.log("");
|
||||
console.log(cyan("─".repeat(60)));
|
||||
console.log("");
|
||||
|
||||
// Parse and display messages
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const msg = JSON.parse(line);
|
||||
const role = msg.role || "unknown";
|
||||
const roleColor = role === "user" ? green : role === "assistant" ? cyan : dim;
|
||||
|
||||
console.log(`${roleColor(`[${role}]`)}`);
|
||||
|
||||
if (typeof msg.content === "string") {
|
||||
// Truncate long content
|
||||
const preview = msg.content.length > 500
|
||||
? msg.content.slice(0, 500) + "..."
|
||||
: msg.content;
|
||||
console.log(preview);
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
for (const part of msg.content) {
|
||||
if (part.type === "text") {
|
||||
const preview = part.text.length > 500
|
||||
? part.text.slice(0, 500) + "..."
|
||||
: part.text;
|
||||
console.log(preview);
|
||||
} else if (part.type === "tool_use") {
|
||||
console.log(`${dim(`[Tool: ${part.name}]`)}`);
|
||||
} else if (part.type === "tool_result") {
|
||||
console.log(`${dim(`[Tool Result]`)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("");
|
||||
} catch {
|
||||
// Skip invalid JSON lines
|
||||
}
|
||||
}
|
||||
|
||||
console.log(cyan("─".repeat(60)));
|
||||
console.log(`\n${dim("Resume with:")} multica --session ${session.id.slice(0, 8)}`);
|
||||
}
|
||||
|
||||
function cmdDelete(sessionId: string | undefined) {
|
||||
if (!sessionId) {
|
||||
console.error("Error: Session ID is required");
|
||||
console.error("Usage: multica session delete <id>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Support partial ID matching
|
||||
const sessions = listSessions();
|
||||
const matches = sessions.filter((s) => s.id.startsWith(sessionId));
|
||||
|
||||
if (matches.length === 0) {
|
||||
console.error(`Error: Session "${sessionId}" not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (matches.length > 1) {
|
||||
console.error(`Error: Multiple sessions match "${sessionId}":`);
|
||||
for (const s of matches) {
|
||||
console.error(` ${s.id.slice(0, 8)}`);
|
||||
}
|
||||
console.error("Please provide a more specific ID.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const session = matches[0];
|
||||
|
||||
try {
|
||||
unlinkSync(session.path);
|
||||
console.log(`${green("Deleted:")} ${session.id}`);
|
||||
} catch (err) {
|
||||
console.error(`${red("Error:")} Failed to delete session: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export async function sessionCommand(args: string[]): Promise<void> {
|
||||
const command = (args[0] || "help") as Command;
|
||||
const arg1 = args[1];
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case "list":
|
||||
cmdList();
|
||||
break;
|
||||
case "show":
|
||||
cmdShow(arg1);
|
||||
break;
|
||||
case "delete":
|
||||
cmdDelete(arg1);
|
||||
break;
|
||||
case "help":
|
||||
default:
|
||||
printHelp();
|
||||
break;
|
||||
}
|
||||
}
|
||||
511
src/agent/cli/commands/skills.ts
Normal file
511
src/agent/cli/commands/skills.ts
Normal file
|
|
@ -0,0 +1,511 @@
|
|||
/**
|
||||
* Skills command - Manage agent skills
|
||||
*
|
||||
* Usage:
|
||||
* multica skills list List all skills
|
||||
* multica skills status [id] Show skill status
|
||||
* multica skills install <id> Install skill dependencies
|
||||
* multica skills add <source> Add skill from GitHub
|
||||
* multica skills remove <name> Remove a skill
|
||||
*/
|
||||
|
||||
import {
|
||||
SkillManager,
|
||||
installSkill,
|
||||
getInstallOptions,
|
||||
addSkill,
|
||||
removeSkill,
|
||||
listInstalledSkills,
|
||||
checkEligibilityDetailed,
|
||||
type DiagnosticItem,
|
||||
} from "../../skills/index.js";
|
||||
import { credentialManager } from "../../credentials.js";
|
||||
import { cyan, yellow, green, dim, red } from "../colors.js";
|
||||
|
||||
type Command = "list" | "status" | "install" | "add" | "remove" | "help";
|
||||
|
||||
interface ParsedArgs {
|
||||
command: Command;
|
||||
args: string[];
|
||||
verbose: boolean;
|
||||
force: boolean;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
${cyan("Usage:")} multica skills <command> [options]
|
||||
|
||||
${cyan("Commands:")}
|
||||
${yellow("list")} List all available skills
|
||||
${yellow("status")} [id] Show skill status (detailed diagnostics)
|
||||
${yellow("install")} <id> Install dependencies for a skill
|
||||
${yellow("add")} <source> Add skill from GitHub
|
||||
${yellow("remove")} <name> Remove an installed skill
|
||||
${yellow("help")} Show this help
|
||||
|
||||
${cyan("Options:")}
|
||||
${yellow("-v, --verbose")} Show more details
|
||||
${yellow("-f, --force")} Force overwrite existing skill
|
||||
|
||||
${cyan("Source Formats:")} ${dim("(for add command)")}
|
||||
owner/repo Clone entire repository
|
||||
owner/repo/skill-name Clone single skill directory
|
||||
owner/repo@branch Clone specific branch/tag
|
||||
|
||||
${cyan("Examples:")}
|
||||
${dim("# List all skills")}
|
||||
multica skills list
|
||||
|
||||
${dim("# Check skill status")}
|
||||
multica skills status commit
|
||||
|
||||
${dim("# Install skill dependencies")}
|
||||
multica skills install nano-pdf
|
||||
|
||||
${dim("# Add skills from GitHub")}
|
||||
multica skills add vercel-labs/agent-skills
|
||||
multica skills add vercel-labs/agent-skills/perplexity
|
||||
|
||||
${dim("# Remove a skill")}
|
||||
multica skills remove agent-skills
|
||||
`);
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): ParsedArgs {
|
||||
const args = [...argv];
|
||||
let verbose = false;
|
||||
let force = false;
|
||||
const positional: string[] = [];
|
||||
|
||||
while (args.length > 0) {
|
||||
const arg = args.shift();
|
||||
if (!arg) break;
|
||||
|
||||
if (arg === "--verbose" || arg === "-v") {
|
||||
verbose = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--force" || arg === "-f") {
|
||||
force = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
return { command: "help", args: [], verbose, force };
|
||||
}
|
||||
|
||||
positional.push(arg);
|
||||
}
|
||||
|
||||
const command = (positional[0] ?? "help") as Command;
|
||||
const commandArgs = positional.slice(1);
|
||||
|
||||
return { command, args: commandArgs, verbose, force };
|
||||
}
|
||||
|
||||
function cmdList(manager: SkillManager, verbose: boolean): void {
|
||||
const skills = manager.listAllSkillsWithStatus();
|
||||
|
||||
if (skills.length === 0) {
|
||||
console.log("No skills found.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n${cyan("Available Skills:")}\n`);
|
||||
|
||||
for (const skill of skills) {
|
||||
const status = skill.eligible ? "✓" : "✗";
|
||||
const statusColor = skill.eligible ? green : red;
|
||||
|
||||
console.log(` ${statusColor(status)} ${skill.emoji} ${skill.name} (${skill.id})`);
|
||||
console.log(` ${dim(skill.description)}`);
|
||||
console.log(` ${dim(`Source: ${skill.source}`)}`);
|
||||
|
||||
if (!skill.eligible && skill.reasons) {
|
||||
for (const reason of skill.reasons) {
|
||||
console.log(` ${red(`└ ${reason}`)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
console.log();
|
||||
const eligibleCount = skills.filter((s) => s.eligible).length;
|
||||
console.log(`${dim(`Total: ${skills.length} skills (${eligibleCount} eligible)`)}`);
|
||||
}
|
||||
|
||||
function cmdStatus(manager: SkillManager, skillId?: string, verbose?: boolean): void {
|
||||
if (!skillId) {
|
||||
cmdStatusSummary(manager, verbose);
|
||||
return;
|
||||
}
|
||||
cmdStatusDetail(manager, skillId, verbose);
|
||||
}
|
||||
|
||||
function cmdStatusSummary(manager: SkillManager, verbose?: boolean): void {
|
||||
const skills = manager.listAllSkillsWithStatus();
|
||||
const eligible = skills.filter((s) => s.eligible);
|
||||
const ineligible = skills.filter((s) => !s.eligible);
|
||||
|
||||
console.log(`\n${cyan("Skills Status Summary:")}\n`);
|
||||
console.log(` Total: ${skills.length}`);
|
||||
console.log(` ${green(`Eligible: ${eligible.length}`)}`);
|
||||
console.log(` ${red(`Ineligible: ${ineligible.length}`)}`);
|
||||
|
||||
if (ineligible.length > 0) {
|
||||
console.log("\n" + dim("─".repeat(45)));
|
||||
console.log("Ineligible Skills:");
|
||||
|
||||
const byIssue: Map<string, string[]> = new Map();
|
||||
for (const s of ineligible) {
|
||||
const skill = manager.getSkillFromAll(s.id);
|
||||
if (skill) {
|
||||
const detailed = checkEligibilityDetailed(skill);
|
||||
const mainIssue = detailed.diagnostics?.[0]?.type ?? "unknown";
|
||||
const existing = byIssue.get(mainIssue) ?? [];
|
||||
existing.push(s.id);
|
||||
byIssue.set(mainIssue, existing);
|
||||
}
|
||||
}
|
||||
|
||||
const issueLabels: Record<string, string> = {
|
||||
disabled: "Disabled in config",
|
||||
not_in_allowlist: "Not in allowlist",
|
||||
platform: "Platform mismatch",
|
||||
binary: "Missing binaries",
|
||||
any_binary: "Missing binaries (any)",
|
||||
env: "Missing environment variables",
|
||||
config: "Missing config values",
|
||||
unknown: "Unknown issues",
|
||||
};
|
||||
|
||||
for (const [issue, skillIds] of byIssue) {
|
||||
const label = issueLabels[issue] ?? issue;
|
||||
console.log(`\n ${yellow(label + ":")}`);
|
||||
for (const id of skillIds) {
|
||||
const skill = manager.getSkillFromAll(id);
|
||||
if (skill && verbose) {
|
||||
const detailed = checkEligibilityDetailed(skill);
|
||||
const diag = detailed.diagnostics?.[0];
|
||||
console.log(` - ${id}`);
|
||||
if (diag?.hint) {
|
||||
console.log(` ${cyan(`Hint: ${diag.hint}`)}`);
|
||||
}
|
||||
} else {
|
||||
console.log(` - ${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n" + dim("─".repeat(45)));
|
||||
console.log(`${cyan("Tip:")} Run 'multica skills status <skill-id>' for detailed diagnostics`);
|
||||
}
|
||||
}
|
||||
|
||||
function cmdStatusDetail(manager: SkillManager, skillId: string, verbose?: boolean): void {
|
||||
const skill = manager.getSkillFromAll(skillId);
|
||||
if (!skill) {
|
||||
console.error(`${red("Error:")} Skill not found: ${skillId}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const detailed = checkEligibilityDetailed(skill);
|
||||
const metadata = skill.frontmatter.metadata;
|
||||
|
||||
console.log(`\n${metadata?.emoji ?? "🔧"} ${skill.frontmatter.name}`);
|
||||
console.log("═".repeat(50));
|
||||
console.log(`ID: ${skill.id}`);
|
||||
console.log(`Description: ${skill.frontmatter.description ?? "N/A"}`);
|
||||
console.log(`Version: ${skill.frontmatter.version ?? "N/A"}`);
|
||||
console.log(`Source: ${skill.source}`);
|
||||
console.log(`Path: ${skill.filePath}`);
|
||||
console.log(`Homepage: ${skill.frontmatter.homepage ?? metadata?.homepage ?? "N/A"}`);
|
||||
|
||||
console.log();
|
||||
console.log("─".repeat(50));
|
||||
console.log(`Status: ${detailed.eligible ? green("✓ ELIGIBLE") : red("✗ NOT ELIGIBLE")}`);
|
||||
|
||||
if (!detailed.eligible && detailed.diagnostics) {
|
||||
console.log("\nDiagnostics:");
|
||||
for (const diag of detailed.diagnostics) {
|
||||
printDiagnostic(diag);
|
||||
}
|
||||
}
|
||||
|
||||
const requirements = metadata?.requires;
|
||||
const hasBins = requirements?.bins?.length ?? metadata?.requiresBinaries?.length ?? 0;
|
||||
const hasAnyBins = requirements?.anyBins?.length ?? 0;
|
||||
const hasEnvs = requirements?.env?.length ?? metadata?.requiresEnv?.length ?? 0;
|
||||
|
||||
if (hasBins > 0 || hasAnyBins > 0 || hasEnvs > 0) {
|
||||
console.log("\n" + "─".repeat(50));
|
||||
console.log("Requirements:");
|
||||
|
||||
if (hasBins > 0) {
|
||||
const bins = requirements?.bins ?? metadata?.requiresBinaries ?? [];
|
||||
printRequirementStatus("Binaries (all required)", bins, checkBinaries);
|
||||
}
|
||||
|
||||
if (hasAnyBins > 0) {
|
||||
const anyBins = requirements?.anyBins ?? [];
|
||||
printRequirementStatus("Binaries (any one)", anyBins, checkBinaries, true);
|
||||
}
|
||||
|
||||
if (hasEnvs > 0) {
|
||||
const envs = requirements?.env ?? metadata?.requiresEnv ?? [];
|
||||
printRequirementStatus("Environment vars", envs, checkEnvVars);
|
||||
}
|
||||
}
|
||||
|
||||
const installOptions = getInstallOptions(skill);
|
||||
if (installOptions.length > 0) {
|
||||
console.log("\n" + "─".repeat(50));
|
||||
console.log("Install Options:");
|
||||
for (const opt of installOptions) {
|
||||
const status = opt.available ? green("✓") : red("✗");
|
||||
console.log(` ${status} [${opt.id}] ${opt.label}`);
|
||||
if (!opt.available && opt.reason) {
|
||||
console.log(` └ ${opt.reason}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!detailed.eligible) {
|
||||
console.log("\n" + "─".repeat(50));
|
||||
console.log(`${yellow("Quick Actions:")}`);
|
||||
|
||||
for (const diag of detailed.diagnostics ?? []) {
|
||||
if (diag.hint) {
|
||||
console.log(` → ${diag.hint}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (installOptions.length > 0) {
|
||||
console.log(` → multica skills install ${skillId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function printDiagnostic(diag: DiagnosticItem): void {
|
||||
const typeColors: Record<string, (s: string) => string> = {
|
||||
disabled: yellow,
|
||||
not_in_allowlist: yellow,
|
||||
platform: dim,
|
||||
binary: red,
|
||||
any_binary: red,
|
||||
env: cyan,
|
||||
config: cyan,
|
||||
};
|
||||
|
||||
const color = typeColors[diag.type] ?? dim;
|
||||
|
||||
console.log(`\n ${color(`[${diag.type.toUpperCase()}]`)}`);
|
||||
console.log(` ${diag.message}`);
|
||||
|
||||
if (diag.values && diag.values.length > 0) {
|
||||
console.log(` Values: ${diag.values.join(", ")}`);
|
||||
}
|
||||
|
||||
if (diag.hint) {
|
||||
console.log(` ${cyan(`💡 ${diag.hint}`)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function printRequirementStatus(
|
||||
label: string,
|
||||
items: string[],
|
||||
checker: (items: string[]) => Map<string, boolean>,
|
||||
anyMode: boolean = false,
|
||||
): void {
|
||||
const status = checker(items);
|
||||
const found = Array.from(status.entries()).filter(([, ok]) => ok).map(([name]) => name);
|
||||
const missing = Array.from(status.entries()).filter(([, ok]) => !ok).map(([name]) => name);
|
||||
|
||||
const allOk = anyMode ? found.length > 0 : missing.length === 0;
|
||||
const statusIcon = allOk ? green("✓") : red("✗");
|
||||
|
||||
console.log(`\n ${statusIcon} ${label}:`);
|
||||
for (const [name, ok] of status) {
|
||||
const icon = ok ? green("✓") : red("✗");
|
||||
console.log(` ${icon} ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
function checkBinaries(bins: string[]): Map<string, boolean> {
|
||||
const result = new Map<string, boolean>();
|
||||
for (const bin of bins) {
|
||||
try {
|
||||
const cmd = process.platform === "win32" ? `where ${bin}` : `which ${bin}`;
|
||||
require("child_process").execSync(cmd, { stdio: "ignore" });
|
||||
result.set(bin, true);
|
||||
} catch {
|
||||
result.set(bin, false);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function checkEnvVars(envs: string[]): Map<string, boolean> {
|
||||
const result = new Map<string, boolean>();
|
||||
for (const env of envs) {
|
||||
result.set(env, credentialManager.hasEnv(env));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function cmdInstall(manager: SkillManager, skillId: string, installId?: string): Promise<void> {
|
||||
const skill = manager.getSkillFromAll(skillId);
|
||||
if (!skill) {
|
||||
console.error(`${red("Error:")} Skill not found: ${skillId}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const installOptions = getInstallOptions(skill);
|
||||
if (installOptions.length === 0) {
|
||||
console.error(`${red("Error:")} Skill '${skillId}' has no install specifications.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!installId && installOptions.length > 1) {
|
||||
console.log(`\nMultiple install options available for '${skillId}':\n`);
|
||||
for (const opt of installOptions) {
|
||||
const status = opt.available ? "available" : `unavailable: ${opt.reason}`;
|
||||
console.log(` [${opt.id}] ${opt.label} (${status})`);
|
||||
}
|
||||
console.log(`\nUse: multica skills install ${skillId} <install-id>`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\nInstalling dependencies for '${skillId}'...`);
|
||||
|
||||
const result = await installSkill({
|
||||
skill,
|
||||
installId,
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
console.log(`\n${green(`✓ ${result.message}`)}`);
|
||||
} else {
|
||||
console.error(`\n${red(`✗ ${result.message}`)}`);
|
||||
if (result.stderr) {
|
||||
console.error("\nError output:");
|
||||
console.error(result.stderr);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdAdd(source: string, force: boolean): Promise<void> {
|
||||
console.log(`\nAdding skill from '${source}'...`);
|
||||
|
||||
const result = await addSkill({
|
||||
source,
|
||||
force,
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
console.log(`\n${green(`✓ ${result.message}`)}`);
|
||||
if (result.skills && result.skills.length > 1) {
|
||||
console.log("\nSkills found:");
|
||||
for (const name of result.skills) {
|
||||
console.log(` - ${name}`);
|
||||
}
|
||||
}
|
||||
if (result.path) {
|
||||
console.log(`\nInstalled to: ${result.path}`);
|
||||
}
|
||||
} else {
|
||||
console.error(`\n${red(`✗ ${result.message}`)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdRemove(name: string): Promise<void> {
|
||||
console.log(`\nRemoving skill '${name}'...`);
|
||||
|
||||
const result = await removeSkill(name);
|
||||
|
||||
if (result.ok) {
|
||||
console.log(`\n${green(`✓ ${result.message}`)}`);
|
||||
} else {
|
||||
console.error(`\n${red(`✗ ${result.message}`)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdListInstalled(): Promise<void> {
|
||||
const skills = await listInstalledSkills();
|
||||
|
||||
if (skills.length === 0) {
|
||||
console.log("\nNo skills installed in ~/.super-multica/skills/");
|
||||
console.log("Use 'multica skills add <source>' to add skills.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("\nInstalled skills (~/.super-multica/skills/):\n");
|
||||
for (const name of skills) {
|
||||
console.log(` - ${name}`);
|
||||
}
|
||||
console.log(`\n${dim(`Total: ${skills.length} installed`)}`);
|
||||
}
|
||||
|
||||
export async function skillsCommand(args: string[]): Promise<void> {
|
||||
const { command, args: cmdArgs, verbose, force } = parseArgs(args);
|
||||
|
||||
if (command === "help") {
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case "add":
|
||||
if (!cmdArgs[0]) {
|
||||
console.error("Usage: multica skills add <source> [--force]");
|
||||
console.error("\nSource formats:");
|
||||
console.error(" owner/repo Clone entire repository");
|
||||
console.error(" owner/repo/skill-name Clone single skill directory");
|
||||
console.error(" owner/repo@branch Clone specific branch/tag");
|
||||
process.exit(1);
|
||||
}
|
||||
await cmdAdd(cmdArgs[0], force);
|
||||
return;
|
||||
|
||||
case "remove":
|
||||
if (!cmdArgs[0]) {
|
||||
console.error("Usage: multica skills remove <skill-name>");
|
||||
await cmdListInstalled();
|
||||
process.exit(1);
|
||||
}
|
||||
await cmdRemove(cmdArgs[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
const manager = new SkillManager();
|
||||
|
||||
switch (command) {
|
||||
case "list":
|
||||
cmdList(manager, verbose);
|
||||
break;
|
||||
|
||||
case "status":
|
||||
cmdStatus(manager, cmdArgs[0], verbose);
|
||||
break;
|
||||
|
||||
case "install":
|
||||
if (!cmdArgs[0]) {
|
||||
console.error("Usage: multica skills install <skill-id> [install-id]");
|
||||
process.exit(1);
|
||||
}
|
||||
await cmdInstall(manager, cmdArgs[0], cmdArgs[1]);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.error(`Unknown command: ${command}`);
|
||||
printHelp();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
210
src/agent/cli/commands/tools.ts
Normal file
210
src/agent/cli/commands/tools.ts
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
/**
|
||||
* Tools command - Inspect and test tool policies
|
||||
*
|
||||
* Usage:
|
||||
* multica tools list [options] List available tools
|
||||
* multica tools groups Show all tool groups
|
||||
* multica tools profiles Show all tool profiles
|
||||
*/
|
||||
|
||||
import { createAllTools } from "../../tools.js";
|
||||
import { filterTools, type ToolsConfig } from "../../tools/policy.js";
|
||||
import { TOOL_GROUPS, TOOL_PROFILES, expandToolGroups } from "../../tools/groups.js";
|
||||
import { cyan, yellow, green, dim } from "../colors.js";
|
||||
|
||||
type Command = "list" | "groups" | "profiles" | "help";
|
||||
|
||||
interface ToolsOptions {
|
||||
command: Command;
|
||||
profile?: string;
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
provider?: string;
|
||||
isSubagent?: boolean;
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
${cyan("Usage:")} multica tools <command> [options]
|
||||
|
||||
${cyan("Commands:")}
|
||||
${yellow("list")} List available tools (with optional filtering)
|
||||
${yellow("groups")} Show all tool groups
|
||||
${yellow("profiles")} Show all tool profiles
|
||||
${yellow("help")} Show this help
|
||||
|
||||
${cyan("Options for 'list':")}
|
||||
${yellow("--profile")} PROFILE Apply profile filter (minimal, coding, web, full)
|
||||
${yellow("--allow")} TOOLS Allow specific tools (comma-separated)
|
||||
${yellow("--deny")} TOOLS Deny specific tools (comma-separated)
|
||||
${yellow("--provider")} NAME Apply provider-specific rules
|
||||
${yellow("--subagent")} Apply subagent restrictions
|
||||
|
||||
${cyan("Examples:")}
|
||||
${dim("# List all tools")}
|
||||
multica tools list
|
||||
|
||||
${dim("# List tools with profile")}
|
||||
multica tools list --profile coding
|
||||
|
||||
${dim("# List tools with allow/deny")}
|
||||
multica tools list --profile coding --deny exec
|
||||
multica tools list --allow group:fs,web_fetch
|
||||
|
||||
${dim("# Show tool groups")}
|
||||
multica tools groups
|
||||
`);
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): ToolsOptions {
|
||||
const args = [...argv];
|
||||
const command = (args.shift() || "help") as Command;
|
||||
|
||||
if (command === "--help" || command === "-h") {
|
||||
return { command: "help" };
|
||||
}
|
||||
|
||||
const opts: ToolsOptions = { command };
|
||||
|
||||
while (args.length > 0) {
|
||||
const arg = args.shift();
|
||||
if (!arg) break;
|
||||
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
return { command: "help" };
|
||||
}
|
||||
if (arg === "--profile") {
|
||||
opts.profile = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--allow") {
|
||||
const value = args.shift();
|
||||
opts.allow = value?.split(",").map((s) => s.trim()) ?? [];
|
||||
continue;
|
||||
}
|
||||
if (arg === "--deny") {
|
||||
const value = args.shift();
|
||||
opts.deny = value?.split(",").map((s) => s.trim()) ?? [];
|
||||
continue;
|
||||
}
|
||||
if (arg === "--provider") {
|
||||
opts.provider = args.shift();
|
||||
continue;
|
||||
}
|
||||
if (arg === "--subagent") {
|
||||
opts.isSubagent = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
function cmdList(opts: ToolsOptions) {
|
||||
const allTools = createAllTools(process.cwd());
|
||||
|
||||
console.log(`\n${cyan("Tools Overview")}`);
|
||||
console.log(`Total tools available: ${allTools.length}\n`);
|
||||
|
||||
// Build config
|
||||
let config: ToolsConfig | undefined;
|
||||
if (opts.profile || opts.allow || opts.deny) {
|
||||
config = {};
|
||||
if (opts.profile) {
|
||||
config.profile = opts.profile as ToolsConfig["profile"];
|
||||
}
|
||||
if (opts.allow) {
|
||||
config.allow = opts.allow;
|
||||
}
|
||||
if (opts.deny) {
|
||||
config.deny = opts.deny;
|
||||
}
|
||||
}
|
||||
|
||||
const filterOpts: import("../../tools/policy.js").FilterToolsOptions = {};
|
||||
if (config) {
|
||||
filterOpts.config = config;
|
||||
}
|
||||
if (opts.provider) {
|
||||
filterOpts.provider = opts.provider;
|
||||
}
|
||||
if (opts.isSubagent) {
|
||||
filterOpts.isSubagent = opts.isSubagent;
|
||||
}
|
||||
|
||||
const filtered = filterTools(allTools, filterOpts);
|
||||
|
||||
if (config || opts.provider || opts.isSubagent) {
|
||||
console.log("Applied filters:");
|
||||
if (opts.profile) console.log(` ${dim("Profile:")} ${yellow(opts.profile)}`);
|
||||
if (opts.allow) console.log(` ${dim("Allow:")} ${opts.allow.join(", ")}`);
|
||||
if (opts.deny) console.log(` ${dim("Deny:")} ${opts.deny.join(", ")}`);
|
||||
if (opts.provider) console.log(` ${dim("Provider:")} ${opts.provider}`);
|
||||
if (opts.isSubagent) console.log(` ${dim("Subagent:")} true`);
|
||||
console.log("");
|
||||
console.log(`Tools after filtering: ${green(String(filtered.length))}`);
|
||||
console.log("");
|
||||
}
|
||||
|
||||
console.log("Tools:");
|
||||
for (const tool of filtered) {
|
||||
const desc = tool.description?.slice(0, 55) || "";
|
||||
console.log(` ${yellow(tool.name.padEnd(15))} ${dim(desc)}${desc.length >= 55 ? "..." : ""}`);
|
||||
}
|
||||
|
||||
if (filtered.length < allTools.length) {
|
||||
const removed = allTools.filter((t) => !filtered.find((f) => f.name === t.name));
|
||||
console.log("");
|
||||
console.log(`${dim(`Filtered out (${removed.length}):`)}`);
|
||||
for (const tool of removed) {
|
||||
console.log(` ${dim(tool.name)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cmdGroups() {
|
||||
console.log(`\n${cyan("Tool Groups:")}\n`);
|
||||
for (const [name, tools] of Object.entries(TOOL_GROUPS)) {
|
||||
console.log(` ${yellow(name)}:`);
|
||||
console.log(` ${dim(tools.join(", "))}`);
|
||||
console.log("");
|
||||
}
|
||||
}
|
||||
|
||||
function cmdProfiles() {
|
||||
console.log(`\n${cyan("Tool Profiles:")}\n`);
|
||||
for (const [name, policy] of Object.entries(TOOL_PROFILES)) {
|
||||
console.log(` ${yellow(name)}:`);
|
||||
if (policy.allow) {
|
||||
const expanded = expandToolGroups(policy.allow);
|
||||
console.log(` ${dim("Allow:")} ${policy.allow.join(", ")}`);
|
||||
console.log(` ${dim("Expands to:")} ${expanded.join(", ")}`);
|
||||
} else {
|
||||
console.log(` ${dim("Allow:")} (all tools)`);
|
||||
}
|
||||
if (policy.deny) {
|
||||
console.log(` ${dim("Deny:")} ${policy.deny.join(", ")}`);
|
||||
}
|
||||
console.log("");
|
||||
}
|
||||
}
|
||||
|
||||
export async function toolsCommand(args: string[]): Promise<void> {
|
||||
const opts = parseArgs(args);
|
||||
|
||||
switch (opts.command) {
|
||||
case "list":
|
||||
cmdList(opts);
|
||||
break;
|
||||
case "groups":
|
||||
cmdGroups();
|
||||
break;
|
||||
case "profiles":
|
||||
cmdProfiles();
|
||||
break;
|
||||
case "help":
|
||||
default:
|
||||
printHelp();
|
||||
break;
|
||||
}
|
||||
}
|
||||
178
src/agent/cli/index.ts
Normal file
178
src/agent/cli/index.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* Multica CLI - Unified command-line interface
|
||||
*
|
||||
* Usage:
|
||||
* multica Interactive mode (default)
|
||||
* multica run <prompt> Run a single prompt
|
||||
* multica chat Interactive mode (explicit)
|
||||
* multica session <cmd> Session management
|
||||
* multica profile <cmd> Profile management
|
||||
* multica skills <cmd> Skills management
|
||||
* multica tools <cmd> Tool policy inspection
|
||||
* multica credentials <cmd> Credentials management
|
||||
* multica dev [service] Development servers
|
||||
* multica help Show help
|
||||
*/
|
||||
|
||||
import { cyan, yellow, green, dim, brightCyan } from "./colors.js";
|
||||
|
||||
// Subcommand handlers (lazy imports for faster startup)
|
||||
type SubcommandHandler = (args: string[]) => Promise<void>;
|
||||
|
||||
const subcommands: Record<string, () => Promise<SubcommandHandler>> = {
|
||||
run: async () => (await import("./commands/run.js")).runCommand,
|
||||
chat: async () => (await import("./commands/chat.js")).chatCommand,
|
||||
session: async () => (await import("./commands/session.js")).sessionCommand,
|
||||
profile: async () => (await import("./commands/profile.js")).profileCommand,
|
||||
skills: async () => (await import("./commands/skills.js")).skillsCommand,
|
||||
tools: async () => (await import("./commands/tools.js")).toolsCommand,
|
||||
credentials: async () => (await import("./commands/credentials.js")).credentialsCommand,
|
||||
dev: async () => (await import("./commands/dev.js")).devCommand,
|
||||
};
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
${brightCyan("Multica CLI")} - AI Agent Framework
|
||||
|
||||
${cyan("Usage:")}
|
||||
${yellow("multica")} Start interactive mode (default)
|
||||
${yellow("multica run")} <prompt> Run a single prompt
|
||||
${yellow("multica chat")} [options] Start interactive mode
|
||||
${yellow("multica session")} <command> Manage sessions
|
||||
${yellow("multica profile")} <command> Manage agent profiles
|
||||
${yellow("multica skills")} <command> Manage skills
|
||||
${yellow("multica tools")} <command> Inspect tool policies
|
||||
${yellow("multica credentials")} <command> Manage credentials
|
||||
${yellow("multica dev")} [service] Start development servers
|
||||
${yellow("multica help")} Show this help
|
||||
|
||||
${cyan("Agent Options:")} ${dim("(for run/chat)")}
|
||||
${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
|
||||
${yellow("--session")} ID Resume session
|
||||
${yellow("--cwd")} DIR Working directory
|
||||
|
||||
${cyan("Commands:")}
|
||||
${green("session")}
|
||||
list List all sessions
|
||||
show <id> Show session details
|
||||
delete <id> Delete a session
|
||||
|
||||
${green("profile")}
|
||||
list List all profiles
|
||||
new <id> Create a new profile
|
||||
show <id> Show profile contents
|
||||
edit <id> Open profile in file manager
|
||||
delete <id> Delete a profile
|
||||
|
||||
${green("skills")}
|
||||
list List all skills
|
||||
status [id] Show skill status
|
||||
install <id> Install skill dependencies
|
||||
add <source> Add skill from GitHub
|
||||
remove <name> Remove a skill
|
||||
|
||||
${green("tools")}
|
||||
list [--profile P] List tools (with optional filter)
|
||||
groups Show tool groups
|
||||
profiles Show tool profiles
|
||||
|
||||
${green("credentials")}
|
||||
init [--force] Create credential files
|
||||
show Show credential paths
|
||||
edit Open credentials in editor
|
||||
|
||||
${green("dev")}
|
||||
${dim("(default)")} Start all services (gateway + console + web)
|
||||
gateway Start gateway only (:3000)
|
||||
console Start console only (:4000)
|
||||
web Start web app only (:3001)
|
||||
desktop Start desktop app
|
||||
|
||||
${cyan("Examples:")}
|
||||
${dim("# Start interactive mode")}
|
||||
multica
|
||||
|
||||
${dim("# Run a single prompt")}
|
||||
multica run "What files are in this directory?"
|
||||
|
||||
${dim("# Use a specific profile")}
|
||||
multica chat --profile coder
|
||||
|
||||
${dim("# Resume a session")}
|
||||
multica --session abc123
|
||||
|
||||
${dim("# Start development servers")}
|
||||
multica dev
|
||||
multica dev gateway
|
||||
`);
|
||||
}
|
||||
|
||||
function printVersion() {
|
||||
// Read version from package.json would be ideal, but for now just print a placeholder
|
||||
console.log("multica 1.0.0");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// Handle global flags
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
// If help is requested with a subcommand, delegate to that subcommand
|
||||
const firstArg = args[0];
|
||||
if (firstArg && !firstArg.startsWith("-") && subcommands[firstArg]) {
|
||||
const handler = await subcommands[firstArg]();
|
||||
await handler(["--help"]);
|
||||
return;
|
||||
}
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.includes("--version") || args.includes("-V")) {
|
||||
printVersion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine command
|
||||
const firstArg = args[0];
|
||||
|
||||
// No args or starts with -- means interactive mode
|
||||
if (!firstArg || firstArg.startsWith("-")) {
|
||||
const chatHandler = await subcommands.chat!();
|
||||
await chatHandler(args);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's "help" command
|
||||
if (firstArg === "help") {
|
||||
const subcommand = args[1];
|
||||
if (subcommand && subcommands[subcommand]) {
|
||||
const handler = await subcommands[subcommand]();
|
||||
await handler(["--help"]);
|
||||
return;
|
||||
}
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a known subcommand
|
||||
if (subcommands[firstArg]) {
|
||||
const handler = await subcommands[firstArg]();
|
||||
await handler(args.slice(1));
|
||||
return;
|
||||
}
|
||||
|
||||
// Unknown command - show error and help
|
||||
console.error(`Unknown command: ${firstArg}`);
|
||||
console.error(`Run 'multica help' for usage information.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err?.stack || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue