latinfo 0.3.0 → 0.4.1

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
@@ -9,10 +9,19 @@ const http_1 = __importDefault(require("http"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const os_1 = __importDefault(require("os"));
11
11
  const child_process_1 = require("child_process");
12
+ const VERSION = '0.4.1';
12
13
  const API_URL = process.env.LATINFO_API_URL || 'https://api.latinfo.dev';
13
- const GITHUB_CLIENT_ID = 'Ov23li5fcQaiCsVtaMKK';
14
+ const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || 'Ov23li5fcQaiCsVtaMKK';
14
15
  const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.latinfo');
15
16
  const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
17
+ // --- JSON mode ---
18
+ const jsonFlag = process.argv.includes('--json');
19
+ const liveFlag = process.argv.includes('--live');
20
+ const rawArgs = process.argv.slice(2).filter(a => a !== '--json' && a !== '--live');
21
+ function jsonError(error, message) {
22
+ process.stderr.write(JSON.stringify({ error, message }) + '\n');
23
+ process.exit(1);
24
+ }
16
25
  function loadConfig() {
17
26
  try {
18
27
  return JSON.parse(fs_1.default.readFileSync(CONFIG_FILE, 'utf-8'));
@@ -58,6 +67,30 @@ async function waitForCallback(port) {
58
67
  setTimeout(() => { server.close(); reject(new Error('Timeout waiting for authorization')); }, 120_000);
59
68
  });
60
69
  }
70
+ function requireAuth() {
71
+ const config = loadConfig();
72
+ if (!config) {
73
+ if (jsonFlag)
74
+ jsonError('auth_required', 'Not logged in. Run: latinfo login');
75
+ console.error('Not logged in. Run: latinfo login');
76
+ process.exit(1);
77
+ }
78
+ return config;
79
+ }
80
+ async function apiRequest(config, path) {
81
+ const res = await fetch(`${API_URL}${path}`, {
82
+ headers: { Authorization: `Bearer ${config.api_key}` },
83
+ });
84
+ if (!res.ok) {
85
+ const err = await res.json();
86
+ if (jsonFlag)
87
+ jsonError(err.error, err.message || err.error);
88
+ console.error(err.message || err.error);
89
+ process.exit(1);
90
+ }
91
+ return res;
92
+ }
93
+ // --- Commands ---
61
94
  async function login() {
62
95
  const port = 8400;
63
96
  const redirectUri = `http://localhost:${port}/callback`;
@@ -65,9 +98,7 @@ async function login() {
65
98
  const authUrl = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${scope}`;
66
99
  console.log('Opening GitHub...');
67
100
  openBrowser(authUrl);
68
- // 1. Wait for GitHub to redirect back with code
69
101
  const code = await waitForCallback(port);
70
- // 2. Exchange code for access token (server-side, needs client_secret)
71
102
  const authRes = await fetch(`${API_URL}/auth/github`, {
72
103
  method: 'POST',
73
104
  headers: { 'Content-Type': 'application/json' },
@@ -81,136 +112,278 @@ async function login() {
81
112
  saveConfig({ api_key: authData.api_key, github_username: authData.github_username });
82
113
  console.log(`Logged in as ${authData.github_username}`);
83
114
  }
84
- // --- Commands ---
85
115
  async function ruc(rucNumber) {
86
- const config = loadConfig();
87
- if (!config) {
88
- console.error('Not logged in. Run: latinfo login');
89
- process.exit(1);
90
- }
91
- if (!/^\d{11}$/.test(rucNumber)) {
116
+ const config = requireAuth();
117
+ if (!rucNumber || !/^\d{11}$/.test(rucNumber)) {
118
+ if (jsonFlag)
119
+ jsonError('invalid_input', 'Invalid RUC. Must be 11 digits.');
92
120
  console.error('Invalid RUC. Must be 11 digits.');
93
121
  process.exit(1);
94
122
  }
95
- const res = await fetch(`${API_URL}/pe/ruc/${rucNumber}`, {
96
- headers: { Authorization: `Bearer ${config.api_key}` },
97
- });
98
- if (!res.ok) {
99
- const err = await res.json();
100
- console.error(err.message || err.error);
101
- process.exit(1);
102
- }
123
+ const res = await apiRequest(config, `/pe/ruc/${rucNumber}`);
103
124
  const data = await res.json();
125
+ if (jsonFlag) {
126
+ console.log(JSON.stringify(data));
127
+ return;
128
+ }
104
129
  console.log(`
105
130
  RUC: ${data.ruc}
106
- Razón Social: ${data.razonSocial}
131
+ Razón Social: ${data.razon_social}
107
132
  Estado: ${data.estado}
108
133
  Condición: ${data.condicion}
109
134
  Ubigeo: ${data.ubigeo}
110
- Dirección: ${[data.tipoVia, data.nombreVia, data.numero].filter(v => v && v !== '-').join(' ')}
111
- Zona: ${[data.codigoZona, data.tipoZona].filter(v => v && v !== '-').join(' ')}
135
+ Dirección: ${[data.tipo_via, data.nombre_via, data.numero].filter(v => v && v !== '-').join(' ')}
136
+ Zona: ${[data.codigo_zona, data.tipo_zona].filter(v => v && v !== '-').join(' ')}
112
137
  `.trim());
113
138
  }
114
139
  async function dni(dniNumber) {
115
- const config = loadConfig();
116
- if (!config) {
117
- console.error('Not logged in. Run: latinfo login');
118
- process.exit(1);
119
- }
120
- if (!/^\d{8}$/.test(dniNumber)) {
140
+ const config = requireAuth();
141
+ if (!dniNumber || !/^\d{8}$/.test(dniNumber)) {
142
+ if (jsonFlag)
143
+ jsonError('invalid_input', 'Invalid DNI. Must be 8 digits.');
121
144
  console.error('Invalid DNI. Must be 8 digits.');
122
145
  process.exit(1);
123
146
  }
124
- const res = await fetch(`${API_URL}/pe/dni/${dniNumber}`, {
125
- headers: { Authorization: `Bearer ${config.api_key}` },
126
- });
127
- if (!res.ok) {
128
- const err = await res.json();
129
- console.error(err.message || err.error);
130
- process.exit(1);
131
- }
147
+ const res = await apiRequest(config, `/pe/dni/${dniNumber}`);
132
148
  const data = await res.json();
149
+ if (jsonFlag) {
150
+ console.log(JSON.stringify(data));
151
+ return;
152
+ }
133
153
  console.log(`
134
- DNI: ${data.dni}
135
154
  RUC: ${data.ruc}
136
- Razón Social: ${data.razonSocial}
155
+ Razón Social: ${data.razon_social}
137
156
  Estado: ${data.estado}
138
157
  Condición: ${data.condicion}
139
158
  Ubigeo: ${data.ubigeo}
140
- Dirección: ${[data.tipoVia, data.nombreVia, data.numero].filter(v => v && v !== '-').join(' ')}
141
- Zona: ${[data.codigoZona, data.tipoZona].filter(v => v && v !== '-').join(' ')}
159
+ Dirección: ${[data.tipo_via, data.nombre_via, data.numero].filter(v => v && v !== '-').join(' ')}
160
+ Zona: ${[data.codigo_zona, data.tipo_zona].filter(v => v && v !== '-').join(' ')}
142
161
  `.trim());
143
162
  }
144
163
  async function search(query) {
145
- const config = loadConfig();
146
- if (!config) {
147
- console.error('Not logged in. Run: latinfo login');
164
+ const config = requireAuth();
165
+ if (!query) {
166
+ if (jsonFlag)
167
+ jsonError('invalid_input', 'Search query is required.');
168
+ console.error('Search query is required.');
148
169
  process.exit(1);
149
170
  }
150
- const res = await fetch(`${API_URL}/pe/search?q=${encodeURIComponent(query)}`, {
151
- headers: { Authorization: `Bearer ${config.api_key}` },
171
+ const res = await apiRequest(config, `/pe/search?q=${encodeURIComponent(query)}`);
172
+ const results = await res.json();
173
+ if (jsonFlag) {
174
+ console.log(JSON.stringify(results));
175
+ return;
176
+ }
177
+ if (results.length === 0) {
178
+ console.log('No results found.');
179
+ return;
180
+ }
181
+ for (const r of results) {
182
+ console.log(` ${r.ruc} ${r.razon_social} [${r.estado}]`);
183
+ }
184
+ console.log(`\n${results.length} result(s)`);
185
+ }
186
+ function whoami() {
187
+ const config = requireAuth();
188
+ if (jsonFlag) {
189
+ console.log(JSON.stringify({ username: config.github_username, api_key: config.api_key }));
190
+ return;
191
+ }
192
+ console.log(config.github_username);
193
+ }
194
+ function plan() {
195
+ const config = requireAuth();
196
+ if (jsonFlag) {
197
+ console.log(JSON.stringify({ plan: 'free', limit: '1M requests/month' }));
198
+ return;
199
+ }
200
+ console.log(`
201
+ User: ${config.github_username}
202
+ Plan: free
203
+ Limit: 1M requests/month
204
+ `.trim());
205
+ }
206
+ function calcCf(requests) {
207
+ if (requests <= 3_000_000)
208
+ return { cost: 0, tier: 'free' };
209
+ if (requests <= 10_000_000)
210
+ return { cost: 5, tier: 'paid' };
211
+ return { cost: 5 + Math.ceil((requests - 10_000_000) / 1_000_000) * 0.50, tier: 'paid' };
212
+ }
213
+ function printCosts(data) {
214
+ if (jsonFlag) {
215
+ console.log(JSON.stringify(data));
216
+ return;
217
+ }
218
+ const fmt = (n) => n.toLocaleString();
219
+ const fmtMoney = (n) => `$${n.toFixed(2)}`;
220
+ console.log(`
221
+ ${data.month ? `Month: ${data.month}\n ` : ''}Users: ${fmt(data.users)}
222
+ Pro users: ${fmt(data.pro_users)}
223
+ Requests: ${fmt(data.requests)}/month
224
+
225
+ CF tier: ${data.cf_tier}
226
+ CF cost: ${fmtMoney(data.cf_cost)}/month
227
+ Revenue: ${fmtMoney(data.revenue)}/month
228
+ Margin: ${data.margin >= 0 ? '+' : ''}${fmtMoney(data.margin)}/month
229
+
230
+ Status: ${data.safe ? 'SAFE' : 'DEFICIT'}
231
+ `.trim());
232
+ }
233
+ async function costsLive() {
234
+ const adminSecret = process.env.LATINFO_ADMIN_SECRET;
235
+ if (!adminSecret) {
236
+ if (jsonFlag)
237
+ jsonError('missing_secret', 'Set LATINFO_ADMIN_SECRET env var.');
238
+ console.error('Set LATINFO_ADMIN_SECRET env var.');
239
+ process.exit(1);
240
+ }
241
+ const res = await fetch(`${API_URL}/admin/stats`, {
242
+ headers: { Authorization: `Bearer ${adminSecret}` },
152
243
  });
153
244
  if (!res.ok) {
154
245
  const err = await res.json();
246
+ if (jsonFlag)
247
+ jsonError(err.error, err.message || err.error);
155
248
  console.error(err.message || err.error);
156
249
  process.exit(1);
157
250
  }
158
251
  const data = await res.json();
159
- if (data.count === 0) {
160
- console.log('No results found.');
161
- return;
162
- }
163
- for (const r of data.results) {
164
- console.log(` ${r.ruc} ${r.razonSocial} [${r.estado}]`);
165
- }
166
- console.log(`\n${data.count} result(s)`);
252
+ printCosts(data);
167
253
  }
168
- function whoami() {
169
- const config = loadConfig();
170
- if (!config) {
171
- console.error('Not logged in. Run: latinfo login');
254
+ function costsSimulate(usersStr, rpmStr, proPctStr) {
255
+ const users = parseInt(usersStr);
256
+ const avgRpm = parseInt(rpmStr || '1000');
257
+ const proPct = parseFloat(proPctStr || '1');
258
+ if (!users || users < 0) {
259
+ if (jsonFlag)
260
+ jsonError('invalid_input', 'Usage: latinfo costs <users> [avg_req/user/month] [pro_%]');
261
+ console.error('Usage: latinfo costs <users> [avg_req/user/month] [pro_%]');
172
262
  process.exit(1);
173
263
  }
174
- console.log(config.github_username);
264
+ const requests = users * avgRpm;
265
+ const proUsers = Math.floor(users * proPct / 100);
266
+ const revenue = proUsers * 1;
267
+ const { cost: cfCost, tier: cfTier } = calcCf(requests);
268
+ const margin = revenue - cfCost;
269
+ printCosts({ users, pro_users: proUsers, requests, cf_tier: cfTier, cf_cost: cfCost, revenue, margin, safe: margin >= 0 });
175
270
  }
176
271
  function logout() {
177
272
  deleteConfig();
178
273
  console.log('Logged out.');
179
274
  }
180
275
  function help() {
181
- console.log(`
182
- latinfo - Public data API for Latin America
183
-
184
- Commands:
185
- login Authenticate with GitHub
186
- logout Sign out
187
- whoami Show current user
188
- ruc <ruc> Lookup by RUC (11 digits)
189
- dni <dni> Lookup by DNI (8 digits)
190
- search <query> Search by business name
191
- help Show this help
192
- `.trim());
276
+ console.log(`latinfo v${VERSION} — Public data API for Latin America
277
+
278
+ API base: ${API_URL}
279
+
280
+ USAGE
281
+ latinfo <command> [args] [--json]
282
+
283
+ COMMANDS
284
+ login
285
+ Authenticate via GitHub OAuth. Opens a browser, waits for callback
286
+ on localhost:8400, and stores the API key in ~/.latinfo/config.json.
287
+
288
+ logout
289
+ Remove stored credentials from ~/.latinfo/config.json.
290
+
291
+ whoami
292
+ Show the authenticated GitHub username.
293
+ Output fields (--json): username, api_key
294
+
295
+ plan
296
+ Show current plan and limits.
297
+ Output fields (--json): plan, limit
298
+
299
+ ruc <ruc>
300
+ Lookup a Peruvian taxpayer by RUC (11 digits).
301
+ Output fields (--json): ruc, razon_social, estado, condicion,
302
+ ubigeo, tipo_via, nombre_via, numero, codigo_zona, tipo_zona,
303
+ departamento, provincia, distrito, direccion_completa, tipo_contribuyente
304
+
305
+ dni <dni>
306
+ Lookup a Peruvian taxpayer by DNI (8 digits). Finds the RUC
307
+ associated with the person and returns the same fields as 'ruc'.
308
+ Output fields (--json): same as ruc
309
+
310
+ search <query>
311
+ Search taxpayers by business name (razón social). Returns an array.
312
+ Output fields (--json): array of objects with same fields as ruc
313
+
314
+ costs <users> [avg_req] [pro_%]
315
+ Simulate Cloudflare cost vs revenue. Defaults: 1000 req/user, 1% Pro.
316
+ Output fields (--json): users, pro_users, requests, cf_tier,
317
+ cf_cost, revenue, margin, safe
318
+
319
+ costs --live
320
+ Real-time cost report from production data (admin only).
321
+ Requires LATINFO_ADMIN_SECRET env var.
322
+ Output fields (--json): month, users, pro_users, requests,
323
+ cf_tier, cf_cost, revenue, margin, safe
324
+
325
+ help
326
+ Show this help text.
327
+
328
+ FLAGS
329
+ --json Output raw JSON instead of human-formatted text.
330
+ Errors are written to stderr as {"error":"...","message":"..."}.
331
+ --live Use real-time production data (for 'costs' command).
332
+ --version, -v
333
+ Print version and exit.
334
+
335
+ COUNTRIES
336
+ pe Peru (SUNAT padrón). Active and available.
337
+
338
+ AUTH
339
+ Run 'latinfo login' to authenticate with GitHub. This opens your
340
+ browser for OAuth, then stores your API key locally. All data
341
+ commands (ruc, dni, search) require authentication.
342
+
343
+ Config file: ~/.latinfo/config.json
344
+ Environment: LATINFO_API_URL to override API base URL.
345
+
346
+ EXIT CODES
347
+ 0 Success
348
+ 1 Error (invalid input, auth failure, API error)`);
349
+ }
350
+ function version() {
351
+ console.log(`latinfo/${VERSION}`);
193
352
  }
194
353
  // --- Main ---
195
- const [command, ...args] = process.argv.slice(2);
196
- switch (command) {
197
- case 'login':
198
- login().catch(e => { console.error(e); process.exit(1); });
199
- break;
200
- case 'logout':
201
- logout();
202
- break;
203
- case 'whoami':
204
- whoami();
205
- break;
206
- case 'ruc':
207
- ruc(args[0]).catch(e => { console.error(e); process.exit(1); });
208
- break;
209
- case 'dni':
210
- dni(args[0]).catch(e => { console.error(e); process.exit(1); });
211
- break;
212
- case 'search':
213
- search(args.join(' ')).catch(e => { console.error(e); process.exit(1); });
214
- break;
215
- default: help();
354
+ const [command, ...args] = rawArgs;
355
+ if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
356
+ version();
357
+ }
358
+ else {
359
+ switch (command) {
360
+ case 'login':
361
+ login().catch(e => { console.error(e); process.exit(1); });
362
+ break;
363
+ case 'logout':
364
+ logout();
365
+ break;
366
+ case 'whoami':
367
+ whoami();
368
+ break;
369
+ case 'plan':
370
+ plan();
371
+ break;
372
+ case 'costs':
373
+ (liveFlag ? costsLive() : Promise.resolve(costsSimulate(args[0], args[1], args[2]))).catch(e => { console.error(e); process.exit(1); });
374
+ break;
375
+ case 'ruc':
376
+ ruc(args[0]).catch(e => { console.error(e); process.exit(1); });
377
+ break;
378
+ case 'dni':
379
+ dni(args[0]).catch(e => { console.error(e); process.exit(1); });
380
+ break;
381
+ case 'search':
382
+ search(args.join(' ')).catch(e => { console.error(e); process.exit(1); });
383
+ break;
384
+ case 'help':
385
+ help();
386
+ break;
387
+ default: help();
388
+ }
216
389
  }
package/dist/sdk.d.ts CHANGED
@@ -1,13 +1,13 @@
1
- interface RucResult {
1
+ export interface PeRecord {
2
2
  ruc: string;
3
- razonSocial: string;
3
+ razon_social: string;
4
4
  estado: string;
5
5
  condicion: string;
6
6
  ubigeo: string;
7
- tipoVia: string;
8
- nombreVia: string;
9
- codigoZona: string;
10
- tipoZona: string;
7
+ tipo_via: string;
8
+ nombre_via: string;
9
+ codigo_zona: string;
10
+ tipo_zona: string;
11
11
  numero: string;
12
12
  interior: string;
13
13
  lote: string;
@@ -15,19 +15,6 @@ interface RucResult {
15
15
  manzana: string;
16
16
  kilometro: string;
17
17
  }
18
- interface DniResult extends RucResult {
19
- dni: string;
20
- }
21
- interface SearchResult {
22
- ruc: string;
23
- razonSocial: string;
24
- estado: string;
25
- condicion: string;
26
- }
27
- interface SearchResponse {
28
- count: number;
29
- results: SearchResult[];
30
- }
31
18
  declare class Country {
32
19
  private request;
33
20
  private prefix;
@@ -36,9 +23,9 @@ declare class Country {
36
23
  }
37
24
  declare class Peru extends Country {
38
25
  constructor(request: <T>(path: string) => Promise<T>);
39
- ruc(ruc: string): Promise<RucResult>;
40
- dni(dni: string): Promise<DniResult>;
41
- search(query: string): Promise<SearchResponse>;
26
+ ruc(ruc: string): Promise<PeRecord>;
27
+ dni(dni: string): Promise<PeRecord>;
28
+ search(query: string): Promise<PeRecord[]>;
42
29
  }
43
30
  export declare class Latinfo {
44
31
  private apiKey;
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "latinfo",
3
- "version": "0.3.0",
4
- "description": "Public data API for Latin America - SDK & CLI",
3
+ "version": "0.4.1",
4
+ "description": "CLI & SDK to query RUC, DNI, and business data from SUNAT Peru. JSON output, AI-friendly.",
5
+ "keywords": ["ruc", "sunat", "peru", "dni", "api", "cli", "tax", "taxpayer", "consulta", "padron", "latin-america", "latam", "json"],
5
6
  "main": "dist/sdk.js",
6
7
  "types": "dist/sdk.d.ts",
7
8
  "bin": {