@apitap/core 1.5.3 → 1.6.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/README.md +28 -8
- package/dist/auth/handoff.js +1 -1
- package/dist/auth/handoff.js.map +1 -1
- package/dist/capture/cdp-attach.d.ts +60 -0
- package/dist/capture/cdp-attach.js +422 -0
- package/dist/capture/cdp-attach.js.map +1 -0
- package/dist/capture/filter.js +6 -0
- package/dist/capture/filter.js.map +1 -1
- package/dist/capture/parameterize.d.ts +7 -6
- package/dist/capture/parameterize.js +204 -12
- package/dist/capture/parameterize.js.map +1 -1
- package/dist/capture/session.js +20 -10
- package/dist/capture/session.js.map +1 -1
- package/dist/cli.js +387 -20
- package/dist/cli.js.map +1 -1
- package/dist/discovery/openapi.js +23 -50
- package/dist/discovery/openapi.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +12 -0
- package/dist/mcp.js.map +1 -1
- package/dist/native-host.js +5 -0
- package/dist/native-host.js.map +1 -1
- package/dist/plugin.js +10 -3
- package/dist/plugin.js.map +1 -1
- package/dist/replay/engine.d.ts +13 -0
- package/dist/replay/engine.js +20 -0
- package/dist/replay/engine.js.map +1 -1
- package/dist/skill/apis-guru.d.ts +35 -0
- package/dist/skill/apis-guru.js +128 -0
- package/dist/skill/apis-guru.js.map +1 -0
- package/dist/skill/generator.d.ts +7 -1
- package/dist/skill/generator.js +35 -3
- package/dist/skill/generator.js.map +1 -1
- package/dist/skill/merge.d.ts +29 -0
- package/dist/skill/merge.js +252 -0
- package/dist/skill/merge.js.map +1 -0
- package/dist/skill/openapi-converter.d.ts +31 -0
- package/dist/skill/openapi-converter.js +383 -0
- package/dist/skill/openapi-converter.js.map +1 -0
- package/dist/types.d.ts +41 -0
- package/package.json +1 -1
- package/src/auth/handoff.ts +1 -1
- package/src/capture/cdp-attach.ts +501 -0
- package/src/capture/filter.ts +5 -0
- package/src/capture/parameterize.ts +207 -11
- package/src/capture/session.ts +20 -10
- package/src/cli.ts +420 -18
- package/src/discovery/openapi.ts +25 -56
- package/src/index.ts +1 -0
- package/src/mcp.ts +12 -0
- package/src/native-host.ts +7 -0
- package/src/plugin.ts +10 -3
- package/src/replay/engine.ts +19 -0
- package/src/skill/apis-guru.ts +163 -0
- package/src/skill/generator.ts +38 -3
- package/src/skill/merge.ts +281 -0
- package/src/skill/openapi-converter.ts +426 -0
- package/src/types.ts +42 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// src/cli.ts
|
|
3
3
|
import { capture } from './capture/monitor.js';
|
|
4
4
|
import { writeSkillFile, readSkillFile, listSkillFiles } from './skill/store.js';
|
|
5
|
-
import { replayEndpoint } from './replay/engine.js';
|
|
5
|
+
import { replayEndpoint, getConfidenceHint } from './replay/engine.js';
|
|
6
6
|
import { AuthManager, getMachineId } from './auth/manager.js';
|
|
7
7
|
import { deriveSigningKey } from './auth/crypto.js';
|
|
8
8
|
import { signSkillFile } from './skill/signing.js';
|
|
@@ -25,6 +25,10 @@ import { readFileSync } from 'node:fs';
|
|
|
25
25
|
import { stat, unlink } from 'node:fs/promises';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
import { createMcpServer } from './mcp.js';
|
|
28
|
+
import { attach, parseDomainPatterns } from './capture/cdp-attach.js';
|
|
29
|
+
import { isOpenAPISpec, convertOpenAPISpec } from './skill/openapi-converter.js';
|
|
30
|
+
import { mergeSkillFile } from './skill/merge.js';
|
|
31
|
+
import { fetchApisGuruList, filterEntries, fetchSpec } from './skill/apis-guru.js';
|
|
28
32
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
29
33
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
30
34
|
const VERSION = pkg.version;
|
|
@@ -56,6 +60,8 @@ function printUsage() {
|
|
|
56
60
|
|
|
57
61
|
Usage:
|
|
58
62
|
apitap capture <url> Capture API traffic from a website
|
|
63
|
+
apitap attach [--port 9222] [--domain *.github.com]
|
|
64
|
+
Attach to running Chrome and capture API traffic
|
|
59
65
|
apitap discover <url> Detect APIs without a browser (fast recon)
|
|
60
66
|
apitap inspect <url> Discover APIs without saving (X-ray vision)
|
|
61
67
|
apitap search <query> Search skill files for a domain or endpoint
|
|
@@ -63,7 +69,9 @@ function printUsage() {
|
|
|
63
69
|
apitap show <domain> Show endpoints for a domain
|
|
64
70
|
apitap replay <domain> <endpoint-id> [key=value...]
|
|
65
71
|
Replay an API endpoint
|
|
66
|
-
apitap import <file>
|
|
72
|
+
apitap import <file|url> Import a skill file or OpenAPI spec
|
|
73
|
+
apitap import --from apis-guru
|
|
74
|
+
Bulk-import from APIs.guru directory
|
|
67
75
|
apitap refresh <domain> Refresh auth tokens via browser
|
|
68
76
|
apitap auth [domain] View or manage stored auth
|
|
69
77
|
apitap mcp Run the full ApiTap MCP server over stdio
|
|
@@ -115,6 +123,11 @@ function printUsage() {
|
|
|
115
123
|
|
|
116
124
|
Import options:
|
|
117
125
|
--yes Skip confirmation prompt
|
|
126
|
+
--dry-run Show what would change without writing
|
|
127
|
+
--json Output machine-readable JSON
|
|
128
|
+
--from apis-guru Bulk-import from APIs.guru directory
|
|
129
|
+
--search <term> Filter APIs.guru entries by name/title
|
|
130
|
+
--limit <n> Max APIs to import (default: 100)
|
|
118
131
|
|
|
119
132
|
Serve options:
|
|
120
133
|
--json Output tool list as JSON on stderr
|
|
@@ -405,40 +418,145 @@ async function handleReplay(positional, flags) {
|
|
|
405
418
|
}, null, 2));
|
|
406
419
|
}
|
|
407
420
|
else {
|
|
421
|
+
const hint = endpoint ? getConfidenceHint(endpoint.confidence) : null;
|
|
422
|
+
if (hint) {
|
|
423
|
+
console.error(` Note: ${hint}`);
|
|
424
|
+
}
|
|
408
425
|
console.log(`\n Status: ${result.status}\n`);
|
|
409
426
|
console.log(JSON.stringify(result.data, null, 2));
|
|
410
427
|
console.log();
|
|
411
428
|
}
|
|
412
429
|
}
|
|
413
430
|
async function handleImport(positional, flags) {
|
|
414
|
-
|
|
415
|
-
if (
|
|
416
|
-
|
|
431
|
+
// --from apis-guru: bulk import mode
|
|
432
|
+
if (flags['from'] === 'apis-guru') {
|
|
433
|
+
await handleApisGuruImport(flags);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const source = positional[0];
|
|
437
|
+
if (!source) {
|
|
438
|
+
console.error('Error: File path or URL required. Usage: apitap import <file|url>');
|
|
417
439
|
process.exit(1);
|
|
418
440
|
}
|
|
419
441
|
const json = flags.json === true;
|
|
442
|
+
// Reject YAML files with helpful message
|
|
443
|
+
if (/\.ya?ml$/i.test(source)) {
|
|
444
|
+
const msg = 'YAML specs not yet supported. Convert to JSON first: npx swagger-cli bundle spec.yaml -o spec.json';
|
|
445
|
+
if (json) {
|
|
446
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
console.error(`Error: ${msg}`);
|
|
450
|
+
}
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
// Load content — from URL or file
|
|
454
|
+
let rawText;
|
|
455
|
+
let sourceUrl = source;
|
|
456
|
+
const isUrl = source.startsWith('http://') || source.startsWith('https://');
|
|
457
|
+
if (isUrl) {
|
|
458
|
+
try {
|
|
459
|
+
const ssrfCheck = await resolveAndValidateUrl(source);
|
|
460
|
+
if (!ssrfCheck.safe) {
|
|
461
|
+
throw new Error(`SSRF check failed: ${ssrfCheck.reason}`);
|
|
462
|
+
}
|
|
463
|
+
const response = await fetch(source, { signal: AbortSignal.timeout(30_000) });
|
|
464
|
+
if (!response.ok) {
|
|
465
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
466
|
+
}
|
|
467
|
+
rawText = await response.text();
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
const msg = `Failed to fetch ${source}: ${err.message}`;
|
|
471
|
+
if (json) {
|
|
472
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
console.error(`Error: ${msg}`);
|
|
476
|
+
}
|
|
477
|
+
process.exit(1);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
try {
|
|
482
|
+
const { readFile } = await import('node:fs/promises');
|
|
483
|
+
rawText = await readFile(source, 'utf-8');
|
|
484
|
+
sourceUrl = `file://${resolve(source)}`;
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
const msg = `Failed to read ${source}: ${err.message}`;
|
|
488
|
+
if (json) {
|
|
489
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
console.error(`Error: ${msg}`);
|
|
493
|
+
}
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Parse JSON
|
|
498
|
+
let parsed;
|
|
499
|
+
try {
|
|
500
|
+
parsed = JSON.parse(rawText);
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
const msg = `Invalid JSON in ${source}`;
|
|
504
|
+
if (json) {
|
|
505
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
console.error(`Error: ${msg}`);
|
|
509
|
+
}
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
// Route: OpenAPI spec vs SkillFile
|
|
513
|
+
if (isOpenAPISpec(parsed)) {
|
|
514
|
+
await handleOpenAPIImport(parsed, sourceUrl, flags);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// --- Existing SkillFile import flow (unchanged) ---
|
|
518
|
+
if (isUrl) {
|
|
519
|
+
// SkillFile imports from URLs: write to temp file first
|
|
520
|
+
const { writeFile: writeTmp } = await import('node:fs/promises');
|
|
521
|
+
const { tmpdir } = await import('node:os');
|
|
522
|
+
const tmpPath = join(tmpdir(), `apitap-import-${Date.now()}.json`);
|
|
523
|
+
await writeTmp(tmpPath, rawText);
|
|
524
|
+
try {
|
|
525
|
+
await handleSkillFileImport(tmpPath, json);
|
|
526
|
+
}
|
|
527
|
+
finally {
|
|
528
|
+
const { unlink: unlinkTmp } = await import('node:fs/promises');
|
|
529
|
+
await unlinkTmp(tmpPath).catch(() => { });
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
await handleSkillFileImport(source, json);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async function handleSkillFileImport(filePath, json) {
|
|
420
537
|
// Get local key for signature verification
|
|
421
538
|
const machineId = await getMachineId();
|
|
422
539
|
const key = deriveSigningKey(machineId);
|
|
423
540
|
// DNS-resolving SSRF check before importing (prevents DNS rebinding attacks)
|
|
541
|
+
let raw;
|
|
424
542
|
try {
|
|
425
|
-
|
|
426
|
-
if (raw.baseUrl) {
|
|
427
|
-
const dnsCheck = await resolveAndValidateUrl(raw.baseUrl);
|
|
428
|
-
if (!dnsCheck.safe) {
|
|
429
|
-
const msg = `DNS rebinding risk: ${dnsCheck.reason}`;
|
|
430
|
-
if (json) {
|
|
431
|
-
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
432
|
-
}
|
|
433
|
-
else {
|
|
434
|
-
console.error(`Error: ${msg}`);
|
|
435
|
-
}
|
|
436
|
-
process.exit(1);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
543
|
+
raw = JSON.parse(await import('node:fs/promises').then(fs => fs.readFile(filePath, 'utf-8')));
|
|
439
544
|
}
|
|
440
545
|
catch {
|
|
441
|
-
// Parse errors will be caught by importSkillFile
|
|
546
|
+
// Parse errors will be caught by importSkillFile below
|
|
547
|
+
}
|
|
548
|
+
if (raw?.baseUrl) {
|
|
549
|
+
const dnsCheck = await resolveAndValidateUrl(raw.baseUrl);
|
|
550
|
+
if (!dnsCheck.safe) {
|
|
551
|
+
const msg = `DNS rebinding risk: ${dnsCheck.reason}`;
|
|
552
|
+
if (json) {
|
|
553
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
console.error(`Error: ${msg}`);
|
|
557
|
+
}
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
442
560
|
}
|
|
443
561
|
const result = await importSkillFile(filePath, undefined, key);
|
|
444
562
|
if (!result.success) {
|
|
@@ -457,6 +575,243 @@ async function handleImport(positional, flags) {
|
|
|
457
575
|
console.log(`\n ✓ Imported skill file: ${result.skillFile}\n`);
|
|
458
576
|
}
|
|
459
577
|
}
|
|
578
|
+
async function handleOpenAPIImport(spec, specUrl, flags) {
|
|
579
|
+
const json = flags.json === true;
|
|
580
|
+
const dryRun = flags['dry-run'] === true;
|
|
581
|
+
const skillsDir = SKILLS_DIR || join(APITAP_DIR, 'skills');
|
|
582
|
+
// Convert OpenAPI spec to endpoints
|
|
583
|
+
let importResult;
|
|
584
|
+
try {
|
|
585
|
+
importResult = convertOpenAPISpec(spec, specUrl);
|
|
586
|
+
}
|
|
587
|
+
catch (err) {
|
|
588
|
+
const msg = `Failed to convert OpenAPI spec: ${err.message}`;
|
|
589
|
+
if (json) {
|
|
590
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
console.error(`Error: ${msg}`);
|
|
594
|
+
}
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
const { domain, endpoints, meta } = importResult;
|
|
598
|
+
const specVersion = spec.openapi || spec.swagger || 'unknown';
|
|
599
|
+
if (!json) {
|
|
600
|
+
console.log(`\n Importing ${domain} from OpenAPI ${specVersion} spec...\n`);
|
|
601
|
+
}
|
|
602
|
+
// SSRF validate the domain
|
|
603
|
+
const dnsCheck = await resolveAndValidateUrl(`https://${domain}`);
|
|
604
|
+
if (!dnsCheck.safe) {
|
|
605
|
+
const msg = `DNS rebinding risk for ${domain}: ${dnsCheck.reason}`;
|
|
606
|
+
if (json) {
|
|
607
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
console.error(`Error: ${msg}`);
|
|
611
|
+
}
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
// Read existing skill file (if any)
|
|
615
|
+
let existing = null;
|
|
616
|
+
try {
|
|
617
|
+
existing = await readSkillFile(domain, skillsDir, {
|
|
618
|
+
verifySignature: true,
|
|
619
|
+
trustUnsigned: true,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
catch (err) {
|
|
623
|
+
if (err?.code !== 'ENOENT') {
|
|
624
|
+
if (!json)
|
|
625
|
+
console.error(` Warning: could not read existing skill file for ${domain}: ${err.message}`);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// Merge
|
|
629
|
+
const { skillFile, diff } = mergeSkillFile(existing, endpoints, meta);
|
|
630
|
+
// Ensure domain and baseUrl reflect the API, not the spec source URL
|
|
631
|
+
skillFile.domain = domain;
|
|
632
|
+
skillFile.baseUrl = `https://${domain}`;
|
|
633
|
+
if (dryRun) {
|
|
634
|
+
if (json) {
|
|
635
|
+
console.log(JSON.stringify({
|
|
636
|
+
success: true,
|
|
637
|
+
dryRun: true,
|
|
638
|
+
domain,
|
|
639
|
+
diff,
|
|
640
|
+
totalEndpoints: skillFile.endpoints.length,
|
|
641
|
+
}));
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
printOpenAPIDiff(diff, skillFile, skillsDir);
|
|
645
|
+
console.log(' (dry run — no changes written)\n');
|
|
646
|
+
}
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
// Sign and write
|
|
650
|
+
const machineId = await getMachineId();
|
|
651
|
+
const key = deriveSigningKey(machineId);
|
|
652
|
+
const signed = signSkillFile(skillFile, key);
|
|
653
|
+
const filePath = await writeSkillFile(signed, skillsDir);
|
|
654
|
+
if (json) {
|
|
655
|
+
console.log(JSON.stringify({
|
|
656
|
+
success: true,
|
|
657
|
+
domain,
|
|
658
|
+
skillFile: filePath,
|
|
659
|
+
diff,
|
|
660
|
+
totalEndpoints: signed.endpoints.length,
|
|
661
|
+
}));
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
printOpenAPIDiff(diff, signed, filePath);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
function printOpenAPIDiff(diff, skillFile, pathOrDir) {
|
|
668
|
+
console.log(` ✓ ${diff.preserved} existing captured endpoints preserved`);
|
|
669
|
+
console.log(` + ${diff.added} new endpoints added from OpenAPI spec`);
|
|
670
|
+
console.log(` ~ ${diff.enriched} endpoints enriched with spec metadata`);
|
|
671
|
+
console.log(` · ${diff.skipped} skipped (already imported)`);
|
|
672
|
+
console.log();
|
|
673
|
+
console.log(` Skill file: ${pathOrDir} (${skillFile.endpoints.length} endpoints)\n`);
|
|
674
|
+
}
|
|
675
|
+
async function handleApisGuruImport(flags) {
|
|
676
|
+
const json = flags.json === true;
|
|
677
|
+
const dryRun = flags['dry-run'] === true;
|
|
678
|
+
const limit = typeof flags.limit === 'string' ? parseInt(flags.limit, 10) : 100;
|
|
679
|
+
const search = typeof flags.search === 'string' ? flags.search : undefined;
|
|
680
|
+
const skillsDir = SKILLS_DIR || join(APITAP_DIR, 'skills');
|
|
681
|
+
if (!json) {
|
|
682
|
+
console.log(`\n Importing from APIs.guru (limit: ${limit})...\n`);
|
|
683
|
+
}
|
|
684
|
+
// Fetch and filter
|
|
685
|
+
let entries;
|
|
686
|
+
try {
|
|
687
|
+
const allEntries = await fetchApisGuruList();
|
|
688
|
+
entries = filterEntries(allEntries, { search, limit, preferOpenapi3: true });
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
const msg = `Failed to fetch APIs.guru list: ${err.message}`;
|
|
692
|
+
if (json) {
|
|
693
|
+
console.log(JSON.stringify({ success: false, reason: msg }));
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
console.error(`Error: ${msg}`);
|
|
697
|
+
}
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
if (entries.length === 0) {
|
|
701
|
+
if (json) {
|
|
702
|
+
console.log(JSON.stringify({ success: true, imported: 0, failed: 0, skipped: 0, totalEndpoints: 0 }));
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
console.log(' No matching APIs found.\n');
|
|
706
|
+
}
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
const total = entries.length;
|
|
710
|
+
let imported = 0;
|
|
711
|
+
let failed = 0;
|
|
712
|
+
let skippedApis = 0;
|
|
713
|
+
let totalEndpointsAdded = 0;
|
|
714
|
+
const machineId = await getMachineId();
|
|
715
|
+
const key = deriveSigningKey(machineId);
|
|
716
|
+
const results = [];
|
|
717
|
+
for (let i = 0; i < entries.length; i++) {
|
|
718
|
+
const entry = entries[i];
|
|
719
|
+
const idx = String(i + 1).padStart(String(total).length, ' ');
|
|
720
|
+
try {
|
|
721
|
+
// Fetch spec
|
|
722
|
+
const spec = await fetchSpec(entry.specUrl);
|
|
723
|
+
// Convert
|
|
724
|
+
const importResult = convertOpenAPISpec(spec, entry.specUrl);
|
|
725
|
+
const { domain, endpoints, meta } = importResult;
|
|
726
|
+
if (endpoints.length === 0) {
|
|
727
|
+
if (!json) {
|
|
728
|
+
console.log(` [${idx}/${total}] SKIP ${domain.padEnd(24)} 0 endpoints (${entry.title})`);
|
|
729
|
+
}
|
|
730
|
+
results.push({ index: i + 1, status: 'skip', domain, title: entry.title, endpointsAdded: 0 });
|
|
731
|
+
skippedApis++;
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
// SSRF validate
|
|
735
|
+
const dnsCheck = await resolveAndValidateUrl(`https://${domain}`);
|
|
736
|
+
if (!dnsCheck.safe) {
|
|
737
|
+
if (!json) {
|
|
738
|
+
console.log(` [${idx}/${total}] SKIP ${domain.padEnd(24)} SSRF risk (${entry.title})`);
|
|
739
|
+
}
|
|
740
|
+
results.push({ index: i + 1, status: 'skip', domain, title: entry.title, endpointsAdded: 0 });
|
|
741
|
+
skippedApis++;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
// Read existing
|
|
745
|
+
let existing = null;
|
|
746
|
+
try {
|
|
747
|
+
existing = await readSkillFile(domain, skillsDir, {
|
|
748
|
+
verifySignature: true,
|
|
749
|
+
trustUnsigned: true,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
catch (err) {
|
|
753
|
+
if (err?.code !== 'ENOENT') {
|
|
754
|
+
if (!json)
|
|
755
|
+
console.error(` Warning: could not read existing skill file for ${domain}: ${err.message}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// Merge
|
|
759
|
+
const { skillFile, diff } = mergeSkillFile(existing, endpoints, meta);
|
|
760
|
+
// Ensure domain and baseUrl reflect the API, not the spec source URL
|
|
761
|
+
skillFile.domain = domain;
|
|
762
|
+
skillFile.baseUrl = `https://${domain}`;
|
|
763
|
+
if (!dryRun) {
|
|
764
|
+
// Sign and write
|
|
765
|
+
const signed = signSkillFile(skillFile, key);
|
|
766
|
+
await writeSkillFile(signed, skillsDir);
|
|
767
|
+
}
|
|
768
|
+
if (!json) {
|
|
769
|
+
console.log(` [${idx}/${total}] OK ${domain.padEnd(24)} +${diff.added} endpoints (${entry.title})`);
|
|
770
|
+
}
|
|
771
|
+
results.push({ index: i + 1, status: 'ok', domain, title: entry.title, endpointsAdded: diff.added });
|
|
772
|
+
imported++;
|
|
773
|
+
totalEndpointsAdded += diff.added;
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
if (!json) {
|
|
777
|
+
console.log(` [${idx}/${total}] FAIL ${entry.providerName.padEnd(24)} ${err.message}`);
|
|
778
|
+
}
|
|
779
|
+
results.push({
|
|
780
|
+
index: i + 1,
|
|
781
|
+
status: 'fail',
|
|
782
|
+
domain: entry.providerName,
|
|
783
|
+
title: entry.title,
|
|
784
|
+
endpointsAdded: 0,
|
|
785
|
+
error: err.message,
|
|
786
|
+
});
|
|
787
|
+
failed++;
|
|
788
|
+
}
|
|
789
|
+
// Small delay between requests to be polite to APIs.guru
|
|
790
|
+
if (i < entries.length - 1) {
|
|
791
|
+
await new Promise(r => setTimeout(r, 100));
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (json) {
|
|
795
|
+
console.log(JSON.stringify({
|
|
796
|
+
success: true,
|
|
797
|
+
dryRun,
|
|
798
|
+
imported,
|
|
799
|
+
failed,
|
|
800
|
+
skipped: skippedApis,
|
|
801
|
+
totalEndpoints: totalEndpointsAdded,
|
|
802
|
+
results,
|
|
803
|
+
}));
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
console.log();
|
|
807
|
+
console.log(` Done: ${imported} imported, ${failed} failed, ${skippedApis} skipped`);
|
|
808
|
+
console.log(` ${totalEndpointsAdded.toLocaleString()} endpoints added across ${imported} APIs`);
|
|
809
|
+
if (dryRun) {
|
|
810
|
+
console.log(' (dry run — no changes written)');
|
|
811
|
+
}
|
|
812
|
+
console.log();
|
|
813
|
+
}
|
|
814
|
+
}
|
|
460
815
|
async function handleRefresh(positional, flags) {
|
|
461
816
|
const domain = positional[0];
|
|
462
817
|
if (!domain) {
|
|
@@ -1115,6 +1470,15 @@ async function handleExtension(positional, flags) {
|
|
|
1115
1470
|
console.error('Usage: apitap extension install --extension-id <id>');
|
|
1116
1471
|
process.exit(1);
|
|
1117
1472
|
}
|
|
1473
|
+
async function handleAttach(_positional, flags) {
|
|
1474
|
+
const port = typeof flags.port === 'string' ? parseInt(flags.port, 10) : 9222;
|
|
1475
|
+
const domainPatterns = parseDomainPatterns(typeof flags.domain === 'string' ? flags.domain : undefined);
|
|
1476
|
+
const json = flags.json === true;
|
|
1477
|
+
const result = await attach({ port, domainPatterns, json });
|
|
1478
|
+
if (json) {
|
|
1479
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1118
1482
|
async function main() {
|
|
1119
1483
|
const { command, positional, flags } = parseArgs(process.argv.slice(2));
|
|
1120
1484
|
// Handle --version flag before command dispatch
|
|
@@ -1180,6 +1544,9 @@ async function main() {
|
|
|
1180
1544
|
case 'extension':
|
|
1181
1545
|
await handleExtension(positional, flags);
|
|
1182
1546
|
break;
|
|
1547
|
+
case 'attach':
|
|
1548
|
+
await handleAttach(positional, flags);
|
|
1549
|
+
break;
|
|
1183
1550
|
default:
|
|
1184
1551
|
printUsage();
|
|
1185
1552
|
}
|