@catch-claw/openclaw-agentar 1.0.0 → 1.1.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 CHANGED
@@ -54,6 +54,22 @@ type InstallResult = {
54
54
  introduction?: string | null;
55
55
  };
56
56
 
57
+ type AgentWorkspaceInfo = {
58
+ id: string;
59
+ name?: string;
60
+ workspace: string;
61
+ isDefault: boolean;
62
+ };
63
+
64
+ type ExportResult = {
65
+ success: boolean;
66
+ agentName: string;
67
+ workspace: string;
68
+ outputPath: string;
69
+ files: string[];
70
+ includeMemory: boolean;
71
+ };
72
+
57
73
  // ─── Constants ───────────────────────────────────────────────────────────────
58
74
 
59
75
  const DEFAULT_API_BASE_URL = "http://127.0.0.1:8080";
@@ -69,6 +85,7 @@ const WORKSPACE_FILES = [
69
85
  "MEMORY.md",
70
86
  ];
71
87
  const SKIP_FILES = ["AGENTS.md", "BOOTSTRAP.md"];
88
+ const EXPORT_SKIP_DIRS = [".git", ".openclaw", "__MACOSX", "memory"];
72
89
  const INTRO_MESSAGE =
73
90
  "请做一下自我介绍,简要说明你是谁、你擅长什么、你能帮用户做哪些事情。";
74
91
 
@@ -119,11 +136,26 @@ function box(text: string): void {
119
136
  // ─── Core Utilities ──────────────────────────────────────────────────────────
120
137
 
121
138
  function findOpenclawBin(): string {
139
+ const isWin = process.platform === "win32";
122
140
  try {
123
- return execSync("which openclaw", { encoding: "utf-8" }).trim();
141
+ return execSync(isWin ? "where openclaw" : "which openclaw", {
142
+ encoding: "utf-8",
143
+ })
144
+ .split(/\r?\n/)[0]
145
+ .trim();
124
146
  } catch {
125
- const pnpmPath = path.join(os.homedir(), ".local/share/pnpm/openclaw");
126
- if (fs.existsSync(pnpmPath)) return pnpmPath;
147
+ const fallbacks = isWin
148
+ ? [
149
+ path.join(
150
+ process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local"),
151
+ "pnpm",
152
+ "openclaw.cmd",
153
+ ),
154
+ ]
155
+ : [path.join(os.homedir(), ".local/share/pnpm/openclaw")];
156
+ for (const p of fallbacks) {
157
+ if (fs.existsSync(p)) return p;
158
+ }
127
159
  throw new Error("openclaw CLI not found on PATH");
128
160
  }
129
161
  }
@@ -199,6 +231,187 @@ function resolveContentDir(extractDir: string): string {
199
231
  return extractDir;
200
232
  }
201
233
 
234
+ function discoverAgents(): AgentWorkspaceInfo[] {
235
+ const openclawBin = findOpenclawBin();
236
+ try {
237
+ const stdout = execSync(`"${openclawBin}" agents list --json`, {
238
+ encoding: "utf-8",
239
+ stdio: "pipe",
240
+ });
241
+ const parsed = JSON.parse(stdout);
242
+ const agents: AgentWorkspaceInfo[] = [];
243
+
244
+ // Handle both array format and wrapped format
245
+ const list = Array.isArray(parsed) ? parsed : parsed.agents ?? [];
246
+ for (const entry of list) {
247
+ agents.push({
248
+ id: String(entry.id ?? ""),
249
+ name: entry.name ?? entry.identityName,
250
+ workspace: String(entry.workspace ?? ""),
251
+ isDefault: Boolean(entry.isDefault),
252
+ });
253
+ }
254
+
255
+ // Ensure at least the default main agent is present
256
+ if (agents.length === 0) {
257
+ agents.push({
258
+ id: "main",
259
+ workspace: MAIN_WORKSPACE,
260
+ isDefault: true,
261
+ });
262
+ }
263
+
264
+ return agents;
265
+ } catch {
266
+ // Fallback: return default main agent if CLI fails
267
+ return [
268
+ {
269
+ id: "main",
270
+ workspace: MAIN_WORKSPACE,
271
+ isDefault: true,
272
+ },
273
+ ];
274
+ }
275
+ }
276
+
277
+ function collectExportFiles(
278
+ workspaceDir: string,
279
+ includeMemory: boolean,
280
+ ): { tempDir: string; files: string[] } {
281
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "agentar-export-"));
282
+ const files: string[] = [];
283
+
284
+ for (const entry of fs.readdirSync(workspaceDir, { withFileTypes: true })) {
285
+ // 1. Skip metadata directories
286
+ if (entry.isDirectory() && EXPORT_SKIP_DIRS.includes(entry.name)) continue;
287
+
288
+ // 2. Skip skills/ (handled separately)
289
+ if (entry.name === "skills" && entry.isDirectory()) continue;
290
+
291
+ // 3. Skip files matching SKIP_FILES (AGENTS.md, BOOTSTRAP.md)
292
+ if (SKIP_FILES.includes(entry.name)) continue;
293
+
294
+ // 4. Conditionally skip MEMORY.md
295
+ if (entry.name === "MEMORY.md" && !includeMemory) continue;
296
+
297
+ // 5. Skip hidden files/directories
298
+ if (entry.name.startsWith(".")) continue;
299
+
300
+ // 6. Copy to temp directory
301
+ const src = path.join(workspaceDir, entry.name);
302
+ const dest = path.join(tempDir, entry.name);
303
+ if (entry.isDirectory()) {
304
+ fs.cpSync(src, dest, { recursive: true });
305
+ } else {
306
+ fs.copyFileSync(src, dest);
307
+ }
308
+ files.push(entry.name);
309
+ }
310
+
311
+ // 7. Handle skills/ separately
312
+ const skillsDir = path.join(workspaceDir, "skills");
313
+ if (fs.existsSync(skillsDir)) {
314
+ fs.cpSync(skillsDir, path.join(tempDir, "skills"), { recursive: true });
315
+ files.push("skills");
316
+ }
317
+
318
+ return { tempDir, files };
319
+ }
320
+
321
+ // ─── Export Pipeline ────────────────────────────────────────────────────────
322
+
323
+ async function exportAgentar(opts: {
324
+ agentName: string;
325
+ outputPath?: string;
326
+ includeMemory?: boolean;
327
+ quiet?: boolean;
328
+ }): Promise<ExportResult> {
329
+ const { agentName, includeMemory = false, quiet } = opts;
330
+ const log = quiet ? () => {} : step;
331
+ const logDetail = quiet ? () => {} : detail;
332
+ const total = 3;
333
+
334
+ // [1/3] Validate workspace
335
+ log(1, total, "Validating workspace ...");
336
+ const agents = discoverAgents();
337
+ const agent = agents.find((a) => a.id === agentName);
338
+ if (!agent) {
339
+ const available = agents.map((a) => a.id).join(", ");
340
+ throw new Error(
341
+ `Agent "${agentName}" not found. Available agents: ${available}`,
342
+ );
343
+ }
344
+
345
+ const workspaceDir = agent.workspace;
346
+ if (!fs.existsSync(workspaceDir)) {
347
+ throw new Error(
348
+ `Workspace directory does not exist: ${workspaceDir}`,
349
+ );
350
+ }
351
+ if (!fs.existsSync(path.join(workspaceDir, "SOUL.md"))) {
352
+ throw new Error(
353
+ `Invalid workspace: missing SOUL.md in ${workspaceDir}`,
354
+ );
355
+ }
356
+ logDetail("workspace", workspaceDir);
357
+ logDetail("SOUL.md", "found");
358
+
359
+ // [2/3] Collect files
360
+ log(2, total, "Collecting files ...");
361
+ const { tempDir, files } = collectExportFiles(workspaceDir, includeMemory);
362
+
363
+ // Preview collected content
364
+ const skillsDir = path.join(tempDir, "skills");
365
+ const skillCount = fs.existsSync(skillsDir)
366
+ ? fs
367
+ .readdirSync(skillsDir, { withFileTypes: true })
368
+ .filter((e) => e.isDirectory()).length
369
+ : 0;
370
+ const filesDesc = files.filter((f) => f !== "skills").join(", ");
371
+ logDetail(
372
+ "files",
373
+ skillCount > 0
374
+ ? `${filesDesc}, skills/ (${skillCount} skills)`
375
+ : filesDesc,
376
+ );
377
+
378
+ // [3/3] Create ZIP package
379
+ log(3, total, "Creating ZIP package ...");
380
+ const resolvedOutput = path.resolve(
381
+ opts.outputPath || `./${agentName}.zip`,
382
+ );
383
+ logDetail("output", resolvedOutput);
384
+
385
+ try {
386
+ if (process.platform === "win32") {
387
+ execSync(
388
+ `powershell -NoProfile -Command "Compress-Archive -Force -Path '${tempDir}\\*' -DestinationPath '${resolvedOutput}'"`,
389
+ { encoding: "utf-8" },
390
+ );
391
+ } else {
392
+ execSync(`cd "${tempDir}" && zip -r -q "${resolvedOutput}" .`, {
393
+ encoding: "utf-8",
394
+ });
395
+ }
396
+ } catch (err) {
397
+ fs.rmSync(tempDir, { recursive: true, force: true });
398
+ throw new Error(
399
+ `Failed to create ZIP: ${err instanceof Error ? err.message : String(err)}`,
400
+ );
401
+ }
402
+
403
+ fs.rmSync(tempDir, { recursive: true, force: true });
404
+
405
+ return {
406
+ success: true,
407
+ agentName,
408
+ workspace: workspaceDir,
409
+ outputPath: resolvedOutput,
410
+ files,
411
+ includeMemory,
412
+ };
413
+ }
414
+
202
415
  // ─── Install Pipeline ────────────────────────────────────────────────────────
203
416
 
204
417
  async function installAgentar(opts: {
@@ -257,9 +470,16 @@ async function installAgentar(opts: {
257
470
  const extractDir = path.join(tmpDir, "extracted");
258
471
  fs.mkdirSync(extractDir, { recursive: true });
259
472
  try {
260
- execSync(`unzip -o -q "${zipPath}" -d "${extractDir}"`, {
261
- encoding: "utf-8",
262
- });
473
+ if (process.platform === "win32") {
474
+ execSync(
475
+ `powershell -NoProfile -Command "Expand-Archive -Force -Path '${zipPath}' -DestinationPath '${extractDir}'"`,
476
+ { encoding: "utf-8" },
477
+ );
478
+ } else {
479
+ execSync(`unzip -o -q "${zipPath}" -d "${extractDir}"`, {
480
+ encoding: "utf-8",
481
+ });
482
+ }
263
483
  } catch {
264
484
  fs.rmSync(tmpDir, { recursive: true, force: true });
265
485
  throw new Error(`Failed to extract agentar zip for "${slug}"`);
@@ -476,6 +696,8 @@ export default function register(api: OpenClawPluginApi) {
476
696
  "1. When user asks to install/switch agent persona, use the `agentar_install` tool.",
477
697
  "2. Always confirm with the user before overwriting the main agent workspace.",
478
698
  "3. When installing, ask if the user wants to overwrite the main agent or create a new one.",
699
+ "4. When user asks to export/share/package an agent, use the `agentar_export` tool.",
700
+ "5. Call agentar_export without parameters first to show available agents, then let user choose.",
479
701
  ].join("\n"),
480
702
  }),
481
703
  { priority: 80 },
@@ -547,6 +769,78 @@ export default function register(api: OpenClawPluginApi) {
547
769
  { name: "agentar_install" },
548
770
  );
549
771
 
772
+ // ── Tool: agentar_export ──────────────────────────────────────────────
773
+
774
+ api.registerTool(
775
+ () => ({
776
+ name: "agentar_export",
777
+ description:
778
+ "Export an agent workspace as a distributable ZIP package. Call without agentName to list available agents first. Returns agent list when agentName is omitted, or export result when agentName is provided.",
779
+ parameters: {
780
+ type: "object" as const,
781
+ properties: {
782
+ agentName: {
783
+ type: "string",
784
+ description:
785
+ "Agent ID to export (e.g. 'main'). Omit to list available agents.",
786
+ },
787
+ outputPath: {
788
+ type: "string",
789
+ description:
790
+ "Output ZIP file path. Defaults to ./<agentName>.zip",
791
+ },
792
+ includeMemory: {
793
+ type: "boolean",
794
+ description:
795
+ "Whether to include MEMORY.md in the export (default: false)",
796
+ },
797
+ },
798
+ required: [],
799
+ },
800
+ execute: async (params: Record<string, unknown>) => {
801
+ const agentName = params.agentName
802
+ ? String(params.agentName)
803
+ : undefined;
804
+ const outputPath = params.outputPath
805
+ ? String(params.outputPath)
806
+ : undefined;
807
+ const includeMemory = Boolean(params.includeMemory ?? false);
808
+
809
+ // No agentName → return available agents list
810
+ if (!agentName) {
811
+ try {
812
+ const agents = discoverAgents();
813
+ return {
814
+ agents: agents.map((a) => ({
815
+ ...a,
816
+ exists: fs.existsSync(a.workspace),
817
+ })),
818
+ };
819
+ } catch (err) {
820
+ return {
821
+ error: `Failed to discover agents: ${err instanceof Error ? err.message : String(err)}`,
822
+ };
823
+ }
824
+ }
825
+
826
+ // agentName provided → execute export
827
+ try {
828
+ return await exportAgentar({
829
+ agentName,
830
+ outputPath,
831
+ includeMemory,
832
+ quiet: true,
833
+ });
834
+ } catch (err) {
835
+ return {
836
+ error: `Export failed: ${err instanceof Error ? err.message : String(err)}`,
837
+ };
838
+ }
839
+ },
840
+ }),
841
+ { name: "agentar_export" },
842
+ );
843
+
550
844
  // ── CLI: openclaw agentar ──────────────────────────────────────────────
551
845
 
552
846
  api.registerCli(
@@ -744,6 +1038,206 @@ export default function register(api: OpenClawPluginApi) {
744
1038
  }
745
1039
  },
746
1040
  );
1041
+
1042
+ // ── export ────────────────────────────────────────────────────────
1043
+
1044
+ agentarCmd
1045
+ .command("export")
1046
+ .description("Export an agent workspace as a ZIP package")
1047
+ .option("--agent <id>", "Agent ID to export")
1048
+ .option("-o, --output <path>", "Output ZIP file path")
1049
+ .option("--include-memory", "Include MEMORY.md in export")
1050
+ .option("--json", "Output result as JSON")
1051
+ .action(
1052
+ async (opts: {
1053
+ agent?: string;
1054
+ output?: string;
1055
+ includeMemory?: boolean;
1056
+ json?: boolean;
1057
+ }) => {
1058
+ try {
1059
+ // Discover available agents
1060
+ const agents = discoverAgents();
1061
+
1062
+ let selectedAgent: AgentWorkspaceInfo;
1063
+
1064
+ if (opts.agent) {
1065
+ // Non-interactive: use provided agent ID
1066
+ const found = agents.find((a) => a.id === opts.agent);
1067
+ if (!found) {
1068
+ const available = agents
1069
+ .map((a) => a.id)
1070
+ .join(", ");
1071
+ console.error(
1072
+ `Agent "${opts.agent}" not found. Available: ${available}`,
1073
+ );
1074
+ process.exit(1);
1075
+ }
1076
+ selectedAgent = found;
1077
+ } else {
1078
+ // Interactive: let user choose
1079
+ console.log("\n⟩ Discovering agents ...\n");
1080
+ console.log(" Available agents:");
1081
+ for (let i = 0; i < agents.length; i++) {
1082
+ const a = agents[i];
1083
+ const exists = fs.existsSync(a.workspace);
1084
+ const defaultTag = a.isDefault ? " [default]" : "";
1085
+ const notFound = exists ? "" : " (not found)";
1086
+ console.log(
1087
+ ` [${i + 1}] ${a.id} (${a.workspace})${defaultTag}${notFound}`,
1088
+ );
1089
+ }
1090
+
1091
+ const readline = await import("node:readline");
1092
+ const rl = readline.createInterface({
1093
+ input: process.stdin,
1094
+ output: process.stdout,
1095
+ });
1096
+
1097
+ const answer = await new Promise<string>((resolve) => {
1098
+ rl.question(
1099
+ `\n Select agent (default: 1): `,
1100
+ (ans: string) => {
1101
+ rl.close();
1102
+ resolve(ans.trim());
1103
+ },
1104
+ );
1105
+ });
1106
+
1107
+ const idx = answer ? parseInt(answer, 10) - 1 : 0;
1108
+ if (idx < 0 || idx >= agents.length) {
1109
+ console.error("Invalid selection.");
1110
+ process.exit(1);
1111
+ }
1112
+ selectedAgent = agents[idx];
1113
+ }
1114
+
1115
+ // Include memory?
1116
+ let includeMemory = opts.includeMemory ?? false;
1117
+ if (!opts.agent && !opts.includeMemory) {
1118
+ const readline = await import("node:readline");
1119
+ const rl = readline.createInterface({
1120
+ input: process.stdin,
1121
+ output: process.stdout,
1122
+ });
1123
+ const memAnswer = await new Promise<string>((resolve) => {
1124
+ rl.question(
1125
+ " Include MEMORY.md? (y/N): ",
1126
+ (ans: string) => {
1127
+ rl.close();
1128
+ resolve(ans.trim().toLowerCase());
1129
+ },
1130
+ );
1131
+ });
1132
+ includeMemory = memAnswer === "y" || memAnswer === "yes";
1133
+ }
1134
+
1135
+ // Output path
1136
+ let outputPath = opts.output;
1137
+ if (!opts.agent && !opts.output) {
1138
+ const defaultPath = `./${selectedAgent.id}.zip`;
1139
+ const readline = await import("node:readline");
1140
+ const rl = readline.createInterface({
1141
+ input: process.stdin,
1142
+ output: process.stdout,
1143
+ });
1144
+ const pathAnswer = await new Promise<string>((resolve) => {
1145
+ rl.question(
1146
+ ` Output path (default: ${defaultPath}): `,
1147
+ (ans: string) => {
1148
+ rl.close();
1149
+ resolve(ans.trim());
1150
+ },
1151
+ );
1152
+ });
1153
+ outputPath = pathAnswer || defaultPath;
1154
+ }
1155
+
1156
+ // Check if output file already exists (interactive mode)
1157
+ const resolvedOutput = path.resolve(
1158
+ outputPath || `./${selectedAgent.id}.zip`,
1159
+ );
1160
+ if (!opts.agent && fs.existsSync(resolvedOutput)) {
1161
+ const readline = await import("node:readline");
1162
+ const rl = readline.createInterface({
1163
+ input: process.stdin,
1164
+ output: process.stdout,
1165
+ });
1166
+ const overwrite = await new Promise<string>((resolve) => {
1167
+ rl.question(
1168
+ ` File "${resolvedOutput}" already exists. Overwrite? (y/N): `,
1169
+ (ans: string) => {
1170
+ rl.close();
1171
+ resolve(ans.trim().toLowerCase());
1172
+ },
1173
+ );
1174
+ });
1175
+ if (overwrite !== "y" && overwrite !== "yes") {
1176
+ console.log(" Export cancelled.");
1177
+ return;
1178
+ }
1179
+ }
1180
+
1181
+ console.log(
1182
+ `\n⟩ Exporting agent "${selectedAgent.id}" ...`,
1183
+ );
1184
+
1185
+ const result = await exportAgentar({
1186
+ agentName: selectedAgent.id,
1187
+ outputPath,
1188
+ includeMemory,
1189
+ });
1190
+
1191
+ if (opts.json) {
1192
+ console.log(JSON.stringify(result, null, 2));
1193
+ return;
1194
+ }
1195
+
1196
+ success("Export complete");
1197
+ console.log(" Summary:");
1198
+ console.log(` agent ${result.agentName}`);
1199
+ console.log(` workspace ${result.workspace}`);
1200
+ console.log(` output ${result.outputPath}`);
1201
+ if (result.files.length > 0) {
1202
+ const skillsEntry = result.files.includes("skills");
1203
+ const nonSkillFiles = result.files.filter(
1204
+ (f) => f !== "skills",
1205
+ );
1206
+ let filesLine = nonSkillFiles.join(", ");
1207
+ if (skillsEntry) {
1208
+ const exportSkillsDir = path.join(
1209
+ result.workspace,
1210
+ "skills",
1211
+ );
1212
+ const skillCount = fs.existsSync(exportSkillsDir)
1213
+ ? fs
1214
+ .readdirSync(exportSkillsDir, {
1215
+ withFileTypes: true,
1216
+ })
1217
+ .filter((e) => e.isDirectory()).length
1218
+ : 0;
1219
+ filesLine +=
1220
+ skillCount > 0
1221
+ ? `, skills/ (${skillCount} skills)`
1222
+ : ", skills/";
1223
+ }
1224
+ console.log(` files ${filesLine}`);
1225
+ }
1226
+ if (result.includeMemory) {
1227
+ console.log(" memory included");
1228
+ }
1229
+
1230
+ console.log(
1231
+ `\n Use "openclaw agentar install <slug>" with the exported ZIP to install elsewhere.\n`,
1232
+ );
1233
+ } catch (err) {
1234
+ console.error(
1235
+ `\nExport failed: ${err instanceof Error ? err.message : String(err)}`,
1236
+ );
1237
+ process.exit(1);
1238
+ }
1239
+ },
1240
+ );
747
1241
  },
748
1242
  { commands: ["agentar"] },
749
1243
  );
@@ -1,5 +1,5 @@
1
1
  {
2
- "id": "agentar",
2
+ "id": "openclaw-agentar",
3
3
  "name": "Agentar Plugin",
4
4
  "description": "Search, download, and install agent templates (agentars) from the marketplace.",
5
5
  "configSchema": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@catch-claw/openclaw-agentar",
3
- "version": "1.0.0",
4
- "description": "Search, download, and install agent templates (agentars) for OpenClaw",
3
+ "version": "1.1.0",
4
+ "description": "Search, download, install, and export agent templates (agentars) for OpenClaw",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "openclaw": {