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 +2198 -1916
- package/package.json +3 -3
- package/templates/USAGE.md +2 -1
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
|
|
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
|
|
9
|
+
loadConfig as loadConfig6,
|
|
10
|
+
saveConfig as saveConfig5,
|
|
11
|
+
getConfigPath as getConfigPath3,
|
|
12
|
+
getDataDir as getDataDir6,
|
|
13
13
|
ConfigSchema as ConfigSchema2,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 (!
|
|
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(
|
|
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 (
|
|
234
|
+
if (existsSync2(path)) {
|
|
156
235
|
rmSync(path, { force: true });
|
|
157
236
|
}
|
|
158
237
|
}
|
|
159
238
|
function resolveServiceStatePath() {
|
|
160
|
-
return
|
|
239
|
+
return resolve2(getDataDir(), "run", "service.json");
|
|
161
240
|
}
|
|
162
241
|
function resolveServiceLogPath() {
|
|
163
|
-
return
|
|
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((
|
|
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 =
|
|
190
|
-
const pkgRoot =
|
|
191
|
-
candidates.push(
|
|
192
|
-
candidates.push(
|
|
193
|
-
candidates.push(
|
|
194
|
-
candidates.push(
|
|
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(
|
|
197
|
-
candidates.push(
|
|
198
|
-
candidates.push(
|
|
199
|
-
candidates.push(
|
|
200
|
-
candidates.push(
|
|
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 (
|
|
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 =
|
|
229
|
-
if (
|
|
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 =
|
|
315
|
+
let current = resolve2(startDir);
|
|
237
316
|
while (current.length > 0) {
|
|
238
|
-
const pkgPath =
|
|
239
|
-
if (
|
|
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 =
|
|
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 =
|
|
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((
|
|
270
|
-
rl.once("line", (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 =
|
|
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 ?
|
|
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/
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (
|
|
356
|
-
|
|
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
|
|
519
|
+
}
|
|
520
|
+
var PluginCommands = class {
|
|
369
521
|
constructor(deps) {
|
|
370
522
|
this.deps = deps;
|
|
371
523
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
414
|
-
const
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
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 (
|
|
420
|
-
|
|
595
|
+
if (opts.json) {
|
|
596
|
+
console.log(JSON.stringify(plugin, null, 2));
|
|
597
|
+
return;
|
|
421
598
|
}
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
438
|
-
|
|
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 (
|
|
455
|
-
|
|
618
|
+
if (plugin.channelIds.length > 0) {
|
|
619
|
+
lines.push(`Channels: ${plugin.channelIds.join(", ")}`);
|
|
456
620
|
}
|
|
457
|
-
if (
|
|
458
|
-
|
|
621
|
+
if (plugin.providerIds.length > 0) {
|
|
622
|
+
lines.push(`Providers: ${plugin.providerIds.join(", ")}`);
|
|
459
623
|
}
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
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
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
|
1040
|
-
const
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
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 (
|
|
1049
|
-
|
|
699
|
+
if (hasInstall) {
|
|
700
|
+
preview.push("install record");
|
|
1050
701
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
702
|
+
if (config2.plugins.allow?.includes(pluginId)) {
|
|
703
|
+
preview.push("allowlist entry");
|
|
1053
704
|
}
|
|
1054
|
-
if (
|
|
1055
|
-
|
|
705
|
+
if (isLinked && install?.sourcePath && config2.plugins.load?.paths?.includes(install.sourcePath)) {
|
|
706
|
+
preview.push("load path");
|
|
1056
707
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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.
|
|
1075
|
-
|
|
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
|
-
|
|
1078
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
-
|
|
1104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
|
|
1179
|
-
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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(
|
|
1233
|
-
if (result.steps.length > 0) {
|
|
1234
|
-
printSteps();
|
|
1235
|
-
}
|
|
842
|
+
console.error(result.error);
|
|
1236
843
|
process.exit(1);
|
|
1237
844
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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
|
-
|
|
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
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
-
|
|
1273
|
-
|
|
1274
|
-
const
|
|
1275
|
-
|
|
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
|
-
|
|
1293
|
-
|
|
880
|
+
}
|
|
881
|
+
if (diagnostics.length > 0) {
|
|
882
|
+
if (pluginErrors.length > 0) {
|
|
883
|
+
console.log("");
|
|
1294
884
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
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
|
-
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
1326
|
-
|
|
909
|
+
const rest = trimmed.slice("file:".length);
|
|
910
|
+
if (!rest) {
|
|
911
|
+
return { ok: false, error: "unsupported file: spec: missing path" };
|
|
1327
912
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
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 (
|
|
1336
|
-
|
|
916
|
+
if (rest.startsWith("//localhost/")) {
|
|
917
|
+
return { ok: true, path: rest.slice("//localhost".length) };
|
|
1337
918
|
}
|
|
1338
|
-
if (
|
|
1339
|
-
|
|
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
|
-
|
|
1342
|
-
|
|
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 (
|
|
1345
|
-
|
|
961
|
+
if (ch === ".") {
|
|
962
|
+
if (current) {
|
|
963
|
+
parts.push(current);
|
|
964
|
+
}
|
|
965
|
+
current = "";
|
|
966
|
+
i += 1;
|
|
967
|
+
continue;
|
|
1346
968
|
}
|
|
1347
|
-
if (
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
if (install.spec) {
|
|
1351
|
-
lines.push(`Spec: ${install.spec}`);
|
|
969
|
+
if (ch === "[") {
|
|
970
|
+
if (current) {
|
|
971
|
+
parts.push(current);
|
|
1352
972
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
973
|
+
current = "";
|
|
974
|
+
const close = trimmed.indexOf("]", i);
|
|
975
|
+
if (close === -1) {
|
|
976
|
+
throw new Error(`Invalid path (missing "]"): ${raw}`);
|
|
1355
977
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
978
|
+
const inside = trimmed.slice(i + 1, close).trim();
|
|
979
|
+
if (!inside) {
|
|
980
|
+
throw new Error(`Invalid path (empty "[]"): ${raw}`);
|
|
1358
981
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
-
|
|
1363
|
-
|
|
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
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
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
|
-
|
|
1033
|
+
current = record[segment];
|
|
1393
1034
|
}
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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
|
-
|
|
1421
|
-
|
|
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
|
|
1438
|
-
const
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1454
|
-
|
|
1455
|
-
if (!
|
|
1456
|
-
|
|
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
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
-
|
|
1477
|
-
|
|
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
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
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
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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
|
|
1528
|
-
|
|
1529
|
-
|
|
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
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
if (
|
|
1540
|
-
|
|
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
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
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
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
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
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
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 (
|
|
1574
|
-
|
|
1147
|
+
if (opts.json) {
|
|
1148
|
+
console.log(JSON.stringify(result.value ?? null, null, 2));
|
|
1149
|
+
return;
|
|
1575
1150
|
}
|
|
1576
|
-
if (result.
|
|
1577
|
-
|
|
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(
|
|
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
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
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
|
-
|
|
1646
|
-
|
|
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
|
-
|
|
1650
|
-
|
|
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
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
-
|
|
1661
|
-
|
|
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
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
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
|
-
|
|
1680
|
-
const
|
|
1681
|
-
|
|
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
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
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
|
-
|
|
1712
|
-
|
|
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
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
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 =
|
|
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 =
|
|
1743
|
-
const report =
|
|
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:
|
|
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 =
|
|
1774
|
-
const workspaceDir =
|
|
1775
|
-
const pluginRegistry =
|
|
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 =
|
|
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 =
|
|
1815
|
-
next =
|
|
1816
|
-
|
|
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
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
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 (
|
|
2074
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
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
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
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
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
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 (
|
|
2138
|
-
|
|
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 (
|
|
2142
|
-
|
|
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
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
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
|
-
|
|
2149
|
-
|
|
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
|
-
|
|
2152
|
-
const
|
|
2153
|
-
|
|
1939
|
+
this.deps.saveConfig(validated);
|
|
1940
|
+
const delayMs = params.restartDelayMs ?? 0;
|
|
1941
|
+
await this.requestRestart({ delayMs, reason: "config.patch" });
|
|
2154
1942
|
return {
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
},
|
|
2173
|
-
|
|
2174
|
-
|
|
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
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
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
|
|
2195
|
-
|
|
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
|
-
|
|
2037
|
+
console.error(`Config reload failed (${reason}): ${String(error)}`);
|
|
2208
2038
|
} finally {
|
|
2209
|
-
|
|
2039
|
+
this.reloadRunning = false;
|
|
2040
|
+
if (this.reloadPending) {
|
|
2041
|
+
this.reloadPending = false;
|
|
2042
|
+
this.scheduleReload("pending");
|
|
2043
|
+
}
|
|
2210
2044
|
}
|
|
2211
2045
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
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
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
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
|
-
|
|
2222
|
-
}
|
|
2223
|
-
|
|
2085
|
+
this.options.providerManager?.set(nextProvider);
|
|
2086
|
+
})();
|
|
2087
|
+
try {
|
|
2088
|
+
await this.providerReloadTask;
|
|
2089
|
+
} finally {
|
|
2090
|
+
this.providerReloadTask = null;
|
|
2224
2091
|
}
|
|
2225
2092
|
}
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
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
|
-
|
|
2246
|
-
|
|
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
|
-
|
|
2279
|
-
|
|
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
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
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 =
|
|
2313
|
-
const workspace =
|
|
2314
|
-
const pluginRegistry =
|
|
2315
|
-
const extensionRegistry =
|
|
2316
|
-
|
|
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 =
|
|
2322
|
-
const cron2 = new
|
|
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
|
|
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:
|
|
2190
|
+
cfg: loadConfig5(),
|
|
2384
2191
|
accountId
|
|
2385
2192
|
})
|
|
2386
2193
|
});
|
|
2387
2194
|
reloader.setApplyAgentRuntimeConfig((nextConfig) => agent.applyRuntimeConfig(nextConfig));
|
|
2388
|
-
const pluginChannelBindings =
|
|
2195
|
+
const pluginChannelBindings = getPluginChannelBindings2(pluginRegistry);
|
|
2389
2196
|
setPluginRuntimeBridge({
|
|
2390
|
-
loadConfig: () =>
|
|
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 =
|
|
2396
|
-
const next =
|
|
2397
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
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: ${
|
|
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 =
|
|
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 ${
|
|
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: ${
|
|
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 ${
|
|
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 ${
|
|
2453
|
+
console.log(`\u2713 ${APP_NAME2} stopped`);
|
|
2709
2454
|
}
|
|
2710
|
-
async
|
|
2711
|
-
const
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
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 ${
|
|
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
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
return
|
|
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
|
-
|
|
2755
|
-
|
|
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
|
-
|
|
2758
|
-
|
|
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
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
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
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
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 =
|
|
2798
|
-
if (!force &&
|
|
2584
|
+
const filePath = join5(workspace, entry.target);
|
|
2585
|
+
if (!force && existsSync6(filePath)) {
|
|
2799
2586
|
continue;
|
|
2800
2587
|
}
|
|
2801
|
-
const templatePath =
|
|
2802
|
-
if (!
|
|
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 =
|
|
2807
|
-
const content = raw.replace(/\$\{APP_NAME\}/g,
|
|
2808
|
-
|
|
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 =
|
|
2813
|
-
if (!
|
|
2814
|
-
|
|
2815
|
-
created.push(
|
|
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
|
-
|
|
2818
|
-
|
|
2819
|
-
mkdirSync2(skillsDir, { recursive: true });
|
|
2820
|
-
created.push(join3("skills", ""));
|
|
2970
|
+
if (opts.uiOpen) {
|
|
2971
|
+
uiOverrides.open = true;
|
|
2821
2972
|
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
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
|
-
|
|
2984
|
+
await this.serviceCommands.startGateway({ uiOverrides, allowMissingProvider: true });
|
|
2827
2985
|
}
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
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
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
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
|
-
|
|
3012
|
+
await this.start(opts);
|
|
2851
3013
|
}
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
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
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
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
|
-
|
|
2876
|
-
|
|
2877
|
-
const
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
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
|
-
|
|
2886
|
-
|
|
2887
|
-
if (
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
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
|
-
|
|
2905
|
-
|
|
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(
|
|
2909
|
-
|
|
2910
|
-
if (
|
|
2911
|
-
|
|
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
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
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
|
|
2918
|
-
if (
|
|
2919
|
-
console.
|
|
2920
|
-
|
|
2921
|
-
|
|
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
|
-
|
|
2926
|
-
|
|
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(
|
|
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(
|
|
2942
|
-
program.command("onboard").description(`Initialize ${
|
|
2943
|
-
program.command("init").description(`Initialize ${
|
|
2944
|
-
program.command("gateway").description(`Start the ${
|
|
2945
|
-
program.command("ui").description(`Start the ${
|
|
2946
|
-
program.command("start").description(`Start the ${
|
|
2947
|
-
program.command("restart").description(`Restart the ${
|
|
2948
|
-
program.command("serve").description(`Run the ${
|
|
2949
|
-
program.command("stop").description(`Stop the ${
|
|
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 ${
|
|
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 ${
|
|
2982
|
-
program.command("doctor").description(`Run ${
|
|
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);
|