@catch-claw/openclaw-agentar 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.ts +750 -0
- package/openclaw.plugin.json +20 -0
- package/package.json +17 -0
package/index.ts
ADDED
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
|
|
7
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
type AgentarConfig = {
|
|
10
|
+
apiBaseUrl?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type AgentarMeta = {
|
|
14
|
+
slug: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
version: string;
|
|
18
|
+
skills?: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type AgentarSearchResult = {
|
|
22
|
+
slug: string;
|
|
23
|
+
displayName: string;
|
|
24
|
+
summary: string;
|
|
25
|
+
version: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type AgentarIndexEntry = AgentarMeta & {
|
|
29
|
+
zip_url?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type InstallContext = {
|
|
33
|
+
slug: string;
|
|
34
|
+
mode: "overwrite" | "new";
|
|
35
|
+
agentName: string;
|
|
36
|
+
contentDir: string;
|
|
37
|
+
targetWorkspace: string;
|
|
38
|
+
metadata?: AgentarMeta;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type InstallHooks = {
|
|
42
|
+
beforeInstall?: (ctx: InstallContext) => Promise<void>;
|
|
43
|
+
afterInstall?: (ctx: InstallContext) => Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type InstallResult = {
|
|
47
|
+
success: boolean;
|
|
48
|
+
slug: string;
|
|
49
|
+
version?: string;
|
|
50
|
+
agentName: string;
|
|
51
|
+
workspace: string;
|
|
52
|
+
skills: string[];
|
|
53
|
+
files: string[];
|
|
54
|
+
introduction?: string | null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
const DEFAULT_API_BASE_URL = "http://127.0.0.1:8080";
|
|
60
|
+
const OPENCLAW_HOME = path.join(os.homedir(), ".openclaw");
|
|
61
|
+
const MAIN_WORKSPACE = path.join(OPENCLAW_HOME, "workspace");
|
|
62
|
+
const WORKSPACES_DIR = path.join(OPENCLAW_HOME, "agentar-workspaces");
|
|
63
|
+
const WORKSPACE_FILES = [
|
|
64
|
+
"SOUL.md",
|
|
65
|
+
"USER.md",
|
|
66
|
+
"IDENTITY.md",
|
|
67
|
+
"TOOLS.md",
|
|
68
|
+
"HEARTBEAT.md",
|
|
69
|
+
"MEMORY.md",
|
|
70
|
+
];
|
|
71
|
+
const SKIP_FILES = ["AGENTS.md", "BOOTSTRAP.md"];
|
|
72
|
+
const INTRO_MESSAGE =
|
|
73
|
+
"请做一下自我介绍,简要说明你是谁、你擅长什么、你能帮用户做哪些事情。";
|
|
74
|
+
|
|
75
|
+
// ─── Fetch Helpers ───────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
async function fetchJson<T>(url: string): Promise<T> {
|
|
78
|
+
const res = await fetch(url);
|
|
79
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
|
80
|
+
return res.json() as Promise<T>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function fetchToFile(url: string, dest: string): Promise<void> {
|
|
84
|
+
const res = await fetch(url);
|
|
85
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
|
86
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
87
|
+
fs.writeFileSync(dest, buffer);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Output Helpers ──────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
function step(n: number, total: number, msg: string): void {
|
|
93
|
+
console.log(`\n [${n}/${total}] ${msg}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function detail(label: string, value: string): void {
|
|
97
|
+
console.log(` ${label}: ${value}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function success(msg: string): void {
|
|
101
|
+
console.log(`\n ✓ ${msg}\n`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function warn(msg: string): void {
|
|
105
|
+
console.log(` ⚠ ${msg}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function box(text: string): void {
|
|
109
|
+
const lines = text.split("\n").filter(Boolean);
|
|
110
|
+
const maxLen = Math.max(...lines.map((l) => l.length));
|
|
111
|
+
const border = "─".repeat(maxLen + 2);
|
|
112
|
+
console.log(` ┌${border}┐`);
|
|
113
|
+
for (const line of lines) {
|
|
114
|
+
console.log(` │ ${line.padEnd(maxLen)} │`);
|
|
115
|
+
}
|
|
116
|
+
console.log(` └${border}┘`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Core Utilities ──────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
function findOpenclawBin(): string {
|
|
122
|
+
try {
|
|
123
|
+
return execSync("which openclaw", { encoding: "utf-8" }).trim();
|
|
124
|
+
} catch {
|
|
125
|
+
const pnpmPath = path.join(os.homedir(), ".local/share/pnpm/openclaw");
|
|
126
|
+
if (fs.existsSync(pnpmPath)) return pnpmPath;
|
|
127
|
+
throw new Error("openclaw CLI not found on PATH");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function backupDirectory(dir: string): string | null {
|
|
132
|
+
if (!fs.existsSync(dir)) return null;
|
|
133
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
134
|
+
const backupDir = `${dir}.bak.${timestamp}`;
|
|
135
|
+
fs.cpSync(dir, backupDir, { recursive: true });
|
|
136
|
+
return backupDir;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function mergeSkills(srcSkillsDir: string, destSkillsDir: string): string[] {
|
|
140
|
+
if (!fs.existsSync(srcSkillsDir)) return [];
|
|
141
|
+
fs.mkdirSync(destSkillsDir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
const merged: string[] = [];
|
|
144
|
+
for (const entry of fs.readdirSync(srcSkillsDir, { withFileTypes: true })) {
|
|
145
|
+
const srcPath = path.join(srcSkillsDir, entry.name);
|
|
146
|
+
const destPath = path.join(destSkillsDir, entry.name);
|
|
147
|
+
if (entry.isDirectory()) {
|
|
148
|
+
fs.cpSync(srcPath, destPath, { recursive: true });
|
|
149
|
+
merged.push(entry.name);
|
|
150
|
+
} else {
|
|
151
|
+
fs.copyFileSync(srcPath, destPath);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return merged;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function extractWorkspaceFiles(
|
|
158
|
+
extractedDir: string,
|
|
159
|
+
targetDir: string,
|
|
160
|
+
): string[] {
|
|
161
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
162
|
+
const copiedFiles: string[] = [];
|
|
163
|
+
|
|
164
|
+
for (const file of WORKSPACE_FILES) {
|
|
165
|
+
const src = path.join(extractedDir, file);
|
|
166
|
+
if (fs.existsSync(src)) {
|
|
167
|
+
fs.copyFileSync(src, path.join(targetDir, file));
|
|
168
|
+
copiedFiles.push(file);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const entry of fs.readdirSync(extractedDir, { withFileTypes: true })) {
|
|
173
|
+
if (entry.name === "skills" && entry.isDirectory()) continue;
|
|
174
|
+
if (SKIP_FILES.includes(entry.name)) continue;
|
|
175
|
+
if (WORKSPACE_FILES.includes(entry.name)) continue;
|
|
176
|
+
const src = path.join(extractedDir, entry.name);
|
|
177
|
+
const dest = path.join(targetDir, entry.name);
|
|
178
|
+
if (entry.isDirectory()) {
|
|
179
|
+
fs.cpSync(src, dest, { recursive: true });
|
|
180
|
+
} else {
|
|
181
|
+
fs.copyFileSync(src, dest);
|
|
182
|
+
}
|
|
183
|
+
copiedFiles.push(entry.name);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return copiedFiles;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resolveContentDir(extractDir: string): string {
|
|
190
|
+
const entries = fs
|
|
191
|
+
.readdirSync(extractDir)
|
|
192
|
+
.filter((e) => !e.startsWith(".") && e !== "__MACOSX");
|
|
193
|
+
if (
|
|
194
|
+
entries.length === 1 &&
|
|
195
|
+
fs.statSync(path.join(extractDir, entries[0])).isDirectory()
|
|
196
|
+
) {
|
|
197
|
+
return path.join(extractDir, entries[0]);
|
|
198
|
+
}
|
|
199
|
+
return extractDir;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── Install Pipeline ────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
async function installAgentar(opts: {
|
|
205
|
+
slug: string;
|
|
206
|
+
mode: "overwrite" | "new";
|
|
207
|
+
agentName?: string;
|
|
208
|
+
apiBaseUrl: string;
|
|
209
|
+
hooks?: InstallHooks;
|
|
210
|
+
quiet?: boolean;
|
|
211
|
+
}): Promise<InstallResult> {
|
|
212
|
+
const { slug, mode, apiBaseUrl, hooks, quiet } = opts;
|
|
213
|
+
const log = quiet ? () => {} : step;
|
|
214
|
+
const logDetail = quiet ? () => {} : detail;
|
|
215
|
+
const total = 5;
|
|
216
|
+
|
|
217
|
+
// [1/5] Fetch metadata
|
|
218
|
+
log(1, total, "Fetching metadata ...");
|
|
219
|
+
let metadata: AgentarMeta | undefined;
|
|
220
|
+
try {
|
|
221
|
+
const data = await fetchJson<{ agentars: AgentarIndexEntry[] }>(
|
|
222
|
+
`${apiBaseUrl}/api/v1/agentar/index`,
|
|
223
|
+
);
|
|
224
|
+
const entry = data.agentars?.find((a) => a.slug === slug);
|
|
225
|
+
if (entry) {
|
|
226
|
+
metadata = {
|
|
227
|
+
slug: entry.slug,
|
|
228
|
+
name: entry.name,
|
|
229
|
+
description: entry.description,
|
|
230
|
+
version: entry.version,
|
|
231
|
+
skills: entry.skills,
|
|
232
|
+
};
|
|
233
|
+
logDetail("name", metadata.name);
|
|
234
|
+
logDetail("version", metadata.version);
|
|
235
|
+
logDetail("description", metadata.description);
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
// Metadata fetch is best-effort — continue without it
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// [2/5] Download package
|
|
242
|
+
log(2, total, "Downloading package ...");
|
|
243
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentar-"));
|
|
244
|
+
const zipPath = path.join(tmpDir, `${slug}.zip`);
|
|
245
|
+
const downloadUrl = `${apiBaseUrl}/api/v1/agentar/download?slug=${encodeURIComponent(slug)}`;
|
|
246
|
+
logDetail("source", downloadUrl);
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
await fetchToFile(downloadUrl, zipPath);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
252
|
+
throw new Error(`Failed to download agentar "${slug}": ${err}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// [3/5] Extract and validate
|
|
256
|
+
log(3, total, "Extracting and validating ...");
|
|
257
|
+
const extractDir = path.join(tmpDir, "extracted");
|
|
258
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
259
|
+
try {
|
|
260
|
+
execSync(`unzip -o -q "${zipPath}" -d "${extractDir}"`, {
|
|
261
|
+
encoding: "utf-8",
|
|
262
|
+
});
|
|
263
|
+
} catch {
|
|
264
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
265
|
+
throw new Error(`Failed to extract agentar zip for "${slug}"`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const contentDir = resolveContentDir(extractDir);
|
|
269
|
+
|
|
270
|
+
if (!fs.existsSync(path.join(contentDir, "SOUL.md"))) {
|
|
271
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
272
|
+
throw new Error(`Invalid agentar "${slug}": missing SOUL.md`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Preview extracted content
|
|
276
|
+
const previewFiles = fs
|
|
277
|
+
.readdirSync(contentDir)
|
|
278
|
+
.filter((e) => !e.startsWith(".") && e !== "__MACOSX");
|
|
279
|
+
const skillsDir = path.join(contentDir, "skills");
|
|
280
|
+
const skillCount = fs.existsSync(skillsDir)
|
|
281
|
+
? fs
|
|
282
|
+
.readdirSync(skillsDir, { withFileTypes: true })
|
|
283
|
+
.filter((e) => e.isDirectory()).length
|
|
284
|
+
: 0;
|
|
285
|
+
const filesDesc = previewFiles.filter((f) => f !== "skills").join(", ");
|
|
286
|
+
logDetail(
|
|
287
|
+
"files",
|
|
288
|
+
skillCount > 0
|
|
289
|
+
? `${filesDesc}, skills/ (${skillCount} skills)`
|
|
290
|
+
: filesDesc,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// Determine agent name
|
|
294
|
+
const agentName =
|
|
295
|
+
mode === "overwrite"
|
|
296
|
+
? "main"
|
|
297
|
+
: opts.agentName || slug.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
298
|
+
|
|
299
|
+
// beforeInstall hook
|
|
300
|
+
const ctx: InstallContext = {
|
|
301
|
+
slug,
|
|
302
|
+
mode,
|
|
303
|
+
agentName,
|
|
304
|
+
contentDir,
|
|
305
|
+
targetWorkspace: "",
|
|
306
|
+
metadata,
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
if (hooks?.beforeInstall) {
|
|
310
|
+
await hooks.beforeInstall(ctx);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// [4/5] Deploy to workspace
|
|
314
|
+
log(4, total, "Deploying to workspace ...");
|
|
315
|
+
let targetWorkspace: string;
|
|
316
|
+
|
|
317
|
+
if (mode === "overwrite") {
|
|
318
|
+
const backupPath = backupDirectory(MAIN_WORKSPACE);
|
|
319
|
+
if (backupPath && !quiet) {
|
|
320
|
+
logDetail("backup", backupPath);
|
|
321
|
+
}
|
|
322
|
+
const copiedFiles = extractWorkspaceFiles(contentDir, MAIN_WORKSPACE);
|
|
323
|
+
const mergedSkills = mergeSkills(
|
|
324
|
+
path.join(contentDir, "skills"),
|
|
325
|
+
path.join(MAIN_WORKSPACE, "skills"),
|
|
326
|
+
);
|
|
327
|
+
targetWorkspace = MAIN_WORKSPACE;
|
|
328
|
+
logDetail("mode", "overwrite main agent");
|
|
329
|
+
logDetail("workspace", targetWorkspace);
|
|
330
|
+
if (mergedSkills.length > 0) {
|
|
331
|
+
logDetail("skills merged", mergedSkills.join(", "));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
ctx.targetWorkspace = targetWorkspace;
|
|
335
|
+
|
|
336
|
+
if (hooks?.afterInstall) {
|
|
337
|
+
await hooks.afterInstall(ctx);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
success: true,
|
|
344
|
+
slug,
|
|
345
|
+
version: metadata?.version,
|
|
346
|
+
agentName: "main",
|
|
347
|
+
workspace: targetWorkspace,
|
|
348
|
+
skills: mergedSkills,
|
|
349
|
+
files: copiedFiles,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// mode === "new"
|
|
354
|
+
const workspaceDir = path.join(WORKSPACES_DIR, agentName);
|
|
355
|
+
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
356
|
+
|
|
357
|
+
const openclawBin = findOpenclawBin();
|
|
358
|
+
try {
|
|
359
|
+
execSync(
|
|
360
|
+
`"${openclawBin}" agents add "${agentName}" --workspace "${workspaceDir}" --non-interactive`,
|
|
361
|
+
{ encoding: "utf-8", stdio: "pipe" },
|
|
362
|
+
);
|
|
363
|
+
} catch (err: unknown) {
|
|
364
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
365
|
+
if (!msg.includes("already exists")) {
|
|
366
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
367
|
+
throw new Error(`Failed to create agent "${agentName}": ${msg}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const copiedFiles = extractWorkspaceFiles(contentDir, workspaceDir);
|
|
372
|
+
const mergedSkills = mergeSkills(
|
|
373
|
+
path.join(contentDir, "skills"),
|
|
374
|
+
path.join(workspaceDir, "skills"),
|
|
375
|
+
);
|
|
376
|
+
targetWorkspace = workspaceDir;
|
|
377
|
+
|
|
378
|
+
logDetail("mode", "new agent");
|
|
379
|
+
logDetail("agent", agentName);
|
|
380
|
+
logDetail("workspace", targetWorkspace);
|
|
381
|
+
if (mergedSkills.length > 0) {
|
|
382
|
+
logDetail("skills merged", mergedSkills.join(", "));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
ctx.targetWorkspace = targetWorkspace;
|
|
386
|
+
|
|
387
|
+
if (hooks?.afterInstall) {
|
|
388
|
+
await hooks.afterInstall(ctx);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
392
|
+
|
|
393
|
+
return {
|
|
394
|
+
success: true,
|
|
395
|
+
slug,
|
|
396
|
+
version: metadata?.version,
|
|
397
|
+
agentName,
|
|
398
|
+
workspace: targetWorkspace,
|
|
399
|
+
skills: mergedSkills,
|
|
400
|
+
files: copiedFiles,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ─── Self-Introduction ───────────────────────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
async function sendIntroduction(opts: {
|
|
407
|
+
agentName: string;
|
|
408
|
+
mode: "overwrite" | "new";
|
|
409
|
+
runtime?: OpenClawPluginApi["runtime"];
|
|
410
|
+
quiet?: boolean;
|
|
411
|
+
}): Promise<string | null> {
|
|
412
|
+
const { agentName, mode, runtime, quiet } = opts;
|
|
413
|
+
const log = quiet ? () => {} : step;
|
|
414
|
+
const logDetail = quiet ? () => {} : detail;
|
|
415
|
+
const logWarn = quiet ? () => {} : warn;
|
|
416
|
+
|
|
417
|
+
log(5, 5, "Activating agent ...");
|
|
418
|
+
logDetail("action", "Sending introduction request ...");
|
|
419
|
+
|
|
420
|
+
// Tool mode: use runtime.subagent.run
|
|
421
|
+
if (runtime?.subagent) {
|
|
422
|
+
try {
|
|
423
|
+
const sessionKey =
|
|
424
|
+
mode === "overwrite"
|
|
425
|
+
? "agent:main:session:main"
|
|
426
|
+
: `agent:${agentName}:session:main`;
|
|
427
|
+
const result = await runtime.subagent.run({
|
|
428
|
+
sessionKey,
|
|
429
|
+
message: INTRO_MESSAGE,
|
|
430
|
+
});
|
|
431
|
+
const reply =
|
|
432
|
+
typeof result === "object" && result !== null && "reply" in result
|
|
433
|
+
? String((result as Record<string, unknown>).reply ?? "")
|
|
434
|
+
: typeof result === "string"
|
|
435
|
+
? result
|
|
436
|
+
: "";
|
|
437
|
+
if (reply) return reply;
|
|
438
|
+
} catch {
|
|
439
|
+
logWarn("Could not reach agent via runtime");
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// CLI mode: use execSync
|
|
445
|
+
try {
|
|
446
|
+
const openclawBin = findOpenclawBin();
|
|
447
|
+
const agentFlag = mode === "new" ? ` --agent "${agentName}"` : "";
|
|
448
|
+
const stdout = execSync(
|
|
449
|
+
`"${openclawBin}" agent${agentFlag} --message "${INTRO_MESSAGE}" --local`,
|
|
450
|
+
{ encoding: "utf-8", stdio: "pipe", timeout: 60_000 },
|
|
451
|
+
);
|
|
452
|
+
return stdout.trim() || null;
|
|
453
|
+
} catch {
|
|
454
|
+
const agentFlag = mode === "new" ? ` --agent ${agentName}` : "";
|
|
455
|
+
logWarn("Could not reach agent (gateway not running?)");
|
|
456
|
+
logWarn(
|
|
457
|
+
`Try manually: openclaw agent${agentFlag} --message "请自我介绍" --local`,
|
|
458
|
+
);
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ─── Plugin Registration ─────────────────────────────────────────────────────
|
|
464
|
+
|
|
465
|
+
export default function register(api: OpenClawPluginApi) {
|
|
466
|
+
const rawConfig = (api.pluginConfig ?? {}) as AgentarConfig;
|
|
467
|
+
const apiBaseUrl = rawConfig.apiBaseUrl?.trim() || DEFAULT_API_BASE_URL;
|
|
468
|
+
|
|
469
|
+
// ── Hook: inject agentar policy ──────────────────────────────────────────
|
|
470
|
+
|
|
471
|
+
api.on(
|
|
472
|
+
"before_prompt_build",
|
|
473
|
+
async () => ({
|
|
474
|
+
prependContext: [
|
|
475
|
+
"Agent template (agentar) policy (operator configured):",
|
|
476
|
+
"1. When user asks to install/switch agent persona, use the `agentar_install` tool.",
|
|
477
|
+
"2. Always confirm with the user before overwriting the main agent workspace.",
|
|
478
|
+
"3. When installing, ask if the user wants to overwrite the main agent or create a new one.",
|
|
479
|
+
].join("\n"),
|
|
480
|
+
}),
|
|
481
|
+
{ priority: 80 },
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
// ── Tool: agentar_install ──────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
api.registerTool(
|
|
487
|
+
() => ({
|
|
488
|
+
name: "agentar_install",
|
|
489
|
+
description:
|
|
490
|
+
"Install an agent template (agentar) from the marketplace. Downloads a zip containing workspace files (SOUL.md, skills, etc.) and either overwrites the main agent or creates a new one. Returns installation details and the agent's self-introduction.",
|
|
491
|
+
parameters: {
|
|
492
|
+
type: "object" as const,
|
|
493
|
+
properties: {
|
|
494
|
+
slug: {
|
|
495
|
+
type: "string",
|
|
496
|
+
description: "The agentar slug to install",
|
|
497
|
+
},
|
|
498
|
+
mode: {
|
|
499
|
+
type: "string",
|
|
500
|
+
enum: ["overwrite", "new"],
|
|
501
|
+
description:
|
|
502
|
+
"overwrite: replace main agent workspace; new: create a new agent",
|
|
503
|
+
},
|
|
504
|
+
agentName: {
|
|
505
|
+
type: "string",
|
|
506
|
+
description:
|
|
507
|
+
"Name for the new agent (only used when mode=new, defaults to slug)",
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
required: ["slug", "mode"],
|
|
511
|
+
},
|
|
512
|
+
execute: async (params: Record<string, unknown>) => {
|
|
513
|
+
const slug = String(params.slug || "");
|
|
514
|
+
const mode = String(params.mode || "new") as "overwrite" | "new";
|
|
515
|
+
const agentName = params.agentName
|
|
516
|
+
? String(params.agentName)
|
|
517
|
+
: undefined;
|
|
518
|
+
|
|
519
|
+
if (!slug) {
|
|
520
|
+
return { error: "slug is required" };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
const result = await installAgentar({
|
|
525
|
+
slug,
|
|
526
|
+
mode,
|
|
527
|
+
agentName,
|
|
528
|
+
apiBaseUrl,
|
|
529
|
+
quiet: true,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
const introduction = await sendIntroduction({
|
|
533
|
+
agentName: result.agentName,
|
|
534
|
+
mode,
|
|
535
|
+
runtime: api.runtime,
|
|
536
|
+
quiet: true,
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
return { ...result, introduction };
|
|
540
|
+
} catch (err) {
|
|
541
|
+
return {
|
|
542
|
+
error: `Installation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
}),
|
|
547
|
+
{ name: "agentar_install" },
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
// ── CLI: openclaw agentar ──────────────────────────────────────────────
|
|
551
|
+
|
|
552
|
+
api.registerCli(
|
|
553
|
+
(cliCtx) => {
|
|
554
|
+
const agentarCmd = cliCtx.program
|
|
555
|
+
.command("agentar")
|
|
556
|
+
.description("Manage agent templates (agentars)");
|
|
557
|
+
|
|
558
|
+
// ── search ─────────────────────────────────────────────────────────
|
|
559
|
+
|
|
560
|
+
agentarCmd
|
|
561
|
+
.command("search <query>")
|
|
562
|
+
.description("Search for agent templates")
|
|
563
|
+
.option("-l, --limit <n>", "Max results", "20")
|
|
564
|
+
.action(async (query: string, opts: { limit: string }) => {
|
|
565
|
+
try {
|
|
566
|
+
const url = `${apiBaseUrl}/api/v1/agentar/search?q=${encodeURIComponent(query)}&limit=${opts.limit}`;
|
|
567
|
+
const data = await fetchJson<{
|
|
568
|
+
results: AgentarSearchResult[];
|
|
569
|
+
}>(url);
|
|
570
|
+
|
|
571
|
+
if (!data.results || data.results.length === 0) {
|
|
572
|
+
console.log("No agentars found.");
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
console.log(
|
|
577
|
+
'\nUse "openclaw agentar install <slug>" to install.\n',
|
|
578
|
+
);
|
|
579
|
+
for (const r of data.results) {
|
|
580
|
+
console.log(` ${r.slug} ${r.displayName}`);
|
|
581
|
+
if (r.summary) console.log(` ${r.summary}`);
|
|
582
|
+
if (r.version) console.log(` version: ${r.version}`);
|
|
583
|
+
}
|
|
584
|
+
console.log();
|
|
585
|
+
} catch (err) {
|
|
586
|
+
console.error(
|
|
587
|
+
`Search failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
588
|
+
);
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
// ── list ───────────────────────────────────────────────────────────
|
|
594
|
+
|
|
595
|
+
agentarCmd
|
|
596
|
+
.command("list")
|
|
597
|
+
.description("List all available agent templates")
|
|
598
|
+
.action(async () => {
|
|
599
|
+
try {
|
|
600
|
+
const url = `${apiBaseUrl}/api/v1/agentar/index`;
|
|
601
|
+
const data = await fetchJson<{
|
|
602
|
+
agentars: AgentarIndexEntry[];
|
|
603
|
+
}>(url);
|
|
604
|
+
|
|
605
|
+
if (!data.agentars || data.agentars.length === 0) {
|
|
606
|
+
console.log("No agentars available.");
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
console.log(
|
|
611
|
+
'\nUse "openclaw agentar install <slug>" to install.\n',
|
|
612
|
+
);
|
|
613
|
+
for (const a of data.agentars) {
|
|
614
|
+
console.log(` ${a.slug} ${a.name}`);
|
|
615
|
+
if (a.description) console.log(` ${a.description}`);
|
|
616
|
+
if (a.version) console.log(` version: ${a.version}`);
|
|
617
|
+
if (a.skills && a.skills.length > 0) {
|
|
618
|
+
console.log(` skills: ${a.skills.join(", ")}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
console.log();
|
|
622
|
+
} catch (err) {
|
|
623
|
+
console.error(
|
|
624
|
+
`List failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
625
|
+
);
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// ── install ────────────────────────────────────────────────────────
|
|
631
|
+
|
|
632
|
+
agentarCmd
|
|
633
|
+
.command("install <slug>")
|
|
634
|
+
.description("Install an agent template")
|
|
635
|
+
.option("--overwrite", "Overwrite the main agent workspace")
|
|
636
|
+
.option(
|
|
637
|
+
"--name <name>",
|
|
638
|
+
"Create a new agent with this name (implies new mode)",
|
|
639
|
+
)
|
|
640
|
+
.action(
|
|
641
|
+
async (
|
|
642
|
+
slug: string,
|
|
643
|
+
opts: {
|
|
644
|
+
overwrite?: boolean;
|
|
645
|
+
name?: string;
|
|
646
|
+
},
|
|
647
|
+
) => {
|
|
648
|
+
let mode: "overwrite" | "new";
|
|
649
|
+
|
|
650
|
+
if (opts.overwrite) {
|
|
651
|
+
mode = "overwrite";
|
|
652
|
+
} else if (opts.name) {
|
|
653
|
+
mode = "new";
|
|
654
|
+
} else {
|
|
655
|
+
const readline = await import("node:readline");
|
|
656
|
+
const rl = readline.createInterface({
|
|
657
|
+
input: process.stdin,
|
|
658
|
+
output: process.stdout,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const answer = await new Promise<string>((resolve) => {
|
|
662
|
+
rl.question(
|
|
663
|
+
`\nInstall agentar "${slug}":\n [1] Overwrite main agent (~/.openclaw/workspace)\n [2] Create a new agent\nChoice (1/2): `,
|
|
664
|
+
(ans: string) => {
|
|
665
|
+
rl.close();
|
|
666
|
+
resolve(ans.trim());
|
|
667
|
+
},
|
|
668
|
+
);
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
mode = answer === "1" ? "overwrite" : "new";
|
|
672
|
+
|
|
673
|
+
if (mode === "new" && !opts.name) {
|
|
674
|
+
const rl2 = readline.createInterface({
|
|
675
|
+
input: process.stdin,
|
|
676
|
+
output: process.stdout,
|
|
677
|
+
});
|
|
678
|
+
opts.name = await new Promise<string>((resolve) => {
|
|
679
|
+
rl2.question(
|
|
680
|
+
`Agent name (default: ${slug}): `,
|
|
681
|
+
(ans: string) => {
|
|
682
|
+
rl2.close();
|
|
683
|
+
resolve(ans.trim() || slug);
|
|
684
|
+
},
|
|
685
|
+
);
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
try {
|
|
691
|
+
console.log(`\n⟩ Installing agentar "${slug}" ...`);
|
|
692
|
+
|
|
693
|
+
const result = await installAgentar({
|
|
694
|
+
slug,
|
|
695
|
+
mode,
|
|
696
|
+
agentName: opts.name,
|
|
697
|
+
apiBaseUrl,
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
const introduction = await sendIntroduction({
|
|
701
|
+
agentName: result.agentName,
|
|
702
|
+
mode,
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
success("Installation complete");
|
|
706
|
+
console.log(" Summary:");
|
|
707
|
+
console.log(
|
|
708
|
+
` agentar ${result.slug}${result.version ? ` (v${result.version})` : ""}`,
|
|
709
|
+
);
|
|
710
|
+
console.log(` agent ${result.agentName}`);
|
|
711
|
+
console.log(` workspace ${result.workspace}`);
|
|
712
|
+
if (result.skills.length > 0) {
|
|
713
|
+
console.log(
|
|
714
|
+
` skills ${result.skills.join(", ")}`,
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
if (result.files.length > 0) {
|
|
718
|
+
console.log(
|
|
719
|
+
` files ${result.files.join(", ")}`,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (introduction) {
|
|
724
|
+
console.log("\n Agent introduction:");
|
|
725
|
+
box(introduction);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
console.log("\n Next steps:");
|
|
729
|
+
if (mode === "new") {
|
|
730
|
+
console.log(
|
|
731
|
+
` openclaw agent --agent ${result.agentName} --message "hello" --local`,
|
|
732
|
+
);
|
|
733
|
+
} else {
|
|
734
|
+
console.log(
|
|
735
|
+
' openclaw agent --message "hello" --local',
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
console.log();
|
|
739
|
+
} catch (err) {
|
|
740
|
+
console.error(
|
|
741
|
+
`\nInstall failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
742
|
+
);
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
);
|
|
747
|
+
},
|
|
748
|
+
{ commands: ["agentar"] },
|
|
749
|
+
);
|
|
750
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "agentar",
|
|
3
|
+
"name": "Agentar Plugin",
|
|
4
|
+
"description": "Search, download, and install agent templates (agentars) from the marketplace.",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"apiBaseUrl": {
|
|
10
|
+
"type": "string"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"uiHints": {
|
|
15
|
+
"apiBaseUrl": {
|
|
16
|
+
"label": "API Base URL",
|
|
17
|
+
"help": "Base URL of the catchclaw-console backend (default: http://127.0.0.1:8080)"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@catch-claw/openclaw-agentar",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Search, download, and install agent templates (agentars) for OpenClaw",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"openclaw": {
|
|
8
|
+
"extensions": ["./index.ts"]
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"openclaw.plugin.json"
|
|
13
|
+
],
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"openclaw": "*"
|
|
16
|
+
}
|
|
17
|
+
}
|