diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..bf2e7648 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +shamefully-hoist=true diff --git a/CLAUDE.md b/CLAUDE.md index 267369b7..93108cd3 100644 --- a/CLAUDE.md +++ b/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 "" # 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 "" # 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 — `(): ` +**Format**: `(): ` 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" +``` diff --git a/apps/cli/package.json b/apps/cli/package.json new file mode 100644 index 00000000..eae2a3e4 --- /dev/null +++ b/apps/cli/package.json @@ -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:" + } +} diff --git a/src/agent/cli/autocomplete.ts b/apps/cli/src/autocomplete.ts similarity index 100% rename from src/agent/cli/autocomplete.ts rename to apps/cli/src/autocomplete.ts diff --git a/src/agent/cli/colors.ts b/apps/cli/src/colors.ts similarity index 100% rename from src/agent/cli/colors.ts rename to apps/cli/src/colors.ts diff --git a/src/agent/cli/commands/chat.ts b/apps/cli/src/commands/chat.ts similarity index 99% rename from src/agent/cli/commands/chat.ts rename to apps/cli/src/commands/chat.ts index 0f26b528..cf40652e 100644 --- a/src/agent/cli/commands/chat.ts +++ b/apps/cli/src/commands/chat.ts @@ -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; diff --git a/src/agent/cli/commands/credentials.ts b/apps/cli/src/commands/credentials.ts similarity index 98% rename from src/agent/cli/commands/credentials.ts rename to apps/cli/src/commands/credentials.ts index 71122a06..68ca2785 100644 --- a/src/agent/cli/commands/credentials.ts +++ b/apps/cli/src/commands/credentials.ts @@ -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"; diff --git a/src/agent/cli/commands/cron.ts b/apps/cli/src/commands/cron.ts similarity index 99% rename from src/agent/cli/commands/cron.ts rename to apps/cli/src/commands/cron.ts index bf5f0e1a..aa4308d9 100644 --- a/src/agent/cli/commands/cron.ts +++ b/apps/cli/src/commands/cron.ts @@ -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"; diff --git a/src/agent/cli/commands/dev.ts b/apps/cli/src/commands/dev.ts similarity index 100% rename from src/agent/cli/commands/dev.ts rename to apps/cli/src/commands/dev.ts diff --git a/src/agent/cli/commands/profile.ts b/apps/cli/src/commands/profile.ts similarity index 98% rename from src/agent/cli/commands/profile.ts rename to apps/cli/src/commands/profile.ts index 1e3537fc..ba181c21 100644 --- a/src/agent/cli/commands/profile.ts +++ b/apps/cli/src/commands/profile.ts @@ -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"); diff --git a/src/agent/cli/commands/run.ts b/apps/cli/src/commands/run.ts similarity index 97% rename from src/agent/cli/commands/run.ts rename to apps/cli/src/commands/run.ts index 2595de56..1f5656be 100644 --- a/src/agent/cli/commands/run.ts +++ b/apps/cli/src/commands/run.ts @@ -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 = { diff --git a/src/agent/cli/commands/session.ts b/apps/cli/src/commands/session.ts similarity index 99% rename from src/agent/cli/commands/session.ts rename to apps/cli/src/commands/session.ts index 6f56dc66..bf96a310 100644 --- a/src/agent/cli/commands/session.ts +++ b/apps/cli/src/commands/session.ts @@ -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"); diff --git a/src/agent/cli/commands/skills.ts b/apps/cli/src/commands/skills.ts similarity index 99% rename from src/agent/cli/commands/skills.ts rename to apps/cli/src/commands/skills.ts index 837fae67..d9bf5e20 100644 --- a/src/agent/cli/commands/skills.ts +++ b/apps/cli/src/commands/skills.ts @@ -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"; diff --git a/src/agent/cli/commands/tools.ts b/apps/cli/src/commands/tools.ts similarity index 95% rename from src/agent/cli/commands/tools.ts rename to apps/cli/src/commands/tools.ts index f5485d06..2650b4a3 100644 --- a/src/agent/cli/commands/tools.ts +++ b/apps/cli/src/commands/tools.ts @@ -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"; diff --git a/src/agent/cli/index.ts b/apps/cli/src/index.ts similarity index 100% rename from src/agent/cli/index.ts rename to apps/cli/src/index.ts diff --git a/src/agent/cli/interactive.ts b/apps/cli/src/interactive.ts similarity index 99% rename from src/agent/cli/interactive.ts rename to apps/cli/src/interactive.ts index 3a4e3659..99509cbf 100644 --- a/src/agent/cli/interactive.ts +++ b/apps/cli/src/interactive.ts @@ -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"; diff --git a/src/agent/cli/non-interactive.ts b/apps/cli/src/non-interactive.ts similarity index 99% rename from src/agent/cli/non-interactive.ts rename to apps/cli/src/non-interactive.ts index 7284bf20..23226a5c 100644 --- a/src/agent/cli/non-interactive.ts +++ b/apps/cli/src/non-interactive.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { Agent } from "../runner.js"; +import { Agent } from "@multica/core"; type CliOptions = { profile?: string | undefined; diff --git a/src/agent/cli/output.test.ts b/apps/cli/src/output.test.ts similarity index 100% rename from src/agent/cli/output.test.ts rename to apps/cli/src/output.test.ts diff --git a/apps/cli/src/output.ts b/apps/cli/src/output.ts new file mode 100644 index 00000000..d0881cac --- /dev/null +++ b/apps/cli/src/output.ts @@ -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 = { + 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; + 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 | 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; + } catch { + continue; + } + } + } + } + + const withDetails = result as { details?: unknown }; + if (withDetails.details && typeof withDetails.details === "object") { + return withDetails.details as Record; + } + + // Try direct object access + return result as Record; +} + +// 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 }; +} diff --git a/src/agent/cli/profile.ts b/apps/cli/src/profile.ts similarity index 98% rename from src/agent/cli/profile.ts rename to apps/cli/src/profile.ts index 7e3b63aa..f250fe02 100644 --- a/src/agent/cli/profile.ts +++ b/apps/cli/src/profile.ts @@ -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"); diff --git a/src/agent/cli/skills.ts b/apps/cli/src/skills.ts similarity index 99% rename from src/agent/cli/skills.ts rename to apps/cli/src/skills.ts index c5d67fe8..64169ca4 100644 --- a/src/agent/cli/skills.ts +++ b/apps/cli/src/skills.ts @@ -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 diff --git a/src/agent/cli/tools.ts b/apps/cli/src/tools.ts similarity index 96% rename from src/agent/cli/tools.ts rename to apps/cli/src/tools.ts index 727f5c59..78772824 100644 --- a/src/agent/cli/tools.ts +++ b/apps/cli/src/tools.ts @@ -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"; diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json new file mode 100644 index 00000000..eb5c5cf3 --- /dev/null +++ b/apps/cli/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/cli/tsup.config.ts b/apps/cli/tsup.config.ts new file mode 100644 index 00000000..4245748e --- /dev/null +++ b/apps/cli/tsup.config.ts @@ -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:/, + ], +}) diff --git a/apps/desktop/electron-builder.json5 b/apps/desktop/electron-builder.json5 index cd633dc7..a2c6ba61 100644 --- a/apps/desktop/electron-builder.json5 +++ b/apps/desktop/electron-builder.json5 @@ -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": [ diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts new file mode 100644 index 00000000..6b2d9996 --- /dev/null +++ b/apps/desktop/electron.vite.config.ts @@ -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'), + }, + }, + }, +}) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 0f085b69..a29419b2 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -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" } diff --git a/apps/desktop/electron/electron-env.d.ts b/apps/desktop/src/main/electron-env.d.ts similarity index 100% rename from apps/desktop/electron/electron-env.d.ts rename to apps/desktop/src/main/electron-env.d.ts diff --git a/apps/desktop/electron/main.ts b/apps/desktop/src/main/index.ts similarity index 83% rename from apps/desktop/electron/main.ts rename to apps/desktop/src/main/index.ts index ea0acb6d..efcf3036 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/src/main/index.ts @@ -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, diff --git a/apps/desktop/electron/ipc/agent.ts b/apps/desktop/src/main/ipc/agent.ts similarity index 100% rename from apps/desktop/electron/ipc/agent.ts rename to apps/desktop/src/main/ipc/agent.ts diff --git a/apps/desktop/electron/ipc/channels.ts b/apps/desktop/src/main/ipc/channels.ts similarity index 97% rename from apps/desktop/electron/ipc/channels.ts rename to apps/desktop/src/main/ipc/channels.ts index 49662e94..4c77d0e3 100644 --- a/apps/desktop/electron/ipc/channels.ts +++ b/apps/desktop/src/main/ipc/channels.ts @@ -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 { diff --git a/apps/desktop/electron/ipc/cron.ts b/apps/desktop/src/main/ipc/cron.ts similarity index 95% rename from apps/desktop/electron/ipc/cron.ts rename to apps/desktop/src/main/ipc/cron.ts index 19cf591f..2a848730 100644 --- a/apps/desktop/electron/ipc/cron.ts +++ b/apps/desktop/src/main/ipc/cron.ts @@ -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. diff --git a/apps/desktop/electron/ipc/heartbeat.ts b/apps/desktop/src/main/ipc/heartbeat.ts similarity index 100% rename from apps/desktop/electron/ipc/heartbeat.ts rename to apps/desktop/src/main/ipc/heartbeat.ts diff --git a/apps/desktop/electron/ipc/hub.ts b/apps/desktop/src/main/ipc/hub.ts similarity index 99% rename from apps/desktop/electron/ipc/hub.ts rename to apps/desktop/src/main/ipc/hub.ts index ffde8786..6654ccb8 100644 --- a/apps/desktop/electron/ipc/hub.ts +++ b/apps/desktop/src/main/ipc/hub.ts @@ -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 diff --git a/apps/desktop/electron/ipc/index.ts b/apps/desktop/src/main/ipc/index.ts similarity index 100% rename from apps/desktop/electron/ipc/index.ts rename to apps/desktop/src/main/ipc/index.ts diff --git a/apps/desktop/electron/ipc/profile.ts b/apps/desktop/src/main/ipc/profile.ts similarity index 100% rename from apps/desktop/electron/ipc/profile.ts rename to apps/desktop/src/main/ipc/profile.ts diff --git a/apps/desktop/electron/ipc/provider.ts b/apps/desktop/src/main/ipc/provider.ts similarity index 97% rename from apps/desktop/electron/ipc/provider.ts rename to apps/desktop/src/main/ipc/provider.ts index 3f31dc9c..40a75eb8 100644 --- a/apps/desktop/electron/ipc/provider.ts +++ b/apps/desktop/src/main/ipc/provider.ts @@ -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). diff --git a/apps/desktop/electron/ipc/skills.ts b/apps/desktop/src/main/ipc/skills.ts similarity index 97% rename from apps/desktop/electron/ipc/skills.ts rename to apps/desktop/src/main/ipc/skills.ts index 71fea31a..fe724dcd 100644 --- a/apps/desktop/electron/ipc/skills.ts +++ b/apps/desktop/src/main/ipc/skills.ts @@ -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) diff --git a/apps/desktop/electron/preload.ts b/apps/desktop/src/preload/index.ts similarity index 100% rename from apps/desktop/electron/preload.ts rename to apps/desktop/src/preload/index.ts diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts deleted file mode 100644 index 4fe06726..00000000 --- a/apps/desktop/vite.config.ts +++ /dev/null @@ -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 - : {}, - }), - ], -}) diff --git a/src/gateway/Dockerfile b/apps/gateway/Dockerfile similarity index 100% rename from src/gateway/Dockerfile rename to apps/gateway/Dockerfile diff --git a/src/gateway/app.controller.ts b/apps/gateway/app.controller.ts similarity index 100% rename from src/gateway/app.controller.ts rename to apps/gateway/app.controller.ts diff --git a/src/gateway/app.module.ts b/apps/gateway/app.module.ts similarity index 100% rename from src/gateway/app.module.ts rename to apps/gateway/app.module.ts diff --git a/src/gateway/database/database.module.ts b/apps/gateway/database/database.module.ts similarity index 100% rename from src/gateway/database/database.module.ts rename to apps/gateway/database/database.module.ts diff --git a/src/gateway/database/database.service.ts b/apps/gateway/database/database.service.ts similarity index 100% rename from src/gateway/database/database.service.ts rename to apps/gateway/database/database.service.ts diff --git a/src/gateway/events.gateway.ts b/apps/gateway/events.gateway.ts similarity index 100% rename from src/gateway/events.gateway.ts rename to apps/gateway/events.gateway.ts diff --git a/src/gateway/gateway.module.ts b/apps/gateway/gateway.module.ts similarity index 100% rename from src/gateway/gateway.module.ts rename to apps/gateway/gateway.module.ts diff --git a/src/gateway/index.ts b/apps/gateway/index.ts similarity index 100% rename from src/gateway/index.ts rename to apps/gateway/index.ts diff --git a/src/gateway/main.ts b/apps/gateway/main.ts similarity index 100% rename from src/gateway/main.ts rename to apps/gateway/main.ts diff --git a/src/gateway/migrations/telegram-users.sql b/apps/gateway/migrations/telegram-users.sql similarity index 100% rename from src/gateway/migrations/telegram-users.sql rename to apps/gateway/migrations/telegram-users.sql diff --git a/apps/gateway/package.json b/apps/gateway/package.json new file mode 100644 index 00000000..a46538d0 --- /dev/null +++ b/apps/gateway/package.json @@ -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:" + } +} diff --git a/src/gateway/public/icon.png b/apps/gateway/public/icon.png similarity index 100% rename from src/gateway/public/icon.png rename to apps/gateway/public/icon.png diff --git a/src/gateway/public/index.html b/apps/gateway/public/index.html similarity index 100% rename from src/gateway/public/index.html rename to apps/gateway/public/index.html diff --git a/src/gateway/public/manifest.json b/apps/gateway/public/manifest.json similarity index 100% rename from src/gateway/public/manifest.json rename to apps/gateway/public/manifest.json diff --git a/src/gateway/public/sw.js b/apps/gateway/public/sw.js similarity index 100% rename from src/gateway/public/sw.js rename to apps/gateway/public/sw.js diff --git a/src/gateway/scripts/build-and-push.sh b/apps/gateway/scripts/build-and-push.sh similarity index 100% rename from src/gateway/scripts/build-and-push.sh rename to apps/gateway/scripts/build-and-push.sh diff --git a/src/gateway/telegram/telegram-user.store.ts b/apps/gateway/telegram/telegram-user.store.ts similarity index 100% rename from src/gateway/telegram/telegram-user.store.ts rename to apps/gateway/telegram/telegram-user.store.ts diff --git a/src/gateway/telegram/telegram.controller.ts b/apps/gateway/telegram/telegram.controller.ts similarity index 100% rename from src/gateway/telegram/telegram.controller.ts rename to apps/gateway/telegram/telegram.controller.ts diff --git a/src/gateway/telegram/telegram.module.ts b/apps/gateway/telegram/telegram.module.ts similarity index 100% rename from src/gateway/telegram/telegram.module.ts rename to apps/gateway/telegram/telegram.module.ts diff --git a/src/gateway/telegram/telegram.service.ts b/apps/gateway/telegram/telegram.service.ts similarity index 100% rename from src/gateway/telegram/telegram.service.ts rename to apps/gateway/telegram/telegram.service.ts diff --git a/src/gateway/telegram/types.ts b/apps/gateway/telegram/types.ts similarity index 100% rename from src/gateway/telegram/types.ts rename to apps/gateway/telegram/types.ts diff --git a/src/gateway/test-app.ts b/apps/gateway/test-app.ts similarity index 100% rename from src/gateway/test-app.ts rename to apps/gateway/test-app.ts diff --git a/src/gateway/test-client.ts b/apps/gateway/test-client.ts similarity index 100% rename from src/gateway/test-client.ts rename to apps/gateway/test-client.ts diff --git a/apps/gateway/tsconfig.json b/apps/gateway/tsconfig.json new file mode 100644 index 00000000..1a0e9ddb --- /dev/null +++ b/apps/gateway/tsconfig.json @@ -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"] +} diff --git a/src/gateway/types.ts b/apps/gateway/types.ts similarity index 100% rename from src/gateway/types.ts rename to apps/gateway/types.ts diff --git a/src/console/app.controller.ts b/apps/server/app.controller.ts similarity index 96% rename from src/console/app.controller.ts rename to apps/server/app.controller.ts index bf4ee28a..0a453766 100644 --- a/src/console/app.controller.ts +++ b/apps/server/app.controller.ts @@ -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 { diff --git a/src/console/app.module.ts b/apps/server/app.module.ts similarity index 97% rename from src/console/app.module.ts rename to apps/server/app.module.ts index 12db26f3..e9ec051c 100644 --- a/src/console/app.module.ts +++ b/apps/server/app.module.ts @@ -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"; diff --git a/src/console/main.ts b/apps/server/main.ts similarity index 100% rename from src/console/main.ts rename to apps/server/main.ts diff --git a/apps/server/package.json b/apps/server/package.json new file mode 100644 index 00000000..408092d9 --- /dev/null +++ b/apps/server/package.json @@ -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:" + } +} diff --git a/src/console/public/client.html b/apps/server/public/client.html similarity index 100% rename from src/console/public/client.html rename to apps/server/public/client.html diff --git a/src/console/public/index.html b/apps/server/public/index.html similarity index 100% rename from src/console/public/index.html rename to apps/server/public/index.html diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json new file mode 100644 index 00000000..1a0e9ddb --- /dev/null +++ b/apps/server/tsconfig.json @@ -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"] +} diff --git a/apps/web/next.config.ts b/apps/web/next.config.ts index 7793796a..17a23e4c 100644 --- a/apps/web/next.config.ts +++ b/apps/web/next.config.ts @@ -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", diff --git a/apps/web/package.json b/apps/web/package.json index 4089488f..2104dc0a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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:" } diff --git a/docs/package-management.md b/docs/package-management.md new file mode 100644 index 00000000..85d2ef0b --- /dev/null +++ b/docs/package-management.md @@ -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 + +# 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 --filter +``` + +### 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 diff --git a/package.json b/package.json index fd91fcea..9626b02f 100644 --- a/package.json +++ b/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" } } diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000..0a2bb7aa --- /dev/null +++ b/packages/core/package.json @@ -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" + } +} diff --git a/src/agent/async-agent.test.ts b/packages/core/src/agent/async-agent.test.ts similarity index 100% rename from src/agent/async-agent.test.ts rename to packages/core/src/agent/async-agent.test.ts diff --git a/src/agent/async-agent.ts b/packages/core/src/agent/async-agent.ts similarity index 100% rename from src/agent/async-agent.ts rename to packages/core/src/agent/async-agent.ts diff --git a/src/agent/auth-profiles/constants.ts b/packages/core/src/agent/auth-profiles/constants.ts similarity index 100% rename from src/agent/auth-profiles/constants.ts rename to packages/core/src/agent/auth-profiles/constants.ts diff --git a/src/agent/auth-profiles/error-classification.test.ts b/packages/core/src/agent/auth-profiles/error-classification.test.ts similarity index 100% rename from src/agent/auth-profiles/error-classification.test.ts rename to packages/core/src/agent/auth-profiles/error-classification.test.ts diff --git a/src/agent/auth-profiles/index.ts b/packages/core/src/agent/auth-profiles/index.ts similarity index 100% rename from src/agent/auth-profiles/index.ts rename to packages/core/src/agent/auth-profiles/index.ts diff --git a/src/agent/auth-profiles/order.test.ts b/packages/core/src/agent/auth-profiles/order.test.ts similarity index 100% rename from src/agent/auth-profiles/order.test.ts rename to packages/core/src/agent/auth-profiles/order.test.ts diff --git a/src/agent/auth-profiles/order.ts b/packages/core/src/agent/auth-profiles/order.ts similarity index 100% rename from src/agent/auth-profiles/order.ts rename to packages/core/src/agent/auth-profiles/order.ts diff --git a/src/agent/auth-profiles/store.test.ts b/packages/core/src/agent/auth-profiles/store.test.ts similarity index 100% rename from src/agent/auth-profiles/store.test.ts rename to packages/core/src/agent/auth-profiles/store.test.ts diff --git a/src/agent/auth-profiles/store.ts b/packages/core/src/agent/auth-profiles/store.ts similarity index 99% rename from src/agent/auth-profiles/store.ts rename to packages/core/src/agent/auth-profiles/store.ts index f50f2788..7b090064 100644 --- a/src/agent/auth-profiles/store.ts +++ b/packages/core/src/agent/auth-profiles/store.ts @@ -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"; diff --git a/src/agent/auth-profiles/types.ts b/packages/core/src/agent/auth-profiles/types.ts similarity index 100% rename from src/agent/auth-profiles/types.ts rename to packages/core/src/agent/auth-profiles/types.ts diff --git a/src/agent/auth-profiles/usage.test.ts b/packages/core/src/agent/auth-profiles/usage.test.ts similarity index 100% rename from src/agent/auth-profiles/usage.test.ts rename to packages/core/src/agent/auth-profiles/usage.test.ts diff --git a/src/agent/auth-profiles/usage.ts b/packages/core/src/agent/auth-profiles/usage.ts similarity index 100% rename from src/agent/auth-profiles/usage.ts rename to packages/core/src/agent/auth-profiles/usage.ts diff --git a/src/agent/channel.ts b/packages/core/src/agent/channel.ts similarity index 100% rename from src/agent/channel.ts rename to packages/core/src/agent/channel.ts diff --git a/packages/core/src/agent/cli/colors.ts b/packages/core/src/agent/cli/colors.ts new file mode 100644 index 00000000..37e22ee5 --- /dev/null +++ b/packages/core/src/agent/cli/colors.ts @@ -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 | 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; + }, + }; +} diff --git a/src/agent/cli/output.ts b/packages/core/src/agent/cli/output.ts similarity index 100% rename from src/agent/cli/output.ts rename to packages/core/src/agent/cli/output.ts diff --git a/src/agent/context-window/guard.test.ts b/packages/core/src/agent/context-window/guard.test.ts similarity index 100% rename from src/agent/context-window/guard.test.ts rename to packages/core/src/agent/context-window/guard.test.ts diff --git a/src/agent/context-window/guard.ts b/packages/core/src/agent/context-window/guard.ts similarity index 100% rename from src/agent/context-window/guard.ts rename to packages/core/src/agent/context-window/guard.ts diff --git a/src/agent/context-window/index.ts b/packages/core/src/agent/context-window/index.ts similarity index 100% rename from src/agent/context-window/index.ts rename to packages/core/src/agent/context-window/index.ts diff --git a/src/agent/context-window/summarization.ts b/packages/core/src/agent/context-window/summarization.ts similarity index 100% rename from src/agent/context-window/summarization.ts rename to packages/core/src/agent/context-window/summarization.ts diff --git a/src/agent/context-window/token-estimation.test.ts b/packages/core/src/agent/context-window/token-estimation.test.ts similarity index 100% rename from src/agent/context-window/token-estimation.test.ts rename to packages/core/src/agent/context-window/token-estimation.test.ts diff --git a/src/agent/context-window/token-estimation.ts b/packages/core/src/agent/context-window/token-estimation.ts similarity index 100% rename from src/agent/context-window/token-estimation.ts rename to packages/core/src/agent/context-window/token-estimation.ts diff --git a/src/agent/context-window/tool-result-pruning.test.ts b/packages/core/src/agent/context-window/tool-result-pruning.test.ts similarity index 100% rename from src/agent/context-window/tool-result-pruning.test.ts rename to packages/core/src/agent/context-window/tool-result-pruning.test.ts diff --git a/src/agent/context-window/tool-result-pruning.ts b/packages/core/src/agent/context-window/tool-result-pruning.ts similarity index 100% rename from src/agent/context-window/tool-result-pruning.ts rename to packages/core/src/agent/context-window/tool-result-pruning.ts diff --git a/src/agent/context-window/types.ts b/packages/core/src/agent/context-window/types.ts similarity index 100% rename from src/agent/context-window/types.ts rename to packages/core/src/agent/context-window/types.ts diff --git a/src/agent/credentials-cli.ts b/packages/core/src/agent/credentials-cli.ts similarity index 100% rename from src/agent/credentials-cli.ts rename to packages/core/src/agent/credentials-cli.ts diff --git a/src/agent/credentials.ts b/packages/core/src/agent/credentials.ts similarity index 99% rename from src/agent/credentials.ts rename to packages/core/src/agent/credentials.ts index 048a5e52..edc501ea 100644 --- a/src/agent/credentials.ts +++ b/packages/core/src/agent/credentials.ts @@ -2,7 +2,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "no import { join, dirname } from "node:path"; import { homedir } from "node:os"; import JSON5 from "json5"; -import { DATA_DIR } from "../shared/paths.js"; +import { DATA_DIR } from "@multica/utils"; type ProviderConfig = { // API Key authentication diff --git a/src/agent/events.ts b/packages/core/src/agent/events.ts similarity index 100% rename from src/agent/events.ts rename to packages/core/src/agent/events.ts diff --git a/src/agent/extract-text.ts b/packages/core/src/agent/extract-text.ts similarity index 100% rename from src/agent/extract-text.ts rename to packages/core/src/agent/extract-text.ts diff --git a/packages/core/src/agent/index.ts b/packages/core/src/agent/index.ts new file mode 100644 index 00000000..acd36784 --- /dev/null +++ b/packages/core/src/agent/index.ts @@ -0,0 +1,30 @@ +export * from "./runner.js"; +export * from "./types.js"; +export * from "./events.js"; +export * from "./profile/index.js"; +export * from "./context-window/index.js"; +export * from "./skills/index.js"; +export * from "./channel.js"; +export * from "./sync-agent.js"; +export * from "./async-agent.js"; +export { credentialManager, getCredentialsPath, getSkillsEnvPath, type CredentialsConfig } from "./credentials.js"; +export * from "./providers/index.js"; +export * from "./tools.js"; +export * from "./tools/policy.js"; +export * from "./tools/groups.js"; +export * from "./extract-text.js"; +export { + readClaudeCliCredentials, + readCodexCliCredentials, + hasValidClaudeCliCredentials, + hasValidCodexCliCredentials, + getClaudeCliAccessToken, + getCodexCliAccessToken, + getCliCredentialStatus, + type ClaudeCliCredential, + type CodexCliCredential, + type OAuthCredential, + type TokenCredential, + type CliCredentialSource, + type CliCredentialStatus, +} from "./providers/oauth/cli-credentials.js"; diff --git a/src/agent/message-timestamp.test.ts b/packages/core/src/agent/message-timestamp.test.ts similarity index 100% rename from src/agent/message-timestamp.test.ts rename to packages/core/src/agent/message-timestamp.test.ts diff --git a/src/agent/message-timestamp.ts b/packages/core/src/agent/message-timestamp.ts similarity index 100% rename from src/agent/message-timestamp.ts rename to packages/core/src/agent/message-timestamp.ts diff --git a/src/agent/profile/README.md b/packages/core/src/agent/profile/README.md similarity index 100% rename from src/agent/profile/README.md rename to packages/core/src/agent/profile/README.md diff --git a/src/agent/profile/index.ts b/packages/core/src/agent/profile/index.ts similarity index 100% rename from src/agent/profile/index.ts rename to packages/core/src/agent/profile/index.ts diff --git a/src/agent/profile/storage.test.ts b/packages/core/src/agent/profile/storage.test.ts similarity index 100% rename from src/agent/profile/storage.test.ts rename to packages/core/src/agent/profile/storage.test.ts diff --git a/src/agent/profile/storage.ts b/packages/core/src/agent/profile/storage.ts similarity index 98% rename from src/agent/profile/storage.ts rename to packages/core/src/agent/profile/storage.ts index 9429aae1..5b2c54d2 100644 --- a/src/agent/profile/storage.ts +++ b/packages/core/src/agent/profile/storage.ts @@ -5,7 +5,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { PROFILE_FILES, type AgentProfile, type ProfileConfig } from "./types.js"; -import { DATA_DIR } from "../../shared/index.js"; +import { DATA_DIR } from "@multica/utils"; const DEFAULT_BASE_DIR = join(DATA_DIR, "agent-profiles"); diff --git a/src/agent/profile/templates.ts b/packages/core/src/agent/profile/templates.ts similarity index 100% rename from src/agent/profile/templates.ts rename to packages/core/src/agent/profile/templates.ts diff --git a/src/agent/profile/types.ts b/packages/core/src/agent/profile/types.ts similarity index 100% rename from src/agent/profile/types.ts rename to packages/core/src/agent/profile/types.ts diff --git a/src/agent/providers/index.ts b/packages/core/src/agent/providers/index.ts similarity index 100% rename from src/agent/providers/index.ts rename to packages/core/src/agent/providers/index.ts diff --git a/src/agent/providers/oauth/cli-credentials.ts b/packages/core/src/agent/providers/oauth/cli-credentials.ts similarity index 100% rename from src/agent/providers/oauth/cli-credentials.ts rename to packages/core/src/agent/providers/oauth/cli-credentials.ts diff --git a/src/agent/providers/oauth/index.ts b/packages/core/src/agent/providers/oauth/index.ts similarity index 100% rename from src/agent/providers/oauth/index.ts rename to packages/core/src/agent/providers/oauth/index.ts diff --git a/src/agent/providers/registry.ts b/packages/core/src/agent/providers/registry.ts similarity index 100% rename from src/agent/providers/registry.ts rename to packages/core/src/agent/providers/registry.ts diff --git a/src/agent/providers/resolver.ts b/packages/core/src/agent/providers/resolver.ts similarity index 100% rename from src/agent/providers/resolver.ts rename to packages/core/src/agent/providers/resolver.ts diff --git a/src/agent/runner.ts b/packages/core/src/agent/runner.ts similarity index 100% rename from src/agent/runner.ts rename to packages/core/src/agent/runner.ts diff --git a/src/agent/session/compaction.test.ts b/packages/core/src/agent/session/compaction.test.ts similarity index 100% rename from src/agent/session/compaction.test.ts rename to packages/core/src/agent/session/compaction.test.ts diff --git a/src/agent/session/compaction.ts b/packages/core/src/agent/session/compaction.ts similarity index 100% rename from src/agent/session/compaction.ts rename to packages/core/src/agent/session/compaction.ts diff --git a/src/agent/session/session-file-repair.test.ts b/packages/core/src/agent/session/session-file-repair.test.ts similarity index 100% rename from src/agent/session/session-file-repair.test.ts rename to packages/core/src/agent/session/session-file-repair.test.ts diff --git a/src/agent/session/session-file-repair.ts b/packages/core/src/agent/session/session-file-repair.ts similarity index 100% rename from src/agent/session/session-file-repair.ts rename to packages/core/src/agent/session/session-file-repair.ts diff --git a/src/agent/session/session-manager.display.test.ts b/packages/core/src/agent/session/session-manager.display.test.ts similarity index 100% rename from src/agent/session/session-manager.display.test.ts rename to packages/core/src/agent/session/session-manager.display.test.ts diff --git a/src/agent/session/session-manager.ts b/packages/core/src/agent/session/session-manager.ts similarity index 100% rename from src/agent/session/session-manager.ts rename to packages/core/src/agent/session/session-manager.ts diff --git a/src/agent/session/session-transcript-repair.test.ts b/packages/core/src/agent/session/session-transcript-repair.test.ts similarity index 100% rename from src/agent/session/session-transcript-repair.test.ts rename to packages/core/src/agent/session/session-transcript-repair.test.ts diff --git a/src/agent/session/session-transcript-repair.ts b/packages/core/src/agent/session/session-transcript-repair.ts similarity index 100% rename from src/agent/session/session-transcript-repair.ts rename to packages/core/src/agent/session/session-transcript-repair.ts diff --git a/src/agent/session/session-write-lock.test.ts b/packages/core/src/agent/session/session-write-lock.test.ts similarity index 100% rename from src/agent/session/session-write-lock.test.ts rename to packages/core/src/agent/session/session-write-lock.test.ts diff --git a/src/agent/session/session-write-lock.ts b/packages/core/src/agent/session/session-write-lock.ts similarity index 100% rename from src/agent/session/session-write-lock.ts rename to packages/core/src/agent/session/session-write-lock.ts diff --git a/src/agent/session/storage.test.ts b/packages/core/src/agent/session/storage.test.ts similarity index 100% rename from src/agent/session/storage.test.ts rename to packages/core/src/agent/session/storage.test.ts diff --git a/src/agent/session/storage.ts b/packages/core/src/agent/session/storage.ts similarity index 98% rename from src/agent/session/storage.ts rename to packages/core/src/agent/session/storage.ts index 9b104a24..75ff9239 100644 --- a/src/agent/session/storage.ts +++ b/packages/core/src/agent/session/storage.ts @@ -2,7 +2,7 @@ import { join } from "path"; import { existsSync, mkdirSync, readFileSync } from "fs"; import { appendFile, writeFile } from "fs/promises"; import type { SessionEntry } from "./types.js"; -import { DATA_DIR } from "../../shared/index.js"; +import { DATA_DIR } from "@multica/utils"; import { acquireSessionWriteLock } from "./session-write-lock.js"; export type SessionStorageOptions = { diff --git a/src/agent/session/types.ts b/packages/core/src/agent/session/types.ts similarity index 100% rename from src/agent/session/types.ts rename to packages/core/src/agent/session/types.ts diff --git a/src/agent/skills/README.md b/packages/core/src/agent/skills/README.md similarity index 100% rename from src/agent/skills/README.md rename to packages/core/src/agent/skills/README.md diff --git a/src/agent/skills/README.zh-CN.md b/packages/core/src/agent/skills/README.zh-CN.md similarity index 100% rename from src/agent/skills/README.zh-CN.md rename to packages/core/src/agent/skills/README.zh-CN.md diff --git a/src/agent/skills/add.ts b/packages/core/src/agent/skills/add.ts similarity index 99% rename from src/agent/skills/add.ts rename to packages/core/src/agent/skills/add.ts index 05ec6adf..82ca05ea 100644 --- a/src/agent/skills/add.ts +++ b/packages/core/src/agent/skills/add.ts @@ -14,7 +14,7 @@ import { join, basename } from "node:path"; import { spawn } from "node:child_process"; import { existsSync } from "node:fs"; -import { DATA_DIR } from "../../shared/index.js"; +import { DATA_DIR } from "@multica/utils"; import { binaryExists } from "./eligibility.js"; import { bumpSkillsVersion } from "./watcher.js"; import { serialize, SerializeKeys } from "./serialize.js"; diff --git a/src/agent/skills/eligibility.test.ts b/packages/core/src/agent/skills/eligibility.test.ts similarity index 100% rename from src/agent/skills/eligibility.test.ts rename to packages/core/src/agent/skills/eligibility.test.ts diff --git a/src/agent/skills/eligibility.ts b/packages/core/src/agent/skills/eligibility.ts similarity index 100% rename from src/agent/skills/eligibility.ts rename to packages/core/src/agent/skills/eligibility.ts diff --git a/src/agent/skills/index.ts b/packages/core/src/agent/skills/index.ts similarity index 100% rename from src/agent/skills/index.ts rename to packages/core/src/agent/skills/index.ts diff --git a/src/agent/skills/install.ts b/packages/core/src/agent/skills/install.ts similarity index 99% rename from src/agent/skills/install.ts rename to packages/core/src/agent/skills/install.ts index 40eeddce..1f058081 100644 --- a/src/agent/skills/install.ts +++ b/packages/core/src/agent/skills/install.ts @@ -11,7 +11,7 @@ import { join, basename, dirname } from "node:path"; import { pipeline } from "node:stream/promises"; import { Readable } from "node:stream"; -import { DATA_DIR } from "../../shared/index.js"; +import { DATA_DIR } from "@multica/utils"; import type { Skill, SkillInstallSpec, SkillsInstallConfig } from "./types.js"; import { getSkillKey } from "./types.js"; import { binaryExists } from "./eligibility.js"; diff --git a/src/agent/skills/invoke.ts b/packages/core/src/agent/skills/invoke.ts similarity index 100% rename from src/agent/skills/invoke.ts rename to packages/core/src/agent/skills/invoke.ts diff --git a/src/agent/skills/loader.test.ts b/packages/core/src/agent/skills/loader.test.ts similarity index 100% rename from src/agent/skills/loader.test.ts rename to packages/core/src/agent/skills/loader.test.ts diff --git a/src/agent/skills/loader.ts b/packages/core/src/agent/skills/loader.ts similarity index 99% rename from src/agent/skills/loader.ts rename to packages/core/src/agent/skills/loader.ts index ea8f3d46..d5dc59be 100644 --- a/src/agent/skills/loader.ts +++ b/packages/core/src/agent/skills/loader.ts @@ -12,7 +12,7 @@ import { fileURLToPath } from "node:url"; import type { Skill, SkillSource, SkillManagerOptions } from "./types.js"; import { SKILL_FILE, SKILL_SOURCE_PRECEDENCE } from "./types.js"; import { parseSkillFile } from "./parser.js"; -import { DATA_DIR } from "../../shared/index.js"; +import { DATA_DIR } from "@multica/utils"; /** * Compare two semver version strings diff --git a/src/agent/skills/parser.test.ts b/packages/core/src/agent/skills/parser.test.ts similarity index 100% rename from src/agent/skills/parser.test.ts rename to packages/core/src/agent/skills/parser.test.ts diff --git a/src/agent/skills/parser.ts b/packages/core/src/agent/skills/parser.ts similarity index 100% rename from src/agent/skills/parser.ts rename to packages/core/src/agent/skills/parser.ts diff --git a/src/agent/skills/serialize.ts b/packages/core/src/agent/skills/serialize.ts similarity index 100% rename from src/agent/skills/serialize.ts rename to packages/core/src/agent/skills/serialize.ts diff --git a/src/agent/skills/types.ts b/packages/core/src/agent/skills/types.ts similarity index 100% rename from src/agent/skills/types.ts rename to packages/core/src/agent/skills/types.ts diff --git a/src/agent/skills/watcher.ts b/packages/core/src/agent/skills/watcher.ts similarity index 98% rename from src/agent/skills/watcher.ts rename to packages/core/src/agent/skills/watcher.ts index f7342f65..34487f21 100644 --- a/src/agent/skills/watcher.ts +++ b/packages/core/src/agent/skills/watcher.ts @@ -7,7 +7,7 @@ import { join } from "node:path"; import { existsSync } from "node:fs"; -import { DATA_DIR } from "../../shared/index.js"; +import { DATA_DIR } from "@multica/utils"; // ============================================================================ // Types @@ -173,7 +173,6 @@ export async function startSkillsWatcher( // eslint-disable-next-line @typescript-eslint/no-explicit-any let chokidar: any; try { - // @ts-expect-error - chokidar is optional, dynamically loaded chokidar = await import("chokidar"); } catch { // chokidar not installed, skip watching diff --git a/src/agent/subagent/announce-findings.test.ts b/packages/core/src/agent/subagent/announce-findings.test.ts similarity index 100% rename from src/agent/subagent/announce-findings.test.ts rename to packages/core/src/agent/subagent/announce-findings.test.ts diff --git a/src/agent/subagent/announce.test.ts b/packages/core/src/agent/subagent/announce.test.ts similarity index 100% rename from src/agent/subagent/announce.test.ts rename to packages/core/src/agent/subagent/announce.test.ts diff --git a/src/agent/subagent/announce.ts b/packages/core/src/agent/subagent/announce.ts similarity index 100% rename from src/agent/subagent/announce.ts rename to packages/core/src/agent/subagent/announce.ts diff --git a/src/agent/subagent/index.ts b/packages/core/src/agent/subagent/index.ts similarity index 100% rename from src/agent/subagent/index.ts rename to packages/core/src/agent/subagent/index.ts diff --git a/src/agent/subagent/registry-recovery.test.ts b/packages/core/src/agent/subagent/registry-recovery.test.ts similarity index 100% rename from src/agent/subagent/registry-recovery.test.ts rename to packages/core/src/agent/subagent/registry-recovery.test.ts diff --git a/src/agent/subagent/registry-store.test.ts b/packages/core/src/agent/subagent/registry-store.test.ts similarity index 100% rename from src/agent/subagent/registry-store.test.ts rename to packages/core/src/agent/subagent/registry-store.test.ts diff --git a/src/agent/subagent/registry-store.ts b/packages/core/src/agent/subagent/registry-store.ts similarity index 96% rename from src/agent/subagent/registry-store.ts rename to packages/core/src/agent/subagent/registry-store.ts index b96a315c..f33edd38 100644 --- a/src/agent/subagent/registry-store.ts +++ b/packages/core/src/agent/subagent/registry-store.ts @@ -6,7 +6,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { DATA_DIR } from "../../shared/index.js"; +import { DATA_DIR } from "@multica/utils"; import type { SubagentRunRecord } from "./types.js"; const SUBAGENTS_DIR = join(DATA_DIR, "subagents"); diff --git a/src/agent/subagent/registry.test.ts b/packages/core/src/agent/subagent/registry.test.ts similarity index 100% rename from src/agent/subagent/registry.test.ts rename to packages/core/src/agent/subagent/registry.test.ts diff --git a/src/agent/subagent/registry.ts b/packages/core/src/agent/subagent/registry.ts similarity index 100% rename from src/agent/subagent/registry.ts rename to packages/core/src/agent/subagent/registry.ts diff --git a/src/agent/subagent/types.ts b/packages/core/src/agent/subagent/types.ts similarity index 100% rename from src/agent/subagent/types.ts rename to packages/core/src/agent/subagent/types.ts diff --git a/src/agent/sync-agent.ts b/packages/core/src/agent/sync-agent.ts similarity index 100% rename from src/agent/sync-agent.ts rename to packages/core/src/agent/sync-agent.ts diff --git a/src/agent/system-prompt/builder.test.ts b/packages/core/src/agent/system-prompt/builder.test.ts similarity index 100% rename from src/agent/system-prompt/builder.test.ts rename to packages/core/src/agent/system-prompt/builder.test.ts diff --git a/src/agent/system-prompt/builder.ts b/packages/core/src/agent/system-prompt/builder.ts similarity index 100% rename from src/agent/system-prompt/builder.ts rename to packages/core/src/agent/system-prompt/builder.ts diff --git a/src/agent/system-prompt/constitution.ts b/packages/core/src/agent/system-prompt/constitution.ts similarity index 100% rename from src/agent/system-prompt/constitution.ts rename to packages/core/src/agent/system-prompt/constitution.ts diff --git a/src/agent/system-prompt/index.ts b/packages/core/src/agent/system-prompt/index.ts similarity index 100% rename from src/agent/system-prompt/index.ts rename to packages/core/src/agent/system-prompt/index.ts diff --git a/src/agent/system-prompt/report.ts b/packages/core/src/agent/system-prompt/report.ts similarity index 100% rename from src/agent/system-prompt/report.ts rename to packages/core/src/agent/system-prompt/report.ts diff --git a/src/agent/system-prompt/runtime-info.ts b/packages/core/src/agent/system-prompt/runtime-info.ts similarity index 100% rename from src/agent/system-prompt/runtime-info.ts rename to packages/core/src/agent/system-prompt/runtime-info.ts diff --git a/src/agent/system-prompt/sections.test.ts b/packages/core/src/agent/system-prompt/sections.test.ts similarity index 100% rename from src/agent/system-prompt/sections.test.ts rename to packages/core/src/agent/system-prompt/sections.test.ts diff --git a/src/agent/system-prompt/sections.ts b/packages/core/src/agent/system-prompt/sections.ts similarity index 100% rename from src/agent/system-prompt/sections.ts rename to packages/core/src/agent/system-prompt/sections.ts diff --git a/src/agent/system-prompt/types.ts b/packages/core/src/agent/system-prompt/types.ts similarity index 100% rename from src/agent/system-prompt/types.ts rename to packages/core/src/agent/system-prompt/types.ts diff --git a/src/agent/tools.ts b/packages/core/src/agent/tools.ts similarity index 98% rename from src/agent/tools.ts rename to packages/core/src/agent/tools.ts index 2edebb5e..f3908d73 100644 --- a/src/agent/tools.ts +++ b/packages/core/src/agent/tools.ts @@ -11,7 +11,7 @@ import { createSessionsListTool } from "./tools/sessions-list.js"; import { createMemorySearchTool } from "./tools/memory-search.js"; import { createCronTool } from "./tools/cron/index.js"; import { filterTools } from "./tools/policy.js"; -import { isMulticaError, isRetryableError } from "../shared/errors.js"; +import { isMulticaError, isRetryableError } from "@multica/utils"; import type { ExecApprovalCallback } from "./tools/exec-approval-types.js"; // Re-export resolveModel from providers for backwards compatibility diff --git a/src/agent/tools/README.md b/packages/core/src/agent/tools/README.md similarity index 100% rename from src/agent/tools/README.md rename to packages/core/src/agent/tools/README.md diff --git a/src/agent/tools/README.zh-CN.md b/packages/core/src/agent/tools/README.zh-CN.md similarity index 100% rename from src/agent/tools/README.zh-CN.md rename to packages/core/src/agent/tools/README.zh-CN.md diff --git a/src/agent/tools/cron/cron-tool.ts b/packages/core/src/agent/tools/cron/cron-tool.ts similarity index 100% rename from src/agent/tools/cron/cron-tool.ts rename to packages/core/src/agent/tools/cron/cron-tool.ts diff --git a/src/agent/tools/cron/index.ts b/packages/core/src/agent/tools/cron/index.ts similarity index 100% rename from src/agent/tools/cron/index.ts rename to packages/core/src/agent/tools/cron/index.ts diff --git a/src/agent/tools/exec-allowlist.test.ts b/packages/core/src/agent/tools/exec-allowlist.test.ts similarity index 100% rename from src/agent/tools/exec-allowlist.test.ts rename to packages/core/src/agent/tools/exec-allowlist.test.ts diff --git a/src/agent/tools/exec-allowlist.ts b/packages/core/src/agent/tools/exec-allowlist.ts similarity index 100% rename from src/agent/tools/exec-allowlist.ts rename to packages/core/src/agent/tools/exec-allowlist.ts diff --git a/src/agent/tools/exec-approval-cli.ts b/packages/core/src/agent/tools/exec-approval-cli.ts similarity index 100% rename from src/agent/tools/exec-approval-cli.ts rename to packages/core/src/agent/tools/exec-approval-cli.ts diff --git a/src/agent/tools/exec-approval-types.ts b/packages/core/src/agent/tools/exec-approval-types.ts similarity index 100% rename from src/agent/tools/exec-approval-types.ts rename to packages/core/src/agent/tools/exec-approval-types.ts diff --git a/src/agent/tools/exec-safety.test.ts b/packages/core/src/agent/tools/exec-safety.test.ts similarity index 100% rename from src/agent/tools/exec-safety.test.ts rename to packages/core/src/agent/tools/exec-safety.test.ts diff --git a/src/agent/tools/exec-safety.ts b/packages/core/src/agent/tools/exec-safety.ts similarity index 100% rename from src/agent/tools/exec-safety.ts rename to packages/core/src/agent/tools/exec-safety.ts diff --git a/src/agent/tools/exec.ts b/packages/core/src/agent/tools/exec.ts similarity index 100% rename from src/agent/tools/exec.ts rename to packages/core/src/agent/tools/exec.ts diff --git a/src/agent/tools/glob.test.ts b/packages/core/src/agent/tools/glob.test.ts similarity index 100% rename from src/agent/tools/glob.test.ts rename to packages/core/src/agent/tools/glob.test.ts diff --git a/src/agent/tools/glob.ts b/packages/core/src/agent/tools/glob.ts similarity index 100% rename from src/agent/tools/glob.ts rename to packages/core/src/agent/tools/glob.ts diff --git a/src/agent/tools/groups.ts b/packages/core/src/agent/tools/groups.ts similarity index 100% rename from src/agent/tools/groups.ts rename to packages/core/src/agent/tools/groups.ts diff --git a/src/agent/tools/index.ts b/packages/core/src/agent/tools/index.ts similarity index 100% rename from src/agent/tools/index.ts rename to packages/core/src/agent/tools/index.ts diff --git a/src/agent/tools/memory-search.test.ts b/packages/core/src/agent/tools/memory-search.test.ts similarity index 100% rename from src/agent/tools/memory-search.test.ts rename to packages/core/src/agent/tools/memory-search.test.ts diff --git a/src/agent/tools/memory-search.ts b/packages/core/src/agent/tools/memory-search.ts similarity index 100% rename from src/agent/tools/memory-search.ts rename to packages/core/src/agent/tools/memory-search.ts diff --git a/src/agent/tools/policy.test.ts b/packages/core/src/agent/tools/policy.test.ts similarity index 100% rename from src/agent/tools/policy.test.ts rename to packages/core/src/agent/tools/policy.test.ts diff --git a/src/agent/tools/policy.ts b/packages/core/src/agent/tools/policy.ts similarity index 100% rename from src/agent/tools/policy.ts rename to packages/core/src/agent/tools/policy.ts diff --git a/src/agent/tools/process-registry.ts b/packages/core/src/agent/tools/process-registry.ts similarity index 100% rename from src/agent/tools/process-registry.ts rename to packages/core/src/agent/tools/process-registry.ts diff --git a/src/agent/tools/process.ts b/packages/core/src/agent/tools/process.ts similarity index 100% rename from src/agent/tools/process.ts rename to packages/core/src/agent/tools/process.ts diff --git a/src/agent/tools/sessions-list.test.ts b/packages/core/src/agent/tools/sessions-list.test.ts similarity index 100% rename from src/agent/tools/sessions-list.test.ts rename to packages/core/src/agent/tools/sessions-list.test.ts diff --git a/src/agent/tools/sessions-list.ts b/packages/core/src/agent/tools/sessions-list.ts similarity index 100% rename from src/agent/tools/sessions-list.ts rename to packages/core/src/agent/tools/sessions-list.ts diff --git a/src/agent/tools/sessions-spawn.test.ts b/packages/core/src/agent/tools/sessions-spawn.test.ts similarity index 100% rename from src/agent/tools/sessions-spawn.test.ts rename to packages/core/src/agent/tools/sessions-spawn.test.ts diff --git a/src/agent/tools/sessions-spawn.ts b/packages/core/src/agent/tools/sessions-spawn.ts similarity index 100% rename from src/agent/tools/sessions-spawn.ts rename to packages/core/src/agent/tools/sessions-spawn.ts diff --git a/src/agent/tools/web/cache.test.ts b/packages/core/src/agent/tools/web/cache.test.ts similarity index 100% rename from src/agent/tools/web/cache.test.ts rename to packages/core/src/agent/tools/web/cache.test.ts diff --git a/src/agent/tools/web/cache.ts b/packages/core/src/agent/tools/web/cache.ts similarity index 100% rename from src/agent/tools/web/cache.ts rename to packages/core/src/agent/tools/web/cache.ts diff --git a/src/agent/tools/web/html-utils.test.ts b/packages/core/src/agent/tools/web/html-utils.test.ts similarity index 100% rename from src/agent/tools/web/html-utils.test.ts rename to packages/core/src/agent/tools/web/html-utils.test.ts diff --git a/src/agent/tools/web/html-utils.ts b/packages/core/src/agent/tools/web/html-utils.ts similarity index 100% rename from src/agent/tools/web/html-utils.ts rename to packages/core/src/agent/tools/web/html-utils.ts diff --git a/src/agent/tools/web/index.ts b/packages/core/src/agent/tools/web/index.ts similarity index 100% rename from src/agent/tools/web/index.ts rename to packages/core/src/agent/tools/web/index.ts diff --git a/src/agent/tools/web/param-helpers.test.ts b/packages/core/src/agent/tools/web/param-helpers.test.ts similarity index 100% rename from src/agent/tools/web/param-helpers.test.ts rename to packages/core/src/agent/tools/web/param-helpers.test.ts diff --git a/src/agent/tools/web/param-helpers.ts b/packages/core/src/agent/tools/web/param-helpers.ts similarity index 100% rename from src/agent/tools/web/param-helpers.ts rename to packages/core/src/agent/tools/web/param-helpers.ts diff --git a/src/agent/tools/web/ssrf.test.ts b/packages/core/src/agent/tools/web/ssrf.test.ts similarity index 100% rename from src/agent/tools/web/ssrf.test.ts rename to packages/core/src/agent/tools/web/ssrf.test.ts diff --git a/src/agent/tools/web/ssrf.ts b/packages/core/src/agent/tools/web/ssrf.ts similarity index 100% rename from src/agent/tools/web/ssrf.ts rename to packages/core/src/agent/tools/web/ssrf.ts diff --git a/src/agent/tools/web/web-fetch.ts b/packages/core/src/agent/tools/web/web-fetch.ts similarity index 100% rename from src/agent/tools/web/web-fetch.ts rename to packages/core/src/agent/tools/web/web-fetch.ts diff --git a/src/agent/tools/web/web-search.ts b/packages/core/src/agent/tools/web/web-search.ts similarity index 100% rename from src/agent/tools/web/web-search.ts rename to packages/core/src/agent/tools/web/web-search.ts diff --git a/src/agent/types.ts b/packages/core/src/agent/types.ts similarity index 100% rename from src/agent/types.ts rename to packages/core/src/agent/types.ts diff --git a/src/channels/config.ts b/packages/core/src/channels/config.ts similarity index 100% rename from src/channels/config.ts rename to packages/core/src/channels/config.ts diff --git a/src/channels/inbound-debouncer.ts b/packages/core/src/channels/inbound-debouncer.ts similarity index 100% rename from src/channels/inbound-debouncer.ts rename to packages/core/src/channels/inbound-debouncer.ts diff --git a/src/channels/index.ts b/packages/core/src/channels/index.ts similarity index 100% rename from src/channels/index.ts rename to packages/core/src/channels/index.ts diff --git a/src/channels/manager.test.ts b/packages/core/src/channels/manager.test.ts similarity index 100% rename from src/channels/manager.test.ts rename to packages/core/src/channels/manager.test.ts diff --git a/src/channels/manager.ts b/packages/core/src/channels/manager.ts similarity index 100% rename from src/channels/manager.ts rename to packages/core/src/channels/manager.ts diff --git a/src/channels/plugins/telegram-format.ts b/packages/core/src/channels/plugins/telegram-format.ts similarity index 100% rename from src/channels/plugins/telegram-format.ts rename to packages/core/src/channels/plugins/telegram-format.ts diff --git a/src/channels/plugins/telegram.ts b/packages/core/src/channels/plugins/telegram.ts similarity index 99% rename from src/channels/plugins/telegram.ts rename to packages/core/src/channels/plugins/telegram.ts index a4bb02c2..fa6f103e 100644 --- a/src/channels/plugins/telegram.ts +++ b/packages/core/src/channels/plugins/telegram.ts @@ -15,7 +15,7 @@ import { v7 as uuidv7 } from "uuid"; import { Bot, GrammyError } from "grammy"; import type { ChannelPlugin, ChannelMessage, ChannelConfigAdapter, ChannelsConfig, DeliveryContext } from "../types.js"; import { markdownToTelegramHtml } from "./telegram-format.js"; -import { MEDIA_CACHE_DIR } from "../../shared/paths.js"; +import { MEDIA_CACHE_DIR } from "@multica/utils"; /** Telegram account config shape */ interface TelegramAccountConfig { diff --git a/src/channels/registry.ts b/packages/core/src/channels/registry.ts similarity index 100% rename from src/channels/registry.ts rename to packages/core/src/channels/registry.ts diff --git a/src/channels/types.ts b/packages/core/src/channels/types.ts similarity index 100% rename from src/channels/types.ts rename to packages/core/src/channels/types.ts diff --git a/packages/core/src/client/actions/exec-approval.ts b/packages/core/src/client/actions/exec-approval.ts new file mode 100644 index 00000000..cd098273 --- /dev/null +++ b/packages/core/src/client/actions/exec-approval.ts @@ -0,0 +1,40 @@ +/** + * Exec Approval Actions — WebSocket protocol types for exec approval flow + */ + +/** Action name for exec approval requests (Hub → Client) */ +export const ExecApprovalRequestAction = "exec-approval-request" as const; + +/** Approval decision types */ +export type ApprovalDecision = "allow-once" | "allow-always" | "deny"; + +/** Payload for exec approval request (Hub → Client) */ +export interface ExecApprovalRequestPayload { + /** Unique approval ID */ + approvalId: string; + /** Agent that initiated the command */ + agentId: string; + /** Shell command requiring approval */ + command: string; + /** Working directory */ + cwd?: string; + /** Evaluated risk level */ + riskLevel: "safe" | "needs-review" | "dangerous"; + /** Reasons for the risk assessment */ + riskReasons: string[]; + /** When this approval expires (ms since epoch). -1 means no timeout. */ + expiresAtMs: number; +} + +/** Params for resolveExecApproval RPC (Client → Hub) */ +export interface ResolveExecApprovalParams { + /** The approval ID to resolve */ + approvalId: string; + /** User decision */ + decision: ApprovalDecision; +} + +/** Result of resolveExecApproval RPC */ +export interface ResolveExecApprovalResult { + ok: boolean; +} diff --git a/packages/core/src/client/actions/hello.ts b/packages/core/src/client/actions/hello.ts new file mode 100644 index 00000000..36e56ba8 --- /dev/null +++ b/packages/core/src/client/actions/hello.ts @@ -0,0 +1,14 @@ +/** Hello Action - test greeting message */ + +export const HelloAction = "hello" as const; +export const HelloResponseAction = "hello_response" as const; + +/** Hello request payload */ +export interface HelloPayload { + greeting: string; +} + +/** Hello response payload */ +export interface HelloResponsePayload { + reply: string; +} diff --git a/packages/core/src/client/actions/index.ts b/packages/core/src/client/actions/index.ts new file mode 100644 index 00000000..b3ef94e3 --- /dev/null +++ b/packages/core/src/client/actions/index.ts @@ -0,0 +1,56 @@ +export { + HelloAction, + HelloResponseAction, + type HelloPayload, + type HelloResponsePayload, +} from "./hello"; + +export { + RequestAction, + ResponseAction, + type RequestPayload, + type ResponsePayload, + type ResponseSuccessPayload, + type ResponseErrorPayload, + isResponseSuccess, + isResponseError, + type AgentMessageItem, + DEFAULT_MESSAGES_LIMIT, + type GetAgentMessagesParams, + type GetAgentMessagesResult, + type GetHubInfoResult, + type ListAgentsResult, + type CreateAgentParams, + type CreateAgentResult, + type DeleteAgentParams, + type DeleteAgentResult, + type UpdateGatewayParams, + type UpdateGatewayResult, + type DeviceMeta, + type VerifyParams, + type VerifyResult, +} from "./rpc"; + +export { + StreamAction, + type StreamPayload, + type AgentEvent, + type CompactionEvent, + type CompactionStartEvent, + type CompactionEndEvent, + type AgentErrorEvent, + type ContentBlock, + type TextContent, + type ThinkingContent, + type ToolCall, + type ImageContent, + extractThinkingFromEvent, +} from "./stream"; + +export { + ExecApprovalRequestAction, + type ApprovalDecision, + type ExecApprovalRequestPayload, + type ResolveExecApprovalParams, + type ResolveExecApprovalResult, +} from "./exec-approval"; diff --git a/packages/core/src/client/actions/rpc.ts b/packages/core/src/client/actions/rpc.ts new file mode 100644 index 00000000..ddbc78f7 --- /dev/null +++ b/packages/core/src/client/actions/rpc.ts @@ -0,0 +1,148 @@ +/** RPC Actions - 请求/响应模式 */ + +import type { Message } from "@mariozechner/pi-ai"; + +export const RequestAction = "request" as const; +export const ResponseAction = "response" as const; + +/** 请求帧 payload */ +export interface RequestPayload { + /** 请求 ID,由客户端生成,服务端原样回传到 ResponsePayload.requestId */ + requestId: string; + /** 调用的方法名 */ + method: string; + /** 方法参数 */ + params?: T; +} + +/** 响应帧 payload - 成功 */ +export interface ResponseSuccessPayload { + /** 与请求消息 ID 匹配 */ + requestId: string; + /** 是否成功 */ + ok: true; + /** 返回数据 */ + payload: T; +} + +/** 响应帧 payload - 失败 */ +export interface ResponseErrorPayload { + /** 与请求消息 ID 匹配 */ + requestId: string; + /** 是否成功 */ + ok: false; + /** 错误信息 */ + error: { + code: string; + message: string; + retryable?: boolean; + }; +} + +/** 响应帧 payload(联合类型) */ +export type ResponsePayload = + | ResponseSuccessPayload + | ResponseErrorPayload; + +/** 类型守卫:判断响应是否成功 */ +export function isResponseSuccess( + response: ResponsePayload +): response is ResponseSuccessPayload { + return response.ok === true; +} + +/** 类型守卫:判断响应是否失败 */ +export function isResponseError( + response: ResponsePayload +): response is ResponseErrorPayload { + return response.ok === false; +} + +// ============ RPC Method Types ============ + +/** Default number of messages returned per page */ +export const DEFAULT_MESSAGES_LIMIT = 200; + +/** getAgentMessages - request params */ +export interface GetAgentMessagesParams { + agentId: string; + offset?: number; + limit?: number; +} + +/** + * Agent message returned by getAgentMessages. + * This is pi-ai's Message type — the backend returns it as-is from SessionManager.loadMessages(). + */ +export type AgentMessageItem = Message; + +/** getAgentMessages - response payload */ +export interface GetAgentMessagesResult { + messages: AgentMessageItem[]; + total: number; + offset: number; + limit: number; +} + +/** getHubInfo - no params needed */ +export interface GetHubInfoResult { + hubId: string; + url: string; + connectionState: string; + agentCount: number; +} + +/** listAgents - no params needed */ +export interface ListAgentsResult { + agents: { id: string; closed: boolean }[]; +} + +/** createAgent - request params */ +export interface CreateAgentParams { + id?: string; +} + +/** createAgent - response payload */ +export interface CreateAgentResult { + id: string; +} + +/** deleteAgent - request params */ +export interface DeleteAgentParams { + id: string; +} + +/** deleteAgent - response payload */ +export interface DeleteAgentResult { + ok: boolean; +} + +/** updateGateway - request params */ +export interface UpdateGatewayParams { + url: string; +} + +/** updateGateway - response payload */ +export interface UpdateGatewayResult { + url: string; + connectionState: string; +} + +/** Device metadata collected during verify handshake */ +export interface DeviceMeta { + userAgent?: string; + platform?: string; + language?: string; +} + +/** verify - request params */ +export interface VerifyParams { + token?: string; + meta?: DeviceMeta; +} + +/** verify - response payload */ +export interface VerifyResult { + hubId: string; + agentId: string; +} diff --git a/packages/core/src/client/actions/stream.ts b/packages/core/src/client/actions/stream.ts new file mode 100644 index 00000000..dfaf06fa --- /dev/null +++ b/packages/core/src/client/actions/stream.ts @@ -0,0 +1,77 @@ +/** Stream Action */ + +export const StreamAction = "stream" as const; + +// --- Content block types (re-exported from pi-ai, the single source of truth) --- + +import type { + TextContent, + ThinkingContent, + ToolCall, + ImageContent, +} from "@mariozechner/pi-ai"; +import type { AgentEvent } from "@mariozechner/pi-agent-core"; + +export type { TextContent, ThinkingContent, ToolCall, ImageContent }; +export type { AgentEvent }; + +/** + * Convenience union of all content block types across message roles. + * + * NOTE: This is a deliberate simplification. The backend uses narrower unions + * per role (e.g. AssistantMessage.content excludes ImageContent, UserMessage + * excludes ThinkingContent/ToolCall). We accept the wider union on the frontend + * for simpler handling — the backend already guarantees correctness. + */ +export type ContentBlock = TextContent | ThinkingContent | ToolCall | ImageContent; + +// --- Compaction event types (Multica-specific, not from pi-agent-core) --- + +/** Emitted when context compaction begins */ +export type CompactionStartEvent = { + type: "compaction_start"; +}; + +/** Emitted when context compaction completes */ +export type CompactionEndEvent = { + type: "compaction_end"; + removed: number; + kept: number; + tokensRemoved?: number; + tokensKept?: number; + reason: string; +}; + +/** Union of all compaction events */ +export type CompactionEvent = CompactionStartEvent | CompactionEndEvent; + +/** Emitted when an agent encounters an error during execution */ +export type AgentErrorEvent = { + type: "agent_error"; + message: string; +}; + +// --- Stream event types --- + +/** + * Hub forwards AgentEvent from pi-agent-core, CompactionEvent, and AgentErrorEvent as-is. + * StreamPayload wraps them with routing metadata. + */ +export interface StreamPayload { + streamId: string; + agentId: string; + event: AgentEvent | CompactionEvent | AgentErrorEvent; +} + +/** Extract thinking/reasoning content from an AgentEvent that carries a message */ +export function extractThinkingFromEvent(event: AgentEvent): string { + if (!("message" in event)) return ""; + const msg = event.message; + if (!msg || !("content" in msg)) return ""; + const content = msg.content; + if (!Array.isArray(content)) return ""; + return content + .filter((c): c is ThinkingContent => c.type === "thinking") + .map((c) => c.thinking ?? "") + .join(""); +} diff --git a/packages/core/src/client/client.ts b/packages/core/src/client/client.ts new file mode 100644 index 00000000..cf7bf932 --- /dev/null +++ b/packages/core/src/client/client.ts @@ -0,0 +1,377 @@ +import { io, Socket } from "socket.io-client"; +import { v7 as uuidv7 } from "uuid"; +import type { + GatewayClientOptions, + GatewayClientCallbacks, + ConnectionState, + RoutedMessage, + RegisteredResponse, + SendErrorResponse, + PingPayload, + DeviceType, + DeviceInfo, + ListDevicesResponse, +} from "./types"; +import { GatewayEvents } from "./types"; +import { + RequestAction, + ResponseAction, + type RequestPayload, + type ResponsePayload, + isResponseSuccess, +} from "./actions/rpc"; + +interface PendingRequest { + resolve: (value: T) => void; + reject: (reason: Error) => void; + timer: ReturnType; +} + +interface ResolvedOptions { + url: string; + path: string; + deviceId: string; + deviceType: DeviceType; + autoReconnect: boolean; + reconnectDelay: number; + hubId: string | undefined; + token: string | undefined; + verifyTimeout: number; +} + +export class GatewayClient { + private socket: Socket | null = null; + private options: ResolvedOptions; + private callbacks: GatewayClientCallbacks = {}; + private _state: ConnectionState = "disconnected"; + private pendingRequests = new Map(); + + constructor(options: GatewayClientOptions) { + if (!options.deviceId) { + throw new Error("deviceId is required"); + } + + this.options = { + url: options.url, + path: options.path ?? "/ws", + deviceId: options.deviceId, + deviceType: options.deviceType, + autoReconnect: options.autoReconnect ?? true, + reconnectDelay: options.reconnectDelay ?? 1000, + hubId: options.hubId, + token: options.token, + verifyTimeout: options.verifyTimeout ?? 30_000, + }; + } + + /** 当前连接状态 */ + get state(): ConnectionState { + return this._state; + } + + /** 设备ID */ + get deviceId(): string { + return this.options.deviceId; + } + + /** 设备类型 */ + get deviceType(): DeviceType { + return this.options.deviceType; + } + + /** Socket ID(连接后可用) */ + get socketId(): string | undefined { + return this.socket?.id; + } + + /** 是否已连接 */ + get isConnected(): boolean { + return this._state === "connected" || this._state === "registered"; + } + + /** 是否已注册 */ + get isRegistered(): boolean { + return this._state === "registered"; + } + + /** 连接到服务器,deviceId 和 deviceType 通过 query 传递 */ + connect(): this { + if (this.socket) { + return this; + } + + this.setState("connecting"); + + const query: Record = { + deviceId: this.options.deviceId, + deviceType: this.options.deviceType, + }; + + this.socket = io(this.options.url, { + path: this.options.path, + query, + reconnection: this.options.autoReconnect, + reconnectionDelay: this.options.reconnectDelay, + }); + + this.setupListeners(); + return this; + } + + /** 断开连接 */ + disconnect(): this { + if (this.socket) { + this.socket.disconnect(); + this.socket = null; + } + this.setState("disconnected"); + return this; + } + + /** 发送消息给指定设备 */ + send( + to: string, + action: string, + payload: T, + messageId?: string + ): string { + if (!this.socket || !this.isRegistered) { + throw new Error("Not registered"); + } + + const id = messageId ?? this.generateMessageId(); + const message: RoutedMessage = { + id, + uid: null, + from: this.options.deviceId, + to, + action, + payload, + }; + + this.socket.emit(GatewayEvents.SEND, message); + return id; + } + + /** 发送 ping */ + ping(data: PingPayload = {}): Promise { + return new Promise((resolve, reject) => { + if (!this.socket || !this.isConnected) { + reject(new Error("Not connected")); + return; + } + + this.socket.emit( + GatewayEvents.PING, + data, + (response: { event: string; data: string }) => { + resolve(response.data); + } + ); + }); + } + + /** List all devices connected to the Gateway */ + listDevices(): Promise { + return new Promise((resolve, reject) => { + if (!this.socket || !this.isRegistered) { + reject(new Error("Not registered")); + return; + } + + this.socket.emit( + GatewayEvents.LIST_DEVICES, + {}, + (response: ListDevicesResponse) => { + resolve(response.devices); + } + ); + }); + } + + /** Send an RPC request and wait for the response */ + request( + to: string, + method: string, + params?: unknown, + timeout = 10_000, + ): Promise { + return new Promise((resolve, reject) => { + if (!this.socket || !this.isRegistered) { + reject(new Error("Not registered")); + return; + } + + const requestId = this.generateMessageId(); + const timer = setTimeout(() => { + this.pendingRequests.delete(requestId); + reject(new Error(`RPC request timed out: ${method}`)); + }, timeout); + + this.pendingRequests.set(requestId, { resolve: resolve as (v: unknown) => void, reject, timer }); + + const payload: RequestPayload = { requestId, method, params }; + this.send(to, RequestAction, payload); + }); + } + + /** 注册连接回调 */ + onConnect(callback: (socketId: string) => void): this { + this.callbacks.onConnect = callback; + return this; + } + + /** 注册断开回调 */ + onDisconnect(callback: (reason: string) => void): this { + this.callbacks.onDisconnect = callback; + return this; + } + + /** 注册成功回调 */ + onRegistered(callback: (deviceId: string) => void): this { + this.callbacks.onRegistered = callback; + return this; + } + + /** Hub 验证成功回调 */ + onVerified(callback: (result: { hubId: string; agentId: string; isNewDevice?: boolean }) => void): this { + this.callbacks.onVerified = callback; + return this; + } + + /** 注册消息回调 */ + onMessage(callback: (message: RoutedMessage) => void): this { + this.callbacks.onMessage = callback; + return this; + } + + /** 注册发送失败回调 */ + onSendError(callback: (error: SendErrorResponse) => void): this { + this.callbacks.onSendError = callback; + return this; + } + + /** 注册 pong 回调 */ + onPong(callback: (data: string) => void): this { + this.callbacks.onPong = callback; + return this; + } + + /** 注册错误回调 */ + onError(callback: (error: Error) => void): this { + this.callbacks.onError = callback; + return this; + } + + /** 注册状态变化回调 */ + onStateChange(callback: (state: ConnectionState) => void): this { + this.callbacks.onStateChange = callback; + return this; + } + + private setState(state: ConnectionState): void { + if (this._state !== state) { + this._state = state; + this.callbacks.onStateChange?.(state); + } + } + + private generateMessageId(): string { + return uuidv7(); + } + + private setupListeners(): void { + if (!this.socket) return; + + this.socket.on("connect", () => { + this.setState("connected"); + this.callbacks.onConnect?.(this.socket!.id!); + // 服务端在连接时从 query 自动注册,等待 registered 事件即可 + }); + + this.socket.on("disconnect", (reason: string) => { + this.setState("disconnected"); + // Reject all pending RPC requests + for (const [id, pending] of this.pendingRequests) { + clearTimeout(pending.timer); + pending.reject(new Error("Disconnected")); + } + this.pendingRequests.clear(); + this.callbacks.onDisconnect?.(reason); + }); + + this.socket.on( + GatewayEvents.REGISTERED, + (response: RegisteredResponse) => { + if (!response.success) { + this.callbacks.onError?.(new Error(response.error ?? "Registration failed")); + return; + } + + // If hubId is configured, auto-verify before exposing "registered" to upper layer + if (this.options.hubId) { + // Set internal state to allow send/request during verify + this._state = "registered"; + this.callbacks.onStateChange?.("verifying"); + const meta = typeof navigator !== "undefined" ? { + userAgent: navigator.userAgent, + platform: navigator.platform, + language: navigator.language, + } : undefined; + this.request<{ hubId: string; agentId: string; isNewDevice?: boolean }>( + this.options.hubId, + "verify", + { token: this.options.token, meta }, + this.options.verifyTimeout, + ) + .then((result) => { + // Verify succeeded — now expose "registered" to upper layer + this.callbacks.onVerified?.(result); + this.callbacks.onRegistered?.(response.deviceId); + this.callbacks.onStateChange?.("registered"); + }) + .catch((err) => { + // Verify failed (UNAUTHORIZED, REJECTED, or timeout) + this.callbacks.onError?.(err instanceof Error ? err : new Error(String(err))); + this.disconnect(); + }); + } else { + // No hubId — original behavior + this.setState("registered"); + this.callbacks.onRegistered?.(response.deviceId); + } + } + ); + + this.socket.on(GatewayEvents.RECEIVE, (message: RoutedMessage) => { + // Intercept RPC responses and resolve pending requests + if (message.action === ResponseAction) { + const response = message.payload as ResponsePayload; + const pending = this.pendingRequests.get(response.requestId); + if (pending) { + this.pendingRequests.delete(response.requestId); + clearTimeout(pending.timer); + if (isResponseSuccess(response)) { + pending.resolve(response.payload); + } else { + pending.reject(new Error(`RPC error [${response.error.code}]: ${response.error.message}`)); + } + return; + } + } + this.callbacks.onMessage?.(message); + }); + + this.socket.on(GatewayEvents.SEND_ERROR, (error: SendErrorResponse) => { + this.callbacks.onSendError?.(error); + }); + + this.socket.on(GatewayEvents.PONG, (data: string) => { + this.callbacks.onPong?.(data); + }); + + this.socket.on("connect_error", (error: Error) => { + this.callbacks.onError?.(error); + }); + } +} diff --git a/packages/core/src/client/index.ts b/packages/core/src/client/index.ts new file mode 100644 index 00000000..a14f8707 --- /dev/null +++ b/packages/core/src/client/index.ts @@ -0,0 +1,7 @@ +/** + * Gateway Client + * WebSocket client for connecting to Gateway + */ +export * from "./client.js"; +export * from "./types.js"; +export * from "./actions/index.js"; diff --git a/packages/core/src/client/types.ts b/packages/core/src/client/types.ts new file mode 100644 index 00000000..f8a3f693 --- /dev/null +++ b/packages/core/src/client/types.ts @@ -0,0 +1,119 @@ +/** WebSocket event names */ +export const GatewayEvents = { + // System events + PING: "ping", + PONG: "pong", + REGISTERED: "registered", + LIST_DEVICES: "list-devices", + + // Message routing + SEND: "send", + RECEIVE: "receive", + SEND_ERROR: "send_error", +} as const; + +// ============ Device Related ============ + +/** Device type */ +export type DeviceType = "client" | "hub" | "agent"; + +/** Device information */ +export interface DeviceInfo { + deviceId: string; + deviceType: DeviceType; +} + +/** Registration response */ +export interface RegisteredResponse { + success: boolean; + deviceId: string; + error?: string; +} + +// ============ Message Routing ============ + +/** Routed message */ +export interface RoutedMessage { + /** Unique message ID (UUID v7, contains timestamp) */ + id: string; + /** User ID (populated after login) */ + uid: string | null; + /** Sender deviceId */ + from: string; + /** Recipient deviceId */ + to: string; + /** Action type */ + action: string; + /** Message payload */ + payload: T; +} + +/** Send failure response */ +export interface SendErrorResponse { + messageId: string; + error: string; + code: "DEVICE_NOT_FOUND" | "NOT_REGISTERED" | "INVALID_MESSAGE"; +} + +/** List devices response */ +export interface ListDevicesResponse { + devices: DeviceInfo[]; +} + +// ============ Ping/Pong ============ + +/** Ping request */ +export interface PingPayload { + [key: string]: unknown; +} + +/** Ping response */ +export interface PongResponse { + event: string; + data: string; +} + +// ============ Client Configuration ============ + +/** Connection configuration */ +export interface GatewayClientOptions { + /** Server address, e.g. http://localhost:3000 */ + url: string; + /** WebSocket path, defaults to /ws */ + path?: string | undefined; + /** Device ID */ + deviceId: string; + /** Device type */ + deviceType: DeviceType; + /** Auto reconnect, defaults to true */ + autoReconnect?: boolean | undefined; + /** Reconnect delay (milliseconds), defaults to 1000 */ + reconnectDelay?: number | undefined; + /** Hub device ID for verification (optional, enables auto-verify after gateway registration) */ + hubId?: string | undefined; + /** Token for first-time verification (optional, omit for reconnection via device whitelist) */ + token?: string | undefined; + /** Verify timeout in ms (default: 30_000, longer because user confirmation may be needed) */ + verifyTimeout?: number | undefined; +} + +/** Connection state */ +export type ConnectionState = + | "disconnected" + | "connecting" + | "connected" + | "verifying" + | "registered"; + +/** Event callback types */ +export interface GatewayClientCallbacks { + onConnect?: (socketId: string) => void; + onDisconnect?: (reason: string) => void; + onRegistered?: (deviceId: string) => void; + onVerified?: (result: { hubId: string; agentId: string; isNewDevice?: boolean }) => void; + onMessage?: (message: RoutedMessage) => void; + onSendError?: (error: SendErrorResponse) => void; + onPong?: (data: string) => void; + onError?: (error: Error) => void; + onStateChange?: (state: ConnectionState) => void; +} diff --git a/src/cron/execute.ts b/packages/core/src/cron/execute.ts similarity index 100% rename from src/cron/execute.ts rename to packages/core/src/cron/execute.ts diff --git a/src/cron/index.ts b/packages/core/src/cron/index.ts similarity index 100% rename from src/cron/index.ts rename to packages/core/src/cron/index.ts diff --git a/src/cron/schedule.ts b/packages/core/src/cron/schedule.ts similarity index 100% rename from src/cron/schedule.ts rename to packages/core/src/cron/schedule.ts diff --git a/src/cron/service.ts b/packages/core/src/cron/service.ts similarity index 100% rename from src/cron/service.ts rename to packages/core/src/cron/service.ts diff --git a/src/cron/store.ts b/packages/core/src/cron/store.ts similarity index 100% rename from src/cron/store.ts rename to packages/core/src/cron/store.ts diff --git a/src/cron/types.ts b/packages/core/src/cron/types.ts similarity index 100% rename from src/cron/types.ts rename to packages/core/src/cron/types.ts diff --git a/src/heartbeat/heartbeat-events.ts b/packages/core/src/heartbeat/heartbeat-events.ts similarity index 100% rename from src/heartbeat/heartbeat-events.ts rename to packages/core/src/heartbeat/heartbeat-events.ts diff --git a/src/heartbeat/heartbeat-text.test.ts b/packages/core/src/heartbeat/heartbeat-text.test.ts similarity index 100% rename from src/heartbeat/heartbeat-text.test.ts rename to packages/core/src/heartbeat/heartbeat-text.test.ts diff --git a/src/heartbeat/heartbeat-text.ts b/packages/core/src/heartbeat/heartbeat-text.ts similarity index 100% rename from src/heartbeat/heartbeat-text.ts rename to packages/core/src/heartbeat/heartbeat-text.ts diff --git a/src/heartbeat/heartbeat-wake.test.ts b/packages/core/src/heartbeat/heartbeat-wake.test.ts similarity index 100% rename from src/heartbeat/heartbeat-wake.test.ts rename to packages/core/src/heartbeat/heartbeat-wake.test.ts diff --git a/src/heartbeat/heartbeat-wake.ts b/packages/core/src/heartbeat/heartbeat-wake.ts similarity index 100% rename from src/heartbeat/heartbeat-wake.ts rename to packages/core/src/heartbeat/heartbeat-wake.ts diff --git a/src/heartbeat/index.ts b/packages/core/src/heartbeat/index.ts similarity index 100% rename from src/heartbeat/index.ts rename to packages/core/src/heartbeat/index.ts diff --git a/src/heartbeat/runner.test.ts b/packages/core/src/heartbeat/runner.test.ts similarity index 100% rename from src/heartbeat/runner.test.ts rename to packages/core/src/heartbeat/runner.test.ts diff --git a/src/heartbeat/runner.ts b/packages/core/src/heartbeat/runner.ts similarity index 100% rename from src/heartbeat/runner.ts rename to packages/core/src/heartbeat/runner.ts diff --git a/src/heartbeat/system-events.ts b/packages/core/src/heartbeat/system-events.ts similarity index 100% rename from src/heartbeat/system-events.ts rename to packages/core/src/heartbeat/system-events.ts diff --git a/src/hub/agent-store.ts b/packages/core/src/hub/agent-store.ts similarity index 96% rename from src/hub/agent-store.ts rename to packages/core/src/hub/agent-store.ts index c5aabe19..832879ae 100644 --- a/src/hub/agent-store.ts +++ b/packages/core/src/hub/agent-store.ts @@ -1,6 +1,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { DATA_DIR } from "../shared/index.js"; +import { DATA_DIR } from "@multica/utils"; export interface AgentRecord { id: string; diff --git a/src/hub/block-chunker.test.ts b/packages/core/src/hub/block-chunker.test.ts similarity index 100% rename from src/hub/block-chunker.test.ts rename to packages/core/src/hub/block-chunker.test.ts diff --git a/src/hub/block-chunker.ts b/packages/core/src/hub/block-chunker.ts similarity index 100% rename from src/hub/block-chunker.ts rename to packages/core/src/hub/block-chunker.ts diff --git a/src/hub/device-store.ts b/packages/core/src/hub/device-store.ts similarity index 98% rename from src/hub/device-store.ts rename to packages/core/src/hub/device-store.ts index 0b1d31b3..ca2656af 100644 --- a/src/hub/device-store.ts +++ b/packages/core/src/hub/device-store.ts @@ -1,6 +1,6 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -import { DATA_DIR } from "../shared/index.js"; +import { DATA_DIR } from "@multica/utils"; // ============ Types ============ diff --git a/src/hub/exec-approval-manager.test.ts b/packages/core/src/hub/exec-approval-manager.test.ts similarity index 100% rename from src/hub/exec-approval-manager.test.ts rename to packages/core/src/hub/exec-approval-manager.test.ts diff --git a/src/hub/exec-approval-manager.ts b/packages/core/src/hub/exec-approval-manager.ts similarity index 100% rename from src/hub/exec-approval-manager.ts rename to packages/core/src/hub/exec-approval-manager.ts diff --git a/src/hub/heartbeat-filter.test.ts b/packages/core/src/hub/heartbeat-filter.test.ts similarity index 100% rename from src/hub/heartbeat-filter.test.ts rename to packages/core/src/hub/heartbeat-filter.test.ts diff --git a/src/hub/heartbeat-filter.ts b/packages/core/src/hub/heartbeat-filter.ts similarity index 100% rename from src/hub/heartbeat-filter.ts rename to packages/core/src/hub/heartbeat-filter.ts diff --git a/src/hub/hub-identity.ts b/packages/core/src/hub/hub-identity.ts similarity index 92% rename from src/hub/hub-identity.ts rename to packages/core/src/hub/hub-identity.ts index 75067eb5..8c4986cb 100644 --- a/src/hub/hub-identity.ts +++ b/packages/core/src/hub/hub-identity.ts @@ -1,7 +1,7 @@ import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { v7 as uuidv7 } from "uuid"; -import { DATA_DIR } from "../shared/index.js"; +import { DATA_DIR } from "@multica/utils"; const HUB_ID_FILE = join(DATA_DIR, "hub-id"); diff --git a/src/hub/hub-singleton.ts b/packages/core/src/hub/hub-singleton.ts similarity index 100% rename from src/hub/hub-singleton.ts rename to packages/core/src/hub/hub-singleton.ts diff --git a/src/hub/hub.ts b/packages/core/src/hub/hub.ts similarity index 99% rename from src/hub/hub.ts rename to packages/core/src/hub/hub.ts index df3b3daa..f9bc7938 100644 --- a/src/hub/hub.ts +++ b/packages/core/src/hub/hub.ts @@ -10,7 +10,7 @@ import { type RequestPayload, type ResponseSuccessPayload, type ResponseErrorPayload, -} from "@multica/sdk"; +} from "../client/index.js"; import { AsyncAgent } from "../agent/async-agent.js"; import type { AgentOptions } from "../agent/types.js"; import { getHubId } from "./hub-identity.js"; diff --git a/src/hub/index.ts b/packages/core/src/hub/index.ts similarity index 100% rename from src/hub/index.ts rename to packages/core/src/hub/index.ts diff --git a/src/hub/message-aggregator.test.ts b/packages/core/src/hub/message-aggregator.test.ts similarity index 100% rename from src/hub/message-aggregator.test.ts rename to packages/core/src/hub/message-aggregator.test.ts diff --git a/src/hub/message-aggregator.ts b/packages/core/src/hub/message-aggregator.ts similarity index 100% rename from src/hub/message-aggregator.ts rename to packages/core/src/hub/message-aggregator.ts diff --git a/src/hub/rpc/dispatcher.ts b/packages/core/src/hub/rpc/dispatcher.ts similarity index 100% rename from src/hub/rpc/dispatcher.ts rename to packages/core/src/hub/rpc/dispatcher.ts diff --git a/src/hub/rpc/handlers/create-agent.ts b/packages/core/src/hub/rpc/handlers/create-agent.ts similarity index 100% rename from src/hub/rpc/handlers/create-agent.ts rename to packages/core/src/hub/rpc/handlers/create-agent.ts diff --git a/src/hub/rpc/handlers/delete-agent.ts b/packages/core/src/hub/rpc/handlers/delete-agent.ts similarity index 100% rename from src/hub/rpc/handlers/delete-agent.ts rename to packages/core/src/hub/rpc/handlers/delete-agent.ts diff --git a/src/hub/rpc/handlers/get-agent-messages.ts b/packages/core/src/hub/rpc/handlers/get-agent-messages.ts similarity index 100% rename from src/hub/rpc/handlers/get-agent-messages.ts rename to packages/core/src/hub/rpc/handlers/get-agent-messages.ts diff --git a/src/hub/rpc/handlers/get-hub-info.ts b/packages/core/src/hub/rpc/handlers/get-hub-info.ts similarity index 100% rename from src/hub/rpc/handlers/get-hub-info.ts rename to packages/core/src/hub/rpc/handlers/get-hub-info.ts diff --git a/src/hub/rpc/handlers/get-last-heartbeat.ts b/packages/core/src/hub/rpc/handlers/get-last-heartbeat.ts similarity index 100% rename from src/hub/rpc/handlers/get-last-heartbeat.ts rename to packages/core/src/hub/rpc/handlers/get-last-heartbeat.ts diff --git a/src/hub/rpc/handlers/list-agents.ts b/packages/core/src/hub/rpc/handlers/list-agents.ts similarity index 100% rename from src/hub/rpc/handlers/list-agents.ts rename to packages/core/src/hub/rpc/handlers/list-agents.ts diff --git a/src/hub/rpc/handlers/resolve-exec-approval.ts b/packages/core/src/hub/rpc/handlers/resolve-exec-approval.ts similarity index 100% rename from src/hub/rpc/handlers/resolve-exec-approval.ts rename to packages/core/src/hub/rpc/handlers/resolve-exec-approval.ts diff --git a/src/hub/rpc/handlers/set-heartbeats.ts b/packages/core/src/hub/rpc/handlers/set-heartbeats.ts similarity index 100% rename from src/hub/rpc/handlers/set-heartbeats.ts rename to packages/core/src/hub/rpc/handlers/set-heartbeats.ts diff --git a/src/hub/rpc/handlers/update-gateway.ts b/packages/core/src/hub/rpc/handlers/update-gateway.ts similarity index 100% rename from src/hub/rpc/handlers/update-gateway.ts rename to packages/core/src/hub/rpc/handlers/update-gateway.ts diff --git a/src/hub/rpc/handlers/verify.ts b/packages/core/src/hub/rpc/handlers/verify.ts similarity index 100% rename from src/hub/rpc/handlers/verify.ts rename to packages/core/src/hub/rpc/handlers/verify.ts diff --git a/src/hub/rpc/handlers/wake-heartbeat.ts b/packages/core/src/hub/rpc/handlers/wake-heartbeat.ts similarity index 100% rename from src/hub/rpc/handlers/wake-heartbeat.ts rename to packages/core/src/hub/rpc/handlers/wake-heartbeat.ts diff --git a/src/hub/rpc/index.ts b/packages/core/src/hub/rpc/index.ts similarity index 100% rename from src/hub/rpc/index.ts rename to packages/core/src/hub/rpc/index.ts diff --git a/src/hub/types.ts b/packages/core/src/hub/types.ts similarity index 100% rename from src/hub/types.ts rename to packages/core/src/hub/types.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 00000000..3d2baf8d --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,60 @@ +/** + * @multica/core - Core package + * + * Contains: Agent, Hub, Channels, Cron, Heartbeat, Media, Client + */ + +// Re-export from submodules +export * from './agent/index.js' +export * from './hub/index.js' +export * from './channels/index.js' +export * from './cron/index.js' +export * from './heartbeat/index.js' +export * from './media/index.js' + +// Client exports (selective to avoid conflicts with agent/events) +export { + GatewayClient, + type ConnectionState, + type RoutedMessage, + type SendErrorResponse, + HelloAction, + HelloResponseAction, + RequestAction, + ResponseAction, + StreamAction, + ExecApprovalRequestAction, + type HelloPayload, + type HelloResponsePayload, + type RequestPayload, + type ResponsePayload, + type ResponseSuccessPayload, + type ResponseErrorPayload, + type StreamPayload, + type ExecApprovalRequestPayload, + type ApprovalDecision, + isResponseSuccess, + isResponseError, + type AgentMessageItem, + DEFAULT_MESSAGES_LIMIT, + type GetAgentMessagesParams, + type GetAgentMessagesResult, + type GetHubInfoResult, + type ListAgentsResult, + type CreateAgentParams, + type CreateAgentResult, + type DeleteAgentParams, + type DeleteAgentResult, + type UpdateGatewayParams, + type UpdateGatewayResult, + type VerifyParams, + type VerifyResult, + type ResolveExecApprovalParams, + type ResolveExecApprovalResult, + type ContentBlock, + type TextContent, + type ThinkingContent, + type ToolCall, + type ImageContent, + extractThinkingFromEvent, +} from './client/index.js' diff --git a/src/media/describe-image.ts b/packages/core/src/media/describe-image.ts similarity index 100% rename from src/media/describe-image.ts rename to packages/core/src/media/describe-image.ts diff --git a/src/media/describe-video.ts b/packages/core/src/media/describe-video.ts similarity index 96% rename from src/media/describe-video.ts rename to packages/core/src/media/describe-video.ts index 4b67672d..b0d43c7e 100644 --- a/src/media/describe-video.ts +++ b/packages/core/src/media/describe-video.ts @@ -11,7 +11,7 @@ import { join } from "node:path"; import { execFile } from "node:child_process"; import { mkdir, unlink } from "node:fs/promises"; import { v7 as uuidv7 } from "uuid"; -import { MEDIA_CACHE_DIR } from "../shared/paths.js"; +import { MEDIA_CACHE_DIR } from "@multica/utils"; import { describeImage } from "./describe-image.js"; /** diff --git a/packages/core/src/media/index.ts b/packages/core/src/media/index.ts new file mode 100644 index 00000000..100930c4 --- /dev/null +++ b/packages/core/src/media/index.ts @@ -0,0 +1,8 @@ +/** + * Media processing utilities + * Audio transcription, image/video description + */ + +export * from './transcribe.js' +export * from './describe-image.js' +export * from './describe-video.js' diff --git a/src/media/transcribe.ts b/packages/core/src/media/transcribe.ts similarity index 100% rename from src/media/transcribe.ts rename to packages/core/src/media/transcribe.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..e4046071 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "verbatimModuleSyntax": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts new file mode 100644 index 00000000..18241096 --- /dev/null +++ b/packages/core/tsup.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: [ + 'src/index.ts', + 'src/agent/index.ts', + 'src/hub/index.ts', + 'src/channels/index.ts', + 'src/cron/index.ts', + 'src/heartbeat/index.ts', + 'src/media/index.ts', + 'src/client/index.ts', + ], + format: ['esm'], + dts: true, + clean: true, + sourcemap: true, + splitting: false, + external: [ + // Native/problematic deps + 'chokidar', + 'fsevents', + // Node built-ins + /^node:/, + ], +}) diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 8c449cb4..669d90c7 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -7,14 +7,18 @@ ".": "./src/index.ts", "./*": "./src/*.ts" }, + "scripts": { + "typecheck": "tsc --noEmit", + "lint": "eslint src" + }, "dependencies": { "@multica/sdk": "workspace:*", "react": "catalog:", - "uuid": "^13.0.0" + "uuid": "catalog:" }, "devDependencies": { "@types/react": "catalog:", - "@types/uuid": "^11.0.0", + "@types/uuid": "catalog:", "typescript": "catalog:" } } diff --git a/packages/hooks/tsconfig.json b/packages/hooks/tsconfig.json index a3034f69..60e6789c 100644 --- a/packages/hooks/tsconfig.json +++ b/packages/hooks/tsconfig.json @@ -1,18 +1,8 @@ { + "extends": "../../tsconfig.base.json", "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "declaration": true, - "declarationMap": true, - "sourceMap": true, "outDir": "./dist", "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "verbatimModuleSyntax": true, "jsx": "react-jsx" }, "include": ["src/**/*"], diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 3eef6b24..19511d33 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -13,13 +13,13 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "socket.io-client": "^4.8.3", - "uuid": "^13.0.0" + "socket.io-client": "catalog:", + "uuid": "catalog:" }, "devDependencies": { - "@mariozechner/pi-agent-core": "^0.50.3", - "@mariozechner/pi-ai": "^0.50.3", - "@types/uuid": "^11.0.0", - "typescript": "^5.9.3" + "@mariozechner/pi-agent-core": "catalog:", + "@mariozechner/pi-ai": "catalog:", + "@types/uuid": "catalog:", + "typescript": "catalog:" } } diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index e7b4fafb..b813d428 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -1,18 +1,8 @@ { + "extends": "../../tsconfig.base.json", "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "declaration": true, - "declarationMap": true, - "sourceMap": true, "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "verbatimModuleSyntax": true + "rootDir": "./src" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/packages/store/package.json b/packages/store/package.json index 3a1c0c1a..fd39f940 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -7,6 +7,10 @@ ".": "./src/index.ts", "./*": "./src/*.ts" }, + "scripts": { + "typecheck": "tsc --noEmit", + "lint": "eslint src" + }, "dependencies": { "@multica/sdk": "workspace:*" }, diff --git a/packages/store/tsconfig.json b/packages/store/tsconfig.json index 2fa65e5c..60e6789c 100644 --- a/packages/store/tsconfig.json +++ b/packages/store/tsconfig.json @@ -1,19 +1,10 @@ { + "extends": "../../tsconfig.base.json", "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "declaration": true, "outDir": "./dist", "rootDir": "./src", - "baseUrl": ".", - "paths": { - "@multica/store/*": ["./src/*"] - } + "jsx": "react-jsx" }, - "include": ["src"] + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] } diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 00000000..7e561758 --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,23 @@ +{ + "name": "@multica/types", + "version": "0.0.1", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "tsup": "^8.0.0", + "typescript": "catalog:" + } +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts new file mode 100644 index 00000000..b311a805 --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1,192 @@ +/** + * @multica/types - Shared type definitions + * Zero dependencies, foundation for all other packages + */ + +// ============================================================================ +// Base Message Types +// ============================================================================ + +export interface Message { + id: string + payload: unknown + timestamp: number +} + +// ============================================================================ +// Connection State +// ============================================================================ + +export type ConnectionState = + | 'disconnected' + | 'connecting' + | 'connected' + | 'reconnecting' + +// ============================================================================ +// Agent Types +// ============================================================================ + +export type AgentStatus = 'idle' | 'running' | 'closed' + +export interface AgentInfo { + agentId: string + status: AgentStatus +} + +// ============================================================================ +// Provider Types +// ============================================================================ + +export type AuthMethod = 'api-key' | 'oauth' + +export interface ProviderMeta { + id: string + name: string + authMethod: AuthMethod + defaultModel: string + models: string[] + loginUrl?: string + loginCommand?: string +} + +// ============================================================================ +// Tool Types +// ============================================================================ + +export interface ToolInfo { + name: string + group: string + enabled: boolean +} + +// ============================================================================ +// Skill Types +// ============================================================================ + +export type SkillSource = 'bundled' | 'global' | 'profile' + +export interface SkillInfo { + id: string + name: string + description: string + version: string + enabled: boolean + source: SkillSource + triggers: string[] +} + +// ============================================================================ +// Device Types +// ============================================================================ + +export interface DeviceInfo { + deviceId: string + userAgent?: string + platform?: string + language?: string + createdAt: number + lastSeenAt: number +} + +// ============================================================================ +// Hub Types +// ============================================================================ + +export interface HubStatus { + hubId: string + status: string + agentCount: number + gatewayConnected: boolean + gatewayUrl?: string + defaultAgent?: AgentInfo | null +} + +// ============================================================================ +// Channel Types +// ============================================================================ + +export type ChannelType = 'telegram' | 'discord' | 'slack' + +export interface ChannelAccountState { + channelId: string + accountId: string + status: 'running' | 'stopped' | 'error' + error?: string +} + +// ============================================================================ +// Cron Types +// ============================================================================ + +export type CronJobStatus = 'pending' | 'running' | 'completed' | 'failed' + +export interface CronJobInfo { + id: string + name: string + description: string + enabled: boolean + schedule: string + nextRunAt: string | null + lastStatus: CronJobStatus | null + lastRunAt: string | null +} + +// ============================================================================ +// RPC Types +// ============================================================================ + +export interface RpcRequest { + method: string + params?: unknown + id?: string +} + +export interface RpcResponse { + result?: T + error?: { + code: number + message: string + data?: unknown + } + id?: string +} + +// ============================================================================ +// Event Types +// ============================================================================ + +export type StreamEventType = + | 'message_start' + | 'message_update' + | 'message_end' + | 'tool_execution_start' + | 'tool_execution_end' + | 'compaction_start' + | 'compaction_end' + | 'agent_error' + +export interface StreamEvent { + type: StreamEventType + agentId: string + streamId?: string + data?: unknown +} + +// ============================================================================ +// Approval Types +// ============================================================================ + +export type RiskLevel = 'safe' | 'needs-review' | 'dangerous' + +export type ApprovalDecision = 'allow-once' | 'allow-always' | 'deny' + +export interface ExecApprovalRequest { + approvalId: string + agentId: string + command: string + cwd?: string + riskLevel: RiskLevel + riskReasons: string[] + expiresAtMs: number +} diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json new file mode 100644 index 00000000..1ebe2f40 --- /dev/null +++ b/packages/types/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "verbatimModuleSyntax": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/types/tsup.config.ts b/packages/types/tsup.config.ts new file mode 100644 index 00000000..35956cab --- /dev/null +++ b/packages/types/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + clean: true, + sourcemap: true, +}) diff --git a/packages/ui/package.json b/packages/ui/package.json index 3bad8caa..a2c2c59f 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -4,6 +4,10 @@ "private": true, "type": "module", "sideEffects": ["**/*.css"], + "scripts": { + "typecheck": "tsc --noEmit", + "lint": "eslint src" + }, "exports": { "./globals.css": "./src/styles/globals.css", "./postcss.config": "./postcss.config.mjs", @@ -16,15 +20,15 @@ }, "dependencies": { "@base-ui/react": "^1.1.0", - "@hugeicons/core-free-icons": "^3.1.1", - "@hugeicons/react": "^1.1.4", + "@hugeicons/core-free-icons": "catalog:", + "@hugeicons/react": "catalog:", "@multica/store": "workspace:*", "@tiptap/extension-placeholder": "^3.19.0", "@tiptap/pm": "^3.19.0", "@tiptap/react": "^3.19.0", "@tiptap/starter-kit": "^3.19.0", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", + "class-variance-authority": "catalog:", + "clsx": "catalog:", "linkify-it": "^5.0.0", "next-themes": "^0.4.6", "qr-scanner": "^1.4.2", @@ -36,12 +40,12 @@ "shadcn": "^3.7.0", "shiki": "^3.21.0", "sonner": "^2.0.7", - "tailwind-merge": "^3.4.0", - "tailwindcss": "^4", + "tailwind-merge": "catalog:", + "tailwindcss": "catalog:", "tw-animate-css": "^1.4.0" }, "devDependencies": { - "@tailwindcss/postcss": "^4", + "@tailwindcss/postcss": "catalog:", "@types/linkify-it": "^5.0.0", "@types/react": "catalog:", "@types/react-dom": "catalog:", diff --git a/packages/ui/src/components/component-example.tsx b/packages/ui/src/components/component-example.tsx deleted file mode 100644 index 7c0be8e1..00000000 --- a/packages/ui/src/components/component-example.tsx +++ /dev/null @@ -1,468 +0,0 @@ -import * as React from "react" - -import { - Example, - ExampleWrapper, -} from "@multica/ui/components/example" -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogMedia, - AlertDialogTitle, - AlertDialogTrigger, -} from "@multica/ui/components/ui/alert-dialog" -import { Badge } from "@multica/ui/components/ui/badge" -import { Button } from "@multica/ui/components/ui/button" -import { - Card, - CardAction, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@multica/ui/components/ui/card" -import { - Combobox, - ComboboxContent, - ComboboxEmpty, - ComboboxInput, - ComboboxItem, - ComboboxList, -} from "@multica/ui/components/ui/combobox" -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuPortal, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuTrigger, -} from "@multica/ui/components/ui/dropdown-menu" -import { Field, FieldGroup, FieldLabel } from "@multica/ui/components/ui/field" -import { Input } from "@multica/ui/components/ui/input" -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@multica/ui/components/ui/select" -import { Textarea } from "@multica/ui/components/ui/textarea" -import { HugeiconsIcon } from "@hugeicons/react" -import { PlusSignIcon, BluetoothIcon, MoreVerticalCircle01Icon, FileIcon, FolderIcon, FolderOpenIcon, CodeIcon, MoreHorizontalCircle01Icon, SearchIcon, FloppyDiskIcon, DownloadIcon, EyeIcon, LayoutIcon, PaintBoardIcon, SunIcon, MoonIcon, ComputerIcon, UserIcon, CreditCardIcon, SettingsIcon, KeyboardIcon, LanguageCircleIcon, NotificationIcon, MailIcon, ShieldIcon, HelpCircleIcon, File01Icon, LogoutIcon } from "@hugeicons/core-free-icons" -export function ComponentExample() { - return ( - - - - - ) -} - -function CardExample() { - return ( - - -
- Photo by mymind on Unsplash - - Observability Plus is replacing Monitoring - - Switch to the improved way to explore your data, with natural - language. Monitoring will no longer be available on the Pro plan in - November, 2025 - - - - - }> - - Show Dialog - - - - - - - Allow accessory to connect? - - Do you want to allow the USB accessory to connect to this - device? - - - - Don't allow - Allow - - - - - Warning - - - - - ) -} - -const frameworks = [ - "Next.js", - "SvelteKit", - "Nuxt.js", - "Remix", - "Astro", -] as const - -const roleItems = [ - { label: "Developer", value: "developer" }, - { label: "Designer", value: "designer" }, - { label: "Manager", value: "manager" }, - { label: "Other", value: "other" }, -] - -function FormExample() { - const [notifications, setNotifications] = React.useState({ - email: true, - sms: false, - push: true, - }) - const [theme, setTheme] = React.useState("light") - - return ( - - - - User Information - Please fill in your details below - - - } - > - - More options - - - - File - - - New File - ⌘N - - - - New Folder - ⇧⌘N - - - - - Open Recent - - - - - Recent Projects - - - Project Alpha - - - - Project Beta - - - - - More Projects - - - - - - Project Gamma - - - - Project Delta - - - - - - - - - - Browse... - - - - - - - - - Save - ⌘S - - - - Export - ⇧⌘E - - - - - View - - setNotifications({ - ...notifications, - email: checked === true, - }) - } - > - - Show Sidebar - - - setNotifications({ - ...notifications, - sms: checked === true, - }) - } - > - - Show Status Bar - - - - - Theme - - - - - Appearance - - - - Light - - - - Dark - - - - System - - - - - - - - - - Account - - - Profile - ⇧⌘P - - - - Billing - - - - - Settings - - - - - Preferences - - - Keyboard Shortcuts - - - - Language - - - - - Notifications - - - - - - Notification Types - - - setNotifications({ - ...notifications, - push: checked === true, - }) - } - > - - Push Notifications - - - setNotifications({ - ...notifications, - email: checked === true, - }) - } - > - - Email Notifications - - - - - - - - - - - Privacy & Security - - - - - - - - - - - Help & Support - - - - Documentation - - - - - - - Sign Out - ⇧⌘Q - - - - - - - -
- -
- - Name - - - - Role - - -
- - - Framework - - - - - No frameworks found. - - {(item) => ( - - {item} - - )} - - - - - - Comments -