diff --git a/index.ts b/index.ts index dcc2236..875efa4 100644 --- a/index.ts +++ b/index.ts @@ -110,7 +110,7 @@ const plugin = { api.registerTool(createOnboardTool(api), { names: ["onboard"] }); // CLI - api.registerCli(({ program }: { program: any }) => registerCli(program), { + api.registerCli(({ program }: { program: any }) => registerCli(program, api), { commands: ["devclaw"], }); diff --git a/lib/binding-manager.ts b/lib/binding-manager.ts index 5aa2179..8d54e2b 100644 --- a/lib/binding-manager.ts +++ b/lib/binding-manager.ts @@ -4,8 +4,7 @@ * Handles detection of existing channel bindings, channel availability, * and safe migration of bindings between agents. */ -import fs from "node:fs/promises"; -import path from "node:path"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; export type ChannelType = string; @@ -28,18 +27,13 @@ export interface BindingAnalysis { * Analyze the current state of channel bindings for a given channel. */ export async function analyzeChannelBindings( + api: OpenClawPluginApi, channel: ChannelType, ): Promise { - const configPath = path.join( - process.env.HOME ?? "/home/lauren", - ".openclaw", - "openclaw.json", - ); - - const config = JSON.parse(await fs.readFile(configPath, "utf-8")); + const config = api.runtime.config.loadConfig(); // Check if channel is configured and enabled - const channelConfig = config.channels?.[channel]; + const channelConfig = (config.channels as any)?.[channel]; const channelConfigured = !!channelConfig; const channelEnabled = channelConfig?.enabled === true; @@ -53,7 +47,7 @@ export async function analyzeChannelBindings( for (const binding of bindings) { if (binding.match?.channel === channel) { const agent = config.agents?.list?.find( - (a: { id: string }) => a.id === binding.agentId, + (a) => a.id === binding.agentId, ); const agentName = agent?.name ?? binding.agentId; @@ -101,25 +95,17 @@ export async function analyzeChannelBindings( * Migrate a channel-wide binding from one agent to another. */ export async function migrateChannelBinding( + api: OpenClawPluginApi, channel: ChannelType, fromAgentId: string, toAgentId: string, ): Promise { - const configPath = path.join( - process.env.HOME ?? "/home/lauren", - ".openclaw", - "openclaw.json", - ); - - const config = JSON.parse(await fs.readFile(configPath, "utf-8")); + const config = api.runtime.config.loadConfig(); const bindings = config.bindings ?? []; // Find the channel-wide binding for this channel and agent const bindingIndex = bindings.findIndex( - (b: { - agentId: string; - match?: { channel: string; peer?: unknown }; - }) => + (b) => b.match?.channel === channel && !b.match.peer && b.agentId === fromAgentId, @@ -133,42 +119,26 @@ export async function migrateChannelBinding( // Update the binding to point to the new agent bindings[bindingIndex].agentId = toAgentId; - config.bindings = bindings; + (config as any).bindings = bindings; - await fs.writeFile( - configPath, - JSON.stringify(config, null, 2) + "\n", - "utf-8", - ); + await api.runtime.config.writeConfigFile(config); } /** * Remove a channel-wide binding for a specific agent. */ export async function removeChannelBinding( + api: OpenClawPluginApi, channel: ChannelType, agentId: string, ): Promise { - const configPath = path.join( - process.env.HOME ?? "/home/lauren", - ".openclaw", - "openclaw.json", - ); - - const config = JSON.parse(await fs.readFile(configPath, "utf-8")); + const config = api.runtime.config.loadConfig(); const bindings = config.bindings ?? []; // Filter out the channel-wide binding for this channel and agent - config.bindings = bindings.filter( - (b: { - agentId: string; - match?: { channel: string; peer?: unknown }; - }) => !(b.match?.channel === channel && !b.match.peer && b.agentId === agentId), + (config as any).bindings = bindings.filter( + (b) => !(b.match?.channel === channel && !b.match.peer && b.agentId === agentId), ); - await fs.writeFile( - configPath, - JSON.stringify(config, null, 2) + "\n", - "utf-8", - ); + await api.runtime.config.writeConfigFile(config); } diff --git a/lib/cli.ts b/lib/cli.ts index a454d4e..82553cb 100644 --- a/lib/cli.ts +++ b/lib/cli.ts @@ -4,13 +4,14 @@ * Uses Commander.js (provided by OpenClaw plugin SDK context). */ import type { Command } from "commander"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { runSetup } from "./setup/index.js"; import { DEV_LEVELS, QA_LEVELS, DEFAULT_MODELS } from "./tiers.js"; /** * Register the `devclaw` CLI command group on a Commander program. */ -export function registerCli(program: Command): void { +export function registerCli(program: Command, api: OpenClawPluginApi): void { const devclaw = program .command("devclaw") .description("DevClaw development pipeline tools"); @@ -41,6 +42,7 @@ export function registerCli(program: Command): void { : undefined; const result = await runSetup({ + api, newAgentName: opts.newAgent, agentId: opts.agent, workspacePath: opts.workspace, diff --git a/lib/setup/agent.ts b/lib/setup/agent.ts index 1cfff58..f135cc6 100644 --- a/lib/setup/agent.ts +++ b/lib/setup/agent.ts @@ -5,18 +5,16 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; import fs from "node:fs/promises"; import path from "node:path"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; const execFileAsync = promisify(execFile); -function openclawConfigPath(): string { - return path.join(process.env.HOME ?? "/home/lauren", ".openclaw", "openclaw.json"); -} - /** * Create a new agent via `openclaw agents add`. * Cleans up .git and BOOTSTRAP.md from the workspace, updates display name. */ export async function createAgent( + api: OpenClawPluginApi, name: string, channelBinding?: "telegram" | "whatsapp" | null, ): Promise<{ agentId: string; workspacePath: string }> { @@ -25,13 +23,7 @@ export async function createAgent( .replace(/[^a-z0-9]+/g, "-") .replace(/^-|-$/g, ""); - const workspacePath = path.join( - process.env.HOME ?? "/home/lauren", - ".openclaw", - `workspace-${agentId}`, - ); - - const args = ["agents", "add", agentId, "--workspace", workspacePath, "--non-interactive"]; + const args = ["agents", "add", agentId, "--non-interactive"]; if (channelBinding) args.push("--bind", channelBinding); try { @@ -40,20 +32,19 @@ export async function createAgent( throw new Error(`Failed to create agent "${name}": ${(err as Error).message}`); } + const workspacePath = resolveWorkspacePath(api, agentId); await cleanupWorkspace(workspacePath); - await updateAgentDisplayName(agentId, name); + await updateAgentDisplayName(api, agentId, name); return { agentId, workspacePath }; } /** - * Resolve workspace path from an agent ID by reading openclaw.json. + * Resolve workspace path from an agent ID via OpenClaw config API. */ -export async function resolveWorkspacePath(agentId: string): Promise { - const raw = await fs.readFile(openclawConfigPath(), "utf-8"); - const config = JSON.parse(raw); - - const agent = config.agents?.list?.find((a: { id: string }) => a.id === agentId); +export function resolveWorkspacePath(api: OpenClawPluginApi, agentId: string): string { + const config = api.runtime.config.loadConfig(); + const agent = config.agents?.list?.find((a) => a.id === agentId); if (!agent?.workspace) { throw new Error(`Agent "${agentId}" not found in openclaw.json or has no workspace configured.`); } @@ -71,15 +62,14 @@ async function cleanupWorkspace(workspacePath: string): Promise { try { await fs.unlink(path.join(workspacePath, "BOOTSTRAP.md")); } catch { /* may not exist */ } } -async function updateAgentDisplayName(agentId: string, name: string): Promise { +async function updateAgentDisplayName(api: OpenClawPluginApi, agentId: string, name: string): Promise { if (name === agentId) return; try { - const configPath = openclawConfigPath(); - const config = JSON.parse(await fs.readFile(configPath, "utf-8")); - const agent = config.agents?.list?.find((a: { id: string }) => a.id === agentId); + const config = api.runtime.config.loadConfig(); + const agent = config.agents?.list?.find((a) => a.id === agentId); if (agent) { - agent.name = name; - await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8"); + (agent as any).name = name; + await api.runtime.config.writeConfigFile(config); } } catch (err) { console.warn(`Warning: Could not update display name: ${(err as Error).message}`); diff --git a/lib/setup/config.ts b/lib/setup/config.ts index 6d1c991..2d4b89d 100644 --- a/lib/setup/config.ts +++ b/lib/setup/config.ts @@ -3,16 +3,11 @@ * * Handles: model level config, devClawAgentIds, tool restrictions, subagent cleanup. */ -import fs from "node:fs/promises"; -import path from "node:path"; +import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { HEARTBEAT_DEFAULTS } from "../services/heartbeat.js"; type ModelConfig = { dev: Record; qa: Record }; -function openclawConfigPath(): string { - return path.join(process.env.HOME ?? "/home/lauren", ".openclaw", "openclaw.json"); -} - /** * Write DevClaw model level config and devClawAgentIds to openclaw.json plugins section. * @@ -23,18 +18,18 @@ function openclawConfigPath(): string { * Read-modify-write to preserve existing config. */ export async function writePluginConfig( + api: OpenClawPluginApi, models: ModelConfig, agentId?: string, projectExecution?: "parallel" | "sequential", ): Promise { - const configPath = openclawConfigPath(); - const config = JSON.parse(await fs.readFile(configPath, "utf-8")); + const config = api.runtime.config.loadConfig() as Record; ensurePluginStructure(config); - config.plugins.entries.devclaw.config.models = models; + (config as any).plugins.entries.devclaw.config.models = models; if (projectExecution) { - config.plugins.entries.devclaw.config.projectExecution = projectExecution; + (config as any).plugins.entries.devclaw.config.projectExecution = projectExecution; } ensureHeartbeatDefaults(config); @@ -45,9 +40,7 @@ export async function writePluginConfig( addToolRestrictions(config, agentId); } - const tmpPath = configPath + ".tmp"; - await fs.writeFile(tmpPath, JSON.stringify(config, null, 2) + "\n", "utf-8"); - await fs.rename(tmpPath, configPath); + await api.runtime.config.writeConfigFile(config as any); } // --------------------------------------------------------------------------- diff --git a/lib/setup/index.ts b/lib/setup/index.ts index b1b8339..307a6cf 100644 --- a/lib/setup/index.ts +++ b/lib/setup/index.ts @@ -4,6 +4,7 @@ * Coordinates: agent creation → model config → workspace scaffolding. * Used by both the `setup` tool and the `openclaw devclaw setup` CLI command. */ +import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { DEFAULT_MODELS } from "../tiers.js"; import { migrateChannelBinding } from "../binding-manager.js"; import { createAgent, resolveWorkspacePath } from "./agent.js"; @@ -13,6 +14,8 @@ import { scaffoldWorkspace } from "./workspace.js"; export type ModelConfig = { dev: Record; qa: Record }; export type SetupOpts = { + /** OpenClaw plugin API for config access. */ + api: OpenClawPluginApi; /** Create a new agent with this name. Mutually exclusive with agentId. */ newAgentName?: string; /** Channel binding for new agent. Only used when newAgentName is set. */ @@ -56,7 +59,7 @@ export async function runSetup(opts: SetupOpts): Promise { await resolveOrCreateAgent(opts, warnings); const models = buildModelConfig(opts.models); - await writePluginConfig(models, agentId, opts.projectExecution); + await writePluginConfig(opts.api, models, agentId, opts.projectExecution); const filesWritten = await scaffoldWorkspace(workspacePath); @@ -77,13 +80,13 @@ async function resolveOrCreateAgent( bindingMigrated?: SetupResult["bindingMigrated"]; }> { if (opts.newAgentName) { - const { agentId, workspacePath } = await createAgent(opts.newAgentName, opts.channelBinding); + const { agentId, workspacePath } = await createAgent(opts.api, opts.newAgentName, opts.channelBinding); const bindingMigrated = await tryMigrateBinding(opts, agentId, warnings); return { agentId, workspacePath, agentCreated: true, bindingMigrated }; } if (opts.agentId) { - const workspacePath = opts.workspacePath ?? await resolveWorkspacePath(opts.agentId); + const workspacePath = opts.workspacePath ?? resolveWorkspacePath(opts.api, opts.agentId); return { agentId: opts.agentId, workspacePath, agentCreated: false }; } @@ -101,7 +104,7 @@ async function tryMigrateBinding( ): Promise { if (!opts.migrateFrom || !opts.channelBinding) return undefined; try { - await migrateChannelBinding(opts.channelBinding, opts.migrateFrom, agentId); + await migrateChannelBinding(opts.api, opts.channelBinding, opts.migrateFrom, agentId); return { from: opts.migrateFrom, channel: opts.channelBinding }; } catch (err) { warnings.push(`Failed to migrate binding from "${opts.migrateFrom}": ${(err as Error).message}`); diff --git a/lib/tools/setup.ts b/lib/tools/setup.ts index 3c85d19..edce3d4 100644 --- a/lib/tools/setup.ts +++ b/lib/tools/setup.ts @@ -81,6 +81,7 @@ export function createSetupTool(api: OpenClawPluginApi) { async execute(_id: string, params: Record) { const result = await runSetup({ + api, newAgentName: params.newAgentName as string | undefined, channelBinding: (params.channelBinding as "telegram" | "whatsapp") ?? null,