nextclaw 0.2.2 → 0.2.4

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/dist/cli/index.js CHANGED
@@ -22,7 +22,16 @@ import {
22
22
 
23
23
  // src/cli/index.ts
24
24
  import { Command } from "commander";
25
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, cpSync, rmSync } from "fs";
25
+ import {
26
+ existsSync as existsSync6,
27
+ mkdirSync as mkdirSync5,
28
+ readFileSync as readFileSync5,
29
+ writeFileSync as writeFileSync4,
30
+ cpSync,
31
+ rmSync,
32
+ openSync,
33
+ closeSync
34
+ } from "fs";
26
35
  import { join as join6, resolve } from "path";
27
36
  import { spawn, spawnSync } from "child_process";
28
37
  import { createInterface } from "readline";
@@ -3101,7 +3110,25 @@ program.command("ui").description(`Start the ${APP_NAME} UI with gateway`).optio
3101
3110
  }
3102
3111
  await startGateway({ uiOverrides, allowMissingProvider: true });
3103
3112
  });
3104
- program.command("start").description(`Start the ${APP_NAME} gateway + UI (backend + optional frontend)`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--frontend", "Start UI frontend dev server", false).option("--frontend-port <port>", "UI frontend dev server port").option("--no-open", "Disable opening browser").action(async (opts) => {
3113
+ program.command("start").description(`Start the ${APP_NAME} gateway + UI in the background`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--frontend", "Start UI frontend dev server", false).option("--frontend-port <port>", "UI frontend dev server port").option("--open", "Open browser after start", false).action(async (opts) => {
3114
+ const uiOverrides = {
3115
+ enabled: true,
3116
+ open: false
3117
+ };
3118
+ if (opts.uiHost) {
3119
+ uiOverrides.host = String(opts.uiHost);
3120
+ }
3121
+ if (opts.uiPort) {
3122
+ uiOverrides.port = Number(opts.uiPort);
3123
+ }
3124
+ await startService({
3125
+ uiOverrides,
3126
+ frontend: Boolean(opts.frontend),
3127
+ frontendPort: Number(opts.frontendPort),
3128
+ open: Boolean(opts.open)
3129
+ });
3130
+ });
3131
+ program.command("serve").description(`Run the ${APP_NAME} gateway + UI in the foreground`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--frontend", "Start UI frontend dev server", false).option("--frontend-port <port>", "UI frontend dev server port").option("--open", "Open browser after start", false).action(async (opts) => {
3105
3132
  const uiOverrides = {
3106
3133
  enabled: true,
3107
3134
  open: false
@@ -3126,15 +3153,12 @@ program.command("start").description(`Start the ${APP_NAME} gateway + UI (backen
3126
3153
  dir: frontendDir
3127
3154
  });
3128
3155
  frontendUrl = frontend?.url ?? null;
3129
- } else if (shouldStartFrontend && !frontendDir && !staticDir) {
3156
+ } else if (shouldStartFrontend && !frontendDir) {
3130
3157
  console.log("Warning: UI frontend not found. Start it separately.");
3131
3158
  }
3132
3159
  if (!frontendUrl && staticDir) {
3133
3160
  frontendUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
3134
3161
  }
3135
- if (!frontendUrl && staticDir) {
3136
- frontendUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
3137
- }
3138
3162
  if (opts.open && frontendUrl) {
3139
3163
  openBrowser(frontendUrl);
3140
3164
  } else if (opts.open && !frontendUrl) {
@@ -3142,6 +3166,9 @@ program.command("start").description(`Start the ${APP_NAME} gateway + UI (backen
3142
3166
  }
3143
3167
  await startGateway({ uiOverrides, allowMissingProvider: true, uiStaticDir: staticDir ?? void 0 });
3144
3168
  });
3169
+ program.command("stop").description(`Stop the ${APP_NAME} background service`).action(async () => {
3170
+ await stopService();
3171
+ });
3145
3172
  program.command("agent").description("Interact with the agent directly").option("-m, --message <message>", "Message to send to the agent").option("-s, --session <session>", "Session ID", "cli:default").option("--no-markdown", "Disable Markdown rendering").action(async (opts) => {
3146
3173
  const config = loadConfig();
3147
3174
  const bus = new MessageBus();
@@ -3418,6 +3445,154 @@ function resolveUiApiBase(host, port) {
3418
3445
  const normalizedHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
3419
3446
  return `http://${normalizedHost}:${port}`;
3420
3447
  }
3448
+ async function startService(options) {
3449
+ const config = loadConfig();
3450
+ const uiConfig = resolveUiConfig(config, options.uiOverrides);
3451
+ const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
3452
+ const apiUrl = `${uiUrl}/api`;
3453
+ const staticDir = resolveUiStaticDir();
3454
+ const existing = readServiceState();
3455
+ if (existing && isProcessRunning(existing.pid)) {
3456
+ console.log(`\u2713 ${APP_NAME} is already running (PID ${existing.pid})`);
3457
+ console.log(`UI: ${existing.uiUrl}`);
3458
+ console.log(`API: ${existing.apiUrl}`);
3459
+ console.log(`Logs: ${existing.logPath}`);
3460
+ console.log(`Stop: ${APP_NAME} stop`);
3461
+ return;
3462
+ }
3463
+ if (existing) {
3464
+ clearServiceState();
3465
+ }
3466
+ if (!staticDir && !options.frontend) {
3467
+ console.log("Warning: UI frontend not found. Use --frontend to start the dev server.");
3468
+ }
3469
+ const logPath = resolveServiceLogPath();
3470
+ const logDir = resolve(logPath, "..");
3471
+ mkdirSync5(logDir, { recursive: true });
3472
+ const logFd = openSync(logPath, "a");
3473
+ const serveArgs = buildServeArgs({
3474
+ uiHost: uiConfig.host,
3475
+ uiPort: uiConfig.port,
3476
+ frontend: options.frontend,
3477
+ frontendPort: options.frontendPort
3478
+ });
3479
+ const child = spawn(process.execPath, [...process.execArgv, ...serveArgs], {
3480
+ env: process.env,
3481
+ stdio: ["ignore", logFd, logFd],
3482
+ detached: true
3483
+ });
3484
+ closeSync(logFd);
3485
+ if (!child.pid) {
3486
+ console.error("Error: Failed to start background service.");
3487
+ return;
3488
+ }
3489
+ child.unref();
3490
+ const state = {
3491
+ pid: child.pid,
3492
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3493
+ uiUrl,
3494
+ apiUrl,
3495
+ logPath
3496
+ };
3497
+ writeServiceState(state);
3498
+ console.log(`\u2713 ${APP_NAME} started in background (PID ${state.pid})`);
3499
+ console.log(`UI: ${uiUrl}`);
3500
+ console.log(`API: ${apiUrl}`);
3501
+ console.log(`Logs: ${logPath}`);
3502
+ console.log(`Stop: ${APP_NAME} stop`);
3503
+ if (options.open) {
3504
+ openBrowser(uiUrl);
3505
+ }
3506
+ }
3507
+ async function stopService() {
3508
+ const state = readServiceState();
3509
+ if (!state) {
3510
+ console.log("No running service found.");
3511
+ return;
3512
+ }
3513
+ if (!isProcessRunning(state.pid)) {
3514
+ console.log("Service is not running. Cleaning up state.");
3515
+ clearServiceState();
3516
+ return;
3517
+ }
3518
+ console.log(`Stopping ${APP_NAME} (PID ${state.pid})...`);
3519
+ try {
3520
+ process.kill(state.pid, "SIGTERM");
3521
+ } catch (error) {
3522
+ console.error(`Failed to stop service: ${String(error)}`);
3523
+ return;
3524
+ }
3525
+ const stopped = await waitForExit(state.pid, 3e3);
3526
+ if (!stopped) {
3527
+ try {
3528
+ process.kill(state.pid, "SIGKILL");
3529
+ } catch (error) {
3530
+ console.error(`Failed to force stop service: ${String(error)}`);
3531
+ return;
3532
+ }
3533
+ await waitForExit(state.pid, 2e3);
3534
+ }
3535
+ clearServiceState();
3536
+ console.log(`\u2713 ${APP_NAME} stopped`);
3537
+ }
3538
+ function buildServeArgs(options) {
3539
+ const cliPath = fileURLToPath(import.meta.url);
3540
+ const args = [cliPath, "serve", "--ui-host", options.uiHost, "--ui-port", String(options.uiPort)];
3541
+ if (options.frontend) {
3542
+ args.push("--frontend");
3543
+ }
3544
+ if (Number.isFinite(options.frontendPort)) {
3545
+ args.push("--frontend-port", String(options.frontendPort));
3546
+ }
3547
+ return args;
3548
+ }
3549
+ function readServiceState() {
3550
+ const path = resolveServiceStatePath();
3551
+ if (!existsSync6(path)) {
3552
+ return null;
3553
+ }
3554
+ try {
3555
+ const raw = readFileSync5(path, "utf-8");
3556
+ return JSON.parse(raw);
3557
+ } catch {
3558
+ return null;
3559
+ }
3560
+ }
3561
+ function writeServiceState(state) {
3562
+ const path = resolveServiceStatePath();
3563
+ mkdirSync5(resolve(path, ".."), { recursive: true });
3564
+ writeFileSync4(path, JSON.stringify(state, null, 2));
3565
+ }
3566
+ function clearServiceState() {
3567
+ const path = resolveServiceStatePath();
3568
+ if (existsSync6(path)) {
3569
+ rmSync(path, { force: true });
3570
+ }
3571
+ }
3572
+ function resolveServiceStatePath() {
3573
+ return resolve(getDataDir(), "run", "service.json");
3574
+ }
3575
+ function resolveServiceLogPath() {
3576
+ return resolve(getDataDir(), "logs", "service.log");
3577
+ }
3578
+ function isProcessRunning(pid) {
3579
+ try {
3580
+ process.kill(pid, 0);
3581
+ return true;
3582
+ } catch {
3583
+ return false;
3584
+ }
3585
+ }
3586
+ async function waitForExit(pid, timeoutMs) {
3587
+ const start = Date.now();
3588
+ while (Date.now() - start < timeoutMs) {
3589
+ if (!isProcessRunning(pid)) {
3590
+ return true;
3591
+ }
3592
+ await new Promise((resolve2) => setTimeout(resolve2, 200));
3593
+ }
3594
+ return !isProcessRunning(pid);
3595
+ }
3421
3596
  function resolveUiStaticDir() {
3422
3597
  const candidates = [];
3423
3598
  const envDir = process.env.NEXTCLAW_UI_STATIC_DIR;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextclaw",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Lightweight personal AI assistant with CLI, multi-provider routing, and channel integrations.",
5
5
  "private": false,
6
6
  "type": "module",