@floomhq/skills 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4377,7 +4377,7 @@ async function getMachineIdentity() {
4377
4377
  }
4378
4378
 
4379
4379
  // src/version.ts
4380
- var VERSION = "1.1.0";
4380
+ var VERSION = "1.1.1";
4381
4381
 
4382
4382
  // src/api-client.ts
4383
4383
  var DEFAULT_TIMEOUT_MS = 2e4;
@@ -7702,19 +7702,24 @@ async function renameDeviceCommand(newLabel) {
7702
7702
 
7703
7703
  // src/commands/install.ts
7704
7704
  init_src();
7705
- import { mkdir as mkdir6, rm as rm3, writeFile as writeFile5, stat as stat7, readFile as readFile11 } from "node:fs/promises";
7705
+ import { mkdir as mkdir6, rm as rm3, writeFile as writeFile5, stat as stat7, lstat as lstat4, readFile as readFile11 } from "node:fs/promises";
7706
7706
  import { dirname as dirname2, join as join12, resolve as resolve8 } from "node:path";
7707
+ import { z as z7 } from "zod";
7707
7708
  init_runtime();
7708
7709
  init_prompt();
7709
7710
  var TOKEN_RE = /^fls_[A-Za-z0-9_-]{32,}$/;
7710
7711
  var SAFE_SLUG_RE2 = /^(?=.{1,80}$)[a-z0-9]+(?:-[a-z0-9]+)*$/;
7711
7712
  var MAX_DECODED_FILE_BYTES2 = 2 * 1024 * 1024;
7713
+ var TRUSTED_CATALOG_HOSTS = /* @__PURE__ */ new Set(["skills.floom.dev", "cyborg.floom.dev"]);
7714
+ var CATALOG_LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost"]);
7712
7715
  function trustedCatalogOrigin(url) {
7713
- if (url.protocol === "https:") return true;
7716
+ if (TRUSTED_CATALOG_HOSTS.has(url.hostname)) return url.protocol === "https:";
7714
7717
  const dev = process.env.FLOOM_DEV;
7715
7718
  const devMode = dev === "1" || dev === "true";
7716
- return devMode && url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost");
7719
+ if (!devMode || !CATALOG_LOOPBACK_HOSTS.has(url.hostname)) return false;
7720
+ return url.protocol === "http:" || url.protocol === "https:";
7717
7721
  }
7722
+ var trustedCatalogHosts = () => [...TRUSTED_CATALOG_HOSTS];
7718
7723
  function parseInstallSource(input) {
7719
7724
  const trimmed = input.trim();
7720
7725
  if (TOKEN_RE.test(trimmed)) return { kind: "share", token: trimmed };
@@ -7734,6 +7739,34 @@ function parseInstallSource(input) {
7734
7739
  }
7735
7740
  return null;
7736
7741
  }
7742
+ function untrustedCatalogHost(input) {
7743
+ let url;
7744
+ try {
7745
+ url = new URL(input.trim());
7746
+ } catch {
7747
+ return null;
7748
+ }
7749
+ const parts = url.pathname.split("/").filter(Boolean);
7750
+ const last = parts[parts.length - 1];
7751
+ const parent = parts.length >= 2 ? parts[parts.length - 2] : null;
7752
+ if (parent !== "skills" || !last || !SAFE_SLUG_RE2.test(last)) return null;
7753
+ return trustedCatalogOrigin(url) ? null : url.host;
7754
+ }
7755
+ var ShareFileSchema = z7.object({
7756
+ path: z7.string().min(1).max(1024),
7757
+ mode: z7.enum(["text", "non_text"]),
7758
+ content_text: z7.string().nullable().optional().default(null),
7759
+ binary: z7.object({ download_url: z7.string().nullable().optional().default(null) }).passthrough().nullable().optional().default(null)
7760
+ }).passthrough();
7761
+ var PublicShareResponseSchema = z7.object({
7762
+ skill: z7.object({
7763
+ title: z7.string().min(1).max(500),
7764
+ description: z7.string().max(2e4).optional().default(""),
7765
+ owner: z7.object({ name: z7.string().min(1).max(500) }).passthrough(),
7766
+ version: z7.object({ version_seq: z7.number().int().nonnegative(), display: z7.string().min(1).max(100) }).passthrough()
7767
+ }).passthrough(),
7768
+ file_contents: z7.array(ShareFileSchema).max(5e3)
7769
+ }).passthrough();
7737
7770
  function safeSkillSlug2(slug) {
7738
7771
  if (!slug || !SAFE_SLUG_RE2.test(slug)) throw new Error(`Unsafe shared skill name: ${slug}`);
7739
7772
  return slug;
@@ -7789,21 +7822,43 @@ async function fetchInstallSource(source) {
7789
7822
  } catch {
7790
7823
  throw new Error("Unexpected response from Floom API. Try again in a moment.");
7791
7824
  }
7792
- const candidate = json;
7793
- if (!candidate?.skill || !Array.isArray(candidate.file_contents)) {
7825
+ const parsed = PublicShareResponseSchema.safeParse(json);
7826
+ if (!parsed.success) {
7794
7827
  throw new Error("Unexpected response shape from Floom API.");
7795
7828
  }
7796
- return candidate;
7829
+ return parsed.data;
7797
7830
  }
7798
7831
  var nativePathOperations = { dirname: dirname2 };
7799
7832
  function installParentDirectory(destination, pathOperations = nativePathOperations) {
7800
7833
  return pathOperations.dirname(destination);
7801
7834
  }
7835
+ async function stagingRoot(skillsDir) {
7836
+ let current = skillsDir;
7837
+ for (const segment of [".floom", "tmp"]) {
7838
+ current = join12(current, segment);
7839
+ let entry;
7840
+ try {
7841
+ entry = await lstat4(current);
7842
+ } catch (error) {
7843
+ if (error.code !== "ENOENT") throw error;
7844
+ await mkdir6(current, { recursive: true });
7845
+ continue;
7846
+ }
7847
+ if (entry.isSymbolicLink()) {
7848
+ throw new Error(`Refusing to stage an install through a symlinked ${tildePath(current)}.`);
7849
+ }
7850
+ if (!entry.isDirectory()) {
7851
+ throw new Error(`Refusing to stage an install: ${tildePath(current)} is not a directory.`);
7852
+ }
7853
+ }
7854
+ return current;
7855
+ }
7802
7856
  async function installToDir(target, slug, files) {
7803
7857
  const finalDir = join12(target.skillsDir, slug);
7804
- const tempDir = join12(target.skillsDir, ".floom", "tmp", `${slug}-install-${Date.now()}`);
7805
- const replacedDir = join12(target.skillsDir, ".floom", "tmp", `${slug}-previous-${Date.now()}`);
7806
- await mkdir6(tempDir, { recursive: true });
7858
+ const staging = await stagingRoot(target.skillsDir);
7859
+ const tempDir = join12(staging, `${slug}-install-${Date.now()}`);
7860
+ const replacedDir = join12(staging, `${slug}-previous-${Date.now()}`);
7861
+ await mkdir6(tempDir);
7807
7862
  try {
7808
7863
  for (const file of files) {
7809
7864
  const safePath = safeRemotePath2(file.path);
@@ -7864,9 +7919,15 @@ async function installCommand(input, rawOpts = {}) {
7864
7919
  const json = flags.json;
7865
7920
  const source = parseInstallSource(input);
7866
7921
  if (!source) {
7922
+ const untrustedHost = untrustedCatalogHost(input);
7867
7923
  if (!json) {
7868
- log.err("That doesn't look like a Floom share link. It must look like: https://skills.floom.dev/s/fls_xxx");
7869
- log.info("A public catalog Skill page also works: https://<catalog host>/skills/<slug>");
7924
+ if (untrustedHost) {
7925
+ log.err(`Refusing to install from ${untrustedHost}: it is not a Floom catalog host.`);
7926
+ log.info(`Catalog installs are accepted only from: ${trustedCatalogHosts().join(", ")}`);
7927
+ } else {
7928
+ log.err("That doesn't look like a Floom share link. It must look like: https://skills.floom.dev/s/fls_xxx");
7929
+ log.info(`A public catalog Skill page also works: https://${trustedCatalogHosts()[0]}/skills/<slug>`);
7930
+ }
7870
7931
  }
7871
7932
  process.exitCode = 2;
7872
7933
  return;
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "1.1.0";
1
+ export const VERSION = "1.1.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/skills",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Floom Skills CLI \u2014 publish, install, sync, and share AI agent skills.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://skills.floom.dev",