saasfunnels 0.2.0 → 0.2.2

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 CHANGED
@@ -10,10 +10,10 @@ This repository contains only the MIT-licensed CLI and its bounded runtime contr
10
10
 
11
11
  ## Install
12
12
 
13
- After the beta is published:
13
+ Install the stable CLI:
14
14
 
15
15
  ```bash
16
- npm install --global saasfunnels@beta
16
+ npm install --global saasfunnels
17
17
  ```
18
18
 
19
19
  ## Use
@@ -24,12 +24,57 @@ saasfunnels verify --json
24
24
  saasfunnels mcp serve
25
25
  ```
26
26
 
27
+ ### Plans and pricing
28
+
29
+ ```bash
30
+ saasfunnels plans discover # propose files, no network call
31
+ saasfunnels plans discover --apply # write .saasfunnels/plan-sources.json
32
+ saasfunnels plans handoff --integration-id <id> \
33
+ --repository-key <repo> --repository-revision <sha> --send
34
+ ```
35
+
36
+ `plans discover` reads the working tree and proposes files that define plans and
37
+ prices. Files that reference a Stripe price are reported as evidence; files that
38
+ only look commercial by name and content are reported separately as guesses.
39
+ Nothing leaves the machine until `.saasfunnels/plan-sources.json` lists the files
40
+ and `plans handoff --send` is run, because plan mapping receives file contents
41
+ rather than the `file:line` references a feature manifest sends. Commit that file
42
+ so the approved set is reviewable.
43
+
44
+ ### GitHub Action
45
+
46
+ ```yaml
47
+ - uses: LeadEngine-ai/saasfunnels-cli/action@v0.2.1
48
+ with:
49
+ api-key: ${{ secrets.SAASFUNNELS_API_KEY }}
50
+ cli-version: 0.2.1 # pin it; a floating version rebaselines drift
51
+ discovery-roots: app,lib
52
+ ```
53
+
54
+ On a pull request the Action scans as a `candidate`: the result is compared
55
+ against the branch's baseline and discarded. Only the default branch advances
56
+ the lineage. Changing `discovery-roots` starts a new lineage, so drift is
57
+ measured against a comparable scan rather than a wider or narrower one.
58
+
59
+ Plan and pricing upload is opt-in:
60
+
61
+ ```yaml
62
+ plan-sources: "true"
63
+ integration-id: <stripe integration id>
64
+ ```
65
+
66
+ It runs only on the default branch, requires a committed
67
+ `.saasfunnels/plan-sources.json`, and fails with an explanation rather than
68
+ uploading anything if that file is missing.
69
+
27
70
  Local validation does not require credentials. Live reads use `SAASFUNNELS_API_KEY`; Direct API smoke events use `SAASFUNNELS_INGEST_API_KEY`. Set `SAASFUNNELS_API_BASE_URL` to target a controlled preview or the current SaaSFunnels host.
28
71
 
29
72
  Hosted interactive MCP uses Clerk OAuth at the application `/mcp` URL and does not require this CLI or a copied API key.
30
73
 
31
74
  The application imports the exact versioned MCP registry through the server-side `saasfunnels/library` export. This keeps hosted Streamable HTTP and local stdio MCP on one package owner; it is not a browser API.
32
75
 
76
+ In hosted OAuth, `list_workspaces` discovers accessible workspace names and IDs. Live read, inspect, and plan tools accept an optional `workspace_id`. The hosted application must select the sole accessible workspace automatically, require an explicit ID when several are accessible, recheck authorization for every call, and include the selected workspace name and ID in each live response. The application passes the selection through to its developer API as the `workspace_id` query parameter. Local stdio API-key tools do not expose workspace selection; their key stays pinned to its workspace.
77
+
33
78
  ## Development
34
79
 
35
80
  ```bash
package/dist/library.js CHANGED
@@ -24260,7 +24260,7 @@ function validateFeatureCatalogManifest(value) {
24260
24260
 
24261
24261
  // src/identity.ts
24262
24262
  var SAASFUNNELS_CLI_NAME = "saasfunnels";
24263
- var SAASFUNNELS_CLI_VERSION = "0.1.0-beta.1";
24263
+ var SAASFUNNELS_CLI_VERSION = "0.2.2";
24264
24264
  var SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
24265
24265
  var SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
24266
24266
  var SAASFUNNELS_ENV = Object.freeze({
@@ -24299,6 +24299,7 @@ var saasFunnelsMcpToolCapabilityIds = {
24299
24299
  get_integration_health: "developer.workspace.read",
24300
24300
  get_signal_payload_preview: "developer.workspace.read",
24301
24301
  get_workspace_readiness: "developer.workspace.read",
24302
+ list_workspaces: "developer.workspace.read",
24302
24303
  inspect_funnel_entry: "funnel.entry.inspect",
24303
24304
  list_mapping_gaps: "developer.workspace.read",
24304
24305
  list_recent_signals: "developer.workspace.read",
@@ -24399,7 +24400,7 @@ function objectArg(args, name) {
24399
24400
  function sanitizeString(value) {
24400
24401
  const withoutSecrets = value.replace(
24401
24402
  tokenPattern,
24402
- (candidate) => /^[A-Z0-9_]+$/.test(candidate) ? candidate : "[redacted-token]"
24403
+ (candidate) => /^[A-Z0-9_]+$/.test(candidate) || /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(candidate) ? candidate : "[redacted-token]"
24403
24404
  ).replace(emailPattern3, "[redacted-email]");
24404
24405
  try {
24405
24406
  const parsed = new URL(
@@ -24668,6 +24669,25 @@ function emptyLiveInputSchema() {
24668
24669
  type: "object"
24669
24670
  };
24670
24671
  }
24672
+ var hostedWorkspaceToolNames = /* @__PURE__ */ new Set([
24673
+ "get_integration_health",
24674
+ "get_workspace_readiness",
24675
+ "list_mapping_gaps",
24676
+ "list_recent_signals",
24677
+ "get_signal_payload_preview",
24678
+ "inspect_funnel_entry",
24679
+ "propose_funnel_entry_installation",
24680
+ "get_account_strategy",
24681
+ "simulate_account_funnel_fit"
24682
+ ]);
24683
+ function developerPathForWorkspace(path, args, options) {
24684
+ if (!options.hostedMode) return path;
24685
+ if ("workspace_id" in args && !stringArg(args, "workspace_id")) {
24686
+ throw new McpProtocolError(-32602, "workspace_id must be a non-empty string.");
24687
+ }
24688
+ const workspaceId = stringArg(args, "workspace_id");
24689
+ return workspaceId ? `${path}${path.includes("?") ? "&" : "?"}workspace_id=${encodeURIComponent(workspaceId)}` : path;
24690
+ }
24671
24691
  function saasFunnelsMcpToolDefinitions({
24672
24692
  allowFunnelTests = false,
24673
24693
  allowFunnelWrites = false,
@@ -25030,6 +25050,29 @@ function hostedSaaSFunnelsMcpToolDefinitions() {
25030
25050
  capability2 && agentCapabilityAllowedAtBoundary(capability2.id, "hosted_mcp")
25031
25051
  );
25032
25052
  });
25053
+ for (const tool of tools) {
25054
+ if (!hostedWorkspaceToolNames.has(tool.name)) continue;
25055
+ const properties = tool.inputSchema.properties;
25056
+ properties.workspace_id = {
25057
+ description: "Workspace ID from list_workspaces. Required when your OAuth identity can access multiple workspaces; optional when it can access one. The server verifies access for every call.",
25058
+ type: "string"
25059
+ };
25060
+ }
25061
+ tools.push({
25062
+ annotations: {
25063
+ idempotentHint: true,
25064
+ openWorldHint: true,
25065
+ readOnlyHint: true
25066
+ },
25067
+ description: "List the names and IDs of workspaces accessible to the OAuth user. Use a workspace ID from this list to target live tools when more than one workspace is accessible.",
25068
+ inputSchema: {
25069
+ additionalProperties: false,
25070
+ properties: {},
25071
+ type: "object"
25072
+ },
25073
+ name: "list_workspaces",
25074
+ title: "List Workspaces"
25075
+ });
25033
25076
  assertMcpToolCapabilityContract(tools, "hosted_mcp");
25034
25077
  return tools;
25035
25078
  }
@@ -25055,7 +25098,7 @@ async function developerGet(path, args, options) {
25055
25098
  });
25056
25099
  }
25057
25100
  const response = await (options.fetch ?? fetch)(
25058
- `${apiBaseUrl(options, args)}${path}`,
25101
+ `${apiBaseUrl(options, args)}${developerPathForWorkspace(path, args, options)}`,
25059
25102
  {
25060
25103
  headers: {
25061
25104
  authorization
@@ -25091,7 +25134,7 @@ async function developerPost(path, args, body, options, requiredScope) {
25091
25134
  });
25092
25135
  }
25093
25136
  const response = await (options.fetch ?? fetch)(
25094
- `${apiBaseUrl(options, args)}${path}`,
25137
+ `${apiBaseUrl(options, args)}${developerPathForWorkspace(path, args, options)}`,
25095
25138
  {
25096
25139
  body: JSON.stringify(body),
25097
25140
  headers: {
@@ -25161,11 +25204,17 @@ async function sendTestEvent(args, options) {
25161
25204
  }
25162
25205
  async function callSaaSFunnelsMcpTool(name, args, options = {}) {
25163
25206
  try {
25164
- if ((name === "get_account_strategy" || name === "simulate_account_funnel_fit") && !options.hostedMode) {
25207
+ if (!options.hostedMode && "workspace_id" in args && hostedWorkspaceToolNames.has(name)) {
25208
+ throw new McpProtocolError(
25209
+ -32602,
25210
+ "workspace_id is available only with hosted OAuth; an API key is pinned to its workspace."
25211
+ );
25212
+ }
25213
+ if ((name === "get_account_strategy" || name === "simulate_account_funnel_fit" || name === "list_workspaces") && !options.hostedMode) {
25165
25214
  throw new McpToolExecutionError(
25166
- "This account tool requires hosted OAuth.",
25215
+ "This tool requires hosted OAuth.",
25167
25216
  {
25168
- error: "This account tool requires hosted OAuth.",
25217
+ error: "This tool requires hosted OAuth.",
25169
25218
  ok: false,
25170
25219
  required_boundary: "hosted_mcp"
25171
25220
  }
@@ -25208,6 +25257,10 @@ async function callSaaSFunnelsMcpTool(name, args, options = {}) {
25208
25257
  options
25209
25258
  )
25210
25259
  );
25260
+ if (name === "list_workspaces")
25261
+ return structuredToolResult(
25262
+ await developerGet("/api/developer-tools/workspaces", {}, options)
25263
+ );
25211
25264
  if (name === "get_workspace_readiness")
25212
25265
  return structuredToolResult(
25213
25266
  await developerGet(