@floomhq/floom 1.0.56 → 1.0.58

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/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
  }
@@ -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,6 +193,8 @@ 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);
137
199
  const activePushKeys = new Set();
138
200
  const activeSyncKeys = new Set();
@@ -155,13 +217,18 @@ export async function pushWatchOnce(opts) {
155
217
  }
156
218
  const key = safeRootRelative(root, skillPackage.skillPath);
157
219
  const pushKey = pushManifestKey(opts.target, key);
158
- activePushKeys.add(pushKey);
159
220
  activeSyncKeys.add(manifestKey(root, skillPackage.skillPath));
160
221
  for (const file of skillPackage.packageFiles) {
161
222
  activeSyncKeys.add(manifestKey(root, join(dirname(skillPackage.skillPath), ...file.path.split("/"))));
162
223
  }
163
224
  const hash = hashPackage(key, skillPackage.skillBody, skillPackage.packageFiles);
164
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);
165
232
  if (pushed?.hash === hash) {
166
233
  if (!isUnchangedSyncedPackage(root, skillPackage, syncManifest)) {
167
234
  adopted += 1;
@@ -183,6 +250,7 @@ export async function pushWatchOnce(opts) {
183
250
  slug: pushedSlug ?? syncedSlug ?? fallbackSlug ?? key,
184
251
  path: key,
185
252
  pushedAt: new Date().toISOString(),
253
+ source: pushed?.source ?? "adopted",
186
254
  };
187
255
  adopted += 1;
188
256
  continue;
@@ -209,7 +277,7 @@ export async function pushWatchOnce(opts) {
209
277
  try {
210
278
  await publishSkillPath({ file: packagePath, update: true, updateSlug: slug, quiet: true });
211
279
  updated += 1;
212
- 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" };
213
281
  markPackageSynced(root, skillPackage, syncManifest, slug);
214
282
  syncManifestChanged = true;
215
283
  await writeSyncManifest(syncManifest);
@@ -218,7 +286,7 @@ export async function pushWatchOnce(opts) {
218
286
  if (err instanceof Error && /Skill not found/i.test(err.message)) {
219
287
  const result = await publishSkillPath({ file: packagePath, visibility: "unlisted", quiet: true });
220
288
  published += 1;
221
- 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" };
222
290
  markPackageSynced(root, skillPackage, syncManifest, result.data.slug);
223
291
  syncManifestChanged = true;
224
292
  await writeSyncManifest(syncManifest);
@@ -234,7 +302,7 @@ export async function pushWatchOnce(opts) {
234
302
  try {
235
303
  const result = await publishSkillPath({ file: packagePath, visibility: "unlisted", quiet: true });
236
304
  published += 1;
237
- 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" };
238
306
  markPackageSynced(root, skillPackage, syncManifest, result.data.slug);
239
307
  syncManifestChanged = true;
240
308
  await writeSyncManifest(syncManifest);
package/dist/sync.js CHANGED
@@ -152,6 +152,7 @@ function syncPackageFiles(target, body, files) {
152
152
  }
153
153
  async function planPackageSync(root, files, manifest) {
154
154
  let missing = 0;
155
+ let missingTracked = 0;
155
156
  let unchanged = 0;
156
157
  let remoteChanged = 0;
157
158
  let firstMissingTarget = null;
@@ -176,6 +177,10 @@ async function planPackageSync(root, files, manifest) {
176
177
  return { kind: "conflict", target: state.conflictTarget ?? file.target, reason: state.reason };
177
178
  if (state.kind === "missing") {
178
179
  firstMissingTarget ??= file.target;
180
+ if (tracked) {
181
+ missingTracked += 1;
182
+ continue;
183
+ }
179
184
  missing += 1;
180
185
  continue;
181
186
  }
@@ -195,15 +200,20 @@ async function planPackageSync(root, files, manifest) {
195
200
  }
196
201
  if (unchanged === files.length)
197
202
  return { kind: "unchanged" };
203
+ if (missing + missingTracked === files.length)
204
+ return { kind: "write" };
205
+ if (missingTracked > 0) {
206
+ return {
207
+ kind: "conflict",
208
+ target: firstMissingTarget ?? files[0]?.target ?? root,
209
+ reason: "local package is only partially installed",
210
+ };
211
+ }
198
212
  if (missing === 0 && remoteChanged > 0)
199
213
  return { kind: "update" };
200
214
  if (missing === files.length)
201
215
  return { kind: "write" };
202
- return {
203
- kind: "conflict",
204
- target: firstMissingTarget ?? files[0]?.target ?? root,
205
- reason: "local package is only partially installed",
206
- };
216
+ return { kind: "update" };
207
217
  }
208
218
  async function overwriteTrackedFile(root, target, body, expectedHash) {
209
219
  const parent = await openSafeParentDirectory(root, target, false);
@@ -410,9 +420,20 @@ export async function sync(opts = {}) {
410
420
  for (const file of packageFiles) {
411
421
  const targetKey = manifestKey(root, file.target);
412
422
  const tracked = manifest.files[targetKey];
413
- if (!tracked)
414
- throw conflictError("existing file is not tracked by Floom sync", "EEXIST");
415
- await overwriteTrackedFile(root, file.target, file.bytes, tracked.hash);
423
+ if (tracked) {
424
+ await overwriteTrackedFile(root, file.target, file.bytes, tracked.hash);
425
+ continue;
426
+ }
427
+ const state = await localState(file.target);
428
+ if (state.kind === "missing") {
429
+ await writeSyncedFile(root, file.target, file.bytes);
430
+ continue;
431
+ }
432
+ if (state.kind === "conflict")
433
+ throw conflictError(state.reason, "EEXIST");
434
+ if (state.hash === file.hash)
435
+ continue;
436
+ throw conflictError("existing file is not tracked by Floom sync", "EEXIST");
416
437
  }
417
438
  }
418
439
  else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@floomhq/floom",
3
- "version": "1.0.56",
3
+ "version": "1.0.58",
4
4
  "description": "Sync AI skills across agents and machines.",
5
5
  "license": "MIT",
6
6
  "type": "module",