diff --git a/apps/web/app/components/chat.tsx b/apps/web/app/components/chat.tsx index 0d300a2e..943274d9 100644 --- a/apps/web/app/components/chat.tsx +++ b/apps/web/app/components/chat.tsx @@ -11,7 +11,7 @@ import { UserIcon, Copy01Icon, CheckmarkCircle02Icon } from "@hugeicons/core-fre import { toast } from "@multica/ui/components/ui/sonner"; import { useMessages } from "../hooks/use-messages"; import { useGateway } from "../hooks/use-gateway"; -import { useHubStore } from "../hooks/use-hub-store"; +import { useHubStore } from "@multica/store"; import { useDeviceId } from "../hooks/use-device-id"; import { useScrollFade } from "../hooks/use-scroll-fade"; import { cn } from "@multica/ui/lib/utils"; diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 579809da..89d78ef4 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -8,7 +8,7 @@ import { import { AppSidebar } from "@multica/ui/components/app-sidebar"; import { ThemeProvider } from "@multica/ui/components/theme-provider"; import { Toaster } from "@multica/ui/components/ui/sonner"; -import { HubSidebar } from "./components/hub-sidebar"; +import { HubSidebar } from "@multica/ui/components/hub-sidebar"; const inter = Inter({ subsets: ["latin"], variable: "--font-sans" }); diff --git a/packages/fetch/package.json b/packages/fetch/package.json new file mode 100644 index 00000000..d5bd6971 --- /dev/null +++ b/packages/fetch/package.json @@ -0,0 +1,13 @@ +{ + "name": "@multica/fetch", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./*": "./src/*.ts" + }, + "devDependencies": { + "typescript": "catalog:" + } +} diff --git a/packages/fetch/src/config.ts b/packages/fetch/src/config.ts new file mode 100644 index 00000000..ff2abde9 --- /dev/null +++ b/packages/fetch/src/config.ts @@ -0,0 +1,15 @@ +let consoleUrl = "http://localhost:4000" +let gatewayUrl = "http://localhost:3000" + +export function setConfig(config: { consoleUrl?: string; gatewayUrl?: string }) { + if (config.consoleUrl) consoleUrl = config.consoleUrl + if (config.gatewayUrl) gatewayUrl = config.gatewayUrl +} + +export function getConsoleUrl(): string { + return consoleUrl +} + +export function getGatewayUrl(): string { + return gatewayUrl +} diff --git a/packages/fetch/src/http-client.ts b/packages/fetch/src/http-client.ts new file mode 100644 index 00000000..e9564d09 --- /dev/null +++ b/packages/fetch/src/http-client.ts @@ -0,0 +1,28 @@ +import { getConsoleUrl } from "./config" + +export class HttpError extends Error { + constructor( + public status: number, + public statusText: string, + ) { + super(`HTTP ${status}: ${statusText}`) + } +} + +async function request(method: string, path: string, body?: unknown): Promise { + const res = await fetch(`${getConsoleUrl()}${path}`, { + method, + headers: body ? { "Content-Type": "application/json" } : undefined, + body: body ? JSON.stringify(body) : undefined, + }) + if (!res.ok) throw new HttpError(res.status, res.statusText) + return res.json() +} + +/** Console REST API */ +export const consoleApi = { + get: (path: string) => request("GET", path), + post: (path: string, body?: unknown) => request("POST", path, body), + put: (path: string, body: unknown) => request("PUT", path, body), + delete: (path: string) => request("DELETE", path), +} diff --git a/packages/fetch/src/index.ts b/packages/fetch/src/index.ts new file mode 100644 index 00000000..8500fa04 --- /dev/null +++ b/packages/fetch/src/index.ts @@ -0,0 +1,2 @@ +export { setConfig, getConsoleUrl, getGatewayUrl } from "./config" +export { consoleApi, HttpError } from "./http-client" diff --git a/packages/fetch/tsconfig.json b/packages/fetch/tsconfig.json new file mode 100644 index 00000000..c1103210 --- /dev/null +++ b/packages/fetch/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"] +} diff --git a/packages/store/package.json b/packages/store/package.json index 2837c0c7..c5b323df 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -8,9 +8,12 @@ "./*": "./src/*.ts" }, "dependencies": { + "@multica/fetch": "workspace:*", + "react": "catalog:", "zustand": "catalog:" }, "devDependencies": { + "@types/react": "catalog:", "typescript": "catalog:" } } diff --git a/packages/store/src/counter.ts b/packages/store/src/counter.ts deleted file mode 100644 index 30ffc733..00000000 --- a/packages/store/src/counter.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { create } from 'zustand' - -interface CounterState { - count: number - increment: () => void - decrement: () => void - reset: () => void -} - -export const useCounterStore = create((set) => ({ - count: 0, - increment: () => set((state) => ({ count: state.count + 1 })), - decrement: () => set((state) => ({ count: state.count - 1 })), - reset: () => set({ count: 0 }), -})) diff --git a/apps/web/app/hooks/use-hub-init.ts b/packages/store/src/hub-init.ts similarity index 91% rename from apps/web/app/hooks/use-hub-init.ts rename to packages/store/src/hub-init.ts index 9e988ebe..2c070770 100644 --- a/apps/web/app/hooks/use-hub-init.ts +++ b/packages/store/src/hub-init.ts @@ -1,7 +1,7 @@ "use client" import { useEffect } from "react" -import { useHubStore } from "./use-hub-store" +import { useHubStore } from "./hub" export function useHubInit() { const fetchHub = useHubStore((s) => s.fetchHub) diff --git a/apps/web/app/hooks/use-hub-store.ts b/packages/store/src/hub.ts similarity index 57% rename from apps/web/app/hooks/use-hub-store.ts rename to packages/store/src/hub.ts index 010a1418..db501628 100644 --- a/apps/web/app/hooks/use-hub-store.ts +++ b/packages/store/src/hub.ts @@ -1,33 +1,37 @@ import { create } from "zustand" -import { CONSOLE_URL } from "../lib/config" +import { consoleApi } from "@multica/fetch" -interface HubInfo { +export interface HubInfo { hubId: string url: string connectionState: string agentCount: number } -interface Agent { +export interface Agent { id: string closed: boolean } -type HubStatus = "idle" | "loading" | "connected" | "error" +export type HubStatus = "idle" | "loading" | "connected" | "error" -interface HubStore { +interface HubState { status: HubStatus hub: HubInfo | null agents: Agent[] activeAgentId: string | null +} +interface HubActions { setActiveAgentId: (id: string | null) => void fetchHub: () => Promise fetchAgents: () => Promise - createAgent: () => Promise + createAgent: (options?: Record) => Promise deleteAgent: (id: string) => Promise } +export type HubStore = HubState & HubActions + export const useHubStore = create()((set, get) => ({ status: "idle", hub: null, @@ -39,9 +43,7 @@ export const useHubStore = create()((set, get) => ({ fetchHub: async () => { set({ status: "loading" }) try { - const res = await fetch(`${CONSOLE_URL}/api/hub`) - if (!res.ok) throw new Error(res.statusText) - const data: HubInfo = await res.json() + const data = await consoleApi.get("/api/hub") set({ hub: data, status: data.connectionState === "registered" ? "connected" : "error", @@ -53,23 +55,24 @@ export const useHubStore = create()((set, get) => ({ fetchAgents: async () => { try { - const res = await fetch(`${CONSOLE_URL}/api/agents`) - if (res.ok) set({ agents: await res.json() }) + const data = await consoleApi.get("/api/agents") + set({ agents: data }) } catch { /* silent */ } }, - createAgent: async () => { - const res = await fetch(`${CONSOLE_URL}/api/agents`, { method: "POST" }) - await get().fetchAgents() - if (res.ok) { - const data = await res.json() + createAgent: async (options?) => { + try { + const data = await consoleApi.post<{ id: string }>("/api/agents", options) + await get().fetchAgents() if (data.id) set({ activeAgentId: data.id }) - } + } catch { /* silent */ } }, deleteAgent: async (id) => { if (get().activeAgentId === id) set({ activeAgentId: null }) - await fetch(`${CONSOLE_URL}/api/agents/${id}`, { method: "DELETE" }) - await get().fetchAgents() + try { + await consoleApi.delete("/api/agents/" + id) + await get().fetchAgents() + } catch { /* silent */ } }, })) diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 778a5959..052edefc 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -1 +1,3 @@ -export { useCounterStore } from './counter' +export { useHubStore } from "./hub" +export type { HubInfo, Agent, HubStatus, HubStore } from "./hub" +export { useHubInit } from "./hub-init" diff --git a/packages/ui/src/components/component-example.tsx b/packages/ui/src/components/component-example.tsx index 7d6fe331..7c0be8e1 100644 --- a/packages/ui/src/components/component-example.tsx +++ b/packages/ui/src/components/component-example.tsx @@ -65,52 +65,15 @@ import { 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" -import { useCounterStore } from "@multica/store/counter" - export function ComponentExample() { return ( - ) } -function CounterExample() { - const { count, increment, decrement, reset } = useCounterStore() - - return ( - - - - Shared Counter - - This counter uses Zustand from @multica/store, shared across web and desktop. - - - -
- - {count} - -
-
- - - Count: {count} - -
-
- ) -} - function CardExample() { return ( diff --git a/apps/web/app/components/hub-sidebar.tsx b/packages/ui/src/components/hub-sidebar.tsx similarity index 95% rename from apps/web/app/components/hub-sidebar.tsx rename to packages/ui/src/components/hub-sidebar.tsx index 6a958ba3..99e34f4f 100644 --- a/apps/web/app/components/hub-sidebar.tsx +++ b/packages/ui/src/components/hub-sidebar.tsx @@ -11,8 +11,8 @@ import { import { Button } from "@multica/ui/components/ui/button" import { HugeiconsIcon } from "@hugeicons/react" import { PlusSignIcon, Delete02Icon } from "@hugeicons/core-free-icons" -import { useHubStore } from "../hooks/use-hub-store" -import { useHubInit } from "../hooks/use-hub-init" +import { useHubStore } from "@multica/store" +import { useHubInit } from "@multica/store" const STATUS_DOT: Record = { connected: "bg-green-500/60", @@ -67,7 +67,7 @@ export function HubSidebar() { {status === "connected" && ( Agents - + createAgent()} title="Create agent"> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 450459fa..87608cd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,13 +34,13 @@ importers: dependencies: '@mariozechner/pi-agent-core': specifier: ^0.50.3 - version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6) + version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76) '@mariozechner/pi-ai': specifier: ^0.50.3 - version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6) + version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76) '@mariozechner/pi-coding-agent': specifier: ^0.50.3 - version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6) + version: 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76) '@mozilla/readability': specifier: ^0.6.0 version: 0.6.0 @@ -255,6 +255,12 @@ importers: specifier: 'catalog:' version: 5.9.3 + packages/fetch: + devDependencies: + typescript: + specifier: 'catalog:' + version: 5.9.3 + packages/sdk: dependencies: socket.io-client: @@ -273,10 +279,19 @@ importers: packages/store: dependencies: + '@multica/fetch': + specifier: workspace:* + version: link:../fetch + react: + specifier: 'catalog:' + version: 19.2.3 zustand: specifier: 'catalog:' version: 5.0.10(@types/react@19.2.10)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) devDependencies: + '@types/react': + specifier: 'catalog:' + version: 19.2.10 typescript: specifier: 'catalog:' version: 5.9.3 @@ -6400,11 +6415,11 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.2 - '@anthropic-ai/sdk@0.71.2(zod@4.3.6)': + '@anthropic-ai/sdk@0.71.2(zod@3.25.76)': dependencies: json-schema-to-ts: 3.1.1 optionalDependencies: - zod: 4.3.6 + zod: 3.25.76 '@aws-crypto/crc32@5.2.0': dependencies: @@ -7427,12 +7442,12 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@google/genai@1.34.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))': + '@google/genai@1.34.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))': dependencies: google-auth-library: 10.5.0 ws: 8.18.3 optionalDependencies: - '@modelcontextprotocol/sdk': 1.25.3(hono@4.11.7)(zod@4.3.6) + '@modelcontextprotocol/sdk': 1.25.3(hono@4.11.7)(zod@3.25.76) transitivePeerDependencies: - bufferutil - supports-color @@ -7687,9 +7702,9 @@ snapshots: std-env: 3.10.0 yoctocolors: 2.1.2 - '@mariozechner/pi-agent-core@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)': + '@mariozechner/pi-agent-core@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)': dependencies: - '@mariozechner/pi-ai': 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6) + '@mariozechner/pi-ai': 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76) '@mariozechner/pi-tui': 0.50.3 transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -7700,21 +7715,21 @@ snapshots: - ws - zod - '@mariozechner/pi-ai@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)': + '@mariozechner/pi-ai@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)': dependencies: - '@anthropic-ai/sdk': 0.71.2(zod@4.3.6) + '@anthropic-ai/sdk': 0.71.2(zod@3.25.76) '@aws-sdk/client-bedrock-runtime': 3.978.0 - '@google/genai': 1.34.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6)) + '@google/genai': 1.34.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76)) '@mistralai/mistralai': 1.10.0 '@sinclair/typebox': 0.34.48 ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) chalk: 5.6.2 - openai: 6.10.0(ws@8.18.3)(zod@4.3.6) + openai: 6.10.0(ws@8.18.3)(zod@3.25.76) partial-json: 0.1.7 proxy-agent: 6.5.0 undici: 7.19.2 - zod-to-json-schema: 3.25.1(zod@4.3.6) + zod-to-json-schema: 3.25.1(zod@3.25.76) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - aws-crt @@ -7724,12 +7739,12 @@ snapshots: - ws - zod - '@mariozechner/pi-coding-agent@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6)': + '@mariozechner/pi-coding-agent@0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76)': dependencies: '@mariozechner/clipboard': 0.3.0 '@mariozechner/jiti': 2.6.5 - '@mariozechner/pi-agent-core': 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6) - '@mariozechner/pi-ai': 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6))(ws@8.18.3)(zod@4.3.6) + '@mariozechner/pi-agent-core': 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76) + '@mariozechner/pi-ai': 0.50.3(@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76))(ws@8.18.3)(zod@3.25.76) '@mariozechner/pi-tui': 0.50.3 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 @@ -7787,29 +7802,6 @@ snapshots: - hono - supports-color - '@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@4.3.6)': - dependencies: - '@hono/node-server': 1.19.9(hono@4.11.7) - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - content-type: 1.0.5 - cors: 2.8.6 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.6 - express: 5.2.1 - express-rate-limit: 7.5.1(express@5.2.1) - jose: 6.1.3 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 4.3.6 - zod-to-json-schema: 3.25.1(zod@4.3.6) - transitivePeerDependencies: - - hono - - supports-color - optional: true - '@mozilla/readability@0.6.0': {} '@mswjs/interceptors@0.40.0': @@ -10013,7 +10005,7 @@ snapshots: eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) @@ -10046,7 +10038,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -10061,7 +10053,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -12049,10 +12041,10 @@ snapshots: powershell-utils: 0.1.0 wsl-utils: 0.3.1 - openai@6.10.0(ws@8.18.3)(zod@4.3.6): + openai@6.10.0(ws@8.18.3)(zod@3.25.76): optionalDependencies: ws: 8.18.3 - zod: 4.3.6 + zod: 3.25.76 optionator@0.9.4: dependencies: @@ -13668,10 +13660,6 @@ snapshots: dependencies: zod: 3.25.76 - zod-to-json-schema@3.25.1(zod@4.3.6): - dependencies: - zod: 4.3.6 - zod-validation-error@4.0.2(zod@4.3.6): dependencies: zod: 4.3.6