mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 08:52:56 +00:00
* feat(onboarding): recommend plugins and skills from installed apps Scan installed macOS apps during classic onboarding (TCC-free), gather candidates from official catalogs + ClawHub search, let the configured model pick genuine matches, and offer an opt-in multiselect install step. Adds a device.apps node-host command (default-off sharing, Android-parity envelope) so remote gateways can request a paired Mac's inventory, and a wizard.appRecommendations kill switch. Custom setup-inference completions no longer inherit the 32-token verification-probe output cap. * feat(onboarding): recommend apps in guided flow * fix(onboarding): harden app recommendations against ClawHub self-promotion Third-party ClawHub skills are never pre-selected regardless of model tier (publisher-controlled listing text reaches the matcher prompt and could promote itself); their labels now say they install third-party code. Installed-app scans follow symlinked .app bundles. Matcher output stays bounded by the resolved model's own maxTokens budget (documented invariant). * fix(onboarding): key official catalog candidates by resolved plugin id Real catalog entries are package manifests without a top-level id; keying the candidate map and channel/provider classification by entry.id collapsed the whole official catalog into one undefined-keyed entry, so no official plugin or channel was ever recommended. Regression test runs against the bundled catalogs. * fix(onboarding): satisfy lint, types, deadcode, and migration gates Split the guided-onboarding test into a self-contained custodian suite to stay under max-lines. Narrow app-recommendation exports (drop dead node-payload normalizer, unexport internal types/helpers, route candidate tests through the public API), replace map-spread with a helper, unexport device.apps result types, add installedAppsSharing to node-host migration expectations, cast the wizard multiselect mock, and regenerate the docs map. * test(onboarding): register new live test in the shard classifier
77 lines
2.2 KiB
TypeScript
77 lines
2.2 KiB
TypeScript
import { z } from "zod";
|
|
import { scanInstalledApps, type InstalledApp } from "../infra/installed-apps.js";
|
|
|
|
const DEFAULT_LIMIT = 100;
|
|
const MAX_LIMIT = 200;
|
|
|
|
const DeviceAppsParamsSchema = z
|
|
.object({
|
|
query: z.string().trim().min(1).optional(),
|
|
limit: z
|
|
.number()
|
|
.int()
|
|
.transform((value) => Math.min(MAX_LIMIT, Math.max(1, value)))
|
|
.optional(),
|
|
includeSystem: z.boolean().optional(),
|
|
})
|
|
.strict();
|
|
|
|
type DeviceAppsPayload = {
|
|
count: number;
|
|
totalMatched: number;
|
|
truncated: boolean;
|
|
apps: InstalledApp[];
|
|
};
|
|
|
|
type DeviceAppsInvokeResult =
|
|
| { ok: true; payload: DeviceAppsPayload }
|
|
| { ok: false; code: string; message: string };
|
|
|
|
export async function invokeDeviceApps(params: {
|
|
paramsJSON?: string | null;
|
|
sharingEnabled: boolean;
|
|
platform?: NodeJS.Platform;
|
|
scan?: typeof scanInstalledApps;
|
|
}): Promise<DeviceAppsInvokeResult> {
|
|
if (!params.sharingEnabled) {
|
|
return {
|
|
ok: false,
|
|
code: "INSTALLED_APPS_SHARING_DISABLED",
|
|
message: "INSTALLED_APPS_SHARING_DISABLED: enable Installed Apps in node-host settings",
|
|
};
|
|
}
|
|
let request: z.infer<typeof DeviceAppsParamsSchema>;
|
|
try {
|
|
request = DeviceAppsParamsSchema.parse(JSON.parse(params.paramsJSON || "{}"));
|
|
} catch (error) {
|
|
return { ok: false, code: "INVALID_REQUEST", message: String(error) };
|
|
}
|
|
const scan = params.scan ?? scanInstalledApps;
|
|
const inventory = await scan({ platform: params.platform ?? process.platform });
|
|
if (inventory.status === "unsupported") {
|
|
return {
|
|
ok: false,
|
|
code: "UNAVAILABLE",
|
|
message: "UNAVAILABLE: installed application inventory is only available on macOS",
|
|
};
|
|
}
|
|
const query = request.query?.toLocaleLowerCase("en-US");
|
|
const matching = inventory.apps.filter(
|
|
(app) =>
|
|
(request.includeSystem === true || !app.system) &&
|
|
(!query ||
|
|
app.label.toLocaleLowerCase("en-US").includes(query) ||
|
|
app.bundleId?.toLocaleLowerCase("en-US").includes(query)),
|
|
);
|
|
const apps = matching.slice(0, request.limit ?? DEFAULT_LIMIT);
|
|
return {
|
|
ok: true,
|
|
payload: {
|
|
count: apps.length,
|
|
totalMatched: matching.length,
|
|
truncated: matching.length > apps.length,
|
|
apps,
|
|
},
|
|
};
|
|
}
|