FN-8990: clean up plugin reload scratch files
Keep plugin hot-reload cache-busting copies ephemeral and remove legacy tracked artifacts. - Delete tracked plugin reload scratch files. - Remove temporary import copies after successful and failed reloads. - Add cleanup and tracked-artifact regression coverage. Files changed: .changeset/fn-8990-plugin-reload-scratch-cleanup.md | 7 ++ packages/core/src/__tests__/plugin-hot-reload.test.ts | 25 ++++ - packages/core/src/plugins/plugin-loader.ts | 16 ++- plugins/fusion-plugin-droid-runtime/src/.index.reload-1.ts | 84 ---------------- plugins/fusion-plugin-droid-runtime/src/.index.reload-2.ts | 84 ---------------- plugins/fusion-plugin-hermes-runtime/src/.index.reload-1.ts | 108 -------------------- plugins/fusion-plugin-hermes-runtime/src/.index.reload-2.ts | 108 -------------------- plugins/fusion-plugin-hermes-runtime/src/.index.reload-3.ts | 108 -------------------- plugins/fusion-plugin-openclaw-runtime/src/.index.reload-1.ts | 95 ----------------- plugins/fusion-plugin-paperclip-runtime/src/.index.reload-1.ts | 112 --------------------- scripts/__tests__/no-tracked-plugin-reload-artifacts.test.mjs | 27 +++++ 11 files changed, 72 insertions(+), 702 deletions(-) Fusion-Task-Id: FN-8990 Fusion-Task-Lineage: 2221ebaf-b8ab-4a3c-8934-addfa348c98c Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8990-plugin-reload-scratch-cleanup.md
Normal file
7
.changeset/fn-8990-plugin-reload-scratch-cleanup.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Plugin hot reload no longer leaves scratch reload files in plugin folders.
|
||||||
|
category: internal
|
||||||
|
dev: Removes seven tracked artifacts and cleans importPluginModule cache-busting copies after import.
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||||
import { writeFile, mkdir, rm } from "node:fs/promises";
|
import { writeFile, mkdir, rm } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { mkdtempSync } from "node:fs";
|
import { mkdtempSync, readdirSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
import { PluginLoader } from "../plugins/plugin-loader.js";
|
import { PluginLoader } from "../plugins/plugin-loader.js";
|
||||||
@@ -390,6 +390,29 @@ describe("PluginLoader Hot-Reload", () => {
|
|||||||
expect(pluginLoader.getPluginTools()[0].name).toBe("reloaded_tool");
|
expect(pluginLoader.getPluginTools()[0].name).toBe("reloaded_tool");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("removes the cache-busting copy after a successful reload", async () => {
|
||||||
|
const entryPath = await writePluginModule(tmpDir, "index.js", baseManifest);
|
||||||
|
(mockPluginStore as any)._installation.path = entryPath;
|
||||||
|
await pluginLoader.loadPlugin("hot-reload-test");
|
||||||
|
|
||||||
|
await writePluginModule(tmpDir, "index.js", makeManifest({ version: "2.0.0" }));
|
||||||
|
await pluginLoader.reloadPlugin("hot-reload-test");
|
||||||
|
|
||||||
|
expect(pluginLoader.getPlugin("hot-reload-test")?.manifest.version).toBe("2.0.0");
|
||||||
|
expect(readdirSync(tmpDir).filter((entry) => /^\.index\.reload-/.test(entry))).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("removes the cache-busting copy when the reload import fails", async () => {
|
||||||
|
const entryPath = await writePluginModule(tmpDir, "index.js", baseManifest);
|
||||||
|
(mockPluginStore as any)._installation.path = entryPath;
|
||||||
|
await pluginLoader.loadPlugin("hot-reload-test");
|
||||||
|
|
||||||
|
await writeFile(entryPath, 'throw new Error("reload import failure");');
|
||||||
|
|
||||||
|
await expect(pluginLoader.reloadPlugin("hot-reload-test")).rejects.toThrow("reload import failure");
|
||||||
|
expect(readdirSync(tmpDir).filter((entry) => /^\.index\.reload-/.test(entry))).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("should emit plugin:reloaded event on successful reload", async () => {
|
it("should emit plugin:reloaded event on successful reload", async () => {
|
||||||
await pluginLoader.loadPlugin("hot-reload-test");
|
await pluginLoader.loadPlugin("hot-reload-test");
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
||||||
import { existsSync, readdirSync, statSync } from "node:fs";
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
||||||
import { copyFile, readFile, stat } from "node:fs/promises";
|
import { copyFile, readFile, rm, stat } from "node:fs/promises";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
import { EventEmitter } from "node:events";
|
import { EventEmitter } from "node:events";
|
||||||
import type { TaskStore } from "../store.js";
|
import type { TaskStore } from "../store.js";
|
||||||
@@ -647,7 +647,19 @@ export class PluginLoader extends EventEmitter<{
|
|||||||
const baseName = basename(path, ext);
|
const baseName = basename(path, ext);
|
||||||
const reloadedPath = resolve(dirname(path), `.${baseName}.reload-${moduleImportVersion}${ext}`);
|
const reloadedPath = resolve(dirname(path), `.${baseName}.reload-${moduleImportVersion}${ext}`);
|
||||||
await copyFile(path, reloadedPath);
|
await copyFile(path, reloadedPath);
|
||||||
mod = await import(pathToFileURL(reloadedPath).href);
|
/*
|
||||||
|
FNXC:PluginLoader 2026-08-11-09:58:
|
||||||
|
FN-8990 requires the cache-busting copy to exist only for the dynamic ESM import.
|
||||||
|
The module remains resident after import settles, so remove the copy to prevent
|
||||||
|
plugin working trees accumulating and accidentally committing reload scratch files.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
mod = await import(pathToFileURL(reloadedPath).href);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
await rm(reloadedPath, { force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
mod = await import(moduleUrl);
|
mod = await import(moduleUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
import { definePlugin } from "@fusion/plugin-sdk";
|
|
||||||
import type { FusionPlugin, PluginRuntimeFactory, PluginRuntimeManifestMetadata } from "@fusion/plugin-sdk";
|
|
||||||
import { resolveCliSettings } from "./cli-spawn.js";
|
|
||||||
import { DroidRuntimeAdapter } from "./runtime-adapter.js";
|
|
||||||
|
|
||||||
export const DROID_RUNTIME_ID = "droid";
|
|
||||||
const DROID_RUNTIME_VERSION = "0.1.0";
|
|
||||||
|
|
||||||
export const droidRuntimeMetadata: PluginRuntimeManifestMetadata = {
|
|
||||||
runtimeId: DROID_RUNTIME_ID,
|
|
||||||
name: "Droid Runtime",
|
|
||||||
description: "Drives the Droid CLI for Fusion agents",
|
|
||||||
version: DROID_RUNTIME_VERSION,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const droidRuntimeFactory: PluginRuntimeFactory = async (ctx) =>
|
|
||||||
new DroidRuntimeAdapter(ctx.settings as Record<string, unknown> | undefined);
|
|
||||||
|
|
||||||
const plugin: FusionPlugin = definePlugin({
|
|
||||||
manifest: {
|
|
||||||
id: "fusion-plugin-droid-runtime",
|
|
||||||
name: "Droid Runtime Plugin",
|
|
||||||
version: DROID_RUNTIME_VERSION,
|
|
||||||
description: "Drives the Droid CLI for Fusion agents",
|
|
||||||
runtime: droidRuntimeMetadata,
|
|
||||||
},
|
|
||||||
state: "installed",
|
|
||||||
hooks: {
|
|
||||||
onLoad: (ctx) => {
|
|
||||||
const settings = resolveCliSettings(ctx.settings as Record<string, unknown>);
|
|
||||||
ctx.logger.info(`Droid Runtime Plugin loaded — binary=${settings.binaryPath} model=${settings.model ?? "(default)"}`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
uiSlots: [
|
|
||||||
{
|
|
||||||
slotId: "settings-provider-card",
|
|
||||||
label: "Droid CLI Provider",
|
|
||||||
componentPath: "./components/settings-provider-card.js",
|
|
||||||
order: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slotId: "settings-integration-card",
|
|
||||||
label: "Droid CLI Integration",
|
|
||||||
componentPath: "./components/settings-integration-card.js",
|
|
||||||
order: 20,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slotId: "onboarding-provider-card",
|
|
||||||
label: "Droid CLI Provider",
|
|
||||||
componentPath: "./components/onboarding-provider-card.js",
|
|
||||||
order: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slotId: "onboarding-setup-help",
|
|
||||||
label: "Droid CLI Setup Help",
|
|
||||||
componentPath: "./components/onboarding-setup-help.js",
|
|
||||||
order: 20,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slotId: "post-onboarding-recommendation",
|
|
||||||
label: "Droid CLI Recommendation",
|
|
||||||
componentPath: "./components/post-onboarding-recommendation.js",
|
|
||||||
order: 10,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
runtime: {
|
|
||||||
metadata: droidRuntimeMetadata,
|
|
||||||
factory: droidRuntimeFactory,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default plugin;
|
|
||||||
export { DroidRuntimeAdapter };
|
|
||||||
export { probeDroidBinary } from "./probe.js";
|
|
||||||
export type { DroidBinaryStatus } from "./probe.js";
|
|
||||||
export { streamViaCli } from "./provider.js";
|
|
||||||
export {
|
|
||||||
discoverDroidModels,
|
|
||||||
validateCliPresenceAsync,
|
|
||||||
validateCliAuthAsync,
|
|
||||||
killAllProcesses,
|
|
||||||
} from "./process-manager.js";
|
|
||||||
export { getCustomToolDefs, toolsFromContext, writeMcpConfig } from "./mcp-config.js";
|
|
||||||
export type { McpToolDef } from "./mcp-config.js";
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
import { definePlugin } from "@fusion/plugin-sdk";
|
|
||||||
import type { FusionPlugin, PluginRuntimeFactory, PluginRuntimeManifestMetadata } from "@fusion/plugin-sdk";
|
|
||||||
import { resolveCliSettings } from "./cli-spawn.js";
|
|
||||||
import { DroidRuntimeAdapter } from "./runtime-adapter.js";
|
|
||||||
|
|
||||||
export const DROID_RUNTIME_ID = "droid";
|
|
||||||
const DROID_RUNTIME_VERSION = "0.1.0";
|
|
||||||
|
|
||||||
export const droidRuntimeMetadata: PluginRuntimeManifestMetadata = {
|
|
||||||
runtimeId: DROID_RUNTIME_ID,
|
|
||||||
name: "Droid Runtime",
|
|
||||||
description: "Drives the Droid CLI for Fusion agents",
|
|
||||||
version: DROID_RUNTIME_VERSION,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const droidRuntimeFactory: PluginRuntimeFactory = async (ctx) =>
|
|
||||||
new DroidRuntimeAdapter(ctx.settings as Record<string, unknown> | undefined);
|
|
||||||
|
|
||||||
const plugin: FusionPlugin = definePlugin({
|
|
||||||
manifest: {
|
|
||||||
id: "fusion-plugin-droid-runtime",
|
|
||||||
name: "Droid Runtime Plugin",
|
|
||||||
version: DROID_RUNTIME_VERSION,
|
|
||||||
description: "Drives the Droid CLI for Fusion agents",
|
|
||||||
runtime: droidRuntimeMetadata,
|
|
||||||
},
|
|
||||||
state: "installed",
|
|
||||||
hooks: {
|
|
||||||
onLoad: (ctx) => {
|
|
||||||
const settings = resolveCliSettings(ctx.settings as Record<string, unknown>);
|
|
||||||
ctx.logger.info(`Droid Runtime Plugin loaded — binary=${settings.binaryPath} model=${settings.model ?? "(default)"}`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
uiSlots: [
|
|
||||||
{
|
|
||||||
slotId: "settings-provider-card",
|
|
||||||
label: "Droid CLI Provider",
|
|
||||||
componentPath: "./components/settings-provider-card.js",
|
|
||||||
order: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slotId: "settings-integration-card",
|
|
||||||
label: "Droid CLI Integration",
|
|
||||||
componentPath: "./components/settings-integration-card.js",
|
|
||||||
order: 20,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slotId: "onboarding-provider-card",
|
|
||||||
label: "Droid CLI Provider",
|
|
||||||
componentPath: "./components/onboarding-provider-card.js",
|
|
||||||
order: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slotId: "onboarding-setup-help",
|
|
||||||
label: "Droid CLI Setup Help",
|
|
||||||
componentPath: "./components/onboarding-setup-help.js",
|
|
||||||
order: 20,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slotId: "post-onboarding-recommendation",
|
|
||||||
label: "Droid CLI Recommendation",
|
|
||||||
componentPath: "./components/post-onboarding-recommendation.js",
|
|
||||||
order: 10,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
runtime: {
|
|
||||||
metadata: droidRuntimeMetadata,
|
|
||||||
factory: droidRuntimeFactory,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default plugin;
|
|
||||||
export { DroidRuntimeAdapter };
|
|
||||||
export { probeDroidBinary } from "./probe.js";
|
|
||||||
export type { DroidBinaryStatus } from "./probe.js";
|
|
||||||
export { streamViaCli } from "./provider.js";
|
|
||||||
export {
|
|
||||||
discoverDroidModels,
|
|
||||||
validateCliPresenceAsync,
|
|
||||||
validateCliAuthAsync,
|
|
||||||
killAllProcesses,
|
|
||||||
} from "./process-manager.js";
|
|
||||||
export { getCustomToolDefs, toolsFromContext, writeMcpConfig } from "./mcp-config.js";
|
|
||||||
export type { McpToolDef } from "./mcp-config.js";
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
/**
|
|
||||||
* Hermes Runtime Plugin
|
|
||||||
*
|
|
||||||
* Provides an executable Hermes runtime adapter that drives the local `hermes`
|
|
||||||
* CLI as a subprocess. Discovered by Fusion's plugin runtime registry; the
|
|
||||||
* settings configured in the dashboard's "Runtimes → Hermes" page flow through
|
|
||||||
* `ctx.settings` into the CLI invocation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { definePlugin } from "@fusion/plugin-sdk";
|
|
||||||
import { resolveCliSettings } from "./cli-spawn.js";
|
|
||||||
import { installFusionSkillIntoHermesHome } from "./fusion-skill-install.js";
|
|
||||||
import { HermesRuntimeAdapter } from "./runtime-adapter.js";
|
|
||||||
import type {
|
|
||||||
FusionPlugin,
|
|
||||||
PluginContext,
|
|
||||||
PluginRuntimeFactory,
|
|
||||||
PluginRuntimeManifestMetadata,
|
|
||||||
} from "@fusion/plugin-sdk";
|
|
||||||
|
|
||||||
// ── Hermes Runtime Metadata ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const HERMES_RUNTIME_ID = "hermes";
|
|
||||||
const HERMES_RUNTIME_VERSION = "0.2.0";
|
|
||||||
|
|
||||||
const hermesRuntimeMetadata: PluginRuntimeManifestMetadata = {
|
|
||||||
runtimeId: HERMES_RUNTIME_ID,
|
|
||||||
name: "Hermes Runtime",
|
|
||||||
description: "Drives the local `hermes` CLI (NousResearch/hermes-agent)",
|
|
||||||
version: HERMES_RUNTIME_VERSION,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Hermes Runtime Factory ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const hermesRuntimeFactory: PluginRuntimeFactory = async (ctx) => {
|
|
||||||
return new HermesRuntimeAdapter(ctx.settings as Record<string, unknown> | undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Plugin Definition ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const plugin: FusionPlugin = definePlugin({
|
|
||||||
manifest: {
|
|
||||||
id: "fusion-plugin-hermes-runtime",
|
|
||||||
name: "Hermes Runtime Plugin",
|
|
||||||
version: HERMES_RUNTIME_VERSION,
|
|
||||||
description:
|
|
||||||
"Drives the local `hermes` CLI for Fusion agents — captures session ids and resumes via --resume.",
|
|
||||||
author: "Fusion Team",
|
|
||||||
homepage: "https://github.com/NousResearch/hermes-agent",
|
|
||||||
runtime: hermesRuntimeMetadata,
|
|
||||||
},
|
|
||||||
state: "installed",
|
|
||||||
hooks: {
|
|
||||||
onLoad: (ctx: PluginContext) => {
|
|
||||||
const settings = resolveCliSettings(ctx.settings);
|
|
||||||
const skillInstall = installFusionSkillIntoHermesHome({ profile: settings.profile });
|
|
||||||
|
|
||||||
if (skillInstall.outcome === "warning") {
|
|
||||||
ctx.logger.warn(
|
|
||||||
`Hermes Runtime Plugin: Fusion skill auto-install warning: ${skillInstall.reason ?? "unknown"}`,
|
|
||||||
);
|
|
||||||
} else if (skillInstall.outcome === "skipped") {
|
|
||||||
ctx.logger.warn(
|
|
||||||
`Hermes Runtime Plugin: Fusion skill auto-install skipped: ${skillInstall.reason ?? "unknown"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.logger.info(
|
|
||||||
`Hermes Runtime Plugin loaded — binary=${settings.binaryPath} model=${settings.model ?? "(default)"} fusionSkill=${skillInstall.outcome}`,
|
|
||||||
);
|
|
||||||
ctx.emitEvent("hermes-runtime:loaded", {
|
|
||||||
runtimeId: HERMES_RUNTIME_ID,
|
|
||||||
version: HERMES_RUNTIME_VERSION,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onUnload: () => {
|
|
||||||
// No persistent state to clean up — each prompt spawns a fresh subprocess.
|
|
||||||
},
|
|
||||||
},
|
|
||||||
runtime: {
|
|
||||||
metadata: hermesRuntimeMetadata,
|
|
||||||
factory: hermesRuntimeFactory,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default plugin;
|
|
||||||
|
|
||||||
// ── Public exports ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export { hermesRuntimeMetadata, hermesRuntimeFactory, HERMES_RUNTIME_ID };
|
|
||||||
export { HermesRuntimeAdapter } from "./runtime-adapter.js";
|
|
||||||
export {
|
|
||||||
resolveCliSettings,
|
|
||||||
invokeHermesCli,
|
|
||||||
buildHermesArgs,
|
|
||||||
parseHermesOutput,
|
|
||||||
listHermesProfiles,
|
|
||||||
} from "./cli-spawn.js";
|
|
||||||
export {
|
|
||||||
installFusionSkillIntoHermesHome,
|
|
||||||
resolveBundledFusionSkillSource,
|
|
||||||
resolveHermesHome,
|
|
||||||
} from "./fusion-skill-install.js";
|
|
||||||
export type { HermesCliSettings, HermesCliResult, HermesProfileSummary } from "./cli-spawn.js";
|
|
||||||
|
|
||||||
// Probe re-export for the dashboard's runtime-provider-probes façade.
|
|
||||||
export { probeHermesBinary } from "./probe.js";
|
|
||||||
export type { HermesBinaryStatus } from "./probe.js";
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
/**
|
|
||||||
* Hermes Runtime Plugin
|
|
||||||
*
|
|
||||||
* Provides an executable Hermes runtime adapter that drives the local `hermes`
|
|
||||||
* CLI as a subprocess. Discovered by Fusion's plugin runtime registry; the
|
|
||||||
* settings configured in the dashboard's "Runtimes → Hermes" page flow through
|
|
||||||
* `ctx.settings` into the CLI invocation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { definePlugin } from "@fusion/plugin-sdk";
|
|
||||||
import { resolveCliSettings } from "./cli-spawn.js";
|
|
||||||
import { installFusionSkillIntoHermesHome } from "./fusion-skill-install.js";
|
|
||||||
import { HermesRuntimeAdapter } from "./runtime-adapter.js";
|
|
||||||
import type {
|
|
||||||
FusionPlugin,
|
|
||||||
PluginContext,
|
|
||||||
PluginRuntimeFactory,
|
|
||||||
PluginRuntimeManifestMetadata,
|
|
||||||
} from "@fusion/plugin-sdk";
|
|
||||||
|
|
||||||
// ── Hermes Runtime Metadata ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const HERMES_RUNTIME_ID = "hermes";
|
|
||||||
const HERMES_RUNTIME_VERSION = "0.2.0";
|
|
||||||
|
|
||||||
const hermesRuntimeMetadata: PluginRuntimeManifestMetadata = {
|
|
||||||
runtimeId: HERMES_RUNTIME_ID,
|
|
||||||
name: "Hermes Runtime",
|
|
||||||
description: "Drives the local `hermes` CLI (NousResearch/hermes-agent)",
|
|
||||||
version: HERMES_RUNTIME_VERSION,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Hermes Runtime Factory ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const hermesRuntimeFactory: PluginRuntimeFactory = async (ctx) => {
|
|
||||||
return new HermesRuntimeAdapter(ctx.settings as Record<string, unknown> | undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Plugin Definition ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const plugin: FusionPlugin = definePlugin({
|
|
||||||
manifest: {
|
|
||||||
id: "fusion-plugin-hermes-runtime",
|
|
||||||
name: "Hermes Runtime Plugin",
|
|
||||||
version: HERMES_RUNTIME_VERSION,
|
|
||||||
description:
|
|
||||||
"Drives the local `hermes` CLI for Fusion agents — captures session ids and resumes via --resume.",
|
|
||||||
author: "Fusion Team",
|
|
||||||
homepage: "https://github.com/NousResearch/hermes-agent",
|
|
||||||
runtime: hermesRuntimeMetadata,
|
|
||||||
},
|
|
||||||
state: "installed",
|
|
||||||
hooks: {
|
|
||||||
onLoad: (ctx: PluginContext) => {
|
|
||||||
const settings = resolveCliSettings(ctx.settings);
|
|
||||||
const skillInstall = installFusionSkillIntoHermesHome({ profile: settings.profile });
|
|
||||||
|
|
||||||
if (skillInstall.outcome === "warning") {
|
|
||||||
ctx.logger.warn(
|
|
||||||
`Hermes Runtime Plugin: Fusion skill auto-install warning: ${skillInstall.reason ?? "unknown"}`,
|
|
||||||
);
|
|
||||||
} else if (skillInstall.outcome === "skipped") {
|
|
||||||
ctx.logger.warn(
|
|
||||||
`Hermes Runtime Plugin: Fusion skill auto-install skipped: ${skillInstall.reason ?? "unknown"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.logger.info(
|
|
||||||
`Hermes Runtime Plugin loaded — binary=${settings.binaryPath} model=${settings.model ?? "(default)"} fusionSkill=${skillInstall.outcome}`,
|
|
||||||
);
|
|
||||||
ctx.emitEvent("hermes-runtime:loaded", {
|
|
||||||
runtimeId: HERMES_RUNTIME_ID,
|
|
||||||
version: HERMES_RUNTIME_VERSION,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onUnload: () => {
|
|
||||||
// No persistent state to clean up — each prompt spawns a fresh subprocess.
|
|
||||||
},
|
|
||||||
},
|
|
||||||
runtime: {
|
|
||||||
metadata: hermesRuntimeMetadata,
|
|
||||||
factory: hermesRuntimeFactory,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default plugin;
|
|
||||||
|
|
||||||
// ── Public exports ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export { hermesRuntimeMetadata, hermesRuntimeFactory, HERMES_RUNTIME_ID };
|
|
||||||
export { HermesRuntimeAdapter } from "./runtime-adapter.js";
|
|
||||||
export {
|
|
||||||
resolveCliSettings,
|
|
||||||
invokeHermesCli,
|
|
||||||
buildHermesArgs,
|
|
||||||
parseHermesOutput,
|
|
||||||
listHermesProfiles,
|
|
||||||
} from "./cli-spawn.js";
|
|
||||||
export {
|
|
||||||
installFusionSkillIntoHermesHome,
|
|
||||||
resolveBundledFusionSkillSource,
|
|
||||||
resolveHermesHome,
|
|
||||||
} from "./fusion-skill-install.js";
|
|
||||||
export type { HermesCliSettings, HermesCliResult, HermesProfileSummary } from "./cli-spawn.js";
|
|
||||||
|
|
||||||
// Probe re-export for the dashboard's runtime-provider-probes façade.
|
|
||||||
export { probeHermesBinary } from "./probe.js";
|
|
||||||
export type { HermesBinaryStatus } from "./probe.js";
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
/**
|
|
||||||
* Hermes Runtime Plugin
|
|
||||||
*
|
|
||||||
* Provides an executable Hermes runtime adapter that drives the local `hermes`
|
|
||||||
* CLI as a subprocess. Discovered by Fusion's plugin runtime registry; the
|
|
||||||
* settings configured in the dashboard's "Runtimes → Hermes" page flow through
|
|
||||||
* `ctx.settings` into the CLI invocation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { definePlugin } from "@fusion/plugin-sdk";
|
|
||||||
import { resolveCliSettings } from "./cli-spawn.js";
|
|
||||||
import { installFusionSkillIntoHermesHome } from "./fusion-skill-install.js";
|
|
||||||
import { HermesRuntimeAdapter } from "./runtime-adapter.js";
|
|
||||||
import type {
|
|
||||||
FusionPlugin,
|
|
||||||
PluginContext,
|
|
||||||
PluginRuntimeFactory,
|
|
||||||
PluginRuntimeManifestMetadata,
|
|
||||||
} from "@fusion/plugin-sdk";
|
|
||||||
|
|
||||||
// ── Hermes Runtime Metadata ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const HERMES_RUNTIME_ID = "hermes";
|
|
||||||
const HERMES_RUNTIME_VERSION = "0.2.0";
|
|
||||||
|
|
||||||
const hermesRuntimeMetadata: PluginRuntimeManifestMetadata = {
|
|
||||||
runtimeId: HERMES_RUNTIME_ID,
|
|
||||||
name: "Hermes Runtime",
|
|
||||||
description: "Drives the local `hermes` CLI (NousResearch/hermes-agent)",
|
|
||||||
version: HERMES_RUNTIME_VERSION,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Hermes Runtime Factory ────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const hermesRuntimeFactory: PluginRuntimeFactory = async (ctx) => {
|
|
||||||
return new HermesRuntimeAdapter(ctx.settings as Record<string, unknown> | undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Plugin Definition ─────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const plugin: FusionPlugin = definePlugin({
|
|
||||||
manifest: {
|
|
||||||
id: "fusion-plugin-hermes-runtime",
|
|
||||||
name: "Hermes Runtime Plugin",
|
|
||||||
version: HERMES_RUNTIME_VERSION,
|
|
||||||
description:
|
|
||||||
"Drives the local `hermes` CLI for Fusion agents — captures session ids and resumes via --resume.",
|
|
||||||
author: "Fusion Team",
|
|
||||||
homepage: "https://github.com/NousResearch/hermes-agent",
|
|
||||||
runtime: hermesRuntimeMetadata,
|
|
||||||
},
|
|
||||||
state: "installed",
|
|
||||||
hooks: {
|
|
||||||
onLoad: (ctx: PluginContext) => {
|
|
||||||
const settings = resolveCliSettings(ctx.settings);
|
|
||||||
const skillInstall = installFusionSkillIntoHermesHome({ profile: settings.profile });
|
|
||||||
|
|
||||||
if (skillInstall.outcome === "warning") {
|
|
||||||
ctx.logger.warn(
|
|
||||||
`Hermes Runtime Plugin: Fusion skill auto-install warning: ${skillInstall.reason ?? "unknown"}`,
|
|
||||||
);
|
|
||||||
} else if (skillInstall.outcome === "skipped") {
|
|
||||||
ctx.logger.warn(
|
|
||||||
`Hermes Runtime Plugin: Fusion skill auto-install skipped: ${skillInstall.reason ?? "unknown"}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.logger.info(
|
|
||||||
`Hermes Runtime Plugin loaded — binary=${settings.binaryPath} model=${settings.model ?? "(default)"} fusionSkill=${skillInstall.outcome}`,
|
|
||||||
);
|
|
||||||
ctx.emitEvent("hermes-runtime:loaded", {
|
|
||||||
runtimeId: HERMES_RUNTIME_ID,
|
|
||||||
version: HERMES_RUNTIME_VERSION,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onUnload: () => {
|
|
||||||
// No persistent state to clean up — each prompt spawns a fresh subprocess.
|
|
||||||
},
|
|
||||||
},
|
|
||||||
runtime: {
|
|
||||||
metadata: hermesRuntimeMetadata,
|
|
||||||
factory: hermesRuntimeFactory,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default plugin;
|
|
||||||
|
|
||||||
// ── Public exports ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export { hermesRuntimeMetadata, hermesRuntimeFactory, HERMES_RUNTIME_ID };
|
|
||||||
export { HermesRuntimeAdapter } from "./runtime-adapter.js";
|
|
||||||
export {
|
|
||||||
resolveCliSettings,
|
|
||||||
invokeHermesCli,
|
|
||||||
buildHermesArgs,
|
|
||||||
parseHermesOutput,
|
|
||||||
listHermesProfiles,
|
|
||||||
} from "./cli-spawn.js";
|
|
||||||
export {
|
|
||||||
installFusionSkillIntoHermesHome,
|
|
||||||
resolveBundledFusionSkillSource,
|
|
||||||
resolveHermesHome,
|
|
||||||
} from "./fusion-skill-install.js";
|
|
||||||
export type { HermesCliSettings, HermesCliResult, HermesProfileSummary } from "./cli-spawn.js";
|
|
||||||
|
|
||||||
// Probe re-export for the dashboard's runtime-provider-probes façade.
|
|
||||||
export { probeHermesBinary } from "./probe.js";
|
|
||||||
export type { HermesBinaryStatus } from "./probe.js";
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
/**
|
|
||||||
* OpenClaw Runtime Plugin
|
|
||||||
*
|
|
||||||
* Drives the local `openclaw` CLI as a subprocess (via
|
|
||||||
* `openclaw --no-color agent --local --json`). No daemon required.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { definePlugin } from "@fusion/plugin-sdk";
|
|
||||||
import { OpenClawRuntimeAdapter } from "./runtime-adapter.js";
|
|
||||||
import { resolveCliConfig } from "./pi-module.js";
|
|
||||||
import { probeOpenClawBinary } from "./probe.js";
|
|
||||||
import type {
|
|
||||||
FusionPlugin,
|
|
||||||
PluginContext,
|
|
||||||
PluginRuntimeFactory,
|
|
||||||
PluginRuntimeManifestMetadata,
|
|
||||||
} from "@fusion/plugin-sdk";
|
|
||||||
|
|
||||||
const OPENCLAW_RUNTIME_ID = "openclaw";
|
|
||||||
const OPENCLAW_RUNTIME_VERSION = "0.2.0";
|
|
||||||
|
|
||||||
const openclawRuntimeMetadata: PluginRuntimeManifestMetadata = {
|
|
||||||
runtimeId: OPENCLAW_RUNTIME_ID,
|
|
||||||
name: "OpenClaw Runtime",
|
|
||||||
description: "Drives the local `openclaw` CLI (openclaw/openclaw)",
|
|
||||||
version: OPENCLAW_RUNTIME_VERSION,
|
|
||||||
};
|
|
||||||
|
|
||||||
const openclawRuntimeFactory: PluginRuntimeFactory = async (ctx?: PluginContext) => {
|
|
||||||
return new OpenClawRuntimeAdapter(ctx?.settings as Record<string, unknown> | undefined);
|
|
||||||
};
|
|
||||||
|
|
||||||
const plugin: FusionPlugin = definePlugin({
|
|
||||||
manifest: {
|
|
||||||
id: "fusion-plugin-openclaw-runtime",
|
|
||||||
name: "OpenClaw Runtime Plugin",
|
|
||||||
version: OPENCLAW_RUNTIME_VERSION,
|
|
||||||
description:
|
|
||||||
"Drives the local `openclaw` CLI for Fusion agents — embedded `--local` mode by default; gateway optional.",
|
|
||||||
author: "Fusion Team",
|
|
||||||
homepage: "https://docs.openclaw.ai/",
|
|
||||||
runtime: openclawRuntimeMetadata,
|
|
||||||
},
|
|
||||||
state: "installed",
|
|
||||||
hooks: {
|
|
||||||
onLoad: async (ctx: PluginContext) => {
|
|
||||||
const config = resolveCliConfig(ctx.settings);
|
|
||||||
const probe = await probeOpenClawBinary({ binaryPath: config.binaryPath });
|
|
||||||
|
|
||||||
ctx.logger.info(
|
|
||||||
probe.available
|
|
||||||
? `OpenClaw Runtime Plugin loaded — binary=${config.binaryPath}${probe.version ? ` (${probe.version})` : ""}`
|
|
||||||
: `OpenClaw Runtime Plugin loaded but binary not detected: ${probe.reason ?? "unknown"}`,
|
|
||||||
);
|
|
||||||
ctx.emitEvent("openclaw-runtime:loaded", {
|
|
||||||
runtimeId: OPENCLAW_RUNTIME_ID,
|
|
||||||
version: OPENCLAW_RUNTIME_VERSION,
|
|
||||||
binaryAvailable: probe.available,
|
|
||||||
binaryPath: probe.binaryPath ?? config.binaryPath,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onUnload: () => {
|
|
||||||
// No persistent state to clean up — each prompt spawns a fresh subprocess.
|
|
||||||
},
|
|
||||||
},
|
|
||||||
runtime: {
|
|
||||||
metadata: openclawRuntimeMetadata,
|
|
||||||
factory: openclawRuntimeFactory,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default plugin;
|
|
||||||
|
|
||||||
// ── Public exports ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export { openclawRuntimeMetadata, openclawRuntimeFactory, OPENCLAW_RUNTIME_ID };
|
|
||||||
export { OpenClawRuntimeAdapter } from "./runtime-adapter.js";
|
|
||||||
export {
|
|
||||||
resolveCliConfig,
|
|
||||||
buildOpenClawArgs,
|
|
||||||
createCliSession,
|
|
||||||
promptCli,
|
|
||||||
describeCliModel,
|
|
||||||
extractStderrError,
|
|
||||||
configureOpenClawMcpServer,
|
|
||||||
} from "./pi-module.js";
|
|
||||||
export type { CliConfig, GatewaySession, OpenClawAgentJson } from "./types.js";
|
|
||||||
export {
|
|
||||||
toolsToMcpToolDefs,
|
|
||||||
writeOpenClawMcpBridgeFiles,
|
|
||||||
} from "./mcp-config.js";
|
|
||||||
|
|
||||||
// Probe re-export for the dashboard's runtime-provider-probes façade.
|
|
||||||
export { probeOpenClawBinary } from "./probe.js";
|
|
||||||
export type { OpenClawBinaryStatus } from "./probe.js";
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
import { definePlugin } from "@fusion/plugin-sdk";
|
|
||||||
import type { PluginContext } from "@fusion/plugin-sdk";
|
|
||||||
import {
|
|
||||||
probePaperclipConnection,
|
|
||||||
resolvePaperclipConfig,
|
|
||||||
} from "./paperclip-client.js";
|
|
||||||
import { PaperclipRuntimeAdapter } from "./runtime-adapter.js";
|
|
||||||
import type {
|
|
||||||
FusionPlugin,
|
|
||||||
PluginRuntimeRegistration,
|
|
||||||
RuntimeLogger,
|
|
||||||
} from "./types.js";
|
|
||||||
|
|
||||||
// Public exports — consumed by the dashboard probe façade and tests.
|
|
||||||
export type {
|
|
||||||
PaperclipAgentSummary,
|
|
||||||
PaperclipCliDiscovery,
|
|
||||||
PaperclipCliDiscoveryResult,
|
|
||||||
PaperclipCompanySummary,
|
|
||||||
PaperclipConnectionStatus,
|
|
||||||
} from "./paperclip-client.js";
|
|
||||||
export {
|
|
||||||
agentsMe,
|
|
||||||
agentsMeViaCli,
|
|
||||||
createIssueViaCli,
|
|
||||||
discoverPaperclipCliConfig,
|
|
||||||
getIssueViaCli,
|
|
||||||
listCompanies,
|
|
||||||
listCompaniesViaCli,
|
|
||||||
listCompanyAgents,
|
|
||||||
listCompanyAgentsViaCli,
|
|
||||||
mintAgentApiKeyViaCli,
|
|
||||||
probePaperclipConnection,
|
|
||||||
probePaperclipViaCli,
|
|
||||||
} from "./paperclip-client.js";
|
|
||||||
export type { MintCliKeyOptions, MintedApiKey } from "./paperclip-client.js";
|
|
||||||
export { PaperclipRuntimeAdapter } from "./runtime-adapter.js";
|
|
||||||
|
|
||||||
function getSettingsConfig(settings: unknown) {
|
|
||||||
return resolvePaperclipConfig((settings ?? {}) as Record<string, unknown>);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function paperclipRuntimeFactory(ctx: {
|
|
||||||
settings?: unknown;
|
|
||||||
logger?: RuntimeLogger;
|
|
||||||
}): Promise<unknown> {
|
|
||||||
const config = getSettingsConfig(ctx.settings);
|
|
||||||
// resolvePaperclipConfig returns `mode: string`; the adapter narrows it.
|
|
||||||
return new PaperclipRuntimeAdapter(
|
|
||||||
config as unknown as Record<string, unknown>,
|
|
||||||
ctx.logger,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const paperclipRuntime: PluginRuntimeRegistration = {
|
|
||||||
metadata: {
|
|
||||||
runtimeId: "paperclip",
|
|
||||||
name: "Paperclip Runtime",
|
|
||||||
description: "Drives a Paperclip agent via the wakeup + heartbeat-run REST API",
|
|
||||||
version: "1.0.0",
|
|
||||||
},
|
|
||||||
factory: paperclipRuntimeFactory,
|
|
||||||
};
|
|
||||||
|
|
||||||
const plugin: FusionPlugin = definePlugin({
|
|
||||||
manifest: {
|
|
||||||
id: "fusion-plugin-paperclip-runtime",
|
|
||||||
name: "Paperclip Runtime Plugin",
|
|
||||||
version: "1.0.0",
|
|
||||||
description: "Drives a Paperclip agent via the wakeup + heartbeat-run REST API",
|
|
||||||
author: "Fusion Team",
|
|
||||||
homepage: "https://paperclip.ing/",
|
|
||||||
fusionVersion: ">=0.1.0",
|
|
||||||
runtime: {
|
|
||||||
runtimeId: "paperclip",
|
|
||||||
name: "Paperclip Runtime",
|
|
||||||
description: "Drives a Paperclip agent via the wakeup + heartbeat-run REST API",
|
|
||||||
version: "1.0.0",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
state: "installed",
|
|
||||||
runtime: paperclipRuntime,
|
|
||||||
hooks: {
|
|
||||||
onLoad: async (ctx: PluginContext) => {
|
|
||||||
const config = getSettingsConfig(ctx.settings);
|
|
||||||
ctx.logger.info(`Paperclip Runtime Plugin loaded (apiUrl=${config.apiUrl})`);
|
|
||||||
|
|
||||||
// Best-effort connectivity probe; failures are warnings, not errors.
|
|
||||||
try {
|
|
||||||
const status = await probePaperclipConnection({
|
|
||||||
apiUrl: config.apiUrl,
|
|
||||||
apiKey: config.apiKey,
|
|
||||||
});
|
|
||||||
if (status.available) {
|
|
||||||
const ident = status.identity;
|
|
||||||
ctx.logger.info(
|
|
||||||
ident
|
|
||||||
? `Paperclip reachable as ${ident.agentName} (${ident.role ?? "agent"}) at ${ident.companyName ?? ident.companyId}`
|
|
||||||
: `Paperclip reachable at ${config.apiUrl}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ctx.logger.warn(`Paperclip probe failed: ${status.reason ?? "unknown"}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
const reason = error instanceof Error ? error.message : String(error);
|
|
||||||
ctx.logger.warn(`Paperclip probe threw: ${reason}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default plugin;
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
const rootDir = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:PluginLoader 2026-08-11-09:58:
|
||||||
|
FN-8990 requires reload scratch copies to remain untracked. Gitignore cannot
|
||||||
|
remove a path already in git's index, so inspect tracked files directly.
|
||||||
|
*/
|
||||||
|
function getTrackedReloadArtifacts() {
|
||||||
|
return execFileSync("git", ["ls-files"], { cwd: rootDir, encoding: "utf8" })
|
||||||
|
.split("\n")
|
||||||
|
.filter((path) => /\.index\.reload-\d+\.ts$/.test(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
test("plugin hot-reload scratch copies are never tracked", () => {
|
||||||
|
const artifacts = getTrackedReloadArtifacts();
|
||||||
|
assert.deepEqual(
|
||||||
|
artifacts,
|
||||||
|
[],
|
||||||
|
`Tracked plugin hot-reload scratch copies:\n${artifacts.map((path) => `- ${path}`).join("\n")}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user