nextclaw 0.4.0 → 0.4.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 +493 -454
- package/package.json +3 -3
package/dist/cli/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
saveConfig,
|
|
11
11
|
getConfigPath,
|
|
12
12
|
getDataDir as getDataDir2,
|
|
13
|
-
ConfigSchema,
|
|
13
|
+
ConfigSchema as ConfigSchema2,
|
|
14
14
|
getApiBase,
|
|
15
15
|
getProvider,
|
|
16
16
|
getProviderName,
|
|
@@ -35,20 +35,29 @@ import { startUiServer } from "nextclaw-server";
|
|
|
35
35
|
import {
|
|
36
36
|
closeSync,
|
|
37
37
|
cpSync,
|
|
38
|
-
existsSync as
|
|
38
|
+
existsSync as existsSync3,
|
|
39
39
|
mkdirSync as mkdirSync2,
|
|
40
40
|
openSync,
|
|
41
|
-
readFileSync as
|
|
41
|
+
readFileSync as readFileSync3,
|
|
42
42
|
rmSync as rmSync2,
|
|
43
43
|
writeFileSync as writeFileSync2
|
|
44
44
|
} from "fs";
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import { spawn as spawn2, spawnSync } from "child_process";
|
|
45
|
+
import { dirname, join as join3, resolve as resolve3 } from "path";
|
|
46
|
+
import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
|
|
48
47
|
import { createInterface } from "readline";
|
|
49
|
-
import { fileURLToPath as
|
|
48
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
50
49
|
import chokidar from "chokidar";
|
|
51
50
|
|
|
51
|
+
// src/cli/gateway/controller.ts
|
|
52
|
+
import { createHash } from "crypto";
|
|
53
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
54
|
+
import { spawnSync } from "child_process";
|
|
55
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
56
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
57
|
+
import {
|
|
58
|
+
ConfigSchema
|
|
59
|
+
} from "nextclaw-core";
|
|
60
|
+
|
|
52
61
|
// src/cli/utils.ts
|
|
53
62
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "fs";
|
|
54
63
|
import { join, resolve } from "path";
|
|
@@ -87,12 +96,12 @@ async function isPortAvailable(port, host) {
|
|
|
87
96
|
return await canBindPort(port, checkHost);
|
|
88
97
|
}
|
|
89
98
|
async function canBindPort(port, host) {
|
|
90
|
-
return await new Promise((
|
|
99
|
+
return await new Promise((resolve4) => {
|
|
91
100
|
const server = createServer();
|
|
92
101
|
server.unref();
|
|
93
|
-
server.once("error", () =>
|
|
102
|
+
server.once("error", () => resolve4(false));
|
|
94
103
|
server.listen({ port, host }, () => {
|
|
95
|
-
server.close(() =>
|
|
104
|
+
server.close(() => resolve4(true));
|
|
96
105
|
});
|
|
97
106
|
});
|
|
98
107
|
}
|
|
@@ -150,7 +159,7 @@ async function waitForExit(pid, timeoutMs) {
|
|
|
150
159
|
if (!isProcessRunning(pid)) {
|
|
151
160
|
return true;
|
|
152
161
|
}
|
|
153
|
-
await new Promise((
|
|
162
|
+
await new Promise((resolve4) => setTimeout(resolve4, 200));
|
|
154
163
|
}
|
|
155
164
|
return !isProcessRunning(pid);
|
|
156
165
|
}
|
|
@@ -281,14 +290,395 @@ function printAgentResponse(response) {
|
|
|
281
290
|
async function prompt(rl, question) {
|
|
282
291
|
rl.setPrompt(question);
|
|
283
292
|
rl.prompt();
|
|
284
|
-
return new Promise((
|
|
285
|
-
rl.once("line", (line) =>
|
|
293
|
+
return new Promise((resolve4) => {
|
|
294
|
+
rl.once("line", (line) => resolve4(line));
|
|
286
295
|
});
|
|
287
296
|
}
|
|
288
297
|
|
|
298
|
+
// src/cli/gateway/controller.ts
|
|
299
|
+
var hashRaw = (raw) => createHash("sha256").update(raw).digest("hex");
|
|
300
|
+
var redactConfig = (value) => {
|
|
301
|
+
if (Array.isArray(value)) {
|
|
302
|
+
return value.map((entry) => redactConfig(entry));
|
|
303
|
+
}
|
|
304
|
+
if (!value || typeof value !== "object") {
|
|
305
|
+
return value;
|
|
306
|
+
}
|
|
307
|
+
const entries = value;
|
|
308
|
+
const output = {};
|
|
309
|
+
for (const [key, val] of Object.entries(entries)) {
|
|
310
|
+
if (/apiKey|token|secret|password|appId|clientSecret|accessKey/i.test(key)) {
|
|
311
|
+
output[key] = val ? "***" : val;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
output[key] = redactConfig(val);
|
|
315
|
+
}
|
|
316
|
+
return output;
|
|
317
|
+
};
|
|
318
|
+
var readConfigSnapshot = (getConfigPath2) => {
|
|
319
|
+
const path = getConfigPath2();
|
|
320
|
+
let raw = "";
|
|
321
|
+
let parsed = {};
|
|
322
|
+
if (existsSync2(path)) {
|
|
323
|
+
raw = readFileSync2(path, "utf-8");
|
|
324
|
+
try {
|
|
325
|
+
parsed = JSON.parse(raw);
|
|
326
|
+
} catch {
|
|
327
|
+
parsed = {};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
let config;
|
|
331
|
+
let valid = true;
|
|
332
|
+
try {
|
|
333
|
+
config = ConfigSchema.parse(parsed);
|
|
334
|
+
} catch {
|
|
335
|
+
config = ConfigSchema.parse({});
|
|
336
|
+
valid = false;
|
|
337
|
+
}
|
|
338
|
+
if (!raw) {
|
|
339
|
+
raw = JSON.stringify(config, null, 2);
|
|
340
|
+
}
|
|
341
|
+
const hash = hashRaw(raw);
|
|
342
|
+
const redacted = redactConfig(config);
|
|
343
|
+
return { raw: valid ? JSON.stringify(redacted, null, 2) : null, hash: valid ? hash : null, config, redacted, valid };
|
|
344
|
+
};
|
|
345
|
+
var mergeDeep = (base, patch) => {
|
|
346
|
+
const next = { ...base };
|
|
347
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
348
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
349
|
+
const baseVal = base[key];
|
|
350
|
+
if (baseVal && typeof baseVal === "object" && !Array.isArray(baseVal)) {
|
|
351
|
+
next[key] = mergeDeep(baseVal, value);
|
|
352
|
+
} else {
|
|
353
|
+
next[key] = mergeDeep({}, value);
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
next[key] = value;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return next;
|
|
360
|
+
};
|
|
361
|
+
var buildSchemaFromValue = (value) => {
|
|
362
|
+
if (Array.isArray(value)) {
|
|
363
|
+
const item = value.length ? buildSchemaFromValue(value[0]) : { type: "string" };
|
|
364
|
+
return { type: "array", items: item };
|
|
365
|
+
}
|
|
366
|
+
if (value && typeof value === "object") {
|
|
367
|
+
const props = {};
|
|
368
|
+
for (const [key, val] of Object.entries(value)) {
|
|
369
|
+
props[key] = buildSchemaFromValue(val);
|
|
370
|
+
}
|
|
371
|
+
return { type: "object", properties: props };
|
|
372
|
+
}
|
|
373
|
+
if (typeof value === "number") {
|
|
374
|
+
return { type: "number" };
|
|
375
|
+
}
|
|
376
|
+
if (typeof value === "boolean") {
|
|
377
|
+
return { type: "boolean" };
|
|
378
|
+
}
|
|
379
|
+
if (value === null) {
|
|
380
|
+
return { type: ["null", "string"] };
|
|
381
|
+
}
|
|
382
|
+
return { type: "string" };
|
|
383
|
+
};
|
|
384
|
+
var scheduleRestart = (delayMs, reason) => {
|
|
385
|
+
const delay = typeof delayMs === "number" && Number.isFinite(delayMs) ? Math.max(0, delayMs) : 100;
|
|
386
|
+
console.log(`Gateway restart requested via tool${reason ? ` (${reason})` : ""}.`);
|
|
387
|
+
setTimeout(() => {
|
|
388
|
+
process.exit(0);
|
|
389
|
+
}, delay);
|
|
390
|
+
};
|
|
391
|
+
var GatewayControllerImpl = class {
|
|
392
|
+
constructor(deps) {
|
|
393
|
+
this.deps = deps;
|
|
394
|
+
}
|
|
395
|
+
status() {
|
|
396
|
+
return {
|
|
397
|
+
channels: this.deps.reloader.getChannels().enabledChannels,
|
|
398
|
+
cron: this.deps.cron.status(),
|
|
399
|
+
configPath: this.deps.getConfigPath()
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
async reloadConfig(reason) {
|
|
403
|
+
return this.deps.reloader.reloadConfig(reason);
|
|
404
|
+
}
|
|
405
|
+
async restart(options) {
|
|
406
|
+
scheduleRestart(options?.delayMs, options?.reason);
|
|
407
|
+
return "Restart scheduled";
|
|
408
|
+
}
|
|
409
|
+
async getConfig() {
|
|
410
|
+
const snapshot = readConfigSnapshot(this.deps.getConfigPath);
|
|
411
|
+
return {
|
|
412
|
+
raw: snapshot.raw,
|
|
413
|
+
hash: snapshot.hash,
|
|
414
|
+
path: this.deps.getConfigPath(),
|
|
415
|
+
config: snapshot.redacted,
|
|
416
|
+
parsed: snapshot.redacted,
|
|
417
|
+
resolved: snapshot.redacted,
|
|
418
|
+
valid: snapshot.valid
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
async getConfigSchema() {
|
|
422
|
+
const base = ConfigSchema.parse({});
|
|
423
|
+
return {
|
|
424
|
+
schema: {
|
|
425
|
+
...buildSchemaFromValue(base),
|
|
426
|
+
title: "NextClawConfig",
|
|
427
|
+
description: "NextClaw config schema (simplified)"
|
|
428
|
+
},
|
|
429
|
+
uiHints: {},
|
|
430
|
+
version: getPackageVersion(),
|
|
431
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
async applyConfig(params) {
|
|
435
|
+
const snapshot = readConfigSnapshot(this.deps.getConfigPath);
|
|
436
|
+
if (!params.baseHash) {
|
|
437
|
+
return { ok: false, error: "config base hash required; re-run config.get and retry" };
|
|
438
|
+
}
|
|
439
|
+
if (!snapshot.valid || !snapshot.hash) {
|
|
440
|
+
return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
|
|
441
|
+
}
|
|
442
|
+
if (params.baseHash !== snapshot.hash) {
|
|
443
|
+
return { ok: false, error: "config changed since last load; re-run config.get and retry" };
|
|
444
|
+
}
|
|
445
|
+
let parsedRaw;
|
|
446
|
+
try {
|
|
447
|
+
parsedRaw = JSON.parse(params.raw);
|
|
448
|
+
} catch {
|
|
449
|
+
return { ok: false, error: "invalid JSON in raw config" };
|
|
450
|
+
}
|
|
451
|
+
let validated;
|
|
452
|
+
try {
|
|
453
|
+
validated = ConfigSchema.parse(parsedRaw);
|
|
454
|
+
} catch (err) {
|
|
455
|
+
return { ok: false, error: `invalid config: ${String(err)}` };
|
|
456
|
+
}
|
|
457
|
+
this.deps.saveConfig(validated);
|
|
458
|
+
const delayMs = params.restartDelayMs ?? 0;
|
|
459
|
+
scheduleRestart(delayMs, "config.apply");
|
|
460
|
+
return {
|
|
461
|
+
ok: true,
|
|
462
|
+
note: params.note ?? null,
|
|
463
|
+
path: this.deps.getConfigPath(),
|
|
464
|
+
config: redactConfig(validated),
|
|
465
|
+
restart: { scheduled: true, delayMs }
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
async patchConfig(params) {
|
|
469
|
+
const snapshot = readConfigSnapshot(this.deps.getConfigPath);
|
|
470
|
+
if (!params.baseHash) {
|
|
471
|
+
return { ok: false, error: "config base hash required; re-run config.get and retry" };
|
|
472
|
+
}
|
|
473
|
+
if (!snapshot.valid || !snapshot.hash) {
|
|
474
|
+
return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
|
|
475
|
+
}
|
|
476
|
+
if (params.baseHash !== snapshot.hash) {
|
|
477
|
+
return { ok: false, error: "config changed since last load; re-run config.get and retry" };
|
|
478
|
+
}
|
|
479
|
+
let patch;
|
|
480
|
+
try {
|
|
481
|
+
patch = JSON.parse(params.raw);
|
|
482
|
+
} catch {
|
|
483
|
+
return { ok: false, error: "invalid JSON in raw config" };
|
|
484
|
+
}
|
|
485
|
+
const merged = mergeDeep(snapshot.config, patch);
|
|
486
|
+
let validated;
|
|
487
|
+
try {
|
|
488
|
+
validated = ConfigSchema.parse(merged);
|
|
489
|
+
} catch (err) {
|
|
490
|
+
return { ok: false, error: `invalid config: ${String(err)}` };
|
|
491
|
+
}
|
|
492
|
+
this.deps.saveConfig(validated);
|
|
493
|
+
const delayMs = params.restartDelayMs ?? 0;
|
|
494
|
+
scheduleRestart(delayMs, "config.patch");
|
|
495
|
+
return {
|
|
496
|
+
ok: true,
|
|
497
|
+
note: params.note ?? null,
|
|
498
|
+
path: this.deps.getConfigPath(),
|
|
499
|
+
config: redactConfig(validated),
|
|
500
|
+
restart: { scheduled: true, delayMs }
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
async updateRun(params) {
|
|
504
|
+
const timeoutMs = params.timeoutMs ?? 20 * 6e4;
|
|
505
|
+
const gatewayDir = resolve2(fileURLToPath2(new URL(".", import.meta.url)));
|
|
506
|
+
const cliDir = resolve2(gatewayDir, "..");
|
|
507
|
+
const pkgRoot = resolve2(cliDir, "..", "..");
|
|
508
|
+
const repoRoot = existsSync2(join2(pkgRoot, ".git")) ? pkgRoot : resolve2(pkgRoot, "..", "..");
|
|
509
|
+
const steps = [];
|
|
510
|
+
const runStep = (cmd, args, cwd) => {
|
|
511
|
+
const result = spawnSync(cmd, args, {
|
|
512
|
+
cwd,
|
|
513
|
+
encoding: "utf-8",
|
|
514
|
+
timeout: timeoutMs,
|
|
515
|
+
stdio: "pipe"
|
|
516
|
+
});
|
|
517
|
+
const step = {
|
|
518
|
+
cmd,
|
|
519
|
+
args,
|
|
520
|
+
cwd,
|
|
521
|
+
code: result.status,
|
|
522
|
+
stdout: (result.stdout ?? "").toString().slice(0, 4e3),
|
|
523
|
+
stderr: (result.stderr ?? "").toString().slice(0, 4e3)
|
|
524
|
+
};
|
|
525
|
+
steps.push(step);
|
|
526
|
+
return { ok: result.status === 0, code: result.status };
|
|
527
|
+
};
|
|
528
|
+
const updateCommand = process.env.NEXTCLAW_UPDATE_COMMAND?.trim();
|
|
529
|
+
if (updateCommand) {
|
|
530
|
+
const ok = runStep("sh", ["-c", updateCommand], process.cwd());
|
|
531
|
+
if (!ok.ok) {
|
|
532
|
+
return { ok: false, error: "update command failed", steps };
|
|
533
|
+
}
|
|
534
|
+
} else if (existsSync2(join2(repoRoot, ".git"))) {
|
|
535
|
+
if (!which("git")) {
|
|
536
|
+
return { ok: false, error: "git not found for repo update", steps };
|
|
537
|
+
}
|
|
538
|
+
const ok = runStep("git", ["-C", repoRoot, "pull", "--rebase"], repoRoot);
|
|
539
|
+
if (!ok.ok) {
|
|
540
|
+
return { ok: false, error: "git pull failed", steps };
|
|
541
|
+
}
|
|
542
|
+
if (existsSync2(join2(repoRoot, "pnpm-lock.yaml")) && which("pnpm")) {
|
|
543
|
+
const installOk = runStep("pnpm", ["install"], repoRoot);
|
|
544
|
+
if (!installOk.ok) {
|
|
545
|
+
return { ok: false, error: "pnpm install failed", steps };
|
|
546
|
+
}
|
|
547
|
+
} else if (existsSync2(join2(repoRoot, "package.json")) && which("npm")) {
|
|
548
|
+
const installOk = runStep("npm", ["install"], repoRoot);
|
|
549
|
+
if (!installOk.ok) {
|
|
550
|
+
return { ok: false, error: "npm install failed", steps };
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
} else if (which("npm")) {
|
|
554
|
+
const ok = runStep("npm", ["i", "-g", "nextclaw"], process.cwd());
|
|
555
|
+
if (!ok.ok) {
|
|
556
|
+
return { ok: false, error: "npm install -g nextclaw failed", steps };
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
return { ok: false, error: "no update strategy available", steps };
|
|
560
|
+
}
|
|
561
|
+
const delayMs = params.restartDelayMs ?? 0;
|
|
562
|
+
scheduleRestart(delayMs, "update.run");
|
|
563
|
+
return {
|
|
564
|
+
ok: true,
|
|
565
|
+
note: params.note ?? null,
|
|
566
|
+
restart: { scheduled: true, delayMs },
|
|
567
|
+
steps
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
289
572
|
// src/cli/runtime.ts
|
|
290
573
|
var LOGO = "\u{1F916}";
|
|
291
574
|
var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "/exit", "/quit", ":q"]);
|
|
575
|
+
var ConfigReloader = class {
|
|
576
|
+
constructor(options) {
|
|
577
|
+
this.options = options;
|
|
578
|
+
this.currentConfig = options.initialConfig;
|
|
579
|
+
this.channels = options.channels;
|
|
580
|
+
}
|
|
581
|
+
currentConfig;
|
|
582
|
+
channels;
|
|
583
|
+
reloadTask = null;
|
|
584
|
+
providerReloadTask = null;
|
|
585
|
+
reloadTimer = null;
|
|
586
|
+
reloadRunning = false;
|
|
587
|
+
reloadPending = false;
|
|
588
|
+
getChannels() {
|
|
589
|
+
return this.channels;
|
|
590
|
+
}
|
|
591
|
+
async applyReloadPlan(nextConfig) {
|
|
592
|
+
const changedPaths = diffConfigPaths(this.currentConfig, nextConfig);
|
|
593
|
+
if (!changedPaths.length) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
this.currentConfig = nextConfig;
|
|
597
|
+
const plan = buildReloadPlan(changedPaths);
|
|
598
|
+
if (plan.restartChannels) {
|
|
599
|
+
await this.reloadChannels(nextConfig);
|
|
600
|
+
}
|
|
601
|
+
if (plan.reloadProviders) {
|
|
602
|
+
await this.reloadProvider(nextConfig);
|
|
603
|
+
}
|
|
604
|
+
if (plan.restartRequired.length > 0) {
|
|
605
|
+
this.options.onRestartRequired(plan.restartRequired);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
scheduleReload(reason) {
|
|
609
|
+
if (this.reloadTimer) {
|
|
610
|
+
clearTimeout(this.reloadTimer);
|
|
611
|
+
}
|
|
612
|
+
this.reloadTimer = setTimeout(() => {
|
|
613
|
+
void this.runReload(reason);
|
|
614
|
+
}, 300);
|
|
615
|
+
}
|
|
616
|
+
async runReload(reason) {
|
|
617
|
+
if (this.reloadRunning) {
|
|
618
|
+
this.reloadPending = true;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
this.reloadRunning = true;
|
|
622
|
+
if (this.reloadTimer) {
|
|
623
|
+
clearTimeout(this.reloadTimer);
|
|
624
|
+
this.reloadTimer = null;
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
const nextConfig = this.options.loadConfig();
|
|
628
|
+
await this.applyReloadPlan(nextConfig);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error(`Config reload failed (${reason}): ${String(error)}`);
|
|
631
|
+
} finally {
|
|
632
|
+
this.reloadRunning = false;
|
|
633
|
+
if (this.reloadPending) {
|
|
634
|
+
this.reloadPending = false;
|
|
635
|
+
this.scheduleReload("pending");
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async reloadConfig(reason) {
|
|
640
|
+
await this.runReload(reason ?? "gateway tool");
|
|
641
|
+
return "Config reload triggered";
|
|
642
|
+
}
|
|
643
|
+
async reloadChannels(nextConfig) {
|
|
644
|
+
if (this.reloadTask) {
|
|
645
|
+
await this.reloadTask;
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
this.reloadTask = (async () => {
|
|
649
|
+
await this.channels.stopAll();
|
|
650
|
+
this.channels = new ChannelManager(nextConfig, this.options.bus, this.options.sessionManager);
|
|
651
|
+
await this.channels.startAll();
|
|
652
|
+
})();
|
|
653
|
+
try {
|
|
654
|
+
await this.reloadTask;
|
|
655
|
+
} finally {
|
|
656
|
+
this.reloadTask = null;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async reloadProvider(nextConfig) {
|
|
660
|
+
if (!this.options.providerManager) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (this.providerReloadTask) {
|
|
664
|
+
await this.providerReloadTask;
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
this.providerReloadTask = (async () => {
|
|
668
|
+
const nextProvider = this.options.makeProvider(nextConfig);
|
|
669
|
+
if (!nextProvider) {
|
|
670
|
+
console.warn("Provider reload skipped: missing API key.");
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
this.options.providerManager?.set(nextProvider);
|
|
674
|
+
})();
|
|
675
|
+
try {
|
|
676
|
+
await this.providerReloadTask;
|
|
677
|
+
} finally {
|
|
678
|
+
this.providerReloadTask = null;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
};
|
|
292
682
|
var CliRuntime = class {
|
|
293
683
|
logo;
|
|
294
684
|
constructor(options = {}) {
|
|
@@ -307,15 +697,15 @@ var CliRuntime = class {
|
|
|
307
697
|
const force = Boolean(options.force);
|
|
308
698
|
const configPath = getConfigPath();
|
|
309
699
|
let createdConfig = false;
|
|
310
|
-
if (!
|
|
311
|
-
const config2 =
|
|
700
|
+
if (!existsSync3(configPath)) {
|
|
701
|
+
const config2 = ConfigSchema2.parse({});
|
|
312
702
|
saveConfig(config2);
|
|
313
703
|
createdConfig = true;
|
|
314
704
|
}
|
|
315
705
|
const config = loadConfig();
|
|
316
706
|
const workspaceSetting = config.agents.defaults.workspace;
|
|
317
|
-
const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ?
|
|
318
|
-
const workspaceExisted =
|
|
707
|
+
const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ? join3(getDataDir2(), DEFAULT_WORKSPACE_DIR) : expandHome(workspaceSetting);
|
|
708
|
+
const workspaceExisted = existsSync3(workspacePath);
|
|
319
709
|
mkdirSync2(workspacePath, { recursive: true });
|
|
320
710
|
const templateResult = this.createWorkspaceTemplates(workspacePath, { force });
|
|
321
711
|
if (createdConfig) {
|
|
@@ -479,10 +869,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
479
869
|
}
|
|
480
870
|
console.log(`${this.logo} Interactive mode (type exit or Ctrl+C to quit)
|
|
481
871
|
`);
|
|
482
|
-
const historyFile =
|
|
483
|
-
const historyDir =
|
|
872
|
+
const historyFile = join3(getDataDir2(), "history", "cli_history");
|
|
873
|
+
const historyDir = resolve3(historyFile, "..");
|
|
484
874
|
mkdirSync2(historyDir, { recursive: true });
|
|
485
|
-
const history =
|
|
875
|
+
const history = existsSync3(historyFile) ? readFileSync3(historyFile, "utf-8").split("\n").filter(Boolean) : [];
|
|
486
876
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
487
877
|
rl.on("close", () => {
|
|
488
878
|
const merged = history.concat(rl.history ?? []);
|
|
@@ -523,13 +913,13 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
523
913
|
const bridgeDir = this.getBridgeDir();
|
|
524
914
|
console.log(`${this.logo} Starting bridge...`);
|
|
525
915
|
console.log("Scan the QR code to connect.\n");
|
|
526
|
-
const result =
|
|
916
|
+
const result = spawnSync2("npm", ["start"], { cwd: bridgeDir, stdio: "inherit" });
|
|
527
917
|
if (result.status !== 0) {
|
|
528
918
|
console.error(`Bridge failed: ${result.status ?? 1}`);
|
|
529
919
|
}
|
|
530
920
|
}
|
|
531
921
|
cronList(opts) {
|
|
532
|
-
const storePath =
|
|
922
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
533
923
|
const service = new CronService(storePath);
|
|
534
924
|
const jobs = service.listJobs(Boolean(opts.all));
|
|
535
925
|
if (!jobs.length) {
|
|
@@ -549,7 +939,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
549
939
|
}
|
|
550
940
|
}
|
|
551
941
|
cronAdd(opts) {
|
|
552
|
-
const storePath =
|
|
942
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
553
943
|
const service = new CronService(storePath);
|
|
554
944
|
let schedule = null;
|
|
555
945
|
if (opts.every) {
|
|
@@ -574,7 +964,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
574
964
|
console.log(`\u2713 Added job '${job.name}' (${job.id})`);
|
|
575
965
|
}
|
|
576
966
|
cronRemove(jobId) {
|
|
577
|
-
const storePath =
|
|
967
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
578
968
|
const service = new CronService(storePath);
|
|
579
969
|
if (service.removeJob(jobId)) {
|
|
580
970
|
console.log(`\u2713 Removed job ${jobId}`);
|
|
@@ -583,7 +973,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
583
973
|
}
|
|
584
974
|
}
|
|
585
975
|
cronEnable(jobId, opts) {
|
|
586
|
-
const storePath =
|
|
976
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
587
977
|
const service = new CronService(storePath);
|
|
588
978
|
const job = service.enableJob(jobId, !opts.disable);
|
|
589
979
|
if (job) {
|
|
@@ -593,7 +983,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
593
983
|
}
|
|
594
984
|
}
|
|
595
985
|
async cronRun(jobId, opts) {
|
|
596
|
-
const storePath =
|
|
986
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
597
987
|
const service = new CronService(storePath);
|
|
598
988
|
const ok = await service.runJob(jobId, Boolean(opts.force));
|
|
599
989
|
console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
|
|
@@ -604,8 +994,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
604
994
|
const workspace = getWorkspacePath(config.agents.defaults.workspace);
|
|
605
995
|
console.log(`${this.logo} ${APP_NAME} Status
|
|
606
996
|
`);
|
|
607
|
-
console.log(`Config: ${configPath} ${
|
|
608
|
-
console.log(`Workspace: ${workspace} ${
|
|
997
|
+
console.log(`Config: ${configPath} ${existsSync3(configPath) ? "\u2713" : "\u2717"}`);
|
|
998
|
+
console.log(`Workspace: ${workspace} ${existsSync3(workspace) ? "\u2713" : "\u2717"}`);
|
|
609
999
|
console.log(`Model: ${config.agents.defaults.model}`);
|
|
610
1000
|
for (const spec of PROVIDERS) {
|
|
611
1001
|
const provider = config.providers[spec.name];
|
|
@@ -625,33 +1015,36 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
625
1015
|
const provider = options.allowMissingProvider === true ? this.makeProvider(config, { allowMissing: true }) : this.makeProvider(config);
|
|
626
1016
|
const providerManager = provider ? new ProviderManager(provider) : null;
|
|
627
1017
|
const sessionManager = new SessionManager(getWorkspacePath(config.agents.defaults.workspace));
|
|
628
|
-
const cronStorePath =
|
|
1018
|
+
const cronStorePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
629
1019
|
const cron2 = new CronService(cronStorePath);
|
|
630
1020
|
const uiConfig = resolveUiConfig(config, options.uiOverrides);
|
|
631
1021
|
const uiStaticDir = options.uiStaticDir === void 0 ? resolveUiStaticDir() : options.uiStaticDir;
|
|
632
1022
|
if (!provider) {
|
|
633
|
-
|
|
634
|
-
const uiServer = startUiServer({
|
|
635
|
-
host: uiConfig.host,
|
|
636
|
-
port: uiConfig.port,
|
|
637
|
-
configPath: getConfigPath(),
|
|
638
|
-
staticDir: uiStaticDir ?? void 0
|
|
639
|
-
});
|
|
640
|
-
const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
|
|
641
|
-
console.log(`\u2713 UI API: ${uiUrl}/api`);
|
|
642
|
-
if (uiStaticDir) {
|
|
643
|
-
console.log(`\u2713 UI frontend: ${uiUrl}`);
|
|
644
|
-
}
|
|
645
|
-
if (uiConfig.open) {
|
|
646
|
-
openBrowser(uiUrl);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
1023
|
+
this.startUiIfEnabled(uiConfig, uiStaticDir);
|
|
649
1024
|
console.log("Warning: No API key configured. UI server only.");
|
|
650
1025
|
await new Promise(() => {
|
|
651
1026
|
});
|
|
652
1027
|
return;
|
|
653
1028
|
}
|
|
654
|
-
const
|
|
1029
|
+
const channels2 = new ChannelManager(config, bus, sessionManager);
|
|
1030
|
+
const reloader = new ConfigReloader({
|
|
1031
|
+
initialConfig: config,
|
|
1032
|
+
channels: channels2,
|
|
1033
|
+
bus,
|
|
1034
|
+
sessionManager,
|
|
1035
|
+
providerManager,
|
|
1036
|
+
makeProvider: (nextConfig) => this.makeProvider(nextConfig, { allowMissing: true }),
|
|
1037
|
+
loadConfig,
|
|
1038
|
+
onRestartRequired: (paths) => {
|
|
1039
|
+
console.warn(`Config changes require restart: ${paths.join(", ")}`);
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
const gatewayController = new GatewayControllerImpl({
|
|
1043
|
+
reloader,
|
|
1044
|
+
cron: cron2,
|
|
1045
|
+
getConfigPath,
|
|
1046
|
+
saveConfig
|
|
1047
|
+
});
|
|
655
1048
|
const agent = new AgentLoop({
|
|
656
1049
|
bus,
|
|
657
1050
|
providerManager: providerManager ?? new ProviderManager(provider),
|
|
@@ -690,385 +1083,12 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
690
1083
|
30 * 60,
|
|
691
1084
|
true
|
|
692
1085
|
);
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
let reloadTask = null;
|
|
696
|
-
const reloadChannels = async (nextConfig) => {
|
|
697
|
-
if (reloadTask) {
|
|
698
|
-
await reloadTask;
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
reloadTask = (async () => {
|
|
702
|
-
await channels2.stopAll();
|
|
703
|
-
channels2 = new ChannelManager(nextConfig, bus, sessionManager);
|
|
704
|
-
await channels2.startAll();
|
|
705
|
-
})();
|
|
706
|
-
try {
|
|
707
|
-
await reloadTask;
|
|
708
|
-
} finally {
|
|
709
|
-
reloadTask = null;
|
|
710
|
-
}
|
|
711
|
-
};
|
|
712
|
-
let providerReloadTask = null;
|
|
713
|
-
const reloadProvider = async (nextConfig) => {
|
|
714
|
-
if (!providerManager) {
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
if (providerReloadTask) {
|
|
718
|
-
await providerReloadTask;
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
providerReloadTask = (async () => {
|
|
722
|
-
const nextProvider = this.makeProvider(nextConfig, { allowMissing: true });
|
|
723
|
-
if (!nextProvider) {
|
|
724
|
-
console.warn("Provider reload skipped: missing API key.");
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
providerManager.set(nextProvider);
|
|
728
|
-
})();
|
|
729
|
-
try {
|
|
730
|
-
await providerReloadTask;
|
|
731
|
-
} finally {
|
|
732
|
-
providerReloadTask = null;
|
|
733
|
-
}
|
|
734
|
-
};
|
|
735
|
-
const applyReloadPlan = async (nextConfig) => {
|
|
736
|
-
const changedPaths = diffConfigPaths(currentConfig, nextConfig);
|
|
737
|
-
if (!changedPaths.length) {
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
currentConfig = nextConfig;
|
|
741
|
-
const plan = buildReloadPlan(changedPaths);
|
|
742
|
-
if (plan.restartChannels) {
|
|
743
|
-
await reloadChannels(nextConfig);
|
|
744
|
-
}
|
|
745
|
-
if (plan.reloadProviders) {
|
|
746
|
-
await reloadProvider(nextConfig);
|
|
747
|
-
}
|
|
748
|
-
if (plan.restartRequired.length > 0) {
|
|
749
|
-
console.warn(`Config changes require restart: ${plan.restartRequired.join(", ")}`);
|
|
750
|
-
}
|
|
751
|
-
};
|
|
752
|
-
let reloadTimer = null;
|
|
753
|
-
let reloadRunning = false;
|
|
754
|
-
let reloadPending = false;
|
|
755
|
-
const scheduleConfigReload = (reason) => {
|
|
756
|
-
if (reloadTimer) {
|
|
757
|
-
clearTimeout(reloadTimer);
|
|
758
|
-
}
|
|
759
|
-
reloadTimer = setTimeout(() => {
|
|
760
|
-
void runConfigReload(reason);
|
|
761
|
-
}, 300);
|
|
762
|
-
};
|
|
763
|
-
const runConfigReload = async (reason) => {
|
|
764
|
-
if (reloadRunning) {
|
|
765
|
-
reloadPending = true;
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
reloadRunning = true;
|
|
769
|
-
if (reloadTimer) {
|
|
770
|
-
clearTimeout(reloadTimer);
|
|
771
|
-
reloadTimer = null;
|
|
772
|
-
}
|
|
773
|
-
try {
|
|
774
|
-
const nextConfig = loadConfig();
|
|
775
|
-
await applyReloadPlan(nextConfig);
|
|
776
|
-
} catch (error) {
|
|
777
|
-
console.error(`Config reload failed (${reason}): ${String(error)}`);
|
|
778
|
-
} finally {
|
|
779
|
-
reloadRunning = false;
|
|
780
|
-
if (reloadPending) {
|
|
781
|
-
reloadPending = false;
|
|
782
|
-
scheduleConfigReload("pending");
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
const hashRaw = (raw) => createHash("sha256").update(raw).digest("hex");
|
|
787
|
-
const redactConfig = (value) => {
|
|
788
|
-
if (Array.isArray(value)) {
|
|
789
|
-
return value.map((entry) => redactConfig(entry));
|
|
790
|
-
}
|
|
791
|
-
if (!value || typeof value !== "object") {
|
|
792
|
-
return value;
|
|
793
|
-
}
|
|
794
|
-
const entries = value;
|
|
795
|
-
const output = {};
|
|
796
|
-
for (const [key, val] of Object.entries(entries)) {
|
|
797
|
-
if (/apiKey|token|secret|password|appId|clientSecret|accessKey/i.test(key)) {
|
|
798
|
-
output[key] = val ? "***" : val;
|
|
799
|
-
continue;
|
|
800
|
-
}
|
|
801
|
-
output[key] = redactConfig(val);
|
|
802
|
-
}
|
|
803
|
-
return output;
|
|
804
|
-
};
|
|
805
|
-
const readConfigSnapshot = () => {
|
|
806
|
-
const path = getConfigPath();
|
|
807
|
-
let raw = "";
|
|
808
|
-
let parsed = {};
|
|
809
|
-
if (existsSync2(path)) {
|
|
810
|
-
raw = readFileSync2(path, "utf-8");
|
|
811
|
-
try {
|
|
812
|
-
parsed = JSON.parse(raw);
|
|
813
|
-
} catch {
|
|
814
|
-
parsed = {};
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
let config2;
|
|
818
|
-
let valid = true;
|
|
819
|
-
try {
|
|
820
|
-
config2 = ConfigSchema.parse(parsed);
|
|
821
|
-
} catch {
|
|
822
|
-
config2 = ConfigSchema.parse({});
|
|
823
|
-
valid = false;
|
|
824
|
-
}
|
|
825
|
-
if (!raw) {
|
|
826
|
-
raw = JSON.stringify(config2, null, 2);
|
|
827
|
-
}
|
|
828
|
-
const hash = hashRaw(raw);
|
|
829
|
-
const redacted = redactConfig(config2);
|
|
830
|
-
return { raw: valid ? JSON.stringify(redacted, null, 2) : null, hash: valid ? hash : null, config: config2, redacted, valid };
|
|
831
|
-
};
|
|
832
|
-
const mergeDeep = (base, patch) => {
|
|
833
|
-
const next = { ...base };
|
|
834
|
-
for (const [key, value] of Object.entries(patch)) {
|
|
835
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
836
|
-
const baseVal = base[key];
|
|
837
|
-
if (baseVal && typeof baseVal === "object" && !Array.isArray(baseVal)) {
|
|
838
|
-
next[key] = mergeDeep(baseVal, value);
|
|
839
|
-
} else {
|
|
840
|
-
next[key] = mergeDeep({}, value);
|
|
841
|
-
}
|
|
842
|
-
} else {
|
|
843
|
-
next[key] = value;
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
return next;
|
|
847
|
-
};
|
|
848
|
-
const buildSchemaFromValue = (value) => {
|
|
849
|
-
if (Array.isArray(value)) {
|
|
850
|
-
const item = value.length ? buildSchemaFromValue(value[0]) : { type: "string" };
|
|
851
|
-
return { type: "array", items: item };
|
|
852
|
-
}
|
|
853
|
-
if (value && typeof value === "object") {
|
|
854
|
-
const props = {};
|
|
855
|
-
for (const [key, val] of Object.entries(value)) {
|
|
856
|
-
props[key] = buildSchemaFromValue(val);
|
|
857
|
-
}
|
|
858
|
-
return { type: "object", properties: props };
|
|
859
|
-
}
|
|
860
|
-
if (typeof value === "number") {
|
|
861
|
-
return { type: "number" };
|
|
862
|
-
}
|
|
863
|
-
if (typeof value === "boolean") {
|
|
864
|
-
return { type: "boolean" };
|
|
865
|
-
}
|
|
866
|
-
if (value === null) {
|
|
867
|
-
return { type: ["null", "string"] };
|
|
868
|
-
}
|
|
869
|
-
return { type: "string" };
|
|
870
|
-
};
|
|
871
|
-
const scheduleRestart = (delayMs, reason) => {
|
|
872
|
-
const delay = typeof delayMs === "number" && Number.isFinite(delayMs) ? Math.max(0, delayMs) : 100;
|
|
873
|
-
console.log(`Gateway restart requested via tool${reason ? ` (${reason})` : ""}.`);
|
|
874
|
-
setTimeout(() => {
|
|
875
|
-
process.exit(0);
|
|
876
|
-
}, delay);
|
|
877
|
-
};
|
|
878
|
-
gatewayController.status = () => ({
|
|
879
|
-
channels: channels2.enabledChannels,
|
|
880
|
-
cron: cron2.status(),
|
|
881
|
-
configPath: getConfigPath()
|
|
882
|
-
});
|
|
883
|
-
gatewayController.reloadConfig = async (reason) => {
|
|
884
|
-
await runConfigReload(reason ?? "gateway tool");
|
|
885
|
-
return "Config reload triggered";
|
|
886
|
-
};
|
|
887
|
-
gatewayController.restart = async (options2) => {
|
|
888
|
-
scheduleRestart(options2?.delayMs, options2?.reason);
|
|
889
|
-
return "Restart scheduled";
|
|
890
|
-
};
|
|
891
|
-
gatewayController.getConfig = async () => {
|
|
892
|
-
const snapshot = readConfigSnapshot();
|
|
893
|
-
return {
|
|
894
|
-
raw: snapshot.raw,
|
|
895
|
-
hash: snapshot.hash,
|
|
896
|
-
path: getConfigPath(),
|
|
897
|
-
config: snapshot.redacted,
|
|
898
|
-
parsed: snapshot.redacted,
|
|
899
|
-
resolved: snapshot.redacted,
|
|
900
|
-
valid: snapshot.valid
|
|
901
|
-
};
|
|
902
|
-
};
|
|
903
|
-
gatewayController.getConfigSchema = async () => {
|
|
904
|
-
const base = ConfigSchema.parse({});
|
|
905
|
-
return {
|
|
906
|
-
schema: {
|
|
907
|
-
...buildSchemaFromValue(base),
|
|
908
|
-
title: "NextClawConfig",
|
|
909
|
-
description: "NextClaw config schema (simplified)"
|
|
910
|
-
},
|
|
911
|
-
uiHints: {},
|
|
912
|
-
version: getPackageVersion(),
|
|
913
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
914
|
-
};
|
|
915
|
-
};
|
|
916
|
-
gatewayController.applyConfig = async (params) => {
|
|
917
|
-
const snapshot = readConfigSnapshot();
|
|
918
|
-
if (!params.baseHash) {
|
|
919
|
-
return { ok: false, error: "config base hash required; re-run config.get and retry" };
|
|
920
|
-
}
|
|
921
|
-
if (!snapshot.valid || !snapshot.hash) {
|
|
922
|
-
return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
|
|
923
|
-
}
|
|
924
|
-
if (params.baseHash !== snapshot.hash) {
|
|
925
|
-
return { ok: false, error: "config changed since last load; re-run config.get and retry" };
|
|
926
|
-
}
|
|
927
|
-
let parsedRaw;
|
|
928
|
-
try {
|
|
929
|
-
parsedRaw = JSON.parse(params.raw);
|
|
930
|
-
} catch {
|
|
931
|
-
return { ok: false, error: "invalid JSON in raw config" };
|
|
932
|
-
}
|
|
933
|
-
let validated;
|
|
934
|
-
try {
|
|
935
|
-
validated = ConfigSchema.parse(parsedRaw);
|
|
936
|
-
} catch (err) {
|
|
937
|
-
return { ok: false, error: `invalid config: ${String(err)}` };
|
|
938
|
-
}
|
|
939
|
-
saveConfig(validated);
|
|
940
|
-
const delayMs = params.restartDelayMs ?? 0;
|
|
941
|
-
scheduleRestart(delayMs, "config.apply");
|
|
942
|
-
return {
|
|
943
|
-
ok: true,
|
|
944
|
-
note: params.note ?? null,
|
|
945
|
-
path: getConfigPath(),
|
|
946
|
-
config: redactConfig(validated),
|
|
947
|
-
restart: { scheduled: true, delayMs }
|
|
948
|
-
};
|
|
949
|
-
};
|
|
950
|
-
gatewayController.patchConfig = async (params) => {
|
|
951
|
-
const snapshot = readConfigSnapshot();
|
|
952
|
-
if (!params.baseHash) {
|
|
953
|
-
return { ok: false, error: "config base hash required; re-run config.get and retry" };
|
|
954
|
-
}
|
|
955
|
-
if (!snapshot.valid || !snapshot.hash) {
|
|
956
|
-
return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
|
|
957
|
-
}
|
|
958
|
-
if (params.baseHash !== snapshot.hash) {
|
|
959
|
-
return { ok: false, error: "config changed since last load; re-run config.get and retry" };
|
|
960
|
-
}
|
|
961
|
-
let patch;
|
|
962
|
-
try {
|
|
963
|
-
patch = JSON.parse(params.raw);
|
|
964
|
-
} catch {
|
|
965
|
-
return { ok: false, error: "invalid JSON in raw config" };
|
|
966
|
-
}
|
|
967
|
-
const merged = mergeDeep(snapshot.config, patch);
|
|
968
|
-
let validated;
|
|
969
|
-
try {
|
|
970
|
-
validated = ConfigSchema.parse(merged);
|
|
971
|
-
} catch (err) {
|
|
972
|
-
return { ok: false, error: `invalid config: ${String(err)}` };
|
|
973
|
-
}
|
|
974
|
-
saveConfig(validated);
|
|
975
|
-
const delayMs = params.restartDelayMs ?? 0;
|
|
976
|
-
scheduleRestart(delayMs, "config.patch");
|
|
977
|
-
return {
|
|
978
|
-
ok: true,
|
|
979
|
-
note: params.note ?? null,
|
|
980
|
-
path: getConfigPath(),
|
|
981
|
-
config: redactConfig(validated),
|
|
982
|
-
restart: { scheduled: true, delayMs }
|
|
983
|
-
};
|
|
984
|
-
};
|
|
985
|
-
gatewayController.updateRun = async (params) => {
|
|
986
|
-
const timeoutMs = params.timeoutMs ?? 20 * 6e4;
|
|
987
|
-
const cliDir = resolve2(fileURLToPath2(new URL(".", import.meta.url)));
|
|
988
|
-
const pkgRoot = resolve2(cliDir, "..", "..");
|
|
989
|
-
const repoRoot = existsSync2(join2(pkgRoot, ".git")) ? pkgRoot : resolve2(pkgRoot, "..", "..");
|
|
990
|
-
const steps = [];
|
|
991
|
-
const runStep = (cmd, args, cwd) => {
|
|
992
|
-
const result = spawnSync(cmd, args, {
|
|
993
|
-
cwd,
|
|
994
|
-
encoding: "utf-8",
|
|
995
|
-
timeout: timeoutMs,
|
|
996
|
-
stdio: "pipe"
|
|
997
|
-
});
|
|
998
|
-
const step = {
|
|
999
|
-
cmd,
|
|
1000
|
-
args,
|
|
1001
|
-
cwd,
|
|
1002
|
-
code: result.status,
|
|
1003
|
-
stdout: (result.stdout ?? "").toString().slice(0, 4e3),
|
|
1004
|
-
stderr: (result.stderr ?? "").toString().slice(0, 4e3)
|
|
1005
|
-
};
|
|
1006
|
-
steps.push(step);
|
|
1007
|
-
return { ok: result.status === 0, code: result.status };
|
|
1008
|
-
};
|
|
1009
|
-
const updateCommand = process.env.NEXTCLAW_UPDATE_COMMAND?.trim();
|
|
1010
|
-
if (updateCommand) {
|
|
1011
|
-
const ok = runStep("sh", ["-c", updateCommand], process.cwd());
|
|
1012
|
-
if (!ok.ok) {
|
|
1013
|
-
return { ok: false, error: "update command failed", steps };
|
|
1014
|
-
}
|
|
1015
|
-
} else if (existsSync2(join2(repoRoot, ".git"))) {
|
|
1016
|
-
if (!which("git")) {
|
|
1017
|
-
return { ok: false, error: "git not found for repo update", steps };
|
|
1018
|
-
}
|
|
1019
|
-
const ok = runStep("git", ["-C", repoRoot, "pull", "--rebase"], repoRoot);
|
|
1020
|
-
if (!ok.ok) {
|
|
1021
|
-
return { ok: false, error: "git pull failed", steps };
|
|
1022
|
-
}
|
|
1023
|
-
if (existsSync2(join2(repoRoot, "pnpm-lock.yaml")) && which("pnpm")) {
|
|
1024
|
-
const installOk = runStep("pnpm", ["install"], repoRoot);
|
|
1025
|
-
if (!installOk.ok) {
|
|
1026
|
-
return { ok: false, error: "pnpm install failed", steps };
|
|
1027
|
-
}
|
|
1028
|
-
} else if (existsSync2(join2(repoRoot, "package.json")) && which("npm")) {
|
|
1029
|
-
const installOk = runStep("npm", ["install"], repoRoot);
|
|
1030
|
-
if (!installOk.ok) {
|
|
1031
|
-
return { ok: false, error: "npm install failed", steps };
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
} else if (which("npm")) {
|
|
1035
|
-
const ok = runStep("npm", ["i", "-g", "nextclaw"], process.cwd());
|
|
1036
|
-
if (!ok.ok) {
|
|
1037
|
-
return { ok: false, error: "npm install -g nextclaw failed", steps };
|
|
1038
|
-
}
|
|
1039
|
-
} else {
|
|
1040
|
-
return { ok: false, error: "no update strategy available", steps };
|
|
1041
|
-
}
|
|
1042
|
-
const delayMs = params.restartDelayMs ?? 0;
|
|
1043
|
-
scheduleRestart(delayMs, "update.run");
|
|
1044
|
-
return {
|
|
1045
|
-
ok: true,
|
|
1046
|
-
note: params.note ?? null,
|
|
1047
|
-
restart: { scheduled: true, delayMs },
|
|
1048
|
-
steps
|
|
1049
|
-
};
|
|
1050
|
-
};
|
|
1051
|
-
if (channels2.enabledChannels.length) {
|
|
1052
|
-
console.log(`\u2713 Channels enabled: ${channels2.enabledChannels.join(", ")}`);
|
|
1086
|
+
if (reloader.getChannels().enabledChannels.length) {
|
|
1087
|
+
console.log(`\u2713 Channels enabled: ${reloader.getChannels().enabledChannels.join(", ")}`);
|
|
1053
1088
|
} else {
|
|
1054
1089
|
console.log("Warning: No channels enabled");
|
|
1055
1090
|
}
|
|
1056
|
-
|
|
1057
|
-
const uiServer = startUiServer({
|
|
1058
|
-
host: uiConfig.host,
|
|
1059
|
-
port: uiConfig.port,
|
|
1060
|
-
configPath: getConfigPath(),
|
|
1061
|
-
staticDir: uiStaticDir ?? void 0
|
|
1062
|
-
});
|
|
1063
|
-
const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
|
|
1064
|
-
console.log(`\u2713 UI API: ${uiUrl}/api`);
|
|
1065
|
-
if (uiStaticDir) {
|
|
1066
|
-
console.log(`\u2713 UI frontend: ${uiUrl}`);
|
|
1067
|
-
}
|
|
1068
|
-
if (uiConfig.open) {
|
|
1069
|
-
openBrowser(uiUrl);
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1091
|
+
this.startUiIfEnabled(uiConfig, uiStaticDir);
|
|
1072
1092
|
const cronStatus = cron2.status();
|
|
1073
1093
|
if (cronStatus.jobs > 0) {
|
|
1074
1094
|
console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
|
|
@@ -1079,12 +1099,31 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
1079
1099
|
ignoreInitial: true,
|
|
1080
1100
|
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
|
|
1081
1101
|
});
|
|
1082
|
-
watcher.on("add", () =>
|
|
1083
|
-
watcher.on("change", () =>
|
|
1084
|
-
watcher.on("unlink", () =>
|
|
1102
|
+
watcher.on("add", () => reloader.scheduleReload("config add"));
|
|
1103
|
+
watcher.on("change", () => reloader.scheduleReload("config change"));
|
|
1104
|
+
watcher.on("unlink", () => reloader.scheduleReload("config unlink"));
|
|
1085
1105
|
await cron2.start();
|
|
1086
1106
|
await heartbeat.start();
|
|
1087
|
-
await Promise.allSettled([agent.run(),
|
|
1107
|
+
await Promise.allSettled([agent.run(), reloader.getChannels().startAll()]);
|
|
1108
|
+
}
|
|
1109
|
+
startUiIfEnabled(uiConfig, uiStaticDir) {
|
|
1110
|
+
if (!uiConfig.enabled) {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const uiServer = startUiServer({
|
|
1114
|
+
host: uiConfig.host,
|
|
1115
|
+
port: uiConfig.port,
|
|
1116
|
+
configPath: getConfigPath(),
|
|
1117
|
+
staticDir: uiStaticDir ?? void 0
|
|
1118
|
+
});
|
|
1119
|
+
const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
|
|
1120
|
+
console.log(`\u2713 UI API: ${uiUrl}/api`);
|
|
1121
|
+
if (uiStaticDir) {
|
|
1122
|
+
console.log(`\u2713 UI frontend: ${uiUrl}`);
|
|
1123
|
+
}
|
|
1124
|
+
if (uiConfig.open) {
|
|
1125
|
+
openBrowser(uiUrl);
|
|
1126
|
+
}
|
|
1088
1127
|
}
|
|
1089
1128
|
async runForeground(options) {
|
|
1090
1129
|
const config = loadConfig();
|
|
@@ -1141,7 +1180,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
1141
1180
|
console.log("Warning: UI frontend not found. Use --frontend to start the dev server.");
|
|
1142
1181
|
}
|
|
1143
1182
|
const logPath = resolveServiceLogPath();
|
|
1144
|
-
const logDir =
|
|
1183
|
+
const logDir = resolve3(logPath, "..");
|
|
1145
1184
|
mkdirSync2(logDir, { recursive: true });
|
|
1146
1185
|
const logFd = openSync(logPath, "a");
|
|
1147
1186
|
const serveArgs = buildServeArgs({
|
|
@@ -1250,30 +1289,30 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
1250
1289
|
{ source: "memory/MEMORY.md", target: "memory/MEMORY.md" }
|
|
1251
1290
|
];
|
|
1252
1291
|
for (const entry of templateFiles) {
|
|
1253
|
-
const filePath =
|
|
1254
|
-
if (!force &&
|
|
1292
|
+
const filePath = join3(workspace, entry.target);
|
|
1293
|
+
if (!force && existsSync3(filePath)) {
|
|
1255
1294
|
continue;
|
|
1256
1295
|
}
|
|
1257
|
-
const templatePath =
|
|
1258
|
-
if (!
|
|
1296
|
+
const templatePath = join3(templateDir, entry.source);
|
|
1297
|
+
if (!existsSync3(templatePath)) {
|
|
1259
1298
|
console.warn(`Warning: Template file missing: ${templatePath}`);
|
|
1260
1299
|
continue;
|
|
1261
1300
|
}
|
|
1262
|
-
const raw =
|
|
1301
|
+
const raw = readFileSync3(templatePath, "utf-8");
|
|
1263
1302
|
const content = raw.replace(/\$\{APP_NAME\}/g, APP_NAME);
|
|
1264
1303
|
mkdirSync2(dirname(filePath), { recursive: true });
|
|
1265
1304
|
writeFileSync2(filePath, content);
|
|
1266
1305
|
created.push(entry.target);
|
|
1267
1306
|
}
|
|
1268
|
-
const memoryDir =
|
|
1269
|
-
if (!
|
|
1307
|
+
const memoryDir = join3(workspace, "memory");
|
|
1308
|
+
if (!existsSync3(memoryDir)) {
|
|
1270
1309
|
mkdirSync2(memoryDir, { recursive: true });
|
|
1271
|
-
created.push(
|
|
1310
|
+
created.push(join3("memory", ""));
|
|
1272
1311
|
}
|
|
1273
|
-
const skillsDir =
|
|
1274
|
-
if (!
|
|
1312
|
+
const skillsDir = join3(workspace, "skills");
|
|
1313
|
+
if (!existsSync3(skillsDir)) {
|
|
1275
1314
|
mkdirSync2(skillsDir, { recursive: true });
|
|
1276
|
-
created.push(
|
|
1315
|
+
created.push(join3("skills", ""));
|
|
1277
1316
|
}
|
|
1278
1317
|
return { created };
|
|
1279
1318
|
}
|
|
@@ -1282,33 +1321,33 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
1282
1321
|
if (override) {
|
|
1283
1322
|
return override;
|
|
1284
1323
|
}
|
|
1285
|
-
const cliDir =
|
|
1286
|
-
const pkgRoot =
|
|
1287
|
-
const candidates = [
|
|
1324
|
+
const cliDir = resolve3(fileURLToPath3(new URL(".", import.meta.url)));
|
|
1325
|
+
const pkgRoot = resolve3(cliDir, "..", "..");
|
|
1326
|
+
const candidates = [join3(pkgRoot, "templates")];
|
|
1288
1327
|
for (const candidate of candidates) {
|
|
1289
|
-
if (
|
|
1328
|
+
if (existsSync3(candidate)) {
|
|
1290
1329
|
return candidate;
|
|
1291
1330
|
}
|
|
1292
1331
|
}
|
|
1293
1332
|
return null;
|
|
1294
1333
|
}
|
|
1295
1334
|
getBridgeDir() {
|
|
1296
|
-
const userBridge =
|
|
1297
|
-
if (
|
|
1335
|
+
const userBridge = join3(getDataDir2(), "bridge");
|
|
1336
|
+
if (existsSync3(join3(userBridge, "dist", "index.js"))) {
|
|
1298
1337
|
return userBridge;
|
|
1299
1338
|
}
|
|
1300
1339
|
if (!which("npm")) {
|
|
1301
1340
|
console.error("npm not found. Please install Node.js >= 18.");
|
|
1302
1341
|
process.exit(1);
|
|
1303
1342
|
}
|
|
1304
|
-
const cliDir =
|
|
1305
|
-
const pkgRoot =
|
|
1306
|
-
const pkgBridge =
|
|
1307
|
-
const srcBridge =
|
|
1343
|
+
const cliDir = resolve3(fileURLToPath3(new URL(".", import.meta.url)));
|
|
1344
|
+
const pkgRoot = resolve3(cliDir, "..", "..");
|
|
1345
|
+
const pkgBridge = join3(pkgRoot, "bridge");
|
|
1346
|
+
const srcBridge = join3(pkgRoot, "..", "..", "bridge");
|
|
1308
1347
|
let source = null;
|
|
1309
|
-
if (
|
|
1348
|
+
if (existsSync3(join3(pkgBridge, "package.json"))) {
|
|
1310
1349
|
source = pkgBridge;
|
|
1311
|
-
} else if (
|
|
1350
|
+
} else if (existsSync3(join3(srcBridge, "package.json"))) {
|
|
1312
1351
|
source = srcBridge;
|
|
1313
1352
|
}
|
|
1314
1353
|
if (!source) {
|
|
@@ -1316,15 +1355,15 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
1316
1355
|
process.exit(1);
|
|
1317
1356
|
}
|
|
1318
1357
|
console.log(`${this.logo} Setting up bridge...`);
|
|
1319
|
-
mkdirSync2(
|
|
1320
|
-
if (
|
|
1358
|
+
mkdirSync2(resolve3(userBridge, ".."), { recursive: true });
|
|
1359
|
+
if (existsSync3(userBridge)) {
|
|
1321
1360
|
rmSync2(userBridge, { recursive: true, force: true });
|
|
1322
1361
|
}
|
|
1323
1362
|
cpSync(source, userBridge, {
|
|
1324
1363
|
recursive: true,
|
|
1325
1364
|
filter: (src) => !src.includes("node_modules") && !src.includes("dist")
|
|
1326
1365
|
});
|
|
1327
|
-
const install =
|
|
1366
|
+
const install = spawnSync2("npm", ["install"], { cwd: userBridge, stdio: "pipe" });
|
|
1328
1367
|
if (install.status !== 0) {
|
|
1329
1368
|
console.error(`Bridge install failed: ${install.status ?? 1}`);
|
|
1330
1369
|
if (install.stderr) {
|
|
@@ -1332,7 +1371,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
1332
1371
|
}
|
|
1333
1372
|
process.exit(1);
|
|
1334
1373
|
}
|
|
1335
|
-
const build =
|
|
1374
|
+
const build = spawnSync2("npm", ["run", "build"], { cwd: userBridge, stdio: "pipe" });
|
|
1336
1375
|
if (build.status !== 0) {
|
|
1337
1376
|
console.error(`Bridge build failed: ${build.status ?? 1}`);
|
|
1338
1377
|
if (build.stderr) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nextclaw",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Lightweight personal AI assistant with CLI, multi-provider routing, and channel integrations.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"chokidar": "^3.6.0",
|
|
40
40
|
"commander": "^12.1.0",
|
|
41
|
-
"nextclaw-core": "^0.4.
|
|
42
|
-
"nextclaw-server": "^0.3.
|
|
41
|
+
"nextclaw-core": "^0.4.1",
|
|
42
|
+
"nextclaw-server": "^0.3.2"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^20.17.6",
|