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:
Jiayuan Zhang 2026-02-01 23:17:43 +08:00 committed by GitHub
commit 9f48dca6af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 2677 additions and 94 deletions

View file

@ -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:

View file

@ -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

View file

@ -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",

View file

@ -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.");
}

View 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();
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}

View 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
View 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);
});