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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/agent",
3
3
  "private": true,
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "AI agent executor, budget control, and session management for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/channels",
3
3
  "private": true,
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Chat platform adapters — Discord, Telegram, Slack, WhatsApp, Signal, iMessage, IRC, LINE",
@@ -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
- await execSystemctl(manager, "start", "comis");
259
- success("Daemon started");
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
- await execSystemctl(manager, "stop", "comis");
290
- success("Daemon stopped");
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
- prompter.log.info("Start later with: comis daemon start");
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
- // 2b. Stop existing daemon before restart
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
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/cli",
3
3
  "private": true,
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Command-line interface for the Comis AI agent platform",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/core",
3
3
  "private": true,
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Core domain types, ports, event bus, security, and config for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/daemon",
3
3
  "private": true,
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Background daemon and orchestrator for the Comis platform",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/gateway",
3
3
  "private": true,
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "HTTP, JSON-RPC, and WebSocket gateway for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/infra",
3
3
  "private": true,
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Structured logging infrastructure for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/memory",
3
3
  "private": true,
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "SQLite memory, embeddings, and RAG storage for Comis agents",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/scheduler",
3
3
  "private": true,
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Task scheduling and cron management for Comis",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/shared",
3
3
  "private": true,
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Shared types and utilities for the Comis platform",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@comis/skills",
3
3
  "private": true,
4
- "version": "1.0.6",
4
+ "version": "1.0.8",
5
5
  "author": "Moshe Anconina",
6
6
  "license": "Apache-2.0",
7
7
  "description": "Skill system, MCP integration, and tool sandbox for Comis agents",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comisai",
3
- "version": "1.0.6",
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.6",
119
- "@comis/core": "1.0.6",
120
- "@comis/infra": "1.0.6",
121
- "@comis/memory": "1.0.6",
122
- "@comis/gateway": "1.0.6",
123
- "@comis/skills": "1.0.6",
124
- "@comis/scheduler": "1.0.6",
125
- "@comis/agent": "1.0.6",
126
- "@comis/channels": "1.0.6",
127
- "@comis/cli": "1.0.6",
128
- "@comis/daemon": "1.0.6",
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",