zephira 0.1.2 → 0.1.4
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/.dockerignore +19 -0
- data/CHANGELOG.md +25 -0
- data/Dockerfile +14 -0
- data/README.md +66 -53
- data/exe/zephira +1 -0
- data/lib/zephira/agent.rb +17 -4
- data/lib/zephira/cli.rb +2 -1
- data/lib/zephira/commands/reload.rb +27 -0
- data/lib/zephira/completions/file_names.rb +1 -1
- data/lib/zephira/onboarding.rb +130 -0
- data/lib/zephira/sandbox.rb +49 -30
- data/lib/zephira/tokens.rb +1 -1
- data/lib/zephira/tools/code_search.rb +6 -2
- data/lib/zephira/tools/web_search.rb +1 -1
- data/lib/zephira/tools.rb +1 -2
- data/lib/zephira/version.rb +1 -1
- data/lib/zephira.rb +1 -0
- metadata +4 -2
- data/zephira-0.1.0.gem +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 513cf2754ac8c868ab2cb8452a480c8d9cf323b7fbefb629f929afe70aa3ffff
|
|
4
|
+
data.tar.gz: 6ed7ffcbbbda681d95eeec742c47da212e99ec325d3b72b8c29854e54a4a3dc1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2d2a968b4a48d35b0e1626e2779f782153acc76fc110887a58d53679b53550e0cb433cb27de3e2895c3a81c65a80bfeae049e3ac632a53393ba6592ba4089b58
|
|
7
|
+
data.tar.gz: 402f995425bff9f4d759d1184ba2618ef5c19599fc1b5c3983e142426256232c7dcd78255b2f0d20728faff7cf931447c6ea57ae1f92ebd53734121ec5858b9e
|
data/.dockerignore
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Zephira Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.4]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- First-run onboarding wizard. On a fresh install, Zephira detects that no API key is configured and walks the user through a guided prompt that writes their OpenAI API key to `~/.zephira.yml` (with `0600` permissions). Existing config keys are preserved when merging. Setting `ZEPHIRA_API_KEY` in the environment or running in a non-TTY context bypasses the wizard without prompting.
|
|
7
|
+
- `/reload` slash command. Re-executes the running agent (via `Kernel.exec`) so that local code edits take effect without losing conversation history (which is persisted to `.zephira/history.jsonl`). Routes through `bundle exec` when launched under Bundler, otherwise execs Ruby directly.
|
|
8
|
+
- Podman support. When Docker is unavailable, the sandbox launcher auto-detects Podman and uses it instead.
|
|
9
|
+
- Containerized development helpers under `bin/`: `docker-build` builds the dev image, `docker-shell` runs an interactive bash (or arbitrary command) inside it, and `docker-zephira` launches Zephira against the live mounted workspace. The dev launcher runs the onboarding wizard as a host-side preflight so first-run users get the same experience they'd get from a normal `gem install zephira` invocation.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- README quickstart rewritten — first-run users no longer need to hand-edit `~/.zephira.yml`; the wizard handles it.
|
|
13
|
+
- Dockerfile now bakes Git and ripgrep into the runtime image so in-sandbox tool calls don't have to reinstall them on first use.
|
|
14
|
+
- `.dockerignore` added to trim the build context and stop committed gem artifacts from polluting the runtime image.
|
|
15
|
+
- `*.gem` is now gitignored so locally-built release artifacts are no longer accidentally committed.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `code_search` tool now resolves ripgrep correctly inside the sandbox image.
|
|
19
|
+
|
|
20
|
+
### Tests
|
|
21
|
+
- Suite: 449 examples, 0 failures, ~96% line coverage.
|
|
22
|
+
|
|
23
|
+
## [0.1.3]
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Added OCI image labels (`org.opencontainers.image.source`, `description`, `licenses`) to the published Docker image so the GHCR package page links back to the repository and surfaces the README.
|
|
27
|
+
|
|
3
28
|
## [0.1.2]
|
|
4
29
|
|
|
5
30
|
### Fixed
|
data/Dockerfile
CHANGED
|
@@ -8,6 +8,7 @@ RUN apt-get update -qq && \
|
|
|
8
8
|
apt-get install -y --no-install-recommends \
|
|
9
9
|
build-essential \
|
|
10
10
|
git \
|
|
11
|
+
ripgrep \
|
|
11
12
|
&& rm -rf /var/lib/apt/lists/*
|
|
12
13
|
|
|
13
14
|
COPY Gemfile Gemfile.lock zephira.gemspec ./
|
|
@@ -23,11 +24,22 @@ RUN gem build zephira.gemspec
|
|
|
23
24
|
|
|
24
25
|
FROM ruby:3.4-slim AS runtime
|
|
25
26
|
|
|
27
|
+
LABEL org.opencontainers.image.source="https://github.com/aarongough/zephira"
|
|
28
|
+
LABEL org.opencontainers.image.description="Command-line AI coding assistant in Ruby."
|
|
29
|
+
LABEL org.opencontainers.image.licenses="MIT"
|
|
30
|
+
|
|
26
31
|
RUN apt-get update -qq && \
|
|
27
32
|
apt-get install -y --no-install-recommends \
|
|
33
|
+
git \
|
|
28
34
|
libreadline-dev \
|
|
35
|
+
ripgrep \
|
|
29
36
|
&& rm -rf /var/lib/apt/lists/*
|
|
30
37
|
|
|
38
|
+
RUN groupadd --system zephira && \
|
|
39
|
+
useradd --system --gid zephira --create-home --shell /bin/bash zephira && \
|
|
40
|
+
mkdir -p /workspace && \
|
|
41
|
+
chown zephira:zephira /workspace
|
|
42
|
+
|
|
31
43
|
COPY --from=deps /usr/local/bundle /usr/local/bundle
|
|
32
44
|
COPY --from=deps /build/zephira-*.gem /tmp/
|
|
33
45
|
|
|
@@ -36,4 +48,6 @@ RUN gem install --local --no-document /tmp/zephira-*.gem && \
|
|
|
36
48
|
|
|
37
49
|
WORKDIR /workspace
|
|
38
50
|
|
|
51
|
+
USER zephira
|
|
52
|
+
|
|
39
53
|
CMD ["zephira"]
|
data/README.md
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
# Zephira
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A command-line AI coding assistant written in Ruby. Runs in your terminal, keeps per-project conversation history, and executes safely contained inside a Docker or Podman sandbox by default.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Quickstart
|
|
6
|
+
|
|
7
|
+
1. Install Docker or Podman — required for the sandbox.
|
|
8
|
+
- Docker: https://docs.docker.com/get-docker/
|
|
9
|
+
- Podman: https://podman.io/getting-started/installation
|
|
10
|
+
|
|
11
|
+
2. Install the gem:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
gem install zephira
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
3. Run it from any project directory:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
zephira
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
On first run, Zephira launches an onboarding wizard that prompts for your OpenAI API key and writes it to `~/.zephira.yml` (with `0600` permissions). You can also set `ZEPHIRA_API_KEY` in your environment to skip the wizard entirely. To target a different OpenAI-compatible endpoint, set `ZEPHIRA_BASE_URL` alongside your key.
|
|
6
24
|
|
|
7
25
|
## Features
|
|
8
26
|
|
|
9
27
|
- Interactive terminal chat loop with per-session token-budget tracking and automatic history compaction
|
|
10
|
-
- Built-in slash commands: `/help`, `/about`, `/model`, `/history`, `/compact`, `/clear`, `/bye`
|
|
28
|
+
- Built-in slash commands: `/help`, `/about`, `/model`, `/history`, `/compact`, `/clear`, `/reload`, `/bye`
|
|
29
|
+
- First-run onboarding wizard that captures your API key the first time you launch — no manual `~/.zephira.yml` editing required
|
|
11
30
|
- Plugin-style tool system — drop a file in `lib/zephira/tools/` and it is auto-loaded:
|
|
12
31
|
- file I/O: `read_file`, `update_file`, `delete_file`, `list_directory`
|
|
13
32
|
- search: `code_search` (ripgrep-backed), `web_search` (Brave Search API)
|
|
@@ -16,25 +35,20 @@ It runs in your terminal, keeps per-project conversation history, calls a plugga
|
|
|
16
35
|
- Concurrent execution of read-only tool calls in a single turn (mutating tools still run sequentially in declared order)
|
|
17
36
|
- Pluggable model + backend layer — register a new model by dropping a file in `lib/zephira/models/`; backends bind per model class
|
|
18
37
|
- OpenAI-compatible backend out of the box; structured to add provider-specific backends without forking the core loop
|
|
19
|
-
- Docker sandbox enabled by default; `--dangerously-skip-sandbox` to opt out
|
|
38
|
+
- Docker or Podman sandbox enabled by default; `--dangerously-skip-sandbox` to opt out
|
|
20
39
|
- Persistent session log + conversation history under `.zephira/` in each project
|
|
21
40
|
- ~95% line coverage on a focused RSpec suite
|
|
22
41
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
Requirements:
|
|
26
|
-
|
|
27
|
-
- Ruby 3.2+
|
|
28
|
-
- Bundler
|
|
29
|
-
- Docker, if you want sandboxed execution
|
|
30
|
-
|
|
31
|
-
Install from RubyGems:
|
|
42
|
+
## CLI
|
|
32
43
|
|
|
33
44
|
```sh
|
|
34
|
-
|
|
45
|
+
zephira # start in the current directory
|
|
46
|
+
zephira --help # CLI help
|
|
47
|
+
zephira --version # installed version
|
|
48
|
+
zephira --dangerously-skip-sandbox # run without container isolation (your filesystem is exposed)
|
|
35
49
|
```
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
## Local development install
|
|
38
52
|
|
|
39
53
|
```sh
|
|
40
54
|
git clone https://github.com/aarongough/zephira.git
|
|
@@ -42,52 +56,22 @@ cd zephira
|
|
|
42
56
|
bundle install
|
|
43
57
|
```
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Start Zephira in the current project directory:
|
|
48
|
-
|
|
49
|
-
```sh
|
|
50
|
-
zephira
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
Show CLI help:
|
|
54
|
-
|
|
55
|
-
```sh
|
|
56
|
-
zephira --help
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Print the installed version:
|
|
60
|
-
|
|
61
|
-
```sh
|
|
62
|
-
zephira --version
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
To run without Docker sandboxing:
|
|
66
|
-
|
|
67
|
-
```sh
|
|
68
|
-
zephira --dangerously-skip-sandbox
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Warning: skipping the sandbox gives the agent direct access to your real filesystem.
|
|
59
|
+
Requirements: Ruby 3.2+, Bundler, Docker or Podman (for sandboxed execution).
|
|
72
60
|
|
|
73
61
|
## Configuration
|
|
74
62
|
|
|
75
|
-
Zephira
|
|
63
|
+
The first time you run Zephira without an API key configured, the onboarding wizard prompts for one and writes it to `~/.zephira.yml`. You can also configure Zephira manually:
|
|
76
64
|
|
|
77
65
|
- environment variables
|
|
78
66
|
- `.zephira.yml` in the current project
|
|
79
67
|
- `~/.zephira.yml` in your home directory
|
|
80
68
|
|
|
81
|
-
Environment variables take precedence.
|
|
69
|
+
Environment variables take precedence. Setting `ZEPHIRA_API_KEY` in your environment also skips the onboarding wizard, which is the recommended path for CI and other non-interactive contexts.
|
|
82
70
|
|
|
83
71
|
Example configuration:
|
|
84
72
|
|
|
85
73
|
```yaml
|
|
86
|
-
ZEPHIRA_API_KEY: "
|
|
87
|
-
ZEPHIRA_MODEL: "gpt-4.1-mini"
|
|
88
|
-
ZEPHIRA_BASE_URL: "https://api.openai.com/v1"
|
|
89
|
-
ZEPHIRA_BACKEND: "openai_compatible"
|
|
90
|
-
ZEPHIRA_BASE_IMAGE: "ruby:3.4-slim"
|
|
74
|
+
ZEPHIRA_API_KEY: "openai_API_KEY_HERE"
|
|
91
75
|
ZEPHIRA_BRAVE_SEARCH_API_KEY: "your_brave_api_key_here"
|
|
92
76
|
```
|
|
93
77
|
|
|
@@ -97,17 +81,23 @@ ZEPHIRA_BRAVE_SEARCH_API_KEY: "your_brave_api_key_here"
|
|
|
97
81
|
- `ZEPHIRA_MODEL` — model name to use
|
|
98
82
|
- `ZEPHIRA_BASE_URL` — base URL for OpenAI-compatible APIs
|
|
99
83
|
- `ZEPHIRA_BACKEND` — backend adapter identifier
|
|
100
|
-
- `ZEPHIRA_BASE_IMAGE` — base
|
|
84
|
+
- `ZEPHIRA_BASE_IMAGE` — base container image for sandbox execution
|
|
101
85
|
- `ZEPHIRA_BRAVE_SEARCH_API_KEY` — required for the web search tool
|
|
102
86
|
- `ZEPHIRA_SANDBOX` — internal/advanced flag to disable sandboxing
|
|
103
87
|
|
|
104
88
|
## Sandbox behavior
|
|
105
89
|
|
|
106
|
-
By default, Zephira attempts to run inside
|
|
90
|
+
By default, Zephira attempts to run inside a container for safer execution.
|
|
91
|
+
|
|
92
|
+
When sandboxing is enabled, Zephira re-executes itself inside Docker or Podman and mounts your current project into `/workspace`. This gives the agent access to the project while helping isolate it from the host system.
|
|
93
|
+
|
|
94
|
+
To keep files created or edited in `/workspace` owned by the host user instead of root, Zephira runs the sandboxed process as your current host UID/GID at container runtime.
|
|
107
95
|
|
|
108
|
-
|
|
96
|
+
Zephira also mounts your global config into a sandbox-specific home directory inside the container so the agent can read `~/.zephira.yml` and `~/.zephira/` without depending on `/root`.
|
|
109
97
|
|
|
110
|
-
If Docker is unavailable
|
|
98
|
+
Zephira prefers Docker when both Docker and Podman are available. If Docker is unavailable but Podman is available, Zephira uses Podman automatically.
|
|
99
|
+
|
|
100
|
+
If neither Docker nor Podman is available, Zephira exits with an error and explains how to proceed.
|
|
111
101
|
|
|
112
102
|
You can bypass sandboxing with:
|
|
113
103
|
|
|
@@ -128,8 +118,11 @@ Inside Zephira, you can use slash commands:
|
|
|
128
118
|
- `/history` — print conversation history
|
|
129
119
|
- `/compact` — manually compact the conversation history
|
|
130
120
|
- `/clear` — clear the screen
|
|
121
|
+
- `/reload` — re-execute the agent process to pick up local code changes (conversation history is preserved)
|
|
131
122
|
- `/bye` — exit the session
|
|
132
123
|
|
|
124
|
+
To change your API key after onboarding, edit `~/.zephira.yml` directly on the host, or delete it and re-launch to trigger the wizard again.
|
|
125
|
+
|
|
133
126
|
## Available models
|
|
134
127
|
|
|
135
128
|
This repository currently includes model definitions for:
|
|
@@ -196,6 +189,26 @@ Run linting:
|
|
|
196
189
|
bundle exec standardrb --fix
|
|
197
190
|
```
|
|
198
191
|
|
|
192
|
+
### Containerized development
|
|
193
|
+
|
|
194
|
+
For an isolated dev environment that mirrors the shipped sandbox image, the `bin/` directory provides helper scripts. All three rebuild the `zephira-dev` image first (Docker caches layers, so this is a no-op after the first run).
|
|
195
|
+
|
|
196
|
+
- `bin/docker-build` — build the `zephira-dev` image from the current working tree.
|
|
197
|
+
- `bin/docker-zephira` — launch Zephira inside the container, running against the mounted working tree (`bundle exec ruby exe/zephira`). Use this when iterating on the agent itself.
|
|
198
|
+
- `bin/docker-shell [command]` — start an interactive `bash` inside the container, or run an arbitrary command. Useful for running specs or linting against the containerized Ruby:
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
bin/docker-shell # interactive shell
|
|
202
|
+
bin/docker-shell 'bundle exec rspec' # run the suite in-container
|
|
203
|
+
bin/docker-shell 'bundle exec standardrb --fix' # lint in-container
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Both runner scripts mount the current directory at `/workspace`, run as the host UID/GID, and mount `~/.zephira.yml` and `~/.zephira/` into the container so configuration and history persist across runs.
|
|
207
|
+
|
|
208
|
+
`bin/docker-zephira` runs the onboarding wizard as a host-side preflight before launching the container, so first-run users get the same prompt-and-write-to-`~/.zephira.yml` experience they'd get with a normal `gem install zephira` invocation.
|
|
209
|
+
|
|
210
|
+
While inside a running Zephira session started this way, the `/reload` slash command re-executes the agent process — picking up edits to `lib/zephira/**` without rebuilding the image or losing conversation history (which is persisted to `.zephira/history.jsonl`). This is the fastest inner loop for iterating on agent code.
|
|
211
|
+
|
|
199
212
|
## Design goals
|
|
200
213
|
|
|
201
214
|
Zephira favors:
|
data/exe/zephira
CHANGED
data/lib/zephira/agent.rb
CHANGED
|
@@ -40,6 +40,19 @@ module Zephira
|
|
|
40
40
|
You should not try to guess what the user is trying to do, or try to
|
|
41
41
|
perform operations that are not explicitly requested by the user.
|
|
42
42
|
|
|
43
|
+
When investigating a bug, failure, or unexpected behavior:
|
|
44
|
+
- Do not speculate when the relevant code can be inspected.
|
|
45
|
+
- Read the implementation and the nearest calling code before answering.
|
|
46
|
+
- Prefer tracing the full execution path over making a local guess from one file.
|
|
47
|
+
- Reproduce the issue when feasible.
|
|
48
|
+
- Identify the most likely root cause from evidence, not intuition.
|
|
49
|
+
- If the issue appears to be caused by your recent changes, fix it immediately.
|
|
50
|
+
- After making a fix, run the narrowest useful test first, then the full spec suite and the linter before declaring completion.
|
|
51
|
+
- If you cannot complete validation, state exactly what blocked you and the next concrete command to run.
|
|
52
|
+
- Do not stop at partial diagnosis when the next investigative step is obvious and feasible.
|
|
53
|
+
|
|
54
|
+
Before replying on debugging or regression tasks, exhaust the obvious local investigation steps available in the repository and runtime environment. Prefer one evidence-backed answer over several speculative possibilities.
|
|
55
|
+
|
|
43
56
|
Additional instructions provided by the user. The project-local instructions
|
|
44
57
|
should overrule the global instructions:
|
|
45
58
|
|
|
@@ -55,7 +68,7 @@ module Zephira
|
|
|
55
68
|
The user's current `ls -R` is: @@@LSR@@@
|
|
56
69
|
PROMPT
|
|
57
70
|
|
|
58
|
-
LOGO = <<~
|
|
71
|
+
LOGO = <<~LOGO
|
|
59
72
|
░▒▓████████▓▒░░▒▓████████▓▒░░▒▓███████▓▒░ ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓███████▓▒░ ░▒▓██████▓▒░
|
|
60
73
|
░▒▓██▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░░▒▓█▓▒░
|
|
61
74
|
░▒▓██▓▒░ ░▒▓██████▓▒░ ░▒▓███████▓▒░ ░▒▓████████▓▒░░▒▓█▓▒░░▒▓███████▓▒░ ░▒▓████████▓▒░
|
|
@@ -93,7 +106,7 @@ module Zephira
|
|
|
93
106
|
def thinking(model_class)
|
|
94
107
|
thinkmojis = %w[🤔 🧠 💭 🤯 🧐 ⏳ 🔄 🌀 🤨 💡 🧩 🔍 📚 ⚙️]
|
|
95
108
|
token_count = Tokens.estimate(history.messages.to_json)
|
|
96
|
-
update_status("Thinking... #{thinkmojis.
|
|
109
|
+
update_status("Thinking... #{thinkmojis.sample} " + Formatter.color(:grey, "(#{model_class.model_name} - #{token_count} tokens)"))
|
|
97
110
|
end
|
|
98
111
|
|
|
99
112
|
def update_status(msg)
|
|
@@ -202,8 +215,8 @@ module Zephira
|
|
|
202
215
|
print TTY::Cursor.move_to(0, screen_height - 3)
|
|
203
216
|
puts Formatter.color(:grey, "-" * width)
|
|
204
217
|
|
|
205
|
-
sandbox_label = ENV["ZEPHIRA_IN_SANDBOX"] == "1" ? "sandboxed" : "⚠ DANGER: NO SANDBOX"
|
|
206
|
-
sandbox_color = ENV["ZEPHIRA_IN_SANDBOX"] == "1" ? :green : :red
|
|
218
|
+
sandbox_label = (ENV["ZEPHIRA_IN_SANDBOX"] == "1") ? "sandboxed" : "⚠ DANGER: NO SANDBOX"
|
|
219
|
+
sandbox_color = (ENV["ZEPHIRA_IN_SANDBOX"] == "1") ? :green : :red
|
|
207
220
|
right_text = "ctrl+c to exit | '/help' + enter to see commands | #{context_pct}% context left"
|
|
208
221
|
padding = [width - sandbox_label.length - right_text.length, 1].max
|
|
209
222
|
puts Formatter.color(sandbox_color, sandbox_label) + " " * padding + Formatter.color(:grey, right_text)
|
data/lib/zephira/cli.rb
CHANGED
|
@@ -8,8 +8,9 @@ module Zephira
|
|
|
8
8
|
|
|
9
9
|
def initialize(argv)
|
|
10
10
|
ENV["ZEPHIRA_SANDBOX"] = "false" if argv.include?(DANGEROUS_SKIP_SANDBOX_FLAG)
|
|
11
|
-
Zephira::Sandbox.exec_if_needed!(argv)
|
|
12
11
|
option_parser.parse!(argv)
|
|
12
|
+
Zephira::Onboarding.run_if_needed!
|
|
13
|
+
Zephira::Sandbox.exec_if_needed!(argv)
|
|
13
14
|
Zephira::Agent.new.run_loop
|
|
14
15
|
rescue OptionParser::InvalidOption
|
|
15
16
|
puts option_parser
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zephira
|
|
4
|
+
class Commands
|
|
5
|
+
class Reload
|
|
6
|
+
class << self
|
|
7
|
+
def name
|
|
8
|
+
"reload"
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def description
|
|
12
|
+
"Reload the agent by re-executing the process (picks up code changes)"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run(agent:, args:)
|
|
16
|
+
puts "Reloading…"
|
|
17
|
+
argv = defined?(::Zephira::ORIGINAL_ARGV) ? ::Zephira::ORIGINAL_ARGV : []
|
|
18
|
+
if ENV["BUNDLE_GEMFILE"] && File.exist?(ENV["BUNDLE_GEMFILE"])
|
|
19
|
+
Kernel.exec("bundle", "exec", RbConfig.ruby, $PROGRAM_NAME, *argv)
|
|
20
|
+
else
|
|
21
|
+
Kernel.exec(RbConfig.ruby, $PROGRAM_NAME, *argv)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Zephira
|
|
7
|
+
class Onboarding
|
|
8
|
+
GLOBAL_CONFIG_PATH = File.expand_path("~/.zephira.yml")
|
|
9
|
+
|
|
10
|
+
OUTER_TL = "╔"
|
|
11
|
+
OUTER_TR = "╗"
|
|
12
|
+
OUTER_BL = "╚"
|
|
13
|
+
OUTER_BR = "╝"
|
|
14
|
+
OUTER_H = "═"
|
|
15
|
+
OUTER_V = "║"
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def run_if_needed!
|
|
19
|
+
return if ENV["ZEPHIRA_IN_SANDBOX"] == "1"
|
|
20
|
+
return unless Config.read("ZEPHIRA_API_KEY").to_s.empty?
|
|
21
|
+
|
|
22
|
+
unless $stdin.tty?
|
|
23
|
+
print_no_tty_error
|
|
24
|
+
exit(1)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
print_welcome
|
|
28
|
+
key = prompt_key
|
|
29
|
+
if key.nil? || key.empty?
|
|
30
|
+
print_cancelled
|
|
31
|
+
exit(1)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
write_config!(key)
|
|
35
|
+
print_success
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def prompt_key
|
|
41
|
+
print " OpenAI API key: "
|
|
42
|
+
raw = $stdin.noecho(&:gets)
|
|
43
|
+
puts
|
|
44
|
+
return nil if raw.nil?
|
|
45
|
+
stripped = raw.strip
|
|
46
|
+
stripped.empty? ? nil : stripped
|
|
47
|
+
rescue Interrupt
|
|
48
|
+
puts
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def write_config!(key)
|
|
53
|
+
existing = File.exist?(GLOBAL_CONFIG_PATH) ? (YAML.load_file(GLOBAL_CONFIG_PATH) || {}) : {}
|
|
54
|
+
merged = existing.merge("ZEPHIRA_API_KEY" => key)
|
|
55
|
+
File.write(GLOBAL_CONFIG_PATH, YAML.dump(merged))
|
|
56
|
+
File.chmod(0o600, GLOBAL_CONFIG_PATH)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def print_welcome
|
|
60
|
+
lines = [
|
|
61
|
+
"",
|
|
62
|
+
" #{Formatter.color(:green, Formatter.style(:bold, "Welcome to Zephira"))}",
|
|
63
|
+
"",
|
|
64
|
+
" Zephira talks to OpenAI by default. Paste your OpenAI API key below",
|
|
65
|
+
" and we'll save it to ~/.zephira.yml for future runs.",
|
|
66
|
+
"",
|
|
67
|
+
" To target a different OpenAI-compatible endpoint instead, cancel and",
|
|
68
|
+
" set ZEPHIRA_BASE_URL alongside your key in ~/.zephira.yml.",
|
|
69
|
+
"",
|
|
70
|
+
" Input is hidden. Press Enter on an empty line to cancel.",
|
|
71
|
+
""
|
|
72
|
+
]
|
|
73
|
+
render_box(lines, :green)
|
|
74
|
+
puts
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def print_success
|
|
78
|
+
puts
|
|
79
|
+
puts " #{Formatter.color(:green, "✓")} Saved API key to #{GLOBAL_CONFIG_PATH}"
|
|
80
|
+
puts
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def print_cancelled
|
|
84
|
+
puts
|
|
85
|
+
puts " #{Formatter.color(:grey, "Cancelled.")} Set ZEPHIRA_API_KEY in your environment"
|
|
86
|
+
puts " or populate ~/.zephira.yml to skip onboarding."
|
|
87
|
+
puts
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def print_no_tty_error
|
|
91
|
+
warn ""
|
|
92
|
+
warn " #{Formatter.color(:red, "ERROR:")} Zephira needs an OpenAI API key, but stdin is not a TTY."
|
|
93
|
+
warn ""
|
|
94
|
+
warn " Set ZEPHIRA_API_KEY in your environment, or populate"
|
|
95
|
+
warn " ~/.zephira.yml with:"
|
|
96
|
+
warn ""
|
|
97
|
+
warn " ZEPHIRA_API_KEY: \"sk-...\""
|
|
98
|
+
warn ""
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def render_box(lines, color)
|
|
102
|
+
width = box_width(lines)
|
|
103
|
+
puts Formatter.color(color, OUTER_TL + OUTER_H * (width - 2) + OUTER_TR)
|
|
104
|
+
lines.each { |line| puts box_row(line, width, color) }
|
|
105
|
+
puts Formatter.color(color, OUTER_BL + OUTER_H * (width - 2) + OUTER_BR)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def box_row(text, width, color)
|
|
109
|
+
padding = " " * [width - 2 - visible_length(text), 0].max
|
|
110
|
+
Formatter.color(color, OUTER_V) + text + padding + Formatter.color(color, OUTER_V)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def box_width(lines)
|
|
114
|
+
content_max = lines.map { |line| visible_length(line) }.max || 0
|
|
115
|
+
desired = content_max + 4
|
|
116
|
+
[desired, terminal_width].min.then { |target| [target, content_max + 4].max }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def visible_length(str)
|
|
120
|
+
str.gsub(/\e\[[0-9;]*m/, "").length
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def terminal_width
|
|
124
|
+
IO.console&.winsize&.last || 80
|
|
125
|
+
rescue
|
|
126
|
+
80
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
data/lib/zephira/sandbox.rb
CHANGED
|
@@ -5,8 +5,10 @@ require "io/console"
|
|
|
5
5
|
|
|
6
6
|
module Zephira
|
|
7
7
|
class Sandbox
|
|
8
|
-
GHCR_IMAGE
|
|
8
|
+
GHCR_IMAGE = "ghcr.io/aarongough/zephira"
|
|
9
9
|
DERIVED_IMAGE_PREFIX = "zephira-sandbox"
|
|
10
|
+
CONTAINER_RUNTIMES = %w[docker podman].freeze
|
|
11
|
+
SANDBOX_HOME = "/tmp/zephira-home"
|
|
10
12
|
|
|
11
13
|
FORWARDED_ENV_PATTERNS = [/\AZEPHIRA_/].freeze
|
|
12
14
|
FORWARDED_ENV_EXCLUDES = %w[ZEPHIRA_IN_SANDBOX ZEPHIRA_SANDBOX].freeze
|
|
@@ -15,15 +17,15 @@ module Zephira
|
|
|
15
17
|
OUTER_TR = "╗"
|
|
16
18
|
OUTER_BL = "╚"
|
|
17
19
|
OUTER_BR = "╝"
|
|
18
|
-
OUTER_H
|
|
19
|
-
OUTER_V
|
|
20
|
+
OUTER_H = "═"
|
|
21
|
+
OUTER_V = "║"
|
|
20
22
|
|
|
21
23
|
INNER_TL = "┌"
|
|
22
24
|
INNER_TR = "┐"
|
|
23
25
|
INNER_BL = "└"
|
|
24
26
|
INNER_BR = "┘"
|
|
25
|
-
INNER_H
|
|
26
|
-
INNER_V
|
|
27
|
+
INNER_H = "─"
|
|
28
|
+
INNER_V = "│"
|
|
27
29
|
INNER_PADDING = 3
|
|
28
30
|
|
|
29
31
|
class << self
|
|
@@ -31,11 +33,12 @@ module Zephira
|
|
|
31
33
|
return if ENV["ZEPHIRA_IN_SANDBOX"] == "1"
|
|
32
34
|
return if ENV["ZEPHIRA_SANDBOX"] == "false"
|
|
33
35
|
|
|
34
|
-
|
|
36
|
+
runtime = container_runtime
|
|
37
|
+
abort_with_sandbox_error unless runtime
|
|
35
38
|
|
|
36
|
-
target = resolve_image
|
|
37
|
-
|
|
38
|
-
Kernel.exec(*
|
|
39
|
+
target = resolve_image(runtime)
|
|
40
|
+
warn "[Zephira] Launching in #{runtime.capitalize} sandbox (#{target})..."
|
|
41
|
+
Kernel.exec(*build_container_command(argv, target, runtime))
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
private
|
|
@@ -54,12 +57,15 @@ module Zephira
|
|
|
54
57
|
|
|
55
58
|
instruction_lines = [
|
|
56
59
|
"",
|
|
57
|
-
" #{Formatter.color(:red, "ERROR:")} Zephira requires Docker to run safely in a sandboxed
|
|
60
|
+
" #{Formatter.color(:red, "ERROR:")} Zephira requires Docker or Podman to run safely in a sandboxed",
|
|
61
|
+
" environment.",
|
|
58
62
|
"",
|
|
59
|
-
" Docker was
|
|
63
|
+
" Neither Docker nor Podman was found or currently running. To fix this:",
|
|
60
64
|
"",
|
|
61
65
|
" 1. Install Docker Desktop: https://docs.docker.com/get-docker/",
|
|
62
|
-
"
|
|
66
|
+
" or install Podman: https://podman.io/getting-started/installation",
|
|
67
|
+
" 2. Start the runtime and confirm it is running:",
|
|
68
|
+
" docker info or podman info",
|
|
63
69
|
"",
|
|
64
70
|
" To bypass the sandbox (not recommended):",
|
|
65
71
|
"",
|
|
@@ -67,9 +73,9 @@ module Zephira
|
|
|
67
73
|
""
|
|
68
74
|
]
|
|
69
75
|
|
|
70
|
-
max_warn_width
|
|
76
|
+
max_warn_width = warn_lines.map { |line| visible_length(line) }.max
|
|
71
77
|
max_content_width = instruction_lines.map { |line| visible_length(line) }.max
|
|
72
|
-
inner_width
|
|
78
|
+
inner_width = [max_warn_width, max_content_width - 10].max
|
|
73
79
|
|
|
74
80
|
inner_box = [
|
|
75
81
|
" " + inner_top(inner_width),
|
|
@@ -84,7 +90,7 @@ module Zephira
|
|
|
84
90
|
*content.map { |line| outer_row(line, width) },
|
|
85
91
|
outer_bottom(width),
|
|
86
92
|
""
|
|
87
|
-
].each { |line|
|
|
93
|
+
].each { |line| warn line }
|
|
88
94
|
exit(1)
|
|
89
95
|
end
|
|
90
96
|
|
|
@@ -121,22 +127,26 @@ module Zephira
|
|
|
121
127
|
|
|
122
128
|
def terminal_width
|
|
123
129
|
IO.console&.winsize&.last || 80
|
|
124
|
-
rescue
|
|
130
|
+
rescue
|
|
125
131
|
80
|
|
126
132
|
end
|
|
127
133
|
|
|
128
|
-
def
|
|
129
|
-
system("
|
|
134
|
+
def runtime_available?(binary)
|
|
135
|
+
system("#{binary} info > /dev/null 2>&1")
|
|
130
136
|
end
|
|
131
137
|
|
|
132
|
-
def
|
|
138
|
+
def container_runtime
|
|
139
|
+
CONTAINER_RUNTIMES.find { |runtime| runtime_available?(runtime) }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def resolve_image(runtime)
|
|
133
143
|
base = Config.read("ZEPHIRA_BASE_IMAGE")
|
|
134
144
|
return "#{GHCR_IMAGE}:#{VERSION}" unless base
|
|
135
145
|
|
|
136
146
|
derived = derived_image_name(base)
|
|
137
|
-
unless image_exists?(derived)
|
|
138
|
-
|
|
139
|
-
build_derived_image(base, derived)
|
|
147
|
+
unless image_exists?(derived, runtime)
|
|
148
|
+
warn "[Zephira] Building sandbox image from #{base} with #{runtime}..."
|
|
149
|
+
build_derived_image(base, derived, runtime)
|
|
140
150
|
end
|
|
141
151
|
derived
|
|
142
152
|
end
|
|
@@ -146,16 +156,16 @@ module Zephira
|
|
|
146
156
|
"#{DERIVED_IMAGE_PREFIX}-#{sanitized}:#{VERSION}"
|
|
147
157
|
end
|
|
148
158
|
|
|
149
|
-
def image_exists?(name)
|
|
150
|
-
system("
|
|
159
|
+
def image_exists?(name, runtime)
|
|
160
|
+
system("#{runtime} image inspect #{name} > /dev/null 2>&1")
|
|
151
161
|
end
|
|
152
162
|
|
|
153
|
-
def build_derived_image(base_image, target_name)
|
|
163
|
+
def build_derived_image(base_image, target_name, runtime)
|
|
154
164
|
dockerfile = "FROM #{base_image}\nRUN gem install zephira:#{VERSION} --no-document\n"
|
|
155
165
|
Tempfile.create(["zephira-sandbox", ".dockerfile"]) do |file|
|
|
156
166
|
file.write(dockerfile)
|
|
157
167
|
file.flush
|
|
158
|
-
system("
|
|
168
|
+
system("#{runtime} build -t #{target_name} -f #{file.path} .")
|
|
159
169
|
end
|
|
160
170
|
end
|
|
161
171
|
|
|
@@ -166,18 +176,21 @@ module Zephira
|
|
|
166
176
|
.sort
|
|
167
177
|
end
|
|
168
178
|
|
|
169
|
-
def
|
|
170
|
-
cmd = [
|
|
179
|
+
def build_container_command(argv, image, runtime)
|
|
180
|
+
cmd = [runtime, "run", "--rm", "-i"]
|
|
171
181
|
cmd << "-t" if $stdout.tty?
|
|
172
182
|
|
|
183
|
+
cmd += ["--user", "#{Process.uid}:#{Process.gid}"]
|
|
173
184
|
cmd += ["-e", "ZEPHIRA_IN_SANDBOX=1"]
|
|
185
|
+
cmd += ["-e", "HOME=#{SANDBOX_HOME}"]
|
|
174
186
|
cmd += ["-v", "#{Dir.pwd}:/workspace:rw"]
|
|
187
|
+
cmd += ["-v", "#{sandbox_home_mount(runtime)}:#{SANDBOX_HOME}:rw"]
|
|
175
188
|
|
|
176
189
|
global_config = File.expand_path("~/.zephira.yml")
|
|
177
|
-
cmd += ["-v", "#{global_config}
|
|
190
|
+
cmd += ["-v", "#{global_config}:#{SANDBOX_HOME}/.zephira.yml:ro"] if File.exist?(global_config)
|
|
178
191
|
|
|
179
192
|
global_dir = File.expand_path("~/.zephira")
|
|
180
|
-
cmd += ["-v", "#{global_dir}
|
|
193
|
+
cmd += ["-v", "#{global_dir}:#{SANDBOX_HOME}/.zephira:ro"] if File.exist?(global_dir) && File.directory?(global_dir)
|
|
181
194
|
|
|
182
195
|
forwarded_env_keys.each do |key|
|
|
183
196
|
cmd += ["-e", "#{key}=#{ENV[key]}"]
|
|
@@ -188,6 +201,12 @@ module Zephira
|
|
|
188
201
|
cmd += ["zephira"] + argv
|
|
189
202
|
cmd
|
|
190
203
|
end
|
|
204
|
+
|
|
205
|
+
def sandbox_home_mount(runtime)
|
|
206
|
+
return "zephira-home-#{Process.uid}" if runtime == "docker"
|
|
207
|
+
|
|
208
|
+
File.expand_path("~/.zephira/sandbox-home")
|
|
209
|
+
end
|
|
191
210
|
end
|
|
192
211
|
end
|
|
193
212
|
end
|
data/lib/zephira/tokens.rb
CHANGED
|
@@ -6,7 +6,7 @@ module Zephira
|
|
|
6
6
|
# which lands within ~20% of real BPE tokenizers (GPT/Claude) for English
|
|
7
7
|
# text — close enough for context-budget decisions, never trust for billing.
|
|
8
8
|
module Tokens
|
|
9
|
-
TOKEN_PATTERN = /\w+|[^\s\w]
|
|
9
|
+
TOKEN_PATTERN = /\w+|[^\s\w]/
|
|
10
10
|
|
|
11
11
|
def self.estimate(text)
|
|
12
12
|
return 0 if text.nil? || text.empty?
|
|
@@ -127,8 +127,12 @@ module Zephira
|
|
|
127
127
|
end
|
|
128
128
|
|
|
129
129
|
def executable_available?(cmd)
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? do |directory|
|
|
131
|
+
next false if directory.nil? || directory.empty?
|
|
132
|
+
|
|
133
|
+
executable = File.join(directory, cmd)
|
|
134
|
+
File.file?(executable) && File.executable?(executable)
|
|
135
|
+
end
|
|
132
136
|
end
|
|
133
137
|
end
|
|
134
138
|
end
|
|
@@ -77,7 +77,7 @@ module Zephira
|
|
|
77
77
|
return error_result(message: "`query` must be a non-empty string")
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
if num_results && (!num_results.is_a?(Integer) || !(1..50).
|
|
80
|
+
if num_results && (!num_results.is_a?(Integer) || !(1..50).cover?(num_results))
|
|
81
81
|
return error_result(message: "`num_results` must be an integer between 1 and 50")
|
|
82
82
|
end
|
|
83
83
|
|
data/lib/zephira/tools.rb
CHANGED
|
@@ -45,8 +45,7 @@ module Zephira
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def read_only?(name)
|
|
48
|
-
|
|
49
|
-
tool && tool.respond_to?(:read_only?) && tool.read_only?
|
|
48
|
+
constants.find { |candidate| candidate.name == name }&.read_only?
|
|
50
49
|
end
|
|
51
50
|
|
|
52
51
|
def find!(name)
|
data/lib/zephira/version.rb
CHANGED
data/lib/zephira.rb
CHANGED
|
@@ -4,6 +4,7 @@ require_relative "zephira/version"
|
|
|
4
4
|
require_relative "zephira/config"
|
|
5
5
|
require_relative "zephira/sandbox"
|
|
6
6
|
require_relative "zephira/formatter"
|
|
7
|
+
require_relative "zephira/onboarding"
|
|
7
8
|
require_relative "zephira/tokens"
|
|
8
9
|
require_relative "zephira/logger"
|
|
9
10
|
require_relative "zephira/backends"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zephira
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aaron Gough
|
|
@@ -158,6 +158,7 @@ executables:
|
|
|
158
158
|
extensions: []
|
|
159
159
|
extra_rdoc_files: []
|
|
160
160
|
files:
|
|
161
|
+
- ".dockerignore"
|
|
161
162
|
- ".rspec"
|
|
162
163
|
- CHANGELOG.md
|
|
163
164
|
- Dockerfile
|
|
@@ -178,6 +179,7 @@ files:
|
|
|
178
179
|
- lib/zephira/commands/help.rb
|
|
179
180
|
- lib/zephira/commands/history.rb
|
|
180
181
|
- lib/zephira/commands/model.rb
|
|
182
|
+
- lib/zephira/commands/reload.rb
|
|
181
183
|
- lib/zephira/completions.rb
|
|
182
184
|
- lib/zephira/completions/file_names.rb
|
|
183
185
|
- lib/zephira/completions/slash_commands.rb
|
|
@@ -194,6 +196,7 @@ files:
|
|
|
194
196
|
- lib/zephira/models/gpt_5_5.rb
|
|
195
197
|
- lib/zephira/models/gpt_o4_mini.rb
|
|
196
198
|
- lib/zephira/models/llama4.rb
|
|
199
|
+
- lib/zephira/onboarding.rb
|
|
197
200
|
- lib/zephira/sandbox.rb
|
|
198
201
|
- lib/zephira/tokens.rb
|
|
199
202
|
- lib/zephira/tools.rb
|
|
@@ -214,7 +217,6 @@ files:
|
|
|
214
217
|
- lib/zephira/version.rb
|
|
215
218
|
- license.txt
|
|
216
219
|
- standard.yml
|
|
217
|
-
- zephira-0.1.0.gem
|
|
218
220
|
homepage: https://github.com/aarongough/zephira
|
|
219
221
|
licenses:
|
|
220
222
|
- MIT
|
data/zephira-0.1.0.gem
DELETED
|
Binary file
|