nextclaw 0.20.5 → 0.21.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/app/index.js +812 -3
- package/dist/cli/app/index.js.map +1 -1
- package/package.json +16 -16
- package/ui-dist/assets/api-BCDb13Uv.js +8 -0
- package/ui-dist/assets/app-presenter-provider-C-QMjK7m.js +3 -0
- package/ui-dist/assets/arrow-left-rIXFA2Zy.js +1 -0
- package/ui-dist/assets/{channels-list-page-BnGxv6ma.js → channels-list-page-DnqwsKGe.js} +6 -6
- package/ui-dist/assets/chat-page-CsQx20xL.js +105 -0
- package/ui-dist/assets/{config-split-page-CWN2YG9U.js → config-split-page-BwhGqmUS.js} +1 -1
- package/ui-dist/assets/confirm-dialog-DBkKcQ1q.js +5 -0
- package/ui-dist/assets/{desktop-update-config-abmYzHSy.js → desktop-update-config-BE9KjiXi.js} +1 -1
- package/ui-dist/assets/dist-BI0bX8jv.js +1 -0
- package/ui-dist/assets/dist-C8JRt-tf.js +1 -0
- package/ui-dist/assets/{doc-browser-context-Cb1sEUT0.js → doc-browser-context-VHUOIUXL.js} +1 -1
- package/ui-dist/assets/doc-browser-eqdNEerh.js +1 -0
- package/ui-dist/assets/doc-browser-lpF_pyx4.js +1 -0
- package/ui-dist/assets/ellipsis-C9VB2CxT.js +1 -0
- package/ui-dist/assets/es2015-BCw3Q7nY.js +41 -0
- package/ui-dist/assets/external-link-Bp7_4s9t.js +1 -0
- package/ui-dist/assets/index-BCq7kbC8.css +1 -0
- package/ui-dist/assets/{index-Ccbswzt9.js → index-DTdPDqto.js} +12 -12
- package/ui-dist/assets/key-round-Ccy6oND3.js +1 -0
- package/ui-dist/assets/loader-circle-OpH4EErP.js +1 -0
- package/ui-dist/assets/{mcp-marketplace-page-CmUKUeno.js → mcp-marketplace-page-B-GxKOqS.js} +2 -2
- package/ui-dist/assets/mcp-marketplace-page-Dm-0-w51.js +1 -0
- package/ui-dist/assets/{model-config-Dbo3zWDl.js → model-config-BXPnV_7n.js} +1 -1
- package/ui-dist/assets/{notice-card-CFTFtJky.js → notice-card-Dsbkl7u7.js} +1 -1
- package/ui-dist/assets/play-CvgYhNbo.js +1 -0
- package/ui-dist/assets/plus-DHcP-K1U.js +1 -0
- package/ui-dist/assets/popover-BTLZE5Iz.js +1 -0
- package/ui-dist/assets/provider-scoped-model-input-DKI9KN7N.js +1 -0
- package/ui-dist/assets/providers-list-Bko6B7dE.js +1 -0
- package/ui-dist/assets/react-BqS8oMyM.js +8 -0
- package/ui-dist/assets/refresh-cw-BIaj7txb.js +1 -0
- package/ui-dist/assets/remote-DHn_b8QU.js +1 -0
- package/ui-dist/assets/rotate-cw-DMqSkRJQ.js +1 -0
- package/ui-dist/assets/{runtime-config-page-Dg8aEgWx.js → runtime-config-page-DX_wSK1J.js} +1 -1
- package/ui-dist/assets/save-BEoORten.js +1 -0
- package/ui-dist/assets/search-COFh_SFI.js +1 -0
- package/ui-dist/assets/{search-config-DAibVFGR.js → search-config-DA5By-tE.js} +1 -1
- package/ui-dist/assets/{secrets-config-CoJzy13_.js → secrets-config-eDxG7iN4.js} +1 -1
- package/ui-dist/assets/{status-dot-D8xx5ZR8.js → status-dot-oWBjTovo.js} +1 -1
- package/ui-dist/assets/{tabs-DxmND60Q.js → tabs-CGaY0BVO.js} +1 -1
- package/ui-dist/assets/{tabs-custom-Cjo0bHw8.js → tabs-custom-D2fPYb4G.js} +1 -1
- package/ui-dist/assets/{tag-chip-DnNWFRMu.js → tag-chip-TVkgofS1.js} +1 -1
- package/ui-dist/assets/tooltip-BwHPKBv9.js +1 -0
- package/ui-dist/assets/trash-2-D3psAGrz.js +1 -0
- package/ui-dist/assets/x-CHAxzyp2.js +1 -0
- package/ui-dist/index.html +25 -24
- package/ui-dist/assets/api-BAyyGaoz.js +0 -15
- package/ui-dist/assets/app-presenter-provider-DqHHfC1K.js +0 -3
- package/ui-dist/assets/arrow-left-Bn-3Pj3p.js +0 -1
- package/ui-dist/assets/boxes-ClVuotcz.js +0 -1
- package/ui-dist/assets/chat-page-DInyWLb4.js +0 -105
- package/ui-dist/assets/confirm-dialog-CVbgKuHp.js +0 -5
- package/ui-dist/assets/createLucideIcon-CjPUPAAK.js +0 -1
- package/ui-dist/assets/dist-hkrb6Ynt.js +0 -41
- package/ui-dist/assets/doc-browser-BlrpUKVJ.js +0 -1
- package/ui-dist/assets/doc-browser-kJ2ClG0H.js +0 -1
- package/ui-dist/assets/ellipsis-CdkxFkVZ.js +0 -1
- package/ui-dist/assets/external-link-CT9_qFwN.js +0 -1
- package/ui-dist/assets/index-BGXTR0F_.css +0 -1
- package/ui-dist/assets/key-round-xJfR4iRe.js +0 -1
- package/ui-dist/assets/loader-circle-BI6HJDg5.js +0 -1
- package/ui-dist/assets/mcp-marketplace-page-DH11bSVx.js +0 -1
- package/ui-dist/assets/play-DyBWW2j4.js +0 -1
- package/ui-dist/assets/plus-BlnBmcKE.js +0 -1
- package/ui-dist/assets/popover-DPqa_Mfb.js +0 -1
- package/ui-dist/assets/provider-scoped-model-input-C6xL7zSJ.js +0 -1
- package/ui-dist/assets/providers-list-DOj_7GL8.js +0 -1
- package/ui-dist/assets/react-oAHNoVya.js +0 -1
- package/ui-dist/assets/refresh-cw-o8NSmIYV.js +0 -1
- package/ui-dist/assets/remote-fCE5fLsj.js +0 -1
- package/ui-dist/assets/rotate-cw-DA8EqNAH.js +0 -1
- package/ui-dist/assets/save-D4-g0EAq.js +0 -1
- package/ui-dist/assets/search-G2cMn3M5.js +0 -1
- package/ui-dist/assets/trash-2-C9tc-9sQ.js +0 -1
- package/ui-dist/assets/x-DmLl2yG6.js +0 -1
- /package/ui-dist/assets/{config-hints-TmNgpFHv.js → config-hints-Ceiol9x4.js} +0 -0
- /package/ui-dist/assets/{provider-models-CXUgDxGR.js → provider-models-C_yOh6DE.js} +0 -0
package/dist/cli/app/index.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as createNextclawDistribution } from "../../nextclaw-distribution.utils-BJZhAnGk.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { APP_NAME, APP_TAGLINE, loadConfig } from "@nextclaw/core";
|
|
4
|
+
import { APP_NAME, APP_TAGLINE, getConfigPath, loadConfig, resolveConfigSecrets } from "@nextclaw/core";
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
-
import "node:fs";
|
|
7
|
-
import "node:path";
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
8
|
import "@nextclaw/server";
|
|
9
9
|
import { NextclawDistributionService, NextclawServiceRuntime, readLearningLoopRuntimeConfig } from "@nextclaw/service";
|
|
10
|
+
import { McpServiceAppRuntimeService, buildServiceActionId, getServiceAppManifestPath, mergeServiceAppRuntimeActions, readServiceAppManifest } from "@nextclaw/kernel";
|
|
11
|
+
import { access, readFile, stat } from "node:fs/promises";
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
13
|
+
import { promisify } from "node:util";
|
|
10
14
|
var __commonJSMin = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
11
15
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
12
16
|
//#endregion
|
|
@@ -3619,6 +3623,810 @@ const registerHostServiceControls = ({ program, nextclaw }) => {
|
|
|
3619
3623
|
autostart.command("doctor").description("Diagnose host autostart setup").option("--user", "Inspect the user-level autostart owner", false).option("--system", "Inspect the system-level autostart owner", false).option("--json", "Output JSON", false).action(async (opts) => serviceCommands.autostartDoctor(opts));
|
|
3620
3624
|
};
|
|
3621
3625
|
//#endregion
|
|
3626
|
+
//#region src/cli/app/services/service-app-dev.service.ts
|
|
3627
|
+
var ServiceAppDevService = class {
|
|
3628
|
+
constructor(params = {}) {
|
|
3629
|
+
this.params = params;
|
|
3630
|
+
}
|
|
3631
|
+
inspect = async (target) => {
|
|
3632
|
+
const appPath = path.resolve(target);
|
|
3633
|
+
const issues = [];
|
|
3634
|
+
const loaded = await this.loadServiceApp(appPath, issues);
|
|
3635
|
+
if (!loaded) return this.buildDevReport(appPath, void 0, [], issues);
|
|
3636
|
+
if (this.hasErrors(issues)) return this.buildDevReport(appPath, this.toServiceAppRecord(appPath, loaded.manifest, this.idleRuntimeStatus), [], issues);
|
|
3637
|
+
const runtime = this.createRuntimeService();
|
|
3638
|
+
try {
|
|
3639
|
+
const startRecord = this.toServiceAppRecord(appPath, loaded.manifest, runtime);
|
|
3640
|
+
const runtimeActions = await runtime.listActions({
|
|
3641
|
+
app: startRecord,
|
|
3642
|
+
manifest: loaded.manifest
|
|
3643
|
+
});
|
|
3644
|
+
const record = this.toServiceAppRecord(appPath, loaded.manifest, runtime);
|
|
3645
|
+
const actions = mergeServiceAppRuntimeActions({
|
|
3646
|
+
record,
|
|
3647
|
+
manifest: loaded.manifest,
|
|
3648
|
+
runtimeActions
|
|
3649
|
+
});
|
|
3650
|
+
this.collectRuntimeIssues(record, actions, issues);
|
|
3651
|
+
return this.buildDevReport(appPath, record, actions, issues);
|
|
3652
|
+
} finally {
|
|
3653
|
+
await runtime.dispose();
|
|
3654
|
+
}
|
|
3655
|
+
};
|
|
3656
|
+
call = async (target, actionName, input) => {
|
|
3657
|
+
const appPath = path.resolve(target);
|
|
3658
|
+
const issues = [];
|
|
3659
|
+
const loaded = await this.loadServiceApp(appPath, issues);
|
|
3660
|
+
if (!loaded) return this.buildCallReport(appPath, void 0, void 0, void 0, issues);
|
|
3661
|
+
if (this.hasErrors(issues)) return this.buildCallReport(appPath, this.toServiceAppRecord(appPath, loaded.manifest, this.idleRuntimeStatus), void 0, void 0, issues);
|
|
3662
|
+
const action = actionName.trim();
|
|
3663
|
+
if (!Object.hasOwn(loaded.manifest.actions, action)) {
|
|
3664
|
+
issues.push({
|
|
3665
|
+
severity: "error",
|
|
3666
|
+
code: "service.action.notDeclared",
|
|
3667
|
+
message: `service-app.json does not declare action: ${action || "(empty)"}.`
|
|
3668
|
+
});
|
|
3669
|
+
const record = this.toServiceAppRecord(appPath, loaded.manifest, this.idleRuntimeStatus);
|
|
3670
|
+
return this.buildCallReport(appPath, record, void 0, void 0, issues);
|
|
3671
|
+
}
|
|
3672
|
+
const runtime = this.createRuntimeService();
|
|
3673
|
+
try {
|
|
3674
|
+
const record = this.toServiceAppRecord(appPath, loaded.manifest, runtime);
|
|
3675
|
+
const result = await runtime.invokeAction({
|
|
3676
|
+
app: record,
|
|
3677
|
+
manifest: loaded.manifest,
|
|
3678
|
+
actionName: action,
|
|
3679
|
+
input
|
|
3680
|
+
});
|
|
3681
|
+
const nextRecord = this.toServiceAppRecord(appPath, loaded.manifest, runtime);
|
|
3682
|
+
return this.buildCallReport(appPath, nextRecord, buildServiceActionId(loaded.manifest.id, action), result, issues);
|
|
3683
|
+
} catch (error) {
|
|
3684
|
+
const record = this.toServiceAppRecord(appPath, loaded.manifest, runtime);
|
|
3685
|
+
issues.push({
|
|
3686
|
+
severity: "error",
|
|
3687
|
+
code: "service.runtime.callFailed",
|
|
3688
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3689
|
+
});
|
|
3690
|
+
return this.buildCallReport(appPath, record, buildServiceActionId(loaded.manifest.id, action), void 0, issues);
|
|
3691
|
+
} finally {
|
|
3692
|
+
await runtime.dispose();
|
|
3693
|
+
}
|
|
3694
|
+
};
|
|
3695
|
+
loadServiceApp = async (appPath, issues) => {
|
|
3696
|
+
try {
|
|
3697
|
+
const manifest = await readServiceAppManifest(appPath);
|
|
3698
|
+
if (manifest.id !== path.basename(appPath)) issues.push({
|
|
3699
|
+
severity: "error",
|
|
3700
|
+
code: "service.id.invalid",
|
|
3701
|
+
message: `service-app.json id must equal directory name: ${path.basename(appPath)}.`
|
|
3702
|
+
});
|
|
3703
|
+
if (!manifest.enabled) issues.push({
|
|
3704
|
+
severity: "error",
|
|
3705
|
+
code: "service.disabled",
|
|
3706
|
+
message: "Service App is disabled and cannot be started by app dev."
|
|
3707
|
+
});
|
|
3708
|
+
return { manifest };
|
|
3709
|
+
} catch (error) {
|
|
3710
|
+
issues.push({
|
|
3711
|
+
severity: "error",
|
|
3712
|
+
code: "service.manifest.readFailed",
|
|
3713
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3714
|
+
});
|
|
3715
|
+
return null;
|
|
3716
|
+
}
|
|
3717
|
+
};
|
|
3718
|
+
createRuntimeService = () => this.params.runtimeService ?? new McpServiceAppRuntimeService({ getConfig: this.params.getConfig ?? this.loadRuntimeConfig });
|
|
3719
|
+
loadRuntimeConfig = () => {
|
|
3720
|
+
const configPath = getConfigPath();
|
|
3721
|
+
return resolveConfigSecrets(loadConfig(configPath), { configPath });
|
|
3722
|
+
};
|
|
3723
|
+
idleRuntimeStatus = { getStatus: () => ({ status: "idle" }) };
|
|
3724
|
+
toServiceAppRecord = (dirPath, manifest, runtime) => {
|
|
3725
|
+
const runtimeStatus = runtime.getStatus(manifest.id);
|
|
3726
|
+
const record = {
|
|
3727
|
+
id: manifest.id,
|
|
3728
|
+
title: manifest.title,
|
|
3729
|
+
dirPath,
|
|
3730
|
+
manifestPath: getServiceAppManifestPath(dirPath),
|
|
3731
|
+
command: manifest.command,
|
|
3732
|
+
args: manifest.args,
|
|
3733
|
+
cwd: dirPath,
|
|
3734
|
+
enabled: manifest.enabled,
|
|
3735
|
+
protocol: manifest.protocol,
|
|
3736
|
+
status: manifest.enabled ? runtimeStatus.status : "stopped"
|
|
3737
|
+
};
|
|
3738
|
+
if (manifest.description) record.description = manifest.description;
|
|
3739
|
+
if (runtimeStatus.lastError) record.lastError = runtimeStatus.lastError;
|
|
3740
|
+
if (runtimeStatus.lastStartedAt) record.lastStartedAt = runtimeStatus.lastStartedAt;
|
|
3741
|
+
if (runtimeStatus.lastReadyAt) record.lastReadyAt = runtimeStatus.lastReadyAt;
|
|
3742
|
+
if (runtimeStatus.lastFailedAt) record.lastFailedAt = runtimeStatus.lastFailedAt;
|
|
3743
|
+
return record;
|
|
3744
|
+
};
|
|
3745
|
+
collectRuntimeIssues = (record, actions, issues) => {
|
|
3746
|
+
if (record.status === "failed") issues.push({
|
|
3747
|
+
severity: "error",
|
|
3748
|
+
code: "service.runtime.startFailed",
|
|
3749
|
+
message: record.lastError ?? "Service App runtime failed to start."
|
|
3750
|
+
});
|
|
3751
|
+
for (const action of actions) {
|
|
3752
|
+
if (action.runtimeState === "missing") issues.push({
|
|
3753
|
+
severity: "error",
|
|
3754
|
+
code: "service.action.runtimeMissing",
|
|
3755
|
+
message: `Declared action is missing from runtime tools/list: ${action.name}.`
|
|
3756
|
+
});
|
|
3757
|
+
if (action.runtimeState === "undeclared") issues.push({
|
|
3758
|
+
severity: "error",
|
|
3759
|
+
code: "service.action.runtimeUndeclared",
|
|
3760
|
+
message: `Runtime exposes an undeclared action: ${action.name}.`,
|
|
3761
|
+
fixHint: `Add "${action.name}" to service-app.json actions or remove it from the MCP server.`
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3764
|
+
};
|
|
3765
|
+
buildDevReport = (target, app, actions, issues) => ({
|
|
3766
|
+
ok: !issues.some((issue) => issue.severity === "error"),
|
|
3767
|
+
target,
|
|
3768
|
+
app,
|
|
3769
|
+
actions,
|
|
3770
|
+
issues
|
|
3771
|
+
});
|
|
3772
|
+
buildCallReport = (target, app, actionId, result, issues) => ({
|
|
3773
|
+
ok: !issues.some((issue) => issue.severity === "error"),
|
|
3774
|
+
target,
|
|
3775
|
+
actionId,
|
|
3776
|
+
app,
|
|
3777
|
+
result,
|
|
3778
|
+
issues
|
|
3779
|
+
});
|
|
3780
|
+
hasErrors = (issues) => issues.some((issue) => issue.severity === "error");
|
|
3781
|
+
};
|
|
3782
|
+
//#endregion
|
|
3783
|
+
//#region src/cli/app/controllers/app-call-command.controller.ts
|
|
3784
|
+
var AppCallCommandController = class {
|
|
3785
|
+
constructor(serviceAppDevService = new ServiceAppDevService()) {
|
|
3786
|
+
this.serviceAppDevService = serviceAppDevService;
|
|
3787
|
+
}
|
|
3788
|
+
call = async (target, actionName, options) => {
|
|
3789
|
+
const input = this.parseInput(options.input);
|
|
3790
|
+
if (!input.ok) {
|
|
3791
|
+
this.writeInputError(input.message, Boolean(options.json));
|
|
3792
|
+
process.exitCode = 1;
|
|
3793
|
+
return;
|
|
3794
|
+
}
|
|
3795
|
+
const report = await this.serviceAppDevService.call(target, actionName, input.value);
|
|
3796
|
+
process.stdout.write(options.json ? `${JSON.stringify(report, null, 2)}\n` : this.format(report));
|
|
3797
|
+
if (!report.ok) process.exitCode = 1;
|
|
3798
|
+
};
|
|
3799
|
+
parseInput = (raw) => {
|
|
3800
|
+
if (!raw) return {
|
|
3801
|
+
ok: true,
|
|
3802
|
+
value: {}
|
|
3803
|
+
};
|
|
3804
|
+
try {
|
|
3805
|
+
const parsed = JSON.parse(raw);
|
|
3806
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {
|
|
3807
|
+
ok: false,
|
|
3808
|
+
message: "--input must be a JSON object."
|
|
3809
|
+
};
|
|
3810
|
+
return {
|
|
3811
|
+
ok: true,
|
|
3812
|
+
value: parsed
|
|
3813
|
+
};
|
|
3814
|
+
} catch (error) {
|
|
3815
|
+
return {
|
|
3816
|
+
ok: false,
|
|
3817
|
+
message: `--input is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
3818
|
+
};
|
|
3819
|
+
}
|
|
3820
|
+
};
|
|
3821
|
+
writeInputError = (message, json) => {
|
|
3822
|
+
if (json) {
|
|
3823
|
+
process.stdout.write(`${JSON.stringify({
|
|
3824
|
+
ok: false,
|
|
3825
|
+
issues: [{
|
|
3826
|
+
severity: "error",
|
|
3827
|
+
code: "input.invalid",
|
|
3828
|
+
message
|
|
3829
|
+
}]
|
|
3830
|
+
}, null, 2)}\n`);
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
process.stdout.write(`NextClaw service app call failed\n\nErrors:\n- [input.invalid] ${message}\n`);
|
|
3834
|
+
};
|
|
3835
|
+
format = (report) => {
|
|
3836
|
+
return [
|
|
3837
|
+
report.ok ? `NextClaw service app call passed: ${report.actionId ?? "(unknown)"}\n` : `NextClaw service app call failed: ${report.actionId ?? "(unknown)"}\n`,
|
|
3838
|
+
report.app ? [
|
|
3839
|
+
`App: ${report.app.title} (${report.app.id})`,
|
|
3840
|
+
`Status: ${report.app.status}`,
|
|
3841
|
+
report.app.lastError ? `Last error: ${report.app.lastError}` : ""
|
|
3842
|
+
].filter(Boolean).join("\n") : "",
|
|
3843
|
+
report.result === void 0 ? "" : `Result:\n${JSON.stringify(report.result, null, 2)}\n`,
|
|
3844
|
+
this.formatIssueSection("Errors", report.issues.filter((issue) => issue.severity === "error")),
|
|
3845
|
+
this.formatIssueSection("Warnings", report.issues.filter((issue) => issue.severity === "warning"))
|
|
3846
|
+
].filter(Boolean).join("\n");
|
|
3847
|
+
};
|
|
3848
|
+
formatIssueSection = (title, issues) => {
|
|
3849
|
+
if (issues.length === 0) return "";
|
|
3850
|
+
return `${title}:\n${issues.flatMap((issue) => [`- [${issue.code}] ${issue.message}`, issue.fixHint ? ` Fix: ${issue.fixHint}` : ""].filter(Boolean)).join("\n")}\n`;
|
|
3851
|
+
};
|
|
3852
|
+
};
|
|
3853
|
+
//#endregion
|
|
3854
|
+
//#region src/cli/app/utils/app-check.utils.ts
|
|
3855
|
+
const PANEL_MANIFEST_FILE = "panel-app.json";
|
|
3856
|
+
const SERVICE_MANIFEST_FILE = "service-app.json";
|
|
3857
|
+
const VALID_AGENT_CAPABILITIES = new Set(["agent:send", "agent:generateObject"]);
|
|
3858
|
+
const VALID_SERVICE_ACTION_RISKS = new Set([
|
|
3859
|
+
"read",
|
|
3860
|
+
"write",
|
|
3861
|
+
"external",
|
|
3862
|
+
"dangerous"
|
|
3863
|
+
]);
|
|
3864
|
+
const KEBAB_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
3865
|
+
const SERVICE_ACTION_ID_PATTERN = /^([a-z0-9]+(?:-[a-z0-9]+)*)\.(.+)$/;
|
|
3866
|
+
function isRecord(value) {
|
|
3867
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3868
|
+
}
|
|
3869
|
+
function readOptionalString(record, key) {
|
|
3870
|
+
return typeof record[key] === "string" && record[key].trim() ? record[key].trim() : void 0;
|
|
3871
|
+
}
|
|
3872
|
+
function readRequiredString(record, key, prefix) {
|
|
3873
|
+
const value = readOptionalString(record, key);
|
|
3874
|
+
if (!value) return { issue: {
|
|
3875
|
+
severity: "error",
|
|
3876
|
+
code: `${prefix}.${key}.missing`,
|
|
3877
|
+
message: `${prefix}-app.json ${key} is required.`
|
|
3878
|
+
} };
|
|
3879
|
+
return { value };
|
|
3880
|
+
}
|
|
3881
|
+
function getRecommendedStringIssue(record, key, prefix) {
|
|
3882
|
+
if (!readOptionalString(record, key)) return {
|
|
3883
|
+
severity: "warning",
|
|
3884
|
+
code: `${prefix}.${key}.missing`,
|
|
3885
|
+
message: `${prefix}-app.json ${key} is recommended.`
|
|
3886
|
+
};
|
|
3887
|
+
}
|
|
3888
|
+
function readStringArray(value, key) {
|
|
3889
|
+
if (value === void 0) return { values: [] };
|
|
3890
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) return {
|
|
3891
|
+
values: [],
|
|
3892
|
+
issue: {
|
|
3893
|
+
severity: "error",
|
|
3894
|
+
code: `${key}.invalid`,
|
|
3895
|
+
message: `${key} must be a string array.`
|
|
3896
|
+
}
|
|
3897
|
+
};
|
|
3898
|
+
return { values: [...new Set(value.map((entry) => entry.trim()).filter(Boolean))] };
|
|
3899
|
+
}
|
|
3900
|
+
async function getTargetDirectoryIssue(appPath) {
|
|
3901
|
+
try {
|
|
3902
|
+
if ((await stat(appPath)).isDirectory()) return;
|
|
3903
|
+
return {
|
|
3904
|
+
severity: "error",
|
|
3905
|
+
code: "app.target.notDirectory",
|
|
3906
|
+
message: "App check target must be a directory."
|
|
3907
|
+
};
|
|
3908
|
+
} catch {
|
|
3909
|
+
return {
|
|
3910
|
+
severity: "error",
|
|
3911
|
+
code: "app.target.missing",
|
|
3912
|
+
message: "App check target directory does not exist."
|
|
3913
|
+
};
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
async function readJsonObject(filePath) {
|
|
3917
|
+
try {
|
|
3918
|
+
const parsed = JSON.parse(await readFile(filePath, "utf8"));
|
|
3919
|
+
if (isRecord(parsed)) return { value: parsed };
|
|
3920
|
+
return {
|
|
3921
|
+
value: null,
|
|
3922
|
+
issue: {
|
|
3923
|
+
severity: "error",
|
|
3924
|
+
code: "manifest.notObject",
|
|
3925
|
+
message: `${path.basename(filePath)} must contain a JSON object.`
|
|
3926
|
+
}
|
|
3927
|
+
};
|
|
3928
|
+
} catch (error) {
|
|
3929
|
+
return {
|
|
3930
|
+
value: null,
|
|
3931
|
+
issue: {
|
|
3932
|
+
severity: "error",
|
|
3933
|
+
code: "manifest.jsonInvalid",
|
|
3934
|
+
message: `${path.basename(filePath)} is not valid JSON.`,
|
|
3935
|
+
fixHint: error instanceof Error ? error.message : String(error)
|
|
3936
|
+
}
|
|
3937
|
+
};
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
async function fileExists(filePath) {
|
|
3941
|
+
try {
|
|
3942
|
+
await access(filePath, constants.F_OK);
|
|
3943
|
+
return true;
|
|
3944
|
+
} catch {
|
|
3945
|
+
return false;
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
async function getMissingRelativeFileIssue(appPath, relativePath, code, message) {
|
|
3949
|
+
const resolved = resolveRelativeFile(appPath, relativePath);
|
|
3950
|
+
if (!resolved || !await fileExists(resolved)) return {
|
|
3951
|
+
severity: "error",
|
|
3952
|
+
code,
|
|
3953
|
+
message: `${message}: ${relativePath}.`
|
|
3954
|
+
};
|
|
3955
|
+
}
|
|
3956
|
+
function resolveRelativeFile(appPath, relativePath) {
|
|
3957
|
+
if (!isRelativeResource(relativePath)) return null;
|
|
3958
|
+
const resolved = path.resolve(appPath, relativePath);
|
|
3959
|
+
return resolved === appPath || resolved.startsWith(`${appPath}${path.sep}`) ? resolved : null;
|
|
3960
|
+
}
|
|
3961
|
+
function isRelativeResource(value) {
|
|
3962
|
+
return Boolean(value) && !value.startsWith("/") && !/^(?:[a-z]+:|#)/i.test(value);
|
|
3963
|
+
}
|
|
3964
|
+
function extractHtmlAssetPaths(html) {
|
|
3965
|
+
return [...html.matchAll(/<(?:img|link)\b[^>]*(?:src|href)\s*=\s*["']([^"']+)["']/gi)].map((match) => match[1] ?? "").filter(isRelativeResource);
|
|
3966
|
+
}
|
|
3967
|
+
function extractScriptSrcs(html) {
|
|
3968
|
+
return [...html.matchAll(/<script\b[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi)].map((match) => match[1] ?? "");
|
|
3969
|
+
}
|
|
3970
|
+
function inferWorkspaceRoot(appPath, containerName) {
|
|
3971
|
+
const container = path.dirname(appPath);
|
|
3972
|
+
return path.basename(container) === containerName ? path.dirname(container) : null;
|
|
3973
|
+
}
|
|
3974
|
+
function isNodeCommand(command) {
|
|
3975
|
+
const name = path.basename(command).toLowerCase();
|
|
3976
|
+
return name === "node" || name === "node.exe";
|
|
3977
|
+
}
|
|
3978
|
+
//#endregion
|
|
3979
|
+
//#region src/cli/app/services/panel-app-check.service.ts
|
|
3980
|
+
var PanelAppCheckService = class {
|
|
3981
|
+
check = async (appPath) => {
|
|
3982
|
+
const issues = [];
|
|
3983
|
+
this.checkDirectoryName(appPath, issues);
|
|
3984
|
+
const manifestResult = await readJsonObject(path.join(appPath, PANEL_MANIFEST_FILE));
|
|
3985
|
+
this.pushIssue(issues, manifestResult.issue);
|
|
3986
|
+
if (!manifestResult.value) return issues;
|
|
3987
|
+
const entry = this.checkManifestFields(appPath, manifestResult.value, issues);
|
|
3988
|
+
this.checkCapabilities(manifestResult.value.capabilities, issues);
|
|
3989
|
+
const declaredActions = this.checkActions(manifestResult.value.actions, issues);
|
|
3990
|
+
await this.checkWorkspaceServiceActions(appPath, declaredActions, issues);
|
|
3991
|
+
await this.checkEntry(appPath, entry, manifestResult.value, declaredActions, issues);
|
|
3992
|
+
return issues;
|
|
3993
|
+
};
|
|
3994
|
+
checkDirectoryName = (appPath, issues) => {
|
|
3995
|
+
if (!path.basename(appPath).endsWith(".panel")) issues.push({
|
|
3996
|
+
severity: "error",
|
|
3997
|
+
code: "panel.directory.invalid",
|
|
3998
|
+
message: "Panel App directory name must end with .panel.",
|
|
3999
|
+
fixHint: "Rename the directory to <app-id>.panel."
|
|
4000
|
+
});
|
|
4001
|
+
};
|
|
4002
|
+
checkManifestFields = (appPath, manifest, issues) => {
|
|
4003
|
+
const appId = path.basename(appPath).replace(/\.panel$/, "");
|
|
4004
|
+
const id = readOptionalString(manifest, "id");
|
|
4005
|
+
if (id && (!KEBAB_ID_PATTERN.test(id) || id !== appId)) issues.push({
|
|
4006
|
+
severity: "error",
|
|
4007
|
+
code: "panel.id.invalid",
|
|
4008
|
+
message: `panel-app.json id must equal directory app id: ${appId}.`,
|
|
4009
|
+
fixHint: "Remove id or set it to the directory name without .panel."
|
|
4010
|
+
});
|
|
4011
|
+
const title = readRequiredString(manifest, "title", "panel");
|
|
4012
|
+
this.pushIssue(issues, title.issue);
|
|
4013
|
+
this.pushIssue(issues, getRecommendedStringIssue(manifest, "description", "panel"));
|
|
4014
|
+
this.pushIssue(issues, getRecommendedStringIssue(manifest, "icon", "panel"));
|
|
4015
|
+
const entry = readRequiredString(manifest, "entry", "panel");
|
|
4016
|
+
this.pushIssue(issues, entry.issue);
|
|
4017
|
+
return entry.value;
|
|
4018
|
+
};
|
|
4019
|
+
checkCapabilities = (value, issues) => {
|
|
4020
|
+
const capabilities = readStringArray(value, "panel.capabilities");
|
|
4021
|
+
this.pushIssue(issues, capabilities.issue);
|
|
4022
|
+
for (const capability of capabilities.values) {
|
|
4023
|
+
if (VALID_AGENT_CAPABILITIES.has(capability)) continue;
|
|
4024
|
+
issues.push({
|
|
4025
|
+
severity: "error",
|
|
4026
|
+
code: "panel.capability.invalid",
|
|
4027
|
+
message: `Invalid agent capability: ${capability}.`,
|
|
4028
|
+
fixHint: capability.includes(".") ? "Use agent:send or agent:generateObject, with a colon." : "Allowed values are agent:send and agent:generateObject."
|
|
4029
|
+
});
|
|
4030
|
+
}
|
|
4031
|
+
};
|
|
4032
|
+
checkActions = (value, issues) => {
|
|
4033
|
+
const actions = readStringArray(value, "panel.actions");
|
|
4034
|
+
this.pushIssue(issues, actions.issue);
|
|
4035
|
+
for (const action of actions.values) {
|
|
4036
|
+
if (SERVICE_ACTION_ID_PATTERN.test(action)) continue;
|
|
4037
|
+
issues.push({
|
|
4038
|
+
severity: "error",
|
|
4039
|
+
code: "panel.action.invalid",
|
|
4040
|
+
message: `Invalid service action id: ${action}.`,
|
|
4041
|
+
fixHint: "Use <service-app-id>.<tool-name>, for example workspace-files.list."
|
|
4042
|
+
});
|
|
4043
|
+
}
|
|
4044
|
+
return actions.values;
|
|
4045
|
+
};
|
|
4046
|
+
checkWorkspaceServiceActions = async (appPath, actions, issues) => {
|
|
4047
|
+
const workspaceRoot = inferWorkspaceRoot(appPath, "panels");
|
|
4048
|
+
if (!workspaceRoot || actions.length === 0) return;
|
|
4049
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4050
|
+
for (const action of actions) {
|
|
4051
|
+
const match = action.match(SERVICE_ACTION_ID_PATTERN);
|
|
4052
|
+
if (!match) continue;
|
|
4053
|
+
await this.checkWorkspaceServiceAction(workspaceRoot, match[1] ?? "", match[2] ?? "", cache, issues);
|
|
4054
|
+
}
|
|
4055
|
+
};
|
|
4056
|
+
checkWorkspaceServiceAction = async (workspaceRoot, serviceId, actionName, cache, issues) => {
|
|
4057
|
+
const manifest = await this.readCachedServiceManifest(workspaceRoot, serviceId, cache, issues);
|
|
4058
|
+
if (!manifest) return;
|
|
4059
|
+
const actions = manifest.actions;
|
|
4060
|
+
if (!isRecord(actions) || !isRecord(actions[actionName])) issues.push({
|
|
4061
|
+
severity: "error",
|
|
4062
|
+
code: "panel.action.missingServiceAction",
|
|
4063
|
+
message: `Declared service action does not exist: ${serviceId}.${actionName}.`,
|
|
4064
|
+
fixHint: `Add "${actionName}" to service-apps/${serviceId}/service-app.json actions or fix panel-app.json actions.`
|
|
4065
|
+
});
|
|
4066
|
+
};
|
|
4067
|
+
checkEntry = async (appPath, entry, manifest, declaredActions, issues) => {
|
|
4068
|
+
if (!entry) return;
|
|
4069
|
+
const entryPath = resolveRelativeFile(appPath, entry);
|
|
4070
|
+
if (!entryPath) {
|
|
4071
|
+
issues.push({
|
|
4072
|
+
severity: "error",
|
|
4073
|
+
code: "panel.entry.invalid",
|
|
4074
|
+
message: `Panel entry must be a relative file inside the app directory: ${entry}.`
|
|
4075
|
+
});
|
|
4076
|
+
return;
|
|
4077
|
+
}
|
|
4078
|
+
if (!await fileExists(entryPath)) {
|
|
4079
|
+
issues.push({
|
|
4080
|
+
severity: "error",
|
|
4081
|
+
code: "panel.entry.missing",
|
|
4082
|
+
message: `Panel entry file does not exist: ${entry}.`
|
|
4083
|
+
});
|
|
4084
|
+
return;
|
|
4085
|
+
}
|
|
4086
|
+
await this.checkEntryContent(appPath, entryPath, manifest, declaredActions, issues);
|
|
4087
|
+
};
|
|
4088
|
+
checkEntryContent = async (appPath, entryPath, manifest, declaredActions, issues) => {
|
|
4089
|
+
const html = await readFile(entryPath, "utf8");
|
|
4090
|
+
this.checkLegacyMeta(html, issues);
|
|
4091
|
+
await this.checkIcon(appPath, manifest.icon, issues);
|
|
4092
|
+
await this.checkRelativeAssets(appPath, html, issues);
|
|
4093
|
+
const scripts = await this.readReferencedScripts(appPath, html, issues);
|
|
4094
|
+
this.checkBridgeUsage([html, ...scripts].join("\n"), manifest, declaredActions, issues);
|
|
4095
|
+
};
|
|
4096
|
+
checkLegacyMeta = (html, issues) => {
|
|
4097
|
+
if (/name\s*=\s*["']nextclaw-panel-(actions|capabilities)["']/i.test(html)) issues.push({
|
|
4098
|
+
severity: "error",
|
|
4099
|
+
code: "panel.meta.deprecated",
|
|
4100
|
+
message: "Directory Panel Apps must not declare NextClaw actions or capabilities in HTML meta tags.",
|
|
4101
|
+
fixHint: "Move actions and capabilities into panel-app.json."
|
|
4102
|
+
});
|
|
4103
|
+
};
|
|
4104
|
+
checkIcon = async (appPath, icon, issues) => {
|
|
4105
|
+
if (typeof icon !== "string" || !this.isRelativeIconFile(icon)) return;
|
|
4106
|
+
this.pushIssue(issues, await getMissingRelativeFileIssue(appPath, icon, "panel.icon.missing", "Panel icon file does not exist"));
|
|
4107
|
+
};
|
|
4108
|
+
isRelativeIconFile = (icon) => isRelativeResource(icon) && (icon.includes("/") || /\.(?:svg|png|jpe?g|gif|webp|ico)$/i.test(icon));
|
|
4109
|
+
checkRelativeAssets = async (appPath, html, issues) => {
|
|
4110
|
+
for (const asset of extractHtmlAssetPaths(html)) this.pushIssue(issues, await getMissingRelativeFileIssue(appPath, asset, "panel.asset.missing", "Panel asset file does not exist"));
|
|
4111
|
+
};
|
|
4112
|
+
readReferencedScripts = async (appPath, html, issues) => {
|
|
4113
|
+
const scripts = [];
|
|
4114
|
+
for (const src of extractScriptSrcs(html)) {
|
|
4115
|
+
if (!isRelativeResource(src)) continue;
|
|
4116
|
+
const scriptPath = resolveRelativeFile(appPath, src);
|
|
4117
|
+
if (scriptPath && await fileExists(scriptPath)) scripts.push(await readFile(scriptPath, "utf8"));
|
|
4118
|
+
else issues.push({
|
|
4119
|
+
severity: "error",
|
|
4120
|
+
code: "panel.script.missing",
|
|
4121
|
+
message: `Panel script file does not exist: ${src}.`
|
|
4122
|
+
});
|
|
4123
|
+
}
|
|
4124
|
+
return scripts;
|
|
4125
|
+
};
|
|
4126
|
+
checkBridgeUsage = (code, manifest, declaredActions, issues) => {
|
|
4127
|
+
const capabilities = readStringArray(manifest.capabilities, "panel.capabilities");
|
|
4128
|
+
this.checkRequiredCapability(code, "generateObject", "agent:generateObject", capabilities.values, issues);
|
|
4129
|
+
this.checkRequiredCapability(code, "send", "agent:send", capabilities.values, issues);
|
|
4130
|
+
this.checkServiceActionInvocations(code, declaredActions, issues);
|
|
4131
|
+
};
|
|
4132
|
+
checkRequiredCapability = (code, method, capability, declared, issues) => {
|
|
4133
|
+
if (new RegExp(`nextclaw\\s*\\.\\s*agent\\s*\\.\\s*${method}\\b`).test(code) && !declared.includes(capability)) issues.push({
|
|
4134
|
+
severity: "error",
|
|
4135
|
+
code: "panel.capability.missing",
|
|
4136
|
+
message: `Panel code calls window.nextclaw.agent.${method} but panel-app.json does not declare ${capability}.`,
|
|
4137
|
+
fixHint: `Add "${capability}" to panel-app.json capabilities.`
|
|
4138
|
+
});
|
|
4139
|
+
};
|
|
4140
|
+
checkServiceActionInvocations = (code, declaredActions, issues) => {
|
|
4141
|
+
const literalActions = [...code.matchAll(/serviceActions\s*\.\s*invoke\s*\(\s*["']([^"']+)["']/g)].map((match) => match[1] ?? "");
|
|
4142
|
+
if (literalActions.length === 0 && /serviceActions\s*\.\s*invoke\s*\(/.test(code)) issues.push({
|
|
4143
|
+
severity: "warning",
|
|
4144
|
+
code: "panel.action.dynamicInvoke",
|
|
4145
|
+
message: "Panel code calls serviceActions.invoke with a dynamic action id.",
|
|
4146
|
+
fixHint: "Make sure every possible action id is declared in panel-app.json actions."
|
|
4147
|
+
});
|
|
4148
|
+
for (const action of literalActions) if (!declaredActions.includes(action)) issues.push({
|
|
4149
|
+
severity: "error",
|
|
4150
|
+
code: "panel.action.missingDeclaration",
|
|
4151
|
+
message: `Panel code invokes ${action} but panel-app.json actions does not declare it.`,
|
|
4152
|
+
fixHint: `Add "${action}" to panel-app.json actions or update the invoke call.`
|
|
4153
|
+
});
|
|
4154
|
+
};
|
|
4155
|
+
readCachedServiceManifest = async (workspaceRoot, serviceId, cache, issues) => {
|
|
4156
|
+
if (cache.has(serviceId)) return cache.get(serviceId) ?? null;
|
|
4157
|
+
const manifestPath = path.join(workspaceRoot, "service-apps", serviceId, SERVICE_MANIFEST_FILE);
|
|
4158
|
+
if (!await fileExists(manifestPath)) {
|
|
4159
|
+
issues.push({
|
|
4160
|
+
severity: "error",
|
|
4161
|
+
code: "panel.action.serviceMissing",
|
|
4162
|
+
message: `Declared service app does not exist: ${serviceId}.`,
|
|
4163
|
+
fixHint: `Create service-apps/${serviceId}/service-app.json or remove actions for ${serviceId}.`
|
|
4164
|
+
});
|
|
4165
|
+
cache.set(serviceId, null);
|
|
4166
|
+
return null;
|
|
4167
|
+
}
|
|
4168
|
+
const manifestResult = await readJsonObject(manifestPath);
|
|
4169
|
+
if (!manifestResult.value) {
|
|
4170
|
+
if (manifestResult.issue) issues.push({
|
|
4171
|
+
...manifestResult.issue,
|
|
4172
|
+
code: `panel.action.${manifestResult.issue.code}`
|
|
4173
|
+
});
|
|
4174
|
+
cache.set(serviceId, null);
|
|
4175
|
+
return null;
|
|
4176
|
+
}
|
|
4177
|
+
cache.set(serviceId, manifestResult.value);
|
|
4178
|
+
return manifestResult.value;
|
|
4179
|
+
};
|
|
4180
|
+
pushIssue = (issues, issue) => {
|
|
4181
|
+
if (issue) issues.push(issue);
|
|
4182
|
+
};
|
|
4183
|
+
};
|
|
4184
|
+
//#endregion
|
|
4185
|
+
//#region src/cli/app/services/service-app-check.service.ts
|
|
4186
|
+
const execFileAsync = promisify(execFile);
|
|
4187
|
+
var ServiceAppCheckService = class {
|
|
4188
|
+
check = async (appPath) => {
|
|
4189
|
+
const issues = [];
|
|
4190
|
+
const manifestResult = await readJsonObject(path.join(appPath, SERVICE_MANIFEST_FILE));
|
|
4191
|
+
this.pushIssue(issues, manifestResult.issue);
|
|
4192
|
+
if (!manifestResult.value) return issues;
|
|
4193
|
+
const serviceId = this.checkManifestFields(appPath, manifestResult.value, issues);
|
|
4194
|
+
const command = readOptionalString(manifestResult.value, "command");
|
|
4195
|
+
const args = readStringArray(manifestResult.value.args, "service.args");
|
|
4196
|
+
this.pushIssue(issues, args.issue);
|
|
4197
|
+
this.checkActions(serviceId, manifestResult.value.actions, issues);
|
|
4198
|
+
await this.checkCommand(appPath, command, args.values, issues);
|
|
4199
|
+
return issues;
|
|
4200
|
+
};
|
|
4201
|
+
checkManifestFields = (appPath, manifest, issues) => {
|
|
4202
|
+
const id = readRequiredString(manifest, "id", "service");
|
|
4203
|
+
this.pushIssue(issues, id.issue);
|
|
4204
|
+
if (id.value && (!KEBAB_ID_PATTERN.test(id.value) || id.value !== path.basename(appPath))) issues.push({
|
|
4205
|
+
severity: "error",
|
|
4206
|
+
code: "service.id.invalid",
|
|
4207
|
+
message: `service-app.json id must be kebab-case and equal directory name: ${path.basename(appPath)}.`
|
|
4208
|
+
});
|
|
4209
|
+
this.pushIssue(issues, readRequiredString(manifest, "title", "service").issue);
|
|
4210
|
+
this.pushIssue(issues, readRequiredString(manifest, "command", "service").issue);
|
|
4211
|
+
if ((readOptionalString(manifest, "protocol") ?? "mcp") !== "mcp") issues.push({
|
|
4212
|
+
severity: "error",
|
|
4213
|
+
code: "service.protocol.invalid",
|
|
4214
|
+
message: "Service App protocol must be mcp."
|
|
4215
|
+
});
|
|
4216
|
+
return id.value;
|
|
4217
|
+
};
|
|
4218
|
+
checkActions = (serviceId, value, issues) => {
|
|
4219
|
+
if (!isRecord(value) || Object.keys(value).length === 0) {
|
|
4220
|
+
issues.push({
|
|
4221
|
+
severity: "error",
|
|
4222
|
+
code: "service.actions.invalid",
|
|
4223
|
+
message: "service-app.json actions must be a non-empty object."
|
|
4224
|
+
});
|
|
4225
|
+
return;
|
|
4226
|
+
}
|
|
4227
|
+
for (const [name, action] of Object.entries(value)) {
|
|
4228
|
+
this.checkActionName(serviceId, name, issues);
|
|
4229
|
+
this.checkActionManifest(name, action, issues);
|
|
4230
|
+
}
|
|
4231
|
+
};
|
|
4232
|
+
checkActionName = (serviceId, name, issues) => {
|
|
4233
|
+
if (!name.trim()) issues.push({
|
|
4234
|
+
severity: "error",
|
|
4235
|
+
code: "service.action.nameEmpty",
|
|
4236
|
+
message: "Service App action name cannot be empty."
|
|
4237
|
+
});
|
|
4238
|
+
if (serviceId && name.startsWith(`${serviceId}.`)) issues.push({
|
|
4239
|
+
severity: "error",
|
|
4240
|
+
code: "service.action.nameContainsServiceId",
|
|
4241
|
+
message: `Service App action key should not include the service id prefix: ${name}.`,
|
|
4242
|
+
fixHint: `Use "${name.slice(serviceId.length + 1)}" as the action key; Panel App actions use "${name}".`
|
|
4243
|
+
});
|
|
4244
|
+
};
|
|
4245
|
+
checkActionManifest = (name, action, issues) => {
|
|
4246
|
+
if (!isRecord(action)) {
|
|
4247
|
+
issues.push({
|
|
4248
|
+
severity: "error",
|
|
4249
|
+
code: "service.action.invalid",
|
|
4250
|
+
message: `Service App action ${name} must be an object.`
|
|
4251
|
+
});
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
const risk = readOptionalString(action, "risk");
|
|
4255
|
+
if (!risk || !VALID_SERVICE_ACTION_RISKS.has(risk)) issues.push({
|
|
4256
|
+
severity: "error",
|
|
4257
|
+
code: "service.action.riskInvalid",
|
|
4258
|
+
message: `Service App action ${name} must declare risk: read, write, external, or dangerous.`
|
|
4259
|
+
});
|
|
4260
|
+
if (action.inputSchema !== void 0 && !isRecord(action.inputSchema)) issues.push({
|
|
4261
|
+
severity: "error",
|
|
4262
|
+
code: "service.action.inputSchemaInvalid",
|
|
4263
|
+
message: `Service App action ${name} inputSchema must be an object.`
|
|
4264
|
+
});
|
|
4265
|
+
};
|
|
4266
|
+
checkCommand = async (appPath, command, args, issues) => {
|
|
4267
|
+
if (!command || !isNodeCommand(command)) return;
|
|
4268
|
+
const script = args.find((arg) => !arg.startsWith("-"));
|
|
4269
|
+
if (!script) {
|
|
4270
|
+
issues.push({
|
|
4271
|
+
severity: "error",
|
|
4272
|
+
code: "service.command.scriptMissing",
|
|
4273
|
+
message: "Node Service App command must point to a script file in args.",
|
|
4274
|
+
fixHint: "Use args like [\"server.mjs\"]."
|
|
4275
|
+
});
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
const scriptPath = resolveRelativeFile(appPath, script);
|
|
4279
|
+
if (!scriptPath || !await fileExists(scriptPath)) {
|
|
4280
|
+
issues.push({
|
|
4281
|
+
severity: "error",
|
|
4282
|
+
code: "service.command.scriptNotFound",
|
|
4283
|
+
message: `Service App script does not exist: ${script}.`
|
|
4284
|
+
});
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
4287
|
+
await this.checkNodeSyntax(scriptPath, issues);
|
|
4288
|
+
};
|
|
4289
|
+
checkNodeSyntax = async (scriptPath, issues) => {
|
|
4290
|
+
if (!/\.(?:cjs|js|mjs)$/i.test(scriptPath)) return;
|
|
4291
|
+
try {
|
|
4292
|
+
await execFileAsync(process.execPath, ["--check", scriptPath], { timeout: 5e3 });
|
|
4293
|
+
} catch (error) {
|
|
4294
|
+
issues.push({
|
|
4295
|
+
severity: "error",
|
|
4296
|
+
code: "service.command.syntaxInvalid",
|
|
4297
|
+
message: `Service App script has a JavaScript syntax error: ${path.basename(scriptPath)}.`,
|
|
4298
|
+
fixHint: this.readExecError(error)
|
|
4299
|
+
});
|
|
4300
|
+
}
|
|
4301
|
+
};
|
|
4302
|
+
readExecError = (error) => {
|
|
4303
|
+
const stderr = isRecord(error) && typeof error.stderr === "string" ? error.stderr.trim() : "";
|
|
4304
|
+
if (stderr) return stderr;
|
|
4305
|
+
return error instanceof Error ? error.message : String(error);
|
|
4306
|
+
};
|
|
4307
|
+
pushIssue = (issues, issue) => {
|
|
4308
|
+
if (issue) issues.push(issue);
|
|
4309
|
+
};
|
|
4310
|
+
};
|
|
4311
|
+
//#endregion
|
|
4312
|
+
//#region src/cli/app/services/app-check.service.ts
|
|
4313
|
+
var AppCheckService = class {
|
|
4314
|
+
constructor(panelAppCheckService = new PanelAppCheckService(), serviceAppCheckService = new ServiceAppCheckService()) {
|
|
4315
|
+
this.panelAppCheckService = panelAppCheckService;
|
|
4316
|
+
this.serviceAppCheckService = serviceAppCheckService;
|
|
4317
|
+
}
|
|
4318
|
+
check = async (target) => {
|
|
4319
|
+
const appPath = path.resolve(target);
|
|
4320
|
+
const issues = [];
|
|
4321
|
+
const directoryIssue = await getTargetDirectoryIssue(appPath);
|
|
4322
|
+
this.pushIssue(issues, directoryIssue);
|
|
4323
|
+
if (directoryIssue) return this.buildReport(appPath, "unknown", issues);
|
|
4324
|
+
const hasPanelManifest = await fileExists(path.join(appPath, PANEL_MANIFEST_FILE));
|
|
4325
|
+
const hasServiceManifest = await fileExists(path.join(appPath, SERVICE_MANIFEST_FILE));
|
|
4326
|
+
if (hasPanelManifest && hasServiceManifest) {
|
|
4327
|
+
issues.push({
|
|
4328
|
+
severity: "error",
|
|
4329
|
+
code: "app.manifest.mixed",
|
|
4330
|
+
message: "App directory contains both panel-app.json and service-app.json.",
|
|
4331
|
+
fixHint: "Keep Panel App and Service App in separate directories."
|
|
4332
|
+
});
|
|
4333
|
+
return this.buildReport(appPath, "mixed", issues);
|
|
4334
|
+
}
|
|
4335
|
+
if (hasPanelManifest) {
|
|
4336
|
+
issues.push(...await this.panelAppCheckService.check(appPath));
|
|
4337
|
+
return this.buildReport(appPath, "panel", issues);
|
|
4338
|
+
}
|
|
4339
|
+
if (hasServiceManifest) {
|
|
4340
|
+
issues.push(...await this.serviceAppCheckService.check(appPath));
|
|
4341
|
+
return this.buildReport(appPath, "service", issues);
|
|
4342
|
+
}
|
|
4343
|
+
issues.push({
|
|
4344
|
+
severity: "error",
|
|
4345
|
+
code: "app.manifest.missing",
|
|
4346
|
+
message: "App directory must contain panel-app.json or service-app.json.",
|
|
4347
|
+
fixHint: "Run this command on a Panel App directory or a Service App directory."
|
|
4348
|
+
});
|
|
4349
|
+
return this.buildReport(appPath, "unknown", issues);
|
|
4350
|
+
};
|
|
4351
|
+
buildReport = (target, kind, issues) => ({
|
|
4352
|
+
ok: !issues.some((issue) => issue.severity === "error"),
|
|
4353
|
+
kind,
|
|
4354
|
+
target,
|
|
4355
|
+
issues
|
|
4356
|
+
});
|
|
4357
|
+
pushIssue = (issues, issue) => {
|
|
4358
|
+
if (issue) issues.push(issue);
|
|
4359
|
+
};
|
|
4360
|
+
};
|
|
4361
|
+
//#endregion
|
|
4362
|
+
//#region src/cli/app/controllers/app-check-command.controller.ts
|
|
4363
|
+
var AppCheckCommandController = class {
|
|
4364
|
+
constructor(appCheckService = new AppCheckService()) {
|
|
4365
|
+
this.appCheckService = appCheckService;
|
|
4366
|
+
}
|
|
4367
|
+
check = async (target, options) => {
|
|
4368
|
+
const report = await this.appCheckService.check(target);
|
|
4369
|
+
const output = options.json ? `${JSON.stringify(report, null, 2)}\n` : this.formatReport(report);
|
|
4370
|
+
process.stdout.write(output);
|
|
4371
|
+
if (!report.ok) process.exitCode = 1;
|
|
4372
|
+
};
|
|
4373
|
+
formatReport = (report) => {
|
|
4374
|
+
const heading = report.ok ? `NextClaw app check passed: ${report.target}\n` : `NextClaw app check failed: ${report.target}\n`;
|
|
4375
|
+
const errors = report.issues.filter((issue) => issue.severity === "error");
|
|
4376
|
+
const warnings = report.issues.filter((issue) => issue.severity === "warning");
|
|
4377
|
+
if (report.issues.length === 0) return `${heading}\nNo issues found.\n`;
|
|
4378
|
+
return [
|
|
4379
|
+
heading,
|
|
4380
|
+
this.formatIssueSection("Errors", errors),
|
|
4381
|
+
this.formatIssueSection("Warnings", warnings)
|
|
4382
|
+
].filter(Boolean).join("\n");
|
|
4383
|
+
};
|
|
4384
|
+
formatIssueSection = (title, issues) => {
|
|
4385
|
+
if (issues.length === 0) return "";
|
|
4386
|
+
return `${title}:\n${issues.flatMap((issue) => [`- [${issue.code}] ${issue.message}`, issue.fixHint ? ` Fix: ${issue.fixHint}` : ""].filter(Boolean)).join("\n")}\n`;
|
|
4387
|
+
};
|
|
4388
|
+
};
|
|
4389
|
+
//#endregion
|
|
4390
|
+
//#region src/cli/app/controllers/app-dev-command.controller.ts
|
|
4391
|
+
var AppDevCommandController = class {
|
|
4392
|
+
constructor(serviceAppDevService = new ServiceAppDevService()) {
|
|
4393
|
+
this.serviceAppDevService = serviceAppDevService;
|
|
4394
|
+
}
|
|
4395
|
+
dev = async (target, options) => {
|
|
4396
|
+
const report = await this.serviceAppDevService.inspect(target);
|
|
4397
|
+
process.stdout.write(options.json ? `${JSON.stringify(report, null, 2)}\n` : this.format(report));
|
|
4398
|
+
if (!report.ok) process.exitCode = 1;
|
|
4399
|
+
};
|
|
4400
|
+
format = (report) => {
|
|
4401
|
+
return [
|
|
4402
|
+
report.ok ? `NextClaw service app dev passed: ${report.target}\n` : `NextClaw service app dev failed: ${report.target}\n`,
|
|
4403
|
+
report.app ? [
|
|
4404
|
+
`App: ${report.app.title} (${report.app.id})`,
|
|
4405
|
+
`Status: ${report.app.status}`,
|
|
4406
|
+
report.app.lastError ? `Last error: ${report.app.lastError}` : ""
|
|
4407
|
+
].filter(Boolean).join("\n") : "",
|
|
4408
|
+
report.actions.length ? `Actions:\n${report.actions.map((action) => `- ${action.id} [${action.runtimeState ?? "declared"}] risk=${action.risk}`).join("\n")}\n` : "Actions: none\n",
|
|
4409
|
+
this.formatIssueSection("Errors", report.issues.filter((issue) => issue.severity === "error")),
|
|
4410
|
+
this.formatIssueSection("Warnings", report.issues.filter((issue) => issue.severity === "warning"))
|
|
4411
|
+
].filter(Boolean).join("\n");
|
|
4412
|
+
};
|
|
4413
|
+
formatIssueSection = (title, issues) => {
|
|
4414
|
+
if (issues.length === 0) return "";
|
|
4415
|
+
return `${title}:\n${issues.flatMap((issue) => [`- [${issue.code}] ${issue.message}`, issue.fixHint ? ` Fix: ${issue.fixHint}` : ""].filter(Boolean)).join("\n")}\n`;
|
|
4416
|
+
};
|
|
4417
|
+
};
|
|
4418
|
+
//#endregion
|
|
4419
|
+
//#region src/cli/app/register-app-commands.ts
|
|
4420
|
+
function registerAppCommands(program) {
|
|
4421
|
+
const app = program.command("app").description("Inspect and validate lightweight NextClaw apps");
|
|
4422
|
+
const appCheck = new AppCheckCommandController();
|
|
4423
|
+
const appDev = new AppDevCommandController();
|
|
4424
|
+
const appCall = new AppCallCommandController();
|
|
4425
|
+
app.command("check <app-dir>").description("Check a Panel App or Service App directory").option("--json", "Output JSON", false).action(async (target, opts) => appCheck.check(target, opts));
|
|
4426
|
+
app.command("dev <service-app-dir>").description("Start a Service App through the real runtime and inspect its actions").option("--json", "Output JSON", false).action(async (target, opts) => appDev.dev(target, opts));
|
|
4427
|
+
app.command("call <service-app-dir> <action-name>").description("Call a Service App action through the real runtime").option("--input <json>", "JSON object input for the action").option("--json", "Output JSON", false).action(async (target, actionName, opts) => appCall.call(target, actionName, opts));
|
|
4428
|
+
}
|
|
4429
|
+
//#endregion
|
|
3622
4430
|
//#region src/cli/app/index.ts
|
|
3623
4431
|
const LOGO = "🤖";
|
|
3624
4432
|
const program = new Command();
|
|
@@ -3650,6 +4458,7 @@ program.command("agent").description("Interact with the agent directly").option(
|
|
|
3650
4458
|
program.command("update").description(`Update the ${APP_NAME} runtime`).option("--check", "Only check for a runtime update", false).option("--download-only", "Download an available runtime update without applying it", false).option("--download", "Alias for --download-only").option("--apply", "Apply the downloaded runtime update", false).option("--channel <channel>", "Update channel (stable or beta)").option("--manifest-url <url>", "Explicit runtime update manifest URL").option("--json", "Output JSON", false).action(async (opts) => runtime.update(opts));
|
|
3651
4459
|
registerSkillsCommands(program, runtime);
|
|
3652
4460
|
registerAgentsCommands(program, runtime);
|
|
4461
|
+
registerAppCommands(program);
|
|
3653
4462
|
const config = program.command("config").description("Manage config values");
|
|
3654
4463
|
config.command("get <path>").description("Get a config value by dot path").option("--json", "Output JSON", false).action((path, opts) => runtime.commands.config.get(path, opts));
|
|
3655
4464
|
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.commands.config.set(path, value, opts));
|