nextclaw 0.6.11 → 0.6.13
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 +217 -224
- package/package.json +2 -2
package/dist/cli/index.js
CHANGED
|
@@ -84,24 +84,120 @@ var RestartCoordinator = class {
|
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
+
// src/cli/restart-sentinel.ts
|
|
88
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
89
|
+
import { resolve } from "path";
|
|
90
|
+
import { getDataDir } from "@nextclaw/core";
|
|
91
|
+
var RESTART_SENTINEL_FILENAME = "restart-sentinel.json";
|
|
92
|
+
var RESTART_REASON_MAX_CHARS = 240;
|
|
93
|
+
var RESTART_NOTE_MAX_CHARS = 600;
|
|
94
|
+
var RESTART_OUTBOUND_MAX_CHARS = 1200;
|
|
95
|
+
function trimTo(value, maxChars) {
|
|
96
|
+
const text = value.trim();
|
|
97
|
+
if (!text) {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
if (text.length <= maxChars) {
|
|
101
|
+
return text;
|
|
102
|
+
}
|
|
103
|
+
return `${text.slice(0, Math.max(0, maxChars - 1)).trimEnd()}\u2026`;
|
|
104
|
+
}
|
|
105
|
+
function normalizeLine(value, maxChars) {
|
|
106
|
+
const trimmed = trimTo(value, maxChars);
|
|
107
|
+
return trimmed ? trimmed : null;
|
|
108
|
+
}
|
|
109
|
+
function resolveRestartSentinelPath() {
|
|
110
|
+
return resolve(getDataDir(), "run", RESTART_SENTINEL_FILENAME);
|
|
111
|
+
}
|
|
112
|
+
async function writeRestartSentinel(payload) {
|
|
113
|
+
const path = resolveRestartSentinelPath();
|
|
114
|
+
mkdirSync(resolve(path, ".."), { recursive: true });
|
|
115
|
+
const file = {
|
|
116
|
+
version: 1,
|
|
117
|
+
payload
|
|
118
|
+
};
|
|
119
|
+
writeFileSync(path, `${JSON.stringify(file, null, 2)}
|
|
120
|
+
`, "utf-8");
|
|
121
|
+
return path;
|
|
122
|
+
}
|
|
123
|
+
async function consumeRestartSentinel() {
|
|
124
|
+
const path = resolveRestartSentinelPath();
|
|
125
|
+
if (!existsSync(path)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const raw = readFileSync(path, "utf-8");
|
|
130
|
+
const parsed = JSON.parse(raw);
|
|
131
|
+
if (!parsed || parsed.version !== 1 || !parsed.payload) {
|
|
132
|
+
rmSync(path, { force: true });
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
rmSync(path, { force: true });
|
|
136
|
+
return parsed;
|
|
137
|
+
} catch {
|
|
138
|
+
rmSync(path, { force: true });
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function summarizeRestartSentinel(payload) {
|
|
143
|
+
const reason = normalizeLine(payload.stats?.reason ?? "", RESTART_REASON_MAX_CHARS);
|
|
144
|
+
if (payload.kind === "update.run") {
|
|
145
|
+
return payload.status === "ok" ? "\u2705 NextClaw update completed and service restarted." : "\u26A0\uFE0F NextClaw update finished with issues.";
|
|
146
|
+
}
|
|
147
|
+
if (payload.kind === "config.apply" || payload.kind === "config.patch") {
|
|
148
|
+
return payload.status === "ok" ? "\u2705 Config applied and service restarted." : "\u26A0\uFE0F Config change restart finished with issues.";
|
|
149
|
+
}
|
|
150
|
+
if (reason) {
|
|
151
|
+
return `Gateway restart complete (${reason}).`;
|
|
152
|
+
}
|
|
153
|
+
return "Gateway restart complete.";
|
|
154
|
+
}
|
|
155
|
+
function formatRestartSentinelMessage(payload) {
|
|
156
|
+
const lines = [summarizeRestartSentinel(payload)];
|
|
157
|
+
const note = normalizeLine(payload.message ?? "", RESTART_NOTE_MAX_CHARS);
|
|
158
|
+
if (note) {
|
|
159
|
+
lines.push(note);
|
|
160
|
+
}
|
|
161
|
+
const reason = normalizeLine(payload.stats?.reason ?? "", RESTART_REASON_MAX_CHARS);
|
|
162
|
+
if (reason && !lines.some((line) => line.includes(reason))) {
|
|
163
|
+
lines.push(`Reason: ${reason}`);
|
|
164
|
+
}
|
|
165
|
+
const message = lines.join("\n").trim();
|
|
166
|
+
return trimTo(message, RESTART_OUTBOUND_MAX_CHARS);
|
|
167
|
+
}
|
|
168
|
+
function parseSessionKey(sessionKey) {
|
|
169
|
+
const value = sessionKey?.trim();
|
|
170
|
+
if (!value) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
const separator = value.indexOf(":");
|
|
174
|
+
if (separator <= 0 || separator >= value.length - 1) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
channel: value.slice(0, separator),
|
|
179
|
+
chatId: value.slice(separator + 1)
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
87
183
|
// src/cli/skills/clawhub.ts
|
|
88
184
|
import { spawnSync } from "child_process";
|
|
89
|
-
import { existsSync } from "fs";
|
|
90
|
-
import { isAbsolute, join, resolve } from "path";
|
|
185
|
+
import { existsSync as existsSync2 } from "fs";
|
|
186
|
+
import { isAbsolute, join, resolve as resolve2 } from "path";
|
|
91
187
|
async function installClawHubSkill(options) {
|
|
92
188
|
const slug = options.slug.trim();
|
|
93
189
|
if (!slug) {
|
|
94
190
|
throw new Error("Skill slug is required.");
|
|
95
191
|
}
|
|
96
|
-
const workdir =
|
|
97
|
-
if (!
|
|
192
|
+
const workdir = resolve2(options.workdir);
|
|
193
|
+
if (!existsSync2(workdir)) {
|
|
98
194
|
throw new Error(`Workdir does not exist: ${workdir}`);
|
|
99
195
|
}
|
|
100
196
|
const dirName = options.dir?.trim() || "skills";
|
|
101
|
-
const destinationDir = isAbsolute(dirName) ?
|
|
197
|
+
const destinationDir = isAbsolute(dirName) ? resolve2(dirName, slug) : resolve2(workdir, dirName, slug);
|
|
102
198
|
const skillFile = join(destinationDir, "SKILL.md");
|
|
103
|
-
if (!options.force &&
|
|
104
|
-
if (
|
|
199
|
+
if (!options.force && existsSync2(destinationDir)) {
|
|
200
|
+
if (existsSync2(skillFile)) {
|
|
105
201
|
return {
|
|
106
202
|
slug,
|
|
107
203
|
version: options.version,
|
|
@@ -156,15 +252,15 @@ function buildClawHubArgs(slug, options) {
|
|
|
156
252
|
|
|
157
253
|
// src/cli/update/runner.ts
|
|
158
254
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
159
|
-
import { resolve as
|
|
255
|
+
import { resolve as resolve4 } from "path";
|
|
160
256
|
|
|
161
257
|
// src/cli/utils.ts
|
|
162
|
-
import { existsSync as
|
|
163
|
-
import { join as join2, resolve as
|
|
258
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
|
|
259
|
+
import { join as join2, resolve as resolve3 } from "path";
|
|
164
260
|
import { spawn } from "child_process";
|
|
165
261
|
import { isIP } from "net";
|
|
166
262
|
import { fileURLToPath } from "url";
|
|
167
|
-
import { getDataDir, getPackageVersion as getCorePackageVersion } from "@nextclaw/core";
|
|
263
|
+
import { getDataDir as getDataDir2, getPackageVersion as getCorePackageVersion } from "@nextclaw/core";
|
|
168
264
|
function resolveUiConfig(config2, overrides) {
|
|
169
265
|
const base = config2.ui ?? { enabled: false, host: "127.0.0.1", port: 18791, open: false };
|
|
170
266
|
return { ...base, ...overrides ?? {} };
|
|
@@ -214,11 +310,11 @@ function buildServeArgs(options) {
|
|
|
214
310
|
}
|
|
215
311
|
function readServiceState() {
|
|
216
312
|
const path = resolveServiceStatePath();
|
|
217
|
-
if (!
|
|
313
|
+
if (!existsSync3(path)) {
|
|
218
314
|
return null;
|
|
219
315
|
}
|
|
220
316
|
try {
|
|
221
|
-
const raw =
|
|
317
|
+
const raw = readFileSync2(path, "utf-8");
|
|
222
318
|
return JSON.parse(raw);
|
|
223
319
|
} catch {
|
|
224
320
|
return null;
|
|
@@ -226,20 +322,20 @@ function readServiceState() {
|
|
|
226
322
|
}
|
|
227
323
|
function writeServiceState(state) {
|
|
228
324
|
const path = resolveServiceStatePath();
|
|
229
|
-
|
|
230
|
-
|
|
325
|
+
mkdirSync2(resolve3(path, ".."), { recursive: true });
|
|
326
|
+
writeFileSync2(path, JSON.stringify(state, null, 2));
|
|
231
327
|
}
|
|
232
328
|
function clearServiceState() {
|
|
233
329
|
const path = resolveServiceStatePath();
|
|
234
|
-
if (
|
|
235
|
-
|
|
330
|
+
if (existsSync3(path)) {
|
|
331
|
+
rmSync2(path, { force: true });
|
|
236
332
|
}
|
|
237
333
|
}
|
|
238
334
|
function resolveServiceStatePath() {
|
|
239
|
-
return
|
|
335
|
+
return resolve3(getDataDir2(), "run", "service.json");
|
|
240
336
|
}
|
|
241
337
|
function resolveServiceLogPath() {
|
|
242
|
-
return
|
|
338
|
+
return resolve3(getDataDir2(), "logs", "service.log");
|
|
243
339
|
}
|
|
244
340
|
function isProcessRunning(pid) {
|
|
245
341
|
try {
|
|
@@ -265,8 +361,8 @@ function resolveUiStaticDir() {
|
|
|
265
361
|
if (envDir) {
|
|
266
362
|
candidates.push(envDir);
|
|
267
363
|
}
|
|
268
|
-
const cliDir =
|
|
269
|
-
const pkgRoot =
|
|
364
|
+
const cliDir = resolve3(fileURLToPath(new URL(".", import.meta.url)));
|
|
365
|
+
const pkgRoot = resolve3(cliDir, "..", "..");
|
|
270
366
|
candidates.push(join2(pkgRoot, "ui-dist"));
|
|
271
367
|
candidates.push(join2(pkgRoot, "ui"));
|
|
272
368
|
candidates.push(join2(pkgRoot, "..", "ui-dist"));
|
|
@@ -278,7 +374,7 @@ function resolveUiStaticDir() {
|
|
|
278
374
|
candidates.push(join2(pkgRoot, "..", "..", "packages", "nextclaw-ui", "dist"));
|
|
279
375
|
candidates.push(join2(pkgRoot, "..", "..", "nextclaw-ui", "dist"));
|
|
280
376
|
for (const dir of candidates) {
|
|
281
|
-
if (
|
|
377
|
+
if (existsSync3(join2(dir, "index.html"))) {
|
|
282
378
|
return dir;
|
|
283
379
|
}
|
|
284
380
|
}
|
|
@@ -305,19 +401,19 @@ function which(binary) {
|
|
|
305
401
|
const paths = (process.env.PATH ?? "").split(":");
|
|
306
402
|
for (const dir of paths) {
|
|
307
403
|
const full = join2(dir, binary);
|
|
308
|
-
if (
|
|
404
|
+
if (existsSync3(full)) {
|
|
309
405
|
return true;
|
|
310
406
|
}
|
|
311
407
|
}
|
|
312
408
|
return false;
|
|
313
409
|
}
|
|
314
410
|
function resolveVersionFromPackageTree(startDir, expectedName) {
|
|
315
|
-
let current =
|
|
411
|
+
let current = resolve3(startDir);
|
|
316
412
|
while (current.length > 0) {
|
|
317
413
|
const pkgPath = join2(current, "package.json");
|
|
318
|
-
if (
|
|
414
|
+
if (existsSync3(pkgPath)) {
|
|
319
415
|
try {
|
|
320
|
-
const raw =
|
|
416
|
+
const raw = readFileSync2(pkgPath, "utf-8");
|
|
321
417
|
const parsed = JSON.parse(raw);
|
|
322
418
|
if (typeof parsed.version === "string") {
|
|
323
419
|
if (!expectedName || parsed.name === expectedName) {
|
|
@@ -327,7 +423,7 @@ function resolveVersionFromPackageTree(startDir, expectedName) {
|
|
|
327
423
|
} catch {
|
|
328
424
|
}
|
|
329
425
|
}
|
|
330
|
-
const parent =
|
|
426
|
+
const parent = resolve3(current, "..");
|
|
331
427
|
if (parent === current) {
|
|
332
428
|
break;
|
|
333
429
|
}
|
|
@@ -336,7 +432,7 @@ function resolveVersionFromPackageTree(startDir, expectedName) {
|
|
|
336
432
|
return null;
|
|
337
433
|
}
|
|
338
434
|
function getPackageVersion() {
|
|
339
|
-
const cliDir =
|
|
435
|
+
const cliDir = resolve3(fileURLToPath(new URL(".", import.meta.url)));
|
|
340
436
|
return resolveVersionFromPackageTree(cliDir, "nextclaw") ?? resolveVersionFromPackageTree(cliDir) ?? getCorePackageVersion();
|
|
341
437
|
}
|
|
342
438
|
function printAgentResponse(response) {
|
|
@@ -375,7 +471,7 @@ function runSelfUpdate(options = {}) {
|
|
|
375
471
|
return { ok: result.status === 0, code: result.status };
|
|
376
472
|
};
|
|
377
473
|
if (updateCommand) {
|
|
378
|
-
const cwd = options.cwd ?
|
|
474
|
+
const cwd = options.cwd ? resolve4(options.cwd) : process.cwd();
|
|
379
475
|
const ok = runStep("sh", ["-c", updateCommand], cwd);
|
|
380
476
|
if (!ok.ok) {
|
|
381
477
|
return { ok: false, error: "update command failed", strategy: "command", steps };
|
|
@@ -413,8 +509,8 @@ import {
|
|
|
413
509
|
expandHome
|
|
414
510
|
} from "@nextclaw/core";
|
|
415
511
|
import { createInterface } from "readline";
|
|
416
|
-
import { existsSync as
|
|
417
|
-
import { resolve as
|
|
512
|
+
import { existsSync as existsSync4 } from "fs";
|
|
513
|
+
import { resolve as resolve5 } from "path";
|
|
418
514
|
function loadPluginRegistry(config2, workspaceDir) {
|
|
419
515
|
return loadOpenClawPlugins({
|
|
420
516
|
config: config2,
|
|
@@ -691,7 +787,7 @@ var PluginCommands = class {
|
|
|
691
787
|
process.exit(1);
|
|
692
788
|
}
|
|
693
789
|
const install = config2.plugins.installs?.[pluginId];
|
|
694
|
-
const isLinked = install?.source === "path" && (!install.installPath || !install.sourcePath ||
|
|
790
|
+
const isLinked = install?.source === "path" && (!install.installPath || !install.sourcePath || resolve5(install.installPath) === resolve5(install.sourcePath));
|
|
695
791
|
const preview = [];
|
|
696
792
|
if (hasEntry) {
|
|
697
793
|
preview.push("config entry");
|
|
@@ -770,9 +866,9 @@ var PluginCommands = class {
|
|
|
770
866
|
process.exit(1);
|
|
771
867
|
}
|
|
772
868
|
const normalized = fileSpec && fileSpec.ok ? fileSpec.path : pathOrSpec;
|
|
773
|
-
const resolved =
|
|
869
|
+
const resolved = resolve5(expandHome(normalized));
|
|
774
870
|
const config2 = loadConfig();
|
|
775
|
-
if (
|
|
871
|
+
if (existsSync4(resolved)) {
|
|
776
872
|
if (opts.link) {
|
|
777
873
|
const probe = await installPluginFromPath({ path: resolved, dryRun: true });
|
|
778
874
|
if (!probe.ok) {
|
|
@@ -1331,11 +1427,11 @@ var ChannelCommands = class {
|
|
|
1331
1427
|
};
|
|
1332
1428
|
|
|
1333
1429
|
// src/cli/commands/cron.ts
|
|
1334
|
-
import { CronService, getDataDir as
|
|
1430
|
+
import { CronService, getDataDir as getDataDir3 } from "@nextclaw/core";
|
|
1335
1431
|
import { join as join3 } from "path";
|
|
1336
1432
|
var CronCommands = class {
|
|
1337
1433
|
cronList(opts) {
|
|
1338
|
-
const storePath = join3(
|
|
1434
|
+
const storePath = join3(getDataDir3(), "cron", "jobs.json");
|
|
1339
1435
|
const service = new CronService(storePath);
|
|
1340
1436
|
const jobs = service.listJobs(Boolean(opts.all));
|
|
1341
1437
|
if (!jobs.length) {
|
|
@@ -1355,7 +1451,7 @@ var CronCommands = class {
|
|
|
1355
1451
|
}
|
|
1356
1452
|
}
|
|
1357
1453
|
cronAdd(opts) {
|
|
1358
|
-
const storePath = join3(
|
|
1454
|
+
const storePath = join3(getDataDir3(), "cron", "jobs.json");
|
|
1359
1455
|
const service = new CronService(storePath);
|
|
1360
1456
|
let schedule = null;
|
|
1361
1457
|
if (opts.every) {
|
|
@@ -1380,7 +1476,7 @@ var CronCommands = class {
|
|
|
1380
1476
|
console.log(`\u2713 Added job '${job.name}' (${job.id})`);
|
|
1381
1477
|
}
|
|
1382
1478
|
cronRemove(jobId) {
|
|
1383
|
-
const storePath = join3(
|
|
1479
|
+
const storePath = join3(getDataDir3(), "cron", "jobs.json");
|
|
1384
1480
|
const service = new CronService(storePath);
|
|
1385
1481
|
if (service.removeJob(jobId)) {
|
|
1386
1482
|
console.log(`\u2713 Removed job ${jobId}`);
|
|
@@ -1389,7 +1485,7 @@ var CronCommands = class {
|
|
|
1389
1485
|
}
|
|
1390
1486
|
}
|
|
1391
1487
|
cronEnable(jobId, opts) {
|
|
1392
|
-
const storePath = join3(
|
|
1488
|
+
const storePath = join3(getDataDir3(), "cron", "jobs.json");
|
|
1393
1489
|
const service = new CronService(storePath);
|
|
1394
1490
|
const job = service.enableJob(jobId, !opts.disable);
|
|
1395
1491
|
if (job) {
|
|
@@ -1399,7 +1495,7 @@ var CronCommands = class {
|
|
|
1399
1495
|
}
|
|
1400
1496
|
}
|
|
1401
1497
|
async cronRun(jobId, opts) {
|
|
1402
|
-
const storePath = join3(
|
|
1498
|
+
const storePath = join3(getDataDir3(), "cron", "jobs.json");
|
|
1403
1499
|
const service = new CronService(storePath);
|
|
1404
1500
|
const ok = await service.runJob(jobId, Boolean(opts.force));
|
|
1405
1501
|
console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
|
|
@@ -1408,12 +1504,12 @@ var CronCommands = class {
|
|
|
1408
1504
|
|
|
1409
1505
|
// src/cli/commands/diagnostics.ts
|
|
1410
1506
|
import { createServer as createNetServer } from "net";
|
|
1411
|
-
import { existsSync as
|
|
1412
|
-
import { resolve as
|
|
1507
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
1508
|
+
import { resolve as resolve6 } from "path";
|
|
1413
1509
|
import {
|
|
1414
1510
|
APP_NAME,
|
|
1415
1511
|
getConfigPath,
|
|
1416
|
-
getDataDir as
|
|
1512
|
+
getDataDir as getDataDir4,
|
|
1417
1513
|
getWorkspacePath as getWorkspacePath3,
|
|
1418
1514
|
loadConfig as loadConfig4,
|
|
1419
1515
|
PROVIDERS as PROVIDERS3
|
|
@@ -1579,7 +1675,7 @@ var DiagnosticsCommands = class {
|
|
|
1579
1675
|
const configPath = getConfigPath();
|
|
1580
1676
|
const config2 = loadConfig4();
|
|
1581
1677
|
const workspacePath = getWorkspacePath3(config2.agents.defaults.workspace);
|
|
1582
|
-
const serviceStatePath =
|
|
1678
|
+
const serviceStatePath = resolve6(getDataDir4(), "run", "service.json");
|
|
1583
1679
|
const fixActions = [];
|
|
1584
1680
|
let serviceState = readServiceState();
|
|
1585
1681
|
if (params.fix && serviceState && !isProcessRunning(serviceState.pid)) {
|
|
@@ -1618,11 +1714,11 @@ var DiagnosticsCommands = class {
|
|
|
1618
1714
|
});
|
|
1619
1715
|
const issues = [];
|
|
1620
1716
|
const recommendations = [];
|
|
1621
|
-
if (!
|
|
1717
|
+
if (!existsSync5(configPath)) {
|
|
1622
1718
|
issues.push("Config file is missing.");
|
|
1623
1719
|
recommendations.push(`Run ${APP_NAME} init to create config files.`);
|
|
1624
1720
|
}
|
|
1625
|
-
if (!
|
|
1721
|
+
if (!existsSync5(workspacePath)) {
|
|
1626
1722
|
issues.push("Workspace directory does not exist.");
|
|
1627
1723
|
recommendations.push(`Run ${APP_NAME} init to create workspace templates.`);
|
|
1628
1724
|
}
|
|
@@ -1650,13 +1746,13 @@ var DiagnosticsCommands = class {
|
|
|
1650
1746
|
return {
|
|
1651
1747
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1652
1748
|
configPath,
|
|
1653
|
-
configExists:
|
|
1749
|
+
configExists: existsSync5(configPath),
|
|
1654
1750
|
workspacePath,
|
|
1655
|
-
workspaceExists:
|
|
1751
|
+
workspaceExists: existsSync5(workspacePath),
|
|
1656
1752
|
model: config2.agents.defaults.model,
|
|
1657
1753
|
providers,
|
|
1658
1754
|
serviceStatePath,
|
|
1659
|
-
serviceStateExists:
|
|
1755
|
+
serviceStateExists: existsSync5(serviceStatePath),
|
|
1660
1756
|
fixActions,
|
|
1661
1757
|
process: {
|
|
1662
1758
|
managedByState,
|
|
@@ -1706,11 +1802,11 @@ var DiagnosticsCommands = class {
|
|
|
1706
1802
|
}
|
|
1707
1803
|
}
|
|
1708
1804
|
readLogTail(path, maxLines = 25) {
|
|
1709
|
-
if (!
|
|
1805
|
+
if (!existsSync5(path)) {
|
|
1710
1806
|
return [];
|
|
1711
1807
|
}
|
|
1712
1808
|
try {
|
|
1713
|
-
const lines =
|
|
1809
|
+
const lines = readFileSync3(path, "utf-8").split(/\r?\n/).filter(Boolean);
|
|
1714
1810
|
if (lines.length <= maxLines) {
|
|
1715
1811
|
return lines;
|
|
1716
1812
|
}
|
|
@@ -1781,124 +1877,6 @@ import {
|
|
|
1781
1877
|
ConfigSchema,
|
|
1782
1878
|
redactConfigObject
|
|
1783
1879
|
} from "@nextclaw/core";
|
|
1784
|
-
|
|
1785
|
-
// src/cli/restart-sentinel.ts
|
|
1786
|
-
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1787
|
-
import { resolve as resolve6 } from "path";
|
|
1788
|
-
import { getDataDir as getDataDir4 } from "@nextclaw/core";
|
|
1789
|
-
var RESTART_SENTINEL_FILENAME = "restart-sentinel.json";
|
|
1790
|
-
var PENDING_SYSTEM_EVENTS_KEY = "pending_system_events";
|
|
1791
|
-
var RESTART_REASON_MAX_CHARS = 240;
|
|
1792
|
-
var RESTART_NOTE_MAX_CHARS = 600;
|
|
1793
|
-
var RESTART_OUTBOUND_MAX_CHARS = 1200;
|
|
1794
|
-
function trimTo(value, maxChars) {
|
|
1795
|
-
const text = value.trim();
|
|
1796
|
-
if (!text) {
|
|
1797
|
-
return "";
|
|
1798
|
-
}
|
|
1799
|
-
if (text.length <= maxChars) {
|
|
1800
|
-
return text;
|
|
1801
|
-
}
|
|
1802
|
-
return `${text.slice(0, Math.max(0, maxChars - 1)).trimEnd()}\u2026`;
|
|
1803
|
-
}
|
|
1804
|
-
function normalizeLine(value, maxChars) {
|
|
1805
|
-
const trimmed = trimTo(value, maxChars);
|
|
1806
|
-
return trimmed ? trimmed : null;
|
|
1807
|
-
}
|
|
1808
|
-
function resolveRestartSentinelPath() {
|
|
1809
|
-
return resolve6(getDataDir4(), "run", RESTART_SENTINEL_FILENAME);
|
|
1810
|
-
}
|
|
1811
|
-
async function writeRestartSentinel(payload) {
|
|
1812
|
-
const path = resolveRestartSentinelPath();
|
|
1813
|
-
mkdirSync2(resolve6(path, ".."), { recursive: true });
|
|
1814
|
-
const file = {
|
|
1815
|
-
version: 1,
|
|
1816
|
-
payload
|
|
1817
|
-
};
|
|
1818
|
-
writeFileSync2(path, `${JSON.stringify(file, null, 2)}
|
|
1819
|
-
`, "utf-8");
|
|
1820
|
-
return path;
|
|
1821
|
-
}
|
|
1822
|
-
async function consumeRestartSentinel() {
|
|
1823
|
-
const path = resolveRestartSentinelPath();
|
|
1824
|
-
if (!existsSync5(path)) {
|
|
1825
|
-
return null;
|
|
1826
|
-
}
|
|
1827
|
-
try {
|
|
1828
|
-
const raw = readFileSync3(path, "utf-8");
|
|
1829
|
-
const parsed = JSON.parse(raw);
|
|
1830
|
-
if (!parsed || parsed.version !== 1 || !parsed.payload) {
|
|
1831
|
-
rmSync2(path, { force: true });
|
|
1832
|
-
return null;
|
|
1833
|
-
}
|
|
1834
|
-
rmSync2(path, { force: true });
|
|
1835
|
-
return parsed;
|
|
1836
|
-
} catch {
|
|
1837
|
-
rmSync2(path, { force: true });
|
|
1838
|
-
return null;
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
function summarizeRestartSentinel(payload) {
|
|
1842
|
-
const reason = normalizeLine(payload.stats?.reason ?? "", RESTART_REASON_MAX_CHARS);
|
|
1843
|
-
if (payload.kind === "update.run") {
|
|
1844
|
-
return payload.status === "ok" ? "\u2705 NextClaw update completed and service restarted." : "\u26A0\uFE0F NextClaw update finished with issues.";
|
|
1845
|
-
}
|
|
1846
|
-
if (payload.kind === "config.apply" || payload.kind === "config.patch") {
|
|
1847
|
-
return payload.status === "ok" ? "\u2705 Config applied and service restarted." : "\u26A0\uFE0F Config change restart finished with issues.";
|
|
1848
|
-
}
|
|
1849
|
-
if (reason) {
|
|
1850
|
-
return `Gateway restart complete (${reason}).`;
|
|
1851
|
-
}
|
|
1852
|
-
return "Gateway restart complete.";
|
|
1853
|
-
}
|
|
1854
|
-
function formatRestartSentinelMessage(payload) {
|
|
1855
|
-
const lines = [summarizeRestartSentinel(payload)];
|
|
1856
|
-
const note = normalizeLine(payload.message ?? "", RESTART_NOTE_MAX_CHARS);
|
|
1857
|
-
if (note) {
|
|
1858
|
-
lines.push(note);
|
|
1859
|
-
}
|
|
1860
|
-
const reason = normalizeLine(payload.stats?.reason ?? "", RESTART_REASON_MAX_CHARS);
|
|
1861
|
-
if (reason && !lines.some((line) => line.includes(reason))) {
|
|
1862
|
-
lines.push(`Reason: ${reason}`);
|
|
1863
|
-
}
|
|
1864
|
-
const message = lines.join("\n").trim();
|
|
1865
|
-
return trimTo(message, RESTART_OUTBOUND_MAX_CHARS);
|
|
1866
|
-
}
|
|
1867
|
-
function parseSessionKey(sessionKey) {
|
|
1868
|
-
const value = sessionKey?.trim();
|
|
1869
|
-
if (!value) {
|
|
1870
|
-
return null;
|
|
1871
|
-
}
|
|
1872
|
-
const separator = value.indexOf(":");
|
|
1873
|
-
if (separator <= 0 || separator >= value.length - 1) {
|
|
1874
|
-
return null;
|
|
1875
|
-
}
|
|
1876
|
-
return {
|
|
1877
|
-
channel: value.slice(0, separator),
|
|
1878
|
-
chatId: value.slice(separator + 1)
|
|
1879
|
-
};
|
|
1880
|
-
}
|
|
1881
|
-
function enqueuePendingSystemEvent(sessionManager, sessionKey, message) {
|
|
1882
|
-
const text = message.trim();
|
|
1883
|
-
if (!text) {
|
|
1884
|
-
return;
|
|
1885
|
-
}
|
|
1886
|
-
const session = sessionManager.getOrCreate(sessionKey);
|
|
1887
|
-
const queueRaw = session.metadata[PENDING_SYSTEM_EVENTS_KEY];
|
|
1888
|
-
const queue = Array.isArray(queueRaw) ? queueRaw.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [];
|
|
1889
|
-
if (queue.at(-1) === text) {
|
|
1890
|
-
return;
|
|
1891
|
-
}
|
|
1892
|
-
queue.push(text);
|
|
1893
|
-
if (queue.length > 20) {
|
|
1894
|
-
queue.splice(0, queue.length - 20);
|
|
1895
|
-
}
|
|
1896
|
-
session.metadata[PENDING_SYSTEM_EVENTS_KEY] = queue;
|
|
1897
|
-
session.updatedAt = /* @__PURE__ */ new Date();
|
|
1898
|
-
sessionManager.save(session);
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
// src/cli/gateway/controller.ts
|
|
1902
1880
|
var hashRaw = (raw) => createHash("sha256").update(raw).digest("hex");
|
|
1903
1881
|
var readConfigSnapshot = (getConfigPath4) => {
|
|
1904
1882
|
const path = getConfigPath4();
|
|
@@ -2028,6 +2006,12 @@ var GatewayControllerImpl = class {
|
|
|
2028
2006
|
return this.deps.reloader.reloadConfig(reason);
|
|
2029
2007
|
}
|
|
2030
2008
|
async restart(options) {
|
|
2009
|
+
await this.writeRestartSentinelPayload({
|
|
2010
|
+
kind: "restart",
|
|
2011
|
+
status: "ok",
|
|
2012
|
+
sessionKey: options?.sessionKey,
|
|
2013
|
+
reason: options?.reason ?? "gateway.restart"
|
|
2014
|
+
});
|
|
2031
2015
|
await this.requestRestart(options);
|
|
2032
2016
|
return "Restart scheduled";
|
|
2033
2017
|
}
|
|
@@ -2499,7 +2483,7 @@ var ServiceCommands = class {
|
|
|
2499
2483
|
}
|
|
2500
2484
|
}
|
|
2501
2485
|
await reloader.getChannels().startAll();
|
|
2502
|
-
await this.wakeFromRestartSentinel({
|
|
2486
|
+
await this.wakeFromRestartSentinel({ bus, sessionManager });
|
|
2503
2487
|
await agent.run();
|
|
2504
2488
|
} finally {
|
|
2505
2489
|
await stopPluginChannelGateways(pluginGatewayHandles);
|
|
@@ -2513,9 +2497,6 @@ var ServiceCommands = class {
|
|
|
2513
2497
|
const trimmed = value.trim();
|
|
2514
2498
|
return trimmed || void 0;
|
|
2515
2499
|
}
|
|
2516
|
-
async sleep(ms) {
|
|
2517
|
-
await new Promise((resolve10) => setTimeout(resolve10, ms));
|
|
2518
|
-
}
|
|
2519
2500
|
resolveMostRecentRoutableSessionKey(sessionManager) {
|
|
2520
2501
|
const sessions = sessionManager.listSessions();
|
|
2521
2502
|
let best = null;
|
|
@@ -2542,42 +2523,27 @@ var ServiceCommands = class {
|
|
|
2542
2523
|
}
|
|
2543
2524
|
return best?.key;
|
|
2544
2525
|
}
|
|
2545
|
-
|
|
2546
|
-
const
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
const
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
] : [{ ...outboundBase }];
|
|
2557
|
-
for (let variantIndex = 0; variantIndex < variants.length; variantIndex += 1) {
|
|
2558
|
-
const outbound = variants[variantIndex];
|
|
2559
|
-
const isLastVariant = variantIndex === variants.length - 1;
|
|
2560
|
-
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
2561
|
-
try {
|
|
2562
|
-
const delivered = await params.channels.deliver(outbound);
|
|
2563
|
-
if (delivered) {
|
|
2564
|
-
return true;
|
|
2565
|
-
}
|
|
2566
|
-
return false;
|
|
2567
|
-
} catch (error) {
|
|
2568
|
-
if (attempt < 3) {
|
|
2569
|
-
await this.sleep(attempt * 500);
|
|
2570
|
-
continue;
|
|
2571
|
-
}
|
|
2572
|
-
if (isLastVariant) {
|
|
2573
|
-
console.warn(
|
|
2574
|
-
`Warning: restart sentinel notify failed for ${params.channel}:${params.chatId} (attempt ${attempt}): ${String(error)}`
|
|
2575
|
-
);
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
}
|
|
2526
|
+
buildRestartWakePrompt(params) {
|
|
2527
|
+
const lines = [
|
|
2528
|
+
"System event: the gateway has restarted successfully.",
|
|
2529
|
+
"Please send one short confirmation to the user that you are back online.",
|
|
2530
|
+
"Do not call any tools.",
|
|
2531
|
+
"Use the same language as the user's recent conversation.",
|
|
2532
|
+
`Reference summary: ${params.summary}`
|
|
2533
|
+
];
|
|
2534
|
+
const reason = this.normalizeOptionalString(params.reason);
|
|
2535
|
+
if (reason) {
|
|
2536
|
+
lines.push(`Restart reason: ${reason}`);
|
|
2579
2537
|
}
|
|
2580
|
-
|
|
2538
|
+
const note = this.normalizeOptionalString(params.note);
|
|
2539
|
+
if (note) {
|
|
2540
|
+
lines.push(`Extra note: ${note}`);
|
|
2541
|
+
}
|
|
2542
|
+
const replyTo = this.normalizeOptionalString(params.replyTo);
|
|
2543
|
+
if (replyTo) {
|
|
2544
|
+
lines.push(`Reply target message id: ${replyTo}. If suitable, include [[reply_to:${replyTo}]].`);
|
|
2545
|
+
}
|
|
2546
|
+
return lines.join("\n");
|
|
2581
2547
|
}
|
|
2582
2548
|
async wakeFromRestartSentinel(params) {
|
|
2583
2549
|
const sentinel = await consumeRestartSentinel();
|
|
@@ -2586,7 +2552,7 @@ var ServiceCommands = class {
|
|
|
2586
2552
|
}
|
|
2587
2553
|
await new Promise((resolve10) => setTimeout(resolve10, 750));
|
|
2588
2554
|
const payload = sentinel.payload;
|
|
2589
|
-
const
|
|
2555
|
+
const summary = formatRestartSentinelMessage(payload);
|
|
2590
2556
|
const sentinelSessionKey = this.normalizeOptionalString(payload.sessionKey);
|
|
2591
2557
|
const fallbackSessionKey = sentinelSessionKey ? void 0 : this.resolveMostRecentRoutableSessionKey(params.sessionManager);
|
|
2592
2558
|
if (!sentinelSessionKey && fallbackSessionKey) {
|
|
@@ -2599,26 +2565,31 @@ var ServiceCommands = class {
|
|
|
2599
2565
|
const chatId = this.normalizeOptionalString(context?.chatId) ?? parsedSession?.chatId ?? this.normalizeOptionalString((params.sessionManager.getIfExists(sessionKey)?.metadata ?? {}).last_to);
|
|
2600
2566
|
const replyTo = this.normalizeOptionalString(context?.replyTo);
|
|
2601
2567
|
const accountId = this.normalizeOptionalString(context?.accountId);
|
|
2602
|
-
const metadataRaw = context?.metadata;
|
|
2603
|
-
const metadata = metadataRaw && typeof metadataRaw === "object" && !Array.isArray(metadataRaw) ? { ...metadataRaw } : {};
|
|
2604
|
-
if (accountId && !this.normalizeOptionalString(metadata.accountId)) {
|
|
2605
|
-
metadata.accountId = accountId;
|
|
2606
|
-
}
|
|
2607
2568
|
if (!channel || !chatId) {
|
|
2608
|
-
|
|
2569
|
+
console.warn(`Warning: restart sentinel cannot resolve route for session ${sessionKey}.`);
|
|
2609
2570
|
return;
|
|
2610
2571
|
}
|
|
2611
|
-
const
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2572
|
+
const prompt2 = this.buildRestartWakePrompt({
|
|
2573
|
+
summary,
|
|
2574
|
+
reason: this.normalizeOptionalString(payload.stats?.reason),
|
|
2575
|
+
note: this.normalizeOptionalString(payload.message),
|
|
2576
|
+
...replyTo ? { replyTo } : {}
|
|
2577
|
+
});
|
|
2578
|
+
const metadata = {
|
|
2579
|
+
source: "restart-sentinel",
|
|
2580
|
+
restart_summary: summary,
|
|
2581
|
+
...replyTo ? { reply_to: replyTo } : {},
|
|
2582
|
+
...accountId ? { account_id: accountId, accountId } : {}
|
|
2583
|
+
};
|
|
2584
|
+
await params.bus.publishInbound({
|
|
2585
|
+
channel: "system",
|
|
2586
|
+
senderId: "restart-sentinel",
|
|
2587
|
+
chatId: `${channel}:${chatId}`,
|
|
2588
|
+
content: prompt2,
|
|
2589
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2590
|
+
attachments: [],
|
|
2617
2591
|
metadata
|
|
2618
2592
|
});
|
|
2619
|
-
if (!delivered) {
|
|
2620
|
-
enqueuePendingSystemEvent(params.sessionManager, sessionKey, message);
|
|
2621
|
-
}
|
|
2622
2593
|
}
|
|
2623
2594
|
async runForeground(options) {
|
|
2624
2595
|
const config2 = loadConfig5();
|
|
@@ -3231,6 +3202,27 @@ var CliRuntime = class {
|
|
|
3231
3202
|
}
|
|
3232
3203
|
console.warn(result.message);
|
|
3233
3204
|
}
|
|
3205
|
+
async writeRestartSentinelFromExecContext(reason) {
|
|
3206
|
+
const sessionKeyRaw = process.env.NEXTCLAW_RUNTIME_SESSION_KEY;
|
|
3207
|
+
const sessionKey = typeof sessionKeyRaw === "string" ? sessionKeyRaw.trim() : "";
|
|
3208
|
+
if (!sessionKey) {
|
|
3209
|
+
return;
|
|
3210
|
+
}
|
|
3211
|
+
try {
|
|
3212
|
+
await writeRestartSentinel({
|
|
3213
|
+
kind: "restart",
|
|
3214
|
+
status: "ok",
|
|
3215
|
+
ts: Date.now(),
|
|
3216
|
+
sessionKey,
|
|
3217
|
+
stats: {
|
|
3218
|
+
reason: reason || "cli.restart",
|
|
3219
|
+
strategy: "exec-tool"
|
|
3220
|
+
}
|
|
3221
|
+
});
|
|
3222
|
+
} catch (error) {
|
|
3223
|
+
console.warn(`Warning: failed to write restart sentinel from exec context: ${String(error)}`);
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3234
3226
|
async onboard() {
|
|
3235
3227
|
console.warn(`Warning: ${APP_NAME4} onboard is deprecated. Use "${APP_NAME4} init" instead.`);
|
|
3236
3228
|
await this.init({ source: "onboard" });
|
|
@@ -3316,6 +3308,7 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
|
|
|
3316
3308
|
});
|
|
3317
3309
|
}
|
|
3318
3310
|
async restart(opts) {
|
|
3311
|
+
await this.writeRestartSentinelFromExecContext("cli.restart");
|
|
3319
3312
|
const state = readServiceState();
|
|
3320
3313
|
if (state && isProcessRunning(state.pid)) {
|
|
3321
3314
|
console.log(`Restarting ${APP_NAME4}...`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nextclaw",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.13",
|
|
4
4
|
"description": "Lightweight personal AI assistant with CLI, multi-provider routing, and channel integrations.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"chokidar": "^3.6.0",
|
|
40
40
|
"commander": "^12.1.0",
|
|
41
|
-
"@nextclaw/core": "^0.6.
|
|
41
|
+
"@nextclaw/core": "^0.6.11",
|
|
42
42
|
"@nextclaw/server": "^0.4.2",
|
|
43
43
|
"@nextclaw/openclaw-compat": "^0.1.5"
|
|
44
44
|
},
|