@floomhq/floom 1.0.35 → 1.0.37

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
  }
@@ -156,9 +156,32 @@ export async function pushWatchOnce(opts) {
156
156
  const pushed = pushManifest.files[pushKey];
157
157
  if (pushed?.hash === hash) {
158
158
  if (!isUnchangedSyncedPackage(root, skillPackage, syncManifest)) {
159
+ if (opts.yolo) {
160
+ try {
161
+ await publishSkillPath({ file: packagePath, update: true, updateSlug: pushed.slug, quiet: true });
162
+ updated += 1;
163
+ }
164
+ catch (err) {
165
+ if (err instanceof Error && /Skill not found/i.test(err.message)) {
166
+ const result = await publishSkillPath({ file: packagePath, visibility: "unlisted", quiet: true });
167
+ published += 1;
168
+ pushManifest.files[pushKey] = { hash, slug: result.data.slug, path: key, pushedAt: new Date().toISOString() };
169
+ markPackageSynced(root, skillPackage, syncManifest, result.data.slug);
170
+ await writeSyncManifest(syncManifest);
171
+ continue;
172
+ }
173
+ skipped += 1;
174
+ if (!opts.quiet) {
175
+ process.stderr.write(`[floom] skipped ${packagePath}: ${err instanceof Error ? err.message : String(err)}\n`);
176
+ }
177
+ continue;
178
+ }
179
+ }
180
+ else {
181
+ adopted += 1;
182
+ }
159
183
  markPackageSynced(root, skillPackage, syncManifest, pushed.slug);
160
184
  await writeSyncManifest(syncManifest);
161
- adopted += 1;
162
185
  }
163
186
  else {
164
187
  skipped += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "1.0.35",
3
+ "version": "1.0.37",
4
4
  "description": "Sync AI skills across agents and machines.",
5
5
  "license": "MIT",
6
6
  "type": "module",