@floomhq/floom 1.0.63 → 2.0.0

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/secrets.js DELETED
@@ -1,119 +0,0 @@
1
- const SECRET_PATTERNS = [
2
- { label: "OpenAI API key", regex: /\bsk-proj-[A-Za-z0-9_-]{20,}\b/g },
3
- { label: "OpenAI API key", regex: /\bsk-[A-Za-z0-9]{32,}\b/g },
4
- { label: "Anthropic API key", regex: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
5
- { label: "Google API key", regex: /\bAIza[0-9A-Za-z_-]{25,}\b/g },
6
- { label: "GitHub token", regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{30,}\b/g },
7
- { label: "GitHub token", regex: /\bgithub_pat_[A-Za-z0-9_]{40,}\b/g },
8
- { label: "Supabase access token", regex: /\bsbp_[A-Za-z0-9]{30,}\b/g },
9
- { label: "Stripe secret key", regex: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/g },
10
- { label: "Slack token", regex: /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g },
11
- { label: "AWS access key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
12
- { label: "Private key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g },
13
- ];
14
- const GENERIC_ASSIGNMENT_RE = /\b(?:api[_-]?key|secret|access[_-]?token|auth[_-]?token|bearer[_-]?token)\b\s*[:=]\s*["']?([A-Za-z0-9_./+=-]{24,})["']?/gi;
15
- const PROVIDER_LIKE_ASSIGNMENT_RE = /\b(?:api[_-]?key|secret|access[_-]?token|auth[_-]?token|bearer[_-]?token)\b\s*[:=]\s*["']?((?:sk|pk|rk)-[A-Za-z0-9_-]{8,}|sbp_[A-Za-z0-9]{12,}|xox[baprs]-[A-Za-z0-9-]{12,})["']?/gi;
16
- const PLACEHOLDER_RE = /(?:^|[_./+=-])(?:your|example|sample|placeholder|replace|changeme|todo|xxx|test|demo|dummy|fake|mock|staging|dev|local|redacted)(?:$|[_./+=-])/i;
17
- const PROMPT_INJECTION_PATTERNS = [
18
- { label: "Prompt injection instruction", regex: /\bignore (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
19
- { label: "Prompt injection instruction", regex: /\bdisregard (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
20
- { label: "Prompt injection instruction", regex: /\bforget (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
21
- { label: "Prompt injection instruction", regex: /\boverride (?:the )?(?:system|developer|safety) instructions\b/gi },
22
- { label: "System prompt extraction", regex: /\b(?:reveal|print|show|dump|expose|leak) (?:the )?(?:system prompt|developer message|hidden instructions)\b/gi },
23
- ];
24
- const DATA_EXFILTRATION_PATTERNS = [
25
- { label: "Data exfiltration instruction", regex: /\b(?:send|post|upload|exfiltrate|copy) (?:[^.\n]{0,80})\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b(?:[^.\n]{0,120})\b(?:to|into) https?:\/\//gi },
26
- { label: "Credential harvesting instruction", regex: /\b(?:collect|harvest|steal|extract) (?:[^.\n]{0,80})\b(?:api keys?|tokens?|secrets?|environment variables|\.env|credentials)\b/gi },
27
- ];
28
- function redact(value) {
29
- if (value.length <= 12)
30
- return "[redacted]";
31
- return `${value.slice(0, 4)}...${value.slice(-4)}`;
32
- }
33
- function lineNumberAt(input, index) {
34
- let line = 1;
35
- for (let i = 0; i < index; i++) {
36
- if (input.charCodeAt(i) === 10)
37
- line++;
38
- }
39
- return line;
40
- }
41
- function pushFinding(findings, seen, label, line, value) {
42
- const key = `${label}:${line}:${value}`;
43
- if (seen.has(key))
44
- return;
45
- seen.add(key);
46
- findings.push({ label, line, preview: redact(value) });
47
- }
48
- function isDocumentedEnvReference(value) {
49
- return /\b(?:process\.env|import\.meta\.env|os\.environ)\b/.test(value);
50
- }
51
- export function detectSecrets(input) {
52
- const findings = [];
53
- const seen = new Set();
54
- for (const pattern of SECRET_PATTERNS) {
55
- pattern.regex.lastIndex = 0;
56
- for (const match of input.matchAll(pattern.regex)) {
57
- const value = match[0] ?? "";
58
- pushFinding(findings, seen, pattern.label, lineNumberAt(input, match.index ?? 0), value);
59
- }
60
- }
61
- GENERIC_ASSIGNMENT_RE.lastIndex = 0;
62
- for (const match of input.matchAll(GENERIC_ASSIGNMENT_RE)) {
63
- const value = match[1] ?? "";
64
- if (!value || PLACEHOLDER_RE.test(value))
65
- continue;
66
- if (isDocumentedEnvReference(value))
67
- continue;
68
- pushFinding(findings, seen, "Possible secret assignment", lineNumberAt(input, match.index ?? 0), value);
69
- }
70
- PROVIDER_LIKE_ASSIGNMENT_RE.lastIndex = 0;
71
- for (const match of input.matchAll(PROVIDER_LIKE_ASSIGNMENT_RE)) {
72
- const value = match[1] ?? "";
73
- if (!value)
74
- continue;
75
- if (isDocumentedEnvReference(value))
76
- continue;
77
- pushFinding(findings, seen, "Provider-like secret assignment", lineNumberAt(input, match.index ?? 0), value);
78
- }
79
- return findings.sort((a, b) => a.line - b.line || a.label.localeCompare(b.label));
80
- }
81
- function detectPatternFindings(input, patterns, category) {
82
- const findings = [];
83
- const seen = new Set();
84
- for (const pattern of patterns) {
85
- pattern.regex.lastIndex = 0;
86
- for (const match of input.matchAll(pattern.regex)) {
87
- const value = (match[0] ?? "").replace(/\s+/g, " ").trim();
88
- const key = `${pattern.label}:${match.index ?? 0}:${value}`;
89
- if (seen.has(key))
90
- continue;
91
- seen.add(key);
92
- findings.push({
93
- label: pattern.label,
94
- line: lineNumberAt(input, match.index ?? 0),
95
- preview: redact(value),
96
- severity: "high",
97
- category,
98
- });
99
- }
100
- }
101
- return findings;
102
- }
103
- export function detectSkillSecurityFindings(input) {
104
- const secretFindings = detectSecrets(input).map((finding) => ({
105
- ...finding,
106
- severity: "high",
107
- category: "secret",
108
- }));
109
- return [
110
- ...secretFindings,
111
- ...detectPatternFindings(input, PROMPT_INJECTION_PATTERNS, "prompt_injection"),
112
- ...detectPatternFindings(input, DATA_EXFILTRATION_PATTERNS, "data_exfiltration"),
113
- ].sort((a, b) => a.line - b.line || a.label.localeCompare(b.label));
114
- }
115
- export function formatSecurityFindings(findings, limit = 5) {
116
- const shown = findings.slice(0, limit).map((finding) => (`line ${finding.line}: ${finding.label} (${finding.preview})`));
117
- const more = findings.length > shown.length ? `\n...and ${findings.length - shown.length} more.` : "";
118
- return `${shown.join("\n")}${more}`;
119
- }
package/dist/setup.js DELETED
@@ -1,301 +0,0 @@
1
- import { access, lstat, mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { dirname, join, resolve } from "node:path";
3
- import { homedir } from "node:os";
4
- import { createInterface } from "node:readline/promises";
5
- import { stdin as input, stdout as output } from "node:process";
6
- import { FloomError } from "./errors.js";
7
- import { c, symbols } from "./ui.js";
8
- import { targetLabel } from "./targets.js";
9
- import { readConfig, resolveApiUrl } from "./config.js";
10
- import { getJson } from "./lib/api.js";
11
- const START_MARKER = "<!-- FLOOM SETUP START -->";
12
- const END_MARKER = "<!-- FLOOM SETUP END -->";
13
- const CLOUD_START_MARKER = "<!-- FLOOM CLOUD INSTRUCTIONS START -->";
14
- const CLOUD_END_MARKER = "<!-- FLOOM CLOUD INSTRUCTIONS END -->";
15
- const CLI_COMMAND = "npx -y @floomhq/floom";
16
- const INSTRUCTION_FILES = {
17
- claude: { filename: "CLAUDE.md" },
18
- codex: { filename: "AGENTS.md" },
19
- };
20
- function cloudInstructionBody(target, profile) {
21
- if (!profile)
22
- return "";
23
- const base = profile.body_md.trim();
24
- const override = profile.target_overrides?.[target]?.trim();
25
- const body = [base, override].filter(Boolean).join("\n\n");
26
- if (!body)
27
- return "";
28
- return [
29
- CLOUD_START_MARKER,
30
- `<!-- Floom profile version ${profile.version}; updated ${profile.updated_at} -->`,
31
- "",
32
- body,
33
- CLOUD_END_MARKER,
34
- ].join("\n");
35
- }
36
- function floomAgentInstructions(target, profile) {
37
- const addCommand = `${CLI_COMMAND} add <slug-or-url> --target ${target}`;
38
- const cloudBody = cloudInstructionBody(target, profile);
39
- return `${START_MARKER}
40
- ## Floom
41
-
42
- - Before recreating agent behavior from scratch, check Floom for reusable skills.
43
- - Search or inspect skills with \`${CLI_COMMAND} search <query>\`, \`${CLI_COMMAND} info <slug-or-url>\`, and \`${CLI_COMMAND} list\`.
44
- - Add shared skills with \`${addCommand}\`; public and unlisted links do not require a Floom account.
45
- - Use the installed \`floom-find-skills\` router skill, Floom search, or MCP lookup before loading local files. Do not enumerate every cached skill into model context.
46
- - \`${CLI_COMMAND} sync --target ${target}\`, \`${CLI_COMMAND} watch --push --target ${target}\`, and \`@floomhq/floom-mcp-sync\` keep saved, published, and subscribed library skills current in the Floom cache; review conflicts before relying on synced output.
47
- ${cloudBody ? `\n${cloudBody}` : ""}
48
- ${END_MARKER}`;
49
- }
50
- async function fileExists(path) {
51
- try {
52
- await access(path);
53
- return true;
54
- }
55
- catch {
56
- return false;
57
- }
58
- }
59
- async function readIfExists(path) {
60
- if (!(await fileExists(path)))
61
- return null;
62
- const stat = await lstat(path);
63
- if (stat.isSymbolicLink()) {
64
- throw new FloomError("Refusing to update an instruction file that is a symbolic link.", `Inspect ${path}, then pass a regular file path.`);
65
- }
66
- if (!stat.isFile()) {
67
- throw new FloomError("Instruction target is not a file.", path);
68
- }
69
- return readFile(path, "utf8");
70
- }
71
- function parseTargetFromFile(file) {
72
- const upper = file.toUpperCase();
73
- if (upper.endsWith("CLAUDE.MD"))
74
- return "claude";
75
- if (upper.endsWith("AGENTS.MD"))
76
- return "codex";
77
- if (upper.endsWith(".MDC") || upper.includes(`${join(".cursor", "rules").toUpperCase()}`))
78
- return "cursor";
79
- if (upper.includes(`${join(".config", "opencode").toUpperCase()}`))
80
- return "opencode";
81
- if (upper.includes(`${join(".kimi", "agents").toUpperCase()}`))
82
- return "kimi";
83
- return undefined;
84
- }
85
- async function findUp(filename) {
86
- let dir = process.cwd();
87
- const home = resolve(homedir());
88
- while (true) {
89
- const candidate = join(dir, filename);
90
- if (await fileExists(candidate))
91
- return candidate;
92
- const parent = dirname(dir);
93
- if (dir === parent || dir === home)
94
- return null;
95
- dir = parent;
96
- }
97
- }
98
- async function detectTarget(opts) {
99
- const agent = opts.target ?? (opts.file ? parseTargetFromFile(opts.file) : undefined);
100
- if (opts.file) {
101
- if (!agent) {
102
- throw new FloomError("Cannot infer agent target from that file name.", "Pass `--target claude`, `--target codex`, `--target cursor`, `--target opencode`, or `--target kimi`.");
103
- }
104
- return {
105
- agent,
106
- label: targetLabel(agent),
107
- path: resolve(process.cwd(), opts.file),
108
- };
109
- }
110
- if (agent) {
111
- if (opts.global && agent === "claude") {
112
- return {
113
- agent,
114
- label: targetLabel(agent),
115
- path: join(homedir(), ".claude", "CLAUDE.md"),
116
- };
117
- }
118
- if (opts.global && agent === "codex") {
119
- return {
120
- agent,
121
- label: targetLabel(agent),
122
- path: join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "AGENTS.md"),
123
- };
124
- }
125
- if (agent === "cursor") {
126
- return {
127
- agent,
128
- label: targetLabel(agent),
129
- path: join(homedir(), ".cursor", "rules", "floom.mdc"),
130
- };
131
- }
132
- if (agent === "opencode") {
133
- return {
134
- agent,
135
- label: targetLabel(agent),
136
- path: join(homedir(), ".config", "opencode", "floom.md"),
137
- };
138
- }
139
- if (agent === "kimi") {
140
- return {
141
- agent,
142
- label: targetLabel(agent),
143
- path: join(homedir(), ".kimi", "agents", "floom-system.md"),
144
- };
145
- }
146
- const existing = await findUp(INSTRUCTION_FILES[agent].filename);
147
- return {
148
- agent,
149
- label: targetLabel(agent),
150
- path: existing ?? resolve(process.cwd(), INSTRUCTION_FILES[agent].filename),
151
- };
152
- }
153
- const claude = await findUp(INSTRUCTION_FILES.claude.filename);
154
- const codex = await findUp(INSTRUCTION_FILES.codex.filename);
155
- if (claude && codex) {
156
- throw new FloomError("Found both Claude Code and Codex instruction files.", "Pass an explicit `--target`.");
157
- }
158
- if (claude)
159
- return { agent: "claude", label: targetLabel("claude"), path: claude };
160
- if (codex)
161
- return { agent: "codex", label: targetLabel("codex"), path: codex };
162
- throw new FloomError("No agent instruction file found.", `Run \`${CLI_COMMAND} setup --target claude --yes\`, \`${CLI_COMMAND} setup --target codex --yes\`, \`${CLI_COMMAND} setup --target cursor --yes\`, \`${CLI_COMMAND} setup --target opencode --yes\`, or \`${CLI_COMMAND} setup --target kimi --yes\`.`);
163
- }
164
- function renderPreview(target, existing, profile) {
165
- const action = existing === null ? "create" : "append";
166
- return [
167
- "",
168
- `${symbols.bullet} Floom setup preview for ${c.bold(target.label)}`,
169
- ` ${c.dim("Target:")} ${target.path}`,
170
- ` ${c.dim("Action:")} ${action}`,
171
- "",
172
- floomAgentInstructions(target.agent, profile),
173
- "",
174
- `${c.dim("MCP setup guidance:")} run ${c.cyan(`${CLI_COMMAND} mcp`)} to print local agent commands.`,
175
- "",
176
- ].join("\n");
177
- }
178
- function renderInstructions(target, existing, profile) {
179
- const body = floomAgentInstructions(target, profile);
180
- if (target === "cursor" && existing === null) {
181
- return [
182
- "---",
183
- "description: Use Floom skills and sync",
184
- "alwaysApply: true",
185
- "---",
186
- "",
187
- body,
188
- "",
189
- ].join("\n");
190
- }
191
- return body;
192
- }
193
- function replaceExistingBlock(existing, instructions) {
194
- const start = existing.indexOf(START_MARKER);
195
- const end = existing.indexOf(END_MARKER, start);
196
- if (start < 0 || end < 0)
197
- return null;
198
- const afterEnd = end + END_MARKER.length;
199
- const prefix = existing.slice(0, start).replace(/\s*$/, "");
200
- const suffix = existing.slice(afterEnd).replace(/^\s*/, "").replace(/\s*$/, "");
201
- return [...(prefix ? [prefix] : []), instructions, ...(suffix ? [suffix] : [])].join("\n\n").replace(/\s*$/, "\n");
202
- }
203
- async function loadCloudProfile() {
204
- const cfg = await readConfig();
205
- if (!cfg)
206
- return null;
207
- try {
208
- const apiUrl = resolveApiUrl(cfg);
209
- const payload = await getJson(`${apiUrl}/api/v1/me/instructions`, "load cloud instruction profile", cfg.accessToken);
210
- return payload.profile;
211
- }
212
- catch {
213
- return null;
214
- }
215
- }
216
- async function ensureOpencodeInstructionReference(instructionPath) {
217
- const configPath = join(homedir(), ".config", "opencode", "opencode.json");
218
- const reference = `{file:${instructionPath.replace(homedir(), "~")}}`;
219
- let parsed = {};
220
- const existing = await readIfExists(configPath);
221
- if (existing) {
222
- try {
223
- parsed = JSON.parse(existing);
224
- }
225
- catch {
226
- throw new FloomError("OpenCode config is not valid JSON.", configPath);
227
- }
228
- }
229
- const current = Array.isArray(parsed.instructions) ? parsed.instructions : [];
230
- if (current.includes(reference))
231
- return;
232
- parsed.instructions = [...current.filter((item) => typeof item === "string"), reference];
233
- await mkdir(dirname(configPath), { recursive: true });
234
- await writeFile(configPath, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
235
- }
236
- async function confirmWrite(target, existing) {
237
- if (!process.stdin.isTTY) {
238
- throw new FloomError("Refusing to update agent instructions without confirmation in non-interactive mode.", "Pass `--yes` to write, or `--dry-run` to preview.");
239
- }
240
- const action = existing === null ? "Create" : "Append to";
241
- const rl = createInterface({ input, output });
242
- try {
243
- const answer = (await rl.question(` ${action} ${target.path} for ${target.label}? ${c.dim("(y/N)")} `)).trim().toLowerCase();
244
- return answer === "y" || answer === "yes";
245
- }
246
- finally {
247
- rl.close();
248
- }
249
- }
250
- export async function setupAgent(opts) {
251
- const target = await detectTarget(opts);
252
- const existing = await readIfExists(target.path);
253
- const profile = opts.global ? await loadCloudProfile() : null;
254
- const instructions = renderInstructions(target.agent, existing, profile);
255
- if (existing?.includes(START_MARKER) && existing.includes(END_MARKER)) {
256
- const replaced = replaceExistingBlock(existing, instructions);
257
- if (replaced === null || replaced === existing) {
258
- process.stdout.write(`\n${symbols.ok} Floom instructions already present in ${c.bold(target.path)}\n\n`);
259
- return;
260
- }
261
- if (opts.dryRun) {
262
- process.stdout.write(renderPreview(target, existing, profile));
263
- return;
264
- }
265
- await writeFile(target.path, replaced, "utf8");
266
- process.stdout.write(`\n${symbols.ok} Updated Floom instructions in ${c.bold(target.path)}\n\n`);
267
- return;
268
- }
269
- if (opts.dryRun) {
270
- process.stdout.write(renderPreview(target, existing, profile));
271
- return;
272
- }
273
- if (!opts.yes) {
274
- process.stdout.write(renderPreview(target, existing, profile));
275
- const ok = await confirmWrite(target, existing);
276
- if (!ok) {
277
- process.stdout.write(`\n${c.dim("Cancelled. Nothing was written.")}\n\n`);
278
- return;
279
- }
280
- }
281
- await mkdir(dirname(target.path), { recursive: true });
282
- const next = existing === null
283
- ? `${instructions}\n`
284
- : `${existing.replace(/\s*$/, "")}\n\n${instructions}\n`;
285
- if (existing === null) {
286
- await writeFile(target.path, next, { encoding: "utf8", flag: "wx" }).catch((err) => {
287
- if (err instanceof Error && "code" in err && err.code === "EEXIST") {
288
- throw new FloomError("Instruction file appeared while setup was running.", `Re-run \`${CLI_COMMAND} setup\` so Floom can inspect the current file before writing.`);
289
- }
290
- throw err;
291
- });
292
- }
293
- else {
294
- await writeFile(target.path, next, "utf8");
295
- }
296
- if (target.agent === "opencode") {
297
- await ensureOpencodeInstructionReference(target.path);
298
- }
299
- process.stdout.write(`\n${symbols.ok} Added Floom instructions to ${c.bold(target.path)}\n`);
300
- process.stdout.write(` ${c.dim("MCP setup guidance:")} ${c.cyan(`${CLI_COMMAND} mcp`)}\n\n`);
301
- }
package/dist/share.js DELETED
@@ -1,70 +0,0 @@
1
- import ora from "ora";
2
- import { readConfig, resolveApiUrl } from "./config.js";
3
- import { friendlyHttp, friendlyNetwork, FloomError } from "./errors.js";
4
- import { c, symbols } from "./ui.js";
5
- function slugFromInput(input) {
6
- const trimmed = input.trim();
7
- try {
8
- const url = new URL(trimmed);
9
- const last = url.pathname.split("/").filter(Boolean).at(-1) ?? "";
10
- return last.replace(/\.(md|json)$/i, "");
11
- }
12
- catch {
13
- return trimmed.replace(/\.(md|json)$/i, "");
14
- }
15
- }
16
- async function readJson(res) {
17
- return (await res.json());
18
- }
19
- export async function share(opts) {
20
- const cfg = await readConfig();
21
- if (!cfg) {
22
- throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
23
- }
24
- const slug = slugFromInput(opts.slug);
25
- if (!slug) {
26
- throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom share <slug> --list`");
27
- }
28
- const apiUrl = resolveApiUrl(cfg);
29
- const endpoint = `${apiUrl}/api/skills/${encodeURIComponent(slug)}/share`;
30
- const spinner = ora({ text: c.dim("Updating sharing..."), color: "yellow" }).start();
31
- let res;
32
- try {
33
- if (opts.kind === "list") {
34
- spinner.text = c.dim("Loading sharing...");
35
- res = await fetch(endpoint, {
36
- headers: { authorization: `Bearer ${cfg.accessToken}` },
37
- });
38
- }
39
- else {
40
- res = await fetch(endpoint, {
41
- method: "PATCH",
42
- headers: {
43
- authorization: `Bearer ${cfg.accessToken}`,
44
- "content-type": "application/json",
45
- },
46
- body: JSON.stringify({ add: opts.add, remove: opts.remove }),
47
- });
48
- }
49
- }
50
- catch (err) {
51
- spinner.stop();
52
- throw friendlyNetwork(err);
53
- }
54
- if (!res.ok) {
55
- spinner.stop();
56
- throw friendlyHttp(res.status, "update sharing");
57
- }
58
- const data = await readJson(res);
59
- spinner.stop();
60
- const emails = data.shared_with_emails;
61
- process.stdout.write(`\n${symbols.ok} Sharing for ${c.bold(data.slug)}\n\n`);
62
- if (emails.length === 0) {
63
- process.stdout.write(` ${c.dim("No emails added.")}\n\n`);
64
- return;
65
- }
66
- for (const email of emails) {
67
- process.stdout.write(` ${email}\n`);
68
- }
69
- process.stdout.write("\n");
70
- }
package/dist/status.js DELETED
@@ -1,181 +0,0 @@
1
- import { constants } from "node:fs";
2
- import { open, readdir } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { CONFIG_DIR, readConfig, resolveApiUrl } from "./config.js";
5
- import { floomFetch } from "./lib/api.js";
6
- import { AGENT_TARGETS, targetSkillsDir } from "./targets.js";
7
- import { c, symbols } from "./ui.js";
8
- const STATUS_CLOUD_TIMEOUT_MS = 8_000;
9
- async function readJsonFile(path) {
10
- try {
11
- const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
12
- try {
13
- return JSON.parse(await handle.readFile("utf8"));
14
- }
15
- finally {
16
- await handle.close();
17
- }
18
- }
19
- catch (err) {
20
- const code = err.code;
21
- if (code === "ENOENT" || code === "ELOOP" || err instanceof SyntaxError)
22
- return null;
23
- throw err;
24
- }
25
- }
26
- async function countDirectSkillPackages(root) {
27
- let count = 0;
28
- let entries;
29
- try {
30
- entries = await readdir(root, { withFileTypes: true });
31
- }
32
- catch (err) {
33
- if (err.code === "ENOENT")
34
- return 0;
35
- throw err;
36
- }
37
- if (entries.some((entry) => entry.isFile() && entry.name === "SKILL.md"))
38
- count += 1;
39
- for (const entry of entries) {
40
- if (!entry.isDirectory() || entry.name.startsWith("."))
41
- continue;
42
- try {
43
- const child = await readdir(join(root, entry.name), { withFileTypes: true });
44
- if (child.some((item) => item.isFile() && item.name === "SKILL.md"))
45
- count += 1;
46
- }
47
- catch (err) {
48
- if (err.code !== "ENOENT")
49
- throw err;
50
- }
51
- }
52
- return count;
53
- }
54
- function manifestCounts(manifest) {
55
- const files = manifest?.files && typeof manifest.files === "object" ? Object.values(manifest.files) : [];
56
- return {
57
- files: files.length,
58
- skills: new Set(files.map((entry) => entry.slug).filter((slug) => typeof slug === "string" && slug.length > 0)).size,
59
- };
60
- }
61
- async function countCloudVisible() {
62
- const cfg = await readConfig();
63
- if (!cfg)
64
- return { signedIn: false, total: null };
65
- const apiUrl = resolveApiUrl(cfg);
66
- try {
67
- const url = new URL(`${apiUrl}/api/v1/me/skills`);
68
- url.searchParams.set("limit", "100");
69
- const res = await floomFetch(url.toString(), "load your skills", {
70
- token: cfg.accessToken,
71
- timeoutMs: STATUS_CLOUD_TIMEOUT_MS,
72
- rateLimitRetries: 0,
73
- });
74
- const payload = (await res.json());
75
- const sampleCount = Array.isArray(payload.skills) ? payload.skills.length : 0;
76
- const hasMore = Boolean(payload.next_cursor);
77
- return {
78
- signedIn: true,
79
- total: hasMore ? null : sampleCount,
80
- sampleCount,
81
- hasMore,
82
- };
83
- }
84
- catch (err) {
85
- return {
86
- signedIn: true,
87
- total: null,
88
- error: err instanceof Error ? err.message : String(err),
89
- };
90
- }
91
- }
92
- async function buildStatus() {
93
- const cloud = await countCloudVisible();
94
- const daemonRaw = await readJsonFile(join(CONFIG_DIR, "daemon-status.json"));
95
- const daemonRunning = daemonIsLive(daemonRaw);
96
- const targets = {};
97
- for (const target of AGENT_TARGETS) {
98
- const cacheRoot = join(CONFIG_DIR, "skill-cache", target);
99
- const nativeRoot = targetSkillsDir(target);
100
- const cacheManifest = manifestCounts(await readJsonFile(join(cacheRoot, ".floom-cli-sync-manifest.json")));
101
- const nativeManifest = manifestCounts(await readJsonFile(join(CONFIG_DIR, "native-sync-manifests", `${target}.json`)));
102
- targets[target] = {
103
- cache_packages: cacheManifest.skills || await countDirectSkillPackages(cacheRoot),
104
- cache_manifest_files: cacheManifest.files,
105
- cache_manifest_skills: cacheManifest.skills,
106
- native_root: nativeRoot,
107
- native_packages: nativeManifest.skills || await countDirectSkillPackages(nativeRoot),
108
- native_manifest_files: nativeManifest.files,
109
- native_manifest_skills: nativeManifest.skills,
110
- ...(daemonRaw?.last_run?.[target] ? { daemon: daemonRaw.last_run[target] } : {}),
111
- };
112
- }
113
- return {
114
- cloud: {
115
- signed_in: cloud.signedIn,
116
- visible_total: cloud.total,
117
- ...(cloud.sampleCount !== undefined ? { visible_sample_count: cloud.sampleCount } : {}),
118
- ...(cloud.hasMore !== undefined ? { visible_has_more: cloud.hasMore } : {}),
119
- ...(cloud.error ? { error: cloud.error } : {}),
120
- },
121
- daemon: {
122
- running: daemonRunning,
123
- ...(daemonRaw?.version ? { version: daemonRaw.version } : {}),
124
- ...(daemonRaw?.hostname ? { hostname: daemonRaw.hostname } : {}),
125
- ...(daemonRaw?.targets ? { targets: daemonRaw.targets } : {}),
126
- ...(daemonRaw?.last_completed_at ? { last_completed_at: daemonRaw.last_completed_at } : {}),
127
- ...(daemonRaw?.next_run_at ? { next_run_at: daemonRaw.next_run_at } : {}),
128
- },
129
- targets,
130
- notes: [
131
- "Floom cache is the synced local package store used by MCP/search/router flows.",
132
- "Native harness roots stay small where the harness would otherwise preload thousands of skills.",
133
- "Instruction files tell agents to use Floom search/MCP instead of enumerating every cached package.",
134
- ],
135
- };
136
- }
137
- function daemonIsLive(status) {
138
- if (!status?.running)
139
- return false;
140
- if (!status.pid || status.pid < 1)
141
- return false;
142
- try {
143
- process.kill(status.pid, 0);
144
- }
145
- catch {
146
- return false;
147
- }
148
- if (!status.next_run_at || !status.interval_seconds)
149
- return true;
150
- const nextRun = Date.parse(status.next_run_at);
151
- if (!Number.isFinite(nextRun))
152
- return false;
153
- const graceMs = Math.max(status.interval_seconds * 2000, 120_000);
154
- return nextRun + graceMs >= Date.now();
155
- }
156
- export async function status(opts) {
157
- const payload = await buildStatus();
158
- if (opts.json) {
159
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
160
- return;
161
- }
162
- process.stdout.write(`\n${symbols.dot} ${c.bold("Floom status")}\n\n`);
163
- process.stdout.write(` ${c.dim("cloud signed in:")} ${payload.cloud.signed_in ? "yes" : "no"}\n`);
164
- const cloudVisible = payload.cloud.visible_total ?? (payload.cloud.visible_has_more ? `${payload.cloud.visible_sample_count ?? "?"}+` : "unknown");
165
- process.stdout.write(` ${c.dim("cloud visible skills:")} ${cloudVisible}\n`);
166
- if (payload.cloud.error)
167
- process.stdout.write(` ${c.dim("cloud status:")} ${payload.cloud.error}\n`);
168
- process.stdout.write(` ${c.dim("daemon:")} ${payload.daemon.running ? "running" : "not running"}${payload.daemon.version ? ` (${payload.daemon.version})` : ""}\n`);
169
- if (payload.daemon.last_completed_at)
170
- process.stdout.write(` ${c.dim("last completed:")} ${payload.daemon.last_completed_at}\n`);
171
- process.stdout.write("\n");
172
- for (const target of AGENT_TARGETS) {
173
- const row = payload.targets[target];
174
- const daemon = row.daemon ? ` daemon=${row.daemon.ok ? "ok" : "error"} synced=${row.daemon.synced ?? "?"} conflicts=${row.daemon.conflicts ?? "?"}` : "";
175
- process.stdout.write(` ${c.cyan(target.padEnd(8))} cache=${row.cache_packages} native=${row.native_packages} nativeTracked=${row.native_manifest_skills}${daemon}\n`);
176
- }
177
- process.stdout.write("\n");
178
- for (const note of payload.notes)
179
- process.stdout.write(` ${c.dim(`- ${note}`)}\n`);
180
- process.stdout.write("\n");
181
- }