zuzu 0.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae81f9e580d8c3e0efd863e9f56b6719a9cd08a092dc6b998ab8bf703ddae000
4
- data.tar.gz: 287c409a0507b53f2f313da7fb2b00466180680641b36b81cc1d23b9046654db
3
+ metadata.gz: b6098f883dec29f87b5dcece9c69f8e115608cfd89b9cf045fd442355fbf48c9
4
+ data.tar.gz: d9ffeb971f19158a6262401bdcadb7791bdbd70523c843dd6b7ac26cc3a4c536
5
5
  SHA512:
6
- metadata.gz: 500c75552b9483fa2414a17971e3cce3d3c02f12ee0e02f63d723165e7ed1ba9c4b2f75b36d8520d4cbc52127331d11560e2bbca0eca4533e02c426e56e1e597
7
- data.tar.gz: aa6f574afac309ef301b12632ee791521ee8ec287fcd4d933472c19cd7adc354e4d92c3f42871dc964ae343dde436ce0be01e6e83166ac7784f2389b9c3684db
6
+ metadata.gz: b7d6f2d65abfc635d0dceec2122a788d7d2cc03159969c9e06a1b03a45e73eb9c03bf430d5f1a6170a7aaa8c717961bc5c51311336cdf80ab4a3b28feb1dc8b4
7
+ data.tar.gz: b10535955c154eee0eaade46edb4ee5b1b9049e8e224cea1d49da58553d1bbd1ae263045977f9a1ac6d87dbb5bf0c7da62e26e73eb5ec0c4b3bf8152ba699f58
data/README.md CHANGED
@@ -1,9 +1,22 @@
1
1
  # Zuzu
2
2
 
3
- **Local-first agentic desktop apps with JRuby.**
3
+ **Build AI-native desktop apps that run entirely on the user's machine.**
4
4
 
5
- Zuzu is a framework for building privacy-respecting AI desktop assistants that
6
- run entirely on your machine. No cloud APIs required — your data stays local.
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.4.0, and all gem dependencies
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 = 'My Assistant'
65
- c.llamafile_path = File.expand_path('models/llava-v1.5-7b-q4.llamafile', __dir__)
66
- c.db_path = File.expand_path('.zuzu/zuzu.db', __dir__)
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
- # macOS (SWT requires first-thread access):
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 with a file browser on the left and a chat
82
- interface on the right. The llamafile model starts automatically in the
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 openjdk@21
106
+ brew install --cask temurin@21
97
107
  ```
98
108
 
99
- Add to your shell profile (`~/.zshrc` or `~/.bash_profile`):
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.4.0 via rbenv
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.4.0
136
- rbenv local jruby-10.0.4.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.4.0 (ruby 3.x.x) ...
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
- Use Warbler to create a standalone Java archive:
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
- gem install warbler
296
- warble jar
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 openjdk@21
25
- echo 'export JAVA_HOME=$(/usr/libexec/java_home -v 21)' >> ~/.zshrc
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
- template_src = File.expand_path('../templates/app.rb', __dir__)
36
+ templates_dir = File.expand_path('../templates', __dir__)
37
37
 
38
38
  FileUtils.mkdir_p(app_dir)
39
- FileUtils.cp(template_src, File.join(app_dir, 'app.rb'))
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'
@@ -96,16 +111,28 @@ when 'package'
96
111
  abort 'warble binary not found after install. Try: gem install warbler' if warble_bin.empty?
97
112
  end
98
113
 
114
+ # Warbler 2.x auto-discovers the main script from bin/ — create a thin launcher
115
+ unless File.exist?('bin/app')
116
+ FileUtils.mkdir_p('bin')
117
+ File.write('bin/app', <<~SCRIPT)
118
+ #!/usr/bin/env jruby
119
+ load File.expand_path('../app.rb', __dir__)
120
+ SCRIPT
121
+ File.chmod(0o755, 'bin/app')
122
+ puts 'Created bin/app launcher for warbler.'
123
+ end
124
+
99
125
  puts 'Packaging app as zuzu-app.jar ...'
100
126
  puts ' (this may take a minute)'
101
127
  success = Bundler.with_unbundled_env { system(warble_bin, 'jar') }
102
128
  if success
103
129
  puts ''
104
- puts 'Done! Created: zuzu-app.jar'
130
+ jar = Dir['*.jar'].first || 'app.jar'
131
+ puts "Done! Created: #{jar}"
105
132
  puts ''
106
133
  puts 'Run it with:'
107
- puts ' java -XstartOnFirstThread -jar zuzu-app.jar # macOS'
108
- puts ' java -jar zuzu-app.jar # Linux / Windows'
134
+ puts " java -XstartOnFirstThread -jar #{jar} # macOS"
135
+ puts " java -jar #{jar} # Linux / Windows"
109
136
  else
110
137
  abort 'warble jar failed. Check output above for details.'
111
138
  end
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
- SYSTEM_PROMPT = <<~PROMPT.strip
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' => SYSTEM_PROMPT },
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 = :info
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zuzu
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
@@ -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.