@floomhq/floom 1.0.14 → 1.0.17
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 +36 -30
- package/dist/cli.js +127 -233
- package/dist/doctor.js +119 -38
- package/dist/errors.js +1 -1
- package/dist/info.js +1 -1
- package/dist/init.js +87 -92
- package/dist/install.js +140 -67
- package/dist/library.js +4 -8
- package/dist/list.js +7 -8
- package/dist/login.js +81 -46
- package/dist/mcp.js +4 -7
- package/dist/package.js +318 -0
- package/dist/publish.js +51 -51
- package/dist/scan.js +18 -23
- package/dist/secrets.js +3 -29
- package/dist/setup.js +12 -14
- package/dist/sync-manifest.js +65 -16
- package/dist/sync.js +216 -172
- package/package.json +3 -2
- package/dist/targets.js +0 -16
package/dist/doctor.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
3
|
+
import { delimiter } from "node:path";
|
|
2
4
|
import { join } from "node:path";
|
|
3
|
-
import { stat, readFile, access, readdir, constants } from "node:fs/promises";
|
|
5
|
+
import { stat, readFile, access, readdir, constants, realpath } from "node:fs/promises";
|
|
6
|
+
import { promisify } from "node:util";
|
|
4
7
|
import { readConfig, CONFIG_PATH, resolveApiUrl } from "./config.js";
|
|
5
8
|
import { floomFetch } from "./lib/api.js";
|
|
6
9
|
import { c, symbols } from "./ui.js";
|
|
7
10
|
import { CLI_VERSION, compareSemverish, formatVersionLabel } from "./version.js";
|
|
8
|
-
|
|
11
|
+
const execFile = promisify(execFileCb);
|
|
9
12
|
function statusBadge(s) {
|
|
10
13
|
if (s === "ok")
|
|
11
14
|
return c.green(symbols.ok);
|
|
@@ -19,7 +22,7 @@ async function checkAuth() {
|
|
|
19
22
|
return {
|
|
20
23
|
name: "Auth",
|
|
21
24
|
status: "ok",
|
|
22
|
-
detail: "Receiver mode ready
|
|
25
|
+
detail: "Receiver mode ready. Sign in only when publishing or listing your own skills.",
|
|
23
26
|
};
|
|
24
27
|
}
|
|
25
28
|
const apiUrl = resolveApiUrl(cfg);
|
|
@@ -89,7 +92,7 @@ async function checkMcp() {
|
|
|
89
92
|
return {
|
|
90
93
|
name: "MCP",
|
|
91
94
|
status: "ok",
|
|
92
|
-
detail: "Optional MCP not registered. `npx -y @floomhq/floom add` still writes local files
|
|
95
|
+
detail: "Optional MCP not registered. `npx -y @floomhq/floom add` still writes local skill files.",
|
|
93
96
|
};
|
|
94
97
|
}
|
|
95
98
|
return {
|
|
@@ -98,24 +101,31 @@ async function checkMcp() {
|
|
|
98
101
|
detail: `Registered with: ${found.map((f) => f.name).join(", ")}`,
|
|
99
102
|
};
|
|
100
103
|
}
|
|
104
|
+
function targetSkillsDir(target) {
|
|
105
|
+
if (target === "codex") {
|
|
106
|
+
const codexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
107
|
+
return process.env.CODEX_SKILLS_DIR ?? join(codexHome, "skills");
|
|
108
|
+
}
|
|
109
|
+
return process.env.CLAUDE_SKILLS_DIR ?? join(homedir(), ".claude", "skills");
|
|
110
|
+
}
|
|
101
111
|
async function checkTargetDir(target) {
|
|
102
|
-
const dir =
|
|
112
|
+
const dir = targetSkillsDir(target);
|
|
103
113
|
try {
|
|
104
114
|
const s = await stat(dir);
|
|
105
115
|
if (!s.isDirectory()) {
|
|
106
116
|
return {
|
|
107
|
-
name:
|
|
117
|
+
name: "Target dir",
|
|
108
118
|
status: "fail",
|
|
109
119
|
detail: `${dir} exists but is not a directory.`,
|
|
110
120
|
};
|
|
111
121
|
}
|
|
112
122
|
try {
|
|
113
123
|
await access(dir, constants.W_OK);
|
|
114
|
-
return { name:
|
|
124
|
+
return { name: "Target dir", status: "ok", detail: `${dir} (writable)` };
|
|
115
125
|
}
|
|
116
126
|
catch {
|
|
117
127
|
return {
|
|
118
|
-
name:
|
|
128
|
+
name: "Target dir",
|
|
119
129
|
status: "fail",
|
|
120
130
|
detail: `${dir} is not writable.`,
|
|
121
131
|
hint: `Try: chmod u+w ${dir}`,
|
|
@@ -125,26 +135,26 @@ async function checkTargetDir(target) {
|
|
|
125
135
|
catch (err) {
|
|
126
136
|
if (err.code === "ENOENT") {
|
|
127
137
|
return {
|
|
128
|
-
name:
|
|
138
|
+
name: "Target dir",
|
|
129
139
|
status: "warn",
|
|
130
140
|
detail: `${dir} does not exist yet.`,
|
|
131
|
-
hint:
|
|
141
|
+
hint: `It will be created on first \`npx -y @floomhq/floom add <link> --target ${target}\`.`,
|
|
132
142
|
};
|
|
133
143
|
}
|
|
134
144
|
throw err;
|
|
135
145
|
}
|
|
136
146
|
}
|
|
137
147
|
async function checkLastSync(target) {
|
|
138
|
-
const dir =
|
|
148
|
+
const dir = targetSkillsDir(target);
|
|
139
149
|
try {
|
|
140
150
|
const entries = await readdir(dir);
|
|
141
151
|
const skills = entries.filter((e) => e.endsWith(".md") || !e.startsWith("."));
|
|
142
152
|
if (skills.length === 0) {
|
|
143
153
|
return {
|
|
144
|
-
name:
|
|
154
|
+
name: "Last sync",
|
|
145
155
|
status: "warn",
|
|
146
156
|
detail: "No synced skills found yet.",
|
|
147
|
-
hint:
|
|
157
|
+
hint: `Run \`npx -y @floomhq/floom add <link> --target ${target}\` to install your first skill.`,
|
|
148
158
|
};
|
|
149
159
|
}
|
|
150
160
|
// Find most recently modified entry
|
|
@@ -161,7 +171,7 @@ async function checkLastSync(target) {
|
|
|
161
171
|
}
|
|
162
172
|
if (newest.mtime === 0) {
|
|
163
173
|
return {
|
|
164
|
-
name:
|
|
174
|
+
name: "Last sync",
|
|
165
175
|
status: "warn",
|
|
166
176
|
detail: `${skills.length} entries, no readable mtime.`,
|
|
167
177
|
};
|
|
@@ -175,7 +185,7 @@ async function checkLastSync(target) {
|
|
|
175
185
|
? `${Math.round(ageSec / 3600)}h ago`
|
|
176
186
|
: `${Math.round(ageSec / 86400)}d ago`;
|
|
177
187
|
return {
|
|
178
|
-
name:
|
|
188
|
+
name: "Last sync",
|
|
179
189
|
status: "ok",
|
|
180
190
|
detail: `${newest.name} — ${human} (${skills.length} total)`,
|
|
181
191
|
};
|
|
@@ -183,13 +193,13 @@ async function checkLastSync(target) {
|
|
|
183
193
|
catch (err) {
|
|
184
194
|
if (err.code === "ENOENT") {
|
|
185
195
|
return {
|
|
186
|
-
name:
|
|
196
|
+
name: "Last sync",
|
|
187
197
|
status: "warn",
|
|
188
198
|
detail: "Skills dir not created yet.",
|
|
189
199
|
};
|
|
190
200
|
}
|
|
191
201
|
return {
|
|
192
|
-
name:
|
|
202
|
+
name: "Last sync",
|
|
193
203
|
status: "warn",
|
|
194
204
|
detail: `Cannot read skills dir: ${err.message}`,
|
|
195
205
|
};
|
|
@@ -216,7 +226,7 @@ async function checkVersion() {
|
|
|
216
226
|
name: "Version",
|
|
217
227
|
status: "fail",
|
|
218
228
|
detail: `CLI ${formatVersionLabel(CLI_VERSION)} below required ${formatVersionLabel(data.min)}.`,
|
|
219
|
-
hint: "Run `npm i -g @floomhq/floom` to upgrade
|
|
229
|
+
hint: "Run `npm i -g @floomhq/floom` to upgrade, then use `floom`.",
|
|
220
230
|
};
|
|
221
231
|
}
|
|
222
232
|
if (data.latest && compareSemverish(CLI_VERSION, data.latest) < 0) {
|
|
@@ -224,7 +234,7 @@ async function checkVersion() {
|
|
|
224
234
|
name: "Version",
|
|
225
235
|
status: "warn",
|
|
226
236
|
detail: `CLI ${formatVersionLabel(CLI_VERSION)}, latest is ${formatVersionLabel(data.latest)}.`,
|
|
227
|
-
hint: "Run `npm i -g @floomhq/floom` to upgrade
|
|
237
|
+
hint: "Run `npm i -g @floomhq/floom` to upgrade, then use `floom`.",
|
|
228
238
|
};
|
|
229
239
|
}
|
|
230
240
|
return { name: "Version", status: "ok", detail: `CLI ${formatVersionLabel(CLI_VERSION)} (current)` };
|
|
@@ -237,32 +247,103 @@ async function checkVersion() {
|
|
|
237
247
|
};
|
|
238
248
|
}
|
|
239
249
|
}
|
|
250
|
+
async function findPathExecutables(name) {
|
|
251
|
+
const seen = new Set();
|
|
252
|
+
const out = [];
|
|
253
|
+
const pathDirs = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
254
|
+
for (const dir of pathDirs) {
|
|
255
|
+
const candidate = join(dir, name);
|
|
256
|
+
if (seen.has(candidate))
|
|
257
|
+
continue;
|
|
258
|
+
seen.add(candidate);
|
|
259
|
+
try {
|
|
260
|
+
await access(candidate, constants.X_OK);
|
|
261
|
+
out.push(candidate);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// not present or not executable
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|
|
269
|
+
async function safeRealpath(path) {
|
|
270
|
+
if (!path)
|
|
271
|
+
return null;
|
|
272
|
+
try {
|
|
273
|
+
return await realpath(path);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function checkCliCommand() {
|
|
280
|
+
const bins = await findPathExecutables("floom");
|
|
281
|
+
if (bins.length === 0) {
|
|
282
|
+
return {
|
|
283
|
+
name: "CLI command",
|
|
284
|
+
status: "ok",
|
|
285
|
+
detail: "`floom` is not globally installed. `npx -y @floomhq/floom ...` is ready.",
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const active = bins[0] ?? "";
|
|
289
|
+
const activeReal = await safeRealpath(active);
|
|
290
|
+
const currentReal = await safeRealpath(process.argv[1]);
|
|
291
|
+
if (activeReal && currentReal && activeReal === currentReal) {
|
|
292
|
+
return {
|
|
293
|
+
name: "CLI command",
|
|
294
|
+
status: "ok",
|
|
295
|
+
detail: `floom resolves to ${active}`,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
const activeText = await readExecutableSample(active);
|
|
299
|
+
const activeVersion = await readExecutableVersion(active);
|
|
300
|
+
const looksLikeOldRuntime = active.includes(".floom/repo") ||
|
|
301
|
+
active.includes("/skills-minimal/") ||
|
|
302
|
+
activeText.includes("unknown command") ||
|
|
303
|
+
activeText.includes("Floom runtime") ||
|
|
304
|
+
(activeVersion !== null && compareSemverish(activeVersion, CLI_VERSION) < 0);
|
|
305
|
+
return {
|
|
306
|
+
name: "CLI command",
|
|
307
|
+
status: looksLikeOldRuntime ? "warn" : "ok",
|
|
308
|
+
detail: looksLikeOldRuntime
|
|
309
|
+
? `Another floom command is first on PATH: ${active}${activeVersion ? ` (${formatVersionLabel(activeVersion)})` : ""}`
|
|
310
|
+
: `floom resolves to ${active}`,
|
|
311
|
+
...(looksLikeOldRuntime
|
|
312
|
+
? {
|
|
313
|
+
hint: `If \`floom doctor\` opens the old runtime CLI, remove that file or move it later in PATH, then run \`npm i -g @floomhq/floom\`. Inspect with: command -v floom`,
|
|
314
|
+
}
|
|
315
|
+
: {}),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
async function readExecutableSample(path) {
|
|
319
|
+
try {
|
|
320
|
+
return (await readFile(path, "utf8")).slice(0, 4000);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return "";
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function readExecutableVersion(path) {
|
|
327
|
+
try {
|
|
328
|
+
const { stdout, stderr } = await execFile(path, ["--version"], { timeout: 1500 });
|
|
329
|
+
const text = `${stdout}\n${stderr}`;
|
|
330
|
+
return text.match(/\d+\.\d+\.\d+(?:[-+][A-Za-z0-9._-]+)?/)?.[0] ?? null;
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
240
336
|
export async function doctor(opts = {}) {
|
|
241
337
|
const target = opts.target ?? "claude";
|
|
338
|
+
process.stdout.write(`\n${c.bold("floom doctor")} ${c.dim(`(${formatVersionLabel(CLI_VERSION)}, target: ${target})`)}\n\n`);
|
|
242
339
|
const checks = await Promise.all([
|
|
340
|
+
checkCliCommand(),
|
|
243
341
|
checkAuth(),
|
|
244
342
|
checkMcp(),
|
|
245
343
|
checkTargetDir(target),
|
|
246
344
|
checkLastSync(target),
|
|
247
345
|
checkVersion(),
|
|
248
346
|
]);
|
|
249
|
-
const anyFail = checks.some((c) => c.status === "fail");
|
|
250
|
-
const anyWarn = checks.some((c) => c.status === "warn");
|
|
251
|
-
const status = anyFail ? "fail" : anyWarn ? "warn" : "ok";
|
|
252
|
-
if (opts.json) {
|
|
253
|
-
process.stdout.write(`${JSON.stringify({
|
|
254
|
-
ok: !anyFail,
|
|
255
|
-
status,
|
|
256
|
-
version: CLI_VERSION,
|
|
257
|
-
target,
|
|
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`);
|
|
266
347
|
// Compute column widths for clean table output.
|
|
267
348
|
const nameW = Math.max(...checks.map((c) => c.name.length), 6);
|
|
268
349
|
for (const check of checks) {
|
|
@@ -272,6 +353,8 @@ export async function doctor(opts = {}) {
|
|
|
272
353
|
process.stdout.write(` ${c.dim("→ " + check.hint)}\n`);
|
|
273
354
|
}
|
|
274
355
|
}
|
|
356
|
+
const anyFail = checks.some((c) => c.status === "fail");
|
|
357
|
+
const anyWarn = checks.some((c) => c.status === "warn");
|
|
275
358
|
process.stdout.write("\n");
|
|
276
359
|
if (anyFail) {
|
|
277
360
|
process.stdout.write(` ${c.red("✗ Some checks failed.")} See hints above.\n\n`);
|
|
@@ -279,10 +362,8 @@ export async function doctor(opts = {}) {
|
|
|
279
362
|
}
|
|
280
363
|
if (anyWarn) {
|
|
281
364
|
process.stdout.write(` ${c.yellow("! All critical checks passed, with warnings.")}\n\n`);
|
|
282
|
-
process.stdout.write(` ${c.dim("MCP sync is optional and account-backed; add still works without MCP.")}\n\n`);
|
|
283
365
|
process.exit(0);
|
|
284
366
|
}
|
|
285
367
|
process.stdout.write(` ${c.green("✓ All checks passed.")} Floom is healthy.\n\n`);
|
|
286
|
-
process.stdout.write(` ${c.dim("MCP sync is optional and account-backed; add still works without MCP.")}\n`);
|
|
287
368
|
process.stdout.write(` ${c.dim("Config: " + CONFIG_PATH)}\n\n`);
|
|
288
369
|
}
|
package/dist/errors.js
CHANGED
|
@@ -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.", "Run `npx -y @floomhq/floom publish <path>` to create a new one.");
|
|
27
27
|
}
|
|
28
28
|
if (status === 413) {
|
|
29
29
|
return new FloomError("That file is too large to publish.");
|
package/dist/info.js
CHANGED
|
@@ -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)}`, "load skill metadata", cfg?.accessToken);
|
|
32
32
|
}
|
|
33
33
|
finally {
|
|
34
34
|
spinner?.stop();
|
package/dist/init.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { writeFile, access } from "node:fs/promises";
|
|
2
|
-
import { dirname, resolve, basename } from "node:path";
|
|
1
|
+
import { writeFile, access, mkdir } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve, basename, extname, join } from "node:path";
|
|
3
3
|
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";
|
|
@@ -30,7 +30,7 @@ version: 1.0
|
|
|
30
30
|
`,
|
|
31
31
|
"brand-voice": `---
|
|
32
32
|
title: Brand voice
|
|
33
|
-
description:
|
|
33
|
+
description: Keep agent writing aligned with our company voice.
|
|
34
34
|
type: knowledge
|
|
35
35
|
installs_as: memory
|
|
36
36
|
version: 1.0
|
|
@@ -38,29 +38,27 @@ version: 1.0
|
|
|
38
38
|
|
|
39
39
|
# Brand Voice
|
|
40
40
|
|
|
41
|
-
## Use when
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
41
|
+
## Use this when
|
|
42
|
+
|
|
43
|
+
- Writing external copy
|
|
44
|
+
- Editing founder posts
|
|
45
|
+
- Drafting website, email, or support text
|
|
45
46
|
|
|
46
47
|
## Voice rules
|
|
47
|
-
- Sound clear, direct, and useful.
|
|
48
|
-
- Prefer concrete nouns and short sentences.
|
|
49
|
-
- Avoid hype, filler, and generic AI language.
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
-
|
|
49
|
+
- Be clear and specific.
|
|
50
|
+
- Use short paragraphs.
|
|
51
|
+
- Avoid hype, filler, and vague claims.
|
|
53
52
|
|
|
54
|
-
##
|
|
55
|
-
- Replace this list with banned or overused terms.
|
|
53
|
+
## Company facts
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
55
|
+
- Add product facts here.
|
|
56
|
+
- Add banned phrases here.
|
|
57
|
+
- Add preferred examples here.
|
|
60
58
|
`,
|
|
61
59
|
"pr-review": `---
|
|
62
60
|
title: PR review
|
|
63
|
-
description: Review
|
|
61
|
+
description: Review pull requests for correctness, regressions, and missing tests.
|
|
64
62
|
type: workflow
|
|
65
63
|
installs_as: claude_skill
|
|
66
64
|
version: 1.0
|
|
@@ -68,21 +66,26 @@ version: 1.0
|
|
|
68
66
|
|
|
69
67
|
# PR Review
|
|
70
68
|
|
|
71
|
-
##
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
## Goal
|
|
70
|
+
|
|
71
|
+
Find concrete bugs, regressions, security issues, and missing tests before code merges.
|
|
72
|
+
|
|
73
|
+
## Inputs
|
|
74
|
+
|
|
75
|
+
- Diff
|
|
76
|
+
- Test output
|
|
77
|
+
- Relevant files and callers
|
|
78
|
+
|
|
79
|
+
## Review steps
|
|
74
80
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
4. Maintainability
|
|
81
|
+
1. Read the diff and affected call sites.
|
|
82
|
+
2. Check behavior changes against the stated intent.
|
|
83
|
+
3. Look for edge cases, auth/security issues, data loss, and broken contracts.
|
|
84
|
+
4. Verify tests cover the changed behavior.
|
|
80
85
|
|
|
81
86
|
## Output
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
- Keep style comments out unless they affect behavior.
|
|
85
|
-
- If no issues are found, say that clearly and name any test gaps.
|
|
87
|
+
|
|
88
|
+
List findings first, ordered by severity, with file and line references.
|
|
86
89
|
`,
|
|
87
90
|
sales: `---
|
|
88
91
|
title: Sales research
|
|
@@ -94,27 +97,25 @@ version: 1.0
|
|
|
94
97
|
|
|
95
98
|
# Sales Research
|
|
96
99
|
|
|
97
|
-
##
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
- Summarizing account context for the team
|
|
100
|
+
## Goal
|
|
101
|
+
|
|
102
|
+
Prepare useful context before contacting an account.
|
|
101
103
|
|
|
102
|
-
##
|
|
103
|
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
104
|
+
## Research checklist
|
|
105
|
+
|
|
106
|
+
- Company description
|
|
107
|
+
- Current priorities or trigger events
|
|
108
|
+
- Relevant people
|
|
106
109
|
- Likely pain
|
|
107
|
-
-
|
|
110
|
+
- Specific reason to reach out
|
|
108
111
|
|
|
109
112
|
## Output
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
- 2 tailored opener angles
|
|
113
|
-
- 1 clear next action
|
|
113
|
+
|
|
114
|
+
Return a short account brief and a first-message angle.
|
|
114
115
|
`,
|
|
115
116
|
support: `---
|
|
116
117
|
title: Support tone
|
|
117
|
-
description:
|
|
118
|
+
description: Keep support replies concise, helpful, and calm.
|
|
118
119
|
type: instruction
|
|
119
120
|
installs_as: memory
|
|
120
121
|
version: 1.0
|
|
@@ -122,68 +123,59 @@ version: 1.0
|
|
|
122
123
|
|
|
123
124
|
# Support Tone
|
|
124
125
|
|
|
125
|
-
## Use when
|
|
126
|
-
- Replying to customer support messages
|
|
127
|
-
- Summarizing customer issues
|
|
128
|
-
- Drafting escalation notes
|
|
129
|
-
|
|
130
126
|
## Rules
|
|
131
|
-
|
|
127
|
+
|
|
128
|
+
- Acknowledge the issue directly.
|
|
132
129
|
- Give the next concrete step.
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
2. Direct answer or next step
|
|
140
|
-
3. What happens next
|
|
130
|
+
- Avoid blaming the user.
|
|
131
|
+
- Avoid long explanations unless the user asks.
|
|
132
|
+
|
|
133
|
+
## Output
|
|
134
|
+
|
|
135
|
+
Write the reply in plain language with a clear next action.
|
|
141
136
|
`,
|
|
142
137
|
onboarding: `---
|
|
143
|
-
title:
|
|
144
|
-
description:
|
|
138
|
+
title: Onboarding context
|
|
139
|
+
description: Give agents the context needed to help new teammates.
|
|
145
140
|
type: knowledge
|
|
146
141
|
installs_as: memory
|
|
147
142
|
version: 1.0
|
|
148
143
|
---
|
|
149
144
|
|
|
150
|
-
#
|
|
145
|
+
# Onboarding Context
|
|
151
146
|
|
|
152
|
-
##
|
|
153
|
-
- A new teammate asks how work gets done
|
|
154
|
-
- An agent needs company or team context
|
|
155
|
-
- Creating first-week task plans
|
|
147
|
+
## Company
|
|
156
148
|
|
|
157
|
-
|
|
158
|
-
-
|
|
159
|
-
- Customers:
|
|
160
|
-
- Current priorities:
|
|
161
|
-
- Tools:
|
|
149
|
+
- Add what the company does.
|
|
150
|
+
- Add important product areas.
|
|
162
151
|
|
|
163
152
|
## How we work
|
|
164
|
-
|
|
165
|
-
-
|
|
166
|
-
-
|
|
167
|
-
-
|
|
168
|
-
|
|
169
|
-
##
|
|
170
|
-
|
|
171
|
-
-
|
|
172
|
-
- Ask:
|
|
173
|
-
- Ship:
|
|
153
|
+
|
|
154
|
+
- Add team norms.
|
|
155
|
+
- Add review expectations.
|
|
156
|
+
- Add recurring workflows.
|
|
157
|
+
|
|
158
|
+
## Useful links
|
|
159
|
+
|
|
160
|
+
- Add docs and repositories.
|
|
174
161
|
`,
|
|
175
162
|
};
|
|
176
|
-
export
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
163
|
+
export const INIT_TEMPLATES = Object.keys(TEMPLATES);
|
|
164
|
+
const CLI_COMMAND = "npx -y @floomhq/floom";
|
|
165
|
+
export async function init(filename, opts = {}) {
|
|
166
|
+
const target = filename ?? "skill";
|
|
167
|
+
const template = opts.template ?? "generic";
|
|
168
|
+
const folderMode = extname(target).toLowerCase() !== ".md";
|
|
169
|
+
const outputTarget = folderMode ? join(target, "SKILL.md") : target;
|
|
170
|
+
const folderPath = folderMode ? resolve(process.cwd(), target) : null;
|
|
171
|
+
const filePath = resolve(process.cwd(), outputTarget);
|
|
180
172
|
const exists = await fileExists(filePath);
|
|
181
173
|
if (exists) {
|
|
182
174
|
if (!process.stdin.isTTY) {
|
|
183
|
-
throw new FloomError(`${
|
|
175
|
+
throw new FloomError(`${outputTarget} already exists.`, "Re-run with a different filename, or delete it first.");
|
|
184
176
|
}
|
|
185
177
|
const rl = createInterface({ input, output });
|
|
186
|
-
const answer = (await rl.question(`${c.yellow("?")} ${
|
|
178
|
+
const answer = (await rl.question(`${c.yellow("?")} ${outputTarget} already exists. Overwrite? ${c.dim("(y/N)")} `)).trim().toLowerCase();
|
|
187
179
|
rl.close();
|
|
188
180
|
if (answer !== "y" && answer !== "yes") {
|
|
189
181
|
process.stdout.write(`\n${c.dim("Cancelled. Nothing was written.")}\n\n`);
|
|
@@ -191,7 +183,9 @@ export async function init(filename, template = "generic") {
|
|
|
191
183
|
}
|
|
192
184
|
}
|
|
193
185
|
try {
|
|
194
|
-
|
|
186
|
+
if (folderPath)
|
|
187
|
+
await mkdir(folderPath, { recursive: true });
|
|
188
|
+
await writeFile(filePath, TEMPLATES[template], "utf8");
|
|
195
189
|
}
|
|
196
190
|
catch (err) {
|
|
197
191
|
const code = err.code;
|
|
@@ -201,14 +195,15 @@ export async function init(filename, template = "generic") {
|
|
|
201
195
|
if (code === "EISDIR") {
|
|
202
196
|
throw new FloomError(`That's a directory, not a file: ${target}`);
|
|
203
197
|
}
|
|
204
|
-
throw new FloomError(`Couldn't create ${
|
|
198
|
+
throw new FloomError(`Couldn't create ${outputTarget}: ${err.message}`);
|
|
205
199
|
}
|
|
206
|
-
process.stdout.write(`\n${symbols.ok} Created ${c.bold(basename(filePath))}\n`);
|
|
207
|
-
|
|
200
|
+
process.stdout.write(`\n${symbols.ok} Created ${c.bold(folderMode ? outputTarget : basename(filePath))}\n`);
|
|
201
|
+
if (template !== "generic")
|
|
202
|
+
process.stdout.write(` ${c.dim(`Template: ${template}`)}\n`);
|
|
208
203
|
process.stdout.write(`\n ${c.bold("Next")}\n`);
|
|
209
204
|
process.stdout.write(` ${c.dim("1.")} Fill in the title, description, and instructions.\n`);
|
|
210
|
-
process.stdout.write(` ${c.dim("2.")} Check it: ${c.cyan(
|
|
211
|
-
process.stdout.write(` ${c.dim("3.")} Publish: ${c.cyan(
|
|
205
|
+
process.stdout.write(` ${c.dim("2.")} Check it: ${c.cyan(`${CLI_COMMAND} scan ${shellQuote(folderMode ? target : outputTarget)}`)}\n`);
|
|
206
|
+
process.stdout.write(` ${c.dim("3.")} Publish: ${c.cyan(`${CLI_COMMAND} publish ${shellQuote(folderMode ? target : outputTarget)} --type instruction --public`)}\n\n`);
|
|
212
207
|
}
|
|
213
208
|
function shellQuote(value) {
|
|
214
209
|
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
|