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/chunk-HXRBJNWA.js +2324 -0
- package/dist/cli/index.js +606 -102
- package/dist/index.d.ts +45 -1
- package/dist/index.js +3 -1
- package/package.json +6 -3
- package/ui-dist/assets/index-BrN4G7FO.js +240 -0
- package/ui-dist/assets/index-VjHB2nG6.css +1 -0
- package/ui-dist/index.html +14 -0
package/dist/cli/index.js
CHANGED
|
@@ -18,13 +18,13 @@ import {
|
|
|
18
18
|
getWorkspacePath,
|
|
19
19
|
loadConfig,
|
|
20
20
|
saveConfig
|
|
21
|
-
} from "../chunk-
|
|
21
|
+
} from "../chunk-HXRBJNWA.js";
|
|
22
22
|
|
|
23
23
|
// src/cli/index.ts
|
|
24
24
|
import { Command } from "commander";
|
|
25
|
-
import { existsSync as
|
|
26
|
-
import { join as
|
|
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 (
|
|
87
|
-
console.error(`Error dispatching to ${msg.channel}: ${String(
|
|
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
|
|
1088
|
-
|
|
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
|
|
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, (
|
|
1144
|
-
if (
|
|
1145
|
-
resolve2({ result: false, message: String(
|
|
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 (
|
|
2392
|
-
console.error(`Failed to start channel ${name}: ${String(
|
|
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 (
|
|
2413
|
-
console.error(`Error stopping ${name}: ${String(
|
|
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 (
|
|
2428
|
-
console.error(`Error sending to ${msg.channel}: ${String(
|
|
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 (
|
|
2587
|
+
} catch (err2) {
|
|
2588
2588
|
job.state.lastStatus = "error";
|
|
2589
|
-
job.state.lastError = String(
|
|
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 (
|
|
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 (
|
|
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
|
|
2795
|
-
const
|
|
2796
|
-
const
|
|
2797
|
-
const
|
|
2798
|
-
const
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
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
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
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
|
-
|
|
2842
|
-
|
|
2843
|
-
console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
|
|
3132
|
+
if (!frontendUrl && staticDir) {
|
|
3133
|
+
frontendUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
|
|
2844
3134
|
}
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
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 =
|
|
3166
|
+
const historyFile = join6(getDataDir(), "history", "cli_history");
|
|
2875
3167
|
const historyDir = resolve(historyFile, "..");
|
|
2876
3168
|
mkdirSync5(historyDir, { recursive: true });
|
|
2877
|
-
const history =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3279
|
+
const storePath = join6(getDataDir(), "cron", "jobs.json");
|
|
2988
3280
|
const service = new CronService(storePath);
|
|
2989
|
-
const
|
|
2990
|
-
console.log(
|
|
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} ${
|
|
2999
|
-
console.log(`Workspace: ${workspace} ${
|
|
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
|
|
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 =
|
|
3053
|
-
if (!
|
|
3501
|
+
const filePath = join6(workspace, filename);
|
|
3502
|
+
if (!existsSync6(filePath)) {
|
|
3054
3503
|
writeFileSync4(filePath, content);
|
|
3055
3504
|
}
|
|
3056
3505
|
}
|
|
3057
|
-
const memoryDir =
|
|
3506
|
+
const memoryDir = join6(workspace, "memory");
|
|
3058
3507
|
mkdirSync5(memoryDir, { recursive: true });
|
|
3059
|
-
const memoryFile =
|
|
3060
|
-
if (!
|
|
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 =
|
|
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 =
|
|
3081
|
-
if (
|
|
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 =
|
|
3091
|
-
const srcBridge =
|
|
3539
|
+
const pkgBridge = join6(pkgRoot, "bridge");
|
|
3540
|
+
const srcBridge = join6(pkgRoot, "..", "..", "bridge");
|
|
3092
3541
|
let source = null;
|
|
3093
|
-
if (
|
|
3542
|
+
if (existsSync6(join6(pkgBridge, "package.json"))) {
|
|
3094
3543
|
source = pkgBridge;
|
|
3095
|
-
} else if (
|
|
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 (
|
|
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 =
|
|
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 =
|
|
3145
|
-
if (
|
|
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
|
+
}
|