nextclaw 0.4.0 → 0.4.1

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +493 -454
  2. package/package.json +3 -3
package/dist/cli/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  saveConfig,
11
11
  getConfigPath,
12
12
  getDataDir as getDataDir2,
13
- ConfigSchema,
13
+ ConfigSchema as ConfigSchema2,
14
14
  getApiBase,
15
15
  getProvider,
16
16
  getProviderName,
@@ -35,20 +35,29 @@ import { startUiServer } from "nextclaw-server";
35
35
  import {
36
36
  closeSync,
37
37
  cpSync,
38
- existsSync as existsSync2,
38
+ existsSync as existsSync3,
39
39
  mkdirSync as mkdirSync2,
40
40
  openSync,
41
- readFileSync as readFileSync2,
41
+ readFileSync as readFileSync3,
42
42
  rmSync as rmSync2,
43
43
  writeFileSync as writeFileSync2
44
44
  } from "fs";
45
- import { createHash } from "crypto";
46
- import { dirname, join as join2, resolve as resolve2 } from "path";
47
- import { spawn as spawn2, spawnSync } from "child_process";
45
+ import { dirname, join as join3, resolve as resolve3 } from "path";
46
+ import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
48
47
  import { createInterface } from "readline";
49
- import { fileURLToPath as fileURLToPath2 } from "url";
48
+ import { fileURLToPath as fileURLToPath3 } from "url";
50
49
  import chokidar from "chokidar";
51
50
 
51
+ // src/cli/gateway/controller.ts
52
+ import { createHash } from "crypto";
53
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
54
+ import { spawnSync } from "child_process";
55
+ import { fileURLToPath as fileURLToPath2 } from "url";
56
+ import { join as join2, resolve as resolve2 } from "path";
57
+ import {
58
+ ConfigSchema
59
+ } from "nextclaw-core";
60
+
52
61
  // src/cli/utils.ts
53
62
  import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "fs";
54
63
  import { join, resolve } from "path";
@@ -87,12 +96,12 @@ async function isPortAvailable(port, host) {
87
96
  return await canBindPort(port, checkHost);
88
97
  }
89
98
  async function canBindPort(port, host) {
90
- return await new Promise((resolve3) => {
99
+ return await new Promise((resolve4) => {
91
100
  const server = createServer();
92
101
  server.unref();
93
- server.once("error", () => resolve3(false));
102
+ server.once("error", () => resolve4(false));
94
103
  server.listen({ port, host }, () => {
95
- server.close(() => resolve3(true));
104
+ server.close(() => resolve4(true));
96
105
  });
97
106
  });
98
107
  }
@@ -150,7 +159,7 @@ async function waitForExit(pid, timeoutMs) {
150
159
  if (!isProcessRunning(pid)) {
151
160
  return true;
152
161
  }
153
- await new Promise((resolve3) => setTimeout(resolve3, 200));
162
+ await new Promise((resolve4) => setTimeout(resolve4, 200));
154
163
  }
155
164
  return !isProcessRunning(pid);
156
165
  }
@@ -281,14 +290,395 @@ function printAgentResponse(response) {
281
290
  async function prompt(rl, question) {
282
291
  rl.setPrompt(question);
283
292
  rl.prompt();
284
- return new Promise((resolve3) => {
285
- rl.once("line", (line) => resolve3(line));
293
+ return new Promise((resolve4) => {
294
+ rl.once("line", (line) => resolve4(line));
286
295
  });
287
296
  }
288
297
 
298
+ // src/cli/gateway/controller.ts
299
+ var hashRaw = (raw) => createHash("sha256").update(raw).digest("hex");
300
+ var redactConfig = (value) => {
301
+ if (Array.isArray(value)) {
302
+ return value.map((entry) => redactConfig(entry));
303
+ }
304
+ if (!value || typeof value !== "object") {
305
+ return value;
306
+ }
307
+ const entries = value;
308
+ const output = {};
309
+ for (const [key, val] of Object.entries(entries)) {
310
+ if (/apiKey|token|secret|password|appId|clientSecret|accessKey/i.test(key)) {
311
+ output[key] = val ? "***" : val;
312
+ continue;
313
+ }
314
+ output[key] = redactConfig(val);
315
+ }
316
+ return output;
317
+ };
318
+ var readConfigSnapshot = (getConfigPath2) => {
319
+ const path = getConfigPath2();
320
+ let raw = "";
321
+ let parsed = {};
322
+ if (existsSync2(path)) {
323
+ raw = readFileSync2(path, "utf-8");
324
+ try {
325
+ parsed = JSON.parse(raw);
326
+ } catch {
327
+ parsed = {};
328
+ }
329
+ }
330
+ let config;
331
+ let valid = true;
332
+ try {
333
+ config = ConfigSchema.parse(parsed);
334
+ } catch {
335
+ config = ConfigSchema.parse({});
336
+ valid = false;
337
+ }
338
+ if (!raw) {
339
+ raw = JSON.stringify(config, null, 2);
340
+ }
341
+ const hash = hashRaw(raw);
342
+ const redacted = redactConfig(config);
343
+ return { raw: valid ? JSON.stringify(redacted, null, 2) : null, hash: valid ? hash : null, config, redacted, valid };
344
+ };
345
+ var mergeDeep = (base, patch) => {
346
+ const next = { ...base };
347
+ for (const [key, value] of Object.entries(patch)) {
348
+ if (value && typeof value === "object" && !Array.isArray(value)) {
349
+ const baseVal = base[key];
350
+ if (baseVal && typeof baseVal === "object" && !Array.isArray(baseVal)) {
351
+ next[key] = mergeDeep(baseVal, value);
352
+ } else {
353
+ next[key] = mergeDeep({}, value);
354
+ }
355
+ } else {
356
+ next[key] = value;
357
+ }
358
+ }
359
+ return next;
360
+ };
361
+ var buildSchemaFromValue = (value) => {
362
+ if (Array.isArray(value)) {
363
+ const item = value.length ? buildSchemaFromValue(value[0]) : { type: "string" };
364
+ return { type: "array", items: item };
365
+ }
366
+ if (value && typeof value === "object") {
367
+ const props = {};
368
+ for (const [key, val] of Object.entries(value)) {
369
+ props[key] = buildSchemaFromValue(val);
370
+ }
371
+ return { type: "object", properties: props };
372
+ }
373
+ if (typeof value === "number") {
374
+ return { type: "number" };
375
+ }
376
+ if (typeof value === "boolean") {
377
+ return { type: "boolean" };
378
+ }
379
+ if (value === null) {
380
+ return { type: ["null", "string"] };
381
+ }
382
+ return { type: "string" };
383
+ };
384
+ var scheduleRestart = (delayMs, reason) => {
385
+ const delay = typeof delayMs === "number" && Number.isFinite(delayMs) ? Math.max(0, delayMs) : 100;
386
+ console.log(`Gateway restart requested via tool${reason ? ` (${reason})` : ""}.`);
387
+ setTimeout(() => {
388
+ process.exit(0);
389
+ }, delay);
390
+ };
391
+ var GatewayControllerImpl = class {
392
+ constructor(deps) {
393
+ this.deps = deps;
394
+ }
395
+ status() {
396
+ return {
397
+ channels: this.deps.reloader.getChannels().enabledChannels,
398
+ cron: this.deps.cron.status(),
399
+ configPath: this.deps.getConfigPath()
400
+ };
401
+ }
402
+ async reloadConfig(reason) {
403
+ return this.deps.reloader.reloadConfig(reason);
404
+ }
405
+ async restart(options) {
406
+ scheduleRestart(options?.delayMs, options?.reason);
407
+ return "Restart scheduled";
408
+ }
409
+ async getConfig() {
410
+ const snapshot = readConfigSnapshot(this.deps.getConfigPath);
411
+ return {
412
+ raw: snapshot.raw,
413
+ hash: snapshot.hash,
414
+ path: this.deps.getConfigPath(),
415
+ config: snapshot.redacted,
416
+ parsed: snapshot.redacted,
417
+ resolved: snapshot.redacted,
418
+ valid: snapshot.valid
419
+ };
420
+ }
421
+ async getConfigSchema() {
422
+ const base = ConfigSchema.parse({});
423
+ return {
424
+ schema: {
425
+ ...buildSchemaFromValue(base),
426
+ title: "NextClawConfig",
427
+ description: "NextClaw config schema (simplified)"
428
+ },
429
+ uiHints: {},
430
+ version: getPackageVersion(),
431
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
432
+ };
433
+ }
434
+ async applyConfig(params) {
435
+ const snapshot = readConfigSnapshot(this.deps.getConfigPath);
436
+ if (!params.baseHash) {
437
+ return { ok: false, error: "config base hash required; re-run config.get and retry" };
438
+ }
439
+ if (!snapshot.valid || !snapshot.hash) {
440
+ return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
441
+ }
442
+ if (params.baseHash !== snapshot.hash) {
443
+ return { ok: false, error: "config changed since last load; re-run config.get and retry" };
444
+ }
445
+ let parsedRaw;
446
+ try {
447
+ parsedRaw = JSON.parse(params.raw);
448
+ } catch {
449
+ return { ok: false, error: "invalid JSON in raw config" };
450
+ }
451
+ let validated;
452
+ try {
453
+ validated = ConfigSchema.parse(parsedRaw);
454
+ } catch (err) {
455
+ return { ok: false, error: `invalid config: ${String(err)}` };
456
+ }
457
+ this.deps.saveConfig(validated);
458
+ const delayMs = params.restartDelayMs ?? 0;
459
+ scheduleRestart(delayMs, "config.apply");
460
+ return {
461
+ ok: true,
462
+ note: params.note ?? null,
463
+ path: this.deps.getConfigPath(),
464
+ config: redactConfig(validated),
465
+ restart: { scheduled: true, delayMs }
466
+ };
467
+ }
468
+ async patchConfig(params) {
469
+ const snapshot = readConfigSnapshot(this.deps.getConfigPath);
470
+ if (!params.baseHash) {
471
+ return { ok: false, error: "config base hash required; re-run config.get and retry" };
472
+ }
473
+ if (!snapshot.valid || !snapshot.hash) {
474
+ return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
475
+ }
476
+ if (params.baseHash !== snapshot.hash) {
477
+ return { ok: false, error: "config changed since last load; re-run config.get and retry" };
478
+ }
479
+ let patch;
480
+ try {
481
+ patch = JSON.parse(params.raw);
482
+ } catch {
483
+ return { ok: false, error: "invalid JSON in raw config" };
484
+ }
485
+ const merged = mergeDeep(snapshot.config, patch);
486
+ let validated;
487
+ try {
488
+ validated = ConfigSchema.parse(merged);
489
+ } catch (err) {
490
+ return { ok: false, error: `invalid config: ${String(err)}` };
491
+ }
492
+ this.deps.saveConfig(validated);
493
+ const delayMs = params.restartDelayMs ?? 0;
494
+ scheduleRestart(delayMs, "config.patch");
495
+ return {
496
+ ok: true,
497
+ note: params.note ?? null,
498
+ path: this.deps.getConfigPath(),
499
+ config: redactConfig(validated),
500
+ restart: { scheduled: true, delayMs }
501
+ };
502
+ }
503
+ async updateRun(params) {
504
+ const timeoutMs = params.timeoutMs ?? 20 * 6e4;
505
+ const gatewayDir = resolve2(fileURLToPath2(new URL(".", import.meta.url)));
506
+ const cliDir = resolve2(gatewayDir, "..");
507
+ const pkgRoot = resolve2(cliDir, "..", "..");
508
+ const repoRoot = existsSync2(join2(pkgRoot, ".git")) ? pkgRoot : resolve2(pkgRoot, "..", "..");
509
+ const steps = [];
510
+ const runStep = (cmd, args, cwd) => {
511
+ const result = spawnSync(cmd, args, {
512
+ cwd,
513
+ encoding: "utf-8",
514
+ timeout: timeoutMs,
515
+ stdio: "pipe"
516
+ });
517
+ const step = {
518
+ cmd,
519
+ args,
520
+ cwd,
521
+ code: result.status,
522
+ stdout: (result.stdout ?? "").toString().slice(0, 4e3),
523
+ stderr: (result.stderr ?? "").toString().slice(0, 4e3)
524
+ };
525
+ steps.push(step);
526
+ return { ok: result.status === 0, code: result.status };
527
+ };
528
+ const updateCommand = process.env.NEXTCLAW_UPDATE_COMMAND?.trim();
529
+ if (updateCommand) {
530
+ const ok = runStep("sh", ["-c", updateCommand], process.cwd());
531
+ if (!ok.ok) {
532
+ return { ok: false, error: "update command failed", steps };
533
+ }
534
+ } else if (existsSync2(join2(repoRoot, ".git"))) {
535
+ if (!which("git")) {
536
+ return { ok: false, error: "git not found for repo update", steps };
537
+ }
538
+ const ok = runStep("git", ["-C", repoRoot, "pull", "--rebase"], repoRoot);
539
+ if (!ok.ok) {
540
+ return { ok: false, error: "git pull failed", steps };
541
+ }
542
+ if (existsSync2(join2(repoRoot, "pnpm-lock.yaml")) && which("pnpm")) {
543
+ const installOk = runStep("pnpm", ["install"], repoRoot);
544
+ if (!installOk.ok) {
545
+ return { ok: false, error: "pnpm install failed", steps };
546
+ }
547
+ } else if (existsSync2(join2(repoRoot, "package.json")) && which("npm")) {
548
+ const installOk = runStep("npm", ["install"], repoRoot);
549
+ if (!installOk.ok) {
550
+ return { ok: false, error: "npm install failed", steps };
551
+ }
552
+ }
553
+ } else if (which("npm")) {
554
+ const ok = runStep("npm", ["i", "-g", "nextclaw"], process.cwd());
555
+ if (!ok.ok) {
556
+ return { ok: false, error: "npm install -g nextclaw failed", steps };
557
+ }
558
+ } else {
559
+ return { ok: false, error: "no update strategy available", steps };
560
+ }
561
+ const delayMs = params.restartDelayMs ?? 0;
562
+ scheduleRestart(delayMs, "update.run");
563
+ return {
564
+ ok: true,
565
+ note: params.note ?? null,
566
+ restart: { scheduled: true, delayMs },
567
+ steps
568
+ };
569
+ }
570
+ };
571
+
289
572
  // src/cli/runtime.ts
290
573
  var LOGO = "\u{1F916}";
291
574
  var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "/exit", "/quit", ":q"]);
575
+ var ConfigReloader = class {
576
+ constructor(options) {
577
+ this.options = options;
578
+ this.currentConfig = options.initialConfig;
579
+ this.channels = options.channels;
580
+ }
581
+ currentConfig;
582
+ channels;
583
+ reloadTask = null;
584
+ providerReloadTask = null;
585
+ reloadTimer = null;
586
+ reloadRunning = false;
587
+ reloadPending = false;
588
+ getChannels() {
589
+ return this.channels;
590
+ }
591
+ async applyReloadPlan(nextConfig) {
592
+ const changedPaths = diffConfigPaths(this.currentConfig, nextConfig);
593
+ if (!changedPaths.length) {
594
+ return;
595
+ }
596
+ this.currentConfig = nextConfig;
597
+ const plan = buildReloadPlan(changedPaths);
598
+ if (plan.restartChannels) {
599
+ await this.reloadChannels(nextConfig);
600
+ }
601
+ if (plan.reloadProviders) {
602
+ await this.reloadProvider(nextConfig);
603
+ }
604
+ if (plan.restartRequired.length > 0) {
605
+ this.options.onRestartRequired(plan.restartRequired);
606
+ }
607
+ }
608
+ scheduleReload(reason) {
609
+ if (this.reloadTimer) {
610
+ clearTimeout(this.reloadTimer);
611
+ }
612
+ this.reloadTimer = setTimeout(() => {
613
+ void this.runReload(reason);
614
+ }, 300);
615
+ }
616
+ async runReload(reason) {
617
+ if (this.reloadRunning) {
618
+ this.reloadPending = true;
619
+ return;
620
+ }
621
+ this.reloadRunning = true;
622
+ if (this.reloadTimer) {
623
+ clearTimeout(this.reloadTimer);
624
+ this.reloadTimer = null;
625
+ }
626
+ try {
627
+ const nextConfig = this.options.loadConfig();
628
+ await this.applyReloadPlan(nextConfig);
629
+ } catch (error) {
630
+ console.error(`Config reload failed (${reason}): ${String(error)}`);
631
+ } finally {
632
+ this.reloadRunning = false;
633
+ if (this.reloadPending) {
634
+ this.reloadPending = false;
635
+ this.scheduleReload("pending");
636
+ }
637
+ }
638
+ }
639
+ async reloadConfig(reason) {
640
+ await this.runReload(reason ?? "gateway tool");
641
+ return "Config reload triggered";
642
+ }
643
+ async reloadChannels(nextConfig) {
644
+ if (this.reloadTask) {
645
+ await this.reloadTask;
646
+ return;
647
+ }
648
+ this.reloadTask = (async () => {
649
+ await this.channels.stopAll();
650
+ this.channels = new ChannelManager(nextConfig, this.options.bus, this.options.sessionManager);
651
+ await this.channels.startAll();
652
+ })();
653
+ try {
654
+ await this.reloadTask;
655
+ } finally {
656
+ this.reloadTask = null;
657
+ }
658
+ }
659
+ async reloadProvider(nextConfig) {
660
+ if (!this.options.providerManager) {
661
+ return;
662
+ }
663
+ if (this.providerReloadTask) {
664
+ await this.providerReloadTask;
665
+ return;
666
+ }
667
+ this.providerReloadTask = (async () => {
668
+ const nextProvider = this.options.makeProvider(nextConfig);
669
+ if (!nextProvider) {
670
+ console.warn("Provider reload skipped: missing API key.");
671
+ return;
672
+ }
673
+ this.options.providerManager?.set(nextProvider);
674
+ })();
675
+ try {
676
+ await this.providerReloadTask;
677
+ } finally {
678
+ this.providerReloadTask = null;
679
+ }
680
+ }
681
+ };
292
682
  var CliRuntime = class {
293
683
  logo;
294
684
  constructor(options = {}) {
@@ -307,15 +697,15 @@ var CliRuntime = class {
307
697
  const force = Boolean(options.force);
308
698
  const configPath = getConfigPath();
309
699
  let createdConfig = false;
310
- if (!existsSync2(configPath)) {
311
- const config2 = ConfigSchema.parse({});
700
+ if (!existsSync3(configPath)) {
701
+ const config2 = ConfigSchema2.parse({});
312
702
  saveConfig(config2);
313
703
  createdConfig = true;
314
704
  }
315
705
  const config = loadConfig();
316
706
  const workspaceSetting = config.agents.defaults.workspace;
317
- const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ? join2(getDataDir2(), DEFAULT_WORKSPACE_DIR) : expandHome(workspaceSetting);
318
- const workspaceExisted = existsSync2(workspacePath);
707
+ const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ? join3(getDataDir2(), DEFAULT_WORKSPACE_DIR) : expandHome(workspaceSetting);
708
+ const workspaceExisted = existsSync3(workspacePath);
319
709
  mkdirSync2(workspacePath, { recursive: true });
320
710
  const templateResult = this.createWorkspaceTemplates(workspacePath, { force });
321
711
  if (createdConfig) {
@@ -479,10 +869,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
479
869
  }
480
870
  console.log(`${this.logo} Interactive mode (type exit or Ctrl+C to quit)
481
871
  `);
482
- const historyFile = join2(getDataDir2(), "history", "cli_history");
483
- const historyDir = resolve2(historyFile, "..");
872
+ const historyFile = join3(getDataDir2(), "history", "cli_history");
873
+ const historyDir = resolve3(historyFile, "..");
484
874
  mkdirSync2(historyDir, { recursive: true });
485
- const history = existsSync2(historyFile) ? readFileSync2(historyFile, "utf-8").split("\n").filter(Boolean) : [];
875
+ const history = existsSync3(historyFile) ? readFileSync3(historyFile, "utf-8").split("\n").filter(Boolean) : [];
486
876
  const rl = createInterface({ input: process.stdin, output: process.stdout });
487
877
  rl.on("close", () => {
488
878
  const merged = history.concat(rl.history ?? []);
@@ -523,13 +913,13 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
523
913
  const bridgeDir = this.getBridgeDir();
524
914
  console.log(`${this.logo} Starting bridge...`);
525
915
  console.log("Scan the QR code to connect.\n");
526
- const result = spawnSync("npm", ["start"], { cwd: bridgeDir, stdio: "inherit" });
916
+ const result = spawnSync2("npm", ["start"], { cwd: bridgeDir, stdio: "inherit" });
527
917
  if (result.status !== 0) {
528
918
  console.error(`Bridge failed: ${result.status ?? 1}`);
529
919
  }
530
920
  }
531
921
  cronList(opts) {
532
- const storePath = join2(getDataDir2(), "cron", "jobs.json");
922
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
533
923
  const service = new CronService(storePath);
534
924
  const jobs = service.listJobs(Boolean(opts.all));
535
925
  if (!jobs.length) {
@@ -549,7 +939,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
549
939
  }
550
940
  }
551
941
  cronAdd(opts) {
552
- const storePath = join2(getDataDir2(), "cron", "jobs.json");
942
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
553
943
  const service = new CronService(storePath);
554
944
  let schedule = null;
555
945
  if (opts.every) {
@@ -574,7 +964,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
574
964
  console.log(`\u2713 Added job '${job.name}' (${job.id})`);
575
965
  }
576
966
  cronRemove(jobId) {
577
- const storePath = join2(getDataDir2(), "cron", "jobs.json");
967
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
578
968
  const service = new CronService(storePath);
579
969
  if (service.removeJob(jobId)) {
580
970
  console.log(`\u2713 Removed job ${jobId}`);
@@ -583,7 +973,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
583
973
  }
584
974
  }
585
975
  cronEnable(jobId, opts) {
586
- const storePath = join2(getDataDir2(), "cron", "jobs.json");
976
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
587
977
  const service = new CronService(storePath);
588
978
  const job = service.enableJob(jobId, !opts.disable);
589
979
  if (job) {
@@ -593,7 +983,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
593
983
  }
594
984
  }
595
985
  async cronRun(jobId, opts) {
596
- const storePath = join2(getDataDir2(), "cron", "jobs.json");
986
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
597
987
  const service = new CronService(storePath);
598
988
  const ok = await service.runJob(jobId, Boolean(opts.force));
599
989
  console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
@@ -604,8 +994,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
604
994
  const workspace = getWorkspacePath(config.agents.defaults.workspace);
605
995
  console.log(`${this.logo} ${APP_NAME} Status
606
996
  `);
607
- console.log(`Config: ${configPath} ${existsSync2(configPath) ? "\u2713" : "\u2717"}`);
608
- console.log(`Workspace: ${workspace} ${existsSync2(workspace) ? "\u2713" : "\u2717"}`);
997
+ console.log(`Config: ${configPath} ${existsSync3(configPath) ? "\u2713" : "\u2717"}`);
998
+ console.log(`Workspace: ${workspace} ${existsSync3(workspace) ? "\u2713" : "\u2717"}`);
609
999
  console.log(`Model: ${config.agents.defaults.model}`);
610
1000
  for (const spec of PROVIDERS) {
611
1001
  const provider = config.providers[spec.name];
@@ -625,33 +1015,36 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
625
1015
  const provider = options.allowMissingProvider === true ? this.makeProvider(config, { allowMissing: true }) : this.makeProvider(config);
626
1016
  const providerManager = provider ? new ProviderManager(provider) : null;
627
1017
  const sessionManager = new SessionManager(getWorkspacePath(config.agents.defaults.workspace));
628
- const cronStorePath = join2(getDataDir2(), "cron", "jobs.json");
1018
+ const cronStorePath = join3(getDataDir2(), "cron", "jobs.json");
629
1019
  const cron2 = new CronService(cronStorePath);
630
1020
  const uiConfig = resolveUiConfig(config, options.uiOverrides);
631
1021
  const uiStaticDir = options.uiStaticDir === void 0 ? resolveUiStaticDir() : options.uiStaticDir;
632
1022
  if (!provider) {
633
- if (uiConfig.enabled) {
634
- const uiServer = startUiServer({
635
- host: uiConfig.host,
636
- port: uiConfig.port,
637
- configPath: getConfigPath(),
638
- staticDir: uiStaticDir ?? void 0
639
- });
640
- const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
641
- console.log(`\u2713 UI API: ${uiUrl}/api`);
642
- if (uiStaticDir) {
643
- console.log(`\u2713 UI frontend: ${uiUrl}`);
644
- }
645
- if (uiConfig.open) {
646
- openBrowser(uiUrl);
647
- }
648
- }
1023
+ this.startUiIfEnabled(uiConfig, uiStaticDir);
649
1024
  console.log("Warning: No API key configured. UI server only.");
650
1025
  await new Promise(() => {
651
1026
  });
652
1027
  return;
653
1028
  }
654
- const gatewayController = {};
1029
+ const channels2 = new ChannelManager(config, bus, sessionManager);
1030
+ const reloader = new ConfigReloader({
1031
+ initialConfig: config,
1032
+ channels: channels2,
1033
+ bus,
1034
+ sessionManager,
1035
+ providerManager,
1036
+ makeProvider: (nextConfig) => this.makeProvider(nextConfig, { allowMissing: true }),
1037
+ loadConfig,
1038
+ onRestartRequired: (paths) => {
1039
+ console.warn(`Config changes require restart: ${paths.join(", ")}`);
1040
+ }
1041
+ });
1042
+ const gatewayController = new GatewayControllerImpl({
1043
+ reloader,
1044
+ cron: cron2,
1045
+ getConfigPath,
1046
+ saveConfig
1047
+ });
655
1048
  const agent = new AgentLoop({
656
1049
  bus,
657
1050
  providerManager: providerManager ?? new ProviderManager(provider),
@@ -690,385 +1083,12 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
690
1083
  30 * 60,
691
1084
  true
692
1085
  );
693
- let currentConfig = config;
694
- let channels2 = new ChannelManager(currentConfig, bus, sessionManager);
695
- let reloadTask = null;
696
- const reloadChannels = async (nextConfig) => {
697
- if (reloadTask) {
698
- await reloadTask;
699
- return;
700
- }
701
- reloadTask = (async () => {
702
- await channels2.stopAll();
703
- channels2 = new ChannelManager(nextConfig, bus, sessionManager);
704
- await channels2.startAll();
705
- })();
706
- try {
707
- await reloadTask;
708
- } finally {
709
- reloadTask = null;
710
- }
711
- };
712
- let providerReloadTask = null;
713
- const reloadProvider = async (nextConfig) => {
714
- if (!providerManager) {
715
- return;
716
- }
717
- if (providerReloadTask) {
718
- await providerReloadTask;
719
- return;
720
- }
721
- providerReloadTask = (async () => {
722
- const nextProvider = this.makeProvider(nextConfig, { allowMissing: true });
723
- if (!nextProvider) {
724
- console.warn("Provider reload skipped: missing API key.");
725
- return;
726
- }
727
- providerManager.set(nextProvider);
728
- })();
729
- try {
730
- await providerReloadTask;
731
- } finally {
732
- providerReloadTask = null;
733
- }
734
- };
735
- const applyReloadPlan = async (nextConfig) => {
736
- const changedPaths = diffConfigPaths(currentConfig, nextConfig);
737
- if (!changedPaths.length) {
738
- return;
739
- }
740
- currentConfig = nextConfig;
741
- const plan = buildReloadPlan(changedPaths);
742
- if (plan.restartChannels) {
743
- await reloadChannels(nextConfig);
744
- }
745
- if (plan.reloadProviders) {
746
- await reloadProvider(nextConfig);
747
- }
748
- if (plan.restartRequired.length > 0) {
749
- console.warn(`Config changes require restart: ${plan.restartRequired.join(", ")}`);
750
- }
751
- };
752
- let reloadTimer = null;
753
- let reloadRunning = false;
754
- let reloadPending = false;
755
- const scheduleConfigReload = (reason) => {
756
- if (reloadTimer) {
757
- clearTimeout(reloadTimer);
758
- }
759
- reloadTimer = setTimeout(() => {
760
- void runConfigReload(reason);
761
- }, 300);
762
- };
763
- const runConfigReload = async (reason) => {
764
- if (reloadRunning) {
765
- reloadPending = true;
766
- return;
767
- }
768
- reloadRunning = true;
769
- if (reloadTimer) {
770
- clearTimeout(reloadTimer);
771
- reloadTimer = null;
772
- }
773
- try {
774
- const nextConfig = loadConfig();
775
- await applyReloadPlan(nextConfig);
776
- } catch (error) {
777
- console.error(`Config reload failed (${reason}): ${String(error)}`);
778
- } finally {
779
- reloadRunning = false;
780
- if (reloadPending) {
781
- reloadPending = false;
782
- scheduleConfigReload("pending");
783
- }
784
- }
785
- };
786
- const hashRaw = (raw) => createHash("sha256").update(raw).digest("hex");
787
- const redactConfig = (value) => {
788
- if (Array.isArray(value)) {
789
- return value.map((entry) => redactConfig(entry));
790
- }
791
- if (!value || typeof value !== "object") {
792
- return value;
793
- }
794
- const entries = value;
795
- const output = {};
796
- for (const [key, val] of Object.entries(entries)) {
797
- if (/apiKey|token|secret|password|appId|clientSecret|accessKey/i.test(key)) {
798
- output[key] = val ? "***" : val;
799
- continue;
800
- }
801
- output[key] = redactConfig(val);
802
- }
803
- return output;
804
- };
805
- const readConfigSnapshot = () => {
806
- const path = getConfigPath();
807
- let raw = "";
808
- let parsed = {};
809
- if (existsSync2(path)) {
810
- raw = readFileSync2(path, "utf-8");
811
- try {
812
- parsed = JSON.parse(raw);
813
- } catch {
814
- parsed = {};
815
- }
816
- }
817
- let config2;
818
- let valid = true;
819
- try {
820
- config2 = ConfigSchema.parse(parsed);
821
- } catch {
822
- config2 = ConfigSchema.parse({});
823
- valid = false;
824
- }
825
- if (!raw) {
826
- raw = JSON.stringify(config2, null, 2);
827
- }
828
- const hash = hashRaw(raw);
829
- const redacted = redactConfig(config2);
830
- return { raw: valid ? JSON.stringify(redacted, null, 2) : null, hash: valid ? hash : null, config: config2, redacted, valid };
831
- };
832
- const mergeDeep = (base, patch) => {
833
- const next = { ...base };
834
- for (const [key, value] of Object.entries(patch)) {
835
- if (value && typeof value === "object" && !Array.isArray(value)) {
836
- const baseVal = base[key];
837
- if (baseVal && typeof baseVal === "object" && !Array.isArray(baseVal)) {
838
- next[key] = mergeDeep(baseVal, value);
839
- } else {
840
- next[key] = mergeDeep({}, value);
841
- }
842
- } else {
843
- next[key] = value;
844
- }
845
- }
846
- return next;
847
- };
848
- const buildSchemaFromValue = (value) => {
849
- if (Array.isArray(value)) {
850
- const item = value.length ? buildSchemaFromValue(value[0]) : { type: "string" };
851
- return { type: "array", items: item };
852
- }
853
- if (value && typeof value === "object") {
854
- const props = {};
855
- for (const [key, val] of Object.entries(value)) {
856
- props[key] = buildSchemaFromValue(val);
857
- }
858
- return { type: "object", properties: props };
859
- }
860
- if (typeof value === "number") {
861
- return { type: "number" };
862
- }
863
- if (typeof value === "boolean") {
864
- return { type: "boolean" };
865
- }
866
- if (value === null) {
867
- return { type: ["null", "string"] };
868
- }
869
- return { type: "string" };
870
- };
871
- const scheduleRestart = (delayMs, reason) => {
872
- const delay = typeof delayMs === "number" && Number.isFinite(delayMs) ? Math.max(0, delayMs) : 100;
873
- console.log(`Gateway restart requested via tool${reason ? ` (${reason})` : ""}.`);
874
- setTimeout(() => {
875
- process.exit(0);
876
- }, delay);
877
- };
878
- gatewayController.status = () => ({
879
- channels: channels2.enabledChannels,
880
- cron: cron2.status(),
881
- configPath: getConfigPath()
882
- });
883
- gatewayController.reloadConfig = async (reason) => {
884
- await runConfigReload(reason ?? "gateway tool");
885
- return "Config reload triggered";
886
- };
887
- gatewayController.restart = async (options2) => {
888
- scheduleRestart(options2?.delayMs, options2?.reason);
889
- return "Restart scheduled";
890
- };
891
- gatewayController.getConfig = async () => {
892
- const snapshot = readConfigSnapshot();
893
- return {
894
- raw: snapshot.raw,
895
- hash: snapshot.hash,
896
- path: getConfigPath(),
897
- config: snapshot.redacted,
898
- parsed: snapshot.redacted,
899
- resolved: snapshot.redacted,
900
- valid: snapshot.valid
901
- };
902
- };
903
- gatewayController.getConfigSchema = async () => {
904
- const base = ConfigSchema.parse({});
905
- return {
906
- schema: {
907
- ...buildSchemaFromValue(base),
908
- title: "NextClawConfig",
909
- description: "NextClaw config schema (simplified)"
910
- },
911
- uiHints: {},
912
- version: getPackageVersion(),
913
- generatedAt: (/* @__PURE__ */ new Date()).toISOString()
914
- };
915
- };
916
- gatewayController.applyConfig = async (params) => {
917
- const snapshot = readConfigSnapshot();
918
- if (!params.baseHash) {
919
- return { ok: false, error: "config base hash required; re-run config.get and retry" };
920
- }
921
- if (!snapshot.valid || !snapshot.hash) {
922
- return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
923
- }
924
- if (params.baseHash !== snapshot.hash) {
925
- return { ok: false, error: "config changed since last load; re-run config.get and retry" };
926
- }
927
- let parsedRaw;
928
- try {
929
- parsedRaw = JSON.parse(params.raw);
930
- } catch {
931
- return { ok: false, error: "invalid JSON in raw config" };
932
- }
933
- let validated;
934
- try {
935
- validated = ConfigSchema.parse(parsedRaw);
936
- } catch (err) {
937
- return { ok: false, error: `invalid config: ${String(err)}` };
938
- }
939
- saveConfig(validated);
940
- const delayMs = params.restartDelayMs ?? 0;
941
- scheduleRestart(delayMs, "config.apply");
942
- return {
943
- ok: true,
944
- note: params.note ?? null,
945
- path: getConfigPath(),
946
- config: redactConfig(validated),
947
- restart: { scheduled: true, delayMs }
948
- };
949
- };
950
- gatewayController.patchConfig = async (params) => {
951
- const snapshot = readConfigSnapshot();
952
- if (!params.baseHash) {
953
- return { ok: false, error: "config base hash required; re-run config.get and retry" };
954
- }
955
- if (!snapshot.valid || !snapshot.hash) {
956
- return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
957
- }
958
- if (params.baseHash !== snapshot.hash) {
959
- return { ok: false, error: "config changed since last load; re-run config.get and retry" };
960
- }
961
- let patch;
962
- try {
963
- patch = JSON.parse(params.raw);
964
- } catch {
965
- return { ok: false, error: "invalid JSON in raw config" };
966
- }
967
- const merged = mergeDeep(snapshot.config, patch);
968
- let validated;
969
- try {
970
- validated = ConfigSchema.parse(merged);
971
- } catch (err) {
972
- return { ok: false, error: `invalid config: ${String(err)}` };
973
- }
974
- saveConfig(validated);
975
- const delayMs = params.restartDelayMs ?? 0;
976
- scheduleRestart(delayMs, "config.patch");
977
- return {
978
- ok: true,
979
- note: params.note ?? null,
980
- path: getConfigPath(),
981
- config: redactConfig(validated),
982
- restart: { scheduled: true, delayMs }
983
- };
984
- };
985
- gatewayController.updateRun = async (params) => {
986
- const timeoutMs = params.timeoutMs ?? 20 * 6e4;
987
- const cliDir = resolve2(fileURLToPath2(new URL(".", import.meta.url)));
988
- const pkgRoot = resolve2(cliDir, "..", "..");
989
- const repoRoot = existsSync2(join2(pkgRoot, ".git")) ? pkgRoot : resolve2(pkgRoot, "..", "..");
990
- const steps = [];
991
- const runStep = (cmd, args, cwd) => {
992
- const result = spawnSync(cmd, args, {
993
- cwd,
994
- encoding: "utf-8",
995
- timeout: timeoutMs,
996
- stdio: "pipe"
997
- });
998
- const step = {
999
- cmd,
1000
- args,
1001
- cwd,
1002
- code: result.status,
1003
- stdout: (result.stdout ?? "").toString().slice(0, 4e3),
1004
- stderr: (result.stderr ?? "").toString().slice(0, 4e3)
1005
- };
1006
- steps.push(step);
1007
- return { ok: result.status === 0, code: result.status };
1008
- };
1009
- const updateCommand = process.env.NEXTCLAW_UPDATE_COMMAND?.trim();
1010
- if (updateCommand) {
1011
- const ok = runStep("sh", ["-c", updateCommand], process.cwd());
1012
- if (!ok.ok) {
1013
- return { ok: false, error: "update command failed", steps };
1014
- }
1015
- } else if (existsSync2(join2(repoRoot, ".git"))) {
1016
- if (!which("git")) {
1017
- return { ok: false, error: "git not found for repo update", steps };
1018
- }
1019
- const ok = runStep("git", ["-C", repoRoot, "pull", "--rebase"], repoRoot);
1020
- if (!ok.ok) {
1021
- return { ok: false, error: "git pull failed", steps };
1022
- }
1023
- if (existsSync2(join2(repoRoot, "pnpm-lock.yaml")) && which("pnpm")) {
1024
- const installOk = runStep("pnpm", ["install"], repoRoot);
1025
- if (!installOk.ok) {
1026
- return { ok: false, error: "pnpm install failed", steps };
1027
- }
1028
- } else if (existsSync2(join2(repoRoot, "package.json")) && which("npm")) {
1029
- const installOk = runStep("npm", ["install"], repoRoot);
1030
- if (!installOk.ok) {
1031
- return { ok: false, error: "npm install failed", steps };
1032
- }
1033
- }
1034
- } else if (which("npm")) {
1035
- const ok = runStep("npm", ["i", "-g", "nextclaw"], process.cwd());
1036
- if (!ok.ok) {
1037
- return { ok: false, error: "npm install -g nextclaw failed", steps };
1038
- }
1039
- } else {
1040
- return { ok: false, error: "no update strategy available", steps };
1041
- }
1042
- const delayMs = params.restartDelayMs ?? 0;
1043
- scheduleRestart(delayMs, "update.run");
1044
- return {
1045
- ok: true,
1046
- note: params.note ?? null,
1047
- restart: { scheduled: true, delayMs },
1048
- steps
1049
- };
1050
- };
1051
- if (channels2.enabledChannels.length) {
1052
- console.log(`\u2713 Channels enabled: ${channels2.enabledChannels.join(", ")}`);
1086
+ if (reloader.getChannels().enabledChannels.length) {
1087
+ console.log(`\u2713 Channels enabled: ${reloader.getChannels().enabledChannels.join(", ")}`);
1053
1088
  } else {
1054
1089
  console.log("Warning: No channels enabled");
1055
1090
  }
1056
- if (uiConfig.enabled) {
1057
- const uiServer = startUiServer({
1058
- host: uiConfig.host,
1059
- port: uiConfig.port,
1060
- configPath: getConfigPath(),
1061
- staticDir: uiStaticDir ?? void 0
1062
- });
1063
- const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
1064
- console.log(`\u2713 UI API: ${uiUrl}/api`);
1065
- if (uiStaticDir) {
1066
- console.log(`\u2713 UI frontend: ${uiUrl}`);
1067
- }
1068
- if (uiConfig.open) {
1069
- openBrowser(uiUrl);
1070
- }
1071
- }
1091
+ this.startUiIfEnabled(uiConfig, uiStaticDir);
1072
1092
  const cronStatus = cron2.status();
1073
1093
  if (cronStatus.jobs > 0) {
1074
1094
  console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
@@ -1079,12 +1099,31 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1079
1099
  ignoreInitial: true,
1080
1100
  awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
1081
1101
  });
1082
- watcher.on("add", () => scheduleConfigReload("config add"));
1083
- watcher.on("change", () => scheduleConfigReload("config change"));
1084
- watcher.on("unlink", () => scheduleConfigReload("config unlink"));
1102
+ watcher.on("add", () => reloader.scheduleReload("config add"));
1103
+ watcher.on("change", () => reloader.scheduleReload("config change"));
1104
+ watcher.on("unlink", () => reloader.scheduleReload("config unlink"));
1085
1105
  await cron2.start();
1086
1106
  await heartbeat.start();
1087
- await Promise.allSettled([agent.run(), channels2.startAll()]);
1107
+ await Promise.allSettled([agent.run(), reloader.getChannels().startAll()]);
1108
+ }
1109
+ startUiIfEnabled(uiConfig, uiStaticDir) {
1110
+ if (!uiConfig.enabled) {
1111
+ return;
1112
+ }
1113
+ const uiServer = startUiServer({
1114
+ host: uiConfig.host,
1115
+ port: uiConfig.port,
1116
+ configPath: getConfigPath(),
1117
+ staticDir: uiStaticDir ?? void 0
1118
+ });
1119
+ const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
1120
+ console.log(`\u2713 UI API: ${uiUrl}/api`);
1121
+ if (uiStaticDir) {
1122
+ console.log(`\u2713 UI frontend: ${uiUrl}`);
1123
+ }
1124
+ if (uiConfig.open) {
1125
+ openBrowser(uiUrl);
1126
+ }
1088
1127
  }
1089
1128
  async runForeground(options) {
1090
1129
  const config = loadConfig();
@@ -1141,7 +1180,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1141
1180
  console.log("Warning: UI frontend not found. Use --frontend to start the dev server.");
1142
1181
  }
1143
1182
  const logPath = resolveServiceLogPath();
1144
- const logDir = resolve2(logPath, "..");
1183
+ const logDir = resolve3(logPath, "..");
1145
1184
  mkdirSync2(logDir, { recursive: true });
1146
1185
  const logFd = openSync(logPath, "a");
1147
1186
  const serveArgs = buildServeArgs({
@@ -1250,30 +1289,30 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1250
1289
  { source: "memory/MEMORY.md", target: "memory/MEMORY.md" }
1251
1290
  ];
1252
1291
  for (const entry of templateFiles) {
1253
- const filePath = join2(workspace, entry.target);
1254
- if (!force && existsSync2(filePath)) {
1292
+ const filePath = join3(workspace, entry.target);
1293
+ if (!force && existsSync3(filePath)) {
1255
1294
  continue;
1256
1295
  }
1257
- const templatePath = join2(templateDir, entry.source);
1258
- if (!existsSync2(templatePath)) {
1296
+ const templatePath = join3(templateDir, entry.source);
1297
+ if (!existsSync3(templatePath)) {
1259
1298
  console.warn(`Warning: Template file missing: ${templatePath}`);
1260
1299
  continue;
1261
1300
  }
1262
- const raw = readFileSync2(templatePath, "utf-8");
1301
+ const raw = readFileSync3(templatePath, "utf-8");
1263
1302
  const content = raw.replace(/\$\{APP_NAME\}/g, APP_NAME);
1264
1303
  mkdirSync2(dirname(filePath), { recursive: true });
1265
1304
  writeFileSync2(filePath, content);
1266
1305
  created.push(entry.target);
1267
1306
  }
1268
- const memoryDir = join2(workspace, "memory");
1269
- if (!existsSync2(memoryDir)) {
1307
+ const memoryDir = join3(workspace, "memory");
1308
+ if (!existsSync3(memoryDir)) {
1270
1309
  mkdirSync2(memoryDir, { recursive: true });
1271
- created.push(join2("memory", ""));
1310
+ created.push(join3("memory", ""));
1272
1311
  }
1273
- const skillsDir = join2(workspace, "skills");
1274
- if (!existsSync2(skillsDir)) {
1312
+ const skillsDir = join3(workspace, "skills");
1313
+ if (!existsSync3(skillsDir)) {
1275
1314
  mkdirSync2(skillsDir, { recursive: true });
1276
- created.push(join2("skills", ""));
1315
+ created.push(join3("skills", ""));
1277
1316
  }
1278
1317
  return { created };
1279
1318
  }
@@ -1282,33 +1321,33 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1282
1321
  if (override) {
1283
1322
  return override;
1284
1323
  }
1285
- const cliDir = resolve2(fileURLToPath2(new URL(".", import.meta.url)));
1286
- const pkgRoot = resolve2(cliDir, "..", "..");
1287
- const candidates = [join2(pkgRoot, "templates")];
1324
+ const cliDir = resolve3(fileURLToPath3(new URL(".", import.meta.url)));
1325
+ const pkgRoot = resolve3(cliDir, "..", "..");
1326
+ const candidates = [join3(pkgRoot, "templates")];
1288
1327
  for (const candidate of candidates) {
1289
- if (existsSync2(candidate)) {
1328
+ if (existsSync3(candidate)) {
1290
1329
  return candidate;
1291
1330
  }
1292
1331
  }
1293
1332
  return null;
1294
1333
  }
1295
1334
  getBridgeDir() {
1296
- const userBridge = join2(getDataDir2(), "bridge");
1297
- if (existsSync2(join2(userBridge, "dist", "index.js"))) {
1335
+ const userBridge = join3(getDataDir2(), "bridge");
1336
+ if (existsSync3(join3(userBridge, "dist", "index.js"))) {
1298
1337
  return userBridge;
1299
1338
  }
1300
1339
  if (!which("npm")) {
1301
1340
  console.error("npm not found. Please install Node.js >= 18.");
1302
1341
  process.exit(1);
1303
1342
  }
1304
- const cliDir = resolve2(fileURLToPath2(new URL(".", import.meta.url)));
1305
- const pkgRoot = resolve2(cliDir, "..", "..");
1306
- const pkgBridge = join2(pkgRoot, "bridge");
1307
- const srcBridge = join2(pkgRoot, "..", "..", "bridge");
1343
+ const cliDir = resolve3(fileURLToPath3(new URL(".", import.meta.url)));
1344
+ const pkgRoot = resolve3(cliDir, "..", "..");
1345
+ const pkgBridge = join3(pkgRoot, "bridge");
1346
+ const srcBridge = join3(pkgRoot, "..", "..", "bridge");
1308
1347
  let source = null;
1309
- if (existsSync2(join2(pkgBridge, "package.json"))) {
1348
+ if (existsSync3(join3(pkgBridge, "package.json"))) {
1310
1349
  source = pkgBridge;
1311
- } else if (existsSync2(join2(srcBridge, "package.json"))) {
1350
+ } else if (existsSync3(join3(srcBridge, "package.json"))) {
1312
1351
  source = srcBridge;
1313
1352
  }
1314
1353
  if (!source) {
@@ -1316,15 +1355,15 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1316
1355
  process.exit(1);
1317
1356
  }
1318
1357
  console.log(`${this.logo} Setting up bridge...`);
1319
- mkdirSync2(resolve2(userBridge, ".."), { recursive: true });
1320
- if (existsSync2(userBridge)) {
1358
+ mkdirSync2(resolve3(userBridge, ".."), { recursive: true });
1359
+ if (existsSync3(userBridge)) {
1321
1360
  rmSync2(userBridge, { recursive: true, force: true });
1322
1361
  }
1323
1362
  cpSync(source, userBridge, {
1324
1363
  recursive: true,
1325
1364
  filter: (src) => !src.includes("node_modules") && !src.includes("dist")
1326
1365
  });
1327
- const install = spawnSync("npm", ["install"], { cwd: userBridge, stdio: "pipe" });
1366
+ const install = spawnSync2("npm", ["install"], { cwd: userBridge, stdio: "pipe" });
1328
1367
  if (install.status !== 0) {
1329
1368
  console.error(`Bridge install failed: ${install.status ?? 1}`);
1330
1369
  if (install.stderr) {
@@ -1332,7 +1371,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1332
1371
  }
1333
1372
  process.exit(1);
1334
1373
  }
1335
- const build = spawnSync("npm", ["run", "build"], { cwd: userBridge, stdio: "pipe" });
1374
+ const build = spawnSync2("npm", ["run", "build"], { cwd: userBridge, stdio: "pipe" });
1336
1375
  if (build.status !== 0) {
1337
1376
  console.error(`Bridge build failed: ${build.status ?? 1}`);
1338
1377
  if (build.stderr) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextclaw",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Lightweight personal AI assistant with CLI, multi-provider routing, and channel integrations.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -38,8 +38,8 @@
38
38
  "dependencies": {
39
39
  "chokidar": "^3.6.0",
40
40
  "commander": "^12.1.0",
41
- "nextclaw-core": "^0.4.0",
42
- "nextclaw-server": "^0.3.1"
41
+ "nextclaw-core": "^0.4.1",
42
+ "nextclaw-server": "^0.3.2"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^20.17.6",