ag-cli 0.1.1
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.
Potentially problematic release.
This version of ag-cli might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/README.md +32 -0
- data/ag-cli.gemspec +24 -0
- data/bin/ag +4 -0
- data/lib/ag_cli.rb +169 -0
- metadata +57 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 273774f57310ca83f546eca5efa5c9eab6298d65d87acbfbfff7b02f951dfcf3
|
|
4
|
+
data.tar.gz: 0a09c48ae701d23f8b4244c662f771955d7861c42a3ef6e72ef56f81eeee8eb1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6f84e2c6abdd9e86edb2966fdcc329b561fe5015ddd3c4fca438cc07c0598990fc96236dd032d921e0398ddab8ed64748ededc62cbf2d9e2f97a22ebbcef088a
|
|
7
|
+
data.tar.gz: d8cdb10d36b8eefb1e354562fce4438b5433d92ddfed96458324a99937e5550fddf525431130cf5d7a15bf5a3724aad36c35972ce1adf0ad5dde6c51870c8c80
|
data/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# ag-cli
|
|
2
|
+
|
|
3
|
+
One small CLI for the shveik.dev agent products. Zero dependencies, Ruby 2.7+ (stdlib only).
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
gem install ag-cli # then: ag data endpoints
|
|
7
|
+
ag data endpoints
|
|
8
|
+
ag data quote unblock '{"url":"https://example.com"}' # the unpaid 402 quote
|
|
9
|
+
ag vps docs
|
|
10
|
+
ag mail pricing
|
|
11
|
+
ag pay health
|
|
12
|
+
ag products
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Products: `data` (agdata.shveik.dev), `proxy` (agproxy), `vps` (agvps), `mail` (agmail), `pay` (agpay); the full names work too.
|
|
16
|
+
Commands: `endpoints`, `docs`, `pricing`, `openapi`, `health`, `quote`, `call`.
|
|
17
|
+
|
|
18
|
+
Paying: the CLI never holds keys and never signs. `quote` prints the 402 quote; sign it with your own wallet and send the call with
|
|
19
|
+
`ag data call unblock '{"url":"..."}' --payment <signature>` (or `AG_PAYMENT`). Exit codes: 0 ok, 1 error, 2 usage, 3 payment required.
|
|
20
|
+
`--origin <url>` or `AG_ORIGIN` overrides the service origin.
|
|
21
|
+
|
|
22
|
+
## The shveik.dev agent products
|
|
23
|
+
|
|
24
|
+
Pay-per-call services for AI agents, paid in stablecoins with x402 or MPP, no accounts or API keys. Built by [shveik.dev](https://shveik.dev).
|
|
25
|
+
|
|
26
|
+
- [agdata](https://agdata.shveik.dev): fresh web data (any URL as Markdown, search, maps, reviews, jobs, social)
|
|
27
|
+
- [agproxy](https://agproxy.shveik.dev): residential proxy packs and a raw fetch
|
|
28
|
+
- [agvps](https://agvps.shveik.dev): Linux servers by the month
|
|
29
|
+
- [agmail](https://agmail.shveik.dev): receive-only agent inboxes
|
|
30
|
+
- [agpay](https://agpay.shveik.dev): custodial hold between two agents
|
|
31
|
+
|
|
32
|
+
Docs for agents: `/agents.md` on each site. Homepage: https://shveik.dev
|
data/ag-cli.gemspec
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Gem::Specification.new do |spec|
|
|
2
|
+
spec.name = 'ag-cli'
|
|
3
|
+
spec.version = '0.1.1'
|
|
4
|
+
spec.summary = 'One small CLI for the shveik.dev agent products: agdata, agproxy, agvps, agmail, agpay'
|
|
5
|
+
spec.description = 'Zero-dependency CLI to list endpoints, show 402 quotes and send payment-signed calls to agdata, agproxy, agvps, agmail and agpay (shveik.dev agent products).'
|
|
6
|
+
spec.authors = ['shveik.dev']
|
|
7
|
+
spec.homepage = 'https://shveik.dev'
|
|
8
|
+
spec.license = 'MIT'
|
|
9
|
+
spec.required_ruby_version = '>= 2.7'
|
|
10
|
+
spec.files = ['ag-cli.gemspec', 'README.md', 'bin/ag', 'lib/ag_cli.rb']
|
|
11
|
+
spec.bindir = 'bin'
|
|
12
|
+
spec.executables = ['ag']
|
|
13
|
+
spec.require_paths = ['lib']
|
|
14
|
+
spec.metadata = {
|
|
15
|
+
'homepage_uri' => 'https://shveik.dev',
|
|
16
|
+
'documentation_uri' => 'https://shveik.dev/agents.md',
|
|
17
|
+
'shveik_dev' => 'https://shveik.dev',
|
|
18
|
+
'agdata' => 'https://agdata.shveik.dev',
|
|
19
|
+
'agproxy' => 'https://agproxy.shveik.dev',
|
|
20
|
+
'agvps' => 'https://agvps.shveik.dev',
|
|
21
|
+
'agmail' => 'https://agmail.shveik.dev',
|
|
22
|
+
'agpay' => 'https://agpay.shveik.dev'
|
|
23
|
+
}
|
|
24
|
+
end
|
data/bin/ag
ADDED
data/lib/ag_cli.rb
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# ag: one small CLI for the shveik.dev agent products (agdata, agproxy, agvps, agmail, agpay).
|
|
2
|
+
# Zero dependencies, Ruby 2.7+ stdlib only. It lists endpoints, shows quotes and sends calls you have
|
|
3
|
+
# signed elsewhere; it never holds keys and never signs a payment.
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'net/http'
|
|
6
|
+
require 'openssl'
|
|
7
|
+
require 'timeout'
|
|
8
|
+
require 'uri'
|
|
9
|
+
|
|
10
|
+
module AgCli
|
|
11
|
+
VERSION = '0.1.1'
|
|
12
|
+
|
|
13
|
+
PRODUCTS = { 'data' => 'agdata', 'proxy' => 'agproxy', 'vps' => 'agvps', 'mail' => 'agmail', 'pay' => 'agpay' }.freeze
|
|
14
|
+
DEFAULT_PRODUCT = nil # a product-named gem (agvps, agpay, ...) sets this
|
|
15
|
+
|
|
16
|
+
USAGE = <<~TEXT
|
|
17
|
+
ag <product> <command> [args]
|
|
18
|
+
|
|
19
|
+
products data proxy vps mail pay (or the full name: agdata, agproxy, ...)
|
|
20
|
+
commands endpoints list the API endpoints (JSON)
|
|
21
|
+
docs | pricing | openapi print the agent guide, the price list, the OpenAPI document
|
|
22
|
+
health liveness check
|
|
23
|
+
quote <endpoint> [json] send the call unpaid and print the 402 quote (exit 3)
|
|
24
|
+
call <endpoint> <json> --payment <signature>
|
|
25
|
+
send a call whose payment you signed elsewhere
|
|
26
|
+
(or set AG_PAYMENT); the CLI never holds keys
|
|
27
|
+
|
|
28
|
+
options --origin <url> override the service origin (or AG_ORIGIN)
|
|
29
|
+
|
|
30
|
+
example ag data endpoints
|
|
31
|
+
ag data quote unblock '{"url":"https://example.com"}'
|
|
32
|
+
exit 0 ok, 1 error, 2 usage, 3 payment required (quote printed)
|
|
33
|
+
TEXT
|
|
34
|
+
|
|
35
|
+
def self.fail_with(message, code = 1)
|
|
36
|
+
$stderr.write(message + "\n")
|
|
37
|
+
exit(code)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.present(value)
|
|
41
|
+
value.nil? || value.empty? ? nil : value
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.take_option(args, name)
|
|
45
|
+
at = args.index(name)
|
|
46
|
+
return nil if at.nil?
|
|
47
|
+
fail_with("#{name} needs a value", 2) if at + 1 >= args.length
|
|
48
|
+
value = args[at + 1]
|
|
49
|
+
args.slice!(at, 2)
|
|
50
|
+
value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.show(text)
|
|
54
|
+
$stdout.write(JSON.pretty_generate(JSON.parse(text)) + "\n")
|
|
55
|
+
rescue JSON::ParserError
|
|
56
|
+
$stdout.write(text.end_with?("\n") ? text : text + "\n")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns [status, response, text]; HTTP error statuses are returned, not raised.
|
|
60
|
+
def self.request(url, method = 'GET', body = nil, headers = {})
|
|
61
|
+
uri = URI.parse(url)
|
|
62
|
+
10.times do
|
|
63
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
64
|
+
http.use_ssl = uri.scheme == 'https'
|
|
65
|
+
http.open_timeout = 180
|
|
66
|
+
http.read_timeout = 180
|
|
67
|
+
req = method == 'POST' ? Net::HTTP::Post.new(uri.request_uri) : Net::HTTP::Get.new(uri.request_uri)
|
|
68
|
+
headers.each { |key, value| req[key] = value }
|
|
69
|
+
req.body = body unless body.nil?
|
|
70
|
+
response = http.request(req)
|
|
71
|
+
if response.is_a?(Net::HTTPRedirection) && response['location']
|
|
72
|
+
code = response.code.to_i
|
|
73
|
+
uri = URI.join(uri.to_s, response['location'])
|
|
74
|
+
if [301, 302, 303].include?(code)
|
|
75
|
+
method = 'GET'
|
|
76
|
+
body = nil
|
|
77
|
+
end
|
|
78
|
+
next
|
|
79
|
+
end
|
|
80
|
+
text = response.body.to_s.dup.force_encoding(Encoding::UTF_8).scrub
|
|
81
|
+
return [response.code.to_i, response, text]
|
|
82
|
+
end
|
|
83
|
+
fail_with('request failed: too many redirects')
|
|
84
|
+
rescue SocketError, SystemCallError, IOError, Timeout::Error, OpenSSL::SSL::SSLError, URI::Error => error
|
|
85
|
+
fail_with("request failed: #{error.message}")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.get(origin, path)
|
|
89
|
+
status, _, text = request(origin + path)
|
|
90
|
+
fail_with("HTTP #{status} #{path}\n#{text}") if status >= 400
|
|
91
|
+
show(text)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.endpoint_path(name)
|
|
95
|
+
name.start_with?('/') ? name : "/x402/#{name}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.paid_request(origin, endpoint, body, extra_headers)
|
|
99
|
+
headers = { 'content-type' => 'application/json', 'user-agent' => "ag-cli/#{VERSION}" }.merge(extra_headers)
|
|
100
|
+
request(origin + endpoint_path(endpoint), 'POST', body, headers)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.show_quote(status, response, text)
|
|
104
|
+
$stdout.write("HTTP #{status}\n")
|
|
105
|
+
required = present(response['payment-required'])
|
|
106
|
+
authenticate = present(response['www-authenticate'])
|
|
107
|
+
if required
|
|
108
|
+
show(required.unpack1('m').dup.force_encoding(Encoding::UTF_8).scrub)
|
|
109
|
+
elsif authenticate
|
|
110
|
+
$stdout.write("WWW-Authenticate: #{authenticate}\n")
|
|
111
|
+
else
|
|
112
|
+
show(text)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.main(argv = nil)
|
|
117
|
+
args = (argv || ARGV).dup
|
|
118
|
+
origin_option = present(take_option(args, '--origin'))
|
|
119
|
+
payment = present(take_option(args, '--payment')) || present(ENV['AG_PAYMENT'])
|
|
120
|
+
full_names = PRODUCTS.values
|
|
121
|
+
if DEFAULT_PRODUCT && (args.empty? || (!PRODUCTS.key?(args[0]) && !full_names.include?(args[0]) && !%w[products help -h --help].include?(args[0])))
|
|
122
|
+
args.unshift(DEFAULT_PRODUCT)
|
|
123
|
+
end
|
|
124
|
+
if args.empty? || %w[help -h --help].include?(args[0])
|
|
125
|
+
$stdout.write(USAGE)
|
|
126
|
+
return
|
|
127
|
+
end
|
|
128
|
+
if args[0] == 'products'
|
|
129
|
+
PRODUCTS.each { |short, full| $stdout.write("#{short}\t#{full}\thttps://#{full}.shveik.dev\n") }
|
|
130
|
+
return
|
|
131
|
+
end
|
|
132
|
+
product = PRODUCTS[args[0]] || (full_names.include?(args[0]) ? args[0] : nil)
|
|
133
|
+
fail_with(%(unknown product "#{args[0]}"\n\n#{USAGE}), 2) unless product
|
|
134
|
+
origin = (origin_option || present(ENV['AG_ORIGIN']) || "https://#{product}.shveik.dev").sub(%r{/+\z}, '')
|
|
135
|
+
command = args.length > 1 ? args[1] : ''
|
|
136
|
+
rest = args.drop(2)
|
|
137
|
+
case command
|
|
138
|
+
when 'endpoints'
|
|
139
|
+
get(origin, '/v1')
|
|
140
|
+
when 'docs'
|
|
141
|
+
get(origin, '/agents.md')
|
|
142
|
+
when 'pricing'
|
|
143
|
+
get(origin, '/pricing.md')
|
|
144
|
+
when 'openapi'
|
|
145
|
+
get(origin, '/openapi.json')
|
|
146
|
+
when 'health'
|
|
147
|
+
get(origin, '/health')
|
|
148
|
+
when 'quote'
|
|
149
|
+
fail_with('quote needs an endpoint name or path', 2) if rest.empty?
|
|
150
|
+
status, response, text = paid_request(origin, rest[0], rest.length > 1 ? rest[1] : '{}', {})
|
|
151
|
+
show_quote(status, response, text)
|
|
152
|
+
exit(status == 402 ? 3 : status < 400 ? 0 : 1)
|
|
153
|
+
when 'call'
|
|
154
|
+
fail_with('call needs an endpoint and a JSON body', 2) if rest.length < 2
|
|
155
|
+
fail_with('a signed payment is required: --payment <signature> or AG_PAYMENT (sign the quote with your own wallet)', 2) unless payment
|
|
156
|
+
status, response, text = paid_request(origin, rest[0], rest[1], { 'PAYMENT-SIGNATURE' => payment })
|
|
157
|
+
if status == 402
|
|
158
|
+
show_quote(status, response, text)
|
|
159
|
+
exit(3)
|
|
160
|
+
end
|
|
161
|
+
show(text)
|
|
162
|
+
exit(status < 400 ? 0 : 1)
|
|
163
|
+
else
|
|
164
|
+
fail_with(%(unknown command "#{command}"\n\n#{USAGE}), 2)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
AgCli.main if __FILE__ == $PROGRAM_NAME
|
metadata
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ag-cli
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- shveik.dev
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-08 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Zero-dependency CLI to list endpoints, show 402 quotes and send payment-signed
|
|
14
|
+
calls to agdata, agproxy, agvps, agmail and agpay (shveik.dev agent products).
|
|
15
|
+
email:
|
|
16
|
+
executables:
|
|
17
|
+
- ag
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- README.md
|
|
22
|
+
- ag-cli.gemspec
|
|
23
|
+
- bin/ag
|
|
24
|
+
- lib/ag_cli.rb
|
|
25
|
+
homepage: https://shveik.dev
|
|
26
|
+
licenses:
|
|
27
|
+
- MIT
|
|
28
|
+
metadata:
|
|
29
|
+
homepage_uri: https://shveik.dev
|
|
30
|
+
documentation_uri: https://shveik.dev/agents.md
|
|
31
|
+
shveik_dev: https://shveik.dev
|
|
32
|
+
agdata: https://agdata.shveik.dev
|
|
33
|
+
agproxy: https://agproxy.shveik.dev
|
|
34
|
+
agvps: https://agvps.shveik.dev
|
|
35
|
+
agmail: https://agmail.shveik.dev
|
|
36
|
+
agpay: https://agpay.shveik.dev
|
|
37
|
+
post_install_message:
|
|
38
|
+
rdoc_options: []
|
|
39
|
+
require_paths:
|
|
40
|
+
- lib
|
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2.7'
|
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '0'
|
|
51
|
+
requirements: []
|
|
52
|
+
rubygems_version: 3.5.22
|
|
53
|
+
signing_key:
|
|
54
|
+
specification_version: 4
|
|
55
|
+
summary: 'One small CLI for the shveik.dev agent products: agdata, agproxy, agvps,
|
|
56
|
+
agmail, agpay'
|
|
57
|
+
test_files: []
|