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.

Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +32 -0
  3. data/ag-cli.gemspec +24 -0
  4. data/bin/ag +4 -0
  5. data/lib/ag_cli.rb +169 -0
  6. 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
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'ag_cli'
3
+
4
+ AgCli.main(ARGV)
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: []