@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 +1 -0
- package/dist/daemon.js +17 -1
- package/dist/push-watch.js +72 -4
- package/dist/sync.js +29 -8
- package/package.json +1 -1
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(
|
|
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
|
}
|
package/dist/push-watch.js
CHANGED
|
@@ -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 (
|
|
414
|
-
|
|
415
|
-
|
|
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 {
|