@1db/cli 0.1.1 → 0.3.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/README.md CHANGED
@@ -13,7 +13,11 @@ npx @1db/cli init acme.com # scan + add the site, write DB_* vars to .env.lo
13
13
  npx @1db/cli send acme.com # send a test lead
14
14
  npx @1db/cli leads acme.com # see it arrive
15
15
  npx @1db/cli keys acme.com # print the site's endpoint, key and form slug
16
+ npx @1db/cli form acme.com --name "Quote request" # add another form, print its slug
17
+ npx @1db/cli site acme.com --product-type pallets # website settings
18
+ npx @1db/cli forwarding # lead forwarding rules
16
19
  ```
17
20
 
18
21
  Run `npx @1db/cli help` for every command, or `npm i -g @1db/cli` to get a plain `1db` command. Zero dependencies, Node 18+.
22
+ What a token may do depends on its scopes (`npx @1db/cli whoami`); see the [docs](https://www.1-db.com/docs/guides/ai-agents#scopes).
19
23
  Credentials live in `~/.config/1db/config.json` (mode 600); `ONEDB_TOKEN` overrides.
package/bin/1db.mjs CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  writeConfig,
21
21
  } from "../lib/core.mjs";
22
22
  import { browserLogin, installMcp, printMcpCommand, promptToken, saveToken } from "../lib/login.mjs";
23
+ import { CONFIG_HELP, configCommands, parseFieldList } from "../lib/config-commands.mjs";
23
24
 
24
25
  // ---------- commands ----------
25
26
  const commands = {
@@ -45,6 +46,7 @@ const commands = {
45
46
  const me = await api("/v1/agent/me");
46
47
  if (flags.json) return json(me);
47
48
  print(`${me.organization?.name} ${dim(`(${me.user?.email ?? me.user?.name})`)}`);
49
+ print(dim(`Scopes: ${(me.scopes ?? []).join(", ")}`));
48
50
  print(dim(`API ${settings().apiUrl}`));
49
51
  },
50
52
 
@@ -72,6 +74,31 @@ const commands = {
72
74
  writeCredentials(result, { ...flags, env: flags["env-file"] ? flags.env : false });
73
75
  },
74
76
 
77
+ async form({ positional, flags }) {
78
+ const website = positional[0];
79
+ if (!website || typeof flags.name !== "string") {
80
+ throw new CliError('Usage: 1db form <website> --name "Quote request" [--fields "email*,name,phone:phone,message:textarea"] [--preset "B2B"] [--slug quote]');
81
+ }
82
+ const result = await api("/v1/agent/forms", {
83
+ method: "POST",
84
+ body: {
85
+ website,
86
+ name: flags.name,
87
+ slug: typeof flags.slug === "string" ? flags.slug : undefined,
88
+ preset: typeof flags.preset === "string" ? flags.preset : undefined,
89
+ fields: typeof flags.fields === "string" ? parseFieldList(flags.fields) : undefined,
90
+ },
91
+ });
92
+ if (flags.json) return json(result);
93
+ const form = result.forms.find((f) => f.slug === result.formSlug);
94
+ print(`${result.created ? green("Created") : "Found"} ${bold(form.name)} on ${result.website.name}`);
95
+ print(dim(`Fields: ${form.fields.map((f) => f.key + (f.required ? "*" : "")).join(", ")}`));
96
+ // Its own env var, so DB_FORM_SLUG keeps pointing at the site's existing form
97
+ print(`\n${result.formEnvVar}=${form.slug}`);
98
+ print(dim(`\nPost its leads to ${result.leadsUrl} with {"formSlug": "${form.slug}", ...} and the site's DB_API_KEY.`));
99
+ print(dim(`Test it: ${CMD} send ${result.website.slug} --form ${form.slug}`));
100
+ },
101
+
75
102
  async send({ positional, flags }) {
76
103
  const [website, ...pairs] = positional;
77
104
  if (!website) throw new CliError("Usage: 1db send <website> [email=jane@acme.com message=\"Hi\" ...] [--form slug]");
@@ -129,11 +156,16 @@ const commands = {
129
156
  1db init <domain> Scan + add a website, write its keys to .env.local
130
157
  [--name "Acme"] [--env-file .env] [--prefix VITE_|NEXT_PUBLIC_|PUBLIC_] [--no-env]
131
158
  1db keys <website> Print a website's endpoint, key and form slug
159
+ 1db form <website> --name "Quote request"
160
+ Add another form to a website and print its slug
161
+ [--fields "email*,name,phone:phone,message:textarea"] [--preset "B2B"] [--slug quote]
132
162
  1db send <website> [k=v ...] Send a (test) lead through the real ingest endpoint
133
163
  1db leads [website] [--limit 20] Most recent leads
134
164
  1db mcp [--install] Print (or run) the command that adds 1DB's MCP server to Claude Code
135
165
  1db logout
136
166
 
167
+ ${CONFIG_HELP}
168
+
137
169
  Every command takes --json. Env: ONEDB_TOKEN, ONEDB_API_URL, ONEDB_APP_URL.`);
138
170
  },
139
171
  };
@@ -164,7 +196,8 @@ function writeCredentials(result, flags) {
164
196
 
165
197
  // ---------- main ----------
166
198
  const [name = "help", ...rest] = process.argv.slice(2);
167
- const command = commands[name === "--help" || name === "-h" ? "help" : name];
199
+ const all = { ...commands, ...configCommands };
200
+ const command = all[name === "--help" || name === "-h" ? "help" : name];
168
201
  if (!command) {
169
202
  console.error(`Unknown command "${name}". Run \`${CMD} help\`.`);
170
203
  process.exit(1);
@@ -0,0 +1,157 @@
1
+ // Configuration commands: website settings, form edits and notifications,
2
+ // lead forwarding, deletes. Each maps to a route in convex/http/agentConfig.ts.
3
+ import { CliError, api, dim, green, json, print, table } from "./core.mjs";
4
+
5
+ const s = (value) => (typeof value === "string" ? value : undefined);
6
+ const list = (value) => (typeof value === "string" ? value.split(",").map((x) => x.trim()).filter(Boolean) : undefined);
7
+ const post = (path, body) => api(path, { method: "POST", body });
8
+
9
+ /** "email*,name,phone:phone" → [{ key: "email", required: true }, { key: "name" }, { key: "phone", type: "phone" }] */
10
+ export function parseFieldList(spec) {
11
+ return spec
12
+ .split(",")
13
+ .map((x) => x.trim())
14
+ .filter(Boolean)
15
+ .map((x) => {
16
+ const required = x.endsWith("*");
17
+ const [key, type] = x.replace(/\*$/, "").split(":");
18
+ return { key, ...(type ? { type } : {}), ...(required ? { required } : {}) };
19
+ });
20
+ }
21
+
22
+ function printForm(result) {
23
+ const { form, notifications: n } = result;
24
+ print(`${form.name} ${dim(`(${form.slug}${result.website ? `, ${result.website.slug}` : ""})`)}`);
25
+ print(dim(`Fields: ${form.fields.map((f) => f.key + (f.required ? "*" : "")).join(", ")}`));
26
+ if (!n) return;
27
+ print(dim(`Email: ${n.email.enabled ? n.email.recipients.join(", ") : "off"}`));
28
+ print(dim(`Webhook: ${n.webhook.enabled ? n.webhook.url : "off"}`));
29
+ }
30
+
31
+ /** --enable / --disable → true / false / undefined */
32
+ const enabledFlag = (flags) => (flags.enable ? true : flags.disable ? false : undefined);
33
+
34
+ function ruleBody(flags) {
35
+ return {
36
+ name: s(flags.name),
37
+ endpoint: s(flags.endpoint),
38
+ authType: s(flags["auth-type"]),
39
+ authHeader: s(flags["auth-header"]),
40
+ authToken: s(flags["auth-token"]),
41
+ sourceIdentifier: s(flags.source),
42
+ enabled: enabledFlag(flags),
43
+ };
44
+ }
45
+
46
+ export const configCommands = {
47
+ async "product-types"({ flags }) {
48
+ const { productTypes } = await api("/v1/agent/product-types");
49
+ if (flags.json) return json(productTypes);
50
+ if (productTypes.length === 0) return print("No product types in this organization's catalog.");
51
+ table(productTypes, ["slug", "name", "active"]);
52
+ },
53
+
54
+ async site({ positional, flags }) {
55
+ const website = positional[0];
56
+ if (!website) {
57
+ throw new CliError("Usage: 1db site <website> [--name ..] [--domain ..] [--status active|development|maintenance|archived] [--product-type slug] [--product-types a,b]");
58
+ }
59
+ const result = await post("/v1/agent/website/update", {
60
+ website,
61
+ name: s(flags.name),
62
+ domain: s(flags.domain),
63
+ status: s(flags.status),
64
+ defaultProductType: s(flags["product-type"]),
65
+ productTypes: list(flags["product-types"]),
66
+ });
67
+ if (flags.json) return json(result);
68
+ const w = result.website;
69
+ print(`${green("Saved")} ${w.name} ${dim(`(${w.slug}${w.domain ? `, ${w.domain}` : ""})`)}`);
70
+ print(dim(`Status: ${w.status ?? "active"} · Default product type: ${w.defaultProductType ?? "none"}`));
71
+ },
72
+
73
+ async "form-edit"({ positional, flags }) {
74
+ const form = positional[0];
75
+ if (!form) throw new CliError('Usage: 1db form-edit <form slug> [--name ..] [--slug ..] [--fields "email*,name,message:textarea"]');
76
+ const result = s(flags.name) || s(flags.slug) || s(flags.fields)
77
+ ? await post("/v1/agent/forms/update", {
78
+ form,
79
+ name: s(flags.name),
80
+ slug: s(flags.slug),
81
+ fields: s(flags.fields) ? parseFieldList(flags.fields) : undefined,
82
+ })
83
+ : await api(`/v1/agent/form?form=${encodeURIComponent(form)}`);
84
+ if (flags.json) return json(result);
85
+ printForm(result);
86
+ },
87
+
88
+ async notify({ positional, flags }) {
89
+ const form = positional[0];
90
+ if (!form || (flags.emails === undefined && flags.webhook === undefined)) {
91
+ throw new CliError("Usage: 1db notify <form slug> [--emails a@x.com,b@x.com | --no-emails] [--webhook https://... | --no-webhook]");
92
+ }
93
+ const result = await post("/v1/agent/forms/notifications", {
94
+ form,
95
+ email: flags.emails === undefined ? undefined : flags.emails === false ? { enabled: false, recipients: [] } : { enabled: true, recipients: list(flags.emails) },
96
+ webhook: flags.webhook === undefined ? undefined : flags.webhook === false ? { enabled: false } : { enabled: true, url: s(flags.webhook) },
97
+ });
98
+ if (flags.json) return json(result);
99
+ printForm(result);
100
+ },
101
+
102
+ async forwarding({ positional, flags }) {
103
+ const [action = "list", id] = positional;
104
+ if (action === "list") {
105
+ const { rules } = await api("/v1/agent/forwarding");
106
+ if (flags.json) return json(rules);
107
+ if (rules.length === 0) return print("No forwarding rules.");
108
+ return table(
109
+ rules.map((r) => ({ ...r, on: r.enabled ? "yes" : "no", auth: r.hasAuthToken ? r.authType : "none" })),
110
+ ["id", "name", "endpoint", "on", "auth"],
111
+ );
112
+ }
113
+ let result;
114
+ if (action === "add") {
115
+ if (!s(flags.name) || !s(flags.endpoint)) {
116
+ throw new CliError("Usage: 1db forwarding add --name .. --endpoint https://.. [--auth-token ..] [--auth-header X-API-Key] [--auth-type api_key|bearer|none] [--source 1db] [--enable]");
117
+ }
118
+ result = await post("/v1/agent/forwarding", ruleBody(flags));
119
+ } else if (action === "update" && id) {
120
+ result = await post("/v1/agent/forwarding/update", { id, ...ruleBody(flags) });
121
+ } else if (action === "delete" && id) {
122
+ result = await post("/v1/agent/forwarding/delete", { id, confirm: s(flags.confirm) ?? "" });
123
+ } else {
124
+ throw new CliError("Usage: 1db forwarding [list | add .. | update <id> .. | delete <id> --confirm <rule name>]");
125
+ }
126
+ if (flags.json) return json(result);
127
+ if (result.deleted) return print(green(`Deleted rule ${result.deleted.rule}`));
128
+ const r = result.rule;
129
+ print(`${green("Saved")} ${r.name} ${dim(`(${r.id})`)} → ${r.endpoint}`);
130
+ print(dim(r.enabled ? "Enabled: new leads are forwarded." : "Disabled. Turn it on with --enable when ready."));
131
+ },
132
+
133
+ async delete({ positional, flags }) {
134
+ const [kind, target] = positional;
135
+ if (!["form", "site"].includes(kind) || !target || typeof flags.confirm !== "string") {
136
+ throw new CliError("Usage: 1db delete form <slug> --confirm <slug> | 1db delete site <website> --confirm <website slug>");
137
+ }
138
+ const result =
139
+ kind === "form"
140
+ ? await post("/v1/agent/forms/delete", { form: target, confirm: flags.confirm })
141
+ : await post("/v1/agent/website/delete", { website: target, confirm: flags.confirm });
142
+ if (flags.json) return json(result);
143
+ print(green(`Deleted ${kind} ${Object.values(result.deleted)[0]}`));
144
+ },
145
+ };
146
+
147
+ export const CONFIG_HELP = `
148
+ Configure (needs a token with the right scope; see \`1db whoami\`)
149
+ 1db site <website> [--status ..] [--product-type slug] [--name ..] [--domain ..]
150
+ 1db product-types List the organization's product types
151
+ 1db form-edit <form> [--name ..] [--slug ..] [--fields "email*,name"]
152
+ Show a form, or change it
153
+ 1db notify <form> [--emails a@x.com,b@x.com | --no-emails] [--webhook https://.. | --no-webhook]
154
+ 1db forwarding [list | add | update <id> | delete <id>]
155
+ add --name .. --endpoint https://.. [--auth-token ..] [--auth-header ..] [--enable]
156
+ 1db delete form <slug> --confirm <slug>
157
+ 1db delete site <website> --confirm <website slug>`;
package/lib/core.mjs CHANGED
@@ -36,7 +36,7 @@ export function settings() {
36
36
  }
37
37
 
38
38
  // ---------- args ----------
39
- export const BOOLEAN_FLAGS = new Set(["json", "help", "install", "paste", "relogin"]);
39
+ export const BOOLEAN_FLAGS = new Set(["json", "help", "install", "paste", "relogin", "enable", "disable"]);
40
40
 
41
41
  export function parseArgs(argv) {
42
42
  const positional = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1db/cli",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Connect websites to 1DB from your terminal or an AI agent: onboard a domain, get its lead keys, send and list leads.",
5
5
  "type": "module",
6
6
  "bin": {