elelem 0.10.0 → 0.12.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -195
  3. data/exe/elelem +7 -1
  4. data/lib/elelem/agent.rb +68 -62
  5. data/lib/elelem/cli.rb +116 -0
  6. data/lib/elelem/commands.rb +26 -10
  7. data/lib/elelem/config.rb +17 -0
  8. data/lib/elelem/conversation.rb +1 -0
  9. data/lib/elelem/input.rb +51 -0
  10. data/lib/elelem/null_input.rb +8 -0
  11. data/lib/elelem/null_output.rb +7 -0
  12. data/lib/elelem/null_tool.rb +18 -0
  13. data/lib/elelem/output.rb +55 -0
  14. data/lib/elelem/plugins.rb +20 -27
  15. data/lib/elelem/prompts/default.erb +3 -0
  16. data/lib/elelem/registration.rb +37 -0
  17. data/lib/elelem/registry.rb +68 -0
  18. data/lib/elelem/repl.rb +34 -0
  19. data/lib/elelem/result.rb +27 -0
  20. data/lib/elelem/schema.rb +20 -0
  21. data/lib/elelem/session.rb +17 -0
  22. data/lib/elelem/slash_command.rb +18 -0
  23. data/lib/elelem/stub_provider.rb +15 -0
  24. data/lib/elelem/system_prompt.rb +12 -135
  25. data/lib/elelem/tool.rb +7 -12
  26. data/lib/elelem/toolbox.rb +40 -31
  27. data/lib/elelem/transcript.rb +26 -0
  28. data/lib/elelem/version.rb +2 -2
  29. data/lib/elelem.rb +28 -113
  30. metadata +48 -155
  31. data/CHANGELOG.md +0 -309
  32. data/lib/elelem/mcp/oauth.rb +0 -217
  33. data/lib/elelem/mcp/token_storage.rb +0 -60
  34. data/lib/elelem/mcp.rb +0 -243
  35. data/lib/elelem/net/claude.rb +0 -202
  36. data/lib/elelem/net/ollama.rb +0 -81
  37. data/lib/elelem/net/openai.rb +0 -88
  38. data/lib/elelem/net.rb +0 -13
  39. data/lib/elelem/permissions.rb +0 -45
  40. data/lib/elelem/plugins/builtins.rb +0 -96
  41. data/lib/elelem/plugins/edit.rb +0 -15
  42. data/lib/elelem/plugins/eval.rb +0 -20
  43. data/lib/elelem/plugins/execute.rb +0 -18
  44. data/lib/elelem/plugins/git.rb +0 -20
  45. data/lib/elelem/plugins/glob.rb +0 -13
  46. data/lib/elelem/plugins/grep.rb +0 -21
  47. data/lib/elelem/plugins/list.rb +0 -14
  48. data/lib/elelem/plugins/mcp.rb +0 -20
  49. data/lib/elelem/plugins/permissions.json +0 -6
  50. data/lib/elelem/plugins/read.rb +0 -21
  51. data/lib/elelem/plugins/task.rb +0 -14
  52. data/lib/elelem/plugins/tools.rb +0 -13
  53. data/lib/elelem/plugins/verify.rb +0 -47
  54. data/lib/elelem/plugins/write.rb +0 -34
  55. data/lib/elelem/plugins/zz_confirm.rb +0 -9
  56. data/lib/elelem/terminal.rb +0 -103
data/CHANGELOG.md DELETED
@@ -1,309 +0,0 @@
1
- ## [0.10.0] - 2026-01-27
2
-
3
- ### Added
4
- - **Async MCP loading** for faster startup - tools load in background thread
5
- - **HTTP MCP servers** with SSE support and session management
6
- - **OAuth authentication** for MCP servers with PKCE, automatic token refresh
7
- - **Global hooks** - `toolbox.before`/`toolbox.after` without tool name applies to all tools
8
- - **`/context` improvements**: `/context <n>` to view entry, `/context json` for full dump
9
- - **ast-grep (`sg`) support** for building repo maps - faster and more accurate than ctags
10
- - **New tools**: `glob`, `grep`, `list`, `git`, `task`, `/tools` command
11
- - **Permissions system** (`lib/elelem/permissions.rb`) for tool access control
12
- - **OpenAI reasoning mode** - enables `Reasoning: high` for o-series models
13
- - **Test coverage** for OAuth, token storage, HTTP MCP, SSE parsing, global hooks
14
-
15
- ### Changed
16
- - **BREAKING: Plugin API** - plugins now receive `agent` instead of `toolbox`
17
- - Old: `Elelem::Plugins.register(:name) { |toolbox| toolbox.add(...) }`
18
- - New: `Elelem::Plugins.register(:name) { |agent| agent.toolbox.add(...) }`
19
- - Plugins can now access `agent.terminal`, `agent.commands`, `agent.conversation`
20
- - Extracted `Conversation` class from `Agent` for better separation of concerns
21
- - Extracted `Commands` class for slash command handling
22
- - Refactored LLM fetch interface to emit separate events for thinking/content/tool_calls
23
- - Simplified system prompt with inline ERB template
24
- - Renamed confirm plugin to `zz_confirm` to ensure it loads last
25
- - MCP logs now write to `~/.elelem/mcp.log` instead of working directory
26
- - Tool schema now frozen to prevent mutation
27
- - Uses `Open3.capture2` instead of backticks for thread safety
28
- - Improved ANSI escape sequence stripping in `/shell` transcripts
29
-
30
- ## [0.9.2] - 2026-01-22
31
-
32
- ### Fixed
33
- - Suppress pathname gem constant redefinition warnings on Ruby 4.0
34
-
35
- ## [0.9.1] - 2026-01-22
36
-
37
- ### Fixed
38
- - Use `next` instead of `return` to exit block in execute plugin after-hook
39
- - Use `break` instead of `return` to exit `inject` block in verify plugin
40
-
41
- ## [0.9.0] - 2026-01-21
42
-
43
- ### Added
44
- - **Plugin system** with support for custom tool definitions
45
- - Load plugins from `lib/elelem/plugins/`, `~/.elelem/plugins/`, and `.elelem/plugins/`
46
- - `Elelem::Plugins.register(name) { |toolbox| ... }` API
47
- - Built-in plugins: `read`, `write`, `edit`, `execute`, `eval`, `verify`, `confirm`, `mcp`
48
- - **MCP (Model Context Protocol)** server support via `.mcp.json` configuration
49
- - **AGENTS.md** file support - searches up directory tree for project instructions
50
- - **`/init` command** to generate an AGENTS.md file for the current project
51
- - **`/shell` command** to drop into a shell session and capture the transcript to context
52
- - **`/reload` command** to hot-reload source code without restarting the process
53
- - **`task` tool** for delegating subtasks to focused sub-agents
54
- - **`edit` tool** for replacing first occurrence of text in a file
55
- - **`eval` tool** for executing Ruby code and dynamically registering new tools
56
- - **`verify` tool** for syntax checking and running project tests
57
- - **Pre/post tool hooks** (`toolbox.before`/`toolbox.after`) for extensibility
58
- - **Confirmation prompt** before executing shell commands (when TTY)
59
- - **Context compaction** for long conversations (summarizes old messages)
60
- - **Repo map** via ctags included in system prompt
61
- - **Markdown rendering** with [glow](https://github.com/charmbracelet/glow) for LLM responses
62
- - **CLI improvements**: optparse-based interface with `-p`/`-m` flags
63
- - `elelem chat` - Interactive REPL (default)
64
- - `elelem ask <prompt>` - One-shot query (reads stdin if piped)
65
- - `elelem files` - Output files as XML
66
- - JSON Schema validation for tool call arguments (via `json_schemer`)
67
- - Tool aliases support (e.g., `bash`, `sh`, `exec` → `execute`)
68
- - **Dependencies documentation** in README with installation links
69
-
70
- ### Changed
71
- - **Breaking**: Requires Ruby >= 4.0.0 (was 3.4.0)
72
- - **Breaking**: Removed `net-llm` dependency - LLM clients now inline in `lib/elelem/net/`
73
- - `Elelem::Net::Claude` (Anthropic and Vertex AI)
74
- - `Elelem::Net::OpenAI`
75
- - `Elelem::Net::Ollama`
76
- - **Breaking**: Simplified LLM client `fetch` contract
77
- - Yields `{content:, thinking:}` deltas
78
- - Returns `tool_calls` array directly
79
- - **Breaking**: Tool schema uses OpenAI format (`{type: "function", function: {...}}`)
80
- - **Breaking**: Tool definitions use `description:` key (was `desc:`)
81
- - **Breaking**: Removed modes and permissions system entirely
82
- - **Breaking**: Removed slash commands (`/mode`, `/env`, `/provider`, `/model`)
83
- - Remaining: `/clear`, `/context`, `/init`, `/reload`, `/shell`, `/exit`, `/help`
84
- - **Breaking**: Removed many dependencies
85
- - Removed: `thor`, `cli-ui`, `erb`, `cgi`, `set`, `timeout`, `logger`, `net-llm`, `json-schema`
86
- - Added: `json_schemer`, `optparse`, `tempfile`, `stringio`, `uri`
87
- - Consolidated multiple exe files into single `exe/elelem` entry point
88
- - Tools are now defined via plugins instead of hardcoded in Toolbox
89
- - System prompt includes hints for `rg`, `fd`, `sg` (ast-grep), `sed`, `patch`
90
- - System prompt regenerated on each fetch (includes dynamic repo map)
91
- - Default tool set: `read`, `write`, `edit`, `execute`, `eval`, `verify`, `task`
92
- - System prompt encourages using `eval` to create tools for repetitive tasks
93
-
94
- ### Removed
95
- - `lib/elelem/application.rb` - CLI now in `exe/elelem`
96
- - `lib/elelem/conversation.rb` - simplified into Agent
97
- - `lib/elelem/git_context.rb` - inlined into Agent
98
- - `lib/elelem/system_prompt.erb` - now generated in Agent
99
- - `web_fetch`, `web_search`, `fetch`, `search_engine` tools
100
- - `patch` tool (use `edit` or `execute` with `sed`/`patch`)
101
- - `grep`, `list` tools (use `execute` with `rg`, `fd`)
102
- - Modes and permissions system
103
- - Events module
104
- - GitHub Actions CI workflow
105
-
106
- ### Fixed
107
- - Handle missing args in Claude provider
108
- - Tool alias resolution (use canonical tool name, not alias)
109
- - Unknown tool error now suggests using `execute` and lists available tools
110
- - Duplicate write operations in edit flow
111
-
112
- ## [0.8.0] - 2026-01-14
113
-
114
- ### Added
115
- - `fetch` tool for HTTP GET requests (returns status and body)
116
- - `search_engine` tool for DuckDuckGo Instant Answer API searches
117
- - Tool aliases: `get`/`web` → `fetch`, `ddg`/`duckduckgo` → `search_engine`
118
- - `net-hippie` and `cgi` dependencies for HTTP requests
119
-
120
- ## [0.7.0] - 2026-01-14
121
-
122
- ### Added
123
- - ASCII spinner animation while waiting for LLM responses
124
- - `Terminal#waiting` method with automatic cleanup on next output
125
- - Decision-making principles in system prompt (prefer reversible actions, ask when uncertain)
126
- - Mode enforcement tests
127
-
128
- ### Changed
129
- - Renamed internal `mode` concept to `permissions` for clarity (read/write/execute are permissions, plan/build/verify are modes)
130
- - Refactored `Toolbox#run_tool` to accept `permissions:` parameter
131
-
132
- ### Fixed
133
- - **Security**: Mode restrictions now enforced at execution time, not just schema time
134
- - Previously, LLMs could call tools outside their mode by guessing tool names
135
- - Now `run_tool` validates the tool is allowed for the current permission set
136
-
137
- ## [0.6.0] - 2026-01-12
138
-
139
- ### Added
140
- - `/env` slash command to capture environment variables for provider connections
141
- - `/shell` slash command
142
- - `/provider` and `/model` slash commands
143
- - Tab completion for commands
144
- - Help output for `/mode` and `/env` commands
145
-
146
- ### Changed
147
- - Renamed `bash` tool to `exec`
148
- - Tuned system prompt
149
- - Changed thinking prompt to ellipsis
150
- - Removed username from system prompt
151
- - Use pessimistic constraint on net-llm dependency
152
- - Extracted Terminal class for IO abstraction (enables E2E testing)
153
-
154
- ### Fixed
155
- - Prevent infinite looping errors
156
- - Provide function schema when tool is called with invalid arguments
157
- - Tab completion for `pass` entries without requiring `show` subcommand
158
- - Password store symlink support in tab completion
159
-
160
- ## [0.5.0] - 2026-01-07
161
-
162
- ### Added
163
- - Multi-provider support: Ollama, Anthropic, OpenAI, and VertexAI
164
- - `--provider` CLI option to select LLM provider (default: ollama)
165
- - `--model` CLI option to override default model
166
- - Tool aliases (`bash` also accepts `exec`, `shell`, `command`, `terminal`, `run`)
167
- - Thinking text output for models that support extended thinking
168
-
169
- ### Changed
170
- - Requires net-llm >= 0.5.0 with unified fetch interface
171
- - Updated gem description to reflect multi-provider support
172
-
173
- ## [0.4.2] - 2025-12-01
174
-
175
- ### Changed
176
- - Renamed `exec` tool to `bash` for clarity
177
- - Improved system prompt with iterative refinements
178
- - Added environment context variables to system prompt
179
-
180
- ## [0.4.1] - 2025-11-26
181
-
182
- ### Added
183
- - `elelem files` subcommand: generates Claude‑compatible XML file listings.
184
- - Rake task `files:prompt` to output a ready‑to‑copy list of files for prompts.
185
-
186
- ### Changed
187
- - Refactor tool‑call formatting to a more compact JSON payload for better LLM parsing.
188
- - Updated CI and documentation to use GitHub instead of previous hosting.
189
- - Runtime validation of command‑line parameters against a JSON schema.
190
-
191
- ### Fixed
192
- - Minor documentation and CI workflow adjustments.
193
-
194
- ## [0.4.0] - 2025-11-10
195
-
196
- ### Added
197
- - **Eval Tool**: Meta-programming tool that allows the LLM to dynamically create and register new tools at runtime
198
- - Eval tool has access to the toolbox for enhanced capabilities
199
- - Comprehensive test coverage with RSpec
200
- - Agent specs
201
- - Conversation specs
202
- - Toolbox specs
203
-
204
- ### Changed
205
- - **Architecture Improvements**: Significant refactoring for better separation of concerns
206
- - Extracted Tool class to separate file (`lib/elelem/tool.rb`)
207
- - Extracted Toolbox class to separate file (`lib/elelem/toolbox.rb`)
208
- - Extracted Shell class for command execution
209
- - Improved tool registration through `#add_tool` method
210
- - Tool constants moved to Toolbox for better organization
211
- - Agent class simplified by delegating to Tool instances
212
-
213
- ### Fixed
214
- - `/context` command now correctly accounts for the current mode
215
-
216
- ## [0.3.0] - 2025-11-05
217
-
218
- ### Added
219
- - **Mode System**: Control agent capabilities with workflow modes
220
- - `/mode plan` - Read-only mode (grep, list, read)
221
- - `/mode build` - Read + Write mode (grep, list, read, patch, write)
222
- - `/mode verify` - Read + Execute mode (grep, list, read, execute)
223
- - `/mode auto` - All tools enabled
224
- - Each mode adapts system prompt to guide appropriate behavior
225
- - Improved output formatting
226
- - Suppressed verbose thinking/reasoning output
227
- - Clean tool call display (e.g., `date` instead of full JSON hash)
228
- - Mode switch confirmation messages
229
- - Clear command feedback
230
- - Design philosophy documentation in README
231
- - Mode system documentation
232
-
233
- ### Changed
234
- - **BREAKING**: Removed `llm-ollama` and `llm-openai` standalone executables (use main `elelem chat` command)
235
- - **BREAKING**: Simplified architecture - consolidated all logic into Agent class
236
- - Removed Configuration class
237
- - Removed Toolbox system
238
- - Removed MCP client infrastructure
239
- - Removed Tool and Tools classes
240
- - Removed TUI abstraction layer (direct puts/Reline usage)
241
- - Removed API wrapper class
242
- - Removed state machine
243
- - Improved execute tool description to guide LLM toward direct command execution
244
- - Extracted tool definitions from long inline strings to readable private methods
245
- - Updated README with clear philosophy and usage examples
246
- - Reduced total codebase from 417 to 395 lines (-5%)
247
-
248
- ### Fixed
249
- - Working directory handling for execute tool (handles empty string cwd)
250
- - REPL EOF handling (graceful exit when input stream ends)
251
- - Tool call formatting now shows clean, readable commands
252
-
253
- ### Removed
254
- - `exe/llm-ollama` (359 lines)
255
- - `exe/llm-openai` (340 lines)
256
- - `lib/elelem/configuration.rb`
257
- - `lib/elelem/toolbox.rb` and toolbox/* files
258
- - `lib/elelem/mcp_client.rb`
259
- - `lib/elelem/tool.rb` and `lib/elelem/tools.rb`
260
- - `lib/elelem/tui.rb`
261
- - `lib/elelem/api.rb`
262
- - `lib/elelem/states/*` (state machine infrastructure)
263
- - Removed ~750 lines of unused/redundant code
264
-
265
- ## [0.2.1] - 2025-10-15
266
-
267
- ### Fixed
268
- - Added missing `exe/llm-ollama` and `exe/llm-openai` files to gemspec
269
- - These executables were added in 0.2.0 but not included in the packaged gem
270
-
271
- ## [0.2.0] - 2025-10-15
272
-
273
- ### Added
274
- - New `llm-ollama` executable - minimal coding agent with streaming support for Ollama
275
- - New `llm-openai` executable - minimal coding agent for OpenAI/compatible APIs
276
- - Memory feature for persistent context storage and retrieval
277
- - Web fetch tool for retrieving and analyzing web content
278
- - Streaming responses with real-time token display
279
- - Visual "thinking" progress indicators with dots during reasoning phase
280
-
281
- ### Changed
282
- - **BREAKING**: Migrated from custom Net::HTTP implementation to `net-llm` gem
283
- - API client now uses `Net::Llm::Ollama` for better reliability and maintainability
284
- - Removed direct dependencies on `net-http` and `uri` (now transitive through net-llm)
285
- - Maps Ollama's `thinking` field to internal `reasoning` field
286
- - Maps Ollama's `done_reason` to internal `finish_reason`
287
- - Improved system prompt for better agent behavior
288
- - Enhanced error handling and logging
289
-
290
- ### Fixed
291
- - Response processing for Ollama's native message format
292
- - Tool argument parsing to handle both string and object formats
293
- - Safe navigation operator usage to prevent nil errors
294
-
295
- ## [0.1.2] - 2025-08-14
296
-
297
- ### Fixed
298
- - Fixed critical bug where bash tool had nested parameters schema causing tool calls to fail with "no implicit conversion of nil into String" error
299
-
300
- ## [0.1.1] - 2025-08-12
301
-
302
- ### Fixed
303
- - Fixed infinite loop bug after tool execution - loop now continues until assistant provides final response
304
- - Fixed conversation history accumulating streaming chunks as separate entries - now properly combines same-role consecutive messages
305
- - Improved state machine logging with better debug output
306
-
307
- ## [0.1.0] - 2025-08-08
308
-
309
- - Initial release
@@ -1,217 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Elelem
4
- class MCP
5
- class OAuth
6
- CALLBACK_PORT = 18273
7
- REDIRECT_URI = "http://127.0.0.1:#{CALLBACK_PORT}/callback"
8
-
9
- def initialize(resource_url, http: Elelem::Net.http)
10
- @resource_url = resource_url
11
- @http = http
12
- @storage = TokenStorage.new
13
- end
14
-
15
- def token
16
- stored = @storage.load(@resource_url)
17
- return stored[:access_token] if stored && !expired?(stored)
18
- return refresh(stored[:refresh_token]) if stored&.dig(:refresh_token)
19
-
20
- authorize
21
- end
22
-
23
- private
24
-
25
- def expired?(stored)
26
- return false unless stored[:expires_at]
27
-
28
- Time.now.to_i >= stored[:expires_at] - 60
29
- end
30
-
31
- def authorize
32
- metadata = discover_auth_server
33
- client = load_or_register_client(metadata)
34
- verifier, challenge = generate_pkce
35
- state = SecureRandom.hex(16)
36
-
37
- auth_url = build_auth_url(metadata, client, challenge, state)
38
- open_browser(auth_url)
39
-
40
- code = wait_for_callback(state)
41
- tokens = exchange_code(metadata, client, code, verifier)
42
-
43
- @storage.save(
44
- @resource_url,
45
- access_token: tokens["access_token"],
46
- refresh_token: tokens["refresh_token"],
47
- expires_in: tokens["expires_in"]
48
- )
49
-
50
- tokens["access_token"]
51
- end
52
-
53
- def refresh(refresh_token)
54
- metadata = discover_auth_server
55
- client = load_or_register_client(metadata)
56
- uri = URI.parse(metadata["token_endpoint"])
57
-
58
- body = {
59
- grant_type: "refresh_token",
60
- refresh_token: refresh_token,
61
- client_id: client[:client_id]
62
- }
63
-
64
- response = post_form(uri, body)
65
- tokens = JSON.parse(response.body)
66
-
67
- @storage.save(
68
- @resource_url,
69
- access_token: tokens["access_token"],
70
- refresh_token: tokens["refresh_token"] || refresh_token,
71
- expires_in: tokens["expires_in"]
72
- )
73
-
74
- tokens["access_token"]
75
- rescue StandardError => e
76
- warn "Token refresh failed: #{e.message}"
77
- authorize
78
- end
79
-
80
- def discover_auth_server
81
- resource_uri = URI.parse(@resource_url)
82
- metadata_url = "#{resource_uri.scheme}://#{resource_uri.host}/.well-known/oauth-protected-resource"
83
-
84
- resource_metadata = fetch_json(metadata_url)
85
- auth_server_url = resource_metadata["authorization_servers"]&.first
86
- raise "No authorization server found" unless auth_server_url
87
-
88
- auth_metadata_url = "#{auth_server_url}/.well-known/oauth-authorization-server"
89
- fetch_json(auth_metadata_url)
90
- end
91
-
92
- def load_or_register_client(metadata)
93
- stored = @storage.load_client(@resource_url)
94
- return stored if stored
95
-
96
- client = register_client(metadata)
97
- @storage.save_client(@resource_url, client)
98
- @storage.load_client(@resource_url)
99
- end
100
-
101
- def register_client(metadata)
102
- endpoint = metadata["registration_endpoint"]
103
- raise "Dynamic registration not supported" unless endpoint
104
-
105
- body = {
106
- client_name: "elelem",
107
- redirect_uris: [REDIRECT_URI],
108
- grant_types: %w[authorization_code refresh_token],
109
- response_types: ["code"],
110
- token_endpoint_auth_method: "none"
111
- }
112
-
113
- response = post_json(endpoint, body)
114
- JSON.parse(response.body)
115
- end
116
-
117
- def generate_pkce
118
- verifier = SecureRandom.urlsafe_base64(32)
119
- challenge = Base64.urlsafe_encode64(
120
- Digest::SHA256.digest(verifier),
121
- padding: false
122
- )
123
- [verifier, challenge]
124
- end
125
-
126
- def build_auth_url(metadata, client, challenge, state)
127
- params = {
128
- response_type: "code",
129
- client_id: client[:client_id],
130
- redirect_uri: REDIRECT_URI,
131
- scope: metadata["scopes_supported"]&.join(" ") || "openid",
132
- state: state,
133
- code_challenge: challenge,
134
- code_challenge_method: "S256"
135
- }
136
-
137
- "#{metadata["authorization_endpoint"]}?#{URI.encode_www_form(params)}"
138
- end
139
-
140
- def open_browser(url)
141
- commands = ["xdg-open", "open", "start"]
142
- commands.each do |cmd|
143
- return if system(cmd, url, out: File::NULL, err: File::NULL)
144
- end
145
- warn "Open this URL in your browser: #{url}"
146
- end
147
-
148
- def wait_for_callback(expected_state)
149
- code = nil
150
- @server = WEBrick::HTTPServer.new(
151
- Port: CALLBACK_PORT,
152
- Logger: WEBrick::Log.new(File::NULL),
153
- AccessLog: []
154
- )
155
-
156
- at_exit { @server&.shutdown }
157
-
158
- @server.mount_proc("/callback") do |req, res|
159
- state = req.query["state"]
160
- raise "State mismatch" unless state == expected_state
161
-
162
- code = req.query["code"]
163
- res.content_type = "text/html"
164
- res.body = "<html><body><h1>Authorization complete</h1><p>You can close this window.</p></body></html>"
165
- @server.shutdown
166
- end
167
-
168
- Timeout.timeout(120) { @server.start }
169
- code
170
- rescue Timeout::Error
171
- @server.shutdown
172
- raise "OAuth callback timed out"
173
- end
174
-
175
- def exchange_code(metadata, client, code, verifier)
176
- uri = URI.parse(metadata["token_endpoint"])
177
-
178
- body = {
179
- grant_type: "authorization_code",
180
- code: code,
181
- redirect_uri: REDIRECT_URI,
182
- client_id: client[:client_id],
183
- code_verifier: verifier
184
- }
185
-
186
- response = post_form(uri, body)
187
- JSON.parse(response.body)
188
- end
189
-
190
- def fetch_json(url)
191
- response = nil
192
- @http.get(url) { |r| response = r }
193
- JSON.parse(response.body)
194
- end
195
-
196
- def post_json(url, body)
197
- response = nil
198
- @http.post(
199
- url,
200
- headers: { "Content-Type" => "application/json" },
201
- body: body.to_json
202
- ) { |r| response = r }
203
- response
204
- end
205
-
206
- def post_form(uri, body)
207
- response = nil
208
- @http.post(
209
- uri.to_s,
210
- headers: { "Content-Type" => "application/x-www-form-urlencoded" },
211
- body: URI.encode_www_form(body)
212
- ) { |r| response = r }
213
- response
214
- end
215
- end
216
- end
217
- end
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Elelem
4
- class MCP
5
- class TokenStorage
6
- STORAGE_DIR = File.expand_path("~/.config/elelem/tokens")
7
-
8
- def initialize
9
- FileUtils.mkdir_p(STORAGE_DIR, mode: 0o700)
10
- end
11
-
12
- def save(resource_url, access_token:, refresh_token: nil, expires_in: nil)
13
- data = {
14
- access_token: access_token,
15
- refresh_token: refresh_token,
16
- expires_at: expires_in ? Time.now.to_i + expires_in : nil
17
- }
18
- path = token_path(resource_url)
19
- File.write(path, data.to_json)
20
- File.chmod(0o600, path)
21
- end
22
-
23
- def load(resource_url)
24
- path = token_path(resource_url)
25
- return nil unless File.exist?(path)
26
-
27
- JSON.parse(File.read(path), symbolize_names: true)
28
- rescue JSON::ParserError
29
- nil
30
- end
31
-
32
- def save_client(resource_url, client_data)
33
- path = client_path(resource_url)
34
- File.write(path, client_data.to_json)
35
- File.chmod(0o600, path)
36
- end
37
-
38
- def load_client(resource_url)
39
- path = client_path(resource_url)
40
- return nil unless File.exist?(path)
41
-
42
- JSON.parse(File.read(path), symbolize_names: true)
43
- rescue JSON::ParserError
44
- nil
45
- end
46
-
47
- private
48
-
49
- def token_path(resource_url)
50
- hash = Digest::SHA256.hexdigest(resource_url)[0, 16]
51
- File.join(STORAGE_DIR, "#{hash}.json")
52
- end
53
-
54
- def client_path(resource_url)
55
- hash = Digest::SHA256.hexdigest(resource_url)[0, 16]
56
- File.join(STORAGE_DIR, "#{hash}_client.json")
57
- end
58
- end
59
- end
60
- end