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 ## Monorepo Structure
- **`src/`** — Core modules (agent engine, gateway, hub, shared types) ```
- **`apps/desktop`** — Electron + Vite + React desktop app (`@multica/desktop`) — **primary development target** super-multica/
- **`apps/web`** — Next.js 16 web app (`@multica/web`, port 3001) ├── apps/
- **`packages/ui`** — Shared UI component library (`@multica/ui`, Shadcn/Tailwind CSS v4) │ ├── cli/ ← Command-line interface (`@multica/cli`)
- **`packages/sdk`** — Gateway client SDK (`@multica/sdk`, Socket.io) │ ├── desktop/ ← Electron + Vite + React (`@multica/desktop`) — primary target
- **`packages/store`** — Zustand state management (`@multica/store`) │ ├── gateway/ ← NestJS WebSocket gateway (`@multica/gateway`)
- **`skills/`** — Bundled agent skills (commit, code-review, skill-creator) │ ├── 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 ## Common Commands
@ -23,33 +37,35 @@ Super Multica is a distributed AI agent framework with a monorepo architecture.
pnpm install pnpm install
# Multica CLI (unified entry point) # Multica CLI (unified entry point)
multica # Interactive mode (default) pnpm multica # Interactive mode (default)
multica run "<prompt>" # Run a single prompt pnpm multica run "<prompt>" # Run a single prompt
multica chat # Interactive REPL mode pnpm multica chat # Interactive REPL mode
multica session list # List sessions pnpm multica session list # List sessions
multica profile list # List profiles pnpm multica profile list # List profiles
multica skills list # List skills pnpm multica skills list # List skills
multica tools list # List tools pnpm multica tools list # List tools
multica credentials init # Initialize credentials pnpm multica credentials init # Initialize credentials
multica dev # Start desktop app (default) pnpm multica help # Show help
multica help # Show help
# Development servers # Development servers
multica dev # Desktop app (default, recommended) pnpm dev # Desktop app (default, recommended)
multica dev gateway # WebSocket gateway only (for remote clients) pnpm dev:desktop # Desktop app
multica dev web # Next.js web app pnpm dev:gateway # WebSocket gateway only
multica dev all # Gateway + web app pnpm dev:web # Next.js web app
pnpm dev:all # Gateway + web app
# Build (turbo-orchestrated) # Build
pnpm build pnpm build # Build all (turbo-orchestrated)
pnpm --filter @multica/desktop build
pnpm --filter @multica/core build
# Type checking # Type checking
pnpm typecheck pnpm typecheck
# Testing (vitest, tests live in src/**/*.test.ts) # Testing (vitest)
pnpm test # Single run pnpm test # Single run
pnpm test:watch # Watch mode pnpm test:watch # Watch mode
pnpm test:coverage # With v8 coverage pnpm test:coverage # With v8 coverage
``` ```
## Architecture ## Architecture
@ -66,21 +82,41 @@ Web App (requires Gateway)
→ Hub + Agent Engine → 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 ## Tech Stack & Config
- **Package manager**: pnpm 10 with workspaces (`pnpm-workspace.yaml`) - **Package manager**: pnpm 10 with workspaces (`pnpm-workspace.yaml`)
- **Build orchestration**: Turborepo (`turbo.json`) - **Build orchestration**: Turborepo (`turbo.json`)
- **TypeScript**: ESNext target, NodeNext modules, strict mode, `verbatimModuleSyntax`, `experimentalDecorators` (NestJS) - **TypeScript**: ESNext target, NodeNext modules, strict mode
- **Testing**: Vitest with globals enabled, node environment - **Testing**: Vitest with globals enabled
- **Frontend**: React 19, Next.js 16, Tailwind CSS v4, Shadcn/UI (zinc base, hugeicons) - **Frontend**: React 19, Next.js 16, Tailwind CSS v4, Shadcn/UI
- **Backend**: NestJS 11, Socket.io, Pino logging - **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 ## Code Style
@ -88,46 +124,36 @@ Web App (requires Gateway)
## Credentials Setup ## Credentials Setup
Use JSON5 credential files instead of `.env`:
```bash ```bash
multica credentials init pnpm multica credentials init
``` ```
This creates: Creates:
- `~/.super-multica/credentials.json5` (LLM providers + built-in tools) - `~/.super-multica/credentials.json5` (LLM providers + built-in tools)
- `~/.super-multica/skills.env.json5` (skills / plugins / integrations) - `~/.super-multica/skills.env.json5` (skills / plugins / integrations)
## Atomic Commits ## 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 1. Run `git status` and `git diff` to see all modifications
2. Skip if no changes exist 2. Skip if no changes exist
3. Group changes by logical purpose (feature, fix, refactor, docs, test, chore) 3. Group changes by logical purpose (feature, fix, refactor, docs, test, chore)
4. Stage and commit each group separately 4. Stage and commit each group separately
**Format**: Conventional commits — `<type>(<scope>): <description>` **Format**: `<type>(<scope>): <description>`
Types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore` 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 ### Examples
If you modified: ```bash
- `src/api/user.ts` (added new endpoint) git add packages/core/src/agent/runner.ts packages/core/src/agent/runner.test.ts
- `src/api/user.test.ts` (tests for new endpoint) git commit -m "feat(agent): add streaming support"
- `src/utils/format.ts` (refactored helper)
- `README.md` (updated docs)
Create three commits: git add packages/utils/src/format.ts
1. `git add src/api/user.ts src/api/user.test.ts && git commit -m "feat(api): add user profile endpoint"` git commit -m "refactor(utils): simplify date formatting"
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 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 * as readline from "readline";
import { Agent } from "../../runner.js"; import { Agent } from "@multica/core";
import type { AgentOptions } from "../../types.js"; import type { AgentOptions } from "@multica/core";
import { SkillManager } from "../../skills/index.js"; import { SkillManager } from "@multica/core";
import { autocompleteInput, type AutocompleteOption } from "../autocomplete.js"; import { autocompleteInput, type AutocompleteOption } from "../autocomplete.js";
import { colors, dim, cyan, brightCyan, yellow, green, gray, red } from "../colors.js"; import { colors, dim, cyan, brightCyan, yellow, green, gray, red } from "../colors.js";
import { import {
@ -18,7 +18,7 @@ import {
getLoginInstructions, getLoginInstructions,
getProviderMeta, getProviderMeta,
type ProviderInfo, type ProviderInfo,
} from "../../providers/index.js"; } from "@multica/core";
type ChatOptions = { type ChatOptions = {
profile?: string | undefined; profile?: string | undefined;

View file

@ -9,7 +9,7 @@
import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs"; import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
import { dirname } from "node:path"; 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"; import { cyan, yellow, green, dim, red } from "../colors.js";
type Command = "init" | "show" | "edit" | "help"; type Command = "init" | "show" | "edit" | "help";

View file

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

View file

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

View file

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

View file

@ -9,7 +9,7 @@
import { existsSync, readdirSync, readFileSync, unlinkSync, statSync } from "node:fs"; import { existsSync, readdirSync, readFileSync, unlinkSync, statSync } from "node:fs";
import { join } from "node:path"; 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"; import { cyan, yellow, green, dim, red } from "../colors.js";
const SESSIONS_DIR = join(DATA_DIR, "sessions"); const SESSIONS_DIR = join(DATA_DIR, "sessions");

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
import { Agent } from "../runner.js"; import { Agent } from "@multica/core";
type CliOptions = { type CliOptions = {
profile?: string | undefined; 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, loadAgentProfile,
getProfileDir, getProfileDir,
profileExists, profileExists,
} from "../profile/index.js"; } from "@multica/core";
import { DATA_DIR } from "../../shared/index.js"; import { DATA_DIR } from "@multica/utils";
const DEFAULT_BASE_DIR = join(DATA_DIR, "agent-profiles"); const DEFAULT_BASE_DIR = join(DATA_DIR, "agent-profiles");

View file

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

View file

@ -9,9 +9,9 @@
* pnpm tools:cli groups # Show all tool groups * pnpm tools:cli groups # Show all tool groups
*/ */
import { createAllTools } from "../tools.js"; import { createAllTools } from "@multica/core";
import { filterTools, type ToolsConfig } from "../tools/policy.js"; import { filterTools, type ToolsConfig } from "@multica/core";
import { TOOL_GROUPS, expandToolGroups } from "../tools/groups.js"; import { TOOL_GROUPS, expandToolGroups } from "@multica/core";
type Command = "list" | "groups" | "help"; 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", "appId": "YourAppID",
"asar": true, "asar": true,
"productName": "YourAppName", "productName": "YourAppName",
"electronVersion": "30.5.1",
"directories": { "directories": {
"output": "release/${version}" "output": "release/${version}"
}, },
"files": [ "files": [
"dist", "out"
"dist-electron"
], ],
"mac": { "mac": {
"target": [ "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", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "electron-vite dev",
"build": "tsc && vite build && electron-builder", "build": "electron-vite build && electron-builder",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "electron-vite preview",
"preview": "vite preview" "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
}, },
"dependencies": { "dependencies": {
"@hugeicons/core-free-icons": "^3.1.1", "@hugeicons/core-free-icons": "catalog:",
"@hugeicons/react": "^1.1.4", "@hugeicons/react": "catalog:",
"@multica/core": "workspace:*",
"@multica/hooks": "workspace:*", "@multica/hooks": "workspace:*",
"@multica/sdk": "workspace:*", "@multica/sdk": "workspace:*",
"@multica/store": "workspace:*", "@multica/store": "workspace:*",
@ -20,27 +21,27 @@
"react": "catalog:", "react": "catalog:",
"react-dom": "catalog:", "react-dom": "catalog:",
"react-router-dom": "^7.13.0", "react-router-dom": "^7.13.0",
"socket.io-client": "^4.8.3", "socket.io-client": "catalog:",
"uuid": "^13.0.0", "uuid": "catalog:",
"zustand": "catalog:" "zustand": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "catalog:",
"@types/react": "catalog:", "@types/react": "catalog:",
"@types/react-dom": "catalog:", "@types/react-dom": "catalog:",
"@typescript-eslint/eslint-plugin": "^7.1.1", "@typescript-eslint/eslint-plugin": "^7.1.1",
"@typescript-eslint/parser": "^7.1.1", "@typescript-eslint/parser": "^7.1.1",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"electron": "^30.0.1", "electron": "^33.4.11",
"electron-builder": "^24.13.3", "electron-builder": "^26.7.0",
"electron-builder-squirrel-windows": "^26.7.0",
"electron-vite": "^5.0.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.5",
"tailwindcss": "^4", "tailwindcss": "catalog:",
"typescript": "catalog:", "typescript": "catalog:",
"vite": "^5.1.6", "vite": "^5.1.6"
"vite-plugin-electron": "^0.28.6",
"vite-plugin-electron-renderer": "^0.14.5"
}, },
"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 path from 'node:path'
import { registerAllIpcHandlers, initializeApp, cleanupAll, setupDeviceConfirmation } from './ipc/index.js' 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)) 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 VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron') // electron-vite outputs to out/ directory
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') 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 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, width: 1200,
height: 800, height: 800,
webPreferences: { webPreferences: {
preload: path.join(__dirname, 'preload.mjs'), preload: path.join(__dirname, '../preload/index.cjs'),
// Enable node integration for IPC // Enable node integration for IPC
contextIsolation: true, contextIsolation: true,
nodeIntegration: false, nodeIntegration: false,

View file

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

View file

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

View file

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

View file

@ -13,13 +13,11 @@ import {
getProviderMeta, getProviderMeta,
isProviderAvailable, isProviderAvailable,
getLoginInstructions, getLoginInstructions,
type ProviderInfo,
} from '../../../../src/agent/providers/index.js'
import {
readClaudeCliCredentials, readClaudeCliCredentials,
readCodexCliCredentials, readCodexCliCredentials,
} from '../../../../src/agent/providers/oauth/cli-credentials.js' credentialManager,
import { credentialManager } from '../../../../src/agent/credentials.js' type ProviderInfo,
} from '@multica/core'
/** /**
* Provider info returned to renderer (matches ProviderInfo from registry). * 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)}`) 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({ const result = await addSkill({
source, source,
@ -261,7 +261,7 @@ export function registerSkillsIpcHandlers(): void {
ipcMain.handle('skills:remove', async (_event, name: string) => { ipcMain.handle('skills:remove', async (_event, name: string) => {
console.log(`[IPC] skills:remove called: name=${name}`) 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) 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, Body,
Inject, Inject,
} from "@nestjs/common"; } from "@nestjs/common";
import { Hub } from "../hub/hub.js"; import { Hub } from "@multica/core";
@Controller("api") @Controller("api")
export class AppController { export class AppController {

View file

@ -4,7 +4,7 @@ import { LoggerModule } from "nestjs-pino";
import { join } from "node:path"; import { join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { AppController } from "./app.controller.js"; 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 __dirname = fileURLToPath(new URL(".", import.meta.url));
const isDev = process.env["NODE_ENV"] !== "production"; 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"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
transpilePackages: ["@multica/ui", "@multica/store"], transpilePackages: ["@multica/ui", "@multica/store", "@multica/hooks", "@multica/sdk"],
headers: async () => [ headers: async () => [
{ {
source: "/sw.js", source: "/sw.js",

View file

@ -13,10 +13,10 @@
"@multica/sdk": "workspace:*", "@multica/sdk": "workspace:*",
"@multica/store": "workspace:*", "@multica/store": "workspace:*",
"@multica/ui": "workspace:*", "@multica/ui": "workspace:*",
"uuid": "^13.0.0", "uuid": "catalog:",
"zustand": "catalog:", "zustand": "catalog:",
"@hugeicons/core-free-icons": "^3.1.1", "@hugeicons/core-free-icons": "catalog:",
"@hugeicons/react": "^1.1.4", "@hugeicons/react": "catalog:",
"next": "16.1.6", "next": "16.1.6",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "catalog:", "react": "catalog:",
@ -26,7 +26,7 @@
"@types/node": "catalog:", "@types/node": "catalog:",
"@types/react": "catalog:", "@types/react": "catalog:",
"@types/react-dom": "catalog:", "@types/react-dom": "catalog:",
"eslint": "^9", "eslint": "catalog:",
"eslint-config-next": "16.1.6", "eslint-config-next": "16.1.6",
"typescript": "catalog:" "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" "mu": "./bin/multica.mjs"
}, },
"scripts": { "scripts": {
"multica": "tsx src/agent/cli/index.ts", "multica": "pnpm --filter @multica/cli dev",
"mu": "tsx src/agent/cli/index.ts", "mu": "pnpm --filter @multica/cli dev",
"dev": "tsx src/agent/cli/index.ts dev", "dev": "pnpm --filter @multica/cli dev -- dev",
"dev:desktop": "tsx src/agent/cli/index.ts dev desktop", "dev:desktop": "pnpm --filter @multica/cli dev -- dev desktop",
"dev:gateway": "tsx src/agent/cli/index.ts dev gateway", "dev:gateway": "pnpm --filter @multica/cli dev -- dev gateway",
"dev:web": "tsx src/agent/cli/index.ts dev web", "dev:web": "pnpm --filter @multica/cli dev -- dev web",
"dev:all": "tsx src/agent/cli/index.ts dev all", "dev:all": "pnpm --filter @multica/cli dev -- dev all",
"build": "turbo build", "build": "turbo build",
"build:sdk": "pnpm --filter @multica/sdk build", "build:sdk": "pnpm --filter @multica/sdk build",
"build:cli": "node scripts/build-cli.js", "build:cli": "node scripts/build-cli.js",
@ -33,12 +33,16 @@
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"electron", "electron",
"esbuild" "esbuild"
] ],
"overrides": {
"@types/react": "catalog:",
"@types/react-dom": "catalog:"
}
}, },
"devDependencies": { "devDependencies": {
"@types/node": "catalog:", "@types/node": "catalog:",
"@types/turndown": "^5.0.6", "@types/turndown": "^5.0.6",
"@types/uuid": "^11.0.0", "@types/uuid": "catalog:",
"@vitest/coverage-v8": "^4.0.18", "@vitest/coverage-v8": "^4.0.18",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"esbuild": "^0.27.2", "esbuild": "^0.27.2",
@ -48,9 +52,9 @@
"vitest": "^4.0.18" "vitest": "^4.0.18"
}, },
"dependencies": { "dependencies": {
"@mariozechner/pi-agent-core": "^0.52.9", "@mariozechner/pi-agent-core": "catalog:",
"@mariozechner/pi-ai": "^0.52.9", "@mariozechner/pi-ai": "catalog:",
"@mariozechner/pi-coding-agent": "^0.52.9", "@mariozechner/pi-coding-agent": "catalog:",
"@mozilla/readability": "^0.6.0", "@mozilla/readability": "^0.6.0",
"@multica/sdk": "workspace:*", "@multica/sdk": "workspace:*",
"@multica/store": "workspace:*", "@multica/store": "workspace:*",
@ -61,6 +65,7 @@
"@nestjs/serve-static": "^5.0.4", "@nestjs/serve-static": "^5.0.4",
"@nestjs/websockets": "^11.1.12", "@nestjs/websockets": "^11.1.12",
"@sinclair/typebox": "^0.34.41", "@sinclair/typebox": "^0.34.41",
"chokidar": "^5.0.0",
"croner": "^10.0.1", "croner": "^10.0.1",
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
"grammy": "^1.39.3", "grammy": "^1.39.3",
@ -74,10 +79,10 @@
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"socket.io": "^4.8.3", "socket.io": "^4.8.3",
"socket.io-client": "^4.8.3", "socket.io-client": "catalog:",
"turndown": "^7.2.2", "turndown": "^7.2.2",
"undici": "^7.19.2", "undici": "^7.19.2",
"uuid": "^13.0.0", "uuid": "catalog:",
"yaml": "^2.8.2" "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, constants as fsConstants,
} from "node:fs"; } from "node:fs";
import { join, dirname } from "node:path"; 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 { AUTH_STORE_VERSION, AUTH_PROFILE_STORE_FILENAME } from "./constants.js";
import type { AuthProfileStore } from "./types.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