nextclaw 0.2.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
@@ -18,13 +18,13 @@ import {
18
18
  getWorkspacePath,
19
19
  loadConfig,
20
20
  saveConfig
21
- } from "../chunk-X77K7Y4T.js";
21
+ } from "../chunk-HXRBJNWA.js";
22
22
 
23
23
  // src/cli/index.ts
24
24
  import { Command } from "commander";
25
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, cpSync, rmSync } from "fs";
26
- import { join as join5, resolve } from "path";
27
- 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";
28
28
  import { createInterface } from "readline";
29
29
  import { fileURLToPath } from "url";
30
30
 
@@ -83,8 +83,8 @@ var MessageBus = class {
83
83
  for (const callback of subscribers) {
84
84
  try {
85
85
  await callback(msg);
86
- } catch (err) {
87
- console.error(`Error dispatching to ${msg.channel}: ${String(err)}`);
86
+ } catch (err2) {
87
+ console.error(`Error dispatching to ${msg.channel}: ${String(err2)}`);
88
88
  }
89
89
  }
90
90
  }
@@ -1084,12 +1084,12 @@ var MochatChannel = class extends BaseChannel {
1084
1084
  async subscribeAll() {
1085
1085
  const sessions = Array.from(this.sessionSet).sort();
1086
1086
  const panels = Array.from(this.panelSet).sort();
1087
- let ok = await this.subscribeSessions(sessions);
1088
- ok = await this.subscribePanels(panels) && ok;
1087
+ let ok2 = await this.subscribeSessions(sessions);
1088
+ ok2 = await this.subscribePanels(panels) && ok2;
1089
1089
  if (this.autoDiscoverSessions || this.autoDiscoverPanels) {
1090
1090
  await this.refreshTargets(true);
1091
1091
  }
1092
- return ok;
1092
+ return ok2;
1093
1093
  }
1094
1094
  async subscribeSessions(sessionIds) {
1095
1095
  if (!sessionIds.length) {
@@ -1140,9 +1140,9 @@ var MochatChannel = class extends BaseChannel {
1140
1140
  return { result: false, message: "socket not connected" };
1141
1141
  }
1142
1142
  return new Promise((resolve2) => {
1143
- this.socket?.timeout(1e4).emit(eventName, payload, (err, response) => {
1144
- if (err) {
1145
- 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) });
1146
1146
  return;
1147
1147
  }
1148
1148
  if (response && typeof response === "object") {
@@ -2388,8 +2388,8 @@ var ChannelManager = class {
2388
2388
  async startChannel(name, channel) {
2389
2389
  try {
2390
2390
  await channel.start();
2391
- } catch (err) {
2392
- console.error(`Failed to start channel ${name}: ${String(err)}`);
2391
+ } catch (err2) {
2392
+ console.error(`Failed to start channel ${name}: ${String(err2)}`);
2393
2393
  }
2394
2394
  }
2395
2395
  async startAll() {
@@ -2409,8 +2409,8 @@ var ChannelManager = class {
2409
2409
  const tasks = Object.entries(this.channels).map(async ([name, channel]) => {
2410
2410
  try {
2411
2411
  await channel.stop();
2412
- } catch (err) {
2413
- console.error(`Error stopping ${name}: ${String(err)}`);
2412
+ } catch (err2) {
2413
+ console.error(`Error stopping ${name}: ${String(err2)}`);
2414
2414
  }
2415
2415
  });
2416
2416
  await Promise.allSettled(tasks);
@@ -2424,8 +2424,8 @@ var ChannelManager = class {
2424
2424
  }
2425
2425
  try {
2426
2426
  await channel.send(msg);
2427
- } catch (err) {
2428
- console.error(`Error sending to ${msg.channel}: ${String(err)}`);
2427
+ } catch (err2) {
2428
+ console.error(`Error sending to ${msg.channel}: ${String(err2)}`);
2429
2429
  }
2430
2430
  }
2431
2431
  }
@@ -2584,9 +2584,9 @@ var CronService = class {
2584
2584
  }
2585
2585
  job.state.lastStatus = "ok";
2586
2586
  job.state.lastError = null;
2587
- } catch (err) {
2587
+ } catch (err2) {
2588
2588
  job.state.lastStatus = "error";
2589
- job.state.lastError = String(err);
2589
+ job.state.lastError = String(err2);
2590
2590
  }
2591
2591
  job.state.lastRunAtMs = start;
2592
2592
  job.updatedAtMs = nowMs();
@@ -2766,6 +2766,289 @@ var HeartbeatService = class {
2766
2766
  }
2767
2767
  };
2768
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
+
2769
3052
  // src/cli/index.ts
2770
3053
  var LOGO = "\u{1F916}";
2771
3054
  var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "/exit", "/quit", ":q"]);
@@ -2774,7 +3057,7 @@ var program = new Command();
2774
3057
  program.name(APP_NAME).description(`${LOGO} ${APP_NAME} - ${APP_TAGLINE}`).version(VERSION, "-v, --version", "show version");
2775
3058
  program.command("onboard").description(`Initialize ${APP_NAME} configuration and workspace`).action(() => {
2776
3059
  const configPath = getConfigPath();
2777
- if (existsSync5(configPath)) {
3060
+ if (existsSync6(configPath)) {
2778
3061
  console.log(`Config already exists at ${configPath}`);
2779
3062
  }
2780
3063
  const config = ConfigSchema.parse({});
@@ -2789,63 +3072,72 @@ ${LOGO} ${APP_NAME} is ready!`);
2789
3072
  console.log(` 1. Add your API key to ${configPath}`);
2790
3073
  console.log(` 2. Chat: ${APP_NAME} agent -m "Hello!"`);
2791
3074
  });
2792
- program.command("gateway").description(`Start the ${APP_NAME} gateway`).option("-p, --port <port>", "Gateway port", "18790").option("-v, --verbose", "Verbose output", false).action(async (_opts) => {
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 });
3103
+ });
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
+ }
2793
3115
  const config = loadConfig();
2794
- const bus = new MessageBus();
2795
- const provider = makeProvider(config);
2796
- const sessionManager = new SessionManager(getWorkspacePath(config.agents.defaults.workspace));
2797
- const cronStorePath = join5(getDataDir(), "cron", "jobs.json");
2798
- const cron2 = new CronService(cronStorePath);
2799
- const agent = new AgentLoop({
2800
- bus,
2801
- provider,
2802
- workspace: getWorkspacePath(config.agents.defaults.workspace),
2803
- model: config.agents.defaults.model,
2804
- maxIterations: config.agents.defaults.maxToolIterations,
2805
- braveApiKey: config.tools.web.search.apiKey || void 0,
2806
- execConfig: config.tools.exec,
2807
- cronService: cron2,
2808
- restrictToWorkspace: config.tools.restrictToWorkspace,
2809
- sessionManager
2810
- });
2811
- cron2.onJob = async (job) => {
2812
- const response = await agent.processDirect({
2813
- content: job.payload.message,
2814
- sessionKey: `cron:${job.id}`,
2815
- channel: job.payload.channel ?? "cli",
2816
- 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
2817
3127
  });
2818
- if (job.payload.deliver && job.payload.to) {
2819
- await bus.publishOutbound({
2820
- channel: job.payload.channel ?? "cli",
2821
- chatId: job.payload.to,
2822
- content: response,
2823
- media: [],
2824
- metadata: {}
2825
- });
2826
- }
2827
- return response;
2828
- };
2829
- const heartbeat = new HeartbeatService(
2830
- getWorkspacePath(config.agents.defaults.workspace),
2831
- async (prompt2) => agent.processDirect({ content: prompt2, sessionKey: "heartbeat" }),
2832
- 30 * 60,
2833
- true
2834
- );
2835
- const channels2 = new ChannelManager(config, bus, sessionManager);
2836
- if (channels2.enabledChannels.length) {
2837
- console.log(`\u2713 Channels enabled: ${channels2.enabledChannels.join(", ")}`);
2838
- } else {
2839
- 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.");
2840
3131
  }
2841
- const cronStatus = cron2.status();
2842
- if (cronStatus.jobs > 0) {
2843
- console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
3132
+ if (!frontendUrl && staticDir) {
3133
+ frontendUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
2844
3134
  }
2845
- console.log("\u2713 Heartbeat: every 30m");
2846
- await cron2.start();
2847
- await heartbeat.start();
2848
- 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 });
2849
3141
  });
2850
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) => {
2851
3143
  const config = loadConfig();
@@ -2871,10 +3163,10 @@ program.command("agent").description("Interact with the agent directly").option(
2871
3163
  }
2872
3164
  console.log(`${LOGO} Interactive mode (type exit or Ctrl+C to quit)
2873
3165
  `);
2874
- const historyFile = join5(getDataDir(), "history", "cli_history");
3166
+ const historyFile = join6(getDataDir(), "history", "cli_history");
2875
3167
  const historyDir = resolve(historyFile, "..");
2876
3168
  mkdirSync5(historyDir, { recursive: true });
2877
- 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) : [];
2878
3170
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2879
3171
  rl.on("close", () => {
2880
3172
  const merged = history.concat(rl.history ?? []);
@@ -2920,7 +3212,7 @@ channels.command("login").description("Link device via QR code").action(() => {
2920
3212
  });
2921
3213
  var cron = program.command("cron").description("Manage scheduled tasks");
2922
3214
  cron.command("list").option("-a, --all", "Include disabled jobs").action((opts) => {
2923
- const storePath = join5(getDataDir(), "cron", "jobs.json");
3215
+ const storePath = join6(getDataDir(), "cron", "jobs.json");
2924
3216
  const service = new CronService(storePath);
2925
3217
  const jobs = service.listJobs(Boolean(opts.all));
2926
3218
  if (!jobs.length) {
@@ -2940,7 +3232,7 @@ cron.command("list").option("-a, --all", "Include disabled jobs").action((opts)
2940
3232
  }
2941
3233
  });
2942
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) => {
2943
- const storePath = join5(getDataDir(), "cron", "jobs.json");
3235
+ const storePath = join6(getDataDir(), "cron", "jobs.json");
2944
3236
  const service = new CronService(storePath);
2945
3237
  let schedule = null;
2946
3238
  if (opts.every) {
@@ -2965,7 +3257,7 @@ cron.command("add").requiredOption("-n, --name <name>", "Job name").requiredOpti
2965
3257
  console.log(`\u2713 Added job '${job.name}' (${job.id})`);
2966
3258
  });
2967
3259
  cron.command("remove <jobId>").action((jobId) => {
2968
- const storePath = join5(getDataDir(), "cron", "jobs.json");
3260
+ const storePath = join6(getDataDir(), "cron", "jobs.json");
2969
3261
  const service = new CronService(storePath);
2970
3262
  if (service.removeJob(jobId)) {
2971
3263
  console.log(`\u2713 Removed job ${jobId}`);
@@ -2974,7 +3266,7 @@ cron.command("remove <jobId>").action((jobId) => {
2974
3266
  }
2975
3267
  });
2976
3268
  cron.command("enable <jobId>").option("--disable", "Disable instead of enable").action((jobId, opts) => {
2977
- const storePath = join5(getDataDir(), "cron", "jobs.json");
3269
+ const storePath = join6(getDataDir(), "cron", "jobs.json");
2978
3270
  const service = new CronService(storePath);
2979
3271
  const job = service.enableJob(jobId, !opts.disable);
2980
3272
  if (job) {
@@ -2984,10 +3276,10 @@ cron.command("enable <jobId>").option("--disable", "Disable instead of enable").
2984
3276
  }
2985
3277
  });
2986
3278
  cron.command("run <jobId>").option("-f, --force", "Run even if disabled").action(async (jobId, opts) => {
2987
- const storePath = join5(getDataDir(), "cron", "jobs.json");
3279
+ const storePath = join6(getDataDir(), "cron", "jobs.json");
2988
3280
  const service = new CronService(storePath);
2989
- const ok = await service.runJob(jobId, Boolean(opts.force));
2990
- 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}`);
2991
3283
  });
2992
3284
  program.command("status").description(`Show ${APP_NAME} status`).action(() => {
2993
3285
  const configPath = getConfigPath();
@@ -2995,8 +3287,8 @@ program.command("status").description(`Show ${APP_NAME} status`).action(() => {
2995
3287
  const workspace = getWorkspacePath(config.agents.defaults.workspace);
2996
3288
  console.log(`${LOGO} ${APP_NAME} Status
2997
3289
  `);
2998
- console.log(`Config: ${configPath} ${existsSync5(configPath) ? "\u2713" : "\u2717"}`);
2999
- 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"}`);
3000
3292
  console.log(`Model: ${config.agents.defaults.model}`);
3001
3293
  for (const spec of PROVIDERS) {
3002
3294
  const provider = config.providers[spec.name];
@@ -3011,10 +3303,167 @@ program.command("status").description(`Show ${APP_NAME} status`).action(() => {
3011
3303
  }
3012
3304
  });
3013
3305
  program.parseAsync(process.argv);
3014
- 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) {
3015
3461
  const provider = getProvider(config);
3016
3462
  const model = config.agents.defaults.model;
3017
3463
  if (!provider?.apiKey && !model.startsWith("bedrock/")) {
3464
+ if (options?.allowMissing) {
3465
+ return null;
3466
+ }
3018
3467
  console.error("Error: No API key configured.");
3019
3468
  console.error(`Set one in ${getConfigPath()} under providers section`);
3020
3469
  process.exit(1);
@@ -3049,21 +3498,21 @@ I am ${APP_NAME}, a lightweight AI assistant.
3049
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"
3050
3499
  };
3051
3500
  for (const [filename, content] of Object.entries(templates)) {
3052
- const filePath = join5(workspace, filename);
3053
- if (!existsSync5(filePath)) {
3501
+ const filePath = join6(workspace, filename);
3502
+ if (!existsSync6(filePath)) {
3054
3503
  writeFileSync4(filePath, content);
3055
3504
  }
3056
3505
  }
3057
- const memoryDir = join5(workspace, "memory");
3506
+ const memoryDir = join6(workspace, "memory");
3058
3507
  mkdirSync5(memoryDir, { recursive: true });
3059
- const memoryFile = join5(memoryDir, "MEMORY.md");
3060
- if (!existsSync5(memoryFile)) {
3508
+ const memoryFile = join6(memoryDir, "MEMORY.md");
3509
+ if (!existsSync6(memoryFile)) {
3061
3510
  writeFileSync4(
3062
3511
  memoryFile,
3063
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"
3064
3513
  );
3065
3514
  }
3066
- const skillsDir = join5(workspace, "skills");
3515
+ const skillsDir = join6(workspace, "skills");
3067
3516
  mkdirSync5(skillsDir, { recursive: true });
3068
3517
  }
3069
3518
  function printAgentResponse(response) {
@@ -3077,8 +3526,8 @@ async function prompt(rl, question) {
3077
3526
  });
3078
3527
  }
3079
3528
  function getBridgeDir() {
3080
- const userBridge = join5(getDataDir(), "bridge");
3081
- if (existsSync5(join5(userBridge, "dist", "index.js"))) {
3529
+ const userBridge = join6(getDataDir(), "bridge");
3530
+ if (existsSync6(join6(userBridge, "dist", "index.js"))) {
3082
3531
  return userBridge;
3083
3532
  }
3084
3533
  if (!which("npm")) {
@@ -3087,12 +3536,12 @@ function getBridgeDir() {
3087
3536
  }
3088
3537
  const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
3089
3538
  const pkgRoot = resolve(cliDir, "..", "..");
3090
- const pkgBridge = join5(pkgRoot, "bridge");
3091
- const srcBridge = join5(pkgRoot, "..", "..", "bridge");
3539
+ const pkgBridge = join6(pkgRoot, "bridge");
3540
+ const srcBridge = join6(pkgRoot, "..", "..", "bridge");
3092
3541
  let source = null;
3093
- if (existsSync5(join5(pkgBridge, "package.json"))) {
3542
+ if (existsSync6(join6(pkgBridge, "package.json"))) {
3094
3543
  source = pkgBridge;
3095
- } else if (existsSync5(join5(srcBridge, "package.json"))) {
3544
+ } else if (existsSync6(join6(srcBridge, "package.json"))) {
3096
3545
  source = srcBridge;
3097
3546
  }
3098
3547
  if (!source) {
@@ -3101,7 +3550,7 @@ function getBridgeDir() {
3101
3550
  }
3102
3551
  console.log(`${LOGO} Setting up bridge...`);
3103
3552
  mkdirSync5(resolve(userBridge, ".."), { recursive: true });
3104
- if (existsSync5(userBridge)) {
3553
+ if (existsSync6(userBridge)) {
3105
3554
  rmSync(userBridge, { recursive: true, force: true });
3106
3555
  }
3107
3556
  cpSync(source, userBridge, {
@@ -3131,7 +3580,7 @@ function getPackageVersion() {
3131
3580
  try {
3132
3581
  const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
3133
3582
  const pkgPath = resolve(cliDir, "..", "..", "package.json");
3134
- const raw = readFileSync4(pkgPath, "utf-8");
3583
+ const raw = readFileSync5(pkgPath, "utf-8");
3135
3584
  const parsed = JSON.parse(raw);
3136
3585
  return typeof parsed.version === "string" ? parsed.version : "0.0.0";
3137
3586
  } catch {
@@ -3141,10 +3590,65 @@ function getPackageVersion() {
3141
3590
  function which(binary) {
3142
3591
  const paths = (process.env.PATH ?? "").split(":");
3143
3592
  for (const dir of paths) {
3144
- const full = join5(dir, binary);
3145
- if (existsSync5(full)) {
3593
+ const full = join6(dir, binary);
3594
+ if (existsSync6(full)) {
3146
3595
  return true;
3147
3596
  }
3148
3597
  }
3149
3598
  return false;
3150
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
+ }