@floomhq/floom 1.0.55 → 1.0.57

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
@@ -26,6 +26,7 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
26
26
 
27
27
  - `npx -y @floomhq/floom login` — sign in with Google. New accounts are created on first login. Token stored at `~/.floom/config.json`.
28
28
  - `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.
29
+ - `npx -y @floomhq/floom scan <path>` — check a skill file or package for high-confidence secrets, prompt-injection text, exfiltration instructions, and unsupported package layout before publishing.
29
30
  - `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>`.
30
31
  - `npx -y @floomhq/floom share <slug>` — email-share one of your skills. Optional `--add <email>`, `--remove <email>`, and `--list`.
31
32
  - `npx -y @floomhq/floom list` — show your published skills. Optional `--json`.
package/dist/audit.js CHANGED
@@ -5,6 +5,7 @@ import { getJson, postJson } from "./lib/api.js";
5
5
  import { FloomError } from "./errors.js";
6
6
  import { c, symbols } from "./ui.js";
7
7
  const FIXTURE_RE = /\b(?:cli lifecycle audit fixture|launch gate|temp skill|test fixture|audit fixture)\b/i;
8
+ const ARCHIVE_CHUNK_SIZE = 1000;
8
9
  function contentHash(skill) {
9
10
  return skill.content_sha256 ?? skill.content_hash ?? undefined;
10
11
  }
@@ -71,10 +72,21 @@ function scoreSkill(skill, duplicateGroup) {
71
72
  reasons,
72
73
  ...(hash ? { content_hash: hash } : {}),
73
74
  ...(duplicateGroup ? { duplicate_group: duplicateGroup } : {}),
74
- recommended_action: score >= 70 ? "archive" : "review",
75
+ recommended_action: archiveRecommended(reasons) ? "archive" : "review",
75
76
  safe: !reasons.includes("possible_secret"),
76
77
  };
77
78
  }
79
+ function archiveRecommended(reasons) {
80
+ if (reasons.includes("possible_secret"))
81
+ return true;
82
+ if (reasons.includes("launch_or_test_fixture"))
83
+ return true;
84
+ if (reasons.includes("blank_title") && reasons.includes("duplicate_content_hash"))
85
+ return true;
86
+ if (reasons.includes("near_empty_body") && reasons.includes("duplicate_content_hash"))
87
+ return true;
88
+ return false;
89
+ }
78
90
  async function loadOwnedSkills() {
79
91
  const cfg = await readConfig();
80
92
  if (!cfg)
@@ -123,7 +135,20 @@ async function applyArchivePlan(planPath, yes, json) {
123
135
  throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
124
136
  const raw = JSON.parse(await readFile(planPath, "utf8"));
125
137
  const slugs = archiveSlugsFromPlan(raw);
126
- const payload = await postJson(`${resolveApiUrl(cfg)}/api/v1/me/skills/archive`, yes ? "archive skills" : "preview skill archive", cfg.accessToken, { slugs, dry_run: !yes });
138
+ const chunks = [];
139
+ for (let i = 0; i < slugs.length; i += ARCHIVE_CHUNK_SIZE)
140
+ chunks.push(slugs.slice(i, i + ARCHIVE_CHUNK_SIZE));
141
+ const responses = [];
142
+ for (const chunk of chunks) {
143
+ responses.push(await postJson(`${resolveApiUrl(cfg)}/api/v1/me/skills/archive`, yes ? "archive skills" : "preview skill archive", cfg.accessToken, { slugs: chunk, dry_run: !yes }));
144
+ }
145
+ const payload = {
146
+ dry_run: responses.every((response) => response.dry_run),
147
+ requested: responses.reduce((sum, response) => sum + response.requested, 0),
148
+ matched: responses.flatMap((response) => response.matched),
149
+ missing: responses.flatMap((response) => response.missing),
150
+ ...(yes ? { archived: responses.flatMap((response) => response.archived ?? []) } : {}),
151
+ };
127
152
  if (json) {
128
153
  process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
129
154
  return;
package/dist/daemon.js CHANGED
@@ -11,6 +11,7 @@ const SERVICE_NAME = "floom-sync.service";
11
11
  const LAUNCHD_LABEL = "dev.floom.sync";
12
12
  const LOG_PATH = join(CONFIG_DIR, "daemon.log");
13
13
  const STATUS_PATH = join(CONFIG_DIR, "daemon-status.json");
14
+ const NATIVE_BASELINE_VERSION = 2;
14
15
  const MIN_INTERVAL_SECONDS = 30;
15
16
  const MIN_TIMEOUT_SECONDS = 30;
16
17
  function targetsFor(value) {
@@ -127,12 +128,15 @@ async function runTarget(target, opts) {
127
128
  }
128
129
  }
129
130
  if (opts.push && ok) {
130
- if (opts.yolo && !(await fileExists(nativeManifestPath))) {
131
+ if (opts.yolo && !(await fileExists(nativeBaselinePath(target)))) {
131
132
  const baselineResult = await runCommand([String(opts.timeoutSeconds), "watch", "--push", "--once", "--target", target, "--no-yolo"], { FLOOM_SYNC_MANIFEST_PATH: nativeManifestPath });
132
133
  if (baselineResult.code !== 0 || baselineResult.timedOut) {
133
134
  ok = false;
134
135
  error = baselineResult.timedOut ? "native baseline timed out" : `${baselineResult.stdout}\n${baselineResult.stderr}`.trim() || "native baseline failed";
135
136
  }
137
+ else {
138
+ await writeNativeBaselineMarker(target);
139
+ }
136
140
  }
137
141
  }
138
142
  if (opts.push && ok) {
@@ -169,6 +173,18 @@ async function fileExists(path) {
169
173
  throw err;
170
174
  }
171
175
  }
176
+ function nativeBaselinePath(target) {
177
+ return join(CONFIG_DIR, "native-sync-manifests", `${target}.baseline-v${NATIVE_BASELINE_VERSION}.json`);
178
+ }
179
+ async function writeNativeBaselineMarker(target) {
180
+ const path = nativeBaselinePath(target);
181
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
182
+ await writeFile(path, `${JSON.stringify({
183
+ version: NATIVE_BASELINE_VERSION,
184
+ target,
185
+ created_at: new Date().toISOString(),
186
+ }, null, 2)}\n`, { mode: 0o600 });
187
+ }
172
188
  async function sleep(ms) {
173
189
  await new Promise((resolve) => setTimeout(resolve, ms));
174
190
  }
@@ -6,7 +6,7 @@ import { CONFIG_DIR } from "./config.js";
6
6
  import { FloomError } from "./errors.js";
7
7
  import { publishSkillPath } from "./publish.js";
8
8
  import { readSkillPackage } from "./package.js";
9
- import { manifestKey, markSynced, readSyncManifest, writeSyncManifest } from "./sync-manifest.js";
9
+ import { manifestKey, markSynced, readSyncManifest, unmarkSynced, writeSyncManifest } from "./sync-manifest.js";
10
10
  import { c, symbols } from "./ui.js";
11
11
  import { targetSkillsDir } from "./targets.js";
12
12
  const MANIFEST_VERSION = 1;
@@ -14,6 +14,7 @@ const PUSH_MANIFEST_PATH = join(CONFIG_DIR, "push-manifest.json");
14
14
  const MAX_SCAN_DEPTH = 8;
15
15
  const SKIP_DIRS = new Set([".cache", ".git", ".next", ".pytest_cache", "__pycache__", "build", "coverage", "dist", "node_modules", "out"]);
16
16
  const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
17
+ const PROJECTION_MANIFEST_FILENAMES = [".floom-cli-sync-manifest.json", ".floom-sync-manifest.json"];
17
18
  function emptyManifest() {
18
19
  return { version: MANIFEST_VERSION, files: {} };
19
20
  }
@@ -103,6 +104,65 @@ function slugFromSyncManifest(root, skillFilePath, syncManifest) {
103
104
  const entry = syncManifest.files[key];
104
105
  return entry?.slug ?? null;
105
106
  }
107
+ async function readProjectionManifest(root, target, syncManifest) {
108
+ const manifest = { version: 1, files: { ...syncManifest.files } };
109
+ const candidates = [
110
+ ...PROJECTION_MANIFEST_FILENAMES.map((name) => join(root, name)),
111
+ join(CONFIG_DIR, "native-sync-manifests", `${target}.json`),
112
+ ...PROJECTION_MANIFEST_FILENAMES.map((name) => join(CONFIG_DIR, "skill-cache", target, name)),
113
+ ];
114
+ const seen = new Set();
115
+ for (const candidate of candidates) {
116
+ const resolved = resolve(candidate);
117
+ if (seen.has(resolved))
118
+ continue;
119
+ seen.add(resolved);
120
+ const extra = await readSyncManifestFile(resolved);
121
+ Object.assign(manifest.files, extra.files);
122
+ }
123
+ return manifest;
124
+ }
125
+ async function readSyncManifestFile(path) {
126
+ try {
127
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
128
+ try {
129
+ const parsed = JSON.parse(await handle.readFile("utf8"));
130
+ if (parsed.version !== 1 || !parsed.files || typeof parsed.files !== "object")
131
+ return { version: 1, files: {} };
132
+ const files = {};
133
+ for (const [key, entry] of Object.entries(parsed.files)) {
134
+ if (entry &&
135
+ typeof entry === "object" &&
136
+ typeof entry.hash === "string" &&
137
+ typeof entry.slug === "string" &&
138
+ typeof entry.target === "string" &&
139
+ typeof entry.syncedAt === "string" &&
140
+ entry.target === key &&
141
+ SLUG_RE.test(entry.slug)) {
142
+ files[key] = entry;
143
+ }
144
+ }
145
+ return { version: 1, files };
146
+ }
147
+ finally {
148
+ await handle.close();
149
+ }
150
+ }
151
+ catch (err) {
152
+ if (err.code === "ENOENT")
153
+ return { version: 1, files: {} };
154
+ if (err instanceof SyntaxError)
155
+ return { version: 1, files: {} };
156
+ throw err;
157
+ }
158
+ }
159
+ function isFloomCacheRoot(root, target) {
160
+ const relativeRoot = relative(resolve(CONFIG_DIR, "skill-cache", target), resolve(root));
161
+ return relativeRoot === "" || (!relativeRoot.startsWith(`..${sep}`) && relativeRoot !== ".." && !isAbsolute(relativeRoot));
162
+ }
163
+ function isExplicitlyPublished(entry) {
164
+ return entry?.source === "published" || entry?.source === "updated";
165
+ }
106
166
  function slugFromPushManifest(key, pushManifest) {
107
167
  return pushManifest.files[key]?.slug ?? null;
108
168
  }
@@ -133,11 +193,16 @@ export async function pushWatchOnce(opts) {
133
193
  const root = targetSkillsDir(opts.target);
134
194
  const pushManifest = await readPushManifest();
135
195
  const syncManifest = await readSyncManifest();
196
+ const projectionManifest = await readProjectionManifest(root, opts.target, syncManifest);
197
+ const cacheRoot = isFloomCacheRoot(root, opts.target);
136
198
  const packages = await findSkillPackages(root);
199
+ const activePushKeys = new Set();
200
+ const activeSyncKeys = new Set();
137
201
  let published = 0;
138
202
  let updated = 0;
139
203
  let adopted = 0;
140
204
  let skipped = 0;
205
+ let syncManifestChanged = false;
141
206
  for (const packagePath of packages) {
142
207
  let skillPackage;
143
208
  try {
@@ -152,12 +217,23 @@ export async function pushWatchOnce(opts) {
152
217
  }
153
218
  const key = safeRootRelative(root, skillPackage.skillPath);
154
219
  const pushKey = pushManifestKey(opts.target, key);
220
+ activeSyncKeys.add(manifestKey(root, skillPackage.skillPath));
221
+ for (const file of skillPackage.packageFiles) {
222
+ activeSyncKeys.add(manifestKey(root, join(dirname(skillPackage.skillPath), ...file.path.split("/"))));
223
+ }
155
224
  const hash = hashPackage(key, skillPackage.skillBody, skillPackage.packageFiles);
156
225
  const pushed = pushManifest.files[pushKey];
226
+ const projectionSlug = slugFromSyncManifest(root, skillPackage.skillPath, projectionManifest);
227
+ if (cacheRoot || (projectionSlug !== null && !isExplicitlyPublished(pushed))) {
228
+ skipped += 1;
229
+ continue;
230
+ }
231
+ activePushKeys.add(pushKey);
157
232
  if (pushed?.hash === hash) {
158
233
  if (!isUnchangedSyncedPackage(root, skillPackage, syncManifest)) {
159
234
  adopted += 1;
160
235
  markPackageSynced(root, skillPackage, syncManifest, pushed.slug);
236
+ syncManifestChanged = true;
161
237
  await writeSyncManifest(syncManifest);
162
238
  }
163
239
  else {
@@ -174,6 +250,7 @@ export async function pushWatchOnce(opts) {
174
250
  slug: pushedSlug ?? syncedSlug ?? fallbackSlug ?? key,
175
251
  path: key,
176
252
  pushedAt: new Date().toISOString(),
253
+ source: pushed?.source ?? "adopted",
177
254
  };
178
255
  adopted += 1;
179
256
  continue;
@@ -200,16 +277,18 @@ export async function pushWatchOnce(opts) {
200
277
  try {
201
278
  await publishSkillPath({ file: packagePath, update: true, updateSlug: slug, quiet: true });
202
279
  updated += 1;
203
- pushManifest.files[pushKey] = { hash, slug, path: key, pushedAt: new Date().toISOString() };
280
+ pushManifest.files[pushKey] = { hash, slug, path: key, pushedAt: new Date().toISOString(), source: "updated" };
204
281
  markPackageSynced(root, skillPackage, syncManifest, slug);
282
+ syncManifestChanged = true;
205
283
  await writeSyncManifest(syncManifest);
206
284
  }
207
285
  catch (err) {
208
286
  if (err instanceof Error && /Skill not found/i.test(err.message)) {
209
287
  const result = await publishSkillPath({ file: packagePath, visibility: "unlisted", quiet: true });
210
288
  published += 1;
211
- pushManifest.files[pushKey] = { hash, slug: result.data.slug, path: key, pushedAt: new Date().toISOString() };
289
+ pushManifest.files[pushKey] = { hash, slug: result.data.slug, path: key, pushedAt: new Date().toISOString(), source: "published" };
212
290
  markPackageSynced(root, skillPackage, syncManifest, result.data.slug);
291
+ syncManifestChanged = true;
213
292
  await writeSyncManifest(syncManifest);
214
293
  continue;
215
294
  }
@@ -223,8 +302,9 @@ export async function pushWatchOnce(opts) {
223
302
  try {
224
303
  const result = await publishSkillPath({ file: packagePath, visibility: "unlisted", quiet: true });
225
304
  published += 1;
226
- pushManifest.files[pushKey] = { hash, slug: result.data.slug, path: key, pushedAt: new Date().toISOString() };
305
+ pushManifest.files[pushKey] = { hash, slug: result.data.slug, path: key, pushedAt: new Date().toISOString(), source: "published" };
227
306
  markPackageSynced(root, skillPackage, syncManifest, result.data.slug);
307
+ syncManifestChanged = true;
228
308
  await writeSyncManifest(syncManifest);
229
309
  }
230
310
  catch (err) {
@@ -234,7 +314,19 @@ export async function pushWatchOnce(opts) {
234
314
  }
235
315
  }
236
316
  }
317
+ for (const key of Object.keys(pushManifest.files)) {
318
+ if (key.startsWith(`${opts.target}:`) && !activePushKeys.has(key))
319
+ delete pushManifest.files[key];
320
+ }
321
+ for (const key of Object.keys(syncManifest.files)) {
322
+ if (!activeSyncKeys.has(key)) {
323
+ unmarkSynced(syncManifest, key);
324
+ syncManifestChanged = true;
325
+ }
326
+ }
237
327
  await writePushManifest(pushManifest);
328
+ if (syncManifestChanged)
329
+ await writeSyncManifest(syncManifest);
238
330
  if (!opts.quiet && (published > 0 || updated > 0 || adopted > 0)) {
239
331
  process.stdout.write(`${symbols.ok} Floom push watch: ${packages.length} scanned, ${published} published, ${updated} updated, ${adopted} adopted\n`);
240
332
  }
package/dist/sync.js CHANGED
@@ -547,7 +547,7 @@ async function loadSyncPayload(apiUrl, token) {
547
547
  if (!Array.isArray(payload.skills))
548
548
  throw new FloomError("Invalid sync response.");
549
549
  all.push(...payload.skills);
550
- fullSync = payload.full_sync === true;
550
+ fullSync = fullSync || payload.full_sync === true;
551
551
  if (!payload.next_cursor) {
552
552
  return { skills: all, full_sync: fullSync };
553
553
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "1.0.55",
3
+ "version": "1.0.57",
4
4
  "description": "Sync AI skills across agents and machines.",
5
5
  "license": "MIT",
6
6
  "type": "module",