zuzu 0.2.1-java → 0.2.2-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 +47 -32
- data/bin/setup +3 -6
- data/bin/zuzu +17 -2
- data/lib/zuzu/agent.rb +18 -11
- data/lib/zuzu/config.rb +4 -3
- 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 +557 -0
- data/templates/CLAUDE.md +70 -0
- data/templates/app.rb +49 -11
- data/warble.rb +19 -0
- metadata +31 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b6098f883dec29f87b5dcece9c69f8e115608cfd89b9cf045fd442355fbf48c9
|
|
4
|
+
data.tar.gz: d9ffeb971f19158a6262401bdcadb7791bdbd70523c843dd6b7ac26cc3a4c536
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b7d6f2d65abfc635d0dceec2122a788d7d2cc03159969c9e06a1b03a45e73eb9c03bf430d5f1a6170a7aaa8c717961bc5c51311336cdf80ab4a3b28feb1dc8b4
|
|
7
|
+
data.tar.gz: b10535955c154eee0eaade46edb4ee5b1b9049e8e224cea1d49da58553d1bbd1ae263045977f9a1ac6d87dbb5bf0c7da62e26e73eb5ec0c4b3bf8152ba699f58
|
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`. Users pay, download, double-click, and run. No Docker. No cloud subscription. No infrastructure to maintain. The JVM handles cross-platform distribution the way it has for 30 years.
|
|
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,15 +295,23 @@ 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
|
-
java -XstartOnFirstThread -jar zuzu-app.jar # macOS
|
|
298
|
-
java -jar zuzu-app.jar # Linux
|
|
308
|
+
java -XstartOnFirstThread -jar my_app.jar # macOS
|
|
309
|
+
java -jar my_app.jar # Linux / Windows
|
|
299
310
|
```
|
|
300
311
|
|
|
312
|
+
> **Note:** Place your llamafile model in a `models/` directory alongside the
|
|
313
|
+
> `.jar` — models are not bundled into the archive.
|
|
314
|
+
|
|
301
315
|
---
|
|
302
316
|
|
|
303
317
|
## Configuration Reference
|
|
@@ -323,6 +337,7 @@ end
|
|
|
323
337
|
```
|
|
324
338
|
zuzu new APP_NAME Scaffold a new Zuzu application
|
|
325
339
|
zuzu start Launch the Zuzu app in the current directory
|
|
340
|
+
zuzu package Package the app as a standalone .jar
|
|
326
341
|
zuzu console Open an IRB session with Zuzu loaded
|
|
327
342
|
zuzu version Print the Zuzu version
|
|
328
343
|
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'
|
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,7 +11,7 @@ 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
16
|
attr_reader :db_path, :llamafile_path
|
|
17
17
|
|
|
@@ -20,8 +20,9 @@ module Zuzu
|
|
|
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
|
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.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: customize
|
|
3
|
+
description: Customize the Zuzu app — change the app name, window size, system prompt persona, or extend the UI. Use when the developer wants to rebrand the app, add personality to the assistant, or make visual/behavioral changes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Customize Zuzu App
|
|
7
|
+
|
|
8
|
+
Ask what the developer wants to change, then make the changes directly.
|
|
9
|
+
|
|
10
|
+
## Step 1 — Understand the request
|
|
11
|
+
|
|
12
|
+
AskUserQuestion: "What would you like to customize? Choose one or more:
|
|
13
|
+
1. App name / window title
|
|
14
|
+
2. Window size
|
|
15
|
+
3. Assistant persona / system prompt instructions
|
|
16
|
+
4. Add a new button or panel to the UI
|
|
17
|
+
5. Something else — describe it"
|
|
18
|
+
|
|
19
|
+
Route to the relevant section below based on the answer.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Route A — App name and window title
|
|
24
|
+
|
|
25
|
+
Read the current value from `app.rb`:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
Zuzu.configure do |c|
|
|
29
|
+
c.app_name = 'Current Name'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
AskUserQuestion: "What should the app be called?"
|
|
33
|
+
|
|
34
|
+
Edit `app.rb` — update `c.app_name`. Done. Tell the user to restart the app.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Route B — Window size
|
|
39
|
+
|
|
40
|
+
Read current values (`c.window_width`, `c.window_height`).
|
|
41
|
+
|
|
42
|
+
AskUserQuestion: "What size should the window be? (e.g. 1024 × 768, 1280 × 800)"
|
|
43
|
+
|
|
44
|
+
Edit `app.rb`:
|
|
45
|
+
```ruby
|
|
46
|
+
c.window_width = 1024
|
|
47
|
+
c.window_height = 768
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Done. Tell the user to restart.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Route C — Assistant persona and system prompt
|
|
55
|
+
|
|
56
|
+
Read the current `c.system_prompt_extras` from `app.rb` (may be nil/absent).
|
|
57
|
+
|
|
58
|
+
AskUserQuestion: "Describe the assistant's persona or any extra rules you want it to follow. Examples:
|
|
59
|
+
- 'You are a Ruby developer assistant. Always use Ruby in code examples.'
|
|
60
|
+
- 'You are a cooking assistant. Only discuss food, recipes, and nutrition.'
|
|
61
|
+
- 'Always respond concisely in bullet points.'"
|
|
62
|
+
|
|
63
|
+
AskUserQuestion: "Should this replace the current instructions or be added to them?"
|
|
64
|
+
|
|
65
|
+
Edit `app.rb` — set or update `c.system_prompt_extras`:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
Zuzu.configure do |c|
|
|
69
|
+
# ...
|
|
70
|
+
c.system_prompt_extras = <<~EXTRA
|
|
71
|
+
<the persona instructions here>
|
|
72
|
+
EXTRA
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Important rules to preserve** — always keep these in `system_prompt_extras` if the developer's text doesn't already cover them:
|
|
77
|
+
- Do not override the tool-calling rules (those come from the base prompt automatically)
|
|
78
|
+
- Keep instructions concise — the model sees this verbatim on every request
|
|
79
|
+
|
|
80
|
+
Verify it loads:
|
|
81
|
+
```bash
|
|
82
|
+
bundle exec ruby -e "require 'zuzu'; load 'app.rb' rescue nil; puts Zuzu.config.system_prompt_extras" 2>&1
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Done. Tell the user to restart.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Route D — UI: add a button to the Admin Panel
|
|
90
|
+
|
|
91
|
+
The Admin Panel is the popup window opened by the "Admin Panel" button.
|
|
92
|
+
The easiest UI extension is adding a button there.
|
|
93
|
+
|
|
94
|
+
AskUserQuestion: "What should the button do when clicked?"
|
|
95
|
+
AskUserQuestion: "What label should the button have?"
|
|
96
|
+
|
|
97
|
+
This requires subclassing `Zuzu::App`. Check if `app.rb` already subclasses it.
|
|
98
|
+
|
|
99
|
+
**If not subclassing yet**, add this pattern to `app.rb` before `Zuzu::App.launch!`:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
class MyApp < Zuzu::App
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def open_admin_panel
|
|
106
|
+
file_list_widget = nil
|
|
107
|
+
|
|
108
|
+
admin = shell {
|
|
109
|
+
text 'Admin Panel'
|
|
110
|
+
minimum_size 380, 500
|
|
111
|
+
grid_layout 1, false
|
|
112
|
+
|
|
113
|
+
label {
|
|
114
|
+
layout_data(:fill, :fill, true, false)
|
|
115
|
+
text 'AgentFS — Virtual File Browser'
|
|
116
|
+
font height: 12, style: :bold
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
file_list_widget = list(:single, :v_scroll, :border) {
|
|
120
|
+
layout_data(:fill, :fill, true, true)
|
|
121
|
+
font name: 'Monospace', height: 11
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# ── Default buttons (keep these) ─────────────────────────
|
|
125
|
+
button {
|
|
126
|
+
layout_data(:fill, :fill, true, false)
|
|
127
|
+
text 'Create Test File'
|
|
128
|
+
on_widget_selected {
|
|
129
|
+
@fs.write_file('/test.txt', "Hello!\nCreated at: #{Time.now}")
|
|
130
|
+
populate_file_list(file_list_widget)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
button {
|
|
135
|
+
layout_data(:fill, :fill, true, false)
|
|
136
|
+
text 'Clear Chat History'
|
|
137
|
+
on_widget_selected {
|
|
138
|
+
@memory.clear
|
|
139
|
+
message_box { text 'Done'; message 'History cleared.' }.open
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
button {
|
|
144
|
+
layout_data(:fill, :fill, true, false)
|
|
145
|
+
text 'Refresh'
|
|
146
|
+
on_widget_selected { populate_file_list(file_list_widget) }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
# ── New custom button ─────────────────────────────────────
|
|
150
|
+
button {
|
|
151
|
+
layout_data(:fill, :fill, true, false)
|
|
152
|
+
text '<Button Label>'
|
|
153
|
+
on_widget_selected {
|
|
154
|
+
# your action here
|
|
155
|
+
message_box { text 'Done'; message 'Action completed.' }.open
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
populate_file_list(file_list_widget)
|
|
161
|
+
admin.open
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Then change the launch line to:
|
|
167
|
+
```ruby
|
|
168
|
+
MyApp.launch!(use_llamafile: true)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Glimmer DSL rules to enforce:**
|
|
172
|
+
- `layout_data(:fill, :fill, true, false)` — always use argument form, never block form
|
|
173
|
+
- Never call widget methods from a background thread — wrap in `async_exec { }`
|
|
174
|
+
- Never use `sash_form` — invisible on macOS
|
|
175
|
+
|
|
176
|
+
Verify syntax:
|
|
177
|
+
```bash
|
|
178
|
+
bundle exec ruby -e "require 'zuzu'; load 'app.rb' rescue puts $!.message" 2>&1
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Done. Tell the user to restart.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Route E — Something else
|
|
186
|
+
|
|
187
|
+
Read `AGENTS.md` for the relevant section and implement accordingly.
|
|
188
|
+
Always verify with a syntax check after making changes:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
bundle exec ruby -e "require 'zuzu'; load 'app.rb' rescue puts $!.message" 2>&1
|
|
192
|
+
```
|