@floomhq/floom 1.0.64 → 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/sync.js DELETED
@@ -1,581 +0,0 @@
1
- import { constants } from "node:fs";
2
- import { lstat, mkdir, open, rm, rmdir } from "node:fs/promises";
3
- import { createHash } from "node:crypto";
4
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
- import ora from "ora";
6
- import { readConfig, resolveApiUrl } from "./config.js";
7
- import { getJson } from "./lib/api.js";
8
- import { c, symbols } from "./ui.js";
9
- import { FloomError } from "./errors.js";
10
- import { ensureSyncManifestDir, manifestKey, markSynced, readSyncManifest, unmarkSynced, withSyncLock, writeSyncManifest } from "./sync-manifest.js";
11
- import { normalizeRemotePackageFiles } from "./package.js";
12
- import { targetSkillsDir } from "./targets.js";
13
- const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
14
- const PATH_SEGMENT_RE = /^[a-z0-9._-]{1,128}$/;
15
- const MANIFEST_SEGMENT_RE = /^[A-Za-z0-9._-]{1,128}$/;
16
- const FD_PATH_ROOT = "/proc/self/fd";
17
- const PACKAGE_SYNC_PAGE_LIMIT = "25";
18
- function sha256(input) {
19
- return createHash("sha256").update(input).digest("hex");
20
- }
21
- async function localState(path) {
22
- try {
23
- const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
24
- try {
25
- const stat = await handle.stat();
26
- if (!stat.isFile()) {
27
- return { kind: "conflict", reason: "path is blocked by an existing local file or directory" };
28
- }
29
- return { kind: "file", hash: sha256(await handle.readFile()) };
30
- }
31
- finally {
32
- await handle.close();
33
- }
34
- }
35
- catch (err) {
36
- const code = err.code;
37
- if (code === "ENOENT")
38
- return { kind: "missing" };
39
- if (code === "ELOOP")
40
- return { kind: "conflict", reason: "path is a symbolic link" };
41
- if (code === "ENOTDIR" || code === "EISDIR") {
42
- return { kind: "conflict", reason: "path is blocked by an existing local file or directory" };
43
- }
44
- throw err;
45
- }
46
- }
47
- function safePathSegments(value, label) {
48
- if (!value)
49
- return [];
50
- if (isAbsolute(value))
51
- throw new FloomError(`Invalid ${label}.`);
52
- const segments = value.split(/[\\/]+/).filter(Boolean);
53
- if (segments.some((segment) => segment === "." || segment === ".." || !PATH_SEGMENT_RE.test(segment))) {
54
- throw new FloomError(`Invalid ${label}.`);
55
- }
56
- return segments;
57
- }
58
- function skillPath(skill, targetAgent) {
59
- if (!SLUG_RE.test(skill.slug))
60
- throw new FloomError(`Invalid skill slug: ${skill.slug}`);
61
- const root = targetSkillsDir(targetAgent);
62
- const segments = [root];
63
- segments.push(...safePathSegments(skill.library_slug, "library slug"));
64
- segments.push(...safePathSegments(skill.folder, "folder"));
65
- segments.push(skill.slug, "SKILL.md");
66
- const target = join(...segments);
67
- const relativeTarget = relative(resolve(root), resolve(target));
68
- if (relativeTarget === ".." || relativeTarget.startsWith(`..${sep}`) || isAbsolute(relativeTarget)) {
69
- throw new FloomError("Invalid skill target path.");
70
- }
71
- return target;
72
- }
73
- function syncKey(skill) {
74
- return `${skill.library_slug ?? ""}\0${skill.folder ?? ""}\0${skill.slug}`;
75
- }
76
- function validateSyncSkillShape(skill) {
77
- if (!skill || typeof skill !== "object")
78
- throw new FloomError("Invalid sync response.");
79
- const candidate = skill;
80
- if (typeof candidate.slug !== "string" || typeof candidate.body_md !== "string") {
81
- throw new FloomError("Invalid sync response.");
82
- }
83
- if (candidate.folder !== undefined &&
84
- candidate.folder !== null &&
85
- typeof candidate.folder !== "string") {
86
- throw new FloomError("Invalid sync response.");
87
- }
88
- if (candidate.library_slug !== undefined &&
89
- candidate.library_slug !== null &&
90
- typeof candidate.library_slug !== "string") {
91
- throw new FloomError("Invalid sync response.");
92
- }
93
- }
94
- function targetFromManifestKey(root, key) {
95
- if (!key || isAbsolute(key) || key.includes("\\") || key.length > 512) {
96
- throw new FloomError("Invalid manifest target path.");
97
- }
98
- const segments = key.split("/");
99
- if (segments.some((segment) => segment === "." || segment === ".." || !MANIFEST_SEGMENT_RE.test(segment))) {
100
- throw new FloomError("Invalid manifest target path.");
101
- }
102
- const target = join(root, ...segments);
103
- const relativeTarget = relative(resolve(root), resolve(target));
104
- if (relativeTarget === ".." || relativeTarget.startsWith(`..${sep}`) || isAbsolute(relativeTarget)) {
105
- throw new FloomError("Invalid manifest target path.");
106
- }
107
- return target;
108
- }
109
- async function writeSyncedFile(root, target, body) {
110
- const parent = await openSafeParentDirectory(root, target, true);
111
- let handle = null;
112
- try {
113
- handle = await open(childCreatePath(parent, dirname(target), basename(target)), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
114
- await writeAll(handle, body);
115
- }
116
- finally {
117
- await handle?.close();
118
- await parent.close();
119
- }
120
- }
121
- async function openSafeParentDirectory(root, target, create) {
122
- if (create)
123
- await ensureSafeParentDirectory(root, target);
124
- else
125
- await assertSafeExistingParentDirectory(root, target);
126
- return open(dirname(target), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
127
- }
128
- function childCreatePath(parent, fallbackParent, name) {
129
- if (process.platform === "linux")
130
- return `${FD_PATH_ROOT}/${parent.fd}/${name}`;
131
- return join(resolve(fallbackParent), name);
132
- }
133
- async function writeAll(handle, body) {
134
- const buffer = Buffer.isBuffer(body) ? body : Buffer.from(body, "utf8");
135
- let offset = 0;
136
- while (offset < buffer.length) {
137
- const result = await handle.write(buffer, offset, buffer.length - offset, offset);
138
- if (result.bytesWritten === 0)
139
- throw conflictError("failed to write local skill file", "EIO");
140
- offset += result.bytesWritten;
141
- }
142
- }
143
- function syncPackageFiles(target, body, files) {
144
- return [
145
- { target, bytes: body, hash: sha256(body) },
146
- ...files.map((file) => ({
147
- target: join(dirname(target), file.path),
148
- bytes: file.bytes,
149
- hash: file.sha256,
150
- })),
151
- ];
152
- }
153
- async function planPackageSync(root, files, manifest) {
154
- let missing = 0;
155
- let missingTracked = 0;
156
- let unchanged = 0;
157
- let remoteChanged = 0;
158
- let firstMissingTarget = null;
159
- for (const file of files) {
160
- const targetKey = manifestKey(root, file.target);
161
- const tracked = manifest.files[targetKey];
162
- try {
163
- await assertSafeExistingParentDirectory(root, file.target);
164
- }
165
- catch (err) {
166
- const code = err.code;
167
- if (code === "ELOOP")
168
- return { kind: "conflict", target: file.target, reason: "path contains a symbolic link" };
169
- if (code === "ENOTDIR" || code === "EISDIR")
170
- return { kind: "conflict", target: file.target, reason: "path is blocked by an existing local file or directory" };
171
- if (code === "EEXIST" || code === "ENOENT")
172
- return { kind: "conflict", target: file.target, reason: err instanceof Error ? err.message : "local file changed during Floom sync" };
173
- throw err;
174
- }
175
- const state = await localState(file.target);
176
- if (state.kind === "conflict")
177
- return { kind: "conflict", target: state.conflictTarget ?? file.target, reason: state.reason };
178
- if (state.kind === "missing") {
179
- firstMissingTarget ??= file.target;
180
- if (tracked) {
181
- missingTracked += 1;
182
- continue;
183
- }
184
- missing += 1;
185
- continue;
186
- }
187
- if (!tracked) {
188
- if (state.hash !== file.hash)
189
- return { kind: "conflict", target: file.target, reason: "existing file is not tracked by Floom sync" };
190
- unchanged += 1;
191
- continue;
192
- }
193
- if (state.hash !== tracked.hash)
194
- return { kind: "conflict", target: file.target, reason: "local file changed since the last Floom sync" };
195
- if (state.hash !== file.hash) {
196
- remoteChanged += 1;
197
- continue;
198
- }
199
- unchanged += 1;
200
- }
201
- if (unchanged === files.length)
202
- return { kind: "unchanged" };
203
- if (missing + missingTracked === files.length)
204
- return { kind: "write" };
205
- if (missingTracked > 0) {
206
- return {
207
- kind: "conflict",
208
- target: firstMissingTarget ?? files[0]?.target ?? root,
209
- reason: "local package is only partially installed",
210
- };
211
- }
212
- if (missing === 0 && remoteChanged > 0)
213
- return { kind: "update" };
214
- if (missing === files.length)
215
- return { kind: "write" };
216
- return { kind: "update" };
217
- }
218
- async function overwriteTrackedFile(root, target, body, expectedHash) {
219
- const parent = await openSafeParentDirectory(root, target, false);
220
- let handle = null;
221
- try {
222
- handle = await open(childCreatePath(parent, dirname(target), basename(target)), constants.O_RDWR | constants.O_NOFOLLOW);
223
- const stat = await handle.stat();
224
- if (!stat.isFile())
225
- throw conflictError("path is blocked by an existing local file or directory", "ENOTDIR");
226
- const currentHash = sha256(await handle.readFile());
227
- if (currentHash !== expectedHash)
228
- throw conflictError("local file changed since the last Floom sync", "EEXIST");
229
- await handle.truncate(0);
230
- await writeAll(handle, body);
231
- }
232
- finally {
233
- await handle?.close();
234
- await parent.close();
235
- }
236
- }
237
- async function deleteSyncedFile(root, target, expectedHash) {
238
- const state = await localState(target);
239
- if (state.kind === "missing")
240
- return;
241
- if (state.kind === "conflict")
242
- throw conflictError(state.reason, "EEXIST");
243
- if (state.hash !== expectedHash)
244
- throw conflictError("local file changed since the last Floom sync", "EEXIST");
245
- await rm(target);
246
- await pruneEmptyParents(root, dirname(target));
247
- }
248
- async function pruneEmptyParents(root, startDir) {
249
- const resolvedRoot = resolve(root);
250
- let current = resolve(startDir);
251
- for (;;) {
252
- const relativeCurrent = relative(resolvedRoot, current);
253
- if (!relativeCurrent || relativeCurrent === ".." || relativeCurrent.startsWith(`..${sep}`) || isAbsolute(relativeCurrent))
254
- return;
255
- try {
256
- await rmdir(current);
257
- }
258
- catch (err) {
259
- const code = err.code;
260
- if (code === "ENOENT") {
261
- current = dirname(current);
262
- continue;
263
- }
264
- if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOTDIR")
265
- return;
266
- throw err;
267
- }
268
- current = dirname(current);
269
- }
270
- }
271
- async function ensureSafeParentDirectory(root, target) {
272
- const resolvedRoot = resolve(root);
273
- const resolvedParent = resolve(dirname(target));
274
- const relativeParent = relative(resolvedRoot, resolvedParent);
275
- if (relativeParent === ".." || relativeParent.startsWith(`..${sep}`) || isAbsolute(relativeParent)) {
276
- throw conflictError("Invalid skill target path.", "EINVAL");
277
- }
278
- await mkdir(resolvedRoot, { recursive: true, mode: 0o700 });
279
- await assertSafeDirectory(resolvedRoot);
280
- if (!relativeParent || relativeParent === ".")
281
- return;
282
- let current = resolvedRoot;
283
- for (const segment of relativeParent.split(sep).filter(Boolean)) {
284
- current = join(current, segment);
285
- try {
286
- await assertSafeDirectory(current);
287
- }
288
- catch (err) {
289
- if (err.code !== "ENOENT")
290
- throw err;
291
- await mkdir(current, { mode: 0o700 });
292
- await assertSafeDirectory(current);
293
- }
294
- }
295
- }
296
- async function assertSafeExistingParentDirectory(root, target) {
297
- const resolvedRoot = resolve(root);
298
- const resolvedParent = resolve(dirname(target));
299
- const relativeParent = relative(resolvedRoot, resolvedParent);
300
- if (relativeParent === ".." || relativeParent.startsWith(`..${sep}`) || isAbsolute(relativeParent)) {
301
- throw conflictError("Invalid skill target path.", "EINVAL");
302
- }
303
- await assertSafeDirectory(resolvedRoot);
304
- if (!relativeParent || relativeParent === ".")
305
- return;
306
- let current = resolvedRoot;
307
- for (const segment of relativeParent.split(sep).filter(Boolean)) {
308
- current = join(current, segment);
309
- try {
310
- await assertSafeDirectory(current);
311
- }
312
- catch (err) {
313
- if (err.code === "ENOENT")
314
- return;
315
- throw err;
316
- }
317
- }
318
- }
319
- async function assertSafeDirectory(path) {
320
- const stat = await lstat(path);
321
- if (stat.isSymbolicLink())
322
- throw conflictError("path contains a symbolic link", "ELOOP");
323
- if (!stat.isDirectory()) {
324
- throw conflictError("path is blocked by an existing local file or directory", "ENOTDIR");
325
- }
326
- }
327
- function conflictError(message, code) {
328
- const err = new Error(message);
329
- err.code = code;
330
- return err;
331
- }
332
- export async function sync(opts = {}) {
333
- const targetAgent = opts.target ?? "claude";
334
- const cfg = await readConfig();
335
- if (!cfg)
336
- throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
337
- await ensureSyncManifestDir();
338
- const apiUrl = resolveApiUrl(cfg);
339
- const spinner = opts.spinner === false ? null : ora({ text: c.dim("Syncing skills..."), color: "yellow" }).start();
340
- let payload;
341
- try {
342
- payload = await loadSyncPayload(apiUrl, cfg.accessToken);
343
- }
344
- catch (err) {
345
- spinner?.stop();
346
- throw err;
347
- }
348
- try {
349
- return await withSyncLock(async () => {
350
- await mkdir(targetSkillsDir(targetAgent), { recursive: true, mode: 0o700 });
351
- if (!Array.isArray(payload.skills)) {
352
- throw new FloomError("Invalid sync response.");
353
- }
354
- for (const skill of payload.skills)
355
- validateSyncSkillShape(skill);
356
- // Version 1 preview syncs published, saved, and subscribed library skills.
357
- const all = payload.skills;
358
- const seen = new Set();
359
- let unchanged = 0;
360
- let updated = 0;
361
- let skipped = 0;
362
- let conflicts = 0;
363
- const conflictNotes = [];
364
- const manifest = await readSyncManifest();
365
- const root = targetSkillsDir(targetAgent);
366
- const activeTargetKeys = new Set();
367
- const pruneBlockedSlugs = new Set();
368
- let manifestChanged = false;
369
- let synced = 0;
370
- const noteConflict = (target, reason) => {
371
- conflicts += 1;
372
- const rel = manifestKey(root, target);
373
- conflictNotes.push(`${rel} (${reason})`);
374
- };
375
- const noteManifestConflict = (key, reason) => {
376
- conflicts += 1;
377
- conflictNotes.push(`${key} (${reason})`);
378
- };
379
- try {
380
- for (const skill of all) {
381
- const key = syncKey(skill);
382
- if (seen.has(key))
383
- continue;
384
- seen.add(key);
385
- if (!SLUG_RE.test(skill.slug)) {
386
- skipped += 1;
387
- continue;
388
- }
389
- let target;
390
- try {
391
- target = skillPath(skill, targetAgent);
392
- }
393
- catch (err) {
394
- if (err instanceof FloomError) {
395
- pruneBlockedSlugs.add(skill.slug);
396
- skipped += 1;
397
- continue;
398
- }
399
- throw err;
400
- }
401
- const remotePackageFiles = normalizeRemotePackageFiles(skill.package_files ?? skill.files);
402
- const packageFiles = syncPackageFiles(target, skill.body_md, remotePackageFiles);
403
- synced += 1;
404
- for (const file of packageFiles)
405
- activeTargetKeys.add(manifestKey(root, file.target));
406
- const plan = await planPackageSync(root, packageFiles, manifest);
407
- if (plan.kind === "conflict") {
408
- noteConflict(plan.target, plan.reason);
409
- continue;
410
- }
411
- if (plan.kind === "unchanged") {
412
- for (const file of packageFiles)
413
- markSynced(manifest, manifestKey(root, file.target), skill.slug, file.hash);
414
- manifestChanged = true;
415
- unchanged += 1;
416
- continue;
417
- }
418
- try {
419
- if (plan.kind === "update") {
420
- for (const file of packageFiles) {
421
- const targetKey = manifestKey(root, file.target);
422
- const tracked = manifest.files[targetKey];
423
- if (tracked) {
424
- await overwriteTrackedFile(root, file.target, file.bytes, tracked.hash);
425
- continue;
426
- }
427
- const state = await localState(file.target);
428
- if (state.kind === "missing") {
429
- await writeSyncedFile(root, file.target, file.bytes);
430
- continue;
431
- }
432
- if (state.kind === "conflict")
433
- throw conflictError(state.reason, "EEXIST");
434
- if (state.hash === file.hash)
435
- continue;
436
- throw conflictError("existing file is not tracked by Floom sync", "EEXIST");
437
- }
438
- }
439
- else {
440
- for (const file of packageFiles)
441
- await writeSyncedFile(root, file.target, file.bytes);
442
- }
443
- }
444
- catch (err) {
445
- const code = err.code;
446
- if (code === "ELOOP") {
447
- noteConflict(target, "path contains a symbolic link");
448
- continue;
449
- }
450
- if (code === "ENOTDIR" || code === "EISDIR") {
451
- noteConflict(target, "path is blocked by an existing local file or directory");
452
- continue;
453
- }
454
- throw err;
455
- }
456
- for (const file of packageFiles)
457
- markSynced(manifest, manifestKey(root, file.target), skill.slug, file.hash);
458
- manifestChanged = true;
459
- updated += 1;
460
- }
461
- if (payload.full_sync === true) {
462
- for (const [key, entry] of Object.entries(manifest.files)) {
463
- if (activeTargetKeys.has(key))
464
- continue;
465
- if (pruneBlockedSlugs.has(entry.slug)) {
466
- noteManifestConflict(key, "remote metadata is invalid for this skill");
467
- continue;
468
- }
469
- let target;
470
- try {
471
- target = targetFromManifestKey(root, key);
472
- await assertSafeExistingParentDirectory(root, target);
473
- }
474
- catch (err) {
475
- if (err instanceof FloomError) {
476
- noteManifestConflict(key, "invalid manifest target path");
477
- continue;
478
- }
479
- const code = err.code;
480
- if (code === "ELOOP") {
481
- noteManifestConflict(key, "path contains a symbolic link");
482
- continue;
483
- }
484
- if (code === "ENOTDIR" || code === "EISDIR") {
485
- noteManifestConflict(key, "path is blocked by an existing local file or directory");
486
- continue;
487
- }
488
- throw err;
489
- }
490
- const state = await localState(target);
491
- if (state.kind === "missing") {
492
- unmarkSynced(manifest, key);
493
- manifestChanged = true;
494
- continue;
495
- }
496
- if (state.kind === "conflict") {
497
- noteConflict(target, state.reason);
498
- continue;
499
- }
500
- if (state.hash !== entry.hash) {
501
- noteConflict(target, "local file changed since the last Floom sync");
502
- continue;
503
- }
504
- try {
505
- await deleteSyncedFile(root, target, entry.hash);
506
- }
507
- catch (err) {
508
- const code = err.code;
509
- if (code === "ENOENT") {
510
- unmarkSynced(manifest, key);
511
- manifestChanged = true;
512
- continue;
513
- }
514
- if (code === "ELOOP") {
515
- noteManifestConflict(key, "path contains a symbolic link");
516
- continue;
517
- }
518
- if (code === "ENOTDIR" || code === "EISDIR" || code === "EEXIST") {
519
- noteManifestConflict(key, err instanceof Error ? err.message : "local file changed since the last Floom sync");
520
- continue;
521
- }
522
- throw err;
523
- }
524
- unmarkSynced(manifest, key);
525
- manifestChanged = true;
526
- }
527
- }
528
- if (manifestChanged)
529
- await writeSyncManifest(manifest);
530
- }
531
- catch (err) {
532
- spinner?.stop();
533
- throw err;
534
- }
535
- spinner?.stop();
536
- const skippedNote = skipped > 0 ? c.dim(` (${skipped} skipped — invalid path)`) : "";
537
- const conflictNote = conflicts > 0 ? c.dim(`, ${conflicts} conflict${conflicts === 1 ? "" : "s"} skipped`) : "";
538
- const result = { synced, unchanged, updated, skipped, conflicts };
539
- if (!(opts.quietUnchanged && updated === 0 && skipped === 0 && conflicts === 0)) {
540
- for (const note of conflictNotes) {
541
- process.stderr.write(`${symbols.bullet} [floom] skipped local conflict: ${note}\n`);
542
- }
543
- if (conflicts > 0) {
544
- process.stderr.write(` ${c.dim("Move or delete the local file, then run `npx -y @floomhq/floom sync` again.")}\n`);
545
- }
546
- process.stdout.write(`\n${symbols.ok} [floom] synced ${synced} skills (${unchanged} unchanged, ${updated} updated${conflictNote})${skippedNote}\n\n`);
547
- }
548
- return result;
549
- });
550
- }
551
- catch (err) {
552
- spinner?.stop();
553
- throw err;
554
- }
555
- }
556
- async function loadSyncPayload(apiUrl, token) {
557
- const all = [];
558
- let fullSync = false;
559
- let cursor;
560
- const seenCursors = new Set();
561
- for (let page = 0; page < 1000; page += 1) {
562
- const url = new URL(`${apiUrl}/api/v1/me/skills`);
563
- url.searchParams.set("limit", PACKAGE_SYNC_PAGE_LIMIT);
564
- url.searchParams.set("packages", "1");
565
- if (cursor)
566
- url.searchParams.set("cursor", cursor);
567
- const payload = await getJson(url.toString(), "load your skills", token);
568
- if (!Array.isArray(payload.skills))
569
- throw new FloomError("Invalid sync response.");
570
- all.push(...payload.skills);
571
- fullSync = fullSync || payload.full_sync === true;
572
- if (!payload.next_cursor) {
573
- return { skills: all, full_sync: fullSync };
574
- }
575
- if (seenCursors.has(payload.next_cursor))
576
- throw new FloomError("Invalid sync response.");
577
- seenCursors.add(payload.next_cursor);
578
- cursor = payload.next_cursor;
579
- }
580
- throw new FloomError("Invalid sync response.");
581
- }
package/dist/targets.js DELETED
@@ -1,49 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { join } from "node:path";
3
- export const AGENT_TARGETS = ["claude", "codex", "cursor", "opencode", "kimi"];
4
- const TARGETS = {
5
- claude: {
6
- label: "Claude Code",
7
- skillsDirEnv: "CLAUDE_SKILLS_DIR",
8
- defaultSkillsDir: () => join(homedir(), ".claude", "skills"),
9
- },
10
- codex: {
11
- label: "Codex",
12
- skillsDirEnv: "CODEX_SKILLS_DIR",
13
- defaultSkillsDir: () => join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "skills"),
14
- },
15
- cursor: {
16
- label: "Cursor",
17
- skillsDirEnv: "CURSOR_SKILLS_DIR",
18
- defaultSkillsDir: () => join(homedir(), ".cursor", "skills-cursor"),
19
- },
20
- opencode: {
21
- label: "OpenCode",
22
- skillsDirEnv: "OPENCODE_SKILLS_DIR",
23
- defaultSkillsDir: () => join(homedir(), ".config", "opencode", "skills"),
24
- },
25
- kimi: {
26
- label: "Kimi",
27
- skillsDirEnv: "KIMI_SKILLS_DIR",
28
- defaultSkillsDir: () => join(homedir(), ".kimi", "skills"),
29
- },
30
- };
31
- export const TARGET_HINT = AGENT_TARGETS.join("|");
32
- export function isAgentTarget(value) {
33
- return AGENT_TARGETS.includes(value);
34
- }
35
- export function parseAgentTarget(value) {
36
- if (isAgentTarget(value))
37
- return value;
38
- throw new Error(`Invalid agent target: ${value}`);
39
- }
40
- export function targetLabel(target) {
41
- return TARGETS[target].label;
42
- }
43
- export function targetSkillsDir(target) {
44
- const info = TARGETS[target];
45
- return process.env[info.skillsDirEnv] ?? info.defaultSkillsDir();
46
- }
47
- export function targetSkillsDirEnv(target) {
48
- return TARGETS[target].skillsDirEnv;
49
- }
package/dist/ui.js DELETED
@@ -1,28 +0,0 @@
1
- import pc from "picocolors";
2
- // Cool, restrained terminal palette. Keep orange out of the default CLI surface.
3
- const isTty = process.stdout.isTTY === true;
4
- export const c = {
5
- coral: (s) => (isTty ? `\x1b[38;5;45m${s}\x1b[0m` : s),
6
- blue: (s) => (isTty ? `\x1b[38;5;75m${s}\x1b[0m` : s),
7
- teal: (s) => (isTty ? `\x1b[38;5;73m${s}\x1b[0m` : s),
8
- green: pc.green,
9
- red: pc.red,
10
- yellow: pc.yellow,
11
- dim: pc.dim,
12
- bold: pc.bold,
13
- cyan: pc.cyan,
14
- };
15
- export function wordmark() {
16
- // bold "floom" with a coral leading dot
17
- return `${c.coral("●")} ${c.bold("floom")}`;
18
- }
19
- export function header() {
20
- return `\n ${c.bold("floom")}\n ${c.dim("─────")}\n`;
21
- }
22
- export const symbols = {
23
- ok: c.green("✓"),
24
- fail: c.red("✗"),
25
- arrow: c.coral("→"),
26
- bullet: c.dim("○"),
27
- dot: c.coral("●"),
28
- };