nextclaw 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +545 -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,31 @@ 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
|
-
|
|
41
|
+
readdirSync,
|
|
42
|
+
readFileSync as readFileSync3,
|
|
42
43
|
rmSync as rmSync2,
|
|
43
44
|
writeFileSync as writeFileSync2
|
|
44
45
|
} from "fs";
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import { spawn as spawn2, spawnSync } from "child_process";
|
|
46
|
+
import { dirname, join as join3, resolve as resolve3 } from "path";
|
|
47
|
+
import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
|
|
48
48
|
import { createInterface } from "readline";
|
|
49
|
-
import {
|
|
49
|
+
import { createRequire } from "module";
|
|
50
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
50
51
|
import chokidar from "chokidar";
|
|
51
52
|
|
|
53
|
+
// src/cli/gateway/controller.ts
|
|
54
|
+
import { createHash } from "crypto";
|
|
55
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
56
|
+
import { spawnSync } from "child_process";
|
|
57
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
58
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
59
|
+
import {
|
|
60
|
+
ConfigSchema
|
|
61
|
+
} from "nextclaw-core";
|
|
62
|
+
|
|
52
63
|
// src/cli/utils.ts
|
|
53
64
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "fs";
|
|
54
65
|
import { join, resolve } from "path";
|
|
@@ -87,12 +98,12 @@ async function isPortAvailable(port, host) {
|
|
|
87
98
|
return await canBindPort(port, checkHost);
|
|
88
99
|
}
|
|
89
100
|
async function canBindPort(port, host) {
|
|
90
|
-
return await new Promise((
|
|
101
|
+
return await new Promise((resolve4) => {
|
|
91
102
|
const server = createServer();
|
|
92
103
|
server.unref();
|
|
93
|
-
server.once("error", () =>
|
|
104
|
+
server.once("error", () => resolve4(false));
|
|
94
105
|
server.listen({ port, host }, () => {
|
|
95
|
-
server.close(() =>
|
|
106
|
+
server.close(() => resolve4(true));
|
|
96
107
|
});
|
|
97
108
|
});
|
|
98
109
|
}
|
|
@@ -150,7 +161,7 @@ async function waitForExit(pid, timeoutMs) {
|
|
|
150
161
|
if (!isProcessRunning(pid)) {
|
|
151
162
|
return true;
|
|
152
163
|
}
|
|
153
|
-
await new Promise((
|
|
164
|
+
await new Promise((resolve4) => setTimeout(resolve4, 200));
|
|
154
165
|
}
|
|
155
166
|
return !isProcessRunning(pid);
|
|
156
167
|
}
|
|
@@ -281,14 +292,395 @@ function printAgentResponse(response) {
|
|
|
281
292
|
async function prompt(rl, question) {
|
|
282
293
|
rl.setPrompt(question);
|
|
283
294
|
rl.prompt();
|
|
284
|
-
return new Promise((
|
|
285
|
-
rl.once("line", (line) =>
|
|
295
|
+
return new Promise((resolve4) => {
|
|
296
|
+
rl.once("line", (line) => resolve4(line));
|
|
286
297
|
});
|
|
287
298
|
}
|
|
288
299
|
|
|
300
|
+
// src/cli/gateway/controller.ts
|
|
301
|
+
var hashRaw = (raw) => createHash("sha256").update(raw).digest("hex");
|
|
302
|
+
var redactConfig = (value) => {
|
|
303
|
+
if (Array.isArray(value)) {
|
|
304
|
+
return value.map((entry) => redactConfig(entry));
|
|
305
|
+
}
|
|
306
|
+
if (!value || typeof value !== "object") {
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
const entries = value;
|
|
310
|
+
const output = {};
|
|
311
|
+
for (const [key, val] of Object.entries(entries)) {
|
|
312
|
+
if (/apiKey|token|secret|password|appId|clientSecret|accessKey/i.test(key)) {
|
|
313
|
+
output[key] = val ? "***" : val;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
output[key] = redactConfig(val);
|
|
317
|
+
}
|
|
318
|
+
return output;
|
|
319
|
+
};
|
|
320
|
+
var readConfigSnapshot = (getConfigPath2) => {
|
|
321
|
+
const path = getConfigPath2();
|
|
322
|
+
let raw = "";
|
|
323
|
+
let parsed = {};
|
|
324
|
+
if (existsSync2(path)) {
|
|
325
|
+
raw = readFileSync2(path, "utf-8");
|
|
326
|
+
try {
|
|
327
|
+
parsed = JSON.parse(raw);
|
|
328
|
+
} catch {
|
|
329
|
+
parsed = {};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
let config;
|
|
333
|
+
let valid = true;
|
|
334
|
+
try {
|
|
335
|
+
config = ConfigSchema.parse(parsed);
|
|
336
|
+
} catch {
|
|
337
|
+
config = ConfigSchema.parse({});
|
|
338
|
+
valid = false;
|
|
339
|
+
}
|
|
340
|
+
if (!raw) {
|
|
341
|
+
raw = JSON.stringify(config, null, 2);
|
|
342
|
+
}
|
|
343
|
+
const hash = hashRaw(raw);
|
|
344
|
+
const redacted = redactConfig(config);
|
|
345
|
+
return { raw: valid ? JSON.stringify(redacted, null, 2) : null, hash: valid ? hash : null, config, redacted, valid };
|
|
346
|
+
};
|
|
347
|
+
var mergeDeep = (base, patch) => {
|
|
348
|
+
const next = { ...base };
|
|
349
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
350
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
351
|
+
const baseVal = base[key];
|
|
352
|
+
if (baseVal && typeof baseVal === "object" && !Array.isArray(baseVal)) {
|
|
353
|
+
next[key] = mergeDeep(baseVal, value);
|
|
354
|
+
} else {
|
|
355
|
+
next[key] = mergeDeep({}, value);
|
|
356
|
+
}
|
|
357
|
+
} else {
|
|
358
|
+
next[key] = value;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return next;
|
|
362
|
+
};
|
|
363
|
+
var buildSchemaFromValue = (value) => {
|
|
364
|
+
if (Array.isArray(value)) {
|
|
365
|
+
const item = value.length ? buildSchemaFromValue(value[0]) : { type: "string" };
|
|
366
|
+
return { type: "array", items: item };
|
|
367
|
+
}
|
|
368
|
+
if (value && typeof value === "object") {
|
|
369
|
+
const props = {};
|
|
370
|
+
for (const [key, val] of Object.entries(value)) {
|
|
371
|
+
props[key] = buildSchemaFromValue(val);
|
|
372
|
+
}
|
|
373
|
+
return { type: "object", properties: props };
|
|
374
|
+
}
|
|
375
|
+
if (typeof value === "number") {
|
|
376
|
+
return { type: "number" };
|
|
377
|
+
}
|
|
378
|
+
if (typeof value === "boolean") {
|
|
379
|
+
return { type: "boolean" };
|
|
380
|
+
}
|
|
381
|
+
if (value === null) {
|
|
382
|
+
return { type: ["null", "string"] };
|
|
383
|
+
}
|
|
384
|
+
return { type: "string" };
|
|
385
|
+
};
|
|
386
|
+
var scheduleRestart = (delayMs, reason) => {
|
|
387
|
+
const delay = typeof delayMs === "number" && Number.isFinite(delayMs) ? Math.max(0, delayMs) : 100;
|
|
388
|
+
console.log(`Gateway restart requested via tool${reason ? ` (${reason})` : ""}.`);
|
|
389
|
+
setTimeout(() => {
|
|
390
|
+
process.exit(0);
|
|
391
|
+
}, delay);
|
|
392
|
+
};
|
|
393
|
+
var GatewayControllerImpl = class {
|
|
394
|
+
constructor(deps) {
|
|
395
|
+
this.deps = deps;
|
|
396
|
+
}
|
|
397
|
+
status() {
|
|
398
|
+
return {
|
|
399
|
+
channels: this.deps.reloader.getChannels().enabledChannels,
|
|
400
|
+
cron: this.deps.cron.status(),
|
|
401
|
+
configPath: this.deps.getConfigPath()
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
async reloadConfig(reason) {
|
|
405
|
+
return this.deps.reloader.reloadConfig(reason);
|
|
406
|
+
}
|
|
407
|
+
async restart(options) {
|
|
408
|
+
scheduleRestart(options?.delayMs, options?.reason);
|
|
409
|
+
return "Restart scheduled";
|
|
410
|
+
}
|
|
411
|
+
async getConfig() {
|
|
412
|
+
const snapshot = readConfigSnapshot(this.deps.getConfigPath);
|
|
413
|
+
return {
|
|
414
|
+
raw: snapshot.raw,
|
|
415
|
+
hash: snapshot.hash,
|
|
416
|
+
path: this.deps.getConfigPath(),
|
|
417
|
+
config: snapshot.redacted,
|
|
418
|
+
parsed: snapshot.redacted,
|
|
419
|
+
resolved: snapshot.redacted,
|
|
420
|
+
valid: snapshot.valid
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
async getConfigSchema() {
|
|
424
|
+
const base = ConfigSchema.parse({});
|
|
425
|
+
return {
|
|
426
|
+
schema: {
|
|
427
|
+
...buildSchemaFromValue(base),
|
|
428
|
+
title: "NextClawConfig",
|
|
429
|
+
description: "NextClaw config schema (simplified)"
|
|
430
|
+
},
|
|
431
|
+
uiHints: {},
|
|
432
|
+
version: getPackageVersion(),
|
|
433
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
async applyConfig(params) {
|
|
437
|
+
const snapshot = readConfigSnapshot(this.deps.getConfigPath);
|
|
438
|
+
if (!params.baseHash) {
|
|
439
|
+
return { ok: false, error: "config base hash required; re-run config.get and retry" };
|
|
440
|
+
}
|
|
441
|
+
if (!snapshot.valid || !snapshot.hash) {
|
|
442
|
+
return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
|
|
443
|
+
}
|
|
444
|
+
if (params.baseHash !== snapshot.hash) {
|
|
445
|
+
return { ok: false, error: "config changed since last load; re-run config.get and retry" };
|
|
446
|
+
}
|
|
447
|
+
let parsedRaw;
|
|
448
|
+
try {
|
|
449
|
+
parsedRaw = JSON.parse(params.raw);
|
|
450
|
+
} catch {
|
|
451
|
+
return { ok: false, error: "invalid JSON in raw config" };
|
|
452
|
+
}
|
|
453
|
+
let validated;
|
|
454
|
+
try {
|
|
455
|
+
validated = ConfigSchema.parse(parsedRaw);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
return { ok: false, error: `invalid config: ${String(err)}` };
|
|
458
|
+
}
|
|
459
|
+
this.deps.saveConfig(validated);
|
|
460
|
+
const delayMs = params.restartDelayMs ?? 0;
|
|
461
|
+
scheduleRestart(delayMs, "config.apply");
|
|
462
|
+
return {
|
|
463
|
+
ok: true,
|
|
464
|
+
note: params.note ?? null,
|
|
465
|
+
path: this.deps.getConfigPath(),
|
|
466
|
+
config: redactConfig(validated),
|
|
467
|
+
restart: { scheduled: true, delayMs }
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
async patchConfig(params) {
|
|
471
|
+
const snapshot = readConfigSnapshot(this.deps.getConfigPath);
|
|
472
|
+
if (!params.baseHash) {
|
|
473
|
+
return { ok: false, error: "config base hash required; re-run config.get and retry" };
|
|
474
|
+
}
|
|
475
|
+
if (!snapshot.valid || !snapshot.hash) {
|
|
476
|
+
return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
|
|
477
|
+
}
|
|
478
|
+
if (params.baseHash !== snapshot.hash) {
|
|
479
|
+
return { ok: false, error: "config changed since last load; re-run config.get and retry" };
|
|
480
|
+
}
|
|
481
|
+
let patch;
|
|
482
|
+
try {
|
|
483
|
+
patch = JSON.parse(params.raw);
|
|
484
|
+
} catch {
|
|
485
|
+
return { ok: false, error: "invalid JSON in raw config" };
|
|
486
|
+
}
|
|
487
|
+
const merged = mergeDeep(snapshot.config, patch);
|
|
488
|
+
let validated;
|
|
489
|
+
try {
|
|
490
|
+
validated = ConfigSchema.parse(merged);
|
|
491
|
+
} catch (err) {
|
|
492
|
+
return { ok: false, error: `invalid config: ${String(err)}` };
|
|
493
|
+
}
|
|
494
|
+
this.deps.saveConfig(validated);
|
|
495
|
+
const delayMs = params.restartDelayMs ?? 0;
|
|
496
|
+
scheduleRestart(delayMs, "config.patch");
|
|
497
|
+
return {
|
|
498
|
+
ok: true,
|
|
499
|
+
note: params.note ?? null,
|
|
500
|
+
path: this.deps.getConfigPath(),
|
|
501
|
+
config: redactConfig(validated),
|
|
502
|
+
restart: { scheduled: true, delayMs }
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
async updateRun(params) {
|
|
506
|
+
const timeoutMs = params.timeoutMs ?? 20 * 6e4;
|
|
507
|
+
const gatewayDir = resolve2(fileURLToPath2(new URL(".", import.meta.url)));
|
|
508
|
+
const cliDir = resolve2(gatewayDir, "..");
|
|
509
|
+
const pkgRoot = resolve2(cliDir, "..", "..");
|
|
510
|
+
const repoRoot = existsSync2(join2(pkgRoot, ".git")) ? pkgRoot : resolve2(pkgRoot, "..", "..");
|
|
511
|
+
const steps = [];
|
|
512
|
+
const runStep = (cmd, args, cwd) => {
|
|
513
|
+
const result = spawnSync(cmd, args, {
|
|
514
|
+
cwd,
|
|
515
|
+
encoding: "utf-8",
|
|
516
|
+
timeout: timeoutMs,
|
|
517
|
+
stdio: "pipe"
|
|
518
|
+
});
|
|
519
|
+
const step = {
|
|
520
|
+
cmd,
|
|
521
|
+
args,
|
|
522
|
+
cwd,
|
|
523
|
+
code: result.status,
|
|
524
|
+
stdout: (result.stdout ?? "").toString().slice(0, 4e3),
|
|
525
|
+
stderr: (result.stderr ?? "").toString().slice(0, 4e3)
|
|
526
|
+
};
|
|
527
|
+
steps.push(step);
|
|
528
|
+
return { ok: result.status === 0, code: result.status };
|
|
529
|
+
};
|
|
530
|
+
const updateCommand = process.env.NEXTCLAW_UPDATE_COMMAND?.trim();
|
|
531
|
+
if (updateCommand) {
|
|
532
|
+
const ok = runStep("sh", ["-c", updateCommand], process.cwd());
|
|
533
|
+
if (!ok.ok) {
|
|
534
|
+
return { ok: false, error: "update command failed", steps };
|
|
535
|
+
}
|
|
536
|
+
} else if (existsSync2(join2(repoRoot, ".git"))) {
|
|
537
|
+
if (!which("git")) {
|
|
538
|
+
return { ok: false, error: "git not found for repo update", steps };
|
|
539
|
+
}
|
|
540
|
+
const ok = runStep("git", ["-C", repoRoot, "pull", "--rebase"], repoRoot);
|
|
541
|
+
if (!ok.ok) {
|
|
542
|
+
return { ok: false, error: "git pull failed", steps };
|
|
543
|
+
}
|
|
544
|
+
if (existsSync2(join2(repoRoot, "pnpm-lock.yaml")) && which("pnpm")) {
|
|
545
|
+
const installOk = runStep("pnpm", ["install"], repoRoot);
|
|
546
|
+
if (!installOk.ok) {
|
|
547
|
+
return { ok: false, error: "pnpm install failed", steps };
|
|
548
|
+
}
|
|
549
|
+
} else if (existsSync2(join2(repoRoot, "package.json")) && which("npm")) {
|
|
550
|
+
const installOk = runStep("npm", ["install"], repoRoot);
|
|
551
|
+
if (!installOk.ok) {
|
|
552
|
+
return { ok: false, error: "npm install failed", steps };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
} else if (which("npm")) {
|
|
556
|
+
const ok = runStep("npm", ["i", "-g", "nextclaw"], process.cwd());
|
|
557
|
+
if (!ok.ok) {
|
|
558
|
+
return { ok: false, error: "npm install -g nextclaw failed", steps };
|
|
559
|
+
}
|
|
560
|
+
} else {
|
|
561
|
+
return { ok: false, error: "no update strategy available", steps };
|
|
562
|
+
}
|
|
563
|
+
const delayMs = params.restartDelayMs ?? 0;
|
|
564
|
+
scheduleRestart(delayMs, "update.run");
|
|
565
|
+
return {
|
|
566
|
+
ok: true,
|
|
567
|
+
note: params.note ?? null,
|
|
568
|
+
restart: { scheduled: true, delayMs },
|
|
569
|
+
steps
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
};
|
|
573
|
+
|
|
289
574
|
// src/cli/runtime.ts
|
|
290
575
|
var LOGO = "\u{1F916}";
|
|
291
576
|
var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "/exit", "/quit", ":q"]);
|
|
577
|
+
var ConfigReloader = class {
|
|
578
|
+
constructor(options) {
|
|
579
|
+
this.options = options;
|
|
580
|
+
this.currentConfig = options.initialConfig;
|
|
581
|
+
this.channels = options.channels;
|
|
582
|
+
}
|
|
583
|
+
currentConfig;
|
|
584
|
+
channels;
|
|
585
|
+
reloadTask = null;
|
|
586
|
+
providerReloadTask = null;
|
|
587
|
+
reloadTimer = null;
|
|
588
|
+
reloadRunning = false;
|
|
589
|
+
reloadPending = false;
|
|
590
|
+
getChannels() {
|
|
591
|
+
return this.channels;
|
|
592
|
+
}
|
|
593
|
+
async applyReloadPlan(nextConfig) {
|
|
594
|
+
const changedPaths = diffConfigPaths(this.currentConfig, nextConfig);
|
|
595
|
+
if (!changedPaths.length) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
this.currentConfig = nextConfig;
|
|
599
|
+
const plan = buildReloadPlan(changedPaths);
|
|
600
|
+
if (plan.restartChannels) {
|
|
601
|
+
await this.reloadChannels(nextConfig);
|
|
602
|
+
}
|
|
603
|
+
if (plan.reloadProviders) {
|
|
604
|
+
await this.reloadProvider(nextConfig);
|
|
605
|
+
}
|
|
606
|
+
if (plan.restartRequired.length > 0) {
|
|
607
|
+
this.options.onRestartRequired(plan.restartRequired);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
scheduleReload(reason) {
|
|
611
|
+
if (this.reloadTimer) {
|
|
612
|
+
clearTimeout(this.reloadTimer);
|
|
613
|
+
}
|
|
614
|
+
this.reloadTimer = setTimeout(() => {
|
|
615
|
+
void this.runReload(reason);
|
|
616
|
+
}, 300);
|
|
617
|
+
}
|
|
618
|
+
async runReload(reason) {
|
|
619
|
+
if (this.reloadRunning) {
|
|
620
|
+
this.reloadPending = true;
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
this.reloadRunning = true;
|
|
624
|
+
if (this.reloadTimer) {
|
|
625
|
+
clearTimeout(this.reloadTimer);
|
|
626
|
+
this.reloadTimer = null;
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
const nextConfig = this.options.loadConfig();
|
|
630
|
+
await this.applyReloadPlan(nextConfig);
|
|
631
|
+
} catch (error) {
|
|
632
|
+
console.error(`Config reload failed (${reason}): ${String(error)}`);
|
|
633
|
+
} finally {
|
|
634
|
+
this.reloadRunning = false;
|
|
635
|
+
if (this.reloadPending) {
|
|
636
|
+
this.reloadPending = false;
|
|
637
|
+
this.scheduleReload("pending");
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
async reloadConfig(reason) {
|
|
642
|
+
await this.runReload(reason ?? "gateway tool");
|
|
643
|
+
return "Config reload triggered";
|
|
644
|
+
}
|
|
645
|
+
async reloadChannels(nextConfig) {
|
|
646
|
+
if (this.reloadTask) {
|
|
647
|
+
await this.reloadTask;
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
this.reloadTask = (async () => {
|
|
651
|
+
await this.channels.stopAll();
|
|
652
|
+
this.channels = new ChannelManager(nextConfig, this.options.bus, this.options.sessionManager);
|
|
653
|
+
await this.channels.startAll();
|
|
654
|
+
})();
|
|
655
|
+
try {
|
|
656
|
+
await this.reloadTask;
|
|
657
|
+
} finally {
|
|
658
|
+
this.reloadTask = null;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
async reloadProvider(nextConfig) {
|
|
662
|
+
if (!this.options.providerManager) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (this.providerReloadTask) {
|
|
666
|
+
await this.providerReloadTask;
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
this.providerReloadTask = (async () => {
|
|
670
|
+
const nextProvider = this.options.makeProvider(nextConfig);
|
|
671
|
+
if (!nextProvider) {
|
|
672
|
+
console.warn("Provider reload skipped: missing API key.");
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
this.options.providerManager?.set(nextProvider);
|
|
676
|
+
})();
|
|
677
|
+
try {
|
|
678
|
+
await this.providerReloadTask;
|
|
679
|
+
} finally {
|
|
680
|
+
this.providerReloadTask = null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
};
|
|
292
684
|
var CliRuntime = class {
|
|
293
685
|
logo;
|
|
294
686
|
constructor(options = {}) {
|
|
@@ -307,15 +699,15 @@ var CliRuntime = class {
|
|
|
307
699
|
const force = Boolean(options.force);
|
|
308
700
|
const configPath = getConfigPath();
|
|
309
701
|
let createdConfig = false;
|
|
310
|
-
if (!
|
|
311
|
-
const config2 =
|
|
702
|
+
if (!existsSync3(configPath)) {
|
|
703
|
+
const config2 = ConfigSchema2.parse({});
|
|
312
704
|
saveConfig(config2);
|
|
313
705
|
createdConfig = true;
|
|
314
706
|
}
|
|
315
707
|
const config = loadConfig();
|
|
316
708
|
const workspaceSetting = config.agents.defaults.workspace;
|
|
317
|
-
const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ?
|
|
318
|
-
const workspaceExisted =
|
|
709
|
+
const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ? join3(getDataDir2(), DEFAULT_WORKSPACE_DIR) : expandHome(workspaceSetting);
|
|
710
|
+
const workspaceExisted = existsSync3(workspacePath);
|
|
319
711
|
mkdirSync2(workspacePath, { recursive: true });
|
|
320
712
|
const templateResult = this.createWorkspaceTemplates(workspacePath, { force });
|
|
321
713
|
if (createdConfig) {
|
|
@@ -479,10 +871,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
479
871
|
}
|
|
480
872
|
console.log(`${this.logo} Interactive mode (type exit or Ctrl+C to quit)
|
|
481
873
|
`);
|
|
482
|
-
const historyFile =
|
|
483
|
-
const historyDir =
|
|
874
|
+
const historyFile = join3(getDataDir2(), "history", "cli_history");
|
|
875
|
+
const historyDir = resolve3(historyFile, "..");
|
|
484
876
|
mkdirSync2(historyDir, { recursive: true });
|
|
485
|
-
const history =
|
|
877
|
+
const history = existsSync3(historyFile) ? readFileSync3(historyFile, "utf-8").split("\n").filter(Boolean) : [];
|
|
486
878
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
487
879
|
rl.on("close", () => {
|
|
488
880
|
const merged = history.concat(rl.history ?? []);
|
|
@@ -523,13 +915,13 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
523
915
|
const bridgeDir = this.getBridgeDir();
|
|
524
916
|
console.log(`${this.logo} Starting bridge...`);
|
|
525
917
|
console.log("Scan the QR code to connect.\n");
|
|
526
|
-
const result =
|
|
918
|
+
const result = spawnSync2("npm", ["start"], { cwd: bridgeDir, stdio: "inherit" });
|
|
527
919
|
if (result.status !== 0) {
|
|
528
920
|
console.error(`Bridge failed: ${result.status ?? 1}`);
|
|
529
921
|
}
|
|
530
922
|
}
|
|
531
923
|
cronList(opts) {
|
|
532
|
-
const storePath =
|
|
924
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
533
925
|
const service = new CronService(storePath);
|
|
534
926
|
const jobs = service.listJobs(Boolean(opts.all));
|
|
535
927
|
if (!jobs.length) {
|
|
@@ -549,7 +941,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
549
941
|
}
|
|
550
942
|
}
|
|
551
943
|
cronAdd(opts) {
|
|
552
|
-
const storePath =
|
|
944
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
553
945
|
const service = new CronService(storePath);
|
|
554
946
|
let schedule = null;
|
|
555
947
|
if (opts.every) {
|
|
@@ -574,7 +966,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
574
966
|
console.log(`\u2713 Added job '${job.name}' (${job.id})`);
|
|
575
967
|
}
|
|
576
968
|
cronRemove(jobId) {
|
|
577
|
-
const storePath =
|
|
969
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
578
970
|
const service = new CronService(storePath);
|
|
579
971
|
if (service.removeJob(jobId)) {
|
|
580
972
|
console.log(`\u2713 Removed job ${jobId}`);
|
|
@@ -583,7 +975,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
583
975
|
}
|
|
584
976
|
}
|
|
585
977
|
cronEnable(jobId, opts) {
|
|
586
|
-
const storePath =
|
|
978
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
587
979
|
const service = new CronService(storePath);
|
|
588
980
|
const job = service.enableJob(jobId, !opts.disable);
|
|
589
981
|
if (job) {
|
|
@@ -593,7 +985,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
593
985
|
}
|
|
594
986
|
}
|
|
595
987
|
async cronRun(jobId, opts) {
|
|
596
|
-
const storePath =
|
|
988
|
+
const storePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
597
989
|
const service = new CronService(storePath);
|
|
598
990
|
const ok = await service.runJob(jobId, Boolean(opts.force));
|
|
599
991
|
console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
|
|
@@ -604,8 +996,8 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
604
996
|
const workspace = getWorkspacePath(config.agents.defaults.workspace);
|
|
605
997
|
console.log(`${this.logo} ${APP_NAME} Status
|
|
606
998
|
`);
|
|
607
|
-
console.log(`Config: ${configPath} ${
|
|
608
|
-
console.log(`Workspace: ${workspace} ${
|
|
999
|
+
console.log(`Config: ${configPath} ${existsSync3(configPath) ? "\u2713" : "\u2717"}`);
|
|
1000
|
+
console.log(`Workspace: ${workspace} ${existsSync3(workspace) ? "\u2713" : "\u2717"}`);
|
|
609
1001
|
console.log(`Model: ${config.agents.defaults.model}`);
|
|
610
1002
|
for (const spec of PROVIDERS) {
|
|
611
1003
|
const provider = config.providers[spec.name];
|
|
@@ -625,33 +1017,36 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
625
1017
|
const provider = options.allowMissingProvider === true ? this.makeProvider(config, { allowMissing: true }) : this.makeProvider(config);
|
|
626
1018
|
const providerManager = provider ? new ProviderManager(provider) : null;
|
|
627
1019
|
const sessionManager = new SessionManager(getWorkspacePath(config.agents.defaults.workspace));
|
|
628
|
-
const cronStorePath =
|
|
1020
|
+
const cronStorePath = join3(getDataDir2(), "cron", "jobs.json");
|
|
629
1021
|
const cron2 = new CronService(cronStorePath);
|
|
630
1022
|
const uiConfig = resolveUiConfig(config, options.uiOverrides);
|
|
631
1023
|
const uiStaticDir = options.uiStaticDir === void 0 ? resolveUiStaticDir() : options.uiStaticDir;
|
|
632
1024
|
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
|
-
}
|
|
1025
|
+
this.startUiIfEnabled(uiConfig, uiStaticDir);
|
|
649
1026
|
console.log("Warning: No API key configured. UI server only.");
|
|
650
1027
|
await new Promise(() => {
|
|
651
1028
|
});
|
|
652
1029
|
return;
|
|
653
1030
|
}
|
|
654
|
-
const
|
|
1031
|
+
const channels2 = new ChannelManager(config, bus, sessionManager);
|
|
1032
|
+
const reloader = new ConfigReloader({
|
|
1033
|
+
initialConfig: config,
|
|
1034
|
+
channels: channels2,
|
|
1035
|
+
bus,
|
|
1036
|
+
sessionManager,
|
|
1037
|
+
providerManager,
|
|
1038
|
+
makeProvider: (nextConfig) => this.makeProvider(nextConfig, { allowMissing: true }),
|
|
1039
|
+
loadConfig,
|
|
1040
|
+
onRestartRequired: (paths) => {
|
|
1041
|
+
console.warn(`Config changes require restart: ${paths.join(", ")}`);
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
const gatewayController = new GatewayControllerImpl({
|
|
1045
|
+
reloader,
|
|
1046
|
+
cron: cron2,
|
|
1047
|
+
getConfigPath,
|
|
1048
|
+
saveConfig
|
|
1049
|
+
});
|
|
655
1050
|
const agent = new AgentLoop({
|
|
656
1051
|
bus,
|
|
657
1052
|
providerManager: providerManager ?? new ProviderManager(provider),
|
|
@@ -690,385 +1085,12 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
690
1085
|
30 * 60,
|
|
691
1086
|
true
|
|
692
1087
|
);
|
|
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(", ")}`);
|
|
1088
|
+
if (reloader.getChannels().enabledChannels.length) {
|
|
1089
|
+
console.log(`\u2713 Channels enabled: ${reloader.getChannels().enabledChannels.join(", ")}`);
|
|
1053
1090
|
} else {
|
|
1054
1091
|
console.log("Warning: No channels enabled");
|
|
1055
1092
|
}
|
|
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
|
-
}
|
|
1093
|
+
this.startUiIfEnabled(uiConfig, uiStaticDir);
|
|
1072
1094
|
const cronStatus = cron2.status();
|
|
1073
1095
|
if (cronStatus.jobs > 0) {
|
|
1074
1096
|
console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
|
|
@@ -1079,12 +1101,31 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
1079
1101
|
ignoreInitial: true,
|
|
1080
1102
|
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
|
|
1081
1103
|
});
|
|
1082
|
-
watcher.on("add", () =>
|
|
1083
|
-
watcher.on("change", () =>
|
|
1084
|
-
watcher.on("unlink", () =>
|
|
1104
|
+
watcher.on("add", () => reloader.scheduleReload("config add"));
|
|
1105
|
+
watcher.on("change", () => reloader.scheduleReload("config change"));
|
|
1106
|
+
watcher.on("unlink", () => reloader.scheduleReload("config unlink"));
|
|
1085
1107
|
await cron2.start();
|
|
1086
1108
|
await heartbeat.start();
|
|
1087
|
-
await Promise.allSettled([agent.run(),
|
|
1109
|
+
await Promise.allSettled([agent.run(), reloader.getChannels().startAll()]);
|
|
1110
|
+
}
|
|
1111
|
+
startUiIfEnabled(uiConfig, uiStaticDir) {
|
|
1112
|
+
if (!uiConfig.enabled) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const uiServer = startUiServer({
|
|
1116
|
+
host: uiConfig.host,
|
|
1117
|
+
port: uiConfig.port,
|
|
1118
|
+
configPath: getConfigPath(),
|
|
1119
|
+
staticDir: uiStaticDir ?? void 0
|
|
1120
|
+
});
|
|
1121
|
+
const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
|
|
1122
|
+
console.log(`\u2713 UI API: ${uiUrl}/api`);
|
|
1123
|
+
if (uiStaticDir) {
|
|
1124
|
+
console.log(`\u2713 UI frontend: ${uiUrl}`);
|
|
1125
|
+
}
|
|
1126
|
+
if (uiConfig.open) {
|
|
1127
|
+
openBrowser(uiUrl);
|
|
1128
|
+
}
|
|
1088
1129
|
}
|
|
1089
1130
|
async runForeground(options) {
|
|
1090
1131
|
const config = loadConfig();
|
|
@@ -1141,7 +1182,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
1141
1182
|
console.log("Warning: UI frontend not found. Use --frontend to start the dev server.");
|
|
1142
1183
|
}
|
|
1143
1184
|
const logPath = resolveServiceLogPath();
|
|
1144
|
-
const logDir =
|
|
1185
|
+
const logDir = resolve3(logPath, "..");
|
|
1145
1186
|
mkdirSync2(logDir, { recursive: true });
|
|
1146
1187
|
const logFd = openSync(logPath, "a");
|
|
1147
1188
|
const serveArgs = buildServeArgs({
|
|
@@ -1250,65 +1291,115 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
1250
1291
|
{ source: "memory/MEMORY.md", target: "memory/MEMORY.md" }
|
|
1251
1292
|
];
|
|
1252
1293
|
for (const entry of templateFiles) {
|
|
1253
|
-
const filePath =
|
|
1254
|
-
if (!force &&
|
|
1294
|
+
const filePath = join3(workspace, entry.target);
|
|
1295
|
+
if (!force && existsSync3(filePath)) {
|
|
1255
1296
|
continue;
|
|
1256
1297
|
}
|
|
1257
|
-
const templatePath =
|
|
1258
|
-
if (!
|
|
1298
|
+
const templatePath = join3(templateDir, entry.source);
|
|
1299
|
+
if (!existsSync3(templatePath)) {
|
|
1259
1300
|
console.warn(`Warning: Template file missing: ${templatePath}`);
|
|
1260
1301
|
continue;
|
|
1261
1302
|
}
|
|
1262
|
-
const raw =
|
|
1303
|
+
const raw = readFileSync3(templatePath, "utf-8");
|
|
1263
1304
|
const content = raw.replace(/\$\{APP_NAME\}/g, APP_NAME);
|
|
1264
1305
|
mkdirSync2(dirname(filePath), { recursive: true });
|
|
1265
1306
|
writeFileSync2(filePath, content);
|
|
1266
1307
|
created.push(entry.target);
|
|
1267
1308
|
}
|
|
1268
|
-
const memoryDir =
|
|
1269
|
-
if (!
|
|
1309
|
+
const memoryDir = join3(workspace, "memory");
|
|
1310
|
+
if (!existsSync3(memoryDir)) {
|
|
1270
1311
|
mkdirSync2(memoryDir, { recursive: true });
|
|
1271
|
-
created.push(
|
|
1312
|
+
created.push(join3("memory", ""));
|
|
1272
1313
|
}
|
|
1273
|
-
const skillsDir =
|
|
1274
|
-
if (!
|
|
1314
|
+
const skillsDir = join3(workspace, "skills");
|
|
1315
|
+
if (!existsSync3(skillsDir)) {
|
|
1275
1316
|
mkdirSync2(skillsDir, { recursive: true });
|
|
1276
|
-
created.push(
|
|
1317
|
+
created.push(join3("skills", ""));
|
|
1318
|
+
}
|
|
1319
|
+
const seeded = this.seedBuiltinSkills(skillsDir, { force });
|
|
1320
|
+
if (seeded > 0) {
|
|
1321
|
+
created.push(`skills (seeded ${seeded} built-ins)`);
|
|
1277
1322
|
}
|
|
1278
1323
|
return { created };
|
|
1279
1324
|
}
|
|
1325
|
+
seedBuiltinSkills(targetDir, options = {}) {
|
|
1326
|
+
const sourceDir = this.resolveBuiltinSkillsDir();
|
|
1327
|
+
if (!sourceDir) {
|
|
1328
|
+
return 0;
|
|
1329
|
+
}
|
|
1330
|
+
const force = Boolean(options.force);
|
|
1331
|
+
const existing = readdirSync(targetDir, { withFileTypes: true }).filter((entry) => !entry.name.startsWith("."));
|
|
1332
|
+
if (!force && existing.length > 0) {
|
|
1333
|
+
return 0;
|
|
1334
|
+
}
|
|
1335
|
+
let seeded = 0;
|
|
1336
|
+
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
1337
|
+
if (!entry.isDirectory()) {
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
const src = join3(sourceDir, entry.name);
|
|
1341
|
+
if (!existsSync3(join3(src, "SKILL.md"))) {
|
|
1342
|
+
continue;
|
|
1343
|
+
}
|
|
1344
|
+
const dest = join3(targetDir, entry.name);
|
|
1345
|
+
if (!force && existsSync3(dest)) {
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
cpSync(src, dest, { recursive: true, force: true });
|
|
1349
|
+
seeded += 1;
|
|
1350
|
+
}
|
|
1351
|
+
return seeded;
|
|
1352
|
+
}
|
|
1353
|
+
resolveBuiltinSkillsDir() {
|
|
1354
|
+
try {
|
|
1355
|
+
const require2 = createRequire(import.meta.url);
|
|
1356
|
+
const entry = require2.resolve("nextclaw-core");
|
|
1357
|
+
const pkgRoot = resolve3(dirname(entry), "..");
|
|
1358
|
+
const distSkills = join3(pkgRoot, "dist", "skills");
|
|
1359
|
+
if (existsSync3(distSkills)) {
|
|
1360
|
+
return distSkills;
|
|
1361
|
+
}
|
|
1362
|
+
const srcSkills = join3(pkgRoot, "src", "agent", "skills");
|
|
1363
|
+
if (existsSync3(srcSkills)) {
|
|
1364
|
+
return srcSkills;
|
|
1365
|
+
}
|
|
1366
|
+
return null;
|
|
1367
|
+
} catch {
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1280
1371
|
resolveTemplateDir() {
|
|
1281
1372
|
const override = process.env.NEXTCLAW_TEMPLATE_DIR?.trim();
|
|
1282
1373
|
if (override) {
|
|
1283
1374
|
return override;
|
|
1284
1375
|
}
|
|
1285
|
-
const cliDir =
|
|
1286
|
-
const pkgRoot =
|
|
1287
|
-
const candidates = [
|
|
1376
|
+
const cliDir = resolve3(fileURLToPath3(new URL(".", import.meta.url)));
|
|
1377
|
+
const pkgRoot = resolve3(cliDir, "..", "..");
|
|
1378
|
+
const candidates = [join3(pkgRoot, "templates")];
|
|
1288
1379
|
for (const candidate of candidates) {
|
|
1289
|
-
if (
|
|
1380
|
+
if (existsSync3(candidate)) {
|
|
1290
1381
|
return candidate;
|
|
1291
1382
|
}
|
|
1292
1383
|
}
|
|
1293
1384
|
return null;
|
|
1294
1385
|
}
|
|
1295
1386
|
getBridgeDir() {
|
|
1296
|
-
const userBridge =
|
|
1297
|
-
if (
|
|
1387
|
+
const userBridge = join3(getDataDir2(), "bridge");
|
|
1388
|
+
if (existsSync3(join3(userBridge, "dist", "index.js"))) {
|
|
1298
1389
|
return userBridge;
|
|
1299
1390
|
}
|
|
1300
1391
|
if (!which("npm")) {
|
|
1301
1392
|
console.error("npm not found. Please install Node.js >= 18.");
|
|
1302
1393
|
process.exit(1);
|
|
1303
1394
|
}
|
|
1304
|
-
const cliDir =
|
|
1305
|
-
const pkgRoot =
|
|
1306
|
-
const pkgBridge =
|
|
1307
|
-
const srcBridge =
|
|
1395
|
+
const cliDir = resolve3(fileURLToPath3(new URL(".", import.meta.url)));
|
|
1396
|
+
const pkgRoot = resolve3(cliDir, "..", "..");
|
|
1397
|
+
const pkgBridge = join3(pkgRoot, "bridge");
|
|
1398
|
+
const srcBridge = join3(pkgRoot, "..", "..", "bridge");
|
|
1308
1399
|
let source = null;
|
|
1309
|
-
if (
|
|
1400
|
+
if (existsSync3(join3(pkgBridge, "package.json"))) {
|
|
1310
1401
|
source = pkgBridge;
|
|
1311
|
-
} else if (
|
|
1402
|
+
} else if (existsSync3(join3(srcBridge, "package.json"))) {
|
|
1312
1403
|
source = srcBridge;
|
|
1313
1404
|
}
|
|
1314
1405
|
if (!source) {
|
|
@@ -1316,15 +1407,15 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
1316
1407
|
process.exit(1);
|
|
1317
1408
|
}
|
|
1318
1409
|
console.log(`${this.logo} Setting up bridge...`);
|
|
1319
|
-
mkdirSync2(
|
|
1320
|
-
if (
|
|
1410
|
+
mkdirSync2(resolve3(userBridge, ".."), { recursive: true });
|
|
1411
|
+
if (existsSync3(userBridge)) {
|
|
1321
1412
|
rmSync2(userBridge, { recursive: true, force: true });
|
|
1322
1413
|
}
|
|
1323
1414
|
cpSync(source, userBridge, {
|
|
1324
1415
|
recursive: true,
|
|
1325
1416
|
filter: (src) => !src.includes("node_modules") && !src.includes("dist")
|
|
1326
1417
|
});
|
|
1327
|
-
const install =
|
|
1418
|
+
const install = spawnSync2("npm", ["install"], { cwd: userBridge, stdio: "pipe" });
|
|
1328
1419
|
if (install.status !== 0) {
|
|
1329
1420
|
console.error(`Bridge install failed: ${install.status ?? 1}`);
|
|
1330
1421
|
if (install.stderr) {
|
|
@@ -1332,7 +1423,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
|
|
|
1332
1423
|
}
|
|
1333
1424
|
process.exit(1);
|
|
1334
1425
|
}
|
|
1335
|
-
const build =
|
|
1426
|
+
const build = spawnSync2("npm", ["run", "build"], { cwd: userBridge, stdio: "pipe" });
|
|
1336
1427
|
if (build.status !== 0) {
|
|
1337
1428
|
console.error(`Bridge build failed: ${build.status ?? 1}`);
|
|
1338
1429
|
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.2",
|
|
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.2",
|
|
42
|
+
"nextclaw-server": "^0.3.2"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^20.17.6",
|