@formo/cli 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/dist/index.js ADDED
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const incur_1 = require("incur");
5
+ const alerts_1 = require("./commands/alerts");
6
+ const boards_1 = require("./commands/boards");
7
+ const charts_1 = require("./commands/charts");
8
+ const contracts_1 = require("./commands/contracts");
9
+ const import_1 = require("./commands/import");
10
+ const profiles_1 = require("./commands/profiles");
11
+ const query_1 = require("./commands/query");
12
+ const segments_1 = require("./commands/segments");
13
+ const config_1 = require("./lib/config");
14
+ const ui_1 = require("./lib/ui");
15
+ const DASHBOARD_URL = 'https://app.formo.so';
16
+ const DOCS_URL = 'https://docs.formo.so';
17
+ const API_BASE_URL = 'https://api.formo.so';
18
+ function loginGuide() {
19
+ return [
20
+ '',
21
+ ui_1.color.boldGreen('How to get your API key:'),
22
+ '',
23
+ ` ${ui_1.color.white('1.')} Go to ${ui_1.color.cyan(DASHBOARD_URL)}`,
24
+ ` ${ui_1.color.white('2.')} Navigate to ${ui_1.color.bold('Settings')} → ${ui_1.color.bold('API Keys')}`,
25
+ ` ${ui_1.color.white('3.')} Click ${ui_1.color.bold('"Create API Key"')} and copy the key`,
26
+ ` ${ui_1.color.white('4.')} Run:`,
27
+ '',
28
+ ` ${ui_1.color.green('formo login <formo_your_api_key_here>')}`,
29
+ '',
30
+ ui_1.color.dim(` Or set the environment variable:`),
31
+ '',
32
+ ` ${ui_1.color.green('export FORMO_API_KEY=formo_your_api_key_here')}`,
33
+ '',
34
+ ui_1.color.dim(` Docs: ${DOCS_URL}`),
35
+ '',
36
+ ].join('\n');
37
+ }
38
+ async function validateAndFetchWorkspace(apiKey) {
39
+ try {
40
+ const res = await fetch(`${API_BASE_URL}/api/validate-api-key`, {
41
+ method: 'POST',
42
+ headers: { 'Content-Type': 'application/json' },
43
+ body: JSON.stringify({ apiKey }),
44
+ });
45
+ if (!res.ok)
46
+ return null;
47
+ const body = (await res.json());
48
+ if (!body.isSuccess || !body.data)
49
+ return null;
50
+ return {
51
+ workspace: body.data.details,
52
+ projectId: body.data.scopes?.project_id ?? '',
53
+ };
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ const cli = incur_1.Cli.create('formo', {
60
+ version: '0.1.0',
61
+ description: 'Formo API CLI — Web3 analytics from the terminal',
62
+ sync: {
63
+ suggestions: [
64
+ 'get the profile for wallet 0xabc',
65
+ 'search profiles with net worth > 10000',
66
+ 'run a SQL query on my analytics data',
67
+ 'search profiles ordered by last_onchain desc',
68
+ 'list all project alerts',
69
+ 'create an alert for high-value transactions',
70
+ 'list all dashboard boards',
71
+ 'list charts in a board',
72
+ 'list all tracked contracts',
73
+ 'register a new smart contract',
74
+ 'list user segments',
75
+ 'import wallet addresses',
76
+ ],
77
+ },
78
+ });
79
+ // ── login ──
80
+ cli.command('login', {
81
+ description: 'Authenticate with your Formo API key',
82
+ args: incur_1.z.object({
83
+ apiKey: incur_1.z.string().optional().describe('Your formo_ API key'),
84
+ }),
85
+ options: incur_1.z.object({}),
86
+ examples: [
87
+ { args: { apiKey: 'formo_abc123' }, description: 'Save API key' },
88
+ ],
89
+ async run({ args }) {
90
+ // No key provided → show guide
91
+ if (!args.apiKey) {
92
+ if (process.stdout.isTTY) {
93
+ process.stderr.write(loginGuide());
94
+ }
95
+ return { ok: false, message: 'No API key provided. See guide above.' };
96
+ }
97
+ const isTTY = process.stdout.isTTY;
98
+ // Validate the key against the API
99
+ if (isTTY) {
100
+ process.stderr.write('\n' + ui_1.color.dim('Validating API key…') + '\n');
101
+ }
102
+ const workspaceInfo = await validateAndFetchWorkspace(args.apiKey);
103
+ // Save key + workspace info (save even if validation fails — user might be offline)
104
+ // Only include workspace/projectId when present so offline logins don't wipe existing values
105
+ (0, config_1.saveConfig)({
106
+ apiKey: args.apiKey,
107
+ ...(workspaceInfo?.workspace !== undefined && { workspace: workspaceInfo.workspace }),
108
+ ...(workspaceInfo?.projectId !== undefined && { projectId: workspaceInfo.projectId }),
109
+ });
110
+ // Show feedback in human mode
111
+ if (isTTY) {
112
+ const masked = args.apiKey.length > 12
113
+ ? args.apiKey.slice(0, 8) + '…' + args.apiKey.slice(-4)
114
+ : '***';
115
+ if (workspaceInfo) {
116
+ process.stderr.write((0, ui_1.success)('API key validated and saved!') +
117
+ '\n' +
118
+ (0, ui_1.info)(`Key: ${ui_1.color.dim(masked)}`) +
119
+ '\n' +
120
+ (0, ui_1.info)(`Workspace: ${ui_1.color.bold(workspaceInfo.workspace)}`) +
121
+ '\n' +
122
+ (workspaceInfo.projectId
123
+ ? (0, ui_1.info)(`Project: ${ui_1.color.dim(workspaceInfo.projectId)}`) + '\n'
124
+ : '') +
125
+ (0, ui_1.info)(`Config: ${ui_1.color.dim('~/.config/formo/config.json')}`) +
126
+ '\n\n' +
127
+ ui_1.color.dim('You can now use all formo commands.') +
128
+ '\n' +
129
+ ui_1.color.dim('Run ') +
130
+ ui_1.color.green('formo --help') +
131
+ ui_1.color.dim(' to see available commands.') +
132
+ '\n\n');
133
+ }
134
+ else {
135
+ process.stderr.write((0, ui_1.warn)('API key saved but could not be validated.') +
136
+ '\n' +
137
+ (0, ui_1.info)(`Key: ${ui_1.color.dim(masked)}`) +
138
+ '\n' +
139
+ (0, ui_1.info)(`Config: ${ui_1.color.dim('~/.config/formo/config.json')}`) +
140
+ '\n' +
141
+ ui_1.color.dim('The API might be unreachable. Your key is saved and will be used for requests.') +
142
+ '\n\n');
143
+ }
144
+ }
145
+ return {
146
+ ok: true,
147
+ message: workspaceInfo
148
+ ? 'API key validated and saved'
149
+ : 'API key saved (not validated)',
150
+ workspace: workspaceInfo?.workspace ?? null,
151
+ projectId: workspaceInfo?.projectId ?? null,
152
+ };
153
+ },
154
+ });
155
+ // ── logout ──
156
+ cli.command('logout', {
157
+ description: 'Remove saved API key and clear authentication',
158
+ run() {
159
+ const hadKey = !!(0, config_1.readConfig)().apiKey;
160
+ (0, config_1.clearConfig)();
161
+ if (process.stdout.isTTY) {
162
+ process.stderr.write('\n');
163
+ if (hadKey) {
164
+ process.stderr.write((0, ui_1.success)('Logged out successfully.') +
165
+ '\n' +
166
+ (0, ui_1.info)('API key removed from ~/.config/formo/config.json') +
167
+ '\n');
168
+ }
169
+ else {
170
+ process.stderr.write((0, ui_1.info)('No saved API key found — already logged out.') + '\n');
171
+ }
172
+ if (process.env.FORMO_API_KEY) {
173
+ process.stderr.write('\n' +
174
+ (0, ui_1.warn)(`${ui_1.color.yellow('FORMO_API_KEY')} env var is still set.`) +
175
+ '\n' +
176
+ (0, ui_1.info)(`Run ${ui_1.color.green('unset FORMO_API_KEY')} to fully log out.`) +
177
+ '\n');
178
+ }
179
+ process.stderr.write('\n');
180
+ }
181
+ return {
182
+ ok: true,
183
+ message: hadKey ? 'API key removed' : 'Already logged out',
184
+ envVarSet: !!process.env.FORMO_API_KEY,
185
+ };
186
+ },
187
+ });
188
+ // ── status ──
189
+ cli.command('status', {
190
+ description: 'Show current authentication and CLI status',
191
+ run() {
192
+ const apiKey = (0, config_1.getApiKey)();
193
+ const config = (0, config_1.readConfig)();
194
+ const source = process.env.FORMO_API_KEY
195
+ ? 'FORMO_API_KEY env var'
196
+ : 'config file';
197
+ if (process.stdout.isTTY) {
198
+ process.stderr.write('\n');
199
+ if (apiKey) {
200
+ const masked = apiKey.length > 12
201
+ ? apiKey.slice(0, 8) + '…' + apiKey.slice(-4)
202
+ : '***';
203
+ process.stderr.write((0, ui_1.success)('Authenticated') +
204
+ '\n' +
205
+ (0, ui_1.info)(`Key: ${ui_1.color.dim(masked)}`) +
206
+ '\n' +
207
+ (0, ui_1.info)(`Source: ${ui_1.color.dim(source)}`) +
208
+ '\n' +
209
+ (config.workspace
210
+ ? (0, ui_1.info)(`Workspace: ${ui_1.color.bold(config.workspace)}`) + '\n'
211
+ : '') +
212
+ (config.projectId
213
+ ? (0, ui_1.info)(`Project ID: ${ui_1.color.dim(config.projectId)}`) + '\n'
214
+ : '') +
215
+ '\n');
216
+ }
217
+ else {
218
+ process.stderr.write((0, ui_1.error)('Not authenticated') +
219
+ '\n\n' +
220
+ (0, ui_1.info)(`Run ${ui_1.color.green('formo login')} to get started.`) +
221
+ '\n\n');
222
+ }
223
+ }
224
+ return {
225
+ authenticated: !!apiKey,
226
+ source: apiKey ? source : null,
227
+ workspace: config.workspace ?? null,
228
+ projectId: config.projectId ?? null,
229
+ configFile: config.apiKey ? '~/.config/formo/config.json' : null,
230
+ };
231
+ },
232
+ });
233
+ // ── query ──
234
+ cli.command('query', {
235
+ description: 'Run a SQL query against your Formo analytics data',
236
+ args: incur_1.z.object({
237
+ sql: incur_1.z.string().describe('SQL query string to execute'),
238
+ }),
239
+ options: incur_1.z.object({}),
240
+ examples: [
241
+ {
242
+ args: { sql: 'SELECT count(*) FROM events' },
243
+ description: 'Count all events',
244
+ },
245
+ {
246
+ args: { sql: 'SELECT address, net_worth_usd FROM wallet_profiles ORDER BY net_worth_usd DESC LIMIT 10' },
247
+ description: 'Top 10 wallets by net worth',
248
+ },
249
+ ],
250
+ hint: 'Requires query:read scope on your API key.',
251
+ run({ args }) {
252
+ return (0, query_1.queryRunRun)(args.sql);
253
+ },
254
+ });
255
+ // ── command groups ──
256
+ cli.command(profiles_1.profiles);
257
+ cli.command(alerts_1.alerts);
258
+ cli.command(boards_1.boards);
259
+ cli.command(charts_1.charts);
260
+ cli.command(contracts_1.contracts);
261
+ cli.command(segments_1.segments);
262
+ cli.command(import_1.importCmd);
263
+ // Show banner when run with no args (root help)
264
+ const args = process.argv.slice(2);
265
+ const isRootHelp = args.length === 0 || (args.length === 1 && args[0] === '--help');
266
+ if (isRootHelp && process.stdout.isTTY) {
267
+ process.stderr.write((0, ui_1.banner)());
268
+ }
269
+ cli.serve();
270
+ exports.default = cli;
@@ -0,0 +1,3 @@
1
+ declare function createClient(): import("axios").AxiosInstance;
2
+ export declare function requireApiKey(): void;
3
+ export { createClient };
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.requireApiKey = requireApiKey;
7
+ exports.createClient = createClient;
8
+ const axios_1 = __importDefault(require("axios"));
9
+ const config_1 = require("./config");
10
+ const BASE_URL = 'https://api.formo.so';
11
+ function createClient() {
12
+ const apiKey = (0, config_1.getApiKey)();
13
+ const baseURL = BASE_URL;
14
+ const instance = axios_1.default.create({
15
+ baseURL,
16
+ timeout: 30000,
17
+ headers: {
18
+ 'Content-Type': 'application/json',
19
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
20
+ },
21
+ });
22
+ instance.interceptors.response.use((res) => res.data, (error) => {
23
+ const status = error.response?.status;
24
+ const data = error.response?.data;
25
+ const errorField = data?.error;
26
+ const message = data?.message ??
27
+ (typeof errorField === 'object' ? errorField?.message : errorField) ??
28
+ error.message;
29
+ throw Object.assign(new Error(message), { status, code: error.code });
30
+ });
31
+ return instance;
32
+ }
33
+ function requireApiKey() {
34
+ if (!(0, config_1.getApiKey)()) {
35
+ throw new Error('No API key configured. Run `formo login <apiKey>` or set FORMO_API_KEY env var.');
36
+ }
37
+ }
@@ -0,0 +1,9 @@
1
+ export interface FormoConfig {
2
+ apiKey?: string;
3
+ workspace?: string;
4
+ projectId?: string;
5
+ }
6
+ export declare function readConfig(): FormoConfig;
7
+ export declare function saveConfig(updates: Partial<FormoConfig>): void;
8
+ export declare function clearConfig(): void;
9
+ export declare function getApiKey(): string | undefined;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.readConfig = readConfig;
7
+ exports.saveConfig = saveConfig;
8
+ exports.clearConfig = clearConfig;
9
+ exports.getApiKey = getApiKey;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const os_1 = __importDefault(require("os"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.config', 'formo');
14
+ const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
15
+ function readConfig() {
16
+ try {
17
+ const raw = fs_1.default.readFileSync(CONFIG_FILE, 'utf-8');
18
+ return JSON.parse(raw);
19
+ }
20
+ catch {
21
+ return {};
22
+ }
23
+ }
24
+ function saveConfig(updates) {
25
+ const existing = readConfig();
26
+ const merged = { ...existing, ...updates };
27
+ fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
28
+ fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), {
29
+ mode: 0o600,
30
+ });
31
+ }
32
+ function clearConfig() {
33
+ try {
34
+ if (fs_1.default.existsSync(CONFIG_FILE)) {
35
+ fs_1.default.writeFileSync(CONFIG_FILE, JSON.stringify({}, null, 2), {
36
+ mode: 0o600,
37
+ });
38
+ }
39
+ }
40
+ catch {
41
+ // Ignore errors if file doesn't exist
42
+ }
43
+ }
44
+ function getApiKey() {
45
+ return process.env.FORMO_API_KEY ?? readConfig().apiKey;
46
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Terminal UI utilities for the Formo CLI.
3
+ *
4
+ * Uses raw ANSI escape codes (no dependencies) for coloring and styling.
5
+ * Colors are automatically disabled when stdout is not a TTY (piped output, CI, etc.)
6
+ * or when the NO_COLOR environment variable is set.
7
+ */
8
+ export declare const color: {
9
+ green: (t: string) => string;
10
+ brightGreen: (t: string) => string;
11
+ dim: (t: string) => string;
12
+ bold: (t: string) => string;
13
+ yellow: (t: string) => string;
14
+ red: (t: string) => string;
15
+ cyan: (t: string) => string;
16
+ gray: (t: string) => string;
17
+ white: (t: string) => string;
18
+ boldGreen: (t: string) => string;
19
+ boldBrightGreen: (t: string) => string;
20
+ };
21
+ /**
22
+ * Returns the Formo ASCII art banner in green.
23
+ * Only shown when stdout is a TTY.
24
+ */
25
+ export declare function banner(): string;
26
+ export declare function success(message: string): string;
27
+ export declare function error(message: string): string;
28
+ export declare function warn(message: string): string;
29
+ export declare function info(message: string): string;
30
+ export declare function keyValue(key: string, value: string): string;
31
+ export declare function heading(text: string): string;
package/dist/lib/ui.js ADDED
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /**
3
+ * Terminal UI utilities for the Formo CLI.
4
+ *
5
+ * Uses raw ANSI escape codes (no dependencies) for coloring and styling.
6
+ * Colors are automatically disabled when stdout is not a TTY (piped output, CI, etc.)
7
+ * or when the NO_COLOR environment variable is set.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.color = void 0;
11
+ exports.banner = banner;
12
+ exports.success = success;
13
+ exports.error = error;
14
+ exports.warn = warn;
15
+ exports.info = info;
16
+ exports.keyValue = keyValue;
17
+ exports.heading = heading;
18
+ const isTTY = process.stdout.isTTY === true;
19
+ const noColor = !!process.env.NO_COLOR;
20
+ /** Whether color output is enabled */
21
+ const colorEnabled = isTTY && !noColor;
22
+ // ── ANSI helpers ──
23
+ function ansi(code, text) {
24
+ return colorEnabled ? `\x1b[${code}m${text}\x1b[0m` : text;
25
+ }
26
+ exports.color = {
27
+ green: (t) => ansi('32', t),
28
+ brightGreen: (t) => ansi('92', t),
29
+ dim: (t) => ansi('2', t),
30
+ bold: (t) => ansi('1', t),
31
+ yellow: (t) => ansi('33', t),
32
+ red: (t) => ansi('31', t),
33
+ cyan: (t) => ansi('36', t),
34
+ gray: (t) => ansi('90', t),
35
+ white: (t) => ansi('97', t),
36
+ boldGreen: (t) => ansi('1;32', t),
37
+ boldBrightGreen: (t) => ansi('1;92', t),
38
+ };
39
+ // ── ASCII Art Logo ──
40
+ const LOGO_LINES = [
41
+ ' ███████╗ ██████╗ ██████╗ ███╗ ███╗ ██████╗ ',
42
+ ' ██╔════╝██╔═══██╗██╔══██╗ ████╗ ████║██╔═══██╗',
43
+ ' █████╗ ██║ ██║██████╔╝ ██╔████╔██║██║ ██║',
44
+ ' ██╔══╝ ██║ ██║██╔══██╗ ██║╚██╔╝██║██║ ██║',
45
+ ' ██║ ╚██████╔╝██║ ██║ ██║ ╚═╝ ██║╚██████╔╝',
46
+ ' ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ',
47
+ ];
48
+ /**
49
+ * Returns the Formo ASCII art banner in green.
50
+ * Only shown when stdout is a TTY.
51
+ */
52
+ function banner() {
53
+ if (!isTTY)
54
+ return '';
55
+ const logo = LOGO_LINES.map((line) => exports.color.boldGreen(line)).join('\n');
56
+ const tagline = exports.color.dim(' Web3 Analytics Platform — CLI');
57
+ const separator = exports.color.dim('─'.repeat(50));
58
+ return `\n${logo}\n${tagline}\n${separator}\n`;
59
+ }
60
+ // ── Status messages ──
61
+ function success(message) {
62
+ return `${exports.color.boldGreen('✔')} ${message}`;
63
+ }
64
+ function error(message) {
65
+ return `${exports.color.red('✖')} ${message}`;
66
+ }
67
+ function warn(message) {
68
+ return `${exports.color.yellow('⚠')} ${message}`;
69
+ }
70
+ function info(message) {
71
+ return `${exports.color.cyan('ℹ')} ${message}`;
72
+ }
73
+ // ── Formatting helpers ──
74
+ function keyValue(key, value) {
75
+ return `${exports.color.dim(key + ':')} ${value}`;
76
+ }
77
+ function heading(text) {
78
+ return exports.color.boldGreen(text);
79
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@formo/cli",
3
+ "version": "0.1.0",
4
+ "description": "Formo API CLI — query profiles and analytics data",
5
+ "bin": {
6
+ "formo": "dist/index.js"
7
+ },
8
+ "main": "dist/index.js",
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "ts-node src/index.ts",
16
+ "start": "node dist/index.js",
17
+ "lint": "eslint src/",
18
+ "lint:fix": "eslint src/ --fix",
19
+ "test": "mocha",
20
+ "test:watch": "mocha --watch"
21
+ },
22
+ "dependencies": {
23
+ "axios": "^1.7.0",
24
+ "incur": "^0.3.4"
25
+ },
26
+ "overrides": {
27
+ "serialize-javascript": ">=7.0.5"
28
+ },
29
+ "devDependencies": {
30
+ "@eslint/js": "^10.0.1",
31
+ "@types/chai": "^5.2.3",
32
+ "@types/mocha": "^10.0.10",
33
+ "@types/node": "^20.0.0",
34
+ "chai": "^6.2.2",
35
+ "dotenv": "^17.4.1",
36
+ "eslint": "^10.2.0",
37
+ "globals": "^17.4.0",
38
+ "mocha": "^11.7.5",
39
+ "ts-node": "^10.9.2",
40
+ "tsx": "^4.21.0",
41
+ "typescript": "^5.5.4",
42
+ "typescript-eslint": "^8.58.1"
43
+ },
44
+ "pnpm": {
45
+ "overrides": {
46
+ "serialize-javascript": ">=7.0.5"
47
+ }
48
+ }
49
+ }