shai-cli 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f13b2f27c4542d4f5fe83f06e781589bd69bde8bd618e7b8ce494fd1d4ab621b
4
+ data.tar.gz: 94129b59f660fcff737602a475b891d87526968bd3ee4b28d495012a225d6cd5
5
+ SHA512:
6
+ metadata.gz: 4d89e81bdb2d09f584d53fb9c652e61d38ec7e40378e2ccca88b12b75bed765dd0aa18692fd0599a17095ec331f0e5aabfdcabea1da5c89df9ad927cacb396dc
7
+ data.tar.gz: e636e3d6f766962c11df07de32e70fd62944e2e09c4a0e2acbb723291b75e5425e2b922a98e55e110bb12c3fd277d27993c9e9eda912713065f6897086ed5248
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 InfinitLab
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.
data/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # shai-cli
2
+
3
+ Command-line tool for managing and sharing AI agent configurations via [shaicli.dev](https://shaicli.dev).
4
+
5
+ ## Installation
6
+
7
+ ### Quick install (recommended)
8
+
9
+ ```bash
10
+ curl -fsSL https://shaicli.dev/install.sh | bash
11
+ ```
12
+
13
+ ### Via RubyGems
14
+
15
+ ```bash
16
+ gem install shai-cli
17
+ ```
18
+
19
+ ### From source
20
+
21
+ ```bash
22
+ git clone https://github.com/infinitlab/shai-cli.git
23
+ cd shai-cli
24
+ bundle install
25
+ bundle exec rake install
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ # Login to your shaicli.dev account
32
+ shai login
33
+
34
+ # Search for public configurations
35
+ shai search "claude code"
36
+
37
+ # Install a configuration to your project
38
+ shai install anthropic/claude-expert
39
+
40
+ # Create and share your own configuration
41
+ shai init
42
+ shai push
43
+ ```
44
+
45
+ ## Commands
46
+
47
+ ### Authentication
48
+
49
+ | Command | Description |
50
+ |---------|-------------|
51
+ | `shai login` | Log in to shaicli.dev |
52
+ | `shai logout` | Log out and remove stored credentials |
53
+ | `shai whoami` | Show current authentication status |
54
+
55
+ ### Discovery
56
+
57
+ | Command | Description |
58
+ |---------|-------------|
59
+ | `shai list` | List your configurations |
60
+ | `shai search <query>` | Search public configurations |
61
+
62
+ ### Using Configurations
63
+
64
+ | Command | Description |
65
+ |---------|-------------|
66
+ | `shai install <config>` | Install a configuration to local project |
67
+ | `shai uninstall <config>` | Remove an installed configuration |
68
+
69
+ ### Authoring Configurations
70
+
71
+ | Command | Description |
72
+ |---------|-------------|
73
+ | `shai init` | Initialize a new configuration |
74
+ | `shai push` | Push local changes to remote |
75
+ | `shai status` | Show local changes vs remote |
76
+ | `shai diff` | Show diff between local and remote |
77
+ | `shai config show` | Show configuration details |
78
+ | `shai config set <key> <value>` | Update configuration metadata |
79
+ | `shai delete <slug>` | Delete a configuration from remote |
80
+
81
+ ## Configuration
82
+
83
+ ### Environment Variables
84
+
85
+ | Variable | Description | Default |
86
+ |----------|-------------|---------|
87
+ | `SHAI_API_URL` | API endpoint URL | `https://shaicli.dev` |
88
+ | `SHAI_CONFIG_DIR` | Directory for credentials | `~/.config/shai` |
89
+ | `SHAI_TOKEN` | Override authentication token | - |
90
+ | `NO_COLOR` | Disable colored output | - |
91
+
92
+ ### .shairc File
93
+
94
+ When authoring configurations, a `.shairc` file is created in your project root:
95
+
96
+ ```yaml
97
+ # .shairc - Shai configuration
98
+ slug: my-config
99
+ include:
100
+ - .claude/**
101
+ - .cursor/**
102
+ exclude:
103
+ - "**/*.local.*"
104
+ - "**/.env"
105
+ ```
106
+
107
+ | Field | Description |
108
+ |-------|-------------|
109
+ | `slug` | Unique identifier for your configuration |
110
+ | `include` | Glob patterns for files to include |
111
+ | `exclude` | Glob patterns for files to exclude |
112
+
113
+ ## Examples
114
+
115
+ ### Search by tags
116
+
117
+ ```bash
118
+ shai search --tag claude --tag coding
119
+ ```
120
+
121
+ ### Install to specific directory
122
+
123
+ ```bash
124
+ shai install anthropic/claude-expert --path ./my-project
125
+ ```
126
+
127
+ ### Preview installation without making changes
128
+
129
+ ```bash
130
+ shai install anthropic/claude-expert --dry-run
131
+ ```
132
+
133
+ ### Force overwrite existing files
134
+
135
+ ```bash
136
+ shai install anthropic/claude-expert --force
137
+ ```
138
+
139
+ ### Create a public configuration
140
+
141
+ ```bash
142
+ shai init
143
+ # Follow prompts, select "public" visibility
144
+ shai push
145
+ ```
146
+
147
+ ### Update configuration metadata
148
+
149
+ ```bash
150
+ shai config set name "My Updated Config"
151
+ shai config set visibility public
152
+ shai config set description "A better description"
153
+ ```
154
+
155
+ ## Development
156
+
157
+ ### Setup
158
+
159
+ ```bash
160
+ bundle install
161
+ ```
162
+
163
+ ### Run tests
164
+
165
+ ```bash
166
+ bundle exec rspec
167
+ ```
168
+
169
+ ### Run linter
170
+
171
+ ```bash
172
+ bundle exec standardrb
173
+ ```
174
+
175
+ ### Local development
176
+
177
+ Create `.env.development`:
178
+
179
+ ```bash
180
+ SHAI_API_URL=http://localhost:3001
181
+ SHAI_CONFIG_DIR=.config/shai-dev
182
+ ```
183
+
184
+ Run commands in development mode:
185
+
186
+ ```bash
187
+ SHAI_ENV=development bundle exec bin/shai <command>
188
+ ```
189
+
190
+ ## License
191
+
192
+ MIT
data/bin/shai ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Load environment variables from .env files (development only)
5
+ begin
6
+ require "dotenv"
7
+
8
+ env = ENV.fetch("SHAI_ENV", "development")
9
+ env_files = [
10
+ ".env.#{env}.local",
11
+ ".env.#{env}",
12
+ ".env.local",
13
+ ".env"
14
+ ].select { |f| File.exist?(f) }
15
+
16
+ Dotenv.load(*env_files) if env_files.any?
17
+ rescue LoadError
18
+ # dotenv not available (production gem install), skip
19
+ end
20
+
21
+ require_relative "../lib/shai"
22
+
23
+ Shai::CLI.start(ARGV)
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module Shai
8
+ class ApiClient
9
+ def initialize
10
+ @connection = build_connection
11
+ end
12
+
13
+ # Authentication
14
+ def login(identifier:, password:, client_name: nil)
15
+ client_name ||= default_client_name
16
+ post("/api/v1/cli/session", {
17
+ identifier: identifier,
18
+ password: password,
19
+ client_name: client_name
20
+ })
21
+ end
22
+
23
+ # Configurations
24
+ def list_configurations
25
+ get("/api/v1/configurations")
26
+ end
27
+
28
+ def search_configurations(query: nil, tags: [])
29
+ params = {}
30
+ params[:q] = query if query
31
+ params["tags[]"] = tags if tags.any?
32
+ get("/api/v1/configurations/search", params)
33
+ end
34
+
35
+ def get_configuration(identifier)
36
+ get("/api/v1/configurations/#{encode_identifier(identifier)}")
37
+ end
38
+
39
+ def create_configuration(name:, description: nil, visibility: "private")
40
+ post("/api/v1/configurations", {
41
+ configuration: {
42
+ name: name,
43
+ description: description,
44
+ visibility: visibility
45
+ }
46
+ })
47
+ end
48
+
49
+ def update_configuration(identifier, **attributes)
50
+ put("/api/v1/configurations/#{encode_identifier(identifier)}", {
51
+ configuration: attributes
52
+ })
53
+ end
54
+
55
+ def delete_configuration(identifier)
56
+ delete("/api/v1/configurations/#{encode_identifier(identifier)}")
57
+ end
58
+
59
+ def get_tree(identifier)
60
+ get("/api/v1/configurations/#{encode_identifier(identifier)}/tree")
61
+ end
62
+
63
+ def update_tree(identifier, tree)
64
+ put("/api/v1/configurations/#{encode_identifier(identifier)}/tree", {tree: tree})
65
+ end
66
+
67
+ # Encode identifier for URL (handles owner/slug format)
68
+ def encode_identifier(identifier)
69
+ URI.encode_www_form_component(identifier)
70
+ end
71
+
72
+ private
73
+
74
+ def build_connection
75
+ Faraday.new(url: Shai.configuration.api_url) do |conn|
76
+ conn.request :json
77
+ conn.response :json, content_type: /\bjson$/
78
+ conn.adapter Faraday.default_adapter
79
+ end
80
+ end
81
+
82
+ def get(path, params = {})
83
+ response = @connection.get(path, params) do |req|
84
+ add_auth_header(req)
85
+ end
86
+ handle_response(response)
87
+ end
88
+
89
+ def post(path, body)
90
+ response = @connection.post(path) do |req|
91
+ add_auth_header(req)
92
+ req.body = body
93
+ end
94
+ handle_response(response)
95
+ end
96
+
97
+ def put(path, body)
98
+ response = @connection.put(path) do |req|
99
+ add_auth_header(req)
100
+ req.body = body
101
+ end
102
+ handle_response(response)
103
+ end
104
+
105
+ def delete(path)
106
+ response = @connection.delete(path) do |req|
107
+ add_auth_header(req)
108
+ end
109
+ handle_response(response)
110
+ end
111
+
112
+ def add_auth_header(request)
113
+ token = Shai.configuration.token || Shai.credentials.token
114
+ request.headers["Authorization"] = "Bearer #{token}" if token
115
+ end
116
+
117
+ def handle_response(response)
118
+ case response.status
119
+ when 200..299
120
+ response.body
121
+ when 401
122
+ raise AuthenticationError, response.body&.dig("error") || "Authentication failed"
123
+ when 403
124
+ raise PermissionDeniedError, response.body&.dig("error") || "Permission denied"
125
+ when 404
126
+ raise NotFoundError, response.body&.dig("error") || "Not found"
127
+ when 422
128
+ raise InvalidConfigurationError, response.body&.dig("error") || "Invalid request"
129
+ else
130
+ raise Error, response.body&.dig("error") || "Request failed with status #{response.status}"
131
+ end
132
+ rescue Faraday::ConnectionFailed, Faraday::TimeoutError
133
+ raise NetworkError, "Could not connect to #{Shai.configuration.api_url}. Check your internet connection."
134
+ end
135
+
136
+ def default_client_name
137
+ hostname = begin
138
+ require "socket"
139
+ Socket.gethostname
140
+ rescue
141
+ "Unknown"
142
+ end
143
+ "CLI (#{hostname})"
144
+ end
145
+ end
146
+ end
data/lib/shai/cli.rb ADDED
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require_relative "commands/auth"
5
+ require_relative "commands/configurations"
6
+ require_relative "commands/sync"
7
+ require_relative "commands/config"
8
+ require_relative "ui"
9
+
10
+ module Shai
11
+ class CLI < Thor
12
+ include Commands::Auth
13
+ include Commands::Configurations
14
+ include Commands::Sync
15
+ include Commands::Config
16
+
17
+ def self.exit_on_failure?
18
+ true
19
+ end
20
+
21
+ # Custom help to group commands by category
22
+ def self.help(shell, subcommand = false)
23
+ shell.say "shai - Manage AI agent configurations"
24
+ shell.say ""
25
+ shell.say "USAGE:"
26
+ shell.say " shai <command> [options]"
27
+ shell.say ""
28
+ shell.say "AUTHENTICATION:"
29
+ shell.say " login Log in to shaicli.dev"
30
+ shell.say " logout Log out and remove stored credentials"
31
+ shell.say " whoami Show current authentication status"
32
+ shell.say ""
33
+ shell.say "DISCOVERY:"
34
+ shell.say " list List your configurations"
35
+ shell.say " search <query> Search public configurations"
36
+ shell.say ""
37
+ shell.say "USING CONFIGURATIONS (install to current project):"
38
+ shell.say " install <config> Install a configuration to local project"
39
+ shell.say " uninstall <config> Remove an installed configuration"
40
+ shell.say ""
41
+ shell.say "AUTHORING CONFIGURATIONS (create and publish):"
42
+ shell.say " init Initialize a new configuration"
43
+ shell.say " push Push local changes to remote"
44
+ shell.say " status Show local changes vs remote"
45
+ shell.say " diff Show diff between local and remote"
46
+ shell.say " config show Show configuration details"
47
+ shell.say " config set <k> <v> Update configuration metadata"
48
+ shell.say " delete <slug> Delete a configuration from remote"
49
+ shell.say ""
50
+ shell.say "OPTIONS:"
51
+ shell.say " -h, --help Show help for a command"
52
+ shell.say " -v, --version Show version"
53
+ shell.say " --verbose Enable verbose output"
54
+ shell.say " --no-color Disable colored output"
55
+ shell.say ""
56
+ shell.say "EXAMPLES:"
57
+ shell.say " shai login"
58
+ shell.say " shai search \"claude code\""
59
+ shell.say " shai install anthropic/claude-expert"
60
+ shell.say " shai init"
61
+ shell.say " shai push"
62
+ shell.say ""
63
+ shell.say "Run 'shai help <command>' for more information on a command."
64
+ end
65
+
66
+ class_option :verbose, type: :boolean, default: false, desc: "Enable verbose output"
67
+ class_option :no_color, type: :boolean, default: false, desc: "Disable colored output"
68
+
69
+ desc "version", "Show version"
70
+ map %w[-v --version] => :version
71
+ def version
72
+ puts "shai #{Shai::VERSION}"
73
+ end
74
+
75
+ private
76
+
77
+ def ui
78
+ @ui ||= UI.new(color: !options[:no_color])
79
+ end
80
+
81
+ def api
82
+ Shai.api_client
83
+ end
84
+
85
+ def credentials
86
+ Shai.credentials
87
+ end
88
+
89
+ def require_auth!
90
+ return if credentials.authenticated?
91
+
92
+ ui.error("Not logged in. Run `shai login` to authenticate.")
93
+ exit EXIT_AUTH_REQUIRED
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shai
4
+ module Commands
5
+ module Auth
6
+ def self.included(base)
7
+ base.class_eval do
8
+ desc "login", "Log in to shaicli.dev"
9
+ def login
10
+ identifier = ui.ask("Email or username:")
11
+ password = ui.mask("Password:")
12
+
13
+ ui.blank
14
+
15
+ begin
16
+ response = ui.spinner("Logging in...") do
17
+ api.login(identifier: identifier, password: password)
18
+ end
19
+
20
+ data = response["data"]
21
+ credentials.save(
22
+ token: data["token"],
23
+ expires_at: data["expires_at"],
24
+ user: data["user"]
25
+ )
26
+
27
+ ui.success("Logged in as #{data.dig("user", "username")}")
28
+ ui.indent("Token expires: #{format_date(data["expires_at"])}")
29
+ ui.indent("Token stored in #{Shai.configuration.credentials_path}")
30
+ rescue AuthenticationError
31
+ ui.error("Invalid credentials")
32
+ exit EXIT_AUTH_REQUIRED
33
+ rescue NetworkError => e
34
+ ui.error(e.message)
35
+ exit EXIT_NETWORK_ERROR
36
+ end
37
+ end
38
+
39
+ desc "logout", "Log out and remove stored credentials"
40
+ def logout
41
+ credentials.clear
42
+ ui.success("Logged out successfully")
43
+ end
44
+
45
+ desc "whoami", "Show current authentication status"
46
+ def whoami
47
+ if credentials.authenticated?
48
+ name = credentials.display_name || credentials.username
49
+ ui.info("Logged in as #{credentials.username} (#{name})")
50
+ ui.info("Token expires: #{format_date(credentials.expires_at)}")
51
+ else
52
+ ui.info("Not logged in. Run `shai login` to authenticate.")
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def format_date(date_string)
61
+ return "unknown" unless date_string
62
+
63
+ date = Time.parse(date_string)
64
+ date.strftime("%B %-d, %Y")
65
+ rescue ArgumentError
66
+ date_string
67
+ end
68
+ end
69
+ end
70
+ end