nextclaw 0.9.10 → 0.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +225 -255
- package/package.json +1 -1
- package/templates/USAGE.md +0 -9
package/dist/cli/index.js
CHANGED
|
@@ -1831,8 +1831,8 @@ import { join as join3 } from "path";
|
|
|
1831
1831
|
var CronCommands = class {
|
|
1832
1832
|
cronList(opts) {
|
|
1833
1833
|
const storePath = join3(getDataDir3(), "cron", "jobs.json");
|
|
1834
|
-
const
|
|
1835
|
-
const jobs =
|
|
1834
|
+
const service = new CronService(storePath);
|
|
1835
|
+
const jobs = service.listJobs(Boolean(opts.all));
|
|
1836
1836
|
if (!jobs.length) {
|
|
1837
1837
|
console.log("No scheduled jobs.");
|
|
1838
1838
|
return;
|
|
@@ -1851,7 +1851,7 @@ var CronCommands = class {
|
|
|
1851
1851
|
}
|
|
1852
1852
|
cronAdd(opts) {
|
|
1853
1853
|
const storePath = join3(getDataDir3(), "cron", "jobs.json");
|
|
1854
|
-
const
|
|
1854
|
+
const service = new CronService(storePath);
|
|
1855
1855
|
let schedule = null;
|
|
1856
1856
|
if (opts.every) {
|
|
1857
1857
|
schedule = { kind: "every", everyMs: Number(opts.every) * 1e3 };
|
|
@@ -1864,7 +1864,7 @@ var CronCommands = class {
|
|
|
1864
1864
|
console.error("Error: Must specify --every, --cron, or --at");
|
|
1865
1865
|
return;
|
|
1866
1866
|
}
|
|
1867
|
-
const job =
|
|
1867
|
+
const job = service.addJob({
|
|
1868
1868
|
name: opts.name,
|
|
1869
1869
|
schedule,
|
|
1870
1870
|
message: opts.message,
|
|
@@ -1876,8 +1876,8 @@ var CronCommands = class {
|
|
|
1876
1876
|
}
|
|
1877
1877
|
cronRemove(jobId) {
|
|
1878
1878
|
const storePath = join3(getDataDir3(), "cron", "jobs.json");
|
|
1879
|
-
const
|
|
1880
|
-
if (
|
|
1879
|
+
const service = new CronService(storePath);
|
|
1880
|
+
if (service.removeJob(jobId)) {
|
|
1881
1881
|
console.log(`\u2713 Removed job ${jobId}`);
|
|
1882
1882
|
} else {
|
|
1883
1883
|
console.log(`Job ${jobId} not found`);
|
|
@@ -1885,8 +1885,8 @@ var CronCommands = class {
|
|
|
1885
1885
|
}
|
|
1886
1886
|
cronEnable(jobId, opts) {
|
|
1887
1887
|
const storePath = join3(getDataDir3(), "cron", "jobs.json");
|
|
1888
|
-
const
|
|
1889
|
-
const job =
|
|
1888
|
+
const service = new CronService(storePath);
|
|
1889
|
+
const job = service.enableJob(jobId, !opts.disable);
|
|
1890
1890
|
if (job) {
|
|
1891
1891
|
console.log(`\u2713 Job '${job.name}' ${opts.disable ? "disabled" : "enabled"}`);
|
|
1892
1892
|
} else {
|
|
@@ -1895,8 +1895,8 @@ var CronCommands = class {
|
|
|
1895
1895
|
}
|
|
1896
1896
|
async cronRun(jobId, opts) {
|
|
1897
1897
|
const storePath = join3(getDataDir3(), "cron", "jobs.json");
|
|
1898
|
-
const
|
|
1899
|
-
const ok = await
|
|
1898
|
+
const service = new CronService(storePath);
|
|
1899
|
+
const ok = await service.runJob(jobId, Boolean(opts.force));
|
|
1900
1900
|
console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
|
|
1901
1901
|
}
|
|
1902
1902
|
};
|
|
@@ -2252,12 +2252,12 @@ import {
|
|
|
2252
2252
|
stopPluginChannelGateways
|
|
2253
2253
|
} from "@nextclaw/openclaw-compat";
|
|
2254
2254
|
import { startUiServer } from "@nextclaw/server";
|
|
2255
|
-
import { appendFileSync, closeSync, cpSync, existsSync as existsSync8, mkdirSync as mkdirSync4, openSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
2255
|
+
import { appendFileSync, closeSync, cpSync, existsSync as existsSync8, mkdirSync as mkdirSync4, mkdtempSync, openSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
2256
2256
|
import { dirname, isAbsolute as isAbsolute2, join as join5, relative, resolve as resolve7 } from "path";
|
|
2257
|
-
import {
|
|
2257
|
+
import { tmpdir } from "os";
|
|
2258
|
+
import { spawn as spawn2 } from "child_process";
|
|
2258
2259
|
import { request as httpRequest } from "http";
|
|
2259
2260
|
import { request as httpsRequest } from "https";
|
|
2260
|
-
import { homedir, userInfo } from "os";
|
|
2261
2261
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2262
2262
|
import chokidar from "chokidar";
|
|
2263
2263
|
|
|
@@ -4035,85 +4035,6 @@ var ServiceCommands = class {
|
|
|
4035
4035
|
clearServiceState();
|
|
4036
4036
|
console.log(`\u2713 ${APP_NAME2} stopped`);
|
|
4037
4037
|
}
|
|
4038
|
-
async installSystemdService(options) {
|
|
4039
|
-
if (process.platform !== "linux") {
|
|
4040
|
-
console.error("Error: systemd installation is only supported on Linux.");
|
|
4041
|
-
return;
|
|
4042
|
-
}
|
|
4043
|
-
if (typeof process.getuid === "function" && process.getuid() !== 0) {
|
|
4044
|
-
console.error("Error: Run this command as root (for example: sudo nextclaw service install-systemd).");
|
|
4045
|
-
return;
|
|
4046
|
-
}
|
|
4047
|
-
const serviceName = this.resolveSystemdServiceName(options.name);
|
|
4048
|
-
const config2 = loadConfig6();
|
|
4049
|
-
const uiConfig = resolveUiConfig(config2, { enabled: true, host: "0.0.0.0" });
|
|
4050
|
-
const uiPort = this.parseSystemdUiPort(options.uiPort, uiConfig.port);
|
|
4051
|
-
if (uiPort === null) {
|
|
4052
|
-
console.error("Error: Invalid --ui-port. Provide a positive integer.");
|
|
4053
|
-
return;
|
|
4054
|
-
}
|
|
4055
|
-
const systemctlAvailable = this.runSystemCommand("systemctl", ["--version"]);
|
|
4056
|
-
if (!systemctlAvailable.ok) {
|
|
4057
|
-
console.error("Error: systemctl is not available on this machine.");
|
|
4058
|
-
return;
|
|
4059
|
-
}
|
|
4060
|
-
const runUser = this.resolveSystemdRunUser();
|
|
4061
|
-
const runHome = this.resolveSystemdRunHome(runUser);
|
|
4062
|
-
const servicePath = `/etc/systemd/system/${serviceName}.service`;
|
|
4063
|
-
const cliPath = fileURLToPath2(new URL("../index.js", import.meta.url));
|
|
4064
|
-
const execArgs = [process.execPath, ...process.execArgv, cliPath, "serve", "--ui-port", String(uiPort)];
|
|
4065
|
-
const unit = this.buildSystemdUnit({
|
|
4066
|
-
runUser,
|
|
4067
|
-
runHome,
|
|
4068
|
-
execArgs
|
|
4069
|
-
});
|
|
4070
|
-
writeFileSync4(servicePath, unit, "utf-8");
|
|
4071
|
-
const daemonReload = this.runSystemCommand("systemctl", ["daemon-reload"]);
|
|
4072
|
-
if (!daemonReload.ok) {
|
|
4073
|
-
console.error(`Error: Failed to reload systemd. ${daemonReload.stderr}`.trim());
|
|
4074
|
-
return;
|
|
4075
|
-
}
|
|
4076
|
-
const enableStart = this.runSystemCommand("systemctl", ["enable", "--now", `${serviceName}.service`]);
|
|
4077
|
-
if (!enableStart.ok) {
|
|
4078
|
-
console.error(`Error: Failed to enable/start ${serviceName}.service. ${enableStart.stderr}`.trim());
|
|
4079
|
-
return;
|
|
4080
|
-
}
|
|
4081
|
-
const active = this.runSystemCommand("systemctl", ["is-active", `${serviceName}.service`]);
|
|
4082
|
-
if (!active.ok || active.stdout.trim() !== "active") {
|
|
4083
|
-
console.error(`Error: ${serviceName}.service is not active. ${active.stderr || active.stdout}`.trim());
|
|
4084
|
-
return;
|
|
4085
|
-
}
|
|
4086
|
-
console.log(`\u2713 Installed systemd service: ${serviceName}.service`);
|
|
4087
|
-
console.log(`Run user: ${runUser}`);
|
|
4088
|
-
console.log(`UI port: ${uiPort}`);
|
|
4089
|
-
console.log(`Unit file: ${servicePath}`);
|
|
4090
|
-
console.log(`Health: http://127.0.0.1:${uiPort}/api/health`);
|
|
4091
|
-
console.log(`Logs: journalctl -u ${serviceName}.service -f`);
|
|
4092
|
-
}
|
|
4093
|
-
async uninstallSystemdService(options) {
|
|
4094
|
-
if (process.platform !== "linux") {
|
|
4095
|
-
console.error("Error: systemd removal is only supported on Linux.");
|
|
4096
|
-
return;
|
|
4097
|
-
}
|
|
4098
|
-
if (typeof process.getuid === "function" && process.getuid() !== 0) {
|
|
4099
|
-
console.error("Error: Run this command as root.");
|
|
4100
|
-
return;
|
|
4101
|
-
}
|
|
4102
|
-
const serviceName = this.resolveSystemdServiceName(options.name);
|
|
4103
|
-
const servicePath = `/etc/systemd/system/${serviceName}.service`;
|
|
4104
|
-
if (!existsSync8(servicePath)) {
|
|
4105
|
-
console.error(`Error: ${servicePath} does not exist.`);
|
|
4106
|
-
return;
|
|
4107
|
-
}
|
|
4108
|
-
this.runSystemCommand("systemctl", ["disable", "--now", `${serviceName}.service`]);
|
|
4109
|
-
rmSync3(servicePath, { force: true });
|
|
4110
|
-
const daemonReload = this.runSystemCommand("systemctl", ["daemon-reload"]);
|
|
4111
|
-
if (!daemonReload.ok) {
|
|
4112
|
-
console.error(`Warning: Removed unit file but failed to reload systemd. ${daemonReload.stderr}`.trim());
|
|
4113
|
-
return;
|
|
4114
|
-
}
|
|
4115
|
-
console.log(`\u2713 Removed systemd service: ${serviceName}.service`);
|
|
4116
|
-
}
|
|
4117
4038
|
async waitForBackgroundServiceReady(params) {
|
|
4118
4039
|
const startedAt = Date.now();
|
|
4119
4040
|
let lastProbeError = null;
|
|
@@ -4144,78 +4065,6 @@ var ServiceCommands = class {
|
|
|
4144
4065
|
const resolved = fromOverride ?? fromEnv ?? fallback;
|
|
4145
4066
|
return Math.max(3e3, resolved);
|
|
4146
4067
|
}
|
|
4147
|
-
resolveSystemdServiceName(rawName) {
|
|
4148
|
-
const trimmed = rawName?.trim() || APP_NAME2;
|
|
4149
|
-
return trimmed.endsWith(".service") ? trimmed.slice(0, -".service".length) : trimmed;
|
|
4150
|
-
}
|
|
4151
|
-
parseSystemdUiPort(rawPort, fallbackPort) {
|
|
4152
|
-
if (rawPort === void 0 || rawPort === null || rawPort === "") {
|
|
4153
|
-
return fallbackPort;
|
|
4154
|
-
}
|
|
4155
|
-
const parsed = Number(rawPort);
|
|
4156
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
4157
|
-
return null;
|
|
4158
|
-
}
|
|
4159
|
-
return parsed;
|
|
4160
|
-
}
|
|
4161
|
-
resolveSystemdRunUser() {
|
|
4162
|
-
const sudoUser = process.env.SUDO_USER?.trim();
|
|
4163
|
-
if (sudoUser && sudoUser !== "root") {
|
|
4164
|
-
return sudoUser;
|
|
4165
|
-
}
|
|
4166
|
-
return userInfo().username;
|
|
4167
|
-
}
|
|
4168
|
-
resolveSystemdRunHome(runUser) {
|
|
4169
|
-
const passwd = this.runSystemCommand("getent", ["passwd", runUser]);
|
|
4170
|
-
if (passwd.ok) {
|
|
4171
|
-
const fields = passwd.stdout.trim().split(":");
|
|
4172
|
-
if (fields.length >= 6 && fields[5]) {
|
|
4173
|
-
return fields[5];
|
|
4174
|
-
}
|
|
4175
|
-
}
|
|
4176
|
-
return process.env.HOME?.trim() || homedir();
|
|
4177
|
-
}
|
|
4178
|
-
buildSystemdUnit(params) {
|
|
4179
|
-
const execStart = params.execArgs.map((arg) => this.escapeSystemdArg(arg)).join(" ");
|
|
4180
|
-
return [
|
|
4181
|
-
"[Unit]",
|
|
4182
|
-
`Description=${APP_NAME2} gateway + UI`,
|
|
4183
|
-
"After=network-online.target",
|
|
4184
|
-
"Wants=network-online.target",
|
|
4185
|
-
"",
|
|
4186
|
-
"[Service]",
|
|
4187
|
-
"Type=simple",
|
|
4188
|
-
`User=${params.runUser}`,
|
|
4189
|
-
`WorkingDirectory=${params.runHome}`,
|
|
4190
|
-
`Environment=HOME=${params.runHome}`,
|
|
4191
|
-
"Environment=NODE_ENV=production",
|
|
4192
|
-
`ExecStart=${execStart}`,
|
|
4193
|
-
"Restart=always",
|
|
4194
|
-
"RestartSec=3",
|
|
4195
|
-
"TimeoutStopSec=20",
|
|
4196
|
-
"",
|
|
4197
|
-
"[Install]",
|
|
4198
|
-
"WantedBy=multi-user.target",
|
|
4199
|
-
""
|
|
4200
|
-
].join("\n");
|
|
4201
|
-
}
|
|
4202
|
-
escapeSystemdArg(value) {
|
|
4203
|
-
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) {
|
|
4204
|
-
return value;
|
|
4205
|
-
}
|
|
4206
|
-
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
4207
|
-
}
|
|
4208
|
-
runSystemCommand(command, args) {
|
|
4209
|
-
const result = spawnSync4(command, args, {
|
|
4210
|
-
encoding: "utf-8",
|
|
4211
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
4212
|
-
});
|
|
4213
|
-
return {
|
|
4214
|
-
ok: result.status === 0,
|
|
4215
|
-
stdout: result.stdout ?? "",
|
|
4216
|
-
stderr: result.stderr ?? ""
|
|
4217
|
-
};
|
|
4218
|
-
}
|
|
4219
4068
|
appendStartupStage(logPath, message) {
|
|
4220
4069
|
try {
|
|
4221
4070
|
appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [startup] ${message}
|
|
@@ -4519,54 +4368,221 @@ var ServiceCommands = class {
|
|
|
4519
4368
|
if (existsSync8(destination) && params.force) {
|
|
4520
4369
|
rmSync3(destination, { recursive: true, force: true });
|
|
4521
4370
|
}
|
|
4522
|
-
const
|
|
4523
|
-
|
|
4524
|
-
|
|
4371
|
+
const materialized = await this.materializeMarketplaceGitSkillSource({ source, skillName });
|
|
4372
|
+
try {
|
|
4373
|
+
const installSkillFile = join5(materialized.skillDir, "SKILL.md");
|
|
4374
|
+
if (!existsSync8(installSkillFile)) {
|
|
4375
|
+
throw new Error(`git skill source does not contain SKILL.md for ${skillName}`);
|
|
4376
|
+
}
|
|
4377
|
+
mkdirSync4(dirname(destination), { recursive: true });
|
|
4378
|
+
cpSync(materialized.skillDir, destination, { recursive: true, force: true });
|
|
4379
|
+
return {
|
|
4380
|
+
message: `Installed skill: ${skillName}`,
|
|
4381
|
+
output: [
|
|
4382
|
+
`Source: ${source}`,
|
|
4383
|
+
`Materialized skill: ${materialized.skillDir}`,
|
|
4384
|
+
`Workspace target: ${destination}`,
|
|
4385
|
+
materialized.commandOutput
|
|
4386
|
+
].filter(Boolean).join("\n")
|
|
4387
|
+
};
|
|
4388
|
+
} finally {
|
|
4389
|
+
rmSync3(materialized.tempRoot, { recursive: true, force: true });
|
|
4525
4390
|
}
|
|
4526
|
-
|
|
4527
|
-
|
|
4391
|
+
}
|
|
4392
|
+
async materializeMarketplaceGitSkillSource(params) {
|
|
4393
|
+
const parsed = this.parseMarketplaceGitSkillSource(params.source);
|
|
4394
|
+
const gitPath = this.resolveGitExecutablePath();
|
|
4395
|
+
const fallbackNotes = [];
|
|
4396
|
+
if (gitPath) {
|
|
4397
|
+
try {
|
|
4398
|
+
return await this.materializeMarketplaceGitSkillViaGit({ gitPath, parsed });
|
|
4399
|
+
} catch (error) {
|
|
4400
|
+
fallbackNotes.push(`Git fast path failed: ${String(error)}`);
|
|
4401
|
+
}
|
|
4402
|
+
} else {
|
|
4403
|
+
fallbackNotes.push("Git fast path unavailable: git executable not found");
|
|
4528
4404
|
}
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4405
|
+
const httpResult = await this.materializeMarketplaceGitSkillViaGithubApi(parsed);
|
|
4406
|
+
return {
|
|
4407
|
+
...httpResult,
|
|
4408
|
+
commandOutput: [...fallbackNotes, httpResult.commandOutput].filter(Boolean).join("\n")
|
|
4409
|
+
};
|
|
4410
|
+
}
|
|
4411
|
+
resolveGitExecutablePath() {
|
|
4412
|
+
return findExecutableOnPath("git");
|
|
4413
|
+
}
|
|
4414
|
+
async materializeMarketplaceGitSkillViaGit(params) {
|
|
4415
|
+
const tempRoot = mkdtempSync(join5(tmpdir(), "nextclaw-marketplace-skill-"));
|
|
4416
|
+
const repoDir = join5(tempRoot, "repo");
|
|
4417
|
+
try {
|
|
4418
|
+
const cloneArgs = ["clone", "--depth", "1", "--filter=blob:none", "--sparse"];
|
|
4419
|
+
if (params.parsed.ref) {
|
|
4420
|
+
cloneArgs.push("--branch", params.parsed.ref);
|
|
4421
|
+
}
|
|
4422
|
+
cloneArgs.push(params.parsed.repoUrl, repoDir);
|
|
4423
|
+
const cloneResult = await this.runCommand(params.gitPath, cloneArgs, {
|
|
4424
|
+
cwd: tempRoot,
|
|
4534
4425
|
timeoutMs: 18e4
|
|
4426
|
+
});
|
|
4427
|
+
const sparseResult = await this.runCommand(params.gitPath, ["-C", repoDir, "sparse-checkout", "set", params.parsed.skillPath], {
|
|
4428
|
+
cwd: tempRoot,
|
|
4429
|
+
timeoutMs: 6e4
|
|
4430
|
+
});
|
|
4431
|
+
const skillDir = join5(repoDir, params.parsed.skillPath);
|
|
4432
|
+
return {
|
|
4433
|
+
tempRoot,
|
|
4434
|
+
skillDir,
|
|
4435
|
+
commandOutput: [
|
|
4436
|
+
"Installer path: git-sparse",
|
|
4437
|
+
`Git repository: ${params.parsed.repoUrl}`,
|
|
4438
|
+
`Git path: ${params.parsed.skillPath}`,
|
|
4439
|
+
this.mergeCommandOutput(cloneResult.stdout, cloneResult.stderr),
|
|
4440
|
+
this.mergeCommandOutput(sparseResult.stdout, sparseResult.stderr)
|
|
4441
|
+
].filter(Boolean).join("\n")
|
|
4442
|
+
};
|
|
4443
|
+
} catch (error) {
|
|
4444
|
+
rmSync3(tempRoot, { recursive: true, force: true });
|
|
4445
|
+
throw error;
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
async materializeMarketplaceGitSkillViaGithubApi(parsed) {
|
|
4449
|
+
const tempRoot = mkdtempSync(join5(tmpdir(), "nextclaw-marketplace-skill-"));
|
|
4450
|
+
const skillDir = join5(tempRoot, "skill");
|
|
4451
|
+
const downloadedFiles = [];
|
|
4452
|
+
try {
|
|
4453
|
+
mkdirSync4(skillDir, { recursive: true });
|
|
4454
|
+
await this.downloadGithubDirectoryToPath({
|
|
4455
|
+
parsed,
|
|
4456
|
+
remotePath: parsed.skillPath,
|
|
4457
|
+
localDir: skillDir,
|
|
4458
|
+
relativePrefix: "",
|
|
4459
|
+
downloadedFiles
|
|
4460
|
+
});
|
|
4461
|
+
return {
|
|
4462
|
+
tempRoot,
|
|
4463
|
+
skillDir,
|
|
4464
|
+
commandOutput: [
|
|
4465
|
+
"Installer path: github-http",
|
|
4466
|
+
`Git repository: ${parsed.repoUrl}`,
|
|
4467
|
+
`Git path: ${parsed.skillPath}`,
|
|
4468
|
+
`Downloaded files: ${downloadedFiles.length}`
|
|
4469
|
+
].join("\n")
|
|
4470
|
+
};
|
|
4471
|
+
} catch (error) {
|
|
4472
|
+
rmSync3(tempRoot, { recursive: true, force: true });
|
|
4473
|
+
throw error;
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4476
|
+
async downloadGithubDirectoryToPath(params) {
|
|
4477
|
+
const encodedPath = params.remotePath.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
|
|
4478
|
+
const apiUrl = new URL(`https://api.github.com/repos/${params.parsed.owner}/${params.parsed.repo}/contents/${encodedPath}`);
|
|
4479
|
+
if (params.parsed.ref) {
|
|
4480
|
+
apiUrl.searchParams.set("ref", params.parsed.ref);
|
|
4481
|
+
}
|
|
4482
|
+
const payload = await this.fetchJsonWithHeaders(apiUrl.toString(), {
|
|
4483
|
+
Accept: "application/vnd.github+json",
|
|
4484
|
+
"User-Agent": `${APP_NAME2}/${getPackageVersion()}`
|
|
4485
|
+
});
|
|
4486
|
+
if (!Array.isArray(payload)) {
|
|
4487
|
+
throw new Error(`GitHub path is not a directory: ${params.remotePath}`);
|
|
4488
|
+
}
|
|
4489
|
+
for (const entry of payload) {
|
|
4490
|
+
if (!entry || typeof entry !== "object") {
|
|
4491
|
+
continue;
|
|
4535
4492
|
}
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4493
|
+
const item = entry;
|
|
4494
|
+
const itemName = typeof item.name === "string" ? item.name.trim() : "";
|
|
4495
|
+
const itemPath = typeof item.path === "string" ? item.path.trim() : "";
|
|
4496
|
+
if (!itemName || !itemPath) {
|
|
4497
|
+
continue;
|
|
4498
|
+
}
|
|
4499
|
+
if (item.type === "dir") {
|
|
4500
|
+
const childLocalDir = join5(params.localDir, itemName);
|
|
4501
|
+
mkdirSync4(childLocalDir, { recursive: true });
|
|
4502
|
+
await this.downloadGithubDirectoryToPath({
|
|
4503
|
+
parsed: params.parsed,
|
|
4504
|
+
remotePath: itemPath,
|
|
4505
|
+
localDir: childLocalDir,
|
|
4506
|
+
relativePrefix: params.relativePrefix ? `${params.relativePrefix}/${itemName}` : itemName,
|
|
4507
|
+
downloadedFiles: params.downloadedFiles
|
|
4508
|
+
});
|
|
4509
|
+
continue;
|
|
4510
|
+
}
|
|
4511
|
+
if (item.type !== "file") {
|
|
4512
|
+
throw new Error(`Unsupported GitHub skill entry type: ${item.type ?? "unknown"}`);
|
|
4513
|
+
}
|
|
4514
|
+
if (!item.download_url) {
|
|
4515
|
+
throw new Error(`GitHub skill file missing download_url: ${itemPath}`);
|
|
4516
|
+
}
|
|
4517
|
+
const content = await this.fetchBinaryWithHeaders(item.download_url, {
|
|
4518
|
+
"User-Agent": `${APP_NAME2}/${getPackageVersion()}`
|
|
4519
|
+
});
|
|
4520
|
+
const localFile = join5(params.localDir, itemName);
|
|
4521
|
+
writeFileSync4(localFile, content);
|
|
4522
|
+
params.downloadedFiles.push(params.relativePrefix ? `${params.relativePrefix}/${itemName}` : itemName);
|
|
4549
4523
|
}
|
|
4550
|
-
|
|
4551
|
-
|
|
4524
|
+
}
|
|
4525
|
+
async fetchJsonWithHeaders(url, headers) {
|
|
4526
|
+
const response = await fetch(url, { headers });
|
|
4527
|
+
if (!response.ok) {
|
|
4528
|
+
throw new Error(`HTTP ${response.status} when fetching ${url}`);
|
|
4552
4529
|
}
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4530
|
+
return await response.json();
|
|
4531
|
+
}
|
|
4532
|
+
async fetchBinaryWithHeaders(url, headers) {
|
|
4533
|
+
const response = await fetch(url, { headers });
|
|
4534
|
+
if (!response.ok) {
|
|
4535
|
+
throw new Error(`HTTP ${response.status} when downloading ${url}`);
|
|
4557
4536
|
}
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4537
|
+
const bytes = await response.arrayBuffer();
|
|
4538
|
+
return Buffer.from(bytes);
|
|
4539
|
+
}
|
|
4540
|
+
parseMarketplaceGitSkillSource(source) {
|
|
4541
|
+
const trimmed = source.trim();
|
|
4542
|
+
if (!trimmed) {
|
|
4543
|
+
throw new Error("Git skill source is required");
|
|
4544
|
+
}
|
|
4545
|
+
if (trimmed.includes("://")) {
|
|
4546
|
+
const parsedUrl = new URL(trimmed);
|
|
4547
|
+
if (parsedUrl.hostname !== "github.com") {
|
|
4548
|
+
throw new Error(`Unsupported git skill source host: ${parsedUrl.hostname}`);
|
|
4549
|
+
}
|
|
4550
|
+
const parts2 = parsedUrl.pathname.split("/").filter(Boolean);
|
|
4551
|
+
if (parts2.length < 5 || parts2[2] !== "tree" && parts2[2] !== "blob") {
|
|
4552
|
+
throw new Error(`Unsupported GitHub skill source URL: ${source}`);
|
|
4553
|
+
}
|
|
4554
|
+
const [owner2, repoRaw, , ref2, ...pathParts2] = parts2;
|
|
4555
|
+
const repo2 = repoRaw.replace(/\.git$/i, "");
|
|
4556
|
+
if (!owner2 || !repo2 || !ref2 || pathParts2.length === 0) {
|
|
4557
|
+
throw new Error(`Unsupported GitHub skill source URL: ${source}`);
|
|
4558
|
+
}
|
|
4559
|
+
return {
|
|
4560
|
+
owner: owner2,
|
|
4561
|
+
repo: repo2,
|
|
4562
|
+
repoUrl: `https://github.com/${owner2}/${repo2}.git`,
|
|
4563
|
+
skillPath: pathParts2.join("/"),
|
|
4564
|
+
ref: ref2
|
|
4565
|
+
};
|
|
4566
|
+
}
|
|
4567
|
+
const parts = trimmed.split("/").filter(Boolean);
|
|
4568
|
+
if (parts.length < 3) {
|
|
4569
|
+
throw new Error(`Unsupported git skill source: ${source}`);
|
|
4570
|
+
}
|
|
4571
|
+
const owner = parts[0] ?? "";
|
|
4572
|
+
const repoWithOptionalRef = parts[1] ?? "";
|
|
4573
|
+
const pathParts = parts.slice(2);
|
|
4574
|
+
const atIndex = repoWithOptionalRef.indexOf("@");
|
|
4575
|
+
const repo = (atIndex >= 0 ? repoWithOptionalRef.slice(0, atIndex) : repoWithOptionalRef).replace(/\.git$/i, "");
|
|
4576
|
+
const ref = atIndex >= 0 ? repoWithOptionalRef.slice(atIndex + 1) : void 0;
|
|
4577
|
+
if (!owner || !repo || pathParts.length === 0) {
|
|
4578
|
+
throw new Error(`Unsupported git skill source: ${source}`);
|
|
4561
4579
|
}
|
|
4562
4580
|
return {
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
this.mergeCommandOutput(result.stdout, result.stderr)
|
|
4569
|
-
].filter(Boolean).join("\n")
|
|
4581
|
+
owner,
|
|
4582
|
+
repo,
|
|
4583
|
+
repoUrl: `https://github.com/${owner}/${repo}.git`,
|
|
4584
|
+
skillPath: pathParts.join("/"),
|
|
4585
|
+
ref: ref && ref.trim().length > 0 ? ref.trim() : void 0
|
|
4570
4586
|
};
|
|
4571
4587
|
}
|
|
4572
4588
|
async enableMarketplacePlugin(id) {
|
|
@@ -4587,17 +4603,13 @@ var ServiceCommands = class {
|
|
|
4587
4603
|
async uninstallMarketplaceSkill(slug) {
|
|
4588
4604
|
const workspace = getWorkspacePath5(loadConfig6().agents.defaults.workspace);
|
|
4589
4605
|
const targetDir = join5(workspace, "skills", slug);
|
|
4590
|
-
|
|
4591
|
-
const existingTargets = [targetDir, skildDir].filter((path) => existsSync8(path));
|
|
4592
|
-
if (existingTargets.length === 0) {
|
|
4606
|
+
if (!existsSync8(targetDir)) {
|
|
4593
4607
|
throw new Error(`Skill not installed in workspace: ${slug}`);
|
|
4594
4608
|
}
|
|
4595
|
-
|
|
4596
|
-
rmSync3(path, { recursive: true, force: true });
|
|
4597
|
-
}
|
|
4609
|
+
rmSync3(targetDir, { recursive: true, force: true });
|
|
4598
4610
|
return {
|
|
4599
4611
|
message: `Uninstalled skill: ${slug}`,
|
|
4600
|
-
output:
|
|
4612
|
+
output: `Removed ${targetDir}`
|
|
4601
4613
|
};
|
|
4602
4614
|
}
|
|
4603
4615
|
installBuiltinMarketplaceSkill(slug, force) {
|
|
@@ -4659,39 +4671,6 @@ var ServiceCommands = class {
|
|
|
4659
4671
|
}
|
|
4660
4672
|
return destination;
|
|
4661
4673
|
}
|
|
4662
|
-
parseSkildJsonOutput(stdout) {
|
|
4663
|
-
const trimmed = stdout.trim();
|
|
4664
|
-
if (!trimmed) {
|
|
4665
|
-
throw new Error("skild returned empty output");
|
|
4666
|
-
}
|
|
4667
|
-
const maybeJson = (() => {
|
|
4668
|
-
const start = trimmed.indexOf("{");
|
|
4669
|
-
const end = trimmed.lastIndexOf("}");
|
|
4670
|
-
if (start >= 0 && end > start) {
|
|
4671
|
-
return trimmed.slice(start, end + 1);
|
|
4672
|
-
}
|
|
4673
|
-
return trimmed;
|
|
4674
|
-
})();
|
|
4675
|
-
try {
|
|
4676
|
-
const parsed = JSON.parse(maybeJson);
|
|
4677
|
-
if (parsed === null) {
|
|
4678
|
-
return null;
|
|
4679
|
-
}
|
|
4680
|
-
if (Array.isArray(parsed)) {
|
|
4681
|
-
const firstObject = parsed.find((item) => item && typeof item === "object" && !Array.isArray(item));
|
|
4682
|
-
if (firstObject) {
|
|
4683
|
-
return firstObject;
|
|
4684
|
-
}
|
|
4685
|
-
throw new Error("skild json output array does not contain an object");
|
|
4686
|
-
}
|
|
4687
|
-
if (typeof parsed !== "object") {
|
|
4688
|
-
throw new Error("skild json output is not an object");
|
|
4689
|
-
}
|
|
4690
|
-
return parsed;
|
|
4691
|
-
} catch (error) {
|
|
4692
|
-
throw new Error(`failed to parse skild --json output: ${String(error)}`);
|
|
4693
|
-
}
|
|
4694
|
-
}
|
|
4695
4674
|
mergeCommandOutput(stdout, stderr) {
|
|
4696
4675
|
return `${stdout}
|
|
4697
4676
|
${stderr}`.trim();
|
|
@@ -4768,7 +4747,7 @@ import { createRequire } from "module";
|
|
|
4768
4747
|
import { dirname as dirname2, join as join6, resolve as resolve8 } from "path";
|
|
4769
4748
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4770
4749
|
import { APP_NAME as APP_NAME3, getDataDir as getDataDir7 } from "@nextclaw/core";
|
|
4771
|
-
import { spawnSync as
|
|
4750
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
4772
4751
|
var WorkspaceManager = class {
|
|
4773
4752
|
constructor(logo) {
|
|
4774
4753
|
this.logo = logo;
|
|
@@ -4915,7 +4894,7 @@ var WorkspaceManager = class {
|
|
|
4915
4894
|
recursive: true,
|
|
4916
4895
|
filter: (src) => !src.includes("node_modules") && !src.includes("dist")
|
|
4917
4896
|
});
|
|
4918
|
-
const install =
|
|
4897
|
+
const install = spawnSync4("npm", ["install"], { cwd: userBridge, stdio: "pipe" });
|
|
4919
4898
|
if (install.status !== 0) {
|
|
4920
4899
|
console.error(`Bridge install failed: ${install.status ?? 1}`);
|
|
4921
4900
|
if (install.stderr) {
|
|
@@ -4923,7 +4902,7 @@ var WorkspaceManager = class {
|
|
|
4923
4902
|
}
|
|
4924
4903
|
process.exit(1);
|
|
4925
4904
|
}
|
|
4926
|
-
const build =
|
|
4905
|
+
const build = spawnSync4("npm", ["run", "build"], { cwd: userBridge, stdio: "pipe" });
|
|
4927
4906
|
if (build.status !== 0) {
|
|
4928
4907
|
console.error(`Bridge build failed: ${build.status ?? 1}`);
|
|
4929
4908
|
if (build.stderr) {
|
|
@@ -5293,12 +5272,6 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
|
|
|
5293
5272
|
async stop() {
|
|
5294
5273
|
await this.serviceCommands.stopService();
|
|
5295
5274
|
}
|
|
5296
|
-
async serviceInstallSystemd(opts) {
|
|
5297
|
-
await this.serviceCommands.installSystemdService(opts);
|
|
5298
|
-
}
|
|
5299
|
-
async serviceUninstallSystemd(opts) {
|
|
5300
|
-
await this.serviceCommands.uninstallSystemdService(opts);
|
|
5301
|
-
}
|
|
5302
5275
|
async agent(opts) {
|
|
5303
5276
|
const configPath = getConfigPath4();
|
|
5304
5277
|
const config2 = resolveConfigSecrets3(loadConfig7(), { configPath });
|
|
@@ -5561,9 +5534,6 @@ program.command("start").description(`Start the ${APP_NAME5} gateway + UI in the
|
|
|
5561
5534
|
program.command("restart").description(`Restart the ${APP_NAME5} background service`).option("--ui-port <port>", "UI port").option("--start-timeout <ms>", "Maximum wait time for startup readiness in milliseconds").option("--open", "Open browser after restart", false).action(async (opts) => runtime.restart(opts));
|
|
5562
5535
|
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));
|
|
5563
5536
|
program.command("stop").description(`Stop the ${APP_NAME5} background service`).action(async () => runtime.stop());
|
|
5564
|
-
var service = program.command("service").description("Manage OS service integration");
|
|
5565
|
-
service.command("install-systemd").description(`Install a systemd unit for ${APP_NAME5} on Linux servers`).option("--name <name>", "Systemd unit name", APP_NAME5).option("--ui-port <port>", "UI port").action(async (opts) => runtime.serviceInstallSystemd(opts));
|
|
5566
|
-
service.command("uninstall-systemd").description(`Remove the systemd unit for ${APP_NAME5}`).option("--name <name>", "Systemd unit name", APP_NAME5).action(async (opts) => runtime.serviceUninstallSystemd(opts));
|
|
5567
5537
|
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("--model <model>", "Session model override for this run").option("--no-markdown", "Disable Markdown rendering").action(async (opts) => runtime.agent(opts));
|
|
5568
5538
|
program.command("update").description(`Update ${APP_NAME5}`).option("--timeout <ms>", "Update command timeout in milliseconds").action(async (opts) => runtime.update(opts));
|
|
5569
5539
|
var registerClawHubInstall = (cmd) => {
|
package/package.json
CHANGED
package/templates/USAGE.md
CHANGED
|
@@ -418,8 +418,6 @@ Created under the workspace:
|
|
|
418
418
|
| `nextclaw start` | Start gateway + UI in the background |
|
|
419
419
|
| `nextclaw restart` | Restart the background service with optional start flags |
|
|
420
420
|
| `nextclaw stop` | Stop the background service |
|
|
421
|
-
| `sudo nextclaw service install-systemd` | Install a managed Linux `systemd` service for public/server deployment |
|
|
422
|
-
| `sudo nextclaw service uninstall-systemd` | Remove the managed Linux `systemd` service |
|
|
423
421
|
| `nextclaw ui` | Start UI and gateway in the foreground |
|
|
424
422
|
| `nextclaw gateway` | Start gateway only (for channels) |
|
|
425
423
|
| `nextclaw serve` | Run gateway + UI in the foreground (no background) |
|
|
@@ -460,13 +458,6 @@ Gateway options (when running `nextclaw gateway` or `nextclaw start`):
|
|
|
460
458
|
|
|
461
459
|
If service is already running, new UI port flags do not hot-apply; use `nextclaw restart ...` to apply them.
|
|
462
460
|
|
|
463
|
-
Linux server deployment tip:
|
|
464
|
-
|
|
465
|
-
- If you put Nginx/Caddy/Traefik in front of NextClaw, do not rely on a one-time `nextclaw start` only.
|
|
466
|
-
- `nextclaw start` is a background convenience command, not a crash/reboot supervisor.
|
|
467
|
-
- On Linux servers, install a managed unit with `sudo nextclaw service install-systemd` so the UI/API comes back automatically after reboot or unexpected exit.
|
|
468
|
-
- Verify with `systemctl status nextclaw`, `journalctl -u nextclaw -f`, and `curl http://127.0.0.1:18791/api/health`.
|
|
469
|
-
|
|
470
461
|
Status/diagnostics tips:
|
|
471
462
|
|
|
472
463
|
- `nextclaw status` shows runtime truth (process + health + config summary).
|