kward 0.78.0 → 0.80.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 +4 -4
- data/.github/workflows/ci.yml +13 -2
- data/.github/workflows/pages.yml +1 -1
- data/CHANGELOG.md +82 -1
- data/Gemfile.lock +2 -2
- data/README.md +33 -38
- data/doc/agent-tools.md +10 -11
- data/doc/api.md +3 -3
- data/doc/authentication.md +79 -110
- data/doc/code-search.md +1 -1
- data/doc/composer.md +1 -1
- data/doc/configuration.md +136 -27
- data/doc/context-budgeting.md +6 -6
- data/doc/context-tools.md +2 -2
- data/doc/editor.md +7 -7
- data/doc/extensibility.md +9 -29
- data/doc/files.md +6 -6
- data/doc/getting-started.md +3 -3
- data/doc/git.md +8 -2
- data/doc/lifecycle-hooks.md +5 -4
- data/doc/local-models.md +130 -0
- data/doc/mcp.md +5 -5
- data/doc/permissions.md +15 -5
- data/doc/plugins.md +39 -4
- data/doc/prompt-templates.md +105 -0
- data/doc/providers.md +140 -0
- data/doc/releasing.md +11 -3
- data/doc/rpc.md +65 -31
- data/doc/sandboxing.md +142 -0
- data/doc/security.md +31 -10
- data/doc/session-management.md +2 -4
- data/doc/skills.md +10 -0
- data/doc/tabs.md +37 -0
- data/doc/telegram.md +138 -0
- data/doc/transports.md +209 -0
- data/doc/usage.md +11 -3
- data/doc/web-search.md +3 -3
- data/doc/workspace-tools.md +4 -4
- data/examples/plugins/stardate_footer.rb +10 -0
- data/examples/plugins/telegram/plugin.rb +39 -0
- data/examples/plugins/telegram/telegram_api.rb +136 -0
- data/examples/plugins/telegram/telegram_transport.rb +304 -0
- data/kward.gemspec +1 -1
- data/lib/kward/ansi.rb +1 -0
- data/lib/kward/auth/api_key_store.rb +99 -0
- data/lib/kward/cli/auth_commands.rb +72 -13
- data/lib/kward/cli/commands.rb +7 -0
- data/lib/kward/cli/doctor.rb +21 -0
- data/lib/kward/cli/git.rb +31 -8
- data/lib/kward/cli/plugins.rb +1 -0
- data/lib/kward/cli/prompt_interface.rb +7 -0
- data/lib/kward/cli/rendering.rb +6 -2
- data/lib/kward/cli/runtime_helpers.rb +4 -2
- data/lib/kward/cli/sessions.rb +1 -1
- data/lib/kward/cli/settings.rb +162 -36
- data/lib/kward/cli/slash_commands.rb +109 -1
- data/lib/kward/cli/tabs.rb +102 -37
- data/lib/kward/cli/transports.rb +67 -0
- data/lib/kward/cli/worktrees.rb +360 -0
- data/lib/kward/cli.rb +22 -1
- data/lib/kward/config_files.rb +106 -4
- data/lib/kward/conversation.rb +27 -8
- data/lib/kward/git_worktree_manager.rb +291 -0
- data/lib/kward/local_command_runner.rb +2 -2
- data/lib/kward/model/azure_openai_config.rb +54 -0
- data/lib/kward/model/catalog.rb +113 -0
- data/lib/kward/model/client.rb +364 -23
- data/lib/kward/model/model_info.rb +60 -1
- data/lib/kward/model/payloads.rb +64 -4
- data/lib/kward/model/provider_catalog.rb +114 -0
- data/lib/kward/model/sources.rb +70 -0
- data/lib/kward/model/stream_parser.rb +141 -39
- data/lib/kward/pan/index.html.erb +50 -0
- data/lib/kward/pan/server.rb +49 -2
- data/lib/kward/permissions/policy.rb +18 -4
- data/lib/kward/plugin_chat_runtime.rb +374 -0
- data/lib/kward/plugin_registry.rb +81 -8
- data/lib/kward/private_file.rb +9 -3
- data/lib/kward/prompt_interface/approval_prompt.rb +2 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +1 -5
- data/lib/kward/prompt_interface/editor/auto_indent.rb +46 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +18 -4
- data/lib/kward/prompt_interface/editor/controller.rb +43 -10
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
- data/lib/kward/prompt_interface/editor/renderer.rb +8 -8
- data/lib/kward/prompt_interface/editor/state.rb +3 -9
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +184 -9
- data/lib/kward/prompt_interface/file_overlay.rb +2 -2
- data/lib/kward/prompt_interface/git_prompt.rb +7 -3
- data/lib/kward/prompt_interface/interactive/controller.rb +4 -4
- data/lib/kward/prompt_interface/interactive/renderer.rb +4 -1
- data/lib/kward/prompt_interface/interactive/state.rb +2 -2
- data/lib/kward/prompt_interface/project_browser.rb +25 -6
- data/lib/kward/prompt_interface/runtime_state.rb +22 -6
- data/lib/kward/prompt_interface/screen.rb +3 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -1
- data/lib/kward/prompt_interface/transcript_buffer.rb +24 -2
- data/lib/kward/prompt_interface.rb +71 -12
- data/lib/kward/prompts/commands.rb +3 -1
- data/lib/kward/prompts.rb +22 -10
- data/lib/kward/rpc/auth_manager.rb +112 -152
- data/lib/kward/rpc/config_manager.rb +46 -6
- data/lib/kward/rpc/plugin_chat_manager.rb +56 -194
- data/lib/kward/rpc/prompt_bridge.rb +8 -4
- data/lib/kward/rpc/redactor.rb +1 -1
- data/lib/kward/rpc/server.rb +102 -14
- data/lib/kward/rpc/session_manager.rb +151 -38
- data/lib/kward/sandbox/capabilities.rb +39 -0
- data/lib/kward/sandbox/command_runner.rb +28 -0
- data/lib/kward/sandbox/environment.rb +24 -0
- data/lib/kward/sandbox/linux_bubblewrap_runner.rb +71 -0
- data/lib/kward/sandbox/macos_seatbelt_runner.rb +96 -0
- data/lib/kward/sandbox/passthrough_runner.rb +13 -0
- data/lib/kward/sandbox/policy.rb +74 -0
- data/lib/kward/sandbox/runner_factory.rb +55 -0
- data/lib/kward/sandbox/unavailable_runner.rb +21 -0
- data/lib/kward/sandbox.rb +9 -0
- data/lib/kward/session_store.rb +110 -24
- data/lib/kward/skills/capture.rb +144 -0
- data/lib/kward/tab_driver.rb +6 -3
- data/lib/kward/tools/git_commit.rb +39 -0
- data/lib/kward/tools/registry.rb +26 -6
- data/lib/kward/tools/tool_call.rb +1 -0
- data/lib/kward/transport/gateway.rb +253 -0
- data/lib/kward/transport/host.rb +343 -0
- data/lib/kward/transport/manager.rb +179 -0
- data/lib/kward/transport/plugin_chat_gateway.rb +186 -0
- data/lib/kward/transport/runtime.rb +41 -0
- data/lib/kward/transport/store.rb +101 -0
- data/lib/kward/transport.rb +204 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workspace.rb +18 -3
- data/lib/kward/workspace_factory.rb +34 -0
- data/templates/default/fulldoc/html/js/kward.js +3 -0
- data/templates/default/kward_navigation.rb +8 -1
- data/templates/default/layout/html/setup.rb +5 -0
- metadata +41 -2
|
@@ -4,6 +4,7 @@ require_relative "../auth/anthropic_oauth"
|
|
|
4
4
|
require_relative "../auth/github_oauth"
|
|
5
5
|
require_relative "../auth/openai_oauth"
|
|
6
6
|
require_relative "../model/client"
|
|
7
|
+
require_relative "../model/provider_catalog"
|
|
7
8
|
require_relative "config_manager"
|
|
8
9
|
|
|
9
10
|
# Namespace for the Kward CLI agent runtime.
|
|
@@ -25,45 +26,44 @@ module Kward
|
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def status
|
|
28
|
-
|
|
29
|
-
config = stored_config
|
|
29
|
+
provider_status = providers.fetch(:providers)
|
|
30
30
|
{
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
openrouterApiKey:
|
|
31
|
+
providers: provider_status,
|
|
32
|
+
openaiOAuth: oauth_status("openai")[:configured],
|
|
33
|
+
openrouterApiKey: @config_manager.api_key_status("openrouter")[:configured],
|
|
34
34
|
openaiAccessToken: !ENV["OPENAI_ACCESS_TOKEN"].to_s.empty?,
|
|
35
|
-
anthropicOAuth:
|
|
36
|
-
githubOAuth:
|
|
35
|
+
anthropicOAuth: oauth_status("anthropic")[:configured],
|
|
36
|
+
githubOAuth: oauth_status("copilot")[:configured]
|
|
37
37
|
}
|
|
38
38
|
rescue StandardError => e
|
|
39
|
-
{ openaiOAuth: false, error: e.message }
|
|
39
|
+
{ providers: [], openaiOAuth: false, error: e.message }
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def providers
|
|
43
|
-
{ providers:
|
|
43
|
+
{ providers: ProviderCatalog.all.map { |provider| provider_payload(provider) } }
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def login_with_api_key(provider_id:, api_key:)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
def login_with_api_key(provider_id:, api_key:, configuration: nil)
|
|
47
|
+
raise ArgumentError, "API key must be a non-empty string" if api_key.to_s.strip.empty?
|
|
48
|
+
|
|
49
|
+
provider = ProviderCatalog.fetch(provider_id)
|
|
50
|
+
@config_manager.configure_azure_openai(configuration || {}) if provider.id == "azure_openai"
|
|
51
|
+
@config_manager.set_api_key(provider.id, api_key)
|
|
52
|
+
{ providerId: provider.id, authMethod: "api_key", configured: true, message: "Saved API key for #{provider.name}." }
|
|
50
53
|
end
|
|
51
54
|
|
|
52
|
-
def logout_provider(provider_id:)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
when "openrouter"
|
|
62
|
-
@config_manager.delete_key("openrouter_api_key")
|
|
63
|
-
{ providerId: provider_id, message: "Logged out of OpenRouter." }
|
|
64
|
-
else
|
|
65
|
-
raise "Unsupported auth provider: #{provider_id}"
|
|
55
|
+
def logout_provider(provider_id:, auth_method: nil)
|
|
56
|
+
provider = ProviderCatalog.fetch(provider_id)
|
|
57
|
+
auth_method = auth_method.to_s unless auth_method.nil?
|
|
58
|
+
removed = false
|
|
59
|
+
if provider.api_key? && (auth_method.nil? || auth_method == "api_key")
|
|
60
|
+
removed = @config_manager.delete_api_key(provider.id) || removed
|
|
61
|
+
end
|
|
62
|
+
if provider.oauth? && (auth_method.nil? || auth_method == "oauth")
|
|
63
|
+
removed = logout_oauth_provider(provider.id) || removed
|
|
66
64
|
end
|
|
65
|
+
|
|
66
|
+
{ providerId: provider.id, authMethod: auth_method, removed: removed, message: "Logged out of #{provider.name}." }.compact
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def login_with_oauth(provider_id:, timeout_seconds: 120)
|
|
@@ -73,8 +73,12 @@ module Kward
|
|
|
73
73
|
start_oauth_login(provider_id: "openai", oauth: @oauth_factory.call, timeout_seconds: timeout_seconds)
|
|
74
74
|
when "anthropic"
|
|
75
75
|
start_oauth_login(provider_id: "anthropic", oauth: @anthropic_oauth_factory.call, timeout_seconds: timeout_seconds)
|
|
76
|
-
when "github"
|
|
77
|
-
raise "GitHub OAuth is
|
|
76
|
+
when "github", "copilot"
|
|
77
|
+
raise "GitHub Copilot OAuth is unavailable over RPC; use the interactive CLI login."
|
|
78
|
+
when "openrouter"
|
|
79
|
+
raise "OpenRouter OAuth is unavailable because Kward has not implemented OpenRouter's official PKCE flow."
|
|
80
|
+
when "xai"
|
|
81
|
+
raise "xAI OAuth is unavailable because no official stable third-party flow is supported."
|
|
78
82
|
else
|
|
79
83
|
raise "Unsupported OAuth provider: #{provider_id}"
|
|
80
84
|
end
|
|
@@ -121,144 +125,100 @@ module Kward
|
|
|
121
125
|
|
|
122
126
|
private
|
|
123
127
|
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
id: "openai",
|
|
140
|
-
name: "OpenAI",
|
|
141
|
-
authType: "oauth",
|
|
142
|
-
configured: env_configured || stored_configured,
|
|
143
|
-
storedCredentialType: "oauth",
|
|
144
|
-
canLogout: stored_configured,
|
|
145
|
-
usesCallbackServer: true
|
|
146
|
-
}
|
|
147
|
-
provider[:source] = env_configured ? "environment" : "stored" if provider[:configured]
|
|
148
|
-
provider[:label] = provider[:configured] ? "Signed in" : "Not signed in"
|
|
149
|
-
provider
|
|
150
|
-
rescue StandardError
|
|
128
|
+
def provider_payload(provider)
|
|
129
|
+
methods = []
|
|
130
|
+
if provider.api_key?
|
|
131
|
+
status = @config_manager.api_key_status(provider.id)
|
|
132
|
+
methods << {
|
|
133
|
+
id: "api_key",
|
|
134
|
+
supported: true,
|
|
135
|
+
configured: status[:configured],
|
|
136
|
+
source: status[:source],
|
|
137
|
+
canLogout: status[:canLogout]
|
|
138
|
+
}.compact
|
|
139
|
+
end
|
|
140
|
+
methods << oauth_method_payload(provider) if provider.oauth?
|
|
141
|
+
configured_methods = methods.select { |method| method[:configured] }
|
|
142
|
+
preferred = methods.find { |method| method[:id] == "oauth" } || methods.first
|
|
151
143
|
{
|
|
152
|
-
id:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
canLogout:
|
|
160
|
-
usesCallbackServer:
|
|
144
|
+
id: provider.id,
|
|
145
|
+
runtimeId: ProviderCatalog.runtime(provider.id).id,
|
|
146
|
+
name: provider.name,
|
|
147
|
+
authType: preferred&.fetch(:id, nil),
|
|
148
|
+
authMethods: methods,
|
|
149
|
+
configured: !configured_methods.empty?,
|
|
150
|
+
source: configured_methods.find { |method| method[:source] == "environment" }&.fetch(:source, nil) || configured_methods.first&.fetch(:source, nil),
|
|
151
|
+
canLogout: configured_methods.any? { |method| method[:canLogout] },
|
|
152
|
+
usesCallbackServer: methods.any? { |method| method[:usesCallbackServer] },
|
|
153
|
+
label: configured_methods.empty? ? "Not configured" : "Configured"
|
|
161
154
|
}.compact
|
|
162
155
|
end
|
|
163
156
|
|
|
164
|
-
def
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
provider[:label] = provider[:configured] ? "Signed in" : "Not signed in"
|
|
178
|
-
provider
|
|
179
|
-
rescue StandardError
|
|
157
|
+
def oauth_method_payload(provider)
|
|
158
|
+
status = oauth_status(provider.id)
|
|
159
|
+
supported = ["openai", "anthropic"].include?(provider.id)
|
|
160
|
+
reason = unless supported
|
|
161
|
+
case provider.id
|
|
162
|
+
when "openrouter"
|
|
163
|
+
"Official PKCE is documented, but Kward has not implemented it."
|
|
164
|
+
when "xai"
|
|
165
|
+
"No official stable third-party OAuth flow is available."
|
|
166
|
+
else
|
|
167
|
+
"OAuth login is available only in the interactive CLI."
|
|
168
|
+
end
|
|
169
|
+
end
|
|
180
170
|
{
|
|
181
|
-
id: "
|
|
182
|
-
name:
|
|
183
|
-
|
|
184
|
-
configured:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def openrouter_provider
|
|
193
|
-
config = stored_config
|
|
194
|
-
env_configured = !ENV["OPENROUTER_API_KEY"].to_s.empty?
|
|
195
|
-
stored_configured = !config["openrouter_api_key"].to_s.empty?
|
|
196
|
-
provider = {
|
|
197
|
-
id: "openrouter",
|
|
198
|
-
name: "OpenRouter",
|
|
199
|
-
authType: "api_key",
|
|
200
|
-
configured: env_configured || stored_configured,
|
|
201
|
-
canLogout: stored_configured
|
|
202
|
-
}
|
|
203
|
-
provider[:source] = env_configured ? "environment" : "stored" if provider[:configured]
|
|
204
|
-
provider[:storedCredentialType] = "api_key" if stored_configured
|
|
205
|
-
provider[:label] = provider[:configured] ? "API key configured" : "API key not configured"
|
|
206
|
-
provider
|
|
171
|
+
id: "oauth",
|
|
172
|
+
name: provider.oauth_name,
|
|
173
|
+
supported: supported,
|
|
174
|
+
configured: status[:configured],
|
|
175
|
+
source: status[:source],
|
|
176
|
+
canLogout: status[:canLogout],
|
|
177
|
+
usesCallbackServer: supported,
|
|
178
|
+
reason: reason
|
|
179
|
+
}.compact
|
|
207
180
|
end
|
|
208
181
|
|
|
209
|
-
def
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
usesCallbackServer: false,
|
|
221
|
-
supported: false,
|
|
222
|
-
reason: "GitHub OAuth is available in the CLI for Copilot scaffolding; RPC login is not implemented yet."
|
|
223
|
-
}
|
|
224
|
-
provider[:source] = env_configured ? "environment" : "stored" if provider[:configured]
|
|
225
|
-
provider[:label] = provider[:configured] ? "Signed in" : "Not signed in"
|
|
226
|
-
provider
|
|
182
|
+
def oauth_status(provider_id)
|
|
183
|
+
case provider_id
|
|
184
|
+
when "openai"
|
|
185
|
+
oauth_credential_status(@oauth_factory.call, env_name: "OPENAI_ACCESS_TOKEN")
|
|
186
|
+
when "anthropic"
|
|
187
|
+
oauth_credential_status(@anthropic_oauth_factory.call)
|
|
188
|
+
when "copilot"
|
|
189
|
+
oauth_credential_status(@github_oauth_factory.call, env_name: "COPILOT_GITHUB_TOKEN", can_logout: false)
|
|
190
|
+
else
|
|
191
|
+
{ configured: false, canLogout: false }
|
|
192
|
+
end
|
|
227
193
|
rescue StandardError
|
|
228
|
-
{
|
|
229
|
-
id: "github",
|
|
230
|
-
name: "GitHub",
|
|
231
|
-
authType: "oauth",
|
|
232
|
-
configured: false,
|
|
233
|
-
label: "Not signed in",
|
|
234
|
-
storedCredentialType: "oauth",
|
|
235
|
-
canLogout: false,
|
|
236
|
-
usesCallbackServer: false,
|
|
237
|
-
supported: false,
|
|
238
|
-
reason: "GitHub OAuth status unavailable."
|
|
239
|
-
}
|
|
194
|
+
{ configured: false, canLogout: false }
|
|
240
195
|
end
|
|
241
196
|
|
|
242
|
-
def
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
197
|
+
def oauth_credential_status(oauth, env_name: nil, can_logout: true)
|
|
198
|
+
environment = env_name && !ENV[env_name].to_s.empty?
|
|
199
|
+
stored = oauth.logged_in?
|
|
200
|
+
{
|
|
201
|
+
configured: environment || stored,
|
|
202
|
+
source: environment ? "environment" : (stored ? "stored" : nil),
|
|
203
|
+
canLogout: can_logout && stored
|
|
204
|
+
}.compact
|
|
250
205
|
end
|
|
251
206
|
|
|
252
|
-
def
|
|
253
|
-
oauth =
|
|
207
|
+
def logout_oauth_provider(provider_id)
|
|
208
|
+
oauth = case provider_id
|
|
209
|
+
when "openai" then @oauth_factory.call
|
|
210
|
+
when "anthropic" then @anthropic_oauth_factory.call
|
|
211
|
+
else return false
|
|
212
|
+
end
|
|
254
213
|
path = oauth.auth_path if oauth.respond_to?(:auth_path)
|
|
255
|
-
|
|
214
|
+
return false unless path && File.exist?(path)
|
|
215
|
+
|
|
216
|
+
File.delete(path)
|
|
217
|
+
true
|
|
256
218
|
end
|
|
257
219
|
|
|
258
|
-
def
|
|
259
|
-
|
|
260
|
-
path = oauth.auth_path if oauth.respond_to?(:auth_path)
|
|
261
|
-
File.delete(path) if path && File.exist?(path)
|
|
220
|
+
def fetch_login(login_id)
|
|
221
|
+
@mutex.synchronize { @logins[login_id.to_s] } || raise("Unknown login: #{login_id}")
|
|
262
222
|
end
|
|
263
223
|
|
|
264
224
|
def wait_for_callback(login, timeout_seconds:)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
require_relative "../auth/openai_oauth"
|
|
2
|
+
require_relative "../auth/api_key_store"
|
|
2
3
|
require_relative "../config_files"
|
|
3
4
|
require_relative "../model/model_info"
|
|
5
|
+
require_relative "../model/provider_catalog"
|
|
6
|
+
require_relative "../model/azure_openai_config"
|
|
4
7
|
require_relative "redactor"
|
|
5
8
|
|
|
6
9
|
# Namespace for the Kward CLI agent runtime.
|
|
@@ -9,8 +12,9 @@ module Kward
|
|
|
9
12
|
module RPC
|
|
10
13
|
# RPC configuration manager for reading and updating user config.
|
|
11
14
|
class ConfigManager
|
|
12
|
-
def initialize(config_path: OpenAIOAuth.default_config_path)
|
|
15
|
+
def initialize(config_path: OpenAIOAuth.default_config_path, api_key_store: nil)
|
|
13
16
|
@config_path = File.expand_path(config_path)
|
|
17
|
+
@api_key_store = api_key_store || APIKeyStore.new(path: File.join(File.dirname(@config_path), "api_keys.json"), config_path: @config_path)
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
attr_reader :config_path
|
|
@@ -21,6 +25,7 @@ module Kward
|
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
def update(values)
|
|
28
|
+
reject_provider_api_keys!(values)
|
|
24
29
|
Redactor.redact(ConfigFiles.update_config(values, @config_path))
|
|
25
30
|
end
|
|
26
31
|
|
|
@@ -39,12 +44,35 @@ module Kward
|
|
|
39
44
|
end
|
|
40
45
|
|
|
41
46
|
def set_api_key(provider_id, api_key)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
raise "API key must be a non-empty string" if api_key.empty?
|
|
45
|
-
raise "Unsupported API key provider: #{provider_id}" unless provider_id == "openrouter"
|
|
47
|
+
provider = ProviderCatalog.fetch(provider_id)
|
|
48
|
+
raise "#{provider.name} does not accept an API key" unless provider.api_key?
|
|
46
49
|
|
|
47
|
-
|
|
50
|
+
@api_key_store.store(provider.id, api_key)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def api_key_status(provider_id)
|
|
54
|
+
provider = ProviderCatalog.fetch(provider_id)
|
|
55
|
+
configured = @api_key_store.configured?(provider.id)
|
|
56
|
+
stored = @api_key_store.stored?(provider.id)
|
|
57
|
+
{
|
|
58
|
+
configured: configured,
|
|
59
|
+
stored: stored,
|
|
60
|
+
source: configured ? (environment_api_key?(provider) ? "environment" : "stored") : nil,
|
|
61
|
+
canLogout: stored
|
|
62
|
+
}.compact
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def delete_api_key(provider_id)
|
|
66
|
+
@api_key_store.delete(provider_id)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def configure_azure_openai(values)
|
|
70
|
+
setup = AzureOpenAIConfig.new(
|
|
71
|
+
endpoint: values["endpoint"] || values[:endpoint],
|
|
72
|
+
deployment: values["deployment"] || values[:deployment],
|
|
73
|
+
api_version: values["apiVersion"] || values[:apiVersion] || values["api_version"] || values[:api_version]
|
|
74
|
+
)
|
|
75
|
+
update(setup.to_config)
|
|
48
76
|
end
|
|
49
77
|
|
|
50
78
|
def delete_key(key)
|
|
@@ -61,6 +89,18 @@ module Kward
|
|
|
61
89
|
|
|
62
90
|
private
|
|
63
91
|
|
|
92
|
+
def reject_provider_api_keys!(values)
|
|
93
|
+
keys = values.respond_to?(:keys) ? values.keys.map(&:to_s) : []
|
|
94
|
+
private_keys = ProviderCatalog.api_key_providers.map { |provider| "#{provider.id}_api_key" }
|
|
95
|
+
return if (keys & private_keys).empty?
|
|
96
|
+
|
|
97
|
+
raise ArgumentError, "Provider API keys must be saved with auth/loginWithApiKey"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def environment_api_key?(provider)
|
|
101
|
+
provider.api_key_env.any? { |name| !ENV[name].to_s.strip.empty? }
|
|
102
|
+
end
|
|
103
|
+
|
|
64
104
|
def load_config
|
|
65
105
|
ConfigFiles.read_config(@config_path)
|
|
66
106
|
end
|