diff --git a/.gitignore b/.gitignore index b947077..5d2e072 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ node_modules/ dist/ +plugin/**/*.js +plugin/**/*.js.map +!routers/*.js diff --git a/package.json b/package.json index b7e9960..d17b3bc 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,10 @@ "name": "prism-facet", "version": "0.1.0", "type": "module", - "main": "dist/index.js", "scripts": { - "build": "tsc", - "watch": "tsc --watch" + "build": "tsc --project tsconfig.plugin.json", + "install-plugin": "node scripts/install.mjs --install", + "uninstall-plugin": "node scripts/install.mjs --uninstall" }, "devDependencies": { "@types/node": "^25.6.0", diff --git a/src/prompt-injector.ts b/plugin/core/prompt-injector.ts similarity index 57% rename from src/prompt-injector.ts rename to plugin/core/prompt-injector.ts index f92ab68..d235c2b 100644 --- a/src/prompt-injector.ts +++ b/plugin/core/prompt-injector.ts @@ -1,7 +1,7 @@ import { readFileSync } from "node:fs"; import { getRouters } from "./router-loader.js"; -import { getRulesForRouter } from "./rule-store.js"; -import type { RouterContext } from "./types.js"; +import { getRule } from "./rule-store.js"; +import type { RouterContext } from "./router-loader.js"; export interface InjectionResult { appendSystemContext?: string; @@ -9,7 +9,7 @@ export interface InjectionResult { export async function resolveInjection( ctx: RouterContext, - logger: { info: (msg: string) => void; warn: (msg: string) => void } + log: { info(msg: string): void; warn(msg: string): void } ): Promise { const routers = getRouters(); const segments: string[] = []; @@ -19,26 +19,23 @@ export async function resolveInjection( const key = await router.module.resolve(ctx); if (!key) continue; - const promptFile = getRulesForRouter(router.name, key); + const promptFile = getRule(router.name, key); if (!promptFile) continue; try { const content = readFileSync(promptFile, "utf8").trim(); if (content) { segments.push(content); - logger.info(`injecting ${router.name}:${key} → ${promptFile}`); + log.info(`[prism-facet] injecting ${router.name}:${key} → ${promptFile}`); } } catch (err) { - logger.warn(`failed to read prompt file ${promptFile}: ${String(err)}`); + log.warn(`[prism-facet] cannot read ${promptFile}: ${String(err)}`); } } catch (err) { - logger.warn(`router ${router.name} failed: ${String(err)}`); + log.warn(`[prism-facet] router ${router.name} failed: ${String(err)}`); } } if (segments.length === 0) return {}; - - return { - appendSystemContext: segments.join("\n\n---\n\n"), - }; + return { appendSystemContext: segments.join("\n\n---\n\n") }; } diff --git a/plugin/core/router-loader.ts b/plugin/core/router-loader.ts new file mode 100644 index 0000000..a7bf157 --- /dev/null +++ b/plugin/core/router-loader.ts @@ -0,0 +1,77 @@ +import { readdirSync } from "node:fs"; +import path from "node:path"; + +export interface RouterContext { + agentId: string; +} + +export interface RouterModule { + resolve(ctx: RouterContext): string | Promise; +} + +export interface LoadedRouter { + name: string; + filePath: string; + module: RouterModule; +} + +const _G = globalThis as Record; +const ROUTERS_KEY = "_prismFacetRouters"; +const LOAD_COUNTER_KEY = "_prismFacetLoadCounter"; + +function getRouterMap(): Map { + if (!(_G[ROUTERS_KEY] instanceof Map)) { + _G[ROUTERS_KEY] = new Map(); + } + return _G[ROUTERS_KEY] as Map; +} + +function routerNameFromFile(filename: string): string { + return filename.replace(/\.(ts|js|mjs)$/, ""); +} + +export async function loadRouters( + routersDir: string, + log: { info(msg: string): void; warn(msg: string): void } +): Promise { + const map = getRouterMap(); + map.clear(); + + if (typeof _G[LOAD_COUNTER_KEY] !== "number") _G[LOAD_COUNTER_KEY] = 0; + (_G[LOAD_COUNTER_KEY] as number)++; + const counter = _G[LOAD_COUNTER_KEY] as number; + + let files: string[]; + try { + files = readdirSync(routersDir).filter( + (f) => /\.(ts|js|mjs)$/.test(f) && !f.startsWith(".") + ); + } catch { + log.warn(`[prism-facet] routers directory not found: ${routersDir}`); + return; + } + + for (const file of files) { + const name = routerNameFromFile(file); + const filePath = path.resolve(routersDir, file); + try { + const mod = (await import(`${filePath}?v=${counter}`)) as RouterModule; + if (typeof mod.resolve !== "function") { + log.warn(`[prism-facet] router ${name}: no resolve() export, skipping`); + continue; + } + map.set(name, { name, filePath, module: mod }); + log.info(`[prism-facet] router loaded: ${name}`); + } catch (err) { + log.warn(`[prism-facet] router ${name}: failed to load — ${String(err)}`); + } + } +} + +export function getRouters(): LoadedRouter[] { + return Array.from(getRouterMap().values()); +} + +export function getRouterNames(): string[] { + return Array.from(getRouterMap().keys()); +} diff --git a/plugin/core/rule-store.ts b/plugin/core/rule-store.ts new file mode 100644 index 0000000..1e1bb9b --- /dev/null +++ b/plugin/core/rule-store.ts @@ -0,0 +1,56 @@ +import { readFileSync, writeFileSync } from "node:fs"; + +export type RuleStore = Record; // "router:key" → prompt file path + +const _G = globalThis as Record; +const RULES_KEY = "_prismFacetRules"; +const RULES_PATH_KEY = "_prismFacetRulesPath"; + +function getRules(): RuleStore { + if (!_G[RULES_KEY] || typeof _G[RULES_KEY] !== "object") { + _G[RULES_KEY] = {}; + } + return _G[RULES_KEY] as RuleStore; +} + +function getRulesPath(): string { + return (_G[RULES_PATH_KEY] as string) || ""; +} + +function save(): void { + const p = getRulesPath(); + if (!p) return; + writeFileSync(p, JSON.stringify(getRules(), null, 2) + "\n", "utf8"); +} + +export function initRuleStore(filePath: string): void { + _G[RULES_PATH_KEY] = filePath; + try { + const raw = readFileSync(filePath, "utf8"); + _G[RULES_KEY] = JSON.parse(raw) as RuleStore; + } catch { + _G[RULES_KEY] = {}; + } +} + +export function addRule(router: string, key: string, promptFile: string): void { + getRules()[`${router}:${key}`] = promptFile; + save(); +} + +export function removeRule(router: string, key: string): boolean { + const rules = getRules(); + const ruleKey = `${router}:${key}`; + if (!(ruleKey in rules)) return false; + delete rules[ruleKey]; + save(); + return true; +} + +export function getRule(router: string, key: string): string | undefined { + return getRules()[`${router}:${key}`]; +} + +export function listRules(): Record { + return { ...getRules() }; +} diff --git a/plugin/hooks/before-prompt-build.ts b/plugin/hooks/before-prompt-build.ts new file mode 100644 index 0000000..5f23c50 --- /dev/null +++ b/plugin/hooks/before-prompt-build.ts @@ -0,0 +1,25 @@ +import { resolveInjection } from "../core/prompt-injector.js"; + +const _G = globalThis as Record; +const DEDUP_KEY = "_prismFacetBPBDedup"; + +export function registerBeforePromptBuild(api: { + on(hook: string, handler: (...args: any[]) => any): void; + logger: { info(msg: string): void; warn(msg: string): void }; +}): void { + if (!(_G[DEDUP_KEY] instanceof WeakSet)) _G[DEDUP_KEY] = new WeakSet(); + const dedup = _G[DEDUP_KEY] as WeakSet; + + api.on("before_prompt_build", async (event: unknown, ctx: { agentId?: string }) => { + if (dedup.has(event as object)) return; + dedup.add(event as object); + + const agentId = ctx.agentId || ""; + if (!agentId) return; + + const result = await resolveInjection({ agentId }, api.logger); + if (result.appendSystemContext) { + return { appendSystemContext: result.appendSystemContext }; + } + }); +} diff --git a/plugin/index.ts b/plugin/index.ts new file mode 100644 index 0000000..b0fb59e --- /dev/null +++ b/plugin/index.ts @@ -0,0 +1,66 @@ +import path from "node:path"; +import { loadRouters } from "./core/router-loader.js"; +import { initRuleStore } from "./core/rule-store.js"; +import { registerBeforePromptBuild } from "./hooks/before-prompt-build.js"; +import { registerPromptRulesTool } from "./tools/prompt-rules.js"; + +interface PluginConfig { + routersDir?: string; + rulesFile?: string; +} + +interface OpenClawPluginApi { + logger: { + info(msg: string): void; + warn(msg: string): void; + error(msg: string): void; + }; + on(hook: string, handler: (...args: any[]) => any): void; + registerTool(def: any): void; + config?: PluginConfig; +} + +const _G = globalThis as Record; +const LIFECYCLE_KEY = "_prismFacetGatewayLifecycleRegistered"; + +function normalizeConfig(api: OpenClawPluginApi): PluginConfig { + const raw = (api as any).config ?? {}; + return { + routersDir: typeof raw.routersDir === "string" ? raw.routersDir : undefined, + rulesFile: typeof raw.rulesFile === "string" ? raw.rulesFile : undefined, + }; +} + +export default { + id: "prism-facet", + name: "PrismFacet", + + register(api: OpenClawPluginApi) { + const config = normalizeConfig(api); + const pluginDir = path.dirname(new URL(import.meta.url).pathname); + const routersDir = config.routersDir || path.resolve(pluginDir, "..", "routers"); + const rulesFile = config.rulesFile || path.resolve(pluginDir, "..", "rules.json"); + + // Gateway lifecycle: init once + if (!_G[LIFECYCLE_KEY]) { + _G[LIFECYCLE_KEY] = true; + + initRuleStore(rulesFile); + loadRouters(routersDir, api.logger).catch((err) => { + api.logger.error(`[prism-facet] failed to load routers: ${String(err)}`); + }); + + api.on("gateway_stop", () => { + _G[LIFECYCLE_KEY] = false; + }); + } + + // Agent session hooks: register every time (dedup inside handler) + registerBeforePromptBuild(api); + + // Tools + registerPromptRulesTool(api, routersDir); + + api.logger.info("[prism-facet] plugin registered"); + }, +}; diff --git a/openclaw.plugin.json b/plugin/openclaw.plugin.json similarity index 89% rename from openclaw.plugin.json rename to plugin/openclaw.plugin.json index 0fd8a2b..7e7ec2e 100644 --- a/openclaw.plugin.json +++ b/plugin/openclaw.plugin.json @@ -3,9 +3,10 @@ "name": "PrismFacet", "version": "0.1.0", "description": "Dynamic system prompt injection via routers and rules", - "main": "dist/index.js", + "main": "index.js", "configSchema": { "type": "object", + "additionalProperties": false, "properties": { "routersDir": { "type": "string", diff --git a/plugin/package.json b/plugin/package.json new file mode 100644 index 0000000..d702479 --- /dev/null +++ b/plugin/package.json @@ -0,0 +1,6 @@ +{ + "name": "prism-facet", + "version": "0.1.0", + "type": "module", + "main": "index.js" +} diff --git a/plugin/tools/prompt-rules.ts b/plugin/tools/prompt-rules.ts new file mode 100644 index 0000000..829ddb5 --- /dev/null +++ b/plugin/tools/prompt-rules.ts @@ -0,0 +1,106 @@ +import { addRule, removeRule, listRules } from "../core/rule-store.js"; +import { loadRouters, getRouterNames } from "../core/router-loader.js"; +import { resolveInjection } from "../core/prompt-injector.js"; + +export function registerPromptRulesTool( + api: { + registerTool(def: any): void; + logger: { info(msg: string): void; warn(msg: string): void }; + }, + routersDir: string +): void { + api.registerTool({ + name: "prompt_rules", + description: + "Manage PrismFacet prompt injection rules. " + + "Actions: add (register a rule mapping router:key to a prompt file), " + + "remove (delete a rule), list (show all rules), " + + "test (preview which prompts would be injected for an agent), " + + "reload-routers (hot-reload all router functions), " + + "list-routers (show loaded routers).", + inputSchema: { + type: "object", + properties: { + action: { + type: "string", + enum: ["add", "remove", "list", "test", "reload-routers", "list-routers"], + description: "The action to perform", + }, + router: { + type: "string", + description: "Router name (for add/remove)", + }, + key: { + type: "string", + description: "Rule key (for add/remove)", + }, + file: { + type: "string", + description: "Absolute path to prompt file (for add)", + }, + agent: { + type: "string", + description: "Agent ID to test (for test)", + }, + }, + required: ["action"], + }, + execute: async (_toolCallId: string, params: Record) => { + const action = params.action as string; + const router = params.router as string | undefined; + const key = params.key as string | undefined; + const file = params.file as string | undefined; + const agent = params.agent as string | undefined; + + switch (action) { + case "add": { + if (!router || !key || !file) { + return { result: "Error: add requires router, key, and file" }; + } + addRule(router, key, file); + return { result: `Rule added: ${router}:${key} → ${file}` }; + } + case "remove": { + if (!router || !key) { + return { result: "Error: remove requires router and key" }; + } + const removed = removeRule(router, key); + return { + result: removed + ? `Rule removed: ${router}:${key}` + : `Rule not found: ${router}:${key}`, + }; + } + case "list": { + const rules = listRules(); + const entries = Object.entries(rules); + if (entries.length === 0) return { result: "No rules registered." }; + return { result: entries.map(([k, v]) => `${k} → ${v}`).join("\n") }; + } + case "test": { + if (!agent) return { result: "Error: test requires agent" }; + const result = await resolveInjection({ agentId: agent }, api.logger); + if (!result.appendSystemContext) { + return { result: `No prompts matched for agent: ${agent}` }; + } + return { result: `Matched prompts for ${agent}:\n\n${result.appendSystemContext}` }; + } + case "reload-routers": { + await loadRouters(routersDir, api.logger); + const names = getRouterNames(); + return { result: `Routers reloaded: ${names.join(", ") || "(none)"}` }; + } + case "list-routers": { + const names = getRouterNames(); + return { + result: names.length > 0 + ? `Loaded routers: ${names.join(", ")}` + : "No routers loaded.", + }; + } + default: + return { result: `Unknown action: ${action}` }; + } + }, + }); +} diff --git a/scripts/install.mjs b/scripts/install.mjs new file mode 100644 index 0000000..3723d91 --- /dev/null +++ b/scripts/install.mjs @@ -0,0 +1,143 @@ +#!/usr/bin/env node +import fs from "node:fs"; +import path from "node:path"; +import { execSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const projRoot = path.resolve(__dirname, ".."); +const pluginSrcDir = path.join(projRoot, "plugin"); +const routersSrcDir = path.join(projRoot, "routers"); + +const PLUGIN_ID = "prism-facet"; +const ocDir = path.join(process.env.HOME || "~", ".openclaw"); +const pluginsDir = path.join(ocDir, "plugins"); +const installDir = path.join(pluginsDir, PLUGIN_ID); +const configPath = path.join(ocDir, "openclaw.json"); + +const args = process.argv.slice(2); +const action = args[0]; + +if (!action || !["--install", "--uninstall", "--update"].includes(action)) { + console.log("Usage: node scripts/install.mjs --install | --uninstall | --update"); + process.exit(1); +} + +function readConfig() { + return JSON.parse(fs.readFileSync(configPath, "utf8")); +} + +function writeConfig(cfg) { + fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf8"); +} + +function setIfMissing(obj, key, value) { + if (obj[key] === undefined || obj[key] === null) { + obj[key] = value; + } +} + +function buildPlugin() { + console.log("Building plugin..."); + // Compile TypeScript from plugin/ to plugin/ (in-place, JS output) + execSync("npx tsc --project tsconfig.plugin.json", { cwd: projRoot, stdio: "inherit" }); +} + +function install() { + // 1. Build + buildPlugin(); + + // 2. Clean and copy plugin to install dir + if (fs.existsSync(installDir)) { + fs.rmSync(installDir, { recursive: true }); + } + fs.mkdirSync(installDir, { recursive: true }); + + // Copy compiled plugin files + copyDirRecursive(pluginSrcDir, installDir, [".ts"]); + + // Copy routers alongside plugin + const routersInstallDir = path.join(installDir, "routers"); + if (fs.existsSync(routersSrcDir)) { + fs.mkdirSync(routersInstallDir, { recursive: true }); + copyDirRecursive(routersSrcDir, routersInstallDir); + } + + // Create empty rules.json if not exists + const rulesPath = path.join(installDir, "rules.json"); + if (!fs.existsSync(rulesPath)) { + fs.writeFileSync(rulesPath, "{}\n", "utf8"); + } + + // 3. Update openclaw.json + const cfg = readConfig(); + const plugins = cfg.plugins ??= {}; + const allow = plugins.allow ??= []; + const loadPaths = (plugins.load ??= {}).paths ??= []; + const entries = plugins.entries ??= {}; + + if (!allow.includes(PLUGIN_ID)) allow.push(PLUGIN_ID); + if (!loadPaths.includes(installDir)) loadPaths.push(installDir); + + const entry = entries[PLUGIN_ID] ??= {}; + setIfMissing(entry, "enabled", true); + const hooks = entry.hooks ??= {}; + setIfMissing(hooks, "allowPromptInjection", true); + + writeConfig(cfg); + console.log(`Installed ${PLUGIN_ID} to ${installDir}`); + console.log("Restart gateway: openclaw gateway restart"); +} + +function uninstall() { + const cfg = readConfig(); + const plugins = cfg.plugins ?? {}; + + // Remove from allow + if (Array.isArray(plugins.allow)) { + plugins.allow = plugins.allow.filter((id) => id !== PLUGIN_ID); + } + + // Remove entry + if (plugins.entries) delete plugins.entries[PLUGIN_ID]; + + // Remove load path + if (plugins.load?.paths) { + plugins.load.paths = plugins.load.paths.filter((p) => !p.includes(PLUGIN_ID)); + } + + writeConfig(cfg); + + // Remove install dir + if (fs.existsSync(installDir)) { + fs.rmSync(installDir, { recursive: true }); + console.log(`Removed ${installDir}`); + } + + console.log(`Uninstalled ${PLUGIN_ID}`); +} + +function copyDirRecursive(src, dest, excludeExts = []) { + if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); + for (const entry of fs.readdirSync(src, { withFileTypes: true })) { + const srcPath = path.join(src, entry.name); + const destPath = path.join(dest, entry.name); + if (entry.isDirectory()) { + if (entry.name === "node_modules") continue; + copyDirRecursive(srcPath, destPath, excludeExts); + } else { + if (excludeExts.some((ext) => entry.name.endsWith(ext))) continue; + fs.copyFileSync(srcPath, destPath); + } + } +} + +switch (action) { + case "--install": + case "--update": + install(); + break; + case "--uninstall": + uninstall(); + break; +} diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 57b5a57..0000000 --- a/src/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import path from "node:path"; -import { loadRouters } from "./router-loader.js"; -import { initRuleStore } from "./rule-store.js"; -import { resolveInjection } from "./prompt-injector.js"; -import { registerTools } from "./tools.js"; -import type { PluginApi, PluginConfig } from "./types.js"; - -export function register(api: PluginApi, config?: PluginConfig) { - const pluginDir = path.dirname(new URL(import.meta.url).pathname); - const routersDir = config?.routersDir || path.resolve(pluginDir, "..", "routers"); - const rulesFile = config?.rulesFile || path.resolve(pluginDir, "..", "rules.json"); - - api.logger.info(`[prism-facet] initializing (routers: ${routersDir}, rules: ${rulesFile})`); - - // Initialize rule store - initRuleStore(rulesFile); - - // Load routers - loadRouters(routersDir, api.logger).catch((err) => { - api.logger.error(`[prism-facet] failed to load routers: ${String(err)}`); - }); - - // Register before_prompt_build hook - api.on("before_prompt_build", async (_event: unknown, ctx: { agentId?: string }) => { - const agentId = ctx.agentId || ""; - if (!agentId) return; - - const result = await resolveInjection({ agentId }, api.logger); - if (result.appendSystemContext) { - return { appendSystemContext: result.appendSystemContext }; - } - }); - - // Register MCP tools - registerTools(api, routersDir); - - api.logger.info("[prism-facet] plugin registered"); -} diff --git a/src/router-loader.ts b/src/router-loader.ts deleted file mode 100644 index 2d3dc73..0000000 --- a/src/router-loader.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { readdirSync } from "node:fs"; -import path from "node:path"; -import type { LoadedRouter, RouterModule } from "./types.js"; - -const loadedRouters: Map = new Map(); -let loadCounter = 0; - -function routerNameFromFile(filename: string): string { - return filename.replace(/\.(ts|js|mjs)$/, ""); -} - -export async function loadRouters( - routersDir: string, - logger: { info: (msg: string) => void; warn: (msg: string) => void } -): Promise { - loadedRouters.clear(); - loadCounter++; - - let files: string[]; - try { - files = readdirSync(routersDir).filter((f) => - /\.(ts|js|mjs)$/.test(f) && !f.startsWith(".") - ); - } catch { - logger.warn(`routers directory not found: ${routersDir}`); - return; - } - - for (const file of files) { - const name = routerNameFromFile(file); - const filePath = path.resolve(routersDir, file); - try { - // Cache-bust: append query param so Node re-imports the module - const mod = (await import(`${filePath}?v=${loadCounter}`)) as RouterModule; - if (typeof mod.resolve !== "function") { - logger.warn(`router ${name}: no resolve() export, skipping`); - continue; - } - loadedRouters.set(name, { name, filePath, module: mod }); - logger.info(`router loaded: ${name}`); - } catch (err) { - logger.warn(`router ${name}: failed to load — ${String(err)}`); - } - } -} - -export function getRouters(): LoadedRouter[] { - return Array.from(loadedRouters.values()); -} - -export function getRouterNames(): string[] { - return Array.from(loadedRouters.keys()); -} diff --git a/src/rule-store.ts b/src/rule-store.ts deleted file mode 100644 index 3c277fd..0000000 --- a/src/rule-store.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { readFileSync, writeFileSync } from "node:fs"; -import type { RuleStore } from "./types.js"; - -let rules: RuleStore = {}; -let rulesFilePath = ""; - -export function initRuleStore(filePath: string): void { - rulesFilePath = filePath; - try { - const raw = readFileSync(filePath, "utf8"); - rules = JSON.parse(raw) as RuleStore; - } catch { - rules = {}; - } -} - -function save(): void { - if (!rulesFilePath) return; - writeFileSync(rulesFilePath, JSON.stringify(rules, null, 2) + "\n", "utf8"); -} - -export function addRule(router: string, key: string, promptFile: string): void { - rules[`${router}:${key}`] = promptFile; - save(); -} - -export function removeRule(router: string, key: string): boolean { - const ruleKey = `${router}:${key}`; - if (!(ruleKey in rules)) return false; - delete rules[ruleKey]; - save(); - return true; -} - -export function getRule(router: string, key: string): string | undefined { - return rules[`${router}:${key}`]; -} - -export function listRules(): Record { - return { ...rules }; -} - -export function getRulesForRouter(router: string, key: string): string | undefined { - return rules[`${router}:${key}`]; -} diff --git a/src/tools.ts b/src/tools.ts deleted file mode 100644 index 22274b8..0000000 --- a/src/tools.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { addRule, removeRule, listRules } from "./rule-store.js"; -import { loadRouters, getRouterNames } from "./router-loader.js"; -import { resolveInjection } from "./prompt-injector.js"; -import type { PluginApi } from "./types.js"; - -export function registerTools( - api: PluginApi, - routersDir: string -): void { - api.registerTool({ - name: "prompt_rules", - description: - "Manage PrismFacet prompt injection rules. " + - "Actions: add (register a rule), remove (delete a rule), list (show all rules), " + - "test (preview which prompts would be injected for an agent), " + - "reload-routers (hot-reload all router functions), list-routers (show loaded routers).", - parameters: { - type: "object", - properties: { - action: { - type: "string", - enum: [ - "add", - "remove", - "list", - "test", - "reload-routers", - "list-routers", - ], - description: "The action to perform", - }, - router: { - type: "string", - description: "Router name (for add/remove)", - }, - key: { - type: "string", - description: "Rule key (for add/remove)", - }, - file: { - type: "string", - description: "Prompt file path (for add)", - }, - agent: { - type: "string", - description: "Agent ID (for test)", - }, - }, - required: ["action"], - }, - handler: async (params: { - action: string; - router?: string; - key?: string; - file?: string; - agent?: string; - }) => { - switch (params.action) { - case "add": { - if (!params.router || !params.key || !params.file) { - return "Error: add requires --router, --key, and --file"; - } - addRule(params.router, params.key, params.file); - return `Rule added: ${params.router}:${params.key} → ${params.file}`; - } - - case "remove": { - if (!params.router || !params.key) { - return "Error: remove requires --router and --key"; - } - const removed = removeRule(params.router, params.key); - return removed - ? `Rule removed: ${params.router}:${params.key}` - : `Rule not found: ${params.router}:${params.key}`; - } - - case "list": { - const rules = listRules(); - const entries = Object.entries(rules); - if (entries.length === 0) return "No rules registered."; - return entries - .map(([k, v]) => `${k} → ${v}`) - .join("\n"); - } - - case "test": { - if (!params.agent) { - return "Error: test requires --agent"; - } - const result = await resolveInjection( - { agentId: params.agent }, - api.logger - ); - if (!result.appendSystemContext) { - return `No prompts matched for agent: ${params.agent}`; - } - return `Matched prompts for ${params.agent}:\n\n${result.appendSystemContext}`; - } - - case "reload-routers": { - await loadRouters(routersDir, api.logger); - const names = getRouterNames(); - return `Routers reloaded: ${names.join(", ") || "(none)"}`; - } - - case "list-routers": { - const names = getRouterNames(); - return names.length > 0 - ? `Loaded routers: ${names.join(", ")}` - : "No routers loaded."; - } - - default: - return `Unknown action: ${params.action}`; - } - }, - }); -} diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index e92398a..0000000 --- a/src/types.ts +++ /dev/null @@ -1,32 +0,0 @@ -export interface RouterContext { - agentId: string; -} - -export interface RouterModule { - resolve(ctx: RouterContext): string | Promise; -} - -export interface LoadedRouter { - name: string; - filePath: string; - module: RouterModule; -} - -export interface RuleStore { - [ruleKey: string]: string; // "router:key" → prompt file path -} - -export interface PluginApi { - logger: { - info(msg: string): void; - warn(msg: string): void; - error(msg: string): void; - }; - on(hook: string, handler: (...args: any[]) => any): void; - registerTool(def: any): void; -} - -export interface PluginConfig { - routersDir?: string; - rulesFile?: string; -} diff --git a/tsconfig.json b/tsconfig.plugin.json similarity index 54% rename from tsconfig.json rename to tsconfig.plugin.json index 16ca579..0489d26 100644 --- a/tsconfig.json +++ b/tsconfig.plugin.json @@ -3,14 +3,16 @@ "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", - "outDir": "dist", - "rootDir": "src", - "declaration": true, + "outDir": "plugin", + "rootDir": "plugin", + "declaration": false, "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true + "allowJs": true, + "emitDeclarationOnly": false }, - "include": ["src/**/*"] + "include": ["plugin/**/*.ts"], + "exclude": ["plugin/**/*.js"] }