mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 04:01:36 +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
86 lines
2.6 KiB
TypeScript
86 lines
2.6 KiB
TypeScript
/** Private JSONL worker exposing the CLI node-host runtime to the macOS app. */
|
|
import { createInterface } from "node:readline";
|
|
import { VERSION } from "../version.js";
|
|
import { loadNodeHostConfig } from "./config.js";
|
|
import { prepareNodeHostRuntime, type NodeHostInventory } from "./runtime.js";
|
|
import {
|
|
NodeHostWorkerBridgeClient,
|
|
parseNodeHostWorkerInput,
|
|
stopNodeHostWorkerFromSignal,
|
|
} from "./worker-support.js";
|
|
|
|
function writeMessage(message: unknown): void {
|
|
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
}
|
|
|
|
function emitInventory(inventory: NodeHostInventory): void {
|
|
writeMessage({ type: "inventory", inventory });
|
|
}
|
|
|
|
export async function runNodeHostWorker(): Promise<void> {
|
|
const nodeConfig = await loadNodeHostConfig();
|
|
const prepared = await prepareNodeHostRuntime({
|
|
enableDuplexPluginCommands: true,
|
|
installedAppsSharingEnabled: nodeConfig?.installedAppsSharing === true,
|
|
});
|
|
const client = new NodeHostWorkerBridgeClient(writeMessage);
|
|
let stopping = false;
|
|
let resolveStopped: (() => void) | undefined;
|
|
const stopped = new Promise<void>((resolve) => {
|
|
resolveStopped = resolve;
|
|
});
|
|
|
|
const stop = async (exitCode: number) => {
|
|
if (stopping) {
|
|
return;
|
|
}
|
|
stopping = true;
|
|
try {
|
|
client.close();
|
|
await runtime.close();
|
|
process.exitCode = exitCode;
|
|
} finally {
|
|
resolveStopped?.();
|
|
}
|
|
};
|
|
|
|
const runtime = prepared.start({ client, onInventoryChanged: emitInventory });
|
|
writeMessage({
|
|
type: "ready",
|
|
version: VERSION,
|
|
manifest: prepared.manifest,
|
|
inventory: prepared.initialInventory,
|
|
});
|
|
|
|
const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
input.on("line", (line) => {
|
|
const message = parseNodeHostWorkerInput(line);
|
|
if (!message) {
|
|
writeMessage({ type: "protocol-error", error: "invalid worker request" });
|
|
return;
|
|
}
|
|
if (message.type === "gateway-response") {
|
|
client.handleResponse(message);
|
|
return;
|
|
}
|
|
if (message.type === "stop") {
|
|
input.close();
|
|
void stop(0);
|
|
return;
|
|
}
|
|
if (message.type === "invoke-input") {
|
|
runtime.handleInput(message.invokeId, message.seq, message.payloadJSON);
|
|
return;
|
|
}
|
|
if (message.type === "invoke-cancel") {
|
|
runtime.cancel(message.invokeId);
|
|
return;
|
|
}
|
|
void runtime.invoke(message.request);
|
|
});
|
|
input.on("close", () => void stop(0));
|
|
process.once("SIGINT", () => void stopNodeHostWorkerFromSignal(input, stop, 130));
|
|
process.once("SIGTERM", () => void stopNodeHostWorkerFromSignal(input, stop, 143));
|
|
await stopped;
|
|
}
|