nextclaw 0.5.4 → 0.5.6

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