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/bridge/src/brand.ts +7 -0
- package/bridge/src/index.ts +4 -18
- package/bridge/src/whatsapp.ts +2 -1
- package/dist/chunk-HXRBJNWA.js +2324 -0
- package/dist/chunk-X77K7Y4T.js +2316 -0
- package/dist/cli/index.js +644 -115
- 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
|
@@ -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-
|
|
21
|
+
} from "../chunk-HXRBJNWA.js";
|
|
18
22
|
|
|
19
23
|
// src/cli/index.ts
|
|
20
24
|
import { Command } from "commander";
|
|
21
|
-
import { existsSync as
|
|
22
|
-
import { join as
|
|
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 (
|
|
83
|
-
console.error(`Error dispatching to ${msg.channel}: ${String(
|
|
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
|
|
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 =
|
|
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
|
|
1078
|
-
|
|
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
|
|
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, (
|
|
1134
|
-
if (
|
|
1135
|
-
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) });
|
|
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:
|
|
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) ??
|
|
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 (
|
|
2382
|
-
console.error(`Failed to start channel ${name}: ${String(
|
|
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 (
|
|
2403
|
-
console.error(`Error stopping ${name}: ${String(
|
|
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 (
|
|
2418
|
-
console.error(`Error sending to ${msg.channel}: ${String(
|
|
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 (
|
|
2587
|
+
} catch (err2) {
|
|
2578
2588
|
job.state.lastStatus = "error";
|
|
2579
|
-
job.state.lastError = String(
|
|
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(
|
|
2765
|
-
program.command("onboard").description(
|
|
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 (
|
|
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}
|
|
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(
|
|
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("
|
|
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
|
|
2785
|
-
const
|
|
2786
|
-
const
|
|
2787
|
-
const
|
|
2788
|
-
const
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
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
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
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
|
-
|
|
2832
|
-
|
|
2833
|
-
console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
|
|
3132
|
+
if (!frontendUrl && staticDir) {
|
|
3133
|
+
frontendUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
|
|
2834
3134
|
}
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
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 =
|
|
3166
|
+
const historyFile = join6(getDataDir(), "history", "cli_history");
|
|
2865
3167
|
const historyDir = resolve(historyFile, "..");
|
|
2866
3168
|
mkdirSync5(historyDir, { recursive: true });
|
|
2867
|
-
const history =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3279
|
+
const storePath = join6(getDataDir(), "cron", "jobs.json");
|
|
2978
3280
|
const service = new CronService(storePath);
|
|
2979
|
-
const
|
|
2980
|
-
console.log(
|
|
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(
|
|
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}
|
|
3288
|
+
console.log(`${LOGO} ${APP_NAME} Status
|
|
2987
3289
|
`);
|
|
2988
|
-
console.log(`Config: ${configPath} ${
|
|
2989
|
-
console.log(`Workspace: ${workspace} ${
|
|
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
|
|
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(
|
|
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":
|
|
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 =
|
|
3028
|
-
if (!
|
|
3501
|
+
const filePath = join6(workspace, filename);
|
|
3502
|
+
if (!existsSync6(filePath)) {
|
|
3029
3503
|
writeFileSync4(filePath, content);
|
|
3030
3504
|
}
|
|
3031
3505
|
}
|
|
3032
|
-
const memoryDir =
|
|
3506
|
+
const memoryDir = join6(workspace, "memory");
|
|
3033
3507
|
mkdirSync5(memoryDir, { recursive: true });
|
|
3034
|
-
const memoryFile =
|
|
3035
|
-
if (!
|
|
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 =
|
|
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 =
|
|
3056
|
-
if (
|
|
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 =
|
|
3066
|
-
const srcBridge =
|
|
3539
|
+
const pkgBridge = join6(pkgRoot, "bridge");
|
|
3540
|
+
const srcBridge = join6(pkgRoot, "..", "..", "bridge");
|
|
3067
3541
|
let source = null;
|
|
3068
|
-
if (
|
|
3542
|
+
if (existsSync6(join6(pkgBridge, "package.json"))) {
|
|
3069
3543
|
source = pkgBridge;
|
|
3070
|
-
} else if (
|
|
3544
|
+
} else if (existsSync6(join6(srcBridge, "package.json"))) {
|
|
3071
3545
|
source = srcBridge;
|
|
3072
3546
|
}
|
|
3073
3547
|
if (!source) {
|
|
3074
|
-
console.error(
|
|
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 (
|
|
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 =
|
|
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 =
|
|
3120
|
-
if (
|
|
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
|
+
}
|