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 +47 -2
- package/dist/library.js +60 -7
- package/dist/saasfunnels.js +755 -84
- package/dist/types/feature-setup.d.ts +3 -0
- package/dist/types/identity.d.ts +1 -1
- package/dist/types/mcp.d.ts +2 -1
- package/package.json +3 -2
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
|
-
|
|
13
|
+
Install the stable CLI:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm install --global saasfunnels
|
|
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.
|
|
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 (
|
|
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
|
|
25215
|
+
"This tool requires hosted OAuth.",
|
|
25167
25216
|
{
|
|
25168
|
-
error: "This
|
|
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(
|