A high-level picture of how NAISYS fits together. See each package's README for feature-level detail.
Each package ships an npm binary and a programmatic export. The exports are what let one package boot another inside the same process.
| Package | Binary | Entry point | Programmatic export |
|---|---|---|---|
naisys |
naisys |
apps/naisys/src/naisys.ts |
— (top-level process) |
@naisys/hub |
naisys-hub |
apps/hub/src/naisysHub.ts |
startHub() |
@naisys/supervisor |
naisys-supervisor |
apps/supervisor/server/src/supervisorServer.ts |
supervisorPlugin (Fastify plugin) |
@naisys/erp |
naisys-erp |
apps/erp/server/src/erpServer.ts |
erpPlugin (Fastify plugin) |
naisys— the runner. Proxies a real shell to an LLM, providesns-*commands (mail, web, images, desktop control), enforces cost and context limits.@naisys/hub— the server. Fastify + Socket.IO. Owns persistence (mail, context logs, cost, variables) so runners stay stateless.@naisys/supervisor— the web UI. Monitors runners and agents, configures models and users, manages permissions.@naisys/erp— optional task system for agents. HATEOAS-driven REST designed for LLMs to self-discover.
The flags --integrated-hub, --supervisor, and --erp chain one package into the next, ending with every package running in a single Node process on a single port:
- The runner parses flags (
commander, innaisys.ts). If--integrated-hubis set, it dynamically imports@naisys/huband callsstartHub(...). Theawait import(moduleName)is deliberate — it avoids a compile-time dependency so the packages can build in parallel, and skips pulling the hub module tree into memory for plain Local runs. - The hub is a Fastify server. If
--supervisorwas passed through, the hub dynamically imports@naisys/supervisorand registerssupervisorPlugin— the supervisor serves its HTTP routes as a Fastify plugin on the hub's port. - If
--erpwas also set, the supervisor registerserpPluginthe same way. ERP is a transitive dependency of supervisor, not of the runner.
naisys --integrated-hub --supervisor --erp
└─ await import("@naisys/hub") → startHub()
└─ await import("@naisys/supervisor") → fastify.register(supervisorPlugin)
└─ await import("@naisys/erp") → fastify.register(erpPlugin)
Everything runs on one port; the runner then connects back to the in-process hub at http://localhost:<port>/hub as if it were remote.
┌──────────────┐
│ Supervisor │ (browser)
└──────┬───────┘
│ WebSocket + REST
▼
┌────────┐ ┌───────┐ ┌────────┐
│ Runner │──▶│ Hub │◀──│ Runner │ (other machines)
└────────┘ └───┬───┘ └────────┘
│
▼
Prisma DB
The hub is the only piece that holds state. Runners and supervisors connect in as clients. Any runner can be restarted or moved between machines without losing mail, logs, or cost history. See doc 005 for the multi-machine model and doc 010 for the auth/security model.
A single runner process can host many agents concurrently.
apps/naisys/src/agent/agentManager.ts owns a runningAgents: AgentRuntime[] list and is the single entry point for start/stop/peek. Only one agent is "active" in the TTY at a time. Inactive agents still run in the background — each has its own OutputService that buffers console writes (capped to 10 lines). setActiveConsoleAgent() swaps the active agent, flushes the buffered lines, and wires stdin to the new one.
Each agent has its own AgentRuntime — a bag of services scoped to that agent — built by createAgentRuntime() in apps/naisys/src/agent/agentRuntime.ts. Everything per-agent hangs off this object:
llmService,costTracker,tools,systemMessage— LLM interactionshellWrapper,workspaces— shell and file contextmailService,chatService,mailQueryService— inter-agent messagingcommandLoop,commandHandler,commandRegistry,commandProtection— main loop andns-*dispatchdesktopService,computerService,lookService,listenService,genimg,subagentService— feature services (see doc 013 for the desktop/computer-use design)logService,output— logging and buffered console
Services are built by factory functions, not classes with constructors. A typical signature:
export function createLLMService(
{ globalConfig }: GlobalConfig,
{ agentConfig }: AgentConfig,
costTracker: CostTracker,
tools: CommandTools,
modelService: ModelService,
computerService?: ComputerService,
) { ... }Two techniques keep the graph acyclic:
- Closures over getters. Config is passed in as a function (
globalConfig()/agentConfig()), not a value. Services read the current value on every call, so config can be reloaded without rebuilding services. - Interface-based late binding. Services that need the agent manager (mail, subagent) depend on the
IAgentManagerinterface atapps/naisys/src/agent/agentManagerInterface.ts, not the concrete class. The manager is the last thing constructed — it closes over the already-built services — and those services call back into it through the interface.
Top-level wiring lives in apps/naisys/src/naisys.ts: services are constructed in dependency order and passed forward, with interfaces substituted wherever a cycle would otherwise form.
- Integrated —
naisys --integrated-hub --supervisor --erpruns every package in a single Node process. This is the "Server" mode in the README. - Distributed — a standalone hub with runners on multiple machines, each connecting via
--hub=https://.... Agents can run on any host or be pinned to specific ones — useful when a host has unique resources (GPU, desktop, Windows).
- Monorepo under
apps/, Turbo-repo builds - Prisma ORM across the stack; snake_case DB, camelCase code
- Zod schemas shared between client and server; OpenAPI derived from Zod
- All packages publish under the
@naisysscope on a single version (surfaced on the supervisor admin page) - Node >= 22 required