@floomhq/floom 3.1.0 → 5.0.1

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.
Files changed (72) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +346 -168
  3. package/bin/floom-mcp +9 -0
  4. package/bin/workeros-mcp +13 -0
  5. package/dist/cli.d.ts +6 -0
  6. package/dist/cli.js +336 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/commands/completion.d.ts +2 -0
  9. package/dist/commands/completion.js +81 -0
  10. package/dist/commands/completion.js.map +1 -0
  11. package/dist/commands/connections.d.ts +22 -0
  12. package/dist/commands/connections.js +158 -0
  13. package/dist/commands/connections.js.map +1 -0
  14. package/dist/commands/contexts.d.ts +34 -0
  15. package/dist/commands/contexts.js +247 -0
  16. package/dist/commands/contexts.js.map +1 -0
  17. package/dist/commands/doctor.d.ts +3 -0
  18. package/dist/commands/doctor.js +158 -0
  19. package/dist/commands/doctor.js.map +1 -0
  20. package/dist/commands/login.d.ts +14 -0
  21. package/dist/commands/login.js +349 -0
  22. package/dist/commands/login.js.map +1 -0
  23. package/dist/commands/logout.d.ts +1 -0
  24. package/dist/commands/logout.js +15 -0
  25. package/dist/commands/logout.js.map +1 -0
  26. package/dist/commands/mcp.d.ts +42 -0
  27. package/dist/commands/mcp.js +382 -0
  28. package/dist/commands/mcp.js.map +1 -0
  29. package/dist/commands/run.d.ts +15 -0
  30. package/dist/commands/run.js +154 -0
  31. package/dist/commands/run.js.map +1 -0
  32. package/dist/commands/runs.d.ts +25 -0
  33. package/dist/commands/runs.js +324 -0
  34. package/dist/commands/runs.js.map +1 -0
  35. package/dist/commands/secrets.d.ts +9 -0
  36. package/dist/commands/secrets.js +97 -0
  37. package/dist/commands/secrets.js.map +1 -0
  38. package/dist/commands/whoami.d.ts +3 -0
  39. package/dist/commands/whoami.js +70 -0
  40. package/dist/commands/whoami.js.map +1 -0
  41. package/dist/commands/workers.d.ts +30 -0
  42. package/dist/commands/workers.js +773 -0
  43. package/dist/commands/workers.js.map +1 -0
  44. package/dist/commands/workspaces.d.ts +10 -0
  45. package/dist/commands/workspaces.js +171 -0
  46. package/dist/commands/workspaces.js.map +1 -0
  47. package/dist/lib/api.d.ts +38 -0
  48. package/dist/lib/api.js +311 -0
  49. package/dist/lib/api.js.map +1 -0
  50. package/dist/lib/cli-errors.d.ts +1 -0
  51. package/dist/lib/cli-errors.js +20 -0
  52. package/dist/lib/cli-errors.js.map +1 -0
  53. package/dist/lib/command-name.d.ts +4 -0
  54. package/dist/lib/command-name.js +23 -0
  55. package/dist/lib/command-name.js.map +1 -0
  56. package/dist/lib/credentials.d.ts +21 -0
  57. package/dist/lib/credentials.js +138 -0
  58. package/dist/lib/credentials.js.map +1 -0
  59. package/dist/lib/output.d.ts +18 -0
  60. package/dist/lib/output.js +44 -0
  61. package/dist/lib/output.js.map +1 -0
  62. package/dist/lib/prompt.d.ts +2 -0
  63. package/dist/lib/prompt.js +55 -0
  64. package/dist/lib/prompt.js.map +1 -0
  65. package/dist/server.d.ts +5 -0
  66. package/dist/server.js +1171 -0
  67. package/dist/server.js.map +1 -0
  68. package/package.json +43 -51
  69. package/dist/index.d.ts +0 -1
  70. package/dist/index.js +0 -8278
  71. package/dist/version.d.ts +0 -1
  72. package/dist/version.js +0 -1
@@ -0,0 +1,773 @@
1
+ import { readdir, readFile, stat } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { inspect } from "node:util";
4
+ import { parse as parseYaml } from "yaml";
5
+ import { createAuthenticatedClient, FloomApiError, FloomConnectionError } from "../lib/api.js";
6
+ import { getCommandName } from "../lib/command-name.js";
7
+ import { log, printJson, renderTable } from "../lib/output.js";
8
+ function emitError(message, hint, json) {
9
+ if (json) {
10
+ // In JSON mode: keep stdout clean; write error to stderr only
11
+ process.stderr.write(`{"error": ${JSON.stringify(message)}, "hint": ${JSON.stringify(hint)}}\n`);
12
+ }
13
+ else {
14
+ log.err(message);
15
+ log.info(hint);
16
+ }
17
+ return 1;
18
+ }
19
+ function isRecord(value) {
20
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21
+ }
22
+ function nonEmptyString(value) {
23
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
24
+ }
25
+ function stringList(value) {
26
+ if (!Array.isArray(value))
27
+ return [];
28
+ return value.filter((item) => typeof item === "string" && item.trim().length > 0);
29
+ }
30
+ function readNestedRecord(parent, key) {
31
+ const value = parent[key];
32
+ return isRecord(value) ? value : undefined;
33
+ }
34
+ function readRuntime(manifest) {
35
+ const runtime = manifest.runtime;
36
+ if (typeof runtime === "string")
37
+ return nonEmptyString(runtime);
38
+ if (isRecord(runtime))
39
+ return nonEmptyString(runtime.type) || nonEmptyString(runtime.name);
40
+ const exec = readNestedRecord(manifest, "exec");
41
+ if (!exec)
42
+ return undefined;
43
+ const execRuntime = exec.runtime;
44
+ if (typeof execRuntime === "string")
45
+ return nonEmptyString(execRuntime);
46
+ if (isRecord(execRuntime))
47
+ return nonEmptyString(execRuntime.type) || nonEmptyString(execRuntime.name);
48
+ return undefined;
49
+ }
50
+ function readEntrypoint(manifest) {
51
+ const exec = readNestedRecord(manifest, "exec");
52
+ const execEntry = exec ? nonEmptyString(exec.entry) : undefined;
53
+ if (execEntry)
54
+ return execEntry;
55
+ const runtime = readNestedRecord(manifest, "runtime");
56
+ const runtimeEntrypoint = runtime ? nonEmptyString(runtime.entrypoint) : undefined;
57
+ if (runtimeEntrypoint)
58
+ return runtimeEntrypoint;
59
+ return nonEmptyString(manifest.entrypoint);
60
+ }
61
+ function isValidTimeZone(value) {
62
+ try {
63
+ new Intl.DateTimeFormat("en-US", { timeZone: value }).format(new Date(0));
64
+ return true;
65
+ }
66
+ catch {
67
+ return false;
68
+ }
69
+ }
70
+ function validateTriggerTimezone(trigger, path) {
71
+ if (!isRecord(trigger))
72
+ return [];
73
+ const timezone = nonEmptyString(trigger.timezone);
74
+ if (!timezone || isValidTimeZone(timezone))
75
+ return [];
76
+ return [`${path}.timezone is not a valid IANA timezone: ${timezone}`];
77
+ }
78
+ function validateTimezones(manifest) {
79
+ const errors = [];
80
+ errors.push(...validateTriggerTimezone(manifest.trigger, "trigger"));
81
+ const cronTimezone = nonEmptyString(manifest.cron_timezone);
82
+ if (cronTimezone && !isValidTimeZone(cronTimezone)) {
83
+ errors.push(`cron_timezone is not a valid IANA timezone: ${cronTimezone}`);
84
+ }
85
+ if (Array.isArray(manifest.triggers)) {
86
+ manifest.triggers.forEach((trigger, index) => {
87
+ errors.push(...validateTriggerTimezone(trigger, `triggers[${index}]`));
88
+ });
89
+ }
90
+ return errors;
91
+ }
92
+ function validateWorkerContractShape(manifest) {
93
+ const errors = [];
94
+ if (Array.isArray(manifest.use_cases)) {
95
+ if (manifest.use_cases.length < 3 || manifest.use_cases.length > 5) {
96
+ errors.push("use_cases must contain 3 to 5 items");
97
+ }
98
+ manifest.use_cases.forEach((item, index) => {
99
+ if (typeof item !== "string" || !item.trim()) {
100
+ errors.push(`use_cases.${index} must be a non-empty string`);
101
+ }
102
+ });
103
+ }
104
+ const validateFields = (value, path) => {
105
+ if (!Array.isArray(value))
106
+ return;
107
+ value.forEach((field, index) => {
108
+ if (!isRecord(field))
109
+ return;
110
+ if ("placeholder" in field && field.placeholder !== undefined && field.placeholder !== null && typeof field.placeholder !== "string") {
111
+ errors.push(`${path}.${index}.placeholder must be a string`);
112
+ }
113
+ });
114
+ };
115
+ validateFields(manifest.inputs, "inputs");
116
+ validateFields(manifest.outputs, "outputs");
117
+ const exec = readNestedRecord(manifest, "exec");
118
+ if (exec) {
119
+ validateFields(exec.inputs, "exec.inputs");
120
+ validateFields(exec.outputs, "exec.outputs");
121
+ }
122
+ return errors;
123
+ }
124
+ function declaredComposioConnections(manifest) {
125
+ const result = new Map();
126
+ const raw = manifest.connections;
127
+ if (!Array.isArray(raw))
128
+ return result;
129
+ for (const item of raw) {
130
+ let app;
131
+ let allowedTools;
132
+ if (typeof item === "string") {
133
+ app = item;
134
+ }
135
+ else if (isRecord(item)) {
136
+ if (typeof item.app === "string") {
137
+ app = item.app;
138
+ allowedTools = Array.isArray(item.allowed_tools)
139
+ ? item.allowed_tools.filter((tool) => typeof tool === "string")
140
+ : undefined;
141
+ }
142
+ else if (isRecord(item.composio) && typeof item.composio.app === "string") {
143
+ app = item.composio.app;
144
+ allowedTools = Array.isArray(item.composio.allowed_tools)
145
+ ? item.composio.allowed_tools.filter((tool) => typeof tool === "string")
146
+ : undefined;
147
+ }
148
+ }
149
+ const normalizedApp = app?.trim().toLowerCase();
150
+ if (!normalizedApp)
151
+ continue;
152
+ if (!allowedTools || allowedTools.length === 0) {
153
+ result.set(normalizedApp, null);
154
+ continue;
155
+ }
156
+ const normalizedTools = new Set(allowedTools.map((tool) => tool.trim().toUpperCase()).filter(Boolean));
157
+ const existing = result.get(normalizedApp);
158
+ if (existing === null)
159
+ continue;
160
+ if (!existing) {
161
+ result.set(normalizedApp, normalizedTools);
162
+ }
163
+ else {
164
+ for (const tool of normalizedTools)
165
+ existing.add(tool);
166
+ }
167
+ }
168
+ return result;
169
+ }
170
+ function declaredSecrets(manifest) {
171
+ const result = new Set();
172
+ const collect = (value) => {
173
+ if (!Array.isArray(value))
174
+ return;
175
+ for (const item of value) {
176
+ if (typeof item === "string" && item.trim()) {
177
+ result.add(item.trim().toUpperCase());
178
+ }
179
+ }
180
+ };
181
+ collect(manifest.secrets);
182
+ const capabilities = readNestedRecord(manifest, "capabilities");
183
+ if (capabilities) {
184
+ collect(capabilities.secrets);
185
+ }
186
+ const exec = readNestedRecord(manifest, "exec");
187
+ if (exec) {
188
+ collect(exec.secrets);
189
+ }
190
+ return result;
191
+ }
192
+ function toolApp(toolSlug, declared) {
193
+ const normalized = toolSlug.toUpperCase();
194
+ const matches = [...declared.keys()].filter((app) => normalized.startsWith(`${app.toUpperCase().replaceAll("-", "_")}_`));
195
+ if (matches.length > 0)
196
+ return matches.sort((a, b) => b.length - a.length)[0];
197
+ const allowlistMatches = [...declared.entries()]
198
+ .filter(([, allowedTools]) => allowedTools !== null && allowedTools.has(normalized))
199
+ .map(([app]) => app);
200
+ if (allowlistMatches.length === 0)
201
+ return "";
202
+ return allowlistMatches.sort((a, b) => b.length - a.length)[0];
203
+ }
204
+ function validateNativeRuntimeContract(manifest, runPy) {
205
+ if (!runPy?.trim())
206
+ return [];
207
+ const errors = [];
208
+ const declared = declaredComposioConnections(manifest);
209
+ const secrets = declaredSecrets(manifest);
210
+ const usesComposioCli = /subprocess\.(?:run|Popen|call|check_call|check_output)\s*\(/.test(runPy) &&
211
+ /["']composio["']/.test(runPy) &&
212
+ /["']execute["']/.test(runPy);
213
+ if (usesComposioCli) {
214
+ errors.push("run.py shells out to `composio execute`; E2B workers must call the Floom proxy at /runs/{FLOOM_RUN_ID}/composio-execute/{TOOL_SLUG}");
215
+ }
216
+ const usesProxy = /composio-execute\/[A-Z0-9_]+/.test(runPy);
217
+ const readsConnections = /connections\.json/.test(runPy);
218
+ if ((usesProxy || readsConnections) && declared.size === 0) {
219
+ errors.push("run.py uses Composio/connections.json but worker.yml has no `connections:` declaration");
220
+ }
221
+ const toolSlugs = new Set();
222
+ for (const match of runPy.matchAll(/composio-execute\/([A-Z0-9_]+)/g)) {
223
+ toolSlugs.add(match[1].toUpperCase());
224
+ }
225
+ for (const match of runPy.matchAll(/["']([A-Z][A-Z0-9]+_[A-Z0-9_]+)["']/g)) {
226
+ const candidate = match[1].toUpperCase();
227
+ if (["FLOOM_RUN_ID", "FLOOM_TRACE_ID", "WORKEROS_API_URL", "WORKEROS_API_BASE"].includes(candidate)) {
228
+ continue;
229
+ }
230
+ if (secrets.has(candidate)) {
231
+ continue;
232
+ }
233
+ toolSlugs.add(candidate);
234
+ }
235
+ for (const slug of toolSlugs) {
236
+ const app = toolApp(slug, declared);
237
+ if (!app || !declared.has(app)) {
238
+ errors.push(`run.py references ${slug}, but worker.yml does not declare connection '${app || "unknown"}'`);
239
+ continue;
240
+ }
241
+ const allowed = declared.get(app);
242
+ if (allowed !== null && allowed && !allowed.has(slug)) {
243
+ errors.push(`run.py references ${slug}, but ${app}.allowed_tools does not include it`);
244
+ }
245
+ }
246
+ if (/FLOOM_RUN_ID/.test(runPy) && !/WORKEROS_API_URL/.test(runPy)) {
247
+ errors.push("run.py uses FLOOM_RUN_ID but does not read WORKEROS_API_URL for the API proxy base");
248
+ }
249
+ return errors;
250
+ }
251
+ async function readOptionalText(path) {
252
+ try {
253
+ return stripUtf8Bom(await readFile(path, "utf8"));
254
+ }
255
+ catch (error) {
256
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
257
+ return undefined;
258
+ }
259
+ throw error;
260
+ }
261
+ }
262
+ function stripUtf8Bom(text) {
263
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
264
+ }
265
+ const IGNORED_BUNDLE_DIRS = new Set([
266
+ ".git",
267
+ ".hg",
268
+ ".svn",
269
+ "__pycache__",
270
+ ".pytest_cache",
271
+ ".mypy_cache",
272
+ ".ruff_cache",
273
+ ".venv",
274
+ "venv",
275
+ "node_modules",
276
+ ]);
277
+ const IGNORED_BUNDLE_FILES = new Set([".DS_Store"]);
278
+ const SECRET_BUNDLE_FILE_RE = /(^|\/)(?:\.env(?:\..*)?|credentials\.json|.*\.(?:pem|key|p12|pfx))$/i;
279
+ function toBundlePath(root, absolutePath) {
280
+ return absolutePath.slice(root.length + 1).replaceAll("\\", "/");
281
+ }
282
+ function looksBinary(buffer) {
283
+ return buffer.includes(0);
284
+ }
285
+ async function collectWorkerFiles(dir) {
286
+ const files = [];
287
+ const errors = [];
288
+ async function walk(current) {
289
+ const entries = await readdir(current, { withFileTypes: true });
290
+ for (const entry of entries) {
291
+ if (entry.isDirectory() && IGNORED_BUNDLE_DIRS.has(entry.name))
292
+ continue;
293
+ if (entry.isFile() && IGNORED_BUNDLE_FILES.has(entry.name))
294
+ continue;
295
+ const absolute = join(current, entry.name);
296
+ if (entry.isDirectory()) {
297
+ await walk(absolute);
298
+ continue;
299
+ }
300
+ if (!entry.isFile())
301
+ continue;
302
+ const info = await stat(absolute);
303
+ const relPath = toBundlePath(dir, absolute);
304
+ if (SECRET_BUNDLE_FILE_RE.test(relPath)) {
305
+ errors.push(`${relPath} looks like credential material; store credentials in Floom secrets instead`);
306
+ continue;
307
+ }
308
+ if (info.size > 5 * 1024 * 1024) {
309
+ errors.push(`${relPath} is larger than 5MB; use runtime download/storage for large assets`);
310
+ continue;
311
+ }
312
+ const content = await readFile(absolute);
313
+ if (looksBinary(content)) {
314
+ errors.push(`${relPath} appears to be binary; workers push currently supports UTF-8 bundle files`);
315
+ continue;
316
+ }
317
+ files.push({ path: relPath, content: stripUtf8Bom(content.toString("utf8")) });
318
+ }
319
+ }
320
+ await walk(dir);
321
+ files.sort((a, b) => a.path.localeCompare(b.path));
322
+ return { files, errors };
323
+ }
324
+ export async function loadWorkerSource(dirArg) {
325
+ const dir = resolve(dirArg);
326
+ const errors = [];
327
+ const workerYmlPath = join(dir, "worker.yml");
328
+ const runPyPath = join(dir, "run.py");
329
+ const skillMdPath = join(dir, "SKILL.md");
330
+ let workerYml = "";
331
+ try {
332
+ workerYml = stripUtf8Bom(await readFile(workerYmlPath, "utf8"));
333
+ }
334
+ catch (error) {
335
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
336
+ return { errors: [`Missing required file: ${workerYmlPath}`] };
337
+ }
338
+ throw error;
339
+ }
340
+ let manifest;
341
+ try {
342
+ manifest = parseYaml(workerYml);
343
+ }
344
+ catch (error) {
345
+ const message = error instanceof Error ? error.message : String(error);
346
+ return { errors: [`worker.yml is not valid YAML: ${message}`] };
347
+ }
348
+ if (!isRecord(manifest)) {
349
+ return { errors: ["worker.yml must contain a YAML mapping"] };
350
+ }
351
+ const runPy = await readOptionalText(runPyPath);
352
+ const skillMd = await readOptionalText(skillMdPath);
353
+ const collected = await collectWorkerFiles(dir);
354
+ errors.push(...collected.errors);
355
+ const hasRunPy = Boolean(runPy?.trim());
356
+ const hasSkillMd = Boolean(skillMd?.trim());
357
+ if (!hasRunPy && !hasSkillMd) {
358
+ errors.push("Worker directory must include a non-empty run.py or SKILL.md");
359
+ }
360
+ const workerId = nonEmptyString(manifest.id) || nonEmptyString(manifest.name);
361
+ if (!workerId) {
362
+ errors.push("worker.yml must include an id or name field");
363
+ }
364
+ const displayName = nonEmptyString(manifest.title) || nonEmptyString(manifest.name) || nonEmptyString(manifest.id);
365
+ if (!displayName) {
366
+ errors.push("worker.yml must include a name, title, or id field");
367
+ }
368
+ const runtime = readRuntime(manifest);
369
+ if (!runtime) {
370
+ errors.push("worker.yml must include a runtime field (runtime, runtime.type, exec.runtime, or exec.runtime.type)");
371
+ }
372
+ errors.push(...validateTimezones(manifest));
373
+ errors.push(...validateWorkerContractShape(manifest));
374
+ const entrypoint = readEntrypoint(manifest);
375
+ if (entrypoint === "run.py" && !hasRunPy) {
376
+ errors.push("worker.yml entrypoint is run.py, but run.py is missing or empty");
377
+ }
378
+ if (entrypoint === "SKILL.md" && !hasSkillMd) {
379
+ errors.push("worker.yml entrypoint is SKILL.md, but SKILL.md is missing or empty");
380
+ }
381
+ if (entrypoint !== "SKILL.md") {
382
+ errors.push(...validateNativeRuntimeContract(manifest, runPy));
383
+ }
384
+ if (errors.length > 0 || !workerId || !displayName || !runtime) {
385
+ return { errors };
386
+ }
387
+ return {
388
+ source: {
389
+ dir,
390
+ workerYml,
391
+ runPy,
392
+ skillMd,
393
+ files: collected.files,
394
+ workerId,
395
+ displayName,
396
+ runtime,
397
+ entrypoint,
398
+ },
399
+ errors: [],
400
+ };
401
+ }
402
+ function sourcePayload(source) {
403
+ return {
404
+ worker_yml: source.workerYml,
405
+ run_py: source.runPy ?? "",
406
+ ...(source.skillMd !== undefined ? { skill_md: source.skillMd } : {}),
407
+ };
408
+ }
409
+ function filesPayload(source) {
410
+ return { files: source.files };
411
+ }
412
+ function hasNonLegacyBundleFiles(source) {
413
+ const legacyPaths = new Set(["worker.yml", "run.py", "SKILL.md"]);
414
+ return source.files.some((file) => !legacyPaths.has(file.path));
415
+ }
416
+ function emitValidationErrors(errors) {
417
+ for (const error of errors) {
418
+ log.err(error);
419
+ }
420
+ return 1;
421
+ }
422
+ function workerConnectionLabel(connection) {
423
+ const composio = readNestedRecord(connection, "composio");
424
+ const candidates = [
425
+ connection.display_name,
426
+ connection.account_label,
427
+ connection.provider,
428
+ connection.app,
429
+ connection.name,
430
+ connection.mcp_label,
431
+ composio?.provider,
432
+ composio?.app,
433
+ connection.id,
434
+ ];
435
+ for (const candidate of candidates) {
436
+ const value = nonEmptyString(candidate);
437
+ if (value)
438
+ return value;
439
+ }
440
+ return "";
441
+ }
442
+ function workerConnectionDetails(connection) {
443
+ const details = [];
444
+ const status = nonEmptyString(connection.status);
445
+ if (status)
446
+ details.push(`status: ${status}`);
447
+ const label = workerConnectionLabel(connection);
448
+ const account = nonEmptyString(connection.account_label) || nonEmptyString(connection.display_name);
449
+ if (account && account !== label) {
450
+ details.push(`account: ${account}`);
451
+ }
452
+ const scopes = stringList(connection.scopes);
453
+ if (scopes.length > 0) {
454
+ details.push(`scopes: ${scopes.join(", ")}`);
455
+ }
456
+ const allowedTools = stringList(connection.allowed_tools);
457
+ const composio = readNestedRecord(connection, "composio");
458
+ const composioAllowedTools = composio ? stringList(composio.allowed_tools) : [];
459
+ const mergedAllowedTools = [...new Set([...allowedTools, ...composioAllowedTools])];
460
+ if (mergedAllowedTools.length > 0) {
461
+ details.push(`allowed_tools: ${mergedAllowedTools.join(", ")}`);
462
+ }
463
+ const expiresAt = nonEmptyString(connection.expires_at) || nonEmptyString(connection.expiresAt);
464
+ if (expiresAt) {
465
+ details.push(`expires_at: ${expiresAt}`);
466
+ }
467
+ return details;
468
+ }
469
+ function formatConnection(connection) {
470
+ if (typeof connection === "string")
471
+ return connection;
472
+ if (!isRecord(connection))
473
+ return String(connection ?? "");
474
+ const label = workerConnectionLabel(connection);
475
+ const details = workerConnectionDetails(connection);
476
+ if (label && details.length === 0)
477
+ return label;
478
+ if (label)
479
+ return `${label}${details.length > 0 ? ` (${details.join("; ")})` : ""}`;
480
+ if (details.length > 0)
481
+ return `connection (${details.join("; ")})`;
482
+ return inspect(connection, { depth: 1, compact: true, breakLength: 80, sorted: true });
483
+ }
484
+ function formatConnections(connections) {
485
+ if (!Array.isArray(connections) || connections.length === 0)
486
+ return [];
487
+ return connections.map((connection) => formatConnection(connection)).filter(Boolean);
488
+ }
489
+ function apiErrorDetail(error) {
490
+ const body = error.body;
491
+ if (body && typeof body === "object" && "detail" in body) {
492
+ const detail = body.detail;
493
+ return typeof detail === "string" ? detail : JSON.stringify(detail);
494
+ }
495
+ return error.message;
496
+ }
497
+ function isExpiredAuthError(error) {
498
+ if (error.status === 401)
499
+ return true;
500
+ if (error.status !== 403)
501
+ return false;
502
+ const detail = apiErrorDetail(error).toLowerCase();
503
+ return detail.includes("expired") || detail.includes("invalid token") || detail.includes("invalid jwt");
504
+ }
505
+ function emitApiError(error) {
506
+ const message = error instanceof Error ? error.message : String(error);
507
+ if (message.includes("Not logged in")) {
508
+ return emitError("Not authenticated.", `Run: ${getCommandName()} login`);
509
+ }
510
+ if (error instanceof FloomConnectionError) {
511
+ return emitError("Floom API is unreachable.", `Tried ${error.apiBase}. Check WORKEROS_API_BASE/FLOOM_API_BASE and network connectivity.`);
512
+ }
513
+ if (error instanceof FloomApiError && isExpiredAuthError(error)) {
514
+ return emitError("Your session expired.", `Re-run: ${getCommandName()} login`);
515
+ }
516
+ if (error instanceof FloomApiError && error.status === 403) {
517
+ return emitError("Request was forbidden.", `API said: ${apiErrorDetail(error)}. Check that this token can access the target worker/workspace.`);
518
+ }
519
+ if (error instanceof FloomApiError && error.status && error.status >= 500) {
520
+ return emitError(`API error: ${message}`, "Check API status, then retry. Report: https://github.com/floomhq/floom/issues");
521
+ }
522
+ if (error instanceof FloomApiError && error.status && error.status >= 400) {
523
+ return emitError(`API rejected worker source: ${apiErrorDetail(error)}`, `Fix the worker files and retry: ${getCommandName()} workers validate <dir>`);
524
+ }
525
+ throw error;
526
+ }
527
+ export async function workersValidateCommand(dir) {
528
+ const result = await loadWorkerSource(dir);
529
+ if (!result.source) {
530
+ return emitValidationErrors(result.errors);
531
+ }
532
+ log.ok(`Validated ${result.source.workerId}`);
533
+ log.kv("Directory", result.source.dir);
534
+ log.kv("Name", result.source.displayName);
535
+ log.kv("Runtime", result.source.runtime);
536
+ log.kv("Source", result.source.entrypoint === "SKILL.md" ? "SKILL.md" : result.source.runPy?.trim() ? "run.py" : "SKILL.md");
537
+ return 0;
538
+ }
539
+ export async function workersPushCommand(dir) {
540
+ const result = await loadWorkerSource(dir);
541
+ if (!result.source) {
542
+ return emitValidationErrors(result.errors);
543
+ }
544
+ const source = result.source;
545
+ const payload = sourcePayload(source);
546
+ try {
547
+ const { client } = await createAuthenticatedClient();
548
+ let exists = false;
549
+ try {
550
+ await client.requestJson("GET", `/workers/${encodeURIComponent(source.workerId)}`);
551
+ exists = true;
552
+ }
553
+ catch (error) {
554
+ if (error instanceof FloomApiError && error.status === 404) {
555
+ exists = false;
556
+ }
557
+ else {
558
+ throw error;
559
+ }
560
+ }
561
+ if (!exists) {
562
+ try {
563
+ await client.requestJson("POST", "/workers", { body: payload });
564
+ }
565
+ catch (error) {
566
+ if (error instanceof FloomApiError && error.status === 409 && /already exists/i.test(apiErrorDetail(error))) {
567
+ return emitError(`Worker id '${source.workerId}' already exists outside the active workspace.`, `Choose a unique worker id in worker.yml, then run: ${getCommandName()} workers validate <dir> && ${getCommandName()} workers push <dir>`);
568
+ }
569
+ throw error;
570
+ }
571
+ if (hasNonLegacyBundleFiles(source)) {
572
+ try {
573
+ await client.requestJson("PUT", `/workers/${encodeURIComponent(source.workerId)}/files`, { body: filesPayload(source) });
574
+ }
575
+ catch (error) {
576
+ if (error instanceof FloomApiError && (error.status === 404 || error.status === 405)) {
577
+ return emitError("This Floom API created the worker but does not support full worker bundle uploads.", `PUT /workers/${source.workerId}/files returned HTTP ${error.status}. Upgrade the API before pushing workers with data/ or lib/ files.`);
578
+ }
579
+ throw error;
580
+ }
581
+ }
582
+ log.ok(`Created ${source.workerId}`);
583
+ return 0;
584
+ }
585
+ try {
586
+ await client.requestJson("PUT", `/workers/${encodeURIComponent(source.workerId)}/files`, { body: filesPayload(source) });
587
+ }
588
+ catch (error) {
589
+ if (error instanceof FloomApiError && (error.status === 404 || error.status === 405)) {
590
+ return emitError("This Floom API does not support full worker bundle updates.", `PUT /workers/${source.workerId}/files returned HTTP ${error.status}. Upgrade the API or use a new worker id.`);
591
+ }
592
+ throw error;
593
+ }
594
+ log.ok(`Updated ${source.workerId}`);
595
+ return 0;
596
+ }
597
+ catch (error) {
598
+ return emitApiError(error);
599
+ }
600
+ }
601
+ export async function workersListCommand(options) {
602
+ try {
603
+ const { client } = await createAuthenticatedClient();
604
+ const workers = (await client.requestJson("GET", "/workers"));
605
+ if (options.json) {
606
+ printJson(workers);
607
+ return 0;
608
+ }
609
+ const rows = workers.map((worker) => ({
610
+ Name: worker.name || worker.id,
611
+ Status: worker.status,
612
+ "Last run": worker.last_run?.created_at || "-",
613
+ Triggers: (worker.triggers || []).join(", ") || "-",
614
+ }));
615
+ process.stdout.write(renderTable(rows, [
616
+ { key: "Name", label: "Name" },
617
+ { key: "Status", label: "Status" },
618
+ { key: "Last run", label: "Last run" },
619
+ { key: "Triggers", label: "Triggers" },
620
+ ]) + "\n");
621
+ return 0;
622
+ }
623
+ catch (error) {
624
+ const message = error instanceof Error ? error.message : String(error);
625
+ if (message.includes("Not logged in")) {
626
+ return emitError("Not authenticated.", `Run: ${getCommandName()} login`, options.json);
627
+ }
628
+ if (error instanceof FloomApiError && (error.status === 401 || error.status === 403)) {
629
+ return emitError("Your session expired.", `Re-run: ${getCommandName()} login`, options.json);
630
+ }
631
+ if (error instanceof FloomApiError && error.status && error.status >= 500) {
632
+ return emitError(`API error: ${message}`, "Check API status, then retry. Report: https://github.com/floomhq/floom/issues", options.json);
633
+ }
634
+ throw error;
635
+ }
636
+ }
637
+ export async function workersShowCommand(workerId, options) {
638
+ try {
639
+ const { client } = await createAuthenticatedClient();
640
+ const worker = (await client.requestJson("GET", `/workers/${encodeURIComponent(workerId)}`));
641
+ if (options.json) {
642
+ printJson(worker);
643
+ return 0;
644
+ }
645
+ process.stdout.write(`${worker.name} (${worker.id})\n`);
646
+ if (worker.description)
647
+ process.stdout.write(`${worker.description}\n`);
648
+ process.stdout.write(`Entry: ${worker.config?.runtime?.entrypoint || "unknown"}\n`);
649
+ const connections = formatConnections(worker.config?.connections);
650
+ if (connections.length === 0) {
651
+ process.stdout.write("Connections: none\n");
652
+ }
653
+ else {
654
+ process.stdout.write("Connections:\n");
655
+ for (const connection of connections) {
656
+ process.stdout.write(` - ${connection}\n`);
657
+ }
658
+ }
659
+ const runs = (worker.recent_runs || []).slice(0, 5);
660
+ if (runs.length) {
661
+ process.stdout.write("\n");
662
+ process.stdout.write("Recent runs:\n");
663
+ process.stdout.write(renderTable(runs.map((run) => ({ id: run.id, status: run.status, created_at: run.created_at || "-" })), [
664
+ { key: "id", label: "Run" },
665
+ { key: "status", label: "Status" },
666
+ { key: "created_at", label: "Created" },
667
+ ]) + "\n");
668
+ }
669
+ return 0;
670
+ }
671
+ catch (error) {
672
+ const message = error instanceof Error ? error.message : String(error);
673
+ if (message.includes("Not logged in")) {
674
+ return emitError("Not authenticated.", `Run: ${getCommandName()} login`, options.json);
675
+ }
676
+ if (error instanceof FloomApiError && error.status === 404) {
677
+ return emitError(`Worker '${workerId}' not found.`, `List available workers: ${getCommandName()} workers list`, options.json);
678
+ }
679
+ if (error instanceof FloomApiError && (error.status === 401 || error.status === 403)) {
680
+ return emitError("Your session expired.", `Re-run: ${getCommandName()} login`, options.json);
681
+ }
682
+ if (error instanceof FloomApiError && error.status && error.status >= 500) {
683
+ return emitError(`API error: ${message}`, "Check API status, then retry. Report: https://github.com/floomhq/floom/issues", options.json);
684
+ }
685
+ throw error;
686
+ }
687
+ }
688
+ function humanizeAge(isoDate) {
689
+ if (!isoDate)
690
+ return "never";
691
+ const ms = Date.now() - new Date(isoDate).getTime();
692
+ const s = Math.floor(ms / 1000);
693
+ if (s < 60)
694
+ return `${s}s ago`;
695
+ const m = Math.floor(s / 60);
696
+ if (m < 60)
697
+ return `${m}m ago`;
698
+ const h = Math.floor(m / 60);
699
+ if (h < 24)
700
+ return `${h}h ago`;
701
+ const d = Math.floor(h / 24);
702
+ return `${d}d ago`;
703
+ }
704
+ function humanizeDuration(ms) {
705
+ if (ms === undefined)
706
+ return "";
707
+ if (ms < 1000)
708
+ return `${ms}ms`;
709
+ return `${(ms / 1000).toFixed(1)}s`;
710
+ }
711
+ export async function workersInfoCommand(workerId, options) {
712
+ try {
713
+ const { client } = await createAuthenticatedClient();
714
+ const worker = (await client.requestJson("GET", `/workers/${encodeURIComponent(workerId)}`));
715
+ if (options.json) {
716
+ printJson(worker);
717
+ return 0;
718
+ }
719
+ const label = worker.is_example ? " [Example]" : "";
720
+ log.heading(`${worker.name}${label}`);
721
+ if (worker.description) {
722
+ log.kv("Description", worker.description);
723
+ }
724
+ const triggers = (worker.config?.triggers || []).map((t) => t.type).join(", ");
725
+ log.kv("Trigger", triggers || "Manual run");
726
+ const connections = formatConnections(worker.config?.connections);
727
+ if (connections.length === 0) {
728
+ log.kv("Connections", "none");
729
+ }
730
+ else {
731
+ log.kv("Connections", connections[0]);
732
+ for (const connection of connections.slice(1)) {
733
+ log.info(` ${connection}`);
734
+ }
735
+ }
736
+ const secrets = (worker.config?.secrets || []).join(", ");
737
+ log.kv("Secrets needed", secrets || "none");
738
+ const recent = (worker.recent_runs || []).slice(0, 20);
739
+ if (recent.length > 0) {
740
+ const lastRun = recent[0];
741
+ const age = humanizeAge(lastRun.created_at);
742
+ const dur = humanizeDuration(lastRun.duration_ms);
743
+ const durStr = dur ? ` (${dur})` : "";
744
+ log.kv("Last run", `${age} — ${lastRun.status}${durStr}`);
745
+ const successCount = recent.filter((r) => r.status === "completed" || r.status === "approved" || r.status === "success").length;
746
+ const pct = Math.round((successCount / recent.length) * 100);
747
+ log.kv("Recent success", `${successCount}/${recent.length} (${pct}%)`);
748
+ }
749
+ else {
750
+ log.kv("Last run", "never");
751
+ }
752
+ log.blank();
753
+ log.info(`Try: ${getCommandName()} run ${workerId}`);
754
+ return 0;
755
+ }
756
+ catch (error) {
757
+ const message = error instanceof Error ? error.message : String(error);
758
+ if (message.includes("Not logged in")) {
759
+ return emitError("Not authenticated.", `Run: ${getCommandName()} login`, options.json);
760
+ }
761
+ if (error instanceof FloomApiError && error.status === 404) {
762
+ return emitError(`Worker '${workerId}' not found.`, `List available workers: ${getCommandName()} workers list`, options.json);
763
+ }
764
+ if (error instanceof FloomApiError && (error.status === 401 || error.status === 403)) {
765
+ return emitError("Your session expired.", `Re-run: ${getCommandName()} login`, options.json);
766
+ }
767
+ if (error instanceof FloomApiError && error.status && error.status >= 500) {
768
+ return emitError(`API error: ${message}`, "Check API status, then retry. Report: https://github.com/floomhq/floom/issues", options.json);
769
+ }
770
+ throw error;
771
+ }
772
+ }
773
+ //# sourceMappingURL=workers.js.map