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
- existsSync,
7
- mkdirSync,
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
- var LOGO = "\u{1F916}";
46
- var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "/exit", "/quit", ":q"]);
47
- var VERSION = getPackageVersion();
48
- var program = new Command();
49
- program.name(APP_NAME).description(`${LOGO} ${APP_NAME} - ${APP_TAGLINE}`).version(VERSION, "-v, --version", "show version");
50
- program.command("onboard").description(`Initialize ${APP_NAME} configuration and workspace`).action(() => {
51
- const configPath = getConfigPath();
52
- if (existsSync(configPath)) {
53
- console.log(`Config already exists at ${configPath}`);
54
- }
55
- const config = ConfigSchema.parse({});
56
- saveConfig(config);
57
- console.log(`\u2713 Created config at ${configPath}`);
58
- const workspace = getWorkspacePath();
59
- console.log(`\u2713 Created workspace at ${workspace}`);
60
- createWorkspaceTemplates(workspace);
61
- console.log(`
62
- ${LOGO} ${APP_NAME} is ready!`);
63
- console.log("\nNext steps:");
64
- console.log(` 1. Add your API key to ${configPath}`);
65
- console.log(` 2. Chat: ${APP_NAME} agent -m "Hello!"`);
66
- });
67
- program.command("gateway").description(`Start the ${APP_NAME} gateway`).option("-p, --port <port>", "Gateway port", "18790").option("-v, --verbose", "Verbose output", false).option("--ui", "Enable UI server", false).option("--ui-host <host>", "UI host").option("--ui-port <port>", "UI port").option("--ui-open", "Open browser when UI starts", false).action(async (opts) => {
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 hostToCheck of hostsToCheck) {
550
- const ok = await canBindPort(port, hostToCheck);
551
- if (!ok) {
552
- return false;
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 true;
95
+ return false;
556
96
  }
557
97
  async function canBindPort(port, host) {
558
- return await new Promise((resolve2) => {
98
+ return await new Promise((resolve3) => {
559
99
  const server = createServer();
560
100
  server.unref();
561
- server.once("error", () => resolve2(false));
101
+ server.once("error", () => resolve3(false));
562
102
  server.listen({ port, host }, () => {
563
- server.close(() => resolve2(true));
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((resolve2) => setTimeout(resolve2, 200));
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);