@1db/cli 0.0.0-stage → 0.1.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 +18 -2
- package/bin/1db.mjs +176 -0
- package/lib/core.mjs +142 -0
- package/lib/login.mjs +99 -0
- package/package.json +27 -4
package/README.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 1db
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Connect websites to [1DB](https://www.1-db.com) lead capture from your terminal or an AI agent.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @1db/cli setup # sign in through your browser + add 1DB to Claude Code
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Then, in any project, ask Claude: “set up 1db for acme.com”. Or drive it yourself:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @1db/cli init acme.com # scan + add the site, write DB_* vars to .env.local
|
|
13
|
+
npx @1db/cli send acme.com # send a test lead
|
|
14
|
+
npx @1db/cli leads acme.com # see it arrive
|
|
15
|
+
npx @1db/cli keys acme.com # print the site's endpoint, key and form slug
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Run `npx @1db/cli help` for every command, or `npm i -g @1db/cli` to get a plain `1db` command. Zero dependencies, Node 18+.
|
|
19
|
+
Credentials live in `~/.config/1db/config.json` (mode 600); `ONEDB_TOKEN` overrides.
|
package/bin/1db.mjs
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 1DB CLI: zero dependencies, talks to the agent API (/v1/agent/*) with an
|
|
3
|
+
// agent token. Run `1db help` for commands.
|
|
4
|
+
import {
|
|
5
|
+
CliError,
|
|
6
|
+
api,
|
|
7
|
+
bold,
|
|
8
|
+
dim,
|
|
9
|
+
ensureIgnored,
|
|
10
|
+
envLines,
|
|
11
|
+
green,
|
|
12
|
+
json,
|
|
13
|
+
parseArgs,
|
|
14
|
+
print,
|
|
15
|
+
readConfig,
|
|
16
|
+
settings,
|
|
17
|
+
table,
|
|
18
|
+
upsertEnvFile,
|
|
19
|
+
writeConfig,
|
|
20
|
+
} from "../lib/core.mjs";
|
|
21
|
+
import { browserLogin, installMcp, printMcpCommand, promptToken, saveToken } from "../lib/login.mjs";
|
|
22
|
+
|
|
23
|
+
// ---------- commands ----------
|
|
24
|
+
const commands = {
|
|
25
|
+
async login({ flags }) {
|
|
26
|
+
if (flags["api-url"] || flags["app-url"]) {
|
|
27
|
+
writeConfig({
|
|
28
|
+
...readConfig(),
|
|
29
|
+
...(flags["api-url"] ? { apiUrl: flags["api-url"] } : {}),
|
|
30
|
+
...(flags["app-url"] ? { appUrl: flags["app-url"] } : {}),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const token = typeof flags.token === "string" ? flags.token : flags.paste ? await promptToken() : await browserLogin();
|
|
34
|
+
await saveToken(token);
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async logout() {
|
|
38
|
+
const { token, ...rest } = readConfig();
|
|
39
|
+
writeConfig(rest);
|
|
40
|
+
print(token ? "Logged out." : "Already logged out.");
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
async whoami({ flags }) {
|
|
44
|
+
const me = await api("/v1/agent/me");
|
|
45
|
+
if (flags.json) return json(me);
|
|
46
|
+
print(`${me.organization?.name} ${dim(`(${me.user?.email ?? me.user?.name})`)}`);
|
|
47
|
+
print(dim(`API ${settings().apiUrl}`));
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async websites({ flags }) {
|
|
51
|
+
const { websites } = await api("/v1/agent/websites");
|
|
52
|
+
if (flags.json) return json(websites);
|
|
53
|
+
if (websites.length === 0) return print("No websites yet. Add one with `1db init <domain>`.");
|
|
54
|
+
table(websites, ["slug", "name", "domain", "framework"]);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async init({ positional, flags }) {
|
|
58
|
+
const domain = positional[0];
|
|
59
|
+
if (!domain) throw new CliError("Usage: 1db init <domain> [--name \"Acme\"] [--env-file .env.local] [--prefix VITE_]");
|
|
60
|
+
if (!flags.json) print(dim(`Scanning ${domain} and setting it up in 1DB…`));
|
|
61
|
+
const result = await api("/v1/agent/onboard", { method: "POST", body: { domain, name: flags.name } });
|
|
62
|
+
if (flags.json) return json(result);
|
|
63
|
+
writeCredentials(result, flags);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async keys({ positional, flags }) {
|
|
67
|
+
const website = positional[0];
|
|
68
|
+
if (!website) throw new CliError("Usage: 1db keys <website> [--env-file .env.local]");
|
|
69
|
+
const result = await api(`/v1/agent/website?website=${encodeURIComponent(website)}`);
|
|
70
|
+
if (flags.json) return json(result);
|
|
71
|
+
writeCredentials(result, { ...flags, env: flags["env-file"] ? flags.env : false });
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async send({ positional, flags }) {
|
|
75
|
+
const [website, ...pairs] = positional;
|
|
76
|
+
if (!website) throw new CliError("Usage: 1db send <website> [email=jane@acme.com message=\"Hi\" ...] [--form slug]");
|
|
77
|
+
const data = pairs.length
|
|
78
|
+
? Object.fromEntries(pairs.map((p) => [p.split("=")[0], p.split("=").slice(1).join("=")]))
|
|
79
|
+
: undefined;
|
|
80
|
+
const result = await api("/v1/agent/test-lead", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: { website, data, formSlug: typeof flags.form === "string" ? flags.form : undefined },
|
|
83
|
+
});
|
|
84
|
+
if (flags.json) return json(result);
|
|
85
|
+
if (result.status === 200) print(green(`Lead sent to ${result.formSlug}: ${result.result?.leadId}`));
|
|
86
|
+
else throw new CliError(`Ingest returned ${result.status}: ${JSON.stringify(result.result)}`);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async leads({ positional, flags }) {
|
|
90
|
+
const website = positional[0];
|
|
91
|
+
const limit = Number(flags.limit) || 20;
|
|
92
|
+
const { leads } = await api(`/v1/agent/leads?limit=${limit}${website ? `&website=${encodeURIComponent(website)}` : ""}`);
|
|
93
|
+
if (flags.json) return json(leads);
|
|
94
|
+
if (leads.length === 0) return print("No leads yet.");
|
|
95
|
+
table(
|
|
96
|
+
leads.map((l) => ({
|
|
97
|
+
when: new Date(l.createdAt).toLocaleString(),
|
|
98
|
+
email: l.email ?? "",
|
|
99
|
+
name: [l.data?.first_name, l.data?.last_name].filter(Boolean).join(" "),
|
|
100
|
+
sync: l.syncStatus,
|
|
101
|
+
})),
|
|
102
|
+
["when", "email", "name", "sync"],
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async mcp({ flags }) {
|
|
107
|
+
if (!settings().token) throw new CliError("Not logged in. Run `1db login` first.");
|
|
108
|
+
if (flags.install) return installMcp();
|
|
109
|
+
printMcpCommand();
|
|
110
|
+
print(dim("\nOr run `1db mcp --install` to do it for you."));
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/** One step for a new machine: sign in, then add the MCP server to Claude Code. */
|
|
114
|
+
async setup({ flags }) {
|
|
115
|
+
if (!settings().token || flags.relogin) await commands.login({ flags });
|
|
116
|
+
else print(dim("Already logged in (use --relogin to switch organization)."));
|
|
117
|
+
installMcp();
|
|
118
|
+
print(`\nIn any project, ask Claude: ${bold("“set up 1db for acme.com”")}`);
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
help() {
|
|
122
|
+
print(`${bold("1db")}: connect websites to 1DB lead capture
|
|
123
|
+
|
|
124
|
+
1db setup Sign in and add 1DB's MCP server to Claude Code (start here)
|
|
125
|
+
1db login Sign in through your browser (or --token 1db_at_..., --paste)
|
|
126
|
+
1db whoami Show the organization the token acts for
|
|
127
|
+
1db websites List websites
|
|
128
|
+
1db init <domain> Scan + add a website, write its keys to .env.local
|
|
129
|
+
[--name "Acme"] [--env-file .env] [--prefix VITE_|NEXT_PUBLIC_|PUBLIC_] [--no-env]
|
|
130
|
+
1db keys <website> Print a website's endpoint, key and form slug
|
|
131
|
+
1db send <website> [k=v ...] Send a (test) lead through the real ingest endpoint
|
|
132
|
+
1db leads [website] [--limit 20] Most recent leads
|
|
133
|
+
1db mcp [--install] Print (or run) the command that adds 1DB's MCP server to Claude Code
|
|
134
|
+
1db logout
|
|
135
|
+
|
|
136
|
+
Every command takes --json. Env: ONEDB_TOKEN, ONEDB_API_URL, ONEDB_APP_URL.`);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
function writeCredentials(result, flags) {
|
|
141
|
+
const site = result.website;
|
|
142
|
+
print(`${result.created ? green("Created") : "Found"} ${bold(site.name)} ${dim(`(${site.slug}${site.domain ? `, ${site.domain}` : ""})`)}`);
|
|
143
|
+
const form = result.forms?.find((f) => f.slug === result.formSlug);
|
|
144
|
+
if (form) print(dim(`Form ${form.slug}: ${form.fields.map((f) => f.key + (f.required ? "*" : "")).join(", ")}`));
|
|
145
|
+
|
|
146
|
+
const prefix = typeof flags.prefix === "string" ? flags.prefix : "";
|
|
147
|
+
const entries = envLines(result.env, prefix);
|
|
148
|
+
if (flags.env === false) {
|
|
149
|
+
print();
|
|
150
|
+
for (const [k, v] of entries) print(`${k}=${v}`);
|
|
151
|
+
} else {
|
|
152
|
+
const file = typeof flags["env-file"] === "string" ? flags["env-file"] : ".env.local";
|
|
153
|
+
const path = upsertEnvFile(file, entries);
|
|
154
|
+
ensureIgnored(file);
|
|
155
|
+
print(green(`Wrote ${entries.map(([k]) => k).join(", ")} to ${path}`));
|
|
156
|
+
}
|
|
157
|
+
print();
|
|
158
|
+
print(`Send leads: POST ${result.leadsUrl}`);
|
|
159
|
+
print(dim(` X-API-Key: $${prefix}DB_API_KEY`));
|
|
160
|
+
print(dim(` {"formSlug": "$${prefix}DB_FORM_SLUG", "email": "...", ...}`));
|
|
161
|
+
print(dim(`Test it: 1db send ${site.slug}`));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------- main ----------
|
|
165
|
+
const [name = "help", ...rest] = process.argv.slice(2);
|
|
166
|
+
const command = commands[name === "--help" || name === "-h" ? "help" : name];
|
|
167
|
+
if (!command) {
|
|
168
|
+
console.error(`Unknown command "${name}". Run \`1db help\`.`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
await command(parseArgs(rest));
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error(error instanceof CliError ? `Error: ${error.message}` : error);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
package/lib/core.mjs
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Shared plumbing for the 1db CLI: config file, args, HTTP, output, env files.
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, appendFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_API_URL = "https://gallant-goshawk-513.convex.site";
|
|
8
|
+
export const DEFAULT_APP_URL = "https://www.1-db.com";
|
|
9
|
+
export const CONFIG_DIR = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "1db");
|
|
10
|
+
export const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
11
|
+
|
|
12
|
+
// ---------- config ----------
|
|
13
|
+
export function readConfig() {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
16
|
+
} catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function writeConfig(config) {
|
|
22
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
23
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
|
|
24
|
+
chmodSync(CONFIG_FILE, 0o600);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function settings() {
|
|
28
|
+
const config = readConfig();
|
|
29
|
+
return {
|
|
30
|
+
token: process.env.ONEDB_TOKEN || config.token,
|
|
31
|
+
apiUrl: (process.env.ONEDB_API_URL || config.apiUrl || DEFAULT_API_URL).replace(/\/$/, ""),
|
|
32
|
+
appUrl: (process.env.ONEDB_APP_URL || config.appUrl || DEFAULT_APP_URL).replace(/\/$/, ""),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------- args ----------
|
|
37
|
+
export const BOOLEAN_FLAGS = new Set(["json", "help", "install", "paste", "relogin"]);
|
|
38
|
+
|
|
39
|
+
export function parseArgs(argv) {
|
|
40
|
+
const positional = [];
|
|
41
|
+
const flags = {};
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const arg = argv[i];
|
|
44
|
+
if (arg.startsWith("--")) {
|
|
45
|
+
const [key, inline] = arg.slice(2).split("=", 2);
|
|
46
|
+
if (inline !== undefined) flags[key] = inline;
|
|
47
|
+
else if (key.startsWith("no-")) flags[key.slice(3)] = false;
|
|
48
|
+
else if (BOOLEAN_FLAGS.has(key)) flags[key] = true;
|
|
49
|
+
else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith("--")) flags[key] = argv[++i];
|
|
50
|
+
else flags[key] = true;
|
|
51
|
+
} else positional.push(arg);
|
|
52
|
+
}
|
|
53
|
+
return { positional, flags };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------- http ----------
|
|
57
|
+
export class CliError extends Error {}
|
|
58
|
+
|
|
59
|
+
export async function api(path, { method = "GET", body } = {}) {
|
|
60
|
+
const { token, apiUrl } = settings();
|
|
61
|
+
if (!token) throw new CliError("Not logged in. Run `1db login` (or set ONEDB_TOKEN).");
|
|
62
|
+
let res;
|
|
63
|
+
try {
|
|
64
|
+
res = await fetch(`${apiUrl}${path}`, {
|
|
65
|
+
method,
|
|
66
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
67
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
throw new CliError(`Can't reach ${apiUrl}: ${error.message}`);
|
|
71
|
+
}
|
|
72
|
+
const data = await res.json().catch(() => ({}));
|
|
73
|
+
if (!res.ok && !(path === "/v1/agent/test-lead" && data.result)) {
|
|
74
|
+
throw new CliError(`${data.error || `HTTP ${res.status}`}${data.hint ? `\n ${data.hint}` : ""}`);
|
|
75
|
+
}
|
|
76
|
+
return data;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------- output ----------
|
|
80
|
+
export const isTTY = process.stdout.isTTY;
|
|
81
|
+
export const dim = (s) => (isTTY ? `\x1b[2m${s}\x1b[0m` : s);
|
|
82
|
+
export const bold = (s) => (isTTY ? `\x1b[1m${s}\x1b[0m` : s);
|
|
83
|
+
export const green = (s) => (isTTY ? `\x1b[32m${s}\x1b[0m` : s);
|
|
84
|
+
export const print = (s = "") => process.stdout.write(s + "\n");
|
|
85
|
+
export const json = (data) => print(JSON.stringify(data, null, 2));
|
|
86
|
+
|
|
87
|
+
export function table(rows, columns) {
|
|
88
|
+
if (rows.length === 0) return;
|
|
89
|
+
const widths = columns.map((c) => Math.max(c.length, ...rows.map((r) => String(r[c] ?? "").length)));
|
|
90
|
+
print(bold(columns.map((c, i) => c.padEnd(widths[i])).join(" ")));
|
|
91
|
+
for (const r of rows) print(columns.map((c, i) => String(r[c] ?? "").padEnd(widths[i])).join(" "));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------- env files ----------
|
|
95
|
+
export function envLines(env, prefix) {
|
|
96
|
+
return Object.entries(env)
|
|
97
|
+
.filter(([, value]) => value)
|
|
98
|
+
.map(([key, value]) => [`${prefix}${key}`, value]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Set keys in a dotenv file, replacing existing lines and keeping everything else. */
|
|
102
|
+
export function upsertEnvFile(file, entries) {
|
|
103
|
+
const path = resolve(file);
|
|
104
|
+
const lines = existsSync(path) ? readFileSync(path, "utf8").split("\n") : [];
|
|
105
|
+
const pending = new Map(entries);
|
|
106
|
+
const out = lines.map((line) => {
|
|
107
|
+
const key = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/)?.[1];
|
|
108
|
+
if (key && pending.has(key)) {
|
|
109
|
+
const value = pending.get(key);
|
|
110
|
+
pending.delete(key);
|
|
111
|
+
return `${key}=${value}`;
|
|
112
|
+
}
|
|
113
|
+
return line;
|
|
114
|
+
});
|
|
115
|
+
if (pending.size > 0) {
|
|
116
|
+
while (out.length && out[out.length - 1] === "") out.pop();
|
|
117
|
+
if (out.length) out.push("");
|
|
118
|
+
out.push("# 1DB lead capture");
|
|
119
|
+
for (const [key, value] of pending) out.push(`${key}=${value}`);
|
|
120
|
+
}
|
|
121
|
+
writeFileSync(path, out.join("\n").replace(/\n*$/, "\n"));
|
|
122
|
+
return path;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Keep secrets out of git: ignore the env file, and warn if git already tracks it. */
|
|
126
|
+
export function ensureIgnored(file) {
|
|
127
|
+
const git = (args) => execFileSync("git", args, { stdio: "ignore" });
|
|
128
|
+
try {
|
|
129
|
+
git(["ls-files", "--error-unmatch", file]);
|
|
130
|
+
print(`Warning: git already tracks ${file}. Run \`git rm --cached ${file}\` so the key isn't committed.`);
|
|
131
|
+
return;
|
|
132
|
+
} catch {
|
|
133
|
+
// Not tracked (or not a git repo)
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
git(["check-ignore", "-q", file]);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error.status !== 1) return; // 128: not a git repo
|
|
139
|
+
appendFileSync(resolve(".gitignore"), `\n${file.replace(/^\.\//, "")}\n`);
|
|
140
|
+
print(dim(`Added ${file} to .gitignore`));
|
|
141
|
+
}
|
|
142
|
+
}
|
package/lib/login.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Sign-in (browser device flow or pasted token) and Claude Code MCP install.
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
5
|
+
import { CliError, CONFIG_FILE, api, bold, dim, green, print, readConfig, settings, writeConfig } from "./core.mjs";
|
|
6
|
+
|
|
7
|
+
// ---------- login ----------
|
|
8
|
+
export function openBrowser(url) {
|
|
9
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
10
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
11
|
+
try {
|
|
12
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).on("error", () => {}).unref();
|
|
13
|
+
} catch {
|
|
14
|
+
// No browser here (CI, SSH): the printed link still works from any device
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Device flow: print a code, open the approval page, poll until approved. */
|
|
19
|
+
export async function browserLogin() {
|
|
20
|
+
const { apiUrl, appUrl } = settings();
|
|
21
|
+
const post = async (path, body) => {
|
|
22
|
+
const res = await fetch(`${apiUrl}${path}`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "Content-Type": "application/json" },
|
|
25
|
+
body: JSON.stringify(body),
|
|
26
|
+
}).catch((error) => {
|
|
27
|
+
throw new CliError(`Can't reach ${apiUrl}: ${error.message}`);
|
|
28
|
+
});
|
|
29
|
+
const data = await res.json().catch(() => ({}));
|
|
30
|
+
if (!res.ok) throw new CliError(data.error || `HTTP ${res.status}`);
|
|
31
|
+
return data;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const start = await post("/v1/agent/login/start", { clientName: `1db CLI on ${hostname()}` });
|
|
35
|
+
const url = `${appUrl}/cli?code=${start.userCode}`;
|
|
36
|
+
print(`Approve this sign-in in your browser: ${bold(url)}`);
|
|
37
|
+
print(`Check the code matches: ${bold(start.userCode)}`);
|
|
38
|
+
openBrowser(url);
|
|
39
|
+
|
|
40
|
+
const deadline = Date.now() + start.expiresIn * 1000;
|
|
41
|
+
while (Date.now() < deadline) {
|
|
42
|
+
await new Promise((r) => setTimeout(r, (start.interval || 2) * 1000));
|
|
43
|
+
const result = await post("/v1/agent/login/poll", { deviceCode: start.deviceCode });
|
|
44
|
+
if (result.status === "approved") return result.token;
|
|
45
|
+
if (result.status === "denied") throw new CliError("Sign-in was denied in the browser.");
|
|
46
|
+
if (result.status === "expired") break;
|
|
47
|
+
}
|
|
48
|
+
throw new CliError("The sign-in code expired. Run `1db login` again.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function promptToken() {
|
|
52
|
+
const { appUrl } = settings();
|
|
53
|
+
print(`Create an agent token at ${bold(`${appUrl}/settings/api-keys`)} (Agent tokens), then paste it here.`);
|
|
54
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
55
|
+
const token = (await rl.question("Agent token: ")).trim();
|
|
56
|
+
rl.close();
|
|
57
|
+
return token;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function saveToken(token) {
|
|
61
|
+
if (!token?.startsWith("1db_at_")) throw new CliError("That isn't an agent token (they start with 1db_at_).");
|
|
62
|
+
const config = readConfig();
|
|
63
|
+
writeConfig({ ...config, token });
|
|
64
|
+
const me = await api("/v1/agent/me").catch((error) => {
|
|
65
|
+
writeConfig(config);
|
|
66
|
+
throw error;
|
|
67
|
+
});
|
|
68
|
+
print(green(`Logged in to ${me.organization?.name ?? "1DB"} as ${me.user?.email ?? me.user?.name ?? "you"}.`));
|
|
69
|
+
print(dim(`Saved to ${CONFIG_FILE}`));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function mcpAddArgs() {
|
|
73
|
+
const { token, appUrl } = settings();
|
|
74
|
+
return ["mcp", "add", "--scope", "user", "--transport", "http", "1db", `${appUrl}/api/mcp`, "--header", `Authorization: Bearer ${token}`];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Print the `claude mcp add` command for clients this CLI can't drive. */
|
|
78
|
+
export function printMcpCommand() {
|
|
79
|
+
print("Add 1DB to Claude Code in every project:\n");
|
|
80
|
+
print(` claude ${mcpAddArgs().map((a) => (/\s/.test(a) ? `"${a}"` : a)).join(" ")}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function installMcp() {
|
|
84
|
+
const run = (args) => execFileSync("claude", args, { stdio: "pipe" }).toString();
|
|
85
|
+
try {
|
|
86
|
+
run(["--version"]);
|
|
87
|
+
} catch {
|
|
88
|
+
print("Claude Code isn't installed here. Add the server to your MCP client with:");
|
|
89
|
+
return printMcpCommand();
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
run(["mcp", "remove", "--scope", "user", "1db"]);
|
|
93
|
+
} catch {
|
|
94
|
+
// Not added yet
|
|
95
|
+
}
|
|
96
|
+
run(mcpAddArgs());
|
|
97
|
+
print(green("Added the 1db MCP server to Claude Code (user scope, every project)."));
|
|
98
|
+
}
|
|
99
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1db/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"1db": "bin/1db.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"lib",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"license": "UNLICENSED",
|
|
18
|
+
"publishConfig": { "access": "public" },
|
|
19
|
+
"homepage": "https://www.1-db.com/docs/guides/ai-agents",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"1db",
|
|
22
|
+
"leads",
|
|
23
|
+
"lead-capture",
|
|
24
|
+
"mcp",
|
|
25
|
+
"claude",
|
|
26
|
+
"cli",
|
|
27
|
+
"crm"
|
|
28
|
+
]
|
|
29
|
+
}
|