@usefillo/cli 0.16.1 → 0.20.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
@@ -182,10 +182,13 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
182
182
  // (`--device` is the alias). Presence-only.
183
183
  "headless",
184
184
  "device",
185
- // Workspace commands (storage/slack/delete): presence-only switches.
185
+ // Workspace commands (storage/slack/discord/delete): presence-only switches.
186
186
  "force-path-style",
187
187
  "channels",
188
188
  "refresh",
189
+ // `discord enable --auto-join`: widens a role grant so a non-member can be
190
+ // added to the server. Presence-only — it is never "=false".
191
+ "auto-join",
189
192
  "yes",
190
193
  "also-unpublish",
191
194
  "cancel"
@@ -292,8 +295,8 @@ async function startLoopbackServer() {
292
295
  let handler = null;
293
296
  let served = false;
294
297
  const server = createServer((req, res) => {
295
- const respond = (status4, message) => {
296
- res.statusCode = status4;
298
+ const respond = (status5, message) => {
299
+ res.statusCode = status5;
297
300
  res.setHeader("Content-Type", "text/html; charset=utf-8");
298
301
  res.setHeader("Cache-Control", "no-store");
299
302
  res.end(resultPage(message));
@@ -592,6 +595,11 @@ async function fetchWhoami(apiBase = API) {
592
595
  if (!workspace) die("Fillo returned an invalid workspace identity.");
593
596
  return {
594
597
  workspace,
598
+ ...typeof me.workspaceId === "string" ? { workspaceId: terminalText(me.workspaceId) } : {},
599
+ ...typeof me.workspaceSlug === "string" ? { workspaceSlug: terminalText(me.workspaceSlug) } : {},
600
+ ...typeof me.project === "string" ? { project: terminalText(me.project) } : {},
601
+ ...typeof me.projectId === "string" ? { projectId: terminalText(me.projectId) } : {},
602
+ ...typeof me.projectSlug === "string" ? { projectSlug: terminalText(me.projectSlug) } : {},
595
603
  canPublishFileFields: typeof me.canPublishFileFields === "boolean" ? me.canPublishFileFields : void 0
596
604
  };
597
605
  }
@@ -607,10 +615,15 @@ async function whoami(flags) {
607
615
  return emitResult({
608
616
  connected: true,
609
617
  workspace: me.workspace,
618
+ ...me.workspaceId ? { workspaceId: me.workspaceId } : {},
619
+ ...me.workspaceSlug ? { workspaceSlug: me.workspaceSlug } : {},
620
+ ...me.project ? { project: me.project } : {},
621
+ ...me.projectId ? { projectId: me.projectId } : {},
622
+ ...me.projectSlug ? { projectSlug: me.projectSlug } : {},
610
623
  ...me.canPublishFileFields === void 0 ? {} : { canPublishFileFields: me.canPublishFileFields }
611
624
  });
612
625
  }
613
- printConnected(me.workspace);
626
+ printConnected(me.project ? `${me.workspace} / ${me.project}` : me.workspace);
614
627
  if (me.canPublishFileFields === true) {
615
628
  console.log(` ${dim("File uploads: storage ready \u2014 a file field can publish now.")}`);
616
629
  } else if (me.canPublishFileFields === false) {
@@ -802,6 +815,7 @@ async function provisionWorkspace(flags, usageHint, apiBase = API) {
802
815
  }
803
816
  }
804
817
  const workspaceName = flagString(flags, "workspace-name") ?? defaultWorkspaceName();
818
+ const promptCopyId = flagString(flags, "pc")?.trim();
805
819
  const res = await api(
806
820
  "/workspaces/provision",
807
821
  {
@@ -810,7 +824,8 @@ async function provisionWorkspace(flags, usageHint, apiBase = API) {
810
824
  email: email3,
811
825
  source: "cli",
812
826
  ...name ? { name } : {},
813
- ...workspaceName ? { workspaceName } : {}
827
+ ...workspaceName ? { workspaceName } : {},
828
+ ...promptCopyId ? { promptCopyId } : {}
814
829
  })
815
830
  },
816
831
  apiBase
@@ -945,7 +960,7 @@ async function init(flags) {
945
960
  }
946
961
  var initCommand = {
947
962
  name: "init",
948
- flags: ["email", "name", "workspace-name"],
963
+ flags: ["email", "name", "workspace-name", "pc"],
949
964
  run: (_args, flags) => init(flags)
950
965
  };
951
966
 
@@ -1153,7 +1168,7 @@ function installSkill(flags, opts = {}) {
1153
1168
  for (const target of targets) console.log(` ${dim(target)}`);
1154
1169
  if (targets.length > 1) {
1155
1170
  console.log(
1156
- ` ${dim("Same bundle in both \u2014 cross-agent compatibility, not duplication.")}`
1171
+ ` ${dim("Same bundle in both \u2014 cross-agent compatibility, not duplication (want one folder? --agent <name> or --dir <path>).")}`
1157
1172
  );
1158
1173
  }
1159
1174
  console.log(`
@@ -1197,7 +1212,7 @@ var skillCommand = {
1197
1212
  };
1198
1213
 
1199
1214
  // src/commands/agent.ts
1200
- var BOOTSTRAP_USAGE = 'To set up a workspace I need an email for your recovery link. Re-run: npx @usefillo/cli@latest agent bootstrap --email you@company.com [--name "Your Name"]. (Already have an account? `fillo login`.)';
1215
+ var BOOTSTRAP_USAGE = 'To set up a workspace I need the human\'s real work email for the recovery link. Ask the human for their work email once \u2014 never invent an address or use a documentation placeholder \u2014 then re-run: npx @usefillo/cli@latest agent bootstrap --email <work-email> [--name "Your Name"]. (Already have an account? `fillo login`.)';
1201
1216
  async function bootstrapStandalone(flags, apiBase) {
1202
1217
  if (flags.account === true) {
1203
1218
  die(
@@ -1294,21 +1309,21 @@ async function agent(subcommand, flags) {
1294
1309
  die("--account can only be used with `fillo agent bootstrap` or `fillo agent connect`.");
1295
1310
  }
1296
1311
  if (subcommand === "event") {
1297
- const status4 = enumFlag(flags, "status", AGENT_EVENT_STATUSES);
1312
+ const status5 = enumFlag(flags, "status", AGENT_EVENT_STATUSES);
1298
1313
  const message = flagString(flags, "message");
1299
- if (!status4)
1314
+ if (!status5)
1300
1315
  die('Usage: fillo agent event --status <status> --message "<what the human does next>"');
1301
1316
  const action = enumFlag(flags, "action", AGENT_ACTIONS);
1302
1317
  const formStatus = enumFlag(flags, "form-status", FORM_STATUSES);
1303
1318
  const formId = optionalStringFlag(flags, "form-id");
1304
- if ((status4 === "done" || status4 === "needs_action") && !formId) {
1305
- die(`--form-id is required when --status is ${status4}.`);
1319
+ if ((status5 === "done" || status5 === "needs_action") && !formId) {
1320
+ die(`--form-id is required when --status is ${status5}.`);
1306
1321
  }
1307
- if (status4 === "needs_action" && !action) {
1322
+ if (status5 === "needs_action" && !action) {
1308
1323
  die("--action is required when --status is needs_action.");
1309
1324
  }
1310
1325
  await postAgentEvent(apiBase, run, token, {
1311
- status: status4,
1326
+ status: status5,
1312
1327
  message,
1313
1328
  appUrl: flagString(flags, "app-url"),
1314
1329
  action,
@@ -1390,9 +1405,10 @@ function agentHelp() {
1390
1405
 
1391
1406
  ${boldRaw("Commands")}
1392
1407
  agent bootstrap Set up a workspace and install the Build with Fillo skill
1393
- ${dimRaw("--email you@company.com provision a fresh workspace (a TTY confirms your git identity)")}
1408
+ ${dimRaw("--email <work-email> provision a fresh workspace (ask the human first; a TTY confirms git identity)")}
1394
1409
  ${dimRaw('--name "Your Name" optional display name')}
1395
1410
  ${dimRaw("--workspace-name <name> name the new workspace (default: this repo/directory)")}
1411
+ ${dimRaw("--pc <uuid> optional marketing stitch id from /agents?pc=")}
1396
1412
  ${dimRaw("--api <url> target a different Fillo deployment (default: FILLO_API or https://fillo.so)")}
1397
1413
  ${dimRaw("--run <id> --token <token> instead: attach to an open browser modal")}
1398
1414
  ${dimRaw("--account with --run/--token: approve an existing workspace in the browser")}
@@ -1419,6 +1435,7 @@ var agentCommand = {
1419
1435
  "email",
1420
1436
  "name",
1421
1437
  "workspace-name",
1438
+ "pc",
1422
1439
  "run",
1423
1440
  "token",
1424
1441
  "api",
@@ -1469,13 +1486,13 @@ async function claim(flags) {
1469
1486
  const grant = await readJson(codeRes);
1470
1487
  const emailFlag = flagString(flags, "email");
1471
1488
  const savedEmail = cfg.email;
1472
- const { approval, sentTo, notice } = await sendClaimEmail(
1489
+ const { approval, sentTo, notice: notice2 } = await sendClaimEmail(
1473
1490
  grant,
1474
1491
  cfg.claimToken,
1475
1492
  emailFlag,
1476
1493
  savedEmail
1477
1494
  );
1478
- const alreadyClaimed = approval === "device" && notice === "already_claimed";
1495
+ const alreadyClaimed = approval === "device" && notice2 === "already_claimed";
1479
1496
  if (json) {
1480
1497
  emitProgress({
1481
1498
  status: "awaiting_claim",
@@ -1483,11 +1500,11 @@ async function claim(flags) {
1483
1500
  ...sentTo ? { email: sentTo } : {},
1484
1501
  verification_uri: grant.verification_uri,
1485
1502
  approval,
1486
- ...notice ? { notice } : {},
1503
+ ...notice2 ? { notice: notice2 } : {},
1487
1504
  note: AGENT_WAIT_LINE
1488
1505
  });
1489
1506
  } else {
1490
- printGuidance(grant, approval, sentTo, notice);
1507
+ printGuidance(grant, approval, sentTo, notice2);
1491
1508
  }
1492
1509
  await pollForGrant(grant, cfg.pk, approval, alreadyClaimed, json);
1493
1510
  }
@@ -1535,10 +1552,10 @@ async function sendClaimEmail(grant, claimToken, emailFlag, savedEmail) {
1535
1552
  if (!res.ok) die(body.error ?? `Couldn't send the claim email (${res.status}).`);
1536
1553
  return { approval: "device", sentTo: email3 };
1537
1554
  }
1538
- function printGuidance(grant, approval, sentTo, notice) {
1539
- if (notice === "already_claimed") {
1555
+ function printGuidance(grant, approval, sentTo, notice2) {
1556
+ if (notice2 === "already_claimed") {
1540
1557
  console.log("\n This workspace is already claimed.");
1541
- } else if (notice === "recent_email") {
1558
+ } else if (notice2 === "recent_email") {
1542
1559
  console.log(
1543
1560
  `
1544
1561
  A claim email was already sent recently${sentTo ? ` to ${bold(terminalText(sentTo))}` : ""} \u2014 open that one.`
@@ -1552,7 +1569,7 @@ function printGuidance(grant, approval, sentTo, notice) {
1552
1569
  console.log(`
1553
1570
  Your code: ${bold(grant.user_code)}
1554
1571
  `);
1555
- if (notice === "already_claimed") {
1572
+ if (notice2 === "already_claimed") {
1556
1573
  console.log(` Sign in at ${terminalText(grant.verification_uri)} and enter the code`);
1557
1574
  console.log(" to approve this terminal.");
1558
1575
  } else if (approval === "page") {
@@ -1612,11 +1629,11 @@ Claim failed: ${body.error}`);
1612
1629
  }
1613
1630
  if (!claimedAnnounced && Date.now() >= nextStatusAt) {
1614
1631
  nextStatusAt = Date.now() + statusEvery;
1615
- const status4 = await api(`/workspaces/claim-status?key=${encodeURIComponent(pk)}`).catch(
1632
+ const status5 = await api(`/workspaces/claim-status?key=${encodeURIComponent(pk)}`).catch(
1616
1633
  () => null
1617
1634
  );
1618
- if (status4?.ok) {
1619
- const state = await status4.json().catch(() => ({}));
1635
+ if (status5?.ok) {
1636
+ const state = await status5.json().catch(() => ({}));
1620
1637
  if (state.claimed === true) {
1621
1638
  claimedAnnounced = true;
1622
1639
  if (json) {
@@ -1858,6 +1875,441 @@ var deleteCommand = {
1858
1875
  help: deleteHelp
1859
1876
  };
1860
1877
 
1878
+ // src/lib/browser-connect.ts
1879
+ var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
1880
+ function pollIntervalMs2(fallback) {
1881
+ const raw = process.env.FILLO_POLL_INTERVAL_MS;
1882
+ if (raw === void 0 || raw === "") return fallback;
1883
+ const value = Number(raw);
1884
+ return Number.isFinite(value) && value >= 0 ? value : fallback;
1885
+ }
1886
+ async function connectViaBrowser(opts) {
1887
+ const interval = pollIntervalMs2(opts.intervalMs ?? 5e3);
1888
+ const deadline = Date.now() + (opts.timeoutMs ?? 6e5);
1889
+ const dots = !opts.json && !agentMode();
1890
+ if (opts.json) {
1891
+ emitProgress({
1892
+ status: "awaiting_connect",
1893
+ provider: opts.what,
1894
+ url: opts.startUrl,
1895
+ note: "Open the URL in your signed-in browser; this command waits. Do not retry in a loop."
1896
+ });
1897
+ } else {
1898
+ console.log(`
1899
+ Connect ${terminalText(opts.what)} in your signed-in browser:
1900
+ `);
1901
+ console.log(` ${terminalText(opts.startUrl)}
1902
+ `);
1903
+ console.log(" Open in your signed-in browser and approve access \u2014 this command waits.");
1904
+ if (agentMode()) {
1905
+ console.log(
1906
+ " Do not retry in a loop; ask the human to complete the browser step. This command waits."
1907
+ );
1908
+ }
1909
+ }
1910
+ if (dots) process.stdout.write(" Waiting for the connection");
1911
+ for (; ; ) {
1912
+ let connected2 = false;
1913
+ try {
1914
+ connected2 = await opts.poll();
1915
+ } catch {
1916
+ }
1917
+ if (connected2) {
1918
+ const { result, lines } = opts.onConnected();
1919
+ if (opts.json) {
1920
+ emitProgress({ status: "connected" });
1921
+ emitResult(result);
1922
+ return;
1923
+ }
1924
+ if (dots) process.stdout.write("\n");
1925
+ for (const line of lines) console.log(line);
1926
+ return;
1927
+ }
1928
+ if (Date.now() >= deadline) break;
1929
+ if (dots) process.stdout.write(".");
1930
+ await sleep3(interval);
1931
+ }
1932
+ if (opts.json) {
1933
+ emitResult({ connected: false, error: "timeout" });
1934
+ process.exit(1);
1935
+ }
1936
+ if (dots) process.stdout.write("\n");
1937
+ console.log(
1938
+ ` Timed out waiting for ${terminalText(opts.what)} to connect. Finish the browser step, then re-run this command.`
1939
+ );
1940
+ process.exit(1);
1941
+ }
1942
+
1943
+ // src/commands/discord.ts
1944
+ var WEBHOOK_URL_ENV = "FILLO_DISCORD_WEBHOOK_URL";
1945
+ var SNOWFLAKE = /^\d{17,20}$/;
1946
+ var EARLY_SIGNAL_CHOICES = ["5", "10", "25", "off"];
1947
+ function notice(json, line) {
1948
+ if (json) emitProgress({ status: "notice", notice: line });
1949
+ else console.log(` ${line}`);
1950
+ }
1951
+ async function fetchConnection(token) {
1952
+ const res = await api("/cli/discord", { token });
1953
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
1954
+ const body = await readJson(res);
1955
+ if (!res.ok) die(body.error ?? `discord status failed (${res.status}).`);
1956
+ return body;
1957
+ }
1958
+ async function fetchDestination(token, form) {
1959
+ const res = await api(`/cli/forms/${encodeURIComponent(form)}/discord`, { token });
1960
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
1961
+ const body = await readJson(res);
1962
+ if (res.status === 404) die(body.error ?? "Form not found in the selected project.");
1963
+ if (!res.ok) die(body.error ?? `discord status failed (${res.status}).`);
1964
+ return body;
1965
+ }
1966
+ async function putDestination(token, form, patch) {
1967
+ const res = await api(`/cli/forms/${encodeURIComponent(form)}/discord`, {
1968
+ method: "PUT",
1969
+ token,
1970
+ body: JSON.stringify(patch)
1971
+ });
1972
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
1973
+ const body = await readJson(res);
1974
+ if (res.status === 404) die(body.error ?? "Form not found in the selected project.");
1975
+ if (!res.ok) die(body.error ?? `discord update failed (${res.status}).`);
1976
+ return body;
1977
+ }
1978
+ async function projectId(token) {
1979
+ const res = await api("/cli/whoami", { token });
1980
+ const body = await readJson(res);
1981
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
1982
+ if (!res.ok || typeof body.projectId !== "string" || !body.projectId) {
1983
+ die(body.error ?? "Couldn't resolve the selected Fillo project.");
1984
+ }
1985
+ return body.projectId;
1986
+ }
1987
+ async function connect(flags) {
1988
+ const json = jsonMode(flags);
1989
+ const token = requireToken();
1990
+ const connection = await fetchConnection(token);
1991
+ if (!connection.appConfigured) {
1992
+ die(
1993
+ "This Fillo deployment has no Discord app, so there's no one-click channel picker. Run `fillo discord webhook` to paste a channel webhook URL instead."
1994
+ );
1995
+ }
1996
+ const startUrl = `${API}/api/integrations/discord/start?return=terminal&project=${encodeURIComponent(
1997
+ await projectId(token)
1998
+ )}`;
1999
+ let connected2 = null;
2000
+ await connectViaBrowser({
2001
+ json,
2002
+ what: "Discord",
2003
+ startUrl,
2004
+ poll: async () => {
2005
+ const snapshot = await fetchConnection(token);
2006
+ connected2 = snapshot;
2007
+ return snapshot.connected === true;
2008
+ },
2009
+ onConnected: () => {
2010
+ const label = connected2?.channelLabel ?? null;
2011
+ return {
2012
+ result: {
2013
+ connected: true,
2014
+ webhookId: connected2?.webhookId ?? null,
2015
+ channelLabel: label
2016
+ },
2017
+ lines: [
2018
+ ` ${okMark()} Discord connected${label ? ` (${terminalText(label)})` : ""}.`,
2019
+ " Turn a form on with `fillo discord enable <form>`."
2020
+ ]
2021
+ };
2022
+ }
2023
+ });
2024
+ }
2025
+ async function webhook(flags) {
2026
+ const json = jsonMode(flags);
2027
+ const token = requireToken();
2028
+ let url = process.env[WEBHOOK_URL_ENV]?.trim() ?? "";
2029
+ if (!url) {
2030
+ if (json || !isInteractive()) {
2031
+ die(
2032
+ `Set ${WEBHOOK_URL_ENV} to the channel webhook URL and re-run. It is never accepted as an argument \u2014 arguments land in shell history and \`ps\` output.`
2033
+ );
2034
+ }
2035
+ url = await readSecret(" Discord webhook URL (hidden): ").catch(() => "");
2036
+ if (!url) {
2037
+ die(`No webhook URL entered. Paste it at the prompt, or set ${WEBHOOK_URL_ENV} and re-run.`);
2038
+ }
2039
+ }
2040
+ const label = flagString(flags, "label");
2041
+ const res = await api("/cli/discord/webhook", {
2042
+ method: "POST",
2043
+ token,
2044
+ body: JSON.stringify({ url, ...label ? { label } : {} })
2045
+ });
2046
+ const body = await readJson(res);
2047
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
2048
+ if (!res.ok || !body.webhookId) die(body.error ?? `discord webhook failed (${res.status}).`);
2049
+ if (json) return emitResult(body);
2050
+ console.log(
2051
+ ` ${okMark()} Connected Discord${body.label ? ` (${terminalText(body.label)})` : ""}.`
2052
+ );
2053
+ console.log(" Turn a form on with `fillo discord enable <form>`.");
2054
+ }
2055
+ function printDestination(body) {
2056
+ if (!body.connected) {
2057
+ console.log(
2058
+ " Discord is not connected to this project. Run `fillo discord connect` (or `fillo discord webhook` to paste a channel URL)."
2059
+ );
2060
+ return;
2061
+ }
2062
+ if (!body.enabled) {
2063
+ console.log(
2064
+ ` Discord is connected${body.connectionLabel ? ` (${terminalText(body.connectionLabel)})` : ""} but this form isn't sending. Turn it on with \`fillo discord enable <form>\`.`
2065
+ );
2066
+ return;
2067
+ }
2068
+ console.log(
2069
+ ` ${okMark()} Sending to Discord${body.channelLabel ? ` \u2014 ${terminalText(body.channelLabel)}` : ""}`
2070
+ );
2071
+ const fields = body.includedFieldIds ?? [];
2072
+ console.log(
2073
+ ` ${dim("Fields:")} ${fields.length > 0 ? terminalText(fields.join(", ")) : "link only"}`
2074
+ );
2075
+ console.log(
2076
+ ` ${dim("Early signal:")} ${body.earlySignalLimit ? `first ${body.earlySignalLimit} responses in full (${body.earlySignalDelivered} sent)` : "off"}`
2077
+ );
2078
+ const grant = body.roleGrant;
2079
+ console.log(
2080
+ ` ${dim("Role grant:")} ${grant ? `${terminalText(grant.roleId)} in ${terminalText(grant.guildId)}${grant.autoJoin ? " (auto-join)" : ""}` : "off"}`
2081
+ );
2082
+ if (!body.webhookMatches && body.connectionLabel) {
2083
+ console.log(
2084
+ `
2085
+ ${dim(
2086
+ `This form posts to its own channel; the project default is ${terminalText(body.connectionLabel)}. Re-point it with \`fillo discord enable <form> --channel <channelId>\`.`
2087
+ )}`
2088
+ );
2089
+ }
2090
+ const servers = body.servers ?? [];
2091
+ if (servers.length > 0) {
2092
+ console.log("");
2093
+ printTable(
2094
+ ["SERVER", "ID"],
2095
+ servers.map((server) => [terminalText(server.name ?? "(name unavailable)"), server.guildId])
2096
+ );
2097
+ console.log(`
2098
+ ${dim("Aim a form: fillo discord enable <form> --channel <channelId>")}`);
2099
+ }
2100
+ const channels = body.channels ?? [];
2101
+ if (servers.length === 0 && channels.length > 1) {
2102
+ console.log("");
2103
+ printTable(
2104
+ ["CHANNEL", "WEBHOOK ID"],
2105
+ channels.map((channel) => [terminalText(channel.label), channel.webhookId])
2106
+ );
2107
+ }
2108
+ }
2109
+ async function status(form, flags) {
2110
+ if (!form) die("Usage: fillo discord status <form> \u2014 a form id, slug, or push handle.");
2111
+ const body = await fetchDestination(requireToken(), form);
2112
+ if (jsonMode(flags)) return emitResult(body);
2113
+ printDestination(body);
2114
+ }
2115
+ function parseFields(raw) {
2116
+ if (raw.trim().toLowerCase() === "none") return [];
2117
+ return raw.split(",").map((id) => id.trim()).filter(Boolean);
2118
+ }
2119
+ function parseRole(raw) {
2120
+ if (raw.trim().toLowerCase() === "none") return null;
2121
+ const [guildId, roleId, ...extra] = raw.trim().split("/");
2122
+ if (extra.length > 0 || !guildId || !roleId || !SNOWFLAKE.test(guildId) || !SNOWFLAKE.test(roleId)) {
2123
+ die(
2124
+ "--role must be <guildId>/<roleId> (Discord ids), or none to clear it. List them with `fillo discord roles [guildId]`."
2125
+ );
2126
+ }
2127
+ return { guildId, roleId };
2128
+ }
2129
+ async function enable(form, flags) {
2130
+ const json = jsonMode(flags);
2131
+ if (!form)
2132
+ die(
2133
+ "Usage: fillo discord enable <form> [--channel <webhookId>] [--fields a,b,c] [--early-signal 5|10|25|off]"
2134
+ );
2135
+ const token = requireToken();
2136
+ const current = await fetchDestination(token, form);
2137
+ const channels = current.channels ?? [];
2138
+ if ((!current.connected || !current.connectionWebhookId) && channels.length === 0) {
2139
+ die(
2140
+ "Discord isn't connected to this project. Run `fillo discord connect`, or `fillo discord webhook` to paste a channel URL."
2141
+ );
2142
+ }
2143
+ const rawChannel = flagString(flags, "channel");
2144
+ if (rawChannel !== void 0 && !SNOWFLAKE.test(rawChannel.trim())) {
2145
+ die("--channel must be a Discord channel id (right-click the channel \u2192 Copy Channel ID).");
2146
+ }
2147
+ const targetChannelId = rawChannel?.trim() ?? null;
2148
+ const fallbackWebhookId = current.connectionWebhookId ?? (channels.length === 1 ? channels[0]?.webhookId ?? null : null);
2149
+ if (!current.enabled && !targetChannelId && !fallbackWebhookId) {
2150
+ die(
2151
+ "Pass --channel <channelId> to pick where this form posts. `fillo discord status <form>` lists the connected servers."
2152
+ );
2153
+ }
2154
+ const patch = {};
2155
+ if (targetChannelId) {
2156
+ patch.enabled = true;
2157
+ patch.channelId = targetChannelId;
2158
+ } else if (!current.enabled && fallbackWebhookId) {
2159
+ patch.enabled = true;
2160
+ patch.webhookId = fallbackWebhookId;
2161
+ }
2162
+ const rawFields = flagString(flags, "fields");
2163
+ if (rawFields !== void 0) patch.includeFieldIds = parseFields(rawFields);
2164
+ const rawEarly = flagString(flags, "early-signal");
2165
+ if (rawEarly !== void 0) {
2166
+ if (!EARLY_SIGNAL_CHOICES.includes(rawEarly)) {
2167
+ die(`--early-signal must be one of: ${EARLY_SIGNAL_CHOICES.join(", ")}`);
2168
+ }
2169
+ patch.earlySignalLimit = rawEarly === "off" ? null : Number(rawEarly);
2170
+ }
2171
+ const rawRole = flagString(flags, "role");
2172
+ const autoJoin = flags["auto-join"] === true;
2173
+ if (rawRole !== void 0) {
2174
+ const grant = parseRole(rawRole);
2175
+ patch.roleGrant = grant ? { ...grant, ...autoJoin ? { autoJoin: true } : {} } : null;
2176
+ } else if (autoJoin) {
2177
+ die(
2178
+ "--auto-join needs --role <guildId>/<roleId> \u2014 it widens that grant, it isn't a setting on its own."
2179
+ );
2180
+ }
2181
+ if (Object.keys(patch).length === 0) {
2182
+ if (json) return emitResult(current);
2183
+ printDestination(current);
2184
+ return;
2185
+ }
2186
+ if (typeof patch.earlySignalLimit === "number") {
2187
+ notice(
2188
+ json,
2189
+ `Early signal sends every answer to this channel for the first ${patch.earlySignalLimit} responses.`
2190
+ );
2191
+ }
2192
+ if (patch.roleGrant && autoJoin) {
2193
+ notice(
2194
+ json,
2195
+ "Auto-join adds respondents to your Discord server with that role \u2014 Discord asks each person's permission first."
2196
+ );
2197
+ } else if (patch.roleGrant) {
2198
+ notice(
2199
+ json,
2200
+ "Respondents who verify with Discord get that role once their response is accepted."
2201
+ );
2202
+ }
2203
+ const body = await putDestination(token, form, patch);
2204
+ if (json) return emitResult(body);
2205
+ printDestination(body);
2206
+ }
2207
+ async function disable(form, flags) {
2208
+ if (!form) die("Usage: fillo discord disable <form> \u2014 a form id, slug, or push handle.");
2209
+ const token = requireToken();
2210
+ const body = await putDestination(token, form, { enabled: false });
2211
+ if (jsonMode(flags)) return emitResult(body);
2212
+ console.log(
2213
+ ` ${okMark()} This form no longer posts to Discord. The workspace's webhook stays connected.`
2214
+ );
2215
+ }
2216
+ async function roles(guildId, flags) {
2217
+ const json = jsonMode(flags);
2218
+ const token = requireToken();
2219
+ const path = guildId ? `/cli/discord/roles?guildId=${encodeURIComponent(guildId)}` : "/cli/discord/roles";
2220
+ const res = await api(path, { token });
2221
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
2222
+ const body = await readJson(res);
2223
+ if (!res.ok) die(body.error ?? `discord roles failed (${res.status}).`);
2224
+ if (json) return emitResult(body);
2225
+ if (!guildId) {
2226
+ const guilds = body.guilds ?? [];
2227
+ if (guilds.length === 0) {
2228
+ console.log(
2229
+ " No Discord servers yet \u2014 connect a channel webhook first with `fillo discord connect`."
2230
+ );
2231
+ return;
2232
+ }
2233
+ console.log("");
2234
+ printTable(
2235
+ ["SERVER", "ID"],
2236
+ guilds.map((guild) => [terminalText(guild.name ?? "(name unavailable)"), guild.id])
2237
+ );
2238
+ if (body.guildsUnavailable === true) {
2239
+ console.log(
2240
+ `
2241
+ ${dim("Discord didn't answer, so some names are missing. Ids still work.")}`
2242
+ );
2243
+ }
2244
+ console.log(`
2245
+ ${dim("List a server's roles: fillo discord roles <id>")}`);
2246
+ return;
2247
+ }
2248
+ const list5 = body.roles ?? [];
2249
+ if (list5.length === 0) {
2250
+ console.log(" That server has no roles Fillo could list.");
2251
+ return;
2252
+ }
2253
+ console.log("");
2254
+ printTable(
2255
+ ["ROLE", "ID", "GRANTABLE"],
2256
+ list5.map((role) => [terminalText(role.name), role.id, role.grantable ? "yes" : "no"])
2257
+ );
2258
+ console.log(`
2259
+ ${dim(`Use one: fillo discord enable <form> --role ${guildId}/<roleId>`)}`);
2260
+ console.log(
2261
+ ` ${dim("A role marked no sits above Fillo's own role, or belongs to another integration.")}`
2262
+ );
2263
+ }
2264
+ async function discord(subcommand, args, flags) {
2265
+ if (subcommand === void 0 || subcommand === "help") return discordHelp();
2266
+ if (subcommand === "connect") return connect(flags);
2267
+ if (subcommand === "webhook") return webhook(flags);
2268
+ if (subcommand === "status") return status(args[0], flags);
2269
+ if (subcommand === "enable") return enable(args[0], flags);
2270
+ if (subcommand === "disable") return disable(args[0], flags);
2271
+ if (subcommand === "roles") return roles(args[0], flags);
2272
+ die(
2273
+ `Unknown discord command: ${terminalText(subcommand)} (expected connect, webhook, status, enable, disable, or roles).`
2274
+ );
2275
+ }
2276
+ function discordHelp() {
2277
+ console.log(`
2278
+ ${boldRaw("fillo discord")} \u2014 post responses to a Discord channel, from the terminal
2279
+
2280
+ ${boldRaw("Commands")}
2281
+ discord connect Let Discord pick the channel and create the webhook
2282
+ ${dimRaw("opens an OAuth URL to approve; needs a Discord app on this deploy")}
2283
+ discord webhook Connect a channel webhook you paste
2284
+ ${dimRaw("--label <name> what Fillo shows for this channel")}
2285
+ ${dimRaw(`reads the URL from a hidden prompt, or ${WEBHOOK_URL_ENV}`)}
2286
+ discord status <form> What this form sends, to where, with which extras
2287
+ discord enable <form> Start sending \u2014 and set any of the options below
2288
+ ${dimRaw("--channel <channelId> which channel this form posts to (bot lane)")}
2289
+ ${dimRaw("--fields a,b,c up to three field ids (none = link only)")}
2290
+ ${dimRaw("--early-signal 5|10|25 first N responses arrive in full (off to stop)")}
2291
+ ${dimRaw("--role <guildId>/<roleId> grant that role on an accepted response")}
2292
+ ${dimRaw("--auto-join with --role: add non-members to the server")}
2293
+ discord disable <form> Stop this form posting (the webhook stays connected)
2294
+ discord roles [guildId] Your servers, or one server's roles and what Fillo can grant
2295
+
2296
+ ${dimRaw("The webhook URL is the whole credential, so it is NEVER an argument \u2014")}
2297
+ ${dimRaw("arguments land in shell history and `ps` output. Paste it at the hidden")}
2298
+ ${dimRaw(`prompt, or set ${WEBHOOK_URL_ENV} for an unattended run.`)}
2299
+ ${dimRaw("--early-signal and --auto-join widen what leaves Fillo, so each prints a")}
2300
+ ${dimRaw("one-line notice saying what was agreed to. Ask the human before setting them.")}
2301
+ ${dimRaw("Re-enabling against a different webhook clears early signal \u2014 that consent")}
2302
+ ${dimRaw("was given for one channel and is never carried into another.")}
2303
+ ${dimRaw("--json prints the raw server response on stdout.")}
2304
+ `);
2305
+ }
2306
+ var discordCommand = {
2307
+ name: "discord",
2308
+ flags: ["label", "channel", "fields", "early-signal", "role", "auto-join"],
2309
+ run: (args, flags) => discord(args[0], args.slice(1), flags),
2310
+ help: discordHelp
2311
+ };
2312
+
1861
2313
  // src/commands/forms.ts
1862
2314
  import { readFileSync as readFileSync3 } from "fs";
1863
2315
  import { isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
@@ -3258,6 +3710,48 @@ function sortForPicker(countries) {
3258
3710
  return [...countries].sort((a, b) => collator.compare(a.name, b.name));
3259
3711
  }
3260
3712
  var byIso = new Map(PHONE_COUNTRIES.map((c) => [c.iso2, c]));
3713
+ var DEFAULT_STRINGS = {
3714
+ back: "Back",
3715
+ next: "Next",
3716
+ submit: "Submit",
3717
+ submitting: "Submitting\u2026",
3718
+ uploading: "Uploading\u2026",
3719
+ optional: " (optional)",
3720
+ other: "Other",
3721
+ otherPrompt: "Other \u2014 please specify",
3722
+ otherPlaceholder: "Your answer",
3723
+ choosePlaceholder: "Choose\u2026",
3724
+ successTitle: "Thanks!",
3725
+ successMessage: "Your response has been recorded.",
3726
+ closed: "This form is no longer accepting responses.",
3727
+ notLive: "This form isn't live yet.",
3728
+ notOpenTitle: "This form isn't open yet",
3729
+ notOpenBody: "The form owner is still setting things up. Please check back soon.",
3730
+ closedTitle: "Responses are closed",
3731
+ submitFailed: "This form can't submit right now. Please try again in a moment.",
3732
+ loadFailedNotFound: "Form not found. Check the link or ask the form owner for help.",
3733
+ loadFailedNetwork: "Couldn't reach the server \u2014 check your connection and try again.",
3734
+ loadFailed: "This form could not be loaded.",
3735
+ renderFailed: "This form could not be rendered.",
3736
+ resumeNotice: "Picked up where you left off.",
3737
+ editNotice: "You're updating your earlier response.",
3738
+ resumeStartOver: "Start over",
3739
+ challengeUnavailable: "The verification check couldn't load. Refresh the page and try again."
3740
+ };
3741
+ var DEFAULT_RESPONDENT_ERROR_STRINGS = {
3742
+ submitFailed: DEFAULT_STRINGS.submitFailed,
3743
+ loadFailedNotFound: DEFAULT_STRINGS.loadFailedNotFound,
3744
+ loadFailedNetwork: DEFAULT_STRINGS.loadFailedNetwork,
3745
+ formUnavailable: "This form is unavailable.",
3746
+ formClosed: DEFAULT_STRINGS.closed,
3747
+ submitRateLimited: "Too many responses are being submitted right now. Wait a moment, then try again.",
3748
+ respondentUnrecognized: "This form couldn't verify who is responding. Ask the form owner for help.",
3749
+ fileUnavailable: "One of the uploaded files is no longer available. Remove it and upload it again.",
3750
+ scopeMissing: "We couldn't tell which entry this response belongs to. Answer the required field and try again.",
3751
+ challengeIncomplete: "Please complete the verification check, then submit.",
3752
+ challengeRetry: "That verification didn't go through. Please complete the check again and resubmit.",
3753
+ reviewAnswers: "We couldn't submit \u2014 please review your answers."
3754
+ };
3261
3755
  var fileValueSchema = object({
3262
3756
  fileId: string2().check(_minLength(1)),
3263
3757
  name: string2(),
@@ -3322,7 +3816,7 @@ var schemaShape = object({
3322
3816
  });
3323
3817
  var MAX_SCHEMA_VERSION = 1;
3324
3818
  var FILLO_SCHEMA_VERSION = 1;
3325
- var FILLO_SDK_VERSION = true ? "0.16.1" : "0.0.0-dev";
3819
+ var FILLO_SDK_VERSION = true ? "0.20.0" : "0.0.0-dev";
3326
3820
  function str(value, max, fallback = "") {
3327
3821
  return typeof value === "string" ? value.trim().slice(0, max) : fallback;
3328
3822
  }
@@ -4183,7 +4677,7 @@ async function list(flags) {
4183
4677
  }
4184
4678
  }
4185
4679
  var liveDot = () => ansiEnabled() ? "\x1B[32m\u25CF\x1B[0m" : "\u25CF";
4186
- async function status(handle, flags) {
4680
+ async function status2(handle, flags) {
4187
4681
  if (!handle) die("Usage: fillo status <formId|handle>");
4188
4682
  const res = await api(`/cli/forms/${encodeURIComponent(handle)}`, { token: requireToken() });
4189
4683
  if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
@@ -4395,6 +4889,7 @@ async function loadSchema(file, allowCode) {
4395
4889
  die(`Couldn't read ${file}: ${e.message}`);
4396
4890
  }
4397
4891
  }
4892
+ var PUSH_STORAGE_VALUES = /* @__PURE__ */ new Set(["gdrive", "box", "s3", "r2"]);
4398
4893
  function isRecord(value) {
4399
4894
  return typeof value === "object" && value !== null && !Array.isArray(value);
4400
4895
  }
@@ -4404,7 +4899,19 @@ function toPushItems(value, handle) {
4404
4899
  return values.map((item, index) => {
4405
4900
  if (isRecord(item) && "schema" in item) {
4406
4901
  const ownHandle = typeof item.id === "string" ? item.id : void 0;
4407
- return { handle: handle ?? ownHandle, schema: item.schema, theme: item.theme };
4902
+ if (item.storage !== void 0 && !PUSH_STORAGE_VALUES.has(String(item.storage))) {
4903
+ die(`Item ${index + 1} has an unsupported storage destination.`);
4904
+ }
4905
+ if (item.purpose !== void 0 && item.purpose !== "file_request") {
4906
+ die(`Item ${index + 1} has an unsupported form purpose.`);
4907
+ }
4908
+ return {
4909
+ handle: handle ?? ownHandle,
4910
+ schema: item.schema,
4911
+ theme: item.theme,
4912
+ ...typeof item.storage === "string" ? { storage: item.storage } : {},
4913
+ ...item.purpose === "file_request" ? { purpose: item.purpose } : {}
4914
+ };
4408
4915
  }
4409
4916
  if (values.length > 1 && !handle) {
4410
4917
  die(`Item ${index + 1} needs an id or --handle.`);
@@ -4414,9 +4921,9 @@ function toPushItems(value, handle) {
4414
4921
  }
4415
4922
  function printNotices(notices) {
4416
4923
  if (!Array.isArray(notices)) return;
4417
- for (const notice of notices) {
4418
- if (typeof notice === "string" && notice.trim()) {
4419
- console.log(` ${bold("Note:")} ${terminalText(notice)}`);
4924
+ for (const notice2 of notices) {
4925
+ if (typeof notice2 === "string" && notice2.trim()) {
4926
+ console.log(` ${bold("Note:")} ${terminalText(notice2)}`);
4420
4927
  }
4421
4928
  }
4422
4929
  }
@@ -4501,7 +5008,9 @@ async function pushWithSync(items, credential, requestedStage, results) {
4501
5008
  ...credential.key ? { key: credential.key } : {},
4502
5009
  id: handle,
4503
5010
  schema: item.schema,
4504
- theme: item.theme ?? null
5011
+ theme: item.theme ?? null,
5012
+ ...item.storage ? { storage: item.storage } : {},
5013
+ ...item.purpose ? { purpose: item.purpose } : {}
4505
5014
  })
4506
5015
  });
4507
5016
  const body = await readJson(res);
@@ -4566,6 +5075,8 @@ async function push(file, flags) {
4566
5075
  body: JSON.stringify({
4567
5076
  schema: item.schema,
4568
5077
  theme: item.theme ?? null,
5078
+ ...item.storage ? { storage: item.storage } : {},
5079
+ ...item.purpose ? { purpose: item.purpose } : {},
4569
5080
  publish: false
4570
5081
  })
4571
5082
  });
@@ -4593,6 +5104,8 @@ async function push(file, flags) {
4593
5104
  schema: item.schema,
4594
5105
  theme: item.theme ?? null,
4595
5106
  handle: item.handle,
5107
+ ...item.storage ? { storage: item.storage } : {},
5108
+ ...item.purpose ? { purpose: item.purpose } : {},
4596
5109
  publish: true
4597
5110
  })
4598
5111
  });
@@ -4608,11 +5121,16 @@ async function push(file, flags) {
4608
5121
  }
4609
5122
  die(body.error ?? `push failed (${res.status}).`);
4610
5123
  }
5124
+ const draft = body.status === "draft";
4611
5125
  if (results) {
4612
5126
  results.push(body);
4613
- emitProgress({ status: "pushed", formId: body.formId, published: true });
5127
+ emitProgress({
5128
+ status: "pushed",
5129
+ formId: body.formId,
5130
+ ...draft ? { draft: true } : { published: true }
5131
+ });
4614
5132
  } else {
4615
- printPushed(body.formId, body.url, !!body.updated, false, {
5133
+ printPushed(body.formId, body.url, !!body.updated, draft, {
4616
5134
  warning: body.warning,
4617
5135
  warningUrl: body.warningUrl,
4618
5136
  notices: body.notices
@@ -4643,7 +5161,7 @@ var listCommand = {
4643
5161
  var statusCommand = {
4644
5162
  name: "status",
4645
5163
  flags: [],
4646
- run: (args, flags) => status(args[0], flags)
5164
+ run: (args, flags) => status2(args[0], flags)
4647
5165
  };
4648
5166
  var publishCommand = {
4649
5167
  name: "publish",
@@ -4656,6 +5174,61 @@ var testResponseCommand = {
4656
5174
  run: (args) => testResponse(args[0], args[1])
4657
5175
  };
4658
5176
 
5177
+ // src/commands/branding.ts
5178
+ function printState(body) {
5179
+ const plan = body.plan === "pro" ? "Everything" : "Free";
5180
+ console.log(` Plan: ${plan}`);
5181
+ console.log(` Badge: ${body.poweredBy ? "shown" : "hidden"}`);
5182
+ if (body.canHide === false) {
5183
+ console.log(dim(" Turning it off needs the Everything plan (Settings \u2192 Plan)."));
5184
+ }
5185
+ }
5186
+ async function branding(subcommand, flags) {
5187
+ if (subcommand === "help") return brandingHelp();
5188
+ if (subcommand !== void 0 && subcommand !== "on" && subcommand !== "off") {
5189
+ die(`Unknown branding command: ${subcommand} (expected on, off, or nothing for status).`);
5190
+ }
5191
+ const token = requireToken();
5192
+ const res = subcommand === void 0 ? await api("/cli/workspace/branding", { token }) : await api("/cli/workspace/branding", {
5193
+ method: "PATCH",
5194
+ token,
5195
+ body: JSON.stringify({ show: subcommand === "on" })
5196
+ });
5197
+ const body = await readJson(res);
5198
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
5199
+ if (!res.ok) die(body.error ?? `branding ${subcommand ?? "status"} failed (${res.status}).`);
5200
+ if (jsonMode(flags)) return emitResult(body);
5201
+ console.log("");
5202
+ if (subcommand !== void 0) {
5203
+ console.log(
5204
+ ` ${okMark()} Badge ${body.poweredBy ? "shown" : "hidden"} on Fillo-rendered forms.
5205
+ `
5206
+ );
5207
+ }
5208
+ printState(body);
5209
+ console.log("");
5210
+ }
5211
+ function brandingHelp() {
5212
+ console.log(`
5213
+ ${boldRaw("fillo branding")} \u2014 the workspace's "Powered by Fillo" badge
5214
+
5215
+ ${boldRaw("Commands")}
5216
+ branding Print the current badge state
5217
+ branding off Hide the badge on Fillo-rendered forms (Everything plan)
5218
+ branding on Show the badge again
5219
+
5220
+ ${dimRaw("Hiding needs the Everything plan; a workspace owner or admin selects")}
5221
+ ${dimRaw("that in Settings \u2192 Plan. Headless embeds never show a badge.")}
5222
+ ${dimRaw("--json prints the raw server response on stdout.")}
5223
+ `);
5224
+ }
5225
+ var brandingCommand = {
5226
+ name: "branding",
5227
+ flags: [],
5228
+ run: (args, flags) => branding(args[0], flags),
5229
+ help: brandingHelp
5230
+ };
5231
+
4659
5232
  // src/commands/keys.ts
4660
5233
  var PRESETS = ["read", "agent", "full"];
4661
5234
  var EXPIRY_CHOICES = ["30d", "90d", "never"];
@@ -4804,7 +5377,7 @@ async function keys(subcommand, args, flags) {
4804
5377
  }
4805
5378
  function keysHelp() {
4806
5379
  console.log(`
4807
- ${boldRaw("fillo keys")} \u2014 workspace API keys (fsk_) for coding agents and integrations
5380
+ ${boldRaw("fillo keys")} \u2014 project API keys (fsk_) for coding agents and integrations
4808
5381
 
4809
5382
  ${boldRaw("Commands")}
4810
5383
  keys create --name <name> Mint a key \u2014 the plaintext is shown once, store it
@@ -5244,73 +5817,16 @@ var settingsCommand = {
5244
5817
  help: settingsHelp
5245
5818
  };
5246
5819
 
5247
- // src/lib/browser-connect.ts
5248
- var sleep3 = (ms) => new Promise((r) => setTimeout(r, ms));
5249
- function pollIntervalMs2(fallback) {
5250
- const raw = process.env.FILLO_POLL_INTERVAL_MS;
5251
- if (raw === void 0 || raw === "") return fallback;
5252
- const value = Number(raw);
5253
- return Number.isFinite(value) && value >= 0 ? value : fallback;
5254
- }
5255
- async function connectViaBrowser(opts) {
5256
- const interval = pollIntervalMs2(opts.intervalMs ?? 5e3);
5257
- const deadline = Date.now() + (opts.timeoutMs ?? 6e5);
5258
- const dots = !opts.json && !agentMode();
5259
- if (opts.json) {
5260
- emitProgress({
5261
- status: "awaiting_connect",
5262
- provider: opts.what,
5263
- url: opts.startUrl,
5264
- note: "Open the URL in your signed-in browser; this command waits. Do not retry in a loop."
5265
- });
5266
- } else {
5267
- console.log(`
5268
- Connect ${terminalText(opts.what)} in your signed-in browser:
5269
- `);
5270
- console.log(` ${terminalText(opts.startUrl)}
5271
- `);
5272
- console.log(" Open in your signed-in browser and approve access \u2014 this command waits.");
5273
- if (agentMode()) {
5274
- console.log(
5275
- " Do not retry in a loop; ask the human to complete the browser step. This command waits."
5276
- );
5277
- }
5278
- }
5279
- if (dots) process.stdout.write(" Waiting for the connection");
5280
- for (; ; ) {
5281
- let connected2 = false;
5282
- try {
5283
- connected2 = await opts.poll();
5284
- } catch {
5285
- }
5286
- if (connected2) {
5287
- const { result, lines } = opts.onConnected();
5288
- if (opts.json) {
5289
- emitProgress({ status: "connected" });
5290
- emitResult(result);
5291
- return;
5292
- }
5293
- if (dots) process.stdout.write("\n");
5294
- for (const line of lines) console.log(line);
5295
- return;
5296
- }
5297
- if (Date.now() >= deadline) break;
5298
- if (dots) process.stdout.write(".");
5299
- await sleep3(interval);
5300
- }
5301
- if (opts.json) {
5302
- emitResult({ connected: false, error: "timeout" });
5303
- process.exit(1);
5820
+ // src/commands/slack.ts
5821
+ async function slackStartUrl(token) {
5822
+ const res = await api("/cli/whoami", { token });
5823
+ const body = await readJson(res);
5824
+ if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
5825
+ if (!res.ok || typeof body.projectId !== "string" || !body.projectId) {
5826
+ die(body.error ?? "Couldn't resolve the selected Fillo project.");
5304
5827
  }
5305
- if (dots) process.stdout.write("\n");
5306
- console.log(
5307
- ` Timed out waiting for ${terminalText(opts.what)} to connect. Finish the browser step, then re-run this command.`
5308
- );
5309
- process.exit(1);
5828
+ return `${API}/api/integrations/slack/start?return=terminal&project=${encodeURIComponent(body.projectId)}`;
5310
5829
  }
5311
-
5312
- // src/commands/slack.ts
5313
- var SLACK_START_URL = `${API}/api/integrations/slack/start?return=terminal`;
5314
5830
  async function refresh(flags) {
5315
5831
  const json = jsonMode(flags);
5316
5832
  const token = requireToken();
@@ -5318,8 +5834,9 @@ async function refresh(flags) {
5318
5834
  const body = await readJson(res);
5319
5835
  if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
5320
5836
  if (res.status === 409) {
5837
+ const startUrl = await slackStartUrl(token);
5321
5838
  die(
5322
- `${body.error ?? "Reconnect Slack in the browser to refresh channels."} Reconnect at ${SLACK_START_URL}`
5839
+ `${body.error ?? "Reconnect Slack in the browser to refresh channels."} Reconnect at ${startUrl}`
5323
5840
  );
5324
5841
  }
5325
5842
  if (res.status === 429) {
@@ -5351,7 +5868,7 @@ function printChannels(channels) {
5351
5868
  console.log("");
5352
5869
  printTable(["NAME", "ID", "VISIBILITY"], rows);
5353
5870
  }
5354
- async function status2(flags) {
5871
+ async function status3(flags) {
5355
5872
  const json = jsonMode(flags);
5356
5873
  const showChannels = flags.channels === true;
5357
5874
  const token = requireToken();
@@ -5376,13 +5893,14 @@ async function status2(flags) {
5376
5893
  );
5377
5894
  }
5378
5895
  }
5379
- async function connect(flags) {
5896
+ async function connect2(flags) {
5380
5897
  const json = jsonMode(flags);
5381
5898
  const token = requireToken();
5899
+ const startUrl = await slackStartUrl(token);
5382
5900
  await connectViaBrowser({
5383
5901
  json,
5384
5902
  what: "Slack",
5385
- startUrl: SLACK_START_URL,
5903
+ startUrl,
5386
5904
  poll: async () => {
5387
5905
  const res = await api("/cli/slack", { token });
5388
5906
  if (res.status === 401) die("Token invalid \u2014 run `fillo login` again.");
@@ -5401,12 +5919,12 @@ async function connect(flags) {
5401
5919
  }
5402
5920
  async function slack(subcommand, flags) {
5403
5921
  if (subcommand === "help") return slackHelp();
5404
- if (subcommand === "connect") return connect(flags);
5922
+ if (subcommand === "connect") return connect2(flags);
5405
5923
  if (subcommand !== void 0 && subcommand !== "status") {
5406
5924
  die(`Unknown slack command: ${terminalText(subcommand)} (expected status or connect).`);
5407
5925
  }
5408
5926
  if (flags.refresh === true) return refresh(flags);
5409
- return status2(flags);
5927
+ return status3(flags);
5410
5928
  }
5411
5929
  function slackHelp() {
5412
5930
  console.log(`
@@ -5455,7 +5973,7 @@ async function fetchStatus(token) {
5455
5973
  if (!res.ok || !body.providers) die(body.error ?? `storage status failed (${res.status}).`);
5456
5974
  return body;
5457
5975
  }
5458
- async function status3(flags) {
5976
+ async function status4(flags) {
5459
5977
  const token = requireToken();
5460
5978
  const body = await fetchStatus(token);
5461
5979
  if (jsonMode(flags)) return emitResult(body);
@@ -5622,7 +6140,7 @@ async function connectBrowserProvider(provider, flags) {
5622
6140
  }
5623
6141
  });
5624
6142
  }
5625
- async function connect2(providerArg, flags) {
6143
+ async function connect3(providerArg, flags) {
5626
6144
  const provider = resolveProviderArg(providerArg);
5627
6145
  if (!provider) {
5628
6146
  die("Usage: fillo storage connect <s3|drive|box> (r2 and gdrive are accepted aliases).");
@@ -5645,9 +6163,9 @@ async function disconnect(providerArg, flags) {
5645
6163
  console.log(` ${okMark()} Disconnected ${PROVIDER_LABEL[provider]} storage.`);
5646
6164
  }
5647
6165
  async function storage(subcommand, args, flags) {
5648
- if (subcommand === void 0 || subcommand === "status") return status3(flags);
6166
+ if (subcommand === void 0 || subcommand === "status") return status4(flags);
5649
6167
  if (subcommand === "help") return storageHelp();
5650
- if (subcommand === "connect") return connect2(args[0], flags);
6168
+ if (subcommand === "connect") return connect3(args[0], flags);
5651
6169
  if (subcommand === "disconnect") return disconnect(args[0], flags);
5652
6170
  die(
5653
6171
  `Unknown storage command: ${terminalText(subcommand)} (expected status, connect, or disconnect).`
@@ -5682,6 +6200,63 @@ var storageCommand = {
5682
6200
  help: storageHelp
5683
6201
  };
5684
6202
 
6203
+ // src/commands/tokens.ts
6204
+ var TOOLS = ["zapier", "n8n"];
6205
+ async function createConnector(flags) {
6206
+ const json = jsonMode(flags);
6207
+ const tool = enumFlag(flags, "tool", TOOLS);
6208
+ if (!tool) die(`Usage: fillo tokens create-connector --tool ${TOOLS.join("|")}`);
6209
+ const res = await api("/cli/tokens/connector", {
6210
+ method: "POST",
6211
+ token: requireToken(),
6212
+ body: JSON.stringify({ tool })
6213
+ });
6214
+ const body = await readJson(res);
6215
+ if (res.status === 401) die(body.error ?? "Invalid or missing CLI token \u2014 run `fillo login`");
6216
+ if (!res.ok || !body.token) die(body.error ?? `tokens create-connector failed (${res.status}).`);
6217
+ if (!/^fcli_[A-Za-z0-9._~-]{8,512}$/.test(body.token)) {
6218
+ die("Fillo returned an unexpected token format.");
6219
+ }
6220
+ if (json) return emitResult(body);
6221
+ console.log(
6222
+ `
6223
+ ${okMark()} Created a ${bold(terminalText(body.label ?? tool))} connector token`
6224
+ );
6225
+ console.log(`
6226
+ ${body.token}
6227
+ `);
6228
+ console.log(` ${bold("Store it now")} \u2014 Fillo cannot show this token again.`);
6229
+ console.log(
6230
+ " Treat it like a password: it can read and change this project's forms and responses."
6231
+ );
6232
+ console.log(" Revoke it any time in Settings \u2192 Connections.\n");
6233
+ }
6234
+ async function tokens(subcommand, flags) {
6235
+ if (subcommand === void 0 || subcommand === "help") return tokensHelp();
6236
+ if (subcommand === "create-connector") return createConnector(flags);
6237
+ die(`Unknown tokens command: ${terminalText(subcommand)} (expected create-connector).`);
6238
+ }
6239
+ function tokensHelp() {
6240
+ console.log(`
6241
+ ${boldRaw("fillo tokens")} \u2014 credentials for no-code connectors
6242
+
6243
+ ${boldRaw("Commands")}
6244
+ tokens create-connector --tool ${TOOLS.join("|")}
6245
+ Mint the connector's API token \u2014 shown once, store it
6246
+
6247
+ ${dimRaw("The token is non-expiring so a live automation doesn't break silently, and")}
6248
+ ${dimRaw("stays revocable in Settings \u2192 Connections. For coding agents and your own")}
6249
+ ${dimRaw("integrations use `fillo keys create` instead \u2014 those are scoped.")}
6250
+ ${dimRaw("--json prints the raw server response on stdout, token included.")}
6251
+ `);
6252
+ }
6253
+ var tokensCommand = {
6254
+ name: "tokens",
6255
+ flags: ["tool"],
6256
+ run: (args, flags) => tokens(args[0], flags),
6257
+ help: tokensHelp
6258
+ };
6259
+
5685
6260
  // src/commands/webhooks.ts
5686
6261
  var dateTime2 = (iso) => iso.slice(0, 16).replace("T", " ");
5687
6262
  async function list3(handle, flags) {
@@ -5812,6 +6387,115 @@ var webhooksCommand = {
5812
6387
  help: webhooksHelp
5813
6388
  };
5814
6389
 
6390
+ // src/commands/projects.ts
6391
+ function authFailure(status5) {
6392
+ if (status5 === 401) die("Token invalid \u2014 run `fillo login` again.");
6393
+ }
6394
+ function saveSelectedProject(project) {
6395
+ if (!project.publishableKey.startsWith("pk_")) {
6396
+ die("Fillo returned an invalid publishable key; the selected project was not saved locally.");
6397
+ }
6398
+ const {
6399
+ claimUrl: _claimUrl,
6400
+ email: _email2,
6401
+ name: _name,
6402
+ claimToken: _claimToken,
6403
+ ...current
6404
+ } = readConfig();
6405
+ writeConfig({ ...current, pk: project.publishableKey });
6406
+ }
6407
+ async function list4(flags) {
6408
+ const res = await api("/cli/projects", { token: requireToken() });
6409
+ const body = await readJson(res);
6410
+ authFailure(res.status);
6411
+ if (!res.ok || !Array.isArray(body.projects)) {
6412
+ die(body.error ?? `project list failed (${res.status}).`);
6413
+ }
6414
+ if (jsonMode(flags)) return emitResult(body);
6415
+ console.log(`
6416
+ ${bold("Projects")}`);
6417
+ printTable(
6418
+ ["", "NAME", "SLUG", "ID"],
6419
+ body.projects.map((project) => [
6420
+ project.current ? "*" : "",
6421
+ terminalText(project.name),
6422
+ terminalText(project.slug),
6423
+ terminalText(project.id)
6424
+ ])
6425
+ );
6426
+ console.log(`
6427
+ ${dim("* selected for this CLI login")}`);
6428
+ console.log(` ${dim("Switch with `fillo project select <id-or-slug>`.")}
6429
+ `);
6430
+ }
6431
+ async function create(name, flags) {
6432
+ if (!name) die('Usage: fillo project create "Project name"');
6433
+ const res = await api("/cli/projects", {
6434
+ method: "POST",
6435
+ token: requireToken(),
6436
+ body: JSON.stringify({ name, source: "cli" })
6437
+ });
6438
+ const body = await readJson(res);
6439
+ authFailure(res.status);
6440
+ if (!res.ok || !body.project || body.selected !== true) {
6441
+ die(body.error ?? `project create failed (${res.status}).`);
6442
+ }
6443
+ saveSelectedProject(body.project);
6444
+ if (jsonMode(flags)) return emitResult(body);
6445
+ console.log(` ${okMark()} Created and selected ${bold(terminalText(body.project.name))}.`);
6446
+ console.log(` ${dim("Forms, keys, origins, identities, and agent access are isolated here.")}`);
6447
+ console.log(` ${dim("Members, billing, storage, and usage totals stay with the workspace.")}`);
6448
+ }
6449
+ async function select(target, flags) {
6450
+ if (!target) die("Usage: fillo project select <id|slug|unique-name>");
6451
+ const res = await api("/cli/projects/select", {
6452
+ method: "POST",
6453
+ token: requireToken(),
6454
+ body: JSON.stringify({ project: target, source: "cli" })
6455
+ });
6456
+ const body = await readJson(res);
6457
+ authFailure(res.status);
6458
+ if (!res.ok || !body.project || body.selected !== true) {
6459
+ die(body.error ?? `project select failed (${res.status}).`);
6460
+ }
6461
+ saveSelectedProject(body.project);
6462
+ if (jsonMode(flags)) return emitResult(body);
6463
+ console.log(` ${okMark()} Selected ${bold(terminalText(body.project.name))}.`);
6464
+ console.log(` ${dim("Future CLI commands use this project until you select another.")}`);
6465
+ }
6466
+ async function projects(subcommand, args, flags) {
6467
+ if (subcommand === void 0 || subcommand === "list" || subcommand === "ls") {
6468
+ return list4(flags);
6469
+ }
6470
+ if (subcommand === "help") return projectHelp();
6471
+ if (subcommand === "create") return create(args.join(" ").trim() || void 0, flags);
6472
+ if (subcommand === "select" || subcommand === "use") {
6473
+ return select(args.join(" ").trim() || void 0, flags);
6474
+ }
6475
+ die(`Unknown project command: ${terminalText(subcommand)} (expected list, create, or select).`);
6476
+ }
6477
+ function projectHelp() {
6478
+ console.log(`
6479
+ ${boldRaw("fillo project")} \u2014 create and select isolated sites/apps
6480
+
6481
+ ${boldRaw("Commands")}
6482
+ project List projects in the current workspace
6483
+ project create <name> Create and select a project
6484
+ project select <id|slug|name> Select a project for future commands
6485
+
6486
+ ${dimRaw("Requires an ordinary `fillo login`. Project-pinned agent handoff")}
6487
+ ${dimRaw("tokens cannot list, create, or select sibling projects.")}
6488
+ ${dimRaw("--json prints the server response on stdout.")}
6489
+ `);
6490
+ }
6491
+ var projectCommand = {
6492
+ name: "project",
6493
+ aliases: ["projects"],
6494
+ flags: [],
6495
+ run: (args, flags) => projects(args[0], args.slice(1), flags),
6496
+ help: projectHelp
6497
+ };
6498
+
5815
6499
  // src/index.ts
5816
6500
  var REGISTRY = [
5817
6501
  initCommand,
@@ -5819,6 +6503,7 @@ var REGISTRY = [
5819
6503
  loginCommand,
5820
6504
  logoutCommand,
5821
6505
  whoamiCommand,
6506
+ projectCommand,
5822
6507
  keysCommand,
5823
6508
  pushCommand,
5824
6509
  listCommand,
@@ -5830,8 +6515,11 @@ var REGISTRY = [
5830
6515
  skillCommand,
5831
6516
  storageCommand,
5832
6517
  slackCommand,
6518
+ discordCommand,
6519
+ tokensCommand,
5833
6520
  webhooksCommand,
5834
6521
  settingsCommand,
6522
+ brandingCommand,
5835
6523
  membersCommand,
5836
6524
  deleteCommand
5837
6525
  ];
@@ -5862,15 +6550,17 @@ function help() {
5862
6550
  ${dimRaw("--headless approve a code instead of the local browser handoff")}
5863
6551
  ${dimRaw("--run <id> --token <token> bind a fresh agent handoff login")}
5864
6552
  logout Forget the stored credentials
5865
- whoami Show the signed-in workspace
5866
- keys <cmd> Workspace API keys for agents/CI: create, list, revoke
6553
+ whoami Show the signed-in workspace and selected project
6554
+ project <cmd> Isolated sites/apps: list, create, select
6555
+ ${dimRaw("project create <name> \xB7 project select <id|slug|name>")}
6556
+ keys <cmd> Project API keys for agents/CI: create, list, revoke
5867
6557
  ${dimRaw("keys create --name <n> --preset read|agent|full")}
5868
6558
  push <file|-> Create/update a form from JSON (file or stdin)
5869
6559
  ${dimRaw("--handle <name> idempotent id for re-pushes")}
5870
6560
  ${dimRaw("--stage stage beside the live form for dashboard review")}
5871
6561
  ${dimRaw("--draft alias with a handle; legacy one-off draft without")}
5872
6562
  ${dimRaw("--allow-code allow a .mjs/.js schema (executes the file)")}
5873
- list List the workspace's forms and their live URLs
6563
+ list List the project's forms and their live URLs
5874
6564
  status <form> Show one form's status, live URL, and publish blockers
5875
6565
  ${dimRaw("<form> is a form id or handle")}
5876
6566
  publish <form> Publish staged changes (or a draft form) \u2014 prints the live URL
@@ -5886,9 +6576,13 @@ function help() {
5886
6576
  storage <cmd> Inspect/connect upload destinations (status, connect, disconnect)
5887
6577
  ${dimRaw("storage connect s3 \xB7 connect drive|box \xB7 disconnect <p>")}
5888
6578
  slack <cmd> Slack notifications: status, --channels, --refresh, connect
6579
+ discord <cmd> Discord destination: connect, webhook, status, enable, disable, roles
6580
+ ${dimRaw("discord enable <form> --fields a,b --early-signal 10 --role <g>/<r>")}
6581
+ tokens <cmd> Connector credentials: create-connector --tool zapier|n8n
5889
6582
  webhooks <cmd> A form's signed webhooks: list, add, set, remove
5890
6583
  ${dimRaw("webhooks add <form> --url <u> shows the signing secret once")}
5891
6584
  settings <cmd> A form's operational settings: get, set key=value
6585
+ branding [on|off] The workspace's "Powered by Fillo" badge (off needs Everything)
5892
6586
  members <cmd> Workspace members: list, invite <email>, cancel-invite <id>
5893
6587
  delete <cmd> Irreversible deletes: delete form <form> \xB7 delete workspace
5894
6588
  ${dimRaw("typed confirmation required; --confirm for agents, --yes never skips")}