comisai 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@comis/agent/package.json +1 -1
- package/node_modules/@comis/channels/package.json +1 -1
- package/node_modules/@comis/cli/dist/commands/daemon.js +34 -4
- package/node_modules/@comis/cli/dist/wizard/steps/11-daemon-start.js +112 -14
- package/node_modules/@comis/cli/package.json +1 -1
- package/node_modules/@comis/core/package.json +1 -1
- package/node_modules/@comis/daemon/package.json +1 -1
- package/node_modules/@comis/gateway/package.json +1 -1
- package/node_modules/@comis/infra/package.json +1 -1
- package/node_modules/@comis/memory/package.json +1 -1
- package/node_modules/@comis/scheduler/package.json +1 -1
- package/node_modules/@comis/shared/package.json +1 -1
- package/node_modules/@comis/skills/package.json +1 -1
- package/package.json +12 -12
|
@@ -246,6 +246,16 @@ async function startDirectMode() {
|
|
|
246
246
|
process.exit(1);
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
|
+
/** Check if a systemd unit is already active (does not require sudo). */
|
|
250
|
+
async function isSystemdActive(manager) {
|
|
251
|
+
try {
|
|
252
|
+
const { stdout } = await exec("systemctl", systemctlArgs(manager, "is-active", "comis"), { timeout: 5_000 });
|
|
253
|
+
return stdout.trim() === "active";
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
249
259
|
/** Handle the `daemon start` subcommand. */
|
|
250
260
|
async function handleDaemonStart() {
|
|
251
261
|
try {
|
|
@@ -253,10 +263,20 @@ async function handleDaemonStart() {
|
|
|
253
263
|
switch (manager) {
|
|
254
264
|
case "systemd":
|
|
255
265
|
case "systemd-user": {
|
|
266
|
+
if (await isSystemdActive(manager)) {
|
|
267
|
+
success("Daemon is already running (systemd)");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
256
270
|
const scope = manager === "systemd-user" ? "systemd (user scope)" : "systemd";
|
|
257
271
|
info(`Starting daemon via ${scope}...`);
|
|
258
|
-
|
|
259
|
-
|
|
272
|
+
try {
|
|
273
|
+
await execSystemctl(manager, "start", "comis");
|
|
274
|
+
success("Daemon started");
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
error("Cannot start daemon: insufficient permissions");
|
|
278
|
+
info("Run as root: sudo systemctl start comis");
|
|
279
|
+
}
|
|
260
280
|
return;
|
|
261
281
|
}
|
|
262
282
|
case "pm2": {
|
|
@@ -284,10 +304,20 @@ async function handleDaemonStop() {
|
|
|
284
304
|
switch (manager) {
|
|
285
305
|
case "systemd":
|
|
286
306
|
case "systemd-user": {
|
|
307
|
+
if (!(await isSystemdActive(manager))) {
|
|
308
|
+
warn("Daemon is not running");
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
287
311
|
const scope = manager === "systemd-user" ? "systemd (user scope)" : "systemd";
|
|
288
312
|
info(`Stopping daemon via ${scope}...`);
|
|
289
|
-
|
|
290
|
-
|
|
313
|
+
try {
|
|
314
|
+
await execSystemctl(manager, "stop", "comis");
|
|
315
|
+
success("Daemon stopped");
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
error("Cannot stop daemon: insufficient permissions");
|
|
319
|
+
info("Run as root: sudo systemctl stop comis");
|
|
320
|
+
}
|
|
291
321
|
return;
|
|
292
322
|
}
|
|
293
323
|
case "pm2": {
|
|
@@ -13,10 +13,12 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @module
|
|
15
15
|
*/
|
|
16
|
-
import { spawn } from "node:child_process";
|
|
16
|
+
import { execFile, spawn } from "node:child_process";
|
|
17
17
|
import { existsSync, mkdirSync, writeFileSync, openSync, closeSync, accessSync, constants as fsConstants, } from "node:fs";
|
|
18
18
|
import * as os from "node:os";
|
|
19
|
+
import { promisify } from "node:util";
|
|
19
20
|
import { safePath } from "@comis/core";
|
|
21
|
+
const exec = promisify(execFile);
|
|
20
22
|
import { updateState, sectionSeparator, success as themeSuccess, error as themeError, } from "../index.js";
|
|
21
23
|
// ---------- Constants ----------
|
|
22
24
|
/** Max time to wait for daemon gateway to become ready (ms). */
|
|
@@ -194,6 +196,78 @@ async function runHealthCheck(state, prompter, gatewayHost, gatewayPort) {
|
|
|
194
196
|
}
|
|
195
197
|
}
|
|
196
198
|
}
|
|
199
|
+
async function detectServiceManager() {
|
|
200
|
+
if (!existsSync("/run/systemd/system"))
|
|
201
|
+
return "direct";
|
|
202
|
+
try {
|
|
203
|
+
const { stdout } = await exec("systemctl", ["list-unit-files", "comis.service", "--no-pager", "--no-legend"], { timeout: 5_000 });
|
|
204
|
+
if (stdout.includes("comis.service"))
|
|
205
|
+
return "systemd";
|
|
206
|
+
}
|
|
207
|
+
catch { /* not available */ }
|
|
208
|
+
try {
|
|
209
|
+
const { stdout } = await exec("systemctl", ["--user", "list-unit-files", "comis.service", "--no-pager", "--no-legend"], { timeout: 5_000 });
|
|
210
|
+
if (stdout.includes("comis.service"))
|
|
211
|
+
return "systemd-user";
|
|
212
|
+
}
|
|
213
|
+
catch { /* not available */ }
|
|
214
|
+
return "direct";
|
|
215
|
+
}
|
|
216
|
+
async function restartViaSystemd(manager) {
|
|
217
|
+
try {
|
|
218
|
+
const args = manager === "systemd-user"
|
|
219
|
+
? ["--user", "restart", "comis"]
|
|
220
|
+
: ["restart", "comis"];
|
|
221
|
+
if (manager === "systemd" && process.getuid?.() !== 0) {
|
|
222
|
+
await exec("sudo", ["systemctl", ...args], { timeout: 15_000 });
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
await exec("systemctl", args, { timeout: 15_000 });
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// sudo failed (comis user has no sudo) — fall back to SIGUSR2.
|
|
231
|
+
// The daemon traps SIGUSR2 and exits with code 42; systemd respawns
|
|
232
|
+
// it via RestartForceExitStatus=42, picking up the new config.
|
|
233
|
+
return restartViaSigusr2(manager);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function restartViaSigusr2(manager) {
|
|
237
|
+
try {
|
|
238
|
+
const pidArgs = manager === "systemd-user"
|
|
239
|
+
? ["--user", "show", "comis", "--property=MainPID", "--value"]
|
|
240
|
+
: ["show", "comis", "--property=MainPID", "--value"];
|
|
241
|
+
const { stdout } = await exec("systemctl", pidArgs, { timeout: 5_000 });
|
|
242
|
+
const pid = Number(stdout.trim());
|
|
243
|
+
if (isNaN(pid) || pid <= 0)
|
|
244
|
+
return false;
|
|
245
|
+
process.kill(pid, "SIGUSR2");
|
|
246
|
+
// Wait for systemd to respawn the process
|
|
247
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async function startViaSystemd(manager) {
|
|
255
|
+
try {
|
|
256
|
+
const args = manager === "systemd-user"
|
|
257
|
+
? ["--user", "start", "comis"]
|
|
258
|
+
: ["start", "comis"];
|
|
259
|
+
if (manager === "systemd" && process.getuid?.() !== 0) {
|
|
260
|
+
await exec("sudo", ["systemctl", ...args], { timeout: 15_000 });
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
await exec("systemctl", args, { timeout: 15_000 });
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
197
271
|
// ---------- Step Implementation ----------
|
|
198
272
|
export const daemonStartStep = {
|
|
199
273
|
id: "daemon-start",
|
|
@@ -216,6 +290,7 @@ export const daemonStartStep = {
|
|
|
216
290
|
catch {
|
|
217
291
|
// Not running
|
|
218
292
|
}
|
|
293
|
+
const serviceManager = await detectServiceManager();
|
|
219
294
|
// 1. Offer auto-start (or restart if already running)
|
|
220
295
|
let choice;
|
|
221
296
|
if (daemonRunning) {
|
|
@@ -239,10 +314,44 @@ export const daemonStartStep = {
|
|
|
239
314
|
}
|
|
240
315
|
// 2. If user declines, show manual command and move on
|
|
241
316
|
if (choice === "no") {
|
|
242
|
-
|
|
317
|
+
if (daemonRunning && serviceManager !== "direct") {
|
|
318
|
+
prompter.log.info("Restart to apply changes: sudo systemctl restart comis");
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
prompter.log.info("Start later with: comis daemon start");
|
|
322
|
+
}
|
|
323
|
+
return updateState(state, {});
|
|
324
|
+
}
|
|
325
|
+
// 3. Use systemd when it owns the daemon
|
|
326
|
+
if (serviceManager !== "direct") {
|
|
327
|
+
const spinner = prompter.spinner();
|
|
328
|
+
const action = choice === "restart" ? "Restarting" : "Starting";
|
|
329
|
+
spinner.start(`${action} daemon via systemd...`);
|
|
330
|
+
const ok = choice === "restart"
|
|
331
|
+
? await restartViaSystemd(serviceManager)
|
|
332
|
+
: await startViaSystemd(serviceManager);
|
|
333
|
+
if (!ok) {
|
|
334
|
+
spinner.stop(`Could not ${choice === "restart" ? "restart" : "start"} via systemd`);
|
|
335
|
+
if (process.getuid?.() !== 0) {
|
|
336
|
+
prompter.log.warn("Run as root: sudo systemctl restart comis");
|
|
337
|
+
}
|
|
338
|
+
return updateState(state, {});
|
|
339
|
+
}
|
|
340
|
+
const ready = await waitForReady(host, port);
|
|
341
|
+
if (ready) {
|
|
342
|
+
spinner.stop(`Daemon ${choice === "restart" ? "restarted" : "started"} and ready`);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
spinner.stop(`Daemon ${choice === "restart" ? "restarted" : "started"} but gateway not yet responding`);
|
|
346
|
+
prompter.log.warn("Check logs: comis daemon logs");
|
|
347
|
+
}
|
|
348
|
+
if (!state.skipHealth) {
|
|
349
|
+
await runHealthCheck(state, prompter, host, port);
|
|
350
|
+
}
|
|
243
351
|
return updateState(state, {});
|
|
244
352
|
}
|
|
245
|
-
//
|
|
353
|
+
// 4. Direct-spawn fallback (no systemd)
|
|
354
|
+
// Stop existing daemon before restart
|
|
246
355
|
if (choice === "restart") {
|
|
247
356
|
const stopSpinner = prompter.spinner();
|
|
248
357
|
stopSpinner.start("Stopping daemon...");
|
|
@@ -256,7 +365,6 @@ export const daemonStartStep = {
|
|
|
256
365
|
process.kill(pid, "SIGTERM");
|
|
257
366
|
}
|
|
258
367
|
catch { /* already stopped */ }
|
|
259
|
-
// Brief wait for graceful shutdown
|
|
260
368
|
await new Promise((r) => setTimeout(r, 1500));
|
|
261
369
|
}
|
|
262
370
|
}
|
|
@@ -266,24 +374,19 @@ export const daemonStartStep = {
|
|
|
266
374
|
stopSpinner.stop("Could not stop daemon (may already be stopped)");
|
|
267
375
|
}
|
|
268
376
|
}
|
|
269
|
-
// 3. Spawn daemon
|
|
270
377
|
const spinner = prompter.spinner();
|
|
271
378
|
spinner.start("Starting daemon...");
|
|
272
379
|
try {
|
|
273
|
-
// Resolve daemon binary path (relative to this file's location in dist/)
|
|
274
380
|
const daemonPath = new URL("../../../../daemon/dist/daemon.js", import.meta.url).pathname;
|
|
275
381
|
if (!existsSync(daemonPath)) {
|
|
276
382
|
spinner.stop("Daemon binary not found");
|
|
277
383
|
prompter.log.warn("Run 'pnpm build' first, then 'comis daemon start'");
|
|
278
384
|
return updateState(state, {});
|
|
279
385
|
}
|
|
280
|
-
// Determine paths using safePath (matching daemon.ts pattern)
|
|
281
386
|
const comisDir = safePath(os.homedir(), ".comis");
|
|
282
387
|
const pidFile = safePath(comisDir, "daemon.pid");
|
|
283
388
|
const logFile = safePath(comisDir, "daemon.log");
|
|
284
|
-
// Ensure directory exists with restricted permissions
|
|
285
389
|
mkdirSync(comisDir, { recursive: true, mode: 0o700 });
|
|
286
|
-
// Open log file for daemon stdout/stderr
|
|
287
390
|
const logFd = openSync(logFile, "a", 0o600);
|
|
288
391
|
let childPid;
|
|
289
392
|
try {
|
|
@@ -295,17 +398,14 @@ export const daemonStartStep = {
|
|
|
295
398
|
childPid = child.pid ?? undefined;
|
|
296
399
|
}
|
|
297
400
|
finally {
|
|
298
|
-
// Close the file descriptor in the parent process after spawn
|
|
299
401
|
closeSync(logFd);
|
|
300
402
|
}
|
|
301
403
|
if (!childPid) {
|
|
302
404
|
spinner.stop("Failed to start daemon: no PID returned");
|
|
303
405
|
return updateState(state, {});
|
|
304
406
|
}
|
|
305
|
-
// Write PID file
|
|
306
407
|
writeFileSync(pidFile, String(childPid));
|
|
307
408
|
spinner.update(`Daemon started (PID ${childPid})`);
|
|
308
|
-
// 4. Wait for readiness
|
|
309
409
|
const ready = await waitForReady(host, port);
|
|
310
410
|
if (ready) {
|
|
311
411
|
spinner.stop(`Daemon started and ready (PID ${childPid})`);
|
|
@@ -314,7 +414,6 @@ export const daemonStartStep = {
|
|
|
314
414
|
spinner.stop("Daemon started but gateway not yet responding");
|
|
315
415
|
prompter.log.warn("Check logs: comis daemon logs");
|
|
316
416
|
}
|
|
317
|
-
// 5. Run health check (skip if --skip-health was passed)
|
|
318
417
|
if (!state.skipHealth) {
|
|
319
418
|
await runHealthCheck(state, prompter, host, port);
|
|
320
419
|
}
|
|
@@ -324,7 +423,6 @@ export const daemonStartStep = {
|
|
|
324
423
|
spinner.stop(`Failed to start daemon: ${msg}`);
|
|
325
424
|
prompter.log.warn("You can start the daemon later with: comis daemon start");
|
|
326
425
|
}
|
|
327
|
-
// 7. Return state
|
|
328
426
|
return updateState(state, {});
|
|
329
427
|
},
|
|
330
428
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comisai",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"author": "Moshe Anconina",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "Security-first AI agent platform — connects AI agents to Discord, Telegram, Slack, WhatsApp, and more",
|
|
@@ -115,17 +115,17 @@
|
|
|
115
115
|
"@comis/daemon"
|
|
116
116
|
],
|
|
117
117
|
"dependencies": {
|
|
118
|
-
"@comis/shared": "1.0.
|
|
119
|
-
"@comis/core": "1.0.
|
|
120
|
-
"@comis/infra": "1.0.
|
|
121
|
-
"@comis/memory": "1.0.
|
|
122
|
-
"@comis/gateway": "1.0.
|
|
123
|
-
"@comis/skills": "1.0.
|
|
124
|
-
"@comis/scheduler": "1.0.
|
|
125
|
-
"@comis/agent": "1.0.
|
|
126
|
-
"@comis/channels": "1.0.
|
|
127
|
-
"@comis/cli": "1.0.
|
|
128
|
-
"@comis/daemon": "1.0.
|
|
118
|
+
"@comis/shared": "1.0.8",
|
|
119
|
+
"@comis/core": "1.0.8",
|
|
120
|
+
"@comis/infra": "1.0.8",
|
|
121
|
+
"@comis/memory": "1.0.8",
|
|
122
|
+
"@comis/gateway": "1.0.8",
|
|
123
|
+
"@comis/skills": "1.0.8",
|
|
124
|
+
"@comis/scheduler": "1.0.8",
|
|
125
|
+
"@comis/agent": "1.0.8",
|
|
126
|
+
"@comis/channels": "1.0.8",
|
|
127
|
+
"@comis/cli": "1.0.8",
|
|
128
|
+
"@comis/daemon": "1.0.8",
|
|
129
129
|
"@agentclientprotocol/sdk": "^0.15.0",
|
|
130
130
|
"@clack/core": "^1.1.0",
|
|
131
131
|
"@clack/prompts": "^1.1.0",
|