clawmarketbot 0.1.0 → 0.1.2

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 (2) hide show
  1. package/dist/index.js +383 -268
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -48,6 +48,17 @@ async function askSecret(question) {
48
48
  const value = await password({ message: question.trim() || "Secret", mask: true });
49
49
  return value.trim();
50
50
  }
51
+ async function askOptional(question, hintUrl) {
52
+ if (hintUrl) {
53
+ console.log(` Get one at: ${hintUrl}`);
54
+ }
55
+ const value = await input({ message: `${question.trim()} (Enter to skip)`, default: "" });
56
+ const trimmed = value.trim();
57
+ return trimmed.length > 0 ? trimmed : null;
58
+ }
59
+ async function confirm(question, defaultYes = true) {
60
+ return promptConfirm({ message: question, default: defaultYes });
61
+ }
51
62
 
52
63
  // src/lib/auth-config.ts
53
64
  import path from "path";
@@ -201,13 +212,13 @@ Hey ${username}! You're now logged in.
201
212
  }
202
213
 
203
214
  // src/commands/config.ts
204
- import fs5 from "fs/promises";
205
- import path4 from "path";
215
+ import fs4 from "fs/promises";
216
+ import path3 from "path";
206
217
  import os3 from "os";
207
- import { createHash as createHash2, randomUUID } from "crypto";
208
- import { spawn as spawn2 } from "child_process";
218
+ import { createHash, randomUUID } from "crypto";
219
+ import { spawn } from "child_process";
209
220
  import JSON52 from "json5";
210
- import fetch3 from "node-fetch";
221
+ import fetch2 from "node-fetch";
211
222
 
212
223
  // ../../contracts/token.js
213
224
  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}";
@@ -576,112 +587,6 @@ async function loadOpenClawContext() {
576
587
  };
577
588
  }
578
589
 
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
590
  // src/commands/config.ts
686
591
  function isValidDownloadToken(token) {
687
592
  return isClawMarketDownloadToken(token);
@@ -692,13 +597,13 @@ function assertValidDownloadToken(token) {
692
597
  }
693
598
  }
694
599
  async function resolveSafeDownloadPath(targetPath) {
695
- const resolved = path4.resolve(targetPath);
600
+ const resolved = path3.resolve(targetPath);
696
601
  if (!await fileExists(resolved)) {
697
602
  return { outputPath: resolved, renamed: false };
698
603
  }
699
- const parsed = path4.parse(resolved);
604
+ const parsed = path3.parse(resolved);
700
605
  for (let i = 1; i <= 1e3; i += 1) {
701
- const candidate = path4.join(parsed.dir, `${parsed.name}-${i}${parsed.ext}`);
606
+ const candidate = path3.join(parsed.dir, `${parsed.name}-${i}${parsed.ext}`);
702
607
  if (!await fileExists(candidate)) {
703
608
  return { outputPath: candidate, renamed: true };
704
609
  }
@@ -709,7 +614,7 @@ function getDefaultServer() {
709
614
  return process.env.CLAWMARKET_API_URL || "http://localhost:8000";
710
615
  }
711
616
  function sha256(content) {
712
- return createHash2("sha256").update(content).digest("hex");
617
+ return createHash("sha256").update(content).digest("hex");
713
618
  }
714
619
  function verifyDownloadedArtifactChecksum(content, checksumHeader) {
715
620
  if (!checksumHeader) return;
@@ -726,7 +631,7 @@ function sanitizeSlugPart(value) {
726
631
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
727
632
  }
728
633
  function sanitizeFileName(value) {
729
- const base = path4.basename(value || "").replace(/[^A-Za-z0-9._-]/g, "-");
634
+ const base = path3.basename(value || "").replace(/[^A-Za-z0-9._-]/g, "-");
730
635
  return base || "download.bin";
731
636
  }
732
637
  function parseContentDispositionFileName(headerValue) {
@@ -745,6 +650,17 @@ function parseContentDispositionFileName(headerValue) {
745
650
  if (plain?.[1]) return sanitizeFileName(plain[1].trim());
746
651
  return null;
747
652
  }
653
+ function normalizeCronJobs(raw) {
654
+ if (Array.isArray(raw)) {
655
+ return raw.filter((item) => !!item && typeof item === "object");
656
+ }
657
+ if (raw && typeof raw === "object" && Array.isArray(raw.jobs)) {
658
+ return raw.jobs.filter(
659
+ (item) => !!item && typeof item === "object"
660
+ );
661
+ }
662
+ return [];
663
+ }
748
664
  function toStringOrUndefined(value) {
749
665
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
750
666
  }
@@ -762,7 +678,7 @@ async function readJsonError(response) {
762
678
  }
763
679
  }
764
680
  async function fetchDownloadArtifact(server, token) {
765
- const response = await fetch3(`${server}/download/${encodeURIComponent(token)}?format=file`, {
681
+ const response = await fetch2(`${server}/download/${encodeURIComponent(token)}?format=file`, {
766
682
  method: "GET"
767
683
  });
768
684
  if (!response.ok) {
@@ -779,7 +695,7 @@ async function fetchDownloadArtifact(server, token) {
779
695
  }
780
696
  async function runCommand(command, args, options = {}) {
781
697
  return new Promise((resolve, reject) => {
782
- const child = spawn2(command, args, {
698
+ const child = spawn(command, args, {
783
699
  cwd: options.cwd,
784
700
  stdio: options.stdio === "inherit" ? "inherit" : "pipe"
785
701
  });
@@ -829,73 +745,251 @@ async function extractZipArtifact(artifactPath, destinationDir) {
829
745
  throw error;
830
746
  }
831
747
  }
832
- async function findInstallerPackage(rootDir) {
833
- const queue = [path4.resolve(rootDir)];
748
+ async function readBotInstallerConfig(configPath) {
749
+ if (!await fileExists(configPath)) {
750
+ throw new Error(`Package is missing required config file: ${configPath}`);
751
+ }
752
+ const raw = await fs4.readFile(configPath, "utf-8");
753
+ try {
754
+ return JSON.parse(raw);
755
+ } catch (error) {
756
+ const reason = error instanceof Error ? error.message : "invalid JSON";
757
+ throw new Error(`Package config is invalid JSON (${configPath}): ${reason}`);
758
+ }
759
+ }
760
+ function resolveInstallPaths(botConfig, agentId, stateDir, sourceBase) {
761
+ function applyTemplate(tpl) {
762
+ return tpl.replace(/\{agent_id\}/g, agentId);
763
+ }
764
+ const srcWorkspace = botConfig.paths?.workspace ?? `workspace-${agentId}`;
765
+ const srcAgentState = botConfig.paths?.agentState ?? `agents/${agentId}`;
766
+ const srcMemoryDb = botConfig.paths?.memoryDb ?? null;
767
+ const srcCronJobs = botConfig.paths?.cronJobs ?? null;
768
+ const tplWorkspace = botConfig.paths?.targetWorkspace ?? "workspace-{agent_id}";
769
+ const tplAgentState = botConfig.paths?.targetAgentState ?? "agents/{agent_id}";
770
+ const tplMemoryDb = botConfig.paths?.targetMemoryDb ?? null;
771
+ const targetAgentRoot = path3.join(stateDir, applyTemplate(tplAgentState));
772
+ return {
773
+ sourceWorkspace: path3.join(sourceBase, srcWorkspace),
774
+ sourceAgentState: path3.join(sourceBase, srcAgentState),
775
+ sourceMemoryDb: srcMemoryDb ? path3.join(sourceBase, srcMemoryDb) : null,
776
+ sourceCronJobs: srcCronJobs ? path3.join(sourceBase, srcCronJobs) : null,
777
+ targetWorkspace: path3.join(stateDir, applyTemplate(tplWorkspace)),
778
+ targetAgentRoot,
779
+ targetAgentDir: path3.join(targetAgentRoot, "agent"),
780
+ targetMemoryDb: tplMemoryDb ? path3.join(stateDir, applyTemplate(tplMemoryDb)) : null
781
+ };
782
+ }
783
+ async function findSourceRoot(extractedDir) {
784
+ const queue = [path3.resolve(extractedDir)];
834
785
  const seen = /* @__PURE__ */ new Set();
835
786
  while (queue.length > 0) {
836
787
  const current = queue.shift();
837
788
  if (!current || seen.has(current)) continue;
838
789
  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
- };
790
+ if (await fileExists(path3.join(current, "source", "bot-config.json"))) {
791
+ return current;
857
792
  }
858
793
  let entries;
859
794
  try {
860
- entries = await fs5.readdir(current, { withFileTypes: true });
795
+ entries = await fs4.readdir(current, { withFileTypes: true });
861
796
  } catch {
862
797
  continue;
863
798
  }
864
799
  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));
800
+ if (!entry.isDirectory() || entry.name === "__MACOSX") continue;
801
+ queue.push(path3.join(current, entry.name));
868
802
  }
869
803
  }
870
804
  return null;
871
805
  }
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
- };
806
+ async function copyDirRecursive(src, dest) {
807
+ await fs4.mkdir(dest, { recursive: true });
808
+ const entries = await fs4.readdir(src, { withFileTypes: true });
809
+ for (const entry of entries) {
810
+ const srcPath = path3.join(src, entry.name);
811
+ const destPath = path3.join(dest, entry.name);
812
+ if (entry.isDirectory()) {
813
+ await copyDirRecursive(srcPath, destPath);
814
+ } else {
815
+ await fs4.copyFile(srcPath, destPath);
816
+ }
817
+ }
818
+ }
819
+ async function stripMacOsJunk(dir) {
820
+ const macosDir = path3.join(dir, "__MACOSX");
821
+ await fs4.rm(macosDir, { recursive: true, force: true }).catch(() => void 0);
822
+ async function removeDsStore(d) {
823
+ let entries;
824
+ try {
825
+ entries = await fs4.readdir(d, { withFileTypes: true });
826
+ } catch {
827
+ return;
828
+ }
829
+ for (const entry of entries) {
830
+ const full = path3.join(d, entry.name);
831
+ if (entry.isFile() && entry.name === ".DS_Store") {
832
+ await fs4.rm(full, { force: true }).catch(() => void 0);
833
+ } else if (entry.isDirectory()) {
834
+ await removeDsStore(full);
835
+ }
836
+ }
837
+ }
838
+ await removeDsStore(dir);
839
+ }
840
+ async function buildOpenClawConfigPatch(configPath, agentId, agentName, botConfig, installPaths) {
841
+ const raw = await fs4.readFile(configPath, "utf-8");
842
+ const config = JSON52.parse(raw);
843
+ const agentEntry = {
844
+ id: agentId,
845
+ name: agentName,
846
+ workspace: installPaths.targetWorkspace,
847
+ agentDir: installPaths.targetAgentDir
848
+ };
849
+ if (botConfig.agent?.heartbeat) agentEntry.heartbeat = botConfig.agent.heartbeat;
850
+ if (botConfig.agent?.identity) agentEntry.identity = botConfig.agent.identity;
851
+ const agents = config.agents ?? {};
852
+ if (!Array.isArray(agents.list)) agents.list = [];
853
+ agents.list.push(agentEntry);
854
+ config.agents = agents;
855
+ if (botConfig.tools?.web?.search?.enabled === true || botConfig.tools?.web?.fetch?.enabled === true) {
856
+ const tools = config.tools ?? {};
857
+ const web = tools.web ?? {};
858
+ if (botConfig.tools?.web?.search?.enabled === true) {
859
+ const s = web.search ?? {};
860
+ s.enabled = true;
861
+ web.search = s;
862
+ }
863
+ if (botConfig.tools?.web?.fetch?.enabled === true) {
864
+ const f = web.fetch ?? {};
865
+ f.enabled = true;
866
+ web.fetch = f;
867
+ }
868
+ tools.web = web;
869
+ config.tools = tools;
879
870
  }
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
871
  return {
884
- packageRoot: pkg.packageRoot,
885
- installerScriptName: "installer.sh",
886
- botConfigPath: pkg.botConfigPath
872
+ configContent: JSON.stringify(config, null, 2) + "\n",
873
+ configBackupContent: raw
887
874
  };
888
875
  }
889
- async function readBotInstallerConfig(configPath) {
890
- if (!await fileExists(configPath)) {
891
- throw new Error(`Package is missing required config file: ${configPath}`);
876
+ async function prepareCronJobsFromBundle(sourceCronPath, cronStorePath, agentId) {
877
+ if (!sourceCronPath || !await fileExists(sourceCronPath)) return null;
878
+ const raw = await fs4.readFile(sourceCronPath, "utf-8");
879
+ const bundleJobs = normalizeCronJobs(JSON.parse(raw));
880
+ if (bundleJobs.length === 0) return null;
881
+ const shouldInstall = await resolveCronChoice(true);
882
+ if (!shouldInstall) return null;
883
+ const newJobs = bundleJobs.map((j) => ({
884
+ ...j,
885
+ agentId,
886
+ state: {},
887
+ delivery: { mode: "announce", channel: "last", bestEffort: true }
888
+ }));
889
+ let existed = false;
890
+ let backupContent = null;
891
+ let existingJobs = [];
892
+ if (await fileExists(cronStorePath)) {
893
+ existed = true;
894
+ backupContent = await fs4.readFile(cronStorePath, "utf-8");
895
+ existingJobs = normalizeCronJobs(JSON52.parse(backupContent));
892
896
  }
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}`);
897
+ const existingKeys = new Set(
898
+ existingJobs.map((j) => `${String(j.agentId)}::${String(j.name)}`)
899
+ );
900
+ const deduped = newJobs.filter(
901
+ (j) => !existingKeys.has(`${String(j.agentId)}::${String(j.name)}`)
902
+ );
903
+ if (deduped.length === 0) {
904
+ console.log("All cron jobs already present, skipping.");
905
+ return null;
906
+ }
907
+ const merged = [...existingJobs, ...deduped];
908
+ return {
909
+ content: JSON.stringify({ version: 1, jobs: merged }, null, 2) + "\n",
910
+ existed,
911
+ backupContent,
912
+ jobCount: deduped.length
913
+ };
914
+ }
915
+ async function setupApiKeysFromConfig(botConfig, configPath, envPath) {
916
+ if (!botConfig.apiKeys?.enabled) return;
917
+ console.log("");
918
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
919
+ console.log(" API Key Setup");
920
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
921
+ console.log(" You can skip any key now and add it later.");
922
+ const brave = botConfig.apiKeys.braveSearch;
923
+ if (brave?.enabled) {
924
+ console.log("");
925
+ const key = await askOptional(brave.prompt ?? "Brave Search API key", brave.url);
926
+ if (key) {
927
+ const raw = await fs4.readFile(configPath, "utf-8");
928
+ const config = JSON52.parse(raw);
929
+ const tools = config.tools ?? {};
930
+ const web = tools.web ?? {};
931
+ const search = web.search ?? {};
932
+ search.apiKey = key;
933
+ web.search = search;
934
+ tools.web = web;
935
+ config.tools = tools;
936
+ await fs4.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
937
+ console.log(" Brave Search key saved to openclaw.json");
938
+ } else {
939
+ console.log(" Skipped Brave Search key.");
940
+ }
941
+ }
942
+ const envKeys = botConfig.apiKeys.env ?? [];
943
+ if (envKeys.length > 0) {
944
+ await fs4.mkdir(path3.dirname(envPath), { recursive: true });
945
+ let envContent = "";
946
+ try {
947
+ envContent = await fs4.readFile(envPath, "utf-8");
948
+ } catch {
949
+ }
950
+ let keysWritten = 0;
951
+ for (const keyDef of envKeys) {
952
+ console.log("");
953
+ const val = await askOptional(keyDef.prompt ?? keyDef.key, keyDef.url);
954
+ if (val) {
955
+ const regex = new RegExp(`^${keyDef.key}=.*$`, "m");
956
+ if (regex.test(envContent)) {
957
+ envContent = envContent.replace(regex, `${keyDef.key}=${val}`);
958
+ } else {
959
+ envContent = envContent.trimEnd() + `
960
+ ${keyDef.key}=${val}`;
961
+ }
962
+ keysWritten++;
963
+ }
964
+ }
965
+ if (keysWritten > 0) {
966
+ await fs4.writeFile(envPath, envContent.trim() + "\n", "utf-8");
967
+ console.log(`
968
+ ${keysWritten} key(s) saved to .env`);
969
+ }
970
+ }
971
+ }
972
+ async function validateInstalledWorkspace(workspacePath, botConfig) {
973
+ const requiredFiles = botConfig.validation?.requiredFiles ?? [];
974
+ const requiredDirs = botConfig.validation?.requiredDirs ?? [];
975
+ if (requiredFiles.length === 0 && requiredDirs.length === 0) return;
976
+ const warnings = [];
977
+ for (const file of requiredFiles) {
978
+ if (!await fileExists(path3.join(workspacePath, file))) {
979
+ warnings.push(`Missing expected file: ${file}`);
980
+ }
981
+ }
982
+ for (const dir of requiredDirs) {
983
+ try {
984
+ const stat = await fs4.stat(path3.join(workspacePath, dir));
985
+ if (!stat.isDirectory()) warnings.push(`Expected directory but found file: ${dir}`);
986
+ } catch {
987
+ warnings.push(`Missing expected directory: ${dir}`);
988
+ }
989
+ }
990
+ if (warnings.length > 0) {
991
+ console.log("\nValidation warnings:");
992
+ for (const w of warnings) console.warn(` \u26A0 ${w}`);
899
993
  }
900
994
  }
901
995
  async function resolveAgentId(suggestedAgentId) {
@@ -909,13 +1003,64 @@ async function resolveAgentId(suggestedAgentId) {
909
1003
  }
910
1004
  return prompted;
911
1005
  }
1006
+ async function resolveCronChoice(hasPackagedCronJobs) {
1007
+ if (!hasPackagedCronJobs) return false;
1008
+ const canPrompt = Boolean(process.stdin.isTTY && process.stdout.isTTY);
1009
+ if (!canPrompt) {
1010
+ throw new Error("Install requires an interactive terminal to confirm cron job installation.");
1011
+ }
1012
+ return confirm("Install packaged cron jobs for this new agent?", true);
1013
+ }
1014
+ async function commitInstallTransaction(input2) {
1015
+ let movedWorkspace = false;
1016
+ let movedAgentDir = false;
1017
+ let copiedMemoryDb = false;
1018
+ try {
1019
+ await fs4.mkdir(path3.dirname(input2.workspacePath), { recursive: true });
1020
+ await fs4.rename(input2.stagedWorkspacePath, input2.workspacePath);
1021
+ movedWorkspace = true;
1022
+ await fs4.mkdir(path3.dirname(input2.agentDirPath), { recursive: true });
1023
+ await fs4.rename(input2.stagedAgentDirPath, input2.agentDirPath);
1024
+ movedAgentDir = true;
1025
+ if (input2.memoryDbStagedPath && input2.memoryDbTargetPath && await fileExists(input2.memoryDbStagedPath)) {
1026
+ await fs4.mkdir(path3.dirname(input2.memoryDbTargetPath), { recursive: true });
1027
+ await fs4.copyFile(input2.memoryDbStagedPath, input2.memoryDbTargetPath);
1028
+ copiedMemoryDb = true;
1029
+ }
1030
+ if (input2.cronContent !== null) {
1031
+ await fs4.mkdir(path3.dirname(input2.cronStorePath), { recursive: true });
1032
+ await fs4.writeFile(input2.cronStorePath, input2.cronContent, "utf-8");
1033
+ }
1034
+ await fs4.writeFile(input2.configPath, input2.configContent, "utf-8");
1035
+ } catch (error) {
1036
+ if (input2.cronContent !== null) {
1037
+ if (input2.cronExisted && input2.cronBackupContent !== null) {
1038
+ await fs4.writeFile(input2.cronStorePath, input2.cronBackupContent, "utf-8").catch(() => void 0);
1039
+ } else {
1040
+ await fs4.rm(input2.cronStorePath, { force: true }).catch(() => void 0);
1041
+ }
1042
+ }
1043
+ if (copiedMemoryDb && input2.memoryDbTargetPath) {
1044
+ await fs4.rm(input2.memoryDbTargetPath, { force: true }).catch(() => void 0);
1045
+ }
1046
+ await fs4.writeFile(input2.configPath, input2.configBackupContent, "utf-8").catch(() => void 0);
1047
+ if (movedWorkspace) {
1048
+ await fs4.rm(input2.workspacePath, { recursive: true, force: true }).catch(() => void 0);
1049
+ }
1050
+ if (movedAgentDir) {
1051
+ await fs4.rm(input2.agentRootPath, { recursive: true, force: true }).catch(() => void 0);
1052
+ }
1053
+ const message = error instanceof Error ? error.message : "unknown failure";
1054
+ throw new Error(`Install failed and was rolled back: ${message}`);
1055
+ }
1056
+ }
912
1057
  function registerConfigCommands(program2) {
913
1058
  const configCommand = program2.command("config").description("Download and install published configs");
914
1059
  configCommand.command("download").description("Download a published config artifact from a one-time token").argument("<token>", "One-time download token").action(async (token) => {
915
1060
  try {
916
1061
  assertValidDownloadToken(token);
917
1062
  const server = getDefaultServer();
918
- const response = await fetch3(`${server}/download/${encodeURIComponent(token)}?format=file`, {
1063
+ const response = await fetch2(`${server}/download/${encodeURIComponent(token)}?format=file`, {
919
1064
  method: "GET"
920
1065
  });
921
1066
  if (!response.ok) {
@@ -923,12 +1068,12 @@ function registerConfigCommands(program2) {
923
1068
  }
924
1069
  const fileFromHeader = parseContentDispositionFileName(response.headers.get("content-disposition"));
925
1070
  const fallbackFile = `clawmarket-${token}.bin`;
926
- const preferredPath = path4.resolve(fileFromHeader || fallbackFile);
1071
+ const preferredPath = path3.resolve(fileFromHeader || fallbackFile);
927
1072
  const { outputPath, renamed } = await resolveSafeDownloadPath(preferredPath);
928
1073
  const bytes = Buffer.from(await response.arrayBuffer());
929
1074
  verifyDownloadedArtifactChecksum(bytes, response.headers.get(CLAWMARKET_ARTIFACT_SHA256_HEADER));
930
- await fs5.mkdir(path4.dirname(outputPath), { recursive: true });
931
- await fs5.writeFile(outputPath, bytes);
1075
+ await fs4.mkdir(path3.dirname(outputPath), { recursive: true });
1076
+ await fs4.writeFile(outputPath, bytes);
932
1077
  if (renamed) {
933
1078
  console.log(`Existing file detected, saved as: ${outputPath}`);
934
1079
  }
@@ -945,21 +1090,24 @@ function registerConfigCommands(program2) {
945
1090
  const server = getDefaultServer();
946
1091
  const context = await loadOpenClawContext();
947
1092
  const artifact = await fetchDownloadArtifact(server, token);
948
- const tempRoot = await fs5.mkdtemp(path4.join(os3.tmpdir(), `.clawmarket-install-${randomUUID().slice(0, 8)}-`));
1093
+ const tempRoot = await fs4.mkdtemp(
1094
+ path3.join(os3.tmpdir(), `.clawmarket-install-${randomUUID().slice(0, 8)}-`)
1095
+ );
949
1096
  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);
1097
+ const artifactPath = path3.join(tempRoot, sanitizeFileName(artifact.fileName));
1098
+ const extractedPath = path3.join(tempRoot, "extracted");
1099
+ await fs4.mkdir(extractedPath, { recursive: true });
1100
+ await fs4.writeFile(artifactPath, artifact.bytes);
954
1101
  await extractZipArtifact(artifactPath, extractedPath);
955
- const packageInfo = await findInstallerPackage(extractedPath);
956
- if (!packageInfo) {
1102
+ await stripMacOsJunk(extractedPath);
1103
+ const sourceRoot = await findSourceRoot(extractedPath);
1104
+ if (!sourceRoot) {
957
1105
  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)."
1106
+ "Downloaded artifact is not an installable bot package (missing source/bot-config.json)."
959
1107
  );
960
1108
  }
961
- const installLayout = await ensureInstallableLayout(packageInfo);
962
- const botConfig = await readBotInstallerConfig(installLayout.botConfigPath);
1109
+ const sourceBase = path3.join(sourceRoot, "source");
1110
+ const botConfig = await readBotInstallerConfig(path3.join(sourceBase, "bot-config.json"));
963
1111
  if (typeof botConfig.installerSpecVersion === "number" && botConfig.installerSpecVersion !== 1) {
964
1112
  console.warn(
965
1113
  `Warning: installerSpecVersion=${botConfig.installerSpecVersion} (this CLI currently expects version 1).`
@@ -969,110 +1117,77 @@ function registerConfigCommands(program2) {
969
1117
  const suggestedAgentId = validateAgentId(configuredAgentId) ? configuredAgentId : sanitizeSlugPart(configuredAgentId) || "agent";
970
1118
  const agentId = await resolveAgentId(suggestedAgentId);
971
1119
  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())) {
1120
+ const existingIds = new Set(context.agents.map((a) => a.id.toLowerCase()));
1121
+ if (existingIds.has(agentId.toLowerCase())) {
974
1122
  throw new Error(`Agent id "${agentId}" already exists. Choose another id.`);
975
1123
  }
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" }
1124
+ const installPaths = resolveInstallPaths(botConfig, agentId, context.paths.stateDir, sourceBase);
1125
+ const stagedWorkspacePath = path3.join(tempRoot, "staged-workspace");
1126
+ const stagedAgentDirPath = path3.join(tempRoot, "staged-agent");
1127
+ await copyDirRecursive(installPaths.sourceWorkspace, stagedWorkspacePath);
1128
+ await copyDirRecursive(installPaths.sourceAgentState, stagedAgentDirPath);
1129
+ const { configContent, configBackupContent } = await buildOpenClawConfigPatch(
1130
+ context.paths.configPath,
1131
+ agentId,
1132
+ agentName,
1133
+ botConfig,
1134
+ installPaths
1135
+ );
1136
+ const cronResult = await prepareCronJobsFromBundle(
1137
+ installPaths.sourceCronJobs,
1138
+ context.paths.cronStorePath,
1139
+ agentId
981
1140
  );
1141
+ console.log("\nInstalling...");
1142
+ await commitInstallTransaction({
1143
+ stagedWorkspacePath,
1144
+ stagedAgentDirPath,
1145
+ workspacePath: installPaths.targetWorkspace,
1146
+ agentDirPath: installPaths.targetAgentDir,
1147
+ agentRootPath: installPaths.targetAgentRoot,
1148
+ configPath: context.paths.configPath,
1149
+ configContent,
1150
+ configBackupContent,
1151
+ cronStorePath: context.paths.cronStorePath,
1152
+ cronContent: cronResult?.content ?? null,
1153
+ cronExisted: cronResult?.existed ?? false,
1154
+ cronBackupContent: cronResult?.backupContent ?? null,
1155
+ memoryDbStagedPath: installPaths.sourceMemoryDb,
1156
+ memoryDbTargetPath: installPaths.targetMemoryDb
1157
+ });
1158
+ await setupApiKeysFromConfig(botConfig, context.paths.configPath, context.paths.envPath);
1159
+ const installerShPath = path3.join(sourceBase, "installer.sh");
1160
+ if (await fileExists(installerShPath)) {
1161
+ console.log("\nRunning post-install hook (installer.sh)...");
1162
+ await runCommand("bash", [installerShPath, context.paths.stateDir], {
1163
+ cwd: sourceBase,
1164
+ stdio: "inherit"
1165
+ });
1166
+ }
1167
+ await validateInstalledWorkspace(installPaths.targetWorkspace, botConfig);
982
1168
  const refreshed = await loadOpenClawContext();
983
- const installed = refreshed.agents.find((agent) => agent.id.toLowerCase() === agentId.toLowerCase());
1169
+ const installed = refreshed.agents.find((a) => a.id.toLowerCase() === agentId.toLowerCase());
984
1170
  if (!installed) {
985
- throw new Error("Installer finished, but the new agent was not detected in openclaw.json.");
1171
+ throw new Error("Install finished, but the new agent was not detected in openclaw.json.");
986
1172
  }
987
- console.log("Installed package into OpenClaw as an additional agent");
988
- console.log(` Agent ID: ${installed.id}`);
989
- console.log(` Name: ${installed.name}`);
1173
+ console.log("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1174
+ console.log(" Installed successfully into OpenClaw");
1175
+ console.log("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1176
+ console.log(` Agent ID: ${installed.id}`);
1177
+ console.log(` Name: ${installed.name}`);
990
1178
  console.log(` Workspace: ${installed.workspace}`);
991
1179
  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
- );
1180
+ if (cronResult) {
1181
+ console.log(` Cron jobs: ${cronResult.jobCount} installed`);
1038
1182
  }
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
- );
1183
+ if (installPaths.targetMemoryDb && await fileExists(installPaths.targetMemoryDb)) {
1184
+ console.log(` Memory DB: ${installPaths.targetMemoryDb}`);
1045
1185
  }
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
1186
  console.log("\nNext steps:");
1072
1187
  console.log(" 1) openclaw agents list");
1073
1188
  console.log(" 2) openclaw gateway restart");
1074
1189
  } finally {
1075
- await fs5.rm(tempRoot, { recursive: true, force: true }).catch(() => void 0);
1190
+ await fs4.rm(tempRoot, { recursive: true, force: true }).catch(() => void 0);
1076
1191
  }
1077
1192
  } catch (error) {
1078
1193
  const message = error instanceof Error ? error.message : "Install failed";
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "clawmarketbot",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "description": "CLI tool for ClawMarket - discover, download, and install OpenClaw configs",
6
6
  "type": "module",
7
7
  "bin": {
8
- "clawmarketbot": "./dist/index.js"
8
+ "clawmarketbot": "dist/index.js"
9
9
  },
10
10
  "main": "./dist/index.js",
11
11
  "exports": {