@floomhq/floom 5.0.11 → 5.1.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.
Files changed (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +381 -381
  3. package/assets/floom-skill.md +32 -0
  4. package/bin/floom-mcp +9 -9
  5. package/bin/workeros-mcp +13 -13
  6. package/dist/cli.js +89 -10
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands/completion.js +85 -85
  9. package/dist/commands/connections.js +3 -3
  10. package/dist/commands/connections.js.map +1 -1
  11. package/dist/commands/contexts.js +9 -9
  12. package/dist/commands/contexts.js.map +1 -1
  13. package/dist/commands/doctor.js +2 -2
  14. package/dist/commands/doctor.js.map +1 -1
  15. package/dist/commands/init.d.ts +5 -0
  16. package/dist/commands/init.js +64 -0
  17. package/dist/commands/init.js.map +1 -0
  18. package/dist/commands/login.js +5 -9
  19. package/dist/commands/login.js.map +1 -1
  20. package/dist/commands/mcp.d.ts +4 -0
  21. package/dist/commands/mcp.js +67 -8
  22. package/dist/commands/mcp.js.map +1 -1
  23. package/dist/commands/run.js +1 -1
  24. package/dist/commands/run.js.map +1 -1
  25. package/dist/commands/runs.d.ts +1 -0
  26. package/dist/commands/runs.js +83 -20
  27. package/dist/commands/runs.js.map +1 -1
  28. package/dist/commands/whoami.js +5 -1
  29. package/dist/commands/whoami.js.map +1 -1
  30. package/dist/commands/workers.d.ts +3 -1
  31. package/dist/commands/workers.js +37 -18
  32. package/dist/commands/workers.js.map +1 -1
  33. package/dist/commands/workspaces.js +9 -4
  34. package/dist/commands/workspaces.js.map +1 -1
  35. package/dist/lib/api.js +2 -2
  36. package/dist/lib/api.js.map +1 -1
  37. package/dist/lib/cli-errors.d.ts +3 -1
  38. package/dist/lib/cli-errors.js +10 -2
  39. package/dist/lib/cli-errors.js.map +1 -1
  40. package/dist/lib/credentials.js +2 -2
  41. package/dist/lib/credentials.js.map +1 -1
  42. package/dist/lib/output.d.ts +1 -0
  43. package/dist/lib/output.js +11 -7
  44. package/dist/lib/output.js.map +1 -1
  45. package/dist/lib/telemetry.d.ts +22 -0
  46. package/dist/lib/telemetry.js +85 -0
  47. package/dist/lib/telemetry.js.map +1 -0
  48. package/dist/lib/worker-authoring.js +154 -154
  49. package/dist/server.js +60 -7
  50. package/dist/server.js.map +1 -1
  51. package/package.json +47 -46
@@ -0,0 +1,85 @@
1
+ import { FloomApiClient } from "./api.js";
2
+ import { readCredentials } from "./credentials.js";
3
+ function telemetryDisabled() {
4
+ const value = (process.env.FLOOM_CLI_TELEMETRY_DISABLED || process.env.WORKEROS_CLI_TELEMETRY_DISABLED || "")
5
+ .trim()
6
+ .toLowerCase();
7
+ return value === "1" || value === "true" || value === "yes" || value === "on";
8
+ }
9
+ export function apiBaseKind(apiBase) {
10
+ let host = "";
11
+ try {
12
+ host = new URL(apiBase).hostname.toLowerCase();
13
+ }
14
+ catch {
15
+ return "custom";
16
+ }
17
+ if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
18
+ return "local";
19
+ }
20
+ if (host.endsWith("floom.dev") ||
21
+ host.endsWith("floom.ai") ||
22
+ host.endsWith("floom.app") ||
23
+ host.endsWith("workeros.com")) {
24
+ return "cloud";
25
+ }
26
+ return "custom";
27
+ }
28
+ async function telemetryClient() {
29
+ if (telemetryDisabled())
30
+ return null;
31
+ try {
32
+ const credentials = await readCredentials();
33
+ if (!credentials)
34
+ return null;
35
+ return { client: new FloomApiClient(credentials.api_base, credentials), credentials };
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ export async function emitCliCommandTelemetry(payload) {
42
+ try {
43
+ const resolved = await telemetryClient();
44
+ if (!resolved)
45
+ return;
46
+ await resolved.client.requestJson("POST", "/telemetry/cli-command", {
47
+ body: {
48
+ command: payload.command,
49
+ success: payload.success,
50
+ duration_ms: Math.max(0, Math.floor(payload.duration_ms)),
51
+ exit_code: payload.exit_code,
52
+ api_base_kind: apiBaseKind(resolved.credentials.api_base),
53
+ worker_id: payload.worker_id,
54
+ run_id: payload.run_id,
55
+ },
56
+ });
57
+ }
58
+ catch {
59
+ // Analytics must never affect CLI behavior.
60
+ }
61
+ }
62
+ export async function emitMcpToolTelemetry(payload) {
63
+ try {
64
+ const resolved = await telemetryClient();
65
+ if (!resolved)
66
+ return;
67
+ await resolved.client.requestJson("POST", "/telemetry/mcp-tool", {
68
+ body: {
69
+ tool_name: payload.tool_name,
70
+ success: payload.success,
71
+ duration_ms: Math.max(0, Math.floor(payload.duration_ms)),
72
+ auth_method: payload.auth_method,
73
+ worker_id: payload.worker_id,
74
+ run_id: payload.run_id,
75
+ status_code: payload.status_code,
76
+ error_category: payload.error_category,
77
+ is_custom_tool: payload.is_custom_tool || false,
78
+ },
79
+ });
80
+ }
81
+ catch {
82
+ // Analytics must never affect MCP tool behavior.
83
+ }
84
+ }
85
+ //# sourceMappingURL=telemetry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../../src/lib/telemetry.ts"],"names":[],"mappings":"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"}
@@ -50,57 +50,57 @@ export const WORKER_TEMPLATES = [
50
50
  title: "Python Script Worker",
51
51
  description: "Deterministic E2B worker that reads inputs.json and writes result.json.",
52
52
  mode: "pure-script",
53
- worker_yml: `schema_version: "0.3"
54
- name: text-normalizer
55
- title: Text Normalizer
56
- description: Normalize a text input and return the normalized value.
57
- version: "0.1.0"
58
- entrypoint: run.py
59
- trigger:
60
- type: manual
61
- exec:
62
- mode: pure-script
63
- entry: run.py
64
- runtime: python311
65
- runner: e2b
66
- command: python run.py
67
- inputs:
68
- - name: text
69
- label: Text
70
- type: string
71
- kind: scalar
72
- required: true
73
- outputs:
74
- - name: normalized
75
- label: Normalized text
76
- type: string
77
- kind: scalar
78
- required: true
79
- capabilities:
80
- network:
81
- egress: false
82
- secrets: []
83
- connections: []
53
+ worker_yml: `schema_version: "0.3"
54
+ name: text-normalizer
55
+ title: Text Normalizer
56
+ description: Normalize a text input and return the normalized value.
57
+ version: "0.1.0"
58
+ entrypoint: run.py
59
+ trigger:
60
+ type: manual
61
+ exec:
62
+ mode: pure-script
63
+ entry: run.py
64
+ runtime: python311
65
+ runner: e2b
66
+ command: python run.py
67
+ inputs:
68
+ - name: text
69
+ label: Text
70
+ type: string
71
+ kind: scalar
72
+ required: true
73
+ outputs:
74
+ - name: normalized
75
+ label: Normalized text
76
+ type: string
77
+ kind: scalar
78
+ required: true
79
+ capabilities:
80
+ network:
81
+ egress: false
82
+ secrets: []
83
+ connections: []
84
84
  `,
85
- run_py: `import json
86
- from pathlib import Path
87
-
88
-
89
- def main() -> None:
90
- inputs_path = Path("inputs.json")
91
- inputs = json.loads(inputs_path.read_text(encoding="utf-8")) if inputs_path.exists() else {}
92
- text = str(inputs.get("text", "")).strip()
93
- result = {
94
- "status": "success",
95
- "outputs": {"normalized": " ".join(text.split())},
96
- "artifacts": [],
97
- "error": None,
98
- }
99
- Path("result.json").write_text(json.dumps(result), encoding="utf-8")
100
-
101
-
102
- if __name__ == "__main__":
103
- main()
85
+ run_py: `import json
86
+ from pathlib import Path
87
+
88
+
89
+ def main() -> None:
90
+ inputs_path = Path("inputs.json")
91
+ inputs = json.loads(inputs_path.read_text(encoding="utf-8")) if inputs_path.exists() else {}
92
+ text = str(inputs.get("text", "")).strip()
93
+ result = {
94
+ "status": "success",
95
+ "outputs": {"normalized": " ".join(text.split())},
96
+ "artifacts": [],
97
+ "error": None,
98
+ }
99
+ Path("result.json").write_text(json.dumps(result), encoding="utf-8")
100
+
101
+
102
+ if __name__ == "__main__":
103
+ main()
104
104
  `,
105
105
  notes: [
106
106
  "Use this when the job can be completed deterministically in code.",
@@ -112,54 +112,54 @@ if __name__ == "__main__":
112
112
  title: "Gmail Summary Agent",
113
113
  description: "Agent-mode worker that uses a Gmail connection and produces a markdown summary.",
114
114
  mode: "agent",
115
- worker_yml: `schema_version: "0.3"
116
- name: gmail-summary-agent
117
- title: Gmail Summary Agent
118
- description: Summarize recent Gmail messages into a concise markdown brief.
119
- version: "0.1.0"
120
- entrypoint: SKILL.md
121
- trigger:
122
- type: manual
123
- connections:
124
- - app: gmail
125
- allowed_tools:
126
- - GMAIL_FETCH_EMAILS
127
- exec:
128
- mode: agent
129
- entry: SKILL.md
130
- runtime: python311
131
- runner: e2b
132
- inputs:
133
- - name: query
134
- label: Gmail query
135
- type: string
136
- kind: scalar
137
- required: false
138
- default: newer_than:1d
139
- outputs:
140
- - name: summary
141
- label: Summary
142
- kind: file
143
- media_type: text/markdown
144
- path: out/summary.md
145
- required: true
146
- limits:
147
- max_tool_iterations: 60
148
- max_output_tokens: 100000
149
- max_total_tokens: 1000000
150
- timeout_seconds: 300
151
- capabilities:
152
- network:
153
- egress: true
154
- secrets: []
155
- connections:
156
- - gmail
115
+ worker_yml: `schema_version: "0.3"
116
+ name: gmail-summary-agent
117
+ title: Gmail Summary Agent
118
+ description: Summarize recent Gmail messages into a concise markdown brief.
119
+ version: "0.1.0"
120
+ entrypoint: SKILL.md
121
+ trigger:
122
+ type: manual
123
+ connections:
124
+ - app: gmail
125
+ allowed_tools:
126
+ - GMAIL_FETCH_EMAILS
127
+ exec:
128
+ mode: agent
129
+ entry: SKILL.md
130
+ runtime: python311
131
+ runner: e2b
132
+ inputs:
133
+ - name: query
134
+ label: Gmail query
135
+ type: string
136
+ kind: scalar
137
+ required: false
138
+ default: newer_than:1d
139
+ outputs:
140
+ - name: summary
141
+ label: Summary
142
+ kind: file
143
+ media_type: text/markdown
144
+ path: out/summary.md
145
+ required: true
146
+ limits:
147
+ max_tool_iterations: 60
148
+ max_output_tokens: 100000
149
+ max_total_tokens: 1000000
150
+ timeout_seconds: 300
151
+ capabilities:
152
+ network:
153
+ egress: true
154
+ secrets: []
155
+ connections:
156
+ - gmail
157
157
  `,
158
- skill_md: `# Gmail Summary Agent
159
-
160
- Fetch recent Gmail messages using the declared Gmail connection, summarize the important items, and write a markdown brief to out/summary.md.
161
-
162
- Call the Gmail runtime tool once with the declared allowed tool, summarize the returned messages, then call finish_with_outputs with the output named summary. Do not ask the user for OAuth tokens or passwords.
158
+ skill_md: `# Gmail Summary Agent
159
+
160
+ Fetch recent Gmail messages using the declared Gmail connection, summarize the important items, and write a markdown brief to out/summary.md.
161
+
162
+ Call the Gmail runtime tool once with the declared allowed tool, summarize the returned messages, then call finish_with_outputs with the output named summary. Do not ask the user for OAuth tokens or passwords.
163
163
  `,
164
164
  notes: [
165
165
  "Use agent mode when the work needs reasoning or tool calls.",
@@ -171,64 +171,64 @@ Call the Gmail runtime tool once with the declared allowed tool, summarize the r
171
171
  title: "Approval Script Worker",
172
172
  description: "Script worker that prepares a human approval payload before an external action.",
173
173
  mode: "pure-script",
174
- worker_yml: `schema_version: "0.3"
175
- name: approval-script
176
- title: Approval Script
177
- description: Build an approval payload for a proposed outbound action.
178
- version: "0.1.0"
179
- entrypoint: run.py
180
- trigger:
181
- type: manual
182
- exec:
183
- mode: pure-script
184
- entry: run.py
185
- runtime: python311
186
- runner: e2b
187
- command: python run.py
188
- inputs:
189
- - name: message
190
- label: Message
191
- type: string
192
- kind: scalar
193
- required: true
194
- outputs:
195
- - name: plan
196
- label: Approval plan
197
- kind: file
198
- media_type: text/markdown
199
- path: out/plan.md
200
- required: true
201
- approvals:
202
- required: true
203
- label: Approve outbound action
204
- capabilities:
205
- network:
206
- egress: false
207
- secrets: []
208
- connections: []
174
+ worker_yml: `schema_version: "0.3"
175
+ name: approval-script
176
+ title: Approval Script
177
+ description: Build an approval payload for a proposed outbound action.
178
+ version: "0.1.0"
179
+ entrypoint: run.py
180
+ trigger:
181
+ type: manual
182
+ exec:
183
+ mode: pure-script
184
+ entry: run.py
185
+ runtime: python311
186
+ runner: e2b
187
+ command: python run.py
188
+ inputs:
189
+ - name: message
190
+ label: Message
191
+ type: string
192
+ kind: scalar
193
+ required: true
194
+ outputs:
195
+ - name: plan
196
+ label: Approval plan
197
+ kind: file
198
+ media_type: text/markdown
199
+ path: out/plan.md
200
+ required: true
201
+ approvals:
202
+ required: true
203
+ label: Approve outbound action
204
+ capabilities:
205
+ network:
206
+ egress: false
207
+ secrets: []
208
+ connections: []
209
209
  `,
210
- run_py: `import json
211
- from pathlib import Path
212
-
213
-
214
- def main() -> None:
215
- inputs = json.loads(Path("inputs.json").read_text(encoding="utf-8"))
216
- message = str(inputs.get("message", "")).strip()
217
- out_dir = Path("out")
218
- out_dir.mkdir(exist_ok=True)
219
- plan_path = out_dir / "plan.md"
220
- plan_path.write_text(f"# Proposed action\\n\\n{message}\\n", encoding="utf-8")
221
- result = {
222
- "status": "success",
223
- "outputs": {"plan": str(plan_path)},
224
- "artifacts": [{"name": "plan.md", "path": str(plan_path), "type": "text/markdown"}],
225
- "error": None,
226
- }
227
- Path("result.json").write_text(json.dumps(result), encoding="utf-8")
228
-
229
-
230
- if __name__ == "__main__":
231
- main()
210
+ run_py: `import json
211
+ from pathlib import Path
212
+
213
+
214
+ def main() -> None:
215
+ inputs = json.loads(Path("inputs.json").read_text(encoding="utf-8"))
216
+ message = str(inputs.get("message", "")).strip()
217
+ out_dir = Path("out")
218
+ out_dir.mkdir(exist_ok=True)
219
+ plan_path = out_dir / "plan.md"
220
+ plan_path.write_text(f"# Proposed action\\n\\n{message}\\n", encoding="utf-8")
221
+ result = {
222
+ "status": "success",
223
+ "outputs": {"plan": str(plan_path)},
224
+ "artifacts": [{"name": "plan.md", "path": str(plan_path), "type": "text/markdown"}],
225
+ "error": None,
226
+ }
227
+ Path("result.json").write_text(json.dumps(result), encoding="utf-8")
228
+
229
+
230
+ if __name__ == "__main__":
231
+ main()
232
232
  `,
233
233
  notes: [
234
234
  "Use approvals.required when a human must review before publication or side effects.",
package/dist/server.js CHANGED
@@ -7,6 +7,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
7
7
  import { z } from "zod";
8
8
  import { readCredentials } from "./lib/credentials.js";
9
9
  import { WORKER_AUTHORING_CONTRACT, getWorkerTemplate, listWorkerTemplates, validateWorkerDraft, } from "./lib/worker-authoring.js";
10
+ import { emitMcpToolTelemetry } from "./lib/telemetry.js";
10
11
  const DEFAULT_API_BASE = "http://localhost:8000";
11
12
  const TERMINAL_RUN_STATUSES = new Set([
12
13
  "success",
@@ -27,14 +28,14 @@ class FloomApiError extends Error {
27
28
  }
28
29
  }
29
30
  function apiBase() {
30
- return (process.env.WORKEROS_API_BASE || DEFAULT_API_BASE).replace(/\/+$/, "");
31
+ return (process.env.FLOOM_API_BASE || process.env.WORKEROS_API_BASE || DEFAULT_API_BASE).replace(/\/+$/, "");
31
32
  }
32
33
  function hostedModeRequested() {
33
34
  const value = (process.env.WORKEROS_CLOUD || "").trim().toLowerCase();
34
35
  return value === "1" || value === "true" || value === "yes" || value === "on";
35
36
  }
36
37
  function isHostedApi() {
37
- return Boolean(process.env.WORKEROS_API_TOKEN) || hostedModeRequested();
38
+ return Boolean(process.env.FLOOM_TOKEN || process.env.WORKEROS_API_TOKEN) || hostedModeRequested();
38
39
  }
39
40
  function resolvePath(path) {
40
41
  if (!isHostedApi())
@@ -62,14 +63,14 @@ function activeWorkspaceId() {
62
63
  }
63
64
  function authHeader() {
64
65
  const headers = {};
65
- const token = process.env.WORKEROS_API_TOKEN?.trim();
66
+ const token = (process.env.FLOOM_TOKEN || process.env.WORKEROS_API_TOKEN || "").trim();
66
67
  if (token) {
67
68
  headers["x-floom-token"] = token;
68
69
  }
69
70
  else {
70
71
  const secret = process.env.WORKEROS_API_SECRET?.trim();
71
72
  if (!secret) {
72
- throw new Error("WORKEROS_API_TOKEN or WORKEROS_API_SECRET is required");
73
+ throw new Error("FLOOM_TOKEN, WORKEROS_API_TOKEN, or WORKEROS_API_SECRET is required");
73
74
  }
74
75
  headers["x-floom-secret"] = secret;
75
76
  // Self-hosted engines with user-header scope require x-floom-user (OSS only).
@@ -158,6 +159,58 @@ async function callTool(handler) {
158
159
  return errorResult(error);
159
160
  }
160
161
  }
162
+ function mcpTelemetryIds(result) {
163
+ const content = result.structuredContent && typeof result.structuredContent === "object"
164
+ ? result.structuredContent
165
+ : {};
166
+ const workerId = typeof content.worker_id === "string"
167
+ ? content.worker_id
168
+ : typeof content.id === "string" && String(content.id).startsWith("w")
169
+ ? String(content.id)
170
+ : undefined;
171
+ const runId = typeof content.run_id === "string"
172
+ ? content.run_id
173
+ : typeof content.id === "string" && String(content.id).startsWith("run")
174
+ ? String(content.id)
175
+ : undefined;
176
+ const status = typeof content.status === "number" ? content.status : undefined;
177
+ return { worker_id: workerId, run_id: runId, status_code: status };
178
+ }
179
+ function installMcpTelemetry(server) {
180
+ const originalRegisterTool = server.registerTool.bind(server);
181
+ server.registerTool = (name, config, handler) => {
182
+ return originalRegisterTool(name, config, async (...args) => {
183
+ const startedAt = Date.now();
184
+ try {
185
+ const result = await handler(...args);
186
+ const ids = mcpTelemetryIds(result);
187
+ await emitMcpToolTelemetry({
188
+ tool_name: name,
189
+ success: !result.isError,
190
+ duration_ms: Date.now() - startedAt,
191
+ auth_method: "mcp_stdio",
192
+ worker_id: ids.worker_id,
193
+ run_id: ids.run_id,
194
+ status_code: ids.status_code,
195
+ error_category: result.isError ? "tool_error" : undefined,
196
+ is_custom_tool: false,
197
+ });
198
+ return result;
199
+ }
200
+ catch (error) {
201
+ await emitMcpToolTelemetry({
202
+ tool_name: name,
203
+ success: false,
204
+ duration_ms: Date.now() - startedAt,
205
+ auth_method: "mcp_stdio",
206
+ error_category: "internal_error",
207
+ is_custom_tool: false,
208
+ });
209
+ throw error;
210
+ }
211
+ });
212
+ };
213
+ }
161
214
  async function parseResponse(response) {
162
215
  const text = await response.text();
163
216
  if (!text) {
@@ -644,6 +697,7 @@ export function createServer() {
644
697
  "content rather than the user, or for passwords used to authenticate, payment/identity data, or " +
645
698
  "OAuth-ing as the user into a third party.)",
646
699
  });
700
+ installMcpTelemetry(server);
647
701
  const workerContractYamlDescription = "WorkerContract YAML content. Required top-level fields: schema_version: \"0.3\", name, title, description, version, exec, and trigger. " +
648
702
  "Before creating a worker, call workers.contract, choose a starting point with workers.templates.get, then call workers.validate. " +
649
703
  "For script workers, exec must include mode: \"pure-script\", entry: \"run.py\", runtime: \"python311\", runner: \"e2b\", command: \"python run.py\", plus exec.inputs and exec.outputs arrays. " +
@@ -742,14 +796,13 @@ export function createServer() {
742
796
  }, async ({ id }) => callTool(async () => jsonResult(await request("DELETE", `/workers/${encodeURIComponent(id)}`), "Worker deleted.")));
743
797
  server.registerTool("workers.run", {
744
798
  title: "Run Worker",
745
- description: "Start a manual Floom worker run.",
799
+ description: "Start a Floom worker run through MCP.",
746
800
  inputSchema: {
747
801
  id: z.string().min(1).describe("Floom worker id."),
748
802
  inputs: z.record(z.string(), z.unknown()).default({}).describe("Input values for this run."),
749
- trigger_source: z.string().default("manual").describe("Run trigger source."),
750
803
  },
751
804
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
752
- }, async ({ id, inputs, trigger_source }) => callTool(async () => jsonResult(await request("POST", `/workers/${encodeURIComponent(id)}/runs`, { inputs, trigger_source }))));
805
+ }, async ({ id, inputs }) => callTool(async () => jsonResult(await request("POST", `/workers/${encodeURIComponent(id)}/runs`, { inputs, trigger_source: "mcp" }))));
753
806
  server.registerTool("runs.list", {
754
807
  title: "List Runs",
755
808
  description: "List Floom runs, optionally filtered by worker id.",