latinfo 0.20.0 → 0.20.2

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
@@ -49,7 +49,7 @@ const odis_search_1 = require("./odis-search");
49
49
  const mphf_search_1 = require("./mphf-search");
50
50
  const VERSION = '0.18.1';
51
51
  const API_URL = process.env.LATINFO_API_URL || 'https://api.latinfo.dev';
52
- const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || 'Ov23li5fcQaiCsVtaMKK';
52
+ const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || 'Ov23liZAqpaGnYQ6Kp5I';
53
53
  const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.latinfo');
54
54
  const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
55
55
  // --- JSON mode ---
@@ -63,6 +63,10 @@ function jsonError(error, message) {
63
63
  process.exit(1);
64
64
  }
65
65
  function loadConfig() {
66
+ // Env var takes priority — zero friction for agents
67
+ if (process.env.LATINFO_API_KEY) {
68
+ return { api_key: process.env.LATINFO_API_KEY, github_username: '', is_team: true, team_role: 'member' };
69
+ }
66
70
  try {
67
71
  return JSON.parse(fs_1.default.readFileSync(CONFIG_FILE, 'utf-8'));
68
72
  }
@@ -153,7 +157,35 @@ async function apiRequest(config, path) {
153
157
  return res;
154
158
  }
155
159
  // --- Commands ---
156
- async function login(token) {
160
+ async function login(token, username) {
161
+ // Zero-friction login: admin generates key for a username (no browser)
162
+ if (username) {
163
+ let adminSecret;
164
+ try {
165
+ adminSecret = requireAdmin();
166
+ }
167
+ catch {
168
+ console.error('Admin access required for username login. Need ADMIN_SECRET in .dev.vars or env.');
169
+ process.exit(1);
170
+ }
171
+ const res = await fetch(`${API_URL}/team/members`, {
172
+ method: 'POST',
173
+ headers: { Authorization: `Bearer ${adminSecret}`, 'Content-Type': 'application/json' },
174
+ body: JSON.stringify({ github_username: username }),
175
+ });
176
+ const data = await res.json();
177
+ if (!res.ok) {
178
+ console.error(data.message || data.error);
179
+ process.exit(1);
180
+ }
181
+ if (!data.api_key) {
182
+ console.error('Failed to generate API key');
183
+ process.exit(1);
184
+ }
185
+ saveConfig({ api_key: data.api_key, github_username: username, is_team: true, team_role: data.role });
186
+ console.log(`Logged in as ${username} (${data.role}).`);
187
+ return;
188
+ }
157
189
  // PAT login: no browser needed
158
190
  if (token) {
159
191
  const authRes = await fetch(`${API_URL}/auth/github`, {
@@ -169,33 +201,86 @@ async function login(token) {
169
201
  process.exit(1);
170
202
  }
171
203
  const authData = await authRes.json();
172
- saveConfig({ api_key: authData.api_key, github_username: authData.github_username });
173
- console.log(`Logged in as ${authData.github_username}`);
204
+ const config = { api_key: authData.api_key, github_username: authData.github_username };
205
+ // Check team membership
206
+ try {
207
+ const teamRes = await fetch(`${API_URL}/team/me`, { headers: { Authorization: `Bearer ${config.api_key}` } });
208
+ if (teamRes.ok) {
209
+ const team = await teamRes.json();
210
+ config.is_team = true;
211
+ config.team_role = team.role;
212
+ }
213
+ }
214
+ catch { }
215
+ saveConfig(config);
216
+ console.log(`Logged in as ${authData.github_username}${config.is_team ? ` (team: ${config.team_role})` : ''}`);
174
217
  return;
175
218
  }
176
- // OAuth login: opens browser
177
- const port = 8400;
178
- const redirectUri = `http://localhost:${port}/callback`;
179
- const scope = 'read:user,user:email';
180
- const state = crypto.randomUUID();
181
- const authUrl = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${scope}&state=${state}`;
182
- console.log('Opening GitHub...');
183
- console.log('If your browser does not open, visit this URL:\n');
184
- console.log(authUrl + '\n');
185
- openBrowser(authUrl);
186
- const code = await waitForCallback(port, state);
219
+ // Device Flow: no browser redirect needed, works in SSH/containers/agents
220
+ const deviceRes = await fetch('https://github.com/login/device/code', {
221
+ method: 'POST',
222
+ headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
223
+ body: JSON.stringify({ client_id: GITHUB_CLIENT_ID, scope: 'read:user,user:email' }),
224
+ });
225
+ const device = await deviceRes.json();
226
+ console.log(`\n Go to: ${device.verification_uri}`);
227
+ console.log(` Enter code: ${device.user_code}\n`);
228
+ // Try to open browser automatically
229
+ try {
230
+ openBrowser(device.verification_uri);
231
+ }
232
+ catch { }
233
+ // Poll until authorized
234
+ let ghAccessToken = null;
235
+ const deadline = Date.now() + device.expires_in * 1000;
236
+ while (Date.now() < deadline) {
237
+ await new Promise(r => setTimeout(r, (device.interval || 5) * 1000));
238
+ const pollRes = await fetch('https://github.com/login/oauth/access_token', {
239
+ method: 'POST',
240
+ headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
241
+ body: JSON.stringify({ client_id: GITHUB_CLIENT_ID, device_code: device.device_code, grant_type: 'urn:ietf:params:oauth:grant-type:device_code' }),
242
+ });
243
+ const poll = await pollRes.json();
244
+ if (poll.access_token) {
245
+ ghAccessToken = poll.access_token;
246
+ break;
247
+ }
248
+ if (poll.error === 'expired_token') {
249
+ console.error('Code expired. Run latinfo login again.');
250
+ process.exit(1);
251
+ }
252
+ if (poll.error && poll.error !== 'authorization_pending' && poll.error !== 'slow_down') {
253
+ console.error(`GitHub error: ${poll.error}`);
254
+ process.exit(1);
255
+ }
256
+ }
257
+ if (!ghAccessToken) {
258
+ console.error('Login timed out.');
259
+ process.exit(1);
260
+ }
187
261
  const authRes = await fetch(`${API_URL}/auth/github`, {
188
262
  method: 'POST',
189
263
  headers: { 'Content-Type': 'application/json' },
190
- body: JSON.stringify({ code, redirect_uri: redirectUri }),
264
+ body: JSON.stringify({ access_token: ghAccessToken }),
191
265
  });
192
266
  if (!authRes.ok) {
193
267
  console.error('Error getting API key:', await authRes.text());
194
268
  process.exit(1);
195
269
  }
196
270
  const authData = await authRes.json();
197
- saveConfig({ api_key: authData.api_key, github_username: authData.github_username });
198
- console.log(`Logged in as ${authData.github_username}`);
271
+ const config = { api_key: authData.api_key, github_username: authData.github_username };
272
+ // Check team membership
273
+ try {
274
+ const teamRes = await fetch(`${API_URL}/team/me`, { headers: { Authorization: `Bearer ${config.api_key}` } });
275
+ if (teamRes.ok) {
276
+ const team = await teamRes.json();
277
+ config.is_team = true;
278
+ config.team_role = team.role;
279
+ }
280
+ }
281
+ catch { }
282
+ saveConfig(config);
283
+ console.log(`Logged in as ${authData.github_username}${config.is_team ? ` (team: ${config.team_role})` : ''}`);
199
284
  }
200
285
  async function ruc(rucNumber) {
201
286
  if (!rucNumber || !/^\d{11}$/.test(rucNumber)) {
@@ -477,10 +562,17 @@ async function search(query) {
477
562
  function whoami() {
478
563
  const config = requireAuth();
479
564
  if (jsonFlag) {
480
- console.log(JSON.stringify({ username: config.github_username, api_key: config.api_key }));
565
+ console.log(JSON.stringify({ username: config.github_username, api_key: config.api_key, is_team: config.is_team, team_role: config.team_role }));
481
566
  return;
482
567
  }
483
- console.log(config.github_username);
568
+ if (config.is_team) {
569
+ const badges = { admin: '★', member: '●' };
570
+ const badge = badges[config.team_role || 'member'] || '●';
571
+ console.log(`${badge} ${config.github_username} [TEAM ${(config.team_role || 'member').toUpperCase()}]`);
572
+ }
573
+ else {
574
+ console.log(config.github_username);
575
+ }
484
576
  }
485
577
  async function plan() {
486
578
  const config = requireAuth();
@@ -553,7 +645,7 @@ async function adminRequest(path) {
553
645
  return res;
554
646
  }
555
647
  async function importsRun(source) {
556
- const valid = ['pe-sunat-padron', 'pe-oece-licitaciones', 'co-rues', 'all'];
648
+ const valid = ['pe-sunat-padron', 'pe-oece-tenders', 'co-rues', 'all'];
557
649
  if (!valid.includes(source)) {
558
650
  console.error(`Unknown source. Valid: ${valid.join(', ')}`);
559
651
  process.exit(1);
@@ -1450,11 +1542,12 @@ function logout() {
1450
1542
  console.log('Logged out.');
1451
1543
  }
1452
1544
  function help() {
1545
+ const config = loadConfig();
1546
+ const isTeam = config?.is_team;
1453
1547
  console.log(`latinfo v${VERSION} — Tax registry API for Latin America
1454
1548
 
1455
1549
  USAGE
1456
1550
  latinfo <country> <institution> <dataset> <id|--search query|--dni id> [--json]
1457
- latinfo <admin-command> [args]
1458
1551
 
1459
1552
  QUICK START
1460
1553
  npm install -g latinfo
@@ -1477,25 +1570,26 @@ DATA SOURCES
1477
1570
  latinfo pe osce fines <ruc> Provider fines
1478
1571
  latinfo pe osce fines --search <query>
1479
1572
 
1573
+ Peru — SERVIR
1574
+ latinfo pe servir sanctions <dni> Public sector sanctions
1575
+ latinfo pe servir sanctions --search <query>
1576
+
1577
+ Peru — REDAM
1578
+ latinfo pe redam registry <dni> Food debt debtors
1579
+ latinfo pe redam registry --search <query>
1580
+
1480
1581
  Peru — OECE
1481
1582
  latinfo pe oece tenders <query> [flags] Government procurement
1482
1583
  Flags: --category, --min-amount, --max-amount, --buyer, --status, --limit
1483
1584
 
1484
1585
  Colombia — RUES
1485
- latinfo co rues registry <nit> Business registry (3.3M records)
1586
+ latinfo co rues registry <nit> Business registry (9M+ records)
1486
1587
  latinfo co rues registry --search <query>
1487
1588
 
1488
- ADMIN
1589
+ COMMANDS
1489
1590
  login [--token <github_pat>] GitHub OAuth or PAT login
1490
1591
  logout Remove credentials
1491
1592
  whoami Show authenticated user
1492
- imports Show import status
1493
- imports run <source> Trigger import
1494
- imports report [days] Import diagnostics
1495
- costs <users> [avg_req] [pro_%] Cost simulation
1496
- costs --live Production cost report
1497
- bench [flags] Stress test API
1498
- easypipe <command> Generic import pipeline
1499
1593
  completion [bash|zsh] Shell completions
1500
1594
  help This help text
1501
1595
 
@@ -1505,13 +1599,34 @@ FLAGS
1505
1599
  --dni Lookup by DNI (Peru only)
1506
1600
  --version Print version
1507
1601
 
1508
- PRICING
1509
- Free 100,000 requests/day
1510
- Pro 10M requests/month $1/month
1511
-
1512
- CONFIG
1513
- ~/.latinfo/config.json API key
1514
- LATINFO_API_URL Override API URL`);
1602
+ Free and unlimited. No credit card needed.`);
1603
+ if (isTeam) {
1604
+ const isAdmin = config?.team_role === 'admin';
1605
+ console.log(`
1606
+ TEAM
1607
+ tasks My tasks
1608
+ tasks complete <id> Mark done
1609
+ tasks rank Team ranking
1610
+ pipe local <source> Import data locally
1611
+ pipe publish <source> Publish to production
1612
+ docs <topic> Internal documentation`);
1613
+ if (isAdmin) {
1614
+ console.log(`
1615
+ ADMIN
1616
+ team add <username> [--admin] Add team member
1617
+ team remove <username> Remove member
1618
+ team list List all members
1619
+ tasks assign <user> "<title>" [--points N] Assign task
1620
+ tasks approve <id> Approve + award points
1621
+ tasks reject <id> "<reason>" Reject back
1622
+ tasks delete <id> Delete task
1623
+ tasks list --all All tasks
1624
+ imports Show import status
1625
+ imports run <source> Trigger import
1626
+ bench [flags] Stress test API
1627
+ costs --live Production cost report`);
1628
+ }
1629
+ }
1515
1630
  }
1516
1631
  function printLogo() {
1517
1632
  if (!process.stdout.isTTY)
@@ -2710,14 +2825,47 @@ async function pipePublish(args) {
2710
2825
  console.error(`[pipe] Git error: ${e.message}`);
2711
2826
  process.exit(1);
2712
2827
  }
2713
- // 2. Deploy Worker
2714
- console.log(`[pipe] Deploying Worker...`);
2828
+ // 2. Deploy Worker via GitHub Actions (no local Cloudflare token needed)
2829
+ console.log(`[pipe] Waiting for deploy workflow (GitHub Actions)...`);
2715
2830
  try {
2716
- run(`npx wrangler deploy`, { cwd: repo, stdio: 'inherit' });
2831
+ const { execSync: exec } = await Promise.resolve().then(() => __importStar(require('child_process')));
2832
+ const maxWait = 300; // 5 min max
2833
+ const interval = 10;
2834
+ let elapsed = 0;
2835
+ let deployed = false;
2836
+ // Give GitHub a moment to register the push event
2837
+ exec('sleep 5');
2838
+ while (elapsed < maxWait) {
2839
+ const result = exec(`gh run list --workflow=deploy.yml --branch=main --limit=1 --json status,conclusion,headSha`, { cwd: repo, encoding: 'utf-8', stdio: 'pipe' }).toString().trim();
2840
+ const ghRuns = JSON.parse(result);
2841
+ if (ghRuns.length > 0) {
2842
+ const latest = ghRuns[0];
2843
+ if (latest.status === 'completed') {
2844
+ if (latest.conclusion === 'success') {
2845
+ console.log(`[pipe] Deploy successful.`);
2846
+ deployed = true;
2847
+ break;
2848
+ }
2849
+ else {
2850
+ console.error(`[pipe] Deploy failed (${latest.conclusion}) — rolling back`);
2851
+ exec(`git checkout HEAD^ -- src/sources.ts .github/workflows/import.yml && git commit -m "Rollback: remove ${sourceName}" && git push`, { cwd: repo, stdio: 'pipe' });
2852
+ process.exit(1);
2853
+ }
2854
+ }
2855
+ if (elapsed % 30 === 0 && elapsed > 0) {
2856
+ console.log(`[pipe] Still deploying... (${elapsed}s)`);
2857
+ }
2858
+ }
2859
+ exec(`sleep ${interval}`);
2860
+ elapsed += interval;
2861
+ }
2862
+ if (!deployed) {
2863
+ console.error(`[pipe] Deploy timed out after ${maxWait}s`);
2864
+ process.exit(1);
2865
+ }
2717
2866
  }
2718
- catch {
2719
- console.error(`[pipe] Deploy failed — rolling back`);
2720
- run(`git checkout HEAD^ -- src/sources.ts .github/workflows/import.yml && git commit -m "Rollback: remove ${sourceName}" && git push`, { cwd: repo, stdio: 'pipe' });
2867
+ catch (e) {
2868
+ console.error(`[pipe] Deploy error: ${e.message}`);
2721
2869
  process.exit(1);
2722
2870
  }
2723
2871
  // 3. Trigger import on runner
@@ -3408,6 +3556,7 @@ const DOCS = {
3408
3556
  index: `latinfo docs — complete documentation
3409
3557
 
3410
3558
  TOPICS
3559
+ latinfo docs team Team system, tasks, ranking, and how the AI PM works
3411
3560
  latinfo docs pipe How to create a data pipeline (full guide)
3412
3561
  latinfo docs fields searchFieldIndex, statusFieldIndex explained
3413
3562
  latinfo docs v2 V2 search index + MPHF (mandatory)
@@ -3416,6 +3565,75 @@ TOPICS
3416
3565
  latinfo docs troubleshooting Common errors and fixes
3417
3566
  latinfo docs architecture How latinfo works internally
3418
3567
  latinfo docs api API endpoints and response format`,
3568
+ team: `TEAM SYSTEM
3569
+
3570
+ The AI agent is the project manager (PM). It assigns tasks, tracks progress,
3571
+ and coordinates the team through the CLI. No human PM needed.
3572
+
3573
+ GETTING STARTED (new member)
3574
+
3575
+ 1. Install: npm i -g latinfo
3576
+ 2. Login: latinfo login (authenticates with your GitHub account)
3577
+ 3. See tasks: latinfo tasks (shows your assigned tasks)
3578
+ 4. Complete: latinfo tasks complete <id>
3579
+ 5. Ranking: latinfo tasks rank
3580
+
3581
+ ADMIN COMMANDS (requires admin access)
3582
+
3583
+ latinfo team add <username> Add team member
3584
+ latinfo team add <username> --admin Add as admin
3585
+ latinfo team remove <username> Remove member
3586
+ latinfo team list List all members + points
3587
+
3588
+ latinfo tasks assign <user> "<title>" [--points N] Assign task (default 10 pts)
3589
+ latinfo tasks approve <id> Approve completed task (awards points)
3590
+ latinfo tasks reject <id> "<reason>" Reject back to member
3591
+ latinfo tasks delete <id> Delete task
3592
+ latinfo tasks list --all See all tasks from all members
3593
+
3594
+ MEMBER COMMANDS (requires latinfo login)
3595
+
3596
+ latinfo tasks My tasks
3597
+ latinfo tasks complete <id> Mark task as done (waits for approval)
3598
+ latinfo tasks rank Team ranking
3599
+
3600
+ TASK FLOW
3601
+
3602
+ Admin assigns task → Member sees it (pending)
3603
+ Member completes → Status changes to "completed"
3604
+ Admin approves → Points awarded, status "approved"
3605
+ Admin rejects → Back to "pending" with reason
3606
+
3607
+ RANKING LEVELS
3608
+
3609
+ Bronze 0-49 pts
3610
+ Silver 50-149 pts
3611
+ Gold 150-299 pts
3612
+ Diamond 300+ pts
3613
+
3614
+ Points come from approved tasks. The ranking updates automatically.
3615
+ Admins cannot manually change points — only approve/reject tasks.
3616
+
3617
+ HOW THE AI PM WORKS
3618
+
3619
+ The AI agent (Claude, GPT, etc.) can run all these commands autonomously:
3620
+
3621
+ latinfo tasks list --all → See everything
3622
+ latinfo tasks assign ... → Assign work
3623
+ latinfo tasks approve ... → Approve completed work
3624
+ latinfo tasks rank → Report progress
3625
+
3626
+ The AI reads "latinfo docs team" to learn how to operate,
3627
+ then manages the team without human intervention.
3628
+
3629
+ FIRST TIME SETUP (admin)
3630
+
3631
+ 1. You (admin) run: latinfo team add <their-github-username>
3632
+ 2. They run: latinfo login
3633
+ 3. You assign: latinfo tasks assign <username> "Find Mexico SAT data sources"
3634
+ 4. They work and: latinfo tasks complete <id>
3635
+ 5. You approve: latinfo tasks approve <id>
3636
+ 6. Everyone sees: latinfo tasks rank`,
3419
3637
  pipe: `HOW TO CREATE A DATA PIPELINE
3420
3638
 
3421
3639
  latinfo pipe handles storage, indexing, search, and API serving automatically.
@@ -3799,6 +4017,288 @@ function docs(args) {
3799
4017
  }
3800
4018
  console.log(content);
3801
4019
  }
4020
+ function requireTeam() {
4021
+ const config = loadConfig();
4022
+ if (!config?.is_team) {
4023
+ console.error('This command is only available to team members.');
4024
+ process.exit(1);
4025
+ }
4026
+ }
4027
+ function requireTeamAdmin() {
4028
+ const config = loadConfig();
4029
+ if (!config?.is_team) {
4030
+ console.error('This command is only available to team members.');
4031
+ process.exit(1);
4032
+ }
4033
+ if (config.team_role !== 'admin') {
4034
+ console.error('This command requires admin access.');
4035
+ process.exit(1);
4036
+ }
4037
+ }
4038
+ // --- Team & Tasks ---
4039
+ async function teamCmd(args) {
4040
+ const sub = args[0];
4041
+ const adminSecret = requireAdmin();
4042
+ const headers = { Authorization: `Bearer ${adminSecret}`, 'Content-Type': 'application/json' };
4043
+ switch (sub) {
4044
+ case 'add': {
4045
+ const username = args[1];
4046
+ const isAdmin = args.includes('--admin');
4047
+ if (!username) {
4048
+ console.error('Usage: latinfo team add <github-username> [--admin]');
4049
+ process.exit(1);
4050
+ }
4051
+ const res = await fetch(`${API_URL}/team/members`, {
4052
+ method: 'POST', headers,
4053
+ body: JSON.stringify({ github_username: username, role: isAdmin ? 'admin' : 'member' }),
4054
+ });
4055
+ const data = await res.json();
4056
+ if (!res.ok) {
4057
+ console.error(data.message || data.error);
4058
+ process.exit(1);
4059
+ }
4060
+ console.log(`Added ${username} to team (${data.role}).`);
4061
+ if (data.api_key) {
4062
+ if (args.includes('--login')) {
4063
+ // Save key locally — login as this user
4064
+ saveConfig({ api_key: data.api_key, github_username: username, is_team: true, team_role: data.role });
4065
+ console.log(`Logged in as ${username} (${data.role}).`);
4066
+ }
4067
+ else {
4068
+ console.log(`\n API key: ${data.api_key}\n`);
4069
+ console.log(` Share this with the member. They set:`);
4070
+ console.log(` export LATINFO_API_KEY=${data.api_key}`);
4071
+ }
4072
+ }
4073
+ try {
4074
+ const { execSync: exec } = await Promise.resolve().then(() => __importStar(require('child_process')));
4075
+ exec(`gh api repos/carrerahaus/latinfo-api/collaborators/${username} -X PUT -f permission=push`, { stdio: 'pipe' });
4076
+ console.log(`Added ${username} as GitHub collaborator.`);
4077
+ }
4078
+ catch {
4079
+ console.log('Note: Could not add as GitHub collaborator (gh CLI required).');
4080
+ }
4081
+ break;
4082
+ }
4083
+ case 'remove': {
4084
+ const username = args[1];
4085
+ if (!username) {
4086
+ console.error('Usage: latinfo team remove <github-username>');
4087
+ process.exit(1);
4088
+ }
4089
+ const res = await fetch(`${API_URL}/team/members/${username}`, { method: 'DELETE', headers });
4090
+ const data = await res.json();
4091
+ if (!res.ok) {
4092
+ console.error(data.message || data.error);
4093
+ process.exit(1);
4094
+ }
4095
+ console.log(`Removed ${username} from team.`);
4096
+ try {
4097
+ const { execSync: exec } = await Promise.resolve().then(() => __importStar(require('child_process')));
4098
+ exec(`gh api repos/carrerahaus/latinfo-api/collaborators/${username} -X DELETE`, { stdio: 'pipe' });
4099
+ console.log(`Removed ${username} from GitHub.`);
4100
+ }
4101
+ catch { }
4102
+ break;
4103
+ }
4104
+ case 'list': {
4105
+ const res = await fetch(`${API_URL}/team/members`, { headers });
4106
+ const data = await res.json();
4107
+ if (!res.ok) {
4108
+ console.error('Failed to list members');
4109
+ process.exit(1);
4110
+ }
4111
+ if (data.length === 0) {
4112
+ console.log('No team members.');
4113
+ return;
4114
+ }
4115
+ console.log('\n TEAM\n');
4116
+ for (const m of data) {
4117
+ const level = m.points >= 300 ? 'Diamond' : m.points >= 150 ? 'Gold' : m.points >= 50 ? 'Silver' : 'Bronze';
4118
+ console.log(` ${m.github_username.padEnd(22)} ${m.role.padEnd(8)} ${String(m.points).padStart(4)} pts ${level}`);
4119
+ }
4120
+ console.log();
4121
+ break;
4122
+ }
4123
+ default:
4124
+ console.log(`Usage:
4125
+ latinfo team add <username> [--admin] Add team member
4126
+ latinfo team remove <username> Remove member
4127
+ latinfo team list List all members`);
4128
+ }
4129
+ }
4130
+ async function tasksCmd(args) {
4131
+ const sub = args[0];
4132
+ // Admin commands use ADMIN_SECRET
4133
+ if (['assign', 'approve', 'reject', 'delete'].includes(sub)) {
4134
+ const adminSecret = requireAdmin();
4135
+ const headers = { Authorization: `Bearer ${adminSecret}`, 'Content-Type': 'application/json' };
4136
+ switch (sub) {
4137
+ case 'assign': {
4138
+ const username = args[1];
4139
+ const title = args[2];
4140
+ const pointsIdx = args.indexOf('--points');
4141
+ const points = pointsIdx !== -1 ? parseInt(args[pointsIdx + 1]) : 10;
4142
+ if (!username || !title) {
4143
+ console.error('Usage: latinfo tasks assign <username> "<title>" [--points N]');
4144
+ process.exit(1);
4145
+ }
4146
+ const res = await fetch(`${API_URL}/team/tasks`, {
4147
+ method: 'POST', headers,
4148
+ body: JSON.stringify({ assignee_username: username, title, points }),
4149
+ });
4150
+ const data = await res.json();
4151
+ if (!res.ok) {
4152
+ console.error(data.message || data.error);
4153
+ process.exit(1);
4154
+ }
4155
+ console.log(`Task #${data.id} assigned to ${username}: "${title}" (${data.points} pts)`);
4156
+ break;
4157
+ }
4158
+ case 'approve': {
4159
+ const taskId = args[1];
4160
+ if (!taskId) {
4161
+ console.error('Usage: latinfo tasks approve <task-id>');
4162
+ process.exit(1);
4163
+ }
4164
+ const res = await fetch(`${API_URL}/team/tasks/${taskId}`, {
4165
+ method: 'PATCH', headers,
4166
+ body: JSON.stringify({ action: 'approve' }),
4167
+ });
4168
+ const data = await res.json();
4169
+ if (!res.ok) {
4170
+ console.error(data.message || data.error);
4171
+ process.exit(1);
4172
+ }
4173
+ console.log(`Task #${taskId} approved. ${data.points_awarded} points awarded.`);
4174
+ break;
4175
+ }
4176
+ case 'reject': {
4177
+ const taskId = args[1];
4178
+ const reason = args[2] || '';
4179
+ if (!taskId) {
4180
+ console.error('Usage: latinfo tasks reject <task-id> "<reason>"');
4181
+ process.exit(1);
4182
+ }
4183
+ const res = await fetch(`${API_URL}/team/tasks/${taskId}`, {
4184
+ method: 'PATCH', headers,
4185
+ body: JSON.stringify({ action: 'reject', reason }),
4186
+ });
4187
+ const data = await res.json();
4188
+ if (!res.ok) {
4189
+ console.error(data.message || data.error);
4190
+ process.exit(1);
4191
+ }
4192
+ console.log(`Task #${taskId} rejected.`);
4193
+ break;
4194
+ }
4195
+ case 'delete': {
4196
+ const taskId = args[1];
4197
+ if (!taskId) {
4198
+ console.error('Usage: latinfo tasks delete <task-id>');
4199
+ process.exit(1);
4200
+ }
4201
+ const res = await fetch(`${API_URL}/team/tasks/${taskId}`, { method: 'DELETE', headers });
4202
+ const data = await res.json();
4203
+ if (!res.ok) {
4204
+ console.error(data.message || data.error);
4205
+ process.exit(1);
4206
+ }
4207
+ console.log(`Task #${taskId} deleted.`);
4208
+ break;
4209
+ }
4210
+ }
4211
+ return;
4212
+ }
4213
+ // Member commands use API key
4214
+ const config = loadConfig();
4215
+ if (!config?.api_key) {
4216
+ console.error('Not logged in. Run: latinfo login');
4217
+ process.exit(1);
4218
+ }
4219
+ const headers = { Authorization: `Bearer ${config.api_key}`, 'Content-Type': 'application/json' };
4220
+ switch (sub) {
4221
+ case 'complete': {
4222
+ const taskId = args[1];
4223
+ if (!taskId) {
4224
+ console.error('Usage: latinfo tasks complete <task-id>');
4225
+ process.exit(1);
4226
+ }
4227
+ const res = await fetch(`${API_URL}/team/tasks/${taskId}`, {
4228
+ method: 'PATCH', headers,
4229
+ body: JSON.stringify({ action: 'complete' }),
4230
+ });
4231
+ const data = await res.json();
4232
+ if (!res.ok) {
4233
+ console.error(data.message || data.error);
4234
+ process.exit(1);
4235
+ }
4236
+ console.log(`Task #${taskId} marked as completed. Waiting for approval.`);
4237
+ break;
4238
+ }
4239
+ case 'rank': {
4240
+ const res = await fetch(`${API_URL}/team/rank`, { headers });
4241
+ const ranking = await res.json();
4242
+ if (!res.ok) {
4243
+ console.error('Failed to get ranking');
4244
+ process.exit(1);
4245
+ }
4246
+ if (ranking.length === 0) {
4247
+ console.log('No team members yet.');
4248
+ return;
4249
+ }
4250
+ const badges = { Bronze: '●', Silver: '◆', Gold: '★', Diamond: '◈' };
4251
+ console.log('\n RANKING\n');
4252
+ for (const r of ranking) {
4253
+ console.log(` #${r.rank} ${r.username.padEnd(22)} ${String(r.points).padStart(4)} pts ${badges[r.level] || ''} ${r.level}`);
4254
+ }
4255
+ console.log();
4256
+ break;
4257
+ }
4258
+ case 'list': {
4259
+ const allFlag = args.includes('--all');
4260
+ const url = allFlag ? `${API_URL}/team/tasks?all=true` : `${API_URL}/team/tasks`;
4261
+ const reqHeaders = allFlag
4262
+ ? { ...headers, 'X-Admin-Secret': requireAdmin() }
4263
+ : headers;
4264
+ const res = await fetch(url, { headers: reqHeaders });
4265
+ const tasksList = await res.json();
4266
+ if (!res.ok) {
4267
+ console.error('Failed to list tasks');
4268
+ process.exit(1);
4269
+ }
4270
+ if (tasksList.length === 0) {
4271
+ console.log('No tasks.');
4272
+ return;
4273
+ }
4274
+ const icons = { pending: '○', completed: '◉', approved: '★' };
4275
+ for (const t of tasksList) {
4276
+ const assignee = allFlag ? ` @${t.assignee_username}` : '';
4277
+ const rejection = t.status === 'pending' && t.reject_reason ? ` (rejected: ${t.reject_reason})` : '';
4278
+ console.log(` [#${t.id}] ${icons[t.status] || t.status} ${t.title}${assignee} (${t.points} pts)${rejection}`);
4279
+ }
4280
+ break;
4281
+ }
4282
+ default: {
4283
+ // No subcommand = show my tasks
4284
+ const res = await fetch(`${API_URL}/team/tasks`, { headers });
4285
+ const tasksList = await res.json();
4286
+ if (!res.ok) {
4287
+ console.error('Failed to list tasks');
4288
+ process.exit(1);
4289
+ }
4290
+ if (tasksList.length === 0) {
4291
+ console.log('No tasks assigned to you.');
4292
+ return;
4293
+ }
4294
+ const icons = { pending: '○', completed: '◉', approved: '★' };
4295
+ for (const t of tasksList) {
4296
+ const rejection = t.status === 'pending' && t.reject_reason ? ` (rejected: ${t.reject_reason})` : '';
4297
+ console.log(` [#${t.id}] ${icons[t.status] || t.status} ${t.title} (${t.points} pts)${rejection}`);
4298
+ }
4299
+ }
4300
+ }
4301
+ }
3802
4302
  // --- Main ---
3803
4303
  const [command, ...args] = rawArgs;
3804
4304
  const COUNTRIES = ['pe', 'co', 'br', 'mx', 'ar', 'cl', 'ec'];
@@ -3826,7 +4326,7 @@ else {
3826
4326
  // Admin commands (flat)
3827
4327
  switch (command) {
3828
4328
  case 'login':
3829
- login(tokenFlag).catch(e => { console.error(e); process.exit(1); });
4329
+ login(tokenFlag, args[0]).catch(e => { console.error(e); process.exit(1); });
3830
4330
  break;
3831
4331
  case 'logout':
3832
4332
  logout();
@@ -3837,7 +4337,15 @@ else {
3837
4337
  case 'users':
3838
4338
  users().catch(e => { console.error(e); process.exit(1); });
3839
4339
  break;
4340
+ case 'plan':
4341
+ plan().catch(e => { console.error(e); process.exit(1); });
4342
+ break;
4343
+ case 'completion':
4344
+ completion();
4345
+ break;
4346
+ // Team-only commands
3840
4347
  case 'imports':
4348
+ requireTeam();
3841
4349
  if (args[0] === 'run')
3842
4350
  importsRun(args[1] || 'all').catch(e => { console.error(e); process.exit(1); });
3843
4351
  else if (args[0] === 'report')
@@ -3845,38 +4353,49 @@ else {
3845
4353
  else
3846
4354
  imports().catch(e => { console.error(e); process.exit(1); });
3847
4355
  break;
3848
- case 'plan':
3849
- plan().catch(e => { console.error(e); process.exit(1); });
3850
- break;
3851
4356
  case 'costs':
4357
+ requireTeamAdmin();
3852
4358
  (liveFlag ? costsLive() : Promise.resolve(costsSimulate(args[0], args[1], args[2]))).catch(e => { console.error(e); process.exit(1); });
3853
4359
  break;
3854
4360
  case 'bench':
4361
+ requireTeamAdmin();
3855
4362
  bench(args).catch(e => { console.error(e); process.exit(1); });
3856
4363
  break;
3857
4364
  case 'search-server':
4365
+ requireTeamAdmin();
3858
4366
  searchServerStatus().catch(e => { console.error(e); process.exit(1); });
3859
4367
  break;
4368
+ case 'team':
4369
+ requireTeamAdmin();
4370
+ teamCmd(args).catch(e => { console.error(e); process.exit(1); });
4371
+ break;
4372
+ case 'tasks':
4373
+ requireTeam();
4374
+ tasksCmd(args).catch(e => { console.error(e); process.exit(1); });
4375
+ break;
3860
4376
  case 'pipe':
4377
+ requireTeam();
3861
4378
  pipe(args).catch(e => { console.error(e); process.exit(1); });
3862
4379
  break;
3863
4380
  case 'admin':
4381
+ requireTeamAdmin();
3864
4382
  pipe(args).catch(e => { console.error(e); process.exit(1); });
3865
- break; // backward compat
4383
+ break;
3866
4384
  case 'easypipe':
3867
4385
  case 'ep':
4386
+ requireTeam();
3868
4387
  easypipe(args).catch(e => { console.error(e); process.exit(1); });
3869
4388
  break;
3870
4389
  case 'report':
4390
+ requireTeamAdmin();
3871
4391
  report(args).catch(e => { console.error(e); process.exit(1); });
3872
4392
  break;
3873
4393
  case 'issues':
4394
+ requireTeamAdmin();
3874
4395
  issues().catch(e => { console.error(e); process.exit(1); });
3875
4396
  break;
3876
- case 'completion':
3877
- completion();
3878
- break;
3879
4397
  case 'docs':
4398
+ requireTeam();
3880
4399
  docs(args);
3881
4400
  break;
3882
4401
  case 'help':
@@ -45,7 +45,7 @@ exports.dataInfo = dataInfo;
45
45
  const fs = __importStar(require("fs"));
46
46
  const path = __importStar(require("path"));
47
47
  const os_1 = __importDefault(require("os"));
48
- const BASE_NAME = 'pe-oece-licitaciones';
48
+ const BASE_NAME = 'pe-oece-tenders';
49
49
  const FIELD_COUNT = 12;
50
50
  const DATA_DIR = path.join(os_1.default.homedir(), '.latinfo', 'data');
51
51
  const FIELD_NAMES = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latinfo",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "description": "Tax registry & procurement API for Latin America. Query RUC, DNI, NIT, licitaciones from Peru & Colombia. Offline MPHF search, full OCDS data, updated daily.",
5
5
  "homepage": "https://latinfo.dev",
6
6
  "repository": {