naisys
Version:
NAISYS - Autonomous AI agent runner with built-in context management and cost tracking
174 lines • 8.2 kB
JavaScript
import { sleep } from "@naisys/common";
import { createDualLogger, ensureDotEnv, expandNaisysFolder, runSetupWizard, } from "@naisys/common-node";
import { HubEvents } from "@naisys/hub-protocol";
import { program } from "commander";
import path from "path";
import { AgentManager } from "./agent/agentManager.js";
import { createUserService } from "./agent/userService.js";
import { createDesktopClaimService } from "./computer-use/desktopClaimService.js";
import { createGlobalConfig } from "./globalConfig.js";
import { createHubClient } from "./hub/hubClient.js";
import { createHubClientConfig } from "./hub/hubClientConfig.js";
import { createHubCostBuffer } from "./hub/hubCostBuffer.js";
import { createHubLogBuffer } from "./hub/hubLogBuffer.js";
import { ensureAgentConfig, getNaisysWizardConfig, printCodexSubscriptionSetupInstructions, } from "./naisysSetup.js";
import { createModelService } from "./services/agent/modelService.js";
import { createHeartbeatService } from "./services/hub/heartbeatService.js";
import { createHostService } from "./services/hub/hostService.js";
import { createUpdateService } from "./services/runtime/updateService.js";
import { createPromptNotificationService } from "./utils/output/promptNotificationService.js";
// dotenv.config() runs in the dispatcher (./naisys.ts) before the wrapper
// guard, so .env-set NAISYS_* vars take effect there. The spawned wrapper
// child inherits the resolved process.env — no need to reload here.
const isHubClient = process.argv.some((a) => a === "--hub" || a.startsWith("--hub="));
const isIntegratedHub = process.argv.includes("--integrated-hub");
let codexSubscriptionSelected = false;
const wizardConfig = getNaisysWizardConfig(isHubClient, isIntegratedHub, {
onCodexSubscriptionSelected: () => {
codexSubscriptionSelected = true;
},
});
const exampleUrl = new URL(isHubClient ? "../.env.hub-client.example" : "../.env.example", import.meta.url);
let wizardRan = false;
if (process.argv.includes("--setup")) {
wizardRan = await runSetupWizard(path.resolve(".env"), exampleUrl, wizardConfig);
expandNaisysFolder();
}
wizardRan = (await ensureDotEnv(exampleUrl, wizardConfig)) || wizardRan;
expandNaisysFolder();
program
.argument("[agent-path]", "Path to agent configuration file (optional, defaults to admin agent)")
.option("--hub <url>", "Connect to a Hub server at the given URL (e.g. --hub=http://localhost:3300)")
.option("--integrated-hub", "Start a hub in the same process space as this NAISYS instance (saves memory)")
.option("--supervisor", "Start integrated Supervisor website (integrated hub required)")
.option("--erp", "Start ERP web app (requires --supervisor)")
.option("--no-auto-update", "Disable automatic version updates")
.option("--setup", "Run interactive setup wizard")
.parse();
if (!isHubClient) {
await ensureAgentConfig(program.args[0], {
useCodexSubscription: codexSubscriptionSelected,
});
if (codexSubscriptionSelected) {
printCodexSubscriptionSetupInstructions();
}
}
const agentPath = program.args[0] || ".";
let hubUrl = program.opts().hub;
const integratedHub = Boolean(program.opts().integratedHub);
let supervisorUrl;
let integratedHubShutdown;
if (integratedHub) {
// Don't import the hub module tree unless needed, sharing the same process space is to save memory on small servers
// Use variable to avoid compile-time type dependency on @naisys/hub (allows parallel builds)
const hubModule = "@naisys/hub";
const { startHub } = (await import(hubModule));
const plugins = [];
if (program.opts().erp)
plugins.push("erp");
const hubResult = await startHub("hosted", program.opts().supervisor, plugins, agentPath, wizardRan);
integratedHubShutdown = hubResult.shutdown;
hubUrl = `http://localhost:${hubResult.serverPort}/hub`;
if (program.opts().supervisor) {
supervisorUrl = `http://localhost:${hubResult.serverPort}/supervisor`;
}
}
const promptNotification = createPromptNotificationService();
let hubClient;
let hubClientConfig;
let hubCostBuffer;
let hubLogBuffer;
let heartbeatService;
if (hubUrl) {
hubClientConfig = createHubClientConfig(hubUrl);
const hubClientLog = createDualLogger("hub-client.log");
hubClient = createHubClient(hubClientConfig, hubClientLog, promptNotification);
hubCostBuffer = createHubCostBuffer(hubClient);
hubLogBuffer = createHubLogBuffer(hubClient);
}
const globalConfig = createGlobalConfig(hubClient, supervisorUrl);
const hostService = createHostService(hubClient);
const userService = createUserService(hubClient, promptNotification, hostService, agentPath);
const modelService = createModelService(hubClient);
if (hubClient) {
try {
await hubClient.waitForConnection();
await userService.waitForUsers();
await modelService.waitForModels();
}
catch (error) {
console.error(`Failed to connect to hub: ${error}`);
await shutdown();
process.exit(1);
}
}
await globalConfig.waitForConfig();
console.log(`[NAISYS] Started`);
const desktopClaimService = createDesktopClaimService();
const agentManager = new AgentManager(globalConfig, hubClient, hubCostBuffer, hubLogBuffer, hostService, userService, modelService, promptNotification, desktopClaimService);
heartbeatService = createHeartbeatService(hubClient, agentManager, userService);
// Resolve the agent path to a username (or admin if no path) and start the agent
const startupUserIds = userService.getStartupUserIds();
for (const userId of startupUserIds) {
await agentManager.startAgent(userId);
}
const updateService = program.opts().autoUpdate
? createUpdateService(globalConfig, agentManager)
: undefined;
let shuttingDown = false;
const handleShutdownSignal = (signal) => {
if (shuttingDown) {
console.log("\nForce exit");
process.exit(1);
}
shuttingDown = true;
console.log(`\n[NAISYS] Shutting down (${signal})...`);
agentManager.stopAll(signal).catch((err) => {
console.error("[NAISYS] Error stopping agents:", err);
});
};
process.on("SIGINT", () => handleShutdownSignal("SIGINT"));
process.on("SIGTERM", () => handleShutdownSignal("SIGTERM"));
// Remote terminate request relayed from the supervisor via the hub. Ack first
// so the supervisor gets a response before the socket closes, then drain
// agents — the main flow exits the process once they complete.
hubClient?.registerEvent(HubEvents.HOST_TERMINATE, (_data, ack) => {
ack({ success: true });
if (shuttingDown)
return;
shuttingDown = true;
console.log("\n[NAISYS] Shutting down (remote terminate request)...");
agentManager.stopAll("Remote terminate request").catch((err) => {
console.error("[NAISYS] Error stopping agents:", err);
});
});
await agentManager.waitForAllAgentsToComplete();
// Auto-update may still be running its git/npm install when agents drain
// (e.g. user signaled mid-install). Wait so we don't kill the spawned
// processes mid-step. Second Ctrl+C still force-exits via handleShutdownSignal.
if (shuttingDown && updateService?.isInProgress()) {
console.log("[NAISYS] Waiting for in-flight update to finish (Ctrl+C again to force exit)...");
}
await updateService?.waitForCompletion();
await shutdown();
console.log(`[NAISYS] Exited`);
process.exit(updateService?.getExitCode() ?? 0);
// Shutdown is process-owned: process.exit() reaps timers, sockets, Fastify,
// and Socket.IO. Drain pending log/cost entries while the socket and hub are
// still alive, disconnect the hub client, then wait only for the integrated
// hub database disconnect.
async function shutdown() {
await Promise.allSettled([
hubLogBuffer?.flushFinal(),
hubCostBuffer?.flushFinal(),
]);
heartbeatService?.cleanup();
hubClient?.cleanup();
if (integratedHubShutdown) {
await Promise.race([
integratedHubShutdown().catch((err) => console.error("[NAISYS] Hub shutdown:", err)),
sleep(5_000).then(() => console.error("[NAISYS] Hub shutdown timed out")),
]);
}
}
//# sourceMappingURL=naisysMain.js.map