nextclaw 0.4.0 → 0.4.2

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 +545 -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,31 @@ 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
+ readdirSync,
42
+ readFileSync as readFileSync3,
42
43
  rmSync as rmSync2,
43
44
  writeFileSync as writeFileSync2
44
45
  } 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";
46
+ import { dirname, join as join3, resolve as resolve3 } from "path";
47
+ import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
48
48
  import { createInterface } from "readline";
49
- import { fileURLToPath as fileURLToPath2 } from "url";
49
+ import { createRequire } from "module";
50
+ import { fileURLToPath as fileURLToPath3 } from "url";
50
51
  import chokidar from "chokidar";
51
52
 
53
+ // src/cli/gateway/controller.ts
54
+ import { createHash } from "crypto";
55
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
56
+ import { spawnSync } from "child_process";
57
+ import { fileURLToPath as fileURLToPath2 } from "url";
58
+ import { join as join2, resolve as resolve2 } from "path";
59
+ import {
60
+ ConfigSchema
61
+ } from "nextclaw-core";
62
+
52
63
  // src/cli/utils.ts
53
64
  import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "fs";
54
65
  import { join, resolve } from "path";
@@ -87,12 +98,12 @@ async function isPortAvailable(port, host) {
87
98
  return await canBindPort(port, checkHost);
88
99
  }
89
100
  async function canBindPort(port, host) {
90
- return await new Promise((resolve3) => {
101
+ return await new Promise((resolve4) => {
91
102
  const server = createServer();
92
103
  server.unref();
93
- server.once("error", () => resolve3(false));
104
+ server.once("error", () => resolve4(false));
94
105
  server.listen({ port, host }, () => {
95
- server.close(() => resolve3(true));
106
+ server.close(() => resolve4(true));
96
107
  });
97
108
  });
98
109
  }
@@ -150,7 +161,7 @@ async function waitForExit(pid, timeoutMs) {
150
161
  if (!isProcessRunning(pid)) {
151
162
  return true;
152
163
  }
153
- await new Promise((resolve3) => setTimeout(resolve3, 200));
164
+ await new Promise((resolve4) => setTimeout(resolve4, 200));
154
165
  }
155
166
  return !isProcessRunning(pid);
156
167
  }
@@ -281,14 +292,395 @@ function printAgentResponse(response) {
281
292
  async function prompt(rl, question) {
282
293
  rl.setPrompt(question);
283
294
  rl.prompt();
284
- return new Promise((resolve3) => {
285
- rl.once("line", (line) => resolve3(line));
295
+ return new Promise((resolve4) => {
296
+ rl.once("line", (line) => resolve4(line));
286
297
  });
287
298
  }
288
299
 
300
+ // src/cli/gateway/controller.ts
301
+ var hashRaw = (raw) => createHash("sha256").update(raw).digest("hex");
302
+ var redactConfig = (value) => {
303
+ if (Array.isArray(value)) {
304
+ return value.map((entry) => redactConfig(entry));
305
+ }
306
+ if (!value || typeof value !== "object") {
307
+ return value;
308
+ }
309
+ const entries = value;
310
+ const output = {};
311
+ for (const [key, val] of Object.entries(entries)) {
312
+ if (/apiKey|token|secret|password|appId|clientSecret|accessKey/i.test(key)) {
313
+ output[key] = val ? "***" : val;
314
+ continue;
315
+ }
316
+ output[key] = redactConfig(val);
317
+ }
318
+ return output;
319
+ };
320
+ var readConfigSnapshot = (getConfigPath2) => {
321
+ const path = getConfigPath2();
322
+ let raw = "";
323
+ let parsed = {};
324
+ if (existsSync2(path)) {
325
+ raw = readFileSync2(path, "utf-8");
326
+ try {
327
+ parsed = JSON.parse(raw);
328
+ } catch {
329
+ parsed = {};
330
+ }
331
+ }
332
+ let config;
333
+ let valid = true;
334
+ try {
335
+ config = ConfigSchema.parse(parsed);
336
+ } catch {
337
+ config = ConfigSchema.parse({});
338
+ valid = false;
339
+ }
340
+ if (!raw) {
341
+ raw = JSON.stringify(config, null, 2);
342
+ }
343
+ const hash = hashRaw(raw);
344
+ const redacted = redactConfig(config);
345
+ return { raw: valid ? JSON.stringify(redacted, null, 2) : null, hash: valid ? hash : null, config, redacted, valid };
346
+ };
347
+ var mergeDeep = (base, patch) => {
348
+ const next = { ...base };
349
+ for (const [key, value] of Object.entries(patch)) {
350
+ if (value && typeof value === "object" && !Array.isArray(value)) {
351
+ const baseVal = base[key];
352
+ if (baseVal && typeof baseVal === "object" && !Array.isArray(baseVal)) {
353
+ next[key] = mergeDeep(baseVal, value);
354
+ } else {
355
+ next[key] = mergeDeep({}, value);
356
+ }
357
+ } else {
358
+ next[key] = value;
359
+ }
360
+ }
361
+ return next;
362
+ };
363
+ var buildSchemaFromValue = (value) => {
364
+ if (Array.isArray(value)) {
365
+ const item = value.length ? buildSchemaFromValue(value[0]) : { type: "string" };
366
+ return { type: "array", items: item };
367
+ }
368
+ if (value && typeof value === "object") {
369
+ const props = {};
370
+ for (const [key, val] of Object.entries(value)) {
371
+ props[key] = buildSchemaFromValue(val);
372
+ }
373
+ return { type: "object", properties: props };
374
+ }
375
+ if (typeof value === "number") {
376
+ return { type: "number" };
377
+ }
378
+ if (typeof value === "boolean") {
379
+ return { type: "boolean" };
380
+ }
381
+ if (value === null) {
382
+ return { type: ["null", "string"] };
383
+ }
384
+ return { type: "string" };
385
+ };
386
+ var scheduleRestart = (delayMs, reason) => {
387
+ const delay = typeof delayMs === "number" && Number.isFinite(delayMs) ? Math.max(0, delayMs) : 100;
388
+ console.log(`Gateway restart requested via tool${reason ? ` (${reason})` : ""}.`);
389
+ setTimeout(() => {
390
+ process.exit(0);
391
+ }, delay);
392
+ };
393
+ var GatewayControllerImpl = class {
394
+ constructor(deps) {
395
+ this.deps = deps;
396
+ }
397
+ status() {
398
+ return {
399
+ channels: this.deps.reloader.getChannels().enabledChannels,
400
+ cron: this.deps.cron.status(),
401
+ configPath: this.deps.getConfigPath()
402
+ };
403
+ }
404
+ async reloadConfig(reason) {
405
+ return this.deps.reloader.reloadConfig(reason);
406
+ }
407
+ async restart(options) {
408
+ scheduleRestart(options?.delayMs, options?.reason);
409
+ return "Restart scheduled";
410
+ }
411
+ async getConfig() {
412
+ const snapshot = readConfigSnapshot(this.deps.getConfigPath);
413
+ return {
414
+ raw: snapshot.raw,
415
+ hash: snapshot.hash,
416
+ path: this.deps.getConfigPath(),
417
+ config: snapshot.redacted,
418
+ parsed: snapshot.redacted,
419
+ resolved: snapshot.redacted,
420
+ valid: snapshot.valid
421
+ };
422
+ }
423
+ async getConfigSchema() {
424
+ const base = ConfigSchema.parse({});
425
+ return {
426
+ schema: {
427
+ ...buildSchemaFromValue(base),
428
+ title: "NextClawConfig",
429
+ description: "NextClaw config schema (simplified)"
430
+ },
431
+ uiHints: {},
432
+ version: getPackageVersion(),
433
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
434
+ };
435
+ }
436
+ async applyConfig(params) {
437
+ const snapshot = readConfigSnapshot(this.deps.getConfigPath);
438
+ if (!params.baseHash) {
439
+ return { ok: false, error: "config base hash required; re-run config.get and retry" };
440
+ }
441
+ if (!snapshot.valid || !snapshot.hash) {
442
+ return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
443
+ }
444
+ if (params.baseHash !== snapshot.hash) {
445
+ return { ok: false, error: "config changed since last load; re-run config.get and retry" };
446
+ }
447
+ let parsedRaw;
448
+ try {
449
+ parsedRaw = JSON.parse(params.raw);
450
+ } catch {
451
+ return { ok: false, error: "invalid JSON in raw config" };
452
+ }
453
+ let validated;
454
+ try {
455
+ validated = ConfigSchema.parse(parsedRaw);
456
+ } catch (err) {
457
+ return { ok: false, error: `invalid config: ${String(err)}` };
458
+ }
459
+ this.deps.saveConfig(validated);
460
+ const delayMs = params.restartDelayMs ?? 0;
461
+ scheduleRestart(delayMs, "config.apply");
462
+ return {
463
+ ok: true,
464
+ note: params.note ?? null,
465
+ path: this.deps.getConfigPath(),
466
+ config: redactConfig(validated),
467
+ restart: { scheduled: true, delayMs }
468
+ };
469
+ }
470
+ async patchConfig(params) {
471
+ const snapshot = readConfigSnapshot(this.deps.getConfigPath);
472
+ if (!params.baseHash) {
473
+ return { ok: false, error: "config base hash required; re-run config.get and retry" };
474
+ }
475
+ if (!snapshot.valid || !snapshot.hash) {
476
+ return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
477
+ }
478
+ if (params.baseHash !== snapshot.hash) {
479
+ return { ok: false, error: "config changed since last load; re-run config.get and retry" };
480
+ }
481
+ let patch;
482
+ try {
483
+ patch = JSON.parse(params.raw);
484
+ } catch {
485
+ return { ok: false, error: "invalid JSON in raw config" };
486
+ }
487
+ const merged = mergeDeep(snapshot.config, patch);
488
+ let validated;
489
+ try {
490
+ validated = ConfigSchema.parse(merged);
491
+ } catch (err) {
492
+ return { ok: false, error: `invalid config: ${String(err)}` };
493
+ }
494
+ this.deps.saveConfig(validated);
495
+ const delayMs = params.restartDelayMs ?? 0;
496
+ scheduleRestart(delayMs, "config.patch");
497
+ return {
498
+ ok: true,
499
+ note: params.note ?? null,
500
+ path: this.deps.getConfigPath(),
501
+ config: redactConfig(validated),
502
+ restart: { scheduled: true, delayMs }
503
+ };
504
+ }
505
+ async updateRun(params) {
506
+ const timeoutMs = params.timeoutMs ?? 20 * 6e4;
507
+ const gatewayDir = resolve2(fileURLToPath2(new URL(".", import.meta.url)));
508
+ const cliDir = resolve2(gatewayDir, "..");
509
+ const pkgRoot = resolve2(cliDir, "..", "..");
510
+ const repoRoot = existsSync2(join2(pkgRoot, ".git")) ? pkgRoot : resolve2(pkgRoot, "..", "..");
511
+ const steps = [];
512
+ const runStep = (cmd, args, cwd) => {
513
+ const result = spawnSync(cmd, args, {
514
+ cwd,
515
+ encoding: "utf-8",
516
+ timeout: timeoutMs,
517
+ stdio: "pipe"
518
+ });
519
+ const step = {
520
+ cmd,
521
+ args,
522
+ cwd,
523
+ code: result.status,
524
+ stdout: (result.stdout ?? "").toString().slice(0, 4e3),
525
+ stderr: (result.stderr ?? "").toString().slice(0, 4e3)
526
+ };
527
+ steps.push(step);
528
+ return { ok: result.status === 0, code: result.status };
529
+ };
530
+ const updateCommand = process.env.NEXTCLAW_UPDATE_COMMAND?.trim();
531
+ if (updateCommand) {
532
+ const ok = runStep("sh", ["-c", updateCommand], process.cwd());
533
+ if (!ok.ok) {
534
+ return { ok: false, error: "update command failed", steps };
535
+ }
536
+ } else if (existsSync2(join2(repoRoot, ".git"))) {
537
+ if (!which("git")) {
538
+ return { ok: false, error: "git not found for repo update", steps };
539
+ }
540
+ const ok = runStep("git", ["-C", repoRoot, "pull", "--rebase"], repoRoot);
541
+ if (!ok.ok) {
542
+ return { ok: false, error: "git pull failed", steps };
543
+ }
544
+ if (existsSync2(join2(repoRoot, "pnpm-lock.yaml")) && which("pnpm")) {
545
+ const installOk = runStep("pnpm", ["install"], repoRoot);
546
+ if (!installOk.ok) {
547
+ return { ok: false, error: "pnpm install failed", steps };
548
+ }
549
+ } else if (existsSync2(join2(repoRoot, "package.json")) && which("npm")) {
550
+ const installOk = runStep("npm", ["install"], repoRoot);
551
+ if (!installOk.ok) {
552
+ return { ok: false, error: "npm install failed", steps };
553
+ }
554
+ }
555
+ } else if (which("npm")) {
556
+ const ok = runStep("npm", ["i", "-g", "nextclaw"], process.cwd());
557
+ if (!ok.ok) {
558
+ return { ok: false, error: "npm install -g nextclaw failed", steps };
559
+ }
560
+ } else {
561
+ return { ok: false, error: "no update strategy available", steps };
562
+ }
563
+ const delayMs = params.restartDelayMs ?? 0;
564
+ scheduleRestart(delayMs, "update.run");
565
+ return {
566
+ ok: true,
567
+ note: params.note ?? null,
568
+ restart: { scheduled: true, delayMs },
569
+ steps
570
+ };
571
+ }
572
+ };
573
+
289
574
  // src/cli/runtime.ts
290
575
  var LOGO = "\u{1F916}";
291
576
  var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "/exit", "/quit", ":q"]);
577
+ var ConfigReloader = class {
578
+ constructor(options) {
579
+ this.options = options;
580
+ this.currentConfig = options.initialConfig;
581
+ this.channels = options.channels;
582
+ }
583
+ currentConfig;
584
+ channels;
585
+ reloadTask = null;
586
+ providerReloadTask = null;
587
+ reloadTimer = null;
588
+ reloadRunning = false;
589
+ reloadPending = false;
590
+ getChannels() {
591
+ return this.channels;
592
+ }
593
+ async applyReloadPlan(nextConfig) {
594
+ const changedPaths = diffConfigPaths(this.currentConfig, nextConfig);
595
+ if (!changedPaths.length) {
596
+ return;
597
+ }
598
+ this.currentConfig = nextConfig;
599
+ const plan = buildReloadPlan(changedPaths);
600
+ if (plan.restartChannels) {
601
+ await this.reloadChannels(nextConfig);
602
+ }
603
+ if (plan.reloadProviders) {
604
+ await this.reloadProvider(nextConfig);
605
+ }
606
+ if (plan.restartRequired.length > 0) {
607
+ this.options.onRestartRequired(plan.restartRequired);
608
+ }
609
+ }
610
+ scheduleReload(reason) {
611
+ if (this.reloadTimer) {
612
+ clearTimeout(this.reloadTimer);
613
+ }
614
+ this.reloadTimer = setTimeout(() => {
615
+ void this.runReload(reason);
616
+ }, 300);
617
+ }
618
+ async runReload(reason) {
619
+ if (this.reloadRunning) {
620
+ this.reloadPending = true;
621
+ return;
622
+ }
623
+ this.reloadRunning = true;
624
+ if (this.reloadTimer) {
625
+ clearTimeout(this.reloadTimer);
626
+ this.reloadTimer = null;
627
+ }
628
+ try {
629
+ const nextConfig = this.options.loadConfig();
630
+ await this.applyReloadPlan(nextConfig);
631
+ } catch (error) {
632
+ console.error(`Config reload failed (${reason}): ${String(error)}`);
633
+ } finally {
634
+ this.reloadRunning = false;
635
+ if (this.reloadPending) {
636
+ this.reloadPending = false;
637
+ this.scheduleReload("pending");
638
+ }
639
+ }
640
+ }
641
+ async reloadConfig(reason) {
642
+ await this.runReload(reason ?? "gateway tool");
643
+ return "Config reload triggered";
644
+ }
645
+ async reloadChannels(nextConfig) {
646
+ if (this.reloadTask) {
647
+ await this.reloadTask;
648
+ return;
649
+ }
650
+ this.reloadTask = (async () => {
651
+ await this.channels.stopAll();
652
+ this.channels = new ChannelManager(nextConfig, this.options.bus, this.options.sessionManager);
653
+ await this.channels.startAll();
654
+ })();
655
+ try {
656
+ await this.reloadTask;
657
+ } finally {
658
+ this.reloadTask = null;
659
+ }
660
+ }
661
+ async reloadProvider(nextConfig) {
662
+ if (!this.options.providerManager) {
663
+ return;
664
+ }
665
+ if (this.providerReloadTask) {
666
+ await this.providerReloadTask;
667
+ return;
668
+ }
669
+ this.providerReloadTask = (async () => {
670
+ const nextProvider = this.options.makeProvider(nextConfig);
671
+ if (!nextProvider) {
672
+ console.warn("Provider reload skipped: missing API key.");
673
+ return;
674
+ }
675
+ this.options.providerManager?.set(nextProvider);
676
+ })();
677
+ try {
678
+ await this.providerReloadTask;
679
+ } finally {
680
+ this.providerReloadTask = null;
681
+ }
682
+ }
683
+ };
292
684
  var CliRuntime = class {
293
685
  logo;
294
686
  constructor(options = {}) {
@@ -307,15 +699,15 @@ var CliRuntime = class {
307
699
  const force = Boolean(options.force);
308
700
  const configPath = getConfigPath();
309
701
  let createdConfig = false;
310
- if (!existsSync2(configPath)) {
311
- const config2 = ConfigSchema.parse({});
702
+ if (!existsSync3(configPath)) {
703
+ const config2 = ConfigSchema2.parse({});
312
704
  saveConfig(config2);
313
705
  createdConfig = true;
314
706
  }
315
707
  const config = loadConfig();
316
708
  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);
709
+ const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ? join3(getDataDir2(), DEFAULT_WORKSPACE_DIR) : expandHome(workspaceSetting);
710
+ const workspaceExisted = existsSync3(workspacePath);
319
711
  mkdirSync2(workspacePath, { recursive: true });
320
712
  const templateResult = this.createWorkspaceTemplates(workspacePath, { force });
321
713
  if (createdConfig) {
@@ -479,10 +871,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
479
871
  }
480
872
  console.log(`${this.logo} Interactive mode (type exit or Ctrl+C to quit)
481
873
  `);
482
- const historyFile = join2(getDataDir2(), "history", "cli_history");
483
- const historyDir = resolve2(historyFile, "..");
874
+ const historyFile = join3(getDataDir2(), "history", "cli_history");
875
+ const historyDir = resolve3(historyFile, "..");
484
876
  mkdirSync2(historyDir, { recursive: true });
485
- const history = existsSync2(historyFile) ? readFileSync2(historyFile, "utf-8").split("\n").filter(Boolean) : [];
877
+ const history = existsSync3(historyFile) ? readFileSync3(historyFile, "utf-8").split("\n").filter(Boolean) : [];
486
878
  const rl = createInterface({ input: process.stdin, output: process.stdout });
487
879
  rl.on("close", () => {
488
880
  const merged = history.concat(rl.history ?? []);
@@ -523,13 +915,13 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
523
915
  const bridgeDir = this.getBridgeDir();
524
916
  console.log(`${this.logo} Starting bridge...`);
525
917
  console.log("Scan the QR code to connect.\n");
526
- const result = spawnSync("npm", ["start"], { cwd: bridgeDir, stdio: "inherit" });
918
+ const result = spawnSync2("npm", ["start"], { cwd: bridgeDir, stdio: "inherit" });
527
919
  if (result.status !== 0) {
528
920
  console.error(`Bridge failed: ${result.status ?? 1}`);
529
921
  }
530
922
  }
531
923
  cronList(opts) {
532
- const storePath = join2(getDataDir2(), "cron", "jobs.json");
924
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
533
925
  const service = new CronService(storePath);
534
926
  const jobs = service.listJobs(Boolean(opts.all));
535
927
  if (!jobs.length) {
@@ -549,7 +941,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
549
941
  }
550
942
  }
551
943
  cronAdd(opts) {
552
- const storePath = join2(getDataDir2(), "cron", "jobs.json");
944
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
553
945
  const service = new CronService(storePath);
554
946
  let schedule = null;
555
947
  if (opts.every) {
@@ -574,7 +966,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
574
966
  console.log(`\u2713 Added job '${job.name}' (${job.id})`);
575
967
  }
576
968
  cronRemove(jobId) {
577
- const storePath = join2(getDataDir2(), "cron", "jobs.json");
969
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
578
970
  const service = new CronService(storePath);
579
971
  if (service.removeJob(jobId)) {
580
972
  console.log(`\u2713 Removed job ${jobId}`);
@@ -583,7 +975,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
583
975
  }
584
976
  }
585
977
  cronEnable(jobId, opts) {
586
- const storePath = join2(getDataDir2(), "cron", "jobs.json");
978
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
587
979
  const service = new CronService(storePath);
588
980
  const job = service.enableJob(jobId, !opts.disable);
589
981
  if (job) {
@@ -593,7 +985,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
593
985
  }
594
986
  }
595
987
  async cronRun(jobId, opts) {
596
- const storePath = join2(getDataDir2(), "cron", "jobs.json");
988
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
597
989
  const service = new CronService(storePath);
598
990
  const ok = await service.runJob(jobId, Boolean(opts.force));
599
991
  console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
@@ -604,8 +996,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
604
996
  const workspace = getWorkspacePath(config.agents.defaults.workspace);
605
997
  console.log(`${this.logo} ${APP_NAME} Status
606
998
  `);
607
- console.log(`Config: ${configPath} ${existsSync2(configPath) ? "\u2713" : "\u2717"}`);
608
- console.log(`Workspace: ${workspace} ${existsSync2(workspace) ? "\u2713" : "\u2717"}`);
999
+ console.log(`Config: ${configPath} ${existsSync3(configPath) ? "\u2713" : "\u2717"}`);
1000
+ console.log(`Workspace: ${workspace} ${existsSync3(workspace) ? "\u2713" : "\u2717"}`);
609
1001
  console.log(`Model: ${config.agents.defaults.model}`);
610
1002
  for (const spec of PROVIDERS) {
611
1003
  const provider = config.providers[spec.name];
@@ -625,33 +1017,36 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
625
1017
  const provider = options.allowMissingProvider === true ? this.makeProvider(config, { allowMissing: true }) : this.makeProvider(config);
626
1018
  const providerManager = provider ? new ProviderManager(provider) : null;
627
1019
  const sessionManager = new SessionManager(getWorkspacePath(config.agents.defaults.workspace));
628
- const cronStorePath = join2(getDataDir2(), "cron", "jobs.json");
1020
+ const cronStorePath = join3(getDataDir2(), "cron", "jobs.json");
629
1021
  const cron2 = new CronService(cronStorePath);
630
1022
  const uiConfig = resolveUiConfig(config, options.uiOverrides);
631
1023
  const uiStaticDir = options.uiStaticDir === void 0 ? resolveUiStaticDir() : options.uiStaticDir;
632
1024
  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
- }
1025
+ this.startUiIfEnabled(uiConfig, uiStaticDir);
649
1026
  console.log("Warning: No API key configured. UI server only.");
650
1027
  await new Promise(() => {
651
1028
  });
652
1029
  return;
653
1030
  }
654
- const gatewayController = {};
1031
+ const channels2 = new ChannelManager(config, bus, sessionManager);
1032
+ const reloader = new ConfigReloader({
1033
+ initialConfig: config,
1034
+ channels: channels2,
1035
+ bus,
1036
+ sessionManager,
1037
+ providerManager,
1038
+ makeProvider: (nextConfig) => this.makeProvider(nextConfig, { allowMissing: true }),
1039
+ loadConfig,
1040
+ onRestartRequired: (paths) => {
1041
+ console.warn(`Config changes require restart: ${paths.join(", ")}`);
1042
+ }
1043
+ });
1044
+ const gatewayController = new GatewayControllerImpl({
1045
+ reloader,
1046
+ cron: cron2,
1047
+ getConfigPath,
1048
+ saveConfig
1049
+ });
655
1050
  const agent = new AgentLoop({
656
1051
  bus,
657
1052
  providerManager: providerManager ?? new ProviderManager(provider),
@@ -690,385 +1085,12 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
690
1085
  30 * 60,
691
1086
  true
692
1087
  );
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(", ")}`);
1088
+ if (reloader.getChannels().enabledChannels.length) {
1089
+ console.log(`\u2713 Channels enabled: ${reloader.getChannels().enabledChannels.join(", ")}`);
1053
1090
  } else {
1054
1091
  console.log("Warning: No channels enabled");
1055
1092
  }
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
- }
1093
+ this.startUiIfEnabled(uiConfig, uiStaticDir);
1072
1094
  const cronStatus = cron2.status();
1073
1095
  if (cronStatus.jobs > 0) {
1074
1096
  console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
@@ -1079,12 +1101,31 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1079
1101
  ignoreInitial: true,
1080
1102
  awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
1081
1103
  });
1082
- watcher.on("add", () => scheduleConfigReload("config add"));
1083
- watcher.on("change", () => scheduleConfigReload("config change"));
1084
- watcher.on("unlink", () => scheduleConfigReload("config unlink"));
1104
+ watcher.on("add", () => reloader.scheduleReload("config add"));
1105
+ watcher.on("change", () => reloader.scheduleReload("config change"));
1106
+ watcher.on("unlink", () => reloader.scheduleReload("config unlink"));
1085
1107
  await cron2.start();
1086
1108
  await heartbeat.start();
1087
- await Promise.allSettled([agent.run(), channels2.startAll()]);
1109
+ await Promise.allSettled([agent.run(), reloader.getChannels().startAll()]);
1110
+ }
1111
+ startUiIfEnabled(uiConfig, uiStaticDir) {
1112
+ if (!uiConfig.enabled) {
1113
+ return;
1114
+ }
1115
+ const uiServer = startUiServer({
1116
+ host: uiConfig.host,
1117
+ port: uiConfig.port,
1118
+ configPath: getConfigPath(),
1119
+ staticDir: uiStaticDir ?? void 0
1120
+ });
1121
+ const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
1122
+ console.log(`\u2713 UI API: ${uiUrl}/api`);
1123
+ if (uiStaticDir) {
1124
+ console.log(`\u2713 UI frontend: ${uiUrl}`);
1125
+ }
1126
+ if (uiConfig.open) {
1127
+ openBrowser(uiUrl);
1128
+ }
1088
1129
  }
1089
1130
  async runForeground(options) {
1090
1131
  const config = loadConfig();
@@ -1141,7 +1182,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1141
1182
  console.log("Warning: UI frontend not found. Use --frontend to start the dev server.");
1142
1183
  }
1143
1184
  const logPath = resolveServiceLogPath();
1144
- const logDir = resolve2(logPath, "..");
1185
+ const logDir = resolve3(logPath, "..");
1145
1186
  mkdirSync2(logDir, { recursive: true });
1146
1187
  const logFd = openSync(logPath, "a");
1147
1188
  const serveArgs = buildServeArgs({
@@ -1250,65 +1291,115 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1250
1291
  { source: "memory/MEMORY.md", target: "memory/MEMORY.md" }
1251
1292
  ];
1252
1293
  for (const entry of templateFiles) {
1253
- const filePath = join2(workspace, entry.target);
1254
- if (!force && existsSync2(filePath)) {
1294
+ const filePath = join3(workspace, entry.target);
1295
+ if (!force && existsSync3(filePath)) {
1255
1296
  continue;
1256
1297
  }
1257
- const templatePath = join2(templateDir, entry.source);
1258
- if (!existsSync2(templatePath)) {
1298
+ const templatePath = join3(templateDir, entry.source);
1299
+ if (!existsSync3(templatePath)) {
1259
1300
  console.warn(`Warning: Template file missing: ${templatePath}`);
1260
1301
  continue;
1261
1302
  }
1262
- const raw = readFileSync2(templatePath, "utf-8");
1303
+ const raw = readFileSync3(templatePath, "utf-8");
1263
1304
  const content = raw.replace(/\$\{APP_NAME\}/g, APP_NAME);
1264
1305
  mkdirSync2(dirname(filePath), { recursive: true });
1265
1306
  writeFileSync2(filePath, content);
1266
1307
  created.push(entry.target);
1267
1308
  }
1268
- const memoryDir = join2(workspace, "memory");
1269
- if (!existsSync2(memoryDir)) {
1309
+ const memoryDir = join3(workspace, "memory");
1310
+ if (!existsSync3(memoryDir)) {
1270
1311
  mkdirSync2(memoryDir, { recursive: true });
1271
- created.push(join2("memory", ""));
1312
+ created.push(join3("memory", ""));
1272
1313
  }
1273
- const skillsDir = join2(workspace, "skills");
1274
- if (!existsSync2(skillsDir)) {
1314
+ const skillsDir = join3(workspace, "skills");
1315
+ if (!existsSync3(skillsDir)) {
1275
1316
  mkdirSync2(skillsDir, { recursive: true });
1276
- created.push(join2("skills", ""));
1317
+ created.push(join3("skills", ""));
1318
+ }
1319
+ const seeded = this.seedBuiltinSkills(skillsDir, { force });
1320
+ if (seeded > 0) {
1321
+ created.push(`skills (seeded ${seeded} built-ins)`);
1277
1322
  }
1278
1323
  return { created };
1279
1324
  }
1325
+ seedBuiltinSkills(targetDir, options = {}) {
1326
+ const sourceDir = this.resolveBuiltinSkillsDir();
1327
+ if (!sourceDir) {
1328
+ return 0;
1329
+ }
1330
+ const force = Boolean(options.force);
1331
+ const existing = readdirSync(targetDir, { withFileTypes: true }).filter((entry) => !entry.name.startsWith("."));
1332
+ if (!force && existing.length > 0) {
1333
+ return 0;
1334
+ }
1335
+ let seeded = 0;
1336
+ for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
1337
+ if (!entry.isDirectory()) {
1338
+ continue;
1339
+ }
1340
+ const src = join3(sourceDir, entry.name);
1341
+ if (!existsSync3(join3(src, "SKILL.md"))) {
1342
+ continue;
1343
+ }
1344
+ const dest = join3(targetDir, entry.name);
1345
+ if (!force && existsSync3(dest)) {
1346
+ continue;
1347
+ }
1348
+ cpSync(src, dest, { recursive: true, force: true });
1349
+ seeded += 1;
1350
+ }
1351
+ return seeded;
1352
+ }
1353
+ resolveBuiltinSkillsDir() {
1354
+ try {
1355
+ const require2 = createRequire(import.meta.url);
1356
+ const entry = require2.resolve("nextclaw-core");
1357
+ const pkgRoot = resolve3(dirname(entry), "..");
1358
+ const distSkills = join3(pkgRoot, "dist", "skills");
1359
+ if (existsSync3(distSkills)) {
1360
+ return distSkills;
1361
+ }
1362
+ const srcSkills = join3(pkgRoot, "src", "agent", "skills");
1363
+ if (existsSync3(srcSkills)) {
1364
+ return srcSkills;
1365
+ }
1366
+ return null;
1367
+ } catch {
1368
+ return null;
1369
+ }
1370
+ }
1280
1371
  resolveTemplateDir() {
1281
1372
  const override = process.env.NEXTCLAW_TEMPLATE_DIR?.trim();
1282
1373
  if (override) {
1283
1374
  return override;
1284
1375
  }
1285
- const cliDir = resolve2(fileURLToPath2(new URL(".", import.meta.url)));
1286
- const pkgRoot = resolve2(cliDir, "..", "..");
1287
- const candidates = [join2(pkgRoot, "templates")];
1376
+ const cliDir = resolve3(fileURLToPath3(new URL(".", import.meta.url)));
1377
+ const pkgRoot = resolve3(cliDir, "..", "..");
1378
+ const candidates = [join3(pkgRoot, "templates")];
1288
1379
  for (const candidate of candidates) {
1289
- if (existsSync2(candidate)) {
1380
+ if (existsSync3(candidate)) {
1290
1381
  return candidate;
1291
1382
  }
1292
1383
  }
1293
1384
  return null;
1294
1385
  }
1295
1386
  getBridgeDir() {
1296
- const userBridge = join2(getDataDir2(), "bridge");
1297
- if (existsSync2(join2(userBridge, "dist", "index.js"))) {
1387
+ const userBridge = join3(getDataDir2(), "bridge");
1388
+ if (existsSync3(join3(userBridge, "dist", "index.js"))) {
1298
1389
  return userBridge;
1299
1390
  }
1300
1391
  if (!which("npm")) {
1301
1392
  console.error("npm not found. Please install Node.js >= 18.");
1302
1393
  process.exit(1);
1303
1394
  }
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");
1395
+ const cliDir = resolve3(fileURLToPath3(new URL(".", import.meta.url)));
1396
+ const pkgRoot = resolve3(cliDir, "..", "..");
1397
+ const pkgBridge = join3(pkgRoot, "bridge");
1398
+ const srcBridge = join3(pkgRoot, "..", "..", "bridge");
1308
1399
  let source = null;
1309
- if (existsSync2(join2(pkgBridge, "package.json"))) {
1400
+ if (existsSync3(join3(pkgBridge, "package.json"))) {
1310
1401
  source = pkgBridge;
1311
- } else if (existsSync2(join2(srcBridge, "package.json"))) {
1402
+ } else if (existsSync3(join3(srcBridge, "package.json"))) {
1312
1403
  source = srcBridge;
1313
1404
  }
1314
1405
  if (!source) {
@@ -1316,15 +1407,15 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1316
1407
  process.exit(1);
1317
1408
  }
1318
1409
  console.log(`${this.logo} Setting up bridge...`);
1319
- mkdirSync2(resolve2(userBridge, ".."), { recursive: true });
1320
- if (existsSync2(userBridge)) {
1410
+ mkdirSync2(resolve3(userBridge, ".."), { recursive: true });
1411
+ if (existsSync3(userBridge)) {
1321
1412
  rmSync2(userBridge, { recursive: true, force: true });
1322
1413
  }
1323
1414
  cpSync(source, userBridge, {
1324
1415
  recursive: true,
1325
1416
  filter: (src) => !src.includes("node_modules") && !src.includes("dist")
1326
1417
  });
1327
- const install = spawnSync("npm", ["install"], { cwd: userBridge, stdio: "pipe" });
1418
+ const install = spawnSync2("npm", ["install"], { cwd: userBridge, stdio: "pipe" });
1328
1419
  if (install.status !== 0) {
1329
1420
  console.error(`Bridge install failed: ${install.status ?? 1}`);
1330
1421
  if (install.stderr) {
@@ -1332,7 +1423,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1332
1423
  }
1333
1424
  process.exit(1);
1334
1425
  }
1335
- const build = spawnSync("npm", ["run", "build"], { cwd: userBridge, stdio: "pipe" });
1426
+ const build = spawnSync2("npm", ["run", "build"], { cwd: userBridge, stdio: "pipe" });
1336
1427
  if (build.status !== 0) {
1337
1428
  console.error(`Bridge build failed: ${build.status ?? 1}`);
1338
1429
  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.2",
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.2",
42
+ "nextclaw-server": "^0.3.2"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^20.17.6",