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