@ktmcp-cli/abstractapi 1.0.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/AGENT.md ADDED
@@ -0,0 +1,36 @@
1
+ # AbstractAPI CLI - AI Agent Guide
2
+
3
+ This CLI provides programmatic access to the AbstractAPI Geolocation API.
4
+
5
+ ## Quick Start for AI Agents
6
+
7
+ ```bash
8
+ abstractapi config set --api-key YOUR_KEY
9
+ abstractapi geolocate current
10
+ abstractapi geolocate lookup 8.8.8.8
11
+ ```
12
+
13
+ ## Available Commands
14
+
15
+ ### config
16
+ - `abstractapi config set --api-key <key>` - Set API key
17
+ - `abstractapi config get <key>` - Get config value
18
+ - `abstractapi config list` - Show all config
19
+
20
+ ### geolocate
21
+ - `abstractapi geolocate lookup <ip>` - Look up geolocation for an IP address
22
+ - `abstractapi geolocate current` - Look up your own current IP's geolocation
23
+
24
+ ### vpn
25
+ - `abstractapi vpn detect [ip]` - Detect if IP is using VPN/proxy (omit IP for your own)
26
+
27
+ ### timezone
28
+ - `abstractapi timezone get [--ip <ip>] [--location <location>]` - Get timezone info
29
+ - `abstractapi timezone current` - Get timezone for your current IP
30
+
31
+ ## Tips for Agents
32
+
33
+ - All commands support `--json` for machine-readable output
34
+ - Pipe to `jq`: `abstractapi geolocate lookup 8.8.8.8 --json | jq '{city, country, timezone}'`
35
+ - Omit IP in vpn detect to check your own IP
36
+ - Location strings work for timezone: "London", "New York", "51.5,-0.09"
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 KTMCP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ > "Six months ago, everyone was talking about MCPs. And I was like, screw MCPs. Every MCP would be better as a CLI."
2
+ >
3
+ > — [Peter Steinberger](https://twitter.com/steipete), Founder of OpenClaw
4
+ > [Watch on YouTube (~2:39:00)](https://www.youtube.com/@lexfridman) | [Lex Fridman Podcast #491](https://lexfridman.com/peter-steinberger/)
5
+
6
+ # AbstractAPI CLI
7
+
8
+ Production-ready CLI for the [AbstractAPI](https://abstractapi.com) Geolocation API. Look up IP addresses, detect VPNs, and get timezone information directly from your terminal.
9
+
10
+ > **Disclaimer**: This is an unofficial CLI tool and is not affiliated with, endorsed by, or supported by Abstract API.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install -g @ktmcp-cli/abstractapi
16
+ ```
17
+
18
+ ## Configuration
19
+
20
+ ```bash
21
+ abstractapi config set --api-key YOUR_API_KEY
22
+ ```
23
+
24
+ Get your API key at [abstractapi.com](https://abstractapi.com).
25
+
26
+ ## Usage
27
+
28
+ ### Configuration
29
+
30
+ ```bash
31
+ # Set API key
32
+ abstractapi config set --api-key YOUR_API_KEY
33
+
34
+ # Show configuration
35
+ abstractapi config list
36
+
37
+ # Get a specific config value
38
+ abstractapi config get apiKey
39
+ ```
40
+
41
+ ### Geolocation
42
+
43
+ ```bash
44
+ # Look up an IP address
45
+ abstractapi geolocate lookup 8.8.8.8
46
+ abstractapi geolocate lookup 1.1.1.1
47
+
48
+ # Look up your own current IP
49
+ abstractapi geolocate current
50
+ ```
51
+
52
+ ### VPN Detection
53
+
54
+ ```bash
55
+ # Detect if an IP is using a VPN or proxy
56
+ abstractapi vpn detect 8.8.8.8
57
+
58
+ # Check your own IP
59
+ abstractapi vpn detect
60
+ ```
61
+
62
+ ### Timezone
63
+
64
+ ```bash
65
+ # Get timezone for an IP
66
+ abstractapi timezone get --ip 8.8.8.8
67
+
68
+ # Get timezone by location name
69
+ abstractapi timezone get --location "London"
70
+ abstractapi timezone get --location "New York"
71
+
72
+ # Get timezone for your current IP
73
+ abstractapi timezone current
74
+ ```
75
+
76
+ ### JSON Output
77
+
78
+ All commands support `--json` for machine-readable output:
79
+
80
+ ```bash
81
+ # Look up IP as JSON
82
+ abstractapi geolocate lookup 8.8.8.8 --json
83
+
84
+ # Pipe to jq
85
+ abstractapi geolocate lookup 1.1.1.1 --json | jq '{city, country, timezone}'
86
+
87
+ # Get your timezone as JSON
88
+ abstractapi timezone current --json | jq '.timezone'
89
+ ```
90
+
91
+ ## Examples
92
+
93
+ ```bash
94
+ # Find where a suspicious IP is located
95
+ abstractapi geolocate lookup 192.168.1.1
96
+
97
+ # Check if your VPN is working
98
+ abstractapi vpn detect
99
+
100
+ # Find timezone for a remote meeting participant
101
+ abstractapi timezone get --ip 123.456.789.0
102
+
103
+ # Script: Get country code for multiple IPs
104
+ for ip in 8.8.8.8 1.1.1.1 9.9.9.9; do
105
+ echo -n "$ip: "
106
+ abstractapi geolocate lookup $ip --json | jq -r '.country_code'
107
+ done
108
+ ```
109
+
110
+ ## License
111
+
112
+ MIT
113
+
114
+ ---
115
+
116
+ Part of the [KTMCP CLI](https://killthemcp.com) project — replacing MCPs with simple, composable CLIs.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../src/index.js';
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@ktmcp-cli/abstractapi",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready CLI for Abstract API Geolocation - Kill The MCP",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "abstractapi": "bin/abstractapi.js"
9
+ },
10
+ "keywords": ["abstractapi", "geolocation", "ip", "cli", "api", "ktmcp"],
11
+ "author": "KTMCP",
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "commander": "^12.0.0",
15
+ "axios": "^1.6.7",
16
+ "chalk": "^5.3.0",
17
+ "ora": "^8.0.1",
18
+ "conf": "^12.0.0"
19
+ },
20
+ "engines": { "node": ">=18.0.0" },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/ktmcp-cli/abstractapi.git"
24
+ },
25
+ "homepage": "https://killthemcp.com/abstractapi-cli",
26
+ "bugs": { "url": "https://github.com/ktmcp-cli/abstractapi/issues" }
27
+ }
package/src/api.js ADDED
@@ -0,0 +1,81 @@
1
+ import axios from 'axios';
2
+ import { getConfig } from './config.js';
3
+
4
+ const BASE_URL = 'https://ipgeolocation.abstractapi.com/v1';
5
+
6
+ function getApiKey() {
7
+ const apiKey = getConfig('apiKey');
8
+ if (!apiKey) {
9
+ throw new Error('API key not configured. Run: abstractapi config set apiKey YOUR_API_KEY');
10
+ }
11
+ return apiKey;
12
+ }
13
+
14
+ function handleApiError(error) {
15
+ if (error.response) {
16
+ const status = error.response.status;
17
+ const data = error.response.data;
18
+ if (status === 401 || status === 403) {
19
+ throw new Error('Authentication failed. Check your API key: abstractapi config set apiKey YOUR_KEY');
20
+ } else if (status === 429) {
21
+ throw new Error('Rate limit exceeded. Please wait before retrying.');
22
+ } else if (status === 404) {
23
+ throw new Error('Resource not found.');
24
+ } else {
25
+ const message = data?.error?.message || data?.message || JSON.stringify(data);
26
+ throw new Error(`API Error (${status}): ${message}`);
27
+ }
28
+ } else if (error.request) {
29
+ throw new Error('No response from AbstractAPI. Check your internet connection.');
30
+ } else {
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ async function apiGet(url, params = {}) {
36
+ const apiKey = getApiKey();
37
+ try {
38
+ const response = await axios.get(url, {
39
+ params: { api_key: apiKey, ...params }
40
+ });
41
+ return response.data;
42
+ } catch (error) {
43
+ handleApiError(error);
44
+ }
45
+ }
46
+
47
+ // ============================================================
48
+ // GEOLOCATE
49
+ // ============================================================
50
+
51
+ export async function lookupIp(ipAddress) {
52
+ return await apiGet(BASE_URL + '/', { ip_address: ipAddress });
53
+ }
54
+
55
+ export async function lookupCurrent() {
56
+ return await apiGet(BASE_URL + '/');
57
+ }
58
+
59
+ // ============================================================
60
+ // VPN DETECTION
61
+ // ============================================================
62
+
63
+ export async function detectVpn(ipAddress) {
64
+ const params = ipAddress ? { ip_address: ipAddress } : {};
65
+ return await apiGet('https://vpndetection.abstractapi.com/v1/', params);
66
+ }
67
+
68
+ // ============================================================
69
+ // TIMEZONE
70
+ // ============================================================
71
+
72
+ export async function getTimezone({ ipAddress, location }) {
73
+ const params = {};
74
+ if (ipAddress) params.ip_address = ipAddress;
75
+ if (location) params.location = location;
76
+ return await apiGet('https://timezone.abstractapi.com/v1/', params);
77
+ }
78
+
79
+ export async function getCurrentTimezone() {
80
+ return await apiGet('https://timezone.abstractapi.com/v1/');
81
+ }
package/src/config.js ADDED
@@ -0,0 +1,19 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({ projectName: '@ktmcp-cli/abstractapi' });
4
+
5
+ export function getConfig(key) {
6
+ return config.get(key);
7
+ }
8
+
9
+ export function setConfig(key, value) {
10
+ config.set(key, value);
11
+ }
12
+
13
+ export function isConfigured() {
14
+ return !!config.get('apiKey');
15
+ }
16
+
17
+ export function getAllConfig() {
18
+ return config.store;
19
+ }
package/src/index.js ADDED
@@ -0,0 +1,251 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getConfig, setConfig, isConfigured, getAllConfig } from './config.js';
5
+ import {
6
+ lookupIp,
7
+ lookupCurrent,
8
+ detectVpn,
9
+ getTimezone,
10
+ getCurrentTimezone
11
+ } from './api.js';
12
+
13
+ const program = new Command();
14
+
15
+ // ============================================================
16
+ // Helpers
17
+ // ============================================================
18
+
19
+ function printSuccess(message) {
20
+ console.log(chalk.green('✓') + ' ' + message);
21
+ }
22
+
23
+ function printError(message) {
24
+ console.error(chalk.red('✗') + ' ' + message);
25
+ }
26
+
27
+ function printJson(data) {
28
+ console.log(JSON.stringify(data, null, 2));
29
+ }
30
+
31
+ async function withSpinner(message, fn) {
32
+ const spinner = ora(message).start();
33
+ try {
34
+ const result = await fn();
35
+ spinner.stop();
36
+ return result;
37
+ } catch (error) {
38
+ spinner.stop();
39
+ throw error;
40
+ }
41
+ }
42
+
43
+ function requireAuth() {
44
+ if (!isConfigured()) {
45
+ printError('AbstractAPI key not configured.');
46
+ console.log('\nRun the following to configure:');
47
+ console.log(chalk.cyan(' abstractapi config set apiKey YOUR_API_KEY'));
48
+ process.exit(1);
49
+ }
50
+ }
51
+
52
+ function printGeoResult(data) {
53
+ console.log(chalk.bold('\nGeolocation Result\n'));
54
+ console.log('IP Address: ', chalk.cyan(data.ip_address || 'N/A'));
55
+ console.log('City: ', data.city || 'N/A');
56
+ console.log('Region: ', data.region || 'N/A');
57
+ console.log('Country: ', `${data.country || 'N/A'} (${data.country_code || '??'})`);
58
+ console.log('Continent: ', data.continent || 'N/A');
59
+ console.log('Latitude: ', data.latitude ?? 'N/A');
60
+ console.log('Longitude: ', data.longitude ?? 'N/A');
61
+ console.log('Timezone: ', data.timezone?.name || 'N/A');
62
+ console.log('Currency: ', data.currency?.currency_name ? `${data.currency.currency_name} (${data.currency.currency_code})` : 'N/A');
63
+ console.log('ISP: ', data.connection?.isp_name || data.connection?.autonomous_system_organization || 'N/A');
64
+ console.log('');
65
+ }
66
+
67
+ // ============================================================
68
+ // Program metadata
69
+ // ============================================================
70
+
71
+ program
72
+ .name('abstractapi')
73
+ .description(chalk.bold('AbstractAPI CLI') + ' - IP geolocation & detection from your terminal')
74
+ .version('1.0.0');
75
+
76
+ // ============================================================
77
+ // CONFIG
78
+ // ============================================================
79
+
80
+ const configCmd = program.command('config').description('Manage CLI configuration');
81
+
82
+ configCmd
83
+ .command('set')
84
+ .description('Set configuration values')
85
+ .option('--api-key <key>', 'AbstractAPI API key')
86
+ .action((options) => {
87
+ if (options.apiKey) {
88
+ setConfig('apiKey', options.apiKey);
89
+ printSuccess('API key set');
90
+ } else {
91
+ printError('No options provided. Use --api-key');
92
+ }
93
+ });
94
+
95
+ configCmd
96
+ .command('get')
97
+ .description('Get a configuration value')
98
+ .argument('<key>', 'Config key to retrieve')
99
+ .action((key) => {
100
+ const value = getConfig(key);
101
+ if (value === undefined || value === null || value === '') {
102
+ console.log(chalk.yellow(`${key}: not set`));
103
+ } else {
104
+ const masked = key.toLowerCase().includes('key') ? '*'.repeat(8) : value;
105
+ console.log(`${key}: ${chalk.green(masked)}`);
106
+ }
107
+ });
108
+
109
+ configCmd
110
+ .command('list')
111
+ .description('List all configuration values')
112
+ .action(() => {
113
+ const all = getAllConfig();
114
+ console.log(chalk.bold('\nAbstractAPI CLI Configuration\n'));
115
+ console.log('API Key:', all.apiKey ? chalk.green('*'.repeat(8)) : chalk.red('not set'));
116
+ console.log('');
117
+ });
118
+
119
+ // ============================================================
120
+ // GEOLOCATE
121
+ // ============================================================
122
+
123
+ const geoCmd = program.command('geolocate').description('IP geolocation lookup');
124
+
125
+ geoCmd
126
+ .command('lookup <ip>')
127
+ .description('Look up geolocation data for an IP address')
128
+ .option('--json', 'Output as JSON')
129
+ .action(async (ip, options) => {
130
+ requireAuth();
131
+ try {
132
+ const data = await withSpinner(`Looking up IP ${ip}...`, () => lookupIp(ip));
133
+ if (options.json) { printJson(data); return; }
134
+ printGeoResult(data);
135
+ } catch (error) {
136
+ printError(error.message);
137
+ process.exit(1);
138
+ }
139
+ });
140
+
141
+ geoCmd
142
+ .command('current')
143
+ .description('Look up geolocation data for your current IP address')
144
+ .option('--json', 'Output as JSON')
145
+ .action(async (options) => {
146
+ requireAuth();
147
+ try {
148
+ const data = await withSpinner('Looking up your IP address...', () => lookupCurrent());
149
+ if (options.json) { printJson(data); return; }
150
+ printGeoResult(data);
151
+ } catch (error) {
152
+ printError(error.message);
153
+ process.exit(1);
154
+ }
155
+ });
156
+
157
+ // ============================================================
158
+ // VPN DETECTION
159
+ // ============================================================
160
+
161
+ const vpnCmd = program.command('vpn').description('VPN and proxy detection');
162
+
163
+ vpnCmd
164
+ .command('detect')
165
+ .description('Detect if an IP address is using a VPN or proxy')
166
+ .argument('[ip]', 'IP address to check (omit for your current IP)')
167
+ .option('--json', 'Output as JSON')
168
+ .action(async (ip, options) => {
169
+ requireAuth();
170
+ try {
171
+ const data = await withSpinner(`Checking VPN status${ip ? ` for ${ip}` : ''}...`, () => detectVpn(ip));
172
+ if (options.json) { printJson(data); return; }
173
+ console.log(chalk.bold('\nVPN Detection Result\n'));
174
+ console.log('IP Address: ', chalk.cyan(data.ip_address || ip || 'N/A'));
175
+ const isVpn = data.is_vpn ?? data.vpn ?? false;
176
+ const isProxy = data.is_proxy ?? data.proxy ?? false;
177
+ const isTor = data.is_tor ?? data.tor ?? false;
178
+ console.log('VPN: ', isVpn ? chalk.red('Yes') : chalk.green('No'));
179
+ console.log('Proxy: ', isProxy ? chalk.red('Yes') : chalk.green('No'));
180
+ console.log('Tor: ', isTor ? chalk.red('Yes') : chalk.green('No'));
181
+ if (data.service) console.log('Service: ', data.service);
182
+ console.log('');
183
+ } catch (error) {
184
+ printError(error.message);
185
+ process.exit(1);
186
+ }
187
+ });
188
+
189
+ // ============================================================
190
+ // TIMEZONE
191
+ // ============================================================
192
+
193
+ const tzCmd = program.command('timezone').description('Timezone lookup');
194
+
195
+ tzCmd
196
+ .command('get')
197
+ .description('Get timezone information for an IP address or location')
198
+ .option('--ip <ip>', 'IP address to look up')
199
+ .option('--location <location>', 'Location name or coordinates (e.g. "London" or "51.5,-0.09")')
200
+ .option('--json', 'Output as JSON')
201
+ .action(async (options) => {
202
+ requireAuth();
203
+ try {
204
+ const data = await withSpinner('Fetching timezone data...', () =>
205
+ getTimezone({ ipAddress: options.ip, location: options.location })
206
+ );
207
+ if (options.json) { printJson(data); return; }
208
+ console.log(chalk.bold('\nTimezone Information\n'));
209
+ console.log('Timezone: ', chalk.cyan(data.timezone || 'N/A'));
210
+ console.log('Abbreviation: ', data.abbreviation || data.timezone_abbreviation || 'N/A');
211
+ console.log('GMT Offset: ', data.gmt_offset ?? 'N/A');
212
+ console.log('Current Time: ', data.datetime || data.current_time || 'N/A');
213
+ console.log('Is DST: ', data.is_dst !== undefined ? (data.is_dst ? chalk.yellow('Yes') : 'No') : 'N/A');
214
+ console.log('');
215
+ } catch (error) {
216
+ printError(error.message);
217
+ process.exit(1);
218
+ }
219
+ });
220
+
221
+ tzCmd
222
+ .command('current')
223
+ .description('Get timezone information for your current IP address')
224
+ .option('--json', 'Output as JSON')
225
+ .action(async (options) => {
226
+ requireAuth();
227
+ try {
228
+ const data = await withSpinner('Fetching your timezone...', () => getCurrentTimezone());
229
+ if (options.json) { printJson(data); return; }
230
+ console.log(chalk.bold('\nYour Timezone Information\n'));
231
+ console.log('Timezone: ', chalk.cyan(data.timezone || 'N/A'));
232
+ console.log('Abbreviation: ', data.abbreviation || data.timezone_abbreviation || 'N/A');
233
+ console.log('GMT Offset: ', data.gmt_offset ?? 'N/A');
234
+ console.log('Current Time: ', data.datetime || data.current_time || 'N/A');
235
+ console.log('Is DST: ', data.is_dst !== undefined ? (data.is_dst ? chalk.yellow('Yes') : 'No') : 'N/A');
236
+ console.log('');
237
+ } catch (error) {
238
+ printError(error.message);
239
+ process.exit(1);
240
+ }
241
+ });
242
+
243
+ // ============================================================
244
+ // Parse
245
+ // ============================================================
246
+
247
+ program.parse(process.argv);
248
+
249
+ if (process.argv.length <= 2) {
250
+ program.help();
251
+ }