@formo/cli 0.1.0 → 0.2.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
@@ -12,213 +12,214 @@ const query_1 = require("./commands/query");
12
12
  const segments_1 = require("./commands/segments");
13
13
  const config_1 = require("./lib/config");
14
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';
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
18
  function loginGuide() {
19
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
- '',
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
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
- '',
31
+ "",
32
+ ` ${ui_1.color.green("export FORMO_API_KEY=formo_your_api_key_here")}`,
33
+ "",
34
34
  ui_1.color.dim(` Docs: ${DOCS_URL}`),
35
- '',
36
- ].join('\n');
35
+ "",
36
+ ].join("\n");
37
37
  }
38
38
  async function validateAndFetchWorkspace(apiKey) {
39
39
  try {
40
40
  const res = await fetch(`${API_BASE_URL}/api/validate-api-key`, {
41
- method: 'POST',
42
- headers: { 'Content-Type': 'application/json' },
41
+ method: "POST",
42
+ headers: { "Content-Type": "application/json" },
43
43
  body: JSON.stringify({ apiKey }),
44
44
  });
45
45
  if (!res.ok)
46
46
  return null;
47
47
  const body = (await res.json());
48
- if (!body.isSuccess || !body.data)
48
+ if (!body.validated)
49
49
  return null;
50
50
  return {
51
- workspace: body.data.details,
52
- projectId: body.data.scopes?.project_id ?? '',
51
+ workspace: body.details,
52
+ projectId: body.scopes?.project_id ?? "",
53
53
  };
54
54
  }
55
55
  catch {
56
56
  return null;
57
57
  }
58
58
  }
59
- const cli = incur_1.Cli.create('formo', {
60
- version: '0.1.0',
61
- description: 'Formo API CLI — Web3 analytics from the terminal',
59
+ const cli = incur_1.Cli.create("formo", {
60
+ version: "0.2.0",
61
+ description: "Formo API CLI — Web3 analytics from the terminal",
62
62
  sync: {
63
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',
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 charts in a board",
71
+ "list all tracked contracts",
72
+ "register a new smart contract",
73
+ "list user segments",
74
+ "import wallet addresses",
76
75
  ],
77
76
  },
78
77
  });
79
78
  // ── login ──
80
- cli.command('login', {
81
- description: 'Authenticate with your Formo API key',
79
+ cli.command("login", {
80
+ description: "Authenticate with your Formo API key",
82
81
  args: incur_1.z.object({
83
- apiKey: incur_1.z.string().optional().describe('Your formo_ API key'),
82
+ apiKey: incur_1.z.string().optional().describe("Your formo_ API key"),
84
83
  }),
85
84
  options: incur_1.z.object({}),
86
- examples: [
87
- { args: { apiKey: 'formo_abc123' }, description: 'Save API key' },
88
- ],
85
+ examples: [{ args: { apiKey: "formo_abc123" }, description: "Save API key" }],
89
86
  async run({ args }) {
90
87
  // No key provided → show guide
91
88
  if (!args.apiKey) {
92
89
  if (process.stdout.isTTY) {
93
90
  process.stderr.write(loginGuide());
94
91
  }
95
- return { ok: false, message: 'No API key provided. See guide above.' };
92
+ return { ok: false, message: "No API key provided. See guide above." };
96
93
  }
97
94
  const isTTY = process.stdout.isTTY;
98
95
  // Validate the key against the API
99
96
  if (isTTY) {
100
- process.stderr.write('\n' + ui_1.color.dim('Validating API key…') + '\n');
97
+ process.stderr.write("\n" + ui_1.color.dim("Validating API key…") + "\n");
101
98
  }
102
99
  const workspaceInfo = await validateAndFetchWorkspace(args.apiKey);
103
100
  // Save key + workspace info (save even if validation fails — user might be offline)
104
101
  // Only include workspace/projectId when present so offline logins don't wipe existing values
105
102
  (0, config_1.saveConfig)({
106
103
  apiKey: args.apiKey,
107
- ...(workspaceInfo?.workspace !== undefined && { workspace: workspaceInfo.workspace }),
108
- ...(workspaceInfo?.projectId !== undefined && { projectId: workspaceInfo.projectId }),
104
+ ...(workspaceInfo?.workspace !== undefined && {
105
+ workspace: workspaceInfo.workspace,
106
+ }),
107
+ ...(workspaceInfo?.projectId !== undefined && {
108
+ projectId: workspaceInfo.projectId,
109
+ }),
109
110
  });
110
111
  // Show feedback in human mode
111
112
  if (isTTY) {
112
113
  const masked = args.apiKey.length > 12
113
- ? args.apiKey.slice(0, 8) + '…' + args.apiKey.slice(-4)
114
- : '***';
114
+ ? args.apiKey.slice(0, 8) + "…" + args.apiKey.slice(-4)
115
+ : "***";
115
116
  if (workspaceInfo) {
116
- process.stderr.write((0, ui_1.success)('API key validated and saved!') +
117
- '\n' +
117
+ process.stderr.write((0, ui_1.success)("API key validated and saved!") +
118
+ "\n" +
118
119
  (0, ui_1.info)(`Key: ${ui_1.color.dim(masked)}`) +
119
- '\n' +
120
+ "\n" +
120
121
  (0, ui_1.info)(`Workspace: ${ui_1.color.bold(workspaceInfo.workspace)}`) +
121
- '\n' +
122
+ "\n" +
122
123
  (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');
124
+ ? (0, ui_1.info)(`Project: ${ui_1.color.dim(workspaceInfo.projectId)}`) + "\n"
125
+ : "") +
126
+ (0, ui_1.info)(`Config: ${ui_1.color.dim("~/.config/formo/config.json")}`) +
127
+ "\n\n" +
128
+ ui_1.color.dim("You can now use all formo commands.") +
129
+ "\n" +
130
+ ui_1.color.dim("Run ") +
131
+ ui_1.color.green("formo --help") +
132
+ ui_1.color.dim(" to see available commands.") +
133
+ "\n\n");
133
134
  }
134
135
  else {
135
- process.stderr.write((0, ui_1.warn)('API key saved but could not be validated.') +
136
- '\n' +
136
+ process.stderr.write((0, ui_1.warn)("API key saved but could not be validated.") +
137
+ "\n" +
137
138
  (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');
139
+ "\n" +
140
+ (0, ui_1.info)(`Config: ${ui_1.color.dim("~/.config/formo/config.json")}`) +
141
+ "\n" +
142
+ ui_1.color.dim("The API might be unreachable. Your key is saved and will be used for requests.") +
143
+ "\n\n");
143
144
  }
144
145
  }
145
146
  return {
146
147
  ok: true,
147
148
  message: workspaceInfo
148
- ? 'API key validated and saved'
149
- : 'API key saved (not validated)',
149
+ ? "API key validated and saved"
150
+ : "API key saved (not validated)",
150
151
  workspace: workspaceInfo?.workspace ?? null,
151
152
  projectId: workspaceInfo?.projectId ?? null,
152
153
  };
153
154
  },
154
155
  });
155
156
  // ── logout ──
156
- cli.command('logout', {
157
- description: 'Remove saved API key and clear authentication',
157
+ cli.command("logout", {
158
+ description: "Remove saved API key and clear authentication",
158
159
  run() {
159
160
  const hadKey = !!(0, config_1.readConfig)().apiKey;
160
161
  (0, config_1.clearConfig)();
161
162
  if (process.stdout.isTTY) {
162
- process.stderr.write('\n');
163
+ process.stderr.write("\n");
163
164
  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');
165
+ process.stderr.write((0, ui_1.success)("Logged out successfully.") +
166
+ "\n" +
167
+ (0, ui_1.info)("API key removed from ~/.config/formo/config.json") +
168
+ "\n");
168
169
  }
169
170
  else {
170
- process.stderr.write((0, ui_1.info)('No saved API key found — already logged out.') + '\n');
171
+ process.stderr.write((0, ui_1.info)("No saved API key found — already logged out.") + "\n");
171
172
  }
172
173
  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');
174
+ process.stderr.write("\n" +
175
+ (0, ui_1.warn)(`${ui_1.color.yellow("FORMO_API_KEY")} env var is still set.`) +
176
+ "\n" +
177
+ (0, ui_1.info)(`Run ${ui_1.color.green("unset FORMO_API_KEY")} to fully log out.`) +
178
+ "\n");
178
179
  }
179
- process.stderr.write('\n');
180
+ process.stderr.write("\n");
180
181
  }
181
182
  return {
182
183
  ok: true,
183
- message: hadKey ? 'API key removed' : 'Already logged out',
184
+ message: hadKey ? "API key removed" : "Already logged out",
184
185
  envVarSet: !!process.env.FORMO_API_KEY,
185
186
  };
186
187
  },
187
188
  });
188
189
  // ── status ──
189
- cli.command('status', {
190
- description: 'Show current authentication and CLI status',
190
+ cli.command("status", {
191
+ description: "Show current authentication and CLI status",
191
192
  run() {
192
193
  const apiKey = (0, config_1.getApiKey)();
193
194
  const config = (0, config_1.readConfig)();
194
195
  const source = process.env.FORMO_API_KEY
195
- ? 'FORMO_API_KEY env var'
196
- : 'config file';
196
+ ? "FORMO_API_KEY env var"
197
+ : "config file";
197
198
  if (process.stdout.isTTY) {
198
- process.stderr.write('\n');
199
+ process.stderr.write("\n");
199
200
  if (apiKey) {
200
201
  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' +
202
+ ? apiKey.slice(0, 8) + "…" + apiKey.slice(-4)
203
+ : "***";
204
+ process.stderr.write((0, ui_1.success)("Authenticated") +
205
+ "\n" +
205
206
  (0, ui_1.info)(`Key: ${ui_1.color.dim(masked)}`) +
206
- '\n' +
207
+ "\n" +
207
208
  (0, ui_1.info)(`Source: ${ui_1.color.dim(source)}`) +
208
- '\n' +
209
+ "\n" +
209
210
  (config.workspace
210
- ? (0, ui_1.info)(`Workspace: ${ui_1.color.bold(config.workspace)}`) + '\n'
211
- : '') +
211
+ ? (0, ui_1.info)(`Workspace: ${ui_1.color.bold(config.workspace)}`) + "\n"
212
+ : "") +
212
213
  (config.projectId
213
- ? (0, ui_1.info)(`Project ID: ${ui_1.color.dim(config.projectId)}`) + '\n'
214
- : '') +
215
- '\n');
214
+ ? (0, ui_1.info)(`Project ID: ${ui_1.color.dim(config.projectId)}`) + "\n"
215
+ : "") +
216
+ "\n");
216
217
  }
217
218
  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');
219
+ process.stderr.write((0, ui_1.error)("Not authenticated") +
220
+ "\n\n" +
221
+ (0, ui_1.info)(`Run ${ui_1.color.green("formo login")} to get started.`) +
222
+ "\n\n");
222
223
  }
223
224
  }
224
225
  return {
@@ -226,34 +227,13 @@ cli.command('status', {
226
227
  source: apiKey ? source : null,
227
228
  workspace: config.workspace ?? null,
228
229
  projectId: config.projectId ?? null,
229
- configFile: config.apiKey ? '~/.config/formo/config.json' : null,
230
+ configFile: config.apiKey ? "~/.config/formo/config.json" : null,
230
231
  };
231
232
  },
232
233
  });
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
234
  // ── command groups ──
256
235
  cli.command(profiles_1.profiles);
236
+ cli.command(query_1.query);
257
237
  cli.command(alerts_1.alerts);
258
238
  cli.command(boards_1.boards);
259
239
  cli.command(charts_1.charts);
@@ -262,7 +242,7 @@ cli.command(segments_1.segments);
262
242
  cli.command(import_1.importCmd);
263
243
  // Show banner when run with no args (root help)
264
244
  const args = process.argv.slice(2);
265
- const isRootHelp = args.length === 0 || (args.length === 1 && args[0] === '--help');
245
+ const isRootHelp = args.length === 0 || (args.length === 1 && args[0] === "--help");
266
246
  if (isRootHelp && process.stdout.isTTY) {
267
247
  process.stderr.write((0, ui_1.banner)());
268
248
  }
@@ -1,3 +1,29 @@
1
+ import { AxiosError } from 'axios';
2
+ export interface ApiErrorBody {
3
+ error?: {
4
+ code?: string;
5
+ message?: string;
6
+ doc_url?: string;
7
+ param?: string;
8
+ details?: Record<string, unknown>;
9
+ };
10
+ }
11
+ export interface DecoratedApiError extends Error {
12
+ status?: number;
13
+ code?: string;
14
+ docUrl?: string;
15
+ param?: string;
16
+ details?: Record<string, unknown>;
17
+ transportCode?: string;
18
+ }
19
+ /**
20
+ * Translate an AxiosError into a thrown Error with the API's structured
21
+ * `{ error: { code, message, doc_url, param, details } }` envelope decoded
22
+ * onto the Error instance and into a multi-line, human-readable `.message`.
23
+ *
24
+ * Exported for unit testing — used by the response interceptor below.
25
+ */
26
+ export declare function parseApiError(error: AxiosError): DecoratedApiError;
1
27
  declare function createClient(): import("axios").AxiosInstance;
2
28
  export declare function requireApiKey(): void;
3
29
  export { createClient };
@@ -3,11 +3,40 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseApiError = parseApiError;
6
7
  exports.requireApiKey = requireApiKey;
7
8
  exports.createClient = createClient;
8
9
  const axios_1 = __importDefault(require("axios"));
9
10
  const config_1 = require("./config");
10
11
  const BASE_URL = 'https://api.formo.so';
12
+ /**
13
+ * Translate an AxiosError into a thrown Error with the API's structured
14
+ * `{ error: { code, message, doc_url, param, details } }` envelope decoded
15
+ * onto the Error instance and into a multi-line, human-readable `.message`.
16
+ *
17
+ * Exported for unit testing — used by the response interceptor below.
18
+ */
19
+ function parseApiError(error) {
20
+ const status = error.response?.status;
21
+ const body = error.response?.data;
22
+ const apiError = body?.error;
23
+ const baseMessage = apiError?.message ?? error.message;
24
+ const parts = [];
25
+ parts.push(apiError?.code ? `[${apiError.code}] ${baseMessage}` : baseMessage);
26
+ if (apiError?.param)
27
+ parts.push(`Param: ${apiError.param}`);
28
+ if (apiError?.doc_url)
29
+ parts.push(`Docs: ${apiError.doc_url}`);
30
+ const message = parts.join('\n ');
31
+ return Object.assign(new Error(message), {
32
+ status,
33
+ code: apiError?.code,
34
+ docUrl: apiError?.doc_url,
35
+ param: apiError?.param,
36
+ details: apiError?.details,
37
+ transportCode: error.code,
38
+ });
39
+ }
11
40
  function createClient() {
12
41
  const apiKey = (0, config_1.getApiKey)();
13
42
  const baseURL = BASE_URL;
@@ -20,13 +49,7 @@ function createClient() {
20
49
  },
21
50
  });
22
51
  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 });
52
+ throw parseApiError(error);
30
53
  });
31
54
  return instance;
32
55
  }
package/package.json CHANGED
@@ -1,7 +1,16 @@
1
1
  {
2
2
  "name": "@formo/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
+ "packageManager": "pnpm@10.28.2",
4
5
  "description": "Formo API CLI — query profiles and analytics data",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/getformo/cli.git"
9
+ },
10
+ "homepage": "https://github.com/getformo/cli#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/getformo/cli/issues"
13
+ },
5
14
  "bin": {
6
15
  "formo": "dist/index.js"
7
16
  },