@usefillo/cli 0.23.1 → 0.24.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.
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,192 @@ 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 TRIAL_DAYS = 30;
1610
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
1611
+ var count = (value) => new Intl.NumberFormat("en-US").format(value);
1612
+ function parseDate(iso) {
1613
+ if (typeof iso !== "string" || !iso) return null;
1614
+ const date2 = new Date(iso);
1615
+ return Number.isNaN(date2.getTime()) ? null : date2;
1616
+ }
1617
+ var longDate = (date2) => `${date2.getUTCDate()} ${MONTHS[date2.getUTCMonth()]} ${date2.getUTCFullYear()}`;
1618
+ var shortDate = (date2) => `${date2.getUTCDate()} ${MONTHS[date2.getUTCMonth()]}`;
1619
+ function periodLabel(period, currentYear) {
1620
+ const match = /^(\d{4})-(\d{2})$/.exec(period);
1621
+ const month = match ? MONTHS[Number(match[2]) - 1] : void 0;
1622
+ if (!match || !month) return terminalText(period);
1623
+ return match[1] === currentYear ? month : `${month} ${match[1]}`;
1624
+ }
1625
+ function daysLeft(end, now = Date.now()) {
1626
+ const days = Math.max(0, Math.ceil((end.getTime() - now) / 864e5));
1627
+ return days === 0 ? "ends today" : days === 1 ? "1 day left" : `${days} days left`;
1628
+ }
1629
+ var tierName = (tier) => tier ? `Everything ${terminalText(tier)}` : "Everything";
1630
+ function planLine(body) {
1631
+ if (body.billingEnabled === false) {
1632
+ return body.plan === "free" ? "Free" : "Everything";
1633
+ }
1634
+ switch (body.plan) {
1635
+ case "free":
1636
+ return `Free \xB7 ${count(body.allowance)} responses a month`;
1637
+ case "trial": {
1638
+ const end = parseDate(body.trialEndsAt);
1639
+ const ends = end ? ` \xB7 ends ${longDate(end)} (${daysLeft(end)})` : "";
1640
+ return `Everything trial \xB7 ${count(body.allowance)} responses${ends} \xB7 no card`;
1641
+ }
1642
+ case "complimentary":
1643
+ return `${tierName(body.tier)} \xB7 complimentary (no charge, no renewal date)`;
1644
+ case "subscribed": {
1645
+ const billed = body.interval === "year" ? "billed yearly" : "billed monthly";
1646
+ const end = parseDate(body.currentPeriodEnd);
1647
+ let next = "";
1648
+ if (body.status === "past_due") next = " \xB7 payment failed";
1649
+ else if (body.cancelAtPeriodEnd) next = end ? ` \xB7 cancels ${longDate(end)}` : " \xB7 canceling";
1650
+ else if (end)
1651
+ next = `${body.status === "trialing" ? " \xB7 first payment" : " \xB7 renews"} ${longDate(end)}`;
1652
+ return `${tierName(body.tier)} \xB7 ${billed}${next}`;
1653
+ }
1654
+ default:
1655
+ return terminalText(body.plan);
1656
+ }
1657
+ }
1658
+ function usageLine(body) {
1659
+ const { responses: responses2 } = body.usage;
1660
+ const reset = parseDate(body.usage.resetsAt);
1661
+ const resets = reset ? ` \xB7 resets ${shortDate(reset)}` : "";
1662
+ if (!(body.allowance > 0)) return `${count(responses2)} responses this month${resets}`;
1663
+ const percent = Math.floor(responses2 / body.allowance * 100);
1664
+ return `${count(responses2)} of ${count(body.allowance)} responses this month (${percent}%)${resets}`;
1665
+ }
1666
+ function historyLine(body) {
1667
+ const currentYear = body.usage.period?.slice(0, 4);
1668
+ const earlier = (body.history ?? []).filter(
1669
+ (row) => typeof row.period === "string" && row.period !== body.usage.period && typeof row.responses === "number"
1670
+ );
1671
+ if (!earlier.some((row) => row.responses > 0)) return null;
1672
+ return earlier.map((row) => `${periodLabel(row.period, currentYear)} ${count(row.responses)}`).join(" \xB7 ");
1673
+ }
1674
+ function manageUrl(body) {
1675
+ try {
1676
+ const url = new URL(body.manageUrl ?? "");
1677
+ if (url.protocol === "https:" || url.protocol === "http:") return terminalText(url.href);
1678
+ } catch {
1679
+ }
1680
+ return `${terminalText(API)}/settings/plan`;
1681
+ }
1682
+ function fittingVolume(body) {
1683
+ return VOLUMES.find(
1684
+ (volume) => volume.responses > body.allowance && volume.responses >= body.usage.responses
1685
+ );
1686
+ }
1687
+ function notes(body) {
1688
+ const lines = [];
1689
+ const billing2 = body.billingEnabled !== false;
1690
+ if (billing2 && body.plan === "subscribed" && body.status === "past_due") {
1691
+ lines.push(
1692
+ "The last payment failed. Stripe is retrying it, and Everything stays on meanwhile.",
1693
+ "An owner or admin updates the card from Manage billing at the link above."
1694
+ );
1695
+ } else if (billing2 && body.plan === "subscribed" && body.cancelAtPeriodEnd) {
1696
+ lines.push(
1697
+ "Everything stays active until the period ends; the workspace returns to Free after that."
1698
+ );
1699
+ } else if (billing2 && body.plan === "trial") {
1700
+ lines.push("When the trial ends without a subscription, the workspace returns to Free.");
1701
+ } else if (billing2 && body.plan === "free" && body.trialAvailable) {
1702
+ lines.push(
1703
+ `The one-time ${TRIAL_DAYS}-day Everything trial (no card) is unused.`,
1704
+ "An owner or admin can start it at the link above."
1705
+ );
1706
+ }
1707
+ if (body.allowance > 0) {
1708
+ const ratio = body.usage.responses / body.allowance;
1709
+ if (ratio >= 0.8) {
1710
+ lines.push(
1711
+ 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.`
1712
+ );
1713
+ if (billing2) {
1714
+ const next = fittingVolume(body);
1715
+ if (next) {
1716
+ lines.push(
1717
+ `${ratio >= 1 ? "The volume that fits" : "The next volume"} is Everything ${next.tier} (${count(next.responses)} a month).`
1718
+ );
1719
+ } else {
1720
+ lines.push(
1721
+ `For steady volume above ${count(body.allowance)} a month, write to hello@fillo.so.`
1722
+ );
1723
+ }
1724
+ }
1725
+ }
1726
+ }
1727
+ if (!billing2) lines.push("Billing isn't enabled on this Fillo server.");
1728
+ return lines;
1729
+ }
1730
+ async function billing(subcommand, flags) {
1731
+ if (subcommand === "help") return billingHelp();
1732
+ if (subcommand !== void 0) {
1733
+ die(`Unknown billing command: ${subcommand} (fillo billing takes no arguments).`);
1734
+ }
1735
+ const body = await callApi(
1736
+ "/cli/workspace/billing",
1737
+ {},
1738
+ {
1739
+ fallback: failed("billing"),
1740
+ expect: (b) => typeof b.plan === "string" && typeof b.allowance === "number" && typeof b.usage?.responses === "number"
1741
+ }
1742
+ );
1743
+ if (jsonMode(flags)) return emitResult(body);
1744
+ const rows = [
1745
+ ["Plan:", planLine(body)],
1746
+ ["Usage:", usageLine(body)]
1747
+ ];
1748
+ const earlier = historyLine(body);
1749
+ if (earlier) rows.push(["Earlier:", earlier]);
1750
+ if (body.billingEnabled !== false) rows.push(["Manage:", `${manageUrl(body)} (owner or admin)`]);
1751
+ console.log("");
1752
+ for (const [label, value] of rows) console.log(` ${label.padEnd(9)}${value}`);
1753
+ const extra = notes(body);
1754
+ if (extra.length > 0) {
1755
+ console.log("");
1756
+ for (const line of extra) console.log(` ${dim(line)}`);
1757
+ }
1758
+ console.log("");
1759
+ }
1760
+ function billingHelp() {
1761
+ console.log(`
1762
+ ${boldRaw("fillo billing")} \u2014 the workspace's plan and this month's response usage
1763
+
1764
+ ${boldRaw("Usage")}
1765
+ billing Print the plan, usage against the monthly allowance,
1766
+ recent months, and the Manage link
1767
+
1768
+ ${dimRaw("Read-only. Needs an owner or admin login. Subscribing, the one-time")}
1769
+ ${dimRaw("30-day trial, volume changes, cards, and invoices are a human click in")}
1770
+ ${dimRaw("Settings \u2192 Billing & plan (the printed Manage link). Allowances are soft:")}
1771
+ ${dimRaw("forms keep collecting past them.")}
1772
+ ${dimRaw("--json prints the raw server response on stdout.")}
1773
+ `);
1774
+ }
1775
+ var billingCommand = {
1776
+ name: "billing",
1777
+ flags: [],
1778
+ run: (args, flags) => billing(args[0], flags),
1779
+ help: billingHelp
1780
+ };
1781
+
1603
1782
  // src/commands/branding.ts
1604
1783
  function printState(body) {
1605
1784
  const plan = body.plan === "pro" ? "Everything" : "Free";
1606
1785
  console.log(` Plan: ${plan}`);
1607
1786
  console.log(` Badge: ${body.poweredBy ? "shown" : "hidden"}`);
1608
1787
  if (body.canHide === false) {
1609
- console.log(dim(" Turning it off needs the Everything plan (Settings \u2192 Plan)."));
1788
+ console.log(dim(" Turning it off needs the Everything plan (Settings \u2192 Billing & plan)."));
1610
1789
  }
1611
1790
  }
1612
1791
  async function branding(subcommand, flags) {
@@ -1639,8 +1818,9 @@ function brandingHelp() {
1639
1818
  branding off Hide the badge on Fillo-rendered forms (Everything plan)
1640
1819
  branding on Show the badge again
1641
1820
 
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.")}
1821
+ ${dimRaw("Hiding needs the Everything plan: a workspace owner or admin subscribes")}
1822
+ ${dimRaw("(or starts the one-time 30-day trial) in Settings \u2192 Billing & plan.")}
1823
+ ${dimRaw("The CLI never buys the plan. Headless embeds never show a badge.")}
1644
1824
  ${dimRaw("--json prints the raw server response on stdout.")}
1645
1825
  `);
1646
1826
  }
@@ -4711,7 +4891,7 @@ var schemaShape = object({
4711
4891
  });
4712
4892
  var MAX_SCHEMA_VERSION = 1;
4713
4893
  var FILLO_SCHEMA_VERSION = 1;
4714
- var FILLO_SDK_VERSION = true ? "0.23.1" : "0.0.0-dev";
4894
+ var FILLO_SDK_VERSION = true ? "0.24.0" : "0.0.0-dev";
4715
4895
  function str(value, max, fallback = "") {
4716
4896
  return typeof value === "string" ? value.trim().slice(0, max) : fallback;
4717
4897
  }
@@ -7461,7 +7641,7 @@ async function summary(handle, flags) {
7461
7641
  );
7462
7642
  const distribution = Object.entries(field2.distribution ?? {});
7463
7643
  if (distribution.length > 0) {
7464
- const shown = distribution.slice(0, 5).map(([label, count]) => `${terminalText(label)} \u2014 ${count}`).join(" \xB7 ");
7644
+ const shown = distribution.slice(0, 5).map(([label, count2]) => `${terminalText(label)} \u2014 ${count2}`).join(" \xB7 ");
7465
7645
  const rest = distribution.length - Math.min(distribution.length, 5);
7466
7646
  console.log(` ${shown}${rest > 0 ? dim(` +${rest} more`) : ""}`);
7467
7647
  }
@@ -8790,10 +8970,10 @@ async function origins(subcommand, flags) {
8790
8970
  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
8971
  );
8792
8972
  }
8793
- function printProtectedForms(count) {
8794
- if (count === 0) return;
8973
+ function printProtectedForms(count2) {
8974
+ if (count2 === 0) return;
8795
8975
  console.log(
8796
- ` ${dim(`${count} ${count === 1 ? "form requires" : "forms require"} verified respondents.`)}`
8976
+ ` ${dim(`${count2} ${count2 === 1 ? "form requires" : "forms require"} verified respondents.`)}`
8797
8977
  );
8798
8978
  }
8799
8979
  async function identityStatus(flags) {
@@ -9171,6 +9351,7 @@ var REGISTRY = [
9171
9351
  webhooksCommand,
9172
9352
  settingsCommand,
9173
9353
  brandingCommand,
9354
+ billingCommand,
9174
9355
  membersCommand,
9175
9356
  workspaceCommand,
9176
9357
  syncTokensCommand,
@@ -9262,6 +9443,7 @@ function help() {
9262
9443
  ${dimRaw("--auth none|bearer|x-api-key secret via prompt or env, never argv")}
9263
9444
  settings <cmd> A form's operational settings: get, set key=value
9264
9445
  branding [on|off] The workspace's "Powered by Fillo" badge (off needs Everything)
9446
+ billing Plan and this month's response usage (read-only)
9265
9447
  members <cmd> Workspace members: list, invite, cancel-invite, role, remove
9266
9448
  workspace rename Rename the workspace (ids, keys, and URLs stay put)
9267
9449
  sync-tokens <cmd> Stage-only deploy credentials: list, create, revoke
@@ -160,17 +160,27 @@ 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
165
- (headless is free on every plan). Never hide or obscure it with CSS, DOM
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
163
+ visible on Free, hideable on the paid Everything plan and its trial (default
164
+ stays visible until someone turns it off), and never present in a fully
165
+ headless layout (headless is free on every plan). Never hide or obscure it
166
+ with CSS, DOM edits, or style overrides — on Free that violates Fillo's
167
+ terms. When the 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
170
+ the user choose: a workspace owner or admin subscribes to Everything (or
171
+ starts the workspace's one-time 30-day no-card trial) in Fillo Settings →
172
+ Billing & plan, or rebuild the embed headless with the host's own
173
173
  components. Do not bring up plans unprompted.
174
+ - Billing is a human's decision. Everything is real paid billing through
175
+ Stripe checkout, in monthly volumes (5k, 25k, or 100k responses; Free is
176
+ 200). Never select a plan, start the trial, change a volume, or open
177
+ checkout on the user's behalf. When the user asks about their plan or usage,
178
+ run `npx @usefillo/cli@latest billing` (owner or admin login; `--json` for
179
+ the raw result) or the MCP `fillo_get_billing` tool, report the plan and
180
+ this month's usage against the allowance, and hand the human the printed
181
+ Manage link. Allowances are soft: forms never stop collecting over them, so
182
+ never describe going over as losing responses. For prices, read
183
+ `https://fillo.so/pricing.md` rather than quoting from memory.
174
184
  - Use `onSubmitted` only for host-side follow-up after Fillo stores the
175
185
  response. Use a webhook when another backend needs durable delivery.
176
186
  - 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, the one-time trial, changing
308
+ a volume, and cards or invoices stay a human click in Settings → Billing &
309
+ plan; `billing` only 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.0",
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.0"
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
  }