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
|
||||
|
||||
- **`src/`** — Core modules (agent engine, gateway, hub, shared types)
|
||||
- **`apps/desktop`** — Electron + Vite + React desktop app (`@multica/desktop`) — **primary development target**
|
||||
- **`apps/web`** — Next.js 16 web app (`@multica/web`, port 3001)
|
||||
- **`packages/ui`** — Shared UI component library (`@multica/ui`, Shadcn/Tailwind CSS v4)
|
||||
- **`packages/sdk`** — Gateway client SDK (`@multica/sdk`, Socket.io)
|
||||
- **`packages/store`** — Zustand state management (`@multica/store`)
|
||||
- **`skills/`** — Bundled agent skills (commit, code-review, skill-creator)
|
||||
```
|
||||
super-multica/
|
||||
├── apps/
|
||||
│ ├── cli/ ← Command-line interface (`@multica/cli`)
|
||||
│ ├── desktop/ ← Electron + Vite + React (`@multica/desktop`) — primary target
|
||||
│ ├── gateway/ ← NestJS WebSocket gateway (`@multica/gateway`)
|
||||
│ ├── server/ ← NestJS REST API server (`@multica/server`)
|
||||
│ ├── web/ ← Next.js 16 web app (`@multica/web`, port 3001)
|
||||
│ └── mobile/ ← React Native mobile app (`@multica/mobile`)
|
||||
│
|
||||
├── packages/
|
||||
│ ├── core/ ← Core agent engine, hub, channels (`@multica/core`)
|
||||
│ ├── sdk/ ← Gateway client SDK (`@multica/sdk`, Socket.io)
|
||||
│ ├── ui/ ← Shared UI components (`@multica/ui`, Shadcn/Tailwind v4)
|
||||
│ ├── store/ ← Zustand state management (`@multica/store`)
|
||||
│ ├── hooks/ ← React hooks (`@multica/hooks`)
|
||||
│ ├── types/ ← Shared TypeScript types (`@multica/types`)
|
||||
│ └── utils/ ← Utility functions (`@multica/utils`)
|
||||
│
|
||||
└── skills/ ← Bundled agent skills
|
||||
```
|
||||
|
||||
## Common Commands
|
||||
|
||||
|
|
@ -23,33 +37,35 @@ Super Multica is a distributed AI agent framework with a monorepo architecture.
|
|||
pnpm install
|
||||
|
||||
# Multica CLI (unified entry point)
|
||||
multica # Interactive mode (default)
|
||||
multica run "<prompt>" # Run a single prompt
|
||||
multica chat # Interactive REPL mode
|
||||
multica session list # List sessions
|
||||
multica profile list # List profiles
|
||||
multica skills list # List skills
|
||||
multica tools list # List tools
|
||||
multica credentials init # Initialize credentials
|
||||
multica dev # Start desktop app (default)
|
||||
multica help # Show help
|
||||
pnpm multica # Interactive mode (default)
|
||||
pnpm multica run "<prompt>" # Run a single prompt
|
||||
pnpm multica chat # Interactive REPL mode
|
||||
pnpm multica session list # List sessions
|
||||
pnpm multica profile list # List profiles
|
||||
pnpm multica skills list # List skills
|
||||
pnpm multica tools list # List tools
|
||||
pnpm multica credentials init # Initialize credentials
|
||||
pnpm multica help # Show help
|
||||
|
||||
# Development servers
|
||||
multica dev # Desktop app (default, recommended)
|
||||
multica dev gateway # WebSocket gateway only (for remote clients)
|
||||
multica dev web # Next.js web app
|
||||
multica dev all # Gateway + web app
|
||||
pnpm dev # Desktop app (default, recommended)
|
||||
pnpm dev:desktop # Desktop app
|
||||
pnpm dev:gateway # WebSocket gateway only
|
||||
pnpm dev:web # Next.js web app
|
||||
pnpm dev:all # Gateway + web app
|
||||
|
||||
# Build (turbo-orchestrated)
|
||||
pnpm build
|
||||
# Build
|
||||
pnpm build # Build all (turbo-orchestrated)
|
||||
pnpm --filter @multica/desktop build
|
||||
pnpm --filter @multica/core build
|
||||
|
||||
# Type checking
|
||||
pnpm typecheck
|
||||
|
||||
# Testing (vitest, tests live in src/**/*.test.ts)
|
||||
pnpm test # Single run
|
||||
pnpm test:watch # Watch mode
|
||||
pnpm test:coverage # With v8 coverage
|
||||
# Testing (vitest)
|
||||
pnpm test # Single run
|
||||
pnpm test:watch # Watch mode
|
||||
pnpm test:coverage # With v8 coverage
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
|
@ -66,21 +82,41 @@ Web App (requires Gateway)
|
|||
→ Hub + Agent Engine
|
||||
```
|
||||
|
||||
**Agent Engine** (`src/agent/`): Orchestrates LLM interactions with multi-provider support (OpenAI, Anthropic, DeepSeek, Kimi, Groq, Mistral, Google, Together). Features session management (JSONL-based, UUIDv7 IDs), profile system (`~/.super-multica/agent-profiles/`), modular skills with hot-reload, and token-aware context window guards (compaction modes: tokens, count, summary). Unified CLI in `src/agent/cli/index.ts` with subcommands in `src/agent/cli/commands/`.
|
||||
**Agent Engine** (`packages/core/src/agent/`): Orchestrates LLM interactions with multi-provider support (OpenAI, Anthropic, DeepSeek, Kimi, Groq, Mistral, Google, Together). Features session management (JSONL-based, UUIDv7 IDs), profile system (`~/.super-multica/agent-profiles/`), modular skills with hot-reload, and token-aware context window guards.
|
||||
|
||||
**Hub** (`src/hub/`): Manages agents and communication channels. Embedded in desktop app, or runs standalone for web clients.
|
||||
**Hub** (`packages/core/src/hub/`): Manages agents and communication channels. Embedded in desktop app, or runs standalone for web clients.
|
||||
|
||||
**Gateway** (`src/gateway/`): NestJS WebSocket server with Socket.io for remote client access, message routing, and device verification.
|
||||
**Gateway** (`apps/gateway/`): NestJS WebSocket server with Socket.io for remote client access, message routing, and device verification.
|
||||
|
||||
**CLI** (`apps/cli/`): Command-line interface. Entry point: `apps/cli/src/index.ts`.
|
||||
|
||||
## Tech Stack & Config
|
||||
|
||||
- **Package manager**: pnpm 10 with workspaces (`pnpm-workspace.yaml`)
|
||||
- **Build orchestration**: Turborepo (`turbo.json`)
|
||||
- **TypeScript**: ESNext target, NodeNext modules, strict mode, `verbatimModuleSyntax`, `experimentalDecorators` (NestJS)
|
||||
- **Testing**: Vitest with globals enabled, node environment
|
||||
- **Frontend**: React 19, Next.js 16, Tailwind CSS v4, Shadcn/UI (zinc base, hugeicons)
|
||||
- **TypeScript**: ESNext target, NodeNext modules, strict mode
|
||||
- **Testing**: Vitest with globals enabled
|
||||
- **Frontend**: React 19, Next.js 16, Tailwind CSS v4, Shadcn/UI
|
||||
- **Backend**: NestJS 11, Socket.io, Pino logging
|
||||
- **CLI bundling**: esbuild → `bin/` directory
|
||||
- **Desktop**: Electron 33+, electron-vite, electron-builder
|
||||
|
||||
## pnpm Configuration
|
||||
|
||||
**Required `.npmrc` for Electron packaging:**
|
||||
|
||||
```ini
|
||||
shamefully-hoist=true
|
||||
```
|
||||
|
||||
After adding/changing `.npmrc`:
|
||||
|
||||
```bash
|
||||
rm -rf node_modules apps/*/node_modules packages/*/node_modules
|
||||
rm pnpm-lock.yaml
|
||||
pnpm install
|
||||
```
|
||||
|
||||
See `docs/package-management.md` for detailed package management guide.
|
||||
|
||||
## Code Style
|
||||
|
||||
|
|
@ -88,46 +124,36 @@ Web App (requires Gateway)
|
|||
|
||||
## Credentials Setup
|
||||
|
||||
Use JSON5 credential files instead of `.env`:
|
||||
|
||||
```bash
|
||||
multica credentials init
|
||||
pnpm multica credentials init
|
||||
```
|
||||
|
||||
This creates:
|
||||
Creates:
|
||||
- `~/.super-multica/credentials.json5` (LLM providers + built-in tools)
|
||||
- `~/.super-multica/skills.env.json5` (skills / plugins / integrations)
|
||||
|
||||
## Atomic Commits
|
||||
|
||||
After completing any task that modifies code, you MUST create atomic commits before ending the conversation.
|
||||
After completing any task that modifies code, create atomic commits:
|
||||
|
||||
1. Run `git status` and `git diff` to see all modifications
|
||||
2. Skip if no changes exist
|
||||
3. Group changes by logical purpose (feature, fix, refactor, docs, test, chore)
|
||||
4. Stage and commit each group separately
|
||||
|
||||
**Format**: Conventional commits — `<type>(<scope>): <description>`
|
||||
**Format**: `<type>(<scope>): <description>`
|
||||
|
||||
Types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`
|
||||
|
||||
**Rules**:
|
||||
- Each commit should be independently meaningful and buildable
|
||||
- Related test files go with their implementation
|
||||
- Never create empty commits or combine unrelated changes
|
||||
- If all changes are related to one logical unit, a single commit is fine
|
||||
- Keep commit messages concise but descriptive
|
||||
- `git commit --amend` only for immediate small fixes to the last commit
|
||||
|
||||
### Examples
|
||||
|
||||
If you modified:
|
||||
- `src/api/user.ts` (added new endpoint)
|
||||
- `src/api/user.test.ts` (tests for new endpoint)
|
||||
- `src/utils/format.ts` (refactored helper)
|
||||
- `README.md` (updated docs)
|
||||
```bash
|
||||
git add packages/core/src/agent/runner.ts packages/core/src/agent/runner.test.ts
|
||||
git commit -m "feat(agent): add streaming support"
|
||||
|
||||
Create three commits:
|
||||
1. `git add src/api/user.ts src/api/user.test.ts && git commit -m "feat(api): add user profile endpoint"`
|
||||
2. `git add src/utils/format.ts && git commit -m "refactor(utils): simplify date formatting logic"`
|
||||
3. `git add README.md && git commit -m "docs: update API documentation"`
|
||||
git add packages/utils/src/format.ts
|
||||
git commit -m "refactor(utils): simplify date formatting"
|
||||
|
||||
git add README.md
|
||||
git commit -m "docs: update API documentation"
|
||||
```
|
||||
|
|
|
|||
24
apps/cli/package.json
Normal file
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 { Agent } from "../../runner.js";
|
||||
import type { AgentOptions } from "../../types.js";
|
||||
import { SkillManager } from "../../skills/index.js";
|
||||
import { Agent } from "@multica/core";
|
||||
import type { AgentOptions } from "@multica/core";
|
||||
import { SkillManager } from "@multica/core";
|
||||
import { autocompleteInput, type AutocompleteOption } from "../autocomplete.js";
|
||||
import { colors, dim, cyan, brightCyan, yellow, green, gray, red } from "../colors.js";
|
||||
import {
|
||||
|
|
@ -18,7 +18,7 @@ import {
|
|||
getLoginInstructions,
|
||||
getProviderMeta,
|
||||
type ProviderInfo,
|
||||
} from "../../providers/index.js";
|
||||
} from "@multica/core";
|
||||
|
||||
type ChatOptions = {
|
||||
profile?: string | undefined;
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import { existsSync, mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
import { getCredentialsPath, getSkillsEnvPath } from "../../credentials.js";
|
||||
import { getCredentialsPath, getSkillsEnvPath } from "@multica/core";
|
||||
import { cyan, yellow, green, dim, red } from "../colors.js";
|
||||
|
||||
type Command = "init" | "show" | "edit" | "help";
|
||||
|
|
@ -22,7 +22,7 @@ import {
|
|||
isValidCronExpr,
|
||||
type CronSchedule,
|
||||
type CronJobInput,
|
||||
} from "../../../cron/index.js";
|
||||
} from "@multica/core";
|
||||
|
||||
type Command = "status" | "list" | "add" | "run" | "enable" | "disable" | "remove" | "logs" | "help";
|
||||
|
||||
|
|
@ -18,10 +18,10 @@ import {
|
|||
loadAgentProfile,
|
||||
getProfileDir,
|
||||
profileExists,
|
||||
} from "../../profile/index.js";
|
||||
import { DATA_DIR } from "../../../shared/index.js";
|
||||
} from "@multica/core";
|
||||
import { DATA_DIR } from "@multica/utils";
|
||||
import { cyan, yellow, green, dim, red, brightCyan, gray, colors } from "../colors.js";
|
||||
import { Agent } from "../../runner.js";
|
||||
import { Agent } from "@multica/core";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const SETUP_SKILL_PATH = join(__dirname, "../../../../skills/profile-setup/SKILL.md");
|
||||
|
|
@ -6,9 +6,9 @@
|
|||
* echo "prompt" | multica run
|
||||
*/
|
||||
|
||||
import { Agent } from "../../runner.js";
|
||||
import type { AgentOptions } from "../../types.js";
|
||||
import type { ToolsConfig } from "../../tools/policy.js";
|
||||
import { Agent } from "@multica/core";
|
||||
import type { AgentOptions } from "@multica/core";
|
||||
import type { ToolsConfig } from "@multica/core";
|
||||
import { cyan, yellow, dim } from "../colors.js";
|
||||
|
||||
type RunOptions = {
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import { existsSync, readdirSync, readFileSync, unlinkSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { DATA_DIR } from "../../../shared/index.js";
|
||||
import { DATA_DIR } from "@multica/utils";
|
||||
import { cyan, yellow, green, dim, red } from "../colors.js";
|
||||
|
||||
const SESSIONS_DIR = join(DATA_DIR, "sessions");
|
||||
|
|
@ -18,8 +18,8 @@ import {
|
|||
listInstalledSkills,
|
||||
checkEligibilityDetailed,
|
||||
type DiagnosticItem,
|
||||
} from "../../skills/index.js";
|
||||
import { credentialManager } from "../../credentials.js";
|
||||
} from "@multica/core";
|
||||
import { credentialManager } from "@multica/core";
|
||||
import { cyan, yellow, green, dim, red } from "../colors.js";
|
||||
|
||||
type Command = "list" | "status" | "install" | "add" | "remove" | "help";
|
||||
|
|
@ -6,9 +6,9 @@
|
|||
* multica tools groups Show all tool groups
|
||||
*/
|
||||
|
||||
import { createAllTools } from "../../tools.js";
|
||||
import { filterTools, type ToolsConfig } from "../../tools/policy.js";
|
||||
import { TOOL_GROUPS, expandToolGroups } from "../../tools/groups.js";
|
||||
import { createAllTools } from "@multica/core";
|
||||
import { filterTools, type ToolsConfig } from "@multica/core";
|
||||
import { TOOL_GROUPS, expandToolGroups } from "@multica/core";
|
||||
import { cyan, yellow, green, dim } from "../colors.js";
|
||||
|
||||
type Command = "list" | "groups" | "help";
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
#!/usr/bin/env node
|
||||
import * as readline from "readline";
|
||||
import { Agent } from "../runner.js";
|
||||
import type { AgentOptions } from "../types.js";
|
||||
import { SkillManager } from "../skills/index.js";
|
||||
import { Agent } from "@multica/core";
|
||||
import type { AgentOptions } from "@multica/core";
|
||||
import { SkillManager } from "@multica/core";
|
||||
import { autocompleteInput, type AutocompleteOption } from "./autocomplete.js";
|
||||
import { colors, dim, cyan, brightCyan, yellow, green, gray } from "./colors.js";
|
||||
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env node
|
||||
import { Agent } from "../runner.js";
|
||||
import { Agent } from "@multica/core";
|
||||
|
||||
type CliOptions = {
|
||||
profile?: string | undefined;
|
||||
318
apps/cli/src/output.ts
Normal file
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,
|
||||
getProfileDir,
|
||||
profileExists,
|
||||
} from "../profile/index.js";
|
||||
import { DATA_DIR } from "../../shared/index.js";
|
||||
} from "@multica/core";
|
||||
import { DATA_DIR } from "@multica/utils";
|
||||
|
||||
const DEFAULT_BASE_DIR = join(DATA_DIR, "agent-profiles");
|
||||
|
||||
|
|
@ -21,8 +21,8 @@ import {
|
|||
listInstalledSkills,
|
||||
checkEligibilityDetailed,
|
||||
type DiagnosticItem,
|
||||
} from "../skills/index.js";
|
||||
import { credentialManager } from "../credentials.js";
|
||||
} from "@multica/core";
|
||||
import { credentialManager } from "@multica/core";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
|
|
@ -9,9 +9,9 @@
|
|||
* pnpm tools:cli groups # Show all tool groups
|
||||
*/
|
||||
|
||||
import { createAllTools } from "../tools.js";
|
||||
import { filterTools, type ToolsConfig } from "../tools/policy.js";
|
||||
import { TOOL_GROUPS, expandToolGroups } from "../tools/groups.js";
|
||||
import { createAllTools } from "@multica/core";
|
||||
import { filterTools, type ToolsConfig } from "@multica/core";
|
||||
import { TOOL_GROUPS, expandToolGroups } from "@multica/core";
|
||||
|
||||
type Command = "list" | "groups" | "help";
|
||||
|
||||
9
apps/cli/tsconfig.json
Normal file
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",
|
||||
"asar": true,
|
||||
"productName": "YourAppName",
|
||||
"electronVersion": "30.5.1",
|
||||
"directories": {
|
||||
"output": "release/${version}"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"dist-electron"
|
||||
"out"
|
||||
],
|
||||
"mac": {
|
||||
"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",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build && electron-builder",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
"dev": "electron-vite dev",
|
||||
"build": "electron-vite build && electron-builder",
|
||||
"preview": "electron-vite preview",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hugeicons/core-free-icons": "^3.1.1",
|
||||
"@hugeicons/react": "^1.1.4",
|
||||
"@hugeicons/core-free-icons": "catalog:",
|
||||
"@hugeicons/react": "catalog:",
|
||||
"@multica/core": "workspace:*",
|
||||
"@multica/hooks": "workspace:*",
|
||||
"@multica/sdk": "workspace:*",
|
||||
"@multica/store": "workspace:*",
|
||||
|
|
@ -20,27 +21,27 @@
|
|||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"uuid": "^13.0.0",
|
||||
"socket.io-client": "catalog:",
|
||||
"uuid": "catalog:",
|
||||
"zustand": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
||||
"@typescript-eslint/parser": "^7.1.1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"electron": "^30.0.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron": "^33.4.11",
|
||||
"electron-builder": "^26.7.0",
|
||||
"electron-builder-squirrel-windows": "^26.7.0",
|
||||
"electron-vite": "^5.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"tailwindcss": "^4",
|
||||
"tailwindcss": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "^5.1.6",
|
||||
"vite-plugin-electron": "^0.28.6",
|
||||
"vite-plugin-electron-renderer": "^0.14.5"
|
||||
"vite": "^5.1.6"
|
||||
},
|
||||
"main": "dist-electron/main.js"
|
||||
"main": "./out/main/index.js"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,13 +49,16 @@ import { fileURLToPath } from 'node:url'
|
|||
import path from 'node:path'
|
||||
import { registerAllIpcHandlers, initializeApp, cleanupAll, setupDeviceConfirmation } from './ipc/index.js'
|
||||
|
||||
// CJS output will have __dirname natively, but TypeScript source needs this for type checking
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
process.env.APP_ROOT = path.join(__dirname, '..')
|
||||
// APP_ROOT points to apps/desktop (two levels up from out/main/)
|
||||
process.env.APP_ROOT = path.join(__dirname, '../..')
|
||||
|
||||
export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL']
|
||||
export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron')
|
||||
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
|
||||
// electron-vite outputs to out/ directory
|
||||
export const MAIN_DIST = path.join(__dirname)
|
||||
export const RENDERER_DIST = path.join(__dirname, '../renderer')
|
||||
|
||||
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST
|
||||
|
||||
|
|
@ -66,7 +69,7 @@ function createWindow() {
|
|||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.mjs'),
|
||||
preload: path.join(__dirname, '../preload/index.cjs'),
|
||||
// Enable node integration for IPC
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
|
|
@ -7,8 +7,7 @@
|
|||
*/
|
||||
import { ipcMain } from 'electron'
|
||||
import { getCurrentHub } from './hub.js'
|
||||
import { credentialManager } from '../../../../src/agent/credentials.js'
|
||||
import { listChannels } from '../../../../src/channels/registry.js'
|
||||
import { credentialManager, listChannels } from '@multica/core'
|
||||
|
||||
/** Validate that a string is a safe identifier (alphanumeric, dashes, underscores) */
|
||||
function isValidId(value: unknown): value is string {
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
* for the Cron Jobs management page.
|
||||
*/
|
||||
import { ipcMain } from 'electron'
|
||||
import { getCronService, formatSchedule } from '../../../../src/cron/index.js'
|
||||
import { getCronService, formatSchedule } from '@multica/core'
|
||||
|
||||
/**
|
||||
* Register all Cron-related IPC handlers.
|
||||
|
|
@ -5,9 +5,8 @@
|
|||
* This follows the same pattern as the Console app.
|
||||
*/
|
||||
import { ipcMain, type BrowserWindow } from 'electron'
|
||||
import { Hub } from '../../../../src/hub/hub.js'
|
||||
import { Hub, type AsyncAgent } from '@multica/core'
|
||||
import type { ConnectionState } from '@multica/sdk'
|
||||
import type { AsyncAgent } from '../../../../src/agent/async-agent.js'
|
||||
|
||||
// Singleton Hub instance
|
||||
let hub: Hub | null = null
|
||||
|
|
@ -13,13 +13,11 @@ import {
|
|||
getProviderMeta,
|
||||
isProviderAvailable,
|
||||
getLoginInstructions,
|
||||
type ProviderInfo,
|
||||
} from '../../../../src/agent/providers/index.js'
|
||||
import {
|
||||
readClaudeCliCredentials,
|
||||
readCodexCliCredentials,
|
||||
} from '../../../../src/agent/providers/oauth/cli-credentials.js'
|
||||
import { credentialManager } from '../../../../src/agent/credentials.js'
|
||||
credentialManager,
|
||||
type ProviderInfo,
|
||||
} from '@multica/core'
|
||||
|
||||
/**
|
||||
* Provider info returned to renderer (matches ProviderInfo from registry).
|
||||
|
|
@ -235,7 +235,7 @@ export function registerSkillsIpcHandlers(): void {
|
|||
) => {
|
||||
console.log(`[IPC] skills:add called: source=${source}, options=${JSON.stringify(options)}`)
|
||||
|
||||
const { addSkill } = await import('../../../../src/agent/skills/add.js')
|
||||
const { addSkill } = await import('@multica/core')
|
||||
|
||||
const result = await addSkill({
|
||||
source,
|
||||
|
|
@ -261,7 +261,7 @@ export function registerSkillsIpcHandlers(): void {
|
|||
ipcMain.handle('skills:remove', async (_event, name: string) => {
|
||||
console.log(`[IPC] skills:remove called: name=${name}`)
|
||||
|
||||
const { removeSkill } = await import('../../../../src/agent/skills/add.js')
|
||||
const { removeSkill } = await import('@multica/core')
|
||||
|
||||
const result = await removeSkill(name)
|
||||
|
||||
|
|
@ -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,
|
||||
Inject,
|
||||
} from "@nestjs/common";
|
||||
import { Hub } from "../hub/hub.js";
|
||||
import { Hub } from "@multica/core";
|
||||
|
||||
@Controller("api")
|
||||
export class AppController {
|
||||
|
|
@ -4,7 +4,7 @@ import { LoggerModule } from "nestjs-pino";
|
|||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { AppController } from "./app.controller.js";
|
||||
import { Hub } from "../hub/hub.js";
|
||||
import { Hub } from "@multica/core";
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
const isDev = process.env["NODE_ENV"] !== "production";
|
||||
27
apps/server/package.json
Normal file
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";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
transpilePackages: ["@multica/ui", "@multica/store"],
|
||||
transpilePackages: ["@multica/ui", "@multica/store", "@multica/hooks", "@multica/sdk"],
|
||||
headers: async () => [
|
||||
{
|
||||
source: "/sw.js",
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@
|
|||
"@multica/sdk": "workspace:*",
|
||||
"@multica/store": "workspace:*",
|
||||
"@multica/ui": "workspace:*",
|
||||
"uuid": "^13.0.0",
|
||||
"uuid": "catalog:",
|
||||
"zustand": "catalog:",
|
||||
"@hugeicons/core-free-icons": "^3.1.1",
|
||||
"@hugeicons/react": "^1.1.4",
|
||||
"@hugeicons/core-free-icons": "catalog:",
|
||||
"@hugeicons/react": "catalog:",
|
||||
"next": "16.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "catalog:",
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
"@types/node": "catalog:",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"eslint": "^9",
|
||||
"eslint": "catalog:",
|
||||
"eslint-config-next": "16.1.6",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
|
|
|
|||
315
docs/package-management.md
Normal file
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"
|
||||
},
|
||||
"scripts": {
|
||||
"multica": "tsx src/agent/cli/index.ts",
|
||||
"mu": "tsx src/agent/cli/index.ts",
|
||||
"dev": "tsx src/agent/cli/index.ts dev",
|
||||
"dev:desktop": "tsx src/agent/cli/index.ts dev desktop",
|
||||
"dev:gateway": "tsx src/agent/cli/index.ts dev gateway",
|
||||
"dev:web": "tsx src/agent/cli/index.ts dev web",
|
||||
"dev:all": "tsx src/agent/cli/index.ts dev all",
|
||||
"multica": "pnpm --filter @multica/cli dev",
|
||||
"mu": "pnpm --filter @multica/cli dev",
|
||||
"dev": "pnpm --filter @multica/cli dev -- dev",
|
||||
"dev:desktop": "pnpm --filter @multica/cli dev -- dev desktop",
|
||||
"dev:gateway": "pnpm --filter @multica/cli dev -- dev gateway",
|
||||
"dev:web": "pnpm --filter @multica/cli dev -- dev web",
|
||||
"dev:all": "pnpm --filter @multica/cli dev -- dev all",
|
||||
"build": "turbo build",
|
||||
"build:sdk": "pnpm --filter @multica/sdk build",
|
||||
"build:cli": "node scripts/build-cli.js",
|
||||
|
|
@ -33,12 +33,16 @@
|
|||
"onlyBuiltDependencies": [
|
||||
"electron",
|
||||
"esbuild"
|
||||
]
|
||||
],
|
||||
"overrides": {
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:"
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"@types/uuid": "catalog:",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
"concurrently": "^9.2.1",
|
||||
"esbuild": "^0.27.2",
|
||||
|
|
@ -48,9 +52,9 @@
|
|||
"vitest": "^4.0.18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-agent-core": "^0.52.9",
|
||||
"@mariozechner/pi-ai": "^0.52.9",
|
||||
"@mariozechner/pi-coding-agent": "^0.52.9",
|
||||
"@mariozechner/pi-agent-core": "catalog:",
|
||||
"@mariozechner/pi-ai": "catalog:",
|
||||
"@mariozechner/pi-coding-agent": "catalog:",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@multica/sdk": "workspace:*",
|
||||
"@multica/store": "workspace:*",
|
||||
|
|
@ -61,6 +65,7 @@
|
|||
"@nestjs/serve-static": "^5.0.4",
|
||||
"@nestjs/websockets": "^11.1.12",
|
||||
"@sinclair/typebox": "^0.34.41",
|
||||
"chokidar": "^5.0.0",
|
||||
"croner": "^10.0.1",
|
||||
"fast-glob": "^3.3.3",
|
||||
"grammy": "^1.39.3",
|
||||
|
|
@ -74,10 +79,10 @@
|
|||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"socket.io": "^4.8.3",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"socket.io-client": "catalog:",
|
||||
"turndown": "^7.2.2",
|
||||
"undici": "^7.19.2",
|
||||
"uuid": "^13.0.0",
|
||||
"uuid": "catalog:",
|
||||
"yaml": "^2.8.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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,
|
||||
} from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { DATA_DIR } from "../../shared/paths.js";
|
||||
import { DATA_DIR } from "@multica/utils";
|
||||
import { AUTH_STORE_VERSION, AUTH_PROFILE_STORE_FILENAME } from "./constants.js";
|
||||
import type { AuthProfileStore } from "./types.js";
|
||||
|
||||
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