refactor: restructure to monorepo architecture

- Move core agent engine to packages/core/
- Add packages/types/ for shared TypeScript types
- Add packages/utils/ for utility functions
- Add apps/cli/ for command-line interface
- Add apps/gateway/ for NestJS WebSocket gateway
- Add apps/server/ for REST API server
- Restructure desktop app (electron/ → src/main/, src/preload/)
- Update pnpm workspace configuration
- Remove legacy src/ directory

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-02-10 17:54:22 +08:00
parent 5ba8c87744
commit 6ef58a0cab
304 changed files with 5699 additions and 3635 deletions

1
.npmrc Normal file
View file

@ -0,0 +1 @@
shamefully-hoist=true

140
CLAUDE.md
View file

@ -8,13 +8,27 @@ Super Multica is a distributed AI agent framework with a monorepo architecture.
## Monorepo Structure
- **`src/`** — Core modules (agent engine, gateway, hub, shared types)
- **`apps/desktop`** — Electron + Vite + React desktop app (`@multica/desktop`) — **primary development target**
- **`apps/web`** — Next.js 16 web app (`@multica/web`, port 3001)
- **`packages/ui`** — Shared UI component library (`@multica/ui`, Shadcn/Tailwind CSS v4)
- **`packages/sdk`** — Gateway client SDK (`@multica/sdk`, Socket.io)
- **`packages/store`** — Zustand state management (`@multica/store`)
- **`skills/`** — Bundled agent skills (commit, code-review, skill-creator)
```
super-multica/
├── apps/
│ ├── cli/ ← Command-line interface (`@multica/cli`)
│ ├── desktop/ ← Electron + Vite + React (`@multica/desktop`) — primary target
│ ├── gateway/ ← NestJS WebSocket gateway (`@multica/gateway`)
│ ├── server/ ← NestJS REST API server (`@multica/server`)
│ ├── web/ ← Next.js 16 web app (`@multica/web`, port 3001)
│ └── mobile/ ← React Native mobile app (`@multica/mobile`)
├── packages/
│ ├── core/ ← Core agent engine, hub, channels (`@multica/core`)
│ ├── sdk/ ← Gateway client SDK (`@multica/sdk`, Socket.io)
│ ├── ui/ ← Shared UI components (`@multica/ui`, Shadcn/Tailwind v4)
│ ├── store/ ← Zustand state management (`@multica/store`)
│ ├── hooks/ ← React hooks (`@multica/hooks`)
│ ├── types/ ← Shared TypeScript types (`@multica/types`)
│ └── utils/ ← Utility functions (`@multica/utils`)
└── skills/ ← Bundled agent skills
```
## Common Commands
@ -23,33 +37,35 @@ Super Multica is a distributed AI agent framework with a monorepo architecture.
pnpm install
# 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 desktop app (default)
multica help # Show help
pnpm multica # Interactive mode (default)
pnpm multica run "<prompt>" # Run a single prompt
pnpm multica chat # Interactive REPL mode
pnpm multica session list # List sessions
pnpm multica profile list # List profiles
pnpm multica skills list # List skills
pnpm multica tools list # List tools
pnpm multica credentials init # Initialize credentials
pnpm multica help # Show help
# Development servers
multica dev # Desktop app (default, recommended)
multica dev gateway # WebSocket gateway only (for remote clients)
multica dev web # Next.js web app
multica dev all # Gateway + web app
pnpm dev # Desktop app (default, recommended)
pnpm dev:desktop # Desktop app
pnpm dev:gateway # WebSocket gateway only
pnpm dev:web # Next.js web app
pnpm dev:all # Gateway + web app
# Build (turbo-orchestrated)
pnpm build
# Build
pnpm build # Build all (turbo-orchestrated)
pnpm --filter @multica/desktop build
pnpm --filter @multica/core build
# Type checking
pnpm typecheck
# Testing (vitest, tests live in src/**/*.test.ts)
pnpm test # Single run
pnpm test:watch # Watch mode
pnpm test:coverage # With v8 coverage
# Testing (vitest)
pnpm test # Single run
pnpm test:watch # Watch mode
pnpm test:coverage # With v8 coverage
```
## Architecture
@ -66,21 +82,41 @@ Web App (requires Gateway)
→ Hub + Agent Engine
```
**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/`.
**Agent Engine** (`packages/core/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.
**Hub** (`src/hub/`): Manages agents and communication channels. Embedded in desktop app, or runs standalone for web clients.
**Hub** (`packages/core/src/hub/`): Manages agents and communication channels. Embedded in desktop app, or runs standalone for web clients.
**Gateway** (`src/gateway/`): NestJS WebSocket server with Socket.io for remote client access, message routing, and device verification.
**Gateway** (`apps/gateway/`): NestJS WebSocket server with Socket.io for remote client access, message routing, and device verification.
**CLI** (`apps/cli/`): Command-line interface. Entry point: `apps/cli/src/index.ts`.
## Tech Stack & Config
- **Package manager**: pnpm 10 with workspaces (`pnpm-workspace.yaml`)
- **Build orchestration**: Turborepo (`turbo.json`)
- **TypeScript**: ESNext target, NodeNext modules, strict mode, `verbatimModuleSyntax`, `experimentalDecorators` (NestJS)
- **Testing**: Vitest with globals enabled, node environment
- **Frontend**: React 19, Next.js 16, Tailwind CSS v4, Shadcn/UI (zinc base, hugeicons)
- **TypeScript**: ESNext target, NodeNext modules, strict mode
- **Testing**: Vitest with globals enabled
- **Frontend**: React 19, Next.js 16, Tailwind CSS v4, Shadcn/UI
- **Backend**: NestJS 11, Socket.io, Pino logging
- **CLI bundling**: esbuild → `bin/` directory
- **Desktop**: Electron 33+, electron-vite, electron-builder
## pnpm Configuration
**Required `.npmrc` for Electron packaging:**
```ini
shamefully-hoist=true
```
After adding/changing `.npmrc`:
```bash
rm -rf node_modules apps/*/node_modules packages/*/node_modules
rm pnpm-lock.yaml
pnpm install
```
See `docs/package-management.md` for detailed package management guide.
## Code Style
@ -88,46 +124,36 @@ Web App (requires Gateway)
## Credentials Setup
Use JSON5 credential files instead of `.env`:
```bash
multica credentials init
pnpm multica credentials init
```
This creates:
Creates:
- `~/.super-multica/credentials.json5` (LLM providers + built-in tools)
- `~/.super-multica/skills.env.json5` (skills / plugins / integrations)
## Atomic Commits
After completing any task that modifies code, you MUST create atomic commits before ending the conversation.
After completing any task that modifies code, create atomic commits:
1. Run `git status` and `git diff` to see all modifications
2. Skip if no changes exist
3. Group changes by logical purpose (feature, fix, refactor, docs, test, chore)
4. Stage and commit each group separately
**Format**: Conventional commits — `<type>(<scope>): <description>`
**Format**: `<type>(<scope>): <description>`
Types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`
**Rules**:
- Each commit should be independently meaningful and buildable
- Related test files go with their implementation
- Never create empty commits or combine unrelated changes
- If all changes are related to one logical unit, a single commit is fine
- Keep commit messages concise but descriptive
- `git commit --amend` only for immediate small fixes to the last commit
### Examples
If you modified:
- `src/api/user.ts` (added new endpoint)
- `src/api/user.test.ts` (tests for new endpoint)
- `src/utils/format.ts` (refactored helper)
- `README.md` (updated docs)
```bash
git add packages/core/src/agent/runner.ts packages/core/src/agent/runner.test.ts
git commit -m "feat(agent): add streaming support"
Create three commits:
1. `git add src/api/user.ts src/api/user.test.ts && git commit -m "feat(api): add user profile endpoint"`
2. `git add src/utils/format.ts && git commit -m "refactor(utils): simplify date formatting logic"`
3. `git add README.md && git commit -m "docs: update API documentation"`
git add packages/utils/src/format.ts
git commit -m "refactor(utils): simplify date formatting"
git add README.md
git commit -m "docs: update API documentation"
```

24
apps/cli/package.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "@multica/cli",
"version": "1.0.0",
"type": "module",
"bin": {
"multica": "./dist/index.js",
"mu": "./dist/index.js"
},
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsup",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@multica/core": "workspace:*",
"@multica/utils": "workspace:*"
},
"devDependencies": {
"@types/node": "catalog:",
"tsup": "^8.0.0",
"tsx": "catalog:",
"typescript": "catalog:"
}
}

View file

@ -7,9 +7,9 @@
*/
import * as readline from "readline";
import { Agent } from "../../runner.js";
import type { AgentOptions } from "../../types.js";
import { SkillManager } from "../../skills/index.js";
import { Agent } from "@multica/core";
import type { AgentOptions } from "@multica/core";
import { SkillManager } from "@multica/core";
import { autocompleteInput, type AutocompleteOption } from "../autocomplete.js";
import { colors, dim, cyan, brightCyan, yellow, green, gray, red } from "../colors.js";
import {
@ -18,7 +18,7 @@ import {
getLoginInstructions,
getProviderMeta,
type ProviderInfo,
} from "../../providers/index.js";
} from "@multica/core";
type ChatOptions = {
profile?: string | undefined;

View file

@ -9,7 +9,7 @@
import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
import { dirname } from "node:path";
import { getCredentialsPath, getSkillsEnvPath } from "../../credentials.js";
import { getCredentialsPath, getSkillsEnvPath } from "@multica/core";
import { cyan, yellow, green, dim, red } from "../colors.js";
type Command = "init" | "show" | "edit" | "help";

View file

@ -22,7 +22,7 @@ import {
isValidCronExpr,
type CronSchedule,
type CronJobInput,
} from "../../../cron/index.js";
} from "@multica/core";
type Command = "status" | "list" | "add" | "run" | "enable" | "disable" | "remove" | "logs" | "help";

View file

@ -18,10 +18,10 @@ import {
loadAgentProfile,
getProfileDir,
profileExists,
} from "../../profile/index.js";
import { DATA_DIR } from "../../../shared/index.js";
} from "@multica/core";
import { DATA_DIR } from "@multica/utils";
import { cyan, yellow, green, dim, red, brightCyan, gray, colors } from "../colors.js";
import { Agent } from "../../runner.js";
import { Agent } from "@multica/core";
const __dirname = dirname(fileURLToPath(import.meta.url));
const SETUP_SKILL_PATH = join(__dirname, "../../../../skills/profile-setup/SKILL.md");

View file

@ -6,9 +6,9 @@
* echo "prompt" | multica run
*/
import { Agent } from "../../runner.js";
import type { AgentOptions } from "../../types.js";
import type { ToolsConfig } from "../../tools/policy.js";
import { Agent } from "@multica/core";
import type { AgentOptions } from "@multica/core";
import type { ToolsConfig } from "@multica/core";
import { cyan, yellow, dim } from "../colors.js";
type RunOptions = {

View file

@ -9,7 +9,7 @@
import { existsSync, readdirSync, readFileSync, unlinkSync, statSync } from "node:fs";
import { join } from "node:path";
import { DATA_DIR } from "../../../shared/index.js";
import { DATA_DIR } from "@multica/utils";
import { cyan, yellow, green, dim, red } from "../colors.js";
const SESSIONS_DIR = join(DATA_DIR, "sessions");

View file

@ -18,8 +18,8 @@ import {
listInstalledSkills,
checkEligibilityDetailed,
type DiagnosticItem,
} from "../../skills/index.js";
import { credentialManager } from "../../credentials.js";
} from "@multica/core";
import { credentialManager } from "@multica/core";
import { cyan, yellow, green, dim, red } from "../colors.js";
type Command = "list" | "status" | "install" | "add" | "remove" | "help";

View file

@ -6,9 +6,9 @@
* multica tools groups Show all tool groups
*/
import { createAllTools } from "../../tools.js";
import { filterTools, type ToolsConfig } from "../../tools/policy.js";
import { TOOL_GROUPS, expandToolGroups } from "../../tools/groups.js";
import { createAllTools } from "@multica/core";
import { filterTools, type ToolsConfig } from "@multica/core";
import { TOOL_GROUPS, expandToolGroups } from "@multica/core";
import { cyan, yellow, green, dim } from "../colors.js";
type Command = "list" | "groups" | "help";

View file

@ -1,8 +1,8 @@
#!/usr/bin/env node
import * as readline from "readline";
import { Agent } from "../runner.js";
import type { AgentOptions } from "../types.js";
import { SkillManager } from "../skills/index.js";
import { Agent } from "@multica/core";
import type { AgentOptions } from "@multica/core";
import { SkillManager } from "@multica/core";
import { autocompleteInput, type AutocompleteOption } from "./autocomplete.js";
import { colors, dim, cyan, brightCyan, yellow, green, gray } from "./colors.js";

View file

@ -1,5 +1,5 @@
#!/usr/bin/env node
import { Agent } from "../runner.js";
import { Agent } from "@multica/core";
type CliOptions = {
profile?: string | undefined;

318
apps/cli/src/output.ts Normal file
View file

@ -0,0 +1,318 @@
import type { AgentEvent, AgentMessage } from "@mariozechner/pi-agent-core";
import { colors, createSpinner, dim } from "./colors.js";
import { extractText, extractThinking } from "@multica/core";
import type { ReasoningMode } from "@multica/core";
export type AgentOutputState = {
lastAssistantText: string;
lastAssistantThinking: string;
printedLen: number;
printedThinkingLen: number;
streaming: boolean;
};
export type AgentOutput = {
state: AgentOutputState;
handleEvent: (event: AgentEvent) => void;
};
function truncate(s: string, max: number): string {
return s.length > max ? s.slice(0, max) + "…" : s;
}
// Exported for testing
export function toolDisplayName(name: string): string {
const map: Record<string, string> = {
read: "ReadFile",
write: "WriteFile",
edit: "EditFile",
exec: "Exec",
process: "Process",
grep: "Grep",
find: "FindFiles",
ls: "ListDir",
glob: "Glob",
web_search: "WebSearch",
web_fetch: "WebFetch",
};
return map[name] || name;
}
// Exported for testing
export function formatToolArgs(name: string, args: unknown): string {
if (!args || typeof args !== "object") return "";
const record = args as Record<string, unknown>;
const get = (key: string) => (record[key] !== undefined ? String(record[key]) : "");
switch (name) {
case "read":
return get("path") || get("file");
case "write":
return get("path") || get("file");
case "edit":
return get("path") || get("file");
case "grep":
return [get("pattern"), get("path") || get("directory")].filter(Boolean).join(" ");
case "find":
return [get("glob") || get("pattern"), get("path") || get("directory")].filter(Boolean).join(" ");
case "ls":
return get("path") || get("directory");
case "exec":
return get("command");
case "process":
return [get("action"), get("id")].filter(Boolean).join(" ");
case "glob":
return [get("pattern"), get("cwd")].filter(Boolean).join(" in ");
case "web_search":
return truncate(get("query"), 50);
case "web_fetch": {
const url = get("url");
try {
const parsed = new URL(url);
return parsed.hostname + (parsed.pathname !== "/" ? truncate(parsed.pathname, 30) : "");
} catch {
return truncate(url, 50);
}
}
default:
return "";
}
}
function formatToolLine(name: string, args: unknown, result?: unknown): string {
const title = colors.toolName(toolDisplayName(name));
const argText = formatToolArgs(name, args);
const resultSummary = formatResultSummary(name, result);
const bullet = colors.toolBullet("•");
let line = `${bullet} ${title}`;
if (argText) {
line += ` ${colors.toolArgs(`(${argText})`)}`;
}
if (resultSummary) {
line += ` ${colors.toolArrow("→")} ${colors.toolArgs(resultSummary)}`;
}
return line;
}
// Exported for testing
export function extractResultDetails(result: unknown): Record<string, unknown> | null {
if (!result || typeof result !== "object") return null;
// Try to extract from AgentMessage content array (JSON result)
const msg = result as { content?: Array<{ type: string; text?: string }> };
if (Array.isArray(msg.content)) {
for (const c of msg.content) {
if (c.type === "text" && c.text) {
try {
return JSON.parse(c.text) as Record<string, unknown>;
} catch {
continue;
}
}
}
}
const withDetails = result as { details?: unknown };
if (withDetails.details && typeof withDetails.details === "object") {
return withDetails.details as Record<string, unknown>;
}
// Try direct object access
return result as Record<string, unknown>;
}
// Exported for testing
export function formatResultSummary(name: string, result: unknown): string {
const details = extractResultDetails(result);
if (!details) return "";
switch (name) {
case "glob": {
const count = details.count ?? (Array.isArray(details.files) ? details.files.length : 0);
const truncated = details.truncated ? "+" : "";
return `${count}${truncated} files`;
}
case "web_search": {
if (details.error) return `error: ${details.message || details.error}`;
if (details.content) {
// Perplexity result
const citations = Array.isArray(details.citations) ? details.citations.length : 0;
return `${citations} citations`;
}
// Brave result
const count = details.count ?? (Array.isArray(details.results) ? details.results.length : 0);
return `${count} results`;
}
case "web_fetch": {
if (details.error) return `error: ${details.message || details.error}`;
const parts: string[] = [];
if (details.title) {
parts.push(`"${truncate(String(details.title), 30)}"`);
}
if (typeof details.length === "number") {
const kb = (details.length / 1024).toFixed(1);
parts.push(`${kb}KB`);
}
if (details.cached) {
parts.push("cached");
}
return parts.join(", ");
}
case "grep": {
// Try to count matches from result text
const text = extractText(result as AgentMessage | undefined);
if (text.includes("No matches found")) return "no matches";
const lines = text.split("\n").filter((l) => l.trim()).length;
if (lines > 0) return `${lines} matches`;
return "";
}
default:
return "";
}
}
export function createAgentOutput(params: {
stdout: NodeJS.WritableStream;
stderr: NodeJS.WritableStream;
reasoningMode?: ReasoningMode;
}): AgentOutput {
const reasoningMode = params.reasoningMode ?? "stream";
const state: AgentOutputState = {
lastAssistantText: "",
lastAssistantThinking: "",
printedLen: 0,
printedThinkingLen: 0,
streaming: false,
};
// Create spinner for thinking indicator
const spinner = createSpinner({ stream: params.stderr });
let pendingToolName = "";
let pendingToolArgs: unknown = null;
const handleEvent = (event: AgentEvent) => {
switch (event.type) {
case "message_start": {
const msg = event.message;
if (msg.role === "assistant") {
// Stop any running spinner when assistant starts responding
if (spinner.isSpinning()) {
spinner.stop();
}
state.streaming = true;
state.printedLen = 0;
state.printedThinkingLen = 0;
const text = extractText(msg);
if (text.length > 0) {
params.stdout.write(text);
state.printedLen = text.length;
}
// Stream thinking content in real-time
if (reasoningMode === "stream") {
const thinking = extractThinking(msg);
if (thinking.length > 0) {
params.stderr.write(dim(thinking));
state.printedThinkingLen = thinking.length;
}
}
}
break;
}
case "message_update": {
const msg = event.message;
if (msg.role === "assistant") {
const text = extractText(msg);
if (text.length > state.printedLen) {
params.stdout.write(text.slice(state.printedLen));
state.printedLen = text.length;
}
// Stream thinking content in real-time
if (reasoningMode === "stream") {
const thinking = extractThinking(msg);
if (thinking.length > state.printedThinkingLen) {
params.stderr.write(dim(thinking.slice(state.printedThinkingLen)));
state.printedThinkingLen = thinking.length;
}
}
}
break;
}
case "message_end": {
const msg = event.message;
if (msg.role === "assistant") {
const text = extractText(msg);
if (text.length > state.printedLen) {
params.stdout.write(text.slice(state.printedLen));
state.printedLen = text.length;
}
if (state.streaming) params.stdout.write("\n");
state.streaming = false;
state.lastAssistantText = text;
// Extract and store thinking content (skip when off)
const thinking = reasoningMode !== "off" ? extractThinking(msg) : "";
state.lastAssistantThinking = thinking;
// Show thinking at end for "on" mode
if (reasoningMode === "on" && thinking) {
params.stderr.write(`\n${dim("--- Thinking ---")}\n`);
params.stderr.write(dim(thinking));
params.stderr.write(`\n${dim("--- End Thinking ---")}\n`);
}
// Finish streaming thinking with a newline
if (reasoningMode === "stream" && state.printedThinkingLen > 0) {
params.stderr.write("\n");
}
}
break;
}
case "tool_execution_start": {
pendingToolName = event.toolName;
pendingToolArgs = event.args;
const title = colors.toolName(toolDisplayName(event.toolName));
const argText = formatToolArgs(event.toolName, event.args);
const displayText = argText ? `${title} ${colors.toolArgs(`(${argText})`)}` : title;
spinner.start(displayText);
break;
}
case "tool_execution_update": {
// Show real-time output updates (e.g., from exec tool)
const updateText = extractText(event.partialResult);
if (updateText && pendingToolName) {
const title = colors.toolName(toolDisplayName(pendingToolName));
const preview = colors.toolArgs(updateText.slice(-50).replace(/\n/g, " "));
spinner.update(`${title} ${colors.toolArrow("→")} ${preview}`);
}
break;
}
case "tool_execution_end": {
// Stop spinner and show final result with summary
const details = extractResultDetails(event.result);
const errorField = details?.error;
const hasError =
event.isError ||
Boolean(errorField) ||
details?.success === false;
if (hasError) {
const errorText =
(typeof details?.message === "string" && details.message) ||
(typeof errorField === "string" && errorField) ||
extractText(event.result) ||
"Tool failed";
const bullet = colors.toolError("✗");
const title = colors.toolName(toolDisplayName(event.toolName));
spinner.stop(`${bullet} ${title}: ${colors.toolError(errorText)}`);
} else {
spinner.stop(formatToolLine(event.toolName, pendingToolArgs, event.result));
}
pendingToolName = "";
pendingToolArgs = null;
break;
}
default:
break;
}
};
return { state, handleEvent };
}

View file

@ -16,8 +16,8 @@ import {
loadAgentProfile,
getProfileDir,
profileExists,
} from "../profile/index.js";
import { DATA_DIR } from "../../shared/index.js";
} from "@multica/core";
import { DATA_DIR } from "@multica/utils";
const DEFAULT_BASE_DIR = join(DATA_DIR, "agent-profiles");

View file

@ -21,8 +21,8 @@ import {
listInstalledSkills,
checkEligibilityDetailed,
type DiagnosticItem,
} from "../skills/index.js";
import { credentialManager } from "../credentials.js";
} from "@multica/core";
import { credentialManager } from "@multica/core";
// ============================================================================
// Types

View file

@ -9,9 +9,9 @@
* pnpm tools:cli groups # Show all tool groups
*/
import { createAllTools } from "../tools.js";
import { filterTools, type ToolsConfig } from "../tools/policy.js";
import { TOOL_GROUPS, expandToolGroups } from "../tools/groups.js";
import { createAllTools } from "@multica/core";
import { filterTools, type ToolsConfig } from "@multica/core";
import { TOOL_GROUPS, expandToolGroups } from "@multica/core";
type Command = "list" | "groups" | "help";

9
apps/cli/tsconfig.json Normal file
View file

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

15
apps/cli/tsup.config.ts Normal file
View file

@ -0,0 +1,15 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
sourcemap: true,
banner: {
js: '#!/usr/bin/env node',
},
external: [
/^node:/,
],
})

View file

@ -4,12 +4,12 @@
"appId": "YourAppID",
"asar": true,
"productName": "YourAppName",
"electronVersion": "30.5.1",
"directories": {
"output": "release/${version}"
},
"files": [
"dist",
"dist-electron"
"out"
],
"mac": {
"target": [

View file

@ -0,0 +1,36 @@
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import path from 'node:path'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
},
preload: {
plugins: [externalizeDepsPlugin()],
build: {
rollupOptions: {
output: {
format: 'cjs',
},
},
},
},
renderer: {
root: '.',
build: {
rollupOptions: {
input: {
index: path.resolve(__dirname, 'index.html'),
},
},
},
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
},
})

View file

@ -4,14 +4,15 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build && electron-builder",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
"dev": "electron-vite dev",
"build": "electron-vite build && electron-builder",
"preview": "electron-vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"@hugeicons/core-free-icons": "^3.1.1",
"@hugeicons/react": "^1.1.4",
"@hugeicons/core-free-icons": "catalog:",
"@hugeicons/react": "catalog:",
"@multica/core": "workspace:*",
"@multica/hooks": "workspace:*",
"@multica/sdk": "workspace:*",
"@multica/store": "workspace:*",
@ -20,27 +21,27 @@
"react": "catalog:",
"react-dom": "catalog:",
"react-router-dom": "^7.13.0",
"socket.io-client": "^4.8.3",
"uuid": "^13.0.0",
"socket.io-client": "catalog:",
"uuid": "catalog:",
"zustand": "catalog:"
},
"devDependencies": {
"@tailwindcss/vite": "^4.1.18",
"@tailwindcss/vite": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1",
"@vitejs/plugin-react": "^4.2.1",
"electron": "^30.0.1",
"electron-builder": "^24.13.3",
"electron": "^33.4.11",
"electron-builder": "^26.7.0",
"electron-builder-squirrel-windows": "^26.7.0",
"electron-vite": "^5.0.0",
"eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"tailwindcss": "^4",
"tailwindcss": "catalog:",
"typescript": "catalog:",
"vite": "^5.1.6",
"vite-plugin-electron": "^0.28.6",
"vite-plugin-electron-renderer": "^0.14.5"
"vite": "^5.1.6"
},
"main": "dist-electron/main.js"
"main": "./out/main/index.js"
}

View file

@ -49,13 +49,16 @@ import { fileURLToPath } from 'node:url'
import path from 'node:path'
import { registerAllIpcHandlers, initializeApp, cleanupAll, setupDeviceConfirmation } from './ipc/index.js'
// CJS output will have __dirname natively, but TypeScript source needs this for type checking
const __dirname = path.dirname(fileURLToPath(import.meta.url))
process.env.APP_ROOT = path.join(__dirname, '..')
// APP_ROOT points to apps/desktop (two levels up from out/main/)
process.env.APP_ROOT = path.join(__dirname, '../..')
export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron')
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
// electron-vite outputs to out/ directory
export const MAIN_DIST = path.join(__dirname)
export const RENDERER_DIST = path.join(__dirname, '../renderer')
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST
@ -66,7 +69,7 @@ function createWindow() {
width: 1200,
height: 800,
webPreferences: {
preload: path.join(__dirname, 'preload.mjs'),
preload: path.join(__dirname, '../preload/index.cjs'),
// Enable node integration for IPC
contextIsolation: true,
nodeIntegration: false,

View file

@ -7,8 +7,7 @@
*/
import { ipcMain } from 'electron'
import { getCurrentHub } from './hub.js'
import { credentialManager } from '../../../../src/agent/credentials.js'
import { listChannels } from '../../../../src/channels/registry.js'
import { credentialManager, listChannels } from '@multica/core'
/** Validate that a string is a safe identifier (alphanumeric, dashes, underscores) */
function isValidId(value: unknown): value is string {

View file

@ -5,7 +5,7 @@
* for the Cron Jobs management page.
*/
import { ipcMain } from 'electron'
import { getCronService, formatSchedule } from '../../../../src/cron/index.js'
import { getCronService, formatSchedule } from '@multica/core'
/**
* Register all Cron-related IPC handlers.

View file

@ -5,9 +5,8 @@
* This follows the same pattern as the Console app.
*/
import { ipcMain, type BrowserWindow } from 'electron'
import { Hub } from '../../../../src/hub/hub.js'
import { Hub, type AsyncAgent } from '@multica/core'
import type { ConnectionState } from '@multica/sdk'
import type { AsyncAgent } from '../../../../src/agent/async-agent.js'
// Singleton Hub instance
let hub: Hub | null = null

View file

@ -13,13 +13,11 @@ import {
getProviderMeta,
isProviderAvailable,
getLoginInstructions,
type ProviderInfo,
} from '../../../../src/agent/providers/index.js'
import {
readClaudeCliCredentials,
readCodexCliCredentials,
} from '../../../../src/agent/providers/oauth/cli-credentials.js'
import { credentialManager } from '../../../../src/agent/credentials.js'
credentialManager,
type ProviderInfo,
} from '@multica/core'
/**
* Provider info returned to renderer (matches ProviderInfo from registry).

View file

@ -235,7 +235,7 @@ export function registerSkillsIpcHandlers(): void {
) => {
console.log(`[IPC] skills:add called: source=${source}, options=${JSON.stringify(options)}`)
const { addSkill } = await import('../../../../src/agent/skills/add.js')
const { addSkill } = await import('@multica/core')
const result = await addSkill({
source,
@ -261,7 +261,7 @@ export function registerSkillsIpcHandlers(): void {
ipcMain.handle('skills:remove', async (_event, name: string) => {
console.log(`[IPC] skills:remove called: name=${name}`)
const { removeSkill } = await import('../../../../src/agent/skills/add.js')
const { removeSkill } = await import('@multica/core')
const result = await removeSkill(name)

View file

@ -1,63 +0,0 @@
import { defineConfig } from 'vite'
import path from 'node:path'
import electron from 'vite-plugin-electron/simple'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { builtinModules } from 'node:module'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
electron({
main: {
entry: 'electron/main.ts',
vite: {
build: {
rollupOptions: {
// Externalize all node_modules - they'll be resolved at runtime
// This is necessary because we import from src/hub which has many Node.js dependencies
external: [
'electron',
...builtinModules,
...builtinModules.map(m => `node:${m}`),
// Add specific packages that should not be bundled
'socket.io-client',
'uuid',
'chokidar',
'fast-glob',
'linkedom',
'undici',
'turndown',
'@mozilla/readability',
'pino',
'pino-pretty',
'yaml',
'json5',
'@mariozechner/pi-agent-core',
'@mariozechner/pi-ai',
'@mariozechner/pi-coding-agent',
'grammy',
],
},
},
resolve: {
alias: {
// Allow importing from root src/
'@multica/hub': path.resolve(__dirname, '../../src/hub'),
'@multica/agent': path.resolve(__dirname, '../../src/agent'),
'@multica/sdk': path.resolve(__dirname, '../../packages/sdk/src'),
},
},
},
},
preload: {
input: path.join(__dirname, 'electron/preload.ts'),
},
renderer: process.env.NODE_ENV === 'test'
? undefined
: {},
}),
],
})

28
apps/gateway/package.json Normal file
View file

@ -0,0 +1,28 @@
{
"name": "@multica/gateway",
"version": "0.0.1",
"type": "module",
"private": true,
"scripts": {
"dev": "tsx watch main.ts",
"start": "node --import tsx main.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@multica/core": "workspace:*",
"@nestjs/common": "^11.1.12",
"@nestjs/core": "^11.1.12",
"@nestjs/platform-socket.io": "^11.1.12",
"@nestjs/websockets": "^11.1.12",
"nestjs-pino": "^4.5.0",
"pino": "^10.3.0",
"pino-http": "^10.0.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"socket.io": "^4.8.3"
},
"devDependencies": {
"tsx": "catalog:",
"typescript": "catalog:"
}
}

View file

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 233 KiB

Before After
Before After

View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"noEmit": true
},
"include": ["*.ts", "**/*.ts"],
"exclude": ["node_modules"]
}

View file

@ -8,7 +8,7 @@ import {
Body,
Inject,
} from "@nestjs/common";
import { Hub } from "../hub/hub.js";
import { Hub } from "@multica/core";
@Controller("api")
export class AppController {

View file

@ -4,7 +4,7 @@ import { LoggerModule } from "nestjs-pino";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { AppController } from "./app.controller.js";
import { Hub } from "../hub/hub.js";
import { Hub } from "@multica/core";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const isDev = process.env["NODE_ENV"] !== "production";

27
apps/server/package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "@multica/server",
"version": "0.0.1",
"type": "module",
"private": true,
"scripts": {
"dev": "tsx watch main.ts",
"start": "node --import tsx main.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@multica/core": "workspace:*",
"@nestjs/common": "^11.1.12",
"@nestjs/core": "^11.1.12",
"@nestjs/serve-static": "^4.0.0",
"nestjs-pino": "^4.5.0",
"pino": "^10.3.0",
"pino-http": "^10.0.0",
"pino-pretty": "^10.0.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2"
},
"devDependencies": {
"tsx": "catalog:",
"typescript": "catalog:"
}
}

15
apps/server/tsconfig.json Normal file
View file

@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"noEmit": true
},
"include": ["*.ts", "**/*.ts"],
"exclude": ["node_modules"]
}

View file

@ -1,7 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: ["@multica/ui", "@multica/store"],
transpilePackages: ["@multica/ui", "@multica/store", "@multica/hooks", "@multica/sdk"],
headers: async () => [
{
source: "/sw.js",

View file

@ -13,10 +13,10 @@
"@multica/sdk": "workspace:*",
"@multica/store": "workspace:*",
"@multica/ui": "workspace:*",
"uuid": "^13.0.0",
"uuid": "catalog:",
"zustand": "catalog:",
"@hugeicons/core-free-icons": "^3.1.1",
"@hugeicons/react": "^1.1.4",
"@hugeicons/core-free-icons": "catalog:",
"@hugeicons/react": "catalog:",
"next": "16.1.6",
"next-themes": "^0.4.6",
"react": "catalog:",
@ -26,7 +26,7 @@
"@types/node": "catalog:",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"eslint": "^9",
"eslint": "catalog:",
"eslint-config-next": "16.1.6",
"typescript": "catalog:"
}

315
docs/package-management.md Normal file
View file

@ -0,0 +1,315 @@
# Package Management Guide
## Overview
Super Multica uses **pnpm workspaces** for monorepo management. This document covers package management, dependency handling, and merge conflict resolution.
---
## Directory Structure
```
super-multica/
├── apps/ # Deployable applications
│ ├── cli/ # @multica/cli
│ ├── desktop/ # @multica/desktop (Electron)
│ ├── gateway/ # @multica/gateway (NestJS WebSocket)
│ ├── server/ # @multica/server (NestJS REST)
│ ├── web/ # @multica/web (Next.js)
│ └── mobile/ # @multica/mobile (React Native)
├── packages/ # Shared libraries
│ ├── core/ # @multica/core (agent, hub, channels)
│ ├── sdk/ # @multica/sdk (gateway client)
│ ├── ui/ # @multica/ui (shared components)
│ ├── store/ # @multica/store (Zustand)
│ ├── hooks/ # @multica/hooks (React hooks)
│ ├── types/ # @multica/types (TypeScript types)
│ └── utils/ # @multica/utils (utility functions)
├── skills/ # Bundled agent skills
├── pnpm-workspace.yaml # Workspace definition
├── pnpm-lock.yaml # Lockfile (auto-generated)
└── .npmrc # pnpm configuration
```
---
## Key Configuration Files
### pnpm-workspace.yaml
Defines which directories are workspace packages:
```yaml
packages:
- "apps/*"
- "packages/*"
```
### .npmrc
**Required configuration for Electron packaging:**
```ini
shamefully-hoist=true
```
**Why?** electron-builder requires all dependencies to be hoisted to the root `node_modules`. Without this, Electron builds will fail with "Cannot find module" errors.
### pnpm-lock.yaml
- Auto-generated lockfile
- **Never manually edit**
- Always regenerate on conflicts
---
## Common Commands
### Install Dependencies
```bash
# Install all workspace dependencies
pnpm install
# Clean install (after changing .npmrc or major updates)
rm -rf node_modules apps/*/node_modules packages/*/node_modules
rm pnpm-lock.yaml
pnpm install
```
### Add Dependencies
```bash
# Add to root (shared dev tools)
pnpm add -D typescript -w
# Add to specific package
pnpm add lodash --filter @multica/core
# Add dev dependency to specific package
pnpm add -D vitest --filter @multica/core
# Add workspace dependency (internal package)
pnpm add @multica/utils --filter @multica/core --workspace
```
### Update Dependencies
```bash
# Update all
pnpm update --recursive
# Update specific package
pnpm update lodash --filter @multica/core
# Interactive update
pnpm update --interactive --recursive
```
### Run Scripts
```bash
# Run script in specific package
pnpm --filter @multica/desktop dev
pnpm --filter @multica/core build
# Run script in all packages
pnpm --recursive run build
# Run script in root
pnpm multica --help
```
---
## Workspace Dependencies
### Internal References
Use `workspace:*` for internal dependencies:
```json
{
"name": "@multica/desktop",
"dependencies": {
"@multica/core": "workspace:*",
"@multica/ui": "workspace:*",
"@multica/utils": "workspace:*"
}
}
```
### Dependency Direction
```
apps/ → depends on → packages/
packages/ui → depends on → packages/core
packages/core → depends on → packages/types, packages/utils
❌ Circular dependencies are forbidden
```
### Catalog (Shared Versions)
`pnpm-workspace.yaml` defines shared versions:
```yaml
catalog:
react: "19.2.3"
typescript: "^5.9.3"
```
Use in package.json:
```json
{
"dependencies": {
"react": "catalog:"
}
}
```
---
## Branch Merge & Conflicts
### High-Conflict Files
| File | Conflict Type | Resolution Strategy |
|------|---------------|---------------------|
| `pnpm-lock.yaml` | Auto-generated | **Always regenerate** |
| `*/package.json` | Version/deps | Manual merge |
| `pnpm-workspace.yaml` | Catalog versions | Manual merge |
| `turbo.json` | Pipeline config | Manual merge |
### Resolving pnpm-lock.yaml Conflicts
**Never manually resolve `pnpm-lock.yaml` conflicts.** It's a machine-generated file with complex checksums.
```bash
# 1. Accept either version (doesn't matter which)
git checkout --theirs pnpm-lock.yaml
# or
git checkout --ours pnpm-lock.yaml
# 2. Delete and regenerate
rm pnpm-lock.yaml
pnpm install
# 3. Stage the new lockfile
git add pnpm-lock.yaml
# 4. Continue with merge
git merge --continue
# or
git commit
```
### Standard Merge Workflow
```bash
# 1. Fetch and merge
git fetch origin main
git merge origin/main
# 2. If conflicts in pnpm-lock.yaml:
git checkout --theirs pnpm-lock.yaml
rm pnpm-lock.yaml
pnpm install
git add pnpm-lock.yaml
# 3. Resolve other conflicts manually
# Edit conflicted files...
git add <resolved-files>
# 4. Complete merge
git commit
# 5. Verify build
pnpm build
pnpm test
```
### After Major Merges
Always verify:
```bash
pnpm install # Ensure deps are correct
pnpm build # Verify build works
pnpm test # Run tests
pnpm typecheck # Check types
```
---
## Troubleshooting
### "Cannot find module" in Electron Build
**Cause:** electron-builder can't find hoisted dependencies.
**Solution:**
```bash
# Ensure .npmrc has:
echo 'shamefully-hoist=true' > .npmrc
# Clean reinstall
rm -rf node_modules apps/*/node_modules packages/*/node_modules
rm pnpm-lock.yaml
pnpm install
```
### Workspace Protocol Not Resolved
**Cause:** workspace:* not resolving correctly.
**Solution:**
```bash
# Check pnpm-workspace.yaml includes the package
# Ensure package name matches exactly
pnpm install
```
### Peer Dependency Warnings
**Cause:** Missing peer dependencies.
**Solution:**
```bash
# Usually safe to ignore, but if causing issues:
pnpm add <missing-peer> --filter <package>
```
### Build Order Issues
**Cause:** Turborepo not building dependencies first.
**Solution:** Check `turbo.json` has correct `dependsOn`:
```json
{
"tasks": {
"build": {
"dependsOn": ["^build"]
}
}
}
```
---
## Best Practices
1. **Always use pnpm** — Don't mix npm/yarn
2. **Commit lockfile** — Always commit `pnpm-lock.yaml` changes
3. **Don't edit lockfile manually** — Regenerate on conflicts
4. **Use workspace:*** — For internal dependencies
5. **Use catalog:** — For shared version management
6. **Clean install after .npmrc changes** — Delete node_modules and lockfile
7. **Verify after merge** — Run build and tests

View file

@ -9,13 +9,13 @@
"mu": "./bin/multica.mjs"
},
"scripts": {
"multica": "tsx src/agent/cli/index.ts",
"mu": "tsx src/agent/cli/index.ts",
"dev": "tsx src/agent/cli/index.ts dev",
"dev:desktop": "tsx src/agent/cli/index.ts dev desktop",
"dev:gateway": "tsx src/agent/cli/index.ts dev gateway",
"dev:web": "tsx src/agent/cli/index.ts dev web",
"dev:all": "tsx src/agent/cli/index.ts dev all",
"multica": "pnpm --filter @multica/cli dev",
"mu": "pnpm --filter @multica/cli dev",
"dev": "pnpm --filter @multica/cli dev -- dev",
"dev:desktop": "pnpm --filter @multica/cli dev -- dev desktop",
"dev:gateway": "pnpm --filter @multica/cli dev -- dev gateway",
"dev:web": "pnpm --filter @multica/cli dev -- dev web",
"dev:all": "pnpm --filter @multica/cli dev -- dev all",
"build": "turbo build",
"build:sdk": "pnpm --filter @multica/sdk build",
"build:cli": "node scripts/build-cli.js",
@ -33,12 +33,16 @@
"onlyBuiltDependencies": [
"electron",
"esbuild"
]
],
"overrides": {
"@types/react": "catalog:",
"@types/react-dom": "catalog:"
}
},
"devDependencies": {
"@types/node": "catalog:",
"@types/turndown": "^5.0.6",
"@types/uuid": "^11.0.0",
"@types/uuid": "catalog:",
"@vitest/coverage-v8": "^4.0.18",
"concurrently": "^9.2.1",
"esbuild": "^0.27.2",
@ -48,9 +52,9 @@
"vitest": "^4.0.18"
},
"dependencies": {
"@mariozechner/pi-agent-core": "^0.52.9",
"@mariozechner/pi-ai": "^0.52.9",
"@mariozechner/pi-coding-agent": "^0.52.9",
"@mariozechner/pi-agent-core": "catalog:",
"@mariozechner/pi-ai": "catalog:",
"@mariozechner/pi-coding-agent": "catalog:",
"@mozilla/readability": "^0.6.0",
"@multica/sdk": "workspace:*",
"@multica/store": "workspace:*",
@ -61,6 +65,7 @@
"@nestjs/serve-static": "^5.0.4",
"@nestjs/websockets": "^11.1.12",
"@sinclair/typebox": "^0.34.41",
"chokidar": "^5.0.0",
"croner": "^10.0.1",
"fast-glob": "^3.3.3",
"grammy": "^1.39.3",
@ -74,10 +79,10 @@
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"socket.io-client": "catalog:",
"turndown": "^7.2.2",
"undici": "^7.19.2",
"uuid": "^13.0.0",
"uuid": "catalog:",
"yaml": "^2.8.2"
}
}

View file

@ -0,0 +1,75 @@
{
"name": "@multica/core",
"version": "0.0.1",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./agent": {
"types": "./dist/agent/index.d.ts",
"import": "./dist/agent/index.js"
},
"./hub": {
"types": "./dist/hub/index.d.ts",
"import": "./dist/hub/index.js"
},
"./channels": {
"types": "./dist/channels/index.d.ts",
"import": "./dist/channels/index.js"
},
"./cron": {
"types": "./dist/cron/index.d.ts",
"import": "./dist/cron/index.js"
},
"./heartbeat": {
"types": "./dist/heartbeat/index.d.ts",
"import": "./dist/heartbeat/index.js"
},
"./media": {
"types": "./dist/media/index.d.ts",
"import": "./dist/media/index.js"
},
"./client": {
"types": "./dist/client/index.d.ts",
"import": "./dist/client/index.js"
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist"],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@multica/types": "workspace:*",
"@multica/utils": "workspace:*",
"socket.io-client": "catalog:",
"@mariozechner/pi-agent-core": "catalog:",
"@mariozechner/pi-ai": "catalog:",
"@mariozechner/pi-coding-agent": "catalog:",
"@sinclair/typebox": "^0.34.41",
"chokidar": "^5.0.0",
"croner": "^10.0.1",
"fast-glob": "^3.3.3",
"grammy": "^1.39.3",
"json5": "^2.2.3",
"linkedom": "^0.18.12",
"turndown": "^7.2.2",
"undici": "^7.19.2",
"uuid": "catalog:",
"yaml": "^2.8.2"
},
"devDependencies": {
"@types/turndown": "^5.0.6",
"@types/uuid": "catalog:",
"tsup": "^8.0.0",
"typescript": "catalog:",
"vitest": "^4.0.18"
}
}

View file

@ -18,7 +18,7 @@ import {
constants as fsConstants,
} from "node:fs";
import { join, dirname } from "node:path";
import { DATA_DIR } from "../../shared/paths.js";
import { DATA_DIR } from "@multica/utils";
import { AUTH_STORE_VERSION, AUTH_PROFILE_STORE_FILENAME } from "./constants.js";
import type { AuthProfileStore } from "./types.js";

View file

@ -0,0 +1,151 @@
/**
* Terminal Colors and Styling
*
* Simple ANSI color utilities for terminal output
*/
// Check if colors should be disabled
const NO_COLOR = process.env.NO_COLOR !== undefined || process.env.TERM === "dumb";
type StyleFn = (s: string) => string;
const identity: StyleFn = (s) => s;
function style(code: number, reset: number = 0): StyleFn {
if (NO_COLOR) return identity;
return (s: string) => `\x1b[${code}m${s}\x1b[${reset}m`;
}
// Basic styles
export const reset = "\x1b[0m";
export const bold = style(1, 22);
export const dim = style(2, 22);
export const italic = style(3, 23);
export const underline = style(4, 24);
export const inverse = style(7, 27);
// Foreground colors
export const black = style(30, 39);
export const red = style(31, 39);
export const green = style(32, 39);
export const yellow = style(33, 39);
export const blue = style(34, 39);
export const magenta = style(35, 39);
export const cyan = style(36, 39);
export const white = style(37, 39);
export const gray = style(90, 39);
// Bright colors
export const brightRed = style(91, 39);
export const brightGreen = style(92, 39);
export const brightYellow = style(93, 39);
export const brightBlue = style(94, 39);
export const brightMagenta = style(95, 39);
export const brightCyan = style(96, 39);
// Background colors
export const bgRed = style(41, 49);
export const bgGreen = style(42, 49);
export const bgYellow = style(43, 49);
export const bgBlue = style(44, 49);
// Semantic colors for the CLI
export const colors = {
// UI elements
prompt: cyan,
promptSymbol: brightCyan,
sessionId: dim,
// Tool output
toolName: yellow,
toolArgs: dim,
toolBullet: cyan,
toolArrow: dim,
toolError: red,
// Messages
error: red,
warning: yellow,
success: green,
info: blue,
// Status bar
statusBg: inverse,
statusLabel: dim,
statusValue: white,
// Welcome banner
bannerBorder: cyan,
bannerText: brightCyan,
// Suggestions
suggestionSelected: inverse,
suggestionDim: dim,
suggestionLabel: gray,
};
// Spinner frames for thinking indicator
export const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
// Alternative spinner styles
export const spinnerStyles = {
dots: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
line: ["-", "\\", "|", "/"],
arc: ["◜", "◠", "◝", "◞", "◡", "◟"],
bounce: ["⠁", "⠂", "⠄", "⠂"],
pulse: ["◯", "◔", "◑", "◕", "●", "◕", "◑", "◔"],
};
/**
* Create a spinner instance
*/
export function createSpinner(options: {
stream?: NodeJS.WritableStream;
frames?: string[];
interval?: number;
} = {}) {
const {
stream = process.stderr,
frames = spinnerFrames,
interval = 80,
} = options;
let frameIndex = 0;
let timer: ReturnType<typeof setInterval> | null = null;
let currentText = "";
const render = () => {
const frame = colors.toolBullet(frames[frameIndex % frames.length]!);
stream.write(`\r\x1b[K${frame} ${currentText}`);
frameIndex++;
};
return {
start(text: string) {
currentText = text;
frameIndex = 0;
if (timer) clearInterval(timer);
render();
timer = setInterval(render, interval);
},
update(text: string) {
currentText = text;
},
stop(finalText?: string) {
if (timer) {
clearInterval(timer);
timer = null;
}
stream.write("\r\x1b[K");
if (finalText) {
stream.write(finalText + "\n");
}
},
isSpinning() {
return timer !== null;
},
};
}

Some files were not shown because too many files have changed in this diff Show more