nextclaw 0.5.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2,86 +2,164 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { Command } from "commander";
5
- import { APP_NAME as APP_NAME2, APP_TAGLINE } from "@nextclaw/core";
5
+ import { APP_NAME as APP_NAME5, APP_TAGLINE } from "@nextclaw/core";
6
6
 
7
7
  // src/cli/runtime.ts
8
8
  import {
9
- loadConfig,
10
- saveConfig,
11
- getConfigPath,
12
- getDataDir as getDataDir2,
9
+ loadConfig as loadConfig5,
10
+ saveConfig as saveConfig3,
11
+ getConfigPath as getConfigPath3,
12
+ getDataDir as getDataDir6,
13
13
  ConfigSchema as ConfigSchema2,
14
- getApiBase,
15
- getProvider,
16
- getProviderName,
17
- buildReloadPlan,
18
- diffConfigPaths,
19
- getWorkspacePath,
14
+ getWorkspacePath as getWorkspacePath3,
20
15
  expandHome,
21
- MessageBus,
22
- AgentLoop,
23
- LiteLLMProvider,
24
- LLMProvider,
25
- ProviderManager,
26
- ChannelManager,
27
- SessionManager,
28
- CronService,
29
- HeartbeatService,
30
- PROVIDERS,
31
- APP_NAME,
16
+ MessageBus as MessageBus2,
17
+ AgentLoop as AgentLoop2,
18
+ ProviderManager as ProviderManager2,
19
+ APP_NAME as APP_NAME4,
32
20
  DEFAULT_WORKSPACE_DIR,
33
21
  DEFAULT_WORKSPACE_PATH
34
22
  } from "@nextclaw/core";
35
- import {
36
- loadOpenClawPlugins,
37
- buildPluginStatusReport,
38
- enablePluginInConfig,
39
- disablePluginInConfig,
40
- addPluginLoadPath,
41
- recordPluginInstall,
42
- installPluginFromPath,
43
- installPluginFromNpmSpec,
44
- uninstallPlugin,
45
- resolveUninstallDirectoryTarget,
46
- setPluginRuntimeBridge,
47
- getPluginChannelBindings,
48
- getPluginUiMetadataFromRegistry,
49
- resolvePluginChannelMessageToolHints,
50
- startPluginChannelGateways,
51
- stopPluginChannelGateways
52
- } from "@nextclaw/openclaw-compat";
53
- import { startUiServer } from "@nextclaw/server";
54
- import {
55
- closeSync,
56
- cpSync,
57
- existsSync as existsSync4,
58
- mkdirSync as mkdirSync2,
59
- openSync,
60
- readdirSync,
61
- readFileSync as readFileSync3,
62
- rmSync as rmSync2,
63
- writeFileSync as writeFileSync2
64
- } from "fs";
65
- import { dirname, join as join3, resolve as resolve4 } from "path";
66
- import { createServer as createNetServer } from "net";
67
- import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
23
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
24
+ import { join as join6, resolve as resolve7 } from "path";
68
25
  import { createInterface } from "readline";
69
- import { createRequire } from "module";
70
- import { fileURLToPath as fileURLToPath2 } from "url";
71
- import chokidar from "chokidar";
26
+ import { fileURLToPath as fileURLToPath3 } from "url";
27
+ import { spawn as spawn3 } from "child_process";
72
28
 
73
- // src/cli/gateway/controller.ts
74
- import { createHash } from "crypto";
75
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
76
- import {
77
- buildConfigSchema,
78
- ConfigSchema,
79
- redactConfigObject
80
- } from "@nextclaw/core";
29
+ // src/cli/restart-coordinator.ts
30
+ var RestartCoordinator = class {
31
+ constructor(deps) {
32
+ this.deps = deps;
33
+ }
34
+ restartingService = false;
35
+ exitScheduled = false;
36
+ async requestRestart(request) {
37
+ const reason = request.reason.trim() || "config changed";
38
+ const strategy = request.strategy ?? "background-service-or-manual";
39
+ if (strategy !== "exit-process") {
40
+ const state = this.deps.readServiceState();
41
+ const serviceRunning = Boolean(state && this.deps.isProcessRunning(state.pid));
42
+ const managedByCurrentProcess = Boolean(state && state.pid === this.deps.currentPid());
43
+ if (serviceRunning && !managedByCurrentProcess) {
44
+ if (this.restartingService) {
45
+ return {
46
+ status: "restart-in-progress",
47
+ message: "Restart already in progress; skipping duplicate request."
48
+ };
49
+ }
50
+ this.restartingService = true;
51
+ try {
52
+ const restarted = await this.deps.restartBackgroundService(reason);
53
+ if (restarted) {
54
+ return {
55
+ status: "service-restarted",
56
+ message: `Restarted background service to apply changes (${reason}).`
57
+ };
58
+ }
59
+ } finally {
60
+ this.restartingService = false;
61
+ }
62
+ }
63
+ }
64
+ if (strategy === "background-service-or-exit" || strategy === "exit-process") {
65
+ if (this.exitScheduled) {
66
+ return {
67
+ status: "exit-scheduled",
68
+ message: "Restart already scheduled; skipping duplicate request."
69
+ };
70
+ }
71
+ const delay = typeof request.delayMs === "number" && Number.isFinite(request.delayMs) ? Math.max(0, Math.floor(request.delayMs)) : 100;
72
+ this.exitScheduled = true;
73
+ this.deps.scheduleProcessExit(delay, reason);
74
+ return {
75
+ status: "exit-scheduled",
76
+ message: `Restart scheduled (${reason}).`
77
+ };
78
+ }
79
+ return {
80
+ status: "manual-required",
81
+ message: request.manualMessage ?? "Restart the gateway to apply changes."
82
+ };
83
+ }
84
+ };
85
+
86
+ // src/cli/skills/clawhub.ts
87
+ import { spawnSync } from "child_process";
88
+ import { existsSync } from "fs";
89
+ import { isAbsolute, join, resolve } from "path";
90
+ async function installClawHubSkill(options) {
91
+ const slug = options.slug.trim();
92
+ if (!slug) {
93
+ throw new Error("Skill slug is required.");
94
+ }
95
+ const workdir = resolve(options.workdir);
96
+ if (!existsSync(workdir)) {
97
+ throw new Error(`Workdir does not exist: ${workdir}`);
98
+ }
99
+ const dirName = options.dir?.trim() || "skills";
100
+ const destinationDir = isAbsolute(dirName) ? resolve(dirName, slug) : resolve(workdir, dirName, slug);
101
+ const skillFile = join(destinationDir, "SKILL.md");
102
+ if (!options.force && existsSync(destinationDir)) {
103
+ if (existsSync(skillFile)) {
104
+ return {
105
+ slug,
106
+ version: options.version,
107
+ registry: options.registry,
108
+ destinationDir,
109
+ alreadyInstalled: true
110
+ };
111
+ }
112
+ throw new Error(`Skill directory already exists: ${destinationDir} (use --force)`);
113
+ }
114
+ const args = buildClawHubArgs(slug, options);
115
+ const result = spawnSync("npx", args, {
116
+ cwd: workdir,
117
+ stdio: "pipe",
118
+ env: process.env
119
+ });
120
+ if (result.error) {
121
+ throw new Error(`Failed to run npx clawhub: ${String(result.error)}`);
122
+ }
123
+ if (result.status !== 0) {
124
+ const stdout = result.stdout ? String(result.stdout).trim() : "";
125
+ const stderr = result.stderr ? String(result.stderr).trim() : "";
126
+ const details = [stderr, stdout].filter(Boolean).join("\n");
127
+ throw new Error(details || `clawhub install failed with code ${result.status ?? 1}`);
128
+ }
129
+ return {
130
+ slug,
131
+ version: options.version,
132
+ registry: options.registry,
133
+ destinationDir
134
+ };
135
+ }
136
+ function buildClawHubArgs(slug, options) {
137
+ const args = ["--yes", "clawhub", "install", slug];
138
+ if (options.version) {
139
+ args.push("--version", options.version);
140
+ }
141
+ if (options.registry) {
142
+ args.push("--registry", options.registry);
143
+ }
144
+ if (options.workdir) {
145
+ args.push("--workdir", options.workdir);
146
+ }
147
+ if (options.dir) {
148
+ args.push("--dir", options.dir);
149
+ }
150
+ if (options.force) {
151
+ args.push("--force");
152
+ }
153
+ return args;
154
+ }
155
+
156
+ // src/cli/update/runner.ts
157
+ import { spawnSync as spawnSync2 } from "child_process";
158
+ import { resolve as resolve3 } from "path";
81
159
 
82
160
  // src/cli/utils.ts
83
- import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "fs";
84
- import { join, resolve } from "path";
161
+ import { existsSync as existsSync2, mkdirSync, readFileSync, writeFileSync, rmSync } from "fs";
162
+ import { join as join2, resolve as resolve2 } from "path";
85
163
  import { spawn } from "child_process";
86
164
  import { isIP } from "net";
87
165
  import { fileURLToPath } from "url";
@@ -135,7 +213,7 @@ function buildServeArgs(options) {
135
213
  }
136
214
  function readServiceState() {
137
215
  const path = resolveServiceStatePath();
138
- if (!existsSync(path)) {
216
+ if (!existsSync2(path)) {
139
217
  return null;
140
218
  }
141
219
  try {
@@ -147,20 +225,20 @@ function readServiceState() {
147
225
  }
148
226
  function writeServiceState(state) {
149
227
  const path = resolveServiceStatePath();
150
- mkdirSync(resolve(path, ".."), { recursive: true });
228
+ mkdirSync(resolve2(path, ".."), { recursive: true });
151
229
  writeFileSync(path, JSON.stringify(state, null, 2));
152
230
  }
153
231
  function clearServiceState() {
154
232
  const path = resolveServiceStatePath();
155
- if (existsSync(path)) {
233
+ if (existsSync2(path)) {
156
234
  rmSync(path, { force: true });
157
235
  }
158
236
  }
159
237
  function resolveServiceStatePath() {
160
- return resolve(getDataDir(), "run", "service.json");
238
+ return resolve2(getDataDir(), "run", "service.json");
161
239
  }
162
240
  function resolveServiceLogPath() {
163
- return resolve(getDataDir(), "logs", "service.log");
241
+ return resolve2(getDataDir(), "logs", "service.log");
164
242
  }
165
243
  function isProcessRunning(pid) {
166
244
  try {
@@ -176,7 +254,7 @@ async function waitForExit(pid, timeoutMs) {
176
254
  if (!isProcessRunning(pid)) {
177
255
  return true;
178
256
  }
179
- await new Promise((resolve5) => setTimeout(resolve5, 200));
257
+ await new Promise((resolve8) => setTimeout(resolve8, 200));
180
258
  }
181
259
  return !isProcessRunning(pid);
182
260
  }
@@ -186,20 +264,20 @@ function resolveUiStaticDir() {
186
264
  if (envDir) {
187
265
  candidates.push(envDir);
188
266
  }
189
- const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
190
- const pkgRoot = resolve(cliDir, "..", "..");
191
- candidates.push(join(pkgRoot, "ui-dist"));
192
- candidates.push(join(pkgRoot, "ui"));
193
- candidates.push(join(pkgRoot, "..", "ui-dist"));
194
- candidates.push(join(pkgRoot, "..", "ui"));
267
+ const cliDir = resolve2(fileURLToPath(new URL(".", import.meta.url)));
268
+ const pkgRoot = resolve2(cliDir, "..", "..");
269
+ candidates.push(join2(pkgRoot, "ui-dist"));
270
+ candidates.push(join2(pkgRoot, "ui"));
271
+ candidates.push(join2(pkgRoot, "..", "ui-dist"));
272
+ candidates.push(join2(pkgRoot, "..", "ui"));
195
273
  const cwd = process.cwd();
196
- candidates.push(join(cwd, "packages", "nextclaw-ui", "dist"));
197
- candidates.push(join(cwd, "nextclaw-ui", "dist"));
198
- candidates.push(join(pkgRoot, "..", "nextclaw-ui", "dist"));
199
- candidates.push(join(pkgRoot, "..", "..", "packages", "nextclaw-ui", "dist"));
200
- candidates.push(join(pkgRoot, "..", "..", "nextclaw-ui", "dist"));
274
+ candidates.push(join2(cwd, "packages", "nextclaw-ui", "dist"));
275
+ candidates.push(join2(cwd, "nextclaw-ui", "dist"));
276
+ candidates.push(join2(pkgRoot, "..", "nextclaw-ui", "dist"));
277
+ candidates.push(join2(pkgRoot, "..", "..", "packages", "nextclaw-ui", "dist"));
278
+ candidates.push(join2(pkgRoot, "..", "..", "nextclaw-ui", "dist"));
201
279
  for (const dir of candidates) {
202
- if (existsSync(join(dir, "index.html"))) {
280
+ if (existsSync2(join2(dir, "index.html"))) {
203
281
  return dir;
204
282
  }
205
283
  }
@@ -225,18 +303,18 @@ function openBrowser(url) {
225
303
  function which(binary) {
226
304
  const paths = (process.env.PATH ?? "").split(":");
227
305
  for (const dir of paths) {
228
- const full = join(dir, binary);
229
- if (existsSync(full)) {
306
+ const full = join2(dir, binary);
307
+ if (existsSync2(full)) {
230
308
  return true;
231
309
  }
232
310
  }
233
311
  return false;
234
312
  }
235
313
  function resolveVersionFromPackageTree(startDir, expectedName) {
236
- let current = resolve(startDir);
314
+ let current = resolve2(startDir);
237
315
  while (current.length > 0) {
238
- const pkgPath = join(current, "package.json");
239
- if (existsSync(pkgPath)) {
316
+ const pkgPath = join2(current, "package.json");
317
+ if (existsSync2(pkgPath)) {
240
318
  try {
241
319
  const raw = readFileSync(pkgPath, "utf-8");
242
320
  const parsed = JSON.parse(raw);
@@ -248,7 +326,7 @@ function resolveVersionFromPackageTree(startDir, expectedName) {
248
326
  } catch {
249
327
  }
250
328
  }
251
- const parent = resolve(current, "..");
329
+ const parent = resolve2(current, "..");
252
330
  if (parent === current) {
253
331
  break;
254
332
  }
@@ -257,7 +335,7 @@ function resolveVersionFromPackageTree(startDir, expectedName) {
257
335
  return null;
258
336
  }
259
337
  function getPackageVersion() {
260
- const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
338
+ const cliDir = resolve2(fileURLToPath(new URL(".", import.meta.url)));
261
339
  return resolveVersionFromPackageTree(cliDir, "nextclaw") ?? resolveVersionFromPackageTree(cliDir) ?? getCorePackageVersion();
262
340
  }
263
341
  function printAgentResponse(response) {
@@ -266,14 +344,12 @@ function printAgentResponse(response) {
266
344
  async function prompt(rl, question) {
267
345
  rl.setPrompt(question);
268
346
  rl.prompt();
269
- return new Promise((resolve5) => {
270
- rl.once("line", (line) => resolve5(line));
347
+ return new Promise((resolve8) => {
348
+ rl.once("line", (line) => resolve8(line));
271
349
  });
272
350
  }
273
351
 
274
352
  // src/cli/update/runner.ts
275
- import { spawnSync } from "child_process";
276
- import { resolve as resolve2 } from "path";
277
353
  var DEFAULT_TIMEOUT_MS = 20 * 6e4;
278
354
  function runSelfUpdate(options = {}) {
279
355
  const steps = [];
@@ -281,7 +357,7 @@ function runSelfUpdate(options = {}) {
281
357
  const updateCommand = options.updateCommand ?? process.env.NEXTCLAW_UPDATE_COMMAND?.trim();
282
358
  const packageName = options.packageName ?? "nextclaw";
283
359
  const runStep = (cmd, args, cwd) => {
284
- const result = spawnSync(cmd, args, {
360
+ const result = spawnSync2(cmd, args, {
285
361
  cwd,
286
362
  encoding: "utf-8",
287
363
  timeout: timeoutMs,
@@ -298,7 +374,7 @@ function runSelfUpdate(options = {}) {
298
374
  return { ok: result.status === 0, code: result.status };
299
375
  };
300
376
  if (updateCommand) {
301
- const cwd = options.cwd ? resolve2(options.cwd) : process.cwd();
377
+ const cwd = options.cwd ? resolve3(options.cwd) : process.cwd();
302
378
  const ok = runStep("sh", ["-c", updateCommand], cwd);
303
379
  if (!ok.ok) {
304
380
  return { ok: false, error: "update command failed", strategy: "command", steps };
@@ -315,1158 +391,210 @@ function runSelfUpdate(options = {}) {
315
391
  return { ok: false, error: "no update strategy available", strategy: "none", steps };
316
392
  }
317
393
 
318
- // src/cli/gateway/controller.ts
319
- var hashRaw = (raw) => createHash("sha256").update(raw).digest("hex");
320
- var readConfigSnapshot = (getConfigPath2, plugins2) => {
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 config2;
333
- let valid = true;
334
- try {
335
- config2 = ConfigSchema.parse(parsed);
336
- } catch {
337
- config2 = ConfigSchema.parse({});
338
- valid = false;
339
- }
340
- if (!raw) {
341
- raw = JSON.stringify(config2, null, 2);
394
+ // src/cli/commands/config.ts
395
+ import { buildReloadPlan, diffConfigPaths, loadConfig, saveConfig } from "@nextclaw/core";
396
+
397
+ // src/cli/config-path.ts
398
+ function isIndexSegment(raw) {
399
+ return /^[0-9]+$/.test(raw);
400
+ }
401
+ function parseConfigPath(raw) {
402
+ const trimmed = raw.trim();
403
+ if (!trimmed) {
404
+ return [];
342
405
  }
343
- const hash = hashRaw(raw);
344
- const schema = buildConfigSchema({ version: getPackageVersion(), plugins: plugins2 });
345
- const redacted = redactConfigObject(config2, schema.uiHints);
346
- return { raw: valid ? JSON.stringify(redacted, null, 2) : null, hash: valid ? hash : null, config: config2, redacted, valid };
347
- };
348
- var redactValue = (value, plugins2) => {
349
- const schema = buildConfigSchema({ version: getPackageVersion(), plugins: plugins2 });
350
- return redactConfigObject(value, schema.uiHints);
351
- };
352
- var mergeDeep = (base, patch) => {
353
- const next = { ...base };
354
- for (const [key, value] of Object.entries(patch)) {
355
- if (value && typeof value === "object" && !Array.isArray(value)) {
356
- const baseVal = base[key];
357
- if (baseVal && typeof baseVal === "object" && !Array.isArray(baseVal)) {
358
- next[key] = mergeDeep(baseVal, value);
359
- } else {
360
- next[key] = mergeDeep({}, value);
406
+ const parts = [];
407
+ let current = "";
408
+ let i = 0;
409
+ while (i < trimmed.length) {
410
+ const ch = trimmed[i];
411
+ if (ch === "\\") {
412
+ const next = trimmed[i + 1];
413
+ if (next) {
414
+ current += next;
361
415
  }
362
- } else {
363
- next[key] = value;
416
+ i += 2;
417
+ continue;
364
418
  }
365
- }
366
- return next;
367
- };
368
- var GatewayControllerImpl = class {
369
- constructor(deps) {
370
- this.deps = deps;
371
- }
372
- async requestRestart(options) {
373
- if (this.deps.requestRestart) {
374
- await this.deps.requestRestart(options);
375
- return;
419
+ if (ch === ".") {
420
+ if (current) {
421
+ parts.push(current);
422
+ }
423
+ current = "";
424
+ i += 1;
425
+ continue;
376
426
  }
377
- const delay = typeof options?.delayMs === "number" && Number.isFinite(options.delayMs) ? Math.max(0, options.delayMs) : 100;
378
- console.log(`Gateway restart requested via tool${options?.reason ? ` (${options.reason})` : ""}.`);
379
- setTimeout(() => {
380
- process.exit(0);
381
- }, delay);
427
+ if (ch === "[") {
428
+ if (current) {
429
+ parts.push(current);
430
+ }
431
+ current = "";
432
+ const close = trimmed.indexOf("]", i);
433
+ if (close === -1) {
434
+ throw new Error(`Invalid path (missing "]"): ${raw}`);
435
+ }
436
+ const inside = trimmed.slice(i + 1, close).trim();
437
+ if (!inside) {
438
+ throw new Error(`Invalid path (empty "[]"): ${raw}`);
439
+ }
440
+ parts.push(inside);
441
+ i = close + 1;
442
+ continue;
443
+ }
444
+ current += ch;
445
+ i += 1;
382
446
  }
383
- status() {
384
- return {
385
- channels: this.deps.reloader.getChannels().enabledChannels,
386
- cron: this.deps.cron.status(),
387
- configPath: this.deps.getConfigPath()
388
- };
447
+ if (current) {
448
+ parts.push(current);
389
449
  }
390
- async reloadConfig(reason) {
391
- return this.deps.reloader.reloadConfig(reason);
392
- }
393
- async restart(options) {
394
- await this.requestRestart(options);
395
- return "Restart scheduled";
450
+ return parts.map((part) => part.trim()).filter(Boolean);
451
+ }
452
+ function parseRequiredConfigPath(raw) {
453
+ const parsedPath = parseConfigPath(raw);
454
+ if (parsedPath.length === 0) {
455
+ throw new Error("Path is empty.");
396
456
  }
397
- async getConfig() {
398
- const plugins2 = this.deps.getPluginUiMetadata?.() ?? [];
399
- const snapshot = readConfigSnapshot(this.deps.getConfigPath, plugins2);
400
- return {
401
- raw: snapshot.raw,
402
- hash: snapshot.hash,
403
- path: this.deps.getConfigPath(),
404
- config: snapshot.redacted,
405
- parsed: snapshot.redacted,
406
- resolved: snapshot.redacted,
407
- valid: snapshot.valid
408
- };
457
+ return parsedPath;
458
+ }
459
+ function parseConfigSetValue(raw, opts) {
460
+ const trimmed = raw.trim();
461
+ if (opts.json) {
462
+ return JSON.parse(trimmed);
409
463
  }
410
- async getConfigSchema() {
411
- return buildConfigSchema({ version: getPackageVersion(), plugins: this.deps.getPluginUiMetadata?.() ?? [] });
464
+ try {
465
+ return JSON.parse(trimmed);
466
+ } catch {
467
+ return raw;
412
468
  }
413
- async applyConfig(params) {
414
- const plugins2 = this.deps.getPluginUiMetadata?.() ?? [];
415
- const snapshot = readConfigSnapshot(this.deps.getConfigPath, plugins2);
416
- if (!params.baseHash) {
417
- return { ok: false, error: "config base hash required; re-run config.get and retry" };
469
+ }
470
+ function getAtConfigPath(root, pathSegments) {
471
+ let current = root;
472
+ for (const segment of pathSegments) {
473
+ if (!current || typeof current !== "object") {
474
+ return { found: false };
418
475
  }
419
- if (!snapshot.valid || !snapshot.hash) {
420
- return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
476
+ if (Array.isArray(current)) {
477
+ if (!isIndexSegment(segment)) {
478
+ return { found: false };
479
+ }
480
+ const index = Number.parseInt(segment, 10);
481
+ if (!Number.isFinite(index) || index < 0 || index >= current.length) {
482
+ return { found: false };
483
+ }
484
+ current = current[index];
485
+ continue;
421
486
  }
422
- if (params.baseHash !== snapshot.hash) {
423
- return { ok: false, error: "config changed since last load; re-run config.get and retry" };
487
+ const record = current;
488
+ if (!Object.prototype.hasOwnProperty.call(record, segment)) {
489
+ return { found: false };
424
490
  }
425
- let parsedRaw;
426
- try {
427
- parsedRaw = JSON.parse(params.raw);
428
- } catch {
429
- return { ok: false, error: "invalid JSON in raw config" };
491
+ current = record[segment];
492
+ }
493
+ return { found: true, value: current };
494
+ }
495
+ function setAtConfigPath(root, pathSegments, value) {
496
+ let current = root;
497
+ for (let i = 0; i < pathSegments.length - 1; i += 1) {
498
+ const segment = pathSegments[i];
499
+ const next = pathSegments[i + 1];
500
+ const nextIsIndex = Boolean(next && isIndexSegment(next));
501
+ if (Array.isArray(current)) {
502
+ if (!isIndexSegment(segment)) {
503
+ throw new Error(`Expected numeric index for array segment "${segment}"`);
504
+ }
505
+ const index = Number.parseInt(segment, 10);
506
+ const existing2 = current[index];
507
+ if (!existing2 || typeof existing2 !== "object") {
508
+ current[index] = nextIsIndex ? [] : {};
509
+ }
510
+ current = current[index];
511
+ continue;
430
512
  }
431
- let validated;
432
- try {
433
- validated = ConfigSchema.parse(parsedRaw);
434
- } catch (err) {
435
- return { ok: false, error: `invalid config: ${String(err)}` };
513
+ if (!current || typeof current !== "object") {
514
+ throw new Error(`Cannot traverse into "${segment}" (not an object)`);
436
515
  }
437
- this.deps.saveConfig(validated);
438
- const delayMs = params.restartDelayMs ?? 0;
439
- await this.requestRestart({ delayMs, reason: "config.apply" });
440
- return {
441
- ok: true,
442
- note: params.note ?? null,
443
- path: this.deps.getConfigPath(),
444
- config: redactValue(validated, plugins2),
445
- restart: { scheduled: true, delayMs }
446
- };
447
- }
448
- async patchConfig(params) {
449
- const plugins2 = this.deps.getPluginUiMetadata?.() ?? [];
450
- const snapshot = readConfigSnapshot(this.deps.getConfigPath, plugins2);
451
- if (!params.baseHash) {
452
- return { ok: false, error: "config base hash required; re-run config.get and retry" };
516
+ const record = current;
517
+ const existing = record[segment];
518
+ if (!existing || typeof existing !== "object") {
519
+ record[segment] = nextIsIndex ? [] : {};
453
520
  }
454
- if (!snapshot.valid || !snapshot.hash) {
455
- return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
521
+ current = record[segment];
522
+ }
523
+ const last = pathSegments[pathSegments.length - 1];
524
+ if (Array.isArray(current)) {
525
+ if (!isIndexSegment(last)) {
526
+ throw new Error(`Expected numeric index for array segment "${last}"`);
456
527
  }
457
- if (params.baseHash !== snapshot.hash) {
458
- return { ok: false, error: "config changed since last load; re-run config.get and retry" };
528
+ const index = Number.parseInt(last, 10);
529
+ current[index] = value;
530
+ return;
531
+ }
532
+ if (!current || typeof current !== "object") {
533
+ throw new Error(`Cannot set "${last}" (parent is not an object)`);
534
+ }
535
+ current[last] = value;
536
+ }
537
+ function unsetAtConfigPath(root, pathSegments) {
538
+ let current = root;
539
+ for (let i = 0; i < pathSegments.length - 1; i += 1) {
540
+ const segment = pathSegments[i];
541
+ if (!current || typeof current !== "object") {
542
+ return false;
459
543
  }
460
- let patch;
461
- try {
462
- patch = JSON.parse(params.raw);
463
- } catch {
464
- return { ok: false, error: "invalid JSON in raw config" };
544
+ if (Array.isArray(current)) {
545
+ if (!isIndexSegment(segment)) {
546
+ return false;
547
+ }
548
+ const index = Number.parseInt(segment, 10);
549
+ if (!Number.isFinite(index) || index < 0 || index >= current.length) {
550
+ return false;
551
+ }
552
+ current = current[index];
553
+ continue;
465
554
  }
466
- const merged = mergeDeep(snapshot.config, patch);
467
- let validated;
468
- try {
469
- validated = ConfigSchema.parse(merged);
470
- } catch (err) {
471
- return { ok: false, error: `invalid config: ${String(err)}` };
555
+ const record2 = current;
556
+ if (!Object.prototype.hasOwnProperty.call(record2, segment)) {
557
+ return false;
472
558
  }
473
- this.deps.saveConfig(validated);
474
- const delayMs = params.restartDelayMs ?? 0;
475
- await this.requestRestart({ delayMs, reason: "config.patch" });
476
- return {
477
- ok: true,
478
- note: params.note ?? null,
479
- path: this.deps.getConfigPath(),
480
- config: redactValue(validated, plugins2),
481
- restart: { scheduled: true, delayMs }
482
- };
559
+ current = record2[segment];
483
560
  }
484
- async updateRun(params) {
485
- const result = runSelfUpdate({ timeoutMs: params.timeoutMs });
486
- if (!result.ok) {
487
- return { ok: false, error: result.error ?? "update failed", steps: result.steps };
561
+ const last = pathSegments[pathSegments.length - 1];
562
+ if (Array.isArray(current)) {
563
+ if (!isIndexSegment(last)) {
564
+ return false;
488
565
  }
489
- const delayMs = params.restartDelayMs ?? 0;
490
- await this.requestRestart({ delayMs, reason: "update.run" });
491
- return {
492
- ok: true,
493
- note: params.note ?? null,
494
- restart: { scheduled: true, delayMs },
495
- strategy: result.strategy,
496
- steps: result.steps
497
- };
566
+ const index = Number.parseInt(last, 10);
567
+ if (!Number.isFinite(index) || index < 0 || index >= current.length) {
568
+ return false;
569
+ }
570
+ current.splice(index, 1);
571
+ return true;
498
572
  }
499
- };
573
+ if (!current || typeof current !== "object") {
574
+ return false;
575
+ }
576
+ const record = current;
577
+ if (!Object.prototype.hasOwnProperty.call(record, last)) {
578
+ return false;
579
+ }
580
+ delete record[last];
581
+ return true;
582
+ }
500
583
 
501
- // src/cli/restart-coordinator.ts
502
- var RestartCoordinator = class {
584
+ // src/cli/commands/config.ts
585
+ var ConfigCommands = class {
503
586
  constructor(deps) {
504
587
  this.deps = deps;
505
588
  }
506
- restartingService = false;
507
- exitScheduled = false;
508
- async requestRestart(request) {
509
- const reason = request.reason.trim() || "config changed";
510
- const strategy = request.strategy ?? "background-service-or-manual";
511
- if (strategy !== "exit-process") {
512
- const state = this.deps.readServiceState();
513
- const serviceRunning = Boolean(state && this.deps.isProcessRunning(state.pid));
514
- const managedByCurrentProcess = Boolean(state && state.pid === this.deps.currentPid());
515
- if (serviceRunning && !managedByCurrentProcess) {
516
- if (this.restartingService) {
517
- return {
518
- status: "restart-in-progress",
519
- message: "Restart already in progress; skipping duplicate request."
520
- };
521
- }
522
- this.restartingService = true;
523
- try {
524
- const restarted = await this.deps.restartBackgroundService(reason);
525
- if (restarted) {
526
- return {
527
- status: "service-restarted",
528
- message: `Restarted background service to apply changes (${reason}).`
529
- };
530
- }
531
- } finally {
532
- this.restartingService = false;
533
- }
534
- }
535
- }
536
- if (strategy === "background-service-or-exit" || strategy === "exit-process") {
537
- if (this.exitScheduled) {
538
- return {
539
- status: "exit-scheduled",
540
- message: "Restart already scheduled; skipping duplicate request."
541
- };
542
- }
543
- const delay = typeof request.delayMs === "number" && Number.isFinite(request.delayMs) ? Math.max(0, Math.floor(request.delayMs)) : 100;
544
- this.exitScheduled = true;
545
- this.deps.scheduleProcessExit(delay, reason);
546
- return {
547
- status: "exit-scheduled",
548
- message: `Restart scheduled (${reason}).`
549
- };
550
- }
551
- return {
552
- status: "manual-required",
553
- message: request.manualMessage ?? "Restart the gateway to apply changes."
554
- };
555
- }
556
- };
557
-
558
- // src/cli/skills/clawhub.ts
559
- import { spawnSync as spawnSync2 } from "child_process";
560
- import { existsSync as existsSync3 } from "fs";
561
- import { isAbsolute, join as join2, resolve as resolve3 } from "path";
562
- async function installClawHubSkill(options) {
563
- const slug = options.slug.trim();
564
- if (!slug) {
565
- throw new Error("Skill slug is required.");
566
- }
567
- const workdir = resolve3(options.workdir);
568
- if (!existsSync3(workdir)) {
569
- throw new Error(`Workdir does not exist: ${workdir}`);
570
- }
571
- const dirName = options.dir?.trim() || "skills";
572
- const destinationDir = isAbsolute(dirName) ? resolve3(dirName, slug) : resolve3(workdir, dirName, slug);
573
- const skillFile = join2(destinationDir, "SKILL.md");
574
- if (!options.force && existsSync3(destinationDir)) {
575
- if (existsSync3(skillFile)) {
576
- return {
577
- slug,
578
- version: options.version,
579
- registry: options.registry,
580
- destinationDir,
581
- alreadyInstalled: true
582
- };
583
- }
584
- throw new Error(`Skill directory already exists: ${destinationDir} (use --force)`);
585
- }
586
- const args = buildClawHubArgs(slug, options);
587
- const result = spawnSync2("npx", args, {
588
- cwd: workdir,
589
- stdio: "pipe",
590
- env: process.env
591
- });
592
- if (result.error) {
593
- throw new Error(`Failed to run npx clawhub: ${String(result.error)}`);
594
- }
595
- if (result.status !== 0) {
596
- const stdout = result.stdout ? String(result.stdout).trim() : "";
597
- const stderr = result.stderr ? String(result.stderr).trim() : "";
598
- const details = [stderr, stdout].filter(Boolean).join("\n");
599
- throw new Error(details || `clawhub install failed with code ${result.status ?? 1}`);
600
- }
601
- return {
602
- slug,
603
- version: options.version,
604
- registry: options.registry,
605
- destinationDir
606
- };
607
- }
608
- function buildClawHubArgs(slug, options) {
609
- const args = ["--yes", "clawhub", "install", slug];
610
- if (options.version) {
611
- args.push("--version", options.version);
612
- }
613
- if (options.registry) {
614
- args.push("--registry", options.registry);
615
- }
616
- if (options.workdir) {
617
- args.push("--workdir", options.workdir);
618
- }
619
- if (options.dir) {
620
- args.push("--dir", options.dir);
621
- }
622
- if (options.force) {
623
- args.push("--force");
624
- }
625
- return args;
626
- }
627
-
628
- // src/cli/runtime.ts
629
- var LOGO = "\u{1F916}";
630
- var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "/exit", "/quit", ":q"]);
631
- var FORCED_PUBLIC_UI_HOST = "0.0.0.0";
632
- function isIndexSegment(raw) {
633
- return /^[0-9]+$/.test(raw);
634
- }
635
- function parseConfigPath(raw) {
636
- const trimmed = raw.trim();
637
- if (!trimmed) {
638
- return [];
639
- }
640
- const parts = [];
641
- let current = "";
642
- let i = 0;
643
- while (i < trimmed.length) {
644
- const ch = trimmed[i];
645
- if (ch === "\\") {
646
- const next = trimmed[i + 1];
647
- if (next) {
648
- current += next;
649
- }
650
- i += 2;
651
- continue;
652
- }
653
- if (ch === ".") {
654
- if (current) {
655
- parts.push(current);
656
- }
657
- current = "";
658
- i += 1;
659
- continue;
660
- }
661
- if (ch === "[") {
662
- if (current) {
663
- parts.push(current);
664
- }
665
- current = "";
666
- const close = trimmed.indexOf("]", i);
667
- if (close === -1) {
668
- throw new Error(`Invalid path (missing "]"): ${raw}`);
669
- }
670
- const inside = trimmed.slice(i + 1, close).trim();
671
- if (!inside) {
672
- throw new Error(`Invalid path (empty "[]"): ${raw}`);
673
- }
674
- parts.push(inside);
675
- i = close + 1;
676
- continue;
677
- }
678
- current += ch;
679
- i += 1;
680
- }
681
- if (current) {
682
- parts.push(current);
683
- }
684
- return parts.map((part) => part.trim()).filter(Boolean);
685
- }
686
- function parseRequiredConfigPath(raw) {
687
- const parsedPath = parseConfigPath(raw);
688
- if (parsedPath.length === 0) {
689
- throw new Error("Path is empty.");
690
- }
691
- return parsedPath;
692
- }
693
- function parseConfigSetValue(raw, opts) {
694
- const trimmed = raw.trim();
695
- if (opts.json) {
696
- return JSON.parse(trimmed);
697
- }
698
- try {
699
- return JSON.parse(trimmed);
700
- } catch {
701
- return raw;
702
- }
703
- }
704
- function getAtConfigPath(root, pathSegments) {
705
- let current = root;
706
- for (const segment of pathSegments) {
707
- if (!current || typeof current !== "object") {
708
- return { found: false };
709
- }
710
- if (Array.isArray(current)) {
711
- if (!isIndexSegment(segment)) {
712
- return { found: false };
713
- }
714
- const index = Number.parseInt(segment, 10);
715
- if (!Number.isFinite(index) || index < 0 || index >= current.length) {
716
- return { found: false };
717
- }
718
- current = current[index];
719
- continue;
720
- }
721
- const record = current;
722
- if (!Object.prototype.hasOwnProperty.call(record, segment)) {
723
- return { found: false };
724
- }
725
- current = record[segment];
726
- }
727
- return { found: true, value: current };
728
- }
729
- function setAtConfigPath(root, pathSegments, value) {
730
- let current = root;
731
- for (let i = 0; i < pathSegments.length - 1; i += 1) {
732
- const segment = pathSegments[i];
733
- const next = pathSegments[i + 1];
734
- const nextIsIndex = Boolean(next && isIndexSegment(next));
735
- if (Array.isArray(current)) {
736
- if (!isIndexSegment(segment)) {
737
- throw new Error(`Expected numeric index for array segment "${segment}"`);
738
- }
739
- const index = Number.parseInt(segment, 10);
740
- const existing2 = current[index];
741
- if (!existing2 || typeof existing2 !== "object") {
742
- current[index] = nextIsIndex ? [] : {};
743
- }
744
- current = current[index];
745
- continue;
746
- }
747
- if (!current || typeof current !== "object") {
748
- throw new Error(`Cannot traverse into "${segment}" (not an object)`);
749
- }
750
- const record = current;
751
- const existing = record[segment];
752
- if (!existing || typeof existing !== "object") {
753
- record[segment] = nextIsIndex ? [] : {};
754
- }
755
- current = record[segment];
756
- }
757
- const last = pathSegments[pathSegments.length - 1];
758
- if (Array.isArray(current)) {
759
- if (!isIndexSegment(last)) {
760
- throw new Error(`Expected numeric index for array segment "${last}"`);
761
- }
762
- const index = Number.parseInt(last, 10);
763
- current[index] = value;
764
- return;
765
- }
766
- if (!current || typeof current !== "object") {
767
- throw new Error(`Cannot set "${last}" (parent is not an object)`);
768
- }
769
- current[last] = value;
770
- }
771
- function unsetAtConfigPath(root, pathSegments) {
772
- let current = root;
773
- for (let i = 0; i < pathSegments.length - 1; i += 1) {
774
- const segment = pathSegments[i];
775
- if (!current || typeof current !== "object") {
776
- return false;
777
- }
778
- if (Array.isArray(current)) {
779
- if (!isIndexSegment(segment)) {
780
- return false;
781
- }
782
- const index = Number.parseInt(segment, 10);
783
- if (!Number.isFinite(index) || index < 0 || index >= current.length) {
784
- return false;
785
- }
786
- current = current[index];
787
- continue;
788
- }
789
- const record2 = current;
790
- if (!Object.prototype.hasOwnProperty.call(record2, segment)) {
791
- return false;
792
- }
793
- current = record2[segment];
794
- }
795
- const last = pathSegments[pathSegments.length - 1];
796
- if (Array.isArray(current)) {
797
- if (!isIndexSegment(last)) {
798
- return false;
799
- }
800
- const index = Number.parseInt(last, 10);
801
- if (!Number.isFinite(index) || index < 0 || index >= current.length) {
802
- return false;
803
- }
804
- current.splice(index, 1);
805
- return true;
806
- }
807
- if (!current || typeof current !== "object") {
808
- return false;
809
- }
810
- const record = current;
811
- if (!Object.prototype.hasOwnProperty.call(record, last)) {
812
- return false;
813
- }
814
- delete record[last];
815
- return true;
816
- }
817
- var MissingProvider = class extends LLMProvider {
818
- constructor(defaultModel) {
819
- super(null, null);
820
- this.defaultModel = defaultModel;
821
- }
822
- setDefaultModel(model) {
823
- this.defaultModel = model;
824
- }
825
- async chat() {
826
- throw new Error("No API key configured yet. Configure provider credentials in UI and retry.");
827
- }
828
- getDefaultModel() {
829
- return this.defaultModel;
830
- }
831
- };
832
- var ConfigReloader = class {
833
- constructor(options) {
834
- this.options = options;
835
- this.currentConfig = options.initialConfig;
836
- this.channels = options.channels;
837
- }
838
- currentConfig;
839
- channels;
840
- reloadTask = null;
841
- providerReloadTask = null;
842
- reloadTimer = null;
843
- reloadRunning = false;
844
- reloadPending = false;
845
- getChannels() {
846
- return this.channels;
847
- }
848
- setApplyAgentRuntimeConfig(callback) {
849
- this.options.applyAgentRuntimeConfig = callback;
850
- }
851
- async applyReloadPlan(nextConfig) {
852
- const changedPaths = diffConfigPaths(this.currentConfig, nextConfig);
853
- if (!changedPaths.length) {
854
- return;
855
- }
856
- this.currentConfig = nextConfig;
857
- const plan = buildReloadPlan(changedPaths);
858
- if (plan.restartChannels) {
859
- await this.reloadChannels(nextConfig);
860
- console.log("Config reload: channels restarted.");
861
- }
862
- if (plan.reloadProviders) {
863
- await this.reloadProvider(nextConfig);
864
- console.log("Config reload: provider settings applied.");
865
- }
866
- if (plan.reloadAgent) {
867
- this.options.applyAgentRuntimeConfig?.(nextConfig);
868
- console.log("Config reload: agent defaults applied.");
869
- }
870
- if (plan.restartRequired.length > 0) {
871
- this.options.onRestartRequired(plan.restartRequired);
872
- }
873
- }
874
- scheduleReload(reason) {
875
- if (this.reloadTimer) {
876
- clearTimeout(this.reloadTimer);
877
- }
878
- this.reloadTimer = setTimeout(() => {
879
- void this.runReload(reason);
880
- }, 300);
881
- }
882
- async runReload(reason) {
883
- if (this.reloadRunning) {
884
- this.reloadPending = true;
885
- return;
886
- }
887
- this.reloadRunning = true;
888
- if (this.reloadTimer) {
889
- clearTimeout(this.reloadTimer);
890
- this.reloadTimer = null;
891
- }
892
- try {
893
- const nextConfig = this.options.loadConfig();
894
- await this.applyReloadPlan(nextConfig);
895
- } catch (error) {
896
- console.error(`Config reload failed (${reason}): ${String(error)}`);
897
- } finally {
898
- this.reloadRunning = false;
899
- if (this.reloadPending) {
900
- this.reloadPending = false;
901
- this.scheduleReload("pending");
902
- }
903
- }
904
- }
905
- async reloadConfig(reason) {
906
- await this.runReload(reason ?? "gateway tool");
907
- return "Config reload triggered";
908
- }
909
- async reloadChannels(nextConfig) {
910
- if (this.reloadTask) {
911
- await this.reloadTask;
912
- return;
913
- }
914
- this.reloadTask = (async () => {
915
- await this.channels.stopAll();
916
- this.channels = new ChannelManager(
917
- nextConfig,
918
- this.options.bus,
919
- this.options.sessionManager,
920
- this.options.getExtensionChannels?.() ?? []
921
- );
922
- await this.channels.startAll();
923
- })();
924
- try {
925
- await this.reloadTask;
926
- } finally {
927
- this.reloadTask = null;
928
- }
929
- }
930
- async reloadProvider(nextConfig) {
931
- if (!this.options.providerManager) {
932
- return;
933
- }
934
- if (this.providerReloadTask) {
935
- await this.providerReloadTask;
936
- return;
937
- }
938
- this.providerReloadTask = (async () => {
939
- const nextProvider = this.options.makeProvider(nextConfig);
940
- if (!nextProvider) {
941
- console.warn("Provider reload skipped: missing API key.");
942
- return;
943
- }
944
- this.options.providerManager?.set(nextProvider);
945
- })();
946
- try {
947
- await this.providerReloadTask;
948
- } finally {
949
- this.providerReloadTask = null;
950
- }
951
- }
952
- };
953
- var CliRuntime = class {
954
- logo;
955
- restartCoordinator;
956
- serviceRestartTask = null;
957
- selfRelaunchArmed = false;
958
- constructor(options = {}) {
959
- this.logo = options.logo ?? LOGO;
960
- this.restartCoordinator = new RestartCoordinator({
961
- readServiceState,
962
- isProcessRunning,
963
- currentPid: () => process.pid,
964
- restartBackgroundService: async (reason) => this.restartBackgroundService(reason),
965
- scheduleProcessExit: (delayMs, reason) => this.scheduleProcessExit(delayMs, reason)
966
- });
967
- }
968
- get version() {
969
- return getPackageVersion();
970
- }
971
- scheduleProcessExit(delayMs, reason) {
972
- console.warn(`Gateway restart requested (${reason}).`);
973
- setTimeout(() => {
974
- process.exit(0);
975
- }, delayMs);
976
- }
977
- async restartBackgroundService(reason) {
978
- if (this.serviceRestartTask) {
979
- return this.serviceRestartTask;
980
- }
981
- this.serviceRestartTask = (async () => {
982
- const state = readServiceState();
983
- if (!state || !isProcessRunning(state.pid) || state.pid === process.pid) {
984
- return false;
985
- }
986
- const uiHost = FORCED_PUBLIC_UI_HOST;
987
- const uiPort = typeof state.uiPort === "number" && Number.isFinite(state.uiPort) ? state.uiPort : 18791;
988
- console.log(`Applying changes (${reason}): restarting ${APP_NAME} background service...`);
989
- await this.stopService();
990
- await this.startService({
991
- uiOverrides: {
992
- enabled: true,
993
- host: uiHost,
994
- port: uiPort
995
- },
996
- open: false
997
- });
998
- return true;
999
- })();
1000
- try {
1001
- return await this.serviceRestartTask;
1002
- } finally {
1003
- this.serviceRestartTask = null;
1004
- }
1005
- }
1006
- armManagedServiceRelaunch(params) {
1007
- const strategy = params.strategy ?? "background-service-or-manual";
1008
- if (strategy !== "background-service-or-exit" && strategy !== "exit-process") {
1009
- return;
1010
- }
1011
- if (this.selfRelaunchArmed) {
1012
- return;
1013
- }
1014
- const state = readServiceState();
1015
- if (!state || state.pid !== process.pid) {
1016
- return;
1017
- }
1018
- const uiPort = typeof state.uiPort === "number" && Number.isFinite(state.uiPort) ? state.uiPort : 18791;
1019
- const delayMs = typeof params.delayMs === "number" && Number.isFinite(params.delayMs) ? Math.max(0, Math.floor(params.delayMs)) : 100;
1020
- const cliPath = process.env.NEXTCLAW_SELF_RELAUNCH_CLI?.trim() || fileURLToPath2(new URL("./index.js", import.meta.url));
1021
- const startArgs = [cliPath, "start", "--ui-port", String(uiPort)];
1022
- const serviceStatePath = resolve4(getDataDir2(), "run", "service.json");
1023
- const helperScript = [
1024
- 'const { spawnSync } = require("node:child_process");',
1025
- 'const { readFileSync } = require("node:fs");',
1026
- `const parentPid = ${process.pid};`,
1027
- `const delayMs = ${delayMs};`,
1028
- "const maxWaitMs = 120000;",
1029
- "const retryIntervalMs = 1000;",
1030
- "const startTimeoutMs = 60000;",
1031
- `const nodePath = ${JSON.stringify(process.execPath)};`,
1032
- `const startArgs = ${JSON.stringify(startArgs)};`,
1033
- `const serviceStatePath = ${JSON.stringify(serviceStatePath)};`,
1034
- "function isRunning(pid) {",
1035
- " try {",
1036
- " process.kill(pid, 0);",
1037
- " return true;",
1038
- " } catch {",
1039
- " return false;",
1040
- " }",
1041
- "}",
1042
- "function hasReplacementService() {",
1043
- " try {",
1044
- ' const raw = readFileSync(serviceStatePath, "utf-8");',
1045
- " const state = JSON.parse(raw);",
1046
- " const pid = Number(state?.pid);",
1047
- " return Number.isFinite(pid) && pid > 0 && pid !== parentPid && isRunning(pid);",
1048
- " } catch {",
1049
- " return false;",
1050
- " }",
1051
- "}",
1052
- "function tryStart() {",
1053
- " spawnSync(nodePath, startArgs, {",
1054
- ' stdio: "ignore",',
1055
- " env: process.env,",
1056
- " timeout: startTimeoutMs",
1057
- " });",
1058
- "}",
1059
- "setTimeout(() => {",
1060
- " const startedAt = Date.now();",
1061
- " const tick = () => {",
1062
- " if (hasReplacementService()) {",
1063
- " process.exit(0);",
1064
- " return;",
1065
- " }",
1066
- " if (Date.now() - startedAt >= maxWaitMs) {",
1067
- " process.exit(0);",
1068
- " return;",
1069
- " }",
1070
- " tryStart();",
1071
- " if (hasReplacementService()) {",
1072
- " process.exit(0);",
1073
- " return;",
1074
- " }",
1075
- " setTimeout(tick, retryIntervalMs);",
1076
- " };",
1077
- " tick();",
1078
- "}, delayMs);"
1079
- ].join("\n");
1080
- try {
1081
- const helper = spawn2(process.execPath, ["-e", helperScript], {
1082
- detached: true,
1083
- stdio: "ignore",
1084
- env: process.env
1085
- });
1086
- helper.unref();
1087
- this.selfRelaunchArmed = true;
1088
- console.warn(`Gateway self-restart armed (${params.reason}).`);
1089
- } catch (error) {
1090
- console.error(`Failed to arm gateway self-restart: ${String(error)}`);
1091
- }
1092
- }
1093
- async requestRestart(params) {
1094
- this.armManagedServiceRelaunch({
1095
- reason: params.reason,
1096
- strategy: params.strategy,
1097
- delayMs: params.delayMs
1098
- });
1099
- const result = await this.restartCoordinator.requestRestart({
1100
- reason: params.reason,
1101
- strategy: params.strategy,
1102
- delayMs: params.delayMs,
1103
- manualMessage: params.manualMessage
1104
- });
1105
- if (result.status === "manual-required" || result.status === "restart-in-progress") {
1106
- console.log(result.message);
1107
- return;
1108
- }
1109
- if (result.status === "service-restarted") {
1110
- if (!params.silentOnServiceRestart) {
1111
- console.log(result.message);
1112
- }
1113
- return;
1114
- }
1115
- console.warn(result.message);
1116
- }
1117
- async onboard() {
1118
- console.warn(`Warning: ${APP_NAME} onboard is deprecated. Use "${APP_NAME} init" instead.`);
1119
- await this.init({ source: "onboard" });
1120
- }
1121
- async init(options = {}) {
1122
- const source = options.source ?? "init";
1123
- const prefix = options.auto ? "Auto init" : "Init";
1124
- const force = Boolean(options.force);
1125
- const configPath = getConfigPath();
1126
- let createdConfig = false;
1127
- if (!existsSync4(configPath)) {
1128
- const config3 = ConfigSchema2.parse({});
1129
- saveConfig(config3);
1130
- createdConfig = true;
1131
- }
1132
- const config2 = loadConfig();
1133
- const workspaceSetting = config2.agents.defaults.workspace;
1134
- const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ? join3(getDataDir2(), DEFAULT_WORKSPACE_DIR) : expandHome(workspaceSetting);
1135
- const workspaceExisted = existsSync4(workspacePath);
1136
- mkdirSync2(workspacePath, { recursive: true });
1137
- const templateResult = this.createWorkspaceTemplates(workspacePath, { force });
1138
- if (createdConfig) {
1139
- console.log(`\u2713 ${prefix}: created config at ${configPath}`);
1140
- }
1141
- if (!workspaceExisted) {
1142
- console.log(`\u2713 ${prefix}: created workspace at ${workspacePath}`);
1143
- }
1144
- for (const file of templateResult.created) {
1145
- console.log(`\u2713 ${prefix}: created ${file}`);
1146
- }
1147
- if (!createdConfig && workspaceExisted && templateResult.created.length === 0) {
1148
- console.log(`${prefix}: already initialized.`);
1149
- }
1150
- if (!options.auto) {
1151
- console.log(`
1152
- ${this.logo} ${APP_NAME} is ready! (${source})`);
1153
- console.log("\nNext steps:");
1154
- console.log(` 1. Add your API key to ${configPath}`);
1155
- console.log(` 2. Chat: ${APP_NAME} agent -m "Hello!"`);
1156
- } else {
1157
- console.log(`Tip: Run "${APP_NAME} init${force ? " --force" : ""}" to re-run initialization if needed.`);
1158
- }
1159
- }
1160
- async gateway(opts) {
1161
- const uiOverrides = {
1162
- host: FORCED_PUBLIC_UI_HOST
1163
- };
1164
- if (opts.ui) {
1165
- uiOverrides.enabled = true;
1166
- }
1167
- if (opts.uiPort) {
1168
- uiOverrides.port = Number(opts.uiPort);
1169
- }
1170
- if (opts.uiOpen) {
1171
- uiOverrides.open = true;
1172
- }
1173
- await this.startGateway({ uiOverrides });
1174
- }
1175
- async ui(opts) {
1176
- const uiOverrides = {
1177
- enabled: true,
1178
- host: FORCED_PUBLIC_UI_HOST,
1179
- open: Boolean(opts.open)
1180
- };
1181
- if (opts.port) {
1182
- uiOverrides.port = Number(opts.port);
1183
- }
1184
- await this.startGateway({ uiOverrides, allowMissingProvider: true });
1185
- }
1186
- async start(opts) {
1187
- await this.init({ source: "start", auto: true });
1188
- const uiOverrides = {
1189
- enabled: true,
1190
- host: FORCED_PUBLIC_UI_HOST,
1191
- open: false
1192
- };
1193
- if (opts.uiPort) {
1194
- uiOverrides.port = Number(opts.uiPort);
1195
- }
1196
- await this.startService({
1197
- uiOverrides,
1198
- open: Boolean(opts.open)
1199
- });
1200
- }
1201
- async restart(opts) {
1202
- const state = readServiceState();
1203
- if (state && isProcessRunning(state.pid)) {
1204
- console.log(`Restarting ${APP_NAME}...`);
1205
- await this.stopService();
1206
- } else if (state) {
1207
- clearServiceState();
1208
- console.log("Service state was stale and has been cleaned up.");
1209
- } else {
1210
- console.log("No running service found. Starting a new service.");
1211
- }
1212
- await this.start(opts);
1213
- }
1214
- async serve(opts) {
1215
- const uiOverrides = {
1216
- enabled: true,
1217
- host: FORCED_PUBLIC_UI_HOST,
1218
- open: false
1219
- };
1220
- if (opts.uiPort) {
1221
- uiOverrides.port = Number(opts.uiPort);
1222
- }
1223
- await this.runForeground({
1224
- uiOverrides,
1225
- open: Boolean(opts.open)
1226
- });
1227
- }
1228
- async stop() {
1229
- await this.stopService();
1230
- }
1231
- async agent(opts) {
1232
- const config2 = loadConfig();
1233
- const workspace = getWorkspacePath(config2.agents.defaults.workspace);
1234
- const pluginRegistry = this.loadPluginRegistry(config2, workspace);
1235
- const extensionRegistry = this.toExtensionRegistry(pluginRegistry);
1236
- this.logPluginDiagnostics(pluginRegistry);
1237
- const bus = new MessageBus();
1238
- const provider = this.makeProvider(config2);
1239
- const providerManager = new ProviderManager(provider);
1240
- const agentLoop = new AgentLoop({
1241
- bus,
1242
- providerManager,
1243
- workspace,
1244
- model: config2.agents.defaults.model,
1245
- maxIterations: config2.agents.defaults.maxToolIterations,
1246
- maxTokens: config2.agents.defaults.maxTokens,
1247
- temperature: config2.agents.defaults.temperature,
1248
- braveApiKey: config2.tools.web.search.apiKey || void 0,
1249
- execConfig: config2.tools.exec,
1250
- restrictToWorkspace: config2.tools.restrictToWorkspace,
1251
- contextConfig: config2.agents.context,
1252
- config: config2,
1253
- extensionRegistry,
1254
- resolveMessageToolHints: ({ channel, accountId }) => resolvePluginChannelMessageToolHints({
1255
- registry: pluginRegistry,
1256
- channel,
1257
- cfg: loadConfig(),
1258
- accountId
1259
- })
1260
- });
1261
- if (opts.message) {
1262
- const response = await agentLoop.processDirect({
1263
- content: opts.message,
1264
- sessionKey: opts.session ?? "cli:default",
1265
- channel: "cli",
1266
- chatId: "direct"
1267
- });
1268
- printAgentResponse(response);
1269
- return;
1270
- }
1271
- console.log(`${this.logo} Interactive mode (type exit or Ctrl+C to quit)
1272
- `);
1273
- const historyFile = join3(getDataDir2(), "history", "cli_history");
1274
- const historyDir = resolve4(historyFile, "..");
1275
- mkdirSync2(historyDir, { recursive: true });
1276
- const history = existsSync4(historyFile) ? readFileSync3(historyFile, "utf-8").split("\n").filter(Boolean) : [];
1277
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1278
- rl.on("close", () => {
1279
- const merged = history.concat(rl.history ?? []);
1280
- writeFileSync2(historyFile, merged.join("\n"));
1281
- process.exit(0);
1282
- });
1283
- let running = true;
1284
- while (running) {
1285
- const line = await prompt(rl, "You: ");
1286
- const trimmed = line.trim();
1287
- if (!trimmed) {
1288
- continue;
1289
- }
1290
- if (EXIT_COMMANDS.has(trimmed.toLowerCase())) {
1291
- rl.close();
1292
- running = false;
1293
- break;
1294
- }
1295
- const response = await agentLoop.processDirect({
1296
- content: trimmed,
1297
- sessionKey: opts.session ?? "cli:default"
1298
- });
1299
- printAgentResponse(response);
1300
- }
1301
- }
1302
- async update(opts) {
1303
- let timeoutMs;
1304
- if (opts.timeout !== void 0) {
1305
- const parsed = Number(opts.timeout);
1306
- if (!Number.isFinite(parsed) || parsed <= 0) {
1307
- console.error("Invalid --timeout value. Provide milliseconds (e.g. 1200000).");
1308
- process.exit(1);
1309
- }
1310
- timeoutMs = parsed;
1311
- }
1312
- const result = runSelfUpdate({ timeoutMs, cwd: process.cwd() });
1313
- const printSteps = () => {
1314
- for (const step of result.steps) {
1315
- console.log(`- ${step.cmd} ${step.args.join(" ")} (code ${step.code ?? "?"})`);
1316
- if (step.stderr) {
1317
- console.log(` stderr: ${step.stderr}`);
1318
- }
1319
- if (step.stdout) {
1320
- console.log(` stdout: ${step.stdout}`);
1321
- }
1322
- }
1323
- };
1324
- if (!result.ok) {
1325
- console.error(`Update failed: ${result.error ?? "unknown error"}`);
1326
- if (result.steps.length > 0) {
1327
- printSteps();
1328
- }
1329
- process.exit(1);
1330
- }
1331
- console.log(`\u2713 Update complete (${result.strategy})`);
1332
- const state = readServiceState();
1333
- if (state && isProcessRunning(state.pid)) {
1334
- console.log(`Tip: restart ${APP_NAME} to apply the update.`);
1335
- }
1336
- }
1337
- pluginsList(opts = {}) {
1338
- const config2 = loadConfig();
1339
- const workspaceDir = getWorkspacePath(config2.agents.defaults.workspace);
1340
- const report = buildPluginStatusReport({
1341
- config: config2,
1342
- workspaceDir,
1343
- reservedChannelIds: Object.keys(config2.channels),
1344
- reservedProviderIds: PROVIDERS.map((provider) => provider.name)
1345
- });
1346
- const list = opts.enabled ? report.plugins.filter((plugin) => plugin.status === "loaded") : report.plugins;
1347
- if (opts.json) {
1348
- console.log(
1349
- JSON.stringify(
1350
- {
1351
- workspaceDir,
1352
- plugins: list,
1353
- diagnostics: report.diagnostics
1354
- },
1355
- null,
1356
- 2
1357
- )
1358
- );
1359
- return;
1360
- }
1361
- if (list.length === 0) {
1362
- console.log("No plugins discovered.");
1363
- return;
1364
- }
1365
- for (const plugin of list) {
1366
- const status = plugin.status === "loaded" ? "loaded" : plugin.status === "disabled" ? "disabled" : "error";
1367
- const title = plugin.name && plugin.name !== plugin.id ? `${plugin.name} (${plugin.id})` : plugin.id;
1368
- if (!opts.verbose) {
1369
- const desc = plugin.description ? plugin.description.length > 80 ? `${plugin.description.slice(0, 77)}...` : plugin.description : "(no description)";
1370
- console.log(`${title} ${status} - ${desc}`);
1371
- continue;
1372
- }
1373
- console.log(`${title} ${status}`);
1374
- console.log(` source: ${plugin.source}`);
1375
- console.log(` origin: ${plugin.origin}`);
1376
- if (plugin.version) {
1377
- console.log(` version: ${plugin.version}`);
1378
- }
1379
- if (plugin.toolNames.length > 0) {
1380
- console.log(` tools: ${plugin.toolNames.join(", ")}`);
1381
- }
1382
- if (plugin.channelIds.length > 0) {
1383
- console.log(` channels: ${plugin.channelIds.join(", ")}`);
1384
- }
1385
- if (plugin.providerIds.length > 0) {
1386
- console.log(` providers: ${plugin.providerIds.join(", ")}`);
1387
- }
1388
- if (plugin.error) {
1389
- console.log(` error: ${plugin.error}`);
1390
- }
1391
- console.log("");
1392
- }
1393
- }
1394
- pluginsInfo(id, opts = {}) {
1395
- const config2 = loadConfig();
1396
- const workspaceDir = getWorkspacePath(config2.agents.defaults.workspace);
1397
- const report = buildPluginStatusReport({
1398
- config: config2,
1399
- workspaceDir,
1400
- reservedChannelIds: Object.keys(config2.channels),
1401
- reservedProviderIds: PROVIDERS.map((provider) => provider.name)
1402
- });
1403
- const plugin = report.plugins.find((entry) => entry.id === id || entry.name === id);
1404
- if (!plugin) {
1405
- console.error(`Plugin not found: ${id}`);
1406
- process.exit(1);
1407
- }
1408
- if (opts.json) {
1409
- console.log(JSON.stringify(plugin, null, 2));
1410
- return;
1411
- }
1412
- const install = config2.plugins.installs?.[plugin.id];
1413
- const lines = [];
1414
- lines.push(plugin.name || plugin.id);
1415
- if (plugin.name && plugin.name !== plugin.id) {
1416
- lines.push(`id: ${plugin.id}`);
1417
- }
1418
- if (plugin.description) {
1419
- lines.push(plugin.description);
1420
- }
1421
- lines.push("");
1422
- lines.push(`Status: ${plugin.status}`);
1423
- lines.push(`Source: ${plugin.source}`);
1424
- lines.push(`Origin: ${plugin.origin}`);
1425
- if (plugin.version) {
1426
- lines.push(`Version: ${plugin.version}`);
1427
- }
1428
- if (plugin.toolNames.length > 0) {
1429
- lines.push(`Tools: ${plugin.toolNames.join(", ")}`);
1430
- }
1431
- if (plugin.channelIds.length > 0) {
1432
- lines.push(`Channels: ${plugin.channelIds.join(", ")}`);
1433
- }
1434
- if (plugin.providerIds.length > 0) {
1435
- lines.push(`Providers: ${plugin.providerIds.join(", ")}`);
1436
- }
1437
- if (plugin.error) {
1438
- lines.push(`Error: ${plugin.error}`);
1439
- }
1440
- if (install) {
1441
- lines.push("");
1442
- lines.push(`Install: ${install.source}`);
1443
- if (install.spec) {
1444
- lines.push(`Spec: ${install.spec}`);
1445
- }
1446
- if (install.sourcePath) {
1447
- lines.push(`Source path: ${install.sourcePath}`);
1448
- }
1449
- if (install.installPath) {
1450
- lines.push(`Install path: ${install.installPath}`);
1451
- }
1452
- if (install.version) {
1453
- lines.push(`Recorded version: ${install.version}`);
1454
- }
1455
- if (install.installedAt) {
1456
- lines.push(`Installed at: ${install.installedAt}`);
1457
- }
1458
- }
1459
- console.log(lines.join("\n"));
1460
- }
1461
- configGet(pathExpr, opts = {}) {
1462
- const config2 = loadConfig();
1463
- let parsedPath;
1464
- try {
1465
- parsedPath = parseRequiredConfigPath(pathExpr);
1466
- } catch (error) {
1467
- console.error(String(error));
1468
- process.exit(1);
1469
- return;
589
+ configGet(pathExpr, opts = {}) {
590
+ const config2 = loadConfig();
591
+ let parsedPath;
592
+ try {
593
+ parsedPath = parseRequiredConfigPath(pathExpr);
594
+ } catch (error) {
595
+ console.error(String(error));
596
+ process.exit(1);
597
+ return;
1470
598
  }
1471
599
  const result = getAtConfigPath(config2, parsedPath);
1472
600
  if (!result.found) {
@@ -1552,874 +680,831 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
1552
680
  if (plan.restartRequired.length === 0) {
1553
681
  return;
1554
682
  }
1555
- await this.requestRestart({
683
+ await this.deps.requestRestart({
1556
684
  reason: `${params.reason} (${plan.restartRequired.join(", ")})`,
1557
685
  manualMessage: params.manualMessage
1558
686
  });
1559
687
  }
1560
- async pluginsEnable(id) {
1561
- const config2 = loadConfig();
1562
- const next = enablePluginInConfig(config2, id);
1563
- saveConfig(next);
1564
- await this.requestRestart({
1565
- reason: `plugin enabled: ${id}`,
1566
- manualMessage: `Enabled plugin "${id}". Restart the gateway to apply.`
1567
- });
688
+ };
689
+
690
+ // src/cli/commands/channels.ts
691
+ import { spawnSync as spawnSync3 } from "child_process";
692
+ import { loadConfig as loadConfig2 } from "@nextclaw/core";
693
+ var ChannelCommands = class {
694
+ constructor(deps) {
695
+ this.deps = deps;
1568
696
  }
1569
- async pluginsDisable(id) {
1570
- const config2 = loadConfig();
1571
- const next = disablePluginInConfig(config2, id);
1572
- saveConfig(next);
1573
- await this.requestRestart({
1574
- reason: `plugin disabled: ${id}`,
1575
- manualMessage: `Disabled plugin "${id}". Restart the gateway to apply.`
1576
- });
697
+ channelsStatus() {
698
+ const config2 = loadConfig2();
699
+ console.log("Channel Status");
700
+ console.log(`WhatsApp: ${config2.channels.whatsapp.enabled ? "\u2713" : "\u2717"}`);
701
+ console.log(`Discord: ${config2.channels.discord.enabled ? "\u2713" : "\u2717"}`);
702
+ console.log(`Feishu: ${config2.channels.feishu.enabled ? "\u2713" : "\u2717"}`);
703
+ console.log(`Mochat: ${config2.channels.mochat.enabled ? "\u2713" : "\u2717"}`);
704
+ console.log(`Telegram: ${config2.channels.telegram.enabled ? "\u2713" : "\u2717"}`);
705
+ console.log(`Slack: ${config2.channels.slack.enabled ? "\u2713" : "\u2717"}`);
706
+ console.log(`QQ: ${config2.channels.qq.enabled ? "\u2713" : "\u2717"}`);
1577
707
  }
1578
- async pluginsUninstall(id, opts = {}) {
1579
- const config2 = loadConfig();
1580
- const workspaceDir = getWorkspacePath(config2.agents.defaults.workspace);
1581
- const report = buildPluginStatusReport({
1582
- config: config2,
1583
- workspaceDir,
1584
- reservedChannelIds: Object.keys(config2.channels),
1585
- reservedProviderIds: PROVIDERS.map((provider) => provider.name)
1586
- });
1587
- const keepFiles = Boolean(opts.keepFiles || opts.keepConfig);
1588
- if (opts.keepConfig) {
1589
- console.log("`--keep-config` is deprecated, use `--keep-files`.");
1590
- }
1591
- const plugin = report.plugins.find((entry) => entry.id === id || entry.name === id);
1592
- const pluginId = plugin?.id ?? id;
1593
- const hasEntry = pluginId in (config2.plugins.entries ?? {});
1594
- const hasInstall = pluginId in (config2.plugins.installs ?? {});
1595
- if (!hasEntry && !hasInstall) {
1596
- if (plugin) {
1597
- console.error(
1598
- `Plugin "${pluginId}" is not managed by plugins config/install records and cannot be uninstalled.`
1599
- );
1600
- } else {
1601
- console.error(`Plugin not found: ${id}`);
1602
- }
1603
- process.exit(1);
708
+ channelsLogin() {
709
+ const bridgeDir = this.deps.getBridgeDir();
710
+ console.log(`${this.deps.logo} Starting bridge...`);
711
+ console.log("Scan the QR code to connect.\n");
712
+ const result = spawnSync3("npm", ["start"], { cwd: bridgeDir, stdio: "inherit" });
713
+ if (result.status !== 0) {
714
+ console.error(`Bridge failed: ${result.status ?? 1}`);
1604
715
  }
1605
- const install = config2.plugins.installs?.[pluginId];
1606
- const isLinked = install?.source === "path" && (!install.installPath || !install.sourcePath || resolve4(install.installPath) === resolve4(install.sourcePath));
1607
- const preview = [];
1608
- if (hasEntry) {
1609
- preview.push("config entry");
1610
- }
1611
- if (hasInstall) {
1612
- preview.push("install record");
1613
- }
1614
- if (config2.plugins.allow?.includes(pluginId)) {
1615
- preview.push("allowlist entry");
1616
- }
1617
- if (isLinked && install?.sourcePath && config2.plugins.load?.paths?.includes(install.sourcePath)) {
1618
- preview.push("load path");
1619
- }
1620
- const deleteTarget = !keepFiles ? resolveUninstallDirectoryTarget({
1621
- pluginId,
1622
- hasInstall,
1623
- installRecord: install
1624
- }) : null;
1625
- if (deleteTarget) {
1626
- preview.push(`directory: ${deleteTarget}`);
1627
- }
1628
- const pluginName = plugin?.name || pluginId;
1629
- const pluginTitle = pluginName !== pluginId ? `${pluginName} (${pluginId})` : pluginName;
1630
- console.log(`Plugin: ${pluginTitle}`);
1631
- console.log(`Will remove: ${preview.length > 0 ? preview.join(", ") : "(nothing)"}`);
1632
- if (opts.dryRun) {
1633
- console.log("Dry run, no changes made.");
716
+ }
717
+ };
718
+
719
+ // src/cli/commands/cron.ts
720
+ import { CronService, getDataDir as getDataDir2 } from "@nextclaw/core";
721
+ import { join as join3 } from "path";
722
+ var CronCommands = class {
723
+ cronList(opts) {
724
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
725
+ const service = new CronService(storePath);
726
+ const jobs = service.listJobs(Boolean(opts.all));
727
+ if (!jobs.length) {
728
+ console.log("No scheduled jobs.");
1634
729
  return;
1635
730
  }
1636
- if (!opts.force) {
1637
- const confirmed = await this.confirmYesNo(`Uninstall plugin "${pluginId}"?`);
1638
- if (!confirmed) {
1639
- console.log("Cancelled.");
1640
- return;
731
+ for (const job of jobs) {
732
+ let schedule = "";
733
+ if (job.schedule.kind === "every") {
734
+ schedule = `every ${Math.round((job.schedule.everyMs ?? 0) / 1e3)}s`;
735
+ } else if (job.schedule.kind === "cron") {
736
+ schedule = job.schedule.expr ?? "";
737
+ } else {
738
+ schedule = job.schedule.atMs ? new Date(job.schedule.atMs).toISOString() : "";
1641
739
  }
740
+ console.log(`${job.id} ${job.name} ${schedule}`);
741
+ }
742
+ }
743
+ cronAdd(opts) {
744
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
745
+ const service = new CronService(storePath);
746
+ let schedule = null;
747
+ if (opts.every) {
748
+ schedule = { kind: "every", everyMs: Number(opts.every) * 1e3 };
749
+ } else if (opts.cron) {
750
+ schedule = { kind: "cron", expr: String(opts.cron) };
751
+ } else if (opts.at) {
752
+ schedule = { kind: "at", atMs: Date.parse(String(opts.at)) };
753
+ }
754
+ if (!schedule) {
755
+ console.error("Error: Must specify --every, --cron, or --at");
756
+ return;
1642
757
  }
1643
- const result = await uninstallPlugin({
1644
- config: config2,
1645
- pluginId,
1646
- deleteFiles: !keepFiles
758
+ const job = service.addJob({
759
+ name: opts.name,
760
+ schedule,
761
+ message: opts.message,
762
+ deliver: Boolean(opts.deliver),
763
+ channel: opts.channel,
764
+ to: opts.to
1647
765
  });
1648
- if (!result.ok) {
1649
- console.error(result.error);
1650
- process.exit(1);
766
+ console.log(`\u2713 Added job '${job.name}' (${job.id})`);
767
+ }
768
+ cronRemove(jobId) {
769
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
770
+ const service = new CronService(storePath);
771
+ if (service.removeJob(jobId)) {
772
+ console.log(`\u2713 Removed job ${jobId}`);
773
+ } else {
774
+ console.log(`Job ${jobId} not found`);
775
+ }
776
+ }
777
+ cronEnable(jobId, opts) {
778
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
779
+ const service = new CronService(storePath);
780
+ const job = service.enableJob(jobId, !opts.disable);
781
+ if (job) {
782
+ console.log(`\u2713 Job '${job.name}' ${opts.disable ? "disabled" : "enabled"}`);
783
+ } else {
784
+ console.log(`Job ${jobId} not found`);
1651
785
  }
1652
- for (const warning of result.warnings) {
1653
- console.warn(warning);
786
+ }
787
+ async cronRun(jobId, opts) {
788
+ const storePath = join3(getDataDir2(), "cron", "jobs.json");
789
+ const service = new CronService(storePath);
790
+ const ok = await service.runJob(jobId, Boolean(opts.force));
791
+ console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
792
+ }
793
+ };
794
+
795
+ // src/cli/commands/diagnostics.ts
796
+ import { createServer as createNetServer } from "net";
797
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
798
+ import { resolve as resolve4 } from "path";
799
+ import {
800
+ APP_NAME,
801
+ getConfigPath,
802
+ getDataDir as getDataDir3,
803
+ getWorkspacePath,
804
+ loadConfig as loadConfig3,
805
+ PROVIDERS
806
+ } from "@nextclaw/core";
807
+ var DiagnosticsCommands = class {
808
+ constructor(deps) {
809
+ this.deps = deps;
810
+ }
811
+ async status(opts = {}) {
812
+ const report = await this.collectRuntimeStatus({
813
+ verbose: Boolean(opts.verbose),
814
+ fix: Boolean(opts.fix)
815
+ });
816
+ if (opts.json) {
817
+ console.log(JSON.stringify(report, null, 2));
818
+ process.exitCode = report.exitCode;
819
+ return;
1654
820
  }
1655
- saveConfig(result.config);
1656
- const removed = [];
1657
- if (result.actions.entry) {
1658
- removed.push("config entry");
821
+ console.log(`${this.deps.logo} ${APP_NAME} Status`);
822
+ console.log(`Level: ${report.level}`);
823
+ console.log(`Generated: ${report.generatedAt}`);
824
+ console.log("");
825
+ const processLabel = report.process.running ? `running (PID ${report.process.pid})` : report.process.staleState ? "stale-state" : "stopped";
826
+ console.log(`Process: ${processLabel}`);
827
+ console.log(`State file: ${report.serviceStatePath} ${report.serviceStateExists ? "\u2713" : "\u2717"}`);
828
+ if (report.process.startedAt) {
829
+ console.log(`Started: ${report.process.startedAt}`);
1659
830
  }
1660
- if (result.actions.install) {
1661
- removed.push("install record");
831
+ console.log(`Managed health: ${report.health.managed.state} (${report.health.managed.detail})`);
832
+ if (!report.process.running) {
833
+ console.log(`Configured health: ${report.health.configured.state} (${report.health.configured.detail})`);
1662
834
  }
1663
- if (result.actions.allowlist) {
1664
- removed.push("allowlist");
835
+ console.log(`UI: ${report.endpoints.uiUrl ?? report.endpoints.configuredUiUrl}`);
836
+ console.log(`API: ${report.endpoints.apiUrl ?? report.endpoints.configuredApiUrl}`);
837
+ console.log(`Config: ${report.configPath} ${report.configExists ? "\u2713" : "\u2717"}`);
838
+ console.log(`Workspace: ${report.workspacePath} ${report.workspaceExists ? "\u2713" : "\u2717"}`);
839
+ console.log(`Model: ${report.model}`);
840
+ for (const provider of report.providers) {
841
+ console.log(`${provider.name}: ${provider.configured ? "\u2713" : "not set"}${provider.detail ? ` (${provider.detail})` : ""}`);
1665
842
  }
1666
- if (result.actions.loadPath) {
1667
- removed.push("load path");
843
+ if (report.fixActions.length > 0) {
844
+ console.log("");
845
+ console.log("Fix actions:");
846
+ for (const action of report.fixActions) {
847
+ console.log(`- ${action}`);
848
+ }
1668
849
  }
1669
- if (result.actions.directory) {
1670
- removed.push("directory");
850
+ if (report.issues.length > 0) {
851
+ console.log("");
852
+ console.log("Issues:");
853
+ for (const issue of report.issues) {
854
+ console.log(`- ${issue}`);
855
+ }
1671
856
  }
1672
- console.log(`Uninstalled plugin "${pluginId}". Removed: ${removed.length > 0 ? removed.join(", ") : "nothing"}.`);
1673
- await this.requestRestart({
1674
- reason: `plugin uninstalled: ${pluginId}`,
1675
- manualMessage: "Restart the gateway to apply changes."
1676
- });
1677
- }
1678
- async pluginsInstall(pathOrSpec, opts = {}) {
1679
- const fileSpec = this.resolveFileNpmSpecToLocalPath(pathOrSpec);
1680
- if (fileSpec && !fileSpec.ok) {
1681
- console.error(fileSpec.error);
1682
- process.exit(1);
857
+ if (report.recommendations.length > 0) {
858
+ console.log("");
859
+ console.log("Recommendations:");
860
+ for (const recommendation of report.recommendations) {
861
+ console.log(`- ${recommendation}`);
862
+ }
1683
863
  }
1684
- const normalized = fileSpec && fileSpec.ok ? fileSpec.path : pathOrSpec;
1685
- const resolved = resolve4(expandHome(normalized));
1686
- const config2 = loadConfig();
1687
- if (existsSync4(resolved)) {
1688
- if (opts.link) {
1689
- const probe = await installPluginFromPath({ path: resolved, dryRun: true });
1690
- if (!probe.ok) {
1691
- console.error(probe.error);
1692
- process.exit(1);
1693
- }
1694
- let next3 = addPluginLoadPath(config2, resolved);
1695
- next3 = enablePluginInConfig(next3, probe.pluginId);
1696
- next3 = recordPluginInstall(next3, {
1697
- pluginId: probe.pluginId,
1698
- source: "path",
1699
- sourcePath: resolved,
1700
- installPath: resolved,
1701
- version: probe.version
1702
- });
1703
- saveConfig(next3);
1704
- console.log(`Linked plugin path: ${resolved}`);
1705
- await this.requestRestart({
1706
- reason: `plugin linked: ${probe.pluginId}`,
1707
- manualMessage: "Restart the gateway to load plugins."
1708
- });
1709
- return;
864
+ if (opts.verbose && report.logTail.length > 0) {
865
+ console.log("");
866
+ console.log("Recent logs:");
867
+ for (const line of report.logTail) {
868
+ console.log(line);
1710
869
  }
1711
- const result2 = await installPluginFromPath({
1712
- path: resolved,
1713
- logger: {
1714
- info: (message) => console.log(message),
1715
- warn: (message) => console.warn(message)
870
+ }
871
+ process.exitCode = report.exitCode;
872
+ }
873
+ async doctor(opts = {}) {
874
+ const report = await this.collectRuntimeStatus({
875
+ verbose: Boolean(opts.verbose),
876
+ fix: Boolean(opts.fix)
877
+ });
878
+ const checkPort = await this.checkPortAvailability({
879
+ host: report.process.running ? report.endpoints.uiUrl ? new URL(report.endpoints.uiUrl).hostname : "127.0.0.1" : "127.0.0.1",
880
+ port: (() => {
881
+ try {
882
+ const base = report.process.running && report.endpoints.uiUrl ? report.endpoints.uiUrl : report.endpoints.configuredUiUrl;
883
+ return Number(new URL(base).port || 80);
884
+ } catch {
885
+ return 18791;
1716
886
  }
1717
- });
1718
- if (!result2.ok) {
1719
- console.error(result2.error);
1720
- process.exit(1);
887
+ })()
888
+ });
889
+ const providerConfigured = report.providers.some((provider) => provider.configured);
890
+ const checks = [
891
+ {
892
+ name: "config-file",
893
+ status: report.configExists ? "pass" : "fail",
894
+ detail: report.configPath
895
+ },
896
+ {
897
+ name: "workspace-dir",
898
+ status: report.workspaceExists ? "pass" : "warn",
899
+ detail: report.workspacePath
900
+ },
901
+ {
902
+ name: "service-state",
903
+ status: report.process.staleState ? "fail" : report.process.running ? "pass" : "warn",
904
+ detail: report.process.running ? `PID ${report.process.pid}` : report.process.staleState ? "state exists but process is not running" : "service not running"
905
+ },
906
+ {
907
+ name: "service-health",
908
+ status: report.process.running ? report.health.managed.state === "ok" ? "pass" : "fail" : report.health.configured.state === "ok" ? "warn" : "warn",
909
+ detail: report.process.running ? `${report.health.managed.state}: ${report.health.managed.detail}` : `${report.health.configured.state}: ${report.health.configured.detail}`
910
+ },
911
+ {
912
+ name: "ui-port-availability",
913
+ status: report.process.running ? "pass" : checkPort.available ? "pass" : "fail",
914
+ detail: report.process.running ? "managed by running service" : checkPort.available ? "available" : checkPort.detail
915
+ },
916
+ {
917
+ name: "provider-config",
918
+ status: providerConfigured ? "pass" : "warn",
919
+ detail: providerConfigured ? "at least one provider configured" : "no provider api key configured"
1721
920
  }
1722
- let next2 = enablePluginInConfig(config2, result2.pluginId);
1723
- next2 = recordPluginInstall(next2, {
1724
- pluginId: result2.pluginId,
1725
- source: this.isArchivePath(resolved) ? "archive" : "path",
1726
- sourcePath: resolved,
1727
- installPath: result2.targetDir,
1728
- version: result2.version
1729
- });
1730
- saveConfig(next2);
1731
- console.log(`Installed plugin: ${result2.pluginId}`);
1732
- await this.requestRestart({
1733
- reason: `plugin installed: ${result2.pluginId}`,
1734
- manualMessage: "Restart the gateway to load plugins."
1735
- });
921
+ ];
922
+ const failed = checks.filter((check) => check.status === "fail");
923
+ const warned = checks.filter((check) => check.status === "warn");
924
+ const exitCode = failed.length > 0 ? 1 : warned.length > 0 ? 1 : 0;
925
+ if (opts.json) {
926
+ console.log(
927
+ JSON.stringify(
928
+ {
929
+ generatedAt: report.generatedAt,
930
+ checks,
931
+ status: report,
932
+ exitCode
933
+ },
934
+ null,
935
+ 2
936
+ )
937
+ );
938
+ process.exitCode = exitCode;
1736
939
  return;
1737
940
  }
1738
- if (opts.link) {
1739
- console.error("`--link` requires a local path.");
1740
- process.exit(1);
941
+ console.log(`${this.deps.logo} ${APP_NAME} Doctor`);
942
+ console.log(`Generated: ${report.generatedAt}`);
943
+ console.log("");
944
+ for (const check of checks) {
945
+ const icon = check.status === "pass" ? "\u2713" : check.status === "warn" ? "!" : "\u2717";
946
+ console.log(`${icon} ${check.name}: ${check.detail}`);
1741
947
  }
1742
- if (this.looksLikePath(pathOrSpec)) {
1743
- console.error(`Path not found: ${resolved}`);
1744
- process.exit(1);
948
+ if (report.recommendations.length > 0) {
949
+ console.log("");
950
+ console.log("Recommendations:");
951
+ for (const recommendation of report.recommendations) {
952
+ console.log(`- ${recommendation}`);
953
+ }
1745
954
  }
1746
- const result = await installPluginFromNpmSpec({
1747
- spec: pathOrSpec,
1748
- logger: {
1749
- info: (message) => console.log(message),
1750
- warn: (message) => console.warn(message)
955
+ if (opts.verbose && report.logTail.length > 0) {
956
+ console.log("");
957
+ console.log("Recent logs:");
958
+ for (const line of report.logTail) {
959
+ console.log(line);
1751
960
  }
1752
- });
1753
- if (!result.ok) {
1754
- console.error(result.error);
1755
- process.exit(1);
1756
961
  }
1757
- let next = enablePluginInConfig(config2, result.pluginId);
1758
- next = recordPluginInstall(next, {
1759
- pluginId: result.pluginId,
1760
- source: "npm",
1761
- spec: pathOrSpec,
1762
- installPath: result.targetDir,
1763
- version: result.version
1764
- });
1765
- saveConfig(next);
1766
- console.log(`Installed plugin: ${result.pluginId}`);
1767
- await this.requestRestart({
1768
- reason: `plugin installed: ${result.pluginId}`,
1769
- manualMessage: "Restart the gateway to load plugins."
1770
- });
962
+ process.exitCode = exitCode;
1771
963
  }
1772
- pluginsDoctor() {
1773
- const config2 = loadConfig();
1774
- const workspaceDir = getWorkspacePath(config2.agents.defaults.workspace);
1775
- const report = buildPluginStatusReport({
1776
- config: config2,
1777
- workspaceDir,
1778
- reservedChannelIds: Object.keys(config2.channels),
1779
- reservedProviderIds: PROVIDERS.map((provider) => provider.name)
1780
- });
1781
- const pluginErrors = report.plugins.filter((plugin) => plugin.status === "error");
1782
- const diagnostics = report.diagnostics.filter((diag) => diag.level === "error");
1783
- if (pluginErrors.length === 0 && diagnostics.length === 0) {
1784
- console.log("No plugin issues detected.");
1785
- return;
1786
- }
1787
- if (pluginErrors.length > 0) {
1788
- console.log("Plugin errors:");
1789
- for (const entry of pluginErrors) {
1790
- console.log(`- ${entry.id}: ${entry.error ?? "failed to load"} (${entry.source})`);
1791
- }
964
+ async collectRuntimeStatus(params) {
965
+ const configPath = getConfigPath();
966
+ const config2 = loadConfig3();
967
+ const workspacePath = getWorkspacePath(config2.agents.defaults.workspace);
968
+ const serviceStatePath = resolve4(getDataDir3(), "run", "service.json");
969
+ const fixActions = [];
970
+ let serviceState = readServiceState();
971
+ if (params.fix && serviceState && !isProcessRunning(serviceState.pid)) {
972
+ clearServiceState();
973
+ fixActions.push("Cleared stale service state file.");
974
+ serviceState = readServiceState();
1792
975
  }
1793
- if (diagnostics.length > 0) {
1794
- if (pluginErrors.length > 0) {
1795
- console.log("");
976
+ const managedByState = Boolean(serviceState);
977
+ const running = Boolean(serviceState && isProcessRunning(serviceState.pid));
978
+ const staleState = Boolean(serviceState && !running);
979
+ const configuredUi = resolveUiConfig(config2, { enabled: true, host: config2.ui.host, port: config2.ui.port });
980
+ const configuredUiUrl = resolveUiApiBase(configuredUi.host, configuredUi.port);
981
+ const configuredApiUrl = `${configuredUiUrl}/api`;
982
+ const managedUiUrl = serviceState?.uiUrl ?? null;
983
+ const managedApiUrl = serviceState?.apiUrl ?? null;
984
+ const managedHealth = running && managedApiUrl ? await this.probeApiHealth(`${managedApiUrl}/health`) : { state: "unreachable", detail: "service not running" };
985
+ const configuredHealth = await this.probeApiHealth(`${configuredApiUrl}/health`, 900);
986
+ const orphanSuspected = !running && configuredHealth.state === "ok";
987
+ const providers = PROVIDERS.map((spec) => {
988
+ const provider = config2.providers[spec.name];
989
+ if (!provider) {
990
+ return { name: spec.displayName ?? spec.name, configured: false, detail: "missing config" };
1796
991
  }
1797
- console.log("Diagnostics:");
1798
- for (const diag of diagnostics) {
1799
- const prefix = diag.pluginId ? `${diag.pluginId}: ` : "";
1800
- console.log(`- ${prefix}${diag.message}`);
992
+ if (spec.isLocal) {
993
+ return {
994
+ name: spec.displayName ?? spec.name,
995
+ configured: Boolean(provider.apiBase),
996
+ detail: provider.apiBase ? provider.apiBase : "apiBase not set"
997
+ };
1801
998
  }
1802
- }
1803
- }
1804
- async skillsInstall(options) {
1805
- const workdir = options.workdir ? expandHome(options.workdir) : getWorkspacePath();
1806
- const result = await installClawHubSkill({
1807
- slug: options.slug,
1808
- version: options.version,
1809
- registry: options.registry,
1810
- workdir,
1811
- dir: options.dir,
1812
- force: options.force
999
+ return {
1000
+ name: spec.displayName ?? spec.name,
1001
+ configured: Boolean(provider.apiKey),
1002
+ detail: provider.apiKey ? "apiKey set" : "apiKey not set"
1003
+ };
1813
1004
  });
1814
- const versionLabel = result.version ?? "latest";
1815
- if (result.alreadyInstalled) {
1816
- console.log(`\u2713 ${result.slug} is already installed`);
1817
- } else {
1818
- console.log(`\u2713 Installed ${result.slug}@${versionLabel}`);
1005
+ const issues = [];
1006
+ const recommendations = [];
1007
+ if (!existsSync3(configPath)) {
1008
+ issues.push("Config file is missing.");
1009
+ recommendations.push(`Run ${APP_NAME} init to create config files.`);
1819
1010
  }
1820
- if (result.registry) {
1821
- console.log(` Registry: ${result.registry}`);
1011
+ if (!existsSync3(workspacePath)) {
1012
+ issues.push("Workspace directory does not exist.");
1013
+ recommendations.push(`Run ${APP_NAME} init to create workspace templates.`);
1822
1014
  }
1823
- console.log(` Path: ${result.destinationDir}`);
1824
- }
1825
- channelsStatus() {
1826
- const config2 = loadConfig();
1827
- console.log("Channel Status");
1828
- console.log(`WhatsApp: ${config2.channels.whatsapp.enabled ? "\u2713" : "\u2717"}`);
1829
- console.log(`Discord: ${config2.channels.discord.enabled ? "\u2713" : "\u2717"}`);
1830
- console.log(`Feishu: ${config2.channels.feishu.enabled ? "\u2713" : "\u2717"}`);
1831
- console.log(`Mochat: ${config2.channels.mochat.enabled ? "\u2713" : "\u2717"}`);
1832
- console.log(`Telegram: ${config2.channels.telegram.enabled ? "\u2713" : "\u2717"}`);
1833
- console.log(`Slack: ${config2.channels.slack.enabled ? "\u2713" : "\u2717"}`);
1834
- console.log(`QQ: ${config2.channels.qq.enabled ? "\u2713" : "\u2717"}`);
1835
- const workspaceDir = getWorkspacePath(config2.agents.defaults.workspace);
1836
- const report = buildPluginStatusReport({
1837
- config: config2,
1838
- workspaceDir,
1839
- reservedChannelIds: Object.keys(config2.channels),
1840
- reservedProviderIds: PROVIDERS.map((provider) => provider.name)
1841
- });
1842
- const pluginChannels = report.plugins.filter((plugin) => plugin.status === "loaded" && plugin.channelIds.length > 0);
1843
- if (pluginChannels.length > 0) {
1844
- console.log("Plugin Channels:");
1845
- for (const plugin of pluginChannels) {
1846
- const channels2 = plugin.channelIds.join(", ");
1847
- console.log(`- ${channels2} (plugin: ${plugin.id})`);
1848
- }
1015
+ if (staleState) {
1016
+ issues.push("Service state is stale (state exists but process is not running).");
1017
+ recommendations.push(`Run ${APP_NAME} status --fix to clean stale state.`);
1849
1018
  }
1850
- }
1851
- channelsLogin() {
1852
- const bridgeDir = this.getBridgeDir();
1853
- console.log(`${this.logo} Starting bridge...`);
1854
- console.log("Scan the QR code to connect.\n");
1855
- const result = spawnSync3("npm", ["start"], { cwd: bridgeDir, stdio: "inherit" });
1856
- if (result.status !== 0) {
1857
- console.error(`Bridge failed: ${result.status ?? 1}`);
1019
+ if (running && managedHealth.state !== "ok") {
1020
+ issues.push(`Managed service health check failed: ${managedHealth.detail}`);
1021
+ recommendations.push(`Check logs at ${serviceState?.logPath ?? resolveServiceLogPath()}.`);
1858
1022
  }
1859
- }
1860
- async channelsAdd(opts) {
1861
- const channelId = opts.channel?.trim();
1862
- if (!channelId) {
1863
- console.error("--channel is required");
1864
- process.exit(1);
1023
+ if (!running) {
1024
+ recommendations.push(`Run ${APP_NAME} start to launch the service.`);
1865
1025
  }
1866
- const config2 = loadConfig();
1867
- const workspaceDir = getWorkspacePath(config2.agents.defaults.workspace);
1868
- const pluginRegistry = this.loadPluginRegistry(config2, workspaceDir);
1869
- const bindings = getPluginChannelBindings(pluginRegistry);
1870
- const binding = bindings.find((entry) => entry.channelId === channelId || entry.pluginId === channelId);
1871
- if (!binding) {
1872
- console.error(`No plugin channel found for: ${channelId}`);
1873
- process.exit(1);
1026
+ if (orphanSuspected) {
1027
+ issues.push("A service appears healthy on configured API endpoint, but state is missing/stale.");
1028
+ recommendations.push("Another process may be occupying the UI port; stop it or use --ui-port with a free port.");
1874
1029
  }
1875
- const setup = binding.channel.setup;
1876
- if (!setup?.applyAccountConfig) {
1877
- console.error(`Channel "${binding.channelId}" does not support setup.`);
1878
- process.exit(1);
1030
+ if (!providers.some((provider) => provider.configured)) {
1031
+ recommendations.push("Configure at least one provider API key in UI or config before expecting agent replies.");
1879
1032
  }
1880
- const input = {
1881
- name: opts.name,
1882
- token: opts.token,
1883
- code: opts.code,
1884
- url: opts.url,
1885
- httpUrl: opts.httpUrl
1033
+ const logTail = params.verbose ? this.readLogTail(serviceState?.logPath ?? resolveServiceLogPath(), 25) : [];
1034
+ const level = running ? managedHealth.state === "ok" ? issues.length > 0 ? "degraded" : "healthy" : "degraded" : "stopped";
1035
+ const exitCode = level === "healthy" ? 0 : level === "degraded" ? 1 : 2;
1036
+ return {
1037
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1038
+ configPath,
1039
+ configExists: existsSync3(configPath),
1040
+ workspacePath,
1041
+ workspaceExists: existsSync3(workspacePath),
1042
+ model: config2.agents.defaults.model,
1043
+ providers,
1044
+ serviceStatePath,
1045
+ serviceStateExists: existsSync3(serviceStatePath),
1046
+ fixActions,
1047
+ process: {
1048
+ managedByState,
1049
+ pid: serviceState?.pid ?? null,
1050
+ running,
1051
+ staleState,
1052
+ orphanSuspected,
1053
+ startedAt: serviceState?.startedAt ?? null
1054
+ },
1055
+ endpoints: {
1056
+ uiUrl: managedUiUrl,
1057
+ apiUrl: managedApiUrl,
1058
+ configuredUiUrl,
1059
+ configuredApiUrl
1060
+ },
1061
+ health: {
1062
+ managed: managedHealth,
1063
+ configured: configuredHealth
1064
+ },
1065
+ issues,
1066
+ recommendations,
1067
+ logTail,
1068
+ level,
1069
+ exitCode
1886
1070
  };
1887
- const currentView = this.toPluginConfigView(config2, bindings);
1888
- const accountId = binding.channel.config?.defaultAccountId?.(currentView) ?? "default";
1889
- const validateError = setup.validateInput?.({
1890
- cfg: currentView,
1891
- input,
1892
- accountId
1893
- });
1894
- if (validateError) {
1895
- console.error(`Channel setup validation failed: ${validateError}`);
1896
- process.exit(1);
1897
- }
1898
- const nextView = setup.applyAccountConfig({
1899
- cfg: currentView,
1900
- input,
1901
- accountId
1902
- });
1903
- if (!nextView || typeof nextView !== "object" || Array.isArray(nextView)) {
1904
- console.error("Channel setup returned invalid config payload.");
1905
- process.exit(1);
1906
- }
1907
- let next = this.mergePluginConfigView(config2, nextView, bindings);
1908
- next = enablePluginInConfig(next, binding.pluginId);
1909
- saveConfig(next);
1910
- console.log(`Configured channel "${binding.channelId}" via plugin "${binding.pluginId}".`);
1911
- await this.requestRestart({
1912
- reason: `channel configured via plugin: ${binding.pluginId}`,
1913
- manualMessage: "Restart the gateway to apply changes."
1914
- });
1915
1071
  }
1916
- toPluginConfigView(config2, bindings) {
1917
- const view = JSON.parse(JSON.stringify(config2));
1918
- const channels2 = view.channels && typeof view.channels === "object" && !Array.isArray(view.channels) ? { ...view.channels } : {};
1919
- for (const binding of bindings) {
1920
- const pluginConfig = config2.plugins.entries?.[binding.pluginId]?.config;
1921
- if (!pluginConfig || typeof pluginConfig !== "object" || Array.isArray(pluginConfig)) {
1922
- continue;
1072
+ async probeApiHealth(url, timeoutMs = 1500) {
1073
+ const controller = new AbortController();
1074
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1075
+ try {
1076
+ const response = await fetch(url, {
1077
+ method: "GET",
1078
+ signal: controller.signal
1079
+ });
1080
+ if (!response.ok) {
1081
+ return { state: "invalid-response", detail: `HTTP ${response.status}` };
1082
+ }
1083
+ const payload = await response.json();
1084
+ if (payload?.ok === true && payload?.data?.status === "ok") {
1085
+ return { state: "ok", detail: "health endpoint returned ok", payload };
1923
1086
  }
1924
- channels2[binding.channelId] = JSON.parse(JSON.stringify(pluginConfig));
1087
+ return { state: "invalid-response", detail: "unexpected health payload", payload };
1088
+ } catch (error) {
1089
+ return { state: "unreachable", detail: String(error) };
1090
+ } finally {
1091
+ clearTimeout(timer);
1925
1092
  }
1926
- view.channels = channels2;
1927
- return view;
1928
1093
  }
1929
- mergePluginConfigView(baseConfig, pluginViewConfig, bindings) {
1930
- const next = JSON.parse(JSON.stringify(baseConfig));
1931
- const pluginChannels = pluginViewConfig.channels && typeof pluginViewConfig.channels === "object" && !Array.isArray(pluginViewConfig.channels) ? pluginViewConfig.channels : {};
1932
- const entries = { ...next.plugins.entries ?? {} };
1933
- for (const binding of bindings) {
1934
- if (!Object.prototype.hasOwnProperty.call(pluginChannels, binding.channelId)) {
1935
- continue;
1936
- }
1937
- const channelConfig = pluginChannels[binding.channelId];
1938
- if (!channelConfig || typeof channelConfig !== "object" || Array.isArray(channelConfig)) {
1939
- continue;
1094
+ readLogTail(path, maxLines = 25) {
1095
+ if (!existsSync3(path)) {
1096
+ return [];
1097
+ }
1098
+ try {
1099
+ const lines = readFileSync2(path, "utf-8").split(/\r?\n/).filter(Boolean);
1100
+ if (lines.length <= maxLines) {
1101
+ return lines;
1940
1102
  }
1941
- entries[binding.pluginId] = {
1942
- ...entries[binding.pluginId] ?? {},
1943
- config: channelConfig
1944
- };
1103
+ return lines.slice(lines.length - maxLines);
1104
+ } catch {
1105
+ return [];
1945
1106
  }
1946
- next.plugins = {
1947
- ...next.plugins,
1948
- entries
1949
- };
1950
- return next;
1951
1107
  }
1952
- cronList(opts) {
1953
- const storePath = join3(getDataDir2(), "cron", "jobs.json");
1954
- const service = new CronService(storePath);
1955
- const jobs = service.listJobs(Boolean(opts.all));
1956
- if (!jobs.length) {
1957
- console.log("No scheduled jobs.");
1958
- return;
1108
+ async checkPortAvailability(params) {
1109
+ return await new Promise((resolve8) => {
1110
+ const server = createNetServer();
1111
+ server.once("error", (error) => {
1112
+ resolve8({
1113
+ available: false,
1114
+ detail: `bind failed on ${params.host}:${params.port} (${String(error)})`
1115
+ });
1116
+ });
1117
+ server.listen(params.port, params.host, () => {
1118
+ server.close(() => {
1119
+ resolve8({
1120
+ available: true,
1121
+ detail: `bind ok on ${params.host}:${params.port}`
1122
+ });
1123
+ });
1124
+ });
1125
+ });
1126
+ }
1127
+ };
1128
+
1129
+ // src/cli/commands/service.ts
1130
+ import {
1131
+ APP_NAME as APP_NAME2,
1132
+ AgentLoop,
1133
+ ChannelManager as ChannelManager2,
1134
+ CronService as CronService2,
1135
+ getApiBase,
1136
+ getConfigPath as getConfigPath2,
1137
+ getDataDir as getDataDir4,
1138
+ getProvider,
1139
+ getProviderName,
1140
+ getWorkspacePath as getWorkspacePath2,
1141
+ HeartbeatService,
1142
+ LiteLLMProvider,
1143
+ loadConfig as loadConfig4,
1144
+ MessageBus,
1145
+ ProviderManager,
1146
+ saveConfig as saveConfig2,
1147
+ SessionManager
1148
+ } from "@nextclaw/core";
1149
+ import { startUiServer } from "@nextclaw/server";
1150
+ import { closeSync, mkdirSync as mkdirSync2, openSync } from "fs";
1151
+ import { join as join4, resolve as resolve5 } from "path";
1152
+ import { spawn as spawn2 } from "child_process";
1153
+ import chokidar from "chokidar";
1154
+
1155
+ // src/cli/gateway/controller.ts
1156
+ import { createHash } from "crypto";
1157
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
1158
+ import {
1159
+ buildConfigSchema,
1160
+ ConfigSchema,
1161
+ redactConfigObject
1162
+ } from "@nextclaw/core";
1163
+ var hashRaw = (raw) => createHash("sha256").update(raw).digest("hex");
1164
+ var readConfigSnapshot = (getConfigPath4) => {
1165
+ const path = getConfigPath4();
1166
+ let raw = "";
1167
+ let parsed = {};
1168
+ if (existsSync4(path)) {
1169
+ raw = readFileSync3(path, "utf-8");
1170
+ try {
1171
+ parsed = JSON.parse(raw);
1172
+ } catch {
1173
+ parsed = {};
1959
1174
  }
1960
- for (const job of jobs) {
1961
- let schedule = "";
1962
- if (job.schedule.kind === "every") {
1963
- schedule = `every ${Math.round((job.schedule.everyMs ?? 0) / 1e3)}s`;
1964
- } else if (job.schedule.kind === "cron") {
1965
- schedule = job.schedule.expr ?? "";
1175
+ }
1176
+ let config2;
1177
+ let valid = true;
1178
+ try {
1179
+ config2 = ConfigSchema.parse(parsed);
1180
+ } catch {
1181
+ config2 = ConfigSchema.parse({});
1182
+ valid = false;
1183
+ }
1184
+ if (!raw) {
1185
+ raw = JSON.stringify(config2, null, 2);
1186
+ }
1187
+ const hash = hashRaw(raw);
1188
+ const schema = buildConfigSchema({ version: getPackageVersion() });
1189
+ const redacted = redactConfigObject(config2, schema.uiHints);
1190
+ return { raw: valid ? JSON.stringify(redacted, null, 2) : null, hash: valid ? hash : null, config: config2, redacted, valid };
1191
+ };
1192
+ var redactValue = (value) => {
1193
+ const schema = buildConfigSchema({ version: getPackageVersion() });
1194
+ return redactConfigObject(value, schema.uiHints);
1195
+ };
1196
+ var mergeDeep = (base, patch) => {
1197
+ const next = { ...base };
1198
+ for (const [key, value] of Object.entries(patch)) {
1199
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1200
+ const baseVal = base[key];
1201
+ if (baseVal && typeof baseVal === "object" && !Array.isArray(baseVal)) {
1202
+ next[key] = mergeDeep(baseVal, value);
1966
1203
  } else {
1967
- schedule = job.schedule.atMs ? new Date(job.schedule.atMs).toISOString() : "";
1204
+ next[key] = mergeDeep({}, value);
1968
1205
  }
1969
- console.log(`${job.id} ${job.name} ${schedule}`);
1206
+ } else {
1207
+ next[key] = value;
1970
1208
  }
1971
1209
  }
1972
- cronAdd(opts) {
1973
- const storePath = join3(getDataDir2(), "cron", "jobs.json");
1974
- const service = new CronService(storePath);
1975
- let schedule = null;
1976
- if (opts.every) {
1977
- schedule = { kind: "every", everyMs: Number(opts.every) * 1e3 };
1978
- } else if (opts.cron) {
1979
- schedule = { kind: "cron", expr: String(opts.cron) };
1980
- } else if (opts.at) {
1981
- schedule = { kind: "at", atMs: Date.parse(String(opts.at)) };
1982
- }
1983
- if (!schedule) {
1984
- console.error("Error: Must specify --every, --cron, or --at");
1210
+ return next;
1211
+ };
1212
+ var GatewayControllerImpl = class {
1213
+ constructor(deps) {
1214
+ this.deps = deps;
1215
+ }
1216
+ async requestRestart(options) {
1217
+ if (this.deps.requestRestart) {
1218
+ await this.deps.requestRestart(options);
1985
1219
  return;
1986
1220
  }
1987
- const job = service.addJob({
1988
- name: opts.name,
1989
- schedule,
1990
- message: opts.message,
1991
- deliver: Boolean(opts.deliver),
1992
- channel: opts.channel,
1993
- to: opts.to
1994
- });
1995
- console.log(`\u2713 Added job '${job.name}' (${job.id})`);
1221
+ const delay = typeof options?.delayMs === "number" && Number.isFinite(options.delayMs) ? Math.max(0, options.delayMs) : 100;
1222
+ console.log(`Gateway restart requested via tool${options?.reason ? ` (${options.reason})` : ""}.`);
1223
+ setTimeout(() => {
1224
+ process.exit(0);
1225
+ }, delay);
1996
1226
  }
1997
- cronRemove(jobId) {
1998
- const storePath = join3(getDataDir2(), "cron", "jobs.json");
1999
- const service = new CronService(storePath);
2000
- if (service.removeJob(jobId)) {
2001
- console.log(`\u2713 Removed job ${jobId}`);
2002
- } else {
2003
- console.log(`Job ${jobId} not found`);
2004
- }
1227
+ status() {
1228
+ return {
1229
+ channels: this.deps.reloader.getChannels().enabledChannels,
1230
+ cron: this.deps.cron.status(),
1231
+ configPath: this.deps.getConfigPath()
1232
+ };
2005
1233
  }
2006
- cronEnable(jobId, opts) {
2007
- const storePath = join3(getDataDir2(), "cron", "jobs.json");
2008
- const service = new CronService(storePath);
2009
- const job = service.enableJob(jobId, !opts.disable);
2010
- if (job) {
2011
- console.log(`\u2713 Job '${job.name}' ${opts.disable ? "disabled" : "enabled"}`);
2012
- } else {
2013
- console.log(`Job ${jobId} not found`);
2014
- }
1234
+ async reloadConfig(reason) {
1235
+ return this.deps.reloader.reloadConfig(reason);
2015
1236
  }
2016
- async cronRun(jobId, opts) {
2017
- const storePath = join3(getDataDir2(), "cron", "jobs.json");
2018
- const service = new CronService(storePath);
2019
- const ok = await service.runJob(jobId, Boolean(opts.force));
2020
- console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
1237
+ async restart(options) {
1238
+ await this.requestRestart(options);
1239
+ return "Restart scheduled";
2021
1240
  }
2022
- async status(opts = {}) {
2023
- const report = await this.collectRuntimeStatus({
2024
- verbose: Boolean(opts.verbose),
2025
- fix: Boolean(opts.fix)
2026
- });
2027
- if (opts.json) {
2028
- console.log(JSON.stringify(report, null, 2));
2029
- process.exitCode = report.exitCode;
2030
- return;
2031
- }
2032
- console.log(`${this.logo} ${APP_NAME} Status`);
2033
- console.log(`Level: ${report.level}`);
2034
- console.log(`Generated: ${report.generatedAt}`);
2035
- console.log("");
2036
- const processLabel = report.process.running ? `running (PID ${report.process.pid})` : report.process.staleState ? "stale-state" : "stopped";
2037
- console.log(`Process: ${processLabel}`);
2038
- console.log(`State file: ${report.serviceStatePath} ${report.serviceStateExists ? "\u2713" : "\u2717"}`);
2039
- if (report.process.startedAt) {
2040
- console.log(`Started: ${report.process.startedAt}`);
2041
- }
2042
- console.log(`Managed health: ${report.health.managed.state} (${report.health.managed.detail})`);
2043
- if (!report.process.running) {
2044
- console.log(`Configured health: ${report.health.configured.state} (${report.health.configured.detail})`);
2045
- }
2046
- console.log(`UI: ${report.endpoints.uiUrl ?? report.endpoints.configuredUiUrl}`);
2047
- console.log(`API: ${report.endpoints.apiUrl ?? report.endpoints.configuredApiUrl}`);
2048
- console.log(`Config: ${report.configPath} ${report.configExists ? "\u2713" : "\u2717"}`);
2049
- console.log(`Workspace: ${report.workspacePath} ${report.workspaceExists ? "\u2713" : "\u2717"}`);
2050
- console.log(`Model: ${report.model}`);
2051
- for (const provider of report.providers) {
2052
- console.log(`${provider.name}: ${provider.configured ? "\u2713" : "not set"}${provider.detail ? ` (${provider.detail})` : ""}`);
2053
- }
2054
- if (report.fixActions.length > 0) {
2055
- console.log("");
2056
- console.log("Fix actions:");
2057
- for (const action of report.fixActions) {
2058
- console.log(`- ${action}`);
2059
- }
2060
- }
2061
- if (report.issues.length > 0) {
2062
- console.log("");
2063
- console.log("Issues:");
2064
- for (const issue of report.issues) {
2065
- console.log(`- ${issue}`);
2066
- }
1241
+ async getConfig() {
1242
+ const snapshot = readConfigSnapshot(this.deps.getConfigPath);
1243
+ return {
1244
+ raw: snapshot.raw,
1245
+ hash: snapshot.hash,
1246
+ path: this.deps.getConfigPath(),
1247
+ config: snapshot.redacted,
1248
+ parsed: snapshot.redacted,
1249
+ resolved: snapshot.redacted,
1250
+ valid: snapshot.valid
1251
+ };
1252
+ }
1253
+ async getConfigSchema() {
1254
+ return buildConfigSchema({ version: getPackageVersion() });
1255
+ }
1256
+ async applyConfig(params) {
1257
+ const snapshot = readConfigSnapshot(this.deps.getConfigPath);
1258
+ if (!params.baseHash) {
1259
+ return { ok: false, error: "config base hash required; re-run config.get and retry" };
2067
1260
  }
2068
- if (report.recommendations.length > 0) {
2069
- console.log("");
2070
- console.log("Recommendations:");
2071
- for (const recommendation of report.recommendations) {
2072
- console.log(`- ${recommendation}`);
2073
- }
1261
+ if (!snapshot.valid || !snapshot.hash) {
1262
+ return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
2074
1263
  }
2075
- if (opts.verbose && report.logTail.length > 0) {
2076
- console.log("");
2077
- console.log("Recent logs:");
2078
- for (const line of report.logTail) {
2079
- console.log(line);
2080
- }
1264
+ if (params.baseHash !== snapshot.hash) {
1265
+ return { ok: false, error: "config changed since last load; re-run config.get and retry" };
2081
1266
  }
2082
- process.exitCode = report.exitCode;
2083
- }
2084
- async doctor(opts = {}) {
2085
- const report = await this.collectRuntimeStatus({
2086
- verbose: Boolean(opts.verbose),
2087
- fix: Boolean(opts.fix)
2088
- });
2089
- const checkPort = await this.checkPortAvailability({
2090
- host: report.process.running ? report.endpoints.uiUrl ? new URL(report.endpoints.uiUrl).hostname : "127.0.0.1" : "127.0.0.1",
2091
- port: (() => {
2092
- try {
2093
- const base = report.process.running && report.endpoints.uiUrl ? report.endpoints.uiUrl : report.endpoints.configuredUiUrl;
2094
- return Number(new URL(base).port || 80);
2095
- } catch {
2096
- return 18791;
2097
- }
2098
- })()
2099
- });
2100
- const providerConfigured = report.providers.some((provider) => provider.configured);
2101
- const checks = [
2102
- {
2103
- name: "config-file",
2104
- status: report.configExists ? "pass" : "fail",
2105
- detail: report.configPath
2106
- },
2107
- {
2108
- name: "workspace-dir",
2109
- status: report.workspaceExists ? "pass" : "warn",
2110
- detail: report.workspacePath
2111
- },
2112
- {
2113
- name: "service-state",
2114
- status: report.process.staleState ? "fail" : report.process.running ? "pass" : "warn",
2115
- detail: report.process.running ? `PID ${report.process.pid}` : report.process.staleState ? "state exists but process is not running" : "service not running"
2116
- },
2117
- {
2118
- name: "service-health",
2119
- status: report.process.running ? report.health.managed.state === "ok" ? "pass" : "fail" : report.health.configured.state === "ok" ? "warn" : "warn",
2120
- detail: report.process.running ? `${report.health.managed.state}: ${report.health.managed.detail}` : `${report.health.configured.state}: ${report.health.configured.detail}`
2121
- },
2122
- {
2123
- name: "ui-port-availability",
2124
- status: report.process.running ? "pass" : checkPort.available ? "pass" : "fail",
2125
- detail: report.process.running ? "managed by running service" : checkPort.available ? "available" : checkPort.detail
2126
- },
2127
- {
2128
- name: "provider-config",
2129
- status: providerConfigured ? "pass" : "warn",
2130
- detail: providerConfigured ? "at least one provider configured" : "no provider api key configured"
2131
- }
2132
- ];
2133
- const failed = checks.filter((check) => check.status === "fail");
2134
- const warned = checks.filter((check) => check.status === "warn");
2135
- const exitCode = failed.length > 0 ? 1 : warned.length > 0 ? 1 : 0;
2136
- if (opts.json) {
2137
- console.log(
2138
- JSON.stringify(
2139
- {
2140
- generatedAt: report.generatedAt,
2141
- checks,
2142
- status: report,
2143
- exitCode
2144
- },
2145
- null,
2146
- 2
2147
- )
2148
- );
2149
- process.exitCode = exitCode;
2150
- return;
1267
+ let parsedRaw;
1268
+ try {
1269
+ parsedRaw = JSON.parse(params.raw);
1270
+ } catch {
1271
+ return { ok: false, error: "invalid JSON in raw config" };
2151
1272
  }
2152
- console.log(`${this.logo} ${APP_NAME} Doctor`);
2153
- console.log(`Generated: ${report.generatedAt}`);
2154
- console.log("");
2155
- for (const check of checks) {
2156
- const icon = check.status === "pass" ? "\u2713" : check.status === "warn" ? "!" : "\u2717";
2157
- console.log(`${icon} ${check.name}: ${check.detail}`);
1273
+ let validated;
1274
+ try {
1275
+ validated = ConfigSchema.parse(parsedRaw);
1276
+ } catch (err) {
1277
+ return { ok: false, error: `invalid config: ${String(err)}` };
2158
1278
  }
2159
- if (report.recommendations.length > 0) {
2160
- console.log("");
2161
- console.log("Recommendations:");
2162
- for (const recommendation of report.recommendations) {
2163
- console.log(`- ${recommendation}`);
2164
- }
1279
+ this.deps.saveConfig(validated);
1280
+ const delayMs = params.restartDelayMs ?? 0;
1281
+ await this.requestRestart({ delayMs, reason: "config.apply" });
1282
+ return {
1283
+ ok: true,
1284
+ note: params.note ?? null,
1285
+ path: this.deps.getConfigPath(),
1286
+ config: redactValue(validated),
1287
+ restart: { scheduled: true, delayMs }
1288
+ };
1289
+ }
1290
+ async patchConfig(params) {
1291
+ const snapshot = readConfigSnapshot(this.deps.getConfigPath);
1292
+ if (!params.baseHash) {
1293
+ return { ok: false, error: "config base hash required; re-run config.get and retry" };
2165
1294
  }
2166
- if (opts.verbose && report.logTail.length > 0) {
2167
- console.log("");
2168
- console.log("Recent logs:");
2169
- for (const line of report.logTail) {
2170
- console.log(line);
2171
- }
1295
+ if (!snapshot.valid || !snapshot.hash) {
1296
+ return { ok: false, error: "config base hash unavailable; re-run config.get and retry" };
2172
1297
  }
2173
- process.exitCode = exitCode;
1298
+ if (params.baseHash !== snapshot.hash) {
1299
+ return { ok: false, error: "config changed since last load; re-run config.get and retry" };
1300
+ }
1301
+ let patch;
1302
+ try {
1303
+ patch = JSON.parse(params.raw);
1304
+ } catch {
1305
+ return { ok: false, error: "invalid JSON in raw config" };
1306
+ }
1307
+ const merged = mergeDeep(snapshot.config, patch);
1308
+ let validated;
1309
+ try {
1310
+ validated = ConfigSchema.parse(merged);
1311
+ } catch (err) {
1312
+ return { ok: false, error: `invalid config: ${String(err)}` };
1313
+ }
1314
+ this.deps.saveConfig(validated);
1315
+ const delayMs = params.restartDelayMs ?? 0;
1316
+ await this.requestRestart({ delayMs, reason: "config.patch" });
1317
+ return {
1318
+ ok: true,
1319
+ note: params.note ?? null,
1320
+ path: this.deps.getConfigPath(),
1321
+ config: redactValue(validated),
1322
+ restart: { scheduled: true, delayMs }
1323
+ };
2174
1324
  }
2175
- async collectRuntimeStatus(params) {
2176
- const configPath = getConfigPath();
2177
- const config2 = loadConfig();
2178
- const workspacePath = getWorkspacePath(config2.agents.defaults.workspace);
2179
- const serviceStatePath = resolve4(getDataDir2(), "run", "service.json");
2180
- const fixActions = [];
2181
- let serviceState = readServiceState();
2182
- if (params.fix && serviceState && !isProcessRunning(serviceState.pid)) {
2183
- clearServiceState();
2184
- fixActions.push("Cleared stale service state file.");
2185
- serviceState = readServiceState();
1325
+ async updateRun(params) {
1326
+ const result = runSelfUpdate({ timeoutMs: params.timeoutMs });
1327
+ if (!result.ok) {
1328
+ return { ok: false, error: result.error ?? "update failed", steps: result.steps };
2186
1329
  }
2187
- const managedByState = Boolean(serviceState);
2188
- const running = Boolean(serviceState && isProcessRunning(serviceState.pid));
2189
- const staleState = Boolean(serviceState && !running);
2190
- const configuredUi = resolveUiConfig(config2, { enabled: true, host: config2.ui.host, port: config2.ui.port });
2191
- const configuredUiUrl = resolveUiApiBase(configuredUi.host, configuredUi.port);
2192
- const configuredApiUrl = `${configuredUiUrl}/api`;
2193
- const managedUiUrl = serviceState?.uiUrl ?? null;
2194
- const managedApiUrl = serviceState?.apiUrl ?? null;
2195
- const managedHealth = running && managedApiUrl ? await this.probeApiHealth(`${managedApiUrl}/health`) : { state: "unreachable", detail: "service not running" };
2196
- const configuredHealth = await this.probeApiHealth(`${configuredApiUrl}/health`, 900);
2197
- const orphanSuspected = !running && configuredHealth.state === "ok";
2198
- const providers = PROVIDERS.map((spec) => {
2199
- const provider = config2.providers[spec.name];
2200
- if (!provider) {
2201
- return { name: spec.displayName ?? spec.name, configured: false, detail: "missing config" };
2202
- }
2203
- if (spec.isLocal) {
2204
- return {
2205
- name: spec.displayName ?? spec.name,
2206
- configured: Boolean(provider.apiBase),
2207
- detail: provider.apiBase ? provider.apiBase : "apiBase not set"
2208
- };
2209
- }
2210
- return {
2211
- name: spec.displayName ?? spec.name,
2212
- configured: Boolean(provider.apiKey),
2213
- detail: provider.apiKey ? "apiKey set" : "apiKey not set"
2214
- };
2215
- });
2216
- const issues = [];
2217
- const recommendations = [];
2218
- if (!existsSync4(configPath)) {
2219
- issues.push("Config file is missing.");
2220
- recommendations.push(`Run ${APP_NAME} init to create config files.`);
1330
+ const delayMs = params.restartDelayMs ?? 0;
1331
+ await this.requestRestart({ delayMs, reason: "update.run" });
1332
+ return {
1333
+ ok: true,
1334
+ note: params.note ?? null,
1335
+ restart: { scheduled: true, delayMs },
1336
+ strategy: result.strategy,
1337
+ steps: result.steps
1338
+ };
1339
+ }
1340
+ };
1341
+
1342
+ // src/cli/config-reloader.ts
1343
+ import {
1344
+ buildReloadPlan as buildReloadPlan2,
1345
+ diffConfigPaths as diffConfigPaths2,
1346
+ ChannelManager
1347
+ } from "@nextclaw/core";
1348
+ var ConfigReloader = class {
1349
+ constructor(options) {
1350
+ this.options = options;
1351
+ this.currentConfig = options.initialConfig;
1352
+ this.channels = options.channels;
1353
+ }
1354
+ currentConfig;
1355
+ channels;
1356
+ reloadTask = null;
1357
+ providerReloadTask = null;
1358
+ reloadTimer = null;
1359
+ reloadRunning = false;
1360
+ reloadPending = false;
1361
+ getChannels() {
1362
+ return this.channels;
1363
+ }
1364
+ setApplyAgentRuntimeConfig(callback) {
1365
+ this.options.applyAgentRuntimeConfig = callback;
1366
+ }
1367
+ async applyReloadPlan(nextConfig) {
1368
+ const changedPaths = diffConfigPaths2(this.currentConfig, nextConfig);
1369
+ if (!changedPaths.length) {
1370
+ return;
2221
1371
  }
2222
- if (!existsSync4(workspacePath)) {
2223
- issues.push("Workspace directory does not exist.");
2224
- recommendations.push(`Run ${APP_NAME} init to create workspace templates.`);
1372
+ this.currentConfig = nextConfig;
1373
+ const plan = buildReloadPlan2(changedPaths);
1374
+ if (plan.restartChannels) {
1375
+ await this.reloadChannels(nextConfig);
1376
+ console.log("Config reload: channels restarted.");
2225
1377
  }
2226
- if (staleState) {
2227
- issues.push("Service state is stale (state exists but process is not running).");
2228
- recommendations.push(`Run ${APP_NAME} status --fix to clean stale state.`);
1378
+ if (plan.reloadProviders) {
1379
+ await this.reloadProvider(nextConfig);
1380
+ console.log("Config reload: provider settings applied.");
2229
1381
  }
2230
- if (running && managedHealth.state !== "ok") {
2231
- issues.push(`Managed service health check failed: ${managedHealth.detail}`);
2232
- recommendations.push(`Check logs at ${serviceState?.logPath ?? resolveServiceLogPath()}.`);
1382
+ if (plan.reloadAgent) {
1383
+ this.options.applyAgentRuntimeConfig?.(nextConfig);
1384
+ console.log("Config reload: agent defaults applied.");
2233
1385
  }
2234
- if (!running) {
2235
- recommendations.push(`Run ${APP_NAME} start to launch the service.`);
1386
+ if (plan.restartRequired.length > 0) {
1387
+ this.options.onRestartRequired(plan.restartRequired);
2236
1388
  }
2237
- if (orphanSuspected) {
2238
- issues.push("A service appears healthy on configured API endpoint, but state is missing/stale.");
2239
- recommendations.push("Another process may be occupying the UI port; stop it or use --ui-port with a free port.");
1389
+ }
1390
+ scheduleReload(reason) {
1391
+ if (this.reloadTimer) {
1392
+ clearTimeout(this.reloadTimer);
2240
1393
  }
2241
- if (!providers.some((provider) => provider.configured)) {
2242
- recommendations.push("Configure at least one provider API key in UI or config before expecting agent replies.");
1394
+ this.reloadTimer = setTimeout(() => {
1395
+ void this.runReload(reason);
1396
+ }, 300);
1397
+ }
1398
+ async runReload(reason) {
1399
+ if (this.reloadRunning) {
1400
+ this.reloadPending = true;
1401
+ return;
1402
+ }
1403
+ this.reloadRunning = true;
1404
+ if (this.reloadTimer) {
1405
+ clearTimeout(this.reloadTimer);
1406
+ this.reloadTimer = null;
1407
+ }
1408
+ try {
1409
+ const nextConfig = this.options.loadConfig();
1410
+ await this.applyReloadPlan(nextConfig);
1411
+ } catch (error) {
1412
+ console.error(`Config reload failed (${reason}): ${String(error)}`);
1413
+ } finally {
1414
+ this.reloadRunning = false;
1415
+ if (this.reloadPending) {
1416
+ this.reloadPending = false;
1417
+ this.scheduleReload("pending");
1418
+ }
2243
1419
  }
2244
- const logTail = params.verbose ? this.readLogTail(serviceState?.logPath ?? resolveServiceLogPath(), 25) : [];
2245
- const level = running ? managedHealth.state === "ok" ? issues.length > 0 ? "degraded" : "healthy" : "degraded" : "stopped";
2246
- const exitCode = level === "healthy" ? 0 : level === "degraded" ? 1 : 2;
2247
- return {
2248
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2249
- configPath,
2250
- configExists: existsSync4(configPath),
2251
- workspacePath,
2252
- workspaceExists: existsSync4(workspacePath),
2253
- model: config2.agents.defaults.model,
2254
- providers,
2255
- serviceStatePath,
2256
- serviceStateExists: existsSync4(serviceStatePath),
2257
- fixActions,
2258
- process: {
2259
- managedByState,
2260
- pid: serviceState?.pid ?? null,
2261
- running,
2262
- staleState,
2263
- orphanSuspected,
2264
- startedAt: serviceState?.startedAt ?? null
2265
- },
2266
- endpoints: {
2267
- uiUrl: managedUiUrl,
2268
- apiUrl: managedApiUrl,
2269
- configuredUiUrl,
2270
- configuredApiUrl
2271
- },
2272
- health: {
2273
- managed: managedHealth,
2274
- configured: configuredHealth
2275
- },
2276
- issues,
2277
- recommendations,
2278
- logTail,
2279
- level,
2280
- exitCode
2281
- };
2282
1420
  }
2283
- async probeApiHealth(url, timeoutMs = 1500) {
2284
- const controller = new AbortController();
2285
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1421
+ async reloadConfig(reason) {
1422
+ await this.runReload(reason ?? "gateway tool");
1423
+ return "Config reload triggered";
1424
+ }
1425
+ async reloadChannels(nextConfig) {
1426
+ if (this.reloadTask) {
1427
+ await this.reloadTask;
1428
+ return;
1429
+ }
1430
+ this.reloadTask = (async () => {
1431
+ await this.channels.stopAll();
1432
+ this.channels = new ChannelManager(
1433
+ nextConfig,
1434
+ this.options.bus,
1435
+ this.options.sessionManager,
1436
+ this.options.getExtensionChannels?.() ?? []
1437
+ );
1438
+ await this.channels.startAll();
1439
+ })();
2286
1440
  try {
2287
- const response = await fetch(url, {
2288
- method: "GET",
2289
- signal: controller.signal
2290
- });
2291
- if (!response.ok) {
2292
- return { state: "invalid-response", detail: `HTTP ${response.status}` };
2293
- }
2294
- const payload = await response.json();
2295
- if (payload?.ok === true && payload?.data?.status === "ok") {
2296
- return { state: "ok", detail: "health endpoint returned ok", payload };
2297
- }
2298
- return { state: "invalid-response", detail: "unexpected health payload", payload };
2299
- } catch (error) {
2300
- return { state: "unreachable", detail: String(error) };
1441
+ await this.reloadTask;
2301
1442
  } finally {
2302
- clearTimeout(timer);
1443
+ this.reloadTask = null;
2303
1444
  }
2304
1445
  }
2305
- readLogTail(path, maxLines = 25) {
2306
- if (!existsSync4(path)) {
2307
- return [];
1446
+ async reloadProvider(nextConfig) {
1447
+ if (!this.options.providerManager) {
1448
+ return;
2308
1449
  }
2309
- try {
2310
- const lines = readFileSync3(path, "utf-8").split(/\r?\n/).filter(Boolean);
2311
- if (lines.length <= maxLines) {
2312
- return lines;
1450
+ if (this.providerReloadTask) {
1451
+ await this.providerReloadTask;
1452
+ return;
1453
+ }
1454
+ this.providerReloadTask = (async () => {
1455
+ const nextProvider = this.options.makeProvider(nextConfig);
1456
+ if (!nextProvider) {
1457
+ console.warn("Provider reload skipped: missing API key.");
1458
+ return;
2313
1459
  }
2314
- return lines.slice(lines.length - maxLines);
2315
- } catch {
2316
- return [];
1460
+ this.options.providerManager?.set(nextProvider);
1461
+ })();
1462
+ try {
1463
+ await this.providerReloadTask;
1464
+ } finally {
1465
+ this.providerReloadTask = null;
2317
1466
  }
2318
1467
  }
2319
- async checkPortAvailability(params) {
2320
- return await new Promise((resolve5) => {
2321
- const server = createNetServer();
2322
- server.once("error", (error) => {
2323
- resolve5({
2324
- available: false,
2325
- detail: `bind failed on ${params.host}:${params.port} (${String(error)})`
2326
- });
2327
- });
2328
- server.listen(params.port, params.host, () => {
2329
- server.close(() => {
2330
- resolve5({
2331
- available: true,
2332
- detail: `bind ok on ${params.host}:${params.port}`
2333
- });
2334
- });
2335
- });
2336
- });
1468
+ };
1469
+
1470
+ // src/cli/missing-provider.ts
1471
+ import { LLMProvider } from "@nextclaw/core";
1472
+ var MissingProvider = class extends LLMProvider {
1473
+ constructor(defaultModel) {
1474
+ super(null, null);
1475
+ this.defaultModel = defaultModel;
2337
1476
  }
2338
- loadPluginRegistry(config2, workspaceDir) {
2339
- return loadOpenClawPlugins({
2340
- config: config2,
2341
- workspaceDir,
2342
- reservedToolNames: [
2343
- "read_file",
2344
- "write_file",
2345
- "edit_file",
2346
- "list_dir",
2347
- "exec",
2348
- "web_search",
2349
- "web_fetch",
2350
- "message",
2351
- "spawn",
2352
- "sessions_list",
2353
- "sessions_history",
2354
- "sessions_send",
2355
- "memory_search",
2356
- "memory_get",
2357
- "subagents",
2358
- "gateway",
2359
- "cron"
2360
- ],
2361
- reservedChannelIds: Object.keys(config2.channels),
2362
- reservedProviderIds: PROVIDERS.map((provider) => provider.name),
2363
- logger: {
2364
- info: (message) => console.log(message),
2365
- warn: (message) => console.warn(message),
2366
- error: (message) => console.error(message),
2367
- debug: (message) => console.debug(message)
2368
- }
2369
- });
1477
+ setDefaultModel(model) {
1478
+ this.defaultModel = model;
2370
1479
  }
2371
- toExtensionRegistry(pluginRegistry) {
2372
- return {
2373
- tools: pluginRegistry.tools.map((tool) => ({
2374
- extensionId: tool.pluginId,
2375
- factory: tool.factory,
2376
- names: tool.names,
2377
- optional: tool.optional,
2378
- source: tool.source
2379
- })),
2380
- channels: pluginRegistry.channels.map((channel) => ({
2381
- extensionId: channel.pluginId,
2382
- channel: channel.channel,
2383
- source: channel.source
2384
- })),
2385
- diagnostics: pluginRegistry.diagnostics.map((diag) => ({
2386
- level: diag.level,
2387
- message: diag.message,
2388
- extensionId: diag.pluginId,
2389
- source: diag.source
2390
- }))
2391
- };
1480
+ async chat() {
1481
+ throw new Error("No API key configured yet. Configure provider credentials in UI and retry.");
2392
1482
  }
2393
- logPluginDiagnostics(registry) {
2394
- for (const diag of registry.diagnostics) {
2395
- const prefix = diag.pluginId ? `${diag.pluginId}: ` : "";
2396
- const text = `${prefix}${diag.message}`;
2397
- if (diag.level === "error") {
2398
- console.error(`[plugins] ${text}`);
2399
- } else {
2400
- console.warn(`[plugins] ${text}`);
2401
- }
2402
- }
1483
+ getDefaultModel() {
1484
+ return this.defaultModel;
1485
+ }
1486
+ };
1487
+
1488
+ // src/cli/commands/service.ts
1489
+ var ServiceCommands = class {
1490
+ constructor(deps) {
1491
+ this.deps = deps;
2403
1492
  }
2404
1493
  async startGateway(options = {}) {
2405
- const config2 = loadConfig();
2406
- const workspace = getWorkspacePath(config2.agents.defaults.workspace);
2407
- const pluginRegistry = this.loadPluginRegistry(config2, workspace);
2408
- const extensionRegistry = this.toExtensionRegistry(pluginRegistry);
2409
- this.logPluginDiagnostics(pluginRegistry);
1494
+ const config2 = loadConfig4();
1495
+ const workspace = getWorkspacePath2(config2.agents.defaults.workspace);
2410
1496
  const bus = new MessageBus();
2411
1497
  const provider = options.allowMissingProvider === true ? this.makeProvider(config2, { allowMissing: true }) : this.makeProvider(config2);
2412
1498
  const providerManager = new ProviderManager(provider ?? this.makeMissingProvider(config2));
2413
1499
  const sessionManager = new SessionManager(workspace);
2414
- const cronStorePath = join3(getDataDir2(), "cron", "jobs.json");
2415
- const cron2 = new CronService(cronStorePath);
2416
- const pluginUiMetadata = getPluginUiMetadataFromRegistry(pluginRegistry);
1500
+ const cronStorePath = join4(getDataDir4(), "cron", "jobs.json");
1501
+ const cron2 = new CronService2(cronStorePath);
2417
1502
  const uiConfig = resolveUiConfig(config2, options.uiOverrides);
2418
1503
  const uiStaticDir = options.uiStaticDir === void 0 ? resolveUiStaticDir() : options.uiStaticDir;
2419
1504
  if (!provider) {
2420
1505
  console.warn("Warning: No API key configured. The gateway is running, but agent replies are disabled until provider config is set.");
2421
1506
  }
2422
- const channels2 = new ChannelManager(config2, bus, sessionManager, extensionRegistry.channels);
1507
+ const channels2 = new ChannelManager2(config2, bus, sessionManager, []);
2423
1508
  const reloader = new ConfigReloader({
2424
1509
  initialConfig: config2,
2425
1510
  channels: channels2,
@@ -2427,10 +1512,9 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2427
1512
  sessionManager,
2428
1513
  providerManager,
2429
1514
  makeProvider: (nextConfig) => this.makeProvider(nextConfig, { allowMissing: true }) ?? this.makeMissingProvider(nextConfig),
2430
- loadConfig,
2431
- getExtensionChannels: () => extensionRegistry.channels,
1515
+ loadConfig: loadConfig4,
2432
1516
  onRestartRequired: (paths) => {
2433
- void this.requestRestart({
1517
+ void this.deps.requestRestart({
2434
1518
  reason: `config reload requires restart: ${paths.join(", ")}`,
2435
1519
  manualMessage: `Config changes require restart: ${paths.join(", ")}`,
2436
1520
  strategy: "background-service-or-manual"
@@ -2440,11 +1524,10 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2440
1524
  const gatewayController = new GatewayControllerImpl({
2441
1525
  reloader,
2442
1526
  cron: cron2,
2443
- getConfigPath,
2444
- saveConfig,
2445
- getPluginUiMetadata: () => pluginUiMetadata,
1527
+ getConfigPath: getConfigPath2,
1528
+ saveConfig: saveConfig2,
2446
1529
  requestRestart: async (options2) => {
2447
- await this.requestRestart({
1530
+ await this.deps.requestRestart({
2448
1531
  reason: options2?.reason ?? "gateway tool restart",
2449
1532
  manualMessage: "Restart the gateway to apply changes.",
2450
1533
  strategy: "background-service-or-exit",
@@ -2468,55 +1551,9 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2468
1551
  sessionManager,
2469
1552
  contextConfig: config2.agents.context,
2470
1553
  gatewayController,
2471
- config: config2,
2472
- extensionRegistry,
2473
- resolveMessageToolHints: ({ channel, accountId }) => resolvePluginChannelMessageToolHints({
2474
- registry: pluginRegistry,
2475
- channel,
2476
- cfg: loadConfig(),
2477
- accountId
2478
- })
1554
+ config: config2
2479
1555
  });
2480
1556
  reloader.setApplyAgentRuntimeConfig((nextConfig) => agent.applyRuntimeConfig(nextConfig));
2481
- const pluginChannelBindings = getPluginChannelBindings(pluginRegistry);
2482
- setPluginRuntimeBridge({
2483
- loadConfig: () => this.toPluginConfigView(loadConfig(), pluginChannelBindings),
2484
- writeConfigFile: async (nextConfigView) => {
2485
- if (!nextConfigView || typeof nextConfigView !== "object" || Array.isArray(nextConfigView)) {
2486
- throw new Error("plugin runtime writeConfigFile expects an object config");
2487
- }
2488
- const current = loadConfig();
2489
- const next = this.mergePluginConfigView(current, nextConfigView, pluginChannelBindings);
2490
- saveConfig(next);
2491
- },
2492
- dispatchReplyWithBufferedBlockDispatcher: async ({ ctx, dispatcherOptions }) => {
2493
- const bodyForAgent = typeof ctx.BodyForAgent === "string" ? ctx.BodyForAgent : "";
2494
- const body = typeof ctx.Body === "string" ? ctx.Body : "";
2495
- const content = (bodyForAgent || body).trim();
2496
- if (!content) {
2497
- return;
2498
- }
2499
- const sessionKey = typeof ctx.SessionKey === "string" && ctx.SessionKey.trim().length > 0 ? ctx.SessionKey : `plugin:${typeof ctx.OriginatingChannel === "string" ? ctx.OriginatingChannel : "channel"}:${typeof ctx.SenderId === "string" ? ctx.SenderId : "unknown"}`;
2500
- const channel = typeof ctx.OriginatingChannel === "string" && ctx.OriginatingChannel.trim().length > 0 ? ctx.OriginatingChannel : "cli";
2501
- const chatId = typeof ctx.OriginatingTo === "string" && ctx.OriginatingTo.trim().length > 0 ? ctx.OriginatingTo : typeof ctx.SenderId === "string" && ctx.SenderId.trim().length > 0 ? ctx.SenderId : "direct";
2502
- try {
2503
- const response = await agent.processDirect({
2504
- content,
2505
- sessionKey,
2506
- channel,
2507
- chatId,
2508
- metadata: typeof ctx.AccountId === "string" && ctx.AccountId.trim().length > 0 ? { account_id: ctx.AccountId } : {}
2509
- });
2510
- const replyText = typeof response === "string" ? response : String(response ?? "");
2511
- if (replyText.trim()) {
2512
- await dispatcherOptions.deliver({ text: replyText }, { kind: "final" });
2513
- }
2514
- } catch (error) {
2515
- dispatcherOptions.onError?.(error);
2516
- throw error;
2517
- }
2518
- }
2519
- });
2520
1557
  cron2.onJob = async (job) => {
2521
1558
  const response = await agent.processDirect({
2522
1559
  content: job.payload.message,
@@ -2552,7 +1589,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2552
1589
  console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
2553
1590
  }
2554
1591
  console.log("\u2713 Heartbeat: every 30m");
2555
- const configPath = getConfigPath();
1592
+ const configPath = getConfigPath2();
2556
1593
  const watcher = chokidar.watch(configPath, {
2557
1594
  ignoreInitial: true,
2558
1595
  awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
@@ -2562,32 +1599,216 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2562
1599
  watcher.on("unlink", () => reloader.scheduleReload("config unlink"));
2563
1600
  await cron2.start();
2564
1601
  await heartbeat.start();
2565
- let pluginGatewayHandles = [];
1602
+ await Promise.allSettled([agent.run(), reloader.getChannels().startAll()]);
1603
+ }
1604
+ async runForeground(options) {
1605
+ const config2 = loadConfig4();
1606
+ const uiConfig = resolveUiConfig(config2, options.uiOverrides);
1607
+ const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
1608
+ if (options.open) {
1609
+ openBrowser(uiUrl);
1610
+ }
1611
+ await this.startGateway({
1612
+ uiOverrides: options.uiOverrides,
1613
+ allowMissingProvider: true,
1614
+ uiStaticDir: resolveUiStaticDir()
1615
+ });
1616
+ }
1617
+ async startService(options) {
1618
+ const config2 = loadConfig4();
1619
+ const uiConfig = resolveUiConfig(config2, options.uiOverrides);
1620
+ const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
1621
+ const apiUrl = `${uiUrl}/api`;
1622
+ const staticDir = resolveUiStaticDir();
1623
+ const existing = readServiceState();
1624
+ if (existing && isProcessRunning(existing.pid)) {
1625
+ console.log(`\u2713 ${APP_NAME2} is already running (PID ${existing.pid})`);
1626
+ console.log(`UI: ${existing.uiUrl}`);
1627
+ console.log(`API: ${existing.apiUrl}`);
1628
+ const parsedUi = (() => {
1629
+ try {
1630
+ const parsed = new URL(existing.uiUrl);
1631
+ const port = Number(parsed.port || 80);
1632
+ return {
1633
+ host: existing.uiHost ?? parsed.hostname,
1634
+ port: Number.isFinite(port) ? port : existing.uiPort ?? 18791
1635
+ };
1636
+ } catch {
1637
+ return {
1638
+ host: existing.uiHost ?? "127.0.0.1",
1639
+ port: existing.uiPort ?? 18791
1640
+ };
1641
+ }
1642
+ })();
1643
+ if (parsedUi.host !== uiConfig.host || parsedUi.port !== uiConfig.port) {
1644
+ console.log(
1645
+ `Detected running service UI bind (${parsedUi.host}:${parsedUi.port}); enforcing (${uiConfig.host}:${uiConfig.port})...`
1646
+ );
1647
+ await this.stopService();
1648
+ const stateAfterStop = readServiceState();
1649
+ if (stateAfterStop && isProcessRunning(stateAfterStop.pid)) {
1650
+ console.error("Error: Failed to stop running service while enforcing public UI exposure.");
1651
+ return;
1652
+ }
1653
+ return this.startService(options);
1654
+ }
1655
+ await this.printPublicUiUrls(parsedUi.host, parsedUi.port);
1656
+ console.log(`Logs: ${existing.logPath}`);
1657
+ console.log(`Stop: ${APP_NAME2} stop`);
1658
+ return;
1659
+ }
1660
+ if (existing) {
1661
+ clearServiceState();
1662
+ }
1663
+ if (!staticDir) {
1664
+ console.log("Warning: UI frontend not found in package assets.");
1665
+ }
1666
+ const logPath = resolveServiceLogPath();
1667
+ const logDir = resolve5(logPath, "..");
1668
+ mkdirSync2(logDir, { recursive: true });
1669
+ const logFd = openSync(logPath, "a");
1670
+ const serveArgs = buildServeArgs({
1671
+ uiPort: uiConfig.port
1672
+ });
1673
+ const child = spawn2(process.execPath, [...process.execArgv, ...serveArgs], {
1674
+ env: process.env,
1675
+ stdio: ["ignore", logFd, logFd],
1676
+ detached: true
1677
+ });
1678
+ closeSync(logFd);
1679
+ if (!child.pid) {
1680
+ console.error("Error: Failed to start background service.");
1681
+ return;
1682
+ }
1683
+ const healthUrl = `${apiUrl}/health`;
1684
+ const started = await this.waitForBackgroundServiceReady({
1685
+ pid: child.pid,
1686
+ healthUrl,
1687
+ timeoutMs: 8e3
1688
+ });
1689
+ if (!started) {
1690
+ if (isProcessRunning(child.pid)) {
1691
+ try {
1692
+ process.kill(child.pid, "SIGTERM");
1693
+ await waitForExit(child.pid, 2e3);
1694
+ } catch {
1695
+ }
1696
+ }
1697
+ clearServiceState();
1698
+ console.error(`Error: Failed to start background service. Check logs: ${logPath}`);
1699
+ return;
1700
+ }
1701
+ child.unref();
1702
+ const state = {
1703
+ pid: child.pid,
1704
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1705
+ uiUrl,
1706
+ apiUrl,
1707
+ uiHost: uiConfig.host,
1708
+ uiPort: uiConfig.port,
1709
+ logPath
1710
+ };
1711
+ writeServiceState(state);
1712
+ console.log(`\u2713 ${APP_NAME2} started in background (PID ${state.pid})`);
1713
+ console.log(`UI: ${uiUrl}`);
1714
+ console.log(`API: ${apiUrl}`);
1715
+ await this.printPublicUiUrls(uiConfig.host, uiConfig.port);
1716
+ console.log(`Logs: ${logPath}`);
1717
+ console.log(`Stop: ${APP_NAME2} stop`);
1718
+ if (options.open) {
1719
+ openBrowser(uiUrl);
1720
+ }
1721
+ }
1722
+ async stopService() {
1723
+ const state = readServiceState();
1724
+ if (!state) {
1725
+ console.log("No running service found.");
1726
+ return;
1727
+ }
1728
+ if (!isProcessRunning(state.pid)) {
1729
+ console.log("Service is not running. Cleaning up state.");
1730
+ clearServiceState();
1731
+ return;
1732
+ }
1733
+ console.log(`Stopping ${APP_NAME2} (PID ${state.pid})...`);
2566
1734
  try {
2567
- const startedPluginGateways = await startPluginChannelGateways({
2568
- registry: pluginRegistry,
2569
- logger: {
2570
- info: (message) => console.log(`[plugins] ${message}`),
2571
- warn: (message) => console.warn(`[plugins] ${message}`),
2572
- error: (message) => console.error(`[plugins] ${message}`),
2573
- debug: (message) => console.debug(`[plugins] ${message}`)
1735
+ process.kill(state.pid, "SIGTERM");
1736
+ } catch (error) {
1737
+ console.error(`Failed to stop service: ${String(error)}`);
1738
+ return;
1739
+ }
1740
+ const stopped = await waitForExit(state.pid, 3e3);
1741
+ if (!stopped) {
1742
+ try {
1743
+ process.kill(state.pid, "SIGKILL");
1744
+ } catch (error) {
1745
+ console.error(`Failed to force stop service: ${String(error)}`);
1746
+ return;
1747
+ }
1748
+ await waitForExit(state.pid, 2e3);
1749
+ }
1750
+ clearServiceState();
1751
+ console.log(`\u2713 ${APP_NAME2} stopped`);
1752
+ }
1753
+ async waitForBackgroundServiceReady(params) {
1754
+ const startedAt = Date.now();
1755
+ while (Date.now() - startedAt < params.timeoutMs) {
1756
+ if (!isProcessRunning(params.pid)) {
1757
+ return false;
1758
+ }
1759
+ try {
1760
+ const response = await fetch(params.healthUrl, { method: "GET" });
1761
+ if (!response.ok) {
1762
+ await new Promise((resolve8) => setTimeout(resolve8, 200));
1763
+ continue;
2574
1764
  }
2575
- });
2576
- pluginGatewayHandles = startedPluginGateways.handles;
2577
- for (const diag of startedPluginGateways.diagnostics) {
2578
- const prefix = diag.pluginId ? `${diag.pluginId}: ` : "";
2579
- const text = `${prefix}${diag.message}`;
2580
- if (diag.level === "error") {
2581
- console.error(`[plugins] ${text}`);
2582
- } else {
2583
- console.warn(`[plugins] ${text}`);
1765
+ const payload = await response.json();
1766
+ const healthy = payload?.ok === true && payload?.data?.status === "ok";
1767
+ if (!healthy) {
1768
+ await new Promise((resolve8) => setTimeout(resolve8, 200));
1769
+ continue;
1770
+ }
1771
+ await new Promise((resolve8) => setTimeout(resolve8, 300));
1772
+ if (isProcessRunning(params.pid)) {
1773
+ return true;
2584
1774
  }
1775
+ } catch {
2585
1776
  }
2586
- await Promise.allSettled([agent.run(), reloader.getChannels().startAll()]);
2587
- } finally {
2588
- await stopPluginChannelGateways(pluginGatewayHandles);
2589
- setPluginRuntimeBridge(null);
1777
+ await new Promise((resolve8) => setTimeout(resolve8, 200));
1778
+ }
1779
+ return false;
1780
+ }
1781
+ createMissingProvider(config2) {
1782
+ return this.makeMissingProvider(config2);
1783
+ }
1784
+ createProvider(config2, options) {
1785
+ if (options?.allowMissing) {
1786
+ return this.makeProvider(config2, { allowMissing: true });
1787
+ }
1788
+ return this.makeProvider(config2);
1789
+ }
1790
+ makeMissingProvider(config2) {
1791
+ return new MissingProvider(config2.agents.defaults.model);
1792
+ }
1793
+ makeProvider(config2, options) {
1794
+ const provider = getProvider(config2);
1795
+ const model = config2.agents.defaults.model;
1796
+ if (!provider?.apiKey && !model.startsWith("bedrock/")) {
1797
+ if (options?.allowMissing) {
1798
+ return null;
1799
+ }
1800
+ console.error("Error: No API key configured.");
1801
+ console.error(`Set one in ${getConfigPath2()} under providers section`);
1802
+ process.exit(1);
2590
1803
  }
1804
+ return new LiteLLMProvider({
1805
+ apiKey: provider?.apiKey ?? null,
1806
+ apiBase: getApiBase(config2),
1807
+ defaultModel: model,
1808
+ extraHeaders: provider?.extraHeaders ?? null,
1809
+ providerName: getProviderName(config2),
1810
+ wireApi: provider?.wireApi ?? null
1811
+ });
2591
1812
  }
2592
1813
  async printPublicUiUrls(host, port) {
2593
1814
  if (isLoopbackHost(host)) {
@@ -2610,7 +1831,7 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2610
1831
  const uiServer = startUiServer({
2611
1832
  host: uiConfig.host,
2612
1833
  port: uiConfig.port,
2613
- configPath: getConfigPath(),
1834
+ configPath: getConfigPath2(),
2614
1835
  staticDir: uiStaticDir ?? void 0
2615
1836
  });
2616
1837
  const uiUrl = `http://${uiServer.host}:${uiServer.port}`;
@@ -2623,425 +1844,652 @@ ${this.logo} ${APP_NAME} is ready! (${source})`);
2623
1844
  openBrowser(uiUrl);
2624
1845
  }
2625
1846
  }
2626
- async runForeground(options) {
2627
- const config2 = loadConfig();
2628
- const uiConfig = resolveUiConfig(config2, options.uiOverrides);
2629
- const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
2630
- if (options.open) {
2631
- openBrowser(uiUrl);
1847
+ };
1848
+
1849
+ // src/cli/workspace.ts
1850
+ import { cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
1851
+ import { createRequire } from "module";
1852
+ import { dirname, join as join5, resolve as resolve6 } from "path";
1853
+ import { fileURLToPath as fileURLToPath2 } from "url";
1854
+ import { APP_NAME as APP_NAME3, getDataDir as getDataDir5 } from "@nextclaw/core";
1855
+ import { spawnSync as spawnSync4 } from "child_process";
1856
+ var WorkspaceManager = class {
1857
+ constructor(logo) {
1858
+ this.logo = logo;
1859
+ }
1860
+ createWorkspaceTemplates(workspace, options = {}) {
1861
+ const created = [];
1862
+ const force = Boolean(options.force);
1863
+ const templateDir = this.resolveTemplateDir();
1864
+ if (!templateDir) {
1865
+ console.warn("Warning: Template directory not found. Skipping workspace templates.");
1866
+ return { created };
2632
1867
  }
2633
- await this.startGateway({
2634
- uiOverrides: options.uiOverrides,
2635
- allowMissingProvider: true,
2636
- uiStaticDir: resolveUiStaticDir()
1868
+ const templateFiles = [
1869
+ { source: "AGENTS.md", target: "AGENTS.md" },
1870
+ { source: "SOUL.md", target: "SOUL.md" },
1871
+ { source: "USER.md", target: "USER.md" },
1872
+ { source: "IDENTITY.md", target: "IDENTITY.md" },
1873
+ { source: "TOOLS.md", target: "TOOLS.md" },
1874
+ { source: "USAGE.md", target: "USAGE.md" },
1875
+ { source: "BOOT.md", target: "BOOT.md" },
1876
+ { source: "BOOTSTRAP.md", target: "BOOTSTRAP.md" },
1877
+ { source: "HEARTBEAT.md", target: "HEARTBEAT.md" },
1878
+ { source: "MEMORY.md", target: "MEMORY.md" },
1879
+ { source: "memory/MEMORY.md", target: "memory/MEMORY.md" }
1880
+ ];
1881
+ for (const entry of templateFiles) {
1882
+ const filePath = join5(workspace, entry.target);
1883
+ if (!force && existsSync5(filePath)) {
1884
+ continue;
1885
+ }
1886
+ const templatePath = join5(templateDir, entry.source);
1887
+ if (!existsSync5(templatePath)) {
1888
+ console.warn(`Warning: Template file missing: ${templatePath}`);
1889
+ continue;
1890
+ }
1891
+ const raw = readFileSync4(templatePath, "utf-8");
1892
+ const content = raw.replace(/\$\{APP_NAME\}/g, APP_NAME3);
1893
+ mkdirSync3(dirname(filePath), { recursive: true });
1894
+ writeFileSync2(filePath, content);
1895
+ created.push(entry.target);
1896
+ }
1897
+ const memoryDir = join5(workspace, "memory");
1898
+ if (!existsSync5(memoryDir)) {
1899
+ mkdirSync3(memoryDir, { recursive: true });
1900
+ created.push(join5("memory", ""));
1901
+ }
1902
+ const skillsDir = join5(workspace, "skills");
1903
+ if (!existsSync5(skillsDir)) {
1904
+ mkdirSync3(skillsDir, { recursive: true });
1905
+ created.push(join5("skills", ""));
1906
+ }
1907
+ const seeded = this.seedBuiltinSkills(skillsDir, { force });
1908
+ if (seeded > 0) {
1909
+ created.push(`skills (seeded ${seeded} built-ins)`);
1910
+ }
1911
+ return { created };
1912
+ }
1913
+ seedBuiltinSkills(targetDir, options = {}) {
1914
+ const sourceDir = this.resolveBuiltinSkillsDir();
1915
+ if (!sourceDir) {
1916
+ return 0;
1917
+ }
1918
+ const force = Boolean(options.force);
1919
+ let seeded = 0;
1920
+ for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
1921
+ if (!entry.isDirectory()) {
1922
+ continue;
1923
+ }
1924
+ const src = join5(sourceDir, entry.name);
1925
+ if (!existsSync5(join5(src, "SKILL.md"))) {
1926
+ continue;
1927
+ }
1928
+ const dest = join5(targetDir, entry.name);
1929
+ if (!force && existsSync5(dest)) {
1930
+ continue;
1931
+ }
1932
+ cpSync(src, dest, { recursive: true, force: true });
1933
+ seeded += 1;
1934
+ }
1935
+ return seeded;
1936
+ }
1937
+ resolveBuiltinSkillsDir() {
1938
+ try {
1939
+ const require2 = createRequire(import.meta.url);
1940
+ const entry = require2.resolve("@nextclaw/core");
1941
+ const pkgRoot = resolve6(dirname(entry), "..");
1942
+ const distSkills = join5(pkgRoot, "dist", "skills");
1943
+ if (existsSync5(distSkills)) {
1944
+ return distSkills;
1945
+ }
1946
+ const srcSkills = join5(pkgRoot, "src", "agent", "skills");
1947
+ if (existsSync5(srcSkills)) {
1948
+ return srcSkills;
1949
+ }
1950
+ return null;
1951
+ } catch {
1952
+ return null;
1953
+ }
1954
+ }
1955
+ resolveTemplateDir() {
1956
+ const override = process.env.NEXTCLAW_TEMPLATE_DIR?.trim();
1957
+ if (override) {
1958
+ return override;
1959
+ }
1960
+ const cliDir = resolve6(fileURLToPath2(new URL(".", import.meta.url)));
1961
+ const pkgRoot = resolve6(cliDir, "..", "..");
1962
+ const candidates = [join5(pkgRoot, "templates")];
1963
+ for (const candidate of candidates) {
1964
+ if (existsSync5(candidate)) {
1965
+ return candidate;
1966
+ }
1967
+ }
1968
+ return null;
1969
+ }
1970
+ getBridgeDir() {
1971
+ const userBridge = join5(getDataDir5(), "bridge");
1972
+ if (existsSync5(join5(userBridge, "dist", "index.js"))) {
1973
+ return userBridge;
1974
+ }
1975
+ if (!which("npm")) {
1976
+ console.error("npm not found. Please install Node.js >= 18.");
1977
+ process.exit(1);
1978
+ }
1979
+ const cliDir = resolve6(fileURLToPath2(new URL(".", import.meta.url)));
1980
+ const pkgRoot = resolve6(cliDir, "..", "..");
1981
+ const pkgBridge = join5(pkgRoot, "bridge");
1982
+ const srcBridge = join5(pkgRoot, "..", "..", "bridge");
1983
+ let source = null;
1984
+ if (existsSync5(join5(pkgBridge, "package.json"))) {
1985
+ source = pkgBridge;
1986
+ } else if (existsSync5(join5(srcBridge, "package.json"))) {
1987
+ source = srcBridge;
1988
+ }
1989
+ if (!source) {
1990
+ console.error(`Bridge source not found. Try reinstalling ${APP_NAME3}.`);
1991
+ process.exit(1);
1992
+ }
1993
+ console.log(`${this.logo} Setting up bridge...`);
1994
+ mkdirSync3(resolve6(userBridge, ".."), { recursive: true });
1995
+ if (existsSync5(userBridge)) {
1996
+ rmSync2(userBridge, { recursive: true, force: true });
1997
+ }
1998
+ cpSync(source, userBridge, {
1999
+ recursive: true,
2000
+ filter: (src) => !src.includes("node_modules") && !src.includes("dist")
2637
2001
  });
2002
+ const install = spawnSync4("npm", ["install"], { cwd: userBridge, stdio: "pipe" });
2003
+ if (install.status !== 0) {
2004
+ console.error(`Bridge install failed: ${install.status ?? 1}`);
2005
+ if (install.stderr) {
2006
+ console.error(String(install.stderr).slice(0, 500));
2007
+ }
2008
+ process.exit(1);
2009
+ }
2010
+ const build = spawnSync4("npm", ["run", "build"], { cwd: userBridge, stdio: "pipe" });
2011
+ if (build.status !== 0) {
2012
+ console.error(`Bridge build failed: ${build.status ?? 1}`);
2013
+ if (build.stderr) {
2014
+ console.error(String(build.stderr).slice(0, 500));
2015
+ }
2016
+ process.exit(1);
2017
+ }
2018
+ console.log("\u2713 Bridge ready\n");
2019
+ return userBridge;
2638
2020
  }
2639
- async startService(options) {
2640
- const config2 = loadConfig();
2641
- const uiConfig = resolveUiConfig(config2, options.uiOverrides);
2642
- const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
2643
- const apiUrl = `${uiUrl}/api`;
2644
- const staticDir = resolveUiStaticDir();
2645
- const existing = readServiceState();
2646
- if (existing && isProcessRunning(existing.pid)) {
2647
- console.log(`\u2713 ${APP_NAME} is already running (PID ${existing.pid})`);
2648
- console.log(`UI: ${existing.uiUrl}`);
2649
- console.log(`API: ${existing.apiUrl}`);
2650
- const parsedUi = (() => {
2651
- try {
2652
- const parsed = new URL(existing.uiUrl);
2653
- const port = Number(parsed.port || 80);
2654
- return {
2655
- host: existing.uiHost ?? parsed.hostname,
2656
- port: Number.isFinite(port) ? port : existing.uiPort ?? 18791
2657
- };
2658
- } catch {
2659
- return {
2660
- host: existing.uiHost ?? "127.0.0.1",
2661
- port: existing.uiPort ?? 18791
2662
- };
2663
- }
2664
- })();
2665
- if (parsedUi.host !== uiConfig.host || parsedUi.port !== uiConfig.port) {
2666
- console.log(
2667
- `Detected running service UI bind (${parsedUi.host}:${parsedUi.port}); enforcing (${uiConfig.host}:${uiConfig.port})...`
2668
- );
2669
- await this.stopService();
2670
- const stateAfterStop = readServiceState();
2671
- if (stateAfterStop && isProcessRunning(stateAfterStop.pid)) {
2672
- console.error("Error: Failed to stop running service while enforcing public UI exposure.");
2673
- return;
2674
- }
2675
- return this.startService(options);
2021
+ };
2022
+
2023
+ // src/cli/runtime.ts
2024
+ var LOGO = "\u{1F916}";
2025
+ var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "/exit", "/quit", ":q"]);
2026
+ var FORCED_PUBLIC_UI_HOST = "0.0.0.0";
2027
+ var CliRuntime = class {
2028
+ logo;
2029
+ restartCoordinator;
2030
+ serviceRestartTask = null;
2031
+ selfRelaunchArmed = false;
2032
+ workspaceManager;
2033
+ serviceCommands;
2034
+ configCommands;
2035
+ channelCommands;
2036
+ cronCommands;
2037
+ diagnosticsCommands;
2038
+ constructor(options = {}) {
2039
+ this.logo = options.logo ?? LOGO;
2040
+ this.workspaceManager = new WorkspaceManager(this.logo);
2041
+ this.serviceCommands = new ServiceCommands({
2042
+ requestRestart: (params) => this.requestRestart(params)
2043
+ });
2044
+ this.configCommands = new ConfigCommands({
2045
+ requestRestart: (params) => this.requestRestart(params)
2046
+ });
2047
+ this.channelCommands = new ChannelCommands({
2048
+ logo: this.logo,
2049
+ getBridgeDir: () => this.workspaceManager.getBridgeDir()
2050
+ });
2051
+ this.cronCommands = new CronCommands();
2052
+ this.diagnosticsCommands = new DiagnosticsCommands({ logo: this.logo });
2053
+ this.restartCoordinator = new RestartCoordinator({
2054
+ readServiceState,
2055
+ isProcessRunning,
2056
+ currentPid: () => process.pid,
2057
+ restartBackgroundService: async (reason) => this.restartBackgroundService(reason),
2058
+ scheduleProcessExit: (delayMs, reason) => this.scheduleProcessExit(delayMs, reason)
2059
+ });
2060
+ }
2061
+ get version() {
2062
+ return getPackageVersion();
2063
+ }
2064
+ scheduleProcessExit(delayMs, reason) {
2065
+ console.warn(`Gateway restart requested (${reason}).`);
2066
+ setTimeout(() => {
2067
+ process.exit(0);
2068
+ }, delayMs);
2069
+ }
2070
+ async restartBackgroundService(reason) {
2071
+ if (this.serviceRestartTask) {
2072
+ return this.serviceRestartTask;
2073
+ }
2074
+ this.serviceRestartTask = (async () => {
2075
+ const state = readServiceState();
2076
+ if (!state || !isProcessRunning(state.pid) || state.pid === process.pid) {
2077
+ return false;
2676
2078
  }
2677
- await this.printPublicUiUrls(parsedUi.host, parsedUi.port);
2678
- console.log(`Logs: ${existing.logPath}`);
2679
- console.log(`Stop: ${APP_NAME} stop`);
2079
+ const uiHost = FORCED_PUBLIC_UI_HOST;
2080
+ const uiPort = typeof state.uiPort === "number" && Number.isFinite(state.uiPort) ? state.uiPort : 18791;
2081
+ console.log(`Applying changes (${reason}): restarting ${APP_NAME4} background service...`);
2082
+ await this.serviceCommands.stopService();
2083
+ await this.serviceCommands.startService({
2084
+ uiOverrides: {
2085
+ enabled: true,
2086
+ host: uiHost,
2087
+ port: uiPort
2088
+ },
2089
+ open: false
2090
+ });
2091
+ return true;
2092
+ })();
2093
+ try {
2094
+ return await this.serviceRestartTask;
2095
+ } finally {
2096
+ this.serviceRestartTask = null;
2097
+ }
2098
+ }
2099
+ armManagedServiceRelaunch(params) {
2100
+ const strategy = params.strategy ?? "background-service-or-manual";
2101
+ if (strategy !== "background-service-or-exit" && strategy !== "exit-process") {
2680
2102
  return;
2681
2103
  }
2682
- if (existing) {
2683
- clearServiceState();
2104
+ if (this.selfRelaunchArmed) {
2105
+ return;
2684
2106
  }
2685
- if (!staticDir) {
2686
- console.log("Warning: UI frontend not found in package assets.");
2107
+ const state = readServiceState();
2108
+ if (!state || state.pid !== process.pid) {
2109
+ return;
2687
2110
  }
2688
- const logPath = resolveServiceLogPath();
2689
- const logDir = resolve4(logPath, "..");
2690
- mkdirSync2(logDir, { recursive: true });
2691
- const logFd = openSync(logPath, "a");
2692
- const serveArgs = buildServeArgs({
2693
- uiPort: uiConfig.port
2111
+ const uiPort = typeof state.uiPort === "number" && Number.isFinite(state.uiPort) ? state.uiPort : 18791;
2112
+ const delayMs = typeof params.delayMs === "number" && Number.isFinite(params.delayMs) ? Math.max(0, Math.floor(params.delayMs)) : 100;
2113
+ const cliPath = process.env.NEXTCLAW_SELF_RELAUNCH_CLI?.trim() || fileURLToPath3(new URL("./index.js", import.meta.url));
2114
+ const startArgs = [cliPath, "start", "--ui-port", String(uiPort)];
2115
+ const serviceStatePath = resolve7(getDataDir6(), "run", "service.json");
2116
+ const helperScript = [
2117
+ 'const { spawnSync } = require("node:child_process");',
2118
+ 'const { readFileSync } = require("node:fs");',
2119
+ `const parentPid = ${process.pid};`,
2120
+ `const delayMs = ${delayMs};`,
2121
+ "const maxWaitMs = 120000;",
2122
+ "const retryIntervalMs = 1000;",
2123
+ "const startTimeoutMs = 60000;",
2124
+ `const nodePath = ${JSON.stringify(process.execPath)};`,
2125
+ `const startArgs = ${JSON.stringify(startArgs)};`,
2126
+ `const serviceStatePath = ${JSON.stringify(serviceStatePath)};`,
2127
+ "function isRunning(pid) {",
2128
+ " try {",
2129
+ " process.kill(pid, 0);",
2130
+ " return true;",
2131
+ " } catch {",
2132
+ " return false;",
2133
+ " }",
2134
+ "}",
2135
+ "function hasReplacementService() {",
2136
+ " try {",
2137
+ ' const raw = readFileSync(serviceStatePath, "utf-8");',
2138
+ " const state = JSON.parse(raw);",
2139
+ " const pid = Number(state?.pid);",
2140
+ " return Number.isFinite(pid) && pid > 0 && pid !== parentPid && isRunning(pid);",
2141
+ " } catch {",
2142
+ " return false;",
2143
+ " }",
2144
+ "}",
2145
+ "function tryStart() {",
2146
+ " spawnSync(nodePath, startArgs, {",
2147
+ ' stdio: "ignore",',
2148
+ " env: process.env,",
2149
+ " timeout: startTimeoutMs",
2150
+ " });",
2151
+ "}",
2152
+ "setTimeout(() => {",
2153
+ " const startedAt = Date.now();",
2154
+ " const tick = () => {",
2155
+ " if (hasReplacementService()) {",
2156
+ " process.exit(0);",
2157
+ " return;",
2158
+ " }",
2159
+ " if (Date.now() - startedAt >= maxWaitMs) {",
2160
+ " process.exit(0);",
2161
+ " return;",
2162
+ " }",
2163
+ " tryStart();",
2164
+ " if (hasReplacementService()) {",
2165
+ " process.exit(0);",
2166
+ " return;",
2167
+ " }",
2168
+ " setTimeout(tick, retryIntervalMs);",
2169
+ " };",
2170
+ " tick();",
2171
+ "}, delayMs);"
2172
+ ].join("\n");
2173
+ try {
2174
+ const helper = spawn3(process.execPath, ["-e", helperScript], {
2175
+ detached: true,
2176
+ stdio: "ignore",
2177
+ env: process.env
2178
+ });
2179
+ helper.unref();
2180
+ this.selfRelaunchArmed = true;
2181
+ console.warn(`Gateway self-restart armed (${params.reason}).`);
2182
+ } catch (error) {
2183
+ console.error(`Failed to arm gateway self-restart: ${String(error)}`);
2184
+ }
2185
+ }
2186
+ async requestRestart(params) {
2187
+ this.armManagedServiceRelaunch({
2188
+ reason: params.reason,
2189
+ strategy: params.strategy,
2190
+ delayMs: params.delayMs
2694
2191
  });
2695
- const child = spawn2(process.execPath, [...process.execArgv, ...serveArgs], {
2696
- env: process.env,
2697
- stdio: ["ignore", logFd, logFd],
2698
- detached: true
2192
+ const result = await this.restartCoordinator.requestRestart({
2193
+ reason: params.reason,
2194
+ strategy: params.strategy,
2195
+ delayMs: params.delayMs,
2196
+ manualMessage: params.manualMessage
2699
2197
  });
2700
- closeSync(logFd);
2701
- if (!child.pid) {
2702
- console.error("Error: Failed to start background service.");
2198
+ if (result.status === "manual-required" || result.status === "restart-in-progress") {
2199
+ console.log(result.message);
2703
2200
  return;
2704
2201
  }
2705
- const healthUrl = `${apiUrl}/health`;
2706
- const started = await this.waitForBackgroundServiceReady({
2707
- pid: child.pid,
2708
- healthUrl,
2709
- timeoutMs: 8e3
2710
- });
2711
- if (!started) {
2712
- if (isProcessRunning(child.pid)) {
2713
- try {
2714
- process.kill(child.pid, "SIGTERM");
2715
- await waitForExit(child.pid, 2e3);
2716
- } catch {
2717
- }
2202
+ if (result.status === "service-restarted") {
2203
+ if (!params.silentOnServiceRestart) {
2204
+ console.log(result.message);
2718
2205
  }
2719
- clearServiceState();
2720
- console.error(`Error: Failed to start background service. Check logs: ${logPath}`);
2721
2206
  return;
2722
2207
  }
2723
- child.unref();
2724
- const state = {
2725
- pid: child.pid,
2726
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2727
- uiUrl,
2728
- apiUrl,
2729
- uiHost: uiConfig.host,
2730
- uiPort: uiConfig.port,
2731
- logPath
2732
- };
2733
- writeServiceState(state);
2734
- console.log(`\u2713 ${APP_NAME} started in background (PID ${state.pid})`);
2735
- console.log(`UI: ${uiUrl}`);
2736
- console.log(`API: ${apiUrl}`);
2737
- await this.printPublicUiUrls(uiConfig.host, uiConfig.port);
2738
- console.log(`Logs: ${logPath}`);
2739
- console.log(`Stop: ${APP_NAME} stop`);
2740
- if (options.open) {
2741
- openBrowser(uiUrl);
2742
- }
2208
+ console.warn(result.message);
2743
2209
  }
2744
- async waitForBackgroundServiceReady(params) {
2745
- const startedAt = Date.now();
2746
- while (Date.now() - startedAt < params.timeoutMs) {
2747
- if (!isProcessRunning(params.pid)) {
2748
- return false;
2749
- }
2750
- try {
2751
- const response = await fetch(params.healthUrl, { method: "GET" });
2752
- if (!response.ok) {
2753
- await new Promise((resolve5) => setTimeout(resolve5, 200));
2754
- continue;
2755
- }
2756
- const payload = await response.json();
2757
- const healthy = payload?.ok === true && payload?.data?.status === "ok";
2758
- if (!healthy) {
2759
- await new Promise((resolve5) => setTimeout(resolve5, 200));
2760
- continue;
2761
- }
2762
- await new Promise((resolve5) => setTimeout(resolve5, 300));
2763
- if (isProcessRunning(params.pid)) {
2764
- return true;
2765
- }
2766
- } catch {
2767
- }
2768
- await new Promise((resolve5) => setTimeout(resolve5, 200));
2769
- }
2770
- return false;
2210
+ async onboard() {
2211
+ console.warn(`Warning: ${APP_NAME4} onboard is deprecated. Use "${APP_NAME4} init" instead.`);
2212
+ await this.init({ source: "onboard" });
2771
2213
  }
2772
- async stopService() {
2773
- const state = readServiceState();
2774
- if (!state) {
2775
- console.log("No running service found.");
2776
- return;
2214
+ async init(options = {}) {
2215
+ const source = options.source ?? "init";
2216
+ const prefix = options.auto ? "Auto init" : "Init";
2217
+ const force = Boolean(options.force);
2218
+ const configPath = getConfigPath3();
2219
+ let createdConfig = false;
2220
+ if (!existsSync6(configPath)) {
2221
+ const config3 = ConfigSchema2.parse({});
2222
+ saveConfig3(config3);
2223
+ createdConfig = true;
2777
2224
  }
2778
- if (!isProcessRunning(state.pid)) {
2779
- console.log("Service is not running. Cleaning up state.");
2780
- clearServiceState();
2781
- return;
2225
+ const config2 = loadConfig5();
2226
+ const workspaceSetting = config2.agents.defaults.workspace;
2227
+ const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ? join6(getDataDir6(), DEFAULT_WORKSPACE_DIR) : expandHome(workspaceSetting);
2228
+ const workspaceExisted = existsSync6(workspacePath);
2229
+ mkdirSync4(workspacePath, { recursive: true });
2230
+ const templateResult = this.workspaceManager.createWorkspaceTemplates(workspacePath, { force });
2231
+ if (createdConfig) {
2232
+ console.log(`\u2713 ${prefix}: created config at ${configPath}`);
2782
2233
  }
2783
- console.log(`Stopping ${APP_NAME} (PID ${state.pid})...`);
2784
- try {
2785
- process.kill(state.pid, "SIGTERM");
2786
- } catch (error) {
2787
- console.error(`Failed to stop service: ${String(error)}`);
2788
- return;
2234
+ if (!workspaceExisted) {
2235
+ console.log(`\u2713 ${prefix}: created workspace at ${workspacePath}`);
2789
2236
  }
2790
- const stopped = await waitForExit(state.pid, 3e3);
2791
- if (!stopped) {
2792
- try {
2793
- process.kill(state.pid, "SIGKILL");
2794
- } catch (error) {
2795
- console.error(`Failed to force stop service: ${String(error)}`);
2796
- return;
2797
- }
2798
- await waitForExit(state.pid, 2e3);
2237
+ for (const file of templateResult.created) {
2238
+ console.log(`\u2713 ${prefix}: created ${file}`);
2239
+ }
2240
+ if (!createdConfig && workspaceExisted && templateResult.created.length === 0) {
2241
+ console.log(`${prefix}: already initialized.`);
2242
+ }
2243
+ if (!options.auto) {
2244
+ console.log(`
2245
+ ${this.logo} ${APP_NAME4} is ready! (${source})`);
2246
+ console.log("\nNext steps:");
2247
+ console.log(` 1. Add your API key to ${configPath}`);
2248
+ console.log(` 2. Chat: ${APP_NAME4} agent -m "Hello!"`);
2249
+ } else {
2250
+ console.log(`Tip: Run "${APP_NAME4} init${force ? " --force" : ""}" to re-run initialization if needed.`);
2799
2251
  }
2800
- clearServiceState();
2801
- console.log(`\u2713 ${APP_NAME} stopped`);
2802
2252
  }
2803
- async confirmYesNo(question) {
2804
- const rl = createInterface({
2805
- input: process.stdin,
2806
- output: process.stdout
2807
- });
2808
- const answer = await new Promise((resolve5) => {
2809
- rl.question(`${question} [y/N] `, (line) => resolve5(line));
2810
- });
2811
- rl.close();
2812
- const normalized = answer.trim().toLowerCase();
2813
- return normalized === "y" || normalized === "yes";
2253
+ async gateway(opts) {
2254
+ const uiOverrides = {
2255
+ host: FORCED_PUBLIC_UI_HOST
2256
+ };
2257
+ if (opts.ui) {
2258
+ uiOverrides.enabled = true;
2259
+ }
2260
+ if (opts.uiPort) {
2261
+ uiOverrides.port = Number(opts.uiPort);
2262
+ }
2263
+ if (opts.uiOpen) {
2264
+ uiOverrides.open = true;
2265
+ }
2266
+ await this.serviceCommands.startGateway({ uiOverrides });
2814
2267
  }
2815
- makeMissingProvider(config2) {
2816
- return new MissingProvider(config2.agents.defaults.model);
2268
+ async ui(opts) {
2269
+ const uiOverrides = {
2270
+ enabled: true,
2271
+ host: FORCED_PUBLIC_UI_HOST,
2272
+ open: Boolean(opts.open)
2273
+ };
2274
+ if (opts.port) {
2275
+ uiOverrides.port = Number(opts.port);
2276
+ }
2277
+ await this.serviceCommands.startGateway({ uiOverrides, allowMissingProvider: true });
2817
2278
  }
2818
- makeProvider(config2, options) {
2819
- const provider = getProvider(config2);
2820
- const model = config2.agents.defaults.model;
2821
- if (!provider?.apiKey && !model.startsWith("bedrock/")) {
2822
- if (options?.allowMissing) {
2823
- return null;
2824
- }
2825
- console.error("Error: No API key configured.");
2826
- console.error(`Set one in ${getConfigPath()} under providers section`);
2827
- process.exit(1);
2279
+ async start(opts) {
2280
+ await this.init({ source: "start", auto: true });
2281
+ const uiOverrides = {
2282
+ enabled: true,
2283
+ host: FORCED_PUBLIC_UI_HOST,
2284
+ open: false
2285
+ };
2286
+ if (opts.uiPort) {
2287
+ uiOverrides.port = Number(opts.uiPort);
2828
2288
  }
2829
- return new LiteLLMProvider({
2830
- apiKey: provider?.apiKey ?? null,
2831
- apiBase: getApiBase(config2),
2832
- defaultModel: model,
2833
- extraHeaders: provider?.extraHeaders ?? null,
2834
- providerName: getProviderName(config2),
2835
- wireApi: provider?.wireApi ?? null
2289
+ await this.serviceCommands.startService({
2290
+ uiOverrides,
2291
+ open: Boolean(opts.open)
2836
2292
  });
2837
2293
  }
2838
- resolveFileNpmSpecToLocalPath(raw) {
2839
- const trimmed = raw.trim();
2840
- if (!trimmed.toLowerCase().startsWith("file:")) {
2841
- return null;
2842
- }
2843
- const rest = trimmed.slice("file:".length);
2844
- if (!rest) {
2845
- return { ok: false, error: "unsupported file: spec: missing path" };
2846
- }
2847
- if (rest.startsWith("///")) {
2848
- return { ok: true, path: rest.slice(2) };
2849
- }
2850
- if (rest.startsWith("//localhost/")) {
2851
- return { ok: true, path: rest.slice("//localhost".length) };
2852
- }
2853
- if (rest.startsWith("//")) {
2854
- return {
2855
- ok: false,
2856
- error: 'unsupported file: URL host (expected "file:<path>" or "file:///abs/path")'
2857
- };
2294
+ async restart(opts) {
2295
+ const state = readServiceState();
2296
+ if (state && isProcessRunning(state.pid)) {
2297
+ console.log(`Restarting ${APP_NAME4}...`);
2298
+ await this.serviceCommands.stopService();
2299
+ } else if (state) {
2300
+ clearServiceState();
2301
+ console.log("Service state was stale and has been cleaned up.");
2302
+ } else {
2303
+ console.log("No running service found. Starting a new service.");
2858
2304
  }
2859
- return { ok: true, path: rest };
2305
+ await this.start(opts);
2860
2306
  }
2861
- looksLikePath(raw) {
2862
- return raw.startsWith(".") || raw.startsWith("~") || raw.startsWith("/") || raw.endsWith(".ts") || raw.endsWith(".js") || raw.endsWith(".mjs") || raw.endsWith(".cjs") || raw.endsWith(".tgz") || raw.endsWith(".tar.gz") || raw.endsWith(".tar") || raw.endsWith(".zip");
2307
+ async serve(opts) {
2308
+ const uiOverrides = {
2309
+ enabled: true,
2310
+ host: FORCED_PUBLIC_UI_HOST,
2311
+ open: false
2312
+ };
2313
+ if (opts.uiPort) {
2314
+ uiOverrides.port = Number(opts.uiPort);
2315
+ }
2316
+ await this.serviceCommands.runForeground({
2317
+ uiOverrides,
2318
+ open: Boolean(opts.open)
2319
+ });
2863
2320
  }
2864
- isArchivePath(filePath) {
2865
- const lower = filePath.toLowerCase();
2866
- return lower.endsWith(".zip") || lower.endsWith(".tgz") || lower.endsWith(".tar.gz") || lower.endsWith(".tar");
2321
+ async stop() {
2322
+ await this.serviceCommands.stopService();
2867
2323
  }
2868
- createWorkspaceTemplates(workspace, options = {}) {
2869
- const created = [];
2870
- const force = Boolean(options.force);
2871
- const templateDir = this.resolveTemplateDir();
2872
- if (!templateDir) {
2873
- console.warn("Warning: Template directory not found. Skipping workspace templates.");
2874
- return { created };
2324
+ async agent(opts) {
2325
+ const config2 = loadConfig5();
2326
+ const workspace = getWorkspacePath3(config2.agents.defaults.workspace);
2327
+ const bus = new MessageBus2();
2328
+ const provider = this.serviceCommands.createProvider(config2) ?? this.serviceCommands.createMissingProvider(config2);
2329
+ const providerManager = new ProviderManager2(provider);
2330
+ const agentLoop = new AgentLoop2({
2331
+ bus,
2332
+ providerManager,
2333
+ workspace,
2334
+ model: config2.agents.defaults.model,
2335
+ maxIterations: config2.agents.defaults.maxToolIterations,
2336
+ maxTokens: config2.agents.defaults.maxTokens,
2337
+ temperature: config2.agents.defaults.temperature,
2338
+ braveApiKey: config2.tools.web.search.apiKey || void 0,
2339
+ execConfig: config2.tools.exec,
2340
+ restrictToWorkspace: config2.tools.restrictToWorkspace,
2341
+ contextConfig: config2.agents.context,
2342
+ config: config2
2343
+ });
2344
+ if (opts.message) {
2345
+ const response = await agentLoop.processDirect({
2346
+ content: opts.message,
2347
+ sessionKey: opts.session ?? "cli:default",
2348
+ channel: "cli",
2349
+ chatId: "direct"
2350
+ });
2351
+ printAgentResponse(response);
2352
+ return;
2875
2353
  }
2876
- const templateFiles = [
2877
- { source: "AGENTS.md", target: "AGENTS.md" },
2878
- { source: "SOUL.md", target: "SOUL.md" },
2879
- { source: "USER.md", target: "USER.md" },
2880
- { source: "IDENTITY.md", target: "IDENTITY.md" },
2881
- { source: "TOOLS.md", target: "TOOLS.md" },
2882
- { source: "USAGE.md", target: "USAGE.md" },
2883
- { source: "BOOT.md", target: "BOOT.md" },
2884
- { source: "BOOTSTRAP.md", target: "BOOTSTRAP.md" },
2885
- { source: "HEARTBEAT.md", target: "HEARTBEAT.md" },
2886
- { source: "MEMORY.md", target: "MEMORY.md" },
2887
- { source: "memory/MEMORY.md", target: "memory/MEMORY.md" }
2888
- ];
2889
- for (const entry of templateFiles) {
2890
- const filePath = join3(workspace, entry.target);
2891
- if (!force && existsSync4(filePath)) {
2354
+ console.log(`${this.logo} Interactive mode (type exit or Ctrl+C to quit)
2355
+ `);
2356
+ const historyFile = join6(getDataDir6(), "history", "cli_history");
2357
+ const historyDir = resolve7(historyFile, "..");
2358
+ mkdirSync4(historyDir, { recursive: true });
2359
+ const history = existsSync6(historyFile) ? readFileSync5(historyFile, "utf-8").split("\n").filter(Boolean) : [];
2360
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2361
+ rl.on("close", () => {
2362
+ const merged = history.concat(rl.history ?? []);
2363
+ writeFileSync3(historyFile, merged.join("\n"));
2364
+ process.exit(0);
2365
+ });
2366
+ let running = true;
2367
+ while (running) {
2368
+ const line = await prompt(rl, "You: ");
2369
+ const trimmed = line.trim();
2370
+ if (!trimmed) {
2892
2371
  continue;
2893
2372
  }
2894
- const templatePath = join3(templateDir, entry.source);
2895
- if (!existsSync4(templatePath)) {
2896
- console.warn(`Warning: Template file missing: ${templatePath}`);
2897
- continue;
2373
+ if (EXIT_COMMANDS.has(trimmed.toLowerCase())) {
2374
+ rl.close();
2375
+ running = false;
2376
+ break;
2898
2377
  }
2899
- const raw = readFileSync3(templatePath, "utf-8");
2900
- const content = raw.replace(/\$\{APP_NAME\}/g, APP_NAME);
2901
- mkdirSync2(dirname(filePath), { recursive: true });
2902
- writeFileSync2(filePath, content);
2903
- created.push(entry.target);
2904
- }
2905
- const memoryDir = join3(workspace, "memory");
2906
- if (!existsSync4(memoryDir)) {
2907
- mkdirSync2(memoryDir, { recursive: true });
2908
- created.push(join3("memory", ""));
2909
- }
2910
- const skillsDir = join3(workspace, "skills");
2911
- if (!existsSync4(skillsDir)) {
2912
- mkdirSync2(skillsDir, { recursive: true });
2913
- created.push(join3("skills", ""));
2914
- }
2915
- const seeded = this.seedBuiltinSkills(skillsDir, { force });
2916
- if (seeded > 0) {
2917
- created.push(`skills (seeded ${seeded} built-ins)`);
2378
+ const response = await agentLoop.processDirect({
2379
+ content: trimmed,
2380
+ sessionKey: opts.session ?? "cli:default"
2381
+ });
2382
+ printAgentResponse(response);
2918
2383
  }
2919
- return { created };
2920
2384
  }
2921
- seedBuiltinSkills(targetDir, options = {}) {
2922
- const sourceDir = this.resolveBuiltinSkillsDir();
2923
- if (!sourceDir) {
2924
- return 0;
2925
- }
2926
- const force = Boolean(options.force);
2927
- let seeded = 0;
2928
- for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
2929
- if (!entry.isDirectory()) {
2930
- continue;
2931
- }
2932
- const src = join3(sourceDir, entry.name);
2933
- if (!existsSync4(join3(src, "SKILL.md"))) {
2934
- continue;
2935
- }
2936
- const dest = join3(targetDir, entry.name);
2937
- if (!force && existsSync4(dest)) {
2938
- continue;
2385
+ async update(opts) {
2386
+ let timeoutMs;
2387
+ if (opts.timeout !== void 0) {
2388
+ const parsed = Number(opts.timeout);
2389
+ if (!Number.isFinite(parsed) || parsed <= 0) {
2390
+ console.error("Invalid --timeout value. Provide milliseconds (e.g. 1200000).");
2391
+ process.exit(1);
2939
2392
  }
2940
- cpSync(src, dest, { recursive: true, force: true });
2941
- seeded += 1;
2393
+ timeoutMs = parsed;
2942
2394
  }
2943
- return seeded;
2944
- }
2945
- resolveBuiltinSkillsDir() {
2946
- try {
2947
- const require2 = createRequire(import.meta.url);
2948
- const entry = require2.resolve("@nextclaw/core");
2949
- const pkgRoot = resolve4(dirname(entry), "..");
2950
- const distSkills = join3(pkgRoot, "dist", "skills");
2951
- if (existsSync4(distSkills)) {
2952
- return distSkills;
2953
- }
2954
- const srcSkills = join3(pkgRoot, "src", "agent", "skills");
2955
- if (existsSync4(srcSkills)) {
2956
- return srcSkills;
2395
+ const result = runSelfUpdate({ timeoutMs, cwd: process.cwd() });
2396
+ const printSteps = () => {
2397
+ for (const step of result.steps) {
2398
+ console.log(`- ${step.cmd} ${step.args.join(" ")} (code ${step.code ?? "?"})`);
2399
+ if (step.stderr) {
2400
+ console.log(` stderr: ${step.stderr}`);
2401
+ }
2402
+ if (step.stdout) {
2403
+ console.log(` stdout: ${step.stdout}`);
2404
+ }
2957
2405
  }
2958
- return null;
2959
- } catch {
2960
- return null;
2961
- }
2962
- }
2963
- resolveTemplateDir() {
2964
- const override = process.env.NEXTCLAW_TEMPLATE_DIR?.trim();
2965
- if (override) {
2966
- return override;
2967
- }
2968
- const cliDir = resolve4(fileURLToPath2(new URL(".", import.meta.url)));
2969
- const pkgRoot = resolve4(cliDir, "..", "..");
2970
- const candidates = [join3(pkgRoot, "templates")];
2971
- for (const candidate of candidates) {
2972
- if (existsSync4(candidate)) {
2973
- return candidate;
2406
+ };
2407
+ if (!result.ok) {
2408
+ console.error(`Update failed: ${result.error ?? "unknown error"}`);
2409
+ if (result.steps.length > 0) {
2410
+ printSteps();
2974
2411
  }
2975
- }
2976
- return null;
2977
- }
2978
- getBridgeDir() {
2979
- const userBridge = join3(getDataDir2(), "bridge");
2980
- if (existsSync4(join3(userBridge, "dist", "index.js"))) {
2981
- return userBridge;
2982
- }
2983
- if (!which("npm")) {
2984
- console.error("npm not found. Please install Node.js >= 18.");
2985
- process.exit(1);
2986
- }
2987
- const cliDir = resolve4(fileURLToPath2(new URL(".", import.meta.url)));
2988
- const pkgRoot = resolve4(cliDir, "..", "..");
2989
- const pkgBridge = join3(pkgRoot, "bridge");
2990
- const srcBridge = join3(pkgRoot, "..", "..", "bridge");
2991
- let source = null;
2992
- if (existsSync4(join3(pkgBridge, "package.json"))) {
2993
- source = pkgBridge;
2994
- } else if (existsSync4(join3(srcBridge, "package.json"))) {
2995
- source = srcBridge;
2996
- }
2997
- if (!source) {
2998
- console.error(`Bridge source not found. Try reinstalling ${APP_NAME}.`);
2999
2412
  process.exit(1);
3000
2413
  }
3001
- console.log(`${this.logo} Setting up bridge...`);
3002
- mkdirSync2(resolve4(userBridge, ".."), { recursive: true });
3003
- if (existsSync4(userBridge)) {
3004
- rmSync2(userBridge, { recursive: true, force: true });
2414
+ console.log(`\u2713 Update complete (${result.strategy})`);
2415
+ const state = readServiceState();
2416
+ if (state && isProcessRunning(state.pid)) {
2417
+ console.log(`Tip: restart ${APP_NAME4} to apply the update.`);
3005
2418
  }
3006
- cpSync(source, userBridge, {
3007
- recursive: true,
3008
- filter: (src) => !src.includes("node_modules") && !src.includes("dist")
2419
+ }
2420
+ configGet(pathExpr, opts = {}) {
2421
+ this.configCommands.configGet(pathExpr, opts);
2422
+ }
2423
+ async configSet(pathExpr, value, opts = {}) {
2424
+ await this.configCommands.configSet(pathExpr, value, opts);
2425
+ }
2426
+ async configUnset(pathExpr) {
2427
+ await this.configCommands.configUnset(pathExpr);
2428
+ }
2429
+ channelsStatus() {
2430
+ this.channelCommands.channelsStatus();
2431
+ }
2432
+ channelsLogin() {
2433
+ this.channelCommands.channelsLogin();
2434
+ }
2435
+ cronList(opts) {
2436
+ this.cronCommands.cronList(opts);
2437
+ }
2438
+ cronAdd(opts) {
2439
+ this.cronCommands.cronAdd(opts);
2440
+ }
2441
+ cronRemove(jobId) {
2442
+ this.cronCommands.cronRemove(jobId);
2443
+ }
2444
+ cronEnable(jobId, opts) {
2445
+ this.cronCommands.cronEnable(jobId, opts);
2446
+ }
2447
+ async cronRun(jobId, opts) {
2448
+ await this.cronCommands.cronRun(jobId, opts);
2449
+ }
2450
+ async status(opts = {}) {
2451
+ await this.diagnosticsCommands.status(opts);
2452
+ }
2453
+ async doctor(opts = {}) {
2454
+ await this.diagnosticsCommands.doctor(opts);
2455
+ }
2456
+ async skillsInstall(options) {
2457
+ const workdir = options.workdir ? expandHome(options.workdir) : getWorkspacePath3();
2458
+ const result = await installClawHubSkill({
2459
+ slug: options.slug,
2460
+ version: options.version,
2461
+ registry: options.registry,
2462
+ workdir,
2463
+ dir: options.dir,
2464
+ force: options.force
3009
2465
  });
3010
- const install = spawnSync3("npm", ["install"], { cwd: userBridge, stdio: "pipe" });
3011
- if (install.status !== 0) {
3012
- console.error(`Bridge install failed: ${install.status ?? 1}`);
3013
- if (install.stderr) {
3014
- console.error(String(install.stderr).slice(0, 500));
3015
- }
3016
- process.exit(1);
2466
+ const versionLabel = result.version ?? "latest";
2467
+ if (result.alreadyInstalled) {
2468
+ console.log(`\u2713 ${result.slug} is already installed`);
2469
+ } else {
2470
+ console.log(`\u2713 Installed ${result.slug}@${versionLabel}`);
3017
2471
  }
3018
- const build = spawnSync3("npm", ["run", "build"], { cwd: userBridge, stdio: "pipe" });
3019
- if (build.status !== 0) {
3020
- console.error(`Bridge build failed: ${build.status ?? 1}`);
3021
- if (build.stderr) {
3022
- console.error(String(build.stderr).slice(0, 500));
3023
- }
3024
- process.exit(1);
2472
+ if (result.registry) {
2473
+ console.log(` Registry: ${result.registry}`);
3025
2474
  }
3026
- console.log("\u2713 Bridge ready\n");
3027
- return userBridge;
2475
+ console.log(` Path: ${result.destinationDir}`);
3028
2476
  }
3029
2477
  };
3030
2478
 
3031
2479
  // src/cli/index.ts
3032
2480
  var program = new Command();
3033
2481
  var runtime = new CliRuntime({ logo: LOGO });
3034
- program.name(APP_NAME2).description(`${LOGO} ${APP_NAME2} - ${APP_TAGLINE}`).version(getPackageVersion(), "-v, --version", "show version");
3035
- program.command("onboard").description(`Initialize ${APP_NAME2} configuration and workspace`).action(async () => runtime.onboard());
3036
- program.command("init").description(`Initialize ${APP_NAME2} configuration and workspace`).option("-f, --force", "Overwrite existing template files").action(async (opts) => runtime.init({ force: Boolean(opts.force) }));
3037
- program.command("gateway").description(`Start the ${APP_NAME2} gateway`).option("-p, --port <port>", "Gateway port", "18790").option("-v, --verbose", "Verbose output", false).option("--ui", "Enable UI server", false).option("--ui-port <port>", "UI port").option("--ui-open", "Open browser when UI starts", false).action(async (opts) => runtime.gateway(opts));
3038
- program.command("ui").description(`Start the ${APP_NAME2} UI with gateway`).option("--port <port>", "UI port").option("--no-open", "Disable opening browser").action(async (opts) => runtime.ui(opts));
3039
- program.command("start").description(`Start the ${APP_NAME2} gateway + UI in the background`).option("--ui-port <port>", "UI port").option("--open", "Open browser after start", false).action(async (opts) => runtime.start(opts));
3040
- program.command("restart").description(`Restart the ${APP_NAME2} background service`).option("--ui-port <port>", "UI port").option("--open", "Open browser after restart", false).action(async (opts) => runtime.restart(opts));
3041
- program.command("serve").description(`Run the ${APP_NAME2} gateway + UI in the foreground`).option("--ui-port <port>", "UI port").option("--open", "Open browser after start", false).action(async (opts) => runtime.serve(opts));
3042
- program.command("stop").description(`Stop the ${APP_NAME2} background service`).action(async () => runtime.stop());
2482
+ program.name(APP_NAME5).description(`${LOGO} ${APP_NAME5} - ${APP_TAGLINE}`).version(getPackageVersion(), "-v, --version", "show version");
2483
+ program.command("onboard").description(`Initialize ${APP_NAME5} configuration and workspace`).action(async () => runtime.onboard());
2484
+ program.command("init").description(`Initialize ${APP_NAME5} configuration and workspace`).option("-f, --force", "Overwrite existing template files").action(async (opts) => runtime.init({ force: Boolean(opts.force) }));
2485
+ program.command("gateway").description(`Start the ${APP_NAME5} gateway`).option("-p, --port <port>", "Gateway port", "18790").option("-v, --verbose", "Verbose output", false).option("--ui", "Enable UI server", false).option("--ui-port <port>", "UI port").option("--ui-open", "Open browser when UI starts", false).action(async (opts) => runtime.gateway(opts));
2486
+ program.command("ui").description(`Start the ${APP_NAME5} UI with gateway`).option("--port <port>", "UI port").option("--no-open", "Disable opening browser").action(async (opts) => runtime.ui(opts));
2487
+ program.command("start").description(`Start the ${APP_NAME5} gateway + UI in the background`).option("--ui-port <port>", "UI port").option("--open", "Open browser after start", false).action(async (opts) => runtime.start(opts));
2488
+ program.command("restart").description(`Restart the ${APP_NAME5} background service`).option("--ui-port <port>", "UI port").option("--open", "Open browser after restart", false).action(async (opts) => runtime.restart(opts));
2489
+ program.command("serve").description(`Run the ${APP_NAME5} gateway + UI in the foreground`).option("--ui-port <port>", "UI port").option("--open", "Open browser after start", false).action(async (opts) => runtime.serve(opts));
2490
+ program.command("stop").description(`Stop the ${APP_NAME5} background service`).action(async () => runtime.stop());
3043
2491
  program.command("agent").description("Interact with the agent directly").option("-m, --message <message>", "Message to send to the agent").option("-s, --session <session>", "Session ID", "cli:default").option("--no-markdown", "Disable Markdown rendering").action(async (opts) => runtime.agent(opts));
3044
- program.command("update").description(`Update ${APP_NAME2}`).option("--timeout <ms>", "Update command timeout in milliseconds").action(async (opts) => runtime.update(opts));
2492
+ program.command("update").description(`Update ${APP_NAME5}`).option("--timeout <ms>", "Update command timeout in milliseconds").action(async (opts) => runtime.update(opts));
3045
2493
  var registerClawHubInstall = (cmd) => {
3046
2494
  cmd.command("install <slug>").description("Install a skill from ClawHub").option("--version <version>", "Skill version (default: latest)").option("--registry <url>", "ClawHub registry base URL").option("--workdir <dir>", "Workspace directory to install into").option("--dir <dir>", "Skills directory name (default: skills)").option("-f, --force", "Overwrite existing skill files", false).action(async (slug, opts) => runtime.skillsInstall({ slug, ...opts }));
3047
2495
  };
@@ -3049,20 +2497,11 @@ var skills = program.command("skills").description("Manage skills");
3049
2497
  registerClawHubInstall(skills);
3050
2498
  var clawhub = program.command("clawhub").description("Install skills from ClawHub");
3051
2499
  registerClawHubInstall(clawhub);
3052
- var plugins = program.command("plugins").description("Manage OpenClaw-compatible plugins");
3053
- plugins.command("list").description("List discovered plugins").option("--json", "Print JSON").option("--enabled", "Only show enabled plugins", false).option("--verbose", "Show detailed entries", false).action((opts) => runtime.pluginsList(opts));
3054
- plugins.command("info <id>").description("Show plugin details").option("--json", "Print JSON").action((id, opts) => runtime.pluginsInfo(id, opts));
3055
- plugins.command("enable <id>").description("Enable a plugin in config").action((id) => runtime.pluginsEnable(id));
3056
- plugins.command("disable <id>").description("Disable a plugin in config").action((id) => runtime.pluginsDisable(id));
3057
- plugins.command("uninstall <id>").description("Uninstall a plugin").option("--keep-files", "Keep installed files on disk", false).option("--keep-config", "Deprecated alias for --keep-files", false).option("--force", "Skip confirmation prompt", false).option("--dry-run", "Show what would be removed without making changes", false).action(async (id, opts) => runtime.pluginsUninstall(id, opts));
3058
- plugins.command("install <path-or-spec>").description("Install a plugin (path, archive, or npm spec)").option("-l, --link", "Link a local path instead of copying", false).action(async (pathOrSpec, opts) => runtime.pluginsInstall(pathOrSpec, opts));
3059
- plugins.command("doctor").description("Report plugin load issues").action(() => runtime.pluginsDoctor());
3060
2500
  var config = program.command("config").description("Manage config values");
3061
2501
  config.command("get <path>").description("Get a config value by dot path").option("--json", "Output JSON", false).action((path, opts) => runtime.configGet(path, opts));
3062
2502
  config.command("set <path> <value>").description("Set a config value by dot path").option("--json", "Parse value as JSON", false).action((path, value, opts) => runtime.configSet(path, value, opts));
3063
2503
  config.command("unset <path>").description("Remove a config value by dot path").action((path) => runtime.configUnset(path));
3064
2504
  var channels = program.command("channels").description("Manage channels");
3065
- channels.command("add").description("Configure a plugin channel (OpenClaw-compatible setup)").requiredOption("--channel <id>", "Plugin channel id").option("--code <code>", "Pairing code").option("--token <token>", "Connector token").option("--name <name>", "Display name").option("--url <url>", "API base URL").option("--http-url <url>", "Alias for --url").action((opts) => runtime.channelsAdd(opts));
3066
2505
  channels.command("status").description("Show channel status").action(() => runtime.channelsStatus());
3067
2506
  channels.command("login").description("Link device via QR code").action(() => runtime.channelsLogin());
3068
2507
  var cron = program.command("cron").description("Manage scheduled tasks");
@@ -3071,6 +2510,6 @@ cron.command("add").requiredOption("-n, --name <name>", "Job name").requiredOpti
3071
2510
  cron.command("remove <jobId>").action((jobId) => runtime.cronRemove(jobId));
3072
2511
  cron.command("enable <jobId>").option("--disable", "Disable instead of enable").action((jobId, opts) => runtime.cronEnable(jobId, opts));
3073
2512
  cron.command("run <jobId>").option("-f, --force", "Run even if disabled").action(async (jobId, opts) => runtime.cronRun(jobId, opts));
3074
- program.command("status").description(`Show ${APP_NAME2} status`).option("--json", "Output JSON", false).option("--verbose", "Show extra diagnostics", false).option("--fix", "Fix stale service state when safe", false).action(async (opts) => runtime.status(opts));
3075
- program.command("doctor").description(`Run ${APP_NAME2} diagnostics`).option("--json", "Output JSON", false).option("--verbose", "Show extra diagnostics", false).option("--fix", "Fix stale service state when safe", false).action(async (opts) => runtime.doctor(opts));
2513
+ program.command("status").description(`Show ${APP_NAME5} status`).option("--json", "Output JSON", false).option("--verbose", "Show extra diagnostics", false).option("--fix", "Fix stale service state when safe", false).action(async (opts) => runtime.status(opts));
2514
+ program.command("doctor").description(`Run ${APP_NAME5} diagnostics`).option("--json", "Output JSON", false).option("--verbose", "Show extra diagnostics", false).option("--fix", "Fix stale service state when safe", false).action(async (opts) => runtime.doctor(opts));
3076
2515
  program.parseAsync(process.argv);