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.
- checksums.yaml +4 -4
- data/README.md +8 -195
- data/exe/elelem +7 -1
- data/lib/elelem/agent.rb +68 -62
- data/lib/elelem/cli.rb +116 -0
- data/lib/elelem/commands.rb +26 -10
- data/lib/elelem/config.rb +17 -0
- data/lib/elelem/conversation.rb +1 -0
- data/lib/elelem/input.rb +51 -0
- data/lib/elelem/null_input.rb +8 -0
- data/lib/elelem/null_output.rb +7 -0
- data/lib/elelem/null_tool.rb +18 -0
- data/lib/elelem/output.rb +55 -0
- data/lib/elelem/plugins.rb +20 -27
- data/lib/elelem/prompts/default.erb +3 -0
- data/lib/elelem/registration.rb +37 -0
- data/lib/elelem/registry.rb +68 -0
- data/lib/elelem/repl.rb +34 -0
- data/lib/elelem/result.rb +27 -0
- data/lib/elelem/schema.rb +20 -0
- data/lib/elelem/session.rb +17 -0
- data/lib/elelem/slash_command.rb +18 -0
- data/lib/elelem/stub_provider.rb +15 -0
- data/lib/elelem/system_prompt.rb +12 -135
- data/lib/elelem/tool.rb +7 -12
- data/lib/elelem/toolbox.rb +40 -31
- data/lib/elelem/transcript.rb +26 -0
- data/lib/elelem/version.rb +2 -2
- data/lib/elelem.rb +28 -113
- metadata +48 -155
- data/CHANGELOG.md +0 -309
- data/lib/elelem/mcp/oauth.rb +0 -217
- data/lib/elelem/mcp/token_storage.rb +0 -60
- data/lib/elelem/mcp.rb +0 -243
- data/lib/elelem/net/claude.rb +0 -202
- data/lib/elelem/net/ollama.rb +0 -81
- data/lib/elelem/net/openai.rb +0 -88
- data/lib/elelem/net.rb +0 -13
- data/lib/elelem/permissions.rb +0 -45
- data/lib/elelem/plugins/builtins.rb +0 -96
- data/lib/elelem/plugins/edit.rb +0 -15
- data/lib/elelem/plugins/eval.rb +0 -20
- data/lib/elelem/plugins/execute.rb +0 -18
- data/lib/elelem/plugins/git.rb +0 -20
- data/lib/elelem/plugins/glob.rb +0 -13
- data/lib/elelem/plugins/grep.rb +0 -21
- data/lib/elelem/plugins/list.rb +0 -14
- data/lib/elelem/plugins/mcp.rb +0 -20
- data/lib/elelem/plugins/permissions.json +0 -6
- data/lib/elelem/plugins/read.rb +0 -21
- data/lib/elelem/plugins/task.rb +0 -14
- data/lib/elelem/plugins/tools.rb +0 -13
- data/lib/elelem/plugins/verify.rb +0 -47
- data/lib/elelem/plugins/write.rb +0 -34
- data/lib/elelem/plugins/zz_confirm.rb +0 -9
- 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
|
data/lib/elelem/mcp/oauth.rb
DELETED
|
@@ -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
|