@floomhq/floom 1.0.49 → 1.0.51
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 +14 -7
- package/dist/audit.js +45 -2
- package/dist/cli.js +32 -2
- package/dist/daemon.js +33 -12
- package/dist/setup.js +1 -1
- package/dist/status.js +166 -0
- package/dist/sync.js +41 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ floom share my-skill --add teammate@example.com
|
|
|
12
12
|
floom search review
|
|
13
13
|
floom add awesome-skill --setup
|
|
14
14
|
floom setup --target claude --dry-run
|
|
15
|
+
floom daemon install --target all
|
|
16
|
+
floom status
|
|
15
17
|
floom list
|
|
16
18
|
floom library list
|
|
17
19
|
```
|
|
@@ -27,14 +29,19 @@ The package is designed for `npx -y @floomhq/floom ...`. Global installs expose
|
|
|
27
29
|
- `npx -y @floomhq/floom publish <path>` — upload a skill folder or Markdown file. Folder packages use `<slug>/SKILL.md` plus optional `references/`, `examples/`, `scripts/`, and `assets/`. Optional `--public` / `--private` / `--unlisted`, `--type knowledge|instruction|workflow|skill`, `--installs-as <target>`, and `--skill-version <label>`.
|
|
28
30
|
- `npx -y @floomhq/floom share <slug>` — email-share one of your skills. Optional `--add <email>`, `--remove <email>`, and `--list`.
|
|
29
31
|
- `npx -y @floomhq/floom list` — show your published skills. Optional `--json`.
|
|
30
|
-
- `npx -y @floomhq/floom add <url-or-slug>` — fetch a skill into
|
|
32
|
+
- `npx -y @floomhq/floom add <url-or-slug>` — fetch a skill into the selected agent skills root with supporting package files. Optional `--target claude|codex|cursor|opencode|kimi`, `--setup` to connect the agent, and `--force` to replace an existing local copy.
|
|
31
33
|
- `npx -y @floomhq/floom info <url-or-slug>` — show skill metadata. Optional `--json`.
|
|
32
34
|
- `npx -y @floomhq/floom search <query>` — search public skills and starter libraries. Optional `--library <slug>`, `--type knowledge|instruction|workflow|skill`, and `--json`.
|
|
33
|
-
- `npx -y @floomhq/floom setup` — add Floom usage guidance to
|
|
35
|
+
- `npx -y @floomhq/floom setup` — add Floom usage guidance to the selected harness instructions file. Optional `--target claude|codex|cursor|opencode|kimi`, `--dry-run`, `--yes`.
|
|
34
36
|
- `npx -y @floomhq/floom connect` — alias for setup.
|
|
35
37
|
- `npx -y @floomhq/floom mcp` — print MCP setup commands for supported agent CLIs.
|
|
36
|
-
- `npx -y @floomhq/floom sync` —
|
|
37
|
-
- `npx -y @floomhq/floom watch` —
|
|
38
|
+
- `npx -y @floomhq/floom sync` — pull your published, saved, and subscribed library skills into one target. Optional `--target claude|codex|cursor|opencode|kimi`.
|
|
39
|
+
- `npx -y @floomhq/floom watch` — run one target sync loop. Optional `--target claude|codex|cursor|opencode|kimi`, `--push`, `--no-yolo`, `--interval <seconds>`; minimum `10`.
|
|
40
|
+
- `npx -y @floomhq/floom daemon install --target all` — install always-on sync for Claude, Codex, Cursor, OpenCode, and Kimi.
|
|
41
|
+
- `npx -y @floomhq/floom daemon status --json` — show the latest daemon cycle and per-target result.
|
|
42
|
+
- `npx -y @floomhq/floom status --json` — reconcile cloud, cache, native projection, and daemon counts.
|
|
43
|
+
- `npx -y @floomhq/floom feedback --kind <kind> --message <text>` — send bounded agent feedback to Floom.
|
|
44
|
+
- `npx -y @floomhq/floom audit skills --json` — report malformed, duplicate, and suspicious owned skills.
|
|
38
45
|
- `npx -y @floomhq/floom library list` — list public starter libraries. Optional `--json`.
|
|
39
46
|
- `npx -y @floomhq/floom library create <slug> --name <name>` — create a personal or starter library. Optional `--public` / `--private` / `--unlisted`.
|
|
40
47
|
- `npx -y @floomhq/floom library add <library> <skill> [--folder <path>] [--tags a,b]` — add a skill to a library.
|
|
@@ -75,7 +82,7 @@ version: 0.1.0
|
|
|
75
82
|
|
|
76
83
|
Override the API host with `FLOOM_API_URL` (defaults to `https://floom.dev`).
|
|
77
84
|
|
|
78
|
-
`floom sync` and `floom
|
|
79
|
-
The manifest records hashes for files Floom previously wrote. Sync writes missing files
|
|
80
|
-
|
|
85
|
+
`floom sync`, `floom watch`, and `floom daemon` handle published, saved, and subscribed library skills. They store a machine-local manifest at `~/.floom/sync-manifest.json`.
|
|
86
|
+
The manifest records hashes for files Floom previously wrote. Sync writes missing files and applies cloud updates when the local file still matches the last Floom-tracked hash.
|
|
87
|
+
Existing untracked files and locally edited tracked files are preserved as conflicts.
|
|
81
88
|
Symlinks are never followed. To replace a local skill manually, run `floom add <url-or-slug> --force`.
|
package/dist/audit.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ora from "ora";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { readConfig, resolveApiUrl } from "./config.js";
|
|
3
|
-
import { getJson } from "./lib/api.js";
|
|
4
|
+
import { getJson, postJson } from "./lib/api.js";
|
|
4
5
|
import { FloomError } from "./errors.js";
|
|
5
6
|
import { c, symbols } from "./ui.js";
|
|
6
7
|
const FIXTURE_RE = /\b(?:cli lifecycle audit fixture|launch gate|temp skill|test fixture|audit fixture)\b/i;
|
|
@@ -99,6 +100,44 @@ async function loadOwnedSkills() {
|
|
|
99
100
|
}
|
|
100
101
|
return skills;
|
|
101
102
|
}
|
|
103
|
+
function archiveSlugsFromPlan(raw) {
|
|
104
|
+
const value = raw && typeof raw === "object" ? raw : {};
|
|
105
|
+
const plan = value.archive_plan;
|
|
106
|
+
if (!Array.isArray(plan))
|
|
107
|
+
throw new FloomError("Invalid archive plan.", "Pass a JSON file from `floom audit skills --fix-plan`.");
|
|
108
|
+
const slugs = plan.map((item) => {
|
|
109
|
+
if (typeof item === "string")
|
|
110
|
+
return item;
|
|
111
|
+
if (item && typeof item === "object" && typeof item.slug === "string") {
|
|
112
|
+
return item.slug;
|
|
113
|
+
}
|
|
114
|
+
return "";
|
|
115
|
+
}).filter(Boolean);
|
|
116
|
+
if (slugs.length === 0)
|
|
117
|
+
throw new FloomError("Archive plan has no slugs.");
|
|
118
|
+
return [...new Set(slugs)];
|
|
119
|
+
}
|
|
120
|
+
async function applyArchivePlan(planPath, yes, json) {
|
|
121
|
+
const cfg = await readConfig();
|
|
122
|
+
if (!cfg)
|
|
123
|
+
throw new FloomError("Not signed in.", "Run `npx -y @floomhq/floom login` first.");
|
|
124
|
+
const raw = JSON.parse(await readFile(planPath, "utf8"));
|
|
125
|
+
const slugs = archiveSlugsFromPlan(raw);
|
|
126
|
+
const payload = await postJson(`${resolveApiUrl(cfg)}/api/v1/me/skills/archive`, yes ? "archive skills" : "preview skill archive", cfg.accessToken, { slugs, dry_run: !yes });
|
|
127
|
+
if (json) {
|
|
128
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const action = payload.dry_run ? "Archive preview" : "Archive applied";
|
|
132
|
+
process.stdout.write(`\n${symbols.ok} ${action}: ${payload.matched.length}/${payload.requested} matched\n`);
|
|
133
|
+
if (payload.archived)
|
|
134
|
+
process.stdout.write(` archived: ${payload.archived.length}\n`);
|
|
135
|
+
if (payload.missing.length > 0)
|
|
136
|
+
process.stdout.write(` missing or not owned: ${payload.missing.join(", ")}\n`);
|
|
137
|
+
if (payload.dry_run)
|
|
138
|
+
process.stdout.write(` ${c.dim("Re-run with --yes to archive matched skills.")}\n`);
|
|
139
|
+
process.stdout.write("\n");
|
|
140
|
+
}
|
|
102
141
|
function duplicateGroups(skills) {
|
|
103
142
|
const byHash = new Map();
|
|
104
143
|
for (const skill of skills) {
|
|
@@ -135,7 +174,11 @@ function summarize(skills, findings) {
|
|
|
135
174
|
};
|
|
136
175
|
}
|
|
137
176
|
export async function auditSkills(opts) {
|
|
138
|
-
|
|
177
|
+
if (opts.archivePlan) {
|
|
178
|
+
await applyArchivePlan(opts.archivePlan, opts.yes, opts.json);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const spinner = opts.json || opts.fixPlan ? null : ora({ text: c.dim("Auditing skills..."), color: "yellow" }).start();
|
|
139
182
|
let skills;
|
|
140
183
|
try {
|
|
141
184
|
skills = await loadOwnedSkills();
|
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ import { scanSkill } from "./scan.js";
|
|
|
20
20
|
import { auditSkills } from "./audit.js";
|
|
21
21
|
import { share } from "./share.js";
|
|
22
22
|
import { feedback } from "./feedback.js";
|
|
23
|
+
import { status } from "./status.js";
|
|
23
24
|
import { libraryAddSkill, libraryCreate, libraryList, libraryRemoveSkill, librarySubscribe, libraryUnsubscribe, moveSkill, } from "./library.js";
|
|
24
25
|
import { c, symbols } from "./ui.js";
|
|
25
26
|
import { printError, FloomError } from "./errors.js";
|
|
@@ -114,6 +115,8 @@ function commandUsage() {
|
|
|
114
115
|
${c.dim(`Flags: --target ${TARGET_HINT} (default: claude)`)}
|
|
115
116
|
|
|
116
117
|
${c.dim("Advanced")}
|
|
118
|
+
${c.cyan("status")} Show cloud, cache, native projection, and daemon counts
|
|
119
|
+
${c.dim("Flags: --json")}
|
|
117
120
|
${c.cyan("library")} Create, browse, and subscribe to libraries
|
|
118
121
|
${c.dim("Alias: lib")}
|
|
119
122
|
${c.cyan("move")} ${c.dim("<slug> --folder <path>")} Place a saved skill in a relative folder
|
|
@@ -278,10 +281,14 @@ function auditUsage() {
|
|
|
278
281
|
${c.cyan(`${CLI_COMMAND} audit skills`)}
|
|
279
282
|
${c.cyan(`${CLI_COMMAND} audit skills --json`)}
|
|
280
283
|
${c.cyan(`${CLI_COMMAND} audit skills --fix-plan`)}
|
|
284
|
+
${c.cyan(`${CLI_COMMAND} audit skills --archive-plan plan.json`)} ${c.dim("(dry-run)")}
|
|
285
|
+
${c.cyan(`${CLI_COMMAND} audit skills --archive-plan plan.json --yes`)}
|
|
281
286
|
|
|
282
287
|
${c.bold("Flags")}
|
|
283
288
|
${c.cyan("--json")} Full machine-readable report
|
|
284
289
|
${c.cyan("--fix-plan")} Include archive recommendations, without applying them
|
|
290
|
+
${c.cyan("--archive-plan <file>")} Preview or apply a reviewed archive plan
|
|
291
|
+
${c.cyan("--yes")} Apply the archive plan instead of dry-run preview
|
|
285
292
|
`);
|
|
286
293
|
}
|
|
287
294
|
function feedbackUsage() {
|
|
@@ -718,17 +725,37 @@ function parseDoctorFlags(argv) {
|
|
|
718
725
|
}
|
|
719
726
|
return out;
|
|
720
727
|
}
|
|
728
|
+
function parseStatusFlags(argv) {
|
|
729
|
+
const out = { json: false };
|
|
730
|
+
for (const a of argv) {
|
|
731
|
+
if (a === "--json")
|
|
732
|
+
out.json = true;
|
|
733
|
+
else if (a.startsWith("--"))
|
|
734
|
+
throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} status --json\`.`);
|
|
735
|
+
else
|
|
736
|
+
throw new FloomError(`Unexpected argument: ${a}`, `Try \`${CLI_COMMAND} status --json\`.`);
|
|
737
|
+
}
|
|
738
|
+
return out;
|
|
739
|
+
}
|
|
721
740
|
function parseAuditFlags(argv) {
|
|
722
741
|
const [subcommand, ...rest] = argv;
|
|
723
742
|
if (subcommand !== "skills") {
|
|
724
743
|
throw new FloomError("Unknown audit command.", `Try \`${CLI_COMMAND} audit skills --json\`.`);
|
|
725
744
|
}
|
|
726
|
-
const out = { json: false, fixPlan: false };
|
|
727
|
-
for (
|
|
745
|
+
const out = { json: false, fixPlan: false, yes: false };
|
|
746
|
+
for (let i = 0; i < rest.length; i++) {
|
|
747
|
+
const a = rest[i] ?? "";
|
|
728
748
|
if (a === "--json")
|
|
729
749
|
out.json = true;
|
|
730
750
|
else if (a === "--fix-plan")
|
|
731
751
|
out.fixPlan = true;
|
|
752
|
+
else if (a === "--archive-plan" || a.startsWith("--archive-plan=")) {
|
|
753
|
+
const { value, nextIndex } = readFlagValue(rest, i, "--archive-plan");
|
|
754
|
+
out.archivePlan = value;
|
|
755
|
+
i = nextIndex;
|
|
756
|
+
}
|
|
757
|
+
else if (a === "--yes")
|
|
758
|
+
out.yes = true;
|
|
732
759
|
else if (a.startsWith("--"))
|
|
733
760
|
throw new FloomError(`Unknown flag: ${a}`, `Try \`${CLI_COMMAND} audit skills --json\`.`);
|
|
734
761
|
else
|
|
@@ -1182,6 +1209,9 @@ async function main() {
|
|
|
1182
1209
|
});
|
|
1183
1210
|
return;
|
|
1184
1211
|
}
|
|
1212
|
+
case "status":
|
|
1213
|
+
await status(parseStatusFlags(rest));
|
|
1214
|
+
return;
|
|
1185
1215
|
case "add":
|
|
1186
1216
|
case "install": {
|
|
1187
1217
|
const flags = parseAddArgs(rest);
|
package/dist/daemon.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { access, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { access, appendFile, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir, hostname, platform } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { CONFIG_DIR } from "./config.js";
|
|
@@ -17,9 +17,10 @@ function targetsFor(value) {
|
|
|
17
17
|
return value === "all" ? [...AGENT_TARGETS] : [value];
|
|
18
18
|
}
|
|
19
19
|
function cliInvocation() {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
const override = process.env.FLOOM_DAEMON_CLI_COMMAND?.trim();
|
|
21
|
+
if (override)
|
|
22
|
+
return override;
|
|
23
|
+
return `npx -y @floomhq/floom@${CLI_VERSION}`;
|
|
23
24
|
}
|
|
24
25
|
function shellQuote(value) {
|
|
25
26
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
@@ -57,12 +58,7 @@ function runCommand(args, extraEnv = {}) {
|
|
|
57
58
|
}
|
|
58
59
|
async function appendLog(message) {
|
|
59
60
|
await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
60
|
-
|
|
61
|
-
if (err.code === "ENOENT")
|
|
62
|
-
return "";
|
|
63
|
-
throw err;
|
|
64
|
-
});
|
|
65
|
-
await writeFile(LOG_PATH, `${existing}${message}`, { mode: 0o600 });
|
|
61
|
+
await appendFile(LOG_PATH, message, { mode: 0o600 });
|
|
66
62
|
}
|
|
67
63
|
async function writeStatus(status) {
|
|
68
64
|
await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
@@ -78,6 +74,30 @@ async function readStatus() {
|
|
|
78
74
|
throw err;
|
|
79
75
|
}
|
|
80
76
|
}
|
|
77
|
+
function pidIsAlive(pid) {
|
|
78
|
+
if (!pid || pid < 1)
|
|
79
|
+
return false;
|
|
80
|
+
try {
|
|
81
|
+
process.kill(pid, 0);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function statusIsLive(status) {
|
|
89
|
+
if (!status?.running)
|
|
90
|
+
return false;
|
|
91
|
+
if (!pidIsAlive(status.pid))
|
|
92
|
+
return false;
|
|
93
|
+
if (!status.next_run_at || !status.interval_seconds)
|
|
94
|
+
return true;
|
|
95
|
+
const nextRun = Date.parse(status.next_run_at);
|
|
96
|
+
if (!Number.isFinite(nextRun))
|
|
97
|
+
return false;
|
|
98
|
+
const graceMs = Math.max(status.interval_seconds * 2000, 120_000);
|
|
99
|
+
return nextRun + graceMs >= Date.now();
|
|
100
|
+
}
|
|
81
101
|
function parseSyncResult(output) {
|
|
82
102
|
const match = output.match(/synced\s+(\d+)\s+skills\s+\((\d+)\s+unchanged,\s+(\d+)\s+updated(?:,\s+(\d+)\s+conflicts?\s+skipped)?/i);
|
|
83
103
|
if (!match)
|
|
@@ -291,15 +311,16 @@ async function installDaemon(opts) {
|
|
|
291
311
|
}
|
|
292
312
|
async function showStatus(opts) {
|
|
293
313
|
const status = await readStatus();
|
|
314
|
+
const live = statusIsLive(status);
|
|
294
315
|
if (opts.json) {
|
|
295
|
-
process.stdout.write(`${JSON.stringify(status
|
|
316
|
+
process.stdout.write(`${JSON.stringify(status ? { ...status, running: live } : { running: false }, null, 2)}\n`);
|
|
296
317
|
return;
|
|
297
318
|
}
|
|
298
319
|
if (!status) {
|
|
299
320
|
process.stdout.write(`${symbols.bullet} Floom daemon has no status yet.\n`);
|
|
300
321
|
return;
|
|
301
322
|
}
|
|
302
|
-
process.stdout.write(`${symbols.ok} Floom daemon ${
|
|
323
|
+
process.stdout.write(`${live ? symbols.ok : symbols.bullet} Floom daemon ${live ? "status" : "last status"}\n`);
|
|
303
324
|
process.stdout.write(` ${c.dim("manager:")} ${status.manager}\n`);
|
|
304
325
|
process.stdout.write(` ${c.dim("targets:")} ${status.targets.join(", ")}\n`);
|
|
305
326
|
if (status.last_completed_at)
|
package/dist/setup.js
CHANGED
|
@@ -140,7 +140,7 @@ async function detectTarget(opts) {
|
|
|
140
140
|
return {
|
|
141
141
|
agent,
|
|
142
142
|
label: targetLabel(agent),
|
|
143
|
-
path: join(homedir(), ".kimi", "agents", "
|
|
143
|
+
path: join(homedir(), ".kimi", "agents", "floom-system.md"),
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
const existing = await findUp(INSTRUCTION_FILES[agent].filename);
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { open, readdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { CONFIG_DIR, readConfig, resolveApiUrl } from "./config.js";
|
|
5
|
+
import { getJson } from "./lib/api.js";
|
|
6
|
+
import { AGENT_TARGETS, targetSkillsDir } from "./targets.js";
|
|
7
|
+
import { c, symbols } from "./ui.js";
|
|
8
|
+
const MAX_PACKAGE_SCAN_DEPTH = 8;
|
|
9
|
+
async function readJsonFile(path) {
|
|
10
|
+
try {
|
|
11
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(await handle.readFile("utf8"));
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await handle.close();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
const code = err.code;
|
|
21
|
+
if (code === "ENOENT" || code === "ELOOP" || err instanceof SyntaxError)
|
|
22
|
+
return null;
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function countSkillPackages(root) {
|
|
27
|
+
let count = 0;
|
|
28
|
+
async function walk(dir, depth) {
|
|
29
|
+
if (depth > MAX_PACKAGE_SCAN_DEPTH)
|
|
30
|
+
return;
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (err.code === "ENOENT")
|
|
37
|
+
return;
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
if (entries.some((entry) => entry.isFile() && entry.name === "SKILL.md")) {
|
|
41
|
+
count += 1;
|
|
42
|
+
}
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (!entry.isDirectory() || entry.name.startsWith("."))
|
|
45
|
+
continue;
|
|
46
|
+
await walk(join(dir, entry.name), depth + 1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
await walk(root, 0);
|
|
50
|
+
return count;
|
|
51
|
+
}
|
|
52
|
+
function manifestCounts(manifest) {
|
|
53
|
+
const files = manifest?.files && typeof manifest.files === "object" ? Object.values(manifest.files) : [];
|
|
54
|
+
return {
|
|
55
|
+
files: files.length,
|
|
56
|
+
skills: new Set(files.map((entry) => entry.slug).filter((slug) => typeof slug === "string" && slug.length > 0)).size,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function countCloudVisible() {
|
|
60
|
+
const cfg = await readConfig();
|
|
61
|
+
if (!cfg)
|
|
62
|
+
return { signedIn: false, total: null };
|
|
63
|
+
const apiUrl = resolveApiUrl(cfg);
|
|
64
|
+
let cursor;
|
|
65
|
+
const seenCursors = new Set();
|
|
66
|
+
let total = 0;
|
|
67
|
+
for (let page = 0; page < 1000; page += 1) {
|
|
68
|
+
const url = new URL(`${apiUrl}/api/v1/me/skills`);
|
|
69
|
+
url.searchParams.set("limit", "100");
|
|
70
|
+
if (cursor)
|
|
71
|
+
url.searchParams.set("cursor", cursor);
|
|
72
|
+
const payload = await getJson(url.toString(), "load your skills", cfg.accessToken);
|
|
73
|
+
total += Array.isArray(payload.skills) ? payload.skills.length : 0;
|
|
74
|
+
if (!payload.next_cursor)
|
|
75
|
+
return { signedIn: true, total };
|
|
76
|
+
if (seenCursors.has(payload.next_cursor))
|
|
77
|
+
return { signedIn: true, total };
|
|
78
|
+
seenCursors.add(payload.next_cursor);
|
|
79
|
+
cursor = payload.next_cursor;
|
|
80
|
+
}
|
|
81
|
+
return { signedIn: true, total };
|
|
82
|
+
}
|
|
83
|
+
async function buildStatus() {
|
|
84
|
+
const cloud = await countCloudVisible();
|
|
85
|
+
const daemonRaw = await readJsonFile(join(CONFIG_DIR, "daemon-status.json"));
|
|
86
|
+
const daemonRunning = daemonIsLive(daemonRaw);
|
|
87
|
+
const targets = {};
|
|
88
|
+
for (const target of AGENT_TARGETS) {
|
|
89
|
+
const cacheRoot = join(CONFIG_DIR, "skill-cache", target);
|
|
90
|
+
const nativeRoot = targetSkillsDir(target);
|
|
91
|
+
const cacheManifest = manifestCounts(await readJsonFile(join(cacheRoot, ".floom-cli-sync-manifest.json")));
|
|
92
|
+
const nativeManifest = manifestCounts(await readJsonFile(join(CONFIG_DIR, "native-sync-manifests", `${target}.json`)));
|
|
93
|
+
targets[target] = {
|
|
94
|
+
cache_packages: await countSkillPackages(cacheRoot),
|
|
95
|
+
cache_manifest_files: cacheManifest.files,
|
|
96
|
+
cache_manifest_skills: cacheManifest.skills,
|
|
97
|
+
native_root: nativeRoot,
|
|
98
|
+
native_packages: await countSkillPackages(nativeRoot),
|
|
99
|
+
native_manifest_files: nativeManifest.files,
|
|
100
|
+
native_manifest_skills: nativeManifest.skills,
|
|
101
|
+
...(daemonRaw?.last_run?.[target] ? { daemon: daemonRaw.last_run[target] } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
cloud: {
|
|
106
|
+
signed_in: cloud.signedIn,
|
|
107
|
+
visible_total: cloud.total,
|
|
108
|
+
},
|
|
109
|
+
daemon: {
|
|
110
|
+
running: daemonRunning,
|
|
111
|
+
...(daemonRaw?.version ? { version: daemonRaw.version } : {}),
|
|
112
|
+
...(daemonRaw?.hostname ? { hostname: daemonRaw.hostname } : {}),
|
|
113
|
+
...(daemonRaw?.targets ? { targets: daemonRaw.targets } : {}),
|
|
114
|
+
...(daemonRaw?.last_completed_at ? { last_completed_at: daemonRaw.last_completed_at } : {}),
|
|
115
|
+
...(daemonRaw?.next_run_at ? { next_run_at: daemonRaw.next_run_at } : {}),
|
|
116
|
+
},
|
|
117
|
+
targets,
|
|
118
|
+
notes: [
|
|
119
|
+
"Floom cache is the synced local package store used by MCP/search/router flows.",
|
|
120
|
+
"Native harness roots stay small where the harness would otherwise preload thousands of skills.",
|
|
121
|
+
"Instruction files tell agents to use Floom search/MCP instead of enumerating every cached package.",
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function daemonIsLive(status) {
|
|
126
|
+
if (!status?.running)
|
|
127
|
+
return false;
|
|
128
|
+
if (!status.pid || status.pid < 1)
|
|
129
|
+
return false;
|
|
130
|
+
try {
|
|
131
|
+
process.kill(status.pid, 0);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
if (!status.next_run_at || !status.interval_seconds)
|
|
137
|
+
return true;
|
|
138
|
+
const nextRun = Date.parse(status.next_run_at);
|
|
139
|
+
if (!Number.isFinite(nextRun))
|
|
140
|
+
return false;
|
|
141
|
+
const graceMs = Math.max(status.interval_seconds * 2000, 120_000);
|
|
142
|
+
return nextRun + graceMs >= Date.now();
|
|
143
|
+
}
|
|
144
|
+
export async function status(opts) {
|
|
145
|
+
const payload = await buildStatus();
|
|
146
|
+
if (opts.json) {
|
|
147
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
process.stdout.write(`\n${symbols.dot} ${c.bold("Floom status")}\n\n`);
|
|
151
|
+
process.stdout.write(` ${c.dim("cloud signed in:")} ${payload.cloud.signed_in ? "yes" : "no"}\n`);
|
|
152
|
+
process.stdout.write(` ${c.dim("cloud visible skills:")} ${payload.cloud.visible_total ?? "unknown"}\n`);
|
|
153
|
+
process.stdout.write(` ${c.dim("daemon:")} ${payload.daemon.running ? "running" : "not running"}${payload.daemon.version ? ` (${payload.daemon.version})` : ""}\n`);
|
|
154
|
+
if (payload.daemon.last_completed_at)
|
|
155
|
+
process.stdout.write(` ${c.dim("last completed:")} ${payload.daemon.last_completed_at}\n`);
|
|
156
|
+
process.stdout.write("\n");
|
|
157
|
+
for (const target of AGENT_TARGETS) {
|
|
158
|
+
const row = payload.targets[target];
|
|
159
|
+
const daemon = row.daemon ? ` daemon=${row.daemon.ok ? "ok" : "error"} synced=${row.daemon.synced ?? "?"} conflicts=${row.daemon.conflicts ?? "?"}` : "";
|
|
160
|
+
process.stdout.write(` ${c.cyan(target.padEnd(8))} cache=${row.cache_packages} native=${row.native_packages} nativeTracked=${row.native_manifest_skills}${daemon}\n`);
|
|
161
|
+
}
|
|
162
|
+
process.stdout.write("\n");
|
|
163
|
+
for (const note of payload.notes)
|
|
164
|
+
process.stdout.write(` ${c.dim(`- ${note}`)}\n`);
|
|
165
|
+
process.stdout.write("\n");
|
|
166
|
+
}
|
package/dist/sync.js
CHANGED
|
@@ -153,6 +153,7 @@ function syncPackageFiles(target, body, files) {
|
|
|
153
153
|
async function planPackageSync(root, files, manifest) {
|
|
154
154
|
let missing = 0;
|
|
155
155
|
let unchanged = 0;
|
|
156
|
+
let remoteChanged = 0;
|
|
156
157
|
let firstMissingTarget = null;
|
|
157
158
|
for (const file of files) {
|
|
158
159
|
const targetKey = manifestKey(root, file.target);
|
|
@@ -186,12 +187,16 @@ async function planPackageSync(root, files, manifest) {
|
|
|
186
187
|
}
|
|
187
188
|
if (state.hash !== tracked.hash)
|
|
188
189
|
return { kind: "conflict", target: file.target, reason: "local file changed since the last Floom sync" };
|
|
189
|
-
if (state.hash !== file.hash)
|
|
190
|
-
|
|
190
|
+
if (state.hash !== file.hash) {
|
|
191
|
+
remoteChanged += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
191
194
|
unchanged += 1;
|
|
192
195
|
}
|
|
193
196
|
if (unchanged === files.length)
|
|
194
197
|
return { kind: "unchanged" };
|
|
198
|
+
if (missing === 0 && remoteChanged > 0)
|
|
199
|
+
return { kind: "update" };
|
|
195
200
|
if (missing === files.length)
|
|
196
201
|
return { kind: "write" };
|
|
197
202
|
return {
|
|
@@ -200,6 +205,25 @@ async function planPackageSync(root, files, manifest) {
|
|
|
200
205
|
reason: "local package is only partially installed",
|
|
201
206
|
};
|
|
202
207
|
}
|
|
208
|
+
async function overwriteTrackedFile(root, target, body, expectedHash) {
|
|
209
|
+
const parent = await openSafeParentDirectory(root, target, false);
|
|
210
|
+
let handle = null;
|
|
211
|
+
try {
|
|
212
|
+
handle = await open(childCreatePath(parent, dirname(target), basename(target)), constants.O_RDWR | constants.O_NOFOLLOW);
|
|
213
|
+
const stat = await handle.stat();
|
|
214
|
+
if (!stat.isFile())
|
|
215
|
+
throw conflictError("path is blocked by an existing local file or directory", "ENOTDIR");
|
|
216
|
+
const currentHash = sha256(await handle.readFile());
|
|
217
|
+
if (currentHash !== expectedHash)
|
|
218
|
+
throw conflictError("local file changed since the last Floom sync", "EEXIST");
|
|
219
|
+
await handle.truncate(0);
|
|
220
|
+
await writeAll(handle, body);
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
await handle?.close();
|
|
224
|
+
await parent.close();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
203
227
|
async function ensureSafeParentDirectory(root, target) {
|
|
204
228
|
const resolvedRoot = resolve(root);
|
|
205
229
|
const resolvedParent = resolve(dirname(target));
|
|
@@ -343,13 +367,24 @@ export async function sync(opts = {}) {
|
|
|
343
367
|
if (plan.kind === "unchanged") {
|
|
344
368
|
for (const file of packageFiles)
|
|
345
369
|
markSynced(manifest, manifestKey(root, file.target), skill.slug, file.hash);
|
|
346
|
-
|
|
370
|
+
manifestChanged = true;
|
|
347
371
|
unchanged += 1;
|
|
348
372
|
continue;
|
|
349
373
|
}
|
|
350
374
|
try {
|
|
351
|
-
|
|
352
|
-
|
|
375
|
+
if (plan.kind === "update") {
|
|
376
|
+
for (const file of packageFiles) {
|
|
377
|
+
const targetKey = manifestKey(root, file.target);
|
|
378
|
+
const tracked = manifest.files[targetKey];
|
|
379
|
+
if (!tracked)
|
|
380
|
+
throw conflictError("existing file is not tracked by Floom sync", "EEXIST");
|
|
381
|
+
await overwriteTrackedFile(root, file.target, file.bytes, tracked.hash);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
for (const file of packageFiles)
|
|
386
|
+
await writeSyncedFile(root, file.target, file.bytes);
|
|
387
|
+
}
|
|
353
388
|
}
|
|
354
389
|
catch (err) {
|
|
355
390
|
const code = err.code;
|
|
@@ -365,7 +400,7 @@ export async function sync(opts = {}) {
|
|
|
365
400
|
}
|
|
366
401
|
for (const file of packageFiles)
|
|
367
402
|
markSynced(manifest, manifestKey(root, file.target), skill.slug, file.hash);
|
|
368
|
-
|
|
403
|
+
manifestChanged = true;
|
|
369
404
|
updated += 1;
|
|
370
405
|
}
|
|
371
406
|
if (payload.full_sync === true) {
|