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