@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/init.js DELETED
@@ -1,221 +0,0 @@
1
- import { writeFile, access, mkdir } from "node:fs/promises";
2
- import { dirname, resolve, basename, extname, join } from "node:path";
3
- import { createInterface } from "node:readline/promises";
4
- import { stdin as input, stdout as output } from "node:process";
5
- import { c, symbols } from "./ui.js";
6
- import { FloomError } from "./errors.js";
7
- const TEMPLATES = {
8
- generic: `---
9
- title:
10
- description:
11
- version: 1.0
12
- ---
13
-
14
- # Goal
15
-
16
-
17
- # When to use
18
-
19
-
20
- # Inputs
21
-
22
-
23
- # Instructions
24
-
25
-
26
- # Output
27
-
28
-
29
- # Examples
30
- `,
31
- "brand-voice": `---
32
- title: Brand voice
33
- description: Keep agent writing aligned with our company voice.
34
- type: knowledge
35
- installs_as: memory
36
- version: 1.0
37
- ---
38
-
39
- # Brand Voice
40
-
41
- ## Use this when
42
-
43
- - Writing external copy
44
- - Editing founder posts
45
- - Drafting website, email, or support text
46
-
47
- ## Voice rules
48
-
49
- - Be clear and specific.
50
- - Use short paragraphs.
51
- - Avoid hype, filler, and vague claims.
52
-
53
- ## Company facts
54
-
55
- - Add product facts here.
56
- - Add banned phrases here.
57
- - Add preferred examples here.
58
- `,
59
- "pr-review": `---
60
- title: PR review
61
- description: Review pull requests for correctness, regressions, and missing tests.
62
- type: workflow
63
- installs_as: claude_skill
64
- version: 1.0
65
- ---
66
-
67
- # PR Review
68
-
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
80
-
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.
85
-
86
- ## Output
87
-
88
- List findings first, ordered by severity, with file and line references.
89
- `,
90
- sales: `---
91
- title: Sales research
92
- description: Prepare concise account research and outreach context.
93
- type: workflow
94
- installs_as: memory
95
- version: 1.0
96
- ---
97
-
98
- # Sales Research
99
-
100
- ## Goal
101
-
102
- Prepare useful context before contacting an account.
103
-
104
- ## Research checklist
105
-
106
- - Company description
107
- - Current priorities or trigger events
108
- - Relevant people
109
- - Likely pain
110
- - Specific reason to reach out
111
-
112
- ## Output
113
-
114
- Return a short account brief and a first-message angle.
115
- `,
116
- support: `---
117
- title: Support tone
118
- description: Keep support replies concise, helpful, and calm.
119
- type: instruction
120
- installs_as: memory
121
- version: 1.0
122
- ---
123
-
124
- # Support Tone
125
-
126
- ## Rules
127
-
128
- - Acknowledge the issue directly.
129
- - Give the next concrete step.
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.
136
- `,
137
- onboarding: `---
138
- title: Onboarding context
139
- description: Give agents the context needed to help new teammates.
140
- type: knowledge
141
- installs_as: memory
142
- version: 1.0
143
- ---
144
-
145
- # Onboarding Context
146
-
147
- ## Company
148
-
149
- - Add what the company does.
150
- - Add important product areas.
151
-
152
- ## How we work
153
-
154
- - Add team norms.
155
- - Add review expectations.
156
- - Add recurring workflows.
157
-
158
- ## Useful links
159
-
160
- - Add docs and repositories.
161
- `,
162
- };
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);
172
- const exists = await fileExists(filePath);
173
- if (exists) {
174
- if (!process.stdin.isTTY) {
175
- throw new FloomError(`${outputTarget} already exists.`, "Re-run with a different filename, or delete it first.");
176
- }
177
- const rl = createInterface({ input, output });
178
- const answer = (await rl.question(`${c.yellow("?")} ${outputTarget} already exists. Overwrite? ${c.dim("(y/N)")} `)).trim().toLowerCase();
179
- rl.close();
180
- if (answer !== "y" && answer !== "yes") {
181
- process.stdout.write(`\n${c.dim("Cancelled. Nothing was written.")}\n\n`);
182
- return;
183
- }
184
- }
185
- try {
186
- if (folderPath)
187
- await mkdir(folderPath, { recursive: true });
188
- await writeFile(filePath, TEMPLATES[template], "utf8");
189
- }
190
- catch (err) {
191
- const code = err.code;
192
- if (code === "ENOENT") {
193
- throw new FloomError(`Directory not found: ${dirname(target)}`, "Create the directory first, or choose a filename in the current directory.");
194
- }
195
- if (code === "EISDIR") {
196
- throw new FloomError(`That's a directory, not a file: ${target}`);
197
- }
198
- throw new FloomError(`Couldn't create ${outputTarget}: ${err.message}`);
199
- }
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`);
203
- process.stdout.write(`\n ${c.bold("Next")}\n`);
204
- process.stdout.write(` ${c.dim("1.")} Fill in the title, description, and instructions.\n`);
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`);
207
- }
208
- function shellQuote(value) {
209
- if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value))
210
- return value;
211
- return `'${value.replace(/'/g, "'\\''")}'`;
212
- }
213
- async function fileExists(p) {
214
- try {
215
- await access(p);
216
- return true;
217
- }
218
- catch {
219
- return false;
220
- }
221
- }
package/dist/install.js DELETED
@@ -1,305 +0,0 @@
1
- import { constants } from "node:fs";
2
- import { lstat, mkdir, open } from "node:fs/promises";
3
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
- import ora from "ora";
5
- import { readConfig, resolveApiUrl } from "./config.js";
6
- import { getJson } from "./lib/api.js";
7
- import { c, symbols } from "./ui.js";
8
- import { FloomError } from "./errors.js";
9
- import { normalizeRemotePackageFiles, packageHash, sha256Bytes } from "./package.js";
10
- import { manifestKey, markSynced, readSyncManifest, withSyncLock, writeSyncManifest } from "./sync-manifest.js";
11
- import { targetLabel, targetSkillsDir, targetSkillsDirEnv } from "./targets.js";
12
- const SLUG_RE = /^[A-Za-z0-9_-]{1,128}$/;
13
- const FD_PATH_ROOT = "/proc/self/fd";
14
- function slugFromInput(input) {
15
- const trimmed = input.trim();
16
- try {
17
- const url = new URL(trimmed);
18
- const last = url.pathname.split("/").filter(Boolean).at(-1) ?? "";
19
- return last.replace(/\.(md|json)$/i, "");
20
- }
21
- catch {
22
- return trimmed.replace(/\.(md|json)$/i, "");
23
- }
24
- }
25
- function skillPath(root, slug) {
26
- return join(root, slug, "SKILL.md");
27
- }
28
- function legacySkillPath(root, slug) {
29
- return join(root, `${slug}.md`);
30
- }
31
- function skillsDirHint(target) {
32
- return targetSkillsDirEnv(target);
33
- }
34
- function setupCommand(target) {
35
- return `npx -y @floomhq/floom setup --target ${target} --yes`;
36
- }
37
- async function readLocalFile(path) {
38
- try {
39
- const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
40
- try {
41
- const stat = await handle.stat();
42
- if (!stat.isFile())
43
- throw new FloomError("Local path is blocked by an existing file or directory.");
44
- return await handle.readFile();
45
- }
46
- finally {
47
- await handle.close();
48
- }
49
- }
50
- catch (err) {
51
- const code = err.code;
52
- if (code === "ENOENT")
53
- return null;
54
- if (code === "ELOOP")
55
- throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
56
- if (code === "ENOTDIR" || code === "EISDIR") {
57
- throw new FloomError("Local path is blocked by an existing file or directory.");
58
- }
59
- throw err;
60
- }
61
- }
62
- async function localPackageHash(root, slug, target, files) {
63
- const main = await readLocalFile(target);
64
- if (main === null) {
65
- const legacy = await readLocalFile(legacySkillPath(root, slug));
66
- if (legacy !== null && files.length === 0)
67
- return packageHash(legacy.toString("utf8"), []);
68
- return null;
69
- }
70
- const localFiles = [];
71
- for (const file of files) {
72
- const bytes = await readLocalFile(join(dirname(target), file.path));
73
- if (bytes === null)
74
- return null;
75
- localFiles.push({ path: file.path, bytes, sha256: file.sha256 });
76
- }
77
- return packageHash(main.toString("utf8"), localFiles);
78
- }
79
- async function markInstallSynced(root, slug, files) {
80
- const manifest = await readSyncManifest();
81
- for (const file of files) {
82
- markSynced(manifest, manifestKey(root, file.target), slug, file.hash);
83
- }
84
- await writeSyncManifest(manifest);
85
- }
86
- async function preflightInstallPackage(root, files, opts) {
87
- for (const file of files) {
88
- await ensureSafeParentDirectory(root, file.target);
89
- const existing = await readLocalFile(file.target);
90
- if (existing === null)
91
- continue;
92
- if (sha256Buffer(existing) === file.hash)
93
- continue;
94
- if (opts.force)
95
- continue;
96
- return file.target;
97
- }
98
- return null;
99
- }
100
- function sha256Buffer(input) {
101
- return sha256Bytes(input);
102
- }
103
- async function writeInstallFile(root, target, body) {
104
- const parent = await openSafeParentDirectory(root, target);
105
- let handle = null;
106
- try {
107
- handle = await open(childCreatePath(parent, dirname(target), basename(target)), constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
108
- await writeAll(handle, body);
109
- }
110
- finally {
111
- await handle?.close();
112
- await parent.close();
113
- }
114
- }
115
- async function overwriteInstallFile(root, target, body) {
116
- const parent = await openSafeParentDirectory(root, target);
117
- const handle = await open(childCreatePath(parent, dirname(target), basename(target)), constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW, 0o600);
118
- try {
119
- const stat = await handle.stat();
120
- if (!stat.isFile())
121
- throw new FloomError("Local path is blocked by an existing file or directory.");
122
- await writeAll(handle, body);
123
- }
124
- finally {
125
- await handle.close();
126
- await parent.close();
127
- }
128
- }
129
- async function openSafeParentDirectory(root, target) {
130
- await ensureSafeParentDirectory(root, target);
131
- return open(dirname(target), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
132
- }
133
- function childCreatePath(parent, fallbackParent, name) {
134
- if (process.platform === "linux")
135
- return `${FD_PATH_ROOT}/${parent.fd}/${name}`;
136
- return join(resolve(fallbackParent), name);
137
- }
138
- async function writeAll(handle, body) {
139
- const buffer = Buffer.isBuffer(body) ? body : Buffer.from(body, "utf8");
140
- let offset = 0;
141
- while (offset < buffer.length) {
142
- const result = await handle.write(buffer, offset, buffer.length - offset, offset);
143
- if (result.bytesWritten === 0)
144
- throw new FloomError("Failed to write local skill file.");
145
- offset += result.bytesWritten;
146
- }
147
- }
148
- async function ensureSafeParentDirectory(root, target) {
149
- const resolvedRoot = resolve(root);
150
- const resolvedParent = resolve(dirname(target));
151
- const relativeParent = relative(resolvedRoot, resolvedParent);
152
- if (relativeParent === ".." || relativeParent.startsWith(`..${sep}`) || isAbsolute(relativeParent)) {
153
- throw new FloomError("Invalid skill target path.");
154
- }
155
- try {
156
- await mkdir(resolvedRoot, { recursive: true, mode: 0o700 });
157
- }
158
- catch (err) {
159
- if (err.code === "EEXIST") {
160
- throw new FloomError("Skills directory points to a file, not a directory.", "Set the skills directory env var to a directory, or remove the file blocking it.");
161
- }
162
- throw err;
163
- }
164
- await assertSafeDirectory(resolvedRoot);
165
- if (!relativeParent || relativeParent === ".")
166
- return;
167
- let current = resolvedRoot;
168
- for (const segment of relativeParent.split(sep).filter(Boolean)) {
169
- current = join(current, segment);
170
- try {
171
- await assertSafeDirectory(current);
172
- }
173
- catch (err) {
174
- if (err.code !== "ENOENT")
175
- throw err;
176
- await mkdir(current, { mode: 0o700 });
177
- await assertSafeDirectory(current);
178
- }
179
- }
180
- }
181
- async function assertSafeDirectory(path) {
182
- const stat = await lstat(path);
183
- if (stat.isSymbolicLink()) {
184
- throw new FloomError("Local path contains a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
185
- }
186
- if (!stat.isDirectory()) {
187
- throw new FloomError("Local path is blocked by an existing file or directory.");
188
- }
189
- }
190
- export async function install(slugInput, opts = {}) {
191
- const targetAgent = opts.target ?? "claude";
192
- const root = targetSkillsDir(targetAgent);
193
- const slug = slugFromInput(slugInput);
194
- if (!SLUG_RE.test(slug)) {
195
- throw new FloomError(`Invalid skill slug: ${slugInput}`);
196
- }
197
- const cfg = await readConfig();
198
- const apiUrl = resolveApiUrl(cfg);
199
- const spinner = ora({ text: c.dim(`Adding ${slug}...`), color: "yellow" }).start();
200
- let detail;
201
- try {
202
- detail = await getJson(`${apiUrl}/api/v1/skills/${encodeURIComponent(slug)}`, "fetch skill", cfg?.accessToken);
203
- if (!detail || typeof detail.body_md !== "string") {
204
- throw new FloomError("Invalid skill response.");
205
- }
206
- }
207
- catch (err) {
208
- spinner.stop();
209
- throw err;
210
- }
211
- const target = skillPath(root, slug);
212
- const remotePackageFiles = normalizeRemotePackageFiles(detail.package_files ?? detail.files);
213
- const installFiles = [
214
- { target, bytes: detail.body_md, hash: sha256Bytes(detail.body_md) },
215
- ...remotePackageFiles.map((file) => ({
216
- target: join(dirname(target), file.path),
217
- bytes: file.bytes,
218
- hash: file.sha256,
219
- })),
220
- ];
221
- const remoteHash = packageHash(detail.body_md, remotePackageFiles);
222
- let action = "installed";
223
- let manifestWarning = null;
224
- await withSyncLock(async () => {
225
- try {
226
- await mkdir(root, { recursive: true, mode: 0o700 });
227
- }
228
- catch (err) {
229
- if (err.code === "EEXIST") {
230
- throw new FloomError(`${skillsDirHint(targetAgent)} points to a file, not a directory.`, `Set ${skillsDirHint(targetAgent)} to a directory, or remove the file blocking it.`);
231
- }
232
- throw err;
233
- }
234
- await ensureSafeParentDirectory(root, target);
235
- const existing = await localPackageHash(root, slug, target, remotePackageFiles);
236
- const conflictingTarget = await preflightInstallPackage(root, installFiles, opts.force ? { force: true } : {});
237
- if (conflictingTarget) {
238
- throw new FloomError("Local skill already exists with different content.", `Run \`npx -y @floomhq/floom add <link> --force\` to replace it, or move the local file first: ${relative(root, conflictingTarget).split(sep).join("/")}`);
239
- }
240
- if (existing === remoteHash) {
241
- action = "unchanged";
242
- }
243
- else if (opts.force) {
244
- try {
245
- await overwriteInstallFile(root, target, detail.body_md);
246
- for (const file of remotePackageFiles) {
247
- await overwriteInstallFile(root, join(dirname(target), file.path), file.bytes);
248
- }
249
- }
250
- catch (err) {
251
- const code = err.code;
252
- if (code === "ELOOP")
253
- throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
254
- throw err;
255
- }
256
- action = "updated";
257
- }
258
- else if (existing !== null) {
259
- throw new FloomError("Local skill already exists with different content.", "Run `npx -y @floomhq/floom add <link> --force` to replace it, or move the local file first.");
260
- }
261
- else {
262
- try {
263
- await writeInstallFile(root, target, detail.body_md);
264
- for (const file of remotePackageFiles) {
265
- await writeInstallFile(root, join(dirname(target), file.path), file.bytes);
266
- }
267
- }
268
- catch (err) {
269
- const code = err.code;
270
- if (code === "EEXIST") {
271
- throw new FloomError("Local skill already exists with different content.", "Move or delete the local file, then run `npx -y @floomhq/floom add` again.");
272
- }
273
- if (code === "ELOOP") {
274
- throw new FloomError("Local path is a symbolic link.", "Move or delete the local path, then run `npx -y @floomhq/floom add` again.");
275
- }
276
- if (code === "ENOENT") {
277
- throw new FloomError("Local path changed during install.", "Run `npx -y @floomhq/floom add` again.");
278
- }
279
- throw err;
280
- }
281
- action = "installed";
282
- }
283
- try {
284
- await markInstallSynced(root, slug, installFiles);
285
- }
286
- catch (err) {
287
- manifestWarning = err instanceof Error ? err.message : String(err);
288
- }
289
- });
290
- spinner.stop();
291
- process.stdout.write(`\n${symbols.ok} [floom] ${action} ${c.bold(slug)}\n`);
292
- process.stdout.write(` ${c.dim(dirname(target))}\n\n`);
293
- if (manifestWarning) {
294
- process.stdout.write(` ${c.yellow("!")} ${c.dim(`Installed, but sync tracking was not updated: ${manifestWarning}`)}\n\n`);
295
- }
296
- process.stdout.write(` ${c.bold("Next")}\n`);
297
- if (opts.setup) {
298
- process.stdout.write(` ${c.dim("1.")} Floom is connecting ${targetLabel(targetAgent)} now.\n`);
299
- process.stdout.write(` ${c.dim("2.")} Tell your agent to use ${c.bold(slug)} when it matches the task.\n\n`);
300
- }
301
- else {
302
- process.stdout.write(` ${c.dim("1.")} Tell your agent to use ${c.bold(slug)} when it matches the task.\n`);
303
- process.stdout.write(` ${c.dim("2.")} One-time setup: ${c.cyan(setupCommand(targetAgent))}\n\n`);
304
- }
305
- }
package/dist/launch.js DELETED
@@ -1,110 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { CONFIG_DIR, readConfig, resolveApiUrl } from "./config.js";
3
- import { floomFetch } from "./lib/api.js";
4
- import { CLI_VERSION } from "./version.js";
5
- import { c, symbols } from "./ui.js";
6
- import { FloomError } from "./errors.js";
7
- import { join } from "node:path";
8
- async function readDaemonStatus() {
9
- try {
10
- return JSON.parse(await readFile(join(CONFIG_DIR, "daemon-status.json"), "utf8"));
11
- }
12
- catch (err) {
13
- if (err.code === "ENOENT")
14
- return null;
15
- throw err;
16
- }
17
- }
18
- async function getJson(url, action, token) {
19
- try {
20
- const res = await floomFetch(url, action, {
21
- ...(token ? { token } : {}),
22
- timeoutMs: 8_000,
23
- rateLimitRetries: 0,
24
- });
25
- return (await res.json());
26
- }
27
- catch {
28
- return null;
29
- }
30
- }
31
- export async function launchGate(opts) {
32
- const cfg = await readConfig();
33
- const apiUrl = resolveApiUrl(cfg ?? undefined);
34
- const [health, cliVersion, daemon] = await Promise.all([
35
- getJson(`${apiUrl}/api/v1/health`, "check launch health", cfg?.accessToken),
36
- getJson(`${apiUrl}/api/v1/cli-version`, "check CLI version", cfg?.accessToken),
37
- readDaemonStatus(),
38
- ]);
39
- const targetResults = daemon?.last_run ?? {};
40
- const targets = daemon?.targets ?? [];
41
- const daemonLive = daemonIsLive(daemon);
42
- const daemonTargetsOk = targets.length > 0 && targets.every((target) => targetResults[target]?.ok === true);
43
- const conflictTargets = targets.filter((target) => (targetResults[target]?.conflicts ?? 0) > 0);
44
- const daemonTargetsClean = daemonTargetsOk && conflictTargets.length === 0;
45
- const releaseAligned = health?.version === CLI_VERSION && cliVersion?.latest === CLI_VERSION;
46
- const daemonAligned = daemonLive && daemon?.version === CLI_VERSION;
47
- const payload = {
48
- ok: Boolean(health?.ok && releaseAligned && daemonAligned && daemonTargetsClean),
49
- release: {
50
- cli: CLI_VERSION,
51
- web: health?.version ?? null,
52
- cli_latest: cliVersion?.latest ?? null,
53
- cli_min: cliVersion?.min ?? null,
54
- release_aligned: releaseAligned,
55
- },
56
- health: health ?? null,
57
- daemon: daemon ? {
58
- running: daemonLive,
59
- version: daemon.version ?? null,
60
- hostname: daemon.hostname ?? null,
61
- targets,
62
- all_targets_ok: daemonTargetsOk,
63
- all_targets_clean: daemonTargetsClean,
64
- conflict_targets: conflictTargets,
65
- last_completed_at: daemon.last_completed_at ?? null,
66
- next_run_at: daemon.next_run_at ?? null,
67
- last_run: targetResults,
68
- } : null,
69
- escalations: [
70
- ...(releaseAligned ? [] : ["CLI, web health, and server latest versions are not aligned."]),
71
- ...(daemonLive ? [] : ["Daemon status is stale or the recorded daemon process is not running."]),
72
- ...(daemon?.version === CLI_VERSION ? [] : ["Daemon is not running on the pinned CLI version."]),
73
- ...(targets.length === 0 || daemonTargetsOk ? [] : ["Latest daemon cycle has not yet passed for every configured target."]),
74
- ...(conflictTargets.length === 0 ? [] : [`Latest daemon cycle has unresolved conflicts for: ${conflictTargets.join(", ")}.`]),
75
- ],
76
- };
77
- if (opts.json) {
78
- process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
79
- return;
80
- }
81
- process.stdout.write(`\n${symbols.dot} ${c.bold("Floom launch gate")}\n\n`);
82
- process.stdout.write(` ${c.dim("CLI:")} ${payload.release.cli}\n`);
83
- process.stdout.write(` ${c.dim("Web:")} ${payload.release.web ?? "unknown"}\n`);
84
- process.stdout.write(` ${c.dim("Daemon:")} ${payload.daemon?.running ? `running (${payload.daemon.version})` : "not running"}\n`);
85
- if (payload.escalations.length > 0) {
86
- for (const escalation of payload.escalations)
87
- process.stdout.write(` ${symbols.bullet} ${escalation}\n`);
88
- throw new FloomError("Launch gate did not pass.", "Run with --json for machine-readable details.");
89
- }
90
- process.stdout.write(`\n${symbols.ok} Launch gate passed for the pinned local release identity.\n\n`);
91
- }
92
- function daemonIsLive(status) {
93
- if (!status?.running)
94
- return false;
95
- if (!status.pid || status.pid < 1)
96
- return false;
97
- try {
98
- process.kill(status.pid, 0);
99
- }
100
- catch {
101
- return false;
102
- }
103
- if (!status.next_run_at || !status.interval_seconds)
104
- return true;
105
- const nextRun = Date.parse(status.next_run_at);
106
- if (!Number.isFinite(nextRun))
107
- return false;
108
- const graceMs = Math.max(status.interval_seconds * 2000, 120_000);
109
- return nextRun + graceMs >= Date.now();
110
- }