@usefillo/cli 0.23.1 → 0.24.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.
package/dist/index.js CHANGED
@@ -104,7 +104,7 @@ function printTable(header, rows) {
104
104
  for (const cells of rows) console.log(line(cells));
105
105
  }
106
106
  var dateOnly = (iso) => iso.slice(0, 10);
107
- var plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
107
+ var plural = (count2, noun) => `${count2} ${noun}${count2 === 1 ? "" : "s"}`;
108
108
  function die(msg) {
109
109
  if (jsonOutput) emitResult({ error: terminalText(msg) });
110
110
  console.error(`${failMark()} ${terminalText(msg)}`);
@@ -1600,13 +1600,185 @@ var agentCommand = {
1600
1600
  help: agentHelp
1601
1601
  };
1602
1602
 
1603
+ // src/commands/billing.ts
1604
+ var VOLUMES = [
1605
+ { tier: "5k", responses: 5e3 },
1606
+ { tier: "25k", responses: 25e3 },
1607
+ { tier: "100k", responses: 1e5 }
1608
+ ];
1609
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1610
+ var count = (value) => new Intl.NumberFormat("en-US").format(value);
1611
+ function parseDate(iso) {
1612
+ if (typeof iso !== "string" || !iso) return null;
1613
+ const date2 = new Date(iso);
1614
+ return Number.isNaN(date2.getTime()) ? null : date2;
1615
+ }
1616
+ var longDate = (date2) => `${date2.getUTCDate()} ${MONTHS[date2.getUTCMonth()]} ${date2.getUTCFullYear()}`;
1617
+ var shortDate = (date2) => `${date2.getUTCDate()} ${MONTHS[date2.getUTCMonth()]}`;
1618
+ function periodLabel(period, currentYear) {
1619
+ const match = /^(\d{4})-(\d{2})$/.exec(period);
1620
+ const month = match ? MONTHS[Number(match[2]) - 1] : void 0;
1621
+ if (!match || !month) return terminalText(period);
1622
+ return match[1] === currentYear ? month : `${month} ${match[1]}`;
1623
+ }
1624
+ function daysLeft(end, now = Date.now()) {
1625
+ const days = Math.max(0, Math.ceil((end.getTime() - now) / 864e5));
1626
+ return days === 0 ? "ends today" : days === 1 ? "1 day left" : `${days} days left`;
1627
+ }
1628
+ var tierName = (tier) => tier ? `Everything ${terminalText(tier)}` : "Everything";
1629
+ function planLine(body) {
1630
+ if (body.billingEnabled === false) {
1631
+ return body.plan === "free" ? "Free" : "Everything";
1632
+ }
1633
+ switch (body.plan) {
1634
+ case "free":
1635
+ return `Free \xB7 ${count(body.allowance)} responses a month`;
1636
+ case "trial": {
1637
+ const end = parseDate(body.trialEndsAt);
1638
+ const ends = end ? ` \xB7 ends ${longDate(end)} (${daysLeft(end)})` : "";
1639
+ return `Everything trial \xB7 ${count(body.allowance)} responses${ends}`;
1640
+ }
1641
+ case "complimentary":
1642
+ return `${tierName(body.tier)} \xB7 complimentary (no charge, no renewal date)`;
1643
+ case "subscribed": {
1644
+ const billed = body.interval === "year" ? "billed yearly" : "billed monthly";
1645
+ const end = parseDate(body.currentPeriodEnd);
1646
+ let next = "";
1647
+ if (body.status === "past_due") next = " \xB7 payment failed";
1648
+ else if (body.cancelAtPeriodEnd) next = end ? ` \xB7 cancels ${longDate(end)}` : " \xB7 canceling";
1649
+ else if (end)
1650
+ next = `${body.status === "trialing" ? " \xB7 first payment" : " \xB7 renews"} ${longDate(end)}`;
1651
+ return `${tierName(body.tier)} \xB7 ${billed}${next}`;
1652
+ }
1653
+ default:
1654
+ return terminalText(body.plan);
1655
+ }
1656
+ }
1657
+ function usageLine(body) {
1658
+ const { responses: responses2 } = body.usage;
1659
+ const reset = parseDate(body.usage.resetsAt);
1660
+ const resets = reset ? ` \xB7 resets ${shortDate(reset)}` : "";
1661
+ if (!(body.allowance > 0)) return `${count(responses2)} responses this month${resets}`;
1662
+ const percent = Math.floor(responses2 / body.allowance * 100);
1663
+ return `${count(responses2)} of ${count(body.allowance)} responses this month (${percent}%)${resets}`;
1664
+ }
1665
+ function historyLine(body) {
1666
+ const currentYear = body.usage.period?.slice(0, 4);
1667
+ const earlier = (body.history ?? []).filter(
1668
+ (row) => typeof row.period === "string" && row.period !== body.usage.period && typeof row.responses === "number"
1669
+ );
1670
+ if (!earlier.some((row) => row.responses > 0)) return null;
1671
+ return earlier.map((row) => `${periodLabel(row.period, currentYear)} ${count(row.responses)}`).join(" \xB7 ");
1672
+ }
1673
+ function manageUrl(body) {
1674
+ try {
1675
+ const url = new URL(body.manageUrl ?? "");
1676
+ if (url.protocol === "https:" || url.protocol === "http:") return terminalText(url.href);
1677
+ } catch {
1678
+ }
1679
+ return `${terminalText(API)}/settings/plan`;
1680
+ }
1681
+ function fittingVolume(body) {
1682
+ return VOLUMES.find(
1683
+ (volume) => volume.responses > body.allowance && volume.responses >= body.usage.responses
1684
+ );
1685
+ }
1686
+ function notes(body) {
1687
+ const lines = [];
1688
+ const billing2 = body.billingEnabled !== false;
1689
+ if (billing2 && body.plan === "subscribed" && body.status === "past_due") {
1690
+ lines.push(
1691
+ "The last payment failed. Stripe is retrying it, and Everything stays on meanwhile.",
1692
+ "An owner or admin updates the card from Manage billing at the link above."
1693
+ );
1694
+ } else if (billing2 && body.plan === "subscribed" && body.cancelAtPeriodEnd) {
1695
+ lines.push(
1696
+ "Everything stays active until the period ends; the workspace returns to Free after that."
1697
+ );
1698
+ } else if (billing2 && body.plan === "trial") {
1699
+ lines.push("When the trial ends without a subscription, the workspace returns to Free.");
1700
+ }
1701
+ if (body.allowance > 0) {
1702
+ const ratio = body.usage.responses / body.allowance;
1703
+ if (ratio >= 0.8) {
1704
+ lines.push(
1705
+ ratio >= 1 ? `${body.usage.responses > body.allowance ? "Past this month's allowance." : "This month's allowance is used up."} Forms keep collecting and nothing is dropped.` : `${Math.floor(ratio * 100)}% of this month's allowance is used. Forms keep collecting if it runs over.`
1706
+ );
1707
+ if (billing2) {
1708
+ const next = fittingVolume(body);
1709
+ if (next) {
1710
+ lines.push(
1711
+ `${ratio >= 1 ? "The volume that fits" : "The next volume"} is Everything ${next.tier} (${count(next.responses)} a month).`
1712
+ );
1713
+ } else {
1714
+ lines.push(
1715
+ `For steady volume above ${count(body.allowance)} a month, write to hello@fillo.so.`
1716
+ );
1717
+ }
1718
+ }
1719
+ }
1720
+ }
1721
+ if (!billing2) lines.push("Billing isn't enabled on this Fillo server.");
1722
+ return lines;
1723
+ }
1724
+ async function billing(subcommand, flags) {
1725
+ if (subcommand === "help") return billingHelp();
1726
+ if (subcommand !== void 0) {
1727
+ die(`Unknown billing command: ${subcommand} (fillo billing takes no arguments).`);
1728
+ }
1729
+ const body = await callApi(
1730
+ "/cli/workspace/billing",
1731
+ {},
1732
+ {
1733
+ fallback: failed("billing"),
1734
+ expect: (b) => typeof b.plan === "string" && typeof b.allowance === "number" && typeof b.usage?.responses === "number"
1735
+ }
1736
+ );
1737
+ if (jsonMode(flags)) return emitResult(body);
1738
+ const rows = [
1739
+ ["Plan:", planLine(body)],
1740
+ ["Usage:", usageLine(body)]
1741
+ ];
1742
+ const earlier = historyLine(body);
1743
+ if (earlier) rows.push(["Earlier:", earlier]);
1744
+ if (body.billingEnabled !== false) rows.push(["Manage:", `${manageUrl(body)} (owner or admin)`]);
1745
+ console.log("");
1746
+ for (const [label, value] of rows) console.log(` ${label.padEnd(9)}${value}`);
1747
+ const extra = notes(body);
1748
+ if (extra.length > 0) {
1749
+ console.log("");
1750
+ for (const line of extra) console.log(` ${dim(line)}`);
1751
+ }
1752
+ console.log("");
1753
+ }
1754
+ function billingHelp() {
1755
+ console.log(`
1756
+ ${boldRaw("fillo billing")} \u2014 the workspace's plan and this month's response usage
1757
+
1758
+ ${boldRaw("Usage")}
1759
+ billing Print the plan, usage against the monthly allowance,
1760
+ recent months, and the Manage link
1761
+
1762
+ ${dimRaw("Read-only. Needs an owner or admin login. Subscribing, volume changes,")}
1763
+ ${dimRaw("cards, and invoices are a human click in Settings \u2192 Billing & plan (the")}
1764
+ ${dimRaw("printed Manage link). Allowances are soft: forms keep collecting past them.")}
1765
+ ${dimRaw("--json prints the raw server response on stdout.")}
1766
+ `);
1767
+ }
1768
+ var billingCommand = {
1769
+ name: "billing",
1770
+ flags: [],
1771
+ run: (args, flags) => billing(args[0], flags),
1772
+ help: billingHelp
1773
+ };
1774
+
1603
1775
  // src/commands/branding.ts
1604
1776
  function printState(body) {
1605
1777
  const plan = body.plan === "pro" ? "Everything" : "Free";
1606
1778
  console.log(` Plan: ${plan}`);
1607
1779
  console.log(` Badge: ${body.poweredBy ? "shown" : "hidden"}`);
1608
1780
  if (body.canHide === false) {
1609
- console.log(dim(" Turning it off needs the Everything plan (Settings \u2192 Plan)."));
1781
+ console.log(dim(" Turning it off needs the Everything plan (Settings \u2192 Billing & plan)."));
1610
1782
  }
1611
1783
  }
1612
1784
  async function branding(subcommand, flags) {
@@ -1639,8 +1811,9 @@ function brandingHelp() {
1639
1811
  branding off Hide the badge on Fillo-rendered forms (Everything plan)
1640
1812
  branding on Show the badge again
1641
1813
 
1642
- ${dimRaw("Hiding needs the Everything plan; a workspace owner or admin selects")}
1643
- ${dimRaw("that in Settings \u2192 Plan. Headless embeds never show a badge.")}
1814
+ ${dimRaw("Hiding needs the Everything plan: a workspace owner or admin subscribes")}
1815
+ ${dimRaw("in Settings \u2192 Billing & plan.")}
1816
+ ${dimRaw("The CLI never buys the plan. Headless embeds never show a badge.")}
1644
1817
  ${dimRaw("--json prints the raw server response on stdout.")}
1645
1818
  `);
1646
1819
  }
@@ -4711,7 +4884,7 @@ var schemaShape = object({
4711
4884
  });
4712
4885
  var MAX_SCHEMA_VERSION = 1;
4713
4886
  var FILLO_SCHEMA_VERSION = 1;
4714
- var FILLO_SDK_VERSION = true ? "0.23.1" : "0.0.0-dev";
4887
+ var FILLO_SDK_VERSION = true ? "0.24.1" : "0.0.0-dev";
4715
4888
  function str(value, max, fallback = "") {
4716
4889
  return typeof value === "string" ? value.trim().slice(0, max) : fallback;
4717
4890
  }
@@ -7461,7 +7634,7 @@ async function summary(handle, flags) {
7461
7634
  );
7462
7635
  const distribution = Object.entries(field2.distribution ?? {});
7463
7636
  if (distribution.length > 0) {
7464
- const shown = distribution.slice(0, 5).map(([label, count]) => `${terminalText(label)} \u2014 ${count}`).join(" \xB7 ");
7637
+ const shown = distribution.slice(0, 5).map(([label, count2]) => `${terminalText(label)} \u2014 ${count2}`).join(" \xB7 ");
7465
7638
  const rest = distribution.length - Math.min(distribution.length, 5);
7466
7639
  console.log(` ${shown}${rest > 0 ? dim(` +${rest} more`) : ""}`);
7467
7640
  }
@@ -8790,10 +8963,10 @@ async function origins(subcommand, flags) {
8790
8963
  body.origins.length === 0 ? ` ${okMark()} Cleared the allow-list \u2014 any origin may stage with the publishable key.` : ` ${okMark()} Allowed origins: ${terminalText(body.origins.join(", "))}.`
8791
8964
  );
8792
8965
  }
8793
- function printProtectedForms(count) {
8794
- if (count === 0) return;
8966
+ function printProtectedForms(count2) {
8967
+ if (count2 === 0) return;
8795
8968
  console.log(
8796
- ` ${dim(`${count} ${count === 1 ? "form requires" : "forms require"} verified respondents.`)}`
8969
+ ` ${dim(`${count2} ${count2 === 1 ? "form requires" : "forms require"} verified respondents.`)}`
8797
8970
  );
8798
8971
  }
8799
8972
  async function identityStatus(flags) {
@@ -9171,6 +9344,7 @@ var REGISTRY = [
9171
9344
  webhooksCommand,
9172
9345
  settingsCommand,
9173
9346
  brandingCommand,
9347
+ billingCommand,
9174
9348
  membersCommand,
9175
9349
  workspaceCommand,
9176
9350
  syncTokensCommand,
@@ -9262,6 +9436,7 @@ function help() {
9262
9436
  ${dimRaw("--auth none|bearer|x-api-key secret via prompt or env, never argv")}
9263
9437
  settings <cmd> A form's operational settings: get, set key=value
9264
9438
  branding [on|off] The workspace's "Powered by Fillo" badge (off needs Everything)
9439
+ billing Plan and this month's response usage (read-only)
9265
9440
  members <cmd> Workspace members: list, invite, cancel-invite, role, remove
9266
9441
  workspace rename Rename the workspace (ids, keys, and URLs stay put)
9267
9442
  sync-tokens <cmd> Stage-only deploy credentials: list, create, revoke
@@ -160,17 +160,26 @@ could not be verified.
160
160
  `"light"` or `"dark"`. Use `"auto"` only for a deliberately OS-driven page.
161
161
  Verify every theme the host exposes, at desktop and narrow widths.
162
162
  - The "Powered by Fillo" badge is a server-driven workspace checkbox: always
163
- visible on Free, hideable on the paid plan (default stays visible until
164
- someone turns it off), and never present in a fully headless layout
163
+ visible on Free, hideable on the paid Everything plan (default stays visible
164
+ until someone turns it off), and never present in a fully headless layout
165
165
  (headless is free on every plan). Never hide or obscure it with CSS, DOM
166
166
  edits, or style overrides — on Free that violates Fillo's terms. When the
167
- user asks to remove it: with a CLI login on the paid plan, run
167
+ user asks to remove it: with a CLI login on Everything, run
168
168
  `npx @usefillo/cli@latest branding off` (`branding` alone prints the state,
169
169
  `branding on` restores it). On Free, present the two honest paths and let
170
- the user choose: select the paid plan in Fillo Settings → Plan (a human
171
- clicks — the page explains pre-billing selection; never select a plan on the
172
- user's behalf), or rebuild the embed headless with the host's own
173
- components. Do not bring up plans unprompted.
170
+ the user choose: a workspace owner or admin subscribes to Everything in
171
+ Fillo Settings → Billing & plan, or rebuild the embed headless with the
172
+ host's own components. Do not bring up plans unprompted.
173
+ - Billing is a human's decision. Every plan includes every feature; Everything
174
+ is real paid billing through Stripe checkout for more monthly responses (5k,
175
+ 25k, or 100k; Free is 200) or a hidden badge. Never select a plan, change a
176
+ volume, or open checkout on the user's behalf. When the user asks about
177
+ their plan or usage, run `npx @usefillo/cli@latest billing` (owner or admin
178
+ login; `--json` for the raw result) or the MCP `fillo_get_billing` tool,
179
+ report the plan and this month's usage against the allowance, and hand the
180
+ human the printed Manage link. Allowances are soft: forms never stop
181
+ collecting over them, so never describe going over as losing responses. For
182
+ prices, read `https://fillo.so/pricing.md` rather than quoting from memory.
174
183
  - Use `onSubmitted` only for host-side follow-up after Fillo stores the
175
184
  response. Use a webhook when another backend needs durable delivery.
176
185
  - When the user asks to build, deploy, or make the form usable, use a plain
@@ -301,8 +301,12 @@ reference: `https://fillo.so/docs/cli/reference.md`.
301
301
  - **Workspace:** `workspace rename`, `project rename`, `members`
302
302
  (`invite|cancel-invite|role|remove`), `keys create|list|revoke`,
303
303
  `tokens create-connector|list|revoke`, `sync-tokens create|list|revoke`,
304
- `developers policy|origins|identity`, `branding`, `mcp list|revoke`,
305
- `delete form|workspace`.
304
+ `developers policy|origins|identity`, `branding`, `billing` (read-only plan
305
+ and usage), `mcp list|revoke`, `delete form|workspace`.
306
+
307
+ Paying for Fillo is the exception. Subscribing, changing a volume, and cards
308
+ or invoices stay a human click in Settings → Billing & plan; `billing` only
309
+ reports and prints that page's Manage link.
306
310
 
307
311
  Add `--json` for a machine-readable result: one JSON object on stdout, progress
308
312
  and consent notices as JSON lines on stderr, `{"error": "…"}` and a non-zero
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usefillo/cli",
3
- "version": "0.23.1",
3
+ "version": "0.24.1",
4
4
  "description": "Create and publish Fillo forms, and install the Fillo Agent Skill.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -34,11 +34,11 @@
34
34
  "@types/node": "^22.10.0",
35
35
  "tsup": "^8.4.0",
36
36
  "typescript": "^5.8.3",
37
- "@usefillo/core": "0.23.1"
37
+ "@usefillo/core": "0.24.1"
38
38
  },
39
39
  "scripts": {
40
40
  "build": "tsup && node scripts/copy-skill.mjs",
41
- "test": "node scripts/test-skill-bundle.mjs && node scripts/test-skill-install.mjs && node scripts/test-agent-account.mjs && node scripts/test-stage-push.mjs && node scripts/test-local-validation.mjs && node scripts/test-loopback.mjs && node scripts/test-init.mjs && node scripts/test-api-target.mjs && node scripts/test-init-tty.mjs && node scripts/test-keys-claim.mjs && node scripts/test-responses.mjs && node scripts/test-workspace-commands.mjs && node scripts/test-form-lifecycle.mjs && node scripts/test-discord-tokens.mjs && node scripts/test-confirm.mjs && node scripts/test-integrations.mjs && node scripts/test-responses-ops.mjs && node scripts/test-workspace-admin.mjs",
41
+ "test": "node scripts/test-skill-bundle.mjs && node scripts/test-skill-install.mjs && node scripts/test-agent-account.mjs && node scripts/test-stage-push.mjs && node scripts/test-local-validation.mjs && node scripts/test-loopback.mjs && node scripts/test-init.mjs && node scripts/test-api-target.mjs && node scripts/test-init-tty.mjs && node scripts/test-keys-claim.mjs && node scripts/test-responses.mjs && node scripts/test-workspace-commands.mjs && node scripts/test-form-lifecycle.mjs && node scripts/test-discord-tokens.mjs && node scripts/test-confirm.mjs && node scripts/test-integrations.mjs && node scripts/test-responses-ops.mjs && node scripts/test-workspace-admin.mjs && node scripts/test-billing.mjs",
42
42
  "typecheck": "tsc --noEmit"
43
43
  }
44
44
  }