nextclaw 0.2.9 → 0.3.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
|
@@ -2,27 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
readFileSync,
|
|
9
|
-
writeFileSync,
|
|
10
|
-
cpSync,
|
|
11
|
-
rmSync,
|
|
12
|
-
openSync,
|
|
13
|
-
closeSync
|
|
14
|
-
} from "fs";
|
|
15
|
-
import { join, resolve } from "path";
|
|
16
|
-
import { spawn, spawnSync } from "child_process";
|
|
17
|
-
import { createInterface } from "readline";
|
|
18
|
-
import { fileURLToPath } from "url";
|
|
19
|
-
import { createServer } from "net";
|
|
20
|
-
import chokidar from "chokidar";
|
|
5
|
+
import { APP_NAME as APP_NAME2, APP_TAGLINE } from "nextclaw-core";
|
|
6
|
+
|
|
7
|
+
// src/cli/runtime.ts
|
|
21
8
|
import {
|
|
22
9
|
loadConfig,
|
|
23
10
|
saveConfig,
|
|
24
11
|
getConfigPath,
|
|
25
|
-
getDataDir,
|
|
12
|
+
getDataDir as getDataDir2,
|
|
26
13
|
ConfigSchema,
|
|
27
14
|
getApiBase,
|
|
28
15
|
getProvider,
|
|
@@ -33,485 +20,38 @@ import {
|
|
|
33
20
|
MessageBus,
|
|
34
21
|
AgentLoop,
|
|
35
22
|
LiteLLMProvider,
|
|
23
|
+
ProviderManager,
|
|
36
24
|
ChannelManager,
|
|
37
25
|
SessionManager,
|
|
38
26
|
CronService,
|
|
39
27
|
HeartbeatService,
|
|
40
28
|
PROVIDERS,
|
|
41
|
-
APP_NAME
|
|
42
|
-
APP_TAGLINE
|
|
29
|
+
APP_NAME
|
|
43
30
|
} from "nextclaw-core";
|
|
44
31
|
import { startUiServer } from "nextclaw-server";
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const uiOverrides = {};
|
|
69
|
-
if (opts.ui) {
|
|
70
|
-
uiOverrides.enabled = true;
|
|
71
|
-
}
|
|
72
|
-
if (opts.uiHost) {
|
|
73
|
-
uiOverrides.host = String(opts.uiHost);
|
|
74
|
-
}
|
|
75
|
-
if (opts.uiPort) {
|
|
76
|
-
uiOverrides.port = Number(opts.uiPort);
|
|
77
|
-
}
|
|
78
|
-
if (opts.uiOpen) {
|
|
79
|
-
uiOverrides.open = true;
|
|
80
|
-
}
|
|
81
|
-
await startGateway({ uiOverrides });
|
|
82
|
-
});
|
|
83
|
-
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) => {
|
|
84
|
-
const uiOverrides = {
|
|
85
|
-
enabled: true,
|
|
86
|
-
open: Boolean(opts.open)
|
|
87
|
-
};
|
|
88
|
-
if (opts.host) {
|
|
89
|
-
uiOverrides.host = String(opts.host);
|
|
90
|
-
}
|
|
91
|
-
if (opts.port) {
|
|
92
|
-
uiOverrides.port = Number(opts.port);
|
|
93
|
-
}
|
|
94
|
-
await startGateway({ uiOverrides, allowMissingProvider: true });
|
|
95
|
-
});
|
|
96
|
-
program.command("start").description(`Start the ${APP_NAME} gateway + UI in the background`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--frontend", "Start UI frontend dev server").option("--frontend-port <port>", "UI frontend dev server port").option("--open", "Open browser after start", false).action(async (opts) => {
|
|
97
|
-
const uiOverrides = {
|
|
98
|
-
enabled: true,
|
|
99
|
-
open: false
|
|
100
|
-
};
|
|
101
|
-
if (opts.uiHost) {
|
|
102
|
-
uiOverrides.host = String(opts.uiHost);
|
|
103
|
-
}
|
|
104
|
-
if (opts.uiPort) {
|
|
105
|
-
uiOverrides.port = Number(opts.uiPort);
|
|
106
|
-
}
|
|
107
|
-
const devMode = isDevRuntime();
|
|
108
|
-
if (devMode) {
|
|
109
|
-
const requestedUiPort = Number.isFinite(Number(opts.uiPort)) ? Number(opts.uiPort) : 18792;
|
|
110
|
-
const requestedFrontendPort = Number.isFinite(Number(opts.frontendPort)) ? Number(opts.frontendPort) : 5174;
|
|
111
|
-
const uiHost = uiOverrides.host ?? "127.0.0.1";
|
|
112
|
-
const devUiPort = await findAvailablePort(requestedUiPort, uiHost);
|
|
113
|
-
const shouldStartFrontend = opts.frontend === void 0 ? true : Boolean(opts.frontend);
|
|
114
|
-
const devFrontendPort = shouldStartFrontend ? await findAvailablePort(requestedFrontendPort, "127.0.0.1") : requestedFrontendPort;
|
|
115
|
-
uiOverrides.port = devUiPort;
|
|
116
|
-
if (requestedUiPort !== devUiPort) {
|
|
117
|
-
console.log(`Dev mode: UI port ${requestedUiPort} is in use, switched to ${devUiPort}.`);
|
|
118
|
-
}
|
|
119
|
-
if (shouldStartFrontend && requestedFrontendPort !== devFrontendPort) {
|
|
120
|
-
console.log(`Dev mode: Frontend port ${requestedFrontendPort} is in use, switched to ${devFrontendPort}.`);
|
|
121
|
-
}
|
|
122
|
-
console.log(`Dev mode: UI ${devUiPort}, Frontend ${devFrontendPort}`);
|
|
123
|
-
console.log("Dev mode runs in the foreground (Ctrl+C to stop).");
|
|
124
|
-
await runForeground({
|
|
125
|
-
uiOverrides,
|
|
126
|
-
frontend: shouldStartFrontend,
|
|
127
|
-
frontendPort: devFrontendPort,
|
|
128
|
-
open: Boolean(opts.open)
|
|
129
|
-
});
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
await startService({
|
|
133
|
-
uiOverrides,
|
|
134
|
-
frontend: Boolean(opts.frontend),
|
|
135
|
-
frontendPort: Number(opts.frontendPort),
|
|
136
|
-
open: Boolean(opts.open)
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
program.command("serve").description(`Run the ${APP_NAME} gateway + UI in the foreground`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--frontend", "Start UI frontend dev server").option("--frontend-port <port>", "UI frontend dev server port").option("--open", "Open browser after start", false).action(async (opts) => {
|
|
140
|
-
const uiOverrides = {
|
|
141
|
-
enabled: true,
|
|
142
|
-
open: false
|
|
143
|
-
};
|
|
144
|
-
if (opts.uiHost) {
|
|
145
|
-
uiOverrides.host = String(opts.uiHost);
|
|
146
|
-
}
|
|
147
|
-
if (opts.uiPort) {
|
|
148
|
-
uiOverrides.port = Number(opts.uiPort);
|
|
149
|
-
}
|
|
150
|
-
const devMode = isDevRuntime();
|
|
151
|
-
if (devMode && uiOverrides.port === void 0) {
|
|
152
|
-
uiOverrides.port = 18792;
|
|
153
|
-
}
|
|
154
|
-
const shouldStartFrontend = Boolean(opts.frontend);
|
|
155
|
-
const defaultFrontendPort = devMode ? 5174 : 5173;
|
|
156
|
-
const requestedFrontendPort = Number.isFinite(Number(opts.frontendPort)) ? Number(opts.frontendPort) : defaultFrontendPort;
|
|
157
|
-
if (devMode && uiOverrides.port !== void 0) {
|
|
158
|
-
const uiHost = uiOverrides.host ?? "127.0.0.1";
|
|
159
|
-
const uiPort = await findAvailablePort(uiOverrides.port, uiHost);
|
|
160
|
-
if (uiPort !== uiOverrides.port) {
|
|
161
|
-
console.log(`Dev mode: UI port ${uiOverrides.port} is in use, switched to ${uiPort}.`);
|
|
162
|
-
uiOverrides.port = uiPort;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const frontendPort = devMode && shouldStartFrontend ? await findAvailablePort(requestedFrontendPort, "127.0.0.1") : requestedFrontendPort;
|
|
166
|
-
if (devMode && shouldStartFrontend && frontendPort !== requestedFrontendPort) {
|
|
167
|
-
console.log(`Dev mode: Frontend port ${requestedFrontendPort} is in use, switched to ${frontendPort}.`);
|
|
168
|
-
}
|
|
169
|
-
await runForeground({
|
|
170
|
-
uiOverrides,
|
|
171
|
-
frontend: shouldStartFrontend,
|
|
172
|
-
frontendPort,
|
|
173
|
-
open: Boolean(opts.open)
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
program.command("stop").description(`Stop the ${APP_NAME} background service`).action(async () => {
|
|
177
|
-
await stopService();
|
|
178
|
-
});
|
|
179
|
-
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) => {
|
|
180
|
-
const config = loadConfig();
|
|
181
|
-
const bus = new MessageBus();
|
|
182
|
-
const provider = makeProvider(config);
|
|
183
|
-
const agentLoop = new AgentLoop({
|
|
184
|
-
bus,
|
|
185
|
-
provider,
|
|
186
|
-
workspace: getWorkspacePath(config.agents.defaults.workspace),
|
|
187
|
-
braveApiKey: config.tools.web.search.apiKey || void 0,
|
|
188
|
-
execConfig: config.tools.exec,
|
|
189
|
-
restrictToWorkspace: config.tools.restrictToWorkspace
|
|
190
|
-
});
|
|
191
|
-
if (opts.message) {
|
|
192
|
-
const response = await agentLoop.processDirect({
|
|
193
|
-
content: opts.message,
|
|
194
|
-
sessionKey: opts.session,
|
|
195
|
-
channel: "cli",
|
|
196
|
-
chatId: "direct"
|
|
197
|
-
});
|
|
198
|
-
printAgentResponse(response);
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
console.log(`${LOGO} Interactive mode (type exit or Ctrl+C to quit)
|
|
202
|
-
`);
|
|
203
|
-
const historyFile = join(getDataDir(), "history", "cli_history");
|
|
204
|
-
const historyDir = resolve(historyFile, "..");
|
|
205
|
-
mkdirSync(historyDir, { recursive: true });
|
|
206
|
-
const history = existsSync(historyFile) ? readFileSync(historyFile, "utf-8").split("\n").filter(Boolean) : [];
|
|
207
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
208
|
-
rl.on("close", () => {
|
|
209
|
-
const merged = history.concat(rl.history ?? []);
|
|
210
|
-
writeFileSync(historyFile, merged.join("\n"));
|
|
211
|
-
process.exit(0);
|
|
212
|
-
});
|
|
213
|
-
let running = true;
|
|
214
|
-
while (running) {
|
|
215
|
-
const line = await prompt(rl, "You: ");
|
|
216
|
-
const trimmed = line.trim();
|
|
217
|
-
if (!trimmed) {
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
if (EXIT_COMMANDS.has(trimmed.toLowerCase())) {
|
|
221
|
-
rl.close();
|
|
222
|
-
running = false;
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
const response = await agentLoop.processDirect({ content: trimmed, sessionKey: opts.session });
|
|
226
|
-
printAgentResponse(response);
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
var channels = program.command("channels").description("Manage channels");
|
|
230
|
-
channels.command("status").description("Show channel status").action(() => {
|
|
231
|
-
const config = loadConfig();
|
|
232
|
-
console.log("Channel Status");
|
|
233
|
-
console.log(`WhatsApp: ${config.channels.whatsapp.enabled ? "\u2713" : "\u2717"}`);
|
|
234
|
-
console.log(`Discord: ${config.channels.discord.enabled ? "\u2713" : "\u2717"}`);
|
|
235
|
-
console.log(`Feishu: ${config.channels.feishu.enabled ? "\u2713" : "\u2717"}`);
|
|
236
|
-
console.log(`Mochat: ${config.channels.mochat.enabled ? "\u2713" : "\u2717"}`);
|
|
237
|
-
console.log(`Telegram: ${config.channels.telegram.enabled ? "\u2713" : "\u2717"}`);
|
|
238
|
-
console.log(`Slack: ${config.channels.slack.enabled ? "\u2713" : "\u2717"}`);
|
|
239
|
-
console.log(`QQ: ${config.channels.qq.enabled ? "\u2713" : "\u2717"}`);
|
|
240
|
-
});
|
|
241
|
-
channels.command("login").description("Link device via QR code").action(() => {
|
|
242
|
-
const bridgeDir = getBridgeDir();
|
|
243
|
-
console.log(`${LOGO} Starting bridge...`);
|
|
244
|
-
console.log("Scan the QR code to connect.\n");
|
|
245
|
-
const result = spawnSync("npm", ["start"], { cwd: bridgeDir, stdio: "inherit" });
|
|
246
|
-
if (result.status !== 0) {
|
|
247
|
-
console.error(`Bridge failed: ${result.status ?? 1}`);
|
|
248
|
-
}
|
|
249
|
-
});
|
|
250
|
-
var cron = program.command("cron").description("Manage scheduled tasks");
|
|
251
|
-
cron.command("list").option("-a, --all", "Include disabled jobs").action((opts) => {
|
|
252
|
-
const storePath = join(getDataDir(), "cron", "jobs.json");
|
|
253
|
-
const service = new CronService(storePath);
|
|
254
|
-
const jobs = service.listJobs(Boolean(opts.all));
|
|
255
|
-
if (!jobs.length) {
|
|
256
|
-
console.log("No scheduled jobs.");
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
for (const job of jobs) {
|
|
260
|
-
let schedule = "";
|
|
261
|
-
if (job.schedule.kind === "every") {
|
|
262
|
-
schedule = `every ${Math.round((job.schedule.everyMs ?? 0) / 1e3)}s`;
|
|
263
|
-
} else if (job.schedule.kind === "cron") {
|
|
264
|
-
schedule = job.schedule.expr ?? "";
|
|
265
|
-
} else {
|
|
266
|
-
schedule = job.schedule.atMs ? new Date(job.schedule.atMs).toISOString() : "";
|
|
267
|
-
}
|
|
268
|
-
console.log(`${job.id} ${job.name} ${schedule}`);
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
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) => {
|
|
272
|
-
const storePath = join(getDataDir(), "cron", "jobs.json");
|
|
273
|
-
const service = new CronService(storePath);
|
|
274
|
-
let schedule = null;
|
|
275
|
-
if (opts.every) {
|
|
276
|
-
schedule = { kind: "every", everyMs: Number(opts.every) * 1e3 };
|
|
277
|
-
} else if (opts.cron) {
|
|
278
|
-
schedule = { kind: "cron", expr: String(opts.cron) };
|
|
279
|
-
} else if (opts.at) {
|
|
280
|
-
schedule = { kind: "at", atMs: Date.parse(String(opts.at)) };
|
|
281
|
-
}
|
|
282
|
-
if (!schedule) {
|
|
283
|
-
console.error("Error: Must specify --every, --cron, or --at");
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
const job = service.addJob({
|
|
287
|
-
name: opts.name,
|
|
288
|
-
schedule,
|
|
289
|
-
message: opts.message,
|
|
290
|
-
deliver: Boolean(opts.deliver),
|
|
291
|
-
channel: opts.channel,
|
|
292
|
-
to: opts.to
|
|
293
|
-
});
|
|
294
|
-
console.log(`\u2713 Added job '${job.name}' (${job.id})`);
|
|
295
|
-
});
|
|
296
|
-
cron.command("remove <jobId>").action((jobId) => {
|
|
297
|
-
const storePath = join(getDataDir(), "cron", "jobs.json");
|
|
298
|
-
const service = new CronService(storePath);
|
|
299
|
-
if (service.removeJob(jobId)) {
|
|
300
|
-
console.log(`\u2713 Removed job ${jobId}`);
|
|
301
|
-
} else {
|
|
302
|
-
console.log(`Job ${jobId} not found`);
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
cron.command("enable <jobId>").option("--disable", "Disable instead of enable").action((jobId, opts) => {
|
|
306
|
-
const storePath = join(getDataDir(), "cron", "jobs.json");
|
|
307
|
-
const service = new CronService(storePath);
|
|
308
|
-
const job = service.enableJob(jobId, !opts.disable);
|
|
309
|
-
if (job) {
|
|
310
|
-
console.log(`\u2713 Job '${job.name}' ${opts.disable ? "disabled" : "enabled"}`);
|
|
311
|
-
} else {
|
|
312
|
-
console.log(`Job ${jobId} not found`);
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
cron.command("run <jobId>").option("-f, --force", "Run even if disabled").action(async (jobId, opts) => {
|
|
316
|
-
const storePath = join(getDataDir(), "cron", "jobs.json");
|
|
317
|
-
const service = new CronService(storePath);
|
|
318
|
-
const ok = await service.runJob(jobId, Boolean(opts.force));
|
|
319
|
-
console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
|
|
320
|
-
});
|
|
321
|
-
program.command("status").description(`Show ${APP_NAME} status`).action(() => {
|
|
322
|
-
const configPath = getConfigPath();
|
|
323
|
-
const config = loadConfig();
|
|
324
|
-
const workspace = getWorkspacePath(config.agents.defaults.workspace);
|
|
325
|
-
console.log(`${LOGO} ${APP_NAME} Status
|
|
326
|
-
`);
|
|
327
|
-
console.log(`Config: ${configPath} ${existsSync(configPath) ? "\u2713" : "\u2717"}`);
|
|
328
|
-
console.log(`Workspace: ${workspace} ${existsSync(workspace) ? "\u2713" : "\u2717"}`);
|
|
329
|
-
console.log(`Model: ${config.agents.defaults.model}`);
|
|
330
|
-
for (const spec of PROVIDERS) {
|
|
331
|
-
const provider = config.providers[spec.name];
|
|
332
|
-
if (!provider) {
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
if (spec.isLocal) {
|
|
336
|
-
console.log(`${spec.displayName ?? spec.name}: ${provider.apiBase ? `\u2713 ${provider.apiBase}` : "not set"}`);
|
|
337
|
-
} else {
|
|
338
|
-
console.log(`${spec.displayName ?? spec.name}: ${provider.apiKey ? "\u2713" : "not set"}`);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
program.parseAsync(process.argv);
|
|
343
|
-
async function startGateway(options = {}) {
|
|
344
|
-
const config = loadConfig();
|
|
345
|
-
const bus = new MessageBus();
|
|
346
|
-
const provider = options.allowMissingProvider === true ? makeProvider(config, { allowMissing: true }) : makeProvider(config);
|
|
347
|
-
const sessionManager = new SessionManager(getWorkspacePath(config.agents.defaults.workspace));
|
|
348
|
-
const cronStorePath = join(getDataDir(), "cron", "jobs.json");
|
|
349
|
-
const cron2 = new CronService(cronStorePath);
|
|
350
|
-
const uiConfig = resolveUiConfig(config, options.uiOverrides);
|
|
351
|
-
const uiStaticDir = options.uiStaticDir === void 0 ? resolveUiStaticDir() : options.uiStaticDir;
|
|
352
|
-
if (!provider) {
|
|
353
|
-
if (uiConfig.enabled) {
|
|
354
|
-
const uiServer = startUiServer({
|
|
355
|
-
host: uiConfig.host,
|
|
356
|
-
port: uiConfig.port,
|
|
357
|
-
configPath: getConfigPath(),
|
|
358
|
-
staticDir: uiStaticDir ?? void 0
|
|
359
|
-
});
|
|
360
|
-
const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
|
|
361
|
-
console.log(`\u2713 UI API: ${uiUrl}/api`);
|
|
362
|
-
if (uiStaticDir) {
|
|
363
|
-
console.log(`\u2713 UI frontend: ${uiUrl}`);
|
|
364
|
-
}
|
|
365
|
-
if (uiConfig.open) {
|
|
366
|
-
openBrowser(uiUrl);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
console.log("Warning: No API key configured. UI server only.");
|
|
370
|
-
await new Promise(() => {
|
|
371
|
-
});
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
const agent = new AgentLoop({
|
|
375
|
-
bus,
|
|
376
|
-
provider,
|
|
377
|
-
workspace: getWorkspacePath(config.agents.defaults.workspace),
|
|
378
|
-
model: config.agents.defaults.model,
|
|
379
|
-
maxIterations: config.agents.defaults.maxToolIterations,
|
|
380
|
-
braveApiKey: config.tools.web.search.apiKey || void 0,
|
|
381
|
-
execConfig: config.tools.exec,
|
|
382
|
-
cronService: cron2,
|
|
383
|
-
restrictToWorkspace: config.tools.restrictToWorkspace,
|
|
384
|
-
sessionManager
|
|
385
|
-
});
|
|
386
|
-
cron2.onJob = async (job) => {
|
|
387
|
-
const response = await agent.processDirect({
|
|
388
|
-
content: job.payload.message,
|
|
389
|
-
sessionKey: `cron:${job.id}`,
|
|
390
|
-
channel: job.payload.channel ?? "cli",
|
|
391
|
-
chatId: job.payload.to ?? "direct"
|
|
392
|
-
});
|
|
393
|
-
if (job.payload.deliver && job.payload.to) {
|
|
394
|
-
await bus.publishOutbound({
|
|
395
|
-
channel: job.payload.channel ?? "cli",
|
|
396
|
-
chatId: job.payload.to,
|
|
397
|
-
content: response,
|
|
398
|
-
media: [],
|
|
399
|
-
metadata: {}
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
return response;
|
|
403
|
-
};
|
|
404
|
-
const heartbeat = new HeartbeatService(
|
|
405
|
-
getWorkspacePath(config.agents.defaults.workspace),
|
|
406
|
-
async (prompt2) => agent.processDirect({ content: prompt2, sessionKey: "heartbeat" }),
|
|
407
|
-
30 * 60,
|
|
408
|
-
true
|
|
409
|
-
);
|
|
410
|
-
let currentConfig = config;
|
|
411
|
-
let channels2 = new ChannelManager(currentConfig, bus, sessionManager);
|
|
412
|
-
let reloadTask = null;
|
|
413
|
-
const reloadChannels = async (nextConfig) => {
|
|
414
|
-
if (reloadTask) {
|
|
415
|
-
await reloadTask;
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
reloadTask = (async () => {
|
|
419
|
-
await channels2.stopAll();
|
|
420
|
-
channels2 = new ChannelManager(nextConfig, bus, sessionManager);
|
|
421
|
-
await channels2.startAll();
|
|
422
|
-
})();
|
|
423
|
-
try {
|
|
424
|
-
await reloadTask;
|
|
425
|
-
} finally {
|
|
426
|
-
reloadTask = null;
|
|
427
|
-
}
|
|
428
|
-
};
|
|
429
|
-
const applyReloadPlan = async (nextConfig) => {
|
|
430
|
-
const changedPaths = diffConfigPaths(currentConfig, nextConfig);
|
|
431
|
-
if (!changedPaths.length) {
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
currentConfig = nextConfig;
|
|
435
|
-
const plan = buildReloadPlan(changedPaths);
|
|
436
|
-
if (plan.restartChannels) {
|
|
437
|
-
await reloadChannels(nextConfig);
|
|
438
|
-
}
|
|
439
|
-
if (plan.restartRequired.length > 0) {
|
|
440
|
-
console.warn(`Config changes require restart: ${plan.restartRequired.join(", ")}`);
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
let reloadTimer = null;
|
|
444
|
-
let reloadRunning = false;
|
|
445
|
-
let reloadPending = false;
|
|
446
|
-
const scheduleConfigReload = (reason) => {
|
|
447
|
-
if (reloadTimer) {
|
|
448
|
-
clearTimeout(reloadTimer);
|
|
449
|
-
}
|
|
450
|
-
reloadTimer = setTimeout(() => {
|
|
451
|
-
void runConfigReload(reason);
|
|
452
|
-
}, 300);
|
|
453
|
-
};
|
|
454
|
-
const runConfigReload = async (reason) => {
|
|
455
|
-
if (reloadRunning) {
|
|
456
|
-
reloadPending = true;
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
reloadRunning = true;
|
|
460
|
-
if (reloadTimer) {
|
|
461
|
-
clearTimeout(reloadTimer);
|
|
462
|
-
reloadTimer = null;
|
|
463
|
-
}
|
|
464
|
-
try {
|
|
465
|
-
const nextConfig = loadConfig();
|
|
466
|
-
await applyReloadPlan(nextConfig);
|
|
467
|
-
} catch (error) {
|
|
468
|
-
console.error(`Config reload failed (${reason}): ${String(error)}`);
|
|
469
|
-
} finally {
|
|
470
|
-
reloadRunning = false;
|
|
471
|
-
if (reloadPending) {
|
|
472
|
-
reloadPending = false;
|
|
473
|
-
scheduleConfigReload("pending");
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
if (channels2.enabledChannels.length) {
|
|
478
|
-
console.log(`\u2713 Channels enabled: ${channels2.enabledChannels.join(", ")}`);
|
|
479
|
-
} else {
|
|
480
|
-
console.log("Warning: No channels enabled");
|
|
481
|
-
}
|
|
482
|
-
if (uiConfig.enabled) {
|
|
483
|
-
const uiServer = startUiServer({
|
|
484
|
-
host: uiConfig.host,
|
|
485
|
-
port: uiConfig.port,
|
|
486
|
-
configPath: getConfigPath(),
|
|
487
|
-
staticDir: uiStaticDir ?? void 0
|
|
488
|
-
});
|
|
489
|
-
const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
|
|
490
|
-
console.log(`\u2713 UI API: ${uiUrl}/api`);
|
|
491
|
-
if (uiStaticDir) {
|
|
492
|
-
console.log(`\u2713 UI frontend: ${uiUrl}`);
|
|
493
|
-
}
|
|
494
|
-
if (uiConfig.open) {
|
|
495
|
-
openBrowser(uiUrl);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
const cronStatus = cron2.status();
|
|
499
|
-
if (cronStatus.jobs > 0) {
|
|
500
|
-
console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
|
|
501
|
-
}
|
|
502
|
-
console.log("\u2713 Heartbeat: every 30m");
|
|
503
|
-
const configPath = getConfigPath();
|
|
504
|
-
const watcher = chokidar.watch(configPath, {
|
|
505
|
-
ignoreInitial: true,
|
|
506
|
-
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
|
|
507
|
-
});
|
|
508
|
-
watcher.on("add", () => scheduleConfigReload("config add"));
|
|
509
|
-
watcher.on("change", () => scheduleConfigReload("config change"));
|
|
510
|
-
watcher.on("unlink", () => scheduleConfigReload("config unlink"));
|
|
511
|
-
await cron2.start();
|
|
512
|
-
await heartbeat.start();
|
|
513
|
-
await Promise.allSettled([agent.run(), channels2.startAll()]);
|
|
514
|
-
}
|
|
32
|
+
import {
|
|
33
|
+
closeSync,
|
|
34
|
+
cpSync,
|
|
35
|
+
existsSync as existsSync2,
|
|
36
|
+
mkdirSync as mkdirSync2,
|
|
37
|
+
openSync,
|
|
38
|
+
readFileSync as readFileSync2,
|
|
39
|
+
rmSync as rmSync2,
|
|
40
|
+
writeFileSync as writeFileSync2
|
|
41
|
+
} from "fs";
|
|
42
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
43
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
44
|
+
import { createInterface } from "readline";
|
|
45
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
46
|
+
import chokidar from "chokidar";
|
|
47
|
+
|
|
48
|
+
// src/cli/utils.ts
|
|
49
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "fs";
|
|
50
|
+
import { join, resolve } from "path";
|
|
51
|
+
import { spawn } from "child_process";
|
|
52
|
+
import { createServer } from "net";
|
|
53
|
+
import { fileURLToPath } from "url";
|
|
54
|
+
import { getDataDir } from "nextclaw-core";
|
|
515
55
|
function resolveUiConfig(config, overrides) {
|
|
516
56
|
const base = config.ui ?? { enabled: false, host: "127.0.0.1", port: 18791, open: false };
|
|
517
57
|
return { ...base, ...overrides ?? {} };
|
|
@@ -546,149 +86,26 @@ async function isPortAvailable(port, host) {
|
|
|
546
86
|
} else if (checkHost === "::1") {
|
|
547
87
|
hostsToCheck.push("127.0.0.1");
|
|
548
88
|
}
|
|
549
|
-
for (const
|
|
550
|
-
const ok = await canBindPort(port,
|
|
551
|
-
if (
|
|
552
|
-
return
|
|
89
|
+
for (const candidate of hostsToCheck) {
|
|
90
|
+
const ok = await canBindPort(port, candidate);
|
|
91
|
+
if (ok) {
|
|
92
|
+
return true;
|
|
553
93
|
}
|
|
554
94
|
}
|
|
555
|
-
return
|
|
95
|
+
return false;
|
|
556
96
|
}
|
|
557
97
|
async function canBindPort(port, host) {
|
|
558
|
-
return await new Promise((
|
|
98
|
+
return await new Promise((resolve3) => {
|
|
559
99
|
const server = createServer();
|
|
560
100
|
server.unref();
|
|
561
|
-
server.once("error", () =>
|
|
101
|
+
server.once("error", () => resolve3(false));
|
|
562
102
|
server.listen({ port, host }, () => {
|
|
563
|
-
server.close(() =>
|
|
103
|
+
server.close(() => resolve3(true));
|
|
564
104
|
});
|
|
565
105
|
});
|
|
566
106
|
}
|
|
567
|
-
async function runForeground(options) {
|
|
568
|
-
const config = loadConfig();
|
|
569
|
-
const uiConfig = resolveUiConfig(config, options.uiOverrides);
|
|
570
|
-
const shouldStartFrontend = options.frontend;
|
|
571
|
-
const frontendPort = Number.isFinite(options.frontendPort) ? options.frontendPort : 5173;
|
|
572
|
-
const frontendDir = shouldStartFrontend ? resolveUiFrontendDir() : null;
|
|
573
|
-
const staticDir = resolveUiStaticDir();
|
|
574
|
-
let frontendUrl = null;
|
|
575
|
-
if (shouldStartFrontend && frontendDir) {
|
|
576
|
-
const frontend = startUiFrontend({
|
|
577
|
-
apiBase: resolveUiApiBase(uiConfig.host, uiConfig.port),
|
|
578
|
-
port: frontendPort,
|
|
579
|
-
dir: frontendDir
|
|
580
|
-
});
|
|
581
|
-
frontendUrl = frontend?.url ?? null;
|
|
582
|
-
} else if (shouldStartFrontend && !frontendDir) {
|
|
583
|
-
console.log("Warning: UI frontend not found. Start it separately.");
|
|
584
|
-
}
|
|
585
|
-
if (!frontendUrl && staticDir) {
|
|
586
|
-
frontendUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
|
|
587
|
-
}
|
|
588
|
-
if (options.open && frontendUrl) {
|
|
589
|
-
openBrowser(frontendUrl);
|
|
590
|
-
} else if (options.open && !frontendUrl) {
|
|
591
|
-
console.log("Warning: UI frontend not started. Browser not opened.");
|
|
592
|
-
}
|
|
593
|
-
const uiStaticDir = shouldStartFrontend && frontendDir ? null : staticDir;
|
|
594
|
-
await startGateway({
|
|
595
|
-
uiOverrides: options.uiOverrides,
|
|
596
|
-
allowMissingProvider: true,
|
|
597
|
-
uiStaticDir
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
async function startService(options) {
|
|
601
|
-
const config = loadConfig();
|
|
602
|
-
const uiConfig = resolveUiConfig(config, options.uiOverrides);
|
|
603
|
-
const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
|
|
604
|
-
const apiUrl = `${uiUrl}/api`;
|
|
605
|
-
const staticDir = resolveUiStaticDir();
|
|
606
|
-
const existing = readServiceState();
|
|
607
|
-
if (existing && isProcessRunning(existing.pid)) {
|
|
608
|
-
console.log(`\u2713 ${APP_NAME} is already running (PID ${existing.pid})`);
|
|
609
|
-
console.log(`UI: ${existing.uiUrl}`);
|
|
610
|
-
console.log(`API: ${existing.apiUrl}`);
|
|
611
|
-
console.log(`Logs: ${existing.logPath}`);
|
|
612
|
-
console.log(`Stop: ${APP_NAME} stop`);
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
if (existing) {
|
|
616
|
-
clearServiceState();
|
|
617
|
-
}
|
|
618
|
-
if (!staticDir && !options.frontend) {
|
|
619
|
-
console.log("Warning: UI frontend not found. Use --frontend to start the dev server.");
|
|
620
|
-
}
|
|
621
|
-
const logPath = resolveServiceLogPath();
|
|
622
|
-
const logDir = resolve(logPath, "..");
|
|
623
|
-
mkdirSync(logDir, { recursive: true });
|
|
624
|
-
const logFd = openSync(logPath, "a");
|
|
625
|
-
const serveArgs = buildServeArgs({
|
|
626
|
-
uiHost: uiConfig.host,
|
|
627
|
-
uiPort: uiConfig.port,
|
|
628
|
-
frontend: options.frontend,
|
|
629
|
-
frontendPort: options.frontendPort
|
|
630
|
-
});
|
|
631
|
-
const child = spawn(process.execPath, [...process.execArgv, ...serveArgs], {
|
|
632
|
-
env: process.env,
|
|
633
|
-
stdio: ["ignore", logFd, logFd],
|
|
634
|
-
detached: true
|
|
635
|
-
});
|
|
636
|
-
closeSync(logFd);
|
|
637
|
-
if (!child.pid) {
|
|
638
|
-
console.error("Error: Failed to start background service.");
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
child.unref();
|
|
642
|
-
const state = {
|
|
643
|
-
pid: child.pid,
|
|
644
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
645
|
-
uiUrl,
|
|
646
|
-
apiUrl,
|
|
647
|
-
logPath
|
|
648
|
-
};
|
|
649
|
-
writeServiceState(state);
|
|
650
|
-
console.log(`\u2713 ${APP_NAME} started in background (PID ${state.pid})`);
|
|
651
|
-
console.log(`UI: ${uiUrl}`);
|
|
652
|
-
console.log(`API: ${apiUrl}`);
|
|
653
|
-
console.log(`Logs: ${logPath}`);
|
|
654
|
-
console.log(`Stop: ${APP_NAME} stop`);
|
|
655
|
-
if (options.open) {
|
|
656
|
-
openBrowser(uiUrl);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
async function stopService() {
|
|
660
|
-
const state = readServiceState();
|
|
661
|
-
if (!state) {
|
|
662
|
-
console.log("No running service found.");
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
if (!isProcessRunning(state.pid)) {
|
|
666
|
-
console.log("Service is not running. Cleaning up state.");
|
|
667
|
-
clearServiceState();
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
console.log(`Stopping ${APP_NAME} (PID ${state.pid})...`);
|
|
671
|
-
try {
|
|
672
|
-
process.kill(state.pid, "SIGTERM");
|
|
673
|
-
} catch (error) {
|
|
674
|
-
console.error(`Failed to stop service: ${String(error)}`);
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
const stopped = await waitForExit(state.pid, 3e3);
|
|
678
|
-
if (!stopped) {
|
|
679
|
-
try {
|
|
680
|
-
process.kill(state.pid, "SIGKILL");
|
|
681
|
-
} catch (error) {
|
|
682
|
-
console.error(`Failed to force stop service: ${String(error)}`);
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
await waitForExit(state.pid, 2e3);
|
|
686
|
-
}
|
|
687
|
-
clearServiceState();
|
|
688
|
-
console.log(`\u2713 ${APP_NAME} stopped`);
|
|
689
|
-
}
|
|
690
107
|
function buildServeArgs(options) {
|
|
691
|
-
const cliPath = fileURLToPath(import.meta.url);
|
|
108
|
+
const cliPath = fileURLToPath(new URL("./index.js", import.meta.url));
|
|
692
109
|
const args = [cliPath, "serve", "--ui-host", options.uiHost, "--ui-port", String(options.uiPort)];
|
|
693
110
|
if (options.frontend) {
|
|
694
111
|
args.push("--frontend");
|
|
@@ -741,7 +158,7 @@ async function waitForExit(pid, timeoutMs) {
|
|
|
741
158
|
if (!isProcessRunning(pid)) {
|
|
742
159
|
return true;
|
|
743
160
|
}
|
|
744
|
-
await new Promise((
|
|
161
|
+
await new Promise((resolve3) => setTimeout(resolve3, 200));
|
|
745
162
|
}
|
|
746
163
|
return !isProcessRunning(pid);
|
|
747
164
|
}
|
|
@@ -787,125 +204,6 @@ function openBrowser(url) {
|
|
|
787
204
|
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
788
205
|
child.unref();
|
|
789
206
|
}
|
|
790
|
-
function makeProvider(config, options) {
|
|
791
|
-
const provider = getProvider(config);
|
|
792
|
-
const model = config.agents.defaults.model;
|
|
793
|
-
if (!provider?.apiKey && !model.startsWith("bedrock/")) {
|
|
794
|
-
if (options?.allowMissing) {
|
|
795
|
-
return null;
|
|
796
|
-
}
|
|
797
|
-
console.error("Error: No API key configured.");
|
|
798
|
-
console.error(`Set one in ${getConfigPath()} under providers section`);
|
|
799
|
-
process.exit(1);
|
|
800
|
-
}
|
|
801
|
-
return new LiteLLMProvider({
|
|
802
|
-
apiKey: provider?.apiKey ?? null,
|
|
803
|
-
apiBase: getApiBase(config),
|
|
804
|
-
defaultModel: model,
|
|
805
|
-
extraHeaders: provider?.extraHeaders ?? null,
|
|
806
|
-
providerName: getProviderName(config)
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
function createWorkspaceTemplates(workspace) {
|
|
810
|
-
const templates = {
|
|
811
|
-
"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",
|
|
812
|
-
"SOUL.md": `# Soul
|
|
813
|
-
|
|
814
|
-
I am ${APP_NAME}, a lightweight AI assistant.
|
|
815
|
-
|
|
816
|
-
## Personality
|
|
817
|
-
|
|
818
|
-
- Helpful and friendly
|
|
819
|
-
- Concise and to the point
|
|
820
|
-
- Curious and eager to learn
|
|
821
|
-
|
|
822
|
-
## Values
|
|
823
|
-
|
|
824
|
-
- Accuracy over speed
|
|
825
|
-
- User privacy and safety
|
|
826
|
-
- Transparency in actions
|
|
827
|
-
`,
|
|
828
|
-
"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"
|
|
829
|
-
};
|
|
830
|
-
for (const [filename, content] of Object.entries(templates)) {
|
|
831
|
-
const filePath = join(workspace, filename);
|
|
832
|
-
if (!existsSync(filePath)) {
|
|
833
|
-
writeFileSync(filePath, content);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
const memoryDir = join(workspace, "memory");
|
|
837
|
-
mkdirSync(memoryDir, { recursive: true });
|
|
838
|
-
const memoryFile = join(memoryDir, "MEMORY.md");
|
|
839
|
-
if (!existsSync(memoryFile)) {
|
|
840
|
-
writeFileSync(
|
|
841
|
-
memoryFile,
|
|
842
|
-
"# 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"
|
|
843
|
-
);
|
|
844
|
-
}
|
|
845
|
-
const skillsDir = join(workspace, "skills");
|
|
846
|
-
mkdirSync(skillsDir, { recursive: true });
|
|
847
|
-
}
|
|
848
|
-
function printAgentResponse(response) {
|
|
849
|
-
console.log("\n" + response + "\n");
|
|
850
|
-
}
|
|
851
|
-
async function prompt(rl, question) {
|
|
852
|
-
rl.setPrompt(question);
|
|
853
|
-
rl.prompt();
|
|
854
|
-
return new Promise((resolve2) => {
|
|
855
|
-
rl.once("line", (line) => resolve2(line));
|
|
856
|
-
});
|
|
857
|
-
}
|
|
858
|
-
function getBridgeDir() {
|
|
859
|
-
const userBridge = join(getDataDir(), "bridge");
|
|
860
|
-
if (existsSync(join(userBridge, "dist", "index.js"))) {
|
|
861
|
-
return userBridge;
|
|
862
|
-
}
|
|
863
|
-
if (!which("npm")) {
|
|
864
|
-
console.error("npm not found. Please install Node.js >= 18.");
|
|
865
|
-
process.exit(1);
|
|
866
|
-
}
|
|
867
|
-
const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
868
|
-
const pkgRoot = resolve(cliDir, "..", "..");
|
|
869
|
-
const pkgBridge = join(pkgRoot, "bridge");
|
|
870
|
-
const srcBridge = join(pkgRoot, "..", "..", "bridge");
|
|
871
|
-
let source = null;
|
|
872
|
-
if (existsSync(join(pkgBridge, "package.json"))) {
|
|
873
|
-
source = pkgBridge;
|
|
874
|
-
} else if (existsSync(join(srcBridge, "package.json"))) {
|
|
875
|
-
source = srcBridge;
|
|
876
|
-
}
|
|
877
|
-
if (!source) {
|
|
878
|
-
console.error(`Bridge source not found. Try reinstalling ${APP_NAME}.`);
|
|
879
|
-
process.exit(1);
|
|
880
|
-
}
|
|
881
|
-
console.log(`${LOGO} Setting up bridge...`);
|
|
882
|
-
mkdirSync(resolve(userBridge, ".."), { recursive: true });
|
|
883
|
-
if (existsSync(userBridge)) {
|
|
884
|
-
rmSync(userBridge, { recursive: true, force: true });
|
|
885
|
-
}
|
|
886
|
-
cpSync(source, userBridge, {
|
|
887
|
-
recursive: true,
|
|
888
|
-
filter: (src) => !src.includes("node_modules") && !src.includes("dist")
|
|
889
|
-
});
|
|
890
|
-
const install = spawnSync("npm", ["install"], { cwd: userBridge, stdio: "pipe" });
|
|
891
|
-
if (install.status !== 0) {
|
|
892
|
-
console.error(`Bridge install failed: ${install.status ?? 1}`);
|
|
893
|
-
if (install.stderr) {
|
|
894
|
-
console.error(String(install.stderr).slice(0, 500));
|
|
895
|
-
}
|
|
896
|
-
process.exit(1);
|
|
897
|
-
}
|
|
898
|
-
const build = spawnSync("npm", ["run", "build"], { cwd: userBridge, stdio: "pipe" });
|
|
899
|
-
if (build.status !== 0) {
|
|
900
|
-
console.error(`Bridge build failed: ${build.status ?? 1}`);
|
|
901
|
-
if (build.stderr) {
|
|
902
|
-
console.error(String(build.stderr).slice(0, 500));
|
|
903
|
-
}
|
|
904
|
-
process.exit(1);
|
|
905
|
-
}
|
|
906
|
-
console.log("\u2713 Bridge ready\n");
|
|
907
|
-
return userBridge;
|
|
908
|
-
}
|
|
909
207
|
function getPackageVersion() {
|
|
910
208
|
try {
|
|
911
209
|
const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
@@ -985,3 +283,776 @@ function resolveUiFrontendDir() {
|
|
|
985
283
|
}
|
|
986
284
|
return null;
|
|
987
285
|
}
|
|
286
|
+
function printAgentResponse(response) {
|
|
287
|
+
console.log("\n" + response + "\n");
|
|
288
|
+
}
|
|
289
|
+
async function prompt(rl, question) {
|
|
290
|
+
rl.setPrompt(question);
|
|
291
|
+
rl.prompt();
|
|
292
|
+
return new Promise((resolve3) => {
|
|
293
|
+
rl.once("line", (line) => resolve3(line));
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/cli/runtime.ts
|
|
298
|
+
var LOGO = "\u{1F916}";
|
|
299
|
+
var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "/exit", "/quit", ":q"]);
|
|
300
|
+
var CliRuntime = class {
|
|
301
|
+
logo;
|
|
302
|
+
constructor(options = {}) {
|
|
303
|
+
this.logo = options.logo ?? LOGO;
|
|
304
|
+
}
|
|
305
|
+
get version() {
|
|
306
|
+
return getPackageVersion();
|
|
307
|
+
}
|
|
308
|
+
async onboard() {
|
|
309
|
+
const configPath = getConfigPath();
|
|
310
|
+
if (existsSync2(configPath)) {
|
|
311
|
+
console.log(`Config already exists at ${configPath}`);
|
|
312
|
+
}
|
|
313
|
+
const config = ConfigSchema.parse({});
|
|
314
|
+
saveConfig(config);
|
|
315
|
+
console.log(`\u2713 Created config at ${configPath}`);
|
|
316
|
+
const workspace = getWorkspacePath();
|
|
317
|
+
console.log(`\u2713 Created workspace at ${workspace}`);
|
|
318
|
+
this.createWorkspaceTemplates(workspace);
|
|
319
|
+
console.log(`
|
|
320
|
+
${this.logo} ${APP_NAME} is ready!`);
|
|
321
|
+
console.log("\nNext steps:");
|
|
322
|
+
console.log(` 1. Add your API key to ${configPath}`);
|
|
323
|
+
console.log(` 2. Chat: ${APP_NAME} agent -m "Hello!"`);
|
|
324
|
+
}
|
|
325
|
+
async gateway(opts) {
|
|
326
|
+
const uiOverrides = {};
|
|
327
|
+
if (opts.ui) {
|
|
328
|
+
uiOverrides.enabled = true;
|
|
329
|
+
}
|
|
330
|
+
if (opts.uiHost) {
|
|
331
|
+
uiOverrides.host = String(opts.uiHost);
|
|
332
|
+
}
|
|
333
|
+
if (opts.uiPort) {
|
|
334
|
+
uiOverrides.port = Number(opts.uiPort);
|
|
335
|
+
}
|
|
336
|
+
if (opts.uiOpen) {
|
|
337
|
+
uiOverrides.open = true;
|
|
338
|
+
}
|
|
339
|
+
await this.startGateway({ uiOverrides });
|
|
340
|
+
}
|
|
341
|
+
async ui(opts) {
|
|
342
|
+
const uiOverrides = {
|
|
343
|
+
enabled: true,
|
|
344
|
+
open: Boolean(opts.open)
|
|
345
|
+
};
|
|
346
|
+
if (opts.host) {
|
|
347
|
+
uiOverrides.host = String(opts.host);
|
|
348
|
+
}
|
|
349
|
+
if (opts.port) {
|
|
350
|
+
uiOverrides.port = Number(opts.port);
|
|
351
|
+
}
|
|
352
|
+
await this.startGateway({ uiOverrides, allowMissingProvider: true });
|
|
353
|
+
}
|
|
354
|
+
async start(opts) {
|
|
355
|
+
const uiOverrides = {
|
|
356
|
+
enabled: true,
|
|
357
|
+
open: false
|
|
358
|
+
};
|
|
359
|
+
if (opts.uiHost) {
|
|
360
|
+
uiOverrides.host = String(opts.uiHost);
|
|
361
|
+
}
|
|
362
|
+
if (opts.uiPort) {
|
|
363
|
+
uiOverrides.port = Number(opts.uiPort);
|
|
364
|
+
}
|
|
365
|
+
const devMode = isDevRuntime();
|
|
366
|
+
if (devMode) {
|
|
367
|
+
const requestedUiPort = Number.isFinite(Number(opts.uiPort)) ? Number(opts.uiPort) : 18792;
|
|
368
|
+
const requestedFrontendPort = Number.isFinite(Number(opts.frontendPort)) ? Number(opts.frontendPort) : 5174;
|
|
369
|
+
const uiHost = uiOverrides.host ?? "127.0.0.1";
|
|
370
|
+
const devUiPort = await findAvailablePort(requestedUiPort, uiHost);
|
|
371
|
+
const shouldStartFrontend = opts.frontend === void 0 ? true : Boolean(opts.frontend);
|
|
372
|
+
const devFrontendPort = shouldStartFrontend ? await findAvailablePort(requestedFrontendPort, "127.0.0.1") : requestedFrontendPort;
|
|
373
|
+
uiOverrides.port = devUiPort;
|
|
374
|
+
if (requestedUiPort !== devUiPort) {
|
|
375
|
+
console.log(`Dev mode: UI port ${requestedUiPort} is in use, switched to ${devUiPort}.`);
|
|
376
|
+
}
|
|
377
|
+
if (shouldStartFrontend && requestedFrontendPort !== devFrontendPort) {
|
|
378
|
+
console.log(`Dev mode: Frontend port ${requestedFrontendPort} is in use, switched to ${devFrontendPort}.`);
|
|
379
|
+
}
|
|
380
|
+
console.log(`Dev mode: UI ${devUiPort}, Frontend ${devFrontendPort}`);
|
|
381
|
+
console.log("Dev mode runs in the foreground (Ctrl+C to stop).");
|
|
382
|
+
await this.runForeground({
|
|
383
|
+
uiOverrides,
|
|
384
|
+
frontend: shouldStartFrontend,
|
|
385
|
+
frontendPort: devFrontendPort,
|
|
386
|
+
open: Boolean(opts.open)
|
|
387
|
+
});
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
await this.startService({
|
|
391
|
+
uiOverrides,
|
|
392
|
+
frontend: Boolean(opts.frontend),
|
|
393
|
+
frontendPort: Number(opts.frontendPort),
|
|
394
|
+
open: Boolean(opts.open)
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
async serve(opts) {
|
|
398
|
+
const uiOverrides = {
|
|
399
|
+
enabled: true,
|
|
400
|
+
open: false
|
|
401
|
+
};
|
|
402
|
+
if (opts.uiHost) {
|
|
403
|
+
uiOverrides.host = String(opts.uiHost);
|
|
404
|
+
}
|
|
405
|
+
if (opts.uiPort) {
|
|
406
|
+
uiOverrides.port = Number(opts.uiPort);
|
|
407
|
+
}
|
|
408
|
+
const devMode = isDevRuntime();
|
|
409
|
+
if (devMode && uiOverrides.port === void 0) {
|
|
410
|
+
uiOverrides.port = 18792;
|
|
411
|
+
}
|
|
412
|
+
const shouldStartFrontend = Boolean(opts.frontend);
|
|
413
|
+
const defaultFrontendPort = devMode ? 5174 : 5173;
|
|
414
|
+
const requestedFrontendPort = Number.isFinite(Number(opts.frontendPort)) ? Number(opts.frontendPort) : defaultFrontendPort;
|
|
415
|
+
if (devMode && uiOverrides.port !== void 0) {
|
|
416
|
+
const uiHost = uiOverrides.host ?? "127.0.0.1";
|
|
417
|
+
const uiPort = await findAvailablePort(uiOverrides.port, uiHost);
|
|
418
|
+
if (uiPort !== uiOverrides.port) {
|
|
419
|
+
console.log(`Dev mode: UI port ${uiOverrides.port} is in use, switched to ${uiPort}.`);
|
|
420
|
+
uiOverrides.port = uiPort;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const frontendPort = devMode && shouldStartFrontend ? await findAvailablePort(requestedFrontendPort, "127.0.0.1") : requestedFrontendPort;
|
|
424
|
+
if (devMode && shouldStartFrontend && frontendPort !== requestedFrontendPort) {
|
|
425
|
+
console.log(`Dev mode: Frontend port ${requestedFrontendPort} is in use, switched to ${frontendPort}.`);
|
|
426
|
+
}
|
|
427
|
+
await this.runForeground({
|
|
428
|
+
uiOverrides,
|
|
429
|
+
frontend: shouldStartFrontend,
|
|
430
|
+
frontendPort,
|
|
431
|
+
open: Boolean(opts.open)
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
async stop() {
|
|
435
|
+
await this.stopService();
|
|
436
|
+
}
|
|
437
|
+
async agent(opts) {
|
|
438
|
+
const config = loadConfig();
|
|
439
|
+
const bus = new MessageBus();
|
|
440
|
+
const provider = this.makeProvider(config);
|
|
441
|
+
const providerManager = new ProviderManager(provider);
|
|
442
|
+
const agentLoop = new AgentLoop({
|
|
443
|
+
bus,
|
|
444
|
+
providerManager,
|
|
445
|
+
workspace: getWorkspacePath(config.agents.defaults.workspace),
|
|
446
|
+
braveApiKey: config.tools.web.search.apiKey || void 0,
|
|
447
|
+
execConfig: config.tools.exec,
|
|
448
|
+
restrictToWorkspace: config.tools.restrictToWorkspace
|
|
449
|
+
});
|
|
450
|
+
if (opts.message) {
|
|
451
|
+
const response = await agentLoop.processDirect({
|
|
452
|
+
content: opts.message,
|
|
453
|
+
sessionKey: opts.session ?? "cli:default",
|
|
454
|
+
channel: "cli",
|
|
455
|
+
chatId: "direct"
|
|
456
|
+
});
|
|
457
|
+
printAgentResponse(response);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
console.log(`${this.logo} Interactive mode (type exit or Ctrl+C to quit)
|
|
461
|
+
`);
|
|
462
|
+
const historyFile = join2(getDataDir2(), "history", "cli_history");
|
|
463
|
+
const historyDir = resolve2(historyFile, "..");
|
|
464
|
+
mkdirSync2(historyDir, { recursive: true });
|
|
465
|
+
const history = existsSync2(historyFile) ? readFileSync2(historyFile, "utf-8").split("\n").filter(Boolean) : [];
|
|
466
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
467
|
+
rl.on("close", () => {
|
|
468
|
+
const merged = history.concat(rl.history ?? []);
|
|
469
|
+
writeFileSync2(historyFile, merged.join("\n"));
|
|
470
|
+
process.exit(0);
|
|
471
|
+
});
|
|
472
|
+
let running = true;
|
|
473
|
+
while (running) {
|
|
474
|
+
const line = await prompt(rl, "You: ");
|
|
475
|
+
const trimmed = line.trim();
|
|
476
|
+
if (!trimmed) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (EXIT_COMMANDS.has(trimmed.toLowerCase())) {
|
|
480
|
+
rl.close();
|
|
481
|
+
running = false;
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
const response = await agentLoop.processDirect({
|
|
485
|
+
content: trimmed,
|
|
486
|
+
sessionKey: opts.session ?? "cli:default"
|
|
487
|
+
});
|
|
488
|
+
printAgentResponse(response);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
channelsStatus() {
|
|
492
|
+
const config = loadConfig();
|
|
493
|
+
console.log("Channel Status");
|
|
494
|
+
console.log(`WhatsApp: ${config.channels.whatsapp.enabled ? "\u2713" : "\u2717"}`);
|
|
495
|
+
console.log(`Discord: ${config.channels.discord.enabled ? "\u2713" : "\u2717"}`);
|
|
496
|
+
console.log(`Feishu: ${config.channels.feishu.enabled ? "\u2713" : "\u2717"}`);
|
|
497
|
+
console.log(`Mochat: ${config.channels.mochat.enabled ? "\u2713" : "\u2717"}`);
|
|
498
|
+
console.log(`Telegram: ${config.channels.telegram.enabled ? "\u2713" : "\u2717"}`);
|
|
499
|
+
console.log(`Slack: ${config.channels.slack.enabled ? "\u2713" : "\u2717"}`);
|
|
500
|
+
console.log(`QQ: ${config.channels.qq.enabled ? "\u2713" : "\u2717"}`);
|
|
501
|
+
}
|
|
502
|
+
channelsLogin() {
|
|
503
|
+
const bridgeDir = this.getBridgeDir();
|
|
504
|
+
console.log(`${this.logo} Starting bridge...`);
|
|
505
|
+
console.log("Scan the QR code to connect.\n");
|
|
506
|
+
const result = spawnSync("npm", ["start"], { cwd: bridgeDir, stdio: "inherit" });
|
|
507
|
+
if (result.status !== 0) {
|
|
508
|
+
console.error(`Bridge failed: ${result.status ?? 1}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
cronList(opts) {
|
|
512
|
+
const storePath = join2(getDataDir2(), "cron", "jobs.json");
|
|
513
|
+
const service = new CronService(storePath);
|
|
514
|
+
const jobs = service.listJobs(Boolean(opts.all));
|
|
515
|
+
if (!jobs.length) {
|
|
516
|
+
console.log("No scheduled jobs.");
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
for (const job of jobs) {
|
|
520
|
+
let schedule = "";
|
|
521
|
+
if (job.schedule.kind === "every") {
|
|
522
|
+
schedule = `every ${Math.round((job.schedule.everyMs ?? 0) / 1e3)}s`;
|
|
523
|
+
} else if (job.schedule.kind === "cron") {
|
|
524
|
+
schedule = job.schedule.expr ?? "";
|
|
525
|
+
} else {
|
|
526
|
+
schedule = job.schedule.atMs ? new Date(job.schedule.atMs).toISOString() : "";
|
|
527
|
+
}
|
|
528
|
+
console.log(`${job.id} ${job.name} ${schedule}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
cronAdd(opts) {
|
|
532
|
+
const storePath = join2(getDataDir2(), "cron", "jobs.json");
|
|
533
|
+
const service = new CronService(storePath);
|
|
534
|
+
let schedule = null;
|
|
535
|
+
if (opts.every) {
|
|
536
|
+
schedule = { kind: "every", everyMs: Number(opts.every) * 1e3 };
|
|
537
|
+
} else if (opts.cron) {
|
|
538
|
+
schedule = { kind: "cron", expr: String(opts.cron) };
|
|
539
|
+
} else if (opts.at) {
|
|
540
|
+
schedule = { kind: "at", atMs: Date.parse(String(opts.at)) };
|
|
541
|
+
}
|
|
542
|
+
if (!schedule) {
|
|
543
|
+
console.error("Error: Must specify --every, --cron, or --at");
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const job = service.addJob({
|
|
547
|
+
name: opts.name,
|
|
548
|
+
schedule,
|
|
549
|
+
message: opts.message,
|
|
550
|
+
deliver: Boolean(opts.deliver),
|
|
551
|
+
channel: opts.channel,
|
|
552
|
+
to: opts.to
|
|
553
|
+
});
|
|
554
|
+
console.log(`\u2713 Added job '${job.name}' (${job.id})`);
|
|
555
|
+
}
|
|
556
|
+
cronRemove(jobId) {
|
|
557
|
+
const storePath = join2(getDataDir2(), "cron", "jobs.json");
|
|
558
|
+
const service = new CronService(storePath);
|
|
559
|
+
if (service.removeJob(jobId)) {
|
|
560
|
+
console.log(`\u2713 Removed job ${jobId}`);
|
|
561
|
+
} else {
|
|
562
|
+
console.log(`Job ${jobId} not found`);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
cronEnable(jobId, opts) {
|
|
566
|
+
const storePath = join2(getDataDir2(), "cron", "jobs.json");
|
|
567
|
+
const service = new CronService(storePath);
|
|
568
|
+
const job = service.enableJob(jobId, !opts.disable);
|
|
569
|
+
if (job) {
|
|
570
|
+
console.log(`\u2713 Job '${job.name}' ${opts.disable ? "disabled" : "enabled"}`);
|
|
571
|
+
} else {
|
|
572
|
+
console.log(`Job ${jobId} not found`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async cronRun(jobId, opts) {
|
|
576
|
+
const storePath = join2(getDataDir2(), "cron", "jobs.json");
|
|
577
|
+
const service = new CronService(storePath);
|
|
578
|
+
const ok = await service.runJob(jobId, Boolean(opts.force));
|
|
579
|
+
console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
|
|
580
|
+
}
|
|
581
|
+
status() {
|
|
582
|
+
const configPath = getConfigPath();
|
|
583
|
+
const config = loadConfig();
|
|
584
|
+
const workspace = getWorkspacePath(config.agents.defaults.workspace);
|
|
585
|
+
console.log(`${this.logo} ${APP_NAME} Status
|
|
586
|
+
`);
|
|
587
|
+
console.log(`Config: ${configPath} ${existsSync2(configPath) ? "\u2713" : "\u2717"}`);
|
|
588
|
+
console.log(`Workspace: ${workspace} ${existsSync2(workspace) ? "\u2713" : "\u2717"}`);
|
|
589
|
+
console.log(`Model: ${config.agents.defaults.model}`);
|
|
590
|
+
for (const spec of PROVIDERS) {
|
|
591
|
+
const provider = config.providers[spec.name];
|
|
592
|
+
if (!provider) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (spec.isLocal) {
|
|
596
|
+
console.log(`${spec.displayName ?? spec.name}: ${provider.apiBase ? `\u2713 ${provider.apiBase}` : "not set"}`);
|
|
597
|
+
} else {
|
|
598
|
+
console.log(`${spec.displayName ?? spec.name}: ${provider.apiKey ? "\u2713" : "not set"}`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async startGateway(options = {}) {
|
|
603
|
+
const config = loadConfig();
|
|
604
|
+
const bus = new MessageBus();
|
|
605
|
+
const provider = options.allowMissingProvider === true ? this.makeProvider(config, { allowMissing: true }) : this.makeProvider(config);
|
|
606
|
+
const providerManager = provider ? new ProviderManager(provider) : null;
|
|
607
|
+
const sessionManager = new SessionManager(getWorkspacePath(config.agents.defaults.workspace));
|
|
608
|
+
const cronStorePath = join2(getDataDir2(), "cron", "jobs.json");
|
|
609
|
+
const cron2 = new CronService(cronStorePath);
|
|
610
|
+
const uiConfig = resolveUiConfig(config, options.uiOverrides);
|
|
611
|
+
const uiStaticDir = options.uiStaticDir === void 0 ? resolveUiStaticDir() : options.uiStaticDir;
|
|
612
|
+
if (!provider) {
|
|
613
|
+
if (uiConfig.enabled) {
|
|
614
|
+
const uiServer = startUiServer({
|
|
615
|
+
host: uiConfig.host,
|
|
616
|
+
port: uiConfig.port,
|
|
617
|
+
configPath: getConfigPath(),
|
|
618
|
+
staticDir: uiStaticDir ?? void 0
|
|
619
|
+
});
|
|
620
|
+
const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
|
|
621
|
+
console.log(`\u2713 UI API: ${uiUrl}/api`);
|
|
622
|
+
if (uiStaticDir) {
|
|
623
|
+
console.log(`\u2713 UI frontend: ${uiUrl}`);
|
|
624
|
+
}
|
|
625
|
+
if (uiConfig.open) {
|
|
626
|
+
openBrowser(uiUrl);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
console.log("Warning: No API key configured. UI server only.");
|
|
630
|
+
await new Promise(() => {
|
|
631
|
+
});
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const agent = new AgentLoop({
|
|
635
|
+
bus,
|
|
636
|
+
providerManager: providerManager ?? new ProviderManager(provider),
|
|
637
|
+
workspace: getWorkspacePath(config.agents.defaults.workspace),
|
|
638
|
+
model: config.agents.defaults.model,
|
|
639
|
+
maxIterations: config.agents.defaults.maxToolIterations,
|
|
640
|
+
braveApiKey: config.tools.web.search.apiKey || void 0,
|
|
641
|
+
execConfig: config.tools.exec,
|
|
642
|
+
cronService: cron2,
|
|
643
|
+
restrictToWorkspace: config.tools.restrictToWorkspace,
|
|
644
|
+
sessionManager
|
|
645
|
+
});
|
|
646
|
+
cron2.onJob = async (job) => {
|
|
647
|
+
const response = await agent.processDirect({
|
|
648
|
+
content: job.payload.message,
|
|
649
|
+
sessionKey: `cron:${job.id}`,
|
|
650
|
+
channel: job.payload.channel ?? "cli",
|
|
651
|
+
chatId: job.payload.to ?? "direct"
|
|
652
|
+
});
|
|
653
|
+
if (job.payload.deliver && job.payload.to) {
|
|
654
|
+
await bus.publishOutbound({
|
|
655
|
+
channel: job.payload.channel ?? "cli",
|
|
656
|
+
chatId: job.payload.to,
|
|
657
|
+
content: response,
|
|
658
|
+
media: [],
|
|
659
|
+
metadata: {}
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
return response;
|
|
663
|
+
};
|
|
664
|
+
const heartbeat = new HeartbeatService(
|
|
665
|
+
getWorkspacePath(config.agents.defaults.workspace),
|
|
666
|
+
async (promptText) => agent.processDirect({ content: promptText, sessionKey: "heartbeat" }),
|
|
667
|
+
30 * 60,
|
|
668
|
+
true
|
|
669
|
+
);
|
|
670
|
+
let currentConfig = config;
|
|
671
|
+
let channels2 = new ChannelManager(currentConfig, bus, sessionManager);
|
|
672
|
+
let reloadTask = null;
|
|
673
|
+
const reloadChannels = async (nextConfig) => {
|
|
674
|
+
if (reloadTask) {
|
|
675
|
+
await reloadTask;
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
reloadTask = (async () => {
|
|
679
|
+
await channels2.stopAll();
|
|
680
|
+
channels2 = new ChannelManager(nextConfig, bus, sessionManager);
|
|
681
|
+
await channels2.startAll();
|
|
682
|
+
})();
|
|
683
|
+
try {
|
|
684
|
+
await reloadTask;
|
|
685
|
+
} finally {
|
|
686
|
+
reloadTask = null;
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
let providerReloadTask = null;
|
|
690
|
+
const reloadProvider = async (nextConfig) => {
|
|
691
|
+
if (!providerManager) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (providerReloadTask) {
|
|
695
|
+
await providerReloadTask;
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
providerReloadTask = (async () => {
|
|
699
|
+
const nextProvider = this.makeProvider(nextConfig, { allowMissing: true });
|
|
700
|
+
if (!nextProvider) {
|
|
701
|
+
console.warn("Provider reload skipped: missing API key.");
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
providerManager.set(nextProvider);
|
|
705
|
+
})();
|
|
706
|
+
try {
|
|
707
|
+
await providerReloadTask;
|
|
708
|
+
} finally {
|
|
709
|
+
providerReloadTask = null;
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
const applyReloadPlan = async (nextConfig) => {
|
|
713
|
+
const changedPaths = diffConfigPaths(currentConfig, nextConfig);
|
|
714
|
+
if (!changedPaths.length) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
currentConfig = nextConfig;
|
|
718
|
+
const plan = buildReloadPlan(changedPaths);
|
|
719
|
+
if (plan.restartChannels) {
|
|
720
|
+
await reloadChannels(nextConfig);
|
|
721
|
+
}
|
|
722
|
+
if (plan.reloadProviders) {
|
|
723
|
+
await reloadProvider(nextConfig);
|
|
724
|
+
}
|
|
725
|
+
if (plan.restartRequired.length > 0) {
|
|
726
|
+
console.warn(`Config changes require restart: ${plan.restartRequired.join(", ")}`);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
let reloadTimer = null;
|
|
730
|
+
let reloadRunning = false;
|
|
731
|
+
let reloadPending = false;
|
|
732
|
+
const scheduleConfigReload = (reason) => {
|
|
733
|
+
if (reloadTimer) {
|
|
734
|
+
clearTimeout(reloadTimer);
|
|
735
|
+
}
|
|
736
|
+
reloadTimer = setTimeout(() => {
|
|
737
|
+
void runConfigReload(reason);
|
|
738
|
+
}, 300);
|
|
739
|
+
};
|
|
740
|
+
const runConfigReload = async (reason) => {
|
|
741
|
+
if (reloadRunning) {
|
|
742
|
+
reloadPending = true;
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
reloadRunning = true;
|
|
746
|
+
if (reloadTimer) {
|
|
747
|
+
clearTimeout(reloadTimer);
|
|
748
|
+
reloadTimer = null;
|
|
749
|
+
}
|
|
750
|
+
try {
|
|
751
|
+
const nextConfig = loadConfig();
|
|
752
|
+
await applyReloadPlan(nextConfig);
|
|
753
|
+
} catch (error) {
|
|
754
|
+
console.error(`Config reload failed (${reason}): ${String(error)}`);
|
|
755
|
+
} finally {
|
|
756
|
+
reloadRunning = false;
|
|
757
|
+
if (reloadPending) {
|
|
758
|
+
reloadPending = false;
|
|
759
|
+
scheduleConfigReload("pending");
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
};
|
|
763
|
+
if (channels2.enabledChannels.length) {
|
|
764
|
+
console.log(`\u2713 Channels enabled: ${channels2.enabledChannels.join(", ")}`);
|
|
765
|
+
} else {
|
|
766
|
+
console.log("Warning: No channels enabled");
|
|
767
|
+
}
|
|
768
|
+
if (uiConfig.enabled) {
|
|
769
|
+
const uiServer = startUiServer({
|
|
770
|
+
host: uiConfig.host,
|
|
771
|
+
port: uiConfig.port,
|
|
772
|
+
configPath: getConfigPath(),
|
|
773
|
+
staticDir: uiStaticDir ?? void 0
|
|
774
|
+
});
|
|
775
|
+
const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
|
|
776
|
+
console.log(`\u2713 UI API: ${uiUrl}/api`);
|
|
777
|
+
if (uiStaticDir) {
|
|
778
|
+
console.log(`\u2713 UI frontend: ${uiUrl}`);
|
|
779
|
+
}
|
|
780
|
+
if (uiConfig.open) {
|
|
781
|
+
openBrowser(uiUrl);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const cronStatus = cron2.status();
|
|
785
|
+
if (cronStatus.jobs > 0) {
|
|
786
|
+
console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
|
|
787
|
+
}
|
|
788
|
+
console.log("\u2713 Heartbeat: every 30m");
|
|
789
|
+
const configPath = getConfigPath();
|
|
790
|
+
const watcher = chokidar.watch(configPath, {
|
|
791
|
+
ignoreInitial: true,
|
|
792
|
+
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
|
|
793
|
+
});
|
|
794
|
+
watcher.on("add", () => scheduleConfigReload("config add"));
|
|
795
|
+
watcher.on("change", () => scheduleConfigReload("config change"));
|
|
796
|
+
watcher.on("unlink", () => scheduleConfigReload("config unlink"));
|
|
797
|
+
await cron2.start();
|
|
798
|
+
await heartbeat.start();
|
|
799
|
+
await Promise.allSettled([agent.run(), channels2.startAll()]);
|
|
800
|
+
}
|
|
801
|
+
async runForeground(options) {
|
|
802
|
+
const config = loadConfig();
|
|
803
|
+
const uiConfig = resolveUiConfig(config, options.uiOverrides);
|
|
804
|
+
const shouldStartFrontend = options.frontend;
|
|
805
|
+
const frontendPort = Number.isFinite(options.frontendPort) ? options.frontendPort : 5173;
|
|
806
|
+
const frontendDir = shouldStartFrontend ? resolveUiFrontendDir() : null;
|
|
807
|
+
const staticDir = resolveUiStaticDir();
|
|
808
|
+
let frontendUrl = null;
|
|
809
|
+
if (shouldStartFrontend && frontendDir) {
|
|
810
|
+
const frontend = startUiFrontend({
|
|
811
|
+
apiBase: resolveUiApiBase(uiConfig.host, uiConfig.port),
|
|
812
|
+
port: frontendPort,
|
|
813
|
+
dir: frontendDir
|
|
814
|
+
});
|
|
815
|
+
frontendUrl = frontend?.url ?? null;
|
|
816
|
+
} else if (shouldStartFrontend && !frontendDir) {
|
|
817
|
+
console.log("Warning: UI frontend not found. Start it separately.");
|
|
818
|
+
}
|
|
819
|
+
if (!frontendUrl && staticDir) {
|
|
820
|
+
frontendUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
|
|
821
|
+
}
|
|
822
|
+
if (options.open && frontendUrl) {
|
|
823
|
+
openBrowser(frontendUrl);
|
|
824
|
+
} else if (options.open && !frontendUrl) {
|
|
825
|
+
console.log("Warning: UI frontend not started. Browser not opened.");
|
|
826
|
+
}
|
|
827
|
+
const uiStaticDir = shouldStartFrontend && frontendDir ? null : staticDir;
|
|
828
|
+
await this.startGateway({
|
|
829
|
+
uiOverrides: options.uiOverrides,
|
|
830
|
+
allowMissingProvider: true,
|
|
831
|
+
uiStaticDir
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
async startService(options) {
|
|
835
|
+
const config = loadConfig();
|
|
836
|
+
const uiConfig = resolveUiConfig(config, options.uiOverrides);
|
|
837
|
+
const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
|
|
838
|
+
const apiUrl = `${uiUrl}/api`;
|
|
839
|
+
const staticDir = resolveUiStaticDir();
|
|
840
|
+
const existing = readServiceState();
|
|
841
|
+
if (existing && isProcessRunning(existing.pid)) {
|
|
842
|
+
console.log(`\u2713 ${APP_NAME} is already running (PID ${existing.pid})`);
|
|
843
|
+
console.log(`UI: ${existing.uiUrl}`);
|
|
844
|
+
console.log(`API: ${existing.apiUrl}`);
|
|
845
|
+
console.log(`Logs: ${existing.logPath}`);
|
|
846
|
+
console.log(`Stop: ${APP_NAME} stop`);
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (existing) {
|
|
850
|
+
clearServiceState();
|
|
851
|
+
}
|
|
852
|
+
if (!staticDir && !options.frontend) {
|
|
853
|
+
console.log("Warning: UI frontend not found. Use --frontend to start the dev server.");
|
|
854
|
+
}
|
|
855
|
+
const logPath = resolveServiceLogPath();
|
|
856
|
+
const logDir = resolve2(logPath, "..");
|
|
857
|
+
mkdirSync2(logDir, { recursive: true });
|
|
858
|
+
const logFd = openSync(logPath, "a");
|
|
859
|
+
const serveArgs = buildServeArgs({
|
|
860
|
+
uiHost: uiConfig.host,
|
|
861
|
+
uiPort: uiConfig.port,
|
|
862
|
+
frontend: options.frontend,
|
|
863
|
+
frontendPort: options.frontendPort
|
|
864
|
+
});
|
|
865
|
+
const child = spawn2(process.execPath, [...process.execArgv, ...serveArgs], {
|
|
866
|
+
env: process.env,
|
|
867
|
+
stdio: ["ignore", logFd, logFd],
|
|
868
|
+
detached: true
|
|
869
|
+
});
|
|
870
|
+
closeSync(logFd);
|
|
871
|
+
if (!child.pid) {
|
|
872
|
+
console.error("Error: Failed to start background service.");
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
child.unref();
|
|
876
|
+
const state = {
|
|
877
|
+
pid: child.pid,
|
|
878
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
879
|
+
uiUrl,
|
|
880
|
+
apiUrl,
|
|
881
|
+
logPath
|
|
882
|
+
};
|
|
883
|
+
writeServiceState(state);
|
|
884
|
+
console.log(`\u2713 ${APP_NAME} started in background (PID ${state.pid})`);
|
|
885
|
+
console.log(`UI: ${uiUrl}`);
|
|
886
|
+
console.log(`API: ${apiUrl}`);
|
|
887
|
+
console.log(`Logs: ${logPath}`);
|
|
888
|
+
console.log(`Stop: ${APP_NAME} stop`);
|
|
889
|
+
if (options.open) {
|
|
890
|
+
openBrowser(uiUrl);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
async stopService() {
|
|
894
|
+
const state = readServiceState();
|
|
895
|
+
if (!state) {
|
|
896
|
+
console.log("No running service found.");
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
if (!isProcessRunning(state.pid)) {
|
|
900
|
+
console.log("Service is not running. Cleaning up state.");
|
|
901
|
+
clearServiceState();
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
console.log(`Stopping ${APP_NAME} (PID ${state.pid})...`);
|
|
905
|
+
try {
|
|
906
|
+
process.kill(state.pid, "SIGTERM");
|
|
907
|
+
} catch (error) {
|
|
908
|
+
console.error(`Failed to stop service: ${String(error)}`);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const stopped = await waitForExit(state.pid, 3e3);
|
|
912
|
+
if (!stopped) {
|
|
913
|
+
try {
|
|
914
|
+
process.kill(state.pid, "SIGKILL");
|
|
915
|
+
} catch (error) {
|
|
916
|
+
console.error(`Failed to force stop service: ${String(error)}`);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
await waitForExit(state.pid, 2e3);
|
|
920
|
+
}
|
|
921
|
+
clearServiceState();
|
|
922
|
+
console.log(`\u2713 ${APP_NAME} stopped`);
|
|
923
|
+
}
|
|
924
|
+
makeProvider(config, options) {
|
|
925
|
+
const provider = getProvider(config);
|
|
926
|
+
const model = config.agents.defaults.model;
|
|
927
|
+
if (!provider?.apiKey && !model.startsWith("bedrock/")) {
|
|
928
|
+
if (options?.allowMissing) {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
console.error("Error: No API key configured.");
|
|
932
|
+
console.error(`Set one in ${getConfigPath()} under providers section`);
|
|
933
|
+
process.exit(1);
|
|
934
|
+
}
|
|
935
|
+
return new LiteLLMProvider({
|
|
936
|
+
apiKey: provider?.apiKey ?? null,
|
|
937
|
+
apiBase: getApiBase(config),
|
|
938
|
+
defaultModel: model,
|
|
939
|
+
extraHeaders: provider?.extraHeaders ?? null,
|
|
940
|
+
providerName: getProviderName(config),
|
|
941
|
+
wireApi: provider?.wireApi ?? null
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
createWorkspaceTemplates(workspace) {
|
|
945
|
+
const templates = {
|
|
946
|
+
"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",
|
|
947
|
+
"SOUL.md": `# Soul
|
|
948
|
+
|
|
949
|
+
I am ${APP_NAME}, a lightweight AI assistant.
|
|
950
|
+
|
|
951
|
+
## Personality
|
|
952
|
+
|
|
953
|
+
- Helpful and friendly
|
|
954
|
+
- Concise and to the point
|
|
955
|
+
- Curious and eager to learn
|
|
956
|
+
|
|
957
|
+
## Values
|
|
958
|
+
|
|
959
|
+
- Accuracy over speed
|
|
960
|
+
- User privacy and safety
|
|
961
|
+
- Transparency in actions
|
|
962
|
+
|
|
963
|
+
`,
|
|
964
|
+
"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"
|
|
965
|
+
};
|
|
966
|
+
for (const [filename, content] of Object.entries(templates)) {
|
|
967
|
+
const filePath = join2(workspace, filename);
|
|
968
|
+
if (!existsSync2(filePath)) {
|
|
969
|
+
writeFileSync2(filePath, content);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const memoryDir = join2(workspace, "memory");
|
|
973
|
+
mkdirSync2(memoryDir, { recursive: true });
|
|
974
|
+
const memoryFile = join2(memoryDir, "MEMORY.md");
|
|
975
|
+
if (!existsSync2(memoryFile)) {
|
|
976
|
+
writeFileSync2(
|
|
977
|
+
memoryFile,
|
|
978
|
+
"# 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"
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
const skillsDir = join2(workspace, "skills");
|
|
982
|
+
mkdirSync2(skillsDir, { recursive: true });
|
|
983
|
+
}
|
|
984
|
+
getBridgeDir() {
|
|
985
|
+
const userBridge = join2(getDataDir2(), "bridge");
|
|
986
|
+
if (existsSync2(join2(userBridge, "dist", "index.js"))) {
|
|
987
|
+
return userBridge;
|
|
988
|
+
}
|
|
989
|
+
if (!which("npm")) {
|
|
990
|
+
console.error("npm not found. Please install Node.js >= 18.");
|
|
991
|
+
process.exit(1);
|
|
992
|
+
}
|
|
993
|
+
const cliDir = resolve2(fileURLToPath2(new URL(".", import.meta.url)));
|
|
994
|
+
const pkgRoot = resolve2(cliDir, "..", "..");
|
|
995
|
+
const pkgBridge = join2(pkgRoot, "bridge");
|
|
996
|
+
const srcBridge = join2(pkgRoot, "..", "..", "bridge");
|
|
997
|
+
let source = null;
|
|
998
|
+
if (existsSync2(join2(pkgBridge, "package.json"))) {
|
|
999
|
+
source = pkgBridge;
|
|
1000
|
+
} else if (existsSync2(join2(srcBridge, "package.json"))) {
|
|
1001
|
+
source = srcBridge;
|
|
1002
|
+
}
|
|
1003
|
+
if (!source) {
|
|
1004
|
+
console.error(`Bridge source not found. Try reinstalling ${APP_NAME}.`);
|
|
1005
|
+
process.exit(1);
|
|
1006
|
+
}
|
|
1007
|
+
console.log(`${this.logo} Setting up bridge...`);
|
|
1008
|
+
mkdirSync2(resolve2(userBridge, ".."), { recursive: true });
|
|
1009
|
+
if (existsSync2(userBridge)) {
|
|
1010
|
+
rmSync2(userBridge, { recursive: true, force: true });
|
|
1011
|
+
}
|
|
1012
|
+
cpSync(source, userBridge, {
|
|
1013
|
+
recursive: true,
|
|
1014
|
+
filter: (src) => !src.includes("node_modules") && !src.includes("dist")
|
|
1015
|
+
});
|
|
1016
|
+
const install = spawnSync("npm", ["install"], { cwd: userBridge, stdio: "pipe" });
|
|
1017
|
+
if (install.status !== 0) {
|
|
1018
|
+
console.error(`Bridge install failed: ${install.status ?? 1}`);
|
|
1019
|
+
if (install.stderr) {
|
|
1020
|
+
console.error(String(install.stderr).slice(0, 500));
|
|
1021
|
+
}
|
|
1022
|
+
process.exit(1);
|
|
1023
|
+
}
|
|
1024
|
+
const build = spawnSync("npm", ["run", "build"], { cwd: userBridge, stdio: "pipe" });
|
|
1025
|
+
if (build.status !== 0) {
|
|
1026
|
+
console.error(`Bridge build failed: ${build.status ?? 1}`);
|
|
1027
|
+
if (build.stderr) {
|
|
1028
|
+
console.error(String(build.stderr).slice(0, 500));
|
|
1029
|
+
}
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
}
|
|
1032
|
+
console.log("\u2713 Bridge ready\n");
|
|
1033
|
+
return userBridge;
|
|
1034
|
+
}
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
// src/cli/index.ts
|
|
1038
|
+
var program = new Command();
|
|
1039
|
+
var runtime = new CliRuntime({ logo: LOGO });
|
|
1040
|
+
program.name(APP_NAME2).description(`${LOGO} ${APP_NAME2} - ${APP_TAGLINE}`).version(getPackageVersion(), "-v, --version", "show version");
|
|
1041
|
+
program.command("onboard").description(`Initialize ${APP_NAME2} configuration and workspace`).action(async () => runtime.onboard());
|
|
1042
|
+
program.command("gateway").description(`Start the ${APP_NAME2} 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) => runtime.gateway(opts));
|
|
1043
|
+
program.command("ui").description(`Start the ${APP_NAME2} UI with gateway`).option("--host <host>", "UI host").option("--port <port>", "UI port").option("--no-open", "Disable opening browser").action(async (opts) => runtime.ui(opts));
|
|
1044
|
+
program.command("start").description(`Start the ${APP_NAME2} gateway + UI in the background`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--frontend", "Start UI frontend dev server").option("--frontend-port <port>", "UI frontend dev server port").option("--open", "Open browser after start", false).action(async (opts) => runtime.start(opts));
|
|
1045
|
+
program.command("serve").description(`Run the ${APP_NAME2} gateway + UI in the foreground`).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--frontend", "Start UI frontend dev server").option("--frontend-port <port>", "UI frontend dev server port").option("--open", "Open browser after start", false).action(async (opts) => runtime.serve(opts));
|
|
1046
|
+
program.command("stop").description(`Stop the ${APP_NAME2} background service`).action(async () => runtime.stop());
|
|
1047
|
+
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) => runtime.agent(opts));
|
|
1048
|
+
var channels = program.command("channels").description("Manage channels");
|
|
1049
|
+
channels.command("status").description("Show channel status").action(() => runtime.channelsStatus());
|
|
1050
|
+
channels.command("login").description("Link device via QR code").action(() => runtime.channelsLogin());
|
|
1051
|
+
var cron = program.command("cron").description("Manage scheduled tasks");
|
|
1052
|
+
cron.command("list").option("-a, --all", "Include disabled jobs").action((opts) => runtime.cronList(opts));
|
|
1053
|
+
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) => runtime.cronAdd(opts));
|
|
1054
|
+
cron.command("remove <jobId>").action((jobId) => runtime.cronRemove(jobId));
|
|
1055
|
+
cron.command("enable <jobId>").option("--disable", "Disable instead of enable").action((jobId, opts) => runtime.cronEnable(jobId, opts));
|
|
1056
|
+
cron.command("run <jobId>").option("-f, --force", "Run even if disabled").action(async (jobId, opts) => runtime.cronRun(jobId, opts));
|
|
1057
|
+
program.command("status").description(`Show ${APP_NAME2} status`).action(() => runtime.status());
|
|
1058
|
+
program.parseAsync(process.argv);
|