zuzu 0.2.1-java → 0.2.3-java
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 +73 -32
- data/bin/setup +3 -6
- data/bin/zuzu +116 -11
- data/lib/zuzu/agent.rb +18 -11
- data/lib/zuzu/config.rb +27 -4
- data/lib/zuzu/version.rb +1 -1
- data/templates/.claude/skills/add-tool/SKILL.md +162 -0
- data/templates/.claude/skills/customize/SKILL.md +192 -0
- data/templates/.claude/skills/debug/SKILL.md +197 -0
- data/templates/.claude/skills/setup/SKILL.md +102 -0
- data/templates/AGENTS.md +589 -0
- data/templates/CLAUDE.md +82 -0
- data/templates/app.rb +49 -11
- data/warble.rb +19 -0
- metadata +33 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd72fe083faded7ee6c7c7461125625f70a02a062da88eb9b28ed459cf767e40
|
|
4
|
+
data.tar.gz: 2096e3e9972ac19255f8b3f2ecdff467a979ac6cd9efe378f1bc226f625ce6aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 779b1e6821bcbe4199fa08868a8e0f4be6d197fae68e42ed6f46605f02c8e402a1e05e34c79b2abae71d7e4c06b4eaacd2a1b20c206eecf3ef64fc134d6762f4
|
|
7
|
+
data.tar.gz: 3453ba743830abb356d912a7f770fd27a7b22869a376830697b9419df11eea2c2cd9ce41fc01cbc827a5ea3370e913f05f8341a5c74a560111d30c40c899dedc
|
data/README.md
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
# Zuzu
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Build AI-native desktop apps that run entirely on the user's machine.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Every application you install on an operating system does the same fundamental thing: it translates human intent into OS system calls. A text editor writes bytes to disk. A browser opens network connections. At its core, every installed app is an orchestrator of operating system capabilities.
|
|
6
|
+
|
|
7
|
+
LLMs are simply a more expressive interface for exactly that orchestration. Zuzu is a framework built on this premise — for developers who want to ship installable, AI-native desktop apps where the intelligence runs on the user's hardware, not in a data center.
|
|
8
|
+
|
|
9
|
+
**Why does this matter?**
|
|
10
|
+
|
|
11
|
+
- **Privacy by architecture.** The agent operates inside AgentFS — a sandboxed virtual filesystem backed by a single SQLite file. It cannot touch the host OS unless you explicitly open that door. There is no network call to make, no token to rotate, no terms of service that changes next quarter.
|
|
12
|
+
|
|
13
|
+
- **Deployable like software, not like a service.** Package your app as a single `.jar` — or go further with `zuzu package` and produce a native installer (`.dmg`/`.app` on macOS, `.deb` on Linux, `.exe` on Windows) that bundles a minimal JRE via jlink. Users download, double-click, and run. No Java pre-installed. No Docker. No cloud subscription. No infrastructure to maintain.
|
|
14
|
+
|
|
15
|
+
- **Built for regulated environments.** A therapist keeping session notes, an auditor running confidential analysis, a corporate team in a restricted environment — these are exactly the users who benefit most from powerful AI but are currently blocked by cloud dependency. A bundled LLM in a self-contained Java application needs no external approval to run.
|
|
16
|
+
|
|
17
|
+
- **Developer experience that matches how software is actually built today.** `zuzu new my_app` scaffolds a project pre-wired for Claude Code: CLAUDE.md, skills for `/setup`, `/add-tool`, `/customize`, and `/debug` — all enforcing Zuzu's patterns. Open the folder, start your coding agent, describe what you want to build.
|
|
18
|
+
|
|
19
|
+
→ [Why Zuzu exists](docs/why.md) · [Quick Demo](https://raw.githubusercontent.com/parolkar/zuzu/refs/heads/main/docs/demo/zuzu_quick_demo_01.mp4)
|
|
7
20
|
|
|
8
21
|
<video src="https://raw.githubusercontent.com/parolkar/zuzu/refs/heads/main/docs/demo/zuzu_quick_demo_01.mp4" controls width="100%"></video>
|
|
9
22
|
[Quick Demo](https://raw.githubusercontent.com/parolkar/zuzu/refs/heads/main/docs/demo/zuzu_quick_demo_01.mp4)
|
|
@@ -32,7 +45,7 @@ cd zuzu
|
|
|
32
45
|
bin/setup
|
|
33
46
|
```
|
|
34
47
|
|
|
35
|
-
`bin/setup` installs Java 21, JRuby 10.0.
|
|
48
|
+
`bin/setup` installs Java 21, JRuby 10.0.3.0, and all gem dependencies
|
|
36
49
|
automatically. If you prefer manual setup, see [Manual Setup](#manual-setup)
|
|
37
50
|
below.
|
|
38
51
|
|
|
@@ -61,9 +74,11 @@ Edit `app.rb` to point at your model:
|
|
|
61
74
|
|
|
62
75
|
```ruby
|
|
63
76
|
Zuzu.configure do |c|
|
|
64
|
-
c.app_name
|
|
65
|
-
|
|
66
|
-
|
|
77
|
+
c.app_name = 'My Assistant'
|
|
78
|
+
# Works both when run directly and from a packaged .jar
|
|
79
|
+
base = __dir__.to_s.start_with?('uri:classloader:') ? Dir.pwd : __dir__
|
|
80
|
+
c.llamafile_path = File.join(base, 'models', 'llava-v1.5-7b-q4.llamafile')
|
|
81
|
+
c.db_path = File.join(base, '.zuzu', 'zuzu.db')
|
|
67
82
|
c.port = 8080
|
|
68
83
|
end
|
|
69
84
|
```
|
|
@@ -71,16 +86,11 @@ end
|
|
|
71
86
|
Launch:
|
|
72
87
|
|
|
73
88
|
```bash
|
|
74
|
-
|
|
75
|
-
JRUBY_OPTS="-J-XstartOnFirstThread -J--enable-native-access=ALL-UNNAMED" bundle exec ruby app.rb
|
|
76
|
-
|
|
77
|
-
# Linux:
|
|
78
|
-
JRUBY_OPTS="-J--enable-native-access=ALL-UNNAMED" bundle exec ruby app.rb
|
|
89
|
+
bundle exec zuzu start
|
|
79
90
|
```
|
|
80
91
|
|
|
81
|
-
You'll see a native desktop window
|
|
82
|
-
|
|
83
|
-
background.
|
|
92
|
+
You'll see a native desktop chat window. The llamafile model starts automatically
|
|
93
|
+
in the background. Click **Admin Panel** to browse the AgentFS virtual filesystem.
|
|
84
94
|
|
|
85
95
|
---
|
|
86
96
|
|
|
@@ -90,18 +100,14 @@ If `bin/setup` doesn't suit your workflow, follow these steps.
|
|
|
90
100
|
|
|
91
101
|
### 1. Install Java 21+
|
|
92
102
|
|
|
93
|
-
**macOS (Homebrew):**
|
|
103
|
+
**macOS (Homebrew — recommended):**
|
|
94
104
|
|
|
95
105
|
```bash
|
|
96
|
-
brew install
|
|
106
|
+
brew install --cask temurin@21
|
|
97
107
|
```
|
|
98
108
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
```bash
|
|
102
|
-
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
|
|
103
|
-
export PATH="$JAVA_HOME/bin:$PATH"
|
|
104
|
-
```
|
|
109
|
+
Temurin is the Eclipse/Adoptium OpenJDK distribution. The cask sets up
|
|
110
|
+
`JAVA_HOME` automatically — no manual PATH changes needed.
|
|
105
111
|
|
|
106
112
|
**macOS (SDKMAN):**
|
|
107
113
|
|
|
@@ -125,17 +131,17 @@ java -version
|
|
|
125
131
|
# → openjdk version "21.x.x" ...
|
|
126
132
|
```
|
|
127
133
|
|
|
128
|
-
### 2. Install JRuby 10.0.
|
|
134
|
+
### 2. Install JRuby 10.0.3.0 via rbenv
|
|
129
135
|
|
|
130
136
|
```bash
|
|
131
137
|
# Install rbenv if needed
|
|
132
138
|
brew install rbenv ruby-build # macOS
|
|
133
139
|
# or: https://github.com/rbenv/rbenv#installation
|
|
134
140
|
|
|
135
|
-
rbenv install jruby-10.0.
|
|
136
|
-
rbenv local jruby-10.0.
|
|
141
|
+
rbenv install jruby-10.0.3.0
|
|
142
|
+
rbenv local jruby-10.0.3.0
|
|
137
143
|
ruby -v
|
|
138
|
-
# → jruby 10.0.
|
|
144
|
+
# → jruby 10.0.3.0 (ruby 3.x.x) ...
|
|
139
145
|
```
|
|
140
146
|
|
|
141
147
|
### 3. Install Gems
|
|
@@ -289,13 +295,47 @@ fs.kv_get('last_query') # → "weather in Tokyo"
|
|
|
289
295
|
|
|
290
296
|
### Packaging as .jar
|
|
291
297
|
|
|
292
|
-
|
|
298
|
+
Package your app as a standalone Java archive with a single command:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
bundle exec zuzu package
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
This auto-installs Warbler if needed, generates the necessary launcher, and
|
|
305
|
+
produces a `.jar` named after your app directory. Run it with:
|
|
293
306
|
|
|
294
307
|
```bash
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
308
|
+
java -XstartOnFirstThread -jar my_app.jar # macOS
|
|
309
|
+
java -jar my_app.jar # Linux / Windows
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
> **Note:** Place your llamafile model in a `models/` directory alongside the
|
|
313
|
+
> `.jar` — models are not bundled into the archive.
|
|
314
|
+
|
|
315
|
+
### Packaging as a native installer (no Java required for users)
|
|
316
|
+
|
|
317
|
+
After building the JAR, `zuzu package` asks if you want to go further:
|
|
318
|
+
|
|
319
|
+
```
|
|
320
|
+
Bundle into a self-contained native executable? (no Java required for users) [y/N]:
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Type `y` and Zuzu uses **jpackage** (bundled with JDK 21+) to produce a native
|
|
324
|
+
installer that includes a minimal JRE — users never need to install Java:
|
|
325
|
+
|
|
326
|
+
| Platform | Output |
|
|
327
|
+
|----------|--------|
|
|
328
|
+
| macOS | `.dmg` with a drag-to-Applications `.app` |
|
|
329
|
+
| Linux | `.deb` package |
|
|
330
|
+
| Windows | `.exe` installer |
|
|
331
|
+
|
|
332
|
+
The model file is not bundled (it can be gigabytes). After installing, users
|
|
333
|
+
place it in the platform user-data directory that Zuzu prints at build time:
|
|
334
|
+
|
|
335
|
+
```
|
|
336
|
+
macOS: ~/Library/Application Support/<AppName>/models/<model>.llamafile
|
|
337
|
+
Linux: ~/.local/share/<AppName>/models/<model>.llamafile
|
|
338
|
+
Windows: %APPDATA%\<AppName>\models\<model>.llamafile
|
|
299
339
|
```
|
|
300
340
|
|
|
301
341
|
---
|
|
@@ -323,6 +363,7 @@ end
|
|
|
323
363
|
```
|
|
324
364
|
zuzu new APP_NAME Scaffold a new Zuzu application
|
|
325
365
|
zuzu start Launch the Zuzu app in the current directory
|
|
366
|
+
zuzu package Package the app as a standalone .jar
|
|
326
367
|
zuzu console Open an IRB session with Zuzu loaded
|
|
327
368
|
zuzu version Print the Zuzu version
|
|
328
369
|
zuzu help Show this message
|
data/bin/setup
CHANGED
|
@@ -20,12 +20,9 @@ if java -version 2>&1 | grep -q 'version "2[1-9]\|version "3'; then
|
|
|
20
20
|
else
|
|
21
21
|
warn "Java 21+ not found."
|
|
22
22
|
if [[ "$OSTYPE" == darwin* ]]; then
|
|
23
|
-
step "Installing Java 21 via Homebrew"
|
|
24
|
-
brew install
|
|
25
|
-
echo
|
|
26
|
-
export JAVA_HOME=$(/usr/libexec/java_home -v 21)
|
|
27
|
-
export PATH="$JAVA_HOME/bin:$PATH"
|
|
28
|
-
echo " Installed. Restart your shell or run: source ~/.zshrc"
|
|
23
|
+
step "Installing Java 21 via Homebrew (Temurin)"
|
|
24
|
+
brew install --cask temurin@21
|
|
25
|
+
echo " Installed. Restart your shell to pick up JAVA_HOME."
|
|
29
26
|
elif [[ "$OSTYPE" == linux-gnu* ]]; then
|
|
30
27
|
step "Installing Java 21 via apt"
|
|
31
28
|
sudo apt-get update -qq && sudo apt-get install -y openjdk-21-jdk
|
data/bin/zuzu
CHANGED
|
@@ -33,10 +33,13 @@ when 'new'
|
|
|
33
33
|
app_dir = File.expand_path(app_name)
|
|
34
34
|
abort "Directory '#{app_name}' already exists." if File.exist?(app_dir)
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
templates_dir = File.expand_path('../templates', __dir__)
|
|
37
37
|
|
|
38
38
|
FileUtils.mkdir_p(app_dir)
|
|
39
|
-
FileUtils.cp(
|
|
39
|
+
FileUtils.cp(File.join(templates_dir, 'app.rb'), File.join(app_dir, 'app.rb'))
|
|
40
|
+
FileUtils.cp(File.join(templates_dir, 'AGENTS.md'), File.join(app_dir, 'AGENTS.md'))
|
|
41
|
+
FileUtils.cp(File.join(templates_dir, 'CLAUDE.md'), File.join(app_dir, 'CLAUDE.md'))
|
|
42
|
+
FileUtils.cp_r(File.join(templates_dir, '.claude'), File.join(app_dir, '.claude'))
|
|
40
43
|
|
|
41
44
|
# Generate Gemfile — uses local path when running from source tree,
|
|
42
45
|
# published gem when installed via `gem install zuzu`.
|
|
@@ -55,6 +58,9 @@ when 'new'
|
|
|
55
58
|
puts "Created new Zuzu app: #{app_name}/"
|
|
56
59
|
puts " #{app_name}/app.rb"
|
|
57
60
|
puts " #{app_name}/Gemfile"
|
|
61
|
+
puts " #{app_name}/AGENTS.md"
|
|
62
|
+
puts " #{app_name}/CLAUDE.md"
|
|
63
|
+
puts " #{app_name}/.claude/skills/ (setup, add-tool, customize, debug)"
|
|
58
64
|
puts ''
|
|
59
65
|
if DEV_MODE
|
|
60
66
|
puts " (dev mode: Gemfile points to #{ZUZU_ROOT})"
|
|
@@ -69,6 +75,15 @@ when 'new'
|
|
|
69
75
|
when 'start'
|
|
70
76
|
entry = ['app.rb', 'lib/app.rb'].find { |f| File.exist?(f) }
|
|
71
77
|
abort 'No app.rb found. Run `zuzu start` from your app directory.' unless entry
|
|
78
|
+
|
|
79
|
+
# SWT on Java 21+ requires --enable-native-access=ALL-UNNAMED.
|
|
80
|
+
# JVM flags must be set before the JVM starts, so re-exec via bundle
|
|
81
|
+
# with the flag injected into JRUBY_OPTS if it isn't already present.
|
|
82
|
+
unless ENV['JRUBY_OPTS'].to_s.include?('enable-native-access')
|
|
83
|
+
ENV['JRUBY_OPTS'] = "#{ENV['JRUBY_OPTS']} -J--enable-native-access=ALL-UNNAMED".strip
|
|
84
|
+
exec('bundle', 'exec', 'zuzu', 'start')
|
|
85
|
+
end
|
|
86
|
+
|
|
72
87
|
load File.expand_path(entry)
|
|
73
88
|
|
|
74
89
|
when 'package'
|
|
@@ -110,16 +125,106 @@ when 'package'
|
|
|
110
125
|
puts 'Packaging app as zuzu-app.jar ...'
|
|
111
126
|
puts ' (this may take a minute)'
|
|
112
127
|
success = Bundler.with_unbundled_env { system(warble_bin, 'jar') }
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
abort 'warble jar failed. Check output above for details.' unless success
|
|
129
|
+
|
|
130
|
+
jar = Dir['*.jar'].first || 'app.jar'
|
|
131
|
+
puts ''
|
|
132
|
+
puts "Done! Created: #{jar}"
|
|
133
|
+
puts ''
|
|
134
|
+
puts 'Run it with:'
|
|
135
|
+
puts " java -XstartOnFirstThread -jar #{jar} # macOS"
|
|
136
|
+
puts " java -jar #{jar} # Linux / Windows"
|
|
137
|
+
puts ''
|
|
138
|
+
|
|
139
|
+
# ── jpackage: optional self-contained native executable ─────────────────
|
|
140
|
+
# jpackage (JDK 14+) bundles a minimal JRE via jlink so users don't need
|
|
141
|
+
# Java pre-installed. Model files can't be bundled — they're resolved from
|
|
142
|
+
# the platform user-data directory at runtime via the zuzu.model JVM property.
|
|
143
|
+
|
|
144
|
+
jpackage_bin = `which jpackage 2>/dev/null`.strip
|
|
145
|
+
if jpackage_bin.empty?
|
|
146
|
+
puts 'Tip: Install JDK 21+ to create a self-contained executable (no Java required for users).'
|
|
121
147
|
else
|
|
122
|
-
|
|
148
|
+
print 'Bundle into a self-contained native executable? (no Java required for users) [y/N]: '
|
|
149
|
+
$stdout.flush
|
|
150
|
+
answer = $stdin.gets.to_s.strip.downcase
|
|
151
|
+
|
|
152
|
+
if answer == 'y' || answer == 'yes'
|
|
153
|
+
# ── detect / ask for model filename ───────────────────────────────────
|
|
154
|
+
model_files = Dir['models/*.llamafile']
|
|
155
|
+
model_name = if model_files.length == 1
|
|
156
|
+
puts "Using model: #{File.basename(model_files.first)}"
|
|
157
|
+
File.basename(model_files.first)
|
|
158
|
+
elsif model_files.length > 1
|
|
159
|
+
puts 'Model files found in models/:'
|
|
160
|
+
model_files.each { |f| puts " #{File.basename(f)}" }
|
|
161
|
+
print 'Enter model filename: '
|
|
162
|
+
$stdout.flush
|
|
163
|
+
$stdin.gets.to_s.strip
|
|
164
|
+
else
|
|
165
|
+
print 'Enter model filename (e.g. llava-v1.5-7b-q4.llamafile): '
|
|
166
|
+
$stdout.flush
|
|
167
|
+
$stdin.gets.to_s.strip
|
|
168
|
+
end
|
|
169
|
+
abort 'No model filename provided.' if model_name.to_s.empty?
|
|
170
|
+
|
|
171
|
+
print 'App version [1.0.0]: '
|
|
172
|
+
$stdout.flush
|
|
173
|
+
version_input = $stdin.gets.to_s.strip
|
|
174
|
+
app_version = version_input.empty? ? '1.0.0' : version_input
|
|
175
|
+
|
|
176
|
+
app_name = File.basename(Dir.pwd).gsub(/[^a-zA-Z0-9._-]/, '-')
|
|
177
|
+
|
|
178
|
+
platform = RbConfig::CONFIG['host_os']
|
|
179
|
+
pkg_type = case platform
|
|
180
|
+
when /darwin/ then 'dmg'
|
|
181
|
+
when /linux/ then 'deb'
|
|
182
|
+
else 'exe'
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
FileUtils.mkdir_p('dist')
|
|
186
|
+
|
|
187
|
+
java_opts = [
|
|
188
|
+
'--enable-native-access=ALL-UNNAMED',
|
|
189
|
+
"-Dzuzu.model=#{model_name}"
|
|
190
|
+
]
|
|
191
|
+
java_opts << '-XstartOnFirstThread' if platform =~ /darwin/
|
|
192
|
+
|
|
193
|
+
jpackage_args = [
|
|
194
|
+
jpackage_bin,
|
|
195
|
+
'--type', pkg_type,
|
|
196
|
+
'--name', app_name,
|
|
197
|
+
'--app-version', app_version,
|
|
198
|
+
'--input', '.',
|
|
199
|
+
'--main-jar', jar,
|
|
200
|
+
'--main-class', 'org.jruby.JarBootstrapMain',
|
|
201
|
+
'--dest', 'dist'
|
|
202
|
+
]
|
|
203
|
+
java_opts.each { |opt| jpackage_args.push('--java-options', opt) }
|
|
204
|
+
|
|
205
|
+
puts ''
|
|
206
|
+
puts "Building native #{pkg_type.upcase} installer with jpackage..."
|
|
207
|
+
puts ' (this may take a minute)'
|
|
208
|
+
native_ok = system(*jpackage_args)
|
|
209
|
+
|
|
210
|
+
if native_ok
|
|
211
|
+
puts ''
|
|
212
|
+
puts 'Done! Native installer created in dist/'
|
|
213
|
+
pkg_file = Dir["dist/*.#{pkg_type}"].first
|
|
214
|
+
puts " #{pkg_file}" if pkg_file
|
|
215
|
+
puts ''
|
|
216
|
+
puts 'Important — place your model file at:'
|
|
217
|
+
model_dest = case platform
|
|
218
|
+
when /darwin/ then " ~/Library/Application\\ Support/#{app_name}/models/#{model_name}"
|
|
219
|
+
when /linux/ then " ~/.local/share/#{app_name}/models/#{model_name}"
|
|
220
|
+
else " %APPDATA%\\#{app_name}\\models\\#{model_name}"
|
|
221
|
+
end
|
|
222
|
+
puts model_dest
|
|
223
|
+
puts ' (create the directory if it does not exist)'
|
|
224
|
+
else
|
|
225
|
+
warn 'jpackage failed. Check output above for details.'
|
|
226
|
+
end
|
|
227
|
+
end
|
|
123
228
|
end
|
|
124
229
|
|
|
125
230
|
when 'console'
|
data/lib/zuzu/agent.rb
CHANGED
|
@@ -11,22 +11,13 @@ module Zuzu
|
|
|
11
11
|
TOOL_CALL_RE = /<zuzu_tool_call>(.*?)<\/zuzu_tool_call>/m
|
|
12
12
|
TOOL_RESULT_RE = /<zuzu_tool_result>.*?<\/zuzu_tool_result>/m
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
BASE_PROMPT = <<~PROMPT
|
|
15
15
|
You are Zuzu, a helpful desktop AI assistant.
|
|
16
16
|
|
|
17
17
|
You have access to a sandboxed virtual filesystem called AgentFS. It is completely
|
|
18
18
|
separate from the host computer's filesystem. All file paths refer to AgentFS only.
|
|
19
19
|
You cannot access or modify any files on the host system.
|
|
20
20
|
|
|
21
|
-
Available tools — use the tag format shown below:
|
|
22
|
-
|
|
23
|
-
- write_file : Write text to an AgentFS file. Args: path (string), content (string)
|
|
24
|
-
- read_file : Read an AgentFS file. Args: path (string)
|
|
25
|
-
- list_directory : List an AgentFS directory. Args: path (string, default "/")
|
|
26
|
-
- run_command : Run a sandboxed command against AgentFS. Args: command (string)
|
|
27
|
-
Supported: ls [path], cat <path>, pwd, echo <text>
|
|
28
|
-
- http_get : Fetch a public URL from the internet. Args: url (string)
|
|
29
|
-
|
|
30
21
|
To call a tool, output exactly this on its own line:
|
|
31
22
|
<zuzu_tool_call>{"name":"TOOL_NAME","args":{"key":"value"}}</zuzu_tool_call>
|
|
32
23
|
|
|
@@ -51,7 +42,7 @@ module Zuzu
|
|
|
51
42
|
# Only system prompt + current message — no history injected into agent context.
|
|
52
43
|
# Prior non-tool-call responses cause models to skip tool use.
|
|
53
44
|
messages = [
|
|
54
|
-
{ 'role' => 'system', 'content' =>
|
|
45
|
+
{ 'role' => 'system', 'content' => build_system_prompt },
|
|
55
46
|
{ 'role' => 'user', 'content' => user_message }
|
|
56
47
|
]
|
|
57
48
|
|
|
@@ -100,6 +91,22 @@ module Zuzu
|
|
|
100
91
|
|
|
101
92
|
private
|
|
102
93
|
|
|
94
|
+
def build_system_prompt
|
|
95
|
+
tool_lines = ToolRegistry.tools.map do |t|
|
|
96
|
+
args = t.schema[:properties]&.keys&.map(&:to_s)&.join(', ')
|
|
97
|
+
line = "- #{t.name} : #{t.description}"
|
|
98
|
+
line += " Args: #{args}" if args && !args.empty?
|
|
99
|
+
line
|
|
100
|
+
end.join("\n")
|
|
101
|
+
|
|
102
|
+
extras = Zuzu.config.system_prompt_extras.to_s.strip
|
|
103
|
+
|
|
104
|
+
prompt = BASE_PROMPT.dup
|
|
105
|
+
prompt << "\nAvailable tools:\n#{tool_lines}\n"
|
|
106
|
+
prompt << "\n#{extras}" unless extras.empty?
|
|
107
|
+
prompt.strip
|
|
108
|
+
end
|
|
109
|
+
|
|
103
110
|
def extract_tool_calls(content)
|
|
104
111
|
content.scan(TOOL_CALL_RE).filter_map do |match|
|
|
105
112
|
data = JSON.parse(match[0].strip)
|
data/lib/zuzu/config.rb
CHANGED
|
@@ -11,17 +11,18 @@ module Zuzu
|
|
|
11
11
|
#
|
|
12
12
|
class Config
|
|
13
13
|
attr_accessor :port, :model, :channels, :log_level, :app_name,
|
|
14
|
-
:window_width, :window_height
|
|
14
|
+
:window_width, :window_height, :system_prompt_extras
|
|
15
15
|
|
|
16
|
-
attr_reader :db_path
|
|
16
|
+
attr_reader :db_path
|
|
17
17
|
|
|
18
18
|
def initialize
|
|
19
19
|
@port = 8080
|
|
20
20
|
@model = 'LLaMA_CPP'
|
|
21
21
|
@db_path = File.join('.zuzu', 'zuzu.db')
|
|
22
22
|
@llamafile_path = nil
|
|
23
|
-
@channels
|
|
24
|
-
@log_level
|
|
23
|
+
@channels = []
|
|
24
|
+
@log_level = :info
|
|
25
|
+
@system_prompt_extras = nil
|
|
25
26
|
@app_name = 'Zuzu'
|
|
26
27
|
@window_width = 860
|
|
27
28
|
@window_height = 620
|
|
@@ -36,6 +37,28 @@ module Zuzu
|
|
|
36
37
|
def llamafile_path=(path)
|
|
37
38
|
@llamafile_path = path ? File.expand_path(path) : path
|
|
38
39
|
end
|
|
40
|
+
|
|
41
|
+
# When running as a jpackage native executable, the model is NOT bundled
|
|
42
|
+
# in the app. Instead it lives in the platform user-data directory and its
|
|
43
|
+
# filename is injected via the `-Dzuzu.model=<filename>` JVM property set
|
|
44
|
+
# by jpackage at build time.
|
|
45
|
+
def llamafile_path
|
|
46
|
+
if defined?(Java) &&
|
|
47
|
+
(model_name = Java::JavaLang::System.getProperty('zuzu.model')) &&
|
|
48
|
+
!model_name.empty?
|
|
49
|
+
data_dir = case RbConfig::CONFIG['host_os']
|
|
50
|
+
when /darwin/
|
|
51
|
+
File.join(Dir.home, 'Library', 'Application Support', @app_name || 'Zuzu')
|
|
52
|
+
when /mswin|mingw/
|
|
53
|
+
File.join(ENV.fetch('APPDATA', Dir.home), @app_name || 'Zuzu')
|
|
54
|
+
else
|
|
55
|
+
File.join(Dir.home, '.local', 'share', @app_name || 'Zuzu')
|
|
56
|
+
end
|
|
57
|
+
File.join(data_dir, 'models', model_name)
|
|
58
|
+
else
|
|
59
|
+
@llamafile_path
|
|
60
|
+
end
|
|
61
|
+
end
|
|
39
62
|
end
|
|
40
63
|
|
|
41
64
|
@config = Config.new
|
data/lib/zuzu/version.rb
CHANGED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: add-tool
|
|
3
|
+
description: Add a new tool that the Zuzu agent can call during conversations. Guides through naming, arguments, implementation, and registers it correctly in app.rb. Use when the developer wants the AI assistant to gain a new capability (e.g. check weather, query a database, read a file, call an API).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Add a Zuzu Tool
|
|
7
|
+
|
|
8
|
+
A tool is a Ruby block the agent calls using `<zuzu_tool_call>` tags. Once registered, it is **automatically listed in the agent's system prompt** — no manual prompt editing needed.
|
|
9
|
+
|
|
10
|
+
## Step 1 — Understand what the tool should do
|
|
11
|
+
|
|
12
|
+
AskUserQuestion: "What should this tool do? Describe it in one sentence — this description will be shown to the AI agent."
|
|
13
|
+
|
|
14
|
+
AskUserQuestion: "What arguments does it need? For each: name, type (string/number/boolean), and what it represents. Or 'none' if no arguments."
|
|
15
|
+
|
|
16
|
+
AskUserQuestion: "Does it need to read/write files (use AgentFS), call an external API, query a database, or just compute something locally?"
|
|
17
|
+
|
|
18
|
+
## Step 2 — Choose a tool name
|
|
19
|
+
|
|
20
|
+
- Must be snake_case, descriptive, unambiguous
|
|
21
|
+
- Examples: `get_weather`, `search_notes`, `send_email`, `calculate_tax`, `lookup_stock_price`
|
|
22
|
+
- Check `app.rb` for existing tool names — avoid duplicates
|
|
23
|
+
- AskUserQuestion: "I'll name this tool `<suggested_name>`. Does that work, or would you prefer a different name?"
|
|
24
|
+
|
|
25
|
+
## Step 3 — Implement the tool
|
|
26
|
+
|
|
27
|
+
Read `app.rb` to find the insertion point — tools go **between the `Zuzu.configure` block and `Zuzu::App.launch!`**.
|
|
28
|
+
|
|
29
|
+
### Pattern A — No arguments, no AgentFS
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
Zuzu::ToolRegistry.register(
|
|
33
|
+
'tool_name',
|
|
34
|
+
'One-sentence description shown to the agent.',
|
|
35
|
+
{ type: 'object', properties: {}, required: [] }
|
|
36
|
+
) { |_args, _fs| "result as a string" }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Pattern B — With arguments
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
Zuzu::ToolRegistry.register(
|
|
43
|
+
'tool_name',
|
|
44
|
+
'Description shown to the agent.',
|
|
45
|
+
{
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
param_one: { type: 'string', description: 'what it is' },
|
|
49
|
+
param_two: { type: 'integer', description: 'what it is' }
|
|
50
|
+
},
|
|
51
|
+
required: ['param_one'] # list only truly required params
|
|
52
|
+
}
|
|
53
|
+
) do |args, _fs|
|
|
54
|
+
# args keys are STRINGS: args['param_one'] not args[:param_one]
|
|
55
|
+
value = args['param_one'].to_s
|
|
56
|
+
"result: #{value}"
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Pattern C — Using AgentFS (sandboxed file/KV access)
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
Zuzu::ToolRegistry.register(
|
|
64
|
+
'save_note',
|
|
65
|
+
'Save a note to the virtual filesystem.',
|
|
66
|
+
{
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
title: { type: 'string', description: 'Note title' },
|
|
70
|
+
content: { type: 'string', description: 'Note content' }
|
|
71
|
+
},
|
|
72
|
+
required: %w[title content]
|
|
73
|
+
}
|
|
74
|
+
) do |args, fs|
|
|
75
|
+
# fs is Zuzu::AgentFS — sandboxed, NOT the host filesystem
|
|
76
|
+
path = "/notes/#{args['title'].downcase.gsub(/\s+/, '_')}.txt"
|
|
77
|
+
fs.write_file(path, args['content'])
|
|
78
|
+
"Saved to #{path}"
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Pattern D — Calling an external HTTP API
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
require 'net/http'
|
|
86
|
+
require 'json'
|
|
87
|
+
|
|
88
|
+
Zuzu::ToolRegistry.register(
|
|
89
|
+
'get_weather',
|
|
90
|
+
'Get current weather for a city using wttr.in.',
|
|
91
|
+
{
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
city: { type: 'string', description: 'City name' }
|
|
95
|
+
},
|
|
96
|
+
required: ['city']
|
|
97
|
+
}
|
|
98
|
+
) do |args, _fs|
|
|
99
|
+
uri = URI("https://wttr.in/#{URI.encode_uri_component(args['city'])}?format=3")
|
|
100
|
+
Net::HTTP.get(uri).strip
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
"Error fetching weather: #{e.message}"
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Step 4 — Enforce these rules before writing
|
|
107
|
+
|
|
108
|
+
Before inserting code, verify:
|
|
109
|
+
|
|
110
|
+
- [ ] Tool is placed **before** `Zuzu::App.launch!`
|
|
111
|
+
- [ ] Block signature uses `|args, fs|` or `|args, _fs|` (never zero args)
|
|
112
|
+
- [ ] `args` keys use **string** form: `args['name']` not `args[:name]`
|
|
113
|
+
- [ ] Return value is a **String** (or will be `.to_s`'d automatically)
|
|
114
|
+
- [ ] No `File.read` / `File.write` / `Dir` calls — use `fs` for file access
|
|
115
|
+
- [ ] External HTTP calls have a rescue block returning an error string
|
|
116
|
+
- [ ] Description is one clear sentence (the agent sees this verbatim)
|
|
117
|
+
|
|
118
|
+
Write the tool to `app.rb` now.
|
|
119
|
+
|
|
120
|
+
## Step 5 — Verify it loads
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
bundle exec ruby -e "
|
|
124
|
+
require 'zuzu'
|
|
125
|
+
load 'app.rb' rescue nil
|
|
126
|
+
tool = Zuzu::ToolRegistry.find('<tool_name>')
|
|
127
|
+
if tool
|
|
128
|
+
puts 'Tool registered: ' + tool.name
|
|
129
|
+
puts 'Description: ' + tool.description
|
|
130
|
+
else
|
|
131
|
+
puts 'ERROR: tool not found'
|
|
132
|
+
end
|
|
133
|
+
" 2>&1
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- If "tool not found": check for syntax errors, verify placement before `launch!`, retry.
|
|
137
|
+
- If syntax error printed: fix it, retry.
|
|
138
|
+
|
|
139
|
+
## Step 6 — Test in console (if possible without side effects)
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
bundle exec zuzu console
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Then in the console:
|
|
146
|
+
```ruby
|
|
147
|
+
store = Zuzu::Store.new
|
|
148
|
+
fs = Zuzu::AgentFS.new(store)
|
|
149
|
+
tool = Zuzu::ToolRegistry.find('<tool_name>')
|
|
150
|
+
puts tool.block.call({'param' => 'test_value'}, fs)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- If it returns a sensible result: done.
|
|
154
|
+
- If error: fix and re-verify.
|
|
155
|
+
|
|
156
|
+
## Step 7 — Tell the user
|
|
157
|
+
|
|
158
|
+
Show the registered tool code and confirm:
|
|
159
|
+
> ✅ Tool `<name>` registered. The agent will now automatically list it in its system prompt and call it when relevant. Restart the app (`bundle exec zuzu start`) to pick up the change.
|
|
160
|
+
|
|
161
|
+
If the tool calls an external service that needs configuration (API key, URL, etc.):
|
|
162
|
+
> ⚠️ Remember to set `ENV['YOUR_API_KEY']` or add the value to your configuration before running.
|