Files
openclaw/src/commands/node-daemon-install-helpers.ts
Peter Steinberger da69daeb72 feat(onboarding): recommend plugins and skills from installed apps (#109668)
* 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
2026-07-17 14:07:59 +01:00

94 lines
3.0 KiB
TypeScript

/** Node-based daemon install plan builder for managed gateway services. */
import { formatNodeServiceDescription } from "../daemon/constants.js";
import { resolveNodeProgramArguments } from "../daemon/program-args.js";
import { buildNodeServiceEnvironment } from "../daemon/service-env.js";
import type { GatewayServiceEnvironmentValueSource } from "../daemon/service-types.js";
import {
emitDaemonInstallRuntimeWarning,
resolveDaemonInstallRuntimeInputs,
resolveDaemonNodeBinDir,
} from "./daemon-install-plan.shared.js";
import type { DaemonInstallWarnFn } from "./daemon-install-runtime-warning.js";
import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
type NodeInstallPlan = {
programArguments: string[];
workingDirectory?: string;
environment: Record<string, string | undefined>;
environmentValueSources?: Record<string, GatewayServiceEnvironmentValueSource | undefined>;
description?: string;
};
function buildNodeInstallEnvironmentValueSources(): Record<
string,
GatewayServiceEnvironmentValueSource | undefined
> {
return {
OPENCLAW_GATEWAY_TOKEN: "file",
OPENCLAW_GATEWAY_PASSWORD: "file", // pragma: allowlist secret
};
}
/** Builds launch arguments, environment, and metadata for a Node daemon service install. */
export async function buildNodeInstallPlan(params: {
env: Record<string, string | undefined>;
host: string;
port: number;
contextPath?: string;
tls?: boolean;
tlsFingerprint?: string;
nodeId?: string;
displayName?: string;
installedAppsSharing?: boolean;
runtime: GatewayDaemonRuntime;
devMode?: boolean;
nodePath?: string;
warn?: DaemonInstallWarnFn;
}): Promise<NodeInstallPlan> {
const { devMode, nodePath } = await resolveDaemonInstallRuntimeInputs({
env: params.env,
runtime: params.runtime,
devMode: params.devMode,
nodePath: params.nodePath,
});
const { programArguments, workingDirectory } = await resolveNodeProgramArguments({
host: params.host,
port: params.port,
contextPath: params.contextPath,
tls: params.tls,
tlsFingerprint: params.tlsFingerprint,
nodeId: params.nodeId,
displayName: params.displayName,
installedAppsSharing: params.installedAppsSharing,
dev: devMode,
runtime: params.runtime,
nodePath,
});
await emitDaemonInstallRuntimeWarning({
env: params.env,
runtime: params.runtime,
programArguments,
warn: params.warn,
title: "Node daemon runtime",
});
const environment = buildNodeServiceEnvironment({
env: params.env,
// Match the gateway install path so supervised node services keep the chosen
// node toolchain on PATH for sibling binaries like npm/pnpm when needed.
extraPathDirs: resolveDaemonNodeBinDir(nodePath),
});
const description = formatNodeServiceDescription({
version: environment.OPENCLAW_SERVICE_VERSION,
});
return {
programArguments,
workingDirectory,
environment,
environmentValueSources: buildNodeInstallEnvironmentValueSources(),
description,
};
}