@floomhq/floom 1.0.7 → 1.0.9
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 +28 -24
- package/dist/cli.js +158 -63
- package/dist/delete.js +2 -2
- package/dist/doctor.js +21 -8
- package/dist/errors.js +2 -2
- package/dist/info.js +2 -2
- package/dist/init.js +154 -6
- package/dist/install.js +20 -8
- package/dist/library.js +7 -7
- package/dist/list.js +2 -2
- package/dist/login.js +1 -1
- package/dist/mcp.js +2 -2
- package/dist/publish.js +11 -9
- package/dist/secrets.js +23 -3
- package/dist/setup.js +8 -8
- package/dist/share.js +2 -2
- package/dist/sync.js +2 -2
- package/dist/whoami.js +1 -1
- package/package.json +1 -1
package/dist/delete.js
CHANGED
|
@@ -31,10 +31,10 @@ async function confirm(question) {
|
|
|
31
31
|
export async function deleteSkill(opts) {
|
|
32
32
|
const slug = slugFromInput(opts.slug);
|
|
33
33
|
if (!slug)
|
|
34
|
-
throw new FloomError("Missing skill slug.", "Try: `floom delete <slug>`");
|
|
34
|
+
throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom delete <slug>`");
|
|
35
35
|
const cfg = await readConfig();
|
|
36
36
|
if (!cfg)
|
|
37
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
37
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
38
38
|
if (!opts.yes) {
|
|
39
39
|
process.stdout.write(`\n${symbols.bullet} About to delete ${c.bold(slug)}.\n`);
|
|
40
40
|
const ok = await confirm(`Delete ${c.bold(slug)}? Cannot be undone in CLI.`);
|
package/dist/doctor.js
CHANGED
|
@@ -32,7 +32,7 @@ async function checkAuth() {
|
|
|
32
32
|
name: "Auth",
|
|
33
33
|
status: "fail",
|
|
34
34
|
detail: "Token rejected (401).",
|
|
35
|
-
hint: "Run `floom logout && floom login` to refresh.",
|
|
35
|
+
hint: "Run `npx -y @floomhq/floom logout && npx -y @floomhq/floom login` to refresh.",
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
if (!res.ok) {
|
|
@@ -88,7 +88,7 @@ async function checkMcp() {
|
|
|
88
88
|
return {
|
|
89
89
|
name: "MCP",
|
|
90
90
|
status: "ok",
|
|
91
|
-
detail: "Optional MCP not registered. `floom add` still writes local skill files.",
|
|
91
|
+
detail: "Optional MCP not registered. `npx -y @floomhq/floom add` still writes local skill files.",
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
return {
|
|
@@ -130,7 +130,7 @@ async function checkTargetDir() {
|
|
|
130
130
|
name: "Target dir",
|
|
131
131
|
status: "warn",
|
|
132
132
|
detail: `${dir} does not exist yet.`,
|
|
133
|
-
hint: "It will be created on first `floom add`.",
|
|
133
|
+
hint: "It will be created on first `npx -y @floomhq/floom add`.",
|
|
134
134
|
};
|
|
135
135
|
}
|
|
136
136
|
throw err;
|
|
@@ -146,7 +146,7 @@ async function checkLastSync() {
|
|
|
146
146
|
name: "Last sync",
|
|
147
147
|
status: "warn",
|
|
148
148
|
detail: "No synced skills found yet.",
|
|
149
|
-
hint: "Run `floom add <link>` to install your first skill.",
|
|
149
|
+
hint: "Run `npx -y @floomhq/floom add <link>` to install your first skill.",
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
152
|
// Find most recently modified entry
|
|
@@ -239,8 +239,7 @@ async function checkVersion() {
|
|
|
239
239
|
};
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
|
-
export async function doctor() {
|
|
243
|
-
process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)})`)}\n\n`);
|
|
242
|
+
export async function doctor(opts = {}) {
|
|
244
243
|
const checks = await Promise.all([
|
|
245
244
|
checkAuth(),
|
|
246
245
|
checkMcp(),
|
|
@@ -248,6 +247,22 @@ export async function doctor() {
|
|
|
248
247
|
checkLastSync(),
|
|
249
248
|
checkVersion(),
|
|
250
249
|
]);
|
|
250
|
+
const anyFail = checks.some((c) => c.status === "fail");
|
|
251
|
+
const anyWarn = checks.some((c) => c.status === "warn");
|
|
252
|
+
const status = anyFail ? "fail" : anyWarn ? "warn" : "ok";
|
|
253
|
+
if (opts.json) {
|
|
254
|
+
process.stdout.write(`${JSON.stringify({
|
|
255
|
+
ok: !anyFail,
|
|
256
|
+
status,
|
|
257
|
+
version: CLI_VERSION,
|
|
258
|
+
config_path: CONFIG_PATH,
|
|
259
|
+
checks,
|
|
260
|
+
}, null, 2)}\n`);
|
|
261
|
+
if (anyFail)
|
|
262
|
+
process.exit(1);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)})`)}\n\n`);
|
|
251
266
|
// Compute column widths for clean table output.
|
|
252
267
|
const nameW = Math.max(...checks.map((c) => c.name.length), 6);
|
|
253
268
|
for (const check of checks) {
|
|
@@ -257,8 +272,6 @@ export async function doctor() {
|
|
|
257
272
|
process.stdout.write(` ${c.dim("→ " + check.hint)}\n`);
|
|
258
273
|
}
|
|
259
274
|
}
|
|
260
|
-
const anyFail = checks.some((c) => c.status === "fail");
|
|
261
|
-
const anyWarn = checks.some((c) => c.status === "warn");
|
|
262
275
|
process.stdout.write("\n");
|
|
263
276
|
if (anyFail) {
|
|
264
277
|
process.stdout.write(` ${c.red("✗ Some checks failed.")} See hints above.\n\n`);
|
package/dist/errors.js
CHANGED
|
@@ -14,7 +14,7 @@ export class FloomError extends Error {
|
|
|
14
14
|
}
|
|
15
15
|
export function friendlyHttp(status, action) {
|
|
16
16
|
if (status === 401) {
|
|
17
|
-
return new FloomError("Your token expired.", "Run `floom login` to refresh.");
|
|
17
|
+
return new FloomError("Your token expired.", "Run `npx -y @floomhq/floom login` to refresh.");
|
|
18
18
|
}
|
|
19
19
|
if (status === 403) {
|
|
20
20
|
return new FloomError(`You don't have permission to ${action}.`);
|
|
@@ -23,7 +23,7 @@ export function friendlyHttp(status, action) {
|
|
|
23
23
|
if (/fetch|inspect|add|install|show|get|search|list|info/i.test(action)) {
|
|
24
24
|
return new FloomError("Skill not found.", "Check the link or slug, then try again.");
|
|
25
25
|
}
|
|
26
|
-
return new FloomError("Skill not found.", "
|
|
26
|
+
return new FloomError("Skill not found.", "Publish as a new skill for now. Publisher-side version updates are planned for a later release.");
|
|
27
27
|
}
|
|
28
28
|
if (status === 413) {
|
|
29
29
|
return new FloomError("That file is too large to publish.");
|
package/dist/info.js
CHANGED
|
@@ -19,7 +19,7 @@ function slugFromInput(input) {
|
|
|
19
19
|
export async function info(opts) {
|
|
20
20
|
const slug = slugFromInput(opts.slug);
|
|
21
21
|
if (!slug)
|
|
22
|
-
throw new FloomError("Missing skill slug.", "Try: `floom info <slug>`");
|
|
22
|
+
throw new FloomError("Missing skill slug.", "Try: `npx -y @floomhq/floom info <slug>`");
|
|
23
23
|
if (!SLUG_RE.test(slug)) {
|
|
24
24
|
throw new FloomError(`Invalid skill slug: ${opts.slug}`, "Use a Floom skill slug or URL.");
|
|
25
25
|
}
|
|
@@ -28,7 +28,7 @@ export async function info(opts) {
|
|
|
28
28
|
const spinner = opts.json ? null : ora({ text: c.dim(`Loading ${slug}...`), color: "yellow" }).start();
|
|
29
29
|
let detail;
|
|
30
30
|
try {
|
|
31
|
-
detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "
|
|
31
|
+
detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "info skill metadata", cfg?.accessToken);
|
|
32
32
|
}
|
|
33
33
|
finally {
|
|
34
34
|
spinner?.stop();
|
package/dist/init.js
CHANGED
|
@@ -4,7 +4,8 @@ import { createInterface } from "node:readline/promises";
|
|
|
4
4
|
import { stdin as input, stdout as output } from "node:process";
|
|
5
5
|
import { c, symbols } from "./ui.js";
|
|
6
6
|
import { FloomError } from "./errors.js";
|
|
7
|
-
const
|
|
7
|
+
const TEMPLATES = {
|
|
8
|
+
generic: `---
|
|
8
9
|
title:
|
|
9
10
|
description:
|
|
10
11
|
version: 1.0
|
|
@@ -26,10 +27,156 @@ version: 1.0
|
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
# Examples
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
`,
|
|
31
|
+
"brand-voice": `---
|
|
32
|
+
title: Brand voice
|
|
33
|
+
description: Help an agent write in our company voice.
|
|
34
|
+
type: knowledge
|
|
35
|
+
installs_as: memory
|
|
36
|
+
version: 1.0
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
# Brand Voice
|
|
40
|
+
|
|
41
|
+
## Use when
|
|
42
|
+
- Writing customer-facing copy
|
|
43
|
+
- Rewriting drafts to match our tone
|
|
44
|
+
- Reviewing messaging before it ships
|
|
45
|
+
|
|
46
|
+
## Voice rules
|
|
47
|
+
- Sound clear, direct, and useful.
|
|
48
|
+
- Prefer concrete nouns and short sentences.
|
|
49
|
+
- Avoid hype, filler, and generic AI language.
|
|
50
|
+
|
|
51
|
+
## Words we use
|
|
52
|
+
- Replace this list with approved terms.
|
|
53
|
+
|
|
54
|
+
## Words we avoid
|
|
55
|
+
- Replace this list with banned or overused terms.
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
- Before:
|
|
59
|
+
- After:
|
|
60
|
+
`,
|
|
61
|
+
"pr-review": `---
|
|
62
|
+
title: PR review
|
|
63
|
+
description: Review code changes with risk-first feedback.
|
|
64
|
+
type: workflow
|
|
65
|
+
installs_as: claude_skill
|
|
66
|
+
version: 1.0
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
# PR Review
|
|
70
|
+
|
|
71
|
+
## Use when
|
|
72
|
+
- Reviewing a pull request or diff
|
|
73
|
+
- Checking a change before merge
|
|
74
|
+
|
|
75
|
+
## Review order
|
|
76
|
+
1. Correctness and regressions
|
|
77
|
+
2. Security and data safety
|
|
78
|
+
3. Tests and missing edge cases
|
|
79
|
+
4. Maintainability
|
|
80
|
+
|
|
81
|
+
## Output
|
|
82
|
+
- Lead with findings.
|
|
83
|
+
- Include file paths and line references.
|
|
84
|
+
- Keep style comments out unless they affect behavior.
|
|
85
|
+
- If no issues are found, say that clearly and name any test gaps.
|
|
86
|
+
`,
|
|
87
|
+
sales: `---
|
|
88
|
+
title: Sales research
|
|
89
|
+
description: Prepare concise account research and outreach context.
|
|
90
|
+
type: workflow
|
|
91
|
+
installs_as: memory
|
|
92
|
+
version: 1.0
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
# Sales Research
|
|
96
|
+
|
|
97
|
+
## Use when
|
|
98
|
+
- Preparing for a prospect call
|
|
99
|
+
- Writing a relevant outbound message
|
|
100
|
+
- Summarizing account context for the team
|
|
101
|
+
|
|
102
|
+
## Gather
|
|
103
|
+
- Company
|
|
104
|
+
- Buyer persona
|
|
105
|
+
- Recent trigger
|
|
106
|
+
- Likely pain
|
|
107
|
+
- Existing tools or workflow
|
|
108
|
+
|
|
109
|
+
## Output
|
|
110
|
+
- 5 bullet account summary
|
|
111
|
+
- 3 likely pain points
|
|
112
|
+
- 2 tailored opener angles
|
|
113
|
+
- 1 clear next action
|
|
114
|
+
`,
|
|
115
|
+
support: `---
|
|
116
|
+
title: Support tone
|
|
117
|
+
description: Answer support tickets with a clear and calm company voice.
|
|
118
|
+
type: instruction
|
|
119
|
+
installs_as: memory
|
|
120
|
+
version: 1.0
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
# Support Tone
|
|
124
|
+
|
|
125
|
+
## Use when
|
|
126
|
+
- Replying to customer support messages
|
|
127
|
+
- Summarizing customer issues
|
|
128
|
+
- Drafting escalation notes
|
|
129
|
+
|
|
130
|
+
## Rules
|
|
131
|
+
- Acknowledge the issue in plain language.
|
|
132
|
+
- Give the next concrete step.
|
|
133
|
+
- Do not over-apologize.
|
|
134
|
+
- Do not invent product behavior.
|
|
135
|
+
- Escalate when data, billing, or account access is involved.
|
|
136
|
+
|
|
137
|
+
## Reply shape
|
|
138
|
+
1. Short acknowledgement
|
|
139
|
+
2. Direct answer or next step
|
|
140
|
+
3. What happens next
|
|
141
|
+
`,
|
|
142
|
+
onboarding: `---
|
|
143
|
+
title: Team onboarding
|
|
144
|
+
description: Help a new teammate understand how this team works.
|
|
145
|
+
type: knowledge
|
|
146
|
+
installs_as: memory
|
|
147
|
+
version: 1.0
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
# Team Onboarding
|
|
151
|
+
|
|
152
|
+
## Use when
|
|
153
|
+
- A new teammate asks how work gets done
|
|
154
|
+
- An agent needs company or team context
|
|
155
|
+
- Creating first-week task plans
|
|
156
|
+
|
|
157
|
+
## Team context
|
|
158
|
+
- Mission:
|
|
159
|
+
- Customers:
|
|
160
|
+
- Current priorities:
|
|
161
|
+
- Tools:
|
|
162
|
+
|
|
163
|
+
## How we work
|
|
164
|
+
- Decision rules:
|
|
165
|
+
- Review process:
|
|
166
|
+
- Communication norms:
|
|
167
|
+
- Definition of done:
|
|
168
|
+
|
|
169
|
+
## First-week checklist
|
|
170
|
+
- Read:
|
|
171
|
+
- Set up:
|
|
172
|
+
- Ask:
|
|
173
|
+
- Ship:
|
|
174
|
+
`,
|
|
175
|
+
};
|
|
176
|
+
export async function init(filename, template = "generic") {
|
|
31
177
|
const target = filename ?? "skill.md";
|
|
32
178
|
const filePath = resolve(process.cwd(), target);
|
|
179
|
+
const body = TEMPLATES[template];
|
|
33
180
|
const exists = await fileExists(filePath);
|
|
34
181
|
if (exists) {
|
|
35
182
|
if (!process.stdin.isTTY) {
|
|
@@ -44,7 +191,7 @@ export async function init(filename) {
|
|
|
44
191
|
}
|
|
45
192
|
}
|
|
46
193
|
try {
|
|
47
|
-
await writeFile(filePath,
|
|
194
|
+
await writeFile(filePath, body, "utf8");
|
|
48
195
|
}
|
|
49
196
|
catch (err) {
|
|
50
197
|
const code = err.code;
|
|
@@ -57,10 +204,11 @@ export async function init(filename) {
|
|
|
57
204
|
throw new FloomError(`Couldn't create ${target}: ${err.message}`);
|
|
58
205
|
}
|
|
59
206
|
process.stdout.write(`\n${symbols.ok} Created ${c.bold(basename(filePath))}\n`);
|
|
207
|
+
process.stdout.write(` ${c.dim(`Template: ${template}`)}\n`);
|
|
60
208
|
process.stdout.write(`\n ${c.bold("Next")}\n`);
|
|
61
209
|
process.stdout.write(` ${c.dim("1.")} Fill in the title, description, and instructions.\n`);
|
|
62
|
-
process.stdout.write(` ${c.dim("2.")} Check it: ${c.cyan(`floom scan ${shellQuote(target)}`)}\n`);
|
|
63
|
-
process.stdout.write(` ${c.dim("3.")} Publish: ${c.cyan(`floom publish ${shellQuote(target)} --
|
|
210
|
+
process.stdout.write(` ${c.dim("2.")} Check it: ${c.cyan(`npx -y @floomhq/floom scan ${shellQuote(target)}`)}\n`);
|
|
211
|
+
process.stdout.write(` ${c.dim("3.")} Publish: ${c.cyan(`npx -y @floomhq/floom publish ${shellQuote(target)} --public`)}\n\n`);
|
|
64
212
|
}
|
|
65
213
|
function shellQuote(value) {
|
|
66
214
|
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
|
package/dist/install.js
CHANGED
|
@@ -58,7 +58,7 @@ async function localHash(path) {
|
|
|
58
58
|
if (code === "ENOENT")
|
|
59
59
|
return null;
|
|
60
60
|
if (code === "ELOOP")
|
|
61
|
-
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `floom add` again.");
|
|
61
|
+
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
62
62
|
if (code === "ENOTDIR" || code === "EISDIR") {
|
|
63
63
|
throw new FloomError("Local path is blocked by an existing file or directory.");
|
|
64
64
|
}
|
|
@@ -144,7 +144,7 @@ async function ensureSafeParentDirectory(root, target) {
|
|
|
144
144
|
async function assertSafeDirectory(path) {
|
|
145
145
|
const stat = await lstat(path);
|
|
146
146
|
if (stat.isSymbolicLink()) {
|
|
147
|
-
throw new FloomError("Local path contains a symbolic link.", "Move or delete the local path, then run `floom add` again.");
|
|
147
|
+
throw new FloomError("Local path contains a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
148
148
|
}
|
|
149
149
|
if (!stat.isDirectory()) {
|
|
150
150
|
throw new FloomError("Local path is blocked by an existing file or directory.");
|
|
@@ -159,7 +159,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
159
159
|
}
|
|
160
160
|
const cfg = await readConfig();
|
|
161
161
|
const apiUrl = resolveApiUrl(cfg);
|
|
162
|
-
const spinner = ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
|
|
162
|
+
const spinner = opts.json ? null : ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
|
|
163
163
|
let detail;
|
|
164
164
|
try {
|
|
165
165
|
detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "fetch skill", cfg?.accessToken);
|
|
@@ -168,7 +168,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
catch (err) {
|
|
171
|
-
spinner
|
|
171
|
+
spinner?.stop();
|
|
172
172
|
throw err;
|
|
173
173
|
}
|
|
174
174
|
try {
|
|
@@ -194,7 +194,7 @@ export async function install(slugInput, opts = {}) {
|
|
|
194
194
|
catch (err) {
|
|
195
195
|
const code = err.code;
|
|
196
196
|
if (code === "ELOOP")
|
|
197
|
-
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `floom add` again.");
|
|
197
|
+
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
198
198
|
throw err;
|
|
199
199
|
}
|
|
200
200
|
action = "updated";
|
|
@@ -212,16 +212,28 @@ export async function install(slugInput, opts = {}) {
|
|
|
212
212
|
throw new FloomError("Local skill already exists with different content.", "Run `npx -y @floomhq/floom add <link> --force` to replace it, or move the local file first.");
|
|
213
213
|
}
|
|
214
214
|
if (code === "ELOOP") {
|
|
215
|
-
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `floom add` again.");
|
|
215
|
+
throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
|
|
216
216
|
}
|
|
217
217
|
if (code === "ENOENT") {
|
|
218
|
-
throw new FloomError("Local path changed during install.", "Run `floom add` again.");
|
|
218
|
+
throw new FloomError("Local path changed during install.", "Run `npx -y @floomhq/floom add` again.");
|
|
219
219
|
}
|
|
220
220
|
throw err;
|
|
221
221
|
}
|
|
222
222
|
action = "installed";
|
|
223
223
|
}
|
|
224
|
-
|
|
224
|
+
const result = {
|
|
225
|
+
slug,
|
|
226
|
+
title: detail.title,
|
|
227
|
+
action,
|
|
228
|
+
target: targetAgent,
|
|
229
|
+
path: target,
|
|
230
|
+
content_hash: remoteHash,
|
|
231
|
+
};
|
|
232
|
+
spinner?.stop();
|
|
233
|
+
if (opts.json) {
|
|
234
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
225
237
|
process.stdout.write(`\n${symbols.ok} [floom] ${action} ${c.bold(slug)}\n`);
|
|
226
238
|
process.stdout.write(` ${c.dim(target)}\n\n`);
|
|
227
239
|
process.stdout.write(` ${c.bold("Next")}\n`);
|
package/dist/library.js
CHANGED
|
@@ -35,7 +35,7 @@ export async function libraryList(opts = {}) {
|
|
|
35
35
|
export async function libraryCreate(opts) {
|
|
36
36
|
const cfg = await readConfig();
|
|
37
37
|
if (!cfg)
|
|
38
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
38
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
39
39
|
const apiUrl = resolveApiUrl(cfg);
|
|
40
40
|
const result = await postJson(`${apiUrl}/api/v1/libraries`, "create library", cfg.accessToken, {
|
|
41
41
|
slug: opts.slug,
|
|
@@ -45,12 +45,12 @@ export async function libraryCreate(opts) {
|
|
|
45
45
|
});
|
|
46
46
|
process.stdout.write(`\n${symbols.ok} Library created: ${c.cyan(result.slug)}\n`);
|
|
47
47
|
process.stdout.write(` ${c.dim("API:")} ${apiUrl}/api/v1/libraries/${result.slug}\n`);
|
|
48
|
-
process.stdout.write(` ${c.dim("Sync:")} floom library subscribe ${result.slug}\n\n`);
|
|
48
|
+
process.stdout.write(` ${c.dim("Sync:")} npx -y @floomhq/floom library subscribe ${result.slug}\n\n`);
|
|
49
49
|
}
|
|
50
50
|
export async function libraryAddSkill(opts) {
|
|
51
51
|
const cfg = await readConfig();
|
|
52
52
|
if (!cfg)
|
|
53
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
53
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
54
54
|
const apiUrl = resolveApiUrl(cfg);
|
|
55
55
|
await postJson(`${apiUrl}/api/v1/libraries/${encodeURIComponent(opts.librarySlug)}/skills`, "add skill to library", cfg.accessToken, {
|
|
56
56
|
skill_slug: opts.skillSlug,
|
|
@@ -66,7 +66,7 @@ export async function libraryAddSkill(opts) {
|
|
|
66
66
|
export async function libraryRemoveSkill(librarySlug, skillSlug) {
|
|
67
67
|
const cfg = await readConfig();
|
|
68
68
|
if (!cfg)
|
|
69
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
69
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
70
70
|
const apiUrl = resolveApiUrl(cfg);
|
|
71
71
|
await deleteRequest(`${apiUrl}/api/v1/libraries/${encodeURIComponent(librarySlug)}/skills/${encodeURIComponent(skillSlug)}`, "remove skill from library", cfg.accessToken);
|
|
72
72
|
process.stdout.write(`\n${symbols.ok} Removed ${c.cyan(skillSlug)} from ${c.cyan(librarySlug)}\n\n`);
|
|
@@ -74,7 +74,7 @@ export async function libraryRemoveSkill(librarySlug, skillSlug) {
|
|
|
74
74
|
export async function librarySubscribe(slug) {
|
|
75
75
|
const cfg = await readConfig();
|
|
76
76
|
if (!cfg)
|
|
77
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
77
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
78
78
|
const apiUrl = resolveApiUrl(cfg);
|
|
79
79
|
await postJson(`${apiUrl}/api/v1/me/subscriptions`, "subscribe to library", cfg.accessToken, { library_slug: slug });
|
|
80
80
|
process.stdout.write(`\n${symbols.ok} Subscribed to ${c.cyan(slug)}\n`);
|
|
@@ -83,7 +83,7 @@ export async function librarySubscribe(slug) {
|
|
|
83
83
|
export async function libraryUnsubscribe(slug) {
|
|
84
84
|
const cfg = await readConfig();
|
|
85
85
|
if (!cfg)
|
|
86
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
86
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
87
87
|
const apiUrl = resolveApiUrl(cfg);
|
|
88
88
|
await deleteRequest(`${apiUrl}/api/v1/me/subscriptions/${encodeURIComponent(slug)}`, "unsubscribe from library", cfg.accessToken);
|
|
89
89
|
process.stdout.write(`\n${symbols.ok} Unsubscribed from ${c.cyan(slug)}\n\n`);
|
|
@@ -91,7 +91,7 @@ export async function libraryUnsubscribe(slug) {
|
|
|
91
91
|
export async function moveSkill(opts) {
|
|
92
92
|
const cfg = await readConfig();
|
|
93
93
|
if (!cfg)
|
|
94
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
94
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
95
95
|
const apiUrl = resolveApiUrl(cfg);
|
|
96
96
|
await putJson(`${apiUrl}/api/v1/me/skills/${encodeURIComponent(opts.slug)}/override`, "set skill override", cfg.accessToken, { folder: opts.folder, tags: opts.tags });
|
|
97
97
|
const folderText = opts.folder ?? c.dim("(root)");
|
package/dist/list.js
CHANGED
|
@@ -36,7 +36,7 @@ function formatRelative(iso) {
|
|
|
36
36
|
export async function list(opts) {
|
|
37
37
|
const cfg = await readConfig();
|
|
38
38
|
if (!cfg) {
|
|
39
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
39
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
40
40
|
}
|
|
41
41
|
const apiUrl = resolveApiUrl(cfg);
|
|
42
42
|
const spinner = opts.json ? null : ora({ text: c.dim("Loading skills..."), color: "yellow" }).start();
|
|
@@ -54,7 +54,7 @@ export async function list(opts) {
|
|
|
54
54
|
}
|
|
55
55
|
process.stdout.write(`\n${symbols.dot} ${c.bold("Published")} ${c.dim(`(${published.length})`)}\n\n`);
|
|
56
56
|
if (published.length === 0) {
|
|
57
|
-
process.stdout.write(` ${c.dim("Nothing published yet. Try `floom publish skill.md`.")}\n`);
|
|
57
|
+
process.stdout.write(` ${c.dim("Nothing published yet. Try `npx -y @floomhq/floom publish skill.md`.")}\n`);
|
|
58
58
|
}
|
|
59
59
|
else {
|
|
60
60
|
for (const s of published)
|
package/dist/login.js
CHANGED
|
@@ -21,7 +21,7 @@ export async function login() {
|
|
|
21
21
|
catch (err) {
|
|
22
22
|
spinner.stop();
|
|
23
23
|
if (err instanceof Error && /timed out/i.test(err.message)) {
|
|
24
|
-
throw new FloomError("No worries — try `floom login` again when ready.");
|
|
24
|
+
throw new FloomError("No worries — try `npx -y @floomhq/floom login` again when ready.");
|
|
25
25
|
}
|
|
26
26
|
throw err;
|
|
27
27
|
}
|
package/dist/mcp.js
CHANGED
|
@@ -2,8 +2,8 @@ import { c } from "./ui.js";
|
|
|
2
2
|
export function printMcpSetup() {
|
|
3
3
|
const snippet = `## Floom
|
|
4
4
|
- Use Floom skills from the local Floom skills folder when they match the task.
|
|
5
|
-
- To install a shared skill, run \`floom add <slug-or-url> --target claude\` or \`floom add <slug-or-url> --target codex\`.
|
|
6
|
-
- To find reusable behavior, run \`floom search <query>\`.
|
|
5
|
+
- To install a shared skill, run \`npx -y @floomhq/floom add <slug-or-url> --target claude\` or \`npx -y @floomhq/floom add <slug-or-url> --target codex\`.
|
|
6
|
+
- To find reusable behavior, run \`npx -y @floomhq/floom search <query>\`.
|
|
7
7
|
- MCP sync is optional preview behavior; use it only while the Floom MCP server is configured and running.`;
|
|
8
8
|
process.stdout.write(`\n${c.bold("Floom MCP setup")}\n\n`);
|
|
9
9
|
process.stdout.write(`${c.dim("Pick your tool:")}\n\n`);
|
package/dist/publish.js
CHANGED
|
@@ -140,7 +140,7 @@ export async function publish(opts) {
|
|
|
140
140
|
const version = opts.version ?? parseVersion(meta.version, "frontmatter version") ?? null;
|
|
141
141
|
const cfg = await readConfig();
|
|
142
142
|
if (!cfg) {
|
|
143
|
-
throw new FloomError("Not signed in.", "Run `floom login` first.");
|
|
143
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
144
144
|
}
|
|
145
145
|
// Later version: detect already-published when --update is missing.
|
|
146
146
|
// The current API does not return a duplicate-skill code, so we leave
|
|
@@ -192,10 +192,15 @@ export async function publish(opts) {
|
|
|
192
192
|
spinner.stop();
|
|
193
193
|
const versionTag = version ? c.dim(` (${formatVersionLabel(version)})`) : "";
|
|
194
194
|
const titleLabel = data.title ? `"${data.title}"` : opts.file;
|
|
195
|
+
const invitedEmails = opts.sharedWithEmails ?? [];
|
|
195
196
|
process.stdout.write(`\n${symbols.ok} Published ${c.bold(titleLabel)}${versionTag}\n\n`);
|
|
197
|
+
process.stdout.write(` ${c.bold("Send this to someone:")}\n`);
|
|
196
198
|
process.stdout.write(` ${c.cyan(humanUrl)}\n\n`);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
+
process.stdout.write(` ${c.bold("They run:")}\n`);
|
|
200
|
+
process.stdout.write(` ${c.cyan(`npx -y @floomhq/floom add ${humanUrl} --setup`)}\n\n`);
|
|
201
|
+
if (invitedEmails.length) {
|
|
202
|
+
process.stdout.write(` ${c.bold("Email invite:")}\n`);
|
|
203
|
+
process.stdout.write(` ${c.dim(invitedEmails.join(", "))}\n\n`);
|
|
199
204
|
}
|
|
200
205
|
let copied = false;
|
|
201
206
|
try {
|
|
@@ -206,14 +211,11 @@ export async function publish(opts) {
|
|
|
206
211
|
copied = false;
|
|
207
212
|
}
|
|
208
213
|
if (copied) {
|
|
209
|
-
process.stdout.write(` ${c.dim("Copied to clipboard.
|
|
214
|
+
process.stdout.write(` ${c.dim("Copied link to clipboard.")}\n\n`);
|
|
210
215
|
}
|
|
211
216
|
else {
|
|
212
217
|
process.stdout.write(` ${c.dim("Share it anywhere.")}\n\n`);
|
|
213
218
|
}
|
|
214
|
-
process.stdout.write(` ${c.bold("
|
|
215
|
-
process.stdout.write(` ${c.
|
|
216
|
-
process.stdout.write(` ${c.dim("2.")} Send the link.\n`);
|
|
217
|
-
process.stdout.write(` ${c.dim("3.")} Receiver runs ${c.cyan(`npx -y @floomhq/floom add ${humanUrl} --setup`)}\n`);
|
|
218
|
-
process.stdout.write(` ${c.dim("4.")} Agent reads the installed Markdown from the local skills folder.\n\n`);
|
|
219
|
+
process.stdout.write(` ${c.bold("Agent prompt:")}\n`);
|
|
220
|
+
process.stdout.write(` ${c.cyan("npx -y @floomhq/floom agent-prompt")}\n\n`);
|
|
219
221
|
}
|
package/dist/secrets.js
CHANGED
|
@@ -5,15 +5,29 @@ const SECRET_PATTERNS = [
|
|
|
5
5
|
{ label: "Google API key", regex: /\bAIza[0-9A-Za-z_-]{25,}\b/g },
|
|
6
6
|
{ label: "GitHub token", regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{30,}\b/g },
|
|
7
7
|
{ label: "GitHub token", regex: /\bgithub_pat_[A-Za-z0-9_]{40,}\b/g },
|
|
8
|
+
{ label: "npm token", regex: /\bnpm_[A-Za-z0-9]{30,}\b/g },
|
|
8
9
|
{ label: "Supabase access token", regex: /\bsbp_[A-Za-z0-9]{30,}\b/g },
|
|
9
10
|
{ label: "Stripe secret key", regex: /\bsk_(?:live|test)_[A-Za-z0-9]{20,}\b/g },
|
|
10
11
|
{ label: "Slack token", regex: /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g },
|
|
11
12
|
{ label: "AWS access key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
|
|
13
|
+
{ label: "Database URL with password", regex: /\b(?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis):\/\/[^:\s/@]+:[^@\s]{8,}@[^\s]+/gi },
|
|
12
14
|
{ label: "Private key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g },
|
|
13
15
|
];
|
|
14
16
|
const GENERIC_ASSIGNMENT_RE = /\b(?:api[_-]?key|secret|access[_-]?token|auth[_-]?token|bearer[_-]?token)\b\s*[:=]\s*["']?([A-Za-z0-9_./+=-]{24,})["']?/gi;
|
|
15
17
|
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|placeholder|replace|changeme|todo|xxx|
|
|
18
|
+
const PLACEHOLDER_RE = /(?:^|[_./+=-])(?:your|example|sample|mock|placeholder|replace|changeme|todo|xxx|demo|dummy|fake|redacted)(?:$|[_./+=-])/i;
|
|
19
|
+
const PLACEHOLDER_PHRASE_WORDS = new Set([
|
|
20
|
+
"and",
|
|
21
|
+
"fake",
|
|
22
|
+
"is",
|
|
23
|
+
"key",
|
|
24
|
+
"long",
|
|
25
|
+
"looks",
|
|
26
|
+
"real",
|
|
27
|
+
"secret",
|
|
28
|
+
"that",
|
|
29
|
+
"very",
|
|
30
|
+
]);
|
|
17
31
|
const PROMPT_INJECTION_PATTERNS = [
|
|
18
32
|
{ label: "Prompt injection instruction", regex: /\bignore (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
|
|
19
33
|
{ label: "Prompt injection instruction", regex: /\bdisregard (?:all )?(?:previous|prior|above|earlier) instructions\b/gi },
|
|
@@ -46,6 +60,12 @@ function pushFinding(findings, seen, label, line, value) {
|
|
|
46
60
|
seen.add(key);
|
|
47
61
|
findings.push({ label, line, preview: redact(value) });
|
|
48
62
|
}
|
|
63
|
+
function isPlaceholderValue(value) {
|
|
64
|
+
if (PLACEHOLDER_RE.test(value))
|
|
65
|
+
return true;
|
|
66
|
+
const words = value.toLowerCase().split(/[^a-z]+/).filter(Boolean);
|
|
67
|
+
return words.length >= 6 && words.every((word) => PLACEHOLDER_PHRASE_WORDS.has(word));
|
|
68
|
+
}
|
|
49
69
|
export function detectSecrets(input) {
|
|
50
70
|
const findings = [];
|
|
51
71
|
const seen = new Set();
|
|
@@ -59,14 +79,14 @@ export function detectSecrets(input) {
|
|
|
59
79
|
GENERIC_ASSIGNMENT_RE.lastIndex = 0;
|
|
60
80
|
for (const match of input.matchAll(GENERIC_ASSIGNMENT_RE)) {
|
|
61
81
|
const value = match[1] ?? "";
|
|
62
|
-
if (!value ||
|
|
82
|
+
if (!value || isPlaceholderValue(value))
|
|
63
83
|
continue;
|
|
64
84
|
pushFinding(findings, seen, "Possible secret assignment", lineNumberAt(input, match.index ?? 0), value);
|
|
65
85
|
}
|
|
66
86
|
PROVIDER_LIKE_ASSIGNMENT_RE.lastIndex = 0;
|
|
67
87
|
for (const match of input.matchAll(PROVIDER_LIKE_ASSIGNMENT_RE)) {
|
|
68
88
|
const value = match[1] ?? "";
|
|
69
|
-
if (!value)
|
|
89
|
+
if (!value || isPlaceholderValue(value))
|
|
70
90
|
continue;
|
|
71
91
|
pushFinding(findings, seen, "Provider-like secret assignment", lineNumberAt(input, match.index ?? 0), value);
|
|
72
92
|
}
|
package/dist/setup.js
CHANGED
|
@@ -13,16 +13,16 @@ const TARGETS = {
|
|
|
13
13
|
};
|
|
14
14
|
function floomAgentInstructions(target) {
|
|
15
15
|
const addCommand = target === "codex"
|
|
16
|
-
? "floom add <slug-or-url> --target codex"
|
|
17
|
-
: "floom add <slug-or-url> --target claude";
|
|
16
|
+
? "npx -y @floomhq/floom add <slug-or-url> --target codex"
|
|
17
|
+
: "npx -y @floomhq/floom add <slug-or-url> --target claude";
|
|
18
18
|
return `${START_MARKER}
|
|
19
19
|
## Floom
|
|
20
20
|
|
|
21
21
|
- Before recreating agent behavior from scratch, check Floom for reusable skills.
|
|
22
|
-
- Search or inspect skills with \`floom search <query>\`, \`floom info <slug-or-url>\`, and \`floom list\`.
|
|
22
|
+
- Search or inspect skills with \`npx -y @floomhq/floom search <query>\`, \`npx -y @floomhq/floom info <slug-or-url>\`, and \`npx -y @floomhq/floom list\`.
|
|
23
23
|
- Add shared skills with \`${addCommand}\`; public and unlisted links do not require a Floom account.
|
|
24
24
|
- Use installed Markdown skills from the local skills folder when they match the task.
|
|
25
|
-
- \`floom sync\`, \`floom watch\`, and \`@floomhq/floom-mcp-sync\` are preview paths for saved, published, and subscribed library skills; review conflicts before relying on synced output.
|
|
25
|
+
- \`npx -y @floomhq/floom sync\`, \`npx -y @floomhq/floom watch\`, and \`@floomhq/floom-mcp-sync\` are preview paths for saved, published, and subscribed library skills; review conflicts before relying on synced output.
|
|
26
26
|
${END_MARKER}`;
|
|
27
27
|
}
|
|
28
28
|
async function fileExists(path) {
|
|
@@ -96,7 +96,7 @@ async function detectTarget(opts) {
|
|
|
96
96
|
return { agent: "claude", label: TARGETS.claude.label, path: claude };
|
|
97
97
|
if (codex)
|
|
98
98
|
return { agent: "codex", label: TARGETS.codex.label, path: codex };
|
|
99
|
-
throw new FloomError("No agent instruction file found.", "Run `floom setup --target claude --yes` or `floom setup --target codex --yes` from the repo root.");
|
|
99
|
+
throw new FloomError("No agent instruction file found.", "Run `npx -y @floomhq/floom setup --target claude --yes` or `npx -y @floomhq/floom setup --target codex --yes` from the repo root.");
|
|
100
100
|
}
|
|
101
101
|
function renderPreview(target, existing) {
|
|
102
102
|
const action = existing === null ? "create" : "append";
|
|
@@ -108,7 +108,7 @@ function renderPreview(target, existing) {
|
|
|
108
108
|
"",
|
|
109
109
|
floomAgentInstructions(target.agent),
|
|
110
110
|
"",
|
|
111
|
-
`${c.dim("MCP setup guidance:")} run ${c.cyan("floom mcp")} to print local agent commands.`,
|
|
111
|
+
`${c.dim("MCP setup guidance:")} run ${c.cyan("npx -y @floomhq/floom mcp")} to print local agent commands.`,
|
|
112
112
|
"",
|
|
113
113
|
].join("\n");
|
|
114
114
|
}
|
|
@@ -153,7 +153,7 @@ export async function setupAgent(opts) {
|
|
|
153
153
|
if (existing === null) {
|
|
154
154
|
await writeFile(target.path, next, { encoding: "utf8", flag: "wx" }).catch((err) => {
|
|
155
155
|
if (err instanceof Error && "code" in err && err.code === "EEXIST") {
|
|
156
|
-
throw new FloomError("Instruction file appeared while setup was running.", "Re-run `floom setup` so Floom can inspect the current file before writing.");
|
|
156
|
+
throw new FloomError("Instruction file appeared while setup was running.", "Re-run `npx -y @floomhq/floom setup` so Floom can inspect the current file before writing.");
|
|
157
157
|
}
|
|
158
158
|
throw err;
|
|
159
159
|
});
|
|
@@ -162,5 +162,5 @@ export async function setupAgent(opts) {
|
|
|
162
162
|
await writeFile(target.path, next, "utf8");
|
|
163
163
|
}
|
|
164
164
|
process.stdout.write(`\n${symbols.ok} Added Floom instructions to ${c.bold(target.path)}\n`);
|
|
165
|
-
process.stdout.write(` ${c.dim("MCP setup guidance:")} ${c.cyan("floom mcp")}\n\n`);
|
|
165
|
+
process.stdout.write(` ${c.dim("MCP setup guidance:")} ${c.cyan("npx -y @floomhq/floom mcp")}\n\n`);
|
|
166
166
|
}
|