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:
parent
5ba8c87744
commit
6ef58a0cab
304 changed files with 5699 additions and 3635 deletions
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
shamefully-hoist=true
|
||||||
140
CLAUDE.md
140
CLAUDE.md
|
|
@ -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
24
apps/cli/package.json
Normal 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:"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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";
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
@ -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");
|
||||||
|
|
@ -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 = {
|
||||||
|
|
@ -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");
|
||||||
|
|
@ -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";
|
||||||
|
|
@ -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";
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
@ -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
318
apps/cli/src/output.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
9
apps/cli/tsconfig.json
Normal 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
15
apps/cli/tsup.config.ts
Normal 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:/,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
@ -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": [
|
||||||
|
|
|
||||||
36
apps/desktop/electron.vite.config.ts
Normal file
36
apps/desktop/electron.vite.config.ts
Normal 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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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).
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
@ -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
28
apps/gateway/package.json
Normal 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:"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 233 KiB |
15
apps/gateway/tsconfig.json
Normal file
15
apps/gateway/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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
27
apps/server/package.json
Normal 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
15
apps/server/tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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
315
docs/package-management.md
Normal 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
|
||||||
33
package.json
33
package.json
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
75
packages/core/package.json
Normal file
75
packages/core/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
151
packages/core/src/agent/cli/colors.ts
Normal file
151
packages/core/src/agent/cli/colors.ts
Normal 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
Loading…
Add table
Add a link
Reference in a new issue