- Migrate src/ → plugin/ (plugin/core/, plugin/web/, plugin/commands/)
and src/mcp/ → services/ per OpenClaw plugin dev spec
- Add Gemini CLI backend (plugin/core/gemini/sdk-adapter.ts) with GEMINI.md
system-prompt injection
- Inject bootstrap as stateless system prompt on every turn instead of
first turn only: Claude via --system-prompt, Gemini via workspace/GEMINI.md;
eliminates isFirstTurn branch, keeps skills in sync with OpenClaw snapshots
- Fix session-map-store defensive parsing (sessions ?? []) to handle bare {}
reset files without crashing on .find()
- Add docs/TEST_FLOW.md with E2E test scenarios and expected outcomes
- Add docs/claude/BRIDGE_MODEL_FINDINGS.md with contractor-probe results
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
102 lines
4.3 KiB
TypeScript
102 lines
4.3 KiB
TypeScript
import fs from "node:fs";
|
|
import net from "node:net";
|
|
import path from "node:path";
|
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
import { normalizePluginConfig } from "./core/types/contractor.js";
|
|
import { resolveContractorAgentMetadata } from "./core/contractor/metadata-resolver.js";
|
|
import { createBridgeServer } from "./web/server.js";
|
|
import { registerCli } from "./commands/register-cli.js";
|
|
import type http from "node:http";
|
|
|
|
function isPortFree(port: number): Promise<boolean> {
|
|
return new Promise((resolve) => {
|
|
const tester = net.createServer();
|
|
tester.once("error", () => resolve(false));
|
|
tester.once("listening", () => tester.close(() => resolve(true)));
|
|
tester.listen(port, "127.0.0.1");
|
|
});
|
|
}
|
|
|
|
// ── GlobalThis state ─────────────────────────────────────────────────────────
|
|
// All persistent state lives on globalThis to survive OpenClaw hot-reloads.
|
|
// See LESSONS_LEARNED.md items 1, 3, 11.
|
|
|
|
const _G = globalThis as Record<string, unknown>;
|
|
const LIFECYCLE_KEY = "_contractorAgentLifecycleRegistered";
|
|
const SERVER_KEY = "_contractorAgentBridgeServer";
|
|
/** Key for the live OpenClaw config accessor (getter fn) shared via globalThis. */
|
|
const OPENCLAW_CONFIG_KEY = "_contractorOpenClawConfig";
|
|
|
|
// ── Plugin entry ─────────────────────────────────────────────────────────────
|
|
|
|
export default {
|
|
id: "contractor-agent",
|
|
name: "Contractor Agent",
|
|
async register(api: OpenClawPluginApi) {
|
|
const config = normalizePluginConfig(api.pluginConfig);
|
|
|
|
// Resolve agent metadata for the bridge server's resolveAgent callback.
|
|
// We do this by reading openclaw.json — the bridge server calls this on every turn.
|
|
function resolveAgent(agentId: string, _sessionKey: string) {
|
|
try {
|
|
const configPath = path.join(
|
|
(process.env.HOME ?? "/root"),
|
|
".openclaw",
|
|
"openclaw.json",
|
|
);
|
|
const raw = JSON.parse(fs.readFileSync(configPath, "utf8")) as {
|
|
agents?: { list?: Array<{ id: string; workspace?: string; model?: string }> };
|
|
};
|
|
const agent = raw.agents?.list?.find((a) => a.id === agentId);
|
|
if (!agent) return null;
|
|
const meta = resolveContractorAgentMetadata(agent, config.permissionMode);
|
|
if (!meta) return null;
|
|
return { workspace: meta.workspace };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ── Gateway lifecycle (start bridge server once per gateway process) ──────
|
|
// Guard with globalThis flag AND a port probe to handle the case where the
|
|
// gateway is already running the server while a CLI subprocess is starting up.
|
|
// (See LESSONS_LEARNED.md item 7 — lock file / port probe pattern)
|
|
// Always update the config accessor so hot-reloads get fresh config.
|
|
// server.ts reads this via globalThis to build tool execution context.
|
|
_G[OPENCLAW_CONFIG_KEY] = api.config;
|
|
|
|
if (!_G[LIFECYCLE_KEY]) {
|
|
_G[LIFECYCLE_KEY] = true;
|
|
|
|
// Only bind if port is not already in use (avoids EADDRINUSE in CLI mode)
|
|
const portFree = await isPortFree(config.bridgePort);
|
|
if (!portFree) {
|
|
api.logger.info(
|
|
`[contractor-agent] bridge already running on port ${config.bridgePort}, skipping bind`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const server = createBridgeServer({
|
|
port: config.bridgePort,
|
|
apiKey: config.bridgeApiKey,
|
|
permissionMode: config.permissionMode,
|
|
resolveAgent,
|
|
logger: api.logger,
|
|
});
|
|
_G[SERVER_KEY] = server;
|
|
|
|
api.on("gateway_stop", () => {
|
|
const s = _G[SERVER_KEY] as http.Server | undefined;
|
|
if (s) s.close();
|
|
api.logger.info("[contractor-agent] bridge server stopped");
|
|
});
|
|
}
|
|
|
|
// ── CLI ───────────────────────────────────────────────────────────────────
|
|
registerCli(api);
|
|
|
|
api.logger.info(`[contractor-agent] plugin registered (bridge port: ${config.bridgePort})`);
|
|
},
|
|
};
|