nextclaw 0.6.11 → 0.6.12

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +195 -169
  2. package/package.json +2 -2
package/dist/cli/index.js CHANGED
@@ -84,24 +84,140 @@ 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 PENDING_SYSTEM_EVENTS_KEY = "pending_system_events";
93
+ var RESTART_REASON_MAX_CHARS = 240;
94
+ var RESTART_NOTE_MAX_CHARS = 600;
95
+ var RESTART_OUTBOUND_MAX_CHARS = 1200;
96
+ function trimTo(value, maxChars) {
97
+ const text = value.trim();
98
+ if (!text) {
99
+ return "";
100
+ }
101
+ if (text.length <= maxChars) {
102
+ return text;
103
+ }
104
+ return `${text.slice(0, Math.max(0, maxChars - 1)).trimEnd()}\u2026`;
105
+ }
106
+ function normalizeLine(value, maxChars) {
107
+ const trimmed = trimTo(value, maxChars);
108
+ return trimmed ? trimmed : null;
109
+ }
110
+ function resolveRestartSentinelPath() {
111
+ return resolve(getDataDir(), "run", RESTART_SENTINEL_FILENAME);
112
+ }
113
+ async function writeRestartSentinel(payload) {
114
+ const path = resolveRestartSentinelPath();
115
+ mkdirSync(resolve(path, ".."), { recursive: true });
116
+ const file = {
117
+ version: 1,
118
+ payload
119
+ };
120
+ writeFileSync(path, `${JSON.stringify(file, null, 2)}
121
+ `, "utf-8");
122
+ return path;
123
+ }
124
+ async function consumeRestartSentinel() {
125
+ const path = resolveRestartSentinelPath();
126
+ if (!existsSync(path)) {
127
+ return null;
128
+ }
129
+ try {
130
+ const raw = readFileSync(path, "utf-8");
131
+ const parsed = JSON.parse(raw);
132
+ if (!parsed || parsed.version !== 1 || !parsed.payload) {
133
+ rmSync(path, { force: true });
134
+ return null;
135
+ }
136
+ rmSync(path, { force: true });
137
+ return parsed;
138
+ } catch {
139
+ rmSync(path, { force: true });
140
+ return null;
141
+ }
142
+ }
143
+ function summarizeRestartSentinel(payload) {
144
+ const reason = normalizeLine(payload.stats?.reason ?? "", RESTART_REASON_MAX_CHARS);
145
+ if (payload.kind === "update.run") {
146
+ return payload.status === "ok" ? "\u2705 NextClaw update completed and service restarted." : "\u26A0\uFE0F NextClaw update finished with issues.";
147
+ }
148
+ if (payload.kind === "config.apply" || payload.kind === "config.patch") {
149
+ return payload.status === "ok" ? "\u2705 Config applied and service restarted." : "\u26A0\uFE0F Config change restart finished with issues.";
150
+ }
151
+ if (reason) {
152
+ return `Gateway restart complete (${reason}).`;
153
+ }
154
+ return "Gateway restart complete.";
155
+ }
156
+ function formatRestartSentinelMessage(payload) {
157
+ const lines = [summarizeRestartSentinel(payload)];
158
+ const note = normalizeLine(payload.message ?? "", RESTART_NOTE_MAX_CHARS);
159
+ if (note) {
160
+ lines.push(note);
161
+ }
162
+ const reason = normalizeLine(payload.stats?.reason ?? "", RESTART_REASON_MAX_CHARS);
163
+ if (reason && !lines.some((line) => line.includes(reason))) {
164
+ lines.push(`Reason: ${reason}`);
165
+ }
166
+ const message = lines.join("\n").trim();
167
+ return trimTo(message, RESTART_OUTBOUND_MAX_CHARS);
168
+ }
169
+ function parseSessionKey(sessionKey) {
170
+ const value = sessionKey?.trim();
171
+ if (!value) {
172
+ return null;
173
+ }
174
+ const separator = value.indexOf(":");
175
+ if (separator <= 0 || separator >= value.length - 1) {
176
+ return null;
177
+ }
178
+ return {
179
+ channel: value.slice(0, separator),
180
+ chatId: value.slice(separator + 1)
181
+ };
182
+ }
183
+ function enqueuePendingSystemEvent(sessionManager, sessionKey, message) {
184
+ const text = message.trim();
185
+ if (!text) {
186
+ return;
187
+ }
188
+ const session = sessionManager.getOrCreate(sessionKey);
189
+ const queueRaw = session.metadata[PENDING_SYSTEM_EVENTS_KEY];
190
+ const queue = Array.isArray(queueRaw) ? queueRaw.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [];
191
+ if (queue.at(-1) === text) {
192
+ return;
193
+ }
194
+ queue.push(text);
195
+ if (queue.length > 20) {
196
+ queue.splice(0, queue.length - 20);
197
+ }
198
+ session.metadata[PENDING_SYSTEM_EVENTS_KEY] = queue;
199
+ session.updatedAt = /* @__PURE__ */ new Date();
200
+ sessionManager.save(session);
201
+ }
202
+
87
203
  // src/cli/skills/clawhub.ts
88
204
  import { spawnSync } from "child_process";
89
- import { existsSync } from "fs";
90
- import { isAbsolute, join, resolve } from "path";
205
+ import { existsSync as existsSync2 } from "fs";
206
+ import { isAbsolute, join, resolve as resolve2 } from "path";
91
207
  async function installClawHubSkill(options) {
92
208
  const slug = options.slug.trim();
93
209
  if (!slug) {
94
210
  throw new Error("Skill slug is required.");
95
211
  }
96
- const workdir = resolve(options.workdir);
97
- if (!existsSync(workdir)) {
212
+ const workdir = resolve2(options.workdir);
213
+ if (!existsSync2(workdir)) {
98
214
  throw new Error(`Workdir does not exist: ${workdir}`);
99
215
  }
100
216
  const dirName = options.dir?.trim() || "skills";
101
- const destinationDir = isAbsolute(dirName) ? resolve(dirName, slug) : resolve(workdir, dirName, slug);
217
+ const destinationDir = isAbsolute(dirName) ? resolve2(dirName, slug) : resolve2(workdir, dirName, slug);
102
218
  const skillFile = join(destinationDir, "SKILL.md");
103
- if (!options.force && existsSync(destinationDir)) {
104
- if (existsSync(skillFile)) {
219
+ if (!options.force && existsSync2(destinationDir)) {
220
+ if (existsSync2(skillFile)) {
105
221
  return {
106
222
  slug,
107
223
  version: options.version,
@@ -156,15 +272,15 @@ function buildClawHubArgs(slug, options) {
156
272
 
157
273
  // src/cli/update/runner.ts
158
274
  import { spawnSync as spawnSync2 } from "child_process";
159
- import { resolve as resolve3 } from "path";
275
+ import { resolve as resolve4 } from "path";
160
276
 
161
277
  // src/cli/utils.ts
162
- import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync, rmSync } from "fs";
163
- import { join as join2, resolve as resolve2 } from "path";
278
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
279
+ import { join as join2, resolve as resolve3 } from "path";
164
280
  import { spawn } from "child_process";
165
281
  import { isIP } from "net";
166
282
  import { fileURLToPath } from "url";
167
- import { getDataDir, getPackageVersion as getCorePackageVersion } from "@nextclaw/core";
283
+ import { getDataDir as getDataDir2, getPackageVersion as getCorePackageVersion } from "@nextclaw/core";
168
284
  function resolveUiConfig(config2, overrides) {
169
285
  const base = config2.ui ?? { enabled: false, host: "127.0.0.1", port: 18791, open: false };
170
286
  return { ...base, ...overrides ?? {} };
@@ -214,11 +330,11 @@ function buildServeArgs(options) {
214
330
  }
215
331
  function readServiceState() {
216
332
  const path = resolveServiceStatePath();
217
- if (!existsSync2(path)) {
333
+ if (!existsSync3(path)) {
218
334
  return null;
219
335
  }
220
336
  try {
221
- const raw = readFileSync(path, "utf-8");
337
+ const raw = readFileSync2(path, "utf-8");
222
338
  return JSON.parse(raw);
223
339
  } catch {
224
340
  return null;
@@ -226,20 +342,20 @@ function readServiceState() {
226
342
  }
227
343
  function writeServiceState(state) {
228
344
  const path = resolveServiceStatePath();
229
- mkdirSync(resolve2(path, ".."), { recursive: true });
230
- writeFileSync(path, JSON.stringify(state, null, 2));
345
+ mkdirSync2(resolve3(path, ".."), { recursive: true });
346
+ writeFileSync2(path, JSON.stringify(state, null, 2));
231
347
  }
232
348
  function clearServiceState() {
233
349
  const path = resolveServiceStatePath();
234
- if (existsSync2(path)) {
235
- rmSync(path, { force: true });
350
+ if (existsSync3(path)) {
351
+ rmSync2(path, { force: true });
236
352
  }
237
353
  }
238
354
  function resolveServiceStatePath() {
239
- return resolve2(getDataDir(), "run", "service.json");
355
+ return resolve3(getDataDir2(), "run", "service.json");
240
356
  }
241
357
  function resolveServiceLogPath() {
242
- return resolve2(getDataDir(), "logs", "service.log");
358
+ return resolve3(getDataDir2(), "logs", "service.log");
243
359
  }
244
360
  function isProcessRunning(pid) {
245
361
  try {
@@ -265,8 +381,8 @@ function resolveUiStaticDir() {
265
381
  if (envDir) {
266
382
  candidates.push(envDir);
267
383
  }
268
- const cliDir = resolve2(fileURLToPath(new URL(".", import.meta.url)));
269
- const pkgRoot = resolve2(cliDir, "..", "..");
384
+ const cliDir = resolve3(fileURLToPath(new URL(".", import.meta.url)));
385
+ const pkgRoot = resolve3(cliDir, "..", "..");
270
386
  candidates.push(join2(pkgRoot, "ui-dist"));
271
387
  candidates.push(join2(pkgRoot, "ui"));
272
388
  candidates.push(join2(pkgRoot, "..", "ui-dist"));
@@ -278,7 +394,7 @@ function resolveUiStaticDir() {
278
394
  candidates.push(join2(pkgRoot, "..", "..", "packages", "nextclaw-ui", "dist"));
279
395
  candidates.push(join2(pkgRoot, "..", "..", "nextclaw-ui", "dist"));
280
396
  for (const dir of candidates) {
281
- if (existsSync2(join2(dir, "index.html"))) {
397
+ if (existsSync3(join2(dir, "index.html"))) {
282
398
  return dir;
283
399
  }
284
400
  }
@@ -305,19 +421,19 @@ function which(binary) {
305
421
  const paths = (process.env.PATH ?? "").split(":");
306
422
  for (const dir of paths) {
307
423
  const full = join2(dir, binary);
308
- if (existsSync2(full)) {
424
+ if (existsSync3(full)) {
309
425
  return true;
310
426
  }
311
427
  }
312
428
  return false;
313
429
  }
314
430
  function resolveVersionFromPackageTree(startDir, expectedName) {
315
- let current = resolve2(startDir);
431
+ let current = resolve3(startDir);
316
432
  while (current.length > 0) {
317
433
  const pkgPath = join2(current, "package.json");
318
- if (existsSync2(pkgPath)) {
434
+ if (existsSync3(pkgPath)) {
319
435
  try {
320
- const raw = readFileSync(pkgPath, "utf-8");
436
+ const raw = readFileSync2(pkgPath, "utf-8");
321
437
  const parsed = JSON.parse(raw);
322
438
  if (typeof parsed.version === "string") {
323
439
  if (!expectedName || parsed.name === expectedName) {
@@ -327,7 +443,7 @@ function resolveVersionFromPackageTree(startDir, expectedName) {
327
443
  } catch {
328
444
  }
329
445
  }
330
- const parent = resolve2(current, "..");
446
+ const parent = resolve3(current, "..");
331
447
  if (parent === current) {
332
448
  break;
333
449
  }
@@ -336,7 +452,7 @@ function resolveVersionFromPackageTree(startDir, expectedName) {
336
452
  return null;
337
453
  }
338
454
  function getPackageVersion() {
339
- const cliDir = resolve2(fileURLToPath(new URL(".", import.meta.url)));
455
+ const cliDir = resolve3(fileURLToPath(new URL(".", import.meta.url)));
340
456
  return resolveVersionFromPackageTree(cliDir, "nextclaw") ?? resolveVersionFromPackageTree(cliDir) ?? getCorePackageVersion();
341
457
  }
342
458
  function printAgentResponse(response) {
@@ -375,7 +491,7 @@ function runSelfUpdate(options = {}) {
375
491
  return { ok: result.status === 0, code: result.status };
376
492
  };
377
493
  if (updateCommand) {
378
- const cwd = options.cwd ? resolve3(options.cwd) : process.cwd();
494
+ const cwd = options.cwd ? resolve4(options.cwd) : process.cwd();
379
495
  const ok = runStep("sh", ["-c", updateCommand], cwd);
380
496
  if (!ok.ok) {
381
497
  return { ok: false, error: "update command failed", strategy: "command", steps };
@@ -413,8 +529,8 @@ import {
413
529
  expandHome
414
530
  } from "@nextclaw/core";
415
531
  import { createInterface } from "readline";
416
- import { existsSync as existsSync3 } from "fs";
417
- import { resolve as resolve4 } from "path";
532
+ import { existsSync as existsSync4 } from "fs";
533
+ import { resolve as resolve5 } from "path";
418
534
  function loadPluginRegistry(config2, workspaceDir) {
419
535
  return loadOpenClawPlugins({
420
536
  config: config2,
@@ -691,7 +807,7 @@ var PluginCommands = class {
691
807
  process.exit(1);
692
808
  }
693
809
  const install = config2.plugins.installs?.[pluginId];
694
- const isLinked = install?.source === "path" && (!install.installPath || !install.sourcePath || resolve4(install.installPath) === resolve4(install.sourcePath));
810
+ const isLinked = install?.source === "path" && (!install.installPath || !install.sourcePath || resolve5(install.installPath) === resolve5(install.sourcePath));
695
811
  const preview = [];
696
812
  if (hasEntry) {
697
813
  preview.push("config entry");
@@ -770,9 +886,9 @@ var PluginCommands = class {
770
886
  process.exit(1);
771
887
  }
772
888
  const normalized = fileSpec && fileSpec.ok ? fileSpec.path : pathOrSpec;
773
- const resolved = resolve4(expandHome(normalized));
889
+ const resolved = resolve5(expandHome(normalized));
774
890
  const config2 = loadConfig();
775
- if (existsSync3(resolved)) {
891
+ if (existsSync4(resolved)) {
776
892
  if (opts.link) {
777
893
  const probe = await installPluginFromPath({ path: resolved, dryRun: true });
778
894
  if (!probe.ok) {
@@ -1331,11 +1447,11 @@ var ChannelCommands = class {
1331
1447
  };
1332
1448
 
1333
1449
  // src/cli/commands/cron.ts
1334
- import { CronService, getDataDir as getDataDir2 } from "@nextclaw/core";
1450
+ import { CronService, getDataDir as getDataDir3 } from "@nextclaw/core";
1335
1451
  import { join as join3 } from "path";
1336
1452
  var CronCommands = class {
1337
1453
  cronList(opts) {
1338
- const storePath = join3(getDataDir2(), "cron", "jobs.json");
1454
+ const storePath = join3(getDataDir3(), "cron", "jobs.json");
1339
1455
  const service = new CronService(storePath);
1340
1456
  const jobs = service.listJobs(Boolean(opts.all));
1341
1457
  if (!jobs.length) {
@@ -1355,7 +1471,7 @@ var CronCommands = class {
1355
1471
  }
1356
1472
  }
1357
1473
  cronAdd(opts) {
1358
- const storePath = join3(getDataDir2(), "cron", "jobs.json");
1474
+ const storePath = join3(getDataDir3(), "cron", "jobs.json");
1359
1475
  const service = new CronService(storePath);
1360
1476
  let schedule = null;
1361
1477
  if (opts.every) {
@@ -1380,7 +1496,7 @@ var CronCommands = class {
1380
1496
  console.log(`\u2713 Added job '${job.name}' (${job.id})`);
1381
1497
  }
1382
1498
  cronRemove(jobId) {
1383
- const storePath = join3(getDataDir2(), "cron", "jobs.json");
1499
+ const storePath = join3(getDataDir3(), "cron", "jobs.json");
1384
1500
  const service = new CronService(storePath);
1385
1501
  if (service.removeJob(jobId)) {
1386
1502
  console.log(`\u2713 Removed job ${jobId}`);
@@ -1389,7 +1505,7 @@ var CronCommands = class {
1389
1505
  }
1390
1506
  }
1391
1507
  cronEnable(jobId, opts) {
1392
- const storePath = join3(getDataDir2(), "cron", "jobs.json");
1508
+ const storePath = join3(getDataDir3(), "cron", "jobs.json");
1393
1509
  const service = new CronService(storePath);
1394
1510
  const job = service.enableJob(jobId, !opts.disable);
1395
1511
  if (job) {
@@ -1399,7 +1515,7 @@ var CronCommands = class {
1399
1515
  }
1400
1516
  }
1401
1517
  async cronRun(jobId, opts) {
1402
- const storePath = join3(getDataDir2(), "cron", "jobs.json");
1518
+ const storePath = join3(getDataDir3(), "cron", "jobs.json");
1403
1519
  const service = new CronService(storePath);
1404
1520
  const ok = await service.runJob(jobId, Boolean(opts.force));
1405
1521
  console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
@@ -1408,12 +1524,12 @@ var CronCommands = class {
1408
1524
 
1409
1525
  // src/cli/commands/diagnostics.ts
1410
1526
  import { createServer as createNetServer } from "net";
1411
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
1412
- import { resolve as resolve5 } from "path";
1527
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
1528
+ import { resolve as resolve6 } from "path";
1413
1529
  import {
1414
1530
  APP_NAME,
1415
1531
  getConfigPath,
1416
- getDataDir as getDataDir3,
1532
+ getDataDir as getDataDir4,
1417
1533
  getWorkspacePath as getWorkspacePath3,
1418
1534
  loadConfig as loadConfig4,
1419
1535
  PROVIDERS as PROVIDERS3
@@ -1579,7 +1695,7 @@ var DiagnosticsCommands = class {
1579
1695
  const configPath = getConfigPath();
1580
1696
  const config2 = loadConfig4();
1581
1697
  const workspacePath = getWorkspacePath3(config2.agents.defaults.workspace);
1582
- const serviceStatePath = resolve5(getDataDir3(), "run", "service.json");
1698
+ const serviceStatePath = resolve6(getDataDir4(), "run", "service.json");
1583
1699
  const fixActions = [];
1584
1700
  let serviceState = readServiceState();
1585
1701
  if (params.fix && serviceState && !isProcessRunning(serviceState.pid)) {
@@ -1618,11 +1734,11 @@ var DiagnosticsCommands = class {
1618
1734
  });
1619
1735
  const issues = [];
1620
1736
  const recommendations = [];
1621
- if (!existsSync4(configPath)) {
1737
+ if (!existsSync5(configPath)) {
1622
1738
  issues.push("Config file is missing.");
1623
1739
  recommendations.push(`Run ${APP_NAME} init to create config files.`);
1624
1740
  }
1625
- if (!existsSync4(workspacePath)) {
1741
+ if (!existsSync5(workspacePath)) {
1626
1742
  issues.push("Workspace directory does not exist.");
1627
1743
  recommendations.push(`Run ${APP_NAME} init to create workspace templates.`);
1628
1744
  }
@@ -1650,13 +1766,13 @@ var DiagnosticsCommands = class {
1650
1766
  return {
1651
1767
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1652
1768
  configPath,
1653
- configExists: existsSync4(configPath),
1769
+ configExists: existsSync5(configPath),
1654
1770
  workspacePath,
1655
- workspaceExists: existsSync4(workspacePath),
1771
+ workspaceExists: existsSync5(workspacePath),
1656
1772
  model: config2.agents.defaults.model,
1657
1773
  providers,
1658
1774
  serviceStatePath,
1659
- serviceStateExists: existsSync4(serviceStatePath),
1775
+ serviceStateExists: existsSync5(serviceStatePath),
1660
1776
  fixActions,
1661
1777
  process: {
1662
1778
  managedByState,
@@ -1706,11 +1822,11 @@ var DiagnosticsCommands = class {
1706
1822
  }
1707
1823
  }
1708
1824
  readLogTail(path, maxLines = 25) {
1709
- if (!existsSync4(path)) {
1825
+ if (!existsSync5(path)) {
1710
1826
  return [];
1711
1827
  }
1712
1828
  try {
1713
- const lines = readFileSync2(path, "utf-8").split(/\r?\n/).filter(Boolean);
1829
+ const lines = readFileSync3(path, "utf-8").split(/\r?\n/).filter(Boolean);
1714
1830
  if (lines.length <= maxLines) {
1715
1831
  return lines;
1716
1832
  }
@@ -1781,124 +1897,6 @@ import {
1781
1897
  ConfigSchema,
1782
1898
  redactConfigObject
1783
1899
  } 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
1900
  var hashRaw = (raw) => createHash("sha256").update(raw).digest("hex");
1903
1901
  var readConfigSnapshot = (getConfigPath4) => {
1904
1902
  const path = getConfigPath4();
@@ -2028,6 +2026,12 @@ var GatewayControllerImpl = class {
2028
2026
  return this.deps.reloader.reloadConfig(reason);
2029
2027
  }
2030
2028
  async restart(options) {
2029
+ await this.writeRestartSentinelPayload({
2030
+ kind: "restart",
2031
+ status: "ok",
2032
+ sessionKey: options?.sessionKey,
2033
+ reason: options?.reason ?? "gateway.restart"
2034
+ });
2031
2035
  await this.requestRestart(options);
2032
2036
  return "Restart scheduled";
2033
2037
  }
@@ -3231,6 +3235,27 @@ var CliRuntime = class {
3231
3235
  }
3232
3236
  console.warn(result.message);
3233
3237
  }
3238
+ async writeRestartSentinelFromExecContext(reason) {
3239
+ const sessionKeyRaw = process.env.NEXTCLAW_RUNTIME_SESSION_KEY;
3240
+ const sessionKey = typeof sessionKeyRaw === "string" ? sessionKeyRaw.trim() : "";
3241
+ if (!sessionKey) {
3242
+ return;
3243
+ }
3244
+ try {
3245
+ await writeRestartSentinel({
3246
+ kind: "restart",
3247
+ status: "ok",
3248
+ ts: Date.now(),
3249
+ sessionKey,
3250
+ stats: {
3251
+ reason: reason || "cli.restart",
3252
+ strategy: "exec-tool"
3253
+ }
3254
+ });
3255
+ } catch (error) {
3256
+ console.warn(`Warning: failed to write restart sentinel from exec context: ${String(error)}`);
3257
+ }
3258
+ }
3234
3259
  async onboard() {
3235
3260
  console.warn(`Warning: ${APP_NAME4} onboard is deprecated. Use "${APP_NAME4} init" instead.`);
3236
3261
  await this.init({ source: "onboard" });
@@ -3316,6 +3341,7 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
3316
3341
  });
3317
3342
  }
3318
3343
  async restart(opts) {
3344
+ await this.writeRestartSentinelFromExecContext("cli.restart");
3319
3345
  const state = readServiceState();
3320
3346
  if (state && isProcessRunning(state.pid)) {
3321
3347
  console.log(`Restarting ${APP_NAME4}...`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextclaw",
3
- "version": "0.6.11",
3
+ "version": "0.6.12",
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.9",
41
+ "@nextclaw/core": "^0.6.10",
42
42
  "@nextclaw/server": "^0.4.2",
43
43
  "@nextclaw/openclaw-compat": "^0.1.5"
44
44
  },