@floomhq/floom 1.0.36 → 1.0.38

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/cli.js CHANGED
@@ -126,6 +126,75 @@ function commandUsage() {
126
126
  `;
127
127
  process.stdout.write(out);
128
128
  }
129
+ function shareUsage() {
130
+ process.stdout.write(`
131
+ ${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} share`)} ${c.dim("<slug> [flags]")}
132
+
133
+ ${c.bold("Manage email access for one of your skills.")}
134
+ ${c.cyan(`${CLI_COMMAND} share support-tone --add person@example.com`)}
135
+ ${c.cyan(`${CLI_COMMAND} share support-tone --remove person@example.com`)}
136
+ ${c.cyan(`${CLI_COMMAND} share support-tone --list`)}
137
+
138
+ ${c.bold("Flags")}
139
+ ${c.cyan("--add <email>")} Grant access
140
+ ${c.cyan("--remove <email>")} Revoke access
141
+ ${c.cyan("--list")} Show who can access the skill
142
+ `);
143
+ }
144
+ function libraryUsage() {
145
+ process.stdout.write(`
146
+ ${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} library`)} ${c.dim("<command> [args] [flags]")}
147
+
148
+ ${c.bold("Commands")}
149
+ ${c.cyan("list")} Browse public libraries
150
+ ${c.cyan("create <slug> --name <name>")} Create a library
151
+ ${c.cyan("add <library> <skill>")} Add a skill to a library
152
+ ${c.cyan("remove <library> <skill>")} Remove a skill from a library
153
+ ${c.cyan("subscribe <library>")} Follow a library
154
+ ${c.cyan("unsubscribe <library>")} Stop following a library
155
+
156
+ ${c.bold("Examples")}
157
+ ${c.cyan(`${CLI_COMMAND} library list --json`)}
158
+ ${c.cyan(`${CLI_COMMAND} library create team-onboarding --name "Team onboarding" --public`)}
159
+ ${c.cyan(`${CLI_COMMAND} library add team-onboarding support-tone --folder support --tags support,tone`)}
160
+ `);
161
+ }
162
+ function moveUsage() {
163
+ process.stdout.write(`
164
+ ${c.bold("Usage:")} ${c.cyan(`${CLI_COMMAND} move`)} ${c.dim("<slug> --folder <path> [--tag <tag>]")}
165
+
166
+ ${c.bold("Place a saved or subscribed skill in a local folder.")}
167
+ ${c.cyan(`${CLI_COMMAND} move support-tone --folder support/tone`)}
168
+ ${c.cyan(`${CLI_COMMAND} move support-tone --root`)}
169
+ ${c.cyan(`${CLI_COMMAND} move support-tone --folder support --tags support,tone`)}
170
+
171
+ ${c.bold("Flags")}
172
+ ${c.cyan("--folder <path>")} Folder path for synced installs
173
+ ${c.cyan("--root")} Put the skill at the root
174
+ ${c.cyan("--tag <tag>")} Add one tag, repeatable
175
+ ${c.cyan("--tags a,b")} Add comma-separated tags
176
+ `);
177
+ }
178
+ function isHelpArg(value) {
179
+ return value === "--help" || value === "-h" || value === "help";
180
+ }
181
+ function subcommandUsage(cmd) {
182
+ switch (cmd) {
183
+ case "share":
184
+ shareUsage();
185
+ return true;
186
+ case "library":
187
+ case "lib":
188
+ libraryUsage();
189
+ return true;
190
+ case "move":
191
+ moveUsage();
192
+ return true;
193
+ default:
194
+ commandUsage();
195
+ return true;
196
+ }
197
+ }
129
198
  const ASSET_TYPES = new Set(["knowledge", "instruction", "workflow", "skill"]);
130
199
  const INSTALL_TARGETS = new Set([
131
200
  "claude_skill",
@@ -695,10 +764,8 @@ async function main() {
695
764
  // never block on update-notifier
696
765
  }
697
766
  }
698
- // Subcommand --help: any rest arg = --help/-h/help → show top-level usage.
699
- // Subcommands are simple enough that one help screen is fine for Version 1.
700
- if (rest.includes("--help") || rest.includes("-h") || rest.includes("help")) {
701
- usage();
767
+ if (rest.some(isHelpArg)) {
768
+ subcommandUsage(cmd);
702
769
  return;
703
770
  }
704
771
  try {
package/dist/errors.js CHANGED
@@ -12,7 +12,7 @@ export class FloomError extends Error {
12
12
  this.hint = hint;
13
13
  }
14
14
  }
15
- export function friendlyHttp(status, action) {
15
+ export function friendlyHttp(status, action, detail) {
16
16
  if (status === 401) {
17
17
  return new FloomError("Your token expired.", "Run `npx -y @floomhq/floom login` to refresh.");
18
18
  }
@@ -34,6 +34,9 @@ export function friendlyHttp(status, action) {
34
34
  if (status >= 500) {
35
35
  return new FloomError("Floom is having trouble right now.", "Try again in a moment.");
36
36
  }
37
+ if (detail) {
38
+ return new FloomError(detail);
39
+ }
37
40
  return new FloomError(`Request failed (HTTP ${status}) while trying to ${action}.`);
38
41
  }
39
42
  export function friendlyNetwork(err) {
package/dist/lib/api.js CHANGED
@@ -39,7 +39,7 @@ export async function floomFetch(url, action, opts = {}) {
39
39
  continue;
40
40
  }
41
41
  if (opts.checkOk !== false && !res.ok) {
42
- throw friendlyHttp(res.status, action);
42
+ throw friendlyHttp(res.status, action, await responseErrorDetail(res));
43
43
  }
44
44
  return res;
45
45
  }
@@ -94,6 +94,29 @@ async function drainResponse(res) {
94
94
  // Ignore bodies from rate-limit responses; the retry decision is header-based.
95
95
  }
96
96
  }
97
+ async function responseErrorDetail(res) {
98
+ try {
99
+ const text = await res.text();
100
+ if (!text.trim())
101
+ return null;
102
+ try {
103
+ const json = JSON.parse(text);
104
+ const detail = typeof json.error === "string"
105
+ ? json.error
106
+ : typeof json.message === "string"
107
+ ? json.message
108
+ : null;
109
+ const hint = typeof json.hint === "string" ? json.hint : null;
110
+ return [detail, hint].filter(Boolean).join("\n") || null;
111
+ }
112
+ catch {
113
+ return text.trim().slice(0, 400);
114
+ }
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
97
120
  function sleep(ms) {
98
121
  return new Promise((resolve) => setTimeout(resolve, ms));
99
122
  }
package/dist/sync.js CHANGED
@@ -270,7 +270,7 @@ export async function sync(opts = {}) {
270
270
  const spinner = opts.spinner === false ? null : ora({ text: c.dim("Syncing skills..."), color: "yellow" }).start();
271
271
  let payload;
272
272
  try {
273
- payload = await getJson(`${apiUrl}/api/v1/me/skills`, "load your skills", cfg.accessToken);
273
+ payload = await loadSyncPayload(apiUrl, cfg.accessToken);
274
274
  }
275
275
  catch (err) {
276
276
  spinner?.stop();
@@ -442,3 +442,28 @@ export async function sync(opts = {}) {
442
442
  throw err;
443
443
  }
444
444
  }
445
+ async function loadSyncPayload(apiUrl, token) {
446
+ const all = [];
447
+ let fullSync = false;
448
+ let cursor;
449
+ const seenCursors = new Set();
450
+ for (let page = 0; page < 1000; page += 1) {
451
+ const url = new URL(`${apiUrl}/api/v1/me/skills`);
452
+ url.searchParams.set("limit", "25");
453
+ if (cursor)
454
+ url.searchParams.set("cursor", cursor);
455
+ const payload = await getJson(url.toString(), "load your skills", token);
456
+ if (!Array.isArray(payload.skills))
457
+ throw new FloomError("Invalid sync response.");
458
+ all.push(...payload.skills);
459
+ fullSync = payload.full_sync === true;
460
+ if (!payload.next_cursor) {
461
+ return { skills: all, full_sync: fullSync };
462
+ }
463
+ if (seenCursors.has(payload.next_cursor))
464
+ throw new FloomError("Invalid sync response.");
465
+ seenCursors.add(payload.next_cursor);
466
+ cursor = payload.next_cursor;
467
+ }
468
+ throw new FloomError("Invalid sync response.");
469
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "1.0.36",
3
+ "version": "1.0.38",
4
4
  "description": "Sync AI skills across agents and machines.",
5
5
  "license": "MIT",
6
6
  "type": "module",