zillacore 0.0.1
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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +126 -0
- data/README.md +1166 -0
- data/Rakefile +12 -0
- data/bin/zillacore +1521 -0
- data/certs/stowzilla.pem +26 -0
- data/docs/waybar-config.md +96 -0
- data/lib/user_registry.rb +159 -0
- data/lib/zillacore/agents.rb +203 -0
- data/lib/zillacore/brain.rb +197 -0
- data/lib/zillacore/card_index.rb +389 -0
- data/lib/zillacore/config.rb +263 -0
- data/lib/zillacore/cron.rb +629 -0
- data/lib/zillacore/deployments.rb +258 -0
- data/lib/zillacore/handlers/discord.rb +1643 -0
- data/lib/zillacore/handlers/fizzy.rb +1249 -0
- data/lib/zillacore/handlers/github.rb +598 -0
- data/lib/zillacore/handlers/zoho.rb +487 -0
- data/lib/zillacore/helpers.rb +760 -0
- data/lib/zillacore/planning.rb +237 -0
- data/lib/zillacore/prompts.rb +620 -0
- data/lib/zillacore/sessions.rb +282 -0
- data/lib/zillacore/skills.rb +276 -0
- data/lib/zillacore/users.rb +76 -0
- data/lib/zillacore/version.rb +6 -0
- data/lib/zillacore/zoho_mail_api.rb +109 -0
- data/lib/zillacore.rb +10 -0
- data/monitor/daemon.rb +99 -0
- data/monitor/deploy-env-macos.rb +131 -0
- data/monitor/menubar.rb +295 -0
- data/monitor/open-action.sh +15 -0
- data/monitor/setup-menubar.rb +78 -0
- data/monitor/setup-waybar-deploy-envs.rb +121 -0
- data/monitor/setup-waybar-deployments.rb +96 -0
- data/monitor/setup-waybar-module.rb +113 -0
- data/monitor/setup-xbar-plugin.rb +35 -0
- data/monitor/view-logs-macos.rb +210 -0
- data/monitor/view-logs-rofi.rb +194 -0
- data/monitor/view-logs.rb +119 -0
- data/monitor/waybar-config-updater.rb +56 -0
- data/monitor/waybar-deploy-env.rb +206 -0
- data/monitor/waybar-deployments.rb +239 -0
- data/monitor/waybar.rb +146 -0
- data/monitor/xbar.3s.rb +179 -0
- data/receiver.rb +956 -0
- data/templates/agents.json.example +10 -0
- data/templates/discord.json.example +17 -0
- data/templates/fizzy.json.example +24 -0
- data/templates/github.json.example +4 -0
- data/templates/testflight.json.example +8 -0
- data/templates/users.json.example +121 -0
- data/templates/zoho.json.example +27 -0
- data/views/dashboard.erb +437 -0
- data/zillacore.gemspec +30 -0
- data.tar.gz.sig +2 -0
- metadata +235 -0
- metadata.gz.sig +0 -0
data/bin/zillacore
ADDED
|
@@ -0,0 +1,1521 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "open3"
|
|
7
|
+
require "net/http"
|
|
8
|
+
require "uri"
|
|
9
|
+
require "time"
|
|
10
|
+
|
|
11
|
+
# ZillaCore CLI - Register and manage projects with ZillaCore server
|
|
12
|
+
|
|
13
|
+
ZILLACORE_ROOT = File.expand_path("..", __dir__)
|
|
14
|
+
require_relative "../lib/zillacore/version"
|
|
15
|
+
ZILLACORE_VERSION = ZillaCore::VERSION
|
|
16
|
+
|
|
17
|
+
ZILLACORE_DIR = File.join(Dir.home, ".zillacore")
|
|
18
|
+
PROJECTS_FILE = File.join(ZILLACORE_DIR, "projects.json")
|
|
19
|
+
CONFIG_FILE = File.join(ZILLACORE_DIR, "config.json")
|
|
20
|
+
PID_FILE = File.join(ZILLACORE_DIR, "server.pid")
|
|
21
|
+
|
|
22
|
+
def ensure_zillacore_dir
|
|
23
|
+
FileUtils.mkdir_p(ZILLACORE_DIR)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def load_projects
|
|
27
|
+
return {} unless File.exist?(PROJECTS_FILE)
|
|
28
|
+
|
|
29
|
+
JSON.parse(File.read(PROJECTS_FILE))
|
|
30
|
+
rescue JSON::ParserError
|
|
31
|
+
{}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def save_projects(projects)
|
|
35
|
+
ensure_zillacore_dir
|
|
36
|
+
File.write(PROJECTS_FILE, JSON.pretty_generate(projects))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def load_config
|
|
40
|
+
return {} unless File.exist?(CONFIG_FILE)
|
|
41
|
+
|
|
42
|
+
JSON.parse(File.read(CONFIG_FILE))
|
|
43
|
+
rescue JSON::ParserError
|
|
44
|
+
{}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def save_config(config)
|
|
48
|
+
ensure_zillacore_dir
|
|
49
|
+
File.write(CONFIG_FILE, JSON.pretty_generate(config))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def find_server_pid
|
|
53
|
+
# First check PID file
|
|
54
|
+
if File.exist?(PID_FILE)
|
|
55
|
+
pid = File.read(PID_FILE).strip.to_i
|
|
56
|
+
return pid if pid.positive? && process_running?(pid)
|
|
57
|
+
|
|
58
|
+
# Stale PID file
|
|
59
|
+
File.delete(PID_FILE)
|
|
60
|
+
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Fallback: search for running receiver.rb process
|
|
64
|
+
# Try multiple methods to find the process
|
|
65
|
+
|
|
66
|
+
# Method 1: pgrep with full pattern
|
|
67
|
+
output, status = Open3.capture2("pgrep", "-f", 'ruby.*receiver\.rb')
|
|
68
|
+
if status.success? && !output.strip.empty?
|
|
69
|
+
pids = output.strip.split("\n").map(&:to_i)
|
|
70
|
+
return pids.first if pids.any?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Method 2: ps + grep (more portable)
|
|
74
|
+
output, status = Open3.capture2("ps", "aux")
|
|
75
|
+
if status.success?
|
|
76
|
+
output.each_line do |line|
|
|
77
|
+
next unless line.include?("ruby") && line.include?("receiver.rb") && !line.include?("grep")
|
|
78
|
+
|
|
79
|
+
# Extract PID (second column in ps aux output)
|
|
80
|
+
parts = line.split
|
|
81
|
+
pid = parts[1].to_i
|
|
82
|
+
return pid if pid.positive? && process_running?(pid)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def process_running?(pid)
|
|
90
|
+
Process.kill(0, pid)
|
|
91
|
+
true
|
|
92
|
+
rescue Errno::ESRCH, Errno::EPERM
|
|
93
|
+
false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def fetch_active_sessions
|
|
97
|
+
uri = URI("http://localhost:4567/api/status")
|
|
98
|
+
response = Net::HTTP.get_response(uri)
|
|
99
|
+
return [] unless response.is_a?(Net::HTTPSuccess)
|
|
100
|
+
|
|
101
|
+
data = JSON.parse(response.body)
|
|
102
|
+
(data["sessions"] || data).select { |s| s["alive"] }
|
|
103
|
+
rescue StandardError
|
|
104
|
+
[]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def wait_for_agents_to_finish
|
|
108
|
+
sessions = fetch_active_sessions
|
|
109
|
+
return true if sessions.empty?
|
|
110
|
+
|
|
111
|
+
agents = sessions.map { |s| s["agent"] || "Unknown" }
|
|
112
|
+
puts "⏳ Waiting for #{sessions.size} active agent(s) to finish: #{agents.join(", ")}"
|
|
113
|
+
puts " (Ctrl+C to restart immediately)"
|
|
114
|
+
|
|
115
|
+
begin
|
|
116
|
+
loop do
|
|
117
|
+
sleep 5
|
|
118
|
+
sessions = fetch_active_sessions
|
|
119
|
+
if sessions.empty?
|
|
120
|
+
puts "✓ All agents finished."
|
|
121
|
+
return true
|
|
122
|
+
end
|
|
123
|
+
agents = sessions.map { |s| s["agent"] || "Unknown" }
|
|
124
|
+
elapsed = sessions.map { |s| s["elapsed_seconds"] || 0 }.max
|
|
125
|
+
puts " Still waiting... #{sessions.size} agent(s) active: #{agents.join(", ")} (#{elapsed}s)"
|
|
126
|
+
end
|
|
127
|
+
rescue Interrupt
|
|
128
|
+
puts "\n⚡ Skipping wait, restarting now..."
|
|
129
|
+
true
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def stop_server
|
|
134
|
+
pid = find_server_pid
|
|
135
|
+
|
|
136
|
+
unless pid
|
|
137
|
+
puts "No running ZillaCore server found."
|
|
138
|
+
return false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
puts "Stopping ZillaCore server (PID: #{pid})..."
|
|
142
|
+
|
|
143
|
+
# Also stop the monitor daemon
|
|
144
|
+
daemon_pid_file = "/tmp/zillacore-daemon.pid"
|
|
145
|
+
if File.exist?(daemon_pid_file)
|
|
146
|
+
daemon_pid = File.read(daemon_pid_file).strip.to_i
|
|
147
|
+
begin
|
|
148
|
+
Process.kill("TERM", daemon_pid)
|
|
149
|
+
puts "✓ Monitor daemon stopped (PID: #{daemon_pid})."
|
|
150
|
+
rescue Errno::ESRCH
|
|
151
|
+
# Already dead
|
|
152
|
+
end
|
|
153
|
+
File.delete(daemon_pid_file)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
begin
|
|
157
|
+
Process.kill("TERM", pid)
|
|
158
|
+
|
|
159
|
+
# Wait up to 5 seconds for graceful shutdown
|
|
160
|
+
5.times do
|
|
161
|
+
sleep 2
|
|
162
|
+
next if process_running?(pid)
|
|
163
|
+
|
|
164
|
+
puts "✓ Server stopped."
|
|
165
|
+
FileUtils.rm_f(PID_FILE)
|
|
166
|
+
return true
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Force kill if still running
|
|
170
|
+
puts "Server didn't stop gracefully, forcing..."
|
|
171
|
+
Process.kill("KILL", pid)
|
|
172
|
+
sleep 1
|
|
173
|
+
|
|
174
|
+
FileUtils.rm_f(PID_FILE)
|
|
175
|
+
puts "✓ Server stopped (forced)."
|
|
176
|
+
true
|
|
177
|
+
rescue Errno::ESRCH
|
|
178
|
+
puts "✓ Server already stopped."
|
|
179
|
+
FileUtils.rm_f(PID_FILE)
|
|
180
|
+
true
|
|
181
|
+
rescue Errno::EPERM
|
|
182
|
+
puts "Error: Permission denied. Try running with sudo or check process ownership."
|
|
183
|
+
false
|
|
184
|
+
rescue StandardError => e
|
|
185
|
+
puts "Error stopping server: #{e.message}"
|
|
186
|
+
false
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def start_server(daemon: false)
|
|
191
|
+
# Resolve the real path of the zillacore script (follows symlinks)
|
|
192
|
+
receiver_path = File.join(ZILLACORE_ROOT, "receiver.rb")
|
|
193
|
+
receiver_dir = ZILLACORE_ROOT
|
|
194
|
+
|
|
195
|
+
unless File.exist?(receiver_path)
|
|
196
|
+
puts "Error: receiver.rb not found at #{receiver_path}"
|
|
197
|
+
exit 1
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Check if already running
|
|
201
|
+
if find_server_pid
|
|
202
|
+
puts "Error: ZillaCore server is already running."
|
|
203
|
+
puts "Run 'zillacore stop' or 'zillacore restart' to manage it."
|
|
204
|
+
exit 1
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
if daemon
|
|
208
|
+
# Daemon mode: start in background (like the old behavior)
|
|
209
|
+
log_dir = File.join(receiver_dir, "tmp")
|
|
210
|
+
FileUtils.mkdir_p(log_dir)
|
|
211
|
+
log_file = File.join(log_dir, "zillacore-server.log")
|
|
212
|
+
|
|
213
|
+
puts "Starting ZillaCore server (daemon)..."
|
|
214
|
+
|
|
215
|
+
pid = spawn("ruby", receiver_path,
|
|
216
|
+
chdir: receiver_dir,
|
|
217
|
+
out: log_file,
|
|
218
|
+
err: %i[child out])
|
|
219
|
+
Process.detach(pid)
|
|
220
|
+
|
|
221
|
+
ensure_zillacore_dir
|
|
222
|
+
File.write(PID_FILE, pid.to_s)
|
|
223
|
+
File.write(File.join(ZILLACORE_DIR, "server.root"), receiver_dir)
|
|
224
|
+
|
|
225
|
+
puts "✓ Server started in background (PID: #{pid})"
|
|
226
|
+
puts " Logs: tail -f #{log_file}"
|
|
227
|
+
puts " Stop: zillacore stop"
|
|
228
|
+
puts " Restart: zillacore restart"
|
|
229
|
+
else
|
|
230
|
+
# Foreground mode (default): run in the current process like `rails server`
|
|
231
|
+
puts "Starting ZillaCore server (pid: #{Process.pid})..."
|
|
232
|
+
puts " Stop: Ctrl+C"
|
|
233
|
+
puts ""
|
|
234
|
+
|
|
235
|
+
ensure_zillacore_dir
|
|
236
|
+
File.write(PID_FILE, Process.pid.to_s)
|
|
237
|
+
File.write(File.join(ZILLACORE_DIR, "server.root"), receiver_dir)
|
|
238
|
+
|
|
239
|
+
begin
|
|
240
|
+
Dir.chdir(receiver_dir) do
|
|
241
|
+
exec("ruby", receiver_path)
|
|
242
|
+
end
|
|
243
|
+
rescue Interrupt
|
|
244
|
+
puts "\nStopping server..."
|
|
245
|
+
ensure
|
|
246
|
+
FileUtils.rm_f(PID_FILE)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def detect_project_info(path)
|
|
252
|
+
path = File.expand_path(path)
|
|
253
|
+
|
|
254
|
+
# Get git remote URL to extract repo name
|
|
255
|
+
github_repo = nil
|
|
256
|
+
if Dir.exist?(File.join(path, ".git"))
|
|
257
|
+
remote_url, _, status = Open3.capture3("git", "remote", "get-url", "origin", chdir: path)
|
|
258
|
+
if status.success?
|
|
259
|
+
# Parse GitHub repo from URL (supports both HTTPS and SSH)
|
|
260
|
+
if remote_url =~ %r{github\.com[/:](.+?)\.git}
|
|
261
|
+
github_repo = Regexp.last_match(1)
|
|
262
|
+
elsif remote_url =~ %r{github\.com[/:](.+)}
|
|
263
|
+
github_repo = Regexp.last_match(1).chomp
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Suggest project key from directory name
|
|
269
|
+
project_key = File.basename(path).downcase.gsub(/[^a-z0-9-]/, "-")
|
|
270
|
+
|
|
271
|
+
{
|
|
272
|
+
"repo_path" => path,
|
|
273
|
+
"github_repo" => github_repo,
|
|
274
|
+
"project_key" => project_key
|
|
275
|
+
}
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def register_project(options)
|
|
279
|
+
path = options[:path] || Dir.pwd
|
|
280
|
+
info = detect_project_info(path)
|
|
281
|
+
|
|
282
|
+
projects = load_projects
|
|
283
|
+
|
|
284
|
+
# Interactive prompts
|
|
285
|
+
puts "Registering project from: #{info["repo_path"]}"
|
|
286
|
+
puts
|
|
287
|
+
|
|
288
|
+
print "Project key [#{info["project_key"]}]: "
|
|
289
|
+
project_key = $stdin.gets.chomp
|
|
290
|
+
project_key = info["project_key"] if project_key.empty?
|
|
291
|
+
|
|
292
|
+
if projects.key?(project_key) && !options[:force]
|
|
293
|
+
puts "Error: Project '#{project_key}' already exists. Use --force to overwrite."
|
|
294
|
+
exit 1
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
print "Fizzy tags (comma-separated) [#{project_key}]: "
|
|
298
|
+
tags_input = $stdin.gets.chomp
|
|
299
|
+
fizzy_tags = tags_input.empty? ? [project_key] : tags_input.split(",").map(&:strip)
|
|
300
|
+
|
|
301
|
+
print "GitHub repo [#{info["github_repo"]}]: "
|
|
302
|
+
github_repo = $stdin.gets.chomp
|
|
303
|
+
github_repo = info["github_repo"] if github_repo.empty?
|
|
304
|
+
|
|
305
|
+
print "CLI provider [kiro]: "
|
|
306
|
+
cli_provider = $stdin.gets.chomp
|
|
307
|
+
cli_provider = "kiro" if cli_provider.empty?
|
|
308
|
+
|
|
309
|
+
print "Default model [auto]: "
|
|
310
|
+
agent_model = $stdin.gets.chomp
|
|
311
|
+
agent_model = "auto" if agent_model.empty?
|
|
312
|
+
|
|
313
|
+
default_agent_name = ENV.fetch("AI_AGENT_NAME", "Galen")
|
|
314
|
+
print "Agent name [#{default_agent_name}]: "
|
|
315
|
+
agent_name = $stdin.gets.chomp
|
|
316
|
+
agent_name = default_agent_name if agent_name.empty?
|
|
317
|
+
|
|
318
|
+
# Build project config
|
|
319
|
+
project_config = {
|
|
320
|
+
"repo_path" => info["repo_path"],
|
|
321
|
+
"fizzy_tags" => fizzy_tags,
|
|
322
|
+
"github_repo" => github_repo,
|
|
323
|
+
"agent_name" => agent_name,
|
|
324
|
+
"agent_model" => agent_model,
|
|
325
|
+
"cli_provider" => cli_provider
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
projects[project_key] = project_config
|
|
329
|
+
save_projects(projects)
|
|
330
|
+
|
|
331
|
+
puts
|
|
332
|
+
puts "✓ Project '#{project_key}' registered successfully!"
|
|
333
|
+
puts " Repo: #{info["repo_path"]}"
|
|
334
|
+
puts " Tags: #{fizzy_tags.join(", ")}"
|
|
335
|
+
puts " GitHub: #{github_repo}" if github_repo
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def unregister_project(project_key)
|
|
339
|
+
projects = load_projects
|
|
340
|
+
|
|
341
|
+
unless projects.key?(project_key)
|
|
342
|
+
puts "Error: Project '#{project_key}' not found."
|
|
343
|
+
exit 1
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
projects.delete(project_key)
|
|
347
|
+
save_projects(projects)
|
|
348
|
+
|
|
349
|
+
puts "✓ Project '#{project_key}' unregistered."
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def list_projects
|
|
353
|
+
projects = load_projects
|
|
354
|
+
|
|
355
|
+
if projects.empty?
|
|
356
|
+
puts "No projects registered."
|
|
357
|
+
puts "Run 'zillacore register' from a project directory to get started."
|
|
358
|
+
return
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
puts "Registered projects:"
|
|
362
|
+
puts
|
|
363
|
+
|
|
364
|
+
projects.each do |key, config|
|
|
365
|
+
default_marker = config["default"] ? " (default)" : ""
|
|
366
|
+
puts " #{key}#{default_marker}"
|
|
367
|
+
puts " Path: #{config["repo_path"]}"
|
|
368
|
+
puts " Tags: #{config["fizzy_tags"].join(", ")}"
|
|
369
|
+
puts " GitHub: #{config["github_repo"]}" if config["github_repo"]
|
|
370
|
+
puts " Agent: #{config["agent_cli"]} (model: #{config["agent_model"]})"
|
|
371
|
+
puts
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def show_project(project_key)
|
|
376
|
+
projects = load_projects
|
|
377
|
+
|
|
378
|
+
unless projects.key?(project_key)
|
|
379
|
+
puts "Error: Project '#{project_key}' not found."
|
|
380
|
+
exit 1
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
config = projects[project_key]
|
|
384
|
+
puts JSON.pretty_generate(config)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def configure_server(options)
|
|
388
|
+
config = load_config
|
|
389
|
+
|
|
390
|
+
config["server_url"] = options[:server_url] if options[:server_url]
|
|
391
|
+
|
|
392
|
+
if options[:show]
|
|
393
|
+
puts JSON.pretty_generate(config)
|
|
394
|
+
return
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Interactive configuration
|
|
398
|
+
current_url = config["server_url"] || "http://localhost:4567"
|
|
399
|
+
print "ZillaCore server URL [#{current_url}]: "
|
|
400
|
+
server_url = $stdin.gets.chomp
|
|
401
|
+
config["server_url"] = server_url.empty? ? current_url : server_url
|
|
402
|
+
|
|
403
|
+
save_config(config)
|
|
404
|
+
puts "✓ Configuration saved."
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# --- Brain management ---
|
|
408
|
+
|
|
409
|
+
BRAIN_BASE_DIR = File.join(ZILLACORE_DIR, "brain")
|
|
410
|
+
KNOWLEDGE_DIR = File.join(BRAIN_BASE_DIR, "knowledge")
|
|
411
|
+
PERSONA_BASE_DIR = File.join(BRAIN_BASE_DIR, "persona")
|
|
412
|
+
MEMORY_BASE_DIR = File.join(BRAIN_BASE_DIR, "memory")
|
|
413
|
+
KNOWLEDGE_COLLECTION = "zillacore-knowledge".freeze
|
|
414
|
+
|
|
415
|
+
def memory_dir_for(agent_name)
|
|
416
|
+
File.join(MEMORY_BASE_DIR, agent_name.downcase.gsub(/[^a-z0-9-]/, "-"))
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def persona_dir_for(agent_name)
|
|
420
|
+
File.join(PERSONA_BASE_DIR, agent_name.downcase.gsub(/[^a-z0-9-]/, "-"))
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def persona_collection_for(agent_name)
|
|
424
|
+
"#{agent_name.downcase.gsub(/[^a-z0-9-]/, "-")}-persona"
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def brain_init(agent_name)
|
|
428
|
+
persona_dir = persona_dir_for(agent_name)
|
|
429
|
+
persona_col = persona_collection_for(agent_name)
|
|
430
|
+
|
|
431
|
+
FileUtils.mkdir_p(KNOWLEDGE_DIR)
|
|
432
|
+
FileUtils.mkdir_p(persona_dir)
|
|
433
|
+
FileUtils.mkdir_p(File.join(BRAIN_BASE_DIR, "memory"))
|
|
434
|
+
|
|
435
|
+
# Check if qmd is available
|
|
436
|
+
unless system("which qmd > /dev/null 2>&1")
|
|
437
|
+
puts "✓ Brain directories created:"
|
|
438
|
+
puts " Knowledge: #{KNOWLEDGE_DIR}"
|
|
439
|
+
puts " Persona: #{persona_dir}"
|
|
440
|
+
puts ""
|
|
441
|
+
puts "⚠ qmd is not installed. Install it to enable semantic search:"
|
|
442
|
+
puts " npm install -g @tobilu/qmd"
|
|
443
|
+
puts ""
|
|
444
|
+
puts "Then run: zillacore brain init #{agent_name}"
|
|
445
|
+
return
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Set up shared knowledge collection (idempotent — qmd won't duplicate)
|
|
449
|
+
puts "Setting up shared knowledge collection..."
|
|
450
|
+
system("qmd", "collection", "add", KNOWLEDGE_DIR, "--name", KNOWLEDGE_COLLECTION)
|
|
451
|
+
system("qmd", "context", "add", "qmd://#{KNOWLEDGE_COLLECTION}",
|
|
452
|
+
"Shared technical knowledge: project conventions, coding patterns, architecture decisions, lessons learned")
|
|
453
|
+
|
|
454
|
+
# Set up per-agent persona collection
|
|
455
|
+
puts "Setting up persona collection for #{agent_name}..."
|
|
456
|
+
system("qmd", "collection", "add", persona_dir, "--name", persona_col)
|
|
457
|
+
system("qmd", "context", "add", "qmd://#{persona_col}",
|
|
458
|
+
"Communication style and personality for AI agent #{agent_name}")
|
|
459
|
+
|
|
460
|
+
# Set up card memory collection (per-agent)
|
|
461
|
+
memory_dir = memory_dir_for(agent_name)
|
|
462
|
+
FileUtils.mkdir_p(memory_dir)
|
|
463
|
+
puts "Setting up card memory collection for #{agent_name}..."
|
|
464
|
+
system("qmd", "collection", "add", memory_dir, "--name", "#{agent_name.downcase}-memory")
|
|
465
|
+
system("qmd", "context", "add", "qmd://#{agent_name.downcase}-memory",
|
|
466
|
+
"Per-card session memory for #{agent_name}: decisions, questions, answers, work status, timelines")
|
|
467
|
+
|
|
468
|
+
puts "Indexing..."
|
|
469
|
+
system("qmd", "update")
|
|
470
|
+
|
|
471
|
+
puts ""
|
|
472
|
+
puts "✓ Brain initialized for #{agent_name}"
|
|
473
|
+
puts " Knowledge (shared): #{KNOWLEDGE_DIR}"
|
|
474
|
+
puts " Memory (#{agent_name}): #{memory_dir}"
|
|
475
|
+
puts " Persona (#{agent_name}): #{persona_dir}"
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def brain_status(agent_name)
|
|
479
|
+
persona_dir = persona_dir_for(agent_name)
|
|
480
|
+
persona_col = persona_collection_for(agent_name)
|
|
481
|
+
memory_dir = memory_dir_for(agent_name)
|
|
482
|
+
|
|
483
|
+
knowledge_files = File.directory?(KNOWLEDGE_DIR) ? Dir.glob(File.join(KNOWLEDGE_DIR, "**", "*.md")) : []
|
|
484
|
+
memory_files = File.directory?(memory_dir) ? Dir.glob(File.join(memory_dir, "**", "*.md")) : []
|
|
485
|
+
persona_files = File.directory?(persona_dir) ? Dir.glob(File.join(persona_dir, "**", "*.md")) : []
|
|
486
|
+
|
|
487
|
+
if knowledge_files.empty? && memory_files.empty? && persona_files.empty?
|
|
488
|
+
puts "No brain found. Run 'zillacore brain init' to create one."
|
|
489
|
+
return
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
puts "Knowledge (shared across all agents):"
|
|
493
|
+
puts " Directory: #{KNOWLEDGE_DIR}"
|
|
494
|
+
puts " Collection: #{KNOWLEDGE_COLLECTION}"
|
|
495
|
+
puts " Files: #{knowledge_files.size}"
|
|
496
|
+
knowledge_files.each do |f|
|
|
497
|
+
puts " #{f.sub("#{KNOWLEDGE_DIR}/", "")} (#{File.size(f)} bytes)"
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
puts ""
|
|
501
|
+
puts "Memory (#{agent_name}, per-card session history):"
|
|
502
|
+
puts " Directory: #{memory_dir}"
|
|
503
|
+
puts " Collection: #{agent_name.downcase}-memory"
|
|
504
|
+
puts " Files: #{memory_files.size}"
|
|
505
|
+
memory_files.each do |f|
|
|
506
|
+
puts " #{f.sub("#{memory_dir}/", "")} (#{File.size(f)} bytes)"
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
puts ""
|
|
510
|
+
puts "Persona (#{agent_name}):"
|
|
511
|
+
puts " Directory: #{persona_dir}"
|
|
512
|
+
puts " Collection: #{persona_col}"
|
|
513
|
+
puts " Files: #{persona_files.size}"
|
|
514
|
+
persona_files.each do |f|
|
|
515
|
+
puts " #{f.sub("#{persona_dir}/", "")} (#{File.size(f)} bytes)"
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def brain_search(query, scope: :knowledge, agent_name: nil)
|
|
520
|
+
unless system("which qmd > /dev/null 2>&1")
|
|
521
|
+
puts "Error: qmd is not installed. Run: npm install -g @tobilu/qmd"
|
|
522
|
+
exit 1
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
collection = case scope
|
|
526
|
+
when :persona
|
|
527
|
+
agent_name ||= ENV.fetch("AI_AGENT_NAME", "Galen")
|
|
528
|
+
persona_collection_for(agent_name)
|
|
529
|
+
else
|
|
530
|
+
KNOWLEDGE_COLLECTION
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
exec("qmd", "search", query, "-c", collection)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def brain_list
|
|
537
|
+
puts "Knowledge (shared):"
|
|
538
|
+
if File.directory?(KNOWLEDGE_DIR)
|
|
539
|
+
files = Dir.glob(File.join(KNOWLEDGE_DIR, "**", "*.md"))
|
|
540
|
+
puts " #{KNOWLEDGE_DIR} (#{files.size} files)"
|
|
541
|
+
else
|
|
542
|
+
puts " Not initialized"
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
puts ""
|
|
546
|
+
puts "Memory (per-agent, per-card):"
|
|
547
|
+
if File.directory?(MEMORY_BASE_DIR)
|
|
548
|
+
agent_dirs = Dir.children(MEMORY_BASE_DIR).select { |d| File.directory?(File.join(MEMORY_BASE_DIR, d)) }
|
|
549
|
+
if agent_dirs.empty?
|
|
550
|
+
puts " None"
|
|
551
|
+
else
|
|
552
|
+
agent_dirs.each do |d|
|
|
553
|
+
agent_memory_dir = File.join(MEMORY_BASE_DIR, d)
|
|
554
|
+
files = Dir.glob(File.join(agent_memory_dir, "**", "*.md"))
|
|
555
|
+
puts " #{d}: #{files.size} files"
|
|
556
|
+
end
|
|
557
|
+
end
|
|
558
|
+
else
|
|
559
|
+
puts " Not initialized"
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
puts ""
|
|
563
|
+
puts "Personas:"
|
|
564
|
+
if File.directory?(PERSONA_BASE_DIR)
|
|
565
|
+
dirs = Dir.children(PERSONA_BASE_DIR).select { |d| File.directory?(File.join(PERSONA_BASE_DIR, d)) }
|
|
566
|
+
if dirs.empty?
|
|
567
|
+
puts " None"
|
|
568
|
+
else
|
|
569
|
+
dirs.each do |d|
|
|
570
|
+
dir = File.join(PERSONA_BASE_DIR, d)
|
|
571
|
+
files = Dir.glob(File.join(dir, "**", "*.md"))
|
|
572
|
+
puts " #{d} (#{files.size} files) — #{dir}"
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
else
|
|
576
|
+
puts " Not initialized"
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
puts ""
|
|
580
|
+
puts "Run 'zillacore brain init <agent-name>' to set up a new agent."
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# Main CLI
|
|
584
|
+
options = {}
|
|
585
|
+
subcommand = ARGV.shift
|
|
586
|
+
|
|
587
|
+
case subcommand
|
|
588
|
+
when "setup"
|
|
589
|
+
puts "🔧 ZillaCore Setup"
|
|
590
|
+
puts ""
|
|
591
|
+
|
|
592
|
+
# Create directory structure
|
|
593
|
+
dirs = %w[brain/knowledge brain/persona brain/memory handlers plans tmp/discord/draft tmp/discord/posted]
|
|
594
|
+
dirs.each do |dir|
|
|
595
|
+
path = File.join(ZILLACORE_DIR, dir)
|
|
596
|
+
FileUtils.mkdir_p(path)
|
|
597
|
+
end
|
|
598
|
+
puts "✓ Created ~/.zillacore/ directory structure"
|
|
599
|
+
|
|
600
|
+
# Copy example configs
|
|
601
|
+
templates_dir = File.join(ZILLACORE_ROOT, "templates")
|
|
602
|
+
if Dir.exist?(templates_dir)
|
|
603
|
+
copied = 0
|
|
604
|
+
Dir.glob(File.join(templates_dir, "*.example")).each do |template|
|
|
605
|
+
dest = File.join(ZILLACORE_DIR, File.basename(template, ".example"))
|
|
606
|
+
unless File.exist?(dest)
|
|
607
|
+
FileUtils.cp(template, dest)
|
|
608
|
+
copied += 1
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
puts "✓ Copied #{copied} example config(s) to ~/.zillacore/ (existing files preserved)"
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
puts ""
|
|
615
|
+
puts "Next steps:"
|
|
616
|
+
puts " 1. Edit config files in ~/.zillacore/ with your secrets and IDs"
|
|
617
|
+
puts " 2. Create agent configs in ~/.kiro/agents/"
|
|
618
|
+
puts " 3. Register projects: cd ~/Code/myproject && zillacore register"
|
|
619
|
+
puts " 4. Initialize brain: zillacore brain init"
|
|
620
|
+
puts " 5. Start server: zillacore server"
|
|
621
|
+
|
|
622
|
+
when "register", "add"
|
|
623
|
+
OptionParser.new do |opts|
|
|
624
|
+
opts.banner = "Usage: zillacore register [options]"
|
|
625
|
+
opts.on("-p", "--path PATH", "Project path (default: current directory)")
|
|
626
|
+
opts.on("-f", "--force", "Overwrite existing project")
|
|
627
|
+
end.parse!(into: options)
|
|
628
|
+
|
|
629
|
+
register_project(options)
|
|
630
|
+
|
|
631
|
+
when "unregister", "remove", "rm"
|
|
632
|
+
if ARGV.empty?
|
|
633
|
+
puts "Usage: zillacore unregister <project-key>"
|
|
634
|
+
exit 1
|
|
635
|
+
end
|
|
636
|
+
unregister_project(ARGV[0])
|
|
637
|
+
|
|
638
|
+
when "list", "ls"
|
|
639
|
+
list_projects
|
|
640
|
+
|
|
641
|
+
when "projects"
|
|
642
|
+
# Support "zillacore projects list" as an alias for "zillacore list"
|
|
643
|
+
projects_cmd = ARGV.shift
|
|
644
|
+
case projects_cmd
|
|
645
|
+
when "list", "ls", nil
|
|
646
|
+
list_projects
|
|
647
|
+
when "show"
|
|
648
|
+
if ARGV.empty?
|
|
649
|
+
puts "Usage: zillacore projects show <project-key>"
|
|
650
|
+
exit 1
|
|
651
|
+
end
|
|
652
|
+
show_project(ARGV[0])
|
|
653
|
+
when "default"
|
|
654
|
+
project_key = ARGV[0]
|
|
655
|
+
unless project_key
|
|
656
|
+
puts "Usage: zillacore projects default <project-key>"
|
|
657
|
+
puts " Sets the default project for project-specific setup (e.g., .env copying)"
|
|
658
|
+
exit 1
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
projects = load_projects
|
|
662
|
+
unless projects.key?(project_key)
|
|
663
|
+
puts "Error: Project '#{project_key}' not found."
|
|
664
|
+
puts "Available projects: #{projects.keys.join(", ")}"
|
|
665
|
+
exit 1
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# Remove default flag from all projects, then set it on the target
|
|
669
|
+
projects.each_value { |config| config.delete("default") }
|
|
670
|
+
projects[project_key]["default"] = true
|
|
671
|
+
save_projects(projects)
|
|
672
|
+
|
|
673
|
+
puts "✓ Set '#{project_key}' as the default project"
|
|
674
|
+
else
|
|
675
|
+
puts "Unknown projects command: #{projects_cmd}"
|
|
676
|
+
puts "Available: list, show, default"
|
|
677
|
+
exit 1
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
when "show"
|
|
681
|
+
if ARGV.empty?
|
|
682
|
+
puts "Usage: zillacore show <project-key>"
|
|
683
|
+
exit 1
|
|
684
|
+
end
|
|
685
|
+
show_project(ARGV[0])
|
|
686
|
+
|
|
687
|
+
when "config"
|
|
688
|
+
OptionParser.new do |opts|
|
|
689
|
+
opts.banner = "Usage: zillacore config [options]"
|
|
690
|
+
opts.on("-s", "--server-url URL", "ZillaCore server URL")
|
|
691
|
+
opts.on("--show", "Show current configuration")
|
|
692
|
+
end.parse!(into: options)
|
|
693
|
+
|
|
694
|
+
configure_server(options)
|
|
695
|
+
|
|
696
|
+
when "server", "s"
|
|
697
|
+
OptionParser.new do |opts|
|
|
698
|
+
opts.banner = "Usage: zillacore server [options]"
|
|
699
|
+
opts.on("-d", "--daemon", "Run server in background (detached)")
|
|
700
|
+
end.parse!(into: options)
|
|
701
|
+
|
|
702
|
+
start_server(daemon: options[:daemon])
|
|
703
|
+
|
|
704
|
+
when "stop"
|
|
705
|
+
stop_server
|
|
706
|
+
|
|
707
|
+
when "restart"
|
|
708
|
+
puts "Restarting ZillaCore server..."
|
|
709
|
+
wait_for_agents_to_finish if find_server_pid
|
|
710
|
+
if stop_server
|
|
711
|
+
# Wait a bit longer to ensure the process is fully dead
|
|
712
|
+
sleep 2
|
|
713
|
+
|
|
714
|
+
# Double-check that the process is really gone
|
|
715
|
+
if find_server_pid
|
|
716
|
+
puts "Warning: Old server process still running, forcing kill..."
|
|
717
|
+
pid = find_server_pid
|
|
718
|
+
begin
|
|
719
|
+
Process.kill("KILL", pid)
|
|
720
|
+
sleep 1
|
|
721
|
+
rescue Errno::ESRCH
|
|
722
|
+
# Already dead
|
|
723
|
+
end
|
|
724
|
+
FileUtils.rm_f(PID_FILE)
|
|
725
|
+
end
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
start_server(daemon: true)
|
|
729
|
+
|
|
730
|
+
when "logs", "log"
|
|
731
|
+
# Tail the server log
|
|
732
|
+
log_file = File.join(ZILLACORE_ROOT, "tmp", "zillacore-server.log")
|
|
733
|
+
|
|
734
|
+
unless File.exist?(log_file)
|
|
735
|
+
puts "No log file found at #{log_file}"
|
|
736
|
+
puts "Is the server running? Check with: zillacore status"
|
|
737
|
+
exit 1
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
exec("tail", "-f", log_file)
|
|
741
|
+
|
|
742
|
+
when "status"
|
|
743
|
+
pid = find_server_pid
|
|
744
|
+
if pid
|
|
745
|
+
puts "ZillaCore server is running (PID: #{pid})"
|
|
746
|
+
else
|
|
747
|
+
puts "ZillaCore server is not running"
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
when "path"
|
|
751
|
+
puts ZILLACORE_DIR
|
|
752
|
+
|
|
753
|
+
when "discord"
|
|
754
|
+
discord_cmd = ARGV.shift
|
|
755
|
+
discord_config_file = File.join(ZILLACORE_DIR, "discord.json")
|
|
756
|
+
agent_registry_file = File.join(ZILLACORE_DIR, "agents.json")
|
|
757
|
+
|
|
758
|
+
case discord_cmd
|
|
759
|
+
when "config"
|
|
760
|
+
if File.exist?(discord_config_file)
|
|
761
|
+
puts File.read(discord_config_file)
|
|
762
|
+
else
|
|
763
|
+
puts "No Discord config found at #{discord_config_file}"
|
|
764
|
+
puts "Create one with: zillacore discord map <channel-id> <project>"
|
|
765
|
+
puts "Or copy discord.json.example to #{discord_config_file}"
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
when "map"
|
|
769
|
+
channel_id = ARGV[0]
|
|
770
|
+
project_key = ARGV[1]
|
|
771
|
+
|
|
772
|
+
unless channel_id && project_key
|
|
773
|
+
puts "Usage: zillacore discord map <channel-id> <project-key>"
|
|
774
|
+
exit 1
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
config = File.exist?(discord_config_file) ? JSON.parse(File.read(discord_config_file)) : { "channel_mappings" => {} }
|
|
778
|
+
config["channel_mappings"] ||= {}
|
|
779
|
+
config["channel_mappings"][channel_id] = { "project" => project_key }
|
|
780
|
+
|
|
781
|
+
ensure_zillacore_dir
|
|
782
|
+
File.write(discord_config_file, JSON.pretty_generate(config))
|
|
783
|
+
puts "✓ Mapped channel #{channel_id} → project '#{project_key}'"
|
|
784
|
+
|
|
785
|
+
when "default"
|
|
786
|
+
project_key = ARGV[0]
|
|
787
|
+
|
|
788
|
+
unless project_key
|
|
789
|
+
puts "Usage: zillacore discord default <project-key>"
|
|
790
|
+
exit 1
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
config = File.exist?(discord_config_file) ? JSON.parse(File.read(discord_config_file)) : { "channel_mappings" => {} }
|
|
794
|
+
config["default_project"] = project_key
|
|
795
|
+
|
|
796
|
+
ensure_zillacore_dir
|
|
797
|
+
File.write(discord_config_file, JSON.pretty_generate(config))
|
|
798
|
+
puts "✓ Default project: #{project_key}"
|
|
799
|
+
|
|
800
|
+
when "token"
|
|
801
|
+
agent_key = ARGV[0]
|
|
802
|
+
token = ARGV[1]
|
|
803
|
+
|
|
804
|
+
unless agent_key && token
|
|
805
|
+
puts "Usage: zillacore discord token <agent-key> <bot-token>"
|
|
806
|
+
puts " Sets the DISCORD_BOT_TOKEN env var for an agent in the registry."
|
|
807
|
+
puts " Example: zillacore discord token galen Bot_TOKEN_HERE"
|
|
808
|
+
exit 1
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
812
|
+
registry[agent_key] ||= {}
|
|
813
|
+
registry[agent_key]["env"] ||= {}
|
|
814
|
+
registry[agent_key]["env"]["DISCORD_BOT_TOKEN"] = token
|
|
815
|
+
|
|
816
|
+
ensure_zillacore_dir
|
|
817
|
+
File.write(agent_registry_file, JSON.pretty_generate(registry))
|
|
818
|
+
puts "✓ Set DISCORD_BOT_TOKEN for '#{agent_key}'"
|
|
819
|
+
|
|
820
|
+
when "status"
|
|
821
|
+
config = load_config
|
|
822
|
+
server_url = config["server_url"] || "http://localhost:4567"
|
|
823
|
+
begin
|
|
824
|
+
uri = URI("#{server_url}/api/discord")
|
|
825
|
+
response = Net::HTTP.get_response(uri)
|
|
826
|
+
data = JSON.parse(response.body)
|
|
827
|
+
if data["enabled"]
|
|
828
|
+
bots = data["bots"] || {}
|
|
829
|
+
if bots.empty?
|
|
830
|
+
puts "Discord: enabled but no bots configured"
|
|
831
|
+
else
|
|
832
|
+
puts "Discord bots:"
|
|
833
|
+
bots.each do |agent, info|
|
|
834
|
+
puts " #{agent}: #{info["status"]} (user_id: #{info["user_id"] || "n/a"})"
|
|
835
|
+
end
|
|
836
|
+
end
|
|
837
|
+
puts "Default project: #{data.dig("config", "default_project") || "none"}"
|
|
838
|
+
puts "Channel mappings: #{data.dig("config", "channel_mappings")}"
|
|
839
|
+
else
|
|
840
|
+
puts "Discord: disabled (#{data["reason"]})"
|
|
841
|
+
end
|
|
842
|
+
rescue StandardError => e
|
|
843
|
+
puts "Could not reach server at #{server_url}: #{e.message}"
|
|
844
|
+
puts "Is the server running? Check with: zillacore status"
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
when "agents"
|
|
848
|
+
registry = File.exist?(agent_registry_file) ? JSON.parse(File.read(agent_registry_file)) : {}
|
|
849
|
+
agents_with_tokens = registry.select { |_k, v| v.is_a?(Hash) && (v.dig("env", "DISCORD_BOT_TOKEN") || v["discord_bot_token"]) }
|
|
850
|
+
if agents_with_tokens.empty?
|
|
851
|
+
puts "No agents have Discord bot tokens configured."
|
|
852
|
+
puts "Add one with: zillacore discord token <agent-key> <bot-token>"
|
|
853
|
+
else
|
|
854
|
+
puts "Agents with Discord bots:"
|
|
855
|
+
agents_with_tokens.each do |key, entry|
|
|
856
|
+
display = entry["fizzy_name"] || key.capitalize
|
|
857
|
+
token = entry.dig("env", "DISCORD_BOT_TOKEN") || entry["discord_bot_token"]
|
|
858
|
+
token_preview = "#{token[0..10]}..."
|
|
859
|
+
puts " #{display} (#{key}): #{token_preview}"
|
|
860
|
+
end
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
when "owner"
|
|
864
|
+
discord_id = ARGV[0]
|
|
865
|
+
config = File.exist?(discord_config_file) ? JSON.parse(File.read(discord_config_file)) : {}
|
|
866
|
+
if discord_id
|
|
867
|
+
config["owner_discord_id"] = discord_id
|
|
868
|
+
ensure_zillacore_dir
|
|
869
|
+
File.write(discord_config_file, JSON.pretty_generate(config))
|
|
870
|
+
puts "✓ Owner set to #{discord_id}"
|
|
871
|
+
elsif config["owner_discord_id"]
|
|
872
|
+
puts "Owner: #{config["owner_discord_id"]}"
|
|
873
|
+
else
|
|
874
|
+
puts "No owner set."
|
|
875
|
+
puts "Usage: zillacore discord owner <discord-user-id>"
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
else
|
|
879
|
+
puts <<~HELP
|
|
880
|
+
Usage: zillacore discord <command>
|
|
881
|
+
|
|
882
|
+
Commands:
|
|
883
|
+
config Show Discord config
|
|
884
|
+
default <project> Set default project for all channels
|
|
885
|
+
map <channel-id> <project> Map a specific channel to a project
|
|
886
|
+
owner [<discord-user-id>] Set/show machine owner (for version notifications)
|
|
887
|
+
token <agent-key> <bot-token> Set Discord bot token for an agent
|
|
888
|
+
agents List agents with Discord bot tokens
|
|
889
|
+
status Check Discord bot status (via server API)
|
|
890
|
+
|
|
891
|
+
Each agent gets its own Discord bot. Users @mention @Galen or @GLaDOS
|
|
892
|
+
directly in Discord — no shared bot needed.
|
|
893
|
+
|
|
894
|
+
Setup:
|
|
895
|
+
1. Create a Discord bot per agent at https://discord.com/developers/applications
|
|
896
|
+
2. Enable MESSAGE CONTENT intent in each bot's settings
|
|
897
|
+
3. Invite each bot with permissions: Send Messages, Create Public Threads,
|
|
898
|
+
Send Messages in Threads, Add Reactions, Read Message History
|
|
899
|
+
4. Register each bot token:
|
|
900
|
+
zillacore discord token galen "BOT_TOKEN_FOR_GALEN"
|
|
901
|
+
zillacore discord token glados "BOT_TOKEN_FOR_GLADOS"
|
|
902
|
+
5. Set a default project:
|
|
903
|
+
zillacore discord default marketplace
|
|
904
|
+
6. Start the server (all bots connect automatically):
|
|
905
|
+
zillacore server
|
|
906
|
+
|
|
907
|
+
Config file: #{discord_config_file}
|
|
908
|
+
Agent registry: #{agent_registry_file}
|
|
909
|
+
HELP
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
when "card-map"
|
|
913
|
+
card_map_file = File.join(ZILLACORE_DIR, "card_map.json")
|
|
914
|
+
cm_cmd = ARGV.shift
|
|
915
|
+
case cm_cmd
|
|
916
|
+
when "clean"
|
|
917
|
+
unless File.exist?(card_map_file)
|
|
918
|
+
puts "No card_map.json found."
|
|
919
|
+
exit 0
|
|
920
|
+
end
|
|
921
|
+
map = JSON.parse(File.read(card_map_file))
|
|
922
|
+
before = map.size
|
|
923
|
+
# Remove entries with no valid worktree on disk
|
|
924
|
+
map.reject! { |_id, info| info["worktree"].nil? || !File.directory?(info["worktree"].to_s) }
|
|
925
|
+
after = map.size
|
|
926
|
+
removed = before - after
|
|
927
|
+
File.write(card_map_file, JSON.pretty_generate(map))
|
|
928
|
+
puts "Cleaned card map: removed #{removed} stale entries (#{before} → #{after})"
|
|
929
|
+
when "list", "ls", nil
|
|
930
|
+
unless File.exist?(card_map_file)
|
|
931
|
+
puts "No card_map.json found."
|
|
932
|
+
exit 0
|
|
933
|
+
end
|
|
934
|
+
map = JSON.parse(File.read(card_map_file))
|
|
935
|
+
if map.empty?
|
|
936
|
+
puts "Card map is empty."
|
|
937
|
+
else
|
|
938
|
+
map.each_value do |info|
|
|
939
|
+
num = info["number"] ? "##{info["number"]}" : "(no number)"
|
|
940
|
+
agent = info["agent"] || "(no agent)"
|
|
941
|
+
worktree_exists = info["worktree"] && File.directory?(info["worktree"].to_s) ? "✓" : "✗"
|
|
942
|
+
puts "#{num.ljust(8)} #{agent.ljust(18)} #{worktree_exists} #{info["worktree"] || "(none)"}"
|
|
943
|
+
end
|
|
944
|
+
end
|
|
945
|
+
else
|
|
946
|
+
puts "Usage: zillacore card-map [clean|list]"
|
|
947
|
+
puts " clean Remove stale entries (missing worktrees)"
|
|
948
|
+
puts " list Show all card map entries"
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
when "brain"
|
|
952
|
+
brain_cmd = ARGV.shift
|
|
953
|
+
case brain_cmd
|
|
954
|
+
when "init"
|
|
955
|
+
agent = ARGV[0] || ENV.fetch("AI_AGENT_NAME", "Galen")
|
|
956
|
+
brain_init(agent)
|
|
957
|
+
when "status", "show"
|
|
958
|
+
agent = ARGV[0] || ENV.fetch("AI_AGENT_NAME", "Galen")
|
|
959
|
+
brain_status(agent)
|
|
960
|
+
when "search", "query"
|
|
961
|
+
scope = :knowledge
|
|
962
|
+
agent_name = ENV.fetch("AI_AGENT_NAME", "Galen")
|
|
963
|
+
if ARGV.include?("--persona")
|
|
964
|
+
ARGV.delete("--persona")
|
|
965
|
+
scope = :persona
|
|
966
|
+
end
|
|
967
|
+
if ARGV.include?("--agent")
|
|
968
|
+
idx = ARGV.index("--agent")
|
|
969
|
+
agent_name = ARGV.delete_at(idx + 1)
|
|
970
|
+
ARGV.delete_at(idx)
|
|
971
|
+
end
|
|
972
|
+
if ARGV.empty?
|
|
973
|
+
puts "Usage: zillacore brain search [--persona] [--agent <name>] <query>"
|
|
974
|
+
exit 1
|
|
975
|
+
end
|
|
976
|
+
brain_search(ARGV.join(" "), scope: scope, agent_name: agent_name)
|
|
977
|
+
when "list", "ls"
|
|
978
|
+
brain_list
|
|
979
|
+
when "path"
|
|
980
|
+
what = ARGV[0]
|
|
981
|
+
if what == "persona"
|
|
982
|
+
agent = ARGV[1] || ENV.fetch("AI_AGENT_NAME", "Galen")
|
|
983
|
+
puts persona_dir_for(agent)
|
|
984
|
+
else
|
|
985
|
+
puts KNOWLEDGE_DIR
|
|
986
|
+
end
|
|
987
|
+
else
|
|
988
|
+
puts <<~HELP
|
|
989
|
+
Usage: zillacore brain <command> [options]
|
|
990
|
+
|
|
991
|
+
Commands:
|
|
992
|
+
init [agent-name] Initialize brain (shared knowledge + agent persona)
|
|
993
|
+
status [agent-name] Show brain status and files
|
|
994
|
+
search [--persona] [--agent name] <q> Search knowledge (default) or persona
|
|
995
|
+
list List knowledge and all personas
|
|
996
|
+
path Show knowledge directory
|
|
997
|
+
path persona [agent-name] Show persona directory
|
|
998
|
+
|
|
999
|
+
The brain has two parts:
|
|
1000
|
+
knowledge/ — shared across all agents (project conventions, patterns, lessons)
|
|
1001
|
+
persona/ — per-agent (communication style, tone, personality)
|
|
1002
|
+
|
|
1003
|
+
Knowledge is automatically queried when agents do work.
|
|
1004
|
+
Persona is only queried when agents write comments.
|
|
1005
|
+
|
|
1006
|
+
Examples:
|
|
1007
|
+
zillacore brain init # Init for default agent
|
|
1008
|
+
zillacore brain init Nova # Init persona for Nova
|
|
1009
|
+
zillacore brain search "ruby style" # Search shared knowledge
|
|
1010
|
+
zillacore brain search --persona "tone" # Search persona
|
|
1011
|
+
zillacore brain list # List everything
|
|
1012
|
+
HELP
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
when "cron"
|
|
1016
|
+
cron_cmd = ARGV.shift
|
|
1017
|
+
config = load_config
|
|
1018
|
+
server_url = config["server_url"] || "http://localhost:4567"
|
|
1019
|
+
|
|
1020
|
+
case cron_cmd
|
|
1021
|
+
when "add", "create"
|
|
1022
|
+
# Parse arguments
|
|
1023
|
+
schedule = nil
|
|
1024
|
+
agent = ENV.fetch("AI_AGENT_NAME", "Galen")
|
|
1025
|
+
project = nil
|
|
1026
|
+
model = nil
|
|
1027
|
+
effort = nil
|
|
1028
|
+
discord_channel_id = nil
|
|
1029
|
+
forum_title = nil
|
|
1030
|
+
repeat_count = nil
|
|
1031
|
+
prompt = nil
|
|
1032
|
+
script = nil
|
|
1033
|
+
|
|
1034
|
+
OptionParser.new do |opts|
|
|
1035
|
+
opts.banner = "Usage: zillacore cron add [options] \"<prompt or --script path>\""
|
|
1036
|
+
opts.on("-s", "--schedule EXPR", "Cron expression, @shorthand, or one-time schedule") { |v| schedule = v }
|
|
1037
|
+
opts.on("-a", "--agent NAME", "Agent name (default: $AI_AGENT_NAME)") { |v| agent = v }
|
|
1038
|
+
opts.on("-p", "--project KEY", "Project key") { |v| project = v }
|
|
1039
|
+
opts.on("-m", "--model MODEL", "Model to use (opus, sonnet, haiku, auto)") { |v| model = v }
|
|
1040
|
+
opts.on("-e", "--effort LEVEL", "Effort level (low, medium, high, xhigh, max)") { |v| effort = v }
|
|
1041
|
+
opts.on("-d", "--discord CHANNEL_ID", "Discord channel ID to post results") { |v| discord_channel_id = v }
|
|
1042
|
+
opts.on("-t", "--title TITLE", "Forum post title (for forum channels)") { |v| forum_title = v }
|
|
1043
|
+
opts.on("-r", "--repeat COUNT", Integer, "Number of times to repeat (for recurring schedules)") { |v| repeat_count = v }
|
|
1044
|
+
opts.on("--script PATH", "Run script directly (no agent, output to Discord)") { |v| script = v }
|
|
1045
|
+
end.parse!
|
|
1046
|
+
|
|
1047
|
+
prompt = ARGV.join(" ") unless script
|
|
1048
|
+
|
|
1049
|
+
unless schedule && project && (prompt&.length&.> 0 || script)
|
|
1050
|
+
puts "Error: Missing required arguments"
|
|
1051
|
+
puts ""
|
|
1052
|
+
puts "Usage examples:"
|
|
1053
|
+
puts " # Agent with prompt (recurring)"
|
|
1054
|
+
puts " zillacore cron add -s \"0 9 * * *\" -p marketplace \"Daily standup\""
|
|
1055
|
+
puts ""
|
|
1056
|
+
puts " # Script mode (no agent, direct execution)"
|
|
1057
|
+
puts " zillacore cron add -s \"0 9 * * *\" -p zillacore --script ~/.zillacore/scripts/report.sh -d 1234567890"
|
|
1058
|
+
puts ""
|
|
1059
|
+
puts " # Recurring (shorthand)"
|
|
1060
|
+
puts " zillacore cron add -s @daily -p zillacore \"Security audit\""
|
|
1061
|
+
puts ""
|
|
1062
|
+
puts " # One-time (natural language)"
|
|
1063
|
+
puts " zillacore cron add -s \"tomorrow at 9am\" -p marketplace \"Reminder\""
|
|
1064
|
+
puts " zillacore cron add -s \"in 2 hours\" -p zillacore \"Follow up\""
|
|
1065
|
+
puts " zillacore cron add -s \"next monday at 3pm\" -p marketplace \"Weekly review\""
|
|
1066
|
+
puts ""
|
|
1067
|
+
puts " # Recurring with repeat limit"
|
|
1068
|
+
puts " zillacore cron add -s \"0 9 * * *\" -r 7 -p marketplace \"Daily reminder for 7 days\""
|
|
1069
|
+
puts " zillacore cron add -s @daily -r 3 -p zillacore \"Reminder for 3 days\""
|
|
1070
|
+
exit 1
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
# Generate ID from prompt or script
|
|
1074
|
+
id = (script ? File.basename(script, ".*") : prompt).downcase.gsub(/[^a-z0-9]+/, "-")[0..30]
|
|
1075
|
+
|
|
1076
|
+
# Call server API
|
|
1077
|
+
uri = URI("#{server_url}/api/cron/add")
|
|
1078
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
1079
|
+
payload = { id: id, schedule: schedule, agent: agent, project: project }
|
|
1080
|
+
payload[:prompt] = prompt if prompt
|
|
1081
|
+
payload[:script] = script if script
|
|
1082
|
+
payload[:model] = model if model
|
|
1083
|
+
payload[:effort] = effort if effort
|
|
1084
|
+
payload[:discord_channel_id] = discord_channel_id if discord_channel_id
|
|
1085
|
+
payload[:forum_title] = forum_title if forum_title
|
|
1086
|
+
payload[:repeat_count] = repeat_count if repeat_count
|
|
1087
|
+
req.body = payload.to_json
|
|
1088
|
+
|
|
1089
|
+
begin
|
|
1090
|
+
response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
|
|
1091
|
+
data = JSON.parse(response.body)
|
|
1092
|
+
if data["success"]
|
|
1093
|
+
puts "✓ Cron job added: #{id}"
|
|
1094
|
+
puts " Schedule: #{schedule}"
|
|
1095
|
+
puts " Agent: #{script ? "none (script mode)" : agent}"
|
|
1096
|
+
puts " Project: #{project}"
|
|
1097
|
+
puts " Model: #{model || "default"}" unless script
|
|
1098
|
+
puts " Effort: #{effort || "default"}" unless script
|
|
1099
|
+
puts " Discord: #{discord_channel_id || "none"}"
|
|
1100
|
+
puts " Forum title: #{forum_title || "auto"}" if discord_channel_id
|
|
1101
|
+
puts " Repeat: #{repeat_count ? "#{repeat_count} times" : "unlimited"}"
|
|
1102
|
+
puts script ? " Script: #{script}" : " Prompt: #{prompt}"
|
|
1103
|
+
else
|
|
1104
|
+
puts "Error: #{data["error"]}"
|
|
1105
|
+
exit 1
|
|
1106
|
+
end
|
|
1107
|
+
rescue StandardError => e
|
|
1108
|
+
puts "Could not reach server: #{e.message}"
|
|
1109
|
+
puts "Is the server running? Check with: zillacore status"
|
|
1110
|
+
exit 1
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
when "list", "ls"
|
|
1114
|
+
begin
|
|
1115
|
+
uri = URI("#{server_url}/api/cron")
|
|
1116
|
+
response = Net::HTTP.get_response(uri)
|
|
1117
|
+
data = JSON.parse(response.body)
|
|
1118
|
+
|
|
1119
|
+
if data["jobs"].empty?
|
|
1120
|
+
puts "No cron jobs configured."
|
|
1121
|
+
puts "Add one with: zillacore cron add -s \"0 9 * * *\" -p marketplace \"Daily summary\""
|
|
1122
|
+
puts "Or one-time: zillacore cron add -s \"tomorrow at 9am\" -p marketplace \"Reminder\""
|
|
1123
|
+
else
|
|
1124
|
+
puts "Cron jobs:"
|
|
1125
|
+
data["jobs"].each do |id, job|
|
|
1126
|
+
status = job["enabled"] ? "✓" : "✗"
|
|
1127
|
+
last_run = job["last_run"] ? Time.parse(job["last_run"]).strftime("%Y-%m-%d %H:%M") : "never"
|
|
1128
|
+
execution_count = job["execution_count"] || 0
|
|
1129
|
+
|
|
1130
|
+
# Show one-time jobs differently
|
|
1131
|
+
schedule_display = job["schedule"]
|
|
1132
|
+
if job["parsed"] && job["parsed"]["one_time"]
|
|
1133
|
+
target_time = Time.parse(job["parsed"]["timestamp"])
|
|
1134
|
+
schedule_display = "#{job["schedule"]} (#{target_time.strftime("%Y-%m-%d %H:%M")})"
|
|
1135
|
+
schedule_display += " [COMPLETED]" unless job["enabled"]
|
|
1136
|
+
elsif job["repeat_count"]
|
|
1137
|
+
schedule_display = "#{job["schedule"]} (#{execution_count}/#{job["repeat_count"]} runs)"
|
|
1138
|
+
schedule_display += " [COMPLETED]" unless job["enabled"]
|
|
1139
|
+
end
|
|
1140
|
+
|
|
1141
|
+
puts " #{status} #{id}"
|
|
1142
|
+
puts " Schedule: #{schedule_display}"
|
|
1143
|
+
puts " Agent: #{job["script"] ? "none (script mode)" : job["agent"]} | Project: #{job["project"]}"
|
|
1144
|
+
puts " Discord: #{job["discord_channel_id"]}" if job["discord_channel_id"]
|
|
1145
|
+
puts job["script"] ? " Script: #{job["script"]}" : " Prompt: #{job["prompt"]}"
|
|
1146
|
+
puts " Last run: #{last_run}"
|
|
1147
|
+
puts
|
|
1148
|
+
end
|
|
1149
|
+
end
|
|
1150
|
+
rescue StandardError => e
|
|
1151
|
+
puts "Could not reach server: #{e.message}"
|
|
1152
|
+
exit 1
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
when "remove", "rm", "delete"
|
|
1156
|
+
id = ARGV[0]
|
|
1157
|
+
unless id
|
|
1158
|
+
puts "Usage: zillacore cron remove <job-id>"
|
|
1159
|
+
exit 1
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
uri = URI("#{server_url}/api/cron/remove")
|
|
1163
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
1164
|
+
req.body = { id: id }.to_json
|
|
1165
|
+
|
|
1166
|
+
begin
|
|
1167
|
+
response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
|
|
1168
|
+
data = JSON.parse(response.body)
|
|
1169
|
+
if data["success"]
|
|
1170
|
+
puts "✓ Removed cron job: #{id}"
|
|
1171
|
+
else
|
|
1172
|
+
puts "Error: #{data["error"]}"
|
|
1173
|
+
exit 1
|
|
1174
|
+
end
|
|
1175
|
+
rescue StandardError => e
|
|
1176
|
+
puts "Could not reach server: #{e.message}"
|
|
1177
|
+
exit 1
|
|
1178
|
+
end
|
|
1179
|
+
|
|
1180
|
+
when "enable"
|
|
1181
|
+
id = ARGV[0]
|
|
1182
|
+
unless id
|
|
1183
|
+
puts "Usage: zillacore cron enable <job-id>"
|
|
1184
|
+
exit 1
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1187
|
+
uri = URI("#{server_url}/api/cron/toggle")
|
|
1188
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
1189
|
+
req.body = { id: id, enabled: true }.to_json
|
|
1190
|
+
|
|
1191
|
+
begin
|
|
1192
|
+
response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
|
|
1193
|
+
data = JSON.parse(response.body)
|
|
1194
|
+
if data["success"]
|
|
1195
|
+
puts "✓ Enabled cron job: #{id}"
|
|
1196
|
+
else
|
|
1197
|
+
puts "Error: #{data["error"]}"
|
|
1198
|
+
exit 1
|
|
1199
|
+
end
|
|
1200
|
+
rescue StandardError => e
|
|
1201
|
+
puts "Could not reach server: #{e.message}"
|
|
1202
|
+
exit 1
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
when "disable"
|
|
1206
|
+
id = ARGV[0]
|
|
1207
|
+
unless id
|
|
1208
|
+
puts "Usage: zillacore cron disable <job-id>"
|
|
1209
|
+
exit 1
|
|
1210
|
+
end
|
|
1211
|
+
|
|
1212
|
+
uri = URI("#{server_url}/api/cron/toggle")
|
|
1213
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
1214
|
+
req.body = { id: id, enabled: false }.to_json
|
|
1215
|
+
|
|
1216
|
+
begin
|
|
1217
|
+
response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
|
|
1218
|
+
data = JSON.parse(response.body)
|
|
1219
|
+
if data["success"]
|
|
1220
|
+
puts "✓ Disabled cron job: #{id}"
|
|
1221
|
+
else
|
|
1222
|
+
puts "Error: #{data["error"]}"
|
|
1223
|
+
exit 1
|
|
1224
|
+
end
|
|
1225
|
+
rescue StandardError => e
|
|
1226
|
+
puts "Could not reach server: #{e.message}"
|
|
1227
|
+
exit 1
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1230
|
+
when "update"
|
|
1231
|
+
id = ARGV[0]
|
|
1232
|
+
schedule = nil
|
|
1233
|
+
discord_channel_id = nil
|
|
1234
|
+
forum_title = nil
|
|
1235
|
+
|
|
1236
|
+
OptionParser.new do |opts|
|
|
1237
|
+
opts.banner = "Usage: zillacore cron update <job-id> [options]"
|
|
1238
|
+
opts.on("-s", "--schedule EXPR", "New cron expression") { |v| schedule = v }
|
|
1239
|
+
opts.on("-c", "--channel CHANNEL_ID", "New Discord channel ID") { |v| discord_channel_id = v }
|
|
1240
|
+
opts.on("-t", "--title TITLE", "New forum post title") { |v| forum_title = v }
|
|
1241
|
+
end.parse!
|
|
1242
|
+
|
|
1243
|
+
unless id && (schedule || discord_channel_id || forum_title)
|
|
1244
|
+
puts "Error: Missing required arguments"
|
|
1245
|
+
puts "Usage: zillacore cron update <job-id> -s \"42 13 * * 1-5\""
|
|
1246
|
+
puts " or: zillacore cron update <job-id> -c \"1423854179880927274\""
|
|
1247
|
+
puts " or: zillacore cron update <job-id> -t \"Daily Update\""
|
|
1248
|
+
puts " or: zillacore cron update <job-id> -s \"42 13 * * 1-5\" -c \"1423854179880927274\""
|
|
1249
|
+
exit 1
|
|
1250
|
+
end
|
|
1251
|
+
|
|
1252
|
+
uri = URI("#{server_url}/api/cron/update")
|
|
1253
|
+
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
|
|
1254
|
+
payload = { id: id }
|
|
1255
|
+
payload[:schedule] = schedule if schedule
|
|
1256
|
+
payload[:discord_channel_id] = discord_channel_id if discord_channel_id
|
|
1257
|
+
payload[:forum_title] = forum_title if forum_title
|
|
1258
|
+
req.body = payload.to_json
|
|
1259
|
+
|
|
1260
|
+
begin
|
|
1261
|
+
response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(req) }
|
|
1262
|
+
data = JSON.parse(response.body)
|
|
1263
|
+
if data["success"]
|
|
1264
|
+
puts "✓ Updated cron job: #{id}"
|
|
1265
|
+
puts " New schedule: #{schedule}" if schedule
|
|
1266
|
+
puts " New Discord channel: #{discord_channel_id}" if discord_channel_id
|
|
1267
|
+
puts " New forum title: #{forum_title}" if forum_title
|
|
1268
|
+
else
|
|
1269
|
+
puts "Error: #{data["error"]}"
|
|
1270
|
+
exit 1
|
|
1271
|
+
end
|
|
1272
|
+
rescue StandardError => e
|
|
1273
|
+
puts "Could not reach server: #{e.message}"
|
|
1274
|
+
exit 1
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
else
|
|
1278
|
+
puts <<~HELP
|
|
1279
|
+
Usage: zillacore cron <command>
|
|
1280
|
+
|
|
1281
|
+
Commands:
|
|
1282
|
+
add -s <schedule> -p <project> [-a <agent>] "<prompt>"
|
|
1283
|
+
Add a new cron job
|
|
1284
|
+
list List all cron jobs
|
|
1285
|
+
remove <job-id> Remove a cron job
|
|
1286
|
+
enable <job-id> Enable a cron job
|
|
1287
|
+
disable <job-id> Disable a cron job
|
|
1288
|
+
update <job-id> [-s <schedule>] [-c <channel>]
|
|
1289
|
+
Update a cron job's schedule and/or Discord channel
|
|
1290
|
+
|
|
1291
|
+
Schedule format:
|
|
1292
|
+
Cron expression: "minute hour day month weekday"
|
|
1293
|
+
Examples:
|
|
1294
|
+
"0 9 * * 1-5" — 9am on weekdays
|
|
1295
|
+
"0 */4 * * *" — Every 4 hours
|
|
1296
|
+
"30 17 * * *" — 5:30pm daily
|
|
1297
|
+
|
|
1298
|
+
Special strings:
|
|
1299
|
+
@hourly — Every hour (minute 0)
|
|
1300
|
+
@daily — Every day at midnight
|
|
1301
|
+
@weekly — Every Sunday at midnight
|
|
1302
|
+
@monthly — First day of month at midnight
|
|
1303
|
+
|
|
1304
|
+
One-time (natural language):
|
|
1305
|
+
"tomorrow at 9am"
|
|
1306
|
+
"in 2 hours"
|
|
1307
|
+
"next monday at 3pm"
|
|
1308
|
+
#{" "}
|
|
1309
|
+
One-time (ISO8601):
|
|
1310
|
+
"2026-02-27T09:00:00-05:00"
|
|
1311
|
+
|
|
1312
|
+
Examples:
|
|
1313
|
+
# Recurring
|
|
1314
|
+
zillacore cron add -s "0 9 * * *" -p marketplace "Summarize yesterday's work"
|
|
1315
|
+
zillacore cron add -s "@daily" -p zillacore -a GLaDOS "Run security audit"
|
|
1316
|
+
#{" "}
|
|
1317
|
+
# One-time
|
|
1318
|
+
zillacore cron add -s "tomorrow at 9am" -p marketplace "Reminder about priorities"
|
|
1319
|
+
zillacore cron add -s "in 30 minutes" -p zillacore "Follow up on PR"
|
|
1320
|
+
#{" "}
|
|
1321
|
+
# Management
|
|
1322
|
+
zillacore cron list
|
|
1323
|
+
zillacore cron update post-morning-message -s "42 13 * * 1-5"
|
|
1324
|
+
zillacore cron disable daily-summary
|
|
1325
|
+
zillacore cron remove daily-summary
|
|
1326
|
+
|
|
1327
|
+
Config file: #{ZILLACORE_DIR}/cron.json
|
|
1328
|
+
HELP
|
|
1329
|
+
end
|
|
1330
|
+
|
|
1331
|
+
when "provider"
|
|
1332
|
+
provider_cmd = ARGV.shift
|
|
1333
|
+
providers_dir = File.join(ZILLACORE_DIR, "cli-providers")
|
|
1334
|
+
|
|
1335
|
+
case provider_cmd
|
|
1336
|
+
when "list", "ls"
|
|
1337
|
+
unless Dir.exist?(providers_dir)
|
|
1338
|
+
puts "No providers configured. Run 'zillacore provider add <name>' to create one."
|
|
1339
|
+
exit 0
|
|
1340
|
+
end
|
|
1341
|
+
files = Dir.glob(File.join(providers_dir, "*.json"))
|
|
1342
|
+
if files.empty?
|
|
1343
|
+
puts "No providers configured."
|
|
1344
|
+
else
|
|
1345
|
+
files.each do |f|
|
|
1346
|
+
name = File.basename(f, ".json")
|
|
1347
|
+
data = JSON.parse(File.read(f))
|
|
1348
|
+
puts "#{name} — #{data["binary"]} (#{data["models"]&.size || 0} models)"
|
|
1349
|
+
end
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
when "show"
|
|
1353
|
+
name = ARGV[0]
|
|
1354
|
+
unless name
|
|
1355
|
+
puts "Usage: zillacore provider show <name>"
|
|
1356
|
+
exit 1
|
|
1357
|
+
end
|
|
1358
|
+
file = File.join(providers_dir, "#{name}.json")
|
|
1359
|
+
unless File.exist?(file)
|
|
1360
|
+
puts "Provider '#{name}' not found."
|
|
1361
|
+
exit 1
|
|
1362
|
+
end
|
|
1363
|
+
puts JSON.pretty_generate(JSON.parse(File.read(file)))
|
|
1364
|
+
|
|
1365
|
+
when "add", "create"
|
|
1366
|
+
name = ARGV[0]
|
|
1367
|
+
unless name
|
|
1368
|
+
puts "Usage: zillacore provider add <name>"
|
|
1369
|
+
exit 1
|
|
1370
|
+
end
|
|
1371
|
+
FileUtils.mkdir_p(providers_dir)
|
|
1372
|
+
file = File.join(providers_dir, "#{name}.json")
|
|
1373
|
+
if File.exist?(file)
|
|
1374
|
+
puts "Provider '#{name}' already exists. Edit #{file} directly."
|
|
1375
|
+
exit 1
|
|
1376
|
+
end
|
|
1377
|
+
scaffold = {
|
|
1378
|
+
"binary" => name,
|
|
1379
|
+
"default_args" => "",
|
|
1380
|
+
"model_flag" => "--model",
|
|
1381
|
+
"list_models_command" => "",
|
|
1382
|
+
"models" => {}
|
|
1383
|
+
}
|
|
1384
|
+
File.write(file, JSON.pretty_generate(scaffold))
|
|
1385
|
+
puts "Created #{file} — edit it to configure the provider."
|
|
1386
|
+
|
|
1387
|
+
else
|
|
1388
|
+
puts <<~HELP
|
|
1389
|
+
Usage: zillacore provider <command>
|
|
1390
|
+
|
|
1391
|
+
Commands:
|
|
1392
|
+
list List configured CLI providers
|
|
1393
|
+
show <name> Show provider configuration
|
|
1394
|
+
add <name> Create a new provider config
|
|
1395
|
+
HELP
|
|
1396
|
+
end
|
|
1397
|
+
|
|
1398
|
+
when "version", "--version", "-v"
|
|
1399
|
+
puts "zillacore #{ZILLACORE_VERSION}"
|
|
1400
|
+
|
|
1401
|
+
when "help", "--help", "-h", nil
|
|
1402
|
+
puts <<~HELP
|
|
1403
|
+
ZillaCore CLI - Manage projects with ZillaCore server
|
|
1404
|
+
|
|
1405
|
+
Usage:
|
|
1406
|
+
zillacore setup Bootstrap a new machine (deps, dirs, configs)
|
|
1407
|
+
zillacore server [options] Start the ZillaCore webhook server (foreground)
|
|
1408
|
+
zillacore stop Stop the running server
|
|
1409
|
+
zillacore restart Restart the server
|
|
1410
|
+
zillacore logs Tail the server log
|
|
1411
|
+
zillacore status Check if server is running
|
|
1412
|
+
zillacore register [options] Register current directory as a project
|
|
1413
|
+
zillacore unregister <key> Unregister a project
|
|
1414
|
+
zillacore list List all registered projects
|
|
1415
|
+
zillacore projects list List all registered projects (alias)
|
|
1416
|
+
zillacore projects default <key> Set the default project
|
|
1417
|
+
zillacore show <key> Show project configuration
|
|
1418
|
+
zillacore brain <command> Manage agent long-term memory (brain)
|
|
1419
|
+
zillacore discord <command> Manage the Discord bot
|
|
1420
|
+
zillacore cron <command> Manage scheduled agent tasks
|
|
1421
|
+
zillacore provider <command> Manage CLI providers
|
|
1422
|
+
zillacore config Configure ZillaCore CLI
|
|
1423
|
+
zillacore path Show ZillaCore config directory
|
|
1424
|
+
zillacore version Show version
|
|
1425
|
+
zillacore help Show this help message
|
|
1426
|
+
|
|
1427
|
+
Server Options:
|
|
1428
|
+
-d, --daemon Run server in background (detached)
|
|
1429
|
+
|
|
1430
|
+
Card Map Commands:
|
|
1431
|
+
card-map list Show all card map entries
|
|
1432
|
+
card-map clean Remove stale entries (missing worktrees)
|
|
1433
|
+
|
|
1434
|
+
Brain Commands:
|
|
1435
|
+
brain init [agent] Initialize brain (knowledge + persona)
|
|
1436
|
+
brain status [agent] Show brain status and files
|
|
1437
|
+
brain search [--persona] <q> Search knowledge or persona
|
|
1438
|
+
brain list List knowledge and all personas
|
|
1439
|
+
brain path Show knowledge directory
|
|
1440
|
+
|
|
1441
|
+
Cron Commands:
|
|
1442
|
+
cron add -s <schedule> -p <project> "<prompt>"
|
|
1443
|
+
Add a scheduled task
|
|
1444
|
+
cron list List all cron jobs
|
|
1445
|
+
cron remove <job-id> Remove a cron job
|
|
1446
|
+
cron enable <job-id> Enable a cron job
|
|
1447
|
+
cron disable <job-id> Disable a cron job
|
|
1448
|
+
cron update <job-id> -s <schedule>
|
|
1449
|
+
Update a cron job's schedule
|
|
1450
|
+
|
|
1451
|
+
Discord Commands:
|
|
1452
|
+
discord config Show Discord config
|
|
1453
|
+
discord default <proj> Set default project for all channels
|
|
1454
|
+
discord map <ch> <proj> Map a channel to a project
|
|
1455
|
+
discord owner [<id>] Set/show machine owner (version notifications)
|
|
1456
|
+
discord token <agent> <token> Set Discord bot token for an agent
|
|
1457
|
+
discord agents List agents with Discord bot tokens
|
|
1458
|
+
discord status Check bot status via server API
|
|
1459
|
+
|
|
1460
|
+
Provider Commands:
|
|
1461
|
+
provider list List configured CLI providers
|
|
1462
|
+
provider show <name> Show provider configuration
|
|
1463
|
+
provider add <name> Create a new provider config
|
|
1464
|
+
|
|
1465
|
+
Examples:
|
|
1466
|
+
# Start the server in the foreground (like rails server)
|
|
1467
|
+
zillacore server
|
|
1468
|
+
zillacore s
|
|
1469
|
+
#{" "}
|
|
1470
|
+
# Start the server in background (detached)
|
|
1471
|
+
zillacore server --daemon
|
|
1472
|
+
zillacore server -d
|
|
1473
|
+
#{" "}
|
|
1474
|
+
# Tail the server logs
|
|
1475
|
+
zillacore logs
|
|
1476
|
+
#{" "}
|
|
1477
|
+
# Restart the server
|
|
1478
|
+
zillacore restart
|
|
1479
|
+
#{" "}
|
|
1480
|
+
# Stop the server
|
|
1481
|
+
zillacore stop
|
|
1482
|
+
#{" "}
|
|
1483
|
+
# Check server status
|
|
1484
|
+
zillacore status
|
|
1485
|
+
#{" "}
|
|
1486
|
+
# Register current directory
|
|
1487
|
+
cd ~/Code/my-project && zillacore register
|
|
1488
|
+
#{" "}
|
|
1489
|
+
# Register with specific path
|
|
1490
|
+
zillacore register --path ~/Code/my-project
|
|
1491
|
+
#{" "}
|
|
1492
|
+
# List all projects
|
|
1493
|
+
zillacore list
|
|
1494
|
+
zillacore projects list # alternative syntax
|
|
1495
|
+
#{" "}
|
|
1496
|
+
# Set default project
|
|
1497
|
+
zillacore projects default my-project
|
|
1498
|
+
#{" "}
|
|
1499
|
+
# Unregister a project
|
|
1500
|
+
zillacore unregister my-project
|
|
1501
|
+
#{" "}
|
|
1502
|
+
# Show project details
|
|
1503
|
+
zillacore show my-project
|
|
1504
|
+
#{" "}
|
|
1505
|
+
# Initialize brain for your agent
|
|
1506
|
+
zillacore brain init Galen
|
|
1507
|
+
#{" "}
|
|
1508
|
+
# Search the brain
|
|
1509
|
+
zillacore brain search "ruby conventions"
|
|
1510
|
+
|
|
1511
|
+
Configuration:
|
|
1512
|
+
Projects are stored in: #{ZILLACORE_DIR}/projects.json
|
|
1513
|
+
Server config: #{ZILLACORE_DIR}/config.json
|
|
1514
|
+
Brain storage: #{ZILLACORE_DIR}/brain/
|
|
1515
|
+
HELP
|
|
1516
|
+
|
|
1517
|
+
else
|
|
1518
|
+
puts "Unknown command: #{subcommand}"
|
|
1519
|
+
puts "Run 'zillacore help' for usage information."
|
|
1520
|
+
exit 1
|
|
1521
|
+
end
|