@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 +1 -0
- package/dist/audit.js +27 -2
- package/dist/daemon.js +17 -1
- package/dist/push-watch.js +96 -4
- package/dist/sync.js +1 -1
- 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/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:
|
|
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
|
|
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(
|
|
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
|
@@ -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
|
}
|