nextclaw 0.1.0 → 0.2.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.
package/dist/cli/index.js CHANGED
@@ -1,5 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ APP_NAME,
4
+ APP_REPLY_SUBJECT,
5
+ APP_TAGLINE,
6
+ APP_TITLE,
3
7
  AgentLoop,
4
8
  ConfigSchema,
5
9
  LiteLLMProvider,
@@ -14,13 +18,13 @@ import {
14
18
  getWorkspacePath,
15
19
  loadConfig,
16
20
  saveConfig
17
- } from "../chunk-RTVGGPPW.js";
21
+ } from "../chunk-HXRBJNWA.js";
18
22
 
19
23
  // src/cli/index.ts
20
24
  import { Command } from "commander";
21
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, cpSync, rmSync } from "fs";
22
- import { join as join5, resolve } from "path";
23
- import { spawnSync } from "child_process";
25
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, cpSync, rmSync } from "fs";
26
+ import { join as join6, resolve } from "path";
27
+ import { spawn, spawnSync } from "child_process";
24
28
  import { createInterface } from "readline";
25
29
  import { fileURLToPath } from "url";
26
30
 
@@ -79,8 +83,8 @@ var MessageBus = class {
79
83
  for (const callback of subscribers) {
80
84
  try {
81
85
  await callback(msg);
82
- } catch (err) {
83
- console.error(`Error dispatching to ${msg.channel}: ${String(err)}`);
86
+ } catch (err2) {
87
+ console.error(`Error dispatching to ${msg.channel}: ${String(err2)}`);
84
88
  }
85
89
  }
86
90
  }
@@ -205,14 +209,20 @@ var TelegramChannel = class extends BaseChannel {
205
209
  this.bot.onText(/^\/start$/, async (msg) => {
206
210
  await this.bot?.sendMessage(
207
211
  msg.chat.id,
208
- `\u{1F44B} Hi ${msg.from?.first_name ?? ""}! I'm nextclaw.
212
+ `\u{1F44B} Hi ${msg.from?.first_name ?? ""}! I'm ${APP_NAME}.
209
213
 
210
214
  Send me a message and I'll respond!
211
215
  Type /help to see available commands.`
212
216
  );
213
217
  });
214
218
  this.bot.onText(/^\/help$/, async (msg) => {
215
- const helpText = "\u{1F916} <b>nextclaw commands</b>\n\n/start \u2014 Start the bot\n/reset \u2014 Reset conversation history\n/help \u2014 Show this help message\n\nJust send me a text message to chat!";
219
+ const helpText = `\u{1F916} <b>${APP_NAME} commands</b>
220
+
221
+ /start \u2014 Start the bot
222
+ /reset \u2014 Reset conversation history
223
+ /help \u2014 Show this help message
224
+
225
+ Just send me a text message to chat!`;
216
226
  await this.bot?.sendMessage(msg.chat.id, helpText, { parse_mode: "HTML" });
217
227
  });
218
228
  this.bot.onText(/^\/reset$/, async (msg) => {
@@ -1074,12 +1084,12 @@ var MochatChannel = class extends BaseChannel {
1074
1084
  async subscribeAll() {
1075
1085
  const sessions = Array.from(this.sessionSet).sort();
1076
1086
  const panels = Array.from(this.panelSet).sort();
1077
- let ok = await this.subscribeSessions(sessions);
1078
- ok = await this.subscribePanels(panels) && ok;
1087
+ let ok2 = await this.subscribeSessions(sessions);
1088
+ ok2 = await this.subscribePanels(panels) && ok2;
1079
1089
  if (this.autoDiscoverSessions || this.autoDiscoverPanels) {
1080
1090
  await this.refreshTargets(true);
1081
1091
  }
1082
- return ok;
1092
+ return ok2;
1083
1093
  }
1084
1094
  async subscribeSessions(sessionIds) {
1085
1095
  if (!sessionIds.length) {
@@ -1130,9 +1140,9 @@ var MochatChannel = class extends BaseChannel {
1130
1140
  return { result: false, message: "socket not connected" };
1131
1141
  }
1132
1142
  return new Promise((resolve2) => {
1133
- this.socket?.timeout(1e4).emit(eventName, payload, (err, response) => {
1134
- if (err) {
1135
- resolve2({ result: false, message: String(err) });
1143
+ this.socket?.timeout(1e4).emit(eventName, payload, (err2, response) => {
1144
+ if (err2) {
1145
+ resolve2({ result: false, message: String(err2) });
1136
1146
  return;
1137
1147
  }
1138
1148
  if (response && typeof response === "object") {
@@ -1844,7 +1854,7 @@ var DingTalkChannel = class extends BaseChannel {
1844
1854
  msgKey: "sampleMarkdown",
1845
1855
  msgParam: JSON.stringify({
1846
1856
  text: msg.content,
1847
- title: "Nextclaw Reply"
1857
+ title: `${APP_TITLE} Reply`
1848
1858
  })
1849
1859
  };
1850
1860
  const response = await fetch4(url, {
@@ -1985,7 +1995,7 @@ var EmailChannel = class extends BaseChannel {
1985
1995
  if (!toAddr) {
1986
1996
  return;
1987
1997
  }
1988
- const baseSubject = this.lastSubjectByChat.get(toAddr) ?? "nextclaw reply";
1998
+ const baseSubject = this.lastSubjectByChat.get(toAddr) ?? APP_REPLY_SUBJECT;
1989
1999
  const subject = msg.metadata?.subject?.trim() || this.replySubject(baseSubject);
1990
2000
  const transporter = nodemailer.createTransport({
1991
2001
  host: this.config.smtpHost,
@@ -2378,8 +2388,8 @@ var ChannelManager = class {
2378
2388
  async startChannel(name, channel) {
2379
2389
  try {
2380
2390
  await channel.start();
2381
- } catch (err) {
2382
- console.error(`Failed to start channel ${name}: ${String(err)}`);
2391
+ } catch (err2) {
2392
+ console.error(`Failed to start channel ${name}: ${String(err2)}`);
2383
2393
  }
2384
2394
  }
2385
2395
  async startAll() {
@@ -2399,8 +2409,8 @@ var ChannelManager = class {
2399
2409
  const tasks = Object.entries(this.channels).map(async ([name, channel]) => {
2400
2410
  try {
2401
2411
  await channel.stop();
2402
- } catch (err) {
2403
- console.error(`Error stopping ${name}: ${String(err)}`);
2412
+ } catch (err2) {
2413
+ console.error(`Error stopping ${name}: ${String(err2)}`);
2404
2414
  }
2405
2415
  });
2406
2416
  await Promise.allSettled(tasks);
@@ -2414,8 +2424,8 @@ var ChannelManager = class {
2414
2424
  }
2415
2425
  try {
2416
2426
  await channel.send(msg);
2417
- } catch (err) {
2418
- console.error(`Error sending to ${msg.channel}: ${String(err)}`);
2427
+ } catch (err2) {
2428
+ console.error(`Error sending to ${msg.channel}: ${String(err2)}`);
2419
2429
  }
2420
2430
  }
2421
2431
  }
@@ -2574,9 +2584,9 @@ var CronService = class {
2574
2584
  }
2575
2585
  job.state.lastStatus = "ok";
2576
2586
  job.state.lastError = null;
2577
- } catch (err) {
2587
+ } catch (err2) {
2578
2588
  job.state.lastStatus = "error";
2579
- job.state.lastError = String(err);
2589
+ job.state.lastError = String(err2);
2580
2590
  }
2581
2591
  job.state.lastRunAtMs = start;
2582
2592
  job.updatedAtMs = nowMs();
@@ -2756,15 +2766,298 @@ var HeartbeatService = class {
2756
2766
  }
2757
2767
  };
2758
2768
 
2769
+ // src/ui/server.ts
2770
+ import { Hono as Hono2 } from "hono";
2771
+ import { cors } from "hono/cors";
2772
+ import { serve } from "@hono/node-server";
2773
+ import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
2774
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
2775
+ import { readFile, stat } from "fs/promises";
2776
+ import { join as join5 } from "path";
2777
+
2778
+ // src/ui/router.ts
2779
+ import { Hono } from "hono";
2780
+
2781
+ // src/ui/config.ts
2782
+ var MASK_MIN_LENGTH = 8;
2783
+ function maskApiKey(value) {
2784
+ if (!value) {
2785
+ return { apiKeySet: false };
2786
+ }
2787
+ if (value.length < MASK_MIN_LENGTH) {
2788
+ return { apiKeySet: true, apiKeyMasked: "****" };
2789
+ }
2790
+ return {
2791
+ apiKeySet: true,
2792
+ apiKeyMasked: `${value.slice(0, 2)}****${value.slice(-4)}`
2793
+ };
2794
+ }
2795
+ function toProviderView(provider) {
2796
+ const masked = maskApiKey(provider.apiKey);
2797
+ return {
2798
+ apiKeySet: masked.apiKeySet,
2799
+ apiKeyMasked: masked.apiKeyMasked,
2800
+ apiBase: provider.apiBase ?? null,
2801
+ extraHeaders: provider.extraHeaders ?? null
2802
+ };
2803
+ }
2804
+ function buildConfigView(config) {
2805
+ const providers = {};
2806
+ for (const [name, provider] of Object.entries(config.providers)) {
2807
+ providers[name] = toProviderView(provider);
2808
+ }
2809
+ return {
2810
+ agents: config.agents,
2811
+ providers,
2812
+ channels: config.channels,
2813
+ tools: config.tools,
2814
+ gateway: config.gateway,
2815
+ ui: config.ui
2816
+ };
2817
+ }
2818
+ function buildConfigMeta(config) {
2819
+ const providers = PROVIDERS.map((spec) => ({
2820
+ name: spec.name,
2821
+ displayName: spec.displayName,
2822
+ keywords: spec.keywords,
2823
+ envKey: spec.envKey,
2824
+ isGateway: spec.isGateway,
2825
+ isLocal: spec.isLocal,
2826
+ defaultApiBase: spec.defaultApiBase
2827
+ }));
2828
+ const channels2 = Object.keys(config.channels).map((name) => ({
2829
+ name,
2830
+ displayName: name,
2831
+ enabled: Boolean(config.channels[name]?.enabled)
2832
+ }));
2833
+ return { providers, channels: channels2 };
2834
+ }
2835
+ function loadConfigOrDefault(configPath) {
2836
+ return loadConfig(configPath);
2837
+ }
2838
+ function updateModel(configPath, model) {
2839
+ const config = loadConfigOrDefault(configPath);
2840
+ config.agents.defaults.model = model;
2841
+ const next = ConfigSchema.parse(config);
2842
+ saveConfig(next, configPath);
2843
+ return buildConfigView(next);
2844
+ }
2845
+ function updateProvider(configPath, providerName, patch) {
2846
+ const config = loadConfigOrDefault(configPath);
2847
+ const provider = config.providers[providerName];
2848
+ if (!provider) {
2849
+ return null;
2850
+ }
2851
+ if (Object.prototype.hasOwnProperty.call(patch, "apiKey")) {
2852
+ provider.apiKey = patch.apiKey ?? "";
2853
+ }
2854
+ if (Object.prototype.hasOwnProperty.call(patch, "apiBase")) {
2855
+ provider.apiBase = patch.apiBase ?? null;
2856
+ }
2857
+ if (Object.prototype.hasOwnProperty.call(patch, "extraHeaders")) {
2858
+ provider.extraHeaders = patch.extraHeaders ?? null;
2859
+ }
2860
+ const next = ConfigSchema.parse(config);
2861
+ saveConfig(next, configPath);
2862
+ const updated = next.providers[providerName];
2863
+ return toProviderView(updated);
2864
+ }
2865
+ function updateChannel(configPath, channelName, patch) {
2866
+ const config = loadConfigOrDefault(configPath);
2867
+ const channel = config.channels[channelName];
2868
+ if (!channel) {
2869
+ return null;
2870
+ }
2871
+ config.channels[channelName] = { ...channel, ...patch };
2872
+ const next = ConfigSchema.parse(config);
2873
+ saveConfig(next, configPath);
2874
+ return next.channels[channelName];
2875
+ }
2876
+ function updateUi(configPath, patch) {
2877
+ const config = loadConfigOrDefault(configPath);
2878
+ config.ui = { ...config.ui, ...patch };
2879
+ const next = ConfigSchema.parse(config);
2880
+ saveConfig(next, configPath);
2881
+ return next.ui;
2882
+ }
2883
+
2884
+ // src/ui/router.ts
2885
+ function ok(data) {
2886
+ return { ok: true, data };
2887
+ }
2888
+ function err(code, message, details) {
2889
+ return { ok: false, error: { code, message, details } };
2890
+ }
2891
+ async function readJson(req) {
2892
+ try {
2893
+ const data = await req.json();
2894
+ return { ok: true, data };
2895
+ } catch {
2896
+ return { ok: false };
2897
+ }
2898
+ }
2899
+ function createUiRouter(options) {
2900
+ const app = new Hono();
2901
+ app.get("/api/health", (c) => c.json(ok({ status: "ok" })));
2902
+ app.get("/api/config", (c) => {
2903
+ const config = loadConfigOrDefault(options.configPath);
2904
+ return c.json(ok(buildConfigView(config)));
2905
+ });
2906
+ app.get("/api/config/meta", (c) => {
2907
+ const config = loadConfigOrDefault(options.configPath);
2908
+ return c.json(ok(buildConfigMeta(config)));
2909
+ });
2910
+ app.put("/api/config/model", async (c) => {
2911
+ const body = await readJson(c.req.raw);
2912
+ if (!body.ok || !body.data.model) {
2913
+ return c.json(err("INVALID_BODY", "model is required"), 400);
2914
+ }
2915
+ const view = updateModel(options.configPath, body.data.model);
2916
+ options.publish({ type: "config.updated", payload: { path: "agents.defaults.model" } });
2917
+ return c.json(ok({ model: view.agents.defaults.model }));
2918
+ });
2919
+ app.put("/api/config/providers/:provider", async (c) => {
2920
+ const provider = c.req.param("provider");
2921
+ const body = await readJson(c.req.raw);
2922
+ if (!body.ok) {
2923
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2924
+ }
2925
+ const result = updateProvider(options.configPath, provider, body.data);
2926
+ if (!result) {
2927
+ return c.json(err("NOT_FOUND", `unknown provider: ${provider}`), 404);
2928
+ }
2929
+ options.publish({ type: "config.updated", payload: { path: `providers.${provider}` } });
2930
+ return c.json(ok(result));
2931
+ });
2932
+ app.put("/api/config/channels/:channel", async (c) => {
2933
+ const channel = c.req.param("channel");
2934
+ const body = await readJson(c.req.raw);
2935
+ if (!body.ok) {
2936
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2937
+ }
2938
+ const result = updateChannel(options.configPath, channel, body.data);
2939
+ if (!result) {
2940
+ return c.json(err("NOT_FOUND", `unknown channel: ${channel}`), 404);
2941
+ }
2942
+ options.publish({ type: "config.updated", payload: { path: `channels.${channel}` } });
2943
+ return c.json(ok(result));
2944
+ });
2945
+ app.put("/api/config/ui", async (c) => {
2946
+ const body = await readJson(c.req.raw);
2947
+ if (!body.ok) {
2948
+ return c.json(err("INVALID_BODY", "invalid json body"), 400);
2949
+ }
2950
+ const result = updateUi(options.configPath, body.data);
2951
+ options.publish({ type: "config.updated", payload: { path: "ui" } });
2952
+ return c.json(ok(result));
2953
+ });
2954
+ app.post("/api/config/reload", async (c) => {
2955
+ options.publish({ type: "config.reload.started" });
2956
+ try {
2957
+ await options.onReload?.();
2958
+ options.publish({ type: "config.reload.finished" });
2959
+ return c.json(ok({ status: "ok" }));
2960
+ } catch (error) {
2961
+ options.publish({
2962
+ type: "error",
2963
+ payload: { message: "reload failed", code: "RELOAD_FAILED" }
2964
+ });
2965
+ return c.json(err("RELOAD_FAILED", String(error ?? "reload failed")), 500);
2966
+ }
2967
+ });
2968
+ return app;
2969
+ }
2970
+
2971
+ // src/ui/server.ts
2972
+ import { serveStatic } from "hono/serve-static";
2973
+ var DEFAULT_CORS_ORIGINS = ["http://localhost:5173", "http://127.0.0.1:5173"];
2974
+ function startUiServer(options) {
2975
+ const app = new Hono2();
2976
+ const origin = options.corsOrigins ?? DEFAULT_CORS_ORIGINS;
2977
+ app.use("/api/*", cors({ origin }));
2978
+ const clients = /* @__PURE__ */ new Set();
2979
+ const publish = (event) => {
2980
+ const payload = JSON.stringify(event);
2981
+ for (const client of clients) {
2982
+ if (client.readyState === WebSocket2.OPEN) {
2983
+ client.send(payload);
2984
+ }
2985
+ }
2986
+ };
2987
+ app.route(
2988
+ "/",
2989
+ createUiRouter({
2990
+ configPath: options.configPath,
2991
+ publish,
2992
+ onReload: options.onReload
2993
+ })
2994
+ );
2995
+ const staticDir = options.staticDir;
2996
+ if (staticDir && existsSync5(join5(staticDir, "index.html"))) {
2997
+ const indexHtml = readFileSync4(join5(staticDir, "index.html"), "utf-8");
2998
+ app.use(
2999
+ "/*",
3000
+ serveStatic({
3001
+ root: staticDir,
3002
+ join: join5,
3003
+ getContent: async (path) => {
3004
+ try {
3005
+ return await readFile(path);
3006
+ } catch {
3007
+ return null;
3008
+ }
3009
+ },
3010
+ isDir: async (path) => {
3011
+ try {
3012
+ return (await stat(path)).isDirectory();
3013
+ } catch {
3014
+ return false;
3015
+ }
3016
+ }
3017
+ })
3018
+ );
3019
+ app.get("*", (c) => {
3020
+ const path = c.req.path;
3021
+ if (path.startsWith("/api") || path.startsWith("/ws")) {
3022
+ return c.notFound();
3023
+ }
3024
+ return c.html(indexHtml);
3025
+ });
3026
+ }
3027
+ const server = serve({
3028
+ fetch: app.fetch,
3029
+ port: options.port,
3030
+ hostname: options.host
3031
+ });
3032
+ const wss = new WebSocketServer({
3033
+ server,
3034
+ path: "/ws"
3035
+ });
3036
+ wss.on("connection", (socket) => {
3037
+ clients.add(socket);
3038
+ socket.on("close", () => clients.delete(socket));
3039
+ });
3040
+ return {
3041
+ host: options.host,
3042
+ port: options.port,
3043
+ publish,
3044
+ close: () => new Promise((resolve2) => {
3045
+ wss.close(() => {
3046
+ server.close(() => resolve2());
3047
+ });
3048
+ })
3049
+ };
3050
+ }
3051
+
2759
3052
  // src/cli/index.ts
2760
3053
  var LOGO = "\u{1F916}";
2761
3054
  var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "/exit", "/quit", ":q"]);
2762
3055
  var VERSION = getPackageVersion();
2763
3056
  var program = new Command();
2764
- program.name("nextclaw").description(`${LOGO} nextclaw - Personal AI Assistant`).version(VERSION, "-v, --version", "show version");
2765
- program.command("onboard").description("Initialize nextclaw configuration and workspace").action(() => {
3057
+ program.name(APP_NAME).description(`${LOGO} ${APP_NAME} - ${APP_TAGLINE}`).version(VERSION, "-v, --version", "show version");
3058
+ program.command("onboard").description(`Initialize ${APP_NAME} configuration and workspace`).action(() => {
2766
3059
  const configPath = getConfigPath();
2767
- if (existsSync5(configPath)) {
3060
+ if (existsSync6(configPath)) {
2768
3061
  console.log(`Config already exists at ${configPath}`);
2769
3062
  }
2770
3063
  const config = ConfigSchema.parse({});
@@ -2774,68 +3067,77 @@ program.command("onboard").description("Initialize nextclaw configuration and wo
2774
3067
  console.log(`\u2713 Created workspace at ${workspace}`);
2775
3068
  createWorkspaceTemplates(workspace);
2776
3069
  console.log(`
2777
- ${LOGO} nextclaw is ready!`);
3070
+ ${LOGO} ${APP_NAME} is ready!`);
2778
3071
  console.log("\nNext steps:");
2779
3072
  console.log(` 1. Add your API key to ${configPath}`);
2780
- console.log(' 2. Chat: nextclaw agent -m "Hello!"');
3073
+ console.log(` 2. Chat: ${APP_NAME} agent -m "Hello!"`);
3074
+ });
3075
+ program.command("gateway").description(`Start the ${APP_NAME} gateway`).option("-p, --port <port>", "Gateway port", "18790").option("-v, --verbose", "Verbose output", false).option("--ui", "Enable UI server", false).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--ui-open", "Open browser when UI starts", false).action(async (opts) => {
3076
+ const uiOverrides = {};
3077
+ if (opts.ui) {
3078
+ uiOverrides.enabled = true;
3079
+ }
3080
+ if (opts.uiHost) {
3081
+ uiOverrides.host = String(opts.uiHost);
3082
+ }
3083
+ if (opts.uiPort) {
3084
+ uiOverrides.port = Number(opts.uiPort);
3085
+ }
3086
+ if (opts.uiOpen) {
3087
+ uiOverrides.open = true;
3088
+ }
3089
+ await startGateway({ uiOverrides });
3090
+ });
3091
+ program.command("ui").description(`Start the ${APP_NAME} UI with gateway`).option("--host <host>", "UI host").option("--port <port>", "UI port").option("--no-open", "Disable opening browser").action(async (opts) => {
3092
+ const uiOverrides = {
3093
+ enabled: true,
3094
+ open: Boolean(opts.open)
3095
+ };
3096
+ if (opts.host) {
3097
+ uiOverrides.host = String(opts.host);
3098
+ }
3099
+ if (opts.port) {
3100
+ uiOverrides.port = Number(opts.port);
3101
+ }
3102
+ await startGateway({ uiOverrides, allowMissingProvider: true });
2781
3103
  });
2782
- program.command("gateway").description("Start the nextclaw gateway").option("-p, --port <port>", "Gateway port", "18790").option("-v, --verbose", "Verbose output", false).action(async (_opts) => {
3104
+ program.command("start").description(`Start the ${APP_NAME} gateway + UI (backend + frontend)`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--frontend-port <port>", "UI frontend dev server port").option("--no-frontend", "Disable UI frontend dev server").option("--no-open", "Disable opening browser").action(async (opts) => {
3105
+ const uiOverrides = {
3106
+ enabled: true,
3107
+ open: false
3108
+ };
3109
+ if (opts.uiHost) {
3110
+ uiOverrides.host = String(opts.uiHost);
3111
+ }
3112
+ if (opts.uiPort) {
3113
+ uiOverrides.port = Number(opts.uiPort);
3114
+ }
2783
3115
  const config = loadConfig();
2784
- const bus = new MessageBus();
2785
- const provider = makeProvider(config);
2786
- const sessionManager = new SessionManager(getWorkspacePath(config.agents.defaults.workspace));
2787
- const cronStorePath = join5(getDataDir(), "cron", "jobs.json");
2788
- const cron2 = new CronService(cronStorePath);
2789
- const agent = new AgentLoop({
2790
- bus,
2791
- provider,
2792
- workspace: getWorkspacePath(config.agents.defaults.workspace),
2793
- model: config.agents.defaults.model,
2794
- maxIterations: config.agents.defaults.maxToolIterations,
2795
- braveApiKey: config.tools.web.search.apiKey || void 0,
2796
- execConfig: config.tools.exec,
2797
- cronService: cron2,
2798
- restrictToWorkspace: config.tools.restrictToWorkspace,
2799
- sessionManager
2800
- });
2801
- cron2.onJob = async (job) => {
2802
- const response = await agent.processDirect({
2803
- content: job.payload.message,
2804
- sessionKey: `cron:${job.id}`,
2805
- channel: job.payload.channel ?? "cli",
2806
- chatId: job.payload.to ?? "direct"
3116
+ const uiConfig = resolveUiConfig(config, uiOverrides);
3117
+ const staticDir = resolveUiStaticDir();
3118
+ const frontendPort = Number.isFinite(Number(opts.frontendPort)) ? Number(opts.frontendPort) : 5173;
3119
+ const shouldStartFrontend = opts.frontend !== false;
3120
+ const frontendDir = shouldStartFrontend ? resolveUiFrontendDir() : null;
3121
+ let frontendUrl = null;
3122
+ if (shouldStartFrontend && frontendDir) {
3123
+ const frontend = startUiFrontend({
3124
+ apiBase: resolveUiApiBase(uiConfig.host, uiConfig.port),
3125
+ port: frontendPort,
3126
+ dir: frontendDir
2807
3127
  });
2808
- if (job.payload.deliver && job.payload.to) {
2809
- await bus.publishOutbound({
2810
- channel: job.payload.channel ?? "cli",
2811
- chatId: job.payload.to,
2812
- content: response,
2813
- media: [],
2814
- metadata: {}
2815
- });
2816
- }
2817
- return response;
2818
- };
2819
- const heartbeat = new HeartbeatService(
2820
- getWorkspacePath(config.agents.defaults.workspace),
2821
- async (prompt2) => agent.processDirect({ content: prompt2, sessionKey: "heartbeat" }),
2822
- 30 * 60,
2823
- true
2824
- );
2825
- const channels2 = new ChannelManager(config, bus, sessionManager);
2826
- if (channels2.enabledChannels.length) {
2827
- console.log(`\u2713 Channels enabled: ${channels2.enabledChannels.join(", ")}`);
2828
- } else {
2829
- console.log("Warning: No channels enabled");
3128
+ frontendUrl = frontend?.url ?? null;
3129
+ } else if (shouldStartFrontend && !frontendDir && !staticDir) {
3130
+ console.log("Warning: UI frontend not found. Start it separately.");
2830
3131
  }
2831
- const cronStatus = cron2.status();
2832
- if (cronStatus.jobs > 0) {
2833
- console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
3132
+ if (!frontendUrl && staticDir) {
3133
+ frontendUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
2834
3134
  }
2835
- console.log("\u2713 Heartbeat: every 30m");
2836
- await cron2.start();
2837
- await heartbeat.start();
2838
- await Promise.allSettled([agent.run(), channels2.startAll()]);
3135
+ if (opts.open && frontendUrl) {
3136
+ openBrowser(frontendUrl);
3137
+ } else if (opts.open && !frontendUrl) {
3138
+ console.log("Warning: UI frontend not started. Browser not opened.");
3139
+ }
3140
+ await startGateway({ uiOverrides, allowMissingProvider: true, uiStaticDir: staticDir ?? void 0 });
2839
3141
  });
2840
3142
  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) => {
2841
3143
  const config = loadConfig();
@@ -2861,10 +3163,10 @@ program.command("agent").description("Interact with the agent directly").option(
2861
3163
  }
2862
3164
  console.log(`${LOGO} Interactive mode (type exit or Ctrl+C to quit)
2863
3165
  `);
2864
- const historyFile = join5(getDataDir(), "history", "cli_history");
3166
+ const historyFile = join6(getDataDir(), "history", "cli_history");
2865
3167
  const historyDir = resolve(historyFile, "..");
2866
3168
  mkdirSync5(historyDir, { recursive: true });
2867
- const history = existsSync5(historyFile) ? readFileSync4(historyFile, "utf-8").split("\n").filter(Boolean) : [];
3169
+ const history = existsSync6(historyFile) ? readFileSync5(historyFile, "utf-8").split("\n").filter(Boolean) : [];
2868
3170
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2869
3171
  rl.on("close", () => {
2870
3172
  const merged = history.concat(rl.history ?? []);
@@ -2910,7 +3212,7 @@ channels.command("login").description("Link device via QR code").action(() => {
2910
3212
  });
2911
3213
  var cron = program.command("cron").description("Manage scheduled tasks");
2912
3214
  cron.command("list").option("-a, --all", "Include disabled jobs").action((opts) => {
2913
- const storePath = join5(getDataDir(), "cron", "jobs.json");
3215
+ const storePath = join6(getDataDir(), "cron", "jobs.json");
2914
3216
  const service = new CronService(storePath);
2915
3217
  const jobs = service.listJobs(Boolean(opts.all));
2916
3218
  if (!jobs.length) {
@@ -2930,7 +3232,7 @@ cron.command("list").option("-a, --all", "Include disabled jobs").action((opts)
2930
3232
  }
2931
3233
  });
2932
3234
  cron.command("add").requiredOption("-n, --name <name>", "Job name").requiredOption("-m, --message <message>", "Message for agent").option("-e, --every <seconds>", "Run every N seconds").option("-c, --cron <expr>", "Cron expression").option("--at <iso>", "Run once at time (ISO format)").option("-d, --deliver", "Deliver response to channel").option("--to <recipient>", "Recipient for delivery").option("--channel <channel>", "Channel for delivery").action((opts) => {
2933
- const storePath = join5(getDataDir(), "cron", "jobs.json");
3235
+ const storePath = join6(getDataDir(), "cron", "jobs.json");
2934
3236
  const service = new CronService(storePath);
2935
3237
  let schedule = null;
2936
3238
  if (opts.every) {
@@ -2955,7 +3257,7 @@ cron.command("add").requiredOption("-n, --name <name>", "Job name").requiredOpti
2955
3257
  console.log(`\u2713 Added job '${job.name}' (${job.id})`);
2956
3258
  });
2957
3259
  cron.command("remove <jobId>").action((jobId) => {
2958
- const storePath = join5(getDataDir(), "cron", "jobs.json");
3260
+ const storePath = join6(getDataDir(), "cron", "jobs.json");
2959
3261
  const service = new CronService(storePath);
2960
3262
  if (service.removeJob(jobId)) {
2961
3263
  console.log(`\u2713 Removed job ${jobId}`);
@@ -2964,7 +3266,7 @@ cron.command("remove <jobId>").action((jobId) => {
2964
3266
  }
2965
3267
  });
2966
3268
  cron.command("enable <jobId>").option("--disable", "Disable instead of enable").action((jobId, opts) => {
2967
- const storePath = join5(getDataDir(), "cron", "jobs.json");
3269
+ const storePath = join6(getDataDir(), "cron", "jobs.json");
2968
3270
  const service = new CronService(storePath);
2969
3271
  const job = service.enableJob(jobId, !opts.disable);
2970
3272
  if (job) {
@@ -2974,19 +3276,19 @@ cron.command("enable <jobId>").option("--disable", "Disable instead of enable").
2974
3276
  }
2975
3277
  });
2976
3278
  cron.command("run <jobId>").option("-f, --force", "Run even if disabled").action(async (jobId, opts) => {
2977
- const storePath = join5(getDataDir(), "cron", "jobs.json");
3279
+ const storePath = join6(getDataDir(), "cron", "jobs.json");
2978
3280
  const service = new CronService(storePath);
2979
- const ok = await service.runJob(jobId, Boolean(opts.force));
2980
- console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
3281
+ const ok2 = await service.runJob(jobId, Boolean(opts.force));
3282
+ console.log(ok2 ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
2981
3283
  });
2982
- program.command("status").description("Show nextclaw status").action(() => {
3284
+ program.command("status").description(`Show ${APP_NAME} status`).action(() => {
2983
3285
  const configPath = getConfigPath();
2984
3286
  const config = loadConfig();
2985
3287
  const workspace = getWorkspacePath(config.agents.defaults.workspace);
2986
- console.log(`${LOGO} nextclaw Status
3288
+ console.log(`${LOGO} ${APP_NAME} Status
2987
3289
  `);
2988
- console.log(`Config: ${configPath} ${existsSync5(configPath) ? "\u2713" : "\u2717"}`);
2989
- console.log(`Workspace: ${workspace} ${existsSync5(workspace) ? "\u2713" : "\u2717"}`);
3290
+ console.log(`Config: ${configPath} ${existsSync6(configPath) ? "\u2713" : "\u2717"}`);
3291
+ console.log(`Workspace: ${workspace} ${existsSync6(workspace) ? "\u2713" : "\u2717"}`);
2990
3292
  console.log(`Model: ${config.agents.defaults.model}`);
2991
3293
  for (const spec of PROVIDERS) {
2992
3294
  const provider = config.providers[spec.name];
@@ -3001,12 +3303,169 @@ program.command("status").description("Show nextclaw status").action(() => {
3001
3303
  }
3002
3304
  });
3003
3305
  program.parseAsync(process.argv);
3004
- function makeProvider(config) {
3306
+ async function startGateway(options = {}) {
3307
+ const config = loadConfig();
3308
+ const bus = new MessageBus();
3309
+ const provider = options.allowMissingProvider === true ? makeProvider(config, { allowMissing: true }) : makeProvider(config);
3310
+ const sessionManager = new SessionManager(getWorkspacePath(config.agents.defaults.workspace));
3311
+ const cronStorePath = join6(getDataDir(), "cron", "jobs.json");
3312
+ const cron2 = new CronService(cronStorePath);
3313
+ const uiConfig = resolveUiConfig(config, options.uiOverrides);
3314
+ const uiStaticDir = options.uiStaticDir ?? resolveUiStaticDir();
3315
+ if (!provider) {
3316
+ if (uiConfig.enabled) {
3317
+ const uiServer = startUiServer({
3318
+ host: uiConfig.host,
3319
+ port: uiConfig.port,
3320
+ configPath: getConfigPath(),
3321
+ staticDir: uiStaticDir ?? void 0,
3322
+ onReload: async () => {
3323
+ return;
3324
+ }
3325
+ });
3326
+ const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
3327
+ console.log(`\u2713 UI API: ${uiUrl}/api`);
3328
+ if (uiStaticDir) {
3329
+ console.log(`\u2713 UI frontend: ${uiUrl}`);
3330
+ }
3331
+ if (uiConfig.open) {
3332
+ openBrowser(uiUrl);
3333
+ }
3334
+ }
3335
+ console.log("Warning: No API key configured. UI server only.");
3336
+ await new Promise(() => {
3337
+ });
3338
+ return;
3339
+ }
3340
+ const agent = new AgentLoop({
3341
+ bus,
3342
+ provider,
3343
+ workspace: getWorkspacePath(config.agents.defaults.workspace),
3344
+ model: config.agents.defaults.model,
3345
+ maxIterations: config.agents.defaults.maxToolIterations,
3346
+ braveApiKey: config.tools.web.search.apiKey || void 0,
3347
+ execConfig: config.tools.exec,
3348
+ cronService: cron2,
3349
+ restrictToWorkspace: config.tools.restrictToWorkspace,
3350
+ sessionManager
3351
+ });
3352
+ cron2.onJob = async (job) => {
3353
+ const response = await agent.processDirect({
3354
+ content: job.payload.message,
3355
+ sessionKey: `cron:${job.id}`,
3356
+ channel: job.payload.channel ?? "cli",
3357
+ chatId: job.payload.to ?? "direct"
3358
+ });
3359
+ if (job.payload.deliver && job.payload.to) {
3360
+ await bus.publishOutbound({
3361
+ channel: job.payload.channel ?? "cli",
3362
+ chatId: job.payload.to,
3363
+ content: response,
3364
+ media: [],
3365
+ metadata: {}
3366
+ });
3367
+ }
3368
+ return response;
3369
+ };
3370
+ const heartbeat = new HeartbeatService(
3371
+ getWorkspacePath(config.agents.defaults.workspace),
3372
+ async (prompt2) => agent.processDirect({ content: prompt2, sessionKey: "heartbeat" }),
3373
+ 30 * 60,
3374
+ true
3375
+ );
3376
+ const channels2 = new ChannelManager(config, bus, sessionManager);
3377
+ if (channels2.enabledChannels.length) {
3378
+ console.log(`\u2713 Channels enabled: ${channels2.enabledChannels.join(", ")}`);
3379
+ } else {
3380
+ console.log("Warning: No channels enabled");
3381
+ }
3382
+ if (uiConfig.enabled) {
3383
+ const uiServer = startUiServer({
3384
+ host: uiConfig.host,
3385
+ port: uiConfig.port,
3386
+ configPath: getConfigPath(),
3387
+ staticDir: uiStaticDir ?? void 0,
3388
+ onReload: async () => {
3389
+ return;
3390
+ }
3391
+ });
3392
+ const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
3393
+ console.log(`\u2713 UI API: ${uiUrl}/api`);
3394
+ if (uiStaticDir) {
3395
+ console.log(`\u2713 UI frontend: ${uiUrl}`);
3396
+ }
3397
+ if (uiConfig.open) {
3398
+ openBrowser(uiUrl);
3399
+ }
3400
+ }
3401
+ const cronStatus = cron2.status();
3402
+ if (cronStatus.jobs > 0) {
3403
+ console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
3404
+ }
3405
+ console.log("\u2713 Heartbeat: every 30m");
3406
+ await cron2.start();
3407
+ await heartbeat.start();
3408
+ await Promise.allSettled([agent.run(), channels2.startAll()]);
3409
+ }
3410
+ function resolveUiConfig(config, overrides) {
3411
+ const base = config.ui ?? { enabled: false, host: "127.0.0.1", port: 18791, open: false };
3412
+ return { ...base, ...overrides ?? {} };
3413
+ }
3414
+ function resolveUiApiBase(host, port) {
3415
+ const normalizedHost = host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host;
3416
+ return `http://${normalizedHost}:${port}`;
3417
+ }
3418
+ function resolveUiStaticDir() {
3419
+ const candidates = [];
3420
+ const envDir = process.env.NEXTCLAW_UI_STATIC_DIR;
3421
+ if (envDir) {
3422
+ candidates.push(envDir);
3423
+ }
3424
+ const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
3425
+ const pkgRoot = resolve(cliDir, "..", "..");
3426
+ candidates.push(join6(pkgRoot, "ui-dist"));
3427
+ candidates.push(join6(pkgRoot, "ui"));
3428
+ candidates.push(join6(pkgRoot, "..", "ui-dist"));
3429
+ candidates.push(join6(pkgRoot, "..", "ui"));
3430
+ const cwd = process.cwd();
3431
+ candidates.push(join6(cwd, "packages", "nextclaw-ui", "dist"));
3432
+ candidates.push(join6(cwd, "nextclaw-ui", "dist"));
3433
+ candidates.push(join6(pkgRoot, "..", "nextclaw-ui", "dist"));
3434
+ candidates.push(join6(pkgRoot, "..", "..", "packages", "nextclaw-ui", "dist"));
3435
+ candidates.push(join6(pkgRoot, "..", "..", "nextclaw-ui", "dist"));
3436
+ for (const dir of candidates) {
3437
+ if (existsSync6(join6(dir, "index.html"))) {
3438
+ return dir;
3439
+ }
3440
+ }
3441
+ return null;
3442
+ }
3443
+ function openBrowser(url) {
3444
+ const platform = process.platform;
3445
+ let command;
3446
+ let args;
3447
+ if (platform === "darwin") {
3448
+ command = "open";
3449
+ args = [url];
3450
+ } else if (platform === "win32") {
3451
+ command = "cmd";
3452
+ args = ["/c", "start", "", url];
3453
+ } else {
3454
+ command = "xdg-open";
3455
+ args = [url];
3456
+ }
3457
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
3458
+ child.unref();
3459
+ }
3460
+ function makeProvider(config, options) {
3005
3461
  const provider = getProvider(config);
3006
3462
  const model = config.agents.defaults.model;
3007
3463
  if (!provider?.apiKey && !model.startsWith("bedrock/")) {
3464
+ if (options?.allowMissing) {
3465
+ return null;
3466
+ }
3008
3467
  console.error("Error: No API key configured.");
3009
- console.error("Set one in ~/.nextclaw/config.json under providers section");
3468
+ console.error(`Set one in ${getConfigPath()} under providers section`);
3010
3469
  process.exit(1);
3011
3470
  }
3012
3471
  return new LiteLLMProvider({
@@ -3020,25 +3479,40 @@ function makeProvider(config) {
3020
3479
  function createWorkspaceTemplates(workspace) {
3021
3480
  const templates = {
3022
3481
  "AGENTS.md": "# Agent Instructions\n\nYou are a helpful AI assistant. Be concise, accurate, and friendly.\n\n## Guidelines\n\n- Always explain what you're doing before taking actions\n- Ask for clarification when the request is ambiguous\n- Use tools to help accomplish tasks\n- Remember important information in your memory files\n",
3023
- "SOUL.md": "# Soul\n\nI am nextclaw, a lightweight AI assistant.\n\n## Personality\n\n- Helpful and friendly\n- Concise and to the point\n- Curious and eager to learn\n\n## Values\n\n- Accuracy over speed\n- User privacy and safety\n- Transparency in actions\n",
3482
+ "SOUL.md": `# Soul
3483
+
3484
+ I am ${APP_NAME}, a lightweight AI assistant.
3485
+
3486
+ ## Personality
3487
+
3488
+ - Helpful and friendly
3489
+ - Concise and to the point
3490
+ - Curious and eager to learn
3491
+
3492
+ ## Values
3493
+
3494
+ - Accuracy over speed
3495
+ - User privacy and safety
3496
+ - Transparency in actions
3497
+ `,
3024
3498
  "USER.md": "# User\n\nInformation about the user goes here.\n\n## Preferences\n\n- Communication style: (casual/formal)\n- Timezone: (your timezone)\n- Language: (your preferred language)\n"
3025
3499
  };
3026
3500
  for (const [filename, content] of Object.entries(templates)) {
3027
- const filePath = join5(workspace, filename);
3028
- if (!existsSync5(filePath)) {
3501
+ const filePath = join6(workspace, filename);
3502
+ if (!existsSync6(filePath)) {
3029
3503
  writeFileSync4(filePath, content);
3030
3504
  }
3031
3505
  }
3032
- const memoryDir = join5(workspace, "memory");
3506
+ const memoryDir = join6(workspace, "memory");
3033
3507
  mkdirSync5(memoryDir, { recursive: true });
3034
- const memoryFile = join5(memoryDir, "MEMORY.md");
3035
- if (!existsSync5(memoryFile)) {
3508
+ const memoryFile = join6(memoryDir, "MEMORY.md");
3509
+ if (!existsSync6(memoryFile)) {
3036
3510
  writeFileSync4(
3037
3511
  memoryFile,
3038
3512
  "# Long-term Memory\n\nThis file stores important information that should persist across sessions.\n\n## User Information\n\n(Important facts about the user)\n\n## Preferences\n\n(User preferences learned over time)\n\n## Important Notes\n\n(Things to remember)\n"
3039
3513
  );
3040
3514
  }
3041
- const skillsDir = join5(workspace, "skills");
3515
+ const skillsDir = join6(workspace, "skills");
3042
3516
  mkdirSync5(skillsDir, { recursive: true });
3043
3517
  }
3044
3518
  function printAgentResponse(response) {
@@ -3052,8 +3526,8 @@ async function prompt(rl, question) {
3052
3526
  });
3053
3527
  }
3054
3528
  function getBridgeDir() {
3055
- const userBridge = join5(getDataDir(), "bridge");
3056
- if (existsSync5(join5(userBridge, "dist", "index.js"))) {
3529
+ const userBridge = join6(getDataDir(), "bridge");
3530
+ if (existsSync6(join6(userBridge, "dist", "index.js"))) {
3057
3531
  return userBridge;
3058
3532
  }
3059
3533
  if (!which("npm")) {
@@ -3062,21 +3536,21 @@ function getBridgeDir() {
3062
3536
  }
3063
3537
  const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
3064
3538
  const pkgRoot = resolve(cliDir, "..", "..");
3065
- const pkgBridge = join5(pkgRoot, "bridge");
3066
- const srcBridge = join5(pkgRoot, "..", "..", "bridge");
3539
+ const pkgBridge = join6(pkgRoot, "bridge");
3540
+ const srcBridge = join6(pkgRoot, "..", "..", "bridge");
3067
3541
  let source = null;
3068
- if (existsSync5(join5(pkgBridge, "package.json"))) {
3542
+ if (existsSync6(join6(pkgBridge, "package.json"))) {
3069
3543
  source = pkgBridge;
3070
- } else if (existsSync5(join5(srcBridge, "package.json"))) {
3544
+ } else if (existsSync6(join6(srcBridge, "package.json"))) {
3071
3545
  source = srcBridge;
3072
3546
  }
3073
3547
  if (!source) {
3074
- console.error("Bridge source not found. Try reinstalling nextclaw.");
3548
+ console.error(`Bridge source not found. Try reinstalling ${APP_NAME}.`);
3075
3549
  process.exit(1);
3076
3550
  }
3077
3551
  console.log(`${LOGO} Setting up bridge...`);
3078
3552
  mkdirSync5(resolve(userBridge, ".."), { recursive: true });
3079
- if (existsSync5(userBridge)) {
3553
+ if (existsSync6(userBridge)) {
3080
3554
  rmSync(userBridge, { recursive: true, force: true });
3081
3555
  }
3082
3556
  cpSync(source, userBridge, {
@@ -3106,7 +3580,7 @@ function getPackageVersion() {
3106
3580
  try {
3107
3581
  const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
3108
3582
  const pkgPath = resolve(cliDir, "..", "..", "package.json");
3109
- const raw = readFileSync4(pkgPath, "utf-8");
3583
+ const raw = readFileSync5(pkgPath, "utf-8");
3110
3584
  const parsed = JSON.parse(raw);
3111
3585
  return typeof parsed.version === "string" ? parsed.version : "0.0.0";
3112
3586
  } catch {
@@ -3116,10 +3590,65 @@ function getPackageVersion() {
3116
3590
  function which(binary) {
3117
3591
  const paths = (process.env.PATH ?? "").split(":");
3118
3592
  for (const dir of paths) {
3119
- const full = join5(dir, binary);
3120
- if (existsSync5(full)) {
3593
+ const full = join6(dir, binary);
3594
+ if (existsSync6(full)) {
3121
3595
  return true;
3122
3596
  }
3123
3597
  }
3124
3598
  return false;
3125
3599
  }
3600
+ function startUiFrontend(options) {
3601
+ const uiDir = options.dir ?? resolveUiFrontendDir();
3602
+ if (!uiDir) {
3603
+ return null;
3604
+ }
3605
+ const runner = resolveUiFrontendRunner();
3606
+ if (!runner) {
3607
+ console.log("Warning: pnpm/npm not found. Skipping UI frontend.");
3608
+ return null;
3609
+ }
3610
+ const args = [...runner.args];
3611
+ if (options.port) {
3612
+ args.push("--", "--port", String(options.port));
3613
+ }
3614
+ const env = { ...process.env, VITE_API_BASE: options.apiBase };
3615
+ const child = spawn(runner.cmd, args, { cwd: uiDir, stdio: "inherit", env });
3616
+ child.on("exit", (code) => {
3617
+ if (code && code !== 0) {
3618
+ console.log(`UI frontend exited with code ${code}`);
3619
+ }
3620
+ });
3621
+ const url = `http://127.0.0.1:${options.port}`;
3622
+ console.log(`\u2713 UI frontend: ${url}`);
3623
+ return { url, dir: uiDir };
3624
+ }
3625
+ function resolveUiFrontendRunner() {
3626
+ if (which("pnpm")) {
3627
+ return { cmd: "pnpm", args: ["dev"] };
3628
+ }
3629
+ if (which("npm")) {
3630
+ return { cmd: "npm", args: ["run", "dev"] };
3631
+ }
3632
+ return null;
3633
+ }
3634
+ function resolveUiFrontendDir() {
3635
+ const candidates = [];
3636
+ const envDir = process.env.NEXTCLAW_UI_DIR;
3637
+ if (envDir) {
3638
+ candidates.push(envDir);
3639
+ }
3640
+ const cwd = process.cwd();
3641
+ candidates.push(join6(cwd, "packages", "nextclaw-ui"));
3642
+ candidates.push(join6(cwd, "nextclaw-ui"));
3643
+ const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
3644
+ const pkgRoot = resolve(cliDir, "..", "..");
3645
+ candidates.push(join6(pkgRoot, "..", "nextclaw-ui"));
3646
+ candidates.push(join6(pkgRoot, "..", "..", "packages", "nextclaw-ui"));
3647
+ candidates.push(join6(pkgRoot, "..", "..", "nextclaw-ui"));
3648
+ for (const dir of candidates) {
3649
+ if (existsSync6(join6(dir, "package.json"))) {
3650
+ return dir;
3651
+ }
3652
+ }
3653
+ return null;
3654
+ }