@usefillo/cli 0.23.0 → 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
@@ -1,17 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync as readFileSync4 } from "fs";
4
+ import { readFileSync as readFileSync5 } from "fs";
5
+
6
+ // src/lib/client-telemetry.ts
7
+ import { readFileSync } from "fs";
8
+ var command;
9
+ var version = "0.0.0";
10
+ try {
11
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
12
+ if (typeof pkg.version === "string") version = pkg.version;
13
+ } catch {
14
+ }
15
+ function setClientCommand(name) {
16
+ command = name;
17
+ }
18
+ function clientTelemetryHeaders() {
19
+ return {
20
+ "X-Fillo-Client": `@usefillo/cli@${version}`,
21
+ ...command ? { "X-Fillo-Command": command } : {}
22
+ };
23
+ }
5
24
 
6
25
  // src/lib/config.ts
7
- import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
26
+ import { chmodSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
8
27
  import { homedir } from "os";
9
28
  import { join } from "path";
10
29
  var CONFIG_DIR = join(homedir(), ".fillo");
11
30
  var CONFIG_PATH = join(CONFIG_DIR, "config.json");
12
31
  function readConfig() {
13
32
  try {
14
- const parsed = JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
33
+ const parsed = JSON.parse(readFileSync2(CONFIG_PATH, "utf8"));
15
34
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
16
35
  const record2 = parsed;
17
36
  return {
@@ -85,7 +104,7 @@ function printTable(header, rows) {
85
104
  for (const cells of rows) console.log(line(cells));
86
105
  }
87
106
  var dateOnly = (iso) => iso.slice(0, 10);
88
- var plural = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
107
+ var plural = (count2, noun) => `${count2} ${noun}${count2 === 1 ? "" : "s"}`;
89
108
  function die(msg) {
90
109
  if (jsonOutput) emitResult({ error: terminalText(msg) });
91
110
  console.error(`${failMark()} ${terminalText(msg)}`);
@@ -148,6 +167,7 @@ async function api(path, init2 = {}, apiBase = API) {
148
167
  signal: rest.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
149
168
  headers: {
150
169
  "Content-Type": "application/json",
170
+ ...clientTelemetryHeaders(),
151
171
  ...token ? { Authorization: `Bearer ${token}` } : {},
152
172
  ...rest.headers
153
173
  }
@@ -232,10 +252,10 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
232
252
  var BOOLISH_FLAGS = /* @__PURE__ */ new Set(["include-abandoned"]);
233
253
  var CONFIRM_FLAGS = /* @__PURE__ */ new Set(["confirm"]);
234
254
  var GLOBAL_FLAGS = /* @__PURE__ */ new Set(["help", "h", "json", "version", "v"]);
235
- function validateFlags(command, flags, commandFlags) {
255
+ function validateFlags(command2, flags, commandFlags) {
236
256
  const allowed = /* @__PURE__ */ new Set([...GLOBAL_FLAGS, ...commandFlags ?? []]);
237
257
  for (const [key, value] of Object.entries(flags)) {
238
- if (!allowed.has(key)) die(`Unknown flag for ${command ?? "fillo"}: --${key}`);
258
+ if (!allowed.has(key)) die(`Unknown flag for ${command2 ?? "fillo"}: --${key}`);
239
259
  if (BOOLEAN_FLAGS.has(key)) {
240
260
  if (value !== true) die(`--${key} does not take a value.`);
241
261
  } else if (BOOLISH_FLAGS.has(key)) {
@@ -1098,7 +1118,7 @@ import {
1098
1118
  lstatSync,
1099
1119
  mkdirSync as mkdirSync2,
1100
1120
  readdirSync,
1101
- readFileSync as readFileSync2,
1121
+ readFileSync as readFileSync3,
1102
1122
  realpathSync,
1103
1123
  renameSync,
1104
1124
  rmSync
@@ -1172,7 +1192,7 @@ function sameSkillBundle(source, target) {
1172
1192
  const targetFiles = skillFiles(target);
1173
1193
  if (sourceFiles.length !== targetFiles.length) return false;
1174
1194
  return sourceFiles.every(
1175
- (file, index) => file === targetFiles[index] && readFileSync2(join3(source, file)).equals(readFileSync2(join3(target, file)))
1195
+ (file, index) => file === targetFiles[index] && readFileSync3(join3(source, file)).equals(readFileSync3(join3(target, file)))
1176
1196
  );
1177
1197
  } catch {
1178
1198
  return false;
@@ -1190,7 +1210,7 @@ function pathEntryExists(path) {
1190
1210
  var MANAGED_SKILL_MARKER = ".fillo-managed.json";
1191
1211
  function isCliManagedFilloSkill(target) {
1192
1212
  try {
1193
- const marker = JSON.parse(readFileSync2(join3(target, MANAGED_SKILL_MARKER), "utf8"));
1213
+ const marker = JSON.parse(readFileSync3(join3(target, MANAGED_SKILL_MARKER), "utf8"));
1194
1214
  return marker.managedBy === "@usefillo/cli" && marker.skill === "build-with-fillo" && marker.format === 1 && marker.allowAutomaticUpdates === true;
1195
1215
  } catch {
1196
1216
  return false;
@@ -1487,6 +1507,7 @@ async function postAgentEvent(apiBase, run, token, body) {
1487
1507
  res = await fetch(`${apiBase}/api/v1/agent-runs/${encodeURIComponent(run)}/events`, {
1488
1508
  method: "POST",
1489
1509
  headers: {
1510
+ ...clientTelemetryHeaders(),
1490
1511
  Authorization: `Bearer ${token}`,
1491
1512
  "Content-Type": "application/json"
1492
1513
  },
@@ -1507,6 +1528,7 @@ async function attachAgentAccount(apiBase, run, progressToken) {
1507
1528
  method: "POST",
1508
1529
  redirect: "error",
1509
1530
  headers: {
1531
+ ...clientTelemetryHeaders(),
1510
1532
  Authorization: `Bearer ${accountToken}`,
1511
1533
  "X-Fillo-Agent-Run-Token": progressToken
1512
1534
  },
@@ -1578,13 +1600,192 @@ var agentCommand = {
1578
1600
  help: agentHelp
1579
1601
  };
1580
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
+
1581
1782
  // src/commands/branding.ts
1582
1783
  function printState(body) {
1583
1784
  const plan = body.plan === "pro" ? "Everything" : "Free";
1584
1785
  console.log(` Plan: ${plan}`);
1585
1786
  console.log(` Badge: ${body.poweredBy ? "shown" : "hidden"}`);
1586
1787
  if (body.canHide === false) {
1587
- 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)."));
1588
1789
  }
1589
1790
  }
1590
1791
  async function branding(subcommand, flags) {
@@ -1617,8 +1818,9 @@ function brandingHelp() {
1617
1818
  branding off Hide the badge on Fillo-rendered forms (Everything plan)
1618
1819
  branding on Show the badge again
1619
1820
 
1620
- ${dimRaw("Hiding needs the Everything plan; a workspace owner or admin selects")}
1621
- ${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.")}
1622
1824
  ${dimRaw("--json prints the raw server response on stdout.")}
1623
1825
  `);
1624
1826
  }
@@ -3183,7 +3385,7 @@ var insightsCommand = {
3183
3385
  };
3184
3386
 
3185
3387
  // src/commands/forms.ts
3186
- import { readFileSync as readFileSync3 } from "fs";
3388
+ import { readFileSync as readFileSync4 } from "fs";
3187
3389
  import { isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
3188
3390
  import { pathToFileURL } from "url";
3189
3391
 
@@ -3640,7 +3842,7 @@ var $ZodCheckStringFormat = /* @__PURE__ */ $constructor("$ZodCheckStringFormat"
3640
3842
  });
3641
3843
 
3642
3844
  // ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/versions.js
3643
- var version = {
3845
+ var version2 = {
3644
3846
  major: 4,
3645
3847
  minor: 4,
3646
3848
  patch: 3
@@ -3652,7 +3854,7 @@ var $ZodType = /* @__PURE__ */ $constructor("$ZodType", (inst, def) => {
3652
3854
  inst ?? (inst = {});
3653
3855
  inst._zod.def = def;
3654
3856
  inst._zod.bag = inst._zod.bag || {};
3655
- inst._zod.version = version;
3857
+ inst._zod.version = version2;
3656
3858
  const checks = [...inst._zod.def.checks ?? []];
3657
3859
  if (inst._zod.traits.has("$ZodCheck")) {
3658
3860
  checks.unshift(inst);
@@ -4689,7 +4891,7 @@ var schemaShape = object({
4689
4891
  });
4690
4892
  var MAX_SCHEMA_VERSION = 1;
4691
4893
  var FILLO_SCHEMA_VERSION = 1;
4692
- var FILLO_SDK_VERSION = true ? "0.23.0" : "0.0.0-dev";
4894
+ var FILLO_SDK_VERSION = true ? "0.24.0" : "0.0.0-dev";
4693
4895
  function str(value, max, fallback = "") {
4694
4896
  return typeof value === "string" ? value.trim().slice(0, max) : fallback;
4695
4897
  }
@@ -5664,7 +5866,7 @@ async function loadResponseData(file) {
5664
5866
  die("Test response data must be a .json file (or - for JSON on stdin).");
5665
5867
  }
5666
5868
  try {
5667
- value = JSON.parse(readFileSync3(abs, "utf8"));
5869
+ value = JSON.parse(readFileSync4(abs, "utf8"));
5668
5870
  } catch (error) {
5669
5871
  die(`Couldn't read ${file}: ${error.message}`);
5670
5872
  }
@@ -5746,7 +5948,7 @@ async function loadSchema(file, allowCode) {
5746
5948
  }
5747
5949
  try {
5748
5950
  if (abs.endsWith(".json")) {
5749
- const parsed = JSON.parse(readFileSync3(abs, "utf8"));
5951
+ const parsed = JSON.parse(readFileSync4(abs, "utf8"));
5750
5952
  if (parsed == null) die(`${file} is empty \u2014 it must contain a form schema.`);
5751
5953
  return parsed;
5752
5954
  }
@@ -7439,7 +7641,7 @@ async function summary(handle, flags) {
7439
7641
  );
7440
7642
  const distribution = Object.entries(field2.distribution ?? {});
7441
7643
  if (distribution.length > 0) {
7442
- 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 ");
7443
7645
  const rest = distribution.length - Math.min(distribution.length, 5);
7444
7646
  console.log(` ${shown}${rest > 0 ? dim(` +${rest} more`) : ""}`);
7445
7647
  }
@@ -8768,10 +8970,10 @@ async function origins(subcommand, flags) {
8768
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(", "))}.`
8769
8971
  );
8770
8972
  }
8771
- function printProtectedForms(count) {
8772
- if (count === 0) return;
8973
+ function printProtectedForms(count2) {
8974
+ if (count2 === 0) return;
8773
8975
  console.log(
8774
- ` ${dim(`${count} ${count === 1 ? "form requires" : "forms require"} verified respondents.`)}`
8976
+ ` ${dim(`${count2} ${count2 === 1 ? "form requires" : "forms require"} verified respondents.`)}`
8775
8977
  );
8776
8978
  }
8777
8979
  async function identityStatus(flags) {
@@ -9149,6 +9351,7 @@ var REGISTRY = [
9149
9351
  webhooksCommand,
9150
9352
  settingsCommand,
9151
9353
  brandingCommand,
9354
+ billingCommand,
9152
9355
  membersCommand,
9153
9356
  workspaceCommand,
9154
9357
  syncTokensCommand,
@@ -9157,13 +9360,13 @@ var REGISTRY = [
9157
9360
  deleteCommand
9158
9361
  ];
9159
9362
  var COMMANDS = /* @__PURE__ */ new Map();
9160
- for (const command of REGISTRY) {
9161
- COMMANDS.set(command.name, command);
9162
- for (const alias of command.aliases ?? []) COMMANDS.set(alias, command);
9363
+ for (const command2 of REGISTRY) {
9364
+ COMMANDS.set(command2.name, command2);
9365
+ for (const alias of command2.aliases ?? []) COMMANDS.set(alias, command2);
9163
9366
  }
9164
9367
  function printVersion() {
9165
9368
  try {
9166
- const pkg = JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8"));
9369
+ const pkg = JSON.parse(readFileSync5(new URL("../package.json", import.meta.url), "utf8"));
9167
9370
  console.log(pkg.version);
9168
9371
  } catch {
9169
9372
  console.log("unknown");
@@ -9240,6 +9443,7 @@ function help() {
9240
9443
  ${dimRaw("--auth none|bearer|x-api-key secret via prompt or env, never argv")}
9241
9444
  settings <cmd> A form's operational settings: get, set key=value
9242
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)
9243
9447
  members <cmd> Workspace members: list, invite, cancel-invite, role, remove
9244
9448
  workspace rename Rename the workspace (ids, keys, and URLs stay put)
9245
9449
  sync-tokens <cmd> Stage-only deploy credentials: list, create, revoke
@@ -9266,18 +9470,21 @@ async function main() {
9266
9470
  const [cmd, ...rest] = process.argv.slice(2);
9267
9471
  const { positional, flags } = parseFlags(rest);
9268
9472
  if (flags.json === true) enableJsonOutput();
9269
- const command = cmd ? COMMANDS.get(cmd) : void 0;
9270
- validateFlags(cmd, flags, command?.flags);
9473
+ const command2 = cmd ? COMMANDS.get(cmd) : void 0;
9474
+ validateFlags(cmd, flags, command2?.flags);
9271
9475
  if (cmd === "version" || cmd === "--version" || cmd === "-v" || flags.version === true || flags.v === true) {
9272
9476
  return printVersion();
9273
9477
  }
9274
9478
  if (flags.help === true || flags.h === true) {
9275
- return (command?.help ?? help)();
9479
+ return (command2?.help ?? help)();
9276
9480
  }
9277
9481
  if (cmd === void 0 || cmd === "help" || cmd === "--help" || cmd === "-h") {
9278
9482
  return help();
9279
9483
  }
9280
- if (command) return command.run(positional, flags);
9484
+ if (command2) {
9485
+ setClientCommand(command2.name);
9486
+ return command2.run(positional, flags);
9487
+ }
9281
9488
  console.error(`Unknown command: ${terminalText(cmd)}`);
9282
9489
  help();
9283
9490
  return process.exit(2);
@@ -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.0",
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.0"
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
  }