prior-cli 1.6.2 → 1.6.4

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/bin/prior.js CHANGED
@@ -45,6 +45,48 @@ let _spinIdx = 0;
45
45
  let _spinStart = null;
46
46
  let _spinLabel = '';
47
47
 
48
+ // ── Tool keyword hints ─────────────────────────────────────────
49
+ // Detects keywords in user input and prepends a hard directive so
50
+ // the model can't second-guess which tool to use.
51
+ const TOOL_HINTS = [
52
+ {
53
+ tool: 'ssl_check',
54
+ patterns: [
55
+ /\bssl\b/i, /\btls\b/i, /\bcertificate\b/i, /\bcert\b/i,
56
+ /\bhttps check\b/i, /\bcert expir/i, /\bssl expir/i,
57
+ ],
58
+ hint: '[TOOL DIRECTIVE: You MUST call ssl_check — do NOT use zap_scan or zap_alerts]',
59
+ },
60
+ {
61
+ tool: 'dns_lookup',
62
+ patterns: [
63
+ /\bdns\b/i, /\bmx record/i, /\bnameserver/i, /\bnslookup\b/i,
64
+ /\bdig\b/i, /\bdns record/i, /\btxt record/i, /\bcname\b/i,
65
+ /\bns record/i, /\baaaa record/i,
66
+ ],
67
+ hint: '[TOOL DIRECTIVE: You MUST call dns_lookup]',
68
+ },
69
+ {
70
+ tool: 'ip_lookup',
71
+ patterns: [
72
+ /\bip lookup\b/i, /\blook up.*ip\b/i, /\bwhere is .+ hosted\b/i,
73
+ /\bwho owns .+(ip|domain)\b/i, /\basn\b/i, /\bgeolocation\b/i,
74
+ /\bwhat ip\b/i, /\bresolve .+(domain|host)\b/i,
75
+ /\blookup\b.*\b(ip|domain|host)\b/i,
76
+ ],
77
+ hint: '[TOOL DIRECTIVE: You MUST call ip_lookup]',
78
+ },
79
+ ];
80
+
81
+ function injectToolHint(text) {
82
+ for (const { patterns, hint } of TOOL_HINTS) {
83
+ if (patterns.some(re => re.test(text))) {
84
+ return `${hint}\n${text}`;
85
+ }
86
+ }
87
+ return text;
88
+ }
89
+
48
90
  function fmtElapsed(ms) {
49
91
  const s = Math.floor(ms / 1000);
50
92
  if (s < 60) return `${s}s`;
@@ -1285,7 +1327,7 @@ Be concise but thorough — this summary replaces the full history to save conte
1285
1327
 
1286
1328
  _currentAbortController = new AbortController();
1287
1329
  await runAgent({
1288
- messages: [...chatHistory, { role: 'user', content: input }],
1330
+ messages: [...chatHistory, { role: 'user', content: injectToolHint(input) }],
1289
1331
  model: currentModel,
1290
1332
  cwd: process.cwd(),
1291
1333
  projectContext,
package/lib/tools.js CHANGED
@@ -416,6 +416,123 @@ const TOOLS = {
416
416
  const data = await res.json();
417
417
  return { output: data.output || JSON.stringify(data), summary: data.summary || `spider started for ${url}` };
418
418
  },
419
+
420
+ async ip_lookup({ target }, {}) {
421
+ if (!target) throw new Error('"target" is required — provide an IP address or domain');
422
+ const encoded = encodeURIComponent(target.trim());
423
+ const res = await fetch(`https://ipinfo.io/${encoded}/json`, {
424
+ headers: { 'User-Agent': 'prior-cli/1.0', Accept: 'application/json' },
425
+ timeout: 10000,
426
+ });
427
+ if (!res.ok) throw new Error(`ipinfo.io error: HTTP ${res.status}`);
428
+ const d = await res.json();
429
+ if (d.error) throw new Error(d.error.message || 'Lookup failed');
430
+ const lines = [
431
+ `IP : ${d.ip || target}`,
432
+ d.hostname ? `Hostname : ${d.hostname}` : null,
433
+ d.org ? `Org / ASN : ${d.org}` : null,
434
+ d.city ? `Location : ${[d.city, d.region, d.country].filter(Boolean).join(', ')}` : null,
435
+ d.postal ? `Postal : ${d.postal}` : null,
436
+ d.timezone ? `Timezone : ${d.timezone}` : null,
437
+ d.loc ? `Coords : ${d.loc}` : null,
438
+ ].filter(Boolean);
439
+ return {
440
+ output: lines.join('\n'),
441
+ summary: `${d.ip || target}${d.org ? ' · ' + d.org : ''}${d.city ? ' · ' + d.city : ''}`,
442
+ };
443
+ },
444
+
445
+ async dns_lookup({ domain, type = 'A' }, {}) {
446
+ if (!domain) throw new Error('"domain" is required');
447
+ const validTypes = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SOA', 'PTR', 'SRV'];
448
+ const qtype = type.toUpperCase();
449
+ if (!validTypes.includes(qtype)) throw new Error(`Invalid type. Choose from: ${validTypes.join(', ')}`);
450
+ const res = await fetch(`https://dns.google/resolve?name=${encodeURIComponent(domain)}&type=${qtype}`, {
451
+ headers: { Accept: 'application/json' },
452
+ timeout: 10000,
453
+ });
454
+ if (!res.ok) throw new Error(`DNS query failed: HTTP ${res.status}`);
455
+ const data = await res.json();
456
+ if (data.Status !== 0) {
457
+ const STATUS = { 1: 'Format error', 2: 'Server failure', 3: 'NXDOMAIN (not found)', 5: 'Refused' };
458
+ throw new Error(STATUS[data.Status] || `DNS error code ${data.Status}`);
459
+ }
460
+ if (!data.Answer || data.Answer.length === 0) {
461
+ return { output: `No ${qtype} records found for ${domain}`, summary: `0 ${qtype} records` };
462
+ }
463
+ const lines = data.Answer.map(r => {
464
+ const ttl = `TTL ${r.TTL}s`;
465
+ return ` ${String(r.type).padEnd(6)} ${ttl.padEnd(12)} ${r.data}`;
466
+ });
467
+ const header = `${domain} ${qtype} records (${data.Answer.length})\n`;
468
+ return {
469
+ output: header + lines.join('\n'),
470
+ summary: `${data.Answer.length} ${qtype} record${data.Answer.length !== 1 ? 's' : ''} for ${domain}`,
471
+ };
472
+ },
473
+
474
+ async ssl_check({ domain }, {}) {
475
+ if (!domain) throw new Error('"domain" is required');
476
+ // Strip protocol if provided
477
+ const host = domain.replace(/^https?:\/\//, '').split('/')[0].split(':')[0];
478
+ // Use crt.sh to get cert info + check via HEAD for expiry details
479
+ const [certRes, headRes] = await Promise.allSettled([
480
+ fetch(`https://crt.sh/?q=${encodeURIComponent(host)}&output=json`, {
481
+ headers: { 'User-Agent': 'prior-cli/1.0' },
482
+ timeout: 12000,
483
+ }),
484
+ fetch(`https://${host}`, {
485
+ method: 'HEAD',
486
+ headers: { 'User-Agent': 'prior-cli/1.0' },
487
+ timeout: 8000,
488
+ }),
489
+ ]);
490
+
491
+ const lines = [`Domain : ${host}`];
492
+
493
+ // Live cert check via HTTPS HEAD — Node's TLS gives us nothing in headers,
494
+ // but we can confirm TLS works and check for errors
495
+ if (headRes.status === 'fulfilled') {
496
+ lines.push(`HTTPS : ✓ reachable (HTTP ${headRes.value.status})`);
497
+ } else {
498
+ const msg = headRes.reason?.message || 'unreachable';
499
+ const expired = /certificate has expired|CERT_HAS_EXPIRED/i.test(msg);
500
+ lines.push(`HTTPS : ✗ ${expired ? 'CERTIFICATE EXPIRED' : msg}`);
501
+ }
502
+
503
+ // crt.sh — most recent issuances
504
+ if (certRes.status === 'fulfilled' && certRes.value.ok) {
505
+ try {
506
+ const certs = await certRes.value.json();
507
+ const recent = certs
508
+ .filter(c => c.name_value && !c.name_value.startsWith('*'))
509
+ .sort((a, b) => new Date(b.not_after) - new Date(a.not_after))
510
+ .slice(0, 3);
511
+ if (recent.length) {
512
+ lines.push('');
513
+ lines.push('Recent certificates (crt.sh):');
514
+ for (const c of recent) {
515
+ const expiry = new Date(c.not_after);
516
+ const issued = new Date(c.not_before);
517
+ const daysLeft = Math.ceil((expiry - Date.now()) / 86400000);
518
+ const status = daysLeft < 0 ? '✗ EXPIRED' : daysLeft < 14 ? `⚠ expires in ${daysLeft}d` : `✓ ${daysLeft}d left`;
519
+ lines.push(` Issuer : ${c.issuer_name?.replace(/^.*?CN=/, 'CN=') || '?'}`);
520
+ lines.push(` Issued : ${issued.toLocaleDateString()}`);
521
+ lines.push(` Expires : ${expiry.toLocaleDateString()} (${status})`);
522
+ lines.push(` Names : ${c.name_value.replace(/\n/g, ', ')}`);
523
+ lines.push(' ─────────────────────────────');
524
+ }
525
+ }
526
+ } catch { /* crt.sh parse failed, HTTPS check is enough */ }
527
+ }
528
+
529
+ // Summary line
530
+ const httpsOk = headRes.status === 'fulfilled';
531
+ return {
532
+ output: lines.join('\n'),
533
+ summary: `${host} · HTTPS ${httpsOk ? '✓' : '✗'}`,
534
+ };
535
+ },
419
536
  };
420
537
 
421
538
  async function executeTool(name, args, context) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prior-cli",
3
- "version": "1.6.2",
3
+ "version": "1.6.4",
4
4
  "description": "Prior Network AI — command-line interface",
5
5
  "bin": {
6
6
  "prior": "bin/prior.js"