clawmarketbot 0.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.
Files changed (3) hide show
  1. package/README.md +70 -0
  2. package/dist/index.js +1090 -0
  3. package/package.json +47 -0
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # clawmarketbot
2
+
3
+ CLI for [ClawMarket](https://clawmarket.cc) — download and install OpenClaw agent configs directly from the marketplace or from npm.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 18+
8
+ - OpenClaw installed and configured (`~/.openclaw/openclaw.json` must exist)
9
+ - `unzip` or `tar` available in your PATH
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g clawmarketbot
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ### Install a bot from an npm package
20
+
21
+ ```bash
22
+ clawmarketbot config install-npm @clawmarket/my-bot
23
+ ```
24
+
25
+ Optionally pin a version:
26
+
27
+ ```bash
28
+ clawmarketbot config install-npm @clawmarket/my-bot@1.2.0
29
+ ```
30
+
31
+ ### Install a bot from a one-time download token
32
+
33
+ ```bash
34
+ clawmarketbot config install <token>
35
+ ```
36
+
37
+ Tokens are obtained from the ClawMarket download modal.
38
+
39
+ ### Download a config artifact to disk
40
+
41
+ ```bash
42
+ clawmarketbot config download <token>
43
+ ```
44
+
45
+ ### Auth
46
+
47
+ ```bash
48
+ clawmarketbot auth login # save an API key
49
+ clawmarketbot auth status # check current auth
50
+ clawmarketbot auth logout # clear credentials
51
+ ```
52
+
53
+ ## Environment Variables
54
+
55
+ | Variable | Default | Purpose |
56
+ |---|---|---|
57
+ | `CLAWMARKET_API_URL` | `https://api.clawmarket.cc` | ClawMarket backend URL |
58
+ | `OPENCLAW_HOME` | `os.homedir()` | OpenClaw home directory |
59
+ | `OPENCLAW_STATE_DIR` | `~/.openclaw` | OpenClaw state directory |
60
+ | `OPENCLAW_CONFIG_PATH` | `~/.openclaw/openclaw.json` | OpenClaw config file |
61
+
62
+ ## How npm-based install works
63
+
64
+ When you run `clawmarketbot config install-npm @clawmarket/some-bot`:
65
+
66
+ 1. Package metadata is fetched from the npm registry
67
+ 2. The tarball is downloaded and its integrity verified
68
+ 3. A `bot.zip` bundled inside the package is extracted
69
+ 4. The bot's `installer.sh` is run, registering the agent in your OpenClaw config
70
+ 5. All temp files are cleaned up automatically
package/dist/index.js ADDED
@@ -0,0 +1,1090 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/auth.ts
7
+ import fetch from "node-fetch";
8
+
9
+ // src/lib/prompts.ts
10
+ import { checkbox, confirm as promptConfirm, input, password, select } from "@inquirer/prompts";
11
+
12
+ // src/lib/constants.ts
13
+ var SECRET_FIELD_NAMES = new Set(
14
+ [
15
+ "apiKey",
16
+ "api_key",
17
+ "apikey",
18
+ "key",
19
+ "secret",
20
+ "token",
21
+ "accessToken",
22
+ "access_token",
23
+ "botToken",
24
+ "bot_token",
25
+ "appToken",
26
+ "app_token",
27
+ "signingSecret",
28
+ "clientSecret",
29
+ "client_secret",
30
+ "password",
31
+ "credential",
32
+ "privateKey",
33
+ "private_key",
34
+ "sessionToken",
35
+ "session_token"
36
+ ].map((key) => key.toLowerCase())
37
+ );
38
+
39
+ // src/lib/prompts.ts
40
+ async function askWithDefault(question, defaultValue) {
41
+ const value = await input({
42
+ message: question.trim() || "Input",
43
+ default: defaultValue
44
+ });
45
+ return value.trim() || defaultValue.trim();
46
+ }
47
+ async function askSecret(question) {
48
+ const value = await password({ message: question.trim() || "Secret", mask: true });
49
+ return value.trim();
50
+ }
51
+
52
+ // src/lib/auth-config.ts
53
+ import path from "path";
54
+ import fs2 from "fs/promises";
55
+ import os from "os";
56
+
57
+ // src/lib/fs.ts
58
+ import fs from "fs/promises";
59
+ async function fileExists(filePath) {
60
+ try {
61
+ await fs.access(filePath);
62
+ return true;
63
+ } catch {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ // src/lib/auth-config.ts
69
+ function authConfigDir() {
70
+ return path.join(os.homedir(), ".clawmarket");
71
+ }
72
+ function authConfigPath() {
73
+ return path.join(authConfigDir(), "clawmarket.json");
74
+ }
75
+ async function readStoredApiKey() {
76
+ const configPath = authConfigPath();
77
+ if (!await fileExists(configPath)) return null;
78
+ try {
79
+ const raw = await fs2.readFile(configPath, "utf-8");
80
+ const parsed = JSON.parse(raw);
81
+ const apiKey = typeof parsed.api_key === "string" ? parsed.api_key.trim() : "";
82
+ return apiKey.length > 0 ? apiKey : null;
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+ async function writeStoredApiKey(apiKey) {
88
+ const configDir = authConfigDir();
89
+ const configPath = authConfigPath();
90
+ await fs2.mkdir(configDir, { recursive: true, mode: 448 });
91
+ await fs2.chmod(configDir, 448).catch(() => void 0);
92
+ const payload = { api_key: apiKey };
93
+ await fs2.writeFile(configPath, JSON.stringify(payload, null, 2), { mode: 384 });
94
+ await fs2.chmod(configPath, 384).catch(() => void 0);
95
+ }
96
+ async function clearStoredApiKey() {
97
+ const configPath = authConfigPath();
98
+ if (!await fileExists(configPath)) return false;
99
+ await fs2.unlink(configPath);
100
+ return true;
101
+ }
102
+
103
+ // src/commands/auth.ts
104
+ var DEFAULT_SERVER = process.env.CLAWMARKET_API_URL || "http://localhost:8000";
105
+ function resolveUsername(payload) {
106
+ return payload.data?.user?.username || payload.user?.username || "user";
107
+ }
108
+ function resolveError(payload, fallback) {
109
+ return payload.error || payload.message || fallback;
110
+ }
111
+ async function readJsonSafe(response) {
112
+ const text = await response.text();
113
+ if (!text.trim()) return {};
114
+ try {
115
+ return JSON.parse(text);
116
+ } catch {
117
+ return { message: text };
118
+ }
119
+ }
120
+ async function readApiKeyFromStdin() {
121
+ if (process.stdin.isTTY) {
122
+ throw new Error("`--stdin` requires piped input");
123
+ }
124
+ const chunks = [];
125
+ for await (const chunk of process.stdin) {
126
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
127
+ }
128
+ return Buffer.concat(chunks).toString("utf-8").trim();
129
+ }
130
+ async function resolveApiKeyInput(apiKeyArg, fromStdin) {
131
+ if (apiKeyArg && apiKeyArg.trim()) {
132
+ console.warn("Warning: using `<api-key>` argument stores it in shell history. Prefer prompt input or `--stdin`.");
133
+ return apiKeyArg.trim();
134
+ }
135
+ if (fromStdin) {
136
+ const fromPipe = await readApiKeyFromStdin();
137
+ if (!fromPipe) throw new Error("No API key received from stdin");
138
+ return fromPipe;
139
+ }
140
+ const entered = await askSecret("API key");
141
+ if (!entered) throw new Error("API key is required");
142
+ return entered;
143
+ }
144
+ async function validateApiKey(server, apiKey) {
145
+ const response = await fetch(`${server}/auth/api-key`, {
146
+ method: "POST",
147
+ headers: { "Content-Type": "application/json" },
148
+ body: JSON.stringify({ api_key: apiKey })
149
+ });
150
+ const payload = await readJsonSafe(response);
151
+ return { ok: response.ok, payload };
152
+ }
153
+ function registerAuthCommands(program2) {
154
+ const authCommand = program2.command("auth").description("Manage authentication");
155
+ authCommand.command("login").description("Login with your ClawMarket API key").argument("[api-key]", "API key (less secure; shows in shell history)").option("--stdin", "Read API key from stdin (recommended for automation)").option("-s, --server <url>", "Server URL", DEFAULT_SERVER).action(async (apiKeyArg, options) => {
156
+ console.log("Validating API key...");
157
+ try {
158
+ const apiKey = await resolveApiKeyInput(apiKeyArg, Boolean(options.stdin));
159
+ const validation = await validateApiKey(options.server, apiKey);
160
+ if (!validation.ok) {
161
+ console.error(`Error: ${resolveError(validation.payload, "Invalid API key")}`);
162
+ process.exit(1);
163
+ }
164
+ const username = resolveUsername(validation.payload);
165
+ await writeStoredApiKey(apiKey);
166
+ console.log(`
167
+ Hey ${username}! You're now logged in.
168
+ `);
169
+ } catch (error) {
170
+ const message = error instanceof Error ? error.message : "";
171
+ if (message.includes("`--stdin`") || message.includes("No API key received") || message.includes("API key is required")) {
172
+ console.error(message);
173
+ process.exit(1);
174
+ }
175
+ console.error("Failed to connect to server. Are you online?");
176
+ process.exit(1);
177
+ }
178
+ });
179
+ authCommand.command("status").description("Check authentication status").option("-s, --server <url>", "Server URL", DEFAULT_SERVER).action(async (options) => {
180
+ const apiKey = await readStoredApiKey();
181
+ if (!apiKey) {
182
+ console.log("Not logged in. Run: clawmarketbot auth login");
183
+ return;
184
+ }
185
+ try {
186
+ const validation = await validateApiKey(options.server, apiKey);
187
+ if (!validation.ok) {
188
+ console.log("Session expired. Run: clawmarketbot auth login");
189
+ return;
190
+ }
191
+ const username = resolveUsername(validation.payload);
192
+ console.log(`Logged in as ${username}`);
193
+ } catch {
194
+ console.log("Logged in (could not verify with server)");
195
+ }
196
+ });
197
+ authCommand.command("logout").description("Logout and clear credentials").action(async () => {
198
+ const removed = await clearStoredApiKey();
199
+ console.log(removed ? "Logged out successfully" : "Not logged in");
200
+ });
201
+ }
202
+
203
+ // src/commands/config.ts
204
+ import fs5 from "fs/promises";
205
+ import path4 from "path";
206
+ import os3 from "os";
207
+ import { createHash as createHash2, randomUUID } from "crypto";
208
+ import { spawn as spawn2 } from "child_process";
209
+ import JSON52 from "json5";
210
+ import fetch3 from "node-fetch";
211
+
212
+ // ../../contracts/token.js
213
+ var CLAWMARKET_DOWNLOAD_TOKEN_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}";
214
+ var CLAWMARKET_DOWNLOAD_TOKEN_REGEX = new RegExp(
215
+ `^${CLAWMARKET_DOWNLOAD_TOKEN_PATTERN}$`,
216
+ "i"
217
+ );
218
+ var CLAWMARKET_ARTIFACT_SHA256_HEADER = "x-clawmarket-sha256";
219
+ function isClawMarketDownloadToken(value) {
220
+ if (typeof value !== "string") return false;
221
+ return CLAWMARKET_DOWNLOAD_TOKEN_REGEX.test(value.trim());
222
+ }
223
+
224
+ // src/lib/openclaw.ts
225
+ import path2 from "path";
226
+ import fs3 from "fs/promises";
227
+ import os2 from "os";
228
+ import JSON5 from "json5";
229
+ var MAX_INCLUDE_DEPTH = 10;
230
+ var DEBUG_LOGGING = process.env.CLAWMARKET_DEBUG === "1";
231
+ function debug(message) {
232
+ if (!DEBUG_LOGGING) return;
233
+ console.error(`[clawmarketbot][debug] ${message}`);
234
+ }
235
+ function asObject(value) {
236
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
237
+ return value;
238
+ }
239
+ function resolveTilde(inputPath, homeDir) {
240
+ if (inputPath === "~") return homeDir;
241
+ if (inputPath.startsWith("~/")) return path2.join(homeDir, inputPath.slice(2));
242
+ return inputPath;
243
+ }
244
+ function resolvePathLike(value, fallback, homeDir, baseDir = process.cwd()) {
245
+ if (typeof value !== "string" || value.trim().length === 0) return fallback;
246
+ const expanded = resolveTilde(value, homeDir);
247
+ if (path2.isAbsolute(expanded)) return path2.normalize(expanded);
248
+ return path2.resolve(baseDir, expanded);
249
+ }
250
+ function isPlainObject(value) {
251
+ return !!value && typeof value === "object" && !Array.isArray(value);
252
+ }
253
+ function deepMerge(base, override) {
254
+ const result = { ...base };
255
+ for (const [key, value] of Object.entries(override)) {
256
+ const baseValue = result[key];
257
+ if (isPlainObject(baseValue) && isPlainObject(value)) {
258
+ result[key] = deepMerge(baseValue, value);
259
+ continue;
260
+ }
261
+ result[key] = value;
262
+ }
263
+ return result;
264
+ }
265
+ async function parseJson5File(filePath) {
266
+ const content = await fs3.readFile(filePath, "utf-8");
267
+ let parsed;
268
+ try {
269
+ parsed = JSON5.parse(content);
270
+ } catch (err) {
271
+ const reason = err instanceof Error ? err.message : "invalid JSON5";
272
+ throw new Error(`Failed to parse OpenClaw config "${filePath}": ${reason}`);
273
+ }
274
+ if (!isPlainObject(parsed)) {
275
+ throw new Error(`OpenClaw config "${filePath}" must be an object`);
276
+ }
277
+ return parsed;
278
+ }
279
+ async function resolveIncludeTarget(includePath, includingFilePath, depth, stack) {
280
+ const rawPath = resolveTilde(includePath, os2.homedir());
281
+ const absPath = path2.isAbsolute(rawPath) ? path2.normalize(rawPath) : path2.resolve(path2.dirname(includingFilePath), rawPath);
282
+ if (!absPath) {
283
+ throw new Error(`Invalid include path "${includePath}" in "${includingFilePath}"`);
284
+ }
285
+ if (stack.includes(absPath)) {
286
+ throw new Error(`Circular OpenClaw config include detected: ${[...stack, absPath].join(" -> ")}`);
287
+ }
288
+ if (!await fileExists(absPath)) {
289
+ throw new Error(`Included OpenClaw config file not found: ${absPath}`);
290
+ }
291
+ const parsed = await parseJson5File(absPath);
292
+ return resolveIncludes(parsed, absPath, depth + 1, [...stack, absPath]);
293
+ }
294
+ async function resolveIncludes(value, currentFilePath, depth, stack) {
295
+ if (depth > MAX_INCLUDE_DEPTH) {
296
+ throw new Error(`OpenClaw config include depth exceeded (${MAX_INCLUDE_DEPTH}) at "${currentFilePath}"`);
297
+ }
298
+ if (!isPlainObject(value)) return {};
299
+ let includedValue = null;
300
+ if ("$include" in value) {
301
+ const includeSpec = value.$include;
302
+ if (typeof includeSpec === "string") {
303
+ includedValue = await resolveIncludeTarget(includeSpec, currentFilePath, depth, stack);
304
+ } else if (Array.isArray(includeSpec)) {
305
+ includedValue = {};
306
+ for (const includePath of includeSpec) {
307
+ if (typeof includePath !== "string") {
308
+ throw new Error(`Invalid $include entry in "${currentFilePath}" (expected string paths)`);
309
+ }
310
+ const resolved = await resolveIncludeTarget(includePath, currentFilePath, depth, stack);
311
+ includedValue = deepMerge(includedValue, resolved);
312
+ }
313
+ } else {
314
+ throw new Error(`Invalid $include value in "${currentFilePath}" (expected string or array)`);
315
+ }
316
+ }
317
+ const resolvedSiblings = {};
318
+ for (const [key, child] of Object.entries(value)) {
319
+ if (key === "$include") continue;
320
+ if (isPlainObject(child)) {
321
+ resolvedSiblings[key] = await resolveIncludes(child, currentFilePath, depth, stack);
322
+ } else if (Array.isArray(child)) {
323
+ const resolvedItems = await Promise.all(
324
+ child.map(
325
+ async (item) => isPlainObject(item) ? resolveIncludes(item, currentFilePath, depth, stack) : item
326
+ )
327
+ );
328
+ resolvedSiblings[key] = resolvedItems;
329
+ } else {
330
+ resolvedSiblings[key] = child;
331
+ }
332
+ }
333
+ if (!includedValue) return resolvedSiblings;
334
+ return deepMerge(includedValue, resolvedSiblings);
335
+ }
336
+ function parseAgentName(agentConfig) {
337
+ const identity = asObject(agentConfig.identity);
338
+ if (identity && typeof identity.name === "string" && identity.name.trim()) {
339
+ return identity.name.trim();
340
+ }
341
+ if (typeof agentConfig.name === "string" && agentConfig.name.trim()) {
342
+ return agentConfig.name.trim();
343
+ }
344
+ return null;
345
+ }
346
+ function parseAgents(config, paths) {
347
+ const agentsRoot = asObject(config.agents);
348
+ const defaults = asObject(agentsRoot?.defaults);
349
+ const configDir = path2.dirname(paths.configPath);
350
+ const defaultWorkspace = resolvePathLike(
351
+ defaults?.workspace,
352
+ path2.join(paths.stateDir, "workspace"),
353
+ paths.homeDir,
354
+ configDir
355
+ );
356
+ const list = Array.isArray(agentsRoot?.list) ? agentsRoot?.list : [];
357
+ if (!list || list.length === 0) {
358
+ const defaultAgent = {
359
+ id: "main",
360
+ name: "Main Agent",
361
+ workspace: defaultWorkspace,
362
+ agentDir: path2.join(paths.stateDir, "agents", "main", "agent"),
363
+ isDefault: true
364
+ };
365
+ return { agents: [defaultAgent], defaultAgentId: defaultAgent.id };
366
+ }
367
+ const agents = [];
368
+ for (const item of list) {
369
+ if (!isPlainObject(item)) continue;
370
+ const id = typeof item.id === "string" ? item.id.trim() : "";
371
+ if (!id) continue;
372
+ const workspaceFallback = defaultWorkspace;
373
+ const workspace = resolvePathLike(item.workspace, workspaceFallback, paths.homeDir, configDir);
374
+ const agentDirFallback = path2.join(paths.stateDir, "agents", id, "agent");
375
+ const agentDir = resolvePathLike(item.agentDir, agentDirFallback, paths.homeDir, configDir);
376
+ const name = parseAgentName(item) || id;
377
+ const isDefault = item.default === true;
378
+ agents.push({ id, name, workspace, agentDir, isDefault });
379
+ }
380
+ if (agents.length === 0) {
381
+ throw new Error("OpenClaw config contains agents.list but no valid agent entries");
382
+ }
383
+ const explicitDefault = agents.find((agent) => agent.isDefault);
384
+ const defaultAgentId = explicitDefault?.id || agents[0].id;
385
+ const normalizedAgents = agents.map((agent) => ({
386
+ ...agent,
387
+ isDefault: agent.id === defaultAgentId
388
+ }));
389
+ return { agents: normalizedAgents, defaultAgentId };
390
+ }
391
+ async function parseSkillName(skillFilePath, fallbackName) {
392
+ try {
393
+ const content = await fs3.readFile(skillFilePath, "utf-8");
394
+ if (!content.startsWith("---")) return fallbackName;
395
+ const lines = content.split(/\r?\n/).slice(1);
396
+ for (const line of lines) {
397
+ if (line.trim() === "---") break;
398
+ const nameMatch = line.match(/^\s*name\s*:\s*(.+)\s*$/);
399
+ if (nameMatch) {
400
+ return nameMatch[1].trim().replace(/^['"]|['"]$/g, "") || fallbackName;
401
+ }
402
+ }
403
+ return fallbackName;
404
+ } catch (error) {
405
+ const reason = error instanceof Error ? error.message : "unknown error";
406
+ debug(`Could not parse skill frontmatter "${skillFilePath}": ${reason}`);
407
+ return fallbackName;
408
+ }
409
+ }
410
+ async function discoverSkillsInDirectory(dirPath, source) {
411
+ if (!await fileExists(dirPath)) return [];
412
+ const entries = await fs3.readdir(dirPath, { withFileTypes: true }).catch(() => []);
413
+ const skills = [];
414
+ for (const entry of entries) {
415
+ if (!entry.isDirectory()) continue;
416
+ const skillDirPath = path2.join(dirPath, entry.name);
417
+ const skillFilePath = path2.join(skillDirPath, "SKILL.md");
418
+ if (!await fileExists(skillFilePath)) continue;
419
+ const key = entry.name;
420
+ const name = await parseSkillName(skillFilePath, key);
421
+ skills.push({
422
+ key,
423
+ name,
424
+ source,
425
+ path: skillDirPath,
426
+ referenceOnly: false
427
+ });
428
+ }
429
+ return skills;
430
+ }
431
+ function skillPriority(source) {
432
+ switch (source) {
433
+ case "workspace":
434
+ return 400;
435
+ case "managed":
436
+ return 300;
437
+ case "extra":
438
+ return 100;
439
+ case "config":
440
+ return 50;
441
+ default:
442
+ return 0;
443
+ }
444
+ }
445
+ async function discoverSkillsForAgent(agent, config, paths) {
446
+ const skillsRoot = asObject(config.skills);
447
+ const loadConfig = asObject(skillsRoot?.load);
448
+ const configDir = path2.dirname(paths.configPath);
449
+ const extraDirs = Array.isArray(loadConfig?.extraDirs) ? loadConfig.extraDirs.filter((v) => typeof v === "string") : [];
450
+ const aggregated = [];
451
+ aggregated.push(...await discoverSkillsInDirectory(path2.join(agent.workspace, "skills"), "workspace"));
452
+ aggregated.push(...await discoverSkillsInDirectory(paths.managedSkillsDir, "managed"));
453
+ for (const dir of extraDirs) {
454
+ const resolvedDir = resolvePathLike(dir, dir, paths.homeDir, configDir);
455
+ aggregated.push(...await discoverSkillsInDirectory(resolvedDir, "extra"));
456
+ }
457
+ const byKey = /* @__PURE__ */ new Map();
458
+ for (const skill of aggregated) {
459
+ const lowerKey = skill.key.toLowerCase();
460
+ const existing = byKey.get(lowerKey);
461
+ if (!existing || skillPriority(skill.source) > skillPriority(existing.source)) {
462
+ byKey.set(lowerKey, skill);
463
+ }
464
+ }
465
+ const entries = asObject(skillsRoot?.entries);
466
+ if (entries) {
467
+ for (const key of Object.keys(entries)) {
468
+ const normalized = key.toLowerCase();
469
+ if (!byKey.has(normalized)) {
470
+ byKey.set(normalized, {
471
+ key,
472
+ name: key,
473
+ source: "config",
474
+ referenceOnly: true
475
+ });
476
+ }
477
+ }
478
+ }
479
+ const allowBundled = Array.isArray(skillsRoot?.allowBundled) ? skillsRoot.allowBundled.filter((v) => typeof v === "string") : [];
480
+ for (const key of allowBundled) {
481
+ const normalized = key.toLowerCase();
482
+ if (!byKey.has(normalized)) {
483
+ byKey.set(normalized, {
484
+ key,
485
+ name: key,
486
+ source: "config",
487
+ referenceOnly: true
488
+ });
489
+ }
490
+ }
491
+ return [...byKey.values()].sort((a, b) => a.name.localeCompare(b.name));
492
+ }
493
+ function parseCronJobs(rawCron) {
494
+ const asArrayOrContainer = Array.isArray(rawCron) ? rawCron : isPlainObject(rawCron) && Array.isArray(rawCron.jobs) ? rawCron.jobs : [];
495
+ const jobs = [];
496
+ for (const item of asArrayOrContainer) {
497
+ if (!isPlainObject(item)) continue;
498
+ const jobIdRaw = typeof item.jobId === "string" ? item.jobId : typeof item.id === "string" ? item.id : null;
499
+ if (!jobIdRaw || !jobIdRaw.trim()) continue;
500
+ const nameRaw = typeof item.name === "string" && item.name.trim().length > 0 ? item.name : jobIdRaw;
501
+ const agentId = typeof item.agentId === "string" && item.agentId.trim() ? item.agentId : void 0;
502
+ const sessionTarget = typeof item.sessionTarget === "string" && item.sessionTarget.trim() ? item.sessionTarget : void 0;
503
+ jobs.push({
504
+ jobId: jobIdRaw.trim(),
505
+ name: nameRaw.trim(),
506
+ agentId,
507
+ sessionTarget,
508
+ schedule: item.schedule,
509
+ payload: item.payload,
510
+ delivery: item.delivery,
511
+ raw: item
512
+ });
513
+ }
514
+ return jobs;
515
+ }
516
+ async function readCronJobs(config, paths) {
517
+ const cronConfig = asObject(config.cron);
518
+ const configDir = path2.dirname(paths.configPath);
519
+ const cronStorePath = resolvePathLike(
520
+ cronConfig?.store,
521
+ path2.join(paths.stateDir, "cron", "jobs.json"),
522
+ paths.homeDir,
523
+ configDir
524
+ );
525
+ if (!await fileExists(cronStorePath)) return [];
526
+ try {
527
+ const raw = await fs3.readFile(cronStorePath, "utf-8");
528
+ const parsed = JSON5.parse(raw);
529
+ return parseCronJobs(parsed);
530
+ } catch (error) {
531
+ const reason = error instanceof Error ? error.message : "unknown error";
532
+ debug(`Failed reading cron jobs from "${cronStorePath}": ${reason}`);
533
+ return [];
534
+ }
535
+ }
536
+ function resolveOpenClawPaths() {
537
+ const osHome = os2.homedir();
538
+ const homeDir = resolveTilde(process.env.OPENCLAW_HOME || osHome, osHome);
539
+ const stateDir = resolvePathLike(process.env.OPENCLAW_STATE_DIR, path2.join(homeDir, ".openclaw"), osHome, osHome);
540
+ const configPath = resolvePathLike(
541
+ process.env.OPENCLAW_CONFIG_PATH,
542
+ path2.join(stateDir, "openclaw.json"),
543
+ osHome,
544
+ osHome
545
+ );
546
+ return {
547
+ homeDir,
548
+ stateDir,
549
+ configPath,
550
+ envPath: path2.join(stateDir, ".env"),
551
+ managedSkillsDir: path2.join(stateDir, "skills"),
552
+ cronStorePath: path2.join(stateDir, "cron", "jobs.json")
553
+ };
554
+ }
555
+ async function loadOpenClawContext() {
556
+ const paths = resolveOpenClawPaths();
557
+ if (!await fileExists(paths.configPath)) {
558
+ throw new Error(`OpenClaw config not found at "${paths.configPath}". Is OpenClaw installed?`);
559
+ }
560
+ const rawConfig = await parseJson5File(paths.configPath);
561
+ const resolvedConfig = await resolveIncludes(rawConfig, paths.configPath, 0, [path2.resolve(paths.configPath)]);
562
+ const { agents, defaultAgentId } = parseAgents(resolvedConfig, paths);
563
+ const skillsByAgent = {};
564
+ for (const agent of agents) {
565
+ skillsByAgent[agent.id] = await discoverSkillsForAgent(agent, resolvedConfig, paths);
566
+ }
567
+ const cronJobs = await readCronJobs(resolvedConfig, paths);
568
+ return {
569
+ paths,
570
+ rawConfig,
571
+ resolvedConfig,
572
+ agents,
573
+ defaultAgentId,
574
+ skillsByAgent,
575
+ cronJobs
576
+ };
577
+ }
578
+
579
+ // src/lib/npm-install.ts
580
+ import fs4 from "fs/promises";
581
+ import path3 from "path";
582
+ import { createHash } from "crypto";
583
+ import { spawn } from "child_process";
584
+ import fetch2 from "node-fetch";
585
+ function parseNpmPackageSpec(spec) {
586
+ if (spec.startsWith("@")) {
587
+ const versionAt2 = spec.indexOf("@", 1);
588
+ if (versionAt2 === -1) return { packageName: spec, version: "latest" };
589
+ return {
590
+ packageName: spec.slice(0, versionAt2),
591
+ version: spec.slice(versionAt2 + 1) || "latest"
592
+ };
593
+ }
594
+ const versionAt = spec.indexOf("@");
595
+ if (versionAt === -1) return { packageName: spec, version: "latest" };
596
+ return {
597
+ packageName: spec.slice(0, versionAt),
598
+ version: spec.slice(versionAt + 1) || "latest"
599
+ };
600
+ }
601
+ async function fetchNpmPackageVersion(packageName, version = "latest") {
602
+ const encodedName = packageName.startsWith("@") ? `@${encodeURIComponent(packageName.slice(1))}` : encodeURIComponent(packageName);
603
+ const url = `https://registry.npmjs.org/${encodedName}/${encodeURIComponent(version)}`;
604
+ const response = await fetch2(url, { headers: { Accept: "application/json" } });
605
+ if (!response.ok) {
606
+ if (response.status === 404) {
607
+ throw new Error(`npm package "${packageName}" not found in the registry.`);
608
+ }
609
+ throw new Error(`npm registry request failed: HTTP ${response.status}`);
610
+ }
611
+ const data = await response.json();
612
+ if (!data?.dist?.tarball) {
613
+ throw new Error(`npm package "${packageName}" has no tarball URL in registry metadata.`);
614
+ }
615
+ return data;
616
+ }
617
+ async function downloadNpmTarball(tarballUrl, destPath, expectedShasum) {
618
+ const response = await fetch2(tarballUrl);
619
+ if (!response.ok) {
620
+ throw new Error(`Failed to download tarball: HTTP ${response.status}`);
621
+ }
622
+ const bytes = Buffer.from(await response.arrayBuffer());
623
+ const actual = createHash("sha1").update(bytes).digest("hex");
624
+ if (actual !== expectedShasum) {
625
+ throw new Error(
626
+ `Tarball integrity check failed: expected ${expectedShasum}, got ${actual}.`
627
+ );
628
+ }
629
+ await fs4.writeFile(destPath, bytes);
630
+ }
631
+ async function extractNpmTarball(tarballPath, destDir) {
632
+ await new Promise((resolve, reject) => {
633
+ const child = spawn("tar", ["-xzf", tarballPath, "-C", destDir]);
634
+ let stderr = "";
635
+ child.stderr?.on("data", (chunk) => {
636
+ stderr += String(chunk);
637
+ });
638
+ child.on("error", (err) => {
639
+ if (err.code === "ENOENT") {
640
+ reject(new Error('"tar" is required to install npm packages but was not found in PATH.'));
641
+ } else {
642
+ reject(err);
643
+ }
644
+ });
645
+ child.on("close", (code) => {
646
+ if (code === 0) {
647
+ resolve();
648
+ } else {
649
+ reject(
650
+ new Error(`tar exited with code ${code}${stderr.trim() ? `: ${stderr.trim()}` : ""}`)
651
+ );
652
+ }
653
+ });
654
+ });
655
+ }
656
+ async function findBotZipInExtractedPackage(extractedDir) {
657
+ const packageRoot = path3.join(extractedDir, "package");
658
+ async function scan(dir) {
659
+ let entries;
660
+ try {
661
+ entries = await fs4.readdir(dir, { withFileTypes: true });
662
+ } catch {
663
+ return null;
664
+ }
665
+ for (const entry of entries) {
666
+ if (entry.isFile() && entry.name === "bot.zip") {
667
+ return path3.join(dir, entry.name);
668
+ }
669
+ }
670
+ for (const entry of entries) {
671
+ if (entry.isFile() && entry.name.endsWith(".zip")) {
672
+ return path3.join(dir, entry.name);
673
+ }
674
+ }
675
+ for (const entry of entries) {
676
+ if (!entry.isDirectory()) continue;
677
+ const found = await scan(path3.join(dir, entry.name));
678
+ if (found) return found;
679
+ }
680
+ return null;
681
+ }
682
+ return scan(packageRoot);
683
+ }
684
+
685
+ // src/commands/config.ts
686
+ function isValidDownloadToken(token) {
687
+ return isClawMarketDownloadToken(token);
688
+ }
689
+ function assertValidDownloadToken(token) {
690
+ if (!isValidDownloadToken(token)) {
691
+ throw new Error("Invalid token format. Expected UUID token from the download modal.");
692
+ }
693
+ }
694
+ async function resolveSafeDownloadPath(targetPath) {
695
+ const resolved = path4.resolve(targetPath);
696
+ if (!await fileExists(resolved)) {
697
+ return { outputPath: resolved, renamed: false };
698
+ }
699
+ const parsed = path4.parse(resolved);
700
+ for (let i = 1; i <= 1e3; i += 1) {
701
+ const candidate = path4.join(parsed.dir, `${parsed.name}-${i}${parsed.ext}`);
702
+ if (!await fileExists(candidate)) {
703
+ return { outputPath: candidate, renamed: true };
704
+ }
705
+ }
706
+ throw new Error(`Unable to choose a safe output filename for: ${resolved}`);
707
+ }
708
+ function getDefaultServer() {
709
+ return process.env.CLAWMARKET_API_URL || "http://localhost:8000";
710
+ }
711
+ function sha256(content) {
712
+ return createHash2("sha256").update(content).digest("hex");
713
+ }
714
+ function verifyDownloadedArtifactChecksum(content, checksumHeader) {
715
+ if (!checksumHeader) return;
716
+ const normalized = checksumHeader.trim().toLowerCase();
717
+ if (!/^[a-f0-9]{64}$/.test(normalized)) {
718
+ throw new Error("Download failed integrity check: invalid checksum header.");
719
+ }
720
+ const actual = sha256(content);
721
+ if (actual !== normalized) {
722
+ throw new Error("Download failed integrity check: checksum mismatch.");
723
+ }
724
+ }
725
+ function sanitizeSlugPart(value) {
726
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
727
+ }
728
+ function sanitizeFileName(value) {
729
+ const base = path4.basename(value || "").replace(/[^A-Za-z0-9._-]/g, "-");
730
+ return base || "download.bin";
731
+ }
732
+ function parseContentDispositionFileName(headerValue) {
733
+ if (!headerValue) return null;
734
+ const utf8Match = headerValue.match(/filename\*=UTF-8''([^;]+)/i);
735
+ if (utf8Match?.[1]) {
736
+ try {
737
+ return sanitizeFileName(decodeURIComponent(utf8Match[1]));
738
+ } catch {
739
+ return sanitizeFileName(utf8Match[1]);
740
+ }
741
+ }
742
+ const quoted = headerValue.match(/filename="([^"]+)"/i);
743
+ if (quoted?.[1]) return sanitizeFileName(quoted[1]);
744
+ const plain = headerValue.match(/filename=([^;]+)/i);
745
+ if (plain?.[1]) return sanitizeFileName(plain[1].trim());
746
+ return null;
747
+ }
748
+ function toStringOrUndefined(value) {
749
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
750
+ }
751
+ function validateAgentId(input2) {
752
+ return /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(input2);
753
+ }
754
+ async function readJsonError(response) {
755
+ const text = await response.text();
756
+ if (!text.trim()) return "Request failed";
757
+ try {
758
+ const parsed = JSON.parse(text);
759
+ return parsed.error || parsed.message || text;
760
+ } catch {
761
+ return text;
762
+ }
763
+ }
764
+ async function fetchDownloadArtifact(server, token) {
765
+ const response = await fetch3(`${server}/download/${encodeURIComponent(token)}?format=file`, {
766
+ method: "GET"
767
+ });
768
+ if (!response.ok) {
769
+ throw new Error(await readJsonError(response));
770
+ }
771
+ const bytes = Buffer.from(await response.arrayBuffer());
772
+ verifyDownloadedArtifactChecksum(bytes, response.headers.get(CLAWMARKET_ARTIFACT_SHA256_HEADER));
773
+ const fileFromHeader = parseContentDispositionFileName(response.headers.get("content-disposition"));
774
+ const fallbackFile = `clawmarket-${token}.zip`;
775
+ return {
776
+ bytes,
777
+ fileName: fileFromHeader || fallbackFile
778
+ };
779
+ }
780
+ async function runCommand(command, args, options = {}) {
781
+ return new Promise((resolve, reject) => {
782
+ const child = spawn2(command, args, {
783
+ cwd: options.cwd,
784
+ stdio: options.stdio === "inherit" ? "inherit" : "pipe"
785
+ });
786
+ let stdout = "";
787
+ let stderr = "";
788
+ if (child.stdout) {
789
+ child.stdout.on("data", (chunk) => {
790
+ stdout += String(chunk);
791
+ });
792
+ }
793
+ if (child.stderr) {
794
+ child.stderr.on("data", (chunk) => {
795
+ stderr += String(chunk);
796
+ });
797
+ }
798
+ child.on("error", (error) => {
799
+ reject(error);
800
+ });
801
+ child.on("close", (code) => {
802
+ if (code === 0) {
803
+ resolve({ stdout, stderr });
804
+ return;
805
+ }
806
+ const detail = stderr.trim() || stdout.trim();
807
+ reject(new Error(`${command} exited with code ${code}${detail ? `: ${detail}` : ""}`));
808
+ });
809
+ });
810
+ }
811
+ function isErrnoWithCode(error, code) {
812
+ return Boolean(error && typeof error === "object" && error.code === code);
813
+ }
814
+ async function extractZipArtifact(artifactPath, destinationDir) {
815
+ try {
816
+ await runCommand("unzip", ["-q", artifactPath, "-d", destinationDir]);
817
+ return;
818
+ } catch (error) {
819
+ if (!isErrnoWithCode(error, "ENOENT")) {
820
+ throw error;
821
+ }
822
+ }
823
+ try {
824
+ await runCommand("tar", ["-xf", artifactPath, "-C", destinationDir]);
825
+ } catch (error) {
826
+ if (isErrnoWithCode(error, "ENOENT")) {
827
+ throw new Error('Could not extract zip artifact: neither "unzip" nor "tar" is available.');
828
+ }
829
+ throw error;
830
+ }
831
+ }
832
+ async function findInstallerPackage(rootDir) {
833
+ const queue = [path4.resolve(rootDir)];
834
+ const seen = /* @__PURE__ */ new Set();
835
+ while (queue.length > 0) {
836
+ const current = queue.shift();
837
+ if (!current || seen.has(current)) continue;
838
+ seen.add(current);
839
+ const rootInstallerPath = path4.join(current, "installer.sh");
840
+ const sourceInstallerPath = path4.join(current, "source", "installer.sh");
841
+ const configPath = path4.join(current, "source", "bot-config.json");
842
+ if (await fileExists(rootInstallerPath) && await fileExists(configPath)) {
843
+ return {
844
+ packageRoot: current,
845
+ installerPath: rootInstallerPath,
846
+ botConfigPath: configPath,
847
+ layout: "root-installer"
848
+ };
849
+ }
850
+ if (await fileExists(sourceInstallerPath) && await fileExists(configPath)) {
851
+ return {
852
+ packageRoot: current,
853
+ installerPath: sourceInstallerPath,
854
+ botConfigPath: configPath,
855
+ layout: "source-installer"
856
+ };
857
+ }
858
+ let entries;
859
+ try {
860
+ entries = await fs5.readdir(current, { withFileTypes: true });
861
+ } catch {
862
+ continue;
863
+ }
864
+ for (const entry of entries) {
865
+ if (!entry.isDirectory()) continue;
866
+ if (entry.name === "." || entry.name === "..") continue;
867
+ queue.push(path4.join(current, entry.name));
868
+ }
869
+ }
870
+ return null;
871
+ }
872
+ async function ensureInstallableLayout(pkg) {
873
+ if (pkg.layout === "root-installer") {
874
+ return {
875
+ packageRoot: pkg.packageRoot,
876
+ installerScriptName: "installer.sh",
877
+ botConfigPath: pkg.botConfigPath
878
+ };
879
+ }
880
+ const promotedInstallerPath = path4.join(pkg.packageRoot, "installer.sh");
881
+ await fs5.copyFile(pkg.installerPath, promotedInstallerPath);
882
+ await fs5.chmod(promotedInstallerPath, 493).catch(() => void 0);
883
+ return {
884
+ packageRoot: pkg.packageRoot,
885
+ installerScriptName: "installer.sh",
886
+ botConfigPath: pkg.botConfigPath
887
+ };
888
+ }
889
+ async function readBotInstallerConfig(configPath) {
890
+ if (!await fileExists(configPath)) {
891
+ throw new Error(`Package is missing required config file: ${configPath}`);
892
+ }
893
+ const raw = await fs5.readFile(configPath, "utf-8");
894
+ try {
895
+ return JSON.parse(raw);
896
+ } catch (error) {
897
+ const reason = error instanceof Error ? error.message : "invalid JSON";
898
+ throw new Error(`Package config is invalid JSON (${configPath}): ${reason}`);
899
+ }
900
+ }
901
+ async function resolveAgentId(suggestedAgentId) {
902
+ const canPrompt = Boolean(process.stdin.isTTY && process.stdout.isTTY);
903
+ if (!canPrompt) {
904
+ throw new Error("Install requires an interactive terminal to choose the new agent id.");
905
+ }
906
+ const prompted = await askWithDefault("New OpenClaw agent id", suggestedAgentId);
907
+ if (!validateAgentId(prompted)) {
908
+ throw new Error('Invalid agent id. Use 1-64 chars: letters, numbers, "-" or "_" (must start with alphanumeric).');
909
+ }
910
+ return prompted;
911
+ }
912
+ function registerConfigCommands(program2) {
913
+ const configCommand = program2.command("config").description("Download and install published configs");
914
+ configCommand.command("download").description("Download a published config artifact from a one-time token").argument("<token>", "One-time download token").action(async (token) => {
915
+ try {
916
+ assertValidDownloadToken(token);
917
+ const server = getDefaultServer();
918
+ const response = await fetch3(`${server}/download/${encodeURIComponent(token)}?format=file`, {
919
+ method: "GET"
920
+ });
921
+ if (!response.ok) {
922
+ throw new Error(await readJsonError(response));
923
+ }
924
+ const fileFromHeader = parseContentDispositionFileName(response.headers.get("content-disposition"));
925
+ const fallbackFile = `clawmarket-${token}.bin`;
926
+ const preferredPath = path4.resolve(fileFromHeader || fallbackFile);
927
+ const { outputPath, renamed } = await resolveSafeDownloadPath(preferredPath);
928
+ const bytes = Buffer.from(await response.arrayBuffer());
929
+ verifyDownloadedArtifactChecksum(bytes, response.headers.get(CLAWMARKET_ARTIFACT_SHA256_HEADER));
930
+ await fs5.mkdir(path4.dirname(outputPath), { recursive: true });
931
+ await fs5.writeFile(outputPath, bytes);
932
+ if (renamed) {
933
+ console.log(`Existing file detected, saved as: ${outputPath}`);
934
+ }
935
+ console.log(`Downloaded artifact: ${outputPath}`);
936
+ } catch (error) {
937
+ const message = error instanceof Error ? error.message : "Download failed";
938
+ console.error(message);
939
+ process.exit(1);
940
+ }
941
+ });
942
+ configCommand.command("install").description("Install an agent config from a one-time token into a new OpenClaw agent").argument("<token>", "One-time download token").action(async (token) => {
943
+ try {
944
+ assertValidDownloadToken(token);
945
+ const server = getDefaultServer();
946
+ const context = await loadOpenClawContext();
947
+ const artifact = await fetchDownloadArtifact(server, token);
948
+ const tempRoot = await fs5.mkdtemp(path4.join(os3.tmpdir(), `.clawmarket-install-${randomUUID().slice(0, 8)}-`));
949
+ try {
950
+ const artifactPath = path4.join(tempRoot, sanitizeFileName(artifact.fileName));
951
+ const extractedPath = path4.join(tempRoot, "extracted");
952
+ await fs5.mkdir(extractedPath, { recursive: true });
953
+ await fs5.writeFile(artifactPath, artifact.bytes);
954
+ await extractZipArtifact(artifactPath, extractedPath);
955
+ const packageInfo = await findInstallerPackage(extractedPath);
956
+ if (!packageInfo) {
957
+ throw new Error(
958
+ "Downloaded artifact is not an installable bot package (missing installer.sh + source/bot-config.json or source/installer.sh + source/bot-config.json)."
959
+ );
960
+ }
961
+ const installLayout = await ensureInstallableLayout(packageInfo);
962
+ const botConfig = await readBotInstallerConfig(installLayout.botConfigPath);
963
+ if (typeof botConfig.installerSpecVersion === "number" && botConfig.installerSpecVersion !== 1) {
964
+ console.warn(
965
+ `Warning: installerSpecVersion=${botConfig.installerSpecVersion} (this CLI currently expects version 1).`
966
+ );
967
+ }
968
+ const configuredAgentId = toStringOrUndefined(botConfig.agent?.id) || "agent";
969
+ const suggestedAgentId = validateAgentId(configuredAgentId) ? configuredAgentId : sanitizeSlugPart(configuredAgentId) || "agent";
970
+ const agentId = await resolveAgentId(suggestedAgentId);
971
+ const agentName = toStringOrUndefined(botConfig.agent?.name) || agentId;
972
+ const existing = new Set(context.agents.map((agent) => agent.id.toLowerCase()));
973
+ if (existing.has(agentId.toLowerCase())) {
974
+ throw new Error(`Agent id "${agentId}" already exists. Choose another id.`);
975
+ }
976
+ console.log("Running packaged installer...");
977
+ await runCommand(
978
+ "bash",
979
+ [installLayout.installerScriptName, "--agent-id", agentId, "--agent-name", agentName, context.paths.stateDir],
980
+ { cwd: installLayout.packageRoot, stdio: "inherit" }
981
+ );
982
+ const refreshed = await loadOpenClawContext();
983
+ const installed = refreshed.agents.find((agent) => agent.id.toLowerCase() === agentId.toLowerCase());
984
+ if (!installed) {
985
+ throw new Error("Installer finished, but the new agent was not detected in openclaw.json.");
986
+ }
987
+ console.log("Installed package into OpenClaw as an additional agent");
988
+ console.log(` Agent ID: ${installed.id}`);
989
+ console.log(` Name: ${installed.name}`);
990
+ console.log(` Workspace: ${installed.workspace}`);
991
+ console.log(` Agent Dir: ${installed.agentDir}`);
992
+ console.log("\nNext steps:");
993
+ console.log(" 1) openclaw agents list");
994
+ console.log(" 2) openclaw gateway restart");
995
+ } finally {
996
+ await fs5.rm(tempRoot, { recursive: true, force: true }).catch(() => void 0);
997
+ }
998
+ } catch (error) {
999
+ const message = error instanceof Error ? error.message : "Install failed";
1000
+ console.error(message);
1001
+ process.exit(1);
1002
+ }
1003
+ });
1004
+ configCommand.command("install-npm").description("Install a bot config from an npm package (e.g. @clawmarket/my-bot)").argument("<package>", "npm package name, optionally with version (e.g. @clawmarket/my-bot or @clawmarket/my-bot@1.2.0)").action(async (packageSpec) => {
1005
+ try {
1006
+ const { packageName, version } = parseNpmPackageSpec(packageSpec);
1007
+ console.log(`Looking up ${packageSpec} in the npm registry...`);
1008
+ const context = await loadOpenClawContext();
1009
+ const pkgMeta = await fetchNpmPackageVersion(packageName, version);
1010
+ console.log(`Found ${pkgMeta.name}@${pkgMeta.version}`);
1011
+ const tempRoot = await fs5.mkdtemp(
1012
+ path4.join(os3.tmpdir(), `.clawmarket-npm-${randomUUID().slice(0, 8)}-`)
1013
+ );
1014
+ try {
1015
+ const tarballPath = path4.join(tempRoot, "package.tgz");
1016
+ console.log("Downloading package...");
1017
+ await downloadNpmTarball(pkgMeta.dist.tarball, tarballPath, pkgMeta.dist.shasum);
1018
+ const npmExtractedPath = path4.join(tempRoot, "npm-extracted");
1019
+ await fs5.mkdir(npmExtractedPath, { recursive: true });
1020
+ await extractNpmTarball(tarballPath, npmExtractedPath);
1021
+ const botZipPath = await findBotZipInExtractedPackage(npmExtractedPath);
1022
+ if (!botZipPath) {
1023
+ throw new Error(
1024
+ `npm package "${pkgMeta.name}" does not contain a bot.zip. Expected a bot.zip at the package root inside the tarball.`
1025
+ );
1026
+ }
1027
+ const botZipFileName = sanitizeFileName(path4.basename(botZipPath));
1028
+ const stagedZipPath = path4.join(tempRoot, botZipFileName);
1029
+ await fs5.copyFile(botZipPath, stagedZipPath);
1030
+ const botExtractedPath = path4.join(tempRoot, "bot-extracted");
1031
+ await fs5.mkdir(botExtractedPath, { recursive: true });
1032
+ await extractZipArtifact(stagedZipPath, botExtractedPath);
1033
+ const packageInfo = await findInstallerPackage(botExtractedPath);
1034
+ if (!packageInfo) {
1035
+ throw new Error(
1036
+ "Bot zip is not an installable package (missing installer.sh + source/bot-config.json)."
1037
+ );
1038
+ }
1039
+ const installLayout = await ensureInstallableLayout(packageInfo);
1040
+ const botConfig = await readBotInstallerConfig(installLayout.botConfigPath);
1041
+ if (typeof botConfig.installerSpecVersion === "number" && botConfig.installerSpecVersion !== 1) {
1042
+ console.warn(
1043
+ `Warning: installerSpecVersion=${botConfig.installerSpecVersion} (this CLI currently expects version 1).`
1044
+ );
1045
+ }
1046
+ const configuredAgentId = toStringOrUndefined(botConfig.agent?.id) || "agent";
1047
+ const suggestedAgentId = validateAgentId(configuredAgentId) ? configuredAgentId : sanitizeSlugPart(configuredAgentId) || "agent";
1048
+ const agentId = await resolveAgentId(suggestedAgentId);
1049
+ const agentName = toStringOrUndefined(botConfig.agent?.name) || agentId;
1050
+ const existing = new Set(context.agents.map((agent) => agent.id.toLowerCase()));
1051
+ if (existing.has(agentId.toLowerCase())) {
1052
+ throw new Error(`Agent id "${agentId}" already exists. Choose another id.`);
1053
+ }
1054
+ console.log("Running packaged installer...");
1055
+ await runCommand(
1056
+ "bash",
1057
+ [installLayout.installerScriptName, "--agent-id", agentId, "--agent-name", agentName, context.paths.stateDir],
1058
+ { cwd: installLayout.packageRoot, stdio: "inherit" }
1059
+ );
1060
+ const refreshed = await loadOpenClawContext();
1061
+ const installed = refreshed.agents.find((agent) => agent.id.toLowerCase() === agentId.toLowerCase());
1062
+ if (!installed) {
1063
+ throw new Error("Installer finished, but the new agent was not detected in openclaw.json.");
1064
+ }
1065
+ console.log(`
1066
+ Installed ${pkgMeta.name}@${pkgMeta.version} into OpenClaw as a new agent`);
1067
+ console.log(` Agent ID: ${installed.id}`);
1068
+ console.log(` Name: ${installed.name}`);
1069
+ console.log(` Workspace: ${installed.workspace}`);
1070
+ console.log(` Agent Dir: ${installed.agentDir}`);
1071
+ console.log("\nNext steps:");
1072
+ console.log(" 1) openclaw agents list");
1073
+ console.log(" 2) openclaw gateway restart");
1074
+ } finally {
1075
+ await fs5.rm(tempRoot, { recursive: true, force: true }).catch(() => void 0);
1076
+ }
1077
+ } catch (error) {
1078
+ const message = error instanceof Error ? error.message : "Install failed";
1079
+ console.error(message);
1080
+ process.exit(1);
1081
+ }
1082
+ });
1083
+ }
1084
+
1085
+ // src/index.ts
1086
+ var program = new Command();
1087
+ program.name("clawmarketbot").description("CLI tool for ClawMarket - discover, download, and install OpenClaw configs").version("0.1.0");
1088
+ registerAuthCommands(program);
1089
+ registerConfigCommands(program);
1090
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "clawmarketbot",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "CLI tool for ClawMarket - discover, download, and install OpenClaw configs",
6
+ "type": "module",
7
+ "bin": {
8
+ "clawmarketbot": "./dist/index.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "exports": {
12
+ ".": "./dist/index.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18.0.0"
20
+ },
21
+ "scripts": {
22
+ "clean": "rm -rf dist",
23
+ "build": "tsup",
24
+ "prepublishOnly": "npm run build",
25
+ "dev": "tsx src/index.ts",
26
+ "test": "tsx --test src/__tests__/**/*.test.ts"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "dependencies": {
32
+ "@inquirer/prompts": "^7.8.4",
33
+ "archiver": "^7.0.1",
34
+ "commander": "^12.0.0",
35
+ "form-data": "^4.0.0",
36
+ "json5": "^2.2.3",
37
+ "node-fetch": "^3.3.2"
38
+ },
39
+ "devDependencies": {
40
+ "@clawmarket/contracts": "file:../../contracts",
41
+ "@types/archiver": "^7.0.0",
42
+ "@types/node": "^22.0.0",
43
+ "tsup": "^8.0.0",
44
+ "tsx": "^4.19.0",
45
+ "typescript": "^5.7.0"
46
+ }
47
+ }