@floomhq/floom-mcp-sync 1.0.10 → 1.0.12
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 -1
- package/dist/auto-sync.js +26 -9
- package/dist/lib/api.js +33 -15
- package/dist/server.js +16 -3
- package/package.json +8 -3
- package/dist/tools/install.js +0 -117
- package/dist/tools/libraries.js +0 -28
- package/dist/tools/publish.js +0 -24
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ Version 1 sync does not replace existing local files. Remote updates, existing u
|
|
|
59
59
|
|
|
60
60
|
The poll uses HTTP `If-None-Match` against `/api/v1/me/skills`, so unchanged responses are `304` with no body. If a Floom-tracked local file is missing after a `304`, MCP refetches once without the ETag so Version 1 can recreate missing files without overwriting existing files.
|
|
61
61
|
|
|
62
|
-
Configure the
|
|
62
|
+
Configure the poll interval with `FLOOM_SYNC_INTERVAL_MS` (default `60000`, minimum `10000`).
|
|
63
63
|
|
|
64
64
|
## Tools
|
|
65
65
|
|
package/dist/auto-sync.js
CHANGED
|
@@ -23,6 +23,17 @@ const PACKAGE_FILE_LIMIT = 100;
|
|
|
23
23
|
const PACKAGE_TOTAL_BYTES_LIMIT = 1_000_000;
|
|
24
24
|
const PACKAGE_FILE_BYTES_LIMIT = 500_000;
|
|
25
25
|
const BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
26
|
+
function hasStructuredPath(skill) {
|
|
27
|
+
return Boolean(skill.library_slug || skill.folder);
|
|
28
|
+
}
|
|
29
|
+
function dedupeSyncSkills(skills) {
|
|
30
|
+
const structuredSlugs = new Set();
|
|
31
|
+
for (const skill of skills) {
|
|
32
|
+
if (hasStructuredPath(skill))
|
|
33
|
+
structuredSlugs.add(skill.slug);
|
|
34
|
+
}
|
|
35
|
+
return skills.filter((skill) => !structuredSlugs.has(skill.slug) || hasStructuredPath(skill));
|
|
36
|
+
}
|
|
26
37
|
async function localState(path) {
|
|
27
38
|
try {
|
|
28
39
|
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
@@ -233,7 +244,7 @@ async function manifestHasMissingTrackedFile(manifest, root) {
|
|
|
233
244
|
}
|
|
234
245
|
return false;
|
|
235
246
|
}
|
|
236
|
-
export async function autoSync(log = (message) => process.stderr.write(`${message}\n`)) {
|
|
247
|
+
export async function autoSync(log = (message) => process.stderr.write(`${message}\n`), signal) {
|
|
237
248
|
const cfg = await readConfig();
|
|
238
249
|
if (!cfg) {
|
|
239
250
|
maybeAuthWarning(log, "[floom] not signed in; skipping sync (run `npx -y @floomhq/floom login`)");
|
|
@@ -246,7 +257,7 @@ export async function autoSync(log = (message) => process.stderr.write(`${messag
|
|
|
246
257
|
const apiUrl = apiUrlFromConfig(cfg);
|
|
247
258
|
let response;
|
|
248
259
|
try {
|
|
249
|
-
response = await getJsonWithEtag(`${apiUrl}/api/v1/me/skills`, cfg.accessToken, cachedEtag);
|
|
260
|
+
response = await getJsonWithEtag(`${apiUrl}/api/v1/me/skills`, cfg.accessToken, cachedEtag, signal);
|
|
250
261
|
}
|
|
251
262
|
catch (err) {
|
|
252
263
|
if (err instanceof FloomApiError && err.status === 401) {
|
|
@@ -257,7 +268,7 @@ export async function autoSync(log = (message) => process.stderr.write(`${messag
|
|
|
257
268
|
}
|
|
258
269
|
if (response.status === 304) {
|
|
259
270
|
if (await manifestHasMissingTrackedFile(manifest, root)) {
|
|
260
|
-
response = await getJsonWithEtag(`${apiUrl}/api/v1/me/skills`, cfg.accessToken, null);
|
|
271
|
+
response = await getJsonWithEtag(`${apiUrl}/api/v1/me/skills`, cfg.accessToken, null, signal);
|
|
261
272
|
}
|
|
262
273
|
else {
|
|
263
274
|
maybeHeartbeat(log);
|
|
@@ -280,9 +291,12 @@ export async function autoSync(log = (message) => process.stderr.write(`${messag
|
|
|
280
291
|
}
|
|
281
292
|
for (const skill of payload.skills)
|
|
282
293
|
validateSyncSkillShape(skill);
|
|
283
|
-
// Version 1
|
|
294
|
+
// Version 1 receives published, saved, and subscribed library skills through
|
|
295
|
+
// the single /me/skills response. If a slug appears both at top level and in
|
|
296
|
+
// a library/folder placement, keep only the structured placement so agents do
|
|
297
|
+
// not see duplicate native packages.
|
|
284
298
|
const buckets = [
|
|
285
|
-
{ skills: payload.skills, defaultLib: null },
|
|
299
|
+
{ skills: dedupeSyncSkills(payload.skills), defaultLib: null },
|
|
286
300
|
];
|
|
287
301
|
let unchanged = 0;
|
|
288
302
|
let updated = 0;
|
|
@@ -414,7 +428,7 @@ export async function autoSync(log = (message) => process.stderr.write(`${messag
|
|
|
414
428
|
if (updated > 0) {
|
|
415
429
|
// Activation telemetry counts syncs that write new content. Best-effort;
|
|
416
430
|
// never blocks or throws.
|
|
417
|
-
void emitSyncCompleted(apiUrl, cfg.accessToken, { total, updated, unchanged }).catch(() => { });
|
|
431
|
+
void emitSyncCompleted(apiUrl, cfg.accessToken, { total, updated, unchanged }, signal).catch(() => { });
|
|
418
432
|
}
|
|
419
433
|
}
|
|
420
434
|
else if (conflicts > 0) {
|
|
@@ -427,9 +441,9 @@ export async function autoSync(log = (message) => process.stderr.write(`${messag
|
|
|
427
441
|
return { synced: total, unchanged, updated, conflicts };
|
|
428
442
|
});
|
|
429
443
|
}
|
|
430
|
-
async function emitSyncCompleted(apiUrl, token, props) {
|
|
444
|
+
async function emitSyncCompleted(apiUrl, token, props, signal) {
|
|
431
445
|
try {
|
|
432
|
-
|
|
446
|
+
const init = {
|
|
433
447
|
method: "POST",
|
|
434
448
|
headers: {
|
|
435
449
|
"Content-Type": "application/json",
|
|
@@ -444,7 +458,10 @@ async function emitSyncCompleted(apiUrl, token, props) {
|
|
|
444
458
|
unchanged: props.unchanged,
|
|
445
459
|
},
|
|
446
460
|
}),
|
|
447
|
-
}
|
|
461
|
+
};
|
|
462
|
+
if (signal)
|
|
463
|
+
init.signal = signal;
|
|
464
|
+
await fetch(`${apiUrl}/api/v1/events`, init);
|
|
448
465
|
}
|
|
449
466
|
catch {
|
|
450
467
|
// never throw from telemetry
|
package/dist/lib/api.js
CHANGED
|
@@ -20,11 +20,14 @@ async function readError(res) {
|
|
|
20
20
|
}
|
|
21
21
|
return text;
|
|
22
22
|
}
|
|
23
|
-
export async function getJson(url, token) {
|
|
23
|
+
export async function getJson(url, token, signal) {
|
|
24
24
|
const headers = {};
|
|
25
25
|
if (token)
|
|
26
26
|
headers.authorization = `Bearer ${token}`;
|
|
27
|
-
const
|
|
27
|
+
const init = { headers };
|
|
28
|
+
if (signal)
|
|
29
|
+
init.signal = signal;
|
|
30
|
+
const res = await fetch(url, init);
|
|
28
31
|
if (!res.ok)
|
|
29
32
|
throw new FloomApiError(res.status, await readError(res));
|
|
30
33
|
return (await res.json());
|
|
@@ -33,11 +36,14 @@ export async function getJson(url, token) {
|
|
|
33
36
|
* GET helper that participates in HTTP conditional requests via ETag.
|
|
34
37
|
* Pass the previously-seen ETag (or null on first call). On 304, body is null.
|
|
35
38
|
*/
|
|
36
|
-
export async function getJsonWithEtag(url, token, etag) {
|
|
39
|
+
export async function getJsonWithEtag(url, token, etag, signal) {
|
|
37
40
|
const headers = { authorization: `Bearer ${token}` };
|
|
38
41
|
if (etag)
|
|
39
42
|
headers["if-none-match"] = etag;
|
|
40
|
-
const
|
|
43
|
+
const init = { headers };
|
|
44
|
+
if (signal)
|
|
45
|
+
init.signal = signal;
|
|
46
|
+
const res = await fetch(url, init);
|
|
41
47
|
const responseEtag = res.headers.get("etag");
|
|
42
48
|
if (res.status === 304) {
|
|
43
49
|
return { status: 304, body: null, etag: responseEtag ?? etag };
|
|
@@ -47,43 +53,55 @@ export async function getJsonWithEtag(url, token, etag) {
|
|
|
47
53
|
const body = (await res.json());
|
|
48
54
|
return { status: res.status, body, etag: responseEtag };
|
|
49
55
|
}
|
|
50
|
-
export async function getText(url) {
|
|
51
|
-
const
|
|
56
|
+
export async function getText(url, signal) {
|
|
57
|
+
const init = {};
|
|
58
|
+
if (signal)
|
|
59
|
+
init.signal = signal;
|
|
60
|
+
const res = await fetch(url, init);
|
|
52
61
|
if (!res.ok)
|
|
53
62
|
throw new FloomApiError(res.status, await readError(res));
|
|
54
63
|
return res.text();
|
|
55
64
|
}
|
|
56
|
-
export async function postJson(url, token, body) {
|
|
57
|
-
const
|
|
65
|
+
export async function postJson(url, token, body, signal) {
|
|
66
|
+
const init = {
|
|
58
67
|
method: "POST",
|
|
59
68
|
headers: {
|
|
60
69
|
authorization: `Bearer ${token}`,
|
|
61
70
|
"content-type": "application/json",
|
|
62
71
|
},
|
|
63
72
|
body: JSON.stringify(body),
|
|
64
|
-
}
|
|
73
|
+
};
|
|
74
|
+
if (signal)
|
|
75
|
+
init.signal = signal;
|
|
76
|
+
const res = await fetch(url, init);
|
|
65
77
|
if (!res.ok)
|
|
66
78
|
throw new FloomApiError(res.status, await readError(res));
|
|
67
79
|
return (await res.json());
|
|
68
80
|
}
|
|
69
|
-
export async function putJson(url, token, body) {
|
|
70
|
-
const
|
|
81
|
+
export async function putJson(url, token, body, signal) {
|
|
82
|
+
const init = {
|
|
71
83
|
method: "PUT",
|
|
72
84
|
headers: {
|
|
73
85
|
authorization: `Bearer ${token}`,
|
|
74
86
|
"content-type": "application/json",
|
|
75
87
|
},
|
|
76
88
|
body: JSON.stringify(body),
|
|
77
|
-
}
|
|
89
|
+
};
|
|
90
|
+
if (signal)
|
|
91
|
+
init.signal = signal;
|
|
92
|
+
const res = await fetch(url, init);
|
|
78
93
|
if (!res.ok)
|
|
79
94
|
throw new FloomApiError(res.status, await readError(res));
|
|
80
95
|
return (await res.json());
|
|
81
96
|
}
|
|
82
|
-
export async function deleteRequest(url, token) {
|
|
83
|
-
const
|
|
97
|
+
export async function deleteRequest(url, token, signal) {
|
|
98
|
+
const init = {
|
|
84
99
|
method: "DELETE",
|
|
85
100
|
headers: { authorization: `Bearer ${token}` },
|
|
86
|
-
}
|
|
101
|
+
};
|
|
102
|
+
if (signal)
|
|
103
|
+
init.signal = signal;
|
|
104
|
+
const res = await fetch(url, init);
|
|
87
105
|
if (!res.ok)
|
|
88
106
|
throw new FloomApiError(res.status, await readError(res));
|
|
89
107
|
return (await res.json());
|
package/dist/server.js
CHANGED
|
@@ -5,21 +5,32 @@ import { autoSync } from "./auto-sync.js";
|
|
|
5
5
|
import { getSkill } from "./tools/get.js";
|
|
6
6
|
import { searchSkills } from "./tools/search.js";
|
|
7
7
|
import { syncStatus } from "./tools/status.js";
|
|
8
|
-
const SERVER_VERSION = "1.0.
|
|
8
|
+
const SERVER_VERSION = "1.0.12";
|
|
9
9
|
const DEFAULT_INTERVAL_MS = 60_000;
|
|
10
10
|
const MIN_INTERVAL_MS = 10_000;
|
|
11
11
|
const SEARCH_TYPES = new Set(["knowledge", "instruction", "workflow", "skill"]);
|
|
12
12
|
let syncInFlight = null;
|
|
13
|
+
let syncAbortController = null;
|
|
13
14
|
function runAutoSync() {
|
|
14
|
-
|
|
15
|
+
if (syncInFlight)
|
|
16
|
+
return syncInFlight;
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
syncAbortController = controller;
|
|
19
|
+
syncInFlight = autoSync(undefined, controller.signal).finally(() => {
|
|
20
|
+
if (syncAbortController === controller)
|
|
21
|
+
syncAbortController = null;
|
|
15
22
|
syncInFlight = null;
|
|
16
23
|
});
|
|
17
24
|
return syncInFlight;
|
|
18
25
|
}
|
|
26
|
+
function abortAutoSync() {
|
|
27
|
+
syncAbortController?.abort();
|
|
28
|
+
syncAbortController = null;
|
|
29
|
+
}
|
|
19
30
|
function usage() {
|
|
20
31
|
return `
|
|
21
32
|
floom-mcp-sync v${SERVER_VERSION}
|
|
22
|
-
Floom MCP server for Version 1
|
|
33
|
+
Floom MCP server for Version 1 sync.
|
|
23
34
|
|
|
24
35
|
Usage
|
|
25
36
|
floom-mcp-sync
|
|
@@ -267,6 +278,7 @@ async function main() {
|
|
|
267
278
|
const pollHandle = startPolling(intervalMs, syncState);
|
|
268
279
|
const shutdown = (signal) => {
|
|
269
280
|
clearInterval(pollHandle);
|
|
281
|
+
abortAutoSync();
|
|
270
282
|
process.stderr.write(`[floom] received ${signal}, stopping sync poller\n`);
|
|
271
283
|
process.exit(0);
|
|
272
284
|
};
|
|
@@ -292,6 +304,7 @@ async function main() {
|
|
|
292
304
|
}
|
|
293
305
|
finally {
|
|
294
306
|
clearInterval(pollHandle);
|
|
307
|
+
abortAutoSync();
|
|
295
308
|
process.stderr.write("[floom] stdin closed, stopping sync poller\n");
|
|
296
309
|
}
|
|
297
310
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@floomhq/floom-mcp-sync",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Lightweight Floom MCP server for
|
|
3
|
+
"version": "1.0.12",
|
|
4
|
+
"description": "Lightweight Floom MCP server for search, on-demand skill fetch, status, and sync.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"bin",
|
|
12
|
-
"dist",
|
|
12
|
+
"dist/auto-sync.js",
|
|
13
|
+
"dist/lib",
|
|
14
|
+
"dist/server.js",
|
|
15
|
+
"dist/tools/get.js",
|
|
16
|
+
"dist/tools/search.js",
|
|
17
|
+
"dist/tools/status.js",
|
|
13
18
|
"README.md",
|
|
14
19
|
"LICENSE"
|
|
15
20
|
],
|
package/dist/tools/install.js
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { constants } from "node:fs";
|
|
2
|
-
import { lstat, mkdir, open } from "node:fs/promises";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { apiUrlFromConfig, readConfig, DEFAULT_API_URL } from "../lib/config.js";
|
|
6
|
-
import { getText } from "../lib/api.js";
|
|
7
|
-
import { assertValidSlug } from "../lib/slug.js";
|
|
8
|
-
import { skillPath, skillsDir } from "../lib/paths.js";
|
|
9
|
-
const FD_PATH_ROOT = "/proc/self/fd";
|
|
10
|
-
export async function installSkill(slug) {
|
|
11
|
-
assertValidSlug(slug);
|
|
12
|
-
const cfg = await readConfig();
|
|
13
|
-
const apiUrl = cfg ? apiUrlFromConfig(cfg) : (process.env.FLOOM_API_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
14
|
-
const body = await getText(`${apiUrl}/s/${slug}.md`);
|
|
15
|
-
if (typeof body !== "string")
|
|
16
|
-
throw new Error("Invalid skill response");
|
|
17
|
-
const target = skillPath(slug);
|
|
18
|
-
const remoteHash = sha256(body);
|
|
19
|
-
const existing = await localHash(target);
|
|
20
|
-
if (existing !== null && existing !== remoteHash) {
|
|
21
|
-
throw new Error("Local skill already exists with different content");
|
|
22
|
-
}
|
|
23
|
-
if (existing === null)
|
|
24
|
-
await writeInstallFile(target, body);
|
|
25
|
-
return { slug, path: target, bytes: Buffer.byteLength(body) };
|
|
26
|
-
}
|
|
27
|
-
function sha256(input) {
|
|
28
|
-
return createHash("sha256").update(input).digest("hex");
|
|
29
|
-
}
|
|
30
|
-
async function localHash(path) {
|
|
31
|
-
try {
|
|
32
|
-
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
33
|
-
try {
|
|
34
|
-
const stat = await handle.stat();
|
|
35
|
-
if (!stat.isFile())
|
|
36
|
-
throw new Error("path is blocked by an existing local file or directory");
|
|
37
|
-
return sha256(await handle.readFile("utf8"));
|
|
38
|
-
}
|
|
39
|
-
finally {
|
|
40
|
-
await handle.close();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
catch (err) {
|
|
44
|
-
if (err.code === "ENOENT")
|
|
45
|
-
return null;
|
|
46
|
-
throw err;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
async function writeInstallFile(target, body) {
|
|
50
|
-
const parent = await openSafeParentDirectory(skillsDir(), target);
|
|
51
|
-
let handle = null;
|
|
52
|
-
try {
|
|
53
|
-
handle = await open(childCreatePath(parent, dirname(target), basename(target)), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
54
|
-
await writeAll(handle, body);
|
|
55
|
-
}
|
|
56
|
-
finally {
|
|
57
|
-
await handle?.close();
|
|
58
|
-
await parent.close();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
async function openSafeParentDirectory(root, target) {
|
|
62
|
-
await ensureSafeParentDirectory(root, target);
|
|
63
|
-
return open(dirname(target), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
64
|
-
}
|
|
65
|
-
function childCreatePath(parent, fallbackParent, name) {
|
|
66
|
-
if (process.platform === "linux")
|
|
67
|
-
return `${FD_PATH_ROOT}/${parent.fd}/${name}`;
|
|
68
|
-
return join(resolve(fallbackParent), name);
|
|
69
|
-
}
|
|
70
|
-
async function writeAll(handle, body) {
|
|
71
|
-
const buffer = Buffer.from(body, "utf8");
|
|
72
|
-
let offset = 0;
|
|
73
|
-
while (offset < buffer.length) {
|
|
74
|
-
const result = await handle.write(buffer, offset, buffer.length - offset, offset);
|
|
75
|
-
if (result.bytesWritten === 0)
|
|
76
|
-
throw new Error("failed to write local skill file");
|
|
77
|
-
offset += result.bytesWritten;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
async function ensureSafeParentDirectory(root, target) {
|
|
81
|
-
const resolvedRoot = resolve(root);
|
|
82
|
-
const resolvedParent = resolve(dirname(target));
|
|
83
|
-
const relativeParent = relative(resolvedRoot, resolvedParent);
|
|
84
|
-
if (relativeParent === ".." || relativeParent.startsWith(`..${sep}`) || isAbsolute(relativeParent)) {
|
|
85
|
-
throw new Error("Invalid skill target path");
|
|
86
|
-
}
|
|
87
|
-
await mkdir(resolvedRoot, { recursive: true, mode: 0o700 });
|
|
88
|
-
await assertSafeDirectory(resolvedRoot);
|
|
89
|
-
if (!relativeParent || relativeParent === ".")
|
|
90
|
-
return;
|
|
91
|
-
let current = resolvedRoot;
|
|
92
|
-
for (const segment of relativeParent.split(sep).filter(Boolean)) {
|
|
93
|
-
current = join(current, segment);
|
|
94
|
-
try {
|
|
95
|
-
await assertSafeDirectory(current);
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
98
|
-
if (err.code !== "ENOENT")
|
|
99
|
-
throw err;
|
|
100
|
-
await mkdir(current, { mode: 0o700 });
|
|
101
|
-
await assertSafeDirectory(current);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
async function assertSafeDirectory(path) {
|
|
106
|
-
const stat = await lstat(path);
|
|
107
|
-
if (stat.isSymbolicLink()) {
|
|
108
|
-
const err = new Error("path contains a symbolic link");
|
|
109
|
-
err.code = "ELOOP";
|
|
110
|
-
throw err;
|
|
111
|
-
}
|
|
112
|
-
if (!stat.isDirectory()) {
|
|
113
|
-
const err = new Error("path is blocked by an existing local file or directory");
|
|
114
|
-
err.code = "ENOTDIR";
|
|
115
|
-
throw err;
|
|
116
|
-
}
|
|
117
|
-
}
|
package/dist/tools/libraries.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { apiUrlFromConfig, readConfig, DEFAULT_API_URL } from "../lib/config.js";
|
|
2
|
-
import { deleteRequest, getJson, postJson, putJson } from "../lib/api.js";
|
|
3
|
-
export async function listLibraries() {
|
|
4
|
-
const cfg = await readConfig();
|
|
5
|
-
const apiUrl = cfg ? apiUrlFromConfig(cfg) : (process.env.FLOOM_API_URL ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
6
|
-
return await getJson(`${apiUrl}/api/v1/libraries`, cfg?.accessToken);
|
|
7
|
-
}
|
|
8
|
-
export async function subscribeLibrary(slug) {
|
|
9
|
-
const cfg = await readConfig();
|
|
10
|
-
if (!cfg)
|
|
11
|
-
throw new Error("Not signed in. Run `npx -y @floomhq/floom login` first.");
|
|
12
|
-
const apiUrl = apiUrlFromConfig(cfg);
|
|
13
|
-
return await postJson(`${apiUrl}/api/v1/me/subscriptions`, cfg.accessToken, { library_slug: slug });
|
|
14
|
-
}
|
|
15
|
-
export async function unsubscribeLibrary(slug) {
|
|
16
|
-
const cfg = await readConfig();
|
|
17
|
-
if (!cfg)
|
|
18
|
-
throw new Error("Not signed in. Run `npx -y @floomhq/floom login` first.");
|
|
19
|
-
const apiUrl = apiUrlFromConfig(cfg);
|
|
20
|
-
return await deleteRequest(`${apiUrl}/api/v1/me/subscriptions/${encodeURIComponent(slug)}`, cfg.accessToken);
|
|
21
|
-
}
|
|
22
|
-
export async function moveSkill(slug, folder, tags) {
|
|
23
|
-
const cfg = await readConfig();
|
|
24
|
-
if (!cfg)
|
|
25
|
-
throw new Error("Not signed in. Run `npx -y @floomhq/floom login` first.");
|
|
26
|
-
const apiUrl = apiUrlFromConfig(cfg);
|
|
27
|
-
return await putJson(`${apiUrl}/api/v1/me/skills/${encodeURIComponent(slug)}/override`, cfg.accessToken, { folder, tags });
|
|
28
|
-
}
|
package/dist/tools/publish.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { apiUrlFromConfig, readConfig } from "../lib/config.js";
|
|
2
|
-
import { postJson } from "../lib/api.js";
|
|
3
|
-
export async function publishSkill(name, content, description, visibility = "unlisted", assetType = "skill", installsAs = "claude_skill", version) {
|
|
4
|
-
const cfg = await readConfig();
|
|
5
|
-
if (!cfg)
|
|
6
|
-
throw new Error("Not signed in. Run `npx -y @floomhq/floom login` first.");
|
|
7
|
-
const apiUrl = apiUrlFromConfig(cfg);
|
|
8
|
-
const data = await postJson(`${apiUrl}/api/skills`, cfg.accessToken, {
|
|
9
|
-
title: name,
|
|
10
|
-
description: description ?? null,
|
|
11
|
-
body_md: content,
|
|
12
|
-
visibility,
|
|
13
|
-
asset_type: assetType,
|
|
14
|
-
source: "markdown",
|
|
15
|
-
installs_as: installsAs,
|
|
16
|
-
version: version ?? null,
|
|
17
|
-
published_via: "mcp",
|
|
18
|
-
});
|
|
19
|
-
return {
|
|
20
|
-
slug: data.slug,
|
|
21
|
-
url: data.url ?? `${apiUrl}/s/${data.slug}.md`,
|
|
22
|
-
...(data.version ? { version: data.version } : {}),
|
|
23
|
-
};
|
|
24
|
-
}
|