mlclaw 0.1.0 → 0.2.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 (59) hide show
  1. package/.agents/skills/mlclaw/SKILL.md +70 -18
  2. package/Dockerfile +21 -4
  3. package/README.md +77 -24
  4. package/assets/hf-logo.svg +8 -0
  5. package/assets/hf-tooling/manifest.json +50 -0
  6. package/assets/hf-tooling/skills/hf-cli/SKILL.md +218 -0
  7. package/assets/hf-tooling/skills/hf-mem/SKILL.md +79 -0
  8. package/assets/hf-tooling/skills/huggingface-best/SKILL.md +134 -0
  9. package/assets/hf-tooling/skills/huggingface-datasets/SKILL.md +107 -0
  10. package/assets/hf-tooling/skills/huggingface-gradio/SKILL.md +298 -0
  11. package/assets/hf-tooling/skills/huggingface-gradio/examples.md +613 -0
  12. package/assets/hf-tooling/skills/huggingface-local-models/SKILL.md +113 -0
  13. package/assets/hf-tooling/skills/huggingface-local-models/references/hardware.md +38 -0
  14. package/assets/hf-tooling/skills/huggingface-local-models/references/hub-discovery.md +178 -0
  15. package/assets/hf-tooling/skills/huggingface-local-models/references/quantization.md +256 -0
  16. package/assets/hf-tooling/skills/huggingface-papers/SKILL.md +239 -0
  17. package/assets/hf-tooling/skills/huggingface-spaces/SKILL.md +239 -0
  18. package/assets/hf-tooling/skills/huggingface-spaces/references/buckets.md +89 -0
  19. package/assets/hf-tooling/skills/huggingface-spaces/references/debugging.md +236 -0
  20. package/assets/hf-tooling/skills/huggingface-spaces/references/gradio.md +200 -0
  21. package/assets/hf-tooling/skills/huggingface-spaces/references/grants.md +56 -0
  22. package/assets/hf-tooling/skills/huggingface-spaces/references/inference-providers.md +85 -0
  23. package/assets/hf-tooling/skills/huggingface-spaces/references/known-errors.md +232 -0
  24. package/assets/hf-tooling/skills/huggingface-spaces/references/requirements.md +169 -0
  25. package/assets/hf-tooling/skills/huggingface-spaces/references/zerogpu.md +349 -0
  26. package/assets/hf-tooling/skills/huggingface-tool-builder/SKILL.md +120 -0
  27. package/assets/hf-tooling/skills/huggingface-tool-builder/references/baseline_hf_api.py +57 -0
  28. package/assets/hf-tooling/skills/huggingface-tool-builder/references/baseline_hf_api.sh +40 -0
  29. package/assets/hf-tooling/skills/huggingface-tool-builder/references/baseline_hf_api.tsx +57 -0
  30. package/assets/hf-tooling/skills/huggingface-tool-builder/references/find_models_by_paper.sh +230 -0
  31. package/assets/hf-tooling/skills/huggingface-tool-builder/references/hf_enrich_models.sh +96 -0
  32. package/assets/hf-tooling/skills/huggingface-tool-builder/references/hf_model_card_frontmatter.sh +188 -0
  33. package/assets/hf-tooling/skills/huggingface-tool-builder/references/hf_model_papers_auth.sh +171 -0
  34. package/assets/hf-tooling/skills/huggingface-zerogpu/SKILL.md +289 -0
  35. package/assets/hf-tooling/skills/huggingface-zerogpu/references/concurrency.md +79 -0
  36. package/assets/hf-tooling/skills/huggingface-zerogpu/references/cuda-and-deps.md +66 -0
  37. package/assets/hf-tooling/skills/huggingface-zerogpu/references/how-quota-works.md +74 -0
  38. package/assets/hf-tooling/skills/huggingface-zerogpu/references/how-zerogpu-works.md +50 -0
  39. package/assets/hf-tooling/templates/.agents/mcp/huggingface.json +11 -0
  40. package/assets/hf-tooling/templates/.env.example +5 -0
  41. package/assets/hf-tooling/templates/examples/huggingface/README.md +16 -0
  42. package/assets/hf-tooling/templates/examples/huggingface/bucket-sync.md +19 -0
  43. package/assets/hf-tooling/templates/examples/huggingface/dataset-upload.py +22 -0
  44. package/assets/hf-tooling/templates/examples/huggingface/hf-discover.md +13 -0
  45. package/assets/hf-tooling/templates/examples/huggingface/runtime-inspection.py +9 -0
  46. package/assets/mlclaw-control-ui/assets/index-D2TFes32.js +9 -0
  47. package/assets/mlclaw-control-ui/assets/index-DP72PFuv.css +1 -0
  48. package/assets/mlclaw-control-ui/index.html +13 -0
  49. package/assets/mlclaw.svg +298 -124
  50. package/dist/hf-tooling-seed.js +261 -0
  51. package/dist/mlclaw-space-runtime.js +6746 -3321
  52. package/dist/mlclaw.mjs +428 -178
  53. package/entrypoint.sh +4 -0
  54. package/mlclaw.ps1 +4 -2
  55. package/mlclaw.sh +5 -3
  56. package/openclaw.default.json +1 -0
  57. package/package.json +16 -2
  58. package/space/README.md +32 -2
  59. package/tsconfig.json +3 -2
package/dist/mlclaw.mjs CHANGED
@@ -9756,10 +9756,10 @@ function parseGatewayLocation(value) {
9756
9756
 
9757
9757
  // src/mlclaw/git.ts
9758
9758
  import { execFile as execFile2 } from "node:child_process";
9759
- import fs11 from "node:fs/promises";
9759
+ import fs12 from "node:fs/promises";
9760
9760
  import os4 from "node:os";
9761
- import path12 from "node:path";
9762
- import { fileURLToPath as fileURLToPath2 } from "node:url";
9761
+ import path13 from "node:path";
9762
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
9763
9763
  import { promisify as promisify2 } from "node:util";
9764
9764
 
9765
9765
  // src/vendor/hfjs-xet/error.ts
@@ -14549,6 +14549,17 @@ var HubApi = class {
14549
14549
  throw err;
14550
14550
  }
14551
14551
  }
14552
+ async bucketExists(bucketId) {
14553
+ try {
14554
+ await this.bucket(bucketId).assertBucketAccessible();
14555
+ return true;
14556
+ } catch (err) {
14557
+ if (err instanceof BucketHttpError && err.status === 404) {
14558
+ return false;
14559
+ }
14560
+ throw err;
14561
+ }
14562
+ }
14552
14563
  async createDockerSpace(repoId, options) {
14553
14564
  const [owner, name] = splitRepoId(repoId);
14554
14565
  const me2 = await this.whoami();
@@ -14578,6 +14589,17 @@ var HubApi = class {
14578
14589
  throw err;
14579
14590
  }
14580
14591
  }
14592
+ async spaceExists(repoId) {
14593
+ try {
14594
+ await this.requestJson(`/api/spaces/${repoId}`);
14595
+ return true;
14596
+ } catch (err) {
14597
+ if (err instanceof HubApiError2 && err.status === 404) {
14598
+ return false;
14599
+ }
14600
+ throw err;
14601
+ }
14602
+ }
14581
14603
  async addSpaceVariable(repoId, key, value) {
14582
14604
  await this.requestJson(`/api/spaces/${repoId}/variables`, {
14583
14605
  method: "POST",
@@ -14778,15 +14800,73 @@ function sseDataToText(raw) {
14778
14800
  return lines.join("");
14779
14801
  }
14780
14802
 
14803
+ // src/mlclaw/runtime-image.ts
14804
+ import fs11 from "node:fs";
14805
+ import path12 from "node:path";
14806
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
14807
+ var DEFAULT_OPENCLAW_VERSION = "2026.7.1-beta.2";
14808
+ var DEFAULT_RUNTIME_IMAGE_REPOSITORY = "ghcr.io/osolmaz/mlclaw";
14809
+ var PACKAGE_METADATA = readPackageMetadata();
14810
+ var PACKAGE_VERSION = packageString("version", "unknown");
14811
+ var OPENCLAW_VERSION = packageConfigString("openclawVersion", DEFAULT_OPENCLAW_VERSION);
14812
+ var OPENCLAW_BASE_IMAGE = `ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}`;
14813
+ var RUNTIME_IMAGE_REPOSITORY = packageConfigString("runtimeImageRepository", DEFAULT_RUNTIME_IMAGE_REPOSITORY);
14814
+ var DEFAULT_RUNTIME_IMAGE_TAG = `${PACKAGE_VERSION}-openclaw-${OPENCLAW_VERSION}`;
14815
+ var DEFAULT_RUNTIME_IMAGE = `${RUNTIME_IMAGE_REPOSITORY}:${DEFAULT_RUNTIME_IMAGE_TAG}`;
14816
+ function resolveRuntimeImage(value, env = process.env) {
14817
+ return value?.trim() || env.MLCLAW_RUNTIME_IMAGE?.trim() || DEFAULT_RUNTIME_IMAGE;
14818
+ }
14819
+ function resolveSpaceRuntimeImage(opts, env = process.env) {
14820
+ if (opts.bundledRuntime) {
14821
+ if (opts.runtimeImage?.trim() || env.MLCLAW_RUNTIME_IMAGE?.trim()) {
14822
+ throw new Error("--bundled-runtime cannot be combined with --runtime-image or MLCLAW_RUNTIME_IMAGE");
14823
+ }
14824
+ return void 0;
14825
+ }
14826
+ return resolveRuntimeImage(opts.runtimeImage, env);
14827
+ }
14828
+ function bundledSpaceRuntimeRef(templateRev) {
14829
+ return `bundled:${templateRev}`;
14830
+ }
14831
+ function packageString(key, fallback) {
14832
+ const value = PACKAGE_METADATA[key];
14833
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
14834
+ }
14835
+ function packageConfigString(key, fallback) {
14836
+ const value = PACKAGE_METADATA.config?.[key];
14837
+ return typeof value === "string" && value.trim() ? value.trim() : fallback;
14838
+ }
14839
+ function readPackageMetadata() {
14840
+ let dir = path12.dirname(fileURLToPath2(import.meta.url));
14841
+ while (true) {
14842
+ const candidate = path12.join(dir, "package.json");
14843
+ try {
14844
+ return JSON.parse(fs11.readFileSync(candidate, "utf8"));
14845
+ } catch (err) {
14846
+ if (!isMissingFileError(err)) {
14847
+ throw err;
14848
+ }
14849
+ }
14850
+ const parent = path12.dirname(dir);
14851
+ if (parent === dir) {
14852
+ throw new Error("could not find package.json while resolving default runtime image");
14853
+ }
14854
+ dir = parent;
14855
+ }
14856
+ }
14857
+ function isMissingFileError(err) {
14858
+ return err instanceof Error && "code" in err && err.code === "ENOENT";
14859
+ }
14860
+
14781
14861
  // src/mlclaw/git.ts
14782
14862
  var execFileAsync2 = promisify2(execFile2);
14783
14863
  async function pushTemplateToSpace(params) {
14784
- const tempRoot = await fs11.mkdtemp(path12.join(os4.tmpdir(), "mlclaw-space-"));
14864
+ const tempRoot = await fs12.mkdtemp(path13.join(os4.tmpdir(), "mlclaw-space-"));
14785
14865
  try {
14786
14866
  const sourceDir = params.sourceDir ?? process.env.MLCLAW_SOURCE_DIR ?? await findPackagedSourceRoot();
14787
14867
  const templateRev = await currentTemplateRev(sourceDir);
14788
- const outDir = path12.join(tempRoot, "space");
14789
- await fs11.mkdir(outDir, { recursive: true });
14868
+ const outDir = path13.join(tempRoot, "space");
14869
+ await fs12.mkdir(outDir, { recursive: true });
14790
14870
  await generateSpaceRepo(sourceDir, outDir, {
14791
14871
  ...params.runtimeImage ? { runtimeImage: params.runtimeImage } : {}
14792
14872
  });
@@ -14804,7 +14884,7 @@ async function pushTemplateToSpace(params) {
14804
14884
  });
14805
14885
  return { templateRev };
14806
14886
  } finally {
14807
- await fs11.rm(tempRoot, { recursive: true, force: true });
14887
+ await fs12.rm(tempRoot, { recursive: true, force: true });
14808
14888
  }
14809
14889
  }
14810
14890
  async function currentTemplateRev(sourceDir) {
@@ -14817,18 +14897,22 @@ async function currentTemplateRev(sourceDir) {
14817
14897
  }
14818
14898
  } catch {
14819
14899
  }
14820
- const pkg = JSON.parse(await fs11.readFile(path12.join(sourceDir, "package.json"), "utf8"));
14900
+ const pkg = JSON.parse(await fs12.readFile(path13.join(sourceDir, "package.json"), "utf8"));
14821
14901
  return `npm:${pkg.name ?? "mlclaw"}@${pkg.version ?? "unknown"}`;
14822
14902
  }
14823
14903
  async function generateSpaceRepo(sourceDir, outDir, options = {}) {
14824
14904
  const copies = [
14825
14905
  [".gitattributes", ".gitattributes"],
14906
+ ["assets/hf-logo.svg", "assets/hf-logo.svg"],
14826
14907
  ["assets/mlclaw.svg", "assets/mlclaw.svg"],
14908
+ ["assets/mlclaw-control-ui", "assets/mlclaw-control-ui"],
14909
+ ["assets/hf-tooling", "assets/hf-tooling"],
14827
14910
  ["space/README.md", "README.md"]
14828
14911
  ];
14829
14912
  if (!options.runtimeImage) {
14830
14913
  copies.push(
14831
14914
  ["dist/hf-state-sync.js", "runtime/hf-state-sync.js"],
14915
+ ["dist/hf-tooling-seed.js", "runtime/hf-tooling-seed.js"],
14832
14916
  ["dist/mlclaw-space-runtime.js", "runtime/mlclaw-space-runtime.js"],
14833
14917
  ["entrypoint.sh", "runtime/entrypoint.sh"],
14834
14918
  ["openclaw.default.json", "runtime/openclaw.default.json"],
@@ -14838,10 +14922,10 @@ async function generateSpaceRepo(sourceDir, outDir, options = {}) {
14838
14922
  );
14839
14923
  }
14840
14924
  for (const [from, to] of copies) {
14841
- await copyExisting(path12.join(sourceDir, from), path12.join(outDir, to));
14925
+ await copyExisting(path13.join(sourceDir, from), path13.join(outDir, to));
14842
14926
  }
14843
- await fs11.writeFile(
14844
- path12.join(outDir, "Dockerfile"),
14927
+ await fs12.writeFile(
14928
+ path13.join(outDir, "Dockerfile"),
14845
14929
  options.runtimeImage ? imageDockerfile(options.runtimeImage) : bundledDockerfile(),
14846
14930
  "utf8"
14847
14931
  );
@@ -14851,17 +14935,30 @@ function imageDockerfile(runtimeImage) {
14851
14935
  `;
14852
14936
  }
14853
14937
  function bundledDockerfile() {
14854
- return `FROM ghcr.io/openclaw/openclaw:latest
14938
+ return `FROM ${OPENCLAW_BASE_IMAGE}
14855
14939
 
14856
14940
  LABEL org.opencontainers.image.source="https://github.com/osolmaz/mlclaw"
14857
14941
  LABEL org.opencontainers.image.description="ML Claw runtime for OpenClaw on Hugging Face"
14858
14942
 
14859
14943
  USER root
14860
14944
  RUN apt-get update \\
14861
- && apt-get install -y --no-install-recommends gosu zstd \\
14945
+ && apt-get install -y --no-install-recommends gosu python3 python3-pip python3-venv zstd \\
14862
14946
  && rm -rf /var/lib/apt/lists/*
14947
+ RUN python3 -m pip install --break-system-packages --no-cache-dir \\
14948
+ "huggingface_hub==1.19.0" \\
14949
+ "datasets==5.0.0" \\
14950
+ "safetensors==0.8.0" \\
14951
+ "fastapi==0.137.1" \\
14952
+ "pydantic==2.13.4" \\
14953
+ "rich==15.0.0" \\
14954
+ "starlette==1.3.1" \\
14955
+ "typer==0.25.1" \\
14956
+ "uvicorn==0.49.0" \\
14957
+ "uv==0.11.28" \\
14958
+ "hf-discover==1.3.7"
14863
14959
 
14864
14960
  COPY --chown=node:node runtime/hf-state-sync.js /app/hf-state-sync.js
14961
+ COPY --chown=node:node runtime/hf-tooling-seed.js /app/hf-tooling-seed.js
14865
14962
  COPY --chown=node:node runtime/mlclaw-space-runtime.js /app/mlclaw-space-runtime.js
14866
14963
  COPY --chown=node:node runtime/openclaw.default.json /app/openclaw.default.json
14867
14964
  COPY --chown=node:node runtime/entrypoint.sh /app/entrypoint.sh
@@ -14886,13 +14983,13 @@ CMD ["/app/entrypoint.sh"]
14886
14983
  `;
14887
14984
  }
14888
14985
  async function findPackagedSourceRoot() {
14889
- const start = path12.dirname(fileURLToPath2(import.meta.url));
14986
+ const start = path13.dirname(fileURLToPath3(import.meta.url));
14890
14987
  let dir = start;
14891
14988
  while (true) {
14892
14989
  if (await hasPackagedSourceFiles(dir)) {
14893
14990
  return dir;
14894
14991
  }
14895
- const parent = path12.dirname(dir);
14992
+ const parent = path13.dirname(dir);
14896
14993
  if (parent === dir) {
14897
14994
  throw new Error("Could not find packaged ML Claw source files. Reinstall the mlclaw npm package.");
14898
14995
  }
@@ -14909,20 +15006,20 @@ async function hasPackagedSourceFiles(dir) {
14909
15006
  "src/hf-bucket-client/client.ts"
14910
15007
  ];
14911
15008
  try {
14912
- await Promise.all(required.map((file) => fs11.access(path12.join(dir, file))));
15009
+ await Promise.all(required.map((file) => fs12.access(path13.join(dir, file))));
14913
15010
  return true;
14914
15011
  } catch {
14915
15012
  return false;
14916
15013
  }
14917
15014
  }
14918
15015
  async function copyExisting(from, to) {
14919
- const stat = await fs11.stat(from);
14920
- await fs11.mkdir(path12.dirname(to), { recursive: true });
15016
+ const stat = await fs12.stat(from);
15017
+ await fs12.mkdir(path13.dirname(to), { recursive: true });
14921
15018
  if (stat.isDirectory()) {
14922
- await fs11.cp(from, to, { recursive: true });
15019
+ await fs12.cp(from, to, { recursive: true });
14923
15020
  } else {
14924
- await fs11.copyFile(from, to);
14925
- await fs11.chmod(to, stat.mode);
15021
+ await fs12.copyFile(from, to);
15022
+ await fs12.chmod(to, stat.mode);
14926
15023
  }
14927
15024
  }
14928
15025
  async function readFilesForCommit(root) {
@@ -14930,24 +15027,24 @@ async function readFilesForCommit(root) {
14930
15027
  for (const relativePath of await listFiles(root)) {
14931
15028
  files.push({
14932
15029
  path: relativePath,
14933
- content: await fs11.readFile(path12.join(root, relativePath))
15030
+ content: await fs12.readFile(path13.join(root, relativePath))
14934
15031
  });
14935
15032
  }
14936
15033
  return files;
14937
15034
  }
14938
15035
  async function listFiles(root, dir = "") {
14939
- const absoluteDir = path12.join(root, dir);
14940
- const entries = await fs11.readdir(absoluteDir, { withFileTypes: true });
15036
+ const absoluteDir = path13.join(root, dir);
15037
+ const entries = await fs12.readdir(absoluteDir, { withFileTypes: true });
14941
15038
  const files = [];
14942
15039
  for (const entry of entries) {
14943
- const relativePath = path12.posix.join(dir.split(path12.sep).join(path12.posix.sep), entry.name);
14944
- const absolutePath = path12.join(root, relativePath);
15040
+ const relativePath = path13.posix.join(dir.split(path13.sep).join(path13.posix.sep), entry.name);
15041
+ const absolutePath = path13.join(root, relativePath);
14945
15042
  if (entry.isDirectory()) {
14946
15043
  files.push(...await listFiles(root, relativePath));
14947
15044
  } else if (entry.isFile()) {
14948
15045
  files.push(relativePath);
14949
15046
  } else {
14950
- const stat = await fs11.stat(absolutePath);
15047
+ const stat = await fs12.stat(absolutePath);
14951
15048
  if (stat.isFile()) {
14952
15049
  files.push(relativePath);
14953
15050
  }
@@ -15014,9 +15111,9 @@ async function assertNoLiveForeignLease(params) {
15014
15111
  }
15015
15112
 
15016
15113
  // src/mlclaw/local-config.ts
15017
- import fs12 from "node:fs/promises";
15114
+ import fs13 from "node:fs/promises";
15018
15115
  import os5 from "node:os";
15019
- import path13 from "node:path";
15116
+ import path14 from "node:path";
15020
15117
  function defaultConfigRoot(env = process.env) {
15021
15118
  const explicit = env.MLCLAW_CONFIG_HOME?.trim();
15022
15119
  if (explicit) {
@@ -15024,32 +15121,32 @@ function defaultConfigRoot(env = process.env) {
15024
15121
  }
15025
15122
  const xdg = env.XDG_CONFIG_HOME?.trim();
15026
15123
  if (xdg) {
15027
- return path13.join(xdg, "mlclaw");
15124
+ return path14.join(xdg, "mlclaw");
15028
15125
  }
15029
- return path13.join(os5.homedir(), ".config", "mlclaw");
15126
+ return path14.join(os5.homedir(), ".config", "mlclaw");
15030
15127
  }
15031
15128
  function localConfigPaths(root) {
15032
15129
  return {
15033
15130
  root,
15034
- deploymentsDir: path13.join(root, "deployments"),
15035
- secretsDir: path13.join(root, "secrets")
15131
+ deploymentsDir: path14.join(root, "deployments"),
15132
+ secretsDir: path14.join(root, "secrets")
15036
15133
  };
15037
15134
  }
15038
15135
  function manifestPath(root, agent) {
15039
- return path13.join(localConfigPaths(root).deploymentsDir, `${agent}.json`);
15136
+ return path14.join(localConfigPaths(root).deploymentsDir, `${agent}.json`);
15040
15137
  }
15041
15138
  function secretEnvPath(root, agent) {
15042
- return path13.join(localConfigPaths(root).secretsDir, `${agent}.env`);
15139
+ return path14.join(localConfigPaths(root).secretsDir, `${agent}.env`);
15043
15140
  }
15044
15141
  async function writeManifest(root, manifest) {
15045
15142
  const file = manifestPath(root, manifest.agent);
15046
- await fs12.mkdir(path13.dirname(file), { recursive: true });
15047
- await fs12.writeFile(file, `${JSON.stringify(manifest, null, 2)}
15143
+ await fs13.mkdir(path14.dirname(file), { recursive: true });
15144
+ await fs13.writeFile(file, `${JSON.stringify(manifest, null, 2)}
15048
15145
  `, "utf8");
15049
15146
  }
15050
15147
  async function readManifest(root, agent) {
15051
15148
  const file = manifestPath(root, agent);
15052
- const parsed = JSON.parse(await fs12.readFile(file, "utf8"));
15149
+ const parsed = JSON.parse(await fs13.readFile(file, "utf8"));
15053
15150
  if (parsed.version !== 1) {
15054
15151
  throw new Error(`unsupported deployment manifest version in ${file}`);
15055
15152
  }
@@ -15057,7 +15154,7 @@ async function readManifest(root, agent) {
15057
15154
  }
15058
15155
  async function manifestExists(root, agent) {
15059
15156
  try {
15060
- await fs12.access(manifestPath(root, agent));
15157
+ await fs13.access(manifestPath(root, agent));
15061
15158
  return true;
15062
15159
  } catch {
15063
15160
  return false;
@@ -15069,12 +15166,12 @@ function renderSecretEnv(values) {
15069
15166
  }
15070
15167
  async function writeSecretEnv(root, agent, values) {
15071
15168
  const file = secretEnvPath(root, agent);
15072
- await fs12.mkdir(path13.dirname(file), { recursive: true, mode: 448 });
15073
- await fs12.writeFile(file, renderSecretEnv(values), { encoding: "utf8", mode: 384 });
15074
- await fs12.chmod(file, 384);
15169
+ await fs13.mkdir(path14.dirname(file), { recursive: true, mode: 448 });
15170
+ await fs13.writeFile(file, renderSecretEnv(values), { encoding: "utf8", mode: 384 });
15171
+ await fs13.chmod(file, 384);
15075
15172
  }
15076
15173
  async function readSecretEnv(root, agent) {
15077
- return parseSecretEnv(await fs12.readFile(secretEnvPath(root, agent), "utf8"));
15174
+ return parseSecretEnv(await fs13.readFile(secretEnvPath(root, agent), "utf8"));
15078
15175
  }
15079
15176
  function parseSecretEnv(raw) {
15080
15177
  const out = {};
@@ -15116,46 +15213,6 @@ function namesFor(owner, agentName) {
15116
15213
  };
15117
15214
  }
15118
15215
 
15119
- // src/mlclaw/runtime-image.ts
15120
- import fs13 from "node:fs";
15121
- import path14 from "node:path";
15122
- import { fileURLToPath as fileURLToPath3 } from "node:url";
15123
- var RUNTIME_IMAGE_REPOSITORY = "ghcr.io/osolmaz/mlclaw-runtime";
15124
- var DEFAULT_RUNTIME_IMAGE = `${RUNTIME_IMAGE_REPOSITORY}:${readPackageVersion()}`;
15125
- function resolveRuntimeImage(value, env = process.env) {
15126
- return value?.trim() || env.MLCLAW_RUNTIME_IMAGE?.trim() || DEFAULT_RUNTIME_IMAGE;
15127
- }
15128
- function resolveRuntimeImageOverride(value, env = process.env) {
15129
- return value?.trim() || env.MLCLAW_RUNTIME_IMAGE?.trim() || void 0;
15130
- }
15131
- function bundledSpaceRuntimeRef(templateRev) {
15132
- return `bundled:${templateRev}`;
15133
- }
15134
- function readPackageVersion() {
15135
- let dir = path14.dirname(fileURLToPath3(import.meta.url));
15136
- while (true) {
15137
- const candidate = path14.join(dir, "package.json");
15138
- try {
15139
- const parsed = JSON.parse(fs13.readFileSync(candidate, "utf8"));
15140
- if (typeof parsed.version === "string" && parsed.version.trim()) {
15141
- return parsed.version.trim();
15142
- }
15143
- } catch (err) {
15144
- if (!isMissingFileError(err)) {
15145
- throw err;
15146
- }
15147
- }
15148
- const parent = path14.dirname(dir);
15149
- if (parent === dir) {
15150
- throw new Error("could not find package.json while resolving default runtime image");
15151
- }
15152
- dir = parent;
15153
- }
15154
- }
15155
- function isMissingFileError(err) {
15156
- return err instanceof Error && "code" in err && err.code === "ENOENT";
15157
- }
15158
-
15159
15216
  // src/mlclaw/telegram.ts
15160
15217
  var TELEGRAM_GET_ME_TIMEOUT_MS = 3e4;
15161
15218
  var TELEGRAM_GET_ME_ATTEMPTS = 4;
@@ -15192,7 +15249,7 @@ function delay(ms) {
15192
15249
  }
15193
15250
 
15194
15251
  // src/mlclaw/cli.ts
15195
- var DEFAULT_MODEL = "huggingface/google/gemma-4-26B-A4B-it";
15252
+ var DEFAULT_MODEL = "huggingface/google/gemma-4-26B-A4B-it:deepinfra";
15196
15253
  var DEFAULT_HARDWARE = "cpu-basic";
15197
15254
  var TELEGRAM_HARDWARE = "cpu-upgrade";
15198
15255
  var TELEGRAM_SLEEP_TIME = -1;
@@ -15205,6 +15262,7 @@ var SPACE_HANDOFF_TIMEOUT_MS = 12e4;
15205
15262
  var SPACE_HANDOFF_POLL_MS = 5e3;
15206
15263
  var STALE_PATH_VARS = ["OPENCLAW_STATE_DIR", "OPENCLAW_WORKSPACE_DIR", "OPENCLAW_CONFIG_PATH"];
15207
15264
  var SNAPSHOT_MANIFEST_REMOTE_NAME = "manifest.json";
15265
+ var DEFAULT_CANONICAL_TEMPLATE_SPACE = "osolmaz/mlclaw";
15208
15266
  var PAID_HARDWARE_COST_NOTE = "Paid Hugging Face Space hardware costs money while allocated. The cheapest option is cpu-upgrade at $0.03/hour, about $22/month if kept always on.";
15209
15267
  var defaultPrompt = {
15210
15268
  isInteractive: () => Boolean(process4.stdin.isTTY && process4.stdout.isTTY),
@@ -15237,10 +15295,10 @@ function createProgram(runtimeOverrides = {}) {
15237
15295
  program2.name("mlclaw").description("Deploy OpenClaw to a Hugging Face Space and private bucket").showHelpAfterError().exitOverride((err) => {
15238
15296
  throw err;
15239
15297
  });
15240
- program2.command("bootstrap", { isDefault: true }).description("Create or update a Hugging Face OpenClaw deployment").option("--owner <owner>", "Hugging Face user or organization").option("--name <name>", "Agent and runtime resource base name").option("--bucket <owner/bucket>", "State bucket to create or adopt").option("--gateway <local|space>", "Where the live gateway runs").option("--telegram-token <token>", "Optional Telegram bot token").option("--telegram-token-file <path>", "File containing TELEGRAM_BOT_TOKEN=... or a raw token").option("--telegram-user-id <id>", "Allowed Telegram user ID").option("--telegram-api-root <url>", "Telegram API root override").option("--telegram-proxy <url>", "Telegram proxy URL override").option("--hardware <flavor>", "Hugging Face Space hardware flavor").option("--sleep-time <seconds>", "Space sleep timeout in seconds; -1 means never sleep", parseInteger).option("--model <model>", "OpenClaw model identifier", DEFAULT_MODEL).option("--runtime-image <image>", "ML Claw runtime image").option("--public-space", "Create the Hugging Face Space as public instead of private", false).addOption(new Option("--gateway-token <token>").hideHelp()).option("--docker-context <name>", "Docker context for local gateway mode").option("--no-pull", "Do not docker pull before starting a local gateway").option("--takeover", "Start even if a stale runtime lease is present", false).option("--yes", "Confirm paid hardware prompts for automation", false).action(async (opts) => {
15298
+ program2.command("bootstrap", { isDefault: true }).description("Create or update a Hugging Face OpenClaw deployment").option("--owner <owner>", "Hugging Face user or organization").option("--name <name>", "Agent and runtime resource base name").option("--bucket <owner/bucket>", "State bucket to create or adopt").option("--gateway <local|space>", "Where the live gateway runs").option("--telegram-token <token>", "Optional Telegram bot token").option("--telegram-token-file <path>", "File containing TELEGRAM_BOT_TOKEN=... or a raw token").option("--telegram-user-id <id>", "Allowed Telegram user ID").option("--telegram-api-root <url>", "Telegram API root override").option("--telegram-proxy <url>", "Telegram proxy URL override").option("--hardware <flavor>", "Hugging Face Space hardware flavor").option("--sleep-time <seconds>", "Space sleep timeout in seconds; -1 means never sleep", parseInteger).option("--model <model>", "OpenClaw model identifier", DEFAULT_MODEL).option("--runtime-image <image>", "ML Claw runtime image").option("--bundled-runtime", "Generate a bundled Space runtime instead of using the prebuilt ML Claw image", false).option("--public-space", "Create the Hugging Face Space as public instead of private", false).addOption(new Option("--gateway-token <token>").hideHelp()).option("--docker-context <name>", "Docker context for local gateway mode").option("--no-pull", "Do not docker pull before starting a local gateway").option("--takeover", "Start even if a stale runtime lease is present", false).option("--yes", "Confirm paid hardware prompts for automation", false).action(async (opts) => {
15241
15299
  await bootstrap(opts, runtime);
15242
15300
  });
15243
- program2.command("update").description("Regenerate and upload current ML Claw Space files").argument("<owner/space>", "Hugging Face Space repo ID").option("--runtime-image <image>", "Runtime image to write into the generated Space Dockerfile").option("--force", "Update even if the Space does not look like ML Claw", false).action(async (repoId, opts) => {
15301
+ program2.command("update").description("Regenerate and upload current ML Claw Space files").argument("<owner/space>", "Hugging Face Space repo ID").option("--runtime-image <image>", "Runtime image to write into the generated Space Dockerfile").option("--bundled-runtime", "Generate a bundled Space runtime instead of using the prebuilt ML Claw image", false).option("--force", "Update even if the Space does not look like ML Claw", false).action(async (repoId, opts) => {
15244
15302
  const token = await runtime.readToken(runtime.env);
15245
15303
  const hub = runtime.hubFactory(token);
15246
15304
  await update(repoId, opts, hub, token, runtime);
@@ -15272,7 +15330,7 @@ function createProgram(runtimeOverrides = {}) {
15272
15330
  gateway.command("logs").argument("<agent>", "Agent name").option("--tail <lines>", "Number of log lines", parseInteger, 200).action(async (agent, opts) => {
15273
15331
  await gatewayLogs(agent, opts, runtime);
15274
15332
  });
15275
- gateway.command("migrate").argument("<agent>", "Agent name").requiredOption("--to <local|space>", "Target gateway location").option("--hardware <flavor>", "Hugging Face Space hardware flavor").option("--sleep-time <seconds>", "Space sleep timeout in seconds; -1 means never sleep", parseInteger).option("--runtime-image <image>", "ML Claw runtime image").option("--public-space", "Create the Hugging Face Space as public instead of private", false).option("--docker-context <name>", "Docker context for local gateway startup when migrating to local").option("--no-pull", "Do not docker pull before starting a local gateway").option("--takeover", "Start even if another live runtime lease is present", false).option("--yes", "Confirm paid hardware prompts for automation", false).action(async (agent, opts) => {
15333
+ gateway.command("migrate").argument("<agent>", "Agent name").requiredOption("--to <local|space>", "Target gateway location").option("--hardware <flavor>", "Hugging Face Space hardware flavor").option("--sleep-time <seconds>", "Space sleep timeout in seconds; -1 means never sleep", parseInteger).option("--runtime-image <image>", "ML Claw runtime image").option("--bundled-runtime", "Generate a bundled Space runtime instead of using the prebuilt ML Claw image", false).option("--public-space", "Create the Hugging Face Space as public instead of private", false).option("--docker-context <name>", "Docker context for local gateway startup when migrating to local").option("--no-pull", "Do not docker pull before starting a local gateway").option("--takeover", "Start even if another live runtime lease is present", false).option("--yes", "Confirm paid hardware prompts for automation", false).action(async (agent, opts) => {
15276
15334
  await gatewayMigrate(agent, opts, runtime);
15277
15335
  });
15278
15336
  gateway.command("rebind").argument("<agent>", "Agent name").requiredOption("--docker-context <name>", "Target Docker context").option("--no-pull", "Do not docker pull before starting the rebound local gateway").option("--takeover", "Rebind even if the old Docker context is unavailable", false).action(async (agent, opts) => {
@@ -15307,75 +15365,43 @@ async function bootstrap(opts, runtime) {
15307
15365
  const owner = opts.owner ?? me2.name;
15308
15366
  const telegramToken = await readOptionalTelegramToken(opts, runtime);
15309
15367
  const bot = telegramToken ? await runtime.getTelegramBot(telegramToken, opts.telegramApiRoot) : void 0;
15310
- const agentName = slugifyAgentName(opts.name ?? bot?.username ?? await promptAgentName(runtime));
15368
+ let agentName = slugifyAgentName(opts.name ?? bot?.username ?? await promptAgentName(runtime));
15311
15369
  const telegramUserId = telegramToken ? opts.telegramUserId ?? runtime.env.TELEGRAM_ALLOWED_USERS ?? await promptRequired("Telegram allowed user ID", runtime) : void 0;
15312
- const names = namesFor(owner, agentName);
15313
15370
  const model = opts.model ?? DEFAULT_MODEL;
15314
15371
  const runtimeImage = resolveRuntimeImage(opts.runtimeImage, runtime.env);
15315
- const templateRuntimeImage = resolveRuntimeImageOverride(opts.runtimeImage, runtime.env);
15316
- const sessionSecret = randomBytes(48).toString("base64url");
15317
- const now = runtime.now().toISOString();
15318
- const existingManifest = await readManifest(runtime.configRoot, agentName).catch(() => null);
15319
- const existingSecrets = await readSecretEnv(runtime.configRoot, agentName).catch(() => ({}));
15320
- const gatewayLocation = requestedGatewayLocation ?? existingManifest?.gatewayLocation ?? DEFAULT_GATEWAY_LOCATION;
15321
- if (opts.dockerContext && gatewayLocation !== "local") {
15322
- throw new Error("--docker-context only applies to local gateway mode");
15372
+ const templateRuntimeImage = resolveSpaceRuntimeImage(opts, runtime.env);
15373
+ let plan;
15374
+ for (; ; ) {
15375
+ plan = await resolveBootstrapPlan({
15376
+ opts,
15377
+ owner,
15378
+ agentName,
15379
+ hfToken,
15380
+ model,
15381
+ runtimeImage,
15382
+ hub,
15383
+ runtime,
15384
+ ...requestedGatewayLocation ? { requestedGatewayLocation } : {},
15385
+ ...telegramToken ? { telegramToken } : {},
15386
+ ...telegramUserId ? { telegramUserId } : {}
15387
+ });
15388
+ const alternative = await promptAlternativeBootstrapName({
15389
+ plan,
15390
+ explicitBucket: opts.bucket,
15391
+ yes: Boolean(opts.yes),
15392
+ runtime
15393
+ });
15394
+ if (!alternative) {
15395
+ break;
15396
+ }
15397
+ agentName = alternative;
15323
15398
  }
15324
- const bucketPrefix = bootstrapBucketPrefix(existingManifest, existingSecrets, runtime);
15325
- const localRuntimeId = existingManifest?.localRuntimeId ?? newLocalRuntimeId(agentName);
15326
- const localGateway = gatewayLocation === "local" ? await resolveLocalGatewayBinding({
15327
- manifest: existingManifest,
15328
- requestedContext: opts.dockerContext,
15329
- runtime,
15330
- persist: false
15331
- }) : existingManifest?.localGateway;
15332
- const bucket = await resolveBootstrapBucket({
15333
- explicitBucket: opts.bucket,
15334
- defaultBucket: names.bucket,
15335
- existingManifest,
15336
- bucketPrefix,
15337
- hub,
15338
- yes: Boolean(opts.yes),
15339
- runtime
15340
- });
15341
- const manifest = {
15342
- version: 1,
15343
- agent: agentName,
15344
- owner,
15345
- bucket,
15346
- space: names.space,
15347
- localRuntimeId,
15348
- gatewayLocation,
15349
- model,
15350
- runtimeImage,
15351
- ...localGateway ? { localGateway } : {},
15352
- createdAt: existingManifest?.createdAt ?? now,
15353
- updatedAt: now
15354
- };
15355
- const secrets = deploymentSecrets({
15356
- hfToken,
15357
- ...telegramToken ? { telegramToken } : {},
15358
- ...telegramUserId ? { telegramUserId } : {},
15359
- sessionSecret,
15360
- bucket,
15361
- model,
15362
- agentName,
15363
- runtimeImage,
15364
- gatewayLocation,
15365
- runtimeId: gatewayLocation === "local" ? manifest.localRuntimeId : spaceRuntimeId(agentName),
15366
- ...bucketPrefix ? { bucketPrefix } : {},
15367
- ...opts.telegramProxy ? { telegramProxy: opts.telegramProxy } : {},
15368
- ...opts.telegramApiRoot ? { telegramApiRoot: opts.telegramApiRoot } : {}
15369
- });
15399
+ const { names, gatewayLocation, bucketPlan, bucket, spacePlan, manifest, secrets } = plan;
15370
15400
  let deployedSpaceRuntime;
15371
15401
  if (gatewayLocation === "space") {
15372
- await assertNoLiveForeignLease({
15373
- hub,
15374
- bucket,
15375
- bucketPrefix,
15376
- runtimeId: spaceRuntimeId(agentName),
15377
- takeover: Boolean(opts.takeover)
15378
- });
15402
+ if (!spacePlan) {
15403
+ throw new Error("internal error: Space plan was not resolved");
15404
+ }
15379
15405
  const paidHardware = await resolveHardware({
15380
15406
  requestedHardware: opts.hardware ?? (telegramToken ? TELEGRAM_HARDWARE : DEFAULT_HARDWARE),
15381
15407
  ...typeof opts.sleepTime === "number" ? { requestedSleepTime: opts.sleepTime } : telegramToken ? { requestedSleepTime: TELEGRAM_SLEEP_TIME } : {},
@@ -15383,6 +15409,24 @@ async function bootstrap(opts, runtime) {
15383
15409
  yes: Boolean(opts.yes),
15384
15410
  runtime
15385
15411
  });
15412
+ await confirmBootstrapPlan({
15413
+ manifest,
15414
+ bucketPlan,
15415
+ spacePlan,
15416
+ hasExistingManifest: plan.hasExistingManifest,
15417
+ hardware: paidHardware.hardware,
15418
+ ...typeof paidHardware.sleepTime === "number" ? { sleepTime: paidHardware.sleepTime } : {},
15419
+ yes: Boolean(opts.yes),
15420
+ runtime
15421
+ });
15422
+ await createOrAdoptBucket({ hub, bucketPlan, runtime });
15423
+ await assertNoLiveForeignLease({
15424
+ hub,
15425
+ bucket,
15426
+ bucketPrefix: plan.bucketPrefix,
15427
+ runtimeId: spaceRuntimeId(agentName),
15428
+ takeover: Boolean(opts.takeover)
15429
+ });
15386
15430
  const deployed = await deploySpaceGateway({
15387
15431
  hub,
15388
15432
  runtime,
@@ -15391,6 +15435,7 @@ async function bootstrap(opts, runtime) {
15391
15435
  secrets,
15392
15436
  allowedUsers: me2.name,
15393
15437
  hardware: paidHardware.hardware,
15438
+ spaceExists: spacePlan.exists,
15394
15439
  publicSpace: Boolean(opts.publicSpace),
15395
15440
  ...typeof paidHardware.sleepTime === "number" ? { sleepTime: paidHardware.sleepTime } : {},
15396
15441
  ...templateRuntimeImage ? { templateRuntimeImage } : {}
@@ -15398,10 +15443,19 @@ async function bootstrap(opts, runtime) {
15398
15443
  deployedSpaceRuntime = deployed.runtimeImage;
15399
15444
  await writeLocalDeployment(runtime.configRoot, manifest, secrets);
15400
15445
  } else {
15446
+ await confirmBootstrapPlan({
15447
+ manifest,
15448
+ bucketPlan,
15449
+ hasExistingManifest: plan.hasExistingManifest,
15450
+ hardware: "local Docker",
15451
+ yes: Boolean(opts.yes),
15452
+ runtime
15453
+ });
15454
+ await createOrAdoptBucket({ hub, bucketPlan, runtime });
15401
15455
  await assertNoLiveForeignLease({
15402
15456
  hub,
15403
15457
  bucket,
15404
- bucketPrefix,
15458
+ bucketPrefix: plan.bucketPrefix,
15405
15459
  runtimeId: manifest.localRuntimeId,
15406
15460
  takeover: Boolean(opts.takeover)
15407
15461
  });
@@ -15417,6 +15471,7 @@ async function bootstrap(opts, runtime) {
15417
15471
  runtime.stdout.log(`Bucket: https://huggingface.co/buckets/${bucket}`);
15418
15472
  if (gatewayLocation === "space") {
15419
15473
  runtime.stdout.log(`Space: https://huggingface.co/spaces/${names.space}`);
15474
+ runtime.stdout.log(`Agent URL: ${spacePublicUrl(names.space)}`);
15420
15475
  } else {
15421
15476
  runtime.stdout.log(`Local: ${containerNameFor(agentName)}`);
15422
15477
  }
@@ -15429,29 +15484,163 @@ async function bootstrap(opts, runtime) {
15429
15484
  if (deployedSpaceRuntime) {
15430
15485
  runtime.stdout.log(`Space runtime: ${deployedSpaceRuntime}`);
15431
15486
  }
15432
- runtime.prompt.outro(gatewayLocation === "space" ? "Restart requested. Build logs may take a few minutes to appear." : "Local gateway start requested.");
15487
+ runtime.prompt.outro(gatewayLocation === "space" ? `Restart requested. Your agent will soon be available at ${spacePublicUrl(names.space)}.` : "Local gateway start requested.");
15488
+ }
15489
+ function spacePublicUrl(repoId) {
15490
+ return `https://${repoId.replace("/", "-")}.hf.space`;
15491
+ }
15492
+ async function resolveBootstrapPlan(params) {
15493
+ const { opts, owner, agentName, requestedGatewayLocation, hfToken, telegramToken, telegramUserId, model, runtimeImage, hub, runtime } = params;
15494
+ const names = namesFor(owner, agentName);
15495
+ const sessionSecret = randomBytes(48).toString("base64url");
15496
+ const now = runtime.now().toISOString();
15497
+ const existingManifest = await readManifest(runtime.configRoot, agentName).catch(() => null);
15498
+ const existingSecrets = await readSecretEnv(runtime.configRoot, agentName).catch(() => ({}));
15499
+ const gatewayLocation = requestedGatewayLocation ?? existingManifest?.gatewayLocation ?? DEFAULT_GATEWAY_LOCATION;
15500
+ if (opts.dockerContext && gatewayLocation !== "local") {
15501
+ throw new Error("--docker-context only applies to local gateway mode");
15502
+ }
15503
+ const bucketPrefix = bootstrapBucketPrefix(existingManifest, existingSecrets, runtime);
15504
+ const localRuntimeId = existingManifest?.localRuntimeId ?? newLocalRuntimeId(agentName);
15505
+ const localGateway = gatewayLocation === "local" ? await resolveLocalGatewayBinding({
15506
+ manifest: existingManifest,
15507
+ requestedContext: opts.dockerContext,
15508
+ runtime,
15509
+ persist: false
15510
+ }) : existingManifest?.localGateway;
15511
+ const bucketPlan = await resolveBootstrapBucket({
15512
+ explicitBucket: opts.bucket,
15513
+ defaultBucket: names.bucket,
15514
+ existingManifest,
15515
+ bucketPrefix,
15516
+ hub
15517
+ });
15518
+ const bucket = bucketPlan.bucket;
15519
+ const spacePlan = gatewayLocation === "space" ? {
15520
+ space: names.space,
15521
+ exists: await hub.spaceExists(names.space),
15522
+ visibility: opts.publicSpace ? "public" : "private"
15523
+ } : void 0;
15524
+ const manifest = {
15525
+ version: 1,
15526
+ agent: agentName,
15527
+ owner,
15528
+ bucket,
15529
+ space: names.space,
15530
+ localRuntimeId,
15531
+ gatewayLocation,
15532
+ model,
15533
+ runtimeImage,
15534
+ ...localGateway ? { localGateway } : {},
15535
+ createdAt: existingManifest?.createdAt ?? now,
15536
+ updatedAt: now
15537
+ };
15538
+ const secrets = deploymentSecrets({
15539
+ hfToken,
15540
+ ...telegramToken ? { telegramToken } : {},
15541
+ ...telegramUserId ? { telegramUserId } : {},
15542
+ sessionSecret,
15543
+ bucket,
15544
+ model,
15545
+ agentName,
15546
+ runtimeImage,
15547
+ gatewayLocation,
15548
+ runtimeId: gatewayLocation === "local" ? manifest.localRuntimeId : spaceRuntimeId(agentName),
15549
+ ...bucketPrefix ? { bucketPrefix } : {},
15550
+ ...opts.telegramProxy ? { telegramProxy: opts.telegramProxy } : {},
15551
+ ...opts.telegramApiRoot ? { telegramApiRoot: opts.telegramApiRoot } : {}
15552
+ });
15553
+ return {
15554
+ agentName,
15555
+ names,
15556
+ hasExistingManifest: Boolean(existingManifest),
15557
+ gatewayLocation,
15558
+ ...bucketPrefix ? { bucketPrefix } : {},
15559
+ bucketPlan,
15560
+ bucket,
15561
+ ...spacePlan ? { spacePlan } : {},
15562
+ manifest,
15563
+ secrets
15564
+ };
15433
15565
  }
15434
15566
  async function resolveBootstrapBucket(params) {
15435
15567
  const explicitBucket = params.explicitBucket ? parseBucketId(params.explicitBucket) : void 0;
15436
15568
  const bucket = explicitBucket ?? params.existingManifest?.bucket ?? params.defaultBucket;
15437
- params.runtime.stdout.log(`Creating or adopting private bucket ${bucket}`);
15438
- await params.hub.createBucket(bucket, true);
15439
- const inspection = await inspectStateBucket(params.hub, bucket, params.bucketPrefix);
15440
- const existingBucket = params.existingManifest?.bucket;
15441
- if (explicitBucket && existingBucket && existingBucket !== explicitBucket) {
15442
- await confirmBucketChange({
15443
- message: `Change ${params.existingManifest?.agent ?? "deployment"} state bucket from ${existingBucket} to ${explicitBucket}?`,
15444
- yes: params.yes,
15445
- runtime: params.runtime
15446
- });
15447
- } else if (explicitBucket && !existingBucket && inspection.objectCount > 0) {
15448
- await confirmBucketChange({
15449
- message: `Adopt existing state bucket ${explicitBucket} with ${inspection.objectCount} object(s)?`,
15450
- yes: params.yes,
15451
- runtime: params.runtime
15452
- });
15569
+ const exists = await params.hub.bucketExists(bucket);
15570
+ const inspection = exists ? await inspectStateBucket(params.hub, bucket, params.bucketPrefix) : { objectCount: 0 };
15571
+ return {
15572
+ bucket,
15573
+ exists,
15574
+ objectCount: inspection.objectCount
15575
+ };
15576
+ }
15577
+ async function promptAlternativeBootstrapName(params) {
15578
+ const existingDefaultBucket = !params.explicitBucket && params.plan.bucketPlan.exists;
15579
+ const existingSpace = params.plan.spacePlan?.exists === true;
15580
+ if (!existingDefaultBucket && !existingSpace || params.yes || !params.runtime.prompt.isInteractive()) {
15581
+ return void 0;
15582
+ }
15583
+ const current = params.plan.agentName;
15584
+ const suggestion = `${current}-2`;
15585
+ params.runtime.prompt.note(
15586
+ `The name ${current} maps to existing ML Claw resources. Enter another name for a fresh deployment, or leave this blank to update the existing one.`,
15587
+ "Existing resources"
15588
+ );
15589
+ const value = await params.runtime.prompt.text({
15590
+ message: "Alternative agent name",
15591
+ placeholder: suggestion
15592
+ });
15593
+ if (q(value)) {
15594
+ params.runtime.prompt.cancel("Cancelled");
15595
+ throw new Error("cancelled");
15596
+ }
15597
+ const raw = value.trim();
15598
+ if (!raw) {
15599
+ return void 0;
15600
+ }
15601
+ const alternative = slugifyAgentName(raw);
15602
+ return alternative === current ? void 0 : alternative;
15603
+ }
15604
+ async function confirmBootstrapPlan(params) {
15605
+ const lines = [
15606
+ `Agent: ${params.manifest.agent}`,
15607
+ `Gateway: ${params.manifest.gatewayLocation}`,
15608
+ `Bucket: ${params.bucketPlan.bucket} (${params.bucketPlan.exists ? `exists; keeping ${params.bucketPlan.objectCount} object(s)` : "will be created as private"})`
15609
+ ];
15610
+ if (params.spacePlan) {
15611
+ lines.push(`Space: ${params.spacePlan.space} (${params.spacePlan.exists ? "exists; files, variables, secrets, and runtime will be updated" : `will be created as ${params.spacePlan.visibility}`})`);
15612
+ lines.push(`Hardware: ${params.hardware}`);
15613
+ if (typeof params.sleepTime === "number") {
15614
+ lines.push(`Sleep time: ${params.sleepTime}`);
15615
+ }
15616
+ } else {
15617
+ lines.push(`Local runtime: ${containerNameFor(params.manifest.agent)} (${params.hardware})`);
15618
+ }
15619
+ if (params.bucketPlan.exists || params.spacePlan?.exists) {
15620
+ lines.push(`Fresh deployment: use a different name, for example --name ${params.manifest.agent}-2`);
15621
+ }
15622
+ lines.push(`Model: ${params.manifest.model}`);
15623
+ lines.push(`Runtime image: ${params.manifest.runtimeImage}`);
15624
+ params.runtime.prompt.note(lines.join("\n"), "Bootstrap plan");
15625
+ if (params.yes) {
15626
+ return;
15627
+ }
15628
+ if (!params.runtime.prompt.isInteractive()) {
15629
+ throw new Error("bootstrap confirmation required. Pass --yes to continue non-interactively.");
15630
+ }
15631
+ const ok = await promptConfirm("Continue with this bootstrap plan?", true, params.runtime);
15632
+ if (!ok) {
15633
+ throw new Error("bootstrap was not confirmed");
15453
15634
  }
15454
- return bucket;
15635
+ }
15636
+ async function createOrAdoptBucket(params) {
15637
+ if (params.bucketPlan.exists) {
15638
+ params.runtime.stdout.log(`Using existing private bucket ${params.bucketPlan.bucket}`);
15639
+ return;
15640
+ } else {
15641
+ params.runtime.stdout.log(`Creating private bucket ${params.bucketPlan.bucket}`);
15642
+ }
15643
+ await params.hub.createBucket(params.bucketPlan.bucket, true);
15455
15644
  }
15456
15645
  async function stateAdopt(agent, opts, runtime) {
15457
15646
  const bucket = parseBucketId(requiredOption(opts.bucket, "--bucket"));
@@ -15622,14 +15811,14 @@ async function writeLocalDeployment(configRoot2, manifest, secrets) {
15622
15811
  }
15623
15812
  async function deploySpaceGateway(params) {
15624
15813
  const { hub, runtime, hfToken, manifest, secrets } = params;
15625
- runtime.stdout.log(`Creating ${params.publicSpace ? "public" : "private"} Space ${manifest.space}`);
15814
+ runtime.stdout.log(params.spaceExists ? `Updating existing Space ${manifest.space}` : `Creating ${params.publicSpace ? "public" : "private"} Space ${manifest.space}`);
15626
15815
  await hub.createDockerSpace(manifest.space, {
15627
15816
  private: !params.publicSpace,
15628
15817
  hardware: params.hardware,
15629
15818
  ...typeof params.sleepTime === "number" ? { sleepTimeSeconds: params.sleepTime } : {}
15630
15819
  });
15631
15820
  await hub.requestSpaceHardware(manifest.space, params.hardware, params.sleepTime);
15632
- runtime.stdout.log(params.templateRuntimeImage ? "Generating Space files from explicit runtime image" : "Generating bundled Space runtime files");
15821
+ runtime.stdout.log(params.templateRuntimeImage ? "Generating Space files from prebuilt runtime image" : "Generating bundled Space runtime files");
15633
15822
  const { templateRev } = await runtime.pushTemplateToSpace({
15634
15823
  targetRepo: manifest.space,
15635
15824
  token: hfToken,
@@ -15830,7 +16019,7 @@ async function gatewayMigrate(agent, opts, runtime) {
15830
16019
  });
15831
16020
  await handoffAndStopLocalGateway({ manifest: current, hub, runtime, bucketPrefix });
15832
16021
  const me2 = await hub.whoami();
15833
- const templateRuntimeImage = resolveRuntimeImageOverride(opts.runtimeImage, runtime.env);
16022
+ const templateRuntimeImage = resolveSpaceRuntimeImage(opts, runtime.env);
15834
16023
  await deploySpaceGateway({
15835
16024
  hub,
15836
16025
  runtime,
@@ -16179,10 +16368,11 @@ function shouldPull(opts) {
16179
16368
  }
16180
16369
  async function update(repoId, opts, hub, hfToken, runtime) {
16181
16370
  const variables = await hub.getSpaceVariables(repoId);
16182
- if (!variables.has("MLCLAW_TEMPLATE_REV") && !variables.has("OPENCLAW_HF_TEMPLATE_REV") && !opts.force) {
16371
+ const canonicalTemplate = isCanonicalTemplateSpace(repoId, runtime.env);
16372
+ if (!canonicalTemplate && !variables.has("MLCLAW_TEMPLATE_REV") && !variables.has("OPENCLAW_HF_TEMPLATE_REV") && !opts.force) {
16183
16373
  throw new Error(`${repoId} does not look like a ML Claw deployment; pass --force to update anyway`);
16184
16374
  }
16185
- const runtimeImage = resolveRuntimeImageOverride(opts.runtimeImage, runtime.env);
16375
+ const runtimeImage = resolveSpaceRuntimeImage(opts, runtime.env);
16186
16376
  const agentName = variables.get("OPENCLAW_AGENT_NAME")?.value?.trim() || repoId.split("/")[1] || "openclaw";
16187
16377
  runtime.stdout.log(`Generating current Space files into ${repoId}`);
16188
16378
  const { templateRev } = await runtime.pushTemplateToSpace({
@@ -16192,6 +16382,12 @@ async function update(repoId, opts, hub, hfToken, runtime) {
16192
16382
  });
16193
16383
  await hub.addSpaceVariable(repoId, "MLCLAW_TEMPLATE_REV", templateRev);
16194
16384
  await hub.addSpaceVariable(repoId, "MLCLAW_RUNTIME_IMAGE", runtimeImage ?? bundledSpaceRuntimeRef(templateRev));
16385
+ if (canonicalTemplate) {
16386
+ await hub.addSpaceVariable(repoId, "MLCLAW_CANONICAL_SPACE_ID", canonicalTemplateSpaceId(runtime.env));
16387
+ await doctor(repoId, { fix: true }, hub, runtime);
16388
+ await hub.restartSpace(repoId, true);
16389
+ return;
16390
+ }
16195
16391
  await hub.addSpaceVariable(repoId, "MLCLAW_GATEWAY_LOCATION", "space");
16196
16392
  await hub.addSpaceVariable(repoId, "MLCLAW_RUNTIME_ID", spaceRuntimeId(agentName));
16197
16393
  await hub.addSpaceVariable(repoId, "MLCLAW_OPENCLAW_PORT", String(DEFAULT_SPACE_OPENCLAW_PORT));
@@ -16209,6 +16405,42 @@ async function doctor(repoId, opts, hub, runtime) {
16209
16405
  const secrets = await hub.getSpaceSecrets(repoId);
16210
16406
  const issues = [];
16211
16407
  const fixed = [];
16408
+ const canonicalTemplate = isCanonicalTemplateSpace(repoId, runtime.env);
16409
+ if (canonicalTemplate) {
16410
+ const expectedCanonicalSpace = canonicalTemplateSpaceId(runtime.env);
16411
+ if ((variables.get("MLCLAW_CANONICAL_SPACE_ID")?.value ?? "") !== expectedCanonicalSpace) {
16412
+ if (fix) {
16413
+ await hub.addSpaceVariable(repoId, "MLCLAW_CANONICAL_SPACE_ID", expectedCanonicalSpace);
16414
+ fixed.push("set MLCLAW_CANONICAL_SPACE_ID");
16415
+ } else {
16416
+ issues.push(`MLCLAW_CANONICAL_SPACE_ID is not ${expectedCanonicalSpace}`);
16417
+ }
16418
+ }
16419
+ if (!variables.has("MLCLAW_TEMPLATE_REV") && !variables.has("OPENCLAW_HF_TEMPLATE_REV")) {
16420
+ issues.push("MLCLAW_TEMPLATE_REV is missing; run `mlclaw update` to refresh the template Space");
16421
+ }
16422
+ addRuntimeImageFindings(variables.get("MLCLAW_RUNTIME_IMAGE")?.value, issues);
16423
+ const runtimeInfo2 = await hub.getSpaceRuntime(repoId);
16424
+ runtime.stdout.log(`Space: ${repoId}`);
16425
+ runtime.stdout.log("Mode: template");
16426
+ runtime.stdout.log(`Stage: ${runtimeInfo2.stage ?? "unknown"}`);
16427
+ runtime.stdout.log(`Hardware: ${formatRuntimeValue(runtimeInfo2.requested_hardware ?? runtimeInfo2.hardware)}`);
16428
+ if (typeof runtimeInfo2.sleep_time === "number") {
16429
+ runtime.stdout.log(`Sleep time: ${runtimeInfo2.sleep_time}`);
16430
+ }
16431
+ if (fixed.length > 0) {
16432
+ runtime.stdout.log(`Fixed: ${fixed.join(", ")}`);
16433
+ }
16434
+ if (issues.length === 0) {
16435
+ runtime.stdout.log("Doctor: clean");
16436
+ } else {
16437
+ runtime.stdout.log("Doctor findings:");
16438
+ for (const issue of issues) {
16439
+ runtime.stdout.log(`- ${issue}`);
16440
+ }
16441
+ }
16442
+ return;
16443
+ }
16212
16444
  const bucket = variables.get("OPENCLAW_HF_STATE_BUCKET")?.value ?? opts.bucket;
16213
16445
  let signedInUser;
16214
16446
  const currentUsername = async () => {
@@ -16248,9 +16480,7 @@ async function doctor(repoId, opts, hub, runtime) {
16248
16480
  if ((variables.get("MLCLAW_GATEWAY_LOCATION")?.value ?? "") !== "space") {
16249
16481
  issues.push("MLCLAW_GATEWAY_LOCATION is not set to space");
16250
16482
  }
16251
- if (!variables.has("MLCLAW_RUNTIME_IMAGE")) {
16252
- issues.push("MLCLAW_RUNTIME_IMAGE is missing");
16253
- }
16483
+ addRuntimeImageFindings(variables.get("MLCLAW_RUNTIME_IMAGE")?.value, issues);
16254
16484
  if ((variables.get("MLCLAW_OPENCLAW_PORT")?.value ?? "") !== String(DEFAULT_SPACE_OPENCLAW_PORT) && fix) {
16255
16485
  await hub.addSpaceVariable(repoId, "MLCLAW_OPENCLAW_PORT", String(DEFAULT_SPACE_OPENCLAW_PORT));
16256
16486
  fixed.push("set MLCLAW_OPENCLAW_PORT");
@@ -16315,6 +16545,26 @@ async function doctor(repoId, opts, hub, runtime) {
16315
16545
  }
16316
16546
  }
16317
16547
  }
16548
+ function canonicalTemplateSpaceId(env) {
16549
+ return nonEmpty(env.MLCLAW_CANONICAL_SPACE_ID) ?? DEFAULT_CANONICAL_TEMPLATE_SPACE;
16550
+ }
16551
+ function isCanonicalTemplateSpace(repoId, env) {
16552
+ return repoId === canonicalTemplateSpaceId(env);
16553
+ }
16554
+ function addRuntimeImageFindings(value, issues) {
16555
+ const runtimeImage = value?.trim();
16556
+ if (!runtimeImage) {
16557
+ issues.push("MLCLAW_RUNTIME_IMAGE is missing; run `mlclaw update` to refresh the Space runtime");
16558
+ return;
16559
+ }
16560
+ if (runtimeImage.startsWith("ghcr.io/osolmaz/mlclaw-runtime:")) {
16561
+ issues.push(`MLCLAW_RUNTIME_IMAGE points at the legacy mlclaw-runtime package; run \`mlclaw update\` to use ${DEFAULT_RUNTIME_IMAGE}`);
16562
+ return;
16563
+ }
16564
+ if (runtimeImage.startsWith("bundled:")) {
16565
+ issues.push(`MLCLAW_RUNTIME_IMAGE uses a bundled runtime; run \`mlclaw update\` to use ${DEFAULT_RUNTIME_IMAGE}`);
16566
+ }
16567
+ }
16318
16568
  async function settings(repoId, opts, hub, runtime) {
16319
16569
  if (opts.gateway) {
16320
16570
  throw new Error("gateway location changes must use `mlclaw gateway migrate` to preserve state");
@@ -16391,7 +16641,7 @@ async function promptAgentName(runtime) {
16391
16641
  }
16392
16642
  const value = await runtime.prompt.text({
16393
16643
  message: "Agent name",
16394
- placeholder: "bob",
16644
+ placeholder: "mlclaw",
16395
16645
  initialValue: "mlclaw"
16396
16646
  });
16397
16647
  return readPromptValue(value, "Agent name");