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