@floomhq/floom 1.0.21 → 1.0.22

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/README.md CHANGED
@@ -4,7 +4,6 @@ Sync AI skills across agents and machines. Publish from your terminal, then add
4
4
 
5
5
  ```bash
6
6
  npm install -g @floomhq/floom
7
- floom init my-skill.md
8
7
  floom init my-skill
9
8
  floom login
10
9
  floom publish my-skill
@@ -24,7 +23,7 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
24
23
 
25
24
  - `npx -y @floomhq/floom login` — sign in with Google. Use `--provider github` for GitHub. New accounts are created on first login. Token stored at `~/.floom/config.json`.
26
25
  - `npx -y @floomhq/floom init [path]` — create a starter skill folder at `<path>/SKILL.md`. Passing an existing-style `file.md` path still creates that Markdown file.
27
- - `npx -y @floomhq/floom publish <path>` — upload a skill folder or Markdown file. Folder packages use `<slug>/SKILL.md` plus optional `references/`, `examples/`, `scripts/`, and `assets/`. Optional `--public` / `--private` / `--unlisted`, `--type knowledge|instruction|workflow|skill`, `--installs-as <target>`, and `--skill-version <label>`.
26
+ - `npx -y @floomhq/floom publish <path>` — upload a skill folder or Markdown file. Folder packages use `<slug>/SKILL.md` plus optional `references/`, `examples/`, `scripts/`, and `assets/`. Optional `--public` / `--private` / `--unlisted`, `--type knowledge|instruction|workflow|skill`, `--installs-as <target>`, `--skill-version <label>`, and `--update [slug-or-url]`.
28
27
  - `npx -y @floomhq/floom share <slug>` — email-share one of your skills. Optional `--add <email>`, `--remove <email>`, and `--list`.
29
28
  - `npx -y @floomhq/floom list` — show your published skills. Optional `--json`.
30
29
  - `npx -y @floomhq/floom add <url-or-slug>` — fetch a skill into `~/.claude/skills/<slug>/SKILL.md` with supporting package files. Optional `--target claude|codex`, `--setup` to connect the agent, and `--force` to replace an existing local copy.
@@ -33,8 +32,8 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
33
32
  - `npx -y @floomhq/floom setup` — add Floom usage guidance to `CLAUDE.md` or `AGENTS.md`. Optional `--target claude|codex`, `--dry-run`, `--yes`.
34
33
  - `npx -y @floomhq/floom connect` — alias for setup.
35
34
  - `npx -y @floomhq/floom mcp` — print MCP setup commands for supported agent CLIs.
36
- - `npx -y @floomhq/floom sync` — preview: pull your published, saved, and subscribed library skills into Claude or Codex. Optional `--target claude|codex`.
37
- - `npx -y @floomhq/floom watch` — preview: run sync repeatedly. Optional `--interval <seconds>`; minimum `10`.
35
+ - `npx -y @floomhq/floom sync` — pull your published, saved, and subscribed library skills into Claude or Codex. Optional `--target claude|codex`.
36
+ - `npx -y @floomhq/floom watch` — run sync repeatedly. Optional `--interval <seconds>`; minimum `10`.
38
37
  - `npx -y @floomhq/floom library list` — list public starter libraries. Optional `--json`.
39
38
  - `npx -y @floomhq/floom library create <slug> --name <name>` — create a personal or starter library. Optional `--public` / `--private` / `--unlisted`.
40
39
  - `npx -y @floomhq/floom library add <library> <skill> [--folder <path>] [--tags a,b]` — add a skill to a library.
@@ -75,7 +74,7 @@ version: 0.1.0
75
74
 
76
75
  Override the API host with `FLOOM_API_URL` (defaults to `https://floom.dev`).
77
76
 
78
- `floom sync` and `floom watch` are v1 preview commands for published, saved, and subscribed library skills. They store a machine-local manifest at `~/.floom/sync-manifest.json`.
77
+ `floom sync` and `floom watch` pull published, saved, and subscribed library skills. They store a machine-local manifest at `~/.floom/sync-manifest.json`.
79
78
  The manifest records hashes for files Floom previously wrote. Sync writes missing files only.
80
79
  Remote updates, existing untracked files, and locally edited tracked files are skipped as conflicts.
81
80
  Symlinks are never followed. To replace a local skill manually, run `floom add <url-or-slug> --force`.
package/dist/cli.js CHANGED
@@ -83,7 +83,7 @@ function commandUsage() {
83
83
  ${c.cyan("scan")} ${c.dim("<path>")} Check for secrets, injection, exfiltration
84
84
  ${c.cyan("publish")} ${c.dim("<path>")} Scan, publish, and print a share link
85
85
  ${c.dim("Flags: --public, --private, --type knowledge|instruction|workflow|skill")}
86
- ${c.dim(" --skill-version <label>")}
86
+ ${c.dim(" --skill-version <label>, --update [slug-or-url]")}
87
87
  ${c.cyan("share")} ${c.dim("<slug>")} Email-share one of your skills
88
88
  ${c.dim("Flags: --add <email>, --remove <email>, --list")}
89
89
 
@@ -107,9 +107,9 @@ function commandUsage() {
107
107
  ${c.dim("Alias: lib")}
108
108
  ${c.cyan("move")} ${c.dim("<slug> --folder <path>")} Place a saved skill in a local folder
109
109
  ${c.cyan("mcp")} Print optional MCP setup guidance
110
- ${c.cyan("sync")} Preview pull of published, saved, and library skills
110
+ ${c.cyan("sync")} Pull published, saved, and library skills
111
111
  ${c.dim("Flags: --target claude|codex (default: claude)")}
112
- ${c.cyan("watch")} Preview polling sync loop
112
+ ${c.cyan("watch")} Run a polling sync loop
113
113
  ${c.dim("Flags: --target claude|codex (default: claude), --interval <seconds>")}
114
114
  ${c.cyan("agent-prompt")} Print the one-line agent instruction
115
115
  ${c.dim("Alias: paste")}
@@ -179,6 +179,7 @@ ${c.bold("Flags")}
179
179
  ${c.cyan("--type")} ${c.dim("knowledge|instruction|workflow|skill")}
180
180
  ${c.cyan("--installs-as")} ${c.dim("claude_skill|memory|rule|codex_instruction|opencode_instruction|cursor_rule|other")}
181
181
  ${c.cyan("--skill-version")} ${c.dim("<label>")}
182
+ ${c.cyan("--update")} ${c.dim("[slug-or-url]")}
182
183
  `,
183
184
  init: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} init`)} ${c.dim("[path] [flags]")}
184
185
 
@@ -189,14 +190,14 @@ ${c.bold("Flags")}
189
190
  `,
190
191
  sync: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} sync`)} ${c.dim("[flags]")}
191
192
 
192
- Preview pull of published, saved, and subscribed library skills.
193
+ Pull published, saved, and subscribed library skills.
193
194
 
194
195
  ${c.bold("Flags")}
195
196
  ${c.cyan("--target")} ${c.dim("claude|codex")} Sync into Claude Code or Codex skills. Default: claude.
196
197
  `,
197
198
  watch: `${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} watch`)} ${c.dim("[flags]")}
198
199
 
199
- Poll sync on an interval. Preview behavior.
200
+ Poll sync on an interval.
200
201
 
201
202
  ${c.bold("Flags")}
202
203
  ${c.cyan("--target")} ${c.dim("claude|codex")} Watch Claude Code or Codex skills. Default: claude.
@@ -292,8 +293,18 @@ function parseFlags(argv) {
292
293
  visibilityFlag = nextVisibility;
293
294
  out.visibility = nextVisibility;
294
295
  }
295
- else if (a === "--update") {
296
- throw new FloomError(V1_NOT_AVAILABLE, `\`${CLI_COMMAND} publish --update\` is planned for a later Floom release.`);
296
+ else if (a === "--update" || a.startsWith("--update=")) {
297
+ out.update = true;
298
+ if (a.startsWith("--update=")) {
299
+ out.updateSlug = a.slice("--update=".length);
300
+ }
301
+ else {
302
+ const next = argv[i + 1];
303
+ if (out.rest.length > 0 && next && !next.startsWith("--")) {
304
+ out.updateSlug = next;
305
+ i += 1;
306
+ }
307
+ }
297
308
  }
298
309
  else if (a === "--share" || a.startsWith("--share=")) {
299
310
  throw new FloomError(V1_NOT_AVAILABLE, `\`${CLI_COMMAND} publish --share\` is planned for a later Floom release.`);
@@ -905,6 +916,7 @@ async function main() {
905
916
  file,
906
917
  visibility: flags.visibility,
907
918
  update: flags.update,
919
+ ...(flags.updateSlug ? { updateSlug: flags.updateSlug } : {}),
908
920
  ...(flags.assetType ? { assetType: flags.assetType } : {}),
909
921
  ...(flags.installsAs ? { installsAs: flags.installsAs } : {}),
910
922
  ...(flags.version ? { version: flags.version } : {}),
package/dist/config.js CHANGED
@@ -5,6 +5,7 @@ export const CONFIG_DIR = join(homedir(), ".floom");
5
5
  export const CONFIG_PATH = join(CONFIG_DIR, "config.json");
6
6
  export const DEFAULT_API_URL = "https://floom.dev";
7
7
  export const DEFAULT_WEB_URL = "https://floom.dev";
8
+ const REFRESH_SKEW_SECONDS = 5 * 60;
8
9
  export function getApiUrl() {
9
10
  return process.env.FLOOM_API_URL?.replace(/\/$/, "") ?? DEFAULT_API_URL;
10
11
  }
@@ -14,7 +15,7 @@ export function resolveApiUrl(cfg) {
14
15
  export function getWebUrl() {
15
16
  return process.env.FLOOM_WEB_URL?.replace(/\/$/, "") ?? DEFAULT_WEB_URL;
16
17
  }
17
- export async function readConfig() {
18
+ export async function readRawConfig() {
18
19
  try {
19
20
  const buf = await readFile(process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH, "utf8");
20
21
  const parsed = JSON.parse(buf);
@@ -28,6 +29,15 @@ export async function readConfig() {
28
29
  throw e;
29
30
  }
30
31
  }
32
+ export async function readConfig() {
33
+ const cfg = await readRawConfig();
34
+ if (!cfg)
35
+ return null;
36
+ if (!needsRefresh(cfg))
37
+ return cfg;
38
+ const refreshed = await refreshConfig(cfg);
39
+ return refreshed;
40
+ }
31
41
  export async function writeConfig(cfg) {
32
42
  const targetPath = process.env.FLOOM_CONFIG_PATH ?? CONFIG_PATH;
33
43
  const targetDir = dirname(targetPath);
@@ -45,3 +55,43 @@ export async function deleteConfig() {
45
55
  throw e;
46
56
  }
47
57
  }
58
+ export function needsRefresh(cfg) {
59
+ return !Number.isFinite(cfg.expiresAt) || cfg.expiresAt <= Math.floor(Date.now() / 1000) + REFRESH_SKEW_SECONDS;
60
+ }
61
+ export async function refreshConfig(cfg) {
62
+ const apiUrl = resolveApiUrl(cfg);
63
+ let res;
64
+ try {
65
+ res = await fetch(`${apiUrl}/api/v1/auth/refresh`, {
66
+ method: "POST",
67
+ headers: { "content-type": "application/json" },
68
+ body: JSON.stringify({ refresh_token: cfg.refreshToken }),
69
+ });
70
+ }
71
+ catch {
72
+ return null;
73
+ }
74
+ if (!res.ok)
75
+ return null;
76
+ let data;
77
+ try {
78
+ data = (await res.json());
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ if (!data.access_token || !data.refresh_token)
84
+ return null;
85
+ const expiresAt = Number.isFinite(data.expires_at)
86
+ ? Number(data.expires_at)
87
+ : Math.floor(Date.now() / 1000) + (Number.isFinite(data.expires_in) ? Number(data.expires_in) : 3600);
88
+ const refreshed = {
89
+ apiUrl,
90
+ accessToken: data.access_token,
91
+ refreshToken: data.refresh_token,
92
+ expiresAt,
93
+ email: data.email ?? cfg.email ?? null,
94
+ };
95
+ await writeConfig(refreshed);
96
+ return refreshed;
97
+ }
package/dist/doctor.js CHANGED
@@ -4,7 +4,7 @@ import { delimiter } from "node:path";
4
4
  import { join } from "node:path";
5
5
  import { stat, readFile, access, readdir, constants, realpath } from "node:fs/promises";
6
6
  import { promisify } from "node:util";
7
- import { readConfig, CONFIG_PATH, resolveApiUrl } from "./config.js";
7
+ import { needsRefresh, readConfig, readRawConfig, CONFIG_PATH, resolveApiUrl } from "./config.js";
8
8
  import { floomFetch } from "./lib/api.js";
9
9
  import { c, symbols } from "./ui.js";
10
10
  import { CLI_VERSION, compareSemverish, formatVersionLabel } from "./version.js";
@@ -17,14 +17,25 @@ function statusBadge(s) {
17
17
  return c.red(symbols.fail);
18
18
  }
19
19
  async function checkAuth() {
20
- const cfg = await readConfig();
21
- if (!cfg) {
20
+ const raw = await readRawConfig();
21
+ if (!raw) {
22
22
  return {
23
23
  name: "Auth",
24
24
  status: "ok",
25
25
  detail: "Receiver mode ready. Sign in only when publishing or listing your own skills.",
26
26
  };
27
27
  }
28
+ const expired = raw.expiresAt <= Math.floor(Date.now() / 1000);
29
+ const expiresSoon = needsRefresh(raw);
30
+ const cfg = await readConfig();
31
+ if (!cfg) {
32
+ return {
33
+ name: "Auth",
34
+ status: "fail",
35
+ detail: expired ? "Token expired and refresh failed." : "Token refresh failed.",
36
+ hint: "Run `npx -y @floomhq/floom logout && npx -y @floomhq/floom login` to sign in again.",
37
+ };
38
+ }
28
39
  const apiUrl = resolveApiUrl(cfg);
29
40
  try {
30
41
  const res = await floomFetch(`${apiUrl}/api/me`, "check authentication", {
@@ -48,10 +59,11 @@ async function checkAuth() {
48
59
  };
49
60
  }
50
61
  const data = (await res.json());
62
+ const suffix = expiresSoon ? " (token refreshed)" : "";
51
63
  return {
52
64
  name: "Auth",
53
65
  status: "ok",
54
- detail: data.email ? `Signed in as ${data.email}` : "Signed in",
66
+ detail: data.email ? `Signed in as ${data.email}${suffix}` : `Signed in${suffix}`,
55
67
  };
56
68
  }
57
69
  catch (err) {
package/dist/errors.js CHANGED
@@ -20,7 +20,7 @@ export function friendlyHttp(status, action) {
20
20
  return new FloomError(`You don't have permission to ${action}.`);
21
21
  }
22
22
  if (status === 404) {
23
- if (/fetch|inspect|add|install|show|get|search|list|info/i.test(action)) {
23
+ if (/fetch|inspect|add|install|show|get|search|list|info|load/i.test(action)) {
24
24
  return new FloomError("Skill not found.", "Check the link or slug, then try again.");
25
25
  }
26
26
  return new FloomError("Skill not found.", "Run `npx -y @floomhq/floom publish <path>` to create a new one.");
package/dist/mcp.js CHANGED
@@ -4,7 +4,7 @@ export function printMcpSetup() {
4
4
  - Use Floom skills from the local Floom skills folder when they match the task.
5
5
  - To install a shared skill, run \`npx -y @floomhq/floom add <slug-or-url> --target claude\` or \`npx -y @floomhq/floom add <slug-or-url> --target codex\`.
6
6
  - To find reusable behavior, run \`npx -y @floomhq/floom search <query>\`.
7
- - MCP sync is optional preview behavior; use it only while the Floom MCP server is configured and running.`;
7
+ - MCP sync is optional; use it while the Floom MCP server is configured and running.`;
8
8
  process.stdout.write(`\n${c.bold("Floom MCP setup")}\n\n`);
9
9
  process.stdout.write(`${c.dim("Pick your tool:")}\n\n`);
10
10
  process.stdout.write(` ${c.bold("Claude Code")}\n`);
package/dist/publish.js CHANGED
@@ -19,6 +19,7 @@ const INSTALL_TARGETS = new Set([
19
19
  "other",
20
20
  ]);
21
21
  const VERSION_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
22
+ const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
22
23
  function parseFrontmatter(input) {
23
24
  const trimmed = input.replace(/^/, "");
24
25
  if (!trimmed.startsWith("---"))
@@ -106,6 +107,33 @@ function parseVersion(value, source) {
106
107
  return value;
107
108
  throw new FloomError(`Invalid ${source}: ${value}`, "Use 1-64 characters: letters, numbers, dots, underscores, plus, or hyphen.");
108
109
  }
110
+ function slugFromInput(input) {
111
+ const trimmed = input.trim();
112
+ try {
113
+ const url = new URL(trimmed);
114
+ const last = url.pathname.split("/").filter(Boolean).at(-1) ?? "";
115
+ return last.replace(/\.(md|json)$/i, "");
116
+ }
117
+ catch {
118
+ return trimmed.replace(/\.(md|json)$/i, "");
119
+ }
120
+ }
121
+ function inferSlugFromPath(path) {
122
+ return path
123
+ .replace(/[\\/]+$/, "")
124
+ .split(/[\\/]+/)
125
+ .filter(Boolean)
126
+ .at(-1)
127
+ ?.replace(/\.md$/i, "") ?? "";
128
+ }
129
+ function resolveUpdateSlug(opts, meta) {
130
+ const raw = opts.updateSlug ?? meta.slug ?? inferSlugFromPath(opts.file);
131
+ const slug = slugFromInput(raw);
132
+ if (!SLUG_RE.test(slug)) {
133
+ throw new FloomError("Missing or invalid update slug.", "Use `npx -y @floomhq/floom publish <path> --update <slug-or-url>`, or add `slug:` to SKILL.md frontmatter.");
134
+ }
135
+ return slug;
136
+ }
109
137
  export async function publish(opts) {
110
138
  const skillPackage = await readSkillPackage(opts.file);
111
139
  const securityFailures = [
@@ -136,11 +164,8 @@ export async function publish(opts) {
136
164
  if (!cfg) {
137
165
  throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
138
166
  }
139
- // Later version: detect already-published when --update is missing.
140
- // The current API does not return a duplicate-skill code, so we leave
141
- // the actual dedupe to the server and surface the friendly hint on 409.
142
167
  const spinner = ora({
143
- text: c.dim(`Publishing ${meta.title ? c.bold(meta.title) : opts.file}...`),
168
+ text: c.dim(`${opts.update ? "Updating" : "Publishing"} ${meta.title ? c.bold(meta.title) : opts.file}...`),
144
169
  color: "yellow",
145
170
  }).start();
146
171
  const apiUrl = resolveApiUrl(cfg);
@@ -156,12 +181,13 @@ export async function publish(opts) {
156
181
  original_filename: skillPackage.originalFilename,
157
182
  published_via: "cli",
158
183
  ...(opts.sharedWithEmails ? { shared_with_emails: opts.sharedWithEmails } : {}),
159
- ...(skillPackage.packageFiles.length > 0 ? { package_files: skillPackage.packageFiles } : {}),
184
+ ...(opts.update || skillPackage.packageFiles.length > 0 ? { package_files: skillPackage.packageFiles } : {}),
160
185
  };
161
186
  let res;
162
187
  try {
163
- res = await floomFetch(`${apiUrl}/api/skills`, "publish your skill", {
164
- method: "POST",
188
+ const updateSlug = opts.update ? resolveUpdateSlug(opts, meta) : null;
189
+ res = await floomFetch(updateSlug ? `${apiUrl}/api/v1/skills/${encodeURIComponent(updateSlug)}` : `${apiUrl}/api/skills`, updateSlug ? "update your skill" : "publish your skill", {
190
+ method: updateSlug ? "PATCH" : "POST",
165
191
  token: cfg.accessToken,
166
192
  checkOk: false,
167
193
  body,
@@ -173,11 +199,11 @@ export async function publish(opts) {
173
199
  }
174
200
  if (res.status === 409) {
175
201
  spinner.stop();
176
- throw new FloomError("Skill already published.", "Use `--update` to publish a new version (coming in a future release).");
202
+ throw new FloomError("Skill already published.", "Use `--update <slug-or-url>` to publish a new version of an existing skill.");
177
203
  }
178
204
  if (!res.ok) {
179
205
  spinner.stop();
180
- throw friendlyHttp(res.status, "publish your skill");
206
+ throw friendlyHttp(res.status, opts.update ? "update your skill" : "publish your skill");
181
207
  }
182
208
  const data = (await res.json());
183
209
  // Build a humans-friendly URL: strip the trailing `.md` if the API returned one.
@@ -188,7 +214,7 @@ export async function publish(opts) {
188
214
  spinner.stop();
189
215
  const versionTag = version ? c.dim(` (${formatVersionLabel(version)})`) : "";
190
216
  const titleLabel = data.title ? `"${data.title}"` : opts.file;
191
- process.stdout.write(`\n${symbols.ok} Published ${c.bold(titleLabel)}${versionTag}\n\n`);
217
+ process.stdout.write(`\n${symbols.ok} ${opts.update ? "Updated" : "Published"} ${c.bold(titleLabel)}${versionTag}\n\n`);
192
218
  process.stdout.write(` ${c.cyan(humanUrl)}\n\n`);
193
219
  if (data.visibility === "shared" && data.shared_with_emails?.length) {
194
220
  process.stdout.write(` ${c.dim(`Shared with ${data.shared_with_emails.join(", ")}`)}\n\n`);
package/dist/setup.js CHANGED
@@ -16,14 +16,16 @@ function floomAgentInstructions(target) {
16
16
  const addCommand = target === "codex"
17
17
  ? `${CLI_COMMAND} add <slug-or-url> --target codex`
18
18
  : `${CLI_COMMAND} add <slug-or-url> --target claude`;
19
+ const localSkillsDir = target === "codex" ? "~/.codex/skills" : "~/.claude/skills";
19
20
  return `${START_MARKER}
20
- ## Floom
21
+ ## Floom Skills
21
22
 
22
- - Before recreating agent behavior from scratch, check Floom for reusable skills.
23
- - Search or inspect skills with \`${CLI_COMMAND} search <query>\`, \`${CLI_COMMAND} info <slug-or-url>\`, and \`${CLI_COMMAND} list\`.
23
+ - Use Floom skills when they fit the task. Search local skills before recreating behavior from scratch.
24
+ - Local skills live in \`${localSkillsDir}\`.
24
25
  - Add shared skills with \`${addCommand}\`; public and unlisted links do not require a Floom account.
25
- - Use installed Markdown skills from the local skills folder when they match the task.
26
- - \`${CLI_COMMAND} sync\`, \`${CLI_COMMAND} watch\`, and \`@floomhq/floom-mcp-sync\` are preview paths for saved, published, and subscribed library skills; review conflicts before relying on synced output.
26
+ - Search or inspect skills with \`${CLI_COMMAND} search <query>\` and \`${CLI_COMMAND} info <slug-or-url>\`.
27
+ - If Floom MCP is available, use it for compact skill search/status and load only the specific skill needed.
28
+ - Do not publish, sync, or subscribe without user approval.
27
29
  ${END_MARKER}`;
28
30
  }
29
31
  async function fileExists(path) {
package/dist/sync.js CHANGED
@@ -298,7 +298,7 @@ export async function sync(opts = {}) {
298
298
  }
299
299
  for (const skill of payload.skills)
300
300
  validateSyncSkillShape(skill);
301
- // Version 1 preview syncs published, saved, and subscribed library skills.
301
+ // Version 1 syncs published, saved, and subscribed library skills.
302
302
  const all = dedupeSyncSkills(payload.skills);
303
303
  const seen = new Set();
304
304
  let unchanged = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "description": "Sync AI skills across agents and machines.",
5
5
  "license": "MIT",
6
6
  "type": "module",