latinfo 0.6.0 → 0.7.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
@@ -10,7 +10,7 @@ const path_1 = __importDefault(require("path"));
10
10
  const os_1 = __importDefault(require("os"));
11
11
  const child_process_1 = require("child_process");
12
12
  const demo_data_1 = require("./demo-data");
13
- const VERSION = '0.6.0';
13
+ const VERSION = '0.7.0';
14
14
  const API_URL = process.env.LATINFO_API_URL || 'https://api.latinfo.dev';
15
15
  const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || 'Ov23li5fcQaiCsVtaMKK';
16
16
  const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.latinfo');
@@ -46,7 +46,7 @@ function deleteConfig() {
46
46
  // --- GitHub Authorization Code Flow ---
47
47
  function openBrowser(url) {
48
48
  if (process.platform === 'win32') {
49
- (0, child_process_1.exec)(`start "" "${url}"`);
49
+ (0, child_process_1.exec)(`cmd /c start "" "${url}"`);
50
50
  }
51
51
  else {
52
52
  const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
@@ -391,6 +391,124 @@ function costsSimulate(usersStr, rpmStr, proPctStr) {
391
391
  const margin = revenue - cfCost;
392
392
  printCosts({ users, pro_users: proUsers, requests, cf_tier: cfTier, cf_cost: cfCost, revenue, margin, safe: margin >= 0 });
393
393
  }
394
+ // --- Licitaciones ---
395
+ function parseFlag(args, flag) {
396
+ const idx = args.indexOf(flag);
397
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
398
+ }
399
+ async function licitaciones(args) {
400
+ // Parse flags from raw process.argv to avoid them being stripped
401
+ const allArgs = process.argv.slice(2);
402
+ const opts = {
403
+ category: parseFlag(allArgs, '--category') || parseFlag(allArgs, '-c'),
404
+ minAmount: parseFlag(allArgs, '--min-amount') ? Number(parseFlag(allArgs, '--min-amount')) : undefined,
405
+ maxAmount: parseFlag(allArgs, '--max-amount') ? Number(parseFlag(allArgs, '--max-amount')) : undefined,
406
+ buyer: parseFlag(allArgs, '--buyer') || parseFlag(allArgs, '-b'),
407
+ method: parseFlag(allArgs, '--method') || parseFlag(allArgs, '-m'),
408
+ status: parseFlag(allArgs, '--status') || parseFlag(allArgs, '-s'),
409
+ limit: parseFlag(allArgs, '--limit') || parseFlag(allArgs, '-l') ? Number(parseFlag(allArgs, '--limit') || parseFlag(allArgs, '-l')) : undefined,
410
+ };
411
+ // Query: everything in args that isn't a flag or flag value
412
+ const flagNames = ['--category', '-c', '--min-amount', '--max-amount', '--buyer', '-b', '--method', '-m', '--status', '-s', '--limit', '-l', '--json'];
413
+ const queryParts = [];
414
+ for (let i = 0; i < args.length; i++) {
415
+ if (flagNames.includes(args[i])) {
416
+ i++;
417
+ continue;
418
+ } // skip flag + value
419
+ queryParts.push(args[i]);
420
+ }
421
+ if (queryParts.length > 0)
422
+ opts.query = queryParts.join(' ');
423
+ // Subcommand: info
424
+ if (args[0] === 'info') {
425
+ const config = requireAuth();
426
+ const res = await apiRequest(config, '/pe/licitaciones/info');
427
+ const info = await res.json();
428
+ if (jsonFlag) {
429
+ console.log(JSON.stringify(info));
430
+ }
431
+ else {
432
+ console.log(` Records: ${info.records.toLocaleString()}`);
433
+ }
434
+ return;
435
+ }
436
+ // Subcommand: help
437
+ if (args[0] === 'help' || (!opts.query && !opts.category && !opts.buyer && !opts.method && !opts.status && opts.minAmount === undefined && opts.maxAmount === undefined)) {
438
+ console.log(`latinfo licitaciones — Search Peru government procurement (OECE/SEACE)
439
+
440
+ USAGE
441
+ latinfo licitaciones <query> [flags]
442
+ latinfo licitaciones [flags]
443
+ latinfo licitaciones info
444
+ latinfo licitaciones help
445
+
446
+ EXAMPLES
447
+ latinfo licitaciones "construccion puente"
448
+ latinfo licitaciones "servicio alimentacion" --category services
449
+ latinfo licitaciones --category works --min-amount 1000000
450
+ latinfo licitaciones --buyer "municipalidad" --limit 5
451
+ latinfo licitaciones "software" --json
452
+
453
+ FILTERS
454
+ --category, -c goods, services, works
455
+ --min-amount Minimum amount in PEN
456
+ --max-amount Maximum amount in PEN
457
+ --buyer, -b Buyer name (substring match)
458
+ --method, -m Procurement method (substring match)
459
+ --status, -s Status: CONVOCADO, CONTRATADO, DESIERTO, NULO, CONSENTIDO
460
+ --limit, -l Max results (default: 20)
461
+ --json JSON output
462
+
463
+ DATA
464
+ Source: OECE/SEACE (contratacionesabiertas.oece.gob.pe)
465
+ License: CC BY 4.0`);
466
+ return;
467
+ }
468
+ const config = requireAuth();
469
+ // Build query string
470
+ const params = new URLSearchParams();
471
+ if (opts.query)
472
+ params.set('q', opts.query);
473
+ if (opts.category)
474
+ params.set('category', opts.category);
475
+ if (opts.minAmount !== undefined)
476
+ params.set('min_amount', String(opts.minAmount));
477
+ if (opts.maxAmount !== undefined)
478
+ params.set('max_amount', String(opts.maxAmount));
479
+ if (opts.buyer)
480
+ params.set('buyer', opts.buyer);
481
+ if (opts.method)
482
+ params.set('method', opts.method);
483
+ if (opts.status)
484
+ params.set('status', opts.status);
485
+ if (opts.limit !== undefined)
486
+ params.set('limit', String(opts.limit));
487
+ const res = await apiRequest(config, `/pe/licitaciones?${params}`);
488
+ const results = await res.json();
489
+ if (jsonFlag) {
490
+ console.log(JSON.stringify(results));
491
+ return;
492
+ }
493
+ if (results.length === 0) {
494
+ console.log('No results found.');
495
+ return;
496
+ }
497
+ for (const r of results) {
498
+ const monto = parseFloat(r.monto);
499
+ const montoFmt = monto > 0 ? `S/ ${monto.toLocaleString('es-PE', { minimumFractionDigits: 2 })}` : 'N/A';
500
+ const fecha = r.fecha?.split('T')[0] || '';
501
+ const desc = r.descripcion?.length > 100 ? r.descripcion.substring(0, 100) + '...' : (r.descripcion || '');
502
+ console.log(` ${fecha} ${montoFmt} [${r.categoria}] [${r.estado}]`);
503
+ console.log(` ${desc}`);
504
+ console.log(` Buyer: ${r.buyer_nombre}`);
505
+ if (r.url)
506
+ console.log(` ${r.url}`);
507
+ console.log(` ${r.metodo} — ${r.ocid}`);
508
+ console.log();
509
+ }
510
+ console.log(`${results.length} result(s)`);
511
+ }
394
512
  function logout() {
395
513
  deleteConfig();
396
514
  console.log('Logged out.');
@@ -451,6 +569,16 @@ COMMANDS
451
569
  Real-time cost report from production (admin only).
452
570
  Requires LATINFO_ADMIN_SECRET env var.
453
571
 
572
+ licitaciones <query> [flags]
573
+ Search Peru government procurement (OECE/SEACE).
574
+ Flags: --category, --min-amount, --max-amount, --buyer, --method, --status, --limit
575
+ Run 'latinfo licitaciones help' for details.
576
+
577
+ completion [bash|zsh]
578
+ Output shell completion script.
579
+ bash: eval "$(latinfo completion bash)"
580
+ zsh: eval "$(latinfo completion zsh)"
581
+
454
582
  help Show this help text.
455
583
 
456
584
  FLAGS
@@ -482,6 +610,41 @@ EXIT CODES
482
610
  function version() {
483
611
  console.log(`latinfo/${VERSION}`);
484
612
  }
613
+ function completion() {
614
+ const shell = rawArgs[1] || 'bash';
615
+ if (shell === 'zsh') {
616
+ console.log(`#compdef latinfo
617
+ _latinfo() {
618
+ local -a commands=(login logout whoami plan costs ruc dni search licitaciones lic help)
619
+ local -a lic_flags=(--category --min-amount --max-amount --buyer --method --status --limit --json)
620
+ local -a global_flags=(--json --live --version --token)
621
+ if (( CURRENT == 2 )); then
622
+ _describe 'command' commands
623
+ elif [[ "\${words[2]}" == "licitaciones" || "\${words[2]}" == "lic" ]]; then
624
+ _describe 'flag' lic_flags
625
+ else
626
+ _describe 'flag' global_flags
627
+ fi
628
+ }
629
+ compdef _latinfo latinfo`);
630
+ }
631
+ else {
632
+ console.log(`_latinfo_completions() {
633
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
634
+ local prev="\${COMP_WORDS[1]}"
635
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
636
+ COMPREPLY=( $(compgen -W "login logout whoami plan costs ruc dni search licitaciones lic help" -- "$cur") )
637
+ elif [[ "$prev" == "licitaciones" || "$prev" == "lic" ]]; then
638
+ COMPREPLY=( $(compgen -W "--category --min-amount --max-amount --buyer --method --status --limit --json info help" -- "$cur") )
639
+ elif [[ "$prev" == "--category" || "$prev" == "-c" ]]; then
640
+ COMPREPLY=( $(compgen -W "goods services works" -- "$cur") )
641
+ elif [[ "$prev" == "--status" || "$prev" == "-s" ]]; then
642
+ COMPREPLY=( $(compgen -W "CONVOCADO CONTRATADO DESIERTO NULO CONSENTIDO" -- "$cur") )
643
+ fi
644
+ }
645
+ complete -F _latinfo_completions latinfo`);
646
+ }
647
+ }
485
648
  // --- Main ---
486
649
  const [command, ...args] = rawArgs;
487
650
  if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
@@ -513,6 +676,13 @@ else {
513
676
  case 'search':
514
677
  search(args.join(' ')).catch(e => { console.error(e); process.exit(1); });
515
678
  break;
679
+ case 'licitaciones':
680
+ case 'lic':
681
+ licitaciones(args).catch(e => { console.error(e); process.exit(1); });
682
+ break;
683
+ case 'completion':
684
+ completion();
685
+ break;
516
686
  case 'help':
517
687
  help();
518
688
  break;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Local licitaciones search for CLI prototype.
3
+ * Reads binary files from ~/.latinfo/data/
4
+ */
5
+ export interface LicitacionesOptions {
6
+ query?: string;
7
+ category?: string;
8
+ minAmount?: number;
9
+ maxAmount?: number;
10
+ buyer?: string;
11
+ method?: string;
12
+ status?: string;
13
+ limit?: number;
14
+ }
15
+ export declare function searchLicitaciones(opts: LicitacionesOptions): Record<string, string>[];
16
+ export declare function dataInfo(): {
17
+ records: number;
18
+ binSize: number;
19
+ date: string;
20
+ } | null;
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ /**
3
+ * Local licitaciones search for CLI prototype.
4
+ * Reads binary files from ~/.latinfo/data/
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ var __importDefault = (this && this.__importDefault) || function (mod) {
40
+ return (mod && mod.__esModule) ? mod : { "default": mod };
41
+ };
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.searchLicitaciones = searchLicitaciones;
44
+ exports.dataInfo = dataInfo;
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ const os_1 = __importDefault(require("os"));
48
+ const BASE_NAME = 'pe-oece-licitaciones';
49
+ const FIELD_COUNT = 12;
50
+ const DATA_DIR = path.join(os_1.default.homedir(), '.latinfo', 'data');
51
+ const FIELD_NAMES = [
52
+ 'ocid', 'titulo', 'descripcion', 'monto', 'moneda',
53
+ 'categoria', 'metodo', 'buyer_nombre', 'buyer_id',
54
+ 'fecha', 'estado', 'unspsc',
55
+ ];
56
+ function decodeRecord(data, offset) {
57
+ if (offset + 4 > data.length)
58
+ return null;
59
+ const recordLen = data.readUInt32LE(offset);
60
+ if (offset + recordLen > data.length)
61
+ return null;
62
+ const fields = [];
63
+ let pos = offset + 4;
64
+ for (let i = 0; i < FIELD_COUNT; i++) {
65
+ if (pos + 2 > offset + recordLen)
66
+ break;
67
+ const fieldLen = data.readUInt16LE(pos);
68
+ pos += 2;
69
+ fields.push(data.subarray(pos, pos + fieldLen).toString('utf-8'));
70
+ pos += fieldLen;
71
+ }
72
+ return { fields, size: recordLen };
73
+ }
74
+ function fieldsToObject(fields) {
75
+ const obj = {};
76
+ for (let i = 0; i < FIELD_NAMES.length && i < fields.length; i++) {
77
+ obj[FIELD_NAMES[i]] = fields[i];
78
+ }
79
+ return obj;
80
+ }
81
+ function searchLicitaciones(opts) {
82
+ const binPath = path.join(DATA_DIR, `${BASE_NAME}.bin`);
83
+ const idxPath = path.join(DATA_DIR, `${BASE_NAME}.idx`);
84
+ if (!fs.existsSync(binPath) || !fs.existsSync(idxPath)) {
85
+ throw new Error('Licitaciones data not found. Build it first:\n' +
86
+ ' npx tsx src/imports/pe-oece-build.ts <input.jsonl> ~/.latinfo/data');
87
+ }
88
+ const bin = fs.readFileSync(binPath);
89
+ const idx = fs.readFileSync(idxPath);
90
+ if (idx.subarray(0, 4).toString() !== 'LOCE')
91
+ throw new Error('Invalid index file');
92
+ const count = idx.readUInt32LE(4);
93
+ const maxResults = opts.limit || 20;
94
+ const results = [];
95
+ const queryTokens = opts.query
96
+ ? opts.query.toLowerCase().split(/\s+/).filter(t => t.length > 0)
97
+ : [];
98
+ const categoryFilter = opts.category?.toLowerCase();
99
+ const buyerFilter = opts.buyer?.toLowerCase();
100
+ const methodFilter = opts.method?.toLowerCase();
101
+ const statusFilter = opts.status?.toUpperCase();
102
+ for (let i = 0; i < count && results.length < maxResults; i++) {
103
+ const offset = idx.readUInt32LE(12 + i * 8);
104
+ const decoded = decodeRecord(bin, offset);
105
+ if (!decoded)
106
+ continue;
107
+ const obj = fieldsToObject(decoded.fields);
108
+ if (categoryFilter && obj.categoria.toLowerCase() !== categoryFilter)
109
+ continue;
110
+ const monto = parseFloat(obj.monto);
111
+ if (opts.minAmount !== undefined && monto < opts.minAmount)
112
+ continue;
113
+ if (opts.maxAmount !== undefined && monto > opts.maxAmount)
114
+ continue;
115
+ if (buyerFilter && !obj.buyer_nombre.toLowerCase().includes(buyerFilter))
116
+ continue;
117
+ if (methodFilter && !obj.metodo.toLowerCase().includes(methodFilter))
118
+ continue;
119
+ if (statusFilter && obj.estado.toUpperCase() !== statusFilter)
120
+ continue;
121
+ if (queryTokens.length > 0) {
122
+ const searchText = (obj.titulo + ' ' + obj.descripcion + ' ' + obj.buyer_nombre).toLowerCase();
123
+ const allMatch = queryTokens.every(token => searchText.includes(token));
124
+ if (!allMatch)
125
+ continue;
126
+ }
127
+ results.push(obj);
128
+ }
129
+ return results;
130
+ }
131
+ function dataInfo() {
132
+ const binPath = path.join(DATA_DIR, `${BASE_NAME}.bin`);
133
+ const idxPath = path.join(DATA_DIR, `${BASE_NAME}.idx`);
134
+ if (!fs.existsSync(binPath) || !fs.existsSync(idxPath))
135
+ return null;
136
+ const idx = fs.readFileSync(idxPath);
137
+ const records = idx.readUInt32LE(4);
138
+ const binSize = fs.statSync(binPath).size;
139
+ const date = fs.statSync(binPath).mtime.toISOString().split('T')[0];
140
+ return { records, binSize, date };
141
+ }
package/dist/sdk.d.ts CHANGED
@@ -15,6 +15,31 @@ export interface PeRecord {
15
15
  manzana: string;
16
16
  kilometro: string;
17
17
  }
18
+ export interface PeLicitacion {
19
+ ocid: string;
20
+ titulo: string;
21
+ descripcion: string;
22
+ monto: string;
23
+ moneda: string;
24
+ categoria: string;
25
+ metodo: string;
26
+ buyer_nombre: string;
27
+ buyer_id: string;
28
+ fecha: string;
29
+ estado: string;
30
+ unspsc: string;
31
+ url: string;
32
+ }
33
+ export interface LicitacionesQuery {
34
+ q?: string;
35
+ category?: string;
36
+ min_amount?: number;
37
+ max_amount?: number;
38
+ buyer?: string;
39
+ method?: string;
40
+ status?: string;
41
+ limit?: number;
42
+ }
18
43
  declare class Country {
19
44
  private request;
20
45
  private prefix;
@@ -26,6 +51,7 @@ declare class Peru extends Country {
26
51
  ruc(ruc: string): Promise<PeRecord>;
27
52
  dni(dni: string): Promise<PeRecord>;
28
53
  search(query: string): Promise<PeRecord[]>;
54
+ licitaciones(query: LicitacionesQuery): Promise<PeLicitacion[]>;
29
55
  }
30
56
  export declare class Latinfo {
31
57
  private apiKey;
package/dist/sdk.js CHANGED
@@ -26,6 +26,26 @@ class Peru extends Country {
26
26
  async search(query) {
27
27
  return this.countryRequest(`/search?q=${encodeURIComponent(query)}`);
28
28
  }
29
+ async licitaciones(query) {
30
+ const params = new URLSearchParams();
31
+ if (query.q)
32
+ params.set('q', query.q);
33
+ if (query.category)
34
+ params.set('category', query.category);
35
+ if (query.min_amount !== undefined)
36
+ params.set('min_amount', String(query.min_amount));
37
+ if (query.max_amount !== undefined)
38
+ params.set('max_amount', String(query.max_amount));
39
+ if (query.buyer)
40
+ params.set('buyer', query.buyer);
41
+ if (query.method)
42
+ params.set('method', query.method);
43
+ if (query.status)
44
+ params.set('status', query.status);
45
+ if (query.limit !== undefined)
46
+ params.set('limit', String(query.limit));
47
+ return this.countryRequest(`/licitaciones?${params}`);
48
+ }
29
49
  }
30
50
  class Latinfo {
31
51
  apiKey;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latinfo",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Tax registry API for Latin America. Query RUC, DNI, and company data from SUNAT Peru. 18M+ records, updated daily, sub-100ms from anywhere.",
5
5
  "homepage": "https://latinfo.dev",
6
6
  "repository": {